\quad 首先介绍一下VGGNet,VGGNet是牛津大学计算机视觉组和Google DeepMind公司的研究员一起研发的深度卷积神经网络。VGGNet探索了卷积神经网络的深度与其性能之间的关系,通过反复堆叠3 × \times × 3的小卷积核和2 × \times × 2的最大池化层,VGGNet成功的构筑了 16-19层的卷积神经网络。VGGNet相比于之前的state-of-the-art的网络结构,错误率大幅下降,并取得ILSVRC2014比赛分类项目的第2名和定位项目的第一名。同时VGGNet的扩展性很强,迁移到其他图片数据上的泛化性能非常好。VGGNet的结构非常简洁,整个网络都使用了同样大小的卷积核尺寸(3 × \times × 3)和最大池化尺寸(2 × \times × 2)。到目前为止,VGGNet仍然被使用来提取图像特征。VGGNet训练后的模型参数在其官方网站上开源了,可以用来在domain specific的图像分类任务上进行再训练(相当于提供了非常好的初始化权重,因此被用在许多地方)。
\quad VGGNet拥有5段卷积,每一段内有2-3个卷积层,同时每段尾部会连接一个最大池化层用来缩小图片尺寸。每段内的卷积核数量完全一样,越靠后的段的卷积核数量越多:64-128-256-512-512。其中经常出现多个完全一样的3 × \times × 3的卷积层堆叠在一起的情况,这其实是非常有用的设计。如图6-8所示,两个3 × \times × 3的卷积层串联相当于1个5 × \times × 5的卷积层,即一个像素会跟周围的5 × \times × 5个像素产生关联,可以说感受野大小为5 × \times × 5。而3个3 × \times × 3的卷积层串联的效果则想、相当于1个7 × \times × 7的卷积层。除此之外,3个串联的3 × \times × 3的卷积层,拥有比1个7 × \times × 7的卷积层更少的参数量,只有后者的 3 × 3 × 3 7 × 7 = 0.55 \frac{3 \times 3 \times 3}{7 \times 7}=0.55 7×73×3×3=0.55。最重要的是,3个3 × \times × 3的卷积层拥有比1个7 × \times × 7的卷积层更多的非线性变换,使得CNN对特征的学习能力更强。

  1. 载入几个系统库和Tensorflow。
#coding=utf-8
#VGG16net.py
from datetime import datetime
import math
import time
import tensorflow as tf
  1. VGGNet-16存在很多层卷积,因此我们先写一个函数conv_op,用来创建卷积层并把本层的参数存入参数列表。先来看conv_op函数的输入,input_op是输入的tensor,name是这一层的名称,kh是kernel height即卷积核的高,kw即kernel width是卷积核的宽,p是参数列表。下面使用get_shape()[-1].value获取输入input_op的通道数,比如输入图片的尺寸224 × \times × 224 × \times × 224 × \times × 3中最后那个3。然后使用tf.name_scope(name)设置scope。我们的kernel(卷积核参数)使用tf.get_variable创建,其中shape就是[kh,kw,n_in,n_out]即[卷积核的高,卷积核的宽,输入通道数,输出通道数],同时使用tf.contrib.layers.xavier_initializer_conv2d()做参数初始化。接着使用tf.nn.conv2d对input_op进行卷积处理,卷积核即kernel,步长是dh × \times × dw,padding模式设为SAME。biases使用tf.constant赋值为0,再使用tf.Variable将其转化为可训练的参数。我们使用tf.nn.bias_add将卷积结果conv和bias相加,再使用tf.nn.relu对其进行非线性处理得到activation。最后将创建卷积层时用到的参数kernel和biases添加进参数列表p,并将卷积层的输出activation作为函数结果返回。
#定义一个创建卷积层的函数conv_op
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
  1. 下面定义全连接层的创建函数fc_op。一样是先获取输入input_op的通道数,然后使用tf.get_variable创建全连接层的参数,只不过参数的维度只有2个,第一个维度为输入的通道数n_in,第二个维度为输出的通道数n_out。同样,参数初始化方法也使用xavier_initializer。这里biases不再初始化为0,而是赋予一个较小的值0.1以避免dead neuron。然后使用tf.nn.relu_layer对输入变量input_op与kernel做矩阵乘法并加上biases,再做ReLU非线性变换得到activation。最后将这个全连接层用到参数kernel,biases添加到参数列表p中,并将activation作为函数输出结果返回。
#定义一个创建全连接层的函数fc_op
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')
        activation = tf.nn.relu_layer(input_op, kernel, biases, name=scope)
        p += [kernel, biases]
        return activation
  1. 再定义最大池化层的创建函数mpool_op。这里直接使用tf.nn.max_pool,输入即为Input_op,池化尺寸为kh × \times × kw,步长是 d h × d w dh \times dw dh×dw,padding模式为SAME。
#定义最大池化层的创建函数mpool_op
def mpool_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)
  1. 完成了卷积层,全连接层,和最大池化层的创建函数,接下来就开始创建VGGNet-16的网络结构。VGGNet-16主要分为6个部分,前5段为卷积网络,最后一段是全连接网路。我们定义创建VGGNet-16网络结构的函数inference_op,输入有input_op和keep_prob,这里的keep_prob是控制dropout比例的一个placeholder。第一步先初始化函数列表p。然后创建第一段卷积网络,这一段由2个卷积层和1个最大池化层组成。我们使用前面写好的conv_op,mpool_op来创建他们。这两个卷积层的卷积核大小都是3 × \times × 3,同时卷积核数量均为64,步长为1 × \times × 1,全像素扫描。第一个卷积层的输入input_op尺寸为224 × \times × 224 × \times × 3,输出尺寸为 224 × \times × 224 × \times × 64。卷积层后的最大池化层则是一个标准的2 × \times × 2的最大池化,将输出结果尺寸变为112 × \times × 112 × \times × 64。
#定义VGGnet的网络结构
def inference_op(input_op, keep_prob):
    p = []
    #第一段卷积网络
    #[1, 224, 224, 3]
    conv1_1 = conv_op(input_op, name="conv1_1", kh=3, kw=3, n_out=64, dh=1, dw=1, p=p)
    conv1_2 = conv_op(conv1_1, name="conv1_2", kh=3, kw=3, n_out=64, dh=1, dw=1, p=p)
    pool1 = mpool_op(conv1_2, name="pool1", kh=2, kw=2, dw=2, dh=2)
  1. 第二段卷积网络和第一段很类似,同样是两个卷积层加一个最大池化层,两个卷积层的卷积核尺寸也是3 × \times × 3,但是树池通道变为128,是以前的两倍。最大池化吃那个则和前面保持一致,因此这一段卷积网络的输出尺寸变为56 × \times × 56 × \times × 128。
#第2段卷积网络
    #[1, 112, 112, 64]
    conv2_1 = conv_op(pool1, name="conv2_1", kh=3, kw=3, n_out=128, dh=1, dw=1, p=p)
    conv2_2 = conv_op(conv2_1, name="conv2_2", kh=3, kw=3, n_out=128, dh=1, dw=1, p=p)
    pool2 = mpool_op(conv2_2, name="pool2", kh=2, kw=2, dh=2, dw=2)
  1. 接下来是第3段卷积网络,这里有3个卷积层和1个最大池化层。3个卷积层的卷积核大小依然是3 × \times × 3,但是输出通道增长为256,而最大池化层保持不变,因此这一段卷积网络的输出是28 × \times × 28 × \times × 256。
#第3段卷积网络
    #[1, 56, 56, 128]
    conv3_1 = conv_op(pool2, name="conv3_1", kh=3, kw=3, n_out=256, dh=1, dw=1, p=p)
    conv3_2 = conv_op(conv3_1, name="conv3_2", kh=3, kw=3, n_out=256, dh=1, dw=1, p=p)
    conv3_3 = conv_op(conv3_2, name="conv3_3", kh=3, kw=3, n_out=256, dh=1, dw=1, p=p)
    pool3 = mpool_op(conv3_3, name="pool3", kh=2, kw=2, dh=2, dw=2)
  1. 第四段卷积网络也是3个卷积层加一个最大池化层。
#第4段卷积网络
    #[1, 28, 28, 256]
    conv4_1 = conv_op(pool3, name="conv4_1", kh=3, kw=3, n_out=512, dh=1, dw=1, p=p)
    conv4_2 = conv_op(conv4_1, name="conv4_2", kh=3, kw=3, n_out=512, dh=1, dw=1, p=p)
    conv4_3 = conv_op(conv4_2, name="conv4_3", kh=3, kw=3, n_out=512, dh=1, dw=1, p=p)
    pool4 = mpool_op(conv4_3, name="pool4", kh=2, kw=2, dh=2, dw=2)
  1. 最后一段卷积网络有所变化,这里卷积输出的通道数不再增加,继续维持在512。最后一段卷积网络同样是3个卷积层加一个最大池化层,卷积核尺寸为3 × \times × 3,步长为1 × \times × 1,池化层尺寸为 2 × \times × 2 ,步长为2 × \times × 2.因此到这里输出的尺寸变为7 × \times × 7 × \times × 512,
#第5段卷积网络
    #[1, 14, 14, 512]
    conv5_1 = conv_op(pool4, name="conv5_1", kh=3, kw=3, n_out=512, dh=1, dw=1, p=p)
    conv5_2 = conv_op(conv5_1, name="conv5_2", kh=3, kw=3, n_out=512, dh=1, dw=1, p=p)
    conv5_3 = conv_op(conv5_2, name="conv5_3", kh=3, kw=3, n_out=512, dh=1, dw=1, p=p)
    pool5 = mpool_op(conv5_3, name="pool5", kh=2, kw=2, dw=2, dh=2)
  1. 我们将第5段卷积网络的输出结果进行扁平化,使用tf.reshape函数将每个样本化为长度为7 × \times × 7 × \times × 512 = 25088的一维向量。然后连接一个隐含节点数为4096的全连接层,激活函数为ReLU。然后再连接一个Dropout层,在训练时节点保留率为0.5,预测时为1.0。接下来是一个和前面一样的全连接层,之后同样接一个Dropout层。最后接一个有1000个输出节点的全连接层,并使用Softmax进行处理得到分类输出概率。这里使用tf.argmax求输出概率最大的类别。最后将fc8,softmax,predictions和参数列表p一起返回,至此完成了VGGNet-16的完整网络结构搭建。
#reshape
    #[1, 7, 7, 512]
    shp = pool5.get_shape()
    flattened_shape = shp[1].value * shp[2].value * shp[3].value
    resh1 = tf.reshape(pool5, [-1, flattened_shape], name="resh1")
    #连接一个隐层节点为4096的全连接层
    fc6 = fc_op(resh1, name="fc6", n_out=4096, p=p)
    fc6_drop = tf.nn.dropout(fc6, keep_prob, name="fc6_drop")
    #连接一个隐层节点为4096的全连接层
    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
    fc8 = fc_op(fc7_drop, name="fc8", n_out=1000, p=p)
    softmax = tf.nn.softmax(fc8)
    predictions = tf.arg_max(softmax, 1)
    return predictions, softmax, fc8, p
  1. 我们的评测函数time_tensorflow_run()和前面AlexNet中的类似,只有一点区别,在session.run()方法中引入了feed_dict,方便后面传入keep_prob来控制Dropout层的保留比例。
#评测函数
def time_tensorflow_run(session, target, feed, info_string):
    num_steps_burn_in = 10
    total_duration = 0.0
    total_duration_squared = 0.0
    for i in range(num_batches + num_steps_burn_in):
        start_time = time.time()
        _ = session.run(target, feed_dict=feed)
        duration = time.time() - start_time
        if i >= num_steps_burn_in:
            if not i % 10:
                print('%s: step %d, duration = %.3f'%(datetime.now(), i-num_steps_burn_in, duration))
            total_duration += duration
            total_duration_squared += duration * duration
    mn = total_duration / num_batches
    vr = total_duration_squared / num_batches - mn * mn
    sd = math.sqrt(vr)
    print('%s: %s across %d steps, %.3f +/- %.3f sec / batch' % (datetime.now(), info_string, num_batches, mn, sd))
  1. 下面定义评测的主函数run_benchmark。我们的目标仍然是仅评测forward和backward的运算性能,并不进行实质的训练和预测。首先是生成尺寸为224 × \times × 224的随机图片,方法与AlexNet中一样,通过tf.random_normal函数生成标准差为0.1的正太分布的随机数。接下来创建keep_prob的placeholder,并调用inference_op函数构建VGGNet-16的网络结构,获得predictions,softmax,fc8和参数列表p。然后创建Session并初始化全局参数。我们通过将keep_prob设为1.0来执行预测,并使用time_tensorflow_run来评测forward运算时间。再计算VGGNet-16最后的全连接层的输出fc8的l2 loss,并使用tf.gradients求相对于这个loss的所有模型参数的梯度。最后使用time_tensorflow_run评测backward运算时间,这里target为求解的梯度grad,keep_prob为0.5。我们设置batch_size为32,因为VGGNet-16的模型体积比较大,如果使用较大的batch_size,GPU显存会不够用,。最后执行评测的主函数run_benchmark(),测试VGGNet-16在tensorflow的forward和backward耗时。
def run_benchmark():
    with tf.Graph().as_default():
        image_size = 224
        images = tf.Variable(tf.random_normal([batch_size, image_size, image_size, 3], dtype=tf.float32, stddev=1e-1))
        keep_prob = tf.placeholder(tf.float32)
        predictions, softmax, fc8, p = inference_op(images, keep_prob)
        init = tf.global_variables_initializer()
        sess = tf.Session()
        sess.run(init)
        time_tensorflow_run(sess, predictions, {keep_prob:1.0}, "Forward")
        objective = tf.nn.l2_loss(fc8)
        grad = tf.gradients(objective, p)
        time_tensorflow_run(sess, grad, {keep_prob:0.5}, "Forward-backward")

run_benchmark()

算法实现的完整代码

#coding=utf-8
#VGG16net.py
from datetime import datetime
import math
import time
import tensorflow as tf

num_batches = 100
batch_size = 32

#定义一个创建卷积层的函数conv_op
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
#定义一个创建全连接层的函数fc_op
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')
        activation = tf.nn.relu_layer(input_op, kernel, biases, name=scope)
        p += [kernel, biases]
        return activation
#定义最大池化层的创建函数mpool_op
def mpool_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)
#定义VGGnet的网络结构
def inference_op(input_op, keep_prob):
    p = []
    #第一段卷积网络
    #[1, 224, 224, 3]
    conv1_1 = conv_op(input_op, name="conv1_1", kh=3, kw=3, n_out=64, dh=1, dw=1, p=p)
    conv1_2 = conv_op(conv1_1, name="conv1_2", kh=3, kw=3, n_out=64, dh=1, dw=1, p=p)
    pool1 = mpool_op(conv1_2, name="pool1", kh=2, kw=2, dw=2, dh=2)
    #第2段卷积网络
    #[1, 112, 112, 64]
    conv2_1 = conv_op(pool1, name="conv2_1", kh=3, kw=3, n_out=128, dh=1, dw=1, p=p)
    conv2_2 = conv_op(conv2_1, name="conv2_2", kh=3, kw=3, n_out=128, dh=1, dw=1, p=p)
    pool2 = mpool_op(conv2_2, name="pool2", kh=2, kw=2, dh=2, dw=2)
    #第3段卷积网络
    #[1, 56, 56, 128]
    conv3_1 = conv_op(pool2, name="conv3_1", kh=3, kw=3, n_out=256, dh=1, dw=1, p=p)
    conv3_2 = conv_op(conv3_1, name="conv3_2", kh=3, kw=3, n_out=256, dh=1, dw=1, p=p)
    conv3_3 = conv_op(conv3_2, name="conv3_3", kh=3, kw=3, n_out=256, dh=1, dw=1, p=p)
    pool3 = mpool_op(conv3_3, name="pool3", kh=2, kw=2, dh=2, dw=2)
    #第4段卷积网络
    #[1, 28, 28, 256]
    conv4_1 = conv_op(pool3, name="conv4_1", kh=3, kw=3, n_out=512, dh=1, dw=1, p=p)
    conv4_2 = conv_op(conv4_1, name="conv4_2", kh=3, kw=3, n_out=512, dh=1, dw=1, p=p)
    conv4_3 = conv_op(conv4_2, name="conv4_3", kh=3, kw=3, n_out=512, dh=1, dw=1, p=p)
    pool4 = mpool_op(conv4_3, name="pool4", kh=2, kw=2, dh=2, dw=2)
    #第5段卷积网络
    #[1, 14, 14, 512]
    conv5_1 = conv_op(pool4, name="conv5_1", kh=3, kw=3, n_out=512, dh=1, dw=1, p=p)
    conv5_2 = conv_op(conv5_1, name="conv5_2", kh=3, kw=3, n_out=512, dh=1, dw=1, p=p)
    conv5_3 = conv_op(conv5_2, name="conv5_3", kh=3, kw=3, n_out=512, dh=1, dw=1, p=p)
    pool5 = mpool_op(conv5_3, name="pool5", kh=2, kw=2, dw=2, dh=2)
    #reshape
    #[1, 7, 7, 512]
    shp = pool5.get_shape()
    flattened_shape = shp[1].value * shp[2].value * shp[3].value
    resh1 = tf.reshape(pool5, [-1, flattened_shape], name="resh1")
    #连接一个隐层节点为4096的全连接层
    fc6 = fc_op(resh1, name="fc6", n_out=4096, p=p)
    fc6_drop = tf.nn.dropout(fc6, keep_prob, name="fc6_drop")
    #连接一个隐层节点为4096的全连接层
    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
    fc8 = fc_op(fc7_drop, name="fc8", n_out=1000, p=p)
    softmax = tf.nn.softmax(fc8)
    predictions = tf.arg_max(softmax, 1)
    return predictions, softmax, fc8, p
#评测函数
def time_tensorflow_run(session, target, feed, info_string):
    num_steps_burn_in = 10
    total_duration = 0.0
    total_duration_squared = 0.0
    for i in range(num_batches + num_steps_burn_in):
        start_time = time.time()
        _ = session.run(target, feed_dict=feed)
        duration = time.time() - start_time
        if i >= num_steps_burn_in:
            if not i % 10:
                print('%s: step %d, duration = %.3f'%(datetime.now(), i-num_steps_burn_in, duration))
            total_duration += duration
            total_duration_squared += duration * duration
    mn = total_duration / num_batches
    vr = total_duration_squared / num_batches - mn * mn
    sd = math.sqrt(vr)
    print('%s: %s across %d steps, %.3f +/- %.3f sec / batch' % (datetime.now(), info_string, num_batches, mn, sd))

def run_benchmark():
    with tf.Graph().as_default():
        image_size = 224
        images = tf.Variable(tf.random_normal([batch_size, image_size, image_size, 3], dtype=tf.float32, stddev=1e-1))
        keep_prob = tf.placeholder(tf.float32)
        predictions, softmax, fc8, p = inference_op(images, keep_prob)
        init = tf.global_variables_initializer()
        sess = tf.Session()
        sess.run(init)
        time_tensorflow_run(sess, predictions, {keep_prob:1.0}, "Forward")
        objective = tf.nn.l2_loss(fc8)
        grad = tf.gradients(objective, p)
        time_tensorflow_run(sess, grad, {keep_prob:0.5}, "Forward-backward")

run_benchmark()