在PageRank算法出现之前,早期的搜索引擎是根据关键词出现次数对网页进行排序,但这样的算法有严重的缺陷,假设网站A像祥林嫂一样反复重复几个热门关键词,那么用户在搜索这些关键词的时候,网站A就会被排在前面,而这显然不是用户希望得到的结果,更严重的后果是网络上会充斥着广告和垃圾信息,而真正有用的信息却难以被用户搜索到。
于是, 1996 年初, 谷歌公司的创始人, 当时还是美国斯坦福大学 (Stanford University) 研究生的佩奇 (Larry Page) 和布林 (Sergey Brin) 开始了对网页排序问题的研究。他们从学术界判别论文重要性的通用方法,即考量引用次数得到了灵感,认为网页的重要性(也就是排序)是不能靠每个网页自己来标榜的,而应该通过全互联网所有网页间的相互链接来确定。具体地说,一个网页被其它网页链接得越多, 它的排序就应该越靠前。不仅如此,Larry和Sergey还进一步提出,一个网页越是被排序靠前的网页所链接,它的排序就也应该越靠前。这一条的意义也是不言而喻的,就好比一篇论文被诺贝尔奖得主所引用,显然要比被普通研究者所引用更说明其价值。
思路虽然有了,实现却并非易事。网页 Wi 的排序 Pi 由所有其他的网页的排序 Pj≠i 决定,而 Pi 又会对 Pj≠i 产生影响,于是就面临了“先有鸡还是先有蛋”的难题。
Larry和Sergey的解决方法是这样的:假设用户A是互联网上所有用户的平均(即A不存在个人喜好),那么再假定经历足够多的网上漫游过后,A在n时刻处在页面 Wi 的概率为 Pi , Wi 页面有 Ni 个外链,由于A没有个人喜好,因此点击每个外链的可能性均为 1/Ni
根据这个假设,我们可以得出: Pi(n+1)=∑jPj(n)pj→i/Nj
其中 pj→i 是一个描述互联网链接结构的指标函数 (indicator function),其定义是如果网页 Wj 有链接指向网页 Wi ,则 pj→i 取值为 1,反之则为 0
为符号简洁起见, 我们将虚拟用户第 n 次浏览时访问各网页的几率合并为一个列向量 Pn , 它的第 i 个分量为 Pi(n) ,并引进一个只与互联网结构有关的矩阵 H , 它的第 i 行 j 列的矩阵元为 Hij=pj→i/Nj , 则上述公式可以改写为:
Pn+1=HPn
Pn+1=⎡⎣⎢⎢⎢⎢P1(n+1)P2(n+1)⋯Pj(n+1)⎤⎦⎥⎥⎥⎥ H=⎡⎣⎢⎢⎢⎢⎢⎢0p1→2/N1⋮p1→j/N1p2→1/N20⋮p2→j/N2⋯⋯⋱⋯pj→1/Njpj→2/Nj⋮0⎤⎦⎥⎥⎥⎥⎥⎥ Pn=⎡⎣⎢⎢⎢⎢P1(n)P2(n)⋯Pj(n)⎤⎦⎥⎥⎥⎥
至此,我们将问题建模为一个平稳马尔科夫链1,H 就是转移矩阵。于是我们有了求解 Pn 的公式:
Pn=HnP0
可是,万里长征才走了一半,我们需要计算的是经过无数用户无数次的网页浏览后,用户停留在每个网页的概率,即 limn→∞Pn
那么,首先我们要解决三个问题:
非常遗憾的是,就目前来看,这三个问题都无法解决:
为了解决以上的问题,需要对用户A的行为进行修正。现实生活中的真实情况是,当A访问到悬停页面时,并不会就此停止,鉴于A是平均用户,合理的行为是随机跳转到互联网上任一网页。于是我们需要把 H 中为0的列向量(悬停页面对应的列向量)换成 e/N (e是所有分量均为 1 的列向量,N为互联网中所有页面的总数);为此,我们引入一个描述“悬挂网页”的指标向量 (indicator vector) a , 它的第 i 个分量的取值视 Wi 是否为”悬停网页”而定——如果是“悬挂网页“,取值为 1,否则为 0
用S表示修正后的转移矩阵: S=H+eaT/N ;这一步称为随机性修正(stochasticity adjustment)
现在我们解决了悬停网页的麻烦,可是其他问题怎么办呢?于是引入了另一个修正,用户并不会受限于当前网页提供的链接,他们也有一定概率跳出来,随机选择一个网页;假定跳转到当前页面提供的链接的概率为阻尼系数 α ,则跳出的概率为 1−α
再次修正: G=αS+(1−α)eeT/N
经过这一步修正,G成为了一个素矩阵2,因此也被称为素修正(primitivity adjustment)
结合马尔可夫链基本定理 (fundamental theorem of Markov chains),在一个马尔可夫过程中,如果转移矩阵是素矩阵, 那么极限收敛且与初始状态无关。
至此,之前的三个问题都得到了解决!
经过两次修正,最后我们得到了: Pn=GnP0
细心的你也许发现了,我们还留了一个小问题,即 α 的取值:按理 α 应该由众多的实际数据统计得出,但还有一个我们不得不考虑的问题制约着 α 的取值,即 Pn 收敛的速度, α 值越小, Pn 收敛越快,即计算速度越快,但矩阵S,即算法中最核心的网页间相互链接的信息,对 Pn 的影响也就越小,这显然是我们不希望看到的。因此,折中考虑之后,Larry和Sergey最终选择的是 α=0.85
具体的编码实现有多种方式,最简单稳定的是如下的迭代法:
R = GX; //G为转移矩阵,R,X为列向量
ITER_LIMIT = 1000;
THRESHOLD = xxx; //THRESHOLD为列向量,每个分量为阙值e
count = 0;
while (true) {
if (abs(X-R) < THRESHOLD) { //如果最后两果近似或者相同,即达到稳定状态,返回R
return R;
} else if (count > ITER_LIMIT) { //如果达到迭代上限,即使未完全稳定,也返回R
return R;
} else {
X =R;
R = GX;
}
}
R语言中的igraph包也对pagerank作了实现,具体使用如下:
page.rank (graph, algo = c("prpack", "arpack", "power"),
vids = V(graph), directed = TRUE, damping = 0.85,
personalized = NULL, weights = NULL, options = NULL)
Option | Description |
---|---|
graph | graph对象 |
algo | 具体算法实现,默认prpack |
vids | 顶点数 |
directed | 是否有向图,true/false |
damping | 阻尼系数 α |
personalized | 用户自定义页面间跳转概率,而不是平均分布,需保证列向量之和为1 |
weights | 为页面间的链接分配权重 |
options | 覆盖power/arpack的算法参数,对prpack算法无效 |
Return Value | Description |
---|---|
vector | PageRank值的向量 |
value | 特征向量的特征值,永远为1 |
options | arpack算法的参数,若采用其他算法,则为NULL |
g <- random.graph.game(20, 5/20, directed=TRUE)
page.rank(g)$vector
[1] 0.06174378 0.04634210 0.06182139 0.02905230
[5] 0.03941021 0.05210103 0.07246689 0.06876744
[9] 0.01526981 0.04776690 0.01775830 0.05296421
[13] 0.06191911 0.06084280 0.04115572 0.05433190
[17] 0.04165332 0.04229852 0.06537069 0.06696358
参考文献:谷歌背后的数学 http://www.changhai.org/articles/technology/misc/google_math.php