4. 自动微分:grad函数、高阶导数、jacfwd与jacrev、vjp与jvp

自动微分,说白了就是让框架帮你算导数。你写个函数,JAX 自动给你把梯度算出来。这事在深度学习里太常见了——反向传播嘛。但 JAX 的自动微分比 PyTorch 或 TensorFlow 要「纯粹」得多,它不维护计算图,而是用函数式变换的方式来做。

我个人习惯把自动微分分成两类:前向模式反向模式。JAX 两者都支持,而且用起来非常灵活。咱们一个一个来看。

4.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,因为 f'(x) = 2x + 3

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

def g(x, y):
    return x ** 2 + y ** 3

grad_g_x = jax.grad(g, argnums=0)
grad_g_y = jax.grad(g, argnums=1)

print(grad_g_x(2.0, 3.0))  # 4.0
print(grad_g_y(2.0, 3.0))  # 27.0
我的经验:我在项目中遇到过一个问题——grad 要求输入必须是标量输出。如果你的函数输出是向量,grad 会报错。这时候你需要用 jacfwdjacrev

4.2 高阶导数:grad 套 grad

JAX 的自动微分是可组合的。你可以对 grad 的结果再求 grad,得到二阶导数。这就是高阶导数的本质——函数变换的嵌套。

def f(x):
    return x ** 3

first_deriv = jax.grad(f)
second_deriv = jax.grad(first_deriv)
third_deriv = jax.grad(second_deriv)

print(first_deriv(2.0))   # 12.0
print(second_deriv(2.0))  # 12.0
print(third_deriv(2.0))   # 6.0

为什么会这样?因为 grad 返回的仍然是一个 JAX 可追踪的函数。你可以无限嵌套下去。不过实际项目中,我很少用到三阶以上——计算量太大,而且数值稳定性会变差。

注意:高阶导数在数值上可能不稳定。我曾经在训练一个物理信息神经网络(PINN)时,用了四阶导数,结果梯度爆炸了。建议先用低阶验证,再逐步提高。

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

当你的函数是 R^n → R^m 时,梯度就不够用了。你需要雅可比矩阵——一个 m×n 的矩阵,包含所有偏导数。

JAX 提供了两种方式: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)
J_rev = jax.jacrev(f)(x)

print(J_fwd)
# [[4. 0.]
#  [0. 27.]
#  [3. 2.]]

两者结果一样,但计算方式不同。你想想看:jacfwd 适合「输入小、输出大」的场景(比如 n=1, m=1000),而 jacrev 适合「输入大、输出小」的场景(比如 n=1000, m=1)。

方法 适用场景 计算复杂度
jacfwd 输入维度小,输出维度大 O(n) 次前向传播
jacrev 输入维度大,输出维度小 O(m) 次反向传播
核心原则:选择 jacfwd 还是 jacrev,取决于你的输入输出维度哪个更小。选错了,计算量可能差好几个数量级。

4.4 vjp 与 jvp:向量-雅可比积与雅可比-向量积

很多时候你不需要完整的雅可比矩阵,只需要它和一个向量的乘积。这就是 vjp(向量-雅可比积)和 jvp(雅可比-向量积)的用武之地。

jvp 是前向模式:给定一个输入方向向量,计算函数在该方向上的变化率。

def f(x):
    return x ** 2

x = 3.0
tangent = 1.0  # 方向向量

val, jvp_out = jax.jvp(f, (x,), (tangent,))
print(val)      # 9.0
print(jvp_out)  # 6.0

vjp 是反向模式:给定输出侧的向量,反向传播到输入侧。

val, vjp_func = jax.vjp(f, x)
cotangent = 1.0
vjp_out = vjp_func(cotangent)
print(val)      # 9.0
print(vjp_out)  # (6.0,)

说白了,jvpvjp 是构建更复杂自动微分系统的基础。比如在神经网络的 Hessian 向量积计算中,我就经常用 jvp 来避免显式构建 Hessian 矩阵。

避坑指南:我曾经在实现一个自定义优化器时,需要计算 Hessian 向量积。一开始我直接用 jacfwd(jacrev(f)),结果内存爆了。后来改用 jvp(grad(f)),效率提升了 10 倍。记住:能用 vjp/jvp 就别显式构建雅可比矩阵。

4.5 知识体系总览

下面这张图展示了 JAX 自动微分的核心逻辑。你可以看到,所有工具都围绕「函数变换」这个核心理念展开。

自动微分 标量输出 → grad 向量输出 → Jacobian 向量积 → vjp/jvp 高阶导数 jacfwd jacrev vjp jvp 应用场景 神经网络训练 · 物理模拟 · 优化算法 · 科学计算

4.6 实战建议

最后,给你几个我在实际项目中总结的经验:

  • 优先用 grad:如果你的函数输出是标量,直接用 grad,简单高效。
  • 雅可比矩阵要谨慎:显式构建雅可比矩阵很贵。能用 vjp/jvp 就别用 jacfwd/jacrev。
  • 高阶导数要测试:写个简单函数验证一下数值正确性,别直接上大模型。
  • 组合使用:比如 jvp(grad(f)) 可以高效计算 Hessian 向量积,这在二阶优化器中非常有用。

自动微分是 JAX 最强大的特性之一。它让你可以像写数学公式一样写代码,然后自动得到导数。嗯,掌握了这些,你就能在科学计算和深度学习里游刃有余了。

专注资料整理