自动求导
自动求导(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(非叶子节点)
小结
本章我们学习了:
- 自动求导原理:动态计算图追踪运算历史
- requires_grad:控制梯度追踪
- backward():执行反向传播计算梯度
- 梯度控制:禁用追踪、清零、裁剪
- 高级用法:高阶导数、自定义梯度函数
- 实战应用:线性回归训练