昨晚上不想做其他的事,突然想起来好久都没更新博客了,shell也差不多学完了,只不过学习的时候都是只带着书出去了,改天总结总结。Hadoop么,黄宜华老师讲完了,自己也马马虎虎快学完了,也是没总结,那今天就写下前段时间写的一个关于英文Wiki的PageRank代码吧。
PageRank的ABC
什么是PageRank
PageRank是一种在搜索引擎中根据网页之间相互的链接关系计算网页排名的技术。
PageRank是Google用来标识网页的等级或重要性的一种方法。其级别从1到10级,PR值越高说明该网页越受欢迎(越重要)。
PageRank的基本设计思想和原则
被许多优质网页所链接的网页,多半也是优质网页。
一个网页要想拥有较高的PR值的条件:
- 有很多网页链接到它;
- 有高质量的网页链接到它
PageRank的简化模型
可以把互联网上的各个网页之间的链接关系看成一个有向图。
对于任意网页Pi,它的PageRank值可表示为:
其中Bi为所有链接到网页i的网页集合,Lj为网页j的对外链接
简化模型面临的缺陷
实际的网络超链接环境没有这么理想化,PageRank会面临两个问题:
- Rank leak:就是有的网页没有链出网页,使用上面的公式时,循环几次后所有的PageRank值都变为0
- Rank sink:整个网页图中的一组紧密链接成环的网页如果没有外出的链接就产生Rank sink,即经过若干次循环后不在环中的网页的PR值会变为0
解决简化模型的缺陷的解决办法——采用随机浏览模型:
假定一个上网者从一个随机的网页开始浏览
上网者不断点击当前网页的链接开始下一次浏览
但是,上网者最终厌倦了,开始了一个随机的网页
随机上网者用以上方式访问一个新网页的概率就等于这个网页的PageRank值
这种随机模型更加接近于用户的浏览行为
采用随机浏览模型后的PR值计算公式变为:
其中d为按照超链进行浏览的概率,1-d即为用户随机跳转一个新网页的概率,显然跳转到每个网页的概率为(1-d)/N,N为所有网页的数目。
依据上述公式,一般经过10次左右的计算,每一个网页可以得到一个稳定的PR值(数学证明使用马尔可夫链收敛定理,不太懂,有兴趣的同学可以关注下)
使用MapReduce实现PageRank
显然这样一个需要进行大量累加计算的工作是适合用MapReduce来做的,那么怎么做呢?分三个阶段完成这个事情:
Phase1: GraphBuilder
建立网页之间的超链接图
Phase2: PageRankIter
迭代计算各个网页的PageRank值
Phase3: RankViewer
按PageRank值从大到小输
具体设计:
En_wiki中每行对应于一个页面,页面的标题包含在“<title>Name of thearticle</title>”中,每个页面的链出标题在一对双中括号中("[[Name of other article]]"),我们需要做的就是用PageRank算法给出每个页面的重要性,并按重要程度从高到低输出。首先要做的就是构建每个页面的链接关系图,这个工作我们GraphBuilder类来实现,给出的结构类似于图的邻接表表示方式;第二步需要做的工作就是用PageRank算法经过一定次数的迭代,得出最终每个页面的PRValue,具体算法在PageRankler类中实现;第三步需要将结果展现出来,并按“PRvalue title”的格式降序输出,类PageRankViewer实现该功能;最后,需要组织整个算法,这是main函数完成的功能。
以下具体阐述每个类的具体实现:
1. 整体类视图
2. GraphBuilder类
map中完成链接图的构造。首先需解决的问题就是如何提取出title及linktitle,我们使用正则表达式来完成:"(?<=<title>)(.*?)(?=</title>)"来完成对title的匹配,使用"(?<=\\[\\[)(.+?)(?=\\]\\])"来完成对linktitle的匹配。此处需要主要注意的在第二个匹配模式中,由于'['和']'属于正则表达式中的元字符,因此需要对其进行转义,在Java语言的字符串中还要对转义字符进行一次转义,因此会出现两个反斜杠。提取完成之后,key设为title,初始PRValue和链出列表以“PRValue%%%%link1 link2 link3 ...”的格式作为value发射出去。key和value的类型都是Text。
reduce阶段不需要做额外的工作,将map阶段给的键值队原样发射出去即可,可以使用默认的reduce来做,此处还是自己实现。
main11函数完成整个job的参数设置及运行该job,如果使用默认的reduce,此处需要具体设置map输出的key、value以及输出的key、value的类型,否则会出现一个类型不匹配的错误。由于输出文件还要在下一个job中作为输入,因此输出类型设为二进制类型(SequenceFileOutputFormat),在下一步中奖输入格式设为二进制后,map可以自己识别出key和value。另外需要做的工作就是,如果输出目录存在需要将其删除,然后开始执行job。
3. PageRankler类
map的输入和输出的key和value都为Text类型,需要做的工作是继续维护图的结构以及“分饼”工作,即将自己的PRValue均分到每个链出的title上。从读入的value中得到自己的PRValue和链出的title,由于value的格式为:“PRValue%%%%%%link1 link2 link3 ...”因此用两次split就可以得到PRValue和linktitle数组。对linktitle数组中的每个元素都分配PRValue并发射出去,发射的key为linkTitle,value为PRValue/linktitle.length(),最后将图的结构“link1 link2 link3 …”发射出去。
需要自己定制combiner,因为一个页面中出现相同的linktitle是有可能的。如果发来的图结构,不处理直接发射出去,如果不是,进行简单的累加后发射。
redece阶段对title从每个链接源得到的PR值进行合并得到PRValue,然后按照公式:newPR = (1 - d) + d * PRvalue得到newPR,注意此处随机浏览的概率设置为1-d而不是(1-d)/N,主要是相对非常大的N,(1-d)/N小到可以忽略。刚开始的时候,我试图在GraphBuilder中的map中得到N,设置了一个全局静态变量,在map处理一行的时候对该变量加一,在本机上这个机制是有作用的,但是到集群上之后,得到的所有PRValue都是无穷大,后来领悟到:map是在各个节点上做的,所以N值不会变化,而是初始的值0,后来自己想写一个job得到N,发现没有任何意义,舍弃之。得到新的PRValue后,将value设为“PRValue%%%%link1 link2 link3 ...”发射,供下轮job使用。
isDouble函数完成对一个字符串是否为一个浮点数进行判断,这个函数用在map阶段,用于判断一个value是图结构还是分到的PRValue。
main11对整个job进行设置,输入输出格式都设为二进制,其他的与上个阶段的main11做的事基本相同。
4. PageRankViewer类
map阶段所做的事是将value中的PRValue提取出来,然后将key设为PRValue,value设为title,发射。
因为要求按降序输出,故此处还要写自己的Comparator,只需要返回父类的相关函数的返回值的相反值即可。
main11函数设置相关的参数和输入输出值的类型,基本同上个类。
5. main函数
main函数完成对所有job流程的控制,包括设置输入输出路径以及参数检查,还有设置PageRank的迭代次数等等。
运行结果:
本次实验将所有title的初始PRValue设置为0.5,阻尼系数设置为0.85,PageRank迭代次数设为10次,在集群上的运行结果的前30个title列表及其PRValue值如下图所示:
代码就不贴了,按照这个思路实现起来应该很简单的