28

SGD——最简单的下坡

阅读约 5 分钟第 28/60 课

上一个单元算出了梯度。这个单元来看怎么用梯度更新参数——也就是优化器(optimizer)。

梯度下降

最简单的更新规则:

参数_new = 参数_old − 学习率 × 梯度

梯度指向 loss 增大的方向,减去梯度就是往 loss 减小的方向走一步。学习率决定步长——每一步走多远。

这个方法叫梯度下降(Gradient Descent)。如果每一步只看一小批数据(而不是全部数据)来估计梯度,就叫随机梯度下降(Stochastic Gradient Descent,SGD)。

为什么只看一小批?因为 Sophia 有 200 亿个 token 的训练数据。如果每一步都在全部数据上算一次梯度,一步就需要做一次完整的前向和反向传播——几千个 GPU 小时才能走一步。实际训练需要走几万步,这完全不可行。SGD 每步只用几十万个 token(一个 batch),梯度是近似的,但更新频率高,总训练时间反而短得多。

用 Python 实现

SGD 的代码只需要两行:

for param in model.parameters():       # 遍历模型的每一个参数
    param.data -= learning_rate * param.grad   # 参数 -= 学习率 × 梯度

param.data 是参数的值,param.grad 是反向传播算出来的梯度。一个循环,所有参数各更新一次。

手动走几步

用第 25 章节的例子继续。假设一个两层的小网络:

输入 x = 2,目标 = 20
y = w₁ × x,z = w₂ × y,loss = (z − 20)²

初始值 w₁ = 3,w₂ = 4,学习率 = 0.001。

第 1 步:

前向:y = 3 × 2 = 6,z = 4 × 6 = 24,loss = (24−20)² = 16
梯度:∂loss/∂w₁ = 64,∂loss/∂w₂ = 48
更新:w₁ = 3 − 0.001 × 64 = 2.936
      w₂ = 4 − 0.001 × 48 = 3.952

第 2 步(用新的 w₁ 和 w₂):

前向:y = 2.936 × 2 = 5.872,z = 3.952 × 5.872 = 23.206
      loss = (23.206−20)² = 10.285
梯度:∂loss/∂w₁ = 2 × 3.206 × 3.952 × 2 = 50.67
      ∂loss/∂w₂ = 2 × 3.206 × 5.872 = 37.65
更新:w₁ = 2.936 − 0.001 × 50.67 = 2.885
      w₂ = 3.952 − 0.001 × 37.65 = 3.914

两步之后,loss 从 16 降到了 10.285——参数在朝着正确的方向移动。继续走下去,z 会越来越接近 20。

SGD 的问题

SGD 在这种简单问题上工作得很好(第 01 章节的房价例子就是 SGD)。但在训练语言模型这种复杂问题上,它有两个严重的缺陷。

问题一:方向不稳。 SGD 每一步的梯度只基于当前这一小批数据。不同的 batch 给出的梯度方向不完全一致——有时候指东,有时候指北,整体大致向目标走,但路径非常曲折。

想象一条狭长的山谷。SGD 的行为是:在山谷两壁之间来回弹跳,同时沿山谷方向缓慢推进。一步冲上左壁,下一步弹回右壁,再弹到左壁……实际走过的距离远超直线距离。这种锯齿形的路径浪费了大量计算。

SGD 的路径(俯视图,从 A 走到 B):

A ╲  ╱╲  ╱╲  ╱╲  ╱╲
   ╲╱  ╲╱  ╲╱  ╲╱  ╲→ B

问题二:所有参数用同一个学习率。 Sophia 有十亿个参数。有些参数的梯度始终很大(它们所在的 loss 曲面很陡峭),有些参数的梯度始终很小(曲面很平缓)。用同一个学习率,陡峭方向上步子太大容易跳过最优点,平缓方向上步子太小进展缓慢。

这两个问题的解决方案分别催生了 Momentum 和 Adam——接下来两个章节的主题。

下一章节先解决第一个问题:怎么让更新方向更稳定。