资讯详情

资讯详情

YOLO训练十大坑:从loss震荡到mAP 92%的实战避雷指南

第一次用YOLO系列模型跑自己的数据集我最初的体验是这样的loss曲线慢吞吞往下走看着像在收敛可验证集mAP六个epoch都纹丝不动稳定停在8%左右。我以为是网络不够强换了更大的骨干网络结果显存直接爆掉。那段时间我翻遍了网上关于训不收敛的讨论得到的答案翻来覆去就两句——调小学习率、换更强模型——对新手来说等于什么都没说。前前后后折腾了快两个月反复试错总算把模型调到了mAP 92%。复盘时我数了一下整个过程中踩过的坑正好十个而且绝大多数都不是模型结构的问题而是配置、数据和评估方式里的细节。这篇就把它们一个个拆开讲清楚每个坑都给症状、定位方法和修复动作。标题说90%的新手会中招以我带过的人来看这数字还保守了。如果你正准备用YOLO训练自己的数据集建议先收藏踩到哪个坑直接跳到对应小节对照。1. 先把答卷亮出来mAP 92%背后的配置与十大坑速览1.1 我手里的任务和最终成绩单复盘之前先把我当时的任务环境和最终结果交代清楚后面讲坑的时候你才知道我踩在什么基线上。任务本身不算复杂一个工业场景的目标检测需要检测10个类别的目标训练集2万多张图验证集2000张。比较棘手的是目标尺度差异非常大既有占画面5%的大件也有只有30x30像素的小件这直接影响了后面坑7和坑8的走向。机器是单卡显存比较紧张这一点在坑8里也起了关键作用。模型用的是YOLO系列主流开源实现初始输入分辨率就按最常见的640。第一次训练跑完300个epoch成绩惨不忍睹指标初版结果修坑后结果mAP0.56.8%92.3%mAP0.5:0.95约2.1%68.7%训练最终loss3.2附近震荡明显0.8附近稳定收敛收敛状态勉强算降但验证集完全不涨稳定收敛验证集同步提升这组数字说明一个事实同样的模型结构同样的数据量结果天差地别差距全在十个看似很小的坑里。我全程没有换更大模型也没有增加任何额外数据纯粹是修数据和调参修出来的。1.2 十个坑的速览症状与解法一句话版先把十个坑做成一张速查表后面每章展开细讲。这张表我自己在调参过程中反复看遇到问题先对症状再翻对应章节。坑号坑名典型症状解决思路一句话坑1初始学习率乱设loss震荡、训不收敛用经验法则随batch缩放lr先用小步实验摸底坑2动量和学习率脱节收敛极慢、调参越调越乱一次只改一个变量动量保持常见默认值坑3损坏图片混入训练集loss突然变nan、训练中断训练前全量校验图像可解码性和尺寸坑4标注坐标格式算错loss下降但mAP极低、框全歪格式转换后必须抽样可视化还原坑5类别ID不连续训练无报错但类别错位扫描全部标签ID重映射成0到C-1坑6验证集划分随意val虚高或剧烈波动、上线崩盘按采集批次/时间段分层划分杜绝帧重叠坑7anchor沿用默认值小目标漏检、召回率卡住对GT宽高做k-means聚类用IoU距离坑8batch size与分辨率打架OOM或精度莫名下降由最小目标尺寸反推分辨率再用梯度累积坑9数据增强一刀切要么过拟合要么训不动分阶段引入强增强mosaic概率别拉满坑10评估阈值没固定mAP数值忽高忽低、不可复现统一conf0.001、NMS IoU0.6的口径排错顺序我建议这样走先看第2章的训不收敛排查因为模型不收敛的时候调anchor和增强都是白费力气收敛之后再按第3章检查数据、第4章做精度优化最后按第5章统一评估口径保证你看到的92%是真实的不是自己骗自己。这个顺序本身也是我踩完坑之后回头看才总结出来的最佳路径。2. 训不收敛的元凶排查从loss震荡到nan我按这个顺序定位2.1 坑1初始学习率靠感觉拍板loss曲线直接起飞这个坑我第一次训练就踩了。初始学习率我完全是照抄默认配置里的0.01但忽略了那套参数是按batch size16调的。我把batch size调到64想加快训练显存也装得下可学习率没同步放大。结果第一个epoch的loss就开始跳前几步直接冲到几十后面再怎么调也没能回到正常轨道说白了就是训不收敛。原理上其实很好理解。SGD类优化器每一步的更新幅度大致正比于学习率乘以梯度而梯度是累加了一个batch的样本得到的。batch变大一倍等效于梯度累计的样本数翻倍更新步长在数学上也会成倍放大。所以有个经典经验法则batch size翻倍学习率大致也要翻倍batch size减半学习率也要减半。这只是起步参考精确值还是要看loss曲线的实际形态。我的做法是先做小步实验摸底拿几百张图跑十几个step分别用1e-2、3e-3、1e-3、1e-4试一轮看哪个学习率下loss前几十步能稳定下降。最后选中的初始lr是2e-2配合前3个epoch的warmup慢慢爬上去loss立刻就变得平滑。warmup的作用是让优化器在前几步熟悉数据分布避免一开始就走偏但它不是用来掩盖lr错误的——如果你lr本身设得离谱warmup拖再久也救不回来。2.2 坑2动量和学习率脱节收敛慢得像蜗牛第二个坑其实和第一个是配套的。我把lr调小之后发现loss下降变得非常慢500步只降了0.1一度怀疑是网络结构坏了。后来才意识到是我动得太乱了那一次同时改了学习率、动量和weight decay三个东西变量互相抵消根本定位不到是谁导致的。动量momentum这个参数的作用是累积前几步的更新方向让梯度的方向更平滑减少震荡。YOLO系常用的配置是0.937配合余弦退火使用这套组合在大量数据集上都被验证过对新手来说是最稳的起点。我当时自作聪明把momentum从0.937改成0.5以为能更稳定结果反而让优化器失去了历史梯度方向的平滑作用训练过程变得拖沓又迟钝。这里最值得说的不是动量本身怎么调而是调参的基本纪律一次只动一个变量。我后来每次实验只改一个超参数并且记录改了什么、结果变了多少很快就定位到了问题。如果你同时改了三四个东西涨了不知道怎么涨的跌了也不知道是谁拖后腿后面所有优化都建立在糊涂账上。2.3 坑3loss变nan先查图像再查标签第三个坑是loss在第40轮左右突然变成nan训练直接中断。刚开始我以为是lr太大调小之后重跑结果第30轮又nan。这次我老老实实开始排查把训练数据逐张过了一遍图像解码和标签读取最后发现训练集里有几十张损坏的图片——文件扩展名是.jpg但解码出来是空的或者只有几行数据。这种图片在训练时产生异常特征梯度直接爆炸。排查脚本核心逻辑并不复杂from PIL import Image import os bad_images [] for f in all_images: try: img Image.open(f) img.verify() # 校验文件完整性 img Image.open(f) img.load() # 加载像素数据 except Exception as e: bad_images.append((f, str(e)))还有一个很容易被忽略的分支即使图像能正常解码部分增强操作在图片尺寸异常时也可能产生nan比如mosaic拼接遇到空图像。所以校验时顺便把每张图的尺寸分布打印出来把极端尺寸的样本一并清掉能省掉后续大量排查时间。损坏样本剔除以后nan问题再没出现过。2.4 训不收敛的完整排查链路把前面三步串成一套可复现的排查顺序基本能覆盖90%的训不收敛场景先看loss曲线的形态。如果是突然变nan直接跳到数据校验不要动学习率如果是震荡幅度大、整体不降查学习率和batch缩放关系如果是一直横盘或降得极慢查动量和退火策略。数据校验优先于超参调整。图像解码失败、标签坐标越界、类别ID越界这些错误会被模型学进去表面上看起来像超参问题实际换什么lr都没用。超参调整坚持一次只动一个并且每次修改都记录基线值。这一步最重要它能保证你所有结论都是可追溯的。这个顺序千万不要反。我见过太多人在loss nan的时候先去调lr调了一整天发现还是nan最后才发现是数据里有几张坏图白白浪费十几个小时的训练时间。3. 数据和标注的暗坑比模型结构更致命3.1 坑4标注坐标格式算错模型学得越快越离谱训不收敛解决之后模型终于能跑了但mAP一直上不去徘徊在30%左右。我开始怀疑是标注有问题于是做了一次标签可视化——把txt里的归一化坐标乘回图像宽高画框出来和原图对照。结果发现框全都偏了有些往上跑有些往右飞。最后检查转换脚本发现我把中心点x的归一化写成了x_center除以图像高度中心点y写成了y_center除以图像宽度宽高归一化也搞混了。这类错误在手工写格式转换代码的时候非常常见尤其是从其他标注格式往YOLO格式转的时候。为什么模型还能训练因为loss照常在下降模型确实学到了一些统计模式但坐标本身就是错的学得再多也学不对验证集上预测框的位置总是歪的mAP自然上不去而且越是拟合得好错误越被放大。从那次以后我定下一条规矩任何标注格式转换脚本转换完成后必须抽样可视化还原绝对不要信任一次性脚本。具体做法是取几十张图像把转换后的txt坐标乘回图像宽高画出框和原标注叠加对比肉眼确认位置、大小、类别都对得上再进训练流程。这个习惯帮我躲过了后面好几次数据迁移的大坑。3.2 坑5类别ID不连续训练没报错但mAP崩了另一个数据暗坑是类别ID不连续。我手里的数据是从老系统导出的类别列表里带着一个无效的背景占位导出时没有过滤导致有效类别ID从0、1、3、4这样跳着走中间缺了2。模型配置的类别数是10但部分标签里出现了越界ID训练过程中框架没有直接报错模型静默地把这些样本学成了错位类别预测结果里类别混淆严重。这种问题是最阴间的因为loss曲线完全正常mAP也有一点但按类别看单个AP就会发现某些类别特别低或者混乱。排查方式是在训练前扫描全部标签文件的类别IDfrom collections import Counter label_ids [] for f in all_labels: with open(f) as fp: for line in fp: label_ids.append(int(line.split()[0])) stats Counter(label_ids) print(sorted(stats.items())) print(min:, min(label_ids), max:, max(label_ids))只要发现min、max和配置文件里的类别数对不上或者set集合里有空缺就要立刻重映射成0到C-1的连续序列。我第一次跑这个脚本就看到ID跳号修正之后mAP直接涨了一截类别混淆问题也消失了。这个脚本后来被我固化成标准流程每接手一个新数据集都先跑一遍成本一分钟能躲掉几天的瞎调试。3.3 坑6验证集划分随意mAP高不代表你有实力验证集划分的问题最容易被忽视因为它不报错、不崩loss只会让你的评估结果失真。我的数据集是从几段监控视频抽帧得到的最初我按随机打乱划分训练集和验证集结果验证mAP虚高到85%换一批没见过的视频测试直接跌到40%。原因就是随机划分把同一段视频的连续帧同时放进了训练集和验证集模型记住了几乎重复的帧而不是学到了泛化能力。正确做法是按采集批次、时间段或场景做分层划分保证验证集里的样本和训练集完全没有重叠。比如抽帧数据按时间顺序切分前90%的视频帧做训练后10%做验证两者来自不同的时间段互不影响。这样得到的mAP才勉强算可信。另外还要注意验证集本身的体量。如果验证集只有几十张图mAP波动会非常剧烈改动一个随机种子结果就相差好几个点你根本判断不了调参是有效还是撞了大运。我的经验是验证集每个类别至少要有50到100个标注框太少就只能换分层划分策略或者增加验证集规模。4. anchor、batch size与数据增强把精度从70%磨到92%4.1 坑7anchor沿用默认值小目标被系统性漏检数据问题和收敛问题都解决之后mAP到了60%左右的平台期怎么调都上不去。此时我开始怀疑anchor配置。YOLO系列默认的anchor是在COCO数据集上聚类出来的COCO场景以自然物体、行人和车辆为主而我的工业场景目标长宽比有明显差异默认的好几组anchor和我数据里的GT完全匹配不上导致正样本匹配数量偏少小目标特别容易被忽略。做法其实不复杂对训练集所有GT框的宽高做k-means聚类聚类数按模型结构来常见的就是9个分3组给3个不同尺度的检测层。关键点是聚类距离一定要用IoU距离不要用欧氏距离。因为不同尺度的目标宽高差异大欧氏距离会把相似像素尺寸但长宽比不同的框混在一起得不到有区分度的候选尺寸。聚类完成后把9组anchor按面积从小到大排序填入配置文件。怎么验证anchor是不是合适观察训练日志里的正样本匹配数量和召回率。聚类后的配置如果明显优于默认配置能直观看到这两个指标在涨。这一步做完后我的mAP从60%跳到了78%小目标漏检也明显改善。4.2 坑8batch size与分辨率相互拖后腿batch size和输入分辨率是互相牵扯的。当时显存有限想跑640分辨率又想开64的batch直接OOM。为了保住batch我把分辨率降到416显存是够用了但小目标检测效果肉眼可见地下降。原因很简单原图上30x30像素的小目标缩到416分辨率后变成了约20x20特征信息流失严重降分辨率等效于把小目标往更小的方向推anchor匹配更难小目标几乎学不到判别特征。我的处理顺序是先从小目标标注框的尺寸分布反推最小可接受分辨率。经验值是最小目标在训练图像上不要小于10x10像素否则在特征图层面只剩一两个像素网络学不到有效信息。确定分辨率之后再看显存能装多大的batch。如果batch不够大用梯度累积gradient accumulation来凑等效batch size。比如实际batch8累积4次等效于batch32虽然BN层统计还是按实际的来但优化器更新步长更稳。还有一个隐藏点batch size太小会让BN层的统计量严重漂移。实测batch4和batch16相比精度能差好几个点而且越训越不稳定。所以不要把batch压到8以下除非你用的是没有BN的模型。4.3 坑9数据增强一刀切要么过拟合要么训不动数据增强这个坑我踩了两轮。第一次用最强增强配置——mosaic、mixup、旋转、色彩抖动全部打开小模型根本扛不住loss始终下不到低位。原因是增强强度太大模型刚学到一部分特征下一轮数据语义又变了学不动。第二次我把增强全部关掉训练loss很快降下去但验证集mAP到80%就再也上不去了典型的过拟合。正确姿势是分阶段、控制强度。训练初期用基础增强翻转、轻微缩放、色彩扰动让网络先稳定收敛等loss掉到低位、模型框架稳定了再逐
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →