
第一次把 DETR 模型跑通、看到输出里没有一行 NMS 代码的时候我盯着结果确认了好几遍——100 个候选框直接给出来分数线一卡就完事。这个细节对做目标检测的人来说是有点震撼的因为过去十年里检测器流水线上最让人烦躁的那几段手工活被一个纯 Transformer 结构一口气干掉了。这篇就按我自己的理解路径从它砍掉了什么讲到它为什么慢、Deformable DETR 又补了什么、怎么拿自己的数据训、部署时哪几个坑必踩一路说透。DETR 全称 DEtection TRansformer2020 年由研究团队提出的端到端目标检测方案核心思路是把检测重构成集合预测问题输入一张图输出固定数量的框每个框带类别训练时用二分图匹配把预测框和真值框一一对上。它不需要锚框、不需要 NMS、不需要手工设计的候选区域。适合谁看如果你已经会跑 YOLO 系列、懂基本的 CNN 检测流程想搞清楚 Transformer 是怎么进检测领域的这篇能给你一条完整的线如果你只是想找个开箱即用的检测器看完第 6 章再决定要不要上它。1. 从锚框加 NMS 的流水线到一次集合预测DETR 砍掉的到底是什么1.1 传统检测器里那几处绕不开的手工活先说清楚传统两阶段和单阶段检测器的共同结构才能理解 DETR 的价值。以典型的两阶段为例骨干网络提特征RPN 层在特征图上撒一堆锚框判断哪些框里可能有东西然后对候选框做回归精修最后对每个类别做一次非极大值抑制把重叠度高的框删到只剩一个。单阶段检测器省掉了 RPN但锚框和 NMS 一个都没少。这里面有几处手工活特别扎眼。锚框的尺寸、比例、数量是人工设计的超参数换一个数据集、换一类目标尺度分布就得重新调NMS 的阈值也是个玄学参数阈值低了密集场景的相邻目标会被误删阈值高了又会重复检出一堆框正负样本分配规则更麻烦IoU 门限、中心点采样、ATSS 这类规则本质上是把哪个预测框该为哪个真值负责这件事写死成规则。我在做密集小目标场景的时候NMS 这块吃过不少亏。两辆并排停的车或者货架上挨着的商品如果检测框本身回归得稍微宽一点NMS 就会把它当成重复框压掉你调阈值调到怀疑人生。这不是模型能力问题是流程设计带来的固有矛盾。DETR 的意义就在于它认为这三处手工活都是可以学出来的或者干脆不需要。1.2 DETR 的赌注把检测当成集合预测问题DETR 的出发点很干脆检测任务本质上就是给定一张图输出一个目标集合每个元素是类别边界框这么一个二元组。既然是集合那它天然就无序、无重复你不需要规定第 3 个框对应谁、第 7 个框对应谁。那怎么让模型学会输出一个集合呢DETR 的做法是让模型一次性输出固定数量的预测比如 100 个训练的时候用二分图匹配算法匈牙利算法在 100 个预测和图像里真实的 N 个目标之间找一个一对一的最优配对配上的那些预测才计算框回归损失和分类损失剩下没配上的预测全部去学无目标这个类别。这个设计的精妙之处在于一对一。既然一对一那一个真值目标只会有唯一一个预测框去负责它理论上就不可能产生重复框NMS 自然就成了多余的。这句话是理解 DETR 的第一把钥匙不是 DETR 不重复所以不需要 NMS而是它用一对一匹配的约束让重复变得没有收益。反过来说这也解释了 DETR 一个经常被吐槽的现象——为什么它输出 100 个框实际有用的只有几个剩下的都在喊无目标。因为 100 是个必须大于最大目标数的常数超出的部分必须有一个地方去那个地方就是无目标类。1.3 读懂源码前先分清三组容易混的概念我见过不少人在读 DETR 代码时被几个词绕晕这里提前拎出来。容易混的概念实际含义常见误解object query解码器的 100 个可学习嵌入向量以为是锚框或候选区域no-object 类分类头的第 num_classes 个输出以为是无目标框的回归输出辅助损失解码器每层都接一个预测头算 loss以为只有最后一层算损失Object query 这一栏要重点说它不是锚框没有尺寸、没有位置初始化、没有尺度先验就是一个 256 维的可学习向量100 个堆在一起。你可以把它理解成 100 个提问者每个提问者带着自己的一套偏好去解码器里问这张图里属于我的那个目标长什么样、在哪。训练完之后去看每个 query 的注意力分布会发现它们确实分化出了不同的空间偏好有的盯左下角有的盯中间偏上这也是 DETR 论文里那几张可视化图的由来。2. 拆开 DETR 的四个零件骨干、编码器、解码器、预测头2.1 ResNet 骨干做的事比你想的少DETR 的数据流是这样的一张图先过一个 ResNet-50取最后一层 stage5 的特征图此时空间下采样倍数是 32。也就是说一张 800×1333 的输入出来的特征图大概是 25×43。然后接一个 1×1 卷积把通道数从 2048 压到 256进入 Transformer。这里有个新手很容易忽略的点DETR 只用了一层特征图。没有 FPN没有多尺度融合所有的检测都在这一个 stride32 的特征图上做。这直接埋下了后面小目标检测弱的伏笔因为 stride 32 意味着特征图上一个像素对应原图 32×32 的区域一个 16×16 的小目标在特征图上连一个完整像素都占不满。那为什么不用多尺度因为在原版 DETR 的框架里多尺度特征进入 Transformer 编码器会让序列长度急剧膨胀而编码器的自注意力复杂度是序列长度的平方算不过来。这个取舍不是设计者没想到是当时算力条件下的妥协也正是 Deformable DETR 后来要解决的核心问题。还有一个实操细节骨干网络的 BatchNorm 在训练时是被冻结的。原因是 DETR 训练的 batch size 通常很小官方是 16单卡能开到 4 就不错了小 batch 下 BN 的统计量非常不稳定冻结掉更安全。你如果自己改代码千万别顺手把 BN 解冻。2.2 位置编码Transformer 看不见位置这件事必须靠它补Transformer 的自注意力本身是置换等变的把输入序列打乱输出的内容集合完全一样只是顺序跟着变。这在自然语言里还好但在图像里是致命的——你把特征图的行列顺序打乱检测器就完全不知道目标在哪了。DETR 的解法是给序列里每个位置加上位置编码。图像这块用的是二维正弦位置编码对每个行列位置分别在 x 方向和 y 方向算一组正弦余弦值然后拼起来。这个选择的好处是分辨率无关训练用 800 的短边推理用 1000 的短边也不会崩。但位置编码的注入位置很讲究。原版 DETR 在编码器输入端加一次解码器这边的 query 也带位置嵌入而解码器的交叉注意力里key 是编码器输出query 是 object query 加上位置。这套加法在后续的 Conditional DETR、DAB-DETR 里被反复拆解和改造因为很多研究者发现解码器交叉注意力的位置信息注入方式正是 DETR 收敛慢的一个关键原因后面第 4 章会细说。如果你自己实现位置编码有两个坑一是归一化方式官方用的是2*pi周期还是1周期不同实现不一致混用会导致完全训不动二是 padding mask同一批里不同尺寸的图被 pad 到同一大小被 pad 的位置必须 mask 掉否则注意力会去关注那些无意义的区域。2.3 解码器里那 100 个 object query 到底在干什么编码器输出的是图像特征序列长度约 107525×43。解码器有 6 层每一层做两件事先让 100 个 query 之间做一次自注意力再让这 100 个 query 去和编码器输出的 1075 个位置做交叉注意力。这里的关键结构是query 之间互相看是为了避免它们都去抢同一个目标。这其实就是把 NMS 想要做的事提前放到了网络内部用一个注意力机制来学。我个人的理解是这一层自注意力是 DETR 里最隐晦也最重要的一层它承担了分工的职责。解码器的每一层都会接一个预测头算出预测结果并计算损失这就是前面说的辅助损失。为什么要这么做因为如果只在最后一层算损失前面 5 层的梯度信号要穿过整个解码器才能回传训练效率很低。每层都算损失等于给中间层一个明确的监督信号收敛速度能明显改善。这个技巧在后来的 DETR 变体里基本成了标配。一个可以做的实验把num_queries从 100 改成 20如果数据集里单图最多只有 8 个目标你几乎不会损失精度但推理速度会快不少。这是我在实际项目里常用的一个优化手段尤其是部署到边缘设备上的时候100 个 query 的冗余太明显了。2.4 预测头为什么用 FFN 而不是卷积每个 query 出来之后过一个 3 层的 MLP输出两部分一个是num_classes 1维的分类分数一个是 4 维的框坐标中心点 x、y 和宽高都是归一化到 0-1 的值。用 MLP 而不是卷积逻辑上是自洽的这个向量已经是一个关于某个目标的高度抽象的表示它对应的空间位置不是固定的你去卷它没有意义。MLP 对每个 query 独立作用参数共享这就是一个标准的集合到集合的映射。框坐标用归一化的中心点加宽高而不是绝对像素值是为了让损失函数的量纲统一。你想L1 损失如果是像素值一张 4000 像素宽的图和一个 400 像素宽的图损失量级差了 10 倍学习率就没法统一设了。归一化之后所有图的框坐标都在 0 到 1 之间损失尺度稳定。这个细节看似小实际对训练稳定性影响很大。另外注意那个1在多分类问题上多出来的那一维就是无目标类。所以当你用torchvision.models.detection.detr或者 HuggingFace 的 DETR 实现时看到分类头的输出维度是num_classes 1不要惊讶那多的一维是必需的删掉模型就废了。3. 二分图匹配与匈牙利算法DETR 训练时到底在算什么损失3.1 为什么不能直接用交叉熵逐框对齐这是理解 DETR 最核心的一问。假设你有 3 个真值目标、100 个预测。最朴素的想法是给每个真值目标分配一个固定的预测槽位比如第 1 个目标给第 1 个预测第 2 个给第 2 个然后算交叉熵。问题是——真值的顺序是人为的标注文件里目标 A 写在前面还是 B 写在前面取决于标注员当时怎么点的跟图像内容半点关系没有。你让模型去拟合一个随机顺序那模型永远学不会因为同一个图换个标注顺序就要求模型给出完全不同的输出。DETR 的解法就是放弃固定对齐改成训练时动态找最优配对。每一轮前向先在预测和真值之间算一个代价矩阵用匈牙利算法求出总代价最小的那个一一配对然后基于这个配对计算损失。配对是临时的随每轮预测变化模型只被要求存在一种配对方式能让损失很低而不要求某个固定槽位永远是某个目标。这个思想其实和集合预测、和匹配问题一脉相承本质上是把标签分配这件事从一个手写规则变成了一个可优化的过程。3.2 匹配代价矩阵的三个分量配对不是只看框重合度DETR 的匹配代价由三部分加权组成分类概率、L1 距离、GIoU。分类部分用的是预测为真值类别的概率的负值也就是预测越确信自己是这个类代价越小。L1 部分直接算框坐标的曼哈顿距离。GIoU 部分算的是广义交并比它跟 L1 的区别在于 GIoU 关注的是形状和位置的综合重叠程度对尺度不敏感而 L1 对大框更宽容。这里有个细节值得注意匹配用的代价和训练用的损失权重可以不一样。官方实现里匹配代价的权重是分类 1、L1 是 5、GIoU 是 2而训练损失的权重也是分类 1、L1 是 5、GIoU 是 2看起来一致但在一些复现版本里匹配阶段会把分类权重调低因为早期预测的分类分数噪声很大容易把匹配带偏。这个技巧我在训练小数据集时用过确实更稳。分类损失还有一个非常关键的处理无目标类的交叉熵权重会被乘上 0.1。原因很直白100 个预测里通常只有几个是真目标剩下 90 多个都在学无目标如果不降权模型会倾向于把所有东西都判成无目标训练直接崩。这个 0.1 是个经验值数据越稀疏、目标越少这个值可以再调小一点。3.3 匈牙利算法在这里的角色与复杂度匈牙利算法解决的是经典的指派问题给定一个 n×n 的代价矩阵找一组一一对应使得总代价最小。DETR 里代价矩阵大小是 100×NN 是真值数量超出的部分用无目标填充成方阵然后求解。复杂度方面标准匈牙利算法是 O(n³)100 的话就是百万量级在一张 GPU 上跑相对于前向反向的开销基本可以忽略。所以实际训练时匹配这一步不是瓶颈不用去优化它。有些实现用scipy.optimize.linear_sum_assignment有些用自定义的 CUDA 版本速度差异在你 batch size 不超过 32 的时候根本感知不到。但这里有个隐藏的坑如果两张图的真值数量差异极大比如一张有 1 个目标、一张有 30 个目标批处理时的 mask 处理要格外小心。填充到 100 的那部分必须用正确的 mask 排除否则会有填充项参与匹配导致某些真值目标莫名其妙匹配到了填充位损失算得一塌糊涂。我踩过一次表现为 loss 一直不降但也不爆炸卡在那儿查了整整一天才发现是 mask 写错了。3.4 手写一个简化版匹配损失理解到位之后自己写一遍匹配逻辑是最有效的巩固方式。下面这段是简化版去掉了多卡和 mask 的处理只保留主干import torch from scipy.optimize import linear_sum_assignment torch.no_grad() def hungarian_match(pred_logits, pred_boxes, gt_labels, gt_boxes, cost_class1.0, cost_l15.0, cost_giou2.0): pred_logits: [Q, C1] Q 个 query 的分类 logits pred_boxes: [Q, 4] 归一化的 cxcywh gt_labels: [N] gt_boxes: [N, 4] 返回: 每个 gt 对应的 query 索引 # 分类代价: 取真值类别那一列的概率, 取负 prob pred_logits.softmax(-1) # [Q, C1] cost_cls -prob[:, gt_labels] # [Q, N] # L1 代价: 曼哈顿距离 cost_l1 torch.cdist(pred_boxes, gt_boxes, p1) # [Q, N] # GIoU 代价: 这里用简化的 IoU 近似, 实际要用完整 GIoU cost_iou 1.0 - pairwise_iou(pred_boxes, gt_boxes) C (cost_class * cost_cls cost_l1 * cost_l1 cost_giou * cost_iou) # [Q, N] # 匈牙利算法求最小代价指派 row, col linear_sum_assignment(C.cpu().numpy()) return torch.as_tensor(col) # 长度为 N 的 query 索引注意最后返回的是col也就是每个真值目标对应哪个 query。得到这个配对之后分类损失只在这 N 个 query 上算类别是真值类别框损失也只在这 N 个上算。剩下的Q - N个 query分类目标全部置为无目标类框损失不参与。这套逻辑写三遍就刻在脑子里了比看十遍论文管用。提示linear_sum_assignment必须传 numpy 数组而且要转到 CPU 上这一步会带来一次同步训练时如果频繁调用可能有微小开销。不过就像前面说的相对于前向反向这点开销可以忽略。4. 收敛慢、小目标差DETR 的几个硬伤与 Deformable DETR 的解法4.1 五百个 epoch 才收敛问题出在哪原版 DETR 在 COCO 上要训 300 到 500 个 epoch 才能收敛这是它最被诟病的地方。YOLO 系列几十个 epoch 就差不多了这个差距是数量级的。原因有几个层面。一个直接原因是匹配不稳定训练早期模型预测的框基本是随机的分类分数也是乱的这时候匈牙利算法算出来的配对每轮都在变模型等于在追逐一个不断移动的靶子梯度信号非常嘈杂。第二个原因是交叉注意力需要从零学起object query 初始是随机的它要去 1075 个编码器位置里找到属于自己那个目标这个找的过程全靠注意力权重一点点学出来早期注意力权重接近均匀分布梯度很小所以学得非常慢论文里管这个叫注意力权重的收敛困难。第三个原因跟位置编码的注入方式有关前面提过后续工作发现解码器交叉注意力里 key 和 query 的位置信息纠缠在一起导致模型难以快速定位到参考点Conditional DETR 就是专门改造这一块来加速收敛的能做到 10 倍提速。4.2 小目标检测弱本质是单尺度加全局注意力的代价小目标弱这件事我用一句话总结分辨率不够注意力又太散。分辨率问题前面说过stride 32 的单层特征图16×16 的目标在特征图上几乎消失。分辨率不够注意力还帮不上忙——编码器的自注意力是全局的1075 个 token 之间两两互相看一个 query 在交叉注意力里也是对着全部 1075 个位置算权重。这种全连接的注意力对小目标特别不友好因为小目标占的 token 少注意力权重很容易被周围大面积的背景和大目标稀释掉。我在一个货架商品检测的项目里对比过同样是 ResNet-50 骨干Faster R-CNN 用 FPN 融合 C3、C4、C5 三层小目标 AP 能到 20 多DETR 只有个位数。这不是调参能解决的问题是结构决定的。4.3 Deformable Attention 的可变形采样点机制Deformable DETR 的核心贡献就是给注意力机制加了一个可变形采样的能力让每个 query 不用对全图所有位置算注意力只在少数几个采样点上算。具体做法是给每个 query 一个参考点reference point然后在特征图上围绕这个参考点预测若干个偏移量在偏移后的位置做双线性插值采样只对这些采样点算注意力权重。论文里的配置是每个尺度取 3 到 4 个层级每个层级每个头取 4 个采样点。这样算下来一个 query 参与计算的位置只有十几个而不是一千多个。好处立刻体现在三个地方计算量降下来了可以上多尺度了收敛快了官方说 10 倍实际用起来 50 个 epoch 就能到不错的水平小目标变好了因为多尺度意味着 stride 8 的特征图也进来了。这套机制理解起来不难但实现上有点绕尤其是偏移量的预测和双线性插值的反向传播。好消息是现在主流检测库都有实现你直接用就行不用自己写。要自己写的话建议从torch.nn.functional.grid_sample入手理解采样点是怎么映射的。4.4 训练策略上能打的几个补丁除了换结构还有一批工作专门在训练策略上做文章理解这些对你调参很有帮助。改进方向代表做法解决了什么加入噪声训练在真值框上加噪声作为额外 query 输入让模型更早看到接近真值的框加速收敛改进 query 设计让 query 同时带位置和尺寸信息把 object query 锚框化学习目标更明确分层匹配不同去噪组分别匹配稳定早期匹配缓解匹配抖动增加正样本引入一对多辅助分支在保持推理端一对一的同时提高训练信号密度这几类改进里加噪声训练的思路我觉得最值得玩味它在训练时把真值框加一点扰动后直接喂给解码器作为额外的 query然后要求这些 query 把框回归回原样。这相当于给模型开小灶——你看正确的答案附近长这样。推理时这批 query 直接丢掉。这个训练策略在低数据量场景下效果特别明显。我个人经验是如果你的数据量少于 5000 张图不要硬训原版 DETR。选一个带噪声训练或者分层匹配的变体收敛快、对超参不敏感能省掉大量的试错成本。5. 拿自己的数据训 DETR数据组织、配置改动和调参心得5.1 COCO 格式的几个格式坑DETR 系列默认吃 COCO 格式标注这算是必修课。一个 JSON 文件里放 images、annotations、categories 三块图片放一个目录。坑集中在几个地方。第一bbox 格式是 [x, y, width, height] 而不是 [x1, y1, x2, y2]这个错我见过太多次了写错了不会报错但模型会学出一堆乱七八糟的框。第二category_id 必须从 1 开始且连续中间断了会导致类别数和实际类别对不上训练时不报错推理时某个类永远检不出来。第三图像宽高必须和实际文件一致有些工具生成时会写错表现是框全部偏移。一个稳妥的做法是把标注文件先喂给一个校验脚本逐条检查 x w 不超图像宽、y h 不超图像高、类别 id 在合法范围内。这个脚本二三十行能帮你省下几天的排查时间。5.2 类别数和 num_queries 怎么定类别数这块你只需要改配置里的num_classes框架会自动把分类头设成num_classes 1。但我遇到过有人手动改了分类头的输出维度把那个1去掉了然后发现 loss 永远是 0——因为模型没法表达这里什么都没有所有 query 都要去抢真值匹配必然崩。这个 1 绝对不能动。num_queries的选择要看你数据集里单张图的最大目标数。经验值是取最大目标数的 1.5 到 2 倍但不要低于 50。如果你做的是单目标检测比如只检车牌可以降到 20 甚至 10效果基本不掉速度明显提升。反过来如果是遥感图像那种一张图几百个目标的场景100 明显不够需要往上加但加了之后训练会更慢而且无目标类的样本会更多权重得相应调整。还有一个我踩过的坑单图目标数超过 num_queries 的时候超出的目标会被直接忽略因为匹配的时候它们没地方可去。这种情况不会报错只是那些目标永远检不出来。做数据集统计的时候一定要画一下单图目标数的分布直方图看一眼长尾到哪。5.3 学习率、batch size 与骨干冻结策略官方配置里骨干网络和 Transformer 用了不同的学习率backbone 是 1e-5Transformer 部分是 1e-4都是 AdamW权重衰减 1e-4。这个分层学习率的设计是因为骨干是预训练好的学习率太大会把预训练特征破坏掉而 Transformer 是随机初始化的需要更大的学习率才能训起来。批量大小官方是 16这在单卡上基本开不起来我用 8 或者 4 训过效果会掉一点但不致命。小批量的对策是把学习率按比例缩一点不过 DETR 对学习率的容忍度还行不会因为个位数的差异就崩掉。学习率调度上官方是在第 200 个 epoch 降 10 倍总共训 300 个 epoch。如果你数据量小这个调度要缩短但降学习率的时机要按比例调比如总共训 50 个 epoch 就在第 33 个 epoch 降。我试过不降学习率直接训完最后的 AP 会低一两个点不算致命但也不值得省这一步。数据增强方面官方用的组合是随机缩放短边在 480 到 800 之间随机长边不超过 1333、随机裁剪、颜色抖动、随机水平翻转。这里面我觉得随机缩放最重要因为 DETR 只有一个尺度缩放是它见多尺度的唯一途径。如果你把随机缩放关掉小目标的检测能力会进一步恶化。5.4 训练过程中的 loss 曲线怎么读DETR 的 loss 由几部分组成分类的交叉熵、L1 框损失、GIoU 损失每部分在解码器 6 层各算一遍所以你看日志里会有一堆分类别损失。几个经验性的判读方法。分类损失前几十个 epoch 下降比较快后面变慢是正常的如果分类损失一开始就不降八成是学习率太小或者匹配 mask 有问题。框损失下降会更慢而且波动大因为匹配在变同一个 query 这轮配这个目标下轮配那个目标损失有跳动很正常。一个值得盯的指标是GIoU 损失的绝对值。如果它长时间停在 0.8 以上说明框基本没学会这时候先别调参去看数据——大概率是框的坐标系或者格式有问题。还有个小技巧训练日志里加上每个类别的正样本数量统计。如果你发现某个类别的正样本数一直是 0那这个类永远学不会回头去查标注文件里这个类的 category_id 是不是写错了。6. 把 DETR 跑起来推理、导出与部署的几个现实问题6.1 推理端的后处理几乎为零但不是真的零推理时 DETR 输出 100 个框每个带num_classes 1个分数。后处理要做的只有两件事把无目标类那一列丢掉然后按分数阈值过滤。没有 NMS没有 top-k 抑制虽然你加一个也无妨。但几乎为零不等于可以不管。有几个细节必须处理一是框可能退化成零宽零高在算 IoU 或者画图之前要 clamp 一下最小宽高否则会除零。二是框可能超出图像边界因为模型输出的归一化坐标理论上在 0 到 1 之间但实际会有轻微越界可视化之前要裁一下。三是阈值的选择DETR 的分数分布跟 YOLO 很不一样它没有经过 NMS 之后那种胜者通吃的锐化所以阈值可以设得低一些0.3 到 0.5 都常见具体看你的场景。我在一个项目里遇到过同一辆车被两个框都检出来的情况一度以为 DETR 的免 NMS 承诺是假的。后来查清楚了那两个框的分数都不高而且它们的 IoU 只有 0.6 左右属于模型在两个位置都给了近似答案。这不是 NMS 能解决的问题而是模型对这个目标的响应本身就有两个峰。解决办法是提高训练数据质量或者在应用层加一个轻量级的 IoU 去重——不影响主要结论但工程上该加还得加。6.2 导出 ONNX 时的算子坑原版 DETR 导出 ONNX 相对顺畅主要是要把动态的序列长度固定下来输入尺寸固定成比如 800×1333输出的 100 个框也是固定的这对部署反而友好。真正的坑在 Deformable DETR 上。可变形注意力的双线性采样在导出时grid_sample这个算子在有些推理引擎上支持得不完整尤其是低版本的 opset。常见的处理办法是自定义算子或者在导出前把可变形注意力替换成普通的注意力精度会掉但能跑。我更推荐的做法是选一个已经解决过这个问题的推理框架或者预导出模型别自己从零趟。还有一个通用坑是多尺度输入的处理。如果你的模型输入尺寸是可变的导出时要么固定死要么把 resize 逻辑放到模型外面用预处理做。我倾向于后者模型只管固定尺寸的张量预处理在 CPU 上做这样 ONNX 图最干净推理引擎的优化也最好做。6.3 显存和延迟100 个 query 的真实代价DETR 的推理开销主要不在 query 上而在编码器。编码器自注意力的计算量跟 token 数的平方成正比800×1333 输入对应约 1075 个 token自注意力矩阵是百万量级乘上 8 个头和 6 层这才是真正吃显存和算力的地方。想降延迟按性价比排序我建议这么调先把num_queries从 100 降到实际需要的数量这个几乎是白送的加速精度几乎无损。再考虑缩短输入短边比如从 800 降到 640代价是小目标精度但速度提升明显。然后才是换更轻的骨干比如 ResNet-18 或者轻量骨干这一步对精度影响最大要谨慎。如果还是不够快考虑换用实时性更好的 DETR 变体比如专门为实时场景设计的 RT-DETR 一类方案它在结构上做了大量针对推理速度的优化同时保持了免 NMS 的特性。我实测过一个组合ResNet-18 骨干加 30 个 query 加 640 输入在单张中端显卡上能跑到 30 FPS 以上精度相比原版掉了大概 4 个 AP。对很多工业视觉场景来说这个取舍是划算的。6.4 什么场景适合上 DETR什么场景别硬上聊到这儿应该能给出一个相对清晰的判断了。适合的场景目标数量不多、类别数中等、对重复框特别敏感。比如工业质检里的缺陷检测一张图往往就几个缺陷而且缺陷之间可能挨得很近NMS 阈值调不好就会漏检这时候 DETR 的一对一匹配优势非常明显。再比如文档版面分析、表格里的元素检测目标数量可控DETR 系列表现不错。不太适合的场景密集小目标、对延迟极其敏感、数据量很小。密集小目标前面说过了原版 DETR 基本不能用得用 Deformable 系列或者干脆换回 YOLO。延迟敏感的场景要看你有没有条件做裁剪和加速如果不行老老实实用单阶段检测器。数据量小的场景就像前面说的原版 DETR 没几千张图训不出东西得选带噪声训练或者分层匹配的变体。最后再说一个我自己的体会DETR 系列真正的价值不在于它在 COCO 上刷了多少点而在于它把检测流程能不能端到端这个问题正面回答了一次。哪怕你最后用的还是 YOLO理解 object query 的分工机制、理解一对一匹配为什么能替代 NMS对你调其他检测器的正负样本分配规则都有帮助。我现在的习惯是每接手一个新的检测任务先用 DETR 系列快速跑一个 baseline——它不需要调锚框、不需要调 NMS出结果快能帮我很快判断这批数据到底能不能训出东西。如果 DETR 都训不动那大概率是数据本身有问题而不是模型的问题。这个判断方法比一上来就调 YOLO 的参数效率高得多。