想象你经营着一家正在迈向智能化的AI餐厅。普通后厨里,每道菜都得靠大厨亲手做;而JAX这间后厨却有一套“函数变换”魔法:你可以把任何一道做菜工序(一个函数)当作可以随意改造的食材,递给三台神奇的机器——品菜师 grad、操作手册 jit、批量流水线 vmap。品菜师尝一口成品,就能立刻报出每味调料该往哪个方向调整多少;操作手册把大厨的每一步动作提前演练成固定流程,从此不用边做边想;批量流水线则让一份菜谱同时喂饱上百桌顾客。这三台机器可以单独使用,也能像积木一样叠在一起,构成一套高效的“自动烹饪流水线”。这就是函数式自动微分:不是教机器“怎么求导”,而是把“求导”本身变成对函数的一次普通加工。

岗位关联

掌握 grad/jit/vmap,等于同时握住了自动求导、编译加速和批量并行三把钥匙。这是所有 JAX 生态(深度学习、科学计算、强化学习)工程的底层基本功,也是面试中区分“会调框架”与“懂框架”的关键分水岭。

目录
  1. 可视化演示
  2. 函数式变换
  3. 自动微分原理
  4. grad梯度变换
  5. jit编译加速
  6. vmap向量化
  7. 三变换组合
  8. 工程实践
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

本节配有交互动效(进入视口后自动加载,约 3KB)。

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

函数式变换

传统后厨里,菜谱是贴在墙上的纸,大厨照着做菜,纸本身不会被送进锅里。而 JAX 的后厨理念完全不同:菜谱(函数)本身就是最重要的食材。你可以把一份菜谱递给品菜师、塞进流水线、甚至和另一份菜谱拼接,得到一份全新的菜谱。这就是“函数是一等公民”——函数可以像数值一样被传递、返回、嵌套,而 JAX 的 grad/jit/vmap 正是三个“加工菜谱的厨具”。

要想让这三台厨具正常工作,菜谱必须满足一条铁律:纯函数。纯函数就像一份“不看窗外天气、不碰邻居锅铲”的菜谱——同样的食材放进去,永远得到同样的菜,绝不偷偷修改后厨里的其他东西。如果函数内部读取了全局变量、改了外部列表,或者偷偷打印日志,品菜师在反推调料时就会被这些“看不见的手”误导,得出完全错误的结论。

面向对象编程(OOP)与此形成鲜明对比:OOP 把行为和状态绑在一个对象里,像一位自带厨具和私房调料的流动大厨;而 JAX 希望大厨两手空空,所有食材和调料都从参数口递入。这种设计看似笨拙,却让变换变得极其简洁——因为没有隐藏状态,任何一步都可以被安全地打包、复制、并行。

把函数当食材、变换当厨具,这样“元烹饪”的思路让 JAX 的代码天然具备可组合性。你用 grad 做出的“梯度菜谱”,可以继续丢进 jit 里翻炒,或者塞进 vmap 的流水线——变换之间浑然一体,这正是后续三台机器的共同地基。

f:Rn→R,T(f):Rn→Rnf: \mathbb{R}^n \to \mathbb{R}, \quad T(f): \mathbb{R}^n \to \mathbb{R}^n
变换 T(如 grad)把一个“多进单出”的函数,改造成“多进多出”的新函数。这就是函数式变换最核心的代数结构。
  1. 原始函数$f$ 接受一个 $n$ 维向量(比如神经网络的参数 $\theta$),输出一个标量(比如损失值)
    f:Rn→Rf: \mathbb{R}^n \to \mathbb{R}
  2. 变换后的函数变换 $T$ 作用于 $f$ 后,新函数的输入仍是 $n$ 维向量,但输出变成 $n$ 维向量(比如每个参数的梯度分量)
    T(f):Rn→RnT(f): \mathbb{R}^n \to \mathbb{R}^n

自动微分原理

传统厨房培养一个品菜师,需要让他把每味调料都单独尝一遍,才知道哪一味出了问题。而 JAX 的品菜师极其聪明:他只尝最终成品,然后沿着“做菜因果链”一步步反推。这条因果链就是计算图——从食材 $x$ 变成中间半成品 $y$,再变成最终菜肴 $L$,每一步都是一个节点,边则是运算关系。

品菜师反推时使用的核心工具叫链式法则:成品味道的偏差 $L$ 对中间半成品 $y$ 有多敏感,乘以半成品 $y$ 对原始食材 $x$ 有多敏感,两者相乘就得到成品对原始食材的总敏感度。这个乘法沿着计算图的每条边一路从后往前传播,所有“敏感度”(偏导数)一次前向烹饪、一次反向品尝即可全部算出——这就是反向传播。

自动微分与手工求导、数值差分都不同:数值差分像“每次只动一味调料重新做一桌菜”,开销巨大且精度有限;手工求导像“请数学家逐项推导”,费时易错。而自动微分把求导规则内嵌进计算图的每条边上,前向做菜、反向品尝,一次性精确得到所有梯度,这正是 grad 变换的效率之源。

∂L∂x=∂L∂y⋅∂y∂x\displaystyle \frac{\partial L}{\partial x} = \frac{\partial L}{\partial y} \cdot \frac{\partial y}{\partial x}
误差从菜品 $L$ 端出发,层层回传,最后算出食材 $x$ 的梯度。整个过程就像品菜师沿因果链“倒着走”。
  1. 成品对半成品的敏感度$\frac{\partial L}{\partial y}$ 表示最终损失 $L$ 随中间量 $y$ 的变化率,即“半成品的火候对最终口感的贡献”
    ∂L∂y\frac{\partial L}{\partial y}
  2. 半成品对食材的敏感度$\frac{\partial y}{\partial x}$ 表示中间量 $y$ 随原始输入 $x$ 的变化率,即“食材分量对半成品咸淡的贡献”
    ∂y∂x\frac{\partial y}{\partial x}
  3. 链式相乘两个变化率相乘,抵消掉中间量 $y$,得到 $L$ 对 $x$ 的直接敏感度
    ∂L∂y⋅∂y∂x\frac{\partial L}{\partial y} \cdot \frac{\partial y}{\partial x}

grad梯度变换

现在让品菜师正式上岗。grad(f) 接受一个函数,返回它的“梯度函数”——这个梯度函数输入同样的参数,输出的是每个参数的调整方向与力度。对深度学习而言,这个方向正是参数下降的指引:它告诉后厨“盐该加还是该减、火候该大还是该小”。

对于多维参数,梯度是一个向量,每一维对应一个参数的偏导数。JAX 默认对第一个参数求导,但如果你的真正“食材”是第二个、第三个参数,就需要用 argnums 指定——就像品菜师要明确“你重点评价的是主料还是辅料”。

品菜师还能自我升级:grad(grad(f)) 得到二阶导数,相当于让品菜师评价“品菜师本人调整调料的策略是否合理”。这在优化算法(如牛顿法)和科学计算中十分有用。此外,梯度裁剪是常见的防御手段——当某个调料调整幅度过大可能毁掉整锅菜时,把梯度限制在一个安全范围内,保证训练稳定。

品菜师的上岗让“调参”从玄学变成科学:你不再需要闭着眼睛试错,每次迭代只需把参数沿着梯度的反方向挪一小步——这就是梯度下降。而 grad 变换最大的魅力在于,无论你的菜谱(模型)多复杂,嵌套了多少层计算,品菜师都能自动完成反推,彻底解放了手工推导导数的负担。

∇θL(θ)=[∂L∂θ1,  ∂L∂θ2,  …,  ∂L∂θd]⊤\displaystyle \nabla_\theta L(\theta) = \left[ \frac{\partial L}{\partial \theta_1},\; \frac{\partial L}{\partial \theta_2},\; \ldots,\; \frac{\partial L}{\partial \theta_d} \right]^\top
损失 $L$ 对参数向量 $\theta$ 的梯度,就是品菜师给出的“全调料调整清单”。
  1. 梯度算子$\nabla_\theta$ 表示对参数向量 $\theta$ 求梯度,读作"nabla"
    ∇θ\nabla_\theta
  2. 单个偏导$\frac{\partial L}{\partial \theta_i}$ 表示损失对第 $i$ 个参数的偏导数,即“第 $i$ 味调料单独变化时对口味的影响”
    ∂L∂θi\frac{\partial L}{\partial \theta_i}
  3. 组合成向量把所有偏导按参数顺序排列,加上转置符号 $\top$ 表示为列向量,形成完整的梯度清单
    [∂L∂θ1,…,∂L∂θd]⊤\left[ \frac{\partial L}{\partial \theta_1}, \ldots, \frac{\partial L}{\partial \theta_d} \right]^\top

jit编译加速

深夜的后厨,客人催菜,大厨却还在翻菜谱、试水温、回忆步骤——这就是纯 Python 运行的典型场景。jit 变换像一位勤奋的学徒:第一次接到任务时,他把大厨的每个动作完整“演练”一遍,记录成一张精确的操作流程图(追踪执行,tracing);然后把这些步骤固化成一册标准作业手册(编译成 XLA 计算图与机器码)。从第二次起,大厨不再思考,直接照着手册行云流水地执行。

但对这本手册有一个关键要求:同一本手册不能适配所有变化。若某次任务的输入形状变了(比如从 3 味调料变成 5 味),手册就得重新编写。JAX 用缓存管理这一切:以输入的形状和类型作为“菜谱索引”,命中缓存就直接照做,否则重新追踪编译。static_argnums 则用于标记那些“不是数值而是配置”的参数(例如循环次数、布尔开关),它们的取值变化同样会触发重编译。

jIT 的加速效果在复杂模型上往往能达到数倍到数十倍。但要记住,这本手册“背下来”的前提是函数行为完全可预测——如果菜谱里藏了 print、随机数、全局变量这类“看心情”的动作,学徒在追踪时会把它们固化一次,之后每次照搬,造成与直觉不符的行为(后文误区会展开)。

jit(f)(x)=[compile∘trace](f)(x)\text{jit}(f)(x) = [\text{compile} \circ \text{trace}](f)(x)
jit 对函数 $f$ 做了两步加工:先追踪成图,再编译成机器码。对外表现为一个更快的函数。
  1. 追踪$\text{trace}(f)$ 模拟执行一次 $f$,把所有运算记录成一张计算图(不真的跑出数值)
    trace(f)\text{trace}(f)
  2. 编译$\text{compile}(\cdot)$ 把这张计算图翻译成底层机器码,并做算子融合等优化
    compile(⋅)\text{compile}(\cdot)
  3. 复合作用$\circ$ 表示“先追踪后编译”,得到的新函数直接作用于输入 $x$ 上
    [compile∘trace](f)(x)[\text{compile} \circ \text{trace}](f)(x)

vmap向量化

一个顾客点一份宫保鸡丁,大厨炒一锅;一百个顾客同时点,还一锅一锅地炒,后厨早就崩溃了。vmap 变换解决的就是这个问题:它把“处理单个样本”的函数,自动改造成“同时处理一批样本”的函数。原本需要手写 for 循环逐份处理的活,现在变成一条高效流水线,一份菜谱同时喂饱一百桌。

想象你有一个函数 $f$ 能处理单个样本 $x_i$,现在来了一个批量数据 $X = [x_1, x_2, \ldots, x_n]$。vmap 会创造一个新函数,一次性消费整个批次,并且对每一行输出 $f(x_i)$。关键在于轴语义:in_axes 告诉 vmap 每个参数应该映射到哪根轴,比如 (None, 0) 表示第一个参数对所有样本共享(如模型权重),第二个参数按第 0 维展开(如样本批次)。

与手写循环相比,vmap 不只是省去 Python 循环开销,更重要的是它能利用底层算子的批量内核(batching rules),把对小矩阵的重复运算融合成对大矩阵的一次高效运算。vmap 还支持嵌套使用:处理“多张桌子每组一百份订单”时,外层 vmap 管桌子、内层 vmap 管订单,维度语义依旧清晰。

∀ i∈{1,2,…,n}:[vmap(f)(X)]i=f(Xi)\forall\, i \in \{1,2,\ldots,n\}:\quad [\text{vmap}(f)(X)]_i = f(X_i)
批量结果的第 $i$ 行,恰好等于单样本函数作用在第 $i$ 个样本上的输出。vmap 保证语义完全一致,只是执行方式高效得多。
  1. 全称量词$\forall\, i$ 表示“对批次中的每一个样本”
    ∀ i∈{1,2,…,n}\forall\, i \in \{1,2,\ldots,n\}
  2. 批量结果$[\text{vmap}(f)(X)]_i$ 取批量输出中第 $i$ 个元素
    [vmap(f)(X)]i[\text{vmap}(f)(X)]_i
  3. 等价的单样本计算$f(X_i)$ 恰好是原函数单独处理第 $i$ 个样本的结果,两者数值完全相等
    f(Xi)f(X_i)

三变换组合

三台机器单独使用已经很强大,真正的魔法在于把它们像积木一样叠起来。深度学习训练中一个典型场景是:对一批样本计算损失并求梯度,然后把这个过程编译加速。此时需要 vmap 负责“批量做菜”,grad 负责“品尝并反推调料调整”,jit 负责“把整套流程背下来”。

组合的顺序有一定讲究。vmap(grad(f)) 是对“梯度函数”做批量映射,适合“每个样本独立求梯度”的场景;而 grad(vmap(f)) 则要求先批量计算再统一求导,通常要求 $f$ 输出标量。实际中最常用的训练步骤是:先 vmap 把模型的前向传播批量展开,再 grad 求出参数梯度,最后用 jit 把整个复合函数编译成单一融合算子。这样每次迭代只需一次启动开销,GPU 的资源利用率也被拉到最高。

在真实项目中,这种组合让代码既简洁又高效:你写的是“单个样本的数学公式”,运行时却享受了批量并行和编译优化双重加速。这也是 JAX 在科学计算和深度学习领域备受青睐的重要原因——写起来像研究代码,跑起来像生产代码。

train_step=jit(grad(vmap(f)))\text{train\_step} = \text{jit}(\text{grad}(\text{vmap}(f)))
一个典型的组合:批量前向、自动求梯度、编译加速,三者合体构成训练闭环的核心。
  1. 内层批量$\text{vmap}(f)$ 先把单样本前向函数变成批量前向函数
    vmap(f)\text{vmap}(f)
  2. 中层求导$\text{grad}(\cdot)$ 对批量损失函数求梯度,输出参数的梯度向量
    grad(vmap(f))\text{grad}(\text{vmap}(f))
  3. 外层编译$\text{jit}(\cdot)$ 把整个求导过程编译成单一加速算子,让每次迭代都快如闪电
    jit(grad(vmap(f)))\text{jit}(\text{grad}(\text{vmap}(f)))

工程实践

后厨再好的菜谱,遇到糟糕的食材管理也会翻车。JAX 工程实践的第一条铁律是随机数控制:不要使用全局随机种子,因为那会破坏纯函数原则,导致 jit 和 grad 的追踪结果不可复现。正确做法是显式传入一个随机密钥,需要更多随机数时用 jax.random.split 把它拆成多个子密钥——就像主厨把一袋盐分成几份,每份用途明确。

第二条是调试技巧。很多人惊讶地发现 jit 函数里的 print 只打印一次——因为 print 发生在追踪阶段,之后的每次调用都在照着手册执行,不再“读台词”。需要调试时,可以先用纯 Python 模式跑通逻辑,再逐步加上 jit 和 vmap,并利用 jax.debug.print 在编译内输出运行期信息。第三条是避免在变换函数内修改全局可变状态,这会像“菜谱外传的暗号”一样让编译器无法安全优化。此外,显存优化(如 donate_argnums 声明参数可被覆盖)可以进一步降低峰值显存占用。

把这条“显式密钥”原则贯彻到训练代码中,你会发现每次实验都可复现、每段逻辑都可测试。工程实践的终极心法只有一句:让每一份菜谱都只依赖端进来的食材,不偷看窗外,不乱碰邻锅——这样品菜师、操作手册和流水线才能百分之百信任它。

key→split(key)=(key1,key2)\text{key} \to \text{split}(\text{key}) = (\text{key}_1, \text{key}_2)
随机密钥的拆分:一个密钥裂变成两个独立子密钥,分别用于生成输入数据和噪声,保证一切都是显式、可复现的。
  1. 原始密钥$\text{key}$ 是 JAX 随机数的唯一来源,任何随机操作都从它出发
    key\text{key}
  2. 拆分操作$\text{split}(\text{key})$ 将一个密钥分裂成两个独立但互不相关的子密钥
    split(key)\text{split}(\text{key})
  3. 两个子密钥$(\text{key}_1, \text{key}_2)$ 可分别用于不同用途(如生成特征与噪声),保证纯函数特性
    (key1,key2)(\text{key}_1, \text{key}_2)

AI 应用场景

场景一:大规模深度学习训练管线加速

阶段:模型训练与调优

解决的问题:传统深度学习框架中,手写批量循环 + 反向传播 + 图编译往往需要拼接多套 API,代码零散且难优化。JAX 用 jit(grad(vmap(f))) 一条表达式统一了“批量、求导、编译”三个需求,让研究者把精力放在模型数学本身。

具体案例:某团队训练一个图像分类模型,原来用 PyTorch 写训练循环,每步要手动 optimizer.zero_grad()、loss.backward()、optimizer.step();迁移到 JAX 后,他们将单样本损失函数 loss_fn 用 vmap 批量展开,再用 grad 求梯度,最后 jit 编译成 train_step。在同等 batch size 下,单步训练时间缩短了约 3 倍,且代码量减少近一半。

场景二:强化学习策略并行采样与更新

阶段:数据采样与策略优化

解决的问题:强化学习中,智能体需要在多个环境中并行采样,并对策略网络的参数批量求梯度。传统做法要么开多个进程,要么手动把 batch 维度塞进网络。vmap 让同一份策略网络代码同时处理数百个环境,且与 grad/jit 无缝衔接。

具体案例:某强化学习团队训练一个连续控制智能体,环境交互得到 256 条轨迹后,需要对每条轨迹计算策略梯度。他们用 vmap(policy_loss) 对整批轨迹批量求梯度,再 jit 编译整个更新过程,将一次策略更新的耗时从 4 秒压缩到 0.6 秒,使每日实验量提升超过 5 倍。

场景三:科学计算中的大规模参数反演

阶段:物理仿真与参数反演

解决的问题:气象、流体等科学计算常需要求解“参数对目标的敏感度”,即大规模 Jacobian。JAX 的 grad + vmap 可以高效处理批量样本的梯度计算,且纯函数式设计天然适配高性能计算场景。

具体案例:某气候建模团队用 JAX 重写了大气辐射传输模型,将 1000 组大气廓线参数同时送入 vmap(grad(simulate)),一次性得到所有廓线的辐射通量梯度,用于反演气溶胶浓度。相比此前基于有限差分的逐点扰动,计算速度提升了约 20 倍,且梯度精度由差分误差 \(10^{-6}\) 提升到机器精度。

Python 演示

python39 行
import jaximport jax.numpy as jnpfrom jax import grad, jit, vmap  def predict(w, x):    return jnp.dot(x, w)  def loss(w, x, y):    pred = vmap(predict, in_axes=(None, 0))(w, x)    return jnp.mean((pred - y) ** 2)  grad_loss = grad(loss)  LR = 0.05  @jitdef update(w, x_batch, y_batch):    return w - LR * grad_loss(w, x_batch, y_batch)  key = jax.random.PRNGKey(42)k1, k2 = jax.random.split(key) x = jax.random.normal(k1, (128, 3))true_w = jnp.array([1.5, -2.0, 3.0])y = x @ true_w + 0.1 * jax.random.normal(k2, (128,)) w = jnp.zeros(3)for epoch in range(50):    w = update(w, x, y)    if epoch % 10 == 0:        print(f"epoch {epoch}: loss = {loss(w, x, y):.4f}") print("final w:", w)
目的:演示 grad/jit/vmap 三者在最简线性回归训练中的协作——vmap 批量预测、grad 自动求梯度、jit 编译加速更新步骤。
  1. vmap(predict, in_axes=(None, 0))让单样本预测函数自动支持批量输入,第二个参数按第 0 维展开,第一个参数 w 对整批共享
  2. grad_loss = grad(loss)生成品菜师:对 loss 第一个参数 w 求梯度,返回一个“输入参数输出梯度”的新函数
  3. @jit把 update 整个流程编译成 XLA 算子,第二次调用起直接命中缓存
  4. jax.random.split(key)把一个随机密钥拆成两个子密钥,分别生成特征和噪声,保证纯函数可复现
  5. w = update(w, x, y)每次迭代传入当前参数,返回更新后的新参数;JAX 没有隐式状态,参数显式流转

常见误区

误区编译器调用时,函数内的 print 都会输出一次;打印日志有助于观察每次迭代的中间结果。

正解jit 会先追踪一遍函数,把它变成计算图。print 发生在追踪阶段,只执行一次,之后的调用都在执行编译后的机器码,print 不再出现。想在编译内调试应使用 jax.debug.print。就像学徒只看大厨演示一遍就记住了流程,之后照做时不会再把步骤念出来。

为何常见:直觉上“函数每次被调用,代码就从头到尾执行一遍”,这是对普通 Python 函数的认知惯性,没意识到 jit 改变了执行模型。

误区grad(f) 返回一个新函数,无论 f 输出的是标量还是向量,它都能直接给出梯度向量。

正解JAX 的 grad 要求被求导函数输出是标量。如果输出是向量,会直接报错 Grad only applies to real scalar-output functions。需要手动把输出求和(如 jnp.sum)或用 jax.jacobian 求矩阵。品菜师只能评述“一整道菜的综合口味”,无法同时给出每桌每道菜的完整点评。

为何常见:数学课上梯度可以对向量函数定义,初学者自然认为框架也能直接处理,没注意到 JAX 为 API 简洁性做的标量约束。

误区vmap 本质上就是帮你写了一个 for 循环,只是语法更简洁,性能上不会有本质差别。

正解vmap 并不是在 Python 层循环,而是通过算子的 batching rule 把“小运算重复多次”融合成“大运算一次完成”,底层调用的是专门为批量张量优化的内核。手写 for 循环要付出 Python 解释开销,且无法触发底层融合;vmap 则直接把批量维交给编译器。

为何常见:从功能语义看 vmap 确实等价于 for 循环,容易让人忽略运行机制上的巨大差异。

误区grad(jit(f)) 和 jit(grad(f)) 都是求梯度加编译,最终数值和性能完全一样,随便写。

正解多数情况下二者数值等价,但性能特征不同。grad(jit(f)) 是先编译再求导,梯度对已编译的计算图求导,JAX 内部可能对编译后的图做额外展开;jit(grad(f)) 是先求导再编译,编译器看到的是包含反向传播的完整计算图,通常能做出更好的算子融合。工程上推荐先 grad 再 jit。

为何常见:默认代数运算满足交换律,容易把函数变换也当成可交换运算,忽略了编译优化层面的差异。

误区在损失函数里直接调用 Python 的 `random.random()` 或 `np.random.rand()` 生成随机噪声做数据增强,没有破坏什么规则。

正解这些调用会读取全局随机状态,属于副作用。grad 和 jit 在追踪时会把随机数结果固化成常量,导致每次调用得到相同数据或梯度完全错误。应使用 jax.random 并显式传入 key,再用 jax.random.split 拆分。

为何常见:随机数在普通编程中被视为“理所当然的全局服务”,而函数式变换要求所有输入显式化,这种范式冲突最容易在不知不觉中踩坑。

面试题

Q1. JAX 的 grad(f) 默认对函数的哪个参数求梯度?

单选题 · easy

  • A.最后一个参数
  • B.第一个参数← 正确答案
  • C.所有参数之和
  • D.需要显式传入名称

答案:B。grad 默认 argnums=0,即第一个参数。就像品菜师默认评价第一味主料,若想评价其他配料需用 argnums 指定。A 是顺序错误,C 是“求所有参数的梯度”的误解(grad 只返回一个参数的梯度),D 中 JAX 不用名称指定位置。

Q2. 关于 jit 的缓存机制,下列说法正确的是?

单选题 · easy

  • A.每次调用都会重新编译一次
  • B.相同参数形状和类型时直接命中缓存← 正确答案
  • C.jit 无法缓存因为函数变化太多
  • D.只有当参数值完全相同时才命中缓存

答案:B。jit 以参数的形状与类型(及静态参数值)作为缓存键,而不是参数的具体取值。就像学徒记住了“三口锅的菜谱”,不管锅里具体是什么菜,只要锅数不变就照同一套流程来。A 与缓存机制矛盾,D 错误地把具体数值当缓存键,C 完全没有理解缓存原理。

Q3. vmap(f, in_axes=(None, 0)) 的含义是?

单选题 · easy

  • A.两个参数都按第 0 维批量映射
  • B.第一个参数不映射,第二个参数按第 0 维映射← 正确答案
  • C.第一个参数按第 0 维,第二个参数不映射
  • D.只有第一个参数参与映射

答案:B。in_axes 元组按位置对应每个参数,None 表示该参数对所有样本共享,0 表示按第 0 维展开。就像主料 w 是共享的锅,食材 x 是每桌各一份的订单。A 忽略 None,C 把顺序搞反,D 漏掉了第二个参数的参与。

Q4. 对输出为向量的函数 f 直接调用 grad(f) 会怎样?

单选题 · mid

  • A.返回一个向量梯度
  • B.报错,grad 要求输出必须是标量← 正确答案
  • C.自动对输出求和后求导
  • D.返回零向量

答案:B。JAX 的 grad 严格要求输出为标量,否则抛出 Grad only applies to real scalar-output functions。品菜师只能评价整道菜的综合口味,不能一次点评整桌菜。A 是数学直觉但不是 JAX 行为,C 需要手动 jnp.sum,D 毫无依据。

Q5. static_argnums 参数在 jit 中的主要作用是?

单选题 · mid

  • A.提高求导的数值精度
  • B.将指定参数视为静态值来建立编译缓存← 正确答案
  • C.禁止该参数在训练中更新
  • D.控制梯度裁剪的阈值

答案:B。static_argnums 标记那些不被追踪为数值张量的参数(如整数开关、循环次数),它们的取值变化会导致重新编译。就像“桌布颜色”变了就换一本手册,但不需要重新配菜。A、C、D 都与编译缓存无关,是对参数的误解。

Q6. grad(grad(f)) 在深度学习中主要用于?

单选题 · mid

  • A.计算二阶导数← 正确答案
  • B.加速反向传播
  • C.参数初始化
  • D.数据增强

答案:A。grad 嵌套一次得到一阶梯度,再嵌一次得到二阶梯度,可用于牛顿法、Hessian 对角近似等场景。就像让品菜师评价“品菜师调料的策略是否合理”。B 是 grad 本身的职责,C、D 与求导无关。

Q7. grad(jit(f)) 与 jit(grad(f)) 的典型差异是?

单选题 · hard

  • A.数值结果完全不同
  • B.前者不包含反向传播图,编译优化空间小
  • C.后者让编译器融合前向与反向计算图← 正确答案
  • D.两者在性能上没有差异

答案:C。jit(grad(f)) 先求梯度,编译器能看到包含反向传播的完整计算图,可以做更好的算子融合;grad(jit(f)) 先编译再对图求导,优化空间受限。A 错在数值通常一致,B 说反了,D 忽略了两者在性能特征上的差异。

Q8. 关于 JAX 的 grad/jit/vmap,说法正确的有?

多选题 · mid

  • A.三种变换都以函数为输入或输出← 正确答案
  • B.三种变换可以任意嵌套组合← 正确答案
  • C.jit 可以替代 grad 完成自动求导
  • D.vmap 把单样本函数变成批量映射函数← 正确答案

答案:ABD。三者都是函数变换——输入函数输出新函数,且可以像积木一样嵌套。vmap 正是把处理单样本的逻辑批量化的工具。C 错误,jit 只做编译加速,不产生梯度;它是“背菜谱”,不是“品菜师”。

Q9. 下列哪些做法会导致 jit 编译结果异常?

多选题 · hard

  • A.在 jit 函数内调用 print 打印中间结果← 正确答案
  • B.在函数内部修改外部全局变量← 正确答案
  • C.使用基于动态数据的 Python 控制流← 正确答案
  • D.将整型参数标记为 static_argnums

答案:ABC。A 的 print 只在追踪时执行一次,运行时不再生效;B 的全局修改是副作用,追踪快照会与真实执行不一致;C 的 Python 分支若依赖 tracer 值会导致编译行为不可预测,应改用 lax.cond。D 是 static_argnums 的标准正确用法,不产生异常。

Q10. 关于组合变换 jit(grad(vmap(f))),下列说法正确的是?

多选题 · extreme

  • A.vmap 的批量维度在梯度计算中会被自动保留← 正确答案
  • B.jit 会把整个复合变换编译成一个融合计算图← 正确答案
  • C.变换顺序无论如何改变都不影响数值结果
  • D.grad 要求内层函数输出为标量才能求导← 正确答案

答案:ABD。vmap 将批量维引入,grad 对批量损失求参数梯度时,批量维自然保留在中间计算中;jit 将整条链融合为单一 XLA 计算图;grad 的标量输出约束让整个组合必须保证内层输出最终归约为标量。C 错误,因为 grad(vmap(f)) 与 vmap(grad(f)) 的合法性依赖不同的输出形状约束,顺序不能随意交换。