本节主要内容:讲解卷积神经网络,利用基础CNN、LeNet、AlexNet、VGGNet、InceptionNet和ResNet实现图像识别。
1、全连接网络回顾
全连接NN特点:每个神经元与前后相邻层的每一个神经元都有连接关系。(可以实现分类和预测) 全连接网络的参数个数:Σ(前层 ? 后层 + 后层)
如图5-1所示,针对一张分辨率仅为28 * 28的黑白图像(像素值个数为28 * 28 * 1 = 784),全连接网络的参数总量就有将近40万个。
在实际应用中,图像的分辨率远高于此,且大多数是彩色图像,如图5-2所示。虽然全连接网络一般被认为是分类预测的最佳网络,但待优化的参数过多,容易导致模型过拟合。 为了解决参数量过大而导致模型过拟合的问题,一般不会将原始图像直接输入,而是先对图像进行特征提取,再将提取得到的特征输入全连接网络,如图5-3所示,就是将汽车图片经过多次特征提取后在喂入全连接网络。
2、卷积神经网络
2.1 卷积的概念
卷积(Convolutional)可以认为是一种有效的提取图像特征的方法。一般会用一个正方形的卷积核,按指定步长,在输入特征图上滑动,遍历输入特征图中的每个像素点。每一个步长,卷积核会与输入特征图出现重合区域,重合区域对应元素相乘、求和再加上偏置项得到输出特征的一个像素点,如图5-4所示,利用大小为3×3×1的卷积核对5×5×1的单通道图像做卷积计算得到相应结果。
2.2 多通道和卷积核
对于彩色图像即多通道(channel),卷积核通道数(深度)和输入特征一致,套接后在对应位置上进行乘加和操作。如图5-5所示,利用三通道卷积核对三通道的彩色特征图做卷积计算
- 用多个卷积核可实现对同一输入层输入特征的多次特征提取,卷积核的个数决定输出层的通道数,即输出特征图(feature map)的深度
2.3感受野 (Receptive Field)
感受野 (Receptive Field)的概念:卷积神经网络各输出层每个像素点自原始图像上的映射区域大小,如图5-7所示: 当我们采用尺寸不同的卷积核时,最大的区别就是感受野的大小不同,所以经常会采用多层小卷积核来替换一层大卷积核,在保持感受野相同的情况下减少参数量和计算量,例如十分常见的用2层3 * 3卷积核来替换1层5 * 5卷积核的方法,如图5-7所示。
这里给出详细推导:不妨设输入特征图的宽、高均为x,卷积计算的步长为1,显然,两个3 * 3卷积核的参数量为9 + 9 = 18,小于5 * 5卷积核的25,前者的参数量更少。 在计算量上,根据图5-8所示的输出特征尺寸计算公式,对于5 * 5卷积核来说,输出特征图共有(x – 5 + 1)^2个像素点,每个像素点需要进行5 * 5 = 25次乘加运算,则总计算量为25 * (x – 5 + 1)^2 = 25x^2 – 200x + 400; 对于两个3 * 3卷积核来说,第一个3 * 3卷积核输出特征图共有(x – 3 + 1)^2个像素点,每个像素点需要进行3 * 3 = 9次乘加运算,,第二个3 * 3卷积核输出特征图共有(x – 3 + 1 – 3 + 1)^2个像素点,每个像素点同样需要进行9次乘加运算,则总计算量为9 * (x – 3 + 1)^2 + 9 * (x – 3 + 1 – 3 + 1)^2 = 18 x^2 – 108x + 180; 对二者的总计算量(乘加运算的次数)进行对比,18 x^2 – 200x + 400 < 25x^2 – 200x + 400,经过简单数学运算可得x < 22/7 or x > 10,x作为特征图的边长,在大多数情况下显然会是一个大于10的值(非常简单的MNIST数据集的尺寸也达到了28 * 28),所以两层3 * 3卷积核的参数量和计算量,在通常情况下都优于一层5 * 5卷积核,尤其是当特征图尺寸比较大的情况下,两层3 * 3卷积核在计算量上的优势会更加明显。
2.4 输出特征图(feature map)
输出特征尺寸计算:在了解神经网络中卷积计算的整个过程后,就可以对输出特征图的尺寸进行计算,如图5-8所示,5×5的图像经过3×3大小的卷积核做卷积计算后输出特征尺寸为3×3。
2.5 全零填充(padding)
全零填充(padding):为了保持输出图像尺寸与输入图像一致,经常会在输入图像周围进行全零填充,如图5-9所示,在5×5的输入图像周围填0,则输出特征尺寸同为5×5。 在Tensorflow框架中,用参数padding = ‘SAME’或padding = ‘VALID’表示是否进行全零填充,其对输出特征尺寸大小的影响如下: 上下两行分别代表对输入图像进行全零填充或不进行填充,对于5×5×1的图像来说,当padding = ‘SAME’时,输出图像边长为5;当padding = ‘VALID’时,输出图像边长为3。 具备以上知识后,就可以在Tensorflow框架下利用Keras来构建CNN中的卷积层,使用的是tf.keras.layers.Conv2D函数,具体的使用方法如下:
tf.keras.layers.Conv2D(
input_shape = (高, 宽, 通道数),
filters = 卷积核个数,
kernel_size = 卷积核尺寸,
strides = 卷积步长,
padding = ‘SAME’ or ‘VALID’,
activation = ‘relu’ or ‘sigmoid’ or ‘tanh’ or ‘softmax’等
)
使用此函数构建卷积层时,需要给出的信息有: A)输入图像的信息,即宽高和通道数; B)卷积核的个数以及尺寸,如filters = 16, kernel_size = (3, 3)代表采用16个大小为3×3的卷积核; C)卷积步长,即卷积核在输入图像上滑动的步长,纵向步长与横向步长通常是相同的,默认值为1; D)是否进行全零填充,全零填充的具体作用上文有描述; E)采用哪种激活函数,例如relu、softmax等,各种函数的具体效果在前面章节中有详细描述
这里需要注意的是,在利用Tensorflow框架构建卷积网络时,一般会利用BatchNormalization函数来构建BN层,进行批归一化操作,所以在Conv2D函数中经常不写BN。BN操作的具体含义和作用见下文。
2.6 Batch Normalization(批标准化)
Batch Normalization(批标准化):对一小批数据在网络各层的输出做标准化处理,其具体实现方式如图5-10所示。( 标准化:使数据符合0均值,1为标准差的分布;批标准化:对一小批数据(batch),做标准化处理。) Batch Normalization将神经网络每层的输入都调整到均值为0,方差为1的标准正态分布,其目的是解决神经网络中梯度消失的问题,如图5-11所示。
BN操作的另一个重要步骤是缩放和偏移,值得注意的是,缩放因子γ以及偏移因子β都是可训练参数,其作用如图5-12所示。 BN操作通常位于卷积层之后,激活层之前,在Tensorflow框架中,通常使用Keras中 的 tf.keras.layers.BatchNormalization函数来构建BN层。 在调用此函数时,需要注意的一个参数是training,此参数只在调用时指定,在模型进行前向推理时产生作用,当training = True时,BN操作采用当前batch的均值和标准差;当training = False时,BN操作采用滑动平均(running)的均值和标准差。在Tensorflow中,通常会指定training = False,可以更好地反映模型在测试集上的真实效果。 滑动平均(running)的解释:滑动平均,即通过一个个batch历史的叠加,最终趋向数据集整体分布的过程,在测试集上进行推理时,滑动平均的参数也就是最终保存的参数。此外,Tensorflow中的BN函数其实还有很多参数,其中比较常用的是momentum,即动量参数,与sgd优化器中的动量参数含义类似但略有区别,具体作用为滑动平均running = momentum * running + (1 – momentum) * batch,一般设置一个比较大的值,在Tensorflow框架中默认为0.99。
2.7 池化(pooling)
池化(pooling):池化的作用是减少特征数量(降维)。最大值池化可提取图片纹理,均值池化可保留背景特征,如图5-13所示 在Tensorflow框架下,可以利用Keras来构建池化层,使用的是tf.keras.layers.MaxPool2D函数和tf.keras.layers.AveragePooling2D函数,具体的使用方法如下:
tf.keras.layers.MaxPool2D(
pool_size=池化核尺寸,
strides=池化步长,
padding=‘valid’or‘same’
)
tf.keras.layers.AveragePooling2D(
pool_size=池化核尺寸,
strides=池化步长,
padding=‘valid’or‘same’
)
model = tf.keras.models.Sequential([
Conv2D(filters=6, kernel_size=(5, 5), padding='same'),
BatchNormalization(),
MaxPool2D(pool_size=(2, 2), strides=2, padding='same'),
Dropout(0.2),
])
2.8 舍弃(Dropout)
舍弃(Dropout):在神经网络的训练过程中,将一部分神经元按照一定概率从神经网络中暂时舍弃,使用时被舍弃的神经元恢复链接,如图5-14所示。 在Tensorflow框架下,利用tf.keras.layers.Dropout函数构建Dropout层,参数为舍弃的概率(大于0小于1)。 利用上述知识,就可以构建出基本的卷积神经网络(CNN)了,其核心思路为在CNN中利用卷积核(kernel)提取特征后,送入全连接网络。
2.9 CNN模型的主要模块
CNN模型的主要模块:一般包括上述的卷积层、BN层、激活函数、池化层以及全连接层,如图5-15所示 在此基础上,可以总结出在Tensorflow框架下,利用Keras来搭建神经网络的“八股”套路,在主干的基础上,还可以添加其他内容,来完善神经网络的功能,如利用自己的图片和标签文件来自制数据集;通过旋转、缩放、平移等操作对数据集进行数据增强;保存模型文件进行断点续训;提取训练后得到的模型参数以及准确率曲线,实现可视化等。 构建神经网络的“八股”套路: A)import引入tensorflow及keras、numpy等所需模块。 B)读取数据集,课程中所利用的MNIST、cifar10等数据集比较基础,可以直接从sklearn等模块中引入,但是在实际应用中,大多需要从图片和标签文件中读取所需的数据集。 C)搭建所需的网络结构,当网络结构比较简单时,可以利用keras模块中的tf.keras.Sequential来搭建顺序网络模型;但是当网络不再是简单的顺序结构,而是有其它特殊结构出现时(例如ResNet中的跳连结构),便需要利用class来定义自己的网络结构。前者使用起来更加方便,但实际应用中往往需要利用后者来搭建网络。 D)对搭建好的网络进行编译(compile),通常在这一步指定所采用的优化器(如Adam、sgd、RMSdrop等)以及损失函数(如交叉熵函数、均方差函数等),选择哪种优化器和损失函数往往对训练的速度和效果有很大的影响,至于具体如何进行选择,前面的章节中有比较详细的介绍。 E)将数据输入编译好的网络来进行训练(model.fit),在这一步中指定训练轮数epochs以及batch_size等信息,由于神经网络的参数量和计算量一般都比较大,训练所需的时间也会比较长,尤其是在硬件条件受限的情况下,所以在这一步中通常会加入断点续训以及模型参数保存等功能,使训练更加方便,同时防止程序意外停止导致数据丢失的情况发生。 F)将神经网络模型的具体信息打印出来(model.summary),包括网络结构、网络各层的参数等,便于对网络进行浏览和检查。
2.10 cifar10 数据集介绍
cifar10 数据集介绍: 该数据集共有 60000 张彩色图像,每张尺寸为 32 * 32,分为 10 类,每类 6000 张。 训练集 50000 张,分为 5 个训练批,每批 10000 张; 从每一类随机取 1000 张构成测试集,共 10000 张,剩下的随机排列组成训练集,如图 5-16 所示 cifar10 数据集的读取: √ 数据集下载: cifar10 = tf.keras.datasets.cifar10 √ 导入训练集和测试集: (x_train, y_train), (x_test, y_test) = cifar10.load_data() √ 打印训练集与测试集的数据维度, 打印结果为:
显然, cifar10 是一个用于图像分类的数据集,共分 10 类,相较于 mnist 数据集会更复 杂一些,训练难度也更大,但是图像尺寸较小,仅为 32 * 32,仍然属于比较基础的数据集,利用一些 CNN 经典网络结构(如 VGGNet、 ResNet 等,下一小节会具体介绍)进行训练的话准确率很容易就能超过 90%,很适合初学者用来练习。目前学术界对于 cifar10 数据集的分类准确率已经达到了相当高的水准,图 5-17 中为 Github 网站上 cifar10 数据集分类准确率的排行榜。
参考网址: http://rodrigob.github.io/are_we_there_yet/build/classification_datasets_results.html
import tensorflow as tf
from matplotlib import pyplot as plt
import numpy as np
np.set_printoptions(threshold=np.inf)
cifar10 = tf.keras.datasets.cifar10
(x_train, y_train), (x_test, y_test) = cifar10.load_data()
plt.imshow(x_train[0])
plt.show()
print("x_train[0]:\n", x_train[0])
print("y_train[0]:\n", y_train[0])
print("x_train.shape:\n", x_train.shape)
print("y_train.shape:\n", y_train.shape)
print("x_test.shape:\n", x_test.shape)
print("y_test.shape:\n", y_test.shape)
x_train.shape: (50000, 32, 32, 3) y_train.shape: (50000, 1) x_test.shape: (10000, 32, 32, 3) y_test.shape: (10000, 1)
2.11 神经网络搭建示例
掌握了利用 tf.keras 来搭建神经网络的八股之后,就可以搭建自己的神经网络来对数据 集进行训练了,这里提供一个实例,利用一个结构简单的基础卷积神经网络(CNN)来对cifar10 数据集进行训练,网络结构如图 5-18 所示。 利用 tf.keras.Sequential模型以及 class 定义两种方式都可以构建出图 5-18 中的基础 CNN网络,在此例中二者的效果是完全相同的,前者看起来会更简洁一些,但后者在实际应用中更加常用,因为这仅仅是一个非常基础的网络,而一些复杂的网络经常会有 Sequential 模型无法表达的结构或设计,所以在这里采用后者,如图 5-19 所示。
3 CNN经典网络
在卷积神经网络的发展历程中,出现过许多经典的网络结构,这些CNN经典网络的提出都曾极大地促进了领域的发展,这里对5个经典的CNN网络结构做一个介绍,从1998年由Yann LeCun提出的LeNet直至2015年由何恺明提出的ResNet,如图5-20所示。
值得一提的是,除了卷积网络的“开篇之作”LeNet以外,AlexNet、VGGNet、InceptionNet以及ResNet这四种经典网络全部是在当年的ImageNet竞赛中问世的,它们作为深度学习的经典代表,使得ImageNet数据集上的错误率逐年降低。下面将会对这五种经典网络逐一进行介绍:
附:CNN经典网络论文出处
LeNet-5: Yann Lecun, Leon Bottou, Y. Bengio, Patrick Haffner. Gradient-Based Learning Applied to Document Recognition. Proceedings of the IEEE, 1998. AlexNet: Alex Krizhevsky, Ilya Sutskever, Geoffrey E. Hinton. ImageNet Classification with Deep Convolutional Neural Networks. In NIPS, 2012. VGG16: K. Simonyan, A. Zisserman. Very Deep Convolutional Networks for Large-Scale Image Recognition.In ICLR, 2015. Inception-v1: Szegedy C, Liu W, Jia Y, et al. Going Deeper with Convolutions. In CVPR, 2015. ResNet: Kaiming He, Xiangyu Zhang, Shaoqing Ren. Deep Residual Learning for Image Recognition. In CPVR, 2016.
3.1 LeNet
模型实现代码:p31_cifar10_lenet5.py 借鉴点:共享卷积核,减少网络参数。 LeNet即LeNet5,由Yann LeCun在1998年提出,做为最早的卷积神经网络之一,是许多神经网络架构的起点,其网络结构如图5-21所示。 根据以上信息,就可以根据上一节所总结出来的方法,在Tensorflow框架下利用tf.Keras来构建LeNet5模型,如图5-22所示。 图中紫色部分为卷积层,红色部分为全连接层,模型图与代码一一对应,模型搭建具体流程如下(各步骤的实现函数在5.2节中均有介绍): A)输入图像大小为32 * 32 * 3,三通道彩色图像输入; B)进行卷积,卷积核大小为5 * 5,个数为6,步长为1,不进行全零填充; C)将卷积结果输入sigmoid激活函数(非线性函数)进行激活; D)进行最大池化,池化核大小为2 * 2,步长为2; E)进行卷积,卷积核大小为5 * 5,个数为16,步长为1,不进行全零填充; F)将卷积结果输入sigmoid激活函数进行激活; G)进行最大池化,池化核大小为2 * 2,步长为2;
H)输入三层全连接网络进行10分类。
与最初的LeNet5网络结构相比,这里做了一点微调,输入图像尺寸为32 * 32 * 3,以适应cifar10数据集(此数据集在5.2节中也有具体介绍)。模型中采用的激活函数有sigmoid和softmax,池化层均采用最大池化,以保留边缘特征。
总体上看,诞生于1998年的LeNet5与如今一些主流的CNN网络相比,其结构可以说是相当简单,不过它成功地利用“卷积提取特征→全连接分类”的经典思路解决了手写数字识别的问题,对神经网络研究的发展有着很重要的意义。
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
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 LeNet5(Model):
def __init__(self):
super(LeNet5, self).__init__()
self.c1 = Conv2D(filters=6, kernel_size=(5, 5),
activation='sigmoid')
self.p1 = MaxPool2D(pool_size=(2, 2), strides=2)
self.c2 = Conv2D(filters=16, kernel_size=(5, 5),
activation='sigmoid')
self.p2 = MaxPool2D(pool_size=(2, 2), strides=2)
self.flatten = Flatten()
self.f1 = Dense(120, activation='sigmoid')
self.f2 = Dense(84, activation='sigmoid')
self.f3 = Dense(10, activation='softmax')
def call(self, x):
x = self.c1(x)
x = self.p1(x)
x = self.c2(x)
x = self.p2(x)
x = self.flatten(x)
x = self.f1(x)
x = self.f2(x)
y = self.f3(x)
return y
model = LeNet5()
model.compile(optimizer='adam',
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False),
metrics=['sparse_categorical_accuracy'])
checkpoint_save_path = "./checkpoint/LeNet5.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=32, epochs=5, validation_data=(x_test, y_test), validation_freq=1,
callbacks=[cp_callback])
model.summary()
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()
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()
3.2 AlexNet
模型实现代码:p34_cifar10_alexnet8.py 借鉴点:激活函数使用Relu,提升训练速度;Dropout防止过拟合。 AlexNet网络诞生于2012年,其ImageNet Top5错误率为16.4 %,可以说AlexNet的出现使得已经沉寂多年的深度学习领域开启了黄金时代。 AlexNet的总体结构和LeNet5有相似之处,但是有一些很重要的改进: A) 由五层卷积、三层全连接组成,输入尺寸为2242243,网络规模远大于LeNet5 B) 进行了Relu激活函数 C)进行了Dropout操作,以防止模型过拟合,提升鲁棒性 D)增加了一些训练上的技巧,包括数据增强、学习率衰减、权重衰减(L2正则化) 可以看到,图5-20所示的网络结构将模型分成了两部分,这是由于当时用于训练AlexNet的显卡为GTX 580(显存为3GB),单块显卡运算资源不足的原因。 在Tensorflow框架下利用Keras来搭建AlexNet模型,这里做了一些调整,将输入图像尺寸改为32 * 32 * 3以适应cifar10数据集,并且将原始的AlexNet模型中的11 * 11、7 * 7、5 * 5等大尺寸卷积核均替换成了3 * 3的小卷积核,如图所示。 图中紫色块代表卷积部分,可以看到卷积操作共进行了5次: A)第1次卷积:共有96个3 * 3的卷积核,不进行全零填充,进行BN操作,激活函数为Relu,进行最大池化,池化核尺寸为3 * 3,步长为2 B)第2次卷积:与第1次卷积类似,除卷积核个数由96增加到256之外几乎相同; C)第3次卷积:共有384个3 * 3的卷积核,进行全零填充,激活函数为Relu,不进行BN操作以及最大池化; D)第4次卷积:与第3次卷积几乎完全相同; E)第5次卷积:共有96个3 * 3的卷积核,进行全零填充,激活函数为Relu,不进行BN操作,进行最大池化,池化核尺寸为3 * 3,步长为2。
图中红色块代表全连接部分,共有三层: A)第一层共2048个神经元,激活函数为Relu,进行0.5的dropout; B)第二层与第一层几乎完全相同;
C)第三层共10个神经元,进行10分类。
可以看到,与结构类似的LeNet5相比,AlexNet模型的参数量有了非常明显的提升,卷积运算的层数也更多了,这有利于更好地提取特征;Relu激活函数的使用加快了模型的训练速度;Dropout的使用提升了模型的鲁棒性,这些优势使得AlexNet的性能大大提升。
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
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 AlexNet8(Model):
def __init__(self):
super(AlexNet8, self).__init__()
self.c1 = Conv2D(filters=96, kernel_size=(3, 3))
self.b1 = BatchNormalization()
self.a1 = Activation('relu')
self.p1 = MaxPool2D(pool_size=(3, 3), strides=2)
self.c2 = Conv2D(filters=256, kernel_size=(3, 3))
self.b2 = BatchNormalization()
self.a2 = Activation('relu')
self.p2 = MaxPool2D(pool_size=(3, 3), strides=2)
self.c3 = Conv2D(filters=384, kernel_size=(3, 3), padding='same',
activation='relu')
self.c4 = Conv2D(filters=384, kernel_size=(3, 3), padding='same',
activation='relu')
self.c5 = Conv2D(filters=256, kernel_size=(3, 3), padding='same',
activation='relu')
self.p3 = MaxPool2D(pool_size=(3, 3), strides=2)
self.flatten = Flatten()
self.f1 = Dense(2048, activation='relu')
self.d1 = Dropout(0.5)
self.f2 = Dense(2048, activation='relu')
self.d2 = Dropout(0.5)
self.f3 = Dense(10, activation='softmax')
def call(self, x):
x = self.c1(x)
x = self.b1(x)
x = self.a1(x)
x = self.p1(x)
x = self.c2(x)
x = self.b2(x)
x = self.a2(x)
x = self.p2(x)
x = self.c3(x)
x = self.c4(x)
x = self.c5(x)
x = self.p3(x)
x = self.flatten(x)
x = self.f1(x)
x = self.d1(x)
x = self.f2(x)
x = self.d2(x)
y = self.f3(x)
return y
model = AlexNet8()
model.compile(optimizer='adam',
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False),
metrics=['sparse_categorical_accuracy'])
checkpoint_save_path = "./checkpoint/AlexNet8.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=32, epochs=5, validation_data=(x_test, y_test), validation_freq=1,
callbacks=[cp_callback])
model.summary()
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()
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()
This message will be only logged once. 1563/1563 [] - 81s 34ms/step - loss: 1.8704 - sparse_categorical_accuracy: 0.3089 - val_loss: 1.5785 - val_sparse_categorical_accuracy: 0.4525 Epoch 2/5 1563/1563 [] - 19s 12ms/step - loss: 1.3179 - sparse_categorical_accuracy: 0.5318 - val_loss: 1.2620 - val_sparse_categorical_accuracy: 0.5439 Epoch 3/5 1563/1563 [] - 19s 12ms/step - loss: 1.1636 - sparse_categorical_accuracy: 0.5988 - val_loss: 1.1881 - val_sparse_categorical_accuracy: 0.5826 Epoch 4/5 1563/1563 [] - 20s 13ms/step - loss: 1.0627 - sparse_categorical_accuracy: 0.6309 - val_loss: 1.1078 - val_sparse_categorical_accuracy: 0.6114 Epoch 5/5 1563/1563 [==============================] - 20s 13ms/step - loss: 0.9856 - sparse_categorical_accuracy: 0.6625 - val_loss: 1.0130 - val_sparse_categorical_accuracy: 0.6519
3.3 VGGNet
模型实现代码:p36_cifar10_vgg16.py 借鉴点:小卷积核减少参数的同时,提高识别准确率;网络结构规整,适合并行加速。 在AlexNet之后,另一个性能提升较大的网络是诞生于2014年的VGGNet,其ImageNet Top5错误率减小到了7.3 %。 VGGNet网络的最大改进是在网络的深度上,由AlexNet的8层增加到了16层和19层,更深的网络意味着更强的表达能力,这得益于强大的运算能力支持。VGGNet的另一个显著特点是仅使用了单一尺寸的3 * 3卷积核,事实上,3 * 3的小卷积核在很多卷积网络中都被大量使用,这是由于在感受野相同的情况下,小卷积核堆积的效果要优于大卷积核,同时参数量也更少。VGGNet就使用了3 * 3的卷积核替代了AlexNet中的大卷积核(11 * 11、7 * 7、5 * 5),取得了较好的效果(事实上课程中利用Keras实现AlexNet时已经采取了这种方式),VGGNet16的网络结构如图5-25所示。
VGGNet16和VGGNet19并没有本质上的区别,只是网络深度不同,前者16层(13层卷积、3层全连接),后者19层(16层卷积、3层全连接)。
在Tensorflow框架下利用Keras来实现VGG16网络,为适应cifar10数据集,将输入图像尺寸由224 * 244 * 3调整为32 * 32 * 3,如图5-26所示。
根据特征图尺寸的变化,可以将VGG16模型分为六个部分(在VGG16中,每进行一次池化操作,特征图的边长缩小为1/2,其余操作均未影响特征图尺寸): A)第一部分:两次卷积(64个3 * 3卷积核、BN、Relu激活)→最大池化→Dropout B)第二部分:两次卷积(128个3 * 3卷积核、BN、Relu激活)→最大池化→Dropout C)第三部分:三次卷积(256个3 * 3卷积核、BN、Relu激活)→最大池化→Dropout
D)第四部分:三次卷积(512个3 * 3卷积核、BN、Relu激活)→最大池化→Dropout
E)第五部分:三次卷积(512个3 * 3卷积核、BN、Relu激活)→最大池化→Dropout F)第六部分:全连接(512个神经元)→Dropout→全连接(512个神经元)→Dropout→全连接(10个神经元) 总体来看,VGGNet的结构是相当规整的,它继承了AlexNet中的Relu激活函数、Dropout操作等有效的方法,同时采用了单一尺寸的3 * 3小卷积核,形成了规整的C(Convolution,卷积)、B(Batch normalization)、A(Activation,激活)、P(Pooling,池化)、D(Dropout)结构,这一典型结构在卷积神经网络中的应用是非常广的。
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
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 VGG16(Model):
def __init__(self):
super().__init__()
self.c1 = Conv2D(filters=64, kernel_size=(3,3), padding='same')
self.b1 = BatchNormalization()
self.a1 = Activation('relu')
self.c2 = Conv2D(filters=64, kernel_size=(3,3), padding='same')
self.b2 = BatchNormalization()
self.a2 = Activation('relu')
self.p2 = MaxPool2D(pool_size=(2,2), strides=2, padding='same')
self.d2 = Dropout(0.2)
self.c3 = Conv2D(filters=128, kernel_size=(3,3), padding='same')
self.b3 = BatchNormalization()
self.a3 = Activation('relu')
self.c4 = Conv2D(filters= 128, kernel_size=(3,3), padding='same')
self.b4 = BatchNormalization()
self.a4 = Activation('relu')
self.p4 = MaxPool2D(pool_size=(2,2), strides=2, padding='same')
self.d4 = Dropout(0.2)
self.c5 = Conv2D(filters=256, kernel_size=(3,3), padding='same')
self.b5 = BatchNormalization()
self.a5 = Activation('relu')
self.c6 = Conv2D(filters=256, kernel_size=(3,3), padding='same')
self.b6 = BatchNormalization()
self.a6 = Activation('relu')
self.c7 = Conv2D(filters=256, kernel_size=(3,3), padding='same')
self.b7 = BatchNormalization()
self.a7 = Activation('relu')
self.p7 = MaxPool2D(pool_size=(2,2), strides=2, padding='same')
self.d7 = Dropout(0.2)
self.c8 = Conv2D(filters=512, kernel_size=(3,3), padding='same')
self.b8 = BatchNormalization()
self.a8 = Activation('relu')
self.c9 = Conv2D(filters=512, kernel_size=(3,3), padding='same')
self.b9 = BatchNormalization()
self.a9 = Activation('relu')
self.c10 = Conv2D(filters=512, kernel_size=(3,3), padding='same')
self.b10 = BatchNormalization()
self.a10 = Activation('relu')
self.p10 = MaxPool2D(pool_size=(2,2), strides=2, padding='same')
self.d10 = Dropout(0.2)
self.c11 = Conv2D(filters=512, kernel_size=(3,3), padding='same')
self.b11 = BatchNormalization()
self.a11 = Activation('relu')
self.c12 = Conv2D(filters=512, kernel_size=(3,3), padding='same')
self.b12 = BatchNormalization()
self.a12 = Activation('relu')
self.c13 = Conv2D(filters=512, kernel_size=(3,3), padding='same')
self.b13 = BatchNormalization()
self.a13 = Activation('relu')
self.p13 = MaxPool2D(pool_size=(2,2), strides=2, padding='same')
self.d13 = Dropout(0.2)
self.flatten = Flatten()
self.f14 = Dense(512, activation='relu')
self.d14 = Dropout(0.2)
self.f15 = Dense(512, activation='relu')
self.d15 = Dropout(0.2)
self.f16 = Dense(10, activation='softmax')
def call(self, x):
x = self.c1(x)
x = self.b1(x)
x = self.a1(x)
x = self.c2(x)
x = self.b2(x)
x = self.a2(x)
x = self.p2(x)
x = self.d2(x)
x = self.c3(x)
x = self.b3(x)
x = self.a3(x)
x = self.c4(x)
x = self.b4(x)
x = self.a4(x)
x = self.p4(x)
x = self.d4(x)
x = self.c5(x)
x = self.b5(x)
x = self.a5(x)
x = self.c6(x)
x = self.b6(x)
x = self.a6(x)
x = self.c7(x)
x = self.b7(x)
x = self.a7(x)
x = self.p7(x)
x = self.d7(x)
x = self.c8(x)
x = self.b8(x)
x = self.a8(x)
x = self.c9(x)
x = self.b9(x)
x = self.a9(x)
x = self.c10(x)
x = self.b10(x)
x = self.a10(x)
x = self.p10(x)
x = self.d10(x)
x = self.c11(x)
x = self.b11(x)
x = self.a11(x)
x = self.c12(x)
x = self.b12(x)
x = self.a12(x)
x = self.c13(x)
x = self.b13(x)
x = self.a13(x)
x = self.p13(x)
x = self.d13(x)
x = self.flatten(x)
x = self.f14(x)
x = self.d14(x)
x = self.f15(x)
x = self.d15(x)
y = self.f16(x)
return y
model = VGG16()
model.compile(optimizer='adam',
loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False),
metrics = ['sparse_categorical_accuracy'])
checkpoint_save_path = "./checkpoint/VGG10.ckpt"
if os.path.exists(checkpoint_save_path):
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 = 32, epochs = 5, validation_data=(x_test, y_test), validation_freq=1, callbacks=[cp_callback])
model.summary()
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')
flie.close()
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()
tegorical_accuracy: 0.7033 - val_loss: 0.8291 - val_sparse_categorical_accuracy: 0.7198 Model: “vg_g16”
3.4 InceptionNet
模型实现代码:p40_cifar10_inception26.py 借鉴点:一层内使用不同尺寸的卷积核,提升感知力(通过padding实现输出特征面积一致);使用1 * 1卷积核,改变输出特征channel数(减少网络参数)。
InceptionNet即GoogLeNet,诞生于2015年,旨在通过增加网络的宽度来提升网络的能力,与VGGNet通过卷积层堆叠的方式(纵向)相比,是一个不同的方向(横向)。
显然,InceptionNet模型的构建与VGGNet及之前的网络会有所区别,不再是简单的纵向堆叠,要理解InceptionNet的结构,首先要理解它的基本单元,如图5-27所示。
可以看到,InceptionNet的基本单元中,卷积部分是比较统一的C、B、A典型结构,即卷积→BN→激活,激活均采用Relu激活函数,同时包含最大池化操作。
在Tensorflow框架下利用Keras构建InceptionNet模型时,可以将C、B、A结构封装在一起,定义成一个新的ConvBNRelu类,以减少代码量,同时更便于阅读。 参数ch代表特征图的通道数,也即卷积核个数;kernelsz代表卷积核尺寸;strides代表卷积步长;padding代表是否进行全零填充。 完成了这一步后,就可以开始构建InceptionNet的基本单元了,同样利用class定义的方式,定义一个新的InceptionBlk类,如5-28所示。 参数ch仍代表通道数,strides代表卷积步长,与ConvBNRelu类中一致;tf.concat函数将四个输出连接在一起,x1、x2_2、x3_2、x4_2分别代表图5-27中的四列输出,结合结构图和代码很容易看出二者的对应关系。
可以看到,InceptionNet的一个显著特点是大量使用了1 * 1的卷积核,事实上,最原始的InceptionNet的结构是不包含1 * 1卷积的,如图5-29所示 由图5-29可以更清楚地看出InceptionNet最初的设计思想,即通过不同尺寸卷积层和池化层的横向组合(卷积、池化后的尺寸相同,通道可以相加)来拓宽网络深度,可以增加网络对尺寸的适应性。但是这样也带来一个问题,所有的卷积核都会在上一层的输出上直接做卷积运算,会导致参数量和计算量过大(尤其是对于5 * 5的卷积核来说)。因此,InceptionNet在3 * 3、5 * 5的卷积运算前、最大池化后均加入了1 * 1的卷积层,形成了图5-24中的结构,这样可以降低特征的厚度,一定程度上避免参数量过大的问题。
那么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网络的主体就是由其基本单元构成的,其模型结构如图5-30所示。 图中橙色框内即为InceptionNet的基本单元,利用之前定义好的InceptionBlk类堆叠而成,模型的实现代码如下。 参数num_layers代表InceptionNet的Block数,每个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很好地解决了)。
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().__init__()
self.model = tf.keras.Sequential([
Conv2D(filters=ch, kernel_size=kernelsz, strides=strides, padding=padding),
BatchNormalization(),
Activation('relu')
])
def call(self, x):
x = self.model(x, training = False)
return x
class InceptionBlk(Model):
def __init__(self, ch, strides=1):
super().__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)
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):
super().__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.in_channels, strides=1)
self.blocks.add(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)
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=32, epochs=5,
validation_data=(x_test, y_test), validation_freq=1,
callbacks=[cp_callback])
model.summary()
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()
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()
797 - val_loss: 0.9224 - val_sparse_categorical_accuracy: 0.6707
Model: "inception10"
______________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv_bn_relu (ConvBNRelu) multiple 512
_________________________________________________________________
sequential_1 (Sequential) (None, 8, 8, 64) 84832
_________________________________________________________________
global_average_pooling2d (Gl multiple 0
_________________________________________________________________
dense (Dense) multiple 650
=================================================================
Total params: 85,994
Trainable params: 85,002
Non-trainable params: 992
____________________________
|