ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

深度学习准确率瓶颈:先做误差归因,再调数据、训练与推理

深度学习准确率瓶颈:先做误差归因,再调数据、训练与推理 准确率卡在某个数字上不去是深度学习从业者最常见也最折磨人的状态损失曲线看起来正常训练也没报错但验证集准确率就是停在 0.87 或者 0.92 附近反复横跳。很多人这个时候的第一反应是换更大的骨干网、加更多层、把 epoch 拉长结果几天算力烧掉指标反而掉了。我自己踩过最狠的一次是花了三周时间把一个分类任务的准确率从 91.2% 调到 91.6%最后发现测试集的划分里存在同源样本泄漏修好数据切分之后什么都没改准确率直接到 94%。这件事之后我形成了一个习惯在动模型之前先确定这个准确率可信吗、瓶颈在哪一层。深度学习提高模型准确率这件事本质上不是一个技巧清单问题而是一个归因问题。同一个准确率瓶颈可能来自标注噪声、增强过强、学习率调度不匹配、损失函数与类别分布不匹配、推理阈值不合理等等每一种对应的解法完全不同用错方向就是在浪费算力。这篇内容我按先诊断、再动数据、再动训练、最后动推理的顺序展开适合已经能跑通训练脚本、但在指标上遇到天花板的人也适合刚入门想建立正确调参直觉的读者。文中涉及的具体参数都是我在图像分类、视频动作分类、文本分类这几类任务上实测过的常见区间不是理论最优值你可以当作起点来试。1. 准确率不动时先做误差归因而不是换模型我在团队里立过一条规矩任何人说模型效果不好想换网络先交一份误差分析。原因很简单网络结构对最终准确率的影响往往排在数据质量、训练策略、推理配置之后。换网络是成本最高、收益最不确定的一步把它放在第一步做是典型的投入产出倒挂。1.1 建立可信基线必须先确认的三件事第一件是数据划分是否干净。分类任务里最容易出的问题是同源样本泄漏比如同一段视频抽出的相邻帧被随机分到了训练集和验证集同一用户在两个集合里都有样本同一张图的增强版本落到了验证集。这种情况下验证准确率会虚高而且虚高的程度随数据冗余度变化你在这个基础上调任何参数都是在拟合噪声。检查方式很直接对样本做一次近邻检索看验证集样本在训练集里的最近邻距离分布如果大量验证样本的最近邻距离接近 0划分就有问题。第二件是评估口径是否一致。我见过有人训练时用top-1准确率验证时用了带中心裁剪的 ten-crop报告出去的却是单 crop 的数字前后对比自然没有意义。还有标签映射表在训练和评估阶段不一致导致某几个类别的预测被系统性地归错整体准确率只掉两个点很难察觉。把评估脚本固定成一份任何人改都要走 review这是最便宜的稳定性投资。第三件是基线本身是否收敛充分。很多所谓新方法没效果的结论其实是基线只训了 30 个 epoch而新方法训了 100 个。我在做消融实验时强制要求基线必须训到验证指标连续 10 个 epoch 不再改善且学习率调度跑完整个周期。否则两组实验的学习率状态根本不可比。1.2 从损失曲线读出是过拟合还是欠拟合损失曲线是最便宜、信息量最大的诊断工具但很多人只看它降不降不看两条曲线的相对位置。现象训练损失验证损失更可能的原因优先尝试的方向欠拟合高且下降慢高与训练损失接近容量不足、学习率过小、正则过强增大模型、提高学习率、减弱正则典型过拟合很低先降后升数据量不足、增强不足加增强、加权重衰减、早停训练损失很低验证损失也低但准确率平很低低但停滞标签噪声、类别边界模糊清标、标签平滑、校准训练损失震荡剧烈波动同步波动学习率过大、batch 过小、梯度爆炸降学习率、加 warmup、梯度裁剪这里有个反直觉的点训练损失很低、验证损失也不高但准确率就是上不去这种情况十有八九是标签问题或者置信度校准问题而不是模型能力问题。因为交叉熵损失鼓励模型把正确类别的 logit 拉得很高只要标签有 5% 到 10% 的错标模型就会花大量容量去拟合这些错标损失能降下去但决策边界是歪的。1.3 数据泄漏与评估口径带来的假高准确率除了同源泄漏还有几种隐蔽的泄漏值得单独说。一是预处理阶段的全局统计量泄漏比如用整个数据集计算均值和方差来做标准化测试集的分布信息就通过这个统计量渗进了训练过程。正确做法是只用训练集统计然后应用到验证和测试集。二是特征工程泄漏比如对时序数据做了全局的滑动窗口统计未来信息混进了过去。三是目标泄漏某些特征本身就是标签的代理例如用是否退款来预测是否投诉。怀疑有泄漏时最快的验证方式是做一个打乱标签的实验把训练标签随机打乱后重新训练如果验证准确率依然明显高于随机水平说明数据里有捷径特征可以走如果准确率掉到随机水平附近说明之前的性能是真实的。这个实验成本不高但能一排一个准。2. 数据侧的动作往往比换网络更划算数据是这个领域里唯一投入一定能看到回报的方向前提是你动对了地方。我在视频动作分类任务上做过一组对比把 UCF101 类的数据做一轮标注复核加近重复剔除准确率涨了 2.8 个点同一份数据换更强的骨干网涨了 1.1 个点。数据侧的收益天花板高得多而且不额外增加推理成本这一点在工程落地时非常关键。2.1 清洗与标注一致性是被低估的准确率来源标注噪声分两种随机噪声和系统性噪声。随机噪声像抛硬币部分能被模型平均掉代价是模型需要更多数据和容量系统性噪声更危险比如标注员对某个类别的判定标准不一致或者某些边界样本被统一标错。这类错误会让模型学到一个错误的边界而且无论加多少数据都纠正不过来因为新数据会被同样的标准污染。实操上我一般这么做先抽 200 到 500 个样本让两个人独立标注算一下一致率。一致率低于 90% 的类别先别训练先把标注规范写清楚重新对齐口径。然后再抽一批模型预测与标注不一致的样本人工复核这部分样本的密度最高往往几十个样本就能暴露出标注规范里没覆盖的边界情况。我在一个细粒度分类任务上靠复核 300 个模型和人都犹豫的样本把该类别的准确率从 78% 提到了 86%,改动量非常小。另外提醒一点清理数据要有版本管理。删掉哪些样本、为什么删必须留记录。不然过两周你自己都不确定当前这份数据是怎么来的复现实验时无从下手。2.2 数据增强的边界哪些增强会破坏语义增强的原则是保持标签语义不变的前提下扩大输入分布。判断某个增强能不能用就问一句人看到增强后的样本还能不能给出正确标签如果不能这个增强就是在制造噪声标签。图像分类里随机裁剪加水平翻转基本是安全的颜色抖动幅度过大会让颜色本身就是类别特征的任务失效比如区分相近的食品类别RandAugment 这类自动增强策略强度要按数据集大小调小数据集上强度过大会明显掉点。视频任务要额外注意时序一致性光流相关的增强不能独立作用在单帧上否则时间信息就被破坏了。文本任务里同义词替换和回译比较安全但随机删除词在短文本上风险很高很容易把情感极性改掉。具体参数上的经验区间Mixup 的 alpha 取 0.2 到 0.4CutMix 的 alpha 取 1.0 左右两者通常二选一或者按概率混用混用时每个 batch 用其中一种的概率各 0.5。Mixup 在小数据集上收益明显在已经很大的数据集上收益会变小甚至因为训练周期变长而在同等 epoch 下看起来更差这一点对比实验时要注意控制总训练步数。# 一个常用的增强组合参数是我在中等规模图像分类上比较稳定的配置 train_transform Compose([ RandomResizedCrop(224, scale(0.35, 1.0), ratio(3/4, 4/3)), RandomHorizontalFlip(p0.5), ColorJitter(brightness0.3, contrast0.3, saturation0.3, hue0.05), RandAugment(num_ops2, magnitude7), RandomErasing(p0.25, scale(0.02, 0.2)), ToTensor(), Normalize(meantrain_mean, stdtrain_std), # 只用训练集统计量 ])提示验证集和测试集绝对不要接任何随机增强。如果验证阶段用了带随机性的 transform评估结果会带方差你会误以为模型在波动。2.3 类别不平衡和长尾分布的处理路径类别不平衡的严重程度不同处理思路差别很大。轻度不平衡最大类与最小类比例在 10:1 以内通常改损失函数就够了中重度不平衡需要采样策略加损失函数一起上。长尾分布的数据还要考虑头部类别主导梯度的问题。采样方面过采样少数类会让同一批样本重复出现容易过拟合欠采样多数类会丢信息。折中方案是用 WeightedRandomSampler 做温和的类别平衡把采样权重按类别频次的倒数开方来缩放而不是直接用倒数这样既缓解了不平衡又不会让少数类被反复复制到过拟合。counts np.bincount(train_labels) class_w 1.0 / np.sqrt(counts) # 开方是关键直接用倒数会过采样过度 sample_w class_w[train_labels] sampler WeightedRandomSampler(sample_w, num_sampleslen(train_labels), replacementTrue)损失函数方面Focal Loss 在极度不平衡正负比 1:100 以上时有效但在普通多分类任务上收益有限而且引入了两个额外超参要调。我的一般顺序是先试类别加权交叉熵不行再上 Focal再不行才考虑解耦训练先正常训特征再冻结特征重训分类头。2.4 伪标签与半监督值不值得投入伪标签的核心逻辑是用模型自己的高置信预测去扩充训练集代价是错误会被放大。它值得做的条件有三个无标注数据量远大于标注数据量、模型在标注数据上已经有一个不错的基础一般要求明显优于随机、以及类别分布大致可控。实操要点置信度阈值不要拍脑袋用验证集上模型的置信度分布来定通常选在准确率还能保持 95% 以上的那个阈值附近伪标签的损失权重要小于真实标签一般取 0.3 到 0.7每一轮迭代后重新评估指标连续两轮不涨就停。我见过最典型的失败案例是阈值定得太低模型把自己的系统性错误反复强化三轮之后准确率比单用标注数据还低。半监督方法如一致性正则那类效果更稳但实现复杂如果你没有明确的算力预算优先把伪标签这套做扎实。3. 模型结构与训练策略的取舍顺序数据和损失都理顺之后才轮到结构和训练策略。这一块的收益通常比数据侧小但胜在通用换个任务还能复用。我把它拆成容量、归一化与初始化、优化与调度、正则化四个层次按顺序做每一步单独评估不要一次改三个。3.1 容量匹配什么时候真的该换骨干网换骨干网的判断依据是欠拟合还是过拟合。如果训练损失都降不下去说明容量或者优化有问题值得加宽加深如果训练损失很低而验证损失高换更大的模型只会更严重。另一个容易忽略的点是输入分辨率。对图像任务来说分辨率对准确率的影响经常比网络深度更大把 224 提到 320 带来的收益可能比把 ResNet50 换成 ResNet101 更明显而且推理成本的增长是可控的。但分辨率提高后batch size 要相应减小或者用梯度累积同时学习率要重新调不然会看到训练不稳定的现象。结构上还有一个便宜收益微调时的归一化层处理。小数据集上冻结 BN 层通常比放开更好因为 batch 统计量在小 batch 下噪声很大数据集够大时放开 BN 往往能多拿零点几个点。这个结论没有绝对实测一下就知道。3.2 归一化、初始化与残差连接对收敛的影响归一化层的位置对训练稳定性影响很大。原始的 post-norm 结构在深层网络里容易出现梯度问题尤其是在没有 warmup 的情况下pre-norm 结构训练更稳但最终精度有时会略低一点需要靠更长的训练周期补回来。现在主流的做法是 pre-norm 加合适的初始化配合 warmup稳定性好很多。初始化方面PyTorch 的默认初始化在大多数场景下够用但如果你自己搭了带残差的结构需要注意残差分支最后一层的权重不能初始化为零方差过大否则残差累加后激活值会逐层放大。有个实用做法是把残差分支最后一层的权重初始化为零让网络从一个恒等映射开始训练收敛稳定性提升很明显尤其是在层数较多的时候。残差连接本身带来的收益不只是梯度还有优化难度。同样的深度加残差和不加残差收敛速度能差出几倍。如果你在复现某个结构时发现训练很慢先检查残差路径有没有被归一化层或者激活函数打断。3.3 优化器、学习率调度与 warmup 的具体配置我的默认起点是 AdamW学习率 3e-4微调时降到 1e-5 到 5e-5 量级权重衰减 0.05配合 cosine 退火和 5% 步数的线性 warmup。SGD 加 momentum 0.9 在图像分类上有时能拿到更好的最终精度但对学习率敏感得多需要更长周期如果算力有限我不建议首选。warmup 不是可选项。训练初期模型权重接近随机梯度方向不稳定直接用大学习率很容易让模型进入一个坏的区域后面再降学习率也拉不回来。warmup 的作用就是让模型先站稳再加速。# 线性 warmup cosine 退火写成一个按 step 更新的调度 def lr_lambda(step): if step warmup_steps: return step / max(1, warmup_steps) progress (step - warmup_steps) / max(1, total_steps - warmup_steps) return 0.5 * (1.0 math.cos(math.pi * progress)) * min_lr_ratio (1 - min_lr_ratio) * 0.5这里有个我实测多次的经验与其花时间精细调峰值学习率不如把 warmup 比例和退火终值调好。峰值学习率在合理区间内差个两三倍对最终指标影响没那么大但退火终值如果设得太高比如只退到峰值的 0.1 倍模型最后阶段一直在跳指标会明显差一截。我一般让最低学习率降到峰值的 1% 到 5%。3.4 正则化组合别一次全开权重衰减、Dropout、随机深度、Label Smoothing、EMA 这几样每一项单独用都有收益全开容易过度正则化导致欠拟合。我的顺序是先用权重衰减加合适的增强不够再加 Label Smoothing还不够再考虑随机深度Dropout 在卷积网络上收益有限但对全连接层依然有用。EMA指数移动平均是我认为性价比最高的一项几乎不需要调参不增加任何推理成本。做法是维护一份权重的滑动平均用它在验证集上评估。衰减系数按训练步数定训练步数在几万步量级时取 0.999 到 0.9998步数越多取值越接近 1。torch.no_grad() def update_ema(ema_model, model, decay): for pe, pm in zip(ema_model.parameters(), model.parameters()): pe.mul_(decay).add_(pm.detach(), alpha1 - decay)注意EMA 的评估结果和当前模型的评估结果要分开记录。只报 EMA 的数字会掩盖训练过程中的震荡出问题时不好定位。4. 损失函数与标签层面的精细调整损失函数是训练目标和业务指标之间的桥。交叉熵优化的是似然不是准确率这两者在模型已经比较准的时候会分叉。这一节讲的就是怎么把这座桥修得更贴合准确率。4.1 标签平滑与置信度校准标签平滑把硬标签 0/1 换成 0 附近的软分布比如正确类别给 0.9其余类别均分 0.1。它的作用是抑制模型过度自信减少对标注噪声的过拟合。在标签有噪声或者类别边界模糊的任务上标签平滑往往能带来零点几到一两个点的提升而在标签非常干净的任务上可能没什么效果甚至略降。平滑系数取 0.1 是常用起点不要超过 0.2否则模型学不到足够的区分度训练损失看起来降不下去会让人误以为优化出了问题。有些任务需要按类别数量调整类别数很多的时候同样的平滑系数对正确类别 logit 的压制会更明显。校准是另一件容易被忽略的事。模型输出的 softmax 概率经常偏高如果你下游要用这个概率做阈值判断或者排序就需要温度缩放之类的校准。做法是在验证集上拟合一个温度参数再乘到 logit 上实现简单但对下游决策质量提升明显。4.2 类别不平衡下的损失重加权策略重加权交叉熵的核心是给少数类更高的损失权重。权重公式上直接用频次倒数容易过激用频次倒数的平方根或者四分之一次方更温和。还有一种做法是有效样本数加权思路是随着样本数增加新增样本带来的边际信息递减因此权重不应严格按频次倒数。这里有个坑加了类别权重之后训练损失和验证损失的数值不再和未加权时可比如果你在同一个图里对比两条曲线的绝对值会得出错误结论。要么把权重同时作用在验证损失上要么干脆只看准确率和 F1别看损失绝对值。另外提醒重加权常常会让少数类的召回上升、精确率下降整体准确率可能不升反降。如果你的业务指标是整体准确率重加权未必划算如果关注宏平均 F1那它就很值。损失函数的选择一定要回到评估指标上不要凭更复杂所以更好来选。4.3 辅助损失、多任务与一致性正则辅助损失是把额外的监督信号加到网络的中间层。常见的做法是在网络的中间位置接一个分类头用同样的标签监督它目的是给浅层提供更直接的梯度。这在深层网络训练初期很有用能让浅层学到更有判别力的特征。辅助头的损失权重一般取 0.3 到 0.5主头训好后可以把它去掉推理时不影响结构。多任务学习是另一个思路如果手头有几个相关任务共享底层特征、各自接任务头通常比每个任务单独训一个模型效果更好因为共享结构起到了正则作用。但要注意任务之间的梯度冲突某个任务的梯度如果主导了共享层其它任务就会受损。实操上可以用梯度归一化或者不确定性加权来平衡各任务损失的量级。一致性正则的思路是让模型对同一输入的两种不同扰动给出一致的预测这在半监督和有噪声标注的场景里很有用。实现代价是要做两次前向训练时间大约增加一倍评估是否值得时要把这个成本算进去。5. 推理阶段的免费收益TTA、集成与阈值训练结束不代表优化结束。推理阶段有一批改动几乎不增加训练成本收益却很实在我把它们按性价比排了个序。5.1 TTA 的适用条件与容易踩的坑TTA测试时增强的思路是对同一张测试样本做多次变换各次预测取平均。对图像分类常用的组合是原图加水平翻转这种 TTA 通常能带来 0.3 到 1 个点的提升成本是推理时间翻倍。更激进的十裁剪 TTA 收益会再高一点但成本高很多而且和训练时的裁剪策略强相关训练用随机裁剪、推理用中心裁剪时效果最明显。坑主要在三个地方。一是变换引入了标签不一致比如水平翻转对区分左右有意义的任务如文字识别会破坏语义这种任务不能翻。二是 TTA 的多次预测平均方式用概率平均和用 logit 平均结果不同我一般先试 logit 平均因为它保留了置信度的相对差异。三是 TTA 和 BatchNorm 的交互如果推理时用不同的输入尺寸或者裁剪方式BN 的 running statistics 可能与输入分布不匹配这时要么重新校准 BN要么改用其它归一化方式。TTA 方案典型收益推理成本适用条件原图 水平翻转0.3 到 1.0 点2 倍任务对左右不敏感多尺度裁剪0.5 到 1.5 点3 到 5 倍训练用了随机尺度多尺度输入尺寸0.5 到 1.0 点2 到 3 倍目标尺度变化大5.2 模型集成的性价比排序集成按性价比大概是这个顺序同架构不同随机种子的权重平均最便宜只训练成本翻倍、EMA 权重与当前权重取平均几乎免费、不同架构的模型集成收益最高成本和实现复杂度也最高、以及不同数据划分训练的模型集成。权重平均有一个技巧直接对参数做算术平均只有在模型处于同一个损失盆地的平坦区域时才有效否则会得到一个性能很差的平均模型。这也是为什么常规做法是先做 EMA 或者用学习率循环让模型落到平坦区域再做平均。如果你只是想快速验证集成有没有用先拿两三个不同种子的 EMA 权重平均一下几分钟就能看到结果不行再考虑更复杂的方案。5.3 阈值与后处理的调优对二分类和多标签任务默认阈值 0.5 往往不是最优的。在类别不平衡时最优阈值会明显偏离 0.5。正确做法是在验证集上按目标指标准确率、F1 或者业务自定义的指标扫描阈值取最优值。这一步经常能带来比模型改进更大的提升而且完全免费。多标签任务还要考虑标签之间的相关性。如果一个样本的标签之间存在共现规律可以用标签共现矩阵做后处理或者用分类器链的方式逐步预测。序列任务则可以考虑用维特比解码替换逐帧的独立预测用转移概率约束标签序列这在动作分割、命名实体识别这类任务上提升非常明显。提示任何在验证集上调出来的阈值和后处理参数都必须在独立的测试集或者交叉验证的折外数据上确认直接在验证集上报告最终指标会高估性能。6. 把提升固化成可复现的实验流程前面讲的所有手段如果没有一套实验流程兜底最后就是一堆无法归因的改动。我见过太多项目在我们试了很多方法之后没人说得清到底是哪一项起了作用。这一节讲讲怎么把提升固化下来。6.1 消融实验的表格怎么设计消融实验的原则是一次只改一个变量而且要有基线。表格的最小列应该是实验编号、改动项、验证准确率、训练步数、是否使用 EMA。训练步数这一列非常重要很多看起来的提升其实来自训练更久。编号改动验证准确率训练步数备注B0基线89.460k无 EMAB1加 EMA90.160k免费收益B2加标签平滑 0.190.660k与 B1 叠加B3加 TTA91.260k推理成本翻倍B4换增强策略90.960k训练时间变长把事情做细还有一个附带好处当指标掉了的时候你能快速定位是哪一项引入的。我习惯在每次实验的配置里记录完整的随机种子、数据版本号、环境依赖版本出问题时按这三个维度排除。6.2 随机种子、多次运行与方差同一个配置换随机种子准确率波动一两个点是常态尤其是数据集规模不大的时候。这意味着单次实验的结果差异小于两个点时你无法可靠判断哪个配置更好。解决办法是用 3 个种子各跑一次看均值和标准差而不是看单次的最优值。如果算力有限至少做到固定随机种子、固定数据加载顺序、固定 cuDNN 的行为。PyTorch 里设置torch.manual_seed和数据加载器的 worker 种子只能解决一部分不确定性某些算子的并行规约顺序本身就带非确定性需要用确定性模式来关闭代价是速度会慢一些。训练一个可复现的基线然后用这个基线去做所有对比比追求每次绝对一致更实际。6.3 超参搜索的投入产出超参搜索不要一上来就网格搜。按影响力排序值得优先搜的是学习率、数据增强强度、权重衰减、标签平滑系数。其中学习率和增强强度的交互最强我一般固定其它参数先在这两个维度上做粗粒度搜索。搜索空间要设窄一点学习率在 1e-5 到 1e-3 之间按对数取五到六个点就够了再细的信息量很低。批量大小尽量由显存决定不要作为搜索维度因为它和学习率耦合一起搜会让空间变大很多。贝叶斯搜索相比随机搜索在低维空间里优势有限如果你的搜索维度超过五个先做降维比换算法更有效。最后一个我踩过的坑所有在验证集上反复挑选的超参数最终都会有一点点过拟合到验证集。如果你对最后的性能数字要求严格留一个完全不参与任何调参的测试集只在最后用一次。我现在的做法是把数据分成训练、验证、测试三份验证集用于日常迭代测试集一个季度才动一次。我个人在这些年的实践里最大的体会是准确率的提升往往不是靠某一个神技而是靠把每一个环节的损失都压到最低。数据侧的 1 个点、训练策略的 0.5 个点、推理阶段的 0.5 个点叠起来就是两三个点的差距而这两三个点常常决定了模型能不能上线。如果只能给一条建议我会说先把评估体系做扎实再动手改任何东西因为在错误的基准上做的所有优化最后都要还回去。
返回列表