此算法最后一步为K-means聚类算法, 所以先简单介绍下K-means聚类算法.
K-means聚类算法简介
目标
K-means聚类算法的核心目标是将给定的数据集划分为K个簇, 并给出每个数据对应的簇中心点.
算法执行步骤
- 数据预处理, 如归一化、离群点处理等.
- 随机选择k簇中心, 记作u1(0)、u2(0)……uk(0).
- 定义代价函数:
其中xi代表第i个样本, ci是xi所属于的簇, uci代表簇对应的中心点, M 是样本总数. - 令t=0,1,2,…为迭代步数, 重复下面过程知道J收敛:
- 对于每一个样本xi, 将其分配到距离最近的簇.
- 对于每一个类簇k, 重新计算该类簇的中心:
K-means聚类算法在迭代时, 假设当前J没有达到最小值, 那么首先固定簇中心{uk}, 调整每个样例xi所属的类别cj来让J函数减少; 然后固定{ci}, 调整簇中心{uk}使J减少. 这两个过程交替循环, J单调递减: 当J减少到最小值时, {uk}和{ci}也同时收敛.
K-means聚类算法优缺点
缺点
- 受初始值和离群点影响每次的结果不稳定.
- 结果通常不是全局最优而是局部最优解.
- 无法很好解决数据簇分布差别比较大的情况, 例如一类是另一类样本数量的100倍.
- 不太适用于离散分类.
优点
- 可伸缩
- 高效
- 计算复杂度O(NKt)接近于线性, 其中N是数据对象的数目, K是聚类簇数, t是迭代的轮数.
谱聚类算法
谱聚类算法是建立在谱图理论的基础上的算法, 与传统的聚类算法相比, 它能在任意形状的样本空间上聚类且能够收敛到全局最优解. 谱聚类算法的主要思想是将聚类问题转换为无向图的划分问题.
谱聚类算法简介
- 首先, 数据点被看做一个图的结点v, 数据的相似度看做图的边, 边的集合E=Aij, 由此构造相似度矩阵A, 并求出拉普拉斯矩阵
L
. - 其次, 根据划分准则使子图内部相似度尽量大, 子图之间的相似度尽量小, 计算出
L
的特征值和特征向量. - 最后选择k个不同的特征向量对数据点聚类.
例子
将相似度矩阵A的每行元素相加就可以得到该节点的度, 以度为对角线元素的对角矩阵称为度矩阵D. 拉普拉斯矩阵由D和A做成. 规范的拉普拉斯矩阵: L=D-A
, 非规范的拉普拉斯矩阵:
通过该图, 我们可以得到相似度矩阵A:
度矩阵D:
谱聚类算法执行过程
- 输入待聚类的数据点集以及聚类数k.
- 根据相似性度量构造数据点集的拉普拉斯矩阵
L
. - 选取L的前k个(默认从小到大, 这里的k可以和聚类数不同) 特征值和特征向量, 构造特征向量空间.
- 使用传统方法对特征向量聚类, 并对应于原始数据的聚类.
幂迭代法求矩阵的特征值
“幂迭代”法求特征值, 也有直接就叫做“幂法”求特征值的, 也是最基础的一种特征值迭代法求解方法. 适合计算大型稀疏矩阵的主特征值, 即按模最大的特征值, 同时也得到了对应的特征向量. 它的优点是方法简单, 理论依据是迭代的收敛性
.
幂法基本思想是: 若我们求某个n阶方阵A的特征值和特征向量, 先任取一个非零初始向量v(0), 进行迭代计算v(k+1)=Av(k), 直到收敛.
当k增大时, 序列的收敛情况与绝对值最大的特征值有密切关系, 分析这一序列的极限, 即可求出按模最大的特征值和特征向量.
假定矩阵A有n个线性无关的特征向量, n个特征值按模由大到小排列:
其相应的特征向量为:
他们构成n维空间的一组正交基. 任取的初始向量v(0)当然可以由他们的线性组合给出:
由递推法可得:
下面按模最大特征值λ1是单根的情况讨论:
将上式变形可得:
若a1≠0, 由于0<|λi/λ1|<1 (i≥2), 考虑到(0,1)之间的数的k次方的极限为0, 因此k充分大时:
所以, 当k充分大时:
幂迭代聚类
原理
在快速迭代算法中, 我们构造另外一个矩阵W=D-1A, 同谱聚类算法做比对, 我们可以知道W的最大特征向量就是拉普拉斯矩阵L的最小特征向量. 我们知道拉普拉斯矩阵有一个特性:第二小特征向量(即第二小特征值对应的特征向量)定义了图最佳划分的一个解, 它可以近似最大化划分准则. 更一般的, k个最小的特征向量所定义的子空间很适合去划分图.
因此拉普拉斯矩阵第二小、第三小直到第k小的特征向量可以很好的将图W划分为k个部分.
注意, 矩阵L的k个最小特征向量也是矩阵W的k个最大特征向量. 计算一个矩阵的最大特征向量可以通过上述幂迭代算法. 循环公式为:
其中c是标准化常量, 是为了避免vt产生过大的值, 这里.
算法的一般步骤:
源码
package com.roobo.ai.library.machinelearning.clustering
import org.apache.log4j.{Level, Logger}
import org.apache.spark.mllib.clustering.PowerIterationClustering
import org.apache.spark.sql.SparkSession
object PowerIterationClustering{
def main(args:Array[String]){
val warehouseLocation = "/Spark/spark-warehouse"
val spark=SparkSession
.builder()
.appName("myClusters")
.master("local[4]")
.config("spark.sql.warehouse.dir",warehouseLocation)
.getOrCreate();
val sc=spark.sparkContext
// 测试数据
val similarities =sc.makeRDD(Seq((1L,2L,1.0),(1L,3L,1.0),(1L,4L,1.0),(2L,3L,1.0)))
val modelPIC = new PowerIterationClustering()
.setK(2)// k : 期望聚类数
.setMaxIterations(40)//幂迭代最大次数
.setInitializationMode("degree")//模型初始化, 默认使用”random” , 即使用随机向量作为初始聚类的边界点, 可以设置”degree”
.run(similarities)
//输出聚类结果
val clusters = modelPIC.assignments.collect().groupBy(_.cluster).mapValues(_.map(_.id))
val assignments = clusters.toList.sortBy { case (k, v) => v.length }
val assignmentsStr = assignments
.map { case (k, v) =>
s"$k -> ${v.sorted.mkString("[", ",", "]")}"
}.mkString(", ")
val sizesStr = assignments.map {
_._2.length
}.sorted.mkString("(", ",", ")")
println(s"Cluster assignments: $assignmentsStr\ncluster sizes: $sizesStr")
}
}
similarities代表了前边的相似矩阵A. k为聚类数, maxIterations为最大迭代次数, initMode代表初始化模式. 初始化模式分为random和degree两种.
run方法实现分析
1. 标准化相似度矩阵A到矩阵W
def normalize(similarities: RDD[(Long, Long, Double)]): Graph[Double, Double] = {
//获得所有的边
val edges = similarities.flatMap { case (i, j, s) =>
//相似度值必须非负
if (s < 0.0) {
throw new SparkException("Similarity must be nonnegative but found s($i, $j) = $s.")
}
if (i != j) {
Seq(Edge(i, j, s), Edge(j, i, s))
} else {
None
}
}
//根据edges信息构造图, 顶点的特征值默认为0
val gA = Graph.fromEdges(edges, 0.0)
//计算从顶点的出发的边的相似度之和, 在这里称为度
val vD = gA.aggregateMessages[Double](
sendMsg = ctx => {
ctx.sendToSrc(ctx.attr)
},
mergeMsg = _ + _,
TripletFields.EdgeOnly)
//计算得到W , W=A/D
GraphImpl.fromExistingRDDs(vD, gA.edges)
.mapTriplets(
//gAi/vDi
//使用边的权重除以起始点的度
e => e.attr / math.max(e.srcAttr, MLUtils.EPSILON),
TripletFields.Src)
}
上面的代码首先通过边集合构造图gA,然后使用aggregateMessages计算每个顶点的度(即所有从该顶点出发的边的相似度之和), 构造出VertexRDD。最后使用现有的VertexRDD和EdgeRDD, 相继通过fromExistingRDDs和mapTriplets方法计算得到最终的图W。在mapTriplets方法中, 对每一个EdgeTriplet, 使用相似度除以出发顶点的度(为什么相除?对角矩阵的逆矩阵是各元素取倒数, W=D-1A就可以通过元素相除得到).
2. 初始化v(0)
根据选择的初始化模式的不同, 我们可以使用不同的方法初始化v0. 一种方式是随机初始化, 一种方式是度(degree)初始化, 下面分别来介绍这两种方式.
- 随机初始化
def randomInit(g: Graph[Double, Double]): Graph[Double, Double] = {
//给每个顶点指定一个随机数
val r = g.vertices.mapPartitionsWithIndex(
(part, iter) => {
val random = new XORShiftRandom(part)
iter.map { case (id, _) =>
(id, random.nextGaussian())
}
}, preservesPartitioning = true).cache()
//所有顶点的随机值的绝对值之和
val sum = r.values.map(math.abs).sum()
//取平均值
val v0 = r.mapValues(x => x / sum)
GraphImpl.fromExistingRDDs(VertexRDD(v0), g.edges)
}
- 度初始化
def initDegreeVector(g: Graph[Double, Double]): Graph[Double, Double] = {
//所有顶点的度之和
val sum = g.vertices.values.sum()
//取度的平均值
val v0 = g.vertices.mapValues(_ / sum)
GraphImpl.fromExistingRDDs(VertexRDD(v0), g.edges)
}
3. 快速迭代求最终v
for (iter <- 0 until maxIterations if math.abs(diffDelta) > tol) {
val msgPrefix = s"Iteration $iter"
// 计算w*vt
val v = curG.aggregateMessages[Double](
//相似度与目标点的度相乘
sendMsg = ctx => ctx.sendToSrc(ctx.attr * ctx.dstAttr),
mergeMsg = _ + _,
TripletFields.Dst).cache()
// 计算||Wvt||_1, 即公式中的c
val norm = v.values.map(math.abs).sum()
val v1 = v.mapValues(x => x / norm)
// 计算v_t+1和v_t的不同
val delta = curG.joinVertices(v1) { case (_, x, y) =>
math.abs(x - y)
}.vertices.values.sum()
diffDelta = math.abs(delta - prevDelta)
// 更新v
curG = GraphImpl.fromExistingRDDs(VertexRDD(v1), g.edges)
prevDelta = delta
}
4. 使用k-means算法对v进行聚类
def kMeans(v: VertexRDD[Double], k: Int): VertexRDD[Int] = {
val points = v.mapValues(x => Vectors.dense(x)).cache()
val model = new KMeans()
.setK(k)
.setRuns(5)
.setSeed(0L)
.run(points.values)
points.mapValues(p => model.predict(p)).cache()
}