个人博客:http://www.chenjianqu.com/
原文链接:http://www.chenjianqu.com/show-57.html
本文是CNN经典论文《VERY DEEP CONVOLUTIONAL NETWORKS FOR LARGE-SCALE IMAGE RECOGNITION》Karen Simonyan∗ & Andrew Zisserman,ICLR2015 的阅读笔记。
论文笔记
1.解决了什么
提高大规模图像分类的精度。
2.使用的方法
搭建更深的卷积神经网络:使用3x3卷积核,模型达到16-19层,16层的被称为VGG16,19层的被称为VGG19。
使用Single-Scale和Multi-Scale训练和评估模型。
3.实验结果
该模型获得ImageNet Challenge 2014的图像localization第一名,图像分类第二名。
4.待解决的问题
该模型还不够深,只达到19层便饱和了,而且没有探索卷积核宽度对网络性能的影响。同时网络参数过多,达到1.3亿参数以上。
VGGNet
VGG名字来源于Visual Geometry Group, Department of Engineering Science, University of Oxford。论文里对多种不同深度的网络进行了测试,分别称为为A-E网络,从11-19层,其中D和E被称为VGG16和VGG19。各网络结构如下:
来一个VGG16的立体图:
各个网络的宽度都小,刚开始为64,最后达到512通道。各网络的总参数如下:
最多的VGG19有1.4亿参数。。我的GTX960M肯定是跑不动了。A-E都使用通用的配置:
网络输入:224x224的RGB图像;
输入图像预处理:减去训练集的像素均值;
卷积核大小:使用3x3的卷积核,这是the smallest size to capture the notion of left/right, up/down,
center。使用1x1卷积核,对输入通道的进行线性变换。
卷积步长:1,卷积时padding。
最大池化层:size=2x2,stride=2
跟AlexNet一样,卷积层后接两层的全连接层和一个一千神经元的输出层。
所有的隐层都使用ReLU作为激活函数。
网络不包含(除了A_LRN网络)局部响应归一化层(LRN),因为发现LRN没卵用而且还费时费力。
VGGNet将AlexNet中的大卷积核都替换为小的卷积核,使用的卷积核size=3x3,stride=1。因为两个3x3的卷积叠加等价于一个5x5的卷积,3个3x3的卷积叠加等价于一个7x7的卷积叠加。下图可说明这点:
小卷积核替换大卷积核的优点:
本来只有一个非线性层,替换后增加到3个,这增加了网络的深度和非线性,有利于决策函数辨别。
减少了参数数量,本来有7x7=49,减少到3x3x3=27,这可以看做是对7x7卷积滤波器进行正则化,迫使他们分解为3x3滤波器。
网络C里面加入了1x1的卷积核,这是在不影响感受野的情况下增加决策函数的非线性的方法。输入通道和输出通道相同,因此是一个线性映射,激活函数的存在引入了非线性。
训练细节
权重初始化
权重的初始化很重要,初始化不好会导致学习的停滞。这里初始化的方法是:首先训练网络A,网络A足够浅以至于可以随机初始化权重。训练好A之后,其它更深的网络的前四层和后两个全连接层使用A的权重进行初始化,其它层的权重随机初始化,且随机初始化的参数为:高斯分布,均值为0,标准差为0.01,偏置权重为0。后来发现使用Xavier初始化跟预训练权重的效果一样好。
训练参数
跟AlexNet一样,使用带动量的梯度下降训练,动量系数为0.9,batch_size=128;使用L2正则化,惩罚系数为5x10^-4;在全连接层使用droopout,系数为0.5。学习率初始化为0.01,当验证准确率不再上升时,将学习率除以10。总共迭代370k,共74epochs。
AlexNet当时用了90epochs,VGGNet参数更多卻训练的更快,作者猜想原因是使用小卷积核,有隐性正则化,此外,某些层被预初始化了因此收敛更快。
数据增强
在AlexNet中,图片由原始图片缩放至2256x2265,再裁剪至224x224。令S是图片缩放至某长宽后的短边,缩放后可以从中裁剪出输入图片224x224,则S>=224。S不能太小,否则数据多样性不足,不能太大,否则只能包含原始图片的一小部分。
这里使用两种方法:
第一是单尺度训练。这里首先使用S=256预训练网络,接着降低学习率至0.001,再使用S=384训练网络。
第二是多尺度训练。每个训练图片被独立的随机缩放,S在[Smin,Smax]范围内,这里的Smin=256,Smax=512。原始训练集上每张图片中目标大小是不确定的,因此采用这一方法是有效的,其实这也可以看做是通过抖动缩放来增加训练集。出于速度考虑,先预训练S=384的单尺度模型,再微调多尺度模型。
测试细节
论文在测试时,将全连接层转换为卷积层。第一个全连接层转换为7x7的卷积层,最后两个全连接层转换为1x1的卷积层。示意图如下:
只是把权重的维度变换和拓展了。经过转换的网络就没有了全连接层,这样网络就可以接受任意尺寸的输入,而不是像之前之能输入固定大小的输入。
这样网络的输出是一个class score map,map的每个通道表示每个分类,map的分辨率是可变的,取决于输入图片的大小。为了获得输出的向量,需要对class score map进行spatially averaged。
代码实现
使用tensorflow.slim实现vgg16的代码如下:
import tensorflow as tf
from tensorflow.contrib.layers import xavier_initializer
slim = tf.contrib.slim
REGULARIZER=0.0005
def VGG16(inputs):
with slim.arg_scope([slim.conv2d],
stride=1,
kernel_size=3,
activation_fn=tf.nn.relu,
padding='SAME',
weights_initializer=xavier_initializer(),
weights_regularizer=slim.l2_regularizer(REGULARIZER),
biases_regularizer=slim.l2_regularizer(REGULARIZER),
biases_initializer=tf.zeros_initializer()):
net = slim.conv2d(inputs, num_outputs=64,scope='conv1')
net = slim.conv2d(net, num_outputs=64,scope='conv2')
net=slim.max_pool2d(net,[2,2],2,padding='SAME',scope='maxpooling1')
net = slim.conv2d(net, num_outputs=128,scope='conv3')
net = slim.conv2d(net, num_outputs=128,scope='conv4')
net=slim.max_pool2d(net,[2,2],2,padding='SAME',scope='maxpooling2')
net = slim.conv2d(net, num_outputs=256,scope='conv5')
net = slim.conv2d(net, num_outputs=256,scope='conv6')
net = slim.conv2d(net, num_outputs=256,scope='conv7')
net=slim.max_pool2d(net,[2,2],2,padding='SAME',scope='maxpooling3')
net = slim.conv2d(net, num_outputs=512,scope='conv8')
net = slim.conv2d(net, num_outputs=512,scope='conv9')
net = slim.conv2d(net, num_outputs=512,scope='conv10')
net=slim.max_pool2d(net,[2,2],2,padding='SAME',scope='maxpooling4')
net = slim.conv2d(net, num_outputs=512,scope='conv11')
net = slim.conv2d(net, num_outputs=512,scope='conv12')
net = slim.conv2d(net, num_outputs=512,scope='conv13')
net=slim.max_pool2d(net,[2,2],2,padding='SAME',scope='maxpooling5')
net=slim.flatten(net,scope='flatten')
with slim.arg_scope([slim.fully_connected],
activation_fn=tf.nn.relu,
weights_initializer=xavier_initializer(),
weights_regularizer=slim.l2_regularizer(REGULARIZER),
biases_initializer=tf.zeros_initializer(),
biases_regularizer=slim.l2_regularizer(REGULARIZER)
):
net=slim.fully_connected(net,num_outputs=4096,scope='fc1')
net = slim.dropout(net, 0.5, scope='dropout1')
net=slim.fully_connected(net,num_outputs=4096,scope='fc2')
net = slim.dropout(net, 0.5, scope='dropout2')
out=slim.fully_connected(net,num_outputs=1000,activation_fn=None,scope='out')
return out
定义训练参数
from tensorflow import name_scope as namespace
BATCH_SIZE=128
DATA_LEN=50000
x = tf.placeholder(tf.float32, shape=[None, 224, 224, 3], name='input')
y_ = tf.placeholder(tf.float32, [None, 1000], name='labels')
global_step=tf.Variable(0,trainable=False)
y=VGG16(x)
with namespace('loss'):
#softmax并计算交叉熵
#print(y.get_shape().as_list() )
ce_loss = slim.losses.softmax_cross_entropy(y, y_) #交叉熵损失
regularization_loss = tf.add_n(slim.losses.get_regularization_losses())#正则损失
loss=ce_loss+regularization_loss
with namespace('train'):
#使用指数衰减学习率
learning_rate=tf.train.exponential_decay(
0.01,#初始学习率
global_step,
DATA_LEN/BATCH_SIZE,#多少次更新一次学习率
0.99,#学习率衰减率
staircase=True#学习率阶梯下降
)
train_step=tf.train.MomentumOptimizer(learning_rate,0.9,#动量系数
).minimize(loss,global_step=global_step)
with namespace('acc'):
correct_prediction=tf.equal(tf.argmax(y,1),tf.argmax(y_,1))
accuracy=tf.reduce_mean(tf.cast(correct_prediction,tf.float32))
tf.summary.scalar('loss',loss)
tf.summary.scalar('accuracy',accuracy)
merged=tf.summary.merge_all();
使用Keras数据生成器进行数据增强,这部分没有遵照原文。训练代码如下:
#定义数据生成器
from keras.preprocessing import image
train_dir=r'F:\BaiduNetdiskDownload\mini-imagenet\images_normal'
steps=100000
train_gen=image.ImageDataGenerator(
featurewise_center=True,#输入数据数据减去数据集均值
width_shift_range=0.2,#水平平移
height_shift_range=0.2,#垂直平移
horizontal_flip=True,#水平翻转
zoom_range=[0.5, 1.5],#缩放范围
brightness_range=[-0.1,0.1] #亮度变化范围
)
tg=train_gen.flow_from_directory(
train_dir,
target_size=(224,224),
batch_size=128,
class_mode='categorical'
)
with tf.Session() as sess:
init_op=tf.global_variables_initializer()
sess.run(init_op)
writer=tf.summary.FileWriter('D:/Jupyter/cv/VGGNet_log',sess.graph)
saver=tf.train.Saver()
for i in range(steps):
next_data,next_label=next(tg)
summary,_,loss_value,step=sess.run([merged,train_step,loss,global_step],
feed_dict={x:next_data,y_:next_label})
writer.add_summary(summary,step)
print('step%d loss:%f'%(step,loss_value))
writer.close()
Fine-tuning
1.数据集准备
使用和AlexNet原理和实现一样的数据集,下载下来并按文件夹分好类,如下:
上面用作训练集,需要从每个分类里面移一些用于验证的数据出来,代码如下:
import os
import shutil
import tqdm
basedir=r'F:\BaiduNetdiskDownload\mini-imagenet\images_normal'
newdir=r'F:\BaiduNetdiskDownload\mini-imagenet\image_normal_test'
NUM=100
for clsdir in tqdm.tqdm(os.listdir(basedir)):
#创建文件夹
newpath=os.path.join(newdir,clsdir)
if(os.path.exists(newpath)==False):
os.makedirs(newpath)
oldpath=os.path.join(basedir,clsdir)
for fileName in (os.listdir(oldpath))[:NUM]:
fileOldPath=os.path.join(oldpath,fileName)
fileNewPath=os.path.join(newpath,fileName)
shutil.move(fileOldPath,fileNewPath)
2.加载模型,并在bottleneck加上自定义的全连接层。
from keras.applications.vgg16 import VGG16
from keras.layers import *
# bulid network
inputs = Input(shape=[224, 224, 3])
base_model = VGG16(include_top=False, weights='imagenet', input_tensor=inputs)
from keras.models import Model, load_model
from keras.utils import plot_model
#首先冻结预训练模型的参数
for layer in base_model.layers:
layer.trainable=False
#搭建自己的全连接层
flatten=Flatten()(base_model.output)
fc1=Dense(512,activation='relu')(flatten)
dropout1=Dropout(rate=0.5)(fc1)
fc2=Dense(512,activation='relu')(dropout1)
dropout2=Dropout(rate=0.5)(fc2)
fc3=Dense(100,activation='softmax')(dropout2)
model=Model(inputs=inputs,outputs=fc3)
model.summary()
plot_model(model,to_file='VGG16.png',show_shapes=True)
3.定义数据增强器,并训练全连接层
from keras.preprocessing import image
from keras import initializers
from keras import optimizers
BATCH_SIZE=32
EPOCHS=20
#定义训练集生成器
train_gen=image.ImageDataGenerator(
featurewise_center=True,#输入数据数据减去数据集均值
width_shift_range=0.2,#水平平移
height_shift_range=0.2,#垂直平移
horizontal_flip=True,#水平翻转
brightness_range=[-0.1,0.1],#亮度变化范围
zoom_range=[0.5,1.5] #缩放的比例范围
)
train_dir=r'F:\BaiduNetdiskDownload\mini-imagenet\images_normal'
x=train_gen.flow_from_directory(
train_dir,
target_size=(224,224),
batch_size=BATCH_SIZE,
class_mode='categorical'
)
#定义验证集生成器
val_datagen = image.ImageDataGenerator()
val_dir=r'F:\BaiduNetdiskDownload\mini-imagenet\image_normal_test'
validation_generator = val_datagen.flow_from_directory(
val_dir,
target_size=(224, 224),
batch_size=BATCH_SIZE,
class_mode='categorical'
)
#编译模型
model.compile(loss='categorical_crossentropy',
optimizer=optimizers.SGD(lr=1e-3,momentum=0.9,decay=0.005),
metrics=['acc']
)
#训练模型
history=model.fit_generator(
x,
steps_per_epoch=int(50000/BATCH_SIZE),#每回合的步数
epochs=EPOCHS,
validation_data=validation_generator,
validation_steps=int(10000/BATCH_SIZE),
shuffle=True,
)
#保存模型
model.save(filepath='D:/Jupyter/cv/VGGNet_FT_log/vgg16.h5')
4.微调所有层
BATCH_SIZE=64
EPOCHS=20
#解冻网络
for layer in base_model.layers:
layer.trainable=True
#编译模型
model.compile(loss='categorical_crossentropy',
optimizer=optimizers.SGD(lr=1e-5,momentum=0.9,decay=0.005),
metrics=['acc']
)
#训练模型
history=model.fit_generator(
x,
steps_per_epoch=int(50000/BATCH_SIZE),#每回合的步数
epochs=EPOCHS,
validation_data=validation_generator,
validation_steps=int(10000/BATCH_SIZE),
shuffle=True,
)
#保存模型
model.save(filepath='D:/Jupyter/cv/VGGNet_FT_log/vgg16.h5')
参考文献
[1] Karen Simonyan∗ & Andrew Zisserman.VERY DEEP CONVOLUTIONAL NETWORKS FOR LARGE-SCALE IMAGE RECOGNITION》.ICLR2015
[2]露秋.VGG 论文阅读记录.https://zhuanlan.zhihu.com/p/42233779?utm_source=qq&utm_medium=social&utm_oi=556883753528516608.2018-08-19