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

资讯详情

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

DETR:基于Transformer的端到端目标检测原理与PyTorch实战

DETR:基于Transformer的端到端目标检测原理与PyTorch实战 1. 项目概述从“两阶段”到“端到端”的范式革命如果你在过去几年里做过目标检测无论是用Faster R-CNN、YOLO还是SSD大概率都绕不开一个核心概念锚框Anchor Boxes和非极大值抑制NMS。我们习惯了先预设一堆大小形状各异的候选框让模型去判断哪个框里有物体、是什么物体最后再用NMS算法去掉那些重叠的、冗余的预测框。这套流程很有效但总让人觉得有点“不优雅”——它充满了手工设计的痕迹像是一个拼凑起来的流水线而不是一个浑然天成的系统。直到2020年Facebook AI ResearchFAIR的那篇论文《End-to-End Object Detection with Transformers》横空出世带来了DETRDEtection TRansformer。我第一次读到它时感觉就像在满是齿轮和杠杆的机械钟表世界里突然看到了一块石英表。DETR的核心思想极其简洁把目标检测彻底变成一个集合预测问题。它不需要锚框也不需要NMS直接输入图像输出一组固定数量的、无序的预测框和类别。这种“端到端”的纯粹性正是Transformer架构在计算机视觉领域一次漂亮的“降维打击”。简单来说DETR干了这么几件事1用CNN骨干网络如ResNet提取图像特征2用Transformer的编码器-解码器结构来理解这些特征中的全局上下文关系3用一个简单的预测头为解码器输出的每个“对象查询”直接预测一个框中心点、宽高和一个类别。整个模型通过一个二分图匹配损失进行训练强制模型为每个真实物体分配一个唯一的预测。那么DETR适合谁呢我认为有三类朋友会特别感兴趣一是厌倦了调Anchor和NMS阈值的研究者或工程师想体验更干净的范式二是希望将检测模型无缝嵌入到更大、更复杂多模态流水线比如图像描述、VQA中的开发者DETR的统一输出格式是绝佳的接口三是任何对Transformer如何在CV领域大放异彩感到好奇的学习者。接下来我们就深入这个“石英表”的内部看看它的齿轮是如何咬合的。2. DETR核心原理深度拆解为什么是Transformer要理解DETR必须先理解它为什么选择Transformer以及它如何解决了传统目标检测的固有顽疾。这不仅仅是“把NLP的东西搬过来”而是一次针对视觉任务特点的深刻重构。2.1 传统检测的“手工流水线”与DETR的“统一建模”传统两阶段或单阶段检测器其流程可以概括为生成候选区域Faster R-CNN用RPN区域提议网络YOLO/SSD在特征图上铺设密集的锚框。这些锚框的大小、长宽比都是超参数需要根据数据集精心设计。对于形状特异的物体比如长杆状的高尔夫球杆预设锚框很难完美匹配。特征提取与分类/回归对每个候选区域提取特征并行执行类别分类和边界框坐标回归。后处理最重要的就是NMS。因为多个锚框可能预测同一个物体需要根据置信度排序抑制掉重叠度高的、置信度低的预测。NMS本身也有阈值如IoU0.5这个超参数调起来并不省心。这套流程的问题在于它把“找物体”和“区分物体”这两个本应紧密关联的任务在某种程度上解耦了。锚框的生成是局部的、启发式的NMS是启发式的、非学习的。整个系统不是在一个统一的、可微分的损失函数下进行端到端优化。DETR的解决方案是集合预测。它设定模型输出一个固定大小为N的集合N远大于图像中物体的典型数量比如100。集合中的每个元素包含一个类别预测包含“无物体”类和一个边界框预测。训练的关键在于如何将这N个预测与图像中M个真实物体对应起来这里就用到了匈牙利算法来寻找最优的二分图匹配使得匹配后的预测框和真实框之间的总体差异最小。匹配完成后再计算类别交叉熵损失和边界框损失L1损失GIOU损失。这样一来模型在训练过程中就直接学会了如何为每个真实物体分配一个唯一的预测从而在推理时天然避免了冗余框彻底抛弃了NMS。2.2 Transformer在DETR中的角色全局关系推理引擎CNN擅长提取局部特征但感受野有限难以建立图像中远距离物体之间的关系比如判断一个人是否拿着一个杯子。Transformer的自注意力机制恰恰是建立这种全局上下文的利器。在DETR中Transformer扮演了“关系推理与信息聚合”的核心角色编码器CNN骨干网络提取的2D特征图例如下采样32倍后的[C, H, W]被展平为1D序列[HW, C]并加上位置编码标准的正弦编码或可学习编码。编码器通过多层自注意力层让序列中的每一个“像素特征”都能与所有其他“像素特征”进行交互。这个过程让模型理解了“这个角落的轮子属于中间那辆车”而不是孤立地看一个个局部特征。解码器这是DETR最具创新性的部分之一。解码器的输入包括两部分一是编码器输出的内存Memory二是对象查询Object Queries。对象查询是一组可学习的嵌入向量长度为N你可以把它们理解为N个“问题”每个问题都在向编码器内存“询问”“图像中有一个物体吗它在哪里它是什么”。解码器通过交叉注意力机制让每个对象查询去关注编码器内存中与它最相关的部分从而解码出物体的信息和位置。不同的对象查询会通过训练自发地学习去关注图像中不同的区域或物体。注意对象查询是无序的。这意味着“第一个查询”并不对应“最重要的物体”。它们的顺序在训练和推理中保持一致但具体哪个查询对应哪个物体是由模型通过二分图匹配动态决定的。这是理解DETR输出是“集合”而非“序列”的关键。2.3 二分图匹配损失让无序输出对应有序真值这是训练DETR的“灵魂”。假设我们有N个预测ŷ和M个真实物体y通常MN。我们需要找到一个从N到M的映射未匹配的预测视为“无物体”背景类使得总成本最低。具体步骤构造成本矩阵对于每一对预测i和真实物体j计算一个成本C_ij。这个成本通常是负的匹配度DETR中定义为C_ij -p_i(c_j) L_box(b_i, b_j)其中p_i(c_j)是预测i对于真实物体j类别的预测概率L_box是边界框损失L1 GIOU。我们希望类别概率高、框位置准的配对成本低。使用匈牙利算法scipy.optimize.linear_sum_assignment找到使总成本最小的唯一匹配。基于这个最优匹配计算最终的损失匹配上的预测计算类别损失和框损失未匹配上的预测只计算“无物体”类别的损失。这个过程强制模型学会去重和分配。它不像传统检测器那样每个锚框独立作战而是让所有预测在损失函数的约束下协同工作避免多个预测都去“抢”同一个简单的物体。3. 模型结构详解与PyTorch实现关键点纸上得来终觉浅我们直接深入到代码层面看看一个标准的DETR模型是如何用PyTorch搭建起来的。这里我会结合官方实现和我的实践经验指出那些容易踩坑的关键部位。3.1 骨干网络与位置编码DETR通常使用ResNet-50或ResNet-101作为骨干网络去掉最后的全局平均池化和全连接层只保留卷积部分。从ImageNet预训练的权重开始微调是标准操作能极大加速收敛。import torch import torch.nn as nn import torchvision.models as models from torchvision.ops import FrozenBatchNorm2d class Backbone(nn.Module): def __init__(self, backbone_nameresnet50, train_backboneFalse, dilationFalse, hidden_dim256): super().__init__() # 加载预训练ResNet backbone getattr(models, backbone_name)(weightsmodels.ResNet50_Weights.DEFAULT) # 通常只训练stage4及以后的部分防止破坏预训练好的低级特征 for name, parameter in backbone.named_parameters(): if not train_backbone and layer2 not in name and layer3 not in name and layer4 not in name: parameter.requires_grad_(False) # 提取中间层输出。DETR需要最后一层特征图有时也会用到中间层特征对于多尺度版本的Deformable DETR。 self.body nn.Sequential(*list(backbone.children())[:-2]) # 去掉avgpool和fc self.num_channels 2048 if backbone_name in [resnet50, resnet101] else 512 # 一个1x1卷积将骨干网络输出通道数投影到Transformer所需的隐藏维度hidden_dim self.conv nn.Conv2d(self.num_channels, hidden_dim, 1) def forward(self, x): # x: [batch, 3, H, W] features self.body(x) # [batch, 2048, H/32, W/32] features self.conv(features) # [batch, hidden_dim, H/32, W/32] return features位置编码至关重要因为Transformer本身是置换不变的需要注入空间信息。DETR使用标准的正弦位置编码但作用于2D特征图。我们需要生成与特征图空间位置对应的编码然后加到特征上。import math import torch.nn.functional as F class PositionEmbeddingSine(nn.Module): def __init__(self, num_pos_feats64, temperature10000, normalizeFalse, scaleNone): super().__init__() self.num_pos_feats num_pos_feats self.temperature temperature self.normalize normalize if scale is not None and normalize is False: raise ValueError(normalize should be True if scale is passed) if scale is None: scale 2 * math.pi self.scale scale def forward(self, x, maskNone): # x: [batch, hidden_dim, H, W] # mask: [batch, H, W] (optional, 表示padding区域) if mask is None: mask torch.zeros((x.size(0), x.size(2), x.size(3)), devicex.device, dtypetorch.bool) not_mask ~mask y_embed not_mask.cumsum(1, dtypetorch.float32) # 沿高度方向累加 x_embed not_mask.cumsum(2, dtypetorch.float32) # 沿宽度方向累加 if self.normalize: eps 1e-6 y_embed y_embed / (y_embed[:, -1:, :] eps) * self.scale x_embed x_embed / (x_embed[:, :, -1:] eps) * self.scale dim_t torch.arange(self.num_pos_feats, dtypetorch.float32, devicex.device) dim_t self.temperature ** (2 * (dim_t // 2) / self.num_pos_feats) pos_x x_embed[:, :, :, None] / dim_t pos_y y_embed[:, :, :, None] / dim_t pos_x torch.stack((pos_x[:, :, :, 0::2].sin(), pos_x[:, :, :, 1::2].cos()), dim4).flatten(3) pos_y torch.stack((pos_y[:, :, :, 0::2].sin(), pos_y[:, :, :, 1::2].cos()), dim4).flatten(3) pos torch.cat((pos_y, pos_x), dim3).permute(0, 3, 1, 2) # [batch, num_pos_feats*2, H, W] return pos实操心得位置编码的维度和hidden_dim的关系。num_pos_feats通常设为hidden_dim // 2这样pos的通道数就是hidden_dim可以直接与features相加。确保features和pos的尺寸完全一致[batch, hidden_dim, H, W]是调试时的第一个检查点。3.2 Transformer编码器-解码器构建这里我们使用PyTorch自带的nn.Transformer模块可以快速搭建但为了更清晰地理解DETR的细节我们仿照其结构进行分解。编码器由多个相同的层堆叠而成每层包含一个多头自注意力MSA和一个前馈网络FFN都有残差连接和层归一化。class TransformerEncoderLayer(nn.Module): def __init__(self, d_model, nhead, dim_feedforward2048, dropout0.1): super().__init__() self.self_attn nn.MultiheadAttention(d_model, nhead, dropoutdropout, batch_firstTrue) self.linear1 nn.Linear(d_model, dim_feedforward) self.dropout nn.Dropout(dropout) self.linear2 nn.Linear(dim_feedforward, d_model) self.norm1 nn.LayerNorm(d_model) self.norm2 nn.LayerNorm(d_model) self.dropout1 nn.Dropout(dropout) self.dropout2 nn.Dropout(dropout) self.activation nn.ReLU() def forward(self, src, src_maskNone, src_key_padding_maskNone): # src: [batch_size, src_len, d_model] src2 self.norm1(src) src2, _ self.self_attn(src2, src2, src2, attn_masksrc_mask, key_padding_masksrc_key_padding_mask) src src self.dropout1(src2) src2 self.norm2(src) src2 self.linear2(self.dropout(self.activation(self.linear1(src2)))) src src self.dropout2(src2) return src解码器层稍复杂包含自注意力关注已解码的输出、交叉注意力关注编码器内存和FFN。class TransformerDecoderLayer(nn.Module): def __init__(self, d_model, nhead, dim_feedforward2048, dropout0.1): super().__init__() self.self_attn nn.MultiheadAttention(d_model, nhead, dropoutdropout, batch_firstTrue) self.multihead_attn nn.MultiheadAttention(d_model, nhead, dropoutdropout, batch_firstTrue) # 交叉注意力 self.linear1 nn.Linear(d_model, dim_feedforward) self.dropout nn.Dropout(dropout) self.linear2 nn.Linear(dim_feedforward, d_model) self.norm1 nn.LayerNorm(d_model) self.norm2 nn.LayerNorm(d_model) self.norm3 nn.LayerNorm(d_model) self.dropout1 nn.Dropout(dropout) self.dropout2 nn.Dropout(dropout) self.dropout3 nn.Dropout(dropout) self.activation nn.ReLU() def forward(self, tgt, memory, tgt_maskNone, memory_maskNone, tgt_key_padding_maskNone, memory_key_padding_maskNone): # tgt: [batch_size, tgt_len, d_model] (对象查询) # memory: [batch_size, src_len, d_model] (编码器输出) tgt2 self.norm1(tgt) tgt2, _ self.self_attn(tgt2, tgt2, tgt2, attn_masktgt_mask, key_padding_masktgt_key_padding_mask) tgt tgt self.dropout1(tgt2) tgt2 self.norm2(tgt) tgt2, attn_weights self.multihead_attn(tgt2, memory, memory, attn_maskmemory_mask, key_padding_maskmemory_key_padding_mask) tgt tgt self.dropout2(tgt2) tgt2 self.norm3(tgt) tgt2 self.linear2(self.dropout(self.activation(self.linear1(tgt2)))) tgt tgt self.dropout3(tgt2) return tgt, attn_weights对象查询是一个可学习的nn.Embedding层在解码开始时被输入。它的形状是[num_queries, d_model]在批次维度上广播。class DETR(nn.Module): def __init__(self, backbone, transformer, num_classes, num_queries, hidden_dim): super().__init__() self.backbone backbone self.transformer transformer self.num_queries num_queries # 对象查询 self.query_embed nn.Embedding(num_queries, hidden_dim) # 预测头一个简单的FFN为每个解码器输出预测类别和框 self.class_embed nn.Linear(hidden_dim, num_classes 1) # 1 for no object self.bbox_embed MLP(hidden_dim, hidden_dim, 4, 3) # 预测 (cx, cy, w, h) 归一化坐标 def forward(self, images): # 1. 特征提取 features self.backbone(images) # [batch, hidden_dim, H, W] batch, dim, H, W features.shape # 2. 展平并添加位置编码 features_flat features.flatten(2).permute(0, 2, 1) # [batch, H*W, hidden_dim] pos_encoding self.position_encoding(features).flatten(2).permute(0, 2, 1) # 3. Transformer编码器 memory self.transformer.encoder(features_flat, pospos_encoding) # 4. 准备对象查询并解码 query_embed self.query_embed.weight.unsqueeze(0).repeat(batch, 1, 1) # [batch, num_queries, hidden_dim] tgt torch.zeros_like(query_embed) hs self.transformer.decoder(tgt, memory, pospos_encoding, query_posquery_embed) # hs: [decoder_layers, batch, num_queries, hidden_dim] # 5. 预测头通常取最后一层解码器输出 outputs_class self.class_embed(hs[-1]) outputs_coord self.bbox_embed(hs[-1]).sigmoid() # 输出归一化到[0,1] return {pred_logits: outputs_class, pred_boxes: outputs_coord}注意事项解码器输入tgt初始化为零这是标准做法。query_embed作为query_pos参数传入为解码过程提供位置先验。不同的对象查询会逐渐分化关注图像的不同部分。预测框坐标通过sigmoid归一化代表相对于图像尺寸的相对位置。3.3 预测头与损失计算预测头非常简单就是两个全连接网络对于框预测DETR用了3层隐藏层的MLP。损失计算是核心难点。import torch import torch.nn.functional as F from scipy.optimize import linear_sum_assignment def hungarian_matcher(pred_logits, pred_boxes, targets): pred_logits: [batch, num_queries, num_classes1] pred_boxes: [batch, num_queries, 4] (cx, cy, w, h) targets: list of dict with keys labels and boxes (绝对坐标) bs, num_queries pred_logits.shape[:2] indices [] for i in range(bs): tgt_labels targets[i][labels] # [M] tgt_boxes targets[i][boxes] # [M, 4] M tgt_boxes.shape[0] # 计算成本矩阵 cost_class -pred_logits[i, :, tgt_labels] # [num_queries, M] # 计算框损失成本 pred_boxes_i pred_boxes[i] # [num_queries, 4] # 将预测的归一化坐标转换为绝对坐标假设知道图像尺寸 # 这里简化处理实际需传入图像尺寸 # cost_bbox ... 计算L1和GIOU # 例如box_costs torch.cdist(pred_boxes_i, tgt_boxes, p1) # L1距离 # 总成本 C cost_class cost_bbox C C.reshape(num_queries, M).cpu().detach().numpy() # 匈牙利匹配 row_ind, col_ind linear_sum_assignment(C) indices.append((row_ind, col_ind)) return indices def detr_loss(pred_logits, pred_boxes, targets, matcher): indices matcher(pred_logits, pred_boxes, targets) # 根据匹配结果分别计算匹配对和未匹配对的损失... # 分类损失用交叉熵框损失用L1GIOU loss_dict {loss_ce: ..., loss_bbox: ..., loss_giou: ...} return loss_dict踩坑实录匈牙利匹配的计算成本很高尤其是当num_queries较大如100且批次内物体数量变化时。在实际实现中通常会将一个批次内所有目标的成本矩阵拼接起来进行一次性的匈牙利匹配以提高效率。此外GIOU损失的计算需要确保框坐标的格式中心点宽高 vs 左上右下一致否则会导致梯度爆炸或训练不稳定。4. 实战应用从零训练一个DETR模型理论说得再多不如跑通一个训练流程来得实在。这里我将带你走一遍在自定义数据集上训练DETR的关键步骤分享我趟过的雷。4.1 数据准备与COCO格式适配DETR官方代码和大多数复现都默认支持COCO数据集格式。如果你的数据是自定义的将其转换为COCO格式是最省事的路径。一个COCO标注文件的核心结构如下{ images: [ {id: 1, file_name: img1.jpg, width: 640, height: 480}, ... ], annotations: [ {id: 1, image_id: 1, category_id: 3, bbox: [x, y, width, height], area: area, iscrowd: 0}, ... ], categories: [ {id: 1, name: person}, {id: 2, name: bicycle}, ... ] }关键点bbox格式是[x_top_left, y_top_left, width, height]不是[cx, cy, w, h]。area是边界框的面积用于评估指标如mAP。iscrowd为0表示单个物体为1表示一组物体人群DETR通常忽略iscrowd1的标注。使用torchvision.datasets.CocoDetection可以方便地加载数据。但需要注意官方DETR实现做了大量的数据增强包括大规模随机裁剪尺度在0.5到2.0之间、随机水平翻转和颜色抖动。数据增强对DETR的训练至关重要因为Transformer模型需要大量的数据来学习而强大的增强相当于免费的数据。from torchvision import transforms as T import torchvision.transforms.functional as F class MyRandomCrop: 仿照DETR官方实现的大尺度随机裁剪 def __init__(self, scales): self.scales scales def __call__(self, image, target): # 随机选择裁剪尺度在原图尺寸上乘以一个系数 scale random.uniform(self.scales[0], self.scales[1]) # 计算裁剪后的尺寸并确保不超过原图 # 随机选择裁剪起点 # 调整图像和所有目标框的位置 # 移除完全在裁剪区域外的框裁剪部分在区域内的框 return image, target4.2 训练配置与超参数选择DETR的训练以“慢”和“吃资源”著称。以下是一组经过验证的、用于ResNet-50骨干网络的基线超参数超参数推荐值说明骨干网络ResNet-50从torchvision加载ImageNet预训练权重优化器AdamWTransformer系模型标配对权重衰减敏感基础学习率1e-4非常关键比CNN检测器小一个数量级骨干网络学习率1e-5骨干网络使用更小的学习率防止破坏预训练特征权重衰减1e-4批次大小8或16取决于GPU显存可使用梯度累积模拟更大批次训练轮数300至少需要150轮才开始有像样结果300轮收敛学习率调度StepLR在第200轮将学习率降至1e-5隐藏维度256Transformer内部特征维度编码/解码器层数6标准配置注意力头数8前馈网络维度2048Dropout0.1对象查询数100足够覆盖常见数据集的物体数量训练脚本核心循环model DETR(...).cuda() model.train() # 骨干网络参数单独设置学习率 param_dicts [ {params: [p for n, p in model.named_parameters() if backbone not in n and p.requires_grad]}, {params: [p for n, p in model.named_parameters() if backbone in n and p.requires_grad], lr: 1e-5}, ] optimizer torch.optim.AdamW(param_dicts, lr1e-4, weight_decay1e-4) lr_scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size200, gamma0.1) for epoch in range(num_epochs): for images, targets in dataloader: images images.cuda() # 将targets列表转换为模型需要的格式 targets [{k: v.cuda() for k, v in t.items()} for t in targets] outputs model(images) loss_dict criterion(outputs, targets) # criterion包含匈牙利匹配和损失计算 losses sum(loss_dict.values()) optimizer.zero_grad() losses.backward() # 梯度裁剪防止Transformer训练不稳定 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm0.1) optimizer.step() lr_scheduler.step()血泪教训学习率是DETR训练中最关键的开关。一开始我用训练CNN的1e-3学习率损失直接NaN。降到1e-4后训练稳定。另外梯度裁剪Gradient Clipping必不可少尤其是训练初期Transformer的梯度可能很大。clip_grad_norm_的值通常设在0.1到1.0之间。4.3 推理与后处理DETR推理出奇地简单因为没有NMS。但需要对输出进行阈值过滤。def detr_inference(model, image, score_threshold0.7): model.eval() with torch.no_grad(): outputs model(image.unsqueeze(0).cuda()) logits outputs[pred_logits][0] # [100, num_classes1] boxes outputs[pred_boxes][0] # [100, 4] (cx, cy, w, h) 归一化 # 应用softmax获取概率并忽略“无物体”类假设是最后一类 prob F.softmax(logits, dim-1)[:, :-1] # [100, num_classes] scores, labels prob.max(-1) # [100], [100] # 根据置信度阈值过滤 keep scores score_threshold scores scores[keep] labels labels[keep] boxes boxes[keep] # 将归一化坐标 [cx, cy, w, h] 转换回原图尺度的 [x1, y1, x2, y2] # 需要知道原图高宽 img_h, img_w image.shape[1], image.shape[2] boxes box_cxcywh_to_xyxy(boxes) # 转换为xyxy格式 scale_fct torch.tensor([img_w, img_h, img_w, img_h]).cuda() boxes boxes * scale_fct return boxes, labels, scores def box_cxcywh_to_xyxy(x): # 将 (center_x, center_y, width, height) 转换为 (x1, y1, x2, y2) x_c, y_c, w, h x.unbind(-1) b [(x_c - 0.5 * w), (y_c - 0.5 * h), (x_c 0.5 * w), (y_c 0.5 * h)] return torch.stack(b, dim-1)推理速度上DETR相比优化良好的YOLOv5或RetinaNet确实要慢一些主要瓶颈在Transformer的自注意力计算其复杂度与特征图序列长度H*W的平方成正比。这也是后续改进模型如Deformable DETR, Conditional DETR主要优化的方向。5. 常见问题、调优技巧与进阶方向即使按照标准流程训练DETR的路上也少不了坑。这里汇总了我遇到的一些典型问题及其解决方案。5.1 训练不稳定与收敛慢问题表现损失震荡剧烈或者下降极其缓慢训练几十轮了mAP还是接近零。检查学习率这是首要怀疑对象。立刻检查你的学习率设置。对于AdamW1e-4是安全的起点。骨干网络部分的学习率应再小10倍1e-5。检查数据增强DETR需要强数据增强。确保你使用了大规模随机裁剪如随机将图像缩放到[480, 800]之间然后裁剪出固定尺寸。弱增强会导致模型严重过拟合在验证集上表现很差。检查梯度裁剪在loss.backward()之后、optimizer.step()之前加入torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm0.1)。这对稳定Transformer训练非常有效。检查损失权重DETR的总损失是分类损失、L1框损失和GIOU损失的加权和。官方实现中分类损失的权重是1L1损失是5GIOU损失是2。如果框损失权重太小模型可能只关注分类而忽略定位。预热Warmup在训练的最开始如前1000次迭代使用一个从0线性增加到基础学习率的学习率调度策略有助于模型稳定进入训练状态。5.2 小物体检测性能差这是DETR被诟病最多的一点。原因在于CNN骨干网络下采样倍数高通常是32倍小物体在最后的特征图上可能只有几个像素甚至消失。Transformer虽然能建模全局关系但无法从“无”中恢复细节。使用更高分辨率的特征图这是最直接的方法。可以修改骨干网络例如使用ResNet的layer3输出下采样16倍甚至layer2输出下采样8倍作为Transformer的输入。但这会显著增加序列长度H*W导致计算量和内存暴涨。引入FPN特征金字塔网络像Deformable DETR那样将多层特征图融合后输入Transformer为模型提供多尺度信息。使用Deformable Attention这是Deformable DETR的核心创新。它让每个查询只关注特征图上一小部分关键采样点而不是所有位置极大降低了计算复杂度使得使用高分辨率、多尺度特征图成为可能。如果你的应用场景小物体很多强烈建议直接使用Deformable DETR或其变体。5.3 模型部署与优化DETR的Transformer部分在部署时可能不如CNN友好尤其是在边缘设备上。转换为ONNX/TensorRTPyTorch的Transformer层可以顺利导出为ONNX。主要注意点是nn.MultiheadAttention的导出以及动态的序列长度H*W。可以使用PyTorch的torch.onnx.export并设置dynamic_axes参数。使用更高效的注意力实现在支持scaled_dot_product_attentionPyTorch 2.0的平台上可以替换原始的注意力计算获得性能提升。知识蒸馏用一个训练好的、更重的DETR模型如DETR-DC5作为教师模型来蒸馏一个轻量级的学生模型如使用MobileNet作为骨干的DETR可以在精度损失很小的情况下大幅提升速度。5.4 超越原始DETR核心改进方向一览原始DETR打开了端到端检测的大门但后续研究提出了许多重要改进改进模型核心创新点解决的问题推荐场景Deformable DETR可变形注意力机制计算复杂度高、小物体检测差、收敛慢通用推荐收敛快~50轮性能好尤其适合小物体Conditional DETR条件空间查询收敛慢需要快速实验原型时DAB-DETR动态锚框查询将查询显式表示为动态锚点提升可解释性需要理解模型关注点的场景DN-DETR去噪训练加速二分图匹配的收敛与Deformable DETR结合收敛极快DINO对比去噪训练 混合查询选择在Deformable DETR基础上进一步提点SOTA性能追求极致精度的研究或应用个人建议对于大多数实际应用从Deformable DETR开始是一个明智的选择。它解决了原始DETR的主要痛点且有官方和社区的良好实现。原始DETR更适合作为理解端到端检测范式的教学模型。在我自己的项目中将Faster R-CNN替换为Deformable DETR后在包含大量细小文字和图标的面板检测任务上mAP提升了约3个百分点并且彻底摆脱了调整锚框尺寸和NMS阈值的繁琐工作。那种“一个模型、一个损失函数搞定一切”的简洁感是传统方法无法给予的。当然付出的代价是对计算资源更高的需求和更长的训练时间但在许多对精度和流程简洁性有要求的场景下这笔交易是值得的。
返回列表