想象你是一位厨师,眼前摆着一整套专业厨房:各种锅具、刀具、佐料架和温度控制器。训练深度学习模型,就像为一道菜反复试味并微调配方;而 Optax 正是 JAX 生态里这间“可编程厨房”——它把从简单到复杂的所有优化器(SGD、Adam 等)都做成一个个独立“厨具”,把梯度裁剪、权重衰减、学习率调度等操作包装成“调味步骤”。你不需要自己焊灶台,只需要像点套餐一样,用 chain 把需要的厨具和调味串起来。JAX 的哲学是函数式:厨具不改变锅里的菜本身,而是返回一份“新菜谱”让你去执行。这篇文章会用这间厨房的比喻,带你认识 Optax 的定位、常用优化器、梯度变换、组合策略、参数更新、学习率调度,以及与 Flax 框架的配合方式。学完以后,复杂的训练流程也能被你拆解成一行行清晰的“菜谱”。

岗位关联

对 AI 从业者而言,Optax 是 JAX/Flax 生态中最常用的训练调参工具箱。掌握它,你能把晦涩的优化器配置变成可读可组合的代码,快速尝试新研究思路,显著提升实验迭代效率。

目录
  1. 可视化演示
  2. Optax 初识
  3. 基础优化器
  4. 梯度变换
  5. 组合策略
  6. 参数更新
  7. 学习率调度
  8. Flax 集成
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

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

Optax函数式优化器设计 传统OOP优化器 有内部状态,难以组合和测试 Optax函数式优化器 Optax优化器可视化:展示函数式设计、梯度变换链、SGD/Adam详解、学习率调度及Flax集成。

Optax 初识

走进这间“可编程厨房”,你会发现墙上挂满了厨具:有的像铁锅一样朴素(SGD),有的像智能电压力锅一样自动化(Adam)。Optax 就是这面厨具墙的管理员,它是 JAX 生态中专用于梯度处理和优化器构建的库。在 JAX 的世界里,训练模型不再是“把参数塞进某个对象然后让它自己训练”,而是把梯度、优化器、状态这些食材和厨具摆在台面上,用纯函数一步步加工。

Optax 的核心设计是函数式的:它不会偷偷修改你手里的参数,而是给你一组返回新值的函数。比如 optax.adam(1e-3) 生成一个优化器对象,这个对象其实就是一对函数——一个负责初始化状态,一个负责在每步更新时处理梯度。因为不修改任何已有数据,你可以放心地把同一个优化器传给多个实验,就像同一份菜谱可以被多位厨师反复使用,互不干扰。

这种设计与 Flax 天然互补:Flax 负责搭建神经网络的结构,Optax 负责优化算法的选择与组合,两者通过梯度这一“食材”衔接。用公式来表达一个优化器,可以抽象成:

看完这面厨具墙,你已经明白:Optax 不生产模型,也不训练模型,它只负责把“如何优化”这件事拆成清晰、可组合的函数块,剩下的交给你的菜谱。

O=(init, update)O = (\text{init},\ \text{update})
优化器被抽象成“初始化函数 + 更新函数”的组合,init 准备锅具状态,update 加工梯度生成更新量。
  1. 优化器 O整个优化器的抽象表示,相当于一份完整的“厨具使用说明书”。
    O=(init, update)O = (\text{init},\ \text{update})
  2. init 初始化创建优化器的内部状态,比如动量缓冲区,相当于先把锅烧热、擦干净。
    init:params↦state\text{init}: \text{params} \mapsto \text{state}
  3. update 更新输入梯度、当前状态和参数,输出更新量与新状态,相当于一次翻炒并记录火候。
    update:(g, s, θ)↦(u, s′)\text{update}: (g,\ s,\ \theta) \mapsto (u,\ s')

基础优化器

厨房里最基础的锅是铁锅,对应深度学习里最简单的优化器——SGD(随机梯度下降)。它只看当前梯度,沿负梯度方向迈一步:损失大,就大步往回退;损失小,就小步微调。它的更新规则是:

铁锅虽然朴素,但颠勺全凭手感,训练容易震荡。于是有了带盖的深锅——Momentum,利用惯性让更新方向更平滑;再后来有了智能电压力锅——Adam,它给每个参数自适应学习率,对稀疏梯度更友好,收敛快且稳。在 Optax 里,三者的选择只是一行代码的差别:

选择哪个优化器,取决于菜的性质:数据量小、想要最可控的训练过程,SGD 是经典稳妥之选;追求快速收敛和稳定性,Adam 是默认起点。对你而言,掌握它们就像厨师熟悉自己的锅具——先会用最稳的,再换最顺手的。

wt+1=wt−η∇L(wt)w_{t+1} = w_t - \eta \nabla L(w_t)
这就是“沿最陡下坡方向走一步”的数学写法,η 控制步子大小。
  1. 当前参数 w_tt 时刻的模型权重,相当于锅里正在翻炒的食材。
    wt+1=wt−η∇L(wt)w_{t+1} = w_t - \eta \nabla L(w_t)
  2. 损失梯度 ∇L(w_t)损失对参数的偏导数,指示“调味该往哪个方向调”。
    ∇L(wt)=∂L∂wt\nabla L(w_t) = \frac{\partial L}{\partial w_t}
  3. 学习率 η每次调整的幅度,类似火候大小;火太大容易烧焦,火太小半天煮不熟。
    η∈(0,1)\eta \in (0, 1)
python3 行
optimizer = optax.sgd(0.1)optimizer = optax.sgd(0.1, momentum=0.9)optimizer = optax.adam(1e-3)

梯度变换

有了锅具,还要会调味。在训练中,“调味”就是对梯度做各种修正,Optax 把这些操作统称为梯度变换(GradientTransformation)。最常用的一味调料是梯度裁剪(gradient clipping):当一道菜突然咸过头——梯度出现爆炸般的巨大值——我们需要把它压回合理范围。

Optax 提供了按全局范数裁剪的 clip_by_global_norm,它的数学规则是:

除了裁剪,梯度变换还有中心化(把梯度减去均值)、加噪(促进探索)等“调料”。它们都不改变梯度的本质,只调整味道的浓淡和口感。了解了这一层,你就明白了 Optax 里几乎所有“梯度变换”都只是在梯度上做加加减减、缩放和截断而已。

g′=g×min⁡(1, δ∥g∥2)g' = g \times \min\left(1,\ \frac{\delta}{\|g\|_2}\right)
当全体梯度的 L2 范数超过阈值 δ 时,按比例缩小;没超过就原样保留。相当于厨师给调味瓶装上限位器。
  1. 原始梯度 g把所有参数的梯度拼接起来看成一个整体向量。
    g=[g1,g2,…,gn]g = [g_1, g_2, \dots, g_n]
  2. 全局 L2 范数 ||g||₂衡量全体梯度的整体长度,表示这次“调味下手有多重”。
    ∥g∥2=∑igi2\|g\|_2 = \sqrt{\sum_i g_i^2}
  3. 裁剪阈值 δ允许的最大范数,超过就会被压缩,像盐罐子最多只能撒那么多。
    min⁡(1, δ∥g∥2)\min\left(1,\ \frac{\delta}{\|g\|_2}\right)
  4. 缩放结果 g'整体按比例缩小到阈值以内,方向不变,只改幅度。
    g′=g×min⁡(1, δ∥g∥2)g' = g \times \min\left(1,\ \frac{\delta}{\|g\|_2}\right)

组合策略

真正让 Optax 像“点套餐”一样好用的,是 optax.chain。它允许你把多项梯度变换按顺序串起来,形成一条流水线:先裁剪梯度,再叠加权重衰减,然后用 Adam 加工,最后统一输出更新量。这就像厨师做一份套餐:先洗菜、再切菜、最后下锅炒,每步都清晰独立。

组合的数学本质是函数复合。假设我们有三个变换 T₁、T₂、T₃,那么 chain 把它们复合为一个新变换:

在 Optax 中,这种组合策略不仅让代码更整洁,还让你能精细控制训练的每一个环节。比如“多阶段训练”:先用预热 + 裁剪 + Adam 训练主干,再切换到低学习率微调头部分支。你可以把整个训练策略定义成一个变量,随时像更换套餐一样替换,而不需要改动训练循环的其余代码。

T=T3∘T2∘T1,g′=T3(T2(T1(g)))T = T_3 \circ T_2 \circ T_1,\qquad g' = T_3\big(T_2(T_1(g))\big)
多个变换从内到外依次作用在同一个梯度上:先归一化,再缩放,再更新状态。
  1. 变换 T₁第一个作用于梯度的操作,比如裁剪梯度范数。
    T1(g)=clip(g)T_1(g) = \text{clip}(g)
  2. 变换 T₂第二个操作,比如对参数施加权重衰减。
    T2(g)=g+λθT_2(g) = g + \lambda \theta
  3. 复合 T₃∘T₂∘T₁按从右到左的运算顺序,先 T₁ 后 T₂ 最后 T₃。
    g′=T3(T2(T1(g)))g' = T_3\big(T_2(T_1(g))\big)

参数更新

每道菜做好后,需要装盘上桌——梯度被加工成更新量后,要真正作用到模型参数上。Optax 把这个流程分为两步:opt.update 加工梯度得到“更新量”,optax.apply_updates 把更新量合并回参数。这就像厨师把调味好的酱汁浇到牛排上,酱汁和牛排都是独立的,但最终融为一体。

更新这一步可以写成:

在函数式设计下,update 不会修改传入的 opt_state,而是返回新状态。这意味着你可以保存旧状态、回滚训练进度,甚至在同一份参数上并排测试两个优化器的效果。这一切都让训练过程变得像做菜一样可观察、可调整——每一次翻炒你都知道锅里发生了什么。

θt+1=θt+ut,ut=update(gt, st)\theta_{t+1} = \theta_t + u_t,\qquad u_t = \text{update}(g_t,\ s_t)
更新量由优化器内部状态和当前梯度共同决定,最终以加法形式叠到参数上。
  1. 梯度 g_t 与状态 s_t状态里保存动量、二阶矩等信息,是优化器的“记性”。
    ut=update(gt, st)u_t = \text{update}(g_t,\ s_t)
  2. 新参数 θ_{t+1}旧参数加上更新量,完成一步训练。
    θt+1=θt+ut\theta_{t+1} = \theta_t + u_t
  3. 状态推进 s_t→s_{t+1}优化器状态也要更新,为下一步做准备。
    st+1=update_state(gt, st)s_{t+1} = \text{update\_state}(g_t,\ s_t)

学习率调度

火候,是厨艺的灵魂;学习率,是训练的灵魂。学习率太高,模型在最优解附近震荡;太低,训练慢得像炖汤。Optax 把学习率调度器也做成了梯度变换,让你能在训练过程中动态调整这份“火候”。

最流行的调度策略之一是余弦退火(cosine annealing):学习率从较高值开始,沿余弦曲线平滑地降到一个较低值。公式如下:

在实际使用中,还会配合“预热”(warmup):前几百步从很小的学习率线性升到 η_max,再开始余弦退火。这就像做菜时先小火预热锅再大火爆炒,最后小火收汁,每个阶段都刚刚好。Optax 的 scale_by_learning_rate 与调度器配合,让火候全程可控。

ηt=ηmin⁡+12(ηmax⁡−ηmin⁡)(1+cos⁡πtT)\eta_t = \eta_{\min} + \frac{1}{2}(\eta_{\max} - \eta_{\min})\left(1 + \cos\frac{\pi t}{T}\right)
训练步数 t 从 0 走到 T,学习率从 η_max 平滑过渡到 η_min,形成一条余弦式下降曲线。
  1. 总步数 T 与当前步 t确定退火周期,相当于设定一餐的烹饪时长。
    ηt=ηmin⁡+12(ηmax⁡−ηmin⁡)(1+cos⁡πtT)\eta_t = \eta_{\min} + \frac{1}{2}(\eta_{\max} - \eta_{\min})\left(1 + \cos\frac{\pi t}{T}\right)
  2. 最大值与最小值η_max 是初始学习率,η_min 是最终降到的最低学习率。
    ηmax⁡, ηmin⁡\eta_{\max},\ \eta_{\min}
  3. 余弦项随着 t/T 从 0 到 1,cos(πt/T) 从 1 平滑降到 -1,整体从 η_max 单调降到 η_min。
    cos⁡πtT∈[−1, 1]\cos\frac{\pi t}{T} \in [-1,\ 1]

Flax 集成

厨房有了全套厨具和调料,还差一位配合默契的主厨——Flax。Flax 是 JAX 生态的神经网络库,负责搭建模型结构并管理参数。它与 Optax 的分工很明确:Flax 定义模型如何前向计算,Optax 决定梯度如何更新参数,二者通过 TrainState 这个“操作台”衔接。

TrainState 把参数、优化器状态和应用梯度的逻辑打包在一起。每次训练,你要做的就是:算损失和梯度,调用 state.apply_gradients 获取新状态。这一动作写作:

典型的 Flax 训练循环里,模型前向、损失计算、梯度计算由你写,优化器和参数更新交给 Optax 与 TrainState,整个循环简洁清晰。至此,从选厨具、调味、组合套餐到端上桌,一整套深度学习训练范式的核心你都掌握了。

params′=apply_updates(params, updates)\text{params}' = \text{apply\_updates}(\text{params},\ \text{updates})
Flax 的训练状态对象封装了参数与优化器状态,一行代码完成“装盘”。
  1. updates 更新量来自 optimizer.update(grads, opt_state, params),是加工好的酱汁。
    u=update(g, s, θ)u = \text{update}(g,\ s,\ \theta)
  2. apply_updates 应用把更新量逐层加到参数树的对应叶子上。
    θ′=apply_updates(θ, u)\theta' = \text{apply\_updates}(\theta,\ u)
  3. 新状态推进TrainState 同时保存新参数与新优化器状态,供下一步使用。
    state′=state.replace(params=θ′, opt_state=s′)\text{state}' = \text{state.replace}(\text{params}=\theta',\ \text{opt\_state}=s')

AI 应用场景

大语言模型微调

阶段:模型训练与微调

解决的问题:大模型参数量巨大,直接使用固定学习率的 Adam 容易在早期产生损失震荡、后期收敛缓慢。传统做法需要反复手工调整学习率和正则项,成本高且难复现。

具体案例:某团队在微调 7B 参数大模型时,使用 optax.chain(optax.clip_by_global_norm(1.0), optax.adamw(learning_rate)),并搭配预热 + 余弦退火调度器。梯度裁剪防止了早期梯度爆炸,权重衰减避免了过拟合,最终在相同算力下把训练时间缩短了 20%。

强化学习策略训练

阶段:强化学习策略优化

解决的问题:PPO 等算法需要同时更新策略网络与价值网络,且每步梯度噪声大,直接用 SGD 容易使策略崩坏。传统写法需要分别管理多套优化器,代码繁琐。

具体案例:在机器人控制任务中,工程师用 Optax 分别为 actor 和 critic 创建优化器,并通过 optax.multi_transform 对两类参数施加不同学习率与梯度裁剪,训练稳定性和最终奖励均有明显提升。

迁移学习与冻结训练

阶段:迁移学习

解决的问题:加载预训练模型做下游任务时,通常希望主干网络小幅微调、新增头部快速学习。若所有层用同一学习率,要么新头学得太慢,要么主干被破坏。

具体案例:在图像分类迁移任务中,研究员用 optax.masked 配合参数分组,自定义 param_labels 让主干学习率为 1e-5、头部为 1e-3。训练期间主干保持稳定,头部快速收敛,整个迁移流程在一次训练内完成。

Python 演示

python41 行
import jaximport jax.numpy as jnpimport optaxfrom flax import linen as nn class MLP(nn.Module):    hidden: int = 16     @nn.compact    def __call__(self, x):        x = nn.Dense(self.hidden)(x)        x = nn.relu(x)        return nn.Dense(1)(x) def loss_fn(params, x, y, model):    pred = model.apply(params, x)    return jnp.mean((pred - y) ** 2) def main():    key = jax.random.PRNGKey(0)    x = jax.random.normal(key, (128, 2))    y = (2.0 * x[:, 0] - 1.5 * x[:, 1]).reshape(-1, 1)     model = MLP()    params = model.init(key, x)     optimizer = optax.chain(        optax.clip_by_global_norm(1.0),        optax.adamw(learning_rate=1e-2, weight_decay=1e-4)    )    opt_state = optimizer.init(params)     for step in range(60):        loss, grads = jax.value_and_grad(loss_fn)(params, x, y, model)        updates, opt_state = optimizer.update(grads, opt_state, params)        params = optax.apply_updates(params, updates)        if step % 20 == 0:            print(step, float(loss)) if __name__ == "__main__":    main()
目的:演示 Optax 的完整训练链路,用 chain 组合梯度裁剪与 AdamW,再通过 update 和 apply_updates 完成参数更新。
  1. optimizer = optax.chain(...)用链式组合把梯度裁剪和 AdamW 串成一条“套餐式”优化流水线。
  2. optax.clip_by_global_norm(1.0)全局梯度裁剪,防止梯度爆炸把参数推向离群值。
  3. optax.adamw(...)AdamW 优化器,在 Adam 基础上加入解耦的权重衰减。
  4. opt_state = optimizer.init(params)初始化优化器状态,相当于把锅具和调料备好。
  5. jax.value_and_grad(loss_fn)(...)同时返回损失和梯度,是 JAX 最典型的微分调用。
  6. optimizer.update(grads, opt_state, params)让优化器加工梯度,返回更新量和新状态。
  7. optax.apply_updates(params, updates)把加工好的更新量合并进参数,完成一步训练。

常见误区

误区我把学习率调大一点,模型不就每步走得更远、收敛更快了吗?所以训练直接设 0.5。

正解学习率过大时,参数更新会跨过最优点来回震荡,甚至发散,就像火太大把菜烧焦。测试集上常见现象是损失先降后升。应该用小学习率配合调度器,先用预热提升到安全区间,再用余弦退火平稳收敛。

为何常见:初学阶段容易把“梯度方向”与“步长合理”混为一谈,直觉上以为走快一点总能到终点。

误区Adam 自带自适应学习率、又能处理稀疏梯度,所以我设了初始学习率之后就完全不管了。

正解Adam 的“自适应”指的是每个参数各自缩放更新量,但初始学习率依然决定整体尺度。大模型实践中几乎总是搭配预热和余弦退火,才能既避免前期不稳定、又在后期细腻收尾。Optax 里调度器就是梯度变换,用 chain 接上即可。

为何常见:对比 SGD 时,Adam 的表现确实“省心”,让人误以为它的学习率不需要任何管理。

误区都是梯度裁剪,用哪个都一样,所以我随便选一个。

正解clip_by_norm 是逐张量独立裁剪,每个参数的梯度各自缩到阈值内;clip_by_global_norm 先算所有梯度拼接的全局 L2 范数,再统一缩放。对深层网络来说,全局裁剪能更真实地反映整体梯度爆炸,实践中最常用的是后者。

为何常见:两者函数名只差一个 "global",API 外观高度相似,不读文档很难察觉行为差异。

误区我在损失里加上 L2 正则项,效果不就等于权重衰减吗?再用 Adam 也没问题。

正解Adam 会对每个梯度按一阶、二阶矩做自适应缩放,直接加的 L2 梯度会被这个缩放破坏,与真正的解耦权重衰减不等价。所以 Optax 提供 optax.adamw,把权重衰减单独应用在参数上,效果稳定且超参更易调。

为何常见:从 SGD 时代沿用的惯性思维——在 SGD 中 L2 正则与权重衰减确实等价,换到自适应优化器时这条等价性就失效了。

面试题

Q1. Optax 中 optax.adam(1e-3) 的参数 1e-3 表示什么?

单选题 · easy

  • A.动量衰减系数
  • B.初始学习率← 正确答案
  • C.一阶矩估计
  • D.二阶矩估计

答案:B。1e-3 是 Adam 的初始学习率,控制整体更新步长。动量衰减、一阶矩、二阶矩都是 Adam 内部维护的系数或缓冲区,不由这个位置参数决定。

Q2. 在 Optax 中,把优化器产生的更新量合入模型参数,需要调用?

单选题 · easy

  • A.optax.update_grads()
  • B.optax.apply_grads()
  • C.optax.apply_updates()← 正确答案
  • D.optax.adam_update()

答案:C。optax.apply_updates 负责把更新量逐叶加到参数树上,是训练循环的“装盘”环节;其余名字不存在于标准 Optax API 或语义不匹配。

Q3. 要把梯度裁剪和 Adam 组合成一条优化流水线,Optax 推荐用哪个函数?

单选题 · easy

  • A.optax.chain← 正确答案
  • B.optax.concat
  • C.optax.merge
  • D.optax.bind

答案:A。optax.chain 按顺序复合多个梯度变换,返回一个新的变换对象,是 Optax 组合策略的核心函数。concat/merge/bind 均不是 Optax 的官方组合函数。

Q4. 关于 optax.clip_by_global_norm(delta) 裁剪的范数,正确的是?

单选题 · mid

  • A.每张量各自的 L2 范数
  • B.全部梯度总和的 L2 范数← 正确答案
  • C.每张量各自的 L1 范数
  • D.全部梯度总和的 L1 范数

答案:B。它把所有参数的梯度拼接成一个大向量,计算全局 L2 范数后统一缩放;选项 A 是 clip_by_norm 的行为,C/D 使用了错误的范数类型。

Q5. 关于 SGD 与 Adam 的区别,正确的说法有?

多选题 · mid

  • A.Adam 含有自适应学习率← 正确答案
  • B.SGD 默认不保存动量← 正确答案
  • C.两者更新公式不同← 正确答案
  • D.Adam 永远优于 SGD

答案:ABC。Adam 为每个参数维护一阶、二阶矩实现自适应缩放;SGD 不带 momentum 时只沿当前梯度方向更新。二者各有适用场景,D 的“永远优于”过于绝对,故错误。

Q6. Flax 中的 TrainState.apply_gradients 核心作用是什么?

单选题 · mid

  • A.计算训练损失
  • B.合并梯度与参数← 正确答案
  • C.初始化模型
  • D.记录训练日志

答案:B。apply_gradients 会用给定梯度和优化器状态算出更新量,再通过 optax.apply_updates 合入参数,返回携带新参数与新状态的新 TrainState,相当于完成一次“装盘”。

Q7. 关于学习率调度说法正确的有?

多选题 · mid

  • A.预热期学习率由小变大← 正确答案
  • B.余弦退火学习率先大后小← 正确答案
  • C.调度是一种梯度变换← 正确答案
  • D.调度总是降低模型效果

答案:ABC。预热从微小学习率逐步升到目标值,余弦退火从大值平滑降到小值;Optax 中调度器通过 scale_by_learning_rate 实现,本质就是梯度变换。D 过于极端,合理调度通常能提升最终效果。

Q8. 在 Adam 类优化器中加入权重衰减,推荐使用的做法是?

单选题 · hard

  • A.直接添加到梯度中
  • B.解耦的权重衰减← 正确答案
  • C.提高学习率替代
  • D.加到动量之上

答案:B。直接加 L2 梯度会被 Adam 的自适应缩放破坏,正确做法是 optax.adamw 的解耦权重衰减,将其独立施加到参数更新上,这是 AdamW 与普通 Adam + L2 的本质区别。

Q9. 训练过程中出现梯度爆炸,哪些处理手段是有效的?

多选题 · hard

  • A.使用全局梯度裁剪← 正确答案
  • B.调大初始学习率
  • C.加入权重衰减正则← 正确答案
  • D.删除所有归一化层

答案:AC。全局梯度裁剪将整体梯度压回安全范围;权重衰减限制参数幅度、间接缓和爆炸。调大学习率会加剧爆炸,删除归一化层通常破坏训练稳定性,均不可取。

Q10. 一个模型包含主干与任务头两部分,需要为它们设置不同学习率。Optax 中最合适的做法是?

单选题 · extreme

  • A.创建两个独立优化器
  • B.借助 mask 分组变换← 正确答案
  • C.所有层统一学习率
  • D.冻结整个网络训练

答案:B。optax.multi_transform 配合 param_labels(或 optax.masked)可以按参数名/结构为不同子网施加不同学习率,仍保留单一优化器状态便于管理。两个独立优化器需手动同步与分别更新,C/D 不做区分或不做训练,均不符合需求。