ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

YOLO猫狗数据集训练避坑指南:VOC/COCO/YOLO标签格式转换与实战

YOLO猫狗数据集训练避坑指南:VOC/COCO/YOLO标签格式转换与实战

简介:这份YOLO猫狗目标检测数据集面向计算机视觉入门与进阶开发者,适用于目标检测模型训练、算法验证及课程实验等场景。数据均为真实场景采集的高质量图片,场景丰富,经labelimg精细标注,标注框质量可靠,并同步提供voc(xml)、coco(json)与yolo(txt)三种格式标签,分目录存放,可直接接入YOLO系列模型训练。压缩包共约2000个文件,以1986个xml标注文件为主,另含少量html教程、txt说明与py脚本,整体约115.12MB。资源附带环境搭建、训练案例教程及数据集划分脚本,可按需自行划分训练集、验证集与测试集,省去从零整理数据的繁琐流程。目前已有535人学习下载,适合希望快速获得规范标注数据、缩短数据准备周期并专注模型调优的读者参考使用。

1. 拿到一份 YOLO 猫狗数据集,先别急着 train:三种标签格式到底怎么选

很多人拿到「YOLO猫狗目标检测数据集(含5000张图片)+对应voc、coco和yolo三种格式标签+划分脚本+训练教程.rar」这类压缩包,第一反应是解压、改data.yaml、直接yolo train。我见过太多人卡在这一步:训练跑起来了,mAP 却低得离谱,回头查半天才发现标签格式和模型配置对不上。猫狗检测本身不难,难的是同一批图片被三种格式各写了一遍,你得先搞清楚它们各自在说什么,再决定用哪一套喂给 YOLO。

这份数据集的价值不在「5000 张」这个数字,而在于它把 VOC、COCO、YOLO 三种标注同时给了出来。VOC 是 XML 逐图描述,COCO 是单个 JSON 汇总,YOLO 是每图一个 txt 的归一化坐标。三种格式描述的是同一件事,但字段含义、坐标基准、类别编号规则完全不同。新手最容易翻车的地方,就是把 COCO 的[x, y, w, h]当成 YOLO 的归一化值直接塞进去,或者把 VOC 的 1-based 类别 id 当成 0-based 用。

这篇文章面向三类人:刚入门目标检测、想拿猫狗数据跑通第一个模型的;手里有类似多格式数据集、需要做格式转换和划分的;以及准备把猫狗检测迁移到其他类别、想复用这套流程的。我会按「格式原理 → 转换与划分 → 训练配置 → 避坑 → 进阶验证」的顺序讲,每一步都给可抄的命令和参数,最后落到怎么判断这份数据集值不值得投入。

2. 三种标签格式的字段对照与转换脚本

2.1 VOC、COCO、YOLO 的坐标基准差异

先把三种格式的核心字段摆在一起看,不然后面转换全是玄学。

维度VOC (XML)COCO (JSON)YOLO (txt)
存储方式每图一个 xml全部图一个 json每图一个 txt
坐标字段xmin,ymin,xmax,ymaxbbox: x,y,w,hclass cx cy w h
坐标基准绝对像素,左上右下绝对像素,左上宽高归一化 0~1,中心宽高
类别编号名称字符串category_id 从 1 开始从 0 开始
图片尺寸存在 size 节点存在 images 数组不存,需外部读取

关键差异有三个。第一,VOC 和 COCO 用绝对像素,YOLO 用归一化值,转换时必须除以图片宽高。第二,VOC 是左上右下两点,COCO 是左上角加宽高,YOLO 是中心点加宽高,三者几何含义不同。第三,类别编号起点不同,COCO 的category_id通常从 1 开始,YOLO 必须从 0 开始,差一位就是全部错位。

提示:转换前先用PIL或cv2读一遍图片真实尺寸,不要相信 XML 里写的size,有些数据集在裁剪后没更新这个字段,会导致归一化全错。

2.2 用 Python 把 VOC 转成 YOLO 格式

下面这段脚本把 VOC 的 xml 批量转成 YOLO 的 txt,同时生成classes.txt。假设目录结构是annotations/放 xml,images/放同名图片。

import os import xml.etree.ElementTree as ET from PIL import Image # 类别顺序决定 YOLO 的 class id,必须和后续 data.yaml 一致 CLASSES = ["cat", "dog"] CLASS_TO_ID = {c: i for i, c in enumerate(CLASSES)} def voc_to_yolo(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 tree = ET.parse(os.path.join(xml_dir, xml_file)) root = tree.getroot() # 用真实图片尺寸,不用 xml 里的 size img_name = root.find("filename").text img_path = os.path.join(img_dir, img_name) w, h = Image.open(img_path).size lines = [] for obj in root.findall("object"): cls_name = obj.find("name").text.strip() if cls_name not in CLASS_TO_ID: continue # 跳过未定义类别,避免 id 错位 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) # 转成中心点 + 宽高,再归一化 cx = (xmin + xmax) / 2.0 / w cy = (ymin + ymax) / 2.0 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h # 裁剪越界值,防止归一化后超出 0~1 cx, cy = min(max(cx, 0), 1), min(max(cy, 0), 1) bw, bh = min(max(bw, 0), 1), min(max(bh, 0), 1) lines.append(f"{CLASS_TO_ID[cls_name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") out_name = os.path.splitext(xml_file)[0] + ".txt" with open(os.path.join(out_dir, out_name), "w") as f: f.write("\n".join(lines)) voc_to_yolo("annotations", "images", "labels")

逻辑说明:CLASSES的顺序就是最终 YOLO 的类别编号,cat是 0,dog是 1,这个顺序必须和训练时的data.yaml完全一致,否则模型学到的「0」可能是狗。坐标转换先算中心点和宽高,再除以图片真实宽高做归一化。越界裁剪是必须的,VOC 标注里经常有框超出图片边界的情况,不裁剪会导致 YOLO 训练时 loss 异常。

参数说明:xml_dir、img_dir、out_dir按实际路径改。CLASSES如果数据集里还有别的类别,按字母序或自定义顺序补全,但一旦定了就不要中途改。:.6f保留六位小数,YOLO 官方推荐精度,太少会丢坐标。

2.3 COCO 转 YOLO 与划分脚本的配合

COCO 转 YOLO 稍微绕一点,因为 COCO 的 json 里图片和标注是分开的两个数组,需要先用image_id做关联。

import json import os from PIL import Image CLASSES = ["cat", "dog"] # COCO 的 category_id 从 1 开始,这里建立映射到 0-based CAT_ID_TO_YOLO = {1: 0, 2: 1} def coco_to_yolo(json_path, img_dir, out_dir): os.makedirs(out_dir, exist_ok=True) data = json.load(open(json_path)) # 建立 image_id -> 文件信息 的索引 img_info = {img["id"]: img for img in data["images"]} # 按 image_id 聚合标注 anns = {} for ann in data["annotations"]: anns.setdefault(ann["image_id"], []).append(ann) for img_id, info in img_info.items(): w, h = info["width"], info["height"] lines = [] for ann in anns.get(img_id, []): if ann["category_id"] not in CAT_ID_TO_YOLO: continue x, y, bw, bh = ann["bbox"] # COCO 是左上角 + 宽高 cx = (x + bw / 2) / w cy = (y + bh / 2) / h nw, nh = bw / w, bh / h cls = CAT_ID_TO_YOLO[ann["category_id"]] lines.append(f"{cls} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}") out_name = os.path.splitext(info["file_name"])[0] + ".txt" with open(os.path.join(out_dir, out_name), "w") as f: f.write("\n".join(lines)) coco_to_yolo("annotations/instances.json", "images", "labels")

逻辑说明:COCO 的bbox是[x, y, width, height],x、y 是左上角绝对坐标,所以中心点是x + w/2。CAT_ID_TO_YOLO这个映射表是必须的,因为 COCO 的category_id不保证连续,也不保证从 1 开始,直接减 1 是常见错误。聚合标注用字典按image_id分组,避免每张图都遍历整个 annotations 数组。

参数说明:json_path指向 COCO 的 instances 文件,img_dir是图片目录。如果数据集里 COCO 的category_id不是 1、2,先打印data["categories"]看清楚再改映射表。

划分脚本一般做三件事:按比例切分 train/val/test、把图片和标签同步移动到对应目录、生成data.yaml。常见做法是 8:1:1 或 7:2:1,猫狗这种两类任务 8:1:1 足够。划分时要注意图片和标签必须同名同批移动,否则会出现「有图无标签」的静默错误,训练时这些图会被当成负样本,拉低召回。

3. 用这份数据集跑通 YOLO 训练的最小配置

3.1 data.yaml 的四个必填字段与路径陷阱

YOLO 训练只认一个data.yaml,字段不多但每个都能让你翻车。

# data.yaml path: /home/user/catdog # 数据集根目录,绝对路径最稳 train: images/train # 相对 path 的训练图片目录 val: images/val # 相对 path 的验证图片目录 nc: 2 # 类别数,猫和狗 names: ["cat", "dog"] # 顺序必须和转换时的 CLASSES 一致

四个字段里,path用绝对路径,train和val用相对路径,这是 YOLO 官方推荐的写法,混用相对和绝对最容易出「找不到图片」的报错。nc和names的长度必须相等,names的顺序就是类别 id 的顺序,和前面转换脚本里的CLASSES一一对应。标签目录不用写进 yaml,YOLO 默认去图片路径里把images替换成labels找同名 txt,所以目录结构必须是images/train对labels/train。

注意:如果你的标签目录不叫labels,或者图片和标签不在同级,YOLO 会静默跳过找不到的标签,训练照跑但学不到东西。跑之前先用ls labels/train | wc -l和ls images/train | wc -l对一下数量。

3.2 从预训练权重开始训练的命令与参数

猫狗检测数据量 5000 张,不算大,从 COCO 预训练权重微调是最稳的路线,别从零训。

yolo detect train \ model=yolov8n.pt \ data=data.yaml \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20 \ project=runs/catdog \ name=exp1

逻辑说明:model=yolov8n.pt用 nano 版本,5000 张图两类目标,nano 足够且训练快,显存占用低。epochs=100配合patience=20,20 轮验证指标不升就早停,避免过拟合。imgsz=640是 YOLO 默认输入尺寸,猫狗目标通常占图比例较大,640 够用,如果图片里猫狗很小再考虑 1280。batch=16是 8G 显存的安全值,显存大可加到 32。

参数说明:lr0=0.01是初始学习率,微调任务常用 0.01,从零训才用 0.01 以上。project和name决定输出目录,方便多次实验对比。如果训练时 loss 一开始就 nan,先查标签里有没有归一化值超过 1 或为负,再查nc和实际类别数是否一致。

3.3 训练过程中的三个观察指标

训练日志里重点看三个:box_loss、cls_loss、mAP50。box_loss下降说明框的位置在收敛,cls_loss下降说明类别判断在变好,mAP50是最终效果的直接体现。猫狗两类任务,5000 张图,正常情况 mAP50 能到 0.9 以上。如果box_loss一直不降,八成是标签坐标有问题;如果cls_loss震荡,检查类别是否严重不均衡,比如猫 4500 张狗 500 张。

验证阶段用yolo detect val model=runs/catdog/exp1/weights/best.pt data=data.yaml,看每类的 precision 和 recall。如果某一类 recall 特别低,去验证集里翻几张漏检的图,大概率是这类目标在训练集里样本太少或标注质量差。

4. 多格式数据集转换与训练的避坑清单

4.1 类别编号错位导致全盘学错

现象:训练 loss 正常下降,mAP 也有数值,但推理时猫狗标签完全反了,或者置信度普遍偏低。

原因:VOC 的类别是字符串,COCO 的category_id从 1 开始,YOLO 要求从 0 开始。转换时如果直接拿 COCO 的 id 减 1,而category_id恰好是 1、2 就没事,但有些数据集是 1、3 或 0、1,减 1 就错位。更隐蔽的是 VOC 转 YOLO 时CLASSES顺序和data.yaml的names不一致。

解决:转换前先打印所有出现过的类别名和 id,建立显式映射表,不要用算术运算推。转换后抽三张图,用yolo detect predict跑一下,人工核对标签和框是否对应。

4.2 图片和标签不同步导致静默丢样本

现象:训练能跑,但 mAP 比预期低一截,验证集上某些图明显有目标却检测不到。

原因:划分脚本只移动了图片没移动标签,或者图片和标签文件名大小写不一致(Linux 下区分大小写)。YOLO 找不到标签时不会报错,直接当负样本处理。

解决:划分后做一次数量核对,images/train和labels/train的文件数必须相等,文件名去掉扩展名后必须一一对应。写个脚本做集合差集,把有图无标签、有标签无图的都列出来。

4.3 归一化坐标越界引发 loss 异常

现象:训练刚开始 loss 就是 nan 或者异常大,几十轮都不收敛。

原因:VOC 或 COCO 的标注框超出图片边界,转换时没裁剪,归一化后出现大于 1 或小于 0 的值。YOLO 对越界坐标的梯度计算会爆炸。

解决:转换脚本里加min(max(v, 0), 1)裁剪,同时统计裁剪了多少个框,如果比例超过 5%,说明原始标注质量有问题,考虑直接丢弃这些框或整图。

4.4 验证集混入训练集造成指标虚高

现象:验证 mAP 高得离谱,接近 1.0,但实际推理新图片效果差。

原因:划分脚本用随机切分时没固定随机种子,或者按图片切分时同一张图的多个标注被分到不同集合。更常见的是数据集本身有重复图片,一张在 train 一张在 val。

解决:划分时固定random.seed(42),按图片文件名做切分而不是按标注。划分后用图片哈希去重,把 train 和 val 之间重复的图删掉。

4.5 直接改 data.yaml 的 names 顺序

现象:换了类别名顺序后重新训练,模型完全学不会。

原因:names的顺序决定了类别 id,改了顺序等于把之前所有标签的 id 含义都改了,但标签文件里的数字没变,模型学到的映射全乱。

解决:names一旦确定就不要改。如果确实要调整类别,重新跑一遍转换脚本,让标签文件和names同步更新。

5. 怎么验证这份数据集值不值得投入,以及一个提效技巧

判断一份猫狗数据集能不能用,别只看图片数量,按下面三步走。第一步,抽 20 张图用yolo detect predict跑预训练模型,看猫狗能不能被检出来,能检出来说明图片质量过关。第二步,统计每类标注框的数量和尺寸分布,如果某一类框数少于总数的 10%,训练时要做过采样或加权。第三步,用yolo detect val在划分好的验证集上跑一遍,看 mAP50 和每类 recall,recall 低于 0.7 的类别重点查标注。

一个提效技巧:把三种格式的转换和划分写成一个prepare.py,用命令行参数控制输入格式,这样换数据集时只改路径不改逻辑。

import argparse def main(): parser = argparse.ArgumentParser() parser.add_argument("--format", choices=["voc", "coco", "yolo"], required=True) parser.add_argument("--src", required=True, help="标注源目录或 json 路径") parser.add_argument("--img", required=True, help="图片目录") parser.add_argument("--out", default="labels", help="输出标签目录") args = parser.parse_args() if args.format == "voc": voc_to_yolo(args.src, args.img, args.out) elif args.format == "coco": coco_to_yolo(args.src, args.img, args.out) else: print("已是 YOLO 格式,直接检查目录结构") if __name__ == "__main__": main()

逻辑说明:用argparse把格式选择做成参数,VOC 和 COCO 走各自的转换函数,YOLO 格式只做检查。这样一份脚本能复用到其他数据集,不用每次重写。参数说明:--format三选一,--src对 VOC 是 xml 目录、对 COCO 是 json 文件路径,--img是图片目录,--out默认labels。

我自己踩过最深的坑是第一次做格式转换时没核对类别顺序,训练了两天才发现猫狗标签反了,白白浪费算力。后来养成的习惯是:转换完先抽三张图可视化,确认框和类别都对,再开始训练。这个习惯帮我省下的时间,远比多写几行检查代码多。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进