先聊个实在的:做目标检测的这些年,从Faster R-CNN到YOLO系列,大家早就习惯了一件事——想拿到一个像样的检测结果,先得准备好一大堆后处理零件:锚框设定、NMS阈值调参、正负样本匹配策略。每个环节单独看不复杂,但串在一起,整个pipeline就变得很脆。而DETR(DEtection TRansformer)这个工作,算是我见过最干脆的一次“掀桌子”:它把目标检测直接定义成一个集合预测问题,用Transformer架构端到端出结果,把锚框、NMS这些东西全删了。这篇文章我会结合我自己训练DETR、调模型、推数据集的经历,把它的核心原理、完整实操链路和几个容易踩的坑,一次说清楚。不管你是刚接触Transformer目标检测的新手,还是想把手头数据迁到DETR上的老兵,这篇都值得花十分钟看完。
1. DETR提出的背景:它到底解决了我做目标检测时的什么痛处
1.1 NMS和后处理那段让人头疼的日子
先回忆一下传统检测器的标准流程。以Faster R-CNN为例,训练时要做锚框分配、RPN提案、ROI Pooling,推理完还得做NMS(非极大值抑制)。NMS这个操作看似简单,实际很“玄学”:阈值调高了,重叠目标的框会被误删;调低了,同一目标出好几个框。再加上锚框的超参数(大小、宽高比)直接继承自数据集经验,换一个应用场景,往往要重新调一轮。
我当时做一个工业场景的零件检测项目,目标是小尺寸、高密度的螺丝和垫片。用Faster R-CNN那套,光是调试锚框尺寸就花了一周。后来换用YOLOv5,虽然省心了点,但针对小目标的NMS处理仍然不省心。我总在想:能不能有一种模型,直接让网络自己去理解“这张图里有哪些目标、各自在什么位置”,不用我手动设计那么多中间规则?
1.2 把检测当作直接集合预测的关键思路
DETR给出的答案很直接:把检测当作集合预测来做。所谓集合预测,就是模型输出一个固定大小的预测集合,比如100个预测框,然后和图像中真实的若干个目标框做匹配。匹配上了就算检出,没匹配上的就是背景。整个过程没有锚框,没有NMS,也不需要任何手工设计的后处理。
这个思路之所以在当时让人觉得眼前一亮,在于它把检测的pipeline真正变成了“端到端”。你想一下,端到端意味着什么?意味着从图像像素到最终预测结果之间,所有步骤都是可微的,误差可以直接从损失函数反向传播到模型参数。而传统检测器中的NMS是不可微的,它只存在于推理阶段,训练时还要模拟它的行为去设计损失。
DETR的另一个关键点是,它把目标检测看成了一个“序列生成”问题。图像经过CNN骨干网络提取特征,展平成一串特征序列,然后交给Transformer的编码器-解码器结构去处理。这跟机器翻译里把一个句子翻译成另一个句子的逻辑很像。解码器一侧输入的不是图像特征,而是一组可学习的“目标查询”(object queries),它们像“空槽位”一样,在解码过程中逐渐定位到具体目标上。
你可能要问了:为什么Transformer在这件事上能行?因为Transformer的核心机制是自注意力(self-attention),它能让每个位置的特征都和其他所有位置的特征交互,天然具备全局建模能力。传统卷积要扩大感受野,得堆层数、加深网络;Transformer一上来就把整个图像的特征都放在一个上下文里处理,目标之间的全局关系、目标与背景的关系,都能直接被建模到。
2. 核心架构逐层拆解:从图像输入到目标查询输出
2.1 CNN骨干网络与位置编码的配合
DETR的输入处理分两步。第一步,图像先喂给一个CNN骨干网络(论文里用的是ResNet-50或ResNet-101,也可换成ResNet-50的变体),得到下采样32倍的特征图。假设输入是800x800,输出的特征图就是25x25,通道数2048。这个特征图其实已经浓缩了图像的语义信息。第二步,用1x1卷积把通道数压缩到256左右,得到一个形状为25x25x256的紧凑特征图。
但这里有个问题:Transformer本身是序列模型,它不知道特征图里每个位置对应图像上的哪个空间位置。所以需要给每个特征位置加上位置编码(positional encoding)。DETR用的位置编码是正弦余弦函数生成的绝对位置编码,二维空间上分别在x和y方向计算。你可以把它理解成给每个特征网格发了一张“身份证”,让Transformer在计算注意力时知道哪个特征来自图的左上方、哪个来自右下方。
这一步细节容易被忽略,但特别重要。如果没有位置编码,Transformer做自注意力时,所有位置的特征初始状态没有空间区分度,模型很难学会“相邻区域倾向于属于同一个目标”这种空间先验。实测把位置编码去掉,训练损失下降明显变慢。
2.2 Transformer编码器:全局上下文建模的优势
编码器部分基本就是标准的Transformer Encoder Block,论文默认6层。每一层包含一个多头自注意力子层和一个前馈网络子层,层归一化采用Post-LN(先加残差再归一化)的方式。输入的序列长度是HxW,对25x25的特征图来说就是625个token。
自注意力在这里的作用,是让每个位置的特征感知到整张图的上下文。举个例子,一张图上有一辆车和一个行人,传统卷积要经过多层堆叠才能让车的位置特征“看到”行人的位置特征;而自注意力一步到位,任意两个位置之间都建立连接。这种全局交互对消除重复检测非常有帮助,因为模型能学到“左上角已经检测到一辆车了,那右下角跟它长得很像的区域很可能不是另一辆车”。
不过全局注意力也带来一个现实问题:计算复杂度是序列长度的平方。如果输入分辨率太大,编码器的显存开销会非常可观。这一点我在后面讲训练实操时会展开。
2.3 解码器与object queries到底是怎么发挥作用的
解码器也默认6层,和编码器不同的是,每层解码器内部有三个子层:自注意力、交叉注意力、前馈网络。交叉注意力负责在解码器输出和编码器输出的图像特征之间建立联系。
核心的机制在于object queries。这是一个形状为(100, 256)的可学习参数矩阵,相当于100个“目标查询向量”。它们在初始状态下没有任何语义,但随着训练进行,每个query会逐渐“锁定”到图像中特定位置、特定尺寸、特定类别的目标上。你可以把这些queries理解为100个“小侦探”,每个侦探负责去图像里找一类目标,有的专找小目标,有的专找大目标,有的偏向图像中心区域,有的偏向边缘。
这100个queries经过解码器的交叉注意力机制,不断“查询”图像特征,最终在最后一层解码器输出后,接一个FFN(前馈网络)预测每个query对应的目标类别和边界框位置。类别预测用一个线性层加softmax,得到100个概率分布;边界框预测用另一个线性层输出4个数值(中心点x、中心点y、宽w、高h),并且用sigmoid函数归一化到0到1之间。
有一点值得注意:DETR的解码器在每一层都引入了辅助损失。也就是说,中间层解码器的输出也会被拿来计算损失,然后和最终层的损失加在一起。这个设计是为了缓解Transformer训练不稳定的问题,让梯度能更直接地传到前面的层。
2.4 预测头与二分图匹配的损失设计
如果说上面的结构还算常规,那DETR的损失设计就属于“点睛之笔”。因为模型输出100个预测框,而图像里可能只有几个真实目标框,那么怎么计算损失?DETR用的是匈牙利匹配算法(Hungarian Algorithm),在100个预测框和真实目标框之间找到一种最优一对一匹配关系,使得总匹配代价最小。
匹配代价由三部分组成:类别预测的负对数概率、边界框的L1距离、以及GIoU损失。简单来说,匈牙利匹配会这样分配:每个真实目标框找到与自己最匹配且类别预测正确的预测框,剩下的预测框视为背景。匹配完成后,损失函数只针对匹配上的预测框计算,没匹配上的预测框用“无对象”(no object)这个类别来监督。
这套设计的巧妙之处在于,它用一次匹配同时解决了目标分配和去重两个问题。一对一的强约束意味着,一个真实目标最多只能匹配一个预测框,这天然抑制了重复检测。也正因如此,推理阶段完全不需要NMS。我第一次跑通DETR,看到最终输出直接就是干净利落的检测框,确实有种“终于摆脱麻烦后处理”的轻松感。
3. 让DETR跑起来:一次训练和推理的完整实操记录
3.1 环境准备与依赖版本选择
DETR的官方实现托管在GitHub(facebookresearch/detr)上,基于PyTorch。直接克隆仓库,装好torch、torchvision和pycocotools就能跑。我的建议是PyTorch版本用1.7以上,太低的版本在autograd机制上对Transformer的支持没那么好。CUDA最好是11.x,训练DETR时显存占用不低,别省这点。
一个很多人会忽略的点:DETR的repo里指定了一个submodule(需要拉取cocoapi的依赖)。如果你直接git clone而不加--recurse-submodules,后面跑评估时会遇到pycocotools导入失败的问题。我自己踩过这个坑,最后的解决办法是手动把cocoapi/PythonAPI下面的pycocotools目录复制到环境site-packages里,或者用pip安装pycocotools。
3.2 数据格式与加载:用COCO格式训练自己的数据
DETR默认使用COCO数据集格式,也就是一张图像对应一个JSON标注文件。标注格式长这样:
{ "images": [ {"id": 1, "file_name": "000001.jpg", "width": 640, "height": 480} ], "annotations": [ {"id": 1, "image_id": 1, "category_id": 1, "bbox": [100, 150, 200, 300], "area": 60000, "iscrowd": 0} ], "categories": [ {"id": 1, "name": "screw"} ] }这里的bbox格式是[x, y, width, height],注意不是YOLO的归一化中心点格式,也不是VOC的[x_min, y_min, x_max, y_max]格式。我在第一次做数据转换时,就因为在bbox的格式上搞混,导致训练出的模型检测框全部偏移,后面排查了很久才意识到是标注格式理解有误。
如果要用自己的数据,强烈建议先写一个检查脚本,随机抽几张图,把标注框画在原图上并保存出来,确认标注框位置正确后再开始训练。这一步虽然笨,但能省掉你后续调试模型时的大量脑细胞。
3.3 训练参数、显存占用和收敛观察
官方默认的DETR训练配置是:batch size 2,epochs 300,初始学习率1e-4,在第200个epoch时降到1e-5。这么长的训练周期肯定劝退不少人。但实际上,如果你的数据集不复杂,类别不多,完全不需要跑到300个epoch。我用一个只有5个类别的工业零件数据集,200个epoch左右就已经能看到收敛趋势了。
显存占用方面,输入尺寸默认是800左右,batch size为2时,一张RTX 3090(24GB显存)能跑,但比较吃紧。如果你只有12GB显存,建议把batch size降为1,同时把输入图片最长边缩放到600。Transformer的注意力复杂度是平方级别的,降低分辨率是控制显存最有效的手段。
训练时的损失曲线,建议同时关注三个指标:总损失、分类损失和边界框损失。DETR收敛有个特点,前期分类损失下降很慢,看起来像没在学,实际上边界框损失已经在悄悄下降了。这是正常现象,因为匈牙利匹配算法前期还在做“试配”,匹配关系稳定后损失下降才会明显加速。
我自己的经验是,如果看到分类损失一直卡在某个高位不动,优先检查是不是类别数量设置错了(比如背景类算进去了没)、或者目标数量太少导致大量query被分配到“无对象”类别。理论上,一张图里只有1个目标,那100个query里头99个都该学成背景,如果模型连这个都没学会,多半是数据或匹配逻辑有问题。
3.4 推理可视化与结果解读
官方推理脚本支持两种模式:图片文件夹推理和单图可视化。我自己习惯写一个更简单的推理脚本,只提取模型输出,再手动画框。下面这段代码是核心部分:
import torch import torchvision.transforms as T from PIL import Image from models import build_model # 加载模型 model, criterion, postprocessors = build_model(args) checkpoint = torch.load("checkpoint.pth", map_location="cpu") model.load_state_dict(checkpoint["model"]) model.eval() # 图像预处理 transform = T.Compose([ T.Resize(800), T.ToTensor(), T.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) img = Image.open("test.jpg").convert("RGB") img_tensor = transform(img).unsqueeze(0) with torch.no_grad(): outputs = model(img_tensor) probas = outputs["pred_logits"].softmax(-1)[0, :, :-1] bboxes = outputs["pred_boxes"][0] # 过滤低置信度预测 keep = probas.max(-1).values > 0.7 boxes = bboxes[keep] labels = probas.argmax(-1)[keep] scores = probas.max(-1).values[keep]这里有个细节:outputs["pred_logits"]的形状是(batch, num_queries, num_classes+1),因为有一个额外的“无对象”类别,所以在做softmax之后我用[:, :, :-1]把最后一类去掉。然后通过置信度阈值过滤掉背景预测,剩下的就是最终检测结果。
推理阶段比较快,一张800x800图片在我的3090上约20毫秒。注意,推理时不需要任何NMS步骤,原封不动地取模型输出就行。
4. 我用DETR时踩过的坑:收敛慢、小目标漏检和其他边界问题
4.1 DETR为什么收敛慢,以及我用的缓解手段
DETR被诟病最多的就是收敛速度慢。原论文在COCO上需要500个epoch才能达到较好的性能,这对实际项目来说确实是个负担。根本原因在于,匈牙利匹配是一种“软”匹配,训练早期的匹配结果不稳定,同一个query可能在一次迭代中被分配到目标A,下一次又被分配到目标B,导致梯度方向不一致,训练效率自然低下。
我实际使用的几个缓解手段,按效果排序如下:
- 使用Deformable DETR的变体(后面会讲到),或者用DETR的改进版DINO,收敛速度和最终精度都明显优于原版。
- 引入更强的特征提取骨干,比如把ResNet-50换成ResNet-101或Swin Transformer的骨干,可以在同样epoch数下获得更好的特征表示。
- 适当降低batch size并提高训练轮数。虽然batch size 2会让梯度噪声更大,但配合lr scheduler的warmup阶段,有时反而能逃离初始的不良匹配局域。
4.2 小目标检测偏弱的根因和可选方案
小目标检测是DETR的一个明显短板。原因也很直观:小目标在特征图上仅占很少的像素点,经过下采样32倍之后,可能只对应到1到2个特征格点,在全局自注意力中容易被其他大目标的特征淹没。此外,小目标的边界框L1损失在总损失中占比很小,匈牙利匹配时不容易被优先匹配到。
这个问题我试过几种解法:
- 多尺度特征融合:把骨干网络不同stage的特征都取出来,拼接后送入编码器。Deformable DETR就是这么干的,效果立竿见影,小目标的召回率提升明显。
- 使用更高分辨率输入:好处直接,坏处是显存翻倍。
- 调整损失权重:把边界框损失中GIoU损失的权重加大,让匹配时更青睐空间位置准确的候选框。
4.3 训练不稳定与随机种子、辅助损失的关系
DETR对随机种子比较敏感,这也是Transformer类模型的通病。同样的数据、同样的超参数,用不同种子跑两次,最终AP可能差1到2个点。我在论文复现时也发现了这一点。建议训练前固定好所有随机种子,包括Python的random、NumPy的np.random和PyTorch的torch.manual_seed,并在代码开头设置torch.backends.cudnn.deterministic = True和torch.backends.cudnn.benchmark = False,保证可复现性。
辅助损失(auxiliary loss)是另一个稳定训练的关键。打开官方配置中的aux_loss=True后,每一层解码器都会产生预测并参与损失计算。我发现关闭辅助损失时,模型在训练前期的分类分支非常容易陷入局部最小,表现为预测全部偏向“背景”类。开启辅助损失后,这个问题得到显著改善,建议默认保持开启。
5. DETR之后:从原版到Deformable DETR和DINO的进化
5.1 Deformable DETR如何加速收敛并改善小目标
Deformable DETR的核心改动是在交叉注意力模块中引入可变形注意力机制。通俗讲,每个query不再需要对整张特征图计算注意力权重,而是通过学习一组稀疏的采样点(默认每个query采样4个点),只在这些位置采集特征。这样既降低了计算复杂度,也让模型聚焦在少数关键位置。
更实际的好处是,Deformable DETR将收敛epoch数从500降到了50左右,几乎一个数量级的提升。它同时引入了多尺度特征输入,把小目标漏检问题一并改善。如果是新项目,我基本不建议直接用原版DETR,Deformable DETR是更稳妥的起点。
5.2 DINO等进阶变体对我们有什么启发
DINO(DETR with Improved deNoising anchOr boxes)是DETR系更后期的一个代表,思路更复杂一些:引入了对比去噪训练、锚框初始化等机制。简单说,它在训练时人为给真实目标框添加一些噪声,让模型学会“去掉噪声、还原目标”,这种去噪预训练任务能极大加速收敛。
从实际应用角度,如果你要在自己的数据集上很快见效,直接找DINO的预训练权重做迁移学习,通常比我前面说的从零训练原版DETR效果更好。我在一个自采集的安防数据集上做过对比:用原版DETR从零训150个epoch的AP约38,用DINO加载COCO预训练权重再微调30个epoch就达到了46,差距非常明显。
不过,DINO的训练配置和代码复杂度比原版DETR高不少,调试成本也上去了。对入门学习来说,原版DETR仍然是最值得先吃透的起点,毕竟理解了基础结构,后面的变体不过是“在哪些环节做了哪些改进”的问题。
最后分享一个按我个人经验可能对你有用的细节
训练DETR系列模型,千万不要上来就放弃默认配置。官方repo的800x800输入、batch size 2、300epoch这些数字,都是针对COCO这种大而复杂的数据集调出来的,对你自己的小数据集未必合适。我的建议是:第一周先用小分辨率、小epoch(比如输入600、训练50个epoch)跑通整体流程,确认数据格式和损失曲线正常,再逐步放大输入分辨率和训练轮数。如果一上来就追求完整配置,很容易在长时间训练后发现某个基础环节出了错,白白浪费大量算力。
还有一个实用技巧:在训练过程中定期保存checkpoint时,不要把整个模型状态存下来,而是只存model.state_dict()和optimizer.state_dict()。DETR模型本身不算特别大,但优化器状态往往占更多空间,长期训练下来能省不少磁盘。而且推理时只需要model.state_dict(),加载更轻量。
DETR的设计哲学我打心底里喜欢:它让我看到了目标检测从“手工规则”走向“数据驱动端到端”的清晰方向。虽然它直接落地到复杂业务场景时,效率和精度还需要依赖各种改进版本来支撑,但它的思想价值远大于那几张可以被新模型刷新的指标表。如果你正准备入门Transformer检测模型,从DETR开始,把每一层结构、每一个损失函数吃透,再去看Deformable DETR、DINO、DN-DETR这些后继者,会顺畅得多。