跳转到内容

6.2.4 Autograd 自动求导

  • 解释 requires_grad=True 改变了什么。
  • 运行 loss.backward() 并检查 .grad
  • 理解 backward() 只计算梯度,不更新参数。
  • zero_grad() 避免梯度累积 bug。
  • 在正确位置使用 torch.no_grad()detach()

PyTorch Autograd 计算图

按这个顺序读图:

参数前向运算lossbackward()parameter.gradoptimizer step

Autograd 会记录产生 loss 的运算。当你调用 backward() 时,PyTorch 会沿着记录下来的图反向走,并应用链式法则。

先从一个数字开始,机制最清楚。

import torch
w = torch.tensor(2.0, requires_grad=True)
loss = (w * 3 - 10) ** 2
print("loss:", loss.item())
loss.backward()
print("w.grad:", w.grad.item())

预期输出:

Terminal window
loss: 16.0
w.grad: -24.0

发生了什么:

  • w 因为 requires_grad=True,所以是可学习值。
  • lossw 计算得到,PyTorch 会记录从 wloss 的路径。
  • loss.backward() 会计算 w 改变时 loss 怎么变。
  • 结果存进 w.grad

计算链是:

ww * 3w * 3 - 10squareloss

backward() 只计算梯度。你仍然需要更新步骤。

import torch
w = torch.tensor(2.0, requires_grad=True)
lr = 0.1
print("single_parameter_training")
for step in range(1, 6):
loss = (w * 3 - 10) ** 2
loss.backward()
with torch.no_grad():
w -= lr * w.grad
print(
f"step={step} "
f"w={w.item():.4f} "
f"loss={loss.item():.4f} "
f"grad={w.grad.item():.4f}"
)
w.grad.zero_()

预期输出:

Terminal window
single_parameter_training
step=1 w=4.4000 loss=16.0000 grad=-24.0000
step=2 w=2.4800 loss=10.2400 grad=19.2000
step=3 w=4.0160 loss=6.5536 grad=-15.3600
step=4 w=2.7872 loss=4.1943 grad=12.2880
step=5 w=3.7702 loss=2.6844 grad=-9.8304

数值来回跳,是因为这个小函数里 lr=0.1 稍微激进。这个现象很有用:梯度告诉你方向和尺度,但学习率决定每次走多远。

为什么需要 torch.no_grad()

  • 更新 w 不是下一次前向计算图的一部分;
  • 不希望 autograd 继续记录更新动作本身;
  • 可以省内存,也能避免图相关错误。

PyTorch 默认会累积梯度,不会自动覆盖 .grad

import torch
x = torch.tensor(3.0, requires_grad=True)
y1 = x ** 2
y1.backward()
print("after first backward:", x.grad.item())
y2 = 2 * x
y2.backward()
print("after second backward:", x.grad.item())
x.grad.zero_()
y3 = 2 * x
y3.backward()
print("after zero and third backward:", x.grad.item())

预期输出:

Terminal window
after first backward: 6.0
after second backward: 8.0
after zero and third backward: 2.0

原因:

  • x=3 时,x ** 2 的梯度是 6
  • 2 * x 的梯度是 2
  • 第二次 backward 后,.grad 变成 6 + 2 = 8
  • 调用 zero_() 后,下一个梯度会从干净状态开始。

Autograd 梯度累积机制图

正常训练代码因此会使用:

optimizer.zero_grad()
loss.backward()
optimizer.step()

现在不用 nn.Linear,也不用 optimizer,手写训练一个小线性模型。这样训练闭环会完全可见。

import torch
# 目标规则:y = 2x + 1
x = torch.tensor([1.0, 2.0, 3.0, 4.0])
y_true = torch.tensor([3.0, 5.0, 7.0, 9.0])
w = torch.tensor(0.0, requires_grad=True)
b = torch.tensor(0.0, requires_grad=True)
lr = 0.05
print("two_parameter_fit")
for epoch in range(201):
y_pred = w * x + b
loss = ((y_pred - y_true) ** 2).mean()
loss.backward()
with torch.no_grad():
w -= lr * w.grad
b -= lr * b.grad
if epoch % 50 == 0:
print(
f"epoch={epoch:3d} "
f"loss={loss.item():.4f} "
f"w={w.item():.4f} "
f"b={b.item():.4f}"
)
w.grad.zero_()
b.grad.zero_()

预期输出:

Terminal window
two_parameter_fit
epoch= 0 loss=41.0000 w=1.7500 b=0.6000
epoch= 50 loss=0.0030 w=2.0452 b=0.8672
epoch=100 loss=0.0007 w=2.0212 b=0.9375
epoch=150 loss=0.0001 w=2.0100 b=0.9706
epoch=200 loss=0.0000 w=2.0047 b=0.9862

Autograd 两参数手写训练结果图

参数会靠近 w=2b=1。神经网络使用的也是同一个循环,只是参数从两个变成了几百万甚至更多。

这三个概念相关,但不能互换。

工具什么时候用效果
requires_grad=True这个张量是参数,或你需要它的梯度未来运算会被追踪
torch.no_grad()推理或手写参数更新临时停止记录计算图
tensor.detach()想拿到不带历史图的张量值返回一个和 autograd 断开的张量

运行检查:

import torch
w = torch.tensor(5.0, requires_grad=True)
tracked = w * 2
detached = tracked.detach()
with torch.no_grad():
untracked = w * 3
print("tracked.requires_grad:", tracked.requires_grad)
print("detached.requires_grad:", detached.requires_grad)
print("untracked.requires_grad:", untracked.requires_grad)

预期输出:

Terminal window
tracked.requires_grad: True
detached.requires_grad: False
untracked.requires_grad: False

实用场景:

  • 验证和预测时用 no_grad()
  • 记录日志、转 NumPy、保存不该保留整张图的值时,用 detach()
  • 如果某个张量还需要通过 loss 传回梯度,不要 detach 它。
现象可能原因修复
.gradNone张量不需要梯度,或它不是叶子张量检查 requires_grad,查看模型参数
训练变得不稳定梯度没有清空backward() 前调用 optimizer.zero_grad()
RuntimeError: Trying to backward through the graph a second timebackward 后重复使用同一张图重新前向计算;只有明确原因时才用 retain_graph=True
内存一直涨把连着计算图的 tensor 存进列表loss.item()tensor.detach()
验证很慢、占内存评估时还在追踪梯度with torch.no_grad(): 包住验证

backward() 前:

print("loss requires_grad:", loss.requires_grad)
print("w requires_grad:", w.requires_grad)

backward() 后:

print("w.grad:", w.grad)
print("b.grad:", b.grad)

普通训练循环的顺序是:

forwardlosszero_gradbackwardstep

有些代码会把 zero_grad 放在 forward 之前,但核心规则一样:下一次更新前清掉旧梯度。

保留一条 autograd trace:

损失是否需要梯度
True
参数_requires_grad
True
反向传播后梯度
不为 None
更新规则
反向传播计算梯度,优化器或手动代码更新数值
安全日志
记录 loss.item() 或 tensor.detach()

这可以避免最常见的误解:backward() 不是更新参数。它只负责填充梯度。

  1. 把实验 4 改成学习 y = 3x - 2wb 应该接近什么?
  2. 删除实验 4 里的 w.grad.zero_()b.grad.zero_(),观察会发生什么。
  3. lr 改成 0.50.005。哪个不稳定,哪个太慢?
  4. 连续 200 个 epoch 把 loss 本身存进列表,再改成存 loss.item()。为什么第二种更安全?
参考实现与讲解
  1. w 应该接近 3b 应该接近 -2。如果数据有噪声或训练提前停止,有小偏差是正常的。
  2. PyTorch 默认会累积梯度。不清零时,每次更新都会混入前面几轮的梯度,相当于步子越来越不受控,训练可能变得不稳定。
  3. lr=0.5 更容易越过最优点或发散;lr=0.005 通常更慢,因为每一步更新太小。
  4. 保存 loss tensor 可能保留计算图引用,造成额外内存占用。loss.item() 只保存 Python 数字,更适合作日志。
  • Autograd 会记录从参数到 loss 的计算图。
  • backward() 计算梯度,但不更新参数。
  • 梯度默认会累积,下一次更新前要清空。
  • 推理和手写更新用 no_grad();只要数值、不保留图时用 detach()