案例拆解:同一网络换六种配置的对照实验
理论篇拆开了四种失败机制。这一篇把它们放进同一个网络、同一批数据、同一个随机种子里,每次只动一个变量,看每个机制到底值多少个百分点的验证准确率。
先说结论:八个轮次的预算下,单变量收益最大的是 Adam(+3.53),其次是 BatchNorm(+2.91)、输入标准化(+2.25),He 初始化几乎无感(+0.34),正则单独用甚至是负收益(−0.29)。但这不是「配置排行榜」,而是「在 8 轮、这个网络、这个优化器下」的排序。任何一项换掉预算,排序都会变。
一、控制变量:把六次实验压成一张表
网络固定为 784 → 128 → 64 → 10,109,386 个参数。数据固定为 MNIST 50,000 张训练、10,000 张验证,batch=64,每轮 781 步。随机种子 20260815,40 轮的 batch 顺序预先生成,所有配置共用同一份。
配置本身是一个扁平字典,每次只改一个键:
base = {
"sizes": [784, 128, 64, 10],
"act": "relu",
"init": "xavier",
"input": "raw", # C1 改成 "std"
"norm": None, # C3 改成 "bn"
"dropout": 0.0, # C5 改成 0.2
"opt": "sgd", # C4 改成 "adam"
"lr": 0.01, # C4 改成 1e-3
"wd": 0.0, # C5 改成 1e-4
"epochs": 8,
}batch 顺序也共用:
rng = np.random.default_rng(20260815)
epochs_idx = [rng.permutation(50000) for _ in range(40)]
# 每个配置的第 ep 轮都用 epochs_idx[ep],不重新抽样为什么固定 batch 顺序:如果每个配置各自 shuffle,C0 与 C4 看到的梯度序列就不一样,8 轮的差异里混进了数据顺序的噪声。固定之后,六条曲线的前后差异只剩配置本身。这也意味着所有实验都只有一个种子,结论不能拿来做显著性检验——本章的用途是看量级,不是争 0.02 个百分点。
基线 C0 选的是一组「教科书默认」:原始 0–1 像素、Xavier 初始化、SGD 学习率 0.01、不加 BN、不加 Dropout。它不是故意设得很差——SGD 0.01 在这个网络上 8 轮能到 93.75%,是一个正常起点。

<!-- 配图:六行配置表,每行标注相对 C0 的唯一改动、8 轮验证 acc 条形与增量 -->
| 配置 | 相对 C0 的唯一改动 | 参数 | 8 轮 val acc | Δ |
|---|---|---|---|---|
| C0 | 基线:原始像素 + Xavier + SGD 0.01 | 109,386 | 93.75% | — |
| C1 | 输入标准化 | 109,386 | 96.00% | +2.25 |
| C2 | He 初始化 | 109,386 | 94.09% | +0.34 |
| C3 | 每层加 BatchNorm | 109,790 | 96.66% | +2.91 |
| C4 | Adam 1e−3 | 109,386 | 97.28% | +3.53 |
| C5 | Dropout 0.2 + Weight Decay 1e−4 | 109,386 | 93.46% | −0.29 |
C3 的参数多了 404 个。这不是误差:128 + 64 + 10 = 202 个输出维度,每个 BN 有 γ、β 两个参数,202 × 2 = 404。注意输出层也建了 BN——这个选择后面还会出现,最终模型也是这么建的。
这张表最重要的信息不是排序,而是每一行的 Δ 都只对应一个改动。C1 只改输入处理,C2 只改初始化,C3 只加 BN,C4 只换优化器,C5 只加正则。没有「C4 = C1 + C3 + Adam」这种叠加行,因为一旦叠加,就说不清收益归谁。
一个几乎无感的变量:He 初始化
C2 只涨了 0.34 个百分点,和 C0 的 93.75% 几乎贴在一起。第一反应是「He 没用」,但对照第一层的激活标准差:
| 配置 | 第 1 轮 act std(层 1 / 层 2) | 第 8 轮 act std |
|---|---|---|
| C0 Xavier | 0.401 / 0.685 | 0.482 / 0.913 |
| C2 He | 0.399 / 0.726 | 0.480 / 0.941 |
| C1 标准化 + Xavier | 0.967 / 1.197 | 1.156 / 1.481 |
C0 和 C2 的激活尺度几乎一模一样。原因是 C0 的输入还是原始 0–1 像素,整体尺度偏小,Xavier 与 He 的方差差异(2/912 对 2/784,约 1.16 倍)被压住了。C1 把输入标准化之后,同一层的激活标准差直接抬到 0.967,He 与 Xavier 的差异才显出来(理论篇的 0.662 对 0.614)。
踩坑记录
- 现象:C2「He 初始化」相对 C0 只提升 0.34 个百分点,看起来像噪声。
- 原因:输入仍是原始 0–1 像素,信号整体偏小;初始化的方差差异被输入尺度掩盖,第一层激活 std 只从 0.401 变到 0.399。
- 修复:先统一输入尺度,再比较初始化。标准化之后,He 的第一层激活 std 到 0.662,Xavier 是 0.614,差异才可读。
这条的顺序很重要:先归一化,再谈初始化。反过来做,会得出「初始化不重要」的错误结论。
二、八轮曲线:差距主要在开局

<!-- 配图:六条验证 acc 曲线,C0 灰、C1 黄、C2 紫、C3 绿、C4 蓝、C5 红;右侧最终值面板 -->
| 配置 | 第 1 轮 val acc | 第 8 轮 val acc | 8 轮 val loss |
|---|---|---|---|
| C0 | 87.51% | 93.75% | 0.2270 |
| C1 | 91.08% | 96.00% | 0.1377 |
| C2 | 88.07% | 94.09% | 0.2142 |
| C3 | 91.67% | 96.66% | 0.1357 |
| C4 | 95.29% | 97.28% | 0.1075 |
| C5 | 86.49% | 93.46% | 0.2246 |
C4 第 1 轮就到 95.29%,C0 第 1 轮只有 87.51%。八轮结束时 C4 97.28%、C3 96.66%、C1 96.00%,三条曲线都还没平——它们不是「收敛到不同的天花板」,而是「以不同速度爬同一座山」。8 轮的排名,本质是速度排名。
把 C0 与 C4 逐轮摊开:
| 轮次 | C0 train acc | C0 val acc | C4 train acc | C4 val acc |
|---|---|---|---|---|
| 1 | 76.32% | 87.51% | 91.70% | 95.29% |
| 2 | 89.42% | 90.19% | 96.45% | 96.50% |
| 3 | 90.90% | 91.13% | 97.44% | 96.91% |
| 4 | 91.69% | 91.70% | 98.13% | 96.91% |
| 5 | 92.32% | 92.36% | 98.44% | 97.01% |
| 6 | 92.89% | 92.72% | 98.84% | 97.30% |
| 7 | 93.26% | 93.10% | 98.97% | 97.37% |
| 8 | 93.77% | 93.75% | 99.18% | 97.28% |
C4 从第 3 轮开始 val acc 就进入 96.9–97.3% 的窄带,训练 acc 却从 97.44% 一路涨到 99.18%;C0 则两条线几乎贴在一起。同样是「还没收敛」,C4 是「验证先到顶、训练继续拟合」,C0 是「两边都还在爬」。这两种「没收敛」对应的下一步动作完全不同。
C5 的曲线最慢:第 1 轮 86.49%,比 C0 还低 1.02。Dropout 0.2 在训练时随机丢掉 20% 的激活,前几轮有效容量更小;Weight Decay 又把权重往 0 拉。单独加正则、不加容量和轮数,收益自然是负的。
把增量拆开看:

<!-- 配图:五根横向条形,从 −0.29 到 +3.53,0 处有竖线;底部注明收益不可相加 -->
单变量收益排序:Adam +3.53 > BatchNorm +2.91 > 输入标准化 +2.25 > He +0.34 > Dropout+WD −0.29。
但这些数字不能相加。2.25 + 2.91 + 3.53 = 8.69,如果真能叠加,C0 的 93.75% 应该变成 102.44%——不可能。真实情况是:同一个变量在不同组合里的边际收益会迅速变小。输入标准化在 SGD 0.01 下值 +2.25;换成 Momentum 0.01、He 初始化时,原始像素 97.29%、标准化 97.41%,只值 +0.12。BN 在 SGD 下的 8 轮增益是 +2.91,在 Momentum 下第 8 轮只差 +0.53,但到 97% 的轮次从 7 降到 3。同一个变量换了优化器,收益的量纲就变了。
最终配置验证 98.46%,比 C0 高 4.71 个百分点,但那个模型换了宽度(256/128)、换了轮数(30 轮)、加了早停。它只能说明「这套组合能到 98.46%」,不能拿来验证「单变量收益可以相加」。
设计取舍:8 轮还是 30 轮
| 方案 | 收益 | 代价 |
|---|---|---|
| A:8 轮快照 | 六次实验几分钟跑完,差异集中在「前几轮速度」,适合看优化器与归一化 | 排名被预算绑死:慢配置可能只是没跑够,C0 在 8 轮时仍明显欠拟合 |
| B:30 轮跑满 | 更接近最终性能,排名更稳 | 六配置 × 30 轮的时间成倍增长;过拟合会混进来,单变量不再干净 |
这一篇选 A。它回答的是「哪个变量能更快把 loss 压下去」,不是「哪个配置最终最强」。最终强度留给实战篇的 30 轮。
三、学习率扫描:平台很宽,悬崖很陡
固定 Adam,只扫学习率,从 1e−4 到 1e−1 取 8 个点,每个训 6 轮。

<!-- 配图:上图 acc 随 lr 变化,下图 val loss;3e−2 起红色悬崖区,1e−3 绿色虚线 -->
| 学习率 | 6 轮 val acc | 6 轮 val loss |
|---|---|---|
| 1e−4 | 96.03% | 0.1312 |
| 3e−4 | 97.00% | 0.0986 |
| 1e−3 | 97.02% | 0.1149 |
| 2e−3 | 95.97% | 0.1543 |
| 3e−3 | 96.23% | 0.1492 |
| 1e−2 | 94.72% | 0.2263 |
| 3e−2 | 89.72% | 0.3913 |
| 1e−1 | 16.94% | 1.9158 |
平台在 3e−4 到 1e−3:97.00% 和 97.02%,差 0.02 个百分点。1e−4 还没跑够,6 轮只有 96.03%;2e−3 开始掉,1e−2 还有 94.72%,到 1e−1 只剩 16.94%,loss 1.9158——模型基本把输出推成了错误类的极端值。
平台内部 0.02 个百分点的差异不值得追。单种子、6 轮、尾批丢弃,这些都会带来同量级的波动。调参的正确粒度是先定数量级(1e−3 还是 1e−2),再在平台里选一个,而不是在小数点后两位上做文章。
四、优化器 × 学习率:没有通用组合
同一个 6 轮预算,三种优化器 × 四个学习率:

<!-- 配图:3×4 热力网格,每格是 6 轮 val acc,蓝框标每行最佳;红差绿好 -->
| 优化器 | lr=1e−4 | lr=1e−3 | lr=1e−2 | lr=1e−1 |
|---|---|---|---|---|
| SGD | 72.53% | 90.11% | 95.56% | 97.13% |
| Momentum | 90.20% | 95.60% | 97.17% | 94.07% |
| Adam | 96.03% | 97.02% | 94.72% | 16.94% |
三行的最佳值只差 0.15 个百分点(97.17 / 97.13 / 97.02),但它们要的学习率差了两个数量级。SGD 的最佳点在 0.1,Momentum 在 0.01,Adam 在 1e−3。
这张表还暴露了「同一档学习率下谁更好」的陷阱:如果只看 1e−3,Adam 的 97.02% 远高于 SGD 的 90.11%,结论会是「Adam 完胜」;如果只看 0.1,SGD 的 97.13% 又高于 Adam 的 16.94%,结论会反过来。优化器和学习率必须一起选,单独比较任何一个都没有意义。
SGD 在 1e−4 只有 72.53%,因为每步更新的相对幅度太小,6 轮走不完;把 lr 放大 1000 倍到 0.1,它才到 97.13%。代价是再往上到 1.0 就 NaN(理论篇图①)。Adam 则在 1e−3 就到位,但对上界更敏感:1e−2 掉到 94.72%,1e−1 直接 16.94%。
「相对更新幅度」可以直接量:每一步结束时,参数变化量的范数除以更新前的权重范数。C0 与 C4 在各自的最佳学习率下:
| 配置 | 第 1 轮(层 1 / 层 2 / 层 3) | 第 8 轮 |
|---|---|---|
| C0:SGD 0.01 | 0.063% / 0.076% / 0.119% | 0.066% / 0.084% / 0.121% |
| C4:Adam 1e−3 | 0.291% / 0.136% / 0.082% | 0.220% / 0.138% / 0.067% |
学习率差了 10 倍(0.01 对 1e−3),实际相对更新只差约 3 倍。因为 Adam 的分母 √v̂ 把梯度尺度吃掉了,lr 不再直接等于步长比例;SGD 的步长则完全由 lr 和梯度范数决定。这也解释了为什么两个优化器不能共用同一套学习率。
五、BN 前后:把「到 97%」从 7 轮压到 3 轮
固定 Momentum 0.01、He 初始化、8 轮,只改输入与层间归一化。

<!-- 配图:raw / std / bn 三条验证 acc 曲线,绿色 97% 水平线,标出各自到达轮次 -->
| 输入处理 | 到 97% 的轮次 | 第 8 轮 val acc | 第 8 轮 val loss |
|---|---|---|---|
| 原始像素 | 7 | 97.29% | 0.0967 |
| 输入标准化 | 4 | 97.41% | 0.0995 |
| 标准化 + BN | 3 | 97.82% | 0.0753 |
第 1 轮差距更明显:原始 93.67%、标准化 95.61%、加 BN 96.10%。到第 8 轮三者只差 0.53 个百分点,因为 8 轮足够让慢配置追上来。BN 的价值主要在「前几轮」和「稳」:它让第 1 轮就接近 96%,而不是从 93.67% 慢慢爬。
BN 真正管的是层间尺度漂移:

<!-- 配图:两层三组柱:初始化 / 无 BN / 有 BN,比较激活标准差 -->
| 层 | 初始化 | 训练 8 轮(无 BN) | 训练 8 轮(有 BN) |
|---|---|---|---|
| 第 1 层 | 0.662 | 1.489 | 0.552 |
| 第 2 层 | 0.580 | 2.104 | 0.574 |
没有 BN 时,第 2 层的激活标准差从 0.580 涨到 2.104,是第 1 层 1.489 的 1.41 倍;网络越深,这个倍率越会滚雪球。BN 每层重新归一化,第 1 层 0.552、第 2 层 0.574,基本不随训练漂移。
代价也在表里:这个网络加 BN 多 404 个参数(C3 的 109,790 对 109,386),每个 BN 层训练时要算当前 batch 的均值方差,推理时要维护 running stats;batch 小到 8 或 16 时,统计量本身就不稳。
六、过拟合曲线:第 4 轮见底,之后在记噪声
换一个更大的网络:784 → 512 → 256 → 10,535,818 个参数,30 轮,无正则。

<!-- 配图:训练 loss 蓝色一路降、验证 loss 红色先降后升,ep4 绿色虚线,末端 gap 标注 -->
| 指标 | 数值 |
|---|---|
| 训练 loss | 0.0114 |
| 验证 loss | 0.1753 |
| 训练 acc | 99.70% |
| 验证 acc | 97.87% |
| 测试 acc | 97.96% |
| 验证 loss 最低轮次 | 第 4 轮,0.1015 |
第 4 轮验证 loss 见底,之后从 0.1015 回升到 0.1753;训练 loss 还在从 0.0465 降到 0.0114。两条线在第 4 轮之后分叉,gap 从 0.055 拉到 0.164。这不是「模型变差了」——验证 acc 从 97.30% 涨到 97.87%,测试 97.96%——而是「模型对错误样本越来越自信」,loss 把这些自信的错误算了进去。
三种正则的对照:

<!-- 配图:无正则 / Dropout 0.3 / WD 1e−2 三条验证 loss,右侧测试 acc 面板 -->
| 配置 | 验证 loss | 验证 acc | 测试 acc |
|---|---|---|---|
| 无正则 | 0.1753 | 97.87% | 97.96% |
| Dropout 0.3 | 0.0840 | 98.18% | 98.27% |
| Weight Decay 1e−2 | 0.1448 | 96.03% | 96.37% |
Dropout 0.3 把验证 loss 压到 0.0840,测试 98.27%,是三种里最好的;Weight Decay 1e−2 把训练 loss 停在 0.145(无正则只有 0.0114),测试反而掉到 96.37%——正则太强,模型连训练集都拟合不好。
设计取舍:Dropout 0.3 还是 Weight Decay 1e−2
| 方案 | 收益 | 代价 |
|---|---|---|
| A:Dropout 0.3 | 测试 98.27%,验证 loss 0.0840;训练时随机丢单元,逼网络学冗余特征 | 训练时间变长;推理要切换 eval(),否则结果抖动 |
| B:Weight Decay 1e−2 | 实现最简单,任何优化器都自带 weight_decay | 强度稍大就欠拟合;本实验掉到 96.37%,比无正则还低 1.59 |
本章主线选 A,但最终模型把 Dropout 降到 0.2,Weight Decay 用 1e−4——只轻轻压,不伤拟合。强度靠验证集选,不靠默认值。
七、激活与初始化:差异只在最前面几轮

<!-- 配图:左:四种激活前 4 轮;右:四种初始化 10 轮;底部图例带最终值 -->
| 激活 | 第 1 轮 val acc | 10 轮 val acc |
|---|---|---|
| ReLU | 95.50% | 97.23% |
| LeakyReLU 0.01 | 95.46% | 97.23% |
| Sigmoid | 93.42% | 97.28% |
| Tanh | 94.99% | 97.11% |
第 1 轮 ReLU 比 Sigmoid 高 2.08 个百分点,10 轮后两者只差 0.05,Sigmoid 甚至略高。浅网络 + Adam 把激活的梯度差异抹平了。这不是说激活不重要,而是说:两层网络上选激活,收益主要体现在前几轮;层数一多,Sigmoid 的 0.25 上限才会变成硬伤。
初始化的差距则是硬的:
| 初始化 | 10 轮 val acc |
|---|---|
| 零 | 11.20% |
| 小随机 0.01 | 97.62% |
| Xavier | 97.38% |
| He | 97.23% |
零初始化 10 轮都是 11.20%,不是「学得慢」,是「根本没学」。其余三种都在 97.2–97.6%,差异很小——只要方差量级对、对称性被打破,浅网络对初始化并不挑。小随机 0.01 反而最高,因为它配合 ReLU 也足够打破对称,而这个网络的宽度不需要精确的方差保持。
八、实验边界:这些结论不能往外推太远
这一篇的十张图都建立在一组有限条件上,读结论时要带着边界:
| 边界 | 具体设定 | 影响 |
|---|---|---|
| 数据集 | 只用 MNIST,固定 50k / 10k 划分 | 换数据集,绝对准确率与排序都可能变 |
| 模型 | 只有 MLP,没有卷积、没有残差 | 结论不覆盖 CNN 的初始化与归一化行为 |
| 轮次 | 六配置 8 轮、网格 6 轮、初始化 10 轮 | 慢配置可能只是没跑够,排名被预算绑死 |
| 种子 | 全部单种子 20260815 | 0.02–0.3 个百分点的差异不能当显著 |
| 测试集 | 只在大网络过拟合实验里开了一次 | 六配置的排名只在验证集上成立 |
| 学习率策略 | 固定 lr,无 warmup、无衰减 | 真实训练常用 cosine / step,最优 lr 会移动 |
最重要的一条是「轮次预算」。C0 在 8 轮时 93.75%,但理论篇的归一化实验里,同一个网络用 Momentum 0.01 跑 8 轮能到 97.29%——C0 的 SGD 0.01 只是还没走完。如果把六配置都跑到 30 轮,C0 与 C4 的差距会缩小,C2 与 C0 的差距可能消失。「哪个配置更好」必须先说「在多少轮、什么优化器、什么数据上」。
这也是为什么这一篇只给「速度排名」,不选「最终模型」。最终模型要跑满 30 轮、用测试集验收、再做错误分析,那是下一篇的事。
九、这一篇没解决的问题
六种配置都只跑了 8 轮,只在验证集上比较,没有测试集、没有早停、没有错误分析,也没有回答「最终该用哪个模型」。下一篇把这些补齐:固定一个最终配置,跑满 30 轮,用测试集验收,再把 180 个错误逐类拆开。
自检
合上笔记,试着回答:
- 为什么六种配置每次只能动一个变量?C3 的参数为什么比 C0 多 404 个?
- 8 轮快照能回答什么问题,不能回答什么问题?
- 单变量收益为什么不能相加?
+2.25 +2.91 +3.53错在哪? - Adam 的平台上 3e−4 与 1e−3 只差 0.02,为什么不该继续细调?
- 只看 1e−3 会得出「Adam 完胜」,只看 0.1 会得出相反结论,正确的比较方式是什么?
- BN 把到 97% 的轮次从 7 压到 3,代价是什么?
- 为什么 He 初始化在 C2 里几乎无感,在标准化之后才显出差异?
- 学习率差 10 倍,为什么 C0 与 C4 的实际相对更新只差约 3 倍?
- 这一篇的结论在什么条件下会失效?为什么不能拿 8 轮排名直接选最终模型?