第一部分:理解优化的目标——梯度下降 (Gradient Descent)
在开始之前,我们必须明白一个最根本的概念:训练模型的目标是找到一组最优的参数(权重 w
和偏置 b
),让模型的损失函数(Loss Function)的值最小化。
你站在一座连绵起伏的山上,天黑了,你的任务是尽快走到山谷的最低点。你看不清整个山脉的全貌,但你能知道你脚下这一点哪个方向是下山最陡峭的。
- 山谷最低点:就是损失函数的最小值。
- 你所在的位置:就是模型当前的参数。
- 下山最陡峭的方向:就是损失函数在当前位置的梯度(Gradient)的反方向。梯度指向的是函数值上升最快的方向,那么梯度的反方向就是下降最快的方向。
梯度下降 (Gradient Descent, GD) 的核心思想很简单:计算出当前位置的梯度,然后沿着梯度的反方向迈一小步,不断重复这个过程,直到走到谷底。
这个“一小步”的大小,我们称之为学习率 (Learning Rate, lr
)。
lr
太大:你可能一步迈过了谷底,跑到对面山坡上,导致无法收敛。lr
太小:你前进得太慢,要花非常长的时间才能走到谷底。
第二部分:SGD (随机梯度下降)——更轻快的下山方式
梯度下降有一个问题:要计算当前位置的梯度,需要把整个训练数据集(比如,一百万张图片)全部拿来计算一次损失,然后求平均梯度。如果数据集非常大,这个计算成本是无法接受的。这就好比,你为了看清脚下的路,非要把整座山的每一寸土地都勘探一遍才决定下一步,效率太低。
SGD (Stochastic Gradient Descent) 应运而生,它提出了一种绝妙的改进:
不计算全部数据的梯度,而是每次随机抽取一小部分数据(一个批次,Batch)来计算梯度,并用这个“局部”的梯度来近似“全局”的梯度,然后更新参数。
这就像你在山中,不再勘探整座山,而是随机朝一个方向看一小段路,判断出这个小范围内的下山方向,然后就立刻朝那个方向走一步。
优点:
- 速度快:每次迭代的计算量大大减少,模型更新速度极快。
- 引入随机性:由于每次只看一部分数据,计算出的梯度是有噪声的。这种“噪声”反而可能是好事,它可能帮助你跳出那些不是真正最低点的“小山谷”(局部最优解),从而有更大机会找到全局的最低点(全局最优解)。
缺点:
- 收敛不稳定:随机性也带来了梯度方向的摇摆和震荡。你每一步都只是根据一小块地形来决策,你前进的路线会是曲折、摇摆的,而不是一条直线,导致收敛过程波动较大。
PyTorch代码理解SGD
import torch
import torch.nn as nn
import torch.optim as optim
# 1. 准备数据和模型 (示例)
# 假设我们有一个简单的线性回归模型
# 输入特征维度为10,输出为1
model = nn.Linear(10, 1)
# 创建一些假数据
input_data = torch.randn(128, 10) # 128个样本,每个样本10个特征
labels = torch.randn(128, 1) # 对应的128个标签
# 2. 定义损失函数
criterion = nn.MSELoss() # 均方误差损失
# 3. 初始化SGD优化器
# 将模型的所有参数(model.parameters())交给优化器管理
# 设置学习率(lr)为0.01
optimizer_sgd = optim.SGD(model.parameters(), lr=0.01)
# --- 训练过程中的一步 ---
def train_step():
# 在每次更新前,清空上一轮的梯度
optimizer_sgd.zero_grad()
# 前向传播:模型根据输入数据进行预测
outputs = model(input_data)
# 计算预测值和真实标签之间的损失
loss = criterion(outputs, labels)
# 反向传播:计算损失函数关于模型参数的梯度
loss.backward()
# 更新参数:优化器根据计算出的梯度,沿着反方向更新模型权重
# 更新规则: new_weight = old_weight - lr * gradient
optimizer_sgd.step()
print(f"Loss: {loss.item()}")
# 执行一次训练步骤
train_step()
第三部分:Adam (自适应矩估计)——更智能的下山专家
SGD虽然高效,但它的“摇摆”问题和对学习率的高度敏感性(需要手动精细调整)促使了更先进的优化器的诞生。Adam是目前最流行、最常用的优化器之一,它结合了两种重要思想:动量(Momentum)和自适应学习率(Adaptive Learning Rate)。
Adam就像一位更聪明的登山专家,他不仅看脚下,还具备了惯性和对地形的感知力。
模块1:动量 (Momentum)
SGD的每一步都只取决于当前计算出的梯度,这导致了摇摆。动量法则引入了“惯性”的概念。
它不仅仅考虑当前这一步的梯度,还会累加过去一段时间的梯度方向。想象一个从山上滚下来的球,它不会因为一块小石头就完全改变方向,它的动量会带着它冲过小的颠簸,继续沿着大致正确的方向前进。
- 好处:在梯度方向一致的维度上,动量会加速下降;在梯度方向摇摆不定的维度上,动量可以抵消震荡,使得更新方向更稳定、更快速。
模块2:自适应学习率 (RMSprop思想)
SGD家族(包括动量法)的另一个问题是,对所有参数都使用同一个学习率 lr
。但模型中不同的参数,其重要性和更新的尺度可能完全不同。有的参数可能已经接近最优值,需要微调(小学习率);有的参数还差得很远,需要大步前进(大学习率)。
Adam通过引入二阶矩估计来为每个参数自动调整学习率。简单来说:
- 它会记录每个参数过去梯度值的平方的累积平均值。
- 如果某个参数的梯度一直很大(说明它总是在剧烈变化、不稳定),那么分母就会变大,从而使得这个参数的实际学习率变小,起到“踩刹车”的作用,让它稳定下来。
- 反之,如果某个参数的梯度一直很小,分母就小,实际学习率会变大,鼓励它“大胆探索”。
Adam = 动量 + 自适应学习率
Adam将上述两种思想完美地结合在一起:
- 一阶矩估计(动量):它像动量法一样,会累积过去的梯度,计算出带有“惯性”的更新方向(记为
m
)。 - 二阶矩估计(自适应学习率):它会累积过去梯度的平方,为每个参数计算一个自适应的“刹车”或“油门”(记为
v
)。 - 最终更新:用带惯性的方向
m
,除以自适应的调节项v
的平方根,来对参数进行更新。
结果就是,Adam能够为每个参数计算出独立的、自适应的学习率,并且更新方向更加稳定,使得它在大部分情况下都能快速、稳健地收敛,且对初始学习率 lr
的选择不像SGD那么敏感。
PyTorch代码理解Adam
import torch
import torch.nn as nn
import torch.optim as optim
# 1. 同样的数据和模型
model = nn.Linear(10, 1)
input_data = torch.randn(128, 10)
labels = torch.randn(128, 1)
# 2. 同样的损失函数
criterion = nn.MSELoss()
# 3. 初始化Adam优化器
# 使用起来和SGD非常相似,只是换了个名字
# lr: 初始学习率
# betas: 用于计算一阶和二阶矩估计的指数衰减率,通常用默认值即可
# eps: 为了防止除以零而增加的一个极小值
optimizer_adam = optim.Adam(model.parameters(), lr=0.001, betas=(0.9, 0.999), eps=1e-8)
# --- 训练过程中的一步 (和SGD的调用方式完全一样) ---
def train_step_adam():
optimizer_adam.zero_grad()
outputs = model(input_data)
loss = criterion(outputs, labels)
loss.backward()
# Adam在内部会自动处理动量和自适应学习率的复杂计算
# 我们只需要调用 step() 即可
optimizer_adam.step()
print(f"Loss with Adam: {loss.item()}")
# 执行一次训练步骤
train_step_adam()
总结与对比
特性 | SGD (随机梯度下降) | Adam (自适应矩估计) |
---|---|---|
核心思想 | 每次随机用一小批数据计算梯度来更新。 | 结合了动量和自适应学习率。 |
学习率 | 所有参数共享一个固定的学习率,需要手动精细调整。 | 为每个参数计算一个自适应的学习率。 |
收敛性 | 速度较慢,收敛过程波动大、不稳定。 | 速度快,收敛过程平稳,通常能快速找到一个较好的解。 |
优点 | 简单,内存占用小。其随机性可能帮助跳出局部最优,找到更好的解。 | 鲁棒性强,对初始学习率不敏感,在大多数任务中表现都很好,是默认首选。 |
缺点 | 对学习率敏感,训练过程不稳定,后期收敛可能较慢。 | 可能会错过最优解(有研究表明其泛化能力有时不如精调的SGD)。计算和内存开销比SGD稍大。 |
适用场景 | 当你有足够的时间和经验去精细调整学习率时;或者在一些研究中发现其泛化能力更好时。 | 绝大多数情况下的首选优化器,尤其是在项目初期和对模型性能没有极致要求时。 |
希望这个分步、带代码的解析能让你对SGD和Adam有一个清晰、深入的理解!
大型模型训练中,一个看似更“笨”、更基础的方法(SGD),在特定条件下,其效果能够超越被广泛使用的高级方法(Adam)。
核心问题:为何“笨办法”有时更有效?
在顶尖高手的较量中,为什么有时看似“笨”的方法反而能取得更好的效果?
两个主角:Adam vs. SGD (笨办法)
- Adam:是当前人工智能领域,尤其是大型语言模型训练中,一个非常主流和先进的优化器 。
- SGD:是一种相对更基础的优化方法。在文中,它被视为那个有效的“笨办法” 。
关键变量:批量大小 (Batch Size)
决定哪个方法更优越的关键,在于一个叫做“批量大小”(Batch Size)的参数 。这指的是模型在一次更新中处理的数据样本量。
- 传统情况 (小批量):在较小的批量大小下,Adam通常是首选。
- 新的发现 (大批量):有研究者意外发现,当把批量大小提升到一个很高的数值时(文中提到了1024 ),情况发生了逆转。
“大力出奇迹”
- SGD在大批量下的逆袭:研究表明,在非常大的批量大小条件下,SGD这个“笨办法”不仅能够有效训练,其最终效果甚至可以超越更复杂的Adam优化器 。
- 资源是前提:这种“大力出奇迹”的策略是建立在拥有足够强大的计算资源之上的 。因为只有算力充足,才能支撑起如此巨大的批量来进行训练。
- 重新审视方法论:这一发现挑战了“高级方法一定更好”的固有观念。它揭示了,方法的选择和其所处的计算资源环境密切相关。在小批量、计算有限的条件下,SGD的成功是有限的 。但在资源充裕、可以使用大批量的情况下,它为模型训练“开了一片天” ,展现了惊人的潜力。
在训练AI模型时,Adam就像一个聪明的徒步者,他会根据路况(梯度)不断调整自己的步伐(学习率),走得很稳很快。而SGD则像一个朴实的徒步者,步伐大小固定。
过去,大家通常都走在小路上(小批量),聪明的徒步者Adam自然更有优势。但现在,我们有了超级计算机这样能开出一条“高速公路”(大批量)的工具 。在这条又宽又直的高速公路上,那个只会埋头迈开大步向前冲的“笨”徒步者SGD,反而因为其简单、直接的策略,最终能比需要不断调整的Adam更快、更好地到达终点 。
Is your batch size the problem? Revisiting the Adam-SGD gap in language modeling
[Max Planck Institute for Intelligent Systems]
https://arxiv.org/abs/2506.12543