VGGNet原理和实现

    本文是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。各网络结构如下:

AE.png

来一个VGG16的立体图:   

2.jpg

各个网络的宽度都小,刚开始为64,最后达到512通道。各网络的总参数如下:

1.png

    最多的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.jpg

小卷积核替换大卷积核的优点:

  1. 本来只有一个非线性层,替换后增加到3个,这增加了网络的深度和非线性,有利于决策函数辨别。

  2. 减少了参数数量,本来有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的卷积层。示意图如下:

4.jpg

    只是把权重的维度变换和拓展了。经过转换的网络就没有了全连接层,这样网络就可以接受任意尺寸的输入,而不是像之前之能输入固定大小的输入。

    这样网络的输出是一个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原理和实现一样的数据集,下载下来并按文件夹分好类,如下:

10.png

    上面用作训练集,需要从每个分类里面移一些用于验证的数据出来,代码如下:

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

首页 所有文章 机器人 计算机视觉 自然语言处理 机器学习 编程随笔 关于