Skip to content

03 — Calculus & Optimization for ML / 微积分与最优化

微积分是机器学习的"引擎"。梯度(gradient /ˈɡreɪdiənt/)(Gradient)、链式法则(Chain Rule)和梯度下降(Gradient Descent)共同构成了神经网络反向传播(backpropagation /ˌbækprəpəˈɡeɪʃən/)(Backpropagation)的理论基础。本章从偏导数的几何意义出发,逐步深入到优化器的直觉理解,为后续学习神经网络打下坚实的微积分基础。


1. 偏导数与梯度(Partial Derivatives & Gradient)

1.1 为什么要从偏导数开始?

在单变量微积分中,导数(Derivative)描述函数在某个点上的瞬时变化率

f(x)=limh0f(x+h)f(x)h

但在机器学习中,函数通常有多个输入变量。例如线性回归(regression /rɪˈɡreʃən/)的损失函数 L(w,b) 同时依赖于权重 w 和偏置 b。我们需要知道:调整 w 会怎样改变 L?调整 b 呢? 这正是**偏导数(Partial Derivative)**要回答的问题。

1.2 偏导数的定义

对于一个多变量函数 f(x1,x2,,xn),它在 xi 方向上的偏导数是:固定其他变量不变,只让 xi 变化,观察函数值的变化率:

fxi=limh0f(x1,,xi+h,,xn)f(x1,,xn)h

直观理解:想象你站在一个山坡上(山坡就是损失函数曲面)。偏导数 fx 告诉你向东走时坡度有多陡,fy 告诉你向北走时坡度有多陡。

1.3 梯度 — 最陡上升方向

梯度(Gradient) 是一个向量,它将所有偏导数组合在一起:

f=(fx1,fx2,,fxn)

梯度有两条核(kernel /ˈkɜːrnl/)心性质:

  1. 方向:梯度指向函数值增加最快的方向(最陡上升方向,Steepest Ascent)
  2. 大小:梯度的模 |f| 表示该方向上的变化率大小

为什么梯度指向最陡上升? 从方向导数(Directional Derivative)的定义出发:函数 f 在单位方向 u 上的变化率为 fu=|f|cosθ。当 θ=0(即 uf 同向)时,变化率最大,等于 |f|

1.4 梯度的可视化

对于一个二维函数 f(x,y)(比如 f(x,y)=x2+2y2),梯度在每个点上都指向"最快的上坡方向":

等高线图(Contour Plot)视野:
  y ^
    |   梯度向量 → 垂直于等高线
    |   → 指向函数值增加最快的方向
    |  ╱╲
    | ╱  ╲   ← 椭圆等高线($x^2 + 2y^2 = const$)
    |╱    ╲
    └─────────────────> x

关键联系:在 ML 中我们要最小化损失函数,因此我们沿着梯度的反方向(负梯度,Negative Gradient)更新参数(parameter /pəˈræmɪtər/)。这就是梯度下降的核心思想。

详见配套代码中的等高线可视化。


2. 链式法则(Chain Rule)

2.1 为什么链式法则是反向传播的关键?

反向传播(Backpropagation) 本质上就是反复应用链式法则。神经网络将输入 x 经过多层变换得到输出 y^,每一层都是前一层的函数。要计算损失 L 对第一层参数的梯度,我们需要逐步"链式"地求导。

没有链式法则,就没有深度学习。这是整章中最重要的知识点。

2.2 标量链式法则(Scalar Chain Rule)

最简单的形式:若 y=f(u)u=g(x),则:

dydx=dydududx

示例y=sin(x2)

u=x2y=sin(u),则:

dydx=cos(u)2x=2xcos(x2)

2.3 多变量链式法则(Multivariable Chain Rule)

当中间变量有多个时,链式法则需要求和。若 z=f(u1,u2,,um),且每个 ui=gi(x1,x2,,xn),则:

zxj=i=1mzuiuixj

示例z=f(u,v) 其中 u=x2yv=xsiny

zx=zuux+zvvx=zu(2xy)+zvsinyzy=zuuy+zvvy=zu(x2)+zv(xcosy)

2.4 向量链式法则(Vector Chain Rule)— 面向 ML

在机器学习中,我们处理的通常是向量和矩阵。记 xRny=f(x)Rmz=g(y)R,则:

xz=(yx)Tyz

其中 yxJacobian 矩阵(雅可比矩阵),大小为 m×n

yx=[y1x1y1x2y1xny2x1y2x2y2xnymx1ymx2ymxn]

具体到神经网络的一个全连接层h=Wx+b,后续有损失 L

已知 Lh,需要求 LWLx

  • LW=LhxT (形状:输出梯度 × 输入转置)
  • Lx=WTLh (形状:权重转置 × 输出梯度)
  • Lb=Lh (偏置梯度等于输出梯度)

记下这三条公式,它们几乎是所有神经网络反向传播的"积木"。

2.5 计算图直观(Computation Graph)

计算图(Computation Graph) 将链式法则可视化:

输入 x → [线性层] → h → [激活函数] → a → [损失函数] → L
           ↑                      ↑
           W, b                  参数

反向传播方向(从右向左):

dL/dW = dL/da · da/dh · dh/dW
dL/dx = dL/da · da/dh · dh/dx      ← 继续往前传

每个方块都是"局部导数"的乘积,这就是链式法则的图形化表达。

2.6 完整示例:线性回归的梯度

假设我们有 MSE 损失 L=12(yy^)2,其中 y^=wx+b

  1. 正向传播(Forward Pass)

    • y^=wx+b
    • L=12(yy^)2
  2. 反向传播(Backward Pass)

    • Ly^=y^y (残差)
    • Lw=Ly^y^w=(y^y)x
    • Lb=Ly^y^b=y^y

这正好是下一节梯度下降中要用到的梯度公式。


3. 梯度下降法(Gradient Descent)

3.1 核心思想

梯度下降(Gradient Descent) 通过反复沿负梯度方向更新参数来最小化目标函数:

wt+1=wtηL(wt)

其中 η学习率(Learning Rate),控制每一步的步长。

为什么是负梯度? 因为梯度指向函数值增加最快的方向(最陡上升),所以向反方向移动就是最陡下降(Steepest Descent)。

3.2 从泰勒展开推导(Taylor Expansion Derivation)

为什么沿负梯度方向能保证损失下降? 用**一阶泰勒展开(First-order Taylor Expansion)**来理解:

L(w+Δw)L(w)+L(w)TΔw

我们希望 L(w+Δw)<L(w),即需要 L(w)TΔw<0

Δw=ηL(w(取负梯度方向),其中 η>0

L(w)T(ηL(w))=ηL(w)20

L(w)>0(不在极值点)时,上式严格小于 0,即损失一定下降

这就是梯度下降的数学保证——前提是学习率 η 足够小使得泰勒近似成立。

3.3 收敛性证明概要(Convergence Proof Sketch)

对于 凸函数(Convex Function)(例如 MSE 损失),梯度下降有收敛性保证。

假设条件

  1. L 是凸函数
  2. L 的梯度是 L-Lipschitz 连续的(梯度变化速度有上限):L(w)L(v)Lwv
  3. 最优值 L(w) 存在且有下界

证明思路(核心三步):

Step 1 — 二次上界:由 L-Lipschitz 梯度可推导出:

L(wt+1)L(wt)+L(wt)T(wt+1wt)+L2wt+1wt2

Step 2 — 代入梯度下降更新wt+1=wtηL(wt)

L(wt+1)L(wt)ηL(wt)2+Lη22L(wt)2=L(wt)η(1Lη2)L(wt)2

η1L 时,1Lη212>0,每次迭代损失严格下降。

Step 3 — 收敛速率:经过 T 次迭代后:

L(wT)L(w)w0w22ηT

这意味着梯度下降以 O(1/T) 的速率收敛到最优值。迭代次数越多,离最优解越近。

直观理解:如果你每次都朝最陡的下坡方向走一步,并且步长合适,你最终一定会走到山谷底部。凸函数保证只有一个山谷,不会陷入局部最优。

3.4 学习率的关键作用(Learning Rate)

学习率 η 是梯度下降最重要的超参数(hyperparameter /ˈhaɪpərpəˈræmɪtər/)。它的影响可以通过下图理解:

损失 L(w)

│  ╱
│ ╱   η 太小:收敛慢,需要很多步
│╱    →→→→→→→→→→→→→→→

│       η 合适:快速稳定下降
│       →→→→→→→→→→

│   η 太大:震荡甚至发散
│   ←→←→←→←→←→←→←→←→
└───────────────────────── w

经验法则

  • η=0.010.001 是常见起点
  • 观察损失曲线:平稳下降 → 可以尝试更大 η;震荡或发散 → 需要减小 η
  • 实践中常用学习率调度(Learning Rate Schedule):开始时较大,后期逐步减小

3.5 三种梯度下降变体

变体每次更新使用的样本数特点
批量梯度下降(BGD)全部样本稳定但慢,不适合大数据集
随机(stochastic /stəˈkæstɪk/)梯度下降(SGD)1 个样本快但有噪声,收敛路径曲折
小批量梯度下降(Mini-batch GD)32/64/128 个样本实践中默认选择,平衡速度与稳定性

4. 常用优化器直觉(Optimizer Intuition)

这一节不深入数学推导,只建立直觉。每个优化器用一段话解释:它解决了什么问题关键超参数

4.1 SGD(Stochastic Gradient Descent)

解决的问题:最基本的梯度下降方法。每次从训练集中随机选一个(或一小批)样本来计算梯度并更新参数。

直觉:就像蒙着眼睛下山——你只能通过脚下这一小块地方感受坡度,然后迈一步。路径可能很曲折(因为有噪声),但方向总体是对的。

关键超参数lr(学习率)——唯一需要调的参数。

wt+1=wtηLi(wt)

其中 Li 是第 i 个样本的损失。

4.2 Momentum(动量法)

解决的问题:SGD 在峡谷地形(一个方向陡、另一个方向缓)中会剧烈震荡。动量(momentum /məˈmentəm/)法通过累积历史梯度来平滑更新方向。

直觉:想象一个小球从山坡上滚下来——它不会在每个凹凸处都急转弯,而是靠着惯性(动量)继续沿主方向前进。动量项 β 控制了保留多少"历史速度":β=0.9 表示保留 90% 的上一步方向,再加 10% 的当前梯度修正。

关键超参数

  • lr:学习率
  • momentumβ):通常取 0.9 或 0.99
vt+1=βvt+L(wt)wt+1=wtηvt+1

Momentum 让 SGD 在"正确的方向"上加速,在"震荡的方向"上抵消。

4.3 Adam(Adaptive Moment Estimation)

解决的问题:不同参数可能需要不同的学习率——频繁更新的特征应该用小步长,稀疏特征应该用大步长。Adam 为每个参数自适应地调整学习率

直觉:Adam = Momentum + RMSProp。它同时维护两样东西:

  1. 梯度的一阶矩(均值) → 相当于动量,判断方向
  2. 梯度的二阶矩(方差) → 判断这个参数的历史梯度波动大小,波动大则缩小步长,波动小则放大步长

这就像一个智能的登山者,不仅知道"该往哪个方向走"(动量),还知道"这个方向的可信度有多高"(自适应学习率)。

关键超参数

  • lr:学习率(默认 0.001 通常工作良好)
  • betas:(β1,β2) —— 一阶和二阶矩的衰减率,默认 (0.9, 0.999)
  • eps:防止除零的小常数,默认 108

Adam 是目前最常用的优化器,通常不需要调参就能获得不错的结果。它是深度学习的"默认选项"。

4.4 优化器对比总结

优化器自适应学习率抗震荡内存开销适用场景
SGD简单任务、调参高手
SGD + Momentum大部分 CV 任务
Adam中(2x 参数内存)默认首选,NLP、推荐、多模态
AdamWAdam + 正确的权重衰减,Transformer(/trænsˈfɔːrmər/) 首选

何时用 SGD 而非 Adam? 当你有足够的调参经验和算力时,精心调优的 SGD + Momentum 在某些任务(如图像分类(classification /ˌklæsɪfɪˈkeɪʃən/))上可能略优于 Adam。但对初学者而言,Adam 永远是更安全的选择


4.5 代码精读:从梯度计算到优化器实现

把§1—§4 的数学直接写成代码,是理解"梯度下降到底在做什么"最好的方式。

python
import numpy as np

# ── 1. 手算梯度 vs 数值梯度验证(§2.6 的代码还原)──────────────
def mse_loss(w, X, y):
    """均方误差损失 L(w) = (1/n) ||Xw - y||²"""
    pred = X @ w
    return np.mean((pred - y) ** 2)

def analytic_grad(w, X, y):
    """解析梯度 ∇L(w) = (2/n) Xᵀ(Xw - y)"""
    n = len(y)
    return (2 / n) * X.T @ (X @ w - y)

def numerical_grad(w, X, y, eps=1e-5):
    """数值梯度(有限差分):∂L/∂wᵢ ≈ [L(w+ε*eᵢ) - L(w-ε*eᵢ)] / 2ε"""
    grad = np.zeros_like(w)
    for i in range(len(w)):
        w_plus  = w.copy(); w_plus[i]  += eps
        w_minus = w.copy(); w_minus[i] -= eps
        grad[i] = (mse_loss(w_plus, X, y) - mse_loss(w_minus, X, y)) / (2 * eps)
    return grad

# 生成简单数据:y = 2*x1 + 3*x2 + 噪声
rng = np.random.default_rng(0)
X = rng.standard_normal((50, 2))
y = X @ np.array([2.0, 3.0]) + rng.standard_normal(50) * 0.1

w = rng.standard_normal(2)
ag = analytic_grad(w, X, y)
ng = numerical_grad(w, X, y)
rel_err = np.abs(ag - ng).max() / (np.abs(ag).max() + 1e-8)
print(f"解析梯度: {ag}")
print(f"数值梯度: {ng}")
print(f"相对误差: {rel_err:.2e}")   # 应 < 1e-5,说明推导正确

梯度下降的三种形式(§3.5)

python
def sgd_variants(X, y, n_epochs=200, lr=0.1, batch_size=None):
    """batch_size=None→全批量GD;=1→随机GD;其他→mini-batch SGD"""
    n, d = X.shape
    w = np.zeros(d)
    losses = []

    for epoch in range(n_epochs):
        if batch_size is None:                  # 全批量 GD:用全部数据算梯度
            idx = np.arange(n)
        elif batch_size == 1:                    # 随机 GD:每次随机 1 个样本
            idx = rng.integers(0, n, size=1)
        else:                                    # mini-batch SGD:随机 batch
            idx = rng.choice(n, size=batch_size, replace=False)

        X_b, y_b = X[idx], y[idx]
        grad = analytic_grad(w, X_b, y_b)
        w -= lr * grad                           # §3.1 的更新公式 w ← w - η∇L
        losses.append(mse_loss(w, X, y))        # 记录全集 loss 用于观察收敛

    return w, losses

w_gd,  loss_gd  = sgd_variants(X, y, batch_size=None)
w_sgd, loss_sgd = sgd_variants(X, y, batch_size=1)
w_mb,  loss_mb  = sgd_variants(X, y, batch_size=16)
print(f"GD 最终权重: {w_gd}")    # 应接近 [2.0, 3.0]

从零实现 Adam(§4.3)

python
def adam(X, y, n_epochs=200, lr=1e-2, beta1=0.9, beta2=0.999, eps=1e-8):
    """
    Adam 优化器:
    m ← β₁ m + (1-β₁) g         # 一阶矩:梯度的指数加权平均(Momentum)
    v ← β₂ v + (1-β₂) g²        # 二阶矩:梯度平方的指数加权平均(自适应方差)
    m̂ = m / (1-β₁ᵗ)              # 偏差修正:t=1 时 m̂=g(不被 0 初始化拉低)
    v̂ = v / (1-β₂ᵗ)
    w ← w - lr * m̂ / (√v̂ + ε)   # 每个参数有自己的有效学习率
    """
    n, d = X.shape
    w = np.zeros(d)
    m = np.zeros(d)   # 一阶矩估计(初始为 0,需要偏差修正)
    v = np.zeros(d)   # 二阶矩估计(初始为 0)

    for t in range(1, n_epochs + 1):
        idx = rng.choice(n, size=16, replace=False)
        g = analytic_grad(w, X[idx], y[idx])   # mini-batch 梯度

        m = beta1 * m + (1 - beta1) * g         # 一阶矩更新
        v = beta2 * v + (1 - beta2) * g ** 2    # 二阶矩更新

        m_hat = m / (1 - beta1 ** t)            # 偏差修正
        v_hat = v / (1 - beta2 ** t)

        w -= lr * m_hat / (np.sqrt(v_hat) + eps)   # 参数更新

    return w

w_adam = adam(X, y)
print(f"Adam 最终权重: {w_adam}")   # 应接近 [2.0, 3.0]

v_hat(二阶矩)的直觉:如果某个参数的梯度长期很大,v 就大,有效学习率 lr/v 就小——自动降低"梯度大"参数的学习率,防止震荡。这就是 §4.3 "自适应学习率"的代码体现。


5. 本章小结

概念要点
偏导数固定其他变量,只对一个变量求导,衡量该方向的变化率
梯度所有偏导数组成的向量,指向最陡上升方向
链式法则Lw=Ly^y^w,反向传播的数学基础
梯度下降wt+1=wtηL(wt),沿负梯度方向更新
学习率 η步长控制——太小收敛慢,太大可能发散
Momentum累积历史梯度,平滑更新方向
Adam自适应学习率 + 动量,默认首选优化器

6. 进一步阅读


下一章:第4章 — 信息论


配套代码:gradient_descent_demo.py — 从零实现梯度下降,可视化损失曲线、参数轨迹和学习率对比