谱聚类算法详解及代码实现

谱聚类算法详解及代码实现

在这里插入图片描述

参考

关于谱聚类介绍

谱聚类概述

谱聚类是从图论中演化出来的算法,在后来我们的聚类中得到了广泛的应用。闭气传统的K-Means算法,谱聚类对数据分布的适应性更强,聚类效果也很不错。其主要思想是将所有的数据看作是空间中的点,这些点之间可以用边连接起来。距离较远的两个点之间的边权重值较低,而距离较近的两个点之间的边权重值较高,通过对所有数据点组成的图进行切图,让切图后不同的子图间边的权重和尽可能低,而子图内的边权重和尽可能高,从而达到聚类的效果。
优点

  • 谱聚类只需要数据之间的相似度矩阵,因此对于处理稀疏数据的聚类很有效
  • 由于使用了降维,因此在处理高维数据聚类时的复杂度比传统聚类算法好
    缺点
  • 聚类效果依赖于相似矩阵,不同的相似矩阵得到的最终聚类效果可能很不同。

谱聚类前置知识

无向权重图

对于一个图,我们一般用电的集合V和边的集合E来描述。即为G(V,E)。其中V即为我们数据集里面所有的点(v1,v2,……,vn)。对于V中的任意两个点vi和vj,我们定义权重wij为二者之间的权重。由于是无向图,所以wij=wji。
W = [ w 11 w 12 w 13 w 14 w 21 w 22 w 23 w 24 w 31 w 32 w 33 w 34 w 41 w 42 w 43 w 44 ] (1) W = \left[ \begin{matrix} {w_{11}}&{w_{12}}&{w_{13}}&{w_{14}}\\ {w_{21}}&{w_{22}}&{w_{23}}&{w_{24}}\\ {w_{31}}&{w_{32}}&{w_{33}}&{w_{34}}\\ {w_{41}}&{w_{42}}&w_{43}&w_{44}\\ \end{matrix} \right] \tag{1} W=w11w21w31w41w12w22w32w42w13w23w33w43w14w24w34w44(1)

邻接矩阵

邻接矩阵:是表示节点之间相邻关系的矩阵。如下图中的权重图中(假设各权重为1),其邻接矩阵可表示为事(2)所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gqvHvn42-1656837856130)(C:\Users\86136\Desktop\graph.png)]

图 一

W = ( 0 0 0 1 1 0 0 1 1 1 0 1 0 0 0 1 1 0 0 0 1 1 0 0 0 ) (2) W = \begin{pmatrix} {0}&{0}&{0}&{1}&{1}\\ {0}&{0}&{1}&{1}&{1}\\ {0}&{1}&{0}&{0}&{0}\\ {1}&{1}&{0}&{0}&{0}\\ {1}&{1}&{0}&{0}&{0}\\ \end{pmatrix} \tag{2} W=0001100111010001100011000(2)

度矩阵

度矩阵是对角矩阵,对角上元素为各个顶点的度。上述图一的度矩阵为式(3)所示。
D = ( 2 0 0 0 0 0 3 0 0 0 0 0 1 0 0 0 0 0 2 0 0 0 0 0 2 ) (3) D = \begin{pmatrix} {2}&{0}&{0}&{0}&{0}\\ {0}&{3}&{0}&{0}&{0}\\ {0}&{0}&{1}&{0}&{0}\\ {0}&{0}&{0}&{2}&{0}\\ {0}&{0}&{0}&{0}&{2}\\ \end{pmatrix} \tag{3} D=2000003000001000002000002(3)

拉普拉斯矩阵

拉普拉斯矩阵L= D - W,其中D为度矩阵, W为邻接矩阵。图一的拉普拉斯矩阵如式(4)所示。
L = ( 2 0 0 − 1 − 1 0 3 − 1 − 1 − 1 0 − 1 1 0 0 − 1 − 1 0 2 0 − 1 − 1 0 0 2 ) (4) L = \begin{pmatrix} {2}&{0}&{0}&{-1}&{-1}\\ {0}&{3}&{-1}&{-1}&{-1}\\ {0}&{-1}&{1}&{0}&{0}\\ {-1}&{-1}&{0}&{2}&{0}\\ {-1}&{-1}&{0}&{0}&{2}\\ \end{pmatrix} \tag{4} L=2001103111011001102011002(4)
用拉普拉斯矩阵来求解特征值,通过确定特征值来确定对应特征向量的个数,从而实现降维,然后再用K-means将降维的特征向量进行聚类。

相似度矩阵

一般通过K-近邻的方法构造相似度矩阵,一般是基于欧式距离进行构造的。其步骤一般分为以下两步:

  1. ✔ 计算点对之间的欧式距离
  2. 通过给定的参数k进行构造

其中第二步通常有以下三种方法实现

方法一

策略:通过给定参数k,选取距离当前点最近的k个点作为邻居(通常用高级核计算距离),令其余点到该点的距离为0。

w i j = { e ∥ x i x j ∥ 2 2 2 σ 2 , if  x i   ∈  knn( x j ) 0 , otherwise (方法1) wij = \begin{cases} e^\frac{\begin{Vmatrix}x_{i}&x_{j}\end{Vmatrix} _{2}^2}{2\sigma^2}, & \text{if $x_{i}$ $\in$ knn($x_{j}$)} \\[2ex] 0, & \text{otherwise} \end{cases} \tag{方法1} wij=e2σ2xixj22,0,if xi  knn(xj)otherwise(1)

这样构造的问题:数据从有向图变成了无向图,即B是A的k近邻,但A不一定是B的k近邻。为了保证相似矩阵的对称性,有了下面两种构造方法。

方法二

策略:与方法一类似,不过在选取k近邻的时候,只要A是B的k近邻了我们也罢B当作A的k近邻(不管B是不是A的k近邻),可以类似理解成或运算。

w i j = w j i = { e ∥ ∥ x i x j ∥ 2 2 2 σ 2 , if  x i   ∈  knn( x j ) or  x j   ∈  knn( x i )  0 , otherwise (方法2) wij = wji = \begin{cases} e^\frac{\Vert\begin{Vmatrix}x_{i}&x_{j}\end{Vmatrix} _{2}^2}{2\sigma^2}, & \text{if $x_{i}$ $\in$ knn($x_{j}$) or $x_{j}$ $\in$ knn($x_{i}$) } \\[2ex] 0, & \text{otherwise} \end{cases} \tag{方法2} wij=wji=e2σ2xixj22,0,if xi  knn(xj) or xj  knn(xiotherwise(2)

方法三

策略:与方法一类似,不过在选取k近邻的时候,要保证A和B互为k近邻的时候这两个点的边才有权重。

w i j = w j i = { e ∥ x i x j ∥ 2 2 2 σ 2 , if  x i   ∈  knn( x j ) and  x j   ∈  knn( x i )  0 , otherwise (方法3) wij = wji = \begin{cases} e^\frac{\begin{Vmatrix}x_{i}&x_{j}\end{Vmatrix} _{2}^2}{2\sigma^2}, & \text{if $x_{i}$ $\in$ knn($x_{j}$) and $x_{j}$ $\in$ knn($x_{i}$) } \\[2ex] 0, & \text{otherwise} \end{cases} \tag{方法3} wij=wji=e2σ2xixj22,0,if xi  knn(xj) and xj  knn(xiotherwise(3)

最常用的方法一般是:

将方法一算的的相似度矩阵与它的转置矩阵相加再除以2,这样就保证了求得的相似度矩阵也是对称矩阵。

W = W + W T 2 W = \frac{W + W ^ T}{2} W=2W+WT

确定目标函数

在确定好图的结构,问题就是将点聚成c个类的问题,可以将问题转化为切割无向图为c个子图。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r4nJabS8-1656837856131)(C:\Users\86136\Desktop\d4729333-e0b4-4283-b552-3862ec8c830c_.jpg)]

根据上图,我们很容易想到一个方法,就是我们将距离较远的两个点切分到不同子图时,需要付出的代价很小。因此我们可以定义一个代价函数来作为我们初步的目标函数。

初始化目标函数(最小割MinCut方法)

对于无向图G(X,E) 进行切分的目标时将G划分为相互无连接的k个子图,每个子图包含点的集合 A 1 A_{1} A1 A 2 A_{2} A2 A k A_{k} Ak,.(当然要保证每个点属于且只属于一个子图)。对于任意的两个子图点的集合A,B,可以定义A和B之间的切图权重为:
W ( A , B ) = ∑ i ∈ A , j ∈ B w i j (A-B 切图权重) W(A,B) = \sum_{i \in A, j \in B}{w_{ij}} \tag{A-B 切图权重} W(A,B)=iA,jBwij(A-B )
那么我们可以把上述的切图权重公式扩展到k个子图的节点的集合种,即 A 1 A_{1} A1, A 2 A_{2} A2 A k A_{k} Ak,定义切图cut为:
c u t ( A 1 , A 2 , A 3 . . . A k ) = 1 2 ∑ i ∈ A , j ∈ B k W ( A i , A i ‾ ) cut(A_{1},A_{2},A_{3}...A_{k}) = \frac{1}{2} \sum_{i \in A, j \in B}^k{W(A_{i}, \overline{A_{i}})} cut(A1,A2,A3...Ak)=21iA,jBkW(Ai,Ai)

这里除以2是因为按照求和公式我们会计算A和B之间的切图权重也会计算B和A之间的切图权重,而根据切图权重计算公式 W ( A , B ) = W ( B , A ) {W(A,B) = W(B,A)} W(A,B)=W(B,A),所以将最后结果除以2处理。

其中 A i ‾ {\overline{A_{i}}} Ai A i {A_{i}} Ai的补集(除了本身以外的所有子集的并集)。

因此我们可以进一步得到初步离散优化问题,最小切割目标函数:
m i n C u t ( V ) = > m i n ∑ v i ∈ A k , v j ∈ A k ‾ w i j min Cut(V) => min \sum_{v_{i} \in A_{k}, v_{j} \in \overline{A_{k}} }{w_{ij}} minCut(V)=>minviAk,vjAkwij

我们先只考虑只有两个类的情况(c = 2)

通过从 c = 2 的情况理解引入指示向量的作用。

在这里我们定义只是向量 f = ( f 1 , f 2 , . . . f n ) T ∈ N n {f = (f_{1}, f{2},...f_{n})^T \in N^n} f=(f1,f2,...fn)TNn
f i = { 1 if  v i   ∈  A 0 if  v i   ∈   A ‾ f_{i} = \begin{cases} 1 & \text{if $v_{i}$ $\in$ A} \\\\ 0 & \text{if $v_{i}$ $\in$ $\overline{A}$} \end{cases} fi=10if vi  Aif vi  A
因此目标函数转化为:
m i n ∑ v i ∈ A k , v j ∈ A k ‾ w i j ⟺ m i n 1 2 ∑ i = 1 n ∑ j = 1 n w i j ( f i − f j ) 2 min \sum_{v_{i} \in A_{k}, v_{j} \in \overline{A_{k}} }{w_{ij}} \Longleftrightarrow min \frac{1}{2} \sum_{i=1}^n\sum_{j=1}^n w_{ij}(f_{i} - f_{j})^2 minviAk,vjAkwijmin21i=1nj=1nwij(fifj)2 {$}

可以重点理解引入指示向量前后式子两边的等价性。

我们将 1 2 ∑ i = 1 s u m j = 1 w i j ( f i − f j ) 2 {\frac{1}{2} \sum_{i=1}sum_{j=1}w_{ij}(f_{i} - f_{j})^2} 21i=1sumj=1wij(fifj)2 进行展开:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-McFMjjXA-1656837856132)(C:\Users\86136\AppData\Roaming\Typora\typora-user-images\image-20220703110453710.png)]

其中,D为度矩阵,W为邻接矩阵(相似矩阵),而 L = D − W {L= D - W} L=DW 为拉普拉斯矩阵,因此原目标函数等价于:
a r g   m i n f i ∈ N n C u t ( V ) = a r g   m i n f i ∈ N n 1 2 ∑ i = 1 n ∑ j = 1 n w i j ( f i − f j ) 2 = a r g   m i n f i ∈ N n f T L f \underset{f_{i} \in N^n}{arg \ min} Cut(V) = \underset{f_{i} \in N^n}{arg \ min}\frac{1}{2} \sum_{i = 1}^n\sum_{j=1}^n w_{ij}(f_{i} - f_{j})^2 = \underset{f_{i} \in N^n}{arg \ min} f ^ T L f fiNnarg minCut(V)=fiNnarg min21i=1nj=1nwij(fifj)2=fiNnarg minfTLf
上面我们通过引入布尔指示向量将一个抽象的切割函数 C u t ( V ) {Cut(V)} Cut(V)转化为了就提可求解的 f T L f {f^T L f} fTLf.但是这个是离散的优化函数,无法进行求导。

推广到多个类(c取任意值)

对于聚多个类的问题,我们要聚c个类 ( k = 1 , 2 , . . . . , c ) (k = 1, 2, ...., c) (k=1,2,....,c), 因此我们需要将原来的一个指示向量扩展为c个指示向量组成的指示矩阵 H H H,其余步骤都一样。

首先我们定义向量 h k = ( h ( 1 , k ) , h 2 , k , . . . , h n , k ) T ∈ N n h_{k} = (h_{(1,k),h_{2,k}, ... ,h_{n,k} })^T \in N^{n} hk=(h(1,k),h2,k,...,hn,k)TNn(其中:$k = 1,2, …,c $)并满足以下公式:
h i k = { 1 if  v i ∈ A k   0 otherwise ( i = 1 , ⋅ ⋅ ⋅ , n ; k = 1 , ⋅ ⋅ ⋅ , c ) (指示向量) h_{ik} = \begin{cases} 1 & \text{if $v_{i} \in A_{k}$ } \\ \\ 0 & \text{otherwise} \end{cases} (i = 1, \cdot\cdot\cdot,n;k = 1,\cdot\cdot\cdot,c) \tag{指示向量} hik=10if viAk otherwise(i=1,,n;k=1,,c)()

上式其实就是给每个类分配了一个对应的布尔指示向量

我们将这c个指示向量 h k h_{k} hk组合成一个指示矩阵 H ∈ N n ∗ c H \in N^{n * c} HNnc, 矩阵 H H H当中的每一列指示向量正交于其他任何一列非线性相关向量。
T r ( H T H ) = n > 0 Tr(H^TH) = n > 0 Tr(HTH)=n>0
我们可以得到以下目标函数:
a r g   m i n C u t ( A 1 , ⋅ ⋅ ⋅ , A c ) A 1 , A 2 , ⋅ ⋅ ⋅ , A c ⟺ a r g   m i n   T r ( H T L H ) s . t    T r   ( H T H ) = n \underset{A_{1},A_{2},\cdot\cdot\cdot,A_{c}} {arg \ minCut(A_{1},\cdot\cdot\cdot,A_{c})} \Longleftrightarrow arg \ min \ Tr(H^TLH) \\ s.t \ \ Tr \ (H^TH) = n A1,A2,,Acarg minCut(A1,,Ac)arg min Tr(HTLH)s.t  Tr (HTH)=n
但是上面的目标函数存在一定局限,的哦到的分割结果往往倾向于将所连边最少且权值较低的孤立点分割出来。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dXQf7FJI-1656837856132)(C:\Users\86136\Desktop\nimg.ws.126.jpg)]
因此我们对原来的目标函数进行改进。

目标函数改进(RatioCut 与 Ncut)

其实原目标函数出现容易出现孤立点的原因是没有对子图规模进行限制,因此我们可以想到在原目标函数的基础上除以一个子图的规模。
子图规模的度量一般有以下两种方式:

  1. ✨从顶点数出发,用子图的顶点数代表子图的规模。
    ∣ A ∣    : =   t h e   n u m b e r   o f   v e r t i c e s i n   A (子图A的势) |A| \ \ := \ the \ number \ of \ verticesin \ A \tag{子图A的势} A  := the number of verticesin A(A)
  2. ✨从的角度出发可以利用子图种边权重之和作为子图的规模。

v o l ( A )    : =   ∑ v i ∈ A d i (子图A的体积) vol(A) \ \ := \ \sum_{v_{i} \in A}d_{i} \tag{子图A的体积} vol(A)  := viAdi(A)
对应的我们得到两种目标函数:

R a t i o C u t ( A 1 , A 2 , ⋅ ⋅ ⋅ , A c ) : = ∑ k = 1 c C u t ( A k , A k ‾ ) ∣ A k ∣ = 1 2 ∑ k = 1 c W ( A k , A k ‾ ) ∣ A k ∣ (比例切割) RatioCut(A_{1},A_{2},\cdot\cdot\cdot,A{c}) := \sum_{k=1}^c \frac{Cut(A_{k}, \overline{A_{k}})}{|A_{k}|} = \frac{1}{2} \sum_{k=1}^c \frac{W(A_{k},\overline{A_{k}})}{|A_{k}|} \tag{比例切割} RatioCut(A1,A2,,Ac):=k=1cAkCut(Ak,Ak)=21k=1cAkW(Ak,Ak)()

N c u t ( A 1 , A 2 , ⋅ ⋅ ⋅ , A c ) : = ∑ k = 1 c C u t ( A k , A k ‾ ) v o l ( A k ) = 1 2 ∑ k = 1 c W ( A k , A k ‾ ) v o l ( A k ) (归一化切割) Ncut(A_{1},A_{2},\cdot\cdot\cdot,A{c}) := \sum_{k=1}^c \frac{Cut(A_{k}, \overline{A_{k}})}{vol(A_{k})} = \frac{1}{2} \sum_{k=1}^c \frac{W(A_{k},\overline{A_{k}})}{vol(A_{k})} \tag{归一化切割} Ncut(A1,A2,,Ac):=k=1cvol(Ak)Cut(Ak,Ak)=21k=1cvol(Ak)W(Ak,Ak)()

目标函数求解

下面,我们先从RatioCut 的目标函数开始

RatioCut 目标函数近似求解
从聚两类开始(c = 2)

在上面我门已经推导出了:
m i n   C u t ( V ) ⟺ m i n   f T L f min \ Cut(V) \Longleftrightarrow min \ f^TLf min Cut(V)min fTLf
使用拉格朗日乘子将约束问题转化为无约束问题

我们将目标函数转化为 L a g r a n g i a n Lagrangian Lagrangian 函数 L ( f , λ ) L(f,\lambda) L(f,λ)
L ( f , λ )   : =   f T L f − λ ( f T 1 ) − λ ( f T f − n ) = f T L f − λ ( f T f − n ) L(f,\lambda) \ := \ f^TLf - \lambda(f^T1) - \lambda(f^Tf - n) \\ = f^TLf - \lambda(f^Tf - n) L(f,λ) := fTLfλ(fT1)λ(fTfn)=fTLfλ(fTfn)
等价将目标约束问题转换:
m i n f ∈ R n L ( f , λ ) ⟺ m i n f ∈ R n f T L f s . t f ⊥ 1 ( t h a t   i s   f T 1 = 0 ) f T f   =   n ( f T f > 0 ) \underset{f \in R^n}{min}L(f, \lambda) \Longleftrightarrow \underset{f \in R^n}{min}f^TLf \\ s.t f \bot 1 (that \ is \ f^T 1 = 0 ) \\ f^Tf \ = \ n \text{($f^Tf > 0$)} fRnminL(f,λ)fRnminfTLfs.tf1(that is fT1=0)fTf = n(fTf>0)
现在我们得到无约束且连续的目标函数,由于二次函数是天然的凸函数亦可导,可以快速找到全局最优解,只需要令器导数为0 即可得到极值。

求微分

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ialSBsyO-1656837856133)(C:\Users\86136\AppData\Roaming\Typora\typora-user-images\image-20220703135612948.png)]

求得导数

由常量微分 d y = ( d y d x ) T d x dy = (\frac{dy}{dx})^Tdx dy=(dxdy)Tdx得:

d L ( f , λ ) d f   =   [ 2 f T L T − 2 λ f T ] T = 2 L f − 2 λ f = 0 ⇒ L f = λ f \frac{dL(f,\lambda)}{df} \ = \ [ 2f^TL^T - 2 \lambda f^T ]^T = 2Lf - 2 \lambda f = 0 \\ \Rightarrow Lf = \lambda f dfdL(f,λ) = [2fTLT2λfT]T=2Lf2λf=0Lf=λf

我们发现,当Lagrange乘子 λ {\lambda} λ是拉普拉斯矩阵 L L L的特征值(eigenvalue)且指示向量 f f f L L L的特征向量(eigenvalue)时,函数有极值。

求出极值

我们在等式两边,分别左乘 f T f^T fT凑成目标函数形成后为:
f T L f = λ f T f = λ n f^TLf = \lambda f^Tf = \lambda n fTLf=λfTf=λn
由于 n = ∣ V ∣ n = |V| n=V是常数,显然取决于目标函数值大小的元素时 λ \lambda λ,即:
m i n   f T L f f T f = m i n   λ min \ \frac{f^TLf}{f^Tf} = min \ \lambda min fTffTLf=min λ
也就是说 L L L的特征值 λ \lambda λ越大,目标函数值越大;特征值 λ \lambda λ越小,目标函数值越小。

那么 λ \lambda λ的最小值时 λ m i n = 0 \lambda_{min} = 0 λmin=0, 令拉普拉斯矩阵的第二小特征值作为目标函数最优值。

对于二聚类( c = 2 c= 2 c=2)问题,,我们最简单粗暴的定性方法是判断 f i f_{i} fi是否大于等于0,即

{ v i ∈   A if   f i ≥ 0 v i ∈   A ‾ if  f i < 0 \begin{cases} v_{i} \in \ A & \text{if \ $f_{i} \geq 0 $} \\ \\ v_{i} \in \ \overline{A} & \text{if $f_{i} < 0 $} \end{cases} vi Avi Aif  fi0if fi<0

多聚类(c 取任意值)

R a t i o C u t   ( A i , ⋅ ⋅ ⋅ , A c )   =   ∑ k = 1 c ( H T L H ) i i   =   T r ( H T L H ) RatioCut \ (A_{i},\cdot\cdot\cdot,A_{c}) \ = \ \sum_{k=1}^{c}(H^TLH)_{ii} \ = \ Tr(H^TLH) RatioCut (Ai,,Ac) = k=1c(HTLH)ii = Tr(HTLH)

由于指示矩阵 H H H中向量之间的正交性,我们可以得到如下完整的目标函数:

a r g   m i n H ∈ R n ∗ c   T r ( H T L H ) s . t   H T H = I          [ T r ( H T H ) > 0 ] \underset{H \in R^{n * c}}{arg \ min} \ Tr(H^TLH) \\ s.t \ H^T H = I \ \ \ \ \ \ \ \ \text{$[Tr(H^TH) > 0]$} HRncarg min Tr(HTLH)s.t HTH=I        [Tr(HTH)>0]

类似的,可以推出:

m i n   T r ( H T L H ) ⟺ m i n   λ min \ Tr(H^TLH) \Longleftrightarrow min \ \lambda min Tr(HTLH)min λ
由于特征值 λ \lambda λ(拉格朗日乘子)在这里的物理意义代表切图的代价,因此我们可以将前 k k k小特征值对应的 k k k个特征向量拼接成一个矩阵 U   ∈ R N ∗ k U \ \in R^{N * k} U RNk作为 H ∈ R n ∗ c H \in R^{n*c} HRnc的近似解。然后使用传统的k-means方法,将连续实数值离散化,将矩阵 U U U的n行向量聚为c行向量,最终得到的label即为最终聚类结果。(推导这么多,看不懂的话可以直接看结果)

RatioCut 谱聚类算法流程

在这里插入图片描述

Ncut 谱聚类算法流程

在这里插入图片描述

Python-RatioCut 代码实现

import math
import os
import sys
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs

plt.rcParams['font.sans-serif']=['SimHei'] #用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False #用来正常显示负号

class MyPen(object):
  """
    ## 用作绘图的装饰器
  """
  def __init__(self, obj):
    obj.Pen = self
    self.obj = obj

  def __call__(self, *args, **kwds):
    return self.obj(*args, **kwds)

  @staticmethod
  def draw(pen,data,labels):
    """
      ## 绘制
    """
    nodeNum = len(data)
    scatterColors = ['black', 'blue', 'green', 'yellow', 'red', 'purple', 'orange']
    for i in range(len(set(labels))):
      color = scatterColors[i % len(scatterColors)]
      x1= []; y1=[]
      for j in range(nodeNum):
        if labels[j] == i:
          x1.append(data[j, 0])
          y1.append(data[j, 1])
      pen.scatter(x1, y1, c=color, marker='+')

  @staticmethod
  def viewResult(data,labels):
    """
      ## 绘制结果

      :pram ``data``: 数据集
      :pram ``clusterNum``: 聚类数量
      :pram ``labels``: 聚类结果
      :return None
    """
    MyPen.draw(plt,data,labels)
    plt.show()
  
  @staticmethod
  def viewCompare(data, trueLabels,predict_labels):
    """
    ## 绘制结果
    """,
    rows = 1
    cols = 2
    fig = plt.figure(figsize=(10,10))
    ax1 = fig.add_subplot(rows, cols, 1)
    ax2 = fig.add_subplot(rows, cols, 2)
    ax1.set_title('正确')
    ax2.set_title('预测')
    MyPen.draw(ax1, data, trueLabels)
    MyPen.draw(ax2, data, predict_labels)
    plt.show()


@MyPen
class MySpectral(object):
  """
    ## 谱聚类算法实现

  """
  def __init__(self,data, cluster_num=8, k=8):
    self.data = data
    self.cluster_num = cluster_num
    self.k = k

  def distance(self,x1,x2):
    """
      # 计算两个点之间的距离
      
    """
    return np.sqrt(np.sum((x1 - x2) ** 2))
  
  def get_dist_matrix(self):
    """
      ## 获取距离矩阵
    """
    nodesNum = len(self.data)
    distMatrix = np.array([
      [self.distance(self.data[i], self.data[j]) for j in range(nodesNum)] 
      for i in range(nodesNum)
    ])
    self.distMatrix = distMatrix
    return distMatrix
  
  def getW(self):
    """
      ## 获取邻接矩阵 W
    """
    distMatrix = self.get_dist_matrix()
    W = np.zeros_like(distMatrix)
    for idx, item in enumerate(distMatrix):
      idxArr = np.argsort(item)[1:self.k + 1]
      W[idx,idxArr] = 1
    self.W = (W + W.T) / 2
    return (W + W.T) / 2
  
  def getD(self):
    """
      ## 获得度矩阵
    """
    D = np.diag(np.sum(self.W,axis=1))
    self.D = D
    return D
  
  def getL(self):
    """
      ## 获得拉普拉斯矩阵
    """
    self.L = self.D - self.W
    return self.L

  def getEigen(self):
    """
    ## 获得特征值和特征向量

    """
    eigenValues, eigenVectors = np.linalg.eig(self.L)
    idx = np.argsort(eigenValues)[:self.cluster_num]
    return eigenVectors[:,idx]
  
  def fit(self):
    """
      ## 谱聚类算法入口
    """
    W = self.getW()
    D = self.getD()
    L = self.getL()
    eigenVectors = self.getEigen()
    clf = KMeans(n_clusters= self.cluster_num)
    s = clf.fit(eigenVectors)
    labels = s.labels_
    self.labels_ = labels
    return labels


def loadData():
  """
  # 导入数据集部分
  """
  ((nodesData, trueLabels)) = make_blobs()
  return nodesData, trueLabels

def test():
  data,trueLabels = loadData()
  model = MySpectral(data,8)
  model.fit()
  model.Pen.viewCompare(data, trueLabels ,model.labels_)

if __name__ == '__main__':
  test()

代码实现思路

主要分成两部分把一部分是可视化的写了一些装饰器的类,另一个就是谱聚类算法的类(重点)。

算法效果
比如我们设置聚8类
在这里插入图片描述

Python-Ncut 算法实现

根据推导结论Ncut 只要在RatioCut 算法的基础上修改一点就ok了,而且根据自己的实验发现Ncut 能更好得保证子图不会过小

import math
import os
import sys
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs

plt.rcParams['font.sans-serif']=['SimHei'] #用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False #用来正常显示负号

class MyPen(object):
  """
    ## 用作绘图的装饰器
  """
  def __init__(self, obj):
    obj.Pen = self
    self.obj = obj

  def __call__(self, *args, **kwds):
    return self.obj(*args, **kwds)

  @staticmethod
  def draw(pen,data,labels):
    """
      ## 绘制
    """
    nodeNum = len(data)
    scatterColors = ['black', 'blue', 'green', 'yellow', 'red', 'purple', 'orange']
    for i in range(len(set(labels))):
      color = scatterColors[i % len(scatterColors)]
      x1= []; y1=[]
      for j in range(nodeNum):
        if labels[j] == i:
          x1.append(data[j, 0])
          y1.append(data[j, 1])
      pen.scatter(x1, y1, c=color, marker='+')

  @staticmethod
  def viewResult(data,labels):
    """
      ## 绘制结果

      :pram ``data``: 数据集
      :pram ``clusterNum``: 聚类数量
      :pram ``labels``: 聚类结果
      :return None
    """
    MyPen.draw(plt,data,labels)
    plt.show()
  
  @staticmethod
  def viewCompare(data, trueLabels,predict_labels):
    """
    ## 绘制结果
    """,
    rows = 1
    cols = 2
    fig = plt.figure(figsize=(10,10))
    ax1 = fig.add_subplot(rows, cols, 1)
    ax2 = fig.add_subplot(rows, cols, 2)
    ax1.set_title('正确')
    ax2.set_title('预测')
    MyPen.draw(ax1, data, trueLabels)
    MyPen.draw(ax2, data, predict_labels)
    plt.show()


@MyPen
class MySpectral(object):
  """
    ## 谱聚类算法实现

  """
  def __init__(self,data, cluster_num=8, k=8):
    self.data = data
    self.cluster_num = cluster_num
    self.k = k

  def distance(self,x1,x2):
    """
      # 计算两个点之间的距离
      
    """
    return np.sqrt(np.sum((x1 - x2) ** 2))
  
  def get_dist_matrix(self):
    """
      ## 获取距离矩阵
    """
    nodesNum = len(self.data)
    distMatrix = np.array([
      [self.distance(self.data[i], self.data[j]) for j in range(nodesNum)] 
      for i in range(nodesNum)
    ])
    self.distMatrix = distMatrix
    return distMatrix
  
  def getW(self):
    """
      ## 获取邻接矩阵 W
    """
    distMatrix = self.get_dist_matrix()
    W = np.zeros_like(distMatrix)
    for idx, item in enumerate(distMatrix):
      idxArr = np.argsort(item)[1:self.k + 1]
      W[idx,idxArr] = 1
    self.W = (W + W.T) / 2
    return (W + W.T) / 2
  
  def getD(self):
    """
      ## 获得度矩阵
    """
    D = np.diag(np.sum(self.W,axis=1))
    self.D = D
    self.halfD = np.diag(np.sum(self.W,axis=1) ** -0.5)
    return D
  
  def getL(self):
    """
      ## 获得拉普拉斯矩阵
    """
    self.L = self.D - self.W
    self.L = self.halfD @ self.L @ self.halfD     ##两种算法的区别之处
    return self.L

  def getEigen(self):
    """
    ## 获得特征值和特征向量

    """
    eigenValues, eigenVectors = np.linalg.eig(self.L)
    idx = np.argsort(eigenValues)[:self.cluster_num]
    return eigenVectors[:,idx]
  
  def fit(self):
    """
      ## 谱聚类算法入口
    """
    W = self.getW()
    D = self.getD()
    L = self.getL()
    eigenVectors = self.getEigen()
    clf = KMeans(n_clusters= self.cluster_num)
    s = clf.fit(eigenVectors)
    labels = s.labels_
    self.labels_ = labels
    return labels


def loadData():
  """
  # 导入数据集部分
  """
  ((nodesData, trueLabels)) = make_blobs()
  return nodesData, trueLabels

def test():
  data,trueLabels = loadData()
  model = MySpectral(data,8)
  model.fit()
  model.Pen.viewCompare(data, trueLabels ,model.labels_)

if __name__ == '__main__':
  test()

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值