☰
DETR训练自己的数据集:从COCO格式到参数调优全流程
2026/10/22 15:59:08 网站建设 项目流程

我最早把 DETR 训练自己的数据集这件事跑通,是在一个工业质检项目里。当时手头大概有一万多张标注图,类别就四类,心想有现成的 COCO 预训练权重,拿来微调不是分分钟的事。结果一上手就发现,格式化数据、改网络参数、看懂训练日志,每一环都比传统检测器麻烦不少。后面踩了几次坑,把这套流程彻底理顺,才明白 DETR 的训练逻辑跟 Faster R-CNN、YOLO 这类方法差的不是一点点。

这篇文章就围绕 DETR 训练自己的数据集,把全流程掰开讲清楚。什么样的人适合读?你是目标检测方向的学生、算法工程师,或者在用现成数据集跑过例程、但想换成自己数据的新手,这篇都能给你一个可以直接照着填的路线图。DETR 本身不是多么难改,难点在于很多人不了解它内部怎么做集合预测,导致数据、参数、训练后的坑都看不透。下面直接用一次实际项目的思路来拆解。

1. 先搞清楚 DETR 的检测逻辑,再动手训练

1.1 DETR 的核心机制:把检测当成集合预测

DETR 全称是 Detection Transformer,它最大的变化是抛弃了传统检测器里那些人工设计的东西:没有锚框,没有 NMS,甚至连 anchor-based 里的正负样本分配都省了。

它把目标检测看作一个集合预测问题。具体说,网络会生成固定数量的查询向量,每个查询向量去“认领”图上某个物体,输出这个物体的类别和边框。Decoder 端通过 Transformer 的全局注意力机制,不断调整这些查询向量,让它们互相之间不要重复预测同一个物体,最终达到一种“一个萝卜一个坑”的效果。

这里最关键的一个环节是匈牙利匹配。训练的时候,模型预测出一堆框,标签也有真实框,不能直接计算损失,而是要先做一个最优匹配,把预测框和真实框一一对应起来。匹配代价综合考虑了类别得分、边框 L1 距离和 GIoU 损失。匹配完之后,没被匹配上的预测框会被强制归为“无目标”,在训练中不会对梯度产生太强干扰。

理解了这件事,你就能明白为什么 DETR 不需要 NMS。因为查询向量之间通过自注意力已经做了“相互抑制”,理论上每个目标最多被一个查询框住。实际推理时,只需要对每个查询的输出取一个置信度阈值,过滤掉低置信度结果就行。后面的很多困惑,比如为什么训练那么久、为什么小目标查不好、为什么 num_queries 不能乱设,追根溯源都跟这套机制有关。

1.2 从 COCO 预训练模型切到自己的数据,有什么特殊之处

很多人直接拿官方权重跑 COCO 的 demo 是没问题的,但一旦换成自己的数据,首先要面对三个变化。

第一,类别数量不一样。COCO 预训练模型有几十个类别,你的数据可能只有几个类别。最后一层分类头尺寸必然不匹配,加载预训练权重的时候需要一个“跳过最后一层或者忽略 key 不匹配”的处理方式。

第二,训练周期不一样。DETR 在 COCO 上的官方配置是训练 300 个 epoch,这在主流检测器里属于很长的。它收敛慢,原因是 Transformer 的注意力机制需要大量样本来学习“查询向量如何和物体稳定匹配”。所以很多人拿自小数据集训练三五十个 epoch 发现 loss 还高得离谱,就误以为模型炸了,其实只是没喂够。

第三,对小目标的适应性更差。DETR 的骨干网络输出特征图尺度偏小,原始 Transformer 又只用单尺度特征,导致小目标信息很容易在注意力计算中被稀释。如果你的数据里小目标占比很高,训练前最好先想清楚:是继续用原始 DETR,还是换一个带多尺度特征、或者对 Head 结构适配更好的变体。这不是劝退,而是让你少走弯路。

前面这些点如果都理解了,后面每一步操作你都能明白“为什么要这么做”。接下来直接进入数据准备,这一关能卡住大半的人。

2. 数据准备:把标注转成 COCO 格式才是第一道坎

2.1 COCO JSON 格式里的每个字段要填什么

DETR 官方代码读取数据用的主要是 COCO 格式,所以不管你之前用的是 VOC 的 XML、LabelMe 的 JSON,还是 YOLO 的 txt,最终都要统一转成 COCO 的 JSON 格式。很多人的第一道坎就卡在这里。

一个标准的 COCO 目标检测标注文件,核心包含三部分:images、annotations、categories。

{ "images": [ { "id": 1, "file_name": "000001.jpg", "width": 1920, "height": 1080 } ], "annotations": [ { "id": 1, "image_id": 1, "category_id": 1, "bbox": [100, 200, 150, 80], "area": 12000, "iscrowd": 0 } ], "categories": [ { "id": 1, "name": "defect" }, { "id": 2, "name": "scratch" } ] }

注意 bbox 里的四个数字是[x, y, width, height],也就是框左上角坐标和宽高,不是 VOC 里的[xmin, ymin, xmax, ymax]。这个坑特别常见,哪怕转出来的 JSON 看着没问题,训练时 loss 也可能完全乱掉,因为框的坐标算错了。

还有一个隐藏的规范是 category_id 最好从 1 开始连续编号。DETR 的分类头是num_classes + 1的维度,最后一个维度留给“无目标”空槽,所以标注里的类别 id 用 1、2、3 这类整数最省心。如果你保留原数据集里的 3、5、7 这种跳跃 ID,要么自己写数据加载逻辑去映射,要么把 num_classes 设成最大 ID 导致输出维度冗余,都很麻烦。

2.2 一份可直接改着用的 VOC XML 转 COCO 脚本

我这里给一个从 VOC XML 转 COCO JSON 的脚本,它不依赖额外框架,只要装了 Python 和 Pillow 就能跑。你可以把它复制到自己的项目里,按需改类别映射。

import os import glob import json import xml.etree.ElementTree as ET from PIL import Image label_map = { "defect": 1, "scratch": 2, "edge": 3, "hole": 4 } def convert_voc_to_coco(xml_dir, img_dir, output_json): categories = [{"id": v, "name": k} for k, v in label_map.items()] images = [] annotations = [] ann_id = 1 xml_list = sorted(glob.glob(os.path.join(xml_dir, "*.xml"))) for img_id, xml_path in enumerate(xml_list, start=1): tree = ET.parse(xml_path) root = tree.getroot() filename = root.find("filename").text img_path = os.path.join(img_dir, filename) if not os.path.exists(img_path): print("skip missing image:", img_path) continue with Image.open(img_path) as img: width, height = img.size images.append({ "id": img_id, "file_name": filename, "width": width, "height": height }) for obj in root.findall("object"): name = obj.find("name").text if name not in label_map: 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) # VOC 的 xmax/ymax 转成 COCO 的 width/height w = max(0.0, xmax - xmin) h = max(0.0, ymax - ymin) if w <= 0 or h <= 0: continue annotations.append({ "id": ann_id, "image_id": img_id, "category_id": label_map[name], "bbox": [xmin, ymin, w, h], "area": w * h, "iscrowd": 0 }) ann_id += 1 coco_data = { "images": images, "annotations": annotations, "categories": categories } with open(output_json, "w", encoding="utf-8") as f: json.dump(coco_data, f, ensure_ascii=False, indent=2) print("done, images:", len(images), "annotations:", len(annotations)) if __name__ == "__main__": convert_voc_to_coco( xml_dir="your_voc_xml_folder", img_dir="your_image_folder", output_json="instances_train.json" )

这个脚本里有两个细节值得说下。一个是过滤掉宽高小于等于 0 的框,因为有些标注软件会导出退化成点或线的框,直接参与训练容易让 GIoU loss 抖动。另一个是判断图片文件是否存在,很多数据集里标记文件残留但图片已经被删掉,不处理的话后面 Dataloader 会报 “File not found”。

如果你的标注不是 VOC,而是 LabelMe 导出的 JSON,逻辑也差不多,只需要把多边形坐标转成外接矩形。实际上 LabelMe 的 polygon 点序列取min(x), min(y), max(x)-min(x), max(y)-min(y)就是 bbox。

2.3 目录结构、类别顺序和图片尺寸的布置建议

转好 JSON 之后,还需要按官方代码默认方式组织目录。最简单的做法是模仿 COCO 数据集的原生结构:

data/ annotations/ instances_train.json instances_val.json train2017/ 000001.jpg 000002.jpg val2017/ 000100.jpg

然后训练和评估时,统一设置--dataset_file coco和--coco_path data。官方代码内部会找train2017和val2017这两个目录名,文件名最好就别乱改了。

如果你实在想用自己的目录名,也简单,去代码里改datasets/coco.py里 image set 对应的img_folder = "train2017"改成自己的文件夹名就行。这个属于小改,不影响主线训练逻辑。

图片尺寸这块,如果你是从零训练,建议先统一短边在 480 到 800 之间,长边最多 1333。DETR 对输入做了随机缩放,短边会在一定范围内随机变化,这本身是一种数据增强。如果你的项目里小目标非常多,短边可以适当提高,比如 600 到 900,但要注意显存会跟着涨。还有一个容易被忽略的点:类别顺序决定了模型输出向量的索引,所以一旦确定 categories 里的 id 顺序,后续评估、部署、可视化都要沿用同一套映射,中途改顺序会导致之前所有 checkpoint 失去可比性。

3. 修改参数与启动训练,关键配置都在命令行里

3.1 环境准备和代码选择

训练 DETR 最省事的方式是把开源仓库的官方实现拉下来直接用,不要自己从头写一套。你需要准备的库主要有 PyTorch、torchvision、pycocotools、tqdm 这些,PyTorch 建议用 1.7 以上的稳定版本,太老的版本会对 Transformer 里的某些算子支持不好。

拉下来代码之后,先不用急着看全部源码,直接跑一次命令,确认环境能正常加载数据。很多人喜欢先改一堆代码再运行,结果报错了都不知道是环境问题还是改代码改出来的。我建议先用官方默认参数跑两个 epoch,能跑通再动自己的数据改。

3.2 训练自己数据集需要改的关键参数

DETR 的主入口是main.py,大量参数通过命令行传入。我不建议把每个参数都背下来,但下面这些一定要弄清楚。

参数作用我的常用设置
--dataset_file选择数据集加载类,自定义普通目标检测用cocococo
--coco_path数据集根目录路径./data
--num_classes实际目标类别数,不额外加背景类按自己数据填
--num_queries查询向量数量,即每张图最多可预测框数默认 100,目标少可 50
--batch_size单卡 batch size先 2,显存不够再降
--epochs总训练轮数100 到 300 之间
--lr整体学习率1e-4
--backbone_lr骨干网络学习率1e-5
--lr_drop哪一轮把学习率降 10 倍总轮数的三分之二
--pretrained预训练权重路径官方 DETR 系列权重
--resume从某个 checkpoint 继续训练用于断点续训
--output_dir权重和日志保存目录按项目起名

这里最容易困惑的是num_queries。官方 COCO 设置的是 100,因为 COCO 单张图最多可能有几十上百个物体。如果你自己的数据集里,每张图最多也就是十几个物体,那 100 个查询其实已经很多了。但不要天真地以为把它降到 10 就能省显存,过度少的查询会让模型丢失“把查询向量排列成有序集合”的能力,训练容易不稳。我的经验是,先看数据里单张图的最大目标数,设成最大目标数的 1.5 到 2 倍,同时不低于 50。如果数据里确实每张图只有两三个目标,设 50 也比设 5 要稳。

--num_classes官方代码里会自动在最后一层多输出一个“无目标”类别,所以命令行里填实际类别数就行。比如你的数据有 4 类,就填 4。这个参数在训练和评估时必须保持一致,否则模型输出维度对不上。

3.3 预训练权重的加载技巧

使用预训练权重是很多项目快速起步的关键。DETR 官方提供了基于 COCO 训练的 DETR-R50 权重,虽然你的类别和它不一样,但骨干网络和 Transformer 部分的通用特征仍然非常有用。

加载方式是在命令里加--pretrained 权重文件路径。官方代码加载的时候用的是strict=False,所以类别数不一致导致最后一层缺失,它不会报错,只是会提示缺少class_embed和query_embed的一部分参数。这里的query_embed是网络里的查询向量,通常是可学习的,它和类别数有关系吗?严格说它不直接依赖类别数,但由于它是随机初始化的,加载预训练权重时可以保留,不影响训练。

真正需要留意的是,如果--pretrained给的是官方权重,它会把class_embed权重丢掉,重新随机初始化。不要觉得奇怪,也不要在这个环节去手动改网络结构。你只需要确认日志里“missing keys”包含的是分类头相关参数,而不是主干或 Transformer 的核心层,否则可能是预训练路径给错了。

3.4 一条可以直接跑起来的训练命令

把所有关键信息整合起来,一条典型的训练命令大概是这样的:

python main.py \ --dataset_file coco \ --coco_path ./data \ --num_classes 4 \ --num_queries 100 \ --batch_size 2 \ --epochs 120 \ --lr 1e-4 \ --backbone_lr 1e-5 \ --lr_drop 80 \ --pretrained ./pretrained_weights/detr_r50.pth \ --output_dir output/defect_detr

这个命令里--lr_drop 80表示在第 80 个 epoch 时把学习率缩小 10 倍。120 个 epoch 的项目,我一般设 80 或 90,保证后期在低学习率下精细调整。如果是 300 个 epoch,就设 200。这个值设置得不好会有明显影响,设太早则后期学不动,设太晚则后期可能震荡。

单机单卡训练时,不需要额外指定分布式参数,默认就能跑。如果你有多卡,官方提供分布式训练脚本,但个人项目我用单卡比较多,先把单卡跑通,再考虑多卡。

4. 训练过程监控与模型选择,别只盯着总 loss

4.1 训练日志里每一栏分别代表什么

DETR 训练时终端会打印很多数值,新手最容易犯的毛病是只盯着一个loss看。事实上 DETR 的损失是一个加权组合,至少要拆开看几个分支。

日志里的loss是总损失,主要由三块组成:loss_ce是分类损失,loss_bbox是 L1 边框回归损失,loss_giou是 GIoU 损失。严格说还有一个cardinality_error,它表示预测框数量和真实框数量的绝对差异,这个值在训练前期会偏大,后期应该慢慢变小。

我自己的习惯是每隔 20 个 epoch 看一次这几个分量是否都保持下降趋势。分类损失下降但框损失不动,往往是正负样本匹配出问题,最常见就是类别 id 映射错误导致匹配时类别代价一直很大。GIoU 损失明显震荡,则大概率是数据里有超出图像边界的极端框,或者学习率太高。

如果你的训练命令加了--aux_loss,日志里还会出现loss_ce_0、loss_bbox_1这类的扩展项。这是 DETR 的一个辅助分支技巧:Decoder 的每一层都会输出预测,并用同一种匹配策略计算损失,帮助梯度更好地传递。正常情况下保持开启就行,它对收敛很有帮助。

4.2 评估 mAP 和保存哪个 epoch 的权重

DETR 官方代码本身不提供按 mAP 自动保存“最优权重”的机制,默认每个 epoch 都会在--output_dir下保存一个checkpoint.pth。如果训练 120 个 epoch,你就会得到 120 个权重文件,磁盘占用不小,但也给了我们自选最优点位的空间。

我的做法是每 10 个 epoch 做一次验证,用验证集计算 COCO 格式的 mAP。等训练结束,把验证 mAP 最高那个 epoch 的权重保留下来,作为最终模型。很多人的误区是觉得训练 loss 越低越好,实际上 DETR 在训练后期很容易出现过拟合,训练 loss 还在下降,但验证 AP 已经不涨甚至下跌。

评估命令相对简单,只要把训练时的主要参数再对一遍,并加上--eval和--resume:

python main.py \ --dataset_file coco \ --coco_path ./data \ --num_classes 4 \ --num_queries 100 \ --resume output/defect_detr/checkpoint0099.pth \ --eval

评估结果里会输出Average Precision (AP) @[ IoU=0.50:0.95 | area=all ]这一串,其中主要看第一个 AP。如果 AP 很高但耗时很长,说明模型复杂度还有压缩空间;如果 AP 很低,但训练 loss 正常,优先检查验证集标签是不是有问题。

4.3 显存不够和收敛太慢的实操处理

DETR 对显存的要求比普通检测器高不少,尤其是 Transformer 部分,序列长度等于 feature map 的 H×W,图像稍微大一点,显存占用就上去了。在单卡 8G 这种环境里,想直接开默认参数是有难度的。

第一个办法是降低--batch_size,降到 1 试一下。DETR 官方默认使用分布式训练,相当于总 batch size 很大,但个人项目很可能只有一张卡,batch size 太小会导致 loss 波动比较厉害。这时可以手工做梯度累积。思路是每跑几个 mini-batch 再更新一次梯度,等效于增大 batch size。比如你想等效 batch size 4,单卡 batch size 2,那就累积 2 步,只是在改main.py时要注意把 loss 除以累积步数,否则梯度会偏大。

第二个办法是换更轻的骨干网络。官方 DETR 用 ResNet-50,如果你显存很紧张,可以换更小的 ResNet-18,或者直接提高下采样倍率小一点的输入缩放。不过要注意,骨干网络变弱之后,原来能学到的全局特征会打折扣,精度会受影响。

第三个办法是检查是不是开了--masks这类附加模块。如果只是纯目标检测,不要加 mask 分支,Mask Head 会额外吃一大块显存,跟你的任务可能毫无关系。

收敛太慢的问题,通常和两个因素有关。一是学习率设置过小,很多人不敢用 1e-4,总喜欢从 1e-5 开始,结果训练几十个 epoch 几乎没变化。如果是从预训练权重继续微调,初始阶段用 1e-4 完全没问题。二是没有使用预训练权重,从零训练 DETR 在小数据集上会非常痛苦,官方都需要 300 个 epoch,个人项目更不可能几十个 epoch 就收敛。

5. 常见问题与排查技巧实录

5.1 类别索引和维度不匹配的报错

如果训练时出现类似IndexError: Target 5 is out of bounds的报错,几乎可以肯定是类别 id 越界。比如你设置了--num_classes 4,但标注 JSON 里出现了category_id 5,模型输出的类别概率只有 4 个有效维度,5 自然就越界了。

解决方式很简单:统一把类别 id 映射成 1 到类别数之间的连续整数。不建议直接用原始标注里的字符串或大 id,即使训练不报错,评估和部署时也会埋雷。另外要注意,如果训练集和验证集用两份不同的 JSON 生成,类别映射必须完全一致,否则会出现“训练正常、评估 AP 为 0”的诡异现象。

5.2 训练 loss 不降,或降到一定数值后不动了

DETR 训练前期 total loss 下降应该是比较明显的,如果前 10 个 epoch 完全没有下降趋势,大概率不是网络问题,而是数据或参数问题。

先检查输入图片路径和标注是否对得上,尤其是 Dataloader 是否真的读到了图片。你可以打印一个 batch 的targets,确认里面boxes数字都在合理像素范围内。再检查是否把 bbox 坐标从 VOC 的[xmin, ymin, xmax, ymax]转成了 COCO 的[x, y, w, h],如果没转,损失函数会拿一个超大“框”去算 GIoU,损失必然异常。

还有一种常见情况是 loss 一直在降但降得很慢。DETR 本身收敛慢,这种时候不要轻易调大学习率,因为学习率过大会导致后期 GIoU 损失反复震荡。建议先把训练轮数加到 200 到 300,同时确保用了预训练权重,再用余弦降学习率或分段降学习率配合。

5.3 训练正常但漏检严重,可能是 num_queries 不匹配

如果你的模型在验证集上总是漏掉一些目标,而训练 loss 又很正常,第一个怀疑对象就是num_queries设置太小。DETR 每个查询向量最多只能预测一个目标,如果一张图里目标数量超过查询数量,必然有目标会被漏掉。特别是工业场景下的密集小部件、生产线上的多缺陷图像,一张图出现几十上百个目标都不奇怪。

你可以先统计数据集中每张图片标注框的最大数量,然后把这个值作为num_queries的下限。比如最大单图标注数是 35,那num_queries 50就很稳妥。如果设得比最大数量还大,也没有太多坏处,只是计算量稍有增加。相反,如果太小,就会漏检。

还有一个细节是,如果很多查询向量都在输出低置信度的“无目标”,说明查询数量相对太大,这会略微降低训练效率,但不会造成漏检。漏检时优先增加num_queries,不要盲目降低置信度阈值,因为降阈值往往会带来误检率的提升。

5.4 部署时的置信度阈值和后处理问题

DETR 推理时不需要 NMS,但需要设置置信度阈值。常见的做法是,对所有查询输出的类别概率做 softmax,取最大类别的概率作为置信度,低于设定阈值的预测全部丢弃。阈值一般从 0.5 开始调,如果漏检多就降到 0.3,如果误检多就升到 0.7。

还有一点容易忽视:DETR 的每个查询输出里包含一个“无目标”类,因此最大概率落在“无目标”类上的查询,即使概率高达 0.9,也不能当作一个有效检测。正确做法是只考虑实际类别对应的概率。我在项目里就见过有人直接在 argmax 里把“无目标”当成第一类,结果推理出来的框全是背景乱框。

如果你最终要把模型部署到服务端或边缘设备,建议自己写一个简单的后处理:解析输出 logits,先去掉“无目标”维度,再取剩余类别的最大值和索引,再根据置信度阈值过滤,最后把框坐标从模型输入尺寸映射回原图尺寸。这一套流程不复杂,但每一步都要和训练时保持一致,否则换到部署环境很容易出现“训练时好好的,推理时全是错框”的情况。

我个人在实际操作中的体会是,DETR 跑自己的数据集,最大的门槛其实不是模型本身,而是你是否愿意把细节抠清楚。数据格式、类别映射、查询数量、学习率衰减轮数,这四个点只要有一个没对齐,训练就很容易处于“能跑,但效果差”的状态。后来再做类似项目时,我会先把数据统计这一步前置,统计好每张图最大目标数、目标宽高分布、类别频次,再决定num_queries和输入分辨率,这套流程跑起来就顺多了。如果你也卡在 DETR 训练自己的数据集上,建议按这个顺序重新检查一遍,会比盲目调参有效得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询