算法的故事 / 全书目录0 / 30 章已读

第 28 章

PageRank——1998年,为什么一个网页的重要性,取决于哪些网页指向了它?

正在加载交互实验…

第一季 · 人类如何驯服数据历史主线:1996—2004年 · Larry Page、Sergey Brin、Rajeev Motwani、Terry Winograd、随机冲浪者、马尔可夫链与幂迭代1998年,美国斯坦福大学。互联网正在以前所未有的速度增长。每天都有大量新网页出现。新闻、论文、论坛、商业网站、个人主页不断涌入网络。搜索引擎面临一个越来越困难的问题。假设用户搜索:
text
computer science
搜索引擎找到了十万个包含这个词组的网页。应该先展示哪一个?如果按关键词出现次数排序,一篇反复堆砌关键词的网页,可能排在真正权威的大学课程资料前面。如果按网页长度排序,一篇冗长但毫无价值的文章,可能比一份简短而准确的研究论文更靠前。如果只计算有多少网页链接某个网页,又可能被大量低质量页面互相链接所误导。这时,斯坦福大学的两名博士生提出了一个非常有影响力的思路:一个链接,不只是通向另一个网页的地址,也可以被视为一次推荐。而且:推荐者本身越重要,它的推荐就越有分量。这两名学生是:Lawrence Page,通常称为 Larry Page。以及 Sergey Brin。他们后来共同创办了 Google。但真正值得研究的,不是 Google 如何成为一家大公司。而是一个令人着迷的数学问题。假设:网页 A 很重要,因为网页 B 链接了它。网页 B 很重要,因为网页 C 链接了它。网页 C 又很重要,因为网页 A 链接了它。重要性互相依赖。我们似乎陷入了一个循环定义。如果每个网页的重要性,都依赖其他网页的重要性,那么我们应该从哪里开始计算?1998年,Larry Page、Sergey Brin、Rajeev Motwani 和 Terry Winograd 共同署名的研究报告:The PageRank Citation Ranking: Bringing Order to the Web《PageRank 引文排序:为互联网建立秩序》,系统描述了这种思想。该研究报告的早期版本可以追溯到1998年1月,后来也以 Stanford 技术报告编号1999-66存档。[Papers We Love][Netlib]它的名字就是:PageRank。

第一幕:1996年,一个叫 BackRub 的搜索引擎

故事要从1996年前后说起。Larry Page 当时在斯坦福大学研究互联网的链接结构。他想到:如果一个网页愿意链接另一个网页,这个链接本身就包含某种人为选择的信息。这与传统学术论文的引用关系有些相似。一篇研究论文如果被许多重要论文引用,通常值得关注。互联网网页之间的超链接,也可以被看成一张巨大的引用网络。Larry Page 与 Sergey Brin 合作开发了一套研究原型,名叫 BackRub。它不仅抓取网页文字,还分析网页之间的链接关系。美国国家科学基金会(NSF)的历史回顾记录了这段研究起源,以及早期原型如何在斯坦福大学的研究支持下发展。[NSF - U.S. National Science Foundation]

一个需要分清的历史细节

PageRank 的研究开始于1996年前后,并不是1998年才突然发明。1998年有两项容易混淆的重要成果。第一项是四人署名的 PageRank 技术报告,重点研究网页引用排名的数学机制。第二项是 Brin 与 Page 共同发表的:The Anatomy of a Large-Scale Hypertextual Web Search Engine《大型超文本网络搜索引擎的结构》。这篇发表于1998年 WWW7 会议的论文,介绍了当时 Google 搜索引擎原型的整体架构,包括抓取、索引、链接分析和查询处理。[Google Research]两者有关联,但不是同一篇论文。还有一点同样重要:PageRank 并不是最早使用链接关系研究文献或网页价值的算法。在学术引文分析、网络分析和同期超链接研究中,已经有其他相关思想。它具有突破性影响的地方,是把递归链接重要性模型与大规模网页搜索系统结合起来,并推动了实用化。

第二幕:网页之间的链接,构成了一张有向图

我们在第六章已经学习了图论。现在,你会看到图论如何改变互联网搜索。假设有四个网页:A:某大学课程主页。B:一篇计算机算法教程。C:一份重要技术资料。D:一篇个人文章。它们之间的链接关系如下:
四网页链接网络
ABCD箭头表示一个网页指向另一个网页D 无出链示意
查看 SVG 配图原件
示意图原件 28-005.svg
28-005.svg · 点击查看大图
下文交互实验使用明确的四节点有向图:A→B、D;B→C;C→B;D 无出链。上图用于帮助理解节点和有向链接,具体计算以所列边表为准。
在这种图里:每个网页是一个顶点(Vertex)。每条超链接是一条有向边(Directed Edge)。如果 A 指向 B:A→BA\rightarrow B则我们可以说 A 给 B 提供了一条入链。但这里有一个问题。如果 B 被100个普通网页链接。而 C 只被一个权威网页链接。谁应该更重要?仅仅计算入链数量,无法很好地区分。PageRank 提出:链接的贡献,应该取决于发出链接的网页自身拥有多少重要性。但如果每个网页的重要性都需要先知道其他网页的重要性,又该怎样计算?这就是下一幕真正的数学突破。

第三幕:PageRank 的核心思想——重要性可以沿着链接流动

假设某个网页 A 有100分的重要性。它向两个网页发出链接:A → B。A → D。一种直观的做法是:将 A 的100分平均分给 B 和 D。于是:B 获得50分。D 获得50分。如果另一个重要网页 C 也链接 B,那么 B 还可以得到来自 C 的贡献。在这种模型下:网页获得多少重要性,不仅取决于它有多少条入链,还取决于这些入链来自哪些网页,以及推荐者把自己的重要性分给多少个目标。一个非常重要的网页,如果同时链接1000个网页,那么它分给每个网页的贡献,就比只链接一个网页时更少。这就是最原始的 PageRank 直觉。

如何写成数学公式?

设:PR(v)PR(v) 表示网页 vv 的 PageRank。In(v)In(v) 表示所有指向 vv 的网页。L(u)L(u) 表示网页 uu 的出链数。如果暂时忽略无出链网页和随机跳转,我们可以写成:PR(v)=∑u∈In(v)PR(u)L(u)PR(v)=\sum_{u\in In(v)}\frac{PR(u)}{L(u)}请仔细观察右边。我们需要先知道所有链接到 vv 的网页的 PageRank。但那些网页的 PageRank,又必须由更多网页决定。于是形成了递归依赖。

循环定义真的无法计算吗?

不一定。我们可以先作出一个初始猜测。假设四个网页开始时同样重要。每个网页占:14=0.25\frac14=0.25然后,按照链接关系重新分配这些分数。得到第一轮结果。再把第一轮结果作为下一轮输入。不断重复。这叫作:Iterative Computation,迭代计算。上一章的 MapReduce 教会我们如何让大量机器并行计算和归组。而 PageRank 进一步告诉我们:有些问题需要在同一张图上执行很多轮相同的计算。每一轮产生新的排名向量。下一轮继续使用它。最终,排名可能逐渐稳定。但还有两个问题:如果某个网页没有出链,它的分数要去哪儿?如果网页图中存在封闭循环,分数会不会被困住?这些问题,需要随机冲浪者模型来解决。

第四幕:随机冲浪者——想象一个永远在互联网上闲逛的人

1998年的论文提出了一个非常形象的解释:Random Surfer,随机冲浪者。想象一个人在互联网上不断浏览网页。他不输入固定关键词,也不总是根据明确目标点击。而是按照一种概率规则移动。站在网页 A 时,他可以:选择 A 的某条超链接,进入另一页。或者突然厌倦当前浏览路线,随机跳到另一个网页。这种行为可以形成一个随机过程。如果我们观察这个人很长时间,统计他停留在每个网页上的比例,就能得到一种网页重要性度量。这就是 PageRank 的概率解释。1998年 Brin 与 Page 的搜索引擎论文明确将 PageRank 与这种随机冲浪者行为联系起来。[ETHW]

阻尼系数 d

我们采用现代 PageRank 文献最常见的约定:d=0.85d=0.85它表示:有85%的概率,随机冲浪者沿着当前网页的链接前进。有15%的概率,不跟随当前链接,而是随机跳转。因此:
沿超链接前进

85%

随机跳转

15%

历史论文中有时用 dd 表示不同方向的概率,阅读时必须核对公式。本章始终采用:dd 是继续跟随链接的概率,1−d1-d 是随机跳转的概率。0.85也是原始 PageRank 研究中采用的典型参数,但它并不是对所有图、所有业务都经过证明的唯一最优值。[数字对象标识符][Boost C++]

为什么要让用户有15%的概率随机跳转?

因为互联网图中存在两种很麻烦的情况。第一种是死胡同:网页 D 没有任何出链。如果只允许沿链接走,那么用户进入 D 后就不知道该去哪里。第二种是封闭链接区域:A 只链接 B。B 只链接 A。用户一旦进入这个封闭结构,就永远走不出去。如果没有随机跳转,这类结构可能导致多个长期行为分布,或者出现不利于简单迭代收敛的周期。加入适当的随机跳转,就能够打破这些封闭结构,使整个随机过程更加稳定。只要所有页面都有正的随机跳转概率,且 0<d<10<d<1,就能够得到一个具有唯一平稳分布的有限状态马尔可夫链。于是,我们拥有了计算稳定排名的数学基础。

第五幕:PageRank 的完整计算公式

假设网页总数为:NN采用均匀随机跳转,每个网页被随机选中的概率为:1N\frac1N那么,每个网页从随机跳转获得的基础概率是:1−dN\frac{1-d}{N}如果网页 uu 拥有至少一条出链,它沿每条出链平均分配:dPR(u)L(u)d\frac{PR(u)}{L(u)}但是,没有出链的网页也需要特殊处理。我们把这种网页称为:Dangling Node,悬挂节点。在本章采用的标准模型中,随机冲浪者进入悬挂节点后,会均匀跳向图中的任意网页。因此,悬挂节点的 PageRank 也要重新分配,而不能凭空消失。完整的一轮迭代可以写成:PRt+1(v)=1−dN+d(∑u→vL(u)>0PRt(u)L(u)+StN)\boxed{ PR_{t+1}(v)= \frac{1-d}{N} +d\left( \sum_{\substack{u\rightarrow v\\L(u)>0}} \frac{PR_t(u)}{L(u)} +\frac{S_t}{N} \right) }其中:St=∑u:L(u)=0PRt(u)S_t=\sum_{u:L(u)=0}PR_t(u)表示当前所有悬挂节点的 PageRank 总量。这个公式包含三部分:
  • 随机跳转带来的基础概率。
  • 所有有出链的页面沿链接送来的贡献。
  • 悬挂节点重新分配给全体页面的概率。
这种悬挂节点处理方式也被 NetworkX 的 PageRank 文档采用为默认方案:无出链节点按照个性化概率向量分配其转移概率,而默认个性化分布是均匀分布。[NetworkX 3.7 documentation]

一个重要的不变量

如果当前所有 PageRank 加起来是1:∑vPRt(v)=1\sum_v PR_t(v)=1那么,按照上述公式更新之后:∑vPRt+1(v)=1\boxed{\sum_v PR_{t+1}(v)=1}为什么?因为每个普通节点的概率质量,都被分给它指向的网页。每个悬挂节点的概率质量,也被重新分配。再加上随机跳转和沿链接前进的两种概率,总概率仍然是1。这意味着 PageRank 是一种概率分布。例如:
text
A = 0.10
B = 0.40
C = 0.30
D = 0.20
表示长期随机访问中,各网页所占的概率比例。并不是说:B 的内容质量一定是 A 的四倍。也不是说:B 在任何关键词搜索中都一定排在 A 前面。PageRank 衡量的是基于链接结构的相对重要性,而非查询相关性的完整答案。

第六幕:第一轮计算,四个网页的排名发生了什么变化?

现在我们使用前面已经给出的正式边表:
text
A → B
A → D
B → C
C → B
D → 无出链
初始时,所有网页同样重要:PR0=[0.25,0.25,0.25,0.25]PR_0=[0.25,0.25,0.25,0.25]取:d=0.85d=0.85四个网页,因此基础随机跳转概率是:1−0.854=0.0375\frac{1-0.85}{4}=0.0375D 没有出链。因此 D 的0.25还要平均分配给四个网页:0.85×0.254=0.0531250.85\times\frac{0.25}{4}=0.053125所以,本轮每个网页共同获得的基础概率为:0.0375+0.053125=0.0906250.0375+0.053125=0.090625接下来才考虑链接贡献。

网页 A

没有任何入链。所以:PR1(A)=0.090625PR_1(A)=0.090625

网页 B

有两条入链:A→B。C→B。A 有两条出链,因此只给 B 一半。C 只有一条出链,因此把沿链接传播的贡献全部给 B。PR1(B)=0.090625+0.85(0.252+0.25)PR_1(B)=0.090625+0.85\left(\frac{0.25}{2}+0.25\right)结果:PR1(B)=0.409375PR_1(B)=0.409375

网页 C

只有 B 指向它。而 B 只有一条出链。PR1(C)=0.090625+0.85×0.25PR_1(C)=0.090625+0.85\times0.25得到:PR1(C)=0.303125PR_1(C)=0.303125

网页 D

只有 A 指向它。而 A 有两条出链。PR1(D)=0.090625+0.85×0.252PR_1(D)=0.090625+0.85\times\frac{0.25}{2}得到:PR1(D)=0.196875PR_1(D)=0.196875因此,第一轮结果是:
网页初始概率第一轮结果
A0.2500000.090625
B0.2500000.409375
C0.2500000.303125
D0.2500000.196875
再检查一次:0.090625+0.409375+0.303125+0.196875=10.090625+0.409375+0.303125+0.196875=1概率总和保持为1。你会发现:B 最初与所有网页一样,只有0.25。但经过一轮传播,它的 PageRank 大幅提高。这是因为它同时获得来自 A 和 C 的链接贡献。不过,这只是第一轮。如果 B 已经变得更重要,那么它下一轮传给 C 的重要性也应该更多。接下来,C 又可能把这些重要性重新传回 B。我们还需要继续迭代。

第七幕:亲手观察排名如何一轮轮稳定

下面是真正的 PageRank 幂迭代实验。它使用的就是刚才四个网页的链接结构。可以调节阻尼系数。也可以从第0轮一直运行到第30轮。观察排名变化、概率总和和连续两轮之间的差异。

PageRank 幂迭代实验室

4网页 · 正确处理悬挂节点 · 均匀随机跳转
第 0 轮
阻尼系数 d

0.85

0.50 · 更多随机跳转0.95 · 更依赖链接
ABCD圆越大,PageRank 越高D 无出链,按均匀分布处理
A
25.00%
B
25.00%
C
25.00%
D
25.00%
概率总和1.000000
本轮最高

A

与上轮 L1 差—
每一轮使用上一轮的完整排名向量同步更新全部节点;图中柱形长度是该节点占总概率的比例。L1 差是所有节点排名变化绝对值之和,越接近零表示迭代变化越小。

建议你做三个实验

先设置:d=0.85d=0.85从第0轮连续运行到第10轮。你会发现,排名在前几轮变化比较明显,随后逐渐稳定。再调整:d=0.50d=0.50你会发现每个网页的概率更接近均匀分布,因为随机跳转所占比例更高。最后调整:d=0.95d=0.95这时排名更依赖链接结构。B、C 之间的互相链接会对最终分布产生更明显的影响。但迭代往往也需要更多轮才能达到相同的误差阈值。这里体现了一个非常典型的数值计算权衡:更强地依赖原始图结构,并不一定意味着更快地得到稳定结果。

第八幕:1998年论文里的一个惊人观点——网页排名其实是一个特征向量

现在我们进入真正的线性代数。假设有:NN个网页。我们用一个向量保存全部排名:r=[PR(A)PR(B)PR(C)PR(D)]\mathbf r= \begin{bmatrix} PR(A)\\ PR(B)\\ PR(C)\\ PR(D) \end{bmatrix}网页链接也可以写成一个矩阵。假设 PP 是一个行随机矩阵:PijP_{ij}表示从网页 ii 沿链接转移到网页 jj 的概率。对于普通网页:Pij={1/L(i),i→j0,没有对应链接P_{ij}= \begin{cases} 1/L(i),&i\rightarrow j\\ 0,&\text{没有对应链接} \end{cases}对于无出链的网页,把对应整行设置为:[1/N,…,1/N][1/N,\ldots,1/N]这样每一行概率之和都是1。再加入随机跳转。设均匀概率向量为:u=[1/N⋮1/N]\mathbf u= \begin{bmatrix} 1/N\\ \vdots\\ 1/N \end{bmatrix}那么一轮 PageRank 可以写作:rt+1=dPTrt+(1−d)u\boxed{ \mathbf r_{t+1} =dP^\mathsf{T}\mathbf r_t+(1-d)\mathbf u }这个公式与前面逐网页求和的公式完全等价。只不过我们现在把它写成矩阵运算。

稳定之后会发生什么?

假设某一轮的排名不再变化。那么:rt+1=rt=r∗\mathbf r_{t+1}=\mathbf r_t=\mathbf r^*因此:r∗=dPTr∗+(1−d)u\boxed{ \mathbf r^* =dP^\mathsf{T}\mathbf r^*+(1-d)\mathbf u }如果定义 Google 转移矩阵:G=dP+(1−d)1uTG=dP+(1-d)\mathbf1\mathbf u^\mathsf{T}那么:GTr∗=r∗\boxed{ G^\mathsf{T}\mathbf r^*=\mathbf r^* }这就是一个特征向量方程。r∗\mathbf r^* 是转移矩阵转置对应特征值1的概率特征向量。在概率论中,这个向量也叫作:Stationary Distribution,平稳分布。而用一个初始向量,反复乘以矩阵直到接近稳定,就是:Power Iteration,幂迭代。1998年原始 PageRank 论文直接把递归排名、链接矩阵、主特征向量和反复应用矩阵计算联系起来。原论文第3—4页还用图示说明重要性如何沿链接传播,以及如何避免封闭链接结构形成排名陷阱。[Ethw Images]

为什么迭代会收敛?

如果:0<d<10<d<1并采用均匀的正概率跳转,那么转移矩阵中的任意网页,都能够以正概率转移到任意其他网页。这使对应有限马尔可夫链不可约且非周期。因此具有唯一的平稳分布。而在这个标准模型下,还可以得到一个很漂亮的误差收缩关系。设两份概率向量为 x\mathbf x、y\mathbf y。经过一轮 PageRank 更新后:∥F(x)−F(y)∥1≤d∥x−y∥1\left\|F(\mathbf x)-F(\mathbf y)\right\|_1 \leq d\left\|\mathbf x-\mathbf y\right\|_1其中:F(r)=dPTr+(1−d)uF(\mathbf r)=dP^\mathsf{T}\mathbf r+(1-d)\mathbf u而:∥x−y∥1=∑i∣xi−yi∣\|\mathbf x-\mathbf y\|_1 =\sum_i |x_i-y_i|表示两份排名的 L1 距离。如果 d=0.85d=0.85,那么连续迭代误差的上界,会按照至多0.85的比例收缩。这解释了为什么反复计算可以逐渐接近稳定状态。但也说明:当 dd 越接近1时,理论上的最坏收缩速度界限越弱。所以,增加阻尼系数可能让迭代需要更多轮。这一点,在你刚才的交互实验中也能够观察到。

第九幕:PageRank 的计算复杂度到底是多少?

假设整张网页图包含:NN 个网页。EE 条超链接。如果我们直接建立一个 N×NN\times N 的稠密矩阵,那么内存需求会是:O(N2)O(N^2)这显然不适合巨大规模的互联网。例如,一亿个网页对应的稠密矩阵,将拥有:101610^{16}个元素。根本不应该采用这种表示方式。好在真实网页图通常可以用稀疏的邻接表保存。只记录真实存在的链接。这样图结构占用空间大致为:O(N+E)O(N+E)一次 PageRank 迭代,需要访问所有节点及其出链,累加贡献。因此时间复杂度为:O(N+E)\boxed{O(N+E)}如果需要迭代 KK 轮:O(K(N+E))\boxed{O(K(N+E))}这还没有计入分布式网络通信、持久化 I/O 和负载不均衡等系统成本。

1998年,真的能够在千万网页上计算吗?

可以。Brin 和 Page 在1998年的 Google 搜索引擎论文中描述了当时 PageRank 的大规模计算能力。论文提到,对约2600万网页计算 PageRank,可以在一台当时的中等规模工作站上花数小时完成。[ETHW]这可能让今天的人有些惊讶。为什么不是必须动用上千台服务器?因为 PageRank 核心是一种结构非常规则的稀疏图迭代计算。只要图的表示方法和数据访问流程设计合理,它并不需要执行复杂的全对全网页比较。不过,这里有一个历史时间差必须讲清楚。PageRank 的关键研究发表于1998年。我们上一章讨论的 Google MapReduce 原始系统开发于2003年,并于2004年发表论文。所以:不能说1998年的 PageRank 是依靠2004年的 MapReduce 系统才发明出来的。正确的理解是:MapReduce 后来提供了一种可以用来分布式实现 PageRank 幂迭代的通用计算框架。

第十幕:用上一章的 MapReduce,怎样计算 PageRank?

现在我们把两章正式连接起来。假设有十亿个网页。我们不能指望一台普通计算机把整个图和所有排名都高效地保存在内存中。于是把网页分片交给不同的 Map Worker。

Map 阶段:向目标网页发送贡献

假设当前:
text
A 的 PageRank = 0.20
并且:
text
A → B
A → C
那么 A 的 Map 任务可以输出:
text
(B, 0.10)
(C, 0.10)
这里先只处理沿链接传播的原始贡献,还没有乘阻尼系数。

Shuffle 阶段:按照目标网页归组

不同 Worker 输出:
text
(B, 0.10)
(B, 0.07)
(B, 0.15)
系统把相同目标 B 的贡献归到一起。于是:
text
B → [0.10, 0.07, 0.15]

Reduce 阶段:累加贡献

B 的 Reduce 任务计算:0.10+0.07+0.15=0.320.10+0.07+0.15=0.32再乘以 dd。并加入:随机跳转的基础概率。当前所有悬挂节点需要分配的全局概率质量。最终得到下一轮的 PageRank。

为什么需要多轮 MapReduce?

因为完成一次 Reduce 后,我们得到的只是:r1\mathbf r_1接着需要:r2\mathbf r_2然后:r3\mathbf r_3一直到误差低于预定阈值。所以,一个典型的批处理实现可以把每一轮 PageRank 当成一个 MapReduce 作业。其输出成为下一轮作业的输入。但这意味着每一轮都可能需要:任务调度。中间数据 Shuffle。读取与写入新的排名。所有分区协调迭代边界。这里的通信成本可能很大。这也解释了为什么后来的图计算系统,会更加重视迭代任务之间的数据驻留和消息交换效率。

两个重要工程细节

第一,每个网页的出链结构必须能够被后续轮次继续使用。不能只传贡献,而把图结构丢掉。第二,网页 D 这样的悬挂节点没有任何出链,但仍然拥有 PageRank。系统必须保留它的节点身份,并收集它的概率质量,再按照约定方式分配。因此,一个正确的 MapReduce PageRank 作业通常还需要全局悬挂质量汇总和某种同步或分轮处理机制。并不是简单写一个 map() 加一个 sum() 就已经完整实现了 PageRank。

第十一幕:自己写一个真正能够收敛的 PageRank

现在我们用 Python 标准库实现完整的基础版本。它支持:有向图。任意数量的网页。无出链的悬挂节点。阻尼系数。迭代收敛判断。不需要 NumPy,也不需要 NetworkX。
python
def pagerank(
    graph,
    damping=0.85,
    tolerance=1e-10,
    max_iterations=1000,
):
    if not 0 < damping < 1:
        raise ValueError("damping must be between 0 and 1")

    # 包含那些只被链接、没有单独列出的网页
    nodes = sorted(
        set(graph)
        | {
            target
            for targets in graph.values()
            for target in targets
        }
    )

    if not nodes:
        return {}, 0

    n = len(nodes)

    # 删除重复出链,并为悬挂节点保留空列表
    links = {
        node: list(dict.fromkeys(graph.get(node, [])))
        for node in nodes
    }

    # 初始均匀分布
    rank = {
        node: 1.0 / n
        for node in nodes
    }

    for iteration in range(1, max_iterations + 1):

        # 所有悬挂节点的概率质量
        dangling_mass = sum(
            rank[node]
            for node in nodes
            if not links[node]
        )

        # 均匀随机跳转 + 悬挂质量均匀分配
        base = (
            (1.0 - damping) / n
            + damping * dangling_mass / n
        )

        new_rank = {
            node: base
            for node in nodes
        }

        # 沿真实出链传播 PageRank
        for source in nodes:
            targets = links[source]

            if not targets:
                continue

            contribution = (
                damping
                * rank[source]
                / len(targets)
            )

            for target in targets:
                new_rank[target] += contribution

        # L1 差:连续两轮的变化
        error = sum(
            abs(new_rank[node] - rank[node])
            for node in nodes
        )

        rank = new_rank

        if error < tolerance:
            return rank, iteration

    raise RuntimeError(
        "PageRank did not converge within max_iterations"
    )
使用刚才的四网页网络:
python
graph = {
    "A": ["B", "D"],
    "B": ["C"],
    "C": ["B"],
    "D": [],
}

ranks, iterations = pagerank(graph)

for page, score in sorted(
    ranks.items(),
    key=lambda item: item[1],
    reverse=True,
):
    print(page, round(score, 4))

print("Total:", round(sum(ranks.values()), 6))
print("Iterations:", iterations)
预期的近似排名是:
网页收敛后的 PageRank(约)
B44.10%
C42.86%
D7.66%
A5.38%
概率总和约为:11B 和 C 的排名非常接近。为什么?因为它们互相链接,而且 B 从 A 获得额外的贡献。但当 B 获得较高 PageRank 后,又会把相当多的重要性传给 C。经过多轮传播,二者形成了相对接近的长期访问概率。

如何验证结果?

可以使用 NetworkX:
python
import networkx as nx

G = nx.DiGraph()

G.add_nodes_from(graph)

for source, targets in graph.items():
    for target in targets:
        G.add_edge(source, target)

reference = nx.pagerank(
    G,
    alpha=0.85,
    tol=1e-12,
)

print(reference)
NetworkX 官方文档明确说明,它使用幂迭代计算 PageRank,默认阻尼系数为0.85,并提供个性化跳转、悬挂节点处理及收敛容差等参数。[NetworkX 3.7 documentation]这也是一个非常好的算法学习方法。先自己实现。再用成熟库进行交叉验证。重点比较:排名是否一致。概率总和是否接近1。迭代停止条件是否正确。对于悬挂节点是否有遗漏。不要仅仅因为代码没有报错,就认为算法已经正确。

第十二幕:PageRank 为什么不能直接代表网页的真实质量?

到这里,我们需要讨论 PageRank 的局限。1998年的原始研究本身就已经意识到,互联网上的链接结构可能被人为操纵。如果一个网页拥有大量入链,它未必就真的包含高质量内容。

链接农场(Link Farm)

假设某个人创建了1000个网页。这些网页相互链接,并且都指向某个目标网站。如果搜索引擎单纯依据链接结构计算排名,就可能受到操纵。后来围绕链接垃圾、链接购买和排名作弊,出现了长期的对抗研究。这也说明:PageRank 是一种基于网页链接关系计算的重要性指标,而不是对网页真实性、专业性和安全性的直接证明。

PageRank 与搜索关键词无关

原始的全局 PageRank 主要基于链接图计算。它不需要知道用户搜索:computer science还是:best restaurants但搜索结果必须考虑用户查询的实际含义。一份非常权威的物理论文,可能拥有很高 PageRank。但如果用户搜索的是如何做蛋糕,这篇论文显然不应该仅仅因为链接权重高,就排在最前面。所以搜索引擎需要综合:词语匹配。内容相关性。用户查询意图。链接关系。时效性。语言。以及其他相关因素。Google 当前的官方排名系统文档明确说明,PageRank 经过多年演变,仍是其核心排名系统的一部分,但 Google 同时采用多种不同的排名信号和系统。[Google for Developers]因此:现代 Google 搜索排名,不等于单独把网页按照1998年的 PageRank 分数从高到低排序。

另一个同时代的重要研究:HITS

1997年前后,Cornell 大学的 Jon Kleinberg 也在研究超链接结构。他提出了著名的 HITS 思想,区分两种角色:Hub:善于链接权威资源的网页。Authority:被优秀 Hub 引用的网页。HITS 在1998年的 SODA 会议上发表,扩展版本于1999年刊登在 Journal of the ACM。[康奈尔计算机科学系]PageRank 与 HITS 都体现了递归链接评分思想,但采用不同的数学定义与计算方式。它们共同说明:1990年代后期的网页搜索研究,正在从单纯分析文字,走向结合图结构理解信息的重要性。

第十三幕:PageRank 后来影响的远不只是搜索引擎

把 PageRank 的数学结构抽象出来,它其实在计算:一个随机游走过程,在图中各节点上的长期稳定访问概率。一旦这样理解,就会发现它不一定只适用于网页。例如:论文引用网络。知识图谱。社交关系网络。交通网络。生物网络。以及其他具有节点和有向关系的系统。但不同应用中,边的意义、是否有权重、是否需要个性化跳转,都可能不同。

Personalized PageRank

标准的随机跳转是均匀的:ui=1Nu_i=\frac1N但我们也可以改用另一个概率向量:v\mathbf v例如,让随机跳转更偏向某个研究主题的一组论文。这样就得到一种个性化 PageRank:r=dPTr+(1−d)v\boxed{ \mathbf r=dP^\mathsf{T}\mathbf r+(1-d)\mathbf v }其中:vi≥0,∑ivi=1v_i\geq0,\qquad\sum_i v_i=1个性化向量决定了随机跳转更倾向哪些节点。当然,如果个性化向量中有些分量为0,就需要进一步检查图的结构与转移矩阵,才能确保所需的唯一性和收敛性质。这种思想后来又发展出 Topic-Sensitive PageRank 等研究方向。2002年,Taher Haveliwala 发表了有关主题敏感 PageRank 的经典研究,探索如何把用户或查询主题与链接重要性计算结合起来。[IEEE Milestones Wiki]

终章挑战:为什么一个链接很少的网页,也可能拥有很高的 PageRank?

假设:网页 A 拥有100条来自低权重网页的入链。网页 B 只有一条入链。但这条入链来自一个 PageRank 很高、而且出链很少的网页 C。那么,哪一种判断正确?
请选择正确答案

尾声:1998年,Google 把一张混乱的互联网图变成了可以计算的数学问题

1996年前后,Larry Page 和 Sergey Brin 开始在斯坦福大学研究网页链接。1998年1月,Page、Brin、Motwani 和 Winograd 的 PageRank 技术报告正式记录了利用递归链接重要性为网页评分的方法。同年,Brin 与 Page 又系统介绍了早期 Google 搜索引擎架构。这些研究展示了一种非常重要的思想转变:过去,搜索引擎主要分析网页里写了什么。PageRank 进一步研究:整个互联网如何通过链接对网页作出分布式推荐。它不要求任何一个人事先为所有网页打分。而是从网页之间的有向链接图出发。通过概率转移与幂迭代,让重要性在整个网络中传播。最终得到一组相互一致的排名分数。这是一种令人印象深刻的数学方法。一个原本看起来无法求解的循环定义:“网页 A 重要,因为重要网页 B 链接了它。”竟然可以转化为:r=GTr\mathbf r=G^\mathsf{T}\mathbf r再通过反复迭代得到稳定解。最值得记住的,不是公式中的0.85。而是更深一层的设计原则:当对象的价值由它与其他对象之间的关系共同决定时,可以尝试把整个系统表示为图,再寻找图结构中的稳定全局分布。这也是 PageRank 的思想后来能够超越网页搜索,进入其他网络分析领域的原因。

第二十八章知识总结

概念核心思想
Directed Graph用有向图表示网页超链接关系
In-degree / Out-degree入链数量与出链数量
PageRank依据链接关系递归传播的重要性概率
Random Surfer通过随机浏览行为解释排名
Damping Factor控制沿链接前进与随机跳转的概率
Dangling Node没有出链、需要特殊概率处理的节点
Markov Chain描述网页之间的概率转移
Stationary Distribution长期稳定的访问概率分布
Eigenvector稳定排名满足的特征向量关系
Power Iteration重复更新排名,逐渐逼近稳定解
Sparse Matrix高效保存和计算大规模链接图
MapReduce将每轮排名贡献分发、归组和求和
Personalized PageRank使用个性化概率向量控制随机跳转

本章真实历史与研究资料

Page、Brin、Motwani、Winograd(1998年1月29日):PageRank 原始技术报告 PDFBrin、Page(1998):《The Anatomy of a Large-Scale Hypertextual Web Search Engine》美国国家科学基金会:BackRub 与 Google 的研究起源IEEE 工程历史档案:PageRank 的发展历史及同期链接分析研究NetworkX 官方文档:PageRank、阻尼系数、悬挂节点与收敛Google Search Central:现代 Google 搜索中的 PageRank 与其他排名系统Jon Kleinberg 官方学术主页:HITS、Hub 与 Authority 研究
下一章预告

第二十九章:倒排索引与 TF-IDF——1972年,为什么一个罕见的词,可能比一千个常见词更有价值?

1972年,英国剑桥大学。计算机科学家 Karen Spärck Jones 在《Journal of Documentation》发表:A Statistical Interpretation of Term Specificity and Its Application in Retrieval《词项专指性的统计解释及其在信息检索中的应用》。她研究了一个直观而深刻的问题。假设某个词出现在100万份文档里。另一个词只出现在10份文档里。如果用户搜索同时包含这两个词的关键词:哪个词更有助于找到真正相关的文档?一个非常常见的词,可能几乎无法区分不同文档。但一个罕见而具体的词,往往具有更强的区分能力。这就成为后来信息检索中著名的 Inverse Document Frequency,逆文档频率(IDF) 思想的重要基础。[Emerald Publishing]下一章,我们将从倒排索引的数据结构讲起,研究 TF、IDF、TF-IDF 与 BM25,并亲自构建一个能够搜索数千份文档的小型搜索引擎。最令人惊讶的问题是:为什么把一个词重复写一千遍,不一定能让一篇文档变得更相关;而一个只出现一次的罕见关键词,却可能彻底改变搜索结果排序?