幂迭代聚类

此算法最后一步为K-means聚类算法, 所以先简单介绍下K-means聚类算法.

K-means聚类算法简介

目标

K-means聚类算法的核心目标是将给定的数据集划分为K个簇, 并给出每个数据对应的簇中心点.




算法执行步骤

  1. 数据预处理, 如归一化、离群点处理等.
  2. 随机选择k簇中心, 记作u1(0)、u2(0)……uk(0).
  3. 定义代价函数:
    其中xi代表第i个样本, ci是xi所属于的簇, uci代表簇对应的中心点, M 是样本总数.
  4. 令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个不同的特征向量对数据点聚类.

例子

sg.png

将相似度矩阵A的每行元素相加就可以得到该节点的度, 以度为对角线元素的对角矩阵称为度矩阵D. 拉普拉斯矩阵由D和A做成. 规范的拉普拉斯矩阵: L=D-A, 非规范的拉普拉斯矩阵:

通过该图, 我们可以得到相似度矩阵A:

度矩阵D:

谱聚类算法执行过程

  1. 输入待聚类的数据点集以及聚类数k.
  2. 根据相似性度量构造数据点集的拉普拉斯矩阵L.
  3. 选取L的前k个(默认从小到大, 这里的k可以和聚类数不同) 特征值和特征向量, 构造特征向量空间.
  4. 使用传统方法对特征向量聚类, 并对应于原始数据的聚类.

幂迭代法求矩阵的特征值

“幂迭代”法求特征值, 也有直接就叫做“幂法”求特征值的, 也是最基础的一种特征值迭代法求解方法. 适合计算大型稀疏矩阵的主特征值, 即按模最大的特征值, 同时也得到了对应的特征向量. 它的优点是方法简单, 理论依据是迭代的收敛性.

幂法基本思想是: 若我们求某个n阶方阵A的特征值和特征向量, 先任取一个非零初始向量v(0), 进行迭代计算v(k+1)=Av(k), 直到收敛.

当k增大时, 序列的收敛情况与绝对值最大的特征值有密切关系, 分析这一序列的极限, 即可求出按模最大的特征值和特征向量.

假定矩阵A有n个线性无关的特征向量, n个特征值按模由大到小排列:
其相应的特征向量为:

他们构成n维空间的一组正交基. 任取的初始向量v(0)当然可以由他们的线性组合给出:

由递推法可得:



下面按模最大特征值λ1是单根的情况讨论:
将上式变形可得:
若a1≠0, 由于0<|λi1|<1 (i≥2), 考虑到(0,1)之间的数的k次方的极限为0, 因此k充分大时:

所以, 当k充分大时:

幂迭代聚类

原理

在快速迭代算法中, 我们构造另外一个矩阵W=D-1A, 同谱聚类算法做比对, 我们可以知道W的最大特征向量就是拉普拉斯矩阵L的最小特征向量. 我们知道拉普拉斯矩阵有一个特性:第二小特征向量(即第二小特征值对应的特征向量)定义了图最佳划分的一个解, 它可以近似最大化划分准则. 更一般的, k个最小的特征向量所定义的子空间很适合去划分图.
因此拉普拉斯矩阵第二小、第三小直到第k小的特征向量可以很好的将图W划分为k个部分.

注意, 矩阵L的k个最小特征向量也是矩阵W的k个最大特征向量. 计算一个矩阵的最大特征向量可以通过上述幂迭代算法. 循环公式为:
其中c是标准化常量, 是为了避免vt产生过大的值, 这里.

算法的一般步骤:

PIC.1.2.png

源码

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()
  }

你可能感兴趣的:(幂迭代聚类)