☰
深度学习优化器选型与调参实践:从SGD到AdamW的完整指南
2026/10/1 6:25:52 网站建设 项目流程

同一份代码、同一种模型结构、同一批训练数据,为什么别人跑出来的精度就是比你高两个点?我在刚转机器学习那会儿,经常卡在这个问题上。起初我以为是初始化种子的问题,后来逐一排查才发现,差距往往集中在两处:优化器选型和优化器配参。当时我用的是一套压根没调过的Adam默认参数,而对方的配置里做了warmup、调了weight decay、甚至换了更匹配任务的优化器。那一次对比实验之后,我才真正开始认真对待"Model-Optimizer"这个词——它远不是model.compile(optimizer='adam')那么轻飘飘的一句话。

这篇内容,我把它定位成一份从原理到实操的优化器与模型优化笔记。适合刚入门的算法工程师、自己跑模型的学生,也适合那些模型能跑但总差一口气、想系统梳理优化思路的从业者。你会看到优化器内部到底在干什么、不同任务应该怎么选、参数之间怎么联动,以及训练收敛之后还能做哪些推理侧的优化。

1. 先从根上理解优化器:它到底在调什么

很多教程上来就甩公式,搞得优化器像个黑盒。但优化器要说复杂也确实复杂,要说本质,其实一句话就能概括:它决定了参数往哪个方向走、每一步走多远,以及如何根据走过的路调整后续的步伐。

1.1 损失曲面与"盲人下山"的比喻

把训练过程想象成一个盲人站在山坡上,要摸到山谷的最低点。损失函数就是地形,模型参数就是盲人的坐标。每一步,盲人都要靠脚下的坡度感来判断方向——坡度就是损失对参数的梯度。

但现实里这个山坡极度崎岖。有平坦的高原(梯度几乎为零)、有狭窄的峡谷(一个方向陡峭另一个方向平缓)、有局部凹陷的坑(局部极小值)、还有马鞍形的隘口(鞍点)。如果只靠原始梯度硬走,盲人会在峡谷两侧来回震荡,在马鞍点原地打转,在高原上寸步难行。优化器家族这些年一直在做的事情,本质上就是给这个盲人配不同的装备——帮他记住惯性(Momentum)、帮他感知哪条路更陡(自适应学习率)、帮他在平地上迈大步(二阶动量)。

1.2 三种改进方向:动量、自适应、二阶近似

我把优化器的发展梳理成三条主线:

  • 动量方向:SGD+Momentum、NAG。核心是让更新方向不仅看当前梯度,还叠加历史梯度的指数衰减平均。你可以理解为给盲人一根手杖,帮他记住刚才的走向,遇到小坑时能借着惯性冲过去。这条线解决的是震荡和局部极小值问题。
  • 自适应学习率方向:AdaGrad、RMSprop、Adam、AdamW。核心是每个参数单独分配学习率:梯度大的参数步子迈小点,梯度小的参数步子迈大点。这解决的是稀疏特征和大坡度峡谷并存时的平衡问题。
  • 二阶近似方向:KFAC、L-BFGS以及一些自然梯度方法。核心是直接估计损失曲面的曲率,用二阶信息修正更新方向。效果通常很好,但计算和内存开销太大,在深度学习里没有成为主流。

大部分人的认知集中在第二条线,也就导致了一个常见误区:以为Adam就是最优解。实际上,这条线里不同算法的行为差异非常大,直接替换往往能让训练曲线大幅变化。

1.3 一张表看懂SGD、Momentum、RMSprop、Adam的关系

我用一个表格来对比主流优化器的更新逻辑,方便你理解它们之间的血缘关系:

优化器核心更新逻辑关键超参典型短板
SGD参数扣掉学习率乘梯度lr收敛慢,容易卡在鞍点或局部极小
SGD+Momentum维护速度变量,衰减叠加梯度lr, momentum(默认0.9)对稀疏特征不友好
AdaGrad累计梯度平方,学习率除以累计量lr, initial_accumulator累计量不断增大,学习率会衰减到零
RMSprop用指数衰减代替累加,解决AdaGrad学习率消失lr, decay(通常0.9)参数多,需要手调细节
AdamRMSprop + Momentum,同时维护一阶和二阶动量lr, beta1, beta2, eps泛化性不如SGD,有权重衰减实现偏差
AdamW修正Adam的权重衰减实现位置lr, beta1, beta2, weight_decay对lr的敏感度仍然偏高

看到这里你应该能感受到,所谓的"选择优化器",实质上是在选择一整套更新策略,而不只是换一个优化器名字。

2. 我的Model-Optimizer选型经验:不同模型该配什么优化器

选型没有银弹,但确实存在比较可靠的默认路线。我自己这些年跑下来的经验是:先明确任务类型和数据规模,再决定优化器家族,最后细调参数。

2.1 小样本和CV任务:带动量的SGD依然是王者

如果你做图像分类、目标检测这一类任务,并且数据集不算大(比如几万张图以内),我强烈建议你先跑一版带动量的SGD。PyTorch里就是optim.SGD(model.parameters(), lr=0.1, momentum=0.9, weight_decay=1e-4)这个经典配方。

为什么SGD在这种场景下还这么能打?核心在于它的"简单":更新方向主要由当前梯度主导,不会像自适应方法那样被历史梯度平均干扰。这让它更容易收敛到更平坦的极小值区域,而平坦的极小值往往对应着更好的泛化性能。深度学习里一个很出名的现象就是:SGD+Momentum训出来的权重,即便人为加一点扰动,损失变化也很平缓;而Adam训出来的权重,往往处于尖锐的极小值里,扰动一下损失就飙上去了。

2.2 NLP和Transformer结构:AdamW几乎是默认答案

BERT、GPT这些Transformer架构出来之后,AdamW几乎成了标配。原因有两点:

第一,Transformer里大量使用LayerNorm和残差连接,网络的各个层之间梯度尺度差异非常大,自适应学习率的能力可以有效应对这种尺度不均衡。

第二,正确的权重衰减实现方式很重要。最早PyTorch里optim.Adam支持weight_decay参数,但这个参数当时是在更新完参数之后,直接把lr * weight_decay * param加到参数上——也就是说权重衰减项被藏在学习率里面。AdamW把这部分拆出来,让权重衰减与学习率解耦,让衰减量是固定的比例,而不是随学习率变化。这个细节直接影响了Transformer训练时的稳定性与最终精度。

我自己的经验是:用Hugging Face的transformers库或者自己手写训练循环时,AdamW配合lr=2e-5到5e-5,weight_decay=0.01,就能在大多数NLP任务上稳定收敛。不要乱动beta参数,尤其是beta2。

2.3 超大batch训练:LAMB让你敢把batch size拉大

做大规模预训练时,往往要把batch size推到几万甚至几十万。普通Adam在小batch下表现良好,但batch size一大,学习率如果跟着线性放大,收敛就极其不稳定。LAMB(Layer-wise Adaptive Moments for Batch training)的核心做法是:对每一层单独计算信任比例,再乘上该层的更新量。

实际使用中,LAMB可以把batch size推到几十万同时保持线性缩放学习率。我在一次大规模文本预训练实验里,把batch size从4096提到32768,用LAMB之后收敛速度几乎线性提升,训练时间肉眼可见地缩短。如果项目里遇到必须上超大batch的场景,直接考虑LAMB。

2.4 我踩过的选型翻车案例

有一个项目让我印象很深:图像生成任务,数据量中等,我当时贪方便直接用了Adam默认配置。训练了大概10个epoch,发现生成质量一直不理想,图像总是模糊且颜色偏灰。后来换了SGD+Momentum,做了学习率warmup和cosine退火,同样epoch数下生成清晰度明显提升。这个案例让我深刻意识到:Adam并不适合所有任务,尤其是数据量不大、模型不是特别深的情况下,SGD族反而能带来更好的泛化结果。

另一个翻车是:在Transformer模型上用普通Adam而不是AdamW,导致weight decay行为异常,训练起来loss下降正常,但验证集指标怎么也上不去,后来横向对比才发现是权重衰减位置引起的正则化效果差异。

3. 把这些超参调明白,模型效果立刻上一个台阶

选对了优化器只是开始。同一套优化器,不同参数配出来可能是一个天上一个地下。这一节我把最影响结果的几个超参逐个拆开说。

3.1 learning rate:永远是最该花时间的超参

学习率决定了每步参数更新的步长。我习惯把学习率想象成"步子大小":步子太大了容易在最优值附近震荡甚至发散;步子太小了训练半天还在原地磨蹭。

实际操作里有一个几乎能work的流程:先做学习率热身扫描(learning rate finder),也就是从小学习率到大学习率跑几个小batch,记录loss变化,找到loss下降最快且最平稳的那段区间对应的学习率,然后在这个范围内选一个初始值。

以SGD为例,ImageNet分类任务常用0.1的初始学习率配合bs=256;但如果你用bs=64,那就把学习率缩成原来的四分之一左右,即0.025。线性缩放规则是:学习率大约随batch size等比例变化,但实际项目中要考虑梯度噪声的变化,通常不会严格线性,而是略微保守一点。

3.2 weight decay和L2正则之间的区别

很多人以为weight decay就是L2正则,其实这两个在SGD里等价,在Adam里完全不同。L2正则是在loss函数里加权重平方和,然后求梯度,相当于把权重乘以一个小于1的系数;而weight decay是参数更新之后,直接对参数做一次比例缩小。

区别在Adam里被放大了。因为Adam会对梯度做归一化处理,L2正则项的梯度也会被归一化,导致L2正则在Adam里成了一个"动态变化"的惩罚,效果很不稳定。AdamW的修正正是为了让权重衰减表现得像真正的L2正则,但又不依赖于学习率和梯度尺度。

实际经验:Transformer训练时weight_decay=0.01是个稳妥起点;图像分类用SGD的时候,weight_decay=1e-4到5e-4之间比较合适。太大会欠拟合,太小又没有正则化效果。

3.3 beta1/beta2和eps:Nan损失的一大来源

Adam里的beta1和beta2分别控制一阶动量(均值)和二阶动量(方差)的指数衰减速率。默认值是beta1=0.9, beta2=0.999,大多数情况下不用动。但如果你训练的是长序列模型或者强化学习这类梯度方差很大的场景,beta2太大会让二阶动量更新过慢,导致学习率在前期偏大,出现NaN损失。

eps是防止分母为零的小常数,默认1e-8。如果你用的是混合精度训练,eps太小会导致梯度过小的参数更新不稳定,这时可以调大到1e-6到1e-7。

我还遇到过一种情况:损失在前几百步是正常的,突然变成NaN,查了很久发现是eps太小,模型某一层的梯度在fp16下直接被裁成了0,二阶动量为0,更新量爆炸。把eps从1e-8调成1e-6之后,问题立刻消失。

3.4 gradient clipping:什么时候必须开

梯度裁剪不是优化器的参数,但它和优化器配合紧密。当你的模型存在梯度爆炸风险时(RNN、Transformer、GAN),这一步几乎是保命的。

PyTorch里的写法是:

torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

放在loss.backward()之后、optimizer.step()之前。max_norm可以理解成梯度的上限——把所有参数的梯度拼接成一个大向量,计算整体范数,超过上限就等比缩放。这个操作能防止某个大梯度过大,把更新方向带偏。

实际案例:我在训练一个深度Transformer时,如果没有梯度裁剪,训练到中后期经常出现损失突然飙升的情况;加了max_norm=1.0之后,训练曲线变得非常稳定,最终精度也更高了。对于SGD和Adam,clip的值有所不同,SGD可以宽松一点(比如5.0),Adam建议从1.0试起。

4. 学习率调度和训练策略:优化器之外的另一半工程

优化器负责"这一步怎么走",学习率调度负责"整个路程的步伐计划"。二者必须配合,否则优化器参数调得再好也白搭。

4.1 warmup到底是玄学还是数学

warmup指训练初期把学习率从零或很小的值线性(或指数地)升到目标值。很多人理解成"让模型先稳定一下",但实际机制没那么玄。尤其是Adam类优化器,训练初期二阶动量还没积累起来,一开始就用大学习率会导致参数更新量被异常放大,warmup就是在等动量估计稳定下来。

具体做法:前t_total * warmup_ratio个step,学习率从0线性升到峰值。一个常见设置是总训练步数10000,warmup_ratio=0.06,即前600步线性warmup。Transformer训练时这个比例很重要,一般设置在5%到10%之间。SGD在极小数据集上可以不warmup,但在大中型数据集和深度模型上,warmup依然能提升稳定性。

4.2 cosine annealing和one cycle怎么选

训练到后期,学习率应该下降,让参数在局部极小值附近精细搜索。最常见的两种方式:

  • Cosine annealing:学习率按余弦曲线从峰值降到极小值附近。实现平滑、好用,配合warmup基本是万金油。
  • One Cycle:先在warmup阶段升到峰值,再在剩余阶段降到比初始值还低的数值,同时还可以配合学习率和动量的一体化调度。One Cycle的收敛速度通常比cosine更快,但对超参更敏感。

我自己的习惯:兼顾效果和稳定的场景选warmup + cosine;需要快速跑通实验、或者尝试打破当前指标的瓶颈时,会特意试一次One Cycle。后者经常能在精度上带来意外惊喜。

4.3 混合精度训练时优化器的隐藏配合

混合精度(AMP)现在几乎是训练的标配。它让模型前向和反向使用fp16,但优化器状态和主权重保留在fp32。这就不只是节省显存的问题了,它还改变了优化器对梯度的感知精度。

实际操作里有一个关键点:在AMP下,梯度是fp16的,数值范围有限,很小的梯度会被冲掉。这时eps的作用比fp32训练时更大。PyTorch的GradScaler会动态调整loss缩放因子,避免梯度下溢。如果你发现自己训练时Scaler的缩放系数持续走低,那大概率是模型本身梯度就不稳,而不是优化器的问题。

还有一个常见误区:混合精度训练时,为了让损失保持稳定,有些人不加思考地把学习率调低。这种做法经常得不偿失,因为AMP本身并不需要调低学习率,应该保持和fp32一致的配置。我踩过的坑就是:以为AMP像换了一个优化器一样改变一切,结果把学习率调低了一个量级,收敛速度肉眼可见地变慢。

4.4 EMA:不改变loss曲线也能提分的trick

指数移动平均(Exponential Moving Average)不算优化器,但它和梯度动量在数学上是同一思想:对参数做移动平均,得到的影子参数往往比当前参数更平滑、泛化更好。

PyTorch里可以用torch.optim.swa_utils或者自己维护一份影子权重。我在图像分类、目标检测任务里,加了EMA之后普遍能提升0.2到0.5个点,几乎零成本。需要特别注意的是:EMA要在训练后期开始才有效果,而且尽量不要从头就EMA,否则影子参数会被前期不稳定的权重污染。轻量做法是每训练N步更新一次影子参数,在验证阶段用影子参数去推理。

5. 训练完不算完:推理侧模型优化的三板斧

很多人把"Model-Optimizer"理解成训练优化器,但在实际工程里,模型优化还包含推理侧的速度与体量优化。训练收敛只是第一步,能不能跑在用户的设备上、能不能实时响应,是另一场硬仗。

5.1 剪枝:把冗余参数删掉

剪枝做的是:找到模型中"不太重要"的权重或通道,把它们直接去掉,减少计算量。最粗放的做法是幅度剪枝——绝对值小的权重直接置零。但实践中更常用的是结构化剪枝,比如对卷积通道做剪枝,因为这样真的能减少计算时间,而稀疏矩阵在GPU上不一定更快。

我做过一次BERT的通道剪枝实验,把FFN层的中间维度从3072剪到2048,精度掉了大约1%,但在CPU推理时速度提升了30%。如果你的业务对时延敏感,这往往是性价比很高的一步。关键在剪枝之后一定要做微调(fine-tuning),否则精度会掉得很难看。

5.2 量化:从FP32到INT8的实战价值

量化是当前推理优化里最有效的通用手段。把权重从FP32(4字节/参数)压到INT8(1字节/参数),模型体积直接降到四分之一,推理速度在支持INT8的硬件上能有2到4倍的提升。

常见的两种路线:

  • PTQ(训练后量化):拿少量校准数据跑一遍,统计权重和激活的数值范围,直接转INT8。简单快速,但部分模型精度损失明显。
  • QAT(量化感知训练):在训练过程中模拟量化误差,让模型去适应低精度。精度损失小,但要重训,成本高。

以Transformer为例,PTQ在一些任务上精度损失在2%以内,QAT可以把损失控制在0.5%以内。我的建议是:先做PTQ,碰到精度问题再上QAT,不要一上来就重训模型。

5.3 蒸馏:让小模型继承大模型能力

知识蒸馏的核心思想是:训练一个小模型(学生),让它去模仿大模型(教师)的软输出。教师输出的概率分布里,往往包含了"类别之间的相似性"——比如一张猫的照片,模型认为它是猫的概率0.7,是狐狸的概率0.2,这种软信息是独立训练小模型时根本无法获得的。

蒸馏在NLP和CV里都是非常成熟的套路。我用BERT蒸馏过一个小模型,保留了99%的精度但速度提升3倍。实际实施时要注意温度参数的选择——温度太高,软标签太平滑,学生会学不到关键差异;温度太低,又成了硬标签,没起到蒸馏的作用。常规做法是温度取2到4,损失函数里同时加权教师软目标和真实标签的交叉熵。

5.4 部署友好的优化组合:剪枝+量化+蒸馏一起上的顺序

做完剪枝、量化、蒸馏之后,应该按什么顺序组合它们?我的经验是:先蒸馏,再剪枝,最后量化。

蒸馏是"能力转移",属于第一层,让模型骨架小下来;剪枝是"结构精简",让模型计算量进一步下降;量化是"精度压缩",在真正的部署平台上压体积。如果你先做量化再做剪枝,量化的统计参数会被剪枝破坏,导致精度下降更严重。这个顺序我踩过坑才得出的结论,一开始先剪枝再量化,最终精度掉了4%,换成反过来的顺序后,整体只掉了1.5%左右。

在部署阶段,还要根据具体推理引擎做适配验证,比如不同的推理后端对量化算子的支持程度不同,需要逐个基准测试。这些内容适用于需要把模型发布到生产环境的场景,如果你只是自己跑研究实验,可以忽略这一节。

一些实操里的碎碎念

写了这么多,最后分享几个我在实际实验里沉淀下来的习惯。

第一,跑任何新模型之前,先花十分钟做一次学习率扫描。这个成本极低,却能直接决定你后续所有实验的有效性。我见过太多人一上来就用一个拍脑袋的学习率,训练了十几个epoch发现loss不降,才开始怀疑模型结构问题。其实换个学习率,问题直接就解决了。

第二,改动一次只动一个变量。优化器、学习率、weight decay、batch size、epoch数,这些参数之间高度耦合。一次动两个以上的变量,即使效果变好了,你也没法确定是哪一步起了作用。实验记录里,参数变化的乱账最难还。

第三,怀疑优化器问题时,先回退到SGD+Momentum做对照。SGD虽然收敛慢,但它的行为最可预测,几乎不会出现莫名其妙的NaN或者loss飞升。如果SGD都表现很差,问题大概率在数据或模型结构,不在优化器配置上。

我之前踩过的最深一个坑是:训练一个Seq2Seq模型,用了默认Adam,conv层和attention层梯度尺度差距极大,模型始终不收敛。后来把学习率调低、把conv层单独设置更小学习率,才慢慢跑出效果。这也让我养成一个习惯——当网络结构里有不同尺度的子模块时,要主动考虑给它们配不同学习率,或者用模型不同区域分组设置参数。

最后,再分享一个我这几年一直沿用的配参范式:Transformer用AdamW + warmup + cosine + weight_decay=0.01;CNN图像任务用SGD+Momentum + weight_decay=1e-4 + One Cycle;生成模型先试AdamW再回头对比SGD;强化学习优先考虑梯度裁剪和保守学习率。这套范式不一定是最优的,但能让你在一个稳定且可预测的基础上开始工作,避免从头踩坑。希望这篇笔记能帮你少走几段弯路。

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

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

立即咨询