Skip to content

自动求导 Autograd:PyTorch 的魔法所在

深度学习的"学习",本质是:算出损失对每个参数的梯度,然后沿梯度反方向微调参数。手推梯度公式是折磨人的体力活,PyTorch 的自动求导引擎(Autograd)把它变成了一行 .backward()。本篇讲清楚它是怎么做到的。

从"要求梯度"开始:requires_grad

不是所有张量都需要求梯度。只有显式声明 requires_grad=True 的张量,Autograd 才会记录它参与的运算:

python
import torch

x = torch.ones(2, 2, requires_grad=True)
print(x.requires_grad)   # True
print(x.grad)            # None —— 还没反向传播过,没有梯度

模型的可学习参数(后面 nn.Module 里的 nn.Parameter)默认就是 requires_grad=True,普通输入数据则不需要。

计算图:边算边记"小抄"

当你做运算时,Autograd 在后台默默构建一张有向无环图(DAG)——每个运算节点都记得"我是由谁、经过什么运算得来的":

python
a = torch.tensor([2.0], requires_grad=True)
b = torch.tensor([3.0], requires_grad=True)
c = a * b        # 节点记录了:c = mul(a, b)
L = c ** 2       # 节点记录了:L = pow(c, 2)

对应的计算图(正向):

a ──┐
    ├──(×)──> c ──(²)──> L
b ──┘

L.backward() 时,PyTorch 从 L 出发逆着图往回走,用链式法则把梯度一层层传回去:

python
L.backward()
print(a.grad)   # tensor([36.])

手算验证:c = 2×3 = 6L = c² = 36∂L/∂c = 2c = 12∂c/∂a = b = 3;链式法则 ∂L/∂a = 12 × 3 = 36Autograd 的结果和手算完全一致——这就是它可信的原因:它不是近似,是精确的链式求值。

自己动手改几个数字再跑一遍,是理解链式法则最快的方式。

三个必须记住的行为

1. backward() 的调用者必须是标量

python
L.backward()       # ✅ L 是 0 维标量

v = torch.tensor([1.0, 2.0], requires_grad=True)
(v * 2).backward() # ❌ RuntimeError: grad can be implicitly created
                   #    only for scalar outputs(输出是向量,梯度形状不明确)

损失函数最终输出标量(一个数),正是为了满足这个约定。如果确实对非标量求导,需传 gradient= 参数(权重),新手阶段基本用不到。

2. 梯度默认累加,不自动清零

连续两次 backward().grad相加而不是覆盖:

python
a.grad = None      # 传统清零方式
# 或
a.grad.zero_()     # 原地清零

在训练循环里,这个工作由 optimizer.zero_grad() 统一完成。忘记清零是新手训练不收敛的头号原因,实战章节会故意踩一次这个坑。

3. 中间张量的图会被释放

backward() 后计算图即被销毁(省内存),所以对同一张图二次 backward 会报错,除非 L.backward(retain_graph=True)。正常情况下每轮前向都会建新图,不需要你操心。

关掉梯度:no_grad 与 detach

推理/评估阶段不需要求梯度,关掉它能省内存、提速

python
with torch.no_grad():
    pred = model(x_test)   # 这段内的运算不建计算图

另外两种常见写法:

  • y = x.detach():从 x 上"剪断"梯度,得到不带图的新张量。常用于记录指标、保存可视化数据。
  • param.requires_grad_(False):冻结参数。微调预训练模型时冻结底层就是这个API。

坑预警: 想把张量转 NumPy 时直接 x.numpy() 会报 Can't call numpy() on Tensor that requires grad(若张量在 GPU 上还会多报一条 device 错误)。正确姿势:x.detach().cpu().numpy() —— 先剪断梯度,再搬回 CPU,最后转数组。三个动作各对应一条常见报错。

torch.no_grad vs eval()

两者职责完全不同,推理时通常要同时用

调用作用
model.eval()切换层的模式:关闭 Dropout、BatchNorm 改用全局统计量
torch.no_grad()关闭计算图构建:省内存提速,不影响数值结果

小结

  • Autograd = 前向建图 + 反向链式求导requires_grad=True 是入图许可证。
  • backward() 只填 .grad,不清零、不更新参数。
  • 推理时记住口诀:先 eval,再 no_grad

下一篇:用 nn.Module 搭模型——把参数、层、前向逻辑包装成模型。