下面是开始实现VGGNet-16。首先,我们载入几个系统库和Tensorflow。
from datetime import datetime
import math
import time
import tensorflow as tf
VGGNet-16包含很多层卷积,我们先写一个函数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_c
onv2d())
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
conv_op的输入,input_op是输入的tensor,name是这一层的名称,kh是kernel height 即卷积核的高,kw是kernel width即卷积的宽,n_out是卷积核数量即输出通道数,dh是步长的高,dw是步长的宽,p是参数列表。
下面使用get_shape()[-1].value获取输入input_op的通道数,比如图片尺寸224x224x3中最后的那个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,步长为dhxdw,padding模式设为SAME。biases使用tf.constant赋值为0,再使用tf.Variable将其转成可训练的参数。我们使用tf.nn.bias_add将卷积结果conv与bias相加,再使用tf.nn.relu对其进行非线性处理得到activation。最后将卷积层时用到的参数kernel和biases添加进参数列表,并将卷积层的输出activation作为函数结果返回。
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
上面的函数为全连接层的创建函数。一样是先获取输入input_op的通道数,然后使用tf.get_variable创建全连接层的参数,只不过参数的维度只有两个,第一个维度为输入的通道数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作为函数结果返回。
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,sw,1],padding='SAME',name=name)
上面是定义最大池化层的创建函数。输入即为input_op,池化尺寸为khxkw,步长为dhxdw,padding模式设为SAME。
def inference_op(input_op,keep_prob):
p=[]
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)
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,dw=2,dh=2)
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,dw=2,dh=2)
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,dw=2,dh=2)
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)
shp=pool5.get_shape()
flattened_shape=shp[1].value*shp[2].value*shp[3].value
resh1=tf.reshape(pool5,[-1,flattened_shape],name="resh1")
fc6=fc_op(resh1,name="fc6",n_out=4096,p=p)
fc6_drop=tf.nn.dropout(fc6,keep_prob,name="fc6_drop")
fc7=fc_op(fc6_drop,name="fc7",n_out=4096,p=p)
fc7_drop=tf.nn.dropout(fc7,keep_prob,name="fc7_drop")
fc8=fc_op(fc7_drop,name="fc8",n_out=1000,p=p)
softmax=tf.nn.softmax(fc8)
predictions=tf.argmax(softmax,1)
return predictions,softmax,fc8,p
上面是创建VGGNet-16的网络结构。VGGNet-16主要分为6个部分,前5段为卷积网络,最后一段是全连接网络。输入有input_op和keep_prob,这里keep_prob是控制dropout比率的一个placeholder。第一步先初始化参数列表p,然后创建第一段卷积网络。由两个卷积层和一个最大池化层构成。我们使用前面写好的函数conv_op,mpool_op来创建他们。这两个卷积层的卷积核的大小都是3x3,同时卷积核数量(输出通道数)均为64,步长为1x1,全像素扫描。第一个卷积层的输入input_op的尺寸为224x224x3,输出尺寸为224x224x64,而第二个卷积层的输入输出尺寸均为224x224x64.卷积层后的最大池化层则是一个标准的2x2最大池化,将结果尺寸变为112x112x64
第二段卷积网络和第一段类似,同样是两个卷积层加一个最大池化层,两个卷积层的卷积核尺寸也是3x3,但是输出通道数变为128,是以前的两倍。最大池化层则和前面保持一致,因此这一段卷积网络的输出尺寸变为56x56x128。
第三段卷积网络,这里有3个卷积层和一个最大池化层。3个卷积层的卷积核大小依然是3x3,但是输出通道数增长为256,而最大池化层保持不变,因此这一段卷机网络的输出尺寸是28x28x256。
第四段卷机网络也是3个卷积层加1个最大池化层。VGGNet-16的每一段卷积网络都会将图像的边长缩小一半,但是卷积输出通道数翻倍,这一层卷积输出通道数增加到512,但是通过最大之处将图片缩小为14x14。
最后一段卷积网络有所变化,这里卷积输出的通道数不在增加,继续维持在512.最后一段卷积网络同样是3个卷积层加一个最大池化层,卷积核尺寸为3x3,步长为1x1,池化层尺寸为2x2,步长为2x2.因此这里输出的尺寸变为7x7x512
将第五段的卷积网络的输出结果进行扁平化,使用tf.reshape函数将每个样本华为长度7x7x512=25088的一维向量。
然后连接一个隐含节点数为4096全连接层,激活函数为RELU.然后连接一个dropout层,在训练时节点保留率为0.5,预测时为1.0
再接下来是一个和前面一样的全连接层,之后同样连接一个dropout层。
最后连接一个有1000个输出节点的全连接层,并使用softmax进行处理得到分类输出概率。这里使用tf.argmax求输出概率最大的类别。最后将fc8,softmax,predictions和参数列表p一起返回,到此为止,VGGNet-16的网络结构就全部构建完成了。
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)
评测函数time_tensorflow_run()和前面AlexNet中的非常相似,只有一点区别:我们在session.run()方法中引入了feed_dict,方便后面传入keep_prob来控制Dropout层的保留比率。
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,目标依然是仅评测forward和backward的运算性能,并不进行实质的训练和预测。首先我们生成尺寸为224x224的随机图片,方法与AlexNet中一样,通过tf.random_normal函数生成标准差为0.1的正太分布的随机数。
接下来keep_prob的Placeholder,并调用inference_op函数构建VGGNet-16的网络结构,获得predictions,softmax,fc8和参数列表p。
然后创建Sessionbiang初始化全局参数.
通过将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
num_batches=100
run_benchmark()
设置batch_size为32,因为VGGNet-16的模型体积比较大,如果使用较大的batch_size,GPU显存会不够用。最后执行评测的主函数run_benchmark(),测试VGGNet-16在tensorflow上的forward和backward耗时。