ICCV2023:Co-DETR:DETRs与协同混分配训练

66 AP on COCO test-dev,DINO+ATSS+faster-rcnn达成最强目标检测器

论文题目

DETRs with Collaborative Hybrid Assignments Training

摘要

我们提出了一种新的协同混合任务训练方案,即Co-DETR,以从多种标签分配方式中学习更高效的基于detr的检测器。这种新的训练方案通过训练ATSS和Faster RCNN等一对多标签分配监督下的多个并行辅助头部,可以很容易地提高编码器在端到端检测器中的学习能力。此外,我们通过从这些辅助头部提取正坐标来进行额外的定制正查询,以提高解码器中正样本的训练效率。在推理中,这些辅助头被丢弃,因此我们的方法不给原始检测器引入额外的参数和计算成本,同时不需要手工制作的非最大抑制(NMS)。

代码:https://github.com/Sense-X/Co-DETR

源码阅读

代码基于mmdetection实现论文的两个核心改进点:1)增加faster-rcnn和ATSS辅助头2)faster-rcnn和ATSS的输出作为query输入给decoder,这部分query不需要做二分匹配,因为都是输出的检测结果
1、配置文件projects\configs\co_deformable_detr\co_deformable_detr_r50_1x_coco.py基于deformable_detr 修改,主要增加faster-rcnn和ATSS辅助头_base_ = [    '../_base_/datasets/coco_detection.py',    '../_base_/default_runtime.py']# model settings解码器层数num_dec_layer = 6辅助头的损失权重lambda_2 = 2.0
model = dict(    type='CoDETR',    backbone=dict(        type='ResNet',        depth=50,        num_stages=4,        out_indices=(1, 2, 3),        frozen_stages=1,        norm_cfg=dict(type='BN', requires_grad=False),        norm_eval=True,        style='pytorch',        init_cfg=dict(type='Pretrained', checkpoint='torchvision://resnet50')),    neck=dict(        type='ChannelMapper',        in_channels=[512, 1024, 2048],        kernel_size=1,        out_channels=256,        act_cfg=None,        norm_cfg=dict(type='GN', num_groups=32),        num_outs=4),    faster-rcnn的辅助rpn头    rpn_head=dict(        type='RPNHead',        in_channels=256,        feat_channels=256,        anchor_generator=dict(            type='AnchorGenerator',            octave_base_scale=4,            scales_per_octave=3,            ratios=[0.5, 1.0, 2.0],            strides=[8, 16, 32, 64, 128]),        bbox_coder=dict(            type='DeltaXYWHBBoxCoder',            target_means=[.0, .0, .0, .0],            target_stds=[1.0, 1.0, 1.0, 1.0]),        loss_cls=dict(            type='CrossEntropyLoss', use_sigmoid=True, loss_weight=1.0*num_dec_layer*lambda_2),        loss_bbox=dict(type='L1Loss', loss_weight=1.0*num_dec_layer*lambda_2)),    query_head=dict(        type='CoDeformDETRHead',        num_query=300,        num_classes=80,        in_channels=2048,        sync_cls_avg_factor=True,        with_box_refine=True,        as_two_stage=True,        mixed_selection=True,        transformer=dict(            type='CoDeformableDetrTransformer',            2个辅助头            num_co_heads=2,            encoder=dict(                正常的encoder                type='DetrTransformerEncoder',                num_layers=6,                transformerlayers=dict(                    type='BaseTransformerLayer',                    attn_cfgs=dict(                        type='MultiScaleDeformableAttention', embed_dims=256, dropout=0.0),                    feedforward_channels=2048,                    ffn_dropout=0.0,                    operation_order=('self_attn', 'norm', 'ffn', 'norm'))),            decoder=dict(                type='CoDeformableDetrTransformerDecoder',                num_layers=num_dec_layer,                return_intermediate=True,                look_forward_twice=True,                transformerlayers=dict(                    type='DetrTransformerDecoderLayer',                    attn_cfgs=[                        dict(                            type='MultiheadAttention',                            embed_dims=256,                            num_heads=8,                            dropout=0.0),                        dict(                            type='MultiScaleDeformableAttention',                            embed_dims=256,                            dropout=0.0)                    ],                    feedforward_channels=2048,                    ffn_dropout=0.0,                    operation_order=('self_attn', 'norm', 'cross_attn', 'norm',                                     'ffn', 'norm')))),        positional_encoding=dict(            type='SinePositionalEncoding',            num_feats=128,            normalize=True,            offset=-0.5),        loss_cls=dict(            type='FocalLoss',            use_sigmoid=True,            gamma=2.0,            alpha=0.25,            loss_weight=2.0),        loss_bbox=dict(type='L1Loss', loss_weight=5.0),        loss_iou=dict(type='GIoULoss', loss_weight=2.0)),    faster-rcnn的辅助roi头    roi_head=[dict(        type='CoStandardRoIHead',        bbox_roi_extractor=dict(            type='SingleRoIExtractor',            roi_layer=dict(type='RoIAlign', output_size=7, sampling_ratio=0),            out_channels=256,            featmap_strides=[8, 16, 32, 64],            finest_scale=112),        bbox_head=dict(            type='Shared2FCBBoxHead',            in_channels=256,            fc_out_channels=1024,            roi_feat_size=7,            num_classes=80,            bbox_coder=dict(                type='DeltaXYWHBBoxCoder',                target_means=[0., 0., 0., 0.],                target_stds=[0.1, 0.1, 0.2, 0.2]),            reg_class_agnostic=False,            reg_decoded_bbox=True,            loss_cls=dict(                type='CrossEntropyLoss', use_sigmoid=False, loss_weight=1.0*num_dec_layer*lambda_2),            loss_bbox=dict(type='GIoULoss', loss_weight=10.0*num_dec_layer*lambda_2)))],    ATSS辅助头    bbox_head=[dict(        type='CoATSSHead',        num_classes=80,        in_channels=256,        stacked_convs=1,        feat_channels=256,        anchor_generator=dict(            type='AnchorGenerator',            ratios=[1.0],            octave_base_scale=8,            scales_per_octave=1,            strides=[8, 16, 32, 64, 128]),        bbox_coder=dict(            type='DeltaXYWHBBoxCoder',            target_means=[.0, .0, .0, .0],            target_stds=[0.1, 0.1, 0.2, 0.2]),        loss_cls=dict(            type='FocalLoss',            use_sigmoid=True,            gamma=2.0,            alpha=0.25,            loss_weight=1.0*num_dec_layer*lambda_2),        loss_bbox=dict(type='GIoULoss', loss_weight=2.0*num_dec_layer*lambda_2),        loss_centerness=dict(            type='CrossEntropyLoss', use_sigmoid=True, loss_weight=1.0*num_dec_layer*lambda_2)),],    # model training and testing settings    train_cfg=[        dict(            assigner=dict(                type='HungarianAssigner',                cls_cost=dict(type='FocalLossCost', weight=2.0),                reg_cost=dict(type='BBoxL1Cost', weight=5.0, box_format='xywh'),                iou_cost=dict(type='IoUCost', iou_mode='giou', weight=2.0))),        dict(            rpn=dict(                assigner=dict(                    type='MaxIoUAssigner',                    pos_iou_thr=0.7,                    neg_iou_thr=0.3,                    min_pos_iou=0.3,                    match_low_quality=True,                    ignore_iof_thr=-1),                sampler=dict(                    type='RandomSampler',                    num=256,                    pos_fraction=0.5,                    neg_pos_ub=-1,                    add_gt_as_proposals=False),                allowed_border=-1,                pos_weight=-1,                debug=False),            rpn_proposal=dict(                nms_pre=4000,                max_per_img=1000,                nms=dict(type='nms', iou_threshold=0.7),                min_bbox_size=0),            rcnn=dict(                assigner=dict(                    type='MaxIoUAssigner',                    pos_iou_thr=0.5,                    neg_iou_thr=0.5,                    min_pos_iou=0.5,                    match_low_quality=False,                    ignore_iof_thr=-1),                sampler=dict(                    type='RandomSampler',                    num=512,                    pos_fraction=0.25,                    neg_pos_ub=-1,                    add_gt_as_proposals=True),                pos_weight=-1,                debug=False)),        dict(            assigner=dict(type='ATSSAssigner', topk=9),            allowed_border=-1,            pos_weight=-1,            debug=False),],    test_cfg=[        dict(max_per_img=100),        dict(            rpn=dict(                nms_pre=1000,                max_per_img=1000,                nms=dict(type='nms', iou_threshold=0.7),                min_bbox_size=0),            rcnn=dict(                score_thr=0.0,                nms=dict(type='nms', iou_threshold=0.5),                max_per_img=100)),        dict(            nms_pre=1000,            min_bbox_size=0,            score_thr=0.0,            nms=dict(type='nms', iou_threshold=0.6),            max_per_img=100),        # soft-nms is also supported for rcnn testing        # e.g., nms=dict(type='soft_nms', iou_threshold=0.5, min_score=0.05)    ])
img_norm_cfg = dict(    mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True)# train_pipeline, NOTE the img_scale and the Pad's size_divisor is different# from the default setting in mmdet.train_pipeline = [    dict(type='LoadImageFromFile'),    dict(type='LoadAnnotations', with_bbox=True),    dict(type='RandomFlip', flip_ratio=0.5),    dict(        type='AutoAugment',        policies=[            [                dict(                    type='Resize',                    img_scale=[(480, 1333), (512, 1333), (544, 1333),                               (576, 1333), (608, 1333), (640, 1333),                               (672, 1333), (704, 1333), (736, 1333),                               (768, 1333), (800, 1333)],                    multiscale_mode='value',                    keep_ratio=True)            ],            [                dict(                    type='Resize',                    # The radio of all image in train dataset < 7                    # follow the original impl                    img_scale=[(400, 4200), (500, 4200), (600, 4200)],                    multiscale_mode='value',                    keep_ratio=True),                dict(                    type='RandomCrop',                    crop_type='absolute_range',                    crop_size=(384, 600),                    allow_negative_crop=True),                dict(                    type='Resize',                    img_scale=[(480, 1333), (512, 1333), (544, 1333),                               (576, 1333), (608, 1333), (640, 1333),                               (672, 1333), (704, 1333), (736, 1333),                               (768, 1333), (800, 1333)],                    multiscale_mode='value',                    override=True,                    keep_ratio=True)            ]        ]),    dict(type='Normalize', **img_norm_cfg),    dict(type='Pad', size_divisor=1),    dict(type='DefaultFormatBundle'),    dict(type='Collect', keys=['img', 'gt_bboxes', 'gt_labels'])]# test_pipeline, NOTE the Pad's size_divisor is different from the default# setting (size_divisor=32). While there is little effect on the performance# whether we use the default setting or use size_divisor=1.test_pipeline = [    dict(type='LoadImageFromFile'),    dict(        type='MultiScaleFlipAug',        img_scale=(1333, 800),        flip=False,        transforms=[            dict(type='Resize', keep_ratio=True),            dict(type='RandomFlip'),            dict(type='Normalize', **img_norm_cfg),            dict(type='Pad', size_divisor=1),            dict(type='ImageToTensor', keys=['img']),            dict(type='Collect', keys=['img'])        ])]
data = dict(    samples_per_gpu=2,    workers_per_gpu=2,    train=dict(filter_empty_gt=False, pipeline=train_pipeline),    val=dict(pipeline=test_pipeline),    test=dict(pipeline=test_pipeline))# optimizeroptimizer = dict(    type='AdamW',    lr=2e-4,    weight_decay=1e-4,    paramwise_cfg=dict(        custom_keys={            'backbone': dict(lr_mult=0.1),            'sampling_offsets': dict(lr_mult=0.1),            'reference_points': dict(lr_mult=0.1)        }))optimizer_config = dict(grad_clip=dict(max_norm=0.1, norm_type=2))# learning policylr_config = dict(policy='step', step=[11])runner = dict(type='EpochBasedRunner', max_epochs=12)
2、主文件projects\models\co_detr.py主要看forward_train的主逻辑
    def forward_train(self,                      img,                      img_metas,                      gt_bboxes,                      gt_labels,                      gt_bboxes_ignore=None,                      gt_masks=None,                      proposals=None,                      **kwargs):        """        Args:            img (Tensor): of shape (N, C, H, W) encoding input images.                Typically these should be mean centered and std scaled.
            img_metas (list[dict]): list of image info dict where each dict                has: 'img_shape', 'scale_factor', 'flip', and may also contain                'filename', 'ori_shape', 'pad_shape', and 'img_norm_cfg'.                For details on the values of these keys see                `mmdet/datasets/pipelines/formatting.py:Collect`.
            gt_bboxes (list[Tensor]): Ground truth bboxes for each image with                shape (num_gts, 4) in [tl_x, tl_y, br_x, br_y] format.
            gt_labels (list[Tensor]): class indices corresponding to each box
            gt_bboxes_ignore (None | list[Tensor]): specify which bounding                boxes can be ignored when computing the loss.
            gt_masks (None | Tensor) : true segmentation masks for each box                used if the architecture supports a segmentation task.
            proposals : override rpn proposals with custom proposals. Use when                `with_rpn` is False.
        Returns:            dict[str, Tensor]: a dictionary of loss components        """        batch_input_shape = tuple(img[0].size()[-2:])        for img_meta in img_metas:            img_meta['batch_input_shape'] = batch_input_shape
        if not self.with_attn_mask: # remove attn mask for LSJ            for i in range(len(img_metas)):                input_img_h, input_img_w = img_metas[i]['batch_input_shape']                img_metas[i]['img_shape'] = [input_img_h, input_img_w, 3]       提取特征        x = self.extract_feat(img, img_metas)
        losses = dict()        def upd_loss(losses, idx, weight=1):            new_losses = dict()            for k,v in losses.items():                new_k = '{}{}'.format(k,idx)                if isinstance(v,list) or isinstance(v,tuple):                    new_losses[new_k] = [i*weight for i in v]                else:new_losses[new_k] = v*weight            return new_losses
        # DETR encoder and decoder forward        本来的detr训练流程        if self.with_query_head:            原本的detr loss 和encoder的输出用于辅助头计算            bbox_losses, x = self.query_head.forward_train(x, img_metas, gt_bboxes,                                                          gt_labels, gt_bboxes_ignore)            losses.update(bbox_losses)            
        # RPN forward and loss        将encoder的输出输入rpn        if self.with_rpn:            proposal_cfg = self.train_cfg[self.head_idx].get('rpn_proposal',                                              self.test_cfg[self.head_idx].rpn)            rpn_losses, proposal_list = self.rpn_head.forward_train(                x,                img_metas,                gt_bboxes,                gt_labels=None,                gt_bboxes_ignore=gt_bboxes_ignore,                proposal_cfg=proposal_cfg,                **kwargs)            losses.update(rpn_losses)        else:            proposal_list = proposals
        positive_coords = []        rpn结果给roi,faster-rcnn的流程        for i in range(len(self.roi_head)):            roi_losses = self.roi_head[i].forward_train(x, img_metas, proposal_list,                                                    gt_bboxes, gt_labels,                                                    gt_bboxes_ignore, gt_masks,                                                    **kwargs)            if self.with_pos_coord:                positive_coords.append(roi_losses.pop('pos_coords'))            else:                 if 'pos_coords' in roi_losses.keys():                    tmp = roi_losses.pop('pos_coords')                 roi_losses = upd_loss(roi_losses, idx=i)            losses.update(roi_losses)        encoder输出给ATSS head            for i in range(len(self.bbox_head)):            bbox_losses = self.bbox_head[i].forward_train(x, img_metas, gt_bboxes,                                                        gt_labels, gt_bboxes_ignore)            if self.with_pos_coord:                pos_coords = bbox_losses.pop('pos_coords')                positive_coords.append(pos_coords)            else:                if 'pos_coords' in bbox_losses.keys():                    tmp = bbox_losses.pop('pos_coords')                      bbox_losses = upd_loss(bbox_losses, idx=i+len(self.roi_head))            losses.update(bbox_losses)        论文的第二个改进点,将辅助头的输出作为query传给decoder        if self.with_pos_coord and len(positive_coords)>0:            for i in range(len(positive_coords)):                使用forward_train_aux对辅助头的query计算损失                bbox_losses = self.query_head.forward_train_aux(x, img_metas, gt_bboxes,                                                            gt_labels, gt_bboxes_ignore, positive_coords[i], i)                bbox_losses = upd_loss(bbox_losses, idx=i)                losses.update(bbox_losses)                                           return losses
3、projects\models\co_deformable_detr_head.py这部分主要看forward_train_aux,没有encoder,计算辅助头的输出作为query的损失
    def forward_train_aux(self,                          x,                          img_metas,                          gt_bboxes,                          gt_labels=None,                          gt_bboxes_ignore=None,                          pos_coords=None,                          head_idx=0,                          **kwargs):        """Forward function for training mode.
        Args:            x (list[Tensor]): Features from backbone.            img_metas (list[dict]): Meta information of each image, e.g.,                image size, scaling factor, etc.            gt_bboxes (Tensor): Ground truth bboxes of the image,                shape (num_gts, 4).            gt_labels (Tensor): Ground truth labels of each box,                shape (num_gts,).            gt_bboxes_ignore (Tensor): Ground truth bboxes to be                ignored, shape (num_ignored_gts, 4).            proposal_cfg (mmcv.Config): Test / postprocessing configuration,                if None, test_cfg would be used.
        Returns:            dict[str, Tensor]: A dictionary of loss components.        """        获取辅助头输入query的gt        aux_targets = self.get_aux_targets(pos_coords, img_metas, x, head_idx)        辅助头输出作为query计算loss        outs = self.forward_aux(x[:-1], img_metas, aux_targets, head_idx)        outs = outs + aux_targets        if gt_labels is None:            loss_inputs = outs + (gt_bboxes, img_metas)        else:            loss_inputs = outs + (gt_bboxes, gt_labels, img_metas)        losses = self.loss_aux(*loss_inputs, gt_bboxes_ignore=gt_bboxes_ignore)        return losses
    def forward_aux(self, mlvl_feats, img_metas, aux_targets, head_idx):        """Forward function.
        Args:            mlvl_feats (tuple[Tensor]): Features from the upstream                network, each is a 4D-tensor with shape                (N, C, H, W).            img_metas (list[dict]): List of image information.
        Returns:            all_cls_scores (Tensor): Outputs from the classification head, \                shape [nb_dec, bs, num_query, cls_out_channels]. Note \                cls_out_channels should includes background.            all_bbox_preds (Tensor): Sigmoid outputs from the regression \                head with normalized coordinate format (cx, cy, w, h). \                Shape [nb_dec, bs, num_query, 4].            enc_outputs_class (Tensor): The score of each point on encode \                feature map, has shape (N, h*w, num_class). Only when \                as_two_stage is True it would be returned, otherwise \                `None` would be returned.            enc_outputs_coord (Tensor): The proposal generate from the \                encode feature map, has shape (N, h*w, 4). Only when \                as_two_stage is True it would be returned, otherwise \                `None` would be returned.        """        aux_coords, aux_labels, aux_targets, aux_label_weights, aux_bbox_weights, aux_feats, attn_masks = aux_targets        batch_size = mlvl_feats[0].size(0)        input_img_h, input_img_w = img_metas[0]['batch_input_shape']        img_masks = mlvl_feats[0].new_ones(            (batch_size, input_img_h, input_img_w))        for img_id in range(batch_size):            img_h, img_w, _ = img_metas[img_id]['img_shape']            img_masks[img_id, :img_h, :img_w] = 0
        mlvl_masks = []        mlvl_positional_encodings = []        for feat in mlvl_feats:            mlvl_masks.append(                F.interpolate(img_masks[None],                              size=feat.shape[-2:]).to(torch.bool).squeeze(0))            mlvl_positional_encodings.append(                self.positional_encoding(mlvl_masks[-1]))
        query_embeds = None        调用transformer.forward_aux,没有encoder的transformer,        其余和原始transformer一致        hs, init_reference, inter_references = self.transformer.forward_aux(                    mlvl_feats,                    mlvl_masks,                    query_embeds,                    mlvl_positional_encodings,                    aux_coords,                    pos_feats=aux_feats,                    reg_branches=self.reg_branches if self.with_box_refine else None,  # noqa:E501                    cls_branches=self.cls_branches if self.as_two_stage else None,  # noqa:E501                    return_encoder_output=True,                    attn_masks=attn_masks,                    head_idx=head_idx            )
        hs = hs.permute(0, 2, 1, 3)        outputs_classes = []        outputs_coords = []        输出结果修正,和detr一致        for lvl in range(hs.shape[0]):            if lvl == 0:                reference = init_reference            else:                reference = inter_references[lvl - 1]            reference = inverse_sigmoid(reference)            outputs_class = self.cls_branches[lvl](hs[lvl])            tmp = self.reg_branches[lvl](hs[lvl])            if reference.shape[-1] == 4:                tmp += reference            else:                assert reference.shape[-1] == 2                tmp[..., :2] += reference            outputs_coord = tmp.sigmoid()            outputs_classes.append(outputs_class)            outputs_coords.append(outputs_coord)
        outputs_classes = torch.stack(outputs_classes)        outputs_coords = torch.stack(outputs_coords)
        return outputs_classes, outputs_coords, \                None, None

1. 介绍

        在本文中,我们试图使基于detr的检测器优于传统检测器,同时保持其端到端优点。为了解决这一挑战,我们将重点放在一对一集匹配的直观缺点上,即它探索的是不太积极的查询。这将导致严重的训练效率低下问题。我们从编码器产生的潜在表征和解码器的注意学习两个方面详细分析了这一点。我们首先比较了deform - detr和一对多标签分配方法之间潜在特征的可判别性得分,其中我们简单地将解码器替换为ATSS头部。利用每个空间坐标上的特征12范数表示可判别性得分。给定编码器的输出F∈RC×H×W,我们可以得到可判别性评分图S∈R1×H×W。目标在相应区域的得分越高,越容易被检测到。如图2所示,我们通过对判别性分数应用不同的阈值来演示IoF-IoB曲线(IoF:前景相交,IoB:背景相交)(详情见第3.4节)。ATSS的IoF-IoB曲线越高,说明它更容易区分前景和背景。

        我们在图3中进一步可视化可判别性评分图S。很明显,一对多标签分配方法充分激活了一些显著区域的特征,但在一对一集匹配中却没有得到充分的探索。对于解码器训练的探索,我们还展示了基于deform - detr和Group-DETR的解码器中交叉注意分数的IoF-IoB曲线,该曲线在解码器中引入了更多的正向查询。图2中的插图显示,太少的积极查询也会影响注意学习,而在解码器中增加更多的积极查询可以稍微缓解这种情况。 

        这一重要的观察结果促使我们提出一种简单而有效的方法——协同混合任务训练方案(Co-DETR)。CoDETR的关键思想是使用通用的一对多标签分配来提高编码器和解码器的训练效率和有效性。更具体地说,我们将辅助头与变压器编码器的输出集成在一起。这些头部可以通过多用途的一对多标签分配来监督,如ATSS、FCOS和Faster RCNN。不同的标签分配丰富了对编码器输出的监督,这迫使它具有足够的判别性,以支持这些头部的训练收敛。为了进一步提高解码器的训练效率,我们在这些辅助头部中对正样本的坐标进行了精心编码,包括正锚点和正提议。它们作为多组正查询发送到原始解码器,以预测预先分配的类别和边界框。每个辅助头部的正坐标作为一个独立的组,与其他组隔离。多用途的一对多标签分配可以引入大量(正查询、真值)对来提高解码器的训练效率。注意,在推理过程中只使用原始解码器,因此所提出的训练方案只在训练过程中引入额外的开销。

        我们进行了大量的实验来评估所提出方法的效率和有效性。如图3所示,Co-DETR极大地缓解了一对一集合匹配中编码器的特征学习问题。作为即插即用的方法,我们很容易将其与不同的DETR变体组合,包括DAB-DETR , DeformableDETR和DINO-Deformable-DETR。如图1所示,Co-DETR实现了更快的训练收敛,甚至更高的性能。具体来说,我们将基本的deform - detr在12 epoch训练中提高了5.8% AP,在36 epoch训练中提高了3.2% AP。使用Swin-L的最先进的DINO-Deformable-DETR在COCO val上的AP仍然可以从58.5%提高到59.5%。令人惊讶的是,结合vit -L主干,我们在COCO测试开发上实现了66.0%的AP,在LVIS val上实现了67.9%的AP,以更小的模型尺寸建立了新的最先进的检测器。

 

2. 相关工作

        一对多标签分配 对于目标检测中的一对多标签分配,可以在训练阶段将多个候选框作为正样本分配到同一个ground-truth box中。在经典的基于锚点的检测器中,如Faster-RCNN和RetinaNet,样本选择由预定义的IoU阈值和锚点与标注框之间的匹配IoU来指导。无锚点FCOS利用中心先验,将每个边界框中心附近的空间位置赋值为正。在一对多标签分配中引入自适应机制,克服了固定标签分配的局限性。ATSS通过最接近的k个锚的统计动态IoU值进行自适应锚选择。PAA以概率方式自适应地将锚点分为阳性和阴性样本。在本文中,我们提出了一种协作混合分配方案,通过一对多标签分配的辅助头来改进编码器表示。

        一对一的集合匹配 开创性的基于变压器的检测器DETR将一对一集合匹配方案融入到目标检测中,实现完全的端到端目标检测。一对一集合匹配策略首先通过匈牙利匹配计算全局匹配成本,并为每个真值盒只分配一个匹配成本最小的正样本。DNDETR证明了由于一对一集合匹配的不稳定性导致的缓慢收敛,因此引入去噪训练来消除这一问题。DINO继承了DAB-DETR的高级查询公式,并结合了改进的对比去噪技术,以实现最先进的性能。Group-DETR构造组明智的一对多标签分配,利用多个正对象查询,类似于H-DETR中的混合匹配方案。在上述后续工作的基础上,本文提出了一对一集合匹配协同优化的新视角。

3.方法

3.1. 概述

        根据标准的DETR协议,输入图像被送入主干和编码器以生成潜在特征。多个预定义对象查询在解码器中通过交叉注意与它们交互。我们引入Co-DETR,通过协同混合任务训练方案和自定义正查询生成来改善编码器的特征学习和解码器的注意学习。我们将详细描述这些模块,并给出它们为什么可以很好地工作的见解。

3.2. 协作式混合分配训练

        为了减轻由于解码器中较少的正查询而导致的编码器输出的稀疏监督,我们结合了具有不同多标签分配范例的多功能辅助头,例如ATSS和Faster R-CNN。不同的标签分配丰富了对编码器输出的监督,这迫使它具有足够的判别性,以支持这些头部的训练收敛。具体来说,给定编码器的潜在特征F,我们首先通过多尺度适配器将其转换为特征金字塔{F1,···,FJ},其中J表示下采样步幅为22+J的特征映射。与ViTDet类似,特征金字塔是由单尺度编码器中的单个特征映射构建的,而我们使用双线性插值和3 × 3卷积进行上采样。例如,对于来自编码器的单尺度特征,我们依次应用下采样(3×3与stride 2卷积)或上采样操作来产生特征金字塔。对于多尺度编码器,我们只对多尺度编码器特征F中最粗的特征进行下采样,构建特征金字塔。定义了K个协作头,并以相应的标签分配方式Ak,对于第i个协作头,将{F1,···,FJ}发送给它,以获得预测值^Pi在第i个头部,Ai用于计算Pi中正样本和负样本的监督目标。记为G为基真集,此过程可表示为:

其中{pos}和{neg}表示由Ai确定的(j, Fj中的正坐标或负坐标)的对集。j表示{F1,···,FJ}中的特征索引。B{pos}是空间正坐标的集合。P{pos} i和P{neg} i是相应坐标下的监督目标,包括类别和回归偏移量。具体地说,我们在表1中描述了每个变量的详细信息。

损失函数可定义为:

 

 注意,对于负样本,回归损失被丢弃。K个辅助头部优化的训练目标为:

3.3. 自定义正查询生成

        在一对一集匹配范式中,每个基础真值框将只被分配给一个特定的查询作为监督目标。正面查询太少会导致转换器解码器中的交叉注意学习效率低下,如图2所示。为了缓解这种情况,我们根据每个辅助头中的标签分配Ai精心生成足够的自定义正查询。具体来说,给定第i个辅助头部的正坐标集B{pos} i∈RMi×4,其中Mi为正样本个数,则额外定制的正查询Qi∈RMi×C可以通过以下方式生成:

 

其中PE(·)表示位置编码,根据索引对(j, Fj中的正坐标或负坐标)从E(·)中选择相应的特征。

因此,在训练期间,有K + 1组查询有助于单个一对一集匹配分支和K个具有一对多标签分配的分支。辅助的一对多标签分配分支与原始主分支中的L个解码器层共享相同的参数。辅助分支中的所有查询都被视为正查询,因此放弃匹配过程。具体来说,是第1解码器层的损失在第i辅助分支中可表示为:

Pi,l为第i辅助支路第l解码器层的输出预测值。最后,Co-DETR的训练目标为:

 其中,~Ldecl表示原一对一集匹配分支中的损失,λ1和λ2为平衡损失的系数。

3.4. 为什么Co-DETR有效

Co-DETR使基于detr的检测器有了明显的改进。接下来,我们尝试定性和定量地考察其有效性。我们基于Deformable-DETR与ResNet50主干使用36epoch设置进行了详细的分析。

丰富编码器的监督 直观地说,太少的正查询会导致稀疏的监督,因为对于每个基真只有一个查询是由回归损失监督的。一对多标签分配方式下的正样本得到更多的定位监督,有助于增强潜在特征学习。为了进一步探索稀疏监督如何阻碍模型训练,我们详细研究了编码器产生的潜在特征。我们引入了IoF-IoB曲线来量化编码器输出的可判别性分数。具体来说,给定编码器的潜在特征F,受图3中特征可视化的启发,我们计算IoF(前景相交)和IoB(背景相交)。给定j层编码器的特征Fj∈RC×Hj×Wj,我们首先计算12范数bFj∈R1×Hj×Wj,并将其大小调整为图像大小H ×W。判别性评分D(F)由各等级得分平均计算得出:

其中省略了调整大小操作。我们在图3中可视化了ATSS、deform - detr和co - deform - detr的判别分数。与Deformable-DETR相比,ATSS和Co-Deformable-DETR都具有更强的关键目标区域识别能力,而Deformable-DETR几乎不受背景干扰。因此,我们将前景和背景的指标分别定义为1(D(F) > S)∈RH×W和1(D(F) < S)∈RH×W。S是预定义的分数阈值,如果x为真,则1(x)为1,否则为0。对于前景Mf g∈RH×W的掩码,如果(h, w)点在前景内,则元素Mf g h为1,否则为0。前景交点面积(IoF)如果g可以计算为:

 

具体来说,我们以类似的方式计算背景区域的交点面积(IoB),并在图2中通过改变S绘制出IoF和IoB曲线。显然,在相同的IoB值下,ATSS和co - deform - detr比deform - detr和Group-DETR获得更高的IoF值,这表明编码器表示从一对多标签分配中受益。

通过减少匈牙利语匹配的不稳定性来提高交叉注意学习 匈牙利匹配是一对一集合匹配的核心方案。交叉注意是帮助正查询编码丰富对象信息的重要操作。要做到这一点,需要充分的训练。我们观察到匈牙利匹配引入了不可控的不稳定性,因为在训练过程中,分配给同一图像中特定正查询的真值是变化的。接下来,我们在图5中给出了不稳定性的比较,我们发现我们的方法有助于更稳定的匹配过程。此外,为了量化交叉注意力的优化程度,我们还计算了注意力得分的IoF-IoB曲线。与特征可判别性分数计算类似,我们对注意力分数设置不同的阈值,得到多个IoF-IoB对。Deformable-DETR、Group-DETR和CoDeformable-DETR之间的比较如图2所示。我们发现,具有更多正查询的detr的IoF-IoB曲线一般都在deform - detr之上,这与我们的动机是一致的。

3.5. 与其他方法比较

我们的方法与其他同行的差异 Group-DETR、H-DETR和SQR[2]通过与重复组和重复的ground-truth box进行一对一匹配来实现一对多赋值。Co-DETR明确地为每个基础真值分配多个空间坐标作为正数。因此,将这些密集的监督信号直接应用到潜在特征映射中,使其具有更强的判别能力。相比之下,Group-DETR、HDETR和SQR缺乏这种机制。虽然在这些对应物中引入了更多的正查询,但匈牙利匹配实现的一对多分配仍然存在一对一匹配的不稳定性问题。我们的方法受益于现成的一对多分配的稳定性,并继承了它们在正查询和真值框之间的特定匹配方式。Group-DETR和H-DETR无法揭示一对一匹配与传统一对多分配之间的互补性。据我们所知,我们是第一个用传统的一对多分配和一对一匹配对检测器进行定量和定性分析的人。这有助于我们更好地理解它们的差异和互补性,这样我们就可以通过利用现成的一对多任务设计来自然地提高DETR的学习能力,而不需要额外的专门的一对多设计经验。

在解码器中不引入否定查询 重复的对象查询不可避免地会给解码器带来大量的负面查询,并显著增加GPU内存。但是,我们的方法只处理解码器中的正坐标,因此消耗的内存较少,如表7所示。

4. 实验

4.1. 设置

数据集和评估指标 我们的实验在MS COCO 2017数据集和LVIS v1.0数据集上进行。COCO数据集由115K用于训练的标记图像和5K用于验证的图像组成。默认情况下,我们在val子集上报告检测结果。我们最大的模型的结果在test-dev (20K映像)。LVIS v1.0是一个大规模的长尾数据集,有1203个类别,用于大词汇实例分割。为了验证Co-DETR的可扩展性,我们进一步将其应用于大规模对象检测基准,即Objects365。在Objects365数据集中,有1.7M的标记图像用于训练,80K的图像用于验证。在IoU阈值范围为0.5 ~ 0.95的不同对象尺度下,所有结果均符合标准平均平均精度(AP)。

实现细节 我们将我们的Co-DETR合并到当前的类似detr的管道中,并保持训练设置与基线一致。K = 2时,我们采用ATSS和Faster-RCNN作为辅助头,K = 1时只保留ATSS。关于我们的辅助头的更多细节可以在补充材料中找到。我们选择可学习对象查询的数量为300,默认设置{λ1, λ2}为{1.0,2.0}。对于Co-DINODeformable-DETR++,我们使用了带有复制粘贴的大规模抖动。

4.2. 主要结果

在本节中,我们在表2和表3中实证分析了Co-DETR对不同DETR变量的有效性和泛化能力。所有结果均使用mmdetection重现。我们首先将协作混合任务训练应用于具有C5特征的单尺度detr。令人惊讶的是,条件- detr和DAB-DETR在较长的训练计划下比基线分别获得2.4%和2.3%的AP增益。对于具有多尺度特征的DeformableDETR,检测性能从37.1%显著提高到42.9%。当训练时间增加到36次时,整体的改进(+3.2% AP)仍然有效。此外,我们在[16]之后对改进的Deformable-DETR(记为Deformable-DETR++)进行了实验,观察到+2.4%的AP增益。采用本方法的最先进的DINO-Deformable DETR可达到51.2%的AP,比竞争基准高+1.8%。

基于两个最先进的基线,我们进一步将骨干容量从ResNet50扩展到swin - l。如表3所示,Co-DETR达到56.9% AP,并大大超过了deformation - detr ++基线(+1.7% AP)。使用swing - l的DINODeformable-DETR的性能仍然可以从58.5%的AP提高到59.5% AP 

 

4.3. 与最先进技术的比较

我们将K = 2的方法应用于deformabledetr++和DINO。此外,我们的Co-DINO-Deformable-DETR采用了质量焦损和NMS。我们在表4中报告了COCO值的比较。与其他竞争对手相比,我们的方法收敛速度快得多。例如,Co-DINO-DeformableDETR在使用ResNet-50骨干网时仅使用12次epoch即可轻松实现52.1%的AP。我们使用SwinL的方法可以在1x调度器上获得58.9%的AP,甚至超过了其他最先进的3x调度器框架。更重要的是,我们的最佳模型Co-DINO-DeformableDETR++在36 epoch训练下,使用ResNet-50实现了54.8%的AP,使用swin- l实现了60.7%的AP,明显优于所有具有相同主干的现有检测器。

 为了进一步探索我们方法的可扩展性,我们将骨干容量扩展到3.04亿个参数。这种大规模主干ViT-L使用自监督学习方法(EV a -02)进行预训练。我们首先在Objects365上使用vitl预训练Co-DINO-Deformable-DETR 26个epoch,然后在COCO数据集上进行12个epoch的微调。在微调阶段,输入分辨率在480×2400和1536×2400之间随机选择。详细设置请参见补充资料。我们的结果是用测试时间增量来评估的。表5给出了最新的比较COCO测试开发基准。在更小的模型尺寸(304M个参数)下,Co-DETR在COCO测试开发上创造了66.0% AP的新记录,比之前的最佳模型InternImage-G高出+0.5% AP。

我们还证明了Co-DETR在长尾lvis检测数据集上的最佳效果。特别地,我们在COCO上使用相同的Co-DINO-Deformable-DETR++模型,但选择FedLoss作为分类损失,以弥补数据分布不平衡的影响。在这里,我们只应用边界框监督,并报告目标检测结果。表6给出了比较结果。Co-DETR Swin-L收益率56.9%和62.3% AP LVIS val和minival超过ViTDet与MAE-pretrained ViT-H和GLIPv2的方法骨干AP分别上涨3.5%和2.5%。我们进一步在此数据集上微调Objects365预训练的Co-DETR。在不增加测试时间的情况下,我们的方法在LVIS val和minival上分别达到67.9%和71.9%的最佳检测性能。与具有测试时间增强的30亿个参数的InternImage-G相比,我们在将模型大小减小到1/10的同时,在LVIS val和minival上获得了+4.7%和+6.1%的AP增益。

 

4.4. 消融研究

除非另有说明,所有消融实验都是在带ResNet-50骨干网的deform - detr上进行的。默认情况下,我们选择辅助头的数量K到1,并将总批大小设置为32。更多的消融和分析可以在补充材料中找到。

辅助头的选择标准 我们在表7和表8中进一步研究了选择辅助头像的标准。表8中的结果显示,任何具有一对多标签分配的辅助头部都可以持续提高基线,并且ATSS可以达到最佳性能。我们发现,当选择K小于3时,随着K的增加,准确率继续提高。值得注意的是,当K = 6时,性能会出现下降,我们推测这是由于辅助头之间的严重冲突造成的。如果辅助头部之间的特征学习不一致,则随着K变大的持续改进将被破坏。并在后续和补充资料中分析了多头的优化一致性。综上所述,我们可以选择任意头部作为辅助头部,为了在K≤2时达到最佳性能,我们通常选择ATSS和Faster-RCNN作为辅助头部。我们不会使用太多不同的正面,例如6个不同的正面,以避免优化冲突。

 冲突分析 当将相同的空间坐标分配给不同的前景框或在不同的辅助头部中作为背景时,会产生冲突,从而混淆检测器的训练。我们首先定义头部Hi与头部Hj之间的距离,以及Hi的平均距离来衡量优化冲突为:

其中KL, D, I, C分别为KL散度、数据集、输入图像和类激活图(CAM)。如图6所示,当K > 1时,我们计算辅助头之间的平均距离,当K = 1时,计算DETR磁头与单个辅助头之间的距离。我们发现,当K = 1时,每个辅助头的距离度量是不显著的,这一观察结果与我们在表8中的结果一致:当K = 1时,DETR头可以与任何头协同改进。当K增加到2时,距离指标略有增加,我们的方法达到了最佳性能,如表7所示。当K从3到6增加时,距离激增,说明这些辅助头之间存在严重的优化冲突导致性能下降。然而,6个不同头部的ATSS基线达到49.5% AP,用6个不同头部替代ATSS可降低到48.9% AP。因此,我们推测过多的不同的辅助头像,例如超过3个不同的头像,会加剧冲突。综上所述,优化冲突受各种辅助头的数量以及这些头之间的关系的影响。

 

增加的正面应该是不同的吗?在我们的分析中,两个ATSS头(49.2% AP)的协同训练仍然改善了一个ATSS头(48.7% AP)的模型,因为ATSS是DETR头的补充。此外,还引入了多种互补的辅助头而不是与原头相同,如Faster-RCNN,可以带来更好的增益(49.5% AP)。请注意,这与上述结论并不矛盾;相反,由于冲突不显著,我们可以在较少的不同头像(K≤2)下获得最佳性能,但当使用许多不同头像(K > 3)时,我们面临严重的冲突。

每个成分的作用 我们执行一个组件消融来彻底分析表9中每个组件的影响。由于密集的空间监督使编码器特征更具判别性,因此合并辅助头产生显著的增益。或者,引入自定义的正查询对最终结果也有显著的贡献,同时提高了一对一集匹配的训练效率。这两种技术都可以加速收敛并提高性能。总之,我们观察到整体的改进源于编码器更多的判别特征和解码器更有效的注意学习。

与较长的训练计划相比 如表10所示,我们发现随着性能饱和,长时间的训练不能使deform - detr受益。相反,Co-DETR大大加快了收敛速度,提高了峰值性能。

辅助支路性能 令人惊讶的是,我们观察到Co-DETR也为表11中的辅助头带来了一致的收益。这意味着我们的训练范式有助于更多的判别编码器表示,这提高了解码器和辅助头的性能。

 

原始和自定义正向查询的分布差异 我们在图7a中可视化了原始正查询和自定义正查询的位置。每张图像只显示一个对象(绿色框)。匈牙利匹配在解码器中分配的正查询用红色标记。我们分别用蓝色和橙色标记从Faster-RCNN和A TSS中提取的阳性查询。这些自定义查询分布在实例的中心区域,为检测器提供足够的监督信号。

 

分布差异会导致不稳定吗?我们在图7b中计算原始查询和定制查询之间的平均距离。原始否定查询和自定义肯定查询之间的平均距离明显大于原始和自定义肯定查询之间的距离。由于原始查询和自定义查询之间的分布差距很小,因此在训练期间不会遇到不稳定性。

5. 结论

在本文中,我们提出了一种新的协同混合任务训练方案,即Co-DETR,以从多种标签分配方式中学习更高效的基于detr的检测器。该训练方案通过对一对多标签分配监督下的多个并行辅助头部进行训练,可以很容易地提高编码器在端到端检测器中的学习能力。此外,我们通过从这些辅助头部提取正坐标来进行额外的定制正查询,以提高解码器中正样本的训练效率。在COCO数据集上的大量实验证明了CoDETR的效率和有效性。令人惊讶的是,结合ViT-L主干,我们在COCO测试开发上实现了66.0%的AP,在lvis val上实现了67.9%的AP,以更小的模型尺寸建立了新的最先进的检测器。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值