简介:一份DETR学习讲解PPT,面向正在跟踪Transformer与目标检测前沿的算法工程师、研究者和学生。内容从自然语言处理中的自注意力机制切入,逐步拆解DETR如何将目标检测建模为序列到序列问题,依次讲解位置编码的作用与常见实现方式、Transformer编码器与解码器的信息流动、解码器输出与真实目标框的匈牙利算法一对一匹配、端到端训练流程等关键环节,并配有自注意力计算片段和关键实现代码,能够帮助读者把论文原理与代码对应起来,建立从理论到落地的完整理解路径。其中也提到收敛速度较慢与Deformable DETR等后续改进方向,让读者了解模型局限。资源为单个pptx演示文稿,大小约2.55MB,内容紧凑,适合组会分享、论文精读和课后复习。目前已有1683人学习/下载,页面附有配套博客地址,便于结合原文章节进一步查阅。
1. DETR 学习分享:为什么说它把目标检测做成了“填空题”
有人把一份《DETR学习分享.pptx》丢给我,问里面画的“端到端检测”到底怎么落到代码里。这里先把结论说清楚:DETR(Detection Transformer)是一套把目标检测改写成“有限集合预测”的完整方案,它把 anchor、NMS、手工后处理全部去掉,用 Transformer 编码器-解码器和二分图匹配直接输出 100 个候选框。它解决的是传统检测器后处理链路长、超参多、复现靠玄学的问题,适合已经熟悉检测基础、想做端到端建模的工程团队。但这套方案收敛慢、小目标偏弱,不是免费的午餐。
这份笔记不做 PPT 逐页复读,只讲复现 DETR 时真正绕不开的三件事:匹配机制怎么理解、最小推理怎么跑通、训练参数怎么设。读完你至少能回答一个问题:“别人讲的 DETR 那么好,我在自己的数据上到底要不要上它?”
2. DETR 原理落地:二分匹配、object queries 和 100 个框的来龙去脉
先给从来没看过 detr 论文的同学一个最小图景。DETR 的输入是一张图,backbone 用 ResNet 提特征,Transformer 编码器在特征图上做全局建模,解码器端拿着 100 个可学习的 object queries 去“查”图里的目标,输出 100 个预测,每个预测包含类别和框。整个网络没有 anchor 生成、没有 RPN、没有 NMS,训练时用匈牙利匹配把预测和真值配对,然后算损失;推理时这 100 个框直接就是结果。
2.1 二分图匹配:为什么 DETR 用匈牙利算法而不是 NMS
传统检测器的框是“先多给、再删减”:RPN 或 anchor 生成几千上万个候选,训练时按 IoU 分配正负样本,推理时用 NMS 把重叠框压掉。NMS 阈值在密集场景和小目标上很敏感,几乎是调参重灾区。DETR 换了一条路:把“去重”这件事实现在损失函数里,让模型自己学会“一个目标只出一个框”。
具体做法是在训练时把预测和真值当成两个集合,算一个 cost 矩阵。矩阵里每个元素表示第 i 个预测和第 j 个真值之间的匹配代价:
- 分类代价:预测类别与真值类别的负对数概率
- 框距离代价:预测框和真值框的 L1 距离
- 形状代价:预测框和真值框的 GIoU 距离(取负)
三个代价按权重相加,常见权重是分类 1、L1 为 5、GIoU 为 2。然后用匈牙利算法(scipy.optimize.linear_sum_assignment)在这个矩阵上求全局最小匹配。这样每个真值最多匹配一个预测,剩下的预测全部归为背景,模型天然不需要 NMS 来去重。这个机制保证了训练和推理行为一致:推理时你看到多少个框,训练时就是按同样的“集合”去约束的。
2.2 object queries 的作用与初始化:学习出来的位置先验
object queries 是 DETR 里最容易被误解的东西。它不是某些固定的锚框,也不是 Transformer 里的 token 序列语义,而是一组形状为 (100, 256) 的可学习参数,和解码器的输出维度一致。初始值是随机出来的,网络训练后,每个 query 会慢慢“长成”一个对特定位置、特定尺寸目标敏感的模式。
我见过有人在代码里把 object queries 初始化为全 0,理由是“让网络自己学,别给先验”。结果训练完全不动,因为 100 个 query 完全相同,解码器输出的预测也完全对称,梯度更新后还是对称的,模型永远无法区分它们。这个坑后面避坑章还会细说。正确做法是保持随机初始化,或者直接加载官方预训练权重里的 query 参数。
你还可以这么理解:每个 query 像一个“探测器”,有的负责查大目标,有的负责查小目标,有的偏向图像左上角,有的偏向中心区域。训练完成后,如果你把 100 个 query 对验证集所有输出框的中心点画成散点图,会看到明显的空间分布偏好。这个特性也能用来做端到端的有趣验证,最后一章会讲。
2.3 DETR 论文里没写透的三个隐含参数
论文把框架讲得很清楚,但复现时真正卡人的细节都在代码里。第一,位置编码用的是正弦位置编码(sine positional encoding),它加在编码器输入的每个 token 上,解码器里的 object queries 也要加位置编码,否则跨层注意力会丢失空间信息。第二,官方解码器有 6 层,训练时每一层都输出预测并参与损失计算,这个叫 aux_loss 的开关必须在训练时打开,单独只算最后一层会让收敛明显变慢。第三,推理时类别阈值怎么选,论文不会给你答案;COCO 上官方效果用的是 0.7 左右,但换成你自己的数据,类别少了、目标更清晰,阈值可以放到 0.5 到 0.6,如果类别特别细、容易混淆,0.8 也不奇怪。
DETR 和 Faster R-CNN 这类检测器的差异,不是简单换了个 backbone,而是把检测变成了一个全局优化问题。下面的对比能帮你快速判断它适不适合你的场景:
| 对比维度 | 传统检测器(Faster R-CNN 系) | DETR |
|---|---|---|
| 候选框来源 | RPN/anchor 生成 | object queries 学习 |
| 去重方式 | NMS | 二分图匹配训练 |
| 后处理超参 | anchor 比例、NMS 阈值 | 少量类别阈值 |
| 收敛速度 | 快(十几个 epoch 见效果) | 慢(需要几百 epoch) |
| 小目标表现 | 依赖 FPN 多尺度 | 原始版本偏弱 |
| 部署复杂度 | 后处理链路长 | 网络简单,后处理极少 |
3. 最小推理复现:从加载 DETR 权重到输出一张图的检测框
原理归原理,能跑通推理才算真正上手。这一章给一套能直接抄的最小推理流程,包含环境准备、权重加载、单张图前向和后处理。代码以官方仓库结构为准,你应该先把官方仓库 clone 到工作目录,确认根目录下有 models/、engine.py、main.py 这些文件,并在 release 页面下载 ResNet-50 的预训练权重,文件通常叫 detr-r50-e632da11.pth。
3.1 环境准备:conda、PyTorch 和 pycocotools 的版本搭配
DETR 依赖的库不多,但版本选不对会在 import 阶段浪费半小时。我的建议是用 conda 单独建一个环境,不要和训练其他模型的库混在一起,免得 torchvision 版本打架。
conda create -n detr python=3.8 conda activate detr # PyTorch 装 CPU 版还是 GPU 版取决于你的机器,日常推理 CPU 也能跑 pip install torch torchvision pip install pycocotools scipyTorch 和 torchvision 的版本要匹配,常用组合是 PyTorch 1.10 配 torchvision 0.11,或者 PyTorch 2.0 配 torchvision 0.15。装完后进到官方仓库根目录,试着跑一句python -c "from models import build_model",不报错就说明依赖没问题。pycocotools 在 Windows 上经常编译失败,优先用预编译 wheel,装不上就换 Linux 环境,没必要在这里硬磕。
3.2 单张图片推理的完整脚本与后处理说明
在仓库根目录新建一个 infer_single.py,把下面这段放进去。脚本里用 argparse.Namespace 构造模型需要的参数,这是官方模型接口的标准用法。
import argparse import torch import torchvision.transforms as T from PIL import Image from models import build_model # 构建模型参数:COCO 的类别 id 规则特殊,91 是官方权重需要的维度 args = argparse.Namespace( num_classes=91, num_queries=100, aux_loss=False, hidden_dim=256, position_embedding="sine", lr_backbone=1e-5, masks=False, dilation=False, ) model, _, postprocessors = build_model(args) checkpoint = torch.load("detr-r50-e632da11.pth", map_location="cpu") model.load_state_dict(checkpoint["model"]) model.eval() # 预处理:等比缩放到短边 800,长边超过 1333 再缩回 1333 def resize_pad(img): w, h = img.size scale = 800 / min(w, h) if max(w, h) * scale > 1333: scale = 1333 / max(w, h) return img.resize((int(w * scale), int(h * scale))) transform = T.Compose([ T.Lambda(resize_pad), T.ToTensor(), T.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) img = Image.open("demo.jpg").convert("RGB") pixel_values = transform(img).unsqueeze(0) with torch.no_grad(): outputs = model(pixel_values) # postprocessors["bbox"] 返回 [{boxes, labels, scores}],已还原为原图尺寸 result = postprocessors["bbox"](outputs, torch.as_tensor([img.size[::-1]]))[0]这段代码的关键在postprocessors["bbox"]。官方训练时返回的这个后处理器会直接把归一化坐标还原成原图像素坐标,省去你手动转换的麻烦。它输出的result是一个字典,包含三样东西:boxes是 (N, 4) 的 xyxy 像素坐标,labels是类别索引,scores是概率。注意这里必须在 eval 模式下运行,否则 BatchNorm 和 Dropout 的行为会干扰单张图推理。
3.3 解析输出:把 100 个 query 变成可用的框和类别
拿到result之后最常踩的坑是不知道 label 和真实类别名怎么对应。官方预训练权重是在 COCO 上训练的,模型输出的 91 维里有很多空余类位,实际类别名只有 80 个。常见做法是从官方仓库的 datasets/coco.py 里把coco_names列表复制出来,它有 91 项,真正要展示的只有非空的那 80 项。
# coco_names 从官方仓库复制,长度 91,这里示意格式 # coco_names = ['N/A', 'person', 'bicycle', 'car', ...] keep = result["scores"] > 0.7 boxes = result["boxes"][keep].cpu().numpy() labels = result["labels"][keep].cpu().numpy() scores = result["scores"][keep].cpu().numpy() for box, label, score in zip(boxes, labels, scores): print(f"{coco_names[label]} {score:.3f} {box.astype(int).tolist()}")阈值 0.7 是 COCO 场景比较保守的设置。如果你自己的图上目标很明显、遮挡少,0.5 也能看;如果类别之间有相似性,比如“轿车”和“SUV”,阈值可以拉到 0.85 再看结果。还有一个容易误判的点:DETR 推理结果是确定性的,同一张图每次输出完全一样,不要因为两次运行结果相同就觉得程序卡死了。真正要检查的是框的数量是否合理——一张普通街景图一般输出 10 到 30 个框,如果每次都输出近百个框,先别急着加 NMS,多半是阈值太低或模型没训练到位。
4. 用自有数据微调 DETR:COCO 格式整理与训练参数设置
跑通推理只是第一步。真正让 DETR 有价值的是在自己的数据集上微调,但这一步劝退了很多人。主要原因是 DETR 官方训练代码只认 COCO 格式的数据,你必须先把自有数据整理成标准 COCO json 目录结构。下面给你一套能直接照做的流程。
4.1 自有数据组织:从 VOC xml 转成 COCO json 的脚本
如果你的标注是 VOC 格式(一个 xml 对一张图),最省事的方案不是手写 JSON,而是写一个小转换脚本。下面这段适合标注文件夹里包含 Annotations 和 JPEGImages 两个目录的标准 VOC 结构。注意类别映射表需要按你的数据实际类别调整。
import xml.etree.ElementTree as ET import os, json, glob from PIL import Image def voc_to_coco(voc_root, output_json): # 这里按自己的类别名改,id 从 1 开始连续编号 category_names = ["person", "car", "dog"] categories = [{"id": i, "name": n} for i, n in enumerate(category_names, 1)] cat_map = {n: i for i, n in enumerate(category_names, 1)} images, annotations = [], [] ann_id = 1 xml_list = glob.glob(os.path.join(voc_root, "Annotations", "*.xml")) for img_id, xml_path in enumerate(xml_list): root = ET.parse(xml_path).getroot() filename = root.find("filename").text img = Image.open(os.path.join(voc_root, "JPEGImages", filename)) w, h = img.size images.append({"id": img_id, "file_name": filename, "width": w, "height": h}) for obj in root.findall("object"): if obj.find("difficult") is not None and obj.find("difficult").text == "1": continue name = obj.find("name").text if name not in cat_map: continue b = obj.find("bndbox") x1 = float(b.find("xmin").text); y1 = float(b.find("ymin").text) x2 = float(b.find("xmax").text); y2 = float(b.find("ymax").text) annotations.append({ "id": ann_id, "image_id": img_id, "category_id": cat_map[name], "bbox": [x1, y1, x2 - x1, y2 - y1], "area": (x2 - x1) * (y2 - y1), "iscrowd": 0, }) ann_id += 1 json.dump({"images": images, "annotations": annotations, "categories": categories}, open(output_json, "w"), indent=2)脚本里最容易忽略的是difficult标记。VOC 数据里有一部分目标被标为 difficult,表示很难辨认,官方 DETR 训练时不会处理这个字段,你不主动跳过它,模型就会被这些模糊样本干扰。另外 COCO 的 bbox 格式是 x, y, w, h,VOC 是 x1, y1, x2, y2,转换时一定要记得换,否则训练 loss 一开始就异常大,而且基本不会降下去。
转换完以后,按 COCO 的目录习惯把它组织成下面这样,因为官方 main.py 默认就是从 annotations/instances_train.json 和 annotations/instances_val.json 读取标注,目录名最好原样保留:
custom_coco/ annotations/ instances_train.json instances_val.json train2017/ val2017/4.2 训练启动命令与学习率缩放要点
单卡训练 DETR 是可以跑的,但要注意学习率不能照搬官方。官方在 8 卡上、单卡 batch_size 为 2 时用的学习率是 1e-4,你单卡 batch_size 为 1 时还用这个值,loss 大概率直接飞掉。我的经验是单卡训练从 2e-5 起步,等前 20 个 epoch 确认 loss 在降,再考虑往上加。
python -m torch.distributed.launch \ --nproc_per_node=1 \ --use_env \ main.py \ --coco_path /path/to/custom_coco \ --batch_size 1 \ --lr 2e-5 \ --backbone_lr 2e-6 \ --epochs 300 \ --output_dir outputs/detr_custom参数说明:--backbone_lr是 ResNet 部分的专用学习率,一般比主干网络的 lr 小一个数量级,因为 backbone 通常加载了 ImageNet 预训练权重,不需要大改。--epochs官方是 500,你换到小数据集可以减到 300 甚至 150,但不要低于 100,DETR 的收敛速度天然比 Faster R-CNN 慢。--coco_path直接指向你的 custom_coco 目录,官方代码会自己拼子目录路径,不需要额外传 train2017 的路径。还要记得加--aux_loss,这个开关让每层解码器都参与 loss 计算,对收敛帮助很大。
4.3 训练日志里真正要盯的三个指标
训练日志每 500 个 iter 打印一次,里面最常见的是 loss、loss_ce、loss_bbox、loss_giou 和 class_error。新手容易只盯总 loss,但 DETR 里这五项各有各的脾气。
class_error是分类错误率,代表当前匹配上的预测里有多少类分错了。这个值在前 20 个 epoch 可能居高不下,只要整体趋势往下就不慌。loss_giou反映框的形状和位置质量,它降得比 loss_ce 慢是正常的,因为框的回归需要先有正确的匹配关系。真正要警惕的是loss_bbox掉到某个值以后长时间横盘不动,这说明匹配已经稳定,但框回归到了瓶颈。如果class_error一直不降,先检查你的类别编号是不是从 0 连续编号,再看背景类有没有占位。
还有一个反直觉的现象:DETR 的 loss 曲线在最初几个 epoch 经常是“先升后降”。这不是代码写错了,而是匈牙利匹配还没找到好的配对关系,预测框和真值框的对应在不断变化,loss 随之波动。第一次跑训练的人看到前 5 个 epoch 的 loss 往上走,很容易直接终止训练,实际上再等 20 个 epoch 就会掉下来。
5. DETR 复现避坑:训练不收敛、小目标漏检、显存溢出排查
DETR 的复现难点不在模型结构,而在细节。这一章把我在实战里踩过、也帮别人排查过的几类高频问题列出来,每条都按现象、原因、解决办法的顺序写,你可以直接对着查。
注意:DETR 的训练曲线先升后降,前几个 epoch 的 loss 上涨是正常现象,别被吓到。下面每题都值得你对照自己的日志看一遍。
一、训练 loss 冲到 10 以上且越往后越爆炸
现象:loss 曲线一路向上,完全没有回落趋势,或者训练到一半 loss 突然跳变。原因:最常见是学习率偏大,其次是没做梯度裁剪。官方代码里用 AdamW 优化器配合梯度裁剪,裁剪阈值是 0.1,很多人换到自己的工程后把裁剪逻辑删了,短 batch 上还好,一旦遇到类别极端不均衡的 batch,梯度范数直接爆掉。解决:单卡训练把 lr 降到 2e-5 起步,同时确认 main.py 里的clip_grad_norm_还在生效,阈值保持 0.1 不要手滑改成 1.0。
二、推理时同一个目标出现多个高置信度框,想给 DETR 加 NMS
现象:一张图上同一个行人被框了两三次,且分数都在 0.6 以上。原因:模型没完全收敛时,多个 query 对同一个目标的响应还没达成“排他协议”。这时加 NMS 虽然能把视觉上重复的框压掉,但本质是在掩盖训练不足。解决:先把置信度阈值从 0.7 提到 0.85,看重复框是否明显减少;如果仍然重复,说明训练 epoch 不够,继续训练而不是加后处理。真正收敛的 DETR 模型,同一个 query 在类似图上会稳定偏向某个位置,不会出现大面积重复。
三、小目标 AP 几乎为零,怀疑数据处理写错了
现象:代码能跑通,大中目标检测正常,但小目标(面积小于 32x32)的 AP 非常低,甚至低于 1 个点。原因:DETR 原始版本没有 FPN 这类多尺度结构,backbone 输出 stride 32 的特征图上,小目标只有几个像素的信息,Transformer 全局注意力再强也难从这么低的分辨率里还原细节。解决:先确认输入尺寸确实到达短边 800,很多复现代码图省事把图缩到 416 或 512,小目标信息直接丢失。如果数据里小目标占比高,更实际的选择是换成 Deformable DETR、DINO 这些带多尺度特征的改进版,而不是继续硬啃原始 DETR。
四、batch_size=1 也报 CUDA out of memory
现象:单张 800x1333 的图,batch_size 1,显存不够。原因:编码器里的自注意力层对序列长度是平方复杂度,特征图 token 数大约在 1050 左右,显存开销很容易到 5GB 以上。解决:先把输入短边缩到 600、长边 1000 验证逻辑是否正确,能跑通再逐步加回去。不要尝试把 backbone 的 stride 从 32 改成 16,那样 token 数直接变成 4 倍,显存会爆炸得更快。如果必须用大图,考虑用梯度检查点或者把 batch 拆成更小的 mini-batch 做梯度累积。
五、自定义数据上准确率出奇地低,但不报错
现象:换了自有数据,loss 能降,但验证集 mAP 始终上不去。原因:大概率是num_classes设置错了。COCO 官方权重用 91 是因为 COCO 类别 id 不连续,你的自有数据如果是从 0 或 1 连续编号,num_classes应该是“类别数 + 1”,其中这个 1 留给背景。很多人直接把 num_classes 改成类别数,等于让模型没地方表达“这里没有目标”。解决:检查 build_model 的 num_classes 参数,比如你只有 3 个目标类别,就填 4。数据本身就存在严重的类别不均衡时,少量同名类别还要考虑类别重加权,DETR 官方没有内置这个逻辑。
6. 进阶验证:用小数据集快速验证 DETR 收敛与匹配效果
在你决定把整个业务切到 DETR 之前,先花半天做一个小实验,能帮你省下一个月的试错成本。这个实验的目标不是刷精度,而是验证“DETR 的机制在你的数据上能不能正常启动”。
6.1 用 200 张小图训练 50 epoch 观察“先升后降”曲线
选 200 张包含清晰目标的小图,不需要均衡类别,标注也不用精修,直接从训练集里抽。用上面的训练命令,lr 设 2e-5,epochs 设 50,其余参数保持不变。训练时每隔 10 个 epoch 记录一次class_error和loss_giou。如果前 10 个 epoch 的 loss 有小幅上升,20 个 epoch 后开始下降,说明匈牙利匹配在正常建立起预测和真值的对应关系。如果 30 个 epoch 了 class_error 还在 90% 以上,多半是数据标签有问题或者类别映射错了。这个小实验能帮你快速判断问题出在模型还是数据。
6.2 检查 object queries 是否形成位置先验
这是判断 DETR 是否有希望进一步提升的实用技巧。取 100 张验证集图片推理,把每个 query 输出的框中心点坐标收集起来,按 query 编号分组,在画布上画散点图。一个训练到位的模型,散点图会明显体现出空间偏好:某些 query 的中心集中在图像中心靠左下的区域,某些集中在右上,而不是在画布上均匀散开。如果你发现所有 query 的中心点都挤在图像中心一小块区域,说明模型没有学到足够多样的位置模式,可以考虑增加训练数据或者调大输入分辨率。
最后留一个我自己的习惯:每次训练 DETR,我都会顺手保存一份“第一个 epoch 结束时的推理输出图”和“第 100 个 epoch 的推理输出图”放在一起对比。这样后面改参数时,能直观看到匹配协议是怎么一步步形成的,而不是只盯着一堆平滑的曲线。这个小习惯帮我排掉过好几次数据标注错乱的坑,希望也帮到你。
本文还有配套的精品资源,点击获取