目录
参考资料:
论文:
CBAM: Convolutional Block Attention Module
博客:
一、前言
为提高 CNN性能
,最近的研究主要研究了网络的三个重要因素:
depth(深度)
;width(宽度)
;cardinality(基数)
;
从20世纪90年代 LeNet 网络的提出,网络的 深度
不断增加;后来 VGG 网络表明,相同形状的块堆叠
效果良好;GoogLeNet 网络的提出,提出宽度
也是提高模型性能的另一个重要因素;同样的,ResNet 将 残差块
以相同拓扑与跳跃式连接堆叠在一起,构建了一个非常深的架构,达到了不错的效果;Xception 和 ResNeXt 网络表明,增加网络 基数
不仅减少了参数量,而且比另两个因素(深度和宽度)
具有更强的表示能力;
除了这些因素之外,作者还研究了网络设计的另一个方面—— 注意力
;“注意力” 也是人类视觉系统的一个很有趣的地方;通过注意力机制来增加网络的表征力:关注重要特征,抑制不必要特征 ;
卷积运算是通过将 跨通道信息和空间信息混合 在一起来提取信息特征的 ;
作者因此提出了 CBAM(Convolutional Block Attention Module) 来强调通道轴
和空间轴
这两个主要维度上的有意义特征 ,创新提出了Channel Attention Module (通道注意模块)
和 Spatial Attention Module (空间注意模块)
;
假设 输入特征图
为 :
F
∈
R
C
×
H
×
W
F ∈ R^{C\times H\times W}
F∈RC×H×W ,利用 CBAM
依此推导出:
一维通道注意图
: M c ∈ R C × 1 × 1 M_c ∈ R^{C\times 1\times 1} Mc∈RC×1×1 ;二维空间注意图
: M s ∈ R 1 × H × W M_s ∈ R^{1\times H\times W} Ms∈R1×H×W ;
总的注意过程可以概括为 :
可以看到 CBAM 包含2个独立的子模块, CAM和SAM,分别进行通道与空间上的Attention, 这样不只能够节约参数和计算力,并且保证了其能够做为即插即用的模块集成到现有的网络架构中去。
通道上的 Attention 机制在 2017 年的 SENet 就被提出。事实上,CAM 与 SENet 相比,只是多了一个并行的 Max Pooling 层。至于为何如此更改,论文也给出了解释和实验数据支持。
二、网络结构
2.1 Channel Attention Module(CAM)
利用 特征间的通道关系
来生成通道注意图:由于feature map的每个channel都被认为是 一个feature检测器
,因此 channel 的注意力集中在 给定输入图像的 "什么" 是有意义的
。
为了有效地计算通道注意力,采用 压缩输入特征映射的空间维度
的方法 :文中同时使用 AvgPool (平均池化)
和 MaxPool (最大池化)
的方法,并证明了这种做法比单独使用一种池化方法更具有表征力;
式中,
σ
σ
σ 为 sigmoid
函数 ,
W
0
∈
R
C
r
×
C
W_0 ∈ R^{\frac{C}{r\times C}}
W0∈Rr×CC ,
W
1
∈
R
C
×
C
r
W_1 ∈ R^{\frac{C×C}{r}}
W1∈RrC×C,MLP的权重
W
0
W_0
W0 和
W
1
W_1
W1 共享,在
W
0
W_0
W0 前是 ReLU
激活函数 ;
具体流程如下:
- (1)将输入的特征图 F ( H × W × C ) F(H×W×C) F(H×W×C)分别经过基于 W W W 和 H H H 的Global Max Pooling(全局最大池化)和Global Average Pooling(全局平均池化),得到两个 1 × 1 × C 1×1×C 1×1×C 的特征图。
- (2)再将它们分别送入一个两层的神经网络(MLP),第一层神经元个数为 C r \frac{C}{r} rC(r为减少率),激活函数为 R e L U ReLU ReLU,第二层神经元个数为 C C C,这个两层的神经网络是共享的。
- (3)将MLP输出的特征进行基于element-wise的加和操作,再经过sigmoid激活操作,生成最终的channel attention feature,即 M c M_c Mc 。
- (4)将 M c M_c Mc 和输入特征图 F F F 做element-wise乘法操作,生成Spatial attention模块需要的输入特征。
pooling的使用:
在channel attention中,表1对于pooling的使用进行了实验对比,发现avg & max的并行池化的效果要更好。这里也有可能是池化丢失的信息太多,avg&&max的并行连接方式比单一的池化丢失的信息更少,所以效果会更好一点。
2.2 Spatial Attention Module(SAM)
利用 特征间的空间关系
生成空间注意图:与通道注意模块不同的是,空间注意模块关注的是 信息部分 "在哪里"
。
作为通道注意模块的补充;为了计算空间注意力,首先沿着通道轴应用 平均池化和最大池化
操作,并将它们连接起来以生成一个有效的 特征描述符
;
文中使用两个池化操作聚合一个feature map的通道信息,生成两个2D maps :
-
F
a
v
g
s
∈
R
1
×
H
×
W
F^s_{avg} ∈ R^{1×H×W}
Favgs∈R1×H×W :表示通道的
平均池化特性
-
F
m
a
x
s
∈
R
1
×
H
×
W
F^s_{max} ∈ R^{1×H×W}
Fmaxs∈R1×H×W :表示通道的
最大池化特性
然后利用一个标准的卷积层进行连接和卷积操作,得到二维空间注意力图;
式中,
σ
σ
σ 为 sigmoid
函数 ,
f
7
x
7
f^{7x7}
f7x7 为
7
x
7
7 x 7
7x7 大小的卷积核;
具体流程如下:
- (1)先将Channel attention模块输出的特征图 F ′ F' F′ 作为本模块的输入特征图。
- (2)做一个基于channel的global max pooling 和global average pooling,得到两个H×W×1 的特征图
- (3)将这2个特征图基于channel 做concat操作(通道拼接)。
- (4)经过一个 7 × 7 7×7 7×7 卷积(7×7比3×3效果要好)操作,降维为1个channel,即 H × W × 1 H×W×1 H×W×1。再经过sigmoid生成spatial attention feature,即 M s M_s Ms 。
- (5)最后将该feature和该模块的输入feature做乘法,得到最终生成的特征。
三、CBAM 的应用
下面是将 CBAM
结合 ResBlock
应用于ResNet中 ;两个模块可以以并行或顺序的方式放置,实验测试发现 顺序排列
比 并行排列
有更好的结果 ;
四、CBAM可视化
最后,分别使用 ResNet50 、ResNet50+SENet 、ResNet50+CBAM 进行实验得到可视化结果 :
五、总结
- 论文提出了一种基于注意力机制的轻量型结构 CBAM ,基本上可以添加到所有常规的卷积层中。
- 文中验证了 Channel Attention Module 中 avg 与 max 并行的方式最好,接下来通过实验验证了 Channel Attention Module 和 Spatial Attention Module 的最佳先后顺序,还对比了 CBAM 与 SENet 的性能。
- 文章还可视化了 CBAM 的关注区域,使得 CBAM 具有更好的解释性。最后在目标检测任务上进行实验,验证了 CBAM 的通用性好。
六、论文复现
参考:
(1)Channel Attention Module:
class ChannelAttention(nn.Module): # Channel Attention Module
def __init__(self, in_planes):
super(ChannelAttention, self).__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.max_pool = nn.AdaptiveMaxPool2d(1)
self.fc1 = nn.Conv2d(in_planes, in_planes // 16, kernel_size=1, bias=False)
self.relu = nn.ReLU()
self.fc2 = nn.Conv2d(in_planes // 16, in_planes, kernel_size=1, bias=False)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
avg_out = self.avg_pool(x)
avg_out = self.fc1(avg_out)
avg_out = self.relu(avg_out)
avg_out = self.fc2(avg_out)
max_out = self.max_pool(x)
max_out = self.fc1(max_out)
max_out = self.relu(max_out)
max_out = self.fc2(max_out)
out = avg_out + max_out
out = self.sigmoid(out)
return out
(2)Spatial Attention Module:
class SpatialAttention(nn.Module): # Spatial Attention Module
def __init__(self):
super(SpatialAttention, self).__init__()
self.conv1 = nn.Conv2d(in_channels=2, out_channels=1,
kernel_size=7, padding=3, bias=False)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
# 并行的两个通道维度上的池化,然后拼接
avg_out = torch.mean(x, dim=1, keepdim=True)
max_out, _ = torch.max(x, dim=1, keepdim=True)
out = torch.cat([avg_out, max_out], dim=1)
out = self.conv1(out)
out = self.sigmoid(out)
return out
(3)BasicBlock:
class BasicBlock(nn.Module): # 左侧的 residual block 结构(18-layer、34-layer)
expansion = 1
def __init__(self, in_planes, planes, stride=1): # 两层卷积 Conv2d + Shutcuts
super(BasicBlock, self).__init__()
self.conv1 = nn.Conv2d(in_planes, self.expansion*planes, kernel_size=3,
stride=stride, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(planes)
self.conv2 = nn.Conv2d(self.expansion*planes, self.expansion*planes, kernel_size=3,
stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(self.expansion*planes)
self.channel = ChannelAttention(self.expansion*planes) # Channel Attention Module
self.spatial = SpatialAttention() # Spatial Attention Module
self.shortcut = nn.Sequential()
if stride != 1 or in_planes != self.expansion*planes: # Shutcuts用于构建 Conv Block 和 Identity Block
self.shortcut = nn.Sequential(
nn.Conv2d(in_planes, self.expansion*planes,
kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(self.expansion*planes)
)
def forward(self, x):
out = F.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
# 加入通道注意力,并在原始输入上加权
CBAM_Cout = self.channel(out)
out = out * CBAM_Cout
# 加入空间注意力,并在原始输入上加权
CBAM_Sout = self.spatial(out)
out = out * CBAM_Sout
# 残差连接
out += self.shortcut(x)
out = F.relu(out)
return out
(4)Bottleneck:
class Bottleneck(nn.Module): # 右侧的 residual block 结构(50-layer、101-layer、152-layer)
expansion = 4
def __init__(self, in_planes, planes, stride=1): # 三层卷积 Conv2d + Shutcuts
super(Bottleneck, self).__init__()
self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=1, bias=False)
self.bn1 = nn.BatchNorm2d(planes)
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3,
stride=stride, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(planes)
self.conv3 = nn.Conv2d(planes, self.expansion*planes,
kernel_size=1, bias=False)
self.bn3 = nn.BatchNorm2d(self.expansion*planes)
self.channel = ChannelAttention(self.expansion*planes) # Channel Attention Module
self.spatial = SpatialAttention() # Spatial Attention Module
self.shortcut = nn.Sequential()
if stride != 1 or in_planes != self.expansion*planes: # Shutcuts用于构建 Conv Block 和 Identity Block
self.shortcut = nn.Sequential(
nn.Conv2d(in_planes, self.expansion*planes,
kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(self.expansion*planes)
)
def forward(self, x):
out = F.relu(self.bn1(self.conv1(x)))
out = F.relu(self.bn2(self.conv2(out)))
out = self.bn3(self.conv3(out))
CBAM_Cout = self.channel(out)
out = out * CBAM_Cout
CBAM_Sout = self.spatial(out)
out = out * CBAM_Sout
out += self.shortcut(x)
out = F.relu(out)
return out
(5)主体结构:
class CBAM_ResNet(nn.Module):
def __init__(self, block, num_blocks, num_classes=1000):
super(CBAM_ResNet, self).__init__()
self.in_planes = 64
self.conv1 = nn.Conv2d(3, 64, kernel_size=3,
stride=1, padding=1, bias=False) # conv1
self.bn1 = nn.BatchNorm2d(64)
self.layer1 = self._make_layer(block, 64, num_blocks[0], stride=1) # conv2_x
self.layer2 = self._make_layer(block, 128, num_blocks[1], stride=2) # conv3_x
self.layer3 = self._make_layer(block, 256, num_blocks[2], stride=2) # conv4_x
self.layer4 = self._make_layer(block, 512, num_blocks[3], stride=2) # conv5_x
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.linear = nn.Linear(512 * block.expansion, num_classes)
def _make_layer(self, block, planes, num_blocks, stride):
strides = [stride] + [1]*(num_blocks-1)
layers = []
for stride in strides:
layers.append(block(self.in_planes, planes, stride))
self.in_planes = planes * block.expansion
return nn.Sequential(*layers)
def forward(self, x):
x = F.relu(self.bn1(self.conv1(x)))
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = self.avgpool(x)
x = torch.flatten(x, 1)
out = self.linear(x)
return out
(6)构建不同的网络:
def make_net(net, num_classes):
if net == 'CBAM_ResNet18':
return CBAM_ResNet(BasicBlock, [2, 2, 2, 2], num_classes)
if net == 'CBAM_ResNet34':
return CBAM_ResNet(BasicBlock, [3, 4, 6, 3], num_classes)
if net == 'CBAM_ResNet50':
return CBAM_ResNet(Bottleneck, [3, 4, 6, 3], num_classes)
if net == 'CBAM_ResNet101':
return CBAM_ResNet(Bottleneck, [3, 4, 23, 3], num_classes)
if net == 'CBAM_ResNet152':
return CBAM_ResNet(Bottleneck, [3, 8, 36, 3], num_classes)
def test():
CBAM_ResNet50 = make_net('CBAM_ResNet50', num_classes=2)
#创建模型,部署gpu
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# CBAM_ResNet50.to(device)
# GPU显存不够,只能放在CPU上
summary(CBAM_ResNet50, (3, 224, 224), batch_size=-1, device="cpu")
if __name__ == '__main__':
test()