1. 从 DETR 到 Deformable DETR:这个项目究竟在解决什么问题
Deformable DETR 是我这两年做检测落地时回头率最高的一个结构。它属于 Transformers 在视觉检测方向的一条重要分支——把注意力机制从"一视同仁地看全图"改成"每个查询只在少数关键位置上采样"。这个改动听起来很小,但它把 DETR 系列从"论文里很美、工程上很难用"拉到了"真的能上线"的水平。
如果你在做目标检测,尤其是小目标多、场景杂、标注格式不统一的业务(工业质检、遥感、病理切片、密集货架),Deformable DETR 值得你花两三天时间认真跑通一遍。它最大的吸引力在于端到端:不需要锚框、不需要 NMS 后处理、不需要手工调一堆先验参数,输入图像直接出框。而过去端到端方案迟迟难以落地,核心卡点就是收敛太慢、小目标太差,这两个问题恰好是 Deformable DETR 的主要贡献点。
这篇文章我会按"为什么这么设计—原理怎么算—环境怎么搭—训练怎么跑—坑怎么排"的顺序完整走一遍。所有配置参数我会说明取值逻辑,涉及计算的地方我把账算给你看,涉及版本差异的地方我会标注清楚,避免你照着老教程踩新版本的坑。
1.1 DETR 的优雅之处与它的三个硬伤
先回顾一下 DETR 的思路,因为不理解它的问题,就看不懂 Deformable DETR 的取舍。DETR 的做法是:CNN 主干抽出一张特征图,拉平后送入 Transformer 编码器做全局自注意力,再准备一组数量固定的可学习查询向量,让解码器把查询映射成检测结果,最后用匈牙利算法做二分图匹配,把预测框和真实框一一对应起来算 loss。
这个设计最漂亮的地方是"集合预测"的思想。传统检测器要生成成千上万个候选框,再用 NMS 去掉重复,本质上是在做"去重后处理"。DETR 直接假设有 N 个查询、N 个结果,用匹配算法强制每个真实目标对应唯一一个查询,重复框天然不会被产生,所以 NMS 这个环节被彻底拿掉了。这在工程上是巨大的简化——NMS 的阈值在不同数据集上几乎都要重新调,而 NMS 本身在密集场景下还经常误杀。
但硬伤也很明显,而且都是工程上致命的:
第一是收敛慢。原版 DETR 在 COCO 上要训练 500 个 epoch 才能达到可用精度,而当时的主流检测器 36 到 72 个 epoch 就够了。500 epoch 意味着单次实验要跑好几天,调参迭代的成本高到无法接受。根因在于全局注意力对每个查询都要扫描全图所有位置,初始化阶段注意力分布接近均匀,模型要花很多轮才能学会"该看哪里"。
第二是小目标差。DETR 只在主干网络最深的一层特征图(通常是 stride 32 的 C5)上做注意力,那个分辨率下小目标可能只剩几个像素,早就被下采样糊掉了。论文里 DETR 的 AP_S 只有 20 出头,而同期的 Faster R-CNN 靠 FPN 能做到 27 以上。
第三是显存和计算量。全局注意力的复杂度是 token 数量的平方。一张 800×1333 的图,C5 特征约 25×42 个位置,大概一千个 token,注意力矩阵就是一百万级别;如果想把高分辨率特征也塞进去改善小目标,token 数翻四倍,计算量直接翻十六倍,显存撑不住。
这三个问题不是调参能解决的,是结构层面的限制,所以才有了 Deformable DETR。
1.2 可变形注意力:把稠密全局换成稀疏采样
Deformable DETR 的核心改动只有一句话:每个查询不再和所有位置交互,而是只在少数几个位置上采样。这些位置不是固定的网格,而是根据查询内容动态预测出来的偏移点。
这里的灵感来自可变形卷积(Deformable Convolution)。可变形卷积让卷积核的采样点位置可以偏移,从而适应目标形变;Deformable DETR 把这个思想搬到了注意力上——不再用固定的、稠密的注意力图,而是用"采样点位置+采样点权重"来近似注意力。
为什么这样就能解决问题?我打个比方。原版 DETR 的注意力像是一个人在图书馆里,为了找一句话把每一本书都翻一遍,效率极低。可变形注意力像是先根据手头的线索判断"这句话大概在第三排书架第二层",然后只去那个位置附近翻几页。判断可能不完全准,但可以多找几个候选位置(K 个采样点)并且给它们不同的权重,最终效果反而更好,成本还低了一个数量级。
这个设计同时解决了三个硬伤:采样点在每个注意力头、每个特征层上独立预测,天然支持多尺度(可以同时在 C3/C4/C5/C6 上采样,小目标在高分辨率层上有采样点);因为稀疏,高分辨率特征也能负担得起,小目标精度直接受益;因为查询从一开始就有明确的关注区域,收敛速度大幅加快,50 个 epoch 就能达到 DETR 500 epoch 的水平。
1.3 和其他主流检测路线的横向对比
我整理了一张对照表,方便你判断自己的场景要不要选它。表中数值是论文报告值,实际复现会因训练细节有波动,仅作量级参考。
| 方案 | 是否需 NMS | 是否需锚框 | 收敛速度(epoch) | 小目标表现 | 部署友好度 | 适合场景 |
|---|---|---|---|---|---|---|
| Faster R-CNN + FPN | 需要 | 需要 | 36 左右 | 好 | 高,生态成熟 | 通用场景,追求稳定 |
| YOLO 系列 | 需要 | 需要 | 300 左右(早期) | 中等 | 极高 | 实时推理、边缘设备 |
| 原版 DETR | 不需要 | 不需要 | 500 | 差 | 中等 | 大目标为主、研究验证 |
| Deformable DETR | 不需要 | 不需要 | 50 | 好 | 中等 | 小目标密集、标注规范 |
| DINO / DAB-DETR 等后续 | 不需要 | 不需要 | 12 到 36 | 很好 | 中等 | 追求最高精度的竞赛/研究 |
选型上我的经验是:如果你的场景里小目标占比超过三成,且推理实时性要求不高(比如离线批处理、服务端推理),Deformable DETR 系列性价比很高。如果要求端侧实时,那还是老老实实上 YOLO 系。如果精度优先且算力充足,可以直接考虑 DINO 这类后续工作,它把 Deformable DETR 的训练策略又往前推了一截。
注意:Deformable DETR 的端到端特性意味着推理后处理代码极简,但代价是训练成本高、调参敏感度大。如果你的团队数据量少于两千张,建议先做迁移学习而不是从头训。
2. 核心机制拆解:可变形注意力到底怎么算
这一节我想把公式讲透,因为很多同学跑通了代码却说不清里面的张量在干什么,一旦需要改结构或者排查异常就无从下手。可变形注意力的计算流程其实只有四步,理解之后你看源码会顺畅很多。
2.1 采样点、偏移量与注意力权重
先看公式,这是整个模块的定义:
DeformAttn(z_q, p_q, x) = Σ_m W_m [ Σ_k A_mqk · W'_m · x(p_q + Δp_mqk) ]逐个符号解释。z_q是查询特征,p_q是查询对应的参考点坐标(归一化到 [-1, 1]),x是输入特征图。m遍历注意力头,k遍历采样点。W'_m是把特征投影到第 m 个头的线性层,W_m是把各头结果融合回原维度的线性层。A_mqk是第 m 头第 k 个采样点的注意力权重,Δp_mqk是相对于参考点的偏移量。
关键在于Δp_mqk和A_mqk都不是固定值,而是由查询特征z_q经过一层线性变换直接预测出来的。这就是"可变形"的来源——采样位置随内容动态变化。
工程实现上有几个细节值得专门记一下:
偏移量的初始值应该接近零,也就是训练刚开始时采样点基本落在参考点上,随着训练逐步学会偏移。代码里偏移由nn.Linear输出后通常会经过一次归一化缩放,MMDetection 的实现里用的是offset_normalizer,取值是各层特征图的长宽减一,目的是把预测的相对偏移映射到实际像素坐标。
注意力权重的归一化维度容易搞错。它不是对每个特征层单独 softmax,而是把所有特征层、所有采样点的权重放在一起做 softmax。也就是说M×K×L个权重(M 为头数,K 为每头采样点数,L 为特征层数)竞争归一,模型会自动决定"主要看哪一层的哪个点"。这一点在改代码时务必注意,我见过有人误改成了按层归一,精度掉了两个点多。
采样点是浮点坐标,需要用双线性插值取值。这也是为什么必须写自定义 CUDA 算子:反向传播时,相邻采样点会共享同一个特征位置,梯度需要累加,用普通 PyTorch 操作做会有性能和精度问题。
2.2 多尺度特征与多头采样的组合方式
多尺度是这个模块的另一半。主干网络输出 C3、C4、C5 三层,再加一层从 C5 卷出来的 C6,stride 分别是 8、16、32、64。每层特征先过一层 1×1 卷积统一到 256 维,再送上可变形注意力。
以论文默认配置为例:注意力头数 M=8,每头采样点数 K=4,特征层数 L=4。那么每个查询在每个特征层上要采样 M×K=32 个点,四层加起来是 128 个位置。相比之下,原版 DETR 在 C5 上做全局注意力,一张常规输入图大概要覆盖上千个位置。128 对 1000+,这个差距就是收敛速度和显存占用的来源。
编码器和解码器用的都是同一个可变形注意力模块,但参考点的来源不同。解码器的参考点来自查询本身(或者在迭代框回归模式下,来自上一层预测出的框中心);编码器的参考点则来自特征图上的网格位置,并且会按有效区域比例缩放,避免 padding 区域被采样到。
另外,编码器的自注意力输出会加上位置编码。Deformable DETR 用的是正弦位置编码,并且在两阶段版本里引入了temperature参数(默认 20)来调节位置编码的尺度。这个小参数容易被忽略,但它对两阶段变体的稳定性有实际影响,调参时不要随手改。
2.3 复杂度与显存的账,算清楚再动手
我习惯在动手前把复杂度算一遍,这样对显存和训练时间心里有数。假设输入 800×1333,四层特征的分辨率大约是 100×167、50×84、25×42、13×21,总 token 数约 16700 + 4200 + 1050 + 273,加起来两万出头。
| 模块 | 每个查询关联的位置数 | 计算复杂度量级 | 备注 |
|---|---|---|---|
| DETR 编码器全局自注意力 | 约 1000(仅 C5) | O((HW)²) | 加高分辨率层会平方级爆炸 |
| Deformable 注意力 | 128(M8×K4×L4) | O(HW·M·K·L) | 对 token 数近似线性 |
| 解码器交叉注意力 | 300 个查询 × 128 | 与 token 数无关 | 这就是能加多层的底气 |
从表里能看出来,Deformable 注意力的复杂度对特征图大小是近似线性的,所以你可以放心把高分辨率层加进来。加一层 C3(stride 8),token 数增加约 1.4 万,但计算量只线性增长,而小目标精度会有肉眼可见的提升。我实测在工业质检的小缺陷数据集上,加上 C3 后 AP_S 能涨三到五个点。
显存方面,训练时的瓶颈通常不在注意力本身,而在主干网络的高分辨率激活值和 batch 大小。以 ResNet-50 为例,单卡 24G 显存、batch size 为 2、输入短边 800,基本是安全的;如果想开 batch 4,要么降分辨率,要么开启梯度检查点,代价是训练速度慢三成左右。
提示:不要一上来就盲目堆特征层。层数从 4 加到 5 时,编码器自注意力的参考点计算和显存都会涨,先确认你的收益来自小目标而不是噪声。
3. 环境搭建与数据准备:把训练真正跑起来
原理讲完,进入动手环节。我按"依赖装对—数据摆对—配置看对"三步来写,每一步都会说明为什么这么做,以及版本不对会出现什么症状。
3.1 依赖版本与自定义 CUDA 算子编译
Deformable DETR 最容易被卡住的地方就是自定义算子的编译。因为它包含前向和反向的 CUDA 核,必须和你的 PyTorch、CUDA 版本严格对应,否则会出现编译报错或者"能编译但运行报 undefined symbol"。
版本选择的经验是:PyTorch 用官方预编译版本对应 CUDA 版本,比如 PyTorch 1.13 配 CUDA 11.7,或者 PyTorch 2.x 配 CUDA 11.8/12.1。系统里的nvcc版本必须和 PyTorch 自带的 CUDA 版本一致,这一点最容易被忽略——很多人 pip 装的 PyTorch 带的是 11.7,但系统装的 CUDA toolkit 是 12.1,编译时就会因为 ABI 不匹配出问题。
编译命令如下:
# 进入自定义算子的目录(以官方 pytorch 实现为例) cd ./models/ops # 指定目标架构可以显著缩短编译时间并避免兼容问题 # 比如 A100 是 8.0,3090 是 8.6,V100 是 7.0,具体查官方算力表 export TORCH_CUDA_ARCH_LIST="8.0" export CUDA_HOME=/usr/local/cuda-11.7 python setup.py build_ext --inplace # 官方自带正确性校验,务必跑通再做后续操作 python test.pytest.py会对比自定义算子和 PyTorch 纯实现的前向、反向结果,误差在容忍范围内才算通过。这一步省不掉,我见过好几次编译成功但反向梯度算错的情况,直接训下去 loss 就是不降,非常折腾。
如果你用 MMDetection 生态,它会通过扩展的方式自动编译。用可编辑模式安装,编译日志会打在终端里,注意看有没有 warning 级别的兼容提示:
pip install -v -e .注意:编译报错后不要直接重试,先把
build/目录和*.so清掉。残留的中间产物会导致"改了配置但报错信息一模一样"这种灵异现象。
3.2 数据集组织与标注格式转换
Deformable DETR 官方实现默认吃 COCO 格式。如果你手上是 VOC 的 XML、LabelImg 的 txt 或者自研平台导出的 json,统一转成 COCO 是最省事的路径。
COCO 的核心结构就三块:images记录图片名、宽高、id;annotations记录每个框的image_id、category_id、bbox;categories记录类别 id 和名称。这里有两个容易翻车的地方。
第一,bbox格式是[x, y, width, height],原点在左上角,而不是[x1, y1, x2, y2]。我第一次转数据时搞混了,训练出来的框全部偏移,loss 看着在降但可视化合不上,查了大半天。
第二,category_id必须从 1 开始连续编号,不能从 0 开始。因为背景在匹配时会被特殊处理,很多实现里 0 被隐式占用了。如果你的类别是 0 到 9,转换时统一加 1。
一个最小可用的转换脚本大概长这样:
import json import os from PIL import Image def voc_to_coco(anno_list, img_dir, class_names, out_path): coco = { "images": [], "annotations": [], "categories": [{"id": i + 1, "name": n} for i, n in enumerate(class_names)] } ann_id = 1 name2id = {n: i + 1 for i, n in enumerate(class_names)} for img_id, (img_name, boxes) in enumerate(anno_list, start=1): w, h = Image.open(os.path.join(img_dir, img_name)).size coco["images"].append( {"id": img_id, "file_name": img_name, "width": w, "height": h} ) for x1, y1, x2, y2, cls in boxes: # 关键:COCO 用 xywh,且需要裁剪到图像范围内 x1, y1 = max(0, x1), max(0, y1) x2, y2 = min(w, x2), min(h, y2) if x2 - x1 < 1 or y2 - y1 < 1: continue # 过滤掉退化的框 coco["annotations"].append({ "id": ann_id, "image_id": img_id, "category_id": name2id[cls], "bbox": [x1, y1, x2 - x1, y2 - y1], "area": (x2 - x1) * (y2 - y1), "iscrowd": 0 }) ann_id += 1 with open(out_path, "w") as f: json.dump(coco, f)写完记得做一次自检:随机抽十张图,按标注画框存图看一眼。这一步花五分钟,能省掉后面几小时的困惑。另外统计一下每类的框数量,如果某类少于 50 个,训练时基本学不出来,考虑合并类别或者补充数据。
3.3 配置文件里那些真正重要的参数
配置文件看起来参数很多,但真正影响结果的就那么十来个。我把它们列出来并说明取值逻辑。
# 取自官方配置的关键片段,数值为论文默认设置 num_queries = 300 # 查询数量 num_feature_levels = 4 # 使用 C3/C4/C5/C6 四层 enc_layers = 6 # 编码器层数 dec_layers = 6 # 解码器层数 hidden_dim = 256 # 特征维度 nheads = 8 # 注意力头数 enc_n_points = 4 # 编码器每头采样点数 dec_n_points = 4 # 解码器每头采样点数 dim_feedforward = 1024 # 前馈层维度 dropout = 0.1 # 损失权重 set_cost_class = 2 # 匹配时分类代价 set_cost_bbox = 5 # 匹配时 L1 代价 set_cost_giou = 2 # 匹配时 GIoU 代价 cls_loss_coef = 2 bbox_loss_coef = 5 giou_loss_coef = 2 focal_alpha = 0.25 # 两阶段版本里 focal loss 的 alpha # 优化器 lr_backbone = 1e-5 # 主干学习率,必须比主体小一个量级 lr = 1e-4 # Transformer 部分学习率 weight_decay = 1e-4 clip_max_norm = 0.1 # 梯度裁剪 lr_drop = 40 # 第 40 epoch 衰减 epochs = 50num_queries从 DETR 的 100 提到 300 是有原因的。查询数必须大于单图最大目标数,否则会出现"目标数超过查询数导致漏检"。但也不是越大越好,查询数翻倍会让解码器计算量和显存同步上升,而且多余查询需要靠 loss 压制(成本最低的匹配是匹配到背景),会轻微拖慢收敛。400 到 500 是个常见上限,除非你的场景单图目标数确实很多。
lr_backbone比lr小十倍,是因为主干是预训练权重,学习率太大会把预训练特征破坏掉,也就是常说的灾难性遗忘。我见过有人为了"加速收敛"把主干学习率也设成 1e-4,结果前几个 epoch 精度直接崩掉,还以为是数据问题。
clip_max_norm = 0.1这个值偏小,是针对注意力机制梯度波动大的特点。如果你的 loss 曲线偶尔有尖刺,不要急着改学习率,先确认裁剪是否生效。
4. 训练、评估与推理的完整实操流程
环境、数据、配置都准备好之后,进入真正的训练环节。这部分我按时间顺序写,包含耗时估算和中间观察点。
4.1 启动训练与资源估算
单机八卡的启动命令:
python -m torch.distributed.launch --nproc_per_node=8 --use_env main.py \ --config_file configs/r50_deformable_detr.sh \ --output_dir ./exps/r50_deformable_detr如果你只有单卡,把nproc_per_node改成 1,同时把 batch size 从 2 降到 1 或者用梯度累积凑等效批量。这里要说明:官方配置是每卡 batch size 为 2、八卡合计 16。批量太小会让匹配算法(匈牙利匹配)的结果不稳定,因为每张图单独匹配,批量只影响梯度平滑度。所以单卡训练时建议用梯度累积,累积步数设成 8,等效批量 16,这时学习率可以保持不变。
耗时上,ResNet-50 版本、50 epoch、COCO 全集、八卡 V100,大概一到一个半天。换成 A100 能压到八小时左右。如果你只想验证流程是否跑通,强烈建议先用 COCO 的一个子集(比如 5000 张图)跑 5 个 epoch,看 loss 是否正常下降、评估脚本是否能跑通,再上全量。
评估命令:
python -m torch.distributed.launch --nproc_per_node=8 --use_env main.py \ --config_file configs/r50_deformable_detr.sh \ --eval --resume ./exps/r50_deformable_detr/checkpoint.pth4.2 训练过程怎么观察,loss 曲线怎么看
跑起来之后不要就等着,盯三个东西:总 loss 的趋势、分类 loss 和回归 loss 的相对关系、以及学习率衰减前后的变化。
正常的曲线长这样:前 3 到 5 个 epoch loss 下降很快,从十几降到个位数;5 到 30 epoch 缓慢下降,中间会有平台期;第 40 epoch 学习率衰减一次,loss 会有一个台阶式下探;最后 10 个 epoch 基本平稳。如果在第 10 个 epoch 之后 loss 就纹丝不动,大概率是学习率太小或者匹配出问题了。
分类 loss 和回归 loss 的比例也要看。如果分类 loss 一直是回归 loss 的十倍以上,说明模型在纠结"这是什么"而不是"在哪里",可能是类别不平衡严重,可以考虑调整 focal loss 的 alpha 或者损失权重。反之如果回归 loss 占主导,通常是标注框噪声大。
一个实用的判断技巧:把num_queries匹配到背景的比例打印出来。如果超过九成查询都匹配到背景,说明查询数严重过剩,可以适当减少。这个比例在训练稳定后通常落在七到九成之间比较正常。
提示:Deformable DETR 的收敛速度和数据量强相关。数据量小于三千张时,建议把 epoch 降到 30 并调小学习率,否则容易过拟合,典型表现是训练 loss 还在降但验证 AP 已经开始掉了。
4.3 推理与可视化验证
推理代码很简洁,因为是端到端,不需要 NMS。核心流程是:预处理图像、前向、取分类分数超过阈值的预测、把归一化坐标还原到原图尺寸。
import torch from PIL import Image import torchvision.transforms as T # 预处理:短边缩放到 800,长边不超过 1333,与训练保持一致 transform = T.Compose([ T.Resize(800), T.ToTensor(), T.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) model.eval() img = Image.open("test.jpg").convert("RGB") w, h = img.size x = transform(img).unsqueeze(0) with torch.no_grad(): out = model(x) # logits: [1, num_queries, num_classes + 1],最后一维是背景 probs = out["pred_logits"].softmax(-1)[0, :, :-1] keep = probs.max(-1).values > 0.5 boxes = out["pred_boxes"][0, keep] # 归一化的 cxcywh 还原到原图坐标 scale = torch.tensor([w, h, w, h]) boxes = boxes * scale cx, cy, bw, bh = boxes.unbind(-1) xyxy = torch.stack([cx - bw / 2, cy - bh / 2, cx + bw / 2, cy + bh / 2], dim=-1)可视化时我建议做两件事。一是把阈值从 0.5 降到 0.1 再看一遍,观察有没有大量低分框堆积在同一位置——如果有,通常是训练不充分或者查询数过多;二是把预测框和真值框画在同一张图上对比,重点看小目标的偏移方向,偏右上或偏左下往往提示坐标格式转换时有半个像素的系统偏差。
4.4 迭代框回归与两阶段变体该怎么选
官方给了三个递进的变体,很多人不知道该用哪个。我的建议是看你的需求:
基础版(多尺度可变形注意力),50 epoch,配置最简单,复现风险最低。如果你第一次上手,从这里开始。
加迭代框回归版,解码器每一层都基于上一层预测的框中心重新采样,逐层细化。这个改动能涨一到两个点,实现上要注意一点:参考点在层间传递时梯度需要 detach,否则反向传播会跨层回传,训练会不稳定甚至发散。这个细节在论文里提了一句,但代码里非常关键。
两阶段版,先用编码器输出加一个小检测头生成候选区域(用 focal loss 监督),再把候选框的中心作为解码器的初始参考点。相当于给解码器一个高质量的起点,精度再涨零点几个点,代价是训练多一点点开销,推理也几乎不变。如果你的场景目标比较密集,两阶段版对小目标和遮挡目标的改善更明显。
我实际项目的选择顺序是:先跑基础版建立基线,再加迭代框回归看提升幅度,最后试两阶段。不要一上来就上两阶段,因为一旦出问题,排查路径会长很多。
5. 常见问题排查与调优经验
这部分是我踩坑最集中的地方,也是我认为最有价值的部分。官方文档不会告诉你这些,因为它们只在特定环境组合下才会出现。
5.1 问题速查表
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 自定义算子编译失败 | nvcc 版本与 PyTorch CUDA 版本不一致 | 统一两者版本,清 build 目录重编 |
| 编译通过但运行报 undefined symbol | 编译时的 PyTorch ABI 与运行环境不一致 | 在同一虚拟环境内完成编译和运行 |
| loss 从头到尾是 NaN | 学习率过大或梯度爆炸 | 确认梯度裁剪生效,主干学习率保持 1e-5 |
| loss 下降但 AP 极低 | 坐标格式错误或类别 id 从 0 开始 | 检查 COCO 的 xywh 与 category_id 起始值 |
| AP 比论文低很多 | 数据增强、输入尺度、BN 设置不一致 | 逐项对齐官方配置,冻结主干 BN |
| 小目标 AP_S 明显偏低 | 未使用高分辨率特征层 | 加入 C3 层并适当提高输入分辨率 |
| 显存溢出 | 分辨率过大、查询数过多、层数过多 | 降 batch,启用梯度检查点,减少特征层 |
| 推理结果出现重复框 | 误加了 NMS 或阈值过低 | 端到端模型无需 NMS,提高置信度阈值 |
| 训练后期精度回退 | 过拟合 | 加数据、增强、降低 epoch 或加 dropout |
| 多卡训练速度没有提升 | 数据加载成为瓶颈 | 增加 dataloader workers,使用预读取 |
5.2 小目标与密集场景的针对性调优
小目标差是很多人换到 Deformable DETR 的主要动机,但如果只是默认配置跑一遍,提升可能没有预期大。我总结了几条实际有效的做法。
第一,确认 C3 层真的被用上了。有些配置里num_feature_levels设成 3,用的是 C4/C5/C6,那就丢掉了高分辨率信息。小目标场景一定要用 4 层,并且检查主干输出的 stride 是否真的是 8。
第二,输入分辨率不要压得太狠。如果显存允许,把短边从 800 提到 1000 甚至 1200,小目标提升立竿见影。代价是显存和耗时线性上升,需要权衡。
第三,数据增强里慎用大幅随机缩放和裁剪。小目标本来就小,再缩一次就没了。我一般把随机缩放的幅度控制在 0.8 到 1.25 之间,不用 0.5 到 2.0 那种大范围。
第四,也是容易被忽略的一点:采样点数可以适当增加。从 4 提到 8,每个查询能覆盖更多位置,对小目标有帮助。但这会让计算量翻倍,属于"有预算就上"的选项。
密集场景下的另一个技巧是调整匹配代价里的分类权重。当目标密集时,相邻目标容易在匹配阶段混淆,适当提高 GIoU 代价的权重(比如从 2 提到 3)能让匹配更倾向位置准确的一对一关系,减少漏检。
5.3 迁移到自定义数据集的真实经验
从 COCO 迁到自己的数据,有几件事我每次都做,效果很实在。
第一件事是先冻结主干训练几个 epoch,再解冻微调。虽然官方配置里主干学习率已经很小,但对于和自然图像差异大的数据(比如 X 光、显微镜图像),一开始就更新主干仍然会破坏特征。我会先把主干的学习率设为 0 跑 3 个 epoch,让检测头先拟合,再恢复到 1e-5。
第二件事是检查类别 id 的连续性。前面提过,但值得再强调一次。自定义数据集里如果类别 id 有跳号,某些实现会直接报错,某些则会静默地多出一堆空类别,表现为某些类永远检不出来。
第三件事是关于评估指标的诚实性。COCO 的 mAP 在类别少、样本不均衡的场景下会失真。比如你只有两类,其中一类占了九成样本,那么 mAP 会被大类主导。我习惯额外算一遍每类的 AP 和混淆矩阵,这两个指标对业务方更有说服力。
第四件事是混合训练。如果自己的数据只有几千张,把 COCO 按一定比例混进来一起训,精度通常比纯自训高,而且收敛更稳。混合比例我一般用自研数据为主、COCO 子集占三分之一左右,采样时按类别做均衡。
6. 一些后续可扩展的方向
跑通之后如果还想继续往下走,有几个方向是自然延伸的。一个是把可变形注意力的采样点可视化出来,看每个查询到底关注了图像的哪些区域,这对排查"为什么这个目标漏检"非常直观,也能发现标注错误。做法是把sampling_offsets和参考点相加,还原到原图坐标画点,我每次调优都会做这一步。
另一个方向是蒸馏和剪枝。Deformable DETR 的编码器六层、解码器六层,推理速度在服务端够用但不算快。把编码器层数降到三层、查询数降到 200,精度掉一个点左右,推理速度能快四成,很多线上场景这个交换是划算的。我做过一次,在保证业务指标不降的前提下把单帧耗时压到了原来的一半多,靠的就是层数和查询数的联合裁剪,再加上把输入短边从 1000 降到 850。
还有一条路是换更强的查询设计,比如后续工作里引入的去噪训练和更好的参考点初始化策略。这部分在 Deformable DETR 的基础上改动不算大,收益却很明显,值得花时间读一读再动手改。我个人在实际操作中的体会是,检测模型的精度提升往往不来自某个花哨的模块,而来自训练策略的细节打磨——匹配代价怎么设、参考点怎么初始化、标签分配怎么稳定,这些看起来不起眼的地方,累积起来常常比换主干网络更管用。