1.CBAM介绍

论文题目:《CBAM: Convolutional Block Attention Module》
论文地址: https://arxiv.org/pdf/1807.06521.pdf
实验证明,将CBAM注意力模块嵌入到YOLOv5网络中,有利于解决原始网络无注意力偏好的问题。主要在分类问题中比较明显

image.png

CBAM注意力结构基本原理:从上图明显可以看到, CBAM一共包含2个独立的子模块, 通道注意力模块(Channel Attention Module,CAM) 和空间注意力模块(Spartial Attention Module,SAM) ,分别进行通道与空间维度上的注意力特征融合。 这样不只能够节约参数和计算力,并且保证了其能够做为即插即用的模块集成到现有的网络架构中去。

那对应YOLOv5结合CBAM需要修改哪些地方:

2.common.py中加入CBAM代码

[CBAM]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class ChannelAttentionModule(nn.Module):
def __init__(self, c1, reduction=16):
super(ChannelAttentionModule, self).__init__()
mid_channel = c1 // reduction
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.max_pool = nn.AdaptiveMaxPool2d(1)
self.shared_MLP = nn.Sequential(
nn.Linear(in_features=c1, out_features=mid_channel),
nn.ReLU(),
nn.Linear(in_features=mid_channel, out_features=c1)
)
self.sigmoid = nn.Sigmoid()
#self.act=SiLU()
def forward(self, x):
avgout = self.shared_MLP(self.avg_pool(x).view(x.size(0),-1)).unsqueeze(2).unsqueeze(3)
maxout = self.shared_MLP(self.max_pool(x).view(x.size(0),-1)).unsqueeze(2).unsqueeze(3)
return self.sigmoid(avgout + maxout)
class SpatialAttentionModule(nn.Module):
def __init__(self):
super(SpatialAttentionModule, self).__init__()
self.conv2d = nn.Conv2d(in_channels=2, out_channels=1, kernel_size=7, stride=1, padding=3)
#self.act=SiLU()
self.sigmoid = nn.Sigmoid()
def forward(self, x):
avgout = torch.mean(x, dim=1, keepdim=True)
maxout, _ = torch.max(x, dim=1, keepdim=True)
out = torch.cat([avgout, maxout], dim=1)
out = self.sigmoid(self.conv2d(out))
return out

class CBAM(nn.Module):
def __init__(self, c1,c2):
super(CBAM, self).__init__()
self.channel_attention = ChannelAttentionModule(c1)
self.spatial_attention = SpatialAttentionModule()

def forward(self, x):
out = self.channel_attention(x) * x
out = self.spatial_attention(out) * out
return out

3.在yolo.py文件中添加对应的CBAM

[yolo.py]
1
2
 if m in [Conv, GhostConv, Bottleneck, GhostBottleneck, SPP, 
DWConv, MixConv2d, Focus, CrossConv, BottleneckCSP, C3, C3TR, CBAM]:

4.在yolov5s.yaml文件中添加CBAM

根据实际训练效果,在配置文件中的C3模块后面适当添加CBAM注意力模块,过程中注意通道数和网络层数的变化(注:不同添加位置效果可能不大一样),例如下面作为参考:在最后检测层上面添加CBAM,其中BiFPND,BiFPNT分别是二分支与三分支的BiFPN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#Head
head:
[[-1, 1, Conv, [512, 1, 1]], #14
[-1, 1, nn.Upsample, [None, 2, 'nearest']], #15
[[-1, 9], 1, BiFPND, [256, 256]], #16
[-1, 3, C3, [512, False]], #17

[-1, 1, Conv, [256, 1, 1]], #18
[-1, 1, nn.Upsample, [None, 2, 'nearest']], #19
[[-1, 6], 1, BiFPND, [128, 128]], #20
[-1, 3, C3, [256, False]], #21

[-1, 1, Conv, [512, 3, 2]], #22
[[-1, 17, 9], 1, BiFPNT, [256, 256]], #23
[-1, 3, C3, [512, False]], #24

[-1, 1, Conv, [512, 3, 2]], #25
[[-1, 14], 1, BiFPND, [256, 256]], #26
[-1, 3, C3, [1024, False]], #27
[-1, 1, CBAM, [1024]], #28

[[21, 24, 28], 1, Detect, [nc, anchors]], #29 Detect
]

其它注意力SE,ECA与CSBAM区别

注意力机制顾名思义就是通过对感兴趣的区域提升更多的注意力,尽可能的抑制不感兴趣的区域在图像分割中的作用。深度学习CNN中可以将注意力机制分为通道注意力和空间注意力两种,通道注意力是确定不同通道之间的权重关系,提升重点通道的权重,抑制作用不大的通道,空间注意力是确定空间邻域不同像素之间的权重关系,提升重点区域像素的权重,让算法更多的关注我们需要的研究区域,减小非必要区域的权重。

一、SE (Squeeze and Excitation)注意力机制
SE注意力机制是通道注意力模式下的一种确定权重的方法,它通过在不同通道间分配权重达到主次优先的目的。如下图所示,为SE注意力机制的结构图。

该结构主要分为以下三个方面:

①:通过将特征图进行Squeeze(压缩),该步骤是通过全局平均池化把特征图从大小为(N,C,H,W)转换为(N,C,1,1),这样就达到了全局上下文信息的融合。

②:Excitation操作,该步骤使用两个全连接层,通过全连接层之间的非线性添加模型的复杂度,达到确定不同通道之间的权重作用,其中第一个全连接层使用ReLU激活函数,第二个全连接层采用Sigmoid激活函数,为了将权重中映射到(0,1)之间。值得注意的是,为了减少计算量进行降维处理,将第一个全连接的输出采用输入的1/4或者1/16。

③:将reshape过后的权重值与原有的特征图做乘法运算(该步骤采用了Python的广播机制),得到不同权重下的特征图。

image.png

具体PyTorch实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import torch
import torch.nn as nn


class Se(nn.Module):
def __init__(self,in_channel,reduction=16):
super(Se, self).__init__()
self.pool=nn.AdaptiveAvgPool2d(output_size=1)
self.fc=nn.Sequential(
nn.Linear(in_features=in_channel,out_features=in_channel//reduction,bias=False),
nn.ReLU(),
nn.Linear(in_features=in_channel//reduction,out_features=in_channel,bias=False),
nn.Sigmoid()
)

def forward(self,x):
out=self.pool(x)
out=self.fc(out.view(out.size(0),-1))
out=out.view(x.size(0),x.size(1),1,1)
return out*x

二、ECA(Efficient Channel Attention)
ECA注意力机制也是通道注意力的一种方法,该算法是在SE算法的基础上做出了一定的改进,首先ECA作者认为SE虽然全连接的降维可以降低模型的复杂度,但是破坏了通道与其权重之间的直接对应关系,先降维后升维,这样权重和通道的对应关系是间接的,基于上述,作者提出一维卷积的方法,避免了降维对数据的影响。

该结构主要分为以下三个方面:

①:通过将特征图进行Squeeze(压缩),该步骤是通过全局平均池化把特征图从大小为(N,C,H,W)转换为(N,C,1,1),这样就达到了全局上下文信息的融合,该步骤和SE一样。

②:计算自适应卷积核的大小,
,其中C为输入的通道数,b=1,
=2,并采用一维卷积计算通道的权重,最后采用Sigmoid激活函数将权重映射在(0-1)之间。

③:将reshape过后的权重值与原有的特征图做乘法运算(该步骤采用了Python的广播机制),得到不同权重下的特征图。

image.png

具体PyTorch实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import torch
import torch.nn as nn
import math
class ECA(nn.Module):
def __init__(self,in_channel,gamma=2,b=1):
super(ECA, self).__init__()
k=int(abs((math.log(in_channel,2)+b)/gamma))
kernel_size=k if k % 2 else k+1
padding=kernel_size//2
self.pool=nn.AdaptiveAvgPool2d(output_size=1)
self.conv=nn.Sequential(
nn.Conv1d(in_channels=1,out_channels=1,kernel_size=kernel_size,padding=padding,bias=False),
nn.Sigmoid()
)

def forward(self,x):
out=self.pool(x)
out=out.view(x.size(0),1,x.size(1))
out=self.conv(out)
out=out.view(x.size(0),x.size(1),1,1)
return out*x

三、CBAM(Convolutional Block Attention Module)
CBAM注意力机制是一种将通道与空间注意力机制相结合的算法模型,算法整体结构如图3所示,输入特征图先进行通道注意力机制再进行空间注意力机制操作,最后输出,这样从通道和空间两个方面达到了强化感兴趣区域的目的。

通道结构主要分为以下三个方面:

①:通过将特征图进行Squeeze(压缩),该步骤分别采用全局平均池化和全局最大池化把特征图从大小为(N,C,H,W)转换为(N,C,1,1),这样就达到了全局上下文信息的融合。

②:分别将全局最大池化和全局平均池化结果进行MLP(多层感知机)操作,MLP在这里定义与SE的操作一样,为两层全连接层,中间采用ReLU激活,最后将两者相加后利用Sigmoid函数激活。

③:将reshape过后的权重值与原有的特征图做乘法运算(该步骤采用了Python的广播机制),得到不同权重下的特征图。

空间结构主要分为以下三个方面:

①:将上述通道注意力操作的结果,分别在通道维度上进行最大池化和平均池化,即将经过通道注意力机制的特征图从(N,C,H,W)转换为(N,1,H,W),达到融合不同通道的信息的效果,然后在通道维度上将最大池化与平均池化结果叠加起来,即采用torch.cat()。

②:将叠加后2个通道的结果做卷积运算,输出通道为1,卷积核大小为7,最后将输出结果采用Sigmoid函数激活。

③:将权重值与原有的特征图做乘法运算(该步骤采用了Python的广播机制),得到不同权重下的特征图。

image.png

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import torch
import torch.nn as nn
import math
class CBAM(nn.Module):
def __init__(self,in_channel,reduction=16,kernel_size=7):
super(CBAM, self).__init__()
#通道注意力机制
self.max_pool=nn.AdaptiveMaxPool2d(output_size=1)
self.avg_pool=nn.AdaptiveAvgPool2d(output_size=1)
self.mlp=nn.Sequential(
nn.Linear(in_features=in_channel,out_features=in_channel//reduction,bias=False),
nn.ReLU(),
nn.Linear(in_features=in_channel//reduction,out_features=in_channel,bias=False)
)
self.sigmoid=nn.Sigmoid()
#空间注意力机制
self.conv=nn.Conv2d(in_channels=2,out_channels=1,kernel_size=kernel_size ,stride=1,padding=kernel_size//2,bias=False)

def forward(self,x):
#通道注意力机制
maxout=self.max_pool(x)
maxout=self.mlp(maxout.view(maxout.size(0),-1))
avgout=self.avg_pool(x)
avgout=self.mlp(avgout.view(avgout.size(0),-1))
channel_out=self.sigmoid(maxout+avgout)
channel_out=channel_out.view(x.size(0),x.size(1),1,1)
channel_out=channel_out*x
#空间注意力机制
max_out,_=torch.max(channel_out,dim=1,keepdim=True)
mean_out=torch.mean(channel_out,dim=1,keepdim=True)
out=torch.cat((max_out,mean_out),dim=1)
out=self.sigmoid(self.conv(out))
out=out*channel_out
return out

另一利用注意力模块的ODConv:即插即用的动态卷积

一定程度上讲,ODConv可以视作CondConv的延续,将CondConv中一个维度上的动态特性进行了扩展,同时了考虑了空域、输入通道、输出通道等维度上的动态性,故称之为全维度动态卷积。ODConv通过并行策略采用多维注意力机制沿核空间的四个维度学习互补性注意力。作为一种“即插即用”的操作,它可以轻易的嵌入到现有CNN网络中。ImageNet分类与COCO检测任务上的实验验证了所提ODConv的优异性:即可提升大模型的性能,又可提升轻量型模型的性能

动态卷积这几年研究的非常多了,比较知名的有谷歌的CondConv,旷视科技的WeightNet、MSRA的DynamicConv、华为的DyNet、商汤的CARAFE等
常规卷积只有一个静态卷积核且与输入样本无关。对于动态卷积来说,它对多个卷积核进行线性加权,而加权值则与输入有关,这就使得动态卷积具有输入依赖性。它可以描述如下:
image.png

image.png

image.png

ODConv比一般的动态卷积效果都要好,比一般的注意力机制效果也要好,其实动态卷积与注意力机制是从模块中不同角度进行解析,动态卷积是相比较于常规卷积,最终需要与输入相乘,与输入有关,而常规卷积与输入无关;注意力机制是相对于特征图的四个维度比较的,也即卷积方式不一样,以往是直接进行特征图与卷积核进行卷积来获取特征,而注意力机制就是将特征图分别对四个通道进行压缩,来获取对比某一通道在特征图的影响力,再分别与卷积核进行线性加权,最后与输入相乘。因此像动态卷积与注意力其实一个东西,只是ODConv是考虑到所有维度的注意力,因此效果最好

参考链接:https://zhuanlan.zhihu.com/p/468466504

5.在yolov5中添加ODConv

1.首先在common.py文件中添加代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.autograd

class ODConv(nn.Sequential):
def __init__(self, in_planes, out_planes, kernel_size=3, stride=1, groups=1, norm_layer=nn.BatchNorm2d,
reduction=0.0625, kernel_num=1):
padding = (kernel_size - 1) // 2
super(ODConv, self).__init__(
ODConv2d(in_planes, out_planes, kernel_size, stride, padding, groups=groups,
reduction=reduction, kernel_num=kernel_num),
norm_layer(out_planes),
nn.SiLU()
)

class Attention(nn.Module):
def __init__(self, in_planes, out_planes, kernel_size,
groups=1,
reduction=0.0625,
kernel_num=4,
min_channel=16):
super(Attention, self).__init__()
attention_channel = max(int(in_planes * reduction), min_channel)
self.kernel_size = kernel_size
self.kernel_num = kernel_num
self.temperature = 1.0

self.avgpool = nn.AdaptiveAvgPool2d(1)
self.fc = nn.Conv2d(in_planes, attention_channel, 1, bias=False)
self.bn = nn.BatchNorm2d(attention_channel)
self.relu = nn.ReLU(inplace=True)

self.channel_fc = nn.Conv2d(attention_channel, in_planes, 1, bias=True)
self.func_channel = self.get_channel_attention

if in_planes == groups and in_planes == out_planes: # depth-wise convolution
self.func_filter = self.skip
else:
self.filter_fc = nn.Conv2d(attention_channel, out_planes, 1, bias=True)
self.func_filter = self.get_filter_attention

if kernel_size == 1: # point-wise convolution
self.func_spatial = self.skip
else:
self.spatial_fc = nn.Conv2d(attention_channel, kernel_size * kernel_size, 1, bias=True)
self.func_spatial = self.get_spatial_attention

if kernel_num == 1:
self.func_kernel = self.skip
else:
self.kernel_fc = nn.Conv2d(attention_channel, kernel_num, 1, bias=True)
self.func_kernel = self.get_kernel_attention
self.bn_1 = nn.LayerNorm([attention_channel,1,1])
self._initialize_weights()

def _initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
if m.bias is not None:
nn.init.constant_(m.bias, 0)
if isinstance(m, nn.BatchNorm2d):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)

def update_temperature(self, temperature):
self.temperature = temperature

@staticmethod
def skip(_):
return 1.0

def get_channel_attention(self, x):
channel_attention = torch.sigmoid(self.channel_fc(x).view(x.size(0), -1, 1, 1) / self.temperature)
return channel_attention

def get_filter_attention(self, x):
filter_attention = torch.sigmoid(self.filter_fc(x).view(x.size(0), -1, 1, 1) / self.temperature)
return filter_attention

def get_spatial_attention(self, x):
spatial_attention = self.spatial_fc(x).view(x.size(0), 1, 1, 1, self.kernel_size, self.kernel_size)
spatial_attention = torch.sigmoid(spatial_attention / self.temperature)
return spatial_attention

def get_kernel_attention(self, x):
kernel_attention = self.kernel_fc(x).view(x.size(0), -1, 1, 1, 1, 1)
kernel_attention = F.softmax(kernel_attention / self.temperature, dim=1)
return kernel_attention

def forward(self, x):
x = self.avgpool(x)
x = self.fc(x)
x = self.bn_1(x)
x = self.relu(x)
return self.func_channel(x), self.func_filter(x), self.func_spatial(x), self.func_kernel(x)

class ODConv2d(nn.Module):
def __init__(self,
in_planes,
out_planes,
kernel_size=3,
stride=1,
padding=0,
dilation=1,
groups=1,
reduction=0.0625,
kernel_num=1):
super(ODConv2d, self).__init__()
self.in_planes = in_planes
self.out_planes = out_planes
self.kernel_size = kernel_size
self.stride = stride
self.padding = padding
self.dilation = dilation
self.groups = groups
self.kernel_num = kernel_num
self.attention = Attention(in_planes, out_planes, kernel_size, groups=groups,
reduction=reduction, kernel_num=kernel_num)
self.weight = nn.Parameter(torch.randn(kernel_num, out_planes, in_planes//groups, kernel_size, kernel_size),
requires_grad=True)
self._initialize_weights()

if self.kernel_size == 1 and self.kernel_num == 1:
self._forward_impl = self._forward_impl_pw1x
else:
self._forward_impl = self._forward_impl_common

def _initialize_weights(self):
for i in range(self.kernel_num):
nn.init.kaiming_normal_(self.weight[i], mode='fan_out', nonlinearity='relu')

def update_temperature(self, temperature):
self.attention.update_temperature(temperature)

def _forward_impl_common(self, x):

channel_attention, filter_attention, spatial_attention, kernel_attention = self.attention(x)
batch_size, in_planes, height, width = x.size()
x = x * channel_attention
x = x.reshape(1, -1, height, width)
aggregate_weight = spatial_attention * kernel_attention * self.weight.unsqueeze(dim=0)
aggregate_weight = torch.sum(aggregate_weight, dim=1).view(
[-1, self.in_planes // self.groups, self.kernel_size, self.kernel_size])
output = F.conv2d(x, weight=aggregate_weight, bias=None, stride=self.stride, padding=self.padding,
dilation=self.dilation, groups=self.groups * batch_size)
output = output.view(batch_size, self.out_planes, output.size(-2), output.size(-1))
output = output * filter_attention
return output

def _forward_impl_pw1x(self, x):
channel_attention, filter_attention, spatial_attention, kernel_attention = self.attention(x)
x = x * channel_attention
output = F.conv2d(x, weight=self.weight.squeeze(dim=0), bias=None, stride=self.stride, padding=self.padding,
dilation=self.dilation, groups=self.groups)
output = output * filter_attention
return output

def forward(self, x):
return self._forward_impl(x)

2.在yolo.py文件中添加对应的CBAM

1
2
3
4
5
6
elif m in [ODConv]:
c1, c2 = ch[f], args[0]
if c2 != no: # if not output
c2 = make_divisible(c2 * gw, 8)

args = [c1, c2, *args[1:]]

在yolov5.yaml文件中则需要除了backbone网络中第一个卷积之外,其它卷积都可以替换为ODConv
例如替换head中一个卷积conv

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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, ODConv, [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)
]