这个系列写到第四篇,今天聊一个大家问得最多的实操话题:YOLOV5怎么训练自己的VOC数据集。很多人手里已经有一批VOC格式的标注数据(JPEGImages图片加上对应的XML标注文件),想用YOLOV5跑目标检测,又不太清楚怎么把数据和框架对接起来。这篇文章就把数据准备、格式转换、配置修改、训练命令和常见坑全部梳理一遍。我会尽量按我实际操作的流程来讲,不只是贴命令,还会说清楚每个步骤为什么这样做,方便你跑通之后自己能改、能扩展。
先说结论:YOLOV5本身并不直接吃XML标签,它内部用的是YOLO格式的txt标签文件(每一行是“类别id 中心点x 中心点y 宽度 高度”,坐标全部归一化到0~1之间)。所以用VOC数据集训练YOLOV5,核心工作只有三件事:把XML解析成txt、把图片和标签按目录规则放好、改配置文件。整个过程不涉及复杂的模型代码,只要把数据链路理顺,训练就是一个train.py命令的事。
1. 在动手之前:先搞清楚数据格式之间的差异
1.1 VOC格式和YOLO格式到底差在哪
VOC格式(我以VOC2007/2012那套结构为例)的标注信息存放在XML文件里,一个XML对应一张图片。XML里面记录了很多信息,比如图片文件名、图片宽高、通道数,以及每个目标的类别名称和bounding box的绝对像素坐标(xmin、ymin、xmax、ymax)。
YOLO格式则完全不同。不存文件名,不存图片尺寸,也不存类别名称,只存数字。每行5个数,依次是:类别index、归一化后的中心点x坐标、归一化后的中心点y坐标、归一化后的宽度、归一化后的高度。归一化指的是除以图片的宽度和高度,所以这些值全部在0到1之间。
这里有个新手常犯的误解:归一化的“中心点x”和“中心点y”是以图片左上角为原点的相对坐标,不是以框的中心为原点。宽度和高度也都是除以整张图尺寸后的比例。写转换代码的时候,如果把这个关系搞反,训练出来的模型会非常怪异——比如检测框全跑到图片外面,或者在错误的位置出现一团乱框。
1.2 训练路径选哪种:先转换还是框架自身转换
YOLOV5仓库里确实内置了VOC数据集的处理逻辑,你在data.yaml里指向VOC的train.txt和val.txt,它也能跑起来。但我个人的建议是:自己动手写一个脚本把VOC转成YOLO格式的txt标签,理由有三个。
第一,转换逻辑越明确越可控。你跟着仓库的内部逻辑走,一旦报错,排查路径会绕很远;自己写转换脚本,出问题一眼就能定位是数据问题还是框架问题。第二,转好的txt标签是纯文本,任何工具都能查看,方便你直接打开检查每张图的标注是否合理。第三,实际业务里很少有人直接拿到现成的标准VOC目录,往往是混合格式的数据(一部分XML,一部分JSON,甚至一部分纯手工整理的表格),最后都会落到一套统一的标签格式上。
所以这篇文章会采用“自己写转换脚本”的方案,数据链路完全透明,训练阶段纯粹用YOLOV5的train.py,不用它内置的VOC自动转换逻辑。
2. 数据准备:从零搭建VOC结构到YOLO标签
2.1 先把目录结构调整成标准姿态
不管你是用LabelImg标注的,还是从网上找的现成VOC数据集,第一步都是把目录组织成下面这种结构:
VOCdevkit/ └── VOC2007/ ├── Annotations/ # 存放所有XML文件 ├── JPEGImages/ # 存放所有图片文件 └── ImageSets/ └── Main/ # train.txt/val.txt/test.txt这个结构非常经典,很多开源数据集都沿用这套布局。其中ImageSets/Main里的txt文件记录的是图片文件名(不带扩展名),每一行一个,比如000001、000002。训练集、验证集、测试集的划分就在这里指定。
如果是从零开始用LabelImg标注,建议直接标注成VOC格式,因为它默认输出的就是XML,目录也顺手按这个结构建好。如果是自己整理的数据,务必备份原始文件,不要在源数据上做修改,转换脚本单独跑,这样出问题还能从头再来。
2.2 XML解析:核心字段逐个拆解
打开一个VOC的XML文件,结构大概是这样的:
<annotation> <folder>VOC2007</folder> <filename>000001.jpg</filename> <source> <database>Unknown</database> </source> <size> <width>1280</width> <height>720</height> <depth>3</depth> </size> <object> <name>person</name> <bndbox> <xmin>100</xmin> <ymin>120</ymin> <xmax>300</xmax> <ymax>400</ymax> </bndbox> </object> </annotation>我们要提取的就是:filename、size(宽高)、object/name和object/bndbox。其中bndbox的四组坐标决定了检测框位置,name决定了类别归属。
有一点要提醒:有些数据集里面还会出现difficult、occluded、truncated这些标签。difficult=1表示这个目标非常模糊,正常训练时建议直接跳过,不然会引入大量难例噪声,模型很难收敛。truncated和occluded可以保留,实际业务里目标被遮挡是很常见的情况,正好用来训练模型的鲁棒性。
2.3 XML转YOLO txt的实用脚本
下面这个脚本是我常用的转换逻辑,不依赖任何框架,只用Python标准库的xml.etree.ElementTree,配合os和glob就能跑。注意这里我用到了cv2只是为了读图片尺寸,如果XML里面size字段齐全,也可以不用OpenCV。
import os import glob import xml.etree.ElementTree as ET VOC_ROOT = "VOCdevkit/VOC2007" OUTPUT_LABEL_ROOT = "VOCdevkit/VOC2007/labels" ALL_CLASSES = ["person", "car", "dog", "cat"] # 按你的类别填 def convert_xml_to_yolo(xml_path, output_dir): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) filename = root.find("filename").text stem = os.path.splitext(filename)[0] lines = [] for obj in root.iter("object"): difficult = obj.find("difficult") if difficult is not None and int(difficult.text) == 1: continue name = obj.find("name").text if name not in ALL_CLASSES: continue class_id = ALL_CLASSES.index(name) box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) # 坐标裁剪,防止越界 xmin = max(0, min(xmin, img_w)) xmax = max(0, min(xmax, img_w)) ymin = max(0, min(ymin, img_h)) ymax = max(0, min(ymax, img_h)) # 如果裁剪后框已经没有面积,直接跳过 if xmax <= xmin or ymax <= ymin: continue x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h box_w = (xmax - xmin) / img_w box_h = (ymax - ymin) / img_h lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") os.makedirs(output_dir, exist_ok=True) with open(os.path.join(output_dir, f"{stem}.txt"), "w") as f: f.write("\n".join(lines)) xml_files = glob.glob(os.path.join(VOC_ROOT, "Annotations", "*.xml")) for xml_file in xml_files: convert_xml_to_yolo(xml_file, OUTPUT_LABEL_ROOT) print(f"转换完成,共处理 {len(xml_files)} 个XML文件")这里有几个细节值得展开说。坐标裁剪很重要,因为有些标注工具生成的坐标偶尔会超过图片边界,比如xmax比图片宽度还大几像素。如果不做裁剪,归一化后会出现大于1的坐标,YOLOV5在训练时虽然不报错,但会导致loss计算异常,最典型的表现是训练过程乱跳、检测结果全是歪框。
分类别列表ALL_CLASSES必须和后面data.yaml里面的names顺序完全一致。很多人在这一步踩坑:转换脚本里写一套类别,data.yaml里写另一套顺序,结果模型训练倒是正常,但推理时类别名称对不上,检测出来的东西张冠李戴。
2.4 划分训练集与验证集
VOC结构里ImageSets/Main下的train.txt和val.txt只存放文件名(不带扩展名)。如果原来是老式VOC数据集,里面可能已经有现成的文件可以直接用。如果是从零标注的数据,你需要自己划分。
一个简单稳妥的划分方式是随机抽样:把所有图片文件名打乱,比如按8:2划分训练集和验证集。下面是按这个思路写的脚本片段:
import random import os xml_paths = os.listdir("VOCdevkit/VOC2007/Annotations") xml_stems = [p[:-4] for p in xml_paths if p.endswith(".xml")] random.seed(2024) random.shuffle(xml_stems) split_idx = int(len(xml_stems) * 0.8) train_stems = xml_stems[:split_idx] val_stems = xml_stems[split_idx:] with open("VOCdevkit/VOC2007/ImageSets/Main/train.txt", "w") as f: f.write("\n".join(train_stems)) with open("VOCdevkit/VOC2007/ImageSets/Main/val.txt", "w") as f: f.write("\n".join(val_stems))这里建议固定随机种子,确保每次划分结果一致。别小看这一步,如果你跑了好几轮训练,中间又调整过数据,结果每次划分都不一样,就很难对比实验效果是来自模型改动还是来自数据变化。
如果你准备把转换后的yolo格式标签放到统一的labels目录下,那目录结构就变成下面这样,YOLOV5官方推荐的形式:
VOCdevkit/ └── VOC2007/ ├── images/ │ ├── train/ # 所有训练图片 │ └── val/ # 所有验证图片 ├── labels/ │ ├── train/ # 对应训练图片的YOLO格式txt │ └── val/ # 对应验证图片的txt └── ...图片和标签靠文件名字对应起来,比如images/train/000001.jpg对应labels/train/000001.txt。这个对应关系非常重要,YOLOV5在训练时不会检查图片里有没有内容,它只按名字去读标签,如果图片存在但标签不存在,就默认当作没有目标的背景图参与训练。
3. 工程配置:data.yaml与模型配置文件
3.1 data.yaml该怎么写
在YOLOV5里面,数据集的配置是一个yaml文件,一般放在yolov5/data/目录下,或者你自己新建一个目录。内容大致是这样的:
train: VOCdevkit/VOC2007/images/train val: VOCdevkit/VOC2007/images/val nc: 4 names: ['person', 'car', 'dog', 'cat']train和val指向图片所在目录即可,YOLOV5会自动到相邻的labels目录去找同名txt文件,labels目录的位置不需要在yaml里配置,它是根据图片路径推出来的:自动把路径里的images换成labels。这个机制确实非常方便,但也容易埋坑——如果你把图片和标签分别放在完全无关的目录里,YOLOV5是找不到标签的,训练时所有图片都会被当成背景图。
如果你的数据是保留VOC原始目录结构,直接在ImageSets/Main/train.txt里写文件名,YOLOV5也有对应的数据处理逻辑。但我个人建议还是转成images和labels分开的布局,因为后续做推理、做数据增广、写自定义数据集加载器都更通用。
3.2 模型配置文件与nc修改
YOLOV5的模型结构配置在models/目录下,最常用的是yolov5s.yaml、yolov5m.yaml、yolov5l.yaml和yolov5x.yaml。打开它们,文件里有一个nc字段,表示类别数。这个值必须改成你自己的类别数量。例如你只检测人和车两个类别,就把nc: 80改成nc: 2。
实际上,train.py在启动时会检查模型配置文件的nc和data.yaml里的nc是否一致。如果不同,有的版本会报错,有的版本会直接覆盖。还是建议显式地同时改掉,保持一致,避免后患。
模型配置除了nc,其他部分一般不用动。很多新手看到anchors、depth_multiple、width_multiple这些字段会困惑,其实它们决定了模型的深度和宽度比例,是YOLOV5的“s/m/l/x”系列的核心差异。如果你没有足够的实验依据,不要轻易修改这两个multiplier参数。默认yolov5s的配置对大多数中小数据集已经足够,训练速度快,显存占用小,效果也不差。
3.3 预训练权重的选择逻辑
训练YOLOV5一般都会加预训练权重,常见的就是--weights yolov5s.pt。这个权重是模型在大型检测数据集上预训练出来的,网络的前面几层已经学会了通用特征(边缘、纹理、颜色块等),迁移到自己的数据集上能明显加快收敛,而且在小数据集上尤其有效。
很多人会担心:“我的数据集类别不是COCO那80类,能用COCO预训练权重吗?”答案是能。YOLOV5的加载逻辑会自动根据类别数调整最后检测头那一层的维度,前面主干网络的参数原样初始化。第一次加载时你会看到一条提示,大意是检测头层因通道数不匹配被跳过,这是正常现象,不需要担心。
如果你的数据集非常特殊,比如医学图像、卫星图像,和自然图像差异很大,那么预训练权重的帮助会小一些,但依然可以用。我的习惯是:只要数据量少于几万张,就先试试预训练权重;数据很少时,冻结主干前几层训练,能有效避免过拟合。
4. 训练全流程:命令、参数与监控
4.1 train.py命令逐项拆解
数据准备和配置完成后,训练命令其实不长,下面是我常用的一个模板:
python train.py \ --data mydata.yaml \ --weights yolov5s.pt \ --epochs 200 \ --batch-size 16 \ --imgsz 640 \ --device 0 \ --workers 4 \ --project runs/train \ --name my_voc_experiment逐项解释一下。--data指向数据集配置yaml;--weights是预训练权重文件,也可以填官方的预训练权重文件名(首次会自动下载);--epochs是训练轮数;--batch-size是每个GPU的批大小,这个参数受显存约束;--imgsz是输入图片的边长,YOLOV5会统一把图片缩放或填充到这个尺寸,一般用640,如果目标比较小可以适当调大到960或1280,但显存占用和训练时间都会上涨;--device指定GPU编号,多GPU可以写0 1 2 3;--workers是数据加载线程数,Windows下建议设为0或2,Linux下可以高一些,线程太高会导致CPU成为瓶颈。
关于--epochs,很多新手只跑几十轮就收工,结果模型还没完全收敛,这点我会在后面详细讲。
4.2 训练过程中的结果文件怎么读
训练启动后,屏幕上会打印进度条、loss、精度等指标,同时会在runs/train/目录下生成一系列文件。训练结束后最重要的一张图是results.png,它把所有关键指标曲线都画在一张图上,从上到下依次是box_loss、obj_loss、cls_loss、precision、recall、mAP@0.5、mAP@0.5:0.95。
我判断训练状态有一个固定的习惯:先看loss有没有持续下降,然后看precision和recall有没有在后期趋于稳定,最后看mAP@0.5有没有达到业务预期。这张results.png上如果mAP@0.5的曲线在训练后期还在缓慢上升,说明模型还没完全收敛,可以考虑加大epochs继续训练或者用最后一次权重复训。
训练过程中,我还会定期打开runs/train/下的权重文件目录,可以看到last.pt和best.pt两个文件。last.pt是最后一轮结束时的权重,best.pt是验证集mAP最高时的权重。实际推理和部署时一般用best.pt。偶尔也会遇到best和last差距很大的情况,基本原因都是验证集划分不合理或者最后一轮学习率还在下降,导致last过拟合了。
4.3 训练完怎么验证效果
训练结束后,先跑一下验证集,用val.py生成客观的精度指标:
python val.py \ --data mydata.yaml \ --weights runs/train/my_voc_experiment/weights/best.pt \ --imgsz 640 \ --device 0输出会显示每一类的precision、recall和mAP。这时候我习惯做两件事:一是看每个类别的AP值是否均衡,如果某一类特别低,通常说明该类样本太少、标注歧义大,或者目标太小难检测;二是打开runs/val/exp*/下面的混淆矩阵图,看哪个类别之间互相误检严重。
然后跑几个真实场景的推理测试:
python detect.py \ --weights runs/train/my_voc_experiment/weights/best.pt \ --source test_images/ \ --imgsz 640 \ --conf-thres 0.25 \ --iou-thres 0.45 \ --save-txt \--source可以是单张图片、图片目录、视频文件,甚至摄像头流。--conf-thres是置信度阈值,低于这个阈值的框会被过滤掉;--iou-thres是NMS的IOU阈值,控制两个重叠框是否合并。这两个值对结果影响非常大,推理现场如果觉得误检多就调高conf-thres,觉得漏检多就调低conf-thres,这是个经验值,没有绝对正确的设置。
5. 常见问题速查表与避坑技巧
5.1 典型报错与解决方案
下面整理了一份我在训练YOLOV5时见过很多次的报错速查表,挑出来最常见的几种:
| 现象 | 主要原因 | 解决办法 |
|---|---|---|
| FileNotFoundError: image not found | 图片路径与yaml中不一致 | 检查train/val目录路径是否正确,检查图片格式是jpg还是png |
| DataLoader workers重复 | --workers设置过高 | Windows下改成--workers 2,或直接设为0 |
| CUDA out of memory | batch-size或imgsz太大 | 减小batch-size,或调小imgsz,优先减batch-size |
| 标签文件全为空白 | XML解析失败或difficult被过滤 | 单独打印转换结果,检查txt文件是否为空,用标签可视化脚本检查 |
| mAP始终为0 | 标签类别序号与names顺序不一致 | 检查data.yaml里的names顺序与转换脚本里的ALL_CLASSES是否一致 |
| loss为nan | 学习率过高或数据异常(如全是黑图) | 降低学习率,检查图片是否全为纯色,查看标签中是否出现inf坐标 |
| 验证集全部无检测框 | 模型权重没收敛或conf-thres调太高 | 检查results.png的mAP曲线,把conf-thres降到0.1看看检测结果 |
5.2 数据标签层面的几类隐蔽问题
训练跑挂的情况并不多,更多时候是模型效果差,这种“软问题”最难排查。我建议你训练前就把标签可视化一遍,直接画出原始标签框覆盖在图片上的效果。这个步骤非常快,但能发现很多低级错误:框明显偏移、框超出整图边界、某类目标完全没有被框住、类别名拼写错误导致类别数不符合预期等。
有一个很经典的坑:XML里写的是car,转换脚本里类别列表是['person', 'bike', 'truck'],car不在列表里就会被静默跳过。你看到的结果是很多图片的txt标签是空的,模型相当于只拿少部分图片训练,检测效果自然差。所以转换脚本里最好打印一下“被跳过的类别和数量”,及时发现异常。
另一个典型坑是图片格式不统一。有的图片是BMP,有的是JPEG,还有的是PNG带透明通道。YOLOV5在读取时会统一处理,但如果你用OpenCV写转换脚本读图片尺寸,可能需要额外处理路径中的中文和空格。建议所有图片文件名都用纯数字或英文字母,路径里不要出现中文或特殊符号,否则跨平台之后很容易出问题。
5.3 模型长时间不收敛怎么办
如果训练了很多轮,loss始终在一个水平线上不动,或者mAP一直很低,优先检查这几件事。
第一,确认标签数据真的是有目标框的,而不是全是空文件。打开labels/train目录随便抽查几个txt文件,看看行数和数值是否合理。第二,确认图片数据本身是清晰可辨的,没有严重压缩、加水印或者模糊到人眼都看不清的程度。第三,检查类别分布,某个类别只有三五十张图,模型基本学不会这个类。第四,看看学习率设置,train.py默认会用内置的学习率调度策略,但如果你自行修改过hyp.yaml,要小心初始学习率过高导致loss震荡。
还有一个容易被忽视的点:图片尺寸差距过大。如果你的训练集里同时有1920x1080的大图和240x240的小图,都被缩放到640x640的话,小图里的物体可能被放大失真,大图里的目标被缩小到难以识别。这种数据分布会让模型收敛困难,检测效果也飘忽不定。最好按目标实际尺寸对图片做分组训练,或者把imgsz调大并配合rect模式(矩形训练,不强制缩放成正方形)。
6. 实操中最实用的几个小技巧
训练流程跑通了,效果也达标了,接着还有几个日常使用频率很高的小技巧值得分享。
第一个技巧是冻结主干训练。如果数据集很小(几万张以内就算小),可以先冻结主干层训练几十轮,然后再解冻全部层微调几十轮。做法是在train.py前面加一个--freeze 10参数,YOLOV5支持这个参数。冻结的好处是防止小数据集上过拟合,收敛也更稳定。我的经验是先训练50轮得到初步结果,再解冻训练到收敛。
第二个技巧是用标签分析脚本快速体检数据。YOLOV5仓库里有一个visdrone相关脚本或者其他自制脚本可以统计每个类别的标签数量、bbox大小分布、图片尺寸分布等。如果没有现成工具,可以自己写个简单的统计脚本,统计结果的几个关键数字看一眼就知道数据有没有明显问题。尤其是bbox太小(比如小于32x32像素的目标)占了一半以上时,检测难度会非常大,模型再怎么调也很难全检出来。
第三个技巧是数据扩增别拉满。YOLOV5默认的hyp.yaml里其实带了不少数据增强策略,比如马赛克增强(mosaic)、翻转、颜色扰动、随机缩放等。对小目标增多的业务场景,马赛克增强效果很好,但如果你发现训练到后面验证集的mAP反而不稳定,可以把马赛克概率适当降一些。这个参数在hyp.scratch-low.yaml或hyp.scratch-high.yaml里对应的键是mosaic。实际情况中,增强太猛会让模型在训练后期难以收敛到更细的精度。
第四个技巧是善用断点续训。train.py支持--resume参数,格式是--resume runs/train/exp/。如果训练过程中断电或显存崩了,不用从头再来。我个人习惯在每个项目里固定用--name参数区分实验,这样每次实验的日志、权重、训练曲线全部分开存放,不会互相覆盖,对比结果时一目了然。
第五个技巧是批量推理时加--save-crop,可以自动裁剪所有检测到的目标保存成图片。这个功能在整理业务样例、检查误报漏报、做模型效果汇报时非常好用。我经常用这个功能快速生成一批检测结果样本给团队看,不用自己写裁剪代码。
做YOLOV5训练自己的VOC数据集,说白了就是把数据结构、路径、类别、尺寸这几件事理清楚,训练命令本身很简单。这篇文章里的转换脚本、划分脚本和配置模板,我基本在每次接新数据时都会复用,换了数据集只需要改类别名称和路径。如果你按照上面的流程操作,先跑通一个小的验证demo,再扩充全量数据训练,整个流程会很顺。希望这期内容能帮你少走一些弯路。