这次的项目不算复杂,就是围绕YOLOv5m做一次完整的目标检测训练与验证,但从环境搭建到数据集清洗再到训练日志分析,整个过程走下来,还是有不少值得复盘的地方。我用的是工业零件表面缺陷检测这个场景,目标类别一共4类:划痕、凹坑、黑点、边缘破损。如果你也准备在自有数据集上训一个YOLOv5m,这篇应该能帮你绕开大部分我踩过的坑。
1. 项目整体设计与模型选型思考
1.1 为什么选YOLOv5m而不是s/l/x
YOLOv5系列按照网络宽度和深度分为n、s、m、l、x五个版本,底层结构都是一套CSPDarknet骨干加SPPF特征融合加PANet检测头,区别在于重复模块的数量和通道数。我在这个项目里的硬件条件是单张RTX 3060 12G显存,目标场景是工业质检,既要求推理速度能跟上产线节拍,又希望漏检率尽量低,所以直接锁定了m版本。
m版本的参数量大概在21M左右,计算量约51 GFLOPs,相比s版本(7M参数)精度上能高出三到五个百分点,而推理时间只增加大约20%。相比l和x版本,m的内存占用和训练耗时又友好得多,我用640分辨率训练,batch size开到16刚刚好,如果是l版本可能就得降到8,训练时间直接翻倍。在这里补充一个经验:如果你的目标是很小尺寸的物体,或者图片分辨率特别高,可以优先考虑l或者x,否则m是性价比最高的选择。
1.2 训练与验证如何形成闭环
训练集负责更新模型权重,验证集负责评估模型在未见数据上的泛化能力,这两者必须严格互斥。我在这个项目里遇到过一个很典型的反面案例,有同学把同一批图片既放进了训练集又放进了验证集,训练出来的指标漂亮得吓人,mAP@0.5达到0.98,结果一上线立刻被打回原形,原因就是数据泄露导致模型根本没有真正学到泛化特征。
正确的流程是:先把全部图片按照生产批次或者工件编号做分组,再按大约8:1:1的比例分成训练集、验证集和测试集。训练过程中,每个epoch结束之后模型会自动在验证集上推理一次,记录当轮的Precision、Recall和mAP指标,如果比历史最好成绩高,就把当前权重保存为best.pt,否则继续训练。这个闭环的价值在于,它给训练过程装了一个"仪表盘":如果训练loss持续下降但验证指标不再上升,那就说明模型开始过拟合了,可以提前止损。
2. 环境准备与工程目录搭建
2.1 硬件要求与运行环境
先说我这次的运行环境:Ubuntu 20.04,显卡RTX 3060 12G,CUDA 11.3,PyTorch 1.10.0,Python 3.9。整套配置不算新,但跑YOLOv5m完全够用。如果你的显卡只有8G显存,建议把输入分辨率降到416,或者batch size减半,硬扛640大概率会直接OOM。
训练过程中还有一个容易被忽略的瓶颈是数据读取速度。YOLOv5默认边训练边从磁盘读图,如果图片放在机械硬盘上,GPU会频繁处于饥饿状态,利用率上不去。我这次把所有图片缓存到内存里,命令加一个--cache ram,训练速度至少提升了30%。内存不够的话可以用--cache disk,也能有不错的效果。建议用SSD存放数据集,这一步能省下大量等待时间。
2.2 拉取YOLOv5源码与数据目录组织
环境搭建的步骤并不复杂,但版本兼容问题值得认真对待。我建议创建一个独立的conda环境,Python版本选3.8或者3.9,然后用官方仓库的requirements.txt安装依赖,不要自己手动装一堆最新版本的库,有些新版本算子跟旧版YOLOv5代码不兼容,跑起来会莫名其妙报错。
git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt数据目录结构一定要严格按照YOLOv5约定的方式来组织,否则训练脚本会卡在路径解析上:
datasets/defect/ ├─ images/ │ ├─ train/ │ └─ val/ ├─ labels/ │ ├─ train/ │ └─ val/ ├─ train.txt └─ val.txtimages和labels两个目录下的文件名必须一一对应,标签文件是.txt格式,每行描述一个目标框。如果你的数据是从其他标注工具导入的,文件格式可能会有差异,一次性转换成标准YOLO格式再开始训练,后面会省心很多。
3. 数据准备:标注检查与yaml配置
3.1 YOLO标签格式与质量检查
YOLO标签格式是每行五个字段:类别ID、目标框中心点x坐标、中心点y坐标、框宽度、框高度。所有坐标值都是相对于图片宽高的归一化数值,范围在0到1之间。一个目标占一行,如果一张图里有三个目标,那这个txt文件就有三行。
0 0.531250 0.478906 0.085938 0.079688 1 0.289063 0.617188 0.053125 0.065625 2 0.750000 0.320312 0.067188 0.045312这里有个高频坑:标注框坐标有时候会出现负数,或者宽度高度为0,训练时轻则报警告,重则loss直接变成nan。我习惯在训练前写一个脚本把所有标签扫一遍,把异常行揪出来。下面这段代码是我每次都会跑的清理检查:
import os for split in ['train', 'val']: label_dir = f'datasets/defect/labels/{split}' for f in os.listdir(label_dir): if not f.endswith('.txt'): continue with open(os.path.join(label_dir, f)) as fp: for line in fp: parts = line.strip().split() if len(parts) != 5: print(f'{f} 行数异常: {line}') try: cx, cy, w, h = map(float, parts[1:]) except ValueError: print(f'{f} 坐标非数字: {line}') continue if cx < 0 or cy < 0 or w <= 0 or h <= 0 or cx > 1 or cy > 1: print(f'{f} 坐标越界: {line}')3.2 按场景划分数据集:避免验证指标虚高
很多人在划分训练集和验证集时习惯直接随机切分,这在工业场景下是有隐患的。因为同一个工件的多张照片往往高度相似,随机划分会导致训练集和验证集之间出现"近亲"关系,验证指标虚高,模型一上线就露馅。
我这次的做法是:按生产批次分组,把A产线采集的图片全部放进训练集,把B产线采集的图片全部放进验证集,完全没有交叉。这样模拟的是模型上线后面对新产线数据的真实情况,验证结果更有参考价值。如果你没有产线批次信息,可以按拍摄时间分组,或者按工件编号分组,原则是保证同一实体的图片不要同时出现在两个集合里。
data.yaml配置文件需要指向训练和验证的图片列表文件,同时声明类别数量和类别名称:
train: datasets/defect/train.txt val: datasets/defect/val.txt nc: 4 names: ['scratch', 'dent', 'blackspot', 'edge_break']train.txt和val.txt这两个文件里存的是图片的绝对路径,每行一张,可以直接用命令生成:
find datasets/defect/images/train -name "*.jpg" > datasets/defect/train.txt find datasets/defect/images/val -name "*.jpg" > datasets/defect/val.txt3.3 数据增强策略与超参数选择
YOLOv5自带的增强策略非常丰富,核心配置在data/hyps/hyp.scratch-low.yaml文件里,包含Mosaic、MixUp、HSV颜色扰动、水平翻转等。其中Mosaic是把四张训练图片随机裁剪拼接成一张新图,对于小目标检测效果提升非常明显,因为它强迫模型在大尺度背景下学习小目标的特征。
不过Mosaic也不是没有副作用。训练后期模型已经收敛得差不多时,Mosaic引入的拼接伪影反而会成为干扰。YOLOv5官方版本在最后若干epoch会自动关闭Mosaic,但如果你用的旧版本没有这个逻辑,我建议手动改一下增强配置,把最后10个epoch的Mosaic概率调成0,验证指标通常会有小幅回升。
另外要提醒一点:不要一上来就改各种增强参数。默认参数是官方在COCO数据集上调试出来的,在你自己的小数据集上如果欠拟合再逐步增强,如果模型已经过拟合了反而要减弱增强。我在这个项目里就用默认参数跑的第一版,效果中规中矩,后来根据验证集的表现微调了HSV饱和度增益,最终mAP才稳定下来。
4. 训练实操:命令、日志与参数调优
4.1 一次完整的训练命令
训练指令本身并不难,难的是理解每个参数在干什么。这是我这次用的最终训练命令:
python train.py \ --img 640 \ --batch 16 \ --epochs 100 \ --data datasets/defect/data.yaml \ --weights yolov5m.pt \ --cache ram \ --name defect_m \ --hyp data/hyps/hyp.scratch-low.yaml逐项说明一下:
--img 640:输入分辨率。工业质检图片如果缺陷目标偏小,可以考虑1280,但显存和推理耗时都会成倍增长。我先用640跑通全流程,后续再根据实际效果决定要不要升级分辨率。--batch 16:batch size在这个显存容量下的临界值。batch太小会导致BN层统计不稳定,模型训练过程震荡;batch太大显存撑不住。如果你只有8G显存,建议开8。--weights yolov5m.pt:官方预训练权重。强烈建议从预训练权重开始训练,而不是从零开始,收敛速度快得多,最终精度也更高。--cache ram:把图片缓存进内存。这里强调一下,如果你的内存小于16G,建议改用--cache disk,否则有可能内存溢出。--name defect_m:实验名称,训练结果会输出到runs/train/defect_m/目录下。--hyp:超参数文件,用于指定数据增强和学习率参数。
4.2 训练日志怎么读:核心指标拆解
训练开始后,终端会滚动输出这样的信息:
Epoch gpu_mem box obj cls labels img_size 50/99 4.32G 0.02342 0.01783 0.00321 5 640 P R mAP@.5 mAP@.5:.95 val_box val_obj val_cls 0.8642 0.8217 0.9023 0.6731 0.02418 0.01973 0.00122第一行是训练集loss,box是框回归损失,obj是置信度损失,cls是分类损失。第二行是验证集指标。我最关注的是val_box和val_obj这两个值:如果它们持续下降、mAP同步上升,说明训练健康;如果训练loss已经很低但验证loss开始反弹,就说明过拟合了。
P和R是一对矛盾指标。P是查准率,预测为正的样本中真正的正样本比例;R是查全率,真实正样本中被正确找出来的比例。mAP@0.5是IoU阈值取0.5时的平均精度,mAP@0.5:0.95则是把IoU从0.5到0.95每间隔0.05计算一次再取平均,对框定位精度更敏感。工业场景里我两个都看,但更重视后者,因为定位不准在实际抓取或者后续处理时同样会出问题。
4.3 训练异常信号的识别与处理
训练过程中最常见的异常信号有这么几种:
- GPU显存OOM:优先把
--batch从16降到8,如果还不行再考虑把--img从640降到512。降分辨率的代价通常比降batch更小,大多数场景下512和640的精度差距在可以接受的范围内。 - loss直接变成nan:大概率是标签文件里有异常数据,比如坐标越界,也可能是学习率过大。先清洗标签,再把初始学习率调低一个量级,比如从0.01改到0.001。
- mAP一直为0:这种情况通常是类别编号错位,或者data.yaml中names的顺序与标注文件里的class id对不上。先确认类别索引,如果确认无误,关掉Mosaic再试一次,有时候Mosaic拼接后的图片里目标被割裂得太严重,小数据集上会导致模型学不好。
- 训练速度慢:先检查数据加载是不是瓶颈,加上
--cache ram,如果还慢再看GPU利用率,利用率低于50%基本就是CPU读图拖后腿了。
4.4 断点续训与早停策略
训练是一个长耗时操作,断电、内存溢出、手动中断都是常有的事。YOLOv5的断点续训非常简单:
python train.py --resume runs/train/defect_m/weights/last.pt--resume会自动读取上次训练的epoch数和优化器状态,接着往下跑。注意不要手动修改--epochs,否则会有逻辑冲突。
早停方面,YOLOv5通过patience参数控制,在hyp文件中配置。它的原理是:如果连续N个epoch验证指标都没有刷新最好成绩,就自动终止训练并保存best.pt。我这个场景里设了20个epoch的耐心值,原因是训练后期指标提升明显放缓,早停能避免浪费算力还防止过拟合。
5. 验证评估:val.py与结果文件分析
5.1 运行一次完整的验证
训练结束后,runs/train/defect_m/weights/目录下会生成两个权重文件,best.pt是验证集表现最好的,last.pt是最后一个epoch的。正式评估用best.pt:
python val.py \ --weights runs/train/defect_m/weights/best.pt \ --data datasets/defect/data.yaml \ --task val \ --img 640执行完成后,验证结果保存在runs/val/exp/目录下,包含混淆矩阵confusion_matrix.png、PR曲线PR_curve.png、F1曲线F1_curve.png、标签分布labels.jpg,以及一批带有预测框的可视化样例。
5.2 混淆矩阵与PR曲线的实际使用方式
混淆矩阵是我每次必看的文件。它能直观地告诉你模型在哪些类别之间容易混淆。我这次训练完发现"blackspot"和"scratch"之间有大约7%的互相误判,看样本才发现这两种缺陷在灰度图上确实很相似,后来通过补充两类特征的极端样本才把混淆比例压下来。
PR曲线用来挑选置信度阈值。如果你打开val.py生成的PR_curve.png,横轴是Recall,纵轴是Precision,曲线越靠近右上角说明模型越强。实际部署时,如果场景对漏检零容忍,就把置信度阈值调低到0.25甚至0.2,接受更多误检;如果误检成本高,就调高到0.5以上,牺牲一部分召回率换取干净的输出结果。
5.3 best.pt与last.pt的选择策略
绝大多数情况下直接选best.pt。我这次训练100个epoch,best出现在第87个epoch,最后十来个epoch验证指标一直在小幅波动没有再创新高,说明模型已经进入平台期,这时候继续训练纯粹是浪费算力。如果你发现best和last的mAP差距很大,比如差了5个点以上,那就要检查验证集是不是太小或者分布不合理,导致指标震荡剧烈,这种情况下的best反而没有参考价值。
6. 常见问题与避坑经验
6.1 高频报错速查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| FileNotFoundError: dataset path not found | data.yaml里路径写错 | 确认train.txt/val.txt路径绝对正确 |
| AssertionError: train: No labels in xxx | labels目录下没有对应标签 | 检查标签文件名和图片文件名是否一致 |
| CUDA out of memory | batch或img尺寸过大 | 降batch或分辨率,或加--cache |
| loss=nan | 标签异常或学习率过大 | 清洗标签,降低初始学习率 |
| 验证mAP一直为0 | 类别编号不匹配 | 检查names顺序,尝试关闭mosaic增强 |
| Unknown class index 5 | 标签类别ID超出nc范围 | 重新生成标签或修改nc数量 |
| 日志出现validation failed提示 | 数据集文件对齐出现问题 | 对比images和labels目录文件集合是否一一对应 |
6.2 我踩过的三个细节坑
第一坑是标注完之后直接开训。我第一次做的时候,用LabelImg标了两千张图,觉得差不多了就开始训练,结果mAP一直在0.2附近上不去。后来把标注框可视化到原图上才发现问题,有一批图片的类别ID整体错位,所有"划痕"都被标成了"凹坑"。从那以后我每次都会抽查至少100张可视化结果再开训。
第二坑是训练中断后手动改epoch续训。有一次训练到第60个epoch时机器重启了,我自作聪明地把--epochs改成40重新跑,结果优化器状态和调度器状态全乱了,后面的训练质量明显下降。正确做法就是直接用--resume,别的参数都不用动。
第三坑是验证集划分太随意。第一次按文件名随机切分,模型指标看起来不错,但换了新产线的数据之后效果差很多。后来改成按生产批次划分验证集,才真实反映出模型的泛化能力。这也是我前面反复强调按场景分组的原因。
6.3 关于validation阶段的一些心得
如果你在验证阶段碰到了"val failed"或者类似字样的报错信息,先别慌,大部分情况下不是模型本身的问题,而是数据和环境层面的问题。我整理了几个排查步骤:
- 先确认数据yaml文件里的train和val路径指向正确;
- 检查images和labels目录下的文件数量是否一致,用脚本比对文件名集合;
- 检查标签文件的类别ID是否都在0到nc-1之间;
- 确认
val.py和train.py用的是同一个data.yaml,不要出现训练用一份、验证用另一份的情况。
按照这个顺序排查,95%的验证阶段报错都能解决。
我个人在实际操作中的体会是,YOLOv5m的整个训练验证流程并不复杂,真正的难点在于数据质量管理和对指标变化的敏感度。数据干净了,参数合理了,模型效果自然会上去。最后再分享一个小技巧:训练完拿到best.pt之后,不要只盯着mAP看,多花点时间看预测可视化结果,把预测错误的图片单独拉一个文件夹,逐张过一遍,你会在里面找到比任何指标都更有价值的改进线索。