本文涉及到的是中国大学慕课《人工智能实践:Tensorflow笔记》第五讲第14节的内容,对tensorflow环境下经典卷积神经网络的搭建进行介绍,其基础是DL with python(14)——tensorflow实现CNN的“八股”中的代码,将其中第三步的代码替换为本文中的代码均可直接运行,其他部分无需改变。
经典的卷积神经网络有以下几种,这里介绍结构较为复杂的InceptionNet,其实现的方法也相对困难。
InceptionNet 即 GoogLeNet,诞生于2014年,是当年ImageNet竞赛冠军,Top5错误率为6.67%。
Szegedy C, Liu W, Jia Y, et al. Going Deeper with Convolutions. In CVPR, 2015.
主要优点:一层内使用不同尺寸的卷积核,提升感知力(通过 padding 实现输出特征面积一致);使用 1 * 1 卷积核,改变输出特征 channel 数(减少网络参数)。
InceptionNet旨在通过增加网络的宽度来提升网络的能力,与 VGGNet 通过卷积层堆叠的方式(纵向)相比,是一个不同的方向(横向)。InceptionNet 模型的构建与 VGGNet 及之前的网络有所区别,不再是简单的纵向堆叠,要理解 InceptionNet 的结构,首先要理解它的基本单元,如下图所示。
一个基本单元内部的结构如下。可以看到,InceptionNet 的基本单元中,卷积部分是比较统一的 C、B、A 典型结构,即卷积→BN→激活,激活均采用 Relu 激活函数,同时包含最大池化操作。
明白了基本结构,就可以进行网络的搭建了。InceptionNet的搭建主要分为三步:
1,构建一个ConvBNRelu类,包含卷积、批归一化、激活三个层,即CBA组合;
2,利用ConvBNRelu类构建Inception结构块,即网络的基本单元;
3,利用Inception结构块和其他网络层构建Inception Net;
在 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): # 调用class类生成一个结构体
x = self.model(x, training=False) #在training=False时,BN通过整个训练集计算均值、方差去做批归一化,training=True时,通过当前batch的均值、方差去做批归一化。推理时 training=False效果好
return x
其中参数 ch 代表特征图的通道数,也即卷积核个数;kernelsz 代表卷积核尺寸;strides 代表卷积步长;padding 代表是否进行全零填充。
开始构建 InceptionNet 的基本单元,同样利用 class 定义的
方式,定义一个新的 InceptionBlk 类,利用这个类生成一个Inception结构块,即基本单元。
## 用ConvBNRelu类实现Inception结构块
class InceptionBlk(Model):
def __init__(self, ch, strides=1): # 默认步长为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') #第四个分支,最大池化+CBA
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)
# 将四个分支的输出堆叠在一起,axis=3表示按照深度堆叠
x = tf.concat([x1, x2_2, x3_2, x4_2], axis=3)
return x
最后一步,利用前面的两步作为基础,搭配其他网络层,搭建10层的InceptionNet。具体内容见代码和注释:
## 利用Inception结构块搭建网络
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 # block的数量,每个含有2个结构块
self.init_ch = init_ch # 为了下一行调用init_ch
self.c1 = ConvBNRelu(init_ch) # 第一个独立的卷积层
self.blocks = tf.keras.models.Sequential() # 构建网络中的block
for block_id in range(num_blocks): # 构建num_blocks个block
for layer_id in range(2): # 一个block中含有2个Inception结构块
if layer_id == 0: # 对于第一个Inception结构块
block = InceptionBlk(self.out_channels, strides=2) # 通道数为前面定义的16,卷积步长为2,输出特征尺寸减半
else:
block = InceptionBlk(self.out_channels, strides=1) # 卷积步长为1
self.blocks.add(block)
# enlarger out_channels per block
self.out_channels *= 2 #输出特征图的深度乘以2,第二个block的通道数为32
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
# 实例化网络,制定了block数是2,是一个10分类网络
model = Inception10(num_blocks=2, num_classes=10) # 两个参数传递给Inception10的相应参数
将以上三段代码依次连接起来,就是10层InceptionNet的搭建代码。因为网络较为复杂,且处理的数据过多,可以将小批量改为1024再运行程序,但是一般的电脑也会运行一阵子,CPU的利用率也会较高。