1. 引言
随着卷积神经网络CNN在图像任务上不断取得的进展,很多学者对CNN的各个因素进行了探讨和改进,以进一步提高CNN在计算机视觉中的任务,比如CNN的卷积核大小、步伐等,在本文,我们将介绍一个非常经典的CNN模型——VGGNet,这个模型是在2014年由Karen Simonyan等人提出来的,在论文中,作者主要研究的是在控制其他变量不变的情况下,逐渐增加CNN的层数,是否可以对CNN的效果带来显著影响。下面我们具体对该模型进行介绍,论文地址如下:
2. VGG模型介绍
2.1 VGG模型的结构
为了控制变量,VGG模型将每张输入图像的尺寸控制在
224
×
224
×
3
224 \times 224 \times 3
224×224×3的大小,对于图像的预处理,VGG对每张图像减去像素的均值,即中心化处理。在每一个卷积层,采用的卷积核大小都是
3
×
3
3 \times 3
3×3,步伐是1,在部分卷积层会加一层max-pooling层,max-pooling层的窗口大小设置的是
2
×
2
2 \times 2
2×2,步伐是2。另外,全连接层采用的是三层全连接层,前两层的隐藏单元数是4096,最后一层带有softmax,其隐藏单元数是1000。所有的隐藏层都带有RELU激活函数。
为了研究CNN层数对模型效果的影响,作者设置了不同深度的VGG模型进行对比,如下图A-E所示,其中模型A和模型A-LRN都是11层的网络,两个的区别是A-LRN加了一层Normalization层,但是作者发现加了该层后对结果并没有什么影响,反而提高了模型的复杂度,所以后面的模型中都不加Normalization层,模型B采用的是13层,模型C和模型D采用的是16层,两者的区别主要是模型C在模型B的基础上加了两层conv1层,而模型D在模型B的基础上加了两层conv3层,作者之所以这样操作是为了证明非线性变换对模型的效果确实有显著的提升,因为conv1只是一层线性变换。模型E采用的则是19层的结构。各个模型的通道数由最开始的64逐渐递增到512后保持不变,每经过一层max-pooling层通道数翻一倍。
虽然VGG增加了CNN的层数,但是其卷积核的尺寸都是
3
×
3
3 \times 3
3×3,因此,在模型的参数量方面要比那些浅层但是采用大尺寸卷积核的CNN网络少,并且在达到同样大小的感受野下,VGG虽然可能需要堆叠多层,但是由于每层都加入了RELU激活函数,因此,其模型的表达能力要比使用单层大尺寸卷积核的模型强。
在训练时,作者对图像的预处理除了中心化外,还做了一个裁剪处理,只不过在裁剪之前,作者对图像会先进行放缩,放缩到图像的最小边大于224的一个尺寸,然后再对图像进行随机裁剪。作者在放缩时选择了两种方法,一种是single_scale方法:即将图像放缩为两个尺寸,分别为256、384,然后先训练256的模型,然后将该模型的参数作为384模型的初始化再进行训练。另一种是jittered_scale方法:即选择一个放缩的范围,如[256,512],然后每张图像放缩时会从该范围随机选择一个放缩尺寸进行放缩,然后再裁剪到
224
×
224
224 \times 224
224×224大小。
在测试时,对于一张新的图像,同样需要进行放缩,作者同样采用了两种方法,一种是single_scale方法:即只设置一种尺度进行放缩,对于训练中的single_scale方法,直接设置测试的放缩尺度与训练时的放缩尺度相同,对于训练中的jittered_scale方法,则直接设置测试的放缩尺度为训练时放缩范围的平均值。另一种是multi_scale方法:即测试时设置多种放缩尺度,对于训练中的single_scale方法,设置三种放缩尺度分别为
Q
=
{
S
−
32
,
S
,
S
+
32
}
Q=\{S-32, S, S+32\}
Q={S−32,S,S+32},其中
S
S
S为训练时的放缩尺度,下同。对于训练中的jittered_scale方法,则设置三种放缩尺度为
Q
=
{
S
min
,
0.5
(
S
min
+
S
max
)
,
S
max
}
Q=\left\{S_{\min }, 0.5\left(S_{\min }+S_{\max }\right), S_{\max }\right\}
Q={Smin,0.5(Smin+Smax),Smax}。作者通过实验发现采用multi_scale方法可以有效提高模型的准确率。
在对图像进行放缩后,传入模型时还是需要进行裁剪,作者在实验时其实采用了三种方法,一种是dense方法:dense方法是不对图像进行裁剪,直接将图像传入网络,但是将三层全连接层都改为卷积层,第一层全连接层改为
7
×
7
7 \times 7
7×7的卷积层,后面两层全连接层改为
1
×
1
1 \times 1
1×1的卷积层,并且确保最后一层卷积层的通道数与类别数相等即可,然后对每一层的score_map直接计算平均即可作为类别输出。另一种方法是multi_crop方法:即对每个放缩尺度得到的图像,随机裁剪多张,比如50张,然后将裁剪后得到的图像集输入模型,最终将他们的概率分布进行平均作为预测结果。第三种方法是dense & multi_crop方法:即将前两种方法进行综合,作者发现这种方法效果最佳。
3. VGG模型的tensorflow实现
下面我们用tensorflow来实现VGG模型,模型的代码如下:
import os
import config
import random
import numpy as np
import tensorflow as tf
from config import vgg_config
from data_loader import DataLoader
from eval.evaluate import accuracy
class VGG(object):
def __init__(self,
height=config.height,
width=config.width,
channel=config.channel,
num_classes=config.num_classes,
batch_size=vgg_config.batch_size,
learning_rate=vgg_config.learning_rate,
learning_decay_rate=vgg_config.learning_decay_rate,
learning_decay_steps=vgg_config.learning_decay_steps,
epoch=vgg_config.epoch,
depth=vgg_config.depth,
dropout_keep_prob=vgg_config.dropout_keep_prob,
model_path=vgg_config.model_path,
summary_path=vgg_config.summary_path,
hidden_dim=vgg_config.hidden_dim):
"""
:param height:
:param width:
:param channel:
:param num_classes:
:param learning_rate:
:param learning_decay_rate:
:param learning_decay_steps:
:param epoch:
:param depth:
:param dropout_keep_prob:
:param train:
"""
self.height = height
self.width = width
self.channel = channel
self.num_classes = num_classes
self.batch_size = batch_size
self.learning_rate = learning_rate
self.learning_decay_rate = learning_decay_rate
self.learning_decay_steps = learning_decay_steps
self.epoch = epoch
self.depth = depth
self.dropout_keep_prob = dropout_keep_prob
self.model_path = model_path
self.summary_path = summary_path
self.hidden_dim = hidden_dim
# vgg block conv configre
self.block_layers_dict = {11: [1, 1, 2, 2, 2],
13: [2, 2, 2, 2, 2],
16: [2, 2, 3, 3, 3],
19: [2, 2, 4, 4, 4]}
self.block_filters_list = [64, 128, 256, 512, 512]
assert self.depth in self.block_layers_dict, 'depth of vgg should be in [11,13,16,19]'
self.block_layers = self.block_layers_dict[self.depth]
self.input_x = tf.placeholder(tf.float32, shape=[None, height, width, channel], name='input_x')
self.input_y = tf.placeholder(tf.float32, shape=[None, self.num_classes])
self.prediction = None
self.loss = None
self.global_step = None
self.optimize = None
self.data_loader = DataLoader()
self.model()
def model(self):
"""
vgg mdoel
:return:
"""
# 卷积层block
convs = self.block_conv(self.input_x)
# 将feature map展开
flatten = tf.reshape(convs, [-1, 7 * 7 * 512])
# fc1
with tf.name_scope('fc1'):
W1 = tf.truncated_normal_initializer(stddev=0.01)
fc1 = tf.layers.dense(inputs=flatten, units=self.hidden_dim, activation=tf.nn.relu, kernel_initializer=W1)
fc1 = tf.layers.dropout(fc1, rate=self.dropout_keep_prob)
# fc2
with tf.name_scope('fc2'):
W2 = tf.truncated_normal_initializer(stddev=0.01)
fc2 = tf.layers.dense(inputs=fc1, units=self.hidden_dim, activation=tf.nn.relu, kernel_initializer=W2)
fc2 = tf.layers.dropout(fc2, rate=self.dropout_keep_prob)
# fc3
with tf.name_scope('fc3'):
W3 = tf.truncated_normal_initializer(stddev=0.01)
fc3 = tf.layers.dense(inputs=fc2, units=self.num_classes, kernel_initializer=W3)
# 预测值
self.prediction = tf.argmax(fc3,axis=1)
# 计算准确率
self.acc = accuracy(fc3,self.input_y)
# 损失值
self.loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=fc3, labels=self.input_y))
# 全局步数
self.global_step = tf.train.get_or_create_global_step()
# 递减学习率
learning_rate = tf.train.exponential_decay(learning_rate=self.learning_rate,
global_step=self.global_step,
decay_rate=self.learning_decay_rate,
decay_steps=self.learning_decay_steps,
staircase=True)
self.optimize = tf.train.AdamOptimizer(learning_rate).minimize(self.loss)
def block_conv(self, inputs):
"""
convolution blocks
:param inputs:
:return:
"""
for block in range(5):
for layer in range(self.block_layers[block]):
with tf.variable_scope('conv_blocks_%s_layer_%s' % (block, layer)):
inputs = tf.layers.conv2d(inputs=inputs, filters=self.block_filters_list[block],
kernel_size=[3, 3], padding='same')
inputs = tf.layers.batch_normalization(inputs, momentum=0.997, epsilon=1e-5, center=True,
scale=True, training=True)
inputs = tf.nn.relu(inputs)
inputs = tf.layers.max_pooling2d(inputs=inputs, pool_size=[2, 2], strides=2,
padding='same', name='conv_blocks_%s_max_pool' % block)
return inputs
def fit(self, train_id_list, valid_img, valid_label):
"""
training model
:return:
"""
# 模型存储路径初始化
if not os.path.exists(self.model_path):
os.makedirs(self.model_path)
if not os.path.exists(self.summary_path):
os.makedirs(self.summary_path)
# train_steps初始化
train_steps = 0
best_valid_acc = 0.0
# summary初始化
tf.summary.scalar('loss', self.loss)
merged = tf.summary.merge_all()
# session初始化
sess = tf.Session()
writer = tf.summary.FileWriter(self.summary_path, sess.graph)
saver = tf.train.Saver(max_to_keep=10)
sess.run(tf.global_variables_initializer())
for epoch in range(self.epoch):
shuffle_id_list = random.sample(train_id_list.tolist(), len(train_id_list))
batch_num = int(np.ceil(len(shuffle_id_list) / self.batch_size))
train_id_batch = np.array_split(shuffle_id_list, batch_num)
for i in range(batch_num):
this_batch = train_id_batch[i]
batch_img, batch_label = self.data_loader.get_batch_data(this_batch)
train_steps += 1
feed_dict = {self.input_x: batch_img, self.input_y: batch_label}
_, train_loss,train_acc = sess.run([self.optimize, self.loss, self.acc], feed_dict=feed_dict)
if train_steps % 1 == 0:
val_loss,val_acc = sess.run([self.loss,self.acc], feed_dict={self.input_x: valid_img, self.input_y: valid_label})
msg = 'epoch:%s | steps:%s | train_loss:%.4f | val_loss:%.4f | train_acc:%.4f | val_acc:%.4f' % (
epoch, train_steps, train_loss,val_loss,train_acc,val_acc)
print(msg)
summary = sess.run(merged, feed_dict={self.input_x: valid_img, self.input_y: valid_label})
writer.add_summary(summary, global_step=train_steps)
if val_acc >= best_valid_acc:
best_valid_acc = val_acc
saver.save(sess, save_path=self.model_path, global_step=train_steps)
sess.close()
def predict(self, x):
"""
predicting
:param x:
:return:
"""
sess = tf.Session()
sess.run(tf.global_variables_initializer())
saver = tf.train.Saver(tf.global_variables())
ckpt = tf.train.get_checkpoint_state(self.model_path)
saver.restore(sess, ckpt.model_checkpoint_path)
prediction = sess.run(self.prediction, feed_dict={self.input_x: x})
prediction = np.bincount(prediction)
prediction = np.argmax(prediction)
return prediction
这里与作者论文中的主要不同点是我在每层卷积层都加了Batch Normalization层,因为笔者在实验中发现加了BN层反而效果更好。
4. 结论
以上就是对VGGNet的介绍,其实模型从现在来看的话没什么新奇,但是我觉得作者对图像的放缩、裁剪技巧反而是比较有意思的,最终大致总结一下模型的优缺点吧:
- 模型卷积核比较小,整体参数量少,复杂度低
- 模型的层度比较深,表达能力比浅层网络更强