☰
深度学习优化器选型与实战调参指南:从SGD到AdamW
2026/10/1 19:29:37 网站建设 项目流程

很多刚接触深度学习的同学看到"Model-Optimizer"这个名字时会有点困惑:这到底是指一个具体工具,还是某类算法的统称?其实在训练管线里,它指向的通常就是那个控制模型参数如何更新的优化器(Optimizer)。我见过太多项目,数据清洗做得挺好、模型结构也照抄了知名开源仓库,结果训练一开跑loss飙成NaN,或者收敛极其缓慢,最后排查半天问题全在优化器这一环。这篇文章想把优化器到底在做什么、主流方案怎么选、背后机制怎么理解,以及我在实战里踩过的调参坑一次讲清楚。内容不追求华丽,但保证都可以直接复现,适合刚入门的小白,也适合已经训练过一些模型但常常被loss曲线折磨的朋友。

1. 先厘清概念:Model-Optimizer在你的训练管线里到底管哪一段

1.1 从数据到参数的完整链条

先把训练过程拆成一条最简链路:

  1. 前向传播:把batch数据喂给模型,计算预测值和损失函数值。
  2. 反向传播:根据损失函数对每个权重求解梯度。
  3. 优化器更新:用梯度以及优化器自己维护的状态,计算出权重的更新量。
  4. 循环上面三步,直到loss不再下降或达到预设epoch。

这个链条里,很多人会把"损失函数"和"优化器"混为一谈。损失函数负责回答"当前模型有多差",优化器负责回答"下一步该怎么调整权重才能让模型变好"。换句话说,同样的模型和损失函数,换一个优化器,训练轨迹和最终效果可能天差地别。

我在实际项目里经常做这样的验证:同样的数据、同样的模型结构、同样的损失函数,把优化器从SGD换成Adam,前几十个step的loss下降速度会有肉眼可见的差异。这也是为什么Model-Optimizer值得单独花时间研究——它是整个训练动力学的主导者,却往往是被新手忽略的一环。

1.2 "下山"类比:三个角色各管什么

损失函数可以想象成一片起伏不平的山地,模型权重就是你在山上的位置,目标是走到最低的谷底。梯度告诉你当前位置哪个方向是下坡,优化器则负责决定你每一步怎么迈。

如果用这个类比拆解,事情会非常清晰:

  • 损失函数:地形本身,越高代表当前模型越差。
  • 梯度:你脚下的坡度方向,指向局部最陡的下坡方向。
  • 优化器:你的走路策略。可以每步一样长(SGD),可以带着惯性往前冲(Momentum),也可以根据每个方向的地形自动调整步长(Adam)。

学习率在这套类比里就是步长。步长太大,你一步跨过山谷,直接跳到对面山坡甚至滚下悬崖;步长太小,你走得慢吞吞,训练几小时还在原地。而自适应优化器的本质,是让每个方向的步长都不太一样,陡峭的地方小步走,平坦的地方大步走。

这样一想就明白为什么优化器选型和调参这么重要了:它对训练效率和安全性的影响甚至高过模型结构设计。

1.3 容易被忽略的代价:优化器要额外占显存

聊优化器时很多人的第一反应是"反正就是一行代码,SGD换Adam而已",但这里有一个非常现实的工程问题:显存开销。

假设模型参数量为N,每个参数用4字节的FP32存储:

  • 纯SGD:除了模型本身的4N字节权重,优化器不额外维护多少状态,显存开销很低。
  • SGD+Momentum:需要额外维护一份动量的buffer,大约4N字节。
  • Adam:需要维护一阶矩m和二阶矩v,两份buffer,额外8N字节。

换句话说,在一个1B参数规模的模型上,用FP32训练,模型权重本身占4GB,纯SGD基本就这个量级,Adam则会额外吃掉8GB显存用来存优化器状态。这个数字在工程师眼里非常惊人,很多大模型训练之所以用混合精度,一部分原因就是优化器状态太吃显存,全部用FP32根本放不进去。

我后来在实际部署和训练大模型时,专门留意过优化器显存优化方案。比较常见的做法是:

  • 混合精度训练框架(比如AMP):梯度用FP16存储,但优化器状态保留FP32精度。
  • 8-bit优化器:把Adam的m和v用8位整数存储,显存直接砍掉近四分之三。
  • 优化器状态CPU offload:让GPU只在需要更新时同步状态。

这里要提醒一句:不要无脑上这些省显存的方案,8-bit优化器在部分框架里和自定义算子之间存在兼容性问题,建议先用小模型跑通再上大规模任务。

2. 主流优化器选型:从SGD到AdamW,每一代解决什么问题

2.1 为什么SGD+Momentum至今没有被淘汰

在深度学习早期,SGD是最朴素的优化器,公式极其简单:

权重 = 权重 - 学习率 × 梯度

但这个方案有两个明显问题:一是收敛速度慢,二是在崎岖地形上震荡非常严重。为了让SGD走出更好的轨迹,工程师们加入了动量(Momentum)机制:每次都保留一部分历史更新方向,相当于给参数更新加了惯性。这样在沟壑地形中,垂直方向的震荡被相互抵消,水平方向的运动被保持,训练自然更稳定、更快。

有趣的是,即使到了今天,SGD+Momentum在一批任务里依然是首选:

  • 很多图像分类网络的baseline还是用SGD,因为它在够大的训练数据和较好的学习率调度下表现出很好的泛化性。
  • 一些强化学习算法和传统机器学习baseline也用SGD,因为它状态少、行为可预测。

SGD最大的问题在于学习率敏感。你调一个合适的学习率可能要跑很多次实验,如果学习率设置不当,它要么收敛慢,要么直接发散。这也是为什么很多新手一上来就用Adam——Adam对学习率的适应能力强得多。

2.2 Adam怎么火起来的,又是怎么被AdamW纠正的

Adam的全称是Adaptive Moment Estimation,它同时维护一阶矩和二阶矩估计。简单理解:

  • 一阶矩m:对梯度的指数移动平均,相当于动量。
  • 二阶矩v:对梯度平方的指数移动平均,大致反映了每个参数维度的梯度波动幅度。

更新时,Adam用m除以v的平方根,相当于给每个参数生成了独立的自适应学习率。如果一个参数的历史梯度一直很大,那么它的有效学习率会自动变小;如果某个参数几乎没有梯度,有效学习率就会变大,从而不错过潜在的有价值方向。

这个机制让Adam在大多数任务上开箱即用,尤其是NLP模型和Transformer,几乎成了标配。但Adam提出后不久,研究者们发现了一个隐藏问题:Adam里引入的权重衰减(weight decay)实现方式有问题。

原始Adam把L2正则化和梯度耦合在一起,而L2正则项的梯度也会被Adam的自适应学习率缩放,导致正则化效果被扭曲。AdamW的解决方案是"解耦":把权重衰减从梯度计算中分离出来,在每次参数更新时直接让权重乘以一个略小于1的系数。这个看似微小的改动,在很多Transformer训练任务上显著提升了效果。

这也能解释为什么现在我们看到的大模型训练代码几乎都在用AdamW而不是原始Adam。

2.3 大模型与分布式训练时代的变种

进入大模型时代后,优化器出现了几个重要的工程变种,需要开发者了解它们的存在:

优化器核心机制优点典型用途
SGD+Momentum动量累积泛化好、显存占用低图像分类、传统baseline
Adam一阶矩+二阶矩开箱即用、对lr不敏感通用任务、快速验证
AdamWAdam + 解耦衰减正则化更合理Transformer、大模型微调
LAMB分层自适应学习率超大batch稳定训练大规模预训练
Adafactor低秩近似二阶矩节省显存长序列Transformer
8-bit Adam量化优化器状态显存大幅下降单卡/多卡大模型训练

比如LAMB,它可以在batch size达到几万甚至几十万时保持训练稳定,原因是它把每一层的权重更新按层级归一化,避免某一层的梯度scale过大影响全局训练。而Adafactor用低秩分解来近似Adam需要的二阶矩,显存占用比Adam低不少,适合在长序列上节俭地训练。

但坦率说,对于大多数个人项目、中小型公司业务模型来说,这些变种不是必需品。最稳妥的组合仍然是AdamW + warmup + weight decay + 余弦退火调度。变种优化器通常是大规模分布式训练里才需要考虑的事。

3. 从零手写一个优化器:真正看懂SGD、Momentum、Adam

3.1 先写一个最朴素的SGD

很多框架封装的优化器像黑盒,但优化器本身并不神秘。我用PyTorch风格给你写一个最小实现,全程只有一次参数更新。

import torch class PlainSGD: def __init__(self, params, lr=0.01): self.params = list(params) self.lr = lr def step(self): with torch.no_grad(): for p in self.params: if p.grad is None: continue p -= self.lr * p.grad

核心逻辑就是:

  1. 遍历所有参数。
  2. 如果该参数没有梯度,跳过。
  3. 在torch.no_grad()环境中原地执行数值更新。

这里的p -= self.lr * p.grad意味着参数朝着负梯度方向移动,这正是"梯度下降"名称的由来。这个实现没有任何状态,刚跑起来速度最快,但对学习率选择非常敏感。

3.2 加上Momentum:给更新方向增加惯性

Momentum的核心是在每个参数上维护一个历史梯度的移动平均buffer,每次更新时先用buffer做平滑,再用平滑后的方向去更新权重。

class SGDMomentum: def __init__(self, params, lr=0.01, momentum=0.9): self.params = list(params) self.lr = lr self.momentum = momentum self.buffer = [torch.zeros_like(p) for p in self.params] def step(self): with torch.no_grad(): for p, buf in zip(self.params, self.buffer): if p.grad is None: continue buf.mul_(self.momentum) buf.add_(p.grad) p -= self.lr * buf

buf就是之前的动量方向。每次更新时,新的动量等于旧动量乘以momentum_factor,再加上当前梯度。momentum=0.9表示保留90%的历史方向,当前梯度只提供10%的修正。这个机制让权重更新在陡峭方向被削弱、在平坦方向能持续加速。

我在图像分类任务上做过对比,同样的学习率下,加入momentum后收敛曲线明显平滑很多,训练前期的震荡幅度大幅下降。

3.3 Adam:一阶矩、二阶矩和偏差修正

Adam的实现比SGD复杂一个量级,但核心仍然可以写在几十行代码内。

class SimpleAdam: def __init__(self, params, lr=1e-3, betas=(0.9, 0.999), eps=1e-8): self.params = list(params) self.lr = lr self.betas = betas self.eps = eps self.m = [torch.zeros_like(p) for p in self.params] self.v = [torch.zeros_like(p) for p in self.params] self.t = 0 def step(self): self.t += 1 b1, b2 = self.betas with torch.no_grad(): for p, m, v in zip(self.params, self.m, self.v): if p.grad is None: continue g = p.grad m.mul_(b1).add_(g, alpha=1 - b1) v.mul_(b2).add_(g * g, alpha=1 - b2) m_hat = m / (1 - b1 ** self.t) v_hat = v / (1 - b2 ** self.t) p -= self.lr * m_hat / (v_hat.sqrt() + self.eps)

这段代码要把握四个关键点:

  1. 一阶矩m:对梯度的指数移动平均,相当于自适应动量。
  2. 二阶矩v:对梯度平方的指数移动平均,刻画了梯度幅度的历史水平。
  3. 偏差修正:训练初期m和v从零开始,数值会偏小,直接使用会低估动量、高估自适应步长。所以用1 - b1 ** t和1 - b2 ** t做分母修正。
  4. eps:避免除零,也影响更新的数值稳定性。在FP16混合精度训练里,eps往往需要调大到1e-7甚至1e-6。

如果你跑一下这个实现,会发现Adam的更新其实是一种"标准化的下降方向"与"全局学习率"的组合:梯度大不一定更新多,反而被自适应缩放抑制。这正是Adam对不同参数尺度都能有效训练的原因。

3.4 用玩具任务实测三种优化器的差别

为了让原理落地,我一般建议用一个极小的回归模型来对比。比如拟合一条带噪声的直线,或者最小化一个二次型函数。用同一份数据,分别用PlainSGD、SGDMomentum、SimpleAdam训练,观察loss下降速度。

我实测下来大概是这样:Adam在最初的几十个step内loss下降非常快,因为它自适应调整了每个方向的步长,几乎不需要人工选学习率;SGDMomentum在合适的lr下也能快速收敛,但要调参;PlainSGD最容易出现"一步跨过头"的现象,学习率稍大loss就反弹。

这个实验虽然简单,但对建立直觉非常有帮助。你自己跑一遍之后,就会理解为什么说"Adam适合快速验证,但SGD+Momentum在精心调参后往往能收敛到更优泛化解"。

4. 训练实战中最容易翻车的优化器参数

4.1 学习率:决定生死但很多人只顾抄默认值

我见过太多训练失败案例,最后定位到的问题就一条:学习率不对。学习率过大,loss曲线直接起飞,甚至变成NaN;学习率过小,loss像蜗牛一样爬,几个epoch过去几乎不动。

在常见任务里,经验性起始范围大概是:

  • SGD+SGD+Momentum:0.1到0.01之间,配合学习率调度器。
  • Adam / AdamW:1e-4到3e-4之间,这个范围在NLP任务上尤其常用。
  • 大模型预训练:通常从1e-4级别起步,甚至更低。
  • 预训练模型微调:1e-5到3e-5很常见,因为预训练权重已经很接近局部最优,步长过大容易破坏已有特征。

除了起始值,warmup也很关键。所谓warmup,就是在最初几百或几千步内,让学习率从一个很小的值逐渐升到目标值。这样做的原因在于,训练刚开始时模型权重处于随机状态,梯度方向可能非常不稳定,直接上大学习率很容易让参数跳到危险区域。warmup给了模型一个"预热"过程,先稳住,再加速。

我自己的一个典型案例是训练一个中型的Transformer文本分类模型,直接把学习率设成3e-3,结果loss在前几百步内直接变成NaN,排查了很久才发现是学习率过高加缺少warmup。改成3e-4并加入500步线性warmup后,训练稳定了,最终效果也达到了预期。

4.2 beta与eps:Adam里容易被忽略的隐藏旋钮

Adam的默认beta参数是(0.9, 0.999),绝大多数人一辈子不改它。但某些任务场景下,调整beta带来的收益甚至大过换优化器。

  • beta1控制一阶矩(动量)的衰减速度。beta1=0.9代表历史动量保留90%,更激进;beta1=0.5会让动量衰减更快,更贴近当前梯度,适用于某些需要快速响应梯度变化的场景。
  • beta2控制二阶矩(梯度平方)的衰减速度。在长时间训练任务中,如果beta2过大,比如0.9999,二阶矩的估计窗口太长,自适应学习率衰减过慢,导致后期更新幅度偏大、loss震荡;如果beta2过小比如0.9,二阶矩对梯度波动太敏感,更新步长会被过度缩放。

另外eps也很有讲究。在FP16混合精度训练中,FP16能表示的最小正数远大于FP32,默认的eps=1e-8在转换成FP16后可能被归零,导致除零或者数值异常。所以我在混合精度训练时会把eps设为1e-6甚至1e-7,先在小规模实验上验证稳定后再上大任务。

4.3 weight decay:L2正则与解耦衰减不是一回事

weight decay是一个非常容易混淆的点。传统SGD里,L2正则等效于weight decay,因为L2正则项引入的梯度等于2×lambda×weight,和直接weight decay在数学上是等价的。但在Adam这类自适应优化器里,L2正则梯度同样会被自适应机制缩放,导致正则化强度被扭曲,这时候L2正则和weight decay就不再等价了。

AdamW的思路是让weight decay独立于梯度自适应过程:每次更新完参数后,直接让所有权重乘以一个(1 - lr * weight_decay)的系数。这样权重衰减的强度与梯度的scale无关,行为更可控。这也是为什么现在几乎所有Transformer训练代码都用AdamW而不是Adam。

实际操作里,weight decay的默认值建议从0.01开始尝试。如果你发现模型过拟合严重,可以适当调大,比如0.05;如果发现欠拟合、训练loss下不去,可以调小甚至改成0。有一点要注意:SGD和AdamW的weight decay设置不能互相照搬,前者的0.0001可能合理,后者的0.0001很可能几乎没有效果。

4.4 梯度裁剪:防止训练被一个异常batch击穿

梯度裁剪是在优化器更新之前,把梯度的范数限制在一定范围内。它对长序列Transformer、GAN、强化学习这类训练不稳定的任务尤其重要。

使用顺序固定为:

  1. 前向传播,计算loss。
  2. 反向传播,得到梯度。
  3. 梯度裁剪,限制梯度范数。
  4. 优化器step。
  5. 可能的调度器step。

PyTorch里的典型写法是:

loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step()

max_norm的选择要根据实际梯度分布来确定。我习惯在训练刚开始时打印一段时间的梯度范数,取一个比较有代表性的中位数偏大的值作为阈值。太小的max_norm会压制正常更新,太大的阈值形同虚设。

有一个反直觉的点:梯度裁剪并不是越频繁越好。有时梯度范数异常偏大反而是梯度本身有意义的更新,强行裁剪会导致震荡。所以除非任务本身非常不稳定,我一般只在NLP大模型或GAN训练里使用,并且会先用小模型跑几十步观察。

5. 不同场景的优化器选型:别拿同一套参数跑遍所有任务

5.1 CV任务:从头训练和微调是两个世界

图像分类、目标检测、分割这些CV任务里,从头训练的情况下,传统做法是用SGD+Momentum搭配warmup和余弦退火。SGD在足够的训练时长下往往有更好的泛化性,很多经典模型复现的官方代码仍然使用它。

但如果你是在ImageNet预训练模型基础上做下游任务微调,情况就完全不同了。这时模型权重已经包含很强的先验特征,AdamW配合小学习率能更快适应下游数据,而且不容易破坏原有特征。我在做迁移学习时常用AdamW,lr从1e-5起步,观察验证集loss变化再逐步调整。

简言之:CV任务别盲目迷信某一个优化器,先问自己一句"我是在从头训练还是在微调"。

5.2 NLP与大模型:AdamW是事实标准

NLP领域几乎全面被AdamW统治。从BERT、GPT到各类LLM,整理训练日志你会发现几乎都是AdamW + weight_decay=0.01 + warmup + 余弦调度。原因在于Transformer对自适应优化器的依赖性很强,SGD直接训练Transformer效果通常很差,收敛慢且不稳定。

大模型训练里还有一个突出问题就是优化器状态显存占用。我前面提过,1B参数量模型用AdamW,优化器状态就可能占掉8GB以上。在这个体量下,很多人会结合混合精度、梯度累积甚至8-bit Adam等手段来压显存。

如果batch size需要扩大到几千甚至上万,LAMB可能比AdamW更稳。LAMB在每层计算归一化的更新量,让不同层的尺度差异不影响整体训练。但LAMB对超参数更敏感,复现成本更高,个人项目一般没必要碰。

5.3 GAN与强化学习:不稳定任务的特殊策略

GAN训练最大的特点是生成器和判别器的训练目标相互博弈,所以优化器选择会影响整个平衡。很多经典GAN实现使用Adam,而不是SGD,因为Adam的自适应特性让双方在对抗中保持相对稳定的更新幅度。我做过一个图像生成项目,生成器用lr=2e-4、判别器用lr=1e-4,各自使用Adam,beta1甚至被调到0.5来降低动量惯性,避免对抗过程中的振荡。

强化学习则是另一个极端。策略梯度方法里,单次采样带来的梯度方差极大,因此极少用纯SGD,Adam或RMSProp更常见,而且梯度裁剪几乎是必然选项。在这些不稳定任务里,优化器的选择更多是为了"稳住训练",而不是追求极致的收敛速度。

汇总一下我的选型经验:

任务类型推荐优化器常用学习率关键注意点
图像分类(从头)SGD+Momentum0.1 → 调度下降warmup + cosine
图像分类(微调)AdamW1e-5 ~ 3e-5尽早观察过拟合趋势
目标检测SGD(Momentum) / AdamW0.01 / 1e-4训练时长较长用SGD更稳
NLP预训练AdamW1e-4 ~ 3e-4weight_decay=0.01, warmup
NLP微调AdamW1e-5 ~ 3e-5小lr防止破坏预训练特征
GANAdam1e-4 ~ 2e-4beta1常设0.5
强化学习Adam / RMSProp3e-4 ~ 1e-3配合梯度裁剪
大规模分布式预训练AdamW / LAMB1e-4级别优化器状态显存优化

6. 一个真实案例复盘:优化器如何影响整个训练节奏

6.1 从现象到配置:一次文本分类任务的完整调优过程

去年我做了一个文本情感分类项目,数据集不算大,约有20万条样本,模型用的是预训练BERT后接分类头。我当时最直观的冲动是"直接拿官方AdamW默认配置开跑",于是初始lr=5e-5,weight_decay=0.01,batch_size=32。结果跑了5个epoch,验证集准确率只有80%出头,明显不如预期。

排查时我做了个对照组实验:

  • 组A:固定初始lr=5e-5,不做warmup,weight_decay=0.01。
  • 组B:初始lr=2e-5,加500步warmup,weight_decay=0.01。
  • 组C:初始lr=2e-5,加500步warmup,weight_decay=0.1。
  • 组D:初始lr=2e-5,加500步warmup,weight_decay=0.01,改用SGD+Momentum。

结果很有意思:组B比组A提升了1.2个百分点,说明warmup和稍低的学习率在微调场景里很重要;组C的weight_decay过大后出现了明显欠拟合;组D用SGD在20个epoch内都没追上AdamW的收敛速度。最终我用组B的组合训练,准确率达到85.8%。

这个项目让我真正意识到,优化器参数不是一个"设置好就完事"的东西,它和warmup、weight_decay、batch size、训练epoch紧密咬合,任何一个失衡都会反映在最终的指标上。

6.2 复盘要点:优化器永远不是孤立的一个旋钮

复盘下来,我总结了三条经验:

第一,任何优化器配置都不要直接照搬论文或开源代码,你需要克隆一个最小实验,把学习率、weight_decay、warmup、训练轮数当成一组整体变量去调,而不是单独拧某个旋钮。因为它们在训练过程中会互相影响,单独调lr而不动warmup,效果可能很差。

第二,训练日志里一定要记录优化器状态相关的指标,尤其是梯度范数、学习率实际变化曲线。很多异常训练问题,比如loss突然变高、梯度爆炸,都可以从这些指标里提前发现端倪,而不是等loss出问题后再去猜。

第三,快速验证阶段优先用AdamW,跑通逻辑后再根据任务特点换优化器。我见过不少同学在项目早期就在SGD和Adam之间反复横跳,结果每个优化器都只跑了几个epoch,根本不足以形成有效结论,白白浪费算力。

这个项目之后,我的Model-Optimizer认知有了明显变化:它不是一个独立的调参步骤,而是整个训练管线的控制器。你选择怎样的优化器、怎样的学习率节奏、怎样的衰减策略,几乎决定了模型在数据资源下能否训练成功。与其追求某个"神级优化器",不如把SGD、Momentum、AdamW这些基础方案的原理吃透,然后在一个具体任务上老老实实做几组对照实验。我自己在踩过那些NaN、震荡、收敛停滞的坑之后,现在更愿意在优化器环节多花两天时间,而不是等到模型训了一半再回头改配置。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询