多元微积分¶
多元微积分将导数和积分推广到多变量函数,这对于机器学习模型(通常拥有数百万参数)至关重要。本章涵盖偏导数、梯度、雅可比矩阵、海森矩阵,以及使反向传播成为可能的多元链式法则。
-
到目前为止,我们的函数都是接受单个输入 \(x\) 并产生单个输出 \(f(x)\)。但在机器学习中,我们几乎从不只处理一个变量。
-
考虑一个二元函数,比如 \(f(x, y) = x^2 + y^2\)。这定义了一个三维空间中的曲面,形状像一个碗。我们想知道:如果固定 \(y\),稍微改变 \(x\),\(f\) 如何变化?这就是偏导数。
-
\(f\) 对 \(x\) 的偏导数,记作 \(\frac{\partial f}{\partial x}\),将其他所有变量视为常数,然后对 \(x\) 正常求导。
-
对于 \(f(x, y) = x^2y + 3x - 2y\):
-
计算 \(\frac{\partial f}{\partial x}\) 时,我们将 \(y\) 视为常数,所以 \(x^2y\) 导数为 \(2xy\),\(3x\) 导数为 \(3\),\(-2y\) 导数为 \(0\)。
-
计算 \(\frac{\partial f}{\partial y}\) 时,我们将 \(x\) 视为常数,所以 \(x^2y\) 导数为 \(x^2\),\(3x\) 导数为 \(0\),\(-2y\) 导数为 \(-2\)。
-
几何上,对 \(x\) 求偏导就像用一个平行于 \(xz\) 平面的平面(在固定的 \(y\) 值处)切割三维曲面,然后求所得曲线的斜率。
- 梯度将所有偏导数收集到一个向量中:
-
对于 \(f(x, y) = x^2 + y^2\):\(\nabla f(x, y) = (2x, 2y)\)。在点 \((1, 2)\) 处:\(\nabla f(1, 2) = (2, 4)\)。
-
梯度有两个关键性质:
-
方向:它指向函数值上升最快的方向。想象山上的徒步者:梯度指向正上坡、最陡峭的路径。
-
大小:\(\|\nabla f\|\) 给出该最陡方向上的上升速率。梯度大意味着地形陡峭;梯度小意味着地形近乎平坦。
-
-
由于梯度指向上升方向,沿相反方向(\(-\nabla f\))移动则向下、朝向更小的值。这个简单的想法是梯度下降的基础,我们将在后续章节详细探讨这一优化技术。目前的关键是:梯度告诉你“上坡”是哪个方向,以及爬升有多陡。
-
方向导数推广了偏导数。问题不再是“沿 \(x\) 轴方向 \(f\) 如何变化?”,而是“沿任意单位向量 \(\mathbf{u}\) 方向 \(f\) 如何变化?”它通过梯度与单位向量的点积计算:
-
对于 \(f(x, y) = x^2 + y^2\) 在点 \((1, 2)\) 处,沿 \(\mathbf{v} = (3, 4)\) 方向:先归一化得到 \(\mathbf{u} = (3/5, 4/5)\),然后 \(D_{\mathbf{u}} f = (2, 4) \cdot (3/5, 4/5) = 6/5 + 16/5 = 22/5\)。
-
偏导数是方向导数的特例,此时方向沿着坐标轴。如果某个方向上的方向导数为零,则函数在该点沿该方向是平坦的。
-
等高线(或等值线)连接函数值相等的点。对于 \(f(x, y) = x^2 + y^2\),等高线是以原点为中心的圆:\(x^2 + y^2 = c\),\(c\) 取不同值。
-
等高线永不相交(一个点不可能有两个不同的函数值)。
-
梯度始终垂直于等高线,从低值指向高值。
-
等高线密集表示地形陡峭;等高线稀疏表示坡度平缓。
-
到目前为止,我们的函数都只输出一个标量。但很多函数输出多个值。函数 \(\mathbf{F}: \mathbb{R}^n \to \mathbb{R}^m\) 接受 \(n\) 个输入,产生 \(m\) 个输出。雅可比矩阵将这个向量值函数的所有偏导数组织起来:
-
雅可比矩阵的每一行是一个输出分量的梯度。对于一个有 3 个输入、2 个输出的函数,雅可比矩阵是一个 \(2 \times 3\) 矩阵。
-
雅可比矩阵将导数推广到向量值函数。
-
正如标量函数的导数告诉你每个单位输入变化会导致输出变化多少,雅可比矩阵告诉你每个输出相对于每个输入的变化情况。
-
雅可比矩阵的行列式衡量一个变换在局部将空间拉伸或压缩的程度。
-
如果行列式为 2,小区域的面积将加倍;如果为 0,该变换会将空间压到更低维度(回想矩阵章节中,零行列式意味着奇异、不可逆变换)。
-
当多个变换复合时(一个的输出作为下一个的输入),整体映射的雅可比矩阵等于各个雅可比矩阵的乘积。我们将在后续章节看到这个思想变得核心。
-
梯度捕捉一阶信息(斜率),而海森矩阵捕捉二阶信息(曲率)。
-
对于标量函数 \(f(x_1, \ldots, x_n)\),海森矩阵是 \(n \times n\) 的二阶偏导数矩阵:
- 对于 \(f(x, y) = x^3 + 2xy^2 - y^3\),梯度为 \((3x^2 + 2y^2,\; 4xy - 3y^2)\),海森矩阵为:
-
对角元(\(6x\) 和 \(4x - 6y\))告诉你沿 \(x\) 方向的斜率随 \(x\) 变化的情况,以及沿 \(y\) 方向的类似信息。
-
非对角元(\(4y\))告诉你沿一个方向的斜率如何随另一方向的变化而变化。
-
克莱罗定理保证:对于具有连续二阶导数的函数,混合偏导数相等:\(\frac{\partial^2 f}{\partial x \partial y} = \frac{\partial^2 f}{\partial y \partial x}\)。
-
这意味着海森矩阵是对称的,从而(如我们在矩阵章节所见)保证实特征值和正交特征向量。
-
海森矩阵告诉我们临界点(梯度为零的点)附近函数的形状:
- 若 \(H\) 正定(所有特征值为正),则该点是局部极小值,曲面在每个方向都向上弯曲,像一个碗。
- 若 \(H\) 负定(所有特征值为负),则该点是局部极大值,曲面在每个方向都向下弯曲,像一个倒扣的碗。
- 若 \(H\) 既有正特征值也有负特征值,则该点是鞍点,曲面在某些方向向上弯曲、另一些方向向下弯曲,像一个山口。
-
多元链式法则将链式法则推广到多变量函数。若 \(z = f(x, y)\),其中 \(x = g(t)\),\(y = h(t)\),则:
-
从 \(t\) 到 \(z\) 的每条路径贡献一项:沿该路径的偏导数乘以中间变量对 \(t\) 的导数。
-
例如,若 \(z = x^2 y + 3x - y^2\),\(x = \cos(t)\),\(y = \sin(t)\):
-
除了手工求导,还有三种方法:
- 数值微分:近似 \(f'(x) \approx \frac{f(x+h) - f(x-h)}{2h}\),其中 \(h\) 很小。简单但有噪声且不精确。
- 符号微分:通过代数规则应用微分法则,产生精确公式。但得到的表达式可能指数级膨胀。
- 自动微分(autodiff):追踪计算操作链,高效计算精确导数。这正是 JAX、PyTorch 和 TensorFlow 使用的方法。它给出精确的数值(不是近似),同时避免产生膨胀的符号表达式。
编程任务(使用 CoLab 或 notebook)¶
-
使用
jax.grad计算 \(f(x, y) = x^2 y + 3x - 2y\) 在点 \((1, 2)\) 处的梯度。由于 \(f\) 接受向量输入,使用带有argnums的jax.grad。 -
使用
jax.jacobian计算向量值函数的雅可比矩阵,并与手动计算结果比较。 -
使用
jax.hessian计算 \(f(x, y) = x^3 + 2xy^2 - y^3\) 的海森矩阵,并验证它是对称的。 -
从零开始构建一个最小自动微分引擎。
- 每个
Var记录其值和如何通过链式法则反向传播梯度。 - 尝试扩展它支持更多运算(除法、幂等)。
- 这是 JAX、PyTorch 和 NumPy 设计的基础。
class Var: def __init__(self, val, children=(), backward_fn=None): self.val = val self.grad = 0.0 self.children = children self.backward_fn = backward_fn def __add__(self, other): out = Var(self.val + other.val, children=(self, other)) def _backward(): self.grad += out.grad # d(a+b)/da = 1 other.grad += out.grad # d(a+b)/db = 1 out.backward_fn = _backward return out def __mul__(self, other): out = Var(self.val * other.val, children=(self, other)) def _backward(): self.grad += other.val * out.grad # d(a*b)/da = b other.grad += self.val * out.grad # d(a*b)/db = a out.backward_fn = _backward return out def backward(self): # 拓扑排序然后传播梯度 # 我们将在数据结构和算法中详细讲解这一过程 order, visited = [], set() def topo(v): if v not in visited: visited.add(v) for c in v.children: topo(c) order.append(v) topo(self) self.grad = 1.0 for v in reversed(order): if v.backward_fn: v.backward_fn() # f(x, y) = x*x*y + x 在 (3, 2) x = Var(3.0) y = Var(2.0) f = x * x * y + x # = 3*3*2 + 3 = 21 f.backward() print(f"f = {f.val}") # 21.0 print(f"df/dx = {x.grad}") # 2*x*y + 1 = 13.0 print(f"df/dy = {y.grad}") # x*x = 9.0
- 每个