Spark圖計算API簡介

一、屬性操作符:

graph中提供了對vertex,edge和triplet的map操作,類似于RDD中的map操作:

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]

使用這些方法不會改變圖的結構,所以這些操作符可以利用原有的圖的structural indicies。所以不要用graph.vertices.map的方法來實現同樣的操作。

mapEdges: transform each edge attribute in the graph using the map function.

實例:注意在mapEdges中使用的函數里,輸入參數x是一個Edge對象,返回對象則是Edge的屬性對象。在例子中,屬性對象的類型并沒有改變,(都是String)但屬性的值有所變化。也可以變成其它的類型的對象。

val sheyouGraph = graph.mapEdges(x => {if("roommate".equals(x.attr)) "sheyou" else x.attr})


mapVertices: transform each vertex attribute in the graph using the map function

跟mapEdges類似,mapVerticies中傳入的對象也是Vertex的實例化對象,返回值也是頂點的屬性對象:

val oneAttrGraph = graph.mapVertices((id, attr) => {attr._1+ " is:"+attr._2})

mapTriplets: Transforms each edge attribute using the map function, passing it the adjacent(臨近的) vertex attributes as well.

也就是在mapTriplets中,與mapEdges不同的地方僅僅在于可以使用的作為map條件的東西多了鄰近的頂點的屬性,最終改變的東西仍然是edge的屬性。如果轉換中不需要根據頂點的屬性,就直接用mapEdges就行了。

什么是Triplet:

Triplet的全稱是EdgeTriplet,繼承自Edge,所代表的entity是:An edge along with the vertex attributes of its neighboring vertices. 一個EdgeTriplet中包含srcId, dstId, attr(繼承自Edge)和srcAttr和dstAttr五個屬性。

graph.mapTriplets(triplet => {.....})

二、Structural Operators:

1. subgraph:

方法的定義:

def subgraph(

????epred: EdgeTriplet[VD, ED] => Boolean = (x => true),

????vpred: (VertexId, VD) => Boolean = ((v, d) => true)

): Graph[VD, ED]

返回的對象是一個圖,圖中包含著的頂點和邊分別要滿足vpred和epred兩個函數。(要注意,頂點和邊是完全不同的概念,如果一個邊被砍掉了,這個邊關聯的兩個頂點并不會受影響)

要注意,在圖里,如果一個頂點沒了,其對應的邊也就沒了,但邊沒了之后,點不會受影響。

所以,subgraph一般用于:restrict the graph to the vertices and edges of interest或者eliminate broken links.

2. joinVertices/outerJoinVerticies:

有時候需要從外部的RDD中跟Graph做數據的連接操作。例如:外部的user屬性想要跟現有的graph做一個合并,或者想把圖的頂點的屬性從一個圖遷移到另一個圖中。這些可以用join來完成。

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

def outerJoinVertices[U, VD2](table: RDD[(VertexId, U)])(mapFunc: (VertexId, VD, Option[U]) => VD2)(implicit eq: VD =:= VD2 = null): Graph[VD2, ED]

joinVertices: 將頂點跟輸入的RDD[(VertexId, U)]做關聯,返回一個新的圖。新的圖的屬性的類型跟原圖是一樣的,但值可以改變;在mapFunc中,可以使用原來的圖的頂點屬性和輸入的RDD的頂點屬性U來計算新的頂點屬性。輸入的RDD中每個vertex最多只能有一個vertex。如果原圖在input table中沒有對應的entry,則原來的屬性不做改變。

def joinVertices[U: ClassTag](table: RDD[(VertexId, U)])(mapFunc: (VertexId, VD, U) => VD): Graph[VD, ED]

從函數的定義可以看出,該操作不會改變vertex的屬性的類型,但值是可以改變的。比如first name需要加上last name。

事實上,joinVerticies方法的實現中就使用了outerJoinVerticies方法:

def joinVertices[U: ClassTag](table: RDD[(VertexId, U)])(mapFunc: (VertexId, VD, U) => VD) : Graph[VD, ED] = {

????val uf = (id: VertexId, data: VD, o: Option[U]) => {

????????o match {

????????????case Some(u) => mapFunc(id, data, u) case None => data

????????}

????}

????graph.outerJoinVertices(table)(uf)

}

outerJoinVertices與joinVertices很類似,但在map方法中可以修改vertex的屬性類型。由于并非所有的圖的頂點都一定能跟傳入的RDD匹配上,所以定義mapFunc的時候使用了option選項。對于joinVerticies方法,如果某個頂點沒有跟傳入的RDD匹配上,就直接用原有的值。因為joinVerticies并不改變頂點的數據類型(有沒有忘了option跟Some、None之間的愛恨情仇?使用Option的時候一定離不開match,要注意match的語法)。

val outDegGraph = graph.outDegrees

val degGraph = graph.outerJoinVertices(outDegGraph){????

????(id, oldAttr, outDeg) => {????

????????outDeg match{

????????????case Some(outDeg) => outDegcase None => 0

????????}

????}

}

3. aggregateMessages(原來的名字叫做mapReduceTriplets):

如果需要將頂點跟其鄰居的信息集成起來,可以使用aggregateMessages方法。比如,想要知道有多少人follow了一個用戶,或者follow用戶的平均年齡。

函數的定義:

def aggregateMessages[A: ClassTag](

????sendMsg: EdgeContext[VD, ED, A] => Unit,

????mergeMessage: (A, A) => A,

????tripletFields: TripletFields = TripletFields.ALL

): VertexRDD[A]

跟mapReduceTriplets的定義很類似:

def mapReduceTriplets[Msg](

????map: EdgeTriplet[VD, ED] => Iterator[(VertexId, Msg)],

????reduce: (Msg, Msg) => Msg

):VertexRDD[Msg]

VertexRDD繼承了RDD[(VertexId, VD)],本身自帶的泛型VD。

API:aggregate values from the neighboring edges and vertices of each vertex.

方法中的sendMsg是在圖中的每個邊都會被調用的,用于將message發送給相鄰頂點。

mergeMsg用于將sendMsg中發送給同一個頂點的message做組合。

tripletFields: 那些fields可以用于EdgeContext中,可用的值包括TripletFields.None, TripletFields.EdgeOnly, TripletFields.Src, TripletFields.Dst, TripletFields.ALL。默認為ALL,也就是所有的信息都要用,如果只需要用部分數據,可以單獨選擇部分屬性發送,可以提升計算效率。

其中,Src和Dst分別會將source和destination field進行傳遞,而且都會添加edge fields:

如果TripletFields中傳入的資源少了,也就是在sendMsg中需要使用到的信息并沒有包含在TripletFields中,可能會報空指針異常。

使用實例:計算graph的出度或入度:

val inDeg: RDD[(VertexId, Int)] = graph.aggregateMessages[Int](edgeContext => edgeContext.sendToDst(1), _+_)

由于這里我們并沒有用到edgeContext中的任何屬性,所以其實也可以在參數中添加TripletFields.None,從而提高一點執行效率:

graph.aggregateMessages[Int](ctx => ctx.sendToDst(1), _+_ , TripletFields.None)

TIPS: 什么是EdgeContext:EdgeContext中會將source和destination屬性以及Edge屬性都暴露出來,包含sendToSrc和sendToDst來將信息發送給source和destination屬性。

4. reverse: return a new graph with all edge directions reversed.

調用方法:

val reverseGraph = graph.reverse

5. mask

mask用于創建一個子圖,子圖中包含在輸入的圖中也包含的頂點和邊。該方法通常跟subgraph方法一起使用,來根據另一個關聯的圖來過濾當前的圖中展示的數據。

6. groupEdges:

groupEdges用于將多重圖中的相同的頂點之間的邊做合并(除了屬性其實沒其他可以合并的)。

def groupEdges(merge: (ED, ED) => ED): Graph[VD, ED]

7. 一組collect:

graph中有多個collect算法,包括collectEdges, collectNeighbours, collectNeighbourIds等

collectEdges: return an RDD that contains for each vertex its local edges

8. Pregel API:

圖本身就是內在的遞歸的數據結構,因為一個頂點的屬性可能依賴于其neighbor,而neighbor的屬性又依賴于他們的neighbour。所以很多重要的圖算法都會迭代計算每個頂點的屬性,直到達到一個穩定狀態。

GraphX中的Pregel操作符是一個批量同步并行(bulk-synchronous parallel message abstraction)的messaging abstraction,用于圖的拓撲結構(topology of the graph)。The Pregel operator executes in a series of super steps in whichvertices receive the sum of their inbound messagesfrom the previous super step,compute a new valuefor the vertex property, and thensend messages to neighboring verticesin the next super step. Message是作為edge triplet的一個函數并行計算的,message的計算可以使用source和dest頂點的屬性。沒有收到message的頂點在super step中被跳過。迭代會在么有剩余的信息之后停止,并返回最終的圖。

pregel的定義:

def pregel[A]

????(initialMsg: A,//在第一次迭代中每個頂點獲取的起始

????msgmaxIter: Int = Int.MaxValue,//迭代計算的次數

????activeDir: EdgeDirection = EdgeDirection.Out

)(

????vprog: (VertexId, VD, A) => VD,//頂點的計算函數,在每個頂點運行,根據頂點的ID,屬性和獲取的inbound message來計算頂點的新屬性值。頂一次迭代的時候,inbound message為initialMsg,且每個頂點都會執行一遍該函數。以后只有上次迭代中接收到信息的頂點會執行。

????sendMsg: EdgeTriplet[VD, ED] => Iterator[(VertexId, A)],//應用于頂點的出邊(out edges)用于接收頂點發出的信息

????mergeMsg: (A, A) => A//合并信息的算法

)

算法實現的大致過程:

var g = mapVertices((vid, vdata) => vprog(vid, vdata, initMsg)).cache //第一步是根據initMsg在每個頂點執行一次vprog算法,從而每個頂點的屬性都會迭代一次。

var messages = g.mapReduceTriplets(sendMsg, mergeMsg)

var messagesCount = messages.count

var i = 0

while(activeMessages > 0 && i < maxIterations){

????g = g.joinVertices(messages)(vprog).cache

????val oldMessages = messages

????messages = g.mapReduceTriplets(

????????sendMsg,

? ? ? ? mergeMsg,

????????Some((oldMessages, activeDirection))

????).cache()

????activeMessages = messages.count

????i += 1

}

g

pregel算法的一個實例:將圖跟一些一些初始的score做關聯,然后將頂點分數根據出度大小向外發散,并自己保留一份:

//將圖中頂點添加上該頂點的出度屬性

val graphWithDegree = graph.outerJoinVertices(graph.outDegrees){

????case (vid, name, deg) => (name, deg match {

????????case Some(deg) => deg+0.0

????????case None => 1.25}

????)

}//將圖與初始分數做關聯

val graphWithScoreAndDegree = graphWithDegree.outerJoinVertices(scoreRDD){

????case (vid, (name, deg), score) => (name,deg, score.getOrElse(0.0))

}

graphWithScoreAndDegree.vertices.foreach(x => println("++++++++++++id:"+x._1+"; deg: "+x._2._2+"; score:"+x._2._3))//將圖與初始分數做關聯

val graphWithScoreAndDegree = graphWithDegree.outerJoinVertices(scoreRDD){

????case (vid, (name, deg), score) => (name,deg, score.getOrElse(0.0))

}

graphWithScoreAndDegree.vertices.foreach(x => println("++++++++++++id:"+x._1+"; deg: "+x._2._2+"; score:"+x._2._3))

算法的第一步:將0.0(也就是傳入的初始值initMsg)跟各個頂點的值相加(還是原來的值),然后除以頂點的出度。這一步很重要,不能忽略。 并且在設計的時候也要考慮結果會不會被這一步所影響。

9. 計算圖的度、入度和出度:

graph.degrees

graph.outDegrees

graph.inDegrees

返回的對象是VertexRDD[Int]

注意的是返回的RDD對象中,度為0的頂點并不包含在內。

10. filter方法:先計算一些用于過濾的值(preprocess),然后在使用predicate進行過濾。

def filter[VD2: ClassTag, ED2: ClassTag](

????preprocess: Graph[VD, ED] => Graph[VD2, ED2],

????epred: (EdgeTriplet[VD2, ED2]) => Boolean = (x: EdgeTriplet[VD2, ED2]) => true,

????vpred: (VertexId, VD2) => Boolean = (v: VertexId, d: VD2) => true

):Graph[VD, ED] = {

????graph.mask(

????????preprocess(graph).subgraph(epred, vpred)

????)

}

preprocess:a function to compute new vertex and edge data before filtering

要注意最后返回的圖跟傳入的圖的頂點和邊的屬性類型是一樣的。

該方法可以用于在不改變頂點和邊的屬性值(要注意的是,在preprocess中,使用graph的時候可能會有類似于修改graph操作的api調用,但在調用的過程中,graph本身的值不會發生改變。比如在下邊的例子的中,graph做了一個跟其degree關聯的操作,但graph本身的值沒有任何變化)的情況下對圖進行基于某些屬性的過濾。這些屬性的值可以是計算得來的。例如,刪除圖中沒有出度的頂點:

graph.filter(

????graph => {

????????val degrees: VertexRDD[Int] = graph.outDegrees

????????graph.outerJoinVertices(degrees) {(vid, data, deg) => deg.getOrElse(0)}

????},

????vpred = (vid: VertexId, deg:Int) => deg > 0

)

11. groupEdges:合并兩個頂點中的多條邊稱為一條邊。要獲取正確的結果,graph必須調用partitionBy來做partition。這是因為該操作假定需要一起合并的邊都分布在同一個partition上。所以在調用groupEdges之前必須調用partitionBy。

def groupEdges(merge: (ED, ED) => ED): Graph[VD, ED],也就是對邊的屬性做合并操作。

partitionBy: repartitions the edges in the graph according to partitionStrategy.

def partitionBy(partitionStrtegy: PartitionStrategy): Graph[VD, ED]

12. convertToCanonicalEdges:將雙向變轉化為單向邊。

具體的算法是:將所有邊都轉化成srcId小于dstId的邊,然后合并多余的邊。

二、Graph Builders:

http://spark.apache.org/docs/latest/graphx-programming-guide.html#graph_builders

GraphX提供了一組使用vertex和edge的集合來構建一個圖的方法。這些Graph Builder默認不會對邊做repartition,邊一般留在其原來的默認的partition中,例如其原來的HDFS的block。

1. GraphLoader.edgeListFile:

用于從一組edge(每個edge中包括簡單的source id和destination id)中來構建一個graph,自動創建其中涉及的頂點,頂點和邊的屬性都設置為1。

def edgeListFile(

????sc: SparkContext,

????path: String,

????canonicalOrientation: Boolean = false,

????minEdgePartitions: Int = 1,

????edgeStorageLevel: StorageLevel = StorageLevel.MEMORY_ONLY,

????vertexStorageLevel: StorageLevel = StorageLevel.MEMORY_ONLY

): Graph[Int, Int]

canonicalOrientation參數可以強制讓邊按照srcId

2. Graph.apply:使用頂點和邊的RDD對象來創建一個圖。重復的頂點被任意拋棄,edgeRDD中有而verticiesRDD中沒有的頂點會被賦予一個默認的屬性值。

def apply[VD, ED](

????vertices: RDD[(VertexId, VD)],

????edges: RDD[Edge[ED]],

????defaultVertexAttr: VD = null,

????edgeStorageLevel: StorageLevel = StorageLevel.MEMORY_ONLY,

????vertexStorageLevel: StorageLevel = StorageLevel.MEMORY_ONLY

): Graph[VD, ED]

3. Graph.fromEdges: 從單獨的一個邊的RDD中構建一個圖。自動創建邊中使用的頂點,并賦予默認值。

def fromEdges[VD, ED](

????edges: RDD[Edge[ED]],

????defaultValue: VD,

????edgeStorageLevel: StorageLevel = StorageLevel.MEMORY_ONLY,

????vertexStorageLevel: StorageLevel = StorageLevel.MEMORY_ONLY

): Graph[VD, ED]

4. Graph.fromEdgeTuples: 使用一個edge tuple的RDD創建圖。邊的值默認為1,頂點自動創建并賦予默認值。該方法也支持對邊的deduplication(也就是去重)。如果發現多個相同的邊,就將他們合并,屬性值計算他們的和。或者將重復的邊當做多條邊。

def fromEdgeTuples[VD](

????rawEdges: RDD[(VertexId, VertexId)],

????defaultValue: VD,

????uniqueEdges: Option[PartitionStrategy] = None,

????edgeStorageLevel: StorageLevel = StorageLevel.MEMORY_ONLY,

????vertexStorageLevel: StorageLevel = StorageLevel.MEMORY_ONLY

): Graph[VD, Int]

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容