跳转至

# 多元微积分

多元微积分将导数和积分扩展到多变量函数，这对于机器学习模型拥有数百万参数的情形至关重要。本章涵盖偏导数、梯度、雅可比矩阵、海森矩阵以及使反向传播成为可能的多变量链式法则。

- 到目前为止，我们的函数都只接受单个输入 \(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} = 2xy + 3 \qquad \frac{\partial f}{\partial y} = x^2 - 2\]

- 计算 \(\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\) 值处）切割三维曲面，然后求所得曲线的斜率。

- 梯度将所有偏导数收集到一个向量中：

\[\nabla f = \left(\frac{\partial f}{\partial x_1}, \frac{\partial f}{\partial x_2}, \ldots, \frac{\partial f}{\partial x_n}\right)\]

- 对于 \(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\)）移动就是下坡，走向更低的值。这个简单的想法是梯度下降的基础，我们将在后续章节中详细探讨这种优化技术。现在，关键要点是：梯度告诉你哪个方向是"上坡"，以及攀登的陡峭程度。

- 方向导数推广了偏导数。它不问"\(f\) 沿 \(x\) 轴如何变化？"，而是问"\(f\) 沿任意方向 \(\mathbf{u}\) 如何变化？"它通过梯度与单位向量的点积来计算：

\[D_{\mathbf{u}} f = \nabla f \cdot \mathbf{u}\]

- 对于 \(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\)，等高线是以原点为中心的圆：对应不同 \(c\) 值的 \(x^2 + y^2 = c\)。

- 等高线永不相交（一个点不可能有两个不同的函数值）。

- 梯度始终垂直于等高线，从低值指向高值。

- 等高线密集表示地形陡峭；等高线稀疏表示坡度平缓。

- 到目前为止，我们的函数都只产生单个输出。但许多函数会产生多个输出。函数 \(\mathbf{F}: \mathbb{R}^n \to \mathbb{R}^m\) 接收 \(n\) 个输入并产生 \(m\) 个输出。雅可比矩阵组织了这样一个向量值函数的所有偏导数：

\[ J = \begin{bmatrix} \frac{\partial f_1}{\partial x_1} & \cdots & \frac{\partial f_1}{\partial x_n} \\ \vdots & \ddots & \vdots \\ \frac{\partial f_m}{\partial x_1} & \cdots & \frac{\partial f_m}{\partial x_n} \end{bmatrix} \]

- 雅可比矩阵的每一行是一个输出分量的梯度。对于一个有 3 个输入和 2 个输出的函数，雅可比矩阵是一个 \(2 \times 3\) 矩阵。

- 雅可比矩阵将导数推广到向量值函数。

- 就像标量函数的导数告诉你每单位输入变化对应的输出变化量一样，雅可比矩阵告诉你每个输出相对于每个输入的变化情况。

- 雅可比行列式衡量一个变换局部拉伸或压缩空间的程度。

- 如果行列式为 2，小区域的面积加倍。如果行列式为 0，该变换将空间压缩到更低维度（回想我们在矩阵章节中学到的：行列式为零意味着奇异变换，不可逆）。

- 当多个变换组合在一起（一个变换的输出作为下一个变换的输入）时，整体映射的雅可比矩阵是各个雅可比矩阵的乘积。我们将在后续章节中看到这个思想变得至关重要。

- 梯度捕获一阶信息（斜率），而海森矩阵捕获二阶信息（曲率）。

- 对于标量函数 \(f(x_1, \ldots, x_n)\)，海森矩阵是所有二阶偏导数的 \(n \times n\) 矩阵：

\[ H = \begin{bmatrix} \frac{\partial^2 f}{\partial x_1^2} & \frac{\partial^2 f}{\partial x_1 \partial x_2} & \cdots \\ \frac{\partial^2 f}{\partial x_2 \partial x_1} & \frac{\partial^2 f}{\partial x_2^2} & \cdots \\ \vdots & \vdots & \ddots \end{bmatrix} \]

- 对于 \(f(x, y) = x^3 + 2xy^2 - y^3\)，梯度为 \((3x^2 + 2y^2,\; 4xy - 3y^2)\)，海森矩阵为：

\[ H = \begin{bmatrix} 6x & 4y \\ 4y & 4x - 6y \end{bmatrix} \]

- 对角线元素（\(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)\)，则：

\[\frac{dz}{dt} = \frac{\partial f}{\partial x}\frac{dx}{dt} + \frac{\partial f}{\partial y}\frac{dy}{dt}\]

- 从 \(t\) 到 \(z\) 的每条路径都贡献一项：沿该路径的偏导数乘以中间变量对 \(t\) 的导数。

- 例如，如果 \(z = x^2 y + 3x - y^2\)，\(x = \cos(t)\)，\(y = \sin(t)\)：

\[\frac{dz}{dt} = (2xy + 3)(-\sin t) + (x^2 - 2y)(\cos t)\]

- 除了手动计算导数，还有三种方法：

  - 数值微分：用 \(f'(x) \approx \frac{f(x+h) - f(x-h)}{2h}\)（取很小的 \(h\)）来近似。简单但有噪声且不精确。
  - 符号微分：通过代数地应用求导法则产生精确表达式。可能导致表达式呈指数级膨胀。
  - 自动微分（autodiff）：跟踪运算链并高效地计算精确导数。JAX、PyTorch 和 TensorFlow 都使用这种方法。它能给出精确的数值（而非近似值），且不会产生臃肿的符号表达式。

## 编程练习（使用 CoLab 或 notebook）

1. 使用 `jax.grad` 计算函数 \(f(x, y) = x^2 y + 3x - 2y\) 在点 \((1, 2)\) 处的梯度。由于 \(f\) 接收向量输入，请使用带 `argnums` 参数的 `jax.grad`。

       import jax
       import jax.numpy as jnp

       def f(x, y):
           return x**2 * y + 3*x - 2*y

       df_dx = jax.grad(f, argnums=0)
       df_dy = jax.grad(f, argnums=1)

       x, y = 1.0, 2.0
       print(f"∂f/∂x = {df_dx(x, y):.4f}  (期望: {2*x*y + 3:.4f})")
       print(f"∂f/∂y = {df_dy(x, y):.4f}  (期望: {x**2 - 2:.4f})")

2. 使用 `jax.jacobian` 计算向量值函数的雅可比矩阵，并与手动计算结果进行比较。

       import jax
       import jax.numpy as jnp

       def F(x):
           return jnp.array([x[0]**2 + x[1], x[0] * x[1]**2])

       J = jax.jacobian(F)
       x = jnp.array([1.0, 2.0])
       print(f"在 (1,2) 处的雅可比矩阵:\n{J(x)}")
       # 期望: [[2*x[0], 1], [x[1]**2, 2*x[0]*x[1]]] = [[2, 1], [4, 4]]

3. 使用 `jax.hessian` 计算 \(f(x, y) = x^3 + 2xy^2 - y^3\) 的海森矩阵，并验证其对称性。

       import jax
       import jax.numpy as jnp

       def f(xy):
           x, y = xy[0], xy[1]
           return x**3 + 2*x*y**2 - y**3

       H = jax.hessian(f)
       point = jnp.array([1.0, 2.0])
       hess = H(point)
       print(f"海森矩阵:\n{hess}")
       print(f"是否对称: {jnp.allclose(hess, hess.T)}")
       # 期望: [[6x, 4y], [4y, 4x-6y]] = [[6, 8], [8, -8]]

4. 从头构建一个极简的自动微分引擎。

   - 每个 `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