聚类分析是一个无监督学习 (Unsupervised Learning) 过程, 一般是用来对数据对象按照其特征属性进行分组,经常被应用在客户分群,欺诈检测,图像分析等领域。K-means 应该是最有名并且最经常使用的聚类算法了,其原理比较容易理解,并且聚类效果良好,有着广泛的使用。
和诸多机器学习算法一样,K-means 算法也是一个迭代式的算法,其主要步骤如下:
第一步,选择 K 个点作为初始聚类中心。
第二步,计算其余所有点到聚类中心的距离,并把每个点划分到离它最近的聚类中心所在的聚类中去。在这里,衡量距离一般有多个函数可以选择,最常用的是欧几里得距离
(Euclidean Distance), 也叫欧式距离。公式如下:
body{margin:0;padding:0}
其中 C 代表中心点,X 代表任意一个非中心点。
第三步,重新计算每个聚类中所有点的平均值,并将其作为新的聚类中心点。
最后,重复 (二),(三)
步的过程,直至聚类中心不再发生改变,或者算法达到预定的迭代次数,又或聚类中心的改变小于预先设定的阀值。
在实际应用中,K-means 算法有两个不得不面对并且克服的问题。
聚类个数 K 的选择。K 的选择是一个比较有学问和讲究的步骤,我们会在后文专门描述如何使用 Spark 提供的工具选择 K。
初始聚类中心点的选择。选择不同的聚类中心可能导致聚类结果的差异。
Spark MLlib K-means 算法的实现在初始聚类点的选择上,借鉴了一个叫 K-means||的类 K-means++ 实现。K-means++ 算法在初始点选择上遵循一个基本原则: 初始聚类中心点相互之间的距离应该尽可能的远。基本步骤如下:
第一步,从数据集 X 中随机选择一个点作为第一个初始点。
第二步,计算数据集中所有点与最新选择的中心点的距离 D(x)。
第三步,选择下一个中心点,使得body{margin:0;padding:0}
第四部,重复 (二),(三) 步过程,直到 K 个初始点选择完成。
MLlib 的 K-means 实现
Spark MLlib 中 K-means 算法的实现类 (KMeans.scala) 具有以下参数,具体如下。
图 1. MLlib K-means 算法实现类预览
body{margin:0;padding:0}
通过下面默认构造函数,我们可以看到这些可调参数具有以下初始值。
图 2. MLlib K-means 算法参数初始值
body{margin:0;padding:0}
参数的含义解释如下:
k 表示期望的聚类的个数。
maxInterations 表示方法单次运行最大的迭代次数。
runs 表示算法被运行的次数。K-means
算法不保证能返回全局最优的聚类结果,所以在目标数据集上多次跑 K-means 算法,有助于返回最佳聚类结果。
initializationMode 表示初始聚类中心点的选择方式, 目前支持随机选择或者
K-means||方式。默认是 K-means||。
initializationSteps表示 K-means||方法中的部数。
epsilon 表示 K-means 算法迭代收敛的阀值。
seed 表示集群初始化时的随机种子。
通常应用时,我们都会先调用 KMeans.train 方法对数据集进行聚类训练,这个方法会返回 KMeansModel 类实例,然后我们也可以使用 KMeansModel.predict 方法对新的数据点进行所属聚类的预测,这是非常实用的功能。
KMeans.train 方法有很多重载方法,这里我们选择参数最全的一个展示。
图 3. KMeans.train 方法预览
body{margin:0;padding:0}
KMeansModel.predict 方法接受不同的参数,可以是向量,或者 RDD,返回是入参所属的聚类的索引号。
图 4. KMeansModel.predict 方法预览
body{margin:0;padding:0}
聚类测试数据集简介
在本文中,我们所用到目标数据集是来自 UCI Machine Learning Repository 的 Wholesale customer Data Set。UCI 是一个关于机器学习测试数据的下载中心站点,里面包含了适用于做聚类,分群,回归等各种机器学习问题的数据集。
Wholesale customer Data Set 是引用某批发经销商的客户在各种类别产品上的年消费数。为了方便处理,本文把原始的 CSV 格式转化成了两个文本文件,分别是训练用数据和测试用数据。
图 5. 客户消费数据格式预览
body{margin:0;padding:0}
读者可以从标题清楚的看到每一列代表的含义,当然读者也可以到 UCI 网站上去找到关于该数据集的更多信息。虽然 UCI 的数据可以自由获取并使用,但是我们还是在此声明,该数据集的版权属 UCI 以及其原始提供组织或公司所有。
案例分析和编码实现
本例中,我们将根据目标客户的消费数据,将每一列视为一个特征指标,对数据集进行聚类分析。代码实现步骤如下
清单 1. 聚类分析实现类源码
import org.apache.spark.{SparkContext, SparkConf}
import org.apache.spark.mllib.clustering.{KMeans, KMeansModel}
import org.apache.spark.mllib.linalg.Vectors
object KMeansClustering {
def main (args: Array[String]) {
if (args.length < 5) {
println("Usage:KMeansClustering trainingDataFilePath testDataFilePath numClusters
numIterations runTimes")
sys.exit(1)
}
val conf = new
SparkConf().setAppName("Spark MLlib Exercise:K-Means Clustering")
val sc = new SparkContext(conf)
val rawTrainingData = sc.textFile(args(0))
val parsedTrainingData =
rawTrainingData.filter(!isColumnNameLine(_)).map(line => {
Vectors.dense(line.split("\t").map(_.trim).filter(!"".equals(_)).map(_.toDouble))
}).cache()
// Cluster the data into two classes using KMeans
val numClusters = args(2).toInt
val numIterations = args(3).toInt
val runTimes =
args(4).toInt
var clusterIndex:Int = 0
val clusters:KMeansModel =
KMeans.train(parsedTrainingData, numClusters, numIterations,runTimes)
println("Cluster Number:" + clusters.clusterCenters.length)
println("Cluster Centers Information Overview:")
clusters.clusterCenters.foreach(
x => {
println("Center Point of Cluster " + clusterIndex + ":")
println(x)
clusterIndex += 1
})
//begin to check which cluster each test data belongs to based on the clustering result
val rawTestData = sc.textFile(args(1))
val parsedTestData = rawTestData.map(line =>
{
Vectors.dense(line.split("\t").map(_.trim).filter(!"".equals(_)).map(_.toDouble))
})
parsedTestData.collect().foreach(testDataLine => {
val predictedClusterIndex:
Int = clusters.predict(testDataLine)
println("The data " + testDataLine.toString + " belongs to cluster " +
predictedClusterIndex)
})
println("Spark MLlib K-means clustering test finished.")
}
private def
isColumnNameLine(line:String):Boolean = {
if (line != null &&
line.contains("Channel")) true
else false
}
该示例程序接受五个入参,分别是
训练数据集文件路径
测试数据集文件路径
聚类的个数
K-means 算法的迭代次数
K-means 算法 run 的次数
运行示例程序
和本系列其他文章一样,我们依然选择使用 HDFS 存储数据文件。运行程序之前,我们需要将前文提到的训练和测试数据集上传到 HDFS。
图 6. 测试数据的 HDFS 目录
body{margin:0;padding:0}
清单 2. 示例程序运行命令
./spark-submit --class com.ibm.spark.exercise.mllib.KMeansClustering \
--master spark://:7077 \
--num-executors 6 \
--driver-memory 3g \
--executor-memory 512m \
--total-executor-cores 6 \
/home/fams/spark_exercise-1.0.jar \
hdfs://:9000/user/fams/mllib/wholesale_customers_data_training.txt \
hdfs://:9000/user/fams/mllib/wholesale_customers_data_test.txt \
8 30 3
图 7. K-means 聚类示例程序运行结果
body{margin:0;padding:0}
如何选择 K
前面提到 K 的选择是 K-means 算法的关键,Spark MLlib 在 KMeansModel 类里提供了 computeCost 方法,该方法通过计算所有数据点到其最近的中心点的平方和来评估聚类的效果。一般来说,同样的迭代次数和算法跑的次数,这个值越小代表聚类的效果越好。但是在实际情况下,我们还要考虑到聚类结果的可解释性,不能一味的选择使 computeCost 结果值最小的那个 K。
清单 3. K 选择示例代码片段
val ks:Array[Int] = Array(3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20)
ks.foreach(cluster => {
val model:KMeansModel = KMeans.train(parsedTrainingData, cluster,30,1)
val ssd = model.computeCost(parsedTrainingData)
println("sum of squared distances of points to their nearest center when k=" + cluster + " -> "+ ssd)
})
图 8. K 选择示例程序运行结果
body{margin:0;padding:0}
从上图的运行结果可以看到,当 K=9 时,cost 值有波动,但是后面又逐渐减小了,所以我们选择 8 这个临界点作为 K 的个数。当然可以多跑几次,找一个稳定的 K 值。理论上 K 的值越大,聚类的 cost 越小,极限情况下,每个点都是一个聚类,这时候 cost 是 0,但是显然这不是一个具有实际意义的聚类结果。