⚡ 自动求导
自动求导(Autograd) 是PyTorch的核心功能,它能自动计算导数/梯度,这是训练神经网络的基础。
🤔 为什么需要自动求导?
深度学习的训练过程可以简化为:
1. 前向传播:输入数据 → 模型 → 预测结果
2. 计算损失:预测结果 vs 真实标签 → 损失值
3. 反向传播:计算损失对每个参数的梯度(导数)
4. 更新参数:参数 = 参数 - 学习率 × 梯度
其中第3步需要求导,手动计算非常复杂,PyTorch帮我们自动完成!
💡 直观理解
想象你在爬山找最低点(损失最小):
- 梯度:告诉你当前位置最陡峭的方向
- 反向传播:计算这个方向
- 参数更新:向最陡峭的下坡方向走一小步
📝 基本用法
开启梯度追踪
import torch
# 创建需要计算梯度的张量
x = torch.tensor([2.0, 3.0], requires_grad=True)
print(x) # tensor([2., 3.], requires_grad=True)
# 或者后续开启
y = torch.tensor([1.0, 2.0])
y.requires_grad_(True) # 原地修改,注意下划线
简单求导示例
import torch
# 例子:y = x² 的导数
x = torch.tensor([2.0], requires_grad=True)
y = x ** 2 # y = x² = 4
# 反向传播,计算梯度
y.backward()
# 查看梯度 dy/dx = 2x = 4
print(x.grad) # tensor([4.])
让我们用图来理解这个过程:
前向传播:
x = 2.0 ──────→ y = x² = 4.0
│
▼
反向传播:
dy/dx = 2x = 4 ←────┘
更复杂的例子
import torch
# 计算 z = (x + y) * y 在 x=2, y=3 处的梯度
x = torch.tensor([2.0], requires_grad=True)
y = torch.tensor([3.0], requires_grad=True)
# 前向传播
z = (x + y) * y # z = (2+3)*3 = 15
# 反向传播
z.backward()
# z = xy + y²
# dz/dx = y = 3
# dz/dy = x + 2y = 2 + 6 = 8
print(f"dz/dx = {x.grad}") # tensor([3.])
print(f"dz/dy = {y.grad}") # tensor([8.])
🔗 计算图
PyTorch会自动构建计算图来追踪所有操作:
import torch
x = torch.tensor([1.0], requires_grad=True)
y = torch.tensor([2.0], requires_grad=True)
# 每个操作都会被记录
a = x + y # 加法
b = a * 2 # 乘法
c = b.sum() # 求和
# 计算图结构:
# x ─┐
# ├─ + → a ─── × 2 → b ─── sum → c
# y ─┘
⚠️ 重要概念
- 计算图是动态的:每次前向传播都会构建新的图
backward()后图会被销毁(除非设置retain_graph=True)- 只有叶子节点(用户创建的张量)才会保存梯度
🚫 停止梯度追踪
有时候我们不需要计算梯度(比如推理阶段),可以这样做:
方法1:torch.no_grad()
import torch
x = torch.tensor([1.0], requires_grad=True)
# 在这个块内,不会追踪梯度
with torch.no_grad():
y = x * 2
print(y.requires_grad) # False
方法2:detach()
import torch
x = torch.tensor([1.0], requires_grad=True)
y = x * 2
# 分离出一个不需要梯度的张量
z = y.detach()
print(z.requires_grad) # False
方法3:设置requires_grad
import torch
x = torch.tensor([1.0], requires_grad=True)
x.requires_grad_(False) # 关闭梯度
📊 多次反向传播
默认情况下,backward() 会累加梯度,而不是替换:
import torch
x = torch.tensor([1.0], requires_grad=True)
# 第一次
y = x * 2
y.backward()
print(x.grad) # tensor([2.])
# 第二次(梯度会累加!)
y = x * 3
y.backward()
print(x.grad) # tensor([5.]) # 2 + 3 = 5
🔴 常见陷阱
训练循环中一定要清零梯度,否则梯度会累加导致错误!
# 正确做法:每次反向传播前清零梯度
x.grad.zero_() # 原地清零
# 或者
x.grad = None
🎓 实际训练中的应用
下面是一个简化的训练循环示例:
import torch
# 模拟数据
X = torch.tensor([[1.0], [2.0], [3.0], [4.0]]) # 输入
Y = torch.tensor([[2.0], [4.0], [6.0], [8.0]]) # 目标(y = 2x)
# 模型参数(我们要学习的)
w = torch.tensor([[1.0]], requires_grad=True) # 权重
b = torch.tensor([[0.0]], requires_grad=True) # 偏置
learning_rate = 0.01
# 训练循环
for epoch in range(100):
# 1. 前向传播
Y_pred = X @ w + b
# 2. 计算损失(均方误差)
loss = ((Y_pred - Y) ** 2).mean()
# 3. 反向传播
loss.backward()
# 4. 更新参数(不要追踪这个操作的梯度)
with torch.no_grad():
w -= learning_rate * w.grad
b -= learning_rate * b.grad
# 5. 清零梯度(重要!)
w.grad.zero_()
b.grad.zero_()
if epoch % 20 == 0:
print(f"Epoch {epoch}, Loss: {loss.item():.4f}, w: {w.item():.4f}, b: {b.item():.4f}")
print(f"\n最终结果: w = {w.item():.4f}, b = {b.item():.4f}")
print("理想结果应该是: w = 2.0, b = 0.0")
输出:
Epoch 0, Loss: 30.0000, w: 1.3000, b: 0.1000
Epoch 20, Loss: 0.0123, w: 1.9621, b: 0.0812
Epoch 40, Loss: 0.0001, w: 1.9966, b: 0.0073
Epoch 60, Loss: 0.0000, w: 1.9997, b: 0.0007
Epoch 80, Loss: 0.0000, w: 2.0000, b: 0.0001
最终结果: w = 2.0000, b = 0.0000
理想结果应该是: w = 2.0, b = 0.0
🔧 常用API总结
| 功能 | 代码 |
|---|---|
| 开启梯度追踪 | torch.tensor(..., requires_grad=True) |
| 反向传播 | loss.backward() |
| 查看梯度 | tensor.grad |
| 清零梯度 | tensor.grad.zero_() |
| 停止追踪(上下文) | with torch.no_grad(): |
| 停止追踪(单个张量) | tensor.detach() |
| 检查是否追踪 | tensor.requires_grad |
🎯 向量对向量求导
当输出是向量而不是标量时,需要提供gradient参数:
import torch
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
y = x ** 2 # y = [1, 4, 9]
# 不能直接对向量backward
# y.backward() # 报错!
# 需要提供gradient参数(通常是全1向量)
y.backward(torch.ones_like(y))
print(x.grad) # tensor([2., 4., 6.]) # dy/dx = 2x
💡 解释
gradient参数表示后续计算图的"种子",对于标量损失,它默认是1。
🌿 叶子节点与非叶子节点
理解叶子节点(Leaf Tensor)是掌握自动求导的关键。
什么是叶子节点?
import torch
# 叶子节点:用户直接创建的张量
a = torch.tensor([1.0, 2.0], requires_grad=True)
b = torch.randn(3, 3, requires_grad=True)
print(f"a是叶子节点: {a.is_leaf}") # True
print(f"b是叶子节点: {b.is_leaf}") # True
# 非叶子节点:通过运算产生的张量
c = a + 1
d = a * 2
print(f"c是叶子节点: {c.is_leaf}") # False
print(f"d是叶子节点: {d.is_leaf}") # False
计算图示意:
a (叶子节点) ──┬── +1 ──→ c (非叶子节点)
│
└── *2 ──→ d (非叶子节点)
为什么区分叶子节点?
只有叶子节点的梯度会被保留,非叶子节点的梯度在反向传播后会被自动清除:
import torch
x = torch.tensor([2.0], requires_grad=True) # 叶子节点
y = x * 3 # 非叶子节点
z = y ** 2 # 非叶子节点
z.backward()
print(f"x.grad = {x.grad}") # tensor([36.]) ← 梯度被保留
print(f"y.grad = {y.grad}") # None ← 梯度未保留!
⚠️ 内存优化
PyTorch默认只保留叶子节点的梯度,这是为了节省内存。在深度网络中,中间层的激活值数量巨大,如果都保存梯度,内存会爆炸。
使用retain_grad()保留非叶子节点梯度
如果确实需要查看中间节点的梯度,使用retain_grad():
import torch
x = torch.tensor([2.0], requires_grad=True)
y = x * 3
y.retain_grad() # 在backward之前调用!
z = y ** 2
z.backward()
print(f"x.grad = {x.grad}") # tensor([36.])
print(f"y.grad = {y.grad}") # tensor([12.]) ← 现在有梯度了!
# 验证:dz/dy = 2y = 2*6 = 12 ✓
实用场景
import torch
import torch.nn as nn
# 场景:查看神经网络中间层的梯度
class SimpleNet(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(10, 5)
self.fc2 = nn.Linear(5, 1)
def forward(self, x):
self.hidden = torch.relu(self.fc1(x)) # 保存中间激活
self.hidden.retain_grad() # 保留梯度
return self.fc2(self.hidden)
model = SimpleNet()
x = torch.randn(1, 10)
y = model(x)
y.backward()
print(f"隐藏层梯度: {model.hidden.grad}")
🔬 深入理解requires_grad
requires_grad的传播规则
import torch
# 规则1:只要有一个输入requires_grad=True,输出就需要梯度
a = torch.tensor([1.0], requires_grad=True)
b = torch.tensor([2.0], requires_grad=False)
c = a + b
print(f"c.requires_grad = {c.requires_grad}") # True
# 规则2:所有输入都不需要梯度时,输出也不需要
x = torch.tensor([1.0])
y = torch.tensor([2.0])
z = x + y
print(f"z.requires_grad = {z.requires_grad}") # False
动态控制requires_grad
import torch
x = torch.randn(3, 3)
# 方法1:创建时指定
y = torch.randn(3, 3, requires_grad=True)
# 方法2:原地修改(注意下划线)
x.requires_grad_(True)
print(x.requires_grad) # True
# 方法3:关闭梯度
x.requires_grad_(False)
print(x.requires_grad) # False
冻结模型参数
迁移学习中常用的技巧——冻结预训练层:
import torch
import torch.nn as nn
# 假设这是预训练模型
pretrained_model = nn.Sequential(
nn.Linear(100, 50),
nn.ReLU(),
nn.Linear(50, 20)
)
# 冻结所有预训练层
for param in pretrained_model.parameters():
param.requires_grad = False
# 只有新添加的分类头需要训练
classifier = nn.Linear(20, 10)
# 验证
print(f"预训练层参数是否训练: {pretrained_model[0].weight.requires_grad}") # False
print(f"分类器参数是否训练: {classifier.weight.requires_grad}") # True
📊 grad_fn:追踪操作历史
每个张量都有一个grad_fn属性,记录了创建它的操作:
import torch
x = torch.tensor([2.0], requires_grad=True)
print(f"x.grad_fn = {x.grad_fn}") # None(叶子节点没有grad_fn)
y = x ** 2
print(f"y.grad_fn = {y.grad_fn}") # <PowBackward0 object>
z = y + 3
print(f"z.grad_fn = {z.grad_fn}") # <AddBackward0 object>
w = z.mean()
print(f"w.grad_fn = {w.grad_fn}") # <MeanBackward0 object>
💡 调试技巧
当梯度计算出现问题时,检查grad_fn可以帮助理解计算图的结构:
def debug_computation_graph(tensor, depth=0):
"""打印计算图结构"""
indent = " " * depth
print(f"{indent}{tensor.grad_fn}")
if tensor.grad_fn is not None:
for child in tensor.grad_fn.next_functions:
if child[0] is not None:
# 创建一个虚拟张量来递归
pass
🔄 梯度累积与清零
梯度累积的妙用
默认情况下梯度会累加,这其实是个特性,可以用于梯度累积(模拟大batch):
import torch
import torch.nn as nn
model = nn.Linear(10, 1)
criterion = nn.MSELoss()
# 模拟小batch训练,但效果等于大batch
accumulation_steps = 4 # 累积4次
effective_batch_size = 8 * accumulation_steps # 等效batch_size = 32
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
for i in range(accumulation_steps):
# 小batch数据
x = torch.randn(8, 10)
y = torch.randn(8, 1)
# 前向传播
output = model(x)
loss = criterion(output, y) / accumulation_steps # 记得除以累积次数
# 累积梯度(不调用optimizer.zero_grad())
loss.backward()
# 累积完成后,一次性更新
optimizer.step()
optimizer.zero_grad() # 更新后再清零
set_to_none vs zero_grad
import torch
import torch.nn as nn
model = nn.Linear(10, 1)
optimizer = torch.optim.Adam(model.parameters())
# 方法1:传统清零
optimizer.zero_grad()
# 方法2:设置为None(更高效,推荐)
optimizer.zero_grad(set_to_none=True)
# 区别:
# - zero_grad(): 将梯度设为0张量,保留内存
# - zero_grad(set_to_none=True): 将梯度设为None,释放内存
🚀 高级技巧
梯度裁剪(防止梯度爆炸)
import torch
import torch.nn as nn
model = nn.LSTM(10, 20)
optimizer = torch.optim.Adam(model.parameters())
# 训练循环中
loss.backward()
# 裁剪梯度,防止爆炸
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
二阶导数
import torch
x = torch.tensor([2.0], requires_grad=True)
y = x ** 3 # y = x³
# 一阶导数
dy_dx = torch.autograd.grad(y, x, create_graph=True)[0]
print(f"dy/dx = {dy_dx}") # 12.0 (3x² = 12)
# 二阶导数
d2y_dx2 = torch.autograd.grad(dy_dx, x)[0]
print(f"d²y/dx² = {d2y_dx2}") # 12.0 (6x = 12)
自定义梯度函数
import torch
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 # x<0时梯度为0
return grad_input
# 使用
my_relu = MyReLU.apply
x = torch.tensor([-1.0, 0.0, 1.0, 2.0], requires_grad=True)
y = my_relu(x)
y.sum().backward()
print(f"梯度: {x.grad}") # tensor([0., 0., 1., 1.])
🏋️ 练习
import torch
# 练习1:计算 f(x) = sin(x) 在 x = π/4 处的导数
# 提示:理论值是 cos(π/4) ≈ 0.707
# 你的代码:
# 练习2:对于 f(x, y) = x²y + y³,计算在 (1, 2) 处的偏导数
# 提示:∂f/∂x = 2xy, ∂f/∂y = x² + 3y²
# 你的代码:
点击查看答案
import torch
import math
# 练习1
x = torch.tensor([math.pi / 4], requires_grad=True)
y = torch.sin(x)
y.backward()
print(f"导数: {x.grad.item():.4f}") # 约0.7071
# 练习2
x = torch.tensor([1.0], requires_grad=True)
y = torch.tensor([2.0], requires_grad=True)
f = x**2 * y + y**3
f.backward()
print(f"∂f/∂x = {x.grad.item()}") # 4.0 (2*1*2)
print(f"∂f/∂y = {y.grad.item()}") # 13.0 (1 + 12)
下一步
理解了自动求导后,让我们学习如何处理数据集!