Spark MLlib分佈式機器學習源碼分析:K-means聚類

​    Spark是一個極爲優秀的大數據框架,在大數據批處理上基本無人能敵,流處理上也有一席之地,機器學習則是當前正火熱AI人工智能的驅動引擎,在大數據場景下如何發揮AI技術成爲優秀的大數據挖掘工程師必備技能。本文結合機器學習思想與Spark框架代碼結構來實現分佈式機器學習過程,希望與大家一起學習進步~

目錄

 1.Kmeans聚類

 2.Kmeans++

 3.Kmeans||

 4.Spark實踐

 5.源代碼分析


 

    本文采用的組件版本爲:Ubuntu 19.10、Jdk 1.8.0_241、Scala 2.11.12、Hadoop 3.2.1、Spark 2.4.5,老規矩先開啓一系列Hadoop、Spark服務與Spark-shell窗口:

 

1.Kmeans聚類

  kmeans算法又名k均值算法。其算法思想大致爲:先從樣本集中隨機選取 kk 個樣本作爲簇中心,並計算所有樣本與這k個“簇中心”的距離,對於每一個樣本,將其劃分到與其距離最近的“簇中心”所在的簇中,對於新的簇計算各個簇的新的“簇中心”。


  根據以上描述,我們大致可以猜測到實現kmeans算法的主要三點:


  (1)簇個數k的選擇
  (2)各個樣本點到“簇中心”的距離
  (3)根據新劃分的簇,更新“簇中心”

 

    K-Means的算法流程如下:

 

  • 從數據集中隨機選取 K 個點作爲初始聚類的中心,中心點爲 

  • 針對數據集中每個樣本 𝑥𝑖xi,計算它們到各個聚類中心點的距離,到哪個聚類中心點的距離最小,就將其劃分到對應聚類中心的類中

  • 針對每個類別 𝑖i ,重新計算該類別的聚類中心  (其中 |𝑖||i| 表示的是該類別數據的總個數)

  • 重複第二步和第三步,直到聚類中心的位置不再發生變化(我們也可以設置迭代次數)

  k-means算法雖然簡單快速,但是存在下面的缺點:

  • 聚類中心的個數K需要事先給定,但在實際中K值的選定是非常困難的,很多時候我們並不知道給定的數據集應該分成多少個類別才最合適。

  • k-means算法需要隨機地確定初始聚類中心,不同的初始聚類中心可能導致完全不同的聚類結果。

 

  第一個缺陷我們很難在k-means算法以及其改進算法中解決,但是我們可以通過k-means++算法來解決第二個缺陷。

 

 2.Kmeans++

    由於 K-means 算法的分類結果會受到初始點的選取而有所區別,因此有提出這種算法的改進: K-means++ 。其實這個算法也只是對初始點的選擇有改進而已,其他步驟都一樣。初始質心選取的基本思路就是,初始的聚類中心之間的相互距離要儘可能的遠。

 

    算法描述如下:

 

  • 步驟一:隨機選取一個樣本作爲第一個聚類中心 c1;

  • 步驟二:

    • 計算每個樣本與當前已有類聚中心最短距離(即與最近一個聚類中心的距離),用 D(x)表示;

    • 這個值越大,表示被選取作爲聚類中心的概率較大;

    • 最後,用輪盤法選出下一個聚類中心;

  • 步驟三:重複步驟二,知道選出 k 個聚類中心。

 

    選出初始點後,就繼續使用標準的 k-means 算法了。K-means++ 能顯著的改善分類結果的最終誤差。

 

    儘管計算初始點時花費了額外的時間,但是在迭代過程中,k-means本身能快速收斂,因此算法實際上降低了計算時間。

 

    雖然k-means++算法可以確定地初始化聚類中心,但是從可擴展性來看,它存在一個缺點,那就是它內在的有序性特性:下一個中心點的選擇依賴於已經選擇的中心點。針對這種缺陷,k-means||算法提供瞭解決方法。

 3.Kmeans||

    k-means++ 最主要的缺點在於其內在的順序執行特性,得到 k 個聚類中心必須遍歷數據集 k 次,並且當前聚類中心的計算依賴於前面得到的所有聚類中心,這使得算法無法並行擴展,極大地限制了算法在大規模數據集上的應用。

    k-means|| 主要思路在於改變每次遍歷時的取樣策略,並非按照 k-means++ 那樣每次遍歷只取樣一個樣本,而是每次遍歷取樣 O(k) 個樣本,重複該取樣過程大約 O(logn) 次,重複取樣過後共得到 O(klogn) 個樣本點組成的集合,該集合以常數因子近似於最優解,然後再聚類這O(klogn) 個點成 k 個點,最後將這 k 個點作爲初始聚類中心送入Lloyd迭代中,實際實驗證明 O(logn) 次重複取樣是不需要的,一般5次重複取樣就可以得到一個較好的聚類初始中心。

 

4.Spark實踐

    在下面的示例中,在加載和解析數據之後,我們使用 KMeans對象將數據聚類爲兩個聚類。所需聚類的數量傳遞給算法。然後,我們計算平方誤差的集合和內(WSSSE)。您可以通過增加k來減少此錯誤度量。實際上,最佳k通常是WSSSE圖中存在“肘”的那個。

import org.apache.spark.mllib.clustering.{KMeans, KMeansModel}import org.apache.spark.mllib.linalg.Vectors// 加載和解析數據val data = sc.textFile("data/mllib/kmeans_data.txt")val parsedData = data.map(s => Vectors.dense(s.split(' ').map(_.toDouble))).cache()// Cluster the data into two classes using KMeans 使用Kmeans將數據聚爲兩類val numClusters = 2val numIterations = 20val clusters = KMeans.train(parsedData, numClusters, numIterations)// 計算WSSSE評估聚類結果val WSSSE = clusters.computeCost(parsedData)println(s"Within Set Sum of Squared Errors = $WSSSE")// 保存和加載模型clusters.save(sc, "target/org/apache/spark/KMeansExample/KMeansModel")val sameModel = KMeansModel.load(sc, "target/org/apache/spark/KMeansExample/KMeansModel")

 5.源代碼分析

   在spark中,org.apache.spark.mllib.clustering.KMeans文件實現了k-means算法以及k-means||算法,LocalKMeans文件實現了k-means++算法。在分步驟分析spark中的源碼之前我們先來了解KMeans類中參數的含義。

class KMeans private (    private var k: Int,//聚類個數    private var maxIterations: Int,//迭代次數    private var runs: Int,//運行kmeans算法的次數    private var initializationMode: String,//初始化模式    private var initializationSteps: Int,//初始化步數    private var epsilon: Double,//判斷kmeans算法是否收斂的閾值    private var seed: Long)

 

    在上面的定義中,k表示聚類的個數,maxIterations表示最大的迭代次數,runs表示運行KMeans算法的次數,在spark 2.0開始,該參數已經不起作用了。爲了更清楚的理解算法我們可以認爲它爲1。initializationMode表示初始化模式,有兩種選擇:隨機初始化和通過k-means||初始化,默認是通過k-means||初始化。initializationSteps表示通過k-means||初始化時的迭代步驟,默認是5,這是spark實現與第三章的算法步驟不一樣的地方,這裏迭代次數人爲指定, 而第三章的算法是根據距離得到的迭代次數,爲log(phi)。epsilon是判斷算法是否已經收斂的閾值。

 

  • 第一步,隨機初始化k箇中心點很簡單,具體代碼如下:

private def initRandom(data: RDD[VectorWithNorm])  : Array[Array[VectorWithNorm]] = {    //採樣固定大小爲k的子集    //這裏run表示我們運行的KMeans算法的次數,默認爲1,以後將停止提供該參數    val sample = data.takeSample(true, runs * k, new XORShiftRandom(this.seed).nextInt()).toSeq    //選取k個初始化中心點    Array.tabulate(runs)(r => sample.slice(r * k, (r + 1) * k).map { v =>      new VectorWithNorm(Vectors.dense(v.vector.toArray), v.norm)    }.toArray)  }
  • 第二步,通過已知的中心點,循環迭代求得其它的中心點。

var step = 0while (step < initializationSteps) {    val bcNewCenters = data.context.broadcast(newCenters)    val preCosts = costs    //每個點距離最近中心的代價    costs = data.zip(preCosts).map { case (point, cost) =>          Array.tabulate(runs) { r =>            //pointCost獲得與最近中心點的距離            //並與前一次迭代的距離對比取更小的那個            math.min(KMeans.pointCost(bcNewCenters.value(r), point), cost(r))          }        }.persist(StorageLevel.MEMORY_AND_DISK)    //所有點的代價和    val sumCosts = costs.aggregate(new Array[Double](runs))(          //分區內迭代          seqOp = (s, v) => {            // s += v            var r = 0            while (r < runs) {              s(r) += v(r)              r += 1            }            s          },          //分區間合併          combOp = (s0, s1) => {            // s0 += s1            var r = 0            while (r < runs) {              s0(r) += s1(r)              r += 1            }            s0          }        )    //選擇滿足概率條件的點    val chosen = data.zip(costs).mapPartitionsWithIndex { (index, pointsWithCosts) =>        val rand = new XORShiftRandom(seed ^ (step << 16) ^ index)        pointsWithCosts.flatMap { case (p, c) =>          val rs = (0 until runs).filter { r =>            //此處設置l=2k            rand.nextDouble() < 2.0 * c(r) * k / sumCosts(r)          }          if (rs.length > 0) Some(p, rs) else None        }      }.collect()      mergeNewCenters()      chosen.foreach { case (p, rs) =>        rs.foreach(newCenters(_) += p.toDense)      }      step += 1}
  • 第三步,求最終的k個點。

val bcCenters = data.context.broadcast(centers)    //計算權重值,即各中心點所在類別的個數    val weightMap = data.flatMap { p =>      Iterator.tabulate(runs) { r =>        ((r, KMeans.findClosest(bcCenters.value(r), p)._1), 1.0)      }    }.reduceByKey(_ + _).collectAsMap()    //最終的初始化中心    val finalCenters = (0 until runs).par.map { r =>      val myCenters = centers(r).toArray      val myWeights = (0 until myCenters.length).map(i => weightMap.getOrElse((r, i), 0.0)).toArray      LocalKMeans.kMeansPlusPlus(r, myCenters, myWeights, k, 30)    }

    Spark kmeans族的聚類算法的內容至此結束,有關Spark的基礎文章可參考前文:

    Spark MLlib分佈式機器學習源碼分析:矩陣向量

    Spark MLlib分佈式機器學習源碼分析:基本統計

    Spark MLlib分佈式機器學習源碼分析:線性模型

    Spark MLlib分佈式機器學習源碼分析:樸素貝葉斯

    Spark MLlib分佈式機器學習源碼分析:決策樹算法

    Spark MLlib分佈式機器學習源碼分析:集成樹模型

    Spark MLlib分佈式機器學習源碼分析:協同過濾

 

    參考鏈接:

    http://spark.apache.org/docs/latest/mllib-clustering.html

    https://github.com/endymecy/spark-ml-source-analysis

 

 

歷史推薦

“高頻面經”之數據分析篇

“高頻面經”之數據結構與算法篇

“高頻面經”之大數據研發篇

“高頻面經”之機器學習篇

“高頻面經”之深度學習篇

爬蟲實戰:Selenium爬取京東商品

爬蟲實戰:豆瓣電影top250爬取

爬蟲實戰:Scrapy框架爬取QQ音樂

數據分析與挖掘

數據結構與算法

機器學習與大數據組件

歡迎關注,感謝“在看”,隨緣稀罕~

 

 

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