转:谱聚类(spectral clustering)及其实现详解

转自:https://blog.csdn.net/yc_1993/article/details/52997074

Preface 
开了很多题,手稿都是写好一直思考如何放到CSDN上来,一方面由于公司技术隐私,一方面由于面向对象不同,要大改,所以一直没贴出完整,希望日后可以把开的题都补充全。


先把大纲列出来:

一、从狄多公主圈地传说说起
二、谱聚类的演算
  (一)、演算
      1、谱聚类的概览
      2、谱聚类构图
      3、谱聚类切图
        (1)、RatioCut
        (2)、Ncut
        (3)、一点题外话
  (二)、pseudo-code
三、谱聚类的实现(scala)
  (一)、Similarity Matrix
  (二)、kNN/mutual kNN
  (三)、Laplacian Matrix
  (四)、Normalized
  (五)、Eigenvector(Jacobi methond)
  (六)、kmeans/GMM
四、一些参考文献
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

一、从狄多公主圈地传说说起

       谱聚类(spectral clustering)的思想最早可以追溯到一个古老的希腊传说,话说当时有一个公主,由于其父王去世后,长兄上位,想独揽大权,便杀害了她的丈夫,而为逃命,公主来到了一个部落,想与当地的酋长买一块地,于是将身上的金银财宝与酋长换了一块牛皮,且与酋长约定只要这块牛皮所占之地即可。聪明的酋长觉得这买卖可行,于是乎便答应了。殊不知,公主把牛皮撕成一条条,沿着海岸线,足足围出了一个城市。 
       故事到这里就结束了,但是我们要说的才刚刚开始,狄多公主圈地传说,是目前知道的最早涉及Isoperimetric problem(等周长问题)的,具体为如何在给定长度的线条下围出一个最大的面积,也可理解为,在给定面积下如何使用更短的线条,而这,也正是谱图聚类想法的端倪,如何在给定一张图,拿出“更短”的边来将其“更好”地切分。而这个“更短”的边,正是对应了spectral clustering中的极小化问题,“更好”地切分,则是对应了spectral clustering中的簇聚类效果。 
       谱聚类最早于1973年被提出,当时Donath 和 Hoffman第一次提出利用特征向量来解决谱聚类中的f向量选取问题,而同年,Fieder发现利用倒数第二小的特征向量,显然更加符合f向量的选取,同比之下,Fieder当时发表的东西更受大家认可,因为其很好地解决了谱聚类极小化问题里的NP-hard问题,这是不可估量的成就,虽然后来有研究发现,这种方法带来的误差,也是无法估量的,下图是Fielder老爷子,于去年15年离世,缅怀。 
SC

二、谱聚类的演算

(一)、演算

 

1、谱聚类概览

       谱聚类演化于图论,后由于其表现出优秀的性能被广泛应用于聚类中,对比其他无监督聚类(如kmeans),spectral clustering的优点主要有以下:

1.过程对数据结构并没有太多的假设要求,如kmeans则要求数据为凸集。
2.可以通过构造稀疏similarity graph,使得对于更大的数据集表现出明显优于其他算法的计算速度。
3.由于spectral clustering是对图切割处理,不会存在像kmesns聚类时将离散的小簇聚合在一起的情况。
4.无需像GMM一样对数据的概率分布做假设。
  • 1
  • 2
  • 3
  • 4
  • 5

       同样,spectral clustering也有自己的缺点,主要存在于构图步骤,有如下:

1.对于选择不同的similarity graph比较敏感(如 epsilon-neighborhood, k-nearest neighborhood,fully connected等)。
2.对于参数的选择也比较敏感(如 epsilon-neighborhood的epsilon,k-nearest neighborhood的k,fully connected的 )。
  • 1
  • 2
  • 3

       谱聚类过程主要有两步,第一步是构图,将采样点数据构造成一张网图,表示为G(V,E),V表示图中的点,E表示点与点之间的边,如下图: 
              谱聚类构图 
                            图1 谱聚类构图(来源wiki) 
       第二步是切图,即将第一步构造出来的按照一定的切边准则,切分成不同的图,而不同的子图,即我们对应的聚类结果,举例如下: 
              切图4 
                            图2 谱聚类切图 
       初看似乎并不难,但是…,下面详细说明推导。 

2、谱聚类构图

       在构图中,一般有三种构图方式: 
       1. εε-neighborhood 
       2. k-nearest neighborhood 
       3. fully connected 
       前两种可以构造出稀疏矩阵,适合大样本的项目,第三种则相反,在大样本中其迭代速度会受到影响制约,在讲解三种构图方式前,需要引入similarity function,即计算两个样本点的距离,一般用欧氏距离:si,j=xixj2si,j=‖xi−xj‖2, si,jsi,j表示样本点xixi与xjxj的距离,或者使用高斯距离si,j=e∥∥xixj∥∥22σ2si,j=e−‖xi−xj‖22σ2,其中σσ 的选取也是对结果有一定影响,其表示为数据分布的分散程度,通过上述两种方式之一即可初步构造矩阵S:Si,j=[s]i,jS:Si,j=[s]i,j,一般称 为Similarity matrix(相似矩阵)。 
       对于第一种构图εε -neighborhood,顾名思义是取si,jεsi,j≤ε的点,则相似矩阵SS可以进一步重构为邻接矩阵(adjacency matrix)WW : 

 
Wi,j={0,ifsi,j>εε,ifsi,jεWi,j={0,ifsi,j>εε,ifsi,j≤ε

       可以看出,在εε-neighborhood重构下,样本点之间的权重没有包含更多的信息了。 
       对于第二种构图k-nearest neighborhood,其利用KNN算法,遍历所有的样本点,取每个样本最近的k个点作为近邻,但是这种方法会造成重构之后的邻接矩阵WW非对称,为克服这种问题,一般采取下面两种方法之一: 
       一是只要点xixi在xjxj的K个近邻中或者xjxj在xixi的K个近邻中,则保留si,jsi,j,并对其做进一步处理WW,此时 为: 
 
Wi,j=Wj,i=⎧⎩⎨0,ifxiKNN(Xj)andxjKNN(Xi)e∥∥xixj∥∥22σ2,ifxiKNN(Xj)orxjKNN(Xi)Wi,j=Wj,i={0,ifxi∉KNN(Xj)andxj∉KNN(Xi)e−‖xi−xj‖22σ2,ifxi∈KNN(Xj)orxj∈KNN(Xi)

       二是必须满足点 在 的K个近邻中且 在 的K个近邻中,才会保留si,jsi,j并做进一步变换,此时WW为: 
 
Wi,j=Wj,i=⎧⎩⎨0,ifxiKNN(Xj)orxjKNN(Xi)e∥∥xixj∥∥22σ2,ifxiKNN(Xj)andxjKNN(Xi)Wi,j=Wj,i={0,ifxi∉KNN(Xj)orxj∉KNN(Xi)e−‖xi−xj‖22σ2,ifxi∈KNN(Xj)andxj∈KNN(Xi)

       对于第三种构图fully connected,一般使用高斯距离: si,j=e∥∥xixj∥∥22σ2si,j=e−‖xi−xj‖22σ2,则重构之后的矩阵WW与之前的相似矩阵SS相同,为:Wi,j=Si,j=[s]i,jWi,j=Si,j=[s]i,j。 
       在了解三种构图方式后,还需要注意一些细节,对于第一二中构图,一般是重构基于欧氏距离的 ,而第三种构图方式,则是基于高斯距离的 ,注意到高斯距离的计算蕴含了这样一个情况:对于xixj2‖xi−xj‖2比较大的样本点,其得到的高斯距离反而值是比较小的,而这也正是SS可以直接作为WW的原因,主要是为了将距离近的点的边赋予高权重。 
       得到邻接矩阵WW后,需要做进一步的处理: 
       (1).计算阶矩(degree matrix)DD: 
 
Di,j={0,ifijjwi,j,ifi=jDi,j={0,ifi≠j∑jwi,j,ifi=j

       其中其中wi,jwi,j为邻接矩阵WW元素,jwi,j∑jwi,j表示将图中某点所连接的其他点的边的权重之和,可以看出,DD为对角矩阵. 
       (2).计算拉普拉斯矩阵(Laplacians matrix)LL:L=DWL=D−W 
       如此,在构图阶段基本就完成了,至于为什么要计算出拉普拉斯矩阵LL,可以说L=DWL=D−W这种形式对于后面极大化问题是非常有利的(不知道前人是怎么想到的,不然LL也不会被命名为拉普拉斯了吧)。 

 

3、谱聚类切图

       谱聚类切图存在两种主流的方式:RatioCut和Ncut,目的是找到一条权重最小,又能平衡切出子图大小的边,下面详细说明这两种切法。 
       在讲解RatioCut和Ncut之前,有必要说明一下问题背景和一些概念,假设V为所有样本点的集合,{A1,A2,,Ak}{A1,A2,⋯,Ak}表示V的子集集合,其中A1A2Ak=VA1A2Ak=A1∪A2∪⋯∪Ak=V且A1∩A2∩⋯∩Ak=∅,则子集与子集之间连边的权重和为: 

 
cut(A1,A2,,Ak)=12ikW(Ai,Ai¯)cut(A1,A2,⋯,Ak)=12∑ikW(Ai,Ai¯)

       其中Ai¯Ai¯为AiAi的补集,意为除AiAi子集外其他V的子集的并集,W(Ai,Ai¯)W(Ai,Ai¯)为AiAi与其他子集的连边的和,为: 
 
W(Ai,Ai¯)=mAi,nAi¯wm,nW(Ai,Ai¯)=∑m∈Ai,n∈Ai¯wm,n

       其中wm,nwm,n为邻接矩阵W中的元素。 
       由于我们切图的目的是使得每个子图内部结构相似,这个相似表现为连边的权重平均都较大,且互相连接,而每个子图间则尽量没有边相连,或者连边的权重很低,那么我们的目的可以表述为: 
 
mincut(A1,A2,,Ak)mincut(A1,A2,⋯,Ak)

       但是稍微留意可以发现,这种极小化的切图存在问题,如下图: 
              切图5 
                            图3 问题切图 
       如上图,如果用mincut(A1,A2,,Ak)mincut(A1,A2,⋯,Ak)这种方法切图,会造成将V切成很多个单点离散的图,显然这样是最快且最能满足那个最小化操作的,但这明显不是想要的结果,所以有了RatioCut和Ncut,具体地: 
 
Ratiocut(A1,A2,,Ak)=12ikW(Ai,Ai¯)|Ai|Ratiocut(A1,A2,⋯,Ak)=12∑ikW(Ai,Ai¯)|Ai|

 
Ncut(A1,A2,,Ak)=12ikW(Ai,Ai¯)vol(Ai)Ncut(A1,A2,⋯,Ak)=12∑ikW(Ai,Ai¯)vol(Ai)

       其中|Ai||Ai| 为AiAi中点的个数,vol(Ai)vol(Ai)为AiAi中所有边的权重和。 
       下面详细讲解这两种方法的计算:Ratiocut & Ncut 

 

       (1).Ratiocut

       Ratiocut切图考虑了目标子图的大小,避免了单个样本点作为一个簇的情况发生,平衡了各个子图的大小。Ratiocut的目标同样是极小化各子图连边和,如下: 

 
minRatiocut(A1,A2,,Ak)minRatiocut(A1,A2,⋯,Ak)

       对上述极小化问题做一下转化,引入{A1,A2,,Ak}{A1,A2,⋯,Ak}的指示向量hj={h1,h2,,hi,,hk},j=1,2,,khj={h1,h2,⋯,hi,⋯,hk},j=1,2,⋯,k. 其中i表示样本下标,j表示子集下标, 表示样本i对子集j的指示,具体为: 
 
hj,i=⎧⎩⎨1|Aj|√,ifviAj0,ifviAjhj,i={1|Aj|,ifvi∈Aj0,ifvi∉Aj

       通俗理解就是,每个子集AjAj对应一个指示向量hjhj,而每个hjhj里有N个元素,分别代表N个样本点的指示结果,如果在原始数据中第i个样本被分割到子集AjAj里,则hjhj的第i个元素为1|Aj|√1|Aj|,否则为0。 
       进一步,计算 ,可以得到: 
 
hTiLhi        =hTi(DW)hi=hTiDhihTiWhi=m=1n=1hi,mhi,nDm,nm=1n=1hi,mhi,nwm,n=m=1h2i,mDm,mm=1n=1hi,mhi,nwm,n=12(m=1h2i,mDm,m2m=1n=1hi,mhi,nwm,n+n=1h2i,nDn,n)  hiTLhi=hiT(D−W)hi  =hiTDhi−hiTWhi  =∑m=1∑n=1hi,mhi,nDm,n−∑m=1∑n=1hi,mhi,nwm,n  =∑m=1hi,m2Dm,m−∑m=1∑n=1hi,mhi,nwm,n  =12(∑m=1hi,m2Dm,m−2∑m=1∑n=1hi,mhi,nwm,n+∑n=1hi,n2Dn,n)  

       上述过程稍微复杂,分解如下: 
       第一步,第二步转化,利用上文拉普拉斯矩阵定义L=D-W; 
       第三步,m,n分别表示第m,n个样本,其最大值均为N,D/W均为对称矩阵,此步主要将矩阵内积离散化为元素连加求和形式; 
       第四步,由于D为对角矩阵,所以只有对角线上元素与向量hihi相乘有意义; 
       第五步,代数变换。 
       进一步地,做如下: 
 
hTiLhi    =12(m=1h2i,mDm,m2m=1n=1hi,mhi,nwm,n+n=1h2i,nDn,n)=12(m=1n=1h2i,mwm,n2m=1n=1hi,mhi,nwm,n+n=1m=1h2i,nwn,m)=12m=1n=1wn,m(hi,mhi,n)2  hiTLhi=12(∑m=1hi,m2Dm,m−2∑m=1∑n=1hi,mhi,nwm,n+∑n=1hi,n2Dn,n)  =12(∑m=1∑n=1hi,m2wm,n−2∑m=1∑n=1hi,mhi,nwm,n+∑n=1∑m=1hi,n2wn,m)  =12∑m=1∑n=1wn,m(hi,m−hi,n)2  

       同样分解如下: 
       第一步到第二步,由上文阶矩的定义可知,其对角线上元素Dm,m=n=1wm,nDm,m=∑n=1wm,n 
       第二部到第三步,二次函数变换; 
       进一步,将上文hj,ihj,i代入上式,最终可以得到: 
 
hTiLhi          =12m=1n=1wm,n(hi,mhi,n)2=12(mAi,nAi¯wm,n(1|Ai|−−−√0)2+mAi¯,nAiwm,n(01|Ai|−−−√)2)=12(mAi,nAi¯wm,n1|Ai|+mAi¯,nAiwm,n1|Ai|)=12(cut(Ai,Ai¯)1|Ai|+cut(Ai¯,Ai)1|Ai|)=cut(Ai,Ai¯)|Ai|=Ratiocut(Ai,Ai¯)  hiTLhi=12∑m=1∑n=1wm,n(hi,m−hi,n)2  =12(∑m∈Ai,n∈Ai¯wm,n(1|Ai|−0)2+∑m∈Ai¯,n∈Aiwm,n(0−1|Ai|)2)  =12(∑m∈Ai,n∈Ai¯wm,n1|Ai|+∑m∈Ai¯,n∈Aiwm,n1|Ai|)  =12(cut(Ai,Ai¯)1|Ai|+cut(Ai¯,Ai)1|Ai|)  =cut(Ai,Ai¯)|Ai|  =Ratiocut(Ai,Ai¯)  

       分解如下: 
       第一步到第二步,将指示变量hi,mhi,m,hi,nhi,n对应AiAi的元素分别代入; 
       第三,四,五步,分别做简单运算,以及将mAi,nAi¯wm,n∑m∈Ai,n∈Ai¯wm,n替换为cut(Ai,Ai¯)cut(Ai,Ai¯); 
       可以看到,通过引入指示变量h,将L矩阵放缩到与Ratiocut等价,这无疑是在谱聚类中划时代的一举。 
       为了更进一步考虑进所有的指示向量,令H={h1,h2,,hk}H={h1,h2,⋯,hk},其中hihi按列排列,由hihi的定义知每个hihi之间都是相互正交,即 hihj=0,ijhi∗hj=0,i≠j,且有hihi=1hi∗hi=1,可得到: 
 
Ratiocut(A1,A2,,Ak)    =i=1khTiLhi=i=1k(HTLH)ii=Tr(HTLH)  Ratiocut(A1,A2,⋯,Ak)=∑i=1khiTLhi  =∑i=1k(HTLH)ii  =Tr(HTLH)  

       Tr表示对角线求和。 
       如此,便将极小化问题:minRatiocut(A1,A2,,Ak)minRatiocut(A1,A2,⋯,Ak),转化为: 
 
argminHTr(HTLH),s.t.HTH=IargminHTr(HTLH),s.t.HTH=I

       将之前切图的思路以一种严谨的数学表达表示出来,对于上面极小化问题,由于L矩阵是容易得到的,所以目标是求满足条件的H矩阵,以使得Tr(HTLH)Tr(HTLH)最小。 
       注意到一点,要求条件下的H,首先是求得组成H的每个hihi,而每个hihi都是Nx1的向量,且向量中每个值都是二值分布,即取值0或者1/|Ai|−−−√1/|Ai|,那么对于每个元素,有2种选择,则当个hihi就有2N2N种情况,对于整个H矩阵,则有(C1k)N(Ck1)N种情况,对于N很大的数据,这无疑是灾难性的NP-hard问题,显然我们不可能遍历所有的情况来求解,那么问题是不是没有解?答案自然是否定的,虽然我们没办法求出精确的 ,但是我们可以用另外一种方法近似代替hihi,而这就是文章开头Fielder提出来的,当k=2的时候,可以用L的倒数第二小的特征向量(因为最小的特征向量为1,其对应的特征值为0,不适合用来求解)来代替 ,关于具体为什么可以这么做,可以参考Rayleigh-Ritz method(对应论文为:A short theory of the Rayleigh-Ritz method),因为涉及泛函变分,这里就不多说了。 
       那么当k取任意数字时,只需取L矩阵对应的最小那k个(毕竟倒数第二小只有1个),即可组成目标H,最后,对H做标准化处理,如下: 
 
Hi,j=Hi,j(kj=1H2i,j)1/2Hi,j∗=Hi,j(∑j=1kHi,j2)1/2

       现在H矩阵也完成了,剩下的就是对样本聚类了,要明白我们的目标不是求Tr(HTLH)Tr(HTLH)的最小值是多少,而是求能最小化Tr(HTLH)Tr(HTLH)的H,所以聚类的时候,分别对H中的行进行聚类即可,通常是kmeans,也可以是GMM,具体看效果而定。 
       至于为什么是对H的行进行聚类,有两点原因: 
       1.注意到H除了是能满足极小化条件的解,还是L的特征向量,也可以理解为W的特征向量,而W则是我们构造出的图,对该图的特征向量做聚类,一方面聚类时不会丢失原图太多信息,另一方面是降维加快计算速度,而且容易发现图背后的模式。 
       2.由于之前定义的指示向量hihi是二值分布,但是由于NP-hard问题的存在导致hihi无法显式求解,只能利用特征向量进行近似逼近,但是特征向量是取任意值,结果是我们对hihi的二值分布限制进行放松,但这样一来hihi如何指示各样本的所属情况?所以kmeans就登场了,利用kmeans对该向量进行聚类,如果是k=2的情况,那么kmeans结果就与之前二值分布的想法相同了,所以kmeans的意义在此,k等于任意数值的情况做进一步类推即可。 
       以上是Ratiocut的内容。 

 

       (2).Ncut

       Ncut切法实际上与Ratiocut相似,但Ncut把Ratiocut的分母|Ai||Ai|换成vol(Ai)vol(Ai) ,这种改变与之而来的,是L的normalized,这种特殊称谓会在下文说明,而且这种normalized,使得Ncut对于spectral clustering来说,其实更好,下文会说明。 
       同样,Ncut的目标,也是极小化各子图连边的和,如下: 

 
minNcut(A1,A2,,Ak)minNcut(A1,A2,⋯,Ak)

       下面对该问题做一下转化,先引入{A1,A2,,Ak}{A1,A2,⋯,Ak}的指示变量hj={h1,h2,,hi,,hk},j=1,2,,khj={h1,h2,⋯,hi,⋯,hk},j=1,2,⋯,k,同样,其中i表示样本下标,j表示子集下标,hj,ihj,i表示样本i对子集j的指示,不同的是,其具体为: 
 
hj,i=⎧⎩⎨1vol(Ai)√,ifviAj0,ifviAjhj,i={1vol(Ai),ifvi∈Aj0,ifvi∉Aj

       如果在原始数据中第i个样本被分割到子集AjAj里,则hjhj的第i个元素为1/vol(Ai)−−−−−−√1/vol(Ai),否则为0。 
       进一步,计算hTiLhihiTLhi,可以得到: 
 
hTiLhi                =hTiDhihTiWhi=m=1n=1hi,mhi,nDm,nm=1n=1hi,mhi,nwm,n=m=1h2i,mDm,mm=1n=1hi,mhi,nwm,n=12(m=1h2i,mDm,m2m=1n=1hi,mhi,nwm,n+n=1h2i,nDn,n)=12(m=1n=1h2i,mwm,n2m=1n=1hi,mhi,nwm,n+n=1m=1h2i,nwn,m)=12m=1n=1wm,n(hi,mhi,n)2=12(mAi,nAi¯wm,n(1vol(Ai)−−−−−−√0)2+mAi¯,nAiwm,n(01vol(Ai)−−−−−−√)2)=cut(Ai,Ai¯)vol(Ai)=Ncut(Ai,Ai¯)  hiTLhi=hiTDhi−hiTWhi  =∑m=1∑n=1hi,mhi,nDm,n−∑m=1∑n=1hi,mhi,nwm,n  =∑m=1hi,m2Dm,m−∑m=1∑n=1hi,mhi,nwm,n  =12(∑m=1hi,m2Dm,m−2∑m=1∑n=1hi,mhi,nwm,n+∑n=1hi,n2Dn,n)  =12(∑m=1∑n=1hi,m2wm,n−2∑m=1∑n=1hi,mhi,nwm,n+∑n=1∑m=1hi,n2wn,m)  =12∑m=1∑n=1wm,n(hi,m−hi,n)2  =12(∑m∈Ai,n∈Ai¯wm,n(1vol(Ai)−0)2+∑m∈Ai¯,n∈Aiwm,n(0−1vol(Ai))2)  =cut(Ai,Ai¯)vol(Ai)  =Ncut(Ai,Ai¯)  

       由于推导与上文类似,这里就综合在一起,主要是为了说明hTiLhihiTLhi如何得到Ncut(Ai,Ai¯)Ncut(Ai,Ai¯),以便将minNcut(A1,A2,,Ak)minNcut(A1,A2,⋯,Ak)用严谨数学式子进行表达。 
       这里对上述一大串式子分解一下: 
       第一,二,三步,做一些简单的变换,如L=D-W代入,将向量内积转换成连加形式,同样,D是对角矩阵,可省去一些项; 
       第四,五,六步,主要是做二次函数变换; 
       第七步,将hi,mhi,m, hi,nhi,n对应AiAi的元素分别代入; 
       第八,九步,得到Ncut(Ai,Ai¯)Ncut(Ai,Ai¯) 形式; 
       此外,相比与Ratiocut,Ncut特有的一点性质是:hTiDhi=1hiTDhi=1,具体如下: 
 
hTiDhi        =n=1h2i,nDn,n=vnAi1vol(Ai)Dn,n+vnAi0Dn,n=1vol(Ai)vnAiwvn+0=1vol(Ai)vol(Ai)=1  hiTDhi=∑n=1hi,n2Dn,n  =∑vn∈Ai1vol(Ai)Dn,n+∑vn∉Ai0⋅Dn,n  =1vol(Ai)∑vn∈Aiwvn+0  =1vol(Ai)vol(Ai)  =1  

       进一步,令H={h1,h2,,hk}H={h1,h2,⋯,hk},则HTDH=IHTDH=I ,其中I为单位对角矩阵。 
       同样,可以得到: 
 
Ncut(A1,A2,,Ak)    =i=1khTiLhi=i=1k(HTLH)ii=Tr(HTLH)  Ncut(A1,A2,⋯,Ak)=∑i=1khiTLhi  =∑i=1k(HTLH)ii  =Tr(HTLH)  

       如此,便将极小化问题:minNcut(A1,A2,,Ak)minNcut(A1,A2,⋯,Ak),转化为: 
 
argminHTr(HTLH),s.t.HTDH=IargminHTr(HTLH),s.t.HTDH=I

       除了H的限制条件,Ncut的极小化与Ratiocut的极小化基本无差异,但是就是这微小的条件,使得Ncut实质上是对L做了normalize(不得不叹数学真是十分奇妙)。 
       下面进一步说明,令H=D1/2FH=D−1/2F, D1/2D−1/2意为对D对角线上元素开方再求逆。将 H=D1/2FH=D−1/2F代入argminHTr(HTLH),s.t.HTDH=IargminHTr(HTLH),s.t.HTDH=I,可以得到: 
 
HTLH  =(D1/2F)TLD1/2F=FTD1/2LD1/2F  HTLH=(D−1/2F)TLD−1/2F  =FTD−1/2LD−1/2F  

        以及: 
 
HTDH      =(D1/2F)TDD1/2F=FTD1/2LD1/2F=FTIF=I  HTDH=(D−1/2F)TDD−1/2F  =FTD−1/2LD−1/2F  =FTIF  =I  

        至此,目标操作argminHTr(HTLH),s.t.HTDH=IargminHTr(HTLH),s.t.HTDH=I可以修改为: 
 
argminHTr(FTD1/2LD1/2F),s.t.FTF=IargminHTr(FTD−1/2LD−1/2F),s.t.FTF=I

        其中,D1/2LD1/2D−1/2LD−1/2这一步操作,就是对L矩阵进行normalize,而normalize可以理解为标准化,具体为:Li,jvol(Ai)vol(Aj)√Li,jvol(Ai)vol(Aj) ,这么做的一个好处就是对L中的元素进行标准化处理使得不同元素的量纲得到归一,具体理解就是,同个子集里,不同样本点之间的连边可能大小比较相似,这点没问题,但是对于不同子集,样本点之间的连边大小可能会差异很大,做 这一步normalize操作,可以将L中的元素归一化在[-1,1]之间,这样量纲一致,对算法迭代速度,结果的精度都是有很大提升。 
       最后,F矩阵的求解可通过求D1/2LD1/2D−1/2LD−1/2的前k个特征向量组成,然后对F再做进一步的标准化: 
 
Fi,j=Fi,j(kj=1F2i,j)1/2Fi,j∗=Fi,j(∑j=1kFi,j2)1/2

       之后再对F的行进行kmeans或GMM聚类即可。 
       以上是Ncut的内容。 

 

       (3).一点题外话

       写到这里,如果只是应用spectral clustering,则此部分可以忽略,直接看下文pseudo-code部分即可,但是对于喜欢深入探究,不妨看一看。 
       值得一提的是,从概率的视角出发,与上文推导也是不谋而合,而且得到的结论,与Ncut更是异曲同工。这种概率视角在多数论文里,称之为随机游走(Random walks),在随机数学里,常见于马尔可夫模型。这部分的详细出处可以参考Lovaszl(1993: Random Walks on Graphs: A Survey),以及Meila & Shi (2001:A Random Walks Views of Spectral Segmentation)。 
       在随机游走框架下,通常都会构建一个转移概率矩阵(transition matrix),同样,利用上文的邻接矩阵,可以得到该转移概率矩阵P为: 

 
Pi,j=wi,j/Di,j,i,j=1,2,,NPi,j=wi,j/Di,j,i,j=1,2,⋯,N

       其中wi,jwi,j,Di,jDi,j 分别为W,D矩阵中的元素。可以看出,其实P=WD1P=WD−1。为方便做进一步论述,假设一个初始分布概率π=(π1,π2,,πi,,πN)Tπ=(π1,π2,⋯,πi,⋯,πN)T,其中: 
 
πi=Di,ivol(V)πi=Di,ivol(V)

       对于最简单的双簇聚类,假设将原图V分割成A1A1以及 A2A2,则目标是极小化下面概率: 
 
min12(P(A1|A2)+P(A2|A1))min12(P(A1|A2)+P(A2|A1))

       这种极小化概率的意义在于,样本在不同簇之间切换的可能性,应该尽量低。 
       那么,对于P(A1|A2)P(A1|A2),可以写为: 
 
P(A1|A2)=P(A1,A2P(A2)P(A1|A2)=P(A1,A2P(A2)

       对于P(A2)P(A2),可以理解为任意样本属于A2A2的概率,则: 
 
P(A2)=vol(A2)vol(A)P(A2)=vol(A2)vol(A)

可通过A2A2簇里边和与整张图V边和做对比。 
对于P(A1,A2)P(A1,A2),可以理解为,任取一点,其从A2A2切换至 A1A1的概率,具体为: 
 
P(A1,A2)    =iA1,jA2πiPi,j=iA1,jA2Di,jvol(V)wi,jDi,j=1vol(V)iA1,jA2wi,j  P(A1,A2)=∑i∈A1,j∈A2πiPi,j  =∑i∈A1,j∈A2Di,jvol(V)wi,jDi,j  =1vol(V)∑i∈A1,j∈A2wi,j  

       则: 
 
P(A1|A2)=1vol(V)iA1,jA2wi,jvol(A2)vol(V)=iA1,jA2wi,jvol(A2)  P(A1|A2)=1vol(V)∑i∈A1,j∈A2wi,jvol(A2)vol(V)=∑i∈A1,j∈A2wi,jvol(A2)  

       最终: 
 
min12(P(A1|A2)+P(A2|A1))=min12(iA1,jA2wi,jvol(A2)+iA1,jA2wi,jvol(A1))=min12i=12W(Ai,Ai¯)vol(Ai)  min12(P(A1|A2)+P(A2|A1))=min12(∑i∈A1,j∈A2wi,jvol(A2)+∑i∈A1,j∈A2wi,jvol(A1))=min12∑i=12W(Ai,Ai¯)vol(Ai)  

       至此,可以看到Ncut在概率的视角上,也有了相应的立足之地,Ratiocut的推导也同理。

 

 

(二)、pesudo-code

       Spectral clustering的pseudo-code有很多种,这里只讲最常用的normalized版本,也就是Ncut作为切图法的版本。 
       具体为: 
       1.构造S矩阵(similarity matrix),同时指定要聚类的簇数k; 
       2.利用S矩阵构造W矩阵(adjacent matrix); 
       3.计算拉普拉斯矩阵L,其中L=D-W; 
       4.对L矩阵标准化,即令 ; 
       5.计算normalize后的L矩阵的前k个特征向量,按特征值升序排列; 
       6.对k个向量组成的矩阵的行进行看kmeans/GMM聚类; 
       7.将聚类结果的各个簇分别打上标记,对应上原数据,输出结果。

input: (data,K,k,kNNType,sigma,epsilon)
1. S = Euclidean(data) 
2. W = Gaussian( kNN(S,k,kNNType) , sigma ) 
3. L = D - W
4. L' = normalized(L)
5. EV = eigenvector(L',K)
6. while( (newCenter - oldCenter) > epsilon){
      newCenter = kmeans(EV,K)
   }
output:K clusters of SC
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

       其中,data为样本,对应code为二维数组,K为要聚类的簇数,k为kNN的邻接个数,kNNType为SC中的kNN函数类型,一般为kNN/mutalkNN,具体看上文说明,sigma为构造W矩阵时的高斯函数参数,epsilon为kmeans或者GMMs中的更新步长。SC输出为聚出K个簇。 

三、谱聚类的实现

       实现过程涉及到的一些概念有:Similarity Matrix、kNN/mutual kNN、Laplacian Matrix、Normalized、Eigenvector(Jacobi methond)、kmeans/GMM;下面一一按序解析,使用语言为scala,这里需要说明一点,由于技术保密,这里有些东西只能介绍一些简单的版本,动手操作可以发现,上述计算在小样本还算过得去,样本一大简直无法入目,只能各位自己多去查看论文了,若日后有缘,会开篇做另外的优化阐述。

       (一)、Similarity Matrix
// calculate the similarity matrix
def calculateSimilarityMatrix(SCInput: Array[Array[Double]]): Array[Array[Double]] = {
    // the Euclidean Distance
    def SCEuclideanDistance(SCEDInput: Array[Double]): Array[Double] = {
        SCInput.map(_.zip(SCEDInput))
               .map(a => a.map(b => math.pow(b._1 - b._2, 2)).sum)
     }
     SCInput.map(SCEuclideanDistance)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

       这里不代入数据了,直接构造一个similarity Matrix了,如下: 
              similarity Matrix 
                     图4 similarity Matrix

       (二)、kNN/mutual kNN

       下面是对上面similarity matrix一步构造的相似图做调整,将其转化为W(adjacency matrix).

    // define the kNN function
    def kNN(k: Int, kNNType: String, sigma: Double = 1.0, SMatrix: Array[Array[Double]]): Array[Array[Double]] = {
        val len = SMatrix.length
        val AdjacencyMatrix = Array.ofDim[Double](len, len)

        // define the function of calculating the mutual adjacency matrix (for mutualkNN)
        @tailrec
        def calculateMutualAdjacencyMatrix(n: Int, S: Array[Array[Double]]): Array[Array[Double]] = {
            if (n < len) {
                // take out the smallest k values
                val kSmallestValue = S(n).zipWithIndex.sortWith((a, b) => a._1 < b._1).take(k + 1)
                val indexOfValue = kSmallestValue.map(_._2).distinct
                // calculate the Gaussian similarity value
                for (i <- indexOfValue) {
                    val GaussianSimilarity = math.exp(-S(n)(i) / 2 / sigma / sigma)
                    AdjacencyMatrix(n)(i) = GaussianSimilarity
                }
                calculateMutualAdjacencyMatrix(n + 1, S)
            } else {
                // judge mutual or not.
                for (i <- 0 until len) {
                    val notZero = AdjacencyMatrix(i).zipWithIndex.filter(_._1 != 0.0).map(_._2)
                    for (j <- notZero) if (AdjacencyMatrix(j)(i) == 0.0) AdjacencyMatrix(i)(j) = 0.0
                }
                AdjacencyMatrix
            }
        }

        // define the function of calculating the mutual adjacency matrix (for kNN)
        @tailrec
        def calculateAdjacencyMatrix(n: Int, S: Array[Array[Double]]): Array[Array[Double]] = {
            if (n < len) {
                // take out the smallest k values
                val kSmallestValue = S(n).zipWithIndex.sortWith((a, b) => a._1 < b._1).take(k + 1)
                val indexOfValue = kSmallestValue.map(_._2).distinct
                // calculate the Gaussian similarity value
                for (i <- indexOfValue) {
                    val GaussianSimilarity = math.exp(-S(n)(i) / 2 / sigma / sigma)
                    AdjacencyMatrix(n)(i) = GaussianSimilarity
                    AdjacencyMatrix(i)(n) = GaussianSimilarity
                }
                calculateAdjacencyMatrix(n + 1, S)
            } else {
                AdjacencyMatrix
            }
        }

        if (kNNType == "mutualkNN") {
            calculateMutualAdjacencyMatrix(0, SMatrix)
        } else {
            calculateAdjacencyMatrix(0, SMatrix)
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53

       两种操作结果如下:kNN和mutualkNN。PS:由于是直接在IntelliJ直接截图,所以看起来可能没那么好看(ㄒoㄒ) 
              kNN 
                     图5 adjacency Matrix (kNN)

              mutualkNN 
                     图6 adjacency Matrix (mutualkNN)

       两个矩阵计算时k值均指定为2,sigma指定为1,可以观察出,上文矩阵⎡⎣⎢⎢⎢0.08.07.05.08.00.06.010.07.06.00.03.05.010.03.00.0⎤⎦⎥⎥⎥[0.08.07.05.08.00.06.010.07.06.00.03.05.010.03.00.0] ,其对应的kNN处理后的W矩阵,比较容易理解,即取任意样本最近的2个样本作为连点即可,而mutualkNN处理后的W矩阵,则需保证连点的两边均是该两点的最近的2个样本之一,举个例子,与第1个样本最近的两个点分别为第3,4个点,其距离分别为7.0和5.0,而第3个点最近的两个点分别为第2,4个点,其距离为6.0和3.0,所以这样下来,与第1个样本连边的点就只有第4个点,看图6可明白。

       (三)、 Laplacian Matrix

       下面利用上面W(adjacency matrix)矩阵,进一步计算D(degree matrix)矩阵和L(laplacian matrix)矩阵.

    // define the function of calculating the Laplacian Matrix
    def calculateLaplacianMatrix(adjacencyMatrix: Array[Array[Double]]): Array[Array[Double]] = {
        val len = adjacencyMatrix.length
        val laplacianMatrix = Array.ofDim[Double](len, len)

        // define the function of calculating the degree matrix
        def calculateDegreeMatrix(AM: Array[Array[Double]]): Array[Array[Double]] = {
            val degreeMatrix = Array.ofDim[Double](len, len)
            for (i <- 0 until len) {
                degreeMatrix(i)(i) = AM(i).sum
            }
            degreeMatrix
        }
        val degreeMatrix = calculateDegreeMatrix(adjacencyMatrix)

        // calculate the Laplacian matrix
        for (i <- 0 until len) {
            laplacianMatrix(i) = degreeMatrix(i).zip(adjacencyMatrix(i)).map(a => a._1 - a._2)
        }
        laplacianMatrix
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

       这里我把D的计算和L的计算写在一起,一步计算到位,结果如下: 
              degreeMatrix 
                     图7 degree Matrix

              laplacianMatrix
                     图8 laplacian Matrix

       (四)、 Normalized

       下面进一步对上面L(laplacian matrix)矩阵进行正则化处理.

    // define the function of calculating the normalized Laplacian Matrix
    def Normalized(laplacianMatrix: Array[Array[Double]], adjacencyMatrix: Array[Array[Double]]): Array[Array[Double]] = {
        val len = adjacencyMatrix.length

        // define the function of calculate the -1/2 power of the degree matrix
        def calculateAdjustDegreeMatrix(AM: Array[Array[Double]]): Array[Array[Double]] = {
            val adjustDegreeMatrix = Array.ofDim[Double](len, len)
            for (i <- 0 until len) {
                adjustDegreeMatrix(i)(i) = 1 / math.pow(AM(i).sum, 0.5)
            }
            adjustDegreeMatrix
        }
        val adjustDegreeMatrix = calculateAdjustDegreeMatrix(adjacencyMatrix)

        // calculate the normalized laplacian matrix
        def matrixProduct(left: Array[Array[Double]], right: Array[Array[Double]]): Array[Array[Double]] = {
            val len = left.length
            val output = Array.ofDim[Double](len,len)

            for(i <- 0 until len; j <- i until len){
                output(i)(j) = left(i).zip(right.map(_(j))).map(a => a._1 * a._2).sum
                output(j)(i) = output(i)(j)
            }
            output
        }
        val temp = matrixProduct(adjustDegreeMatrix,laplacianMatrix)
        val normalizedLaplacianMatrix = matrixProduct(temp,adjustDegreeMatrix)
        normalizedLaplacianMatrix
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

       下面是上文L(laplacian matrix)矩阵的正则结果: 
              normalized
                     图9 Normalized laplacian Matrix

       (五)、 Eigenvector(Jacobi methond)

       下面进一步对上面正则化处理之后的L矩阵L’,取对应的特征向量,组成新的矩阵,特征向量的计算这里用的是串行的Jacobi旋转方法,内在逻辑就是对L’矩阵行列转换,得到L’的相似矩阵,且满足该相似矩阵为对角矩阵。值得一说的是,这种方法小样本可行,大样本效率很低,之后看情况,看是否再把优化的方法写出来,看缘分吧。。。

    // define the function of calculating the k smallest eigenvectors of normalized laplacian matrix with Jacobi method.
    def kSmallestEigenvectors(k: Int, normalizedLaplacian: Array[Array[Double]]): (Array[Double], Array[Array[Double]]) = {

        val len = normalizedLaplacian.length

        // initial the eigenvector matrix.
        val eigenvectorMatrix = Array.ofDim[Double](len, len)
        for (i <- 0 until len) eigenvectorMatrix(i)(i) = 1.0

        // initial the parameter of epsilon.
        val epsilon = (math.pow(10, -10), -1)

        // calculate the largest one Of normalized laplacian matrix(off-diagonal).
        def calculateLargestOfNormL(Input: Array[Array[Double]]): (Double, Int) = {
            val temp = Input.map(_.zipWithIndex)
            val largestOfRow = new Array[(Double, Int)](len - 1)
            for (i <- 0 until (len - 1)) {
                largestOfRow(i) = temp(i).filter(_._2 > i).sortWith((a, b) => a._1.abs > b._1.abs).head
            }
            val largestOfInput = largestOfRow.sortWith((a, b) => a._1.abs > b._1.abs).head
            if (epsilon._1 > largestOfInput._1.abs) epsilon else largestOfInput
        }
        var largestOfNormL = calculateLargestOfNormL(normalizedLaplacian)

        // Judge condition.
        var loop: Boolean = true

        // main loop.
        while (loop) {

            // the index of the largest value of normalized laplacian matrix.
            val mRow = largestOfNormL._2
            val mCol = normalizedLaplacian(largestOfNormL._2).indexOf(largestOfNormL._1)

            // calculate the new normalized Laplacian matrix: angle, sin, cos, new(row,col)
            // cache the temp value.
            val nL_ij = normalizedLaplacian(mRow)(mCol)
            val nL_ii = normalizedLaplacian(mRow)(mRow)
            val nL_jj = normalizedLaplacian(mCol)(mCol)
            val angle = if (nL_jj == nL_ii) math.Pi / 4.0 else 0.5 * math.atan2(2 * nL_ij, nL_jj - nL_ii)
            val sinAngle = math.sin(angle)
            val cosAngle = math.cos(angle)
            // update the normalized Laplacian matrix (ii, jj, ij, ji)
            normalizedLaplacian(mRow)(mRow) = nL_ii * cosAngle * cosAngle + nL_jj * sinAngle * sinAngle - 2 * nL_ij * cosAngle * sinAngle
            normalizedLaplacian(mCol)(mCol) = nL_ii * sinAngle * sinAngle + nL_jj * cosAngle * cosAngle + 2 * nL_ij * cosAngle * sinAngle
            normalizedLaplacian(mRow)(mCol) = (cosAngle * cosAngle - sinAngle * sinAngle) * nL_ij + sinAngle * cosAngle * (nL_ii - nL_jj)
            normalizedLaplacian(mCol)(mRow) = normalizedLaplacian(mRow)(mCol)
            for (i <- (0 until len).filter(a => a != mRow && a != mCol)) {
                val tempRi = normalizedLaplacian(mRow)(i)
                val tempCi = normalizedLaplacian(mCol)(i)
                normalizedLaplacian(mRow)(i) = cosAngle * tempRi - sinAngle * tempCi
                normalizedLaplacian(mCol)(i) = sinAngle * tempRi + cosAngle * tempCi
                normalizedLaplacian(i)(mRow) = normalizedLaplacian(mRow)(i)
                normalizedLaplacian(i)(mCol) = normalizedLaplacian(mCol)(i)
            }

            // update the eigenvector matrix.
            for (i <- 0 until len) {
                val eigenIR = eigenvectorMatrix(i)(mRow)
                val eigenIC = eigenvectorMatrix(i)(mCol)
                eigenvectorMatrix(i)(mRow) = eigenIR * cosAngle - eigenIC * sinAngle
                eigenvectorMatrix(i)(mCol) = eigenIC * cosAngle + eigenIR * sinAngle
            }

            // update the temp value again.
            largestOfNormL = calculateLargestOfNormL(normalizedLaplacian)

            // update the judge condition.
            if (largestOfNormL._2 == -1) loop = false

        }

        // return the k smallest eigenvalue and it's corresponding eigenvector matrix.
        val eigenvalue = new Array[Double](len)
        for (i <- 0 until len) eigenvalue(i) = normalizedLaplacian(i)(i)
        def kSmallest(eigenvalue: Array[Double], eigenvector: Array[Array[Double]]): (Array[Double], Array[Array[Double]]) = {
            val eigenvalueWithIndex = eigenvalue.zipWithIndex.sortWith((a, b) => a._1 < b._1).take(k)
            val ouput = Array.ofDim[Double](len, k)
            var n: Int = 0
            for ((v, i) <- eigenvalueWithIndex) {
                for (j <- 0 until len) {
                    ouput(j)(n) = eigenvector(j)(i)
                }
                n += 1
            }
            (eigenvalueWithIndex.map(_._1), ouput)
        }
        val kSmallestValueVector = kSmallest(eigenvalue, eigenvectorMatrix)

        // final ouput.
        (kSmallestValueVector._1, kSmallestValueVector._2)

    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93

       为直观一点,下面拿矩阵⎡⎣⎢⎢⎢0.08.07.05.08.00.06.010.07.06.00.03.05.010.03.00.0⎤⎦⎥⎥⎥[0.08.07.05.08.00.06.010.07.06.00.03.05.010.03.00.0] 计算特征值和特征向量,结果如下: 
              特征值
                     图10 特征值(对角线)

              特征向量
                     图11 特征向量

       结果可以和Matlab和R,python做比较,我只和R比对过,基本无差。

       (六)、 kmeans/GMM

       再做一步最终的聚类,便基本完成了算法。

    // define the kmeans function.
    def kmeans(K: Int, eigenvector: Array[Array[Double]]): Array[(Int, Array[Double])] = {

        val len = eigenvector.length
        val col = eigenvector(0).length

        // initial the random centers.
        var center: Map[Int, Array[Double]] = Map()
        var Karr: List[Int] = Nil
        while(Karr.length < K){
            val RandomK = Random.nextInt(len)
            if(!Karr.contains(RandomK)) Karr = Karr ::: List(RandomK)
        }
        for (i <- 0 until K) {
            center += (i -> eigenvector(Karr(i)))
        }

        // classify the points into K clusters with the present center.
        def classify(ct: Map[Int, Array[Double]], input: Array[Array[Double]]): Array[(Int, Array[Double])] = {

            // calculate the euclidean distance.
            val tempArr = input.map(a => {
                val euclidean = new Array[Double](K)
                for (i <- 0 until K) {
                    euclidean(i) = ct(i).zip(a).map(m => math.pow(m._1 - m._2, 2)).sum
                }
                euclidean.zipWithIndex
            })

            // tagging the points.
            val tagging = tempArr.map(a => {
                val tag = a.sortWith((x, y) => x._1 < y._1).head
                tag._2
            })

            // output.
            val output = tagging.zip(input)
            output
        }
        //val pointsWithTag = classify(center, eigenvector)

        // update the center.
        def updateCenter(oldCenter: Map[Int, Array[Double]], PWT: Array[(Int, Array[Double])]): Map[Int, Array[Double]] = {

            // groupby the result for computing.
            val clusters = PWT.groupBy(_._1)

            // update the newCenter
            var newCenter: Map[Int, Array[Double]] = Map()
            for (i <- 0 until K) {
                val clustersI = clusters.get(i) match {
                    case Some(s) => s.map(_._2)
                    //case None => new Array(col)
                }
                val n = clustersI.length
                val centerI = for (j <- 0 until col) yield clustersI.map(_ (j)).sum / n
                newCenter += (i -> centerI.toArray)
            }
            newCenter
        }
        //val newCenter = updateCenter(center,pointsWithTag)

        // initialize the blank variable.
        var pointsWithTag: Array[(Int, Array[Double])] = Array()
        var newCenter: Map[Int, Array[Double]] = Map()
        var movement: Seq[Double] = Seq()

        // loop.
        var loop: Boolean = true
        var j = 0
        while (loop) {
            j += 1

            // tagging the points and update the center.
            pointsWithTag = classify(center, eigenvector)
            newCenter = updateCenter(center, pointsWithTag)

            // the movement of the center.
            movement = for (i <- 0 until K) yield newCenter(i).zip(center(i)).map(a => (a._1 - a._2).abs).sum

            // judge the movement is small enough or not.  movement.exists(_ > math.pow(10, -10))
            if (movement.exists(_ > math.pow(10, -5))) {
                center = newCenter
            } else {
                loop = false
            }
        }
        pointsWithTag
        //newCenter
        //j
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91

       按照上文pesudo-code的指引,一步一步操作,最终输出结果如下,spectral clustering和kmeans的对比(twocircles datase): 
spectral clustering和kmeans的对比
                     图12 spectral clustering和kmeans的对比

 

四、一些参考资料

  1. Meila,shi: A Random Walks View of Spectral Segmentation
  2. Harry Yserentant: A short theory of the Rayleigh-Ritz method
  3. Ulrike von Luxburg: A Tutorial on Spectral Clustering
  4. Andrew Y.Ng, Michael I.Jordan, Yair Weiss: On Spectral Clustering Analysis and an algorithm
  5. L. LOVASZ: Random Walks on Graphs: A Survey
  6. Fan R.K. Chung: Spectral Graph Theory
  7. Xiao-Dong Zhang: The Laplacian eigenvalues of graphs: a survey
  8. Bojan Mohar: THE LAPLACIAN SPECTRUM OF GRAPHS
  9. pluskid: http://blog.pluskid.org/?p=287
  10. Wiki: https://en.wikipedia.org/wiki/Jacobi_eigenvalue_algorithm
  11. G. E. FORSYTHE AND P. HENRICI:THE CYCLIC JACOBI METHOD FOR COMPUTING THE PRINCIPAL VALUES OF A COMPLEX MATRIX
  12. Jacobi Transformations of a Symmetric Matrix

posted on 2018-08-05 17:02  -小神飞  阅读(325)  评论(0编辑  收藏  举报

导航