图像分类用最简短的代码复现SeNet,小白一定要收藏(keras,Tensorflow2.x)

Posted AI浩

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了图像分类用最简短的代码复现SeNet,小白一定要收藏(keras,Tensorflow2.x)相关的知识,希望对你有一定的参考价值。

目录

摘要

一、SENet概述

二、SENet 结构组成详解

三、详细的计算过程

                                                  ​

SENet 在具体网络中应用(代码实现SE_ResNet)

第一个残差模块

第二个残差模块

ResNet18、ResNet34模型的完整代码

ResNet50、ResNet101、ResNet152完整代码


摘要

一、SENet概述

           Squeeze-and-Excitation Networks(简称 SENet)是 Momenta 胡杰团队(WMW)提出的新的网络结构,利用SENet,一举取得最后一届 ImageNet 2017 竞赛 Image Classification 任务的冠军,在ImageNet数据集上将top-5 error降低到2.251%,原先的最好成绩是2.991%。

     作者在文中将SENet block插入到现有的多种分类网络中,都取得了不错的效果。作者的动机是希望显式地建模特征通道之间的相互依赖关系。另外,作者并未引入新的空间维度来进行特征通道间的融合,而是采用了一种全新的「特征重标定」策略。具体来说,就是通过学习的方式来自动获取到每个特征通道的重要程度,然后依照这个重要程度去提升有用的特征并抑制对当前任务用处不大的特征。

     通俗的来说SENet的核心思想在于通过网络根据loss去学习特征权重,使得有效的feature map权重大,无效或效果小的feature map权重小的方式训练模型达到更好的结果。SE block嵌在原有的一些分类网络中不可避免地增加了一些参数和计算量,但是在效果面前还是可以接受的 。Sequeeze-and-Excitation(SE) block并不是一个完整的网络结构,而是一个子结构,可以嵌到其他分类或检测模型中。

二、SENet 结构组成详解

    上述结构中,Squeeze 和 Excitation 是两个非常关键的操作,下面进行详细说明。

  

    上图是SE 模块的示意图。给定一个输入 x,其特征通道数为 {C}',通过一系列卷积等一般变换后得到一个特征通道数为C 的特征。通过下面的三个操作还重标前面得到的特征:

   1、Squeeze 操作,顺着空间维度来进行特征压缩,将每个二维的特征通道变成一个实数,这个实数某种程度上具有全局的感受野,并且输出的维度和输入的特征通道数相匹配。它表征着在特征通道上响应的全局分布,而且使得靠近输入的层也可以获得全局的感受野,这一点在很多任务中都是非常有用的。

  2、 Excitation 操作,它是一个类似于循环神经网络中门的机制。通过参数 w 来为每个特征通道生成权重,其中参数 w 被学习用来显式地建模特征通道间的相关性。

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

三、详细的计算过程

 首先F_{tr}这一步是转换操作(严格讲并不属于SENet,而是属于原网络,可以看后面SENet和Inception及ResNet网络的结合),在文中就是一个标准的卷积操作而已,输入输出的定义如下表示:

                                       

    那么这个F_{tr}的公式就是下面的公式1(卷积操作,V_{c}表示第c个卷积核,X^{s}表示第s个输入)。

                                                
    F_{tr}得到的U就是Figure1中的左边第二个三维矩阵,也叫tensor,或者叫C个大小为H*W的feature map。而uc表示U中第c个二维矩阵,下标c表示channel。
    接下来就是Squeeze操作,公式非常简单,就是一个global average pooling:
                              
    因此公式2就将H*W*C的输入转换成1*1*C的输出,对应Figure1中的Fsq操作。为什么会有这一步呢?这一步的结果相当于表明该层C个feature map的数值分布情况,或者叫全局信息。
    再接下来就是Excitation操作,如公式3。直接看最后一个等号,前面squeeze得到的结果是z,这里先用W1乘以z,就是一个全连接层操作,W1的维度是C/r * C,这个r是一个缩放参数,在文中取的是16,这个参数的目的是为了减少channel个数从而降低计算量。又因为z的维度是1*1*C,所以W1z的结果就是1*1*C/r;然后再经过一个ReLU层,输出的维度不变;然后再和W2相乘,和W2相乘也是一个全连接层的过程,W2的维度是C*C/r,因此输出的维度就是1*1*C;最后再经过sigmoid函数,得到s:
                                    
    也就是说最后得到的这个s的维度是1*1*C,C表示channel数目。这个s其实是本文的核心,它是用来刻画tensor U中C个feature map的权重。而且这个权重是通过前面这些全连接层和非线性层学习得到的,因此可以end-to-end训练。这两个全连接层的作用就是融合各通道的feature map信息,因为前面的squeeze都是在某个channel的feature map里面操作。
    在得到s之后,就可以对原来的tensor U操作了,就是下面的公式4。也很简单,就是channel-wise multiplication,什么意思呢?u_{c}是一个二维矩阵,s_{c}是一个数,也就是权重,因此相当于把u_{c}矩阵中的每个值都乘以s_{c}。对应Figure1中的Fscale。

                                                  

SENet 在具体网络中应用(代码实现SE_ResNet)

介绍完具体的公式实现,下面介绍下SE block怎么运用到具体的网络之中。


    上图是将 SE 模块嵌入到 Inception 结构的一个示例。方框旁边的维度信息代表该层的输出。

    这里我们使用 global average pooling 作为 Squeeze 操作。紧接着两个 Fully Connected 层组成一个 Bottleneck 结构去建模通道间的相关性,并输出和输入特征同样数目的权重。我们首先将特征维度降低到输入的 1/16,然后经过 ReLu 激活后再通过一个 Fully Connected 层升回到原来的维度。这样做比直接用一个 Fully Connected 层的好处在于:

    1)具有更多的非线性,可以更好地拟合通道间复杂的相关性;

    2)极大地减少了参数量和计算量。然后通过一个 Sigmoid 的门获得 0~1 之间归一化的权重,最后通过一个 Scale 的操作来将归一化后的权重加权到每个通道的特征上。

    除此之外,SE 模块还可以嵌入到含有 skip-connections 的模块中。上右图是将 SE 嵌入到 ResNet 模块中的一个例子,操作过程基本和 SE-Inception 一样,只不过是在 Addition 前对分支上 Residual 的特征进行了特征重标定。如果对 Addition 后主支上的特征进行重标定,由于在主干上存在 0~1 的 scale 操作,在网络较深 BP 优化时就会在靠近输入层容易出现梯度消散的情况,导致模型难以优化。

    目前大多数的主流网络都是基于这两种类似的单元通过 repeat 方式叠加来构造的。由此可见,SE 模块可以嵌入到现在几乎所有的网络结构中。通过在原始网络结构的 building block 单元中嵌入 SE 模块,我们可以获得不同种类的 SENet。如 SE-BN-Inception、SE-ResNet、SE-ReNeXt、SE-Inception-ResNet-v2 等等。

本例通过实现SE-ResNet,来显示如何将SE模块嵌入到ResNet网络中。SE-ResNet模型如下图:

第一个残差模块

第一个残差模块用于实现ResNet18、ResNet34模型,SENet嵌入到第二个卷积的后面。

# 第一个残差模块
class BasicBlock(layers.Layer):
    def __init__(self, filter_num, stride=1):
        super(BasicBlock, self).__init__()
        self.conv1 = layers.Conv2D(filter_num, (3, 3), strides=stride, padding='same')
        self.bn1 = layers.BatchNormalization()
        self.relu = layers.Activation('relu')
        self.conv2 = layers.Conv2D(filter_num, (3, 3), strides=1, padding='same')
        self.bn2 = layers.BatchNormalization()
        # se-block
        self.se_globalpool = keras.layers.GlobalAveragePooling2D()
        self.se_resize = keras.layers.Reshape((1, 1, filter_num))
        self.se_fc1 = keras.layers.Dense(units=filter_num // 16, activation='relu',
                                         use_bias=False)
        self.se_fc2 = keras.layers.Dense(units=filter_num, activation='sigmoid',
                                         use_bias=False)
        if stride != 1:
            self.downsample = Sequential()
            self.downsample.add(layers.Conv2D(filter_num, (1, 1), strides=stride))
        else:
            self.downsample = lambda x: x

    def call(self, input, training=None):
        out = self.conv1(input)
        out = self.bn1(out)
        out = self.relu(out)
        out = self.conv2(out)
        out = self.bn2(out)
        # se_block
        b = out
        out = self.se_globalpool(out)
        out = self.se_resize(out)
        out = self.se_fc1(out)
        out = self.se_fc2(out)
        out = keras.layers.Multiply()([b, out])

        identity = self.downsample(input)
        output = layers.add([out, identity])
        output = tf.nn.relu(output)
        return output

第二个残差模块

第二个残差模块用于实现ResNet50、ResNet101、ResNet152模型,SENet模块嵌入到第三个卷积后面。

# 第二个残差模块
class Block(layers.Layer):
    def __init__(self, filters, downsample=False, stride=1):
        super(Block, self).__init__()
        self.downsample = downsample
        self.conv1 = layers.Conv2D(filters, (1, 1), strides=stride, padding='same')
        self.bn1 = layers.BatchNormalization()
        self.relu = layers.Activation('relu')
        self.conv2 = layers.Conv2D(filters, (3, 3), strides=1, padding='same')
        self.bn2 = layers.BatchNormalization()
        self.conv3 = layers.Conv2D(4 * filters, (1, 1), strides=1, padding='same')
        self.bn3 = layers.BatchNormalization()
        # se-block
        self.se_globalpool = keras.layers.GlobalAveragePooling2D()
        self.se_resize = keras.layers.Reshape((1, 1, 4 * filters))
        self.se_fc1 = keras.layers.Dense(units=4 * filters // 16, activation='relu',
                                         use_bias=False)
        self.se_fc2 = keras.layers.Dense(units=4 * filters, activation='sigmoid',
                                         use_bias=False)

        if self.downsample:
            self.shortcut = Sequential()
            self.shortcut.add(layers.Conv2D(4 * filters, (1, 1), strides=stride))
            self.shortcut.add(layers.BatchNormalization(axis=3))

    def call(self, input, training=None):
        out = self.conv1(input)
        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)
        b = out
        out = self.se_globalpool(out)
        out = self.se_resize(out)
        out = self.se_fc1(out)
        out = self.se_fc2(out)
        out = keras.layers.Multiply()([b, out])
        if self.downsample:
            shortcut = self.shortcut(input)
        else:
            shortcut = input
        output = layers.add([out, shortcut])
        output = tf.nn.relu(output)
        return output

ResNet18、ResNet34模型的完整代码

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


# 第一个残差模块
class BasicBlock(layers.Layer):
    def __init__(self, filter_num, stride=1):
        super(BasicBlock, self).__init__()
        self.conv1 = layers.Conv2D(filter_num, (3, 3), strides=stride, padding='same')
        self.bn1 = layers.BatchNormalization()
        self.relu = layers.Activation('relu')
        self.conv2 = layers.Conv2D(filter_num, (3, 3), strides=1, padding='same')
        self.bn2 = layers.BatchNormalization()
        # se-block
        self.se_globalpool = keras.layers.GlobalAveragePooling2D()
        self.se_resize = keras.layers.Reshape((1, 1, filter_num))
        self.se_fc1 = keras.layers.Dense(units=filter_num // 16, activation='relu',
                                         use_bias=False)
        self.se_fc2 = keras.layers.Dense(units=filter_num, activation='sigmoid',
                                         use_bias=False)
        if stride != 1:
            self.downsample = Sequential()
            self.downsample.add(layers.Conv2D(filter_num, (1, 1), strides=stride))
        else:
            self.downsample = lambda x: x

    def call(self, input, training=None):
        out = self.conv1(input)
        out = self.bn1(out)
        out = self.relu(out)
        out = self.conv2(out)
        out = self.bn2(out)
        # se_block
        b = out
        out = self.se_globalpool(out)
        out = self.se_resize(out)
        out = self.se_fc1(out)
        out = self.se_fc2(out)
        out = keras.layers.Multiply()([b, out])

        identity = self.downsample(input)
        output = layers.add([out, identity])
        output = tf.nn.relu(output)
        return output


class ResNet(keras.Model):
    def __init__(self, layer_dims, num_classes=10):
        super(ResNet, self).__init__()
        # 预处理层
        self.padding = keras.layers.ZeroPadding2D((3, 3))
        self.stem = Sequential([
            layers.Conv2D(64, (7, 7), strides=(2, 2)),
            layers.BatchNormalization(),
            layers.Activation('relu'),
            layers.MaxPool2D(pool_size=(3, 3), strides=(2, 2), padding='same')
        ])
        # resblock
        self.layer1 = self.build_resblock(64, layer_dims[0])
        self.layer2 = self.build_resblock(128, layer_dims[1], stride=2)
        self.layer3 = self.build_resblock(256, layer_dims[2], stride=2)
        self.layer4 = self.build_resblock(512, layer_dims[3], stride=2)
        # 全局池化
        self.avgpool = layers.GlobalAveragePooling2D()
        # 全连接层
        self.fc = layers.Dense(num_classes, activation=tf.keras.activations.softmax)

    def call(self, input, training=None):
        x= self.padding(input)
        x = self.stem(x)
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)
        # [b,c]
        x = self.avgpool(x)
        x = self.fc(x)
        return x

    def build_resblock(self, filter_num, blocks, stride=1):
        res_blocks = Sequential()
        res_blocks.add(BasicBlock(filter_num, stride))
        for pre in range(1, blocks):
            res_blocks.add(BasicBlock(filter_num, stride=1))
        return res_blocks


def ResNet34(num_classes=10):
    return ResNet([2, 2, 2, 2], num_classes=num_classes)


def ResNet34(num_classes=10):
    return ResNet([3, 4, 6, 3], num_classes=num_classes)


model = ResNet34(num_classes=1000)
model.build(input_shape=(1, 224, 224, 3))
print(model.summary())  # 统计网络参数

ResNet50、ResNet101、ResNet152完整代码

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


# 第二个残差模块
class Block(layers.Layer):
    def __init__(self, filters, downsample=False, stride=1):
        super(Block, self).__init__()
        self.downsample = downsample
        self.conv1 = layers.Conv2D(filters, (1, 1), strides=stride, padding='same')
        self.bn1 = layers.BatchNormalization()
        self.relu = layers.Activation('relu')
        self.conv2 = layers.Conv2D(filters, (3, 3), strides=1, padding='same')
        self.bn2 = layers.BatchNormalization()
        self.conv3 = layers.Conv2D(4 * filters, (1, 1), strides=1, padding='same')
        self.bn3 = layers.BatchNormalization()
        # se-block
        self.se_globalpool = keras.layers.GlobalAveragePooling2D()
        self.se_resize = keras.layers.Reshape((1, 1, 4 * filters))
        self.se_fc1 = keras.layers.Dense(units=4 * filters // 16, activation='relu',
                                         use_bias=False)
        self.se_fc2 = keras.layers.Dense(units=4 * filters, activation='sigmoid',
                                         use_bias=False)

        if self.downsample:
            self.shortcut = Sequential()
            self.shortcut.add(layers.Conv2D(4 * filters, (1, 1), strides=stride))
            self.shortcut.add(layers.BatchNormalization(axis=3))

    def call(self, input, training=None):
        out = self.conv1(input)
        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)
        b = out
        out = self.se_globalpool(out)
        out = self.se_resize(out)
        out = self.se_fc1(out)
        out = self.se_fc2(out)
        out = keras.layers.Multiply()([b, out])
        if self.downsample:
            shortcut = self.shortcut(input)
        else:
            shortcut = input
        output = layers.add([out, shortcut])
        output = tf.nn.relu(output)
        return output


class ResNet(keras.Model):
    def __init__(self, layer_dims, num_classes=10):
        super(ResNet, self).__init__()
        # 预处理层
        self.padding = keras.layers.ZeroPadding2D((3, 3))
        self.stem = Sequential([
            layers.Conv2D(64, (7, 7), strides=(2, 2)),
            layers.BatchNormalization(),
            layers.Activation('relu'),
            layers.MaxPool2D(pool_size=(3, 3), strides=(2, 2), padding='same')
        ])
        # resblock
        self.layer1 = self.build_resblock(64, layer_dims[0],stride=1)
        self.layer2 = self.build_resblock(128, layer_dims[1], stride=2)
        self.layer3 = self.build_resblock(256, layer_dims[2], stride=2)
        self.layer4 = self.build_resblock(512, layer_dims[3], stride=2)
        # 全局池化
        self.avgpool = layers.GlobalAveragePooling2D()
        # 全连接层
        self.fc = layers.Dense(num_classes, activation=tf.keras.activations.softmax)

    def call(self, input, training=None):
        x = self.padding(input)
        x = self.stem(x)
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)
        # [b,c]
        x = self.avgpool(x)
        x = self.fc(x)
        return x

    def build_resblock(self, filter_num, blocks, stride=1):
        res_blocks = Sequential()
        if stride != 1 or filter_num * 4 != 64:
            res_blocks.add(Block(filter_num, downsample=True,stride=stride))
        for pre in range(1, blocks):
            res_blocks.add(Block(filter_num, stride=1))
        return res_blocks


def ResNet50(num_classes=10):
    return ResNet([3, 4, 6, 3], num_classes=num_classes)


def ResNet101(num_classes=10):
    return ResNet([3, 4, 23, 3], num_classes=num_classes)

def ResNet152(num_classes=10):
    return ResNet([3, 8, 36, 3], num_classes=num_classes)

model = ResNet50(num_classes=1000)
model.build(input_shape=(1, 224, 224, 3))
print(model.summary())  # 统计网络参数


 

以上是关于图像分类用最简短的代码复现SeNet,小白一定要收藏(keras,Tensorflow2.x)的主要内容,如果未能解决你的问题,请参考以下文章

SeNet论文解读/总结

SENet代码复现+超详细注释(PyTorch)

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

用最简短的代码搞定数组去重

图像分类用通俗易懂代码的复现EfficientNetV2,入门的绝佳选择(pytorch)

图像分类实战——使用EfficientNetV2实现图像分类(Pytorch)