优化 优化是模型训练的数学核心——找到让损失函数最小的那些参数。本文件涵盖临界点、凸性、梯度下降、牛顿法、带拉格朗日乘子的约束优化,以及驱动现代深度学习的优化器(SGD、Adam)。 训练一个神经网络、拟合一条回归直线、调超参数:几乎每一个机器学习算法的核心都是一个优化(optimisation)问题。 我们手上有某个函数(一个损失、一个代价、一个目标),我们想找到让这个函数尽可能小(或尽可能大)的输入。 在开始优化之前,我们需要先理解函数的零点(zeros,或根 roots)。$f(x)$ 的一个零点就是满足 $f(x) = 0$ 的那个 $x$ 值。从图像上看,它们就是与 x 轴相交的点。
优化是模型训练的数学核心——找到让损失函数最小的那些参数。本文件涵盖临界点、凸性、梯度下降、牛顿法、带拉格朗日乘子的约束优化,以及驱动现代深度学习的优化器(SGD、Adam)。
训练一个神经网络、拟合一条回归直线、调超参数:几乎每一个机器学习算法的核心都是一个**优化(optimisation)**问题。
我们手上有某个函数(一个损失、一个代价、一个目标),我们想找到让这个函数尽可能小(或尽可能大)的输入。
在开始优化之前,我们需要先理解函数的零点(zeros,或根 roots)。f(x) 的一个零点就是满足 f(x) = 0 的那个 x 值。从图像上看,它们就是与 x 轴相交的点。
例如,f(x) = x^2 - 3x + 2 = (x-1)(x-2) 在 x = 1 和 x = 2 处有零点。在两个零点之间函数为负(f(1.5) = -0.25),在两个零点之外函数为正。零点把数轴划分成若干区间,每个区间里函数的符号保持不变。
一个零点的**重数(multiplicity)**是指对应的因子出现了多少次。
在单重零点(重数为 1)处,图像会穿过 x 轴。在双重零点(重数为 2)处,图像会碰到 x 轴但不穿过,而是反弹回来,在该点显得「平」。
找零点之所以重要,是因为导数 f'(x) 的零点正是 f(x) 的临界点(critical points)——也就是极大值和极小值的候选者。
在极大值或极小值处,切线是水平的(斜率为 0),所以 f'(x) = 0。
但并不是每个临界点都是极大值或极小值。f'(x) = 0 的点也可能是拐点(inflection point)(比如 f(x) = x^3 在 x = 0 处),函数在那里暂时变平,但并不改变方向。
**二阶导数判别法(second derivative test)**可以解决这个问题。在临界点 x = c 处(满足 f'(c) = 0):
例如,f(x) = x^3 - 3x。导数是 f'(x) = 3x^2 - 3 = 3(x-1)(x+1),所以临界点在 x = -1 和 x = 1。二阶导数是 f''(x) = 6x。在 x = -1 处:f''(-1) = -6 < 0(局部极大)。在 x = 1 处:f''(1) = 6 > 0(局部极小)。
如果函数图像上任意两点之间的连线都位于图像上方(或恰好落在图像上),就称这个函数是凸的(convex)。可以把它想象成一个碗的形状,处处向上弯曲。从数学上说,只要对所有 x 都有 f''(x) \geq 0,f 就是凸的。
凸性之所以强大,是因为凸函数有一个了不起的性质:每一个局部极小值同时也是全局最小值(global minimum)。没有那种会把你困住的、具有欺骗性的局部小谷底。如果你把一个球滚进一只凸的碗里,它一定会落到底部。
如果 -f 是凸的,那么 f 就是凹的(concave)(向下弯曲)。函数在凹凸之间切换的那些点就是拐点,出现在 f''(x) = 0 处。
**牛顿法(Newton's method)**利用切线来求函数的零点(相应地,也能求其导数的临界点)。从一个初始猜测 x_0 出发,它不断迭代修正:
思路是这样的:在 x_n 处画出切线,找出它与 x 轴的交点,这个交点就作为 x_{n+1}。对于性质良好、且初始点选得不错的函数,牛顿法收敛得非常快(是二次收敛,也就是每一步正确的小数位数大约翻倍)。
例如,求 \sqrt{5}(即 f(x) = x^2 - 5 的一个零点):f'(x) = 2x,所以 x_{n+1} = x_n - \frac{x_n^2 - 5}{2x_n}。从 x_0 = 2 开始:x_1 = 2.25,x_2 = 2.2361\ldots,已经精确到四位小数了。
如果初始猜测离根太远、在根附近 f'(x) = 0、或者函数附近有拐点,牛顿法都可能失败。它还需要计算导数,这有时代价不低。
用于优化(求极小值而非零点)时,我们对 f'(x) = 0 应用牛顿法,得到更新式:
在多维情况下,它变成 \mathbf{x}_{n+1} = \mathbf{x}_n - H^{-1} \nabla f(\mathbf{x}_n),其中 H 是海森矩阵。这其实就是上一个文件里那个二阶泰勒逼近在实战中登场:把函数近似成一个二次型,跳到那个二次型的最小值处,再重复。
拉格朗日乘子(Lagrange multipliers)用来求解约束优化(constrained optimisation):在满足约束 g(x, y) = c 的前提下,求 f(x, y) 的最优值。我们不再在整个 \mathbb{R}^n 上搜索,而是被限制在满足约束的那个集合上(一条曲线或一张曲面)。
关键的洞见是几何上的:在带约束的最优点处,f 的梯度必须与 g 的梯度平行。如果它们不平行,我们就能沿着约束方向找到一个还能进一步改进 f 的移动方向,那就说明还没到最优。
我们引入一个新的变量 \lambda(拉格朗日乘子),并定义拉格朗日函数(Lagrangian):
由第一个方程(假设 x \neq 0):\lambda = y。代入第二个方程:x^2 = 2y^2。结合约束:2y^2 + y^2 = 1,所以 y = \frac{1}{\sqrt{3}}。最大值为 f = \frac{2}{3\sqrt{3}}。
对于不等式约束(g(x,y) \leq c,而非 = c),Karush-Kuhn-Tucker(KKT)条件推广了拉格朗日乘子。此时约束要么是积极的(起作用,当作等式处理),要么是消极的(解落在内部,约束无关紧要)。
在实践中,我们极少手动去优化。下面是主要的几类算法:
一阶方法(first-order methods)(只用梯度):梯度下降、随机梯度下降(SGD)、Adam。每一步开销低,但收敛可能较慢,尤其是在条件数较差的问题上。
二阶方法(second-order methods)(用梯度和海森矩阵):牛顿法收敛快,但计算并求逆海森矩阵代价很高(对 n 个参数是 O(n^3))。拟牛顿法(quasi-Newton methods)(如 BFGS 和 L-BFGS)只用梯度信息来近似海森矩阵,在不承担二阶方法全部代价的前提下,获得了比一阶方法更快的收敛。
共轭梯度法(conjugate gradient):适用于大型稀疏系统,只用矩阵-向量乘积,而不必存储完整的海森矩阵。
Gauss-Newton 和 Levenberg-Marquardt:专门用于最小二乘问题(回归中很常见),通过雅可比矩阵来近似海森矩阵。
自然梯度下降(natural gradient descent):用 Fisher 信息矩阵来考虑参数空间的几何结构,对概率模型往往更有效。
优化器的选择取决于具体问题。对于深度学习,一阶方法(尤其是 Adam)占据主导,因为参数数量极其庞大(数百万到数十亿),计算海森矩阵并不现实。对于规模较小、目标函数光滑的问题,二阶方法可能快得多。
import jax.numpy as jnp f = lambda x: x**2 - 7 df = lambda x: 2*x x = 3.0 # 初始猜测 for i in range(6): x = x - f(x) / df(x) print(f"step {i+1}: x = {x:.10f} (error: {abs(x - jnp.sqrt(7.0)):.2e})")
import jax import jax.numpy as jnp def f(params): x, y = params return (x - 3)**2 + (y + 1)**2 grad_f = jax.grad(f) params = jnp.array([0.0, 0.0]) lr = 0.1 for i in range(20): g = grad_f(params) params = params - lr * g if i % 5 == 0 or i == 19: print(f"step {i:2d}: ({params[0]:.4f}, {params[1]:.4f}) loss={f(params):.6f}")
import jax import jax.numpy as jnp # 代入约束: y = 10 - x,于是 f = x(10 - x) = 10x - x² f = lambda x: x * (10 - x) df = jax.grad(f) # 梯度上升(我们要求最大值,所以加上梯度) x = 1.0 lr = 0.1 for i in range(20): x = x + lr * df(x) print(f"x={x:.4f}, y={10-x:.4f}, f={f(x):.4f}") # 应得到 x=5, y=5, f=25