1. 先泼盆冷水:DETR 到底革了什么命
我平时不太信“告别 XX”这类说法,但“告别YOLO!基于Transformer的DETR实战”能成为高频搜索词,确实戳中了很多同学做检测时的真实痛点。DETR(Detection Transformer)把目标检测从“锚框+回归”一路带到了“集合预测”,一套网络把检测问题彻底改写了一遍。我完整跑通了一遍环境搭建、训练、预测、源码阅读的流程之后,最大的感受是:方向确实值,玩法也确实和以前不一样。
1.1 从“锚框+回归”到“集合预测”的范式切换
传统检测器怎么干?基本逃不出一条链路:先铺一堆预设锚框,然后让网络预测“每个锚框里有没有物体、物体离锚框偏移多少”,最后再做非极大值抑制把重复框压掉。YOLO 系列在速度上把这些事做到了极致,但“先铺框、再修正、后去重”这套思路从根上就没有变过。DETR 做的却是另一件事:网络直接输出固定数量的预测集合,每个预测包含类别和框坐标,并用二分图匹配的方式把这些预测和真实目标对应起来。整个过程没有手工设计的锚框,也没有传统意义上的 NMS 依赖。
为什么这算得上“革命”?因为整个检测任务的归纳偏置被换掉了。DETR 用 Transformer 的编码器-解码器结构对全图建模,解码器端有一组可学习的“查询向量”(query),每个查询负责去图上某个位置“找目标”,找到就输出框和类别。这很像一个小组被分派任务:每个人负责去图上盯一块区域,最后汇总结果;而不是以前那种“满图撒网、再按大概率捞鱼”的做法。也是从这里开始,你几乎可以把 DETR 的整个训练过程理解成“端到端集合预测”,而不是一堆后处理模块的拼凑。
1.2 为什么不能直接套用 YOLO 的训练直觉
我接触过不少做过 YOLO 项目的同学,第一次跑 DETR 基本都会懵:怎么收敛这么慢?怎么把训练调到 36 个 epoch 而不是 300 个 epoch?为什么我用小数据集一练就发散?这里的关键是,YOLO 的收敛速度快很大程度来自锚框先验和大量手工调参空间,而 DETR 把“先验”换成了 Transformer 的全局注意力,它需要在训练中自己学会目标匹配关系。打比方说,YOLO 像是拿到一张写满“哪块区域大概率有什么”的攻略地图,DETR 更像是只拿到一块白板和一支笔,要自己把地图画出来。
所以,如果你过去习惯用小 batch、快 lr、几百张图就跑出效果,这套经验在 DETR 上大概率失灵。我在实测里发现,DETR 对学习率、batch size、训练轮数都更敏感,尤其是早期训练阶段,loss 下降的曲线相当“不性感”,前几个 epoch 看起来像没在学一样。如果你没提前做好这个心理预期,很容易在第一个晚上就想把它删掉。从我自己的项目经验看,DETR 更适合两类场景:一是整体流程希望简化、不想在后处理上反复肝的;二是任务里有遮挡、重叠、密集小目标之外但又不极端依赖多尺度的场景。它不是“万能药”,但确实值得从 YOLO 的舒适区里跳出来试一次。
2. 环境搭建:实测中更稳的版本组合与坑
环境搭建这一步,标题写起来只有一行,实际操作起来却最容易劝退人。DETR 本身代码结构不算复杂,但依赖版本一旦错位,跑起来全是“看起来没问题、到训练就崩”的怪病。我前后调整了两轮才跑到一个相对稳定的组合,这里把过程记录一下。
2.1 我最终使用的版本组合与原因
先给一个我实际跑通的组合,供参考。我的机器是单张显存 12G 左右的老卡,系统环境是用 conda 维护的:
| 依赖项 | 我用的版本 | 选择原因 |
|---|---|---|
| Python | 3.9.13 | 生态兼容性最好,动态编译不容易出幺蛾子 |
| 深度学习框架 | 常见的动态图框架 2.x 稳定版 | 配合相应视觉工具库较顺,算子支持齐全 |
| CUDA | 11.8 对应驱动 | 兼容性最稳,训练和推理速度均衡 |
| 标注解析工具 | 处理公开数据集标注格式的标准库 | 依赖少,编译通过率高 |
| 编译辅助包 | cython 与若干打包工具 | 安装标注解析库前置依赖,跳过必报错 |
选定这套组合的原因其实就一句话:全部都选“已经被大量项目验证过的稳定版本”,而不是追新。我吃过太多“装最新版然后回归倒退”的亏,尤其是 Transformer 相关代码对算子版本很敏感时,稳定版本反而省时间。DETR 源码没有特别复杂的新算子,因此不需要更新版本来提升速度,稳定压倒一切。
2.2 安装阶段遇到的三个意外
第一次安装时我犯了个很经典的低级错误:直接跳过编译辅助工具去安装数据标注解析库,结果编译时报了找不到头文件、没有编译器的错,还一度怀疑是 Python 版本问题,折腾了半小时才发现是前置没装。第二个意外是,框架与视觉工具库版本不匹配,装完之后 import 不报错,但一跑模型就提示算子与 CUDA 版本不匹配。这个问题最阴的地方在于,它只会在训练或推理的中途跳出来,不容易第一时间定位。第三个意外是 conda 创建环境后没有激活,pip 装到了另一个环境里,导致命令行里 import 成功、脚本运行时却又是另一套解释器。
这几个问题解决之后,整个安装其实两三分钟就能完成。如果你也卡在这一步,做法很简单:先把 Python 锁到一个版本,再按“框架→视觉库→标注解析库→源码包”的顺序安装,每装一步就打印一下版本,别跳步。网络不通畅的时候,留意一下包的下载源配置是否生效,避免卡在下载阶段。
2.3 环境通不通,用一个最小推理自检
环境装完别急着训练,先用一个最小自检脚本验证模型能不能正常前向。DETR 源码包通常有 models 模块和 util 模块,我们可以临时写一个几行的脚本。这里给你一个可以作为模板的最小验证思路:
import torch from models import build_model # 源码包内导入,具体路径以你的目录结构为准 # 先用骨架化配置构建模型,只保留默认参数即可 args = {} # 这里放默认训练配置,或者直接用配置文件的参数 model, criterion, postprocessors = build_model(args) model.eval() dummy = torch.randn(1, 3, 800, 800) with torch.no_grad(): out = model(dummy, mask=None) print({k: (tuple(v.shape) if isinstance(v, torch.Tensor) else v) for k, v in out.items()})如果输出里能看到分类 logits 和预测框的 shape,说明模型结构、算子、显存链路都正常。我当时跑完这个自检后松了一口气,因为后面训练阶段遇到的问题,基本都能确认不是环境问题,而是训练策略问题。这个自检脚本建议一直留着,改代码改到一半怀疑“是不是环境坏了”时,拿它跑一下就能快速区分问题区间。
3. 训练 DETR:数据处理、超参与收敛判断
训练这步是整个流程里最容易“从入门到放弃”的环节。DETR 不是傻瓜式一键训练,数据、参数、指标都得重新适应。我把实际训练中最重要的几个点拆开讲,希望你少走我走过的弯路。
3.1 标注转换:从常见格式到 DETR 输入
DETR 官方训练一般使用公开检测数据集的标准标注 JSON 格式:整体是一个 dict,包含 images 列表、annotations 列表和 categories 列表;annotations 里每个目标记录 image_id、category_id、bbox 和 area 等字段。很多同学手头数据是 CSV 或者简单 XML 格式,这就需要先写一个转换脚本。常见转换脚本的逻辑包括:读原始标注、按图片分组、过滤掉无效框、统一类别 id。之所以要过滤无效框,是因为 DETR 对“错得离谱的标注”容忍度比 YOLO 更低——它做的是全局匹配,一个脏框会影响匹配结果。
我遇到的一个比较隐蔽的坑是:类别 id 必须从 1 开始连续编号,不能留空洞。因为 DETR 解码端通常会把 id 0 作为背景类,如果你的类别 id 从 3 开始,那分类头的索引就对不上了,训练时会出现“永远把某些类别当背景”的诡异现象。检查方法非常简单:读入标注后打印一下 categories 列表,确认从 1 到 N 连续。另一个坑是框坐标格式,尽量统一成 xywh 绝对坐标。DETR 在损失里直接算框回归,和某些检测框架用 xyxy 或归一化坐标的习惯不一样。我建议在转换阶段就统一,宁可多花十分钟,也别在训练中途返工。
3.2 关键超参数的调节逻辑
DETR 训练中几个关键超参数是:num_queries(默认通常 100)、backbone 类型、学习率、batch size、损失权重、epoch 数。先说 num_queries,它表示解码端查询向量的数量,本质是“模型最多预测多少个目标”。如果场景中单张图最多几十个目标,默认 100 足够。但如果你检测密集小目标,100 可能不够用,需要适当调大。调大的代价是解码器计算量上涨,所以这不是越多越好,是要压着场景上限来。
学习率上,常见范式是用主干网络预训练权重,主干部分的学习率一般比 Transformer 头低一些。我跑的稳定配置里,Transformer 部分学习率在 1e-4 量级,主干部分用 1e-5 左右甚至更低。Batch size 受显存限制,我用 12G 显存跑 800 像素输入时,batch 一般在 2 到 4 之间,再大就爆显存。但 batch 太小也会影响 Transformer 的收敛稳定性,所以有条件的话优先用梯度累积模拟更大的 batch。损失权重上,分类损失、L1 框损失和 GIoU 损失三者通常有一个默认比例,实际任务中如果目标大小差异很大,可以适当提高 GIoU 的权重,让模型更关注“框得准不准”。
3.3 训练日志里真正值得看的指标
训练 DETR 时不要只盯着总 loss 发呆。我看日志时更关注几个内部指标:分类损失、L1 损失、GIoU 损失,以及一个很特别的 cardinality error。Cardinality error 衡量的是“模型预测的目标数量与真实目标数量的差异”,可以很直观地告诉你模型有没有学会“这图里有几个物体”。如果训练到后半段,cardinality error 仍然很大,说明模型还在乱猜数量,这时先不要调超参,先去检查数据标注和匹配逻辑是不是有 bug。
我自己跑第一版时遇到一个现象:总 loss 在缓慢下降,看起来有点用,但验证集 mAP 一直平着不动。后面查了日志才发现,分类损失降得很慢,cardinality error 也偏高。问题出在类别数量偏差太大,很多类别只有零星样本,模型把它当噪声忽略了。处理办法是调整每个类别的分类损失权重,让少数类得到更多关注。还有一点:训练中期 loss 出现短暂回升不要慌,Transformer 类模型在训练过程里出现阶段性的 loss 震荡是常见现象,关键是看整体趋势和验证集指标。
3.4 把模型练崩的几种常见姿势
我把训练阶段翻车较多的原因归纳成四类,遇到对应现象可以对号入座:
- Loss 变 NaN:最常见是学习率偏大。调低学习率,尤其主干网络学习率,再试试用梯度裁剪,通常能压住。
- 前几个 epoch loss 纹丝不动:不一定是坏了,可能是匹配还没找到合适的“对应点”。继续往后跑,但前提是数据量要够。如果只有几百张图,DETR 很难自己学会匹配关系。
- 验证集 mAP 上不去:先看是不是类别 id 不连续,这个我上面提过;再看是不是 num_queries 远小于单图目标数量。
- 显存爆炸:减小输入尺寸或者 batch,而不是直接放弃训练。
很多同学把模型练崩归结为“Transformer 太难调”,实际上,大部分翻车问题都出在数据和标注细节上。把训练日志打印全面一点,慢慢看指标变化,比盲目换模型有效得多。
4. 预测环节:单图推理到批量前向
训练完模型,接下来就到推理预测。这一步看起来简单,但 DETR 的输出处理方式和 YOLO 系有微妙的不同,如果不处理好,预测结果的观感会大打折扣。
4.1 权重加载与单图推理实现
推理的第一步是加载训练好的 checkpoint。DETR 源码保存权重时通常是一个 dict,里面包含 model 参数、优化器状态和当前 epoch。加载时要先用相同配置重建模型,再拿 checkpoint 中的 model 部分加载权重。我整理过一个简洁的单图推理片段,核心流程是:读图→做与训练一致的归一化→前向→后处理。伪代码结构大致如下:
import torch import torchvision.transforms as T # 构建模型并加载权重 checkpoint = torch.load("output/checkpoint.pth", map_location="cuda:0") model.load_state_dict(checkpoint["model"]) 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 = transform(origin).unsqueeze(0).to("cuda:0") with torch.no_grad(): out = model(img) # 后处理拿到 bbox results = postprocessors["bbox"](out, img.shape[-2:])注意后处理时传入的 size 是原始图片尺寸,这样输出框坐标才会映射回原图。我没有按固定分辨率跑推理,而是让短边 800、长边等比例缩放,这样对小图和大图的适配都好一些。预测出来的置信度阈值可以过滤掉很多无效框,实际效果中我通常把阈值设置在 0.5 到 0.7 之间。
4.2 不用 NMS 的底气在哪
做过 YOLO 的同学都知道,YOLO 输出后一般还要过一层 NMS,否则同一目标会叠出好几个框。DETR 为什么可以不做强 NMS?因为它的训练目标强制每个查询向量负责一个唯一目标,通过匹配把预测和目标一一对应之后,解码器会自己学成各管一段、互不干扰。这就像给每个人分配了明确岗位,不会一群人挤在同一件事上。
话虽如此,我在实际推理时仍会遇到极小概率的重复框,主要是两个查询对同一个目标都打了高分。处理办法有两个:一是把置信度阈值提高一点;二是加一个轻量 NMS,但只针对同类别的框,也不需要像 YOLO 那样用复杂调参。DETR 的架构本身已经大幅降低了后处理工作量,这确实是我觉得它值得一试的重要原因。
4.3 批量推理与显存表现的感受
单张图推理更多是为了验证,实际落地时一般要批量跑。批量推理时,图片尺寸最好尽量一致,否则 Transformer 处理带掩码的批量输入会更复杂,也会更慢。我把推理脚本改成按 batch 处理之后,显存利用率明显提升,吞吐量比单张循环快了将近一半。如果你的场景对速度要求很高,可以考虑混合精度推理,DETR 的注意力计算在 float16 下表现稳定,显存占用也能进一步降低。
速度方面,我也要说句公道话:DETR 的推理速度通常比不上同级别 YOLO。我在同一张卡上跑,输入尺寸相近的情况下,DETR 大概比 YOLO 慢几倍。这是纯架构层面的代价,因为它做的是全局注意力计算。如果你要部署到边缘设备或者需要视频实时处理,DETR 未必是第一选择。但我个人认为,它的推理稳定性、后处理简洁性在一些场景中可以弥补速度差距,关键看项目本身。
5. 源码阅读路线:抓住两条主要骨架
很多同学跑通 demo 之后就停了,但我觉得 DETR 这个项目最适合做源码学习的点恰恰在“麻雀虽小、五脏俱全”。源码里最核心的几条线其实并不复杂,抓住匹配器和 Transformer 骨架之后,你基本就能动手改自己的版本了。
5.1 matcher 决定“预测跟谁算账”
DETR 源码里有一个实现二分图匹配的核心模块,官方名一般叫 HungarianMatcher,作用是计算预测框和真实框之间的最优对应关系。匹配的代价由三部分组成:分类代价、L1 框回归代价和 GIoU 代价。匈牙利算法在这里做的是:把所有预测集合和真实目标集合拉成一张代价矩阵,然后找出总代价最小的排列。这个“排列”就是我们说的匹配结果。训练时,损失就是基于这个最优匹配结果来算的;推理时则不需要匹配,直接按置信度输出即可。
读这部分源码时,我建议重点看代价矩阵怎么构建,以及背景类怎么处理。源码里通常会把目标框和预测框的匹配限制在预测集合这个轴上,背景只负责吸收多余的预测。理解了这一步,你就明白了 DETR 为什么不需要锚框:因为匹配本身就是一种“自适应分配”,它把每个预测动态地指派给某个真实目标或背景。
5.2 transformer 与 decoder 的辅助出口
Transformer 骨架的部分通常分为编码器和解码器。编码器对主干网络提取的特征做全局建模;解码器接收一组可学习的查询向量,通过自注意力、交叉注意力一步步“查询”出目标信息。源码里比较有意思的是,解码器每个中间层通常都会接一个辅助预测头,让模型在每一层都计算一次损失。这样做的意义是:梯度信号可以在解码过程中更早回流,训练也更稳定。我读这段代码时花了最多时间的地方,反而是查询向量到底怎么初始化的。它其实就是一个随机初始化的参数矩阵,训练完会变成一组“分工明确”的查询模式。
5.3 如果你想做二次开发,先问三个问题
读完源码想动手改造之前,我建议你先问自己三个问题。一是场景目标数是否会超过 num_queries?如果目标数量波动极大,原版 DETR 的固定输出数量就不太合适,你可能需要走多尺度变体路线。二是是否需要检测小目标?原版 DETR 对密集小目标的检测能力一般,要做小目标检测建议直接考虑加入多尺度特征。三是你的数据量和计算资源能否支撑 Transformer 的收敛需求?如果只有一两千张图,起步会比 YOLO 困难,这种情况下可以考虑主干网络继续用预训练权重,并适当降低输入分辨率。这三个问题想清楚,你再决定是直接用原版,还是在其上做二次开发,比盲目改结构靠谱得多。
走到这里,我对 DETR 的整体判断其实已经比较清晰了:它用更统一的思路解决了目标检测的问题,但工程化和性能之间的权衡仍然存在。如果你期待的是一个能从 YOLO 无缝切换的模型,可能多少会有些落差;但如果你希望在检测任务里引入更简洁的解决方案、减少后处理工程负担,那么这次基于 Transformer 的实战很值得完整走一遍。最后补一个自己实测后的习惯:训练过程中定期保存最好验证指标对应的权重,不要只看最后一个 epoch,DETR 训练尾声波动不小,保留中途最优权重能省很多回头路。