RT-DETR系列之AIFI

RT-DETR系列之AIFI

还是那张图,我们学习了Backbone后,接下来学习AIFI(Intra-scale feature interaction)部分是干什么的-----单尺度内的特征交互。简而言之就是如图。输入为S5,输出为F5。

首先我们来看模型的配置文件

nc: 80 # number of classes

scales: # model compound scaling constants, i.e. 'model=yolov8n-cls.yaml' will call yolov8-cls.yaml with scale 'n'

# [depth, width, max_channels]

l: [1.00, 1.00, 1024]

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

head:

- [-1, 1, Conv, [256, 1, 1, None, 1, 1, False]] # 8 input_proj.2

- [-1, 1, AIFI, [1024, 8]] # 9

- [-1, 1, Conv, [256, 1, 1]] # 10, Y5, lateral_convs.0

- [-1, 1, nn.Upsample, [None, 2, 'nearest']] # 11

- [6, 1, Conv, [256, 1, 1, None, 1, 1, False]] # 12 input_proj.1

- [[-2, -1], 1, Concat, [1]] # 13

- [-1, 3, RepC3, [256, 0.5]] # 14, fpn_blocks.0

- [-1, 1, Conv, [256, 1, 1]] # 15, Y4, lateral_convs.1

- [-1, 1, nn.Upsample, [None, 2, 'nearest']] # 16

- [5, 1, Conv, [256, 1, 1, None, 1, 1, False]] # 17 input_proj.0

- [[-2, -1], 1, Concat, [1]] # 18 cat backbone P4

- [-1, 3, RepC3, [256, 0.5]] # X3 (19), fpn_blocks.1

- [-1, 1, Conv, [256, 3, 2]] # 20, downsample_convs.0

- [[-1, 15], 1, Concat, [1]] # 21 cat Y4

- [-1, 3, RepC3, [256, 0.5]] # F4 (22), pan_blocks.0

- [-1, 1, Conv, [256, 3, 2]] # 23, downsample_convs.1

- [[-1, 10], 1, Concat, [1]] # 24 cat Y5

- [-1, 3, RepC3, [256, 0.5]] # F5 (25), pan_blocks.1

- [[19, 22, 25], 1, RTDETRDecoder, [nc, 256, 300, 4, 8, 3]] # Detect(P3, P4, P5)

这次我们就来学习有关AIFI的这部分

- [-1, 1, Conv, [256, 1, 1, None, 1, 1, False]] # 8 input_proj.2

- [-1, 1, AIFI, [1024, 8]] # 9

- [-1, 1, Conv, [256, 1, 1]] # 10, Y5, lateral_convs.0

这里的第一行代码很显然是一个卷积,但是我们在Backbone中也涉及到卷积操作,名字(ConvNormLayer)和这个(Conv)不一样 。所以首先学习一下,这两个有什么区别:

Backbone中卷积操作ConvNormLayer和head中卷积操作Conv的区别

先分别展示他们两个的源代码

ConvNormLayer源代码

class ConvNormLayer(nn.Module):

def __init__(self, ch_in, ch_out, kernel_size, stride, padding=None, bias=False, act=None):

super().__init__()

self.conv = nn.Conv2d(

ch_in,

ch_out,

kernel_size,

stride,

padding=(kernel_size-1)//2 if padding is None else padding,

bias=bias)

self.norm = nn.BatchNorm2d(ch_out)

self.act = nn.Identity() if act is None else get_activation(act)

def forward(self, x):

return self.act(self.norm(self.conv(x)))

def forward_fuse(self, x):

"""Perform transposed convolution of 2D data."""

return self.act(self.conv(x))

Conv源代码

class Conv(nn.Module):

"""Standard convolution with args(ch_in, ch_out, kernel, stride, padding, groups, dilation, activation)."""

default_act = nn.SiLU() # default activation

def __init__(self, c1, c2, k=1, s=1, p=None, g=1, d=1, act=True):

"""Initialize Conv layer with given arguments including activation."""

super().__init__()

self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p, d), groups=g, dilation=d, bias=False)

self.bn = nn.BatchNorm2d(c2)

self.act = self.default_act if act is True else act if isinstance(act, nn.Module) else nn.Identity()

def forward(self, x):

"""Apply convolution, batch normalization and activation to input tensor."""

return self.act(self.bn(self.conv(x)))

def forward_fuse(self, x):

"""Perform transposed convolution of 2D data."""

return self.act(self.conv(x))

区别

首先第一个区别,卷积nn.Conv2d 中参数设置不一样

ConvNormLayer中的nn.Conv2d

self.conv = nn.Conv2d(

ch_in,

ch_out,

kernel_size,

stride,

padding=(kernel_size-1)//2 if padding is None else padding,

bias=bias)

Conv中的nn.Conv2d

self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p, d), groups=g, dilation=d, bias=False)

,前4个参数是一样的,都是输入通道数(ConvNormLayer中的ch_in和Conv中的c1),输出通道数(ch_out和c2),卷积核(kernel_size和k),步长(stride和s)。从第5个参数就不一样了。先看ConvNormLayer中剩下的参数,padding和bias。就是设置了简单步长和偏置。Conv中的就更加复杂。

autopad(k, p, d):自动填充计算

根据卷积核大小 k、膨胀率 d 和用户指定的填充 p,自动计算卷积层的填充值,确保输出尺寸与输入尺寸匹配(当 stride=1 时)。

当 p=None 时,默认填充方式为 "SAME" 填充(保持输入输出尺寸一致)。

groups=g:分组卷积

将输入和输出的通道分成 g 组,每组独立进行卷积操作。

优势:减少参数量和计算量,提升模型效率。常用于深度可分类卷积。

dilation=d:膨胀卷积

通过控制卷积核的膨胀率(空洞间隔),扩大感受野而不增加参数数量。

用于捕获大范围上下文信息(如焊接缺陷的全局分布)。

卷积核结构

膨胀率 d=1:普通卷积核(无间隔)

膨胀率 d=2:卷积核元素间隔1个像素(感受野等效于 k + (k-1)*(d-1))

示例

卷积核 k=3,膨胀率 d=2:

实际覆盖区域:3x3 → 等效 5x5 的感受野(但仅用 9 个参数)。

像素间隔

区别二:

激活函数的配置

先看 ConvNormLayer的

self.act = nn.Identity() if act is None else get_activation(act)

默认无激活(act=None 时使用 nn.Identity),需手动指定激活函数

再看Conv的

default_act = nn.SiLU() # default activation

self.act = self.default_act if act is True else act if isinstance(act, nn.Module) else nn.Identity()

默认使用 SiLU 激活(即 Swish 激活),兼顾 ReLU 的非线性和 Sigmoid 的平滑性

总结:

Conv:

当需要灵活配置膨胀/分组卷积,或希望利用 SiLU 激活函数增强非线性表达能力时(尤其适合检测小目标)。

ConvNormLayer:

在基础特征提取层(如主干网络前半部分),追求计算效率且无需复杂卷积配置时。

现在我们来看

配置文件第一行

- [-1, 1, Conv, [256, 1, 1, None, 1, 1, False]] # 8 input_proj.2

回忆一下,最开始我们的输入图片为3*640*640,经过Backbone后,最后一层图片输出为512x20x20。

对应参数解析:

输出通道数 c2=256

卷积核大小 k=1×1k=1×1

步长 s=1s=1

填充 p=Nonep=None(自动计算为 00)

分组数 g=1g=1

膨胀率 d=1d=1

激活函数 act=False

输出计算

空间尺寸

输入尺寸为 Hin×Win=20×20,卷积核为 1×1,步长 s=1,填充 p=0,膨胀率 d=1。

输出尺寸公式:

Hout=[Hin+2p−d×(k−1)−1]/s+1=20

同理 Wout=20。

通道数

输入通道数 c1=512 → 输出通道数 c2​=256。

作用总结

通道降维:将通道数从 512 压缩至 256,减少后续计算量。

特征融合:通过 1×1卷积实现跨通道信息融合,增强特征表达能力。

保持分辨率:不改变空间尺寸(无下采样),保留缺陷定位精度。

归一化:通过内置的 BatchNorm2d 稳定训练过程。

配置文件第二行

- [-1, 1, AIFI, [1024, 8]] # 9

输入通道数为 1024。多头注意力机制中的头数为 8

接下来看AIFI的源码

class AIFI(TransformerEncoderLayer):

"""Defines the AIFI transformer layer."""

def __init__(self, c1, cm=2048, num_heads=8, dropout=0, act=nn.GELU(), normalize_before=False):

"""Initialize the AIFI instance with specified parameters."""

super().__init__(c1, cm, num_heads, dropout, act, normalize_before)

def forward(self, x):

"""Forward pass for the AIFI transformer layer."""

c, h, w = x.shape[1:]

pos_embed = self.build_2d_sincos_position_embedding(w, h, c)

# Flatten [B, C, H, W] to [B, HxW, C]

x = super().forward(x.flatten(2).permute(0, 2, 1), pos=pos_embed.to(device=x.device, dtype=x.dtype))

return x.permute(0, 2, 1).view([-1, c, h, w]).contiguous()

@staticmethod

def build_2d_sincos_position_embedding(w, h, embed_dim=256, temperature=10000.0):

"""Builds 2D sine-cosine position embedding."""

grid_w = torch.arange(int(w), dtype=torch.float32)

grid_h = torch.arange(int(h), dtype=torch.float32)

grid_w, grid_h = torch.meshgrid(grid_w, grid_h, indexing='ij')

assert embed_dim % 4 == 0, \

'Embed dimension must be divisible by 4 for 2D sin-cos position embedding'

pos_dim = embed_dim // 4

omega = torch.arange(pos_dim, dtype=torch.float32) / pos_dim

omega = 1. / (temperature ** omega)

out_w = grid_w.flatten()[..., None] @ omega[None]

out_h = grid_h.flatten()[..., None] @ omega[None]

return torch.cat([torch.sin(out_w), torch.cos(out_w), torch.sin(out_h), torch.cos(out_h)], 1)[None]

初始化

c1: 输入通道数。cm: 中间特征维度,默认为 2048。num_heads: 多头注意力机制中的头数,默认为 8。dropout: dropout 概率,默认为 0。act: 激活函数,默认为 GELU。normalize_before: 是否在注意力机制之前进行归一化,默认为 False。

super().__init__(c1, cm, num_heads, dropout, act, normalize_before) 调用基类 TransformerEncoderLayer 的初始化方法。

前向传播

获取形状信息:

c, h, w = x.shape[1:] 提取输入特征图的通道数、高度和宽度。x.shape 将返回一个包含四个元素的元组 (B, C, H, W)。x.shape[1:] 会跳过第一个元素(即跳过批量大小 B),取后面的部分,即 (C, H, W)。c, h, w = x.shape[1:] 是一种 Python 的多变量赋值(解包)语法,它将元组 (C, H, W) 解包为三个独立的变量 c、h 和 w,分别表示通道数、图像高度和图像宽度。

生成位置嵌入:

pos_embed = self.build_2d_sincos_position_embedding(w, h, c) 生成 2D 正弦-余弦位置嵌入。

特征处理:

x.flatten(2).permute(0, 2, 1) 将输入特征从 [B, C, H, W] 变换为 [B, H*W, C],以适应 transformer 输入格式。

x.flatten(2) 会将张量 x 从第 2 个维度开始展平(flatten),这意味着它会将 H 和 W 维度展平为一个维度。展平后的张量形状会变成 [B, C, H*W]。

例如,如果 x 的形状是 [B, C, H, W]:x.flatten(2) 的形状会变成 [B, C, H*W]。permute 方法x.flatten(2).permute(0, 2, 1) 会对展平后的张量进行维度重排。permute 方法会改变张量的维度顺序。在这个例子中,permute(0, 2, 1) 表示将第 0 个维度保持不变,将第 2 个维度移到第 1 个位置,将第 1 个维度移到第 2 个位置。具体来说:0 代表批量大小 B。2 代表展平后的维度 H*W。1 代表通道数 C,因此,x.flatten(2).permute(0, 2, 1) 会将形状从 [B, C, H*W] 变成 [B, H*W, C]

super().forward() 调用基类的前向传播方法,传入处理后的特征和位置嵌入。x.permute(0, 2, 1).view([-1, c, h, w]).contiguous() 将输出特征重整为 [B, C, H, W] 形状。

x.permute(0, 2, 1) 会改变张量的维度顺序。permute(0, 2, 1) 表示将第 0 个维度保持不变,将第 2 个维度移到第 1 个位置,将第 1 个维度移到第 2 个位置。

具体来说:0 代表批量大小 B。2 代表特征维度 C。1 代表序列长度 N。因此,x.permute(0, 2, 1) 会将形状从 [B, N, C] 变成 [B, C, N]。view 方法view 方法用于重新整形张量。x.permute(0, 2, 1).view([-1, c, h, w]) 将张量重新整形为 [B, C, H, W]。假设:B 是批量大小。C 是通道数。H 和 W 是图像的高度和宽度,且 N = H * W。那么 x.permute(0, 2, 1) 的形状是 [B, C, N],即 [B, C, H * W]。view([-1, c, h, w]) 将其重新整形为 [B, C, H, W]。其中 -1 表示自动推断出批量大小 B。contiguous 方法contiguous 方法用于确保张量在内存中的存储是连续的。经过 permute 和 view 操作后,张量在内存中的存储可能不再是连续的,contiguous 会创建一个新的连续张量。

2D 正弦-余弦位置嵌入

生成网格:

grid_w 和 grid_h 分别生成宽度和高度方向的网格坐标。torch.arange

torch.arange 是 PyTorch 中生成序列数据的方法。其常见用法是 torch.arange(start, end, step),其中:start 是序列的起始值(默认为0)。end 是序列的终止值(不包含此值)。step 是序列中相邻值之间的间隔(默认为1)。在这行代码中,只用了一个参数 end,默认为从 0 开始,到 int(w) 结束(不包括 int(w) 本身),步长为 1。3. dtype=torch.float32,dtype 参数指定生成的张量的数据类型。在这里,dtype=torch.float32 表示生成的张量中的元素将是 32 位浮点数。

grid_w, grid_h = torch.meshgrid(grid_w, grid_h, indexing='ij') 创建 2D 网格。torch.meshgrid 是 PyTorch 中用于生成网格坐标的方法。其常见用法是 torch.meshgrid(tensor1, tensor2, ..., indexing='xy' or 'ij')。它接受多个一维张量,并返回相应的多维网格张量。'ij':基于矩阵索引顺序,返回的网格是 (i, j) 形式,即网格的第一个维度是 i 方向,第二个维度是 j 方向。

计算 Omega:

检查 embed_dim 是否可以被 4 整除

assert embed_dim % 4 == 0

为了进行2D正弦-余弦位置嵌入,嵌入维度 embed_dim 必须是 4 的倍数。这个检查是为了确保后续的计算能够正确进行。

pos_dim = embed_dim // 4

把 embed_dim 除以 4 得到 pos_dim。这是因为对于 2D 正弦-余弦位置嵌入,每个维度会使用 4 个值来进行编码(sin 和 cos 各两个方向)。

omega = torch.arange(pos_dim, dtype=torch.float32) / pos_dim 生成 omega 值。这里使用 torch.arange(pos_dim, dtype=torch.float32) 生成一个从 0 到 pos_dim-1 的一维张量。然后将这个张量除以 pos_dim,以得到一个在 [0, 1) 范围内的值序列。omega = 1. / (temperature ** omega) 通过温度参数进行缩放。

temperature 这里通常是一个常数,用于控制位置编码的尺度。通常在位置编码中,temperature 被设定为一个较大的值,如 10000。这意味着 omega 序列中的每个值都是一个倒数,基于指数增长的方式。

总结

omega 是一个包含不同频率倒数的序列,这些频率用于生成 2D 正弦-余弦位置编码的频率成分。通过这些频率,位置编码能够有效地表示输入序列中每个位置的信息,并帮助模型捕捉序列中位置间的相对关系。

计算正弦和余弦嵌入:

out_w 和 out_h 分别计算宽度和高度方向的正弦和余弦值。torch.cat([...], 1)[None] 将所有嵌入值合并为 [1, H*W, embed_dim] 形状。

@ 是矩阵乘法运算符。这里我们进行的是矩阵乘法,将两个二维张量相乘:grid_w.flatten()[..., None] 的形状为 (H * W, 1)。omega[None] 的形状为 (1, pos_dim)。矩阵乘法的结果 out_w 的形状为 (H * W, pos_dim),因为 (H * W, 1) @ (1, pos_dim) = (H * W, pos_dim)。同样,out_h 的形状也是 (H * W, pos_dim)。用途和意义out_w 和 out_h 是位置编码的一部分,它们表示网格中每个位置在宽度和高度方向上的编码。grid_w.flatten()[..., None] @ omega[None] 计算得到的 out_w 表示网格中所有位置的宽度编码。grid_h.flatten()[..., None] @ omega[None] 计算得到的 out_h 表示网格中所有位置的高度编码。

输出为1024x20 x 20

配置文件第三行

- [-1, 1, Conv, [256, 1, 1]]

通过一个1*1的卷积,输出为256x20x20

AIFI 作用总结(作用于P5(S5)层,生成F5层)

Transformer 编码层: AIFI 类继承自 TransformerEncoderLayer,意味着它主要功能是实现一种基于 Transformer 的编码层。

位置嵌入: 在前向传播过程中,AIFI 类生成二维正弦-余弦位置嵌入(2D Sin-Cos Positional Embedding),这在处理图像数据时很常见,因为图像的空间信息对模型理解非常重要。

特征处理:

输入特征格式变换:输入特征的形状为 [B, C, H, W](批量大小 B,通道数 C,高度 H,宽度 W),在处理前被展平并转置为 [B, H*W, C] 的格式,这样更适合 Transformer 模型处理。位置嵌入添加:位置嵌入被添加到特征中,以提供位置信息。Transformer 处理:经过 Transformer 编码层处理后,特征被转置回原始格式 [B, C, H, W]。

相关推荐