搜索引擎的主要工作,是爬虫建立资料库,根据关键词快速查找(倒排索引)含有关键词的页面,将结果按照重要程度排序后呈现给用户。核心难题在于如何对海量检索结果排序(Rank, Not Sort)。
互联网的超链接(hyperlink)构成了一个巨大的有向图,图中的结点代表网页,有向连接表示超链接,称入链(inlink) 为连入某页面的、指向结点的超链接,而出链(outlink) 就是结点出发的超链接。
可以将超链接视为一种推荐,由我的主页指向你的主页的超链接,就是我对你的HomePage的一种认可,具有更多推荐的页面肯定比只具有少数入链的页面要更重要。同时,诸如文献引用或推荐信,推荐者本身的地位也同样重要。而且,如果推荐者相当随意和慷慨,他推荐过的数量特别多,那么他的每个推荐的权重就应当相应下调。这正是PageRank基于欢迎度评分的思想:如果一个网页被其他重要的页面所指向,那它就是重要的。
HITS方法定义了枢纽(hub)和权威(authority),如果一个页面包含了许多出链,就被认为是一个枢纽网页。如果一个页面有许多入链,就称之为一个权威网页(authority)。HITS的主要论点就是:如果一个页面指向好的权威网页,那它就是一个好的枢纽网页(给予一个较高的枢纽评分);如果一个网页被好的枢纽网页所指向,那它就是个好的权威网页。
某个页面 P i P_i Pi的PageRank记为 r ( P i ) r(P_i) r(Pi),它是所有指向 P i P_i Pi的页面的PageRank之和:
r ( P i ) = ∑ P j ∈ B P i r ( P j ) ∣ P j ∣ r(P_i) = \sum\limits_{P_j \in B_{P_i}}\dfrac{r(P_j)}{|P_j|} r(Pi)=Pj∈BPi∑∣Pj∣r(Pj)
式中, B P i B_{P_i} BPi为指向 P i P_i Pi的页面集合(就是回连至 B P i B_{P_i} BPi),而** ∣ P j ∣ |P_j| ∣Pj∣是由 P j P_j Pj发出的出链数量**。
由于入链至页面 P i P_i Pi的那些页面的PageRank值 r ( P j ) r(P_j) r(Pj)是未知的,可以用迭代的方法来绕开这个问题:假设在开始时,
所有页面都具有相等的PageRank值: 1 n \frac{1}{n} n1,n为所有页面的总数。将前一次循环中的值代入 r ( P j ) r(P_j) r(Pj),不断迭代,直到PageRank得分最终收敛到某些稳定值。
具有6个页面的网络有向图,如图所示:
假设有四个页面的有向图结构如下:
上图中A页面链向B、C、D,所以一个用户从A跳转到B、C、D的概率各为1/3。设一共有N个网页,则可以组织这样一个N维矩阵:其中i行j列的值表示用户从页面j转到页面i的概率。这样一个矩阵叫做转移矩阵(Transition Matrix)。下面的转移矩阵M对应上图:
M = [ 0 1 / 2 0 1 / 2 1 / 3 0 0 1 / 2 1 / 3 1 / 2 0 0 1 / 3 0 1 0 ] \large M=\begin{bmatrix} 0 & 1/2 & 0 & 1/2\\ 1/3 & 0 & 0 & 1/2\\ 1/3 & 1/2 & 0 & 0\\ 1/3 & 0 & 1 & 0 \end{bmatrix} M=⎣⎢⎢⎢⎡01/31/31/31/201/2000011/21/200⎦⎥⎥⎥⎤
然后,设初始时每个页面的rank值为1/N,这里就是1/4。按A-D顺序将页面rank为向量v:
v 0 = [ 1 / 4 1 / 4 1 / 4 1 / 4 ] \large v_0=\begin{bmatrix} 1/4\\ 1/4\\ 1/4\\ 1/4 \end{bmatrix} v0=⎣⎢⎢⎢⎡1/41/41/41/4⎦⎥⎥⎥⎤
M第一行分别是A、B、C和D转移到页面A的概率,而v的第一列分别是A、B、C和D当前的rank,因此用M的第一行乘以v的第一列,所得结果就是页面A最新rank的合理估计,同理,Mv的结果就分别代表A、B、C、D新rank:
M v 0 = [ 1 / 4 5 / 24 5 / 24 1 / 3 ] \large Mv_0=\begin{bmatrix} 1/4\\ 5/24\\ 5/24\\ 1/3 \end{bmatrix} Mv0=⎣⎢⎢⎢⎡1/45/245/241/3⎦⎥⎥⎥⎤
然后用M再乘以这个新的rank向量,又会产生一个更新的rank向量。迭代这个过程,可以证明v最终会收敛,即v约等于Mv,此时计算停止。最终的v就是各个页面的pagerank值。例如上面的向量经过几步迭代后,大约收敛在(1/4, 1/4, 1/5, 1/4),这就是A、B、C、D最后的pagerank。
在讨论PageRank公式之前还要先讨论两个在实际中会遇到的问题:
1)Spider Traps问题
可以预见,如果把真实的Web组织成转移矩阵,那么这将是一个极为稀疏的矩阵,从矩阵论知识可以推断,极度稀疏的转移矩阵迭代相乘可能会使得向量v变得非常不平滑,即一些节点拥有很大的rank,而大多数节点rank值接近0。而一种叫做Spider Traps节点的存在加剧了这种不平滑。例如下图:
注意,D只链向自己(链向自己也算外链,当然同时也是个内链)。这种节点叫做Spider Trap,如果对这个图进行计算,会发现D的rank越来越大趋近于1,而其它节点rank值几乎归零。
2)Dead Ends问题
Dead Ends,就是这样一类节点:它们不存在外链。如下图所示:
这里D页面不存在外链,是一个Dead End。上面的算法之所以能成功收敛到非零值,很大程度依赖转移矩阵这样一个性质:每列的加和为1。而在这个图中,M第四列将全为0。在没有Dead Ends的情况下,每次迭代后向量v各项的和始终保持为1,而有了Dead Ends,迭代结果将最终归零。
为了克服这种由于矩阵稀疏性和Spider Traps带来的问题,需要对PageRank计算方法进行一个平滑处理,加入一个随机转移概率():就是我们假设在任何一个页面浏览的用户都有可能以一个极小的概率瞬间转移到另外一个随机页面。当然,这两个页面可能不存在超链接,随机转移概率只是为了算法需要而强加的一种素性调整(primitivity adjustment)。加入了随机转移概率后,每个节点向其他节点转移的概率更加倾向于“均等化”了,这就等于削弱了原本的网络结构的先验特性。它本质上就是一个结构风险最小化思想。
加入随机概率转移后,向量迭代公式变为:
v ′ = ( 1 − β ) M v + e β N \large {v}'=(1-\beta)Mv+e\frac{\beta}{N} v′=(1−β)Mv+eNβ
有的也写作:
V ′ = α M V + ( 1 − α ) e \large{V'}=\alpha MV + (1-\alpha)e V′=αMV+(1−α)e,其中e是单位矩阵,N是page总数。
另一种理解:
为了处理那些“没有外部链接的页面”(这些页面就像“黑洞”一样吞噬掉用户继续向下浏览的概率)所带来的问题,我们假设:这类页面链接到集合中所有的网页(不管它们是否相关),使得这类网页的PR值将被所有网页均分。对于这种残差概率(residual probability),我们引入阻尼系数 d(damping factor),并声明d=0.85,其意义是:任意时刻,用户访问到某页面后继续访问下一个页面的概率,相对应的1-d=0.15则是用户停止点击,随机浏览新网页的概率。d的大小由一般上网者使用浏览器书签功能的频率的平均值估算得到。
P R ( p i ) = d ∑ p j ∈ M ( p i ) P R ( p j ) L ( p j ) + 1 − d N PR(p_i)=d\sum_{p_j\in M(p_i)}\dfrac{PR(p_j)}{L(p_{j})}+{\dfrac{1-d}{N}} PR(pi)=d∑pj∈M(pi)L(pj)PR(pj)+N1−d
其中, p 1 , p 2 , … , p N p_{1},p_{2},…,p_{N} p1,p2,…,pN是目标页面, M ( p i ) M(p_{i}) M(pi)是链入 p i p_i pi页面的集合, L ( p j ) L(p_{j}) L(pj)是页面 p j p_j pj链出页面的数量,而N是所有頁面的數量。
PageRank的计算过程:
什么时候迭代结束?
优点:
是一个与查询无关的静态算法,所有网页的PageRank值通过离线计算获得;有效减少在线查询时的计算量,极大降低了查询响应时间。
缺点:
Generalize在机器学习中是泛化的意思,也译作推广。
When To Use(适用场景):
PageRank可以应用于很多领域。以下是一些值得注意的用例:
when Not to use:
不适宜的场景比如:
如果您在运行算法时看到了意外的结果,那么有必要对图进行一些探索性分析,看看这些问题是否是原因。你可以阅读谷歌 PageRank算法及其工作原理 来了解更多。
Ref:
https://blog.codinglabs.org/articles/intro-to-pagerank.html
https://www.biaodianfu.com/pagerank.html
https://neo4j.com/docs/graph-algorithms/current/algorithms/page-rank/#algorithms-pagerank-personalized
补充:
spark实现pagerank算法:
package org.apache.spark.examples
import org.apache.spark.sql.SparkSession
/**
* Computes the PageRank of URLs from an input file. Input file should
* be in format of:
* URL neighbor URL
* URL neighbor URL
* URL neighbor URL
* ...
* where URL and their neighbors are separated by space(s).
*
* This is an example implementation for learning how to use Spark. For more conventional use,
* please refer to org.apache.spark.graphx.lib.PageRank
*
* Example Usage:
* {{{
* bin/run-example SparkPageRank data/mllib/pagerank_data.txt 10
* }}}
*/
object SparkPageRank {
def showWarning() {
System.err.println(
"""WARN: This is a naive implementation of PageRank and is given as an example!
|Please use the PageRank implementation found in org.apache.spark.graphx.lib.PageRank
|for more conventional use.
""".stripMargin)
}
def main(args: Array[String]) {
if (args.length < 1) {
System.err.println("Usage: SparkPageRank " )
System.exit(1)
}
showWarning()
val spark = SparkSession
.builder
.appName("SparkPageRank")
.getOrCreate()
val iters = if (args.length > 1) args(1).toInt else 10
val lines = spark.read.textFile(args(0)).rdd
val links = lines.map{ s =>
val parts = s.split("\\s+")
(parts(0), parts(1))
}.distinct().groupByKey().cache()
var ranks = links.mapValues(v => 1.0)
for (i <- 1 to iters) {
val contribs = links.join(ranks).values.flatMap{ case (urls, rank) =>
val size = urls.size
urls.map(url => (url, rank / size))
}
ranks = contribs.reduceByKey(_ + _).mapValues(0.15 + 0.85 * _)
}
val output = ranks.collect()
output.foreach(tup => println(s"${tup._1} has rank: ${tup._2} ."))
spark.stop()
}
}