ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

从优化器选型到模型压缩:深度学习模型优化全面指南

从优化器选型到模型压缩:深度学习模型优化全面指南 做模型训练的人十个里有八个都听过“换了个优化器效果直接起飞”这种话剩下两个是踩了优化器参数的坑loss死活不降最后才发现问题出在权重衰减上。Model-Optimizer这个名字字面上看是一个工具但往下拆它其实覆盖了两层完全不同的东西一层是训练阶段告诉模型“怎么学”的优化器Optimizer另一层是模型训练完之后为了让它跑得更快、占得更少而做的推理侧优化Model Optimization。这篇文章就围绕这两件事展开把选型思路、参数调优、压缩手段和排坑经验一次讲清楚适合正在训练模型的算法工程师、准备做部署优化的推理工程师以及刚入行想把原理弄明白的初学者。1. 先把概念厘清Model-Optimizer到底在优化什么1.1 训练阶段的优化器模型能不能学出来一半看它在深度学习里优化器不是一个“锦上添花”的组件而是负责把模型参数一步步调整到最优解的驱动器。训练过程说白了就是一个循环前向计算损失反向传播梯度然后优化器拿着梯度去更新参数。这个“拿着梯度去更新参数”的动作看似简单但步长怎么定、方向怎么修正、历史梯度要不要记全都由优化器决定。我在实际项目里见过太多这样的情况模型结构没问题、数据也没问题但loss曲线就是异常难看要么剧烈震荡要么直接发散最后排查一圈发现只是优化器选错或者参数设置不合理。选SGD的时候不知道配momentum选Adam的时候不知道默认学习率也未必适合你的任务这些都是高频问题。所以训练阶段的优化器直接决定了模型能不能在合理时间内收敛到有意义的解重要性再怎么强调也不为过。1.2 推理侧的模型优化把模型做“轻”训练结束不等于工作结束。模型从实验到上线还要过部署这道关卡。推理阶段的优化关心的是另一组指标延迟一次推理要多久、吞吐单位时间能处理多少请求、显存占用、模型体积。推理侧优化手段常见的有四类剪枝、量化、知识蒸馏、算子融合。剪枝是把不重要的连接或通道去掉量化是把FP32/FP16的权重换成INT8甚至更低精度蒸馏是让一个小模型去模仿大模型的行为算子融合是把多个计算合并成一个减少访存开销。它们对应的是不同的瓶颈场景显存不够用就做量化和剪枝延迟太高就做算子和图优化精度和效率要平衡就上蒸馏加量化。理解这个对应关系才不会在部署时乱投医。2. 训练优化器的选型思路与参数调优2.1 从SGD到Adam优化器的进化逻辑要搞清楚怎么选优化器得先理解它们是怎么一步步演化出来的。最早用的SGD很简单每次沿着梯度的反方向挪一小步但问题也很明显如果某个维度的梯度忽大忽小学习率设大了会震荡设小了收敛又太慢。于是出现了Momentum它模拟物理中的惯性把历史梯度的方向加入当前更新中相当于在反复震荡的方向上做了平滑能显著加速收敛。再往后人们发现不同的参数应该有不同的学习率。频繁更新的参数希望步长小一点不常更新的参数希望步长大一点。RMSProp和Adagrad就是干这件事的它们按参数历史梯度幅度的平方根来归一化学习率。到了Adam思路更进一步既保留Momentum的一阶动量也就是速度又加入RMSProp的二阶动量也就是自适应缩放两者结合后在大多数任务上都能稳定且快速地收敛。那为什么后来大家又批判Adam、回归AdamW因为Adam原版在实现权重衰减时是把L2正则项直接加到梯度里这和真正的解耦权重衰减decoupled weight decay在数学上并不等价尤其在Transformer结构里容易让泛化能力打折扣。AdamW就是把权重衰减直接从梯度计算里抽出来更新参数时单独做衰减效果更干净。关于优化器的工作原理我不想只停留在背书层面用一句话概括就是SGD相信当前梯度Momentum相信历史的惯性Adam相信历史的惯性加梯度的尺度。选型的本质就是在“收敛速度”和“泛化能力”之间做取舍。2.2 常用优化器横向对比谁适合什么场景为了方便对照我直接整理了我自己一直在用的选型表。优化器核心机制适合场景注意事项SGD Momentum梯度方向 惯性平滑CNN、传统分类任务泛化能力最强学习率要仔细调配合warmup和衰减RMSProp按梯度平方自适应缩放RNN、LSTM等序列模型对梯度噪声较敏感batch不宜太小Adam一阶动量 二阶动量NLP、Transformer、GAN等宽泛任务默认学习率1e-3或3e-4起步AdamWAdam 解耦权重衰减预训练、微调、大规模Transformerweight_decay通常取0.01到0.1LAMB逐层自适应学习率超大批量batch size过万训练需要配合gpu集群调参门槛高这里有个关键认知不存在哪个优化器“全面最优”。我实测下来的体会是Adam系列在前80%的训练里效率极高loss降得飞快但到了后期微调阶段容易在最优解附近震荡SGD系列虽然前期慢但配合好的学习率调度往往能逼近更平坦的极小值泛化表现更稳。所以有些团队的做法是“两段式”先用Adam快速跑到好的区域再切SGD精调这种方式在部分CV任务里效果很好但操作复杂新手我不推荐一上来就玩这套。2.3 学习率调度优化器之外的隐藏变量优化器只是定步长的一环真正控制步长大小的其实是学习率。很多人只盯着选Adam还是SGD忽略了学习率调度策略结果一样出问题。我把学习率调度的作用比作开车优化器决定方向盘的转向方式学习率决定踩油门的力度。起步的时候需要慢慢加油warmup高速巡航时要稳住油门常数或小幅度衰减快到目的地时必须提前减速衰减到接近0不然肯定冲过头。最常见的调度策略是warmup 余弦衰减。warmup阶段一般占训练总步数的1%到10%在这一段里学习率从很小的值线性升到目标值作用是避免训练初期梯度统计量的偏差特别是在大batch和Adam类优化器组合下warmup几乎是必须的。余弦衰减则让学习率在训练后期平滑下降到接近0有利于稳定收敛。另外一个我常用的经验值视觉任务SGD的学习率大约从0.01到0.1起步配合batch size按线性缩放batch翻倍学习率也粗略翻倍Transformer类任务AdamW的学习率从3e-4附近起步这是被大量实验验证过的安全区间。3. 实操打造一份可复用的优化器配置流程3.1 从基线到收敛一套可靠的实验流程很多教程直接甩出“用AdamWlr3e-4weight_decay0.01”然后就完事了。但我在自己项目里从来不敢这么做因为不同任务、不同数据规模、不同模型结构最适合的参数至少差一个量级。我现在走的是一套相对标准化的流程你可以直接拿去用。第一步先用小规模数据比如十分之一的训练集跑一次过拟合测试目标是把loss降到接近0。如果这一步都降不下去先别调优化器去查模型结构和数据预处理。第二步做一次学习率范围测试LR Finder从一个极小的学习率开始每几步增大一点画出loss随学习率变化的曲线曲线下降最陡的区域右下角对应的学习率就是一个不错的初始峰值。第三步按这个学习率跑正式训练同时监控梯度的范数如果梯度范数出现剧烈跳变说明学习率还是偏高需要降一个量级或者加梯度裁剪。第四步等loss曲线进入平台期后再回头微调weight_decay和batch size。这套流程本质上是把“调参”从一个玄学问题变成了有依据的工程搜索。顺便说一句PyTorch里实现LR Finder并不复杂用torch.optim.lr_scheduler里带step的LambdaLR就能做一个简易版本。不需要额外装工具库逻辑大约就是按指数间隔升高学习率记录每一步的loss最后画图观察。3.2 关键参数逐个拆解betas、weight_decay、epsAdamW里被默认值掩藏的几个参数每一个都值得你手动看一眼。betas是Adam类优化器的核心一阶动量系数beta1控制着“对历史速度的信任程度”默认0.9意味着当前梯度只占10%的权重二阶动量系数beta2控制着“对历史梯度平方的信任程度”默认0.999这个值越大对梯度变化的响应越慢。如果你发现训练后期loss来回震荡尝试把beta2降到0.98或者0.99会有奇效如果你用的是小batch梯度噪声偏大可以适当增大beta1到0.95让历史信息发挥更大平滑作用。weight_decay在很多框架里是数值不是开关它的含义是每次更新时把参数整体往0方向拉一拉比例就是weight_decay的系数。L2正则和weight_decay在SGD里是等价的但在Adam里不等价所以如果你用PyTorch的AdamW这里的weight_decay才是真正的解耦权重衰减。我常用的初始值是0.01到0.05如果是大模型微调0.1也不算夸张但超过0.1容易欠拟合需要警惕。eps是为了防止除以0而加在分母上的小常数Adam默认1e-8。绝大多数时候不用动它但如果你训练的是半精度FP16建议把eps调到1e-6或1e-7因为FP16能表示的最小正数比FP32大很多eps太小会导致更新数值不稳甚至NaN。3.3 权重衰减的正确姿势AdamW优于Adam在“Adam要不要配L2正则”这个问题上很多人栽过跟头。PyTorch里Adam优化器本身也接受weight_decay参数但它的实现方式是先把L2正则的梯度加进去再走一阶二阶动量更新这种方式在动量的影响下权重衰减会被“打折”而且越晚更新的参数折扣越大达不到预期的正则效果。AdamW的解法是把权重衰减放在动量更新之外动量负责更新方向更新完成后直接按系数缩小参数。这样一来权重衰减的力度对每个参数都是均匀的数学性质干净得多。也正因为如此现在几乎所有Transformer预训练和微调任务权威做法都是AdamW加解耦权重衰减。你要是还在用老式Adam加L2赶紧换过来这属于性价比极高的一处改动。4. 推理侧模型压缩让模型在部署环境里“轻装上阵”4.1 剪枝拿掉不重要的参数训练好的模型里存在大量冗余。剪枝的思路就是把这些不影响精度的冗余参数或者结构去掉。剪枝分两类非结构化剪枝和结构化剪枝。非结构化剪枝是把权重矩阵里绝对值小于阈值的元素置零精度损失很小但得到的稀疏矩阵在普通硬件上很难真正加速除非你的部署平台对稀疏计算有专门支持结构化剪枝是直接把整个通道、整个卷积核剪掉得到的模型是稠密的CPU和GPU都能直接受益但精度损失更大通常需要剪完后微调。实际项目里我更推荐从结构化剪枝入手尤其是通道剪枝。操作流程是这样的先训练好一个精度达标的模型统计每个通道对输出的贡献度常用的指标是BN层的缩放因子gamma|gamma|越小的通道越不重要剪掉最不重要的30%到50%通道再在训练集上微调几个epoch恢复精度。我实测下来的经验是剪掉30%通道对精度影响基本可控超过50%就要非常谨慎了且剪枝加微调组合起来远比直接OMPOne-shot Pruning效果好。4.2 量化高位宽换低延迟但别把精度一起换掉量化是另一个大头。训练好的模型权重默认是FP32一个权重占4字节转成FP16直接减半转成INT8则只有FP32的1/4。内存占用降下来了访存带宽瓶颈也随之缓解推理延迟在大部分场景下都能显著下降。量化的实现路径有两条训练后量化PTQ和量化感知训练QAT。PTQ是在模型训练完成后直接做校准拿一段有代表性的数据跑一遍统计每层激活值的范围然后据此把权重和激活映射到INT8。这个方法速度快成本低适合大多数场景但如果你模型里有对数值范围极度敏感的结构比如某些LayerNorm和GELU组合PTQ后掉点会很明显。这时候就得上QAT在训练阶段就模拟量化的舍入误差让模型自己去适应低精度代价是训练时间变长但精度恢复效果通常显著。关于量化我知道一个特别容易被忽略的点权重对称量化还是非对称量化。默认情况下TensorRT等框架会按层自动选择合适的量化方式但如果你的权重分布偏态明显非对称量化能多保留一些有效精度。判断方法很简单直接看权重直方图如果最大值和最小值的绝对值差异很大就需要关注这个问题。4.3 知识蒸馏让小模型学会大模型的“判断边界”蒸馏的思路不是从零训练一个小模型而是让一个已经训好的大模型Teacher去引导小模型Student的训练。大模型不仅告诉小模型“这张图是猫”还告诉它“这张图有90%概率是猫、6%概率是狗、4%概率是狐狸”这种软标签比硬标签包含的信息丰富得多相当于把大模型的泛化边界迁移了过去。蒸馏有一个很重要的参数叫温度T。温度越高softmax输出的概率分布越平滑软标签携带的“类间关系”信息越多温度越低输出越接近原始硬标签。我常用的配置是T4左右做训练阶段的蒸馏T1做最终推理。需要留意的是蒸馏并不是万能的当小模型容量和大模型差距太悬殊时迁移效果会大打折扣。有一个不算严谨但可用的经验参考小模型参数是大模型的1/10以内时蒸馏难度会明显上升。4.4 实战案例一次推理优化项目从7.2ms到2.8ms说一个我自己做过的部署优化项目方便你把前面几种手段串起来。当时是一个OCR检测模型基于轻量级语义分割网络输入尺寸640x640原始在GPU上的单次推理延迟是7.2ms批量大小1。业务要求压到3ms以下。我第一步做的是算子融合和精度校准把模型转到TensorRT的FP16模式延迟从7.2ms降到4.5ms。这一步几乎白捡因为FP16在支持Tensor Core的GPU上加速明显而且语义分割这类任务对FP16精度损失极不敏感。第二步做通道剪枝用BN因子统计方法剪掉25%的通道微调150个step之后精度从mAP 0.82掉到0.811基本可接受延迟从4.5ms进一步降到3.6ms。第三步是INT8量化加PTQ校准选200张覆盖多种场景的校准图延迟降到2.8ms精度又掉了一点到0.804但还在业务容忍范围内。这个案例里的关键经验是先做收益最大、风险最低的FP16和算子融合再上剪枝微调最后才考虑INT8量化。一步步验证精度损失不要在第一步就直接叠加所有优化手段否则出了精度问题你根本定位不到是哪个环节造成的。最后补充一点校准数据集一定要覆盖目标场景的真实分布最好从线上采样不然量化模型在真实数据上可能会掉点严重而这在设计阶段几乎是隐形的。5. 常见问题与排查技巧实录5.1 训练不收敛先别急着怪优化器训练loss原地踏步或不断上升时第一反应不该是换优化器或调学习率。按照排查优先级先看数据归一化和标签有没有噪声再看模型结构里是否有梯度传递断裂比如错误的detach、停用梯度之类的操作然后用小数据做过拟合测试排除模型实现错误。这些都没问题再回头看优化器配置。有个特别隐蔽的坑混合精度训练时梯度出现NaN。这种情况通常不是优化器本身的错而是FP16下梯度下溢。解决办法是开启dynamic loss scaling或者在优化器里把eps调大一点或者在参数更新前做梯度裁剪。我在项目里遇到过类似问题排查到最后一个简单的梯度裁剪就解决了建议你在任何大模型训练脚本里都默认开启grad_clip_norm1.0成本极低但能规避很多奇奇怪怪的训练崩溃。5.2 量化后掉点严重如何定位INT8量化掉点超过预期时不要第一时间骂量化工具先做三个检查。第一看每个层的激活值分布有没有某些层输出范围特别大导致量化步长过大、有效精度不足。第二检查有没有太多饱和截断如果某层激活值出现大量集中在边界的情况说明校准数据选得不好该层的最大激活值被异常样本带偏了。第三逐层做敏感度分析一次只让一层保持INT8、其他层回退FP16快速定位对量化最敏感的层对这些层单独用更高精度或者QAT处理。这个定位方法帮我解决过很多次问题。印象最深的一次一个YOLO系列的检测模型头部输出层的数值范围窄且敏感PTQ后检测框数量直接少了一半后来把那几层保留FP16精度就完全恢复了。类似这种对量化敏感的层工程上叫“敏感算子”处理它们没有捷径就靠calibration和逐层分析。5.3 一张排查速查表现象可能原因优先处理方式训练初期loss发散学习率过大/数据问题先做过拟合测试再调LRLoss震荡剧烈学习率偏高、beta2偏大调低LR或beta2至0.98混合精度训练出现NaNFP16梯度下溢梯度裁剪 / 动态loss scaling模型收敛慢学习率过小、无warmup用LR Finder找峰值LR量化后精度骤降校准集偏差、敏感层精度损失换校准集、逐层敏感度分析剪枝后精度不可恢复剪枝比例过高降低比例并做微调训练推理延迟没降但体积降了内存瓶颈非计算瓶颈考虑算子融合和并行推理6. 关于Model-Optimizer我有几点个人体会玩模型优化这么多年最大的体会是优化器不是黑盒它是一套可以被理解、被拆解的数学规则。只要你愿意花时间看一两个关键公式就再也不会因为“模型不收敛”而对着代码发呆。第二个体会是“优化”永远是一个衡量收益和风险的过程无论训练还是推理先求稳再求快、先保住精度再压延迟。第三个体会是累积一套属于自己的实验流程比任何现成的“最佳配置”都有用因为每个项目的数据分布、算力约束、精度要求都不一样标准答案根本不存在。如果你手里刚好在折腾一个新模型我的建议很简单从AdamW加默认参数跑通一条基线记录loss曲线和精度然后花半小时做个学习率范围测试再跑一版微调后的结果。这一趟下来你对Model-Optimizer的体感会从“听说过”变成“用得熟”。剩下的经验都是在无数个调参不眠夜和推理压测里攒出来的慢慢来不着急。
返回列表