一. 背景介绍
VERY DEEP CONVOLUTIONAL NETWORKS FOR LARGE-SCALE IMAGE RECOGNITION
是牛津大学计算机视觉实验室参加2014年ILSVRC(ImageNet Large Scale Visual Recognition Challenge)比赛的网络结构。解决ImageNet中的1000类图像分类和localization。
实验结果是VGGNet斩获了2014年ILSVRC分类第二,定位第一。(当年分类第一是GoogleNet,后续会介绍)
Oxford Visual Geometry Group
Robotics Research Group
Paper link
二. Abstract
1.VGGNet 探索的是神经网络的深度(depth)与其性能之间的关系。
VGG通过反复堆叠3×3的小型卷积核和2×2的最大池化层,VGG成功构建了16-19层的卷积神经网络。是当时在论文发表前最深的深度网络。实际上,VGG在探索深度对神经网络影响的同时,其实本身广度也是很深的。那么:
神经网络的深度和广度对其本身的影响是什么呢?
- 卷积核的种类对应了网络的广度,卷积层数对应了网络的深度。这两者对网络的拟合都有影响。但是在现代深度学习中,大家普遍认为深度比广度的影响更加高。
- 宽度即卷积核的种类个数,在LeNet那篇文章里我们说了,权值共享(每个神经元对应一块局部区域,如果局部区域是10*10,那么就有100的权重参数,但如果我们把每个神经元的权重参数设置为一样,相当于每个神经元用的是同一个卷积核去卷积图像,最终两层间的连接只有 100 个参数 !)可以大大减少我们的训练参数,但是由于使用了同一个卷积核,最终特征个数太少,效果也不会好,所以一般神经网络都会有多个卷积核,这里说明宽度的增加在一开始对网络的性能提升是有效的。但是,随着广度的增加,对网络整体的性能其实是开始趋于饱和,并且有下降趋势,因为过多的特征(一个卷积核对应发现一种特征)可能对带来噪声的影响。
- 深度即卷积层的个数,对网络的性能是极其重要的,ResNet已经表明越深的深度网络性能也就越好。深度网络自然集成了低、中、高层特征。多层特征可以通过网络的堆叠的数量(深度)来丰富其表达。挑战imagenet数据集的优秀网络都是采用较深的模型。网络的深度很重要,但是否能够简单的通过增加更多的网络层次学习更好的网络?这个问题的障碍就是臭名昭著的梯度消失(爆炸)问题,这从根本上阻碍了深度模型的收敛。
- 增加更多的卷积核可以发现更多的特征,但是特征是需要进行组合的,只有知道了特征之间的关系才能够更好的表达图片内容,而增加深度就是组合特征的过程。
2. VGG结构全部都采用较小的卷积核(3×3,部分1×1)
在VGG出现之前的深度网络,比如ZFNet或Overfeat普遍都采用了7×7和11×11的卷积核。VGG通篇全部采用很小的卷积核。我们再回顾一下在深度学习中卷积核的感受野的作用。
如何选择卷积核的大小?越大越好还是越小越好?
答案是小而深,单独较小的卷积核也是不好的,只有堆叠很多小的卷积核,模型的性能才会提升。
- 如上图所示,CNN的卷积核对应一个感受野,这使得每一个神经元不需要对全局图像做感受,每个神经元只感受局部的图像区域,然后在更高层,将这些感受不同局部的神经元综合起来就可以得到全局信息。这样做的一个好处就是可以减少大量训练的参数。
-
VGG经常出现多个完全一样的3×3的卷积核堆叠在一起的情况,这些多个小型卷积核堆叠的设计其实是非常有效的。如下图所示,两个3×3的卷积层串联相当于1个5×5的卷积层,即一个像素会和周围5×5的像素产生关联,可以说感受野是5×5。同时,3个串联的3×3卷积层串联的效果相当于一个7×7的卷积层。除此之外,3个串联的3×3的卷积层拥有比一个7×7更少的参数量,只有后者的 (3×3×3) / (7×7) = 55%。最重要的是3个3×3的卷积层拥有比一个7×7的卷积层更多的非线性变换(前者可以使用三次ReLu激活,而后者只有一次)。
3.VGG获得了2014年ILSVRC分类第二,定位第一。(当年分类第一是GoogleNet,后续会介绍)
三. Architecture
1. vgg模型的输入是固定的224×224的彩色RGB通道图像。
2. 输入做的唯一一个数据预处理就是各自减去 RGB 3个通道的均值
3. 使用的是非常小的3×3的卷积核。
4. 其中一个结构采用了一些1×1的卷积核。
1×1的卷积核到底有什么作用呢?
- 1×1的卷积核和正常的滤波器完全是一样的,只不过它不再感受一个局部区域,不考虑像素与像素之间的关系。1×1的卷积本身就是不同feature channel的线性叠加。1×1的卷积最早出现在Network in Network这篇文章中,在Google的inception结构中也采用了大量1×1的卷积。
- NIN论文中解释1×1的卷积实现了多个feature map的结合,从而整合了不同通道间的信息。(个人认为这个作用并不是特点,因为其它大小的卷积核也可以实现)
-
1×1的卷积可以实现通道数量的升维和降维。并且是低成本的特征变换(计算量比3×3小很多)。是一个性价比很高的聚合操作。怎么理解1×1是性价比很高的升降通道数的操作呢?
(以google inception为例)
原始结构:
参数:(1×1×192×64) + (3×3×192×128) + (5×5×192×32) = 153600
最终输出的feature map:64+128+32+192 = 416
加入不同channel的1×1卷积后:
参数:1×1×192×64+(1×1×192×96+3×3×96×128)+(1×1×192×16+5×5×16×32)=15872
最终输出的feature map: 64+128+32+32=256
所以加入1×1的卷积后,在降低大量运算的前提下,降低了维度。
5. 卷积步长是一个像素
6.采用最大池化层
7. 不是所有卷积层后面都接一个池化层。(和之前的网络有区别,是反复堆叠几个3×3的卷积)
8. 最大池化是2×2,步长为2.
9. 最后接了3个全连接层
10. 前两个全连接都是4096,最后一个根据imagenet1000类定为1000个输出
11. 分类层是softmax
12. 所有隐层都进行了ReLU激活。
13. 只有一个地方使用了LRN,并且实验表明LRN没有任何用处。
四. ConvNet Configurations
- VGG全部使用了3×3的卷积核和2×2的池化核,通过不断加深网络结构来提升性能。上图为VGG各个级别的网络结构图。
- VGG各种级别的结构都采用了5段卷积,每一段有一个或多个卷积层。同时每一段的尾部都接着一个最大池化层来缩小图片尺寸。每一段内的卷积核数量一致,越靠后的卷积核数量越多 64-128-256-512-512。经常出现多个完全一样的卷积层堆叠在一起的情况。
- A-LRN结构使用了LRN,结果表明并没有什么用处。
- C 结构比B多了几个1×1的卷积。在VGG里,1×1的卷积意义主要是线性变换,输入输出通道数量并没有变化。没有发生降维。所以作者认为1×1没有3×3好,大一些的卷积核可以学到更大的空间特征。
- A-E 每一级网络逐渐变深,但是参数并没有变多很多。这是因为参数量主要消耗在最后3个全连接层,卷积虽然深但是参数消耗并不多。但是训练耗时的仍然是卷积,因其计算量大。
- D E就是我们经常说的VGG-16和VGG-19
五. Training
这个部分是VGG当时是怎么训练的具体过程,有很多值得借鉴的地方。
1. 使用mini-batch的梯度下降法,并且是带动量的。batch_size设置为256,动量是0.9。
2. 前两个全连接使用了dropout,值是0.5, 用来缓解过拟合。
3. 学习率初始设置为0.01,衰减系数为10,每当验证集上准确率不再变好时,会降低学习率。学习率一共被衰减3次。总共训练了74个epoch,370k个iteration。
VGG的参数初始化方式是怎么样的?
- 上图中间红框部分作者介绍了VGG训练时参数的初始化方式,这个部分比较有意思。作者认为这么深的网络(论文发表前最深)训练收敛是很困难的,必须借助有效的参数初始化方式。
- 作者先训练上面网络结构中的A结构,A收敛之后呢,将A的网络权重保存下来,再复用A网络的权重来初始化后面几个简单模型。
- 复用A的网络权重,只是前四个卷积层,以及后三层全连接层,其它的都是随机初始化。
- 随机初始化,均值是0,方差是0.01。bias是0.
六. Image Size
在训练和测试阶段,VGG都采用了Multi-scale的方式。
VGG的Multi-Scale方法
- VGG在训练阶段使用了Multi-Scale的方法做数据增强,将原始图片缩放到不同的尺寸S,然后再随机裁剪224×224的图片,这样能增加很多数据量,对于防止模型过拟合有很不错的效果。
- 实验中,作者令S在[256, 512]这个区间,使用Multi-Scale获得了多个版本的数据,并将多个版本的数据合在一起训练。
- 在测试时,也采用了Multi-Scale的方法,将图像scale到一个尺寸Q,并将图片输入卷积网络计算,然后再最后一个卷积层使用滑窗的方式进行分类预测,将不同窗口的分类结果平均,再将不同尺寸Q的结果平均,得到最后的结果。这样可以提高数据的利用率和预测准确率。
-
下图是VGG各种不同scale的训练结果,融合了Multi-Scale的D和E是最好的。
七. Tensorflow实现简单的VGG-16的结构
本部分整理自《Tensorflow实战》
1. 实现卷积操作函数
VGG包含很多卷积,函数conv_op创建卷积层,并把本层参数存入参数列表。这样可以方便后面VGG结构中多次使用卷积操作。
输入参数
- input_op: 输入tensor,是一个4D的tensor,可以理解为image batch。shape=[batch, in_height, in_width, in_channels],即:[训练时一个batch的图片数量, 图片高度, 图片宽度, 图像通道数]
- kh:卷积核的高
- kw:卷积核的宽
- n_out:输出通道数;这三个参数恰好定义了一个卷积核的参数,卷积核kernel的参数即为: shape=[filter_height, filter_width, in_channels, out_channels],即:[卷积核的高度,卷积核的宽度,图像通道数,卷积核个数]
- dh:卷积的步长高
- dw:卷积的步长宽。 进行卷积操作时,我们除了需要输入的tensor和卷积核参数外,还需要定义卷积的步长strides,strides卷积时在每一维上的步长,strides[0]=strides[3]=1。
- p:参数列表。将卷积核和bias的参数写入p,以便后面使用
输出:
经过卷积,和激活函数的tensor。[batch_size, new_h, new_w, n_out]
def conv_op(input_op, name, kh, kw, n_out, dh, dw, p):
n_in = input_op.get_shape()[-1].value
with tf.name_scope(name) as scope:
kernel = tf.get_variable(scope+'w', shape=[kh, kw, n_in, n_out], dtype = tf.float32,
initializer = tf.contrib.layers.xavier_initializer_conv2d())
conv = tf.nn.conv2d(input_op, kernel, (1, dh, dw, 1), padding = 'SAME')
bias_init_val = tf.constant(0.0, shape = [n_out], dtype = tf.float32)
biases = tf.variable(bias_init_val, trainable = True, name = 'b')
z = tf.nn.bias_add(conv, biases)
activation = tf.nn.relu(z, name = scope)
p += [kernel, biases]
return activation
- 整体过程非常简单,建议熟背这一段代码。首先获得输入的tensor,即image batch,并且定义name,卷积核的大小,卷积的步长(输入参数)。
- 使用get_shape()获得输入tensor的输入通道数。
- name_scope将scope内生成的variable自动命名为name/xxx,用于区分不同卷积层的组件
- get_variable()定义卷积核的参数,注意shape和初始化方式
- conv2d对输入tensor,使用刚刚的卷积核进行卷积操作,注意此处定义strides和padding
- constant定义bias,注意shape等于输出通道数。一个卷积核对应一个输出通道,对应一个bias。tf.variable再将其转换成可训练的参数。
- 进行relu激活
2. 实现池化操作函数
输入参数
- input_op: 输入tensor
- kh:池化的高
- kw:池化的宽
- dh:池化的步长高
- dw:池化的步长宽。
def maxpool_op(input_op, name, kh, kw, dh, dw):
return tf.nn.max_pool(input_op,
ksize=[1, kh, kw, 1],
strides=[1, dh, dw, 1],
padding='SAME',
name=name)
这部分代码也很容易理解,nn.max_pool可以直接使用,需要定义池化的大小ksize,步长strides,以及边界填充方式padding。此部分没有需要训练的参数。
3. 定义全连接操作函数
输入参数
- input_op: 输入的tensor
- n_out: 输出向量长度。 全连接只需要这两个参数
def fc_op(input_op, name, n_out, p):
n_in = input_op.get_shape()[-1].value
with tf.name_scope(name) as scope:
kernel = tf.get_variable(scope+"w", shape=[n_in, n_out], dtype=tf.float32,
initializer=tf.contrib.layers.xavier_initializer())
biases = tf.Variable(tf.constant(0.1, shape=[n_out], dtype=tf.float32), name='b')n
activation = tf.nn.relu_layer(input_op, kernel, biases, name= scope)
p += [kernel, biases]
return activation
此部分代码也很简单,全连接层需要训练参数,并且比卷积层更多(卷积层是局部连接),同样获得输入图片tensor的通道数(向量长度),同样获得输入图片tensor的通道数,注意每个训练参数都需要给定初始化值或初始化方式。bias利用constant函数初始化为较小的值0.1,而不是0, 再做relu非线性变。
4. 根据论文结构创建VGG16网络
input_op是输入的图像tensor shape=[batch, in_height, in_width, in_channels]
keep_prob是控制dropout比率的一个placeholder
def inference_op(input_op,keep_prob):
# 初始化参数p列表
p = []
VGG16包含6个部分,前面5段卷积,最后一段全连接,
每段卷积包含多个卷积层和pooling层.
下面是第一段卷积,包含2个卷积层和一个pooling层,
利用前面定义好的函数conv_op,mpool_op 创建这些层
# 第一段卷积的第一个卷积层 卷积核3*3,共64个卷积核(输出通道数),步长1*1
# input_op:224*224*3 输出尺寸224*224*64
conv1_1 = conv_op(input_op, name="conv1_1", kh=3, kw=3, n_out=64, dh=1,
dw=1, p=p)
# 第一段卷积的第2个卷积层 卷积核3*3,共64个卷积核(输出通道数),步长1*1
# input_op:224*224*64 输出尺寸224*224*64
conv1_2 = conv_op(conv1_1, name="conv1_2", kh=3, kw=3, n_out=64, dh=1,
dw=1, p=p)
# 第一段卷积的pooling层,核2*2,步长2*2
# input_op:224*224*64 输出尺寸112*112*64
pool1 = mpool_op(conv1_2, name="pool1", kh=2, kw=2, dh=2, dw=2)
下面是第2段卷积,包含2个卷积层和一个pooling层
# 第2段卷积的第一个卷积层 卷积核3*3,共128个卷积核(输出通道数),步长1*1
# input_op:112*112*64 输出尺寸112*112*128
conv2_1 = conv_op(pool1, name="conv2_1", kh=3, kw=3, n_out=128, dh=1,
dw=1, p=p)
# input_op:112*112*128 输出尺寸112*112*128
conv2_2 = conv_op(conv2_1, name="conv2_2", kh=3, kw=3, n_out=128, dh=1,
dw=1, p=p)
# input_op:112*112*128 输出尺寸56*56*128
pool2 = mpool_op(conv2_2, name="pool2", kh=2, kw=2, dh=2, dw=2)
下面是第3段卷积,包含3个卷积层和一个pooling层
# 第3段卷积的第一个卷积层 卷积核3*3,共256个卷积核(输出通道数),步长1*1
# input_op:56*56*128 输出尺寸56*56*256
conv3_1 = conv_op(pool2, name="conv3_1", kh=3, kw=3, n_out=256, dh=1,
dw=1, p=p)
# input_op:56*56*256 输出尺寸56*56*256
conv3_2 = conv_op(conv3_1, name="conv3_2", kh=3, kw=3, n_out=256, dh=1,
dw=1, p=p)
# input_op:56*56*256 输出尺寸56*56*256
conv3_3 = conv_op(conv3_2, name="conv3_3", kh=3, kw=3, n_out=256, dh=1,
dw=1, p=p)
# input_op:56*56*256 输出尺寸28*28*256
pool3 = mpool_op(conv3_3, name="pool3", kh=2, kw=2, dh=2, dw=2)
下面是第4段卷积,包含3个卷积层和一个pooling层
# 第3段卷积的第一个卷积层 卷积核3*3,共512个卷积核(输出通道数),步长1*1
# input_op:28*28*256 输出尺寸28*28*512
conv4_1 = conv_op(pool3, name="conv4_1", kh=3, kw=3, n_out=512, dh=1,
dw=1, p=p)
# input_op:28*28*512 输出尺寸28*28*512
conv4_2 = conv_op(conv4_1, name="conv4_2", kh=3, kw=3, n_out=512, dh=1,
dw=1, p=p)
# input_op:28*28*512 输出尺寸28*28*512
conv4_3 = conv_op(conv4_2, name="conv4_3", kh=3, kw=3, n_out=512, dh=1,
dw=1, p=p)
# input_op:28*28*512 输出尺寸14*14*512
pool4 = mpool_op(conv4_3, name="pool4", kh=2, kw=2, dh=2, dw=2)
前面4段卷积发现,VGG16每段卷积都是把图像面积变为1/4,但是通道数翻倍, 因此图像tensor的总尺寸缩小一半。
下面是第5段卷积,包含3个卷积层和一个pooling层
# 第5段卷积的第一个卷积层 卷积核3*3,共512个卷积核(输出通道数),步长1*1
# input_op:14*14*512 输出尺寸14*14*512
conv5_1 = conv_op(pool4, name="conv5_1", kh=3, kw=3, n_out=512, dh=1,
dw=1, p=p)
# input_op:14*14*512 输出尺寸14*14*512
conv5_2 = conv_op(conv5_1, name="conv5_2", kh=3, kw=3, n_out=512, dh=1,
dw=1, p=p)
# input_op:14*14*512 输出尺寸14*14*512
conv5_3 = conv_op(conv5_2, name="conv5_3", kh=3, kw=3, n_out=512, dh=1,
dw=1, p=p)
# input_op:28*28*512 输出尺寸7*7*512
pool5 = mpool_op(conv5_3, name="pool5", kh=2, kw=2, dh=2, dw=2)
下面要经过全连接,需将第五段卷积网络的结果扁平化, reshape将每张图片变为77512=25088的一维向量
shp = pool5.get_shape()
flattened_shape = shp[1].value * shp[2].value * shp[3].value
# tf.reshape(tensor, shape, name=None) 将tensor变换为参数shape的形式。
resh1 = tf.reshape(pool5, [-1, flattened_shape], name="resh1")
第一个全连接层,是一个隐藏节点数为4096的全连接层,后面接一个dropout层,训练时保留率为0.5,预测时为1.0
fc6 = fc_op(resh1, name="fc6", n_out=4096, p=p)
fc6_drop = tf.nn.dropout(fc6, keep_prob, name="fc6_drop")
第2个全连接层,是一个隐藏节点数为4096的全连接层,后面接一个dropout层,训练时保留率为0.5,预测时为1.0
fc7 = fc_op(fc6_drop, name="fc7", n_out=4096, p=p)
fc7_drop = tf.nn.dropout(fc7, keep_prob, name="fc7_drop")
最后是一个1000个输出节点的全连接层,利用softmax输出分类概率,argmax输出概率最大的类别。
fc8 = fc_op(fc7_drop, name="fc8", n_out=1000, p=p)
softmax = tf.nn.softmax(fc8)
predictions = tf.argmax(softmax, 1)
return predictions, softmax, fc8, p
个人原创作品,转载需征求本人同意