3. 自动微分与梯度计算:grad函数、高阶梯度、jacfwd与jacrev、vmap与pmap的自动微分

自动微分,说白了就是让框架帮我们把导数算好。你想想看,以前我们手写反向传播,一个链式法则推半天,稍微改个网络结构就得重新推导。JAX 的自动微分系统,是我用过最顺手的工具之一。它不像 PyTorch 那样维护一个动态计算图,而是把函数变换玩到了极致。

我个人习惯把 JAX 的自动微分分成三个层次:标量梯度向量雅可比批量并行。咱们一层层往下聊。

3.1 grad:最基础的梯度计算

grad 是 JAX 里最常用的自动微分函数。它接收一个函数,返回这个函数的梯度函数。说白了,就是把你写的函数变成它的导数版本。

import jax
import jax.numpy as jnp

def f(x):
    return x**2 + 3*x + 1

grad_f = jax.grad(f)
print(grad_f(2.0))  # 输出 7.0 (2*2 + 3 = 7)

嗯,这里要注意:grad 默认只对第一个参数求导。如果你的函数有多个参数,可以用 argnums 指定。

我的经验: 我在项目中遇到过一个问题——用 grad 对神经网络参数求导时,忘了把参数打包成单个结构。后来我习惯把所有参数放在一个字典或列表里,然后用 argnums=0 统一处理。

3.2 高阶梯度:grad 的嵌套使用

JAX 的 grad 可以嵌套使用,用来计算高阶导数。比如二阶导数、三阶导数,甚至更高阶。这在实际应用中很有用,比如物理模拟中的拉普拉斯算子,或者优化算法中的 Hessian 矩阵。

def f(x):
    return x**3

# 一阶导数
grad_f = jax.grad(f)
print(grad_f(2.0))  # 12.0 (3*2^2)

# 二阶导数
grad2_f = jax.grad(jax.grad(f))
print(grad2_f(2.0))  # 12.0 (6*2)

# 三阶导数
grad3_f = jax.grad(jax.grad(jax.grad(f)))
print(grad3_f(2.0))  # 6.0 (常数)

为什么会这样?因为 grad 返回的仍然是一个函数,你可以继续对它求导。这就像洋葱一样,一层层剥开。

避坑指南: 我曾经在计算二阶梯度时,不小心把输入搞成了整数类型。JAX 的自动微分要求输入是浮点数,否则会报错。记住:jax.grad 只对浮点数有效。

3.3 jacfwd 与 jacrev:雅可比矩阵的两种计算方式

当你的函数是向量到向量的映射时,梯度就变成了雅可比矩阵。JAX 提供了两种计算方式:jacfwd(前向模式)和 jacrev(反向模式)。

说白了,jacfwd 适合「输入小、输出大」的情况,jacrev 适合「输入大、输出小」的情况。怎么理解?

  • jacfwd:一次前向传播,计算所有输出对单个输入的导数。适合输出维度远大于输入维度。
  • jacrev:一次反向传播,计算单个输出对所有输入的导数。适合输入维度远大于输出维度。
def f(x):
    return jnp.array([x[0]**2, x[1]**3, x[0]*x[1]])

x = jnp.array([2.0, 3.0])

# 前向模式
J_fwd = jax.jacfwd(f)(x)
print("jacfwd:\n", J_fwd)

# 反向模式
J_rev = jax.jacrev(f)(x)
print("jacrev:\n", J_rev)

两种方式的结果是一样的,但计算效率不同。我个人的经验是:在神经网络中,参数数量通常远大于损失函数的输出维度,所以 jacrev 更常用。但在某些生成模型中,比如 Neural ODE,输出维度可能很大,这时候 jacfwd 就派上用场了。

核心要点: jacfwd 和 jacrev 在数学上是等价的,区别在于计算顺序和内存占用。选择哪个取决于你的输入输出维度比例。

3.4 vmap:自动向量化

vmap 是 JAX 里我最喜欢的工具之一。它能把一个处理单个样本的函数,自动变成处理批量样本的函数。你想想看,不用手动写 for 循环,也不用操心 batch 维度,多省心。

def single_sample(x):
    return x**2 + 1

# 批量处理
batch_x = jnp.array([[1.0, 2.0], [3.0, 4.0]])
batched_fn = jax.vmap(single_sample)
print(batched_fn(batch_x))

嗯,这里要注意:vmap 默认在第一个维度上映射。如果你的 batch 维度在其他位置,可以用 in_axesout_axes 指定。

我的经验: 我在做图像生成模型时,经常用 vmap 把单张图像的预处理函数变成批量处理。比如归一化、裁剪、旋转等操作,一行代码搞定批量处理,比手写 for 循环快多了。

3.5 pmap:分布式自动微分

pmap 是 JAX 的分布式并行工具。它把计算分布到多个设备(比如多个 GPU 或 TPU)上,每个设备处理一部分数据。和 vmap 类似,但 pmap 是跨设备的。

# 假设你有 2 个 GPU
def f(x):
    return x * 2

# 在多个设备上并行计算
parallel_f = jax.pmap(f)
data = jnp.array([[1.0, 2.0], [3.0, 4.0]])
result = parallel_f(data)

为什么会用到 pmap?说白了,当你的模型太大,单张 GPU 放不下时,或者数据量太大,单张 GPU 处理太慢时,pmap 就是你的救星。

避坑指南: 我曾经在 pmap 中使用了全局变量,结果每个设备上的变量值不一样,导致计算结果出错。记住:pmap 中的函数应该是纯函数,不能依赖外部状态。

3.6 自动微分与 vmap/pmap 的结合

JAX 最强大的地方在于,这些变换可以组合使用。比如,你可以先对函数求梯度,然后用 vmap 批量处理,最后用 pmap 分布到多个设备上。

def loss_fn(params, x, y):
    pred = jnp.dot(x, params)
    return jnp.mean((pred - y)**2)

# 对参数求梯度
grad_fn = jax.grad(loss_fn)

# 批量处理
batched_grad = jax.vmap(grad_fn, in_axes=(None, 0, 0))

# 分布式并行
parallel_grad = jax.pmap(batched_grad)

# 使用示例
params = jnp.array([1.0, 2.0])
x_batch = jnp.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])
y_batch = jnp.array([3.0, 7.0, 11.0])

# 注意:pmap 需要数据在多个设备上分片
# 这里只是演示组合方式
grads = batched_grad(params, x_batch, y_batch)

这种组合能力,让 JAX 在深度学习研究中特别受欢迎。你想想看,写一次函数,就能自动获得梯度、批量处理、分布式并行,多省事。

核心要点: JAX 的自动微分系统是函数式的。你写的函数是纯函数,gradvmappmap 都是函数变换。这种设计让代码更简洁、更易调试、更易扩展。

3.7 知识体系总览

下面这张图展示了本章的知识结构,帮你理清各个概念之间的关系:

JAX自动微分知识体系 自动微分 grad:标量梯度 一阶导数 高阶梯度(嵌套grad) argnums多参数 jacfwd / jacrev:雅可比 jacfwd(前向) jacrev(反向) vmap:自动向量化 in_axes / out_axes 批量处理 pmap:分布式并行 多设备分片 纯函数要求 数据并行 组合使用:grad + vmap + pmap

这张图把本章的核心内容串起来了。从中心的自动微分出发,四个分支分别对应 gradjacfwd/jacrevvmappmap,最后汇聚到组合使用。你想想看,掌握了这些工具,写深度学习代码就像搭积木一样简单。


专注资料整理