GraphX PageRank

原文鏈接:https://blog.csdn.net/lsshlsw/article/details/41176093

原文出處:https://blog.csdn.net/lsshlsw/article/details/41176093

 

一:算法介紹
        PageRank是Google專有的算法,用於衡量特定網頁相對於搜索引擎索引中的其他網頁而言的重要程度。

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

二:源碼分析
I:PregelPageRank
文件位置:spark-1.0.1\graphx\src\main\scala\org\apache\spark\graphx\lib\PageRank.scala

1.1:代碼簡介
        該PageRank模型提供了兩種調用方式:

        第一種:(靜態)在調用時提供一個參數number,用於指定迭代次數,即無論結果如何,該算法在迭代number次後停止計算,返回圖結果。

        第二種:(動態)在調用時提供一個參數tol,用於指定前後兩次迭代的結果差值應小於tol,以達到最終收斂的效果時才停止計算,返回圖結果。

        這是GraphX提供的用Pregel的模型改進後產生的圖算法,通常我們在進行使用PageRank的代碼編寫時並不涉及去改動這份源碼,而是直接調用:

1.2:源碼解析
     首先解釋下列代碼中run()的幾個參數:

         graph:進行PageRank計算的圖模型

         numIter:固定的PageRank計算的迭代次數

         resetProb:隨機重置的概率,通常都是0.15

         Graph:返回值,以圖的形式包括最終的頂點值(pagerank值)和邊值(權重值),進而得到最終的排名結果

def run[VD: ClassTag, ED: ClassTag](
      graph: Graph[VD, ED],numIter: Int, resetProb: Double = 0.15): Graph[Double, Double] =
  {
   //下列這段代碼用於初始化PageRank圖模型,具體內容是賦予每個頂點屬性爲值1.0,賦予每條邊屬性爲值“1/該邊的出發頂點的出度數”。
    val pagerankGraph: Graph[Double, Double] = graph
   //將每個頂點進行連接(度的傳遞)得到頂點屬性值爲出度數
     .outerJoinVertices(graph.outDegrees) { (vid, vdata, deg) =>deg.getOrElse(0) }
   //通過頂點的出度數爲每條邊設置權重值;這裏是Triplet型的迭代器不停地執行一個map函數來遍歷得到每條邊的權重值,值爲1.0/頂點出度數
      .mapTriplets( e => 1.0 / e.srcAttr )
   //設置每個頂點的初始屬性值爲1.0
      .mapVertices( (id,attr) => 1.0 )
      .cache()  //將完成初始化的圖緩存操作
   //以下將定義三個所需函數來完成GraphX對PageRank的算法實現
    //用作 Pregel的message
 
    //第一個函數用於返回一個考慮“隨機事件”發生後的計算結果
    def vertexProgram(id: VertexId, attr: Double, msgSum: Double): Double=
      resetProb + (1.0 - resetProb) * msgSum
    //第二個函數用於得到一個迭代器,裏面包含了兩個信息:該邊的目的ID、該邊的源屬性值和權重的乘積(該邊傳遞的實際PR值)
    def sendMessage(edge: EdgeTriplet[Double, Double]) =
      Iterator((edge.dstId, edge.srcAttr* edge.attr))
    //第三個函數用於將頂點屬性值和傳遞的值進行累加
    def messageCombiner(a: Double, b: Double): Double = a + b
 
    //在該PageRank模型中每個頂點接受到的初始傳遞信息都是0.0
    val initialMessage = 0.0
 
    // 執行 pregel 模型算法(固定的迭代次數)
    Pregel(pagerankGraph, initialMessage, numIter, activeDirection= EdgeDirection.Out)(
      vertexProgram,sendMessage, messageCombiner)
  }
        至此第一種(靜態)PageRank模型計算結束


        以下是第二種(動態)PageRank模型計算,相同代碼就不再累贅解釋

        初始化參數和上面不同的是少了numIter(迭代次數),多了tol(比較兩次迭代的結果差)

def runUntilConvergence[VD: ClassTag, ED: ClassTag](
      graph: Graph[VD, ED], tol: Double, resetProb:Double = 0.15): Graph[Double,Double] =
  {
// 下段代碼同樣用於初始化圖形
    val pagerankGraph: Graph[(Double, Double), Double] = graph
//同上,將每個頂點進行連接(度的傳遞)得到頂點屬性值爲出度數
     .outerJoinVertices(graph.outDegrees) {
        (vid, vdata, deg)=> deg.getOrElse(0)
      }
//邊屬性值(權重)的初始化,值爲1.0/頂點出度數
      .mapTriplets( e => 1.0 / e.srcAttr )
// 頂點屬性值的初始化,但是屬性值帶兩個參數即(初始PR值,兩次迭代結果的差值)
      .mapVertices( (id,attr) => (0.0, 0.0) )
      .cache()
        同樣需要定義以下三個函數來完成GraphX對PageRank的實現

        同樣用作Pregel的message

 

// 第一個函數多了一個返回值delta(newPR-oldPR)
    def vertexProgram(id: VertexId, attr: (Double, Double), msgSum: Double): (Double, Double) = {
      val (oldPR, lastDelta) = attr
      val newPR = oldPR + (1.0 - resetProb) * msgSum
      (newPR, newPR - oldPR)
    }
// 第二個函數同樣用於得到一個迭代器,但是多了一個條件判定:如果源頂點的delta值小於tol就清空迭代器即返回空迭代。
    def sendMessage(edge: EdgeTriplet[(Double, Double), Double]) = {
      if (edge.srcAttr._2 > tol) {
        Iterator((edge.dstId, edge.srcAttr._2 * edge.attr))
      } else {
        Iterator.empty
      }
    }
 
    def messageCombiner(a: Double, b: Double): Double = a + b
 
// 每個頂點接受到的初始傳遞信息值不是0,而是resetProb / (1.0 - resetProb)
    val initialMessage = resetProb / (1.0 - resetProb)
 
// 動態執行 Pregel 模型(直至結果最終收斂)
    Pregel(pagerankGraph, initialMessage, activeDirection = EdgeDirection.Out)(
      vertexProgram, sendMessage, messageCombiner)
      .mapVertices((vid, attr) => attr._1)
  } 
    至此第二種(動態)PageRank模型計算結束

1.3:涉及代碼
spark-1.0.1\graphx\src\main\scala\org\apache\spark\graphx:

  GraphOps.scala

  Pregel.scala

 

應該說明一下:

    Pregel相當於圖計算的引擎,用於圖計算的大框架(對頂點的消息計算、消息發送、消息合併),它是圖迭代的執行者。lib中的所有算法模型最後都會調用Pregel。

     GraohOps 則相當於一個可以快速調用方法的清單,裏面給出了很多類或方法的入口;在此例中的PageRank()、Pregel()方法都是從這啓動的。

II:PageRank簡例
2.1:代碼簡介
      這段代碼是官方GraphX guide 提供的,由於使用的就是spark包中的自帶數據,所以用於測試非常簡單。另外,這個例子確實非常簡短,因爲自帶數據量非常非常小(但這並不意味我們不可以修改原始數據)!

      下面是全部的源碼,除開註釋可看出代碼非常的少,那是因爲GraphX爲了讓開發者方便直接提供了多個算法模型(如上述的PageRank.scala文件),只需代入數據直接調用就行。

2.2:源碼解析
// 從特定的邊列表文件中讀取數據生成圖框架
val graph = GraphLoader.edgeListFile(sc, "graphx/data/followers.txt")
// 用上面的圖框架來調用pageRank(動態)算法
//特別注意:靜態調用的方法名是staticPageRank(Int)
// vertices將返回頂點屬性
val ranks = graph.pageRank(0.0001).vertices
// 將上面得到的ranks(頂點屬性)和用戶進行關係連接
// 首先也是讀取一個包含了用戶信息的文件,然後調用了一個map函數,即將文件裏的每行數據按 ”,” 切開並返回存儲了處理後數據的RDD
val users = sc.textFile("graphx/data/users.txt").map { line =>
  val fields = line.split(",")
  (fields(0).toLong, fields(1))
}
// 這裏具體實現了將ranks和用戶列表一一對應起來
// 從map函數的內容可以看出是按id來進行連接,但返回的結果只含用戶名和它的相應rank值
val ranksByUsername = users.join(ranks).map {
  case (id, (username, rank)) => (username, rank)
}
// 收集上面RDD裏的數據並打印出來
println(ranksByUsername.collect().mkString("\n"))
2.3:輸入數據
在spark-1.0.1\graphx\data 目錄下有兩份數據文件:

followers.txt

 

Users.txt

 

 

這兩份數據的數據量非常小,但是作爲測試可以更好的分析其算法原理。

 

2.4:輸出結果
㈠動態調用

結果1:

 

 

下面變更下參數(tol值)

結果2:

可以看出和上面的結果相差較大,事實上,參數值越小得到的結果越有說服力。

 

⑵靜態調用

結果1:

 

 

增加迭代次數:

 

 

結果2:

 

結果依然不夠準確,再次增加迭代次數:

 

結果3:

 

結果依然不夠準確,再次增加迭代次數:

 

結果4:

 

可以看出對於該數據,用靜態算法很難得到準確的結果

三:問題及改進
I:格式問題
    ①不同的方法對數據格式有不同的要求,例如edgeListFile 讀取邊列表文件時,要求數據格式必須爲:

     vId  vId

     vId  vId

     ...  ...

 

   ②數據的提取,對於一行數據,哪些纔是我們真正使用和想表現的,需要自定義方法來達成。同時要注意包含相應的標識數據(如VertexID)

 

   ③有個問題要特別注意:即兩種調用方式的方法名,他們除了參數類型不同,名字也是不同的:

       動態調用:pageRank(double)    //開頭不大寫

       靜態調用:staticPageRank(int)   //開頭不大寫,但P處要大寫

II:模型(代碼)問題
①PageRank本身是存在多種計算漏洞的,如“黑洞效應”:當一個頂點只有入度而沒有出度時將不斷的吞噬掉該有向圖其他頂點的PR值,最終使得所有頂點的PR值都變成0。

不過上述的計算模型用阻尼係數resetProb解決了這個問題。

 

②代入參數不同而造成的結果不同,例如靜態和動態調用哪種更適合,又或者迭代次數的選擇、前後兩次迭代的差值限定又該選擇多少,這些都是沒有固定標準的。

另外,從測試結果可以看出目前靜態調用方式(即設置固定迭代次數)的結果是存在問題的,但這個問題究竟是我的使用方式不對,還是數據本身的原因或者是其他因素還有待確認。

III:應用場景
Google的網頁排名並非如此單純的PageRank算法,它考慮的綜合因素至少有10點以上。

但該算法仍然可以爲排名計算(網頁排名、用戶排名等)提供其中一個可靠的依據。在目前,我們應該不會去改動模型代碼,而是合理的安排需要處理的數據。該算法處理的場景原型很容易看出:有相互聯繫的事物網中,評選出最受“歡迎”的事物。什麼叫受歡迎?——被其他事物選擇、依賴、信任、消費等等。
 ———————————————— 
版權聲明:本文爲CSDN博主「breeze_lsw」的原創文章,遵循CC 4.0 by-sa版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/lsshlsw/article/details/41176093

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章