同心圆 400 个点:K 均值 ARI 0.0047,等于瞎猜;核 K 均值在带宽合适时能到 1.000。 但单次运行并不可靠:最好的带宽 1.4 也只有 6/10 次成功。 必须跑多次、按核目标取最好,这时 0.8 到 1.4 都能稳定拿到 1.000。 最反直觉的一条:带宽 3.0 时求得的核目标 105.79 比真标签的 126.59 还低, 可 ARI 只有负 0.002——目标函数降低了,划分反而更错。
上一节的结论是:K 均值只会用「到中心的距离」做判断,所以它只能分球。 同心圆、月牙这类数据,它败得毫无悬念(ARI 0.0047)。 这一节不换算法,只换空间。想法和 13.4 节的核 PCA 完全一样:先把点映射到一个高维空间, 在那里用 K 均值——因为「高维里的球」退回原始空间看,可以是任意弯的形状。 而这一切依然只需要核矩阵,不需要知道那个映射长什么样。
一、为什么 K 均值在同心圆上必败
先把失败的原因说透,因为它决定了后面所有补救的方向。 K 均值给每个点分配簇,靠的是「到哪个中心更近」。两个中心的势力范围分界, 是一条直线(三维里是平面,高维里是超平面)——这是欧氏距离的性质,不是实现细节。 所以 K 均值能画的边界,本质上是一条直线。而同心圆需要的是一个圆。 一条直线穿过圆心,切出来的两半里,每一半都同时含有内圈和外圈的点。 不管你怎么初始化、跑多少次,结果都一样——这不是优化问题,是表达能力问题。 实测 ARI 0.0047。ARI 为 0 意味着「和随机划分一样好」,0.0047 基本就是噪声。 注意这和 14.1 节那种「初始化不好所以掉到 0.432」是两种失败:那个能靠多跑几次救回来, 这个救不回来。 区分这两种失败很重要。 优化失败:目标函数还能更低,只是没找到——多跑、换初始化、换更好的求解器。 表达失败:目标函数的全局最优本身就不对——换模型、换空间、换目标函数。 分不清这两者,就会在一个根本不可能work的模型上反复调参。
二、把「距离」换成核
核 K 均值的推导只有一步:把 K 均值里的「点到中心的距离平方」换成核空间里的版本。 原始空间:||x 减 c_k||²,其中 c_k 是第 k 簇的平均。 核空间:||φ(x) 减 m_k||²,其中 m_k 是第 k 簇在特征空间里的平均。 展开之后,它变成三项:K(x,x) 减去 2 除以簇大小乘以「x 与簇内所有点的核值之和」, 再加上「簇内所有点两两核值之和」除以簇大小的平方。 三项全都只用到核矩阵,一个高维坐标都不用算。 代码上就是三层循环:对每个点、对每个簇,算那两个和。 复杂度是每轮 n²K,比普通 K 均值(每轮 nK)贵 n 倍,但换来的是能弯曲的边界。 这里有一个必须提前说清的取舍:核 K 均值没有「中心」的显式坐标。 普通 K 均值可以打印出三个中心点,核 K 均值打印不出来——中心只存在于那个看不见的特征空间里。 要判断一个新点属于哪簇,只能拿它和所有训练点算核值。 这和第 13.3 节 LPP 有显式投影矩阵、13.4 节核 PCA 没有,是同一类区别。
三、带宽:这一次真的决定生死
核 K 均值只有一个参数:带宽 h。它决定「多远算远」。 每种带宽跑十个种子,再跑一次「十次取最优」:
| 带宽 h | 单次最小 | 单次中位 | 单次最大 | 成功次数 | 十次取优 |
|---|---|---|---|---|---|
| 0.5 | -0.002 | 0.070 | 0.175 | 0/10 | 0.000 |
| 0.8 | -0.002 | 0.101 | 1.000 | 4/10 | 1.000 |
| 1.0 | -0.001 | 0.526 | 1.000 | 5/10 | 1.000 |
| 1.4 | -0.002 | 1.000 | 1.000 | 6/10 | 1.000 |
| 2.0 | -0.002 | 0.165 | 0.249 | 0/10 | -0.002 |
| 3.0 | -0.002 | 0.151 | 0.197 | 0/10 | -0.002 |
| 先看「成功次数」这一列:没有一种带宽能做到十次全对,最好的 1.4 也只有 6 次。 | |||||
| 这就是核 K 均值最该记住的一句:它很容易卡在局部最优,工程上一定要跑多次、按核目标取最好。 | |||||
| 做完这个动作之后,0.8 到 1.4 之间都能稳定拿到 1.000。 | |||||
| 注意「单次中位」从 0.070 一路涨到 1.000 再跌下来——0.5 时完全是随机,2.0 时又回到线性水平。 | |||||
| 两头都救不回来,但原因不一样: | |||||
| h 太小(0.5)时核值只在自己身上还算大,核矩阵接近单位阵。 | |||||
| 这时簇内平方和几乎和「怎么分」无关——跑一百次也没用,因为目标函数本身已经没有区分能力了。 | |||||
| h 太大(2.0 以上)时核矩阵被拉平,所有点彼此都「有点像」,于是又退回到线性那套, | |||||
| 怎么切都切不开。 | |||||
| 这里内圈半径是 1、外圈半径是 3,最优带宽落在 1.0 到 1.4,刚好和两个环的间距(2)同量级。 | |||||
| 换一份数据,h 必须重新找。这不是「调参调得好」,是量纲问题。 |
四、目标函数降低,划分反而更错
这一节最反直觉的一段。 同样取十次最优,换带宽看两个数:核目标函数值,以及真标签本身的核目标函数值。
| 带宽 h | 求得核目标 | 真标签核目标 | ARI |
|---|---|---|---|
| 1.0 | 278.90 | 278.90 | 1.000 |
| 2.0 | 178.28 | 183.45 | -0.002 |
| 3.0 | 105.79 | 126.59 | -0.002 |
| h 等于 1.0 时,求得的目标值 278.90 和真标签的一模一样——它找到的就是全局最优, | |||
| 而且这个最优正好是对的划分。 | |||
| h 等于 3.0 时,求得的目标值 105.79,比真标签的 126.59 还低。 | |||
| 按「目标函数越小越好」这个标准,它的解比真答案还「好」。 | |||
| 可 ARI 是负 0.002——它把数据切成了毫无意义的两半。 | |||
| 这件事的教训比任何公式都重要:目标函数不是客观真理,它只是「你想要什么」的编码。 | |||
| 带宽选错了,那段编码就写错了;这时候优化得越彻底,错得越离谱。 | |||
| 所以在核方法里,「交叉验证挑带宽」不是可选的讲究,是必须做的事。 | |||
| 同样的道理在第 13 章出现过一次:13.1 节里「重建误差」是方差这个标准的产物, | |||
| 13.3 节里换成「邻居不许散开」,13.5 节又换成「弹簧别太紧」。 | |||
| 每一次换目标函数,都是在换「什么算好」的定义。 |
五、初始化:同一个病,只是更重
h 取 1.0,随机初始化 vs 特征空间里的 ++ 初始化:
| 初始化 | ARI 最小 | 中位 | 最大 |
|---|---|---|---|
| 随机 | 0.000 | 0.400 | 1.000 |
| ++ | -0.001 | 0.526 | 1.000 |
| ++ 比随机略好(中位 0.400 对 0.526),但都好不到哪去——单次运行的正确率只有一半上下。 | |||
| 所以别指望换个聪明的初始化就一劳永逸。 | |||
| 真正管用的还是那个笨办法:多跑几次,按目标函数挑最好的。 | |||
| 这也顺便说明了核方法的普遍毛病:核空间里的代价地形比原始空间崎岖得多,局部最优多得多, | |||
| 于是「把目标函数最小化」这件事本身变得更难做对。 | |||
| 为什么核空间更崎岖?直觉是:维度高了,点与点之间的「地形」有更多褶皱可以藏局部极小。 | |||
| 而 K 均值的每一步都是「硬分配」——每个点只能全归某一簇,不能「80% 属于这簇」。 | |||
| 这种硬分配让目标函数是分段常数的,没法用梯度信息,只能靠撞。 | |||
| 第 14.3 节的谱聚类正是从这一点上绕开的:它解的是一个连续的特征值问题。 |
六、动手 Demo:同心圆:K 均值 0.005,核 K 均值 1.000
这段代码在同心圆上跑两件事:先看普通 K 均值怎么败,再扫带宽、统计每个带宽的成功率、 并对照「求得的目标值」和「真标签的目标值」。 重点看两张表。第一张的「成功次数」说明单次运行不可靠; 第二张说明带宽太大时,目标函数会主动选择更错的划分。
import numpy as np
def d2(A, B):
return np.maximum(((A[:, None, :] - B[None, :, :]) ** 2).sum(-1), 0.0)
def circles(n=200, seed=5, r2=3.0):
rg = np.random.default_rng(seed)
a1 = rg.uniform(0.0, 2 * np.pi, n)
a2 = rg.uniform(0.0, 2 * np.pi, n)
X = np.vstack([np.column_stack([np.cos(a1), np.sin(a1)]),
np.column_stack([r2 * np.cos(a2), r2 * np.sin(a2)])])
return X, np.r_[np.zeros(n), np.ones(n)]
def ari(a, b):
ua, ub = np.unique(a), np.unique(b)
tab = np.zeros((len(ua), len(ub)))
for i, x in enumerate(ua):
for j, y in enumerate(ub):
tab[i, j] = np.sum((a == x) & (b == y))
c2 = lambda z: z * (z - 1.0) / 2.0
sij, sa, sb = c2(tab).sum(), c2(tab.sum(1)).sum(), c2(tab.sum(0)).sum()
exp, mx = sa * sb / c2(float(len(a))), 0.5 * (sa + sb)
return float((sij - exp) / (mx - exp))
def kgauss(A, B, h):
return np.exp(-d2(A, B) / (2.0 * h * h))
def kk_val(Km, lab, K): # 核空间里的簇内平方和
tot = 0.0
for k in range(K):
m = lab == k
blk = Km[np.ix_(m, m)]
tot += float(np.diag(blk).sum() - blk.sum() / m.sum())
return tot
def kk_core(Km, K, lab, iters=60):
n = len(Km)
dg = np.diag(Km).copy()
for _ in range(iters):
cnt, s, q = np.zeros(K), np.zeros((n, K)), np.zeros(K)
for k in range(K):
m = lab == k
cnt[k] = max(m.sum(), 1)
s[:, k] = Km[:, m].sum(axis=1)
q[k] = Km[np.ix_(m, m)].sum() / cnt[k] ** 2
new = np.argmin(dg[:, None] - 2.0 * s / cnt[None, :] + q[None, :], axis=1)
if np.array_equal(new, lab):
break
lab = new
return lab
def kernel_kmeans(X, K, h=1.0, seed=0, n_init=1):
Km = kgauss(X, X, h)
best, bv = None, None
for r in range(n_init):
rg = np.random.default_rng(seed + r)
lab = kk_core(Km, K, rg.integers(K, size=len(X)))
v = kk_val(Km, lab, K)
if bv is None or v < bv:
best, bv = lab, v
return best
def plain_kmeans(X, K, seed=0): rg = np.random.default_rng(seed) C = X[rg.choice(len(X), K, replace=False)].astype(float) for _ in range(100): lab = np.argmin(d2(X, C), axis=1) newC = np.array([X[lab == k].mean(0) if (lab == k).any() else C[k] for k in range(K)]) if np.allclose(newC, C): break C = newC return np.argmin(d2(X, C), axis=1)
X, y = circles(200, 5)
print('K 均值 ARI %.4f' % ari(plain_kmeans(X, 2), y))
for h in (0.5, 0.8, 1.0, 1.4, 2.0, 3.0):
a = [ari(kernel_kmeans(X, 2, h, seed=s), y) for s in range(10)]
bb = ari(kernel_kmeans(X, 2, h, seed=0, n_init=10), y)
ok = sum(1 for v in a if v > 0.999)
print('h=%.1f 单次最小 %.3f 中位 %.3f 最大 %.3f 成功 %d/10 取优 %.3f'
% (h, min(a), np.median(a), max(a), ok, bb))
for h in (1.0, 2.0, 3.0):
Km = kgauss(X, X, h)
lab = kernel_kmeans(X, 2, h, seed=0, n_init=10)
print('h=%.1f 求得 %.2f 真标签 %.2f ARI %.3f'
% (h, kk_val(Km, lab, 2), kk_val(Km, y.astype(int), 2), ari(lab, y)))
实跑输出:
--------------------------------------------------------------------
同心圆:K 均值 ARI 0.005,核 K 均值换对带宽就 1.000
--------------------------------------------------------------------
数据 400 个二维点:内圈半径 1 共 200 点,外圈半径 3 共 200 点
任务 分成两簇,看分得对不对(ARI:1 完全一致,0 等于瞎猜)
K 均值的结果 ARI 0.0047
**K 均值在同心圆上必败,而且不是参数没调好——它一定能找到一条直线把平面切成两半。**
**两半里,每一半都同时含有内圈和外圈的点。ARI 0.005 说的就是「跟瞎猜一样」。**
核 K 均值换一个空间做同一件事:先算核矩阵,再在核空间里算「点到簇中心」的距离。
**注意代码里从头到尾没有真的算出任何一个高维坐标,只有 n×n 的核矩阵。**
带宽 h 从 0.5 扫到 3.0。每种带宽跑 10 个随机种子,再跑一次「十次取最优」:
带宽 h 单次最小 单次中位 单次最大 成功次数 十次取优
------------------------------------------------------------
0.5 -0.002 0.070 0.175 0/10 0.000
0.8 -0.002 0.101 1.000 4/10 1.000
1.0 -0.001 0.526 1.000 5/10 1.000
1.4 -0.002 1.000 1.000 6/10 1.000
2.0 -0.002 0.165 0.249 0/10 -0.002
3.0 -0.002 0.151 0.197 0/10 -0.002
**先看「成功次数」这一列:没有一种带宽能做到十次全对,最好的 1.4 也只有 6 次。**
**这就是这一节最该记住的一句:核 K 均值很容易卡在局部最优,所以工程上一定要多跑几次、按核目标取最好。**
**做这个动作之后,h 在 0.8 到 1.4 之间都能稳定拿到 1.000。**
两头都救不回来,但原因不一样:
h 太小(0.5)时核值只在自己身上还算大,核矩阵接近单位阵,
**这时「簇内平方和」几乎和怎么分无关——一行代码都不报错,跑一百次也没用。**
h 太大(2.0 以上)时核矩阵被拉平,退回到线性那套,怎么切都切不开。
这里有一个必须看清楚的现象。同样取十次最优,换带宽看目标值:
h=1.0 求得 / 真标签 核目标 278.90 / 278.90 ARI 1.000
h=2.0 求得 / 真标签 核目标 178.28 / 183.45 ARI -0.002
h=3.0 求得 / 真标签 核目标 105.79 / 126.59 ARI -0.002
**h 等于 1.0 时,求得的目标值 278.90 和真标签的一模一样——它找到的就是全局最优。**
**h 等于 3.0 时,求得的目标值比真标签的 126.59 还低,可 ARI 只有 -0.002。**
**这句话很反直觉但必须接受:目标函数降低了,划分反而更错。**
**带宽一旦不合适,目标函数衡量的就不是你想要的东西了——它自己变成了一个错误的标准,而且优化得越好,错得越离谱。**
最后对照一下初始化。h 取 1.0,随机初始化 vs 特征空间里的 ++ 初始化:
随机 初始化 ARI 最小 0.000 中位 0.400 最大 1.000
++ 初始化 ARI 最小 -0.001 中位 0.526 最大 1.000
**++ 比随机略好,但都好不到哪去——单次运行的正确率只有一半上下。**
**所以别指望换个聪明的初始化就一劳永逸,真正管用的还是「多跑几次、按目标函数挑最好」这个笨办法。**
**这也顺便说明了核方法的普遍毛病:核空间里代价地形比原始空间崎岖,局部最优多得多,所以「目标函数最小」这件事本身变得更难做对。**
第一行 0.0047 就是普通 K 均值的判决。 这个数意味着「和随机划分一样好」——不是差,是完全没有信息。 带宽表里最该看的是「成功」那一列:0/10、4/10、5/10、6/10、0/10、0/10。 最强的带宽也只有六成把握,这说明核 K 均值不能「跑一次看结果」,必须多跑几次按目标函数挑。 做完这一步,0.8 到 1.4 都能稳定拿到 1.000。 注意 h=0.5 那一行:单次中位 0.070,取优还是 0.000。 多跑救不回来,因为目标函数本身已经失去区分能力(核矩阵接近单位阵)。 这类失败要认出来,否则会误以为「再多跑几次就好了」。 最后三行是这一节的重头戏:h=1.0 时求得 278.90 与真标签 278.90 完全一致; h=3.0 时求得 105.79,比真标签的 126.59 还低,可 ARI 是负的。 目标函数说「我找到更好的解了」,而真相说「你错得更彻底了」。 所以核方法里「挑带宽」这件事,不能靠看目标函数——它一定会挑那个让它自己变小的值。 得靠外部信息:有标签就用交叉验证,没标签就只能靠对数据尺度的判断。
30 秒实验
- 把
circles的r2从 3.0 改成 1.5(两圈挨得很近)。 最优带宽会明显变小,而且成功率整体下降——两圈在核空间里也不容易分开了。 - 把
kernel_kmeans的n_init从 10 改成 1,跑一遍 h=1.0 的十个种子。 看「成功次数」变成多少次,体会「单次运行」为什么不可靠。 - 把
kk_core里的iters改成 2,只迭代两步。 ARI 会崩。这和 14.1 节「迭代次数要够」是同一件事,只是核空间收敛更慢。
常见误区
- 「核 K 均值是 K 均值的升级版,效果更好」。它换的是空间,代价是:每轮贵 n 倍、 多一个必须调对的带宽、而且局部最优更多(最强带宽也只有 6/10 次成功)。 数据本来就是球状时,用它纯属浪费。
- 「目标函数更小说明聚类更好」。 实测 h=3.0 时求得的目标值 105.79 比真标签的 126.59 还低,ARI 却是负的。 带宽错了,目标函数衡量的就不是你想要的东西,优化得越好错得越离谱。
- 「带宽随便试几个,取最好的」。「最好」按什么算?按目标函数算一定选错(见上一条)。 h 必须和数据尺度对上:这里两环间距是 2,最优在 1.0 到 1.4。换数据就得重新定,不能沿用。
- 「核 K 均值能给出簇中心,可以拿来解释」。 它没有显式中心——中心只存在于看不见的特征空间里。 能拿到的只有「每个点属于哪簇」。要解释,得回到原始空间算每簇的均值或中位数,那是另一回事。
自测三问(点开对答案)
1. K 均值在同心圆上失败,是优化失败还是表达失败? —— 表达失败。它用「到中心的距离」做判断,分界面必然是直线,而同心圆需要的是圆。 不管怎么初始化、跑多少次都不会对。 这和 14.1 节「初始化不好掉到 0.432」那种优化失败是两回事——那个能靠多跑救回来,这个不能。 2. 核 K 均值为什么一定要跑多次? —— 因为核空间的代价地形比原始空间崎岖,局部最优多。实测最强的带宽 1.4 也只有 6/10 次单跑成功。 跑多次按核目标取最好之后,0.8 到 1.4 才能稳定拿到 ARI 1.000。 3. 为什么 h=3.0 时目标函数更低、ARI 却是负的? —— 因为带宽太大时核矩阵被拉平,目标函数衡量的已经不是「簇内紧不紧」,而是一个和目标无关的量。 实测求得 105.79、真标签 126.59,目标函数认为前者更好,但 ARI 是负 0.002。 带宽选错,目标函数就是错的标准。核 K 均值放宽了「球」的形状,但没放宽「中心」这个东西——它仍然要求每簇有个代表。 下一节把「中心」整个扔掉,只在图上说「谁和谁连着」,于是连月牙这种没有中心的形状也能分。
- ☐ (本节完,下一节 14.3 14.3)