反向传播
从损失开始按链式法则逆序计算每个参数对损失的影响。
学习目标
- 手算链式梯度
- 理解 backward 职责
- 检查参数梯度
为什么需要这个概念
神经网络学习核心
连接损失与优化器
学习前需要掌握
背景与问题
前向传播得到损失之后,还需要知道每个参数对损失的局部影响,才能决定怎样更新。逐个参数重新试探会非常昂贵,尤其在同一中间计算被很多后续操作复用时。
反向传播利用计算图和链式法则复用这些局部导数,让一个标量损失对大量参数的梯度可以有组织地计算出来。理解它能帮助区分“前向数值错了”“梯度没流到参数”“更新步骤没有发生”等不同问题。
概念定义
反向传播是沿计算图逆序应用链式法则、计算损失对各个需要求导的量的梯度的方法。图中的节点记录运算及其依赖关系,反向时把来自下游的梯度与当前运算的局部导数结合。
自动微分框架可以自动组织这项计算,但反向传播本身不等于优化器更新。调用 backward 后通常得到的是参数的 grad;参数是否变化、怎样变化,仍由后续更新规则决定。
直观理解
前向是在问“用这组参数会算出什么”,反向是在问“如果只轻微改变某个参数,损失大约怎样变”。中间节点把一个很长的依赖链拆成许多可以检查的小关系。
如果同一个参数影响损失的多条路径,每条路径都会产生贡献,最后需要相加。不能只看距离损失最近的一条边,也不能把局部导数直接当成完整梯度。
核心原理
对链式计算,局部导数沿路径相乘;当多条路径汇合到同一个量时,梯度贡献相加。反向顺序保证计算某个节点时,依赖它的下游贡献已经准备好。
默认对标量损失反向,相当于从损失对自身的导数 1 开始。非标量输出需要说明上游梯度,或先使用有明确意义的归约得到标量;不能随意改变 sum/mean 而忽略它们对梯度尺度的影响。
在 PyTorch 中,叶子张量的 grad 通常是累计的。普通训练循环需要按计划清理梯度,重新前向建立当前参数对应的图,再反向和更新;意外累计旧梯度会改变实际优化过程。
默认的一次 backward 通常会释放反向需要的中间缓存。应区分重新前向计算与保留同一张图,避免通过反复设置保留图来掩盖训练循环问题。参数更新也不应作为需要继续求导的普通图运算随意原地执行。
数学表达
核心公式
链式法则连接损失和权重。
变量含义
L损失w权重z中间值
计算步骤
- 1前向:z = 0.5×2+1 = 2,损失 L = 2² = 4。
- 2损失对 z 的导数为 2z = 4;z 对 w 的导数为 x = 2,因此 dL/dw = 4×2 = 8。
- 3z 对 b 的导数为 1,因此 dL/db = 4×1 = 4。
- 4同步更新:w 新值为 0.5−0.1×8 = -0.3,b 新值为 1−0.1×4 = 0.6。
代码对应位置
示例 1对照手算梯度,再明确执行一次参数更新:把前向、反向和参数更新分开,核对每个阶段的数值。
变量解释
| 变量 | 含义 |
|---|---|
L | 损失 |
w | 权重 |
z | 中间值 |
完整数值示例
从 L=(wx+b)² 手算两个参数的梯度
已知条件
- 输入 x = 2,参数 w = 0.5、b = 1。
- 中间分数 z = wx+b,损失 L = z²,相当于目标值为 0 的一个平方误差。
- 演示更新使用学习率 0.1,且 w、b 使用同一次前向产生的梯度。
计算步骤
- 前向:z = 0.5×2+1 = 2,损失 L = 2² = 4。
- 损失对 z 的导数为 2z = 4;z 对 w 的导数为 x = 2,因此 dL/dw = 4×2 = 8。
- z 对 b 的导数为 1,因此 dL/db = 4×1 = 4。
- 同步更新:w 新值为 0.5−0.1×8 = -0.3,b 新值为 1−0.1×4 = 0.6。
- 用新参数重新前向:z = -0.3×2+0.6 = 0,损失变为 0。
计算结果
反向得到 dw=8、db=4;本例更新后的损失为 0(浮点计算显示到指定精度)。
处理前后对比
- 只执行 backward,w 仍然是 0.5,b 仍然是 1;得到梯度不等于已经更新。
- 如果把损失定义成 z²/2,梯度会少一个因子 2;比较实现前要先核对损失定义。
- 一步达到零损失只是这个单样本、特定学习率的构造结果,不是一般训练的保证。
计算与实现步骤
- 明确前向计算与标量损失的定义,检查输入和目标形状。
- 将需要学习的参数标记为可求导,并按训练策略清理已有梯度。
- 使用当前参数前向,保留计算图中的依赖关系。
- 对损失执行反向传播,核对 grad 是否存在、是否有限,以及数量级是否合理。
- 用优化器或明确的更新规则修改参数,再重新前向检查效果。
- 循环时正确处理梯度累计、图释放及训练/评估模式,而不是重复利用不合适的旧计算。
代码实现
示例 1
对照手算梯度,再明确执行一次参数更新
example_01.py用途:把前向、反向和参数更新分开,核对每个阶段的数值。
import torch
x = torch.tensor(2.0, dtype=torch.float64)
w = torch.tensor(0.5, dtype=torch.float64, requires_grad=True)
b = torch.tensor(1.0, dtype=torch.float64, requires_grad=True)
loss = (w * x + b).square()
loss.backward()
print(f"loss={loss.item():.4f}, dw={w.grad.item():.4f}, db={b.grad.item():.4f}")
print(f"before_step: w={w.item():.4f}, b={b.item():.4f}")
with torch.no_grad():
w -= 0.1 * w.grad
b -= 0.1 * b.grad
new_loss = (w * x + b).square()
print(f"after_step_loss={new_loss.item():.6f}")代码解析
解析始终位于完整代码下方,并按实际代码段逐项对应。
输入数据与任务
把前向、反向和参数更新分开,核对每个阶段的数值。
loss- 当前预测与目标之间的损失值。
w- 模型权重或当前待优化参数。
b- 偏置项。
Step 1 · 1–1 行
导入当前步骤需要的数值计算、预处理、模型或评价工具。依赖集中写在代码开头,便于复现。
import torchStep 2 · 3–14 行
计算损失对参数的梯度,对应数学表达中的 ∇J;梯度只给出局部变化方向。
x = torch.tensor(2.0, dtype=torch.float64)
w = torch.tensor(0.5, dtype=torch.float64, requires_grad=True)
b = torch.tensor(1.0, dtype=torch.float64, requires_grad=True)
loss = (w * x + b).square()
loss.backward()
print(f"loss={loss.item():.4f}, dw={w.grad.item():.4f}, db={b.grad.item():.4f}")
print(f"before_step: w={w.item():.4f}, b={b.item():.4f}")
with torch.no_grad():
w -= 0.1 * w.grad
b -= 0.1 * b.grad
new_loss = (w * x + b).square()
print(f"after_step_loss={new_loss.item():.6f}")代码与数学原理
参数更新由当前梯度与学习率共同决定。
平方误差代码对应均方损失。
预期输出或运行结果
loss=4.0000, dw=8.0000, db=4.0000 before_step: w=0.5000, b=1.0000 after_step_loss=0.000000
常见错误 · 4 条
- 认为 backward 会自动执行参数更新,或者把 grad 的数值当成参数新值。
- 忘记清理梯度,使多个本来不应累计的反向结果叠加。
- 在需要梯度的路径上意外 detach、转成普通数值,或在 no_grad 环境中计算训练损失。
- 不核对损失的 sum/mean、常数因子或批次大小,却认为梯度大小异常一定是优化器问题。
实际应用
- 训练由可微运算组成的神经网络,计算多个参数对任务损失的梯度。
- 在小型计算图中检查手算导数,定位梯度中断、形状或损失尺度错误。
- 按任务需要计算输入或中间表示的敏感性,但梯度本身不自动构成因果解释。
常见错误
- 认为 backward 会自动执行参数更新,或者把 grad 的数值当成参数新值。
- 忘记清理梯度,使多个本来不应累计的反向结果叠加。
- 在需要梯度的路径上意外 detach、转成普通数值,或在 no_grad 环境中计算训练损失。
- 不核对损失的 sum/mean、常数因子或批次大小,却认为梯度大小异常一定是优化器问题。
常见问题
backward 会更新参数吗?
不会,只计算梯度。
为何 loss 常归约为标量?
可直接从上游梯度 1 反传。
推荐学习资料
PyTorch Tutorials
从张量、数据加载、自动微分和网络训练逐步进入视觉、NLP、性能与部署。
PyTorch Contributors · PyTorch