梯度,如何走回参数?
从一个可以手算的模型出发,看看数值怎样算出来,
梯度又怎样沿着计算图传回去。
输入 x = 2,目标 y = 5;初始参数 w = 3,b = 1。
顺着算,反着求导
计算图记录一次计算由哪些运算组成,以及各个量之间的依赖关系。点击“下一步”查看梯度,再点击“更新参数”,观察参数沿梯度的反方向变化。
每经过一步,乘上局部导数
沿一条路径:相乘
以参数 w 为例:先看损失对误差的变化,再看误差对预测的变化,最后一路回到 w。
= 4 × 1 × 1 × 2 = 8
平方 → 减法 → 加法 → 乘法
同理,b 经过的路径更短:
多条路径汇合:相加
如果同一个量通过多条路径影响损失,就把各条路径的梯度贡献相加。这也是更复杂网络中反向传播的基本规则。
局部导数和梯度是两回事
乘法节点 a = wx 对 w 的局部导数是 x = 2;但最终损失对 w 的梯度是 8,因为还要乘上后续运算传回来的梯度 4。
PyTorch 替我们按链式法则完成这些计算,不需要手动为整个网络推导公式。
optimizer.zero_grad()清除上一次存储的参数梯度pred = model(x)正向计算预测,再计算损失loss.backward()反向计算参数梯度optimizer.step()根据梯度更新参数在 PyTorch 中亲眼验证
把下面的代码放进一个 Notebook 单元格,完整运行一次。它与上面的计算图一一对应,只依赖 PyTorch。
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
- requires_grad=True
允许自动求导记录相关运算。它不表示梯度已经算好了;新建 w、b 后,它们的
.grad仍然是None。 - retain_grad()
中间量 a、pred、error 也参与反向传播,但默认不保存自己的
.grad。这里额外保留,是为了观察过程。 - loss.backward()
从损失出发,计算并累积梯度。计算完成后,可以通过
w.grad和b.grad查看结果。 - item()
把只有一个元素的张量转换成普通 Python 数值,方便打印。参与反向传播的 loss 本身仍应保持为张量。
- optimizer.step()
这里使用普通 SGD,按“参数减去学习率乘梯度”更新 w、b。优化器不会自动清空梯度,也不会刷新之前算出的预测和损失。
普通训练通常不需要给中间量调用 retain_grad()。
重新演示时,请重新运行整个单元格,创建新的参数和计算图。
把几个容易混淆的地方分开
输入 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 等;不会自动播放梯度传播,也不会自动标出上面演示中的全部梯度数值。