5.8 多元微分学与微分形式初步
前四节讨论的函数都只有一个自变量。最优化、机器学习与统计中的函数几乎都是多变量的:目标函数依赖成千上万个参数,损失函数是参数向量的函数。把微分学推广到多维,需要处理新的问题:多变量的函数没有单一的「变化率」,每个方向都有变化率;局部形状由一个矩阵(Hessian)而不是一个数描述;方程组定义的隐式关系需要用矩阵的逆来表达。
本节的组织沿四条线索展开。微分线索定义偏导数、方向导数、全微分与梯度,其中可微是一个比「偏导数存在」更强的条件,实验一用中心差分核对
5.8.1 多元函数的极限与连续
多元函数的极限沿用数列的语言,但「接近」的含义变了:点
路径的多样性带来一元情形没有的现象:沿所有路径的极限都存在且相等才可能有极限,而检查所有路径是不可能的。判定极限不存在的方法比较直接:找两条路径给出不同的极限。例:
连续的定义与一元相同:
5.8.2 全微分、偏导数与梯度
偏导数与方向导数
偏导数是把其他变量固定后的一元导数:
其中
方向导数把方向从坐标轴推广到任意单位向量
偏导数是方向导数在
可微与全微分
可微的定义是一元情形(5.4 节)的直接推广:存在向量
右端的线性部分
其中
偏导数存在不蕴含可微。反例是
两个偏导数在原点都存在且等于零(沿坐标轴函数恒为零),但沿
可微的两个推论在计算中常用:可微蕴含连续;可微函数沿水平集移动时函数值的一阶变化为零,因此梯度与水平集(等高线、等值面)正交。
梯度与最速上升方向
由
- 梯度的方向是函数上升最快的方向(最速上升方向),负梯度方向是下降最快方向。
- 梯度垂直于水平集,因为沿水平集移动时方向导数为零。
- 梯度的范数是最大方向导数的值,度量该点最陡的程度。
梯度下降法(第三章 3.3 节)的全部依据就是第 1 条:沿着
5.8.3 链式法则与 Jacobian 矩阵
链式法则
设
其中
这条公式是一元链式法则在多变量下的形式,求和出现在中间变量上。神经网络的反向传播就是这条公式的递归应用:把网络看作多层复合函数,从输出层往输入层逐层用链式法则传递梯度,每层的 Jacobian 由该层的运算决定。
变换与 Jacobian 行列式
坐标变换
5.8.4 高阶导数、Hessian 与 Taylor 公式
混合偏导数与 Hessian 矩阵
二阶偏导数记作
因此二阶偏导数构成一个对称矩阵
称 Hessian 矩阵。对称性使
二阶 Taylor 公式与极值判别
极值判别(
正定(所有特征值为正)时 是局部极小点。 负定(所有特征值为负)时 是局部极大点。 不定(特征值有正有负)时 是鞍点。 半定(有零特征值)时二阶信息不足,需要更高阶项。
实验二的三行数据给出三种情形:
有一点需要区分:Hessian 的特征值符号只在临界点给出极值判据。实验二在
二阶 Taylor 的误差阶
二阶 Taylor 的误差是
5.8.5 反函数定理与隐函数定理
反函数定理
设
定理的内容可以概括为「可逆的线性化给出局部的可逆性」。证明的思路是用压缩映射原理(5.1 节)把求解
右端关于
隐函数定理
隐函数定理:设
公式的来源是把
它把隐函数的导数化成了偏导数矩阵的逆乘偏导数矩阵,全部是线性代数运算。实验三用三个例子核对这条公式。
一元隐函数:
在
二元隐函数:
在
方程组:两个方程决定两个未知数
关于
在
与 Lagrange 乘子的连接
隐函数定理是 Lagrange 乘子法的证明工具。在约束
5.8.6 微分形式初步
外微分与微分形式
微分形式是可以在曲面上积分、并且对坐标变换有确定规律的对象。0 形式是函数
反交换律是微分形式的核心代数性质,它自动处理了定向与符号。外微分算子
三条性质定义了
最后一条由混合偏导数的对称性与外积的反交换性相消得到,是最重要的结构性结论。
梯度、旋度、散度的统一
在
三个系数是旋度的分量。对 2 形式
系数是散度。三个算子由同一个算子
闭合形式与恰当形式
若
在
在
与三大积分公式的关系
广义 Stokes 公式把微积分基本定理、Green 公式、Gauss 公式、Stokes 公式统一为一条:
右端是
这条线索的用途
微分形式的语言在日常计算中不是必需的(大多数工程计算用梯度、旋度、散度的坐标公式就够了),它的价值有两处。一是结构性:把四五个看似无关的积分公式归为一条,把三个微分算子归为一个,理解成本大幅下降。二是几何与拓扑:闭合但不恰当的形式度量空间的洞,这条思路在电磁学(磁通量量子化)、规范场论(曲率形式与陈类)与机器学习的信息几何(第四章 4.3 节的 Fisher 信息矩阵作为度量)中都出现。
5.8.7 数值实验
实验一:梯度、方向导数与最速上升方向
核对解析梯度与中心差分,并验证方向导数关于方向的线性与极值性质。
import math
def f(x, y):
return x * x * y + math.sin(x * y)
def grad_f(x, y):
return (2 * x * y + y * math.cos(x * y), x * x + x * math.cos(x * y))
x0, y0 = 1.2, 0.7
ga = grad_f(x0, y0)
h = 1e-6
gn = ((f(x0 + h, y0) - f(x0 - h, y0)) / (2 * h),
(f(x0, y0 + h) - f(x0, y0 - h)) / (2 * h))
print(f"f(x, y) = x^2*y + sin(x*y),在 ({x0}, {y0}) 处")
print(f" 解析梯度 = ({ga[0]:.12f}, {ga[1]:.12f})")
print(f" 中心差分数值 = ({gn[0]:.12f}, {gn[1]:.12f})")
print(f" 最大偏差 = {max(abs(ga[0] - gn[0]), abs(ga[1] - gn[1])):.3e}")
nrm = math.hypot(*ga)
print(f"梯度范数 |grad f| = {nrm:.12f}")
print(f"{'角度(度)':>10} {'方向向量':>22} {'解析方向导数':>16} {'数值方向导数':>16}")
for deg in [0, 30, 60, 90, 120, 180, 270]:
th = math.radians(deg)
vx, vy = math.cos(th), math.sin(th)
dan = ga[0] * vx + ga[1] * vy
t = 1e-6
dnu = (f(x0 + t * vx, y0 + t * vy) - f(x0 - t * vx, y0 - t * vy)) / (2 * t)
print(f"{deg:>10} ({vx:>8.5f}, {vy:>8.5f}) {dan:>16.10f} {dnu:>16.10f}")输出:
f(x, y) = x^2*y + sin(x*y),在 (1.2, 0.7) 处
解析梯度 = (2.147223978089, 2.240955391010)
中心差分数值 = (2.147223977733, 2.240955391031)
最大偏差 = 3.558e-10
梯度范数 |grad f| = 3.103619157786
角度(度) 方向向量 解析方向导数 数值方向导数
0 ( 1.00000, 0.00000) 2.1472239781 2.1472239777
30 ( 0.86603, 0.50000) 2.9800282081 2.9800282082
60 ( 0.50000, 0.86603) 3.0143362864 3.0143362867
90 ( 0.00000, 1.00000) 2.2409553910 2.2409553910
120 (-0.50000, 0.86603) 0.8671123083 0.8671123082
180 (-1.00000, 0.00000) -2.1472239781 -2.1472239777
270 (-0.00000, -1.00000) -2.2409553910 -2.2409553910前三行给出梯度核对的结果:解析值
方向导数的表格验证了三条结论。解析值与数值逐位相符(偏差在
实验二:Hessian 矩阵与二阶 Taylor
核对解析 Hessian 与数值差分,测量二阶 Taylor 的误差阶,并用二次型的特征值判断极值类型。
import math
import numpy as np
def g(x, y):
return x * x * y + math.sin(x * y)
def hess_analytic(x, y):
return np.array([
[2 * y - y * y * math.sin(x * y), 2 * x + math.cos(x * y) - x * y * math.sin(x * y)],
[2 * x + math.cos(x * y) - x * y * math.sin(x * y), -x * x * math.sin(x * y)],
])
def hess_numeric(x, y, hh=1e-4):
def gx(a, b):
return 2 * a * b + b * math.cos(a * b)
def gy(a, b):
return a * a + a * math.cos(a * b)
H = np.zeros((2, 2))
H[0, 0] = (gx(x + hh, y) - gx(x - hh, y)) / (2 * hh)
H[0, 1] = (gx(x, y + hh) - gx(x, y - hh)) / (2 * hh)
H[1, 0] = (gy(x + hh, y) - gy(x - hh, y)) / (2 * hh)
H[1, 1] = (gy(x, y + hh) - gy(x, y - hh)) / (2 * hh)
return H
x0, y0 = 1.2, 0.7
ga = (2 * x0 * y0 + y0 * math.cos(x0 * y0), x0 * x0 + x0 * math.cos(x0 * y0))
Ha = hess_analytic(x0, y0)
Hn = hess_numeric(x0, y0)
print(f" 数值差分的最大偏差 = {float(np.max(np.abs(Ha - Hn))):.3e}")
print(f" 对称性偏差 = {abs(Ha[0, 1] - Ha[1, 0]):.3e}")
print(f" 特征值 = {np.linalg.eigvalsh(Ha)[0]:.8f}, {np.linalg.eigvalsh(Ha)[1]:.8f}")
base = g(x0, y0)
rng = np.random.default_rng(1)
dirv = rng.normal(size=2)
dirv /= np.linalg.norm(dirv)
print(f"{'|h|':>10} {'误差':>14} {'误差/|h|^3':>14}")
for sc in [1e-1, 5e-2, 2e-2, 1e-2, 5e-3]:
hx, hy = sc * dirv[0], sc * dirv[1]
approx = (base + ga[0] * hx + ga[1] * hy
+ 0.5 * (Ha[0, 0] * hx * hx + 2 * Ha[0, 1] * hx * hy + Ha[1, 1] * hy * hy))
err = abs(g(x0 + hx, y0 + hy) - approx)
print(f"{sc:>10.0e} {err:>14.4e} {err / sc ** 3:>14.6f}")
for name, Q in [("x^2 + 3xy + 5y^2", np.array([[1.0, 1.5], [1.5, 5.0]])),
("x^2 - y^2", np.array([[1.0, 0.0], [0.0, -1.0]])),
("x^2 - 2xy + y^2", np.array([[1.0, -1.0], [-1.0, 1.0]]))]:
ev = np.linalg.eigvalsh(Q)
if np.all(ev > 0):
kind = "正定,局部极小"
elif np.all(ev < 0):
kind = "负定,局部极大"
elif np.all(ev >= 0) or np.all(ev <= 0):
kind = "半定,需要高阶信息"
else:
kind = "不定,鞍点"
print(f" {name:>16}: 特征值 ({ev[0]:>8.4f}, {ev[1]:>8.4f}) {kind}")输出:
数值差分的最大偏差 = 3.306e-09
对称性偏差 = 0.000e+00
特征值 = -2.67818145, 2.64102023
|h| 误差 误差/|h|^3
1e-01 5.3412e-04 0.534124
5e-02 6.5839e-05 0.526714
2e-02 4.1763e-06 0.522035
1e-02 5.2044e-07 0.520440
5e-03 6.4954e-08 0.519635
x^2 + 3xy + 5y^2: 特征值 ( 0.5000, 5.5000) 正定,局部极小
x^2 - y^2: 特征值 ( -1.0000, 1.0000) 不定,鞍点
x^2 - 2xy + y^2: 特征值 ( 0.0000, 2.0000) 半定,需要高阶信息解析 Hessian 在
特征值
二阶 Taylor 的五组数据验证误差阶:
二次型的三行数据把极值判别整理成表。
实验三:隐函数定理的数值验证
用解析公式与数值差分分别计算隐函数的导数,观察两者的一致性与公式失效的位置。
import math
print("(a) F(x, y) = x^2 + y^2 - 1 = 0,y(x) = sqrt(1 - x^2)")
for xt in [0.0, 0.3, 0.6, 0.8]:
yt = math.sqrt(1 - xt * xt)
dydx_analytic = -xt / yt
eps = 1e-6
yp = math.sqrt(1 - (xt + eps) ** 2)
ym = math.sqrt(1 - (xt - eps) ** 2)
dydx_num = (yp - ym) / (2 * eps)
print(f" x = {xt:>4.1f}: y = {yt:.10f}, dy/dx 解析 = {dydx_analytic:>12.8f}, "
f"数值 = {dydx_num:>12.8f}, 偏差 = {abs(dydx_analytic - dydx_num):.2e}")
xa, ya = 0.3, 0.4
za = math.sqrt(1 - xa * xa - ya * ya)
zx_a, zy_a = -xa / za, -ya / za
eps = 1e-6
zx_n = (math.sqrt(1 - (xa + eps) ** 2 - ya ** 2) - math.sqrt(1 - (xa - eps) ** 2 - ya ** 2)) / (2 * eps)
zy_n = (math.sqrt(1 - xa ** 2 - (ya + eps) ** 2) - math.sqrt(1 - xa ** 2 - (ya - eps) ** 2)) / (2 * eps)
print(f" 在 ({xa}, {ya}) 处:z = {za:.10f}")
print(f" (dz/dx, dz/dy) 解析 = ({zx_a:>12.8f}, {zy_a:>12.8f})")
print(f" (dz/dx, dz/dy) 数值 = ({zx_n:>12.8f}, {zy_n:>12.8f})")
print("(c) 方程组 u^2 + v^2 - 1 = 0, u - v - x = 0")
print(f"{'x':>8} {'u':>14} {'v':>14} {'du/dx':>12} {'dv/dx':>12}")
for xt in [0.0, 0.2, 0.4, 0.5]:
ut = (xt + math.sqrt(2 - xt * xt)) / 2.0
vt = ut - xt
dudx = (1 - xt / math.sqrt(2 - xt * xt)) / 2.0
print(f"{xt:>8.1f} {ut:>14.10f} {vt:>14.10f} {dudx:>12.8f} {dudx - 1.0:>12.8f}")输出:
x = 0.0: y = 1.0000000000, dy/dx 解析 = -0.00000000, 数值 = 0.00000000, 偏差 = 0.00e+00
x = 0.3: y = 0.9539392014, dy/dx 解析 = -0.31448545, 数值 = -0.31448545, 偏差 = 2.60e-12
x = 0.6: y = 0.8000000000, dy/dx 解析 = -0.75000000, 数值 = -0.75000000, 偏差 = 4.93e-11
x = 0.8: y = 0.6000000000, dy/dx 解析 = -1.33333333, 数值 = -1.33333333, 偏差 = 1.98e-11
在 (0.3, 0.4) 处:z = 0.8660254038
(dz/dx, dz/dy) 解析 = ( -0.34641016, -0.46188022)
(dz/dx, dz/dy) 数值 = ( -0.34641016, -0.46188022)
x u v du/dx dv/dx
0.0 0.7071067812 0.7071067812 0.50000000 -0.50000000
0.2 0.8000000000 0.6000000000 0.42857143 -0.57142857
0.4 0.8782329983 0.4782329983 0.35255804 -0.64744196
0.5 0.9114378278 0.4114378278 0.31101776 -0.68898224三组数据都验证了
第二组(球面)在
第三组(方程组)的数据展示了矩阵形式的公式。
实验四:反向模式自动微分
用计算图实现反向模式自动微分,与解析梯度、数值梯度对照,并观察梯度计算代价随维数的变化。
import math
import time
class Var:
__slots__ = ("v", "parents", "local", "g")
def __init__(self, v, parents=(), local=()):
self.v = v
self.parents = parents
self.local = local
self.g = 0.0
def vadd(a, b):
return Var(a.v + b.v, (a, b), (1.0, 1.0))
def vmul(a, b):
return Var(a.v * b.v, (a, b), (b.v, a.v))
def vexp(a):
return Var(math.exp(a.v), (a,), (math.exp(a.v),))
def vsin(a):
return Var(math.sin(a.v), (a,), (math.cos(a.v),))
def backward(out):
order, seen = [], set()
def build(node):
if id(node) in seen:
return
seen.add(id(node))
for p in node.parents:
build(p)
order.append(node)
build(out)
out.g = 1.0
for node in reversed(order):
for p, l in zip(node.parents, node.local):
p.g += node.g * l
def make_tape(xs):
"""f(x) = sum_i exp(x_i) * sin(x_i)"""
x = [Var(v) for v in xs]
terms = [vmul(vexp(xi), vsin(xi)) for xi in x]
out = terms[0]
for t in terms[1:]:
out = vadd(out, t)
return x, out
xs = [0.3, -0.7, 1.1]
xv, out = make_tape(xs)
backward(out)
g_ad = [xi.g for xi in xv]
g_exact = [math.exp(v) * (math.sin(v) + math.cos(v)) for v in xs]
print(f" f = {out.v:.12f}")
print(f" 反向模式梯度 = ({g_ad[0]:.12f}, {g_ad[1]:.12f}, {g_ad[2]:.12f})")
print(f" 解析梯度 = ({g_exact[0]:.12f}, {g_exact[1]:.12f}, {g_exact[2]:.12f})")
print(f" 最大偏差 = {max(abs(a - b) for a, b in zip(g_ad, g_exact)):.3e}")
def f_plain(xs):
s = 0.0
for v in xs:
s += math.exp(v) * math.sin(v)
return s
def grad_fd(xs, hh=1e-6):
g = []
for i in range(len(xs)):
xp = list(xs); xm = list(xs)
xp[i] += hh; xm[i] -= hh
g.append((f_plain(xp) - f_plain(xm)) / (2 * hh))
return g
xs_big = [0.01 * i - 3.0 for i in range(200)]
xv_big, out_big = make_tape(xs_big)
backward(out_big)
g_ad_big = [xi.g for xi in xv_big]
g_fd_big = grad_fd(xs_big)
print(f"\n维数 200 时两种方法的一致性:最大偏差 = "
f"{max(abs(a - b) for a, b in zip(g_ad_big, g_fd_big)):.3e}")
print("梯度计算的代价随维数的变化(同一实现下的计时)")
print(f"{'n':>6} {'反向模式 (ms)':>16} {'有限差分 (ms)':>16} {'代价比':>10}")
for n in [10, 50, 200, 800]:
xt = [0.01 * i - 2.0 for i in range(n)]
t0 = time.perf_counter()
for _ in range(20):
xv2, out2 = make_tape(xt)
backward(out2)
t1 = time.perf_counter()
for _ in range(20):
grad_fd(xt)
t2 = time.perf_counter()
tr = (t1 - t0) / 20 * 1000
tf = (t2 - t1) / 20 * 1000
print(f"{n:>6} {tr:>16.3f} {tf:>16.3f} {tf / tr:>10.1f}")输出:
f = 2.756336389241
反向模式梯度 = (1.688479927823, 0.059900354000, 4.040012927968)
解析梯度 = (1.688479927823, 0.059900354000, 4.040012927968)
最大偏差 = 0.000e+00
维数 200 时两种方法的一致性:最大偏差 = 1.741e-09
n 反向模式 (ms) 有限差分 (ms) 代价比
10 0.032 0.013 0.4
50 0.138 0.296 2.1
200 0.535 4.000 7.5
800 2.383 68.789 28.9第一段数据验证反向模式的正确性:三个分量的梯度与解析值逐位一致(最大偏差精确为零),与 5.4 节前向模式双数的结果性质相同。反向模式的实现只有二十余行,核心是两步:前向构造计算图(每个运算记录父节点与局部偏导),反向按逆拓扑序累加伴随量。累加的规则是链式法则:节点把它的伴随量乘以局部偏导后加到每个父节点上。拓扑序的作用是保证一个节点被处理时它所有的下游贡献都已经累加完毕(同一个节点可能被多个下游节点使用,例如本例中每个
第二段数据把代价的结构写清楚了。维数
这组数据的实用含义是一个明确的判据:变量少时用有限差分(实现简单、常数小),变量多时用反向模式自动微分(代价与变量个数几乎无关)。深度学习中损失函数有
5.8.8 本节小结
多元微分的四层内容
本节把多元微分学组织为四层。微分层给出偏导数、方向导数、全微分与梯度:可微是比「偏导数存在」更强的条件(反例
多元的微分说清楚了,下一节处理多元的积分。重积分的换元与 Jacobian 行列式、曲线曲面积分、Green 与 Gauss 与 Stokes 公式、以及高维积分的数值方法(网格法与 Monte Carlo)构成了下一节的内容,其间会用到本节的全部结论。
练习题
第 1 题 概念推导
(a) 说明为什么「所有偏导数存在」不蕴含可微,并举出反例。(b) 证明梯度与水平集正交。(c) 说明隐函数定理中条件
参考答案
(a) 偏导数只沿
沿
(b) 设水平集是
(c) 公式
第 2 题 计算推理
(a) 求
参考答案
(a) 一阶偏导数:
在
方向导数:
(b) 由隐函数定理,曲面
这条结论的用法是把「切平面」化为「梯度与切向量正交」这个线性条件。切向量可以取为
(c) Jacobian 行列式的绝对值
行列式的符号记录方向:
第 3 题 代码验证
用本节代码做三组实验并解释:(a) 在实验一里把函数换成
参考答案
(a) 解析梯度为
在
(b) 求临界点需要解
(c) 反向模式的时间与计算图的运算个数成正比,与变量的个数无关(变量对图规模的贡献是它们参与的运算数)。把
第 4 题 综合应用
某数值程序需要求解一个由
参考答案
方案一:Newton 法。每步求解线性方程组
方案二:拟 Newton 法(BFGS)。不计算 Jacobian,用函数值之差构造近似的 Jacobian(逆)的更新。每步代价
方案三:固定点迭代(例如把方程整理成
推荐:Jacobian 可解析求得、
常见错误
错误 1 · 用偏导数存在作为可微的判据
原因:偏导数只沿坐标方向定义,
解决:区分三个条件(偏导数存在、可微、偏导数连续),后者蕴含前者、前者不蕴含后者。可微性的验证方法是检查
错误 2 · 在非临界点用 Hessian 的特征值判断极值
原因:二阶判别的前提是
解决:判断极值前先检查梯度范数(是否小于容差),确认是临界点后再看 Hessian 的特征值。梯度不为零时 Hessian 的信息用于其他目的:负特征值给出可以加速下降的方向(信赖域与鞍点逃离),特征值的条件数给出问题的病态程度(第三章 3.5 节的二阶方法)。
错误 3 · 忽略隐函数定理的条件直接套公式
原因:
解决:套用公式前检查 Jacobian 是否可逆(计算行列式或条件数)。条件接近失效时用正则化或改用参数化方式(圆的情形改用角度参数,避免在奇点处求显式函数)。在方程组情形,若 Jacobian 奇异,说明约束之间存在线性相关(第三章 3.6 节的约束资格条件讨论的正是这种情形),需要检查约束的线性无关性。
错误 4 · 把反向模式与前向模式的适用场景搞混
原因:两种模式的代价结构不同,选错会在高维问题上付出巨大代价。前向模式的代价与输入个数成正比(每个输入需要一次传播),适合输入少、输出多;反向模式的代价与输出个数成正比(每个输出需要一次反向传播),适合输入多、输出少。本节实验四的数据给出量化的对照:
解决:按输入与输出的个数选择。损失函数(参数极多、输出一个标量)用反向模式;计算 Jacobian 的全部元素(输入少、输出多)用前向模式;