跳到主要内容

自动求导

自动求导(Autograd)是 PyTorch 的核心特性之一,它能够自动计算张量的梯度,是深度学习反向传播的基础。

什么是自动求导?

在深度学习中,我们需要计算损失函数对模型参数的梯度,然后使用梯度下降更新参数。手动计算梯度非常繁琐且容易出错,PyTorch 的自动求导机制可以自动完成这一过程。

前向传播:输入 → 模型 → 输出 → 损失
反向传播:损失 → ∂L/∂输出 → ∂L/∂参数 → 更新参数

自动求导自动计算反向传播中的所有梯度

计算图

PyTorch 使用动态计算图(Dynamic Computational Graph)来追踪张量的运算历史。

计算图结构

计算图示例:y = (x + 2) * (x + 3)

x (叶子节点)
/ \
+ +
/ \
2 3
\ /
* *
\ /
y (输出节点)

前向传播:从叶子节点到输出节点
反向传播:从输出节点到叶子节点,计算梯度

requires_grad 属性

只有设置了 requires_grad=True 的张量才会追踪梯度:

import torch

# 创建需要梯度的张量
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
print(x.requires_grad) # True

# 运算后的张量也会追踪梯度
y = x + 2
print(y.requires_grad) # True

z = y * y * 3
print(z.requires_grad) # True

# 不需要梯度的张量
a = torch.tensor([1.0, 2.0])
print(a.requires_grad) # False

b = a + 2
print(b.requires_grad) # False

查看计算图

x = torch.tensor([2.0], requires_grad=True)
y = x ** 2
z = y + 1

# 查看梯度函数
print(z.grad_fn) # <AddBackward0 object>
print(y.grad_fn) # <PowBackward0 object>
print(x.grad_fn) # None(叶子节点)

# 查看是否为叶子节点
print(x.is_leaf) # True
print(y.is_leaf) # False
print(z.is_leaf) # False

反向传播

backward() 方法

x = torch.tensor([2.0], requires_grad=True)
y = x ** 2
z = y.mean()

# 反向传播计算梯度
z.backward()

# 查看 x 的梯度
print(x.grad) # tensor([4.])
# 解释:z = x^2, dz/dx = 2x = 4

标量输出

backward() 默认只能对标量输出求导:

x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
y = x * 2

# 错误:y 不是标量
# y.backward() # RuntimeError

# 正确:先求和得到标量
z = y.sum()
z.backward()
print(x.grad) # tensor([2., 2., 2.])

向量输出

对于向量输出,需要提供梯度权重:

x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
y = x * 2

# 提供梯度权重
gradient = torch.tensor([1.0, 1.0, 1.0])
y.backward(gradient)
print(x.grad) # tensor([2., 2., 2.])

# 等价于
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
y = x * 2
y.sum().backward()
print(x.grad) # tensor([2., 2., 2.])

梯度累积

PyTorch 默认会累积梯度:

x = torch.tensor([1.0], requires_grad=True)

# 第一次反向传播
y = x ** 2
y.backward()
print(x.grad) # tensor([2.])

# 第二次反向传播(梯度累积)
y = x ** 2
y.backward()
print(x.grad) # tensor([4.])

# 清零梯度
x.grad.zero_()
y = x ** 2
y.backward()
print(x.grad) # tensor([2.])

梯度控制

禁用梯度追踪

在某些情况下(如模型评估),不需要计算梯度:

x = torch.tensor([1.0], requires_grad=True)

# 方法1:torch.no_grad()
with torch.no_grad():
y = x * 2
print(y.requires_grad) # False

# 方法2:@torch.no_grad() 装饰器
@torch.no_grad()
def inference(x):
return x * 2

# 方法3:detach()
y = x * 2
z = y.detach() # 创建不需要梯度的新张量
print(z.requires_grad) # False

# 方法4:requires_grad_(False)
x.requires_grad_(False)
print(x.requires_grad) # False

梯度清零

训练时需要在每次迭代前清零梯度:

import torch
import torch.nn as nn

model = nn.Linear(10, 1)
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

# 方法1:手动清零
optimizer.zero_grad()
loss = model(torch.randn(1, 10)).sum()
loss.backward()
optimizer.step()

# 方法2:设置梯度为 None(更高效)
for param in model.parameters():
param.grad = None

梯度裁剪

防止梯度爆炸:

x = torch.tensor([100.0], requires_grad=True)
y = x ** 2
y.backward()
print(x.grad) # tensor([200.])

# 梯度裁剪
x.grad.zero_()
y = x ** 2
y.backward()
torch.nn.utils.clip_grad_norm_([x], max_norm=1.0)
print(x.grad) # tensor([1.]) 被裁剪到 1

# clip_grad_value_:裁剪到指定范围
torch.nn.utils.clip_grad_value_([x], clip_value=0.5)

高级用法

保留计算图

默认情况下,backward() 会释放计算图:

x = torch.tensor([1.0], requires_grad=True)
y = x ** 2

# 保留计算图
y.backward(retain_graph=True)
print(x.grad) # tensor([2.])

# 可以再次反向传播
y.backward()
print(x.grad) # tensor([4.])

非标量梯度

x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
y = x ** 2

# 自定义梯度权重
weights = torch.tensor([1.0, 2.0, 3.0])
y.backward(weights)
print(x.grad) # tensor([2., 8., 18.])
# 解释:grad = weights * 2x

计算高阶导数

x = torch.tensor([2.0], requires_grad=True)
y = x ** 3

# 一阶导数
grad1 = torch.autograd.grad(y, x, create_graph=True)[0]
print(grad1) # tensor([12.], grad_fn=<MulBackward0>)

# 二阶导数
grad2 = torch.autograd.grad(grad1, x)[0]
print(grad2) # tensor([12.])

autograd.grad 函数

x = torch.tensor([1.0, 2.0], requires_grad=True)
y = x ** 2

# 计算梯度但不修改 .grad 属性
grad = torch.autograd.grad(y.sum(), x)
print(grad) # (tensor([2., 4.]),)
print(x.grad) # None

# 计算多个张量的梯度
a = torch.tensor([1.0], requires_grad=True)
b = torch.tensor([2.0], requires_grad=True)
c = a * b + a ** 2

grads = torch.autograd.grad(c, [a, b])
print(grads) # (tensor([4.]), tensor([1.]))

自定义梯度函数

继承 autograd.Function

class MyReLU(torch.autograd.Function):

@staticmethod
def forward(ctx, x):
# 保存用于反向传播的信息
ctx.save_for_backward(x)
return x.clamp(min=0)

@staticmethod
def backward(ctx, grad_output):
x, = ctx.saved_tensors
# 计算梯度
grad_input = grad_output.clone()
grad_input[x < 0] = 0
return grad_input

# 使用自定义函数
x = torch.tensor([-1.0, 0.0, 1.0], requires_grad=True)
my_relu = MyReLU.apply
y = my_relu(x)
print(y) # tensor([0., 0., 1.], grad_fn=<MyReLUBackward>)

y.sum().backward()
print(x.grad) # tensor([0., 0., 1.])

自定义梯度示例:平方根

class SqrtFunction(torch.autograd.Function):

@staticmethod
def forward(ctx, x):
ctx.save_for_backward(x)
return x.sqrt()

@staticmethod
def backward(ctx, grad_output):
x, = ctx.saved_tensors
grad_input = grad_output / (2 * x.sqrt())
return grad_input

x = torch.tensor([4.0], requires_grad=True)
y = SqrtFunction.apply(x)
print(y) # tensor([2.], grad_fn=<SqrtFunctionBackward>)

y.backward()
print(x.grad) # tensor([0.2500])
# 解释:d(sqrt(x))/dx = 1/(2*sqrt(x)) = 1/4 = 0.25

实战示例:线性回归

import torch
import matplotlib.pyplot as plt

# 生成数据
torch.manual_seed(42)
x = torch.randn(100, 1)
y = 3 * x + 2 + torch.randn(100, 1) * 0.5

# 初始化参数
w = torch.randn(1, requires_grad=True)
b = torch.randn(1, requires_grad=True)

# 超参数
learning_rate = 0.1
epochs = 100

# 训练
losses = []
for epoch in range(epochs):
# 前向传播
y_pred = w * x + b
loss = ((y_pred - y) ** 2).mean()

# 反向传播
loss.backward()

# 更新参数
with torch.no_grad():
w -= learning_rate * w.grad
b -= learning_rate * b.grad

# 清零梯度
w.grad.zero_()
b.grad.zero_()

losses.append(loss.item())

print(f"学习到的参数: w={w.item():.3f}, b={b.item():.3f}")
print(f"真实参数: w=3.000, b=2.000")

# 绘制损失曲线
plt.plot(losses)
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training Loss')
plt.show()

常见问题

1. 梯度为 None

x = torch.tensor([1.0], requires_grad=True)
y = x * 2

# 错误:没有调用 backward()
print(x.grad) # None

# 正确:先调用 backward()
y.backward()
print(x.grad) # tensor([2.])

2. 计算图已释放

x = torch.tensor([1.0], requires_grad=True)
y = x ** 2

# 第一次反向传播
y.backward()

# 错误:计算图已释放
# y.backward() # RuntimeError

# 正确:重新构建计算图
y = x ** 2
y.backward()

3. 原地操作错误

x = torch.tensor([1.0], requires_grad=True)
y = x ** 2

# 错误:原地操作破坏计算图
# y += 1 # RuntimeError

# 正确:非原地操作
y = y + 1

4. 叶子节点梯度

x = torch.tensor([1.0], requires_grad=True)
y = x ** 2
z = y ** 2

z.backward()

# 只有叶子节点的梯度会被保留
print(x.grad) # tensor([4.])
print(y.grad) # None(非叶子节点)

小结

本章我们学习了:

  1. 自动求导原理:动态计算图追踪运算历史
  2. requires_grad:控制梯度追踪
  3. backward():执行反向传播计算梯度
  4. 梯度控制:禁用追踪、清零、裁剪
  5. 高级用法:高阶导数、自定义梯度函数
  6. 实战应用:线性回归训练

参考资源