尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Deformable DETR:可变形注意力如何解决目标检测训练与小目标难题

Deformable DETR:可变形注意力如何解决目标检测训练与小目标难题 Transformer 进目标检测这条线从 DETR 到 Deformable DETR中间隔着一道特别现实的坎——训练成本。2020 年 DETR 出来的时候圈子里几乎所有人都在夸它的框架干净没有 anchor、没有 NMS、不需要手工设计先验一个端到端的集合预测就把检测做完了。但真上手去复现的人会很快发现官方配置里写的是 500 个 epoch八卡跑起来差不多要一周而且小目标的 AP 低得让人怀疑人生。Deformable DETR 做的事情本质上是给注意力机制装了一个取景器不让每个查询去看全图而是只让它盯着自己关心的那 K 个采样点。这个改动把训练轮数从 500 压到了 50小目标的表现也翻了身。这篇文章我会从注意力机制本身的计算账讲起拆开 DETR 的两个瓶颈再钻到可变形注意力的公式和实现细节里最后给一套能真正跑起来的训练配置、排查链路和部署取舍。适合已经写过 CNN 检测器、想搞清楚 Transformer 检测这条线到底该怎么用的读者如果你只是想了解大意前面两节也能看懂。1. 一个反直觉的起点Transformer 在检测任务里最先被质疑的三件事1.1 丢掉卷积的归纳偏置为什么反而在检测上成立卷积网络最大的优势是它自带先验局部连接、权重共享、平移等变。这意味着模型不需要从数据里学相邻像素关系更紧密这件事样本效率天然就高。Transformer 几乎没有这种先验位置关系得靠位置编码硬塞进去所以早期在分类任务上ViT 必须先在超大规模数据上预训练直接在小数据集上训是训不动的。那为什么放到检测上这套逻辑就变了我的理解是检测任务里的先验本来就有很大一部分是人工塞进去的也就是 anchor。三个尺度、九种宽高比、几十个 anchor 点这些超参本质上是在用人力去描述目标可能长什么样。DETR 把这部分手工先验拿掉换成每个查询是一个槽位负责描述一个目标这种更弱的约束然后把描述目标这件事交给数据去学。代价是需要更多数据收益是框架干净得多——这一点在后来的 DAB-DETR、DINO 上被进一步验证它们把查询显式地解释成四维 anchor 之后收敛速度又上了一个台阶。另一个容易被忽略的点是自注意力其实是一种数据相关的动态卷积。卷积核的权重在推理时是固定的而注意力的权重是每次前向都根据输入内容重新算出来的。检测场景里目标的大小、位置、遮挡关系变化极大这种动态聚合能力确实比固定卷积核更合适。所以与其说 Transformer 丢掉了归纳偏置不如说它把局部性换成了内容相关性。1.2 自注意力的计算账O(N²) 的代价换来的是全图视野把账算清楚后面很多设计选择就自动能解释通了。标准自注意力的核心是softmax(QK^T / sqrt(d_k)) V其中 Q、K、V 的形状都是(N, d)N 是序列长度。QK^T这一步就是N × d × N次乘加复杂度是 O(N²d)后面的加权求和又是 O(N²d)而生成 Q、K、V 的三个线性投影加起来是 O(Nd²)。关键就在于这两个量谁更大。当 N 远大于 d 的时候N²d 会迅速吃掉一切。拿 DETR 的典型输入算一下800×1333 的图走 stride 32 的骨干网络特征图是 25×42 约 1050 个位置。这时候 N²d 大约是 1050²×256也就是 2.8 亿量级的操作而投影部分是 3×1050×256² 约 2 亿两者还在一个数量级上。但如果换成 stride 16 的特征图N 变成 4200注意力的开销直接变成原来的 16 倍而投影只涨 4 倍差距就拉开了。这解释了 DETR 那个看起来很奇怪的设计——它不但只用单尺度特征还在骨干网络的 C5 后面额外加了一个空洞卷积模块把有效步长从 32 拉到 32 且保持分辨率不涨。目的很直接把 N 摁住。但代价也明摆着一个 32×32 像素的目标在这张特征图上只占一个格子全局注意力再怎么聪明也没法从一个格子里恢复出细节。内存的问题比算力更致命。注意力图QK^T的形状是(N, N)每个头都要存一份。N 取 10000 的时候单头单样本就是 1 亿个浮点数fp32 下 400MB。这就是为什么早期 Transformer 检测模型的分辨率上不去也是 Deformable Attention 要解决的核心痛点。1.3 位置编码与多头两个最容易被当成超参糊弄过去的设计位置编码在 Transformer 检测里不是可有可无的装饰。自注意力本身是置换不变的你把输入序列打乱输出只是跟着打乱内容完全一样。检测任务要的恰恰是空间位置所以位置信息必须显式注入。DETR 用的是固定的正弦位置编码加在编码器输入上解码器的对象查询则是一组可学习的嵌入向量。Deformable DETR 沿用了这套但多了个新东西因为要区分不同层级的特征它额外给每个层级加了一个可学习的level_embed直接加到 value 上让模型知道这一份特征来自哪个尺度。多头注意力则常常被当成调参项随便设成 8。它的实际作用是给模型多个独立的子空间每个子空间可以关注不同的模式。在 Deformable DETR 里多头还有一个更具体的作用——因为每个头只采 K 个点如果所有头的采样位置重合那就等于浪费。所以官方实现里专门设计了一个初始化技巧让 8 个头一开始就朝不同方向看。这个技巧很多人根本不知道但它对前几个 epoch 的收敛速度影响相当明显我在第 3 节会贴出具体代码。2. DETR 的骨架好看但训练要 500 个 epoch 这件事绕不过去2.1 二分图匹配损失把检测改写成集合预测的那一步DETR 最漂亮的地方在于它把检测输出定义成了一个集合然后直接用集合之间的匹配来解决哪个预测对应哪个真值这个问题。具体做法是模型固定输出 N 个预测N 通常取 100真值有 M 个构造一个 N×M 的代价矩阵用匈牙利算法求一个最小代价的一对一匹配。代价函数是分类概率的对数、边界框 L1 距离和 GIoU 三项的加权和。匹配完成之后损失就只在这 M 个匹配对上加分类用交叉熵框回归用 L1 加 GIoU。没被匹配上的预测分类目标就是空这一类。这套机制直接消掉了重复框的问题——因为一对一匹配本身不允许两个查询同时负责同一个真值所以 NMS 这个后处理彻底不需要了。代价矩阵的具体形式是cost -log p(c_i) L1(box) GIoU(box)损失的权重分别是分类 1、L1 为 5、GIoU 为 2。这里有个细节值得注意匹配用的代价和反向传播用的损失不是同一个函数匹配时用的是概率的对数而训练损失里的分类项是交叉熵。这种匹配和训练分离的做法在很多后续工作里被沿用因为它让匹配过程可以更灵活不必对可导性有要求。2.2 收敛慢的根因训练初期注意力权重几乎是均匀的500 个 epoch 这件事官方论文里其实给过分析。他们可视化了不同训练阶段解码器的交叉注意力图发现早期阶段注意力几乎均匀地铺在整个图像上随着训练推进才逐渐聚焦到目标的头部、腿部这些有判别力的区域。也就是说模型需要几十个 epoch 才能学会该看哪里。为什么会这样根因在于注意力的计算方式。softmax的输入是查询和键的点积训练初期这些投影矩阵都是小随机数初始化的点积的数值范围很窄经过 softmax 之后分布就接近均匀。要让分布变尖锐需要投影矩阵的数值增长好几个数量级这个过程靠梯度慢慢磨自然快不了。还有一个更隐蔽的问题匈牙利匹配在训练早期是不稳定的。参数每更新一步代价矩阵就变了匹配结果也跟着变某个查询这一轮负责左边的车下一轮可能被分配去负责右边的人。监督信号这样来回横跳模型很难稳定下来。DINO 后来提出的去噪训练contrastive denoising就是专门治这个病的——直接构造带噪的查询并让它们去重建真值给模型一个稳定的监督信号把收敛压到 12 个 epoch。2.3 小目标漏检的物理原因分辨率和感受野的矛盾小目标的问题说到底是物理层面的信息缺失不是损失函数能救的。前面算过800×1333 的输入在 stride 32 的特征图上只有 25×42 个位置一个 32 像素见方的目标就占一个格子。而这个格子的特征是骨干网络连续下采样五次之后的产物里面已经混进了周围大片背景的信息。全局注意力再怎么加也只是在这个已经糊掉的表示上做加权平均恢复不出原本的细节。更要命的是检测头的注意力本身。DETR 的解码器要给每个对象查询生成一个框而这个框通常是相对于整张图的归一化坐标。当目标的真实尺寸只有图像的 2% 时模型需要输出的坐标精度非常高但注意力在均匀分布的状态下根本提供不了这种定位精度。这也是为什么 DETR 的 AP_S 在当时明显落后于 Faster R-CNN FPN——后者通过 P2、P3 这些高分辨率层级天然保留了小目标的细节。3. 稀疏采样这一步怎么走通的Deformable Attention 的机制拆解3.1 从看全图到只看 K 个点公式里每一项在干什么Deformable Attention 的核心思想可以用一句话概括不是所有位置都值得看让模型自己学着挑几个点就够了。多尺度版本的完整公式长这样MSDeformAttn(z_q, p_q, {x^l}) sum_m W_m [ sum_l sum_k A_mlqk · W_m · x^l(p_q Δp_mlqk) ]这里的符号需要逐个交代清楚。z_q是查询 q 的特征向量p_q是它对应的参考点坐标{x^l}是 L 个层级的特征图。下标 m 是注意力头的编号l 是层级的编号k 是采样点的编号通常是 4 个。Δp_mlqk是第 m 个头、第 l 层、第 k 个采样点相对于参考点的偏移量。A_mlqk是对应的注意力权重它被约束成在所有层级和所有采样点上归一化也就是sum_l sum_k A_mlqk 1。x^l(p)表示在特征图第 l 层上的双线性插值取值因为p_q Δp_mlqk一般是小数坐标。W_m是 value 的投影矩阵W_m是输出的投影矩阵。和标准注意力的区别关键在复杂度上。标准注意力里每个查询要和所有 N_k 个键做点积开销是N_q × N_k。而这里每个查询只和K × L 16个点打交道开销是N_q × 16跟特征图大小完全脱钩。当查询数N_q就是特征图的像素数时整体复杂度对图像分辨率近似线性——这就是它能上高分辨率特征图的根本原因。3.2 参考点、偏移量与权重三个量分别怎么来这三个量的生成方式是整个模块里最需要掰扯清楚的部分。参考点p_q在编码器和解码器里含义不同。在编码器里查询就是特征图上的每个位置参考点就是那个位置归一化到 [0,1] 的坐标官方实现里还会加上半个像素的偏移让坐标落在像素中心而不是角点上。在解码器里参考点是预测框的中心坐标——注意是框而不是点。这就带来一个顺序问题解码器必须先算出框才能算参考点然后才能做可变形注意力。所以解码器层的结构是自注意力 → 框回归头预测框 → 取出框中心作为参考点 → 可变形交叉注意力这个顺序不能乱。偏移量和权重是通过对查询特征做线性投影得到的# 偏移量每个头、每个层级、每个采样点需要 2 个分量x, y self.sampling_offsets nn.Linear(d_model, n_heads * n_levels * n_points * 2) # 权重每个头、每个层级、每个采样点一个标量 self.attention_weights nn.Linear(d_model, n_heads * n_levels * n_points) # 权重在 (层级 × 采样点) 这个维度上做 softmax attention_weights attention_weights.softmax(-1) # 偏移量除以该层级的 (W, H)换算到归一化坐标系 offset_normalizer torch.stack([spatial_shapes[:, 1], spatial_shapes[:, 0]], -1) sampling_locations reference_points sampling_offsets / offset_normalizer这里有个容易踩的坑偏移量没有经过 sigmoid。原版实现是把线性输出的原始值直接除以层级的宽高得到一个以归一化坐标为单位、但范围不受限的偏移。好处是模型可以学到很大的偏移量配合双线性插值的边界处理采样点跑到图外也不会崩。后来的 mmdetection 版本加了 sigmoid 限制范围训练更稳但灵活性下降两者各有取舍。初始化的技巧在这里很值得抄constant_(self.sampling_offsets.weight.data, 0.) # 让 n_heads 个头的初始方向均匀分布在圆周上 thetas torch.arange(n_heads, dtypetorch.float32) * (2.0 * math.pi / n_heads) grid_init torch.stack([thetas.cos(), thetas.sin()], -1) grid_init (grid_init / grid_init.abs().max(-1, keepdimTrue)[0]) grid_init grid_init.view(n_heads, 1, 1, 2).repeat(1, n_levels, n_points, 1) for i in range(n_points): grid_init[:, :, i, :] * i 1 # 同一头内的 K 个点沿同一方向拉开距离 with torch.no_grad(): self.sampling_offsets.bias nn.Parameter(grid_init.view(-1))把权重置零、偏置设成这个圆周分布意味着训练刚开始时8 个头各自朝 8 个不同方向采样同一头内部的 4 个点沿着该方向由近及远排列。这个初始化让模型一上来就有多方向、多尺度的观察能力比全零初始化收敛快不少。我在自己搭的小数据集上对比过去掉这个初始化前 5 个 epoch 的 loss 下降明显更慢。3.3 多尺度版本如何把 FPN 的活接过来Deformable DETR 用的是 4 个层级的特征。前三个来自 ResNet 的 C3、C4、C5步长分别是 8、16、32第四个是在 C5 上再做一个步长 2 的 3×3 卷积得到的有效步长 64。每个层级的通道数先用 1×1 卷积统一投影到 256 维。然后是最关键的排布方式把这 4 个层级的特征在空间维度上展平再沿着序列维度拼接成一条长序列一起送进编码器。同时记录两个辅助张量——spatial_shapes保存每层的 (H, W)level_start_index保存每层在长序列里的起始位置。有了这两个东西可变形注意力在做双线性插值时就能准确定位到某个层级上的某个位置。这套设计实际上是把 FPN 的功能吃掉了。FPN 的做法是把高层语义特征上采样后和低层特征相加是一种手工设计的融合路径而这里不做任何融合直接让注意力去跨层级采样融合过程完全交给学习。好处是省掉了 FPN 那一堆额外的卷积层坏处是模型必须自己学会什么时候该看高层、什么时候该看低层。他们加的那个level_embed就是给模型的一个提示告诉它当前 value 来自哪个尺度。从实测效果看这套设计对小目标的提升非常直接。论文里的数字大致是ResNet-50 骨干、50 个 epoch 的配置下整体 AP 能到 43~44 左右已经超过了训 500 个 epoch 的 DETR叠加迭代式框回归能到 45 出头再上两阶段变体能到 46 左右。具体数字不同版本会有浮动以官方配置为准但趋势是稳的。3.4 感受野和计算量的账为什么它敢上高分辨率算一下复杂度。可变形注意力里每个查询需要做的操作是生成偏移量和权重两次线性投影O(d²)、K×L 个点的双线性插值每个点是常数开销、然后是对 K×L 个采样值做加权求和。所以单查询的开销是 O(d² KL·d)和特征图大小无关。总的复杂度是 O(N_q · (d² KL·d))对特征图空间尺寸完全线性。内存方面更明显。标准注意力要存(N_q, N_k)的注意力图可变形注意力只需要存(N_q, M, L, K)的采样坐标和权重K 取 4、L 取 4、M 取 8 的话每个查询只有 128 个数。N_q 取 10 万对应较高的分辨率也只是 1280 万个浮点数几十 MB 的事。当然稀疏采样也有代价。感受野变成了学习出来的不再是理论上全图可达。如果某个目标需要跨越大半个图像才能找到关联可变形注意力就得靠多层堆叠来传递信息——6 层编码器、每层 4 层级的采样理论上能够覆盖很大范围但实际能不能学到取决于数据和训练。这也是为什么 Deformable DETR 在需要长距离关系推理的任务上比不过全局注意力但在通用检测这种局部特征为主的任务上反而赢了。4. 把训练脚本跑通环境、编译与配置里的具体数字4.1 可变形注意力算子的编译和那些反复出现的报错原版的可变形注意力是用 CUDA 写的自定义算子推理前必须先编译。流程本身很简单cd models/ops sh ./make.sh # 验证是否编译成功并正确注册 python test.py # 全部输出 True 才算过真正折磨人的是编译失败的排查。下面这张表是我和同事踩过的总结按出现频率排序报错信息根因处理方式undefined symbol: _ZN2at...PyTorch 和扩展的 C ABI 不一致setup.py 里加-D_GLIBCXX_USE_CXX11_ABI0或与新 ABI 保持一致nvcc fatal: Unsupported gpu architecture显卡架构超出 nvcc 支持范围设export TORCH_CUDA_ARCH_LIST7.0;7.5;8.0;8.6只编译需要的架构identifier AT_CHECK is undefinedPyTorch 版本较新旧 API 被移除把AT_CHECK全局替换成TORCH_CHECKerror: xxx is not a member of at::cudaCUDA 版本和 PyTorch 编译时用的 CUDA 不一致torch.version.cuda和nvcc --version必须对齐编译过了但 forward 输出全 0输入张量不连续或spatial_shapes在 CPU 上输入全部.contiguous()辅助张量统一放到同一 device半精度下报类型错误原版 kernel 只支持 float32要么改 kernel要么在推理时切回 fp32从效率和可维护性考虑我现在的做法是如果只是做实验直接编译官方算子性能最好如果是要长期维护或者要导出部署就换成纯 PyTorch 的 grid_sample 实现慢一些但省心得多。这个回退实现在 4.4 节后面会给。4.2 数据准备COCO 格式里几个不太显眼的坑数据这块本身不难但有几个坑几乎人人都会踩一次。标准目录结构是这样的data/coco/ ├── annotations/ │ ├── instances_train2017.json │ └── instances_val2017.json ├── train2017/ └── val2017/第一个坑是类别数的91 和 80。COCO 的 JSON 里类别 id 是 1 到 90 之间的不连续整数但实际有效类别只有 80 个。很多官方代码在构建类别映射时用的是最大 id 加一也就是 91然后靠一个映射表把有效 id 压到 0~79。如果你自己清洗过数据集直接把 id 重排了那这个映射就会错位——表现是模型训练完全正常但验证时所有类别的 AP 都是零。第二个坑是验证速度。COCO 的完整评估要跑几千张图单卡跑一次十几分钟。调试阶段我一般会在配置文件里把验证集换成一个几百张的子集等配置确认没问题了再跑全量。省下来的时间相当可观。第三个坑是坐标归一化。DETR 系列要求框坐标在送入损失函数之前是归一化到 [0,1] 的而 COCO 标注里的框是绝对像素坐标加上左上角宽高格式。官方代码里有一个NormalizedXYWH的变换链负责转换。如果你自己写数据加载器一定要注意这里的顺序先转成中心点加宽高再归一化顺序反了结果会错得很隐蔽。4.3 50 个 epoch 的训练配置学习率、损失权重和梯度裁剪配置这块我直接给一份能用的对照表配置项取值说明epochs50第 40 个 epoch 时学习率降为原来的 0.1优化器AdamW骨干网络用 SGD 也可以但 AdamW 更省心基础学习率2e-4骨干网络单独用 2e-5低十倍权重衰减1e-4偏置项和归一化层参数不加权重衰减单卡 batch size28 卡合计 16显存 24G 够用梯度裁剪max norm 0.1这个值很关键不要漏分类损失系数2用的是 focal lossalpha 取 0.25L1 损失系数5用于框回归GIoU 损失系数2用于框回归对象查询数300比 DETR 的 100 多因为匹配更稀疏两个地方需要特别注意。梯度裁剪的阈值 0.1 看起来很小但这是官方配置实测确实需要——Deformable DETR 在训练早期梯度范数会突然冲高不裁剪的话很容易出现 loss 变成 NaN。另一个是分类损失用的是 sigmoid 加 focal loss而不是 softmax 加交叉熵。这意味着每个查询对每个类别独立做二分类匹配时用的代价函数也相应换成了 focal loss 的形式。这个改动是论文里明确提到的很多复现版本漏掉了结果就是收敛变慢。4.4 迭代式框回归和两阶段变体什么时候值得开这两个是 Deformable DETR 提升 AP 的两个主要增量但都不是默认必须要开的。迭代式框回归的思路是让解码器的每一层都在前一层预测框的基础上做一次精修。第 i 层的框是这样来的b_i sigmoid(Δb_i inverse_sigmoid(b_{i-1}))也就是先把上一层的框反 sigmoid 变换到实数域加上一个预测出的偏移量再 sigmoid 回来。这里有个关键细节——参考点必须 detach也就是梯度不能通过参考点回传到上一层。如果不 detach梯度会沿着层间路径反传训练会非常不稳定。这个坑我在第一次自己实现的时候踩过loss 会莫名其妙地跳。两阶段变体则是让编码器的输出先接一个检测头产生一批粗略的候选框取分数最高的若干个作为解码器的参考点和查询初始化。因为参考点是从编码器特征里直接预测出来的天然带有位置信息比随机初始化的查询要好得多小目标的收益尤其明显。代价是多了一个阶段的计算量训练速度会慢一到两成。我的建议是验证框架是否跑通的时候先只开迭代式框回归等基线稳定了再上两阶段。如果数据集里小目标占比较高两阶段几乎是必开的。5. 实测踩坑记录不收敛、漏检、显存爆掉分别怎么排查5.1 loss 卡住不动时的完整排查链路遇到训练不收敛最忌讳的是漫无目的地调学习率。我现在的排查顺序是固定的从最便宜的开始。第一步把损失拆成四个分量分别打印分类损失、L1 损失、GIoU 损失、匹配数。分类损失一直不下降问题在分类头或者标签分配L1 和 GIoU 不动问题在框回归或者坐标格式匹配数一直是 0那基本可以确定是数据加载出了问题。第二步检查匹配数。正常情况下匹配数应该等于这个 batch 里真值的总数。如果明显偏小说明很多真值没被匹配上原因通常是类别映射错位回想 4.2 节那个 91 和 80 的坑或者坐标归一化错了。如果匹配数一直是 0那必然是数据问题不用再怀疑模型。第三步检查输入归一化。Deformable DETR 沿用了 ImageNet 的均值和方差做归一化如果你换成自己的数据集统计值要确保训练和推理一致。这一步错了的表现是 loss 在下降但验证 AP 上不去。第四步看梯度范数。如果梯度范数长期在几十以上说明学习率偏高或者需要更强的裁剪如果长期接近零那可能是梯度在某处断掉了检查一下参考点的 detach 是不是加错了位置。第五步确认数据加载器真的读到了标注。这一步听起来很蠢但我遇到过两次——一次是路径配错了导致加载了空标注另一次是缓存文件过期。打印一下每个 batch 的框数量分布能省下大量时间。5.2 小目标召回上不去我试过的四件事小目标是个系统工程没有单点解决方案。按性价比排序我实际试过的做法有这些。提高输入分辨率是最直接的一招。把短边从 800 提到 1000 甚至 1200小目标 AP 通常能有明显提升因为目标在特征图上的占格数直接翻了。代价是显存和耗时都按平方增长24G 卡上 1200 的分辨率基本是上限。多尺度训练是第二招。训练时随机在几个尺寸之间切换让模型对不同尺度的目标都见过。这一招不需要额外显存只是让训练时的尺寸抖动起来收益稳定但幅度不如直接提分辨率。第三招是调整特征层级。默认是从 C3 开始的如果目标特别小可以考虑再加一层 stride 4 的特征或者把 C3 的通道数提上去。加层级的代价是序列长度增加训练变慢。第四招是开两阶段变体。前面说过两阶段的参考点是从编码器特征里预测出来的小目标本来就容易在编码器输出上留下响应所以参考点更容易落到小目标上。我的实测里单阶段切到两阶段小目标 AP 大概能涨 2 个点左右是所有招里收益最明显的。5.3 部署阶段算子导出和量化时不得不做的取舍训练跑通不难难的是部署。核心问题在于那个自定义 CUDA 算子——ONNX、TensorRT、各种推理引擎都不认识它必须替换成等价的标准算子组合。好在可变形注意力的计算过程可以拆解成几步标准操作算采样坐标、按层级做双线性插值、再按权重加权求和。双线性插值正好对应grid_sample加权求和就是普通矩阵乘法两者在主流推理框架里都有。下面是一个简化版的纯 PyTorch 回退实现思路和导出时用的完全一致import torch import torch.nn.functional as F def deform_attn_forward(value, spatial_shapes, sampling_locations, attention_weights, n_points): value: (bs, sum(H*W), n_heads, head_dim) spatial_shapes: List[(h, w)]每个层级的特征图尺寸 sampling_locations: (bs, n_q, n_heads, n_levels, n_points, 2)值域 [0, 1] attention_weights: (bs, n_q, n_heads, n_levels, n_points) bs, n_q, n_heads, n_levels, _, _ sampling_locations.shape head_dim value.shape[-1] split_sizes [h * w for h, w in spatial_shapes] value_list value.split(split_sizes, dim1) sampled_levels [] for lvl, (h, w) in enumerate(spatial_shapes): v value_list[lvl].permute(0, 2, 1).reshape(bs * n_heads, head_dim, h, w) # 归一化坐标 [0,1] 映射到 grid_sample 要求的 [-1,1] grid sampling_locations[:, :, :, lvl] * 2 - 1 grid grid.permute(0, 2, 1, 3, 4).reshape(bs * n_heads, n_q, n_points, 2) out F.grid_sample(v, grid, modebilinear, padding_modezeros, align_cornersFalse) sampled_levels.append(out.view(bs, n_heads, head_dim, n_q, n_points)) sampled torch.stack(sampled_levels, dim4) # (bs, heads, dim, n_q, levels, points) weights attention_weights.permute(0, 2, 1, 3, 4) # (bs, heads, n_q, levels, points) weights weights.unsqueeze(2) # 广播到 head_dim 维度 out (sampled * weights).sum(dim(4, 5)) # 层级和采样点维度求和 out out.permute(0, 3, 1, 2).reshape(bs, n_q, n_heads * head_dim) return out这段代码的正确性可以用官方算子做对拍验证随机生成输入两个实现的结果差应该在一两个浮点误差范围内。导出 ONNX 的时候要注意grid_sample需要 opset 16 及以上低版本不支持align_cornersFalse这个组合。量化的取舍更麻烦。原版 kernel 只支持 fp32而且双线性插值对量化误差很敏感——采样坐标稍微偏一点取到的就是完全不同的特征。我的经验是可变形注意力这部分尽量保持 fp32只对骨干网络做量化这样精度损失可控。整体速度提升会打折但比为了全量化把 AP 掉好几个点要划算。6. 放在变体谱系里看Deformable Attention 与相邻工作的关系6.1 和可变形卷积、稀疏注意力、FPN 的对照不少人第一次看到 Deformable Attention 会觉得眼熟因为它的思路和可变形卷积Deformable Conv非常像。两者的共同点都是让网络自己学采样位置但粒度不同可变形卷积是在卷积核的固定网格上加偏移采样点数等于卷积核大小是局部的可变形注意力是在参考点周围学偏移采样点数由 K 控制而且权重也是学出来的聚合方式是加权的而不是卷积核加权。可以说它是可变形卷积在注意力框架里的一个推广。方案采样方式复杂度对分辨率训练轮数小目标表现Faster R-CNN FPN密集 anchor与 anchor 数相关数十轮好DETR全局稠密注意力平方约 500 轮较差Deformable DETR稀疏可学习采样近似线性约 50 轮好DAB-DETR / DINO四维 anchor 加去噪同可变形注意力12 到 36 轮更好和稀疏注意力比如各种局部窗口注意力的区别也值得说一句。Swin 那种做法是固定窗口位置是硬的优点是实现简单、硬件友好可变形注意力的采样位置是软的、学出来的表达能力强但要求自定义算子。工程上选哪个很大程度取决于你能不能接受底层算子。和 FPN 的关系前面提过它其实是把多尺度融合这件事从手工设计变成了学习。这也是为什么后来的检测器普遍不再单独强调 FPN——多尺度特征还在但融合方式换了。6.2 ViT、Swin、DINO 这条线上的位置如果把视觉 Transformer 画成一棵树Deformable DETR 属于编码器-解码器检测框架这一支和纯骨干网络改进的 ViT、Swin 不在同一个分支上但两者是能组合的。Deformable DETR 用的骨干网络是 ResNet后面有人换成 SwinAP 又能涨几个点。这说明它解决的问题注意力的计算效率和收敛速度和骨干网络解决的问题特征提取能力是正交的。DINO 这条线则是沿着 Deformable DETR 往前走保留了可变形注意力作为底层算子在上面加了对比去噪训练、混合查询选择、以及基于 look-forward-twice 的框更新策略。从工程角度看如果你今天要做一个新的检测项目直接从 DINO 或它的后续版本起步比从 Deformable DETR 起步要省事——但前提是你理解可变形注意力这一层否则出了问题时根本不知道从哪查。6.3 如果今天重新选型我会怎么决定回到最实际的问题一个新项目该选什么。我自己的判断标准大致是这样几条。如果数据集不大比如几千张图以内我会优先考虑带强预训练权重的方案因为 DETR 系列的收敛依赖数据量。这种情况下用 DINO 系列配大规模的预训练骨干能省下大量训练时间。如果数据里有大量小目标比如遥感、工业质检这类场景那么多尺度可变形注意力基本是必选项同时要把输入分辨率提上去并且开两阶段。单纯堆骨干网络的容量收益很小。如果部署环境受限比如只能跑在通用推理引擎上那要提前评估自定义算子的替换成本。前面给的 grid_sample 回退方案能用但速度会掉需要提前做性能预算。最后说个我自己的体会。刚开始接触 Deformable DETR 的时候我最想不通的是为什么它的初始化要写得那么绕——又是余弦正弦又是按序号放大。后来自己从零实现了一遍把那个初始化换成全零看着前几个 epoch 的 loss 曲线慢慢爬才真正明白那几行代码值多少钱。有些设计在论文里只有半句话但它背后是作者试过无数次才留下来的动手复现一遍比读十遍论文管用。
返回列表