算法系列:PageRank算法的MapReduce实现
首先简单介绍PageRank的算法公式:
(图片来源:http://en.wikipedia.org/wiki/Page_rank)
PR(A)即A的PageRank值;d为阻尼因子,一般设为0.85;L(B)即B网站所有的出链数量(即B网站内的所有链接的数量)。
所以公式的意义是:A的PageRank值=(1-d)+d*(链接到A的所有网站的PR值/该网站的所有出链数量之和)。这里首次计算时为每个链接附上一个初始值,我设的是0.85。
公式相对还是比较简单的,至于原理推荐几篇自己学习参考的文章:http://en.wikipedia.org/wiki/Page_rank(英文),http://www.cnblogs.com/FengYan/archive/2011/11/12/2246461.html(讲解原理,不是太容易懂)。
下面主要讲解mapreduce的实现。
- 输入文件格式:
a b,c,d,e,f,g,h b a,c,d,r,g c s,f,g,w,h,b d f,e,s,t,g,a e f,s,a,c,t,g,h f d,s,a,q,v,g,h g d,e,t,g,h,j,y h d,e,t,g,h,y,j i d,w,a,c,d,s j a,c,v,f,d,s k d,f,h,r,s,a
其中字母代表链接,第一个字母是网站链接,其后的字符串代表该网站所有的出链,以','分割。
- Mapper的实现:
public class PageRankMapper extends Mapper<Text, Text, Text, Text> { private static final Log log = LogFactory.getLog(PageRankMapper.class); public static float factor = 0.85f;// 阻尼因子 @Override protected void setup(Context context) throws IOException, InterruptedException { // 获取阻尼因子值,默认0.85 factor = context.getConfiguration().getFloat("mapred.pagerank.factor", 0.85f); } @Override protected void map(Text key, Text value, Context context) throws IOException, InterruptedException { log.info(key.toString() + ";" + value.toString()); // 输入文件格式为:A b,c,d,... // 即key为目标网站,value为A所有的出链,并以','分割 String[] outLinks = value.toString().split(","); // 分割key,获取key的rank值,以','分割 String[] link = key.toString().split(","); float rank = factor; if (link.length > 1) { rank = Float.parseFloat(link[1]);// 存在rank值,则取得,不存在则设为默认值:阻尼因子 } int outLinkLen = outLinks.length;// A的出链数量 // 遍历A所有的出链,输出格式:key-A的各个出链(b,c,d...);value-[A+','+rank+','+outLinkLen] // 得到每个链接的所有入链的PR值以及链接到该链接的链接的出链数 for (String s : outLinks) { context.write(new Text(s), new Text(link[0] + ";" + rank + ";" + outLinkLen)); } // 还需要输出A的所有出链信息,以便进行下一次mapreduce任务的处理 context.write(new Text(link[0]), value); } }这是Mapper的实现代码,根据输入文件,这里选择KeyValueInputFormat,读入的key为一个网站链接,而value为该网站的所有出链,并以','分割。对value进行按','进行分割,得到该网站所有的出链,然后将出链作为key,value为之前的key+','+其rank值。这里注意,因为第一次计算时所有网站无rank值,故设置一个初始值,我这里设的是0.85(同阻尼因子)。同时还需要将输入的key+rank值以及输入的value一起输出,以便下次job的执行。
说明一下,除了第一次job之外,其他的job输入文件的格式其实如下:
a,0.85 b,c,d,e,f,g,h b,0.85 a,c,d,r,g c,0.85 s,f,g,w,h,b d,0.85 f,e,s,t,g,a e,0.85 f,s,a,c,t,g,h f,0.85 d,s,a,q,v,g,h g,0.85 d,e,t,g,h,j,y h,0.85 d,e,t,g,h,y,j i,0.85 d,w,a,c,d,s j,0.85 a,c,v,f,d,s k,0.85 d,f,h,r,s,a相比第一次的输入文件,这里的输入文件的key发生了变化,key是网站链接+','+其rank值组成,初始时无初始值,所以没有后面的。
Reducer的实现:
public class PageRankReducer extends Reducer<Text, Text, Text, Text> { private static final Log log = LogFactory.getLog(PageRankReducer.class); public static float factor = 0.85f;// 阻尼因子 @Override protected void setup(Context context) throws IOException, InterruptedException { // 获取阻尼因子值,默认0.85 factor = context.getConfiguration().getFloat("mapred.pagerank.factor", 0.85f); } @Override protected void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException { log.info(key.toString()); float rank = 1 - factor;// PageRank值 String[] str; Text outLinks = new Text();// 记录该链接的所有出链信息 // 集合的数据位key的所有入链链接的page,rank,count值,以及key的所有出链信息 for (Text t : values) { // 入链信息以';'分割,出链信息以','分割,以此区别 str = t.toString().split(";"); if (str.length == 3) { // 计算key的rank值=(1-d)+d*key的入链rank值/其出链数 rank += Float.parseFloat(str[1]) / Integer.parseInt(str[2]) * factor; } else { outLinks.set(t.toString()); } } context.write(new Text(key.toString() + "," + rank), outLinks); } }以上是Reducer的实现代码。经过Mapper阶段的计算,到达Reducer这里的数据结构是:key-->{page1;rank1;count1,page2;rank2;count2,...,page1,page2...}。
根据key的所有入链的rank值和其对应的出链数量,可以轻松的计算出key的rank值,然后将key+‘,’+rank作为key,value则是[page1,page2,...pagen],输出到文件,在作为下次job的输入文件进行计算,也就得到了上面提到的输入文件格式。
主要代码就这两个,剩下的代码主要完成多次迭代计算的功能,就不贴出来了,想参考的可以从点击打开链接(网盘)下载完整代码。
欢迎大家一起讨论学习,谢谢
一路随行,伴尔成长