1.VGGNet模型结构简介
VGGNet是由牛津大学计算机视觉几何组(Visual Geomety Group,VGG)和Google Deepmind公司的研究员合作研发的深度卷积神经网络,VGG的成员Karen Simonyan和Andrew Zisserman在2014年撰写的论文《Very Deep Convolutional Networks for Large-Scale Image Recongnition》中正式提出了该深度卷积神经网络的结构。
VGGNet对卷积神经网络的深度与其性能之间的关系进行了探索。网络的结构非常简洁,在整个网络中使用了大小相同的卷积核(3*3)和最大池化核(2*2)。通过重复堆叠的方式,使用这些卷积层和最大池化层成功地搭建了11~19层深地卷积神经网络。
VGGNet通过不断地加深网络结构来提升性能,牛津大学计算机视觉几何组对11~19层地网络都进行了详尽的性能测试。根据网络深度的不同以及是否使用LRN,VGGNet可以分为A-E 6个级别。如下图所示为VGGNet各级别的网络结构表。
尽管从A级到E级对网络进行了逐步加深,但是网络的参数量并没有显著增加,这是因为最后3个全连层占据了大量的参数。在6个级别的VGGNet中,全连层都是相同的。卷积层的参数共享和局部连接对降低参数量做出了重大的贡献,但是由于卷积操作和池化操作的运算过程比较复杂,所以训练中比较耗时的依然是卷积层。下图展示了每一级别的参数量。
从第1张图可以看出,VGGNet共有5段卷积,每一段卷积内部有一定数量的卷积层(或1个或4个),所以是5阶段卷积特征提取。每一段卷积之后都有一个max-pool层,这些最大池化层被用来缩小图片的尺寸。
同一段内的卷积层拥有相同的卷积核数,之后每增加一段,该段内卷积层的卷积核数就增加1倍。接受input的第一段卷积中,每个卷积层拥有最少的64个卷积核,接着第二段卷积中每个卷积层的卷积核数量上升到128个;最后一段卷积拥有最多的卷积层数,每个卷积层拥有最多的512个卷积核。
C级的VGGNet有些例外,它的第一段和第二段卷积都与B级VGGNet相同,只是在第三段,第四段,第五段卷积中相比B级VGGNet各多了一个1*1大小卷积核的卷积层。这里核大小1*1的卷积运算主要用于在输入通道数和输出通道数不变(不发生数据降维)的情况下实现线性变换。
将多个3*3卷积核的卷积层堆叠在一起是一种非常有趣也非常有用的设计,这对降低卷积核的参数非常有帮助,同时也会加强CNN对特征的学习能力。下图展示了2个3*3卷积核的卷积层堆叠在一起的情况,我们将借助这幅图来进行一些说明。
如上图所示,左侧展示了2个核大小为3*3的卷积层堆叠起来的情况,右侧展示了只有1个核大小5*5的卷积层的情况。经过对比之后我们发现两者的效果相同,2个核大小为3*3的卷积层堆叠起来相当于一个核大小5*5的卷积层,即得到的每一个值会跟经过卷积操作前的5*5个像素产生关联(感受野由3*3变成5*5)。使用堆叠卷积层的方法能降低参数数量,左侧的情况下需要18(9+9)个参数,而右侧的情况下需要25个参数。
可以将这种2层相叠的情况扩展到3层或者4层或者更多的层,比如使用3个核大小3*3的卷积层堆叠起来的效果相当于1个核大小7*7的卷积层。当使用1个卷积层时,只能对卷积的结果使用一次激活函数(进行一次非线性变换),但是当堆叠在一起的卷积层达到3个或者4个之后,因为在每一次卷积结束之后都使用了一次激活函数,所以计算的过程中相当于进行了多次非线性变换,进而加强了CNN对特征的学习能力。
在网络训练时,VGGNet通过一个称为Multi-Scale的方法对图像进行数据增强处理。这个过程大致就是先将原始的图像缩放到不同的尺寸(缩放后的短边长度用S表示,在实践中,一般令S在[256,512]这个区间内取值),然后将得到的图像进行224*224的随机剪裁。数据增强处理能增加很多数据量,对于防止模型过拟合有很好的效果。因为卷积神经网络对于图像的的缩放有一定的不变性,所以将这种经过Multi-Scale多尺度缩放裁剪后的图片输入到卷积神经网络中训练可以增加网络的不变性。经过Multi-Scale多尺度缩放裁剪后可以获得多个版本的图像数据。下表所示为VGGNet使用Multi-Scale训练时得到的结果,可以看到D和E结构的网络都可以达到7.5%的错误率。![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/571c6db79a40a7b9b8ef7077fb80f377.png)
在预测时,VGGNet也采用了Multi-Scale的方法,将图像scale到一个尺寸(缩放后的短边长度用Q表示,Q会大于224且不必等于S)再剪裁,并将裁剪后的图片输入到卷积神经网络中计算。输入到网络中的图片是某一张图片经过缩放裁剪后的多个样本,这样会得到一张图片的多个分类结果,所以紧接着要做的就是对这些分类结果进行平均以得到最后这张图片的分类结果。这种平均的方式会提高图片数据的利用率并使分类的效果更好。
下表展示了没有使用Multi-Scale的方式进行数据增强处理时各级别的VGGNet在测试时得到的top-5错误率(也称为Single-Scale)。与之进行比较的的上表所示的使用了Multi-Scale的方式进行数据增强处理时各级别的VGGNet在测试时得到的top-5错误率。![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/3efb8827798ca5775666d5bd2ab6b409.png)
上两张表的数据均出自论文《Very Deep Convolutional Networks for Large-Scale Image Recongnition》。通过比较A与A-LRN的错位率,我们发现A-LRN的效果没有A的好,这说明LRN的作用不是很明显;将A与B,C,D,D进行比较,我们发现网络越深越好;将A与C进行比较,我们发现增加1*1的fillter对非线性的提升有很好的作用;将C与D进行比较,我们发现使用3*3的大fillter比使用1*1的小fillter能捕获更大的空间特征。
VGG将Single-Scale的6个不同等级的网络与Multi-Scale的D网络进行融合,并将融合后的网络作为VGGNet的最终版本提交到ILSVRC 2014大赛举办方。这个VGGNet的最终版本在ILSVRC 2014竞赛的图像分类问题中达到了7.3%的top-5错位误率。
2.使用TensorFlow搭建VGGNet并实现Cifar10数据集分类
在这里,我们选择D结构的VGGNet(又称为VGGNet-16。同理,E结构的VGGNet又称为VGGNet-19)进行说明。由于ImageNet数据集太庞大,所以我选择了对Cifar10数据集进行分类来了解VGGNet-16。
首先,编写Cifar10_data.py文件来实现对数据的读取和预处理
导入相关库和定义一些需要用到的变量
import os
import tensorflow as tf
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
import numpy as np
import time
import math
num_classes = 10 #一共有10个类别
num_examples_pre_epoch_for_train = 50000 #50000个样本用于训练
num_examples_pre_epoch_for_eval = 10000 #10000个样本用于测试
max_steps = 4000 #训练4000步
batch_size = 100 #每次训练100个样本
num_examples_for_eval = 10000
data_dir = "C:/Users/Administrator/Desktop/Tensorflow/cifar-10-batches-bin" #下载的样本的路径
class CIFAR10Record(object): #定义一个空类,用于返回读取的Cifar-10数据
pass
接着定义一个read_cifar10()函数用于读取文件队列中的数据
def read_cifar10(file_queue): #file_queue为图片路径
result = CIFAR10Record() #创建一个CIFAR10Record对象
label_bytes = 1 #标签占一个字节
result.height = 32 #图像高为32像素
result.width = 32 #图像宽为32像素
result.depth = 3 #因为是RGB三通道,所以深度为3
image_bytes = result.height * result.width * result.depth #结果为3072,即一幅图像的大小为3072字节
record_bytes = label_bytes + image_bytes #加上标签,即一个样本一共有3073字节
reader = tf.FixedLengthRecordReader(record_bytes=record_bytes) #使用FixedLengthRecordReader类创建一个用于读取固定长度字节数信息的对象(针对bin文件而言)
result.key, value = reader.read(file_queue) #使用该类的read()方法读取指定路径下的文件
#这里得到的value就是record_bytes长度的包含多个label数据和image数据的字符串
record_bytes = tf.decode_raw(value, tf.uint8)
#decode_raw()可以将字符串解析成图像对应的像素数组
#strided_slice(input,begin,end)用于对输入的input截取[begin,end)区间的数据
result.label = tf.cast(tf.strided_slice(record_bytes, [0], [label_bytes]), tf.int32)
#这里把record_bytes的第一个元素截取下来然后转换成int32类型的数
#剪切label之后剩下的就是图片数据,我们将这些数据的格式从[depth*height*width]转换为[depth,height,width]
depth_major = tf.reshape(tf.strided_slice(record_bytes, [label_bytes], [label_bytes + image_bytes]),
[result.depth, result.height, result.width])
#将[depth,height,width]的格式转换为[height,width,depth]的格式
result.uint8image = tf.transpose(depth_major, [1, 2, 0])
return result
紧接着read_cifar10()函数的是inputs函数,这个函数用于构建文件路径,将构建的文件路径传给read_cifar10()函数读取样本,对读取到的样本进行数据增强处理。
def inputs(data_dir, batch_size, distorted): #data_dir为文件路径,batch_size为读取批量大小,distorted是否对样本进行增强处理
#拼接文件名路径列表
filenames = [os.path.join(data_dir, "data_batch_%d.bin" % i) for i in range(1, 6)]
#创建一个文件队列,并调用read_cifar10()函数读取队列中的文件,在后面还要调用一个tf.train.start_queue_runners()函数才开始读取图片
file_queue = tf.train.string_input_producer(filenames)
read_input = read_cifar10(file_queue)
#使用tf.cast()对图片数据进行转换
reshaped_image = tf.cast(read_input.uint8image, tf.float32)
num_examples_per_epoch = num_examples_pre_epoch_for_train
if distorted != None:
#将[32,32,3]大小的图片随机剪裁成[24,24,3]的大小
cropped_image = tf.random_crop(reshaped_image, [24, 24, 3])
#随机左右翻转图片
flipped_image = tf.image.random_flip_left