PageRank 算法在 spark 上的简单实现

本文介绍了一个使用Scala和Spark实现的PageRank算法实验。实验基于4个页面组成的简单网络,展示了如何通过几行代码实现PageRank,并详细解释了代码的工作原理及优化策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

1. 实验环境

2. PageRank 算法简介

3. 模拟数据

4. 测试代码

5. 代码说明


        在《Spark快速大数据分析》里有一段不明觉厉的 Scala 代码,只用了区区几行即实现了 Google 的 PageRank 算法,于是照猫画虎做了个小实验验证了一下。

1. 实验环境

spark 1.5.0

2. PageRank 算法简介

        摘自《Spark快速大数据分析》。

        PageRank 是执行多次连接的一个迭代算法,因此它是 RDD 分区操作的一个很好的用例。算法会维护两个数据集:一个由(pageID,linkList)的元素组成,包含每个页面的相邻页面的列表;另一个由(pageID,rank)元素组成,包含每个页面的当前排序值。它按如下步骤进行计算。

  1. 将每个页面的排序值初始化为 1.0。
  2. 在每次迭代中,对页面 p,向其每个相邻页面(有直接链接的页面)发送一个值为 rank(p)/numNeighbors(p) 的贡献值。
  3. 将每个页面的排序值设为 0.15 + 0.85 * contributionsReceived。

        最后两个步骤会重复几个循环,在此过程中,算法会逐渐收敛于每个页面的实际 PageRank值。在实际操作中,收敛通常需要大约 10 轮迭代。

3. 模拟数据

        假设一个由 4 个页面组成的小团体:A,B,C 和 D。相邻页面如下所示:

A:B C
B:A C
C:A B D
D:C

4. 测试代码

import org.apache.spark.HashPartitioner

val links = sc.parallelize(List(("A",List("B","C")),("B",List("A","C")),("C",List("A","B","D")),("D",List("C")))).partitionBy(new HashPartitioner(100)).persist()

var ranks=links.mapValues(v=>1.0)

for (i <- 0 until 10) {
val contributions=links.join(ranks).flatMap {
case (pageId,(links,rank)) => links.map(dest=>(dest,rank/links.size))
}
ranks=contributions.reduceByKey((x,y)=>x+y).mapValues(v=>0.15+0.85*v)
}

ranks.sortByKey().collect()

        执行结果如下图所示。

        初始的 linksRDD 和 ranksRDD 如下所示:
linksRDD:
Array[(String, List[String])] = Array((A,List(B, C)), (B,List(A, C)), (C,List(A, B, D)), (D,List(C)))
ranksRDD:
Array[(String, Double)] = Array((A,1.0), (B,1.0), (C,1.0), (D,1.0))

        首次迭代后的 contributionsRDD 和 ranksRDD 如下所示:
contributionsRDD:
Array[(String, Double)] = Array((A,0.5), (A,0.3333333333333333), (B,0.5), (B,0.3333333333333333), (C,0.5), (C,0.5), (C,1.0), (D,0.3333333333333333))
ranksRDD:
Array[(String, Double)] = Array((A,0.8583333333333333), (B,0.8583333333333333), (C,1.8499999999999999), (D,0.43333333333333335))

        第 1 次迭代:
PR(A)=0.15 + 0.85 * (1/2 + 1/3) = 0.858333
PR(B)=0.15 + 0.85 * (1/2 + 1/3) = 0.858333
PR(C)=0.15 + 0.85 * (1/2 + 1/2 + 1/1) = 1.85
PR(D)=0.15 + 0.85 * (1/3) = 0.433333

        第 2 次迭代:
PR(A)=0.15 + 0.85 * (0.858333/2 + 1.85/3) = 1.038958191100
PR(B)=0.15 + 0.85 * (0.858333/2 + 1.85/3) = 1.038958191100
PR(C)=0.15 + 0.85 * (0.858333/2 + 0.858333/2 + 0.433333/1) = 1.247916100000
PR(D)=0.15 + 0.85 * (1.85/3) = 0.67416667

        第 3 次迭代:
PR(A)=0.15 + 0.85 * (1.038958191100/2 + 1.247916100000/3) = 0.945133459550833333
PR(B)=0.15 + 0.85 * (1.038958191100/2 + 1.247916100000/3) = 0.945133459550833333
PR(C)=0.15 + 0.85 * (1.038958191100/2 + 1.038958191100/2 + 0.67416667/1) = 1.606156131935000000
PR(D)=0.15 + 0.85 * (1.247916100000/3) = 0.503576228333333333

5. 代码说明

        摘自《Spark快速大数据分析》。

        这就行了!算法从将 ranksRDD 的每个元素的值初始化为 1.0 开始,然后在每次迭代中不断更新 ranks 变量。在 Spark 中编写 PageRank 的主体相当简单:首先对当前的 ranksRDD 和静态的 linkRDD 进行一次 join() 操作,来获取每个页面 ID 对应的相邻页面列表和当前的排序值,然后使用 flatMap 创建出“contributions”来记录每个页面对各个相邻页面的贡献。然后再把这些贡献值按照页面 ID(根据获得共享的页面)分别累加起来,把该页面的排序值设为 0.15 + 0.85 * contributionsReceived。

        虽然代码本身很简单,这个示例程序还是做了不少事情来确保 RDD 以比较高效的方式进行分区,以最小化通信开销:

(1)请注意,linksRDD 在每次迭代中都会和 ranks 发生连接操作。由于 links 是一个静态数据集,所以我们在程序一开始的时候就对它进行了分区操作,这样就不需要把它通过网络进行数据混洗了。实际上,linksRDD 的字节数一般来说也会比 ranks 大得多,毕竟它包含每个页面的相邻页面列表(由页面 ID 组成),而不仅仅是一个 Double 值,因此这一优化相比 PageRank 的原始实现(例如普通的 MapReduce)节约了相当可观的网络通信开销。

(2)出于相同的原因,我们调用 links 的 persist() 方法,将它保留在内存中以供每次迭代使用。

(3)当我们第一次创建 ranks 时,我们使用 mapValues() 而不是 map() 来保留父 RDD(links)的分区方式,这样对它进行的第一次连接操作就会开销很小。

(4)在循环体中,我们在 reduceByKey() 后使用 mapValues();因为 reduceByKey() 的结果已经是哈希分区的了,这样一来,下一次循环中将映射操作的结果再次与 links 进行连接操作时就会更加高效。

        scala 这语言是真的很简洁,大数据上的通用示例程序 wordcount,用 scala 写一行搞定,如下图所示。

var input = sc.textFile("/NOTICE.txt")
input.flatMap(x=>x.split(" ")).countByValue()

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值