CBAM: Convolutional Block Attention Module 卷積塊注意模塊詳解

image

論文地址:https://arxiv.org/abs/1807.06521

1. 摘要

我們提出了卷積塊注意模塊 (CBAM), 一個簡單而有效的注意模塊的前饋卷積神經網絡。給出了一個中間特征映射, 我們的模塊按照兩個獨立的維度、通道和空間順序推斷出注意力映射, 然后將注意力映射相乘為自適應特征細化的輸入特征映射。因為 CBAM 是一個輕量級和通用的模塊, 它可以無縫地集成到任何 CNN 架構只增加微不足道的間接開銷, 可以集成到端到端的CNN里面去。通過對 ImageNet-1K、COCO、MS 檢測和 VOC 2007 檢測數據集的廣泛實驗, 我們驗證了我們的 CBAM。我們的實驗表明, 各種模型的分類和檢測性能都有了一致的改進, 證明了 CBAM 的廣泛適用性。這些代碼和模型將公開提供。


在這里插入圖片描述

2. 相關工作

網絡架構的構建,一直是計算機視覺中最重要的研究之一, 因為精心設計的網絡確保了在各種應用中顯著的性能提高。自成功實施大型 CNN以來, 已經提出了一系列廣泛的體系結構。一種直觀而簡單的擴展方法是增加神經網絡的深度如 VGG-NET、ResNet及其變體,如 WideResNet和 ResNeXt。GoogLeNet展現了增加網絡的寬度對于結果的提升的幫助,典型的分類網絡都在提升深度與寬度上下了很大功夫。

眾所周知, 注意力在人的知覺中起著重要的作用。一個人并不是試圖一次處理整個場景。相反, 人類注意部分場景, 并有選擇地專注于突出部分, 以便更好地捕捉視覺結構。

最近, 有幾次嘗試加入注意處理, 以提高CNNs在大規模分類任務的性能。Residual attention network for image classification中使用 encoder-decoder 樣式的注意模塊的Residual attention network。通過細化特征映射,不僅網絡性能良好, 而且對噪聲輸入也很健壯。我們不直接計算3d 的注意力映射, 而是分解了單獨學習通道注意和空間注意的過程。對于3D 特征圖, 單獨的注意生成過程的計算和參數開銷要小得多, 因此可以作為CNN的前置基礎架構的模塊使用。

Squeeze-and-excitation networks引入一個緊湊模塊來利用通道間的關系。在他們的壓縮和激勵模塊中, 他們使用全局平均池功能來計算通道的注意力。然而, 我們表明, 這些都是次優特征, 以推斷良好的通道注意, 我們使用最大池化的特點。然而,他們也錯過了空間注意力機制, 在決定 "Where"。在我們的 CBAM 中, 我們利用一個有效的體系結構來開發空間和通道的注意力, 并通過經驗驗證, 利用兩者都優于僅使用通道的注意作為。此外, 我們的實驗表明, 我們的模塊在檢測任務 (MS COCO和 VOC2017)上是有效的。特別是, 我們通過將我們的模塊放在VOC2007 測試集中的現有的目標檢測器結合實現了最先進的性能。

3. Convolutional Block Attention Module

給定一個中間特征映射F∈RC xHxW作為輸入, CBAM的1維通道注意圖Mc ∈RC ×1×1 和2D 空間注意圖Ms ∈R1×HxW 如圖1所示??偟淖⒁膺^程可以概括為:


在這里插入圖片描述

表示逐元素相乘。在相乘過程中,注意值被廣播。相應地,通道注意值被沿著空間維度廣播,反之亦然。F’’是最終輸出。


在這里插入圖片描述

3.1 Channel attention module

我們利用特征的通道間關系, 生成了通道注意圖。當一個特征圖的每個通道被考慮作為特征探測器, 通道注意聚焦于 ' what ' 是有意義的輸入圖像。為了有效地計算通道的注意力, 我們壓縮了輸入特征圖的空間維數。為了聚焦空間信息,我們同時使用平均池化和最大池化。我們的實驗證實, 同時使用這兩種功能大大提高了網絡的表示能力。下面將描述詳細操作。

我們首先使用平均池化和最大池化操作來聚合特征映射的空間信息, 生成兩個不同的空間上下文描述符:Fcavg 和Fcmax , 分別表示平均池化和最大池化。兩個描述符然后送到一個共享網絡, 以產生我們的通道注意力圖 Mc ∈ Rc×1×1。共享網絡由多層感知機(MLP) 和一個隱藏層組成。為了減少參數開銷, 隱藏的激活大小被設置為 rc/c++×1×1, 其中 r 是壓縮率。在將共享網絡應用于每個描述符之后, 我們使用逐元素求和合并輸出特征向量。簡而言之, 通道的注意力被計算為:


在這里插入圖片描述

3.2 Spatial attention module

我們利用特征間的空間關系, 生成空間注意圖。與通道注意力不同的是, 空間注意力集中在 "where" 是一個信息的部分, 這是對通道注意力的補充。為了計算空間注意力, 我們首先在通道軸上應用平均池和最大池運算, 并將它們連接起來以生成一個有效的特征描述符。在串聯特征描述符上, 我們應用7×7的卷積生成空間注意圖的層Ms (F) ∈RH×W 。我們描述下面的詳細操作.

我們使用兩個池化操作來聚合功能映射的通道信息, 生成兩個2維映射:Fsavg∈R1×HxW 和Fsmax∈R1×HxW 每個通道都表示平均池化和最大池化。然后通過一個標準的卷積層連接和卷積混合, 產生我們的2D 空間注意圖。簡而言之, 空間注意力被計算為:

在這里插入圖片描述

在這里插入圖片描述

通過實驗我們發現串聯兩個注意力模塊的效果要優于并聯。通道注意力放在前面要優于空間注意力模塊放在前面。

4. 實驗

在本小節中,我們憑實驗證明了我們的設計選擇的有效性。 在這次實驗中,我們使用ImageNet-1K數據集并采用ResNet-50作為基礎架構。 ImageNet-1K分類數據集[1]由1.2組成用于訓練的百萬個圖像和用于1,000個對象類的驗證的50,000個圖像

我們采用相同的數據增強方案進行訓練和測試時間進行單一作物評估,大小為224×224。 學習率從0.1開始,每30個時期下降一次。 我們訓練網絡90迭代。

4.1 通道注意力和空間注意力機制實驗

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

4.2 使用Grad-CAM進行網絡可視化

在這里插入圖片描述

在這里插入圖片描述

4.3 CBAM在目標檢測的結果

在這里插入圖片描述

在這里插入圖片描述

5. 總結

作者提出了一種提高 CNN 網絡表示的新方法--卷積瓶頸注意模塊 (CBAM)。作者將基于注意力的特征細化成兩個不同的模塊、通道和空間結合起來, 實現了顯著的性能改進, 同時保持了小的開銷。對于通道的關注,使用最大池化和平均池化,最終模塊 (CBAM) 學習了如何有效地強調或壓縮提取中間特征。為了驗證它的有效性, 我們進行了廣泛的實驗與并證實, CBAM 優于所有基線上的三不同的基準數據集: ImageNet-1K, COCO, 和 VOC 2007。此外, 我們可視化模塊如何準確推斷給定的輸入圖像。CBAM 或許會成為各種網絡體系結構的重要組成部分。

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

7. 最后

image
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容