ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

DETR目标检测实战:从二分图匹配到端到端训练全解析

DETR目标检测实战:从二分图匹配到端到端训练全解析 1. DETR到底想解决什么问题目标检测这个领域在DETR出现之前基本上被Anchor-Based的方法统治了将近十年。从Faster R-CNN到YOLO系列再到SSD大家的核心思路都差不多先在特征图上撒一大堆预设的锚框然后通过分类和回归去修正这些框最后再用NMS非极大值抑制把重复的框去掉。这套流程确实work但它有一个让人非常头疼的问题——整个pipeline太复杂了。你想想看anchor的尺寸怎么设、长宽比怎么选、IoU阈值定多少、NMS的阈值怎么调这些东西全都是超参数。每换一个数据集你可能就得重新调一遍。更麻烦的是NMS这个后处理步骤是不可微的意味着你没法端到端地训练整个网络。而且NMS本身也有问题当两个同类物体靠得很近的时候NMS很容易把其中一个给误删了。DETRDEtection TRansformer的思路就完全不一样了。Facebook AI在2020年发表的这篇论文核心思想就一句话把目标检测当成一个集合预测问题来做。什么意思呢就是说我不再需要anchor不再需要NMS直接让网络输出一个固定大小的预测集合每个元素要么是一个物体包含类别和边界框要么就是没有物体。这样一来整个检测流程就变成了真正的end-to-end。我第一次读DETR论文的时候最直观的感受就是干净。整个架构出奇地简洁一个CNN backbone负责提特征一个Transformer encoder负责全局建模一个Transformer decoder负责生成预测最后接几个简单的FFN头输出类别和框坐标。没有anchor没有NMS没有各种手工设计的组件。这种简洁性在目标检测领域是非常罕见的。但DETR也不是没有代价的。它最大的问题就是收敛慢。在COCO数据集上DETR需要训练500个epoch才能达到比较好的效果而Faster R-CNN可能50个epoch就够了。这个10倍的训练成本在实际项目中是很要命的。后来Deformable DETR、DAB-DETR、DN-DETR等一系列工作都在试图解决这个问题但那是后话了。这篇文章我打算从工程实践的角度把DETR的每个组件都拆开来讲清楚。不只是讲是什么更要讲为什么这么设计以及实际用的时候要注意什么。适合已经了解目标检测基础、想深入理解Transformer在视觉检测中应用的读者也适合想拿DETR来跑自己数据的朋友。2. DETR整体架构拆解2.1 从一张图看懂DETR的完整流程DETR的整个前向过程可以分成四个阶段。假设你输入一张3×800×1066的图片这是DETR论文里的标准输入尺寸整个流程是这样的第一阶段CNN backbone提特征。论文用的是ResNet-50把图片下采样32倍得到一张2048×25×34的特征图。这个特征图就是图片的高层语义表示每个位置对应原图32×32的区域。第二阶段Transformer encoder做全局建模。先把2048×25×34的特征图通过1×1卷积降维到256通道然后拉平成一个850×256的序列25×34850。因为Transformer本身没有位置概念所以需要加上位置编码。然后经过6层标准Transformer encoder每层包含多头自注意力和FFN。这一步的作用是让每个位置都能看到图中所有其他位置的信息建立全局的上下文关系。第三阶段Transformer decoder生成预测。decoder的输入是N个learnable object queries论文里N100每个query是一个256维的向量。这些query通过多层交叉注意力去encoder的输出里查询信息同时通过自注意力互相通信避免预测重复的物体。经过6层decoder之后得到100个256维的输出向量。第四阶段FFN预测头。每个decoder输出向量分别通过一个3层MLP分类头输出类别包括无物体类和一个3层MLP回归头输出归一化的cx,cy,w,h。最终得到100个预测每个预测包含类别概率和框坐标。整个流程里最关键的几个设计决策我下面逐个拆解。2.2 为什么用ResNet做backbone而不是ViTDETR论文发表的时候ViT还没出来ViT是2020年10月发的DETR是5月所以用CNN做backbone是很自然的选择。但即使放到今天来看用CNN做backbone仍然有它的道理。CNN有天然的平移不变性和局部性归纳偏置这意味着它在小数据上也能学得比较好。而ViT需要大量数据或者强增强才能训好。目标检测的数据集通常不会特别大COCO也就12万张图所以CNN backbone更稳妥。另外DETR用的是ResNet-50的stage4输出也就是下采样32倍的那层而不是stage5。这是因为stage5下采样太多空间分辨率太低对小物体检测不利。但即使这样DETR在小物体上的表现仍然不如Faster R-CNN这是它被诟病最多的地方之一。实际用的时候你可以换成ResNet-101或者更强的backbone来提升效果但要注意显存开销。DETR本身就很吃显存了换大backbone要谨慎。2.3 Transformer encoder和decoder的具体配置DETR的encoder和decoder都是6层每层的结构是标准的Transformer结构。具体参数如下d_model 256nhead 8dim_feedforward 2048dropout 0.1激活函数 ReLU这里有个细节值得注意DETR的encoder和decoder都用了post-norm也就是先做残差再做LayerNorm而不是pre-norm。post-norm在训练初期不太稳定需要warmup。这也是DETR收敛慢的原因之一。后来的很多改进工作都换成了pre-norm。还有一个细节是位置编码。DETR用的是固定的正弦位置编码而不是可学习的。论文里做了消融实验发现固定编码和可学习编码效果差不多但固定编码在训练时不需要额外参数而且可以泛化到不同长度的序列。2.4 预测头为什么用FFN而不是卷积DETR的预测头是简单的3层MLP而不是像其他检测器那样用卷积。这是因为decoder的输出已经是每个object query的向量表示它对应的是一个物体而不是一个空间位置。所以用MLP做映射是合理的。分类头的输出维度是num_classes 1那个1就是无物体类。回归头输出4个值分别是归一化的中心点坐标和宽高。注意这里用的是sigmoid激活把输出限制在0到1之间。3. 核心机制深入解析3.1 二分图匹配损失DETR的灵魂DETR最核心的创新就是用了二分图匹配bipartite matching来计算损失。这是整个方法能work的关键。传统的检测器在计算损失的时候需要先把预测和GT做关联。Faster R-CNN是通过IoU阈值来分配正负样本的YOLO是通过网格和anchor来分配的。这些分配策略都是手工设计的而且会影响最终效果。DETR的做法是把预测集合和GT集合做最优二分图匹配让匹配的总代价最小。具体来说假设我们有N个预测和M个GTN通常远大于M我们需要找到一个N到M的单射使得匹配代价最小。这个匹配问题可以用匈牙利算法在多项式时间内求解。匹配代价的定义很关键。DETR用的是分类概率和框距离的加权和cost -log(p_hat[c_i]) L_box(b_i, b_hat_i)其中L_box用的是L1损失和GIoU损失的组合。为什么用GIoU而不是IoU因为IoU在框不重叠的时候梯度为0没法优化。GIoU通过引入最小闭包区域解决了这个问题。匹配完成之后再计算真正的损失。分类损失用的是交叉熵框损失和匹配代价里的定义一样。注意无物体类的损失权重被降到了0.1这是因为大部分预测都是无物体如果不降权的话负样本会主导整个损失。这个设计的好处是每个GT只会匹配一个预测每个预测最多匹配一个GT。这样就天然地避免了重复预测的问题不需要NMS。而且整个匹配和损失计算过程都是可微的除了匈牙利算法本身但它不涉及梯度所以可以端到端训练。3.2 Object Queries100个可学习的探测器Object queries是DETR里另一个非常巧妙的设计。它们是100个可学习的向量每个向量最终会对应一个预测。你可以把它们理解成100个探测器每个探测器负责在图中寻找一个特定的物体。这些query是通过自注意力互相通信的。这意味着如果query A已经找到了一个物体query B就会知道这个位置已经被占了从而去关注其他位置。这就是为什么DETR不需要NMS——重复预测的问题在decoder内部就被解决了。但object queries也有它的问题。100个query是固定的如果图中物体数量超过100个DETR就无能为力了。而且这100个query是全局共享的它们没有位置概念所以DETR在小物体上的表现不太好。后来的DAB-DETR把object queries解释成4D anchor boxx,y,w,hDN-DETR引入了去噪训练来加速收敛这些改进都是围绕object queries做的。3.3 匈牙利算法的实现细节匈牙利算法也叫Kuhn-Munkres算法是求解二分图最大权匹配的经典算法时间复杂度是O(n^3)。在DETR里n100所以每次迭代需要计算100×100的代价矩阵然后跑一次匈牙利算法。实际实现的时候scipy提供了linear_sum_assignment函数可以直接用。但要注意这个函数是在CPU上跑的而且不支持batch并行。所以训练的时候每个样本都要单独跑一次匈牙利算法这会成为训练速度的瓶颈之一。我实测下来在V100上匈牙利算法的耗时大概占总训练时间的5%到10%。虽然不算特别大但如果你要训练很大的数据集这个开销还是值得注意的。3.4 位置编码的具体计算方式DETR用的是标准的正弦位置编码公式如下PE(pos, 2i) sin(pos / 10000^(2i/d_model)) PE(pos, 2i1) cos(pos / 10000^(2i/d_model))其中pos是位置索引i是维度索引。对于2D特征图需要分别在x和y方向上计算位置编码然后拼接起来。这里有个细节DETR的位置编码是加在encoder输入上的而不是像原始Transformer那样加在attention里。而且decoder的object queries也有自己的位置编码可学习的。实际用的时候如果你要处理不同分辨率的输入位置编码需要做相应的插值。DETR论文里用的是固定的800×1066输入所以位置编码也是固定的。如果你要处理其他尺寸需要自己实现插值逻辑。4. 动手训练DETR从环境搭建到跑通自己的数据4.1 环境准备与依赖安装DETR的官方实现是基于PyTorch的代码在facebookresearch/detr这个repo里。我建议直接用官方代码因为复现的坑比较多。环境要求Python 3.7PyTorch 1.5torchvision 0.6scipy用于匈牙利算法pycocotools用于COCO数据集评估安装步骤git clone https://github.com/facebookresearch/detr.git cd detr pip install -r requirements.txt如果你要用Deformable DETR收敛更快可以装pip install deformable-detr但Deformable DETR需要编译CUDA算子对环境要求更高。新手建议先从原版DETR开始。4.2 数据准备把标注转成COCO格式DETR默认用的是COCO格式的标注。如果你的数据是VOC格式或者YOLO格式需要先转换。COCO格式的核心是一个JSON文件包含images、annotations、categories三个字段。images里记录每张图的id、文件名、宽高annotations里记录每个标注的id、image_id、category_id、bboxxywh格式、area、iscrowdcategories里记录类别id和名称。我写了一个VOC转COCO的脚本核心逻辑如下import json import xml.etree.ElementTree as ET from pathlib import Path def voc_to_coco(voc_dir, output_json): categories [...] # 你的类别列表 images [] annotations [] ann_id 1 for img_id, xml_file in enumerate(Path(voc_dir).glob(*.xml)): tree ET.parse(xml_file) root tree.getroot() size root.find(size) w int(size.find(width).text) h int(size.find(height).text) images.append({ id: img_id, file_name: xml_file.stem .jpg, width: w, height: h }) for obj in root.findall(object): cls_name obj.find(name).text bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) annotations.append({ id: ann_id, image_id: img_id, category_id: categories.index(cls_name), bbox: [xmin, ymin, xmax-xmin, ymax-ymin], area: (xmax-xmin) * (ymax-ymin), iscrowd: 0 }) ann_id 1 with open(output_json, w) as f: json.dump({ images: images, annotations: annotations, categories: [{id: i, name: n} for i, n in enumerate(categories)] }, f)注意bbox的格式是xywh而且坐标是绝对像素值不是归一化的。这一点和YOLO格式不一样转换的时候要小心。4.3 修改配置文件适配自己的数据集DETR的配置文件在configs/detr/目录下是一个Python文件。你需要改几个地方# 类别数改成你的数据集类别数 num_classes 你的类别数 # 数据集路径 dataset_file coco coco_path /path/to/your/coco/format/data # 如果显存不够可以调小batch size batch_size 2 # 训练epoch数DETR需要训很久 epochs 300这里有个坑DETR的num_classes是不包含背景类的但模型内部会自动加1。所以如果你有5个类别num_classes设成5就行不用设成6。4.4 训练过程中的关键参数与调参经验DETR的训练有几个关键参数学习率backbone用1e-5transformer用1e-4。这是因为backbone是预训练的需要小学习率微调。权重衰减1e-4学习率调度在epoch 200的时候降10倍warmup前几个epoch线性warmup我实测下来DETR对学习率非常敏感。如果学习率设大了训练会发散设小了收敛会非常慢。建议先用官方推荐的参数跑一遍确认能work之后再调。还有一个经验DETR在小数据集上很容易过拟合。如果你的数据少于5000张建议用强增强随机裁剪、颜色抖动、随机翻转并且考虑冻结backbone的前几层。4.5 推理与后处理其实没什么后处理DETR的推理非常简单。前向得到100个预测之后只需要做两件事对每个预测取分类概率最大的类别如果最大概率对应的不是无物体类就保留这个预测就这两步没有NMS没有anchor解码没有各种花里胡哨的后处理。这也是DETR最让人舒服的地方。但要注意DETR的输出框是归一化的cxcywh格式需要转成xyxy的绝对坐标才能可视化。转换公式def cxcywh_to_xyxy(boxes, img_w, img_h): cx, cy, w, h boxes.unbind(-1) x1 (cx - 0.5 * w) * img_w y1 (cy - 0.5 * h) * img_h x2 (cx 0.5 * w) * img_w y2 (cy 0.5 * h) * img_h return torch.stack([x1, y1, x2, y2], dim-1)5. 常见问题与排查实录5.1 训练不收敛怎么办这是新手最常遇到的问题。DETR训练不收敛通常有几个原因第一学习率太大。DETR的transformer部分学习率是1e-4如果你不小心设成了1e-3基本上必炸。检查一下配置文件里的lr和lr_backbone。第二warmup不够。DETR用了post-norm训练初期梯度很大需要warmup来稳定。官方代码里warmup是默认开的但如果你自己改了训练脚本可能把warmup去掉了。第三数据格式不对。如果标注的坐标超出了图片范围或者bbox的宽高是负数训练会出问题。建议写个脚本检查一下所有标注的合法性。第四batch size太小。DETR的BN层需要足够的batch size才能估计准确的统计量。如果显存不够可以用SyncBN或者冻结BN。5.2 小物体检测效果差怎么改善DETR在小物体上的表现确实不如Faster R-CNN这是它的固有缺陷。如果你必须检测小物体有几个思路一是提高输入分辨率。DETR默认用800×1066你可以改成1200×1600但显存开销会大幅增加。二是用多尺度特征。原版DETR只用了一层特征图你可以参考Deformable DETR的做法用多尺度可变形注意力。三是用更强的backbone。ResNet-101比ResNet-50好一些Swin Transformer更好但计算量也更大。四是在数据增强上下功夫。小物体检测很依赖数据增强mosaic、mixup这些方法都可以试试。5.3 显存不够用的优化策略DETR很吃显存这是出了名的。一张800×1066的图batch size2在V10032G上大概占20G左右。如果你的显存不够可以试试这些方法减小输入分辨率比如改成600×800减小batch size但太小会影响BN用梯度累积模拟大batch用混合精度训练AMP冻结backbone的前几层用Deformable DETR它的显存效率更高我实测下来混合精度训练能省大概30%的显存而且速度还能快20%左右。强烈推荐。5.4 常见问题速查表问题可能原因解决方法训练loss不下降学习率太大检查lr和lr_backbone建议1e-4和1e-5训练loss震荡warmup不够增加warmup epoch数显存OOMbatch size太大减小batch size或用梯度累积小物体漏检特征分辨率不够提高输入分辨率或用多尺度特征推理速度慢匈牙利算法在CPU上跑推理时不需要匈牙利算法检查代码类别预测全错类别id映射错误检查categories的id是否从0开始框的位置偏移坐标格式不对确认是cxcywh还是xyxy是否归一化训练过拟合数据量太少加强数据增强冻结backbone5.5 几个我踩过的坑第一个坑DETR的官方代码里COCO数据集的类别id是从1开始的但模型输出的类别id是从0开始的。如果你自己转数据的时候没注意评估的时候会全部对不上。建议统一用从0开始的id。第二个坑DETR的位置编码是固定的如果你改了输入分辨率位置编码需要插值。官方代码里没有自动插值需要自己实现。我一开始没注意改了分辨率之后效果直接崩了。第三个坑DETR的评估用的是COCO的评估指标mAP需要pycocotools。如果你用的是自己写的评估脚本可能和官方指标对不上。建议直接用官方的评估代码。第四个坑DETR训练很慢500个epoch在单卡V100上大概要跑3到4天。如果你要调参建议先用小数据集比如COCO的1/10跑通流程确认没问题再上全量数据。6. DETR的变体与后续改进6.1 Deformable DETR收敛速度提升10倍Deformable DETR是DETR最重要的改进之一核心思想是用可变形注意力deformable attention替代原来的全局注意力。原来的注意力是每个query和所有位置计算注意力计算量是O(N^2)。可变形注意力是每个query只和少数几个采样点计算注意力计算量降到O(NK)K通常设4或8。这样一来Deformable DETR可以用多尺度特征而且收敛速度大幅提升。原来DETR需要500个epochDeformable DETR只需要50个epoch就能达到更好的效果。但Deformable DETR需要编译CUDA算子安装比较麻烦。如果你只是想学习DETR的原理建议先从原版开始。6.2 DAB-DETR和DN-DETR把query解释成anchorDAB-DETRDynamic Anchor Box DETR把object queries解释成4D anchor boxx,y,w,h每一层decoder都会基于上一层的预测来更新anchor。这样一来query就有了明确的位置含义收敛速度也更快。DN-DETRDenoising DETR引入了去噪训练在训练的时候给GT加噪声然后让模型去恢复。这个方法进一步加速了收敛而且不需要改模型结构。这两个工作都是围绕object queries做的改进思路很巧妙。如果你要深入理解DETR建议把这两篇论文也读一下。6.3 DETR在三维检测和分割上的扩展DETR的框架其实很通用不只能做2D检测。后来有很多工作把它扩展到了其他任务3D检测用点云或者多视角图像做3D目标检测全景分割在DETR的基础上加一个mask分支视频检测把时序信息引入DETR遥感图像检测针对遥感图像的特点做改进这些扩展工作的核心思路都是一样的用Transformer做集合预测用二分图匹配算损失。理解了DETR的核心机制这些变体都很容易看懂。7. 实际项目中的选型建议7.1 DETR vs YOLO什么时候用哪个如果你要做实际项目选型是最重要的一步。DETR和YOLO各有优劣我根据自己的经验给个建议选DETR的场景需要端到端训练不想调NMS和anchor的超参数物体之间遮挡严重NMS容易误删对推理速度要求不高但对精度要求高想做全景分割等多任务选YOLO的场景需要实时检测对速度要求高小物体检测很重要数据量不大需要快速迭代部署到边缘设备说实话在大多数工业场景下YOLO系列特别是YOLOv8仍然是更实用的选择。DETR的优势在于架构简洁和端到端但它的训练成本和推理速度都是硬伤。7.2 训练成本估算与硬件建议DETR的训练成本确实不低。以COCO数据集为例500个epoch在8卡V100上大概需要3天。如果你只有单卡可能要跑两三周。硬件建议最低配置单卡RTX 309024Gbatch size2训练时间约2周推荐配置4卡V100或A100batch size8训练时间约3天理想配置8卡A100batch size16训练时间约1天如果你只是想学习DETR建议用COCO的子集或者自己标注一个小数据集先跑通流程。不要一上来就上全量COCO那样太浪费时间了。7.3 从DETR到RT-DETR实时检测的尝试RT-DETR是百度提出的实时DETR它通过高效混合编码器和IoU-aware查询选择把DETR的推理速度提升到了实时水平。在COCO上RT-DETR-L可以达到53%的AP和108 FPS这个指标已经超过了YOLOv8-L。如果你需要实时检测但又想用DETR的架构RT-DETR是一个很好的选择。它的代码在PaddleDetection里也有PyTorch的实现。但RT-DETR的训练成本仍然比YOLO高而且部署生态不如YOLO成熟。选型的时候要综合考虑。7.4 我个人的选型心得做了这么多项目我的体会是没有最好的模型只有最合适的模型。DETR的架构确实优雅但优雅不等于实用。在实际项目中我通常会先跑一个YOLO的baseline看看效果和速度能不能满足需求。如果满足就用YOLO如果不满足再考虑DETR或者它的变体。另外数据质量比模型选择更重要。我见过太多项目花大量时间调模型但数据标注一塌糊涂。与其纠结用DETR还是YOLO不如先把数据标好、清洗好。好的数据加上一般的模型效果往往比差的数据加上好模型要好得多。最后分享一个小技巧如果你要用DETR跑自己的数据建议先用小分辨率比如400×600快速跑一遍确认整个流程没问题。然后再用全分辨率训练。这样可以节省大量调试时间。我在实际项目中就是这么做的能省下至少一半的调试时间。
返回列表