如何利用 TensorFlow 构建识别手写数字的神经网络

简介

神经网络是深度学习的一种方法,也是人工智能的众多子领域之一。神经网络在大约 70 年前首次被提出,试图模拟人脑的工作方式,不过其形式要简化得多。单个 "神经元 "层层相连,当信号在网络中传播时,权重的分配决定了神经元的反应。以前,神经网络所能模拟的神经元数量有限,因此所能实现的学习复杂度也有限。但近年来,由于硬件开发的进步,我们已经能够构建非常深入的网络,并在巨大的数据集上对其进行训练,从而在机器智能领域实现突破。

这些突破使机器在执行某些任务时能够与人类相媲美,甚至超越人类。其中一项任务就是物体识别。虽然机器的视觉能力历来无法与人类相提并论,但最近在深度学习方面取得的进展已经使构建神经网络成为可能,它可以识别物体、人脸、文本,甚至是情绪。

在本教程中,您将实现一小部分物体识别--数字识别。TensorFlow](https://www.tensorflow.org/)是 Google Brain 实验室为深度学习研究而开发的开源 Python 库,您将使用手绘的 0-9 数字图像,构建并训练一个神经网络,以识别并预测所显示数字的正确标签。

虽然您不需要有实际深度学习或 TensorFlow 方面的经验就能跟上本教程,但我们假定您对机器学习术语和概念(如训练和测试、特征和标签、优化和评估)有一定的了解。您可以在 An Introduction to Machine Learning 中了解有关这些概念的更多信息。

先决条件

要完成本教程,您需要

<$>[注] 注意:TensorFlow 要求的 Python 最低版本为 3.5,并支持最高 3.8 的版本。不支持较旧或较新版本的 Python。本教程适用于 Python 3.6 的 TensorFlow 1.4.0-1.15.5 版本。较新或较旧的版本都可能导致安装或运行错误。有关 Windows、macOS 和 Linux 版本要求的完整列表,请访问 TensorFlow 网站上的 Install TensorFlow with pip 页面。 <$>

步骤 1 - 配置项目

在开发识别程序之前,您需要安装一些依赖项并创建一个工作区来存放文件。

我们将使用 Python 3 虚拟环境来管理项目的依赖关系。为项目创建一个新目录,并导航到新目录:

1mkdir tensorflow-demo
2cd tensorflow-demo

执行以下命令为本教程设置虚拟环境:

1python3 -m venv tensorflow-demo
2source tensorflow-demo/bin/activate

接下来,安装本教程中要用到的库。我们将使用这些库的特定版本,方法是在项目目录中创建一个requirements.txt文件,其中指定我们需要的需求和版本。创建 requirements.txt 文件:

1touch requirements.txt

在文本编辑器中打开文件,添加以下几行,指定 Image、NumPy 和 TensorFlow 库及其版本:

1[secondary_label requirements.txt]
2image==1.5.20
3numpy==1.14.3
4tensorflow==1.4.0

保存文件并退出编辑器。然后使用以下命令安装这些库:

1pip install -r requirements.txt

安装好依赖项后,我们就可以开始我们的项目了。

第 2 步 - 导入 MNIST 数据集

我们将在本教程中使用的数据集名为 MNIST,它是机器学习界的经典数据集。该数据集由大小为 28x28 像素的手写数字图像组成。以下是数据集中包含的一些数字示例:

MNIST 图像示例

让我们创建一个 Python 程序来处理这个数据集。在本教程中,我们将使用一个文件完成所有工作。创建一个名为 main.py 的新文件:

1touch main.py

现在用你选择的文本编辑器打开该文件,在文件中添加这行代码以导入 TensorFlow 库:

1[label main.py]
2import tensorflow as tf

在文件中添加以下代码行,以导入 MNIST 数据集,并将图像数据存储在变量 mnist 中:

1[label main.py]
2...
3from tensorflow.examples.tutorials.mnist import input_data
4
5mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)  # y labels are oh-encoded

在读取数据时,我们使用_one-hot-encoding_来表示图像的标签(实际绘制的数字,如 "3")。 一热编码使用二进制值向量来表示数值或分类值。由于我们的标签是 0-9 位数,因此向量包含十个值,每个可能的数字一个。例如,数字 3 使用向量"[0, 0, 0, 1, 0, 0, 0, 0, 0, 0]"表示。由于索引 3 处的值存储为 1,因此该向量表示数字 3。

为了表示实际图像本身,28x28 像素被平铺成一个 784 像素大小的一维矢量。组成图像的 784 个像素中的每个像素都存储为 0 到 255 之间的值。这决定了像素的灰度,因为我们的图像只以黑白显示。因此,黑色像素用 255 表示,白色像素用 0 表示,各种灰度介于两者之间。

我们可以使用 mnist 变量来了解刚刚导入的数据集的大小。通过观察三个子集中每个子集的 num_examples 变量,我们可以确定数据集被分成了 55000 个图像用于训练,5000 个图像用于验证,10000 个图像用于测试。在文件中添加以下几行:

1[label main.py]
2...
3n_train = mnist.train.num_examples  # 55,000
4n_validation = mnist.validation.num_examples  # 5000
5n_test = mnist.test.num_examples  # 10,000

现在我们已经导入了数据,是时候考虑神经网络了。

第 3 步 - 确定神经网络架构

神经网络的架构指的是网络的层数、每层的单元数以及单元在层与层之间的连接方式等要素。由于神经网络的灵感来源于人脑的工作原理,因此这里的单元一词指的是我们在生物学上认为的神经元。就像神经元在大脑中传递信号一样,单元从前面的单元获取一些值作为输入,进行计算,然后将新值作为输出传递给其他单元。这些单元分层构成网络,最低限度是一层用于输入值,一层用于输出值。 术语 "隐藏层 "指的是输入层和输出层之间的所有层,即那些 "隐藏 "在现实世界中的层。

不同的架构会产生截然不同的结果,因为性能可以看作是架构与参数、数据和训练时间等其他因素的函数。

在文件中添加以下代码行,将每层的单元数存储在全局变量中。这样我们就可以在一个地方改变网络结构,在教程的最后,您可以亲自测试不同的层数和单元数将如何影响我们模型的结果:

1[label main.py]
2...
3n_input = 784  # input layer (28x28 pixels)
4n_hidden1 = 512  # 1st hidden layer
5n_hidden2 = 256  # 2nd hidden layer
6n_hidden3 = 128  # 3rd hidden layer
7n_output = 10  # output layer (0-9 digits)

下图展示了我们设计的可视化架构,每一层都与周围各层完全相连:

神经网络示意图](assets/handwriting_tensorflow_python3/cnwitLM.png)

术语 "深度神经网络 "与隐藏层的数量有关,"浅层 "通常指只有一个隐藏层,而 "深度 "则指多个隐藏层。如果有足够多的训练数据,理论上,一个拥有足够多单元的浅层神经网络应该能够表示深度神经网络所能表示的任何函数。但是,使用一个较小的深度神经网络来完成同样的任务,往往在计算上更有效率,而这需要一个隐藏单元数量呈指数级增长的浅层网络。浅层神经网络还经常会遇到过拟合问题,即网络基本上会记住它所见过的训练数据,而无法将知识泛化到新数据中。这也是深度神经网络更常用的原因:原始输入数据和输出标签之间的多层结构允许网络学习不同抽象层次的特征,从而使网络本身具有更好的泛化能力。

这里需要定义的神经网络其他元素是超参数。与在训练过程中更新的参数不同,这些参数值最初是设定好的,并在整个过程中保持不变。在文件中设置以下变量和值:

1[label main.py]
2...
3learning_rate = 1e-4
4n_iterations = 1000
5batch_size = 128
6dropout = 0.5

学习率表示学习过程中每一步参数的调整幅度。这些调整是训练的关键组成部分:每次通过网络后,我们都会对权重进行轻微调整,以尽量减少损失。学习率越大,收敛速度越快,但也有可能在更新时偏离最佳值。迭代次数指的是我们通过训练步骤的次数,批量大小指的是我们在每个步骤中使用的训练实例的数量。剔除 "变量代表一个阈值,在这个阈值上我们会随机剔除一些单元。我们将在最终隐藏层中使用 dropout 变量,使每个单元在每一步训练中都有 50% 的机会被淘汰。这有助于防止过度拟合。

现在,我们已经定义了神经网络的架构,以及影响学习过程的超参数。下一步是将网络构建为 TensorFlow 图。

第 4 步 - 构建 TensorFlow 图形

为了构建我们的网络,我们将把网络设置为供 TensorFlow 执行的计算图。TensorFlow 的核心概念是_tensor_,这是一种类似于数组或列表的数据结构。

首先,我们将定义三个张量作为_placeholder_,这三个张量是我们稍后要输入数值的张量。在文件中添加以下内容:

1[label main.py]
2...
3X = tf.placeholder("float", [None, n_input])
4Y = tf.placeholder("float", [None, n_output])
5keep_prob = tf.placeholder(tf.float32)

唯一需要在声明时指定的参数是我们要输入的数据大小。对于 X,我们使用的形状是 [无, 784],其中 None 表示任何数量,因为我们将输入未定义数量的 784 像素图像。Y "的形状是"[None, 10]",因为我们将使用它来处理数量不确定的标签输出,即 10 个可能的类别。keep_prob 张量用于控制辍学率,我们将其初始化为一个占位符,而不是一个不可变变量,因为我们希望在训练(将dropout设置为0.5)和测试(将dropout设置为1.0)时使用相同的张量。

网络在训练过程中会更新的参数是 "权重 "和 "偏置 "值,因此我们需要为它们设置一个初始值,而不是一个空的占位符。这些值是网络进行学习的基础,因为它们被用于神经元的激活函数,代表单元间连接的强度。

由于这些值是在训练过程中优化的,因此我们可以暂时将它们设置为零。但实际上,初始值对模型的最终准确性有很大影响。我们将使用截断正态分布中的随机值作为权重。我们希望它们接近于零,这样它们就可以向正或负的方向调整,同时又略有不同,这样它们就会产生不同的误差。这将确保模型能学到有用的东西。添加这几行

1[label main.py]
2...
3weights = {
4    'w1': tf.Variable(tf.truncated_normal([n_input, n_hidden1], stddev=0.1)),
5    'w2': tf.Variable(tf.truncated_normal([n_hidden1, n_hidden2], stddev=0.1)),
6    'w3': tf.Variable(tf.truncated_normal([n_hidden2, n_hidden3], stddev=0.1)),
7    'out': tf.Variable(tf.truncated_normal([n_hidden3, n_output], stddev=0.1)),
8}

对于偏置,我们使用一个小的常量值,以确保张量在初始阶段激活,从而促进传播。权重和偏置张量存储在字典对象中,以便于访问。请在文件中添加此代码以定义偏置:

1[label main.py]
2...
3biases = {
4    'b1': tf.Variable(tf.constant(0.1, shape=[n_hidden1])),
5    'b2': tf.Variable(tf.constant(0.1, shape=[n_hidden2])),
6    'b3': tf.Variable(tf.constant(0.1, shape=[n_hidden3])),
7    'out': tf.Variable(tf.constant(0.1, shape=[n_output]))
8}

接下来,通过定义操作张量的操作来设置网络的层。将这几行添加到文件中:

1[label main.py]
2...
3layer_1 = tf.add(tf.matmul(X, weights['w1']), biases['b1'])
4layer_2 = tf.add(tf.matmul(layer_1, weights['w2']), biases['b2'])
5layer_3 = tf.add(tf.matmul(layer_2, weights['w3']), biases['b3'])
6layer_drop = tf.nn.dropout(layer_3, keep_prob)
7output_layer = tf.matmul(layer_3, weights['out']) + biases['out']

每个隐藏层都将对前一层的输出和当前层的权重执行矩阵乘法运算,并将偏置加到这些值上。在最后一个隐藏层,我们将使用 0.5 的 "keep_prob "值进行剔除操作。

构建图的最后一步是定义我们要优化的损失函数。TensorFlow 程序中常用的损失函数是_cross-entropy_(交叉熵),也称为_log-loss_,它量化了两个概率分布(预测值和标签)之间的差异。完美分类的交叉熵为 0,损失完全最小。

我们还需要选择用于最小化损失函数的优化算法。梯度下降优化(gradient descent optimization)是一种常见的方法,通过沿着负(下降)方向的梯度迭代,找到函数的(局部)最小值。TensorFlow 中已经实现了多种梯度下降优化算法,本教程中我们将使用 Adam optimizer。亚当优化器在梯度下降优化算法的基础上进行了扩展,通过计算梯度的指数加权平均值并在调整中使用该平均值,利用动量来加快优化过程。在文件中添加以下代码:

1[label main.py]
2...
3cross_entropy = tf.reduce_mean(
4    tf.nn.softmax_cross_entropy_with_logits(
5        labels=Y, logits=output_layer
6        ))
7train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)

现在,我们已经定义了网络,并用 TensorFlow 将其构建出来。下一步是通过图输入数据对其进行训练,然后测试它是否真的学到了东西。

步骤 5 - 培训和测试

训练过程包括通过图形输入训练数据集,并优化损失函数。每当网络迭代一批更多的训练图像时,它就会更新参数以减少损失,从而更准确地预测所显示的数字。测试过程包括通过训练图运行测试数据集,并跟踪正确预测的图像数量,从而计算出准确率。

在开始训练过程之前,我们将确定评估准确率的方法,以便在训练时将其打印到小批量数据上。通过这些打印出来的语句,我们可以检查从第一次迭代到最后一次迭代,损失是否在减少,准确率是否在提高;还可以跟踪我们是否进行了足够多的迭代,以达到一致的最佳结果:

1[label main.py]
2...
3correct_pred = tf.equal(tf.argmax(output_layer, 1), tf.argmax(Y, 1))
4accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))

correct_pred 中,我们使用 arg_max 函数通过查看 output_layer (预测值)和 Y(标签)来比较哪些图像被正确预测,并使用 equal 函数将其返回为 Booleans 列表。然后,我们可以将该列表转换为浮点数,并计算平均值,从而得到总的准确度得分。

现在,我们准备初始化一个会话来运行图形。在这个会话中,我们将向网络输入训练示例,训练完成后,我们将向同一图表输入新的测试示例,以确定模型的准确性。在文件中添加以下代码行:

1[label main.py]
2...
3init = tf.global_variables_initializer()
4sess = tf.Session()
5sess.run(init)

深度学习训练过程的本质是优化损失函数。在这里,我们的目标是最小化图像预测标签与图像真实标签之间的差异。这一过程包括四个步骤,每个步骤都会重复一定次数的迭代:

  • 通过网络传播数值
  • 计算损耗
  • 通过网络向后传播数值
  • 更新参数

在每一步训练中,我们都会对参数稍作调整,以减少下一步的损失。随着学习的深入,我们应该会看到损失的减少,最终我们可以停止训练,将网络作为测试新数据的模型。

将此代码添加到文件中:

 1[label main.py]
 2...
 3# train on mini batches
 4for i in range(n_iterations):
 5    batch_x, batch_y = mnist.train.next_batch(batch_size)
 6    sess.run(train_step, feed_dict={
 7        X: batch_x, Y: batch_y, keep_prob: dropout
 8        })
 9
10    # print loss and accuracy (per minibatch)
11    if i % 100 == 0:
12        minibatch_loss, minibatch_accuracy = sess.run(
13            [cross_entropy, accuracy],
14            feed_dict={X: batch_x, Y: batch_y, keep_prob: 1.0}
15            )
16        print(
17            "Iteration",
18            str(i),
19            "\t| Loss =",
20            str(minibatch_loss),
21            "\t| Accuracy =",
22            str(minibatch_accuracy)
23            )

在每个训练步骤中,我们通过网络输入一批小型图像,经过 100 次迭代后,我们会打印出该批图像的损失和准确率。请注意,我们不应该期望这里的损失会不断减少,准确率会不断提高,因为这些值是每个批次的,而不是整个模型的。我们使用小批量图像,而不是逐个输入,是为了加快训练过程,让网络在更新参数前看到大量不同的示例。

训练完成后,我们就可以在测试图像上运行会话。这一次,我们使用的 "keep_prob "丢弃率为 "1.0",以确保所有单元在测试过程中都处于活动状态。

将此代码添加到文件中:

1[label main.py]
2...
3test_accuracy = sess.run(accuracy, feed_dict={X: mnist.test.images, Y: mnist.test.labels, keep_prob: 1.0})
4print("\nAccuracy on test set:", test_accuracy)

现在运行我们的程序,看看我们的神经网络能多准确地识别这些手写数字。保存 main.py 文件,然后在终端执行以下命令运行脚本:

1python main.py

您将看到类似于下面的输出结果,但每个人的损耗和精确度结果可能略有不同:

 1[secondary_label Output]
 2Iteration 0 	| Loss = 3.67079 	| Accuracy = 0.140625
 3Iteration 100 	| Loss = 0.492122 	| Accuracy = 0.84375
 4Iteration 200 	| Loss = 0.421595 	| Accuracy = 0.882812
 5Iteration 300 	| Loss = 0.307726 	| Accuracy = 0.921875
 6Iteration 400 	| Loss = 0.392948 	| Accuracy = 0.882812
 7Iteration 500 	| Loss = 0.371461 	| Accuracy = 0.90625
 8Iteration 600 	| Loss = 0.378425 	| Accuracy = 0.882812
 9Iteration 700 	| Loss = 0.338605 	| Accuracy = 0.914062
10Iteration 800 	| Loss = 0.379697 	| Accuracy = 0.875
11Iteration 900 	| Loss = 0.444303 	| Accuracy = 0.90625
12
13Accuracy on test set: 0.9206

为了尝试提高模型的准确性,或者进一步了解调整超参数的影响,我们可以测试改变学习率、辍学阈值、批量大小和迭代次数的效果。我们还可以改变隐藏层中的单元数量,以及改变隐藏层本身的数量,以了解不同的架构如何提高或降低模型的准确性。

为了证明网络确实能识别手绘图像,让我们用一张自己的图像来测试一下。

如果您想在本地计算机上使用自己手绘的数字,可以使用图形编辑器创建 28x28 像素的数字图像。否则,您可以使用 curl 将以下示例测试图像下载到您的服务器或计算机上:

1curl -O https://raw.githubusercontent.com/do-community/tensorflow-digit-recognition/master/test_img.png

在编辑器中打开 main.py 文件,在文件顶部添加以下几行代码,以导入图像处理所需的两个库。

1[label main.py]
2import numpy as np
3from PIL import Image
4...

然后在文件末尾添加以下代码行,以加载手写数字的测试图像:

1[label main.py]
2...
3img = np.invert(Image.open("test_img.png").convert('L')).ravel()

图像 "库的 "打开 "函数将测试图像加载为包含三个 RGB 颜色通道和 Alpha 透明度的 4D 数组。这与我们之前使用 TensorFlow 读取数据集时使用的表示法不同,因此我们需要做一些额外的工作来匹配格式。

首先,我们使用带有 L 参数的 convert 函数将 4D RGBA 表示法还原为一个灰度颜色通道。我们将其存储为一个 numpy 数组,并使用 np.invert 将其反转,因为当前矩阵将黑色表示为 0,白色表示为 255,而我们需要的正好相反。最后,我们调用 ravel 来平铺数组。

既然图像数据的结构已经正确,我们就可以按照之前的方法运行会话,但这次只输入单张图像进行测试。

在文件中添加以下代码,以测试图像并打印输出的标签。

1[label main.py]
2...
3prediction = sess.run(tf.argmax(output_layer, 1), feed_dict={X: [img]})
4print ("Prediction for test image:", np.squeeze(prediction))

在预测时调用 np.squeeze 函数,从数组中返回单个整数(即从 [2] 到 2)。结果输出表明,网络已将该图像识别为数字 2。

1[secondary_label Output]
2Prediction for test image: 2

您可以尝试用更复杂的图像来测试网络,例如,看起来像其他数字的数字,或者画得不好或不正确的数字,看看它的表现如何。

结论

在本教程中,你成功地训练了一个神经网络,对 MNIST 数据集进行了分类,准确率约为 92%,并在你自己的图像上进行了测试。目前最先进的研究使用卷积层等更复杂的网络架构,在同样的问题上达到了约 99% 的准确率。这些方法利用图像的二维结构来更好地表示图像内容,而不像我们的方法将所有像素平铺成一个 784 个单位的向量。您可以在TensorFlow 网站上阅读更多相关内容,也可以在MNIST 网站上查看详细介绍最精确结果的研究论文。

现在,您已经知道如何构建和训练神经网络,可以尝试在自己的数据上使用这一实现,或在其他流行的数据集上进行测试,如 Google StreetView House Numbers 或用于更通用图像识别的 CIFAR-10 数据集。

Published At
Categories with 技术
comments powered by Disqus