经典卷积网络–InceptionNet

借鉴点:一层内使用不同尺寸的卷积核,提升感知力(通过 padding 实现输出特征面积一致); 使用 1 * 1 卷积核,改变输出特征 channel 数(减少网络参数)。

1、InceptionNet网络模型

  InceptionNet 即 GoogLeNet,诞生于 2015 年,旨在通过增加网络的宽度来提升网络的能力,与 VGGNet 通过卷积层堆叠的方式(纵向)相比,是一个不同的方向(横向)。

  显然,InceptionNet 模型的构建与 VGGNet 及之前的网络会有所区别,不再是简单的纵向堆叠,要理解 InceptionNet 的结构,首先要理解它的基本单元,如图1.1所示。

image-20220515162127253

  可以看到,InceptionNet 的基本单元中,卷积部分是比较统一的 C、B、A 典型结构,即 卷积→BN→激活,激活均采用 Relu 激活函数,同时包含最大池化操作。

  在 Tensorflow 框架下利用 Keras 构建 InceptionNet 模型时,可以将 C、B、A 结构封装 在一起,定义成一个新的 ConvBNRelu 类,以减少代码量,同时更便于阅读。

class ConvBNRelu(Model):
    def __init__(self, ch, kernelsz=3, strides=1, padding='same'):
        super(ConvBNRelu, self).__init__()
        self.model = tf.keras.models.Sequential([
            Conv2D(ch, kernelsz, strides=strides, padding=padding),
            BatchNormalization(),
            Activation('relu')
        ])

    def call(self, x):
        x = self.model(x, training=False) #在training=False时,BN通过整个训练集计算均值、方差去做批归一化,training=True时,通过当前batch的均值、方差去做批归一化。推理时 training=False效果好
        return x

  参数 ch 代表特征图的通道数,也即卷积核个数;kernelsz 代表卷积核尺寸;strides 代表卷积步长;padding 代表是否进行全零填充。

  完成了这一步后,就可以开始构建 InceptionNet 的基本单元了,同样利用 class 定义的 方式,定义一个新的 InceptionBlk 类,如图1.2所示。

image-20220515163839211

  参数 ch 仍代表通道数,strides 代表卷积步长,与 ConvBNRelu 类中一致;tf.concat 函数将四个输出连接在一起,x1、x2_2、x3_2、x4_2 分别代表图1.1中的四列输出,结合结构图和代码很容易看出二者的对应关系。

  可以看到,InceptionNet 的一个显著特点是大量使用了 1 * 1 的卷积核,事实上,最原始 的 InceptionNet 的结构是不包含 1 * 1 卷积的,如图1.3所示。

image-20220515164022525

  由图 1.3可以更清楚地看出 InceptionNet 最初的设计思想,即通过不同尺寸卷积层和 池化层的横向组合(卷积、池化后的尺寸相同,通道可以相加)来拓宽网络深度,可以增加 网络对尺寸的适应性。但是这样也带来一个问题,所有的卷积核都会在上一层的输出上直接做卷积运算,会导致参数量和计算量过大(尤其是对于 5 * 5 的卷积核来说)。因此, InceptionNet 在 3 * 3、5 * 5 的卷积运算前、最大池化后均加入了 1 * 1 的卷积层,形成了图 1.1中的结构,这样可以降低特征的厚度,一定程度上避免参数量过大的问题。

2、1 * 1的卷积运算是如何降低特征厚度?

  下面以5 * 5的卷积运算为例说明这个 问题。假设网络上一层的输出为 100 * 100 * 128(H * W * C),通过 32 * 5 * 5(32 个大小 为 5 * 5 的卷积核)的卷积层(步长为 1、全零填充)后,输出为 100 * 100 * 32,卷积层的 参数量为 32 * 5 * 5 * 128 = 102400;如果先通过 32 * 1 * 1 的卷积层(输出为 100 * 100 * 32), 再通过 32 * 5 * 5 的卷积层,输出仍为 100 * 100 * 32,但卷积层的参数量变为 32 * 1 * 1 * 128 + 32 * 5 * 5 * 32 = 29696,仅为原参数量的 30 %左右,这就是小卷积核的降维作用。

  InceptionNet 网络的主体就是由其基本单元构成的,其模型结构如图 2.1 所示。

image-20220515164323501

  图中橙色框内即为 InceptionNet 的基本单元,利用之前定义好的 InceptionBlk 类堆叠而 成,模型的实现代码如下。

class Inception10(Model):
    def __init__(self, num_blocks, num_classes, init_ch=16, **kwargs):
        super(Inception10, self).__init__(**kwargs)
        self.in_channels = init_ch
        self.out_channels = init_ch
        self.num_blocks = num_blocks
        self.init_ch = init_ch
        self.c1 = ConvBNRelu(init_ch)
        self.blocks = tf.keras.models.Sequential()
        for block_id in range(num_blocks):
            for layer_id in range(2):
                if layer_id == 0:
                    block = InceptionBlk(self.out_channels, strides=2)
                else:
                    block = InceptionBlk(self.out_channels, strides=1)
                self.blocks.add(block)
            # enlarger out_channels per block
            self.out_channels *= 2
        self.p1 = GlobalAveragePooling2D()
        self.f1 = Dense(num_classes, activation='softmax')

    def call(self, x):
        x = self.c1(x)
        x = self.blocks(x)
        x = self.p1(x)
        y = self.f1(x)
        return y

  参数 num_layers 代表 InceptionNetBlock 数,每个 Block 由两个基本单元构成,每经 过一个 Block,特征图尺寸变为 1/2,通道数变为 2 倍;num_classes 代表分类数,对于 cifar10 数据集来说即为 10;init_ch 代表初始通道数,也即 InceptionNet 基本单元的初始卷积核个数。

  InceptionNet 网络不再像 VGGNet 一样有三层全连接层(全连接层的参数量占 VGGNet 总参数量的 90 %),而是采用“全局平均池化+全连接层”的方式,这减少了大量的参数

  这里介绍一下全局平均池化,在 tf.keras 中用 GlobalAveragePooling2D 函数实现,相比 于平均池化(在特征图上以窗口的形式滑动,取窗口内的平均值为采样值),全局平均池化 不再以窗口滑动的形式取均值,而是直接针对特征图取平均值,即每个特征图输出一个值。 通过这种方式,每个特征图都与分类概率直接联系起来,这替代了全连接层的功能,并且不 产生额外的训练参数,减小了过拟合的可能,但需要注意的是,使用全局平均池化会导致网络收敛的速度变慢。

  总体来看,InceptionNet 采取了多尺寸卷积再聚合的方式拓宽网络结构,并通过 1 * 1 的卷积运算来减小参数量,取得了比较好的效果,与同年诞生的 VGGNet 相比,提供了卷积神经网络构建的另一种思路。但 InceptionNet 的问题是,当网络深度不断增加时,训练会十分困难,甚至无法收敛(这一点被 ResNet 很好地解决了)。

3、InceptionNet完整实现(使用CIFAR10数据集)

import tensorflow as tf
import os
import numpy as np
from matplotlib import pyplot as plt
from tensorflow.keras.layers import Conv2D, BatchNormalization, Activation, MaxPool2D, Dropout, Flatten, Dense, \
    GlobalAveragePooling2D
from tensorflow.keras import Model

np.set_printoptions(threshold=np.inf)

cifar10 = tf.keras.datasets.cifar10
(x_train, y_train), (x_test, y_test) = cifar10.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0

# 将C、B、A操作封装,减少代码量
class ConvBNRelu(Model):
    # ch:特征图的通道数,也即卷积核个数
    # kernelsz代表卷积核尺寸
    # strides代表卷积步长
    # padding代表是否进行全零填充
    def __init__(self, ch, kernelsz=3, strides=1, padding='same'):
        super(ConvBNRelu, self).__init__()
        self.model = tf.keras.models.Sequential([
            Conv2D(ch, kernelsz, strides=strides, padding=padding),
            BatchNormalization(),
            Activation('relu')
        ])

    def call(self, x):
        x = self.model(x, training=False) #在training=False时,BN通过整个训练集计算均值、方差去做批归一化,training=True时,通过当前batch的均值、方差去做批归一化。推理时 training=False效果好
        return x

# InceptionNet基本单元,定义成class
class InceptionBlk(Model):
    def __init__(self, ch, strides=1):
        super(InceptionBlk, self).__init__()
        self.ch = ch
        self.strides = strides
        self.c1 = ConvBNRelu(ch, kernelsz=1, strides=strides)
        self.c2_1 = ConvBNRelu(ch, kernelsz=1, strides=strides)
        self.c2_2 = ConvBNRelu(ch, kernelsz=3, strides=1)
        self.c3_1 = ConvBNRelu(ch, kernelsz=1, strides=strides)
        self.c3_2 = ConvBNRelu(ch, kernelsz=5, strides=1)
        self.p4_1 = MaxPool2D(3, strides=1, padding='same')
        self.c4_2 = ConvBNRelu(ch, kernelsz=1, strides=strides)

    def call(self, x):
        x1 = self.c1(x)
        x2_1 = self.c2_1(x)
        x2_2 = self.c2_2(x2_1)
        x3_1 = self.c3_1(x)
        x3_2 = self.c3_2(x3_1)
        x4_1 = self.p4_1(x)
        x4_2 = self.c4_2(x4_1)
        # concat along axis=channel
        # 将四个输出堆叠在一起,x1, x2_2, x3_2, x4_2分别代表四列输出
        # axis=3指定堆叠的维度是沿深度方向
        x = tf.concat([x1, x2_2, x3_2, x4_2], axis=3)
        return x

# InceptionNet模型结构
class Inception10(Model):
    # num_blocks代表InceptionNet的Block数,每个Block由两个基本单元构成,没经过
    # 一个Block,特征图尺寸变为1/2,通道数变为2倍
    # num_classes 代表分类数,对于 cifar10数据集来说即为 10
    # init_ch代表初始通道数,也即InceptionNet基本单元的初始卷积核个数
    def __init__(self, num_blocks, num_classes, init_ch=16, **kwargs):
        super(Inception10, self).__init__(**kwargs)
        self.in_channels = init_ch
        self.out_channels = init_ch
        self.num_blocks = num_blocks
        self.init_ch = init_ch
        self.c1 = ConvBNRelu(init_ch)   # 第一层3*3卷积
        self.blocks = tf.keras.models.Sequential()
        # 每两个Inception结构块组成一个block
        for block_id in range(num_blocks):
            for layer_id in range(2):
                if layer_id == 0:   # 每次第1个Inception2结构块卷积步长是2,使得输出特征图尺寸减半
                    block = InceptionBlk(self.out_channels, strides=2)
                else:               # 每次第2个Inception2结构块卷积步长是1
                    block = InceptionBlk(self.out_channels, strides=1)
                self.blocks.add(block)
            # enlarger out_channels per block
            # 加深输出特征图深度,尽可能保持特征抽取中信息的承载量一致。
            self.out_channels *= 2
        # 平均池化
        self.p1 = GlobalAveragePooling2D()
        self.f1 = Dense(num_classes, activation='softmax')

    def call(self, x):
        x = self.c1(x)
        x = self.blocks(x)
        x = self.p1(x)
        y = self.f1(x)
        return y

# 每两个Inception结构块组成一个block,10分类
model = Inception10(num_blocks=2, num_classes=10)

model.compile(optimizer='adam',
              loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False),
              metrics=['sparse_categorical_accuracy'])

checkpoint_save_path = "./checkpoint/Inception10.ckpt"
if os.path.exists(checkpoint_save_path + '.index'):
    print('-------------load the model-----------------')
    model.load_weights(checkpoint_save_path)

cp_callback = tf.keras.callbacks.ModelCheckpoint(filepath=checkpoint_save_path,
                                                 save_weights_only=True,
                                                 save_best_only=True)
history = model.fit(x_train, y_train, batch_size=1024, epochs=5, validation_data=(x_test, y_test), validation_freq=1,
                    callbacks=[cp_callback])
# history = model.fit(x_train, y_train, batch_size=32, epochs=5, validation_data=(x_test, y_test), validation_freq=1,
#                     callbacks=[cp_callback])
model.summary()

# print(model.trainable_variables)
file = open('./weights.txt', 'w')
for v in model.trainable_variables:
    file.write(str(v.name) + '\n')
    file.write(str(v.shape) + '\n')
    file.write(str(v.numpy()) + '\n')
file.close()

###############################################    show   ###############################################

# 显示训练集和验证集的acc和loss曲线
acc = history.history['sparse_categorical_accuracy']
val_acc = history.history['val_sparse_categorical_accuracy']
loss = history.history['loss']
val_loss = history.history['val_loss']

plt.subplot(1, 2, 1)
plt.plot(acc, label='Training Accuracy')
plt.plot(val_acc, label='Validation Accuracy')
plt.title('Training and Validation Accuracy')
plt.legend()

plt.subplot(1, 2, 2)
plt.plot(loss, label='Training Loss')
plt.plot(val_loss, label='Validation Loss')
plt.title('Training and Validation Loss')
plt.legend()
plt.show()

acc和loss曲线:

image-20220515164718746

模型摘要:

image-20220515164732211

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
扎眼的阳光的头像扎眼的阳光普通用户
上一篇 2022年5月17日
下一篇 2022年5月17日

相关推荐