简介:这份资源是面向目标检测初学者与进阶开发者的YOLOv5实战项目包,围绕VOC格式的20分类数据集展开,涵盖火车、船、人、电视、飞机等常见类别,适合用来练习数据标注解析、模型训练与推理部署的完整流程。压缩包共约2000个文件,以1921个txt标签与说明、40个Python脚本、23个yaml配置、9个sh脚本及若干md文档为主,整体约357.51MB,目录结构清晰,便于按模块查阅。项目已迭代100个epoch,runs目录下保存了训练结果,最佳精度达到map0.5=0.62、map0.5:0.95=0.42,并在runs/detect中保留了网络推理训练集的全部结果,可直接观察实际检测效果。训练集含13700张图片及对应标签,测试集含3425张图片及对应标签,代码经测试可直接运行。目前已有159人学习,适合希望快速复现YOLOv5训练与推理、理解参数配置与结果分析的读者参考。
1. 从一份 20 分类 VOC 数据集说起:YOLOV5 实战到底在练什么
手里拿到一份 20 分类的 VOC 格式目标检测数据集,很多人第一反应是直接train.py一把梭,结果跑完发现 mAP 卡在 0.3 上不去,回头查才发现标注里有一堆宽高为 0 的脏框,类别名和data.yaml对不上,训练集里还有几张图压根没标注文件。YOLOV5 实战项目配 VOC 数据集这件事,难点从来不在模型本身,而在数据从 VOC 的 XML 结构搬到 YOLO 的 txt 结构这一段的工程细节。这篇笔记就围绕「20 分类 VOC 数据集怎么喂给 YOLOV5」这条主线,把格式转换、类别映射、超参数设置、训练排错、部署验证整条链路拆开讲。适合已经跑通过官方 COCO 示例、想换成自己业务数据集的人,也适合被 VOC 转 YOLO 格式坑过一次、想搞清楚边界条件的熟手。读完你应该能独立把一份 20 类的 VOC 数据集跑出可用的检测模型,并且知道每一步出错时该看哪个文件。
2. VOC 的 XML 和 YOLO 的 txt 差在哪:先把格式对齐再谈训练
2.1 两种标注格式的结构差异
VOC 格式的核心是每张图对应一个同名 XML 文件,里面用<object>节点描述每个目标,包含<name>类别名和<bndbox>下的xmin/ymin/xmax/ymax四个绝对像素坐标。YOLO 格式则是每张图对应一个同名 txt 文件,每行一个目标,格式是class_id x_center y_center width height,后四个值全部是相对图像宽高的归一化值,范围 0 到 1。这个差异决定了转换脚本必须做两件事:把类别名映射成从 0 开始的整数索引,把绝对坐标除以图像宽高做归一化。
20 分类意味着类别索引是 0 到 19,映射关系必须固定且和data.yaml里的names列表顺序严格一致。我见过最常见的翻车就是转换时按set()去重类别名,结果每次运行顺序不一样,训练时标签全乱,loss 看着在降但 mAP 死活上不去。正确做法是先人工确定一个类别顺序,写死在脚本里。
2.2 转换脚本:从 XML 批量生成 YOLO txt
下面这个脚本是我一般会用的版本,处理 20 分类 VOC 数据集,同时做脏框过滤和图像尺寸校验。
import os import xml.etree.ElementTree as ET from PIL import Image # 类别顺序必须和 data.yaml 的 names 完全一致,写死不要用 set 动态生成 CLASSES = [ "aeroplane", "bicycle", "bird", "boat", "bottle", "bus", "car", "cat", "chair", "cow", "diningtable", "dog", "horse", "motorbike", "person", "pottedplant", "sheep", "sofa", "train", "tvmonitor" ] CLASS_TO_ID = {name: i for i, name in enumerate(CLASSES)} def convert_annotation(xml_path, img_path, out_txt_path): tree = ET.parse(xml_path) root = tree.getroot() # 用真实图像尺寸做归一化,不要信 XML 里的 size 节点,经常和实际图不符 with Image.open(img_path) as im: w, h = im.size lines = [] for obj in root.iter("object"): name = obj.find("name").text.strip() if name not in CLASS_TO_ID: continue # 类别不在 20 类里直接跳过,避免索引越界 bbox = obj.find("bndbox") xmin = float(bbox.find("xmin").text) ymin = float(bbox.find("ymin").text) xmax = float(bbox.find("xmax").text) ymax = float(bbox.find("ymax").text) # 裁剪到图像边界内,VOC 里常有超出边界的框 xmin, xmax = max(0, xmin), min(w, xmax) ymin, ymax = max(0, ymin), min(h, ymax) bw, bh = xmax - xmin, ymax - ymin if bw <= 1 or bh <= 1: continue # 宽高过小的脏框直接丢,训练时是纯噪声 xc = (xmin + xmax) / 2.0 / w yc = (ymin + ymax) / 2.0 / h lines.append(f"{CLASS_TO_ID[name]} {xc:.6f} {yc:.6f} {bw/w:.6f} {bh/h:.6f}") with open(out_txt_path, "w") as f: f.write("\n".join(lines)) def batch_convert(xml_dir, img_dir, out_dir): os.makedirs(out_dir, exist_ok=True) for xml_file in os.listdir(xml_dir): if not xml_file.endswith(".xml"): continue stem = os.path.splitext(xml_file)[0] img_path = os.path.join(img_dir, stem + ".jpg") if not os.path.exists(img_path): print(f"缺图跳过: {stem}") continue convert_annotation( os.path.join(xml_dir, xml_file), img_path, os.path.join(out_dir, stem + ".txt") ) if __name__ == "__main__": batch_convert("./VOCdevkit/VOC2007/Annotations", "./VOCdevkit/VOC2007/JPEGImages", "./labels/train")逻辑上分三步:先读 XML 拿到所有 object,再用 PIL 打开原图拿真实宽高,最后逐框做边界裁剪、脏框过滤、归一化写出。参数上CLASSES列表是唯一真相来源,data.yaml必须和它逐字对应;bw <= 1 or bh <= 1这个阈值可以根据数据集调整,小目标多的场景可以放宽到 0.5 像素,但不要设成 0,否则会引入大量无效框。xc/yc/bw/bh保留 6 位小数是 YOLOV5 官方脚本的习惯,精度足够且文件不会太大。
2.3 划分训练集验证集并生成 data.yaml
转换完 txt 之后,按 8:2 或 9:1 划分 train 和 val,注意划分的是图像和标签成对移动,不要只移一边。data.yaml是 YOLOV5 读取数据的入口,20 分类的写法如下:
path: /home/user/voc20 train: images/train val: images/val nc: 20 names: 0: aeroplane 1: bicycle 2: bird 3: boat 4: bottle 5: bus 6: car 7: cat 8: chair 9: cow 10: diningtable 11: dog 12: horse 13: motorbike 14: person 15: pottedplant 16: sheep 17: sofa 18: train 19: tvmonitornc必须等于 20,names的键值对顺序必须和转换脚本里的CLASSES一致。YOLOV5 在加载时会校验标签里的 class_id 是否小于nc,如果转换时用了别的顺序,这里不会报错但训练结果会完全错乱,这是最隐蔽的坑之一。
3. 用 conda 把 YOLOV5 环境配起来:版本对齐比装得快更重要
3.1 环境配置的版本选择
YOLOV5 对 PyTorch 和 CUDA 的版本比较敏感,尤其是涉及torchvision的 NMS 算子。我一般用 conda 建独立环境,Python 选 3.8 或 3.9,PyTorch 选 1.12 到 2.0 之间的稳定版,CUDA 11.6 或 11.8。不要盲目追最新版,YOLOV5 仓库的requirements.txt里对某些包有上限约束,装太新反而会触发兼容问题。
conda create -n yolov5_voc python=3.9 -y conda activate yolov5_voc # 按本机 CUDA 版本选对应 wheel,这里以 cu118 为例 pip install torch==2.0.1 torchvision==0.15.2 --index-url https://download.pytorch.org/whl/cu118 git clone https://github.com/ultralytics/yolov5.git cd yolov5 pip install -r requirements.txt装完后跑一句python -c "import torch; print(torch.cuda.is_available())"确认 GPU 可用。如果返回 False,先查驱动版本和 CUDA 运行时是否匹配,不要急着改代码。requirements.txt里会装opencv-python、matplotlib、pyyaml这些,如果服务器没有图形界面,opencv-python可能报 libGL 缺失,换成opencv-python-headless即可。
3.2 用预训练权重做一次冒烟测试
在正式训练自己的 20 分类数据集之前,先用官方 COCO 预训练权重跑一次推理,确认环境和模型加载链路没问题。
python detect.py --weights yolov5s.pt --source data/images/bus.jpg --device 0这条命令会用yolov5s.pt对示例图做推理,输出到runs/detect/exp/。如果能看到带框的结果图,说明环境、权重加载、推理后处理都通了。这一步的意义在于把环境问题和数据问题隔离开,后面训练出问题就不用怀疑环境。--device 0指定第一块 GPU,CPU 推理去掉这个参数即可,但速度会慢很多,20 分类数据集不建议用 CPU 训练。
3.3 训练命令与关键超参数
正式训练 20 分类 VOC 数据集,我一般从yolov5s起步,显存够的话上yolov5m。命令如下:
python train.py \ --data data/voc20.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --hyp data/hyps/hyp.scratch-low.yaml \ --device 0 \ --workers 8 \ --project runs/train \ --name voc20_exp1--img 640是输入分辨率,小目标多的数据集可以提到 1280,但显存占用会翻倍。--batch 16要根据显存调,8G 显存跑 640 分辨率大概能到 16,12G 能到 32。--hyp选hyp.scratch-low.yaml是因为从预训练权重微调时,低学习率增强策略更稳,如果数据集很小(每类不到 500 张),可以换成hyp.finetune.yaml。--workers是数据加载线程数,设成 CPU 核数的 1 到 2 倍,设太高反而会因为 IO 争抢变慢。
训练过程中重点看三个指标:box_loss、obj_loss、mAP@0.5。正常情况下 box_loss 在前 10 个 epoch 快速下降,obj_loss 缓慢下降,mAP 在 20 epoch 后开始爬升。如果 box_loss 一直不降,大概率是标签格式有问题;如果 mAP 卡在 0.1 以下,检查data.yaml的路径和类别数。
4. 20 分类 VOC 训练避坑:那些让 mAP 原地踏步的细节
4.1 类别不平衡导致小类几乎不召回
现象:训练完看混淆矩阵,person、car这类大类 mAP 能到 0.7,但pottedplant、diningtable这些小类 mAP 接近 0。原因:VOC 数据集本身类别分布极不均匀,20 类里前几类占了 70% 以上的框,模型倾向于预测高频类。解决:在data.yaml同级目录建一个train_balanced.txt,对小类图像做重复采样,或者用--hyp里的cls参数调大分类损失权重,从默认 0.5 提到 0.8 到 1.0。更彻底的做法是用WeightedRandomSampler改 dataloader,但改动量大,一般先调cls权重试试。
4.2 图像和标签文件名不匹配
现象:训练启动时报No labels found或者assertion error,但明明 txt 文件都在。原因:YOLOV5 默认按图像路径把扩展名替换成.txt去找标签,如果图像是.jpeg而标签是.txt,或者图像在images/train而标签在labels/train但目录结构不对,就会找不到。解决:确认目录结构是images/train/xxx.jpg对应labels/train/xxx.txt,文件名主干完全一致。YOLOV5 支持通过--img-path和标签路径的约定来定位,不要手动改代码里的路径拼接逻辑,按官方约定放就行。
4.3 缓存文件导致改了标签不生效
现象:改了某个 txt 里的类别,重新训练发现结果没变化。原因:YOLOV5 第一次训练会在同目录生成.cache缓存文件,记录图像尺寸和标签索引,后续训练直接读缓存。解决:删掉labels/train.cache和labels/val.cache,或者加--noval先跑一个 epoch 重建缓存。这个坑很隐蔽,因为缓存文件是隐藏的,ls不加-a看不到。
4.4 显存溢出和 batch 设置
现象:训练到第几个 batch 突然CUDA out of memory。原因:--batch设太大,或者--img分辨率太高,或者--workers太多导致数据加载占用显存。解决:先把--batch减半,如果还溢出就降--img到 512 或 416。另外 YOLOV5 支持--batch-size -1自动批大小,会根据显存自动调整,不确定时用这个。注意自动批大小在训练中途不会动态调整,只是启动时探测一次。
4.5 验证集 mAP 波动大
现象:每个 epoch 的 mAP 上下跳动超过 0.1。原因:验证集太小,20 分类如果验证集只有几百张,统计噪声很大。解决:把验证集比例提到 20% 以上,或者用--val时增加--conf-thres和--iou-thres的稳定性。另外检查验证集里是否有类别缺失,如果某个类在验证集里一张都没有,那一类的 mAP 会是 0 并拉低整体。用python -c "from collections import Counter; ..."统计一下验证集类别分布,确保 20 类都有样本。
5. 训练完怎么验证和部署:从 mAP 到实际推理
5.1 用 val.py 做完整评估
训练结束后不要只看训练日志里的 mAP,用val.py单独跑一次完整评估,输出每类的 P、R、mAP@0.5、mAP@0.5:0.95。
python val.py \ --data data/voc20.yaml \ --weights runs/train/voc20_exp1/weights/best.pt \ --img 640 \ --batch 16 \ --task val \ --device 0输出会按类别列出指标,重点看小类的召回率。如果某个类 mAP 低于 0.2,回到第 4 章查类别不平衡和标注质量。--task val是标准验证模式,--task test会用 test 集,--task speed只测速度不测精度。best.pt是验证集 mAP 最高的权重,last.pt是最后一个 epoch 的,一般用best.pt部署。
5.2 导出 ONNX 并在 Python 里推理
YOLOV5 部署最常见的是导出 ONNX,然后用onnxruntime推理,跨平台且不依赖 PyTorch。
python export.py --weights runs/train/voc20_exp1/weights/best.pt --include onnx --img 640 --batch 1导出后在 Python 里加载:
import onnxruntime as ort import numpy as np import cv2 sess = ort.InferenceSession("best.onnx", providers=["CUDAExecutionProvider"]) img = cv2.imread("test.jpg") img = cv2.resize(img, (640, 640)) img = img[:, :, ::-1].transpose(2, 0, 1) # BGR 转 RGB 再转 CHW img = np.ascontiguousarray(img, dtype=np.float32) / 255.0 img = img[None, ...] # 加 batch 维度 outputs = sess.run(None, {sess.get_inputs()[0].name: img}) # outputs[0] 形状是 [1, 25200, 25],25 = 4 框坐标 + 1 obj + 20 类 pred = outputs[0][0] scores = pred[:, 4:5] * pred[:, 5:] # obj 置信度乘类别概率参数上providers选CUDAExecutionProvider走 GPU,没有 GPU 就换CPUExecutionProvider。25200是 640 分辨率下三个检测头的锚框总数,25是 4 个框参数加 1 个 objectness 加 20 个类别概率。后处理需要自己做 NMS,onnxruntime不包含 NMS 算子,可以用cv2.dnn.NMSBoxes或者torchvision.ops.nms。如果部署到树莓派这类边缘设备,建议导出时加--include onnx后用onnxsim简化,再转成 NCNN 或 TFLite,速度会好很多。
5.3 一个容易被忽略的验证技巧
训练完先别急着部署,拿几张训练集里没见过的图,用detect.py跑一遍,把结果图和原图并排看。重点看两类错误:一类是漏检,尤其是小目标和遮挡目标;另一类是误检,把背景纹理当成目标。如果误检集中在某个类别,比如把sofa误判成chair,说明这两个类的特征在 20 分类里区分度不够,可以考虑合并类别或者增加这两类的难例样本。这个肉眼验证步骤比看 mAP 数字更直接,我一般会花半小时做这件事,能发现很多指标反映不出来的问题。
6. 把 20 分类 VOC 数据集跑稳的几个进阶习惯
训练 20 分类 VOC 数据集这件事,跑通一次不难,难的是每次换数据集都能稳定复现。我自己的习惯是先把转换脚本和data.yaml放进版本控制,每次改类别顺序或增删类别都留 commit,这样出问题能回滚。另外训练前一定跑一遍数据校验脚本,统计每类框数、图像数、宽高分布,把异常值打印出来。下面这个校验片段我基本每次都会用:
import os from collections import Counter def check_labels(label_dir, nc=20): cls_counter = Counter() empty_files = 0 for fname in os.listdir(label_dir): if not fname.endswith(".txt"): continue with open(os.path.join(label_dir, fname)) as f: lines = [l for l in f.read().strip().split("\n") if l] if not lines: empty_files += 1 continue for line in lines: cid = int(line.split()[0]) assert 0 <= cid < nc, f"{fname} 类别越界: {cid}" cls_counter[cid] += 1 print("每类框数:", dict(sorted(cls_counter.items()))) print("空标签文件数:", empty_files) check_labels("./labels/train")这个脚本能一次性暴露类别越界、空标签、类别分布三个问题。空标签文件在 YOLOV5 里是合法的,表示这张图是纯背景,但如果空标签占比超过 10%,说明数据集里负样本太多,需要检查是不是漏标了。类别分布打印出来后,如果最大类和最小类框数差 100 倍以上,就要考虑第 4 章说的重采样或调cls权重。
还有一个习惯是固定随机种子。YOLOV5 默认不固定种子,每次训练结果会有小幅波动,做对比实验时容易误判。在train.py里加--seed 42,配合--deterministic能让结果可复现。注意--deterministic会稍微降低训练速度,但换来的是实验可比性,调参阶段值得开。
最后说一个我踩过的坑:有次换了一份 20 分类数据集,data.yaml里names写的是中文类别名,训练能跑但导出 ONNX 后推理结果全乱。原因是 ONNX 导出时类别名编码不一致,后处理按索引取名字时对不上。后来统一改成英文类别名,中文只在可视化时映射,问题就没了。所以类别名尽量用英文或拼音,别在data.yaml里直接写中文,这个习惯能省掉后面部署环节的很多麻烦。希望帮到你。
本文还有配套的精品资源,点击获取