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音乐

数据分析与挖掘

数据结构与算法

机器学习与大数据组件

欢迎关注,感谢“在看”,随缘稀罕~

 

 

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