第 6 章 自动求导
学习目标
- 理解「计算图」与
requires_grad的含义 - 掌握
backward()计算梯度,读取.grad - 理解梯度累积与
grad.zero_()清零 - 掌握
detach()与torch.no_grad()的用途 - 知道为什么叶子张量不能被原地修改
6.1 为什么需要自动求导
训练神经网络的核心问题是:给定一个损失 loss,求它对每个参数(权重)的导数(梯度),然后按梯度方向更新参数。手算导数对两层网络还能忍,对几十层网络和百万参数完全不可行。
PyTorch 的 autograd(自动求导) 解决这个问题:只要张量声明「需要梯度」,PyTorch 就会在运算时记录一张计算图(computational graph),并在调用 backward() 时沿图反向传播,自动算出每个参与张量的梯度。
6.2 requires_grad 与计算图
创建张量时指定 requires_grad=True,告诉 PyTorch「请记录针对这个张量的运算」:
import torch
x = torch.tensor(3.0, requires_grad=True)
print(x.requires_grad)
y = x ** 2
print(type(y.grad_fn).__name__)输出:
True
PowBackward0x.requires_grad 是 True。y = x ** 2 产生的新张量带一个 grad_fn(梯度函数),PowBackward0 表示「我是由幂运算产生的,我知道怎么把梯度传回去」。由运算产生、带着 grad_fn 的张量叫非叶子张量;直接创建、没有 grad_fn 的叫叶子张量(leaf)。
6.3 backward:计算梯度
对 y 调用 backward(),梯度会反向传播到叶子张量,存进它的 .grad:
import torch
x = torch.tensor(3.0, requires_grad=True)
y = x ** 2
y.backward()
print(x.grad)输出:
tensor(6.)y = x² 在 x = 3 处的导数正是 2x = 6。多步运算同样成立,y = x² + 3x 的导数是 2x + 3,在 x = 2 处为 7:
import torch
x = torch.tensor(2.0, requires_grad=True)
y = x ** 2 + 3 * x
y.backward()
print(x.grad)
print(x.grad.item())输出:
tensor(7.)
7.0.grad 是张量,.item() 取出纯数字。链式法则、多层网络,autograd 都会自动处理,你只需要保证从损失到参数的路径上所有张量都能求梯度。
6.4 线性组合的梯度
第 4 章的矩阵乘法在这里有了用武之地。求 loss = (w * x).sum() 对 w 的梯度:
import torch
w = torch.tensor([1., 2.], requires_grad=True) # 待更新的「权重」
x = torch.tensor([3., 4.]) # 输入数据,不需要梯度
loss = (w * x).sum()
loss.backward()
print(w.grad)输出:
tensor([3., 4.])对 w 的梯度恰是 x——这正是「损失对权重的梯度由输入决定」的直观体现,也是第 8 章梯度下降更新公式 w = w - lr * w.grad 的基础。
6.5 梯度累积与清零
每次 backward(),梯度累加到 .grad,不会自动清零:
import torch
x = torch.tensor(2.0, requires_grad=True)
y = x * x
y.backward()
print("第一次 backward 后:", x.grad) # 2x = 4
y = x * x
y.backward()
print("第二次 backward 后:", x.grad) # 4 + 4 = 8,累加了输出:
第一次 backward 后: tensor(4.)
第二次 backward 后: tensor(8.)所以每轮训练在 backward() 之前要先清零:x.grad.zero_()(或 optimizer.zero_grad(),第 8 章):
x.grad.zero_()
print(x.grad)输出:
tensor(0.)6.6 detach 与 torch.no_grad
detach()(分离)返回一个新张量,它不要求梯度、也没有 grad_fn,与原来的张量共享数据但不参与计算图:
import torch
x = torch.tensor(2.0, requires_grad=True)
d = x.detach()
print(d.requires_grad)
print(d.grad_fn)输出:
False
Nonetorch.no_grad() 是一个上下文管理器,里面的所有运算都不记录计算图:
import torch
x = torch.tensor(2.0, requires_grad=True)
with torch.no_grad():
z = x * 2
print(z.requires_grad)
print(z.grad_fn)输出:
False
Noneno_grad 在评估模型、测试时必备:推理不需要梯度,不开 no_grad 会白存整张计算图,又慢又占内存(第 11 章)。
6.7 叶子张量的原地修改禁令
第 4 章提过原地运算对求梯度张量有风险,现在看具体报错:
>>> import torch
>>> a = torch.tensor([1., 2., 3.], requires_grad=True)
>>> a.add_(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
RuntimeError: a leaf Variable that requires grad is being used in an in-place operation.原因:autograd 记录的「求值轨迹」依赖叶子张量的原值。原地修改相当于篡改历史,反向传播时算出的梯度就不对了。PyTorch 选择直接报错,而不是悄悄给你错答案。需要修改参数时,用 data 或「复制后替换」;正常训练中参数更新由优化器完成(第 8 章),你几乎不需要手动原地改参数。
动手实践
- 创建
x = torch.tensor(4.0, requires_grad=True),计算y = x ** 3,求导并验证dy/dx = 3x² = 48。 - 对
y = x² + 2x + 1求x = 3处的梯度,手算2x + 2 = 8验证结果。 - 连续两次对同一张量
backward(),观察.grad累加;清零后再 backward,确认只得到一次的结果。 - 用
torch.no_grad()包住一次运算,打印结果的requires_grad和grad_fn;再用detach()做同样的事,对比两者。 - 故意对
requires_grad=True的叶子张量执行add_(1),记录报错信息并用自己的话解释为什么。
常见错误
| 错误写法 | 现象 | 原因 |
|---|---|---|
忘记 requires_grad=True | backward() 报 RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn | 参数没有声明需要梯度;创建时加 requires_grad=True |
backward() 后不清零再 backward | 梯度累加,数值翻倍 | PyTorch 梯度默认累积,每轮用 zero_() 清零 |
对非标量直接 backward() | RuntimeError: grad can be implicitly created only for scalar outputs | backward() 只对标量直接调用;损失函数返回的都是标量,若必须对向量求导需传 gradient 参数 |
评估时不开 no_grad() | 内存越用越多、速度慢 | 推理不需要梯度,用 with torch.no_grad(): 包住 |
对叶子张量 add_(1) | RuntimeError: a leaf Variable that requires grad is being used in an in-place operation. | 原地修改会破坏计算图历史 |
打印 .grad 后直接用 | 拿到的是张量不是数 | 需要数字时 .item() |
章末练习
基础
- 写代码计算
y = x²在x = 5处的梯度,输出x.grad.item()。 - 写代码验证:对同一张量连续两次
backward(),.grad翻倍;.grad.zero_()后再 backward 恢复单次结果。 - 用
torch.no_grad()计算z = x * 3,打印z.requires_grad和z.grad_fn。
提高
- 对
w = torch.tensor([1., 2., 3.], requires_grad=True)和输入x = torch.tensor([2., 3., 4.]),计算loss = (w * x).sum(),求w.grad,并解释为什么每个梯度恰好等于对应的 x 分量。 - 解释
detach()与torch.no_grad()的相同点与不同点,各举一个适用场景。
挑战
- 不用 PyTorch,手算
y = (x² + 1) * (2x)对 x 的导数;用 autograd 在x = 1.0处验证(提示:把表达式拆成两个中间变量再组合)。 - 解释「梯度累积」在什么场景是有意为之的(提示:内存不够时把一个大 batch 拆成几个小 batch 分别 backward),并说明为什么要手动清零。
章末自测
每题选择一个最佳答案。本书不附答案:完成后交由老师或 AI 老师批改讲解。
- 让张量参与自动求导,创建时的写法是?
- A.
torch.tensor(3.0) - B.
torch.tensor(3.0, requires_grad=True) - C.
torch.tensor(3.0, grad=True) - D.
torch.tensor(3.0).autograd()
- A.
x = torch.tensor(3.0, requires_grad=True); y = x ** 2; y.backward(); x.grad的结果是?- A.
tensor(9.) - B.
tensor(6.) - C.
tensor(3.) - D.
tensor(2.)
- A.
- 每次
backward()后,.grad默认会?- A. 自动清零
- B. 累加
- C. 变成 None
- D. 变成随机数
- 清零梯度的正确写法是?
- A.
x.grad.delete() - B.
x.grad.zero_() - C.
x.grad.clear() - D.
x.grad = None
- A.
- 评估模型时,应该?
- A. 不开任何保护,直接推理
- B. 用
with torch.no_grad():包住推理 - C. 用
with torch.grad():包住推理 - D. 每次推理都 backward
x.detach()返回的张量?- A. 保留 requires_grad 和 grad_fn
- B. requires_grad 为 False、grad_fn 为 None
- C. 复制一份数据
- D. 一定在 GPU 上
- 对
requires_grad=True的叶子张量调用add_(),会?- A. 正常修改
- B. 报
RuntimeError,提示叶子张量不能原地修改 - C. 静默丢弃计算图
- D. 报
ValueError
torch.no_grad()的作用是?- A. 让运算更快但无法求梯度
- B. 阻止所有运算
- C. 清除已有梯度
- D. 固定随机种子
w = torch.tensor([1., 2.], requires_grad=True); loss = (w * torch.tensor([3., 4.])).sum(); loss.backward(); w.grad的结果是?- A.
tensor([1., 2.]) - B.
tensor([3., 4.]) - C.
tensor([6., 8.]) - D.
tensor(11.)
- A.
- 关于计算图,正确的说法是?
- A. 只有手动搭建才有
- B.
grad_fn记录了张量由什么运算产生 - C. 计算图在
no_grad()中也完整记录 - D. 与训练无关
