AI × Physics课堂实践
PYTORCH / AUTOGRAD

梯度,如何走回参数?

从一个可以手算的模型出发,看看数值怎样算出来,
梯度又怎样沿着计算图传回去。

ŷ = wx + b   ·   L = (ŷ − y)2

输入 x = 2,目标 y = 5;初始参数 w = 3,b = 1。

预测 ŷ = 7   →   损失 L = 4
01

顺着算,反着求导

计算图记录一次计算由哪些运算组成,以及各个量之间的依赖关系。点击“下一步”查看梯度,再点击“更新参数”,观察参数沿梯度的反方向变化。

灰色:正向依赖蓝色:梯度反向传播橙色:参数更新
02

每经过一步,乘上局部导数

沿一条路径:相乘

以参数 w 为例:先看损失对误差的变化,再看误差对预测的变化,最后一路回到 w。

∂L/∂w = 2e × 1 × 1 × x
= 4 × 1 × 1 × 2 = 8

平方 → 减法 → 加法 → 乘法

同理,b 经过的路径更短:

∂L/∂b = 2e × 1 × 1 = 4

多条路径汇合:相加

如果同一个量通过多条路径影响损失,就把各条路径的梯度贡献相加。这也是更复杂网络中反向传播的基本规则。

局部导数和梯度是两回事

乘法节点 a = wx 对 w 的局部导数是 x = 2;但最终损失对 w 的梯度是 8,因为还要乘上后续运算传回来的梯度 4。

PyTorch 替我们按链式法则完成这些计算,不需要手动为整个网络推导公式。

optimizer.zero_grad()清除上一次存储的参数梯度
pred = model(x)正向计算预测,再计算损失
loss.backward()反向计算参数梯度
optimizer.step()根据梯度更新参数
03

在 PyTorch 中亲眼验证

把下面的代码放进一个 Notebook 单元格,完整运行一次。它与上面的计算图一一对应,只依赖 PyTorch。

autograd_demo.py
下载代码
import torch

# 已知数据:本例不需要计算它们的梯度
x = torch.tensor(2.0)
y = torch.tensor(5.0)

# 模型参数:需要计算损失对它们的梯度
w = torch.tensor(3.0, requires_grad=True)
b = torch.tensor(1.0, requires_grad=True)
optimizer = torch.optim.SGD([w, b], lr=0.05)

# 正向计算
a = w * x
pred = a + b
error = pred - y
loss = error ** 2

# 仅为了观察:保留中间量的梯度
for value in (a, pred, error):
    value.retain_grad()

# 反向传播:计算梯度,不更新参数
loss.backward()

for name, value in [
    ("error", error), ("pred", pred), ("a", a),
    ("w", w), ("b", b)
]:
    print(f"{name}: 数值={value.item()}, 梯度={value.grad.item()}")

# 梯度计算完成后,由优化器更新参数
optimizer.step()
print(f"更新后的 w = {w.item():.1f}")
print(f"更新后的 b = {b.item():.1f}")
# 已有的 pred、loss 不会自动刷新;新的结果需要重新前向计算。
预期输出
error: 数值=2.0, 梯度=4.0
pred: 数值=7.0, 梯度=4.0
a: 数值=6.0, 梯度=4.0
w: 数值=3.0, 梯度=8.0
b: 数值=1.0, 梯度=4.0
更新后的 w = 2.6
更新后的 b = 0.8
  1. requires_grad=True

    允许自动求导记录相关运算。它不表示梯度已经算好了;新建 w、b 后,它们的 .grad 仍然是 None。

  2. retain_grad()

    中间量 a、pred、error 也参与反向传播,但默认不保存自己的 .grad。这里额外保留,是为了观察过程。

  3. loss.backward()

    从损失出发,计算并累积梯度。计算完成后,可以通过 w.grad 和 b.grad 查看结果。

  4. item()

    把只有一个元素的张量转换成普通 Python 数值,方便打印。参与反向传播的 loss 本身仍应保持为张量。

  5. optimizer.step()

    这里使用普通 SGD,按“参数减去学习率乘梯度”更新 w、b。优化器不会自动清空梯度,也不会刷新之前算出的预测和损失。

普通训练通常不需要给中间量调用 retain_grad()。

重新演示时,请重新运行整个单元格,创建新的参数和计算图。

04

把几个容易混淆的地方分开

输入 x 不求梯度,能计算 w 的梯度吗?

可以。计算 ∂L/∂w 时会用到 x 的数值,但不要求计算 x 自己的梯度。本例中 x.grad 是 None,表示没有为它存储梯度,并不是数学导数为零。普通函数拟合通常只需要模型参数的梯度。

为什么每轮训练要 zero_grad()?

PyTorch 默认把新算出的梯度累积到已有的 .grad 中。通常每轮更新只使用本轮的梯度,所以先清除上一轮存储的梯度。这与一张计算图中多条路径贡献相加,是两个不同层面的“累加”。

backward() 和 step() 有什么不同?

backward() 负责计算梯度;step() 负责更新参数。以普通 SGD 为例,更新规则是 w ← w − 学习率 × ∂L/∂w。参数更新后,要重新进行前向计算,才能得到新的预测和损失。

计算图是先画好再执行的吗?

在本例使用的 PyTorch 默认即时执行模式中,相关运算发生时就会建立计算图。多层网络只是组合了更多运算,求导的链式法则不变。

拓展:查看自己网络的实际计算图

torchviz 可以绘制 PyTorch 的反向运算图。下面假设已经定义好课堂示例中的 model、grid、potential 和 loss_fn。

from torchviz import make_dot
from IPython.display import display

pred_demo = model(grid[:2])
loss_demo = loss_fn(pred_demo, potential(grid[:2]))

display(make_dot(
    loss_demo,
    params=dict(model.named_parameters())
))

需要 Python 包 torchviz,以及运行环境中的 Graphviz 程序。它展示的是静态运算关系,节点可能叫 AddmmBackward、EluBackward 等;不会自动播放梯度传播,也不会自动标出上面演示中的全部梯度数值。