news 2026/9/15 2:08:16

YOLOV5实战:VOC 20类目标检测从数据转换到部署全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
YOLOV5实战:VOC 20类目标检测从数据转换到部署全流程

简介:面向目标检测入门与实战的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.txt

train 和 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 4000tvmonitor 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-size16每批图片数量8 或 4
--img640输入分辨率512 或 480
--epochs300训练轮数100,看早停效果
--workers8数据加载线程数Windows 建议 0 或 2
--devicecpu训练设备0 表示第一张 GPU
--patience100验证指标不提升时早停的轮数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中的clsfl_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 末尾,保持两个列表顺序一致,然后重新转换标签再训练,整个流程可以无缝衔接。这个习惯从第一版数据集就开始建立,越往后越省时间。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 2:04:26

基于STM32的NES游戏机制作全记录:从PCB设计到6502模拟器实现

做这一版基于 STM32 的 NES 游戏机&#xff0c;起因其实特别简单&#xff1a;家里那台老红白机彻底不开机了&#xff0c;但抽屉里几十盘卡带还都好好的。正好手上一直想做一个能覆盖嵌入式全流程的完整项目&#xff0c;于是决定用 STM32 把 NES 模拟器跑起来&#xff0c;再做一…

作者头像 李华
网站建设 2026/9/15 2:04:20

科研计算避坑指南:从环境配置到GPU并行的实战经验

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 2:04:17

3个新手入门坑:wordpress域名空间怎么买才不亏

3个新手入门坑:wordpress域名空间怎么买才不亏 备案流程一头雾水,域名和空间选错,几千块打水漂。很多新手入门做 WordPress 建站,第一关就卡在了“域名”和“空间(服务器/主机)”的选择上。别急,这俩东西看似简单,实则坑多。选错了,网站打开慢如蜗牛,备案卡半个月,甚至后期迁移数据头大。…

作者头像 李华
网站建设 2026/9/15 2:03:38

跨境电商广告投放优化:精准定位与成本控制策略

1. 海外广告投放的常见误区与成本黑洞做跨境电商的朋友们应该都深有体会&#xff1a;每次查看广告报表时&#xff0c;总有种"钱花得不明不白"的感觉。我服务过上百家出海企业&#xff0c;发现平均有47%的广告预算其实都浪费在了错误的地方。这不是危言耸听——当你在…

作者头像 李华
网站建设 2026/9/15 2:00:00

51单片机秒表课程设计完整方案:定时器中断、动态扫描与仿真调试详解

又是51单片机秒表。带过课程设计的老师看到这个题目恐怕会心一笑&#xff1a;它不算难&#xff0c;但每年都能筛掉一批“原理图乱接、代码跑不通、报告全靠抄”的学生。作为带过好几年工训课的人&#xff0c;我一直认为秒表是所有单片机课设里性价比最高的题目——它把定时器、…

作者头像 李华
网站建设 2026/9/15 1:58:13

ECG信号处理与QRS波检测的Matlab实现

1. ECG信号处理与QRS波检测概述心电信号(ECG)是反映心脏电活动的重要生理信号&#xff0c;其中QRS波群对应着心室的去极化过程&#xff0c;是ECG信号中最显著的特征波。准确检测QRS波对于计算心率、诊断心律失常等临床应用至关重要。然而在实际采集过程中&#xff0c;ECG信号常…

作者头像 李华