卷积神经网络(2)
定义LeNet-5模型的前向传播过程:
介绍LeNet-5模型的每一层结构:
1、卷积层
这一层的输入就是原始的图像像素,LeNet-5模型接受的输入层大小为32x32x1,第一个卷积层过滤器的尺寸为5x5,深度为6,不使用全0填充,步长为1。因为没有使用全0填充,所以这一层的输出尺寸为32-5+1=28,深度为6。这一个卷积层总共有5x5x1x6+6=156个参数,其中6个为偏置项参数。因为下一层的节点矩阵有28x28x6=4704个节点,每个节点和5x5=25个当前层节点相连,所以本成卷积层总共有4704x(25+1)=122304个连接。
2、池化层
这一层的输入为第一层的输出,是一个28x28x6的节点矩阵。本成采用的过滤器大小为2x2,长和宽的步长均为2,所以本层的输出矩阵大小为14x14x6。
3、卷积层
本层的输入矩阵大小为14x14x6,使用的过滤器大小为5x5,深度为16。本层不使用全0填充,步长为1。本层的输出矩阵大小为10x10x16。按照标准的卷积层,本层应该有5x5x6x16+16=2416个参数,10x10x16x(25+1)=41600个连接。
4、池化层
本层的输入矩阵大小为10x10x16,采用的过滤器大小为2x2,步长为2。本层的输出矩阵大小为5x5x16。
5、全连接层
本层的输入矩阵大小为5x5x16,在LeNet-5模型的论文中将这一层称为卷积层,但是因为过滤器的大小就是5x5,所以和全连接层没区别,在之后的tensorflow程序实现中也会将这一层看成全连接层。如果将5x5x16矩阵中的节点拉成一个向量,那么这一层和在上一部分中介绍的全连接层输入就一样了。本层的输出节点个数为120,总共有5x5x16x120+120=48120个参数。
6、全连接层
本层的输入节点个数为120个,输出节点个数为84个,总共参数为120x84+84=10164个
7、全连接层
本层的输入节点个数为84个,输出节点个数为10个,总共参数为84x10+10=850个。以下部分则是LeNet-5传播过程的代码实现:
import tensorflow as tf input_node=784 output_node=10 image_size=28 num_channels=1 num_labels=10 conv1_deep=32 #第一层卷积层的尺寸和深度 conv1_size=5 conv2_deep=64 #第二层卷积层的尺寸和深度 conv2_size=5 fc_size=512 #全连接层的节点个数
regularizer=tf.contrib.layers.l2_regularizer(regularization_rate) #正则化项 def inference(input_tensor,train,regularizer): #定义神经网络的前向传播过程,train参数用于区分训练过程和测试过程 with tf.variable_scope('layer1_conv1'): conv1_weight=tf.get_variable('weight',[conv1_size,conv1_size,num_channels,conv1_deep],initializer=tf.truncated_normal_initializer(stddev=0.1)) conv1_biases=tf.get_variable('bias',[conv1_deep],initializer=tf.constant_initializer(0.0)) conv1=tf.nn.conv2d(input_tensor,conv1_weight,strides=[1,1,1,1],padding='SAME') relu1=tf.nn.relu(tf.nn.bias_add(conv1,conv1_biases)) with tf.name_scope('layer2_pool1'): pool1=tf.nn.max_pool(relu1,ksize=[1,2,2,1],strides=[1,2,2,1],padding='SAME') with tf.variable_scope('layer3_conv2'): #卷积层 conv2_weight=tf.get_variable('weight',[conv2_size,conv2_size,conv1_deep,conv2_deep],initializer=tf.truncated_normal_initializer(stddev=0.1)) conv2_biases=tf.get_variable('bias',[conv2_deep],initializer=tf.constant_initializer(0.0)) conv2=tf.nn.conv2d(pool1,conv2_weights,strides=[1,1,1,1],padding='SAME') relu2=tf.nn.relu(tf.nn.bias_add(conv2,conv2_biases)) with tf.name_scope('layer4_pool2'): #池化层 pool2=tf.nn.max_pool(relu2,ksize=[1,2,2,1],strides=[1,2,2,1],padding='SAME') pool_shape=pool2.get_shape().as_list() nodes=pool_shape[1]*pool_shape[2]*pool_shape[3] reshaped=tf.reshape(pool2,[pool_shape[0],nodes]) with tf.variable_scope('layer5-fc1'): #全连接层 fc1_weight=tf.get_variable('weight',[node,fc_size],initializer=tf.truncated_normal_initializer(stddev=0.1)) if regularizer!=None: tf.add_to_collection('losses',regularizer(fc1_weights)) fc1_biases=tf.get_variable('bias',[fc_size],initializer=tf.constant_initializer(0.1)) fc1=tf.nn.relu(tf.matmul(reshaped,fc1_weights)+fc1_biases) if train: fc1=tf.nn.dropout(fc1,0.5) with tf.variable_scope('layer6-fc2'): fc2_weights=tf.get_variable('weight',[fc_size,num_labels],initializer=tf.truncated_normal_initializer(stddev=0.1)) if regularizer!=None: tf.add_to_collection('losses',regularizer(fc2_weights)) fc2_biases=tf.get_variable('bias',[num_labels],initializer=tf.constant_initializer(0.1)) logit=tf.matmul(fc1,fc2_weights)+fc2_biases return logit
由于一种卷积神经网络架构不能解决所有问题,比如LeNet-5就无法很好处理类似ImageNet这样比较大的图像数据集,那么我们通过如下表达式公式总结了一些经典的用于图片分类问题的卷积神经网络架构:
输入层→(卷积层+→池化层?)+→全连接层+
在如上公式中,卷积层+表示一层或多层卷积层,大部分卷积神经网络中一般最多连续使用三层卷积层。池化层?表示没有或者一层池化层,池化层虽然可以起到减少参数防止过拟合问题,但是在部分论文中也可以发现直接通过调整卷积层步长来完成,所以有些卷积神经网络中没有池化层。在多轮卷积层和池化层后,卷积神经网络在输出之前一般会经过1-2个全连接层。比如LeNet-5模型就可以表示为下面的结构。
输入层→卷积层→池化层→卷积层→池化层→全连接层→全连接层→输出层
除了LeNet-5模型,2012年ImageNet ILSVRC图像分类挑战的第一名AlexNet模型,2013年ILSVRC第一名ZF Net模型以及2014年第二名VGGNet模型的架构都满足上面介绍的正则表达式。
在过滤器的深度上,大部分卷积神经网络都采用逐层递增的方式,在有些模型中,每经过一次池化层之后,卷积层过滤器的深度会乘以2,虽然不同的模型会选择使用不同的具体数字,但是逐层递增是比较普遍的模式。卷积层的步长一般为1,但是在有些模型中也会使用2,或者3作为步长。池化层的配置相对简单,用的最多的是最大池化层。池化层的过滤器边长一般为2或3,步长也一般为2或者3.
使用tensorflow常用实现卷积层:
with tf.variable_scope(scope_name): weights=tf.get_varialbe('weight',....) biases=tf.get_variable('bias',...) conv=tf.nn.conv2d(...) relu=tf.nn.relu(tf.nn.bias_add(conv,biases))
#使用tensorflow-slim可以在如下一行中实现一个卷积层,下面有3个参数,第一个参数为输入节点矩阵,第二个参数是当前卷积层过滤器的深度,第三个参数是过滤器的尺寸。可选的参数包括过滤器的步长、是否使用全0填充、激活函数选择、变量空间命名等。
net=slim.conv2d(input,32,[3,3])
Inception-v3模型
上一部分介绍LeNet-5模型整理出了一类经典的卷积神经网络架构设计,在这里则介绍Inception结构及Inception-v3卷积神经网络模型。这是一种和LeNet-5结构完全不同的卷积神经网络结构。在LeNet-5模型中,不同卷积层通过串联的方式连接在一起,而Inception-v3模型中的Inception结构是将不同的卷积层通过并联的方式结合在一起,这里则初步介绍Inception结构。
在上一部分中提到一个卷积层可以使用边长为1、3或者5的过滤器,那么如何在这些边长中选择呢。Inception模块给出了一个方案,那就是同时使用所有不同尺寸的过滤器,然后将得到的矩阵拼接起来。下图给出了Inception模型的一个单元结构示意图。
。。。。。
从上图可以发现,Inception模块首先使用不同尺寸的过滤器处理输入矩阵,在上图中,最上方的矩阵使用了边长为1的过滤器的卷积层前向传播的结果。类似的,中间矩阵使用的过滤器边长为3,下方矩阵使用的过滤器边长为5。不同的矩阵代表了Inception模块中的一条计算路径,虽然过滤器大小不同,但若所有的过滤器都是用全0填充且步长为1,那么前向传播得到的结果长和宽都与输入矩阵一致。这样经过不同过滤器处理的结果可以拼接成一个更深的矩阵,如上图所示,可以将它们在深度这个维度上组合起来。
上图所示的Inception模块得到的结果矩阵的长和宽输入一样,深度为红黄蓝三个矩阵深度的和。
Inception-v3模型总共有46层,由11个Inception模块组成,共有96个卷积层,若将上面部分常用的卷积实现代码直接搬运过来,那么一个卷积层就需要5行代码,于是总共需要480行代码来实现所有的卷积层。这样使得代码的可读性非常差,为了更好实现类似Inception-v3模型这样的复杂卷积神经网络,则会通过使用tensorflow-slim工具来更加简洁的实现一个卷积层。
import tensorflow.contrib.slim as slim
with slim.arg_scope([slim.conv2d,slim.max_pool2d,slim.avg_pool2d],stride=1,padding='SAME',weights_initializer=tf.truncated_normal_initializer(stddev=0.1),weights_regularizer=slim.l2_regularizer(0.001)): #该函数用于设置默认参数,第一个参数是函数列表,也即这个参数中的函数将使用后面stride和padding的默认值,并不需要再重新设置 .... net=上一层的输出节点矩阵 with tf.variable_scope('mixed_7c'): #为Inception模块声明一个统一的变量命名空间 with tf.variable_scope('branch_0'): #为Inception模块中每一条路径声明一个命名空间 branch_0=slim.conv2d(net,320,[1,1],scope='conv2d_0a_1x') #实现一个过滤器边长为1,深度为320的卷积层 with tf.variable_scope('branch_1'): #Inception模块中的第二条路径,这也是一个Inception结构 branch_1=slim.conv2d(net,384,[1,1],scope='conv2d_01_1x') branch_1=tf.concat(3,[slim.conv2d(branch_1,384,[1,3],scope='conv2d_0b_1x3'),slim.conv2d(branch_1,384,[3,1],scope='conv2d_0c_3x1')])
#通过tf.concat函数将多个矩阵拼接起来,tf.concat函数的第一个参数指定了拼接的维度,这里的3代表了矩阵是在深度这个维度上进行拼接(长、宽、深)此处2层卷积层的输入都是branch_1而不是net with tf.variable_scope('branch_2'): #Inception模块中的第三条路径,这条计算路径上的结构本身也是一个Inception结构 branch_2=slim.conv2d(net,448,[1,1],scope='conv2d_0a_1x1') branch_2=slim.conv2d(branch_2,384,[3,3],scope='conv2d_0b_3x3') branch_2=tf.concat(3,[slim.conv2d(branch_2,384,[1,3],scope='conv2d_0c_1x3'),slim.conv2d(branch_2,384,[3,1],scope='conv2d_0b_3x1')])
#通过tf.concat函数将多个矩阵拼接起来 with tf.variable_scope('branch_3'): #Inception模块中的第四条路径 branch_3=slim.avg_pool2d(net,[3,3],scope='avgpool_0a_3x3') branch_3=slim.conv2d(branch_3,192,[1,1],scope='conv2d_0b_1x1') net=tf.concat(3,[branch_0,branch_1,branch_2,branch_3]) #当前Inception模块的最后输出是由上面四个计算结果拼接得到