Skip to content

反向传播详解

反向传播(Backpropagation,简称 backprop)是训练深度神经网络的核心算法——它用微积分中的链式法则高效计算损失函数对每个参数的梯度,让梯度下降成为可能。自 1986 年 Rumelhart、Hinton 和 Williams 在 Nature 上发表里程碑论文以来,反向传播一直是深度学习几乎不可替代的训练引擎;今天的 PyTorch、TensorFlow、JAX 等所有主流框架,其自动微分(Automatic Differentiation,即”自动求导”)底层都依赖它。

本页从计算图出发,逐步推导前向传播与反向传播的完整数学过程,再讲解 PyTorch 自动微分引擎的原理与实战。前置阅读:梯度下降与优化器、线性代数基础、数值优化与数学基础。

反向传播的本质就是链式法则的工业级自动化。

想象一家工厂的生产流水线(前向传播):原材料(输入数据)经过一道道工序(网络层),逐步加工成成品(预测结果)。质检员发现成品有缺陷(损失值),需要追究每一道工序的责任——到底哪一步出了问题、责任多大?

  • 前向传播(Forward Pass)= 原材料从流水线起点流到终点,每一步记录中间产品(激活值,即每层运算后的输出)。这里的”记录”至关重要:反向传播要复用这些中间值来算梯度。
  • 反向传播(Backward Pass)= 质检报告从终点逆流回起点,逐级追问”你这步对最终误差贡献了多少?“,由此算出每道工序的改进方向(梯度,即参数应该朝哪个方向调整)。
  • 梯度下降(Gradient Descent)= 根据责任大小调整每道工序的参数,让下次生产的产品更合格。

假设你想知道参数 ww 对损失 LL 的影响,最原始的办法是数值微分:给 ww 加一个小扰动 ϵ\epsilon,重新跑一次前向传播看 LL 变了多少,近似 dL/dw≈ΔL/ϵdL/dw \approx \Delta L / \epsilon。

问题在于:网络有数百万甚至数十亿参数,数值微分需要对每个参数各做一次前向传播,总代价是 O(N)O(N) 次前向传播(NN 是参数总数)。一个 70B 参数的模型跑一次前向就要数秒,7×10107 \times 10^{10} 次根本不现实。

而反向传播只需一次前向 + 一次反向,总共两次遍历,就能算出所有参数的梯度——计算量大约只有两次前向传播。这正是反向传播高效的根本原因,也是深度学习能扩展到千亿参数的关键。

一句话总结:前向传播算结果,反向传播算责任,梯度下降改参数。

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
# Simulate gradient norms across layers in a 20-layer sigmoid network
np.random.seed(42)
steps = np.arange(0, 200)
layer_depths = ["Layer 1\n(shallow)", "Layer 5", "Layer 10", "Layer 15", "Layer 20\n(deep)"]
colors = ["#e91e63", "#FF9800", "#FFC107", "#8BC34A", "#2196F3"]
fig, ax = plt.subplots(figsize=(9, 5.5))
fig.patch.set_facecolor("white")
for i, (depth, color) in enumerate(zip(layer_depths, colors)):
decay = 0.25 ** (i * 4) # sigmoid gradient vanishes exponentially with depth
base_grad = 0.5 * decay
grad_norm = base_grad * np.exp(-steps / 300) + base_grad * 0.1 * np.random.randn(len(steps))
grad_norm = np.maximum(grad_norm, 0)
ax.plot(steps, grad_norm, linewidth=1.5, label=depth, color=color, alpha=0.8)
ax.set_yscale("log")
ax.set_xlabel("Training Step", fontsize=11, fontweight="bold")
ax.set_ylabel("Gradient L2 Norm (log scale)", fontsize=11, fontweight="bold")
ax.set_title("Gradient Norm Across Layers: Vanishing Gradient in Deep Networks", fontsize=12, fontweight="bold")
ax.legend(fontsize=8, loc="upper right")
ax.grid(True, alpha=0.2, linestyle="--", which="both")
ax.set_ylim(1e-14, 1)
ax.axhspan(1e-12, 1e-8, alpha=0.05, color="red")
ax.text(180, 1e-10, "Vanishing\nGradient\nZone", fontsize=8, color="red", ha="center", va="center")
plt.tight_layout()
plt.savefig("/mnt/kvm_ata-Netac_SSD_480GB_AA000000000000000904-part1/proj/docs/img/generated/backprop-gradient-flow.png",
dpi=180, bbox_inches="tight", facecolor="white")

反向传播梯度流动:深层网络中梯度逐层衰减(梯度消失)

反向传播在计算图(Computational Graph)上执行。计算图是一种有向无环图(DAG),每个节点代表一个运算(加、乘、激活函数等),边表示数据流动方向。把复杂的神经网络拆解成计算图后,每个节点只需要处理自己那一步的”局部求导”,整个网络的梯度就能通过链式法则拼起来。

以一个最简单的单层网络为例。设某一层有:输入 xx,权重 ww,偏置 bb,输出 a=relu(w⋅x+b)a = \text{relu}(w \cdot x + b),最终损失 L=Loss(a,y)L = \text{Loss}(a, y),其中 yy 是真实标签。画成计算图:

x ──┐
├──(×)── z ──┐
w ──┘ ├──(+)── s ──(relu)── a ──(Loss)── L
b ────────────────────── y ────────┘

即:z=w⋅xz = w \cdot x,s=z+bs = z + b,a=relu(s)a = \text{relu}(s),L=Loss(a,y)L = \text{Loss}(a, y)。

从输入到输出逐节点计算,每个节点保存自己的输出值和输入值,供反向传播使用:

z=w⋅xz = w \cdot x s=z+bs = z + b a=relu(s)=max⁡(0,s)a = \text{relu}(s) = \max(0, s) L=Loss(a,y)L = \text{Loss}(a, y)

为什么要保存中间值? 因为反向传播计算梯度时需要用到前向传播中的输入值。例如 dL/dw=dL/dz⋅xdL/dw = dL/dz \cdot x,需要用到 xx;dL/dx=dL/dz⋅wdL/dx = dL/dz \cdot w,需要用到 ww。这就是为什么训练神经网络比推理(只做前向传播)消耗更多内存——需要保存整条计算路径的中间结果。这也是后面要讨论的激活检查点(Activation Checkpointing)技术要解决的问题。

反向传播从损失 LL 出发,逐节点倒退,计算 LL 对每个变量的偏导数(梯度)。核心是链式法则:

链式法则(Chain Rule):如果 y=f(x)y = f(x) 且 L=g(y)L = g(y),则 dLdx=dLdy⋅dydx\frac{dL}{dx} = \frac{dL}{dy} \cdot \frac{dy}{dx}。

推广到多元函数:如果 y=f(x1,x2,…,xn)y = f(x_1, x_2, \ldots, x_n),则 ∂L∂xi=∂L∂y⋅∂y∂xi\frac{\partial L}{\partial x_i} = \frac{\partial L}{\partial y} \cdot \frac{\partial y}{\partial x_i}。

下面逐步推导。

第一步:穿过损失节点——计算 ∂L∂a\frac{\partial L}{\partial a}

这取决于具体的损失函数。对于 MSE 损失(均方误差)L=12(a−y)2L = \frac{1}{2}(a - y)^2:

∂L∂a=a−y\frac{\partial L}{\partial a} = a - y

对于交叉熵损失配合 softmax 输出,梯度恰好简化为 ∂L∂a=a−y\frac{\partial L}{\partial a} = a - y(这种简洁性是 softmax + 交叉熵成为分类标配的原因之一,详见损失函数)。

第二步:穿过 ReLU 节点——计算 ∂L∂s\frac{\partial L}{\partial s}

ReLU(修正线性单元,一种激活函数 relu(s)=max⁡(0,s)\text{relu}(s) = \max(0, s))的导数是一个分段函数:

dads={1if s>00if s≤0\frac{da}{ds} = \begin{cases} 1 & \text{if } s > 0 \\ 0 & \text{if } s \leq 0 \end{cases}

因此:

∂L∂s=∂L∂a⋅relu′(s)={∂L∂aif s>00if s≤0\frac{\partial L}{\partial s} = \frac{\partial L}{\partial a} \cdot \text{relu}'(s) = \begin{cases} \frac{\partial L}{\partial a} & \text{if } s > 0 \\ 0 & \text{if } s \leq 0 \end{cases}

直观理解:ReLU 在正区间原样传递梯度(导数为 1),在负区间直接阻断(导数为 0)。这种”不衰减也不放大”的特性是 ReLU 能缓解梯度消失的关键原因。

第三步:穿过加法节点——计算 ∂L∂z\frac{\partial L}{\partial z} 和 ∂L∂b\frac{\partial L}{\partial b}

加法 s=z+bs = z + b 的偏导:∂s∂z=1\frac{\partial s}{\partial z} = 1,∂s∂b=1\frac{\partial s}{\partial b} = 1。所以:

∂L∂z=∂L∂s⋅1=∂L∂s,∂L∂b=∂L∂s⋅1=∂L∂s\frac{\partial L}{\partial z} = \frac{\partial L}{\partial s} \cdot 1 = \frac{\partial L}{\partial s}, \quad \frac{\partial L}{\partial b} = \frac{\partial L}{\partial s} \cdot 1 = \frac{\partial L}{\partial s}

直观理解:加法节点是”梯度分配器”——它把上游梯度原样复制给每个输入,不做任何缩放。

第四步:穿过乘法节点——计算 ∂L∂w\frac{\partial L}{\partial w} 和 ∂L∂x\frac{\partial L}{\partial x}

乘法 z=w⋅xz = w \cdot x 的偏导:∂z∂w=x\frac{\partial z}{\partial w} = x,∂z∂x=w\frac{\partial z}{\partial x} = w。所以:

∂L∂w=∂L∂z⋅x,∂L∂x=∂L∂z⋅w\frac{\partial L}{\partial w} = \frac{\partial L}{\partial z} \cdot x, \quad \frac{\partial L}{\partial x} = \frac{\partial L}{\partial z} \cdot w

直观理解:乘法节点是”梯度交换器”——对某个输入的梯度 = 上游梯度 × 另一个输入的值。如果 xx 很大,则 ww 的梯度也很大(ww 的微小变化会被放大);反之亦然。

通用规则:局部梯度 × 上游梯度

Section titled “通用规则:局部梯度 × 上游梯度”

对于任意节点 y=f(x1,x2,…,xn)y = f(x_1, x_2, \ldots, x_n),反向传播只需一个公式:

∂L∂xi=∂L∂y⋅∂f∂xi\boxed{\frac{\partial L}{\partial x_i} = \frac{\partial L}{\partial y} \cdot \frac{\partial f}{\partial x_i}}

即”上游梯度 × 局部梯度”。每个节点只需知道自己的局部导数 ∂f/∂xi\partial f / \partial x_i,不需要知道整个网络的结构——这就是模块化的优势,也是 PyTorch 自动微分引擎的设计基础。你可以在框架中定义任意自定义层,只要提供前向传播和局部导数(反向传播),框架就能自动将其接入整个计算图。

实际神经网络用矩阵运算表示。对于全连接层(Fully Connected Layer,也叫线性层):

s=Wx+b,a=σ(s)\mathbf{s} = \mathbf{W}\mathbf{x} + \mathbf{b}, \quad \mathbf{a} = \sigma(\mathbf{s})

其中 W∈Rm×n\mathbf{W} \in \mathbb{R}^{m \times n} 是权重矩阵,x∈Rn\mathbf{x} \in \mathbb{R}^{n} 是输入,b∈Rm\mathbf{b} \in \mathbb{R}^{m} 是偏置,σ\sigma 是激活函数。给定上游梯度 ∂L∂a\frac{\partial L}{\partial \mathbf{a}},反向传播的矩阵推导为:

∂L∂s=∂L∂a⊙σ′(s)(⊙ 表示逐元素乘法,Hadamard 积)\frac{\partial L}{\partial \mathbf{s}} = \frac{\partial L}{\partial \mathbf{a}} \odot \sigma'(\mathbf{s}) \quad \text{(}\odot\text{ 表示逐元素乘法,Hadamard 积)} ∂L∂W=∂L∂s⋅x⊤(外积,结果形状与 W 相同)\frac{\partial L}{\partial \mathbf{W}} = \frac{\partial L}{\partial \mathbf{s}} \cdot \mathbf{x}^\top \quad \text{(外积,结果形状与 } \mathbf{W} \text{ 相同)} ∂L∂b=∂L∂s(偏置梯度 = 上游梯度,逐元素)\frac{\partial L}{\partial \mathbf{b}} = \frac{\partial L}{\partial \mathbf{s}} \quad \text{(偏置梯度 = 上游梯度,逐元素)} ∂L∂x=W⊤⋅∂L∂s(传给前一层的梯度)\frac{\partial L}{\partial \mathbf{x}} = \mathbf{W}^\top \cdot \frac{\partial L}{\partial \mathbf{s}} \quad \text{(传给前一层的梯度)}

注意 ∂L∂W=∂L∂s⋅x⊤\frac{\partial L}{\partial \mathbf{W}} = \frac{\partial L}{\partial \mathbf{s}} \cdot \mathbf{x}^\top 是一个外积(Outer Product),每个权重的梯度等于”上游传来的误差信号 × 对应位置的输入值”。这个矩阵公式就是反向传播在所有主流框架中的实际计算形式——GPU 通过高度优化的矩阵乘法(GEMM)来执行。

为什么反向传播是 O(1)O(1) 次而非 O(N)O(N) 次前向传播? 因为矩阵形式下,∂L∂W=∂L∂s⋅x⊤\frac{\partial L}{\partial \mathbf{W}} = \frac{\partial L}{\partial \mathbf{s}} \cdot \mathbf{x}^\top 一次矩阵运算就同时算出了该层所有权重(m×nm \times n 个参数)的梯度。每一层都是如此,全部层的梯度只需一次从后到前的遍历。

实际训练不会逐样本算梯度,而是用小批量(Mini-batch)——同时处理 BB 个样本。此时 X∈Rn×B\mathbf{X} \in \mathbb{R}^{n \times B},前向传播和反向传播的矩阵形式不变,梯度变为 BB 个样本梯度的平均:

∂L∂W=1B⋅∂L∂S⋅X⊤\frac{\partial L}{\partial \mathbf{W}} = \frac{1}{B} \cdot \frac{\partial L}{\partial \mathbf{S}} \cdot \mathbf{X}^\top

批量处理让 GPU 的矩阵运算单元(Tensor Core)能充分发挥并行能力,是现代训练效率的基础。

单节点反向传播流程(通用模式)

Section titled “单节点反向传播流程(通用模式)”
import torch
# 构建计算图:y = relu(w * x + b),目标值 3.0
x = torch.tensor(2.0)
w = torch.tensor(1.5, requires_grad=True) # requires_grad=True 告诉 PyTorch 追踪该张量的梯度
b = torch.tensor(0.5, requires_grad=True)
# 前向传播(每步自动记录到计算图)
z = w * x # z = 3.0
a = torch.relu(z) # a = 3.0(z>0,relu 不截断)
loss = 0.5 * (a - 3.0) ** 2 # loss = 0.0
# 反向传播:一行代码自动算出所有梯度
loss.backward()
print(f"dL/dw = {w.grad}") # 梯度自动计算,结果 = 0.0(因为 loss=0 时梯度为 0)
print(f"dL/db = {b.grad}") # PyTorch 沿计算图反向递推
# 手动梯度下降一步(实际中用 optimizer.step() 自动完成)
lr = 0.01
w.data -= lr * w.grad
b.data -= lr * b.grad
w.grad.zero_() # 清空梯度,否则下次 backward() 会累加
b.grad.zero_()

为什么需要 zero_grad()? PyTorch 的梯度默认是累加的(.grad += new_grad),这是为了支持梯度累积(Gradient Accumulation)——在显存不够时,把一个大 batch 拆成多个小 batch 累计梯度再更新。但如果不需要这个行为,必须在每次 backward() 前清零,否则梯度会越来越大。

示例 2:NumPy 手写两层网络的反向传播

Section titled “示例 2:NumPy 手写两层网络的反向传播”

这个例子帮助你从底层理解反向传播到底在做什么——没有任何自动微分引擎,纯手工链式法则。

import numpy as np
# 两层网络: input(3) → hidden(4, relu) → output(1, sigmoid)
np.random.seed(42)
X = np.random.randn(3, 1) # 输入 (3×1)
Y = np.array([[1.0]]) # 目标 (1×1)
W1 = np.random.randn(4, 3) * 0.5 # 第一层权重 (4×3)
W2 = np.random.randn(1, 4) * 0.5 # 第二层权重 (1×4)
# ========== 前向传播 ==========
Z1 = W1 @ X # 线性变换 (4×1)
A1 = np.maximum(0, Z1) # ReLU 激活 (4×1)
Z2 = W2 @ A1 # 线性变换 (1×1)
A2 = 1 / (1 + np.exp(-Z2)) # Sigmoid 激活 (1×1)
# 二元交叉熵损失(Binary Cross-Entropy,用于二分类)
eps = 1e-8
loss = -np.mean(Y * np.log(A2 + eps) + (1 - Y) * np.log(1 - A2 + eps))
# ========== 反向传播(链式法则逐层求导)==========
# 输出层梯度:sigmoid + 交叉熵的合并导数恰好是 (A2 - Y)
dA2 = (A2 - Y) # dL/dA2 (1×1)
dW2 = dA2 @ A1.T # dL/dW2 = dL/dZ2 · A1ᵀ (1×4)
# 穿过 W2 传回隐藏层,再穿过 ReLU
dA1 = W2.T @ dA2 # dL/dA1 (4×1)
dZ1 = dA1 * (Z1 > 0) # ReLU 的导数:正区间为 1,否则为 0 (4×1)
dW1 = dZ1 @ X.T # dL/dW1 = dL/dZ1 · Xᵀ (4×3)
# ========== 梯度下降更新 ==========
lr = 0.1
W1 -= lr * dW1
W2 -= lr * dW2
print(f"Loss: {loss:.4f}")
print(f"W1 grad shape: {dW1.shape}, W2 grad shape: {dW2.shape}")

将前向、反向、更新组合成一个完整的训练循环,这是所有深度学习训练的标准范式:

import torch
import torch.nn as nn
# 简单的两层网络
model = nn.Sequential(
nn.Linear(10, 64),
nn.ReLU(),
nn.Linear(64, 1),
nn.Sigmoid(),
)
criterion = nn.BCELoss() # 二元交叉熵损失
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
# 模拟数据
X = torch.randn(100, 10) # 100 个样本,每个 10 维
Y = torch.randint(0, 2, (100, 1)).float() # 随机标签
# 标准训练循环(每个 epoch 遍历一次全部数据)
for epoch in range(50):
# 1. 前向传播:输入数据,得到预测值
pred = model(X)
loss = criterion(pred, Y)
# 2. 反向传播:计算所有参数的梯度
optimizer.zero_grad() # 清空上一步的残余梯度
loss.backward() # 自动反向传播!
# 3. 参数更新:沿梯度反方向走一步
optimizer.step()
if epoch % 10 == 0:
print(f"Epoch {epoch}, Loss: {loss.item():.4f}")

这三步——前向、反向、更新——就是深度学习训练的”心跳”。无论模型是简单的 MLP 还是千亿的 GPT,训练循环的结构都是这个三步曲的变体。大规模训练只是在此基础上增加了分布式并行、混合精度、梯度累积等加速手段,详见分布式训练和混合精度训练。

PyTorch 采用动态计算图(Dynamic Computational Graph)——每次前向传播时实时构建计算图,backward() 后立即销毁。这叫”Define-by-Run”,好处是:

  • 可以用 Python 的 if/for/while 控制流,计算图随代码执行动态变化
  • 调试方便:可以在任何地方打断点检查张量值
  • 支持动态结构(如变长序列、条件计算)

代价是每次前向传播都要重新构建计算图,优化空间不如静态图(TensorFlow 1.x 的 “Define-and-Run” 模式)。但 PyTorch 2.0 引入的 torch.compile 通过 JIT 编译部分弥补了这个差距。

PyTorch 的自动微分引擎叫 Autograd,核心机制如下:

  1. 前向传播时:每个 requires_grad=True 的张量都有一个 .grad_fn 属性,指向创建它的函数(如 MulBackward0、AddBackward0)。这些 grad_fn 组成反向传播的”路线图”。
  2. 调用 .backward() 时:从 loss 开始,沿着 grad_fn 链表逆序遍历,在每个节点执行 forward 操作对应的 backward 函数,将上游梯度乘以局部梯度,得到下游梯度。
  3. 梯度累积:同一个叶子节点(leaf tensor,即用户直接创建的 requires_grad=True 张量)被多次使用时,来自不同路径的梯度会相加(多路径链式法则:∂L∂x=∑i∂L∂yi⋅∂yi∂x\frac{\partial L}{\partial x} = \sum_i \frac{\partial L}{\partial y_i} \cdot \frac{\partial y_i}{\partial x})。
import torch
# 验证梯度累积:x 同时用于两条路径
x = torch.tensor(3.0, requires_grad=True)
y = x * 2 # 路径 1
z = x * x # 路径 2
loss = y + z # loss = 2x + x²
loss.backward()
print(f"x.grad = {x.grad}") # dL/dx = 2 + 2x = 2 + 6 = 8.0
# 两条路径的梯度 2(来自 y)和 6(来自 z)自动相加

当内置操作不够用时,可以通过继承 torch.autograd.Function 定义自定义的前向传播和反向传播:

from torch.autograd import Function
class MyReLU(Function):
@staticmethod
def forward(ctx, input):
ctx.save_for_backward(input) # 保存前向输入,供反向使用
return input.clamp(min=0) # 前向:ReLU
@staticmethod
def backward(ctx, grad_output):
input, = ctx.saved_tensors # 取回前向保存的值
grad_input = grad_output.clone()
grad_input[input < 0] = 0 # 反向:ReLU 的导数(正区间为 1)
return grad_input
# 使用:my_relu = MyReLU.apply

这展示了反向传播的模块化本质:你只需要告诉框架”前向怎么算”和”局部导数怎么算”,框架负责链式法则的传递。

问题:Sigmoid 和 Tanh 激活函数的导数最大值分别只有 0.25 和 1.0。当网络有很多层时,反向传播每经过一层,梯度就要乘以一个小于 1 的数:

∂L∂x0=∂L∂an∏l=1n∂al∂sl⋅∂sl∂xl−1\frac{\partial L}{\partial \mathbf{x}_0} = \frac{\partial L}{\partial \mathbf{a}_n} \prod_{l=1}^{n} \frac{\partial \mathbf{a}_l}{\partial \mathbf{s}_l} \cdot \frac{\partial \mathbf{s}_l}{\partial \mathbf{x}_{l-1}}

连乘 nn 个 0.25 就是 0.25n0.25^n——20 层网络中梯度衰减到 0.2520≈10−120.25^{20} \approx 10^{-12},浅层参数几乎收不到学习信号。这是 1990 年代到 2000 年代深度学习”寒冬”的核心技术原因之一。

解决方案:

  • ReLU 及其变体(Leaky ReLU、GELU、SwiGLU):导数为 0 或 1,不衰减梯度。详见激活函数。
  • 残差连接(Residual Connection / Skip Connection):ResNet 引入的 al+1=al+f(al)\mathbf{a}_{l+1} = \mathbf{a}_l + f(\mathbf{a}_l),梯度可以跳过非线性层直接传播。详见 ResNet。
  • Layer Normalization(层归一化):每层归一化激活值分布,间接稳定梯度流。
  • 合理的权重初始化:让前向激活值和反向梯度的方差都保持在 1 附近(如 Xavier/He 初始化)。详见权重初始化。

问题:RNN(循环神经网络)中权重矩阵在时间维度上反复相乘,当特征值大于 1 时,梯度指数增长,导致 NaN(Not a Number,数值溢出)。

∂L∂h0=∂L∂hT∏t=1T∂ht∂ht−1\frac{\partial L}{\partial \mathbf{h}_0} = \frac{\partial L}{\partial \mathbf{h}_T} \prod_{t=1}^{T} \frac{\partial \mathbf{h}_t}{\partial \mathbf{h}_{t-1}}

连乘 TT 个大于 1 的矩阵,梯度爆炸。

解决方案:

  • 梯度裁剪(Gradient Clipping):当梯度范数超过阈值时按比例缩小:
g←τmax⁡(∥g∥,τ)⋅g\mathbf{g} \leftarrow \frac{\tau}{\max(\|\mathbf{g}\|, \tau)} \cdot \mathbf{g}

其中 τ\tau 是预设阈值。详见RNN 与序列模型。

  • 门控机制(LSTM/GRU):通过遗忘门、输入门控制信息流,缓解梯度爆炸/消失。
  • Layer Normalization:同样有助于稳定训练。

梯度检查点(Activation Checkpointing / Gradient Checkpointing)

Section titled “梯度检查点(Activation Checkpointing / Gradient Checkpointing)”

训练大型模型时,前向传播需要保存大量中间激活值(供反向传播使用),这些激活值是显存的主要消耗来源。梯度检查点技术只保存部分层的激活值,其余的在反向传播时重新计算:

策略显存计算量适用场景
保存所有激活O(n)O(n)O(n)O(n)小模型
梯度检查点O(n)O(\sqrt{n})≈1.5×O(n)\approx 1.5 \times O(n)大模型,显存不足

这是一种”以计算换显存”的权衡,是训练百亿参数以上模型的标准技术。详见分布式训练。

  • 梯度消失:sigmoid/tanh 的导数最大值分别是 0.25 和 1,多层网络反向传播时梯度逐层衰减,深层几乎学不到东西。这是 ReLU(导数为 0 或 1)取代 sigmoid 的主要原因。详见激活函数。
  • 梯度爆炸:RNN 中权重反复相乘,梯度指数增长导致 NaN。用梯度裁剪(gradient clipping)限制梯度范数。详见RNN 与序列模型。
  • requires_grad 和计算图:PyTorch 中只有 requires_grad=True 的张量才会被追踪梯度。调用 .backward() 后计算图被释放(默认),再次 backward() 会报错——需要 retain_graph=True 才能复用。
  • detach() 和 torch.no_grad():detach() 把张量从计算图分离(不再追踪梯度),torch.no_grad() 上下文管理器暂停梯度追踪。推理和评估时务必用后者,节省内存并加速。
  • 梯度累积(Gradient Accumulation):显存不够时,可以把大 batch 拆成多个小 batch,每次只做前向+反向但不更新参数,累积几步梯度后再统一更新。等效于用更大的 batch size 训练。
  • 混合精度训练:前向传播用 FP16/BF16 加速、反向传播自动缩放梯度防止下溢,是现代大模型训练的标配。详见混合精度训练。
  • 检查梯度正确性:用 torch.autograd.gradcheck 做数值梯度检验——用有限差分法验证自动微分结果是否正确,开发自定义层时必备。原理是对每个参数手动做数值微分 ∂L∂w≈L(w+ϵ)−L(w−ϵ)2ϵ\frac{\partial L}{\partial w} \approx \frac{L(w+\epsilon) - L(w-\epsilon)}{2\epsilon},与 autograd 的结果对比。
  • 高阶导数:PyTorch 和 JAX 都支持高阶导数(梯度的梯度),用于元学习(MAML)、物理信息神经网络(PINN)等场景。JAX 的函数式风格让 grad(grad(f)) 非常自然。
  • 所有深度学习训练:PyTorch 的 loss.backward() 和 TensorFlow 的 GradientTape 内部执行的就是反向传播。详见梯度下降与优化器。
  • Transformer 训练:自注意力机制(Self-Attention)的反向传播需要处理 softmax 和矩阵乘法的梯度链,是 GPT/BERT 训练的计算核心。一个关键优化是 Flash Attention 算法,它融合了前向和反向传播中 softmax 相关的多个操作,大幅减少显存读写。详见Transformer 架构和Flash Attention。
  • CNN 训练:卷积层的反向传播等价于反卷积(转置卷积),ReLU 的反向传播只需掩码。详见卷积神经网络。
  • 神经网络可解释性:Grad-CAM 等方法用反向传播提取梯度,生成显著性图(saliency map)解释模型关注了图像的哪些区域。
  • 对抗样本生成:FGSM(快速梯度符号法,Fast Gradient Sign Method)用反向传播计算输入的梯度(而非参数梯度),沿梯度符号方向扰动输入来欺骗模型。这是对抗训练(Adversarial Training)的基础。
  • 元学习(Meta-Learning):MAML(Model-Agnostic Meta-Learning)需要在梯度上再算梯度(二阶导数),即”通过反向传播的反向传播”来学习如何快速适应新任务。
类库语言说明
torch.autogradPythonPyTorch 自动微分引擎,动态计算图,loss.backward() 一行搞定
TensorFlow GradientTapePythonTensorFlow 2.x 的自动微分 API,在 tape 上下文中记录前向操作
JAX gradPython函数式自动微分,grad(f) 返回 f 的梯度函数,支持高阶导数和 vmap 批量求导
autogradPython经典自动微分库(HIPS),JAX 的前身,NumPy 接口
MicroGradPythonKarpathy 写的教学级自动微分引擎(约 100 行),理解反向传播的最佳读物
术语英文解释
反向传播Backpropagation用链式法则反向计算损失对每个参数梯度的算法
计算图Computational Graph用有向无环图表示运算和数据依赖关系
前向传播Forward Pass输入数据经网络逐层计算到输出的过程
链式法则Chain Rule复合函数求导法则:dLdx=dLdy⋅dydx\frac{dL}{dx} = \frac{dL}{dy} \cdot \frac{dy}{dx}
局部梯度Local Gradient当前节点输出对输入的偏导数
上游梯度Upstream Gradient从损失传到当前节点的梯度
动态计算图Dynamic Graph每次前向传播时构建计算图(PyTorch 模式),灵活易调试
静态计算图Static Graph先定义计算图再执行(TensorFlow 1.x 模式),利于全局优化
自动微分Automatic Differentiation自动计算精确梯度的技术,反向模式即反向传播
梯度消失Vanishing Gradient深层网络梯度逐层衰减至零,导致浅层无法学习
梯度爆炸Exploding Gradient深层网络梯度指数增长,导致参数更新发散
梯度累积Gradient Accumulation多次小 batch 的梯度累加后再更新,等效于更大 batch size
激活检查点Activation Checkpointing只保存部分激活值,其余反向时重算,以计算换显存
  • Rumelhart, Hinton & Williams,「Learning Representations by Back-propagating Errors」(Nature 1986):反向传播的里程碑论文,让多层神经网络训练变得可行,深度学习的奠基工作之一。
  • Karpathy,「MicroGrad: a tiny autograd engine」:约 100 行 Python 实现完整的自动微分引擎,是理解反向传播实现细节的最佳教学资源(GitHub 开源)。
  • Goodfellow, Bengio & Courville,「Deep Learning」第 6 章:深度学习圣经中对反向传播的计算图推导和链式法则讲解最为系统。
  • Baydin et al.,「Automatic Differentiation in Machine Learning: a Survey」(JMLR 2018):自动微分综述,梳理前向模式与反向模式的区别,以及与数值微分、符号微分的关系。
  • Paske et al.,「Automatic Differentiation in PyTorch」(2017):PyTorch autograd 的白皮书,解释动态计算图的设计哲学与实现。

反向传播自 1986 年提出至今近 40 年,其核心算法本身没有变化——链式法则不会过时。但围绕反向传播的工程实现、内存优化、替代方案在 2025-2026 年有显著进展。

反向传播的内存瓶颈与低秩梯度投影(GaLore)

Section titled “反向传播的内存瓶颈与低秩梯度投影(GaLore)”

随着模型规模扩展到千亿甚至万亿参数,反向传播需要存储的梯度和优化器状态成为显存的主要瓶颈。一个 70B 参数的模型,仅 Adam 优化器状态就需要约 1.1 TB 显存(FP32 精度)。

2024-2025 年,GaLore(Gradient Low-Rank Projection) 成为热门技术:它不存储完整的梯度矩阵,而是将梯度投影到低秩子空间中,将优化器状态的内存从 O(d1×d2)O(d_1 \times d_2) 降低到 O(min⁡(d1,d2)×r)O(\min(d_1, d_2) \times r)(rr 是秩,通常远小于维度)。与 LoRA(低秩微调)不同,GaLore 不改变模型结构,只改变梯度在优化器中的表示方式,因此可以用于预训练而非仅限微调。这使得在单张 24GB 消费级 GPU 上预训练 7B 模型成为可能。

PyTorch 2.x 引入的 torch.compile(底层使用 TorchDynamo + Triton 编译器)在 2025 年趋于成熟。它将 Python 动态计算图编译为优化的静态图,对反向传播的优化包括:

  • 算子融合(Operator Fusion):将多个逐元素操作(如 ReLU + 偏置加法 + 损失计算)合并为单个 GPU kernel,减少显存读写。反向传播中,激活函数的导数计算可以与相邻的矩阵乘法融合。
  • 自动激活检查点:编译器自动分析计算图,决定哪些中间值值得保存、哪些适合重算。
  • 生成优化的反向传播 kernel:使用 OpenAI 的 Triton 语言为每个前向操作生成对应的、特定于 GPU 架构的反向传播 kernel,避免 Python 解释器开销。

在 2025 年的基准测试中,torch.compile 对训练循环的端到端加速通常在 1.3x-2.5x,对纯反向传播部分可达 3x 以上。

Triton(OpenAI 开发的 GPU kernel 编程语言)在 2025 年成为编写高性能反向传播 kernel 的首选工具。Flash Attention v2/v3、Megatron-LM 的张量并行通信、DeepSpeed 的 Zero 优化器,其核心 kernel 大量用 Triton 重写。相比直接写 CUDA C++,Triton 的抽象层级更高(类似 Python),但性能接近手写 CUDA。

一个典型应用是融合反向传播 kernel:将 softmax 反向 + 矩阵乘法梯度计算 + dropout 反向融合成一个 kernel,避免中间结果落回显存,这在 Transformer 训练中可节省 30-50% 的反向传播时间。

尽管反向传播是绝对主流,学术界一直在探索生物上更合理的学习算法。真正的生物神经元不执行反向传播——大脑中没有”全局损失函数”和”精确梯度传递”。

  • Forward-Forward Algorithm(FF):由 Geoffrey Hinton 在 2022 年底提出,FF 用两个前向传播(正样本 + 负样本)替代前向+反向,避免了反向传播的非局部梯度传递。到 2025 年,FF 在小规模任务上取得了一些进展,但在 ImageNet 等大规模任务上仍远不如反向传播,目前更多是理论探索。
  • Equilibrium Propagation(平衡传播):基于循环网络的能量模型,在收敛状态附近用局部信号近似全局梯度。2025 年有研究将其扩展到现代 Transformer 架构,但训练效率仍远低于反向传播。
  • Predictive Coding(预测编码):每个神经元层预测下一层的输入,用预测误差作为学习信号。这种方案天然支持局部计算(每层只需与相邻层通信),被认为是大脑皮层可能的计算原理之一。2025 年的研究开始将预测编码与大型语言模型结合。

截至 2026 年,反向传播在工程效率上的优势依然碾压所有替代方案,但这些探索为理解生物学习机制和设计新型神经形态芯片(Neuromorphic Chip)提供了重要启发。

2025 年,NVIDIA H100/H200 GPU 和 Google TPU v5/v6 的设计越来越围绕反向传播的内存瓶颈优化:

  • HBM3e 高带宽显存:2025 年量产的 HBM3e 提供 1.2 TB/s 以上带宽,缓解反向传播时读取激活值和写入梯度的内存墙问题。
  • 稀疏梯度加速:硬件支持 2:4 结构化稀疏(Structured Sparsity),反向传播时只计算非零位置的梯度,理论翻倍计算密度。
  • FP8 训练:H100 原生支持 FP8(8 位浮点)前向和反向传播,将激活值存储减半、梯度计算翻倍。2025 年,FP8 训练在 LLM 预训练中被广泛采用,精度损失已可忽略。

这些硬件进步意味着,反向传播——这个 40 年前的算法——在未来几年仍将是深度学习训练的核心。创新不在于替代它,而在于让它在新的硬件上跑得更快、更省内存。