优化:把损失曲面一步步压到谷底 本节摘要:训练神经网络,本质上就是「找到山谷的底部」。你手上有损失函数(告诉你模型有多错)和梯度(告诉你哪个方向更糟),现在需要一套「下坡策略」。最朴素的做法是沿梯度反方向走一步、步长由学习率缩放——这就是梯度下降,能用,但有陷阱:学习率太大冲出山谷来回弹跳,太小则爬行万步;撞上鞍点就停下来却未到极小。深度学习里的每一个优化器,都在回答同一个问题:如何更快、更稳地到达谷底?
本节摘要:训练神经网络,本质上就是「找到山谷的底部」。你手上有损失函数(告诉你模型有多错)和梯度(告诉你哪个方向更糟),现在需要一套「下坡策略」。最朴素的做法是沿梯度反方向走一步、步长由学习率缩放——这就是梯度下降,能用,但有陷阱:学习率太大冲出山谷来回弹跳,太小则爬行万步;撞上鞍点就停下来却未到极小。深度学习里的每一个优化器,都在回答同一个问题:如何更快、更稳地到达谷底? 本节从零实现原始梯度下降、SGD+动量、Adam 三大主力,在经典的 Rosenbrock 函数(「易找难跟的窄弯谷」)上对比收敛;讲清动量如何像滚球一样积累速度、抑制震荡;讲清 Adam 如何用一阶矩与二阶矩的滑动平均,给每个权重分配自适应学习率;区分凸与非凸损失、解释高维下鞍点才是真障碍;最后给出学习率调度(步阶、余弦退火、warmup)的实战配置。
对应原课程:Phase 01 · Lesson 08 ·
optimization(原英文phases/01-math-foundations/08-optimization/docs/en.md)。前置:第 4~5 节(导数、梯度、自动微分)。
阅读完本节,你应当能够:
你有损失函数(模型有多错)、有梯度(哪个方向更糟),现在需要一套下坡策略。
朴素做法:沿梯度反向走一步,步长由学习率缩放——这就是梯度下降,能用。但「能用」有前提:学习率太大冲出山谷来回弹跳,太小爬行万步,撞上鞍点就停下却没到极小。深度学习的每个优化器都在回答:如何更快更稳到谷底?
优化即「找到使函数最小(或最大)的输入值」。在 ML 里,函数是损失,输入是权重,训练即优化。
minimize L(w),其中 L=损失函数,w=模型权重(可能百万参数)
最简单的优化器:算损失对每个权重的梯度,把每个权重沿其梯度反方向移动,步长由学习率缩放。
w = w − lr · gradient
整个算法就这一行。
学习率控制步长,决定一切收敛行为。
没有公式告诉你正确学习率,靠实验找。常用起点:Adam 用 0.001,SGD+动量用 0.01。
| 变体 | 批大小 | 梯度质量 | 每步速度 | 噪声 |
|---|---|---|---|---|
| 批量 GD | 全数据集 | 精确 | 慢 | 无 |
| SGD | 1 样本 | 极噪声 | 快 | 高 |
| 小批量 | 32~256 | 好估计 | 平衡 | 中等 |
SGD 与小批量的噪声不是 bug,它帮助逃出浅局部极小与鞍点。
原始梯度下降只看当前梯度。若梯度在窄谷里来回锯齿,进展缓慢。动量把历史梯度累积成速度项:
v = β · v + gradient w = w − lr · v
类比滚下山的球:它不会在每个颠簸处停下重启,而是在一致方向上加速、在震荡方向上阻尼。β(典型 0.9)控制保留多少历史。
不同权重需要不同学习率。罕遇大梯度的权重,真遇到时该走大步;常遇巨大梯度的权重,该走小步。
Adam(自适应矩估计) 每个权重跟踪两件事:
m = β₁·m + (1−β₁)·gradient v = β₂·v + (1−β₂)·gradient² m̂ = m / (1−β₁ᵗ) 偏差校正 v̂ = v / (1−β₂ᵗ) 偏差校正 w = w − lr · m̂ / (√v̂ + ε)
除以 √v̂ 是关键洞见:大梯度的权重除以大数(有效步小),小梯度的权重除以小数(有效步大)。每个权重得到自己的自适应学习率。默认 lr=0.001, β₁=0.9, β₂=0.999, ε=1e-8,对多数问题都好用。
固定学习率是妥协:训练早期想大步快进,后期想小步精调。
| 调度 | 公式 | 用例 |
|---|---|---|
| 步阶衰减 | 每 N 轮 lr × factor | 简单、手动控制 |
| 指数衰减 | lr = lr₀ · decayᵗ | 平滑下降 |
| 余弦退火 | lr = lr_min + 0.5(lr_max−lr_min)(1 + cos(πt/T)) | Transformer、现代训练 |
| Warmup + 衰减 | 线性升温再衰减 | 大模型,防早期不稳 |
凸函数只有一个极小,梯度下降总能找到(如 f(x)=x²)。神经网络损失非凸,有许多局部极小、鞍点、平坦区。
实践中,高维神经网络的局部极小很少是问题——多数局部极小的损失接近全局极小。鞍点(某些方向极小、某些方向极大)才是真障碍。动量与小批量噪声帮助逃出。
损失是所有权重的函数。百万权重模型的损失景观活在 100 万+1 维空间。我们通过在权重空间随机选两个方向、沿它们切片画 2D 曲面来可视化。
💡 尖锐极小泛化差,平缓极小泛化好。这是 SGD+动量在最终测试精度上常胜过 Adam 的原因之一——它的噪声阻止陷入尖锐极小。
完整源码见 phases/01-math-foundations/08-optimization/code/。
经典优化基准,极小在 (1,1),位于「易找难跟」的窄弯谷中:
def rosenbrock(p): x, y = p return (1 - x)**2 + 100 * (y - x**2)**2 def rosenbrock_gradient(p): x, y = p df_dx = -2*(1 - x) + 200*(y - x**2)*(-2*x) df_dy = 200*(y - x**2) return [df_dx, df_dy]
class GradientDescent: def __init__(self, lr=0.001): self.lr = lr def step(self, params, grads): return [p - self.lr * g for p, g in zip(params, grads)]
class SGDMomentum: def __init__(self, lr=0.001, momentum=0.9): self.lr, self.momentum, self.velocity = lr, momentum, None def step(self, params, grads): if self.velocity is None: self.velocity = [0.0]*len(params) self.velocity = [self.momentum*v + g for v, g in zip(self.velocity, grads)] return [p - self.lr*v for p, v in zip(params, self.velocity)]
class Adam: def __init__(self, lr=0.001, beta1=0.9, beta2=0.999, epsilon=1e-8): self.lr, self.b1, self.b2, self.eps = lr, beta1, beta2, epsilon self.m = self.v = None; self.t = 0 def step(self, params, grads): if self.m is None: self.m = [0.0]*len(params); self.v = [0.0]*len(params) self.t += 1 self.m = [self.b1*m + (1-self.b1)*g for m, g in zip(self.m, grads)] self.v = [self.b2*v + (1-self.b2)*g**2 for v, g in zip(self.v, grads)] m_hat = [m / (1 - self.b1**self.t) for m in self.m] # 偏差校正 v_hat = [v / (1 - self.b2**self.t) for v in self.v] return [p - self.lr*mh / (vh**0.5 + self.eps) for p, mh, vh in zip(params, m_hat, v_hat)]
def optimize(optimizer, func, grad_func, start, steps=5000): params = list(start); history = [params[:]] for _ in range(steps): params = optimizer.step(params, grad_func(params)) history.append(params[:]) return history start = [-1.0, 1.0] for opt in [GradientDescent(lr=0.0005), SGDMomentum(lr=0.0001), Adam(lr=0.01)]: h = optimize(opt, rosenbrock, rosenbrock_gradient, start) print(f"final={h[-1]}, loss={rosenbrock(h[-1]):.8f}")
预期:Adam 收敛最快,SGD+动量路径更平滑,原始 GD 在窄谷里进展缓慢。
设计要点:Adam 的偏差校正
1−βᵗ在训练初期至关重要——否则 m、v 从 0 启动会被严重低估,导致前几步几乎不动。
实战中用 PyTorch 或 JAX 的优化器,它们处理参数组、权重衰减、梯度裁剪、GPU 加速:
import torch model = torch.nn.Linear(784, 10) sgd = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9) adam = torch.optim.Adam(model.parameters(), lr=0.001) adamw = torch.optim.AdamW(model.parameters(), lr=0.001, weight_decay=0.01) sched = torch.optim.lr_scheduler.CosineAnnealingLR(adam, T_max=100)
经验法则:
outputs/prompt-optimizer-guide.md:一个帮你选择正确优化器的提示。源码见 phases/01-math-foundations/08-optimization/code/。
[0.0001, 0.0005, 0.001, 0.005, 0.01] 跑原始 GD,打印 5000 步后损失,找最大仍收敛的学习率。[0.0, 0.5, 0.9, 0.99] 跑 SGD+动量,记录每步损失,哪个最快?哪个冲过头?f(x,y)=x²−y²(原点为鞍点),从 (0.01, 0.01) 出发,对比三种优化器谁能逃出。GradientDescent 加指数衰减调度 lr = lr₀·0.999^step,在 Rosenbrock 上比较有/无衰减。√v̂ 给每权重自适应学习率,偏差校正修复冷启动。下一节,我们换一种衡量「分布相似度」的方式——信息论:熵、交叉熵、KL 散度,以及它们如何统一分类损失、模型压缩与变分推断。