技术博客

从 SGD 的一行更新看深度学习的最小结构

第 17 / 147 篇 · 2025-08-10 · ai · SGD优化基础随机梯度

剥掉所有工程外衣,深度学习的训练循环只剩一行:

$$ \theta_{t+1}=\theta_t-\eta\,\hat g_t,\qquad \hat g_t=\frac{1}{B}\sum_{i\in\mathcal{B}_t}\nabla_\theta\ell(\theta_t;x_i). $$

这一行里的每个符号都不是可有可无的。把它逐件拆开——参数、损失、数据、步长——恰好是深度学习系统的最小完备集。少任何一件,整个机器就停转;多出来的东西(动量、自适应、调度)都是这一行的改装件。

先做一个轻量的 sanity check 确认它真的"最小":去掉数据,没有 $\hat g$ 可算;去掉损失,不知道往哪降;去掉参数,没有可改的东西;去掉 $\eta$,更新幅度失去控制。四件缺一不可,而常见的训练系统组件——tokenizer、框架、并行策略——全是围绕这四件的服务设施,不在结构本身里。

第一要素:标量损失

梯度只对标量输出有定义——内积视角那篇说过,微分作为线性泛函要"吐标量"。所以无论模型输出是 logits 张量还是生成序列,训练前必须把它压成一个数 $\ell$。这个压缩动作决定了模型学什么:交叉熵压出"预测贴近数据",对比损失压出"同类靠近异类远离"。

"压成标量"也划出了这套框架的边界:多目标优化里不想做加权仲裁的场景(比如 Pareto 前沿探索),一行式子本身就不够用,需要引入向量值梯度与方向选择机制。先认清最小结构的边界,才知道什么时候该越出它。

标量损失还有一个不常被提的角色:它是多目标冲突的仲裁所。大模型的训练损失里混着各种语言、各种任务、各种质量的信号,它们之所以能共存于一次更新,是因为先被加权压成了一个标量——梯度方向就是这场多目标谈判的结果。后来 RLHF 里的 reward + KL 加权,也是同一个"先压标量再求降"的结构。

工程上对应一个具体的教训:多 loss 混合训练时,各 loss 的量级差直接变成话语权。一个数值范围在 $10^2$ 的辅助 loss 和一个 $10^{-2}$ 的主 loss 相加,后者梯度被淹没,调权重系数本质是重新分配仲裁席的票数。发现"加了辅助 loss 没效果"时,先查两者的梯度范数差几个数量级,再谈加权。

第二要素:无偏的小批量梯度

为什么用 minibatch 而不是全量数据?全量梯度 $\nabla L=\frac1N\sum_{i=1}^N\nabla\ell_i$ 当然是"真"方向,但 $N$ 是十亿级 token 时算一次要扫完全部数据。minibatch 梯度是无偏估计:

$$ \mathbb{E}[\hat g]=\frac{1}{B}\sum_{i\in\mathcal B}\mathbb{E}[\nabla\ell_i]=\nabla L, $$

样本均匀抽取时等号严格成立。注意这里把扫全数据的代价换成了每步只摸 $B$ 个样本——这是用统计噪声换计算可行性的交易,整个随机优化大厦就建在这一次交换上。无偏性意味着长期平均方向不错,剩下的问题是方差。若单样本梯度的协方差为 $\Sigma$,i.i.d. 采样下

$$ \mathrm{Cov}[\hat g]=\frac{\Sigma}{B}, $$

方差随 batch 线性下降。这给出一条朴素账本:batch 扩大 $k$ 倍,梯度噪声缩小 $k$ 倍,但每步成本也涨 $k$ 倍——小 batch 区间里降噪声是划算的,大到一定程度后噪声已低、边际收益递减,这就是"临界 batch size"概念的来源(McCandlish et al. 2018 的 gradient noise scale 把这件事定量化:噪声尺度决定 $B$ 多大之后再加就浪费)。

注意这个账本是逐坐标成立的:不同参数方向的梯度噪声幅度不同,同一 $B$ 对各方向的降噪收益并不均摊。实践中观察到的"加大 batch 主要稳定了少数难训的分支"正源于此。

还有一个与硬件对齐的实践点:$\Sigma/B$ 这条账决定了"加卡"的收益曲线。数据并行把 $B$ 摊到多卡,线性区里吞吐随卡数涨、噪声同步降;过了临界 batch,再加卡只是平摊一个已经够准的梯度,通信开销反而开始吃净收益。压测训练吞吐时看到的"加速比拐点",底层就是这个噪声账本的拐点。

有一个细节常被一笔带过但值得较真:无偏的是每个参数的梯度估计,不是损失本身的任何非线性函数。$E[\hat g]=\nabla L$ 不蕴含 $E[\|\hat g\|]=\|\nabla L\|$(Jensen 不等式让它系统偏大),也不蕴含基于 $\hat g$ 的二阶矩估计无偏——Adam 的偏差修正项有一部分病根就在这儿。把"无偏"二字外推到任何梯度下游的统计量上,是读优化器论文时最常见的隐蔽跳跃。

噪声不全是坏事。它让更新方向不完全贴合当前点的最速下降,客观上起到探索作用;实验上全 batch 的梯度下降常常收敛到更尖锐、泛化更差的极小值。但要记住这是副作用级的收益,不是设计目标——想要正则化有专门的手段,把希望寄托在噪声上不可控。

第三要素:参数与步长——欧拉离散的视角

把更新式改写一下,先把学习率的语义从"超参数"还原为"离散步长":

$$ \frac{\theta_{t+1}-\theta_t}{\eta}=-\hat g_t\ \approx\ -\nabla L(\theta_t). $$

这是常微分方程 $\dot\theta=-\nabla L(\theta)$(梯度流)的前向欧拉离散,$\eta$ 就是时间步长。ODE 视角立刻回答两个问题:

  1. $\eta$ 为什么不能大:欧拉离散只在局部追随真实轨迹,步长超过系统"曲率时间尺度"就发散——二次问题上的精确判据是 $\eta<2/\lambda_{\max}(H)$,下一篇泰勒展开会推这个界。
  2. 训练轨迹是什么:它是梯度流轨迹的带噪折线逼近。loss 曲线的下降、平台、尖峰,对应流场的陡坡、鞍部与数值失稳,不是玄学。

ODE 视角还有一个实用推论:训练动力学研究(edge of stability、catapult 现象)全部建立在这个连续极限上——把离散更新当 ODE 分析、再把离散化误差当作扰动项,是理解"训练为什么会震荡、何时进入锐化区"的主流框架。

参数 $\theta$ 在这套结构里是唯一被"记住"的东西:数据流过就丢,梯度算完就弃,只有 $\theta$ 跨步存活,承载着全部学到的内容。训练就是在 $\theta$ 空间里走一条由数据与损失共同铺出的路径。

这个"唯一状态"属性还有个推论:训练的可复现性与断点续训语义都归结为对 $\theta$(外加优化器状态这份"轨迹动量")的管理。梯度累积、流水并行这些工程机制之所以可以自由组合,前提正是它们不引入新的跨步状态——最小结构里没给它们留位置,它们就必须把自己折叠回这一行式子。

SGD 最小结构:数据与参数在标量损失处汇合出梯度,欧拉步更新唯一跨步存活的 θ 数据 batch B 无偏采样,方差 Σ/B 参数 θₜ 唯一跨步状态 标量损失 ℓ(θ;x) 多目标压缩成一个数 ĝ = Σ∇ℓ/B E[ĝ]=∇L 无偏 θₜ₊₁ = θₜ − η·ĝ(欧拉步,η = 步长) 大循环:梯度流 ODE θ̇ = −∇L(θ) 的带噪离散轨迹

batch、学习率与梯度累积的耦合

三要素之外有一个工程性的第四耦合:$B$、$\eta$ 与梯度累积。显存装不下大 batch 时,把 $K$ 个 micro-batch 的梯度相加再平均后更新一次,数学上等价于大 batch——前提是归约是 mean 而不是 sum(广播归约那篇的尺度约定在这里又出现一次),且中间不能有依赖 batch 统计量的操作。BatchNorm 是最著名的例外:它的统计量随 micro-batch 大小而变,梯度累积与大批量的行为不再等价,这是 BN 在大模型训练里消失的原因之一,LayerNorm 没有这个问题。

$B$ 与 $\eta$ 的联动有理论依据:梯度噪声方差 $\Sigma/B$ 决定稳态游荡半径,线性缩放律(batch 乘 $k$,学习率乘 $k$)保持"信号/噪声比"恒定,在小到中等 batch 区间实测有效;大到临界 batch 之后失效,缩放转为 $\sqrt{k}$ 甚至不再缩放。这不是经验迷信:稳态分布的宽度正比于 $\eta\Sigma/B$,保持 $\eta/B$ 恒定即保持热平衡温度恒定——随机微分方程视角下,$\eta/B$ 扮演的正是温度的角色。

采样方式也有讲究:无替换的 epoch 内 shuffle(每个样本恰出现一次)比有替换的 i.i.d. 采样方差更小,实践上几乎总是更优,代价是打乱"每次采样独立"的干净理论。framework 的 DataLoader(shuffle=True) 默认走前者。

这行式子读出的设计判据

最小结构反过来给工程决策提供了一把尺。任何对训练循环的改动都可以问一句:它动的是三要素里的哪一个?换损失函数动的是标量压缩环节,影响"学什么";改采样与配比动的是数据环节,影响梯度的无偏性与方差;改优化器动的只是"用梯度怎么走",理论上不改变收敛点集合(在凸情形),只改变抵达速度与噪声底。很多"调了优化器没用"的困惑,其实是问题出在数据或损失,改装件救不了底盘。

同理,分布式训练的全部花样——数据并行、梯度累积、流水并行——都在小心翼翼地保持这一行式子语义不变:全局看来,它们必须等价于"某个有效 batch 上的一次无偏梯度估计加一步更新"。谁破坏了这个等价(比如各 rank 数据重复、归约漏了求和),训练就会在无人察觉处偏离。用最简结构当基准去对照工程实现,是排查训练 bug 的顶层方法。

反过来,这个基准也告诉你哪些改动是"动底盘"级别的:课程学习改变了采样的无偏性(样本分布随步数漂移),重要性采样用加权换取有偏的低方差,经验回放故意让旧样本重复出现——这些技术都以牺牲最小结构的某条假设为代价换收益,用的时候要清楚自己典当了什么。

跑一遍最小闭环

不用任何框架,40 行 numpy 把这个结构完整跑起来——线性回归 + 小批量 SGD,终点对照闭式解。选线性回归是因为它有解析最优解,收敛到什么程度一眼可判:

import numpy as np
rng = np.random.default_rng(0)

N, d, B, eta = 4096, 8, 64, 0.05
X = rng.normal(size=(N, d))
w_star = rng.normal(size=d)
y = X @ w_star + 0.1 * rng.normal(size=N)

w = np.zeros(d)
for step in range(2000):
    idx = rng.choice(N, size=B, replace=False)   # 无偏采样
    xb, yb = X[idx], y[idx]
    g = (2.0 / B) * xb.T @ (xb @ w - yb)         # MSE 的 minibatch 梯度
    w -= eta * g                                  # 一行更新

# 循环体一共四行,对应数据、损失、参数、步长四要素

w_ls = np.linalg.lstsq(X, y, rcond=None)[0]       # 闭式解做裁判
print(np.linalg.norm(w - w_ls))                   # ~1e-2 量级,噪声底

值得留意代码里每个元素与三要素的对应:X, y 是数据,(xb @ w - yb) 的平方是标量损失,w 是参数,eta 是步长——剥到这一步,没有第四样必需品。

剩下一层 $10^{-2}$ 量级的残差不是 bug:固定步长 SGD 不收敛到点,而是在极小值附近做平稳分布的游荡(稳态协方差正比于 $\eta\,\Sigma/B$)。想让残差归零只有两条路:衰减 $\eta$,或者加大 $B$。这就是学习率调度在最小结构层面的存在理由。

可以顺手验证稳态半径对 $\eta$ 的依赖:把上面代码的 $\eta$ 减半再跑,最终残差大约缩小一半;$\eta$ 除以 4,残差除以 4——线性关系持续到离散误差主导为止。这个实验五分钟就能跑完,却把"学习率决定噪声底"从口诀变成了亲眼所见。它也解释了为什么训练曲线末段的 loss 抖动幅度可以直接用来反推当前 $\eta/B$ 是否合适:抖动大说明温度高,该衰减了。

小结

一行 SGD 拆出四件最小要素:标量损失负责定义"学什么"并仲裁多目标,minibatch 采样给出方差 $\Sigma/B$ 的无偏梯度,参数是唯一跨步存活的状态,更新是梯度流的欧拉离散、步长受曲率约束。动量、Adam、调度器全是这行式子的改装件,看懂底盘才能评价改装。

走之前把这个最小结构和本章前面几篇接起来:广播归约、矩阵乘法梯度、数值检验,回答的是"那一行里的 $\hat g$ 怎么算对";这篇回答"为什么这一行本身就够"。两件事合在一起,手写一个小型训练器所需的数学就齐了——剩下的全是工程。下一篇把欧拉步长的限制算精确:泰勒展开与可信域给出学习率的理论起点。