基于MMdetection的RT-DETR目标检测器的源码解析(上)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

对于MMdetection的安装和初步使用,可见我的另一篇博客:
链接: link

一、RT-DETR目标检测器

RT-DETR是采用了DETR的结构第一个实时的端到端物体检测器,有效的避免 NMS 导致的推理延迟同时提升性能。

二、源码阅读及debug过程

1.下载项目

关于MMdetection版本的RT-DETR项目文件可在下面链接进行下载:
链接:link

打开项目文件后,通过配置正确的数据集路径,以及修改分类类别数 num_classes后,便可进行debug.

2.网络结构

在这里插入图片描述视频讲解可以参考这位大佬的讲解视频
链接:link

3. 目标检测器文件:rtdetr.py解析

进入到mmdet/models/detectors/rtdetr.py文件中,可以发现创建了一个检测器RTDETR的类,该类继承了DINO检测器,当进入DINO类,其继承了DeformableDETR类,DeformableDETR类又继承了DetectionTransformer类。

RTDETR类主要方法包括:pre_transformer(),forward_encoder(),pre_decoder(),forward_decoder(),generate_proposals()
DetectionTransformer 是一个基于DETR(transformer)结构目标检测器的基类。

在这里插入图片描述
在这里插入图片描述在DetectionTransforme类的forword函数中打上断点

在这里插入图片描述进入该类下面的 extract_feat函数,即特征提取模块
在这里插入图片描述
输入的batch图像进行backbone输出特征图x,根据rtdetr的config配置文件可知backbone为resnet50,

4.Backbone文件解析:resnet.py

进入到mmdet/models/backbones/resnet.py,即配置骨干网络的文件,在class ResNet(BaseModule)类的forword中打上断点

    def forward(self, x):
        """Forward function."""
        # 是否启用深度干扰(deep stem)
        if self.deep_stem:
            x = self.stem(x)
        else:
            x = self.conv1(x)
            x = self.norm1(x)
            x = self.relu(x)
        x = self.maxpool(x)  # 对特征图进行最大池化操作

        outs = []  # 创建一个空列表 outs 用于存储网络的不同层的输出
        # for 循环遍历网络的各个层(通过 self.res_layers 中的层名称来访问),其中 self.res_layers 存储了网络中的残差块(Residual Block)的名称
        for i, layer_name in enumerate(self.res_layers):
            # 对于每个残差块,通过 getattr(self, layer_name) 来获取相应的残差块模块
            res_layer = getattr(self, layer_name)
            x = res_layer(x)  # 输入 x 传递给这个残差块模块,执行残差块的前向传播操作

            # 如果当前残差块的索引 i 在 self.out_indices 中(这是一个包含输出索引的列表),则将当前层的输出 x 添加到 outs 列表中
            if i in self.out_indices:
                outs.append(x)
        return tuple(outs)

resnet包含两种不同类型的blocks,BasicBlock和Bottleneck,每个BasicBlock包含两个卷积层,每个Bottleneck包含三个卷积层,在resnet50中采用的是第一种块。在module的config文件中,能发现这里输出了backbone中的三个stage

backbone=dict(
        type='ResNetV1d',
        depth=50,
        num_stages=4,
        out_indices=(1, 2, 3),# 三个stage(123)输出特征图
        frozen_stages=1,
        norm_cfg=dict(type='BN', requires_grad=False),
        norm_eval=True,
        style='pytorch',
        init_cfg=dict(
            type='Pretrained',
            checkpoint=
            'https://github.com/nijkah/storage/releases/download/v0.0.1/resnet50vd_ssld_v2_pretrained.pth'
        )),

在forward函数执行完之后会得到一个outs[ ]列表,其中包含三个特征图,shape值分别为(bs,channel,w,h)
在这里插入图片描述当再进行单步调试,在DetectionTransformer的extract_feat()函数中,此时的特征图x已经是backbone输出的out[ ]列表,之后会进入neck层进行特征整合

x = self.backbone(batch_inputs)
        if self.with_neck:    # 判断是否存在neck层
            x = self.neck(x)  # neck层进行特征整合
        return x

5.neck层文件解析:HybridEncoder.py

进入mmdet/models/layers/transformer/hybrid_encoder.py中,在下面的forward()函数中打上断点

    def forward(self, inputs: Tuple[Tensor]) -> Tuple[Tensor]:
        """Forward function."""
        assert len(inputs) == len(self.in_channels) 
        # 特征投影:将输入的三个层级的特征图投影成channel=256的特征图
        proj_feats = [
            self.input_proj[i](inputs[i]) for i in range(len(inputs))
        ]
        # encoder
        if self.num_encoder_layers > 0:
            for i, enc_ind in enumerate(self.use_encoder_idx):    
                h, w = proj_feats[enc_ind].shape[2:] 
                # flatten [B, C, H, W] to [B, HxW, C]
                # flatten(2)将第二维及以后的维度压缩,permute(0,2,1)H*WC 的位置改变
                src_flatten = proj_feats[enc_ind].flatten(2).permute( 
                    0, 2, 1).contiguous()                              

                if self.training or self.eval_size is None:  
                    pos_embed = self.build_2d_sincos_position_embedding(  # 得到输入层级特征图的正余弦位置编码
                        w, h, self.hidden_dim, self.pe_temperature)
                else:
                    pos_embed = getattr(self, f'pos_embed{enc_ind}', None)
                    
                memory = self.encoder[i](
                    src_flatten,
                    query_pos=pos_embed.to(src_flatten.device),  
                    key_padding_mask=None)  

                # 重新排列memory的维度,将其形状变回 [B, hidden_dim, h, w],复制给proj_feats[2],得到F5
                proj_feats[enc_ind] = memory.permute(
                    0, 2, 1).contiguous().view([-1, self.hidden_dim, h, w])

        # top-down fpn
        inner_outs = [proj_feats[-1]]  # 将inner_outs[]列表设置为 proj_feats 列表中的最后一个向量,即F5特征图
        for idx in range(len(self.in_channels) - 1, 0, -1):  # 遍历s5(F5)->s4 (2->1->0)特征图
            feat_high = inner_outs[0]  # feat_high= s5(F5)
            feat_low = proj_feats[idx - 1]  # feat_low= s4
            
            # 1*1卷积调整通道数
            feat_high = self.lateral_convs[len(self.in_channels) - 1 - idx](  
                feat_high)
            inner_outs[0] = feat_high

            # F.interpolate插值函数,scale_factor=2.0 表示将特征图的尺寸放大两倍,
            # mode='nearest' 表示使用最近邻插值,以保持像素的类别标签不变
            upsample_feat = F.interpolate(
                feat_high, scale_factor=2., mode='nearest')
            # 将经过最近邻插值后的F532564040)与s4进行concat
            inner_out = self.fpn_blocks[len(self.in_channels) - 1 - idx](
                torch.cat([upsample_feat, feat_low], axis=1))

            # inner_out 是经过处理的特征图,它被插入到 inner_outs 列表的开头,以成为下一轮迭代的高分辨率特征图
            inner_outs.insert(0, inner_out)

        # bottom-up pan
        # 进行底部到顶部的特征金字塔网络 (Bottom-Up Pathway) 的构建和特征融合,以便将底部的低分辨率特征与顶部的高分辨率特征相结合

        outs = [inner_outs[0]]  # inner_outs 列表中的第一个特征图 M3
        for idx in range(len(self.in_channels) - 1):  # 它从M3特征图向上遍历,直到达到M5特征图之前的一个特征图。
            feat_low = outs[-1]
            feat_high = inner_outs[idx + 1]  # 将 feat_high 设置为 inner_outs 列表中的下一个s4,一直到s5
            downsample_feat = self.downsample_convs[idx](feat_low)  # 将更高分辨率特征图下采样或压缩以匹配低分辨率特征图的大小
            out = self.pan_blocks[idx](
                torch.cat([downsample_feat, feat_high], axis=1))
            outs.append(out)

        if self.projector is not None:
            outs = self.projector(outs)

        return tuple(outs)

neck层主要包含两个模块,包括AIFI和CCFM,AIFI就是将backbone输出的最后的一层特征图经过transformer的一层Encoder,得到F5。

5.1 AIFI模块详解

# Encoder过程
# pos_embed = self.build_2d_sincos_position_embedding 得到输入层级特征图的正余弦位置编码

memory = self.encoder[i](
                    src_flatten, # s5特征图拉长后的序列 shape为(3256400)
                    query_pos=pos_embed.to(src_flatten.device),  
                    key_padding_mask=None)

如果要想进行更深层次的了解Encoder过程,可以进入self.encoder层,在初始化self.encoder层时可以发现其继承了DetrTransformerEncoder类,
在这里插入图片描述在mmdet/models/layers/transformer/detr_layers.py中找到DetrTransformerEncoder类,在其forward函数过程中调用了DetrTransformerEncoderLayer(**self.layer_cfg)

在这里插入图片描述
在上述的forward函数中,将 s5特征图拉长后的序列src_flatten赋给query,pos_embed赋值给query_pos,以进行encoder。

在这里插入图片描述

进入该文件下的DetrTransformerEncoderLayer类中的forward函数
在这里插入图片描述
该前向传播中的self.self_attn(),即自注意力计算,将前面得到的query重新赋值给自注意力计算所需的query,key,value,将query_pos赋值给key_pos,不使用mask掩码屏蔽。经过自注意力计算后,对输出的query进行BN[0],FFN以及BN[1]。

进入核心的self.self_atten的初始化layers:

在这里插入图片描述
可以发现其调用了MultiheadAttention类,进入D:\Anaconda\envs\pytorch2\Lib\site-packages\mmcv\cnn\bricks\transformer.py的MultiheadAttention类的前向传播forward中,找到核心的attn计算部分
在这里插入图片描述
此时的self.attn()方法调用了activation.py中的MultiheadAttention类,在该类的forward中,其主要是采用F.multi_head_attention_forward()方法进行一头的注意力计算
在这里插入图片描述在该方法中,核心方法是_scaled_dot_product_attention(计算缩放点积注意力)

 B, Nt, E = q.shape  # B is batch size, Nt is the target sequence length,and E is embedding dimension.
    q = q / math.sqrt(E)
    # (B, Nt, E) x (B, E, Ns) -> (B, Nt, Ns)
    attn = torch.bmm(q, k.transpose(-2, -1))
    if attn_mask is not None:
        attn += attn_mask
    attn = softmax(attn, dim=-1) #  dim=-1 表示在最后一个维度(序列长度维度)上执行 softmax。
    if dropout_p > 0.0:
        attn = dropout(attn, p=dropout_p)
    # (B, Nt, Ns) x (B, Ns, E) -> (B, Nt, E)
    output = torch.bmm(attn, v) # 使用计算得到的注意力权重 attn 对值张量 v 进行加权求和,得到最终的输出张量 output。这一步是通过矩阵相乘来实现的。
    return output, attn

得到注意力输出output和注意力权重attn,之后经过维度变换和残差连接后赋给query,再经过FFN和BN后输出给memory,通过调整维度得到F5特征图。这就是AIFI模块实现的主要功能。

5.2 CCFM模块介绍

在HybridEncoder.py的forward()中,CCFM模块的构建是一个FPN+PAN的特征金字塔的特征融合网络结构

  # top-down fpn
        inner_outs = [proj_feats[-1]]  
        for idx in range(len(self.in_channels) - 1, 0, -1): 
            feat_high = inner_outs[0]  # feat_high= s5(F5)
            feat_low = proj_feats[idx - 1]  # feat_low= s4
            feat_high = self.lateral_convs[len(self.in_channels) - 1 - idx](  
                feat_high)
            inner_outs[0] = feat_high
            upsample_feat = F.interpolate(
                feat_high, scale_factor=2., mode='nearest')
            inner_out = self.fpn_blocks[len(self.in_channels) - 1 - idx](
                torch.cat([upsample_feat, feat_low], axis=1))
            inner_outs.insert(0, inner_out)

   # bottom-up pan
        # 进行底部到顶部的特征金字塔网络 (Bottom-Up Pathway) 的构建和特征融合,以便将底部的低分辨率特征与顶部的高分辨率特征相结合
        outs = [inner_outs[0]] 
        for idx in range(len(self.in_channels) - 1):  
            feat_low = outs[-1]
            feat_high = inner_outs[idx + 1]  
            downsample_feat = self.downsample_convs[idx](feat_low) 
            out = self.pan_blocks[idx](
                torch.cat([downsample_feat, feat_high], axis=1))
            outs.append(out)
        if self.projector is not None:
            outs = self.projector(outs)
        return tuple(outs)

总结

此次博客介绍了基于MMdetection的RT-DETR项目源码解读与debug,主要是bockbone网络的特征提取和neck层网络的特征融合,在后续更新的博客 ‘基于MMdetection的RT-DETR目标检测器的源码解析(下)’ 会介绍encoder部分以及loss的计算。创作不易,如需观看源码解析(下)可私信我

  • 2
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值