image.png

摘要:有了损失函数和梯度下降,我们已经知道模型如何“朝着 loss 变小的方向更新参数”。但真实的大模型训练远比手写梯度下降复杂:loss 曲面不平坦,学习率会影响稳定性,梯度可能爆炸,优化器不同会导致收敛速度和泛化效果差异很大。本文从开发者熟悉的调参和训练日志出发,解释最优化在大模型训练中的作用,并用 NumPy 对比 SGD、Momentum、Adam 和 AdamW 的直觉。

前置知识:第 3 篇梯度下降,第 4 篇矩阵运算
阅读时间:约35分钟
代码环境:Python 3.10+,numpy >= 1.24,matplotlib >= 3.7

入门导读:先抓住主线

如果你是第一次读“最优化理论”,先不要把它理解成高深数学。把它放到训练工程里理解:我们有一个 loss,要在海量参数空间里找到一组足够好的参数;优化器决定每一步怎么走,学习率决定步子多大,稳定性技巧决定训练能不能走到最后。本文的重点是理解“为什么训练不是简单地一直梯度下降”。

读完先达到这个程度就够了:

  • 能解释优化器和普通梯度下降的区别;
  • 能理解学习率、动量、Adam、AdamW 分别解决什么问题;
  • 能知道梯度裁剪和 warmup 为什么常见;
  • 能从训练日志里判断 loss 发散、震荡或过拟合的基本信号。

带着这 3 个问题读:

  1. 为什么同一个模型和数据,换优化器可能结果差很多?
  2. 为什么大模型训练通常不用固定学习率从头跑到尾?
  3. 训练不稳定时,应该先怀疑哪些优化相关因素?

一、优化到底在优化什么

训练模型时,我们最终想找到一组参数 θ \theta θ,让损失函数 L ( θ ) L(\theta) L(θ) 尽可能小。

写成数学形式:

θ ∗ = arg ⁡ min ⁡ θ L ( θ ) \theta^*=\arg\min_{\theta} L(\theta) θ∗=argθmin​L(θ)

这行公式读作:找到一组最优参数 θ ∗ \theta^* θ∗,使 loss 最小。

但在深度学习里,“最小”通常不是严格的全局最优。大模型参数太多,loss 曲面太复杂,我们真正追求的是:在有限算力、有限时间和有限数据下,找到一组泛化效果足够好的参数。

从工程角度看,优化不是一件孤立的数学事,它会直接影响:

  • loss 是否稳定下降;
  • 是否出现 loss spike;
  • 训练速度快不快;
  • 最终模型是否过拟合;
  • 同样预算下模型能力能到什么水平。

二、loss 曲面:为什么不是一路下山那么简单

在第 3 篇里,我们用 f ( x ) = x 2 f(x)=x^2 f(x)=x2 理解梯度下降。那是一个非常简单的碗形曲面,只有一个最低点。但神经网络的 loss 曲面复杂得多。

它可能包含:

  • 局部低点:附近都更高,但不一定是全局最低;
  • 鞍点:某些方向像低点,另一些方向还能继续下降;
  • 平坦区域:梯度很小,模型学得很慢;
  • 陡峭区域:梯度很大,参数更新容易不稳定。

所以真实训练不是“沿着梯度走就完了”。优化器要处理噪声、曲率、尺度差异和稳定性问题。


三、SGD:最基础的更新规则

SGD(Stochastic Gradient Descent,随机梯度下降)是最基础的优化方法。它每次用一个 batch 的数据估计梯度,然后更新参数:

θ t + 1 = θ t − η g t \theta_{t+1}=\theta_t-\eta g_t θt+1​=θt​−ηgt​

其中:

  • θ t \theta_t θt​ 是当前参数;
  • g t g_t gt​ 是当前 batch 计算出的梯度;
  • η \eta η 是学习率;
  • 更新方向是梯度反方向。

SGD 简单、直观,但问题也明显:batch 梯度有噪声,更新方向可能抖动;不同参数的梯度尺度可能差很多;学习率不好选。

import numpy as np

# 一个简单函数:f(x) = (x - 3)^2,最低点在 x=3
def loss_fn(x):
    return (x - 3) ** 2

def grad_fn(x):
    return 2 * (x - 3)

x = -5.0
lr = 0.1

for step in range(10):
    grad = grad_fn(x)
    x -= lr * grad
    print(f"step={step}, x={x:.4f}, loss={loss_fn(x):.4f}")

这个例子很顺利,因为函数太简单。真实大模型训练的梯度会有噪声,loss 曲面也不会这么规整。


四、Momentum:让更新带一点惯性

动量(Momentum)解决的是 SGD 更新方向抖动的问题。它会累计过去的梯度方向,让参数更新更像“带惯性的运动”。

公式可以写成:

v t = β v t − 1 + g t v_t=\beta v_{t-1}+g_t vt​=βvt−1​+gt​

θ t + 1 = θ t − η v t \theta_{t+1}=\theta_t-\eta v_t θt+1​=θt​−ηvt​

其中:

  • v t v_t vt​ 是累计的速度;
  • β \beta β 控制保留多少历史方向;
  • g t g_t gt​ 是当前梯度。

直觉上,如果很多步都指向相似方向,Momentum 会加速;如果梯度来回抖动,动量会抵消一部分噪声。

def optimize_momentum(x0=-5.0, lr=0.1, beta=0.9, steps=10):
    x = x0
    v = 0.0
    for step in range(steps):
        g = grad_fn(x)
        v = beta * v + g
        x -= lr * v
        print(f"step={step}, x={x:.4f}, loss={loss_fn(x):.4f}")

optimize_momentum()

Momentum 在很多传统深度学习任务里非常有效。后来的 Adam 也继承了“动量”的思想。


五、Adam:给每个参数自适应学习率

Adam(Adaptive Moment Estimation)是深度学习中最常见的优化器之一。它同时维护两个量:

  • 一阶矩估计:类似 Momentum,记录梯度方向的滑动平均;
  • 二阶矩估计:记录梯度平方的滑动平均,用来估计梯度尺度。

简化理解:Adam 会让每个参数拥有自己的自适应学习率。梯度经常很大的参数,步子会被压小;梯度经常很小的参数,步子相对不会太小。

这对大模型很重要,因为不同层、不同矩阵、不同参数的梯度尺度差异非常大。如果所有参数共用同样的更新尺度,训练可能很不稳定。

下面是一个简化版 Adam,用来理解核心思想:

def optimize_adam(x0=-5.0, lr=0.1, beta1=0.9, beta2=0.999, steps=10):
    x = x0
    m = 0.0
    v = 0.0
    eps = 1e-8
    for t in range(1, steps + 1):
        g = grad_fn(x)
        m = beta1 * m + (1 - beta1) * g
        v = beta2 * v + (1 - beta2) * (g ** 2)

        # bias correction:修正初期估计偏小的问题
        m_hat = m / (1 - beta1 ** t)
        v_hat = v / (1 - beta2 ** t)

        x -= lr * m_hat / (np.sqrt(v_hat) + eps)
        print(f"step={t}, x={x:.4f}, loss={loss_fn(x):.4f}")

optimize_adam()

第一遍不需要记住 Adam 的完整公式。你只要知道:Adam 比 SGD 多记录了梯度历史和梯度尺度,因此在复杂神经网络里通常更容易稳定训练https://weibo.com/ttarticle/p/show?id=2309405363587312123943


六、AdamW:为什么大模型常用它

Adam 有一个常见搭档:权重衰减(weight decay)。权重衰减用于防止参数无限变大,帮助泛化。

传统做法会把 L2 正则项混进梯度里,但在 Adam 这种自适应优化器里,这种耦合方式可能不理想。AdamW 的核心改动是:把权重衰减从梯度更新里解耦出来。

简化写法:

θ t + 1 = θ t − η ⋅ A d a m U p d a t e − η λ θ t \theta_{t+1}=\theta_t-\eta \cdot AdamUpdate-\eta\lambda\theta_t θt+1​=θt​−η⋅AdamUpdate−ηλθt​

这里 λ \lambda λ 是 weight decay 系数。最后一项会让参数按比例变小。

用大白话说:AdamW 一边按 Adam 的方式处理梯度,一边单独做权重衰减,不把两件事搅在一起。现代 Transformer 训练和微调里,AdamW 是非常常见的默认选择。


七、学习率调度:为什么不能固定一个 lr 跑到底

学习率是训练中最敏感的超参数之一。大模型训练通常不会从头到尾使用固定学习率,而是使用调度策略。

常见策略是 warmup + cosine decay:

  1. 训练初期,学习率从很小逐渐升高,避免模型刚开始就被大步更新冲坏;
  2. 到达峰值后,学习率按余弦曲线逐渐下降,让训练后期更细致地收敛。
import math

def lr_schedule(step, total_steps=100, warmup_steps=10, max_lr=1e-3):
    if step < warmup_steps:
        return max_lr * step / warmup_steps
    progress = (step - warmup_steps) / (total_steps - warmup_steps)
    return max_lr * 0.5 * (1 + math.cos(math.pi * progress))

for step in [0, 3, 10, 30, 60, 99]:
    print(f"step={step:02d}, lr={lr_schedule(step):.6f}")

warmup 尤其适合大模型训练,因为初始阶段参数和梯度都还不稳定。直接使用较大学习率,容易导致 loss spike 或训练崩掉。


八、梯度裁剪:防止一步走飞

训练过程中,有时某个 batch 会产生特别大的梯度。如果直接更新,参数可能发生巨大变化,loss 突然飙升。这就是训练日志里常见的 loss spike。

梯度裁剪(gradient clipping)会限制梯度范数不超过某个阈值。完整的分段公式是:

g c l i p p e d = { g , if  ∥ g ∥ ≤ m a x _ n o r m g ⋅ m a x _ n o r m ∥ g ∥ , if  ∥ g ∥ > m a x _ n o r m g_{clipped}=\begin{cases} g, & \text{if } \|g\|\le max\_norm \\ g\cdot\dfrac{max\_norm}{\|g\|}, & \text{if } \|g\|> max\_norm \end{cases} gclipped​=⎩ ⎨ ⎧​g,g⋅∥g∥max_norm​,​if ∥g∥≤max_normif ∥g∥>max_norm​

也就是说,只有当梯度范数超出阈值时,才把整个梯度向量等比例缩放到阈值大小;否则原样保留。这样做只改变梯度的模,不改变梯度的方向。

def clip_grad(g, max_norm=1.0):
    norm = np.linalg.norm(g)
    if norm > max_norm:
        return g * (max_norm / norm)
    return g

grad = np.array([10.0, 5.0, -3.0])
clipped = clip_grad(grad, max_norm=1.0)

print("原梯度范数:", np.linalg.norm(grad))
print("裁剪后范数:", np.linalg.norm(clipped))
print("裁剪后梯度:", clipped)

梯度裁剪不会让训练一定变好,但它能降低极端 batch 把训练带崩的风险。大模型训练和微调中非常常见。


九、训练日志该怎么看

优化理论最终要落到训练日志。下面是一些常见信号:

现象可能原因优先排查
loss 持续下降正常学习继续观察验证集
loss 震荡很大学习率过高、batch 太小、数据噪声大降 lr、增 batch、查数据
loss 突然 spike坏 batch、梯度爆炸、混合精度溢出梯度裁剪、查样本、查 AMP
训练 loss 降,验证效果差过拟合数据增强、正则、早停
loss 不动学习率太低、模型容量不足、数据/标签错误查 lr、查数据、查代码

如果你后面做微调,最重要的不是背优化器公式,而是能根据训练曲线判断问题方向。


十、一个实用默认配置

对很多大模型微调任务,一个常见起点是:

config = {
    "optimizer": "AdamW",
    "learning_rate": 2e-5,
    "weight_decay": 0.01,
    "warmup_ratio": 0.03,
    "lr_scheduler": "cosine",
    "max_grad_norm": 1.0,
}

这不是万能最优,只是一个比较稳的起点。实际项目里还要根据 base model、数据规模、任务类型、batch size 和 LoRA/full fine-tuning 方式调整。


常见误区

误区 1:优化器越新越好。
不是。优化器要看任务、模型和训练稳定性。AdamW 之所以常见,是因为它稳、成熟、生态支持好。

误区 2:训练不收敛就盲目加 epoch。
如果学习率、数据或 loss mask 有问题,训练更久只会浪费算力。

误区 3:loss 越低模型越好。
训练 loss 低不代表真实任务好。要看验证集、人工评估、事实性、安全性和成本。

误区 4:优化只是训练团队关心的事。
做应用和微调也必须懂优化,否则很难判断训练曲线是否健康。


总结

最优化理论回答的是:如何在复杂参数空间里找到足够好的模型参数。SGD 是基础更新规则,Momentum 缓解方向抖动,Adam 自适应调整每个参数的步长,AdamW 解耦权重衰减,warmup 和 cosine decay 控制训练节奏,梯度裁剪降低训练崩掉的风险。

第一遍记住主线即可:梯度告诉方向,优化器决定怎么走,学习率决定走多大一步,稳定性技巧保证别走飞。

大模型视角

后面讲预训练、SFT、LoRA、QLoRA、训练稳定性时,AdamW、warmup、cosine、gradient clipping 会反复出现。理解这些概念后,你看到训练配置不再只是复制参数,而能判断它们各自负责什么风险。

下一篇

机器学习三大范式:监督/无监督/强化学习概览 —— 数学基础到这里告一段落。下一篇开始进入机器学习范式,理解预训练、SFT、RLHF 分别属于什么学习方式.

Logo

更多推荐