拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

目标检测数据集制作全流程:从标注到VOC/COCO/YOLO格式转换避坑指南

目标检测数据集制作全流程:从标注到VOC/COCO/YOLO格式转换避坑指南

上周帮人排查一个检测训练发散的问题,模型结构是照抄的 SOTA,预处理也按论文来了,可训练到第 20 轮 loss 突然放飞自我,一开始怀疑学习率,后来怀疑 BN 崩溃,折腾一晚上才发现是 COCO 转 YOLO 的时候中心点坐标忘了归一化,训练脚本读进来的框全部偏出图像。这种问题在检测数据集制作全流程里非常典型:收集、标注、VOC、COCO、YOLO 格式互转,每一步单独拿出来都有现成工具,但连起来跑的时候,各种隐性约定才是真正的坑。这篇就按我实际走通的流程聊一遍,适合正在自己攒数据集、或者需要给团队搭数据标注管线的朋友,目标是让你从零做完一套检测数据集,并且在格式转换这件事上不再返工。

1. 为什么数据集制作决定了检测项目的生死

1.1 模型再强也喂不饱脏数据

很多朋友一开始做检测项目,最关心的是换哪个 backbone、加哪个注意力模块、抄哪篇论文的训练 trick。但以我接触过的项目体感,模型结构能拉开的那几个点,在脏数据面前不值一提。数据标注错 10%,mAP 掉的不只是 10%,而是可能让某个类别直接废掉,因为错框会同时污染正样本和负样本:该框的地方没框,模型学到的是"这里没目标";不该框的地方框了,模型学到的是"这里有目标"。这种矛盾样本积累多了,loss 会一直在高位震荡,怎么看都像模型不收敛。

真正的检测项目落地流程,大概 70%~80% 的精力都花在数据上。这里说的数据不只是"图片数量",而是清洗过、标注过、格式正确的样本集合。我见过太多人拿公开数据集直接训练,loss 曲线很漂亮,一到自己场景就拉胯,原因是公开数据和你实际部署环境的光照、角度、目标尺度分布完全不一样。反过来说,一份标注准确、场景覆盖合理、格式统一的数据集,哪怕用三四年前的 YOLOv5,也能在垂直场景里跑出可用的效果。这就是为什么我坚持认为,数据集的制作水准直接决定检测项目的生死。

1.2 三种格式本质上是在用不同方言说同一句话

先给新手一个定心丸:VOC、COCO、YOLO 这三种格式,描述的是同一件事——"哪张图里,有哪些类别,各自在什么位置"。区别只是存储方式、坐标单位和组织结构的差异。你可以把它们理解成用三种方言说同一句话,意思一样,但发音和语法不一样。

  • VOC 格式:一个图片对应一个 XML 文件,框的坐标用xmin, ymin, xmax, ymax表示,单位是像素,且是绝对坐标。
  • COCO 格式:所有图片和标注统一存在一个 JSON 文件里,框的坐标用x, y, w, h表示,单位是像素,但 (x,y) 是左上角。
  • YOLO 格式:一张图片对应一个 txt 文件,框的坐标用x_center, y_center, w, h表示,且全部做了归一化,值在 0~1 之间。

既然描述的是同一件事,为什么还要互转?因为不同训练框架、不同预训练模型、不同评测工具要求的输入格式不一样。你拿 COCO 预训练权重微调自己的数据,可能数据得是 COCO 风格;上了 YOLO 训练脚本,又要 txt;有些线上标注平台导出的是 VOC,你没得选。所以"格式互转"不是可选项,而是每个做检测的人都要过的关。

1.3 格式错误的爆发点都在训练中后期

这是最坑的地方:格式错误很少在启动训练时报错。你转完格式,训练能跑起来,loss 也在降,模型看起来一切正常。然后训到一半,或者训完验证的时候,发现某些类别完全没有预测框,或者框的位置全是偏的,这时候你再回头查数据,之前几十个小时的训练时间已经搭进去了。

我个人的经验是,格式转换里的坐标系错误、类别索引错位、尺寸信息丢失这三类问题,全是"潜伏型"问题。它们不像语法错误那样在解析阶段就炸给你看,而是进入训练流程后才通过损失曲线、验证曲线、可视化结果逐步暴露。所以这篇文章后面专门会讲验证环节,那一步做好了,能帮你省掉大量返工时间。

2. 数据收集:先定义问题,再找图

2.1 任务边界决定收集倾向

数据收集的第一步不是打开爬虫或者找公开数据集,而是把任务边界写清楚。同样是"目标检测",类别数量、目标尺度、场景复杂度不同,数据收集的策略完全不一样。

举个例子,你在热搜里能看到各种各样的检测需求:鸟类目标检测、开关闭合状态检测、传送带异物检测、遥感图像目标检测、车辆检测(比如 BDD100 这类自动驾驶数据集)。这几个任务看起来都是"找目标画框",但实际要求天差地别:

  • 鸟类细分检测:类别之间长得像,需要重点收集不同姿态、不同光照、不同背景下的同种鸟,分类难度远大于定位难度。
  • 开关闭合检测:一个开关只有"合"和"断"两个状态,目标小、位置固定,难点在于小目标特征区别微弱,对图像分辨率和标注精度要求极高。
  • 传送带异物检测:异物种类不确定,属于开放集问题,收集阶段就得刻意涵盖"正常物料"和"各种异物"两类,并且异物样本通常极少,需要靠产线采集慢慢积累。
  • 遥感图像检测:目标密集、尺度变化巨大、大量小目标,收集时要特别注意卫星或无人机成像的分辨率、拍摄角度,以及不同地域的样本均衡。
  • 自动驾驶车辆检测:要求在复杂道路场景下不漏检,遮挡、截断、夜间、雨雾都是必须覆盖的维度,还要区分轿车、卡车、公交车、行人等类别。

所以说,数据收集不只是"找图",而是在用图片定义你的问题。我习惯先写一份数据需求说明,把每个类别至少需要多少张、每张图期望包含多少个目标、必须覆盖哪些环境变化列清楚,然后再开始找数据,效率会高很多。

2.2 公开数据集和自有采集怎么搭

公开数据集是起步最快的燃料。COCO、Pascal VOC、Open Images 这类通用数据集,适合做预训练和通用场景验证;如果你做的是垂直领域,优先搜有没有现成的领域数据集,比如车辆场景有 BDD100K,医疗影像有一些公开的细胞或病灶数据集,电力设备红外检测也有相关的开源数据。用公开数据起步的好处是标注相对规范,格式统一(多为 COCO 或 VOC),可以直接拿来练手和跑通流程。

但垂直场景的检测项目,自制数据永远是不可替代的。公开数据集覆盖不了你产线上那个光影环境,也覆盖不了你无人机飞的那个高度和角度。我自己的做法是"公开数据打底、自有数据调优":先用公开数据把模型结构跑通,然后花主要精力采集和标注自有场景数据,尤其是那些公开数据集里覆盖不到的长尾情况。采集时至少要做到:

  • 同一个目标,从多个角度、多个距离、多种光照条件下拍摄;
  • 目标之间要有遮挡、靠近、截断的场景,不要全是"完美摆拍";
  • 背景要多样化,否则模型会学到"背景记忆"而不是"目标记忆";
  • 记录真实部署时相机的型号和安装位置,尽量用同款设备收集。

另外,数据增强不能解决数据量不足的问题,它只能解决"不变性"问题——比如平移、缩放、亮度变化这些模型应该具备的抗干扰能力。如果某个场景本身就缺样本,靠增强是补不回来的,增强出来的还是同一个场景的变换,不是新场景。

2.3 别忽略清洗与合规

收集阶段的清洗比很多人想象得重要。公开数据集里经常有 label 错的图、模糊到人眼都无法判断的图、以及同一张图在不同数据集里重复出现的情况。我见过有人把两个公开数据集合并直接用,结果训练集和验证集之间出现重复图片,验证指标虚高得一塌糊涂,换到真实场景立刻原形毕露。清洗手段很朴素:算感知哈希或直接用文件名去重;抽样人工看图;把明显模糊、过暗、过曝的图删掉。

合规这块也要提一句,虽然不展开,但采集图像素材时要注意来源合法、避免侵犯隐私和个人信息,尤其涉及人脸、车牌等敏感信息时,要么脱敏要么避开。这不是小题大做,而是工程长期化必须考虑的事。

3. 标注阶段:框怎么画,比框画得多重要

3.1 工具选型要看流程而不只看手感

标注工具的选择,别只看"画框顺不顺手",要看它对你的整个数据管线友不友好。我自己用过的几类工具,感受如下:

工具适合场景特点注意点
LabelImg单机小规模标注轻量、直接导出 VOC XML 和 YOLO txt项目大了管理困难,多人协作基本靠手动拷贝
Label Studio团队协作、多模态标注支持分类、检测、分割、文本等多种标注,可配置模板需要部署,标注界面稍重,但格式导出能力强
CVAT团队 + 视频/密集标注有半自动标注插件,适合大量重复场景需要 Docker 部署,上手曲线高一些
X-AnyLabeling个人 + 半自动辅助集成推理模型辅助标注,节省人工依赖模型效果,脏标注需要人工确认

我的建议是:如果你只是几百张图验证想法,LabelImg 完全够用;如果你要批量做几千上万张,直接上 Label Studio 或 CVAT,因为它们自带多人任务分配、标注进度管理和格式导出,后面省事得多。另外,无论用哪个工具,标注人员的操作习惯要和工具导出格式匹配,否则导出结果经常出现不是我预期结构的坑。

3.2 标注规范:那些容易吵起来的边界情况

标注规范如果只写一句"把目标框出来",后面一定会出问题。最容易引发分歧的边界情况有这么几类:

  • 目标被遮挡:框应该紧贴可见部分,还是按完整目标的外形框?行业惯例是框可见部分,但如果遮挡比例超过一定阈值(比如 50%),建议加 flag 或直接不标,取决于你的应用目标。
  • 目标截断在图像边缘:框超出图像边界时,YOLO 训练一般要求坐标在 [0,1] 内,所以要么把越界部分截断到边界,要么干脆删除这种样本,看你的训练逻辑。
  • 极小目标:低于某个像素阈值(比如小于 16x16)的目标,人眼都容易标歪。要么专门定义小目标处理策略,要么在下采样标注前做好筛选。
  • 两个目标重叠严重:要明确是标两个实例还是一个,否则模型学到的框会产生歧义。
  • 类别边界模糊:比如"鸟"和"飞鸟"是不是一类?"轿车"和"跑车"要不要分开?这种问题要在标注规范里提前写死,否则不同人标出来的类别语义不一致。

这些规范落实到文档里,再配几张"正确框 / 错误框"的示例图给标注人员看,比反复口头解释强得多。

3.3 质检:抽检之外的一致性校验

很多团队的质检方式是"按比例抽检",比如标完 1000 张抽 100 张人工看。抽检有用,但不够,它抓不住系统性的坐标偏移和类别混淆。如果条件允许,我建议让两个人标注同一批图,然后计算一致性:框级别可以用 IoU 判断,类别级别可以直接对比标签。如果两个人的 IoU 均值低于 0.8,说明标注规范执行有问题,得回去重新对齐。

更实用的做法是转成可视化检查:标注完成后随机抽图,把框画回原图并显示类别和置信度(如果工具支持),人眼扫一遍。这一步能发现绝大多数"框大了一圈""类别标串了"的问题。质检发现问题后,要能定位到具体标注员并反馈,形成闭环,否则问题只会重复出现。

4. 三种格式的精讲:从目录结构到坐标约定

4.1 VOC 是"目录 + XML",不是随便一个文件夹

Pascal VOC 的经典目录结构长这样:

VOCdevkit/ VOC2007/ JPEGImages/ # 原始图片 Annotations/ # 每个图片对应的 XML ImageSets/ Main/ train.txt # 训练图片文件名列表(不带扩展名) val.txt trainval.txt

每个 XML 文件里记录了图片文件名、尺寸、以及每个目标的类别和边界框:

<annotation> <filename>000001.jpg</filename> <size> <width>500</width> <height>375</height> <depth>3</depth> </size> <object> <name>bird</name> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>100</xmin> <ymin>150</ymin> <xmax>200</xmax> <ymax>250</ymax> </bndbox> </object> </annotation>

这里最关键的是<size>里的宽高,因为后续转 YOLO 要做归一化,必须靠它做分母。另外<difficult>表示这个目标是否属于难例,很多训练框架会忽略 difficult=1 的样本,转格式时要注意筛选逻辑。

注意:图片文件名要和 XML 文件名一一对应,而且 VOC 的 train.txt 里只写文件名不含扩展名,比如000001而不是000001.jpg。这是新手最容易忽略的点。

4.2 COCO 是"一个大 JSON",结构错了连解析都过不去

COCO 格式把整个数据集的元信息和标注塞进一个 JSON 文件里,核心是五个字段:info、licenses、images、annotations、categories。真正干活的是后三个。

images是一个数组,每个元素描述一张图:

{ "id": 1, "file_name": "000001.jpg", "width": 500, "height": 375 }

annotations是标注数组,每个元素描述一个目标实例:

{ "id": 1, "image_id": 1, "category_id": 3, "bbox": [100, 150, 100, 100], "area": 10000, "iscrowd": 0 }

注意bbox是[x, y, w, h],单位像素,(x,y) 是框左上角。category_id必须对应categories数组里的 id,而categories里每个类别又有自己的 id 和 name:

{"id": 3, "name": "bird", "supercategory": "animal"}

COCO 的每一张图都要有唯一的 id,每个标注实例也要有唯一的 id,并且通过image_id关联到图。新手写转换脚本时最容易犯的错就是 id 不连续、或者category_id和images里的 id 混用。

4.3 YOLO 是"每张图一个小 txt",类别藏在文件名里

YOLO 格式最简单也最"松散":每张图片对应一个 txt 文件,名字和图片同名,但扩展名是.txt。txt 里每一行是一个目标:

2 0.500000 0.600000 0.400000 0.266667

五个数字依次是:class_id, x_center, y_center, width, height。其中坐标全部相对原图宽高做了归一化,所以值都在 0~1 之间。class_id 是从 0 开始的整数,对应一个名为classes.txt(不同框架叫法略有不同)的文件,按行排列类别名,第一行是 id 0,第二行是 id 1,顺序绝对不能乱。

YOLO 格式有个隐性约定:它自己不带任何"图片尺寸"信息。如果你只知道 txt 不知道原图尺寸,你连绝对坐标都恢复不出来。这也是为什么做反向转换(YOLO→VOC/COCO)时必须额外保留一份图片尺寸信息的原因,后面实操部分会再强调。

4.4 坐标系的换算关系

把三个格式放在一起对比,核心换算关系就一目了然:

格式存储位置坐标语义单位归一化
VOCXMLxmin, ymin, xmax, ymax像素否
COCOJSONx, y, w, h(左上角+宽高)像素否
YOLOTXTx_center, y_center, w, h归一化 0~1是

转换公式不复杂:

  • VOC → YOLO:x_center = (xmin + xmax) / 2 / width,y_center = (ymin + ymax) / 2 / height,w = (xmax - xmin) / width,h = (ymax - ymin) / height
  • COCO → YOLO:x_center = x / width + w / width / 2,y_center = y / height + h / height / 2
  • YOLO → COCO:x = (x_center - w / 2) * width,y = (y_center - h / 2) * height,bbox_w = w * width,bbox_h = h * height

这里有个容易绕晕的点:COCO 的 (x,y) 是左上角,而 YOLO 的 (x_center, y_center) 是中心点。换算时千万别直接把 COCO 的 x 当中心点用。我见过好几次类似错误,结果就是所有框整体向右下角偏移半个框的距离,训练出来框全偏。

5. 互转实战:先做中间结构,再谈转格式

5.1 为什么不要写 N×N 个转换函数

新手一听要三种格式互转,第一反应是写 6 个函数(VOC→COCO、VOC→YOLO、COCO→VOC……)。但更稳的做法是定义一个统一的中间结构,所有格式先解析成这个结构,再从这个结构导出成任意目标格式。这样只需要写 3 个解析函数 + 3 个导出函数,而且逻辑清晰、好调试。

我用 Python 的话,中间结构就是一个普通 dict:

{ "file_name": "000001.jpg", "width": 500, "height": 375, "objects": [ {"category": "bird", "bbox": [100, 150, 200, 250]}, # [xmin, ymin, xmax, ymax] ... ] }

注意我这里统一把 bbox 存成绝对像素的[xmin, ymin, xmax, ymax],再做任何导出都基于这个表示,能极大减少坐标系混乱带来的 bug。这个中间结构除了内存中用,我还会以 JSON 格式落一份到磁盘,相当于把整个数据集固化成一份"标准答案",后续不管要转成什么格式都从这份文件出发。

5.2 VOC 转 YOLO 的代码与细节

先解析 XML:

import xml.etree.ElementTree as ET def parse_voc(xml_path): tree = ET.parse(xml_path) root = tree.getroot() sample = { "file_name": root.find("filename").text, "width": int(root.find("size/width").text), "height": int(root.find("size/height").text), "objects": [] } for obj in root.findall("object"): name = obj.find("name").text difficult = int(obj.find("difficult").text) if obj.find("difficult") is not None else 0 if difficult == 1: continue # 按需过滤难例 bndbox = obj.find("bndbox") xmin = float(bndbox.find("xmin").text) ymin = float(bndbox.find("ymin").text) xmax = float(bndbox.find("xmax").text) ymax = float(bndbox.find("ymax").text) sample["objects"].append({"category": name, "bbox": [xmin, ymin, xmax, ymax]}) return sample

解析完成后,导出 YOLO txt:

def voc_to_yolo(sample, class_names): lines = [] w = sample["width"] h = sample["height"] for obj in sample["objects"]: cls_id = class_names.index(obj["category"]) xmin, ymin, xmax, ymax = obj["bbox"] x_center = (xmin + xmax) / 2 / w y_center = (ymin + ymax) / 2 / h box_w = (xmax - xmin) / w box_h = (ymax - ymin) / h # 越界保护,避免出现 >1 或 <0 的值 x_center = min(max(x_center, 0), 1) y_center = min(max(y_center, 0), 1) box_w = min(max(box_w, 0), 1) box_h = min(max(box_h, 0), 1) lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") return "\n".join(lines)

这里有个细节:VOC 的filename字段有时带路径,比如"JPEGImages/000001.jpg",有时只有000001.jpg,有时甚至不存在。所以解析端要做一个兜底:如果filename缺失,就用 XML 的文件名作为图片名。再有,很多情况下 VOC 的 XML 宽高和真实图片宽高不一致,最稳的办法是解析时直接用cv2.imread或PIL读一遍真实尺寸,别完全信 XML 里的<size>。读图确实慢一点,但换来的是坐标归一化的准确性,值得。

5.3 COCO 转 YOLO 的代码与细节

COCO 转 YOLO 有两条索引要理清:COCO 的category_id(比如 3)和 YOLO 的class_id(比如 0、1、2)。中间要做一个category_id → class_id的映射,怎么映射取决于你class_names列表的定义。如果你希望 YOLO 的类别顺序按 COCO JSON 里categories数组的顺序来,那就先遍历 categories 建一个字典:

import json def load_coco(coco_json_path): with open(coco_json_path, "r", encoding="utf-8") as f: coco = json.load(f) img_info = {img["id"]: img for img in coco["images"]} cat_id_to_name = {cat["id"]: cat["name"] for cat in coco["categories"]} samples = {} for ann in coco["annotations"]: if ann["iscrowd"] == 1: continue # 群体目标通常跳过 img_id = ann["image_id"] img = img_info[img_id] if img_id not in samples: samples[img_id] = { "file_name": img["file_name"], "width": img["width"], "height": img["height"], "objects": [] } x, y, w, h = ann["bbox"] samples[img_id]["objects"].append({ "category": cat_id_to_name[ann["category_id"]], "bbox": [x, y, x + w, y + h] # 转成 xmin, ymin, xmax, ymax }) return list(samples.values())

导出成 YOLO 时,class_id 的映射要和训练用的classes.txt严格一致。一个常见错误是:COCO JSON 里 categories 顺序是[1: "bird", 2: "cat"],然后你导出 YOLO 时按这个顺序生成了classes.txt,看起来合情合理。但如果你换了别人的脚本,那个脚本默认从 train.txt 里提取类别或者按字母序排,就全乱了。所以每次导出 YOLO 时,同时生成classes.txt,并且提前确认训练 YAML 里的names和这个文件逐行一致。

还有一个坑:COCO 里一张图可能没有任何 annotation,JSON 里images有它,但annotations里找不到对应记录。这种图在 YOLO 里就会生成一个空 txt。空 txt 不是错误,但训练时如果你用了mosaic增强,空图可能会被拼进去当负样本,具体好坏看你的策略。我习惯在导出时把空图单独列一个清单,由你决定是删掉还是保留。

5.4 反向转换:为什么必须保留原图宽高

YOLO→COCO 和 YOLO→VOC 的难点,在于 txt 只有归一化坐标,没有原图尺寸。如果你手头有原图文件,可以直接读图拿宽高;如果没有,就只能依赖一份额外的元数据。

我来说个真实教训:有一次我从标注平台导出一批 YOLO 格式的数据,回头要转 COCO 给另一个模型训练,但原始图片在另一台机器上,我只能先拿到文件列表,没图片。想着 txt 里 0.5 0.5 这种坐标看着也够用,就写死一个统一尺寸去转。结果某些图片实际是 1920x1080,某些是 1280x720,转出来 COCO 的像素框全错位,白白返工了两天。

所以我的强烈建议是:在你最初做任何格式转换时,就同步生成一份dataset_meta.json,记录每张图的file_name / width / height。有了它,反向转换就只是算术问题:

def yolo_to_coco_line(yolo_line, img_w, img_h): cls_id, x_center, y_center, w_norm, h_norm = map(float, yolo_line.split()) x = (x_center - w_norm / 2) * img_w y = (y_center - h_norm / 2) * img_h w = w_norm * img_w h = h_norm * img_h return int(cls_id), [x, y, w, h]

反向转换还要注意:YOLO 的坐标在归一化时可能被裁剪到 [0,1],所以反向算出来的x + w理论上等于img_w,但因为有浮点误差和越界裁剪,建议用min(max(...))做一次钳制,保证坐标不出界。

6. 一次搞对的验证清单和那些踩过的坑

6.1 转换完先做三件事

每次转换完,不管你是从哪种格式转到哪种格式,我建议立刻做三件事再进入训练:

第一,可视化抽查。随机抽 20~50 张图,把转换后的框画回原图,按类别着不同颜色,人眼扫一遍。这个动作能拦截掉 80% 以上的坐标系错误和类别错位。脚本不复杂:cv2.rectangle + cv2.putText,输出到一个visualize/目录,翻一遍也就几分钟。

第二,统计自检。写个脚本统计每个类别的标注框数量、框的宽高分布、以及是否存在异常值(比如宽高为 0、坐标超出 [-0.05, 1.05] 范围)。类别分布能帮你发现某个类别可能在转换时被整体漏掉;框的尺寸分布能帮你发现归一化的分母是不是错了——如果某一类框的宽高全部小于 0.01,大概率是宽度和高度单位搞错了。

第三,划分一致性检查。训练集、验证集、测试集的图片名单必须和转换后的标签文件一一对应。我习惯用 Python 断言:每张图片文件都存在,且对应的标签文件也存在(YOLO 场景),或者 XML/JSON 里能找到对应项。宁可跑一次挺慢的完整校验,也不要等训练中途再发现缺文件。

6.2 常见错误 Top 5 和根因

错误现象根因规避方法
训练 loss 炸到 NaN坐标未归一化,或归一化时除以了 0解析时校验宽高 > 0;统一在中间结构存绝对坐标,导出时再算归一化
所有框整体偏移把 COCO 左上角坐标当成了 YOLO 中心点严格按照公式换算,可视化抽查
某个类别的框全部消失classes.txt 的类别顺序和训练 YAML 不一致,或 category_id→class_id 映射写错每次导出时同时导出 classes.txt,并在训练配置里逐行对照
图片能读但标签对不上文件名大小写、扩展名不一致(.jpg / .JPG / .jpeg),或 Windows 路径分隔符问题统一用小写扩展名,路径统一为相对路径,脚本里做字符串规范化
转回 COCO 后框偏移YOLO→COCO 时缺少原图宽高,用了错误的统一尺寸项目初始化时就生成 dataset_meta.json 记录宽高

其中第一条我要多说一句:很多新手在归一化时直接把(xmax - xmin)当w,这没错;但如果你的xmax和xmin是字符串或者不小心被整型截断成 0,除出来就是无穷大,loss 立刻爆炸。所以解析 XML 和 JSON 时,坐标字段一定要转成float,并且对宽高做一次if box_w <= 0: continue的过滤。这种 0 宽 0 高的脏标注在公开数据里其实并不少见。

6.3 训练前的最后一公里

格式转换完成,不等于数据集就绪。训练前的最后一公里,通常还有三件事:

  • 数据集划分。不要随机划分,按场景或视频序列划分,避免同一场景的相似帧同时出现在训练集和验证集,导致验证结果虚高。比如车载视频连续帧,按时间切段,前 80% 段做训练,后 20% 段做验证。
  • 类别重算与 anchors。YOLO 自带聚类 anchors 的脚本,换数据集后建议重新聚类,不要沿用 COCO 的默认 anchors。目标尺度差异大的数据集,默认 anchors 会导致小目标漏检严重。
  • 预训练模型类别数对齐。如果要用 COCO 预训练权重微调,注意你的类别数如果和 COCO 不一致,最后一层 head 的输出维度要改。改完先冻结 backbone 训几个 epoch,再解冻全模型,这是比较稳的路径。

把这些都跑通了,再启动训练。此时如果 loss 还不正常,至少你可以底气十足地说"数据这边我确认过了",然后专注排查模型和超参。我自己的习惯是,每次转换完数据都跑一个 5~10 轮的冒烟训练,不只是看 loss,更主要的是用tensorboard或者日志里的图片预览看预测框大概落在哪。只要框大致贴住目标,说明数据管线没问题;如果框满天飞,赶紧停下来查数据,别硬训。

最后分享一个我踩过多次坑之后养成的习惯:所有转换脚本的输入输出都做成可重复执行的函数,并在每次转换完成后自动打印一行摘要,比如"从 VOC 解析 1200 张图,共 4500 个目标,其中过滤 difficult 80 个,导出 YOLO 1200 个 txt"。这行摘要看着不起眼,但它能让你在三天后回顾时一眼看出当次转换有没有异常,也能在数据集版本迭代时快速定位是哪一步出了问题。检测数据集的制作就是这样一个耐心活,前面每多花一分钟做校验,后面训练和调优阶段就能少熬一个通宵。

返回列表