ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

DETR目标检测:Transformer如何用集合预测取代NMS与anchor

DETR目标检测:Transformer如何用集合预测取代NMS与anchor 第一次看到 DETR 这篇论文时我盯着标题里的 DEtection TRansformer 看了很久。那是 2020 年Transformer 在自然语言处理领域已经封神但把它搬进目标检测还是让很多人觉得这是一次大胆的试水。毕竟过去十年检测领域被 CNN 和 anchor 体系统治得太久了。直到我自己动手跑通 DETR 的推理和训练才真正意识到它的价值不在于某一个精度数字的提升而在于它把目标检测重新定义成了一个集合预测问题让检测头第一次摆脱了 NMS非极大值抑制、anchor 设计这些手工组件的束缚。这篇文章我会从 DETR 的设计思路、Transformer 架构在其中的工作原理、如何用自己的数据训练 DETR再到实际训练中踩过的坑和后续改进方向完整梳理一遍。适合刚接触 DETR 的读者、想用 Transformer 做视觉任务的工程师以及在目标检测领域做技术选型的研究人员。看过之后你至少能明白 DETR 到底改了什么、为什么这么改、以及它凭什么能在检测领域站稳脚跟。1. DETR 在检测任务里到底改了什么从 NMS 到集合预测1.1 传统检测器的“手工设计”困境在聊 DETR 之前得先搞清楚一个前提传统检测器为什么让研究者头疼。以 Faster R-CNN 为代表的 two-stage 方法要先通过 RPN 生成候选框再对候选框做分类和回归以 YOLO、SSD 为代表的 one-stage 方法则依赖在特征图上预定义的 anchor 框通过回归 anchor 与真实框的偏移来预测目标。这里的核心问题在于anchor 的尺度、长宽比、数量都需要针对数据集精心设计COCO 上表现好的参数换了数据集可能要重新调而 NMS 后处理本身也带着一堆超参数比如 IoU 阈值选 0.5 还是 0.7对最终结果影响非常大。这些设计在工程上被验证了无数次很稳定但始终绕不开一个尴尬检测框的数量是动态变化的而模型输出的候选框是固定且冗余的。于是我们必须在最后一步通过 NMS 把重叠的框合并掉。NMS 这个操作本身不可微它是检测流程里的一个“外挂”你没法让 loss 直接告诉模型“你的框和另一个框重复了你应该主动让位”。整个系统是碎裂的前段用深度网络学特征中段用各种手工规则筛框后段再接一个不可微的处理逻辑。我记得第一次在实习项目里调 NMS 阈值时发现目标密集场景下阈值稍微调低一点漏检率就明显上升调高一点又出现大量重复框。那种感觉就像你在做饭时发现菜谱里少写了一步关键调味只能凭感觉试。DETR 所做的正是要彻底抹掉这套手工后处理。1.2 把检测重新定义为集合预测DETR 的核心视角非常朴素检测任务的本质是给定一张图片输出一个“目标集合”集合里每个元素包含类别和边界框坐标。既然是集合就应该没有顺序概念也不应该有重复元素。传统检测器输出的是一堆带冗余的框所以需要 NMS 去冗余DETR 则希望设计一个模型让它端到端地直接输出一个干净集合。这个思路的转化很有意思。你不再预测一堆可能重叠的候选框而是固定预测 N 个框DETR 里默认 N100并让模型学习如何把这 100 个预测对应到图里的真实目标上。如果图里只有两个目标剩下的 98 个预测都要学会输出“无目标”用一个特殊的 no object 类别表示。这就是集合预测的直观含义。但这里有个关键问题模型输出的 100 个预测是无序的你怎么定义它和真实目标的匹配关系总不能随便把第一个预测和第一个真实框比较吧。DETR 的答案是用二分图匹配。我拿生活中举个例子如果 5 个实习生要分配 5 个任务每个人的技能熟练度不同如何让总效率最高这就是一个典型的指派问题可以用匈牙利算法求解。DETR 在训练时也做同样的事用匈牙利算法找出预测框和真实框之间代价最小的匹配组合然后只对被匹配上的预测计算损失。二分图匹配的引入本质上是把“模型自己学会去重”这件事交给了 loss。因为匹配过程是一对一的同一个真实目标只会被一个预测框匹配模型为了降低 loss就必须学会让多个预测框不要同时瞄准同一个目标。这也是 DETR 推理时不需要 NMS 的根本原因。1.3 匹配代价与损失函数的设计细节匹配代价函数的设计直接决定了训练时模型看到的“反馈信号”。DETR 的匹配代价由三部分组成分类的负对数概率、边界框的 L1 距离、以及 GIoU 损失。注意匹配阶段用的代价与最终的训练损失可以不同这给了设计者很大的自由度。为什么用 L1 距离还要叠加 GIoU单看 L1 距离有个问题它对大框和小框的误差尺度不敏感。一个 500×500 的大框偏移 20 个像素和一个 50×50 的小框偏移 20 个像素L1 距离完全一样但后者显然是更严重的错误。GIoU 则能反映两个框的重叠程度和形状差异它对尺度不敏感更适合衡量检测框的定位质量。所以在匹配代价里DETR 用了权重为 2 的 L1 损失加权重为 1 的 GIoU 损失让分类、定位和形状约束共同参与最优匹配的决策。最终训练损失同样包含三部分分类用交叉熵框回归用 L1 加 GIoU。这里有一个小细节对于匹配到 no object 的预测分类损失会计算但框回归损失会被忽略因为那些“无目标”的预测并没有有意义的框坐标。这个处理让模型可以安心地学会“不输出任何框”而不是为了强行预测一个框而制造噪声。对比维度传统检测器Faster R-CNN / YOLO 等DETR候选框生成需要 RPN 或 anchor 预设无需 anchor直接集合预测后处理需要 NMS 且不可微无需 NMS匹配天然去重检测头结构多分支、需要手工设计Transformer 编解码统一处理训练管线多阶段/多任务、相对复杂端到端一次性训练对数据集适配anchor 参数需重新设计无需 anchor语义建模更通用2. Transformer 架构在 DETR 里怎么工作编码器与解码器的分工2.1 自注意力机制为什么适合做全局上下文建模要理解 DETR就必须理解 Transformer 的核心组件——自注意力机制。当年我第一次看 Attention Is All You Need 这篇文章时最困惑的是 Q、K、V 这些概念到底在干什么。后来我找到一个比较容易理解的类比假设你在一个非常拥挤的教室里找人你手里有一张朋友的照片这就是 Query你逐个看教室里每个人的脸每个人的脸就是 Key当某张脸和照片比较像时你会在心里多留意几分算出一个注意力权重然后把对应的人的信息Value放进你的记忆里最终综合所有人的信息形成“我要找的人在哪个方向”的判断。在 DETR 的编码器里图片被切成很多个“块”patch或者说是特征图上的一个位置每个块都对应一个特征向量。自注意力让每个块都能去看全局所有块的信息并加权聚合。这个特性非常关键传统 CNN 的感受野是逐步扩大的底层特征只能看到局部区域如果一个目标被另一个目标部分遮挡局部特征往往不够用而自注意力一步到位让每个位置都能感知到全图的上下文。DETR 用 Transformer 编码器的另一个隐藏优势在这里体现得很明显它天然适合处理遮挡和重叠目标。比如检测人群场景里的多个人彼此身体重叠严重时CNN 特征容易混淆边界而 Transformer 编码器通过自注意力可以建模“人与周围环境”的长程依赖关系让模型理解“这里虽然叠了一块但其实是两个不同的人”。2.2 编码器CNN backbone 与空间位置编码的结合DETR 不是把原始像素直接丢给 Transformer 的而是先用一个 CNN backboneResNet-50 或 ResNet-101提取特征图。假设输入图片是 800×800经过 ResNet 的 C5 层特征图会缩小到 25×25缩放倍数为 32。这个特征图每个位置代表原图一个 32×32 区域的语义信息通道数通常是 2048。接下来要把二维特征图转换成一维序列。DETR 做的很简单把 25×25 的空间维度直接展平成 625 个位置每个位置的维度是 2048。经过一个 1×1 卷积把通道降到 d_model256就得到了 Transformer 编码器的输入序列。这里要特别注意Transformer 本身是顺序不敏感的如果直接把展平的特征输入模型完全不知道这些特征在原图上的空间位置关系。所以必须在输入端加入位置编码。DETR 使用的是空间位置编码spatial positional encoding它不是对序列顺序编码而是对二维空间坐标编码。具体做法是分别生成一个高方向的编码和一个宽方向的编码然后把它们拼接到通道维上。我稍后会详细说明位置编码的计算过程。这个设计很聪明它让编码器在计算注意力时既能感知特征内容又能感知特征在图像中的相对位置相当于同时看到了“这是什么”和“它在哪里”。2.3 解码器object queries 是 DETR 里最精髓的设计很多初学者看 DETR 的代码时最看不懂的就是解码器输入的 object queries对象查询。它其实是一组可学习的参数形状是 100×256代表 100 个随机初始化的查询向量。在训练过程中模型会慢慢把这 100 个向量训练成“100 个不同目标的探测器”。我习惯把 object queries 理解为“一组带着任务清单的调查员”。每个调查员都有一个固定的调查目标比如一个负责“找图中左上角的大型物体”另一个负责“找图中中间偏右的小型物体”他们拿着自己的问题清单query去图片特征里逐一比对cross-attention找到最符合自己问题的区域然后汇报结果。解码器内部有两层注意力第一层是 self-attention让 100 个 query 之间相互沟通避免多个 query 重复关注同一个目标第二层是 cross-attention让每个 query 去查询编码器输出的图像特征。两层配合效果就是 100 个 query 在学习的过程中自动分化每个 query 负责图里的一种目标。这也是为什么 DETR 不需要 NMS当某个 query 选中了一个目标其他 query 在 self-attention 阶段就会收到“这里已被占据”的信号从而转向其他区域。刚入门的人可能会问为什么不直接把 100 个 object queries 设成 anchor 的语义版本其实二者有本质区别。anchor 是硬编码的、固定的几何先验object queries 是数据驱动的、可学习的语义先验。模型根据训练数据自己决定这 100 个查询应该关注什么样的尺度、位置和类别组合适应性更强。2.4 位置编码 PE 计算与输入嵌入表示顺着热词里反复出现的“嵌入表示层 PE 计算”说明一下。Transformer 的位置编码最常见的方案是三角函数绝对位置编码公式如下对于位置 pos 和维度 id_model 为特征维度当 i 为偶数时PE(pos, 2i) sin(pos / 10000^(2i / d_model))当 i 为奇数时PE(pos, 2i1) cos(pos / 10000^(2i / d_model))用 Python 实现一段可以直接跑的位置编码生成代码import numpy as np def positional_encoding(max_len, d_model): pe np.zeros((max_len, d_model)) position np.arange(0, max_len).reshape(-1, 1) div_term np.exp(np.arange(0, d_model, 2) * -(np.log(10000.0) / d_model)) pe[:, 0::2] np.sin(position * div_term) pe[:, 1::2] np.cos(position * div_term) return pe # 生成一个长度为 50、维度为 256 的位置编码 pe positional_encoding(50, 256) print(pe.shape) # (50, 256)这段代码的关键点在于 div_term 的构造。np.arange(0, d_model, 2)生成 0, 2, 4... 等偶数索引分母上的 10000^(2i/d_model) 可以通过exp(2i * -(log(10000) / d_model))变换得到数值上比直接算 10000 的幂次更稳定。这样每位位置都能获得一个唯一的编码向量不同位置的编码向量之间有明确的相对关系。为什么用不同频率的正弦余弦组合而不是直接用整数索引作为编码因为整数编码的数值范围容易随序列长度变化且相邻编码之间的差值不规整而正弦余弦函数的值域固定在 [-1, 1]且不同频率的组合可以让模型更容易学到相对位置关系。你可以这样理解这个编码就像给每个位置发放了一张带有坐标刻度的地图地图上标明了“我是第几个位置”而且这种标注方式对任意序列长度都有效。DETR 里的空间位置编码本质上也是用这个思路生成的只不过增加了一个维度。代码里常见的做法是分别生成 height 方向和 width 方向的位置编码再用repeat_interleave或拼接的方式组合起来最终加到像素特征上。这一步虽然是“一行代码”的事但直接决定了检测精度很多人训练 DETR 效果不好回头检查发现是位置编码的广播维度出错了。3. 实操用自己的数据训练和推理 DETR3.1 环境准备与依赖安装DETR 官方代码是基于 PyTorch 的配置环境并不复杂。建议使用 Python 3.8 以上PyTorch 1.7 以上都可以。安装依赖pip install torch torchvision pip install scipy pip install pycocotoolsDETR 训练时需要 COCO 格式的数据集所以 pycocotools 是必需品。这里有个小坑Windows 上直接安装 pycocotools 经常报编译错误建议安装pycocotools-windows的预编译版本或者干脆使用 Linux 环境做训练。我自己第一次在 Windows 上配环境浪费了大半天后面果断换到 Linux 服务器上跑几分钟就搞定了。3.2 数据准备从自定义标注到 COCO 格式DETR 的训练脚本默认读取 COCO 格式的 JSON 标注。COCO 格式比较复杂但核心就三个字段images每张图的 id、宽高、文件名、annotations每个目标的 image_id、类别 id、bbox、categories类别 id 到名称的映射。如果你用的是 LabelImg 或 labelme 标注的数据通常得到的是 Pascal VOC 格式或单张图的 JSON。你需要写一个转换脚本把标注统一转成 COCO 格式。一个简化的转换逻辑如下import json import os from PIL import Image def convert_to_coco(images_dir, annotations_dir, classes, output_path): coco {images: [], annotations: [], categories: []} for idx, cls in enumerate(classes, start1): coco[categories].append({id: idx, name: cls}) ann_id 1 for img_id, filename in enumerate(os.listdir(images_dir), start1): if not filename.lower().endswith((.jpg, .png)): continue img Image.open(os.path.join(images_dir, filename)) w, h img.size coco[images].append({ id: img_id, file_name: filename, width: w, height: h }) # 这里假设每个图片对应一个同名的 labelme json 文件 label_file os.path.join(annotations_dir, filename.rsplit(., 1)[0] .json) if not os.path.exists(label_file): continue with open(label_file, r, encodingutf-8) as f: label_data json.load(f) for shape in label_data[shapes]: # labelme 坐标是 [x1, y1, x2, y2] 的形式 x1, y1 shape[points][0] x2, y2 shape[points][1] cx, cy, bw, bh (x1 x2) / 2, (y1 y2) / 2, x2 - x1, y2 - y1 cls_name shape[label] cls_id classes.index(cls_name) 1 coco[annotations].append({ id: ann_id, image_id: img_id, category_id: cls_id, bbox: [cx, cy, bw, bh], area: bw * bh, iscrowd: 0 }) ann_id 1 with open(output_path, w, encodingutf-8) as f: json.dump(coco, f, indent2)一个我踩过坑的细节COCO 的 bbox 格式是[x_center, y_center, width, height]不是左上角和右下角。DETR 的代码在处理预测框时输出的是中心点加宽高的归一化坐标所以数据标注阶段保持统一格式可以省掉很多麻烦。如果你的数据集和目标检测无关只是想快速验证 DETR 流程可以直接下载 COCO 2017 的一个子集或者用你自己已有的任何目标检测数据集转换成上述格式即可。3.3 训练参数与损失计算过程DETR 官方训练命令是这样启动的python -m torch.distributed.launch --nproc_per_node8 --use_env main.py \ --coco_path /path/to/coco \ --output_dir /output/dir \ --batch_size 2 \ --lr 1e-4 \ --weight_decay 1e-4 \ --epochs 500 \ --lr_drop 400这些参数看着简单但每个都值得唠唠。--lr 1e-4是官方针对 batch size328 卡×每卡 2 张设计的。如果你只有单卡、每张图片的训练 batch size 是 2那总 batch size 变成了 2学习率还按 1e-4 用训练大概率会不稳定。我采用的经验公式是让学习率随 batch size 做线性缩放lr 1e-4 × (batch_size / 32)比如单卡 batch size 为 2 时学习率取 6.25e-6 左右起步比较安全。这不是什么高深理论而是分布式训练里常见的“线性缩放规则”梯度是多个样本的平均batch size 越小梯度噪声相对越大过大的学习率会导致 loss 震荡。--epochs 500是 DETR 的一个显著特征。它不像 YOLO 那样 300 epoch 以内就能出不错的效果DETR 收敛确实更慢官方实验需要 500 epoch 才能达到 42 AP 的水平。训练中途你会发现 loss 下降得很慢可能在 100 个 epoch 时还看起来一般这是 Transformer 训练的正常现象。--lr_drop 400表示在第 400 个 epoch 时学习率衰减 10 倍帮助 loss 在后期进一步收敛。训练过程中你可以在日志里看到类似这样的输出Epoch 0: loss 12.34, loss_ce 3.56, loss_bbox 4.12, loss_giou 4.66 Epoch 50: loss 6.12, loss_ce 1.80, loss_bbox 2.05, loss_giou 2.27loss_ce 是分类损失loss_bbox 是 L1 回归损失loss_giou 是 GIoU 损失。观察这三个分量的变化可以判断训练状态如果 loss_ce 降不下去但 bbox 在降可能是类别定义或数据标注有问题如果 loss_giou 震荡剧烈可能是学习率偏大。3.4 推理与可视化推理是感受 DETR 能力最直观的方式。用 torch.hub 可以直接加载官方预训练模型import torch import torchvision.transforms as T from PIL import Image model torch.hub.load(facebookresearch/detr:main, detr_resnet50, pretrainedTrue) 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) inputs transform(img).unsqueeze(0) with torch.no_grad(): outputs model(inputs) logits outputs[pred_logits] # (1, 100, 92) boxes outputs[pred_boxes] # (1, 100, 4)这里的pred_logits形状是 (1, 100, 92)100 是 object queries 数量92 是 COCO 的 91 个类别加一个“无目标”类。pred_boxes是归一化到 [0, 1] 的坐标格式为 (center_x, center_y, width, height)。从输出到可视化还需要做一步根据类别置信度过滤低分框。这一步非常轻量只需要取每个 query 预测概率最大的类别然后过滤掉概率低于阈值的结果完全不需要 NMS。我第一次跑通时特意对比了有 NMS 和无 NMS 的差异发现结果几乎一致这让我真正理解了集合预测“天然去重”的设计意图。4. 训练 DETR 最容易踩的坑与改进方向4.1 收敛慢、小目标检测差DETR 最大的槽点DETR 在 COCO 上的 AP 能达到 42 左右但训练时长是传统检测器的数倍。500 epoch 的训练在 8 张 V100 上也要跑好几天。更让人头疼的是小目标检测效果DETR 在小目标上的 AP 明显低于 Faster R-CNN。原因不难理解ResNet 的 C5 特征图只有原图的 1/32 分辨率一个小目标可能只占几个像素经过下采样后在特征图上几乎消失了。自注意力虽然能看全局但前提是信息还在特征图里如果信息在主干阶段就丢了再强的注意力也救不回来。改进方法也很明确一是用更高分辨率的输入或者用类似 FPN特征金字塔的结构保留多尺度特征二是改用 Deformable DETR它对多尺度特征图做可变形注意力在 50 个 epoch 内就能超过 DETR 的 300 epoch 效果。如果你做业务落地我建议直接以 Deformable DETR 作为起点省时省力。4.2 显存占用过高和注意力矩阵的平方复杂度Transformer 的自注意力复杂度是序列长度的平方。在 DETR 里如果特征图是 25×25序列长度 625注意力矩阵就是 625×625这还可以接受但如果输入分辨率增加到 1600特征图变成 50×50序列长度 2500注意力矩阵直接膨胀到 2500×2500显存压力呈指数级上升。训练时遇到 OOM显存不足最直接的解决办法是降低输入分辨率或减小 batch size。但降分辨率会损害小目标检测性能所以更建议用梯度累积来模拟较大的 batch size而不是单纯调小 batch。另一个思路是用可变形注意力替代标准注意力它只对稀疏采样的 K 个关键点做计算复杂度从平方降到线性。4.3 常见问题速查表我把实际中遇到的高频问题整理成一张表方便快速定位现象可能原因排查与解决方向训练 loss 下降极慢Transformer 本身收敛慢或学习率偏低检查学习率缩放必要时用 warmup考虑 Deformable DETR小目标 AP 特别低特征图分辨率不足目标信息丢失提高输入分辨率或引入 FPN 结构或换 Swin Transformer 等层级化骨干网络一个目标被多个框重复框住object queries 之间没有“沟通”好检查解码器 self-attention 参数适当增大 loss_giou 权重显存不足 OOM注意力矩阵过大降低分辨率、减小 batch、梯度累积换可变形注意力推理结果有大量低置信度框过滤阈值偏低提高置信度阈值检查类别定义是否一致表中提到的 Swin Transformer 值得多说一句它是带有层级结构和窗口注意力的 Transformer 骨干网络作为 DETR 的 backbone 使用时能在保持全局建模能力的同时恢复部分局部先验对小目标和密集场景更友好。Vision TransformerViT也一样虽然最初是为分类设计的但在检测任务中做 backbone 也验证过不少场景。这些模型本质上都在解决同一个问题如何让 Transformer 在视觉任务里既保持全局上下文建模能力又不丢失局部细粒度信息。4.4 后续改进方向与 DETR 家族Deformable DETR、Conditional DETR、DINO 这几个名字在做技术预研时一定会反复遇到。Deformable DETR 用可变形交叉注意力替代标准交叉注意力大幅降低计算量解决了收敛慢的问题Conditional DETR 通过条件空间查询把解码器的 cross-attention 变成依赖目标空间位置的查询进一步加快收敛DINO 则引入对比去噪训练在 COCO 上把 AP 做到了 63 以上同时训练 epoch 大幅缩减。这一系列改进让我明白了一件事DETR 真正的价值不只是它本身的好坏而是它开辟了一个端到端检测的范式。后续各种变体都是在这个范式上做的优化有的从注意力结构入手有的从训练策略入手有的从多尺度特征入手。如果你现在要做检测任务的技术选型不应该问“DETR 好不好”而应该问“我是要做一块干净、稳定的基线还是追求极致精度”。我个人在实际操作中的最大感受是DETR 是一个需要耐心对待的模型。第一次训练它时我看着前 200 个 epoch 平平无奇的 loss 曲线差点中途弃坑直到第 300 个 epoch 之后指标才明显抬头。这种“慢热”和传统 CNN 检测器的体验完全不同。如果只是验证思路建议直接用官方 COCO 预训练权重做推理先感受一下端到端检测带来的流畅体验如果确实需要在自己的数据上训练也请先准备好足够的 GPU 算力或者直接转向 Deformable DETR 这类更高效的变体。检测这个领域卷了很多年DETR 最大的贡献是让人明白检测器里的手工设计不是不能消失而是我们还没有找到让模型自己学会“做选择”的正确方式。
返回列表