
简介面向目标检测入门与实战的YOLOV5完整项目包涵盖VOC格式20类数据集、训练/推理代码及训练好的权重参数经测试可直接运行。数据集包含训练集13700张图片及对应txt标注验证集3425张图片及标签覆盖火车、船、人、电视、飞机等常见类别。项目训练了100个epoch最优精度达到map0.50.62、map0.5:0.950.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 VOC20 个类别分别是 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 folderVOC2007/folder filename000001.jpg/filename size width500/width height375/height depth3/depth /size object nameperson/name bndbox xmin100/xmin ymin150/ymin xmax200/xmax ymax250/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.yamlYOLOV5 会扫描 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_okTrue) 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必须等于 20names的顺序和转换脚本里的 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 ($20 || $21 || $30 || $31 || $40 || $41 || $50 || $51) 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, pathruns/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召回率真框被找出的比例漏检多时偏低mAP0.5平均精度均值IoU 阈值 0.5 下的综合表现最常用的主指标mAP0.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 与加速推理的常用做法需要落地到服务或边缘设备时最常见的做法是先导出 ONNXpython 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 末尾保持两个列表顺序一致然后重新转换标签再训练整个流程可以无缝衔接。这个习惯从第一版数据集就开始建立越往后越省时间。本文还有配套的精品资源点击获取