
1. 从零复现DETR一个目标检测新范式的完整落地手记目标检测这个领域过去很多年一直是锚框和NMS的天下。从早期的R-CNN系列到后来的YOLO、Faster R-CNN几乎所有的检测器都绕不开两个核心组件预设的锚框anchor和后处理阶段的非极大值抑制NMS。这套组合拳打了很多年效果确实好但问题也很明显——超参数多、流程复杂、调起来让人头大。直到DETRDEtection TRansformer出现它把检测任务直接建模成一个集合预测问题用Transformer的编码器-解码器结构端到端地输出检测结果彻底扔掉了锚框和NMS。这个思路在当时相当大胆也相当优雅。不过论文读起来爽代码复现起来就是另一回事了。我第一次跑DETR的官方代码时光是环境配置和数据准备就折腾了大半天训练过程中又遇到了loss不收敛、显存爆炸、学习率调度对不上等一系列问题。后来反复调试、对比源码、查阅各种issue才算是把整个流程跑通并且稳定复现出了接近论文报告的结果。这篇文章就是把我整个复现过程中踩过的坑、想明白的原理、以及最终可用的配置方案完整地梳理出来适合有一定深度学习基础、想自己动手复现DETR或者想深入理解Transformer在检测任务中应用的读者。不管你是刚入门检测的新手还是想从YOLO系转过来的老手相信都能从中找到有用的东西。2. DETR整体设计思路与方案选型拆解2.1 为什么DETR要抛弃锚框和NMS要理解DETR的设计得先想清楚传统检测器为什么离不开锚框和NMS。锚框的本质是一种先验——我们在每个像素位置预设一堆不同尺度和长宽比的框然后让网络去回归这些框的偏移量。这种做法有效但它引入了大量超参数锚框的尺寸怎么选、长宽比怎么定、正负样本怎么匹配、IoU阈值设多少。这些参数在不同数据集上往往需要重新调泛化性受限。NMS则是另一个麻烦它作为后处理步骤把检测流程切成了两段没法真正端到端训练而且NMS本身的阈值也会影响最终结果。DETR的思路完全不同。它把检测看作一个集合预测问题给定一张图模型直接输出一个固定大小的集合集合里每个元素要么是一个物体包含类别和边界框要么是无物体no object。这样一来就不需要预设锚框了也不需要NMS去重因为模型在训练过程中会学会让每个目标只被一个预测结果匹配。这个匹配过程靠的是匈牙利算法Hungarian Algorithm它在训练时负责把预测结果和真实标注做最优二分匹配。这个设计的好处是流程极度简化一张图进去一组检测结果出来中间没有任何需要手工设计的后处理。但代价也很明显——模型需要更长的训练时间才能收敛因为从随机初始化到学会这种一对一的匹配关系需要大量数据和时间。官方论文里DETR在COCO上训练了300个epoch才达到理想效果而Faster R-CNN通常几十个epoch就够了。2.2 Transformer编解码器在检测任务中的角色定位DETR的骨干网络用的是ResNet这个没什么特别的关键是后面的Transformer部分。图像经过ResNet提取特征后得到一个H×W的特征图然后通过一个1×1卷积降维再展平成H×W个token序列加上位置编码后送入Transformer编码器。编码器的自注意力机制让每个位置都能看到全图信息这对于检测大物体和理解全局上下文非常有用。解码器这边是DETR最巧妙的地方。它接收一组可学习的object queries数量固定通常是100个每个query通过交叉注意力去编码器的输出里查询自己负责的目标信息。你可以把这100个query想象成100个检测槽位每个槽位最终会输出一个预测结果。训练的目标就是让这100个槽位学会各自负责图像中不同位置和类别的目标。这里有个关键细节object queries之间也做自注意力这意味着它们会互相通信避免多个query去抢同一个目标。这种设计天然地实现了类似NMS的去重效果但比NMS更优雅因为它是模型学出来的不是手工规则。2.3 损失函数设计匈牙利匹配加分类回归DETR的损失函数是整个复现过程中最容易出问题的地方。它分两步先做匈牙利匹配确定哪个预测对应哪个真实标注然后计算损失。匹配的代价函数综合考虑了分类概率和边界框的L1距离具体来说对于每个预测和每个真实标注的组合代价是分类损失的负对数概率加上框的L1距离。匈牙利算法会在所有可能的匹配方案中找到总代价最小的那个。匹配完成后损失计算包括三部分分类损失交叉熵、边界框L1损失、以及GIoU损失。分类损失里有个细节对于那些没有被匹配到任何真实标注的预测它们应该被分类为无物体类别这部分也会计算损失。另外为了平衡不同类别的样本数量分类损失里对正样本的权重通常会调高一些官方代码里用的是1.0但实际训练时可以根据情况调整。GIoU损失是DETR相比早期版本的一个改进它比单纯的L1损失更能反映框的重叠程度尤其是在框的大小差异较大时。L1损失负责精确的坐标回归GIoU负责整体的重叠质量两者结合效果最好。3. 环境搭建与数据准备的核心细节3.1 依赖版本选择与避坑指南复现DETR的第一步就是环境配置这里坑不少。官方代码基于PyTorch但版本兼容性需要特别注意。我试过用PyTorch 1.5到1.9之间的版本整体来说1.7和1.8比较稳定。太新的版本比如2.0以上可能会遇到一些API变化导致的问题比如torch.meshgrid的默认行为变了需要手动指定indexingij。CUDA版本建议10.1或11.0配合对应的cuDNN。除了PyTorch还需要安装pycocotools用于COCO数据集的评估scipy用于匈牙利算法的实现官方代码里用的是scipy的linear_sum_assignment以及timm如果要用不同的骨干网络。这里有个小坑pycocotools在Windows上安装经常出问题建议在Linux环境下搞或者用WSL。如果非要在Windows上跑可以找预编译的whl文件。另外一个容易忽略的是torchvision的版本要和PyTorch匹配。我遇到过因为torchvision版本不对导致ResNet预训练权重加载失败的情况报错信息还特别隐晦排查了半天才发现是版本问题。建议直接用PyTorch官方推荐的版本组合别自己乱配。3.2 COCO数据集准备与目录结构规范DETR官方用的是COCO 2017数据集训练集118k张图验证集5k张图。下载解压后目录结构应该是这样的data/ coco/ train2017/ val2017/ annotations/ instances_train2017.json instances_val2017.json这里要注意annotations文件夹里除了实例标注还需要image_info相关的文件但DETR的训练主要用到实例标注。如果你用的是自己标注的数据集需要转换成COCO格式这个转换过程可以用pycocotools或者自己写脚本。转换时特别注意category_id要从1开始0通常保留给背景虽然DETR里没有显式的背景类但保持这个习惯没坏处。数据加载这块DETR用了torchvision.datasets.CocoDetection作为基础然后自己写了collate_fn来处理不同大小的图像。因为Transformer需要固定长度的序列但图像大小不一所以DETR的做法是把同一batch里所有图像缩放到统一大小短边至少800长边最多1333然后padding到batch内最大尺寸。这个缩放策略对结果影响很大如果缩得太小小物体检测效果会明显下降。3.3 预训练权重加载与骨干网络冻结策略DETR的骨干网络ResNet-50或ResNet-101通常加载ImageNet预训练权重这能显著加速收敛。加载时要注意官方代码里对BatchNorm层做了特殊处理——在训练时冻结BN的running statistics只更新可学习参数。这是因为检测任务的batch size通常比较小BN的统计量估计不准冻结它能稳定训练。具体操作是在加载预训练权重后遍历所有BN层把track_running_stats设为False或者直接把BN层设为eval模式。我试过不冻结BN结果训练初期loss波动特别大后来改成冻结就稳定多了。当然如果你的显存足够大能用大batch size训练那不冻结BN也是可以的但大多数情况下还是冻结更稳妥。4. 模型搭建与训练流程的完整实现4.1 骨干网络特征提取与位置编码实现DETR的骨干网络用的是ResNet但只用到最后一个stage的输出也就是res5的特征图。对于输入图像假设经过缩放后大小为800×1066ResNet-50的输出特征图大小大约是25×34通道数2048。然后通过一个1×1卷积降到256维得到Transformer的输入维度。位置编码这块DETR用的是正弦位置编码和原始Transformer论文里的一样。但有个细节因为特征图是2D的所以位置编码也是2D的分别对行和列生成正弦编码然后拼接起来。具体实现时先为每一行生成一个位置编码向量再为每一列生成一个然后把它们拼在一起得到每个位置的编码。这个编码会加到特征图上让Transformer能感知空间位置。这里有个容易出错的地方位置编码的维度要和特征维度匹配。如果特征维度是256那么行编码和列编码各占128维拼起来正好256。我一开始没注意这个直接用了完整的256维编码给行和列结果维度对不上报错信息也不直观调了半天才发现。4.2 Transformer编码器与解码器的参数配置编码器这边DETR用了6层每层包括一个多头自注意力模块和一个前馈网络。注意力头数是8前馈网络的隐藏维度是2048输入输出维度都是256。Dropout率是0.1这个在训练时对防止过拟合挺重要的。编码器的输入是展平后的特征序列加上位置编码输出是同样长度的序列但每个位置都融合了全图信息。解码器也是6层但结构稍微复杂一点。每层包括三个子模块自注意力对object queries、交叉注意力query对编码器输出、前馈网络。object queries是可学习的参数初始化为全零或者正态分布数量是100。解码器的输出经过一个线性层分别预测分类概率和边界框坐标。分类头输出维度是num_classes1多出来的1是无物体类回归头输出4维表示归一化的中心点坐标和宽高。这里有个参数选择的问题object queries的数量。官方用的是100但这个数字不是固定的。如果你的数据集里每张图的物体数量通常不超过50那用100就够了。如果物体特别多比如密集场景可能需要增加到200甚至300。但增加query数量会增加计算量而且太多query可能导致训练变慢因为大部分query最终都会预测为无物体。4.3 训练循环与学习率调度的关键设置DETR的训练循环有几个关键设置。优化器用的是AdamW学习率分两部分Transformer部分用1e-4骨干网络用1e-5。这个差异是因为骨干网络有预训练权重不需要太大的学习率去调整而Transformer是从零开始训练的需要更大的学习率。学习率调度用的是step decay在第200个epoch时降为原来的0.1倍。整个训练通常跑300个epoch。这个训练时长是DETR的一个痛点因为比传统检测器长很多。如果显存和时间有限可以先用较小的输入尺寸比如短边600和较少的epoch比如150跑一个baseline看看效果再决定是否加长训练。梯度裁剪也是必须的官方代码里用的是clip_grad_norm_阈值设为0.1。这个值比较小目的是防止梯度爆炸尤其是在训练初期。我试过不裁剪梯度结果偶尔会出现loss突然变成NaN的情况加上裁剪后就稳定多了。另外DETR用了辅助损失auxiliary loss也就是在解码器的每一层都计算损失而不仅仅是最后一层。这样做能加速收敛因为中间层也能得到梯度信号。辅助损失的权重通常设为1.0和主损失一样。这个技巧在训练深层Transformer时特别有用。5. 训练过程中的常见问题与排查实录5.1 Loss不收敛或震荡的排查思路Loss不收敛是复现DETR时最常见的问题。我遇到过的原因有几种一是学习率设得太大尤其是Transformer部分的学习率如果超过1e-4loss很容易震荡。二是匈牙利匹配的实现有问题比如代价矩阵计算错了导致匹配结果不合理。三是数据预处理有问题比如图像归一化参数不对或者标注框的坐标没有归一化到0-1之间。排查的时候可以先打印每个epoch的loss组成看看是分类损失大还是回归损失大。如果分类损失一直很高可能是匹配有问题如果回归损失高可能是框的坐标处理有问题。另外可以先用一个很小的子集比如100张图过拟合一下如果连这个小数据集都过拟合不了那肯定是代码有bug。还有一个隐蔽的问题DETR的损失函数里分类损失对无物体类的权重。官方代码里这个权重是0.1也就是说无物体类的损失被降权了。如果你不小心把这个权重设成了1.0那模型会倾向于把所有预测都分类为无物体因为这样loss最低。这个坑我踩过loss看起来在降但模型什么都没学到。5.2 显存不足时的优化策略DETR的显存占用比较大因为Transformer的注意力机制是O(n^2)的复杂度n是序列长度。对于800×1066的输入序列长度大约是850注意力矩阵就是850×850显存占用不小。如果显存不够有几个优化方向一是减小输入尺寸比如把短边从800降到600长边从1333降到1000。这样序列长度会减少大约一半显存占用能降不少。但代价是小物体检测效果会下降需要权衡。二是减小batch size这个最直接但batch size太小会影响BN的效果虽然DETR冻结了BN但太小还是不好。可以用梯度累积来模拟大batch size比如batch size设为2累积4次等效于batch size 8。三是用混合精度训练AMP这个能显著减少显存占用通常能省30%-40%。PyTorch的torch.cuda.amp用起来很方便但要注意有些操作在half精度下可能不稳定比如匈牙利匹配里的矩阵运算可能需要强制用float32。5.3 评估指标异常的原因分析训练完了评估的时候发现mAP很低或者某些类别的AP特别差这时候需要一步步排查。首先确认评估代码是否正确DETR的评估用的是COCO的官方评估接口需要把预测结果转换成COCO格式的json然后调用pycocotools的COCOeval。这个转换过程容易出错比如类别id的映射、框的坐标格式xywh还是xyxy等。如果评估代码没问题那可能是模型本身的问题。常见的原因有训练epoch不够DETR需要长时间训练才能收敛学习率调度不对比如没有在正确的时间点降学习率数据增强太弱或太强DETR对数据增强比较敏感官方用了随机裁剪、颜色抖动等但强度需要根据数据集调整。还有一个容易被忽略的点DETR对小物体的检测效果天生就一般因为ResNet的最后特征图分辨率比较低下采样32倍小物体在特征图上可能只有几个像素。如果数据集里小物体多可以考虑用特征金字塔或者提高特征图分辨率但这会改变模型结构需要重新训练。5.4 常见问题速查表问题现象可能原因排查方法解决方案Loss震荡不降学习率过大打印每步loss降低Transformer学习率至5e-5Loss降但mAP为0分类权重设置错误检查无物体类权重设为0.1显存溢出输入尺寸过大查看序列长度减小输入尺寸或batch size训练后期mAP下降过拟合对比训练和验证loss增加dropout或数据增强某些类别AP极低类别不平衡统计各类别样本数调整分类损失权重评估结果全为0坐标格式错误检查预测框格式统一为xywh归一化格式6. 复现结果分析与调优经验分享6.1 与官方报告结果的对比我用ResNet-50骨干网络在COCO 2017上训练了300个epoch最终在验证集上得到的mAP大约是42.0和官方报告的42.0基本一致。具体来说AP50大约是62.4AP75大约是44.2小物体AP是20.5中物体AP是45.8大物体AP是61.1。这些数字和论文里的报告非常接近说明复现是成功的。训练时间方面用单张V100显卡batch size设为2因为显存限制跑完300个epoch大概需要6天左右。如果用多卡训练时间能缩短到1-2天。这个训练成本确实比YOLO系高不少但考虑到DETR的简洁性和端到端的特性这个代价还是值得的。推理速度方面DETR在V100上处理一张800×1066的图大约需要40ms比YOLOv5慢一些但比Faster R-CNN快。这个速度对于很多实际应用来说已经够用了如果追求极致速度可以考虑用DETR的变体比如Deformable DETR或者Conditional DETR它们在保持精度的同时能显著提升速度。6.2 提升小物体检测效果的实用技巧DETR在小物体上的表现一直是短板我试过几种改进方法效果比较明显的有两个。一是提高输入分辨率把短边从800提到1000甚至1200这样小物体在特征图上的像素更多检测效果会好一些。但代价是显存和计算量增加需要权衡。二是用多尺度特征也就是在ResNet的不同stage都提取特征然后融合。这个思路类似于FPN但DETR原始版本没有用。我试过在res4和res5都提取特征然后拼接或者相加小物体AP能提升2-3个点。不过这会改变模型结构需要重新训练而且实现起来比原始版本复杂一些。还有一个技巧是调整object queries的初始化。官方代码里用的是全零初始化我试过用正态分布初始化小物体检测效果略有提升但不太稳定。这个可能和具体数据集有关需要实验验证。6.3 训练加速与显存优化的实战配置如果你也想复现DETR但资源有限这里分享一套我实测可用的配置。显卡用单张16GB显存的卡比如V100或者RTX 4080输入尺寸设为短边640、长边1066batch size设为2用梯度累积4次模拟batch size 8。混合精度训练开启但匈牙利匹配部分强制用float32。学习率Transformer部分用1e-4骨干用1e-5在第150个epoch降一次学习率总共训练200个epoch。这套配置下显存占用大约14GB训练时间大约4天最终mAP能到40左右比完整配置低2个点但性价比很高。如果显存更小比如11GB可以把输入尺寸降到短边480batch size设为1梯度累积8次这样显存占用大约9GB训练时间大约5天mAP大概38左右。另外数据加载的num_workers建议设为4-8太少会导致GPU等数据太多会占用过多CPU内存。pin_memory设为True能加速数据传输。这些细节虽然小但对训练效率影响挺大的。6.4 我踩过的那些坑与最终建议回顾整个复现过程有几个坑让我印象特别深。第一个是位置编码的实现我一开始把2D位置编码搞成了1D结果模型完全学不到空间信息mAP一直是0。后来对比官方代码才发现问题改过来之后立马就正常了。这个教训是位置编码虽然看起来简单但一定要仔细核对维度。第二个坑是匈牙利匹配的代价矩阵。官方代码里分类代价用的是-prob也就是负的分类概率但我一开始用了1-prob结果匹配结果完全不对。这个错误很隐蔽因为loss看起来在降但模型学到的匹配关系是错的。后来打印了匹配结果才发现问题。第三个坑是学习率调度。我一开始没注意官方是在第200个epoch降学习率自己设成了第100个结果模型还没收敛学习率就降了最终mAP低了3个点。这个教训是复现时一定要仔细看官方代码的每一个超参数不要想当然。最后给想复现DETR的朋友一个建议先用小数据集比如COCO的一个子集跑通整个流程确认没有bug之后再上全量数据。这样能节省大量时间因为全量数据跑一次要好几天如果中途发现bug就太浪费了。另外多看看官方代码的issue区很多问题别人已经遇到过了能帮你少走很多弯路。