前面几章反复撞见同一个敌人:梯度。感知机学不会 XOR,是表达力问题;但从反向传播开始,几乎所有训练困难都能归到梯度上——梯度消失(第 04 章 RNN)、梯度爆炸、softmax 饱和区梯度小(第 08 章为什么除以根号 d)。原因在第 02 章就埋下了:反向传播是把一长串导数连乘起来,只要这些乘子系统性地偏离 1,连乘的结果就会指数级地趋零或炸开。

所以“让深层网络训得动”这个问题,本质上是“如何让梯度在很多层之间健康地流动”。本章的四项技术——ReLU、BatchNorm、残差连接、Adam——从四个不同角度回答这个问题。它们不改变网络要学的函数,只改变“能不能学到”。把它们理解为深度学习的地基,比理解为调参技巧更准确。


第一块地基是激活函数。早期网络用 sigmoid 或 tanh 做非线性,它们有一个致命缺陷藏在导数里。

sigmoid 函数 σ(z)=1/(1+ez)\sigma(z) = 1/(1+e^{-z}) 的导数是 σ(z)=σ(z)(1σ(z))\sigma’(z) = \sigma(z)(1-\sigma(z)),它的最大值在 z=0z=0 处,只有 0.25;当 zz 偏离 0(输入很大或很小)时,导数迅速趋近 0——这叫“饱和”。回忆第 02 章:误差每反传一层,都要乘一次该层激活函数的导数。如果每层都乘一个 0.25\le 0.25 的数,传过 LL 层后梯度量级至多是 0.25L0.25^L——10 层就只剩约 10610^{-6}。深层网络的前几层根本收不到学习信号。

2010 年,Nair 和 Hinton 推广了一个简单到几乎不像“函数”的激活:修正线性单元(Rectified Linear Unit, ReLU)1

ReLU(z)=max(0,z)\text{ReLU}(z) = \max(0, z)

它的导数极其干净:z>0z > 0 时导数是 1z<0z < 0 时是 0。正区间导数恒为 1 意味着——误差反传穿过激活时不衰减。把这件事跑出来对比最直观(配套代码 code/09_enablers.py):

代码 · 09_enablers.py
展开代码 · 09_enablers.py
"""
第 09 章配套代码:深层可训练性的工程突破
(1) ReLU vs sigmoid 的梯度(为什么 ReLU 缓解梯度消失)
(2) 残差连接如何让梯度直达(ResNet)
(3) Adam 优化器的一步更新
Runnable with: numpy only.  python3 09_enablers.py
"""
import numpy as np


def sigmoid(x): return 1 / (1 + np.exp(-x))


print("=== (1) 深层网络里激活函数导数的连乘 ===")
# 10 层,每层乘一次激活导数。sigmoid 导数最大仅 0.25 => 连乘指数衰减
L = 10
# sigmoid'(z) 在 z=0 处最大 = 0.25
sig_grad_chain = 0.25 ** L
# ReLU'(z) = 1 (正区间) => 连乘不衰减
relu_grad_chain = 1.0 ** L
print(f"  sigmoid: 10 层导数连乘(每层最多0.25) ~ {sig_grad_chain:.2e}  -> 梯度消失")
print(f"  ReLU   : 10 层导数连乘(正区间每层1.0) ~ {relu_grad_chain:.2e}  -> 梯度保持")

print("\n=== (2) 残差连接 y = x + F(x) 的梯度 ===")
# 普通层 dy/dx = F'(x);残差层 dy/dx = 1 + F'(x),那个 '1' 保证梯度有直达通路
for Fp in [0.01, 0.5, 2.0]:
    print(f"  F'(x)={Fp:>4}:  普通层 dy/dx={Fp:>5}  |  残差层 dy/dx=1+{Fp}={1+Fp}"
          f"  ({'梯度近乎消失' if Fp < 0.1 else '梯度健康'} vs 残差恒有直达项)")

print("\n=== (3) Adam 优化器一步更新(自适应动量)===")


def adam_step(g, m, v, t, lr=0.01, b1=0.9, b2=0.999, eps=1e-8):
    m = b1 * m + (1 - b1) * g           # 一阶矩(动量)
    v = b2 * v + (1 - b2) * g * g       # 二阶矩(梯度平方的滑动平均)
    m_hat = m / (1 - b1 ** t)           # 偏差校正
    v_hat = v / (1 - b2 ** t)
    update = lr * m_hat / (np.sqrt(v_hat) + eps)   # 每个参数自适应步长
    return update, m, v


# 模拟一个参数,梯度尺度差异很大
m, v = 0.0, 0.0
for t in range(1, 6):
    g = np.array([10.0, 0.01])[t % 2]   # 交替大/小梯度
    upd, m, v = adam_step(g, m, v, t)
    print(f"  t={t} 原始梯度={g:>6}  Adam 更新量={upd:.5f}  (大小梯度被自适应地归一到相近步长)")

↓ 下载 09_enablers.py

sigmoid: 10 层导数连乘(每层最多0.25) ~ 9.54e-07   -> 梯度消失
ReLU   : 10 层导数连乘(正区间每层1.0) ~ 1.00e+00   -> 梯度保持

同样 10 层,sigmoid 把梯度压到百万分之一,ReLU 原样保留。ReLU 还有两个附带好处:计算极快(就是一个取大)、且对负输入直接输出 0,带来稀疏激活。代价是“死亡 ReLU”问题——若一个单元长期落在负区间,梯度恒 0、永不更新;后续的 Leaky ReLU、GELU 等变体对此做了改良。但 ReLU 缓解梯度消失这一条,已足以让它成为 AlexNet(第 05 章)及之后几乎所有网络的默认激活。


第二块地基是批归一化(Batch Normalization, BatchNorm),2015 年由 Ioffe 和 Szegedy 提出2

它针对的问题是:训练时,每一层的输入分布会随着前面层参数的更新而不断漂移——作者称之为“内部协变量偏移”(internal covariate shift)。下一层刚适应了某种输入分布,上一层一更新,分布又变了,下一层得重新适应,训练因此变慢、变得对学习率和初始化敏感。

BatchNorm 的做法是:在每一层(通常是激活之前),对一个小批次(mini-batch)内的数据,把每个特征维度归一化到均值 0、方差 1,再用两个可学习参数 γ,β\gamma, \beta 做缩放和平移:

x^=xμBσB2+ϵ,y=γx^+β\hat{x} = \frac{x - \mu_B}{\sqrt{\sigma_B^2 + \epsilon}}, \qquad y = \gamma,\hat{x} + \beta

其中 μB,σB2\mu_B, \sigma_B^2 是当前批次该特征的均值和方差。先归一化稳住分布,再用 γ,β\gamma, \beta 让网络保留“在需要时恢复原始尺度”的自由(如果归一化不利,网络可以学 γ=σB,β=μB\gamma = \sigma_B, \beta = \mu_B 把它撤销)。

BatchNorm 的效果立竿见影:允许用大得多的学习率、对初始化不再那么敏感、训练显著加快,还附带轻微的正则化效果(因为每个样本的归一化依赖随机的批次统计,引入了噪声)。后来人们对“内部协变量偏移”这个解释本身有争议——有研究认为 BatchNorm 真正的作用是让损失曲面更平滑、优化更容易——但无论机制解释如何,它“稳住数值分布、加速深层训练”的实用价值毋庸置疑。在序列模型和 Transformer 里,因为序列变长、批统计不稳,人们更常用层归一化(LayerNorm,在特征维度而非批次维度归一化,见第 08 章)。


第三块、也是最优雅的一块地基,是残差连接(residual connection),2015 年由何恺明等人在 ResNet 里提出3

它解决一个反直觉的现象:把网络做得更深,效果反而变差——而且不是过拟合(训练误差也更高)。理论上深层网络至少能表达浅层网络能表达的一切(多出来的层学成恒等映射即可),可实践中优化器学不到这个恒等映射,深层网络退化了。

ResNet 的洞察是:与其让每一层去学一个完整的目标映射 H(x)H(x),不如让它去学“目标与输入的差” F(x)=H(x)xF(x) = H(x) - x,然后把输入直接加回来

y=F(x)+xy = F(x) + x

那条把 xx 直接加到输出的线,叫“捷径连接”(shortcut / skip connection)。它带来两个好处。其一,学恒等更容易:要表达恒等映射 y=xy = x,只需让 F(x)=0F(x) = 0(把权重压到 0),这比让一堆非线性层精确拟合出恒等容易得多。其二、也是关键——梯度有了直达通路。看残差层的梯度:

yx=1+Fx\frac{\partial y}{\partial x} = 1 + \frac{\partial F}{\partial x}

那个 +1+1 是魔法所在。即便 F/x\partial F/\partial x 很小(趋于梯度消失),梯度里永远有一个恒为 1 的直达项,误差可以沿着捷径无衰减地传回浅层。跑出来看(code/09_enablers.py):

F'(x)=0.01:  普通层 dy/dx=0.01     |  残差层 dy/dx=1+0.01=1.01
F'(x)= 0.5:  普通层 dy/dx=0.5      |  残差层 dy/dx=1+0.5=1.5

普通层在 F=0.01F’=0.01 时梯度近乎消失,残差层却稳稳保持在 1 附近。靠这条捷径,ResNet 把网络深度推到了 152 层——比当时的 VGG 深 8 倍——并以 3.57% 的 top-5 错误率赢得 ILSVRC 20153。残差连接的思想此后无处不在:第 08 章的 Transformer 每个子层都包着 x+Sublayer(x)x + \text{Sublayer}(x),正是同一个 +1+1 在让上百层的大模型梯度畅通。


第四块地基是优化器。最朴素的梯度下降对所有参数用同一个学习率 η\eta,沿负梯度走 wwηgw \leftarrow w - \eta,g。但不同参数的梯度尺度可能差好几个数量级——有的方向梯度很大、需要小步,有的很小、需要大步,统一学习率顾此失彼。

一系列自适应优化器解决这个问题,集大成者是 2014 年 Kingma 和 Ba 提出的 Adam4。它为每个参数维护两个滑动平均:一阶矩 mm(梯度的动量,平滑掉噪声、保持方向惯性)和二阶矩 vv(梯度平方的平均,估计该方向梯度的典型大小):

mt=β1mt1+(1β1)gt,vt=β2vt1+(1β2)gt2m_t = \beta_1 m_{t-1} + (1-\beta_1) g_t, \qquad v_t = \beta_2 v_{t-1} + (1-\beta_2) g_t^2

做偏差校正(修正初期 m,vm,v 偏向 0 的问题)后,更新为:

m^t=mt1β1t,v^t=vt1β2t,wwηm^tv^t+ϵ\hat{m}_t = \frac{m_t}{1-\beta_1^t}, \quad \hat{v}_t = \frac{v_t}{1-\beta_2^t}, \qquad w \leftarrow w - \eta,\frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon}

关键是那个 m^/v^\hat{m}/\sqrt{\hat{v}}:用动量做分子(稳定方向),用梯度大小做分母(自动缩放步长)。梯度一向很大的参数,分母大、步子被压小;梯度一向很小的参数,分母小、步子被放大。每个参数都得到了适合自己的有效学习率。跑出来看(code/09_enablers.py),交替喂入大梯度(10)和小梯度(0.01),Adam 的实际更新量都被归一到 0.006–0.007 这个相近的量级——尺度被自动拉平。Adam 默认参数(β1=0.9,β2=0.999\beta_1=0.9, \beta_2=0.999)鲁棒、几乎开箱即用,成了深度学习最常用的优化器,从 CNN 到 Transformer 大模型训练的默认选择。


把四块地基放在一起看,它们其实是从四个方向围剿同一个敌人——让梯度在深层网络里健康地流动

敌人: 反向传播是导数连乘 => 系统性偏离 1 就指数消失/爆炸

  ReLU        : 换掉饱和激活,正区间导数=1     → 激活这一环不衰减
  残差连接 +x  : 给梯度一条恒等捷径 dy/dx=1+F'  → 有一条永不衰减的直达通路
  BatchNorm   : 稳住每层输入分布(均值0方差1)   → 数值不漂移,可用大学习率
  Adam        : 每参数自适应步长 m̂/√v̂        → 不同尺度的梯度都走合适的步子
        └────────────── 合力 ──────────────┘
        几层就训不动  ──►  几十上百层稳定收敛

配套的 manim 动画 assets/manim/ch09_enablers.py(含 ReLUGateResidualBatchNormSceneAdamTrajectory 四个 Scene)把四块地基各自的几何直觉演出来:ReLU 是一道折线门,正区导数恒为 1、不饱和,梯度不被掐死;残差 y=F(x)+xy=F(x)+x 的 skip 像一条让梯度直达底层的高速公路;BatchNorm 把漂移的激活分布拉回标准正态;Adam 在被拉长的病态损失曲面上用自适应步长,比朴素 SGD 更直地冲向谷底。四段动画说的是同一件事:这些“工程细节”不是锦上添花,而是深层网络能否被训练出来的生死线。

这一章的技术都不像 Transformer 那样有一个戏剧性的“发明时刻”,它们是一群在 2010–2015 年间陆续到位的拼图。但正是它们的合流,加上第 05 章的数据与算力,才让“把网络堆得很深很大并训练到收敛”从奢望变成日常。地基铺好,就可以盖最高的楼了——把 Transformer 这个骨架,用海量文本做大规模预训练,造出能读会写的语言模型。那是 BERT 与 GPT 的故事,也是大模型时代的开端。


本质

这一章的四项技术看似无关,实则都在对付深层网络的同一个敌人:信号(无论是前向的激活还是反向的梯度)在层与层的反复变换中失控——要么衰减到零,要么膨胀到爆,要么分布漂移到难以优化。ReLU 让梯度有一条不饱和的通路,残差让恒等映射成为默认、给梯度修一条直达底层的近路,归一化把每层的输入分布钉在一个稳定的尺度上,自适应优化器则按每个参数自己的曲率调步长。它们的共同主题是:把深度从一个会放大病态的累赘,变成一个可以安全堆叠的资源。没有任何一项是“提升精度的技巧”,它们合起来回答的是一个更基础的问题——一个几十上百层的网络,凭什么能被训练到收敛。架构决定了模型能表达什么,而这些地基决定了它能不能真的被学出来。


参考文献

  1. Nair, V., & Hinton, G. E. (2010). Rectified Linear Units Improve Restricted Boltzmann Machines. ICML 2010. ReLU 缓解梯度消失。综述见 batch/activation 相关文献;ReLU 在 AlexNet 中的角色见 https://en.wikipedia.org/wiki/AlexNet

  2. Ioffe, S., & Szegedy, C. (2015). Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift. arXiv:https://arxiv.org/abs/1502.03167 ;ar5iv(含公式):https://ar5iv.labs.arxiv.org/html/1502.03167

  3. He, K., Zhang, X., Ren, S., & Sun, J. (2015/2016). Deep Residual Learning for Image Recognition. CVPR 2016. 残差连接、152 层、ILSVRC2015 3.57%。arXiv:https://arxiv.org/abs/1512.03385 ;官方代码:https://github.com/KaimingHe/deep-residual-networks

  4. Kingma, D. P., & Ba, J. (2014). Adam: A Method for Stochastic Optimization. 一阶/二阶矩、偏差校正、自适应步长。arXiv:https://arxiv.org/abs/1412.6980

把反向传播=导数连乘的敌人立起来:sigmoid 每层乘子≤0.25,10 层连乘到百万分之一(梯度消失);ReLU 借集8 的 AND 门语义把负数清零、正区导数恒为 1(乘子=1 不衰减);残差 y=F(x)+x 求导得 ∂y/∂x=1+∂F/∂x,那个恒为 1 的 +1 是让梯度无衰减直达浅层的高速公路。
用散点+高斯曲线演示某层激活分布漂移(μ=2.5、σ=1.8 红),按 x̂=(x−μ_B)/√(σ_B²+ε) 做平移收缩动画把分布钉回标准正态 N(0,1)(绿),再补 y=γx̂+β 的可学缩放/平移——稳住每层输入尺度,可用大学习率。
在各向异性二次型 C=½(12x²+y²) 的拉长椭圆等高线上,先用负梯度箭头给下山动机(三件套);再用真迭代(非硬编码)跑出 SGD 沿陡方向之字震荡的红轨迹 vs Adam 用 m̂/√v̂ 自适应步长更直冲谷底的绿轨迹,角落实时显示 C 值下降。