简介:面向目标检测入门与实战的YOLOV5完整项目包,涵盖VOC格式20类数据集、训练/推理代码及训练好的权重参数,经测试可直接运行。数据集包含训练集13700张图片及对应txt标注,验证集3425张图片及标签,覆盖火车、船、人、电视、飞机等常见类别。项目训练了100个epoch,最优精度达到map0.5=0.62、map0.5:0.95=0.42,并保存了训练结果与推理效果,便于对比参考。压缩包共2000个文件,以txt标签文件、Python脚本、yaml配置为主,另有shell脚本和md文档,整体大小357.51MB,结构清晰,省去了自行收集标注与调试环境的环节。已有159人学习,适合希望快速开展目标检测实验或进行VOC数据上手的读者。
1. YOLOV5 与 VOC 20 类目标检测:先解决标签、配置、验证三件事
YOLOV5 和 VOC 数据集的组合,是目标检测入门最常见、也最容易踩坑的路线之一。VOC 提供 20 个常见类别的标注数据,YOLOV5 则是当前把训练、评估、导出串得最完整的开源项目之一;但真正动手时,卡住人的往往不是模型结构,而是标签格式、数据 yaml、类别顺序这三件琐碎事。很多人直接拿官网默认配置训练,loss 能掉,验证时却发现 bottle、pottedplant 这类小目标几乎检测不到,问题就出在数据没对齐。这篇文章从 VOC 的 XML 标注开始,走到 YOLOV5 的标签转换、训练命令、评估指标和部署导出,按一条可完整复现的路径讲完,适合刚接触检测项目的新手,也适合想系统核对一遍流程的工程师。
2. VOC 标签体系与 YOLOV5 的数据集转换步骤
2.1 VOC 20 类与 XML 标注格式
VOC 全称 PASCAL VOC,20 个类别分别是 aeroplane、bicycle、bird、boat、bottle、bus、car、cat、chair、cow、diningtable、dog、horse、motorbike、person、pottedplant、sheep、sofa、train、tvmonitor。从目标尺寸看,person、car 这类目标占画面比例大,bottle、pottedplant、bird 这类小目标多,类别分布天然不均衡,后续训练和评估都要围绕这一点做取舍。
VOC 的原始标注是 XML 文件,核心信息包括图片宽高、目标名称和 bndbox 矩形框。一份典型标注如下:
<annotation> <folder>VOC2007</folder> <filename>000001.jpg</filename> <size> <width>500</width> <height>375</height> <depth>3</depth> </size> <object> <name>person</name> <bndbox> <xmin>100</xmin> <ymin>150</ymin> <xmax>200</xmax> <ymax>250</ymax> </bndbox> </object> </annotation>这段 XML 表示图片中有一个人,框的左上角在 (100, 150),右下角在 (200, 250),坐标单位是像素。YOLOV5 不直接消费 XML,它要求每个图片对应一个同名 txt 文件,每行是“类别 id、归一化中心 x、归一化中心 y、归一化宽、归一化高”。转换时最常犯的错是把 xmin/xmax 当成中心坐标直接除以宽,或者忘记坐标需要归一化,这些都会让模型学到完全错误的定位关系。
| XML 字段 | YOLO txt 字段 | 转换说明 |
|---|---|---|
| object/name | 第 0 列 cls_id | 按 voc.yaml 中的 names 顺序映射 |
| bndbox xmin, ymin | 计算中心点用 | (xmin + xmax) / 2 / 图片宽 |
| bndbox xmax, ymax | 计算中心点用 | (ymin + ymax) / 2 / 图片高 |
| size width, height | 归一化分母 | 为 0 或缺失时会导致坐标异常 |
2.2 把 Annotations 转换为 YOLOV5 labels 的脚本与目录规则
先明确 YOLOV5 的目录结构。常见做法是把数据集整理成如下形式:
datasets/VOC/ ├── images/ │ ├── train/000001.jpg │ └── val/000001.jpg └── labels/ ├── train/000001.txt └── val/000001.txttrain 和 val 是目录,不是文件列表。训练时给定--data voc.yaml,YOLOV5 会扫描 images/train 下的所有图片,并把路径中的 images 替换成 labels 来寻找同名 txt。理解了这条替换规则,就明白为什么目录名和文件名必须严格一致。
下面这段脚本可以把 VOC XML 批量转换为 YOLO 格式:
import os import xml.etree.ElementTree as ET CLASSES = [ 'aeroplane', 'bicycle', 'bird', 'boat', 'bottle', 'bus', 'car', 'cat', 'chair', 'cow', 'diningtable', 'dog', 'horse', 'motorbike', 'person', 'pottedplant', 'sheep', 'sofa', 'train', 'tvmonitor' ] def xml_to_yolo(xml_path, out_dir): tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') width = float(size.find('width').text) height = float(size.find('height').text) lines = [] for obj in root.iter('object'): name = obj.find('name').text if name not in CLASSES: continue cls_id = 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) x_center = (xmin + xmax) / 2.0 / width y_center = (ymin + ymax) / 2.0 / height box_w = (xmax - xmin) / width box_h = (ymax - ymin) / height lines.append(f'{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}') base = os.path.splitext(os.path.basename(xml_path))[0] with open(os.path.join(out_dir, base + '.txt'), 'w') as f: f.write('\n'.join(lines)) if __name__ == '__main__': os.makedirs('labels/train', exist_ok=True) for xml_file in os.listdir('Annotations'): if xml_file.endswith('.xml'): xml_to_yolo(os.path.join('Annotations', xml_file), 'labels/train')这段脚本先读取图片宽高,再把 bndbox 的绝对像素坐标转成归一化中心点和宽高。CLASSES列表的顺序必须和后面 voc.yaml 里的 names 完全一致,因为落到 txt 里的 cls_id 是这个列表的下标。转换完成后,把图片和标签各自按 train/val 划分目录,再用下面的命令把文件搬进对应目录:
mkdir -p images/train images/val labels/train labels/val while read name; do mv JPEGImages/${name}.jpg images/train/ mv labels/${name}.txt labels/train/ done < ImageSets/Main/train.txt while read name; do mv JPEGImages/${name}.jpg images/val/ mv labels/${name}.txt labels/val/ done < ImageSets/Main/val.txt这里直接用官方 ImageSets/Main 下的 train.txt 和 val.txt 做划分。while read循环把每个不带后缀的文件名取出来,分别移动图片和标签。注意如果某张图片在标注文件里没有任何有效目标,脚本也会生成空 txt,这是 YOLOV5 能正常识别的状态,训练时会被当作背景样本跳过。
2.3 训练集划分与每个类别的目标数量统计
划分完成后,不要急着训练,先统计一次类别分布。VOC 官方划分里 person、car 数量多,tvmonitor、pottedplant 数量少,如果只看总 loss 很难发现小类别被淹没。统计标签文件里每个类别出现的次数:
from collections import Counter import glob classes = Counter() for txt_path in glob.glob('labels/train/*.txt'): with open(txt_path) as f: for line in f: parts = line.strip().split() if parts: classes[int(parts[0])] += 1 names = [ 'aeroplane', 'bicycle', 'bird', 'boat', 'bottle', 'bus', 'car', 'cat', 'chair', 'cow', 'diningtable', 'dog', 'horse', 'motorbike', 'person', 'pottedplant', 'sheep', 'sofa', 'train', 'tvmonitor' ] for cls_id, count in sorted(classes.items()): print(f'{names[cls_id]:12s} {count}')这段代码按类别 id 计数,输出形如person 4000、tvmonitor 500的分布,目的是让你在训练前知道哪些类别天然少。统计结果如果能明显看出长尾分布,可以在后续训练中针对少数类别做重采样,而不是指望模型自己学好稀有类。
| 划分方式 | 适用场景 | 注意事项 |
|---|---|---|
| 官方 ImageSets 划分 | 学术对比、复现论文指标 | 直接用官方 train.txt / val.txt |
| 业务数据自行划分 | 自定义类别或新增数据 | 按 8:2 随机切分,确保每类在验证集中出现 |
| 按视频时间切分 | 连续帧数据 | 不能随机切,否则验证集和训练集帧几乎相同 |
3. YOLOV5 训练 VOC 20 分类的配置、命令与关键参数
3.1 voc.yaml 与模型 yaml 的编写要点
数据集准备好后,第一步是写数据 yaml。YOLOV5 的数据配置比老版本简单,只需要指定图片目录和类别名:
# datasets/voc.yaml path: ./datasets/VOC train: images/train val: images/val nc: 20 names: [ 'aeroplane', 'bicycle', 'bird', 'boat', 'bottle', 'bus', 'car', 'cat', 'chair', 'cow', 'diningtable', 'dog', 'horse', 'motorbike', 'person', 'pottedplant', 'sheep', 'sofa', 'train', 'tvmonitor' ]path是数据集根目录,train 和 val 相对于 path 填写;nc必须等于 20;names的顺序和转换脚本里的 CLASSES 一致,这一点出错的概率最高。模型侧的models/yolov5s.yaml里也有 nc 字段,但训练时会被数据 yaml 覆盖,所以不需要每次修改模型 yaml 中的类别数。如果你不需要联网下载原图,把 COCO 数据 yaml 里常见的 download 字段删除,避免 train.py 在启动时触发不必要的下载逻辑。
3.2 用 train.py 启动训练,超参数怎么给
训练命令是 YOLOV5 里最常见的启动方式:
python train.py \ --data voc.yaml \ --weights yolov5s.pt \ --img 640 \ --batch-size 16 \ --epochs 100 \ --device 0 \ --workers 4 \ --cache--weights yolov5s.pt指定预训练权重,官方 release 页面提供 s/m/l/x 几档,VOC 数据量在一万张左右时用 s 或 m 就够;--data指向刚才写的 voc.yaml;--img 640是输入分辨率,分辨率越高小目标越容易保留,但显存占用按平方增长;--batch-size受显存限制,不够就减半。--cache会把图片提前加载到内存,减少磁盘 IO,但机器内存小于 16GB 时反而容易把内存占满,可以不加或用--cache disk。
| 参数 | 默认值 | 作用 | 显存不够时怎么改 |
|---|---|---|---|
| --batch-size | 16 | 每批图片数量 | 8 或 4 |
| --img | 640 | 输入分辨率 | 512 或 480 |
| --epochs | 300 | 训练轮数 | 100,看早停效果 |
| --workers | 8 | 数据加载线程数 | Windows 建议 0 或 2 |
| --device | cpu | 训练设备 | 0 表示第一张 GPU |
| --patience | 100 | 验证指标不提升时早停的轮数 | 50 |
| --cos-lr | 关闭 | 学习率按余弦曲线下降 | 保持关闭,数据集小时更稳 |
3.3 显存不足和 NaN Loss 的排查顺序
训练中最常遇到的两个问题,一个是 RuntimeError: CUDA out of memory,一个是 loss 变成 NaN。显存不足时优先减 batch-size,其次减 img,不要一上来就换更小的模型;如果用了--cache,内存不足也可能让系统进入交换分区,表现是训练速度骤降。
NaN 的排查顺序是:先检查标签坐标是否越界。VOC 转 YOLO 格式时,如果某个 xmax 大于图片宽,或者计算出的中心点超出 [0,1],模型会在损失计算阶段产生异常值。快速扫描所有标签:
awk '{ if ($2<0 || $2>1 || $3<0 || $3>1 || $4<0 || $4>1 || $5<0 || $5>1) print FILENAME, $0 }' labels/train/*.txt这条命令检查 txt 的第 2 到第 5 列,也就是归一化后的中心坐标和宽高。任何一行落在区间外,都说明 XML 标注本身有越界框,或者脚本的归一化逻辑写错了。修完数据后如果还是 NaN,把 AMP 关掉再试,混合精度在特定 PyTorch 版本和某些 GPU 上会引发梯度溢出,--amp false能快速排除这种可能。
4. 用 val.py 评估 VOC 20 类模型,并针对短板类别调优
4.1 val.py 的评估命令与五个核心指标
训练完成后,不要只看 train loss,用验证集单独评估一轮:
python val.py \ --data voc.yaml \ --weights runs/train/exp/weights/best.pt \ --img 640 \ --batch-size 32 \ --conf-thres 0.001 \ --iou-thres 0.6 \ --verbose--conf-thres 0.001是计算 mAP 时的置信度下限,设得极低是为了把所有可能框都纳入评估,得到完整的 PR 曲线,这不是笔误,和推理时用的高阈值是两回事;--iou-thres 0.6是判断预测框和真值框匹配的 IoU 门槛。输出里会按类别列出 AP,最后给出总体 mAP。如果训练过程正常,runs/train/exp/下还会生成 result.png 和 confusion_matrix.png。
# 更直观的聚合指标可以用官方 weights 自带脚本查看 python - <<'PY' import torch model = torch.hub.load('ultralytics/yolov5', 'custom', path='runs/train/exp/weights/best.pt') results = model.val() # 返回 mAP、Precision、Recall print(results) PY这段代码用 torch.hub 加载训练好的权重,直接调用验证接口。注意model.val()内部会读取模型配置里的数据集路径,所以传入的 best.pt 在训练时已经绑定了 voc.yaml 信息。实际工作中我更多直接跑上面的 val.py,因为它输出的 per-class 表格更完整。
| 指标 | 全称 | 关注点 | VOC 项目里怎么看 |
|---|---|---|---|
| Precision | 精确率 | 预测框中真框占比 | 误检多时偏低 |
| Recall | 召回率 | 真框被找出的比例 | 漏检多时偏低 |
| mAP@0.5 | 平均精度均值 | IoU 阈值 0.5 下的综合表现 | 最常用的主指标 |
| mAP@0.5:0.95 | 严格版 mAP | 多个 IoU 阈值下平均 | 框得准不准 |
| F1 | 调和平均 | P 和 R 的平衡点 | 挑 conf-thres 用 |
4.2 从混淆矩阵和每类 AP 定位难例
confusion_matrix.png 是 YOLOV5 训练目录里最有价值的一张图,横轴是真实类别,纵轴是预测类别。对角线越亮说明该类正确率高;如果某个真实类别的亮度散布到好几列,说明它容易被哪些类混淆。VOC 项目里最常见的混淆是 person 和 chair、bird 和 aeroplane,因为外观和尺度接近。
看到混淆后,先不要急着调训练参数。把 val.py 输出的逐类 AP 和整体 mAP 对比,AP 明显低于均值的类别才是真正的短板。如果是数量少导致的,属于数据问题;如果数量不少但 AP 低,才是模型或参数问题。两种问题的处理方式不同,混在一起调参只会越调越乱。
4.3 类别不均衡与小目标的优化方向
VOC 的 20 类长尾分布明显,person、car 样本多,pottedplant、tvmonitor 样本少。YOLOV5 本身没有类别权重的官方参数,常见做法是对稀有类别做图片级重采样,把这些图片在训练目录中重复一份或多份,变相提高参与训练的次数:
import os import shutil import glob rare_ids = {15, 19} # pottedplant, tvmonitor for txt_path in glob.glob('data/images-raw/labels/train/*.txt'): classes = set() with open(txt_path) as f: for line in f: classes.add(int(line.split()[0])) if classes & rare_ids: base = os.path.splitext(txt_path)[0] img_src = base.replace('/labels/', '/images/') + '.jpg' if os.path.exists(img_src): shutil.copy(txt_path, base + '_rare.txt') shutil.copy(img_src, base + '_rare.jpg')这段脚本把包含稀有类别的图片和标签复制成_rare结尾的新文件,放在同一个 images/labels 目录里,YOLOV5 扫描时会把它们当作新样本读入。注意复制后数据集总量变大,训练轮数可以适当减少。另一种方法是修改data/hyps/hyp.scratch-low.yaml中的cls和fl_gamma参数,cls控制分类损失权重,fl_gamma让模型更关注难分样本,对长尾分布有一定缓解。
小目标检测方面,优先尝试把--img从 640 提到 800,分辨率提高对小目标的特征保留明显。如果显存撑不住,就做滑窗切图,把大图切成 640x640 的 patch 再训练,这是工程上处理小目标最直接、最可控的方案,效果通常比换更大的模型更明显。
5. YOLOV5 推理部署与 3 个实战技巧
5.1 detect.py 参数选择与常见业务取值
训练完模型,第一件事是用 detect.py 跑一组真实图片:
python detect.py \ --weights runs/train/exp/weights/best.pt \ --source test_images/ \ --conf-thres 0.25 \ --iou-thres 0.45 \ --line-thickness 2--source可以指向图片目录、单张图片或视频文件;--conf-thres 0.25是置信度阈值,低于这个值的框会被过滤,业务里误检多就调到 0.4 到 0.5,漏检多就往下调;--iou-thres 0.45是 NMS 的 IoU 阈值,控制重叠框的合并力度,一般保持默认。实际项目里,先用低阈值看漏检,再用高阈值看误检,最后选一个业务可接受的平衡点。
5.2 导出 ONNX 与加速推理的常用做法
需要落地到服务或边缘设备时,最常见的做法是先导出 ONNX:
python export.py \ --weights runs/train/exp/weights/best.pt \ --include onnx \ --opset 12导出后可以用 onnxruntime 或 TensorRT 加载。在 Jetson Nano 这类设备上,常见的做法是先导 ONNX,再转成 TensorRT engine,同时把输入分辨率固定到训练时的尺寸,避免动态 shape 带来的额外开销。如果推理框架不支持某些算子,优先调低--opset版本,比改模型结构快得多。
5.3 伪标签加人工修正扩展数据集
最后一个技巧针对“数据不够”的场景。先用当前模型对未标注图片做批量预测,保留高置信度结果:
python detect.py \ --weights best.pt \ --source unlabeled_images/ \ --save-txt \ --save-conf \ --conf-thres 0.5--save-txt会把预测框写成 YOLO 格式 txt,--save-conf在每行末尾附带置信度。这些 txt 可以直接导入 labelImg 或 CVAT 作为初始标注,人工只需要修正框位置和类别,而不是从零画框,标注效率能提升很多。伪标签的置信度阈值建议设高一些,0.5 甚至 0.6,避免把模型自己的错误学回去。
后续要为业务新增第 21 个类别时,只需要把新类别追加到转换脚本的 CLASSES 和 voc.yaml 的 names 末尾,保持两个列表顺序一致,然后重新转换标签再训练,整个流程可以无缝衔接。这个习惯从第一版数据集就开始建立,越往后越省时间。
本文还有配套的精品资源,点击获取