源码地址https://github.com/mcttn1/DeepLearning
下面就开始实现VGGNet-16,也就是上面的版本D,其他版本读者可以仿照本节的代码自行修改并实现,难度不大。首先,我们载入几个系统库和Tensorflow。本节代码主要来自tensorflow-vgg的开源实现。https://github.com/machrisaa/tensorflow-vgg
from datetime import datetime
import math
import time
import tensorflow as tf
VGGNet-16包含很多层的卷积,因此我们先写一个函数conv_op,用来创建卷积层并把本层的参数存入参数列表。先来看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()做参数初始化。
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())
接着使用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添加进参数列表p,并将卷积层的输出activation作为函数结果返回。
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。一样是先获取输入input_op的通道数,然后使用tf.get_variable创建全连接层的参数,只不过参数的维度只有两个,第一个维度为输入的通道数n_in,第二个维度为输出的通道数。同样,参数初始化方法也使用xavier_initializer。这里biases不再初始化为0,而是赋予一个较小的值0.1以避免dead neuron。然后使用tf.nn.relu_layer对输入变量input_op与kernel做矩阵乘法并加上biases,再做ReLU非线性变换得到activation。最后将这个全连接层用到参数kernel、biases添加到参数列表p,并将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
再定义最大池化层的创建函数mpool_op。这里直接使用tf.nn.max_pool,输入即为input_op,池化尺寸为khxkw,步长是dhxdw,padding模式设为SAME。
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-16的网络结构。VGGNet-16主要分为6个部分,前5段为卷积网络,最后一段是全连接网络。我们定义创建VGGNet-16网络结构的函数inference_op,输入有input_op和keep_prob,这里的keep_prob是控制比率的一个placeholder。第一步先初始化参数列表p。然后创建第一段卷及网络,这一段如图1中的网络结构,由两个卷积层和一个最大池化层构成。我们使用前面写好的函数conv_op、mpool_op来创建他们。这两个卷积层的卷积核大小都是3x3,同时卷积核数量(输出通道数)均为64,步长为1x1,全像素扫描。第一个卷积层的输入input_opde的尺寸为224x224x3,输出尺寸为224x224x64;而第二个卷积层的输入输出尺寸均为224x224x64。卷积层后的最大池化层是一个标准的2x2的最大池化,将输出结果尺寸变为了112x112x64。
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,dh=2,dw=2)
第二段卷及网络和第一段非常类似,同样是两个卷积层加一个最大池化层,两个卷积层的卷积核尺寸也是3x3,步长1x1,但是输出通道数变为128,是以前的两倍。最大池化层和前面保持一致,因此这一段卷及网络的输出尺寸变为56x56x128。
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个最大池化层。3个卷积层的卷积核大小依然是3x3, 步长为1x1,但是输出通道数增长为256,而最大池化层保持不变,因此这一段卷积网络的输出尺寸是28x28x256。
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_2",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)
第四段卷积网络也是3个卷积层加1个最大池化层。读者可能已经发现规律了,到目前为止,VGGNet-16的每一段卷积都会将图像缩小一半,但是卷积通道数翻倍。这样图像面积缩小到1/4,输出通道数变为2倍,因此输出tensor的总尺寸每次缩小一半。这一层就是将卷积输出通道数增加到512,但是通过最大池化将图片缩小为14x14。
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=1,dh=1,dw=1,p=p)
conv4_3=conv_op(conv4_2,name="conv4_3",kh=3,kw=3,n_out=512=1,dh=1,dw=1,p=p)
pool4=mpool_op(conv4_3,name="pool4",kh=2,kw=2,dh=2,dw=2)
最后一段卷积网络有所变化,这里卷积输出的通道数不再增加,继续维持在512。最后一段卷积网络同样是3个卷积层加一个最大池化层,卷积核尺寸为3x3,步长为1x1,池化层尺寸为2x2,步长为2x2。因此到这里输出的尺寸变为7x7x512。
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=1,dh=1,dw=1,p=p)
conv5_3=conv_op(conv5_2,name="conv5_3",kh=3,kw=3,n_out=512=1,dh=1,dw=1,p=p)
pool5=mpool_op(conv5_3,name="pool5",kh=2,kw=2,dh=2,dw=2)
我们将第5段卷积网络的输出结果进行扁平化,使用tf.reshape函数将每个样本化为长度为7x7x512=25088的一维向量。
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的全连接层,激活函数为ReLU。然后连接一个Dropout层,在训练时节点保留率为0.5,预测时为1.0。
fc6=fc_op(resh1,name="fc6",n_out=4096,p=p)
fc6_drop=tf.nn.dropout(fc6,keep_prob,name="fc6_drop")#keep_prob是每个元素被保留的概率,那么 keep_prob=1就是所有元素全部保留的意思
接下来是一个和前面一样的全连接层,之后同样连接一个Dropout层。
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进行处理得到分类输出率。这里使用tf.argmax求输出概率最大的类别。最后将fc8、softmax、predictions和参数列表p一起返回。到此为止,VGGNet-16的网络结构就全部构建完成了。
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
测评函数time_tensorflow_run(),评估VGGNet16每轮计算时间,这个函数的第一个输入时Tensorflow的Session,第二个变量是要测评的运算算子,第四个变量是测试的名称。在session.run()方法中引入了feed_dict,方便后面传入keep_prob来控制Dropout层的保留比率。先定义预热轮数num_step_burn_in=10,它的作用是给程序热身,头几轮迭代有显存加载、cache命中等问题因此可以跳过,我们只考量10轮迭代之后的计算时间。同时,也记录总时间total_duration和平方和total_duration_squared用以计算方差。
def time_tensorflow_run(session,traget,feed,info_string):
num_step_burn_in=10
total_duration=0.0
total_duration_squared=0.0
我们进行num_batches+num_step_burn_in此迭代计算,使用time.time()记录时间,每次迭代通过session.run(target)执行。在初始热身的num_step_burn_in次迭代后,每10轮迭代显示当前迭代所需要的时间。同时每轮将total_duration和total_duration_squared累加,以便后面计算每轮耗时的均值和标准差。
for i in range(num_batches+num_step_burn_in):
start_time=time.time()
_=session.run(target)
duration=time.time()-start_time
if i>=num_step_burn_in:
if not i%10:#判断是否为None
print('%s:step %d,duration=%.3f'%(datetime.now(),i-num_steps_burn_in,duration))
total_duration+=duration
total_duration_squared+=duration*duration
在循环结束后,计算每轮迭代的平均耗时mn和标准差sd,最后将结果显示出来。这样就完成了计算每轮迭代耗时的评测函数time_tensorflow_run。
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))
接下来是主函数run_benchmark。首先使用with tf.Graph().as_default()定义默认的Graph方便后面使用。如前面所说,我们并不使用ImageNet数据集来训练,只使用随机图像数据测试前馈和反馈计算的耗时。我们使用tf.random_normal函数构造正态分布(标准差为0.1)的随机tensor,第一个维度是batch_size,即每轮迭代的样本数,第二个和第三个维度是图片的尺寸image_size=224,第四个维度是图片的颜色通道数。
def run_benchmark():
with tf.Graph().as_default():
image_size=224
images=tf.Variable(tf.randrom_normal([batch_size,image_size,image_size,3],
dtype=tf.float32,stddev=1e-1))
接下来创建keep_prob的placeholder,并调用Inference_op函数构建VGGNet-16的网络结构,获得predictions、softmax、fc8和参数 列表p
keep_prob=tf.placeholder(tf.float32)
predictions,softmax,fc8,p=inference_op(images,keep_prob)
然后创建Session并初始化全局参数
init=tf.global_variables_initializer()
sess=tf.Session()
sess.run(init)
我们通过将keep_prob设为1.0来执行预测,并使用time_tensorflow_run测评forward运算时间。再计算VGGNet-16最后的全连接层的输出fc8的l2loss,并使用tf.gradients求相对于这个loss的所有模型参数的梯度。最后使用time_tensorflow_run评测backward运算时间,这里target为求解梯度操作grad,keep_prob为0.5。
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")
设置batch_size为32,因为VGGNet-16的模型体积比较大,如果使用较大的batch_size,GPU显存会不够用。最后执行评测的主函数 run_benchmark(),测试VGGNet-16在tensorflow上forward和backward耗时。
batch_size=32
num_batches=100
run_benchmark()
forward计算时每个batch的平均耗时:
2019-01-08 15:39:14.617169: step 0,duration=43.854
2019-01-08 15:45:16.466930: step 10,duration=25.438
2019-01-08 15:51:34.463691: step 20,duration=49.127
2019-01-08 15:56:12.268783: step 30,duration=24.447
2019-01-08 16:01:05.908951: step 40,duration=30.076
2019-01-08 16:02:25.168161: step 50,duration=4.213
2019-01-08 16:03:24.630595: step 60,duration=4.032
2019-01-08 16:04:08.969369: step 70,duration=3.496
2019-01-08 16:04:48.592290: step 80,duration=6.589
2019-01-08 16:05:35.395297: step 90,duration=4.061
2019-01-08 16:06:14.046486:Forward across 100 steps, 16.633 +/- 14.782 sec /batch
backward求解梯度时,每个batch的平均耗时:
2019-01-08 16:10:01.807624: step 0,duration=19.103
2019-01-08 16:13:10.404442: step 10,duration=20.527
2019-01-08 16:16:05.957063: step 20,duration=17.272
2019-01-08 16:19:06.447500: step 30,duration=18.080
2019-01-08 16:22:06.283585: step 40,duration=20.063
2019-01-08 16:24:52.184556: step 50,duration=21.301
2019-01-08 16:27:24.695263: step 60,duration=14.913
2019-01-08 16:30:00.931430: step 70,duration=17.591
2019-01-08 16:33:01.062166: step 80,duration=14.908
2019-01-08 16:36:28.285703: step 90,duration=22.373
2019-01-08 16:40:23.472396:Forward-backward across 100 steps, 18.408 +/- 3.821 sec /batch