TensorFlow实战——卷积神经网络与图像识别

经典的图像识别数据集

在图像识别领域里,经典数据集包括MNIST数据集、Cifar数据集和ImageNet数据集。
在处理上述这些数据集时,全连接神经网络往往无法很好的处理图像数据。因为使用全连接神经网络处理图像的最大问题在于全连接层的参数太多。参数增多除了导致计算速度减慢,还很容易导致过拟合问题。

于是,卷积神经网络CNN应运而来。

卷积神经网络CNN

一个卷积神经网络主要由以下五种结构组成:
1. 输入层。输入层是整个神经网络的输入,在处理图像的卷积神经网络中,它一般代表了一张图片的像素矩阵。比如,三维矩阵就可以代表一张图片。其中三维矩阵的长和宽代表了图像的大小,而三维矩阵的深度代表了图像的色彩通道。比如黑白图片的深度为1,而在RGB色彩模式下,图像的深度为3.
2. 卷积层。卷积层中每一个节点的输入只是上一层神经网络的一小块,这个小块常用的大小有3 × 3或者5 × 5。卷积层试图将神经网络中的每一小块进行更加深入地分析从而得到抽象程度更高的特征。一般来说,通过卷积层处理过的节点矩阵会变得更深。
3. 池化层(Pooling)。池化层神经网络不会改变三维矩阵的深度,但是它可以缩小矩阵的大小。池化操作可以认为是将一张分辨率较高的图片转化为分辨率较低的图片。通过池化层,可以进一步缩小最后全连接层中节点的个数,从而达到减少整个神经网络中参数的目的。
4. 全连接层。在经过多轮卷积层和池化层的处理之后,在卷积神经网络的最后一般会是由1到2个全连接层来给出最后的分类结果。经过几轮卷积层和池化层的处理之后,可以认为图像中的信息已经被抽象成了信息含量更高的特征。我们可以将卷积层和池化层看成自动图像特征提取的过程。在特征提取完成之后,仍然需要使用全连接层来完成分类任务。
5. Softmax层。Softmax层主要用于分类问题。通过Softmax层,可以得到当前样例属于不同种类的概率分布情况。

卷积神经网络常用结构

卷积层

图中显示了卷积层神经网络结构中最重要的部分,这个部分被称之为过滤器filter或者内核kernel。

过滤器可以将当前层神经网络上的一个子节点矩阵转化为下一层神经网络上的一个单位节点矩阵。单位节点矩阵指的是一个长和宽都为1,但深度不限的节点矩阵。
在一个卷积层中,过滤器所处理的节点矩阵的长和宽都是由人工指定的,这个节点矩阵的尺寸也被称之为过滤器的尺寸。常用的过滤器尺寸有3×3或5×5.因为过滤器处理的矩阵深度和当前层神经网络节点矩阵的深度是一致的,所以虽然节点矩阵是三维的,但过滤器的尺寸只需要指定两个维度。过滤器中另外一个需要人工指定的设置是处理得到的单位节点矩阵的深度,这个设置称为过滤器的深度。注意过滤器的尺寸指的是一个过滤器输入节点矩阵的大小,而深度指的是输出单位节点矩阵的深度。
过滤器的前向传播过程就是通过图中左侧小矩阵中的节点计算出右侧单位矩阵中节点的过程。一个具体的样例如下:
通过过滤器将一个2×2×3的节点矩阵变化为一个1×1×5的单位节点矩阵。一个过滤器的前向传播过程和全连接层相似,它总共需要2×2×3×5+5=65个参数,其中最后的+5为偏置项参数的个数。
假设使用wix,y,z w x , y , z i 来表示对于输出单位节点矩阵中的第i个节点,过滤器输入节点(x,y,z)的权重,使用bi b i 表示第i个输出节点对应的偏置项参数,那么单位矩阵中的第i个节点的取值g(i)为:


g(i)=f(∑x=12∑y=12∑z=13ax,y,z∗wix,y,z+bi) g ( i ) = f ( ∑ x = 1 2 ∑ y = 1 2 ∑ z = 1 3 a x , y , z ∗ w x , y , z i + b i )


其中

ax,y,z a x , y , z 为过滤器中节点(x,y,z)的取值,f为激活函数。



在下图中,展示了在3×3矩阵上使用2×2过滤器的卷积层前向传播过程。在这个过程中,首先将这个过滤器用于左上角子矩阵,然后移动到左下角矩阵,再到右上角矩阵,最后到右下角矩阵。过滤器每移动一次,可以计算得到一个值,将这些数值拼接成一个新的矩阵,就完成了卷积层前向传播的过程。



当过滤器的大小不为1×1时,卷积层前向传播得到的矩阵的尺寸要小于当前层矩阵的尺寸。为了避免尺寸的变化,可以在当前层矩阵的边界上加入全0填充。这样可以使得卷积层前向传播结果矩阵的大小和当前层矩阵保持一致。下图中显示了使用全0填充后卷积层前向传播过程示意图。



除了使用全0填充,还可以通过设置过滤器移动的步长来调整结果矩阵的大小。下图中显示了当移动步长为2且使用全0填充时,卷积层前向传播的结果。



下面的公式给出了在同时使用全0填充时结果矩阵的大小。




如果不使用全0填充,下面公式给出了结果矩阵的大小。



在卷积神经网络中,每一个卷积层中使用的过滤器中的参数都是一样的。这是卷积神经网络一个非常重要的性质。比如,输入层矩阵的维度是32×32×3,第一层卷积层使用尺寸为5×5,深度为16的过滤器,那么这个卷积层的参数个数为5×5×3×16+16=1216个。


共享每一个卷积层中过滤器中的参数可以巨幅减少神经网络上的参数。卷积层的参数个数要远远小于全连接层,而且卷积层的参数个数和图片的大小无关,它只和过滤器的尺寸、深度以及当前层节点矩阵的深度有关,这使得卷积神经网络可以很好的扩展到更大的图像数据上。


下面的程序实现了一个卷积层的前向传播过程

#通过tf.get_variable的方式创建过滤器的权重变量和偏置项变量。上面介绍了卷积层的参数个数只和过滤器的尺寸、深度以及当前层节点矩阵的深度有关,所以这里声明的参数变量是一个四维矩阵,前面两个维度代表了过滤器的尺寸,第三个维度表示当前层的深度,第四个维度表示过滤器的深度。
filter_weight = tf.get_variable('weights', [5, 5, 3, 16], initializer=tf.truncated_normal_initializer(stddev=0.1))
#和卷积层的权重类似,当前层矩阵上不同位置的偏置项也是共享的,所以总共有下一层深度个不同的偏置项。
biases = tf.get_variable('biases', [16], initializer=tf.constant_initializer(0.1))

#tf.nn.conv2d提供了一个非常方便的函数来实现卷积层前向传播的算法。这个函数的第一个输入为当前层的节点矩阵。注意这个矩阵是一个四维矩阵,后面三个维度对应一个节点矩阵,第一维对应一个输入batch。比如在输入层,input[0,:,:,:]表示第一张图片,input[1,:,:,:]表示第二张图片,以此类推。tf.nn.conv2d第二个参数提供了卷积层的权重,第三个参数为不同维度上的步长。虽然第三个参数提供的是一个长度为4的数组,但是第一维和最后一维的数字要求一定是1。这是因为卷积层的步长只对矩阵的长和宽有效。最后一个参数是填充的方法,TensorFlow提供了SAME或VALID两种选择,其中SAME表示添加全0填充,VALID表示不添加
conv = tf.nn.conv2d(input, filter_weight, strides=[1, 1, 1, 1], padding='SAME')
#tf.nn.bias_add提供了一个方便的函数给每一个节点加上偏置项。注意这里不能直接使用加法,因为矩阵上不同位置上的节点都需要加上同样的偏置项。
bias = tf.nn.bias_add(conv, biases)
actived_conv = tf.nn.relu(bias)#将计算结果通过ReLU激活函数完成去线性化

池化层

池化层可以非常有效的缩小矩阵的尺寸,从而减少最后全连接层中的参数。使用池化层既可以加快计算速度也有防止过拟合问题的作用。

池化层前向传播的过程也是通过移动一个类似过滤器的结构完成的。不过池化层过滤器中的计算不是节点的加权和,而是采用更加简单的最大值或者平均值运算。使用最大值操作的池化层被称之为最大池化层max pooling,这是被使用得最多的池化层结构。使用平均值操作的池化层被称之为平均池化层average pooling。

池化层的过滤器也需要人工设定过滤器的尺寸、是否使用全0填充以及过滤器移动的步长等设置。唯一的区别在于卷积层使用的过滤器是横跨整个深度的,而池化层使用的过滤器只影响一个深度上的节点。所以池化层的过滤器除了在长和宽两个维度移动之外,它还需要在深度这个维度移动。


下面的TensorFlow程序实现了最大池化层的前向传播算法

#tf.nn.max_pool实现了最大池化层的前向传播过程,它的参数和tf.nn.conv2d函数类似。ksize提供了过滤器的尺寸,strides提供了步长信息,padding提供了是否使用全0填充。
pool = tf.nn.max_pool(actived_conv, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='SAME')

在tfnn.max_pool函数中,首先需要传入当前层的节点矩阵,这个矩阵是一个四维矩阵,格式和tf.nn.conv2d函数中的第一个参数一致。第二个参数为过滤器尺寸。虽然给出的是一个长度为4的一维数组,但这个数组的第一个和最后一个数必须是1.这意味着池化层的过滤器是不可以跨不同输入样例或者节点矩阵深度的。在实际应用中使用得最多的池化层过滤器尺寸为[1,2,2,1]或者[1,3,3,1]。
TensorFlow还提供了tf.nn.avg_pool来实现平均池化层,调用格式和tf.nn.max_pool函数是一致的。

经典卷积网络模型

LeNet-5模型


第一层,卷积层
这一层的输入就是原始的图像像素,LeNet-5模型接收的输入层大小为32×32×1.第一个卷积层过滤器尺寸为5×5,深度为6,不使用全0填充,步长为1。
第二层,池化层
这一层的输入为第一层的输出,是一个28×28×6的节点矩阵。过滤器大小为2×2,长和宽的步长均为2.
第三层,卷积层
本层的输入矩阵大小为14×14×6,使用的过滤器大小为5×5,深度为16,不使用全0填充,步长为1.
第四层,池化层
本层的输入矩阵大小为10×10×16,采用的过滤器大小为2×2,步长为2.
第五层,全连接层
本层的输入矩阵大小为5×5×16,在LeNet5模型的论文中将这一层称为卷积层,但因为过滤器的大小就是5×5,所以和全连接层没有区别。
第六层,全连接层
本层的输入节点个数为120个,输出节点个数为84个。
第七层,全连接层
本层的输入节点个数为84个,输出节点个数为10个。

通过TensorFlow训练卷积神经网络的过程和第五章中介绍的训练全连接神经网络是完全一样的。唯一的区别在于因为卷积神经网络的输入层为一个三维矩阵,所以需要调整一下输入数据的格式:

x = tf.plackholder(tf.float32, [BATCH_SIZE, mnist_inference.IMAGE_SIZE, mnist_inference.IMAGE_SIZE, mnist_inference.NUM_CHANNELS], name='x-input')#第一维表示一个batch中样例的个数,第二维和第三维表示图片的尺寸。第四维表示图片的深度,对于RBG格式的图片,深度为5
#类似的将输入的训练数据格式调整为一个四维矩阵,并将这个调整后的数据传入sess.run过程
reshaped_xs = np.reshape(BATCH_SIZE, mnist_inference.IMAGE_SIZE, mnist_inference.IMAGE_SIZE, mnist_inference.NUM_CHANNELS))

调整完输入格式后,修改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_DEPP = 64
CONV2_SIZE = 5
#全连接层的节点个数
FC_SIZE = 512
#定义卷积神经网络的前向传播过程,这里的新的参数train用于区分训练过程和测试过程。在这个程序中将用到dropout方法,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))
        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, covn2_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')
    #将第四层池化层的输出转化为第五层全连接层的输入格式。第四层的输出为7×7×64的矩阵,然而第五层全连接层需要的输入格式为向量,所以在这里需要将这个7×7×64的矩阵拉直成一个向量。pool2.get_shape函数可以得到第四层输出矩阵的维度而不需要手工计算。注意因为每一层神经网络的输入输出都为一个batch的矩阵,所以这里得到的维度也包含了一个batch中数据的个数。
    pool_shape = pool2.get_shape().as_list()#把得到的维度变成列表形式
    #计算将矩阵拉直成向量之后的长度,这个长度就是矩阵长宽及深度的乘积,注意这里的pool_shape[0]为一个batch中数据的个数
    nodes = pool_shape[1] * pool_shape[2] * pool_shape[3]
    #通过tf.reshape函数将第四层的输出变成一个batch的向量
    reshaped = tf.reshape(pool2, [pool_shape[0], nodes])
    #接下来是第五层全连接层的变量并实现前向传播过程。这一层和之前全连接网络基本一致,唯一的区别就是引入了dropout概念。dropout在训练时会随机将部分节点的输出改为0,可以避免过拟合问题,从而使得模型在测试数据上的效果更好。dropout一般只在全连接层而不是卷积层或者池化层使用。
    with tf.variable_scope('layer5-fc1'):
        fc1_weights = tf.get_variable("weight", [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)
        if train:
            fc1 = tf.nn.dropout(tc1, 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_eval程序的输入部分,就可以测试卷积神经网络在MNIST数据集上的正确率,最终结果大约是99.4%,比全连接的98.4%要好得多。
然而一种卷积神经网络架构不能解决所有问题,比如LeNet5模型就无法很好的处理类似ImageNet这样比较大的图像数据集,下面的正则表达式公式总结了一些经典的用于图片分类问题的卷积神经网络架构:
输入层 ->(卷积层+->池化层?)+ -> 全连接层+
卷积层+ 表示一层或者多层卷积层,大部分卷积神经网络中一般最多连续使用三层卷积层。“池化层?”表示没有或者一层池化层。池化层虽然可以起到减少参数防止过拟合问题,但是在部分论文中也发现可以直接通过调整卷积层步长来完成,所以有些卷积神经网络中没有池化层(但我目前没有找到这种)。在多轮卷积层和池化层之后,卷积神经网络在输出之前一般会经过1-2个全连接层。
在过滤器的深度上,大部分卷积神经网络都采用逐层递增的方式。池化层的配置相对简单,用的最多的是最大池化层。