YOLOv5改进系列(16)——添加EMA注意力机制(ICASSP2023|实测涨点)
🚀一、EMA介绍
- 论文题目:《Efficient Multi-Scale Attention Module with Cross-Spatial Learning》
- 原文链接:[2305.13563v1] Efficient Multi-Scale Attention Module with Cross-Spatial Learning (arxiv.org)
1.1 简介
通道或空间的显著有效性注意机制对产生更多可辨识的特征表示的显著效果,然而用通道降维对跨通道关系进行建模关系,可能会给提取深层视觉表征带来副作用。比如CA注意力机制,虽对精度有一定的提升但是由于需要对整个特征图进行注意力权重的计算,因此需要额外的计算导致消耗资源,另外无法捕捉通道之间长距离的依赖关系。
深圳的航天科工2023年6月提出的新型的高效多尺度注意力EMA模块,该模块首先将部分通道维度重塑为批量维度,以避免通用卷积进行某种形式的降维;接着在每个并行子网络中构建局部的跨通道交互,利用一种新的跨空间学习方法融合两个并行子网络的输出特征图,设计了一个多尺度并行子网络来建立短和长依赖关系。EMA的网络结构图如图所示。

🚀二、在Neck端添加EMA注意力机制方法
2.1 添加顺序
(1)models/common.py --> 加入新增的网络结构
(2) models/yolo.py --> 设定网络结构的传参细节,将EMA类名加入其中。(当新的自定义模块中存在输入输出维度时,要使用qw调整输出维度)
(3) models/yolov5*.yaml --> 新建一个文件夹,如yolov5s_EMA.yaml,修改现有模型结构配置文件。(当引入新的层时,要修改后续的结构中的from参数)
(4) train.py --> 修改‘--cfg’默认参数,训练时指定模型结构配置文件
2.2 具体添加步骤
第①步:在common.py中添加EMA模块
第一步,通过与CA类似的处理,将两个编码特征连接在图像高度方向上,并使其共享相同的1×1卷积,而不会降低1x1分支的维数。在将1x1卷积的输出分解为2个向量后,使用两个非线性Sigmid函数来拟合线性卷积上的2D二进制分布。为了在1×1分支中的两个平行路线之间实现不同的跨通道交互特征,通过简单的乘法将每组内的两个通道注意力图聚合在一起。

第二步,3×3分支通过卷积捕获局部跨通道交互,以扩大特征空间。

这样,EMA不仅对通道间信息进行编码以调整不同通道的重要性,而目将精确的空间结构信息保存到通道中。
将下面的EMA代码复制粘贴到common.py文件的末尾。
- #EMA
- class EMA(nn.Module):
- def __init__(self, channels, factor=8):
- super(EMA, self).__init__()
- self.groups = factor # 分组因子
- assert channels // self.groups > 0
- self.softmax = nn.Softmax(-1) #softmax操作
- self.agp = nn.AdaptiveAvgPool2d((1, 1)) # 1×1平均池化层
- self.pool_h = nn.AdaptiveAvgPool2d((None, 1)) # X平均池化层 h=1
- self.pool_w = nn.AdaptiveAvgPool2d((1, None)) # Y平均池化层 w=1
- self.gn = nn.GroupNorm(channels // self.groups, channels // self.groups) # 分组操作
- self.conv1x1 = nn.Conv2d(channels // self.groups, channels // self.groups, kernel_size=1, stride=1, padding=0) # 1×1卷积分支
- self.conv3x3 = nn.Conv2d(channels // self.groups, channels // self.groups, kernel_size=3, stride=1, padding=1) # 3×3卷积分支
- def forward(self, x):
- b, c, h, w = x.size()
- group_x = x.reshape(b * self.groups, -1, h, w) # b*g,c//g,h,w
- x_h = self.pool_h(group_x) # 得到平均池化之后的h
- x_w = self.pool_w(group_x).permute(0, 1, 3, 2) # 得到平均池化之后的w
- hw = self.conv1x1(torch.cat([x_h, x_w], dim=2)) # 先拼接,然后送入1×1卷积
- x_h, x_w = torch.split(hw, [h, w], dim=2)
- x1 = self.gn(group_x * x_h.sigmoid() * x_w.permute(0, 1, 3, 2).sigmoid())
- x2 = self.conv3x3(group_x) # 3×3卷积分支
- x11 = self.softmax(self.agp(x1).reshape(b * self.groups, -1, 1).permute(0, 2, 1))
- x12 = x2.reshape(b * self.groups, c // self.groups, -1) # b*g, c//g, hw
- x21 = self.softmax(self.agp(x2).reshape(b * self.groups, -1, 1).permute(0, 2, 1))
- x22 = x1.reshape(b * self.groups, c // self.groups, -1) # b*g, c//g, hw
- weights = (torch.matmul(x11, x12) + torch.matmul(x21, x22)).reshape(b * self.groups, 1, h, w)
- return (group_x * weights.sigmoid()).reshape(b, c, h, w)
如下图所示:

第②步:在yolo.py文件里的parse_model函数加入类名
首先找到yolo.py里面parse_model函数的这一行。

然后把刚才加入的类EMA添加到这个注册表里面:

第③步:创建自定义的yaml文件
首先在models文件夹下复制yolov5s.yaml 文件,粘贴并重命名为 yolov5s_EMA.yaml

接着修改 yolov5s_EMA.yaml ,将EMA模块加到我们想添加的位置。 这里先演示的是在Neck端添加。
复制下面代码,粘贴到刚才新创建的yaml文件。
- # YOLOv5 🚀 by Ultralytics, GPL-3.0 license
- # Parameters
- nc: 1 # number of classes
- depth_multiple: 0.33 # model depth multiple
- width_multiple: 0.50 # layer channel multiple
- anchors:
- - [10,13, 16,30, 33,23] # P3/8
- - [30,61, 62,45, 59,119] # P4/16
- - [116,90, 156,198, 373,326] # P5/32
- # YOLOv5 v6.0 backbone
- backbone:
- # [from, number, module, args]
- [[-1, 1, Conv, [64, 6, 2, 2]], # 0-P1/2
- [-1, 1, Conv, [128, 3, 2]], # 1-P2/4
- [-1, 3, C3, [128]],
- [-1, 1, Conv, [256, 3, 2]], # 3-P3/8
- [-1, 6, C3, [256]],
- [-1, 1, Conv, [512, 3, 2]], # 5-P4/16
- [-1, 9, C3, [512]],
- [-1, 1, Conv, [1024, 3, 2]], # 7-P5/32
- [-1, 3, C3, [1024]],
- [-1, 1, SPPF, [1024, 5]], # 9
- ]
- # YOLOv5 v6.0 head
- head:
- [[-1, 1, Conv, [512, 1, 1]],
- [-1, 1, nn.Upsample, [None, 2, 'nearest']],
- [[-1, 6], 1, Concat, [1]], # cat backbone P4
- [-1, 3, C3, [512, False]], # 13
- [-1, 1, Conv, [256, 1, 1]],
- [-1, 1, nn.Upsample, [None, 2, 'nearest']],
- [[-1, 4], 1, Concat, [1]], # cat backbone P3
- [-1, 3, C3, [256, False]], # 17 (P3/8-small)
- [-1, 1, EMA, [256]], # 加入到小目标层后
- [-1, 1, Conv, [256, 3, 2]],
- [[-1, 14], 1, Concat, [1]], # cat head P4
- [-1, 3, C3, [512, False]], # 20 (P4/16-medium)
- [-1, 1, EMA, [512]], # 加入到中目标层后
- [-1, 1, Conv, [512, 3, 2]],
- [[-1, 10], 1, Concat, [1]], # cat head P5
- [-1, 3, C3, [1024, False]], # 23 (P5/32-large)
- [-1, 1, EMA, [1024]], # 加入到大目标层后
- [[18, 22, 26], 1, Detect, [nc, anchors]], # Detect(P3, P4, P5)
- ]
第④步:验证是否加入成功
运行yolo.py

这样就修改成功了~
第⑤步:修改train.py中 ‘--cfg’默认参数
我们先找到 train.py 文件的parse_opt函数,然后将第二行‘--cfg’的default改为yolov5s_EMA.yaml,然后就可以开始训练啦~

🚀三、在C3中添加EMA注意力机制方法
3.1 具体添加步骤
第①步:在common.py中添加C3_EMA模块
将下面的代码复制粘贴到common.py文件的末尾。
- #EMA
- class EMA(nn.Module):
- def __init__(self, channels, factor=8):
- super(EMA, self).__init__()
- self.groups = factor # 分组率
- assert channels // self.groups > 0
- self.softmax = nn.Softmax(-1) # Softmax
- self.agp = nn.AdaptiveAvgPool2d((1, 1)) # 平均池化层
- self.pool_h = nn.AdaptiveAvgPool2d((None, 1)) # x平均池化层 h=1
- self.pool_w = nn.AdaptiveAvgPool2d((1, None)) # y平均池化层 w=1
- self.gn = nn.GroupNorm(channels // self.groups, channels // self.groups) # 分组操作
- self.conv1x1 = nn.Conv2d(channels // self.groups, channels // self.groups, kernel_size=1, stride=1, padding=0) # 1×1卷积分支
- self.conv3x3 = nn.Conv2d(channels // self.groups, channels // self.groups, kernel_size=3, stride=1, padding=1) # 3×3卷积分支
- def forward(self, x):
- b, c, h, w = x.size()
- group_x = x.reshape(b * self.groups, -1, h, w) # b*g,c//g,h,w
- x_h = self.pool_h(group_x)
- x_w = self.pool_w(group_x).permute(0, 1, 3, 2)
- hw = self.conv1x1(torch.cat([x_h, x_w], dim=2))
- x_h, x_w = torch.split(hw, [h, w], dim=2)
- x1 = self.gn(group_x * x_h.sigmoid() * x_w.permute(0, 1, 3, 2).sigmoid())
- x2 = self.conv3x3(group_x)
- x11 = self.softmax(self.agp(x1).reshape(b * self.groups, -1, 1).permute(0, 2, 1))
- x12 = x2.reshape(b * self.groups, c // self.groups, -1) # b*g, c//g, hw
- x21 = self.softmax(self.agp(x2).reshape(b * self.groups, -1, 1).permute(0, 2, 1))
- x22 = x1.reshape(b * self.groups, c // self.groups, -1) # b*g, c//g, hw
- weights = (torch.matmul(x11, x12) + torch.matmul(x21, x22)).reshape(b * self.groups, 1, h, w)
- return (group_x * weights.sigmoid()).reshape(b, c, h, w)
- class C3_EMA3(nn.Module):
- # CSP Bottleneck with 3 convolutions
- def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): # ch_in, ch_out, number, shortcut, groups, expansion
- super().__init__()
- c_ = int(c2 * e) # hidden channels
- self.cv1 = Conv(c1, c_, 1, 1)
- self.cv2 = Conv(c1, c_, 1, 1)
- self.cv3 = Conv(2 * c_, c2, 1) # optional act=FReLU(c2)
- self.m = nn.Sequential(*(Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)))
- self.m1 = nn.ModuleList([EMA(2 * c_)]) # 添加在最后一个卷积之前
- def forward(self, x):
- return self.cv3(self.m1[0](torch.cat((self.m(self.cv1(x)), self.cv2(x)), 1)))
- class C3_EMA2(nn.Module):
- # CSP Bottleneck with 3 convolutions
- def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): # ch_in, ch_out, number, shortcut, groups, expansion
- super().__init__()
- c_ = int(c2 * e) # hidden channels
- self.cv1 = Conv(c1, c_, 1, 1)
- self.cv2 = Conv(c1, c_, 1, 1)
- self.cv3 = Conv(2 * c_, c2, 1) # optional act=FReLU(c2)
- self.m = nn.Sequential(*(Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)))
- self.m1 = nn.ModuleList([EMA(c1)]) # 添加在最后一个卷积之前
- def forward(self, x):
- return self.cv3(torch.cat((self.m(self.cv1(x)), self.cv2(self.m1[0](x))), 1))
- class C3_EMA1(nn.Module):
- # CSP Bottleneck with 3 convolutions
- def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): # ch_in, ch_out, number, shortcut, groups, expansion
- super().__init__()
- c_ = int(c2 * e) # hidden channels
- self.cv1 = Conv(c1, c_, 1, 1)
- self.cv2 = Conv(c1, c_, 1, 1)
- self.cv3 = Conv(2 * c_, c2, 1) # optional act=FReLU(c2)
- self.m = nn.Sequential(*(Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)))
- self.m1 = nn.ModuleList([EMA(c_)]) # 添加在最后一个卷积之前
- def forward(self, x):
- return self.cv3(torch.cat((self.m(self.m1[0](self.cv1(x))), self.cv2(x)), 1))
第②步:在yolo.py文件里的parse_model函数加入类名

第③步:创建自定义的yaml文件
- # YOLOv5 🚀 by Ultralytics, GPL-3.0 license
- # Parameters
- nc: 80 # number of classes
- depth_multiple: 0.33 # model depth multiple
- width_multiple: 0.50 # layer channel multiple
- anchors:
- - [10,13, 16,30, 33,23] # P3/8
- - [30,61, 62,45, 59,119] # P4/16
- - [116,90, 156,198, 373,326] # P5/32
- # YOLOv5 v6.0 backbone
- backbone:
- # [from, number, module, args]
- [[-1, 1, Conv, [64, 6, 2, 2]], # 0-P1/2
- [-1, 1, Conv, [128, 3, 2]], # 1-P2/4
- [-1, 3, C3_EMA1, [128]],
- [-1, 1, Conv, [256, 3, 2]], # 3-P3/8
- [-1, 6, C3, [256]],
- [-1, 1, Conv, [512, 3, 2]], # 5-P4/16
- [-1, 9, C3, [512]],
- [-1, 1, Conv, [1024, 3, 2]], # 7-P5/32
- [-1, 3, C3, [1024]],
- [-1, 1, SPPF, [1024, 5]], # 9
- ]
- # YOLOv5 v6.0 head
- head:
- [[-1, 1, Conv, [512, 1, 1]],
- [-1, 1, nn.Upsample, [None, 2, 'nearest']],
- [[-1, 6], 1, Concat, [1]], # cat backbone P4
- [-1, 3, C3, [512, False]], # 13
- [-1, 1, Conv, [256, 1, 1]],
- [-1, 1, nn.Upsample, [None, 2, 'nearest']],
- [[-1, 4], 1, Concat, [1]], # cat backbone P3
- [-1, 3, C3, [256, False]], # 17 (P3/8-small)
- [-1, 1, Conv, [256, 3, 2]],
- [[-1, 14], 1, Concat, [1]], # cat head P4
- [-1, 3, C3, [512, False]], # 20 (P4/16-medium)
- [-1, 1, Conv, [512, 3, 2]],
- [[-1, 10], 1, Concat, [1]], # cat head P5
- [-1, 3, C3, [1024, False]], # 23 (P5/32-large)
- [[17, 20, 23], 1, Detect, [nc, anchors]], # Detect(P3, P4, P5)
- ]
第④步:验证是否加入成功
运行yolo.py

第⑤步:修改train.py中 ‘--cfg’默认参数
这步同上,就不再过多叙述了~
PS:
(1)在我的数据集上,使用EMA比CA涨了1.3,还是很不错的。
CA:

EMA:

(2)在我的数据集上,两种方法精度差不多,但第一种比第二种方法精度略高一丝。