没有标签怎样自动分组?K 均值的分配—更新循环与失败几何
从无标签用户分群出发,推导 K 均值的簇内平方和,手算分配与中心更新,并解释初始化、尺度、选 K、调试方法和失败几何。
上一篇的线性判别分析(Linear Discriminant Analysis,LDA)知道每个训练样本属于哪一类,因此能估计各类均值、先验和共享协方差。但用户画像、设备状态或新药分子常常只有特征,没有现成标签:我们甚至不知道应当寻找哪些类别。
聚类(Clustering)试图从样本之间的相似性发现结构。K 均值(K-Means)是最基本的原型聚类方法:预先指定 个中心,让每个样本靠近某个中心,再让中心移动到所负责样本的均值。本文只讲透四件紧密相关的事:目标函数、分配—更新循环、初始化与尺度,以及它为什么会被错误几何欺骗。
01 没有标签时,“分得好”是什么意思?#
设输入矩阵为:
是样本数, 是特征数。我们希望得到:
- 簇编号 ,全部编号 ;
- 簇中心 ,全部中心 。
K 均值把“好分组”定义为最小化簇内平方和(Within-Cluster Sum of Squares,WCSS):
scikit-learn 把这个值称为 inertia_。它只关心样本到最近中心的平方欧氏距离,不知道“客户类型”“疾病亚型”等语义,也没有分类准确率可供优化。
02 为什么一次不能同时求出编号和中心?#
若中心 已知,每个样本的最佳编号很直接:
若编号 已知,第 个中心的最佳位置是该簇样本均值:
其中 。困难在于两者互相依赖:不知道中心就无法分配,不知道分配又无法算中心。Lloyd 算法采用交替优化(Alternating Optimization):固定一边优化另一边。
X [N,D] + 初始中心 M⁽⁰⁾ [K,D]
│
▼
两两平方距离 [N,K]
│ 每行 argmin
▼
簇编号 z [N]
│ 按编号分组求均值
▼
新中心 M⁽¹⁾ [K,D]
│
中心移动是否足够小?
├── 否:继续循环
└── 是:输出 z、M、Jtext每个分配步不会增大 ,每个更新步也不会增大 ,所以目标会下降并最终停止。但这只保证到达一个局部最优解,不保证全局最好。
03 为什么更新一定是“均值”?#
先看一个簇,固定它包含的样本集合 ,中心为 :
对向量 求梯度:
令梯度为零:
所以“均值”并非经验规则,而是平方欧氏距离下的最优代表点。若把损失换成绝对距离,最优代表会转向中位数;算法名称与几何也随之改变。
04 用四个二维点手算一轮#
四个样本为:
令 ,初始中心取 、。
分配步的平方距离矩阵为:
每行取最小值位置,得到 。更新中心:
再次分配时编号不变,算法收敛。最终目标为:
x₂
2 ● x₂ ● x₄
1 × μ₀ × μ₁
0 ● x₁ ● x₃
0 6 x₁
左右两个圆团适合 K 均值;× 是均值中心。text05 用广播写出可检查的 NumPy 核心#
下面的实现故意不调用 fit,以暴露每个中间张量:
import numpy as np
X = np.array([
[0.0, 0.0],
[0.0, 2.0],
[6.0, 0.0],
[6.0, 2.0],
], dtype=np.float64) # [N=4,D=2]
centers = np.array([[0.0, 0.0], [6.0, 2.0]]) # [K=2,D=2]
for step in range(20):
delta = X[:, None, :] - centers[None, :, :] # [N,K,D]
squared_distances = np.sum(delta**2, axis=2) # [N,K]
labels = np.argmin(squared_distances, axis=1) # [N]
new_centers = np.stack([
X[labels == k].mean(axis=0)
for k in range(centers.shape[0])
]) # [K,D]
movement = np.linalg.norm(new_centers - centers)
centers = new_centers
if movement < 1e-8:
break
inertia = np.sum((X - centers[labels]) ** 2)
assert labels.shape == (4,)
assert centers.shape == (2, 2)
assert np.array_equal(labels, [0, 0, 1, 1])
assert np.allclose(centers, [[0.0, 1.0], [6.0, 1.0]])
assert np.isclose(inertia, 4.0)python真实实现还必须处理空簇、样本权重、稀疏矩阵、停止容差和高效距离计算。上面的列表推导若某个 labels == k 没有样本,会产生非数(Not a Number,NaN);这是手写实现最先应添加的保护。
06 初始化为什么能改变最终答案?#
K 均值目标非凸。若初始中心挤在同一个真实簇附近,算法可能把另一个大簇与少数离群点错误合并,最后停在较差的局部最优。
K-Means++ 初始化的核心思路是让后续中心更倾向于从“离已有中心很远”的样本中产生:
- 随机选择第一个中心;
- 计算每个样本到最近已有中心的平方距离 ;
- 按与 成比例的概率选择下一个中心;
- 重复到获得 个中心。
这不是最终聚类,只是为 Lloyd 循环提供更分散的起点。scikit-learn 1.9 默认 init='k-means++',其实现会对候选做多次试探;n_init='auto' 在该初始化下只运行一次。对高维、稀疏或重要任务,显式设 n_init=10 并比较不同种子通常更稳妥。
07 特征尺度如何偷偷改写“相似”?#
假设年龄范围约 20–60,而年收入以元计,范围 30 000–1 000 000。平方欧氏距离中收入差会压倒年龄差。此时模型不是“发现收入更重要”,而是被单位选择支配。
若所有连续特征都应等权,可在训练数据上标准化:
但标准化也不是无条件正确:经纬度、周期角度、计数、类别变量和有明确业务权重的特征需要合适的距离或编码。异常值还会同时拉动标准差与簇中心;必要时比较稳健缩放、截尾或专门的异常检测。
08 用 scikit-learn 1.9 正确落地#
按照当前官方 KMeans 应用程序接口(Application Programming Interface,API) ↗,把会学习数据统计量的预处理和聚类放进同一条流水线(Pipeline):
import numpy as np
from sklearn.cluster import KMeans
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
pipeline = make_pipeline(
StandardScaler(),
KMeans(
n_clusters=3,
init='k-means++',
n_init=10,
max_iter=300,
tol=1e-4,
algorithm='lloyd',
random_state=42,
),
)
labels = pipeline.fit_predict(X_train) # [N]
new_labels = pipeline.predict(X_new) # [Q]
distances = pipeline.transform(X_new) # [Q,K]
model = pipeline.named_steps['kmeans']
print(model.cluster_centers_.shape) # [K,D],位于标准化空间
print(model.labels_.shape) # [N]
print(model.inertia_) # 标量:训练集 WCSS
print(model.n_iter_) # 实际迭代轮数
assert distances.shape == (X_new.shape[0], 3)
assert np.all(np.isfinite(model.cluster_centers_))python几个 API 细节值得明确:
fit_predict(X)等价于拟合后返回训练样本编号;predict(X_new)只把新样本分配给最近的既有中心,不会更新中心;transform(X_new)输出到每个中心的欧氏距离,而不是平方距离;score(X)返回 K 均值目标的相反数,因此越大越好但通常为负;algorithm='elkan'可用三角不等式减少部分距离计算,但额外需要[N,K]内存;是否更快取决于簇是否分离良好和实际数据表示。
若样本量很大,可评估 MiniBatchKMeans。它用小批量近似更新中心,速度更快,但最终 inertia_ 和稳定性可能略差;不能只因名称相近就假设与全量 K 均值结果完全一致。
09 K 应该怎样选?#
训练目标会随 增大而单调下降:当 时每个点自成一簇,,却通常毫无概括价值。因此不能用最小训练 inertia_ 直接选 K。
可联合使用三类证据:
- 肘部图(Elbow Plot):画 与 WCSS,寻找继续增加中心后收益明显变缓的位置;
- 轮廓系数(Silhouette Coefficient):比较样本与本簇的紧密度和最近其他簇的分离度,范围约为 ;
- 稳定性与可用性:在重采样、时间切片和不同种子下重复聚类,检查簇大小、中心与业务解释能否稳定复现。
from sklearn.metrics import silhouette_score
records = []
for k in range(2, 9):
candidate = make_pipeline(
StandardScaler(),
KMeans(n_clusters=k, n_init=10, random_state=42),
)
labels = candidate.fit_predict(X_train)
scaled = candidate[:-1].transform(X_train)
records.append({
'k': k,
'inertia': candidate[-1].inertia_,
'silhouette': silhouette_score(scaled, labels),
'smallest_cluster': np.bincount(labels).min(),
})python轮廓系数也偏好分离良好的凸簇;它不是领域真相。若业务必须得到 5 个可执行人群,而统计曲线在 3–6 都接近,应把约束、稳定性和后续效用一起写进决策。
10 怎样调试一个“能运行但分错了”的聚类?#
按数据流逐层检查:
- 输入:确认无非数(Not a Number,
NaN)或无穷值(Infinity,Inf),重复行、单位和类别编码符合预期; - 尺度:打印缩放后每列均值与标准差,定位支配距离的特征;
- 优化:记录多种
random_state的inertia_、n_iter_与簇大小; - 几何:在原特征和二维投影中画样本、中心和边界,但不要把二维图等同于全部高维结构;
- 稳定性:对重采样数据重新拟合,用调整兰德指数等置换不变指标比较分区;
- 外部效用:只在分群确定后,用未参与聚类的结果变量检查群体是否产生可复现差异。
最小检查代码:
counts = np.bincount(labels, minlength=model.n_clusters)
assert counts.sum() == X_train.shape[0]
assert np.all(counts > 0)
assert model.n_iter_ <= model.max_iter
print('cluster sizes:', counts)
print('inertia per sample:', model.inertia_ / X_train.shape[0])python11 K 均值会在哪些几何中失败?#
K 均值隐含偏好大小相近、密度相近、近似球形且可由维诺(Voronoi)边界分开的簇。
适合:两个紧凑圆团 失败:两个月牙 失败:密度悬殊
●● ○○ ●●●○○○ ●●●●● ○ ○
●●● ○○○ ●● ○○ ●●● ○
●● ○○ ● ○ ●●
最近中心边界合理 直线切碎弯曲流形 大簇被拆、小簇被吞text典型失败场景包括:
- 同心圆、月牙和细长流形;
- 不同簇方差或样本量相差悬殊;
- 离群点把均值中心拉远;
- 高维空间距离集中,最近与最远差别变小;
- 簇重叠而任务需要概率或不确定性;
- 纯类别数据,均值本身没有意义;
- 数据持续漂移,但部署端仍使用旧中心。
12 与相近方法的边界#
基于密度的含噪空间聚类(Density-Based Spatial Clustering of Applications with Noise,DBSCAN)通过密度连通形成簇。
| 方法 | 分配方式 | 主要几何/假设 | 更适合什么 |
|---|---|---|---|
| K 均值 | 到最近均值的硬分配 | 近似球形、平方欧氏距离 | 快速基线、向量量化 |
| 高斯混合模型 | 概率软分配 | 椭圆高斯、可估计协方差 | 重叠簇与不确定性 |
| DBSCAN | 核心点密度连通 | 任意形状、密度阈值 | 噪声点与非凸簇 |
| 层次聚类 | 逐步合并或拆分 | 由距离与 linkage 决定 | 需要树状层级、小中数据 |
| K 中心点 | 最近真实样本 | 可配更一般距离、较抗异常 | 中心必须可解释为样本 |
表中这些方法回答的不是同一道题;换算法前先说明你希望保持的结构:中心、密度、连通性、概率,还是层次。
13 今天真正需要记住什么?#
K 均值通过两步循环降低簇内平方和:给定中心时分配到最近中心,给定分配时把中心更新为均值。它快速、可扩展、容易解释,但结果依赖 、初始化、特征尺度和近似球形几何。一个低 inertia_ 只说明模型优化了自己的目标,不代表发现了真实类别。
14 思考题与小练习#
- 对点 做一维 聚类,初始中心为 和 。手算每轮编号、中心和 WCSS,观察是否得到直觉中的分组。
- 若把四点例子中的第二维整体乘以 100,分配会不会改变?构造一个确实改变的六点数据集,并解释单位为何等价于特征权重。
- 在同一数据上用 20 个种子运行
KMeans(n_init=1),记录最优与最差inertia_、簇大小和轮廓系数;再与n_init=20比较。
相关工作#
- Lloyd (1982), Least Squares Quantization in PCM ↗:现代 K 均值分配—更新算法的经典表述。
- MacQueen (1967), Some Methods for Classification and Analysis of Multivariate Observations ↗:提出 “k-means” 名称并讨论在线式更新。
- Arthur & Vassilvitskii (2007), k-means++: The Advantages of Careful Seeding ↗:用距离加权初始化改善期望质量。
- Rousseeuw (1987), Silhouettes: A Graphical Aid to the Interpretation and Validation of Cluster Analysis ↗:轮廓系数的原始工作。
15 下一篇预告#
K 均值直接在原始特征空间计算距离;当几十个传感器高度相关或图像像素维度巨大时,冗余方向会增加计算并掩盖结构。下一篇将进入主成分分析:怎样在尽量保留方差的前提下,把高维样本投影到少数正交方向,并从重构误差看清“保留信息”到底是什么意思。