深度学习入门站 · 理解深度学习
首页 › 第 6 章

训练模型

梯度下降怎么走下山?批次、学习率、动量、Adam 优化器。
图 6-1:模型训练循环(前向—损失—反向—更新)
图 6-1:模型训练循环(前向—损失—反向—更新)

这一章讲的是怎么把"下山"走好。上一章我们有了损失函数这张"地形图",这一章的任务就是想办法走到地图的最低点——也就是找到让损失最小的那组参数。学完这一章,你会搞懂梯度下降、随机梯度下降(SGD)、动量(Momentum)和 Adam 这些天天听到的优化器到底在做什么,还会学会盯着损失曲线判断训练是否健康。

下山:梯度下降

第 5 章的损失函数 $L(\phi)$ 可以画成一张以参数为坐标的"地形图":每个参数组合对应一个损失值,把损失当成海拔,就得到一个坑坑洼洼的曲面。训练的目标,就是找到海拔最低的那个点:

$$\hat{\phi} = \arg\min_{\phi} L(\phi)$$

怎么找?最朴素的办法是梯度下降(Gradient Descent)。站在当前点,用数学算出"哪个方向上升最快",那就是梯度;我们反着走,也就是沿最陡的下坡方向迈一小步。每走一步,就重新测一次坡,再走一步,直到坡变平、走不动为止。写成公式:

$$\phi \leftarrow \phi - \alpha \cdot \frac{\partial L(\phi)}{\partial \phi}$$

这里的 $\alpha$ 叫学习率(learning rate),就是一步迈多大。步子太小,走得磨磨蹭蹭,半天到不了山脚;步子太大,可能一脚踩空、冲过头,在山谷两边来回震荡,甚至越走越高、直接"滚出地图"(损失爆炸)。学习率是训练里第一个要小心拧的旋钮。

⚠️
**学习率太小**:训练极慢,半天不见进展。
**学习率太大**:损失震荡甚至越来越大,训练直接失败。如果看到损失曲线像心电图一样乱跳,第一反应就是把学习率调小。

还有个现实问题:神经网络的"地形"不是光滑的大碗,而是有无数局部最小值(小坑)和鞍点(像马鞍一样,一个方向向下、另一个方向向上的平缓地带)。在这些地方梯度都是零,算法会以为到"底"了,其实可能只是卡在了半山腰。

⛰️
生活类比
想象你蒙着眼睛下山:每走一步,先用脚探一探哪个方向最陡、朝下,就往那个方向迈一步,走完再探。这就是梯度下降。步长(学习率)就是迈步大小——步子太大容易冲进沟里爬不出来,步子太小天黑了还困在山顶。而"局部最小值"就像是路上碰到的浅坑,蒙着眼的话你可能觉得"脚底平了,到平地了",其实离真正的山脚还远着呢。

随机梯度下降:一次只看一小批

前面说的梯度下降有个致命缺点:每一步算梯度都要把全部训练样本过一遍,数据一多就慢得离谱。改进思路很朴素——每次只看一小部分数据。随机梯度下降(Stochastic Gradient Descent,SGD)在每一步随机抽出一小批样本(叫小批量,minibatch,或简称批次),只用这批样本估算梯度就更新参数:

$$\phi_{t+1} \leftarrow \phi_t - \alpha \cdot \sum_{i \in \mathcal{B}_t} \frac{\partial \ell_i(\phi_t)}{\partial \phi}$$

其中 $\mathcal{B}_t$ 是第 $t$ 步抽到的批次(一批样本的下标集合),$\ell_i$ 是第 $i$ 个样本的损失。把训练数据全部过一遍(每个样本都被用到一次)叫做一个周期(epoch)。

有人会担心:用一小批估算的梯度不准啊?其实不准确反而是优点。这个"噪声"让 SGD 的路径歪歪扭扭,但也让它可能从浅坑里"颠"出来,跳到另一个更低的山谷;而老老实实的全量梯度下降一旦进坑就再也出不来。再加上每步计算便宜,SGD 就成了训练神经网络的事实标配。

动量:给小球加个惯性

SGD 还有个毛病:路径歪歪扭扭,在狭窄的山谷里会来回震荡,走得很慢。改进思路来自物理直觉——动量(Momentum)。想象一个球从山坡滚下:它有惯性,方向不会突变;遇到小坑,凭惯性能直接冲过去。算法里我们维护一个"速度" $m$,每步把上一步的速度和当前梯度混合起来,再用这个速度更新参数:

$$m_{t+1} \leftarrow \beta \cdot m_t + (1-\beta) \sum_{i \in \mathcal{B}_t} \frac{\partial \ell_i(\phi_t)}{\partial \phi}, \qquad \phi_{t+1} \leftarrow \phi_t - \alpha \cdot m_{t+1}$$

系数 $\beta$(常取 0.9)决定"记住多少过去的方向":如果梯度方向老是变来变去,新旧方向互相抵消,路径就被抹平了,震荡大大减小;如果梯度一直朝着同一个方向(比如一路下坡),惯性会让有效步长变大,下山更快。

💡
**核心要点**:优化器的本质,就是回答"下一步往哪走、走多远"。
梯度下降——看完全部数据,走最陡下坡;SGD——只看一小批,快,还能颠出浅坑;动量——像带惯性的球,平滑路径、加速下山。下一节的 Adam 则是把这些思路全揉在一起,还顺手解决了"步长怎么定"的问题。

Adam:自动给每个参数配步长

固定步长的梯度下降有个尴尬:地形陡的方向,同样一步会迈得太大(震荡);地形缓的方向,又迈得太小(磨蹭)。理想情况是给每个参数单独配一个合适的步长。Adam(Adaptive Moment Estimation,自适应矩估计)正是这么做的:它同时维护两个统计量——梯度的滑动平均 $m$(代表方向)和平方梯度的滑动平均 $v$(代表尺度),然后用"方向除以尺度"的方式更新:

$$\phi_{t+1} \leftarrow \phi_t - \alpha \cdot \frac{m_{t+1}}{\sqrt{v_{t+1}} + \epsilon}$$

$\epsilon$ 是个极小的常数,防止除数为零。直观地说:哪个方向梯度一直很大,就自动把步子调小一点;哪个方向梯度一直很小,就放大步子。因为训练初期滑动平均还不准,Adam 还会做一个偏差校正。它的好处是对学习率不敏感:一个默认的学习率往往就能跑得不错,不用费心设计复杂的学习率方案。所以 Adam 成了如今最常用的默认优化器。

用 PyTorch 训练时,换优化器只是换一行代码的事:

import torch

model = torch.nn.Linear(1, 1)         # 一个线性模型 y = w*x + b
loss_fn = torch.nn.MSELoss()          # 均方误差损失
x = torch.tensor([[1.0], [2.0], [3.0], [4.0]])
y = torch.tensor([[3.0], [5.0], [7.0], [9.0]])

# 换优化器只需改这一行:
# SGD 带动量(惯性) vs Adam(自适应步长)
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
# optimizer = torch.optim.Adam(model.parameters(), lr=0.01)

for epoch in range(200):
    optimizer.zero_grad()             # 清零上一次的梯度
    loss = loss_fn(model(x), y)       # 前向:预测并算损失
    loss.backward()                   # 反向:算梯度
    optimizer.step()                  # 优化器按规则更新参数

监控训练:看损失曲线,学会早停

训练的时候,最好把每个周期结束时的损失值画成曲线,一边训一边看,这比盯着日志里的数字直观得多。正常的曲线是一路下降、最后变平。如果曲线乱跳或上升,多半是学习率太大;如果下降得极慢,多半是学习率太小(或者模型该换更复杂的了)。

还要留一个心眼:训练损失在降,不代表模型真的变好了。如果验证集(没参与训练的数据)上的损失先降后升,而训练损失还在降,说明模型开始把训练数据的细节背下来了——过拟合的苗头。这时候最省事的对策就是早停(Early Stopping):一旦发现验证损失连续几个周期不再下降(甚至回升),就立刻停手,把之前验证损失最低的那份参数拿出来用。它像是训练里的一颗"刹车",帮你恰好停在泛化最好的时刻。

🧭
**日常训练口诀**:损失不降 → 调大学习率或加动量;损失乱跳 → 调小学习率;训练损失降、验证损失升 → 早停。监控损失曲线是每个炼丹师的必修课。

🎯 小测验

1. 随机梯度下降(SGD)与普通梯度下降最大的区别是什么?
2. 训练时损失曲线像"心电图"一样剧烈震荡甚至上升,最可能的原因是什么?
3. 动量(Momentum)为什么能让 SGD 收敛更快、更稳?