論文地址:https://arxiv.org/abs/1807.06521
1. 摘要
我們提出了卷積塊注意模塊 (CBAM), 一個(gè)簡單而有效的注意模塊的前饋卷積神經(jīng)網(wǎng)絡(luò)。給出了一個(gè)中間特征映射, 我們的模塊按照兩個(gè)獨(dú)立的維度、通道和空間順序推斷出注意力映射, 然后將注意力映射相乘為自適應(yīng)特征細(xì)化的輸入特征映射。因?yàn)?CBAM 是一個(gè)輕量級和通用的模塊, 它可以無縫地集成到任何 CNN 架構(gòu)只增加微不足道的間接開銷, 可以集成到端到端的CNN里面去。通過對 ImageNet-1K、COCO、MS 檢測和 VOC 2007 檢測數(shù)據(jù)集的廣泛實(shí)驗(yàn), 我們驗(yàn)證了我們的 CBAM。我們的實(shí)驗(yàn)表明, 各種模型的分類和檢測性能都有了一致的改進(jìn), 證明了 CBAM 的廣泛適用性。這些代碼和模型將公開提供。
2. 相關(guān)工作
網(wǎng)絡(luò)架構(gòu)的構(gòu)建,一直是計(jì)算機(jī)視覺中最重要的研究之一, 因?yàn)榫脑O(shè)計(jì)的網(wǎng)絡(luò)確保了在各種應(yīng)用中顯著的性能提高。自成功實(shí)施大型 CNN以來, 已經(jīng)提出了一系列廣泛的體系結(jié)構(gòu)。一種直觀而簡單的擴(kuò)展方法是增加神經(jīng)網(wǎng)絡(luò)的深度如 VGG-NET、ResNet及其變體,如 WideResNet和 ResNeXt。GoogLeNet展現(xiàn)了增加網(wǎng)絡(luò)的寬度對于結(jié)果的提升的幫助,典型的分類網(wǎng)絡(luò)都在提升深度與寬度上下了很大功夫。
眾所周知, 注意力在人的知覺中起著重要的作用。一個(gè)人并不是試圖一次處理整個(gè)場景。相反, 人類注意部分場景, 并有選擇地專注于突出部分, 以便更好地捕捉視覺結(jié)構(gòu)。
最近, 有幾次嘗試加入注意處理, 以提高CNNs在大規(guī)模分類任務(wù)的性能。Residual attention network for image classification中使用 encoder-decoder 樣式的注意模塊的Residual attention network。通過細(xì)化特征映射,不僅網(wǎng)絡(luò)性能良好, 而且對噪聲輸入也很健壯。我們不直接計(jì)算3d 的注意力映射, 而是分解了單獨(dú)學(xué)習(xí)通道注意和空間注意的過程。對于3D 特征圖, 單獨(dú)的注意生成過程的計(jì)算和參數(shù)開銷要小得多, 因此可以作為CNN的前置基礎(chǔ)架構(gòu)的模塊使用。
Squeeze-and-excitation networks引入一個(gè)緊湊模塊來利用通道間的關(guān)系。在他們的壓縮和激勵(lì)模塊中, 他們使用全局平均池功能來計(jì)算通道的注意力。然而, 我們表明, 這些都是次優(yōu)特征, 以推斷良好的通道注意, 我們使用最大池化的特點(diǎn)。然而,他們也錯(cuò)過了空間注意力機(jī)制, 在決定 "Where"。在我們的 CBAM 中, 我們利用一個(gè)有效的體系結(jié)構(gòu)來開發(fā)空間和通道的注意力, 并通過經(jīng)驗(yàn)驗(yàn)證, 利用兩者都優(yōu)于僅使用通道的注意作為。此外, 我們的實(shí)驗(yàn)表明, 我們的模塊在檢測任務(wù) (MS COCO和 VOC2017)上是有效的。特別是, 我們通過將我們的模塊放在VOC2007 測試集中的現(xiàn)有的目標(biāo)檢測器結(jié)合實(shí)現(xiàn)了最先進(jìn)的性能。
3. Convolutional Block Attention Module
給定一個(gè)中間特征映射F∈RC xHxW作為輸入, CBAM的1維通道注意圖Mc ∈RC ×1×1 和2D 空間注意圖Ms ∈R1×HxW 如圖1所示。總的注意過程可以概括為:
表示逐元素相乘。在相乘過程中,注意值被廣播。相應(yīng)地,通道注意值被沿著空間維度廣播,反之亦然。F’’是最終輸出。
3.1 Channel attention module
我們利用特征的通道間關(guān)系, 生成了通道注意圖。當(dāng)一個(gè)特征圖的每個(gè)通道被考慮作為特征探測器, 通道注意聚焦于 ' what ' 是有意義的輸入圖像。為了有效地計(jì)算通道的注意力, 我們壓縮了輸入特征圖的空間維數(shù)。為了聚焦空間信息,我們同時(shí)使用平均池化和最大池化。我們的實(shí)驗(yàn)證實(shí), 同時(shí)使用這兩種功能大大提高了網(wǎng)絡(luò)的表示能力。下面將描述詳細(xì)操作。
我們首先使用平均池化和最大池化操作來聚合特征映射的空間信息, 生成兩個(gè)不同的空間上下文描述符:Fcavg 和Fcmax , 分別表示平均池化和最大池化。兩個(gè)描述符然后送到一個(gè)共享網(wǎng)絡(luò), 以產(chǎn)生我們的通道注意力圖 Mc ∈ Rc×1×1。共享網(wǎng)絡(luò)由多層感知機(jī)(MLP) 和一個(gè)隱藏層組成。為了減少參數(shù)開銷, 隱藏的激活大小被設(shè)置為 rc/c++×1×1, 其中 r 是壓縮率。在將共享網(wǎng)絡(luò)應(yīng)用于每個(gè)描述符之后, 我們使用逐元素求和合并輸出特征向量。簡而言之, 通道的注意力被計(jì)算為:
3.2 Spatial attention module
我們利用特征間的空間關(guān)系, 生成空間注意圖。與通道注意力不同的是, 空間注意力集中在 "where" 是一個(gè)信息的部分, 這是對通道注意力的補(bǔ)充。為了計(jì)算空間注意力, 我們首先在通道軸上應(yīng)用平均池和最大池運(yùn)算, 并將它們連接起來以生成一個(gè)有效的特征描述符。在串聯(lián)特征描述符上, 我們應(yīng)用7×7的卷積生成空間注意圖的層Ms (F) ∈RH×W 。我們描述下面的詳細(xì)操作.
我們使用兩個(gè)池化操作來聚合功能映射的通道信息, 生成兩個(gè)2維映射:Fsavg∈R1×HxW 和Fsmax∈R1×HxW 每個(gè)通道都表示平均池化和最大池化。然后通過一個(gè)標(biāo)準(zhǔn)的卷積層連接和卷積混合, 產(chǎn)生我們的2D 空間注意圖。簡而言之, 空間注意力被計(jì)算為:
通過實(shí)驗(yàn)我們發(fā)現(xiàn)串聯(lián)兩個(gè)注意力模塊的效果要優(yōu)于并聯(lián)。通道注意力放在前面要優(yōu)于空間注意力模塊放在前面。
4. 實(shí)驗(yàn)
在本小節(jié)中,我們憑實(shí)驗(yàn)證明了我們的設(shè)計(jì)選擇的有效性。 在這次實(shí)驗(yàn)中,我們使用ImageNet-1K數(shù)據(jù)集并采用ResNet-50作為基礎(chǔ)架構(gòu)。 ImageNet-1K分類數(shù)據(jù)集[1]由1.2組成用于訓(xùn)練的百萬個(gè)圖像和用于1,000個(gè)對象類的驗(yàn)證的50,000個(gè)圖像
我們采用相同的數(shù)據(jù)增強(qiáng)方案進(jìn)行訓(xùn)練和測試時(shí)間進(jìn)行單一作物評估,大小為224×224。 學(xué)習(xí)率從0.1開始,每30個(gè)時(shí)期下降一次。 我們訓(xùn)練網(wǎng)絡(luò)90迭代。
4.1 通道注意力和空間注意力機(jī)制實(shí)驗(yàn)
4.2 使用Grad-CAM進(jìn)行網(wǎng)絡(luò)可視化
4.3 CBAM在目標(biāo)檢測的結(jié)果
5. 總結(jié)
作者提出了一種提高 CNN 網(wǎng)絡(luò)表示的新方法--卷積瓶頸注意模塊 (CBAM)。作者將基于注意力的特征細(xì)化成兩個(gè)不同的模塊、通道和空間結(jié)合起來, 實(shí)現(xiàn)了顯著的性能改進(jìn), 同時(shí)保持了小的開銷。對于通道的關(guān)注,使用最大池化和平均池化,最終模塊 (CBAM) 學(xué)習(xí)了如何有效地強(qiáng)調(diào)或壓縮提取中間特征。為了驗(yàn)證它的有效性, 我們進(jìn)行了廣泛的實(shí)驗(yàn)與并證實(shí), CBAM 優(yōu)于所有基線上的三不同的基準(zhǔn)數(shù)據(jù)集: ImageNet-1K, COCO, 和 VOC 2007。此外, 我們可視化模塊如何準(zhǔn)確推斷給定的輸入圖像。CBAM 或許會(huì)成為各種網(wǎng)絡(luò)體系結(jié)構(gòu)的重要組成部分。
6. 代碼
6.1 Pytorch版本
from collections import OrderedDict
import math
import torch
import torch.nn as nn
# import torchvision.models.resnet
class CBAM_Module(nn.Module):
def __init__(self, channels, reduction):
super(CBAM_Module, self).__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.max_pool = nn.AdaptiveMaxPool2d(1)
self.fc1 = nn.Conv2d(channels, channels // reduction, kernel_size=1,
padding=0)
self.relu = nn.ReLU(inplace=True)
self.fc2 = nn.Conv2d(channels // reduction, channels, kernel_size=1,
padding=0)
self.sigmoid_channel = nn.Sigmoid()
self.conv_after_concat = nn.Conv2d(2, 1, kernel_size = 3, stride=1, padding = 1)
self.sigmoid_spatial = nn.Sigmoid()
def forward(self, x):
# Channel attention module:(Mc(f) = σ(MLP(AvgPool(f)) + MLP(MaxPool(f))))
module_input = x
avg = self.avg_pool(x)
mx = self.max_pool(x)
avg = self.fc1(avg)
mx = self.fc1(mx)
avg = self.relu(avg)
mx = self.relu(mx)
avg = self.fc2(avg)
mx = self.fc2(mx)
x = avg + mx
x = self.sigmoid_channel(x)
# Spatial attention module:Ms (f) = σ( f7×7( AvgPool(f) ; MaxPool(F)] )))
x = module_input * x
module_input = x
avg = torch.mean(x, 1, keepdim=True)
mx, _ = torch.max(x, 1, keepdim=True)
x = torch.cat((avg, mx), 1)
x = self.conv_after_concat(x)
x = self.sigmoid_spatial(x)
x = module_input * x
return x
class Bottleneck(nn.Module):
"""
Base class for bottlenecks that implements `forward()` method.
"""
def forward(self, x):
residual = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
if self.downsample is not None:
residual = self.downsample(x)
out = self.se_module(out) + residual
out = self.relu(out)
return out
class CBAMResNetBottleneck(Bottleneck):
"""
ResNet bottleneck with a CBAM_Module. It follows Caffe
implementation and uses `stride=stride` in `conv1` and not in `conv2`
(the latter is used in the torchvision implementation of ResNet).
"""
expansion = 4
def __init__(self, inplanes, planes, groups, reduction, stride=1,
downsample=None):
super(CBAMResNetBottleneck, self).__init__()
self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=1, bias=False,
stride=stride)
self.bn1 = nn.BatchNorm2d(planes)
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, padding=1,
groups=groups, bias=False)
self.bn2 = nn.BatchNorm2d(planes)
self.conv3 = nn.Conv2d(planes, planes * 4, kernel_size=1, bias=False)
self.bn3 = nn.BatchNorm2d(planes * 4)
self.relu = nn.ReLU(inplace=True)
self.se_module = CBAM_Module(planes * 4, reduction=reduction)
self.downsample = downsample
self.stride = stride
class CABMNet(nn.Module):
def __init__(self, block, layers, groups, reduction, dropout_p=0.2,
inplanes=128, input_3x3=True, downsample_kernel_size=3,
downsample_padding=1, num_classes=1000):
super(CABMNet, self).__init__()
self.inplanes = inplanes
if input_3x3:
layer0_modules = [
('conv1', nn.Conv2d(3, 64, 3, stride=2, padding=1,
bias=False)),
('bn1', nn.BatchNorm2d(64)),
('relu1', nn.ReLU(inplace=True)),
('conv2', nn.Conv2d(64, 64, 3, stride=1, padding=1,
bias=False)),
('bn2', nn.BatchNorm2d(64)),
('relu2', nn.ReLU(inplace=True)),
('conv3', nn.Conv2d(64, inplanes, 3, stride=1, padding=1,
bias=False)),
('bn3', nn.BatchNorm2d(inplanes)),
('relu3', nn.ReLU(inplace=True)),
]
else:
layer0_modules = [
('conv1', nn.Conv2d(3, inplanes, kernel_size=7, stride=2,
padding=3, bias=False)),
('bn1', nn.BatchNorm2d(inplanes)),
('relu1', nn.ReLU(inplace=True)),
]
# To preserve compatibility with Caffe weights `ceil_mode=True`
# is used instead of `padding=1`.
layer0_modules.append(('pool', nn.MaxPool2d(3, stride=2,
ceil_mode=True)))
self.layer0 = nn.Sequential(OrderedDict(layer0_modules))
self.layer1 = self._make_layer(
block,
planes=64,
blocks=layers[0],
groups=groups,
reduction=reduction,
downsample_kernel_size=1,
downsample_padding=0
)
self.layer2 = self._make_layer(
block,
planes=128,
blocks=layers[1],
stride=2,
groups=groups,
reduction=reduction,
downsample_kernel_size=downsample_kernel_size,
downsample_padding=downsample_padding
)
self.layer3 = self._make_layer(
block,
planes=256,
blocks=layers[2],
stride=2,
groups=groups,
reduction=reduction,
downsample_kernel_size=downsample_kernel_size,
downsample_padding=downsample_padding
)
self.layer4 = self._make_layer(
block,
planes=512,
blocks=layers[3],
stride=2,
groups=groups,
reduction=reduction,
downsample_kernel_size=downsample_kernel_size,
downsample_padding=downsample_padding
)
self.avg_pool = nn.AvgPool2d(7, stride=1)
self.dropout = nn.Dropout(dropout_p) if dropout_p is not None else None
self.last_linear = nn.Linear(512 * block.expansion, num_classes)
for m in self.modules():
if isinstance(m, nn.Conv2d):
n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
m.weight.data.normal_(0, math.sqrt(2. / n))
elif isinstance(m, nn.BatchNorm2d):
m.weight.data.fill_(1)
m.bias.data.zero_()
# for m in self.modules():
# if isinstance(m, nn.Conv2d):
# nn.init.kaiming_normal(m.weight.data)
# elif isinstance(m, nn.BatchNorm2d):
# m.weight.data.fill_(1)
# m.bias.data.zero_()
def _make_layer(self, block, planes, blocks, groups, reduction, stride=1,
downsample_kernel_size=1, downsample_padding=0):
downsample = None
if stride != 1 or self.inplanes != planes * block.expansion:
downsample = nn.Sequential(
nn.Conv2d(self.inplanes, planes * block.expansion,
kernel_size=downsample_kernel_size, stride=stride,
padding=downsample_padding, bias=False),
nn.BatchNorm2d(planes * block.expansion),
)
layers = []
layers.append(block(self.inplanes, planes, groups, reduction, stride,
downsample))
self.inplanes = planes * block.expansion
for i in range(1, blocks):
layers.append(block(self.inplanes, planes, groups, reduction))
return nn.Sequential(*layers)
def features(self, x):
x = self.layer0(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
return x
def logits(self, x):
x = self.avg_pool(x)
if self.dropout is not None:
x = self.dropout(x)
x = x.view(x.size(0), -1)
x = self.last_linear(x)
return x
def forward(self, x):
x = self.features(x)
x = self.logits(x)
return x
def cbam_resnet50(num_classes=1000):
model = CABMNet(CBAMResNetBottleneck, [3, 4, 6, 3], groups=1, reduction=16,
dropout_p=None, inplanes=64, input_3x3=False,
downsample_kernel_size=1, downsample_padding=0,
num_classes=num_classes)
print(model)
return model
cbam_resnet50()
6.2 Keras版本
def CBAM(input, channel, ratio):
# channel attention
avg_pool = GlobalAveragePooling2D()(input)
avg_pool = Dense(channel // ratio, avtivation='relu')(avg_pool)
max_pool = GlobalAveragePooling2D()(input)
max_pool = Dense(channel // ratio, avtivation='relu')(max_pool)
avg_pool = Dense(channel, avtivation=None)(avg_pool)
max_pool = Dense(channel, avtivation=None)(max_pool)
mask = Add()([avg_pool, max_pool])
mask = Activation('sigmoid')(mask)
x = multiply([input,mask])
# spatial attention
avg_pool = Lambda(lambda x: K.mean(x, axis=3, keepdims=True))(x)
max_pool = Lambda(lambda x: K.max(x, axis=3, keepdims=True))(x)
concat = Concatenate(axis=3)([avg_pool, max_pool])
# x = Conv2D(8, (1, 1), padding='same', activation='tanh')(x)
mask = Conv2D(1, (1, 1), padding='same', activation='sigmoid')(x)
output = multiply([x, mask])
return output