使用深度CNN识别MNIST数据集

使用深度CNN识别MNIST数据集

TensorFlow是一个非常强大的用来做大规模数值计算的库。其所擅长的任务之一就是实现以及训练深度神经网络。
在本教程中,我们将学到构建一个TensorFlow模型的基本步骤,并将通过这些步骤为MNIST构建一个深度卷积神经网络。

准备工作

在创建模型之前,需要先加载MNIST数据集,然后启动一个TensorFlow的session。MNIST数据集在上一堂课程中已经具体介绍过,这里不再赘述。

创建骨干程序

为主程序设置一个骨干程序。创建一个名为cnn_mnist.py的文件,添加下列代码:

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

# Imports
import numpy as np
import tensorflow as tf

tf.logging.set_verbosity(tf.logging.INFO)

# Our application logic will be added here

if __name__ == "__main__":
tf.app.run()

完成本实验时,需要添加代码来构建,训练和评估卷积神经网络。完整的最终代码可以在 这里找到。

加载数据集

为了方便起见,使用一个脚本来自动下载和导入MNIST数据集。它会自动创建一个MNIST_data的目录来存储数据。

def loadData():
mnist = input_data.read_data_sets("./tmp/data", one_hot=True)
train_data = mnist.train.images # 返回 np.array
train_labels = np.asarray(mnist.train.labels, dtype=np.int32)
test_data = mnist.test.images # 返回 np.array
test_labels = np.asarray(mnist.test.labels, dtype=np.int32)
return mnist, train_data, train_labels, test_data, test_labels

这里,mnist是一个轻量级的类。它以Numpy数组的形式存储着训练、校验和测试数据集。同时提供了一个函数,用于在迭代中获得minibatch,后面我们将会用到。

CNN介绍

卷积神经网络(CNN)是当前用于图像分类任务的最先进的模型体系结构。CNN将一系列filter应用于图像的原始像素数据以提取和学习更高级别的特征,使得该模型可用于分类。CNN包含三个模块:

  • 卷积层。将特定数量的卷积滤镜应用于图像。对于每个子区域,图层执行一组卷积运算,在输出特征映射中生成单个值。卷积层通常将 ReLU激活函数应用于输出从而将非线性引入到模型中。
  • 池化层。对由卷积层提取的图像数据进行下采样,减少特征映射的维度,从而减少处理时间。常用的池化算法是最大池化,对其提取特征的子区域(例如,2×2像素的块),保持其最大值并丢弃所有其他值。
  • 全连接层。对由卷积图层提取的特征执行分类,并由池化层进行下采样。全连接层中,图层中的每个节点都连接到前一图层中的每个节点。

通常,CNN由执行特征提取的多个卷积模块组成。每个模块由一个卷积层和一个池层组成。最后的卷积模块之后是一个或多个执行分类的全连接层。CNN最后的全连接层包含模型中每个目标类的单个节点(模型可能预测的所有可能的类),使用 softmax激活函数为每个节点生成0-1之间的值(softmax值之和等于1)。可以将给定图像的softmax值解释为图像落入每个目标类别可能性的相对测量值。

深度CNN分类器

在上一个实验中,我们采用softmax regression训练了一个多分类器,其在MINIST上仅有91%的正确率,识别效果差强人意,因此,此次实验将其扩展为一个拥有多层卷积网络的softmax回归模型,将准确率提升至97.07%左右。模型构造为:

  1. 卷积层1:使用32个5x5滤波器(提取5x5像素子区域),具有ReLU激活函数;
  2. 池化层1:使用2x2过滤器和步长2执行最大池化(指定池与池不重叠);
  3. 卷积层2:使用64个5x5滤波器,具有ReLU激活函数;
  4. 池化层2:使用2x2过滤器和步长2执行最大池化;
  5. 全连接层1:1,024个神经元,Dropout正则化率为0.4(概率为0.4,任何给定元素在训练期间将被丢弃);
  6. 全连接层2(Logits Layer):10个神经元,每个数字目标类别(0-9)一个。

tf.layers模块包含创建上述三种图层类型的方法:

  • conv2d()。构造一个二维卷积层。采用过滤器数量,过滤内核大小,填充和激活函数作为参数。
  • max_pooling2d()。使用max-pooling算法构造一个二维池化层。采用过滤器大小和步幅作为参数。
  • dense()。构建一个全连接层。以神经元数量和激活函数作为参数。
    这些方法中的每一个都接受张量作为输入,并将变换后的张量作为输出返回。这样可以很容易地将一个图层连接到另一个图层:只需从一个图层创建方法获取输出并将其作为输入提供给另一个图层。

    创建分类器

    创建cnn_model_fn()函数,函数符合TensorFlow的Estimator API预期的界面(稍后在创建估算器中的更多内容)。cnn_mnist.py取MNIST特征数据,标签和 模型模式(TRAIN,EVAL,PREDICT)作为参数; 配置CNN; 并返回预测,损失和培训操作:
    def cnn_model_fn(features, labels, mode):

创建模型的相关操作都在这个函数中编写。

输入层

layers用于为二维图像数据创建卷积层和合并层的模块中的方法期望输入张量具有如下定义的形状 :[batch_size, image_width, image_height, channels]

  • batch_size:在训练期间执行梯度下降时使用的示例子集的大小。
  • image_width:示例图像的宽度。
  • image_height:示例图像的高度。
  • channels:示例图像中的颜色通道数量。对于彩色图像,通道数量是3(红色,绿色,蓝色)。对于单色图像,只有1个通道(黑色)。
    这里,MNIST数据集由单色的28x28像素图像组成,因此输入图层的所需形状为:[batch_size, 28, 28, 1]
    为了将我们的输入特征映射(features)转换为这种形状,我们可以执行以下reshape操作:
    input_layer = tf.reshape(features["x"], [-1,28,28,1])

请注意,我们已经指定-1为批量大小,它指定此维度应根据输入值的数量进行动态计算 features["x"],并保持所有其他维度的大小不变。这使我们可以把它batch_size当作一个我们可以调整的超参数。例如,如果我们将样例以5批次的形式提供给我们的模型,features["x"]将包含3,920个值(每个图像中的每个像素都有一个值),并且input_layer的形状为[5, 28, 28, 1]。同样,如果我们以100个批次为例提供示例,features["x"]将包含78,400个值,并且input_layer的形状为[100, 28, 28, 1]

卷积层1

现在可以开始实现第一层。它由一个卷积接一个max pooling组成。在第一个卷积层中,我们希望将32个5x5滤波器应用到输入层,并使用ReLU激活函数。可以使用模块中layersconv2d()方法来创建此图层,如下所示:

conv1 = tf.layers.conv2d(
inputs=input_layer,
filters=32,
kernel_size=[5, 5],
padding="same",
activation=tf.nn.relu)

  • inputs参数指定了输入张量,形状为 :[batch_size, image_width, image_height, channels]。在这里,将第一个卷积层连接到input_layer
  • filters参数指定过滤器的数量,这里是32;
  • kernel_size指定了作为过滤器的尺寸[width, height],这里是[5, 5]
  • padding参数指定两个枚举值之一(不区分大小写):valid(默认值)或same。为了指定输出张量应该与输入张量具有相同的宽度和高度值,在这里设置padding=same,它表示TensorFlow将0值添加到输入张量的边缘以保持宽度和高度为28.(没有填充,a在28x28的张量上进行5x5的卷积将产生形状为24x24的张量);
  • activation参数指定应用于卷积输出的激活函数。在这里,我们指定了使用ReLU激活 tf.nn.relu
    输出张量conv2d()的形状为 :[batch_size, 28, 28, 32],与输入相同的宽度和高度尺寸,但现在有32个通道保持每个滤波器的输出。

池化层1

接下来,将第一个池化层连接到刚刚创建的卷积层。可以使用layersmax_pooling2d()方法来构建一个使用2x2过滤器和步长为2的最大池化层:

pool1 = tf.layers.max_pooling2d(inputs=conv1, pool_size=[2, 2], strides=2)

  • inputs指定输入张量,形状为 :[batch_size, image_width, image_height, channels]。在这里,将第一个卷积层的输出连接到inputs
  • pool_size表示最大池化过滤器的尺寸[width, height],这里是[2, 2]
  • strides指定步长的大小。在这里,我们设置步长为2,这表明由滤波器提取的子区域应该在宽度和高度维度上分开2个像素(对于2x2滤波器,这意味着没有提取的区域将重叠)。如果要为宽度和高度设置不同的步长值,则可以改为指定元组或列表(例如,stride=[3, 6])。
    max_pooling2d()(pool1)生成的输出张量具有以下形状 :[batch_size, 14, 14, 32],2x2滤波器每个将宽度和高度减小50%。

卷积层2

类似于第一个卷积层,创建第二个卷积层,不过此处需要将其输入变为第一个池化层的输出。改变滤波器的个数,配置64个滤波器,尺寸不变:

conv2 = tf.layers.conv2d(
inputs=pool1,
filters=64,
kernel_size=[5, 5],
padding="same",
activation=tf.nn.relu)

池化层2

和第一个池化层类似,只需要改变其输入为第二个卷积层的输出其步长和滤波器尺寸均不变:

pool2 = tf.layers.max_pooling2d(inputs=conv2, pool_size=[2, 2], strides=2)

全连接层1

现在,图片尺寸减小到7x7,最后加入一个有1024个神经元的全连接层,用于处理整个图片。把池化层输出的张量reshape成一些向量,再使用dense()方法来连接全连接层。

pool2_flat = tf.reshape(pool2, [-1, 7*7*64])
dense = tf.layers.dense(inputs=pool2_flat, units=1024, activation=tf.nn.relu)

  • units指定全连接层中的神经元数(1024)
  • activation表示可使用的激活函数,使用tf.nn.relu添加ReLU激活。

Dropout正则化

为了减少过拟合,在输出层之前加入dropout。

dropout = tf.layers.dropout(
inputs=dense, rate=0.4, training=mode == tf.estimator.ModeKeys.TRAIN)

  • inputs指定输入张量,它是来自全连接层1(dense)的输出张量;
  • rate指定了丢弃概率; 在这里,我们使用0.4,这意味着40%的元素将在训练中被随机丢弃;
  • training使用布尔指定模型目前是否训练模式下运行; 如果trainingTrue,则仅执行丢弃。在这里,我们检查mode传递的模型函数cnn_model_fn是否是TRAIN模式。

输出张量dropout的形状为:[batch_size, 1024]

Logits层

神经网络中的最后一层是logits层,它会返回预测的原始值。创建一个包含10个神经元(每个目标类为0-9)的全连接层,并使用ReLU激活函数(默认值):

logits = tf.layers.dense(inputs=dropout, units=10)

最终的CNN输出张量logits形状为:[batch_size, 10]

生成预测

对于一个给定的例子,预测的类别是具有最高原始值的对数张量对应行中的元素。可以使用tf.argmax 函数找到这个元素的索引:

tf.argmax(input=logits, axis=1)

通过softmax激活函数从logits层中得出该样本属于不同类别的概率,使用tf.nn.softmax:

tf.nn.softmax(logits, name="softmax_tensor")

用字典存储预测值,并返回一个EstimatorSpec对象:

predictions = {
"classes": tf.argmax(input=logits, axis=1),
"probabilities": tf.nn.softmax(logits, name="softmax_tensor")
}
if mode == tf.estimator.ModeKeys.PREDICT:
return tf.estimator.EstimatorSpec(mode=mode, predictions=predictions)

计算损失

对于训练和评估,需要定义一个损失函数来衡量模型的预测和目标类别的匹配度。对于MNIST多分类问题,通常采用交叉熵作为损失度量。以下代码计算模型以任一模式TRAINEVAL模式运行时的交叉熵:

onehot_labels = tf.one_hot(indices=tf.cast(labels, tf.int32), depth=10)
loss = tf.losses.softmax_cross_entropy(
onehot_labels=onehot_labels, logits=logits)

labels张量包含了样本的预测列表,例如[1, 9, …]。为了计算交叉熵,首先需要转换labels 成相应的单热编码:

[[0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
...]

模型训练

在前面,将CNN的损失定义为logits层和标签的softmax交叉熵。现在配置模以在训练期间优化这个损失值。使用0.001的学习率和随机梯度下降算法作为优化算法:

if mode == tf.estimator.ModeKeys.TRAIN:
optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.001)
train_op = optimizer.minimize(
loss=loss,
global_step=tf.train.get_global_step())
return tf.estimator.EstimatorSpec(mode=mode, loss=loss, train_op=train_op)

模型评估

要在模型中添加准确性度量,eval_metric_opsEVAL模式中定义字典,如下所示:

eval_metric_ops = {
"accuracy": tf.metrics.accuracy(
labels=labels, predictions=predictions["classes"])}
return tf.estimator.EstimatorSpec(
mode=mode, loss=loss, eval_metric_ops=eval_metric_ops)

训练模型过程

创建模型评估器

创建main()函数,将下面的步骤加入main()函数中。

创建一个Estimator来评估模型(TensorFlow类,用于执行高级模型训练,评估和推理)。将以下代码添加到main():

# Create the Estimator
mnist_classifier = tf.estimator.Estimator(
model_fn=cnn_model_fn, model_dir="./tmp/mnist_convnet_model")
  • model_fn指定模型用于训练、评估或预测,传递cnn_model_fn,即分类器函数;
  • model_dir指定模型数据的保存目录,这里指定一个临时目录,可以更改为需要存储的路径。

添加输出到日志系统

由于CNN可能需要一段时间才能进行训练,因此设置一些日志记录,以便在训练期间跟踪进度。可以使用tf.train.SessionRunHook创建一个 tf.train.LoggingTensorHook,记录softmax层的概率值。将以下内容添加到main():

# Set up logging for predictions
tensors_to_log = {"probabilities": "softmax_tensor"}
logging_hook = tf.train.LoggingTensorHook(
tensors=tensors_to_log, every_n_iter=50)

tensors_to_log是一个字典,用于存储需要输出日志的张量。字典的key是选择的标签,将打印在日志输出中,相应的value是TensorTensorFlow图模型中的张量名称。

训练过程

现在,已经准备好训练模型,可以通过创建train_input_fn和调用mnist_classifiertrain()函数进行训练。将以下内容添加到main()函数:

# Train the model
train_input_fn = tf.estimator.inputs.numpy_input_fn(
x={"x": train_data},
y=train_labels,
batch_size=100,
num_epochs=None,
shuffle=True)
mnist_classifier.train(
input_fn=train_input_fn,
steps=20000,
hooks=[logging_hook])

numpy_input_fn函数中:

  • 分别将训练特征数据和标签传递给x(作为字典)y
  • 设置batch_size的100(每一步在100个样本中训练模型);
  • num_epochs=None表示模型将训练到达到指定的步数;
  • shuffle=True表示洗牌训练数据。
    mnist_classifier.train函数中:

  • 设置steps=20000 (模型将训练共20,000步);

  • 设置hookslogging_hook,它在训练期间被触发。
    由于运行输出数据较多,只选取其中一部分展示:


模型评估

训练完成后,需要评估模型以确定其在MNIST测试集上的准确性。使用evaluate方法评估模型,和训练模型类似,但只需要将迭代步数改变为1,即只需要执行一次操作。将以下内容添加到main():

# Evaluate the model and print results
eval_input_fn = tf.estimator.inputs.numpy_input_fn(
x={"x": eval_data},
y=eval_labels,
num_epochs=1,
shuffle=False)
eval_results = mnist_classifier.evaluate(input_fn=eval_input_fn)
print(eval_results)

运行结果:



INFO:tensorflow:Saving checkpoints for 20000 into /tmp/mnist_convnet_model/model.ckpt.                                                                                                      
INFO:tensorflow:Loss for final step: 0.0446627.
INFO:tensorflow:Starting evaluation at 2018-04-18-11:59:47
2018-04-18 11:59:47.857775: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1120] Creating TensorFlow device (/device:GPU:0) -> (device: 0, name: Quadro P4000, pci bus id: 0000:01:00.0,
compute capability: 6.1)
INFO:tensorflow:Restoring parameters from /tmp/mnist_convnet_model/model.ckpt-20000
INFO:tensorflow:Finished evaluation at 2018-04-18-11:59:48
INFO:tensorflow:Saving dict for global step 20000: accuracy = 0.9707, global_step = 20000, loss = 0.100675
{'loss': 0.10067523, 'global_step': 20000, 'accuracy': 0.97070003}

备注:

完整的代码保存在”/home/student/public/deep_learning/TensorFlow/class3“中。

Author: NYY
Link: http://yoursite.com/2018/12/06/tensorflow/3.1CNNforMNIST/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.