数值微分 · 有限差分,以及它为什么不稳
用函数值近似导数,靠有限差分。但它藏着一个尴尬的矛盾:步长 h 越小,截断误差越小,可 h 太小又会引发灾难性抵消——存在一个最优 h,再小反而更差。这一章用「U 形误差曲线」把这个「天生不稳」讲透。
数值微分 · 有限差分,以及它为什么不稳
第五站进入微积分的数值版。先看导数:很多时候我们只有 的函数值(甚至 是一段仿真代码,没有解析导数),却需要它的导数 。怎么办?用有限差分。但这一章的主角,是数值微分一个尴尬的性质——它天生不稳。
有限差分:用差商近似导数
导数的定义是 。计算机取不了极限,就取一个小而非零的 :
更准的是中心差分,用对称的两点,误差降一阶:
由泰勒展开可证:前向差分的截断误差正比于 ,中心差分正比于 。所以 越小,截断误差越小——听起来该把 取得越小越好?
陷阱:h 太小,抵消登场
不能。 这里 越小越好的直觉,撞上了灾难性抵消:
当 很小时, 和 几乎相等,它们的差 正是「两个相近的数相减」——灾难性抵消! 有效数字大量湮灭,而这个已经不准的差还要除以一个极小的 (放大误差)。于是 太小时,抵消误差(舍入)反而主导,结果越来越烂。
两股误差此消彼长: 大 → 截断误差大(近似太粗); 小 → 抵消误差大(舍入被放大)。中间存在一个最优 ,让两者之和最小——再小,误差不降反升。这就是数值微分「天生不稳」的根源:它同时踩了截断和抵消两个坑。
眼见为实:U 形误差曲线
用中心差分算 在 的导数(真值 ),扫一遍 :
import numpy as np
f = lambda x: np.sin(x)
true = np.cos(1.0)
for h in [1e-1, 1e-2, 1e-4, 1e-6, 1e-8, 1e-10, 1e-13]:
approx = (f(1 + h) - f(1 - h)) / (2 * h) # 中心差分
print(f"h={h:.0e} 误差={abs(approx - true):.2e}")h=1e-01 误差=1.67e-03 ┐
h=1e-02 误差=1.67e-05 │ h 变小,截断误差主导 → 误差下降
h=1e-04 误差=1.67e-09 │
h=1e-06 误差=5.26e-11 ┘ 最优点附近(≈ 最小)
h=1e-08 误差=6.08e-09 ┐
h=1e-10 误差=2.94e-07 │ h 再小,抵消误差主导 → 误差回升!
h=1e-13 误差=7.36e-04 ┘误差先降后升,画出来是一条 U 形曲线。 最优 在 附近(中心差分约 ),此时误差最小;再往小取,舍入抵消占上风,越取越差。「 取得越小导数越准」是彻头彻尾的错觉。
解药:绕开减法
既然病根是「减法抵消」,自然想到抵消那章的思路——绕开减法。有两个漂亮的办法:
- 复步长微分(complex-step):取虚步长,。它根本不做减法(信息藏在虚部),所以没有抵消, 可以取到 都不失精度!前提是 能接受复数输入。
- 自动微分(automatic differentiation,AD):不做任何近似,靠链式法则精确算出导数,误差只有舍入级。这是现代深度学习框架(PyTorch、JAX)反向传播的底层——它让「求导」从「危险的数值近似」变成了「精确的机械计算」。
# 复步长微分:无抵消,精度惊人
def complex_step_diff(f, x, h=1e-20):
return (f(x + 1j*h)).imag / h
print(complex_step_diff(np.sin, 1.0)) # 0.5403023058681398 ≈ cos(1),几乎机器精度小结与下一章
数值微分用有限差分(前向 、中心 ),但它天生不稳: 大截断误差大、 小抵消误差大,存在最优 ,误差随 呈 U 形——「越小越准」是错觉。解药是绕开减法:复步长微分(无抵消)或自动微分(精确)。
有趣的是,数值积分和微分恰好相反——它又稳又好算(积分是「求平均、抹平」,天然压制误差,不像微分放大误差)。下一章看数值积分,你会松一口气。👉 数值积分:梯形、Simpson 与自适应。
参考文献
- [1] Nick Higham.《Accuracy and Stability of Numerical Algorithms》—— 有限差分的舍入误差。
- [2] Squire & Trapp.「Using complex variables to estimate derivatives.」1998. —— 复步长微分。
- [3] Baydin et al.「Automatic Differentiation in Machine Learning: a Survey.」2018. —— 自动微分综述。