简介:围绕DETR论文的文献汇报PPT,面向计算机视觉方向的研究生、算法初学者及需要做论文分享的读者,聚焦端到端目标检测如何以集合预测替代Anchor与NMS等手工组件。压缩包仅含1个pptx文件,大小约3.44MB,页面按研究背景、研究目的、研究过程与结论组织,便于直接用于组会汇报或课程展示。内容系统拆解DETR的CNN特征提取、Transformer编解码与FFN预测三部分,并延伸到位置编码、object queries、二分图匹配损失及“无对象”类别等关键机制,也提到COCO数据集上与Faster R-CNN的对比和全景分割推广。已有1094人学习,适合据此快速搭建汇报框架、理解模型流程并补充讲稿要点。
1. 集合预测:为什么目标检测可以没有 Anchor 和 NMS
目标检测过去十年的主流做法,本质上都是“先撒网、再筛鱼”。在机器学习的目标检测分支里,RCNN 系列和 YOLO 系列沿用了同一条流水线:在特征图上铺满不同尺度和长宽比的先验框,为每个先验框回归偏移量,再靠 NMS 把重叠的候选框压掉。而 DETR 把这条流水线整个拆了:一次并行输出固定数量的 100 个预测,训练时用匈牙利算法把预测和真值框一对一配上,推理时直接取分数最高的结果,既没有 anchor,也没有 NMS,也没有滑动窗口。反直觉的地方在于,去重本该是推理阶段的后处理,DETR 把它提前到了损失函数里:在训练时就规定“一个真值只能被一个预测框认领”,重复预测会直接被 ∅ 类惩罚。搞懂这条机制,你才算看懂端到端的 Transformer 检测到底省掉了什么,又为此付出了什么代价。
2. 从特征图到序列:CNN backbone 到 Encoder 的三步改造
Transformer 处理的是序列,而卷积网络吐出的是二维特征图,两者之间隔着一整套适配工作。DETR 的整体结构可以拆成三块:CNN backbone 负责把图像变成特征图,Transformer 编解码器负责建模元素之间的依赖关系,前馈网络负责把每个输出向量翻译成类别和框坐标。真正的设计难点集中在第一块到第二块的衔接上,这段衔接工作可以拆成三步:降维、展平、补位置。
2.1 1×1 卷积降维:2048 通道压到 256 到底在压什么
backbone 用的是去掉分类头和全局池化的 ResNet,输出一个2048 × H × W的张量,其中 H 和 W 是原图尺寸除以 32。图像短边缩放到 800 像素时,特征图大约是 25×42,每个位置挂着 2048 维的描述向量。这个通道数对卷积来说是家常便饭,但对 Transformer 来说就很浪费:自注意力的计算量与 token 数的平方成正比,还要再乘上特征维度,维度越高,显存和算力消耗越大。因此模型先用一个 1×1 卷积把通道从 2048 压到 256。
这个 1×1 卷积的参数量只有2048 × 256,作用相当于对每个空间位置做一次线性投影。它在每个像素位置上独立作用,不混合邻域,本质上只是特征降维,并非用于提取新的空间结构。有人会问为什么不直接用 2048 维进 Transformer,答案在后面的注意力矩阵里:维度翻 8 倍,QKV 投影和 FFN 的中间层都要跟着翻,而检测精度并不会因此提升,因为主干网络已经提取到足够的信息了。这一步还存在一个血泪教训式的细节:1×1 卷积后面通常接 BatchNorm,训练和推理的统计量不一致时,小 batch 下性能会飘,复现时如果发现验证集指标比训练集差一大截,先查这里。
2.2 展平与位置编码:顺序无关的 Transformer 怎么知道谁在左边
降维之后是展平。把256 × H × W的张量 reshape 成256 × HW,也就是 HW 个长度为 256 的 token,每个 token 对应原图上一个 32×32 的区块。到这里问题来了:自注意力对所有 token 是置换等变的,你把 token 顺序打乱,输出只是跟着打乱,模型本身感知不到“左边”和“右边”的区别。卷积网络天生带位置信息,是因为卷积核在空间上滑动;Transformer 没有这个先验,必须显式补回去。
DETR 选用的是正弦位置编码,但做了 2D 改造:把 256 维拆成两半,前 128 维编码纵坐标,后 128 维编码横坐标,各自再分成 sin 和 cos 两部分。这样每个 token 拿到的位置向量同时携带 x 和 y 的信息,而且是确定性的、无需学习的。常见的实现思路大致如下:
import torch def sincos_2d(d_model, h, w, temperature=10000.0): """生成 [h*w, d_model] 的 2D 正弦位置编码,x/y 各占一半维度""" num_feats = d_model // 2 # 每个方向分到的维度,这里是 128 # 不同维度对应不同波长,形成从粗到细的多尺度位置指纹 omega = 1.0 / (temperature ** (torch.arange(num_feats // 2) / (num_feats // 2))) y = torch.arange(h).float().unsqueeze(1) # [h, 1] x = torch.arange(w).float().unsqueeze(1) # [w, 1] # 每个方向内,一半维度用 sin,一半用 cos pe_x = torch.cat([(x * omega).sin(), (x * omega).cos()], dim=1) # [w, 128] pe_y = torch.cat([(y * omega).sin(), (y * omega).cos()], dim=1) # [h, 128] pe_x = pe_x.unsqueeze(0).expand(h, w, -1) # [h, w, 128] pe_y = pe_y.unsqueeze(1).expand(h, w, -1) # [h, w, 128] # 先 y 后 x 拼接,最后展平成序列维度,与特征图 token 一一对应 return torch.cat([pe_y, pe_x], dim=-1).flatten(0, 1) # [h*w, 256]参数说明里最关键的是num_feats:它决定每个方向能用多少个不同频率,频率越多,位置分辨越细。temperature控制最低频率的波长,取 10000 时波长跨度足够覆盖整张图。这段编码是一个[h*w, 256]的矩阵,直接加到展平后的特征上,广播到 batch 维度即可。这里没有做坐标归一化,用的是整数坐标,所以换分辨率时编码的绝对数值会变,但因为 sin/cos 是周期函数,相对位置关系仍然成立。
消融结果说明了这一步的分量:完全不用空间位置编码,AP 掉得非常明显;只在编码器输入端加一次、还是每层注意力都加,差别反而不大;换成可学习的 2D 位置嵌入,效果和正弦版本接近。也就是说,真正重要的是“有没有位置信息”,至于“用哪种编码”,模型并不挑剔。
2.3 编码器自注意力:全局建模如何把实例“分开”
补完位置信息,序列才被送进编码器。编码器是标准的 Transformer 结构,6 层,每层一个多头自注意力加一个前馈网络,隐藏维度 256,8 个头。它做的事情可以用一句话概括:让每个 token 都能看到图上所有其他 token,然后把自己的表示更新成“结合了全局上下文”的版本。
这件事对检测的意义在后面的可视化里能看出来。解码器的注意力图显示,模型往往只关注目标的局部,比如大象的头部或者腿部,而不是完整的轮廓。这说明编码器已经完成了实例分离的工作:同一个物体上的 token 在自注意力中互相加强,形成了一簇具有一致性的表示;不同物体的 token 之间则被拉开。到了解码器阶段,只需要从这簇表示里抽取类别和边界框信息就够了。对于被遮挡的物体,即使只露出一小块,全局注意力依然能把它和周围背景区分开,这是卷积的局部感受野难以实现的。
代价也在这里。自注意力的复杂度是 token 数的平方,短边 800 像素时大约 1050 个 token,注意力矩阵一百多万个元素,尚可接受;如果把 backbone 的下采样倍率从 32 降到 8,token 数会飙到一万七千,矩阵规模涨两百多倍,显存直接不够用。所以 DETR 系列后续工作大多在“怎么用更少的 token 拿到更细的特征”上做文章,比如用多尺度可变形注意力,只在稀疏采样点上计算,把复杂度从平方级压回线性级。这是后话,但明白了这一步的瓶颈,再看那些改进就知道它们在补什么。
3. Object Queries:解码器凭什么并行吐出 100 个框
编码器输出的是一张“理解过全局”的特征序列,但序列本身没有告诉模型“图里有几个物体、分别在哪”。这个任务交给了解码器,而解码器的输入除了编码器输出,还有一组和图像毫无关系的向量:object queries。整个 DETR 最反直觉的设计就在这里。
3.1 100 个可学习查询向量到底在问什么
object queries 是 N 个(N 取 100)长度为 256 的可学习向量,随机初始化,跟着训练一起更新。它们不来自图像,也不是从特征里算出来的,就是 100 个凭空存在的“槽位”。解码器让这 100 个槽位各自和编码器特征做交叉注意力,每个槽位从图上取走它需要的信息,最后输出一个预测。
那么它们学到了什么?把每个 query 的注意力图可视化出来大致能看出:有的 query 固定在问“左下角有什么”,有的问“画面中间靠下有什么”,有的专门盯住大面积的物体。这很像传统检测里 anchor 的职责分工,区别在于 anchor 是人工设定的先验框,位置、尺度、长宽比都要人调;而 query 的空间偏好是从数据里学出来的,没有任何一条规则告诉它“第 7 个 query 负责左上角”。这份汇报里用“目标查询”这个词来翻译 object queries,其实很贴切:它就是在向图像提问。
一个常见的误解是“query 数量等于最大检测数,所以超过 100 个物体就废了”。严格说,N 是模型能同时输出的槽位数,确实构成上限,但设计时选的 N 显著大于图像中的典型目标数,绝大多数图里只有几个到几十个物体,100 个槽位留了充足余量。剩下的空槽位由 ∅ 类吸收,不影响实际检测。
3.2 解码器两层注意力:self-attention 与 encoder-decoder attention 的分工
解码器也是 6 层,每层有两个注意力子层,分工完全不同。
第一个是 query 之间的自注意力。100 个槽位在这一层互相通信,交换“我准备预测什么”的信息。这一层是替代 NMS 的关键:如果两个槽位都想认领同一只狗,它们会通过自注意力发现彼此重复,训练时的匹配机制会惩罚这种重复,久而久之模型学会了让槽位之间自动错开。也就是说,去重能力是在训练中由损失函数塑造出来的,而不是在推理时靠后处理删掉的。
第二个是编码器与解码器之间的交叉注意力。query 作为 Q,编码器输出作为 K 和 V,每个槽位按自己的偏好从特征序列里取信息。这一层是真正“看图”的地方,query 的空间偏好在这里起作用。
结构上还有一个细节:解码器每一层的输出都会接上同一个预测头(共享 FFN),并且每一层都算一次辅助损失。这不是为了多输出几组结果,而是为了给中间层更密的梯度。如果只在最后一层算损失,6 层解码器的梯度要传很深,训练会更慢。加上辅助损失后,每一层都被要求直接给出合理预测,收敛速度明显改善。训练早期模型的输出还很乱,辅助损失提供了额外的约束,这在后面讲收敛慢的问题时还会提到。
3.3 FFN 头与 ∅ 类:固定槽位怎么处理“空”
每个槽位的输出向量(256 维)最后被送进一个三层感知机,中间用 ReLU,输出分成两支:一支是类别概率,维度是类别数加 1;另一支是归一化的框坐标,即中心点 cx、cy 和宽高 w、h,都在 0 到 1 之间,用 sigmoid 压住。
import torch.nn as nn class SetPredictionHead(nn.Module): """DETR 的预测头:分类与回归共享同一个 3 层 MLP""" def __init__(self, d_model=256, num_classes=91): super().__init__() self.shared = nn.Sequential( nn.Linear(d_model, d_model), nn.ReLU(), nn.Linear(d_model, d_model), nn.ReLU(), nn.Linear(d_model, d_model), nn.ReLU(), ) # 多出来的那一类就是 ∅,用来吸收没有匹配到真值的槽位 self.cls = nn.Linear(d_model, num_classes + 1) # 回归头只输出 4 个数,不用 softmax,sigmoid 保证落在 [0,1] self.box = nn.Linear(d_model, 4) def forward(self, hs): # hs: [num_layers, B, N, d],解码器每层输出都送进同一个头 x = self.shared(hs) return self.cls(x), self.box(x).sigmoid()参数里最值得注意的是num_classes + 1。这个额外的类别不是“背景”的同义词,虽然作用类似:它表示“这个槽位里没有物体”。为什么需要它?因为输出数量是固定的 100 个,而真值框数量是变长的。训练时必须把真值集合补齐到 100 个,缺的位置填 ∅;预测端也必须有对应的一类来接住这些空槽位,否则每个空槽位都会被强行分到某个真实类别,损失无法下降。这个设计把变长输出问题转成了定长分类问题,是整个集合预测能成立的前提。
另外,框坐标用 sigmoid 而不是 exp 或者线性输出,是为了保证预测值始终落在图像范围内,避免训练早期出现离谱的坐标把损失拉爆。分类分支和回归分支共享前面的三层 MLP,是因为两者需要的特征高度重叠:判断“这里是什么”和“这里的边界在哪”都要依赖同一个位置的上下文表示。
4. 二分图匹配:训练时预测框怎么和真值一对一配对
模型输出 100 个预测,真值可能只有 3 个框,中间隔着一次分配。这次分配的质量直接决定训练信号的质量,也是 DETR 收敛慢的主要来源。理解这一步,才能理解为什么端到端检测的代价是“要训 500 轮”。
4.1 代价矩阵与匈牙利算法:最优分配怎么求
训练时,先构造一个 N×M 的代价矩阵(N 是预测数 100,M 是这张图的真值数)。矩阵里每个元素表示“第 i 个预测去匹配第 j 个真值”要付多少代价,代价由三部分组成:分类的负对数概率、框的 L1 距离、框的 GIoU 损失。然后在这个矩阵上求一个最优分配,让所有配对的总代价最小,每个预测最多配一个真值,每个真值最多配一个预测。
求解用的是匈牙利算法。它原本要求方阵,实际实现里会把 M 那一侧补齐到 N 列,补出来的列代价设成一个很大的常数,这样最优解自然不会去选它们,求解完再把填充列去掉。
import numpy as np import torch from scipy.optimize import linear_sum_assignment @torch.no_grad() def hungarian_match(cost_cls, cost_l1, cost_giou, lam_l1=5.0, lam_giou=2.0): """cost_* 形状为 [B, N, M]:N 个预测、M 个真值;返回每个样本的匹配索引""" # 分类项取负对数概率,越小越好;框的两项直接加权相加 cost = -cost_cls + lam_l1 * cost_l1 + lam_giou * cost_giou results = [] for b in range(cost.shape[0]): c = cost[b].cpu().numpy() n, m = c.shape # 补到方阵,填充项代价设极大,保证不会被选中 padded = np.concatenate([c, np.full((n, n - m), 1e6)], axis=1) rows, cols = linear_sum_assignment(padded) keep = cols < m # 去掉落在填充列上的配对 results.append((rows[keep], cols[keep])) return results这段代码里有三处容易翻车。第一,cost_cls必须是负对数概率,不能直接传概率,否则方向反了,最优解会匹配到最差的分类结果。第二,填充项的数值要足够大但不能是inf,否则数值求解会出问题。第三,匹配过程必须放在torch.no_grad()里,它是离散决策,不需要梯度,也不需要参与反向传播。匈牙利算法的复杂度是 O(n³),N=100 时单次求解在毫秒级,相对于前向反向的计算量可以忽略,所以放在训练循环里不会成为瓶颈。
4.2 损失函数:L1 加 GIoU 为什么这么配
匹配确定之后才算损失。总损失是分类的交叉熵加上框回归的线性组合,权重上 L1 取 5,GIoU 取 2。这里的关键是分类损失和框损失作用范围不一样:分类损失在全部 100 个槽位上计算,包括那些匹配到 ∅ 的槽位;框损失只在真正匹配上的那几对之间计算。
def set_criterion(pred_cls, pred_box, targets, indices, lam_cls=1.0, lam_l1=5.0, lam_giou=2.0): """pred_cls: [B, N, C+1] logits;pred_box: [B, N, 4];targets 为真值列表""" idx_pred, idx_gt = indices # 匹配得到的预测下标和真值下标 # 1) 分类:所有槽位都参与,未匹配的槽位目标类别是 ∅ loss_cls = focal_or_ce(pred_cls, build_target_labels(targets, indices)) # 2) 框回归:只取匹配上的预测和对应的真值 matched_pred = pred_box[idx_pred] # [K, 4] matched_gt = targets.boxes[idx_gt] # [K, 4] loss_l1 = (matched_pred - matched_gt).abs().sum(-1).mean() loss_giou = (1 - generalized_iou(matched_pred, matched_gt)).mean() return lam_cls * loss_cls + lam_l1 * loss_l1 + lam_giou * loss_giou为什么要用 L1 和 GIoU 两个框损失,而且 GIoU 的权重是 2 不是更大?单独用 L1 有个明显缺陷:它对框的尺度不敏感。同样偏 10 个像素,对一个 300 像素的大框几乎无伤,对一个 20 像素的小框却是灾难性的偏移,而 L1 给出同样的惩罚。GIoU 衡量的是两个框的重合程度,通过面积归一把尺度因素消掉,小框和大框的错位在它眼里轻重不同。消融表里能看到:只用 L1 时,大目标的 AP 还算能看,中目标的 AP 提升主要来自 GIoU 的加入;两个一起用,AP 在各个尺度上都更均衡。
注意:GIoU 在框完全不相交时退化为一种“推向相交”的梯度,能比 IoU 提供更连续的信号,这也是它被选来替代纯 IoU 的原因。
还有一点容易被忽略:分类损失在 ∅ 槽位上也要算,而且 ∅ 的数量远多于真实类别。如果不做处理,模型会倾向于把所有槽位都预测成 ∅,因为这样损失最低。原始实现用的是普通的交叉熵,后续工作普遍改用 focal loss 或者对 ∅ 类降权来缓解这个不平衡。复现时如果发现训练到中途模型完全不输出任何框,先看 ∅ 类的梯度是不是占了主导。
4.3 匹配只在训练时发生:推理阶段为什么不匹配
训练结束后,推理路径短得多:100 个槽位各自算一遍分类和回归,类别取 argmax,分数低于阈值的丢掉,剩下的框直接输出。没有代价矩阵,没有匈牙利算法,也没有 NMS。
为什么不匹配也能工作?因为匹配的作用是给模型建立“一个真值对应一个预测”的监督信号。训练足够久之后,模型内部的 query 已经形成了稳定的分工:某个 query 固定负责某类物体、某个区域,重复预测会持续被惩罚,所以推理时不需要再靠外部算法去重。这正是端到端的意义所在:后处理逻辑被吸收进了参数里。
但这里有个隐藏的依赖关系。正因为去重能力是靠一对一监督训出来的,如果训练不充分,模型还没学会错开,推理时就会出现同一目标被多个槽位重复预测的情况。这时候有人会想“那我加个 NMS 补救一下”,结果往往是在密集场景里误删真框,指标反而掉。这个问题留到下一章细说。
5. 常见问题排查:DETR 复现里最容易翻车的几处
模型结构看起来不复杂,能跑起来是一回事,能训到论文指标是另一回事。下面几条是复现时高频出现的现象,每条都按“看到什么、为什么、怎么改”的顺序说。
5.1 训练几百轮 loss 不动:一对一监督的代价
现象:前 50 到 100 轮,分类损失缓慢下降,框的损失几乎平着走,验证集 AP 长期在个位数徘徊,很容易让人以为代码写错了。
原因:一对一匹配意味着每个真值框只有一个正样本,一张图里只有几个到几十个正样本参与框回归,梯度信号极其稀疏。相比之下,传统检测器每个真值会匹配几十上百个 anchor,梯度密集得多。再加上匈牙利匹配本身是离散的,早期预测很乱,匹配结果会在不同轮次之间跳来跳去,模型相当于在追一个移动的目标。
解决:接受训练周期长这个事实,用长 schedule,不要跑 50 轮就下结论。学习率上,backbone 和 Transformer 要分开设,backbone 用 1e-5,Transformer 用 1e-4,前者是后者的十分之一,因为预训练主干的参数已经很好了,学太快反而破坏特征。学习率还要预热,从 1e-6 线性升到 1e-4,大概占前五分之一的总轮数,之后余弦降到 1e-6。梯度裁剪设 0.1,防止早期 loss 尖峰把参数带偏。这几项凑齐,收敛曲线会明显好看很多。
5.2 位置编码加错位置:AP 凭空掉一大截
现象:模型能训,loss 也降,但 AP 比预期低十几到二十个点,可视化发现框的位置系统性偏移,或者小目标基本检不出来。
原因:位置编码加错了地方。常见有三种:一是在展平后忘了加,直接把特征送进编码器,Transformer 完全不知道 token 的空间顺序;二是把可学习的 object queries 当成位置编码加到了编码器输入端,两者混用;三是 x 和 y 方向的维度拼接顺序搞反,训练时模型能部分适应,但泛化变差。
解决:位置编码只加在编码器输入端,和展平后的特征逐元素相加,不要把 query 扯进来。实现时严格按“一半维度给 y、一半给 x”的方式拼接,展平顺序要和特征图的[H, W]索引一致,否则位置和 token 会对不上。改完以后,把某一个 query 的注意力图可视化出来,如果它能稳定地聚焦在图像的某个区域,说明位置信息通路是通的。这份文献汇报里把编码器层数和位置编码的消融表放在一起,对照着看会更有感觉:层数从少加到多,AP 单调上升;位置编码一去掉,AP 断崖式下跌。
5.3 推理时手痒加 NMS:把一对一监督的好处抵消掉
现象:不加 NMS 时 AP 正常,加上 NMS 之后掉点,或者在某些密集场景里掉得厉害。
原因:模型已经被训练成“每个目标只输出一个框”,重复框本来就少,NMS 在这里起不到净化作用,反而会把两个真实目标的重叠框当成重复去掉,比如并排站着的两个人、堆在一起的货物。NMS 的阈值越激进,误删越严重。
解决:推理路径里不要加 NMS,这本来就是 DETR 的设计卖点。如果确实观察到大量重复预测,说明训练不充分,应该回头查学习率和训练轮数,而不是在推理端打补丁。判断方法很简单:关掉 NMS 跑一遍验证集,如果 AP 反而更高,就说明模型的一对一监督是有效的,之前那些重复框是训练不足造成的假象。
5.4 输出槽位数量与真值补齐:批量训练的边界情况
现象:训练时偶发报错,或者某些 batch 的 loss 变成 NaN;换用不同数据集的标注格式后,报错位置换了个地方。
原因:N是固定值,而每张图的真值数量不同,构造代价矩阵时 M 是变长的。批量训练时,不同样本的 M 不一样,如果硬拼成一个张量,就会触发形状不匹配;另外,如果某张图没有真值,M 等于 0,代价矩阵退化成空,匈牙利算法直接报错,损失也无从计算。
解决:逐样本做匹配,不要试图把整批的代价矩阵堆成一个三维张量再一起求解;代价矩阵按样本补到 N×N 再求解,填充项设大常数。对空标注的图,跳过匹配,只计算全部槽位对 ∅ 的分类损失,让这些图仍然贡献“这里没有物体”的监督信号。还有一种情况要防:真值框数量如果超过 N,理论上不可能发生,但换到小目标极其密集的数据集时确实可能溢出,这时多出来的真值会被静默丢弃,损失表面上正常,实际漏检严重。训练前先统计一下每张图的真值数量分布,把 N 定得比最大值高出足够余量。
6. 设计取舍:端到端去掉 NMS,代价到底付在哪
DETR 的账面上,收益写得很清楚:流程从“生成先验框、为每个先验框回归偏移量、再做后处理去重”三步压成一歩;anchor 的尺度、长宽比、匹配阈值这些超参全部消失;同一个模型加一个 mask 头就能做全景分割;注意力图还能给出一层可解释性。这些收益是真实的,也是它被广泛引用和扩展的原因。
代价同样结构化,而且都能从前面几章的机制里推出来。我把最容易感知的三项列在一起对照:
| 维度 | 传统先验框方案 | DETR 集合预测 | 代价来源 |
|---|---|---|---|
| 收敛速度 | 十几到几十轮 | 数百轮 | 一对一匹配,每张图正样本极少 |
| 小目标精度 | 靠多尺度特征图和高分辨率先验框覆盖 | 明显偏弱 | 单尺度低分辨率特征,浅层细节丢失 |
| 显存占用 | 随先验框数量线性增长 | 随 token 数平方增长 | 全局自注意力 |
| 后处理 | 需要 NMS,阈值敏感 | 不需要 | 密度高时重复框多,误删真框 |
表格里最值得琢磨的是第二行。小目标弱的根因不在 Transformer,而在“单尺度、下采样 32 倍”这个组合:一个 16 像素的物体在特征图上只剩半个像素的响应,位置编码也分辨不出它的精确位置。后续的多尺度版本,比如可变形注意力系列,本质就是把 backbone 的几个层级都接进来,同时把注意力计算限制在稀疏采样点上,让显存别炸。这是所有端到端检测器的共同修补方向,并不是 DETR 一个模型的补丁。
验证这套设计是否真的成立,有个简单的做法:拿训练好的模型,在验证集上对比“带 NMS”和“不带 NMS”两次指标,再看一眼解码器注意力图是否落在目标的有效区域上,最后统计输出里 ∅ 类槽位的占比。如果去掉 NMS 后 AP 不降,注意力图有局部聚集,∅ 占比很高,说明一对一监督确实学到了东西;如果 ∅ 占比很低,说明模型还在硬把每个槽位都塞上物体,就是训练不足的信号。
我自己在这里栽过一次。复现时看到重复框很多,第一反应是加 NMS 兜底,指标居然还小涨了一点,就以为找到了捷径。结果换到密集小目标场景,AP 直接跳水,回头查才发现原来那点涨幅是评估阈值宽松造成的假象,模型本身根本没训到位。从那以后我每次跑端到端检测,都强制走一遍“关 NMS 看指标、可视化 ∅ 占比、抽查注意力图”这三步,再把结论写进实验记录,不再靠一次数字好看下判断。这份文献汇报里把结构图、匹配示意和消融表并排整理在一起,顺着图往下看细节会很省力,想自己动手核对一遍的话,把它拿去对照着读是个不错的起点。希望帮到你。
本文还有配套的精品资源,点击获取