Spark GraphX學習筆記

概述

  • GraphX是 Spark中用於圖(如Web-Graphs and Social Networks)和圖並行計算(如 PageRank and Collaborative Filtering)的API,可以認爲是GraphLab(C++)和Pregel(C++)在Spark(Scala)上的重寫及優化,跟其他分佈式 圖計算框架相比,GraphX最大的貢獻是,在Spark之上提供一站式數據解決方案,可以方便且高效地完成圖計算的一整套流水作業。
  • Graphx是Spark生態中的非常重要的組件,融合了圖並行以及數據並行的優勢,雖然在單純的計算機段的性能相比不如GraphLab等計算框架,但是如果從整個圖處理流水線的視角(圖構建,圖合併,最終結果的查詢)看,那麼性能就非常具有競爭性了。
    這裏寫圖片描述

圖計算應用場景

    “圖計算”是以“圖論”爲基礎的對現實世界的一種“圖”結構的抽象表達,以及在這種數據結構上的計算模式。通常,在圖計算中,基本的數據結構表達就是:G = (V,E,D) V = vertex (頂點或者節點) E = edge (邊) D = data (權重)。
    圖數據結構很好的表達了數據之間的關聯性,因此,很多應用中出現的問題都可以抽象成圖來表示,以圖論的思想或者以圖爲基礎建立模型來解決問題。
下面是一些圖計算的應用場景:
PageRank讓鏈接來”投票”
基於GraphX的社區發現算法FastUnfolding分佈式實現
http://bbs.pinggu.org/thread-3614747-1-1.html
社交網絡分析
如基於Louvian社區發現的新浪微博社交網絡分析
社交網絡最適合用圖來表達和計算了,圖的“頂點”表示社交中的人,“邊”表示人與人之間的關係。
基於三角形計數的關係衡量
基於隨機遊走的用戶屬性傳播
推薦應用
如淘寶推薦商品,騰訊推薦好友等等(同樣是基於社交網絡這個大數據,可以很好構建一張大圖)
淘寶應用
度分佈、二跳鄰居數、連通圖、多圖合併、能量傳播模型
所有的關係都可以從“圖”的角度來看待和處理,但到底一個關係的價值多大?健康與否?適合用於什麼場景?
快刀初試:Spark GraphX在淘寶的實踐
http://www.csdn.net/article/2014-08-07/2821097

Spark中圖的建立及圖的基本操作

圖的構建

          首先利用“頂點”和“邊”RDD建立一個簡單的屬性圖,通過這個例子,瞭解完整的GraphX圖構建的基本流程。
          如下圖所示,頂點的屬性包含用戶的姓名和職業,帶標註的邊表示不同用戶之間的關係。
這裏寫圖片描述

import org.apache.spark.SparkConf
import org.apache.spark.SparkContext

import org.apache.spark._
import org.apache.spark.graphx._
import org.apache.spark.rdd.RDD

object myGraphX {

  def main(args:Array[String]){

    // Create the context  
    val sparkConf = new SparkConf().setAppName("myGraphPractice").setMaster("local[2]")
    val sc=new SparkContext(sparkConf) 

    // 頂點RDD[頂點的id,頂點的屬性值]
    val users: RDD[(VertexId, (String, String))] =
      sc.parallelize(Array((3L, ("rxin", "student")), (7L, ("jgonzal", "postdoc")),
                       (5L, ("franklin", "prof")), (2L, ("istoica", "prof"))))
    // 邊RDD[起始點id,終點id,邊的屬性(邊的標註,邊的權重等)]
    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")

    //使用RDDs建立一個Graph(有許多建立Graph的數據來源和方法,後面會詳細介紹)
    val graph = Graph(users, relationships, defaultUser)     
  }
}

          上面是一個簡單的例子,說明如何建立一個屬性圖,那麼建立一個圖主要有哪些方法呢?我們先看圖的定義:

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]

}

          由上面的定義我們可以看到,GraphX主要有三種方法可以建立圖:
          (1)在構造圖的時候,會自動使用apply方法,因此前面那個例子中實際上就是使用apply方法。相當於Java/C++語言的構造函數。有三個參數,分別是:vertices: RDD[(VertexId, VD)], edges: RDD[Edge[ED]], defaultVertexAttr: VD = null),前兩個必須有,最後一個可選擇。“頂點“和”邊“的RDD來自不同的數據源,與Spark中其他RDD的建立並沒有區別。
          這裏再舉讀取文件,產生RDD,然後利用RDD建立圖的例子:

(1)讀取文件,建立頂點和邊的RRD,然後利用RDD建立屬性圖

//讀入數據文件
val articles: RDD[String] = sc.textFile("E:/data/graphx/graphx-wiki-vertices.txt")
val links: RDD[String] = sc.textFile("E:/data/graphx/graphx-wiki-edges.txt")

//裝載“頂點”和“邊”RDD
val vertices = articles.map { line =>
    val fields = line.split('\t')
      (fields(0).toLong, fields(1))
    }//注意第一列爲vertexId,必須爲Long,第二列爲頂點屬性,可以爲任意類型,包括Map等序列。

val edges = links.map { line =>
    val fields = line.split('\t')
      Edge(fields(0).toLong, fields(1).toLong, 1L)//起始點ID必須爲Long,最後一個是屬性,可以爲任意類型
    }
//建立圖
val graph = Graph(vertices, edges, "").persist()//自動使用apply方法建立圖

(2)Graph.fromEdges方法:這種方法相對而言最爲簡單,只是由”邊”RDD建立圖,由邊RDD中出現所有“頂點”(無論是起始點src還是終點dst)自動產生頂點vertextId,頂點的屬性將被設置爲一個默認值。
      Graph.fromEdges allows creating a graph from only an RDD of edges, automatically creating any vertices mentioned by edges and assigning them the default value.
          舉例如下:

//讀入數據文件        
val records: RDD[String] = sc.textFile("/microblogPCU/microblogPCU/follower_followee")   
//微博數據:000000261066,郜振博585,3044070630,redashuaicheng,1929305865,1994,229,3472,male,first
// 第三列是粉絲Id:3044070630,第五列是用戶Id:1929305865
val followers=records.map {case x => val fields=x.split(",")
          Edge(fields(2).toLong, fields(4).toLong,1L )       
      }    
val graph=Graph.fromEdges(followers, 1L)

(3)Graph.fromEdgeTuples方法
          Graph.fromEdgeTuples allows creating a graph from only an RDD of edge tuples, assigning the edges the value 1, and automatically creating any vertices mentioned by edges and assigning them the default value. It also supports deduplicating the edges; to deduplicate, pass Some of a PartitionStrategy as the uniqueEdges parameter (for example, uniqueEdges = Some(PartitionStrategy.RandomVertexCut)). A partition strategy is necessary to colocate identical edges on the same partition so they can be deduplicated.

          除了三種方法,還可以用GraphLoader構建圖。如下面GraphLoader.edgeListFile:
(4)GraphLoader.edgeListFile建立圖的基本結構,然後Join屬性
(a)首先建立圖的基本結構:
          利用GraphLoader.edgeListFile函數從邊List文件中建立圖的基本結構(所有“頂點”+“邊”),且頂點和邊的屬性都默認爲1。

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

使用方法如下:

val graph=GraphLoader.edgeListFile(sc, "/data/graphx/followers.txt") 
//文件的格式如下:
//2 1
//4 1
//1 2  依次爲第一個頂點和第二個頂點

(b)然後讀取屬性文件,獲得RDD後和(1)中得到的基本結構圖join在一起,就可以組合成完整的屬性圖。

三種視圖及操作

  Spark中圖有以下三種視圖可以訪問,分別通過graph.vertices,graph.edges,graph.triplets來訪問。
這裏寫圖片描述

          在Scala語言中,可以用case語句進行形式簡單、功能強大的模式匹配

//假設graph頂點屬性(String,Int)-(name,age),邊有一個權重(int)
val graph: Graph[(String, Int), Int] = Graph(vertexRDD, edgeRDD)
用case匹配可以很方便訪問頂點和邊的屬性及id
graph.vertices.map{
      case (id,(name,age))=>//利用case進行匹配
        (age,name)//可以在這裏加上自己想要的任何轉換
    }

graph.edges.map{
      case Edge(srcid,dstid,weight)=>//利用case進行匹配
        (dstid,weight*0.01)//可以在這裏加上自己想要的任何轉換
    }

          也可以通過下標訪問

graph.vertices.map{
      v=>(v._1,v._2._1,v._2._2)//v._1,v._2._1,v._2._2分別對應Id,name,age
}

graph.edges.map {
      e=>(e.attr,e.srcId,e.dstId)
}

graph.triplets.map{
      triplet=>(triplet.srcAttr._1,triplet.dstAttr._2,triplet.srcId,triplet.dstId)
    }

     可以不用graph.vertices先提取頂點再map的方法,也可以通過graph.mapVertices直接對頂點進行map,返回是相同結構的另一個Graph,訪問屬性的方法和上述方法是一模一樣的。如下:

graph.mapVertices{
      case (id,(name,age))=>//利用case進行匹配
        (age,name)//可以在這裏加上自己想要的任何轉換
}

graph.mapEdges(e=>(e.attr,e.srcId,e.dstId))

graph.mapTriplets(triplet=>(triplet.srcAttr._1))

Spark GraphX中的圖的函數大全

/** 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)

  // Aggregate information about adjacent triplets 
  //圖的鄰邊信息聚合,collectNeighborIds都是效率不高的操作,優先使用aggregateMessages,這也是GraphX最重要的操作之一。
  =================================================
  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 
  //圖的算法API(目前給出了三類四個API)  ========================================================================
  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]
}

結構操作

Structural Operators
      Spark2.0版本中,僅僅有四種最基本的結構操作,未來將開發更多的結構操作。

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]
}

子圖subgraph

      子圖(subgraph)是圖論的基本概念之一。子圖是指節點集和邊集分別是某一圖的節點集的子集和邊集的子集的圖。
  Spark API–subgraph利用EdgeTriplet(epred)或/和頂點(vpred)滿足一定條件,來提取子圖。利用這個操作可以使頂點和邊被限制在感興趣的範圍內,比如刪除失效的鏈接。
        The subgraph operator takes vertex and edge predicates and returns the graph containing only the vertices that satisfy the vertex predicate (evaluate to true) and edges that satisfy the edge predicate and connect vertices that satisfy the vertex predicate. The subgraph operator can be used in number of situations to restrict the graph to the vertices and edges of interest or eliminate broken links. For example in the following code we remove broken links:

//假設graph有如下的頂點和邊 頂點RDD(id,(name,age) 邊上有一個Int權重(屬性)
(4,(David,42))(6,(Fran,50))(2,(Bob,27)) (1,(Alice,28))(3,(Charlie,65))(5,(Ed,55))
Edge(5,3,8)Edge(2,1,7)Edge(3,2,4) Edge(5,6,3)Edge(3,6,3)

//可以使用以下三種操作方法獲取滿足條件的子圖
//方法1,對頂點進行操作
val subGraph1=graph.subgraph(vpred=(id,attr)=>attr._2>30)
//vpred=(id,attr)=>attr._2>30 頂點vpred第二個屬性(age)>30歲
subGraph1.vertices.foreach(print)
println
subGraph1.edges.foreach {print}
println
輸出結果:
頂點:(4,(David,42))(6,(Fran,50))(3,(Charlie,65))(5,(Ed,55))
邊:Edge(3,6,3)Edge(5,3,8)Edge(5,6,3)

//方法2--對EdgeTriplet進行操作
val subGraph2=graph.subgraph(epred=>epred.attr>2)
//epred(邊)的屬性(權重)大於2
輸出結果:
頂點:(4,(David,42))(6,(Fran,50))(2,(Bob,27))(1,(Alice,28)) (3,(Charlie,65))(5,(Ed,55))
邊:Edge(5,3,8)Edge(5,6,3)Edge(2,1,7)Edge(3,2,4) Edge(3,6,3)
//也可以定義如下的操作
val subGraph2=graph.subgraph(epred=>pred.srcAttr._2<epred.dstAttr._2))
//起始頂點的年齡小於終點頂點年齡
頂點:1,(Alice,28))(4,(David,42))(3,(Charlie,65))(6,(Fran,50)) (2,(Bob,27))(5,(Ed,55))
邊 :Edge(5,3,8)Edge(2,1,7)Edge(2,4,2)

//方法3--對頂點和邊Triplet兩種同時操作“,”號隔開epred和vpred
val subGraph3=graph.subgraph(epred=>epred.attr>3,vpred=(id,attr)=>attr._2>30)
輸出結果:
頂點:(3,(Charlie,65))(5,(Ed,55))(4,(David,42))(6,(Fran,50))
邊:Edge(5,3,8)

圖的基本信息統計-度計算

度分佈:這是一個圖最基礎和重要的指標。度分佈檢測的目的,主要是瞭解圖中“超級節點”的個數和規模,以及所有節點度的分佈曲線。超級節點的存在對各種傳播算法都會有重大的影響(不論是正面助力還是反面阻力),因此要預先對這些數據量有個預估。藉助GraphX最基本的圖信息接口degrees: VertexRDD[Int](包括inDegrees和outDegrees),這個指標可以輕鬆計算出來,並進行各種各樣的統計(摘自《快刀初試:Spark GraphX在淘寶的實踐》。

//-----------------度的Reduce,統計度的最大值-----------------
def max(a:(VertexId,Int),b:(VertexId,Int)):(VertexId,Int)={
            if (a._2>b._2) a  else b }

val totalDegree=graph.degrees.reduce((a,b)=>max(a, b))
val inDegree=graph.inDegrees.reduce((a,b)=>max(a,b))
val outDegree=graph.outDegrees.reduce((a,b)=>max(a,b))

print("max total Degree = "+totalDegree)
print("max in Degree = "+inDegree)
print("max out Degree = "+outDegree)
//小技巧:如何知道ab的類型爲(VertexId,Int)?
//當你敲完graph.degrees.reduce((a,b)=>,再將鼠標點到a和b上查看,
//就會發現a和b是(VertexId,Int),當然reduce後的返回值也是(VertexId,Int)
//這樣就很清楚自己該如何定義max函數了  

//平均度
val sumOfDegree=graph.degrees.map(x=>(x._2.toLong)).reduce((a,b)=>a+b)    
val meanDegree=sumOfDegree.toDouble/graph.vertices.count().toDouble
print("meanDegree "+meanDegree)
println     

//------------------使用RDD自帶的統計函數進行度分佈分析--------
//度的統計分析
//最大,最小
val degree2=graph.degrees.map(a=>(a._2,a._1))
//graph.degrees是VertexRDD[Int],即(VertexID,Int)。
//通過上面map調換成map(a=>(a._2,a._1)),即RDD[(Int,VetexId)]
//這樣下面就可以將度(Int)當作鍵值(key)來操作了,
//包括下面的min,max,sortByKey,top等等,因爲這些函數都是對第一個值也就是key操作的
//max degree
print("max degree = " + (degree2.max()._2,degree2.max()._1))
println

//min degree
print("min degree =" +(degree2.min()._2,degree2.min()._1))
println

//top(N) degree"超級節點"
print("top 3 degrees:\n")   
degree2.sortByKey(true, 1).top(3).foreach(x=>print(x._2,x._1))
println

/*輸出結果:
 * max degree = (2,4)//(Vetext,degree)
 * min degree =(1,2)
 * top 3 degrees:
 * (2,4)(5,3)(3,3)
 */ 

相鄰聚合—消息聚合

       相鄰聚合(Neighborhood Aggregation)
       圖分析任務的一個關鍵步驟是彙總每個頂點附近的信息。例如我們可能想知道每個用戶的追隨者的數量或者每個用戶的追隨者的平均年齡。許多迭代圖算法(如PageRank,最短路徑和連通體) 多次聚合相鄰頂點的屬性。
       聚合消息(aggregateMessages)
 GraphX中的核心聚合操作是 aggregateMessages,它主要功能是向鄰邊發消息,合併鄰邊收到的消息,返回messageRDD。這個操作將用戶定義的sendMsg函數應用到圖的每個邊三元組(edge triplet),然後應用mergeMsg函數在其目的頂點聚合這些消息。

class Graph[VD, ED] {
  def aggregateMessages[Msg: ClassTag](
      sendMsg: EdgeContext[VD, ED, Msg] => Unit,//(1)--sendMsg:向鄰邊發消息,相當與MR中的Map函數
      mergeMsg: (Msg, Msg) => Msg,//(2)--mergeMsg:合併鄰邊收到的消息,相當於Reduce函數
      tripletFields: TripletFields = TripletFields.All)//(3)可選項,TripletFields.Src/Dst/All
    : VertexRDD[Msg]//(4)--返回messageRDD
}

(1)sendMsg:
        將sendMsg函數看做map-reduce過程中的map函數,向鄰邊發消息,應用到圖的每個邊三元組(edge triplet),即函數的左側爲每個邊三元組(edge triplet)。
    The user defined sendMsg function takes an EdgeContext, which exposes the source and destination attributes along with the edge attribute and functions (sendToSrc, and sendToDst) to send messages to the source and destination attributes. Think of sendMsg as the map function in map-reduce.

//關鍵數據結構EdgeContext源碼解析

package org.apache.spark.graphx

/**
 * Represents an edge along with its neighboring vertices and allows sending messages along the
 * edge. Used in [[Graph#aggregateMessages]].
 */
abstract class EdgeContext[VD, ED, A] {//三個類型分別是:頂點、邊、自定義發送消息的類型(返回值的類型)
  /** The vertex id of the edge's source vertex. */
  def srcId: VertexId
  /** The vertex id of the edge's destination vertex. */
  def dstId: VertexId
  /** The vertex attribute of the edge's source vertex. */
  def srcAttr: VD
  /** The vertex attribute of the edge's destination vertex. */
  def dstAttr: VD
  /** The attribute associated with the edge. */
  def attr: ED

  /** Sends a message to the source vertex. */
  def sendToSrc(msg: A): Unit
  /** Sends a message to the destination vertex. */
  def sendToDst(msg: A): Unit

  /** Converts the edge and vertex properties into an [[EdgeTriplet]] for convenience. */
  def toEdgeTriplet: EdgeTriplet[VD, ED] = {
    val et = new EdgeTriplet[VD, ED]
    et.srcId = srcId
    et.srcAttr = srcAttr
    et.dstId = dstId
    et.dstAttr = dstAttr
    et.attr = attr
    et
  }
}

(2)mergeMsg :
        用戶自定義的mergeMsg函數指定兩個消息到相同的頂點並保存爲一個消息。可以將mergeMsg函數看做map-reduce過程中的reduce函數。

    The user defined mergeMsg function takes two messages destined to the same vertex and yields a single message. Think of mergeMsg as the reduce function in map-reduce.

這裏寫代碼片

(3)TripletFields可選項
        它指出哪些數據將被訪問(源頂點特徵,目的頂點特徵或者兩者同時,即有三種可選擇的值:TripletFields.Src,TripletFieldsDst,TripletFields.All。
      因此這個參數的作用是通知GraphX僅僅只需要EdgeContext的一部分參與計算,是一個優化的連接策略。例如,如果我們想計算每個用戶的追隨者的平均年齡,我們僅僅只需要源字段。 所以我們用TripletFields.Src表示我們僅僅只需要源字段。
     takes an optional tripletsFields which indicates what data is accessed in the EdgeContext (i.e., the source vertex attribute but not the destination vertex attribute). The possible options for the tripletsFields are defined in TripletFields and the default value is TripletFields.All which indicates that the user defined sendMsg function may access any of the fields in the EdgeContext. The tripletFields argument can be used to notify GraphX that only part of the EdgeContext will be needed allowing GraphX to select an optimized join strategy. For example if we are computing the average age of the followers of each user we would only require the source field and so we would use TripletFields.Src to indicate that we only require the source field

(4)返回值:
    The aggregateMessages operator returns a VertexRDD[Msg] containing the aggregate message (of type Msg) destined to each vertex. Vertices that did not receive a message are not included in the returned VertexRDD.

//假設已經定義好如下圖:
//頂點:[Id,(name,age)]
//(4,(David,18))(1,(Alice,28))(6,(Fran,40))(3,(Charlie,30))(2,(Bob,70))(5,Ed,55))
//邊:Edge(4,2,2)Edge(2,1,7)Edge(4,5,8)Edge(2,4,2)Edge(5,6,3)Edge(3,2,4)
//    Edge(6,1,2)Edge(3,6,3)Edge(6,2,8)Edge(4,1,1)Edge(6,4,3)(4,(2,110))

//定義一個相鄰聚合,統計比自己年紀大的粉絲數(count)及其平均年齡(totalAge/count)
val olderFollowers=graph.aggregateMessages[(Int,Int)](
//方括號內的元組(Int,Int)是函數返回值的類型,也就是Reduce函數(mergeMsg )右側得到的值(count,totalAge)
        triplet=> {
          if(triplet.srcAttr._2>triplet.dstAttr._2){            
              triplet.sendToDst((1,triplet.srcAttr._2))
          }
        },//(1)--函數左側是邊三元組,也就是對邊三元組進行操作,有兩種發送方式sendToSrc和 sendToDst
        (a,b)=>(a._1+b._1,a._2+b._2),//(2)相當於Reduce函數,a,b各代表一個元組(count,Age)
        //對count和Age不斷相加(reduce),最終得到總的count和totalAge
        TripletFields.All)//(3)可選項,TripletFields.All/Src/Dst
olderFollowers.collect().foreach(println)
輸出結果:
(4,(2,110))//頂點Id=4的用戶,有2個年齡比自己大的粉絲,同年齡是110歲
(6,(1,55))
(1,(2,110))

//計算平均年齡
val averageOfOlderFollowers=olderFollowers.mapValues((id,value)=>value match{
      case (count,totalAge) =>(count,totalAge/count)//由於不是所有頂點都有結果,所以用match-case語句
    })    

averageOfOlderFollowers.foreach(print)  
輸出結果:
(1,(2,55))(4,(2,55))(6,(1,55))//Id=1的用戶,有2個粉絲,平均年齡是55歲

Spark Join連接操作

         許多情況下,需要將圖與外部獲取的RDDs進行連接。比如將一個額外的屬性添加到一個已經存在的圖上,或者將頂點屬性從一個圖導出到另一圖中(在自己編寫圖計算API 時,往往需要多次進行aggregateMessages和Join操作,因此這兩個操作可以說是Graphx中非常重要的操作,需要非常熟練地掌握,在本文最後的實例中,有更多的例子可供學習)
         In many cases it is necessary to join data from external collections (RDDs) with graphs. For example, we might have extra user properties that we want to merge with an existing graph or we might want to pull vertex properties from one graph into another.

有兩個join API可供使用:

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連接

          返回值的類型就是graph頂點屬性的類型,不能新增,也不可以減少(即不能改變原始graph頂點屬性類型和個數)。
         經常會遇到這樣的情形,”一個額外的費用(extraCost)增加到老的費用(oldCost)中”,oldCost爲graph的頂點屬性值,extraCost來自外部RDD,這時候就要用到joinVertices:
         extraCosts: RDD[(VertexID, Double)]//額外的費用
         graph:Graph[Double,Long]//oldCost
         val totlCosts = graph.joinVertices(extraCosts)( (id, oldCost, extraCost) => oldCost + extraCost)
         //extraCost和oldCost數據類型一致,且返回時無需改變原始graph頂點屬性的類型。

再舉一個例子:

// 假設graph的頂點如下[id,(user_name,initial_energy)]
//(6,(Fran,0))(2,(Bob,3))(4,(David,3))(3,(Charlie,1))(1,(Alice,2))(5,(Ed,2))

// graph邊如下:
//Edge(2,1,1)Edge(2,4,1)Edge(4,1,1)Edge(5,2,1)Edge(5,3,1)Edge(5,6,1)Edge(3,2,1)Edge(3,6,1)

// 每個src向dst鄰居發送生命值爲2能量
val energys=graph.aggregateMessages[Long](
            triplet=>triplet.sendToDst(2), (a,b)=>a+b)      

// 輸出結果:
// (1,4)(4,2)(3,2)(6,4)(2,4)
val energys_name=graph.joinVertices(energys){
              case(id,(name,initialEnergy),energy)=>(name,initialEnergy+energy)
              }
//輸出結果:
// (3,(Charlie,3))(1,(Alice,6))(5,(Ed,2))(4,(David,5))(6,(Fran,4))(2,(Bob,7))

// 我們注意到,如果energys:RDD中沒有graph某些頂點對應的值,則graph不進行任何改變,如(5,(Ed,2))。

         從上面的例子我們知道:將外部RDD joinvertices到graph中,對應於graph某些頂點,RDD中無對應的屬性,則保留graph原有屬性值不進行任何改變。
         而與之相反的是另一種情況,對應於graph某一些頂點,RDD中的值不止一個,這種情況下將只有一個值在join時起作用。可以先使用aggregateUsingIndex的進行reduce操作,然後再join graph。

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)

         If the RDD contains more than one value for a given vertex only one will be used. It is therefore recommended that the input RDD be made unique using the following which will also pre-index the resulting values to substantially accelerate the subsequent join.

(2)outerJoinVertices

         更爲常用,使用起來也更加自由的是outerJoinVertices,至於爲什麼後面會詳細分析。
         The more general outerJoinVertices behaves similarly to joinVertices except that the user defined map function is applied to all vertices and can change the vertex property type. Because not all vertices may have a matching value in the input RDD the map function takes an Option type.

         從下面函數的定義我們注意到,與前面JoinVertices不同之處在於map函數右側類型是VD2,不再是VD,因此不受原圖graph頂點屬性類型VD的限制,在outerJoinVertices中使用者可以隨意定義自己想要的返回類型,從而可以完全改變圖的頂點屬性值的類型和屬性的個數。

class Graph[VD, ED] {

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

}

用上面例子中的graph和energys數據:

 val graph_energy_total=graph.outerJoinVertices(energys){
      case(id,(name,initialEnergy),Some(energy))=>(name,initialEnergy,energy,initialEnergy+energy)
      case(id,(name,initialEnergy),None)=>(name,initialEnergy,0,initialEnergy)
    }

// 輸出結果:
// (3,(Charlie,1,2,3))(1,(Alice,2,4,6))(5,(Ed,2,0,2))
// (4,(David,3,2,5))(6,(Fran,0,4,4))(2,(Bob,3,4,7))

Spark Scala幾個語法問題

(1)遇到null怎麼處理?
可參考【Scala】使用Option、Some、None,避免使用null
http://www.jianshu.com/p/95896d06a94d

         大多數語言都有一個特殊的關鍵字或者對象來表示一個對象引用的是“無”,在Java,它是null。
         Scala鼓勵你在變量和函數返回值可能不會引用任何值的時候使用Option類型。在沒有值的時候,使用None,這是Option的一個子類。如果有值可以引用,就使用Some來包含這個值。Some也是Option的子類。
         通過模式匹配分離可選值,如果匹配的值是Some的話,將Some裏的值抽出賦給x變量。舉一個綜合的例子:

def showCapital(x: Option[String]) = x match {
    case Some(s) => s
    case None => "?"
}

/*
Option用法:Scala推薦使用Option類型來代表一些可選值。使用Option類型,讀者一眼就可以看出這種類型的值可能爲None。
如上面:x: Option[String])參數,就是因爲參數可能是String,也可能爲null,這樣程序不會在爲null時拋出異常
*/

Spark中,經常使用在map中使用case語句進行匹配None和Some,再舉一個例子

//假設graph.Vertice:(id,(name,weight))如下:
//(4,(David,Some(2)))(3,(Charlie,Some(2)))(6,(Fran,Some(4)))(2,(Bob,Some(4)))(1,(Alice,Some(4)))(5,(Ed,None))
//id=5時,weight=None,其他的爲Some

val weights=graph.vertices.map{
      case (id,(name,Some(weight)))=>(id,weight)
      case (id,(name,None))=>(id,0)
    }    
weights.foreach(print)
println

//輸出結果如下(id,weight):
//(3,2)(6,4)(2,4)(4,2)(1,4)(5,0)

在上面的例子中,其實我們也可以選用另外一個方法,getOrElse。這個方法在這個Option是Some的實例時返回對應的值,而在是None的實例時返函數參數。
上面例子可以用下面的語句獲得同樣的結果:

val weights=graph.vertices.map{
      attr=>(attr._1,attr._2._2.getOrElse(0))
      //如果attr._2._2!=None,返回attr._2._2(weight)的值,
      //否則(即attr._2._2==None),返回自己設置的函數參數(0)
    }

//輸出同樣的結果:
//(id,weight)
(4,2)(6,4)(2,4)(3,2)(1,4)(5,0)

圖算法工具包

1.數三角形

TriangleCount主要用途之一是用於社區發現,如下圖所示:
這裏寫圖片描述
例如說在微博上你關注的人也互相關注,大家的關注關係中就會有很多三角形,這說明社區很強很穩定,大家的聯繫都比較緊密;如果說只是你一個人關注很多人,這說明你的社交羣體是非常小的。(摘自《大數據Spark企業級實戰》一書)

graph.triangleCount().vertices.foreach(x=>print(x+"\n"))
    /*輸出結果
     * (1,1)//頂點1有1個三角形
     * (3,2)//頂點3有2個三角形
     * (5,2)
     * (4,1)
     * (6,1)
     * (2,2)
     */

2.連通圖

        現實生活中存在各種各樣的網絡,諸如人際關係網、交易網、運輸網等等。對這些網絡進行社區發現具有極大的意義,如在人際關係網中,可以發現出具有不同興趣、背景的社會團體,方便進行不同的宣傳策略;在交易網中,不同的社區代表不同購買力的客戶羣體,方便運營爲他們推薦合適的商品;在資金網絡中,社區有可能是潛在的洗錢團伙、刷鑽聯盟,方便安全部門進行相應處理;在相似店鋪網絡中,社區發現可以檢測出商幫、價格聯盟等,對商家進行指導等等。總的來看,社區發現在各種具體的網絡中都能有重點的應用場景,圖1展示了基於圖的拓撲結構進行社區發現的例子。

這裏寫圖片描述

        檢測連通圖可以弄清一個圖有幾個連通部分及每個連通部分有多少頂點。這樣可以將一個大圖分割爲多個小圖,並去掉零碎的連通部分,從而可以在多個小子圖上進行更加精細的操作。目前,GraphX提供了ConnectedComponents和StronglyConnected-Components算法,使用它們可以快速計算出相應的連通圖。
        連通圖可以進一步演化變成社區發現算法,而該算法優劣的評判標準之一,是計算模塊的Q值,來查看所謂的modularity情況。
         如果一個有向圖中的每對頂點都可以從通過路徑可達,那麼就稱這個圖是強連通的。一個 strongly connected component就是一個有向圖中最大的強連通子圖。下圖中就有三個強連通子圖:
這裏寫圖片描述

//連通圖
def connectedComponents(maxIterations: Int): Graph[VertexId, ED]
def connectedComponents(): Graph[VertexId, ED]

//強連通圖
//numIter:the maximum number of iterations to run for
def stronglyConnectedComponents(numIter: Int): Graph[VertexId, ED]
//連通圖計算社區發現
import org.apache.spark.SparkConf
import org.apache.spark.SparkContext
import org.apache.log4j.{Level, Logger}

import org.apache.spark._
import org.apache.spark.graphx._
import org.apache.spark.rdd.RDD

object myConnectComponent {
  def main(args:Array[String]){    

    val sparkConf = new SparkConf().setAppName("myGraphPractice").setMaster("local[2]")
    val sc=new SparkContext(sparkConf) 
    //屏蔽日誌
    Logger.getLogger("org.apache.spark").setLevel(Level.ERROR)
    Logger.getLogger("org.eclipse.jetty.server").setLevel(Level.OFF)

    val graph=GraphLoader.edgeListFile(sc, "/spark-2.0.0-bin-hadoop2.6/data/graphx/followers.txt")    

    graph.vertices.foreach(print)
    println
    graph.edges.foreach(print)  
    println

    val cc=graph.connectedComponents().vertices
    cc.foreach(print)
    println 
    /*輸出結果 
     *  (VertexId,cc)
     * (4,1)(1,1)(6,1)(3,1)(2,1)(7,1)
     */

    //強連通圖-stronglyConnectedComponents
    val maxIterations=10//the maximum number of iterations to run for
    val cc2=graph.stronglyConnectedComponents(maxIterations).vertices
    cc2.foreach(print)
    println 


    val path2="/spark-2.0.0-bin-hadoop2.6/data/graphx/users.txt"
    val users=sc.textFile(path2).map{//map 中包含多行 必須使用{}    
      line=>val fields=line.split(",")
      (fields(0).toLong,fields(1))//(id,name) 多行書寫 最後一行纔是返回值 且與上行splitsplit(",")之間要有換行
    }    
    users.collect().foreach { println}
    println
    /*輸出結果 (VertexId,name)
     * (1,BarackObama)
     * (2,ladygaga)
     * ...
     */


    val joint=cc.join(users)
    joint.collect().foreach { println}
    println

    /*輸出結果
     * (VertexId,(cc,name))
     * (4,(1,justinbieber))
     * (6,(3,matei_zaharia))
     */

    val name_cc=joint.map{
      case (VertexId,(cc,name))=>(name,cc)
    }
    name_cc.foreach(print)   
    /*
     * (name,cc)
     * (BarackObama,1)(jeresig,3)(odersky,3)(justinbieber,1)(matei_zaharia,3)(ladygaga,1)
     */

  }  

}

3.PageRank讓鏈接來”投票”

        一個頁面的“得票數”由所有鏈向它的頁面的重要性來決定,到一個頁面的超鏈接相當於對該頁投一票。一個頁面的PageRank是由所有鏈向它的頁面(“鏈入頁面”)的重要性經過遞歸算法得到的。一個有較多鏈入的頁面會有較高的等級,相反如果一個頁面沒有任何鏈入頁面,那麼它沒有等級。
這裏寫圖片描述

Spark Graphx實例直接參考:
http://www.cnblogs.com/shishanyuan/p/4747793.html

def pageRank(tol: Double, resetProb: Double = 0.15): Graph[Double, Double]
//兩個參數
//tol:the tolerance allowed at convergence (smaller => more accurate).
//tol越小計算結果越精確,但是會花更長的時間
//resetProb:the random reset probability (alpha)
//返回一個圖,頂點的屬性是PageRank(Double);邊的屬性是規範化的權重(Double)

Run a dynamic version of PageRank returning a graph with vertex attributes containing the PageRank and edge attributes containing the normalized edge weight.
val prGraph = graph.pageRank(tol=0.001).cache()

pregel

     在迭代計算中,釋放內存是必要的,在新圖產生後,需要快速將舊圖徹底釋放掉,否則,十幾輪迭代後,會有內存泄漏問題,很快耗光作業緩存空間。但是直接使用Spark提供的API cache、unpersist和checkpoint,非常需要使用技巧。所以Spark官方文檔建議:對於迭代計算,建議使用Pregal API,它能夠正確的釋放中間結果,這樣就不需要自己費心去操作了。
     In iterative computations, uncaching may also be necessary for best performance.However, because graphs are composed of multiple RDDs, it can be difficult to unpersist them correctly. For iterative computation we recommend using the Pregel API, which correctly unpersists intermediate results.
        圖是天然的迭代數據結構,頂點的屬性值依賴於鄰居的屬性值,而鄰居們的屬性值同樣也依賴於他們各自鄰居屬性值(即鄰居的鄰居)。許多重要的圖算法迭代式的重新計算頂點的屬性直到達到預設的迭代條件。這些迭代的圖算法被抽象成一系列圖並行操作。
     Graphs are inherently recursive data structures as properties of vertices depend on properties of their neighbors which in turn depend on properties of their neighbors. As a consequence many important graph algorithms iteratively recompute the properties of each vertex until a fixed-point condition is reached. A range of graph-parallel abstractions have been proposed to express these iterative algorithms. GraphX exposes a variant of the Pregel API.
     
     At a high level the Pregel operator in GraphX is a bulk-synchronous parallel messaging abstraction constrained to the topology of the graph. The Pregel operator executes in a series of super steps in which vertices receive the sum of their inbound messages from the previous super step, compute a new value for the vertex property, and then send messages to neighboring vertices in the next super step. Unlike Pregel, messages are computed in parallel as a function of the edge triplet and the message computation has access to both the source and destination vertex attributes. Vertices that do not receive a message are skipped within a super step. The Pregel operators terminates iteration and returns the final graph when there are no messages remaining.

Note, unlike more standard Pregel implementations, vertices in GraphX can only send messages to neighboring vertices and the message construction is done in parallel using a user defined messaging function. These constraints allow additional optimization within GraphX.

//Graphx 中pregel 所用到的主要優化:
1.   Caching for Iterative mrTriplets & Incremental Updates for Iterative mrTriplets :在
很多圖分析算法中,不同點的收斂速度變化很大。在迭代後期,只有很少的點會有更新。因此,對於沒有更新的點,下一
次 mrTriplets 計算時 EdgeRDD 無需更新相應點值的本地緩存,大幅降低了通信開銷。

2. Indexing Active Edges :沒有更新的頂點在下一輪迭代時不需要向鄰居重新發送消息。因此, mrTriplets 
遍歷邊時,如果一條邊的鄰居點值在上一輪迭代時沒有更新,則直接跳過,避免了大量無用的計算和通信。

3. Join Elimination : Triplet 是由一條邊和其兩個鄰居點組成的三元組,操作 Triplet 的 map 函數常常只
需訪問其兩個鄰居點值中的一個。例如,在 PageRank 計算中,一個點值的更新只與其源頂點的值有關,而與其所指向
的目的頂點的值無關。那麼在 mrTriplets 計算中,就不需要 VertexRDD 和 EdgeRDD 的 3-way join ,而只需
要 2-way join 。

所有這些優化使 GraphX 的性能逐漸逼近 GraphLab 。雖然還有一定差距,但一體化的流水線服務和豐富的編程接口,
可以彌補性能的微小差距。

//pregel 操作計算過程分析:
class GraphOps[VD, ED] {
  def pregel[A]
      //包含兩個參數列表
      //第一個參數列表包含配置參數初始消息、最大迭代數、發送消息的邊的方向(默認是沿邊方向出)。
      //VD:頂點的數據類型。
      //ED:邊的數據類型
      //A:Pregel message的類型。
      //graph:輸入的圖
      //initialMsg:在第一次迭代的時候頂點收到的消息。

maxIterations:迭代的次數

      (initialMsg: A,
       maxIter: Int = Int.MaxValue,
       activeDir: EdgeDirection = EdgeDirection.Out)

      //第二個參數列表包含用戶 自定義的函數用來接收消息(vprog)、計算消息(sendMsg)、合併消息(mergeMsg)。 
      //vprog:用戶定義的頂點程序運行在每一個頂點中,負責接收進來的信息,和計算新的頂點值。
      //在第一次迭代的時候,所有的頂點程序將會被默認的defaultMessage調用,
      //在次輪迭代中,頂點程序只有接收到message纔會被調用。      
      (vprog: (VertexId, VD, A) => VD,//vprog:
      //sendMsg:用戶提供的函數,應用於邊緣頂點在當前迭代中接收message
      sendMsg: EdgeTriplet[VD, ED] => Iterator[(VertexId, A)],
      //用戶提供定義的函數,將兩個類型爲A的message合併爲一個類型爲A的message
      mergeMsg: (A, A) => A)
    : Graph[VD, ED] = {

    // Receive the initial message at each vertex
    // 在第一次迭代的時候,所有的頂點都會接收到initialMsg消息,
    // 在次輪迭代的時候,如果頂點沒有接收到消息,verteProgram(vprog)就不會被調用。
    var g = mapVertices( (vid, vdata) => vprog(vid, vdata, initialMsg) ).cache()

    // 使用mapReduceTriplets compute the messages(即map和reduce message,不斷減少messages)
    var messages = g.mapReduceTriplets(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 = g.mapReduceTriplets(
        sendMsg, mergeMsg, Some((oldMessages, activeDirection))).cache()
      activeMessages = messages.count()
      i += 1
    }
    g
  }
}
整個過程不是很容易理解,更詳細的計算過程分析可以參考:
Spark的Graphx學習筆記--Pregel:http://www.ithao123.cn/content-3510265.html

總之,把握住整個迭代過程:
vertexProgram(vprog)在第一次在初始化的時候,會在所有頂點上運行,之後,只有接收到消息的頂點纔會運行vertexProgram,重複這個步驟直到迭代條件。

//計算最短路徑代碼
import org.apache.log4j.{Level, Logger}
import org.apache.spark.{SparkContext, SparkConf}
import org.apache.spark.graphx._
import org.apache.spark.rdd.RDD

object myPregal {
  def main(args:Array[String]){

    //設置運行環境
    val conf = new SparkConf().setAppName("myGraphPractice").setMaster("local[4]")
    val sc=new SparkContext(conf)

    //屏蔽日誌
    Logger.getLogger("org.apache.spark").setLevel(Level.ERROR)
    Logger.getLogger("org.eclipse.jetty.server").setLevel(Level.OFF)    

    val vertexArray = Array(
      (1L, ("Alice", 28)),(2L, ("Bob", 27)),(3L, ("Charlie", 65)),(4L, ("David", 42)),
      (5L, ("Ed", 55)),(6L, ("Fran", 50))
    )
    //邊的數據類型ED:Int
    val edgeArray = Array(
      Edge(2L, 1L, 7),Edge(2L, 4L, 2),Edge(3L, 2L, 4),Edge(3L, 6L, 3),
      Edge(4L, 1L, 1),Edge(5L, 2L, 2),Edge(5L, 3L, 8),Edge(5L, 6L, 3)
    )

    //構造vertexRDD和edgeRDD
    val vertexRDD: RDD[(Long, (String, Int))] = sc.parallelize(vertexArray)
    val edgeRDD: RDD[Edge[Int]] = sc.parallelize(edgeArray)

    //構造圖Graph[VD,ED]
    val graph: Graph[(String, Int), Int] = Graph(vertexRDD, edgeRDD)    


    val sourceId:VertexId=5//定義源點
    val initialGraph=graph.mapVertices((id,_)=>if (id==sourceId) 0 else Double.PositiveInfinity)  
    //pregel函數有兩個參數列表
    val shorestPath=initialGraph.pregel(initialMsg=Double.PositiveInfinity,
                                        maxIterations=100,                                  
                                        activeDirection=EdgeDirection.Out)(

                                 //1-頂點屬性迭代更新方式,與上一次迭代後保存的屬性相比,取較小值
                                 //(將從源點到頂點的最小距離放在頂點屬性中)    
                                 (id,dist,newDist)=>math.min(dist,newDist), 

                                 //2-Send Message,在所有能到達目的點的鄰居中,計算鄰居頂點屬性+邊屬性
                                 //即(鄰居-源點的距離+鄰居-目的點的距離,並將這個距離放在迭代器中
                                 triplet=>{
                                   if(triplet.srcAttr+triplet.attr<triplet.dstAttr){
                                     Iterator((triplet.dstId,triplet.srcAttr+triplet.attr))
                                   }else{
                                     Iterator.empty
                                   }
                                 }, 

                                 //3-Merge Message,相當於Reduce函數
                                 //對所有能達到目的點的鄰居發送的消息,進行min-reduce
                                 //鄰居中最終reduce後最小的結果,作爲newDist,發送至目的點,
                                 //至此,目的點中有新舊兩個dist了,在下一次迭代開始的時候,步驟1中就可以進行更新迭代了
                                 (a,b)=>math.min(a,b))

    shorestPath.vertices.map(x=>(x._2,x._1)).top(30).foreach(print)  

    /*outprint(shorest distance,vertexId)
     * 8.0,3)(5.0,1)(4.0,4)(3.0,6)(2.0,2)(0.0,5)
     */ 
  }  
}

應用實例1:Louvain算法社區發現

      實例來自《Spark最佳實踐》陳歡等著 一書,整個這個實例可參考原書即可。
      源代碼來自https://github.com/Sotera/spark-distributed-louvain-modularity git clone後就可以使用了。
      但是2.0版本Spark源碼需要進行修改,因爲老的聚合函數不能再使用了,需要修改成消息聚合函數,《Spark最佳實踐》一書中已經進行了修改,可惜這本書沒有給出完整的修改後代碼,後面我會貼出修改的後的代碼,替換git上的相應部分就可以使用了。

      社區發現算法可供參考的資料也比較多,算法也比較多。
http://blog.csdn.net/peghoty/article/details/9286905

關鍵概念–模塊度(Modularity )
      很多的社區發現算法都是基於模塊度設計的,模塊度用於衡量社區劃分結構的合理性。
      用某種算法劃分結果的內聚性與隨機劃分結果的內聚性的差值,對劃分結果進行評估。
  模塊度是評估一個社區網絡劃分好壞的度量方法,它的物理含義是社區內節點的連邊數與隨機情況下的邊數只差,它的取值範圍是 [−1/2,1),其定義如下:

Q=12mi,j[Aijkikj2m]δ(ci,cj)

δ(u,v)={1when u==v0 else

  其中,Aij 節點i和節點j之間邊的權重,網絡不是帶權圖時,所有邊的權重可以看做是1;ki=jAij 表示所有與節點i相連的邊的權重之和(度數);ci 表示節點i所屬的社區;m=12ijAij 表示所有邊的權重之和(邊的數目)。

  公式中Aijkikj2m=Aijkikj2m ,節點j連接到任意一個節點的概率是kj2m ,現在節點i有ki 的度數,因此在隨機情況下節點i與j的邊爲kikj2m .
  模塊度的公式定義可以作如下簡化:
  

Q=12mi,j[Aijkikj2m]δ(ci,cj)  =12m[i,jAijikijkj2m]δ(ci,cj)  =12mc[Σin(Σtot)22m]

其中ΣinΣin表示社區c內的邊的權重之和,ΣtotΣtot表示與社區c內的節點相連的邊的權重之和。

  上面的公式還可以進一步簡化成:

Q=c[Σin2m(Σtot2m)2]=c[ecac2]

  這樣模塊度也可以理解是社區內部邊的權重減去所有與社區節點相連的邊的權重和,對無向圖更好理解,即社區內部邊的度數減去社區內節點的總度數。

  基於模塊度的社區發現算法,大都是以最大化模塊度Q爲目標。

Louvain算法流程
Louvain算法的思想很簡單:

  1)將圖中的每個節點看成一個獨立的社區,次數社區的數目與節點個數相同;

  2)對每個節點i,依次嘗試把節點i分配到其每個鄰居節點所在的社區,計算分配前與分配後的模塊度變化ΔQ ΔQ,並記錄ΔQ ΔQ最大的那個鄰居節點,如果maxΔQ>0 maxΔQ>0,則把節點i分配ΔQ ΔQ最大的那個鄰居節點所在的社區,否則保持不變;

  3)重複2),直到所有節點的所屬社區不再變化;

  4)對圖進行壓縮,將所有在同一個社區的節點壓縮成一個新節點,社區內節點之間的邊的權重轉化爲新節點的環的權重,社區間的邊權重轉化爲新節點間的邊權重;

  5)重複1)直到整個圖的模塊度不再發生變化。
  從流程來看,該算法能夠產生層次性的社區結構,其中計算耗時較多的是最底一層的社區劃分,節點按社區壓縮後,將大大縮小邊和節點數目,並且計算節點i分配到其鄰居j的時模塊度的變化只與節點i、j的社區有關,與其他社區無關,因此計算很快。

代碼修改
由於版本的問題,Spark2.0中不再使用不穩定的mapReduceTriplets函數,替換爲aggregateMessages。

(第1處修改)
def createLouvainGraph[VD: ClassTag](graph: Graph[VD,Long]) : Graph[VertexState,Long]與
def compressGraph(graph:Graph[VertexState,Long],debug:Boolean=true) : Graph[VertexState,Long]函數中:
//老版本
val nodeWeightMapFunc = (e:EdgeTriplet[VD,Long]) =>Iterator((e.srcId,e.attr), (e.dstId,e.attr))
val nodeWeightReduceFunc = (e1:Long,e2:Long) => e1+e2    
val nodeWeights = graph.mapReduceTriplets(nodeWeightMapFunc,nodeWeightReduceFunc)

//修改爲:   
val nodeWeights = graph.aggregateMessages[Long](triplet=>
          (triplet.sendToSrc(triplet.attr),triplet.sendToDst(triplet.attr)),    
          (a,b)=>a+b)

(第2處修改)def louvain(sc:SparkContext...)函數中sendMsg函數:
//老版本
 private def sendMsg(et:EdgeTriplet[VertexState,Long]) = {
    val m1 = (et.dstId,Map((et.srcAttr.community,et.srcAttr.communitySigmaTot)->et.attr))
    val m2 = (et.srcId,Map((et.dstAttr.community,et.dstAttr.communitySigmaTot)->et.attr))
    Iterator(m1, m2)    
  }

//修改爲
//import scala.collection.immutable.Map
private def sendMsg(et:EdgeContext[VertexState,Long,Map[(Long,Long),Long]]) = {    
      et.sendToDst(Map((et.srcAttr.community,et.srcAttr.communitySigmaTot)->et.attr))
      et.sendToSrc(Map((et.dstAttr.community,et.dstAttr.communitySigmaTot)->et.attr))     
  }  

使用新浪微博數據進行分析
詳細分析可以參考《Spark最佳實踐一書》

參考文獻

(1)Spark 官方文檔
http://spark.apache.org/docs/latest/graphx-programming-guide.html#pregel-api
(2)大數據Spark企業級實戰 王家林
(3)GraphX迭代的瓶頸與分析
http://blog.csdn.net/pelick/article/details/50630003
(4)基於Spark的圖計算框架 GraphX 入門介紹
http://www.open-open.com/lib/view/open1420689305781.html
(5)Spark入門實戰系列–9.Spark圖計算GraphX介紹及實例
http://www.cnblogs.com/shishanyuan/p/4747793.html
(6)快刀初試:Spark GraphX在淘寶的實踐
http://www.csdn.net/article/2014-08-07/2821097
(7)基於GraphX的社區發現算法FastUnfolding分佈式實現
http://bbs.pinggu.org/thread-3614747-1-1.html
(8)關於圖計算和graphx的一些思考
http://www.tuicool.com/articles/3MjURj
(9)用 LDA 做主題模型:當 MLlib 邂逅 GraphX
http://blog.jobbole.com/86130/
(10)Spark的Graphx學習筆記–Pregel
http://www.ithao123.cn/content-3510265.html
(11)Spark最佳實踐 陳歡 林世飛
(12)社區發現(Community Detection)算法
http://blog.csdn.net/peghoty/article/details/9286905

發佈了42 篇原創文章 · 獲贊 57 · 訪問量 30萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章