机器学习数学基础专题【左扬精讲】—— 前向传播与反向传播:从数据流到梯度流的完整闭环
机器学习数学基础专题【左扬精讲】—— 前向传播与反向传播:从数据流到梯度流的完整闭环
在前面的梯度下降系列文章中,我们反复提到两个核心概念:前向传播(Forward Propagation)和反向传播(Backpropagation)。这两个概念构成了神经网络的"任督二脉"——前向传播是数据从输入到输出的流动过程,反向传播是梯度从输出到输入的回传过程。没有前向传播,神经网络无法产生预测;没有反向传播,神经网络无法学习。
本文将从 What-How-Why 三段式框架出发,不带任何生活类比,纯粹从数学和计算图的角度,把这两个核心机制讲清楚。所有公式均来自深度学习领域的公认定义(主要参考 Ian Goodfellow 的《深度学习》第三章)。
本文涉及的开源库/工具:
torch (PyTorch) ← autograd 自动微分实现
torch.nn ← 神经网络模块封装
torch.optim ← 优化器(SGD/Adam)
numpy ← 数值计算验证
核心概念:
前向传播 (Forward Propagation) ← 数据流:输入 → 输出
反向传播 (Backpropagation) ← 梯度流:输出 → 输入
链式法则 (Chain Rule) ← 连接前向与反向的数学纽带
计算图 (Computational Graph) ← PyTorch autograd 的核心数据结构
深度学习 前向传播 反向传播 链式法则 计算图 梯度下降 PyTorch
学习重点提示
★ 必须掌握
- 必须掌握:前向传播的定义——输入数据如何通过网络层层计算,最终产生输出
- 必须掌握:反向传播的定义——如何通过链式法则高效计算损失函数对每个参数的梯度
- 必须掌握:前向传播与反向传播的时序关系——先 forward 再 backward
- 必须掌握:链式法则是反向传播的数学基础,理解链式法则才能理解 BP
- 必须掌握:反向传播不是学习算法,而是梯度计算方法;真正的学习算法是 SGD/Adam 等优化器
★ 建议掌握
- 建议掌握:计算图(Computational Graph)的概念及其在前向/反向传播中的作用
- 建议掌握:反向传播的三种模式——符号式、命令式、混合式(以 PyTorch 为代表的动态图)
- 建议掌握:梯度消失与梯度爆炸在反向传播中的表现
- 建议掌握:PyTorch 中 tensor.backward() 的底层机制
本文目录
- 开篇:从一个例子理解前向与反向的关系
- 一、前向传播(Forward Propagation)—— What & How
- 二、反向传播(Backpropagation)—— What & How
- 三、链式法则——前向与反向的数学纽带
- 四、前向传播与反向传播的对比
- 五、PyTorch 源码视角:autograd 如何实现反向传播
- 六、常见问题 FAQ(20 组)
- 七、Roadmap 后续预告
一、开篇:从一个例子理解前向与反向的关系
在深入理论之前,我们先用一个最简单的例子建立直观认识。假设有一个单层神经网络,用于二分类:
import torch
# 输入: x = [1.0, 2.0]
# 真实标签: y = 1.0
# 权重: W = [[0.5, 0.3], [0.2, 0.4]] (2x2 权重矩阵)
# 偏置: b = [0.1, 0.2]
x = torch.tensor([1.0, 2.0], requires_grad=True)
W = torch.tensor([[0.5, 0.3], [0.2, 0.4]], requires_grad=True)
b = torch.tensor([0.1, 0.2], requires_grad=True)
y = torch.tensor([1.0])
# ===== 前向传播 =====
z = torch.matmul(W, x) + b # 线性变换: z = Wx + b
a = torch.sigmoid(z) # 激活函数: a = sigmoid(z)
loss = torch.binary_cross_entropy(a, y) # 损失函数
print(f"前向传播输出: {a}")
# ===== 反向传播 =====
loss.backward()
print(f"W 的梯度: {W.grad}")
print(f"b 的梯度: {b.grad}")
print(f"x 的梯度: {x.grad}")
核心观察:前向传播与反向传播的时序关系
在这个例子中:
- 前向传播:x → z = Wx + b → a = sigmoid(z) → loss。数据从输入流向输出。
- 反向传播:调用 loss.backward() 后,梯度从 loss 开始,沿着与前向相反的方向传播,最终计算出 W.grad、b.grad、x.grad。
- 时序关系:必须先完成前向传播,才能进行反向传播。
二、前向传播(Forward Propagation)—— What & How
2.1 前向传播的定义(What)
What — 前向传播是什么?
前向传播(Forward Propagation)是指神经网络在推理阶段,输入数据从输入层开始,依次经过每一层的线性变换(矩阵乘法 + 偏置)和非线性激活函数,最终到达输出层,产生预测结果的过程。
在数学上,对于一个有 L 层的神经网络,前向传播可以表示为:
a^(l) = f^(l)(z^(l)) = f^(l)(W^(l) · a^(l-1) + b^(l))
其中:
- a^(l) 是第 l 层的输出(激活值),也是第 l+1 层的输入
- W^(l) 是第 l 层的权重矩阵
- b^(l) 是第 l 层的偏置向量
- f^(l) 是第 l 层的激活函数
- z^(l) = W^(l) · a^(l-1) + b^(l) 是线性变换的结果
2.2 前向传播的数学推导(How)
How — 前向传播是怎么计算的?
让我们以一个三层神经网络为例,详细推导前向传播的计算过程:
输入层: a^(0) = x ∈ R^(d_in)
隐藏层1: z^(1) = W^(1) · a^(0) + b^(1), a^(1) = f(z^(1))
隐藏层2: z^(2) = W^(2) · a^(1) + b^(2), a^(2) = f(z^(2))
输出层: z^(3) = W^(3) · a^(2) + b^(3), a^(3) = output
以一个具体的数值例子来说明。假设:
- 输入:x = [1.0, 2.0],即 a^(0) = [1.0, 2.0]
- 第一层权重:W^(1) = [[1, 0], [0, 1]],b^(1) = [0, 0],激活函数:ReLU
# 前向传播数值示例
import torch
# 输入
x = torch.tensor([1.0, 2.0])
# 第一层
W1 = torch.tensor([[1.0, 0.0], [0.0, 1.0]])
b1 = torch.tensor([0.0, 0.0])
z1 = torch.matmul(W1, x) + b1 # z1 = [1.0, 2.0]
a1 = torch.relu(z1) # a1 = [1.0, 2.0] (ReLU: max(0, x))
# 第二层
W2 = torch.tensor([[1.0, 1.0]])
b2 = torch.tensor([0.5])
z2 = torch.matmul(W2, a1) + b2 # z2 = [1.0*1.0 + 1.0*2.0 + 0.5] = [3.5]
a2 = torch.sigmoid(z2) # a2 = sigmoid(3.5) ≈ 0.9707
print(f"z1 (第一层线性变换): {z1}")
print(f"a1 (第一层激活): {a1}")
print(f"z2 (第二层线性变换): {z2}")
print(f"a2 (第二层输出): {a2}")
2.3 为什么需要前向传播?(Why)
Why — 为什么需要前向传播?
前向传播是神经网络的预测过程,它的价值在于:
- 推理与预测:训练完成后,前向传播是模型产生预测结果的唯一方式。没有前向传播,模型无法回答任何问题。
- 学习的前提:反向传播需要先计算损失函数,而损失函数需要前向传播产生的预测结果。
没有前向传播会发生什么?
- 模型无法产生任何输出,无法进行推理
- 无法计算损失函数,无法评估预测质量
- 整个深度学习系统瘫痪
三、反向传播(Backpropagation)—— What & How
3.1 反向传播的定义(What)
What — 反向传播是什么?
反向传播(Backpropagation,简称 BP)是一种高效计算梯度的算法。它利用链式法则,从输出层开始,沿与前向传播相反的方向,逐层计算损失函数对每个参数的梯度。
关键区分(来自 Wikipedia "Backpropagation" 条目原文):
- 反向传播不是学习算法,而是一种梯度计算方法
- 真正的学习算法是随机梯度下降(SGD)、Adam 等优化器,它们使用反向传播计算出的梯度去更新权重
3.2 反向传播的数学推导(How)
How — 反向传播是怎么计算的?
反向传播的核心是链式法则。假设我们有一个损失函数 L,它最终依赖于第 l 层的权重 W^(l)。通过链式法则:
∂L/∂W^(l) = ∂L/∂a^(L) · ∂a^(L)/∂z^(L) · … · ∂a^(l+1)/∂z^(l+1) · ∂z^(l+1)/∂a^(l) · ∂a^(l)/∂z^(l) · ∂z^(l)/∂W^(l)
这个公式看起来复杂,但核心思想是:梯度从后往前传,每层只计算自己的局部梯度。
以之前的三层网络为例,假设使用均方误差损失:
L = (1/2) ||y - a^(3)||²
# 反向传播数值示例
import torch
# 重新构建计算图
x = torch.tensor([1.0, 2.0], requires_grad=True)
W1 = torch.tensor([[1.0, 0.0], [0.0, 1.0]], requires_grad=True)
b1 = torch.tensor([0.0, 0.0], requires_grad=True)
W2 = torch.tensor([[1.0, 1.0]], requires_grad=True)
b2 = torch.tensor([0.5], requires_grad=True)
y = torch.tensor([1.0])
# 前向传播
z1 = torch.matmul(W1, x) + b1
a1 = torch.relu(z1)
z2 = torch.matmul(W2, a1) + b2
a2 = torch.sigmoid(z2)
loss = torch.mean((y - a2) ** 2)
print(f"前向传播 - 损失: {loss.item():.4f}")
# 反向传播
loss.backward()
print(f"\n反向传播 - 梯度:")
print(f"W1.grad:\n{W1.grad}")
print(f"b1.grad: {b1.grad}")
print(f"W2.grad: {W2.grad}")
print(f"b2.grad: {b2.grad}")
3.3 为什么需要反向传播?(Why)
Why — 为什么需要反向传播?
反向传播解决了深层神经网络梯度计算效率的问题。
假设我们用数值微分(Numerical Differentiation)计算每个参数的梯度:对于一个有 1000 万参数的神经网络,每次需要计算 1000 万次前向传播——这在计算上是不可能的。
反向传播利用动态规划的思想,将中间计算结果缓存复用,将复杂度从 O(N) 降低到 O(1)(每参数)。
没有反向传播会发生什么?
- 只能用数值微分,计算量巨大,无法训练深层网络
- 无法高效计算梯度,深度学习的训练成本将指数级上升
- 现代深度学习框架(PyTorch/TensorFlow)将无法工作
四、链式法则——前向与反向的数学纽带
4.1 链式法则的定义
What — 链式法则是什么?
链式法则(Chain Rule)是微积分中计算复合函数导数的基本规则。假设 y = f(g(x)),则:
dy/dx = (dy/dg) · (dg/dx)
在多变量情况下,假设 y = f(u1, u2, ...),且每个 ui = gi(x1, x2, ...),则:
∂y/∂xi = ∑k (∂y/∂uk) · (∂uk/∂xi)
4.2 链式法则在反向传播中的应用
How — 链式法则如何应用于反向传播?
在神经网络中,每个运算节点都应用链式法则。假设我们有:
L = f(z^(L)) = f(W^(L) · a^(L-1) + b^(L))
要计算 ∂L/∂W^(L):
∂L/∂W^(L) = (∂L/∂z^(L)) · (∂z^(L)/∂W^(L))
其中:
- ∂L/∂z^(L) 称为"局部梯度",可以通过上一层的梯度传递得到
- ∂z^(L)/∂W^(L) = a^(L-1) 是当前层的局部梯度
# 手动实现链式法则验证
import torch
# 手动计算梯度
x = torch.tensor([1.0, 2.0])
W = torch.tensor([[0.5, 0.3], [0.2, 0.4]], requires_grad=True)
b = torch.tensor([0.1, 0.2], requires_grad=True)
# 前向传播
z = torch.matmul(W, x) + b
a = torch.relu(z)
loss = torch.sum(a ** 2)
# PyTorch 自动求导
loss.backward()
# 手动验证梯度
# z = Wx + b, a = relu(z), loss = sum(a^2)
# dl/dz = d(sum(a^2))/da * da/dz
# d(sum(a^2))/da = 2a
# da/dz = relu'(z) = 1 if z > 0 else 0
# dl/dW = dl/dz * dz/dW = dl/dz * x^T
manual_dL_dz = torch.where(z > 0, 2 * a, torch.zeros_like(a))
manual_dL_dW = torch.outer(manual_dL_dz, x)
manual_dL_db = manual_dL_dz
print(f"PyTorch 计算的 dL/dW:\n{W.grad}")
print(f"手动计算的 dL/dW:\n{manual_dL_dW}")
print(f"梯度一致: {torch.allclose(W.grad, manual_dL_dW)}")
五、前向传播与反向传播的对比
核心对比
| 维度 | 前向传播(Forward) | 反向传播(Backward) |
|---|---|---|
| 方向 | 从输入到输出(正向) | 从输出到输入(反向) |
| 计算内容 | 激活值 a^(l)、线性变换结果 z^(l) | 损失函数对各参数的梯度 |
| 数学基础 | 矩阵乘法 + 激活函数 | 链式法则 |
| 作用阶段 | 推理阶段(Inference) | 训练阶段(Training) |
| 必须先执行? | 是反向传播的前提 | 需要前向传播的结果 |
| 输出 | 预测值、损失值 | 梯度(用于参数更新) |
| 是否必须 | 推理必须,训练也必须 | 训练必须,推理不需要 |
理解了前向传播和反向传播后,我们可以完整理解深度学习的训练流程:
# 深度学习训练的一个完整步骤
for epoch in range(num_epochs):
for batch in dataloader:
# Step 1: 前向传播 - 计算预测和损失
predictions = model(batch.x) # forward
loss = criterion(predictions, batch.y)
# Step 2: 反向传播 - 计算梯度
optimizer.zero_grad() # 清除旧梯度
loss.backward() # backward - 计算 dL/dW
# Step 3: 参数更新 - 使用优化器更新权重
optimizer.step() # W = W - lr * dL/dW
一个完整的训练步骤 = 1次前向 + 1次反向 + 1次参数更新
六、PyTorch 源码视角:autograd 如何实现反向传播
PyTorch 的自动微分(autograd)系统是实现反向传播的核心。理解它的实现,可以从根本上理解反向传播的工作原理。
源码视角一:Tensor 的 requires_grad 属性
当创建一个 Tensor 时,默认 requires_grad=False。设置为 True 后,PyTorch 会跟踪所有在该 Tensor 上的操作,构建计算图。
# requires_grad 决定了是否跟踪梯度
x = torch.tensor([1.0, 2.0], requires_grad=True) # 跟踪梯度
y = torch.tensor([3.0, 4.0]) # 不跟踪梯度
z = x * 2 # z 会构建计算图,依赖 x
w = y * 2 # w 不构建计算图,不依赖 y
z.backward() # OK,z 有 grad_fn
# w.backward() # ERROR,w 没有 grad_fn
源码视角二:计算图的构建
在 PyTorch 中,每个 Tensor 都存储了 grad_fn,它是 Function 的子类,记录了创建该 Tensor 的操作。计算图是一个有向无环图(DAG)。
# 查看 grad_fn
x = torch.tensor([1.0], requires_grad=True)
y = x * 2
z = y + 3
w = z ** 2
print(f"y.grad_fn: {y.grad_fn}") # <MulBackward0>
print(f"z.grad_fn: {z.grad_fn}") # <AddBackward0>
print(f"w.grad_fn: {w.grad_fn}") # <PowBackward0>
源码视角三:backward() 的底层实现
当调用 tensor.backward() 时,PyTorch 会:
- 从当前 Tensor 开始,沿着 grad_fn 反向遍历计算图
- 对每个节点应用链式法则,计算局部梯度
- 将梯度累积到 requires_grad=True 的叶节点的 grad 属性中
# 验证梯度累积机制
x = torch.tensor([1.0], requires_grad=True)
# 第一次 backward
y = x * 2
y.backward()
print(f"第一次后: x.grad = {x.grad}") # tensor([2.])
# 第二次 backward(梯度会累加!)
y = x * 2
y.backward()
print(f"第二次后: x.grad = {x.grad}") # tensor([4.])
# 正确的做法:每次前先清零
x = torch.tensor([1.0], requires_grad=True)
y = x * 2
y.backward()
print(f"清零前: x.grad = {x.grad}") # tensor([2.])
x.grad.zero_()
print(f"清零后: x.grad = {x.grad}") # tensor([0.])
y = x * 2
y.backward()
print(f"重新计算后: x.grad = {x.grad}") # tensor([2.])
源码视角四:retain_graph 参数
默认情况下,backward() 会释放计算图。如果需要多次反向传播,需要使用 retain_graph=True。
# 多次反向传播需要 retain_graph=True
x = torch.tensor([1.0], requires_grad=True)
y = x ** 2
# 默认情况:backward() 后计算图被释放
# y.backward() # OK
# y.backward() # ERROR: grad can be implicitly created only for scalar outputs
# 使用 retain_graph=True
y.backward(retain_graph=True)
print(f"第一次: x.grad = {x.grad}") # tensor([2.])
x.grad.zero_()
# 重新创建梯度
(y ** 1).backward() # 等价于 y.backward()
print(f"第二次: x.grad = {x.grad}") # tensor([2.])
七、常见问题 FAQ(20 组)
FAQ 常见问题解答
Q1. 前向传播和反向传播的时间复杂度是多少?
相同。对于一个有 L 层、每层有 n 个参数的神经网络,前向传播和反向传播的计算复杂度都是 O(L · n)。这是反向传播最重要的优势之一——它不会引入额外的计算开销。
Q2. 为什么反向传播比数值微分高效?
因为反向传播利用了计算图的结构。数值微分需要对每个参数单独做一次前向传播,时间复杂度是 O(N · M)(N 为参数量)。反向传播只做一次前向传播和一次反向传播,时间复杂度是 O(M)(M 为计算量),每个参数的梯度计算被复用。
Q3. 前向传播和反向传播的顺序能颠倒吗?
不能。反向传播需要前向传播的计算结果(激活值、中间变量)。必须先完成前向传播,才能进行反向传播。
Q4. 推理时需要反向传播吗?
不需要。推理(Inference)只需要前向传播来产生预测结果,不需要计算梯度。只有训练(Training)阶段才需要反向传播。
Q5. 反向传播是学习算法吗?
不是。反向传播只是梯度计算方法,真正的学习算法是 SGD、Adam 等优化器。它们使用反向传播计算的梯度去更新权重。
Q6. 为什么需要非线性激活函数?
如果没有非线性激活函数,多层神经网络会退化为单层。因为连续的线性变换等价于一个线性变换:W2 · (W1 · x + b1) + b2 = (W2 · W1) · x + (W2 · b1 + b2)。非线性激活函数打破了这种等价性,使网络能够学习非线性模式。
Q7. 反向传播和梯度下降是什么关系?
梯度下降是优化器使用的策略,反向传播是计算梯度的方法。梯度下降定义了参数更新的方向和步长(负梯度方向),反向传播负责计算这个梯度。
Q8. 什么是计算图?
计算图是表达数学表达式的有向无环图(DAG)。节点表示变量(Tensor),边表示操作(Function)。PyTorch 使用动态计算图,在前向传播时实时构建。
Q9. PyTorch 的动态图和 TensorFlow 1.x 的静态图有什么区别?
动态图在运行时构建,静态图在执行前构建并编译。PyTorch 使用动态图(eager execution),每个前向传播都会重新构建计算图。TensorFlow 1.x 使用静态图(graph execution),需要先定义再执行。TensorFlow 2.x 默认使用动态图。
Q10. 反向传播会梯度消失怎么办?
有多种解决方案【推测】:① 使用 ReLU 替代 Sigmoid/Tanh;② 使用残差连接(ResNet);③ 使用批量归一化(BatchNorm);④ 使用 LSTM/GRU 等特殊结构;⑤ 使用梯度裁剪防止梯度爆炸。
Q11. 什么是一次前向传播、一次反向传播?
这是深度学习训练的一个基本单位。一次前向传播计算损失值,一次反向传播计算梯度。一次完整的参数更新需要:前向传播 → 反向传播 → 参数更新。
Q12. 为什么梯度要清零(zero_grad)?
因为 PyTorch 默认会累积梯度。如果不清零,梯度会不断累加,导致参数更新错误。正确做法是在每个 batch 开始前调用 optimizer.zero_grad()。
Q13. 反向传播中哪些 Tensor 需要 requires_grad=True?
需要梯度的参数必须设置为 True。模型参数(权重、偏置)需要 requires_grad=True,输入数据通常不需要(除非是学习的内容),标签不需要。
Q14. 为什么说反向传播是"反向模式自动微分"?
因为它应用了反向模式的自动微分算法。自动微分有两种模式:前向模式(从输入到输出)和反向模式(从输出到输入)。对于神经网络(多输入少输出),反向模式更高效。
Q15. 梯度爆炸会导致什么后果?
参数更新步长过大。可能导致损失函数发散、权重变成 NaN、训练不稳定。解决方案包括梯度裁剪(gradient clipping)和合适的权重初始化(如 Xavier/He)。
Q16. 反向传播可以并行化吗?
可以,但有依赖关系【推测】。同一层的不同神经元可以并行计算,但层与层之间必须按顺序执行。现代 GPU 通过向量化操作实现层内并行,分布式训练实现多设备并行。
Q17. 什么是梯度检查(Gradient Checking)?
一种验证反向传播实现正确性的方法。用数值微分计算近似梯度,与反向传播计算的梯度对比,相对误差小于 1e-7 表示实现正确。这是调试神经网络的重要工具。
Q18. 反向传播计算图太大会有什么影响?
内存占用增大。计算图需要存储前向传播的中间变量用于反向传播。如果图太大,可能导致内存不足(OOM)。解决方案:使用 torch.no_grad() 推理、用 del 删除中间变量、减少 batch size。
Q19. PyTorch 中 tensor.backward() 和 optimizer.step() 的关系是什么?
backward() 计算梯度,step() 使用梯度更新参数。backward() 将梯度写入参数的 grad 属性,optimizer.step() 根据梯度更新参数(如 SGD 的 W = W - lr * grad)。
Q20. 深度学习框架如何选择使用 CPU 还是 GPU?
通过 .to(device) 或 .cuda() 移动张量。模型和数据都需要移动到同一设备(CPU 或 GPU)。GPU 适合大规模矩阵运算,可显著加速前向和反向传播。
本文核心知识点回顾
- 1 个核心区别:前向传播计算输出(数据流),反向传播计算梯度(梯度流)
- 1 个数学基础:链式法则是反向传播的核心
- 2 个必须顺序:必须先 forward 再 backward
- 3 个关键澄清:反向传播不是学习算法、是梯度计算方法、真正的学习是优化器
- 4 个常见问题:梯度消失、梯度爆炸、梯度清零、计算图内存
Roadmap 后续预告
在理解了前向传播和反向传播之后,后续我们将深入探讨以下主题:
- PyTorch autograd 进阶:自定义 autograd Function,实现前向和反向的完全控制
- 梯度下降家族:SGD、Momentum、Adam 等优化器的对比与选择
- 神经网络训练技巧:BatchNorm、Dropout、Early Stopping 等
- 分布式训练:DataParallel、DistributedDataParallel 原理

浙公网安备 33010602011771号