用 scikit-learn 跑通 K-Means:选簇、管道与调参实战

2026-08-04 45 预计阅读时间: 1 分钟
来源: realpython.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:7 分钟

K-Means 的 API 很短,真正影响结果的却是簇数、特征尺度、初始化方式和评估指标。把这些决策放进 scikit-learn 管道,可以避免预处理不一致,也能让调参与复现实验更可靠。

K-Means 实际在优化什么

给定簇数 k,K-Means 会反复执行两步:把样本分配给最近的质心,再根据簇内样本更新质心。它试图最小化簇内平方和,也就是 scikit-learn 中 inertia_ 表示的目标。

这带来几个直接限制:

  • 必须提前指定 n_clusters
  • 欧氏距离决定样本归属,因此特征尺度会直接改变结果。
  • 不同初始质心可能收敛到不同的局部最优解。
  • 算法更适合大小和密度相近、形状接近凸集的簇。
  • 离群值可能明显拉动质心。

例如,一个取值范围在数十万的收入特征,可能完全压过范围为 0 到 30 的购买次数。除非这种权重正是业务意图,否则应先缩放特征。

不要只凭肘部法选择簇数

inertia_ 会随着 k 增大而下降,因为更多质心总能让样本离质心更近。因此,不能简单选择惯性最低的模型。肘部法关注的是收益开始明显变小的位置,但真实数据的曲线未必存在清晰拐点。

轮廓系数提供了另一个视角。它同时比较样本在本簇内的紧密程度和与最近其他簇的分离程度,通常越接近 1 越好。不过,指标最高的 k 也不一定最有业务价值:客户分群最终还要检查每个簇的规模、稳定性和可解释性。

更稳妥的选择流程是:

  1. 在合理范围内比较多个 k
  2. 同时记录惯性与轮廓系数。
  3. 检查不同随机种子下结果是否稳定。
  4. 查看每个簇的样本量和特征画像。
  5. 用下游任务或业务约束完成最终判断。

可以这样实践:用管道比较多个 k

下面的示例生成三组二维数据,将标准化与 K-Means 放进同一个 Pipeline,然后比较 k=2k=6。运行前安装依赖:

python -m pip install scikit-learn

将以下代码保存为 kmeans_demo.py 后直接运行:

from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
from sklearn.metrics import silhouette_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

X, _ = make_blobs(
    n_samples=600,
    centers=3,
    cluster_std=[1.0, 1.4, 0.8],
    random_state=42,
)

results = []
for k in range(2, 7):
    pipeline = Pipeline(
        steps=[
            ("scale", StandardScaler()),
            (
                "cluster",
                KMeans(
                    n_clusters=k,
                    init="k-means++",
                    n_init=20,
                    random_state=42,
                ),
            ),
        ]
    )

    labels = pipeline.fit_predict(X)
    scaled_X = pipeline.named_steps["scale"].transform(X)
    model = pipeline.named_steps["cluster"]

    results.append(
        {
            "k": k,
            "inertia": round(model.inertia_, 2),
            "silhouette": round(silhouette_score(scaled_X, labels), 3),
        }
    )

for result in results:
    print(result)

这里有一个容易忽略的细节:轮廓系数必须基于模型实际接收的特征空间计算。由于 K-Means 使用的是标准化后的数据,silhouette_score 也应接收 scaled_X,而不是原始 X

示例中的 random_state 固定了随机过程,便于复现实验;n_init=20 会尝试多组初始质心并保留目标值更好的结果。计算成本会随 n_init 增加,但在数据规模允许时,这通常比只运行一次更可靠。

从实验代码走向可用模型

确定 k 后,可以重新训练最终管道,并把它作为一个整体持久化。这样预测新样本时会自动执行完全相同的标准化操作:

import joblib
from sklearn.cluster import KMeans
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

final_pipeline = Pipeline(
    steps=[
        ("scale", StandardScaler()),
        (
            "cluster",
            KMeans(
                n_clusters=3,
                n_init=20,
                random_state=42,
            ),
        ),
    ]
)

final_pipeline.fit(X)
joblib.dump(final_pipeline, "kmeans_pipeline.joblib")

loaded = joblib.load("kmeans_pipeline.joblib")
print(loaded.predict([[0.2, -1.1], [4.8, 3.9]]))

真实项目还应保存训练时的字段顺序、单位和缺失值处理规则。K-Means 不会自动理解类别字段,也不能直接处理缺失值;可以在管道中加入 SimpleImputer,并在进入模型前正确编码类别特征。

上线前的检查清单

  • 确认距离度量符合业务含义,避免无意中让大尺度特征主导聚类。
  • 不把惯性下降当作选择最大 k 的理由。
  • 检查轮廓系数、簇规模、质心画像和多次训练稳定性。
  • random_staten_init、特征列表和训练数据版本留下记录。
  • 监控新数据到各质心的距离分布;距离整体上升可能意味着数据漂移。
  • 谨慎解释簇编号。标签 012 没有天然顺序,重新训练后编号也可能互换。

K-Means 的价值不在于快速生成一列标签,而在于建立一套可重复审查的分群过程。管道解决预处理一致性,多个指标帮助选择 k,稳定性与业务解释则决定这些簇能否真正投入使用。


相关推荐