前沿:

端到端目标检测近年来发展迅速,其中 DETR、DINO、RT-DETR、D-FINE 和 DEIM 在 DETR 系列中占据了重要地位,并在精度与实效性上逐渐超越 YOLO 系列。DETR 首次引入 Transformer 进行端到端检测,开创了新范式,但存在收敛慢、小目标表现差的问题;DINO 通过更优的查询设计和对比噪声训练显著加快收敛并提升精度;RT-DETR 面向实时场景优化结构,最小化查询不确定性来筛选高质量查询,使检测过程更加稳定和鲁棒;D-FINE 采用了细粒度分布细化(FDR) 和全局最优定位自蒸馏(GO-LSD) 显著提升定位精度;而 DEIM 则通过提升匹配数量、设计一种新的损失函数MAL匹配质量对损失进行缩放,优化了不同质量水平上的匹配。明显地加速现RT-DETR、D-FINE模型的收敛。结论(2025): DEIM训练比yolo快,推理比yolo快,精度比yolo高,全面超过yolo。训练时数据集少建议使用D‑FINE‑X在Objects365+COCO的预训练权重(精度最高),做二次微调。数据集多直接使用DEIM训练或做二次微调。

1、DETR

1.1、 DETR前言:

DETR(DEtection TRansformer)End-to-End Object Detection with Transformer,是一种创新的目标检测方法,它将目标检测视为直接的集合预测问题,利用Transformer编码器-解码器架构和基于集合的全局损失函数(通过二分匹配实现唯一预测),从而简化了检测流程并去除了传统组件如非极大值抑制和锚框生成。DETR使用一组固定的对象查询来并行输出最终预测结果,在COCO数据集上展示了与Faster R-CNN相当的准确性和运行效率,并能扩展到全景分割任务中,为后续DETR系类模型的发展奠定了基础。相比之前的Faster R-CNN和YOLO网络,DETR具有以下优点:

  1. 端到端:DETR简化了目标检测流程,不需要非极大值抑制(NMS,预测生成大量的box框,再后处理)这样的后处理步骤,也不需要手工设计的组件如锚框(anchors)。
  2. 全局推理能力:通过使用Transformer架构,DETR能够更有效的捕捉整个图像的全局信息(实例分割效果更好),这对于处理大物体或者需要理解整个场景的情况特别有用。
  3. 集合预测损失:DETR引入了一个基于二分匹配的集合预测损失函数,确保每个目标都有唯一对应的预测,避免了重复预测的问题。
  4. 无需手工设计先验知识:传统的目标检测方法往往依赖于手工设计的先验知识,如anchor(锚框)的设计和尺寸(后续的一些模型又引入anchor的概念)。而DETR则通过学习的方式自动获取这些信息,减少了对人工干预的需求。
  5. 简洁性:DETR的设计理念非常简洁,没有复杂的分支结构或大量的超参数需要调整,使其更易于理解和实现。

尽管DETR在性能和收敛速度方面表现一般,但它为诸如DINO、RT-DETR、D-FINE以及DEIM等模型奠定了基础,并间接促进了这些模型在目标检测领域中挑战YOLO系列地位的能力。

1.2、模型结构

DETR模型使用了,特征提取网络Resnet50、transformer完整的6层encoding和6层decoding,最后接入了两个FFN,分别为分类头和检测框预测头。

1.3、推理过程:

一张输入图像 [3, H, W] 首先通过 CNN 提取特征,得到特征图 feature_map: [C, h, w];由于 Transformer 对序列顺序不敏感,需为其添加位置编码(Positional Encoding),形成带有位置信息的输入。该输入经过 Transformer Encoder 编码后,得到全局特征表示 feature_map: [C, d](其中 d 为特征维度)。在解码阶段,将编码后的特征与预设的可学习的 object queries [N, d]相结合(在第二个self- attention作为Q矩阵),通过 Transformer Decoder 得到解码输出 feature_map: [N, d],其中 N 表示预设的目标检测数量。最终,该输出分别通过分类头(Class FFN)和边界框回归头(Box FFN)生成 N 个预测框(box) 及其对应的类别概率,类别总数为 n+1(包含一个表示“无目标”的类别),通过类别的置信度直接可以选出最终的预测结果。其中positional encodeing公式如下:

1.4、训练阶段:

  1. 把1-6层的decoding的输出通过class FFN 和 box FFN,得到6组预测类别预测(pred_logits:[6, 100, 92],92为无效类别)和检测框预测(pred_boxes:[6, 100, 4])。
  2. 计算出第6层代价矩阵,使用匈牙利匹配选出的代价最小的n个预测标签。
  3. 通过筛选出的预测标签,计算出所有1-6层的平均loss。

DETR 通过解码器单次前向传播,推断出一个固定大小为 N 的预测集合。训练过程中的一个主要难点在于如何将预测出的物体(类别、位置、大小)与真实标注进行匹配和评分。为此,DETR 使用的损失函数首先在预测结果与真实标注之间建立一个匈牙利匹配,然后对每个匹配成功的物体优化其特定的损失。

1.4.1 匈牙利匹配代码:

import numpy as np
from scipy.optimize import linear_sum_assignment

# 构造一个代价矩阵,例如:3预测结果分配给2个真实结果的成本
cost_matrix = np.array([[0.6, 0.2],
                        [0.6, 0.4],
                        [0.5, 0.3]])

# 示例:设置代价阈值,超越阈值就不参与匹配
threshold = 0.5
cost_matrix[cost_matrix > threshold] = np.inf

print("处理后的代价矩阵:")
print(cost_matrix)

# 使用 linear_sum_assignment 来找到最优匹配
row_ind, col_ind = linear_sum_assignment(cost_matrix)

# 输出结果
print("最优匹配行索引:", row_ind)
print("最优匹配列索引:", col_ind)
print("总成本:", cost_matrix[row_ind, col_ind].sum())

"""
处理后的代价矩阵:
[[inf 0.2]
 [inf 0.4]
 [0.5 0.3]]
最优匹配行索引: [0 2]
最优匹配列索引: [1 0]
总成本: 0.7

第 0 行 与 第 1 列 匹配,低第 0 个预测框匹配上了第 1 个真实框。
第 2 行 与 第 0 列 匹配,低第 2 个预测框匹配上了第 0 个真实框。
"""

1.4.2 计算出代价矩阵

计算代价矩阵需要考虑三部分,分类代价,预测框回归代价,预测框GIOU代价,最后一层的类别预测(pred_logits:[100, 92])和检测框预测(pred_boxes:[100, 4])。

(1)计算分类代价

图片中有物体类标签为:36,54,22,就只保留预测为36,54,22这部分的置信度(如下表)。

# pred_logits:[100, 92] gt_logits:[3]。
cost_class = -pred_logits[:, gt_logits] # [100, 3]

(2)检测框损失

检测框损失 分为L1 cost 和 GIoU cost。

# 输入pred_boxes:[100, 4]和gt_boxes:[3, 4],中心点和高宽。
cost_bbox = torch.cdist(pred_boxes, gt_boxes) # [100, 3]  计算两点的距离
cost_giou = -generalized_box_iou(to_xyxy(pred_boxes), to_xyxy(gt_boxes)) # [100, 3]

GIoU

(3)计算出代价矩阵

# cost = -cost_class + 5*cost_giou - 2*cost_bbox    # [100, 3]
cost = self.cost_bbox * cost_bbox + self.cost_class * cost_class + self.cost_giou * cost_giou

(4) 计算出匹配结果

row_ind, col_ind = linear_sum_assignment(cost)

# row_ind 值:[13, 25, 60]
# col_ind 值:[0, 2, 1]

1.5、通过最优匹配结果计算出1-6层的加权和loss,1-5层是辅助loss。

out = {'pred_logits': outputs_class[-1], 'pred_boxes': outputs_coord[-1]} # 计算最后一层的loss
if self.aux_loss:
    out['aux_outputs'] = self._set_aux_loss(outputs_class, outputs_coord) # 计算1-5层的loss

真实标签进行扩充,第13预测结果匹配类别为22的box框,第25预测结果匹配类别为36的box框,第60预测结果匹配类别为54的box框,其他都为无效类别(92)。分类使用交叉熵损失。

loss_ce = F.cross_entropy(src_logits.transpose(1, 2), target_classes, self.empty_weight)

同理box loss 也只选择被选出来的预测和真的的标签进行匹配。也就是3预测框与3个真的标签框进行L1 loss和GIoU loss计算。计算出6层结果后,再求出加权和loss。

losses = sum(loss_dict[k] * weight_dict[k] for k in loss_dict.keys() if k in weight_dict)
optimizer.zero_grad()
losses.backward()

1.6、DETR 伪代码

如果你想转行到AI,或者想了解大模型方面的知识,但是不知道从何下手,我也准备了一个可以帮你三个月入门到深度学习的籽料包:

【1.超详细的人工智能学习大纲】:一个月精心整理,快速理清学习思路!

【2.基础知识】:Python基础+高数基础

【3.机器学习入门】:机器学习经典算法详解

【4.深度学习入门】:神经网络基础(CNN+RNN+GAN)

【5.项目实战合集】:34个机器学习+36深度学习项目实战

可以关住我的公//粽//呺【迪哥谈AI】回复333 无偿获取!(或者扫马)

2、DINO

2.1 DINO前言:

DINO: DETR with Improved DeNoising Anchor Boxes for End-to-End Object Detectionarxiv.org/pdf/2203.03605

DINO:主要借鉴了DAB-DETR、DN-DETR和Deformable DETR 这几篇文章,对比与detr加快的收敛输入,在当时端到端的检查任务中达到了非常高的准确率。

2.2、模型结构流程图

2.3、图片特征提取

可选择图片分类模型提取图片特征,论文使用的resnet和swin-Transformer。在backbone提取特征时,可同时保存多个尺度的特征(scale feature),最近加上位置编码特征向量,生成一个token序列(shape[2,1024,512],表示batch-size=2,token-len=1024,embedding=512)。

2.4、Encoder 过程

encoder过程和vison-transformer一样,通过6层的self-transformer和FFN。

2.5、Decoder 过程

Decoder 过程我这里理解和diffusion model思想一样,模型很难直接识别box的位置,但是可以给一个大致的框,让Decoder模型一层一层的修正。把DETR的positional query显示地建模为四维的框,每一层decoder都会去预测一组相对偏移量并去更新检测框,得到一个更加精确的检测框预测,动态更新这个检测框并用它来帮助decoder cross-attention来抽取feature。

2.5.1 如何生成Decoder的Q(queries)。

Q是通过anchor和可学习的参数相加得到,训练过程q(anchor)主要分为两部分,一部分是Decoder生成的,一部份是真实标签生成(只在训练有,可以加快收敛)。

2.5.2 Decoder生成的proposals,原理就是在anchor加上Decoder修正值得到proposals再让decoder一层一层的修正,得到最终的精确预测值。

def gen_encoder_output_proposals(memory:Tensor, memory_padding_mask:Tensor, spatial_shapes:Tensor, learnedwh=None):
    """
    Input:
        - memory: bs, \sum{hw}, d_model
        - memory_padding_mask: bs, \sum{hw}
        - spatial_shapes: nlevel, 2
        - learnedwh: 2
    Output:
        - output_memory: bs, \sum{hw}, d_model
        - output_proposals: bs, \sum{hw}, 4
    """
    N_, S_, C_ = memory.shape
    base_scale = 4.0
    proposals = []
    _cur = 0
    for lvl, (H_, W_) in enumerate(spatial_shapes):
        mask_flatten_ = memory_padding_mask[:, _cur:(_cur + H_ * W_)].view(N_, H_, W_, 1)
        valid_H = torch.sum(~mask_flatten_[:, :, 0, 0], 1)
        valid_W = torch.sum(~mask_flatten_[:, 0, :, 0], 1)

        grid_y, grid_x = torch.meshgrid(torch.linspace(0, H_ - 1, H_, dtype=torch.float32, device=memory.device),
                                        torch.linspace(0, W_ - 1, W_, dtype=torch.float32, device=memory.device))
        grid = torch.cat([grid_x.unsqueeze(-1), grid_y.unsqueeze(-1)], -1) # H_, W_, 2

        scale = torch.cat([valid_W.unsqueeze(-1), valid_H.unsqueeze(-1)], 1).view(N_, 1, 1, 2)
        grid = (grid.unsqueeze(0).expand(N_, -1, -1, -1) + 0.5) / scale

        if learnedwh is not None:
            wh = torch.ones_like(grid) * learnedwh.sigmoid() * (2.0 ** lvl)
        else:
            wh = torch.ones_like(grid) * 0.05 * (2.0 ** lvl)

        proposal = torch.cat((grid, wh), -1).view(N_, -1, 4)
        proposals.append(proposal)
        _cur += (H_ * W_)

    output_proposals = torch.cat(proposals, 1)
    output_proposals_valid = ((output_proposals > 0.01) & (output_proposals < 0.99)).all(-1, keepdim=True)
    output_proposals = torch.log(output_proposals / (1 - output_proposals)) # unsigmoid
    output_proposals = output_proposals.masked_fill(memory_padding_mask.unsqueeze(-1), float('inf'))
    output_proposals = output_proposals.masked_fill(~output_proposals_valid, float('inf'))

    output_memory = memory
    output_memory = output_memory.masked_fill(memory_padding_mask.unsqueeze(-1), float(0))
    output_memory = output_memory.masked_fill(~output_proposals_valid, float(0))

    return output_memory, output_proposals

上面代码逻辑,从encoder的输出中,利用编码器多尺度特征图的每个空间位置,生成具有尺度感知的初始边界框建议(以 logit 形式),并清洗无效区域,为解码器提供 位置先验 + 特征记忆,是实现端到端检测中“query 初始化”的核心机制。选出有效的token和proposals(anchor,可以理解为box的坐标xyxy)。

enc_outputs_class_unselected = self.enc_out_class_embed(output_memory)
enc_outputs_coord_unselected = self.enc_out_bbox_embed(output_memory) + output_proposals # (bs, \sum{hw}, 4) unsigmoid
topk_proposals = torch.topk(enc_outputs_class_unselected.max(-1)[0], topk, dim=1)[1] # bs, nq

再把有效的token通过上面两个头,得到类别预测和修正  。上原始的anchor加上预测的  ,就等于encoder的粗糙目标box框预测值  ,最后选着分类token置信度最大的900个。其中enc_out_bbox_embed(output_memory)只预测与真实box的修正。

2.5.3 真实标签生成proposals,原理就是在真实label上加噪声得到proposals,再让decoder一层一层的修正,得到最终的精确预测值。

对比度去噪训练Contrastive DeNoising Training),为了提高一对一匹配的效果,DINO引入了对比度去噪训练。通过同时添加同一真实标签的正负样本来训练模型,避免模型对同一目标的重复输出(也就是把真实标签重复多组,一组中分为positive和negative两部分)。该方法可以加上模型收敛。对于positive 每个点添加噪声范围在  ,对于negative每个点添加的噪声范围在 到之间 。

(1)、如果在一次推理过程中只有两张图,把图一和图二组成batch:

batch_label = [1,0,1] # 狗:0;猫: 1
batch_box = [[0.4,0.5,0.5,0.8],[0.3,0.5,0.2,0.7],[0.7,0.6,0.2,0.5]]

为了使每次推理有多个正样本和负样本,需要将label样本扩充多次,计算出group个数:找出图片中目标最大的目标数,如上图,图二有两个目标。计算公式方式:dn_number // (int(max(known_num) * 2))=100/(2*2)=25,其中dn_number默认为100,known_num为每一张图片的目标数。计算出group数为25。也就需要把batch_label和batch_box重复25*2次。目的找出这个batch中,目标最多的图片,生成100目标框。

# len(batch_label_repeat) : 150
batch_label_repeat = batch_label.repeat(2 * dn_number, 1)
batch_label_repeat = [1,0,1,1,0,1,1,0,1,1,0,1......,1,0,1]
# len(batch_box_repeat) : 150
batch_box_repeat = batch_box.repeat(2 * dn_number, 1)
batch_box_repeat = [[0.4,0.5,0.5,0.8],[0.3,0.5,0.2,0.7],[0.7,0.6,0.2,0.5],....,[0.4,0.5,0.5,0.8],[0.3,0.5,0.2,0.7],[0.7,0.6,0.2,0.5]]

(2)、对label添加噪声,随机50%的概率挑选batch_label_repeat中的标签进行随机修改。

label_noise_ratio=1.0
chosen_indice = torch.nonzero(p < (label_noise_ratio * 0.5)).view(-1)

(3)、对box添加噪声。

batch_box_repeat 总共重复的50组,其中,一组中分为positive和negative两部分。

diff = torch.zeros_like(known_bboxs)
diff[:, :2] = known_bboxs[:, 2:] / 2 # wh 的一半
diff[:, 2:] = known_bboxs[:, 2:] / 2 # wh 的一半

# 独立地以相等概率取值为 -1.0 或 1.0
rand_sign = torch.randint_like(batch_box_repeat, low=0, high=2, dtype=torch.float32) * 2.0 - 1.0
rand_part = torch.rand_like(batch_box_repeat)

# rand_part对positive的组进行[0,1]之间的分布调整,rand_part对positive的组进行[1,2]之间的分布调整,
rand_part[negative_idx] += 1.0

# 调整方向
rand_part *= rand_sign

# 按照 diff 方向调整
known_bbox_ = known_bbox_ + torch.mul(rand_part, diff).cuda() * box_noise_scale
known_bbox_ = known_bbox_.clamp(min=0.0, max=1.0)

解释上面代码,在获取w,h 的一半保存在diff中。rand_sign表示随机选一个box的值,进行加或减。rand_part对positive的组进行[0,1]之间的分布调整,rand_part对positive的组进行[1,2]之间的分布调整,rand_part *= rand_sign表示选择好方向和值的大小。torch.mul(rand_part, diff)表示还需要考虑box的大小,用选好的值去成以w或h 值的一半。known_bbox_ + torch.mul(rand_part, diff).cuda() * box_noise_scale 表示选择好方向和值的大小对box进行调整。最后得到:带有噪声的label和带有噪声box。

2.5.4 生成Q

混合查询选择Mixed Query Selection):DINO提出了一种混合查询选择方法。该方法从编码器的输出中选择位置查询作为初始锚点,而内容查询(分类)则保持可学习。可以理解先找有物体的位置(encoder 输出),再确定位置的物体是什么(可以学参数)。上面有两部份proposals:(1)、真实标签生成带有噪声的label,带有的噪声box;(2)、encoder预测的类别和box。encoder预测的类别只是为了用于encoder选择有效的box预测,并不参与后面的计算。encoder预测box对应的类别标签是单独采用的一组可学习参数。

tgt_ = self.tgt_embed.weight[:, None, :].repeat(1, bs, 1).transpose(0, 1) # nq, bs, d_model

最后把真实标签生成带有噪声的label,带有噪声box和encoder预测结果拼在一起,得到最终的proposals。

refpoint_embed=torch.cat([refpoint_embed,refpoint_embed_],dim=1)
tgt=torch.cat([tgt,tgt_],dim=1)

其中refpoint_embed是标签真实标签生成带噪声的box;refpoint_embed_是encoder预测的box;tgt是标签真实标签生成带噪声的类别;tgt_是采用的一组可学习参数。

2.5.5 decoder 每一层的流程。

if self.training and self.decoder_query_perturber is not None and layer_id != 0:
    reference_points = self.decoder_query_perturber(reference_points)
  • 在训练过程阶段。每一层会给(x,y,w,h)添加一个很小的噪声,让模型学会从扰动的位置恢复,增加训练鲁棒性,防止过拟合。
query_sine_embed = gen_sineembed_for_position(reference_points) # nq, bs, 256*2
  • 使用 gen_sineembed_for_position 生成基于参考点的位置编码。
raw_query_pos = self.ref_point_head(query_sine_embed) # nq, bs, 256
pos_scale = self.query_scale(output) if self.query_scale is not None else 1
query_pos = pos_scale * raw_query_pos
  • ref_point_head: 一个小型 MLP,将 query_sine_embed 映射为 d_model 维的查询位置嵌入。
  • query_scale: 一个可学习的缩放因子(通常是 Linear),根据当前 output 动态调整位置嵌入的强度。

计算cross-transformer 得到output。

if self.bbox_embed is not None:
    reference_before_sigmoid = inverse_sigmoid(reference_points)
    delta_unsig = self.bbox_embed[layer_id](output)  # 预测相对于当前参考点的偏移量
    outputs_unsig = delta_unsig + reference_before_sigmoid
    new_reference_points = outputs_unsig.sigmoid()
  • bbox_embed: 一个 MLP,每层独立预测边界框的更新量(Δx, Δy, Δw, Δh)。
  • inverse_sigmoid: 将当前 reference_points 转回未归一化空间。
  • 加上预测的偏移量,再 sigmoid 回 (0,1) 空间。
  • 实现了 逐步精修 的思想:每层都基于前一层的输出改进框的位置。
if self.dec_layer_number is not None and layer_id != self.num_layers - 1:
    nq_now = new_reference_points.shape[0]
    select_number = self.dec_layer_number[layer_id + 1]
    if nq_now != select_number:
        class_unselected = self.class_embed[layer_id](output) # nq, bs, 91
        topk_proposals = torch.topk(class_unselected.max(-1)[0], select_number, dim=0)[1] # new_nq, bs
        new_reference_points = torch.gather(new_reference_points, 0, topk_proposals.unsqueeze(-1).repeat(1, 1, 4))
  • 模型有多个解码器层(decoder layers),每一层都会输出一组对象查询(object queries)。
  • new_reference_points 是从当前解码器层输出的参考点(reference points),用于指导下一层或用于预测框。[nq, bs, 4](通常是中心 x, y 和宽高 w, h,或归一化的边界框参数)
  • layer_id 表示当前是第几个解码器层(从 0 开始)。
  • self.num_layers 是总解码器层数。
  • self.dec_layer_number 是一个列表[300, 200, 100,xxx],表示每一层应该保留的查询数量(number of queries)。
  • self.class_embed[layer_id] 是第 layer_id 层对应的分类头(classification head),得到每个查询的类别预测得分。
  • topk_proposals:选出select_number个“最有前景”的查询索引
  • 新的 new_reference_points :根据选出的索引topk_proposals,从 new_reference_points 中提取对应的参考点

假设:

  • self.dec_layer_number = [300, 200, 100]
  • 当前 layer_id = 0(第一层解码器)
  • new_reference_points.shape = [300, bs, 4]
  • output.shape = [300, bs, d_model]

执行过程:

  1. select_number = self.dec_layer_number[1] = 200
  2. nq_now = 300 ≠ 200 → 进入筛选
  3. 用 class_embed[0] 对 output 打分 → [300, bs, 91]
  4. 取每个查询的最大类得分 → [300, bs]
  5. 选出 top-200 的索引 → [200, bs]
  6. 从 new_reference_points 中 gather 出 200 个 → [200, bs, 4]
  7. 这个新的 new_reference_points 将作为下一层的输入参考点

if self.rm_detach and 'dec' in self.rm_detach:
    reference_points = new_reference_points
else:
    reference_points = new_reference_points.detach()  
if self.use_detached_boxes_dec_out:
    ref_points.append(reference_points)
else:
    ref_points.append(new_reference_points)
  • 两次前向传播Look Forward Twice):为了利用后续层精炼的框信息来优化相邻早期层的参数,DINO提出了一种新的两次两次前向传播。所有相比于Deformable DETR不使用detach操作。最后通过多层的decoder得到最终的预测结果。这里可以理解为:上一层预测的偏移量会被上一层和当前层反向传播更新两次。

2.6、DINO损失函数

需要三组loss分别为:1、outputs loss:计算最终输出的loss,2,interm_outputs loss:计算出decoder中间层输出的loss。3、enc output loss :计算encoder输出的loss。后面两个算是辅助loss,帮助模型收敛。它们的计算方式一致。首先找到匹配关系再计算loss下面以计算最终loss(outputs loss)为例:

decoder 最后一层通过噪声修正后得到分类预测和目标框预测,2是batch_size,1050是预测目标框个数,3是类别个数(猫,狗,背景),4是box坐标。

# 预测:
output_cls:torch.Size([2, 1050, 3]) 
output_box:torch.Size([2, 1050, 4])
# 真实:
tgt_ids:torch.Size([2, 3, 3]) 
tgt_bbox:torch.Size([2, 3, 4])

2.6.1 使用匈牙利算法选择最优的3个预测结果,代码流程如下:

def forward(self, outputs, targets):
    """ Performs the matching
    Params:
        outputs: This is a dict that contains at least these entries:
                "pred_logits": Tensor of dim [batch_size, num_queries, num_classes] with the classification logits
                "pred_boxes": Tensor of dim [batch_size, num_queries, 4] with the predicted box coordinates
        targets: This is a list of targets (len(targets) = batch_size), where each target is a dict containing:
                "labels": Tensor of dim [num_target_boxes] (where num_target_boxes is the number of ground-truth
                        objects in the target) containing the class labels
                "boxes": Tensor of dim [num_target_boxes, 4] containing the target box coordinates
    Returns:
        A list of size batch_size, containing tuples of (index_i, index_j) where:
            - index_i is the indices of the selected predictions (in order)
            - index_j is the indices of the corresponding selected targets (in order)
        For each batch element, it holds:
            len(index_i) = len(index_j) = min(num_queries, num_target_boxes)
    """

    bs, num_queries = outputs["pred_logits"].shape[:2]

    # We flatten to compute the cost matrices in a batch
    out_prob = outputs["pred_logits"].flatten(0, 1).sigmoid()  # [batch_size * num_queries, num_classes]
    out_bbox = outputs["pred_boxes"].flatten(0, 1)  # [batch_size * num_queries, 4]

    # Also concat the target labels and boxes
    tgt_ids = torch.cat([v["labels"] for v in targets])
    tgt_bbox = torch.cat([v["boxes"] for v in targets])

    # Compute the classification cost.
    alpha = self.focal_alpha
    gamma = 2.0
    neg_cost_class = (1 - alpha) * (out_prob ** gamma) * (-(1 - out_prob + 1e-8).log())
    pos_cost_class = alpha * ((1 - out_prob) ** gamma) * (-(out_prob + 1e-8).log())
    cost_class = pos_cost_class[:, tgt_ids] - neg_cost_class[:, tgt_ids]

    # Compute the L1 cost between boxes
    cost_bbox = torch.cdist(out_bbox, tgt_bbox, p=1)
        
    # Compute the giou cost betwen boxes            
    cost_giou = -generalized_box_iou(box_cxcywh_to_xyxy(out_bbox), box_cxcywh_to_xyxy(tgt_bbox))

    # Final cost matrix
    C = self.cost_bbox * cost_bbox + self.cost_class * cost_class + self.cost_giou * cost_giou
    C = C.view(bs, num_queries, -1).cpu()

    sizes = [len(v["boxes"]) for v in targets]
    indices = [linear_sum_assignment(c[i]) for i, c in enumerate(C.split(sizes, -1))]
    return [(torch.as_tensor(i, dtype=torch.int64), torch.as_tensor(j, dtype=torch.int64)) for i, j in indices]

计算出代价方程:C = self.cost_bbox * cost_bbox + self.cost_class * cost_class + self.cost_giou * cost_giou,通过linear_sum_assignment(c[i])计算出最优匹配索引,其中:

  • self.cost_class: 计算分类代价
  • self.cost_bbox: 计算 L1 回归代价(坐标差异)
  • self.cost_giou :计算 GIoU 代价(几何重叠度)

从预测集合中找到需要匹配的索引后就可以进行更详细的loss计算了。

2.6.2、分类loss计算:

比如从1050预测中选出了索引8,64,512预测结果,就使用one_hot编码,把8,64,512设置为对应标,其他位置就为背景。torch.Size([2, 1050, 3])。与output_cls计算loss。计算代码如下:

def sigmoid_focal_loss(inputs, targets, num_boxes, alpha: float = 0.25, gamma: float = 2):
    """
    Loss used in RetinaNet for dense detection: https://arxiv.org/abs/1708.02002.
    Args:
        inputs: A float tensor of arbitrary shape.
                The predictions for each example.
        targets: A float tensor with the same shape as inputs. Stores the binary
                 classification label for each element in inputs
                (0 for the negative class and 1 for the positive class).
        alpha: (optional) Weighting factor in range (0,1) to balance
                positive vs negative examples. Default = -1 (no weighting).
        gamma: Exponent of the modulating factor (1 - p_t) to
               balance easy vs hard examples.
    Returns:
        Loss tensor
    """
    prob = inputs.sigmoid()
    ce_loss = F.binary_cross_entropy_with_logits(inputs, targets, reduction="none")
    p_t = prob * targets + (1 - prob) * (1 - targets)
    loss = ce_loss * ((1 - p_t) ** gamma)

    if alpha >= 0:
        alpha_t = alpha * targets + (1 - alpha) * (1 - targets)
        loss = alpha_t * loss

    return loss.mean(1).sum() / num_boxes

2.6.3 、Box损失计算,包含l1_loss和generalized_box_iou(广义交并比,简称 GIoU)

def loss_boxes(self, outputs, targets, indices, num_boxes):
    """Compute the losses related to the bounding boxes, the L1 regression loss and the GIoU loss
        targets dicts must contain the key "boxes" containing a tensor of dim [nb_target_boxes, 4]
        The target boxes are expected in format (center_x, center_y, w, h), normalized by the image size.
    """
    assert 'pred_boxes' in outputs
    idx = self._get_src_permutation_idx(indices)
    src_boxes = outputs['pred_boxes'][idx]
    target_boxes = torch.cat([t['boxes'][i] for t, (_, i) in zip(targets, indices)], dim=0)

    loss_bbox = F.l1_loss(src_boxes, target_boxes, reduction='none')

    losses = {}
    losses['loss_bbox'] = loss_bbox.sum() / num_boxes

    loss_giou = 1 - torch.diag(box_ops.generalized_box_iou(
        box_ops.box_cxcywh_to_xyxy(src_boxes),
        box_ops.box_cxcywh_to_xyxy(target_boxes)))
    losses['loss_giou'] = loss_giou.sum() / num_boxes

    # calculate the x,y and h,w loss
    with torch.no_grad():
        losses['loss_xy'] = loss_bbox[..., :2].sum() / num_boxes
        losses['loss_hw'] = loss_bbox[..., 2:].sum() / num_boxes

    return losses

通过上面计算出了3个loss(loss_cls,l1_loss,loss_giou)。1、outputs loss,2,interm_outputs loss,3、encodeing output loss 3组,每组3个loss,就可以计算出9个loss。

2.7、DINO实验

2.7.1 实验结果

在 COCO val2017 上使用 12 个训练周期和 ResNet50 主干网络的 DINO 及其他检测模型的结果。对于没有多尺度特征的模型,测试其最佳模型 ResNet-50-DC5 的 GFLOPS 和 FPS。DINO 使用 900 个查询。† 表示使用 900 个查询或使用 300 个查询 3 种模式(其效果与 900 个查询类似)的模型。除 DETR(100 个查询)外,其他类 DETR 模型使用 300 个查询。∗ 表示使用 mmdetection [5] 框架进行测试的模型。

在24个epoch达到最优,对比其它模型指标也最好。

2.7.2 消融实验

所提算法各组件的消融对比实验。使用“QS”、“CDN”和“LFT”分别表示“查询选择”(Query Selection)、“对比去噪训练”(Contrastive De-Noising Training)和“向前看两次”(Look Forward Twice)。

 如果你想转行到AI,或者想了解大模型方面的知识,但是不知道从何下手,我也准备了一个可以帮你三个月入门到深度学习的籽料包:

【1.超详细的人工智能学习大纲】:一个月精心整理,快速理清学习思路!

【2.基础知识】:Python基础+高数基础

【3.机器学习入门】:机器学习经典算法详解

【4.深度学习入门】:神经网络基础(CNN+RNN+GAN)

【5.项目实战合集】:34个机器学习+36深度学习项目实战

可以关住我的公//粽//呺【迪哥谈AI】回复333 无偿获取!(或者扫马)

3、RT-DETR

3.1 RT-DETR 前言

DETRs Beat YOLOs on Real-time Object Detectionarxiv.org/pdf/2304.08069

RT-DETR通过减少NMS(非极大值抑制)操作,不仅有效提升了检测速度,还避免了因人工设置IoU阈值而带来的负面影响。尽管早期的DETR模型也减少了NMS,但由于其模型结构的限制,训练和推理速度依然较慢。而RT-DETR在继承端到端优势的同时,实现了速度与精度的双重保障。主要贡献:

  1. 设计了一种高效的混合编码器,通过解耦尺度内交互和跨尺度融合来快速处理多尺度特征,从而提升速度。接着,
  2. 提出了不确定性最小化查询选择机制,为解码器提供高质量的初始查询,从而提高精度。
  3. RT-DETR支持通过调整解码器层数来灵活调节速度,以适应不同场景,且无需重新训练。

RT-DETR-R50 / R101在COCO数据集上分别达到了53.1% / 54.3%的AP,并在T4 GPU上实现了108 / 74 FPS,无论在速度还是精度上都优于此前先进的YOLO系列。此外,RT-DETR-R50相比DINO-R50在精度上高出2.2% AP,FPS提升了约21倍。在使用Objects365进行预训练后,RT-DETR-R50 / R101的AP达到了55.3% / 56.2%。项目主页:

https://github.com/lyuwenyu/RT-DETRgithub.com/lyuwenyu/RT-DETR

3.2 如何减少计算提高精度?

早期backbone会提取图片中多尺度特征(multi-scale features),尽管引入多尺度特征有助于加速训练收敛,但它导致输入编码器的序列变长,计算量显著增加。为了高效处理多尺度特征,设计了一种高效的混合编码器,取代了传统的Transformer编码器。该编码器通过解耦不同尺度特征的尺度内交互与跨尺度融合,显著提升了推理速度。为了避免位置置信度较低的编码器特征被选为对象查询,提出了不确定性最小化查询选择机制,通过显式优化不确定性,为解码器提供高质量的初始查询,从而提升检测精度。此外,得益于DETR的多层解码器结构,RT-DETR支持灵活的速度调节,能够适应各种实时应用场景,且无需重新训练。

主要贡献总结如下:(i). 提出了首个实时端到端目标检测器RT-DETR,它不仅在速度和精度上均优于此前先进的YOLO检测器,还消除了NMS后处理对实时目标检测带来的负面影响;(ii). 定量分析了NMS对YOLO检测器速度和精度的影响,并建立了一个端到端速度基准,用于测试实时检测器的端到端推理速度;(iii). 所提出的RT-DETR支持通过调整解码器层数实现灵活的速度调节,以适应不同应用场景,且无需重新训练。

3.2.1 分析NMS对推理时间的影响

上图是yolov5和yolov8,限制置信度过滤后剩下的box框数。x轴是不同的box自信度,y轴是剩余box框数。下表是限制不同的置信度和IoU,得到的AP精度和NMS耗时。IoU越大,置信度越小,剩余box越多,NMS消耗的时间越多。DETR的模型去掉了NMS,直接预测结果,节省了后处理的时间。

3.3 模型结构

3.3.1 backbone

backbone 以 ResNet 为例,RT-DETR 会获取最后3个stages,统一通道数256得到  ,辅助模型能获取不同尺度的目标。以输入图片尺寸[3,576,576] 为例,S3:[256, 72, 72]; S4:[256, 36, 36]; S5:[256, 18, 18]。

3.3.2 AIFI (the Attention-based Intra-scale Feature Interaction)

AIFI尺度内注意力计算,实际上就是把之前的transformer 6层encoding 换成了一层encoding。输入为S5:[256, 18, 18]和Position Embedding。Position Embedding只作用于K和Q。最后得到的输出output:[256, 18, 18]。

3.3.3 CCFF (the CNN-based Cross-scale Feature Fusion)

CCFF 的整体结构和PAN结构类似,可以看出两个特征金字塔拼接而成,更有利于多尺度的特征融合,在YOLOv5和YOLOv8都有借鉴这种架构。RT-DETR 大概率是借鉴了YOLO系类,只是内部的每个小模块不一样,融合后得到[6804, 256]。

3.3.4 decoding 部分和DINO 的 decoding 几乎一样。可以参考DINO的decoding。

B站 平凡的兵

训练Q和K 来自真实标签生成和encoder预测。预测box也是偏移量,一层一层修正。

3.4 不确定性最小查询选择(Uncertainty-minimal Query Selection)

检测器需要同时对物体的类别和位置进行建模,这两者共同决定了特征的质量。选择出有效的token时,在之前的方案中,需要考虑分类和定位置信度,这样就会引入一个潜在变量,可能不能更有效选择出Query token。为了解决这一问题,提出了不确定性最小化查询选择方案,该方法通过显式构建并优化认知不确定性(epistemic uncertainty),以建模编码器特征的联合潜在变量,从而为解码器提供高质量的查询。

不确定性怎么理解?如果分类预测和定位预测分数近似,就代表可信度较高。例如,分类预测和定位预测都为0.5就可以表示:预测距离还有一些距离,是一个可行的预测。只有当分类预测和定位预测分数一高一低的预测,就说明该预测不可信,因为理论上分类预测和定位预测应该同步优化,不应该出现两个值差距太大。

特征不确定性  定义为式 (2) 中定位预测分布  与分类预测分布  之间的差异。为了最小化查询的不确定性,在训练阶段通过促使高IoU的特征获得高分类得分、低IoU的特征获得低分类得分,使模型在推断时根据分类得分选取的Top-K特征所对应的预测token同时具备高分类得分和高IoU值。挑选代码如下:

enc_outputs_class = self.enc_score_head(output_memory)
_, topk_ind = torch.topk(enc_outputs_class.max(-1).values, self.num_queries, dim=1)

同时将该不确定性引入损失函数中,在训练阶段通过促使高IoU的特征获得高分类得分、低IoU的特征获得低分类得分,如式 (3) 所示。

3.5 RT-DETR 分类损失函数

显然训练过程中,类别的标签不再是此前0和1离散值,而是0~1的连续值,所以不能采用focal loss,而是采用了variable focal loss(VFL),尽可能保证分类预测和定位预测分数近似(分类置信度和GIOU值相等)。Variable Focal Loss 是一种针对目标检测或实例分割等视觉任务中分类与定位质量不匹配问题而提出的损失函数改进方法,旨在动态调整样本的损失权重,使分类置信度(classification confidence)更好地反映预测结果的真实定位精度(如IoU)。

(1)Focal Loss:

交叉熵损失会使模型训练过程中会倾向于样本多的类别,造成模型对少样本类别的性能较差。Focal Loss在交叉上基础上增加了,当分类预测值,趋近于0。对于分类不准确的样本,趋近于1,可以使得模型更加关键预测不准的分类。即相比交叉熵损失,focal loss会更加关注分类不准确的样本。

(2)variable focal loss(训练可以保证p更加趋近y)。

3.6 RT-DETR 实验结果

表2将RT-DETR与当前的实时检测器(YOLOs)和端到端检测器(DETRs)进行了比较,其中仅对比了YOLO检测器的L和X模型,S和M模型的对比在附录中。RT-DETR与YOLO检测器采用相同的输入尺寸(640, 640),而其他DETRs则使用(800, 1333)的输入尺寸。FPS(每秒帧数)是在T4 GPU上使用TensorRT FP16模式测得的,并且对于YOLO检测器,RT-DETR-R50达到了53.1%的AP和108 FPS,而RT-DETR-R101达到了54.3%的AP和74 FPS,在速度和精度上均优于同规模的最先进YOLO检测器以及使用相同主干网络的DETR检测器。

Hybrid Encoder

  • A → B:变体B在A的基础上引入了一个单尺度Transformer编码器,该编码器使用了一层Transformer模块。多尺度特征共享该编码器进行单尺度内的特征交互,然后进行拼接作为输出。
  • B → C:变体C在B的基础上引入了跨尺度特征融合,将拼接后的特征输入到多尺度Transformer编码器中,以实现单尺度内和跨尺度特征的同步交互。
  • C → D:变体D通过使用单尺度Transformer编码器处理单尺度内交互,同时采用PANet风格的结构(PANet-style [22])实现跨尺度融合,从而将这两种机制解耦。
  • D → E:变体E在D的基础上进一步增强了单尺度内交互和跨尺度融合,采用了我们设计的一种高效的混合编码器(hybrid encoder)。

uncertainty-minimal query selection

不确定性最小化查询选择的消融实验结果。Propcls 和 Propboth 分别表示分类得分,以及分类得分与IoU得分均大于0.5的比例。RT-DETR中的查询选择根据分类分数选择前K (K = 300)个编码器特征作为内容查询,所选特征对应的预测框作为初始位置查询。

展示了使用不同数量解码器层训练的RT-DETR-R50各解码器层的推理延迟和精度。ID 表示解码器层的索引。Detk 表示具有 k 个解码器层的检测器。所有结果均基于采用 6× 配置的 RT-DETR-R50 报告。随着解码器层索引的增加,相邻解码器层之间的精度差异逐渐减小。以“RT-DETR-R50-Det6”这一列为例,使用第5个解码器层进行推理仅损失0.1% AP的精度(53.1% AP vs 53.0% AP),同时延迟减少了0.5毫秒(9.3毫秒 vs 8.8毫秒)。因此,RT-DETR支持通过调整解码器层数来灵活调节推理速度,而无需重新训练模型,从而提升了其实际应用的灵活性。

 如果你想转行到AI,或者想了解大模型方面的知识,但是不知道从何下手,我也准备了一个可以帮你三个月入门到深度学习的籽料包:

【1.超详细的人工智能学习大纲】:一个月精心整理,快速理清学习思路!

【2.基础知识】:Python基础+高数基础

【3.机器学习入门】:机器学习经典算法详解

【4.深度学习入门】:神经网络基础(CNN+RNN+GAN)

【5.项目实战合集】:34个机器学习+36深度学习项目实战

可以关住我的公//粽//呺【迪哥谈AI】回复333 无偿获取!(或者扫马)

4、D-FINE

4.1 D-FINE 前言

D-Fine是24年发布的DETR系模型,代码主要基于RT-DETR做的修改,同时借鉴了GFocal,YOLOv9内容。

D-FINE: R EDEFINE REGRESSION TASK IN DETR S AS F INE- GRAINED D ISTRIBUTION REFINEMENarxiv.org/pdf/2410.13842

D-FINE:将 DETR 中的回归任务重新定义为细粒度分布细化。D-FINE重新定义 DETR 模型中的边界框回归任务,实现了卓越的定位精度。D-FINE 包含两个核心组件:细粒度分布细化(Fine-grained Distribution Refinement, FDR)和全局最优定位自蒸馏(Global Optimal Localization Self-Distillation, GO-LSD)。FDR 将回归过程从预测固定坐标转变为迭代地细化概率分布,提供了一种细粒度的中间表示,显著提升了定位精度。GO-LSD 是一种双向优化策略,通过自蒸馏将来自精细化分布的定位知识传递到较浅层,同时简化了深层网络的残差预测任务。

早期离散化的概率分布对从锚点到四条边的距离进行回归:(1)锚点依赖性:回归与锚框中心绑定,限制了预测的多样性以及与无锚框(anchor-free)框架的兼容性;(2)缺乏迭代优化:预测为一次性完成,没有进行迭代优化,降低了回归的鲁棒性。(3)粗定位:固定的距离范围和均匀的仓间隔可能导致粗定位,特别是对于小对象,因为每个仓代表了广泛的可能值范围。之前工作是采用狄拉克(Dirac delta)方式,假设将边界盒边缘视为精确和固定的,使得难以对定位不确定性进行建模,特别是在模棱两可的情况下。这种僵化的表示不仅限制了优化,而且在预测偏移很小的情况下会导致严重的定位修正。

4.2 细粒度分布细化(Fine-grained Distribution Refinement, FDR)

FDR(细粒度分布优化)通过迭代优化作为边界框预测修正项的概率分布,提供更细粒度的中间表示。该方法能够独立捕捉并优化每条边的不确定性。借助非均匀权重函数,FDR 能够在每个解码器层进行更精确、渐进式的调整,从而提升定位精度并减少预测修正。传统早期的目标检测对于target框是回归预测一个固定的值,D-Fine把预测值修改预测一个分布。在代码实现中会把bbox的每一个点特征通过bbox_head(修正预测头)转换成纬度为33的分布。代码部份详细解释。

4.3 全局最优定位自蒸馏(Global Optimal Localization Self-Distillation, GO-LSD)

GO-LSD(全局最优定位自蒸馏)将来自精细化分布的定位知识蒸馏至较浅层。随着训练的进行,最后一层生成的软标签(soft labels)越来越精确。较浅层通过 GO-LSD 使其预测与这些标签对齐,从而获得更准确的预测结果。随着早期阶段的预测性能提升,后续层则可以专注于优化更小的残差。这种相互增强机制产生了协同效应,使得定位精度逐步提升。也就是把最后一层的decoder预测当老师,把前面的预测当学生,让学生学习老师的分布,就形成自蒸馏了。代码部份详细解释。

细粒度分布优化(Fine-grained Distribution Refinement, FDR)通过解码器层迭代优化生成的细粒度分布,如图2所示。最初,第一层解码器通过传统的边界框回归头和一个D-FINE头(两者均为多层感知机MLP,仅输出维度不同)预测初步的边界框和初步的概率分布。每个边界框对应四个分布,分别对应四条边。初始预测的边界框作为参考框,后续解码层则专注于通过以残差方式调整分布来对其进行优化。优化后的分布随后被用于调整对应初始边界框的四条边,随着每次迭代逐步提升其定位精度。

4.4 D-FINE 代码

4.4.1 D-FINE 类主要分为三部份backbone、encoder和decoder

backbone 采用的HGNetv2,encoder采用类似PAN结构,decoder采用的self-Attention和Deformable Attention。

def forward(self, x, targets=None):  # [1, 3, 640, 640]
    x = self.backbone(x)    # output:[[1, 512, 80, 80],[1, 1024, 40, 40],[1, 2048, 20, 20]]
    x = self.encoder(x)     # output:[[1, 384, 80, 80],[1, 384, 40, 40],[1, 384, 20, 20]]
    x = self.decoder(x, targets) # output: pred_logits:[1, 300, 80]; pred_boxes: [1, 300, 4]
    return x

4.4.2 backbone(HGNetv2)

backbone最后得到一个list,每个元素保存的shape如:[[1, 512, 80, 80],[1, 1024, 40, 40],[1, 2048, 20, 20]]。

4.4.3 encoder

encoder的结构和PAN类似,首先通过self-attention,在做self-attention时会提前对三个特征图对Q和K进行位置编码(q + pos_embed),(k + pos_embed)。再通过PAN结构,最后得到一个list,每个元素保存的shape如[[1, 384, 80, 80],[1, 384, 40, 40],[1, 384, 20, 20]]。

4.4.4 decoder

和dino一样,在训练过程中,proposals来自两部份,一部分是由真实标签加噪声生成的带噪声proposals(推理过程中没有),一部分是encoder的预测结果生成的proposals。为什么要用真实标签加噪声生成的复杂label?一般图像中正样本很少,负样本很多,导致监督信息不足。为了加快收敛,增加更多监督信息,采用对真实标签添加噪声,再用encoder一层一层的修正proposals,得到最总预测结果。

4.4.1 通过真实标签生成训练数据集辅助训练(这部分只有训练过程中有,get_contrastive_denoising_training_group)

如下图原始的batch中的label。找出batch中样本最多target数,第三个样本有5个target,其他少于5个点就用mask填充(白色)。

然后再重复多组,每一组都由两个完整的batach组成,分为positive(正样本)和negative(负样本,白色区域也为负样本),总长度不超过200,其中正样本100,负样本100。

(1)给class添加噪声,针对正负样本所有类别,每个类别都会有25%概率随机从类别数中挑选一次,然后再对类别进行编码成256向量,input_query_logits[32, 198, 256]

mask = torch.rand_like(input_query_class, dtype=torch.float) < (label_noise_ratio * 0.5)
# randomly put a new one here
new_label = torch.randint_like(mask, 0, num_classes, dtype=input_query_class.dtype)
input_query_class = torch.where(mask & pad_gt_mask, new_label, input_query_class)
input_query_logits = class_embed(input_query_class) # 对分类进行编码 长度为256向量

(2)给box加噪声,对于positive(正样本)左上和右下x坐标随机添加的噪声小于真实box宽的1/2,左上和右下y坐标随机添加的噪声小于真实box高的1/2。对于negative(负样本)左上和右下x坐标随机添加的噪声是真实box宽的1/2到1之间,左上和右下y坐标随机添加的噪声是真实box高的1/2到1之间。如下图是一个目标框的4个点,对于正样本噪声的范围是在白色区域,对于负样本噪声范围是在浅蓝色区域。

known_bbox = box_cxcywh_to_xyxy(input_query_bbox)
diff = torch.tile(input_query_bbox[..., 2:] * 0.5, [1, 1, 2]) * box_noise_scale # [w/2, h/2, w/2, h/2]
rand_sign = torch.randint_like(input_query_bbox, 0, 2) * 2.0 - 1.0  # [x, y, x, y]  ∈ {-1, 1} 随机选一个方向
rand_part = torch.rand_like(input_query_bbox)              # 随机选一个大小
rand_part = (rand_part + 1.0) * negative_gt_mask + rand_part * (1 - negative_gt_mask)
known_bbox += rand_sign * rand_part * diff                 # 添加噪声
known_bbox = torch.clip(known_bbox, min=0.0, max=1.0)      # 限制在0-1之间
input_query_bbox = box_xyxy_to_cxcywh(known_bbox)          # 转换回去
input_query_bbox[input_query_bbox < 0] *= -1               # 保证bbox不为负数
input_query_bbox_unact = inverse_sigmoid(input_query_bbox) # 反sigmoid

(3) 创建 mask ,在计算注意力时只能看到自己组内的查询,不能看到其他组的查询。为什么要这么做,查看上图分组图,每一组就是一个完整的label预测。

tgt_size = num_denoising + num_queries
attn_mask = torch.full([tgt_size, tgt_size], False, dtype=torch.bool, device=device)
# match query cannot see the reconstruction
attn_mask[num_denoising:, :num_denoising] = True

# reconstruct cannot see each other
for i in range(num_group): #只能看到自己组内的查询,不能看到其他组的查询
    if i == 0:
        attn_mask[
            max_gt_num * 2 * i : max_gt_num * 2 * (i + 1),
            max_gt_num * 2 * (i + 1) : num_denoising,
        ] = True
    if i == num_group - 1:
        attn_mask[max_gt_num * 2 * i : max_gt_num * 2 * (i + 1), : max_gt_num * i * 2] = True
    else:
        attn_mask[
            max_gt_num * 2 * i : max_gt_num * 2 * (i + 1),
            max_gt_num * 2 * (i + 1) : num_denoising,
        ] = True
        attn_mask[max_gt_num * 2 * i : max_gt_num * 2 * (i + 1), : max_gt_num * 2 * i] = True

生成的attn_mask 图像如下,主要分为两个区域一部份是真实label生成的(33x2x3=198),33表示每个batch最大label数,2表示一组中含有positive和negative,3表示一共有三组;另外一部份是encoder输出的300表示筛选出了300最可能的proposals:

4.4.2 encoder的预测结果生成的proposals(get_contrastive_denoising_training_group)+真实标签生成的proposals(只有训练有这部分)

上图是生成encoding输入的流程图,特征图通过en_output和enc_score_head得到8400个分类置信度,再通过分类置信度从大到小筛选出300个有效的分类置信度和原始特征。塞选出的300个原始的特征通过enc_bbox_head层得到box的修正预测,再加上anchors就得到proposals(初始预测框预测)。再分别拼接真实标签生成带噪声的分类和带噪声的proposals,得到decoder的输入。

(1)生成对图特征图的每个像素点生成一个anchors,把边缘区域mask掉。

anchors = []
for lvl, (h, w) in enumerate(spatial_shapes):
    grid_y, grid_x = torch.meshgrid(torch.arange(h), torch.arange(w), indexing="ij")
    grid_xy = torch.stack([grid_x, grid_y], dim=-1)                 # 左上角坐标
    grid_xy = (grid_xy.unsqueeze(0) + 0.5) / torch.tensor([w, h], dtype=dtype)  # 左上角坐标 变为 中心点
    wh = torch.ones_like(grid_xy) * grid_size * (2.0**lvl)          # 生成每个锚点的 宽高(w, h)
    lvl_anchors = torch.concat([grid_xy, wh], dim=-1).reshape(-1, h * w, 4)
    anchors.append(lvl_anchors)

anchors = torch.concat(anchors, dim=1).to(device)
valid_mask = ((anchors > self.eps) * (anchors < 1 - self.eps)).all(-1, keepdim=True) # mask边缘anchors
anchors = torch.log(anchors / (1 - anchors))
anchors = torch.where(valid_mask, anchors, torch.inf)

(2)生成的proposals,使用valid_mask把encoder的memory边缘特征给mask掉,再通过检测头和分类头得到初始的proposals

memory = valid_mask.to(memory.dtype) * memory

output_memory: torch.Tensor = self.enc_output(memory) # torch.Size([32, 5376, 256])
enc_outputs_logits: torch.Tensor = self.enc_score_head(output_memory) # torch.Size([32, 5376, 80])

(3) 通过ecnoder的outputs_logits选出enc_topk_memory, enc_topk_logits, enc_topk_anchors

# torch.topk(outputs_logits.max(-1).values, topk, dim=-1)

memory = valid_mask.to(memory.dtype) * memory

output_memory: torch.Tensor = self.enc_output(memory)
enc_outputs_logits: torch.Tensor = self.enc_score_head(output_memory)

enc_topk_bboxes_list, enc_topk_logits_list = [], []
enc_topk_memory, enc_topk_logits, enc_topk_anchors = self._select_topk(output_memory, enc_outputs_logits, anchors, self.num_queries)
enc_topk_bbox_unact: torch.Tensor = self.enc_bbox_head(enc_topk_memory) + enc_topk_anchors # 通过encoder生成proposals

if self.training:
    enc_topk_bboxes = F.sigmoid(enc_topk_bbox_unact)
    enc_topk_bboxes_list.append(enc_topk_bboxes)
    enc_topk_logits_list.append(enc_topk_logits)

if self.learn_query_content:
    content = self.tgt_embed.weight.unsqueeze(0).tile([memory.shape[0], 1, 1])
else:
    content = enc_topk_memory.detach()

enc_topk_bbox_unact = enc_topk_bbox_unact.detach()

if denoising_bbox_unact is not None:
    enc_topk_bbox_unact = torch.concat([denoising_bbox_unact, enc_topk_bbox_unact], dim=1)
    content = torch.concat([denoising_logits, content], dim=1)

return content, enc_topk_bbox_unact, enc_topk_bboxes_list, enc_topk_logits_list
  • enc_bbox_head:预测为坐标和真实标签的偏差值,所以需要加上enc_topk_anchors,才是当前预测的proposals。
  • content:剔除梯度(梯度最多回传到这里),真实标签生成的分类分布 + encoder生成分类特征。
  • enc_topk_bbox_unact:剔除梯度(梯度最多回传到这里),真实标签生成的proposals + encoder生成proposals。
  • enc_topk_bboxes_list:保存encoder生成proposals(box)。
  • enc_topk_logits_list:保存encoder生成logits(分类)。

其中,decoder输出的结果通过self.enc_score_head的到分类类别概率分布。enc_outputs_logits.shape:[32, 8400, 80],8400表示8400个box框,80表示80个类别。

enc_outputs_logits: torch.Tensor = self.enc_score_head(output_memory)

其中,decoder输出的结果通过self.enc_bbox_head的到box修正值,修正值加上anchors的到enc_topk_bbox_unact.shape:[32, 300, 4],300表示分类置信度最高的300歌候选box框,4表示左上右下坐标。

enc_topk_bbox_unact: torch.Tensor = self.enc_bbox_head(enc_topk_memory) + enc_topk_anchors

(4)decoder一层 (decoder 的每一层有两个attention和进行与真实target预测操作)

一张图片通过Backbone和Encoder,再通过decoder Layer 1 得到初始的修正预测和初始的预测框,再后面decoder Layer2-6层依次累加修正预测值概率分布(每一层的预测是修正值概率分布,每一层的修正值会叠加上一层的修正值概率分布),逐渐精修预测结果,最后一层得到最终的预测。

  • init_ref_contents:真实标签生成的分类分布 + encoder生成分类特征。
  • init_ref_points_unact:真实标签生成的proposals + encoder生成proposals。
  • memory:encoder提取的特征。
  • spatial_shapes:不同特征尺度的高宽。
  • attn_mask:不同组之前的mask。
  • 初始情况下:output_detach = pred_corners_undetach = 0。ref_points_initial 只在第一层初始化好,后面层不再改动,只在这个基础上一层一层修正。
  • detach是禁止反响传播。
  • ref_points_detach 通过两层的(linear(4,512),linear(512,256))得到位置编码query_pos_embed。
  • value是list,每一个的shape分别为:[32, 8, 32, 6400]、[32, 8, 32, 1600] 、([32, 8, 32, 400]。其中32是batch 数, 8 是num_head,32是,最后一个表示box数量。
  • pred_corners:[32, 496, 132], 其中32是batch,496是保留box数量,132=4*33,4表示上下左右4个边,33表示每个边的离散分布。每次离散分布的修正值都需要考虑上一次的decoder输出,离散分布的修正值计算是累积前面所有层的结果。
  • project:离散分布转换成修正值的权重。是一个33个固定的数。

第一个attention,qkv都来自于分类特征:

q = k = self.with_pos_embed(target, query_pos_embed)
target2, _ = self.self_attn(q, k, value=target, attn_mask=attn_mask)
target = target + self.dropout1(target2)
target = self.norm1(target)

其中target 为content:剔除梯度(梯度最多回传到这里),真实标签生成的分类分布 + encoder生成分类特征。对q和k进行了位置编码。

第二个attention,采用的MSDeformableAttention(后面单独写文章介绍):

# cross attention
target2 = self.cross_attn(self.with_pos_embed(target, query_pos_embed), reference_points, value, spatial_shapes)

target = self.gateway(target, self.dropout2(target2))

target2 = self.forward_ffn(target)
target = target + self.dropout4(target2)
target = self.norm3(target.clamp(min=-65504, max=65504))

ref_points_detach[0].shape:[32, 498, 4](上一层预测误差值)映射到query_pos_embed.shape[32, 498, 256]做为attenton的输入:

query_pos_embed = query_pos_head(ref_points_detach).clamp(min=-10, max=10)

decoder 第一层预测box框(pre_bboxes:[32, 498, 4])是第一层预测的修正值【pre_bbox_head(output)】加上在encoder预测box(ref_points_detach)得到的。pre_scores:[32, 498, 80]通过第一层的输出通过score_head[0]得到的分类分数。ref_points_initial [32, 498, 4]为初始box,只在第一层初始化,后面每一层decoder都在这个基础上进行累积修正。

if i == 0:
    # Initial bounding box predictions with inverse sigmoid refinement
    pre_bboxes = F.sigmoid(pre_bbox_head(output) + inverse_sigmoid(ref_points_detach))
    pre_scores = score_head[0](output)
    ref_points_initial = pre_bboxes.detach()

pred_corners[32, 498, 132]是预测当前层box的误差值的离散分布形式。当前层误差值的离散分布是当前层decoder输出加上上一层的decoder输出,通过bbox_head层的输出,再加上一层误差值的离散分布预测得到的。第一层初始化:output_detach =0,pred_corners_undetach=0。

pred_corners = bbox_head[i](output + output_detach) + pred_corners_undetach

下面计算每一层的box预测值inter_ref_bbox[32, 498, 4],现在得到box初始预测值ref_points_initial [32, 498, 4](第一层得到的)和每一层得到的box离散分布形式的误差值pred_corners[32, 498, 132]。ref_points_initial就可以通过pred_corners修正得到每一层的box预测值,在计算误差之前需要通过integral(pred_corners, project)把离散分布转换成box预测值[32, 498, 4],后面有详细解释。

inter_ref_bbox = distance2bbox(ref_points_initial, integral(pred_corners, project), reg_scale)

修正值的计算公式:

x.shape是[63488, 33] 其中33表示每个边的误差分布,w(n)参考下面project公式和图,修正值计算对应位置相乘相加,代码如下。

F.linear(x, project.to(x.device))

修正值分布的计算公式

图中project值的计算公式:

project可视化图形:

分类预测,通过box的离散分布修正分类预测,其中prob_topk.shape[32, 498, 4, 4]、prob.shape[32, 498, 4, 33]、stat.shape[32, 498, 4, 5]、quality_score.shape[32, 498, 1]。

 def forward(self, scores, pred_corners):
        B, L, _ = pred_corners.size()
        prob = F.softmax(pred_corners.reshape(B, L, 4, self.reg_max + 1), dim=-1)
        prob_topk, _ = prob.topk(self.k, dim=-1)
        stat = torch.cat([prob_topk, prob_topk.mean(dim=-1, keepdim=True)], dim=-1)
        quality_score = self.reg_conf(stat.reshape(B, L, -1))
 return scores + quality_score

4.5 D-FINE 损失函数

4.5.1 encoder 会提前保存下面内容,498可以拆分成两部分,198为带噪声的辅助预测和300为encoder的输出预测:

  • inter_ref_bboxs[6, 32, 498, 4]:每一层decoder box预测。
  • scores[6, 32, 498, 80]:每一层decoder lcs 预测。
  • pred_corners[6, 32, 498, 132]:每一层decoder box 修正值离散分布预测。
  • ref_points_initial[6, 32, 498, 4]:第一层 decoder box 的初始预测,重复保存了6次。
  • pre_bboxes[32, 498, 4]:第一层decoder box 的初始预测,和ref_points_initial 值一样。
  • pre_logits[32, 498, 80]:第一层decoder cls 的初始预测,没有引入修正。
  • enc_topk_bboxes_list[32, 300, 4]:encoder 输出目标框预测。
  • enc_topk_logits_list[32, 300, 80]:encoder 输出分类预测。

1、encoder输出的300个预测:

outputs:

  • pred_logits:[32, 300, 80]表示decoder最后一层decoder cls预测。
  • pred_boxes:[32, 300, 4]表示decoder最后一层box预测。
  • pred_corners :[32, 300, 132]表示decoder最后一层box修正值离散分布预测。
  • ref_points:[32, 300, 4] 表示decoder最后一层box基准预测(和pre_bboxes一样,每一层的值一样,第一层初始化好,就不再改动)。

outputs['aux_outputs']:

  • pred_logits:5个[32, 300, 80],表示1-5层decoder cls预测。
  • pred_boxes:5个[32, 300, 4],表示1-5层decoder box预测。
  • pred_corners:5个[32, 300, 132],表示1-5层box修正值离散分布预测。
  • ref_points:5个[32, 300, 4],表示1-5层decoder box初始基准预测,实际每一层都一样。
  • teacher_corners:5个[32, 300, 132],5个的值相同,表示decoder最后一层box修正值离散分布预测,作为自监督学习的老师。
  • teacher_logits:5个[32, 300, 80],5个的值相同,表示decoder最后一层decoder cls预测,作为自监督学习的老师。

outputs["enc_aux_outputs"]:

  • pred_logits:[32, 300, 80],表示encdoer cls预测。
  • pred_boxes:[32, 300, 4],表示encdoer box预测。

outputs["pre_outputs"]:

  • pred_logits:[32, 300, 80],第一层decoder cls 的初始预测,没有引入修正。
  • pred_boxes:[32, 300, 4],第一层decoder box 的初始预测,没有引入修正,和ref_points_initial 值一样。

2、真实标签生成198个带噪声辅助训练的预测 :

outputs["dn_outputs"]:

  • pred_logits:6个[32, 198, 80],表示1-6层 decoder cls预测。
  • pred_boxes:6个[32, 198, 4],表示1-6层decoder box预测。
  • pred_corners:6个[32, 198, 132],表示1-6层box修正值离散分布预测。
  • ref_points:6个[32, 198, 4],表示1-6层decoder box初始基准预测,实际每一层都一样。
  • teacher_corners:6个[32, 198, 132],表示decoder最后一层box修正值离散分布预测,作为自监督学习的老师。
  • teacher_logits:6个[32, 198, 80],表示decoder最后一层decoder cls预测,作为自监督学习的老师。

outputs["dn_pre_outputs"]:

  • pred_logits:[32, 198, 80],第一层decoder cls 的初始预测,没有引入修正。
  • pred_boxes:[32, 198, 4],第一层decoder box 的初始预测,没有引入修正,和ref_points_initial 值一样。

计算匹配的代价矩阵:

分为三部分:Focal Loss代价矩阵,box框距离cost_bbox,box框重叠程度cost_giou。

encoder输出的300个对应decoder最后一层decoder cls预测out_prob([32, 300, 80]->[9600, 80]),通过真实分类标签tgt_ids(len 192),提取所有预测 query 在目标类别上的概率 [9600, 192],可以理解成把任何一个真实标签需要去预测的batch中找到对应分类的置信度。计算负样本和正样本的损失项。得到cost_class[9600, 192]。

out_prob = out_prob[:, tgt_ids]
neg_cost_class = ((1 - self.alpha) * (out_prob**self.gamma) * (-(1 - out_prob + 1e-8).log()))
pos_cost_class = (self.alpha * ((1 - out_prob) ** self.gamma) * (-(out_prob + 1e-8).log()))
cost_class = pos_cost_class - neg_cost_class

cost_bbox:[9600, 192]。

cost_bbox = torch.cdist(out_bbox, tgt_bbox, p=1)

cost_giou:[9600, 192]。

cost_giou = -generalized_box_iou(box_cxcywh_to_xyxy(out_bbox), box_cxcywh_to_xyxy(tgt_bbox))
C = self.cost_bbox * cost_bbox + self.cost_class * cost_class + self.cost_giou * cost_giou

通过代价矩阵C计算出预测值与真实值的匹配关系。indices[0]:(tensor([0, 1, 7]), tensor([0, 1, 2]))

  • tensor([0, 1, 7]) → 预测框的索引(在 预测 里面的位置)
  • tensor([0, 1, 2]) → 真实框的索引(在 GT 列表里的位置)

4.5.2、VFL 使用预测框与真实框之间的 IoU 作为正样本的软标签(soft label),步骤如下:

构建分类label(one_hot),背景类别为:self.num_classes + 1,对应类别位置的为预测box与真实box的iou值,即正类的目标值为 IoU,其他类目标为 0。

target_score_o = torch.zeros_like(target_classes, dtype=src_logits.dtype)
target_score_o[idx] = ious.to(target_score_o.dtype)
target_score = target_score_o.unsqueeze(-1) * target

计算动态损失权重:

pred_score = F.sigmoid(src_logits).detach()
weight = self.alpha * pred_score.pow(self.gamma) * (1 - target) + target_score

计算loss:

loss = F.binary_cross_entropy_with_logits(src_logits, target_score, weight=weight, reduction="none")
loss = loss.mean(1).sum() * src_logits.shape[1] / num_boxes

4.5.3、loss_boxes(L1 损失(L1 Regression Loss)+GIoU 损失(Generalized IoU Loss))

L1 损失(L1 Regression Loss):

loss_bbox = F.l1_loss(src_boxes, target_boxes, reduction="none")  # (N, 4)
losses["loss_bbox"] = loss_bbox.sum() / num_boxes

GIoU 损失(Generalized IoU Loss):

loss_giou = 1 - torch.diag(generalized_box_iou(box_cxcywh_to_xyxy(src_boxes), box_cxcywh_to_xyxy(target_boxes)))
loss_giou = loss_giou if boxes_weight is None else loss_giou * boxes_weight

4.5.4 Fine-Grained Localization (FGL) Loss:细粒度定位(FGL)损失:

原始 Distribution Focal Loss (DFL) 的核心思想是:将连续的坐标值回归问题,转化为一个离散的概率分布估计问题。具体来说:

  1. 离散化候选值:对于边界框的每一个坐标(例如,x 坐标),预先定义一组离散的候选值(例如,从 0 到 100,步长为 1,共 101 个值)。
  2. 预测概率分布:模型不再直接输出一个具体的坐标值,而是输出一个概率分布,表示真实坐标值落在每一个候选值附近的概率。
  3. 计算期望值:最终的预测坐标值是通过这个概率分布与候选值的加权平均(期望) 得到的。
  4. 使用 Focal Loss 训练:为了训练这个概率分布,使用 Focal Loss 来监督模型。真实标签是一个在真实坐标值最近的两个候选值上具有非零概率的“软标签”分布(通常使用线性插值),而不是一个简单的 one-hot 向量。

Fine-Grained Localization (FGL) Loss:细粒度定位(FGL)损失:

计算IOU权重,对于修正预测值不再是一个固定的值,而是一个分布。早期使用分类置信度和box质量置信度相乘联合计算NMS。只有正样本参与了box质量训练,负样本不参与box质量训练,会导致分类置信度非常低,但是box置信度非常高,就会影响nms结果。用一个分类头同时表示分类和box质量分数。真实标签是动态生成的,下图表示真实标签label为2,预测值与真实的box的iou为0.6。

计算W权重,通过预测box与真实的box label求解出误差。误差通过weighting_function计算出loss权重,

  • 比如原始某一个误差值为0.29,对应weighting_function的索引在18与19之间,
  • 可以找到10对应的值为left_values=0.23,19对应的值为right_values=0.37,
  • left_diffs=0.29-0.23=0.06;right_diffs=0.37-0.29=0.08。
  • weight_right=left_diffs/(right_values-left_values)=0.06/(0.37-0.23);
  • weight_left=right_diffs/(right_values-left_values)=0.08/(0.37-0.23);(代码采用的1-weight_right)。

计算FGL:

其中L表示层数。K表示 真实目标个数。每一层都会做匈牙利匹配,理论上一共会匹配L*K个预测。但需要排除,一个预测框再第一层分配给真实标签1,在后面层又分配给真实标签2的情况。n<-表示在weighting_function中索引18,n->表示索引19。

例如L=6,K=3。一共会获取8个匹配结果,分别是decoder最后一层的匹配结果;decoder 的1-5层匹配结果;encoder的输出匹配结果;第一层decoder通过pre_bbox_head生成的结果。按照匹配次数排序,预测框(8个匹配层)与真实框匹配次数越高,预测框越容易被保留下来。比如第6个预测,与第3个真实匹配了3次,与第5个真实匹配了2次,就把与第5个真实匹配给去掉。

修正预测值:pred_corners [32, 498, 132],修正真实值:用真实label减去上个时时刻的 inter_ref_bbox [32, 498, 4],得到的修正真实值。用预测减去function_value(project:shape[33]),计算出修正值在function_value的位置,left_value 和 right_value。

def _df_loss(pred_dist, target):
    """Return sum of left and right DFL losses."""
    # Distribution Focal Loss (DFL) proposed in Generalized Focal Loss https://ieeexplore.ieee.org/document/9792391
    tl = target.long()  # target left
    tr = tl + 1  # target right
    wl = tr - target  # weight left
    wr = 1 - wl  # weight right
    return (F.cross_entropy(pred_dist, tl.view(-1), reduction='none').view(tl.shape) * wl +
         F.cross_entropy(pred_dist, tr.view(-1), reduction='none').view(tl.shape) * wr).mean(-1, keepdim=True)`

这里的tl表示在weighting_function中索引18,tr表示索引19。

4.5.6 Global Optimal Localization Self-Distillation (GO-LSD):全局最优定位自蒸馏

把最后一层的decoder修正预测值当老师,把前面的修正预测值当学生,让学生学习老师的分布,就形成自蒸馏。最后一层的输出预测分布(pred_corners:[32, 498, 132])作为Teacher,前面几层输出预测分布(pred_corners:[32, 498, 132])作为student。其中以最后一层与真实target匈牙利匹配结果为划分,分为匹配成功的预测和未匹配成功的预测两组(Decoupled Distillation Focal (DDF) Loss,解耦蒸馏),分布计算loss。分布的误差采用KL散度计算老师与学生的距离。同时考虑匹配成功和匹配的失败的比例。

loss_match_local = (weight_targets_local* (T**2)* 
                  (nn.KLDivLoss(reduction="none")
                  (F.log_softmax(pred_corners / T, dim=1),
                  F.softmax(target_corners.detach() / T, dim=1),)).sum(-1))

loss_match_local1 = loss_match_local[mask].mean() if mask.any() else 0
loss_match_local2 = loss_match_local[~mask].mean() if (~mask).any() else 0
losses["loss_ddf"] = (loss_match_local1 * self.num_pos + loss_match_local2 * self.num_neg) / (self.num_pos + self.num_neg)

T=5,实际上是考虑匹配策略下,计算前面预测的误差离散部分与最后一层离散部分KL散度,让前面层向最后一层学习。

4.6 D-Fine试验结果

4.6.1 D-Fine试验对比

4.6.2 D-Fine消融试验

 如果你想转行到AI,或者想了解大模型方面的知识,但是不知道从何下手,我也准备了一个可以帮你三个月入门到深度学习的籽料包:

【1.超详细的人工智能学习大纲】:一个月精心整理,快速理清学习思路!

【2.基础知识】:Python基础+高数基础

【3.机器学习入门】:机器学习经典算法详解

【4.深度学习入门】:神经网络基础(CNN+RNN+GAN)

【5.项目实战合集】:34个机器学习+36深度学习项目实战

可以关住我的公//粽//呺【迪哥谈AI】回复333 无偿获取!(或者扫马)

5、DEIM

5.1 DEIM 前言

DEIM: DETR with Improved Matching for Fast Convergencearxiv.org/pdf/2412.04234

目标检测发展迅速,YOLO 系列以 一对多(O2M,一个目标与多个预测结果匹配)匹配 提供Dense supervision(稠密监督)信号,在收敛速度和精度上表现出色,但需要 NMS(非极大值抑制),会引入延迟和不稳定性。DETR 则采用 一对一(O2O,一个目标与一个预测结果匹配)匹配,直接学习目标和预测的一一对应,省去了 NMS,采用匈牙利匹配算法,实现端到端检测,但收敛速度慢、sparse supervision(监督稀疏),成为瓶颈。虽然部份DETR工作,通过真样本引入噪声(group DETR),缓解了监督稀疏,但相比yolo几百几千匹配远低于一个真实框。总结当前存在问题:

  1. 监督稀疏:O2O 每个目标只有一个正样本,远少于 O2M,大幅限制了学习效率,尤其对小目标不友好。
  2. 低质量匹配:DETR 使用少量随机初始化的 query,常与目标对齐不佳,导致大量低 IoU 的匹配,学习困难

5.2 Dense O2O 匹配(解决监督稀疏)从匹配数量和匹配质量两方面优化 DETR 训练

保持 O2O 框架,但通过 数据增强(mosaic、mixup) 增加每张图像中的目标数量。理解就是一张图片真样本不够,我就把几张图拼接在一起,增加正样本数。从而提升正样本数量,使监督更稠密,类似 O2M 的效果,但无额外解码器开销。但是会引入低质量框,所以本文有加入了另一创新点,优化低质量框的匹配MAL。

5.3 Matchability-Aware Loss (MAL)(解决低质量匹配)

  1. 现有 VFL(Varifocal Loss)对低 IoU 匹配惩罚过小,难以优化。
  2. MAL 引入匹配质量 (IoU) 到损失函数中,对低质量匹配给出更强的梯度,同时对高质量匹配与 VFL 类似。
  3. 一个目标框匹配到的一个预测框的iou为0,那么vfl计算loss就会使用负样本公式(q=0这个公式),就会导致正样本就更好少了。

希望IoU和分类分数一致,q是真实标签,采用预测框与真实框的iou作为q的值;p为分类的预测值分数。与VFL相比,优化了低IoU情况。目标标签已从  修改为  ,简化了正样本和负样本的损失权重,并去除了用于平衡正样本和负样本的超参数α。这一变化有助于避免对高质量框的过度强调,并改善整体训练过程,加快收敛速度。

5.4 DEIM 实验结果

RT-DETRv2 在 24 epoch即可达到 53.2% AP(在 RTX 4090 上),相比原始需要更长时间。在 D-FINE 上也能达到更快收敛。 Dense O2O 提升了小目标检测效果,但相比 YOLO,仍存在一定差距(例如 YOLOv9-E 在小目标上略优)。

 如果你想转行到AI,或者想了解大模型方面的知识,但是不知道从何下手,我也准备了一个可以帮你三个月入门到深度学习的籽料包:

【1.超详细的人工智能学习大纲】:一个月精心整理,快速理清学习思路!

【2.基础知识】:Python基础+高数基础

【3.机器学习入门】:机器学习经典算法详解

【4.深度学习入门】:神经网络基础(CNN+RNN+GAN)

【5.项目实战合集】:34个机器学习+36深度学习项目实战

可以关住我的公//粽//呺【迪哥谈AI】回复333 无偿获取!(或者扫马)

5、DEIM V2

5.1 DEIM V2前言

https://arxiv.org/pdf/2509.20787arxiv.org/pdf/2509.20787

论文Real-Time Object Detection Meets DINOv3。 DEIMv2这是一种实时目标检测器,基于先前的 DEIM 框架构建,并融合了 DINOv3 特征以进一步提升性能。DEIMv2 在其最大的两个变体(L 和 X 尺寸)中采用官方 DINOv3 预训练的骨干网络(ViT-Small 和 ViT-Small+),以最大化特征表达的丰富性;而其 S 和 M 变体则使用从 DINOv3 蒸馏得到的 ViT-Tiny 和 ViT-Tiny+ 骨干网络,在性能与效率之间实现精细平衡。为应对超轻量级应用场景,进一步推出了四个专用变体:Nano、Pico、Femto 和 Atto,从而将 DEIMv2 的可扩展性拓展至广泛的计算资源区间。全面覆盖 GPU、边缘设备和移动端部署场景。

为了在实时性约束下更好地利用 DINOv3 在大规模数据上预训练所得的强大特征表示,设计了空间调制适配器(Spatial Tuning Adapter, STA)。STA 与 DINOv3 并行运行,以无需额外参数的方式高效地将其单尺度输出转换为物体检测所需的多尺度特征。同时,STA 对输入图像进行快速下采样,生成具有极小感受野的细粒度多尺度细节特征,从而有效补充 DINOv3 所提供的强语义信息。

借鉴 Transformer 领域的最新进展,对解码器进行了简化。具体而言,将传统的前馈网络(FFN)和层归一化(LayerNorm)分别替换为 SwishFFN 和 RMSNorm ,这两种组件已被证明在不显著影响性能的前提下具有更高的效率。此外,注意到在迭代优化过程中,目标查询(object query)的位置变化极小,因此提出在所有解码器层之间共享查询位置编码。除此之外,还通过引入目标级 CopyBlend 数据增强(object-level CopyBlend augmentation)来增强 Dense O2O 训练策略,从而提升有效监督信号,进一步提高模型性能。贡献:

  • 对于较大模型,我们利用 DINOv3 提取强大的语义特征,并引入空间调制适配器(STA),以高效地将这些特征融入实时目标检测框架中。
  • 对于超轻量级模型,我们借助专家知识,对 HGNetv2-B0 的深度和宽度进行有效剪枝,以满足严格的计算资源约束。
  • 在骨干网络之外,我们进一步简化了解码器结构,并对 Dense O2O 训练策略进行了升级,从而进一步突破性能边界。

5.2 DEIM V2 优化方法

5.2.1 整体架构。整体架构遵循 RT-DETR 的设计,包含骨干网络(backbone)、混合编码器(hybrid encoder)和解码器(decoder)。如表 1 所示,对于主流变体 X、L、M 和 S,骨干网络基于 DINOv3,并结合了我们提出的空间调制适配器(Spatial Tuning Adapter, STA);其余变体则采用 HGNetv2 作为骨干网络。骨干网络输出的多尺度特征首先经过编码器处理,生成初始检测结果并筛选出 Top-K 个候选边界框;随后,解码器对这些候选框进行迭代优化,最终输出预测结果。

5.2.2 基于 ViT 的变体。对于较大的 DEIMv2 变体(S、M、L、X),围绕 Vision Transformer [3](ViT)系列精心设计骨干网络,在模型容量与效率之间取得平衡。 对于 L 和 X 变体,我们采用两个公开的 DINOv3 模型:ViT-Small 和 ViT-Small+,它们均包含 12 层,隐藏层维度为 384,能够提供强大的语义表征能力。 对于更轻量的 S 和 M 变体,我们直接从 ViT-Small(DINOv3 版本)中蒸馏出紧凑型骨干网络 ViT-Tiny 和 ViT-Tiny+,保留了 12 层的深度,同时将隐藏层维度分别缩减至 192 和 256。这一设计实现了从 S → M → L → X 的平滑缩放路径,确保每个变体在适应不同效率需求的同时,仍能保持具有竞争力的精度。

5.2.3 基于 HGNetv2 的变体。HGNetv2 由百度 PaddlePaddle 团队开发,因其高效性被广泛应用于实时 DETR 框架中,例如,D-FINE 就采用了完整的 HGNetv2 系列作为其骨干网络。 在超轻量级 DEIMv2 模型(Nano、Pico、Femto 和 Atto)中,以 HGNetv2-B0 为基础,但逐步对其深度和宽度进行剪枝,以满足不同的参数量预算。具体而言:Pico骨干网络移除了 B0 的第四阶段,仅保留到 1/16 尺度的输出;Femto在 Pico 的基础上,进一步将其最后一个阶段的模块数量从两个减少到一个;Atto则更进一步,将该最后一个模块的通道数从 512 压缩至 256,通过这种渐进式剪枝策略,在极低计算开销下实现了模型的高效部署,同时保持合理的检测性能。

5.2.4 DEIM V2 空间调制适配器(Spatial Tuning Adapter, STA)

为了更好地将 DINOv3 特征适配于实时目标检测任务,提出了空间调制适配器(STA),如图所示。STA 是一个全卷积网络,包含两个核心组件:

  • 一个超轻量级前馈网络,用于提取细粒度的多尺度细节特征;
  • 一个双向融合(Bi-Fusion),用于进一步增强来自 DINOv3 的特征表示。

DINOv3 基于 Vision Transformer(ViT)骨干网络,天然输出单尺度(1/16 分辨率)的密集特征。然在目标检测任务中,物体尺度变化极大,多尺度特征是提升性能最有效的方法之一。直接从 ViT 的若干中间块(例如第 5、8 和 11 层)提取 1/16 尺度的特征,并通过无参数的双线性插值将其上采样或下采样至多个目标尺度。随后,这些多尺度特征被送入 Bi-Fusion 操作器,该操作器由 1×1 卷积和一个超轻量级 CNN 构成,专门用于提取细粒度空间细节,从而有效补充 DINOv3 输出的强语义但缺乏局部细节的特征。这一设计在效率与精度之间实现了卓越的平衡,使其非常适合实时目标检测场景。

5.2.5 高效解码器(Efficient Decoder)

在标准的可变形注意力解码器(deformable attention decoder)[31] 基础上,融入了多项 Transformer 领域广泛采用的、面向效率优化的技术,从而在性能与计算开销之间实现了良好的平衡。具体而言:

  • 引入了 SwiGLU FFN,以增强模型的非线性表达能力;

  • 采用 RMSNorm 替代传统归一化方法,有效提升训练稳定性并加速收敛;

在迭代优化过程中,目标查询(object query)的位置变化极小,因此进一步提出在所有解码器层之间共享同一组位置编码,避免了重复计算,显著降低了冗余开销。这些改进共同构建了一个轻量、高效且高性能的解码器结构。

5.2.6 增强版 Dense O2O(Enhanced Dense O2O)

在先前的工作 DEIM 中,提出了 Dense O2O(密集一对一匹配)策略,通过增加每张训练图像中的目标数量来提供更强的监督信号,从而加快收敛速度并提升检测性能。该方法的有效性最初通过图像级增强技术(如 Mosaic 和 MixUp [28])得到了验证。在 DEIMv2 中,进一步将 Dense O2O 扩展到目标级别,并引入了 Copy-Blend增强策略:该方法将额外的目标对象(不含其原始背景)添加到图像中。与 Copy-Paste不同后者会完全覆盖目标区域Copy-Blend 会将新对象与原图进行融合,而非简单覆盖。这种更平滑的合成方式更契合我们的训练场景,并在各类配置下持续带来性能提升。

Copy-Blend增强策略:区域拷贝 + 混合的增强方式。具体做法是:从一个有噪声(noisy)图像中复制一个块(patch / region),然后把它混合(blend)到一个干净的(clean)图像上;反过来也可能把干净图像的块混合到有噪声图像上。目的在于让模型学习识别并恢复被损坏或被混入块的区域,从而提升模型在恢复、增强图像质量任务中的性能。

5.2.7 训练设置与损失函数(参考4 D-Fine)

训练策略基于 DEIM ,这是一个旨在实现快速收敛和高性能的基础框架。整体优化目标是五个组件的加权和:匹配性感知损失(Matchability-Aware Loss, MAL)、细粒度定位损失(Fine-Grained Localization Loss, FGL)、解耦蒸馏焦点损失(Decoupled Distillation Focal Loss, DDF)、边界框损失(L1)以及 GIoU 损失 。总损失定义为:

权重在所有实验中设置为:,,,,。

表 2 中总结了训练超参数,包括输入分辨率、学习率、训练轮次以及 Dense O2O 设置。一个有趣的观察结果是,将 FGL 和 DDF 损失应用于超轻量级模型时会导致性能下降。认为这是由于这些模型容量有限且基线精度较低,从而削弱了自蒸馏的效果。因此,在训练 Pico、Femto 和 Atto 变体时,排除了这两个组件(即局部损失)。

5.2 DEIM V2 实验结果

轻量级模型实验对比:

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐