深度学习训练的本质是:选一个损失函数 loss,然后对参数求梯度,用优化算法更新参数。
而“对参数求梯度”这一步,在 PyTorch 里基本靠 autograd 自动完成。

这一节我按李沐的思路讲清楚三件事:

  1. 什么是自动求导、计算图

  2. requires_grad / backward / grad 怎么用

  3. 为什么要 zero_grad、detach、no_grad(最常踩坑)


1. 自动求导在干什么:链式法则 + 计算图

你写的运算(加、乘、matmul、sin…)会被 PyTorch 记录成一张计算图。
当你对某个标量(通常是 loss)调用 backward() 时,PyTorch 会沿图反向传播,用链式法则把梯度一路传回去。

一句话:

loss.backward() = 自动帮你算 (\partial loss / \partial \theta)

2. 最小例子:从标量到梯度

import torch

x = torch.tensor(3.0, requires_grad=True)  # 告诉框架:我要对 x 求导
y = x * x + 2 * x + 1                      # y = x^2 + 2x + 1
y.backward()                               # dy/dx

print(x.grad)  # 2x + 2 = 8

注意点:

  • requires_grad=True 是“追踪梯度”的开关

  • backward() 只能对标量直接调用(严格来说是对标量输出最自然)


3. 向量/矩阵的求导:必须让输出变成标量

如果 y 不是标量,而是向量/矩阵,PyTorch 不知道你想要哪个方向的梯度,需要你提供一个 “外部梯度” gradient,或者先把它变成标量(常用 sum())。

3.1 用 sum() 变成标量(最常用)

x = torch.arange(4.0, requires_grad=True)  # [0,1,2,3]
y = x * x                                  # 向量

y.sum().backward()                         # 对 sum(y) 求导
print(x.grad)                              # d/dx sum(x^2) = 2x -> [0,2,4,6]

3.2 直接传入外部梯度(理解用)

x = torch.arange(4.0, requires_grad=True)
y = x * x

v = torch.tensor([1.0, 0.1, 0.01, 0.001])   # 外部梯度(相当于加权求和)
y.backward(v)

print(x.grad)  # = v * 2x

直觉:y.backward(v) 相当于对 (\sum_i v_i y_i) 求导。


4. 梯度会累加:必须清零(非常重要)

这是训练时最容易踩的大坑:梯度默认是累加的。

x = torch.tensor(2.0, requires_grad=True)

y = x * x
y.backward()
print(x.grad)  # 4

y = x * x
y.backward()
print(x.grad)  # 8  (累加了!)

训练循环里你必须做:

  • param.grad.zero_() 或

  • optimizer.zero_grad()

简化写法(自己写参数时):

x = torch.tensor(2.0, requires_grad=True)

for _ in range(3):
    y = x * x
    y.backward()
    print(x.grad.item())
    x.grad.zero_()

5. detach():切断计算图(让某个张量不再追踪梯度)

有时你希望把某个中间结果当“常量”用,不想让梯度穿过去,就要 detach()。

x = torch.tensor(3.0, requires_grad=True)
y = x * x

u = y.detach()      # u 不在计算图里
z = u * x

z.backward()
print(x.grad)       # dz/dx = u = x^2 = 9(没有 y 对 x 的那条梯度链)

直觉:detach() 就是“把它从图里拽出来”。


6. torch.no_grad():推理/评估时关闭梯度,省显存省时间

训练需要梯度;推理不需要。推理时建议包起来:

x = torch.randn(3, requires_grad=True)

with torch.no_grad():
    y = x * 2
    # y.requires_grad = False

典型场景:

  • 验证集评估

  • 预测

  • 保存中间结果但不想建图


7. 更贴近“模型训练”的例子:对向量参数求导

线性回归会遇到这种形式:(y = Xw),对 (w) 求导。

X = torch.tensor([[1.0, 2.0],
                  [3.0, 4.0]])           # (2,2)
w = torch.tensor([[2.0],
                  [1.0]], requires_grad=True)  # (2,1)

y_hat = X @ w                            # (2,1)
loss = y_hat.sum()                       # 变成标量
loss.backward()

print(w.grad)  # = X^T @ 1  -> 每列求和的形式

这里你能直观看到:梯度就是线性代数结构的体现,后面学反向传播会更顺。


8. 本节最常见坑

  1. 忘了 requires_grad=True
    → grad 永远是 None

  2. 对非标量直接 backward() 报错
    → 用 sum()/mean() 变标量,或传 gradient

  3. 忘了清梯度,导致训练“越训越离谱”
    → 每步更新前 zero_grad

  4. 推理还在建图,显存飙升
    → 用 torch.no_grad()

  5. 想断开梯度但没断开
    → 用 .detach() 或 with torch.no_grad()


小结:你现在应该能做到什么?

  • 写出 loss.backward() 并读懂 param.grad

  • 理解“梯度累加”和“清零”

  • 知道什么时候用 detach() / no_grad()

  • 明白:训练就是“算梯度 → 更新参数”的循环


更多推荐