基本概念
首先了解下与图相关的概念
图(Graph)由顶点(Vertex)和边(Edge)组成
图根据边是否有方向,可以分为有向图和无向图
有向图:
无向图:
根据是否构成环形(并不是指边和边组成了封闭的图形就叫是有环,而是指从某个顶点出发,经过若干边后可以回到该顶点),分为有环图和无环图
有环图:
无环图:
度:一个顶点,与其连接的边的数量,就叫做该顶点的度
出度:指从当前顶点指向其他顶点的边的数量,相对应的,从当前顶点指向其他顶点的边就叫做当前顶点的出边
入度:指其他顶点指向当前顶点的数量,相对应的,从其他顶点指向当前顶点的边就叫做当前顶点的入边
GraphX简介
SparkGraphX是Spark提供的分布式图计算API,通过弹性分布式属性图(Property Graph)统一了图试图和表视图,可以与Spark Streaming、Spark SQL和Spark MLlib无缝衔接。
对graph视图的全部操作,最终都会转换成其关联的Table视图的RDD操作来完成
演示案例
导入依赖
<!--根据实际使用版本导入-->
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.11</artifactId>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql_2.11</artifactId>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-graphx_2.11</artifactId>
<version>${spark.version}</version>
</dependency>
创建Graph对象
以创建下图的graph图对象为例
import org.apache.spark.graphx.{Edge, Graph, VertexId}
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object CreateGraph {
def main(args: Array[String]): Unit = {
// 创建SparkContext对象
val sc: SparkContext = SparkContext.getOrCreate(new SparkConf().setMaster("local[*]").setAppName("CreateGraph"))
// 创建保存顶点信息的RDD
val users: RDD[(VertexId, (String, String))] =
sc.parallelize(Array((3L, ("rxin", "student")), (7L, ("jgonzal", "postdoc")),
(5L, ("franklin", "prof")), (2L, ("istoica", "prof"))))
// 创建保存边信息的RDD
val relationships: RDD[Edge[String]] =
sc.parallelize(Array(Edge(3L, 7L, "collab"), Edge(5L, 3L, "advisor"),
Edge(2L, 5L, "colleague"), Edge(5L, 7L, "pi")))
// 定义一个默认用户
val defaultUser = ("John Doe", "Missing")
// 构建图对象
val graph = Graph(users, relationships, defaultUser)
// 测试图对象
graph.triplets.foreach(triple => println(s"(${triple.srcId},${triple.srcAttr}) =(${triple.attr})=> (${triple.dstId},${triple.dstAttr})"))
}
}
打印结果:
(3,(rxin,student)) =(collab)=> (7,(jgonzal,postdoc))
(5,(franklin,prof)) =(pi)=> (7,(jgonzal,postdoc))
(5,(franklin,prof)) =(advisor)=> (3,(rxin,student))
(2,(istoica,prof)) =(colleague)=> (5,(franklin,prof))
介绍下triplets
,其实就是起点、边和终点组成的三元组,相对于Edges
多了两个顶点的信息
除了使用RDD定义顶点和边,最后组成图,也可以调用GraphLoader的edgeListFile从文件中加载图信息
源码文档中给出了读取文件的格式:每行代表一条边,每行有两个数字,用空格分隔,前后两个数字分别代表起始点ID和终点ID
* Loads a graph from an edge list formatted file where each line contains two integers: a source
* id and a target id. Skips lines that begin with `#`.
*
* If desired the edges can be automatically oriented in the positive
* direction (source Id is less than target Id) by setting `canonicalOrientation` to
* true.
例如
1 2
2 3
3 1
打印获得的图对象
val graph2 = GraphLoader.edgeListFile(sc,"in/a.txt")
graph2.triplets.collect().foreach(println)
输出结果:
((1,1),(2,1),1)
((2,1),(3,1),1)
((3,1),(1,1),1)
GraphAPI
能够创建Graph对象了,我们看下使用graph对象能够做什么:
class Graph[VD, ED] {
// graph相关信息
val numEdges: Long // 获取图中边数量
val numVertices: Long // 获取图中顶点数量
val inDegrees: VertexRDD[Int] // 获取图中所有顶点的入度
val outDegrees: VertexRDD[Int] // 获取图中所有顶点的出度
val degrees: VertexRDD[Int] // 获取图中所有顶点的度
// 以集合方式查看graph信息
val vertices: VertexRDD[VD] // 返回包含图中vertex的RDD
val edges: EdgeRDD[ED] // 返回包含图中edge的RDD
val triplets: RDD[EdgeTriplet[VD, ED]] // 返回包含图中triplet的RDD
// 图的持久化操作
def persist(newLevel: StorageLevel = StorageLevel.MEMORY_ONLY): Graph[VD, ED] // 自定义方式的持久化
def cache(): Graph[VD, ED] // 持久化到内存
def unpersistVertices(blocking: Boolean = true): Graph[VD, ED] // 清除持久化数据
// 根据给定的分区策略对边重分区
def partitionBy(partitionStrategy: PartitionStrategy): Graph[VD, ED]
// 修改顶点/边的属性,就类似于RDD的map操作,对图中全部顶点/边执行给定的转换操作
def mapVertices[VD2](map: (VertexId, VD) => VD2): Graph[VD2, ED]
def mapEdges[ED2](map: Edge[ED] => ED2): Graph[VD, ED2]
def mapEdges[ED2](map: (PartitionID, Iterator[Edge[ED]]) => Iterator[ED2]): Graph[VD, ED2]
def mapTriplets[ED2](map: EdgeTriplet[VD, ED] => ED2): Graph[VD, ED2]
def mapTriplets[ED2](map: (PartitionID, Iterator[EdgeTriplet[VD, ED]]) => Iterator[ED2])
: Graph[VD, ED2]
// 改变graph结构
def reverse: Graph[VD, ED] // 反转图中所有边的方向
def subgraph( // 获取符合给定条件的子图
epred: EdgeTriplet[VD,ED] => Boolean = (x => true), // 指定边条件
vpred: (VertexId, VD) => Boolean = ((v, d) => true)) // 指定定点条件
: Graph[VD, ED]
def mask[VD2, ED2](other: Graph[VD2, ED2]): Graph[VD, ED] // 顶点和边与另外一个图取“交集”,但是保留本图的属性值
def groupEdges(merge: (ED, ED) => ED): Graph[VD, ED]
// Join操作,注意:两个join和表与表之间的join完全不同
// 传入一个包含vertexId和属性二元组的RDD,根据vertexId和图中顶点匹配,执行传入的操作(最终顶点数据格式不会发生变化),没有匹配上id的vertex会保留原有值
def joinVertices[U](table: RDD[(VertexId, U)])(mapFunc: (VertexId, VD, U) => VD): Graph[VD, ED]
// 和joinVertice类似,只是传入的mapFunc中数据类型由U变为了Option[U],即没有关联上id的vertex也可以选择要执行的操作
def outerJoinVertices[U, VD2](other: RDD[(VertexId, U)])
(mapFunc: (VertexId, VD, Option[U]) => VD2)
: Graph[VD2, ED]
// 相邻边/顶点属性聚合操作,这部分是图相关算法的基础
// 查看各顶点和那些顶点相邻
def collectNeighborIds(edgeDirection: EdgeDirection): VertexRDD[Array[VertexId]]
// 类似于collectNeighborIds,区别在于不仅是返回vertexId,而是返回整个vertex
def collectNeighbors(edgeDirection: EdgeDirection): VertexRDD[Array[(VertexId, VD)]]
// 对图中每一个顶点的相邻顶点和边执行一次自定义的聚合操作,图算法的常用函数
def aggregateMessages[Msg: ClassTag](
sendMsg: EdgeContext[VD, ED, Msg] => Unit,
mergeMsg: (Msg, Msg) => Msg,
tripletFields: TripletFields = TripletFields.All)
: VertexRDD[A]
// 可迭代的pregel运算,后面详细解释
def pregel[A](initialMsg: A, maxIterations: Int, activeDirection: EdgeDirection)(
vprog: (VertexId, VD, A) => VD,
sendMsg: EdgeTriplet[VD, ED] => Iterator[(VertexId,A)],
mergeMsg: (A, A) => A)
: Graph[VD, ED]
// 基本的图算法
def pageRank(tol: Double, resetProb: Double = 0.15): Graph[Double, Double] // pageRank算法,后面详细解释
def connectedComponents(): Graph[VertexId, ED] // 求连通分量
def triangleCount(): Graph[Int, ED] // 求出图中每个顶点经过三角形的个数
def stronglyConnectedComponents(numIter: Int): Graph[VertexId, ED] // 求强连通分量
}
以上这些就是定义在Graph
和GraphOps
中外部可调用的几乎全部(部分重载和类似功能除外)功能了
PageRank
PageRank算法是一个用于评估以超链接为基础的网页重要性的算法
算法简单描述:
- PageRank算法认为起始访问每个网页的概率相等,且从某个网页的超链接访问其他网页的概率相等
- 以PR值代表某个网页的评估权重,值越大,对应的网页越重要
- PR值计算公式:
v'=αMv+(1-α)e
- v’:本次迭代结束后页面PR值向量
- α:基尼系数,跳转到当前页面(包含当前页面上的链接)的概率,一般取0.85
- M:从某个网页通过超链接去往另一个网页的转移矩阵
- v:包含每个网页当前权重的向量
- e:网页数目的倒数
- 如果一个网页有链接到其他三个网页的超链接,那么其他三个网页都可以获得该页面PR值的三分之一
- 但是,如果该网页没有超链接,那么经过多轮迭代后,最终所有PR值都会向0收敛,所以此时认为该网页有通向所有网页的超链接
- α可理解为不使用网址跳转,遵循超链接跳转的规则的概率
PageRank算法在GraphX中一系列的方法,使用方式很简单,大部分传入迭代次数就可以了
def pageRank(tol: Double, resetProb: Double = 0.15): Graph[Double, Double]
一般迭代10次,PR值可以基本收敛
以下图为例,演示pageRank效果:
案例代码:
object PageRank {
def main(args: Array[String]): Unit = {
val spark = SparkSession.builder().master("local[*]").appName("graph").getOrCreate()
val sc = spark.sparkContext
val users = sc.makeRDD(Array(
(1L, ("Alice", 28)),
(2L, ("Bob", 27)),
(3L, ("Charlie", 65)),
(4L, ("David", 42)),
(5L, ("Ed", 55)),
(6L, ("Fran", 42))))
val relationship = sc.makeRDD(Array(
Edge(2L, 1L, 7),
Edge(3L, 2L, 4),
Edge(4L, 1L, 1),
Edge(2L, 4L, 2),
Edge(5L, 2L, 2),
Edge(5L, 3L, 8),
Edge(5L, 6L, 3),
Edge(3L, 6L, 3)
))
val graph = Graph(users, relationship)
graph.pageRank(0.001).vertices.foreach(println)
}
}
输出结果:
(4,0.9688717814927127)
(2,0.9969646507526427)
(5,0.5451618049228395)
(1,1.7924127957615184)
(3,0.6996243163176441)
(6,0.9969646507526427)
算法原理解释:https://www.jianshu.com/p/7485cac02e95
pageRank的优点在于简单有效,且全部网页的PR值可以通过离线计算获取,有效的减少了在线查询的计算量。
同时pageRank也有缺点,一方面PageRank忽略了主题相关性,导致结果的相关性和主题性降低;另一方面,旧的页面等级会比新的页面高。因为即使是非常好的新页面也不会有很多的上游链接。
Pregel
Pregel算法一般用来求最短路径问题,Spark Graph中该算法实际底层调用的mapReduceTriplets
和aggregateMessagesWithActiveSet
,依靠一轮轮的迭代完成的
Pregel中,图的顶点有两种状态:
- 钝化状态:类似于休眠,激活状态的顶点若本轮迭代中未成功发送或接收消息的顶点在下次迭代中将变为钝化状态
- 激活状态:工作状态,每轮迭代都将尝试向周边顶点发送消息,钝化状态的顶点在成功接收消息后在下次迭代中将变为激活状态
Pregel算法流程:
- 以顶点的属性值代表与起点的距离,所以初始给起点赋值为0,其余顶点赋值一个极大值。
所有顶点起始状态为激活状态 - 以边的属性代表src到dst的距离,消息发送成功的标准为发送端属性与边属性之和小于接受端属性。
接收端接收到消息后,会更新自己的属性值 - 第一轮遍历后,由于除起点外其他顶点属性值均为极大值,只有起点和起点能直接到达的点处于激活状态,其他顶点切换至钝化状态
- 多轮迭代中,起点和靠近起点的顶点将逐渐进行钝化状态,远离起点的顶点将进入激活状态
- 最终,所有顶点都将进入钝化状态,当所有顶点进入钝化状态后,计算结束。此时所有顶点的属性值即为到起点的距离
pregel函数签名:
def pregel[A: ClassTag](
initialMsg: A, // 初始消息,开始运行时会将这条消息发送给所有顶点
maxIterations: Int = Int.MaxValue, // 最大迭代次数
activeDirection: EdgeDirection = EdgeDirection.Either)( // 边的活跃方向
vprog: (VertexId, VD, A) => VD, // 每个顶点执行的操作,处理接受的消息
sendMsg: EdgeTriplet[VD, ED] => Iterator[(VertexId, A)], // 每个triple执行的操作,判断是否发送消息以及向哪个方向发送什么消息
mergeMsg: (A, A) => A) // 一个顶点接收多条消息的处理函数
: Graph[VD, ED]
仍以上面案例的图对象为例,求各节点到5结点的最小距离
object PregelTest{
def main(args: Array[String]): Unit = {
val spark = SparkSession.builder().master("local[*]").appName("graph").getOrCreate()
val sc = spark.sparkContext
val users = sc.makeRDD(Array(
(1L, ("Alice", 28)),
(2L, ("Bob", 27)),
(3L, ("Charlie", 65)),
(4L, ("David", 42)),
(5L, ("Ed", 55)),
(6L, ("Fran", 42))))
val relationship = sc.makeRDD(Array(
Edge(2L, 1L, 7),
Edge(3L, 2L, 4),
Edge(4L, 1L, 1),
Edge(2L, 4L, 2),
Edge(5L, 2L, 2),
Edge(5L, 3L, 8),
Edge(5L, 6L, 3),
Edge(3L, 6L, 3)
))
val graph = Graph(users, relationship)
val srcVertex = 5L
// 首先将起始点的属性改为0
val initGraph = graph.mapVertices {
case (vid, (name, age)) =>
if (vid == srcVertex) 0.0 else Double.PositiveInfinity
}
val pregelGraph = initGraph.pregel(
Double.PositiveInfinity,
Int.MaxValue,
EdgeDirection.Either // 按边的方向发送消息,OUT和EITHER都可以,不可以设置为IN和BOTH
)(
(_: VertexId, vd: Double, disMsg: Double) => math.min(vd, disMsg),
(edgeTriplet: EdgeTriplet[Double, PartitionID]) => {
if (edgeTriplet.srcAttr + edgeTriplet.attr < edgeTriplet.dstAttr) {
Iterator[(VertexId, Double)]((edgeTriplet.dstId, edgeTriplet.srcAttr + edgeTriplet.attr))
} else Iterator.empty
},
(msg1: Double, msg2: Double) => math.min(msg1, msg2)
)
pregelGraph.vertices.foreach(println)
}
}
输出结果
(5,0.0)
(4,4.0)
(6,3.0)
(2,2.0)
(3,8.0)
(1,5.0)
算法原理解释:https://blog.csdn.net/hanweileilei/article/details/89764466
aggregateMessages
aggregateMessages
函数是实现各种算法极其重要的一个函数,功能如同上文提到的,对图中每一个顶点的相邻顶点和边执行一次自定义的聚合操作。很抽象的功能但是非常强大
既然很多算法用到了该函数,下面就以pregel
算法为例,以aggregateMessages
函数演示上面pregel
案例中单轮迭代发生的操作
graph起始状态:
我们操作的目标状态:
下面直接上代码:
object OneIter {
def main(args: Array[String]): Unit = {
val spark = SparkSession.builder().master("local[*]").appName("graph").getOrCreate()
val sc = spark.sparkContext
val users = sc.makeRDD(Array(
(1L, ("Alice", 28)),
(2L, ("Bob", 27)),
(3L, ("Charlie", 65)),
(4L, ("David", 42)),
(5L, ("Ed", 55)),
(6L, ("Fran", 42))))
val relationship = sc.makeRDD(Array(
Edge(2L, 1L, 7),
Edge(3L, 2L, 4),
Edge(4L, 1L, 1),
Edge(2L, 4L, 2),
Edge(5L, 2L, 2),
Edge(5L, 3L, 8),
Edge(5L, 6L, 3),
Edge(3L, 6L, 3)
))
val graph = Graph(users, relationship)
graph.mapVertices((id, _) => if (id == 5L) 0 else Double.PositiveInfinity) // 初始化graph
.aggregateMessages[Double](
edgeContext => { // 如果目标顶点属性小于当前顶点属性与边长之后,就向目标顶点发送消息
if (edgeContext.srcAttr + edgeContext.attr < edgeContext.dstAttr) {
edgeContext.sendToDst(edgeContext.srcAttr + edgeContext.attr)
}
},
math.min // 当前顶点的值和接收的值取较小值
).foreach(println)
}
}
输出结果:
(3,8.0)
(2,2.0)
(6,3.0)
因为其他顶点的属性没有发生变化,所以没有输出
实际pregel
调用的并非aggregateMessages
,而是aggregateMessagesWithActiveSet
,该方法为graph包私有,外部无法调用,实际逻辑类似于aggregateMessages
,只不过限定了每轮迭代的活跃边。该操作再搭配上缓存和清除缓存操作,就是pregel
每轮迭代的主要任务