LeNet -5 模型
LeNet-5 模型总共有 7 层,以数字识别为例,图展示了 LeNet-5 模型的架构
第一层,卷积层
这一层的输入就是原始的图像像素,LeNet-5 模型接受的输入层大小为 32*32*1 。第一个卷积层过滤器的尺寸为 5 * 5,深度为 6,步长为 1 。因为没有使用全 0 填充,所以这一层的输出尺寸为 32-5+1 = 28,深度为 6 。这一个卷积层共有 5 * 5 * 1 * 6 + 6 = 156 个参数,其中 6 个偏置项参数。
第二层,池化层
这一层的输入为第一层的输出,是一个 28 * 28 * 6 的节点矩阵。本层采用的过滤器大小为 2 * 2,长和宽的步长均为 2 ,所以本层的输出矩阵大小为 14 * 14 * 6 。
第三层,卷积层
本层的输入矩阵大小为 14 * 14 * 6,使用的过滤器大小为 5 * 5,深度为 16。本层不使用全 0 填充,步长为 1 。本层的输出局针大小为 10 * 10 * 16。有 5 * 5 * 6 * 16 + 16 =2416 个参数。
第四层,池化层
本层的输入矩阵大小为 10 * 10 * 16,采用的过滤器大小为 2 * 2,步长为 2。 本层的输出矩阵大小为 5 * 5 * 16。
第五层,全连接层
本层的输入矩阵大小为 5 * 5 * 16,在LeNet-5 论文中将这一层称之为卷积层。但是因为过滤器的大小就是 5 * 5,所以和全连接层没有区别。如果将 5 * 5 * 16 矩阵中的节点拉成一个向量,那就和全连接层的输入一样了。本层的输出节点个数为 120,总共有 5 * 5 * 16 * 120 + 120 = 48120 个参数。
第六层,全连接层
输入节点 120 个,输出节点 84 个。 参数 120 * 84 + 84 = 10164 个。
第七层,全连接层
输入节点 84 个,输出节点 10 个,参数 84 * 10 + 10 = 850 个。
下面给出TensorFlow 程序来实现一个类似 LeNet-5 模型的卷积神经网络来解决 mnist 数字识别问题。由于卷积神经网络的输入是一个三维矩阵,所以调整数据输入格式如下:
#调整输入数据 placeholder 的格式,输入为一个四维矩阵
x = tf.placeholder(tf.float32,[
BATCH_SIZE, #第一维表示一个 batch
mnist_inference.IMAGE_SIZE, #第二位和第三维表示图片的尺寸
mnist_inference.IMAGE_SIZE,
mnist_inference.NUM_CHANNELS] #第四维表示图片的深度,对于黑白图片深度为 1;对于 RGB 格式的图片,深度为 3
name = 'x-input')
#类似的将输入的训练数据格式调整为一个四维矩阵,并将这个调整后的数据传入 sess.run 过程
reshaped_xs = np.reshape(xs,(BATCH_SIZE,
mnist_inference.IMAGE_SIZE,
mnist_inference.IMAGE_SIZE,
mnist_inference.NUM_CHANNELS))
其中修改后的类似 LeNet-5 模型的前向传播过程 mnist_inference.py 代码如下:
#-*- coding:utf-8 -*-
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
#定义卷积神经网络的前向传播过程
#这个程序中将用到 dropout 方法,可以进一步提升模型可靠性并防止过拟合
#dropout 过程只有在训练时使用
def inference(input_tensor,train,regularizer):
#第一层卷积层
with tf.variable_scope('layer1-conv1'):
conv1_weights = 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))
#步长为 1,全 0 填充
conv1 = tf.nn.conv2d(input_tensor,conv1_weights,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_weights = 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_weights = tf.get_variable("weights",[nodes,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)
#如果是训练模式,加入 dropout
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
上述给出的卷积神经网络可以在 mnist 数据集上达到 99.4% 的正确率,相比全连接网络模型有很大的提升。然而一种卷积神经网络架构并不能解决所有的问题,比如 LeNet-5 模型就无法很好的处理比较大的图像数据集。那么如何设计卷积神经网络的架构呢?下面的正则表达式公式总结了一些经典的用于图片分类问题的卷积神经网络架构:
输入层 -> (卷积层+ -> 池化层? )+ -> 全连接层+
"卷积层+" 表示一层或多层卷积层,大部分卷积神经网络中一般最多连续使用三层卷积层。"池化层? " 表示没有或者一层池化层。池化层虽然可以达到减少参数的作用,但也可以直接通过调整卷积层步长来完成,所以有些卷积神经网络中没有池化层。
上述代码的架构为: 输入层->卷积层->池化层->卷积层->池化层->全连接层->全连接层->输出层
在过滤器深度上,大部分卷积神经网络都采用逐层递增的方式。卷积层的步长一般为1,但有些模型中也会使用 2 或者3 作为步长。池化层的配置相对简单,用得最多的是最大池化层,池化层的过滤器边长一般为 2 或者 3,步长也一般为 2 或者 3 。
Inception-v3 模型
Inception-v3 结构是一种和 LeNet-5 结构是完全不同的卷积神经网络结构。在 LeNet-5 模型中,不同卷积层通过串联的方式连接在一起,而 Inception-v3 模型中的 Inception 结构是将不同的卷积层通过并联的方式结合在一起,下图给出了 Inception 模块的一个单元结构示意图:
Inception 模块会首先使用不同尺寸的过滤器处理输入矩阵。在图中,最上方矩阵为使用了边长为 1 的过滤器的卷积层前向传播的结果。类似的,中间矩阵使用的过滤器边长为 3 ,下方矩阵使用的边长为 5 。不同的矩阵代表了 Inception 模块中的一条计算路径。虽然过滤器的大小不同,但如果所有的过滤器都使用全 0 填充并且步长为 1 ,那么前向传播得到的结果矩阵的长和宽都与输入矩阵一致。这样经过不同过滤器处理的结果矩阵可以拼接成一个更深的矩阵。如上图所示,可以将它们在深度这个维度上组合起来。
图中所示的 Inception 模块得到的结果矩阵的长和宽与输入一样,深度为红黄蓝三个矩阵深度的和。上图展示的是 Inception 模块的核心思想,真正的 Inception-v3 模型中的 Inception 模块要更加复杂且多样,下图给出了 Inception-v3 模型的架构图:
Inception-v3模型总共有 46 层,由 11 个 Inception 模块组成。图中方框框出来的就是其中一个 Inception 模块。在 Inception-v3 模型中有 96 个卷积层,如果用 LeNet-5 中卷积层实现的代码,那么一个卷积层就需要 5 行代码,96个就需要写 480 行代码来实现。这样使得代码的可读性非常差。为了更好的实现类似 Inception-v3 模型这样的复杂卷积神经网络,下面代码展示了 TensorFlow-Slim 工具来更加简洁的实现一个卷积层:
# 直接使用 TensorFlow 原始 API 实现卷积层
with tf.variable_scope(scope_name):
weights = tf.get_variable("weight",...)
biases = tf.get_variable("bias",...)
conv = tf.nn.conv2d(...)
relu = tf.nn.relu(tf.nn.bias_add(conv,biases))
#使用TensorFlow-Slim 实现卷积层,可以在一行代码中实现卷积层的前向传播算法
# slim.conv2d 函数有 3 个参数是必填的
# 第一个参数是输入节点矩阵,第二个参数是当前卷积层过滤器的深度,第三个参数是过滤器的尺寸
# 可选的参数有移动步长,是否使用全 0 填充,激活函数的选择以及变量的命名空间等
net = slim.conv2d(input,32,[3,3])
利用 slim.conv2d 函数实现 Imception-v3 模型中方框框出来的那个 Inception 模块(第11个 Inception)的代码示例如下:
#slim.arg_scope 函数可以用于设置默认的参数值。slim.arg_scope 函数的第一个参数是一个列表
#这个列表中的参数将会使用默认的参数取值,在调用 slim.conv2d(net,320,[1,1]) 时会自动加上 stride = 1 ,padding = 'SAME'
with slim.arg_scope([slim.conv2d,slim.max_pool2d,slim.avg_pool2d],
stride = 1,padding = 'SAME'):
...
#此处省略了 Inception-v3 中前面的网络结构而直接实现最后的 Inception 模块
net = 上一层的输出节点矩阵
#为这个 Inception 模块声明一个统一的变量命名空间
with tf.variable_scope('last_inception'):
#给 Inception 模块中每一条并联的路径声明一个命名空间
with tf.variable_scope('Branch_0'):
# 实现一个过滤器边长为 1,深度为 320 的卷积层
branch_0 = slim.conv2d(net,320,[1,1],scope = 'Conv2d_0a_lxl')
#实现第二条路径,本身也是一个 Inception 结构,最终深度为 384*2
with tf.variable_scope('Branch_1'):
branch_1 = slim.conv2d(net,384,[1,1],scope = 'Conv2d_0a_1x1')
#tf.concat 函数可以将多个矩阵拼接起来,第一个参数指定了拼接的维度,
#这里给出的 3 代表了矩阵是在深度这个维度上进行拼接
branch_1 = tf.concat(3,[
#如图所示,此处 2 层卷积层的输入都是 branch_1 而不是 net
slim.conv2d(branch_1,384,[1,3],scope = 'Conv2d_0b_1x3'),
slim.conv2d(branch_1,384,[3,1],scope = 'Conv2d_0b_3x1')])
#实现第三条路径,此处也是一个小的 Inception 模型
with tf.variable_scope('Branch_2'):
branch_2 = slim.conv2d(net,448,[1,1],scope = 'Conv2d_0b_3x3')
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_0c_3x1')])
#实现第四条路径,池化卷积层,输出层深度为 192
with tf.variable_scope('Branch_3'):
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')
#当前 Inception 模块的最后输出为以上 4 条路径在第三维(深度)上的合并
net = tf.concat(3,[branch_0,branch_1,branch_2,branch_3])
以上代码就是图中所示 Inception-v3 模型中的第 11 个 Inception 模块的实现。也可以不使用 TensorFlow-Slim 工具实现,只是代码会显得非常不好阅读,或者定义一个传统的卷积层的函数,类似替代 slim.conv2d 也行。主要是使用 tf.concat 进行了深度上的拼接,实现了图 6-16 中 Inception 模块的核心思想。