
K-Means是我觉得最值得花一整章去讲清楚的算法之一。它不是最简单的机器学习方法但绝对是最适合建立“无监督学习直觉”的切入点。你拿到一堆数据没有标签没有标准答案甚至不确定该分成几组这就是聚类要解决的问题。现实中的用户分群、图像压缩、异常检测、文档归类、基因表达数据分析背后都可能是K-Means或者它的变体在起作用。这一章我从直觉理解讲到数学原理再带你手写一遍核心逻辑最后落到sklearn的工程实现顺带把聚类家族里其他几个常用算法层次聚类、DBSCAN、谱聚类都串一遍。不管你是刚学机器学习的初学者还是要做实际项目的开发者这章内容都值得完整读一遍。1. 聚类是什么没有标准答案的“分门别类”1.1 有监督和无监督的本质区别学K-Means之前先得搞清楚它和前面章节里那些算法比如线性回归、逻辑回归、决策树的根本差异。线性回归也好分类树也好训练数据里都带着“答案”——你要预测房价就有真实的成交价你要判断邮件是否是垃圾邮件就有打过标签的训练集。这类算法统称有监督学习核心模式是“看例子学规律”。无监督学习是另外一种玩法数据摆在那里没有标签没有任何“正确答案”。你拿到10000条用户行为日志不会有人告诉你哪条记录属于“高价值用户”或者“即将流失用户”你得靠自己发现数据内部的结构。聚类算法干的就是这件事——把相似的东西自动归到一堆。我经常用一句话总结两者的区别有监督学习是拿着地图找路无监督学习是靠自己画出地图。1.2 聚类在解决什么现实问题聚类不是学术圈自嗨的工具它的应用场景遍及各行各业。电商领域最典型你有一堆用户的购买记录但没有任何人群标签用K-Means把用户分成几组高消费低频次的一组低消费高频次的一组每组单独做运营策略这就是用户分群。视频平台的内容推荐里聚类也常用来做召回层的粗筛把相似视频先聚个类再在类内做精细推荐。图像压缩是另一个经典案例。一张真彩图片每个像素点有RGB三个通道颜色数量可能达到几百万种。用K-Means把相近的颜色归为一类用聚类中心替代类内所有像素的颜色256色或者16色就能近似还原原图文件体积大幅减小。异常检测也常用聚类正常数据会聚集在几个簇里离所有簇都很远的样本大概率是噪声或者异常。银行风控、工业设备故障诊断、网络入侵检测都能看到聚类的身影。所以说聚类不是一个孤立的算法而是一整套“无标签数据的结构化方法论”K-Means是这整套方法里最容易上手的第一站。2. K-Means的核心思想什么叫“物以类聚”2.1 用一句话说清楚K-Means在干什么K-Means做的事情可以浓缩成一句话预先指定要分成K组然后找K个“中心点”让每个样本离它所属组的中心点尽可能近。“K”是用户自己给定的参数“Means”指每组中心点就是该组所有样本的均值Mean。2025年机器学习实践里K-Means依然稳居最常用的聚类算法榜首靠的就是它直观、高效、易实现。你想象一个二维平面上面散布着几百个点。K-Means做的事情就是任凭你随手画几个初始中心点然后不断迭代“每个点认领离它最近的中心点为一组组内算平均得到新的中心点”反复执行这个过程直到中心点不再移动。听起来简单但它背后是有数学原理支撑的。2.2 目标函数聚类效果的“评分标准”K-Means并不是漫无目的地乱分它在优化一个明确的目标函数[ J \sum_{i1}^{n} \min_{k} |x_i - \mu_{c(i)}|^2 ]这个公式看着吓人拆开看就很清楚把n个样本分别分配到K个簇中的某一个用(c(i))表示样本(i)所属的簇然后计算每个样本到它所属簇中心(\mu_{c(i)})的欧氏距离平方全部加起来就是(J)。(J)的名字叫组内平方和WCSSWithin-Cluster Sum of Squares也叫惯性Inertia。聚类效果好坏就看这个值大小——越小代表簇内样本越紧凑簇内相似度越高。K-Means的整个训练过程本质上就是在做一件事不断寻找让WCSS最小的簇中心位置。这是个NP难问题全局最优解理论上无法高效获得但用迭代优化的启发式方法在绝大多数实际场景中都能得到足够好的结果。2.3 算法迭代过程全拆解K-Means的完整执行流程可以用5步说清楚指定聚类数K。这个只能由人来指定算法不会自己告诉你该分几类。初始化K个中心点。可以随机选K个样本点作为初始中心也可以用更聪明的策略后面讲K-Means。分配步骤E步计算每个样本到K个中心的距离把样本分配给距离最近的簇。更新步骤M步每个簇内所有样本做算术平均将计算出的均值作为新的簇中心。重复第3、4步直到簇中心变化量小于某个阈值比如0.0001或达到最大迭代次数。第3步和“E步Expectation”、第4步和“M步Maximization”的对应关系不是巧合——K-Means其实是EM算法期望最大化算法的一个特例。簇中心是隐变量的代表分配过程计算每个点的期望归属更新过程则是最大化似然估计。理解了这层关系以后再看高斯混合模型GMM会感觉无比顺畅因为GMM就是K-Means的“概率化”升级版。一个关键细节值得注意在“分配步骤”里距离度量用的是欧氏距离的平方。很多人以为K-Means可以随便换距离度量比如换成曼哈顿距离或余弦距离但那样做的话目标函数就不是“簇内均值最小化”这个意义上的WCSS了“Means”这个更新逻辑也会失去意义。K-Means和欧氏距离是一对绑定CP换了距离就换了一个算法。3. 手写一遍K-Means从零实现才算真懂学算法有一个笨但极其有效的方法不看现成库把核心逻辑用基础库手工实现一遍。能写出代码说明你真正理解了迭代过程写不出来看再多原理讲解都是空中楼阁。3.1 核心代码实现下面是用NumPy手写的K-Means核心逻辑代码很短但信息密度很高import numpy as np def kmeans_manual(X, K, max_iters100, tol1e-4, random_state42): 手写K-Means聚类 Parameters: ----------- X : np.ndarray, shape (n_samples, n_features) 输入数据每一行是一个样本 K : int 聚类数量 max_iters : int 最大迭代次数 tol : float 中心点变化阈值小于该值视为收敛 Returns: -------- centers : np.ndarray, shape (K, n_features) 最终的簇中心 labels : np.ndarray, shape (n_samples,) 每个样本的簇标签 # 1. 初始化从样本中随机选K个点作为初始中心 rng np.random.RandomState(random_state) indices rng.choice(X.shape[0], K, replaceFalse) centers X[indices].copy() for i in range(max_iters): # 2. 分配步骤计算每个样本到K个中心的欧氏距离的平方 distances np.linalg.norm(X[:, None, :] - centers[None, :, :], axis2) ** 2 labels np.argmin(distances, axis1) # 3. 更新步骤按簇内均值更新中心 new_centers np.array([X[labels k].mean(axis0) for k in range(K)]) # 4. 检查收敛 shift np.linalg.norm(new_centers - centers) centers new_centers if shift tol: print(f第{i 1}轮迭代后收敛) break else: print(f达到最大迭代次数{max_iters}停止) return centers, labels这段代码的核心在“分配”和“更新”两步。分配那行用了NumPy的广播机制X[:, None, :]把X扩展成(n_samples, 1, n_features)centers[None, :, :]扩展成(1, K, n_features)相减后每个维度都对齐一次性算出所有样本到所有中心的距离矩阵形状是(n_samples, K)每行取最小值的索引就是该样本的簇归属。你可能注意到我用了np.linalg.norm(...) ** 2而不是直接求欧氏距离。这是个小优化我们只需要比较距离的相对大小而平方运算对大小排序没有任何影响省掉一次开根号计算在大数据集上能快不少。3.2 在模拟数据上验证效果用一个经典的三簇模拟数据来测试from sklearn.datasets import make_blobs import matplotlib.pyplot as plt # 生成3个簇的模拟数据 X, y_true make_blobs(n_samples300, centers3, cluster_std1.0, random_state42) centers, labels kmeans_manual(X, K3) # 可视化结果 plt.figure(figsize(8, 5)) plt.scatter(X[:, 0], X[:, 1], clabels, cmapviridis, s30, alpha0.7) plt.scatter(centers[:, 0], centers[:, 1], cred, markerx, s200, linewidths3) plt.title(Manual K-Means Clustering Result) plt.show()运行正常的话你会看到三个簇被正确分离红色X号标记的簇中心落在每组数据的几何重心附近。cluster_std1.0保证了三个簇分离度较好间隔明显K-Means对这种“圆形且大小均匀的簇”处理得非常好。我强烈建议你把K改成4或2试试直观感受一下“K值给错会分到什么程度”。选4时算法会把某个大簇硬生生劈成两半选2时两个相邻的簇会被强行并成一个巨大的簇——这种“强行分组”的感觉是理解K-Means局限性的第一手体验。3.3 K-Means聪明一点的初始化策略手写版里随机选K个点作为初始中心的方案存在明显缺陷。想象一个最坏情况K3但随机选中的3个点全部落在同一个大簇里。第一轮分配时其他两个簇里的点根本抢不到中心迭代可能收敛到局部最优聚出来的簇完全不符合真实结构。为了解决这个问题David Arthur和Sergei Vassilvitskii在2007年提出了K-Means初始化思路很巧妙先从样本中随机选第一个中心点。计算每个样本到已有中心的最近距离记为(D(x))。按概率正比于(D(x)^2)来抽取下一个中心点——离已有中心越远的点被选中的概率越大。重复第2、3步直到选满K个中心。一句话总结K-Means让初始中心点尽量彼此远离从源头上降低陷进局部最优的概率。实际效果提升非常明显sklearn默认就用它。写代码时几乎永远应该选K-Means而不是纯随机初始化代价只是init阶段多算几次距离但聚类质量通常会有可感知的提升这笔交易极其划算。4. sklearn中的K-Means工程实战全流程手写一遍理解了原理实际项目里就该用成熟库了。sklearn.cluster.KMeans是官方实现C语言底层优化效率远高于我的纯Python版本。但在调库之前有几个参数和行为逻辑值得仔细讲解。4.1 参数详解每个参数背后都是一次调优经验KMeans的完整参数列表很长但真正核心的只有几个from sklearn.cluster import KMeans kmeans KMeans( n_clusters3, # 聚类数K最关键的超参数 initk-means, # 初始化策略默认就是K-Means n_init10, # 用不同随机种子跑10次取WCSS最小的结果 max_iter300, # 单次运行的最大迭代次数 tol1e-4, # 中心点变化量阈值 random_state42, # 随机种子确保结果可复现 algorithmlloyd # 默认Lloyd算法就是上面讲的经典迭代 )n_init10这个参数特别值得展开由于K-Means初始中心的选择是随机的即使有了K-Means单次运行还是有概率收敛到不太好的局部最优。sklearn的策略很简单粗暴——同一个K值用不同随机种子独立跑10次每次算WCSS最后选WCSS最小的那次结果作为最终输出。这是“集成思想”在聚类里的朴素应用多花一点计算时间换来结果稳定性的显著提升。所以你在实际使用中会发现设置random_state之后只要固定了其他参数每次运行结果完全一致这就是工程上“可复现性”的体现。实验研究、报告撰写、模型审计都需要这个特性。algorithmlloyd是2023年后sklearn版本里新增的显式选项对应的是最经典的Lloyd算法。旧版本里默认的auto是在Lloyd和Elkan之间自动选择新版直接把默认值改成了Lloyd。对普通用户没什么感知差异但理解Lloyd就是上面手写的那套迭代逻辑能帮你看懂sklearn内部的运行机制。4.2 数据预处理是K-Means的生死线聚类和分类在数据预处理上有本质区别分类算法里特征量纲不同很多树模型照样跑得好好的但K-Means是距离度量算法特征尺度直接决定聚类结果走向。举个具体例子特征A取值在[0, 1]之间比如转化率特征B取值在[10000, 100000]之间比如消费金额。计算欧氏距离时特征B的贡献完全碾压特征A——两个用户哪怕转化率天差地别只要消费金额接近就可能被聚到同一组。K-Means对量纲极其敏感使用前必须做标准化StandardScaler或归一化MinMaxScaler。这不是一个可选项而是必选项。实操代码from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans from sklearn.pipeline import make_pipeline # 正确的做法把标准化和K-Means放进同一个Pipeline pipeline make_pipeline( StandardScaler(), KMeans(n_clusters3, n_init10, random_state42) ) X_train_scaled pipeline.fit_transform(X_train) labels pipeline[-1].labels_用Pipeline的好处是如果后面需要做交叉验证或者应用在新数据上标准化参数不会泄漏处理逻辑始终一致。4.3 怎么确定K值肘部法则和轮廓系数K-Means最大的使用难点不是算法本身而是回答“到底分几类”这个问题。两种最常见的方法论各有适用场景。方法一肘部法则The Elbow Method原理不难理解随着K增大每个簇内的样本量变少WCSS惯性必然下降。K从1增加到2时WCSS下降最多因为把一个混杂的大类拆成两个相对纯粹的簇回报极高。继续增加KWCSS下降幅度逐渐变小。把K作为横轴、WCSS作为纵轴画折线图曲线会有一个明显的“拐点”形状像胳膊肘这个拐点对应的K就是最合理的选择。import matplotlib.pyplot as plt wcss [] K_range range(1, 11) for k in K_range: kmeans KMeans(n_clustersk, n_init10, random_state42) kmeans.fit(X_scaled) wcss.append(kmeans.inertia_) plt.plot(K_range, wcss, o-) plt.xlabel(Number of clusters (K)) plt.ylabel(WCSS / Inertia) plt.title(Elbow Method) plt.show()生成模拟数据时centers3画出来的折线会在K3处出现明显拐点K大于3之后曲线快速变平。这个方法的局限在于真实数据往往没有特别清楚的拐点曲线平滑得像一条抛物线看半天也不知道该选哪。这种情况就换轮廓系数。方法二轮廓系数Silhouette Coefficient轮廓系数不只看簇内紧凑度还同时评估簇间分离度。对每一个样本(i)定义[ s(i) \frac{b(i) - a(i)}{\max{a(i), b(i)}} ](a(i))样本(i)与同簇其他样本的平均距离越小越好(b(i))样本(i)与最近的其他簇所有样本的平均距离越大越好。(s(i))的取值范围在[-1, 1]之间越接近1说明样本离自己簇近、离其他簇远聚类效果越好。把所有样本的(s(i))取平均就得到整体轮廓系数。sklearn直接给了现成函数from sklearn.metrics import silhouette_score silhouette_scores [] for k in range(2, 11): # 注意轮廓系数至少需要K2 kmeans KMeans(n_clustersk, n_init10, random_state42) labels kmeans.fit_predict(X_scaled) score silhouette_score(X_scaled, labels) silhouette_scores.append(score) best_k silhouette_scores.index(max(silhouette_scores)) 2 print(f最优K值为: {best_k})轮廓系数比肘部法则更自动化但也不能盲信。它倾向选择“簇形圆且大小均匀”的划分结果如果数据有严重的类别不平衡轮廓系数给出的K不一定符合业务需求。我自己的经验是**先跑轮廓系数圈定一个K的范围再用肘部法则在这个范围内找拐点最后结合业务解读确定最终K值。**算法告诉你的永远只是“统计上的建议”最终选多少类必须回归业务问题——用户分群分得太细运营成本高分得太粗没有差异性里头的权衡只有业务方知道。4.4 完整实操案例用户行为分群把上面的知识点拼起来看一个完整的实操案例。假设我们拿到了3000个用户的两个行为特征平均登录时长分钟/天和月消费金额元/月。import pandas as pd import numpy as np from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans # 模拟数据实际项目中这里读入真实业务数据 rng np.random.RandomState(42) n 3000 data pd.DataFrame({ avg_minutes: np.concatenate([ rng.normal(10, 3, n // 3), # 轻量用户 rng.normal(30, 5, n // 3), # 中度用户 rng.normal(60, 8, n // 3) # 重度用户 ]), monthly_spend: np.concatenate([ rng.normal(50, 20, n // 3), rng.normal(300, 80, n // 3), rng.normal(1500, 300, n // 3) ]) }) # 1. 标准化关键 scaler StandardScaler() X_scaled scaler.fit_transform(data) # 2. 训练K-Means kmeans KMeans(n_clusters3, initk-means, n_init10, random_state42) labels kmeans.fit_predict(X_scaled) # 3. 把标签加回原始数据 data[cluster] labels # 4. 查看每个簇的特征均值判断业务含义 cluster_summary data.groupby(cluster).mean() print(cluster_summary)第4步很有讲究。聚类完成后聚类标签本身没有意义必须回到原始特征空间看每个簇的特征均值才能给每个簇赋予业务含义。假设输出结果类似这样簇平均登录时长分钟月消费金额元解释0组11.248.5轻度用户低价值很少有活跃度1组29.8310.2中度活跃中等消费可做提升转化2组61.51520.4重度用户高价值需要重点维护到这一步聚类才真正产生业务价值。三个群体对应完全不同的运营策略高价值用户做VIP服务、推送会员权益中度用户做定向优惠刺激复购低活跃用户做唤醒拉活。K-Means在这里不是用来做预测的而是用来“发现数据的组织结构”。5. 聚类家族大团圆K-Means和它的亲戚们热词里除了K-Means还有层次聚类、DBSCAN、谱聚类等正好借这一节把整个聚类算法家族梳理一遍。理解不同算法的适用场景比多记几个公式重要得多。5.1 原型聚类K-Means的直系家族K-Means属于“原型聚类”家族这个家族的共同特征是每个簇用一个“原型点”Prototype来代表。K-Means的原型是簇内均值K-Medoids改用簇内最中心的实际样本点作为原型提高了对噪声和异常值的鲁棒性但计算代价更高。原型聚类的优点是快、好懂、适合大规模数据缺点是只能处理“圆形簇”对非凸形状的数据分布无能为力。5.2 层次聚类用树状图表达聚类的层级关系层次聚类Hierarchical Clustering走了另一条路线。它不直接对样本做硬切分而是构建一棵“聚类树”树状图Dendrogram。最常见的算法是自底向上的凝聚式层次聚类AGNES一开始每个样本自成一簇每次合并两个距离最近的簇重复合并直到所有样本归入一个簇或者达到预设的簇数量。“两个簇之间的距离”有不同度量方式单链接两个簇间最近样本距离、全链接最远样本距离、平均链接所有样本对平均距离、Ward距离合并后组内方差增量。其中Ward方法在目标函数上和K-Means最接近因此在实际应用中最常用。层次聚类的优势是不需要预先指定K值你可以在树状图上找合适的位置“切一刀”层次关系本身也有信息量——比如商品分类的层层细分天然就是层级结构。缺点是计算量大时间复杂度至少(O(n^2))几百条数据没问题几十万条数据就扛不住了。5.3 密度聚类DBSCAN如何解决“非球形”痛点DBSCAN的全称是Density-Based Spatial Clustering of Applications with Noise名字很长核心思想却很直白把样本密集的区域划成一个簇中间稀薄的地方就是边界。它有两个关键参数(eps)邻域半径和(min_samples)一个核心点邻域内的最少样本数。如果一个点周围(eps)半径内的样本数量超过(min_samples)这个点就是“核心点”核心点之间通过密度可达关系连成一片形成簇不满足核心点条件但落在核心点邻域内的叫做边界点完全孤立的点直接标记为噪声。DBSCAN最大的优势有三点不需要指定K值簇的数量由数据密度自动决定可以发现任意形状的簇环形、弯月形、S形都能处理自带噪声识别离群点自动分离出来。缺点也很明显两个参数(eps)和(min_samples)对结果极其敏感不同数据集需要反复调试当数据密度差异很大时一个(eps)值很难同时照顾到稠密区域和稀疏区域在高维空间里距离的区分度急剧下降维度灾难DBSCAN表现会大打折扣。5.4 谱聚类和真实选型建议谱聚类Spectral Clustering的思路则完全不同它先把样本看作图的节点用相似度矩阵表示节点间边的权重然后对图的拉普拉斯矩阵做特征分解用特征向量做降维最后在低维空间里跑K-Means。谱聚类的亮点在于通过图的视角能处理很多K-Means完全搞不定的形状甚至能处理“同轴圆环”这种极具挑战性的分布。代价是计算复杂度更高需要构造(n \times n)的相似度矩阵同样不适合海量数据。拿热词里提到的“适用于谱聚类的K-Means实现”来说不少教材习题会要求你实现谱聚类的“最后一步”——把特征向量喂给K-Means。理解这一层的前提是先理解谱聚类本身而理解谱聚类的前提是先扎实掌握K-Means这就是我把K-Means放在这一章的原因它既是聚类家族的基石也是通往更复杂算法的桥梁。我给自己总结了一份选型速查表数据特点推荐算法原因数据量大、簇近似圆形、低维K-Means速度快、效果好、调参简单簇形状不规则、非凸DBSCAN密度驱动不需要预设形状数据量小、需要层级关系层次聚类树状图有额外解释价值数据量中等、形状极其复杂谱聚类图论方法灵活性强数据中含有大量噪声DBSCAN自带噪声标记功能所有特征量纲一致、无异常值K-Means简单直接可解释性好6. 避坑指南K-Means实战中的五大常见错误这一节全是实战踩过的坑的总结每一个都有具体的场景说明和替换方案。6.1 犯了错误数据没标准化就直接聚类现象消费金额和登录次数同时进入特征聚类结果几乎完全由金额变量决定。修复训练前对每个特征做标准化或归一化消除量纲影响。特别是特征取值范围差异超过一个数量级时这一步绝对不能跳。判断技巧聚类完成后查看每个簇的特征均值如果某个特征在各簇间差异极大而其他特征几乎没区分度大概率就是量纲问题。6.2 犯了错误K值拍脑袋决定现象业务方拍脑袋说“我们要把用户分成4类”K-Means就设4结果两个簇高度重叠另外两个簇边界模糊怎么看都不合理。修复先用轮廓系数扫一遍2到10的K值范围再结合肘部法则和业务可解释性综合判断。如果业务方坚持要某个K值也可以先按算法建议跑一遍再让业务方判断算法结果能否满足业务需求。6.3 犯了错误特征里混入相关性极高的冗余变量现象特征里有“用户年龄”和“出生年份”两者高度负相关等效于把年龄特征在距离计算中的权重翻倍。修复聚类前先用相关矩阵检查冗余特征保留其中一个即可。更严格的做法是先做PCA降维再去掉多重共线性特征再聚类。6.4 犯了错误忽略异常值现象某个用户月消费金额是平均值的100倍这个点把簇中心拉得很远导致周围的正常用户被错误分配到其他簇。修复K-Means对异常值极度敏感因为目标函数用的是平方距离异常值的影响被平方放大。聚类前做异常值检测比如3倍标准差法则、IQR法把明显异常的点剔除或单独处理。6.5 忘了固定随机种子现象同一份代码、同样的参数、同一份数据前后跑两次聚类结果完全不同下游分析全部作废。修复训练K-Means时必须固定random_state同时设置n_init10保证重复运行的结果稳定。这个习惯能避免大量不必要的困惑。7. 真实项目中的一个常规陷阱聚类结果的可解释性聚类完成以后最重要的步骤不是画图不是算指标而是“读懂每个簇”。我见过很多同学跑完K-Means拿着labels_就开始做可视化看到不同颜色的散点图欢呼“聚类成功”然后就没有然后了。真正的工程实践里聚类只是开始。你需要回到原始业务数据统计每个簇在关键指标上的分布差异给每个簇起一个业务上说得通的名字这个聚类才算有效。实际的用户分群操作里我一般会做三件套分析。第一是簇内特征画像统计每个簇在所有特征上的均值、中位数、分布区间判断各簇的业务定性第二是交叉验证换一个不同特征的子集重新聚类看同一批用户是否还保持相同的大类归属分组稳定性检验第三是业务落地跟踪给每个簇起一个业务外号比如“重度夜猫族”“高潜犹豫族”持续跟踪这些群体的行为演变让聚类结果成为可迭代的业务资产。K-Means的终极价值不是“把数据分开了”而是“分开之后帮你理解了数据的结构”。拿到结果后多问自己一句这个簇和其他簇的本质差异是什么这个差异在业务上有没有意义这样才算是真正把无监督学习用对了地方。总结一句我个人的实操心得K-Means是机器学习所有算法里“性价比”最高的一课学会它你不仅掌握了一个高频使用的聚类工具还会顺带理解EM算法的雏形、K-Means的工程思想、数据预处理的重要性——这一连串知识链条比K-Means本身更值钱。