2019 年 4 月

第 34 卷,第 4 期

[人工智能]

神经网络如何学习?

作者 Frank La La | 2019 年 4 月

Frank La Vigne在我的上一篇专栏文章“深入了解神经网络”(msdn.com/magazine/mt833269) 中,我探讨了神经网络的基本结构,并使用 Python 从头开始创建了一个神经网络。在回顾了所有神经网络通用的基本结构后,我创建了一个用于计算加权和及输出值的示例框架。神经元本身很简单,并执行基本数学函数,以将输出规范化至介于 1 和 0 之间或介于 -1 和 1 之间。不过,它们在彼此连接后就变得很强大。神经元在神经网络中分层排列,每个神经元将值传递到下一层。输入值在整个网络中前向级联,并在前向传播过程中影响输出。

不过,神经网络究竟是如何学习的呢?过程是怎样的?在学习时神经网络中会发生什么?上一篇专栏文章以值的前向传播为重点。对于受监督的学习方案,神经网络可以利用反向传播过程。

反向传播、损失和时期

回想一下,神经网络中的每个神经元都接受用输入值乘以权重来表示连接强度。反向传播通过将网络的当前输出与所需输出或正确输出进行比较,发现应该应用于神经网络中节点的正确权重。所需输出与当前输出之间的差值是由 Loss 或 Cost 函数进行计算。换句话说,Loss 函数指示神经网络对给定输入的预测有多准确。

Loss 计算公式如图 1 所示。不要被数学公式吓倒,它只是将所有差值的平方相加。通常情况下,权重和偏差最初设置为随机值,这经常会在开始定型神经网络时生成高 Loss 值。

Cost 或 Loss 函数
图 1:Cost 或 Loss 函数

然后,算法调整每个权重,以最大限度地减小计算值与正确值之间的差值。“反向传播”一词是源于,算法在计算答案后返回并调整权重和偏差。神经网络的 Loss 值越小,它的准确度就越高。然后,可以将学习过程量化为最大限度地减小 Loss 函数的输出。旨在减小 Loss 值的每个前向传播和反向传播更正周期称为“时期”。简单地说,反向传播是指寻找最佳输入权重和偏差,以获得更准确的输出或“最大限度地减小 Loss 值”。 如果你认为这样做的计算成本很高,的确如此。实际上,过去的计算能力不足,直到最近此过程才得以广泛使用。

梯度下降、学习速率和随机梯度下降

在每个时期中,权重是如何调整的?是随机调整,还是有过程?这是许多初学者开始感到困惑的地方,因为有很多不熟悉的术语,如梯度下降和学习速率。不过,如果能正确解释,这其实并不是很复杂。Loss 函数将神经网络的化繁为简,仅以一个表示神经网络的答案距离所需答案有多远的数字为指标。将神经网络的输出视为一个数字,可以用简单的术语来描述性能。目标是找到生成最低 Loss 值或最小值的一系列权重。

绘制如图 2 所示的图表,可以看到 Loss 函数有自己的曲线和梯度,可用作权重调整参考。以指向最小值的 Loss 函数曲线斜率为参考。目标是找到整个曲线中的斜率最小值,这表示神经网络最准确时的输入。

Loss 函数的简单曲线图
图 2:Loss 函数的简单曲线图

在图 2 中,如果增加权重,斜率先是达到低点,然后再次开始攀升。曲线斜率显示了到达曲线上最低点(表示最低 Loss 值)的方向。如果斜率为负,增加权重。如果斜率为正,减少权重。增加或减少的具体权重量称为“学习速率”。确定理想的学习速率既是一门艺术,也是一门科学。如果学习速率太大,算法可能会超过最小值。如果太小,定型时间会太长。此过程称为“梯度下降”。如果读者更熟悉复杂精细的微积分,将会把这个过程看作是确定 Loss 函数的导数。

不过,Loss 函数图很少会像图 2**** 那样简单。实际上有许多波峰和波谷。然后挑战就变成,如何找到所有低点中的最低点(全局最小值),而不被附近的低点(局部最小值)所蒙蔽。在这种情况下,最佳方法是随机选择曲线中的一点,然后继续执行前面描述的梯度下降过程,所以就有了“随机梯度下降”一词。 若要详细了解此过程涉及的数学概念,请观看 YouTube视频“神经网络如何学习之梯度下降 | 深度学习第 2 章”(youtu.be/IHZwWFHWa-w)。

Keras 和 TensorFlow 等库已将这种级别的神经网络体系结构的极大部分抽象出来。与任何软件工程一样,了解基础知识对应对这一领域的挑战总是有帮助的。

将理论付诸实践

在上一篇专栏文章中,我从头开始创建了用于处理 MNIST 数字图像的神经网络。生成的用于引导问题的代码库很好地说明了神经网络体系结构的内部工作原理,但无法更进一步。现在有很多框架和库能用更少的代码执行相同的任务。

首先,打开新的 Jupyter 笔记本,并在空白单元格中输入下面的代码,同时执行它来导入所有必需库:

import keras
from keras.models import Sequential
from keras.layers import Dense
from keras.utils import to_categorical
import matplotlib.pyplot as plt

请注意,此单元格中的输出表明 Keras 使用的是 TensorFlow 后端。由于 MNIST 神经网络示例很常见,因此 Keras 将它添加为自己 API 的一部分,甚至将数据分为定型集和测试集。在新单元格中输入下面的代码,并执行它来下载数据并将数据读入相应变量:

# import the data
from keras.datasets import mnist
# read the data
(X_train, y_train), (X_test, y_test) = mnist.load_data()

在输出指明文件已下载后,使用下面的代码来简要检查一下定型数据集和测试数据集:

print(X_train.shape)
print(X_test.shape)

输出应指明,x_train 数据集有 60,000 个项,x_test 数据集有 10,000 个项。两个数据集都由 28x28 像素矩阵组成。若要查看 MNIST 数据中的特定图像,请使用 MatPlotLib 通过以下代码呈现图像:

plt.imshow(X_train[10])

输出应该看起来像手写的“3”。 若要查看测试数据集中的内容,请输入以下代码:

plt.imshow(X_test[10])

输出显示零。随时都可以通过尝试更改索引号和数据集来探索图像数据集。

数据构形

与任何 AI 或数据科学项目一样,必须重新构形输入数据来满足算法需求。需要将图像数据合并成一维矢量。由于每个图像都为 28x28 像素,因此一维矢量为 1x (28x28) 或 1x784。在新单元格中输入下面的代码,并执行它(请注意,这不会生成输出文本):

num_pixels = X_train.shape[1] * X_train.shape[2]
X_train = X_train.reshape(X_train.shape[0], num_pixels).astype('float32')
X_test = X_test.reshape(X_test.shape[0], num_pixels).astype('float32')

像素值介于 0 和 255 之间。必须将这些值规范化为介于 0 和 1 之间,才能使用它们。为此,请使用以下代码:

X_train = X_train / 255
X_test = X_test / 255

然后,输入下面的代码,看看现在的数据是什么样:

X_train[0]

输出显示一个包含 784 个值的数组,这些值介于 0 和 1 之间。

接收各种手写数字图像,并确定它们代表什么数字的任务称为“分类”。生成模型前,需要先将目标变量分为不同类别。在此示例中,你知道有 10 个类别,但也可以使用 Keras 中的 to_categorical 函数来自动确定。输入并执行以下代码(输出应显示 10):

y_train = to_categorical(y_train)
y_test = to_categorical(y_test)
num_classes = y_test.shape[1]
print(num_classes)

生成、定型并测试神经网络

至此,数据已构形并准备就绪,是时候使用 Keras 生成神经网络了。输入以下代码来创建函数,此函数用于创建一个包含三层的顺序神经网络,它的输入层包含 num_pixels(或 784)个神经元:

def classification_model():
  model = Sequential()
  model.add(Dense(num_pixels, activation='relu', input_shape=(num_pixels,)))
  model.add(Dense(100, activation='relu'))
  model.add(Dense(num_classes, activation='softmax'))
  model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
  return model

将此代码与我在上一篇专栏文章的“从头开始创建”方法中使用的代码进行比较。可能会注意到,激活函数中引用了“relu”或“softmax”等新词。到目前为止,我只研究了 Sigmoid 激活函数,但还有多种其他激活函数。现在,请注意,所有激活函数都是通过输出介于 0 和 1 或 -1 和 1 之间的值来压缩输入值。

在基础结构全都就位后,是时候生成、定型和评分模型了。在空白单元格中输入以下代码,并执行它:

model = classification_model()
model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=10, verbose=2)
scores = model.evaluate(X_test, y_test, verbose=0)

当神经网络运行时,请注意每次迭代都会有 Loss 值下降。准确度也相应提高了。另外,还请注意每个时期的执行时间。完成后,输入以下代码来查看准确度和误差百分比:

print('Model Accuracy: {} \n Error: {}'.format(scores[1], 1 - scores[1]))

输出显示准确度大于 98%,误差为 1.97%。

暂留模型

至此,模型已经过定型,实现了高准确度。可以保存模型以供日后使用,这样就不用再次定型了。幸运的是,Keras 简化了此操作。将以下代码输入新单元格并执行它:

model.save('MNIST_classification_model.h5')

上面的代码创建大小约为 8KB 的二进制文件,其中包含最佳权重值和偏差值。使用 Keras 加载模型也很容易,如下所示:

from keras.models import load_model
pretrained_model = load_model('MNIST_classification_model.h5')

此 h5 文件包含模型,可以与代码一起部署,以重新构形并准备输入图像数据。换句话说,漫长的模型定型过程只需要进行一次。引用预定义模型不需要执行计算成本很高的定型过程,并且在最终的生产系统中,神经网络可以快速实现。

总结

神经网络可以解决几十年来一直困扰传统算法的问题。正如我们所见,它们将真正复杂的地方隐藏在简单结构下。神经网络的工作方式为前向传播输入、权重和偏差。不过,神经网络实际上是在相反的反向传播过程中进行学习,具体是通过确定如何确切更改权重和偏差才能生成准确结果。

从机器角度来看,学习是最大限度地减小实际结果与正确结果之间的差值。此过程既麻烦,计算成本又很高,运行一整个时期所花费的时间就是证明。幸运的是,这种定型只需要进行一次,而不是每次都需要定型模型。另外,我还探讨了如何使用 Keras 生成此神经网络。虽然可以编写从头开始生成神经网络所需的代码,但使用像 Keras 这样的现有库会简单得多,它们会为你处理细枝末节。


Frank La Vigne 是 Microsoft 的 AI 技术解决方案专家,他通过充分利用分析后的数据和 AI 来帮助公司实现更大价值。他还共同主持 DataDriven 播客。他定期在 FranksWorld.com 上发表博客,你还可以在他的 YouTube 频道“Frank’s World TV”(FranksWorld.TV) 中看到他。

衷心感谢以下技术专家对本文的审阅:Andy Leonard