层次聚类算法全解析:从核心原理到Python实战应用

📅 2026/8/4 8:51:55
层次聚类算法全解析:从核心原理到Python实战应用
1. 项目概述从“分而治之”到“聚沙成塔”的层次化思维在数据挖掘和机器学习的工具箱里聚类算法一直扮演着“无监督探索者”的角色。我们之前聊过K-Means、DBSCAN这些耳熟能详的算法它们各有各的脾气K-Means喜欢球形数据DBSCAN能发现任意形状但参数敏感。今天要聊的“层次聚类”它的思路完全不同更像是在构建一棵“数据家谱树”。想象一下你有一堆零散的照片K-Means的做法是直接指定几个相册簇然后把每张照片硬塞进去而层次聚类则是先找出最相似的两张照片贴在一起然后这个“照片对”再去找下一个最相似的伙伴如此一层层合并最终所有照片都归入一个大家族或者反过来从一个大家族开始一层层分裂。这种“自底向上”或“自顶向下”的层次化组织方式就是层次聚类的核心魅力。它不要求你事先指定簇的个数而是输出一个完整的树状结构称为树状图让你可以像切蛋糕一样在任意“高度”上横切一刀得到你想要的簇划分。这对于数据内在结构不明确、或者你想探索不同粒度下的聚类结果的场景尤其有价值。比如在生物信息学中研究物种进化关系在文档分析中构建主题层级甚至在社交网络中发现社区的子社区结构层次聚类都能提供一种直观的、可解释的视角。接下来我们就深入这棵“数据树”的内部看看它是如何生长又如何为我们所用的。2. 层次聚类核心原理与算法家族层次聚类主要分为两大类凝聚的自底向上和分裂的自顶向下。凝聚式更为常用也是我们本次讨论的重点。它的过程非常直观就像一场持续不断的“联谊会”。2.1 凝聚式层次聚类的工作流程凝聚式层次聚类的算法可以概括为以下几个步骤初始化将数据集中的每个样本点都视为一个独立的簇。此时我们有N个簇。计算距离矩阵计算所有簇两两之间的距离。这里的“距离”定义是关键我们稍后详细讨论。合并最相似的簇找到距离矩阵中值最小的那一对簇即最相似的两个簇将它们合并为一个新的簇。更新距离矩阵合并簇后簇的总数减少了一个。我们需要重新计算这个新簇与其他所有簇之间的距离。如何定义两个“簇”之间的距离是区分不同层次聚类变种的核心称为“连接准则”。重复迭代重复步骤3和4直到满足某个终止条件。最常见的终止条件是所有样本点都合并到了一个簇中或者达到了预设的簇数量。这个过程会产生一个二叉树结构的合并记录即树状图。树状图的y轴通常代表合并时的距离或相似度清晰地展示了合并的先后顺序和代价。2.2 连接准则如何衡量“簇与簇”的远近单个点之间的距离我们可以用欧氏距离、曼哈顿距离、余弦相似度等。但当对象变成包含多个点的“簇”时如何定义簇间距离这里有几种主流策略单连接也称为最近邻连接。两个簇之间的距离定义为两个簇中最近的两个点之间的距离。公式d(C_i, C_j) min_{x in C_i, y in C_j} d(x, y)特点与影响单连接对噪声和离群点非常敏感容易形成“链式效应”——只要两个簇之间有一条由相近点组成的“桥梁”它们就会被优先合并导致最终聚类结果可能拉得很长像一条链子难以形成紧致的球状簇。它擅长发现非球形的、拉长的簇但抗噪能力差。全连接也称为最远邻连接。两个簇之间的距离定义为两个簇中最远的两个点之间的距离。公式d(C_i, C_j) max_{x in C_i, y in C_j} d(x, y)特点与影响与单连接相反全连接非常保守。它要求两个簇中所有点对都不能太远才会认为它们相近。这倾向于产生紧凑的、大小相近的球形簇。它对噪声点也有一定的抑制能力因为一个离群点会导致它所在簇与其他簇的距离变得很大。但缺点是可能过早地中断合并将本应属于一个的大簇分割成多个小簇。平均连接计算两个簇中所有点对之间的平均距离。公式d(C_i, C_j) (1 / (|C_i| * |C_j|)) * sum_{x in C_i} sum_{y in C_j} d(x, y)特点与影响这是单连接和全连接的一个折中方案。它既不像单连接那样容易受噪声影响产生链式结构也不像全连接那样严格导致过度分割。平均连接通常能产生相对均衡的簇是实践中比较稳健和常用的选择。沃德法这是一种方差最小化的方法。它合并簇的标准是使得合并后新簇的类内方差增量最小。换句话说它每次合并都力求让所有点尽可能紧致地围绕在簇中心周围。公式合并使得Δ(C_i, C_j) ESS(C_i ∪ C_j) - [ESS(C_i) ESS(C_j)]最小其中ESS是误差平方和。特点与影响沃德法倾向于生成大小相近、形状规则的球形簇效果上与K-Means的目标函数有相似之处。它对噪声和离群点也比较敏感因为离群点会显著增加类内方差。在许多场景下特别是当数据近似呈球形分布时沃德法能产生高质量的聚类结果。实操心得连接准则的选择选择哪种连接准则没有绝对的金科玉律它高度依赖于你的数据特性和分析目标。我的经验是如果你的数据有明显的“链状”或“流形”结构想发现这种结构可以尝试单连接但务必做好清洗噪声的准备。如果你希望得到紧凑、分离度好的球形簇全连接或沃德法是更好的选择。沃德法在数值上更稳定通常是我的首选。平均连接是一个很好的默认选项它在大多数情况下都能提供一个平衡的结果。当你对数据结构没有先验知识时从平均连接开始尝试是稳妥的。一个实用的技巧是同时运行多种连接准则对比它们的树状图。如果不同方法在某个高度上给出了相似的切割结果那么这个聚类很可能就是稳健的。2.3 分裂式层次聚类简介与凝聚式相反分裂式从一个包含所有样本的簇开始递归地将其分裂为更小的簇直到每个样本自成一体或满足停止条件。最著名的算法是DIANA。它的计算复杂度通常比凝聚式更高O(N²)甚至更高因为每一步都需要找到当前簇中“最不相似”的部分进行切分这本身就是一个聚类问题。因此在实践中凝聚式层次聚类的应用远多于分裂式。3. 层次聚类的完整实操流程与核心环节理解了原理我们来看如何动手实现一个层次聚类分析。这里我以Python的scikit-learn和scipy库为例因为它们是生态中最主流的工具。3.1 环境准备与数据模拟首先我们创建一个具有明显层次结构的数据集以便直观地观察效果。import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import make_blobs from scipy.cluster.hierarchy import dendrogram, linkage, fcluster from sklearn.metrics import silhouette_score import pandas as pd # 设置随机种子保证可复现性 np.random.seed(42) # 生成模拟数据先产生3个大簇每个大簇内部再细分 # 第一层3个大簇的中心 centers_big [[1, 1], [1, 5], [5, 3]] # 在每个大簇中心周围再生成2个小簇模拟层次结构 all_data [] all_labels_true [] # 用于记录真实的两层标签大簇和小簇 big_cluster_id 0 for i, center_big in enumerate(centers_big): # 每个大簇下有两个小簇中心偏移一些 offset 0.7 centers_small [ [center_big[0] - offset, center_big[1]], [center_big[0] offset, center_big[1]] ] for j, center_small in enumerate(centers_small): # 每个小簇生成30个点 data, _ make_blobs(n_samples30, centers[center_small], cluster_std0.3, random_state42i*10j) all_data.append(data) # 标签可以用一个元组或整数编码表示层次这里用整数编码百位表示大簇个位表示小簇 all_labels_true.extend([i*100 j] * 30) big_cluster_id 1 X np.vstack(all_data) # 最终数据矩阵形状 (180, 2) labels_true_hierarchical np.array(all_labels_true) # 真实层次标签 # 可视化原始数据 plt.figure(figsize(12, 4)) plt.subplot(1, 2, 1) plt.scatter(X[:, 0], X[:, 1], s30, clabels_true_hierarchical % 100, cmapviridis, alpha0.7) plt.title(Ground Truth - 6 Small Clusters) plt.xlabel(Feature 1) plt.ylabel(Feature 2) plt.subplot(1, 2, 2) big_cluster_color labels_true_hierarchical // 100 plt.scatter(X[:, 0], X[:, 1], s30, cbig_cluster_color, cmapplasma, alpha0.7) plt.title(Ground Truth - 3 Big Clusters) plt.xlabel(Feature 1) plt.ylabel(Feature 2) plt.tight_layout() plt.show()这段代码生成了180个二维点它们先被分到3个大簇颜色区分每个大簇内部又包含2个紧挨着的小簇。我们的目标是让层次聚类算法能揭示出这种两层结构。3.2 计算连接矩阵与生成树状图这是层次聚类的核心计算步骤。我们使用scipy.cluster.hierarchy.linkage函数。# 计算连接矩阵使用沃德法 Z_ward linkage(X, methodward, metriceuclidean) # Z 就是连接矩阵 # 同样我们也计算平均连接和单连接的结果以便对比 Z_average linkage(X, methodaverage, metriceuclidean) Z_single linkage(X, methodsingle, metriceuclidean) # 绘制树状图 fig, axes plt.subplots(1, 3, figsize(18, 5)) methods [Ward, Average, Single] linkage_matrices [Z_ward, Z_average, Z_single] for ax, Z, method in zip(axes, linkage_matrices, methods): # 绘制树状图 truncate_modelastp 可以只显示最后p个合并这里我们显示全部 dendrogram(Z, axax, orientationtop, truncate_modelastp, p30, show_leaf_countsTrue) ax.set_title(fDendrogram - {method} Linkage) ax.set_xlabel(Sample index or (cluster size)) ax.set_ylabel(Distance (Wards variance)) plt.tight_layout() plt.show()运行后你会得到三幅树状图。重点关注y轴距离和树的结构沃德法树状图在距离大约10和20的位置有明显的“跳跃”。在距离10处横切可能会得到6个簇对应底层小簇在距离20处横切会得到3个簇对应上层大簇。层次结构清晰。平均连接结构类似沃德法但距离尺度不同跳跃点可能不那么突兀。单连接你可能会看到典型的“链式效应”——合并距离增长缓慢没有明显的断层很难找到一个合适的切割点来区分3个或6个簇。这正体现了单连接对这类紧凑球形簇数据的局限性。注意事项树状图的解读叶子节点树状图底部的每个叶子代表一个初始样本。如果样本太多默认会拥挤在一起可以使用truncate_mode参数进行截断显示。合并高度y轴的值代表此次合并的“代价”。一个陡峭的上升意味着两个差异很大的簇被合并了这通常是一个好的切割位置。颜色阈值dendrogram函数可以通过color_threshold参数自动根据距离阈值给不同簇上色非常直观。例如dendrogram(Z, color_threshold15)会把合并距离小于15的链接涂成同一种颜色。3.3 切割树状图以获取聚类结果树状图展示了所有可能的合并历史但我们需要一个具体的簇划分。通过指定一个距离阈值或目标簇数量我们可以“切割”这棵树。# 方法1基于距离阈值切割 # 观察沃德法的树状图在距离~10和~20处有跳跃。我们尝试在距离15处切割。 distance_threshold 15 labels_ward_by_distance fcluster(Z_ward, tdistance_threshold, criteriondistance) print(f在距离阈值 {distance_threshold} 下形成了 {len(np.unique(labels_ward_by_distance))} 个簇。) # 方法2基于目标簇数量切割 # 我们希望得到3个大簇 k_big 3 labels_ward_by_k fcluster(Z_ward, tk_big, criterionmaxclust) print(f设定目标为 {k_big} 个簇实际形成了 {len(np.unique(labels_ward_by_k))} 个簇。) # 我们希望得到6个小簇 k_small 6 labels_ward_by_k_small fcluster(Z_ward, tk_small, criterionmaxclust) # 可视化基于簇数量的切割结果 fig, axes plt.subplots(1, 2, figsize(12, 4)) scatter1 axes[0].scatter(X[:, 0], X[:, 1], s30, clabels_ward_by_k, cmapplasma, alpha0.7) axes[0].set_title(fHierarchical Clustering - {k_big} Clusters (Ward)) axes[0].set_xlabel(Feature 1) axes[0].set_ylabel(Feature 2) plt.colorbar(scatter1, axaxes[0]) scatter2 axes[1].scatter(X[:, 0], X[:, 1], s30, clabels_ward_by_k_small, cmapviridis, alpha0.7) axes[1].set_title(fHierarchical Clustering - {k_small} Clusters (Ward)) axes[1].set_xlabel(Feature 1) axes[1].set_ylabel(Feature 2) plt.colorbar(scatter2, axaxes[1]) plt.tight_layout() plt.show()通过scipy.cluster.hierarchy.fcluster函数我们可以轻松地获得切割后的簇标签。criterionmaxclust指定按簇数量切割criteriondistance指定按距离阈值切割。3.4 聚类效果评估对于无监督学习评估需要一些技巧。我们可以使用内部指标如轮廓系数和外部指标如果已知真实标签如调整兰德指数ARI和标准化互信息NMI。from sklearn.metrics import adjusted_rand_score, normalized_mutual_info_score # 计算轮廓系数 (内部指标) - 越高越好范围[-1,1] silhouette_3 silhouette_score(X, labels_ward_by_k) silhouette_6 silhouette_score(X, labels_ward_by_k_small) print(f轮廓系数 (3簇): {silhouette_3:.4f}) print(f轮廓系数 (6簇): {silhouette_6:.4f}) # 计算外部指标 (需要真实标签)。我们分别与“3个大簇”和“6个小簇”的真实情况对比。 # 注意我们的 labels_true_hierarchical 编码了层次信息需要先提取出来。 true_labels_big (labels_true_hierarchical // 100).astype(int) # 提取大簇标签 (0,1,2) true_labels_small (labels_true_hierarchical % 100).astype(int) # 提取小簇标签 (0,1,2,3,4,5) # 评估3簇结果 vs 真实3个大簇 ari_3 adjusted_rand_score(true_labels_big, labels_ward_by_k) nmi_3 normalized_mutual_info_score(true_labels_big, labels_ward_by_k) print(f\n--- 评估 vs 3个大簇 ---) print(f调整兰德指数 ARI: {ari_3:.4f}) print(f标准化互信息 NMI: {nmi_3:.4f}) # 评估6簇结果 vs 真实6个小簇 ari_6 adjusted_rand_score(true_labels_small, labels_ward_by_k_small) nmi_6 normalized_mutual_info_score(true_labels_small, labels_ward_by_k_small) print(f\n--- 评估 vs 6个小簇 ---) print(f调整兰德指数 ARI: {ari_6:.4f}) print(f标准化互信息 NMI: {nmi_6:.4f})调整兰德指数和标准化互信息是衡量两个划分一致性的常用外部指标其值越接近1越好接近0表示与随机划分无异。通过对比你可以定量地知道在“3个大簇”和“6个小簇”这两个层次上你的聚类结果与真实结构有多吻合。4. 层次聚类的优势、局限与实战避坑指南层次聚类并非银弹理解其优缺点和适用场景才能让它发挥最大价值。4.1 核心优势无需预设簇数K这是其最吸引人的特点。你可以通过树状图直观地探索数据在不同尺度下的自然分组然后根据业务需求或统计指标如轮廓系数、类内距离跳跃点来决定切割位置。输出直观的树状图树状图本身就是一个强大的可视化工具和数据探索报告。它揭示了数据点之间的相似性关系和潜在的层次结构这是扁平化的K-Means结果无法提供的。确定性结果对于给定的距离度量和连接准则层次聚类的合并过程是确定的不受随机初始化的影响不像K-Means。灵活性可以与任何有效的距离度量结合使用适用于非数值型数据如使用编辑距离处理文本序列。4.2 主要局限与挑战计算和存储复杂度高这是最大的瓶颈。计算所有点对之间的距离矩阵需要 O(N²) 的时间和 O(N²) 的内存。对于大规模数据集例如超过1万个样本这几乎是不可行的。虽然有优化算法如SLINK, CLINK但复杂度依然很高。对噪声和离群点敏感尤其是单连接方法一个噪声点可能成为连接两个本不相关簇的“桥梁”。全连接和沃德法稍好但离群点仍可能扭曲距离计算。一旦合并不可撤销凝聚式算法是贪心算法每一步都合并当前最相似的簇。这个决定是最终的如果早期合并了一个“错误”的点这个错误会一直传递下去无法修正。切割点的选择具有主观性虽然树状图给了我们选择权但“在哪里下刀”往往没有绝对正确的答案需要结合领域知识和辅助指标来判断。4.3 常见问题与排查技巧实录在实际操作中你可能会遇到以下典型问题问题1数据量太大程序卡死或内存溢出。排查与解决第一步采样。对于探索性分析可以先对数据进行随机采样在子集上运行层次聚类观察大致结构。第二步降维。如果特征维度很高先使用PCA、t-SNE或UMAP等降维技术将数据压缩到2-3维再进行聚类。这不仅能提速还能可视化。第三步使用近似算法或分布式计算。研究scipy的linkage函数是否支持稀疏矩阵或更快的算法。对于超大规模数据考虑专为大数据设计的层次聚类实现或者转向基于采样的聚类方法如BIRCH。第四步换用其他算法。如果你并不需要树状图只是想要聚类结果且数据量巨大K-Means或Mini-Batch K-Means、DBSCAN通常是更高效的选择。问题2树状图看起来像“草丛”没有清晰的层次或跳跃点。排查与解决检查数据尺度确保所有特征都已标准化StandardScaler或归一化MinMaxScaler。一个量纲为万级的特征会完全主导量纲为个位的特征的距离计算。尝试不同的连接准则和距离度量。单连接容易产生“链”全连接容易产生“紧凑簇”。尝试沃德法适用于欧氏距离和近似球状数据或平均连接。对于文本或集合数据可以尝试杰卡德距离、余弦距离等。数据可能本就无层次结构。不是所有数据集都适合层次聚类。如果数据是均匀分布或只有一个密集区域树状图自然会显得平淡。此时用K-Means或DBSCAN看看肘部法则或轮廓系数可能更有意义。问题3如何客观地选择切割距离或簇数排查与解决观察距离跳跃在树状图的y轴上寻找合并距离突然大幅增加的位置。这个位置通常意味着两个差异很大的簇被合并了在此之下切割是合理的。可以绘制合并距离的增量图来辅助判断。使用统计指标像轮廓系数这样的内部指标可以在不同的切割方案不同簇数K下计算选择使轮廓系数最大的K。但要注意轮廓系数倾向于凸形的簇。结合业务逻辑这是最重要的。如果你知道客户应该分为“高/中/低”三档价值那么K3就是你的答案。层次聚类给了你验证这个假设的工具看看在K3时树状图的切割是否发生在距离跳跃处簇的纯度如何。问题4聚类结果中某个簇特别大其他簇特别小不均匀。排查与解决这可能是真实情况现实中客户规模、文章热度本身就是长尾分布。检查连接准则单连接特别容易导致“大海绵”簇吞噬小簇。尝试改用全连接或沃德法它们对簇的大小平衡更敏感。检查距离度量对于高维稀疏数据如文本TF-IDF余弦距离比欧氏距离更合适因为它更关注方向而非绝对距离能缓解一些维度灾难带来的问题。考虑后处理可以先得到一个较细的聚类比如K20然后根据业务规则手动合并一些小簇或者使用聚类集成技术来稳定结果。独家避坑技巧层次聚类与扁平化聚类的结合使用这是我常用的一个策略尤其适用于中等规模数据几千到几万样本。步骤如下先用K-Means做粗聚类设定一个较大的K值比如50或100将数据初步划分为大量微簇。这一步复杂度是 O(NKiter)对于大数据比 O(N²) 快得多。将微簇视为新样本计算每个微簇的中心或使用其内部所有点的代表。在微簇中心上运行层次聚类此时样本量从N减少到了K微簇个数计算距离矩阵的代价大大降低。生成微簇层次的树状图。分析和切割在微簇的树状图上进行分析和切割得到最终的簇划分。每个最终簇由若干个微簇组成。这个方法结合了K-Means的效率优势和层次聚类的可解释性、无需预设最终K值的优势是一种非常实用的两阶段聚类策略。5. 进阶话题与其它聚类算法的对比与选型思考我们提到了K-Means、DBSCAN现在又深入了解了层次聚类。在实际项目中如何选择这里提供一个简单的决策框架特性维度K-MeansDBSCAN层次聚类 (凝聚式)簇形状凸形球形任意形状取决于连接准则单连接可发现非凸形是否需要预设K是否 (需预设邻域参数)否(输出树状图后切割)处理噪声差 (所有点必属一簇)优(有噪声点类别)一般 (对噪声敏感尤其是单连接)复杂度O(NKiter)O(N log N) (使用空间索引)O(N²) 时间, O(N²) 内存结果稳定性依赖初始化可能局部最优对参数敏感确定性强输出可解释性簇中心核心点、边界点、噪声点树状图层次关系适用数据规模大中到大小到中(通常N10k)主要优势简单、高效、大规模数据发现任意形状簇、识别噪声可视化层次结构、无需预设K选型建议如果你的数据量很大10k且你知道或能估计大致的簇数量追求速度选K-Means。如果你的数据形状复杂非球形且有噪声你对簇数没概念但对邻域密度有感觉选DBSCAN。如果你的数据量不大10k你想探索数据内在的层次或谱系关系或者你完全不知道簇数需要可视化指导选层次聚类。多模态聚类或多视图聚类是更前沿的方向当你的数据来自多个来源或具有多种特征表示时如图像文本可以考虑这些方法但它们通常以基础聚类算法如谱聚类、K-Means为构建模块。层次聚类为我们提供了一种理解数据层次关系的独特视角。它更像是一个探索性数据分析工具而不仅仅是一个分类器。下次当你面对一组未知的数据不妨先画一张树状图也许数据的“家族秘密”就隐藏在那分叉的枝干之中。关键在于不要孤立地使用某一种算法理解其原理和代价才能根据手头的问题和资源做出最合适的选择。