f 转置乘 L 乘 f 等于「所有边上加权差的平方和的一半」。 实测两种算法给出同一个数:66.243718 对 66.243718。 但真方向并不是最平滑的方向:200 个随机方向里,有 132 个(66%)比它更平滑。 所以「让这一项最小」和「让分类正确」是两件不同的事。 反例最能说明问题:k 从 3 加到 150,平滑度从 0.1042 一路降到 0.0002, 准确率却从 0.9800 先升到 1.0000、再崩到 0.7500。
16.1 节说「要求相邻点的预测值接近」,16.2 节把它解出来了。这一节只回答一个问题: 这一项到底在算什么,它又保证不了什么。 先给结论:它算的是「图上所有边两端的预测差,按边权加权,再平方求和,最后取一半」。 这个量有个正式名字叫「图的狄利克雷能量」,几何上就是「函数的梯度平方的积分」的离散版。 这个解释很有用,因为它立刻告诉你一件事: 最小化它只能得到「变化最小的函数」,得不到「变化方向和标签一致的函数」。 两者在标签足够多的时候会重合,标签少的时候不会。
一、它在算相邻点的预测差
先看两个定义。 图的邻接矩阵 W 里,W 的第 i 行第 j 列是点 i 和点 j 之间的边权,没连边就是 0。 度矩阵 D 是对角阵,对角线上放每一行的和。 拉普拉斯矩阵 L 等于 D 减 W。 现在把预测值 f 当成定义在点上的函数,算 f 转置乘 L 乘 f:
f' L f = 二分之一 * 求和(所有 i, j) W[i][j] * ( f[i] - f[j] ) 的平方
这个等式可以直接验证。 取 f 等于「用真方向算出来的预测值」,在 8 近邻图上算两遍:
| 算法 | 结果 |
|---|---|
| 先算 L,再算 f 转置乘 L 乘 f | 66.243718 |
| 逐条边按权重加权差平方再求和 | 66.243718 |
| 两个数完全一致,这不是巧合,是恒等式。 | |
| 证明只有一行:把 f 转置乘 D 乘 f 展开成「每个点预测值的平方乘以它的度」, | |
| 把 f 转置乘 W 乘 f 展开成「每条边的两端预测值乘积乘以边权」,两者相减正好凑成完全平方。 | |
| 这个式子把「矩阵运算」翻译成了人话: | |
| 相邻两个点的预测值差得越多,这一项越大;边权越大(两点越近),差一点就罚得越重。 | |
| 所以在最小化它的过程中,模型被迫让挨得近的点给出相近的预测。 | |
| 反过来说,它对「预测值本身是多少」毫无意见。 | |
| 把所有点的预测值都乘上一个常数,这一项会跟着变成原来的平方倍; | |
| 把所有预测值都加上一个常数,这一项一点不变。 | |
| 这就是它只能约束形状、约束不了位置和方向的原因。 |
二、三种读法
同一个式子有三种理解方式,各有各的用处。 读法一:扩散。 把有标签的点看成热源,标签值沿着图的边走,越走越淡。 拉普拉斯正则化等价于让这个扩散过程跑到平衡——没有标签的点最终拿到的是邻居的加权平均。 这个读法最直观,也最接近「半监督」三个字给人的印象。 读法二:弹簧。 每条边是一根弹簧,边权是弹簧的硬度,弹簧的自然长度是零——它希望两端的预测值相等。 有标签的点被钉子钉在标签值上。 最小化总弹性势能,就是让整张网在「钉子和弹簧」之间找平衡。 lam 控制弹簧整体有多硬:lam 大,网被拉得很平;lam 小,钉子说了算。 读法三:离散的梯度。 连续世界里,「函数变化有多剧烈」用梯度的平方积分衡量:积分 |梯度 f| 的平方。 把这个积分换成图上的求和,就得到 f 转置乘 L 乘 f。 所以 L 是「二阶差分算子」在图上的版本,和 13.5 节拉普拉斯特征映射里那个 L 是同一个东西。 三种读法指向同一个结论:它衡量的是「f 在图上有多不平滑」。 没有一个读法能说明「f 的方向对不对」。 这不是解释得不够,是这个量本身就不含方向信息。
三、它保证不了什么
这一节最重要的数字在这里:
| 方向 | 平滑度 |
|---|---|
| 真标签方向 | 66.2437 |
| 200 个随机方向的中位数 | 43.8687 |
| 200 个随机方向的最小值 | 2.4559 |
| 200 个随机方向的最大值 | 377.7483 |
| 真方向的平滑度是 66.2437,而随机方向的中位数只有 43.8687——也就是说随便扔一个方向, | |
| 一半以上的概率比真方向更平滑。 | |
| 具体数出来:200 个里有 132 个更平滑,占 66%。 | |
| 所以如果你只按「平滑度最小」去挑方向,有三分之二的概率挑到错的。 | |
| 这解释了 16.1 里那个奇怪的现象:每类只给 1 个标签时,怎么调都没用。 | |
| 因为那时候标签项几乎不起作用,模型基本只按平滑度挑方向, | |
| 而平滑度跟标签方向之间只有三分之一的吻合概率。 | |
| 标签够多的时候为什么就行了? | |
| 因为标签项把方向钉住了,平滑项退化成「在这个方向附近做微调」。 | |
| 两个目标不再冲突,而是分工:标签定方向,几何定细节。 |
四、反例:越平滑,越不准
把 k 从 3 加到 150,固定 lam 等于 10:
| k | 准确率 | 平滑度 | 近邻同类比例 |
|---|---|---|---|
| 3 | 0.9800 | 0.1042 | 0.9783 |
| 8 | 1.0000 | 0.0247 | 0.9500 |
| 20 | 0.9750 | 0.0046 | 0.9130 |
| 80 | 0.8500 | 0.0004 | 0.7454 |
| 150 | 0.7500 | 0.0002 | 0.5845 |
| 第三列从 0.1042 一路降到 0.0002,降了五百倍——按「越小越好」的逻辑,k 等于 150 应该是最优的。 | |||
| 可第二列显示它最差:0.7500,还不如 k 等于 3 的 0.9800。 | |||
| 原因在第四列。 | |||
| k 等于 150 时,每个点连了 150 个邻居(总共才 200 个), | |||
| 近邻同类比例掉到 0.5845——差不多一半的邻居是另一类。 | |||
| 这时图里的边大量跨类,「要求两端预测接近」等于「要求把两类混在一起」。 | |||
| 模型非常听话地把预测值拉平了,平滑度降到了 0.0002,代价是把两类也拉平了。 | |||
| 这就是这一节的核心教训: | |||
| 正则项只是把「你相信什么」翻译成数学,它不判断「你信的对不对」。 | |||
| 你相信「挨着就同类」,它就按这个信念去平滑; | |||
| 如果这个信念在数据上不成立,它会把错误平滑得非常漂亮。 | |||
| 一句话总结这一节: | |||
| 拉普拉斯正则化等于「在图上惩罚相邻点的预测差」,它把无标签数据的形状翻译成一个可优化的量, | |||
| 但方向必须由标签提供——标签不够,几何再好也只能挑出形状,挑不出答案。 |
五、动手 Demo:这一项到底在惩罚什么:相邻点的预测值之差
这段代码先验证「f 转置乘 L 乘 f」与「逐边加权差平方和的一半」是同一个数; 再拿真方向和 200 个随机方向比平滑度; 最后把 k 从 3 加到 150,看平滑度和准确率怎么走向两个相反的方向。 重点看三处:第一段两个 66.243718;第二段那个「132 个更平滑」; 以及第三段里平滑度降五百倍、准确率却从满分掉到 0.7500。
import numpy as np
def manifold_data(n=200, d=30, seed=2, noise=0.05):
"""200 个点躺在 30 维空间里的一个二维流形上,标签由线性方向决定。"""
rg = np.random.default_rng(seed)
z = rg.normal(0.0, 1.0, (n, 2))
A = rg.normal(0.0, 1.0, (d, 2))
X = z @ A.T + rg.normal(0.0, noise, (n, d))
v = rg.normal(0.0, 1.0, d)
v = v / np.linalg.norm(v)
y = np.where(X @ v > 0.0, 1.0, -1.0)
return X, y, v
def pick_labels(y, per=5, seed=0):
"""每类随机挑 per 个下标当有标签样本。"""
rg = np.random.default_rng(seed)
lab = []
for c in np.unique(y):
idx = np.where(y == c)[0]
lab.extend(rg.choice(idx, per, replace=False).tolist())
return np.array(sorted(lab))
def graph(X, k=8):
"""k 近邻图:W 邻接矩阵,L 等于度矩阵减 W。"""
n = len(X)
D2 = np.maximum(((X[:, None, :] - X[None, :, :]) ** 2).sum(-1), 0.0)
np.fill_diagonal(D2, np.inf)
idx = np.argsort(D2, axis=1)[:, :k]
near = np.take_along_axis(D2, idx, axis=1)
s2 = float(np.median(near))
W = np.zeros((n, n))
W[np.repeat(np.arange(n), k), idx.ravel()] = np.exp(-near.ravel() / (2.0 * s2))
W = np.maximum(W, W.T)
return W, np.diag(W.sum(axis=1)) - W
def lap_rls(X, y, lab, lam=1.0, k=8):
"""拉普拉斯正则化最小二乘:标签项加上图上相邻点要接近。"""
W, L = graph(X, k)
Xl, yl = X[lab], y[lab]
return np.linalg.solve(Xl.T @ Xl + lam * (X.T @ L @ X), Xl.T @ yl)
def acc(w, X, y):
return float(np.mean(np.sign(X @ w) == y))
def purity(X, y, k=8):
"""每个点的 k 个近邻里同类的比例,用来看流形假设成不成立。"""
D2 = np.maximum(((X[:, None, :] - X[None, :, :]) ** 2).sum(-1), 0.0)
np.fill_diagonal(D2, np.inf)
idx = np.argsort(D2, axis=1)[:, :k]
return float((y[idx] == y[:, None]).mean())
def smooth(f, W):
"""二分之一乘上所有边上加权差的平方。"""
d = f[:, None] - f[None, :]
return 0.5 * float((W * d * d).sum())
X, Y, V = manifold_data(200, 30, 2)
LAB = pick_labels(Y, 5, seed=0)
W, L = graph(X, 8)
F = X @ V
print('[1] 正则项到底在算什么:两种算法必须给出同一个数')
print(' f 取真方向时,f 转置乘 L 乘 f %.6f' % float(F @ L @ F))
print(' 二分之一乘上逐边加权差平方 %.6f' % smooth(F, W))
print(' 图为 %d 个点、%d 条有向边' % (len(X), int((W > 0).sum())))
print('')
print('[2] 标签方向并不是最平滑的方向')
print(' 沿真方向的平滑度 %.4f' % smooth(F, W))
rg = np.random.default_rng(0)
vals = []
for _ in range(200):
r = rg.normal(0.0, 1.0, 30)
r = r / np.linalg.norm(r)
vals.append(smooth(X @ r, W))
vals = np.array(vals)
print(' 200 个随机方向:中位 %.4f 最小 %.4f 最大 %.4f'
% (float(np.median(vals)), vals.min(), vals.max()))
print(' 其中比真方向更平滑的有 %d 个,占 %.0f%%'
% (int((vals < smooth(F, W)).sum()), 100.0 * (vals < smooth(F, W)).mean()))
print(' 所以「最平滑」不等于「对标签」——正则项只能挑形状,挑不了方向')
print('')
print('[3] 反例:越平滑,反而越不准(lam=10)')
for k in (3, 8, 20, 80, 150):
Wk, Lk = graph(X, k)
w = lap_rls(X, Y, LAB, lam=10.0, k=k)
print(' k=%-3d 准确率 %.4f 平滑度 %8.4f 近邻同类比例 %.4f'
% (k, acc(w, X, Y), smooth(X @ w, Wk), purity(X, Y, k)))
实跑输出:
--------------------------------------------------------------------
这一项到底在惩罚什么:相邻点的预测值之差
--------------------------------------------------------------------
[1] 正则项到底在算什么:两种算法必须给出同一个数
f 取真方向时,f 转置乘 L 乘 f 66.243718
二分之一乘上逐边加权差平方 66.243718
图为 200 个点、1994 条有向边
[2] 标签方向并不是最平滑的方向
沿真方向的平滑度 66.2437
200 个随机方向:中位 43.8687 最小 2.4559 最大 377.7483
其中比真方向更平滑的有 132 个,占 66%
所以「最平滑」不等于「对标签」——正则项只能挑形状,挑不了方向
[3] 反例:越平滑,反而越不准(lam=10)
k=3 准确率 0.9800 平滑度 0.1042 近邻同类比例 0.9783
k=8 准确率 1.0000 平滑度 0.0247 近邻同类比例 0.9500
k=20 准确率 0.9750 平滑度 0.0046 近邻同类比例 0.9130
k=80 准确率 0.8500 平滑度 0.0004 近邻同类比例 0.7454
k=150 准确率 0.7500 平滑度 0.0002 近邻同类比例 0.5845
第一段只有三行,但它是整节的地基。 f 转置乘 L 乘 f 是 66.243718, 逐边加权差平方和也是 66.243718——两个完全独立的算法给出同一个数,说明这个恒等式成立。 1994 条有向边,每条都贡献一点点,加起来正好是这个数。 第二段是这一节最反直觉的地方。 真方向的平滑度 66.2437,看着挺小;但 200 个随机方向的中位数是 43.8687,比它更小。 数出来有 132 个更平滑,占 66%。 所以「平滑」这件事和「对标签」之间没有必然联系——这次是三分之一的吻合率。 想想这意味着什么:如果只有两个标签,模型基本就是在这 200 个方向里挑最平滑的, 那它有三分之二的概率挑错。 16.1 里那片 0.625 到 0.655 就是这么来的。 第三段五行,第三列和平滑度有关,第二列和准确率有关。 k 等于 3 时平滑度 0.1042,k 等于 150 时 0.0002——降了五百倍。 而准确率是 0.9800、1.0000、0.9750、0.8500、0.7500,先升后崩。 第四列的近邻同类比例 0.9783 到 0.5845 是解释。 k 等于 150 时大部分边都跨类,图在说谎,而正则项老老实实照着谎言平滑。 这也是半监督最常见的翻车方式:不是算法不收敛,是图建错了。
30 秒实验
- 把
[2]里的200改成2000,重跑。 样本多了之后,「比真方向更平滑的比例」会更稳定地接近某个值。 看它是多少——这个比例就是「只按平滑度挑方向」的失败率。 - 把
[3]里的lam从 10 改成 1000,重跑。 lam 越大平滑项越强势,k 等于 150 那一行会更难看。 如果连 k 等于 3 都掉下来,说明 lam 已经压过标签项了。 - 把
smooth里的f换成X @ w,其中w用lap_rls解出来的那个。 这算的是「最终解在图上有多平滑」。 和真方向的 66.2437 比一比——差多少,就是模型为了平滑牺牲掉的东西。
常见误区
- 「拉普拉斯正则化会让预测更平滑,所以一定更准」。 反例就在本节:k 从 8 加到 150,平滑度从 0.0247 降到 0.0002, 准确率却从 1.0000 掉到 0.7500。 平滑是手段,不是目的。
- 「平滑度最小的方向就是对标签的方向」。 实测 200 个随机方向里有 132 个比真方向更平滑。 真方向的平滑度 66.2437 甚至高于随机方向的中位数 43.8687。 这个正则项只含形状信息,不含方向信息。
- 「这一项等价于核方法里的某个核,所以有同样的理论保证」。 确实可以写成核形式,但理论保证的前提是「图正确反映相似性」。 k 等于 150 时这个前提被破坏,理论上的保证一条都不成立。
- 「f 转置乘 L 乘 f 是近似算法,逐边求和才是精确的」。 两个是恒等的,实测都是 66.243718,差在小数点后六位之后。 恒等式的证明只有一行:把 f 转置乘 D 乘 f 与 f 转置乘 W 乘 f 相减,凑成完全平方。
自测三问(点开对答案)
1. f 转置乘 L 乘 f 在图上具体等于什么? —— 等于二分之一乘上「所有边上两端预测值之差的平方,按边权加权」的总和。 实测两种算法给出同一个数 66.243718。所以最小化它就等于要求挨得近的点给出相近的预测。 2. 为什么说这个正则项只约束形状、不约束方向? —— 因为它对预测值整体加一个常数完全不变,对整体乘一个常数只按平方缩放——它只看「相对变化」, 不看「绝对位置」。 实测 200 个随机方向里有 132 个比真标签方向更平滑,所以「最平滑」和「对标签」没有必然联系。 3. k 太大为什么会让准确率崩溃? —— 因为每个点连的邻居太多,边开始跨类。 实测 k 等于 150 时近邻同类比例只有 0.5845, 这时候「要求两端预测接近」等于「要求把两类混在一起」。 模型照做了:平滑度降到 0.0002,准确率也降到 0.7500。第 16 章三节讲完了「怎么用没标签的数据」。 下一章换一个方向:如果标签一直有,但维数太高、噪声维度太多,能不能让标签反过来帮你降维?
- ☐ (本章完,下一节 17.1 17.1)