聚类算法的代码解析与实现

谱聚类算法的代码解析与实现

本文将对一个基于未归一化拉普拉斯矩阵的谱聚类算法进行详细解析。该算法的实现源自 SpeechBrain 项目,适用于语音片段的聚类,例如语音分离、说话人识别等任务。我们将逐步分析代码的每个部分,并解释其背后的数学原理。

代码简介

以下是 SpectralCluster 类的完整代码:

python 复制代码
class SpectralCluster:
    """A spectral clustering method using unnormalized Laplacian of affinity matrix."""
    def __init__(self, min_num_spks=1, max_num_spks=15, pval=0.022):
        self.min_num_spks = min_num_spks
        self.max_num_spks = max_num_spks
        self.pval = pval

    def __call__(self, X, oracle_num=None):
        # 计算相似度矩阵
        sim_mat = self.get_sim_mat(X)

        # p-剪枝
        pruned_sim_mat = self.p_pruning(sim_mat)

        # 矩阵对称化
        sym_pruned_sim_mat = 0.5 * (pruned_sim_mat + pruned_sim_mat.T)

        # 计算拉普拉斯矩阵
        laplacian = self.get_laplacian(sym_pruned_sim_mat)

        # 获取谱嵌入
        emb, num_of_spk = self.get_spec_embs(laplacian, oracle_num)

        # 聚类
        labels = self.cluster_embs(emb, num_of_spk)

        return labels

    def get_sim_mat(self, X):
        # 计算余弦相似度
        M = sklearn.metrics.pairwise.cosine_similarity(X, X)
        return M

    def p_pruning(self, A):
        if A.shape[0] * self.pval < 6:
            pval = 6.0 / A.shape[0]
        else:
            pval = self.pval

        n_elems = int((1 - pval) * A.shape[0])

        # 对每一行进行处理
        for i in range(A.shape[0]):
            low_indexes = np.argsort(A[i, :])
            low_indexes = low_indexes[0:n_elems]

            # 将较小的相似度值设为0
            A[i, low_indexes] = 0
        return A

    def get_laplacian(self, M):
        M[np.diag_indices(M.shape[0])] = 0
        D = np.sum(np.abs(M), axis=1)
        D = np.diag(D)
        L = D - M
        return L

    def get_spec_embs(self, L, k_oracle=None):
        lambdas, eig_vecs = scipy.linalg.eigh(L)

        if k_oracle is not None:
            num_of_spk = k_oracle
        else:
            lambda_gap_list = self.getEigenGaps(
                lambdas[self.min_num_spks - 1 : self.max_num_spks + 1]
            )
            num_of_spk = np.argmax(lambda_gap_list) + self.min_num_spks

        emb = eig_vecs[:, :num_of_spk]
        return emb, num_of_spk

    def cluster_embs(self, emb, k):
        _, labels, _ = k_means(emb, k)
        return labels

    def getEigenGaps(self, eig_vals):
        eig_vals_gap_list = []
        for i in range(len(eig_vals) - 1):
            gap = float(eig_vals[i + 1]) - float(eig_vals[i])
            eig_vals_gap_list.append(gap)
        return eig_vals_gap_list

1. 类的定义

python 复制代码
class SpectralCluster:
    """A spectral clustering method using unnormalized Laplacian of affinity matrix."""
    def __init__(self, min_num_spks=1, max_num_spks=15, pval=0.022):
        self.min_num_spks = min_num_spks  # 最小聚类数
        self.max_num_spks = max_num_spks  # 最大聚类数
        self.pval = pval  # p 值,用于 p-剪枝
  • 作用 :定义了一个 SpectralCluster 类,用于执行谱聚类算法。
  • 参数
    • min_num_spks:最小聚类数量,默认为 1。
    • max_num_spks:最大聚类数量,默认为 15。
    • pval:p-剪枝中的阈值参数,默认为 0.022。

2. 调用方法 __call__

python 复制代码
def __call__(self, X, oracle_num=None):
    # 计算相似度矩阵
    sim_mat = self.get_sim_mat(X)

    # p-剪枝
    pruned_sim_mat = self.p_pruning(sim_mat)

    # 矩阵对称化
    sym_pruned_sim_mat = 0.5 * (pruned_sim_mat + pruned_sim_mat.T)

    # 计算拉普拉斯矩阵
    laplacian = self.get_laplacian(sym_pruned_sim_mat)

    # 获取谱嵌入
    emb, num_of_spk = self.get_spec_embs(laplacian, oracle_num)

    # 聚类
    labels = self.cluster_embs(emb, num_of_spk)

    return labels
  • 流程概述
    1. 计算相似度矩阵 :调用 get_sim_mat 方法,计算数据点之间的相似度。
    2. p-剪枝 :调用 p_pruning 方法,对相似度矩阵进行稀疏化,保留较大的相似度值,去除较小的相似度值。
    3. 矩阵对称化:将剪枝后的相似度矩阵对称化,得到对称的相似度矩阵。
    4. 计算拉普拉斯矩阵 :调用 get_laplacian 方法,计算未归一化的拉普拉斯矩阵。
    5. 获取谱嵌入 :调用 get_spec_embs 方法,计算拉普拉斯矩阵的特征值和特征向量,得到谱嵌入。
    6. 聚类 :调用 cluster_embs 方法,对谱嵌入进行聚类(使用 k-means 算法)。
    7. 返回结果:输出聚类标签。

3. 详细步骤分析

3.1 计算相似度矩阵

python 复制代码
def get_sim_mat(self, X):
    # 计算余弦相似度
    M = sklearn.metrics.pairwise.cosine_similarity(X, X)
    return M
  • 作用:计算数据点之间的余弦相似度,得到相似度矩阵 ( M )。
  • 数学公式

M i j = cos ⁡ ( θ i j ) = X i ⋅ X j ∥ X i ∥ ∥ X j ∥ M_{ij} = \cos(\theta_{ij}) = \frac{X_i \cdot X_j}{\|X_i\| \|X_j\|} Mij=cos(θij)=∥Xi∥∥Xj∥Xi⋅Xj

  • 说明
    • 输入:特征矩阵 ( X ),形状为 ( (n_{\text{samples}}, n_{\text{features}}) )。
    • 输出:相似度矩阵 ( M ),形状为 ( (n_{\text{samples}}, n_{\text{samples}}) ),其中 ( M_{ij} ) 表示样本 ( X_i ) 和 ( X_j ) 的余弦相似度。

3.2 p-剪枝(p-pruning)

python 复制代码
def p_pruning(self, A):
    if A.shape[0] * self.pval < 6:
        pval = 6.0 / A.shape[0]
    else:
        pval = self.pval

    n_elems = int((1 - pval) * A.shape[0])

    # 对每一行进行处理
    for i in range(A.shape[0]):
        low_indexes = np.argsort(A[i, :])
        low_indexes = low_indexes[0:n_elems]

        # 将较小的相似度值设为0
        A[i, low_indexes] = 0
    return A
  • 作用:对相似度矩阵 ( A ) 进行稀疏化,保留每个数据点与其他数据点中最大的 ( p ) 百分比的相似度值,其余的相似度值设为零。
  • 说明
    • 调整 ( p ) 值:如果 ( A.shape[0] \times pval < 6 ),则调整 ( pval = \dfrac{6.0}{A.shape[0]} ),确保每行至少保留 6 个非零元素。

    • 计算要保留的元素数量

      n elems = int ( ( 1 − p v a l ) × A . s h a p e [ 0 ] ) n_{\text{elems}} = \text{int}\left((1 - pval) \times A.shape[0]\right) nelems=int((1−pval)×A.shape[0])

    • 对每一行

      • 排序索引:对第 ( i ) 行的相似度值进行升序排序,得到索引 ( \text{low_indexes} )。
      • 保留较大的相似度值:将较小的 ( n_{\text{elems}} ) 个相似度值设为零,保留较大的相似度值。
  • 目的:减少计算复杂度,构建稀疏的相似度矩阵,同时保留最相关的相似性信息。

3.3 矩阵对称化

python 复制代码
sym_pruned_sim_mat = 0.5 * (pruned_sim_mat + pruned_sim_mat.T)
  • 作用:将剪枝后的相似度矩阵对称化,得到对称的相似度矩阵 ( W )。
  • 说明
    • 对于非对称矩阵 ( A ),对称化操作为:

      W = 1 2 ( A + A ⊤ ) W = \dfrac{1}{2}(A + A^\top) W=21(A+A⊤)

    • 对称化后,( W_{ij} = W_{ji} ),满足相似度矩阵的对称性。

3.4 计算拉普拉斯矩阵

python 复制代码
def get_laplacian(self, M):
    M[np.diag_indices(M.shape[0])] = 0
    D = np.sum(np.abs(M), axis=1)
    D = np.diag(D)
    L = D - M
    return L
  • 作用:计算未归一化的拉普拉斯矩阵 ( L )。

  • 步骤

    1. 将对角元素设为零:( M_{ii} = 0 ),避免自环(节点自身的连接)。

    2. 计算度矩阵 ( D )

      D i i = ∑ j ∣ M i j ∣ D_{ii} = \sum_{j} |M_{ij}| Dii=j∑∣Mij∣

    3. 计算拉普拉斯矩阵 ( L )

      L = D − M L = D - M L=D−M

  • 说明

    • 度矩阵 ( D ):是一个对角矩阵,元素 ( D_{ii} ) 表示节点 ( i ) 的度数(与其他节点的连接强度之和)。
    • 拉普拉斯矩阵 ( L ):反映了图的结构性质,包含了节点之间的连接信息。

3.5 获取谱嵌入

python 复制代码
def get_spec_embs(self, L, k_oracle=None):
    lambdas, eig_vecs = scipy.linalg.eigh(L)

    if k_oracle is not None:
        num_of_spk = k_oracle
    else:
        lambda_gap_list = self.getEigenGaps(
            lambdas[self.min_num_spks - 1 : self.max_num_spks + 1]
        )
        num_of_spk = np.argmax(lambda_gap_list) + self.min_num_spks

    emb = eig_vecs[:, :num_of_spk]
    return emb, num_of_spk
  • 作用:计算拉普拉斯矩阵的特征值和特征向量,得到谱嵌入,并确定聚类的数量。
  • 步骤
    1. 特征值分解 :使用 scipy.linalg.eigh 计算拉普拉斯矩阵 ( L ) 的特征值 lambdas 和特征向量 eig_vecs

    2. 确定聚类数量 num_of_spk

      • 如果提供了真实的类别数 k_oracle,则直接使用。

      • 否则,计算特征值的间隙(差值),选择间隙最大的索引作为聚类数量。

        Gap i = λ i + 1 − λ i \text{Gap}i = \lambda{i+1} - \lambda_i Gapi=λi+1−λi

      • self.min_num_spksself.max_num_spks 范围内,找到最大的特征值间隙,推断出聚类数量。

    3. 获取谱嵌入 emb

      • 选择对应于最小特征值的前 num_of_spk 个特征向量。
  • 说明
    • 特征值和特征向量:拉普拉斯矩阵的特征值和特征向量包含了图的结构信息。
    • 谱嵌入:使用特征向量作为新的表示,将数据点从高维空间映射到低维空间。

3.6 聚类

python 复制代码
def cluster_embs(self, emb, k):
    _, labels, _ = k_means(emb, k)
    return labels
  • 作用 :对谱嵌入 emb 进行聚类,得到聚类标签 labels
  • 说明
    • 使用 k-means 算法:在谱嵌入的低维空间中,应用 k-means 聚类算法。
    • 输入
      • emb:谱嵌入,形状为 ( (n_{\text{samples}}, \text{num_of_spk}) )。
      • k:聚类数量,即 num_of_spk
    • 输出
      • labels:聚类标签,表示每个样本所属的簇。

4. 辅助方法

4.1 计算特征值间隙

python 复制代码
def getEigenGaps(self, eig_vals):
    eig_vals_gap_list = []
    for i in range(len(eig_vals) - 1):
        gap = float(eig_vals[i + 1]) - float(eig_vals[i])
        eig_vals_gap_list.append(gap)
    return eig_vals_gap_list
  • 作用:计算特征值之间的差值,用于确定聚类的数量。
  • 说明
    • 输入 :特征值列表 eig_vals,取自特征值序列的一部分(从最小的特征值开始)。
    • 输出 :特征值差值列表 eig_vals_gap_list,用于寻找最大的间隙。

5. 总结

谱聚类流程如下

  1. 计算相似度矩阵(Similarity Matrix)

    • 使用余弦相似度计算数据点之间的相似性,得到相似度矩阵 ( M )。
  2. p-剪枝(p-Pruning)

    • 对相似度矩阵进行稀疏化,保留每个数据点与其他数据点中最大的 ( p ) 百分比的相似度值,去除较小的相似度值,构建稀疏图。
  3. 矩阵对称化(Symmetrization)

    • 将剪枝后的相似度矩阵对称化,得到对称的相似度矩阵 ( W )。
  4. 计算拉普拉斯矩阵(Laplacian Matrix)

    • 计算度矩阵 ( D ),然后计算未归一化的拉普拉斯矩阵 ( L = D - W )。
  5. 计算特征值和特征向量(Spectral Embedding)

    • 对拉普拉斯矩阵 ( L ) 进行特征值分解,得到特征值和特征向量。
    • 通过寻找特征值的最大间隙,确定聚类数量 ( k )。
    • 选择对应于最小特征值的前 ( k ) 个特征向量,构成谱嵌入。
  6. 聚类(Clustering)

    • 在谱嵌入的低维空间中,使用 k-means 算法对数据进行聚类,得到聚类标签。

6. 数学原理

6.1 谱聚类的核心思想

  • 数据表示为图结构:将数据点视为图的节点,节点之间的边权重由数据点之间的相似度决定。
  • 拉普拉斯矩阵的作用:利用图的拉普拉斯矩阵的特征值和特征向量,获取数据的聚类信息。
  • 低维嵌入与聚类:通过拉普拉斯矩阵的特征向量,将数据从高维空间映射到低维空间,然后在该空间中进行聚类。

6.2 未归一化拉普拉斯矩阵

  • 度矩阵 ( D )

    D i i = ∑ j W i j D_{ii} = \sum_{j} W_{ij} Dii=j∑Wij

  • 拉普拉斯矩阵 ( L )

    L = D − W L = D - W L=D−W

  • 特征值与特征向量:拉普拉斯矩阵的特征值和特征向量反映了图的结构性质。

6.3 特征值的意义

  • 特征值为零的数量:与图的连通分量数量有关。
  • 特征值间隙:特征值之间的差值可以用于估计最优的聚类数量。

7. 代码中的特殊处理

  • p-剪枝的调整

    • 为了确保在小规模数据集上仍能保留足够的相似度信息,如果 ( n \times pval < 6 ),则调整 ( pval ),使得每行至少保留 6 个非零元素:

      如果 A . s h a p e [ 0 ] × p v a l < 6 , 则 p v a l = 6.0 A . s h a p e [ 0 ] \text{如果 } A.shape[0] \times pval < 6, \text{ 则 } pval = \dfrac{6.0}{A.shape[0]} 如果 A.shape[0]×pval<6, 则 pval=A.shape[0]6.0

  • 确定聚类数量

    • 如果未提供真实的聚类数量(oracle_num),则通过特征值间隙自动推断。
    • 这种方法利用了谱聚类中"特征值间隙最大处对应最优聚类数"的理论。

8. 实际应用中的注意事项

  • 参数选择

    • pval:影响相似度矩阵的稀疏程度,需要根据数据规模和特性进行调整。
    • min_num_spksmax_num_spks:设置聚类数量的搜索范围,需要根据先验知识设定。
  • 计算复杂度

    • 特征值分解的计算复杂度较高,对于大型数据集,可能需要优化或使用近似算法。
  • 数据预处理

    • 余弦相似度适用于已归一化的数据,如果输入数据未归一化,可能需要进行预处理。

9. 结论

该代码实现了谱聚类的标准流程,通过计算相似度矩阵、构建拉普拉斯矩阵、特征值分解和聚类,完成对数据的聚类任务。特殊之处在于使用 p-剪枝来稀疏化相似度矩阵,提高计算效率,并通过特征值间隙自动确定聚类数量。

该实现适用于语音说话者聚类等任务,但在其他应用中,需要根据具体情况调整参数和方法,以获得最佳效果。


参考文献


希望本文能帮助您深入理解谱聚类算法的实现与原理!

相关推荐
earthzhang202113 分钟前
《深入浅出HTTPS》读书笔记(12):块密码算法之迭代模式
网络协议·算法·http·https·1024程序员节
TANGLONG2221 小时前
【初阶数据结构和算法】leetcode刷题之设计循环队列
java·c语言·数据结构·c++·python·算法·leetcode
禁默2 小时前
路径规划算法之Dijkstra算法
算法
2401_878937712 小时前
数组和正则表达式
数据结构·算法
subject625Ruben2 小时前
亚太杯数学建模C题思路与算法(2024)
算法·数学建模
峰度偏偏2 小时前
【适配】屏幕拖拽-滑动手感在不同分辨率下的机型适配
算法·游戏·unity·ue5·ue4·godot
猫猫猫喵2 小时前
题目:素数列
算法
想成为高手4992 小时前
深入理解二叉搜索树(BST)
数据结构·c++·算法
是阿建吖!2 小时前
【优选算法】前缀和
算法
闻缺陷则喜何志丹3 小时前
【C++数论 因数分解】829. 连续整数求和|1694
c++·数学·算法·力扣··因数分解·组数