幂迭代聚类

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

K-means聚类算法简介

目标

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




算法执行步骤

  1. 数据预处理, 如归一化、离群点处理等.
  2. 随机选择k簇中心, 记作u1(0)、u2(0)……uk(0).
  3. 定义代价函数: J(c,u)=\min _ u \min _c \sum_{i=1}^{M}\||x_i-u_{c_i}||^2
    其中xi代表第i个样本, ci是xi所属于的簇, uci代表簇对应的中心点, M 是样本总数.
  4. 令t=0,1,2,…为迭代步数, 重复下面过程知道J收敛:
  • 对于每一个样本xi, 将其分配到距离最近的簇. c_i^{(t)}\leftarrow \mathop{\arg\min}_{k}\||x_i-u_k^{(t)}||^2
  • 对于每一个类簇k, 重新计算该类簇的中心:u^{(t+1)}_k\leftarrow \mathop{\arg\min}_{u} \sum_{i:c_i^{(t)}=k}||x_i-u||^2

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, 非规范的拉普拉斯矩阵:L=I-D^{-1}A

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

A=\begin{bmatrix} 0 & 1 & 1 & 1 \\\\ 1 & 0 & 1 & 1 \\\\ 1 & 1 & 0 & 0 \\\\ 1 & 1 & 0 & 0 \end{bmatrix}

度矩阵D:

D=\begin{bmatrix} 3 & 0 & 0 & 0 \\\\ 0 & 3 & 0 & 0 \\\\ 0 & 0 & 2 & 0 \\\\ 0 & 0 & 0 & 2 \end{bmatrix}

谱聚类算法执行过程

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

幂迭代法求矩阵的特征值

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

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

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

假定矩阵A有n个线性无关的特征向量, n个特征值按模由大到小排列:|\lambda_1| >=|\lambda_2| >= ...>=|\lambda_n|
其相应的特征向量为: e_1,e_2,...,e_n

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

由递推法可得: v^{(0)}=c_1e_1+c_2e_2+...+c_ne_n
v^{(1)}=Av^{(0)}=A(c_1e_1+c_2e_2+...+c_ne_n)=c_1(Ae_1)+c_2(Ae_2)+...+c_1(Ae_n)
=c_1\lambda_1e_1+c_2\lambda_2e_2+...+c_n\lambda_ne_n
...
v^{k}=c_1\lambda_1^{k}e_1+c_2\lambda_2^{k}e_2+...+c_n\lambda_n^{k}e_n

下面按模最大特征值λ1是单根的情况讨论:
将上式变形可得:v^{(k)}=\lambda_1^k(c_1e_1+c_2(\frac{\lambda_2}{\lambda_1})^ke_2+...+c_n(\frac{\lambda_n}{\lambda_1})^ke_n)
若a1≠0, 由于0<|λi1|<1 (i≥2), 考虑到(0,1)之间的数的k次方的极限为0, 因此k充分大时:v^{(k)}≈\lambda_1^kc_1e_1

所以, 当k充分大时:
v^{(k)}≈\lambda_1^kc_1e_1, v^{(k+1)}≈\lambda_1^{k+1}c_1e_1 \Rightarrow \lambda_1≈ \frac{v^{(k+1)}}{v^{k}}

幂迭代聚类

原理

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

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

算法的一般步骤:

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()
  }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容