本文内容来自《Tensorflow深度学习算法原理与编程实战》第八章
论文:《ImageNet Classification with Deep Convolutional Neural Networks》
背景介绍
第一个典型的CNN是LeNet5网络结构,但是第一个引起大家注意的网络却是AlexNet,也就是文章《ImageNet Classification with Deep Convolutional Neural Networks》介绍的网络结构。这篇文章的网络是在2012年的ImageNet竞赛中取得冠军的一个模型整理后发表的文章。作者是多伦多大学的Alex Krizhevsky等人。这篇文章在2012年发表,文章中的模型参加的竞赛是ImageNet LSVRC-2010,该ImageNet数据集有1.2 million幅高分辨率图像,总共有1000个类别。测试集分为top-1和top-5,并且分别拿到了37.5%和17%的error rates。这样的结果在当时已经超过了之前的工艺水平。AlexNet网络结构在整体上类似于LeNet,都是先卷积然后在全连接。但在细节上有很大不同。AlexNet更为复杂。AlexNet有60 million个参数和65000个 神经元,五层卷积,三层全连接网络,最终的输出层是1000通道的softmax。AlexNet利用了两块GPU进行计算,大大提高了运算效率,并且在ILSVRC-2012竞赛中获得了top-5测试的15.3%error rate, 获得第二名的方法error rate 是 26.2%,可以说差距是非常的大了,足以说明这个网络在当时给学术界和工业界带来的冲击之大。
网络结构介绍
LRN 局部响应归一化
在神经网络中,我们用激活函数将神经元的输出做一个非线性映射,但是tanh和sigmoid这些传统的激活函数的值域都是有范围的,但是ReLU激活函数得到的值域没有一个区间,所以要对ReLU得到的结果进行归一化。也就是Local Response Normalization。局部响应归一化的方法如下面的公式:
b x , y i = a x , y i / ( k + α ∑ j = m a x ( 0 , i − n / 2 ) m i n ( N − 1 , i + n / 2 ) ( a x , y j ) 2 ) β b^i_{x,y} = a_{x,y}^i / \big( k + \alpha \sum _{j = max(0, i-n / 2)} ^{min(N-1, i+n / 2)} (a_{x,y}^j)^2 \big)^\beta bx,yi=ax,yi/(k+αj=max(0,i−n/2)∑min(N−1,i+n/2)(ax,yj)2)β
a代表的是ReLU在第i个kernel的(x, y)位置的输出,n表示的是a的邻居个数,N表示该kernel的总数量。b表示的是LRN的结果。
关键是参数α , β , k 如何确定,论文中说在验证集中确定,最终确定的结果为:
k = 2 , n = 5 , α = 1 0 − 4 , β = 0.75
网络架构
首先这幅图分为上下两个部分的网络,论文中提到这两部分网络是分别对应两个GPU,只有到了特定的网络层后才需要两块GPU进行交互,这种设置完全是利用两块GPU来提高运算的效率,其实在网络结构上差异不是很大。为了更方便的理解,我们假设现在只有一块GPU或者我们用CPU进行运算,我们从这个稍微简化点的方向区分析这个网络结构。网络总共的层数为8层,5层卷积,3层全连接层。不过,在下文的代码实现中,将只在一块gpu上进行操作。
网络包含8个带权重的层;前5层是卷积层,剩下的3层是全连接层。最后一层全连接层的输出是1000维softmax的输入,softmax会产生1000类标签的分布网络包含8个带权重的层;前5层是卷积层,剩下的3层是全连接层。最后一层全连接层的输出是1000维softmax的输入,softmax会产生1000类标签的分布。
卷积层C1
该层的处理流程是: 卷积–>ReLU–>池化–>归一化。
卷积,输入是227×227,使用96个11×11×3的卷积核,得到的FeatureMap为55×55×96。
ReLU,将卷积层输出的FeatureMap输入到ReLU函数中。
池化,使用3×3步长为2的池化单元(重叠池化,步长小于池化单元的宽度),输出为27×27×96((55−3)/2+1=27)
局部响应归一化,使用k=2,n=5,α=10−4,β=0.75进行局部归一化,输出的仍然为27×27×96,输出分为两组,每组的大小为27×27×48
卷积层C2
该层的处理流程是:卷积–>ReLU–>池化–>归一化
卷积,输入是2组27×27×48。使用2组,每组128个尺寸为5×5×48的卷积核,并作了边缘填充padding=2,卷积的步长为1. 则输出的FeatureMap为2组,每组的大小为27×27 times128. ((27+2∗2−5)/1+1=27)
ReLU,将卷积层输出的FeatureMap输入到ReLU函数中
池化运算的尺寸为3×3,步长为2,池化后图像的尺寸为(27−3)/2+1=13,输出为13×13×256
局部响应归一化,使用k=2,n=5,α=10−4,β=0.75进行局部归一化,输出的仍然为13×13×256,输出分为2组,每组的大小为13×13×128
卷积层C3
该层的处理流程是: 卷积–>ReLU
卷积,输入是13×13×256,使用2组共384尺寸为3×3×256的卷积核,做了边缘填充padding=1,卷积的步长为1.则输出的FeatureMap为13×13 times384
ReLU,将卷积层输出的FeatureMap输入到ReLU函数中
卷积层C4
该层的处理流程是: 卷积–>ReLU
该层和C3类似。
卷积,输入是13×13×384,分为两组,每组为13×13×192.使用2组,每组192个尺寸为3×3×192的卷积核,做了边缘填充padding=1,卷积的步长为1.则输出的FeatureMap为13×13 times384,分为两组,每组为13×13×192
ReLU,将卷积层输出的FeatureMap输入到ReLU函数中
卷积层C5
该层处理流程为:卷积–>ReLU–>池化
卷积,输入为13×13×384,分为两组,每组为13×13×192。使用2组,每组为128尺寸为3×3×192的卷积核,做了边缘填充padding=1,卷积的步长为1.则输出的FeatureMap为13×13×256
ReLU,将卷积层输出的FeatureMap输入到ReLU函数中
池化,池化运算的尺寸为3×3,步长为2,池化后图像的尺寸为 (13−3)/2+1=6,即池化后的输出为6×6×256
全连接层FC6
该层的流程为:(卷积)全连接 -->ReLU -->Dropout
卷积->全连接: 输入为6×6×256,该层有4096个卷积核,每个卷积核的大小为6×6×256。由于卷积核的尺寸刚好与待处理特征图(输入)的尺寸相同,即卷积核中的每个系数只与特征图(输入)尺寸的一个像素值相乘,一一对应,因此,该层被称为全连接层。由于卷积核与特征图的尺寸相同,卷积运算后只有一个值,因此,卷积后的像素层尺寸为4096×1×1,即有4096个神经元。
ReLU,这4096个运算结果通过ReLU激活函数生成4096个值
Dropout,抑制过拟合,随机的断开某些神经元的连接或者是不激活某些神经元
全连接层FC7
流程为:全连接–>ReLU–>Dropout
全连接,输入为4096的向量
ReLU,这4096个运算结果通过ReLU激活函数生成4096个值
Dropout,抑制过拟合,随机的断开某些神经元的连接或者是不激活某些神经元
输出层
第七层输出的4096个数据与第八层的1000个神经元进行全连接,经过训练后输出1000个float型的值,这就是预测结果。
代码示例
import tensorflow as tf
import math
import time
from datetime import datetime
import time
time_start=time.time()
batch_size = 32
num_batches = 100
# 在函数inference_op()内定义前向传播的过程
def inference_op(images):
parameters = []
# 在命名空间conv1下实现第一个卷积层
with tf.name_scope("conv1"):
kernel = tf.Variable(tf.truncated_normal([11, 11, 3, 96], dtype=tf.float32,
stddev=1e-1), name="weights")
conv = tf.nn.conv2d(images, kernel, [1, 4, 4, 1], padding="SAME")
biases = tf.Variable(tf.constant(0.0, shape=[96], dtype=tf.float32),
trainable=True, name="biases")
conv1 = tf.nn.relu(tf.nn.bias_add(conv, biases))
# 打印第一个卷积层的网络结构
print(conv1.op.name, ' ', conv1.get_shape().as_list())
parameters += [kernel, biases]
# 添加一个LRN层和最大池化层
lrn1 = tf.nn.lrn(conv1, 4, bias=1.0, alpha=0.001 / 9.0, beta=0.75, name="lrn1")
pool1 = tf.nn.max_pool(lrn1, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1],
padding="VALID", name="pool1")
# 打印池化层网络结构
print(pool1.op.name, ' ', pool1.get_shape().as_list())
# 在命名空间conv2下实现第二个卷积层
with tf.name_scope("conv2"):
kernel = tf.Variable(tf.truncated_normal([5, 5, 96, 256], dtype=tf.float32,
stddev=1e-1), name="weights")
conv = tf.nn.conv2d(pool1, kernel, [1, 1, 1, 1], padding="SAME")
biases = tf.Variable(tf.constant(0.0, shape=[256], dtype=tf.float32),
trainable=True, name="biases")
conv2 = tf.nn.relu(tf.nn.bias_add(conv, biases))
parameters += [kernel, biases]
# 打印第二个卷积层的网络结构
print(conv2.op.name, ' ', conv2.get_shape().as_list())
# 添加一个LRN层和最大池化层
lrn2 = tf.nn.lrn(conv2, 4, bias=1.0, alpha=0.001 / 9.0, beta=0.75, name="lrn2")
pool2 = tf.nn.max_pool(lrn2, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1],
padding="VALID", name="pool2")
# 打印池化层的网络结构
print(pool2.op.name, ' ', pool2.get_shape().as_list())
# 在命名空间conv3下实现第三个卷积层
with tf.name_scope("conv3"):
kernel = tf.Variable(tf.truncated_normal([3, 3, 256, 384],
dtype=tf.float32, stddev=1e-1),
name="weights")
conv = tf.nn.conv2d(pool2, kernel, [1, 1, 1, 1], padding="SAME")
biases = tf.Variable(tf.constant(0.0, shape=[384], dtype=tf.float32),
trainable=True, name="biases")
conv3 = tf.nn.relu(tf.nn.bias_add(conv, biases))
parameters += [kernel, biases]
# 打印第三个卷积层的网络结构
print(conv3.op.name, ' ', conv3.get_shape().as_list())
# 在命名空间conv4下实现第四个卷积层
with tf.name_scope("conv4"):
kernel = tf.Variable(tf.truncated_normal([3, 3, 384, 384],
dtype=tf.float32, stddev=1e-1),
name="weights")
conv = tf.nn.conv2d(conv3, kernel, [1, 1, 1, 1], padding="SAME")
biases = tf.Variable(tf.constant(0.0, shape=[384], dtype=tf.float32),
trainable=True, name="biases")
conv4 = tf.nn.relu(tf.nn.bias_add(conv, biases))
parameters += [kernel, biases]
# 打印第四个卷积层的网络结构
print(conv4.op.name, ' ', conv4.get_shape().as_list())
# 在命名空间conv5下实现第五个卷积层
with tf.name_scope("conv5"):
kernel = tf.Variable(tf.truncated_normal([3, 3, 384, 256],
dtype=tf.float32, stddev=1e-1),
name="weights")
conv = tf.nn.conv2d(conv4, kernel, [1, 1, 1, 1], padding="SAME")
biases = tf.Variable(tf.constant(0.0, shape=[256], dtype=tf.float32),
trainable=True, name="biases")
conv5 = tf.nn.relu(tf.nn.bias_add(conv, biases))
parameters += [kernel, biases]
# 打印第五个卷积层的网络结构
print(conv5.op.name, ' ', conv5.get_shape().as_list())
# 添加一个最大池化层
pool5 = tf.nn.max_pool(conv5, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1],
padding="VALID", name="pool5")
# 打印最大池化层的网络结构
print(pool5.op.name, ' ', pool5.get_shape().as_list())
# 将pool5输出的矩阵汇总为向量的形式,为的是方便作为全连层的输入
pool_shape = pool5.get_shape().as_list()
nodes = pool_shape[1] * pool_shape[2] * pool_shape[3]
reshaped = tf.reshape(pool5, [pool_shape[0], nodes])
# 创建第一个全连接层
with tf.name_scope("fc_1"):
fc1_weights = tf.Variable(tf.truncated_normal([nodes, 4096], dtype=tf.float32,
stddev=1e-1), name="weights")
fc1_bias = tf.Variable(tf.constant(0.0, shape=[4096],
dtype=tf.float32), trainable=True, name="biases")
fc_1 = tf.nn.relu(tf.matmul(reshaped, fc1_weights) + fc1_bias)
parameters += [fc1_weights, fc1_bias]
# 打印第一个全连接层的网络结构信息
print(fc_1.op.name, ' ', fc_1.get_shape().as_list())
# 创建第二个全连接层
with tf.name_scope("fc_2"):
fc2_weights = tf.Variable(tf.truncated_normal([4096, 4096], dtype=tf.float32,
stddev=1e-1), name="weights")
fc2_bias = tf.Variable(tf.constant(0.0, shape=[4096],
dtype=tf.float32), trainable=True, name="biases")
fc_2 = tf.nn.relu(tf.matmul(fc_1, fc2_weights) + fc2_bias)
parameters += [fc2_weights, fc2_bias]
# 打印第二个全连接层的网络结构信息
print(fc_2.op.name, ' ', fc_2.get_shape().as_list())
# 返回全连接层处理的结果
return fc_2, parameters
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))
# 在计算图中定义前向传播模型的运行,并得到不包括全连部分的参数
# 这些参数用于之后的梯度计算
fc_2, parameters = inference_op(images)
init_op = tf.global_variables_initializer()
# 配置会话,gpu_options.allocator_type 用于设置GPU的分配策略,值为"BFC"表示
# 采用最佳适配合并算法
config = tf.ConfigProto()
config.gpu_options.allocator_type = "BFC"
with tf.Session(config=config) as sess:
sess.run(init_op)
num_steps_burn_in = 10
total_dura = 0.0
total_dura_squared = 0.0
back_total_dura = 0.0
back_total_dura_squared = 0.0
for i in range(num_batches + num_steps_burn_in):
start_time = time.time()
_ = sess.run(fc_2)
duration = time.time() - start_time
if i >= num_steps_burn_in:
if i % 10 == 0:
print('%s: step %d, duration = %.3f' %
(datetime.now(), i - num_steps_burn_in, duration))
total_dura += duration
total_dura_squared += duration * duration
average_time = total_dura / num_batches
# 打印前向传播的运算时间信息
print('%s: Forward across %d steps, %.3f +/- %.3f sec / batch' %
(datetime.now(), num_batches, average_time,
math.sqrt(total_dura_squared / num_batches - average_time * average_time)))
# 使用gradients()求相对于pool5的L2 loss的所有模型参数的梯度
# 函数原型gradients(ys,xs,grad_ys,name,colocate_gradients_with_ops,gate_gradients,
# aggregation_method=None)
# 一般情况下我们只需对参数ys、xs传递参数,他会计算ys相对于xs的偏导数,并将
# 结果作为一个长度为len(xs)的列表返回,其他参数在函数定义时都带有默认值,
# 比如grad_ys默认为None,name默认为gradients,colocate_gradients_with_ops默认
# 为False,gate_gradients默认为False
grad = tf.gradients(tf.nn.l2_loss(fc_2), parameters)
# 运行反向传播测试过程
for i in range(num_batches + num_steps_burn_in):
start_time = time.time()
_ = sess.run(grad)
duration = time.time() - start_time
if i >= num_steps_burn_in:
if i % 10 == 0:
print('%s: step %d, duration = %.3f' %
(datetime.now(), i - num_steps_burn_in, duration))
back_total_dura += duration
back_total_dura_squared += duration * duration
back_avg_t = back_total_dura / num_batches
# 打印反向传播的运算时间信息
print('%s: Forward-backward across %d steps, %.3f +/- %.3f sec / batch' %
(datetime.now(), num_batches, back_avg_t,
math.sqrt(back_total_dura_squared / num_batches - back_avg_t * back_avg_t)))
time_end = time.time()
print('time cost', time_end - time_start, 's')
time_start = time.time()
'''打印打内容
conv1/Relu [32, 56, 56, 96]
pool1 [32, 27, 27, 96]
conv2/Relu [32, 27, 27, 256]
pool2 [32, 13, 13, 256]
conv3/Relu [32, 13, 13, 384]
conv4/Relu [32, 13, 13, 384]
conv5/Relu [32, 13, 13, 256]
pool5 [32, 6, 6, 256]
fc_1/Relu [32, 4096]
fc_2/Relu [32, 4096]
2018-04-27 22:36:29.513579: step 0, duration = 0.069
2018-04-27 22:36:30.244733: step 10, duration = 0.070
2018-04-27 22:36:30.946855: step 20, duration = 0.069
2018-04-27 22:36:31.640846: step 30, duration = 0.069
2018-04-27 22:36:32.338336: step 40, duration = 0.070
2018-04-27 22:36:33.034304: step 50, duration = 0.069
2018-04-27 22:36:33.727489: step 60, duration = 0.069
2018-04-27 22:36:34.563139: step 70, duration = 0.080
2018-04-27 22:36:35.262315: step 80, duration = 0.073
2018-04-27 22:36:35.992172: step 90, duration = 0.075
2018-04-27 22:36:36.636055: Forward across 100 steps, 0.072 +/- 0.006 sec / batch
2018-04-27 22:39:24.976134: step 0, duration = 0.227
2018-04-27 22:39:27.256709: step 10, duration = 0.228
2018-04-27 22:39:29.541159: step 20, duration = 0.228
2018-04-27 22:39:31.820606: step 30, duration = 0.227
2018-04-27 22:39:34.101613: step 40, duration = 0.227
2018-04-27 22:39:36.382223: step 50, duration = 0.228
2018-04-27 22:39:38.662726: step 60, duration = 0.227
2018-04-27 22:39:40.943501: step 70, duration = 0.227
2018-04-27 22:39:43.225993: step 80, duration = 0.228
2018-04-27 22:39:45.511031: step 90, duration = 0.230
2018-04-27 22:39:47.659823: Forward-backward across 100 steps, 0.229 +/- 0.008 sec / batch
'''