介绍
神经网络被用作深度学习的方法,深度学习是人工智能的许多子领域之一。 它们大约在70年前首次提出,试图模拟人类大脑的工作方式,尽管它的形式要简化得多。 各个“神经元”分层连接,分配权重以确定当信号通过网络传播时神经元如何响应。 以前,神经网络在他们能够模拟的神经元数量上受到限制,因此他们可以实现学习的复杂性。 但近年来,由于硬件开发的进步,我们已经能够构建非常深的网络,并在大量数据集上训练它们以实现机器智能的突破。
这些突破使机器在执行某些任务时能够匹配并超越人类的能力。 一个这样的任务是对象识别。 虽然历史上机器无法与人类视觉相匹配,但深度学习的最新进展使得构建可识别物体,面部,文本甚至情绪的神经网络成为可能。
在本教程中,您将实现对象识别 - 数字识别的一小部分。 使用由Google Brain实验室开发的用于深度学习研究的开源Python库TensorFlow ,您将获取数字0-9的手绘图像,并构建和训练神经网络以识别和预测数字的正确标签显示。
虽然您不需要实际深度学习或TensorFlow的先前经验来跟随本教程,但我们将假设您熟悉机器学习术语和概念,例如培训和测试,功能和标签,优化和评估。 您可以在了解有关这些概念的更多信息。
先决条件
要完成本教程,您需要:
- 本地Python 3开发环境 ,包括用于安装Python包的pip和用于创建虚拟环境的venv 。
第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库及其版本:
image==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
在读取数据时,我们使用单热编码来表示图像的标签(绘制的实际数字,例如“3”)。 单热编码使用二进制值向量来表示数值或分类值。 由于我们的标签用于数字0-9,因此向量包含十个值,每个可能的数字一个。 其中一个值设置为1,表示向量索引处的数字,其余值设置为0.例如,数字3使用向量[0, 0, 0, 1, 0, 0, 0, 0, 0, 0]
表示[0, 0, 0, 1, 0, 0, 0, 0, 0, 0]
。 由于索引3处的值存储为1,因此向量表示数字3。
为了表示实际图像本身,将28x28像素平坦化为1D向量,其大小为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
现在我们已经导入了数据,现在是时候考虑神经网络了。
第3步 - 定义神经网络架构
神经网络的体系结构指的是诸如网络中的层数,每层中的单元数以及单元如何在层之间连接的元素。 由于神经网络受到人类大脑运作的松散启发,因此术语单位用于表示我们在生物学上认为的神经元。 就像神经元在大脑周围传递信号一样,单位将先前单位的某些值作为输入,执行计算,然后将新值作为输出传递给其他单位。 这些单元分层形成网络,从一层开始输入值,一层输出值。 隐藏层一词用于输入和输出层之间的所有层,即那些与现实世界“隐藏”的层。
不同的体系结构可以产生截然不同的结果,因为性能可以被认为是体系结构的函数,例如参数,数据和训练的持续时间。
将以下代码行添加到文件中,以存储全局变量中每层的单元数。 这允许我们在一个地方改变网络架构,在本教程结束时,您可以自己测试不同数量的层和单元将如何影响我们模型的结果:
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%的机会被淘汰。 这有助于防止过度拟合。
我们现在已经定义了神经网络的架构,以及影响学习过程的超参数。 下一步是将网络构建为TensorFlow图。
第4步 - 构建TensorFlow图
为了构建我们的网络,我们将网络设置为TensorFlow执行的计算图。 TensorFlow的核心概念是张量 ,一种类似于数组或列表的数据结构。 初始化,在通过图表时进行操作,并通过学习过程进行更新。
我们首先将三个张量定义为占位符 ,这些张量是我们稍后将值输入的张量。 将以下内容添加到您的文件中:
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
设置为0.5
)和测试(当dropout
设置为1.0
)。
网络将在训练过程中更新的参数是weight
和bias
值,因此对于这些参数,我们需要设置初始值而不是空占位符。 这些值基本上是网络学习的地方,因为它们用于神经元的激活功能,代表单元之间连接的强度。
由于在训练期间优化了这些值,我们现在可以将它们设置为零。 但初始值实际上对模型的最终准确性有重大影响。 我们将使用截断的正态分布中的随机值作为权重。 我们希望它们接近于零,因此它们可以在正方向或负方向上调整,并且稍微不同,因此它们会产生不同的错误。 这将确保模型学到有用的东西。 添加以下行:
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来应用keep_prob
操作。
构建图形的最后一步是定义我们想要优化的损失函数。 TensorFlow程序中流行的损失函数选择是交叉熵 ,也称为对数损失 ,它量化了两个概率分布(预测和标签)之间的差异。 完美的分类将导致交叉熵为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
( arg_max
)和Y
(标签)来比较正确预测的图像,我们使用equal
函数将其作为[Booleans]列表返回(tps:/ / www.digitalocean.com/community/tutorials/understanding-data-types-in-python-3#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))
在每个培训第10步0次迭代后,我们通过网络提供一小批图像,我们打印出该批次的损失和准确性。 请注意,我们不应期望减少损失并提高准确性,因为值是按批次而不是整个模型。 我们使用小批量图像而不是单独提供它们以加快训练过程并允许网络在更新参数之前看到许多不同的示例。
培训完成后,我们可以在测试图像上运行会话。 这次我们使用keep_prob
rate 1.0
来确保所有单元在测试过程中都处于活动状态。
将此代码添加到文件中:
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
文件并在终端中执行以下命令以运行脚本:
python3 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
为了尝试提高模型的准确性,或者想要了解调整超参数的影响的更多信息,我们可以测试更改学习速率,退出阈值,批量大小和迭代次数的效果。 我们还可以更改隐藏层中的单元数,并更改隐藏层本身的数量,以查看不同架构如何增加或降低模型精度。
为了证明网络实际上是在识别手绘图像,让我们在我们自己的单个图像上进行测试。
首先要么下载这个要么打开图形编辑器并创建一个自己的28x28像素的数字图像。
在编辑器中打开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
您可以尝试使用更复杂的图像测试网络 - 例如,看起来像其他数字的数字,或者绘制得很差或不正确的数字 - 以查看它的票价。
结论
在本教程中,您成功地训练了一个神经网络,对MNIST数据集进行了大约92%的准确度分类,并在您自己的图像上进行了测试。 当前最先进的研究使用涉及卷积层的更复杂的网络架构,在同一问题上实现了大约99%的研究。 这些使用图像的2D结构来更好地表示内容,不像我们将所有像素平铺成784个单位的一个矢量的方法。 您可以在TensorFlow网站上阅读有关此主题的更多信息,并查看详细介绍MNIST网站上最准确结果的研究论文。
既然您已经知道如何构建和训练神经网络,您可以尝试在您自己的数据上使用此实现,或者在其他流行的数据集上进行测试,例如Google StreetView House Numbers或CIFAR-10数据集以获得更一般的图像承认。