ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

KMeans聚类算法与Python代码实践:从原理到sklearn调参避坑指南

KMeans聚类算法与Python代码实践:从原理到sklearn调参避坑指南 简介面向机器学习初学者与数据分析人员的KMeans聚类算法Python实现与配套说明文档解决无标签数据下自然分群的核心问题。内容覆盖算法原理、初始化与迭代更新流程、sklearn接口调用、数据标准化处理及可视化展示并梳理了优缺点、适用场景用户分群、图像分割、文本聚类和DBSCAN、谱聚类等扩展方向。资源共3个文件包含可直接运行的.py脚本和两份TXT说明文档压缩包仅355KB轻量精简便于对照学习。脚本示例清晰说明文档能帮助快速理解每一步骤适合希望系统掌握聚类分析并参考完整代码的开发者。目前已有4902人学习下载是一份高效入门无监督学习的实用资料。1. KMeans聚类算法代码为什么它依然是入门首选说到KMeans聚类算法很多人的第一反应是课本里的老古董。但现实是做用户分群、数据预处理、异常检测时KMeans往往是我跑的第一个模型——它快、可解释、代码量还小。这篇笔记不背教材而是顺着“KMeans聚类算法代码”这条线把一个能投入使用的方案拆开从手写实现讲到sklearn调参从K值选择讲到避坑清单最后告诉你怎么证明聚类结果不是在自嗨。适合刚学聚类、或者已经在用但总觉得结果“差口气”的从业者。注意这里不讨论DBSCAN、层次聚类这些替代品先把KMeans用到极致你自然知道什么时候该换。2. KMeans的原理与数学直觉先搞懂它在算什么2.1 从K个中心点到SSE目标函数怎么定义KMeans的输入就两个一个样本矩阵X以及你想把数据分成几簇的K值。输出是每个样本的簇标签label以及K个簇中心centroid。它做的事情很朴素让每个样本离自己簇中心的距离尽可能近。严格说KMeans在最小化这样一个损失函数[ SSE \sum_{i1}^{N} \sum_{k1}^{K} r_{ik} | x_i - \mu_k |^2 ]其中(r_{ik})表示第i个样本是否属于第k簇属于为1否则为0(\mu_k)是第k簇的中心。这个函数叫簇内平方和Sum of Squared Errors也叫惯性inertia。KMeans就是找一组簇中心(\mu)和样本分配(r)让SSE最小。注意这里的“距离”默认是欧氏距离。欧氏距离对量纲极其敏感特征A的范围是0到1特征B的范围是0到10000那距离基本由特征B说了算。这就是后面要说的标准化问题的根源。那么SSE最小化怎么求如果你固定簇中心每个样本分给哪个簇是确定的——离哪个中心近就归哪个。如果你固定样本分配簇中心的最优解就是该簇所有样本的均值。这两个条件互相依赖没法一次算出来只能迭代。2.2 Lloyd算法迭代步骤初始化-分配-更新KMeans的标准求解算法叫Lloyd算法流程只有四步选K个初始中心。分配对每个样本计算它到K个中心的距离把它归到最近的中心。更新对每一簇重新计算所有样本的均值作为新中心。重复第2、3步直到中心不再变化或达到最大迭代次数。我用一个一维数据的例子带你把迭代走一遍。假设样本是[1, 2, 10, 11]K2。第一步随机选初始中心。假设选了1和2这俩样本点本身作中心。初始中心c11c22。第一轮分配1到c1距离0到c2距离1归c1。2到c1距离1到c2距离0归c2。10到c1距离9到c2距离8归c2。11到c1距离10到c2距离9归c2。第一轮更新c1新值 1c2新值 (21011)/3 7.67第二轮分配1到c1距离0到c2距离6.67归c1。2到c1距离1到c2距离5.67归c1。10到c1距离9到c2距离2.33归c2。11到c1距离10到c2距离3.33归c2。第二轮更新c1新值 (12)/2 1.5c2新值 (1011)/2 10.5第三轮分配1、2归c110、11归c2。中心更新后还是1.5和10.5于是收敛。你看最终结果把数据分成了[1,2]和[10,11]很合理。但如果你初始中心选的是1和10呢第一轮分配后2会归到1距离111归到10距离1更新后中心变成1.5和10.5结果还是一样的。但如果你选初始中心为1和11第一轮分配10会归11距离12归1更新后中心1.5和10.5结果也一样。这个例子比较幸运。后面你会看到初始中心选得不好可能陷在局部最优分出一堆重叠的团。这个迭代过程的时间复杂度大约是O(迭代次数 × 样本数 × 特征数 × K)。样本量和特征维数增长时代价直线上升所以后面用numpy向量化是必要的。2.3 为什么KMeans对初始中心敏感EM视角简述把Lloyd算法放到更大背景下看它其实是EM算法的硬版本。E步Expectation对应“分配样本到最近中心”M步Maximization对应“重新计算中心”。因为KMeans的分配是硬分配一个样本只能属于一个簇所以它比高斯混合模型GMM的软分配更简单粗暴。EM算法有一个特点它只能保证收敛到目标函数的局部最小值不能保证全局最优。也就是说不同的初始中心最终可能停在不同的局部最优解上。比如有两个簇靠得很近初始中心选在一个模糊地带最后可能把一簇劈成两半另一簇吞并两个簇的一部分。这也是为什么sklearn的KMeans默认使用K-Means初始化它让初始中心尽量彼此远离大幅降低落到糟糕局部最优的概率。K-Means的步骤是随机选第一个中心。对每个样本计算它到最近已有中心的距离d(x)。以概率正比于d(x)² 选取下一个中心。重复直到选满K个中心。这个策略让初始中心均匀覆盖数据空间后来被证明在理论和实践上都远好于纯随机选点。你在调参时init参数就是控制这个的。理解KMeans的EM本质还有一个用处当你的数据不是球形分布、簇大小差异巨大时KMeans天然会有偏向。它不是算法bug而是目标函数决定的。提前知道这个边界比事后怀疑人生更重要。3. 用Python写KMeans从零实现到sklearn3.1 最小可用实现纯Python列表的50行代码很多教材直接甩sklearn但遇到“不能用第三方库”的面试题或者要魔改距离公式时自己写一遍能救命。下面是纯Python实现没有numpy便于逐行理解。def kmeans(X, k, max_iter100): # X: list of list每一行是一个样本 # k: 簇数 # max_iter: 最大迭代次数 centers X[:k] # 前k个样本当初始中心这是最简陋的初始化 n_samples len(X) n_features len(X[0]) for _ in range(max_iter): # 分配每个样本归到最近中心 labels [] for x in X: distances [] for c in centers: # 计算欧氏距离不开根号也不影响比较 dist sum((x[i] - c[i]) ** 2 for i in range(n_features)) distances.append(dist) labels.append(distances.index(min(distances))) # 更新重新计算每簇均值 new_centers [] for k_idx in range(k): # 取出这一簇的所有样本 cluster_points [X[i] for i in range(n_samples) if labels[i] k_idx] if cluster_points: # 逐特征求平均值 new_centers.append([sum(p[f] for p in cluster_points) / len(cluster_points) for f in range(n_features)]) else: # 空簇保留旧中心避免崩溃 new_centers.append(centers[k_idx]) # 检查是否收敛新旧中心是否完全一致 if new_centers centers: break centers new_centers return centers, labels逻辑说明每轮迭代先做分配再更新中心。分配阶段不开平方根因为比较距离大小不需要开根号平方和的大小关系不变。更新阶段如果某个簇没有样本空簇保留旧中心否则直接崩溃。收敛条件是新旧中心完全相等用浮点数比较可能不严格但在小数据上够用。参数说明max_iter限制最大迭代次数防止不收敛时死循环。初始中心直接取了前k个样本这在真实数据上非常糟糕只允许在演示场景使用。实际手写代码时至少要用随机抽样替代centers random.sample(X, k)。这段代码的时间复杂度是O(max_iter × k × n_samples × n_features)所以样本一多就慢。但它给了你一个完全透明的基线后面所有加速和调参都建立在理解它的基础上。3.2 用numpy向量化提速和代码规范纯Python版跑10000个样本就明显卡顿因为内层循环全是Python解释器开销。用numpy把距离计算和均值计算交给C级别代码性能提升几十倍。import numpy as np def kmeans_np(X, k, max_iter100, tol1e-4): # X: numpy数组shape (n_samples, n_features) # k: 簇数 # max_iter: 最大迭代次数 # tol: 中心变化阈值小于它就算收敛 n_samples, n_features X.shape # 随机采样k个样本作为初始中心 rng np.random.default_rng(0) idx rng.choice(n_samples, k, replaceFalse) centers X[idx] for i in range(max_iter): # 分配利用广播计算每个样本到每个中心的距离 # X[:, None, :] 形状 (n_samples, 1, n_features) # centers[None, :, :] 形状 (1, k, n_features) distances np.sum((X[:, None, :] - centers[None, :, :]) ** 2, axis2) labels np.argmin(distances, axis1) # 更新用花式索引聚合 new_centers np.zeros_like(centers) for k_idx in range(k): cluster_points X[labels k_idx] if len(cluster_points) 0: new_centers[k_idx] cluster_points.mean(axis0) else: # 空簇处理沿用旧中心 new_centers[k_idx] centers[k_idx] # 计算中心偏移量 shift np.linalg.norm(new_centers - centers) centers new_centers if shift tol: break return centers, labels逻辑说明核心加速点是距离矩阵计算。X[:, None, :]把X扩展成三维数组centers[None, :, :]也扩展成三维两者相减时numpy广播成(n_samples, k, n_features)的差值矩阵再平方求和得到每个样本到每个中心的距离矩阵。argmin在第二维找最小距离的下标一次得到所有样本的标签。更新阶段用布尔掩码labels k_idx选样本mean(axis0)求中心。参数说明tol是收敛阈值用中心位置变化的L2范数判断。默认1e-4比纯Python版更精细。random seed固定为0保证每次运行结果一致。空簇处理依旧保留这在实际数据中并不罕见特别是k设太大时。这段代码还有优化空间比如用np.add.at避免循环更新中心但在k不大时循环性能损失可忽略。优先保证可读性。3.3 调用sklearn的KMeans真实项目里怎么用自己手写是为了理解真实项目用sklearn就够了。接口稳定、Cython优化、参数齐全。from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler # X是原始数据numpy数组或者pandas DataFrame都行 X_scaled StandardScaler().fit_transform(X) # 指定聚类数为5 km KMeans(n_clusters5, initk-means, n_init10, max_iter300, tol1e-4, random_state42) km.fit(X_scaled) # 获取标签和中心 labels km.labels_ centers km.cluster_centers_ # 获取SSE惯性 sse km.inertia_逻辑说明fit之前先做标准化这是KMeans能不能用的前提。sklearn的KMeans默认initk-meansn_init10表示它用10组不同的初始中心跑10次挑SSE最小的结果。n_init越大越稳但耗时线性增长。inertia_属性直接给出SSE用来做手肘法很方便。参数说明random_state42固定随机种子让实验可复现。max_iter300是单次运行的迭代上限tol1e-4是收敛阈值。sklearn的fit方法会自动处理空簇但会打印警告。你还可以设置n_jobs并行加速多次初始化但样本量不大时没必要。4. KMeans必调的4个参数K值、初始化、收敛判断与随机种子4.1 手肘法和轮廓系数怎么选KK值是最关键的参数也是最难拍的。没有监督信号告诉你“真实验类别数”只能靠启发式方法。手肘法看SSE随K变化的曲线。K越大每个簇内越紧凑SSE必然下降。你找那个“下降变缓的拐点”就像胳膊肘。代码如下import matplotlib.pyplot as plt from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler X_scaled StandardScaler().fit_transform(X) sse_list [] K_range range(2, 11) for k in K_range: km KMeans(n_clustersk, n_init10, random_state42) km.fit(X_scaled) sse_list.append(km.inertia_) plt.plot(K_range, sse_list, markero) plt.xlabel(K) plt.ylabel(SSE) plt.title(Elbow Method) plt.show()逻辑说明这段代码跑9次KMeans记录每个K的SSE。手肘法的判断是主观的如果曲线没有明显拐点说明数据根本不适合KMeans——簇重叠严重或没有簇结构。轮廓系数则不需要画图就能给出数值。它衡量每个样本和自身簇的相似度以及和最近其他簇的差异度范围-1到1越大越好。from sklearn.metrics import silhouette_score best_k -1 best_score -1 for k in range(2, 11): km KMeans(n_clustersk, n_init10, random_state42) labels km.fit_predict(X_scaled) score silhouette_score(X_scaled, labels) print(fK{k}, silhouette{score:.4f}) if score best_score: best_score score best_k k print(fBest K: {best_k})参数说明轮廓系数的计算代价是O(n²)样本超过5万会明显变慢。这时可以改用抽样或直接用SSE手肘法。此外轮廓系数对凸簇有效如果你的簇形状奇怪它可能会误判。所以建议手肘法定范围轮廓系数在范围内精挑两者结合。4.2 init与n_initK-Means为什么能救你init参数决定初始中心怎么选。random是纯随机选样本点k-means是前面讲的改进策略。sklearn默认用k-means这是有原因的。n_init控制用几组初始中心跑KMeans最终返回SSE最小的一组。这个参数是我的后悔药——早期我为了省时间把n_init设为1结果某些K值下聚类结果一会儿一个样。后来调成n_init10世界清静了。一个实用建议如果数据量大先把n_init从默认10降到5跑一版看看效果确认K值之后再用n_init10做最终模型。如果数据量不大直接保持10别在这上面省。4.3 max_iter和tol收敛判断与计算开销的平衡max_iter是每次运行的最大迭代轮数tol是中心变化阈值。sklearn默认max_iter300、tol1e-4。这两个值的组合决定算法何时“见好就收”。见过一个翻车案例有人为了“提高精度”把tol设为1e-8结果KMeans跑了上万轮还没收敛因为浮点误差让中心微调永远达不到这个值。tol设得过小没有意义数据本身有噪声精细到1e-8已经不是聚类该做的事。max_iter设太小的后果是算法没收敛就停了。判断方法很简单查看km.n_iter_属性如果它等于max_iter说明你撞上了上限。此时应该加大max_iter而不是假装结果没问题。km KMeans(n_clusters5, max_iter50, tol1e-4, random_state42) km.fit(X_scaled) print(实际迭代次数:, km.n_iter_) if km.n_iter_ 50: print(警告达到最大迭代上限没有完全收敛)参数说明这个警告很有用。默认300对大多数数据都够但如果你的数据有1万个样本且特征维度很高300次迭代也可能不够。建议始终关注这个输出。4.4 random_state可复现实验的后悔药random_state控制KMeans内部所有随机行为初始中心采样、k-means的随机选择、空簇处理时是否有补充样本。不设random_state的话你每次跑结果都可能不同做实验对比时看不到真实差异。我习惯固定random_state42并在代码注释里写明。这能让你在调参时判断改动是否真的有效而不是随机种子造成的假象。另外一个俗技巧当你觉得当前聚类结果“碰巧好”时把random_state换几个值跑一遍如果结果差异很大说明你的K值或数据本身不稳定别急着用这个好结果。5. KMeans常见问题与避坑跑出结果不代表跑对5.1 数据没做标准化量纲大的特征主导聚类现象聚类结果几乎只由某个量纲大的特征决定。比如用户数据里有“年龄”20-60和“收入”3万-100万KMeans分出来的簇基本是低收入、中收入、高收入年龄完全不起作用。原因欧氏距离计算时收入列的取值范围大平方后贡献了绝大部分距离值。目标的SSE也是按欧氏距离算的KMeans自然优先去拟合收入这个特征。解决聚类前对所有特征做标准化。常见做法是z-score归一化。from sklearn.preprocessing import StandardScaler, MinMaxScaler # 方案一z-score推荐默认 X_scaled StandardScaler().fit_transform(X) # 方案二min-max到[0,1]适合稀疏数据或需要保留0的语义 X_scaled MinMaxScaler().fit_transform(X)逻辑说明z-score让每个特征均值为0、方差为1它们的量纲影响被消除。min-max适合特征本身是稀疏的或者你不想让0值被偏移的情况。注意聚类和后续验证都要用同一份标准化后的数据不要在聚类前用未标准化数据聚类后却在标准化数据上算轮廓系数。5.2 离散点太多KMeans被迫把噪声当一类现象聚类结果里有一个簇只有一两个样本离其他簇很远。这个孤立簇拉低了整体SSE吗不一定它可能只是一个噪声点。原因KMeans会把每个点都分配到某个簇它没有“拒绝”机制。如果数据里混入几个远离主体的离群点它们会自成一簇不仅占用K还会让邻近样本的簇中心被拉偏。解决在聚类前预先剔除离群点。常见做法是基于密度或者距离的简单过滤。from sklearn.neighbors import LocalOutlierFactor # LOF算法识别离群点 lof LocalOutlierFactor(n_neighbors20, contamination0.05) outlier_mask lof.fit_predict(X_scaled) -1 X_clean X_scaled[~outlier_mask] print(f原始样本数: {len(X_scaled)}剔除离群点后: {len(X_clean)})逻辑说明LocalOutlierFactor同时用局部密度和可达距离判断离群点比一维的z-score阈值更适用多特征场景。contamination0.05表示认为5%的数据是离群点这个比例要根据业务调整不是固定值。剔除后再做KMeans你会看到簇的轮廓系数明显上升。另一种思路是调低K值让离群点被归入最近的大簇里但这会污染簇中心。所以预先清洗是更干净的方案。5.3 类别不平衡时KMeans会把大类劈开现象数据里两个真实类别样本量比例是10:1但KMeans聚类结果把大类分成三块小类完整保留。看着簇数够了实际并没有反映真实结构。原因KMeans的目标是最小化SSE它对簇的大小没有约束。当某个簇样本多、分布范围广把它劈成两半并各自设中心SSE下降比保留一个小簇多得多。算法认为“劈大簇”更划算。解决调整聚类目标或者改用更适合不均衡数据的聚类算法。如果坚持用KMeans可以增大K值让大类被劈得更细再用层次聚类合并过细的块。但更省事的办法是换用DBSCAN或者GMM。from sklearn.mixture import GaussianMixture # GMM用概率分配样本能更好地处理重叠和不均衡的簇 gmm GaussianMixture(n_components5, random_state42) gmm_labels gmm.fit_predict(X_scaled)逻辑说明GMM的软分配让它对样本量差异更宽容因为每个簇有自己的协方差结构不会被“距离中心最近”这个单一规则支配。如果你的业务场景天然存在不均衡比如正常用户远多于异常用户那么KMeans劈分异常现象会频繁发生尽早换GMM或DBSCAN更稳妥。5.4 高维稀疏数据的距离失效问题现象特征是几百维很多维度取值为0比如文本TF-IDF向量。KMeans聚类结果不稳定轮廓系数很低。原因高维空间中欧氏距离退化了几乎所有点到中心点的距离都差不多。这是“维度灾难”的直接后果。KMeans依赖距离进行分配距离分辨力下降结果就变成随机分团。解决先降维再聚类或者更换距离度量。对文本数据常用方法from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.decomposition import TruncatedSVD from sklearn.cluster import KMeans # 原始文本向量化后一般几千维 tfidf TfidfVectorizer(max_features5000) X_tfidf tfidf.fit_transform(documents) # 用SVD降到50维 svd TruncatedSVD(n_components50, random_state42) X_reduced svd.fit_transform(X_tfidf) # 再用KMeans km KMeans(n_clusters8, random_state42) km.fit(X_reduced)逻辑说明TruncatedSVD适合稀疏矩阵降到50维后距离计算更有效。这个方案的原理是保留主要方差同时压缩噪声维度。降维的维数没有黄金标准常见做法是试25、50、100看轮廓系数选最佳。6. 让聚类结果真正可用轮廓系数之外的三个实操检查轮廓系数只能说明“簇内紧凑、簇间分离”但它不知道你的业务是否买账。我习惯在出结果后做这样三件事第一画簇分布图。如果数据能降到2维用PCA或t-SNE降维散点图看每个簇的边界是否重叠。簇边界大面积重叠说明KMeans强行分出了本不存在的结构。第二检查每个簇的样本量。正常KMeans的簇应该大小均匀如果出现某个簇只有1个点或某个簇占了80%的样本回去看前面说的离群点和类别不平衡问题。第三用业务指标验证。比如做用户分群每个簇的转化率、客单价是否呈现明显差异如果分出来的簇在业务指标上没有区分度那这个聚类结果再数学上再漂亮也是废的。留下这个习惯能帮你过滤掉很多“看起来能用”的假聚类。最后说个我的习惯每次跑KMeans时顺手把n_init、random_state、tol写进代码注释然后固定下来。改一个参数就重跑一次对比而不是同时改三个。这让我少踩了很多“调参调了个寂寞”的坑。KMeans虽然简单但每一步都在考验你对数据的理解。希望帮到你。本文还有配套的精品资源点击获取
返回列表