Skip to content

第 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「请记录针对这个张量的运算」:

python
import torch

x = torch.tensor(3.0, requires_grad=True)
print(x.requires_grad)

y = x ** 2
print(type(y.grad_fn).__name__)

输出:

True
PowBackward0

x.requires_gradTruey = x ** 2 产生的新张量带一个 grad_fn(梯度函数),PowBackward0 表示「我是由幂运算产生的,我知道怎么把梯度传回去」。由运算产生、带着 grad_fn 的张量叫非叶子张量;直接创建、没有 grad_fn 的叫叶子张量(leaf)。

6.3 backward:计算梯度

对 y 调用 backward(),梯度会反向传播到叶子张量,存进它的 .grad:

python
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:

python
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 的梯度:

python
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,不会自动清零:

python
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 章):

python
x.grad.zero_()
print(x.grad)

输出:

tensor(0.)

6.6 detach 与 torch.no_grad

detach()(分离)返回一个新张量,它不要求梯度、也没有 grad_fn,与原来的张量共享数据但不参与计算图:

python
import torch

x = torch.tensor(2.0, requires_grad=True)
d = x.detach()
print(d.requires_grad)
print(d.grad_fn)

输出:

False
None

torch.no_grad() 是一个上下文管理器,里面的所有运算都不记录计算图:

python
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
None

no_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 章),你几乎不需要手动原地改参数。

动手实践

  1. 创建 x = torch.tensor(4.0, requires_grad=True),计算 y = x ** 3,求导并验证 dy/dx = 3x² = 48
  2. y = x² + 2x + 1x = 3 处的梯度,手算 2x + 2 = 8 验证结果。
  3. 连续两次对同一张量 backward(),观察 .grad 累加;清零后再 backward,确认只得到一次的结果。
  4. torch.no_grad() 包住一次运算,打印结果的 requires_gradgrad_fn;再用 detach() 做同样的事,对比两者。
  5. 故意对 requires_grad=True 的叶子张量执行 add_(1),记录报错信息并用自己的话解释为什么。

常见错误

错误写法现象原因
忘记 requires_grad=Truebackward()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 outputsbackward() 只对标量直接调用;损失函数返回的都是标量,若必须对向量求导需传 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()

章末练习

基础

  1. 写代码计算 y = x²x = 5 处的梯度,输出 x.grad.item()
  2. 写代码验证:对同一张量连续两次 backward(),.grad 翻倍;.grad.zero_() 后再 backward 恢复单次结果。
  3. torch.no_grad() 计算 z = x * 3,打印 z.requires_gradz.grad_fn

提高

  1. w = torch.tensor([1., 2., 3.], requires_grad=True) 和输入 x = torch.tensor([2., 3., 4.]),计算 loss = (w * x).sum(),求 w.grad,并解释为什么每个梯度恰好等于对应的 x 分量。
  2. 解释 detach()torch.no_grad() 的相同点与不同点,各举一个适用场景。

挑战

  1. 不用 PyTorch,手算 y = (x² + 1) * (2x) 对 x 的导数;用 autograd 在 x = 1.0 处验证(提示:把表达式拆成两个中间变量再组合)。
  2. 解释「梯度累积」在什么场景是有意为之的(提示:内存不够时把一个大 batch 拆成几个小 batch 分别 backward),并说明为什么要手动清零。

章末自测

每题选择一个最佳答案。本书不附答案:完成后交由老师或 AI 老师批改讲解。

  1. 让张量参与自动求导,创建时的写法是?
    • 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()
  2. 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.)
  3. 每次 backward() 后,.grad 默认会?
    • A. 自动清零
    • B. 累加
    • C. 变成 None
    • D. 变成随机数
  4. 清零梯度的正确写法是?
    • A. x.grad.delete()
    • B. x.grad.zero_()
    • C. x.grad.clear()
    • D. x.grad = None
  5. 评估模型时,应该?
    • A. 不开任何保护,直接推理
    • B. 用 with torch.no_grad(): 包住推理
    • C. 用 with torch.grad(): 包住推理
    • D. 每次推理都 backward
  6. x.detach() 返回的张量?
    • A. 保留 requires_grad 和 grad_fn
    • B. requires_grad 为 False、grad_fn 为 None
    • C. 复制一份数据
    • D. 一定在 GPU 上
  7. requires_grad=True 的叶子张量调用 add_(),会?
    • A. 正常修改
    • B. 报 RuntimeError,提示叶子张量不能原地修改
    • C. 静默丢弃计算图
    • D. 报 ValueError
  8. torch.no_grad() 的作用是?
    • A. 让运算更快但无法求梯度
    • B. 阻止所有运算
    • C. 清除已有梯度
    • D. 固定随机种子
  9. 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.)
  10. 关于计算图,正确的说法是?
    • A. 只有手动搭建才有
    • B. grad_fn 记录了张量由什么运算产生
    • C. 计算图在 no_grad() 中也完整记录
    • D. 与训练无关