DETR训练自己的数据集:从COCO格式到参数调优全流程
我最早把 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, start1): 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, encodingutf-8) as f: json.dump(coco_data, f, ensure_asciiFalse, indent2) print(done, images:, len(images), annotations:, len(annotations)) if __name__ __main__: convert_voc_to_coco( xml_diryour_voc_xml_folder, img_diryour_image_folder, output_jsoninstances_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 权重文件路径。官方代码加载的时候用的是strictFalse所以类别数不一致导致最后一层缺失它不会报错只是会提示缺少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. 训练过程监控与模型选择别只盯着总 loss4.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和--resumepython main.py \ --dataset_file coco \ --coco_path ./data \ --num_classes 4 \ --num_queries 100 \ --resume output/defect_detr/checkpoint0099.pth \ --eval评估结果里会输出Average Precision (AP) [ IoU0.50:0.95 | areaall ]这一串其中主要看第一个 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 训练自己的数据集上建议按这个顺序重新检查一遍会比盲目调参有效得多。