论文解读2:Deformable DETR论文阅读和源码分析

论文题目:Deformable detr: Deformable transformers for end-to-end object detection 

                  2020 ICLR

论文链接:arxiv.org/pdf/2010.04159

源码链接:github链接


前言:

Deformable DETR主要解决了不能输入多个骨干网络提取的特征图的问题,它是通过修改transformer的attention的结构实现的,

它的这个结构使得训练轮次缩小10倍以上,原始的DETR的训练轮次需要500个才能达到和Faster-RCNN类似的效果。

同时在小目标的检测结果更好。

为什么?应该是因为文章提出的可变形注意力机制的结构,它利用了可变形卷积的类似结构,可以学习到稀疏的有意义的位置。同时,这个结构使得模型可以输入更大的分辨率的特征图,而特征图分辨率越大,小目标检测的效果就是更好的。


优势(与原始DETR相比):

原始的DETR缺点:

1.收敛慢

2.能够处理的特征分辨率有限

i). Transformer在初始化时,分配给所有特征像素的注意力权重几乎是均等的,这就造成了模型需要长时间去学习关注真正有意义的位置,这些位置应该是稀疏的

ii). Transformer在计算注意力权重时,伴随着高计算量与空间复杂度。特别是在编码器部分,与特征像素点的数量成平方级关系,因此难以处理高分辨率的特征(这点也是DETR检测小目标效果差的原因)

那deformable detr可以:

1.加快收敛速度

2.可以接受多个特征图的输入到encoder中,处理更大的特征图

同时,本文使用的多尺寸的特征图并不需要FPN结构。


改进内容总结为下面两点:

  • Deformable attention机制
  • 两种训练提升的策略

Deformable detr模型:

结构和DETR类似,分为四个组件:

1.backbone的特征提取阶段

2.transformer的encoder

3.transformer的decoder

4.预测层FFN阶段

1.backbone的特征提取阶段

首先不管后面是如何使用输出的多层特征层,先关注使用的特征层是如何处理的。

输入:图像

输出:resnet50的后三层特征层(C3,C4,C5)+位置编码

注意,由于输出的是多层的特征层,位置编码也是多层的

首先是在backbone中如何返回多层特征层(src+mask)

class BackboneBase(nn.Module):

    def __init__(self, backbone: nn.Module, train_backbone: bool, return_interm_layers: bool):
        super().__init__()
        for name, parameter in backbone.named_parameters():
            if not train_backbone or 'layer2' not in name and 'layer3' not in name and 'layer4' not in name:
                parameter.requires_grad_(False)
        if return_interm_layers:
            # return_layers = {"layer1": "0", "layer2": "1", "layer3": "2", "layer4": "3"}
            return_layers = {"layer2": "0", "layer3": "1", "layer4": "2"}
            self.strides = [8, 16, 32]
            self.num_channels = [512, 1024, 2048]
        else:
            return_layers = {'layer4': "0"}
            self.strides = [32]
            self.num_channels = [2048]
        self.body = IntermediateLayerGetter(backbone, return_layers=return_layers)

    def forward(self, tensor_list: NestedTensor):
        # 输入特征图  [bs, C, H, W]  ->  返回ResNet50中 layer2 layer3 layer4层的输出特征图
        # 0 = [bs, 512, H/8, W/8]  1 = [bs, 1024, H/16, W/16]  2 = [bs, 2048, H/32, W/32]
        xs = self.body(tensor_list.tensors)
        out: Dict[str, NestedTensor] = {}
        for name, x in xs.items():
            m = tensor_list.mask
            assert m is not None
            # 原图片mask下采样8、16、32倍
            mask = F.interpolate(m[None].float(), size=x.shape[-2:]).to(torch.bool)[0]
            out[name] = NestedTensor(x, mask)
        # 3个不同尺度的输出特征和mask  dict: 3
        # 0: tensors[bs, 512, H/8, W/8]  mask[bs, H/8, W/8]
        # 1: tensors[bs, 1024, H/16, W/16]  mask[bs, H/16, W/16]
        # 3: tensors[bs, 2048, H/32, W/32]  mask[bs, H/32, W/32]
        return out

接着,在deformable detr的forward函数中回处理来自backbone阶段的特征

features, pos = self.backbone(samples) 这个backbone返回的是(src+mask,pos)

pos是位置编码

class DeformableDETR(nn.Module):
    """ This is the Deformable DETR module that performs object detection """
    def __init__(self, backbone, transformer, num_classes, num_queries, num_feature_levels,
                 aux_loss=True, with_box_refine=False, two_stage=False):
        ...
        # 3个1x1conv + 1个3x3conv
        if num_feature_levels > 1:
            num_backbone_outs = len(backbone.strides)
            input_proj_list = []
            for _ in range(num_backbone_outs):            # 3个1x1conv
                in_channels = backbone.num_channels[_]    # 512  1024  2048
                input_proj_list.append(nn.Sequential(     # conv1x1  -> 256 channel
                    nn.Conv2d(in_channels, hidden_dim, kernel_size=1),
                    nn.GroupNorm(32, hidden_dim),
                ))
            for _ in range(num_feature_levels - num_backbone_outs):   # 1个3x3conv
                input_proj_list.append(nn.Sequential(
                    nn.Conv2d(in_channels, hidden_dim, kernel_size=3, stride=2, padding=1),  # 3x3conv s=2 -> 256channel
                    nn.GroupNorm(32, hidden_dim),
                ))
                in_channels = hidden_dim
            self.input_proj = nn.ModuleList(input_proj_list)
        else:
            self.input_proj = nn.ModuleList([
                nn.Sequential(
                    nn.Conv2d(backbone.num_channels[0], hidden_dim, kernel_size=1),
                    nn.GroupNorm(32, hidden_dim),
                )])
    	...
    def forward(self, samples: NestedTensor):
        ...
        # 经过backbone resnet50  输出三个尺度的特征信息  features list:3  NestedTensor
        # 0 = mask[bs, W/8, H/8]     tensors[bs, 512, W/8, H/8]
        # 1 = mask[bs, W/16, H/16]   tensors[bs, 1024, W/16, H/16]
        # 2 = mask[bs, W/32, H/32]   tensors[bs, 2048, W/32, H/32]
        # pos: 3个不同尺度的特征对应的3个位置编码(这里一步到位直接生成经过1x1conv降维后的位置编码)
        # 0: [bs, 256, H/8, W/8]  1: [bs, 256, H/16, W/16]  2: [bs, 256, H/32, W/32]
    	features, pos = self.backbone(samples)
    	
    	# 前三个1x1conv + GroupNorm 前向传播
        srcs = []
        masks = []
        for l, feat in enumerate(features):
            src, mask = feat.decompose()
            srcs.append(self.input_proj[l](src))  # 1x1 降维度 -> 256
            masks.append(mask)  # mask shape不变
            assert mask is not None

        # 最后一层特征 -> conv3x3 + GroupNorm 前向传播
        if self.num_feature_levels > len(srcs):
            _len_srcs = len(srcs)
            for l in range(_len_srcs, self.num_feature_levels):
                if l == _len_srcs:
                    # C5层输出 bs x 2048 x H/32 x W/32 x  -> bs x 256 x H/64 x W/64     3x3Conv s=2
                    src = self.input_proj[l](features[-1].tensors)
                else:
                    src = self.input_proj[l](srcs[-1])
                m = samples.mask
                # 这一层的特征图shape变为原来一半   mask shape也要变为原来一半  [bs, H/32, H/32] -> [bs, H/64, W/64]
                mask = F.interpolate(m[None].float(), size=src.shape[-2:]).to(torch.bool)[0]
                # 生成这一层的位置编码  [bs, 256, H/64, W/64]
                pos_l = self.backbone[1](NestedTensor(src, mask)).to(src.dtype)
                srcs.append(src)
                masks.append(mask)
                pos.append(pos_l)

        # 到了这一步就完成了全部的backbone的前向传播了  最终生成4个不同尺度的特征srcs已经对应的mask和位置编码pos
        # srcs:  list4  0=[bs,256,H/8,W/8] 1=[bs,256,H/16,W/16] 2=[bs,256,H/32,W/32] 3=[bs,256,H/64,W/64]
        # masks: list4  0=[bs,H/8,W/8] 1=[bs,H/16,W/16] 2=[bs,H/32,W/32] 3=[bs,H/64,W/64]
        # pos:   list4  0=[bs,256,H/8,W/8] 1=[bs,256,H/16,W/16] 2=[bs,256,H/32,W/32] 3=[bs,256,H/64,W/64]

最后,说明一下位置编码的部分。

对于原始的detr来说,只有一层特征层,使用三角函数的位置的位置编码可以让不同(x,y)坐标的特征的位置编码都不同。但是,由于deformable detr的attention机制可以接受多个特征层的输入,那么就有必要区分不同特征层。原来的位置编码方式会使得不同特征层的相同(x,y)坐标的位置编码是相同的,因此,作者提出了一个’scale-level embedding’的变量,可以用来解决这个问题:用来区分不同特征层的相同位置。

源码在deformable_transformer.py的DeformableTransformer中定义了一个level_embed变量,然后在每一层的原始位置编码(pos_embed)的基础上加上对应的Scale-Level Embedding(level_embed )

注意,每一特征层所有位置加上相同的level_embed 但是 不同层的level_embed不同。

class DeformableTransformer(nn.Module):
    def __init__(self, d_model=256, nhead=8,
                 num_encoder_layers=6, num_decoder_layers=6, dim_feedforward=1024, dropout=0.1,
                 activation="relu", return_intermediate_dec=False,
                 num_feature_levels=4, dec_n_points=4,  enc_n_points=4,
                 two_stage=False, two_stage_num_proposals=300):
        super().__init__()
        ...
        # scale-level position embedding  [4, 256]  可学习的
        # 因为deformable detr用到了多尺度特征  经过backbone会生成4个不同尺度的特征图  但是如果还是使用原先的sine position embedding
        # detr是针对h和w进行编码的 不同位置的特征点会对应不同的编码值 但是deformable detr不同的特征图的不同位置就有可能会产生相同的位置编码,就无法区分了
        # 为了解决这个问题,这里引入level_embed这个遍历  不同层的特征图会有不同的level_embed 再让原先的每层位置编码+每层的level_embed
        # 这样就很好的区分不同层的位置编码了  而且这个level_embed是可学习的
        self.level_embed = nn.Parameter(torch.Tensor(num_feature_levels, d_model))
        ...
	def forward(self, srcs, masks, pos_embeds, query_embed=None):
		...
		for lvl, (src, mask, pos_embed) in enumerate(zip(srcs, masks, pos_embeds)):
			# pos_embed: detr的位置编码 仅仅可以区分h,w的位置 因此对应不同的特征图有相同的h、w位置的话,是无法区分的
            pos_embed = pos_embed.flatten(2).transpose(1, 2)  # [bs,c,h,w] -> [bs,hxw,c]
            # scale-level position embedding  [bs,hxw,c] + [1,1,c] -> [bs,hxw,c]
            # 每一层所有位置加上相同的level_embed 且 不同层的level_embed不同
            # 所以这里pos_embed + level_embed,这样即使不同层特征有相同的w和h,那么也会产生不同的lvl_pos_embed  这样就可以区分了
            lvl_pos_embed = pos_embed + self.level_embed[lvl].view(1, 1, -1)


2.transformer的encoder

# 将调整过形状的源数据、掩码和位置嵌入拼接在一起,形成单个输入
# 将所有特征层拼在一起, bs all hw 256,在维度1上扩展
src_flatten = torch.cat(src_flatten, 1)
mask_flatten = torch.cat(mask_flatten, 1)
lvl_pos_embed_flatten = torch.cat(lvl_pos_embed_flatten, 1)

# 计算每个空间形状的累积总和,用于确定每个级别的开始索引
spatial_shapes = torch.as_tensor(spatial_shapes, dtype=torch.long, device=src_flatten.device)
level_start_index = torch.cat((spatial_shapes.new_zeros((1, )), spatial_shapes.prod(1).cumsum(0)[:-1]))
# 有效高宽占总batch的比例 计算每个掩码的有效比率,用于处理不同大小的输入
valid_ratios = torch.stack([self.get_valid_ratio(m) for m in masks], 1)
# encoder
memory = self.encoder(src_flatten, spatial_shapes, level_start_index, valid_ratios, lvl_pos_embed_flatten, mask_flatten)
# encoder
memory = self.encoder(src_flatten, spatial_shapes, level_start_index, valid_ratios, lvl_pos_embed_flatten, mask_flatten)

输入:src_flatten, spatial_shapes, level_start_index, valid_ratios, lvl_pos_embed_flatten, mask_flatten

输出:memory 

输入输出是一样的

输入多尺度特征层:[bs, H/8 * W/8 + H/16 * W/16 + H/32 * W/32 + H/64 * W/64, 256];

每个encoder layer都会不断学习特征层中每个位置和4个采样点的相关性,最终输出的特征是增强后的特征图:[bs, H/8 * W/8 + H/16 * W/16 + H/32 * W/32 + H/64 * W/64, 256];

对于原始的detr,多了spatial_shapes, level_start_index, valid_ratios部分,lvl_pos_embed_flatten部分是pos_embed加上区分不同层的位置level_embed变量,本质和原始DETR的pos_embed没有什么不同

为什么需要这些新加入的部分?因为本文提出的可变形注意力机制的计算。

可变形注意力机制的计算如上图,为了更好的理解,先从代码流程的角度理解encoder的整个过程,然后再回到这幅图进行解释。

前面调用encoder的类之后,就进入encoder的计算流程:

class DeformableTransformerEncoder(nn.Module):
    def __init__(self, encoder_layer, num_layers):
        super().__init__()
        self.layers = _get_clones(encoder_layer, num_layers)
        self.num_layers = num_layers

    @staticmethod
    def get_reference_points(spatial_shapes, valid_ratios, device):
        reference_points_list = [] # 用于储存每一层的参考点
        for lvl, (H_, W_) in enumerate(spatial_shapes):
            # H_ W_ 是特征图的高宽  spatial_shapes是每一层的特征图的H,W
            # 对于每个特征图,我们生成一组参考点
            ref_y, ref_x = torch.meshgrid(torch.linspace(0.5, H_ - 0.5, H_, dtype=torch.float32, device=device),
                                          torch.linspace(0.5, W_ - 0.5, W_, dtype=torch.float32, device=device))
            # 使用 torch.linspace 创建从 0.5 到 H-0.5 和 W-0.5 的线性空间,这确保了参考点位于每个像素的中心
            # meshgrid 函数生成一个网格,其中包含所有可能的坐标对 (x, y)
            # 也就是根据H,W生成该特征图大小网格中心的参考点
            ref_y = ref_y.reshape(-1)[None] / (valid_ratios[:, None, lvl, 1] * H_)
            ref_x = ref_x.reshape(-1)[None] / (valid_ratios[:, None, lvl, 0] * W_)
            # valid_ratios 是一个张量,它包含了每个特征图的有效比例
            # 这是因为原始图像可能会被缩放或裁剪,导致特征图的比例与原图不同
            # valid_ratios就是特征图的大小和原始图像大小的比例,但是由于原图会经历缩放或裁剪,所以特征图的比例可能会发生变化
            # 因此,这个比例并不是直接表示特征图的大小与原始图像大小的比例,而是更具体地描述了特征图相对于原始图像的有效区域的比例。
            ref = torch.stack((ref_x, ref_y), -1)
            reference_points_list.append(ref)
        reference_points = torch.cat(reference_points_list, 1)
        reference_points = reference_points[:, :, None] * valid_ratios[:, None]
        return reference_points

    def forward(self, src, spatial_shapes, level_start_index, valid_ratios, pos=None, padding_mask=None):
        output = src
        # encoder的参考点是grid生成的,不会有迭代更新
        # 生成网格点,网格点与真实大小的比例相乘得到,最终的参考点。相当于这个参考点它是预设生成的
        reference_points = self.get_reference_points(spatial_shapes, valid_ratios, device=src.device)
        for _, layer in enumerate(self.layers):
            output = layer(output, pos, reference_points, spatial_shapes, level_start_index, padding_mask)
        return output

        return output

从forward函数来看整个流程,先是为每一层特征图生成参考点

生成参考点之后再进入每一层layer的计算

class DeformableTransformerEncoderLayer(nn.Module):
    def __init__(self,
                 d_model=256, d_ffn=1024,
                 dropout=0.1, activation="relu",
                 n_levels=4, n_heads=8, n_points=4):
        super().__init__()
        # self attention
        self.self_attn = MSDeformAttn(d_model, n_levels, n_heads, n_points)
        self.dropout1 = nn.Dropout(dropout)
        self.norm1 = nn.LayerNorm(d_model)
        # ffn
        self.linear1 = nn.Linear(d_model, d_ffn)
        self.activation = _get_activation_fn(activation)
        self.dropout2 = nn.Dropout(dropout)
        self.linear2 = nn.Linear(d_ffn, d_model)
        self.dropout3 = nn.Dropout(dropout)
        self.norm2 = nn.LayerNorm(d_model)
    @staticmethod
    def with_pos_embed(tensor, pos):
        return tensor if pos is None else tensor + pos
    def forward_ffn(self, src):
        src2 = self.linear2(self.dropout2(self.activation(self.linear1(src))))
        src = src + self.dropout3(src2)
        src = self.norm2(src)
        return src
    def forward(self, src, pos, reference_points, spatial_shapes, level_start_index, padding_mask=None):
        # self attention 就是自定义的可变形attention
        src2 = self.self_attn(self.with_pos_embed(src, pos), reference_points, src, spatial_shapes, level_start_index, padding_mask)
        src = src + self.dropout1(src2)
        src = self.norm1(src)
        # ffn
        src = self.forward_ffn(src)

        return src

流程和原始DETR大致相同,就是attention的部分替换成了deformable attention机制,也就是回到了前面那幅图的计算过程

简单理解这个机制就是query不是和所有的key都进行计算注意力权重,而是对于每个query来说,它只在全局位置中采样部分的key(本文设置的是4)进行计算得到注意力权重。

原来的注意力权重是QdotK得到的,但是这个是由线性层(3)+sofmax激活函数得到的,

之前理解了很久在找Q,K是怎么对应点积的,找了半天,后面发现根本没有使用Q,K进行点积得带注意力权重。。。所以不能这么理解

所以左边整个部分都是找到每一个参考点它对应的四个位置的值,根据这四个位置的值找到value,就是左边是为了找value值

所以,这个过程分可以分为两个模块

1)偏移量模块

2)注意力模块

 如果想仔细学习的话,推荐这个连接中的资料:

详解可变形注意力模块(Deformable Attention Module)-CSDN博客

多尺度可变形注意力替代了Encoder中的自注意力(self-attention)及Decoder中的交叉注意力(cross-attention)


3.transformer的decoder(简单版)

由于decoder中也使用了多尺度可变形注意力在cross attention中,因此,decoder也许需要初始化参考点的位置。

原始的DETR的decoder的self-attention中的输入是object query=上一层输入tgt+query_embed

而这里的object query就是代码中的query_embed

query_embed=tgt+query_embed

注意object query是DETR论文中图像中的名称,在代码实现中DETR和deformable detr都是用query_embed作为名称,因为是用query_embed取初始化另一部分,加起来才是object query。总之,没有object query的变量。

这是因为query_embed的生成有点不一样,维度需要*2(也就是256*2),

if not two_stage:
   self.query_embed = nn.Embedding(num_queries, hidden_dim*2)
        else:
            query_embed, tgt = torch.split(query_embed, c, dim=1)
            #
            query_embed = query_embed.unsqueeze(0).expand(bs, -1, -1)
            tgt = tgt.unsqueeze(0).expand(bs, -1, -1)
            # 通过线性层得到的
            # 与encoder阶段不同,它需要得到300预测的起始位置。
            reference_points = self.reference_points(query_embed).sigmoid()
            # ini也表示这是一个初始的值
            init_reference_out = reference_points
        # decoder
        # 对于不使用box强化,6层的输出和初始的是一致的。
        hs, inter_references = self.decoder(tgt, reference_points, memory,
                                            spatial_shapes, level_start_index, valid_ratios, query_embed, mask_flatten)

        inter_references_out = inter_references
        if self.two_stage:
            return hs, init_reference_out, inter_references_out, enc_outputs_class, enc_outputs_coord_unact #
        return hs, init_reference_out, inter_references_out, None, None

这个query_embed分解为query_embed和tgt,tgt就是上一层输入,之前是初始化为0,这里由分解得到的。初始化是可学习变量。

reference_points是query_embed通过一个全连接层->2维 [bs, 300, 2]。

所以reference_points+tgt是由两倍维度的query_embed得到的

后面decoderlayer中的计算,self-attention和cross-attention(多头注意力机制)分别使用不同的内容就行。

普通版本的参考点在进入decoder之前和之后都是相同的。


4.预测层FFN阶段 简单版

原始DETR中,FFN预测头预测的是分类结果和位置坐标xywh

deformable detr的FFN预测头预测的则是分类结果和偏移量xy(相对于300个参考点)+wh


高配版:iterative bounding box refinement & two-stage

1.iterative bounding box refinement

首先说区别,iterative bounding box refinement的预测头(组件的第四部分)各个预测头之前参数是不共享的,之前的DETR预测头的参数是共享的。

具体的做法是,每一层的decoder输出进入非共享参数预测头后,根据得到预测的bbox坐标xy(偏移量),对reference_points进行矫正,得到矫正后的reference_points,并以先验的reference_points送入下一层decoder,继续执行。

而普通版本的decoder过程中在每一层reference_points都是相同的,没有矫正的过程,进入和输出decoder的这个参考点都是相同的,相当于高配版本的会对参考点进行微调。

此外,2-stage模式,那么参考点就是由Encoder预测的top-k得分最高的proposal boxes(注意,这时参考点是4d的,是bbox形式,普通版本的是2D的)。然后通过对参考点进行位置嵌入(position embedding)来生成Decoder的object query(target) 和对应的 query embedding;


2.two-stage

two-stage策略必须和iterative bounding box refinement策略一起使用,但可以单独使用iterative bounding box refinement策略

two-stage的加入目的是为了生成query_embed和tgt

encoder会生成特征的memory,也就是普通版的输出,但是高配版对这个输出增加了一些处理,

 encoder生成特征的memory,会再自己生成初步proposals(其实就是特征图上的点坐标 xywh)

因为ecoder需要提取置信度(用类别cls预测头)tok个bbox的预测框,因此需要按照1.给出位置-2.类别预测-3.topk筛选的步骤来进行:

1.给出位置的部分就是通过类似anchor生成的位置生成proposal

    def gen_encoder_output_proposals(self, memory, memory_padding_mask, spatial_shapes):
        """得到第一阶段预测的所有proposal box output_proposals和处理后的Encoder输出output_memory
        memory: Encoder输出特征  [bs, H/8 * W/8 + ... + H/64 * W/64, 256]
        memory_padding_mask: Encoder输出特征对应的mask [bs, H/8 * W/8 + H/16 * W/16 + H/32 * W/32 + H/64 * W/64]
        spatial_shapes: [4, 2] backbone输出的4个特征图的shape
        """
        N_, S_, C_ = memory.shape  # bs  H/8 * W/8 + ... + H/64 * W/64  256
        base_scale = 4.0
        proposals = []
        _cur = 0   # 帮助找到mask中每个特征图的初始index
        for lvl, (H_, W_) in enumerate(spatial_shapes):  # 如H_=76  W_=112
            # 1、生成所有proposal box的中心点坐标xy
            # 展平后的mask [bs, 76, 112, 1]
            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 = [76, 112]   76行112列  第一行全是0  第二行全是1 ... 第76行全是75
            # grid_x = [76, 112]   76行112列  76行全是 0 1 2 ... 111
            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 = [76, 112, 2(xy)]   这个特征图上的所有坐标点x,y
            grid = torch.cat([grid_x.unsqueeze(-1), grid_y.unsqueeze(-1)], -1)
            scale = torch.cat([valid_W.unsqueeze(-1), valid_H.unsqueeze(-1)], 1).view(N_, 1, 1, 2)  # [bs, 1, 1, 2(xy)]
            # [76, 112, 2(xy)] -> [1, 76, 112, 2] + 0.5 得到所有网格中心点坐标  这里和one-stage的get_reference_points函数原理是一样的
            grid = (grid.unsqueeze(0).expand(N_, -1, -1, -1) + 0.5) / scale

            # 2、生成所有proposal box的宽高wh  第i层特征默认wh = 0.05 * (2**i)
            wh = torch.ones_like(grid) * 0.05 * (2.0 ** lvl)
            # 3、concat xy+wh -> proposal xywh [bs, 76x112, 4(xywh)]
            proposal = torch.cat((grid, wh), -1).view(N_, -1, 4)
            proposals.append(proposal)
            _cur += (H_ * W_)
        # concat 4 feature map proposals [bs, H/8 x W/8 + ... + H/64 x W/64] = [bs, 11312, 4]
        output_proposals = torch.cat(proposals, 1)
        # 筛选一下 xywh 都要处于(0.01,0.99)之间
        output_proposals_valid = ((output_proposals > 0.01) & (output_proposals < 0.99)).all(-1, keepdim=True)
        # 这里为什么要用log(x/1-x)这个公式???
        output_proposals = torch.log(output_proposals / (1 - output_proposals))
        # mask的地方是无效的 直接用inf代替
        output_proposals = output_proposals.masked_fill(memory_padding_mask.unsqueeze(-1), float('inf'))
        # 再按条件筛选一下 不符合的用用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))
        # 对encoder输出进行处理:全连接层 + LayerNorm
        output_memory = self.enc_output_norm(self.enc_output(output_memory))
        return output_memory, output_proposals

2.类别预测的部分就是类别检测头预测的部分(这里同时也用位置预测的部分self.decoder.bbox_embed,得到相对于proposal的偏移量,接着将偏移量加在output_proposals的中心坐标和宽、高上得到第一阶段预测的proposals,也就是代码中的enc_outputs_coord_unact)

非共享参数分类头和回归头的第7个head分别对处理过的Encoder的输出结果output_memory进行分类和回归

3.筛选的部分就是后面topk的过程

注意:很多人都提到了提取分类结果第1个类别的问题,为什么这个预测头不是二分类的?

(其实这样做不是很合理,直接二分类判断前景背景不是更好嘛?)

        bs, _, c = memory.shape
        if self.two_stage:
            # 对memory进行处理得到output_memory: [bs, H/8 * W/8 + ... + H/64 * W/64, 256]
            # 并生成初步output_proposals: [bs, H/8 * W/8 + ... + H/64 * W/64, 4]  其实就是特征图上的一个个的点坐标
            output_memory, output_proposals = self.gen_encoder_output_proposals(memory, mask_flatten, spatial_shapes)

            # hack implementation for two-stage Deformable DETR
            # 多分类:[bs, H/8 * W/8 + ... + H/64 * W/64, 256] -> [bs, H/8 * W/8 + ... + H/64 * W/64, 91]
            # 其实个人觉得这里直接进行一个二分类足够了
            enc_outputs_class = self.decoder.class_embed[self.decoder.num_layers](output_memory)
            # 回归:预测偏移量 + 参考点坐标   [bs, H/8 * W/8 + ... + H/64 * W/64, 4]
            # two-stage 必须和 iterative bounding box refinement一起使用 不然bbox_embed=None 报错
            enc_outputs_coord_unact = self.decoder.bbox_embed[self.decoder.num_layers](output_memory) + output_proposals

            # 得到参考点reference_points/先验框
            topk = self.two_stage_num_proposals  # 300
            # 直接用第一个类别的预测结果来算top-k,代表二分类
            # 如果不使用iterative bounding box refinement那么所有class_embed共享参数 导致第二阶段对解码输出进行分类时都会偏向于第一个类别
            # topk_proposals: [bs, 300]  top300 index
            topk_proposals = torch.topk(enc_outputs_class[..., 0], topk, dim=1)[1]
            # topk_coords_unact: top300个分类得分最高的index对应的预测bbox [bs, 300, 4]
            topk_coords_unact = torch.gather(enc_outputs_coord_unact, 1, topk_proposals.unsqueeze(-1).repeat(1, 1, 4))
            topk_coords_unact = topk_coords_unact.detach()  # 以先验框的形式存在  取消梯度
            reference_points = topk_coords_unact.sigmoid()  # 得到归一化参考点坐标  最终会送到decoder中作为初始的参考点
            init_reference_out = reference_points

            # 生成Docder的query和query pos
            # 先对top-k proposal box进行位置编码,编码方式是给xywh每个都赋予128维 其中每个128维使用sine编码  最后用全连接层和LN处理
            # 最终得到pos_trans_out: [bs, 300, 512] 前256为query pos(x、y信息)  后256为query(w、h信息)
            pos_trans_out = self.pos_trans_norm(self.pos_trans(self.get_proposal_pos_embed(topk_coords_unact)))
            query_embed, tgt = torch.split(pos_trans_out, c, dim=2)

得到的topk个proposal的目的是生成decoder阶段的query_embed和tgt,之前是直接由

self.query_embed = nn.Embedding(num_queries, hidden_dim*2)

query_embed, tgt = torch.split(query_embed, c, dim=1)这样的过程得到的

之后输入进入decoder的参考点是4D的,deformable attention中判断维度4D就是decoder的计算过程

        # N, Len_q, n_heads, n_levels, n_points, 2
        if reference_points.shape[-1] == 2:    # one stage
            # [4, 2]  每个(h, w) -> (w, h)
            offset_normalizer = torch.stack([input_spatial_shapes[..., 1], input_spatial_shapes[..., 0]], -1)
            # [bs, Len_q, 1, n_point, 1, 2] + [bs, Len_q, n_head, n_level, n_point, 2] / [1, 1, 1, n_point, 1, 2]
            # -> [bs, Len_q, 1, n_levels, n_points, 2]
            # 参考点 + 偏移量/特征层宽高 = 采样点
            sampling_locations = reference_points[:, :, None, :, None, :] \
                                 + sampling_offsets / offset_normalizer[None, None, None, :, None, :]
        # two stage  +  iterative bounding box refinement
        elif reference_points.shape[-1] == 4:
            # 前两个是xy 后两个是wh
            # 初始化时offset是在 -n_points ~ n_points 范围之间 这里除以self.n_points是相当于把offset归一化到 0~1
            # 然后再乘以宽高的一半 再加上参考点的中心坐标 这就相当于使得最后的采样点坐标总是位于proposal box内
            # 相当于对采样范围进行了约束 减少了搜索空间
            sampling_locations = reference_points[:, :, None, :, None, :2] \
                                 + sampling_offsets / self.n_points * reference_points[:, :, None, :, None, 2:] * 0.5

损失函数部分

由于有two-stage的部分,因此损失函数这部分也增加了增加的预测头的损失函数部分

        # two-stage的时候会有这个输出
        # encoder输出的结果进行上面loss的计算
        if 'enc_outputs' in outputs:
            enc_outputs = outputs['enc_outputs']
            bin_targets = copy.deepcopy(targets)

            for bt in bin_targets:
                bt['labels'] = torch.zeros_like(bt['labels'])
            indices = self.matcher(enc_outputs, bin_targets)
            for loss in self.losses:
                if loss == 'masks':
                    # Intermediate masks losses are too costly to compute, we ignore them.
                    continue
                kwargs = {}
                if loss == 'labels':
                    # Logging is enabled only for the last layer
                    kwargs['log'] = False
                #
                l_dict = self.get_loss(loss, enc_outputs, bin_targets, indices, num_boxes, **kwargs)
                l_dict = {k + f'_enc': v for k, v in l_dict.items()}
                losses.update(l_dict)


其他补充:

1.可变形卷积的思路借鉴了DCN(Deformable Convolutional Networks)


总结:

总的来说,本文重点在于理解

1.多尺度可变形注意力机制是怎么减少计算量的同时关注到更多的区域的

2.two stage的过程中的proposal生成decoder输入的作用(读完DAB-DETR,对这方面的理解会更加深入)

3.预测头部分预测的是偏移量而不是坐标,因此有7个预测头,6个是对应decoder的每层输出预测偏移量,多余的一个是用来two-stage的topk个的选择,它们不能共享参数。特别是two-stage的那一个,由于只使用了第一个分类的结果进行筛选,使用到后面预测中会使得模型更倾向于第一个分类结果


参考资料:

Deformable DETR: 基于稀疏空间采样的注意力机制,让DCN与Transformer一起玩! - 知乎 (zhihu.com)

【Deformable DETR 论文+源码解读】Deformable Transformers for End-to-End Object Detection_deformable detr: deformable transformers for end-t-CSDN博客

强烈建议再阅读一下这两篇文章,读完受益匪浅!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BagMM

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值