一、本文介绍
本文给大家带来的改进机制是实现级联群体 注意力机制 CascadedGroupAttention ,其主要思想为增强输入到注意力头的特征的多样性。与以前的 自注意力 不同,它为每个头提供不同的输入分割,并跨头级联输出特征。这种方法不仅减少了多头注意力中的计算冗余,而且通过增加网络深度来提升 模型 容量, 亲测在我的25个类别的数据上,大部分的类别均有一定的涨点效果 ,仅有部分的类别保持不变,同时给该注意力机制含有二次创新的机会
欢迎大家订阅我的专栏一起学习YOLO!
二、 CascadedGroupAttention的基本原理
官方论文地址: 官方论文地址点击即可跳转
官方代码地址: 官方代码地址点击即可跳转
Cascaded Group Attention (CGA) 是在文章 "EfficientViT: Memory Efficient Vision Transformer with Cascaded Group Attention" 中提出的一种新型注意力机制。其核心思想是增强输入到注意力头的特征的多样性。与以前的自注意力不同,它为每个头提供不同的输入分割,并跨头级联输出特征。这种方法不仅减少了多头注意力中的计算冗余,而且通过增加网络深度来提升模型容量。
具体来说,CGA 将输入特征分成不同的部分,每部分输入到一个注意力头。每个头计算其自注意力映射,然后将所有头的输出级联起来,并通过一个线性层将它们投影回输入的维度。通过这样的方式,CGA 在不增加额外参数的情况下提高了模型的计算效率。另外,通过串联的方式,每个头的输出都会添加到下一个头的输入中,从而逐步精化特征表示。
Cascaded Group Attention 的优点包括:
1. 提高了注意力图的多样性。
2. 减少了计算冗余,因为它减少了 QKV 层中输入和输出通道的数量。
3. 增加了网络深度,从而进一步提高了模型容量,同时只增加了很小的延迟开销,因为每个头的 QK 通道维度较小。
这张图描绘了 "EfficientViT" 模型中 "Cascaded Group Attention" (CGA) 模块的架构。
CGA模块位于图中的(c)部分,可以看到它的作用是处理输入特征,并提供分级的注意力机制。在这个模块中,输入首先被分割成多个部分,每个部分对应一个注意力头。每个头独立地计算其自注意力,并产生一个输出。然后,所有头的输出被级联(concatenate)在一起,通过一个线性投影层形成最终的输出。这种设计允许模型在不同的层次上捕捉特征,通过级联增强了特征之间的交互,同时提高了计算效率。
级联组注意力的关键点在于每个注意力头只关注输入的一部分,然后把所有头的注意力合并起来,来获取一个全面的特征表示。这样做的好处是减少了计算重复并增加了注意力的多样性,因为不同的头可能会关注输入的不同方面。这种方法提高了模型的内存和计算效率,同时保持或增强模型的 性能 。
三、 CGA的核心代码
代码使用方式看章节四!
- # https://github.com/microsoft/Cream/blob/ef68993c764f241a768cd69a087ed567dec6cb40/EfficientViT/classification/model/efficientvit.py#L104-L181
- import itertools
- import torch
- from torch import nn
- __all__ = ['C2PSA_CGA', 'LocalWindowAttention']
- class Conv2d_BN(torch.nn.Sequential):
- def __init__(self, a, b, ks=1, stride=1, pad=0, dilation=1,
- groups=1, bn_weight_init=1, resolution=-10000):
- super().__init__()
- self.add_module('c', torch.nn.Conv2d(
- a, b, ks, stride, pad, dilation, groups, bias=False))
- self.add_module('bn', torch.nn.BatchNorm2d(b))
- torch.nn.init.constant_(self.bn.weight, bn_weight_init)
- torch.nn.init.constant_(self.bn.bias, 0)
- @torch.no_grad()
- def switch_to_deploy(self):
- c, bn = self._modules.values()
- w = bn.weight / (bn.running_var + bn.eps)**0.5
- w = c.weight * w[:, None, None, None]
- b = bn.bias - bn.running_mean * bn.weight / \
- (bn.running_var + bn.eps)**0.5
- m = torch.nn.Conv2d(w.size(1) * self.c.groups, w.size(
- 0), w.shape[2:], stride=self.c.stride, padding=self.c.padding, dilation=self.c.dilation, groups=self.c.groups)
- m.weight.data.copy_(w)
- m.bias.data.copy_(b)
- return m
- class CascadedGroupAttention(torch.nn.Module):
- r""" Cascaded Group Attention.
- Args:
- dim (int): Number of input channels.
- key_dim (int): The dimension for query and key.
- num_heads (int): Number of attention heads.
- attn_ratio (int): Multiplier for the query dim for value dimension.
- resolution (int): Input resolution, correspond to the window size.
- kernels (List[int]): The kernel size of the dw conv on query.
- """
- def __init__(self, dim, key_dim, num_heads=8,
- attn_ratio=4,
- resolution=14,
- kernels=[5, 5, 5, 5], ):
- super().__init__()
- self.num_heads = num_heads
- self.scale = key_dim ** -0.5
- self.key_dim = key_dim
- self.d = int(attn_ratio * key_dim)
- self.attn_ratio = attn_ratio
- qkvs = []
- dws = []
- for i in range(num_heads):
- qkvs.append(Conv2d_BN(dim // (num_heads), self.key_dim * 2 + self.d, resolution=resolution))
- dws.append(Conv2d_BN(self.key_dim, self.key_dim, kernels[i], 1, kernels[i] // 2, groups=self.key_dim,
- resolution=resolution))
- self.qkvs = torch.nn.ModuleList(qkvs)
- self.dws = torch.nn.ModuleList(dws)
- self.proj = torch.nn.Sequential(torch.nn.ReLU(), Conv2d_BN(
- self.d * num_heads, dim, bn_weight_init=0, resolution=resolution))
- points = list(itertools.product(range(resolution), range(resolution)))
- N = len(points)
- attention_offsets = {}
- idxs = []
- for p1 in points:
- for p2 in points:
- offset = (abs(p1[0] - p2[0]), abs(p1[1] - p2[1]))
- if offset not in attention_offsets:
- attention_offsets[offset] = len(attention_offsets)
- idxs.append(attention_offsets[offset])
- self.attention_biases = torch.nn.Parameter(
- torch.zeros(num_heads, len(attention_offsets)))
- self.register_buffer('attention_bias_idxs',
- torch.LongTensor(idxs).view(N, N))
- @torch.no_grad()
- def train(self, mode=True):
- super().train(mode)
- if mode and hasattr(self, 'ab'):
- del self.ab
- else:
- self.ab = self.attention_biases[:, self.attention_bias_idxs]
- def forward(self, x): # x (B,C,H,W)
- B, C, H, W = x.shape
- trainingab = self.attention_biases[:, self.attention_bias_idxs]
- feats_in = x.chunk(len(self.qkvs), dim=1)
- feats_out = []
- feat = feats_in[0]
- for i, qkv in enumerate(self.qkvs):
- if i > 0: # add the previous output to the input
- feat = feat + feats_in[i]
- feat = qkv(feat)
- q, k, v = feat.view(B, -1, H, W).split([self.key_dim, self.key_dim, self.d], dim=1) # B, C/h, H, W
- q = self.dws[i](q)
- q, k, v = q.flatten(2), k.flatten(2), v.flatten(2) # B, C/h, N
- attn = (
- (q.transpose(-2, -1) @ k) * self.scale
- +
- (trainingab[i] if self.training else self.ab[i])
- )
- attn = attn.softmax(dim=-1) # BNN
- feat = (v @ attn.transpose(-2, -1)).view(B, self.d, H, W) # BCHW
- feats_out.append(feat)
- x = self.proj(torch.cat(feats_out, 1))
- return x
- class LocalWindowAttention(torch.nn.Module):
- r""" Local Window Attention.
- Args:
- dim (int): Number of input channels.
- key_dim (int): The dimension for query and key.
- num_heads (int): Number of attention heads.
- attn_ratio (int): Multiplier for the query dim for value dimension.
- resolution (int): Input resolution.
- window_resolution (int): Local window resolution.
- kernels (List[int]): The kernel size of the dw conv on query.
- """
- def __init__(self, dim, num_heads=4,
- attn_ratio=4,
- resolution=14,
- window_resolution=7,
- kernels=[5, 5, 5, 5], ):
- super().__init__()
- key_dim = dim // 16 # 必须放缩16倍否则会报错
- self.dim = dim
- self.num_heads = num_heads
- self.resolution = resolution
- assert window_resolution > 0, 'window_size must be greater than 0'
- self.window_resolution = window_resolution
- self.attn = CascadedGroupAttention(dim, key_dim, num_heads,
- attn_ratio=attn_ratio,
- resolution=window_resolution,
- kernels=kernels, )
- def forward(self, x):
- B, C, H, W = x.shape
- if H <= self.window_resolution and W <= self.window_resolution:
- x = self.attn(x)
- else:
- x = x.permute(0, 2, 3, 1)
- pad_b = (self.window_resolution - H %
- self.window_resolution) % self.window_resolution
- pad_r = (self.window_resolution - W %
- self.window_resolution) % self.window_resolution
- padding = pad_b > 0 or pad_r > 0
- if padding:
- x = torch.nn.functional.pad(x, (0, 0, 0, pad_r, 0, pad_b))
- pH, pW = H + pad_b, W + pad_r
- nH = pH // self.window_resolution
- nW = pW // self.window_resolution
- # window partition, BHWC -> B(nHh)(nWw)C -> BnHnWhwC -> (BnHnW)hwC -> (BnHnW)Chw
- x = x.view(B, nH, self.window_resolution, nW, self.window_resolution, C).transpose(2, 3).reshape(
- B * nH * nW, self.window_resolution, self.window_resolution, C
- ).permute(0, 3, 1, 2)
- x = self.attn(x)
- # window reverse, (BnHnW)Chw -> (BnHnW)hwC -> BnHnWhwC -> B(nHh)(nWw)C -> BHWC
- x = x.permute(0, 2, 3, 1).view(B, nH, nW, self.window_resolution, self.window_resolution,
- C).transpose(2, 3).reshape(B, pH, pW, C)
- if padding:
- x = x[:, :H, :W].contiguous()
- x = x.permute(0, 3, 1, 2)
- return x
- def autopad(k, p=None, d=1): # kernel, padding, dilation
- """Pad to 'same' shape outputs."""
- if d > 1:
- k = d * (k - 1) + 1 if isinstance(k, int) else [d * (x - 1) + 1 for x in k] # actual kernel-size
- if p is None:
- p = k // 2 if isinstance(k, int) else [x // 2 for x in k] # auto-pad
- return p
- 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))
- class PSABlock(nn.Module):
- """
- PSABlock class implementing a Position-Sensitive Attention block for neural networks.
- This class encapsulates the functionality for applying multi-head attention and feed-forward neural network layers
- with optional shortcut connections.
- Attributes:
- attn (Attention): Multi-head attention module.
- ffn (nn.Sequential): Feed-forward neural network module.
- add (bool): Flag indicating whether to add shortcut connections.
- Methods:
- forward: Performs a forward pass through the PSABlock, applying attention and feed-forward layers.
- Examples:
- Create a PSABlock and perform a forward pass
- """
- def __init__(self, c, attn_ratio=0.5, num_heads=4, shortcut=True) -> None:
- """Initializes the PSABlock with attention and feed-forward layers for enhanced feature extraction."""
- super().__init__()
- self.attn = LocalWindowAttention(c)
- self.ffn = nn.Sequential(Conv(c, c * 2, 1), Conv(c * 2, c, 1, act=False))
- self.add = shortcut
- def forward(self, x):
- """Executes a forward pass through PSABlock, applying attention and feed-forward layers to the input tensor."""
- x = x + self.attn(x) if self.add else self.attn(x)
- x = x + self.ffn(x) if self.add else self.ffn(x)
- return x
- class C2PSA_CGA(nn.Module):
- """
- C2PSA module with attention mechanism for enhanced feature extraction and processing.
- This module implements a convolutional block with attention mechanisms to enhance feature extraction and processing
- capabilities. It includes a series of PSABlock modules for self-attention and feed-forward operations.
- Attributes:
- c (int): Number of hidden channels.
- cv1 (Conv): 1x1 convolution layer to reduce the number of input channels to 2*c.
- cv2 (Conv): 1x1 convolution layer to reduce the number of output channels to c.
- m (nn.Sequential): Sequential container of PSABlock modules for attention and feed-forward operations.
- Methods:
- forward: Performs a forward pass through the C2PSA module, applying attention and feed-forward operations.
- Notes:
- This module essentially is the same as PSA module, but refactored to allow stacking more PSABlock modules.
- Examples:
- """
- def __init__(self, c1, c2, n=1, e=0.5):
- """Initializes the C2PSA module with specified input/output channels, number of layers, and expansion ratio."""
- super().__init__()
- assert c1 == c2
- self.c = int(c1 * e)
- self.cv1 = Conv(c1, 2 * self.c, 1, 1)
- self.cv2 = Conv(2 * self.c, c1, 1)
- self.m = nn.Sequential(*(PSABlock(self.c, attn_ratio=0.5, num_heads=self.c // 64) for _ in range(n)))
- def forward(self, x):
- """Processes the input tensor 'x' through a series of PSA blocks and returns the transformed tensor."""
- a, b = self.cv1(x).split((self.c, self.c), dim=1)
- b = self.m(b)
- return self.cv2(torch.cat((a, b), 1))
- if __name__ == "__main__":
- # Generating Sample image
- image_size = (1, 64, 224, 224)
- image = torch.rand(*image_size)
- # Model
- model = C2PSA_CGA(64, 64)
- out = model(image)
- print(out.size())
四、 CGA 的添加方式
这个添加方式和之前的变了一下,以后的添加方法都按照这个来了,是为了和群内的文件适配。
4.1 修改一
第一还是建立文件,我们找到如下 ultralytics /nn文件夹下建立一个目录名字呢就是'Addmodules'文件夹( 用群内的文件的话已经有了无需新建) !然后在其内部建立一个新的py文件将核心代码复制粘贴进去即可。
4.2 修改二
第二步我们在该目录下创建一个新的py文件名字为'__init__.py'( 用群内的文件的话已经有了无需新建) ,然后在其内部导入我们的检测头如下图所示。
4.3 修改三
第三步我门中到如下文件'ultralytics/nn/tasks.py'进行导入和注册我们的模块( 用群内的文件的话已经有了无需重新导入直接开始第四步即可) !
从今天开始以后的教程就都统一成这个样子了,因为我默认大家用了我群内的文件来进行修改!!
4.4 修改四
按照我的添加在parse_model里添加即可。
五、 CGA 的yaml文件和运行记录
5.1 CGA 的yaml文件一(推荐)
此版本训练信息:YOLO11-C2PSA-CGA summary: 340 layers, 2,567,615 parameters, 2,567,599 gradients, 6.4 GFLOPs
- # Ultralytics YOLO 🚀, AGPL-3.0 license
- # YOLO11 object detection model with P3-P5 outputs. For Usage examples see https://docs.ultralytics.com/tasks/detect
- # Parameters
- nc: 80 # number of classes
- scales: # model compound scaling constants, i.e. 'model=yolo11n.yaml' will call yolo11.yaml with scale 'n'
- # [depth, width, max_channels]
- n: [0.50, 0.25, 1024] # summary: 319 layers, 2624080 parameters, 2624064 gradients, 6.6 GFLOPs
- s: [0.50, 0.50, 1024] # summary: 319 layers, 9458752 parameters, 9458736 gradients, 21.7 GFLOPs
- m: [0.50, 1.00, 512] # summary: 409 layers, 20114688 parameters, 20114672 gradients, 68.5 GFLOPs
- l: [1.00, 1.00, 512] # summary: 631 layers, 25372160 parameters, 25372144 gradients, 87.6 GFLOPs
- x: [1.00, 1.50, 512] # summary: 631 layers, 56966176 parameters, 56966160 gradients, 196.0 GFLOPs
- # YOLO11n backbone
- backbone:
- # [from, repeats, module, args]
- - [-1, 1, Conv, [64, 3, 2]] # 0-P1/2
- - [-1, 1, Conv, [128, 3, 2]] # 1-P2/4
- - [-1, 2, C3k2, [256, False, 0.25]]
- - [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
- - [-1, 2, C3k2, [512, False, 0.25]]
- - [-1, 1, Conv, [512, 3, 2]] # 5-P4/16
- - [-1, 2, C3k2, [512, True]]
- - [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32
- - [-1, 2, C3k2, [1024, True]]
- - [-1, 1, SPPF, [1024, 5]] # 9
- - [-1, 2, C2PSA_CGA, [1024]] # 10
- # YOLO11n head
- head:
- - [-1, 1, nn.Upsample, [None, 2, "nearest"]]
- - [[-1, 6], 1, Concat, [1]] # cat backbone P4
- - [-1, 2, C3k2, [512, False]] # 13
- - [-1, 1, nn.Upsample, [None, 2, "nearest"]]
- - [[-1, 4], 1, Concat, [1]] # cat backbone P3
- - [-1, 2, C3k2, [256, False]] # 16 (P3/8-small)
- - [-1, 1, Conv, [256, 3, 2]]
- - [[-1, 13], 1, Concat, [1]] # cat head P4
- - [-1, 2, C3k2, [512, False]] # 19 (P4/16-medium)
- - [-1, 1, Conv, [512, 3, 2]]
- - [[-1, 10], 1, Concat, [1]] # cat head P5
- - [-1, 2, C3k2, [1024, True]] # 22 (P5/32-large)
- - [[16, 19, 22], 1, Detect, [nc]] # Detect(P3, P4, P5)
5.2 CGA 的yaml文件二
此版本的训练信息:YOLO11-CGA summary: 418 layers, 2,718,839 parameters, 2,718,823 gradients, 6.7 GFLOPs
- # Ultralytics YOLO 🚀, AGPL-3.0 license
- # YOLO11 object detection model with P3-P5 outputs. For Usage examples see https://docs.ultralytics.com/tasks/detect
- # Parameters
- nc: 80 # number of classes
- scales: # model compound scaling constants, i.e. 'model=yolo11n.yaml' will call yolo11.yaml with scale 'n'
- # [depth, width, max_channels]
- n: [0.50, 0.25, 1024] # summary: 319 layers, 2624080 parameters, 2624064 gradients, 6.6 GFLOPs
- s: [0.50, 0.50, 1024] # summary: 319 layers, 9458752 parameters, 9458736 gradients, 21.7 GFLOPs
- m: [0.50, 1.00, 512] # summary: 409 layers, 20114688 parameters, 20114672 gradients, 68.5 GFLOPs
- l: [1.00, 1.00, 512] # summary: 631 layers, 25372160 parameters, 25372144 gradients, 87.6 GFLOPs
- x: [1.00, 1.50, 512] # summary: 631 layers, 56966176 parameters, 56966160 gradients, 196.0 GFLOPs
- # YOLO11n backbone
- backbone:
- # [from, repeats, module, args]
- - [-1, 1, Conv, [64, 3, 2]] # 0-P1/2
- - [-1, 1, Conv, [128, 3, 2]] # 1-P2/4
- - [-1, 2, C3k2, [256, False, 0.25]]
- - [-1, 1, Conv, [256, 3, 2]] # 3-P3/8
- - [-1, 2, C3k2, [512, False, 0.25]]
- - [-1, 1, Conv, [512, 3, 2]] # 5-P4/16
- - [-1, 2, C3k2, [512, True]]
- - [-1, 1, Conv, [1024, 3, 2]] # 7-P5/32
- - [-1, 2, C3k2, [1024, True]]
- - [-1, 1, SPPF, [1024, 5]] # 9
- - [-1, 2, C2PSA, [1024]] # 10
- # YOLO11n head
- head:
- - [-1, 1, nn.Upsample, [None, 2, "nearest"]]
- - [[-1, 6], 1, Concat, [1]] # cat backbone P4
- - [-1, 2, C3k2, [512, False]] # 13
- - [-1, 1, nn.Upsample, [None, 2, "nearest"]]
- - [[-1, 4], 1, Concat, [1]] # cat backbone P3
- - [-1, 2, C3k2, [256, False]] # 16 (P3/8-small)
- - [-1, 1, LocalWindowAttention, []] # 17 (P3/8-small) 小目标检测层输出位置增加注意力机制
- - [-1, 1, Conv, [256, 3, 2]]
- - [[-1, 13], 1, Concat, [1]] # cat head P4
- - [-1, 2, C3k2, [512, False]] # 20 (P4/16-medium)
- - [-1, 1, LocalWindowAttention, []] # 21 (P4/16-medium) 中目标检测层输出位置增加注意力机制
- - [-1, 1, Conv, [512, 3, 2]]
- - [[-1, 10], 1, Concat, [1]] # cat head P5
- - [-1, 2, C3k2, [1024, True]] # 24 (P5/32-large)
- - [-1, 1, LocalWindowAttention, []] # 25 (P5/32-large) 大目标检测层输出位置增加注意力机制
- # 注意力机制我这里其实是添加了三个但是实际一般生效就只添加一个就可以了,所以大家可以自行注释来尝试, 上面三个仅建议大家保留一个, 但是from位置要对齐.
- # 具体在那一层用注意力机制可以根据自己的数据集场景进行选择。
- # 如果你自己配置注意力位置注意from[17, 21, 25]位置要对应上对应的检测层!
- - [[17, 21, 25], 1, Detect, [nc]] # Detect(P3, P4, P5)
5.3 CGA 的训练过程截图
五、本文总结
到此本文的正式分享内容就结束了,在这里给大家推荐我的YOLOv11改进有效涨点专栏,本专栏目前为新开的平均质量分98分,后期我会根据各种最新的前沿顶会进行论文复现,也会对一些老的改进机制进行补充,如果大家觉得本文帮助到你了,订阅本专栏,关注后续更多的更新~