想象你是一位厨师,眼前摆着一整套专业厨房:各种锅具、刀具、佐料架和温度控制器。训练深度学习模型,就像为一道菜反复试味并微调配方;而 Optax 正是 JAX 生态里这间“可编程厨房”——它把从简单到复杂的所有优化器(SGD、Adam 等)都做成一个个独立“厨具”,把梯度裁剪、权重衰减、学习率调度等操作包装成“调味步骤”。你不需要自己焊灶台,只需要像点套餐一样,用 chain 把需要的厨具和调味串起来。JAX 的哲学是函数式:厨具不改变锅里的菜本身,而是返回一份“新菜谱”让你去执行。这篇文章会用这间厨房的比喻,带你认识 Optax 的定位、常用优化器、梯度变换、组合策略、参数更新、学习率调度,以及与 Flax 框架的配合方式。学完以后,复杂的训练流程也能被你拆解成一行行清晰的“菜谱”。
对 AI 从业者而言,Optax 是 JAX/Flax 生态中最常用的训练调参工具箱。掌握它,你能把晦涩的优化器配置变成可读可组合的代码,快速尝试新研究思路,显著提升实验迭代效率。
可视化演示
本节配有交互动效(进入视口后自动加载,约 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 更新输入梯度、当前状态和参数,输出更新量与新状态,相当于一次翻炒并记录火候。
基础优化器
厨房里最基础的锅是铁锅,对应深度学习里最简单的优化器——SGD(随机梯度下降)。它只看当前梯度,沿负梯度方向迈一步:损失大,就大步往回退;损失小,就小步微调。它的更新规则是:
铁锅虽然朴素,但颠勺全凭手感,训练容易震荡。于是有了带盖的深锅——Momentum,利用惯性让更新方向更平滑;再后来有了智能电压力锅——Adam,它给每个参数自适应学习率,对稀疏梯度更友好,收敛快且稳。在 Optax 里,三者的选择只是一行代码的差别:
选择哪个优化器,取决于菜的性质:数据量小、想要最可控的训练过程,SGD 是经典稳妥之选;追求快速收敛和稳定性,Adam 是默认起点。对你而言,掌握它们就像厨师熟悉自己的锅具——先会用最稳的,再换最顺手的。
- 当前参数 w_tt 时刻的模型权重,相当于锅里正在翻炒的食材。
- 损失梯度 ∇L(w_t)损失对参数的偏导数,指示“调味该往哪个方向调”。
- 学习率 η每次调整的幅度,类似火候大小;火太大容易烧焦,火太小半天煮不熟。
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把所有参数的梯度拼接起来看成一个整体向量。
- 全局 L2 范数 ||g||₂衡量全体梯度的整体长度,表示这次“调味下手有多重”。
- 裁剪阈值 δ允许的最大范数,超过就会被压缩,像盐罐子最多只能撒那么多。
- 缩放结果 g'整体按比例缩小到阈值以内,方向不变,只改幅度。
组合策略
真正让 Optax 像“点套餐”一样好用的,是 optax.chain。它允许你把多项梯度变换按顺序串起来,形成一条流水线:先裁剪梯度,再叠加权重衰减,然后用 Adam 加工,最后统一输出更新量。这就像厨师做一份套餐:先洗菜、再切菜、最后下锅炒,每步都清晰独立。
组合的数学本质是函数复合。假设我们有三个变换 T₁、T₂、T₃,那么 chain 把它们复合为一个新变换:
在 Optax 中,这种组合策略不仅让代码更整洁,还让你能精细控制训练的每一个环节。比如“多阶段训练”:先用预热 + 裁剪 + Adam 训练主干,再切换到低学习率微调头部分支。你可以把整个训练策略定义成一个变量,随时像更换套餐一样替换,而不需要改动训练循环的其余代码。
- 变换 T₁第一个作用于梯度的操作,比如裁剪梯度范数。
- 变换 T₂第二个操作,比如对参数施加权重衰减。
- 复合 T₃∘T₂∘T₁按从右到左的运算顺序,先 T₁ 后 T₂ 最后 T₃。
参数更新
每道菜做好后,需要装盘上桌——梯度被加工成更新量后,要真正作用到模型参数上。Optax 把这个流程分为两步:opt.update 加工梯度得到“更新量”,optax.apply_updates 把更新量合并回参数。这就像厨师把调味好的酱汁浇到牛排上,酱汁和牛排都是独立的,但最终融为一体。
更新这一步可以写成:
在函数式设计下,update 不会修改传入的 opt_state,而是返回新状态。这意味着你可以保存旧状态、回滚训练进度,甚至在同一份参数上并排测试两个优化器的效果。这一切都让训练过程变得像做菜一样可观察、可调整——每一次翻炒你都知道锅里发生了什么。
- 梯度 g_t 与状态 s_t状态里保存动量、二阶矩等信息,是优化器的“记性”。
- 新参数 θ_{t+1}旧参数加上更新量,完成一步训练。
- 状态推进 s_t→s_{t+1}优化器状态也要更新,为下一步做准备。
学习率调度
火候,是厨艺的灵魂;学习率,是训练的灵魂。学习率太高,模型在最优解附近震荡;太低,训练慢得像炖汤。Optax 把学习率调度器也做成了梯度变换,让你能在训练过程中动态调整这份“火候”。
最流行的调度策略之一是余弦退火(cosine annealing):学习率从较高值开始,沿余弦曲线平滑地降到一个较低值。公式如下:
在实际使用中,还会配合“预热”(warmup):前几百步从很小的学习率线性升到 η_max,再开始余弦退火。这就像做菜时先小火预热锅再大火爆炒,最后小火收汁,每个阶段都刚刚好。Optax 的 scale_by_learning_rate 与调度器配合,让火候全程可控。
- 总步数 T 与当前步 t确定退火周期,相当于设定一餐的烹饪时长。
- 最大值与最小值η_max 是初始学习率,η_min 是最终降到的最低学习率。
- 余弦项随着 t/T 从 0 到 1,cos(πt/T) 从 1 平滑降到 -1,整体从 η_max 单调降到 η_min。
Flax 集成
厨房有了全套厨具和调料,还差一位配合默契的主厨——Flax。Flax 是 JAX 生态的神经网络库,负责搭建模型结构并管理参数。它与 Optax 的分工很明确:Flax 定义模型如何前向计算,Optax 决定梯度如何更新参数,二者通过 TrainState 这个“操作台”衔接。
TrainState 把参数、优化器状态和应用梯度的逻辑打包在一起。每次训练,你要做的就是:算损失和梯度,调用 state.apply_gradients 获取新状态。这一动作写作:
典型的 Flax 训练循环里,模型前向、损失计算、梯度计算由你写,优化器和参数更新交给 Optax 与 TrainState,整个循环简洁清晰。至此,从选厨具、调味、组合套餐到端上桌,一整套深度学习训练范式的核心你都掌握了。
- updates 更新量来自
optimizer.update(grads, opt_state, params),是加工好的酱汁。 - apply_updates 应用把更新量逐层加到参数树的对应叶子上。
- 新状态推进TrainState 同时保存新参数与新优化器状态,供下一步使用。
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 演示
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()
optimizer = optax.chain(...)用链式组合把梯度裁剪和 AdamW 串成一条“套餐式”优化流水线。optax.clip_by_global_norm(1.0)全局梯度裁剪,防止梯度爆炸把参数推向离群值。optax.adamw(...)AdamW 优化器,在 Adam 基础上加入解耦的权重衰减。opt_state = optimizer.init(params)初始化优化器状态,相当于把锅具和调料备好。jax.value_and_grad(loss_fn)(...)同时返回损失和梯度,是 JAX 最典型的微分调用。optimizer.update(grads, opt_state, params)让优化器加工梯度,返回更新量和新状态。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 表示什么?
- A.动量衰减系数
- B.初始学习率← 正确答案
- C.一阶矩估计
- D.二阶矩估计
答案:B。1e-3 是 Adam 的初始学习率,控制整体更新步长。动量衰减、一阶矩、二阶矩都是 Adam 内部维护的系数或缓冲区,不由这个位置参数决定。
Q2. 在 Optax 中,把优化器产生的更新量合入模型参数,需要调用?
- 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 推荐用哪个函数?
- 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) 裁剪的范数,正确的是?
- A.每张量各自的 L2 范数
- B.全部梯度总和的 L2 范数← 正确答案
- C.每张量各自的 L1 范数
- D.全部梯度总和的 L1 范数
答案:B。它把所有参数的梯度拼接成一个大向量,计算全局 L2 范数后统一缩放;选项 A 是 clip_by_norm 的行为,C/D 使用了错误的范数类型。
Q5. 关于 SGD 与 Adam 的区别,正确的说法有?
- A.Adam 含有自适应学习率← 正确答案
- B.SGD 默认不保存动量← 正确答案
- C.两者更新公式不同← 正确答案
- D.Adam 永远优于 SGD
答案:ABC。Adam 为每个参数维护一阶、二阶矩实现自适应缩放;SGD 不带 momentum 时只沿当前梯度方向更新。二者各有适用场景,D 的“永远优于”过于绝对,故错误。
Q6. Flax 中的 TrainState.apply_gradients 核心作用是什么?
- A.计算训练损失
- B.合并梯度与参数← 正确答案
- C.初始化模型
- D.记录训练日志
答案:B。apply_gradients 会用给定梯度和优化器状态算出更新量,再通过 optax.apply_updates 合入参数,返回携带新参数与新状态的新 TrainState,相当于完成一次“装盘”。
Q7. 关于学习率调度说法正确的有?
- A.预热期学习率由小变大← 正确答案
- B.余弦退火学习率先大后小← 正确答案
- C.调度是一种梯度变换← 正确答案
- D.调度总是降低模型效果
答案:ABC。预热从微小学习率逐步升到目标值,余弦退火从大值平滑降到小值;Optax 中调度器通过 scale_by_learning_rate 实现,本质就是梯度变换。D 过于极端,合理调度通常能提升最终效果。
Q8. 在 Adam 类优化器中加入权重衰减,推荐使用的做法是?
- A.直接添加到梯度中
- B.解耦的权重衰减← 正确答案
- C.提高学习率替代
- D.加到动量之上
答案:B。直接加 L2 梯度会被 Adam 的自适应缩放破坏,正确做法是 optax.adamw 的解耦权重衰减,将其独立施加到参数更新上,这是 AdamW 与普通 Adam + L2 的本质区别。
Q9. 训练过程中出现梯度爆炸,哪些处理手段是有效的?
- A.使用全局梯度裁剪← 正确答案
- B.调大初始学习率
- C.加入权重衰减正则← 正确答案
- D.删除所有归一化层
答案:AC。全局梯度裁剪将整体梯度压回安全范围;权重衰减限制参数幅度、间接缓和爆炸。调大学习率会加剧爆炸,删除归一化层通常破坏训练稳定性,均不可取。
Q10. 一个模型包含主干与任务头两部分,需要为它们设置不同学习率。Optax 中最合适的做法是?
- A.创建两个独立优化器
- B.借助 mask 分组变换← 正确答案
- C.所有层统一学习率
- D.冻结整个网络训练
答案:B。optax.multi_transform 配合 param_labels(或 optax.masked)可以按参数名/结构为不同子网施加不同学习率,仍保留单一优化器状态便于管理。两个独立优化器需手动同步与分别更新,C/D 不做区分或不做训练,均不符合需求。