自动微分
一、概念阐述
1.1 什么是自动微分?
自动微分(Automatic Differentiation, AD)是一种计算机程序精确计算函数导数的方法。它是深度学习框架的核心技术,使得反向传播算法可以自动实现,无需手动推导梯度公式。
1.2 三种微分方法对比
| 方法 | 原理 | 优点 | 缺点 | 精度 |
|---|---|---|---|---|
| 数值微分 | 使用有限差分近似:f’(x) ≈ [f(x+h)-f(x-h)]/(2h) | 实现简单,适用于任何函数 | 计算量大,存在截断误差和舍入误差 | 近似 |
| 符号微分 | 使用代数规则推导解析表达式 | 得到精确的解析解 | 表达式膨胀,难以处理控制流 | 精确 |
| 自动微分 | 分解为基本运算,通过链式法则组合 | 精确、高效、可处理复杂程序 | 需要构建计算图,内存开销 | 精确 |
1.3 为什么需要自动微分?
深度学习模型通常有数百万到数十亿参数
手动推导梯度公式 → 几乎不可能且容易出错
自动微分 → 自动、精确、高效地计算梯度
二、核心原理
2.1 计算图(Computational Graph)
计算图将计算过程表示为有向无环图(DAG):
- 节点:变量或运算
- 边:数据依赖关系
示例:y = 2 * x^T * x
x ──┬──→ [点积] ──→ u ──→ [×2] ──→ v ──→ y
x ──┘
前向传播:x → u → v → y
反向传播:y → v → u → x (计算梯度)
2.2 两种模式对比
| 特性 | 前向模式 (Forward Mode) | 反向模式 (Reverse Mode) |
|---|---|---|
| 计算方向 | 从输入到输出 | 先正向后反向 |
| 适用场景 | 输入维度 < 输出维度 | 输出维度 < 输入维度 |
| 深度学习 | 不常用 | 核心算法(反向传播) |
| 内存开销 | 低 | 高(需存储中间结果) |
| 计算效率 | O(n) 次前向传播 | 1 次正向 + 1 次反向 |
2.3 链式法则的实现
对于复合函数 y = f(g(x)):
前向模式:
计算 g(x) 和 g'(x)
计算 f(g(x)) 和 f'(g(x)) * g'(x)
反向模式:
先计算 g(x),再计算 f(g(x))
反向:计算 f'(g(x)),再计算 f'(g(x)) * g'(x)
三、操作指南(PyTorch)
3.1 基本使用流程
import torch
# 步骤 1: 创建张量并设置 requires_grad=True
x = torch.arange(4.0, requires_grad=True)
# 步骤 2: 执行计算(自动构建计算图)
y = 2 * torch.dot(x, x)
# 步骤 3: 反向传播计算梯度
y.backward()
# 步骤 4: 获取梯度
print(x.grad) # tensor([0., 4., 8., 12.])
3.2 关键操作步骤
| 步骤 | 代码 | 说明 |
|---|---|---|
| 启用梯度追踪 | x.requires_grad_(True) |
标记张量需要计算梯度 |
| 执行计算 | y = f(x) |
自动构建计算图 |
| 反向传播 | y.backward() |
计算梯度并累加到 .grad |
| 获取梯度 | x.grad |
读取计算好的梯度 |
| 梯度清零 | x.grad.zero_() |
重要:每次迭代前必须清零 |
3.3 非标量输出的反向传播
当输出不是标量时,需要传入 gradient 参数:
x = torch.arange(4.0, requires_grad=True)
y = x * x # 向量输出
# 需要指定梯度参数(相当于链式法则的上游梯度)
y.backward(gradient=torch.ones_like(x))
# 等价于 y.sum().backward()
四、API 手册
4.1 核心 API
torch.tensor(data, requires_grad=False)
创建张量,requires_grad=True 启用梯度追踪。
tensor.requires_grad_(True/False)
动态设置是否需要梯度追踪。
tensor.backward(gradient=None, retain_graph=False, create_graph=False)
执行反向传播。
gradient:非标量输出时的上游梯度retain_graph:是否保留计算图(多次 backward 时需要)create_graph:是否创建梯度的计算图(用于高阶导数)
tensor.grad
存储计算好的梯度,初始为 None。
tensor.grad.zero_()
清零梯度(原地操作)。
tensor.detach()
从计算图中分离张量,返回一个不需要梯度的新张量。
torch.no_grad()
上下文管理器,临时禁用梯度追踪(用于推理/评估)。
4.2 高级 API
torch.autograd.grad(outputs, inputs, grad_outputs=None, create_graph=False, retain_graph=None)
计算并返回梯度,不修改 .grad 属性。
# 计算高阶导数
x = torch.tensor(2.0, requires_grad=True)
y = x ** 3
dy_dx = torch.autograd.grad(y, x)[0] # 3*x^2 = 12
d2y_dx2 = torch.autograd.grad(dy_dx, x)[0] # 6*x = 12
torch.autograd.functional.jacobian(func, inputs)
计算雅可比矩阵。
tensor.register_hook(hook)
注册梯度钩子函数,在梯度计算时调用。
五、最佳实践
5.1 训练循环标准模式
for epoch in range(num_epochs):
for batch_x, batch_y in dataloader:
# 1. 梯度清零
optimizer.zero_grad() # 或 model.zero_grad()
# 2. 前向传播
output = model(batch_x)
loss = criterion(output, batch_y)
# 3. 反向传播
loss.backward()
# 4. 参数更新
optimizer.step()
5.2 推理/评估模式
model.eval() # 切换到评估模式
with torch.no_grad(): # 禁用梯度追踪,节省内存
output = model(input)
5.3 分离计算场景
# 场景:希望将某部分计算视为常数
x = torch.randn(4, requires_grad=True)
y = x * x
u = y.detach() # 分离,梯度不会流经 y
z = u * x
z.sum().backward() # 只计算 z 对 x 的梯度,y 视为常数
5.4 控制流中的梯度
def f(a):
b = a * 2
while b.norm() < 1000: # 动态控制流
b = b * 2
if b.sum() > 0:
c = b
else:
c = 100 * b
return c
a = torch.randn((), requires_grad=True)
d = f(a)
d.backward()
# 梯度仍然可以正确计算!
六、避坑指南
❌ 坑 1:忘记梯度清零
# 错误写法
for x, y in dataloader:
output = model(x)
loss = criterion(output, y)
loss.backward() # 梯度会累加!
optimizer.step()
# 正确写法
for x, y in dataloader:
optimizer.zero_grad() # 必须先清零
output = model(x)
loss = criterion(output, y)
loss.backward()
optimizer.step()
❌ 坑 2:在 no_grad 中修改参数
# 错误:这样参数不会追踪梯度
with torch.no_grad():
param -= lr * param.grad # param.grad 存在但 param 不追踪
# 正确:在 no_grad 外更新,或使用 optimizer
❌ 坑 3:多次 backward 未保留计算图
# 错误:第二次 backward 会报错
y = x ** 2
y.backward()
y.backward() # RuntimeError: Trying to backward through the graph a second time
# 正确:设置 retain_graph=True
y.backward(retain_graph=True)
y.backward()
❌ 坑 4:原地操作破坏计算图
# 错误:原地操作可能破坏计算图
x = torch.randn(4, requires_grad=True)
y = x * 2
x = x + 1 # 原地修改 x,可能导致梯度计算错误
# 正确:避免原地操作,或使用 .data
❌ 坑 5:非标量输出未传 gradient 参数
# 错误:y 是向量,直接 backward() 会报错
x = torch.randn(4, requires_grad=True)
y = x * x
y.backward() # RuntimeError: grad can be implicitly created only for scalar outputs
# 正确:传入 gradient 参数
y.backward(gradient=torch.ones_like(x))
# 或
y.sum().backward()
❌ 坑 6:detach 使用不当
# 场景:想要固定某部分网络
# 错误:这样还是会计算梯度
fixed_layer = model.layer1
# 正确:使用 eval() 或 requires_grad_(False)
for param in model.layer1.parameters():
param.requires_grad = False
七、总结
| 关键点 | 说明 |
|---|---|
| 核心思想 | 将复杂函数分解为基本运算,通过链式法则自动计算导数 |
| 计算图 | 有向无环图,记录计算过程和依赖关系 |
| 反向模式 | 深度学习的核心,适合输出维度 < 输入维度的场景 |
| requires_grad | 标记需要计算梯度的张量 |
| backward() | 执行反向传播,梯度累加到 .grad |
| zero_grad() | 每次迭代前必须清零梯度 |
| detach() | 从计算图中分离张量 |
| no_grad() | 临时禁用梯度追踪,节省内存 |