资讯详情

深度学习训练实战:优化器、学习率调度与过拟合排查全攻略

📅 2026/10/19 16:08:42 | 华诺云谱 👁 阅读
深度学习训练实战:优化器、学习率调度与过拟合排查全攻略
前两篇笔记我写的是正经网络结构和数学推导反向传播怎么推、卷积怎么算、注意力机制的内积为什么除根号d。那会儿觉得自己什么都懂了直到开始在真实数据集上训练模型才发现真正折磨人的不是公式而是那些训练过程里“玄学”一样的细节。这篇笔记换个角度不聊网络设计专门记录我这几个月调模型攒下来的实战经验包括优化器选型、学习率调度、过拟合排查、训练不收敛的解决思路还有迁移学习里容易被忽略的坑。内容主要来自我自己跑实验的训练日志和踩坑记录适合已经掌握深度学习基础、正在动手训练自己模型的读者也适合那些理论看了不少但一上手就loss乱飘的人。1. 先从优化器说起不是所有任务都适合默认配置1.1 三个常用优化器的本质区别很多人拿到一个模型就默认“优化器用Adam就行”这没有错但不够聪明。我自己在这个问题上吃过亏所以先搞清楚三个主流优化器到底差在哪。第一是带动量的SGD。它的核心思想很简单在梯度下降的基础上引入一个速度项让更新方向不只依赖当前梯度还参考之前几步的运动方向。这么做能抑制震荡尤其在loss曲面比较狭长的场景里效果很明显。缺点是学习率要自己调得很小心而且对参数的每个维度用同一个学习率适应性差一些。第二是Adam。它在SGD动量思路上更进一步给每个参数单独维护一阶动量梯度均值和二阶动量梯度平方的均值相当于每个参数都有自己的自适应学习率。这个特性让它对初始学习率的敏感度低很多很多新手上来设1e-3也能跑起来。但Adam有个问题它在训练后期容易出现极端的学习率泛化能力理论上不如调好的带动量SGD。第三是AdamW本质上是把权重衰减从梯度更新中解耦出来。在传统Adam里权重衰减会被除以二阶动量的平方根导致大梯度参数衰减得多、小梯度参数衰减得少这其实是错的。AdamW直接把权重衰减单独作用在参数上不参与动量的归一化计算行为更可控。几乎所有Transformer类模型都用它不是没道理的。这里有个我在实际项目里体会很深的例子。有一回我训练一个图像分类模型主干网络用的是预训练的卷积网络刚开始图省事直接上了Adam初始学习率1e-3训练50个epoch验证集准确率卡在86%左右上不去。后来把优化器换成AdamW权重衰减设5e-2同样的epoch数准确率涨到了89%。原因就是AdamW的权重衰减作用得更干净没有和自适应学习率搅在一起。1.2 学习率调度才是重头戏优化器决定了参数往哪个方向走、走多快但怎么走完全程要看学习率调度。我目前用得最多的是两个阶段组合先把学习率从一个很小的值线性升到目标值warmup阶段再用余弦退火慢慢降下来。为什么要warmup因为训练初期模型参数是随机状态梯度方向噪声很大如果一上来就用大学习率很容易把参数推到某个不合适的区域后面很难收回来。尤其是在用Adam类优化器时初期二阶动量估计不够准确学习率偏大容易产生震荡。warmup相当于让模型先试探性地走几步等梯度方向稳定了再加速。余弦退火的理由比较直观训练后期需要更细腻的步长来落入好的局部最优解余弦曲线前半段缓慢下降、后半段快速下降正好符合训练由粗到细的节奏。我也试过Step Decay每30个epoch减半效果其实也不差但它需要手动设定衰减节点不同数据集的最佳节点还不一样不够省心。warmup加余弦退火的实现非常简单用PyTorch可以这样写import math from torch.optim import AdamW from torch.optim.lr_scheduler import LambdaLR def linear_warmup_cosine_decay(step, warmup_steps, total_steps): if step warmup_steps: return float(step 1) / float(warmup_steps) progress float(step - warmup_steps) / max(1, float(total_steps - warmup_steps)) return 0.5 * (1.0 math.cos(math.pi * progress)) total_steps 10000 warmup_steps 500 optimizer AdamW(model.parameters(), lr2e-4, weight_decay0.05) scheduler LambdaLR( optimizer, lr_lambdalambda step: linear_warmup_cosine_decay(step, warmup_steps, total_steps) )这段代码配合一个逐step调用的循环就行注意这里lambda接收的是step计数而不是epoch因为不同batch size下每个epoch的步数不一样按step调度更均匀。1.3 我常用的参数参考表下面是我在几种常见任务上试出来比较稳的初始配置可以作为起点再细调任务类型优化器初始学习率调度器权重衰减CNN图像分类SGD(动量0.9)0.1配合batch size 256余弦退火1e-4CNN图像分类AdamW1e-4warmup余弦0.05Transformer文本分类AdamW2e-5到5e-5warmup线性衰减0.01目标检测微调SGD(动量0.9)0.001Step Decay1e-4生成对抗网络Adam2e-4不衰减或固定0需要注意一个关联变量初始学习率不是孤立设定的它和batch size成正比关系。经验法则是batch size翻倍学习率大致也要翻倍因为梯度估计更稳定了可以承担更大的更新步长。我一开始不懂这个batch size从32调到128的时候没动学习率结果训练非常慢白白浪费了一个下午。2. 过拟合训练集涨、验证集跌最让人头疼的问题2.1 先判断是不是真的过拟合过拟合这个事判断错了比不判断还麻烦。我之前有过一次经历看到验证集准确率停下来就急着加各种正则手段结果把模型压得太狠训练集都不收敛了。后来认真看曲线才发现训练集loss还在下降验证集loss已经反弹这才是典型的过拟合信号。判断过拟合的几条标准训练集loss持续降低验证集loss先降后升中间出现拐点。训练集准确率接近甚至超过验证集准确率并且差距越来越大。模型权重的L2范数在训练后期明显增大说明部分参数被“撑”得很大。过拟合的根本原因是模型容量相对于数据量偏大模型把训练集里的噪声也当成规律学进去了。所以对策有两个方向一是减小模型有效容量二是增加数据的多样性和数量。正则化手段的本质都是在限制模型对训练数据的过度拟合程度。2.2 正则化手段怎么组合正则化不是堆得越多越好关键要看任务类型。Dropout是注意力模型里几乎必用的手段一般加在Feed-Forward层和注意力输出的残差连接之后dropout率取0.1或0.2就够。但在CNN主干网络里Dropout的效果远没有在Transformer里明显因为卷积本身带有空间共享的天然正则效果。我见过有人给ResNet50强行加了很多Dropout准确率不升反降就是这个道理。如果要在CNN里用建议只加在全连接层之前而且概率不要超过0.5。权重衰减是最省心的正则手段它的对偶理解就是给参数空间画了一个约束球防止参数变得过大。权重衰减系数的选择我建议做一次快速小规模搜索比如在1e-5、1e-4、1e-3、0.01、0.05这几个量级里对比。我踩过的一个坑是在AdamW里权重衰减设得太小比如1e-4几乎不起作用设得太大又会欠拟合所以这个参数一定要放在和任务类型相关的上下文里调。标签平滑是另一个容易被忽视的正则技巧。传统交叉熵在训练时要求模型对正确类别输出接近1的置信度模型容易变得过度自信。标签平滑把正确类别的目标概率从1降到比如0.9剩下的0.1分摊给其他类别相当于告诉模型“不要那么绝对”。这在分类任务里基本没有副作用我一般设0.1。早停是性价比最高的止损失控手段。验证集loss在连续N个epoch不下降时直接停掉保存历史最优的模型。这里有个细节早停不是看验证集loss本身而是看它有没有打破历史最低记录比如连续7个epoch没有新低就停止。我用的是保存最优权重加记录“最优epoch序号”的方式而不是简单淘汰模型文件。这几种手段可以组合但要注意主次手段适用场景常见系数/参数备注DropoutTransformer、MLP0.1-0.2CNN主干慎用权重衰减所有任务1e-4到0.05AdamW里不要设太小标签平滑分类任务0.05-0.1对噪声标签也有好处早停所有任务patience 5-10配合模型保存梯度裁剪训练不稳定时裁剪阈值1.0不是正则但防爆炸2.3 数据增强的正确姿势数据增强是隐形的正则化它通过改变训练样本的形态让模型学到更本质的特征而不是输入表面的统计模式。对图像任务我常用随机水平翻转、随机裁剪、颜色抖动亮度、对比度、饱和度的组合。颜色抖动对光照变化明显的场景很有效但也别把颜色变到失真否则模型学不到真实分布的纹理。增强强度是个需要把握的度。过度增强会导致训练集样本和真实数据分布差距过大模型在验证集上表现反而下降。我的经验是先从标准增强开始比如水平翻转加随机裁剪观察曲线形态如果过拟合明显再逐步加入颜色抖动、旋转、尺度变换每一步都记录验证集的变化。对Transformer类模型我现在用得比较多的是随机掩码。具体做法是对输入嵌入的序列在时间维度上随机mask掉一部分这能增强模型对缺失输入的鲁棒性。注意mask的比例不要超过15%否则训练信号太稀疏收敛很慢。3. 训练不收敛怎么办一套可复制的排查流程3.1 先看loss曲线形态模型训练不收敛的时候最忌讳的就是盲目改参数。我现在的做法是先看loss曲线长什么样不同形态对应完全不同的病因。loss曲线形态可能原因排查方向一直不降在初始值附近震荡学习率太低或数据没配对好检查数据、提高学习率loss直接跳NaN梯度爆炸、除零、数值溢出检查模型数值稳定性、用梯度裁剪快速下降后反弹并且震荡剧烈学习率过高降低学习率或加warmup训练loss正常验证loss乱窜overfitting或验证集太小检查数据划分、加正则降到某个平台就是不动陷入局部极值或学习率太小换优化器、重启训练、cosine退火我印象最深的一次排查是loss一直不降试了很多办法都没用。后来抱着怀疑的心态打印了一批训练样本发现输入x和标签y在数据加载时对齐错了x还是第一版的索引y已经换成了新版本。这种低级错误最耗时间因为它不报错损失只是不降而已。所以排查不收敛时第一件事永远是检查数据。3.2 从数据侧排查数据问题排在模型问题前面因为数据比模型更容易出错。我整理了自己常用的排查清单确认x和y的对应关系随机打印几个样本人工核对标签。检查数据是否每个epoch都进行了shuffle。如果不shuffle模型会学到样本顺序的假规律验证集表现会很不稳定。检查样本分布是否极端不均衡。某个类占比超过90%时模型会倾向全部预测成这个类看起来loss在降但实际没学到东西这时候需要class weight或重采样。检查归一化方式。图像一般用均值0.5、标准差0.5归一化到[-1,1]或者用数据集自身的均值方差。文本任务里padding mask有没有正确传给损失函数也容易被忽略。这里有个容易踩的坑在验证集上也使用了训练时的数据增强。增强会在验证时给同一个样本引入随机性导致每次验证结果都不一致你根本分不清模型是变好了还是变坏了。验证阶段必须关闭所有随机增强只保留确定性预处理。3.3 从模型侧排查数据检查完没问题就该看模型内部的数值情况。我最常用的手段是打印每一步的梯度范数。训练不收敛的模型梯度范数要么小到接近0要么大到几千。梯度接近0说明梯度在反向传播过程中消失了常见原因是网络太深、激活函数选择不当或者初始化不当导致输出饱和梯度很大说明反向传播时梯度越积越大层数多的时候尤其容易发生梯度爆炸。具体检查方法是在每个step的backward之后、optimizer.step之前把各层参数的梯度范数打印出来total_norm 0.0 for p in model.parameters(): if p.grad is not None: param_norm p.grad.data.norm(2) total_norm param_norm.item() ** 2 total_norm total_norm ** 0.5 print(fgrad norm: {total_norm:.4f})如果发现某一层的梯度和别层差了好几个数量级基本就是数值不稳定的根源。解决方案通常是梯度裁剪把整体梯度范数限制在1.0或5.0以内torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)初始化方式也值得检查。现代网络结构一般自带默认初始化但如果是自己搭的非常规结构要用Kaiming或Xavier初始化来匹配激活函数的尺度。我见过一个自己写的MLP用了全零初始化结果训练了200步loss纹丝不动换成Kaiming初始化后马上开始下降这种问题从数值上查是最快的。3.4 关于随机种子的执念排查不收敛的时候我还有一个习惯固定随机种子。不固定的种子会让每次运行结果都有细微不同排查问题的时候很难判断改动到底有没有效果。建议在训练脚本开头固定所有相关种子import random import numpy as np import torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)但我也要提醒一点固定种子只是排查和复现实验的手段不是解决不收敛的方案。如果模型在好几个不同的种子下训练结果波动巨大那说明训练本身不稳定光靠固定种子是在掩盖问题真正的病因可能是学习率太高或者数据太少。4. 迁移学习与微调小数据集救星但不是无脑加载权重4.1 什么时候适合微调手上数据量少的时候从头训练一个大模型基本是死路。预训练模型已经在海量数据上学到了通用的底层特征比如边缘、纹理、形状结构这些知识对小数据集很有价值。我的经验判断标准是如果训练集只有几千张图或几万条文本从头训练很难达到满意的效果微调几乎必然优于从头训练。当数据量达到百万级时从头训练才可能和微调打平。在数据量为中等规模比如几万到几十万微调依然是最省时间的方案。4.2 冻结与解冻策略微调最忌讳的是直接用一个大学习率去更新全部参数尤其是预训练模型的底层特征已经足够通用大更新会把它们破坏掉。我常用的策略分两步走第一步冻结backbone部分参数只训练新加的任务头。比如图像分类时冻结卷积网络的所有层只训练global pooling和最后的全连接层。这一步让任务头先适应预训练特征输出大约训练5到10个epoch。冻结的实现很简单for param in model.backbone.parameters(): param.requires_grad False optimizer AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr1e-3)第二步解冻backbone但用一个小得多的学习率比如1e-5到3e-5和任务头一起继续训练。这么做是为了让预训练特征在目标任务数据上做细微调整又不至于偏离太远。有些任务甚至全程都只训任务头就够了尤其是任务和预训练任务比较接近、数据量很小的时候。我还试过一种变体把参数分成两个组backbone一个组用低学习率任务头一个组用正常学习率用一个optimizer就能管理optimizer AdamW([ {params: model.backbone.parameters(), lr: 1e-5}, {params: model.head.parameters(), lr: 1e-3}, ], weight_decay0.05)这种分层学习率的方法比手动冻结解冻更方便适合一次跑完整个流程。4.3 经验坑BN层在微调时的行为微调时最容易被忽略的是BatchNorm层。BN层在训练时维护一组running mean和running variance这是从训练数据上积累出来的统计量。如果你的batch size比较小比如8或16BN统计量会很不稳定因为它只用当前batch的均值和方差来归一化小batch本身噪声就大。刚才说的“先冻结backbone只训分类头”对含BN的网络要格外小心。冻结参数只是不再更新scale和shift但BN的running stats在训练模式下还是在更新的这可能导致预训练模型统计量被小batch小数据带偏。我踩过这个坑微调一个带BN的卷积网络时冻结backbone解冻head验证集准确率反而下降就是因为BN统计量被污染了。解决办法有两个方向。一个是把BN层也冻结通过设置requires_grad之外再维护一个“只跑前向不更新统计量”的模式另一个是batch size尽量保持适中不要太小让BN统计量有足够的样本支撑。如果显存不允许大batch建议干脆用LayerNorm替代BN结构的模型或者使用GroupNorm这类不依赖batch大小的归一化方式。还有个更典型的坑是解冻全部层之后验证集准确率反而下降。这往往发生在目标数据和预训练数据分布差异比较大的场景。比如预训练模型是从通用图像数据上学的你的任务是某种特定的医学影像浅层特征虽然通用但高层特征差异很大。此时要谨慎解冻最后几层或者学习率设得更低比如1e-6量级同时加上早停在验证集最优的时候及时停止。5. 实验管理别让结果无法复现5.1 日志记录与模型保存训练深度学习模型实验管理做得越好后期效率越高。我最早训练模型时只在控制台打印日志模型只保存最后一个checkpoint结果想分析某个epoch为什么效果最好时根本没有数据可用。现在我的习惯是至少记录三样东西每个epoch的loss和指标、模型的历史最优权重、完整的配置参数。模型保存方面我建议保存历史最好的几个checkpoint而不是只保留最后一个。可以用类似这样一个简单逻辑每个epoch结束后在验证集上评估如果指标超过历史最优就把当前权重存成best_model.pth同时把epoch号写在文件名里。这样即使后面训练崩了也还有最优模型兜底。配置参数一定要保存成独立文件和模型权重放在同一个目录下。很多复现不了实验的问题都是因为过了一周自己都忘了当时用了什么学习率、什么batch size、什么增强方式。用PyTorch的话可以在启动训练时把所有超参数整理成字典存成JSONimport json config { optimizer: adamw, lr: 2e-4, weight_decay: 0.05, batch_size: 64, epochs: 30, warmup_steps: 500, loss: cross_entropy, seed: 42, model_name: resnet50, } with open(config.json, w) as f: json.dump(config, f, indent2)5.2 对比实验的变量控制做对比实验时最大的敌人是变量不干净。我见过别人满怀信心地跟我说某技巧提升了两个点但仔细一问他同时改了学习率调度、改了数据增强、还换了优化器这根本没法归因。正确的打开方式是一次只改一个变量其余全部保持一致。具体执行时我习惯给每轮实验编一个简短的名字比如baseline_v1、aug_v2、adamw_v3然后把每轮实验对应的config文件和日志文件都存好。横向比较时固定评价指标和评估数据集比如只用验证集上的top-1准确率做比较。不要一会儿看准确率一会儿看loss这两个指标关心的问题不一样。这里有个我自己的小习惯每次实验前先在表格里记下“这次要改的变量是什么、预期效果是什么、对照组是哪个”。虽然看起来有点繁琐但它能逼着你想清楚每个实验的目的避免白白浪费算力在无意义的试错上。实验编号改动变量固定不变的因素验证集指标baseline_v1无全部默认86.2%aug_v2增加颜色抖动默认其余全部87.1%adamw_v3换成AdamW默认增强调度89.0%lr_v4学习率降到1e-4同上88.4%这张表看起来简单但坚持记录下来你会发现几轮实验之后完成了一套很有价值的“经验地图”哪些改动对当前任务有效、哪些无效一目了然。结束语这篇笔记写到这里其实还没有把我想记录的都写完比如分布式训练、混合精度、模型量化这些话题都还没展开以后有机会再单独写。但从我自己这半年的实战感受来说深度学习训练中真正拉开差距的往往不是谁看了更多论文而是谁能在loss不降的时候冷静地一步步排查在过拟合出现的时候找到正确的组合拳在微调的时候尊重预训练模型的“体质”。如果你是正在自己动手跑模型的读者我建议把优化器、学习率调度、早期停止、种子固定这几件事先踏踏实实做扎实大部分训练问题都能在这些基础设置里找到答案。最后再分享一个小技巧每次训练前先在验证集上跑一个epoch看看模型能不能快速用少数样本过拟合这一步看起来多余但它能帮你快速确认代码链路没有低级错误省下来的时间远比训练一次多。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑