本文是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()
我的电脑是甚至连网络都不能编译。。。因此接下来我找一个预训练的vgg16微调。
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