RT-DETR系列之backbone

本人正在学习RT-DETR中,将学习过程记录下来,如有错误,请指正!通融通融

本博客以RT-DETR的r18为基准模型学习。

最开始学习要知道整个RT-DETR的大致结构,如图。backbone位于最前面,骨干网络(Backbone)用于从输入图像中提取特征。它的任务是通过一系列卷积操作和其他处理方法,将输入的原始图像转换为一个高维的特征表示。P1P2P3P4P5是不同网络层的输出特征图的尺寸(通常是下采样的结果),这些代表了卷积神经网络(CNN)中特征图的不同层级。这里图片的S3,S4,S5就是指的P3,P4,P5。

开始学习

首先来看配置文件(yaml文件)中backbone的代码

backbone:
  # [from, repeats, module, args]
  - [-1, 1, ConvNormLayer, [32, 3, 2, None, False, 'relu']] # 0-P1/2
  - [-1, 1, ConvNormLayer, [32, 3, 1, None, False, 'relu']] # 1
  - [-1, 1, ConvNormLayer, [64, 3, 1, None, False, 'relu']] # 2
  - [-1, 1, nn.MaxPool2d, [3, 2, 1]] # 3-P2/4
  
  # [ch_out, block_type, block_nums, stage_num, act, variant]
  - [-1, 1, Blocks, [64, BasicBlock, 2, 2, 'relu']] # 4
  - [-1, 1, Blocks, [128, BasicBlock, 2, 3, 'relu']] # 5-P3/8
  - [-1, 1, Blocks, [256, BasicBlock, 2, 4, 'relu']] # 6-P4/16
  - [-1, 1, Blocks, [512, BasicBlock, 2, 5, 'relu']] # 7-P5/32
第一行代码(配置文件第0层)
 - [-1, 1, ConvNormLayer, [32, 3, 2, None, False, 'relu']] # 0-P1/2

这个配置代表了一层的设置,它有四个元素:

  1. from-1 表示输入来自于上一层的输出。对于第一行的配置(即 -1 作为输入),它通常表示输入来自于“最初的输入图像”或者说是“图像输入层”。
  2. repeats1 表示该层只会被执行一次。如果为大于 1 的值,则该层会重复多次。
  3. moduleConvNormLayer 表示这一层的模块类型是 ConvNormLayer,即卷积 + 批归一化 + 激活函数的层。(后面介绍)
  4. args:这个部分是一个列表,包含了 ConvNormLayer 所需要的参数:
    • 32:输出通道数(ch_out)。
    • 3:卷积核的大小(kernel_size)。
    • 2:卷积步长(stride)。步长就是卷积的时候滑动窗口一次滑动几格,这里就是一次滑动两格。如图1,对一个5*5的图像做卷积,卷积核是3*3,步长是2,就可以得到一个2*2的特征输出图像。
    • None:填充方式(padding),默认为 None,即会自动根据卷积核大小计算填充。有时候需要在原图像周围加一圈或者几圈0或1,来使原图像更大。如图2
    • False:是否使用偏置(bias),这里设置为 False偏置(bias) 是一种额外的参数,它的作用是为每个神经元(或者每个神经层的输出)提供一个额外的自由度。也就是y=wx+b,中的b。
    • 'relu':激活函数(act),这里使用 ReLU 激活函数。

                              图1

                                                                       图2 

默认输入图像是640*640,3通道的图像

输入图片解析:

  • 输入图片为 640x640 的 3 通道图像,即每张图片的尺寸是 3x640x640

计算卷积后的输出尺寸:

给定输入图像的尺寸为 640x640,卷积层的参数为:

  • kernel_size = 3
  • stride = 2
  • padding = None

在这种配置下,卷积的输出尺寸可以使用以下公式来计算:

Output Height=(StrideInput Height+2×Padding−Kernel Size)/stride​+1

Output Width=(StrideInput Width+2×Padding−Kernel Size​)/stride+1

将 640x640 输入和卷积层的参数代入公式:

Output Height=(640+2×0−3​)/2+1=637/2+1​=318.5+1=319.5

Output Width=(640+2×0−3​)/2+1=637/2​+1=318.5+1=319.5

因此,卷积操作之后的输出尺寸将是 32x320x320因为结果是 319.5,所以通常 取整 变成 320,即高度和宽度都缩小了一半,输出通道数变为 32。这就是P1/2。P1/2: P1层的特征图的尺寸是输入图像尺寸的一半,代表网络中的第一个下采样层。

结果:

  • 输入图像大小:3x640x640
  • 卷积层输出大小:32x320x320
第二行(配置文件第1层)
 - [-1, 1, ConvNormLayer, [32, 3, 1, None, False, 'relu']] # 1

这一层配置实际上是在 第一层卷积层之后添加的另一层卷积操作。它的主要作用是继续处理前一层卷积后的特征图,进一步提取特征。和第一行唯一的区别就是步长,从2变成了1. 意味着卷积操作不会减少特征图的尺寸(宽度和高度保持不变)。

因此:

  • 输出通道数:这一层的卷积核数量是 32,因此输出的通道数是 32

第1层卷积的输出尺寸32x320x320

第三行(配置文件第2层)
- [-1, 1, ConvNormLayer, [64, 3, 1, None, False, 'relu']] # 2

同样是 backbone 中的一个卷积层,只不过这次它的输出通道数变成了 64。

  • 64 个 3x3 的卷积核,用于提取更多的特征。
  • 步长为 1,表示不会改变特征图的空间尺寸,保持图像的宽度和高度不变。
  • 第二层卷积的输出尺寸64x320x320

为什么增加通道数?

在深度神经网络中,通常会在深层次逐渐增加通道数(即卷积核的数量)。这样做的目的是在特征图的空间维度(宽度和高度)不变的情况下,增加网络可以捕捉到的特征的复杂性和多样性。通过增加通道数,网络能够学习更多的高维特征,从而提高最终的模型表现。

第四行(配置文件第3层)
 - [-1, 1, nn.MaxPool2d, [3, 2, 1]] # 3-P2/4
  • nn.MaxPool2d:这是一个 2D 最大池化层,它将对输入图像进行池化操作,从而减少图像的空间维度(宽度和高度),通常用于特征压缩或降维。
  • 参数 [3, 2, 1] 表示:
    • 3:池化核的大小,即 3x3 的矩阵用于池化操作。
    • 2:步长(stride),池化核每次滑动的步长为 2。
    • 1:填充(padding),在池化时对输入的边缘进行填充,确保池化后的输出尺寸。

池化操作尺寸计算

对于 MaxPool2d 操作,输出的高度和宽度可以通过以下公式计算:

输出尺寸=(步长输入尺寸+2×填充−池化核尺寸​)/步长+1

在这里:

  • 输入尺寸320(高度和宽度相同,所以这里就用 316 计算)。
  • 池化核尺寸3
  • 步长2
  • 填充1

计算过程

计算输出高度和宽度:

输出高度/宽度=(320+2×1−3)/2+1

=(320+2−3​)/2+1

=319​/2+1

=159+1=160

所以,输出高度和宽度 都是 160

输出通道数:

池化层不会改变通道数,所以输出的通道数仍然是 64(输入的通道数)。

结果

经过 MaxPool2d 层后,输入尺寸 316x316x64 变为输出尺寸:

  • 输出尺寸64x160x160 (P2/4: P2层的特征图的尺寸是输入图像尺寸的1/4)
第五行代码(配置文件第4层) 
- [-1, 1, Blocks, [64, BasicBlock, 2, 2, 'relu']]
  • [-1, 1]:表示当前层输入来自上一层,并且重复一次。
  • Blocks:表示一个包含多个 BasicBlock 的模块。在这里,Blocks 模块会包含若干个 BasicBlock
  • [64, BasicBlock, 2, 2, 'relu']
    • 64:表示每个 BasicBlock 的输出通道数。
    • BasicBlock:模块类型,意味着每个块是一个 BasicBlock
    • 2:表示在当前 Blocks 模块中有 2 个 BasicBlock
    • 2:阶段编号,还是P2层的大小,没有变化,后面几行的代码,这个位置分别是3,4,5,就代表P3,P4,P5。
    • 'relu':表示使用 ReLU 激活函数。

先来看Blocks的代码

class Blocks(nn.Module):
    def __init__(self, ch_in, ch_out, block, count, stage_num, act='relu', input_resolution=None, sr_ratio=None, kernel_size=None, kan_name=None, variant='d'):
        super().__init__()

        self.blocks = nn.ModuleList()
        for i in range(count):
            if input_resolution is not None and sr_ratio is not None:
                self.blocks.append(
                    block(
                        ch_in, 
                        ch_out,
                        stride=2 if i == 0 and stage_num != 2 else 1, 
                        shortcut=False if i == 0 else True,
                        variant=variant,
                        act=act,
                        input_resolution=input_resolution,
                        sr_ratio=sr_ratio)
                )
            elif kernel_size is not None:
                self.blocks.append(
                    block(
                        ch_in, 
                        ch_out,
                        stride=2 if i == 0 and stage_num != 2 else 1, 
                        shortcut=False if i == 0 else True,
                        variant=variant,
                        act=act,
                        kernel_size=kernel_size)
                )
            elif kan_name is not None:
                self.blocks.append(
                    block(
                        ch_in, 
                        ch_out,
                        stride=2 if i == 0 and stage_num != 2 else 1, 
                        shortcut=False if i == 0 else True,
                        variant=variant,
                        act=act,
                        kan_name=kan_name)
                )
            else:
                self.blocks.append(
                    block(
                        ch_in, 
                        ch_out,
                        stride=2 if i == 0 and stage_num != 2 else 1, 
                        shortcut=False if i == 0 else True,
                        variant=variant,
                        act=act)
                )
            if i == 0:
                ch_in = ch_out * block.expansion

    def forward(self, x):
        out = x
        for block in self.blocks:
            out = block(out)
        return out

__init__ 方法

  • 这是 Blocks 类的构造函数,接收多个参数来定义块的特性:
    • ch_in: 输入通道数。
    • ch_out: 输出通道数。
    • block: 使用的块类型(例如卷积块、残差块等)。
    • count: 该层中块的数量。
    • stage_num: 网络的阶段,用于决定第一个块的步幅。
    • act: 激活函数(默认为 'relu')。
    • input_resolutionsr_ratiokernel_sizekan_name: 用于定制块的附加参数(根据块的要求有条件地使用)。没有传入 input_resolutionsr_ratiokernel_size 或 kan_name,它们的值默认是 None,并未在构造过程中使用
    • variant: 用于定制化的变种参数(默认为 'd')。
self.blocks = nn.ModuleList()
  • 这行代码初始化了一个空的块列表(nn.ModuleList())。这个列表用来存储每个块。ModuleList 是 PyTorch 中用于注册子模块的容器,这样每个块就可以被包含在计算图中,并在训练时进行优化。

for循环 

  • 这个循环会执行 count 次,创建指定数量的块。

在循环内部:

  • 每个块都是通过 block 函数(可能是一个自定义的层,如卷积块、残差块等)创建的,传入的参数包括:

    • ch_inch_out:输入和输出的通道数。
    • stride:如果是第一个块且 stage_num 不是 2,则步幅为 2,否则为 1。
    • shortcut:第一个块的 shortcut 设置为 False,后续的块为 True,以便使用残差连接。
    • variantact:激活函数和块的变种。
    • input_resolutionsr_ratiokernel_size 或 kan_name:根据条件提供的额外参数。这里没用
  • 在添加第一个块后,输入通道 ch_in 会被更新为 ch_out * block.expansion,这是因为有些块(如残差块)可能会扩展通道数。

所以其实 Blocks 看起来很长,但是for循环中有用的只有

self.blocks.append(
                    block(
                        ch_in, 
                        ch_out,
                        stride=2 if i == 0 and stage_num != 2 else 1, 
                        shortcut=False if i == 0 else True,
                        variant=variant,
                        act=act)
                )

Blocks 初始化逻辑: 

  • stride 的赋值规则
    在 Blocks 的循环中,每个 BasicBlock 的 stride 由以下条件决定:

    stride=2 if i == 0 and stage_num != 2 else 1, 
    • 第一个块(i=0)且 stage_num ≠ 2stride=2(下采样)。
    • 其他情况stride=1(保持分辨率)。
  • shortcut 的赋值规则

      shortcut=False if i == 0 else True,
    • 第一个块(i=0shortcut=False(需要调整通道或分辨率)。
    • 后续块(i>0shortcut=True(直接使用输入作为 shortcut)。

通道数更新 

if i == 0:
                ch_in = ch_out * block.expansion

  1. 第一个块(i=0)执行后,输入通道 ch_in 会被更新为 ch_out * block.expansion(即 ch_out * 1,因为 BasicBlock.expansion = 1)。

  2. 后续块的输入通道与输出通道相同(ch_in = ch_out),因此无需调整通道数,直接使用 shortcut=True

forward方法 

  • 这是 Blocks 类的前向传播方法。它接收输入 x,将其依次通过每个块,最终返回处理后的输出。
  • 输入 x 被传递给每个块,且每个块的输出都成为下一个块的输入。

接下来根据BasicBlock代码解释其作用 

class BasicBlock(nn.Module):
    expansion = 1

    def __init__(self, ch_in, ch_out, stride, shortcut, act='relu', variant='d'):
        super().__init__()

        self.shortcut = shortcut

        if not shortcut:
            if variant == 'd' and stride == 2:
                self.short = nn.Sequential(OrderedDict([
                    ('pool', nn.AvgPool2d(2, 2, 0, ceil_mode=True)),
                    ('conv', ConvNormLayer(ch_in, ch_out, 1, 1))
                ]))
            else:
                self.short = ConvNormLayer(ch_in, ch_out, 1, stride)

        self.branch2a = ConvNormLayer(ch_in, ch_out, 3, stride, act=act)
        self.branch2b = ConvNormLayer(ch_out, ch_out, 3, 1, act=None)
        self.act = nn.Identity() if act is None else get_activation(act) 


    def forward(self, x):
        out = self.branch2a(x)
        out = self.branch2b(out)
        if self.shortcut:
            short = x
        else:
            short = self.short(x)
        
        out = out + short
        out = self.act(out)

        return out
  • expansion: 这个变量用于确定 BasicBlock 最终输出的通道数与输入通道数之间的比例。在 BasicBlock 中,expansion 被设置为 1
  • __init__ 方法:初始化方法接收参数 ch_in(输入通道数),ch_out(输出通道数),stride(步幅),shortcut(是否使用快捷连接),act(激活函数类型),variant(变体类型)。
    • self.shortcut: 指示是否使用快捷连接。
    • self.short: 如果不使用快捷连接,则创建一个 short 分支。
      • 变体 d 且步幅为 2 时,short 分支包括一个平均池化和一个 ConvNormLayer 卷积层。
      • 否则,short 分支仅为一个 ConvNormLayer 卷积层。
    • self.branch2a 和 self.branch2b: 定义了两层卷积层。branch2a 使用传入的激活函数,branch2b 没有激活函数。
    • self.act: 激活函数,如果 act 为 None,使用 nn.Identity(),否则使用指定的激活函数。
  • forward 方法:定义了数据在 BasicBlock 中的前向传播路径。
    • out = self.branch2a(x):输入 x 通过第一个卷积层 branch2a
    • out = self.branch2b(out):输出 out 通过第二个卷积层 branch2b
    • short = x 或 short = self.short(x):如果使用快捷连接,则 short 为输入 x。否则,short 为通过 self.short 分支后的输出。
    • out = out + short:将 branch2b 的输出与 short 相加,进行残差连接。
    • out = self.act(out):通过激活函数。
    • 返回 out:返回结果。

先说明Blocks在BasicBlock中的作用,就是定义shortcut和stride的值

BasicBlock 中 shortcut 的 stride 值

  • 当 shortcut=False 时(仅在第一个块发生):

    • self.short 层的 stride 直接继承自 Blocks 传入的 stride 参数。
    • 具体值
      • 如果 stage_num ≠ 2stride=2(下采样)。
      • 如果 stage_num = 2stride=1(保持分辨率)。
  • 当 shortcut=True 时(后续块):
    直接使用输入作为 shortcut,不涉及 stride 操作。

所以当输入是158*158*64时 

  1. 第一个 BasicBlocki=0

    • stride:由于 stage_num = 2,根据规则 stride=1
    • shortcutFalse(强制调整通道,即使输入输出通道相同)。
    • 操作
      • self.branch2a 和 self.branch2b:3x3卷积,stride=1,分辨率保持 160x160
      • self.short:1x1卷积,stride=1,仅调整通道(输入输出均为64,实际无意义但代码仍执行)。
    • 输出:64x158x158。
  2. 第二个 BasicBlocki=1

    • stride1
    • shortcutTrue(输入输出通道相同,直接相加)。
    • 操作:3x3卷积,stride=1,分辨率保持 160x160
    • 输出:64x160x160。

最终输出

  • 尺寸64x160x160(分辨率不变,通道数不变)。模块没有改变分辨率和通道数,但它通过增加网络深度、增强特征提取能力和引入残差连接,提高了模型的表现力和鲁棒性。这些模块在现代卷积神经网络(如 ResNet)中的应用已经证明了它们的有效性。

每个 BasicBlock 对输入数据进行两次卷积操作,并通过残差连接(shortcut connection)将原始输入添加到卷积输出中,最终通过激活函数。如果 stride 为 2 且 shortcut=False,则进行空间尺寸缩减。每个卷积操作的输出通道数由 ch_out 决定,空间尺寸根据卷积核和步幅决定。 

第六行代码(配置文件第5层)
 - [-1, 1, Blocks, [128, BasicBlock, 2, 3, 'relu']] # 5-P3/8

参数解析

  • ch_out = 128:输出通道为128。
  • count = 2:包含2个 BasicBlock
  • stage_num = 3:阶段编号为3。

处理逻辑

  1. 第一个 BasicBlocki=0

    • stride:由于 stage_num ≠ 2,根据规则 stride=2
    • shortcutFalse(需要下采样并调整通道)。
    • 操作
      • self.branch2a:3x3卷积,stride=2,输入 64x160x160 → 输出 128x80x80。
      • self.branch2b:3x3卷积,stride=1,保持 128x80x80。
      • self.short:若 variant='d',使用 AvgPool2d(2,2) + 1x1卷积(stride=1),将输入 64x160x160 → 128x80x80。
    • 输出:128x80x80(分辨率减半,通道数翻倍)。
  2. 第二个 BasicBlocki=1

    • stride1
    • shortcutTrue(输入输出通道相同,直接相加)。
    • 操作:3x3卷积,stride=1,分辨率保持 80x80
    • 输出:128x80x80。

最终输出

  • 尺寸128x80x80(标记为 P3/8,表示相对原图下采样 8 倍)。
第七行代码(配置文件第6层) 
  - [-1, 1, Blocks, [256, BasicBlock, 2, 4, 'relu']] # 6-P4/16

参数解析

  • ch_out = 256:输出通道为256。
  • count = 2:包含2个 BasicBlock
  • stage_num = 4:阶段编号为4。

处理逻辑

  1. 第一个 BasicBlocki=0

    • stride:由于 stage_num ≠ 2,根据规则 stride=2
    • shortcutFalse(需要下采样并调整通道)。
    • 操作
      • self.branch2a:3x3卷积,stride=2,输入 128x80x80 → 输出 256x40x40。
      • self.branch2b:3x3卷积,stride=1,保持 256x40x40。
      • self.short:若 variant='d',使用 AvgPool2d(2,2) + 1x1卷积(stride=1),将输入 128x80x80 → 256x40x40。
    • 输出:256x40x40(分辨率减半,通道数翻倍)。
  2. 第二个 BasicBlocki=1

    • stride1
    • shortcutTrue(输入输出通道相同,直接相加)。
    • 操作:3x3卷积,stride=1,分辨率保持 39x39。
    • 输出:256x40x40。

最终输出

  • 尺寸256x40x40(标记为 P4/16,表示相对原图下采样 16 倍)。
第八行代码(配置文件第7层)
 - [-1, 1, Blocks, [512, BasicBlock, 2, 5, 'relu']] # 7-P5/32

参数解析

  • ch_out = 512:输出通道为512。
  • count = 2:包含2个 BasicBlock
  • stage_num = 5:阶段编号为5。

处理逻辑

  1. 第一个 BasicBlocki=0

    • stride:由于 stage_num ≠ 2,根据规则 stride=2
    • shortcutFalse(需要下采样并调整通道)。
    • 操作
      • self.branch2a:3x3卷积,stride=2,输入 256x40x40 → 输出 512x20x20。
      • self.branch2b:3x3卷积,stride=1,保持 512x20x20。
      • self.short:若 variant='d',使用 AvgPool2d(2,2) + 1x1卷积(stride=1),将输入 256x40x40 → 512x20x20。
    • 输出:512x20x20(分辨率减半,通道数翻倍)。
  2. 第二个 BasicBlocki=1

    • stride1
    • shortcutTrue(输入输出通道相同,直接相加)。
    • 操作:3x3卷积,stride=1,分辨率保持 20x20。
    • 输出:512x20x20。

最终输出

  • 尺寸512x20x20(标记为 P5/32,表示相对原图下采样 32 倍)。

RTDetr (Real-Time Detection Transformer) 是一种基于Transformer架构的实时目标检测模型,特别适用于视频目标检测任务。R18通常指的是ResNet-18,它是其中的一个版本,它作为基础特征提取器。RTDetr-R18结合了Transformer的自注意力机制和卷积神经网络的优点,用于捕捉物体的全局上下文信息。 网络结构大致包括以下几个部分: 1. **输入处理**:视频帧首先通过预处理,如缩放、归一化等步骤进入网络。 2. **ResNet-18 backbone**:ResNet-18负责提取低级到高级的视觉特征。它包含一系列残差块,每个块包含两个卷积层,可以高效地处理图像特征。 3. **Feature Pyramid Network (FPN)**:用于生成多尺度的特征图,这有助于检测不同大小的目标。 4. **Spatial-Temporal Attention Module (STAM)**:这是RTDetr的独特之处,它引入了时空注意力机制,对来自不同时间步的帧进行融合,增强对动作和运动的理解。 5. **Detection Heads**:通常有两个,一个是用于预测物体的位置(Box Regressor)、类别(Class Classifier)和置信度(Objectness Score),另一个可能用于跟踪(如Track Head)。 6. **Transformer Encoder**:在这个阶段,使用自注意力机制和前馈神经网络(FFN)来进一步整合和分析特征,提高跨帧的依赖理解。 7. **Decoder and Prediction**:解码器将来自编码器的信息转换成最终的检测结果,包括框的位置、类别以及可能的额外信息。 在实际代码中,你可能会看到这样的伪代码片段: ```python class RTDETR(nn.Module): def __init__(self, resnet18_backbone): super().__init__() self.backbone = resnet18_backbone # 添加其他模块如FPN、STAM、检测头... # ... def forward(self, video_frames): features = self.backbone(video_frames) feats_fpn = self.fpn(features) feats_stam = self.stam(feats_fpn) boxes, classes, scores = self.decoder(feats_stam) return boxes, classes, scores # 实例化网络并传入ResNet-18实例 model = RTDETR(resnet18(pretrained=True)) ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值