[ 注意力机制 ] 经典网络模型2——CBAM 详解与复现

Posted Horizon Max

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[ 注意力机制 ] 经典网络模型2——CBAM 详解与复现相关的知识,希望对你有一定的参考价值。


🤵 AuthorHorizon Max

编程技巧篇各种操作小结

🎇 机器视觉篇会变魔术 OpenCV

💥 深度学习篇简单入门 PyTorch

🏆 神经网络篇经典网络模型

💻 算法篇再忙也别忘了 LeetCode


[ 注意力机制 ] 经典网络模型2——CBAM 详解与复现

🚀 Convolutional Block Attention Module

Convolutional Block Attention Module 简称 CBAM,Sanghyun等人于2018年提出的一种新的 卷积注意力模块

创新提出了 通道注意力与空间注意力融合 的注意力机制 ;

对前馈卷积神经网络 是一个 简单而有效的 注意力模块 ;

因为它的 轻量级和通用性 ,可以 无缝集成到任何CNN网络 当中 ;

作者实验表明,不同的模型在 分类和检测性能 上都有持续的提高 ;


🔗 论文地址:CBAM: Convolutional Block Attention Module


🚀 CBAM 详解

🎨 背景知识

为提高 CNN性能 ,最近的研究主要研究了网络的三个重要因素: depth(深度) , width(宽度) , cardinality(基数)

从20世纪90年代 LeNet 网络的提出,网络的 深度 不断增加;
后来 VGG 网络表明,相同形状的块堆叠 效果良好;
GoogLeNet 网络的提出,提出宽度 也是提高模型性能的另一个重要因素;
同样的,ResNet残差块 以相同拓扑与跳跃式连接堆叠在一起,构建了一个非常深的架构,达到了不错的效果;
XceptionResNeXt 网络表明,增加网络 基数 不仅减少了参数量,而且比另 两个因素(深度和宽度) 具有更强的表示能力;

除了这些因素之外,作者还研究了网络设计的另一个方面—— 注意力
“注意力” 也是 人类视觉系统 的一个很有趣的地方 ;
通过注意力机制来增加网络的表征力:关注重要特征,抑制不必要特征

卷积运算是通过将 跨通道信息和空间信息混合 在一起来提取信息特征的 ;
因此提出了 CBAM 来强调通道轴和空间轴这两个主要维度上的有意义特征 ;
并对此依次应用了 Channel Attention Module (通道注意模块)Spatial Attention Module (空间注意模块)


Convolutional Block Attention Module


🎨 论文贡献

(1)提出了一个简单而有效的注意力模块(CBAM),可以广泛应用于提高 CNN 的表示能力 ;
(2)通过广泛的消融研究来验证我们的注意力模块的有效性 ;
(3)通过插入轻量级模块(CBAM),验证了各种网络的性能在多个基准(ImageNet-1K、MS COCO和VOC 2007)上都得到了极大的提高;


假设 输入特征图为 : F ∈ R CxHxW
利用 CBAM 依此推导出 一维通道注意图 : Mc ∈ R Cx1x1二维空间注意图 : Ms ∈ R 1xHxW
总的注意过程可以概括为 :

🎨 Convolutional Block Attention Module

🚩 Channel Attention Module

利用 特征间的通道关系 来生成通道注意图 ;

由于feature map的每个channel都被认为是 一个feature检测器 ,因此 channel 的注意力集中在 给定输入图像的 "什么" 是有意义的
为了有效地计算通道注意力,采用 压缩输入特征映射的空间维度 的方法 ;
文中同时使用 AvgPool (平均池化)MaxPool (最大池化) 的方法,并证明了这种做法比单独使用一种池化方法更具有表征力;


式中,σ 为 sigmoid 函数 ,W0 ∈ RC/r×C ,W1 ∈ RC×C/r ,MLP的权重 W0 和 W1 共享,在W0 前是 ReLU 激活函数 ;


🚩 Spatial Attention Module

利用 特征间的空间关系 生成空间注意图 ;

与通道注意模块不同的是,空间注意模块关注的是 信息部分 "在哪里" ,作为通道注意模块的补充 ;
为了计算空间注意力,首先沿着通道轴应用 平均池化和最大池化 操作,并将它们连接起来以生成一个有效的 特征描述符

使用两个池化操作聚合一个feature map的通道信息,生成两个2D maps :
Fsavg ∈ R1×H×W 和 Fsmax ∈ R1×H×W
每个都表示通道的 平均池化特性最大池化特性 ,然后利用一个标准的卷积层进行连接和卷积操作,得到二维空间注意力图 ;


式中,σ 为 sigmoid 函数 ,f 7x7 为 7 x 7 大小的卷积核 ;



🚩 CBAM 的应用

以上是将 CBAM 结合 ResBlock 应用于ResNet中 ;
两个模块可以以并行或顺序的方式放置,实验测试发现 顺序排列并行排列 有更好的结果 ;


最后,分别使用 ResNet50ResNet50+SENetResNet50+CBAM 进行实验得到可视化结果 :

实验表明 CBAM 性能超越了 SENet


🚀 CBAM 复现

这里实现的是 CBAM-ResNet 系列网络 :

# Here is the code :

import torch
import torch.nn as nn
import torch.nn.functional as F
from torchinfo import summary


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


class SpatialAttention(nn.Module):           # Spatial Attention Module
    def __init__(self):
        super(SpatialAttention, self).__init__()
        self.conv1 = nn.Conv2d(2, 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


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, planes, kernel_size=3,
                               stride=stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(planes)
        self.conv2 = nn.Conv2d(planes, planes, kernel_size=3,
                               stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(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


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


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


def CBAM_ResNet18():
    return CBAM_ResNet(BasicBlock, [2, 2, 2, 2])


def CBAM_ResNet34():
    return CBAM_ResNet(BasicBlock, [3, 4, 6, 3])


def CBAM_ResNet50():
    return CBAM_ResNet(Bottleneck, [3, 4, 6, 3])


def CBAM_ResNet101():
    return CBAM_ResNet(Bottleneck, [3, 4, 23, 3])


def CBAM_ResNet152():
    return CBAM_ResNet(Bottleneck, [3, 8, 36, 3])


def test():
    net = CBAM_ResNet50()
    y = net(torch.randn(1, 3, 224, 224))
    print(y.size())
    summary(net, (1, 3, 224, 224))


if __name__ == '__main__':
    test()

输出结果:

torch.Size([1, 1000])
===============================================================================================
Layer (type:depth-idx)                        Output Shape              Param #
===============================================================================================
CBAM_ResNet                                   --                        --
├─Conv2d: 1-1                                 [1, 64, 224, 224]         1,728
├─BatchNorm2d: 1-2                            [1, 64, 224, 224]         128
├─Sequential: 1-3                             [1, 256, 224, 224]        --
│    └─Bottleneck: 2-1                        [1, 256, 224, 224]        --
│    │    └─Conv2d: 3-1                       [1, 64, 224, 224]         4,096
│    │    └─BatchNorm2d: 3-2                  [1, 64, 224, 224]         128
│    │    └─Conv2d: 3-3                       [1, 64, 224, 224]         36,864
│    │    └─BatchNorm2d: 3-4                  [1, 64, 224, 224]         128
│    │    └─Conv2d: 3-5                       [1, 256, 224, 224]        16,384
│    │    └─BatchNorm2d: 3-6                  [1, 256, 224, 224]        512
│    │    └─ChannelAttention: 3-7             [1, 256, 1, 1]            8,192
│    │    └─SpatialAttention: 3-8             [1, 1, 1, 1]              98
│    │    └─Sequential: 3-9                   [<

深度学习 CNN中的混合域注意力机制(DANet,CBAM),附Tensorflow完整代码

各位同学好,今天和大家分享一下如何使用 Tensorflow 构建 DANetCBAM 混合域注意力机制模型。在之前的文章中我介绍了CNN中的通道注意力机制 SENet 和 ECANet,感兴趣的可以看一下:https://blog.csdn.net/dgvv4/article/details/123572065


1. 注意力机制介绍

注意力机制本质上是一种资源分配机制,它可以根据关注目标的重要性程度改变资源分配方式,使资源更多的向attention的对象倾斜。在卷积神经网络中,注意力机制所要分配的资源就是权重参数。在模型训练过程中对于attention的对象分配更多的权重参数,能够提高对于attention对象的特征提取能力。在目标检测任务中添加注意力机制,可以提高模型的表征能力,有效减少无效目标的千扰,提升对关注目标的检测效果,进而提高模型的整体检测准确率。


2. CBAM 注意力机制

2.1 方法介绍

CBAM注意力机制是由通道注意力机制(channel)和空间注意力机制(spatial)组成。

CNAM注意力机制的优点:

(1)轻量化程度高:CBAM模块内部无大量卷积结构,少量池化层和特征融合操作,这种结构避免了卷积乘法带来的大量计算,使得其模块复杂度低,计算量小。实验证明,在轻量模型上添加CBAM模块能够带来稳定的性能提升,相较于其带来的少量的计算量的增加,CBAM的引入具有很局的性价比。

(2)通用性强:其结构特点决定了CBAM的通用性强,可移植性高,主要体现在两方面:一方面,基于池化操作的CBAM模块能够直接嵌入到卷积操作后,这意味着该模块可以添加到诸如VGG的传统神经网络中,同时也可以添加到包含基于shortcut连接的残差结构的网络中,如ResNet50、MobileNetV3;另一方面,CBAM同时适用于目标检测和分类任务,且对于不同数据特征的数据集,在检测或者分类精度上均能取得较好的性能提升。

(3)作用效果佳:传统基于卷积神经网络的注意力机制更多的是关注对通道域的分析,局限于考虑特征图通道之间的作用关系。CBAM从 channel 和 spatial 两个作用域出发,引入空间注意力和通道注意力两个分析维度,实现从通道到空间的顺序注意力结构。空间注意力可使神经网络更加关注图像中对分类起决定作用的像素区域而忽略无关紧要的区域通道注意力则用于处理特征图通道的分配关系,同时对两个维度进行注意力分配增强了注意力机制对模型性能的提升效果。


2.2 网络结构

(1)通道注意力机制

CBAM中的通道注意力机制模块流程图如下。先将输入特征图分别进行全局最大池化和全局平均池化,对特征映射基于两个维度压缩,获得两张不同维度的特征描述。池化后的特征图共用一个多层感知器网络,先通过1*1卷积降维再1*1卷积升维。将两张特征图叠加layers.add(),经过sigmoid激活函数归一化特征图的每个通道的权重将归一化后的权重和输入特征图相乘。

代码展示

#(1)通道注意力
def channel_attenstion(inputs, ratio=0.25):
    '''ratio代表第一个全连接层下降通道数的倍数'''

    channel = inputs.shape[-1]  # 获取输入特征图的通道数

    # 分别对输出特征图进行全局最大池化和全局平均池化
    # [h,w,c]==>[None,c]
    x_max = layers.GlobalMaxPooling2D()(inputs)
    x_avg = layers.GlobalAveragePooling2D()(inputs)

    # [None,c]==>[1,1,c]
    x_max = layers.Reshape([1,1,-1])(x_max)  # -1代表自动寻找通道维度的大小
    x_avg = layers.Reshape([1,1,-1])(x_avg)  # 也可以用变量channel代替-1

    # 第一个全连接层通道数下降1/4, [1,1,c]==>[1,1,c//4]
    x_max = layers.Dense(channel*ratio)(x_max)
    x_avg = layers.Dense(channel*ratio)(x_avg)

    # relu激活函数
    x_max = layers.Activation('relu')(x_max)
    x_avg = layers.Activation('relu')(x_avg)

    # 第二个全连接层上升通道数, [1,1,c//4]==>[1,1,c]
    x_max = layers.Dense(channel)(x_max)
    x_avg = layers.Dense(channel)(x_avg)

    # 结果在相叠加 [1,1,c]+[1,1,c]==>[1,1,c]
    x = layers.Add()([x_max, x_avg])

    # 经过sigmoid归一化权重
    x = tf.nn.sigmoid(x)

    # 输入特征图和权重向量相乘,给每个通道赋予权重
    x = layers.Multiply()([inputs, x])  # [h,w,c]*[1,1,c]==>[h,w,c]

    return x

(2)空间注意力机制

CBAM中的空间注意力机制模块如下。对通道注意力机制的输出特征图进行空间域的处理。首先,特征图分别经过基于通道维度的最大池化和平均池化将输出的两张特征图在通道维度堆叠 layers.concatenate()。然后使用1*1卷积调整通道数,最后经过sigmoid函数归一化权重。将归一化权重和输入特征度相乘。

代码展示

#(2)空间注意力机制
def spatial_attention(inputs):

    # 在通道维度上做最大池化和平均池化[b,h,w,c]==>[b,h,w,1]
    # keepdims=Fale那么[b,h,w,c]==>[b,h,w]
    x_max = tf.reduce_max(inputs, axis=3, keepdims=True)  # 在通道维度求最大值
    x_avg = tf.reduce_mean(inputs, axis=3, keepdims=True)  # axis也可以为-1

    # 在通道维度上堆叠[b,h,w,2]
    x = layers.concatenate([x_max, x_avg])

    # 1*1卷积调整通道[b,h,w,1]
    x = layers.Conv2D(filters=1, kernel_size=(1,1), strides=1, padding='same')(x)

    # sigmoid函数权重归一化
    x = tf.nn.sigmoid(x)

    # 输入特征图和权重相乘
    x = layers.Multiply()([inputs, x])

    return x

(3)总体流程

CBAM的总体流程图如下。输入特征图想经过通道注意力机制,将权重和输入特征图相乘后再送入空间注意力机制,将归一化权重和空间注意力机制的输入特征图相乘,得到最终的特征图。

完整代码展示

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, Model

#(1)通道注意力
def channel_attenstion(inputs, ratio=0.25):
    '''ratio代表第一个全连接层下降通道数的倍数'''

    channel = inputs.shape[-1]  # 获取输入特征图的通道数

    # 分别对输出特征图进行全局最大池化和全局平均池化
    # [h,w,c]==>[None,c]
    x_max = layers.GlobalMaxPooling2D()(inputs)
    x_avg = layers.GlobalAveragePooling2D()(inputs)

    # [None,c]==>[1,1,c]
    x_max = layers.Reshape([1,1,-1])(x_max)  # -1代表自动寻找通道维度的大小
    x_avg = layers.Reshape([1,1,-1])(x_avg)  # 也可以用变量channel代替-1

    # 第一个全连接层通道数下降1/4, [1,1,c]==>[1,1,c//4]
    x_max = layers.Dense(channel*ratio)(x_max)
    x_avg = layers.Dense(channel*ratio)(x_avg)

    # relu激活函数
    x_max = layers.Activation('relu')(x_max)
    x_avg = layers.Activation('relu')(x_avg)

    # 第二个全连接层上升通道数, [1,1,c//4]==>[1,1,c]
    x_max = layers.Dense(channel)(x_max)
    x_avg = layers.Dense(channel)(x_avg)

    # 结果在相叠加 [1,1,c]+[1,1,c]==>[1,1,c]
    x = layers.Add()([x_max, x_avg])

    # 经过sigmoid归一化权重
    x = tf.nn.sigmoid(x)

    # 输入特征图和权重向量相乘,给每个通道赋予权重
    x = layers.Multiply()([inputs, x])  # [h,w,c]*[1,1,c]==>[h,w,c]

    return x

#(2)空间注意力机制
def spatial_attention(inputs):

    # 在通道维度上做最大池化和平均池化[b,h,w,c]==>[b,h,w,1]
    # keepdims=Fale那么[b,h,w,c]==>[b,h,w]
    x_max = tf.reduce_max(inputs, axis=3, keepdims=True)  # 在通道维度求最大值
    x_avg = tf.reduce_mean(inputs, axis=3, keepdims=True)  # axis也可以为-1

    # 在通道维度上堆叠[b,h,w,2]
    x = layers.concatenate([x_max, x_avg])

    # 1*1卷积调整通道[b,h,w,1]
    x = layers.Conv2D(filters=1, kernel_size=(1,1), strides=1, padding='same')(x)

    # sigmoid函数权重归一化
    x = tf.nn.sigmoid(x)

    # 输入特征图和权重相乘
    x = layers.Multiply()([inputs, x])

    return x

#(3)CBAM注意力
def CBAM_attention(inputs):

    # 先经过通道注意力再经过空间注意力
    x = channel_attenstion(inputs)
    x = spatial_attention(x)
    return x

#(4)构建模型结构
if __name__ == '__main__':
    
    # 构建输入层
    inputs = keras.Input(shape=[26,26,512])
    # CBAM注意力机制
    x = CBAM_attention(inputs)
    # 构建模型
    model = Model(inputs, x)
    # 查看模型结构
    model.summary()

参数量如下

Total params: 263,427
Trainable params: 263,427
Non-trainable params: 0

3. DANet 注意力机制

DANet 注意力机制由位置注意力机制(position)和通道注意力机制(channel)组合而成。

位置注意力机制负责捕获特征图在任意两个位置的空间依赖关系,无论距离如何,类似的特征都会彼此相关。通道注意力机制负责整合所有通道映射之间的相关特征来选择性地强调存在相互依赖的通道映射


 3.1 位置注意力机制

位置注意力机制的流程图如下

(1)输入特征图A(C×H×W)首先分别通过3个卷积层得到3个特征图B,C,D,然后将B,C,D reshape为C×N,其中N=H×W

(2)然后将reshape后的特征图B的转置(NxC)与reshape后的特征图C(CxN)矩阵相乘tf.multul(),再通过softmax得到归一化后的权重 S(N×N)

(3)接着在reshape后的特征图D(CxN)和权重S的转置(NxN)之间执行矩阵乘法tf.multul(),再乘以尺度系数α,再reshape为原来形状,其中α初始化为0,并逐渐的学习得到更大的权重

(4)最后与输入特征图A相叠加layers.add()得到最后的输出E

代码展示

# 位置注意力
def position_attention(inputs):
    # 定义可训练变量,反向传播可更新
    gama = tf.Variable(tf.ones(1))  # 初始化1

    # 获取输入特征图的shape
    b, h, w, c = inputs.shape

    # 深度可分离卷积[b,h,w,c]==>[b,h,w,c//8]
    x1 = layers.SeparableConv2D(filters=c//8, kernel_size=(1,1), strides=1, padding='same')(inputs)
    # 调整维度排序[b,h,w,c//8]==>[b,c//8,h,w]
    x1_trans = tf.transpose(x1, perm=[0,3,1,2])
    # 重塑特征图尺寸[b,c//8,h,w]==>[b,c//8,h*w]
    x1_trans_reshape = tf.reshape(x1_trans, shape=[-1,c//8,h*w])
    # 调整维度排序[b,c//8,h*w]==>[b,h*w,c//8]
    x1_trans_reshape_trans = tf.transpose(x1_trans_reshape, perm=[0,2,1])
    # 矩阵相乘
    x1_mutmul = x1_trans_reshape_trans @ x1_trans_reshape
    # 经过softmax归一化权重
    x1_mutmul = tf.nn.softmax(x1_mutmul)

    # 深度可分离卷积[b,h,w,c]==>[b,h,w,c]
    x2 = layers.SeparableConv2D(filters=c, kernel_size=(1,1), strides=1, padding='same')(inputs)
    # 调整维度排序[b,h,w,c]==>[b,c,h,w]
    x2_trans = tf.transpose(x2, perm=[0,3,1,2])
    # 重塑尺寸[b,c,h,w]==>[b,c,h*w]
    x2_trans_reshape = tf.reshape(x2_trans, shape=[-1,c,h*w])

    # 调整x1_mutmul的轴,和x2矩阵相乘
    x1_mutmul_trans = tf.transpose(x1_mutmul, perm=[0,2,1])
    x2_mutmul = x2_trans_reshape @ x1_mutmul_trans

    # 重塑尺寸[b,c,h*w]==>[b,c,h,w]
    x2_mutmul = tf.reshape(x2_mutmul, shape=[-1,c,h,w])
    # 轴变换[b,c,h,w]==>[b,h,w,c]
    x2_mutmul = tf.transpose(x2_mutmul, perm=[0,2,3,1])
    # 结果乘以可训练变量
    x2_mutmul = x2_mutmul * gama

    # 输入和输出叠加
    x = layers.add([x2_mutmul, inputs])
    return x

3.2 通道注意力模块

通道注意力模块的流程图如下。

(1)分别对特征图A做reshape(CxN)以及reshape与transpose(NxC);

(2)将得到的两个特征图矩阵相乘tf.multul(),再通过softmax得到归一化后的权重X(C×C)

(3)接着把权重X的转置(CxC)与reshape后的特征图A(CxN)做矩阵乘法tf.multul(),再乘以尺度系数β,再reshape为原来形状。其中β初始化为0,并逐渐的学习得到更大的权重

(4)最后与输入特征图A相叠加得到最后的输出特征图E

代码展示

# 通道注意力
def channel_attention(inputs):
    # 定义可训练变量,反向传播可更新
    gama = tf.Variable(tf.ones(1))  # 初始化1

    # 获取输入特征图的shape
    b, h, w, c = inputs.shape

    # 重新排序维度[b,h,w,c]==>[b,c,h,w]
    x = tf.transpose(inputs, perm=[0,3,1,2])  # perm代表重新排序的轴
    # 重塑特征图尺寸[b,c,h,w]==>[b,c,h*w]
    x_reshape = tf.reshape(x, shape=[-1,c,h*w])

    # 重新排序维度[b,c,h*w]==>[b,h*w,c]
    x_reshape_trans = tf.transpose(x_reshape, perm=[0,2,1])  # 指定需要交换的轴
    # 矩阵相乘
    x_mutmul = x_reshape_trans @ x_reshape
    # 经过softmax归一化权重
    x_mutmul = tf.nn.softmax(x_mutmul)

    # reshape后的特征图与归一化权重矩阵相乘[b,x,h*w]
    x = x_reshape @ x_mutmul
    # 重塑形状[b,c,h*w]==>[b,c,h,w]
    x = tf.reshape(x, shape=[-1,c,h,w])
    # 重新排序维度[b,c,h,w]==>[b,h,w,c]
    x = tf.transpose(x, perm=[0,2,3,1])
    # 结果乘以可训练变量
    x = x * gama

    # 输入和输出特征图叠加
    x = layers.add([x, inputs])

    return x

3.3 总体流程

DANet的总体流程图如下,输入图像分别经过位置注意力机制和通道注意力机制,将输出的特征图叠加layers.add(),得到输出特征图。

完整代码展示

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, Model

#(1)通道注意力
def channel_attention(inputs):
    # 定义可训练变量,反向传播可更新
    gama = tf.Variable(tf.ones(1))  # 初始化1

    # 获取输入特征图的shape
    b, h, w, c = inputs.shape

    # 重新排序维度[b,h,w,c]==>[b,c,h,w]
    x = tf.transpose(inputs, perm=[0,3,1,2])  # perm代表重新排序的轴
    # 重塑特征图尺寸[b,c,h,w]==>[b,c,h*w]
    x_reshape = tf.reshape(x, shape=[-1,c,h*w])

    # 重新排序维度[b,c,h*w]==>[b,h*w,c]
    x_reshape_trans = tf.transpose(x_reshape, perm=[0,2,1])  # 指定需要交换的轴
    # 矩阵相乘
    x_mutmul = x_reshape_trans @ x_reshape
    # 经过softmax归一化权重
    x_mutmul = tf.nn.softmax(x_mutmul)

    # reshape后的特征图与归一化权重矩阵相乘[b,x,h*w]
    x = x_reshape @ x_mutmul
    # 重塑形状[b,c,h*w]==>[b,c,h,w]
    x = tf.reshape(x, shape=[-1,c,h,w])
    # 重新排序维度[b,c,h,w]==>[b,h,w,c]
    x = tf.transpose(x, perm=[0,2,3,1])
    # 结果乘以可训练变量
    x = x * gama

    # 输入和输出特征图叠加
    x = layers.add([x, inputs])

    return x

#(2)位置注意力
def position_attention(inputs):
    # 定义可训练变量,反向传播可更新
    gama = tf.Variable(tf.ones(1))  # 初始化1

    # 获取输入特征图的shape
    b, h, w, c = inputs.shape

    # 深度可分离卷积[b,h,w,c]==>[b,h,w,c//8]
    x1 = layers.SeparableConv2D(filters=c//8, kernel_size=(1,1), strides=1, padding='same')(inputs)
    # 调整维度排序[b,h,w,c//8]==>[b,c//8,h,w]
    x1_trans = tf.transpose(x1, perm=[0,3,1,2])
    # 重塑特征图尺寸[b,c//8,h,w]==>[b,c//8,h*w]
    x1_trans_reshape = tf.reshape(x1_trans, shape=[-1,c//8,h*w])
    # 调整维度排序[b,c//8,h*w]==>[b,h*w,c//8]
    x1_trans_reshape_trans = tf.transpose(x1_trans_reshape, perm=[0,2,1])
    # 矩阵相乘
    x1_mutmul = x1_trans_reshape_trans @ x1_trans_reshape
    # 经过softmax归一化权重
    x1_mutmul = tf.nn.softmax(x1_mutmul)

    # 深度可分离卷积[b,h,w,c]==>[b,h,w,c]
    x2 = layers.SeparableConv2D(filters=c, kernel_size=(1,1), strides=1, padding='same')(inputs)
    # 调整维度排序[b,h,w,c]==>[b,c,h,w]
    x2_trans = tf.transpose(x2, perm=[0,3,1,2])
    # 重塑尺寸[b,c,h,w]==>[b,c,h*w]
    x2_trans_reshape = tf.reshape(x2_trans, shape=[-1,c,h*w])

    # 调整x1_mutmul的轴,和x2矩阵相乘
    x1_mutmul_trans = tf.transpose(x1_mutmul, perm=[0,2,1])
    x2_mutmul = x2_trans_reshape @ x1_mutmul_trans

    # 重塑尺寸[b,c,h*w]==>[b,c,h,w]
    x2_mutmul = tf.reshape(x2_mutmul, shape=[-1,c,h,w])
    # 轴变换[b,c,h,w]==>[b,h,w,c]
    x2_mutmul = tf.transpose(x2_mutmul, perm=[0,2,3,1])
    # 结果乘以可训练变量
    x2_mutmul = x2_mutmul * gama

    # 输入和输出叠加
    x = layers.add([x2_mutmul, inputs])
    return x

#(3)DANet网络架构
def danet(inputs):

    # 输入分为两个分支
    x1 = channel_attention(inputs)  # 通道注意力
    x2 = position_attention(inputs)  # 位置注意力

    # 叠加两个注意力的结果
    x = layers.add([x1,x2])
    return x

# 构建网络
if __name__ == '__main__':

    # 构造输入层
    inputs = keras.Input(shape=[26,26,512])
    # 经过DANet注意力机制返回结果
    outputs = danet(inputs)

    # 构造模型
    model = Model(inputs, outputs)
    # 查看模型结构
    model.summary()

查看网络参数量

Total params: 296,512
Trainable params: 296,512
Non-trainable params: 0

以上是关于[ 注意力机制 ] 经典网络模型2——CBAM 详解与复现的主要内容,如果未能解决你的问题,请参考以下文章

注意机制CBAM

深度学习 CNN中的混合域注意力机制(DANet,CBAM),附Tensorflow完整代码

YOLOv5添加注意力机制的具体步骤

CBAM 解读混合注意力机制:Convolutional Block Attention Module

CBAM 解读混合注意力机制:Convolutional Block Attention Module

CBAM 解读混合注意力机制:Convolutional Block Attention Module