卷积神经网络之AlexNet
AlexNet网络结构剖析
2012年AlexNet在ImageNet大赛上一举夺冠,展现了深度CNN在图像任务上的惊人表现,掀起了CNN研究的热潮,深度学习自此引爆,在AlexNet之前,深度学习已经沉寂了很久。该网络是以论文的第一作者 Alex Krizhevsky 的名字命名的,另外两位合著者是 Ilya Sutskever 和 Geoffery Hinton。
该网络结构如下(为了讲清楚AlexNet结构,找了不同的示意图):
需要注意的是,论文发表时的GPU运算能力达不到该网络的训练需求,所以使用的是两个GPU并行,中间为了提高模型效果使用了不少交互相关的Trick。我们下面只关注网络结构,不关注两个GPU处理之间的交互。
下面对该结构做详细介绍,数据预处理首先将所有图像都处理为256×256×3的图像,随后AlexNet通过随机采样的方式从中选取一张227×227×3的图片作为输入(原论文AlexNet结构图示写的是224×224×3,AlexNet2是吴恩达老师讲义中的图示,经计算确实应该是227)。
注:以下卷积操作由于卷积核通道数和上一层次输出FeatureMap个数相同,故没有写进Kernel中,Kernel的最后一个参数是卷积核个数。
- 第一个卷积层Kernel=(11,11,96),Padding=0,Stride= 4,因此输出大小为,将输出经过Relu激活层后,进行Local Response Normalization(LRN局部响应归一化),之后经过一个3×3的Maxpooling,Stride=2,Padding=0,于是输出大小为;
- 第二个卷积层,Kernel=(5,5,256),Padding=2,Stride=1,于是输出为,接着Relu,LRN,之后经过一个3×3的Maxpooling,Stride=2,Padding=0,于是输出大小为;
- 第三个卷积层,Kernel=(3,3,384),Padding=1,Stride=1,输出为,接着经过Relu层;
- 第四个卷积层和第三层操作一样;
- 第五个卷积层,Kernel=(3,3,256),Padding=1,Stride=1,输出为,Relu,之后经过一个3×3的Maxpooling,Stride=2,Padding=0,于是输出大小为;-
- 全连接层1,这一层实际上是4096个(6,6,256)的Kernel,只是正好可以看做拉直后的全连接,接着依次经过Relu,Dropout;
- 全连接层2,4096,依次经过Relu,Dropout。
- 全连接层3,输出1000个结果,Softmax给出分类。
在原论文的双GPU并行计算结构下,卷积层 Conv2,Conv4,Conv5中的卷积核只和位于同一GPU的上一层的FeatureMap相连,于是需要训练的参数数量是:
卷积层的参数 = 卷积核的数量 * 卷积核 + 偏置
Conv1: 96个11×11×3的卷积核,96×11×11×3+96=34848
Conv2: 2组,每组128个5×5×48的卷积核,(128×5×5×48+128)×2=307456
Conv3: 384个3×3×256的卷积核,3×3×256×384+384=885120
Conv4: 2组,每组192个3×3×192的卷积核,(3×3×192×192+192)×2=663936
Conv5: 2组,每组128个3×3×192的卷积核,(3×3×192×128+128)×2=442624
FuulC6: 4096个6×6×256的卷积核,6×6×256×4096+4096=37752832
FuulC7: 4096∗4096+4096=16781312
output(FuulC8): 4096∗1000=4096000
而实际如果Conv2,Conv4,Conv5也合在一块计算的话,这3个层次中相应的参数数量会翻倍。
不过,从上面也可以看出,参数大多数集中在全连接层,在卷积层由于局部连接和权值共享,权值参数相对较少。
AlexNet的特点及其解释
AlexNet首次在CNN中成功应用了ReLU和Dropout等Trick,同时也使用了GPU进行运算加速。
- 激活函数ReLu:
在最初,Sigmoid和Tanh函数最常用的激活函数。在网络层数较少时,Sigmoid函数的特性能够很好地向后传递信息。但它有一个很大的问题就是梯度饱和(梯度消失)。
所谓梯度饱和就是当输入的数字很大(或很小)时,其导数值接近于0。这样在深层次网络结构中进行反向传播时,由于链式法则,很多个很小的sigmoid导数相乘,导致其结果趋于0,权值更新将非常缓慢。
而ReLU是一个分段线性函数:
相比于Sigmoid不仅运算更快,且导数是恒定值,一般不会发生严重的梯度饱和。另外Relu会使一部分神经元的输出为0,这样就形成了网络的稀疏性,减少了参数的相互依赖,从而一定程度抑制了过拟合。
- Dropout
对Dropout可以有以下几个层面的解释。
组合解释:每次的Dropout都相当于训练了一个子网络,最终结果是这些子网络的组合;
动机解释:Dropout的使用降低了参数之间的依赖性,减弱了神经元之间相互依赖协作的工作模式,从而提高了神经元的独立学习能力,增加了模型泛化能力;
数据解释:对于Dropout后的训练结果总能找到与之对应的样本,这相当于数据增强。
AlexNet逻辑的TensorFlow实现
由于是用个人的PC跑程序,相当于只有一个运算中心,所以不可能实现论文最原始的网络了,我们只实现AlexNet的核心逻辑。另外,后来的实验发现LRN会让前向传播和反向传播的速度降低,但最终对模型效果提升却不明显,所以只有AlexNet用LRN,其之后的模型都放弃了。这里实现的网络中虽然加入了LRN,但为了训练速度可以自行删除LRN过程:
# -*- coding:utf-8 -*-
import tensorflow as tf
import time
import math
from datetime import datetime
batch_size=32
num_batch=100
keep_prob=0.5
def print_architecture(t):
print(t.op.name," ",t.get_shape().as_list())
def inference(images):
'''构建网络'''
parameters=[] #储存参数
with tf.name_scope('conv1') as scope:
kernel=tf.Variable(tf.truncated_normal([11,11,3,96],
dtype=tf.float32,stddev=0.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")
bias=tf.nn.bias_add(conv,biases) # w*x+b
conv1=tf.nn.relu(bias,name=scope) # reLu
print_architecture(conv1)
parameters +=[kernel,biases]
#添加LRN层和max_pool层
lrn1=tf.nn.lrn(conv1,depth_radius=4,bias=1,alpha=0.001/9,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_architecture(pool1)
with tf.name_scope('conv2') as scope:
kernel = tf.Variable(tf.truncated_normal([5, 5, 96, 256],
dtype=tf.float32, stddev=0.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")
bias = tf.nn.bias_add(conv, biases) # w*x+b
conv2 = tf.nn.relu(bias, name=scope) # reLu
parameters += [kernel, biases]
# 添加LRN层和max_pool层
lrn2 = tf.nn.lrn(conv2, depth_radius=4, bias=1, alpha=0.001 / 9, beta=0.75, name="lrn1")
pool2 = tf.nn.max_pool(lrn2, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1],
padding="VALID", name="pool2")
print_architecture(pool2)
with tf.name_scope('conv3') as scope:
kernel = tf.Variable(tf.truncated_normal([3, 3, 256, 384],
dtype=tf.float32, stddev=0.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")
bias = tf.nn.bias_add(conv, biases) # w*x+b
conv3 = tf.nn.relu(bias, name=scope) # reLu
parameters += [kernel, biases]
print_architecture(conv3)
with tf.name_scope('conv4') as scope:
kernel = tf.Variable(tf.truncated_normal([3, 3, 384, 384],
dtype=tf.float32, stddev=0.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")
bias = tf.nn.bias_add(conv, biases) # w*x+b
conv4 = tf.nn.relu(bias, name=scope) # reLu
parameters += [kernel, biases]
print_architecture(conv4)
with tf.name_scope('conv5') as scope:
kernel = tf.Variable(tf.truncated_normal([3, 3, 384, 256],
dtype=tf.float32, stddev=0.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")
bias = tf.nn.bias_add(conv, biases) # w*x+b
conv5 = tf.nn.relu(bias, name=scope) # reLu
pool5 = tf.nn.max_pool(conv5, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1],
padding="VALID", name="pool5")
parameters += [kernel, biases]
print_architecture(pool5)
#全连接层6
with tf.name_scope('fc6') as scope:
kernel = tf.Variable(tf.truncated_normal([6*6*256,4096],
dtype=tf.float32, stddev=0.1), name="weights")
biases = tf.Variable(tf.constant(0.0, shape=[4096], dtype=tf.float32),
trainable=True, name="biases")
# 输入数据变换
flat = tf.reshape(pool5, [-1, 6*6*256] ) # 整形成m*n,列n为7*7*64
# 进行全连接操作
fc = tf.nn.relu(tf.matmul(flat, kernel) + biases,name='fc6')
# 防止过拟合 nn.dropout
fc6 = tf.nn.dropout(fc, keep_prob)
parameters += [kernel, biases]
print_architecture(fc6)
# 全连接层7
with tf.name_scope('fc7') as scope:
kernel = tf.Variable(tf.truncated_normal([4096, 4096],
dtype=tf.float32, stddev=0.1), name="weights")
biases = tf.Variable(tf.constant(0.0, shape=[4096], dtype=tf.float32),
trainable=True, name="biases")
# 进行全连接操作
fc = tf.nn.relu(tf.matmul(fc6, kernel) + biases, name='fc7')
# 防止过拟合 nn.dropout
fc7 = tf.nn.dropout(fc, keep_prob)
parameters += [kernel, biases]
print_architecture(fc7)
# 全连接层8
with tf.name_scope('fc8') as scope:
kernel = tf.Variable(tf.truncated_normal([4096, 1000],
dtype=tf.float32, stddev=0.1), name="weights")
biases = tf.Variable(tf.constant(0.0, shape=[1000], dtype=tf.float32),
trainable=True, name="biases")
# 进行全连接操作
fc8 = tf.nn.xw_plus_b(fc7, kernel, biases, name='fc8')
parameters += [kernel, biases]
print_architecture(fc8)
return fc8,parameters
def time_compute(session,target,info_string):
num_step_burn_in=10
total_duration=0.0 #总时间
total_duration_squared=0.0
for i in range(num_batch+num_step_burn_in):
start_time=time.time()
_ = session.run(target)
duration= time.time() -start_time
if i>= num_step_burn_in:
if i%10==0: #每迭代10次显示一次duration
print("%s: step %d,duration=%.5f "% (datetime.now(),i-num_step_burn_in,duration))
total_duration += duration
total_duration_squared += duration *duration
time_mean=total_duration /num_batch
time_variance=total_duration_squared / num_batch - time_mean*time_mean
time_stddev=math.sqrt(time_variance)
#迭代完成,输出
print("%s: %s across %d steps,%.3f +/- %.3f sec per batch "%
(datetime.now(),info_string,num_batch,time_mean,time_stddev))
def main():
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=0.1 ) )
fc8,parameters=inference(images)
init=tf.global_variables_initializer()
sess=tf.Session()
sess.run(init)
time_compute(sess,target=fc8,info_string="Forward")
obj=tf.nn.l2_loss(fc8)
grad=tf.gradients(obj,parameters)
time_compute(sess,grad,"Forward-backward")
if __name__=="__main__":
main()
原数据集太大,随机生成图片进行测试。