InceptionNet 即 GoogLeNet,诞生于 2015 年,旨在通过增加网络的宽度来提升网络的能 力,与 VGGNet 通过卷积层堆叠的方式(纵向)相比,是一个不同的方向(横向)。 显然,InceptionNet 模型的构建与 VGGNet 及之前的网络会有所区别,不再是简单的纵 向堆叠,要理解 InceptionNet 的结构,首先要理解它的基本单元,如图下 所示。
借鉴点:一层内使用不同尺寸的卷积核,提升感知力(通过 padding 实现输出特征面积一致); 使用 1 * 1 卷积核,改变输出特征 channel 数(减少网络参数)。
可以看到,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 类,如下所示。
class InceptionBlk(Model):
def __init__(self, ch, strides=1):
super(InceptionBlk, self).__init__()
self.ch = ch
self.strides = strides
#第1分支
self.c1 = ConvBNRelu(ch, kernelsz=1, strides=strides)
#第2分支
self.c2_1 = ConvBNRelu(ch, kernelsz=1, strides=strides)
self.c2_2 = ConvBNRelu(ch, kernelsz=3, strides=1)
#第3分支
self.c3_1 = ConvBNRelu(ch, kernelsz=1, strides=strides)
self.c3_2 = ConvBNRelu(ch, kernelsz=5, strides=1)
#第4分支
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
x = tf.concat([x1, x2_2, x3_2, x4_2], axis=3)
return x
参数 ch 仍代表通道数,strides 代表卷积步长,与 ConvBNRelu 类中一致;tf.concat 函数将四 个输出连接在一起,x1、x2_2、x3_2、x4_2 分别代表图 5-27 中的四列输出,结合结构图和 代码很容易看出二者的对应关系。
可以看到,InceptionNet 的一个显著特点是大量使用了 1 * 1 的卷积核,事实上,最原始 的 InceptionNet 的结构是不包含 1 * 1 卷积的,如图下所示。 图上 InceptionNet 最原始的基本单元 由图 可以更清楚地看出 InceptionNet 最初的设计思想,即通过不同尺寸卷积层和 池化层的横向组合(卷积、池化后的尺寸相同,通道可以相加)来拓宽网络深度,可以增加 网络对尺寸的适应性。但是这样也带来一个问题,所有的卷积核都会在上一层的输出上直接 做卷积运算,会导致参数量和计算量过大(尤其是对于 5 * 5 的卷积核来说)。因此, InceptionNet 在 3 * 3、5 * 5 的卷积运算前、最大池化后均加入了 1 * 1 的卷积层,形成了图 1中的结构,这样可以降低特征的厚度,一定程度上避免参数量过大的问题。
那么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 网络的主体就是由其基本单元构成的,其模型结构如图下所示。
class Inception10(Model):
def __init__(self, num_blocks, num_classes, init_ch=16, **kwargs):#输出深度16
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
代码实现:
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
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
class InceptionBlk(Model):
def __init__(self, ch, strides=1):
super(InceptionBlk, self).__init__()
self.ch = ch
self.strides = strides
#第1分支
self.c1 = ConvBNRelu(ch, kernelsz=1, strides=strides)
#第2分支
self.c2_1 = ConvBNRelu(ch, kernelsz=1, strides=strides)
self.c2_2 = ConvBNRelu(ch, kernelsz=3, strides=1)
#第3分支
self.c3_1 = ConvBNRelu(ch, kernelsz=1, strides=strides)
self.c3_2 = ConvBNRelu(ch, kernelsz=5, strides=1)
#第4分支
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
x = tf.concat([x1, x2_2, x3_2, x4_2], axis=3)
return x
class Inception10(Model):
def __init__(self, num_blocks, num_classes, init_ch=16, **kwargs):#输出深度16
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
model = Inception10(num_blocks=2, num_classes=10)
optimizer = tf.keras.optimizers.Adam()
model.compile(optimizer=optimizer ,
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False),
metrics=['sparse_categorical_accuracy'])
history = model.fit(x_train, y_train, batch_size=32, epochs=5, validation_data=(x_test, y_test), validation_freq=1)
model.summary()