预处理改的是数据的摆放方式,不是数据本身。 这一节量了三件事:零均值化、标准化、白化,还量了它们的「条件数」。 实测结果和教材的口气不太一样:原始像素 0.9150,PCA 白化 0.9475,ZCA 白化反而只有 0.8788。 这份数据的条件数只有 357.7,本来就不算歪,没什么好掰的。 把数据人为放大 25 倍后条件数涨到 53087,原样喂只有 0.7275,只除标准差就回到 0.9225。 所以预处理的真正作用是「让学习率不敏感」,不是「提升精度」。
上一节改数据量和数据质量,这一节改数据的样子。 同样一批像素,缩放、平移、去相关之后,梯度在不同方向上被放大的倍数不一样, 训练起来难度天差地别。 这一节要引入一个量:条件数。 它把「数据歪不歪」从一个形容词变成一个数,后面就能拿它跟准确率放在同一张表里看关系。
一、什么叫「数据歪」
拿 64 个像素当 64 个坐标轴,训练数据就是空间里的一团点。 如果这团点在一个方向上摊得极开、在另一个方向上挤成一条线,就叫「歪」。 量它的办法是对协方差矩阵做特征分解,拿最大特征值除以最小特征值,得到条件数。 条件数越大,说明各方向尺度差得越悬殊。 为什么这件事要紧:梯度下降每走一步,在各个方向上走的距离是同一个学习率乘梯度。 尺度大的方向容易走过头,尺度小的方向半天挪不动。 条件数就是「这种不平衡有多严重」的度量。
二、四种处理,条件数各是多少
对同一批数据做四种处理,各自算条件数:
| 数据形态 | 条件数 |
|---|---|
| 原始像素 | 357.7 |
| 减均值 | 357.7 |
| 减均值再除标准差 | 251.4 |
| PCA 白化(降到 32 维) | 1.0 |
| ZCA 白化(保持 64 维) | 1.0 |
| 减均值一头一尾没变,因为它只是把整团点平移,形状没动。 | |
| 除标准差把每个坐标轴各自拉伸,条件数只从 357.7 降到 251.4——轴与轴之间的倾斜还在。 | |
| 两种白化都直接把条件数压到 1。 | |
| 这件事可以验算:ZCA 白化后协方差矩阵的非对角元绝对值最大只有 2.70e-05,而原始数据是 0.2213。 | |
| 协方差基本变成了单位阵,这就是「白化」这个词的字面意思。 |
三、可是准确率没跟着涨
条件数压到 1 了,下游任务应该变好吧?实测并不是:
| 输入 | 维度 | 测试准确率 |
|---|---|---|
| 原始像素 | 64 | 0.9150 |
| 减均值 | 64 | 0.9150 |
| 减均值再除标准差 | 64 | 0.9225 |
| PCA 白化 | 32 | 0.9475 |
| ZCA 白化 | 64 | 0.8788 |
| 原始 0.9150,ZCA 白化 0.8788,反而低了 3.6 个百分点。 | ||
| PCA 白化最高,可它是把 64 维压到 32 维,分数里有一部分是降维去噪挣的,不全算白化的功劳。 | ||
| ZCA 为什么输:白化要求每个方向的方差都变成 1,而那些特征值极小的方向本来就只装着噪声。 | ||
| 把噪声方向的方差拉成 1,等于给噪声发了个放大器。 |
四、把数据真的掰歪,白化才显出价值
上一张表说明这份数据不够歪。那就人为把它掰歪:每三个像素挑一个放大 25 倍。
| 数据形态 | 条件数 | 测试准确率 |
|---|---|---|
| 拉歪,原样喂 | 53087 | 0.7275 |
| 拉歪,只除标准差 | 251 | 0.9225 |
| 拉歪,再 ZCA 白化 | 1 | 0.8712 |
| 条件数从 357.7 涨到 53087,原样喂掉到 0.7275。 | ||
| 因为放大 25 倍的那些像素把 tanh 一把推进饱和区,导数接近 0,学不动。 | ||
| 这一局只除标准差就够用:0.9225,和没拉歪之前一模一样。 | ||
| 白化反而略低,还是那个原因——它顺手把噪声方向也放大了。 | ||
| 所以预处理不是越彻底越好,做到「不歪」就够,多做一步可能是负收益。 |
五、白化省的其实是调学习率
再换个角度看:固定数据形态,只扫学习率。
| 学习率 | 原始像素 | ZCA 白化 |
|---|---|---|
| 0.02 | 0.2525 | 0.6300 |
| 0.10 | 0.9050 | 0.8762 |
| 0.50 | 0.9463 | 0.8775 |
| 2.00 | 0.9563 | 0.8812 |
| 学习率 0.02 那一行,白化领先了三十八个百分点——原始数据根本推不动。 | ||
| 学习率给到 0.1 以上,原始数据追上并反超。 | ||
| 原因是白化把整体尺度压到了 1,同样写 0.1 的实际步长比原来小。 | ||
| 所以白化的价值可以这么表述:它让「学习率」这个超参数变得不敏感。 | ||
| 如果你愿意花时间慢慢调,它在不少任务上换不来分数。 |
六、动手 Demo:条件数、白化、以及那个会算出 1e8 的坑
数据 400 张训练、800 张测试,都用平移 1 格加 1 个噪点。
网络是 64 到 32 到 16 到 10,统一用学习率 0.1、1500 步——这样每一步的比较才是同一起跑线。
代码里 safe_std 这个函数不是凑数的,它是这次踩坑之后加的,稍后解释。
def safe_std(A):
u"""方差为 0 的维度不缩放:直接加 1e-8 会把「0 减 0」变成 1e8。"""
s = A.std(0)
return np.where(s > 1e-6, s, 1.0)
def cond(X):
u"""条件数:协方差最大特征值除以最小特征值。"""
lam = np.linalg.eigvalsh(np.cov(X.T))
return float(lam[-1] / max(lam[0], 1e-12))
def whiten(Xc, keep=None, eps=1e-6):
u"""白化:keep 给整数就是 PCA 白化,给 None 就是 ZCA 白化。"""
C = Xc.T @ Xc / len(Xc)
lam, U = np.linalg.eigh(C)
lam, U = lam[::-1], U[:, ::-1] # eigh 是升序,这里翻成降序
if keep is not None:
lam, U = lam[:keep], U[:, :keep] # 丢掉小特征值方向,顺带降维
P = U / np.sqrt(lam + eps)
else:
P = (U / np.sqrt(lam + eps)) @ U.T
Z = Xc @ P
return Z / np.sqrt((Z * Z).mean()), P, lam
mu, sd = Xtr.mean(0), safe_std(Xtr)
Z_tr, P_z, _ = whiten(Xtr - mu) # 白化矩阵只用训练集算
Z_te = ((Xte - mu) @ P_z) # 测试集必须套同一个矩阵
Z_te = Z_te / np.sqrt((Z_tr * Z_tr).mean())
实跑输出:
--------------------------------------------------------------------
预处理:减均值、标准化、白化,各自管什么
--------------------------------------------------------------------
[1] 预处理要解决的问题:不同像素的「量纲」和「相关性」不齐
64 个像素里,笔画点亮的像素几乎总是 1,空白像素几乎总是 0
有些像素几乎不变,有些像素跟着别的像素一起变
这会让梯度在不同方向上被放大或缩小得不一样,训练就走得歪
先量一下「歪」的程度。协方差矩阵的最大特征值除以最小特征值,
叫条件数:它越大,说明数据在一个方向上摊得极开,在另一个方向上挤成一团。
数据形态 条件数
原始像素 357.7
减均值 357.7
减均值除标准差 251.4
PCA 白化(32 维) 1.0
ZCA 白化(64 维) 1.0
减均值不改变条件数——它只是把整团数据平移,形状没变
除标准差只把每个坐标轴各自拉伸,轴之间的倾斜还在
白化才真正把协方差掰成单位阵,条件数掉到 1 附近
[2] 「掰成单位阵」这句话可以直接验算
ZCA 白化后,协方差矩阵对角元全在 1 附近,最大 1.002527,最小 1.002395
非对角元(也就是「像素之间还相关吗」)绝对值最大 2.70e-05
原始数据的非对角元绝对值最大 0.2213,白化后基本清零
[3] 但预处理好不好,要看它对下游任务有没有用
同一个网络(64-32-16-10)、同一个学习率 0.1、同样 1500 步,只换输入形态:
输入 维度 测试准确率
原始像素 64 0.9150
减均值 64 0.9150
减均值除标准差 64 0.9225
PCA 白化 32 0.9475
ZCA 白化 64 0.8788
这张表跟教材的口气不太一样:白化没有全面胜出
原始像素 0.9150,ZCA 白化 0.8788,反而低了 3.6 个百分点
PCA 白化最高,可它是把 64 维压到 32 维,分数里有一部分是降维去噪挣的
为什么 ZCA 会输:白化要把每个方向的方差都拉成 1,
那些特征值极小的方向本来就只有噪声,拉成 1 等于把噪声喂大
根子在条件数:这份数据只有 358,本来就不算歪,没什么好掰的
[4] 那把数据人为掰歪,白化的价值就露出来了
把每三个像素挑一个放大 25 倍,人为制造「各方向尺度悬殊」:
数据形态 条件数 测试准确率
拉歪,原样喂 53087 0.7275
拉歪,只除标准差 251 0.9225
拉歪,再 ZCA 白化 1 0.8712
条件数从 358 涨到 53087,原样喂进去掉了近二十个百分点
因为放大 25 倍的那些像素把 tanh 一把推进饱和区,导数接近 0,学不动
这一局 rescale 就够用:只除标准差回到 0.9225,跟没拉歪之前一模一样
白化反而略低——它把那些极小的特征值方向也拉成 1,顺手把噪声喂大了
所以「预处理有用」真正的前提是:数据本身歪,或者优化器步长没配好
数据不歪的时候,多做一步白化可能白忙,甚至会掉分
[5] 白化省的其实是「调学习率」这件事
固定数据形态,只扫学习率(都只跑 800 步):
学习率 原始像素 ZCA 白化
0.02 0.2525 0.6300
0.10 0.9050 0.8762
0.50 0.9463 0.8775
2.00 0.9563 0.8812
学习率 0.02 那一行,白化领先了三十多个百分点——原始数据根本推不动
学习率给到 0.1 以上,原始数据就追上并反超了
原因是白化把整体尺度压到了 1,同样写 0.1 的实际步长比原来小
结论:白化不是「提升精度」的偏方,它是「让学习率不敏感」的手段
如果你愿意慢慢调学习率,它在不少任务上换不来分数
[6] 代价与注意
白化矩阵是从训练集算出来的,测试集必须用同一个矩阵,不能自己再算一遍
减均值和除标准差也一样:mu 和 sd 只能来自训练集
这一步做错,测试集的信息就漏进训练过程,分数会虚高
另外 ZCA 白化的特征值里有极小的值,取倒数会放大噪声
所以要加一个小常数 eps 兜底,或者干脆只保留前 k 个主成分(PCA 白化)
下一节换激活函数:不改数据,只换门的形状
whiten 里那句「eigh 是升序,这里翻成降序」值得留意。
np.linalg.eigh 返回的特征值从小到大排,而 PCA 白化要保留的是最大的那几个,所以必须先反转,
否则保留的正好是要丢的。
safe_std 处理的是这篇最有价值的一个坑。
最直觉的写法是 sd = A.std(0) + 1e-8,防止除零。
可如果某个像素在训练集里恒为 0,它的标准差就是 0;
一旦增强时被平移点亮,(1 - 0) / 1e-8 就是 1e8。
这个值进网络,tanh 会把它压成 1,看着没事;换成 ReLU 就直接把整个网络冲垮。
最后两行的顺序也容易写错:白化矩阵必须只用训练集算,测试集套同一个矩阵。
如果测试集自己再算一遍,测试集的分布信息就漏进训练过程,分数会虚高——这就是常说的数据泄漏。
30 秒实验
- 把
whiten的eps从 1e-6 改到 1e-2 再跑。 ZCA 白化的准确率会明显回升——因为小特征值方向不再被放大到夸张。 - 把测试集的标准化改成「用测试集自己的均值和标准差」。 准确率会虚高几个点,但那是假的:真实场景里你拿不到测试集的分布。
- 把放大倍数从 25 改成 5 再跑「拉歪」那段。 条件数会小一个量级,原样喂的准确率也会好看很多,白化的优势随之缩小。
常见误区
- 「白化一定能提升精度。」 实测 ZCA 白化 0.8788,比原始像素的 0.9150 还低。 白化改的是「尺度」,不是「信息量」;数据本来不歪的时候,它只会顺手放大噪声。
- 「除标准差时加个 1e-8 就安全了。」
这次就踩了这个坑:恒为 0 的像素被
(1 - 0) / 1e-8算成 1e8,tanh 把它压住了, ReLU 直接崩。 正确做法是判断方差是否为 0,为 0 就不缩放,而不是加一个极小数。 - 「预处理就是把数据标准化,一行代码的事。」 标准化只处理「每个轴自己的尺度」,不处理「轴与轴之间的倾斜」。 这份数据上,除标准差把条件数从 357.7 降到 251.4,白化才降到 1。 要不要做到白化,得看数据歪到什么程度。
自测三问(点开对答案)
1. **为什么减均值不改变条件数?** —— **因为条件数看的是协方差矩阵的特征值之比,而协方差不随平移改变。** **把所有点减掉同一个向量,整团点只是挪了位置,形状一模一样。** 2. **白化把条件数压到 1,为什么准确率不一定涨?** —— **因为条件数影响的是「优化好不好走」,不是「数据里有多少信息」。** **学习率配得当时,优化本来就走得动;这时候白化只改了尺度,没加信息。** **只有当优化走不动时(学习率太小、数据太歪),白化才真正值钱。** 3. **`sd = std + 1e-8` 这种写法,什么时候会出事?** —— **当某一维的方差恰好是 0,而测试或增强数据在这一维上不是 0 的时候。** **除数接近 0,商就会爆炸。** **判断方差为 0 时直接不缩放,比加一个小数安全得多。**数据这一头讲完了。 下一节换网络内部:激活函数那个「门」的形状,决定梯度从后往前传时还剩多少。
- ☐ (本节完,下一节 6.3 激活函数)