深入解析k-means系列算法:从基础到进阶在load_iris数据集上的实战对比

📅 发布时间:2026/7/10 14:11:17 👁️ 浏览次数:
深入解析k-means系列算法:从基础到进阶在load_iris数据集上的实战对比
1. 从零开始什么是K-Means为什么它如此经典如果你刚接触机器学习听到“聚类”这个词可能会觉得有点玄乎。其实你可以把它想象成“物以类聚人以群分”的自动化版本。比如你有一堆没贴标签的鸢尾花数据花瓣长度、花瓣宽度等K-Means算法能自动帮你把这堆花分成几个不同的组每个组里的花彼此相似不同组的花则有明显差异。这就是无监督学习的魅力——让机器自己发现数据中的规律。K-Means算法之所以经典是因为它的思想直白得惊人就像我们整理衣柜先把衣服大致分成“上衣”、“裤子”、“外套”几堆这就是设定K值即簇的数量然后随便从每堆里拎出一件作为“代表”随机初始中心点。接着你拿着每一件衣服看它和哪个“代表”最像计算距离就把它放进哪一堆里分配样本。放完之后你会发现每一堆的衣服可能变了于是你重新从每一堆里找一件最具“代表性”的衣服重新计算中心点。如此反复直到“代表”衣服不再变化或者衣服不再换堆整理就算完成了。这个“不断迭代寻找中心”的过程就是K-Means的核心。听起来很简单对吧但正是这种简单让它成为了无数数据科学项目的入门首选。我在早期做用户分群分析时第一个想到的就是它。快速、直观能立刻给你一个数据分布的感性认识。不过简单也意味着有“坑”。最直接的两个问题就是我该分成几堆K值怎么定以及一开始随便选的那几件“代表”衣服万一选得不好怎么办这直接影响了最终整理衣柜的效果。我们接下来要聊的K-Means和Kernel K-Means其实就是针对这两个“坑”的升级解决方案。为了让大家有个直观感受我们这次全程都会用一个经典的数据集——load_iris鸢尾花数据集来实战。它包含了150朵鸢尾花的四个特征萼片和花瓣的长宽以及真实的品种标签。虽然我们做聚类时不用标签但最后可以用标签来检验我们的“自动分组”和“真实品种”吻合得怎么样非常方便。2. 基础实战手把手实现K-Means并剖析其局限理论说再多不如亲手跑一遍代码来得实在。我们这就用Python从加载数据开始一步步把经典的K-Means算法实现出来。我会把每一步为什么这么做、可能会遇到什么问题都讲清楚。2.1 数据准备与预处理首先我们得把数据拿到手并处理成算法能方便吃的“食材”。鸢尾花数据有四个维度为了方便我们肉眼观察和画图我们先选取其中两个维度比如花瓣长度和花瓣宽度来演示。这就像从一个人的身高、体重、年龄、收入中先挑出身高和体重来画个散点图看看分布道理是一样的。from sklearn.datasets import load_iris import numpy as np import matplotlib.pyplot as plt # 1. 加载数据 iris load_iris() data iris.data # 这是150x4的特征矩阵 target iris.target # 这是真实的品种标签0, 1, 2聚类时我们不用它但最后可以评估用 # 2. 为了方便可视化我们只取后两个特征花瓣长度和花瓣宽度 # 注意在实际项目中选择特征或降维是需要仔细考虑的这里仅为演示。 X data[:, 2:4] # 3. 看一眼原始数据分布 plt.figure(figsize(8, 6)) plt.scatter(X[:, 0], X[:, 1], cgray, alpha0.6, edgecolorsk, s50) plt.xlabel(Petal Length (cm)) plt.ylabel(Petal Width (cm)) plt.title(Raw Iris Data (Petal Features)) plt.grid(True, linestyle--, alpha0.5) plt.show()运行这段代码你会看到一个散点图。肉眼大概能看出数据点似乎可以分成两到三团。这印证了我们聚类是可行的。接下来就是重头戏实现K-Means算法本身。2.2 核心算法实现距离、分配与迭代K-Means的核心循环就三步计算距离、分配标签、更新中心。我们把它写成一个函数。这里我选择用欧氏距离就是初中几何的两点间直线距离因为它最常用也最直观。def my_kmeans(data, k, max_iters100, tol1e-4): 手写实现K-Means算法 参数 data: 二维数组形状为 (n_samples, n_features) k: 要聚类的簇数量 max_iters: 最大迭代次数防止无限循环 tol: 容忍度当中心点移动距离小于此值时认为已收敛 返回 centroids: 最终的中心点坐标 labels: 每个样本点所属的簇标签 n_samples, n_features data.shape # 1. 随机初始化中心点这是经典K-Means的起点也是其不稳定的根源 # 从数据点中随机选择k个作为初始中心 random_indices np.random.choice(n_samples, sizek, replaceFalse) centroids data[random_indices].copy() # 用于记录上一次的中心点用于判断收敛 prev_centroids np.zeros_like(centroids) for iteration in range(max_iters): # 2. 计算每个点到各个中心点的距离 distances np.zeros((n_samples, k)) for i in range(k): # 利用广播机制计算所有点到第i个中心的欧氏距离 distances[:, i] np.linalg.norm(data - centroids[i], axis1) # 3. 分配标签每个点选择距离最近的中心点所在的簇 labels np.argmin(distances, axis1) # 4. 更新中心点计算每个簇所有点的均值作为新中心 prev_centroids centroids.copy() for i in range(k): # 找到属于第i簇的所有点 cluster_points data[labels i] if len(cluster_points) 0: # 防止空簇出现 centroids[i] cluster_points.mean(axis0) # 如果出现空簇一个简单的策略是重新随机初始化该中心 # 这里为了简单我们保留旧中心实际项目中需要更鲁棒的处理 # 5. 检查收敛如果所有中心点的移动距离都小于容忍度则停止 centroid_shift np.linalg.norm(centroids - prev_centroids, axis1).max() if centroid_shift tol: print(f算法在 {iteration1} 次迭代后收敛。) break return centroids, labels写好了函数我们就可以在鸢尾花数据上跑一下了。假设我们凭经验或业务知识知道鸢尾花大概有3个品种所以我们设K3。# 运行我们手写的K-Means k 3 centroids, labels my_kmeans(X, k) # 可视化聚类结果 plt.figure(figsize(12, 5)) # 子图1显示聚类结果 plt.subplot(1, 2, 1) colors [red, blue, green] for i in range(k): cluster_points X[labels i] plt.scatter(cluster_points[:, 0], cluster_points[:, 1], ccolors[i], alpha0.6, edgecolorsk, s50, labelfCluster {i}) # 画出最终的中心点 plt.scatter(centroids[:, 0], centroids[:, 1], cblack, markerX, s200, labelCentroids) plt.xlabel(Petal Length) plt.ylabel(Petal Width) plt.title(fMy K-Means Result (K{k})) plt.legend() plt.grid(True, linestyle--, alpha0.5) # 子图2与真实标签对比看看我们“猜”得准不准 plt.subplot(1, 2, 2) for true_label in range(3): true_points X[target true_label] plt.scatter(true_points[:, 0], true_points[:, 1], ccolors[true_label], alpha0.6, edgecolorsk, s50, labelfTrue Species {true_label}) plt.xlabel(Petal Length) plt.ylabel(Petal Width) plt.title(True Iris Species) plt.legend() plt.grid(True, linestyle--, alpha0.5) plt.tight_layout() plt.show()把两张图放在一起对比你会发现一个有趣的现象我们算法分出来的三个簇和真实的三个鸢尾花品种重合度可能很高但不一定完全一样。每次运行由于初始中心点是随机的结果都可能略有不同。这就是经典K-Means的第一个大问题对初始值敏感。你可能运气好一次就分得很准也可能运气差分出来的结果明显不合理。2.3 如何评估好坏聊聊内部与外部指标既然结果可能波动我们怎么判断哪次运行的结果好呢这就需要评估指标。对于有真实标签的数据比如iris我们可以用外部指标比如调整兰德指数Adjusted Rand Index, ARI。ARI在-1到1之间值越大表示聚类结果与真实标签越吻合1表示完全一致0表示随机分配负数表示比随机还差。from sklearn import metrics # 计算调整兰德指数 ari_score metrics.adjusted_rand_score(target, labels) print(f调整兰德指数 (ARI) 为: {ari_score:.4f})对于没有真实标签的数据我们则要用内部指标比如轮廓系数Silhouette Coefficient。它衡量的是同一个簇内的样本是否足够紧密不同簇的样本是否足够分离。轮廓系数也在-1到1之间越大越好。# 计算轮廓系数需要用到所有特征这里用完整的4维数据演示更准确 from sklearn.metrics import silhouette_score full_data_labels my_kmeans(data, k)[1] # 在完整4维数据上聚类 sil_score silhouette_score(data, full_data_labels) print(f轮廓系数为: {sil_score:.4f})在实际项目中我常常会多次运行K-Means比如10次然后取ARI或轮廓系数最高的那次结果作为最终输出。这是一种简单有效的策略。2.4 经典K-Means的优缺点总结通过上面的实战我们可以切身感受到K-Means的脾性优点确实突出简单好懂逻辑清晰跟任何人解释起来都不费劲。效率很高计算复杂度是线性的即使处理几十万、上百万的数据点速度也很快。我记得第一次处理用户行为日志时用K-Means做初步分群速度让我很惊喜。对于像“球形”一样分布的数据各向同性效果通常不错。鸢尾花数据就近似这种分布。但缺点也很明显正是我们需要改进的地方K值需要事先指定这就像让你蒙着眼睛分豆子却要先说好分几碗。选错了K结果可能毫无意义。常用的“肘部法则”看误差随K变化的拐点或轮廓系数法可以辅助选择但都不是绝对可靠的。对初始中心点敏感随机性可能导致次优解。我们上面多次运行取最优的策略其实就是一种笨办法的补救。对异常值敏感均值计算会被极端值拉偏。想象一个簇里混进了一个距离非常远的点整个簇的中心就会被它“拖走”。只能发现球形的簇这是几何限制。如果真实的数据簇是流形、环形或者任何非凸形状经典K-Means就无能为力了。它假设簇是“各向同性”的这在现实世界中往往不成立。3. 进阶第一步K-Means 如何聪明地选择起点了解了经典K-Means的软肋我们很自然就会想能不能别那么“随机”地选起点K-Means算法就是来解决这个问题的。它的核心思想非常巧妙让初始的聚类中心彼此尽量离得远一些。这样它们就更有可能落在不同的、真实的簇里面为后续迭代提供一个更好的起点。3.1 K-Means 的初始化策略具体怎么做呢它采用了一种“概率化”的最远点采样第一个中心点完全随机从数据点中选一个。第二个中心点计算剩余每个点到当前已选中心点的最近距离。距离越远的点被选为下一个中心的概率越大。然后按照这个概率分布随机选取下一个中心。重复步骤2直到选够K个中心。这个“距离越远概率越大”的机制保证了新中心点不会扎堆在已选中心附近而是有倾向性地去探索数据空间的其他区域。这比纯随机选找到更优起点的概率大得多。3.2 代码实现与对比我们来修改一下初始化部分的代码实现K-Meansdef kmeans_plus_plus_init(data, k): K-Means 初始化中心点 n_samples, n_features data.shape centroids np.zeros((k, n_features)) # 1. 随机选择第一个中心点 first_idx np.random.randint(n_samples) centroids[0] data[first_idx] for i in range(1, k): # 2. 计算每个数据点到已有中心点的最短距离的平方 distances np.zeros(n_samples) for j in range(n_samples): # 计算该点到所有已有中心点的距离取最小的那个 min_dist np.min([np.linalg.norm(data[j] - centroids[c]) ** 2 for c in range(i)]) distances[j] min_dist # 3. 将距离平方转换为概率距离越远概率越大 probabilities distances / distances.sum() # 4. 根据概率分布随机选择下一个中心点 next_idx np.random.choice(n_samples, pprobabilities) centroids[i] data[next_idx] return centroids # 将我们之前的my_kmeans函数升级为使用K-Means初始化 def my_kmeans_plusplus(data, k, max_iters100, tol1e-4): n_samples, n_features data.shape # 使用K-Means初始化 centroids kmeans_plus_plus_init(data, k) prev_centroids np.zeros_like(centroids) for iteration in range(max_iters): # 分配标签与经典K-Means完全相同 distances np.zeros((n_samples, k)) for i in range(k): distances[:, i] np.linalg.norm(data - centroids[i], axis1) labels np.argmin(distances, axis1) # 更新中心点与经典K-Means完全相同 prev_centroids centroids.copy() for i in range(k): cluster_points data[labels i] if len(cluster_points) 0: centroids[i] cluster_points.mean(axis0) # 检查收敛 centroid_shift np.linalg.norm(centroids - prev_centroids, axis1).max() if centroid_shift tol: print(fK-Means 在 {iteration1} 次迭代后收敛。) break return centroids, labels现在让我们在同样的鸢尾花数据上运行K-Means并和经典K-Means做个快速对比。为了公平我们各运行10次看看它们的稳定性。def compare_initializations(data, true_labels, k3, n_runs10): classic_ari_scores [] plusplus_ari_scores [] for run in range(n_runs): # 经典K-Means _, labels_classic my_kmeans(data, k) ari_classic metrics.adjusted_rand_score(true_labels, labels_classic) classic_ari_scores.append(ari_classic) # K-Means _, labels_pp my_kmeans_plusplus(data, k) ari_pp metrics.adjusted_rand_score(true_labels, labels_pp) plusplus_ari_scores.append(ari_pp) # 打印统计结果 print( 初始化方法对比 (10次运行) ) print(f经典K-Means ARI: 均值{np.mean(classic_ari_scores):.4f}, 标准差{np.std(classic_ari_scores):.4f}) print(fK-Means ARI: 均值{np.mean(plusplus_ari_scores):.4f}, 标准差{np.std(plusplus_ari_scores):.4f}) # 可视化得分分布 plt.figure(figsize(8, 5)) positions [1, 2] box_data [classic_ari_scores, plusplus_ari_scores] plt.boxplot(box_data, labels[Classic K-Means, K-Means]) plt.ylabel(Adjusted Rand Index (ARI)) plt.title(Clustering Stability Comparison) plt.grid(True, axisy, linestyle--, alpha0.7) plt.show() # 执行对比 compare_initializations(X, target, k3)你大概率会看到K-Means得到的ARI均值更高而且标准差更小。这意味着K-Means不仅平均效果更好而且每次运行的结果更稳定不再那么依赖“运气”。虽然初始化时多花了一点计算距离的时间但因为它提供了更好的起点算法整体收敛往往更快迭代次数更少很多时候总时间反而更优。3.3 何时使用K-Means我的经验是几乎在任何使用K-Means的场景下都应该默认使用K-Means初始化。这是成本极低、收益明显的改进。现在主流的机器学习库如scikit-learn里KMeans类的默认初始化方法就是k-means。它已经成为了事实上的标准。4. 高阶跨越Kernel K-Means 如何解决非线性难题好了我们现在有了更稳定的起点。但K-Means还有一个根本性的限制没解决它只能处理线性可分、近似球形的簇。现实中的数据可没这么规矩。比如想象一下卫星遥感图像中不同地貌的分布或者社交网络中社区的结构它们很可能是复杂交织在一起的。4.1 从线性到非线性核技巧的魔法Kernel K-Means的灵感来源于支持向量机SVM中的核方法Kernel Trick。它的想法非常聪明既然数据在原始空间比如二维平面中缠绕在一起线性不可分那我们能不能把它映射到一个更高维的空间里去在那个高维空间里原来纠缠的数据点可能就变得线性可分了。最经典的例子就是“同心圆”数据。在二维平面上两个环套在一起用直线怎么也分不开。但如果我们把每个点(x, y)映射到三维空间(x, y, x^2 y^2)你会发现原来在二维平面上的两个圆环在三维空间里被“拉”开了高度差变得可以用一个平面轻松分开。这个新加的维度x^2 y^2其实就是点到原点的距离平方它是一种非线性变换。核技巧的妙处在于我们不需要真的显式地把所有数据点都计算并存储到这个高维空间可能维度爆炸计算不了。我们只需要知道在高维空间里任意两个点之间的距离更一般地是内积怎么计算就行。而这个距离可以通过一个在原空间定义的核函数Kernel Function来巧妙得到。常用的核函数有径向基函数RBF核、多项式核等。4.2 在合成数据上实战 Kernel K-Means为了彻底看清Kernel K-Means的能力我们先用sklearn生成一个经典的“同心圆”数据集来演示。我们会对比经典K-Means和Kernel K-Means的处理效果。from sklearn.datasets import make_circles from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans # 注意Scikit-learn没有直接提供KernelKMeans我们可以用 SpectralClustering使用了核方法来近似演示原理。 # 更地道的实现可能需要参考其他库或自己实现这里以原理演示为主。 from sklearn.cluster import SpectralClustering # 1. 生成非线性数据集两个同心圆 X_circles, y_circles make_circles(n_samples400, factor0.5, noise0.05, random_state42) X_circles StandardScaler().fit_transform(X_circles) # 标准化 # 2. 尝试用经典K-Means聚类注定会失败 kmeans_circles KMeans(n_clusters2, random_state42, initk-means) y_pred_kmeans kmeans_circles.fit_predict(X_circles) # 3. 尝试用基于RBF核的谱聚类近似Kernel K-Means的效果 # 谱聚类先构建数据的相似度图常用RBF核再对图进行切割能处理非线性结构。 spectral_circles SpectralClustering(n_clusters2, affinityrbf, gamma5.0, random_state42) y_pred_spectral spectral_circles.fit_predict(X_circles) # 4. 可视化对比 fig, axes plt.subplots(1, 3, figsize(15, 4)) # 真实分布 axes[0].scatter(X_circles[:, 0], X_circles[:, 1], cy_circles, cmapviridis, edgecolorsk, s30) axes[0].set_title(Ground Truth (Two Circles)) axes[0].set_xlabel(Feature 1) axes[0].set_ylabel(Feature 2) axes[0].grid(True, linestyle--, alpha0.5) # 经典K-Means结果 axes[1].scatter(X_circles[:, 0], X_circles[:, 1], cy_pred_kmeans, cmapviridis, edgecolorsk, s30) axes[1].scatter(kmeans_circles.cluster_centers_[:, 0], kmeans_circles.cluster_centers_[:, 1], cred, markerX, s200, labelCenters) axes[1].set_title(Classic K-Means (Fails)) axes[1].set_xlabel(Feature 1) axes[1].legend() axes[1].grid(True, linestyle--, alpha0.5) # 谱聚类核方法结果 axes[2].scatter(X_circles[:, 0], X_circles[:, 1], cy_pred_spectral, cmapviridis, edgecolorsk, s30) axes[2].set_title(Spectral Clustering (RBF Kernel)) axes[2].set_xlabel(Feature 1) axes[2].grid(True, linestyle--, alpha0.5) plt.tight_layout() plt.show()这张对比图会非常直观经典K-Means完全搞不定环形数据它固执地按照“距离最近”的原则把空间切成了两个半圆形的簇。而使用了RBF核的谱聚类则成功识别出了两个环形的真实结构。这就是核方法的威力。4.3 在 Iris 数据集上尝试核变换那么对于我们的鸢尾花数据Kernel K-Means会不会有提升呢鸢尾花数据本身近似线性可分所以提升可能不像环形数据那么戏剧性。但我们可以尝试引入一个多项式核看看能否发现一些更细微的结构。这里我们手动模拟一个多项式核的升维思想# 尝试对鸢尾花数据进行简单的多项式特征映射模拟核函数思想 from sklearn.preprocessing import PolynomialFeatures # 使用原始4维数据 X_iris_full data # 1. 经典K-Means (在原始空间) kmeans_orig KMeans(n_clusters3, initk-means, n_init10, random_state42) labels_orig kmeans_orig.fit_predict(X_iris_full) ari_orig metrics.adjusted_rand_score(target, labels_orig) # 2. 先进行多项式特征变换例如加入特征的平方和交互项再运行K-Means # 这相当于使用了一个多项式核将数据映射到更高维空间 poly PolynomialFeatures(degree2, include_biasFalse) # 二次多项式 X_poly poly.fit_transform(X_iris_full) print(f原始特征维度: {X_iris_full.shape[1]} 多项式变换后维度: {X_poly.shape[1]}) kmeans_poly KMeans(n_clusters3, initk-means, n_init10, random_state42) labels_poly kmeans_poly.fit_predict(X_poly) ari_poly metrics.adjusted_rand_score(target, labels_poly) print(f原始空间 K-Means ARI: {ari_orig:.4f}) print(f多项式特征空间 K-Means ARI: {ari_poly:.4f})你会发现加了多项式特征后ARI可能会有微小的提升或波动。这说明了对于某些数据在高维空间中数据的可分性确实会变好。但务必注意盲目升维也可能引入噪声和过拟合并且计算量会急剧增加。核函数的精妙之处就在于它避免了显式升维的计算灾难。4.4 Kernel K-Means 的代价与选择能力越强责任越大代价也越高。Kernel K-Means或其近似实现如谱聚类的主要缺点是计算复杂度高需要计算所有样本两两之间的核矩阵相似度矩阵是O(n²)的复杂度对于大数据集非常吃力。参数选择更复杂除了K值你还需要选择核函数的类型RBF、多项式等及其参数如gamma这通常需要交叉验证调参成本高。结果可解释性降低我们得到了高维空间的聚类结果但很难直观理解这个簇在原始特征空间到底意味着什么。所以我的实用建议是先从经典K-Means开始。如果发现聚类结果明显不合理比如轮廓系数很低或者可视化后看到簇的形状明显非球形并且你的数据量不是特别大比如几万条以内再考虑尝试Kernel K-Means或谱聚类。它更像是一个专门解决复杂非线性结构的“高级武器”而不是日常的“瑞士军刀”。5. 综合对比与实战选择指南走过了从经典到改进再到高阶的旅程是时候把K-Means家族的三位成员拉出来做个全方位的性能对比了。我根据多年的使用经验整理了一个决策表格你可以把它当成速查手册。特性维度经典 K-MeansK-MeansKernel K-Means (以谱聚类为例)核心改进基准算法优化初始化提升稳定性与效果引入核函数处理非线性可分数据时间复杂度O(n * k * i)(n样本数k簇数i迭代数)O(n * k * i) 初始化开销(通常总时间更优)O(n²) 到 O(n³)(计算核矩阵内存和计算瓶颈)空间复杂度O(n * k)O(n * k)O(n²)(需要存储核矩阵)关键参数K (簇数)K (簇数)K (簇数)核函数类型核参数(如gamma)对数据形状的假设球形簇各向同性方差球形簇各向同性方差无特定形状要求能处理流形、环形等对异常值敏感(均值受影响)敏感(均值受影响)相对稳健(依赖于核函数如RBF对远距离不敏感)结果可解释性高(中心点即簇均值)高(中心点即簇均值)较低(簇是图划分结果无显式中心)Scikit-learn 对应类KMeans(initrandom)KMeans(initk-means)(默认)SpectralClustering5.1 在 Iris 数据集上的量化对比让我们用load_iris的完整数据4个特征以多次运行的方式从几个关键指标上量化对比一下。我们会使用scikit-learn的成熟实现以保证效率和准确性。from sklearn.cluster import KMeans, SpectralClustering from sklearn.metrics import adjusted_rand_score, silhouette_score, calinski_harabasz_score import time X data # 使用完整的4维鸢尾花数据 true_labels target k 3 n_runs 5 # 每种方法运行5次取平均 results {Classic K-Means: {ARI: [], Silhouette: [], Time: []}, K-Means: {ARI: [], Silhouette: [], Time: []}, Spectral (RBF): {ARI: [], Silhouette: [], Time: []}} for run in range(n_runs): # 1. 经典K-Means (随机初始化) start time.time() kmeans_classic KMeans(n_clustersk, initrandom, n_init1, random_staterun).fit(X) time_classic time.time() - start results[Classic K-Means][ARI].append(adjusted_rand_score(true_labels, kmeans_classic.labels_)) results[Classic K-Means][Silhouette].append(silhouette_score(X, kmeans_classic.labels_)) results[Classic K-Means][Time].append(time_classic) # 2. K-Means (默认就是k-means) start time.time() kmeans_pp KMeans(n_clustersk, initk-means, n_init1, random_staterun).fit(X) time_pp time.time() - start results[K-Means][ARI].append(adjusted_rand_score(true_labels, kmeans_pp.labels_)) results[K-Means][Silhouette].append(silhouette_score(X, kmeans_pp.labels_)) results[K-Means][Time].append(time_pp) # 3. 谱聚类 (RBF核) - 注意谱聚类本身具有随机性我们固定random_state start time.time() # 谱聚类参数需要调这里gamma选一个经验值。对于小数据集affinity也可以用‘nearest_neighbors’ spectral SpectralClustering(n_clustersk, affinityrbf, gamma0.1, random_staterun).fit(X) time_spectral time.time() - start results[Spectral (RBF)][ARI].append(adjusted_rand_score(true_labels, spectral.labels_)) results[Spectral (RBF)][Silhouette].append(silhouette_score(X, spectral.labels_)) results[Spectral (RBF)][Time].append(time_spectral) # 打印平均结果 print( 在 Iris 数据集上的平均性能对比 (5次运行) ) print(f{算法:20} {ARI均值:10} {Silhouette均值:15} {平均耗时(秒):15}) print(- * 65) for algo, vals in results.items(): avg_ari np.mean(vals[ARI]) avg_sil np.mean(vals[Silhouette]) avg_time np.mean(vals[Time]) print(f{algo:20} {avg_ari:10.4f} {avg_sil:15.4f} {avg_time:15.4f})运行这段对比代码你会得到清晰的数字结论。在Iris这个线性可分性较好的数据集上K-Means大概率在ARI和轮廓系数上表现最优且最稳定同时速度极快。经典K-Means的ARI波动会更大。而谱聚类作为Kernel K-Means的代理的ARI可能与之相当或略低但计算耗时通常会高出1-2个数量级。这完美印证了我们的理论分析对于近似球形簇的数据K-Means是性价比最高的选择。5.2 给你的实战选择流程图面对一个新聚类任务我通常会遵循下面这个思考路径你也可以参考数据探索与可视化无论如何先用PCA或t-SNE降维后画个图看看数据点的大致形状。如果肉眼可见是几团“云朵”那么K-Means家族很可能适用。基线模型永远从K-Means开始。用肘部法则或轮廓系数确定一个大概的K值范围快速跑出基线结果。评估与诊断计算轮廓系数。如果系数很低比如0.5或者可视化发现簇的形状很奇怪比如长条形、环形就怀疑数据是非线性结构。考虑升级如果数据量不大比如1万条且怀疑是非线性问题尝试谱聚类。准备好调参主要是affinity和gamma。处理异常值与量纲无论用哪种算法都要注意数据预处理。标准化StandardScaler通常很重要因为K-Means基于距离。对于异常值可以考虑用RobustScaler或在聚类前进行清洗。最终验证如果有可能结合业务知识验证聚类结果的意义。纯粹的数据指标再高如果分出来的簇业务上无法解释那这个模型也可能没有价值。在我经历的一个电商用户画像项目中初期用K-Means对用户消费行为聚类效果不错。但后来加入用户社交关系图数据后发现簇的结构变得复杂K-Means的轮廓系数骤降。后来切换到了谱聚类使用‘nearest_neighbors’ affinity才得到了业务方认可的、具有“小圈子”特性的分群结果。这个案例告诉我没有万能的算法只有最适合数据和问题的工具。