K-Means 的 API 很短,真正影响结果的却是簇数、特征尺度、初始化方式和评估指标。把这些决策放进 scikit-learn 管道,可以避免预处理不一致,也能让调参与复现实验更可靠。
K-Means 实际在优化什么
给定簇数 k,K-Means 会反复执行两步:把样本分配给最近的质心,再根据簇内样本更新质心。它试图最小化簇内平方和,也就是 scikit-learn 中 inertia_ 表示的目标。
这带来几个直接限制:
- 必须提前指定
n_clusters。 - 欧氏距离决定样本归属,因此特征尺度会直接改变结果。
- 不同初始质心可能收敛到不同的局部最优解。
- 算法更适合大小和密度相近、形状接近凸集的簇。
- 离群值可能明显拉动质心。
例如,一个取值范围在数十万的收入特征,可能完全压过范围为 0 到 30 的购买次数。除非这种权重正是业务意图,否则应先缩放特征。
不要只凭肘部法选择簇数
inertia_ 会随着 k 增大而下降,因为更多质心总能让样本离质心更近。因此,不能简单选择惯性最低的模型。肘部法关注的是收益开始明显变小的位置,但真实数据的曲线未必存在清晰拐点。
轮廓系数提供了另一个视角。它同时比较样本在本簇内的紧密程度和与最近其他簇的分离程度,通常越接近 1 越好。不过,指标最高的 k 也不一定最有业务价值:客户分群最终还要检查每个簇的规模、稳定性和可解释性。
更稳妥的选择流程是:
- 在合理范围内比较多个
k。 - 同时记录惯性与轮廓系数。
- 检查不同随机种子下结果是否稳定。
- 查看每个簇的样本量和特征画像。
- 用下游任务或业务约束完成最终判断。
可以这样实践:用管道比较多个 k
下面的示例生成三组二维数据,将标准化与 K-Means 放进同一个 Pipeline,然后比较 k=2 到 k=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_state、n_init、特征列表和训练数据版本留下记录。 - 监控新数据到各质心的距离分布;距离整体上升可能意味着数据漂移。
- 谨慎解释簇编号。标签
0、1、2没有天然顺序,重新训练后编号也可能互换。
K-Means 的价值不在于快速生成一列标签,而在于建立一套可重复审查的分群过程。管道解决预处理一致性,多个指标帮助选择 k,稳定性与业务解释则决定这些簇能否真正投入使用。