理论学习:为什么你的网络不收敛
第 2 章已经把梯度算对了,「方向」有了。可把同一段代码接到真实网络上,SGD 学习率开到 1.0,第 1 轮 loss 就变成 NaN;退回 1e−3,loss 又停在 2.3016 一动不动。方向对了,步子怎么迈,才是训练工程要回答的问题。
这一篇不堆技巧,只把四种典型失败拆开:激活饱和、初始化方差、归一化位置、优化器与正则。每一种都先给现象,再给机制,最后给本章实测的数字。

<!-- 配图:四宫格:平线、NaN、爬升、过拟合,各自带一条曲线和一行结论 -->
一、四种失败形态:先看曲线,再猜原因
训练不收敛不是一个问题,是四类问题。它们的曲线形状完全不同,第一眼就能分开。
| 形态 | 现象 | 第一嫌疑 | 本章对应 |
|---|---|---|---|
| 平线 | loss 十轮停在 2.3014,acc 11.2% | 初始化、梯度是否为 0 | 第三节 |
| 爆炸 | 第 1 轮 loss 变 NaN,acc 10.2% | 学习率过大 | 第五节 |
| 爬升 | loss 从 1.55 升到 2.13,acc 34.1% → 16.9% | 学习率过大、优化器不匹配 | 第五节 |
| 过拟合 | train 0.0114,val 0.1753,两者越拉越开 | 容量过大、没有正则 | 第六节 |
图里四格都来自真实运行。② 甚至没留下曲线:SGD 学习率 1.0 时,第一个 epoch 内 loss 就不是有限值,训练循环直接退出。这就是为什么要先看 loss 的数值范围,再看准确率。
参数更新只有一行:
θ ← θ − η ∇L(θ)η 是学习率,∇L 是梯度。失败可以粗分成两类:梯度本身尺度不对(太大、太小、全零),或者步长与梯度不匹配(方向对,步子太大)。前者主要来自激活、初始化、归一化;后者主要来自优化器和学习率。
二、激活函数:饱和把梯度压没了
先看激活函数的导数上限。

<!-- 配图:Sigmoid / Tanh / ReLU / LeakyReLU 四条曲线,两侧红色饱和区,右侧导数上页面板 -->
Sigmoid 的导数:
σ(z) = 1 / (1 + e^{−z})
σ′(z) = σ(z)(1 − σ(z)) ≤ 0.25最大值在 z = 0 处,只有 0.25。|z| 一大,导数就趋近 0:输入落在「饱和区」,这一层几乎不传梯度。Tanh 好一些,导数上限是 1;ReLU 在正半轴导数恒为 1,负半轴恒为 0。
单层 0.25 不可怕,可怕的是连乘。一个 8 层 Sigmoid 网络,梯度从输出传到第 1 层,每层最多乘 0.25:

<!-- 配图:九个方块从 1.000 依次乘 0.25 降到 1.53e−5;下方 ReLU 与 Sigmoid 的首末层梯度实测 -->
0.25⁸ = 1.53e−5第 1 层只剩十万分之十五。这不是比喻,是乘法。
实测也一致。同一个 784→128→64→10 网络、同一份 He 初始化,只看第一个 batch 的首层梯度范数:

<!-- 配图:ReLU 4.57、LeakyReLU 4.59、Sigmoid 0.42、Tanh 2.79 四根柱,底部注明十轮后结果 -->
| 激活 | 首层梯度范数 | 首层 / 末层 | 初始零输出比例 |
|---|---|---|---|
| ReLU | 4.57 | 2.37 | 50.98% |
| LeakyReLU 0.01 | 4.59 | 2.44 | 50.98% |
| Sigmoid | 0.42 | 0.50 | 0.22% |
| Tanh | 2.79 | 1.83 | 0.00% |
Sigmoid 的 0.42 比 ReLU 的 4.57 小 10.9 倍。但这里有一个容易误读的地方:10 轮之后,四种激活的验证准确率都在 97.1–97.3%。ReLU 97.23%、LeakyReLU 97.23%、Sigmoid 97.28%、Tanh 97.11%。
原因是网络只有两个隐层,Adam 又对每个参数做了归一化,把梯度尺度差异抹平了。第 2 章那个 8 层实验里,Sigmoid 的首末层比值是 5.50e−5,ReLU 仍有 2.25——深度一上来,差距就藏不住了。
设计取舍:ReLU 还是 Sigmoid
| 方案 | 收益 | 代价 |
|---|---|---|
| A:ReLU / LeakyReLU | 正半轴梯度恒为 1,首层梯度 4.57 | 负半轴直接归零;本实验最终模型 75.4% 的 ReLU 输出为 0 |
| B:Sigmoid | 输出有界、平滑,适合概率型输出 | 导数上限 0.25,深层连乘消失 |
本章选 A。代价是「死单元」:学习率太大或初始化太偏时,一批单元永久落在负半轴,梯度永远是 0。LeakyReLU 用 0.01 的负斜率留了一条细缝,本章实测它和 ReLU 几乎同分。
三、初始化:方差与对称性
激活决定单层导数,初始化决定信号能不能一层层传下去。

<!-- 配图:四种初始化在输入 / 第一层 / 第二层的激活标准差折线;零初始化贴 0,小随机掉到 0.0092 -->
全连接层的预激活方差近似:
Var(z) = fan_in · Var(w) · Var(a)要让 Var(z) 和 Var(a) 同量级,权重的方差就必须约为 1 / fan_in。
实测(784→128→64→10,ReLU,输入标准化):
| 初始化 | 第 1 层激活 std | 第 2 层激活 std | 10 轮验证 acc |
|---|---|---|---|
| 零 | 0.000 | 0.000 | 11.20% |
| 小随机 0.01 | 0.131 | 0.0092 | 97.62% |
| Xavier | 0.614 | 0.439 | 97.38% |
| He | 0.662 | 0.580 | 97.23% |
小随机 0.01 从 0.131 掉到 0.0092,两层衰减 14.3 倍;He 从 0.662 到 0.580,基本持平。这就是「方差保持」四个字的实际含义。
比方差更严重的是对称性。

<!-- 配图:128 个相同隐单元复制同一输出;右侧三个后果;底部指标表 -->
如果所有权重都是 0,128 个隐单元的前向输出完全相同,反向梯度也完全相同,更新之后仍然完全相同。网络实际上只有 1 个有效特征,被复制了 128 份。
实测结果是:
| 指标 | 数值 | 含义 |
|---|---|---|
| 验证准确率 | 11.2% | 只会猜先验(每类 10%) |
| 验证 loss | 2.3016 | = ln(10),输出均匀分布 |
| 首层梯度范数 | 0.0 | 没有任何更新 |
| 单元余弦相似度 | 1.000 | 128 个方向完全重合 |
踩坑记录
- 现象:第一次跑零初始化,10 轮验证 acc 卡在 11.20%,loss 2.3014,一条水平线。
- 原因:权重全 0 → 所有隐单元输出相同 → 梯度相同 → 更新后仍相同,对称性从未被打破。
- 修复:改成随机初始化,方差按 fan_in 缩放。He 初始化后单元余弦相似度降到 0.028,10 轮 acc 97.23%。
设计取舍:Xavier 还是 He

<!-- 配图:fan_in / fan_out 示意;Xavier 2/(fan_in+fan_out);He 2/fan_in -->
| 方案 | 公式 | 适用 | 代价 |
|---|---|---|---|
| A:Xavier | Var(w) = 2 / (fan_in + fan_out) | tanh / sigmoid,导数上限接近 1 | 对 ReLU 偏小;本实验第 2 层 std 0.439,比 He 低 24% |
| B:He | Var(w) = 2 / fan_in | ReLU / LeakyReLU,负半轴砍掉一半激活 | 换成 Sigmoid 时容易把预激活推进饱和区 |
本章固定用 B,因为主线激活是 ReLU。两者在两层网络上最终只差 0.15 个百分点(97.38% vs 97.23%),但第 2 层的信号强度差了 24%;网络一深,这个差距会被逐层放大。
四、归一化:输入一次,还是每层一次
输入标准化的统计量来自训练集:mean = 0.130612,std = 0.308064。验证集和测试集直接复用这两个数,不重新计算。

<!-- 配图:原始像素 / 输入标准化 / 标准化+BN 三列流程,底部到 97% 的轮次 -->
| 输入处理 | 到 97% 的轮次 | 8 轮验证 acc |
|---|---|---|
| 原始像素 0–1 | 7 | 97.29% |
| 输入标准化 | 4 | 97.41% |
| 输入标准化 + BN | 3 | 97.82% |
BN 放在 Linear 之后、激活之前。它做的是同一件事,但对象不是输入,而是每一层的预激活:

<!-- 配图:训练模式用 batch 均值方差并累计 running stats;推理模式用 running stats;右侧为什么 BN 好训 -->
μ = mean(z) σ² = var(z)
ẑ = (z − μ) / √(σ² + ε)
y = γ · ẑ + βγ、β 是可学习参数。训练时用当前 batch 的统计量,同时把均值方差累计到 running_mean / running_var;推理时改用累计值。这就是 model.train() 与 model.eval() 对 BN 的真实差别——不是「关掉 BN」,而是「换一套统计量」。
设计取舍:输入标准化还是 BN
| 方案 | 收益 | 代价 |
|---|---|---|
| A:只做输入标准化 | 一次计算,零额外参数,不依赖 batch | 只管输入;层间分布漂移照旧,到 97% 要 4 轮 |
| B:标准化 + BN | 每层重算统计量,到 97% 只要 3 轮 | 每个 BN 层多 2×宽度 个参数;batch 太小时统计量不稳;训练/推理两套模式 |
本章主线选 B:BN 让 30 轮训练里学习率更宽容,也让第 2 层不必看第 1 层的尺度脸色。代价是参数从 235,914 变成 235,934——多出来的 20 个,来自输出层也建了一个 10 维 BN。这不是笔误,后文会解释。
五、优化器与学习率:方向对,步子也要对
同样一个峡谷,三种走法。

<!-- 配图:SGD 来回横跳、Momentum 抵消震荡、Adam 每维缩放;右侧三个更新式与实测表 -->
SGD: θ ← θ − η∇L
Momentum: v ← βv − η∇L, θ ← θ + v
Adam: m ← β₁m + (1−β₁)g
v ← β₂v + (1−β₂)g²
θ ← θ − η m̂ / (√v̂ + ε)SGD 对所有参数用同一个步长;Momentum 把历史方向带上,窄方向上的来回震荡会相互抵消;Adam 对每个参数分别缩放,步长大约固定在 η 附近,和梯度大小无关。
6 轮实测:
| 优化器 | 最佳验证 acc | 最佳学习率 | 学习率过大时 |
|---|---|---|---|
| SGD | 97.13% | 0.1 | lr=1.0 → 第 1 轮 NaN |
| Momentum | 97.17% | 0.01 | lr=0.1 → 94.07% |
| Adam | 97.02% | 1e−3 | lr=0.1 → 16.94% |
三者的最佳值只差 0.15 个百分点,但它们要的学习率差两个数量级。SGD 在 1e−4 时只有 72.53%,学习率放大 1000 倍到 0.1 才到 97.13%;Adam 在 1e−3 就到位,放大 100 倍到 0.1 直接崩到 16.94%。
学习率本身的地形也值得单独看。

<!-- 配图:对数横轴,1e−4 到 1e−1 八个点,右侧红色悬崖区,标出平台与 3e−2 起的崩塌 -->
| 学习率 | 1e−4 | 3e−4 | 1e−3 | 2e−3 | 3e−3 | 1e−2 | 3e−2 | 1e−1 |
|---|---|---|---|---|---|---|---|---|
| 6 轮验证 acc | 96.03 | 97.00 | 97.02 | 95.97 | 96.23 | 94.72 | 89.72 | 16.94 |
平台很宽:3e−4 到 1e−3 都在 97% 附近。悬崖很陡:再大 10 倍到 1e−2 还有 94.72%,到 1e−1 只剩 16.94%。所以调学习率的顺序是「先找悬崖,再找平台」:从大到小降,直到 loss 不再发散,再在平台里细挑。
设计取舍:Adam 还是 Momentum
| 方案 | 收益 | 代价 |
|---|---|---|
| A:Adam 1e−3 | 前几轮最快,6 轮 97.02%,无需精细调 lr | 对每个参数单独缩放,噪声也被放大;lr=0.1 时只剩 16.94% |
| B:Momentum 1e−2 | 6 轮 97.17%,略高;梯度噪声被动量平滑 | 需要手动选 lr;lr=0.1 时 94.07%,比 Adam 的同档好,但仍不如自己的最佳 |
本章主线选 A:30 轮、有 BN 和 Dropout 的网络,Adam 的稳定性比那 0.15 个百分点更值钱。代价是要接受它对学习率上界更敏感——这也是为什么后文最终配置把 lr 固定在 1e−3,而不是 0.1。
六、正则与早停:别把欠拟合当成收敛

<!-- 配图:L2 / Dropout / Early Stopping 三面板,各自公式、示意与实测数字 -->
三种常用手段解决的是同一个问题:模型把训练集背下来了。
L2 / Weight Decay: L = L_data + λ‖W‖²
Dropout: 训练时按概率 p 把激活置 0,推理时整体缩放
Early Stopping: 验证指标连续 patience 轮不改善就停一个 784→512→256→10、535,818 个参数的大网络,30 轮实测:
| 配置 | 验证 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 把测试 acc 从 97.96% 提到 98.27%,验证 loss 从 0.1753 压到 0.0840。Weight Decay 1e−2 反而把测试 acc 压到 96.37%——正则不是越强越好,强度过头就是欠拟合。
早停也有一个容易忽略的选择:盯 loss 还是盯 acc。
大网络无正则时,验证 loss 在第 4 轮见底(0.1015),按 patience=4 在第 8 轮停,测试 96.86%。但如果换成一个 10,000 张的小训练集,同样的网络:
| 选择规则 | 选中轮次 | val loss | val acc | test acc |
|---|---|---|---|---|
| 按 val loss 最低 | ep4 | 0.1705 | 95.33% | 95.40% |
| 按 val acc 最高 | ep30 | 0.2062 | 96.69% | 96.71% |
验证 loss 上升了 0.0357(+20.9%),验证 acc 却涨了 1.36 个百分点,测试 acc 涨了 1.31 个百分点。原因在 loss 和 acc 量的不是同一件事:loss 由正确类的概率决定,acc 只看最大值是不是正确类。模型后期把正确类从 0.95 推到 0.98,acc 不变甚至小涨;但错误样本也被推得更自信,把 loss 拉高了。
踩坑记录
- 现象:按 val loss 早停,测试只有 95.40%;同一份数据按 val acc 跑到第 30 轮,测试 96.71%。
- 原因:loss 对置信度敏感,acc 对置信度不敏感;小数据上 loss 会更早见底,早停因此过早。
- 修复:先明确验收指标。验收看 acc 就按 acc 选模型;如果要输出概率,loss 和可靠性图要一起看。
七、这一章没解决的问题
上面所有手段——换激活、调初始化、加 BN、换优化器、上正则——改的都是「怎么把 784 维像素向量拟合得更好」。它们能修掉平线、爆炸、爬升、过拟合,却修不掉一类错误:最终模型测试 98.20%,180 个错误里,9→4 有 13 张、7→2 有 10 张、3→5 有 9 张。这些数字的形状只差一笔,全连接层把 28×28 拉平成 784 维之后,相邻像素的关系就没了。
下一个问题只能换网络结构来回答:卷积为什么能看见图像?
自检
合上笔记,试着回答:
- 四种失败形态各是什么曲线形状?第一嫌疑分别是什么?
- Sigmoid 的导数上限是多少?8 层连乘后第 1 层还剩多少?
- 零初始化为什么会「训多久都分不开」?用哪两个指标能确认?
- Xavier 和 He 的公式差在哪?为什么 ReLU 要用 He?
- BN 在
train()和eval()下分别用哪套统计量?忘记切换会发生什么? - 学习率 1e−3 与 1e−1 在同一个 Adam 上差多少?先找悬崖还是先找平台?
- 早停按 loss 和按 acc,哪个更容易过早停?为什么?