
本文介绍在使用 optax 的 scale_by_lbfgs 时,因 1.0 / vdot_diff_params_updates 在分母极小(非零但接近零)时产生 NaN 的根本原因,并提供基于 JAX 梯度安全性的双层 jnp.where 修复方案,兼顾数值鲁棒性与反向传播正确性。
本文介绍在使用 optax 的 `scale_by_lbfgs` 时,因 `1.0 / vdot_diff_params_updates` 在分母极小(非零但接近零)时产生 nan 的根本原因,并提供基于 jax 梯度安全性的双层 `jnp.where` 修复方案,兼顾数值鲁棒性与反向传播正确性。
在基于 JAX 的优化实践中,optax.scale_by_lbfgs 是实现内存高效拟牛顿法的关键组件。其内部通过计算参数差与更新方向的内积 vdot_diff_params_updates 来动态缩放梯度,核心逻辑包含如下易出错片段:
weight = jnp.where(
vdot_diff_params_updates == 0.0, 0.0, 1.0 / vdot_diff_params_updates
)
该写法看似合理——当内积为零时返回 0,否则取倒数。但问题在于:JAX 的 jnp.where 在反向传播中仍会对所有分支求导。即使 vdot_diff_params_updates 非零,1.0 / vdot_diff_params_updates 的梯度表达式 −1.0 / (x²) 在 x 极小时会溢出(如 x ≈ 1e−16 → x² ≈ 1e−32 → 倒数达 1e32),进而触发浮点上溢、次正规数下溢或梯度 NaN。
标准解法是采用 “双层掩码”(double-where)策略,确保梯度计算路径中永不接触病态除法:
mask = vdot_diff_params_updates == 0.0 # 第一层 where:为分母提供安全兜底值(非零) safe_denom = jnp.where(mask, 1.0, vdot_diff_params_updates) # 第二层 where:仅在有效区域应用倒数,无效区域直接设 0 weight = jnp.where(mask, 0.0, 1.0 / safe_denom)
此方案满足两个关键约束:
✅ 前向一致性:结果与原逻辑完全等价(mask 为真时输出 0.0,否则输出 1/x);
✅ 梯度安全性:反向传播中,1.0 / safe_denom 的输入 safe_denom 永远不为零,且 mask 分支的梯度被 jnp.where 正确屏蔽,避免未定义操作污染梯度流。
⚠️ 注意事项:
- 不要简单用
jnp.clip(vdot_diff_params_updates, 1e−8, None)替代——这会扭曲 LBFGS 的曲率估计,破坏 Hessian 近似精度;- 也不建议全局梯度裁剪(
optax.clip_by_global_norm)来掩盖该问题,因其无法根治反向传播中的数值奇点;- 若问题持续出现,建议检查上游梯度是否已含 NaN(可用
jax.debug_nans(True)启用自动检测);- 对高维稀疏参数场景,可进一步结合
jnp.finfo(dtype).tiny动态设定阈值,提升泛化性。
综上,双层 jnp.where 不仅是修复 NaN 的最小侵入式方案,更体现了 JAX 函数式微分对控制流语义的严格要求——数值稳定性和梯度正确性必须同步保障,而非仅关注前向输出。










