Tensorflow卷积神经网络识别MINST手写数字

开发环境:

Ubuntu16.04+Tensorflow1.5.0-GPU+CUDN9.0+CUDNN7.0
如果是Debian系列的系统,请参考这篇博客进行安装。

所有完整代码的github地址为:https://github.com/StudentErick/tensorflow_cnn_mnist/tree/master

卷积神经网络的架构

关于卷积神经网络的一般架构的理解,参考这篇博客
在这里,给出本次实验需要用到的神经网络的架构:
Tensorflow卷积神经网络识别MINST手写数字_第1张图片
输入的是一个28*28的二维灰度图片,其中每一个小单元的取值 n[0,255] n ∈ [ 0 , 255 ] ,表示这个像素的灰度值。不过,MNIST数据集合把这个图片压缩成了一个[1,784]的矩阵。因此,在读取数据的时候,需要进行一次图片恢复成28*28的操作。
本次实验用的模型连同输入和输出层共有8层(图上把卷积与池化视为一层,我们拆分来看),分别是:
1. 输入层,28*28的灰度图片
2. 第一个卷积层,把1卷积为28*28*32的数据集,并把该数据集传入Relu激活函数。卷积过滤器的尺寸是5*5,每次移动的步长是1.
3. 第一个最大池化层,把2进行最大池化操作,输出14*14*32的数据集。采取的池化的过滤器的尺寸是2*2,每次移动的步长是2 。
4. 第二个卷积层,把3再次进行卷积操作,输出14*14*64的数据集,并把数据集传入Relu激活函数。卷积过滤器的尺寸是5*5,移动的步长是1
5. 第二个最大池化层,把4进行最大池化操作,输出7*7*64的数据集。池化的过滤器的尺寸是2*2,每次移动的步长是2.
6. 该层是把5中所有的数据集展开成一个7*7*64=3136的一维向量(图片中的数据是错的!!!),并输入Relu激活函数
7. 该层新建了1000个神经元,与6经过激活后输出的所有神经元进行全连接。该层不进行 Softmax处理!!!
8. 10个最终预测结果,与9所有的神经元进行全连接,并把该层输入 进Softmax函数,作为最终的预测概率分布。

Tensorflow完成网络架构

加载数据操作

要把一维向量恢复成28*28的操作。[-1, 28, 28, 1]是让计算机自动调整维数,在这篇博客中提到,调整合适的数据格式,这种方式经常用到。

# 导入MNIST数据集
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)

# 输入的数据,None可以根据batch-size的结果自动调整
x_input = tf.placeholder(dtype=tf.float32, shape=[None, 784], name="x_input")
# 预测结果
y_true = tf.placeholder(dtype=tf.float32, shape=[None, 1])
# 把输入的每张图片的数据进行恢复
X_input = tf.reshape(x_input, shape=[-1, 28, 28, 1])

设置权重和偏置项操作

这里的权重相当于卷积层或者池化层的过滤器。抽象成函数,那么在每次设置的过滤器时,仅需调整函数的参数即可。

def create_weights(shape):
    '''
    随机初始化权重
    :param shape: 权重张量的形状
    :return: 初始化够的张量
    '''
    return tf.Variable(tf.truncated_normal(shape=shape, stddev=0.05))


def create_biases(shape):
    '''
    随机初始化偏置项
    :param shape:
    :return:
    '''
    return tf.Variable(tf.constant(0.5, shape=shape))

创建卷积层操作

关于卷积层的各个参数的意义,可以参照这篇关于卷积神经网络架构的博客

def create_conv_layer(input_layer,
                      in_channels,
                      filter_size,
                      stride_size,
                      out_channels,
                      use_relu=True):
    '''
    创建卷积层
    :param input_layer: 前一层的输入
    :param in_channels: 输入的channel个数
    :param filter_size: filter的边长
    :param stride_size: stride的步长
    :param out_channels: 输出的channel个数
    :param use_relu: 是否使用relu
    :return: 当前层卷积后的结果
    '''
    weights = create_weights([filter_size, filter_size,
                              in_channels, out_channels])
    biases = create_weights([out_channels])

    layer = tf.nn.conv2d(input=input_layer,
                         filter=weights,
                         strides=[1, stride_size, stride_size, 1],
                         padding='SAME')
    layer += biases

    if use_relu is True:
        layer = tf.nn.relu(layer)

    return layerpython

创建最大池化层

池化层一般情况下,紧跟着卷积层,池化是减少神经元数量的关键操作。关于各个参数的意义,同样参照卷积层提到的那篇博客。

def create_max_pooling_layer(input_layer,  #
                             filter_size,  #
                             stride_size):  #
    '''
    :param input_layer: 输出层
    :param filter_size: filter的边长
    :param stride_size: stride的步长
    :return: 卷积层最大池化后的结果
    '''
    input_layer = tf.nn.max_pool(input_layer,
                                 [1, filter_size, filter_size, 1],
                                 [1, stride_size, stride_size, 1],
                                 padding='SAME')
    return input_layer

对池化层进行flatten

全连接一般在最后一个池化操作后处理,它把池化层所有的元素展开成一维向量,这样方便后面的全连接操作。关于张量变形的操作,参照这篇博客。另外说明一点,get_shape()函数不包括第一维,因为第一维是batch-size,如果增加了这一维,就成了一次展开所有输入的训练图片了,没有意义。

def create_flatten_layer(layer):
    '''
    把输入的layer进行flatten操作
    :param layer: 输入的layer
    :return: flatten操作后的结果
    '''
    layer_num = layer.get_shape()[1:4].num_elements()
    layer = tf.reshape(layer, [-1, layer_num])

    return layer

创建全连接层

全连接层就是通过普通的矩阵乘法实现,注意权重的维数等于[输入的维数,输出的维数],乘法时是输入矩阵乘以权重,并添加上偏置项

def create_fc_layer(layer, in_num, out_num, use_relu):
    '''
    创建全连接层
    :param layer: 输入的层
    :return: 全连接后的输出层
    '''
    weight = create_weights(shape=[in_num, out_num])
    biases = create_biases([out_num])

    layer = tf.matmul(layer, weight) + biases

    if use_relu is True:
        return tf.nn.relu(layer)
    return layer

最终的预测结果

把全连接的最后一层的维数设置成需要分类的实体的个数,然后把全连接的最后一层通过Softmax函数,输出的向量的各个维就是属于各类的概率,并选择概率最大的作为最终的预测结果。

网络向前传播的架构

经过前边的分析,网络各层的架构可以表示为:

# 输入的数据,None可以根据batch-size的结果自动调整
x_input = tf.placeholder(dtype=tf.float32, shape=[None, 784], name="x_input")
# 预测结果
y_true = tf.placeholder(dtype=tf.float32, shape=[None, 1])
# 把输入的每张图片的数据进行恢复
X_input = tf.reshape(x_input, shape=[-1, 28, 28, 1])

# 第一个卷积层
layer_conv1 = create_conv_layer(input_layer=X_input,
                                in_channels=1,
                                filter_size=5,
                                stride_size=1,
                                out_channels=32,
                                use_relu=True)
# 第一个卷积层的池化层
layer_pool1 = create_max_pooling_layer(input_layer=layer_conv1,
                                       filter_size=2,
                                       stride_size=2)
# 第二个卷积层
layer_conv2 = create_conv_layer(input_layer=layer_pool1,
                                in_channels=32,
                                filter_size=2,
                                stride_size=1,
                                out_channels=64)
# 第二个卷积层池化后的结果
layer_pool2 = create_conv_layer(input_layer=layer_conv2,
                                in_channels=64,
                                filter_size=2,
                                stride_size=2,
                                out_channels=64,
                                use_relu=True)
# layer_pool2进行flatten展开,得到3136维的列向量
fc_layer1 = create_flatten_layer(layer=layer_pool2)
# 全连接,输出1000维的向量,并经过relu函数激活
fc_layer2 = create_fc_layer(layer=fc_layer1,
                            in_num=3136,
                            out_num=1000,
                            use_relu=True)
# 全连接,输出10维向量,作为最终预测的结果
fc_layer3 = create_fc_layer(layer=fc_layer2,
                            in_num=1000,
                            out_num=10,
                            use_relu=False)

# 预测结果和真实值
y_pred = tf.nn.softmax(fc_layer3)
y_true = tf.placeholder(dtype=tf.int64, shape=[None, 10], name="y_true")

损失函数的定义与预测精度的设置

损失函数使用交叉熵表示两个向量的相似程度。注意,Tensorflow中的tf.nn.softmax_cross_entropy_with_logits函数计算完交叉熵后,返回的是一个张量,需对张量进行reduce_mean操作。还有,该函数本身就自带有Softmax操作了,因此不能把经过Softmax操作后的数据传入该函数,否则会导致结果不准确!!!!由此可见,我们在这里传入的数据是未经过Softmax的输出层fc_layer3,而经过Softmax操作输出的y_pred用于做精确度计算的。

# 计算交叉熵,注意这里的logits是fc_layer3,因为这个函数本身自带有Softmax的操作
cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits=fc_layer3,
                                                        labels=y_true)
# 损失函数
cost = tf.reduce_mean(cross_entropy)
# 随机梯度下降优化
optimizer = tf.train.AdamOptimizer(learning_rate=1e-3).minimize(cost)

精度计算就是预测正确的数目占预测总数的百分比。首先,我们要明确y_pred的结构:

ypred=y(1)1y(1)1y(m)1y(1)2y(2)2y(m)2y(1)10y(2)10y(m)10 y p r e d = [ y 1 ( 1 ) y 2 ( 1 ) ⋯ y 10 ( 1 ) y 1 ( 1 ) y 2 ( 2 ) ⋯ y 10 ( 2 ) ⋮ ⋮ ⋱ ⋮ y 1 ( m ) y 2 ( m ) ⋯ y 10 ( m ) ]

矩阵总共有 m m 行,表示这一批次里面有 m m 个图片作为输入;总共有10列,表示每个元素的属于0-9的概率。我们要计算的精度是这 m m 个元素中,预测正确的百分比。思路是:每一行保留概率最大的那一个元素所在的列数,得到一个列向量 ypred_clsRm y p r e d _ c l s ∈ R m ,并把这个向量与标准结果矩阵 ytrueRm×10 y t r u e ∈ R m × 10 进行比较,计算两者对应元素相等的个数占维数的百分比即可。不过,在这里需要注意,MNIST数据集中,给出的编号向量也是 ypred y p r e d 这种格式的,因此我们还需要对MNIST的数据集进行一次转化!Tensorflow的原文是这么定义真实数据标签的:
Tensorflow卷积神经网络识别MINST手写数字_第2张图片
注意红色部分。因此需要对交叉熵进行一次 reduce_mean的操作

# 多批次计算的时候是二维的,在这里提取出第列的,作为比较
y_pred_cls = tf.argmax(y_pred, axis=1)  # 预测结果
y_true_cls = tf.argmax(y_true, axis=1)  # MNIST标准结果
# 计算精准度
correction_prediction = tf.equal(y_pred_cls, y_true_cls)
accuracy = tf.reduce_mean(tf.cast(correction_prediction, tf.float32), name="accuracy")

存储模型和加载模型进行识别

关于存储模型的基本操作,请参考这篇博客
首先,需要定义一个类,用于存储模型,在这里,我们存储这个计算图所有的数据,所以不添加参数。

# 存储模型的对象
saver = tf.train.Saver()

在训练迭代完成之后,保存整个模型:

# 把当前计算图存储为my_test_model
saver.save(sess, './my_test_model')

保存结束后,会在同级目录下多出几个文件:
Tensorflow卷积神经网络识别MINST手写数字_第3张图片
在这之前所有的操作都是在train.py这个文件中的。之后,我们新建一个predict.py文件,用于加载之前保存的数据:

saver = tf.train.import_meta_graph('my_test_model.meta')
saver.restore(sess, tf.train.latest_checkpoint('./'))

加载完成之后,在加载之前的占位符、操作等。注意一点,占位符需要重新feed数据!

graph = tf.get_default_graph()  
x_input = graph.get_tensor_by_name("x_input:0")
y_true = graph.get_tensor_by_name("y_true:0")
accuracy = graph.get_tensor_by_name("accuracy:0")

xs, ys = mnist.train.next_batch(5000)

print(sess.run(accuracy, feed_dict={x_input: xs, y_true: ys}))

注意上述代码中,没有使用官方推荐的mnist.test.image喂取x_inputminist.test.lables喂取y_true,因为使用的GPU版本会耗尽内存。因此仅仅使用了5000组训练数据。真正合理的做法可以使用一个输入输出的队列来处理,本文暂时不做这种介绍。上述模型的识别正确率可以到97.92%
Tensorflow卷积神经网络识别MINST手写数字_第4张图片

你可能感兴趣的:(机器学习)