☰
Model-Optimizer实战:从SGD到AdamW的选型与调参全攻略
2026/9/29 19:12:12 网站建设 项目流程

优化器这个东西,说大不大,说小不小,但你在AI项目里只要稍微认真调过模型,就会知道它绝不是“换个名字那么简单”。我早期做视觉模型的时候,习惯性地把优化器当成固定参数,模型训不出来就怪网络结构、怪数据增强,折腾半天才发现,问题有时候就出在这个最容易被忽略的组件上。后来我专门花了很长时间研究优化器的选型与调参,才意识到一个事实:优化器本质上是训练系统的“底层操作系统”,它决定了模型能在多大程度上利用好你的数据和算力。这篇就围绕Model-Optimizer这个话题,把我踩过的坑、验证过的配置、以及背后的原理逻辑都整理出来,希望能帮你少走几条弯路。

1. 为什么Model-Optimizer值得单独聊

1.1 优化器不是超参数,是训练系统的“底层操作系统”

很多人把优化器当作一个可有可无的超参数,落地的时候默认用Adam,学习率给个1e-3,然后就去调别的了。但实际训练中,优化器选择的差异可能比模型结构带来的差异还明显。打个比方:网络结构决定了模型的理论容量上限,数据决定了信息的上限,而优化器决定了你实际能逼近这个上限多少。一个不好的优化器,会让你在同样的算力下多花3到5倍的训练时间,甚至直接不收敛。

我最早意识到这一点,是在一个文本分类任务上。当时同一个BERT变体,我换了两套优化器设置,一套是AdamW加线性warmup,另一套是普通Adam配固定学习率,结果前者的验证集准确率比后者高出将近2个百分点。注意,模型结构、数据、种子完全一样,差别全在优化器及其配套策略上。这还不算完,后来在YOLO类的检测任务上,我又发现SGD配上合适的momentum和weight decay,竟然比Adam跑出更高的mAP。从那时起我就养成一个习惯:每到一个新任务,先花半天把优化器和学习率策略挨个试一遍,而不是直接沿用上次的配置。

所以,与其说“优化器”是一个超参数,不如说它是一个训练系统。它包含了更新规则、学习率调度、权重衰减策略、梯度裁剪等多个层次。你在项目里需要把它当成一个整体来设计,而不是单独调一个learning rate。

1.2 一个案例对比,同样的模型不同的命运

这里分享一个我自己做过的小实验。同样是ResNet-50在ImageNet子集上的训练,我固定batch size为256,训练90个epoch,只改优化器,其他一切不变。结果如下表所示:

优化器学习率最终Top-1准确率达到70%所需epoch
SGD (momentum=0.9)0.1 (cosine decay)76.4%38
Adam1e-3 (cosine decay)74.8%45
AdamW1e-3 (cosine decay)75.6%41
SGD (momentum=0.9)0.01 (固定)72.1%52

可以看到,SGD配大学习率加余弦退火在这个任务上赢了,Adam反而没那么理想。但这里有个关键前提:SGD需要一套精心设计的学习率策略才能发挥威力,而且它对初始学习率极其敏感;Adam的优势在于省心,默认参数在很多任务上都能跑出差不多的结果,但想吃满模型性能就需要更多的调参配合。

这个实验也引出一个重要观点:选优化器不是选“最好的”,而是选“最适合当前任务和资源约束的”。如果你有足够的调参时间,SGD+momentum在多数视觉任务上仍然有一战之力;如果你的项目强调快速迭代,Adam/AdamW更合适;如果你在训Transformer大模型,基本就锁死AdamW了。

2. 主流优化器的核心原理与选型逻辑

2.1 SGD+Momentum:经典但不好伺候

随机梯度下降(SGD)是所有优化器的基石,它做的事情很简单:沿着当前batch梯度的反方向更新参数。但纯粹SGD有两个让人头疼的问题:一是收敛慢,尤其在损失面比较“平缓”的区域,步子太小,容易卡死;二是容易震荡,在梯度方向剧烈变化的区域,参数会来回摆动,训练不稳定。

Momentum的引入就是为了解决这两个问题。它的核心思想是:让参数更新不再只看当前梯度,而是累积历史梯度的“动量”,就像推一辆沉重的购物车,你推一下,它会借着惯性继续往前滑一段。对于方向一致的梯度,动量会不断加速;对于方向频繁变化的梯度,动量会中和掉震荡。动量系数通常取0.9,表示保留90%的上一步动量,加上10%的当前梯度。这个0.9的取值经验性很强,实际调参中我也试过0.95和0.8,效果差异不算大,但0.9在不同任务上表现最稳定。

SGD+momentum的脾气是“上限高但门槛也高”。它的收敛效果非常依赖学习率策略,通常需要一个较大的初始学习率(比如0.1),配合warmup和cosine decay才能在视觉任务上发挥威力。如果你直接用默认学习率0.001跑SGD,大概率会在loss还很差的时候就陷入瓶颈。我早期就吃过这个亏,总以为模型不行,实际上是我没给SGD配一个像样的学习率“剧本”。

2.2 Adam:自适应学习率的代表作与脾性

Adam的全称是Adaptive Moment Estimation,它融合了两个核心机制:一是像momentum一样维护一阶动量(梯度的指数滑动平均),用于平滑更新方向;二是维护二阶动量(梯度平方的指数滑动平均),用于感知每个参数的历史梯度幅度,进而对每个参数自动调整学习率。

这里的直觉是:对于梯度幅度大的参数,说明这个方向可能比较“陡峭”,步长应该缩小;对于梯度幅度小的参数,说明方向比较“平缓”,步长应该放大。Adam就是这么通过二阶动量实现了“自适应”学习率,所以它对初始学习率的敏感度远低于SGD。

但Adam也有一个深层的毛病:它会挤出模型的泛化能力。原因比较复杂,一个直观的解释是,Adam的自适应机制本质上是对梯度的缩放,这种缩放会改变参数更新路径,让模型到达的极值点“尖锐度”偏高,泛化性相对SGD略逊。另一个常见问题是,Adam在训练后期二阶动量累积偏大,导致有效步长越变越小,不利于收敛到更优的极值区域。

我自己用下来的感觉是,Adam非常适合NLP、多模态这类模型结构和数据分布都比较复杂的任务,因为它能让训练快速稳定下来,不需要花太多时间在初始学习率上纠结。但如果要在CV任务上追求极致精度,Adam往往需要配合大幅度的学习率衰减,才能稍微弥补它泛化性偏弱的问题。

2.3 AdamW:解耦权重衰减才是正解

AdamW这个W,指的是它的创建者发现Adam默认实现里做L2正则化的方式有问题,进而提出的改进版本。L2正则化的标准做法是把权重衰减项加到损失函数里,这样计算梯度时,正则项的梯度会混入其中。但Adam在自适应更新规则下,会把正则项的梯度也做“自适应缩放”,导致不同参数的权重衰减强度变得不一致,正则效果被扭曲。

AdamW的解耦思路是把权重衰减从损失函数中拆出来,不参与梯度的自适应计算,而是直接在参数更新的时候减去一个固定比例的衰减值。这样做的好处是,权重衰减不会再被Adam的缩放机制影响,正则强度更可控,而且不需要在损失函数里加额外项,写代码也更干净。

我在实际项目中几乎都用AdamW替代掉Adam,尤其是在Transformer类模型上,二者的差异非常明显。同样的任务,AdamW能够让训练更稳定,而且在小数据集上不容易过拟合。搭配上一些新出现的优化器或调度策略,AdamW已经成了我训练大模型时的默认选择。

3. 学习率策略:优化器真正的胜负手

3.1 学习率基线:不同优化器的不同脾气

很多人问“学习率该设多少”,这个问题没有标准答案,但有默认基线。SGD+momentum的常用基线是0.01到0.1,需要注意配合batch size调整;Adam一般取1e-3,batch size偏大时则降到1e-4量级;AdamW在大模型场景下通常直接给1e-4或更低,配合warmup慢慢爬升。

这里面有一个容易被忽视的“batch size和学习率联动”原则:batch size翻倍时,梯度估计更稳定,可以适当调大学习率。最简单的缩放规则是线性缩放:batch size从64涨到128,学习率相应乘以2。但这种规则在超大batch size下会失效,所以在8卡、16卡并行训练时,我更习惯用平方根缩放,也就是batch size翻倍时学习率乘以根号2,这样会更稳一些。

上次我做一个多卡训练任务时,直接把单卡学习率用在多卡上,导致训练损失上升得厉害。排查了半天才发现是等价batch size变了,学习率却没有对应调整。这就是优化器“周边”细节的典型坑。

3.2 Warmup的必要性

Warmup指的是训练早期让学习率先从一个很小的值开始,逐步升温到目标学习率。它的必要性在于:模型初始化时参数是随机的,早期的梯度信号往往比较“杂乱”,如果一开始就用大学习率,容易把参数推到不理想的区域,而且这种早期破坏往往是不可逆的。

尤其是Transformer这类模型,没有预训练时候的稳定特征基底,对warmup非常依赖。我训过一个小型GPT模型,没有warmup时loss在最初几百步直接冲到9以上,训练两三个小时后都无法回到正常水平;加上warmup之后,模型很快就进入稳定下降状态。

实现上,warmup一般有两种:线性warmup(从0逐步线性增加到目标学习率)和指数warmup(从很小值逐步指数增长到目标学习率)。前一种简单直接,后一种更平滑。对于短训练任务,我建议min(warmup_steps, 500)个step内完成升温;对于长训练任务(比如上万step),可以按总步数的5%到10%来设定warmup时长。

3.3 余弦退火与重启

余弦退火(Cosine Annealing)是我个人最喜欢的学习率调度方式,思路很简单:让学习率按余弦曲线的形状逐步衰减到接近0。它的好处是前半程降得比较慢,给模型充足时间来探索较好的区域;后半程降得快一点,帮助模型收敛到更精细的极值点。

这里的“前半程慢、后半程快”并不是玄学。训练的目标是先找到大方向的“盆地”,再在盆地底部找到“坑底”。如果一开始就降低学习率,模型就没有足够的探索能力离开较差的局部区域;如果到最后才降,模型又可能在坑底附近来回震荡。余弦退火正好提供了一个自然的尺度。

还有一种进阶玩法是带热重启的余弦退火(Cosine Annealing with Restarts),也就是学习率衰减到一定程度后突然跳回高位,让模型跳出当前盆地。这种策略在部分任务上能提升最终精度,但它对训练时间的要求较高,而且重启时机不容易掌握。我在图像分类和OCR识别任务上试过,有时候有效,有时候只是白白浪费训练时间,所以除非你有充裕的算力去试,否则谨慎使用。

4. 实战调参:一份可抄作业的配置清单

4.1 视觉模型怎么配

以我最近做的一个图像分割项目为例,这个任务用的骨干是类ResNet结构,我在调参时发现,视觉领域确实存在一套比较通用的默认配方。这里整理成表格,方便直接参考:

训练场景优化器学习率权重衰减学习率策略
小型CNN(几十万参数)AdamW1e-30.01~0.05Step Decay
ResNet级CV模型SGD+Momentum0.05~0.11e-4~5e-4Cosine Decay
检测/分割类复杂任务SGD+Momentum0.01~0.025e-4Cosine Decay
数据量极小(<1万)AdamW1e-40.1固定+早停

需要注意,小数据量场景下,过拟合风险比对优化器的追求更重要,所以权重衰减会设置得更大一些。另外,当数据集比较小时,我直接用固定学习率加早停,效果往往比复杂调度更好,因为模型没有足够的梯度信号来支持长时间的学习率变化。

4.2 Transformer类模型怎么配

Transformer类模型(BERT、GPT、ViT等)几乎成了深度学习的“通用积木”,它们的优化器配置也基本统一。我的默认配置是:AdamW、学习率1e-4到5e-5、权重衰减0.01到0.1、线性warmup加线性衰减或余弦衰减。

这里有一个重要细节:Transformer的权重衰减对象需要区分为“所有参数”和“部分参数”。实际应用中,像LayerNorm的bias、归一化层的gamma/beta这类参数,不应该施加权重衰减,因为它们本身是数值稳定的关键,正则化反而会干扰它们的自适应调整。一个常见做法是:只有矩阵类权重(如Linear层、Embedding矩阵)施加weight decay,bias和LayerNorm参数不施加。很多成熟的代码库中会用optimizer_grouped_parameters来分组设置。

我早期做BERT微调的时候不懂这个细节,给所有参数统一加weight decay 0.01,结果收敛速度明显变慢。后来我把bias和LayerNorm参数单独拎出来,设置weight decay为0,效果才恢复正常。所以如果你是手动写训练循环,这个点值得特别留意。

4.3 强化学习里的特殊处理

强化学习项目里用的优化器逻辑和监督学习有区别,核心在于样本分布的变化。强化学习的数据不是静态的,模型每个epoch的行为会改变下一个epoch的数据分布,这对优化器提出了更高要求。

在实际的RL训练中,我会用AdamW,但学习率要比监督学习更低一些,一般取3e-4到1e-4,同时要配合梯度裁剪。这里的逻辑是:强化学习的优势函数估计存在较大方差,如果学习率太大,一个batch的偶然高估就会让策略参数剧变,导致后续回合整体崩溃。梯度裁剪的作用类似安全带,它能保证单次更新的幅度不会过大,给训练一个相对平稳的步调。

还有一个提升实践是引入KL penalty来约束策略更新的跨度,这个属于策略层面的措施,但从优化器角度看,它实际上也间接限制了有效梯度的大小,和梯度裁剪一起,共同维持RL训练的稳定性。

5. 优化器周边:那些容易翻车的配套细节

5.1 混合精度训练下的scaler与optimizer

混合精度训练(AMP)现在几乎成了大模型训练的标配,因为它能显著加速训练并降低显存占用。但在AMP模式下,优化器相关的一个细节很容易翻车:梯度在FP16格式下幅度太小,直接做梯度更新会出现精度不足的问题。这时就需要GradScaler参与,它先把loss乘以一个缩放因子,放大梯度到FP16的表示范围,再在更新前把梯度缩放回去。

这里的坑是:如果忘了调用scaler.scale(loss)和scaler.step(optimizer),模型可能看起来在训练,但loss降不下去,因为你更新用的梯度全是接近0的噪声。还有一个小细节是scaler.update()的调用位置,它必须在每次迭代最后执行,用来动态调整缩放因子的数值,否则训练到后期可能出现溢出问题而不自知。

我一个朋友曾经在AMP训练中反复遇到“loss突然变NaN”的问题,我帮他排查时才发现,他在每次迭代结束后忘了调用scaler.update(),导致缩放因子永远停留在初始值,无法感知梯度溢出。修完之后训练一下子稳定下来。这类问题一般不会在模型结构上体现,但训练日志会非常诚实地告诉你“不对劲”。

5.2 多卡DDP下优化器状态如何保存与恢复

多卡训练(DDP)时的优化器状态管理,是一个看起来不起眼但实际很容易出错的环节。很多人在每张卡上都保存一份模型checkpoint,然后加载时随机拿一张卡的结果来用,这会导致优化器状态不一致,进而让“断点续训”变成“断点重训”。

正确的做法是:只在rank 0进程上保存模型和优化器状态,其他进程只保留模型结构。加载checkpoint时,所有进程要加载同一份优化器状态,确保每个进程的优化器内部统计值完全一致。这里特别要注意的是,AMP模式下还涉及scaler的状态,它也需要一并保存和恢复,完整的checkpoint应该包含model.state_dict()、optimizer.state_dict()、scaler.state_dict()以及epoch和global_step。

我之前就有过一次断点续训时acc突然下降的经历,查了半天,最后发现是scheduler的step计数没有保存,恢复时学习率跳回了初始值,直接导致模型在一轮大学习率更新后偏离了原来的优化轨迹。所以如果要用断点续训,优化器、学习率调度器和梯度缩放器的状态必须作为整体保存。

5.3 梯度裁剪与EMA的平衡取舍

梯度裁剪是防止梯度爆炸的手段,常见做法是设置一个max_grad_norm,比如1.0,然后将整个梯度的范数截断到这个值以内。这个操作在RCNN、Transformer和RNN训练里属于标配。需要注意的是,如果你用Adam类优化器,梯度裁剪的作用会被部分“吸收”,因为Adam对梯度本身有缩放效果;但对SGD来说,梯度裁剪的意义非常大,它直接决定了更新步长是否越界。

EMA(指数移动平均)是我在几乎所有视觉任务里的固定搭配,思路是维护一份参数的历史滑动平均值,用于最后的模型推断。这份平均后的模型往往比训练末尾的即时参数表现更稳定,尤其在目标检测和语义分割任务上能带来0.2到0.5个百分点的提升。它能与任意优化器搭配,但要注意它的滑动系数(比如0.999)需要配合总训练步数来设定,步数太短会让EMA模型偏离真实参数太远,反而劣化。

5.4 权重衰减到底该加在哪

权重衰减的目的是让模型参数不要太大,从而抑制过拟合。但具体实现里,并不是所有参数都需要被“衰减”。我现在的习惯是:除了bias和归一化层参数之外,其他权重都施加weight decay。很多主流框架的默认做法也类似,比如Hugging Face的Trainer工具就是按照这个规则自动分组的。

如果你用PyTorch写手动训练循环,可以这样构造optimizer的参数组:

optimizer_grouped_parameters = [ {"params": [p for n, p in model.named_parameters() if "bias" not in n and "LayerNorm" not in n], "weight_decay": 0.01}, {"params": [p for n, p in model.named_parameters() if "bias" in n or "LayerNorm" in n], "weight_decay": 0.0} ] optimizer = AdamW(optimizer_grouped_parameters, lr=1e-4)

这样明确分组,能让正则化作用到真正需要约束的权重上,又不干扰归一化的自适应调整。这个细节在论文里通常只是脚注,但实际训练中却能真实影响收敛速度。

6. 一个真实的调优复盘:把F1从0.87抬到0.91

6.1 基线情况

最后分享一个近期做过的完整案例。当时是在一个多标签文本分类任务上,模型用的是bert-base版本,数据量大概3万条,标签有26类,初始F1是0.87。最初的配置是常规操作:AdamW学习率2e-5,线性warmup加线性衰减,weight decay默认0.01,batch size 16,训练3个epoch。

这个效果能接受,但离上线要求差了一点。我决定从优化器这个维度做一轮系统调优。说实话,这类任务在绝大多数项目里就是“默认配置直接训练”,很多人不会在优化器上深挖,但我觉得只有把优化器这条线彻底捋清楚,才能真正把模型潜力榨出来。

6.2 调整过程

第一轮,我把学习率从2e-5提到3e-5,同时把weight decay提高到0.05。结果验证集F1涨到了0.882,提升幅度不大,但说明方向是对的。这轮调整的核心逻辑是:数据量不算小,让模型多“跑”一点,并加强正则化避免过拟合。

第二轮,我把warmup从10%降到6%,并把衰减策略从线性衰减切到余弦衰减。这里的原因是:在长序列任务上,后期过快降低学习率容易让模型固化到不够好的局部极值;余弦衰减的后半段更平滑,能给模型更多时间在精细区域搜索。这一轮F1到了0.895。

第三轮,我在优化器层面做了一次更激进的改变:把一阶动量beta1从0.9调整到0.95。这个调整的动机是让优化器对近期梯度的平滑更强,减少梯度噪声对更新的影响。出人意料的是,F1突破了0.9,达到了0.904。第四轮,我又把梯度裁剪从无到有,max_grad_norm设为1.0,稳定了训练后期的震荡,最终F1停在0.91。

6.3 最终结果分析

整个调优过程没有动模型结构,没有换模型体量,数据增强也没有变化,纯粹靠优化器及配套策略的组合拳,F1从0.87提到了0.91。这个绝对值看起来不算大,但在多标签分类任务里,68个样本的正确与否就对应了约0.1的F1变化,所以提升非常可观。更重要的是,这些调整不增加任何训练时间,实际只是换几个配置然后跑实验的成本。

从我的实践来看,优化器调优不是“碰运气”,它背后有几个明确杠杆:学习率的调度方式是第一杠杆,权重衰减的位置和量级是第二杠杆,优化器自身的超参数(momentum、beta值)是第三杠杆,梯度裁剪和EMA则是压轴的第四杠杆。一开始从这些方向入手,基本都能拿到稳定的提升。

7. 我的一些保留心得

优化器这个组件,很多人以为用默认参数就够了,但实际上,它是你与模型性能之间最后一道可调门槛。我自己踩过不少坑,也有一些怎么用都不太对的地方,这里一并分享出来。

首先,优化器参数一定要配合你的数据和任务量级来调整,不能照搬论文或别人的配置。论文里给出的学习率和weight decay往往是在他们特定的任务和数据规模下调的,直接拿来用,效果可能完全相反。我之前用某篇论文的优化器配置去复现一个图像分类模型,结果收敛极慢,后来才发现是因为我batch size比论文小了一半,学习率却没有减。

其次,如果你要做一个需要长期训练的大规模模型,我强烈建议你预留一个“优化器对照实验”的预算:至少跑三组,SGD+momentum、AdamW、以及你当前任务最推荐的优化器,每个配置都配上合理的学习率调度方式。这个对照看起来费时间,其实能帮你建立对这个任务的深度理解,后续所有模型迭代都会受益。

最后分享一个小技巧:在每一次训练结束后,记录下优化器相关的全部配置,包括初始学习率、warmup步数、衰减方式、weight decay、梯度裁剪阈值,以及当时的验证集指标。这些看起来琐碎的记录,实际上是你后续调优最宝贵的资产。我自己的项目笔记里,优化器配置永远排在模型结构之前,因为在我踩过这么多坑之后发现,模型结构决定了能力的上限,而优化器决定了你能实际到达的高度。

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

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

立即咨询