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

来自菜鸟教程
跳转至:导航、​搜索

介绍

神经网络被用作深度学习的一种方法,深度学习是人工智能的众多子领域之一。 大约 70 年前,它们首次被提出,旨在模拟人脑的工作方式,尽管形式更为简化。 单个“神经元”分层连接,分配权重以确定当信号通过网络传播时神经元如何响应。 以前,神经网络能够模拟的神经元数量有限,因此它们可以实现的学习复杂性有限。 但近年来,由于硬件开发的进步,我们已经能够构建非常深的网络,并在庞大的数据集上对其进行训练,从而实现机器智能的突破。

这些突破使机器在执行某些任务时能够匹配并超过人类的能力。 其中一项任务是对象识别。 尽管机器在历史上一直无法与人类视觉相匹配,但深度学习的最新进展使得构建能够识别物体、面部、文本甚至情绪的神经网络成为可能。

在本教程中,您将实现对象识别的一小部分——数字识别。 使用由 Google Brain 实验室开发的用于深度学习研究的开源 Python 库 TensorFlow,您将拍摄数字 0-9 的手绘图像,并构建和训练神经网络以识别和预测显示数字的正确标签。

虽然您不需要具备实际深度学习或 TensorFlow 方面的经验来学习本教程,但我们假设您熟悉机器学习术语和概念,例如训练和测试、特征和标签、优化和评估。 您可以在 机器学习简介 中了解有关这些概念的更多信息。

先决条件

要完成本教程,您需要:

注意:TensorFlow 需要至少 3.5 的 Python 版本,并支持最高 3.8 的版本。 不支持较旧或较新版本的 Python。 本教程旨在使用 TensorFlow 版本 1.4.0-1.15.5 和 Python 3.6。 较新或较旧版本都可能导致安装或运行时错误。 有关 Windows、macOS 和 Linux 版本要求的完整列表,请访问 TensorFlow 站点上的 Install TensorFlow with pip 页面。


第 1 步 — 配置项目

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

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

mkdir tensorflow-demo
cd tensorflow-demo

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

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

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

touch requirements.txt

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

requirements.txtimage==1.5.20
numpy==1.14.3
tensorflow==1.4.0

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

pip install -r requirements.txt

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

第 2 步——导入 MNIST 数据集

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

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

touch main.py

现在在您选择的文本编辑器中打开此文件,并将这行代码添加到文件中以导入 TensorFlow 库:

主文件

import tensorflow as tf

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

主文件

...
from tensorflow.examples.tutorials.mnist import input_data


mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)  # y labels are oh-encoded

在读取数据时,我们使用 one-hot-encoding 来表示标签(实际绘制的数字,例如 “3”)的图像。 One-hot-encoding 使用二进制值向量来表示数值或分类值。 由于我们的标签用于数字 0-9,因此向量包含十个值,每个可能的数字一个。 其中一个值设置为 1,以表示该向量索引处的数字,其余值设置为 0。 例如,数字 3 使用向量 [0, 0, 0, 1, 0, 0, 0, 0, 0, 0] 表示。 由于索引 3 处的值存储为 1,因此该向量表示数字 3。

为了表示实际图像本身,将 28x28 像素展平为大小为 784 像素的一维矢量。 构成图像的 784 个像素中的每一个都存储为 0 到 255 之间的值。 这决定了像素的灰度,因为我们的图像仅以黑白显示。 所以一个黑色像素用 255 表示,一个白色像素用 0 表示,中间有各种灰度。

我们可以使用 mnist 变量来找出我们刚刚导入的数据集的大小。 查看三个子集的 num_examples,我们可以确定数据集已被分成 55,000 张用于训练的图像、5000 张用于验证的图像和 10,000 张用于测试的图像。 将以下行添加到您的文件中:

主文件

...
n_train = mnist.train.num_examples  # 55,000
n_validation = mnist.validation.num_examples  # 5000
n_test = mnist.test.num_examples  # 10,000

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

第三步——定义神经网络架构

神经网络的架构是指网络中的层数、每层中的单元数以及单元在层之间如何连接等元素。 由于神经网络受到人脑工作原理的粗略启发,因此这里使用术语单元来表示我们在生物学上认为的神经元。 就像在大脑周围传递信号的神经元一样,单元从以前的单元中获取一些值作为输入,执行计算,然后将新值作为输出传递给其他单元。 这些单元被分层形成网络,至少从一层输入值开始,一层输出值。 术语 hidden layer 用于输入和输出层之间的所有层,即 那些“隐藏”在现实世界之外的人。

不同的架构可能会产生截然不同的结果,因为性能可以被认为是架构的函数,例如参数、数据和训练持续时间。

将以下代码行添加到文件中,以将每层的单元数存储在全局变量中。 这使我们可以在一处更改网络架构,在教程结束时,您可以自己测试不同数量的层和单元将如何影响我们模型的结果:

主文件

...
n_input = 784  # input layer (28x28 pixels)
n_hidden1 = 512  # 1st hidden layer
n_hidden2 = 256  # 2nd hidden layer
n_hidden3 = 128  # 3rd hidden layer
n_output = 10  # output layer (0-9 digits)

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

术语“深度神经网络”与隐藏层的数量有关,“浅”通常意味着只有一个隐藏层,“深度”是指多个隐藏层。 给定足够的训练数据,具有足够数量单元的浅层神经网络理论上应该能够表示深度神经网络可以表示的任何功能。 但是,使用较小的深度神经网络来完成相同的任务通常在计算上更有效,而这需要一个具有指数级更多隐藏单元的浅层网络。 浅层神经网络也经常遇到过拟合,网络本质上是记住了它所看到的训练数据,并且无法将知识推广到新数据。 这就是为什么深度神经网络更常用的原因:原始输入数据和输出标签之间的多层允许网络学习不同抽象级别的特征,从而使网络本身能够更好地泛化。

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

主文件

...
learning_rate = 1e-4
n_iterations = 1000
batch_size = 128
dropout = 0.5

学习率表示在学习过程的每个步骤中参数将调整多少。 这些调整是训练的关键组成部分:每次通过网络后,我们都会稍微调整权重以尝试减少损失。 较大的学习率可以更快地收敛,但也有可能在更新时超出最佳值。 迭代次数是指我们通过训练步骤的次数,批量大小是指我们在每个步骤中使用了多少训练示例。 dropout 变量表示我们随机消除一些单位的阈值。 我们将在我们的最终隐藏层中使用 dropout,以使每个单元在每个训练步骤中都有 50% c 的几率被淘汰。 这有助于防止过度拟合。

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

第 4 步 — 构建 TensorFlow 图

为了构建我们的网络,我们将网络设置为 TensorFlow 执行的计算图。 TensorFlow 的核心概念是 tensor,一种类似于数组或列表的数据结构。 初始化,在它们通过图时进行操作,并通过学习过程进行更新。

我们首先将三个张量定义为 placeholders,它们是我们稍后将值输入的张量。 将以下内容添加到您的文件中:

主文件

...
X = tf.placeholder("float", [None, n_input])
Y = tf.placeholder("float", [None, n_output])
keep_prob = tf.placeholder(tf.float32)

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

网络在训练过程中将更新的参数是 weightbias 值,因此我们需要设置一个初始值而不是一个空的占位符。 这些值本质上是网络进行学习的地方,因为它们用于神经元的激活函数,代表单元之间连接的强度。

由于这些值在训练期间进行了优化,我们现在可以将它们设置为零。 但初始值实际上对模型的最终精度有很大影响。 我们将使用截断正态分布中的随机值作为权重。 我们希望它们接近于零,因此它们可以正向或负向调整,并且略有不同,因此它们会产生不同的误差。 这将确保模型学到一些有用的东西。 添加这些行:

主文件

...
weights = {
    'w1': tf.Variable(tf.truncated_normal([n_input, n_hidden1], stddev=0.1)),
    'w2': tf.Variable(tf.truncated_normal([n_hidden1, n_hidden2], stddev=0.1)),
    'w3': tf.Variable(tf.truncated_normal([n_hidden2, n_hidden3], stddev=0.1)),
    'out': tf.Variable(tf.truncated_normal([n_hidden3, n_output], stddev=0.1)),
}

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

主文件

...
biases = {
    'b1': tf.Variable(tf.constant(0.1, shape=[n_hidden1])),
    'b2': tf.Variable(tf.constant(0.1, shape=[n_hidden2])),
    'b3': tf.Variable(tf.constant(0.1, shape=[n_hidden3])),
    'out': tf.Variable(tf.constant(0.1, shape=[n_output]))
}

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

主文件

...
layer_1 = tf.add(tf.matmul(X, weights['w1']), biases['b1'])
layer_2 = tf.add(tf.matmul(layer_1, weights['w2']), biases['b2'])
layer_3 = tf.add(tf.matmul(layer_2, weights['w3']), biases['b3'])
layer_drop = tf.nn.dropout(layer_3, keep_prob)
output_layer = tf.matmul(layer_3, weights['out']) + biases['out']

每个隐藏层将对前一层的输出和当前层的权重执行矩阵乘法,并将偏差添加到这些值上。 在最后一个隐藏层,我们将使用我们的 keep_prob 值 0.5 应用 dropout 操作。

构建图的最后一步是定义我们想要优化的损失函数。 TensorFlow 程序中一个流行的损失函数选择是 cross-entropy,也称为 log-loss,它量化了两个概率分布(预测和标签)之间的差异。 完美的分类将导致交叉熵为 0,损失完全最小化。

我们还需要选择用于最小化损失函数的优化算法。 一个名为 梯度下降优化 的过程是一种常用方法,用于通过沿负(下降)方向的梯度采取迭代步骤来找到函数的(局部)最小值。 TensorFlow 中已经实现了多种梯度下降优化算法,在本教程中,我们将使用 Adam 优化器 。 这通过使用动量通过计算梯度的指数加权平均值并在调整中使用来加速过程来扩展梯度下降优化。 将以下代码添加到您的文件中:

主文件

...
cross_entropy = tf.reduce_mean(
    tf.nn.softmax_cross_entropy_with_logits(
        labels=Y, logits=output_layer
        ))
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)

我们现在已经定义了网络并使用 TensorFlow 构建了它。 下一步是通过图表提供数据以训练它,然后测试它是否真的学到了一些东西。

第 5 步 — 培训和测试

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

在开始训练过程之前,我们将定义评估准确性的方法,以便我们可以在训练时将其打印在小批量数据上。 这些打印的语句将允许我们检查从第一次迭代到最后一次迭代,损失减少并且准确性增加; 它们还将允许我们跟踪我们是否已经运行了足够的迭代以达到一致和最佳的结果:

主文件

...
correct_pred = tf.equal(tf.argmax(output_layer, 1), tf.argmax(Y, 1))
accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))

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

我们现在准备好初始化一个会话以运行图形。 在本次会议中,我们将向网络提供我们的训练示例,一旦训练完毕,我们将向同一张图提供新的测试示例,以确定模型的准确性。 将以下代码行添加到您的文件中:

主文件

...
init = tf.global_variables_initializer()
sess = tf.Session()
sess.run(init)

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

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

在每个训练步骤中,都会稍微调整参数以尝试减少下一步的损失。 随着学习的进行,我们应该会看到损失的减少,最终我们可以停止训练并使用网络作为模型来测试我们的新数据。

将此代码添加到文件中:

主文件

...
# train on mini batches
for i in range(n_iterations):
    batch_x, batch_y = mnist.train.next_batch(batch_size)
    sess.run(train_step, feed_dict={
        X: batch_x, Y: batch_y, keep_prob: dropout
        })

    # print loss and accuracy (per minibatch)
    if i % 100 == 0:
        minibatch_loss, minibatch_accuracy = sess.run(
            [cross_entropy, accuracy],
            feed_dict={X: batch_x, Y: batch_y, keep_prob: 1.0}
            )
        print(
            "Iteration",
            str(i),
            "\t| Loss =",
            str(minibatch_loss),
            "\t| Accuracy =",
            str(minibatch_accuracy)
            )

在每个训练步骤迭代 100 次后,我们通过网络输入一小批图像,我们打印出该批次的损失和准确度。 请注意,我们不应该期望这里的损失减少和准确性增加,因为这些值是每批的,而不是整个模型的。 我们使用小批量图像而不是单独输入它们来加快训练过程,并允许网络在更新参数之前查看许多不同的示例。

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

将此代码添加到文件中:

主文件

...
test_accuracy = sess.run(accuracy, feed_dict={X: mnist.test.images, Y: mnist.test.labels, keep_prob: 1.0})
print("\nAccuracy on test set:", test_accuracy)

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

python main.py

您将看到类似于以下的输出,尽管个别损失和准确度结果可能略有不同:

OutputIteration 0     | Loss = 3.67079    | Accuracy = 0.140625
Iteration 100   | Loss = 0.492122   | Accuracy = 0.84375
Iteration 200   | Loss = 0.421595   | Accuracy = 0.882812
Iteration 300   | Loss = 0.307726   | Accuracy = 0.921875
Iteration 400   | Loss = 0.392948   | Accuracy = 0.882812
Iteration 500   | Loss = 0.371461   | Accuracy = 0.90625
Iteration 600   | Loss = 0.378425   | Accuracy = 0.882812
Iteration 700   | Loss = 0.338605   | Accuracy = 0.914062
Iteration 800   | Loss = 0.379697   | Accuracy = 0.875
Iteration 900   | Loss = 0.444303   | Accuracy = 0.90625

Accuracy on test set: 0.9206

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

为了证明网络实际上是在识别手绘图像,让我们在我们自己的单个图像上进行测试。

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

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

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

主文件

import numpy as np
from PIL import Image
...

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

主文件

...
img = np.invert(Image.open("test_img.png").convert('L')).ravel()

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

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

现在图像数据结构正确,我们可以像以前一样运行会话,但这次只输入单个图像进行测试。

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

主文件

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

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

OutputPrediction for test image: 2

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

结论

在本教程中,您成功地训练了一个神经网络,以大约 92% 的准确率对 MNIST 数据集进行分类,并在您自己的图像上对其进行了测试。 当前最先进的研究使用涉及卷积层的更复杂的网络架构,解决了大约 99% on 个相同的问题。 这些使用图像的 2D 结构来更好地表示内容,这与我们将所有像素展平为一个 784 个单位的向量的方法不同。 您可以在 TensorFlow 网站 上阅读有关此主题的更多信息,并在 MNIST 网站 上查看详细介绍最准确结果的研究论文。

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