JOVANA
Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

随机梯度下降、小批量与轮次

普通的梯度下降要读完整个数据集才肯走一步——对真实数据来说慢得离谱。本篇将告诉你:每次只采样一点点、频繁地迈步、甚至主动欢迎一些噪声,是如何把这个迟钝的算法变成几乎所有现代训练背后的主力。

先读完全部数据,问题出在哪

上一篇你学了普通的梯度下降:站在损失曲面上的某处,量出哪个方向是下坡,迈一步,再重复。但在「量出哪个方向是下坡」里悄悄藏着一个假设。要算出损失的真实梯度,你必须在每一个训练样本上都跑一遍模型、把所有误差加起来,然后才能动一步。这叫做全批量(full-batch)梯度下降。

如果是几百行的教科书式小数据集,这没问题。可一旦换成真实数据——上百万张图片、几十亿个词——那就是灾难。你得把整个数据集啃一遍,仅仅为了往下坡迈步,而模型需要成千上万步。更糟的是,整个数据集往往根本无法一次性塞进内存。实话实说:全批量下降在数学上很干净,但在大规模场景下实际不可用。

突破口就在这里。真实梯度不过是每个样本各自梯度的平均值。而要估计一个平均值,你并不需要问遍所有成员——抽样问一部分就够了。随机挑一百个样本问「哪边是下坡?」,它们的平均答案与全体人群的答案方向大致相同,可计算成本只有原来的万分之一。正是这一个想法,才让训练大模型成为可能。

随机梯度下降:步子勤,看得少

这种采样思路最极端的版本,就是[[stochastic-gradient-descent|随机梯度下降]](SGD)。你不再用整个数据集算出一个巨大的平均步,而是随手抓一个随机样本、算出它的梯度、立刻迈步;然后再抓下一个,再迈一步。「随机」(stochastic)这个词就是「随意、带运气」的意思——每一步都基于一个随机挑出的数据点,而不是全局画面。

你可以把全批量梯度想成:动身之前先对全体人口做一次细致普查;而 SGD 则像随便拦住一个路人、立刻照他的回答行动。任何单个回答都可能出错,甚至指向上坡。但在大量快速的步子里,这些误差大体相互抵消,而你每分钟取得的进展反而多得多,因为每一步都极其廉价。你是在用「每一步的质量」去换「步子的数量」。

SGD 走出的轨迹不是沿着山谷平滑滑下去的——它会左右锯齿摆动、抖动,偶尔还短暂往上爬。这种抖动不是一个要被消灭的缺陷;正如我们下面会看到的,它其实能帮上忙。你之前认识的学习率在这里仍然控制步长,而且在这里更要紧:方向本就这么嘈杂,学习率一旦太大,抖动就会失控放大。

小批量:处在中间的甜蜜点

纯粹「一次一个」的 SGD 也有自己的毛病:它在现代硬件上很浪费。一块 GPU 天生就是为成千上万次并行乘法而造的,而每次只喂它一个样本,会让它几乎全部算力闲置。所以实践中没人真用大小恰好为 1 的批量。我们折中处理:把一小组样本放在一起算。这一组就叫[[mini-batch|小批量]](mini-batch)。

你在比如 32 个或 256 个样本上求平均梯度,然后迈一步。今天人们口中说的「SGD」,几乎都暗指的是这个版本。它处在两个极端之间:比单样本 SGD 更稳定,因为在 32 个点上平均能抵消掉更多噪声;但又远比全批量便宜、快得多。整场博弈,就是要决定这个旋钮该停在哪个位置。

轮次与迭代:把工作量数清楚

有两个词总让人栽跟头,我们把它们彻底钉死。一次[[epoch-vs-iteration|迭代]](iteration,也叫一步 step 或一次更新 update)指的是:处理一个小批量、迈出一步。一个轮次(epoch)则是把整个训练集完整扫过一遍——每个样本恰好被看过一次。它们数的是不同的东西:迭代数的是「步数」,轮次数的是「在数据上走了几趟」。

两者之间的关系不过是一道除法。如果你有 10000 个训练样本、批量大小为 100,那么一个轮次就是 10000 / 100 = 100 次迭代。训练 20 个轮次,总共就迈了 2000 步。注意这意味着什么:把批量调小,每个轮次里的步数就更多——从同一批数据里学习的机会更多——这也是为什么小批量常常在「每个轮次」的尺度上学得更快。

shuffle(training_data)            # reshuffle each epoch
for epoch in range(num_epochs):
    for batch in split(training_data, batch_size):
        g = average_gradient(loss, batch)   # one mini-batch
        weights = weights - learning_rate * g   # one iteration
# iterations_per_epoch = dataset_size / batch_size
核心训练循环。外层循环计的是轮次;每个内层步骤就是一次迭代。每个轮次重新打乱,能让各批量保持新鲜、让噪声不带偏向。

为什么噪声其实帮了忙

一个更嘈杂、更草率的梯度居然能赢过一个精确的梯度,这听起来很反直觉。但深度网络的损失曲面布满了陷阱——浅浅的凹坑和平坦的鞍点区域,完美光滑的全批量下降一旦走进去就可能卡住,因为在这种点上精确梯度几乎为零,明显的去处一个也没有。而 SGD 不那么容易被困住。

这种随机性就像一种温和而持续的轻推。当小批量下降落进一个浅陷阱里,下一个带噪声的梯度很可能恰好「错得够多」,把它一脚踹出来,让它继续去寻找更深、更好的山谷。噪声是一种内建的探索来源——而且多少有点神奇的是,它往往会把训练引向「平坦」的极小值,也就是那些宽阔的盆地,它们通常比狭窄而脆弱的极小值更能泛化到新数据上。

不过要小心:这是一种倾向,不是保证,而且学界对「平坦极小值为什么更能泛化」究竟原因何在,至今仍有争论。也别把「噪声有帮助」过度解读成「噪声越多越好」——噪声太多(极小的批量配上很高的学习率)只会让训练剧烈乱晃、永远安定不下来。噪声是一味有用的调料,不是主菜。

速度与稳定的取舍:选一个批量大小

现在来看那个核心旋钮。小批量意味着梯度嘈杂、抖动,但步子廉价而快、还带来大量有益的探索——只是它没把 GPU 喂饱,而且可能需要更小的学习率才稳得住。大批量意味着梯度平滑、准确、硬件被充分利用,但每一步代价更高、总步数更少,还有滑进一个尖锐极小值、泛化变差的风险。两端都不是免费的。

还有一道与理论无关的硬性天花板:一个小批量必须能和模型本身及其中间激活值一起塞进 GPU 内存。在很多真实实践中,决定批量大小上限的是这道「内存墙」,而不是数学。一条常见的经验法则:当你增大批量时,通常可以把学习率大致成比例地一起调大,因为梯度噪声变小了,你就能更大胆地迈步。

那到底怎么选?老实说,批量大小是一个超参数——并不存在一个放之四海皆准的正确值,32 到 512 已能覆盖大多数情形。挑一个你的硬件喜欢的值,盯着学习曲线看,再去调整。还要记住本篇的边界:SGD 只告诉你*方向和步长*。至于现代那些精巧的技巧——*在什么时候该迈多大一步*,比如动量、Adam、学习率调度——是下一篇的主题,那台引擎将在那里真正活起来。