Apache Spark【从无到有从有到无】【编程指南】【AS7】GraphX编程指南(略)

目录

1.概观

2.入门

3.属性图(The Property Graph)

3.1.示例属性图

4.图形运算符

4.1.摘要列表

4.2.属性运算符(Property Operators)

4.3.结构运算符(Structural Operators)

4.4.联接运算符(Join Operators)

4.5.邻域聚合

4.5.1.聚合消息(aggregateMessages)

4.5.2.地图减少三连体转换指南(Map Reduce Triplets Transition Guide(Legacy))

4.5.3.计算度信息(Computing Degree Information)

4.5.4.收集邻居(Collecting Neighbors)

4.6.缓存和取消缓存(Caching and Uncaching)

5.Pregel API

6.图形构建器(Graph Builders)

7.顶点和边缘RDD(Vertex and Edge RDDs)

7.1.VertexRDDs

7.2.EdgeRDDs

8.优化表示(Optimized Representation)

9.图算法(Graph Algorithms)

9.1.网页排名(PageRank)

9.2.连接组件(Connected Components)

9.3.三角计数(Triangle Counting)

10.例子

 


参考:官方文档

GraphX

1.概观

GraphX是Spark中用于图形和图形并行计算的新组件。在较高的层次上,GraphX 通过引入一个新的Graph抽象来扩展Spark RDD:一个定向的多图,其属性附加到每个顶点和边。为了支持图计算,GraphX公开了一组基本的操作符(例如,subgraphjoinVertices, 和aggregateMessages),以及所述的优化的变体Pregel API。此外,GraphX包含越来越多的图算法和 构建器,以简化图形分析任务。

 

2.入门

首先,您需要将Spark和GraphX导入项目,如下所示:

import org.apache.spark._
import org.apache.spark.graphx._
// To make some of the examples work we will also need RDD
import org.apache.spark.rdd.RDD

如果您不使用Spark shell,您还需要一个SparkContext。要了解有关Spark入门的更多信息,请参阅Spark快速入门指南

 

3.属性图(The Property Graph)

 property graph属性曲线图)是一个有向多重图与连接到每个顶点和边缘的用户定义的对象。有向多图是有向图,其可能有多个平行边共享相同的源和目标顶点。支持平行边缘的能力简化了在相同顶点之间可存在多个关系(例如,同事和朋友)的建模场景。每个顶点都由唯一的 64位长标识符(VertexId)键控。GraphX不对顶点标识符施加任何排序约束。类似地,边具有对应的源和目标顶点标识符。

属性图在vertex(VD)和edge(ED)类型上进行参数化。这些是分别与每个顶点和边相关联的对象的类型。

当GraphX是原始数据类型(例如,int,double等等)时,GraphX优化顶点和边缘类型的表示,通过将它们存储在专用数组中来减少内存占用。

在某些情况下,可能需要在同一图表中具有不同属性类型的顶点。这可以通过继承来完成。例如,要将用户和产品建模为二分图,我们可能会执行以下操作:

class VertexProperty()
case class UserProperty(val name: String) extends VertexProperty
case class ProductProperty(val name: String, val price: Double) extends VertexProperty
// The graph might then have the type:
var graph: Graph[VertexProperty, String] = null

与RDD一样,属性图是不可变的,分布式的和容错的。通过生成具有所需更改的新图来完成对图的值或结构的更改。请注意,原始图形的大部分(即,未受影响的结构,属性和索引)在新图形中重复使用,从而降低了此固有功能数据结构的成本。使用一系列顶点分区启发法将图形划分为执行程序。与RDD一样,图形的每个分区都可以在发生故障时在不同的机器上重新创建。

逻辑上,属性图对应于编码每个顶点和边的属性的一对类型集(RDD)。因此,图类包含访问图的顶点和边的成员:

class Graph[VD, ED] {
  val vertices: VertexRDD[VD]
  val edges: EdgeRDD[ED]
}

classes VertexRDD[VD]EdgeRDD[ED]延伸,并且被优化的版本RDD[(VertexId, VD)]RDD[Edge[ED]]分别。双方VertexRDD[VD]EdgeRDD[ED]提供围绕图形计算,并利用内部优化内置附加功能。我们在关于顶点和边缘RDD的部分中更详细地讨论了 VertexRDDVertexRDDEdgeRDDEdgeRDDAPI,但是现在它们可以被认为是形式的简单RDD: 和。RDD[(VertexId, VD)]RDD[Edge[ED]]

 

3.1.示例属性图

假设我们要构建一个由GraphX项目上的各种协作者组成的属性图。vertex属性可能包含用户名和职业。我们可以使用描述协作者之间关系的字符串来注释边:

å±æ§å¾

生成的图形将具有类型签名:

val userGraph: Graph[(String, String), String]

有许多方法可以从原始文件,RDD甚至合成生成器构建属性图,这些在图构建器一节中有更详细的讨论 。可能最常用的方法是使用 Graph对象。例如,以下代码从RDD集合构造图形:

// Assume the SparkContext has already been constructed
val sc: SparkContext
// Create an RDD for the vertices
val users: RDD[(VertexId, (String, String))] =
  sc.parallelize(Array((3L, ("rxin", "student")), (7L, ("jgonzal", "postdoc")),
                       (5L, ("franklin", "prof")), (2L, ("istoica", "prof"))))
// Create an RDD for edges
val relationships: RDD[Edge[String]] =
  sc.parallelize(Array(Edge(3L, 7L, "collab"),    Edge(5L, 3L, "advisor"),
                       Edge(2L, 5L, "colleague"), Edge(5L, 7L, "pi")))
// Define a default user in case there are relationship with missing user
val defaultUser = ("John Doe", "Missing")
// Build the initial Graph
val graph = Graph(users, relationships, defaultUser)

在上面的例子中,我们使用了Edgecase类。边缘具有a srcId和a dstId对应的源和目标顶点标识符。此外,Edge 该类具有attr存储edge属性的成员。

我们可以分别使用graph.vertices 和graph.edges成员将图解构为相应的顶点和边视图。

val graph: Graph[(String, String), String] // Constructed from above
// Count all users which are postdocs
graph.vertices.filter { case (id, (name, pos)) => pos == "postdoc" }.count
// Count all the edges where src > dst
graph.edges.filter(e => e.srcId > e.dstId).count
请注意,graph.vertices返回一个VertexRDD[(String, String)]扩展的 RDD[(VertexId, (String, String))],因此我们使用scala case表达式来解构元组。
另一方面,graph.edges返回EdgeRDD包含Edge[String]对象。我们也可以使用case类类型构造函数,如下所示
graph.edges.filter { case Edge(src, dst, prop) => src > dst }.count

了属性图的顶点和边视图外,GraphX还公开了三元组视图。三元组视图在逻辑上连接顶点和边缘属性,从而产生类的 RDD[EdgeTriplet[VD, ED]]包含实例EdgeTriplet。此 连接可以用以下SQL表达式表示:

SELECT src.id, dst.id, src.attr, e.attr, dst.attr
FROM edges AS e LEFT JOIN vertices AS src, vertices AS dst
ON e.srcId = src.Id AND e.dstId = dst.Id

或图形为:

Edge Triplet

EdgeTriplet类扩展Edge通过添加类srcAttr和 dstAttr分别包含源和目的地属性成员。我们可以使用图的三元组视图来呈现描述用户之间关系的字符串集合。

val graph: Graph[(String, String), String] // Constructed from above
// Use the triplets view to create an RDD of facts.
val facts: RDD[String] =
  graph.triplets.map(triplet =>
    triplet.srcAttr._1 + " is the " + triplet.attr + " of " + triplet.dstAttr._1)
facts.collect.foreach(println(_))

 

4.图形运算符

正如RDDS有这样的基本操作mapfilter以及reduceByKey,性能图表也有采取用户定义的函数基本运算符的集合,产生具有转化特性和结构的新图。已经定义了具有优化实现的核心运算符,并且在其中定义了Graph表示为核心运算符的组合的便捷运算符GraphOps。但是,由于Scala暗示操作符GraphOps会自动作为成员使用Graph。例如,我们可以通过以下方式计算每个顶点(在中定义GraphOps)的入度:

val graph: Graph[(String, String), String]
// Use the implicit GraphOps.inDegrees operator
val inDegrees: VertexRDD[Int] = graph.inDegrees

区分核心图操作的原因GraphOps是为了能够在将来支持不同的图表表示。每个图形表示必须提供核心操作的实现,并重用其中定义的许多有用操作GraphOps

 

4.1.摘要列表

下面是在这两个定义的功能的快速摘要Graph和 GraphOps而是呈现为为简单起见图的成员。请注意,某些功能签名已经简化(例如,删除了默认参数和类型约束),并且已删除了一些更高级的功能,因此请参阅API文档以获取正式的操作列表。

/** Summary of the functionality in the property graph */
class Graph[VD, ED] {
  // Information about the Graph ===================================================================
  val numEdges: Long
  val numVertices: Long
  val inDegrees: VertexRDD[Int]
  val outDegrees: VertexRDD[Int]
  val degrees: VertexRDD[Int]
  // Views of the graph as collections =============================================================
  val vertices: VertexRDD[VD]
  val edges: EdgeRDD[ED]
  val triplets: RDD[EdgeTriplet[VD, ED]]
  // Functions for caching graphs ==================================================================
  def persist(newLevel: StorageLevel = StorageLevel.MEMORY_ONLY): Graph[VD, ED]
  def cache(): Graph[VD, ED]
  def unpersistVertices(blocking: Boolean = true): Graph[VD, ED]
  // Change the partitioning heuristic  ============================================================
  def partitionBy(partitionStrategy: PartitionStrategy): Graph[VD, ED]
  // Transform vertex and edge attributes ==========================================================
  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]
  // Modify the graph structure ====================================================================
  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 RDDs with the graph ======================================================================
  def joinVertices[U](table: RDD[(VertexId, U)])(mapFunc: (VertexId, VD, U) => VD): Graph[VD, ED]
  def outerJoinVertices[U, VD2](other: RDD[(VertexId, U)])
      (mapFunc: (VertexId, VD, Option[U]) => VD2)
    : Graph[VD2, ED]
  // Aggregate information about adjacent triplets =================================================
  def collectNeighborIds(edgeDirection: EdgeDirection): VertexRDD[Array[VertexId]]
  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]
  // Iterative graph-parallel computation ==========================================================
  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]
  // Basic graph algorithms ========================================================================
  def pageRank(tol: Double, resetProb: Double = 0.15): Graph[Double, Double]
  def connectedComponents(): Graph[VertexId, ED]
  def triangleCount(): Graph[Int, ED]
  def stronglyConnectedComponents(numIter: Int): Graph[VertexId, ED]
}

 

4.2.属性运算符(Property Operators)

与RDD map运算符一样,属性图包含以下内容:

class Graph[VD, ED] {
  def mapVertices[VD2](map: (VertexId, VD) => VD2): Graph[VD2, ED]
  def mapEdges[ED2](map: Edge[ED] => ED2): Graph[VD, ED2]
  def mapTriplets[ED2](map: EdgeTriplet[VD, ED] => ED2): Graph[VD, ED2]
}

这些运算符中的每一个都生成一个新图形,其顶点或边缘属性由用户定义的map函数修改。

请注意,在每种情况下,图形结构都不受影响。这是这些运算符的关键特性,它允许结果图重用原始图的结构索引。以下片段在逻辑上是等效的,但第一个片段不保留结构索引,并且不会受益于GraphX系统优化:

val newVertices = graph.vertices.map { case (id, attr) => (id, mapUdf(id, attr)) }
val newGraph = Graph(newVertices, graph.edges)

相反,用于mapVertices保留索引:

val newGraph = graph.mapVertices((id, attr) => mapUdf(id, attr))

这些运算符通常用于初始化特定计算的图形或远离不必要的属性。例如,给定一个以out度为顶点属性的图形(我们稍后将描述如何构造这样的图形),我们为PageRank初始化它:

// Given a graph where the vertex property is the out degree
val inputGraph: Graph[Int, String] =
  graph.outerJoinVertices(graph.outDegrees)((vid, _, degOpt) => degOpt.getOrElse(0))
// Construct a graph where each edge contains the weight
// and each vertex is the initial PageRank
val outputGraph: Graph[Double, Double] =
  inputGraph.mapTriplets(triplet => 1.0 / triplet.srcAttr).mapVertices((id, _) => 1.0)

 

4.3.结构运算符(Structural Operators)

目前GraphX仅支持一组简单的常用结构运算符,我们希望将来可以添加更多。以下是基本结构运算符的列表。

class Graph[VD, ED] {
  def reverse: Graph[VD, ED]
  def subgraph(epred: EdgeTriplet[VD,ED] => Boolean,
               vpred: (VertexId, VD) => Boolean): Graph[VD, ED]
  def mask[VD2, ED2](other: Graph[VD2, ED2]): Graph[VD, ED]
  def groupEdges(merge: (ED, ED) => ED): Graph[VD,ED]
}

reverse运营商将返回逆转的所有边缘方向上的新图。例如,当尝试计算反向PageRank时,这可能很有用。由于反向操作不会修改顶点或边缘属性或更改边数,因此可以有效地实现,而无需数据移动或复制。

subgraph操作者需要的顶点和边缘的谓词,并返回包含只有满足谓词顶点的顶点的曲线图(评估为真),并且满足谓词边缘边缘并连接满足顶点谓词顶点。所述subgraph 操作员可在情况编号被用来限制图形以顶点和感兴趣的边缘或消除断开的链接。例如,在以下代码中,我们删除了断开的链接:

// Create an RDD for the vertices
val users: RDD[(VertexId, (String, String))] =
  sc.parallelize(Array((3L, ("rxin", "student")), (7L, ("jgonzal", "postdoc")),
                       (5L, ("franklin", "prof")), (2L, ("istoica", "prof")),
                       (4L, ("peter", "student"))))
// Create an RDD for edges
val relationships: RDD[Edge[String]] =
  sc.parallelize(Array(Edge(3L, 7L, "collab"),    Edge(5L, 3L, "advisor"),
                       Edge(2L, 5L, "colleague"), Edge(5L, 7L, "pi"),
                       Edge(4L, 0L, "student"),   Edge(5L, 0L, "colleague")))
// Define a default user in case there are relationship with missing user
val defaultUser = ("John Doe", "Missing")
// Build the initial Graph
val graph = Graph(users, relationships, defaultUser)
// Notice that there is a user 0 (for which we have no information) connected to users
// 4 (peter) and 5 (franklin).
graph.triplets.map(
  triplet => triplet.srcAttr._1 + " is the " + triplet.attr + " of " + triplet.dstAttr._1
).collect.foreach(println(_))
// Remove missing vertices as well as the edges to connected to them
val validGraph = graph.subgraph(vpred = (id, attr) => attr._2 != "Missing")
// The valid subgraph will disconnect users 4 and 5 by removing user 0
validGraph.vertices.collect.foreach(println(_))
validGraph.triplets.map(
  triplet => triplet.srcAttr._1 + " is the " + triplet.attr + " of " + triplet.dstAttr._1
).collect.foreach(println(_))

请注意,在上面的示例中,仅提供了顶点谓词。subgraph操作者默认为true如果不设置顶点或边谓词。

// Run Connected Components
val ccGraph = graph.connectedComponents() // No longer contains missing field
// Remove missing vertices as well as the edges to connected to them
val validGraph = graph.subgraph(vpred = (id, attr) => attr._2 != "Missing")
// Restrict the answer to the valid subgraph
val validCCGraph = ccGraph.mask(validGraph)

groupEdges操作者合并平行边在多重图(即,对顶点之间重复边缘)。在许多数字应用中,可以将平行边缘 (它们的权重组合)添加到单个边缘中,从而减小图形的大小。

 

4.4.联接运算符(Join Operators)

在许多情况下,有必要将外部集合(RDD)中的数据与图形连接起来。例如,我们可能有我们想要与现有图形合并的额外用户属性,或者我们可能希望将顶点属性从一个图形拉到另一个图形。可以使用连接运算符完成这些任务。下面我们列出了键连接运算符:

class Graph[VD, ED] {
  def joinVertices[U](table: RDD[(VertexId, U)])(map: (VertexId, VD, U) => VD)
    : Graph[VD, ED]
  def outerJoinVertices[U, VD2](table: RDD[(VertexId, U)])(map: (VertexId, VD, Option[U]) => VD2)
    : Graph[VD2, ED]
}

joinVertices运算符连接与输入RDD顶点并返回与通过应用用户定义获得的顶点属性的新图形map函数到连接的顶点的结果。RDD中没有匹配值的顶点保留其原始值。

请注意,如果RDD包含给定顶点的多个值,则只使用一个。因此,建议使用以下内容使输入RDD唯一,这也将对结果值进行预索引,以显着加速后续连接。

val nonUniqueCosts: RDD[(VertexId, Double)]
val uniqueCosts: VertexRDD[Double] =
  graph.vertices.aggregateUsingIndex(nonUnique, (a,b) => a + b)
val joinedGraph = graph.joinVertices(uniqueCosts)(
  (id, oldCost, extraCost) => oldCost + extraCost)

更一般的outerjoinVertices与joinVertices的行为类似,只是用户定义的映射函数应用于所有顶点,并且可以更改顶点属性类型。因为并非所有顶点在输入RDD中都有匹配的值,所以map函数采用选项类型。例如,我们可以通过用顶点属性的Outdegree初始化顶点属性来为PageRank建立一个图形。

val outDegrees: VertexRDD[Int] = graph.outDegrees
val degreeGraph = graph.outerJoinVertices(outDegrees) { (id, oldAttr, outDegOpt) =>
  outDegOpt match {
    case Some(outDeg) => outDeg
    case None => 0 // No outDegree means zero outDegree
  }
}

您可能已经注意到上面示例中使用的多参数列表(例如,f(a)(b))curried函数模式。虽然我们可以将f(a)(b)等效为f(a,b),但这意味着b上的类型推断不依赖a。因此,用户需要为用户定义的函数提供类型注释:

val joinedGraph = graph.joinVertices(uniqueCosts,
  (id: VertexId, oldCost: Double, extraCost: Double) => oldCost + extraCost)

 

4.5.邻域聚合

许多图形分析任务中的关键步骤是聚合关于每个顶点的邻域的信息。例如,我们可能想知道每个用户拥有的关注者数量或每个用户的关注者的平均年龄。许多迭代图算法(例如,PageRank,最短路径和连通分量)重复地聚合相邻顶点的属性(例如,当前PageRank值,到源的最短路径和最小可到达顶点id)。

为了提高性能,主要聚合运算符从更改 graph.mapReduceTriplets为新的graph.AggregateMessages。虽然API的变化相对较小,但我们在下面提供了过渡指南。

 

4.5.1.聚合消息(aggregateMessages)

GraphX中的核心聚合操作是aggregateMessages。此运算符将用户定义的sendMsg函数应用于图中的每个边三元组,然后使用该mergeMsg函数在其目标顶点聚合这些消息。

class Graph[VD, ED] {
  def aggregateMessages[Msg: ClassTag](
      sendMsg: EdgeContext[VD, ED, Msg] => Unit,
      mergeMsg: (Msg, Msg) => Msg,
      tripletFields: TripletFields = TripletFields.All)
    : VertexRDD[Msg]
}

用户定义的sendMsg函数采用 EdgeContext,它公开源和目标属性以及edge属性和函数(sendToSrc,和sendToDst)以将消息发送到源和目标属性。可以将其sendMsg视为 map-reduce中的map函数。用户定义的mergeMsg函数接收两个发往同一顶点的消息,并产生一条消息。可以认为是map-reduce中mergeMsgreduce函数。 aggregateMessages操作者返回一个VertexRDD[Msg] 包含该聚合消息(类型的Msg)发往每个顶点。未收到消息的顶点不包含在返回的VertexRDD VertexRDD中

此外,还有aggregateMessages一个可选项 tripletsFields,用于指示访问哪些数据EdgeContext (即源顶点属性但不是目标顶点属性)。tripletsFields定义的可能选项TripletFields和默认值是TripletFields.All指示用户定义的sendMsg函数可以访问的任何字段EdgeContext。该tripletFields参数可用于通知GraphX只需要部分内容, EdgeContext允许GraphX选择优化的连接策略。例如,如果我们计算每个用户的关注者的平均年龄,我们只需要源字段,因此我们TripletFields.Src用来表明我们只需要源字段

在早期版本的GraphX中,我们使用字节代码检查来推断, TripletFields但是我们发现字节码检查稍微不可靠,而是选择更明确的用户控制。

在下面的示例中,我们使用aggregateMessages运算符来计算每个用户的更高级关注者的平均年龄。

import org.apache.spark.graphx.{Graph, VertexRDD}
import org.apache.spark.graphx.util.GraphGenerators

// Create a graph with "age" as the vertex property.
// Here we use a random graph for simplicity.
val graph: Graph[Double, Int] =
  GraphGenerators.logNormalGraph(sc, numVertices = 100).mapVertices( (id, _) => id.toDouble )
// Compute the number of older followers and their total age
val olderFollowers: VertexRDD[(Int, Double)] = graph.aggregateMessages[(Int, Double)](
  triplet => { // Map Function
    if (triplet.srcAttr > triplet.dstAttr) {
      // Send message to destination vertex containing counter and age
      triplet.sendToDst((1, triplet.srcAttr))
    }
  },
  // Add counter and age
  (a, b) => (a._1 + b._1, a._2 + b._2) // Reduce Function
)
// Divide total age by number of older followers to get average age of older followers
val avgAgeOfOlderFollowers: VertexRDD[Double] =
  olderFollowers.mapValues( (id, value) =>
    value match { case (count, totalAge) => totalAge / count } )
// Display the results
avgAgeOfOlderFollowers.collect.foreach(println(_))

aggregateMessages当消息(和消息总和)的大小恒定时(例如,浮动和添加而不是列表和连接),操作执行最佳。

 

4.5.2.地图减少三连体转换指南(Map Reduce Triplets Transition Guide(Legacy))

在早期版本的GraphX中,使用mapReduceTriplets运算符完成了邻居聚合 :

class Graph[VD, ED] {
  def mapReduceTriplets[Msg](
      map: EdgeTriplet[VD, ED] => Iterator[(VertexId, Msg)],
      reduce: (Msg, Msg) => Msg)
    : VertexRDD[Msg]
}

mapReduceTriplets操作者需要被施加到每个三联体,并且可以产生用户定义的映射函数的消息,其是使用用户定义的汇总的 reduce功能。但是,我们发现返回的迭代器的用户很昂贵,并且它阻止了我们应用其他优化的能力(例如,本地顶点重新编号)。在aggregateMessages我们推出暴露三重领域,还起到明确的消息发送到源和目标顶点EdgeContext。此外,我们删除了字节码检查,而是要求用户指示实际需要三元组中的哪些字段。

以下代码块使用mapReduceTriplets

val graph: Graph[Int, Float] = ...
def msgFun(triplet: Triplet[Int, Float]): Iterator[(Int, String)] = {
  Iterator((triplet.dstId, "Hi"))
}
def reduceFun(a: String, b: String): String = a + " " + b
val result = graph.mapReduceTriplets[String](msgFun, reduceFun)

可以使用AggregateMessage重写为:

val graph: Graph[Int, Float] = ...
def msgFun(triplet: EdgeContext[Int, Float, String]) {
  triplet.sendToDst("Hi")
}
def reduceFun(a: String, b: String): String = a + " " + b
val result = graph.aggregateMessages[String](msgFun, reduceFun)

 

4.5.3.计算度信息(Computing Degree Information)

常见的聚合任务是计算每个顶点的程度:与每个顶点相邻的边数。在有向图的上下文中,通常需要知道每个顶点的度数,出度和总度。本 GraphOps类包含运营商计算度数每个顶点的集合。例如,在下面我们计算最大入,出和总度:

// Define a reduce operation to compute the highest degree vertex
def max(a: (VertexId, Int), b: (VertexId, Int)): (VertexId, Int) = {
  if (a._2 > b._2) a else b
}
// Compute the max degrees
val maxInDegree: (VertexId, Int)  = graph.inDegrees.reduce(max)
val maxOutDegree: (VertexId, Int) = graph.outDegrees.reduce(max)
val maxDegrees: (VertexId, Int)   = graph.degrees.reduce(max)

 

4.5.4.收集邻居(Collecting Neighbors)

在某些情况下,通过在每个顶点收集相邻顶点及其属性来表达计算可能更容易。这可以使用collectNeighborIds和 collectNeighbors操作员轻松完成 。

class GraphOps[VD, ED] {
  def collectNeighborIds(edgeDirection: EdgeDirection): VertexRDD[Array[VertexId]]
  def collectNeighbors(edgeDirection: EdgeDirection): VertexRDD[ Array[(VertexId, VD)] ]
}

这些运算符可能非常昂贵,因为它们复制信息并需要大量通信。如果可能,尝试aggregateMessages 直接使用运算符表示相同的计算 。

 

4.6.缓存和取消缓存(Caching and Uncaching)

在Spark中,默认情况下RDD不会保留在内存中。为避免重新计算,必须多次使用它们时显式缓存它们(请参阅Spark编程指南)。GraphX中的图表行为相同。多次使用图表时,请务必先调用Graph.cache()它。

在迭代计算中,为了获得最佳性能,卸载也可能是必要的。默认情况下,缓存的RDD和图形将保留在内存中,直到内存压力强制它们按LRU顺序逐出。对于迭代计算,来自先前迭代的中间结果将填充高速缓存。虽然它们最终会被逐出,但存储在内存中的不必要数据会减慢垃圾收集速度。一旦不再需要中间结果,就更有效率。这涉及在每次迭代时实现(缓存和强制)图形或RDD,解除所有其他数据集,并且仅在将来的迭代中使用实体化数据集。但是,由于图形由多个RDD组成,因此很难正确地取消它们。对于迭代计算,我们建议使用Pregel API,它正确地解决了中间结果。

 

5.Pregel API

图是固有的递归数据结构,因为顶点的属性取决于它们的邻居的属性,而这些属性又取决于它们的邻居的属性。因此,许多重要的图算法迭代地重新计算每个顶点的属性,直到达到定点条件。已经提出了一系列图形并行抽象来表达这些迭代算法。GraphX公开了Pregel API的变体。

在高层次上,GraphX中的Pregel运算符是一种大规模同步并行消息传递抽象,它 受到图形拓扑的约束。Pregel运算符执行一系列超级步骤,其中顶点从前一个超级步骤接收其入站消息的总和,计算顶点属性的新值,然后在下一个超级步骤中将消息发送到相邻顶点。与Pregel不同,消息是作为边缘三元组的函数并行计算的,并且消息计算可以访问源和目标顶点属性。在超级步骤中跳过未接收消息的顶点。Pregel运算符终止迭代并在没有剩余消息时返回最终图。

注意,与更标准的Pregel实现不同,GraphX中的顶点只能向邻近顶点发送消息,并且消息构造是使用用户定义的消息传递函数并行完成的。这些约束允许在GraphX中进行额外的优化。

以下是Pregel运算符的类型签名以及 它的实现草图(注意:为了避免由于长谱系链而导致的stackOverflowError,pregel通过将“spark.graphx.pregel.checkpointInterval”设置为定期检查点图和消息正数,比如10.使用SparkContext.setCheckpointDir(目录:String)设置检查点目录:

class GraphOps[VD, ED] {
  def pregel[A]
      (initialMsg: A,
       maxIter: Int = Int.MaxValue,
       activeDir: EdgeDirection = EdgeDirection.Out)
      (vprog: (VertexId, VD, A) => VD,
       sendMsg: EdgeTriplet[VD, ED] => Iterator[(VertexId, A)],
       mergeMsg: (A, A) => A)
    : Graph[VD, ED] = {
    // Receive the initial message at each vertex
    var g = mapVertices( (vid, vdata) => vprog(vid, vdata, initialMsg) ).cache()

    // compute the messages
    var messages = GraphXUtils.mapReduceTriplets(g, sendMsg, mergeMsg)
    var activeMessages = messages.count()
    // Loop until no messages remain or maxIterations is achieved
    var i = 0
    while (activeMessages > 0 && i < maxIterations) {
      // Receive the messages and update the vertices.
      g = g.joinVertices(messages)(vprog).cache()
      val oldMessages = messages
      // Send new messages, skipping edges where neither side received a message. We must cache
      // messages so it can be materialized on the next line, allowing us to uncache the previous
      // iteration.
      messages = GraphXUtils.mapReduceTriplets(
        g, sendMsg, mergeMsg, Some((oldMessages, activeDirection))).cache()
      activeMessages = messages.count()
      i += 1
    }
    g
  }
}

请注意,Pregel有两个参数列表(即graph.pregel(list1)(list2))。第一个参数列表包含配置参数,包括初始消息,最大迭代次数以及发送消息的边缘方向(默认情况下沿边缘)。第二个参数列表包含用户定义的函数,用于接收消息(顶点程序 vprog),计算消息(sendMsg)和组合消息mergeMsg

我们可以使用Pregel运算符来表示以下示例中的单源最短路径等计算。

import org.apache.spark.graphx.{Graph, VertexId}
import org.apache.spark.graphx.util.GraphGenerators

// A graph with edge attributes containing distances
val graph: Graph[Long, Double] =
  GraphGenerators.logNormalGraph(sc, numVertices = 100).mapEdges(e => e.attr.toDouble)
val sourceId: VertexId = 42 // The ultimate source
// Initialize the graph such that all vertices except the root have distance infinity.
val initialGraph = graph.mapVertices((id, _) =>
    if (id == sourceId) 0.0 else Double.PositiveInfinity)
val sssp = initialGraph.pregel(Double.PositiveInfinity)(
  (id, dist, newDist) => math.min(dist, newDist), // Vertex Program
  triplet => {  // Send Message
    if (triplet.srcAttr + triplet.attr < triplet.dstAttr) {
      Iterator((triplet.dstId, triplet.srcAttr + triplet.attr))
    } else {
      Iterator.empty
    }
  },
  (a, b) => math.min(a, b) // Merge Message
)
println(sssp.vertices.collect.mkString("\n"))

 

6.图形构建器(Graph Builders)

GraphX提供了几种从RDD或磁盘上的顶点和边集合构建图形的方法。默认情况下,没有任何图形构建器重新分区图形的边缘; 相反,边缘保留在其默认分区中(例如HDFS中的原始块)。Graph.groupEdges需要对图表进行重新分区,因为它假定相同的边缘将位于同一分区上,因此您必须在调用Graph.partitionBy之前调用groupEdges

object GraphLoader {
  def edgeListFile(
      sc: SparkContext,
      path: String,
      canonicalOrientation: Boolean = false,
      minEdgePartitions: Int = 1)
    : Graph[Int, Int]
}

GraphLoader.edgeListFile提供了一种从磁盘上的边缘列表加载图形的方法。它解析以下形式的(源顶点ID,目标顶点ID)对的邻接列表,跳过以下列开头的注释行#

# This is a comment
2 1
4 1
1 2

Graph从指定的边创建一个,自动创建边提到的任何顶点。所有顶点和边缘属性都默认为1.该canonicalOrientation参数允许重定向正方向(srcId < dstId)的边,这是连接组件算法所需的。所述minEdgePartitions参数指定边缘的分区,以产生的最小数量; 例如,如果HDFS文件具有更多块,则可能存在比指定的边缘分区更多的边缘分区。

object Graph {
  def apply[VD, ED](
      vertices: RDD[(VertexId, VD)],
      edges: RDD[Edge[ED]],
      defaultVertexAttr: VD = null)
    : Graph[VD, ED]

  def fromEdges[VD, ED](
      edges: RDD[Edge[ED]],
      defaultValue: VD): Graph[VD, ED]

  def fromEdgeTuples[VD](
      rawEdges: RDD[(VertexId, VertexId)],
      defaultValue: VD,
      uniqueEdges: Option[PartitionStrategy] = None): Graph[VD, Int]

}

Graph.apply允许从顶点和边的RDD创建图形。任意拾取重复顶点,并且在边缘RDD但不是顶点RDD中找到的顶点被分配默认属性。

Graph.fromEdges 允许仅从边缘的RDD创建图形,自动创建边缘提到的任何顶点并为其指定默认值。

Graph.fromEdgeTuples允许仅从边缘元组的RDD创建图形,为边缘指定值1,并自动创建边缘提到的任何顶点并为其指定默认值。它还支持重复删除边缘; 进行重复数据删除,传递SomePartitionStrategy作为uniqueEdges参数(例如uniqueEdges = Some(PartitionStrategy.RandomVertexCut))。需要一个分区策略来在同一个分区上放置相同的边缘,以便对它们进行重复数据删除。

 

7.顶点和边缘RDD(Vertex and Edge RDDs)

GraphX公开RDD图中存储的顶点和边的视图。但是,由于GraphX在优化的数据结构中维护顶点和边缘,并且这些数据结构提供了额外的功能,因此顶点和边缘分别作为VertexRDDVertexRDDEdgeRDDEdgeRDD返回 。在本节中,我们将回顾这些类型中的一些其他有用功能。请注意,这只是一个不完整的列表,请参阅API文档以获取正式的操作列表。

 

7.1.VertexRDDs

VertexRDD[A]扩展RDD[(VertexId, A)]并增加了额外的限制,每个 VertexId只发生一次。此外,VertexRDD[A]表示一顶点,每个顶点具有类型的属性A。在内部,这是通过将顶点属性存储在可重用的哈希映射数据结构中来实现的。因此,如果两个VertexRDDs来自相同的基础VertexRDDVertexRDD(例如,通过filtermapValues),则它们可以在恒定时间内连接而无需散列评估。为了利用这种索引数据结构,VertexRDDVertexRDD公开了以下附加功能:

class VertexRDD[VD] extends RDD[(VertexId, VD)] {
  // Filter the vertex set but preserves the internal index
  def filter(pred: Tuple2[VertexId, VD] => Boolean): VertexRDD[VD]
  // Transform the values without changing the ids (preserves the internal index)
  def mapValues[VD2](map: VD => VD2): VertexRDD[VD2]
  def mapValues[VD2](map: (VertexId, VD) => VD2): VertexRDD[VD2]
  // Show only vertices unique to this set based on their VertexId's
  def minus(other: RDD[(VertexId, VD)])
  // Remove vertices from this set that appear in the other set
  def diff(other: VertexRDD[VD]): VertexRDD[VD]
  // Join operators that take advantage of the internal indexing to accelerate joins (substantially)
  def leftJoin[VD2, VD3](other: RDD[(VertexId, VD2)])(f: (VertexId, VD, Option[VD2]) => VD3): VertexRDD[VD3]
  def innerJoin[U, VD2](other: RDD[(VertexId, U)])(f: (VertexId, VD, U) => VD2): VertexRDD[VD2]
  // Use the index on this RDD to accelerate a `reduceByKey` operation on the input RDD.
  def aggregateUsingIndex[VD2](other: RDD[(VertexId, VD2)], reduceFunc: (VD2, VD2) => VD2): VertexRDD[VD2]
}

请注意,例如,filter运算符如何返回VertexRDDVertexRDD。过滤器实际上是使用BitSet从而重用索引并保留与其他VertexRDDs 快速连接的能力来实现的。同样,mapValues操作员不允许该map功能改变,VertexId从而使相同的HashMap数据结构能够被重用。无论是 leftJoininnerJoin能够连接两个时识别VertexRDD来自同一来源的小号 HashMap和落实线性扫描,而不是昂贵的点查找的加入。

aggregateUsingIndex操作是一个新的高效施工有用VertexRDDVertexRDD从 RDD[(VertexId, A)]。从概念上讲,如果我构造VertexRDD[B]了一组顶点, 这是一些顶点的超集RDD[(VertexId, A)]那么我可以重用索引来聚合然后随后索引RDD[(VertexId, A)]。例如:

val setA: VertexRDD[Int] = VertexRDD(sc.parallelize(0L until 100L).map(id => (id, 1)))
val rddB: RDD[(VertexId, Double)] = sc.parallelize(0L until 100L).flatMap(id => List((id, 1.0), (id, 2.0)))
// There should be 200 entries in rddB
rddB.count
val setB: VertexRDD[Double] = setA.aggregateUsingIndex(rddB, _ + _)
// There should be 100 entries in setB
setB.count
// Joining A and B should now be fast!
val setC: VertexRDD[Double] = setA.innerJoin(setB)((id, a, b) => a + b)

 

7.2.EdgeRDDs

EdgeRDD[ED],其延伸RDD[Edge[ED]]组织在块的边缘在使用中定义的各种分区策略的一个分区PartitionStrategy。在每个分区内,边缘属性和邻接结构分别存储,以便在更改属性值时最大程度地重用。

EdgeRDDEdgeRDD公开的三个附加功能是:

// Transform the edge attributes while preserving the structure
def mapValues[ED2](f: Edge[ED] => ED2): EdgeRDD[ED2]
// Reverse the edges reusing both attributes and structure
def reverse: EdgeRDD[ED]
// Join two `EdgeRDD`s partitioned using the same partitioning strategy.
def innerJoin[ED2, ED3](other: EdgeRDD[ED2])(f: (VertexId, VertexId, ED, ED2) => ED3): EdgeRDD[ED3]

在大多数应用程序中,我们发现EdgeRDDEdgeRDD上的操作是通过图形运算符完成的,或者依赖于基RDD类中定义的操作。

 

8.优化表示(Optimized Representation)

虽然分布式图形的GraphX表示中使用的优化的详细描述超出了本指南的范围,但一些高级别的理解可能有助于可伸缩算法的设计以及API的最佳使用。GraphX采用顶点切割方法进行分布式图分区:

è¾¹ç¼åå²ä¸é¡¶ç¹åå²

GraphX不是沿着边缘分割图形,而是沿着顶点划分图形,这可以减少通信和存储开销。从逻辑上讲,这对应于为机器分配边缘并允许顶点跨越多台机器。分配边缘的确切方法取决于PartitionStrategy各种启发式方法的几种权衡。用户可以通过使用Graph.partitionBy运算符重新分区图来选择不同的策略。默认分区策略是使用图形构造中提供的边的初始分区。但是,用户可以轻松切换到GraphX中包含的2D分区或其他启发式方法。

 

一旦边缘被分割,有效图形并行计算的关键挑战就是有效地将顶点属性与边缘连接起来。因为真实世界的图形通常具有比顶点更多的边缘,所以我们将顶点属性移动到边缘。因为并非所有分区都包含与所有顶点相邻的边,所以我们在内部维护一个路由表,该路由表标识在实现像triplets和等 操作所需的连接时广播顶点的位置aggregateMessages

 

9.图算法(Graph Algorithms)

GraphX包含一组图算法,可简化分析任务。算法包含在org.apache.spark.graphx.lib包中,可以作为Graphvia上的方法直接访问GraphOps。本节介绍算法及其使用方法。

 

9.1.网页排名(PageRank)

PageRank测量在图中每个顶点的重要性,假设从边缘ùv表示的认可v通过的重要性ü。例如,如果Twitter用户跟随许多其他用户,则用户将被排名很高。

GraphX带有PageRank的静态和动态实现作为PageRank对象的方法。静态PageRank运行固定次数的迭代,而动态PageRank运行直到排名收敛(即,停止更改超过指定的容差)。GraphOps允许直接调用这些算法作为方法Graph

GraphX还包括一个我们可以运行PageRank的示例社交网络数据集。给出一组用户data/graphx/users.txt,并给出用户之间的一组关系data/graphx/followers.txt。我们按如下方式计算每个用户的PageRank:

import org.apache.spark.graphx.GraphLoader

// Load the edges as a graph
val graph = GraphLoader.edgeListFile(sc, "data/graphx/followers.txt")
// Run PageRank
val ranks = graph.pageRank(0.0001).vertices
// Join the ranks with the usernames
val users = sc.textFile("data/graphx/users.txt").map { line =>
  val fields = line.split(",")
  (fields(0).toLong, fields(1))
}
val ranksByUsername = users.join(ranks).map {
  case (id, (username, rank)) => (username, rank)
}
// Print the result
println(ranksByUsername.collect().mkString("\n"))

 

9.2.连接组件(Connected Components)

连通分量算法使用其编号最小的顶点的ID标记图的每个连通分量。例如,在社交网络中,连接的组件可以近似聚类。GraphX包含ConnectedComponents对象中算法的实现,我们从PageRank部分计算示例社交网络数据集的连接组件,如下所示:

import org.apache.spark.graphx.GraphLoader

// Load the graph as in the PageRank example
val graph = GraphLoader.edgeListFile(sc, "data/graphx/followers.txt")
// Find the connected components
val cc = graph.connectedComponents().vertices
// Join the connected components with the usernames
val users = sc.textFile("data/graphx/users.txt").map { line =>
  val fields = line.split(",")
  (fields(0).toLong, fields(1))
}
val ccByUsername = users.join(cc).map {
  case (id, (username, cc)) => (username, cc)
}
// Print the result
println(ccByUsername.collect().mkString("\n"))

 

9.3.三角计数(Triangle Counting)

当顶点具有两个相邻顶点并且在它们之间具有边缘时,顶点是三角形的一部分。GraphX在TriangleCount对象中实现了一个三角形计数算法,该算法确定了通过每个顶点的三角形数量,从而提供了一种聚类度量。我们从PageRank部分计算社交网络数据集的三角形计数。请注意,TriangleCount要求边缘处于规范方向(srcId < dstId)并且要使用分区图形Graph.partitionBy

import org.apache.spark.graphx.{GraphLoader, PartitionStrategy}

// Load the edges in canonical order and partition the graph for triangle count
val graph = GraphLoader.edgeListFile(sc, "data/graphx/followers.txt", true)
  .partitionBy(PartitionStrategy.RandomVertexCut)
// Find the triangle count for each vertex
val triCounts = graph.triangleCount().vertices
// Join the triangle counts with the usernames
val users = sc.textFile("data/graphx/users.txt").map { line =>
  val fields = line.split(",")
  (fields(0).toLong, fields(1))
}
val triCountByUsername = users.join(triCounts).map { case (id, (username, tc)) =>
  (username, tc)
}
// Print the result
println(triCountByUsername.collect().mkString("\n"))

 

10.例子

假设我想从一些文本文件构建图形,将图形限制为重要的关系和用户,在子图上运行页面排名,然后最终返回与顶级用户关联的属性。我可以用GraphX在几行中完成所有这些工作:

import org.apache.spark.graphx.GraphLoader

// Load my user data and parse into tuples of user id and attribute list
val users = (sc.textFile("data/graphx/users.txt")
  .map(line => line.split(",")).map( parts => (parts.head.toLong, parts.tail) ))

// Parse the edge data which is already in userId -> userId format
val followerGraph = GraphLoader.edgeListFile(sc, "data/graphx/followers.txt")

// Attach the user attributes
val graph = followerGraph.outerJoinVertices(users) {
  case (uid, deg, Some(attrList)) => attrList
  // Some users may not have attributes so we set them as empty
  case (uid, deg, None) => Array.empty[String]
}

// Restrict the graph to users with usernames and names
val subgraph = graph.subgraph(vpred = (vid, attr) => attr.size == 2)

// Compute the PageRank
val pagerankGraph = subgraph.pageRank(0.001)

// Get the attributes of the top pagerank users
val userInfoWithPageRank = subgraph.outerJoinVertices(pagerankGraph.vertices) {
  case (uid, attrList, Some(pr)) => (pr, attrList.toList)
  case (uid, attrList, None) => (0.0, attrList.toList)
}

println(userInfoWithPageRank.vertices.top(5)(Ordering.by(_._2._1)).mkString("\n"))


 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值