1. 从一次训练崩掉的经历说起很多人第一次认真审视优化器都是因为模型训练出了岔子。我印象很深的一次帮朋友看一个图像分类的小项目网络结构不算深数据也清洗过可 loss 曲线就是一副“心电图”的样子上下剧烈抖动学到最后准确率卡在一个尴尬的位置上不去。他一开始怀疑是数据标签错了又怀疑是学习率太大来回折腾了两天。最后我让他把优化器从 SGD 换成带动量的版本再把学习率按 batch size 重新算了一遍曲线当场就顺了。这件事说明一个很朴素的问题优化器不是训练里一个可以随便填的默认参数它直接决定了参数更新的方向和步长进而决定模型能不能收敛、收敛得多快、最终停在哪。这篇内容我想把 SGD 到 Adam(W) 这条主线捋清楚。它适合刚入门深度学习、正在用 PyTorch 搭模型的人也适合那些“会用但说不清为什么”的从业者。我会讲清楚每个优化器到底在解决什么问题、公式背后的直觉是什么、PyTorch 里对应的参数怎么设、什么场景该选谁最后附上可以直接跑起来的对比代码。核心关键词就三个SGD、Adam、优化器对比围绕它们展开不跑偏。先说结论性的判断方便你带着预期往下读SGD 是地基动量是给地基加了惯性自适应学习率方法AdaGrad、RMSProp、Adam是给每个参数配了独立的油门而 AdamW 是把权重衰减从梯度更新里拆出来、修正了 Adam 的一个历史遗留问题。理解这条演进线比死记每个优化器的公式有用得多因为每一步演进都是被前一步的某个缺陷逼出来的。2. 优化器到底在优化什么先把问题定义清楚2.1 参数更新这件事的本质训练一个神经网络本质是在一个高维参数空间里找损失函数的最低点。你有一堆参数 θ有一个损失函数 L(θ)目标是让 L 尽量小。但高维空间里你没法直接求解只能一步一步走每一步根据当前点的梯度也就是最陡下降方向决定往哪走、走多远。所有优化器干的都是这一件事区别只在于“方向怎么定”和“步长怎么给”。最朴素的更新规则是 θ θ - η·∇L(θ)η 是学习率。这个式子看着简单但它藏着两个大坑一是梯度方向噪声大单批次算出来的梯度未必指向全局最优二是所有参数共用一个 η可不同参数的梯度尺度差异可能极大。后面所有的优化器演进几乎都是在补这两个坑。2.2 为什么不能一个学习率走天下举个生活化的例子。假设你在下山有的坡很陡梯度大有的坡很缓梯度小。如果步长固定陡坡上你一步迈太大容易冲过头缓坡上你一步迈太小又磨磨蹭蹭。理想情况是陡的地方小步走、缓的地方大步走。自适应学习率方法就是干这个的它记录每个参数历史上梯度的大小梯度一直很大的参数就把它的有效学习率压小梯度一直很小的参数就相对放大。这就是 AdaGrad 系列的核心思想也是理解 Adam 的钥匙。注意这里说的“每个参数独立学习率”是有效学习率不是你手动设的那个全局 η。全局 η 仍然是一个基准自适应方法是在它基础上做缩放。2.3 优化器选型影响的三件事选优化器实际影响的是三件事收敛速度多少 epoch 能到一个可用的 loss、最终泛化测试集表现好不好而不只是训练集、训练稳定性对学习率、batch size 这些超参敏不敏感。很多教程只讲第一点但后两点在实际项目里往往更致命。Adam 收敛快是公认的但在某些视觉任务上精心调过的 SGD 加动量最终泛化反而更好这个现象后面会专门讲。3. SGD 与动量最朴素也最容易被低估的组合3.1 原始 SGD 的更新逻辑与它的两个毛病原始 SGDStochastic Gradient Descent每次用一个 mini-batch 的梯度来近似全量梯度更新式就是 θ θ - η·gg 是当前 batch 的梯度。它的优点是简单、内存开销小、理论性质清楚。但毛病也很明显一是梯度噪声大更新方向来回摆尤其在损失曲面像狭长山谷的时候一个方向陡、一个方向缓会在陡的方向反复震荡二是所有参数一个学习率前面说过的问题它全占。PyTorch 里最基础的用法是这样import torch.optim as optim optimizer optim.SGD(model.parameters(), lr0.01)就这一行没有动量、没有自适应。很多老论文里的 baseline 就是它但今天直接拿它训深网络大概率会训得很痛苦。3.2 动量给更新加一点“惯性”动量的思路特别直观不要每一步都完全听当前梯度的而是把历史梯度累积起来形成一个“速度”项。公式大致是 v β·v g然后 θ θ - η·vβ 通常取 0.9。这样做的效果是方向一致的梯度会越积越大加速来回震荡的梯度会互相抵消抑制震荡。就像一个小球滚下山有惯性不会因为一个小坑就立刻改变方向。PyTorch 里加动量只要一个参数optimizer optim.SGD(model.parameters(), lr0.01, momentum0.9)实测下来momentum0.9 是绝大多数场景的稳妥起点。它带来的收敛加速非常明显而且几乎不增加什么计算开销。我个人的经验是如果你只打算用一个“笨但可靠”的优化器SGD momentum0.9 是首选尤其在卷积网络做图像任务时它经常能给出比 Adam 更好的最终精度代价是你得认真调学习率和学习率衰减策略。3.3 Nesterov 动量往前多看一步Nesterov 动量是对普通动量的一个小改进计算梯度时不用当前位置而是用“按当前动量再走一步之后”的位置。直觉上就是“我先预判我要去哪然后在那个位置看坡度”。PyTorch 里通过nesterovTrue开启前提是 momentum 必须大于 0optimizer optim.SGD(model.parameters(), lr0.01, momentum0.9, nesterovTrue)它在凸问题上理论收敛更好实际深度网络里效果因任务而异有时略好有时差不多。我的建议是把它当成一个可以试的开关别指望它带来质变但在一些对收敛精度敏感的任务上值得开一下对比。3.4 SGD 的学习率怎么定一个可复现的估算思路SGD 最让人头疼的是学习率。这里给一个我常用的粗略估算方法先从一个很小的值比如 1e-5开始每个 epoch 乘以一个系数逐步放大观察 loss找到 loss 开始明显震荡或发散的临界值然后取那个临界值的十分之一到三分之一作为初始学习率。这个方法不需要精确计算靠几次实验就能定位一个合理区间。另外学习率和 batch size 是耦合的。一个常见的经验法则是batch size 放大 k 倍学习率也大致放大 k 倍线性缩放规则但这个规则在超大 batch 时会失效需要配合 warmup。这些细节在纯 SGD 训练里尤其重要因为 SGD 对学习率比 Adam 敏感得多。4. 自适应学习率家族AdaGrad、RMSProp 到 Adam4.1 AdaGrad给每个参数配独立油门AdaGrad 的核心是维护一个梯度平方的累加量用它来缩放学习率。梯度历史大的参数累加量大有效学习率就小历史小的有效学习率就大。公式上大致是 s s g²然后 θ θ - η·g / (√s ε)。这个思路对稀疏特征特别友好比如自然语言处理里很多词出现频率差异极大AdaGrad 能让低频词获得相对更大的更新。但它有个致命缺陷s 是单调累加的训练越久 s 越大有效学习率会一路衰减到接近零模型后期几乎学不动。这个“学习率过早衰减”的问题直接催生了下一个方法。4.2 RMSProp把累加改成滑动平均RMSProp 的改动很关键不再累加所有历史梯度平方而是用指数移动平均只保留近期信息。公式上 s β·s (1-β)·g²β 通常取 0.9 或 0.99。这样有效学习率不会无限衰减能持续保持一个合理的更新幅度。RMSProp 在非平稳目标深度网络训练就是典型的非平稳上表现比 AdaGrad 稳得多。PyTorch 里optimizer optim.RMSprop(model.parameters(), lr0.01, alpha0.99)alpha就是那个 β。RMSProp 在 RNN 类模型上曾经是标配现在虽然被 Adam 抢了风头但理解它是理解 Adam 的必经之路因为 Adam 的一阶矩和二阶矩估计就是从动量和 RMSProp 各借了一半。4.3 Adam动量 自适应两个都想占AdamAdaptive Moment Estimation同时维护两个量一阶矩 m梯度的滑动平均相当于动量和二阶矩 v梯度平方的滑动平均相当于 RMSProp 的分母。更新时用 m 除以 √v既有了方向上的平滑又有了每个参数的自适应步长。公式里还有一步偏差修正bias correction因为 m 和 v 初始化为 0训练初期会偏向 0修正后能让早期更新更准。PyTorch 用法optimizer optim.Adam(model.parameters(), lr1e-3, betas(0.9, 0.999), eps1e-8)lr1e-3是 Adam 的经典默认值betas分别控制一阶和二阶矩的衰减。Adam 的最大优点是“开箱即用”对学习率不敏感很多任务上不调参就能跑出不错的结果这也是它成为默认选择的原因。4.4 Adam 的泛化争议为什么有人回头用 SGD这里必须讲一个业内长期讨论的现象在某些图像分类任务上Adam 训练集 loss 降得飞快但测试集精度不如精心调过的 SGD momentum。一种被广泛接受的解释是自适应方法容易收敛到损失曲面上比较“尖锐”的极小值而 SGD 的噪声特性反而倾向于找到更“平坦”的极小值平坦极小值通常泛化更好。这个解释不是定论但它提醒我们收敛快不等于最终好。如果你的任务对泛化精度要求极高别偷懒把 SGD 调参这条路也走一遍做对比。5. AdamW把权重衰减从梯度里拆出来5.1 L2 正则和权重衰减不是一回事这是 AdamW 要解决的核心问题也是很多人搞混的地方。在原始 SGD 里L2 正则在损失里加 λ·θ²和权重衰减每步把参数乘一个略小于 1 的系数在数学上是等价的。但在 Adam 里因为梯度会被自适应地缩放L2 正则项混进梯度后会被那个 1/√v 的缩放因子扭曲导致大梯度的参数实际受到的衰减反而小这就偏离了“所有权重同等衰减”的本意。AdamW 的做法是把权重衰减从梯度更新里拿出来单独对参数做衰减θ θ - η·(自适应更新) - η·λ·θ。这样衰减就和自适应缩放解耦了行为更符合直觉正则效果也更稳定。5.2 PyTorch 里的 AdamW 与参数设置optimizer optim.AdamW(model.parameters(), lr1e-3, weight_decay0.01)注意weight_decay在 AdamW 里的推荐值和 Adam 里不一样。Adam 里常用 1e-4 到 1e-5而 AdamW 因为解耦了常用 0.01 甚至 0.1 这个量级。这个差异经常被忽略直接照搬 Adam 的 weight_decay 到 AdamW 上正则效果会偏弱。我踩过这个坑调了半天以为是模型容量问题最后发现是衰减系数小了一个数量级。5.3 什么时候该用 AdamW现在很多 Transformer 类模型、大语言模型的训练默认就是 AdamW因为它对权重衰减的处理更干净配合 warmup 和余弦退火学习率调度稳定性很好。如果你在做 NLP 或者任何用了大量归一化和残差结构的模型AdamW 基本是首选。视觉任务里它也越来越常见尤其是 ViT 这类结构。6. 优化器横向对比与选型决策6.1 一张表看清差异优化器核心机制学习率敏感度收敛速度泛化倾向典型场景SGD纯梯度下降高慢好调好后CNN 图像分类 baselineSGDMomentum加惯性中高中好视觉任务主力AdaGrad梯度平方累加低前期快后期慢中稀疏特征RMSProp梯度平方滑动平均中中快中RNN、非平稳目标Adam一阶二阶矩低快中通用默认AdamWAdam解耦衰减低快较好Transformer、大模型6.2 选型的三个判断维度我一般按三个维度来选。第一看任务类型视觉 CNN 优先试 SGDmomentumNLP 和 Transformer 优先 AdamW。第二看调参预算预算少、想快速出 baseline直接 Adam/AdamW预算充足、追求极致精度值得花时间调 SGD。第三看模型规模大模型训练里 AdamW 几乎是事实标准因为 SGD 在大模型上对学习率调度太敏感调起来成本太高。6.3 学习率调度器和优化器是配套的单独谈优化器没意义它总是和学习率调度器一起工作。SGD 常配 step decay 或 cosine annealingAdam 常配 warmup cosine。一个常见错误是换了优化器但没换调度策略比如把 SGD 换成 Adam 却还沿用原来那个很激进的学习率衰减结果就是前期没充分学、后期又降太快。换优化器时调度器要一起重新审视。7. 可直接复现的 PyTorch 对比实验7.1 实验设计思路为了公平对比我用同一个简单网络、同一份数据、同样的 epoch 数只换优化器记录训练 loss 和验证精度。网络用一个两层全连接加一个卷积的简化结构数据用随机生成的分类数据避免依赖外部数据集方便你直接跑。重点是观察不同优化器的收敛曲线形态而不是追求某个绝对精度。7.2 完整对比代码import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset torch.manual_seed(42) # 构造一个简单的分类数据集 X torch.randn(2000, 20) y (X[:, 0] X[:, 1] * 0.5 0).long() dataset TensorDataset(X, y) loader DataLoader(dataset, batch_size64, shuffleTrue) class SimpleNet(nn.Module): def __init__(self): super().__init__() self.net nn.Sequential( nn.Linear(20, 64), nn.ReLU(), nn.Linear(64, 32), nn.ReLU(), nn.Linear(32, 2) ) def forward(self, x): return self.net(x) def train_with(opt_name, epochs30): model SimpleNet() if opt_name sgd: opt optim.SGD(model.parameters(), lr0.01) elif opt_name sgd_momentum: opt optim.SGD(model.parameters(), lr0.01, momentum0.9) elif opt_name rmsprop: opt optim.RMSprop(model.parameters(), lr0.01) elif opt_name adam: opt optim.Adam(model.parameters(), lr1e-3) elif opt_name adamw: opt optim.AdamW(model.parameters(), lr1e-3, weight_decay0.01) criterion nn.CrossEntropyLoss() losses [] for epoch in range(epochs): total 0.0 for xb, yb in loader: opt.zero_grad() out model(xb) loss criterion(out, yb) loss.backward() opt.step() total loss.item() losses.append(total / len(loader)) return losses for name in [sgd, sgd_momentum, rmsprop, adam, adamw]: ls train_with(name) print(f{name:14s} final_loss{ls[-1]:.4f} first5{[round(v,3) for v in ls[:5]]})跑下来你会看到纯 SGD 的 loss 下降最慢SGDmomentum 明显加速Adam 和 AdamW 前期下降最快。这个结果和理论预期一致。你可以把 epoch 拉长到 100观察后期 SGD 是否反超——在简单任务上不一定但在更复杂的真实任务上这个反超现象更常见。7.3 实验里容易忽略的两个细节第一个细节是zero_grad()的位置。必须在反向传播前清零否则梯度会累加不同优化器的对比就失真了。第二个细节是随机种子。对比实验一定要固定种子否则数据打乱顺序不同曲线差异可能掩盖优化器本身的差异。我见过有人对比优化器不固定种子得出“Adam 不如 SGD”的结论其实是随机波动。提示做优化器对比时除了最终 loss一定要记录 loss 曲线的形状。震荡幅度、收敛速度、是否出现平台期这些信息比一个最终数字有价值得多。8. 实操中踩过的坑与排查清单8.1 常见问题速查表现象可能原因排查方向loss 剧烈震荡学习率过大 / batch 太小降 lr增大 batch换 Adamloss 不下降学习率过小 / 梯度消失升 lr检查梯度范数前期降后期卡住AdaGrad 学习率衰减过度换 RMSProp 或 Adam训练好测试差过拟合 / 尖锐极小值加 weight_decay试 SGDAdamW 正则无效weight_decay 沿用 Adam 量级调到 0.01 量级重试8.2 三个我实际踩过的坑第一个坑是把 Adam 的 weight_decay 直接搬到 AdamW。前面提过量级差一个数量级导致正则几乎没起作用模型过拟合还以为是数据问题。第二个坑是换优化器不换学习率。从 SGD 换 Adam 时还沿用 SGD 的 lr0.1结果直接发散因为 Adam 的有效步长机制不同1e-3 才是它的舒适区。第三个坑是忽略梯度裁剪。用 Adam 训 RNN 类模型时偶尔会遇到梯度爆炸加一个torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)就能稳住这个和优化器选择无关但经常被一起忽略。8.3 一个实用的调参顺序我的习惯顺序是先固定优化器为 AdamW 跑通流程拿到 baseline再决定是否值得换 SGD 精调。换 SGD 时先调学习率用前面说的临界值法再加 momentum最后考虑 nesterov 和 weight_decay。这个顺序能保证每一步的改动都有明确的观察目标不会一次改太多变量导致无法归因。9. 我个人在实际操作中的体会优化器这条线从 SGD 到 AdamW表面上是公式越来越复杂实质上是每一步都在回答“怎么让参数更新更聪明一点”。但聪明不等于适合。我做过的好几个项目里最后上线的模型用的既不是最潮的也不是最复杂的而是那个在当前数据和算力条件下、调参成本可接受、结果可复现的方案。Adam 让你快速看到希望SGD 让你有机会把精度再抠一抠AdamW 让你在大模型上少操点心。真正值钱的不是记住哪个优化器最强而是知道在什么约束下该选谁、以及选完之后配套的学习率调度和正则怎么跟上。这套判断力只能靠一次次对比实验喂出来代码就在上面跑一遍比看十篇总结都管用。