ELAI S.r.l.

梯度检查失败:模型真的出错了吗?

从正弦到 ReLU:推导有限差分、测量步长影响,并解释检查为何可能比较不同的量。

梯度检查失败:模型真的出错了吗?

检查器也需要被检查

开发 AI 模型时,我们常把程序计算的导数与微调参数后的数值估计比较。不一致容易让人立即怀疑反向传播,但数值检查本身也有前提和限制:步长过小反而可能变差,折点处可能根本不存在可比较的导数。如何区分有价值的警报与误报?

我们使用两个标量函数:光滑且导数已知的正弦,以及神经网络中常见的分段函数 ReLU。不训练网络,也不执行自动微分库。实验用于展示梯度检查的前提。量均为无量纲,正弦输入 0.7 表示弧度。文中全部数值结果来自归档代码。

局部斜率,而不只是差值

导数描述输入变化趋于零时,输出变化与输入变化的比值,图形上对应切线斜率。对于标量函数 f,在 x 处的前向比值使用 f(x+h) 和 f(x),中心比值使用 x 两侧的点。计算步长 h 必须非零。中心比值不是非光滑点导数的定义,只是在适当条件下与导数一致的估计量。

D_forward(h) = [f(x+h) - f(x)] / h D_central(h) = [f(x+h) - f(x-h)] / (2h)

为避免只依赖图形,可以在 x 附近展开 f。若有足够光滑的导数,f(x+h)=f(x)+hf′(x)+h²f″(x)/2+h³f‴(x)/6+… 。前向比值的主误差与 h 成正比。减去 −h 处的展开后,偶次项抵消,中心比值主误差为 h²f‴(x)/6。这解释了更高的理论阶数,却不是对任意程序精度的保证。

为什么减小 h 并不够

上述推导假设精确运算。计算机求 f 时会舍入,还要把几乎相等的值相减。差值中接近机器精度的绝对不确定性又被 h 除。在简化的局部误差模型中,中心差分误差包含 C₁h² 和 C₂u/h,u 描述相对精度,常数依赖函数和尺度。减小 h 会降低第一项,却可能增大第二项。

平衡两项量级,在考虑问题尺度后,可得到此中心差分模型中 h 与 u^(1/3) 成比例的启示。这不是所有网络都可照搬的通用值;噪声、舍入、参数大小及接近零的项都会改变情况。实践重点是观察一系列步长,寻找一致区间,而不是把“更小”等同于“更可靠”。

正弦:实际执行的光滑案例

在 x=0.7 处计算 f(x)=sin(x),步长从 10⁻¹ 到 10⁻¹⁶。参考值 math.cos(0.7) 约为 0.7648421872844885,是解析导数的双精度求值,不是任意精度真值。本次扫描中,中心误差最小出现在 h=10⁻⁵,约为 1.70×10⁻¹¹。h=10⁻¹⁶ 时,估计值却约为 1.1102230246251565,误差约 0.34538。此时 x+h、x−h 仍不等于 x,因此检查很差并不要求输入点已经完全重合。

复步长:强大,但有条件

若函数能适当地解析延拓到复数域,可以采用虚数步长 ih。展开为 f(x)+ihf′(x)−h²f″(x)/2−ih³f‴(x)/6+… 。取虚部再除以 h,可得到理论误差为 h² 阶的 f′(x),无需相减两个相近实数。i 满足 i²=−1,Im 表示虚部。程序接受 complex 类型并不等于该方法有效。

D_complex(h) = Im[f(x + i*h)] / h

对 cmath.sin 计算的正弦,h=10⁻¹⁶ 时与参考值之差约为 1.11×10⁻¹⁶。这说明该函数上的效果很好,并不证明普遍不存在舍入。abs 提供反例:x=0.7 时 |x| 的实导数为 1,但 abs(x+ih) 返回实数模长,丢失虚部信息。同样的配方将返回零,代码已验证。因此使用前必须检查分支、比较、实数转换及非解析操作。

ReLU:0.5 不是导数的时候

ReLU 是 Rectified Linear Unit,定义 r(x)=max(0, x)。x 为负时输出和斜率均为零,x 为正时输出为 x、斜率为一。在零点,左右斜率极限不同,普通导数不存在。然而任意正 h 的中心比值均为 (h−0)/(2h)=1/2。缩小步长不会使它趋于零或一,因为它混合了两个分支。

h左侧右侧中心
1e-1010.5
1e-3010.5
1e-6010.5

非光滑优化可采用次梯度约定。对于这个凸函数,零点次梯度集合为 [0,1]。选择零是一种算法约定,不是在宣称普通导数存在且等于零。将这种约定与中心差分比较,可能得到 0 对 0.5,却不能仅据此证明程序错误。本文未执行 autograd,也不把这一结果归于任何框架版本。

左:各实际步长下与 math.cos(0.7) 的差。横轴递减,纵轴采用对称对数坐标,±10⁻¹⁷ 内为线性区以显示零。右:ReLU 零点处的斜率,此处普通导数不存在。
左:各实际步长下与 math.cos(0.7) 的差。横轴递减,纵轴采用对称对数坐标,±10⁻¹⁷ 内为线性区以显示零。右:ReLU 零点处的斜率,此处普通导数不存在。

从标量检查到多参数函数

对具有 d 个参数的标量损失 L(theta),逐坐标中心差分每个参数需要两次求值,总计约 2d 次损失求值,另加待检查梯度。方向检查则比较 [L(theta+hv)−L(theta−hv)]/(2h) 与 g·v,g 为待检梯度,v 为选定方向。每个方向更便宜,但与 v 正交的误差可能漏检。这里未实际执行多维检查,仅给出数学扩展,不编造网络结果。

求值的函数本身也必须一致:随机性、调用间状态更新或不同数据,都可能把函数差异伪装成导数错误。少数点的数值一致只是局部证据,不能证明整个模型正确。靠近分支切换时,应检查扰动是否跨越边界;在其他位置,若多个步长下仍稳定不一致,就仍是值得调查的重要线索。结论是理解测试对象,而不是忽略测试。

对 AI 开发有用的答案

梯度检查失败本身不能确定责任方。正弦案例中步长过小导致误差增大;ReLU 案例在没有导数的点得到 0.5。两者原因不同,都说明不能把估计量当成绝对真值。修改模型前,应明确函数、光滑性、步长、精度及比较约定。检查由此成为有适用范围的科学工具,而不是不透明裁决。

来源与复现

已阅读 SciPy 1.18.0 least_squares 文档中 jac 参数对复步长的条件说明;未执行 SciPy 或其中优化示例。自有代码使用 Python 3.14.0 的 math、cmath,无随机过程,保存全部十六个步长、左右差分及 abs 反例。公式在本文推导,不是论文或 EL-AI 产品的实验成果。图由 Matplotlib 计算生成,不声称硬件性能或人工审核。

SciPy 1.18.0 — least_squares, jac.

Python 3.14 — math.

Python 3.14 — cmath.

import math
x=0.7
reference=math.cos(x)
for h in [1e-3,1e-5,1e-16]:
    estimate=(math.sin(x+h)-math.sin(x-h))/(2*h)
    print(h,estimate,abs(estimate-reference))
relu=lambda z:max(0.0,z)
h=1e-6
assert (relu(h)-relu(-h))/(2*h)==0.5

代码、数据与说明 · JSON. 教学计算使用 Python 3.14.0,图使用 Matplotlib 3.11.2。分析由 AI 辅助,不声称经过同行评审或人工审核。原创 ImageGen 封面仅作示意,不记录 EL-AI 人员、场所或实际安装。来源查阅于 2026 年 10 月 21 日。