简介:KMeans聚类算法是无监督学习中的经典方法,这份资源面向机器学习初学者与数据分析人员,结合Python与scikit-learn完整演示了从数据加载、标准化、模型训练、预测到可视化的流程,并讨论初始质心选择、K值设定等关键问题。压缩包为rar格式,共3个文件,含2个txt说明文档与1个Python脚本,整体仅355KB;txt文档对算法步骤、数据预处理、优缺点及适用场景进行了系统梳理,py脚本注释清晰,可直接运行观察聚类边界与质心位置。已有4902人学习下载,特别适合用户分群、图像分割、文本聚类等入门实践,读者不仅能掌握KMeans的核心原理,还能获得一份可复用的聚类分析模板,为后续学习DBSCAN、谱聚类等扩展算法打下基础。
1. 拿到 KMeans 聚类算法资源后,先搞明白它到底能帮你解决什么问题
第一次接触聚类算法的人,多半是手里有一份没标签的数据,老板却急着要一个“客户分组”。KMeans 通常是被第一个翻出来的算法:原理简单、几行 sklearn 就能出图,但它也是最容易被误用的算法之一。这份资源里的 KMeans.py 是一个可以直接改路径运行的最小实现,覆盖了数据读取、标准化、训练、可视化全流程;ReadMe 里则写了运行环境和关键参数的说明。读完这篇,你不仅能看懂代码每个函数在干什么,还能知道 K 值怎么选、为什么标准化不能省、同一条数据为什么换种子结果会变。适合刚接触机器学习的从业者,也适合做用户分群、图像分割或文本聚类时不想只会跑 demo 的人。
2. KMeans 原理与 K 值选择:先说清楚每个参数在算法里到底管什么
KMeans 的核心原理其实很简单:把 N 个没有标签的样本点分成 K 组,让每个样本点离它所属簇的中心尽可能近,同时让不同簇的中心尽可能互相远离。形式上,算法在最小化簇内平方和,也就是inertia,它等于每个样本到所属质心欧氏距离的平方之和。你后续在 sklearn 里看到的kmeans.inertia_,就是这个值。理解这一点很重要,因为后面选 K、评估聚类效果、判断模型有没有收敛,看的都是它。
2.1 四个步骤与 sklearn 参数的映射关系
KMeans 的迭代过程可以拆成四步:初始化质心、分配样本、更新质心、判断是否收敛。为了让你不被 sklearn 的黑匣子挡住,我建议先看一段手写实现,哪怕你最后不会在项目里用它。它能把算法本质暴露得很清楚。
import numpy as np def simple_kmeans(X, k, max_iter=100, seed=42): # 1. 初始化:随机挑选 k 个样本作为初始质心 rng = np.random.default_rng(seed) centers = X[rng.choice(len(X), size=k, replace=False)] for i in range(max_iter): # 2. 分配:计算每个样本到各质心的距离,取最近的那个簇 dist = np.linalg.norm(X[:, None, :] - centers[None, :, :], axis=2) labels = dist.argmin(axis=1) # 3. 更新:每个簇内所有样本的均值,成为新质心 new_centers = np.array([X[labels == j].mean(axis=0) for j in range(k)]) # 4. 判断:质心变化很小,算法提前停止 if np.allclose(centers, new_centers, atol=1e-4): break centers = new_centers return labels, centers这段代码里,X[:, None, :] - centers[None, :, :]是在用广播机制把每个样本和每个质心做差,结果形状是(样本数, k, 特征数),再做np.linalg.norm(..., axis=2)得到每个样本到 k 个质心的距离矩阵。argmin(axis=1)取距离最小的簇编号。更新步骤里X[labels == j].mean(axis=0)是求第 j 个簇内所有样本的均值向量。
如果你已经跑过 sklearn 的 KMeans,会发现核心差别在初始化:手写版是纯随机选点,而 sklearn 默认使用k-means++初始化策略,并且通过n_init参数多次尝试、取惯性最小的一次,大幅降低了“随机初始点太烂”带来的局部最优风险。所以实际项目里我不会用手写版,但理解了这段循环,你再看fit、predict、cluster_centers_、inertia_这些接口时,就知道它们内部到底在算什么。
2.2 手肘法与轮廓系数:K 值不是玄学,但也不只是数学
KMeans 有一个让人头疼的前提:必须先告诉算法“我要分成几簇”。这里的 K 不是拍脑袋定的,常见做法是在一个 K 值范围内分别训练模型,观察两个指标:惯性inertia和轮廓系数silhouette_score。惯性会随着 K 增大不断下降,因为簇越多,样本离质心越近;真正有用的是“下降速度突变”的那个拐点,也就是所谓手肘。
from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score inertia_list = [] silhouette_list = [] K_range = range(2, 11) for k in K_range: km = KMeans(n_clusters=k, n_init=10, random_state=42) labels = km.fit_predict(X_scaled) inertia_list.append(km.inertia_) silhouette_list.append(silhouette_score(X_scaled, labels)) # 手肘图:横轴 K,纵轴 inertia,找下降变缓的位置 plt.plot(K_range, inertia_list, marker='o') plt.xlabel('K') plt.ylabel('inertia') plt.show() # 轮廓系数图:横轴 K,纵轴 silhouette_score,选峰值附近的 K plt.plot(K_range, silhouette_list, marker='o') plt.xlabel('K') plt.ylabel('silhouette_score') plt.show()这里X_scaled是标准化之后的数据,后面会专门讲。轮廓系数范围是 -1 到 1,数值越大代表样本越接近所在簇中心、同时越远离其他簇。但我在实际项目里通常把这两个图当作“建议值”而不是“最终答案”:真实数据的手肘往往不是那么尖锐,轮廓系数也经常有多个局部峰值,最终定 K 还是要结合业务可解释性和后续动作的成本。比如用户分群时 K=5 比 K=6 更容易给运营讲清楚,那就选 5。
2.3 初始化陷阱:k-means++ 与 random_state 的边界
KMeans 的初始化策略直接影响最终结果。纯随机选初始质心有一个明显问题:如果两个初始点靠得太近,后续迭代很容易陷入局部最优,表现为结果不稳定、inertia 偏大。sklearn 默认的init='k-means++'就是为了缓解这点:它先随机选第一个质心,再用一个与距离相关的概率分布选后续质心,让初始质心尽可能分散开。
但k-means++不是万能的。它只是让初始点“更可能”分散,不保证全局最优。所以我通常还会保留n_init=10的默认值,意思是算法会从 10 组不同初始质心出发,各跑一轮,最后取惯性最小的一组结果。如果追求极致的稳定性,可以把n_init调到 20 以上,但训练时间会线性上升。另一个经常被忽略的点是random_state:不固定它,代码每次运行结果都可能不同,这对实验对比和线上复现来说是灾难。我自己的习惯是固定random_state=42,至少在模型调试阶段必须固定;等模型上线前,再用多个种子做稳定性验证,这个问题第 4 章还会详细讲。
3. Python 实现与调参:把 KMeans 流程跑通并看懂每个参数
前面把原理讲清楚了,这章进入正题:如何把 KMeans 跑在自己的数据上。这里给出的流程和资源包里的 KMeans.py 基本一致:读取文件 → 选择数值特征 → 标准化 → 训练 → 可视化 → 输出标签。我额外加了一节模型保存与预测,因为这是从 demo 走向真实项目时绕不开的一步。
3.1 数据准备:StandardScaler 为什么必须要做
KMeans 依赖欧氏距离计算,而欧氏距离对量纲极其敏感。假设你有两个特征,一个是“年龄”(20~60),一个是“年收入”(几万到几十万),如果不做任何处理,收入会在距离计算里占据绝对主导,聚类结果基本等于“按收入分段”,年龄几乎不起作用。这不是算法错了,是输入数据的问题。
pip install numpy pandas matplotlib scikit-learn joblibimport pandas as pd import numpy as np from sklearn.preprocessing import StandardScaler # 读取数据,CSV 用 read_csv,Excel 用 read_excel data = pd.read_csv("your_data.csv") # 只保留参与聚类的数值特征 feat_cols = ["age", "income", "spend_score"] X = data[feat_cols] # 先统一量纲,让每个特征在距离计算中权重相等 scaler = StandardScaler() X_scaled = scaler.fit_transform(X)fit_transform做的事情是:先用fit统计每个特征的均值和标准差,再用transform把数据转换成均值为 0、标准差为 1 的形式。注意一个必须养成的习惯:这里的scaler对象要保存下来,用来处理后续的新数据,而不是在新数据上重新fit。至于选StandardScaler还是MinMaxScaler,我一般会先看数据分布:如果特征大致接近正态分布,标准化就可以;如果特征范围差异极大,且存在明显离群点,RobustScaler更稳,因为它用中位数和四分位距,不容易被极端值带偏。
3.2 训练、预测与可视化:代码逐行说明
数据准备好之后,训练过程本身其实只有几行代码。但容易踩坑的点都在细节里:用fit_predict还是先fit再predict、质心坐标是标准化空间里的值、画图时用什么颜色映射,下面这段代码把整个链路串起来。
from sklearn.cluster import KMeans import matplotlib.pyplot as plt k = 4 kmeans = KMeans(n_clusters=k, n_init=10, random_state=42) labels = kmeans.fit_predict(X_scaled) # 把簇标签写回原始 DataFrame,后续做分析和导出都用它 data["cluster"] = labels # 质心坐标是标准化空间里的值,直接看不好解释 print(kmeans.cluster_centers_) # 如果只用了两个特征,画散点图最直观 plt.figure(figsize=(8, 6)) scatter = plt.scatter( X_scaled[:, 0], X_scaled[:, 1], c=labels, cmap="viridis", s=30, alpha=0.7 ) plt.scatter( kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1], marker="x", color="red", s=120, linewidths=2 ) plt.xlabel(feat_cols[0]) plt.ylabel(feat_cols[1]) plt.colorbar(scatter) plt.show()fit_predict等价于先fit再predict,但只适合训练这批样本时用;当有增量数据进来时,应该用已经训练好的模型去predict,否则会改变原模型结构。cluster_centers_返回的是形状为(k, n_features)的数组,每一行是一个簇的质心,但因为是标准化空间里的坐标,输出报告前通常需要scaler.inverse_transform还原成原始量纲。画图时,c=labels决定了每个点的颜色,cmap="viridis"只是配色方案,可以换。
3.3 核心参数怎么设:n_clusters、n_init、max_iter、random_state
下面这张表是 sklearn KMeans 里我每次都会确认的参数,按重要性排序。
| 参数 | 默认值 | 作用 | 常见调整方式 |
|---|---|---|---|
| n_clusters | 8 | 簇数量 | 用手肘图 + 轮廓系数 + 业务解释共同决定 |
| init | k-means++ | 初始质心策略 | 默认即可;极大数据量想提速可改 random |
| n_init | 10 | 独立运行次数,取最优 | 数据量大可降到 3~5,追求稳定可提高到 20 |
| max_iter | 300 | 单次运行迭代上限 | 一般不用动 |
| tol | 1e-4 | 收敛阈值 | 想加速可以放宽到 1e-3 |
| random_state | None | 随机种子 | 调试阶段固定为某个整数,保证可复现 |
| algorithm | lloyd | 求解算法 | sklearn 新版本推荐 lloyd;旧版本 elkan 是默认 |
n_init的直觉理解是“多试几次,挑最好的结果”,它对抗的是局部最优。max_iter是单次尝试内部的迭代上限,通常没机会触达,因为质心移动足够小时算法会提前停止。tol则是判断质心移动“够不够小”的阈值,调大一点训练会快一些,但簇质量可能微降。这些参数不是每次都要动,我一般只固定random_state,只有在速度明显瓶颈时才考虑降低n_init。
3.4 保存模型与预测新数据:joblib 全流程
训练完模型,下一步通常是要保存下来,给后续的新数据打分。这里有一个典型的理解误区:很多人把scaler和kmeans分开保存,但预测时忘了对样本用同一个标准化器,或者干脆又fit了一次。
import joblib # 保存训练好的模型和标准化器,两者要一起保存 joblib.dump(kmeans, "kmeans_model.pkl") joblib.dump(scaler, "scaler.pkl") # 新数据进来:只 transform,不重新 fit new_data = pd.read_csv("new_data.csv") new_scaled = scaler.transform(new_data[feat_cols]) new_label = kmeans.predict(new_scaled) new_data["cluster"] = new_labelscaler.transform用的是训练集上统计出来的均值和标准差,保证新旧数据处在同一个坐标系里。如果在这里调用fit_transform,新数据就会用自己的均值和标准差重算一遍,相当于坐标系变了,之前的质心位置不再匹配,分组结果自然乱套。另外还要记住:KMeans 的predict只是把新样本划分到距离最近的现有质心,它不会更新质心,不存在“在线学习”的能力。要做模型更新,常见做法是定期用旧数据加新数据重新训练一次,而不是在线上逐条预测。
4. 避坑手记:KMeans 实际使用中最常见的五个翻车现场
这章的每一条都是我在真实数据上踩过的,或者是看别人踩完后验证过的。它们有一个共同特点:代码全部能跑通,甚至图也好看,但交付时经不起追问。你可以在自己的项目里对照排查。
4.1 现象:同一份代码,跑两次结果不一样
聚类结果不稳定,簇的编号和边界都在变。原因通常是random_state没有固定,而 KMeans 的初始质心是随机的,不同初始点可能收敛到不同的局部最优解。解决方法是先固定random_state=42做开发调试,保证同事能复现;如果固定种子后依然不稳,说明数据本身的簇结构就不清晰,需要重新审视 K 的取值,或者考虑后文提到的 DBSCAN。
4.2 现象:标准化前后,聚类结果天差地别
有人拿原始数据直接聚类,发现结果与预期完全不符,比如高价值客户被分到了同一个簇,但特征均值差异并不合理。原因多半是某个量纲大的特征,比如年收入几万元和几十万元的差距,在欧氏距离里直接“碾压”了年龄和消费频次。解决方法是全部数值特征统一过StandardScaler,并且用scaler.inverse_transform(kmeans.cluster_centers_)把质心还原成原始量纲再解释。要记住:标准化不是可选项,是 KMeans 的前提,除非你确认所有特征的量纲天然一致。
4.3 现象:轮廓系数很高,但业务上完全没法解释
我遇到过 silhouette_score 达到 0.72,看起来簇分得很清楚,结果画出降维图一看,边界位置和业务方理解完全错位。原因是轮廓系数衡量的是几何距离意义上的“紧凑”和“分离”,它不关心簇的业务含义。解决方法是别只盯指标,画出实际分布,并把每个簇的特征均值列成表格,找业务方一起确认“这三个人群到底是高活跃用户、沉睡用户还是流失边缘用户”,只有业务解释通了,聚类才有价值。
4.4 现象:预测新数据时,分组结果明显对不上号
训练时明明分得挺好,拿到一批新数据预测后,全部样本被分到同一个簇,或者新旧分组比例完全不合理。常见原因是预测阶段重新fit了标准化器,相当于换了坐标系;另一个原因是新数据和训练集分布差异过大,比如训练集是电商用户在 1—3 月的消费行为,新数据却来自大促期间,特征分布完全漂移。解决方法是把kmeans和scaler一起持久化,预测时只transform和predict;同时简单统计新数据的均值、标准差,和训练集对比,差异过大就重训模型,不要硬预测。
4.5 现象:离群点把质心拉偏,正常样本被硬凑进一个大簇
聚类结果里,某个簇的中心明显偏向一个孤立点,其他样本则被勉强塞进另一个大簇。原因是 KMeans 用均值做质心,均值对极端值几乎没有抵抗力,一个离群点就能让质心移动一大截。解決方法是在训练前先做离群点筛查,常见做法是用 IQR(四分位距)或先跑一版 DBSCAN 把噪声样本标出来,从训练集中剔除后再跑 KMeans。但要注意区分“数据错误导致的异常”和“业务上合理的小众群体”,后者不应该被直接删掉,可以考虑单独建簇。
5. 场景落地:用户分群、图像分割与文本聚类的实际套路
KMeans 不是只能在二维散点图上画圈,它的典型应用集中在三个方向:用户分群、图像分割、文本聚类。每个场景都有自己特定的数据形态,处理方式差异很大,下面逐个说。
5.1 用户分群:基于 RFM 特征的客户分层流程
用户分群是 KMeans 在业务中最常见的落地场景。通常用 RFM 模型构造特征:recency是最近一次消费距离今天的天数,frequency是消费频率,monetary是累计消费金额。这三个特征量纲完全不同,尤其是金额和天数,标准化是必须的。
rfm = pd.read_csv("rfm_data.csv") X = rfm[["recency", "frequency", "monetary"]] scaler = StandardScaler() X_scaled = scaler.fit_transform(X) km = KMeans(n_clusters=4, n_init=10, random_state=42) rfm["cluster"] = km.fit_predict(X_scaled) # 看每个簇的业务画像,groupby 后取均值 profile = rfm.groupby("cluster").agg({ "recency": "mean", "frequency": "mean", "monetary": "mean", "cluster": "count" }).rename(columns={"cluster": "size"}) print(profile)groupby("cluster").agg会输出每个簇下的平均最近消费天数、平均消费频率、平均金额和人数。这组数直接喂给运营,按数值高低就能翻译成“高价值高活跃”“高价值沉睡”“低价值活跃”等业务标签。有一个细节我每次都会提醒:如果原始数据里有大量从未消费的用户,金额全为 0 甚至缺失,这类样本会显著干扰聚类,常见做法是先按业务规则单独划分出来,再对剩余用户做 KMeans。
5.2 图像分割:把像素当作特征点来做压缩与分区
图像分割是 KMeans 一个很有说服力的用法:把每个像素的 RGB 值当成一个三维特征点,整张图就是几十万到几百万个点,聚类后每个像素点被贴上簇标签,再用簇中心的颜色替换原像素颜色,就得到了分割或压缩后的图像。
import numpy as np import matplotlib.image as mpimg from sklearn.cluster import KMeans img = mpimg.imread("demo.jpg") h, w, c = img.shape pixels = img.reshape(-1, c).astype(np.float64) # 像素数量通常很大,先采样训练,再预测全部像素,能省大量时间 sample_idx = np.random.default_rng(0).choice(len(pixels), size=10000, replace=False) km = KMeans(n_clusters=5, n_init=5, random_state=42) km.fit(pixels[sample_idx]) labels = km.predict(pixels) segmented_img = km.cluster_centers_[labels].reshape(h, w, c).astype(np.uint8)这里pixels的形状是(h*w, c),c通常是 3(RGB)。先采样 1 万个像素点训练,再把所有像素交给predict,是因为一张 1920×1080 的图有约 200 万个像素,直接全量训练耗时和内存开销都不划算。最后km.cluster_centers_[labels]把每个像素的标签映射回质心颜色,重新reshape成图像尺寸就完成了分割。实际经验是,RGB 空间的欧氏距离与人的颜色感知不完全一致,处理自然图像时可以先转成 LAB 色彩空间再聚类,效果往往更接近肉眼的分区结果。
5.3 文本聚类:TF-IDF 向量化之后,别漏掉 L2 归一化
文本聚类的基础流程是:先用 TF-IDF 把文本转成向量,再做聚类。这里有一个容易忽略的细节:TF-IDF 矩阵通常非常稀疏,而且文档长度差异大,直接用StandardScaler只会让矩阵更稀疏,应该用Normalizer做 L2 归一化,让每个文档向量的模长为 1。
from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.preprocessing import Normalizer from sklearn.cluster import KMeans docs = [] # 你的中文/英文文本列表 vec = TfidfVectorizer(max_features=5000) X_tfidf = vec.fit_transform(docs) X_norm = Normalizer().fit_transform(X_tfidf) km = KMeans(n_clusters=5, n_init=10, random_state=42) labels = km.fit_predict(X_norm) # 每个簇输出 top 关键词 order_centroids = km.cluster_centers_.argsort()[:, ::-1] terms = vec.get_feature_names_out() for i in range(5): top_words = [terms[idx] for idx in order_centroids[i, :10]] print(f"cluster {i}: {top_words}")argsort()[:, ::-1]的作用是把每个簇的质心向量按特征权重从大到小排列,取前 10 个对应的词,就是该簇的代表性关键词。max_features=5000限制了词表规模,避免维度过高拖慢训练。文本聚类里,分词质量比算法本身更决定成败,比如中文文本直接用原始字符串做 TF-IDF 基本不可用,要先分词再组装成以空格分隔的词语文本。另外,如果你的语料有明显的停用词,比如“的”“了”“和”,应该在向量化阶段就把它们过滤掉,否则这些词会频繁出现在多个簇的 top 关键词里,干扰主题解读。
5.4 与 DBSCAN 对比:什么时候你根本不该用 KMeans
KMeans 不是银弹。它默认数据簇是近似球形的、密度接近的,如果你手头的数据形状不规则,KMeans 再调参效果也有限。这里把 KMeans 和 DBSCAN 做个对比,方便你在项目一开始就选对算法。
| 维度 | KMeans | DBSCAN 聚类算法 |
|---|---|---|
| 簇形状 | 适合凸形、球形簇 | 可以发现任意形状簇 |
| 簇数量 K | 必须预先指定 | 不需要指定 |
| 离群点 | 敏感,会拉偏质心 | 自动标记为噪声点 |
| 数据规模 | 百万级样本也能跑 | 大样本时距离计算压力大 |
| 高维稀疏数据 | 尚可 | 距离度量容易失效 |
判断条件其实很简单:如果你不知道 K 值、但数据中明显有“离群点”和“不规则形状”,用 DBSCAN;如果你的数据是典型的用户行为评分、商品属性,簇的形状相对圆润,且需要稳定的 K 值输出给业务,KMeans 更合适。DBSCAN 的调参难点在eps和min_samples两个参数,eps的直观含义是“多大范围内算邻居”,选得不好结果波动很大,常见做法是画 k 距离曲线找拐点。KMeans 容易上手,但这正是它被滥用的原因,选哪个不取决于哪个“更好”,取决于你的数据长什么样。
6. 交付前最后一遍验证:簇画像、稳定性检查与结果解释
聚类模型训练完,不代表可以立刻交付。我见过太多“代码跑通、图画好,一追问细节就翻车”的项目,所以每次交付前,我都会用一套极简的流程做最终校验,这里把它完整分享出来。
6.1 簇画像输出
不要只说“分了 4 个簇”,要输出每个簇的样本占比、特征均值和关键指标对比。这一步用groupby就能完成,关键是要把标准化空间的质心还原到原始量纲,否则业务方看着一堆负数均值无法交流。我的习惯是:一张表列出每个簇的样本数量、占比、各特征均值,再起一个业务名,比如“高活跃高客单”“沉睡腰部用户”,贴到交付文档第一页。
6.2 稳定性验证
换几个不同的random_state重新训练,观察簇的分布是否基本一致。具体做法是跑 5 个种子,统计每个样本在多次运行中是否被分到同一个簇;如果超过 10% 的样本在不同种子下摇摆,就说明聚类结构本身不稳定,需要调整 K 值或换算法。
from collections import Counter def stability_check(X, k=4, seeds=[0, 1, 2, 3, 4]): labels_matrix = [] for seed in seeds: km = KMeans(n_clusters=k, n_init=10, random_state=seed) labels_matrix.append(km.fit_predict(X)) stable_ratio = 0.0 for i in range(X.shape[0]): votes = Counter([labels[i] for labels in labels_matrix]) stable_ratio += max(votes.values()) / len(seeds) return stable_ratio / X.shape[0]这段脚本的Counter统计每个样本在多个种子下的簇编号,max(votes.values())是多数投票的票数占比。返回值接近 1 代表结果稳定,明显低于 0.9 就说明模型对初始条件敏感。这个验证只花几分钟,能避免“换一个随机种子,整个结论都变了”的尴尬。
其实我刚开始做聚类时,不习惯做这些检查,直到有一次客户分群交付后,业务方重新跑了一遍脚本,发现换种子后两个高价值人群占比完全变了,结论没法用。从那以后,我每次聚类交付前都强制走一遍:画分布、看轮廓系数、列出簇画像、换种子做稳定性验证。这套流程不复杂,但能挡住绝大多数“代码能跑、结果不可信”的问题。希望帮到你。
本文还有配套的精品资源,点击获取