当前位置:   article > 正文

【卷积神经网络系列】十、SENet_bn卷积神经网络

bn卷积神经网络

参考资料

论文:

Squeeze-and-Excitation Networks

博客:

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

SENet解析

在特征通道提升网络性能 --SENet网络详解


一、简介

 在深度学习领域,CNN分类网络的发展对其它计算机视觉任务如目标检测和语义分割都起到至关重要的作用,因为检测和分割模型通常是构建在CNN分类网络(称为backbone)之上。提到CNN分类网络,我们所熟知的是VGG,ResNet,Inception,DenseNet等模型,它们的效果已经被充分验证,而且被广泛应用在各类计算机视觉任务上。这里我们介绍一篇CVPR2017的文章SENet,它在2017年最后一届 ImageNet 挑战赛(ILSVRC) 任务中获得 冠军,将错误率降低到 2.251% ;重要的一点是SENet思路很简单,很容易扩展在已有网络结构中。

 Squeeze-and-Excitation Networks 简称SENet,目标是通过建模卷积特征通道之间的相互依赖关系来提高网络的表示能力;SE block并不是一个完整的网络结构,而是一个子结构,可以嵌入到其他分类或检测模型中。

SENet 的核心思想在于通过网络根据 loss 去学习特征权重,使得有效的 feature map 权重大,无效或效果小的 feature map 权重小的方式训练模型达到更好的结果

  • Squeeze挤压
  • Excitation激励

二、网络结构

2.1 传统卷积

 卷积神经网络建立在卷积运算的基础上,通过融合局部感受野内的空间信息和通道信息来提取信息特征。

 从本质上讲,卷积是对一个局部区域进行特征融合,这包括空间上(W和H维度)以及通道间(C 维度)的特征融合,而对于卷积操作,很大一部分工作是提高感受野,即空间上融合更多特征,或者是提取多尺度空间信息。

在这里插入图片描述

从空间维度层面来提升网络的性能:

  • 下图左:Inception 结构中嵌入了多尺度信息,聚合多种不同感受野上的特征来获得性能增益;
  • 下图右:Inside-Outside 网络中考虑了空间中的上下文信息;

在这里插入图片描述


2.2 SENet结构

中心思想:对于每个输出 channel,预测一个常数权重,对每个channel进行加权,本质上,SE模块是在channel维度上做 attention 或者 gating 操作,这种注意力机制让模型可以更加关注信息量最大的channel特征,而抑制那些不重要的 channel 特征。SENet可以很方便地集成到现有网络中,提升网络性能,并且代价很小。

 总的来说SE Block就是在 Layer 的输入和输出之间添加结构:
Global Average Pooling - FC - ReLU - FC- Sigmoid
SE block 的灵活性意味着它可以直接应用于标准卷积以外的转换,通过将SE block集成到任何复杂模型当中来开发SENet

在这里插入图片描述

(1)Ftr:

 转换操作,一个或一组标准的卷积操作,将输入 X X X变为输出 U U U

在这里插入图片描述

(2)Squeeze: Global Information Embedding

挤压:全局信息嵌入

 即为压缩部分,顺着空间维度来进行特征压缩,将每个二维的特征通道变成一个实数,这个实数某种程度上具有全局的感受野,并且输出的维度和输入的特征通道数相匹配。它表征着在特征通道上响应的全局分布,而且使得靠近输入的层也可以获得全局的感受野;

 假设原始feature map的为 H × W × C 2 H\times W\times C_2 H×W×C2 ,其中 H H H是高度(Height), W W W是宽度(Width), C 2 C_2 C2是通道数(channel)。Squeeze做的事情是把 H × W × C 2 H\times W\times C_2 H×W×C2压缩为 1 × 1 × C 2 1\times 1\times C_2 1×1×C2,相当于把 H × W × C 2 H\times W\times C_2 H×W×C2压缩成一维的实数,这里采用**global average pooling**:

在这里插入图片描述

(3)Excitation: Adaptive Recalibration

激励:自适应调整

 每个通道通过一个基于通道依赖gate自选门机制来学习特定样本的激活,使其学会使用全局信息,有选择地强调信息特征,并抑制不太有用的特征,这里采用的是sigmoid,并在中间嵌入了ReLU函数用于限制模型的复杂性和帮助训练 ;

 通过两个全连接层(FC) 构成的瓶颈来参数化门控机制,即W1用于降低维度,W2用于维度递增 ;

在这里插入图片描述

 最后得到的s的维度是 1 × 1 × C 1\times 1\times C 1×1×C C C C表示 c h a n n e l channel channel数目。这个s其实是本文的核心,它是用来刻画tensor U中C个feature map的权重。而且这个权重是通过前面这些全连接层和非线性层学习得到的,因此可以end-to-end训练。这两个全连接层的作用就是融合各通道的feature map信息,因为前面的squeeze都是在某个channel的feature map里面操作。其中整个操作可以看成学到了各个channel的权重系数,使得模型对各个 channel 的特征更有辨识能力。

(4)Scale:Reweight

尺度:重新加权

 将Excitation的输出的权重看做是进行过特征选择后的每个特征通道的重要性,然后通过乘法逐通道加权到先前的特征上,完成在通道维度上的对原始特征的重标定。

在这里插入图片描述

U U U是一个 W × H × C W\times H \times C W×H×C的矩阵, S S S是前面得到的 1 × 1 × C 1\times 1\times C 1×1×C的矩阵, S c S_c Sc U c U_c Uc相乘相当于按通道 C C C W × H W\times H W×H中的每个值都乘以 S S S 1 × 1 1\times 1 1×1的部分。


2.3 应用

2.3.1 SE-Inception Module

 应用于非残差网络 Inception network 当中,形成 SE-Inception module

在这里插入图片描述

2.3.2 SE-ResNet Module

 应用于 残差网络 Residual network 当中,形成 SE-ResNet module

在这里插入图片描述

2.3.3 论文中的具体应用

SE-Inception Module:我们首先将特征维度降低到输入的 1/16(即缩放系数r=16),然后经过 ReLu 激活后再通过一个Fully Connected升回到原来的维度。这样做比直接用一个 Fully Connected 层的好处在于具有更多的非线性,可以更好地拟合通道间复杂的相关性;极大地减少了参数量和计算量。然后通过一个Sigmoid获得 0~1 之间归一化的权重,最后通过一个Scale的操作来将归一化后的权重加权到每个通道的特征上。

SE-ResNet Module:上右图是将SE嵌入到ResNet模块中的例子,操作过程基本和SE-Inception一样,只不过是在Addition前对分支上Residual的特征进行了特征重标定

如果对Addition后主支上的特征进行重标定,由于在主干上存在0~1的scale操作,在网络较深BP优化时就会在靠近输入层容易出现梯度消散的情况,导致模型难以优化。


三、论文复现

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


(1)定义不带ReLu的卷积:卷积+BN

class ConvBN(nn.Module):
    def __init__(self, in_channels, out_channels, **kwargs):
        super(ConvBN, self).__init__()
        self.conv = nn.Conv2d(in_channels, out_channels, **kwargs)
        self.bn = nn.BatchNorm2d(out_channels)

    def forward(self, x):
        x = self.conv(x)
        x = self.bn(x)
        return x
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

(2)SE-Block

在这里插入图片描述

class SE_Block(nn.Module):                         # Squeeze-and-Excitation block
    def __init__(self, in_channels):
        super(SE_Block, self).__init__()
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))     # GAP
        self.conv1 = nn.Conv2d(in_channels, in_channels // 16, kernel_size=1)   # 1x1的卷积核充当FC
        self.relu = nn.ReLU()
        self.conv2 = nn.Conv2d(in_channels // 16, in_channels, kernel_size=1)   # 1x1的卷积核充当FC
        self.sigmoid = nn.Sigmoid()

    def forward(self, x):
        x = self.avgpool(x)
        x = self.conv1(x)
        x = self.relu(x)
        x = self.conv2(x)
        out = self.sigmoid(x)
        return out
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

(3)BasicBlock

在这里插入图片描述
在这里插入图片描述

class BasicBlock(nn.Module):      # 左侧的 residual block 结构(18-layer、34-layer)

    # 如果是BasicBlock,每个小block的输入=输出
    expansion = 1
    
    def __init__(self, in_channels, out_channels, stride=1):      # 两层卷积 Conv2d + Shutcuts
        super(BasicBlock, self).__init__()
        
        # 两个3x3卷积核
        self.conv = nn.Sequential(
            # 第一个cov是用来改变WxH的(stride可指定)
            ConvBN(in_channels=in_channels, out_channels=out_channels, 
                       kernel_size=3, stride=stride, padding=1),
            
            nn.ReLU(inplace=True),
            
            # 第二个conv的stride恒为1,不改变WxH
            ConvBN(in_channels=out_channels, out_channels=out_channels, 
                   kernel_size=3, stride=1, padding=1),     
        )
        
        # 新增的SE-Block
        self.SE = SE_Block(out_channels)           # Squeeze-and-Excitation block

        self.shortcut = nn.Sequential()
        
        # Shortcuts用于构建 Conv Block 和 Identity Block
        if stride != 1 or in_channels != self.expansion*out_channels:
            self.shortcut = nn.Sequential(
                # 卷积+BN,不激活
                ConvBN(in_channels=in_channels, out_channels=self.expansion*out_channels, 
                   kernel_size=1, stride=stride)
            )
            
        self.relu = nn.ReLU(inplace=True)

    def forward(self, x):
        out = self.conv(x)
        
        SE_out = self.SE(out)   # 经过Residual-block后再经过SE-block
        
        out = out * SE_out      # 对位相乘,重新加权
        
        out += self.shortcut(x)
        return self.relu(out)
  • 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

(4)Bottleneck

在这里插入图片描述
在这里插入图片描述

class Bottleneck(nn.Module):      # 右侧的 residual block 结构(50-layer、101-layer、152-layer)
    
    # 观察50-layer可以发现,各个block内部卷积核通道数是4倍的关系
    expansion = 4
    
    def __init__(self, in_channels, out_channels, stride=1):      # 三层卷积 Conv2d + Shutcuts
        super(Bottleneck, self).__init__()
        
        # 1x1 -> 3x3 -> 1x1
        self.conv = nn.Sequential(
            # 第一个cov是用来降维的,减少参数量
            ConvBN(in_channels=in_channels, out_channels=out_channels, 
                   kernel_size=1),
            nn.ReLU(inplace=True),
            
            # 第二个conv是用来改变WxH的(stride可指定)
            ConvBN(in_channels=out_channels, out_channels=out_channels, 
                   kernel_size=3, stride=stride, padding=1),
            nn.ReLU(inplace=True),  
            
            # 第三个conv用来升维
            ConvBN(in_channels=out_channels, out_channels=self.expansion*out_channels, 
                   kernel_size=1)       
        )  
        
        self.SE = SE_Block(self.expansion*out_channels)           # Squeeze-and-Excitation block
        
        self.shortcut = nn.Sequential()
        
        # Shortcuts用于构建 Conv Block 和 Identity Block
        if stride != 1 or in_channels != self.expansion*out_channels:
            self.shortcut = nn.Sequential(
                # 卷积+BN,不激活
                ConvBN(in_channels=in_channels, out_channels=self.expansion*out_channels, 
                   kernel_size=1, stride=stride)
            )
            
        self.relu = nn.ReLU(inplace=True)

    def forward(self, x):
        out = self.conv(x)
        
        SE_out = self.SE(out)   # 经过Residual-block后再经过SE-block
        
        out = out * SE_out      # 对位相乘,重新加权
        
        out += self.shortcut(x)
        return self.relu(out)
  • 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

(5)主干网络:

在这里插入图片描述
在这里插入图片描述

class SE_ResNet(nn.Module):
    def __init__(self, block, numlist_blocks, num_classes=2):
        """
        Args:
            block:      选用BasicBlock还是Bottleneck这两种残差结构
            num_blocks: 针对不同数量的layers,有不同的组合,比如ResNet50为[3, 4, 6, 3]
            num_classes:最终分类数量
        """
        super(SE_ResNet, self).__init__()
        
        self.in_channels = 64

        # 原始输入为229x229x3 -> 112x112x64
        self.conv1 = ConvBN(in_channels=3, out_channels=64, kernel_size=7, stride=2) # conv1
        # 112x112x64 -> 56x56x64
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, ceil_mode=True)         # maxpool
        
        self.layer1 = self._make_layer(block, 64,  numlist_blocks[0], stride=1)      # conv2_x
        self.layer2 = self._make_layer(block, 128, numlist_blocks[1], stride=2)      # conv3_x
        self.layer3 = self._make_layer(block, 256, numlist_blocks[2], stride=2)      # conv4_x
        self.layer4 = self._make_layer(block, 512, numlist_blocks[3], stride=2)      # conv5_x
        
        
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))     # 平均池化
        self.linear = nn.Linear(2048, num_classes)      # 线性层
        self.relu = nn.ReLU(inplace=True)

    def _make_layer(self, block, in_channels, num_blocks, stride):
        # 虽然每个convn_x由多个block组成,但是其中只有某个block的stride为2,剩余的为1
        strides = [stride] + [1]*(num_blocks-1)
        layers = []
        
        for stride in strides:
            layers.append(block(self.in_channels, in_channels, stride))
            
            # 经过某个convn_x之后,in_channels被放大对应expansion倍
            self.in_channels = in_channels * block.expansion
        
        return nn.Sequential(*layers)

    def forward(self, x):
        x = self.relu(self.conv1(x))    # conv1
        x = self.maxpool(x)             # maxpool
        x = self.layer1(x)              # conv2_x
        x = self.layer2(x)              # conv3_x
        x = self.layer3(x)              # conv4_x
        x = self.layer4(x)              # conv5_x
        
        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        out = self.linear(x)
        
        return out
  • 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

(6)构建不同的网络

def make_net(net, num_classes):
    if net == 'SE_ResNet18':
        return SE_ResNet(BasicBlock, [2, 2, 2, 2], num_classes)
    if net == 'SE_ResNet34':
        return SE_ResNet(BasicBlock, [3, 4, 6, 3], num_classes)
    if net == 'SE_ResNet50':
        return SE_ResNet(Bottleneck, [3, 4, 6, 3], num_classes)
    if net == 'SE_ResNet101':
        return SE_ResNet(Bottleneck, [3, 4, 23, 3], num_classes)
    if net == 'SE_ResNet152':
        return SE_ResNet(Bottleneck, [3, 8, 36, 3], num_classes)
        
def test():
    SE_ResNet50 = make_net('SE_ResNet50', num_classes=2)
    #创建模型,部署gpu
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    SE_ResNet50.to(device)
    summary(SE_ResNet50, (3, 229, 229))
    
test()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/467767
推荐阅读
相关标签
  

闽ICP备14008679号