5.4 一元微分学与局部近似
上一节讨论连续:函数图像不断开。微分追问的是更细的问题:函数在某点附近像不像一条直线。导数就是这个问题的答案,它是差商的极限;微分给出线性近似的具体形式;中值定理把一点处的导数信息推广到区间的整体性质;Taylor 公式把线性近似推广到多项式近似,误差有显式估计。这条线索是数学分析最重要的主干之一,也是数值计算、最优化与统计学中大量方法的共同来源。
导数在计算中扮演两个角色。理论角色是提供函数形状的信息:导数的符号决定单调性,导数的零点给出极值候选,二阶导数的符号决定凹凸。计算角色是提供迭代算法的方向与步长:求根的 Newton 法、优化的梯度下降、概率模型的得分函数都建立在导数上。当解析导数无法获得时,可以用差商近似(数值微分),但有限差分有两类误差互相制约:截断误差随步长增大,舍入误差随步长减小,最优步长落在两者的平衡点附近,本节给出
避免有限差分的两条路线是本节最后的内容:符号微分(需要表达式树与化简)与自动微分(用链式法则精确计算导数,代价与函数求值同阶)。自动微分是现代深度学习框架的核心部件,它的前向模式可以用「双数」的简单代数实现,本节用三十行代码演示它与解析导数的逐位一致。
5.4.1 导数、微分与求导法则
导数
设
存在且有限,称
可导与连续的关系:可导蕴含连续,反之不成立。可导给出
微分与线性近似
此时
线性近似是最常用的近似手段:
求导法则
四组法则覆盖初等函数的全部组合。
四则运算:
链式法则:
反函数法则:若
高阶导数:
5.4.2 中值定理
三个中值定理
Fermat 引理:若
Rolle 定理:
Lagrange 中值定理:
证明思路是构造辅助函数
中值定理的三个用法
判断单调性:
证明不等式:把不等式整理成两个函数值的比较形式,用中值定理把差写成导数乘以距离,再由导数的界给出结论。例:证明
估计误差:导数的界给出函数值差的界。这条用法在数值分析中随处可见:
L'Hospital 法则
求
5.4.3 Taylor 公式
公式与余项
中值定理把差
Peano 余项:
Lagrange 余项:
积分余项:
应用与误差
求极限:把各函数展开到足够高的阶,抵消主项后取首项。例:
误差估计与数值计算:用
凹凸性与极值判别:
凸性与最优化一章的连接
5.4.4 数值微分与收敛阶
差商与误差结构
解析导数不可得时用差商近似。三种常用格式与它们的误差:
前向差分
中心差分
五点格式
误差由两部分组成:截断误差(Taylor 余项,随
收敛阶
迭代序列
称收敛阶为
收敛阶可以从误差序列实验测定:
本节实验三用这个方法验证 Newton 法的
三种求根方法的取舍
二分法只用函数值的符号,速率是线性的(误差每步减半),但保证收敛,是可靠的兜底方法。Newton 法用导数,二次收敛最快,但需要导数值且对初值敏感(远处可能发散或跳出自变量范围)。割线法用两次函数值近似导数(差商的差商),速率
5.4.5 自动微分
自动微分(automatic differentiation)用链式法则精确计算导数,既不引入截断误差(不同于有限差分),也不做表达式化简(不同于符号微分)。两种模式。
前向模式(forward mode)把每个中间变量带上它的导数信息,计算一次得到「一个输入方向上的方向导数」。实现方式是双数:把数写成
初值取
反向模式(reverse mode)先做一次前向计算记录计算图,再从输出往回传播导数(每个中间变量记录一个伴随量),一次反向传播得到「一个输出对所有输入的梯度」。适用于输入多、输出少的函数,神经网络的损失函数正是这种情形(
5.4.6 数值实验
实验一:数值微分的步长权衡
计算
import math
f = math.sin
x0 = 1.0
exact = math.cos(x0)
print(f"f(x) = sin(x) 在 x = {x0} 处的导数精确值 = {exact:.16f}")
print(f"{'h':>10} {'前向差分':>20} {'误差':>12} {'中心差分':>20} {'误差':>12}")
for h in [1e-1, 1e-2, 1e-4, 1e-6, 1e-8, 1e-10, 1e-12, 1e-14, 1e-16]:
fwd = (f(x0 + h) - f(x0)) / h
ctr = (f(x0 + h) - f(x0 - h)) / (2 * h)
print(f"{h:>10.0e} {fwd:>20.12f} {abs(fwd - exact):>12.3e} "
f"{ctr:>20.12f} {abs(ctr - exact):>12.3e}")输出:
f(x) = sin(x) 在 x = 1.0 处的导数精确值 = 0.5403023058681398
h 前向差分 误差 中心差分 误差
1e-01 0.497363752535 4.294e-02 0.539402252170 9.001e-04
1e-02 0.536085981012 4.216e-03 0.540293300875 9.005e-06
1e-04 0.540260231419 4.207e-05 0.540302304968 9.004e-10
1e-06 0.540301885121 4.207e-07 0.540302305896 2.772e-11
1e-08 0.540302302898 2.970e-09 0.540302308449 2.581e-09
1e-10 0.540302247387 5.848e-08 0.540302247387 5.848e-08
1e-12 0.540345546085 4.324e-05 0.540290034934 1.227e-05
1e-14 0.544009282066 3.707e-03 0.544009282066 3.707e-03
1e-16 0.000000000000 5.403e-01 0.555111512313 1.481e-02数据分成三段,每段对应一种误差主导。
实验二:Taylor 展开的误差阶梯
用
import math
xt = 1.0
for n in [1, 3, 5, 7, 9, 11]:
s = 0.0
for k in range(0, n + 1, 2):
s += (-1) ** (k // 2) * xt ** (2 * (k // 2) + 1) / math.factorial(2 * (k // 2) + 1)
print(f" n = {n:>2}: 近似值 = {s:.15f}, 误差 = {abs(s - math.sin(1.0)):.3e}")
print(f" 参考:sin(1) = {math.sin(1.0):.15f}")输出:
n = 1: 近似值 = 1.000000000000000, 误差 = 1.585e-01
n = 3: 近似值 = 0.833333333333333, 误差 = 8.138e-03
n = 5: 近似值 = 0.841666666666667, 误差 = 1.957e-04
n = 7: 近似值 = 0.841468253968254, 误差 = 2.731e-06
n = 9: 近似值 = 0.841471009700176, 误差 = 2.489e-08
n = 11: 近似值 = 0.841470984648068, 误差 = 1.598e-10
参考:sin(1) = 0.841470984807897误差从
实验三:迭代法的收敛阶
用 Newton 法与割线法求
import math
def newton(fun, dfun, x0, n=8):
x = x0
errs = []
root = 2 ** (1 / 3)
for _ in range(n):
errs.append(abs(x - root))
x = x - fun(x) / dfun(x)
return errs
def secant(fun, x0, x1, n=10):
root = 2 ** (1 / 3)
errs = []
a, b = x0, x1
for _ in range(n):
errs.append(abs(b - root))
fa, fb = fun(a), fun(b)
if fb == fa:
break
a, b = b, b - fb * (b - a) / (fb - fa)
return errs
fx = lambda t: t ** 3 - 2.0
dfx = lambda t: 3.0 * t ** 2
err_n = [e for e in newton(fx, dfx, 1.0) if e > 1e-14]
err_s = [e for e in secant(fx, 1.0, 1.5) if e > 1e-14]
print("Newton 法(根为 2^(1/3) = 1.259921):")
print(" 误差:", " ".join(f"{e:.3e}" for e in err_n))
print(" 收敛阶估计:", " ".join(f"{math.log(err_n[i+1]/err_n[i])/math.log(err_n[i]/err_n[i-1]):.3f}"
for i in range(1, len(err_n) - 1)))
print("割线法:")
print(" 误差:", " ".join(f"{e:.3e}" for e in err_s))
print(" 收敛阶估计:", " ".join(f"{math.log(err_s[i+1]/err_s[i])/math.log(err_s[i]/err_s[i-1]):.3f}"
for i in range(1, len(err_s) - 1)))
print(f" 理论值:(1 + sqrt(5))/2 = {(1 + math.sqrt(5)) / 2:.3f}")输出:
Newton 法(根为 2^(1/3) = 1.259921):
误差: 2.599e-01 7.341e-02 3.968e-03 1.244e-05 1.229e-10
收敛阶估计: 2.308 1.976 1.999
割线法:
误差: 2.401e-01 4.939e-02 8.513e-03 3.442e-04 2.336e-06 6.381e-10
收敛阶估计: 1.112 1.824 1.556 1.643
理论值:(1 + sqrt(5))/2 = 1.618Newton 法的误差序列展示二次收敛的形态:
割线法的误差下降比例约为
实验四:前向模式自动微分
用双数类实现前向模式自动微分,与解析导数逐位对照。
import math
class Dual:
"""双数:value + derivative * eps,其中 eps^2 = 0"""
def __init__(self, v, d=0.0):
self.v = v
self.d = d
def __add__(self, o):
o = o if isinstance(o, Dual) else Dual(o)
return Dual(self.v + o.v, self.d + o.d)
def __radd__(self, o):
return self.__add__(o)
def __sub__(self, o):
o = o if isinstance(o, Dual) else Dual(o)
return Dual(self.v - o.v, self.d - o.d)
def __mul__(self, o):
o = o if isinstance(o, Dual) else Dual(o)
return Dual(self.v * o.v, self.d * o.v + self.v * o.d)
def __rmul__(self, o):
return self.__mul__(o)
def dsin(a):
return Dual(math.sin(a.v), math.cos(a.v) * a.d)
def f_ad(x):
return x * dsin(x) + x * x
xv = 1.2
res = f_ad(Dual(xv, 1.0))
analytic = math.sin(xv) + xv * math.cos(xv) + 2 * xv
print(f"f(x) = x*sin(x) + x^2 在 x = {xv}:")
print(f" 前向模式自动微分 = {res.d:.15f}")
print(f" 解析导数 = {analytic:.15f}")
print(f" 差 = {abs(res.d - analytic):.3e}")
def g_ad(x, y):
return x * x * y + dsin(x * y)
x1, y1 = 1.2, 0.7
gx = g_ad(Dual(x1, 1.0), Dual(y1, 0.0))
gy = g_ad(Dual(x1, 0.0), Dual(y1, 1.0))
print(f"g(x, y) = x^2*y + sin(x*y) 在 ({x1}, {y1}):")
print(f" 自动微分偏导 = ({gx.d:.15f}, {gy.d:.15f})")
print(f" 解析偏导 = ({2 * x1 * y1 + y1 * math.cos(x1 * y1):.15f}, "
f"{x1 * x1 + x1 * math.cos(x1 * y1):.15f})")输出:
f(x) = x*sin(x) + x^2 在 x = 1.2:
前向模式自动微分 = 3.766868391339234
解析导数 = 3.766868391339234
差 = 0.000e+00
g(x, y) = x^2*y + sin(x*y) 在 (1.2, 0.7):
自动微分偏导 = (2.147223978088916, 2.240955391009570)
解析偏导 = (2.147223978088916, 2.240955391009570)两个函数的结果与解析导数完全一致(差值精确为零)。对照实验一可以看出精确性来自何处:自动微分不做差商近似,不引入截断误差,每一次运算都把导数信息按链式法则精确地传递下去;舍入误差的量级与函数求值本身相同(
一元函数的偏导数计算需要一次前向传播,二元函数需要两次(分别把导数种子放到
5.4.7 本节小结
微分学的三层内容
本节把微分学组织为三层。基础层是导数与微分:导数是差商的极限(可导蕴含连续,反之不成立),微分给出线性近似
微分处理的是「变化率」,积分处理的是「累积量」。下一节讨论它们的逆运算关系:微积分基本定理把求积分化为求原函数,反常积分把积分推广到无穷区间与无界函数,数值求积则在原函数不可得时提供数值计算方法。
练习题
第 1 题 概念推导
(a) 用中值定理证明:
参考答案
(a) 对
由 log1p 的误差分析基础:log1p。
(b)
前三项之和为
(c) 可导蕴含连续:由
第 2 题 计算推理
(a) 求
参考答案
(a) 展开
(b) 总误差
取
(c) 用
第一组:
第 3 题 代码验证
用本节代码做三组实验并解释:(a) 在实验一里把函数换成
参考答案
(a)
(b) 初值
(c) 增加两个运算:
def dexp(a):
return Dual(math.exp(a.v), math.exp(a.v) * a.d)
def __truediv__(self, o):
o = o if isinstance(o, Dual) else Dual(o)
return Dual(self.v / o.v, (self.d * o.v - self.v * o.d) / o.v ** 2)计算 autograd、JAX,以及深度学习框架的自动求导引擎)的核心思想:定义有限的运算集合,让每个运算携带导数传播规则,复合表达式自动获得精确导数。
第 4 题 综合应用
某优化程序需要最小化一个由若干初等函数复合而成的目标函数
参考答案
方案一:有限差分。中心差分每步需要
方案二:符号微分。得到的是表达式,精度是精确的(与手写推导相同),但表达式长度会随复合层数指数增长(每个运算节点复制其子表达式的导数),对中等规模的问题就会产生巨大的中间表达式,化简本身可能比求值更贵。这一方案适合符号计算系统处理的小规模解析推导,不适合数值迭代。
方案三:自动微分。前向模式的代价与输入个数成正比(
推荐与结论:选反向模式自动微分,代价 expm1、log1p、logaddexp)使用。
常见错误
错误 1 · 把「可导」与「连续」的关系记反
原因:连续是可导的必要条件。把「连续所以可导」当作推理依据会得到错误结论(
解决:判断可导性时检查左右差商的极限是否相等,或者检查函数的局部行为是否与直线一致。涉及不可导点的算法要使用次梯度、近端算子或平滑化,报告中说明最优解处的可导性状态。
错误 2 · 在数值微分中一味减小步长
原因:有限差分的误差由截断误差与舍入误差两部分组成,只看到前者会得出「步长越小越好」的错误结论。本节实验一的数据:
解决:按格式的理论最优步长取值(前向
错误 3 · 用单步误差比值估计收敛阶
原因:收敛阶
解决:用连续多步的估计值观察趋势,等估计值稳定后再报告(实验三中 Newton 的估计稳定到
错误 4 · 把自动微分与有限差分或符号微分混为一谈
原因:三者的精度与代价差别很大,混用会做出错误的方案选择。有限差分有
解决:按「输入个数、输出个数、精度要求」三个维度选方案。输入少输出多选前向模式;输入多输出少(损失函数)选反向模式;需要解析表达式供后续推导时用符号微分;参数极少且精度要求低时可以用有限差分。报告中写明所用方法的精度上限,避免把有限差分的结果当作精确梯度用于高精度分析。