到目前为止,本书介绍了多种针对特定目标设计的算法。本章讨论的算法略有不同:它们旨在在图上引入随机行为。通过分析图上的随机运动,我们可以对具有非确定性行为的系统建模和研究,例如随机网络路由或现实世界中的社交互动。
图上的随机游走具有丰富的数学历史,其深度远超本书的范围。本章将概述随机游走,介绍如何使用马尔可夫链进行分析,并提供在图上实现随机游走的代码示例。我们会讨论可以用随机游走研究的问题类型和可以建模的系统,例如赌博和基于运气的棋盘游戏。最后,我们会探讨如何通过样本观察重建底层图结构。
引入随机游走
图上的随机游走是一系列节点,其中序列中的下一个节点是根据某种概率分布随机选择的。我们将从节点 uu 到节点 vv 的转移概率表示为:
这意味着每当我们处于节点 uu 时,我们会根据给定的概率分布从 uu 的邻居中选择下一个节点。
我们可以把随机游走形象化为一个绝不提前规划的游客。他坚信偶然性才能带来最佳假期,因此没有地图,也不知去向,就开始探索城市。到达一个路口时,他会考虑可能的路线,并随机选择一条。游客每次决策都是孤立的,不考虑过去或未来的选择。
我们利用图的结构来限制概率,有以下几种方式:
- 限制移动范围:仅允许移动到与当前节点通过边相连的节点。在有向图中,这必须是正确方向的边:
换句话说,除非有道路连接,否则游客无法从节点 uu 到达节点 vv。为了清晰起见,本章还要求如果边存在,则概率必须大于零:
这意味着理论上,游客可以遍历城市中的所有道路。
- 概率总和必须为 1:对于所有出边节点,转移概率之和必须为 1:
这保证了概率形成一个有效的分布。每个节点至少有一条出边。在有向图中,我们可以使用自环 p(u→u)>0p(u \to u) > 0 来模拟随机游走停留在当前节点的情况。这相当于保证游客总有至少一条路径可以前进,即使这条路会回到当前位置。
随机游走中的概率
最简单的随机游走是对所有出边采用等概率选择。在这种情况下,我们的游客会完全随机地选择当前路口的街道。如果一个路口有两条出边,每条的概率为 50%;如果有四条出边,每条的概率为 25%。图 13-1 展示了一个无向无权图(a)以及每个节点对应的转移概率(b)。
虽然我们讨论的是有向图和无向图上的随机游走,但我们总是将这些系统建模为有向图,因为在许多情况下,两个节点之间的转移概率并不对称。用正式的术语表示为:
例如,在图 13-1(b) 中,从节点 0 移动到节点 1 的概率是 1/2,而反方向移动的概率仅为 1/3。对于我们的漫游游客来说,从一个路口到另一个路口的概率取决于当前路口分出的道路数量。
我们可以使用加权图来建模更现实的场景,通过给每条边分配不同的概率,并将其存储在边权中。我们约束这些概率(即边权),使得所有出边的概率之和等于 1.0。图 13-2 展示了一个有向加权图的示例。在节点 3 上的随机游走有三种可能的下一状态:它可以以 0.2 的概率移动到节点 1,以 0.6 的概率移动到节点 2,或者通过自环以 0.2 的概率停留在节点 3。
这些更一般化的图对应于一种情境:游客的行为不仅受道路数量影响,还受到概率性因素的驱动。他们可能会倾向于前往建筑更有特色的区域,或者跟随咖啡的香味。当到达某个四叉路口时,他们竟有 90% 的概率选择向左,走向咖啡馆街道。
将随机游走视为马尔可夫链
图上的随机游走是马尔可夫链(或马尔可夫模型)的一个例子,这类系统的特点是:下一状态的概率仅依赖于当前状态。每一步的决策都不考虑之前的路径,这种性质称为时间不变性。我们引入马尔可夫链的概念,是为了借助其丰富的分析方法。研究不同类型马尔可夫链的学术成果远超本书范围。本章仅简要介绍几个分析随机游走的概念和术语,并展示其建模能力。
时间不变性对应于我们漫游游客的行为习惯:只考虑面前开放的路径,不考虑已走过的路、走了多少步,甚至不关心时间。这种方式虽然不适合常规活动(如吃饭和睡觉),但游客坚定遵循他们的随机化度假原则。
在概率与统计文献中,这些转移概率常表示为 ,意思是:在时间步 系统处于状态 的情况下,时间步 处于状态 的概率。结合图的表示法,我们得到:
由于每次转移相互独立,我们可以通过乘以每次转移的概率来计算从固定起点 v0v_0 出发的整个路径 的概率:
马尔可夫链适用于各种转移独立于过去路径的任务。人工智能使用更强大的马尔可夫模型来模拟或推理真实现象,例如语音理解或自主智能体决策。例如,**隐马尔可夫模型(HMM)**是机器学习中常用的模型,通过在不可见状态间随机转移,每个状态产生噪声输出。存在高效算法可以从输出估计潜在状态,甚至从样本数据学习转移概率和输出分布。
相比之下,本章考虑的随机游走代表一种特别简单的马尔可夫模型:当前状态(节点)是可见的,决策完全随机。然而,即便是这种简单模型,也能提供丰富的模拟和分析能力。
转移概率
在使用边权建模图上的随机游走时,我们要求每个节点的出边权重构成一个有效的概率分布。可以通过遍历每个节点并检查其出边权重之和是否为 1.0 来测试概率图是否有效,如清单 13-1 所示。
def is_valid_probability_graph(g: Graph) -> bool:
for node in g.nodes:
❶ edge_list: list = node.get_edge_list()
if len(edge_list) == 0:
return False
total: float = 0.0
for edge in edge_list:
❷ if edge.weight < 0.0 or edge.weight > 1.0:
return False
total += edge.weight
❸ if abs(total - 1.0) > 1e-10:
return False
return True
清单 13-1:检查边权中的概率有效性
代码通过 for 循环遍历每个节点,检查其出边权重是否形成有效概率分布。首先,提取节点的边列表并检查是否为空 ❶。如果为空,表示该节点无出路,返回 False。节点出边概率之和等于 1 的约束要求每个节点至少有一条非零权重的出边,即使是自环。
第二个 for 循环遍历出边,检查每条边的权重是否在 0.0 到 1.0 之间,若不符合立即返回 False ❷,并将当前边权累加到 total。检查完所有边后,验证总权重是否为 1.0,允许小量浮点误差 ❸。只有当所有节点和边都满足条件时,返回 True。
矩阵表示
图的矩阵表示在分析图上随机游走的性质时非常有用,也常用于统计学和机器学习教材中。矩阵表示中,转移概率通常用转移矩阵 MM 表示,其中第 ii 行第 jj 列的值对应于在节点 ii 时移动到节点 jj 的概率:
M[i][j]=p(i→j)M[i][j] = p(i \to j)
我们甚至可以重用第 1 章的 GraphMatrix 数据结构来存储这些值。由于矩阵元素是概率,我们对 GraphMatrix 的连接列表施加如下约束:
0≤connections[i][j]≤1∀i,j0 \le \text{connections}[i][j] \le 1 \quad \forall i,j ∑jconnections[i][j]=1∀i\sum_j \text{connections}[i][j] = 1 \quad \forall i
这些约束与之前对边权的限制一致。
我们可以用矩阵运算模拟随机游走的单步效果。令 为概率向量,其中 表示在时间步 随机游走位于节点 的概率( 且 )。例如, 表示图 13-2 四个节点上随机游走的概率分布:节点 0 为 50%,节点 1 为 40%,节点 2 为 0%,节点 3 为 10%。
向量 给出随机游走的起始概率。例如, 表示确定性从节点 0 出发,而 表示有相等概率从节点 0 或节点 1 出发。向量 表示根据 随机开始游走并进行一次随机步后的节点概率分布; 表示两步后的分布,以此类推。
利用转移矩阵 MM 可以计算后续概率分布:
每个元素 表示随机游走在下一步 位于节点 的概率。我们可以在 GraphMatrix 类中添加方法来执行此计算,如清单 13-2 所示。
def simulate_random_step(self, Vt: list) -> list:
if len(Vt) != self.num_nodes:
raise ValueError("Incorrect length of probability dist")
Vnext: list = [0.0] * self.num_nodes
for i in range(self.num_nodes):
for j in range(self.num_nodes):
Vnext[j] += Vt[i] * self.connections[i][j]
return Vnext
清单 13-2:模拟图上随机游走的单步
代码首先检查输入向量 长度是否正确,否则抛出异常。然后创建结果向量 ,用双层嵌套循环进行计算,并返回新的概率向量。
注意
如第 1 章所述,本书示例用列表嵌套表示矩阵以便说明。实际生产代码应使用支持高效矩阵运算的库(如 numpy)以提升性能。
从确定性状态也可以进行随机步模拟:令 对应唯一节点 。然后通过 可得到从 出发一步后的概率分布。重复此过程可得两步概率分布:
或者,我们可以将矩阵表示扩展为 ,表示恰好 步的转移矩阵,即 为从 到 在 步内的概率,可通过矩阵乘法直接计算:
虽然矩阵表示有助于描述和分析随机游走性质,但本章后续代码仍使用 Graph 类的邻接表表示,以与其他章节保持一致。所有函数均可适配 GraphMatrix 类。
使用场景
随机游走可用于建模和分析涉及非确定性行为的问题。现实世界中,随机行为广泛存在——从人际互动到某些计算机算法中的显式随机性。在本节中,我们介绍三个示例场景:社交网络、随机探索和机会游戏。
社交网络中的信息链
我们可以用随机游走来建模谣言在社交网络中的传播,这类互动具有随机成分。每个人都决定不过度传播,当他们听到一个谣言时,只将信息传递给一个人。然而,他们迫不及待地想分享最新的八卦,所以会将消息传递给最先碰到的朋友。这本质上是一种概率选择,因为他们不知道会先遇到哪个朋友。分享完消息后,他们暂时满足,不会继续讨论,直到再次接收到谣言。
可以将社交网络建模为图,边表示将谣言分享给某邻居的概率。谣言在图上以随机游走的方式,从一个人传到另一个人。
探索
前几章讨论了多种确定性探索图的算法,如深度优先搜索或 A* 搜索。然而,现实中很多探索任务包含随机因素,比如因天气导致的路径封闭。随机游走允许我们建模这种带约束的系统。
以第 8 章的探险者为例,他们寻找通往考古遗址的最佳路径。当前条件可能给探索增添随机性:当面对分叉路时,北路可能有 50% 的概率被洪水阻断,而南路可能有 10% 的概率被一群愤怒的黄蜂封路。考虑这些概率,我们可以将他们在丛林中的不愉快旅程建模为一次随机游走。
类似地,我们可以用随机游走分析动态环境下的机器人路径规划。搜索与救援机器人在损坏建筑中探索时,可能会遇到不同障碍(如火灾或被淹没的通道),需要重新规划路线。
机会游戏
随机游走也可以用来模拟机会游戏的结果。图的节点表示不同的游戏状态,边表示状态可能发生的(概率性)变化。例如,我们可以用图 13-3 中的马尔可夫链表示一名玩家玩一美元老虎机的情况。
每个状态表示赌徒所拥有的美元数量。每次拉动老虎机都决定下一个状态,而不考虑之前的拉动或赌徒当前的财务状况。比如,机器有 1/100 的概率支付 10 美元,使赌徒从状态 k 变为 k + 9;有 99/100 的概率不支付任何金额,使赌徒从状态 k 变为 k – 1。
当我们建模更复杂的机会游戏时,需要相应更复杂的图。在本章后面,我们将展示如何使用图来建模基于运气的棋盘游戏。我们会讨论同时表示多个玩家状态的节点,以及它们之间的状态转换。
随机游走的模拟
理解随机游走及其底层图的一种强有力方法是重复模拟随机游走,并分析所经过的路径。我们在图上模拟随机游走的方法是:基于当前状态邻居的概率分布,重复选择下一个状态。作为前提,我们需要一个函数,从有限选项集中按照预设概率进行采样。
这里给出一个简单算法用于演示:在 [0, 1) 范围内均匀生成一个随机数,然后迭代每条出边,累加前面节点的累计概率,判断随机数对应哪个邻居。实际上,我们是将 [0, 1) 范围划分为若干区域,每个选项对应的区域大小与其概率相等。图 13-4 展示了一个示例:50% 的范围对应节点 0,20% 对应节点 1,30% 对应节点 3。
在迭代选项时,通过追踪迄今为止的累计概率,我们就记录了每个区域的起点和终点,并与随机选择的值进行比较。我们要找到覆盖该随机值的“区间”。一旦累计值超过随机选择的值,我们就知道已经越过了对应的区间边界。
在图上进行随机游走的代码包含随机数生成,以及一个遍历出边的 for 循环,如清单 13-3 所示。
def choose_next_node(current: Node) -> int:
❶ prob: float = random.random()
cumulative: float = 0.0
edge_list: list = current.get_edge_list()
for edge in edge_list:
cumulative += edge.weight
❷ if cumulative >= prob:
return edge.to_node
❸ return edge_list[-1].to_node
清单 13-3:随机游走中选择下一个节点
代码首先使用 Python 的 random 库从 [0, 1) 中生成一个随机数 ❶。然后通过 for 循环遍历每条出边,计算当前累计概率。被选择的边是第一个使累计概率超过随机数的边 ❷。如果循环到列表末尾仍未找到(可能因浮点数存储精度问题),则直接返回最后一条边 ❸。
清单 13-3 中的 choose_next_node() 函数不会对每个节点的概率分布进行有效性检查,这是有意为之,以避免每次调用都进行检查的开销。推荐使用清单 13-1 中的 is_valid_probability_graph() 函数一次性检查所有节点的概率分布。
借助该辅助函数,从给定起点 start 执行随机游走的代码如下:
def random_walk(g: Graph, start: int, steps: int) -> list:
❶ if not is_valid_probability_graph(g):
raise ValueError("Graph weights are not probabilities.")
walk: list = [-1] * steps
current: int = start
walk[0] = current
for i in range(1, steps):
❷ current = choose_next_node(g.nodes[current])
walk[i] = current
return walk
函数首先确认图的权重是否表示有效的概率分布,如无效则抛出异常 ❶。然后分配一个列表 walk 存储结果,将当前节点设为起点,并将游走的第一步设置为起点。接着使用 for 循环遍历每一步,调用清单 13-3 的 choose_next_node() 函数不断从当前节点选择下一个节点 ❷。每次选择的新节点会加入 walk 列表,待完成所有步骤后返回该列表。
统计度量
随机游走是理解随机系统并计算各种实际统计指标的强大工具。例如,我们可能想计算到达某个特定节点的概率,或者估算到达该节点需要多少步。这些指标对于回答诸如以下问题非常有用:“赌徒输光所有钱的概率是多少?”、“一条谣言传到我这里平均需要多久?”或者“如果游客随机游览这座城市多年,他们会在每个地点停留多久?”
在本节中,我们将简要探讨这些问题如何应用于我们的漫游游客案例,并概述如何计算答案。在考虑到达特定节点的概率及平均所需步数后,我们还将分析随机游走的长期行为。
首次到达时间与吸收时间
节点集合 的 首次到达时间(hitting time) 是指从给定起点出发,随机游走首次到达集合 中某个节点所需的平均步数。例如,如果 是游客探索的小镇中所有有咖啡馆的路口,他们可能希望计算该集合的首次到达时间,以了解自己下一次喝到咖啡的概率何时最大。
如果随机游走无法离开集合 中的节点,这些首次到达时间就称为 吸收时间(absorption time) ,因为游走会被“吸收”到集合 中。例如,在图 13-5 中,节点 构成了一个吸收集合。一旦游走到达节点 ,它就会永远停留在那里。
吸收节点也可以用来表示随机游走的终止。例如,游客可以决定在到达酒店时停止他们的随机游走。
在分析图上的随机游走时,我们通常会考虑与首次到达时间相关的两个统计量:游走到达子集的概率,以及达到该子集的期望时间。
首次到达概率与吸收概率
节点子集 的 首次到达概率(hitting probability) 是指从节点 出发的随机游走会到达该子集中的某个节点 的概率。我们可以用这个指标回答诸如“游客遇到咖啡馆的概率是多少?”之类的问题。
类似地,节点子集 的 吸收概率(absorption probability) 是指从节点 出发的随机游走会被该子集吸收的概率。这使我们能够回答诸如“游客返回酒店并停止随机游走的概率是多少?”的问题。
节点的吸收子集可能会影响非吸收节点的首次到达概率。例如,考虑图 13-6 所示的加权图,其中图中有三个节点,而 {0, 1} 构成了一个吸收子集。
从节点 2 出发的随机游走到达节点 0 的概率为 1.0(假设步数可能无限)。相反,一旦游走到达节点集合 ,它就永远无法到达节点 2,因为它将被困在与节点 0 和 1 相连的无限步数中。
期望首次到达时间
期望首次到达时间(expected hitting time)是指随机游走从起点出发,首次到达集合 中某个节点所需的平均步数。这可以帮助我们估算游客平均需要多久才能喝到下一杯咖啡。吸收时间类似,用于量化随机游走到达吸收集合所需的期望时间。对于游客来说,这就是他们平均要走多久,才能到达酒店并结束当天的游览。
例如,图 13-6 中从节点 0 到节点 1 的期望首次到达时间(记为 )计算如下:
代入具体概率:
最终计算结果为:
这是因为从节点 0 出发的可能路径包括 [0, 1]、[0, 0, 1]、[0, 0, 0, 1] 等。如果我们有完整的转移矩阵,可以通过一组方程求解期望首次到达时间。
期望首次到达时间也可能是无限的,比如图 13-6 中从节点 0 到节点 2 的 。无论考虑多长的游走,都无法从节点 0 到达节点 2。
平稳分布
平稳分布(stationary distribution)表示如果我们在一个强连通图上无限期地随机游走,每个状态被访问的概率分布。这个分布能让我们了解在长期游走中,每个节点被停留的可能性。例如,我们可以问:“如果游客在城市里随机游荡了好几天,他们现在出现在第五街的咖啡馆的概率是多少?”同样,平稳分布也可用于预测数百万遵循相同随机规则的游客在城市中的可能位置。
回到本章前面引入的矩阵表示法(其中 是转移矩阵, 是概率向量, 表示在时间步 随机游走在节点 的概率),平稳分布是一个向量 ,满足:
换句话说,再增加一步随机游走也不会改变可能位置的分布。
我们可以通过图的结构来推导平稳分布。考虑图 13-7 中的两节点图,其中:
图 13-7 中的图结构表明,从长期来看,随机游走在节点 1 的停留时间倾向于比节点 0 多。节点 0 由于自环停留的概率只有 0.25,而节点 1 停留的概率为 0.5。我们可以通过平稳分布来量化在每个节点的停留时间差,对于该图,平稳分布为 。
基于运气的棋盘游戏
我们可以将本节讨论的内容结合起来,考虑随机游走最有趣的应用之一:分析儿童的运气型棋盘游戏。这类游戏实际上不涉及策略选择,而是依赖于转盘或骰子生成的随机数来决定移动步数。在长时间旋转转盘来决定棋子前进一格、两格或三格之后,即使是非统计学背景的人也可能会好奇目标状态的期望吸收时间,并问:“我还要玩多久才能结束这局游戏?”我们可以通过将游戏建模为图上的随机游走来回答这些问题。
考虑一个简单的例子:游戏目标是成为第一个绕棋盘一圈完成的玩家。在每个回合中,每位玩家使用一个标有 1、2、3 的小转盘来决定前进的步数。图 13-8 展示了该游戏中几个状态的图形表示。节点编号对应棋盘上的格子,玩家当前位于格子 。根据随机转盘,他们可能前进到格子 ,每种情况的概率都是 1/3。
为了让游戏更刺激(或者可能只是为了让更多孩子因为挫败而哭泣),游戏设计者在某些格子上标注了“退回 X 格”。这些格子相当于陷阱,应尽量避免。我们可以将这种行为直接纳入图模型中。
图 13-9 展示了状态图,其中格子 上写着“退回一格”。这实际上相当于将节点 从图中移除(在图中用灰色节点表示)。在该状态下无法完成回合。相应地,相邻节点的转移概率也发生了变化。节点 到 的概率从 1/3 增加到 2/3,因为现在转盘出现 1 或 2 都会落在格子 。同样地,从节点 出现转盘 1 时,玩家会回到节点 ,我们将其建模为一个概率为 1/3 的自环。
为了捕捉基于格子和转盘的移动,我们可以增加更多的边;但如果想建模玩家间的互动,就需要显著增加复杂度。本节中展示的图模型仅捕捉了单个玩家在棋盘上的随机行走动态。如果所有玩家相互独立,这种建模是足够的。然而,如果游戏规则允许某玩家落在别人所在格子时,将其击退两格,我们就需要同时考虑两位玩家位置的模型。
我们可以通过增加节点数量来构建这样的模型。比如,不再用 N 个节点表示 N 个格子,而是用 个节点,将游戏状态建模为一个三元组:Alice 的位置(玩家 1 的状态)、Bob 的位置(玩家 2 的状态)、以及一个布尔值表示当前轮到谁。例如,三元组 表示 Alice 在格子 5,Bob 在格子 4,且轮到 Bob。如果结合三选一转盘和“击退”规则,Bob 有 1/3 的概率掷出 1,前进一步并将 Alice 击退两格。更正式地表示为 。
转移概率
除了分析给定的图,我们还可以将本章的概念扩展到从观测数据中估计图本身。假设我们找到了一位游客过去一年随机游走未知城市的日志,每条记录至少包含位置和时间。为了了解他们的旅程,我们可以考虑他们访问地点的长序列。
我们可以通过位置名称轻松重建单个节点,比如 Integer Square 和 Floating Point Harbor。稍加推理,还能识别出边的存在,例如 Integer Square 到 If-Then Intersection 的转移暗示二者之间存在一条边。经过对游客曲折路径的研究后,我们尝试重建其转移矩阵。
最大似然估计
我们可以使用一次或多次随机行走得到的观测序列来统计估计转移概率。最大似然估计(MLE)允许我们找到使观测数据出现概率最大的模型参数(转移概率)。简言之,最大似然估计利用每一步的独立性,通过计数两个量来计算从节点 到节点 的转移概率:
- :数据中节点 出现在移动起点的次数(不是路径中的最后节点)
- :数据中节点 紧跟在节点 之后出现的次数
计算转移概率的方法为:
例如,如果从节点 1 出发 100 次,其中 30 次直接移动到节点 3,则估计 。
转移矩阵估计算法
从观测数据估计底层图的算法包含三个阶段:
- 节点统计:遍历所有随机行走的路径,确定图中节点数量(扫描日志并确定需要追踪多少交叉口)
- 计数构建:构建每个节点被访问次数的数组(NuN_u)和节点间转移次数矩阵(Nu→vN_{u→v}),通过遍历每一步统计
- 图构建:对于所有非零转移,插入边并计算转移概率
在构建图时,只包含至少访问过一次的节点。如果图是断开的,则仅捕捉从初始节点可达的部分。比如游客害怕过桥,可能无法到达河的另一边。
代码示例(列表 13-4):
def estimate_graph_from_random_walks(walks: list) -> Graph:
num_nodes: int = 0
❶ for path in walks:
for node in path:
if node >= num_nodes:
num_nodes = node + 1
counts: list = [0.0] * num_nodes
move_counts: list = [[0.0] * num_nodes for _ in range(num_nodes)]
❷ for path in walks:
for i in range(0, len(path) - 1):
counts[path[i]] += 1.0
move_counts[path[i]][path[i + 1]] += 1.0
g: Graph = Graph(num_nodes)
❸ for i in range(num_nodes):
if counts[i] > 0.0:
for j in range(num_nodes):
❹ if move_counts[i][j] > 0.0:
g.insert_edge(i, j, move_counts[i][j] / counts[i])
return g
代码说明:
- 输入
walks为多次随机行走的节点列表 - ❶ 遍历每条路径和节点,记录出现的最大节点索引
- ❷ 分配统计数组并计算访问次数及节点间转移次数
- ❸ 创建图并遍历节点对
- ❹ 对非零转移计算概率并插入边
注意,如果基础图包含多个不连通的部分,使用最大索引法可能会创建永远不会访问的节点。它们的计数为零,不会生成出边,因此最终图无法通过 is_valid_probability_graph() 检查。列表 13-4 为简便起见采用此方法,但更稳健的做法是使用节点名称到索引的映射(见附录 A)。
示例
假设游客日志包含两条路径:
path1 = [0, 1, 0, 0, 1, 0, 0]
path2 = [0, 1, 0, 1, 0, 0, 0]
g = estimate_graph_from_random_walks([path1, path2])
观察路径可知:
- 游客总是从节点 0 开始
- 仅访问了两个建筑,0 和 1(0 是带咖啡厅的酒店,1 是街对面的咖啡店)
计算最大似然估计统计值:
转移概率估计为:
这些概率就是估计图的边权,如图 13-10 所示。注意,边 (1, 1) 因权重为 0 而未包括在内。
在酒店时,游客有 50% 的概率留在酒店,50% 的概率穿过街道去咖啡店。然而,当在咖啡店时,他们总是直接返回酒店。
使用有限数据的局限性
当然,在累积了足够多的观测之前,我们对转移概率的估计可能非常嘈杂。如果图很大,我们可能永远无法观察到某些节点或低概率边。这是处理随机数据的根本问题。比如,如果游客有 5% 的概率走进一条狭窄小巷,在我们观察到他们 10 次在小巷入口处时,他们可能一次都不会进入。
通过统计学方法,我们不仅可以分析最大似然估计值,还可以分析其误差范围和置信水平。不过这些分析超出了本书讨论范围。目前,我们只是提醒读者不要试图从少量随机数据中得出严格结论。
随机起始节点
我们可以扩展模型及其估计方法,以考虑随机行走从不同节点开始的情况。随机起始节点的概念可能不直观,因为实际的行走通常从单一地点开始,比如游客的酒店。然而,随机起始可以表示多种现实现象,例如游客可能从多个酒店中的任意一个出发,或谣言在社交网络中从随机节点开始传播。
我们用向量 表示起始概率,其中 表示随机行走从节点 开始的概率。由于 包含概率,因此 。
选择随机起始节点
可以用与列表 13-3 中 choose_next_node() 相同的方法直接抽样起始节点:
def choose_start(S: list) -> int:
❶ prob: float = random.random()
cumulative: float = 0.0
❷ for i in range(len(S)):
cumulative += S[i]
if cumulative >= prob:
return i
return len(S) - 1
解释:
- ❶
choose_start()从 [0, 1) 中生成随机数 - ❷ 不同于
choose_next_node()遍历节点边,这里遍历 SS 中的概率值,直到找到对应的节点
举例来说,想象一个邪恶巫师建造了迷宫,通过随机入口防止冒险者写攻略(干扰他们的退休计划)。巫师通过精心选择的分布 SS 将每个新到来的冒险者传送到随机位置。巫师将宝藏房间的起始概率设为 0,S[treasure] = 0,确保冒险者永远不会从宝藏房间开始。为了减少信息共享,每个房间的起始概率都小于 0.1,即冒险者从某个特定房间开始的概率不足 10%。
估计起始节点的概率分布
如果运行大量随机行走,我们可能想估计起始状态的分布。方法与转移概率估计类似。设 为从节点 开始的随机行走次数,则节点 的起始概率为:
实现代码如下:
def estimate_start_from_random_walks(walks: list) -> list:
num_nodes: int = 0
❶ for path in walks:
for node in path:
if node >= num_nodes:
num_nodes = node + 1
counts: list = [0.0] * num_nodes
❷ for path in walks:
counts[path[0]] += 1.0
for i in range(num_nodes):
counts[i] = counts[i] / len(walks)
return counts
解释:
- ❶ 计算最大节点索引并创建统计结构
- ❷ 仅统计每条路径的第一个节点,计算每个节点作为起始节点的经验概率
理论上,我们的冒险者可以用此方法为即将撰写的攻略收集信息:他们进入迷宫数百次,每次仔细记录落点位置,由此决定是否值得花费大量精力写一份全面的迷宫攻略。
为什么这很重要
在图上分析随机游走不仅有助于理解图的结构,还能分析其底层系统。然而,更重要的是,随机游走的概念扩展了我们可以用图算法建模的现实问题范围。我们可以超越确定性问题——比如“如何找到两点之间的最短路径”——去考虑更贴近现实的行为。例如,为了模拟路径规划中偶尔走错路的情况,我们可以使用随机游走:大部分时间沿最优路径前进,但在某些路口会随机出错。
在下一章中,我们将切换话题,从整体容量的角度研究图,寻找网络中的最大流量,以便建模从管道系统到交通运输的各种系统。