View on GitHub

我的极简博客

记录学习与生活

自动微分

一、概念阐述

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)

执行反向传播。

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() 临时禁用梯度追踪,节省内存