反向传播 入门

「对每个参数算损失的梯度」 —— 当参数有几百万个时,这听上去根本不可能 —— 如果靠手算,确实不可能。反向传播(backpropagation)就是那个 替你做这件事的算法:沿网络反向走一遍,在每条连接上套用微积分 primer 里的链式法则。 4 个主题:反向传播本身、为什么权重不能全初始化为 0、深网络挥之不去的 梯度消失 / 梯度爆炸问题,以及多层感知机(MLP)—— 那个躲在每个 Transformer 块里的 简单深度网络。

01

反向传播

链式法则,一次过完每一个参数。

梯度下降 primer 收尾收得很爽:每一步就是 w ← w − η · ∇L。 但那篇文章也悄悄假定了:你能算出那个梯度。 参数只有一个时,当然可以 —— 微积分 primer §1。 但一旦参数变成几百万个、铺在五十层网络里,「手算梯度」就不再是一件能做的事了。反向传播(backpropagation)就是让这件事「依然能做」的算法。

反向传播有两条性质,合在一起就是「深度学习能跑得动」的全部原因:

  • 它是自动的。你写好前向计算,框架(PyTorch、JAX、TensorFlow)就免费 把每一个梯度交给你。不需要符号代数,不需要手算微分。
  • 它的代价是网络规模的线性倍。「一遍前向 + 一遍反向」 差不多就等于「两遍前向」。 一个有一百万参数的网络,这一百万个梯度就在这一遍反向里全部冒出来 —— 不是分一百万次去算。

诀窍是把微积分 primer §3 的链式法则一路套到底。沿着计算反向走: 每一步,流回来的梯度都是下一步的梯度乘上这一步的局部导数。 每个参数的梯度,就是从这个参数走到损失的那条路径上所有局部导数的乘积。

具体例子:1 输入、1 个隐藏神经元、1 个输出的小网络。 输入 x = 2、目标 y = 5。 权重 w₁ = 1.5, b₁ = 0.5, w₂ = 0.8, b₂ = 0.1。损失用平方误差。

前向:
  z   =  w₁·x  + b₁  =  1.5·2 + 0.5  =  3.5
  a   =  ReLU(z)     =  ReLU(3.5)    =  3.5
  ŷ   =  w₂·a  + b₂  =  0.8·3.5 + 0.1 =  2.9
  L   =  (ŷ − y)²    =  (−2.1)²      =  4.41

反向(每行就是再乘一个局部导数):
  ∂L/∂ŷ   =  2 · (ŷ − y)                  =  −4.20
  ∂L/∂w₂  =  ∂L/∂ŷ · a                    =  −4.20 · 3.5  =  −14.70
  ∂L/∂a   =  ∂L/∂ŷ · w₂                   =  −4.20 · 0.8  =  −3.36
  ∂L/∂z   =  ∂L/∂a  · ReLU'(z)            =  −3.36 · 1    =  −3.36
  ∂L/∂w₁  =  ∂L/∂z  · x                   =  −3.36 · 2    =  −6.72
L = (ŷ − y)² 前向 → ← 反向w₁, b₁w₂, b₂loss2x3.5a = ReLU(w₁·x + b₁)2.90ŷ = w₂·a + b₂4.41L (target y = 5)∂L/∂w₁ = ?∂L/∂w₂ = ?∂L/∂ŷ = ?前向:x = 2 → a = 3.5 → ŷ = 2.9 → L = 4.41。目标 y = 5。
1 / 4
前向走一遍、反向走一遍 —— 每个参数通过链式法则拿到自己的梯度,既不需要符号微分,也不用手算导数。

看那节奏:反向的每一行都是「上一行的反向结果,乘上一个局部导数」∂L/∂w₁ = ∂L/∂z · ∂z/∂w₁ —— 就是微积分 primer 的链式法则,一字不差。 每个参数的梯度,无非就是从「自己」走到「损失」那条路径上的局部导数堆乘起来。

反向传播里的「反向」也很关键:梯度是从输出往输入算的,不是反过来。 为什么?因为一旦你知道了 ∂L/∂ŷ, 就可以拿它去算上一层的所有梯度(这里是 w₂、b₂、a)。 再一旦你知道了 ∂L/∂a,又可以拿它去算更上一层的所有东西。 每一层「继承下来的梯度」乘上这一层的局部导数,再传给上一层。 总工作量就是「在图上反着走一遍」。

说一点实现细节。前向时,框架会把计算图记下来 —— 每一个操作、以及它产生的值。 反向时,它就沿着这张图反着走,在每一个内置算子(矩阵乘、ReLU、sigmoid、softmax、 LayerNorm、attention —— 每一个内置算子都有手写好的梯度公式)上乘对应的局部导数。 用户完全不用碰这一层,只负责写前向;loss.backward() 负责剩下的。

在 Transformer 里:完全是同一个算法,只是图大得多。 一个 700 亿参数的模型,所有这 700 亿个梯度依然是从「同一遍反向」里一次性出来的, 走的就是产生它输出的那张计算图。 每个 attention head 里的每个权重、每个 FFN、每个 LayerNorm, 反向传播一次性把它们的梯度全算完,代价只比「再多一遍前向」高一点点。 没有反向传播,训练 Transformer 不是「慢」的问题,而是「做不到」的问题。

02

权重初始化

为什么每个权重都从一个很小的随机数开始 —— 而不是从 0。

反向传播告诉优化器怎么改进权重。但在第一次更新之前,权重得有初值。 「全设成 0」是最自然的第一反应 —— 但这一手会在训练开始之前就把它弄死。 下面解释为什么,以及该怎么做。

想象一层有 100 个神经元,权重全初始化为 0。这一层里每个神经元拿到的输入是一样的, 套用的「配方」也一样,所以每个神经元的输出也一样。它们在功能上就是同一个神经元 —— 只是被复制了 100 份。一个有 100 个零神经元的「宽层」, 其实跟只有 1 个神经元的层一样宽。

反向走的时候情况更糟:

  • 既然这层每个神经元的输出都一样,从上层流下来的梯度也都一样。
  • 相同输入 + 相同梯度 → 相同的权重更新。
  • 更新完之后,这 100 个神经元的权重仍然一模一样。这一层永远都是 「1 个神经元假装成 100 个」。训练打不破这个对称 —— 反向传播只会维持它。

这就是对称性问题。任何让一层里的神经元变得相同的初始化, 都会把它们永远绑成一队 —— 不管你的损失多聪明、优化器多用心、跑多少 epoch,都解不开。

修法:打破对称。把每个权重初始化成一个独立的小随机数 (高斯或均匀分布都行)。这样这一层里每个神经元「看到的输入」就不一样了, 输出不一样、梯度不一样、训练里逐渐发展出不同的角色。「宽层」才真的宽。

两个网络,相同输入 x = (1, 2)全 0 初始化12h1h2h3inputhidden随机初始化12h1h2h3inputhidden同样的输入喂给两个 2→3 网络。左边:所有权重 = 0。右边:每个神经元拿到不同的随机权重。
1 / 3
全零初始化让每个神经元变成同一个神经元。随机初始化让每个神经元有不同的角色去专攻。

随机初始化打破了对称,但随机的尺度也不能乱选。 方差太小(接近 0),信号会一层一层衰减 —— 到第 10 层每个激活基本都是 0,网络学不动。 方差太大(接近 1),信号会一层一层放大 —— 激活全饱和,损失变 NaN。 只有窄窄一个尺度区间能让信号在深网络里干净地传下去,标准的初始化方法 就是为了落在这个区间里:

  • Xavier(Glorot)初始化 —— 在假定激活是 sigmoid / tanh 的前提下, 让权重的尺度使每层激活的方差大致保持不变。方差 ≈ 1 / n_in
  • He 初始化 —— 思路一样,但是针对 ReLU 调过的。 ReLU 会把一半的输入直接归零,所以方差得翻倍补一补。方差 ≈ 2 / n_in
  • 偏置 —— 一般直接初始化为 0。偏置不存在对称性问题 (绑死神经元的只有权重),而且梯度会很快把它们推到该去的地方。

「小」这个字很关键。深 ReLU 网络如果用方差 1 的权重初始化, 第一次前向就会在输出端炸出巨大的激活; 如果用方差 0.001,过 5 层之后每个激活都已经是尘埃。 方差 ≈ 1/n_in 是那个金发姑娘区,允许 ±2 倍的浮动。

在 Transformer 里:线性层(QKV 投影、attention 输出、FFN 层) 通常用方差 ≈ 1 / d_model 的高斯初始化。 有些配方还会把「输出投影」的初始化再缩小(比如再乘 1 / √(2N), N 是层数),让残差流的方差不会随着块堆得多而越加越大。 LayerNorm 通常以恒等映射开局(gain = 1、bias = 0)。 具体常数会变,但原则不变 —— 打破对称、把方差控制住。

03

梯度消失与梯度爆炸

50 个局部导数相乘,几乎不会是一个适中的数。

反向传播算靠前那些层的 ∂L/∂w 时,是「从这个权重走到损失」一路上所有 局部导数的乘积。50 层的网络里,这个乘积就是 50 项左右相乘。 如果每一项都略小于 1,乘起来基本是 0;如果每一项都略大于 1,乘起来基本是无穷大。 哪一边都让训练崩。

同一个乘法,两种崩法:

  • 梯度消失 —— 大多数局部导数都很小。乘积随着深度指数衰减。 越靠近输入的层拿到的梯度越接近 0,权重几乎不更新; 网络表现得就像那些层被冻住了 —— 哪怕它们技术上是可训练的。
  • 梯度爆炸 —— 大多数局部导数都很大。乘积随深度指数膨胀。 靠前那些层拿到一个巨大的梯度,优化器朝着某个随机方向迈出超大一步, 权重发散,几个迭代后损失变 NaN

具体一点:0.5 的 50 次方 ≈ 8 × 10⁻¹⁶。1.5 的 50 次方 ≈ 6.4 × 10⁸。 每层一个看上去无害的乘数,在深度上一复合就变成灾难。

|∂L / ∂w| 在第 k 层 —— k 个局部导数的乘积k = 0输出端k = 9最深层消失(σ = 0.5)0.002稳定(σ = 1.0)爆炸(σ = 1.5)消失。每层梯度是下一层的一半 —— 到第 10 层只剩 ≈ 0.002。最深的那些权重在训练里几乎不动。
1 / 3
深网络里,靠近输入那一层的梯度,是许多个局部导数相乘的结果。每层都大于 1 就爆炸,都小于 1 就消失,只有窄窄一条带能正常训练。

常见的元凶,大致按实际中出现的顺序:

  • 饱和的激活函数。sigmoid 的导数最大也就 σ'(z) ≤ 0.25。 一个 20 层的 sigmoid 网络要把 20 个 ≤ 0.25 的数乘起来 —— 梯度从数学上就被保证不超过 0.25²⁰ ≈ 10⁻¹²。 tanh 稍微好一点(最大 1.0),但在尾巴上同样饱和到导数 0。 ReLU 帮了大忙(导数要么是 0 要么是 1,在「活着」的那一半完全不缩水), 这是它取代 sigmoid / tanh 的原因之一。
  • 不好的初始化。§2 已经提过 —— 权重方差初始化成 4, 局部导数会被一并放大,乘起来爆炸;初始化成 0.01,就消失。 Xavier / He 初始化就是为了让「每层那个乘数」稳稳落在 1 附近。
  • 纯粹的深。就算激活和初始化都合理, 随便把 100 层叠起来也会漂走。100 个接近 1 的数相乘,还是会飘离 1。

2025 年让深网络可训练的几个补丁:

  • 更好的激活函数 —— ReLU、GELU、SiLU。正半边不饱和。
  • 合理的初始化 —— tanh 用 Xavier、ReLU 用 He、 Transformer 用 1/d_model
  • 归一化层 —— BatchNorm、LayerNorm、RMSNorm。 每一层之后都把激活重新居中、重新缩放,这样乘积没法无限漂走。
  • 残差连接 —— x + sublayer(x)。 任意一层的梯度至少等于下一层的梯度(因为「+ x」那一部分), 所以梯度衰不到 0。这是让「非常深」的网络变得可能的那一招。
  • 梯度裁剪 —— 对付爆炸的廉价 hack。如果梯度的范数超过某个阈值 (比如 1.0),就缩回去。能救你不出 NaN,偶尔代价是更新带点偏。

在 Transformer 里:五个补丁一起上。GELU 激活、 按深度缩放的小心初始化、每个子层前的 LayerNorm、把每个子层包起来的残差连接、 以及通常 ‖g‖ ≤ 1 的梯度裁剪。它们合起来让 100 层的 Transformer 能训。 少一个,哪怕是当年 12 层的 GPT-1 都会很挣扎。其中残差连接尤其不是「锦上添花」—— 它是让深度本身成为可能的那一件。

04

MLP(= Transformer 的 FFN)

把神经网络 primer 里的「一层」堆几次,就是一个深度网络。

把神经网络 primer 里的「一层神经元」拿过来,中间夹上激活函数堆两三层,你就有了 一个 MLP(Multi-Layer Perceptron,多层感知机)。 它就是最简单的深度网络,2014 年之前几乎每篇深度学习论文都在讲它, 它也是每个 Transformer 块里的 FFN 子层。 如果你看懂了神经网络 primer 里的前向传播,你就看懂了 MLP —— 它就是同一个操作,做三遍。

配方一行:Linear → 激活,反复;最后一层只 Linear,不加激活(因为通常你要原始 logits 或者标量输出)。

MLP(x):
  h₁  =  ReLU( W₁ · x  + b₁ )      ← 第 1 层
  h₂  =  ReLU( W₂ · h₁ + b₂ )      ← 第 2 层
   ŷ  =          W₃ · h₂ + b₃      ← 第 3 层(不加激活)
  return ŷ

定义就这么多。一个「更深」的 MLP 就是中间多塞几行 ReLU(W·_+b) 而已。 深度(层数)和宽度(每个 h_i 的大小)是超参; 除此之外就是一个操作,反复用。

MLP —— Linear → ReLU 两次,再一个 Linear输入隐藏 1隐藏 2输出1.0-1.00.5ŷ1ŷ23 维输入就位。要走过 3 层才能看到输出。
1 / 4
MLP 就是把(线性 + 激活)层往上堆 —— 这里是 3 → 4 → 4 → 2。Transformer 的 FFN 子层就是这个形状,被塞进每个块里。

关于 MLP,两个让人意外的事实:

  • 原则上,一层隐藏层就够了。万能逼近定理说:足够多的神经元在一个 隐藏层里,就能以任意精度逼近任何连续函数 —— 原则上。 实际操作里,「足够多」意味着荒唐的数量,而且训练起来不可忍受。 所以我们堆 —— 深度比宽度便宜。
  • 没有激活的 MLP 就是一个线性层。把 5 个线性层中间不加非线性叠起来, 整个叠层就坍缩成 (W₅ · W₄ · W₃ · W₂ · W₁) · x 这一个线性映射。 深度啥也没换来。非线性才是「深」跟「宽」不同的原因。

为什么叫「feedforward」? 因为信息只朝一个方向走 —— 左边输入、右边输出、没有回路。 (有回路的网络叫循环网络 —— RNN 之类。) 前向就是一次「从左走到右」,反向就是一次「从右走到左」。 反向传播喜欢这种结构:没有环就没有不动点迭代,也没有梯度顺序的歧义。

「多层感知机」这个名字一半是历史包袱。「感知机」(perceptron)是 Rosenblatt 在 1958 年的单层线性分类器。「多层」这个前缀加上了深度和非线性,变成的就是我们整个 primer 在描述的网络。「MLP」和「FFN」(Feed-Forward Network)指的就是同一种架构, 看你读的是哪篇论文,会用其中之一。

在 Transformer 里:FFN 子层就是一个 MLP,不多不少。标准形状是两层:从 d_model 「升维」到 4 · d_model、 过一个 GELU(或 SiLU)激活、再「降维」回 d_model。 每个 Transformer 块里都正好有一个,紧接在 attention 子层后面。 在 GPT-3 那个量级的模型里,这些 FFN 子层占了大约三分之二的参数 —— attention 管「路由」、 MLP 管「真正的计算」的大头。归根到底,一个 1750 亿参数的语言模型, 权重就花在这些事上:96 份 "Linear → GELU → Linear",被 attention 粘起来, 再加上几亿个边缘上的 embedding 和归一化参数。 到这里你已经知道每个权重在干什么了。下一步,去读那块「粘合剂」。