2021-07-02

基于TensorFlow搭建的几种经典的卷积神经网络

注:本文是本人一门课程的期末大作业,在学习曹建老师(人工智能实践:TensorFlow笔记)的课程时记录的笔记。在进行整理后写的一篇小文章,具体详解可以在B站或者MOOC上搜索相关课程。

课程网站:https://www.icourse163.org/learn/PKU-1002536002?tid=1003797005#/learn/announce

目录:

引言

一、 卷积神经网络的搭建

1.1 全连接网络

1.2 卷积神经网络

1.2.1 卷积操作

1.2.2 感受野

1.2.3 全零填充

1.2.4 批标准化

1.2.5 池化

1.2.6 舍弃

1.2.7 卷积神经网络的总结

二、 经典卷积神经网络的搭建

2.1 引入 cifar10 数据集

2.2 基础框架的搭建

2.3 LeNet 结构

2.4 AlexNet 结构

2.5 InceptionNet 结构

2.6 ResNet 结构

三、 关于这几种网络的总结

引言

近几年来随着大数据技术的蓬勃发展,人工智能、机器学习也迎来了发展的高峰。作为深度学习的代表算法之一的卷积神经网络,也随着新的学习理论的提出和数据计算设备的革新得到了极大的发展,被广泛的应用于计算机视觉、自然语言处理等领域之中。而由 Google Brain 团队于 2015 年开发的 TensorFlow 为大众提供了一个由端到端的开源机器学习平台,使得我们能够在即刻执行的环境中使用Keras 等直观的高阶 API 更加轻松地构建和训练机器学习模型。因此我们可以通过它来简单的搭建几种经典卷积神经网络,以检验其实用性与简易性。

一、 卷积神经网络的搭建

1.1 全连接网络

全连接神经网络(FC)中的每个神经元都会与前后相邻层中的每个神经元有连接关系。因此我们一般在整个网络中需要设置的参数个数为 ∑(前层 × 后层 +后层),对于一张分辨率仅为 28×28 的黑白图像,它在一个全连接网络中的参数就有将近 40 万个。
在实际应用中,不仅图像的分辨率远高于这个数值,而且大多为彩色图像,如图 1-1 所示(RGB 三通道的彩色图像)。虽然全连接网络在分类预测的领域表现优异,但如果待优化参数过多会导致模型过拟合。在实际操作中,为了解决由于待优化参数过多而带来的模型过拟合问题,一般都会先对图像进行特征提取,然后再将提取得到的特征输入到全连接网络中去。如下图 1-2 所示,就是将一张彩色图像经过多次特征提取后再喂入全连接网络的过程。
2021-07-02_第1张图片
2021-07-02_第2张图片

1.2 卷积神经网络

1.2.1 卷积操作

卷积是将图像特征提取出来的一种有效方法。一般采用一个尺寸为 n×n 的卷积核(n为奇数),然后按照指定的步长在输入特征图上滑动,并且遍历图像上的每一个像素点。每移动一个步长,卷积核就会与图像产生一个重合区域,将重合区域中的对应元素相乘、求和再加上偏置项,就可以得到输出特征中的一个像素点。如图1-3所示,采用一个尺寸为3×3×1的卷积核对一个尺寸为5×5×1的单通道图像进行采集。
2021-07-02_第3张图片
其中卷积的计算过程是:(-1)×1+0×0+2×1+5×(-1)+4×0+2×1+3×(-1)+4×0+5×1+1=1
同理,对于彩色图像的卷积操作类似。但是彩色图像的卷积核的通道数应该与输入图像的通道数保持一致,如图1-4所示。
2021-07-02_第4张图片
当我们使用多个卷积核对同一层输入的图像进行次数较多的特征提取时,输出层的通道数也就是输出特征图的深度将由卷积核的个数决定。

1.2.2 感受野

我们将卷积神经网络各输出层每个像素点在原始图像上对应的区域大小叫做感受野。当我们使用的卷积核尺寸大小不同时,每个像素点对应的感受野肯定也是不同的。因此我们经常使用多层小卷积核去代替单层大卷积核,希望可以在保证感受野不变的情况下减小计算量和计算量。
最常见的操作就是用两层3×3的卷积核来代替一层5×5的卷积核。这里可以简单的从计算量方面验证一下。假设这里有一个宽和高都是 x的特征图,如果规定卷积步长为1,那么两层3×3的卷积核就有9+9个参数。同时第一层输出特征图的像素点共有(x-3+1)^2个,每个像素点都会进行9次乘加运算。而第二层共有 (x-3+1-3+1)^2 个像素点,每个像素点同样需要进行9次乘加运算。那么总的计算量为:9×(x-3+1)^2 +9×(x-6+2)^2 =18x^2-108x+180。同理,尺寸为 5×5 的卷积核的总计算量为:25x^2-200x+400。计算可得当 22/7

1.2.3 全零填充

全零填充是保持输出图像与输入图像的尺寸一致的常用方法。一个5×5×1的灰度图像经过全零填充后,在通过3×3×1的卷积核进行步长为1的卷积计算,

1.2.4 批标准化

我们知道,神经网络对0附近的数据会更加敏感,但一般随着网络层数的增加,特征数据会出现偏离0均值的情况。标准化可以使数据符合以0为均值,1为标准差的标准正态分布。而批标准化就是对一小批数据进行标准化处理,是数据回归标准分布。通常用在卷积操作和激活操作之间。对于批标准化后的输出特征图可以用下面这个式子进行计算:
在这里插入图片描述
其中,H_i’k表示批标准化后第k个卷积核的输出特征图中的第i个像素点。H_ik表示批标准化前第k个卷积核的输出特征图中的第i个像素点。μ_batchk和σ_batchk表示批标转化之前,第k个卷积核,batch张输出特征图中所有像素点的平均值和标准差。可以表示如下:
2021-07-02_第5张图片
批标准化操作将原本偏移的特征数据重新拉回到0均值,使进入激活函数的数据分布在激活函数的线性区,提升了激活函数对输入数据的区分力。但是批标准化也有其不足之处,它最大的缺点就是使得激活函数丧失了原有的非线性特性,为了缓解这一问题,我们通常在执行这一步操作时为每一个卷积核引入了两个可训练的参数,缩放因子γ_k、偏移因子β_k,使得x_i^k=γ_k H_i^'k+β_k。在反向传播时,这两个参数会和其他待训练参数一同被训练优化,使标准正态分布后的特征数据通过它们优化特征数据分布的宽窄和偏移量,保证网络的非线性表达力。

1.2.5 池化

池化操作通常用于减少卷积神经网络中特征数据量。主要方法有最大池化和均值池化。需要提取图片的纹理时采用最大池化,而为了保留背景特征则可以采用均值池化。池化过程如图1-5所示:
2021-07-02_第6张图片

1.2.6 舍弃

为了缓解神经网络的过拟合问题,在训练过程中,常把隐藏层的部分神经元按一定的比例从神经网络中临时舍弃。当使用神经网络时再将将被舍弃的神经元恢复链接。如下图所示:
2021-07-02_第7张图片
在实际的训练过程中舍弃神经元的比例也可以当作一种超参数,但是它不参与反向传播过程。一般凭借操作者的经验和实验来确定。

1.2.7 卷积神经网络的总结

综合前面提到的6点,我们可以对其本质进行简单的解释。卷积神经网络就是借助卷积核对输入特征进行特征提取,再将提取到的特征送入全连接网络进行识别预测的方法。其中提取特征的步骤包括卷积、批标准化、激活和池化四步。从中我们也可以知道,卷积其实就是一种特征提取器。卷积神经网络的主要流程如图:
2021-07-02_第8张图片

二、 经典卷积神经网络的搭建

2.1 引入 cifar10 数据集

cifar10数据集是由60000张32×32的彩色图像组成的一种用于10分类的数据集。每一类中含6000张图像。训练集由50000张图像组成,共分5个批次,每一批次10000张图像。每一类中随机抽取1000张构成测试集。其他图像随机组合构成训练集。
相较于MNIST数据集来说cifar10数据集更加复杂,训练难度也更大,但是图像尺寸较小,利用一些经典的CNN网络结构可以达到较高的识别率。验证Tensor Flow2.0的搭建效果是非常适用的。
2021-07-02_第9张图片

2.2 基础框架的搭建

在搭建经典卷积神经网络之前,我们先用cifar10数据集来搭建一个基础框架。那么在后续搭建经典网络结构时就会方便很多。

1.	# import相关模块  
2.	import tensorflow as tf  
3.	import os  
4.	import numpy as np  
5.	from matplotlib import pyplot as plt  
6.	from tensorflow.keras.layers import Conv2D, BatchNormalization, Activation, MaxPool2D, Dropout, Flatten, Dense  
7.	from tensorflow.keras import Model  
8.	np.set_printoptions(threshold=np.inf)  
9.	# 读入cifar10数据集  
10.	cifar10 = tf.keras.datasets.cifar10  
11.	# 给训练集输入特征、训练集标签、测试集输入特征、测试集标签赋值  
12.	(x_train, y_train),(x_test, y_test) = cifar10.load_data()  
13.	  
14.	x_train, x_test = x_train / 255.0, x_test / 255.0  
15.	############
16.	可替换部分
17.	############
18.	# compile配置训练方法,选择哪种优化器,选择哪种损失函数,选择哪种评测指标  
19.	model.compile(optimizer='adam',  
20.	              loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False),  
21.	              metrics = ['sparse_categorical_accuracy'])  
22.	# 断点续训  
23.	checkpoint_save_path = "./checkpoint/Baseline.ckpt"  
24.	if os.path.exists(checkpoint_save_path + '.index'):  
25.	    print('---------------load the model---------------')  
26.	    model.load_weights(checkpoint_save_path)  
27.	cp_callback = tf.keras.callbacks.ModelCheckpoint(filepath=checkpoint_save_path,  
28.	                                                 save_weights_only=True,  
29.	                                                 save_best_only=True)  
30.	# 执行训练过程  
31.	# 包含训练集、训练集特征、每个batch的值、迭代次数、测试集、多少次训练验证一次准确率、回调函数实现断点续训  
32.	history = model.fit(x_train, y_train, batch_size=32, epochs=5, validation_data=(x_test, y_test), validation_freq=1,  
33.	                    callbacks=[cp_callback])  
34.	# 打印网络结构和参数  
35.	model.summary()  
36.	# 参数提取  
37.	file = open('./weights.txt', 'w')  
38.	for v in model.trainable_variables:  
39.	    file.write(str(v.name) + '\n')  
40.	    file.write(str(v.shape) + '\n')  
41.	    file.write(str(v.numpy()) + '\n')  
42.	file.close()  
43.	# acc/loss的可视化  
44.	acc = history.history['sparse_categorical_accuracy']  
45.	val_acc = history.history['val_sparse_categorical_accuracy']  
46.	loss = history.history['loss']  
47.	val_loss = history.history['val_loss']  
48.	plt.subplot(1,2,1)  
49.	plt.plot(acc, label='Training Accuracy')  
50.	plt.plot(val_acc, label='Validation Accuracy')  
51.	plt.title('Training and Validation Accuracy')  
52.	plt.legend()  
53.	plt.subplot(1,2,2)  
54.	plt.plot(loss, label='Training loss')  
55.	plt.plot(val_loss, label='Validation loss')  
56.	plt.title('Training and Validation loss')  
57.	plt.legend()  
58.	plt.show()  

为了训练cifar10数据集本文搭建了一个使用一层卷积、两层全连接的基础网络。按照顺序经过6个5×5的卷积核、2×2的池化核(池化步长为2),再经过128个神经元的全连接层,最后经过10个神经元的全连接层以实现10分类。模型结构如图所示:
2021-07-02_第10张图片

1.	class Baseline(Model):  
2.	    def __init__(self):   # 在init()函数中搭建神经网络要用到的每一层结构  
3.	        super(Baseline, self).__init__()  
4.	        # 6个卷积核,尺寸为5*5,使用全零填充  
5.	        self.c1 = Conv2D(filters=6, kernel_size=(5, 5), padding='same')  
6.	        # 批标准化  
7.	        self.b1 = BatchNormalization()  
8.	        # 使用relu激活函数  
9.	        self.a1 = Activation('relu')  
10.	        # 最大池化,池化核尺寸2*2,池化步长为2,使用全零填充  
11.	        self.p1 = MaxPool2D(pool_size=(2, 2), strides=2, padding='same')  
12.	        # 按照20%的比例随机休眠神经元  
13.	        self.d1 = Dropout(0.2)  
14.	        # 拉直  
15.	        self.flatten = Flatten()  
16.	        # 送入128层的全连接,用relu激活函数  
17.	        self.f1 = Dense(128, activation='relu')  
18.	        # 休眠20%神经元  
19.	        self.d2 = Dropout(0.2)  
20.	        # 送入10个神经元的全连接,用softmax使输出符合概率分布  
21.	        self.f2 = Dense(10, activation='softmax')  
22.	    def call(self, x):   # 调用搭建好的每一层网络结构,走一遍前向传播  
23.	        x = self.c1(x)  
24.	        x = self.b1(x)  
25.	        x = self.a1(x)  
26.	        x = self.p1(x)  
27.	        x = self.d1(x)  
28.	        x = self.flatten(x)  
29.	        x = self.f1(x)  
30.	        x = self.d2(x)  
31.	        y = self.f2(x)  
32.	        return y # 返回推理结果y  
33.	model = Baseline()  

2.3 LeNet 结构

LeNet卷积神经网络作为卷积神经网络的开篇之作,由LeCun于1998年提出。它的核心思想是通过共享卷积核来减少网络的参数。其网络结构如图所示:
2021-07-02_第11张图片
因为在统计卷积神经网络层数时,一般只统计卷积计算层和全连接计算层,其余的可以当作卷积计算层的附属。所以LeNet可以认为一共有5层网络。其中C1和C3是两层卷积,C5、F6和Output是连续的三层全连接。
卷积层的第一层是卷积步长为1且不使用全零填充的6个5×5的卷积核。因为LeNet提出时还没有批标准化的操作,所以在TensorFlow中搭建网络时不选用BN操作。而激活函数则选用当时最为流行的sigm-oid函数。池化层采用池化步长为2、尺寸为2×2的池化核的最大池化。这里不使用Dropout层。而第二层的结构除了卷积核变成16个以外,其他结构与第一层一致。
在Flatten拉直后连续经过三层全连接网络,神经元个数分别为120、84和10。前两层采用sigmoid激活函数,最后一层采用softmax使输出符合概率分布。模型结构如图所示:
2021-07-02_第12张图片

1.	class LeNet5(Model):  
2.	    def __init__(self):  
3.	        super(LeNet5, self).__init__()  
4.	        # 第一层(卷积)  
5.	        self.c1 = Conv2D(filters=6, kernel_size=(5, 5),  
6.	                         activation='sigmoid')  
7.	        self.p1 = MaxPool2D(pool_size=(2, 2), strides=2)  
8.	        # 第二层(卷积)  
9.	        self.c2 = Conv2D(filters=16, kernel_size=(5, 5),  
10.	                         activation='sigmoid')  
11.	        self.p2 = MaxPool2D(pool_size=(2, 2), strides=2)  
12.	        # 拉直  
13.	        self.flatten = Flatten()  
14.	        # 第三层(全连接)  
15.	        self.f1 = Dense(120, activation='sigmoid')  
16.	        # 第四层(全连接)  
17.	        self.f2 = Dense(84, activation='sigmoid')  
18.	        # 第五层(全连接)  
19.	        self.f3 = Dense(10, activation='softmax')  
20.	    def call(self, x):# 前向传播  
21.	        x = self.c1(x)  
22.	        x = self.p1(x)  
23.	        x = self.c2(x)  
24.	        x = self.p2(x)  
25.	        x = self.flatten(x)  
26.	        x = self.f1(x)  
27.	        x = self.f2(x)  
28.	        y = self.f3(x)  
29.	        return y  
30.	model = LeNet5() 

TensorFlow2.0中搭建的网络训练结果如下图所示:
2021-07-02_第13张图片
最高训练与验证集的准确率可以达到54%和53%。LeNet5最初的目的是训练手写数据集mnist的,用其训练彩色图像可以达到这样的准确率已经很高了。不过如果进一步优化网络结构中的超参数是可以将这一数值提高到60%以上的。

2.4 AlexNet 结构

作为Hinton的代表作之一,AlexNet网络诞生于2012年,作为当年的Image Net竞赛冠军,其Top5错误率为16.4%。
它的网络结构通过使用Relu函数提高了训练数据集的速度,而且因为Dro pout操作缓解了过拟合问题,所以准确率得到了较大的提高。其网络结构如图所示:
2021-07-02_第14张图片
从图中可以看出AlexNet一共有八层。本文搭建的网络中,第一层使用了步长为1,不全零填充的96个3×3的卷积核。批标准化用BN操作来代替原论文中的局部相应标准化操作LRN。用3×3的池化核在步长为2的情况下做最大池化。第二层与第一层相比,仅仅只有卷积核个数变为256个。第三层与第四层均使用了384个3×3的卷积核、步长为1,但是这里使用全零填充。没有BN、池化以及舍弃操作,激活函数仍然选用relu。第五层使用256个3×3的卷积核、步长为1,使用全零填充。没有BN和舍弃操作。选用relu函数作为这一层的激活函数。池化操作与第一层一样。
AlexNet结构中的舍弃操作主要集中在全连接层中。在拉直后,使用2048个神经元、relu激活函数和50%的休眠率构建第六以及第七层。最后一层由10个神经元和softmax构成输出层。搭建好的模型结构如图所示:
2021-07-02_第15张图片

1.	class AlexNet8(Model):  
2.	    def __init__(self):  
3.	        super(AlexNet8, self).__init__()  
4.	        # 第一层(卷积)  
5.	        self.c1 = Conv2D(filters=96, kernel_size=(3, 3))  
6.	        self.b1 = BatchNormalization()  
7.	        self.a1 = Activation('relu')  
8.	        self.p1 = MaxPool2D(pool_size=(3, 3), strides=2)  
9.	        # 第二层(卷积)  
10.	        self.c2 = Conv2D(filters=256, kernel_size=(3, 3))  
11.	        self.b2 = BatchNormalization()  
12.	        self.a2 = Activation('relu')  
13.	        self.p2 = MaxPool2D(pool_size=(3, 3), strides=2)  
14.	        # 第三层(卷积)  
15.	        self.c3 = Conv2D(filters=384, kernel_size=(3, 3), padding='same',  
16.	                         activation='relu')  
17.	        # 第四层(卷积)  
18.	        self.c4 = Conv2D(filters=384, kernel_size=(3, 3), padding='same',  
19.	                         activation='relu')  
20.	        # 第五层(卷积)  
21.	        self.c5 = Conv2D(filters=256, kernel_size=(3, 3), padding='same',  
22.	                         activation='relu')  
23.	        self.p3 = MaxPool2D(pool_size=(3, 3), strides=2)  
24.	        # 拉直  
25.	        self.flatten = Flatten()  
26.	        # 第六层(全连接)  
27.	        self.f1 = Dense(2048, activation='relu')  
28.	        self.d1 = Dropout(0.5)  
29.	        # 第七层(全连接)  
30.	        self.f2 = Dense(2048, activation='relu')  
31.	        self.d2 = Dropout(0.5)  
32.	        # 第八层(全连接)  
33.	        self.f3 = Dense(10, activation='softmax')  
34.	    def call(self, x): # 前向传播  
35.	        x = self.c1(x)  
36.	        x = self.b1(x)  
37.	        x = self.a1(x)  
38.	        x = self.p1(x)  
39.	  
40.	        x = self.c2(x)  
41.	        x = self.b2(x)  
42.	        x = self.a2(x)  
43.	        x = self.p2(x)  
44.	  
45.	        x = self.c3(x)  
46.	  
47.	        x = self.c4(x)  
48.	  
49.	        x = self.c5(x)  
50.	        x = self.p3(x)  
51.	  
52.	        x = self.flatten(x)  
53.	        x = self.f1(x)  
54.	        x = self.d1(x)  
55.	        x = self.f2(x)  
56.	        x = self.d2(x)  
57.	        y = self.f3(x)  
58.	        return y  
59.	model = AlexNet8()  

TensorFlow2.0中搭建的网络训练结果如下图所示:
2021-07-02_第16张图片
从图中可以看到在AlexNet结构下搭建的网络相较于LeNet的准确率有了一定的提升,达到63%和65%的准确率。

2.5 InceptionNet 结构

InceptionNet诞生于2014年的Image Net竞赛,作为当年的冠军,其Top5错误率为6.67%。它相较于其他结构引入了Inception结构块,他的核心思想就是在同一层网络内使用不同尺寸的卷积核,以此来提升模型的感知力,然后通过批标准化操作来缓解梯度消失。无论是GoogleNet(Inception v1)还是后续版本(v2、v3、v4)都是基于Inception结构块搭建的网络。
Inception结构块会在同一层网络中搭建多个尺寸的卷积核,所以可以提取不同尺寸的特征。而且通过1×1卷积核作用到输入特征图的每一个像素点,然后通过设定少于输入特征图深度的卷积核的个数来减少输出的特征图深度。从而降低了图片的维度,减少了参数量和计算量。结构块的具体形式如图所示:
2021-07-02_第17张图片
一个完整的Inception结构块由4个分支构成。分别经过一个1×1的卷积核输出到卷积连接器;经过1×1的卷积核配合3×3或者5×5的卷积核输出的卷积连接器;经过3×3最大池化核配合1×1卷积核输出到卷积连接器。在结构块中要求送到卷积连接器的四路特征数据尺寸应该相同,然后将这些数据按深度方向拼接,即可得到结构块的输出。这里的每一个部分都可以同前面的模型中的每一层网络一样,通过图1-7的形式表示出来,这里不再赘述。
了解了Inception结构块的构成后,本文搭建了一个精简版本的InceptionNet网络(原网络有22层,训练难度过高)。网络一共有10层。第一层采用步长为1且全零填充的16个3×3的卷积核。选择BN操作和relu函数激活。然后是4个Inc eption结构块相连,每两个结构块组成一个block。而每个block中的第一个结构块的卷积步长为2,第二个结构块中的卷积步长为1。这使得第一个输出特征图的尺寸减半,因此我们需要将输出特征图的深度加深以保证特征抽取中的信息承载量尽可能一致。最后block中的结果会被送入平均池化以及10个分类的全连接。即如下图所示:
2021-07-02_第18张图片

1.	class ConvBNRelu(Model):# 定义一个类,后面直接调用  
2.	    def __init__(self, ch, kernelsz=3, strides=1, padding='same'):  
3.	        # 默认卷积核边长是3,步长为1,全零填充  
4.	        super(ConvBNRelu, self).__init__()  
5.	        self.model = tf.keras.models.Sequential([  
6.	            Conv2D(ch, kernelsz, strides=strides, padding=padding),  
7.	            BatchNormalization(),  
8.	            Activation('relu')  
9.	        ])  
10.	    def call(self, x):  
11.	        x = self.model(x)  
12.	        return x  
13.	  
14.	class InceptionBIK(Model): # 定义一个类来实现一个结构块
15.	    def __init__(self, ch, strides=1):  
16.	        super(InceptionBIK, self).__init__()  
17.	        self.ch = ch  
18.	        self.strides = strides  
19.	        # Inception结构块的四个分支  
20.	        # 第一个分支  
21.	        self.c1 = ConvBNRelu(ch, kernelsz=1, strides=strides)  
22.	        # 第二个分支  
23.	        self.c2_1 = ConvBNRelu(ch, kernelsz=1, strides=strides)  
24.	        self.c2_2 = ConvBNRelu(ch, kernelsz=3, strides=1)  
25.	        # 第三个分支  
26.	        self.c3_1 = ConvBNRelu(ch,kernelsz=1, strides=strides)  
27.	        self.c3_2 = ConvBNRelu(ch, kernelsz=5, strides=1)  
28.	        # 第四个分支  
29.	        self.p4_1 = MaxPool2D(3, strides=1, padding='same')  
30.	        self.c4_2 = ConvBNRelu(ch, kernelsz=1, strides=strides)  
31.	  
32.	    def call(self, x):  
33.	        x1 = self.c1(x)  
34.	        x2_1 = self.c2_1(x)  
35.	        x2_2 = self.c2_2(x2_1)  
36.	        x3_1 = self.c3_1(x)  
37.	        x3_2 = self.c3_2(x3_1)  
38.	        x4_1 = self.p4_1(x)  
39.	        x4_2 = self.c4_2(x4_1)  
40.	        # 将四个分支的输出堆叠,方向沿深度方向  
41.	        x = tf.concat([x1, x2_2, x3_2, x4_2], axis=3)  
42.	        return x  
43.	class Inception10(Model): # 搭建的一个精简版本的Inception网络
44.	    def __init__(self, num_blocks, num_classes, init_ch=16, **kwargs):  
45.	        # 默认输出深度是16  
46.	        super(Inception10,self).__init__(**kwargs)  
47.	        self.in_channels = init_ch  
48.	        self.out_channels = init_ch  
49.	        self.num_blocks = num_blocks  
50.	        self.init_ch = init_ch  
51.	        self.c1 = ConvBNRelu(init_ch)  
52.	        self.blocks = tf.keras.models.Sequential()  
53.	        for block_id in range(num_blocks):  
54.	            for layer_id in range(2):  
55.	                if layer_id == 0:  
56.	                    # 第一个block结构块,卷积步长2(使得输出特征图尺寸减半)  
57.	                    block = InceptionBIK(self.out_channels, strides=2)  
58.	                else:  
59.	                    # 第二个block结构块,卷积步长1  
60.	                    block = InceptionBIK(self.out_channels, strides=1)  
61.	                self.blocks.add(block)  
62.	            # 输出特征图深度加深  
63.	            self.out_channels *= 2  
64.	        # 平均池化  
65.	        self.p1 = GlobalAveragePooling2D()  
66.	        self.f1 = Dense(num_classes, activation='softmax')  
67.	  
68.	    def call(self, x):  
69.	        x = self.c1(x)  
70.	        x = self.blocks(x)  
71.	        x = self.p1(x)  
72.	        y =self.f1(x)  
73.	        return y  
74.	# 实例化类,指定block数为2且网络是10分类  
75.	model = Inception10(num_blocks=2, num_classes=10)  

TensorFlow2.0中搭建的网络训练结果如下图所示:
2021-07-02_第19张图片
因为现在的网络规模较之前有较大提升,所以我们将每次喂入神经网络的数据调整到1024。以此充分发挥显卡的性能,提高训练的速度。这时的训练准确率已经达到70%左右。

2.6 ResNet 结构

2015年由何恺明及其团队提出,是当年的ImageNet竞赛冠军,其Top5错误率为3.57%。ResNet网络结构最大的改变就是提出了层间残差跳连,引入前方信息、缓解梯度消失,使神经网络的层数增加成为可能。
从前面几种经典网络结构中我们可以发现,随着时间的推移,卷积神经网络的层数越来越多,其训练准确率也同步提高。那是不是说神经网络的层数越多越好呢?显然不是的。
ResNet的作者曾经在cifar10数据集上做过一个实验。他发现56层卷积网络的错误率要高与20层卷积网络的错误率。如图所示:
2021-07-02_第20张图片
因为单纯的堆叠神经网络的层数会使神经网络模型退化,导致后面的特征丢失了前面特征的原本模样。所以这里引入了ResNet块来解决这一问题。其结构如图所示:
2021-07-02_第21张图片
它由一根跳连线将前边的特征直接连到后边,使输出结果H(x)包括堆叠卷积的非线性输出F(x)和跳过这两层堆叠卷积的恒等映射x。这样的操作有效的缓解了神经网络模型堆叠导致的退化,使得神经网络可以向着更深的层级发展。还要注意,这里的+与前面Inception块中的+有所不同。前面是沿深度方向的叠加,而这里是和矩阵加和类似的对应元素的+和。
实际上在ResNet块中有两种不同的情况,如下图所示:
2021-07-02_第22张图片
其中,实线表示两层堆叠卷积没有改变特征图的维度,其高、宽和深度都相同,可以直接加和(即H(x)=F(x)+x);虚线表示两层堆叠卷积改变了特征图的维度,需要1×1的卷积来调整x的维度,其中W就是1×1卷积操作,其加和为(H(x)=F(x) +W(x))。
我们使用ResNet块就可以在Tensor Flow中搭建Res Net18的网络结构,其第一层是个卷积,紧接着会有8个ResNet块,然后通过平均池化后进入一个全连接层。这个网络总共有18层,因为每一个ResNet块包含两层卷积。第一个ResNet块中两个跳连都是实线,而第二、三和第四个中则是先虚线后实线的跳连方式[9]。关于模型中的卷积核、池化核的尺寸在附录六的代码中有详细注释。其具体结构可以用下图表示:
2021-07-02_第23张图片

1.	# 每调用一次ResnetBlock类会生成一个ResNet块  
2.	class ResnetBlock(Model):  
3.	    def __init__(self, filters, strides=1, residual_path=False):  
4.	        super(ResnetBlock, self).__init__()  
5.	        self.filters = filters  
6.	        self.strides = strides  
7.	        self.residual_path = residual_path  
8.	  
9.	        self.c1 = Conv2D(filters, (3, 3), strides=strides, padding='same', use_bias=False)  
10.	        self.b1 = BatchNormalization()  
11.	        self.a1 = Activation('relu')  
12.	  
13.	        self.c2 = Conv2D(filters, (3, 3), strides=1, padding='same', use_bias=False)  
14.	        self.b2 = BatchNormalization()  
15.	        # residual_path为True时,对输入进行下采样,即用1*1的卷积核做卷积,保证x与F(x)维度相同  
16.	        if residual_path:  
17.	            self.down_c1 = Conv2D(filters, (1, 1), strides=strides, padding='same', use_bias=False)  
18.	            self.down_b1 = BatchNormalization()  
19.	  
20.	        self.a2 = Activation('relu')  
21.	  
22.	    def call(self, inputs):  
23.	        # residual等于输入,即residual=x  
24.	        residual = inputs   
25.	        # 将输入通过卷积、BN层、激活层,计算F(x)  
26.	        x = self.c1(inputs)  
27.	        x = self.b1(x)  
28.	        x = self.a1(x)  
29.	        x = self.c2(x)  
30.	        y = self.b2(x)  
31.	        # 当堆叠卷积层前后维度相同,不执行IF  
32.	        if self.residual_path:  
33.	            residual = self.down_c1(inputs)  
34.	            residual = self.down_b1(residual)  
35.	        # 最后输出的是两部分的和,即F(x)+x或F(x)+W(x)  
36.	        # 再经过激活函数  
37.	        out = self.a2(y + residual)  
38.	        return out  
39.	  
40.	class ResNet18(Model):  
41.	    # block_list表示每个block有几个卷积层  
42.	    def __init__(self, block_list, initial_filters=64):  
43.	        super(ResNet18, self).__init__()  
44.	        self.num_blocks = len(block_list) # block的总数  
45.	        self.block_list = block_list  
46.	        self.out_filters = initial_filters  
47.	        self.c1 = Conv2D(self.out_filters, (3, 3), strides=1, padding='same', use_bias=False)  
48.	        self.b1 = BatchNormalization()  
49.	        self.a1 = Activation('relu')  
50.	        self.blocks = tf.keras.models.Sequential()  
51.	        # 构建ResNet网络结构(*4)  
52.	        # 当前是第几个resnet block  
53.	        for block_id in range(len(block_list)):  
54.	            # 当前是哪一个卷积层  
55.	            for layer_id in range(block_list[block_id]):   
56.	                # 对除第一个block以外的每个block的输入进行下采样  
57.	                if block_id != 0 and layer_id == 0:  
58.	                    block = ResnetBlock(self.out_filters, strides=2, residual_path=True)  
59.	                else:  
60.	                    block = ResnetBlock(self.out_filters, residual_path=False)  
61.	                # 将构建好的block加入resnet  
62.	                self.blocks.add(block)  
63.	            # 下一个block的卷积核是上一个的两倍  
64.	            self.out_filters *= 2  
65.	        self.p1 = tf.keras.layers.GlobalAveragePooling2D()  
66.	        self.f1 = tf.keras.layers.Dense(10, activation='softmax', kernel_regularizer=tf.keras.regularizers.l2())  
67.	  
68.	    def call(self, inputs):  
69.	        x = self.c1(inputs)  
70.	        x = self.b1(x)  
71.	        x = self.a1(x)  
72.	        x = self.blocks(x)  
73.	        x = self.p1(x)  
74.	        y = self.f1(x)  
75.	        return y  
76.	  
77.	model = ResNet18([2, 2, 2, 2])  

TensorFlow2.0中搭建的网络训练结果如下图所示:
2021-07-02_第24张图片
可以看出相较于前面三种网络结构,ResNet网络对于cifar10数据集的的训练效果是最好的,最大值接近90%。

三、 关于这几种网络的总结

从2.3到2.6中可以看到,随着网络复杂程度的提高,以及Relu、Dropout、BN等操作的使用,cifar10数据集的准确率基本上是逐步提高的。四个网络中,InceptionNet的训练效果是最不理想的。可能是因为其本身的设计理念是采用不同尺寸的卷积核,提供不同的感受野。但是cifar10只是一个单一的分类任务,所以二者的契合度并不高。还要指出的是,本文所搭建的网络极其超参数的设定都是可以改变的。而大多数时候这些变量的改变会使整个网络的效果有显著的提高,所以如何选择合适的模型极其训练的方式也是一个值得探究的问题。
最后,本文虽然利用TensorFlow中的Keras库搭建了卷积神经网络中较为经典的四种网络结构。通过已经编写好的程序包,简化了网络的搭建过程,切实感受到了TensorFlow对于神经网络构建的便利性。但是最终的训练结果看起来没有特别突出的地方。但这不影响本文中提及的搭建网络的思路的正确性与泛化性,可以作为我们以后搭建其他的网络的一个很好的示例。

这是本人第一篇博文,其中许多公式并未专门编辑,阅读体验可能较差,后面有机会会改。如果文章中有什么不妥的地方欢迎大家指正(如有指正,非常感谢)。最后希望这篇文章可以给需要的朋友一点点帮助。

你可能感兴趣的:(tensorflow,神经网络)