Python-机器学习示例-四-

62 阅读1小时+

Python 机器学习示例(四)

原文:Python machine learning by example

协议:CC BY-NC-SA 4.0

十二、使用卷积神经网络分类服装图像

前一章总结了我们对通用和传统机器学习的最佳实践的介绍。从这一章开始,我们将深入探讨深度学习和强化学习的更高级主题。

当我们处理图像分类时,我们通常会将图像展平并获得像素向量,然后将其馈送给神经网络(或另一个模型)。尽管这可能会完成任务,但我们会丢失关键的空间信息。在本章中,我们将使用卷积神经网络 ( 中枢神经系统)从图像中提取丰富且可区分的表示。你会看到美国有线电视新闻网的表述是如何把一个“9”变成一个“9”,一个“4”变成一个“4”,一只猫变成一只猫,或者一只狗变成一只狗。

我们将从探索美国有线电视新闻网架构中的单个构件开始。然后,我们将在 TensorFlow 中开发一个 CNN 分类器来对服装图像进行分类,并解开卷积机制。最后,我们将引入数据增强来提升 CNN 模型的性能。

我们将在本章中讨论以下主题:

  • 美国有线电视新闻网积木
  • 分类用氯化萘
  • 用 TensorFlow 和 Keras 实现中枢神经系统
  • 用氯化萘对服装图像进行分类
  • 卷积滤波器的可视化
  • 数据扩充和实施

开始使用美国有线电视新闻网积木

虽然规则的隐藏层(我们到目前为止已经看到的完全连接的层)在从特定级别的数据中提取特征方面做得很好,但是这些表示在区分不同类别的图像时可能没有用。CNNs 可用于提取更丰富、更易区分的表示,例如,使汽车成为汽车,使飞机成为飞机,或手写字母“y”a“y”“z”a“z”等等。中枢神经系统是一种受人类视觉皮层生物启发的神经网络。为了揭开有线电视新闻网的神秘面纱,我将首先介绍典型有线电视新闻网的组成部分,包括卷积层、非线性层和汇聚层。

卷积层

卷积层是 CNN 中的第一层,或者如果 CNN 有多个卷积层,则是 CNN 中的前几层。它接收输入图像或矩阵,并通过对输入进行卷积运算来模拟神经元细胞对感受野的反应方式。数学上,它计算卷积层的节点和输入层中各个小区域之间的点积。小区域是感受野,卷积层的节点可以看作滤波器上的值。随着滤波器在输入层上移动,计算滤波器和当前感受野(子区域)之间的点积。在滤波器已经在所有子区域上卷积之后,获得称为特征图的新层。让我们看一个简单的例子,如下所示:

图 12.1:如何生成要素图

在本例中,图层 l 有 5 个节点,过滤器由 3 个节点组成【 w 1w 2w 3 】。我们首先计算过滤器与图层 l 中前三个节点的点积,得到输出特征图中的第一个节点;然后,我们计算过滤器和中间三个节点之间的点积,并生成输出特征图中的第二个节点;最后第三个节点由层 l 中最后三个节点上的卷积生成。

现在,我们在下面的例子中仔细看看卷积是如何工作的:

图 12.2:卷积是如何工作的

在这个例子中,一个 33 的滤波器围绕一个 55 的输入矩阵从左上子区域滑动到右下子区域。对于每个子区域,使用过滤器计算点积。以左上角子区域(在橙色矩形中)为例:我们有 1 * 1 + 1 * 0 + 1 * 1 = 2,因此要素图中左上角节点(在左上角橙色矩形中)的值为 2。对于下一个最左边的子区域(在蓝色矩形中),我们将卷积计算为 1 * 1 + 1 * 1 + 1 * 1 = 3,因此结果要素图中下一个节点(在中上蓝色矩形中)的值变为 3。最后,生成一个 3*3 的要素图。

那么我们用卷积层做什么呢?它们实际上用于提取边缘和曲线等特征。如果相应的感受野包含被过滤器识别的边缘或曲线,则输出特征图中的像素将具有高值。例如,在前面的例子中,过滤器描绘了一个反斜杠形状的“\”对角线边缘;蓝色矩形中的感受野包含类似的曲线,因此产生最高强度 3。但是,右上角的感受野不包含这样的反斜杠形状,因此它会在输出要素图中产生一个值为 0 的像素。卷积层充当曲线检测器或形状检测器。

此外,卷积层通常具有检测不同曲线和形状的多个滤波器。在前面的简单示例中,我们只应用了一个过滤器并生成了一个要素图,该图表明输入图像中的形状与过滤器中表示的曲线有多相似。为了从输入数据中检测更多的模式,我们可以使用更多的过滤器,例如水平、垂直曲线、30 度和直角形状。

此外,我们可以堆叠几个卷积层,以产生更高级别的表示,如整体形状和轮廓。链接更多的层将导致更大的感受野,能够捕捉更多的全局模式。

实际上,中枢神经系统,特别是它们的卷积层,模仿我们的视觉细胞的工作方式,如下所示:

  • 我们的视觉皮层有一组复杂的神经元细胞,它们对视野的特定子区域敏感,这些子区域被称为感受野。例如,一些细胞只在有垂直边缘的情况下才作出反应;有些细胞只有暴露在水平边缘时才会着火;有些人在看到某个方向的边缘时反应更强烈。这些细胞被组织在一起,产生整个视觉感知,每个细胞都有一个特定的组成部分。美国有线电视新闻网的卷积层由一组过滤器组成,这些过滤器充当人类视觉皮层中的细胞。
  • 一个简单的细胞只有当边缘样模式出现在它的感受子区域时才会有反应。一个更复杂的细胞对更大的子区域敏感,因此可以对整个视野中的边缘样模式做出反应。卷积层的堆叠是一堆复杂的单元,可以在更大的范围内检测模式。

就在每个卷积层之后,我们通常应用一个非线性层。

非线性层

非线性层基本上就是我们在第八章中看到的用人工神经网络预测股价的激活层。显然,它是用来引入非线性的。回想一下在卷积层,我们只进行线性运算(乘法和加法)。不管一个神经网络有多少个线性隐藏层,它都将表现为一个单层感知器。因此,我们需要在卷积层之后立即进行非线性激活。同样,ReLU 是深度神经网络中非线性层最受欢迎的候选对象。

汇集层

通常在一个或多个卷积层之后(伴随着非线性激活),我们可以直接使用导出的特征进行分类。例如,我们可以在多类分类的情况下应用 softmax 层。但是让我们先做一些数学。

给定 28 * 28 个输入图像,假设我们在第一个卷积层中应用 20 个 5 * 5 滤波器,我们将获得 20 个输出特征图,并且每个特征图层的大小为(28–5+1)*(28–5+1)= 24 * 24 = 576。这意味着作为下一层输入的要素数量从 784 个(28 * 28)增加到 11,520 个(20 * 576)。然后,我们在第二卷积层应用 50 个 5 * 5 滤波器。输出大小增长到 50 * 20 (24–5+1)(24–5+1)= 400,000。这比我们最初的 784 要高得多。我们可以看到,在最终的 softmax 层之前,每个卷积层的维数都会急剧增加。这可能会有问题,因为它很容易导致过度训练,更不用说训练如此大量的重量的成本了。

为了解决维度急剧增长的问题,我们通常在卷积和非线性层之后使用一个“T1”汇聚层“T2”。汇聚层也称为下采样层。可以想象,它降低了要素地图的维度。这是通过聚集子区域上的特征统计来完成的。典型的池化方法包括:

  • 最大池,取所有非重叠子区域的最大值
  • 平均池,取所有不重叠的子区域的平均值

在以下示例中,我们在 4 * 4 要素图上应用了 2 * 2 最大池过滤器,并输出了 2 * 2 过滤器:

图 12.3:最大池如何工作

除了降维,汇聚层还有另一个优势:平移不变性。这意味着即使输入矩阵经过少量的转换,它的输出也不会改变。例如,如果我们将输入图像向左或向右移动几个像素,只要最高像素在子区域中保持不变,最大池层的输出将仍然相同。换句话说,使用汇集层时,预测变得对位置不那么敏感。以下示例说明了 max pooling 如何实现平移不变性。

这是 4 * 4 原始图像,以及带有 2 * 2 过滤器的最大池输出:

图 12.4:原始图像和最大池的输出

如果我们将图像向右移动 1 像素,我们会得到以下移动的图像和相应的输出:

图 12.5:移位图像和输出

即使我们水平移动输入图像,我们也有相同的输出。合并图层增加了图像转换的鲁棒性。

你现在已经学会了美国有线电视新闻网的所有组成部分。比你想象的要简单,对吧?接下来让我们看看他们是如何组成美国有线电视新闻网的。

为分类设计美国有线电视新闻网

将三种类型的卷积相关层以及全连接层放在一起,我们可以构建 CNN 模型进行分类,如下所示:

图 12.6: CNN 架构

在这个例子中,输入图像首先被馈送到由一堆滤波器组成的卷积层(通过 ReLU 激活)。卷积滤波器的系数是可训练的。训练有素的初始卷积层能够导出输入图像的良好低级表示,这对于下游卷积层(如果有)以及下游分类任务至关重要。然后,池图层对每个生成的要素图进行下采样。

接下来,聚集的特征映射被馈送到第二卷积层。类似地,第二个池层减小了输出要素地图的大小。您可以根据需要链接任意多对卷积层和池层。第二(或更多,如果有的话)卷积层试图通过一系列从先前层导出的低级表示来组成高级表示,例如整体形状和轮廓。

到目前为止,特征图都是矩阵。在执行任何下游分类之前,我们需要将它们展平成一个向量。展平的要素仅被视为一个或多个完全连接的隐藏图层的输入。我们可以把美国有线电视新闻网想象成一个在常规神经网络之上的分层特征提取器。中枢神经系统非常适合利用强而独特的特征来区分图像。

如果我们处理一个二元分类问题,网络以一个逻辑函数结束,一个软最大值函数用于多类情况,或者一组逻辑函数用于多标签情况。

到现在你应该对 CNNs 有了很好的了解,应该准备好解决服装图像分类问题了。让我们从探索数据集开始。

探索服装图像数据集

服装时尚-MNIST(github.com/zalandorese…)是来自 Zalando(欧洲最大的在线时尚零售商)的图像数据集。它由 6 万个训练样本和 1 万个测试样本组成。每个样本都是 28 * 28 灰度图像,与以下 10 个类别的标签相关联,每个类别代表一件衣服:

  • 0: T 恤/上衣
  • 1:裤子
  • 2:套头衫
  • 3:着装
  • 4:外套
  • 5:凉鞋
  • 6:衬衫
  • 7:运动鞋
  • 8:包
  • 9:踝靴

扎兰多试图让这个数据集像 MNIST 数据集()一样受欢迎,用于基准算法,因此称之为时尚-MNIST。

您可以使用 GitHub 链接从获取数据部分的直接链接下载数据集,或者直接从 Keras 导入数据集,Keras 已经包含数据集及其 API。我们将采用后一种方法,如下所示:

>>> import tensorflow as tf
>>> fashion_mnist = tf.keras.datasets.fashion_mnist
>>> (train_images, train_labels), (test_images, test_labels) = fashion_mnist.load_data() 

我们只需导入 TensorFlow 并从 Keras 模块加载时尚 MNIST。我们现在有了训练图像和它们的标签,以及测试图像和它们的标签。请随意打印这四个阵列中的一些样本,例如,如下所示的训练标签:

>>> print(train_labels)
[9 0 0 ... 3 0 5] 

标签数组不包括类名。因此,我们将它们定义如下,并在以后用于绘图:

>>> class_names = ['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat', 'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot'] 

看一下图像数据的格式如下:

>>> print(train_images.shape)
(60000, 28, 28) 

有 60,000 个训练样本,每个样本表示为 28 * 28 像素。

同样,对于 10,000 个测试样本,我们检查格式如下:

>>> print(test_images.shape)
(10000, 28, 28) 

现在让我们检查一个随机训练样本,如下所示:

>>> import matplotlib.pyplot as plt
>>> plt.figure()
>>> plt.imshow(train_images[42])
>>> plt.colorbar()
>>> plt.grid(False)
>>> plt.title(class_names[train_labels[42]])
>>> plt.show() 

最终结果如下图所示:

图 12.7:来自时尚 MNIST 的训练样本

您可能会遇到类似以下的错误:

OMP: Error #15: Initializing libiomp5.dylib, but found libiomp5.dylib already initialized.
OMP: Hint This means that multiple copies of the OpenMP runtime have been linked into the program. That is dangerous, since it can degrade performance or cause incorrect results. The best thing to do is to ensure that only a single OpenMP runtime is linked into the process, e.g. by avoiding static linking of the OpenMP runtime in any library. As an unsafe, unsupported, undocumented workaround you can set the environment variable KMP_DUPLICATE_LIB_OK=TRUE to allow the program to continue to execute, but that may cause crashes or silently produce incorrect results. For more information, please see [http://www.intel.com/software/products/support/](http://www.intel.com/software/products/support/).
Abort trap: 6 

如果是,请在代码的开头添加以下代码:

>>> import os
>>> os.environ['KMP_DUPLICATE_LIB_OK'] = 'True' 

在踝靴样本中,像素值在 0 到 255 的范围内。因此,在将数据输入神经网络之前,我们需要将数据重新缩放到 0 到 1 的范围。我们将训练样本和测试样本的值除以 255,如下所示:

>>> train_images = train_images / 255.0
>>> test_images = test_images / 255.0 

现在,我们显示预处理后的前 16 个训练样本,如下所示:

>>> for i in range(16):
...     plt.subplot(4, 4, i + 1)
...     plt.subplots_adjust(hspace=.3)
...     plt.xticks([])
...     plt.yticks([])
...     plt.grid(False)
...     plt.imshow(train_images[i], cmap=plt.cm.binary)
...     plt.title(class_names[train_labels[i]])
... plt.show() 

最终结果见下图:

图 12.8:最终结果

在下一节中,我们将构建我们的 CNN 模型来对这些服装图像进行分类。

用氯化萘对服装图像进行分类

如上所述, CNN 模型有两个主要组成部分:由一组卷积层和池层组成的特征提取器,以及类似于常规神经网络的分类器后端。

构建有线电视新闻网模型

由于 Keras 中的卷积层只接收三维的单个样本,我们需要首先将数据重塑为四维,如下所示:

>>> X_train = train_images.reshape((train_images.shape[0], 28, 28, 1))
>>> X_test = test_images.reshape((test_images.shape[0], 28, 28, 1))
>>> print(X_train.shape)
(60000, 28, 28, 1) 

第一维是样本数,第四维是表示灰度图像的附加维。

在我们开发 CNN 模型之前,让我们在 TensorFlow 中指定随机种子,以实现再现性:

>>> tf.random.set_seed(42) 

我们现在从 Keras 导入必要的模块,并初始化一个基于 Keras 的模型:

>>> from tensorflow.keras import datasets, layers, models, losses
>>> model = models.Sequential() 

对于卷积提取器,我们将使用三个卷积层。我们从具有 32 个小尺寸 3 * 3 滤波器的第一卷积层开始。这由以下代码实现:

>>> model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1))) 

请注意,我们使用 ReLU 作为激活函数。

卷积层之后是最大池层,带有 2 * 2 滤波器:

>>> model.add(layers.MaxPooling2D((2, 2))) 

第二个卷积层来了。它有 64 个 3 * 3 过滤器,并带有 ReLU 激活功能:

>>> model.add(layers.Conv2D(64, (3, 3), activation='relu')) 

第二个卷积层之后是另一个具有 2 * 2 滤波器的最大池层:

>>> model.add(layers.MaxPooling2D((2, 2))) 

我们继续添加第三个卷积层。目前它有 128 个 3 * 3 过滤器:

>>> model.add(layers.Conv2D(128, (3, 3), activation='relu')) 

生成的过滤器映射然后被展平,以向下游分类器后端提供特征:

>>> model.add(layers.Flatten()) 

对于分类器后端,我们只使用一个 64 节点的隐藏层:

>>> model.add(layers.Dense(64, activation='relu')) 

这里的隐藏层是规则的全连通密层,以 ReLU 作为激活函数。

最后,输出层有 10 个节点,在我们的例子中代表 10 个不同的类,以及一个 softmax 激活:

>>> model.add(layers.Dense(10, activation='softmax')) 

现在,我们使用 Adam 作为优化器,交叉熵作为损失函数,分类精度作为度量来编译模型:

>>> model.compile(optimizer='adam',
...               loss=losses.sparse_categorical_crossentropy,
...               metrics=['accuracy']) 

让我们看一下模型摘要,如下所示:

>>> model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
conv2d (Conv2D)              (None, 26, 26, 32)        320
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 13, 13, 32)        0
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 11, 11, 64)        18496
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 5, 5, 64)          0
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 3, 3, 128)         73856
_________________________________________________________________
flatten (Flatten)            (None, 1152)              0
_________________________________________________________________
dense (Dense)                (None, 64)                73792
_________________________________________________________________
dense_1 (Dense)              (None, 10)                650
=================================================================
Total params: 167,114
Trainable params: 167,114
Non-trainable params: 0
_________________________________________________________________ 

它显示模型中的每一层、其单个输出的形状以及其可训练参数的数量。您可能会注意到,卷积层的输出是三维的,其中前两个是特征图的维度,第三个是卷积层中使用的滤波器的数量。在该示例中,最大池输出的大小(前两个维度)是其输入要素图的一半。要素地图由汇集图层进行下采样。如果去掉所有池层,您可能想知道需要训练多少参数。其实是 4,058,314!因此,应用共享的好处是显而易见的:避免过度适配和降低培训成本。

你可能想知道为什么卷积滤波器的数量在各层不断增加。回想一下,每个卷积层都试图捕获特定层次的模式。第一个卷积层捕获低级模式,如边缘、点和曲线。然后,后续的层组合在前几层中提取的图案,以形成高级图案,例如形状和轮廓。当我们在这些卷积层中前进时,在大多数情况下有越来越多的模式组合需要捕获。因此,我们需要不断增加(或至少不减少)卷积层中的滤波器数量。

拟合美国有线电视新闻网模型

现在是时候训练我们刚刚建立的模型了。我们对其进行 10 次迭代训练,并使用测试样本进行评估:

>>> model.fit(X_train, train_labels, validation_data=(X_test, test_labels), epochs=10) 

请注意,默认情况下,批处理大小为 32。以下是培训的进展情况:

Train on 60000 samples, validate on 10000 samples
Epoch 1/10
60000/60000 [==============================] - 68s 1ms/sample - loss: 0.4703 - accuracy: 0.8259 - val_loss: 0.3586 - val_accuracy: 0.8706
Epoch 2/10
60000/60000 [==============================] - 68s 1ms/sample - loss: 0.3056 - accuracy: 0.8882 - val_loss: 0.3391 - val_accuracy: 0.8783
Epoch 3/10
60000/60000 [==============================] - 69s 1ms/sample - loss: 0.2615 - accuracy: 0.9026 - val_loss: 0.2655 - val_accuracy: 0.9028
Epoch 4/10
60000/60000 [==============================] - 69s 1ms/sample - loss: 0.2304 - accuracy: 0.9143 - val_loss: 0.2506 - val_accuracy: 0.9096
Epoch 5/10
60000/60000 [==============================] - 69s 1ms/sample - loss: 0.2049 - accuracy: 0.9233 - val_loss: 0.2556 - val_accuracy: 0.9058
Epoch 6/10
60000/60000 [==============================] - 71s 1ms/sample - loss: 0.1828 - accuracy: 0.9312 - val_loss: 0.2497 - val_accuracy: 0.9122
Epoch 7/10
60000/60000 [==============================] - 68s 1ms/sample - loss: 0.1638 - accuracy: 0.9386 - val_loss: 0.3006 - val_accuracy: 0.9002
Epoch 8/10
60000/60000 [==============================] - 70s 1ms/sample - loss: 0.1453 - accuracy: 0.9455 - val_loss: 0.2662 - val_accuracy: 0.9119
Epoch 9/10
60000/60000 [==============================] - 69s 1ms/sample - loss: 0.1301 - accuracy: 0.9506 - val_loss: 0.2885 - val_accuracy: 0.9057
Epoch 10/10
60000/60000 [==============================] - 68s 1ms/sample - loss: 0.1163 - accuracy: 0.9559 - val_loss: 0.3081 - val_accuracy: 0.9100
10000/1 - 5s - loss: 0.2933 - accuracy: 0.9100 

我们能够在训练集上达到大约 96%的准确率,在测试集上达到 91%。

如果您想再次检查测试集的性能,可以执行以下操作:

>>> test_loss, test_acc = model.evaluate(X_test, test_labels, verbose=2)
>>> print('Accuracy on test set:', test_acc)
Accuracy on test set: 0.91 

现在我们有了一个训练有素的模型,我们可以使用以下代码对测试集进行预测:

>>> predictions = model.predict(X_test) 

看一看第一个样本;我们的预测如下:

>>> print(predictions[0])
[1.8473367e-11 1.1924335e-07 1.0303306e-13 1.2061150e-12 3.1937938e-07
 3.5260896e-07 6.2364621e-13 9.1853758e-07 4.0739218e-11 9.9999821e-01] 

我们有这个样本的预测概率。为了获得预测标签,我们执行以下操作:

>>> import numpy as np
>>> print('Predicted label for the first test sample: ', np.argmax(predictions[0]))
Predicted label for the first test sample: 9 

我们做了如下事实核查:

>>> print('True label for the first test sample: ',test_labels[0])
True label for the first test sample: 9 

我们进一步绘制了样本图像和预测结果,包括 10 种可能类别的概率:

>>> def plot_image_prediction(i, images, predictions, labels, class_names):
...     plt.subplot(1,2,1)
...     plt.imshow(images[i], cmap=plt.cm.binary)
...     prediction = np.argmax(predictions[i])
...     color = 'blue' if prediction == labels[i] else 'red'
...     plt.title(f"{class_names[labels[i]]} (predicted 
            {class_names[prediction]})", color=color)
...     plt.subplot(1,2,2)
...     plt.grid(False)
...     plt.xticks(range(10))
...     plot = plt.bar(range(10), predictions[i], color="#777777")
...     plt.ylim([0, 1])
...     plot[prediction].set_color('red')
...     plot[labels[i]].set_color('blue')
...     plt.show() 

原始图像(左侧)的标题为 <真实标签>(预测<预测标签> ) 如果预测与标签匹配,则为蓝色,否则为红色。预测概率(右侧)将是真实标签上的蓝色条,或者如果预测标签与真实标签不同,则是预测标签上的红色条。

让我们用第一个测试样本来试试:

>>> plot_image_prediction(0, test_images, predictions, test_labels, class_names) 

有关最终结果,请参考以下屏幕截图:

图 12.9:原始图像样本及其预测结果

随意玩弄其他样本,尤其是那些预测不准确的,比如第 17 项。

您已经看到了训练好的模型的表现,您可能会想知道学习过的卷积滤波器是什么样子的。你将在下一部分找到答案。

可视化卷积滤波器

我们从训练好的模型中提取卷积滤波器,并按照以下步骤进行可视化:

  1. 从模型总结中,我们知道模型中索引 0、2 和 4 的层是卷积层。以第二卷积层为例,我们得到它的滤波器如下:

    >>> filters, _ = model.layers[2].get_weights() 
    
  2. 接下来,我们将过滤器值标准化到 0 到 1 的范围,这样我们可以更容易地可视化它们:

    >>> f_min, f_max = filters.min(), filters.max()
    >>> filters = (filters - f_min) / (f_max - f_min) 
    
  3. Recall we have 64 filters in this convolutional layer. We visualize the first 16 filters in four rows and four columns:

    >>> n_filters = 16
    >>> for i in range(n_filters):
    ...     filter = filters[:, :, :, i]
    ...     plt.subplot(4, 4, i+1)
    ...     plt.xticks([])
    ...     plt.yticks([])
    ...     plt.imshow(filter[:, :, 0], cmap='gray')
    ... plt.show() 
    

    最终结果参见下面的截图:

图 12.10:训练好的卷积滤波器

在卷积滤波器中,黑色方块表示小权重,白色方块表示大权重。基于这种直觉,我们可以看到,第二行的第二个滤镜检测到了一个感受野的垂直线,而第一行的第三个滤镜检测到了一个从右下方的亮到左上方的暗的渐变。

在前面的例子中,我们用 60,000 个标记样本训练了服装图像分类器。然而,现实中要收集这么大的标注数据集并不容易。具体来说,图像标记既昂贵又耗时。如何用有限的样本有效训练图像分类器?一个解决方案是数据扩充。

用数据扩充增强美国有线电视新闻网分类器

数据扩充意味着扩展现有训练数据集的大小,以提高泛化性能。它克服了收集和标记更多数据的成本。在 TensorFlow 中,我们使用来自 Keras API 的 ImageDataGenerator 模块(https://www . TensorFlow . org/API _ docs/python/TF/Keras/预处理/image/ImageDataGenerator )实时实现图像增强。

数据增强的水平翻转

有很多方法可以扩充图像数据。最简单的可能是水平或垂直翻转图像。例如,如果我们水平翻转一个现有的图像,我们将有一个新的图像。要生成水平图像,我们应该创建图像数据生成器,如下所示:

>>> import os
>>> from tensorflow.keras.preprocessing.image import ImageDataGenerator, load_img 
>>> da tagen = ImageDataGenerator(horizontal_flip=True) 

我们将使用这个生成器创建被操纵的图像。现在,我们首先开发一个实用函数来生成给定增强图像生成器的图像,并按如下方式显示它们:

>>> def generate_plot_pics(datagen, original_img, save_prefix):
...     folder = 'aug_images'
...     i = 0
...     for batch in datagen.flow(original_img.reshape(
                                   (1, 28, 28, 1)),
...                               batch_size=1,
...                               save_to_dir=folder,
...                               save_prefix=save_prefix,
...                               save_format='jpeg'):
...         i += 1
...         if i > 2:
...             break
...     plt.subplot(2, 2, 1, xticks=[],yticks=[])
...     plt.imshow(original_img)
...     plt.title("Original")
...     i = 1
...     for file in os.listdir(folder):
...         if file.startswith(save_prefix):
...             plt.subplot(2, 2, i + 1, xticks=[],yticks=[])
...             aug_img = load_img(folder + "/" + file)
...             plt.imshow(aug_img)
...             plt.title(f"Augmented {i}")
...             i += 1
...     plt.show() 

生成器首先在给定原始图像和增强条件的情况下随机生成三个(在本例中)图像。然后,该函数绘制原始图像和三幅人工图像。生成的图像也存储在本地磁盘名为aug_images的文件夹中。

让我们使用第一个训练图像(请随意使用任何其他图像)来试用我们的horizontal_flip generator,如下所示:

>>> generate_plot_pics(datagen, train_images[0], 'horizontal_flip') 

有关最终结果,请参考以下屏幕截图:

图 12.11:用于数据增强的水平翻转图像

正如你看到的,生成的图像不是水平翻转就是不翻转。为什么我们不试试水平和垂直翻转同时进行的呢?我们可以这样做:

>>> datagen = ImageDataGenerator(horizontal_flip=True,
...                              vertical_flip=True)
>>> generate_plot_pics(datagen, train_images[0], 'hv_flip') 

有关最终结果,请参考以下屏幕截图:

图 12.12:用于数据增强的水平和垂直翻转图像

除了是否水平翻转外,生成的图像要么垂直翻转,要么不翻转。

一般来说,水平翻转的图像传达了与原始图像相同的信息。垂直翻转的图像并不常见。还值得注意的是,翻转只在对方向不敏感的情况下起作用,例如对猫和狗进行分类或识别汽车的部件。相反,在方向很重要的情况下这样做是危险的,比如在右转和左转标志之间进行分类。

数据扩充的轮换

不像在水平或垂直翻转中那样每 90 度旋转一次,也可以在图像数据增强中应用小到中等程度的旋转。让我们看看下面例子中的旋转:

>>> datagen = ImageDataGenerator(rotation_range=30)
>>> generate_plot_pics(datagen, train_images[0], 'rotation') 

有关最终结果,请参考以下屏幕截图:

图 12.13:用于数据增强的旋转图像

在前面的示例中,图像旋转了-30 度(逆时针)到 30 度(顺时针)的任意角度。

为数据扩充而转移

换挡是另一种常用的增力方法。它通过将原始图像水平或垂直移动少量像素来生成新图像。在 TensorFlow 中,您可以指定图像移动的最大像素数,也可以指定权重或高度的最大部分。让我们看一下下面的例子,我们将图像水平移动最多 8 个像素:

>>> datagen = ImageDataGenerator(width_shift_range=8)
>>> generate_plot_pics(datagen, train_images[0], 'width_shift') 

有关最终结果,请参考以下屏幕截图:

12.14:用于数据增强的水平移位图像

如您所见,生成的图像水平移动不超过 8 个像素。现在让我们尝试同时水平和垂直移动:

>>> datagen = ImageDataGenerator(width_shift_range=8,
...                              height_shift_range=8)
>>> generate_plot_pics(datagen, train_images[0], 'width_height_shift') 

有关最终结果,请参考以下屏幕截图:

图 12.15:用于数据增强的水平和垂直移动图像

用数据扩充改进服装图像分类器

用几种常见的增强方法武装,我们现在应用它们在小数据集上训练我们的图像分类器,步骤如下:

  1. We start by constructing a small training set:

    >>> n_small = 500
    >>> X_train = X_train[:n_small]
    >>> train_labels = train_labels[:n_small]
    >>> print(X_train.shape)
    (500, 28, 28, 1) 
    

    我们只使用 500 个样本进行训练。

  2. We architect the CNN model using the Keras Sequential API:

    >>> model = models.Sequential()
    >>> model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)))
    >>> model.add(layers.MaxPooling2D((2, 2)))
    >>> model.add(layers.Conv2D(64, (3, 3), activation='relu'))
    >>> model.add(layers.Flatten())
    >>> model.add(layers.Dense(32, activation='relu'))
    >>> model.add(layers.Dense(10, activation='softmax')) 
    

    由于我们有小尺寸的训练数据,所以我们只使用两个卷积层,并相应地调整隐藏层的尺寸:第一个卷积层有 32 个小尺寸的 3 * 3 滤波器,第二个卷积层有 64 个相同尺寸的滤波器,全连通隐藏层有 32 个节点。

  3. 我们用 Adam 作为优化器,交叉熵作为损失函数,分类精度作为度量来编译模型:

    >>> model.compile(optimizer='adam',
    ...               loss=losses.sparse_categorical_crossentropy,
    ...               metrics=['accuracy']) 
    
  4. We first train the model without data augmentation:

    >>> model.fit(X_train, train_labels, validation_data=(X_test, test_labels), epochs=20, batch_size=40)
    Train on 500 samples, validate on 10000 samples
    Epoch 1/20
    500/500 [==============================] - 6s 11ms/sample - loss: 1.8791 - accuracy: 0.3200 - val_loss: 1.3738 - val_accuracy: 0.4288
    Epoch 2/20
    500/500 [==============================] - 4s 8ms/sample - loss: 1.1363 - accuracy: 0.6100 - val_loss: 1.0929 - val_accuracy: 0.6198
    Epoch 3/20
    500/500 [==============================] - 4s 9ms/sample - loss: 0.8669 - accuracy: 0.7140 - val_loss: 0.9237 - val_accuracy: 0.6753
    ……
    ……
    Epoch 18/20
    500/500 [==============================] - 5s 10ms/sample - loss: 0.1372 - accuracy: 0.9640 - val_loss: 0.7142 - val_
    accuracy: 0.7947
    Epoch 19/20
    500/500 [==============================] - 5s 10ms/sample - loss: 0.1195 - accuracy: 0.9600 - val_loss: 0.6885 - val_accuracy: 0.7982
    Epoch 20/20
    500/500 [==============================] - 5s 10ms/sample - loss: 0.0944 - accuracy: 0.9780 - val_loss: 0.7342 - val_accuracy: 0.7924 
    

    我们训练模型 20 次迭代。

  5. Let's see how it performs on the test set:

    >>> test_loss, test_acc = model.evaluate(X_test, test_labels, verbose=2)
    >>> print('Accuracy on test set:', test_acc)
       Accuracy on test set: 0.7924 
    

    未增加数据的模型在测试集上的分类准确率为 79.24%。

  6. Now we work on the data augmentation and see if it can boost the performance. We first define the augmented data generator:

    >>> datagen = ImageDataGenerator(height_shift_range=3,
    ...                              horizontal_flip=True
    ...                              ) 
    

    我们在此应用水平翻转和垂直移动。我们注意到没有一个服装图像是颠倒的,因此垂直翻转不会提供任何正常外观的图像。此外,大多数服装图像完全水平居中,所以我们不会执行任何宽度移动。简单地说,我们尽量避免创建看起来与原始图像不同的增强图像。

  7. We clone the CNN model we used previously:

    >>> model_aug = tf.keras.models.clone_model(model) 
    

    它只是复制了 CNN 架构,并创建了新的权重,而不是共享现有模型的权重。

    我们像以前一样编译克隆模型,用 Adam 作为优化器,交叉熵作为损失函数,分类精度作为度量:

    >>> model_aug.compile(optimizer='adam',
    ...               loss=losses.sparse_categorical_crossentropy,
    ...               metrics=['accuracy']) 
    
  8. Finally, we fit this CNN model on data with real-time augmentation:

    >>> train_generator = datagen.flow(X_train, train_labels, seed=42, batch_size=40)
    >>> model_aug.fit(train_generator, epochs=50, validation_data=(X_test, test_labels))
    Epoch 1/50
    13/13 [==============================] - 5s 374ms/step - loss: 2.2150 - accuracy: 0.2060 - val_loss: 2.0099 - val_accuracy: 0.3104
    ……
    ……
    Epoch 48/50
    13/13 [==============================] - 4s 300ms/step - loss: 0.1541 - accuracy: 0.9460 - val_loss: 0.7367 - val_accuracy: 0.8003
    Epoch 49/50
    13/13 [==============================] - 4s 304ms/step - loss: 0.1487 - accuracy: 0.9340 - val_loss: 0.7211 - val_accuracy: 0.8035
    Epoch 50/50
    13/13 [==============================] - 4s 306ms/step - loss: 0.1031 - accuracy: 0.9680 - val_loss: 0.7446 - val_accuracy: 0.8109 
    

    在训练过程中,动态随机生成增强的图像,为模型提供素材。这次我们用数据扩充训练模型 50 次迭代,因为模型需要更多的迭代来学习模式。

  9. Let's see how it performs on the test set:

    >>> test_loss, test_acc = model_aug.evaluate(X_test, test_labels, verbose=2)
    >>> print('Accuracy on test set:', test_acc)
       Accuracy on test set: 0.8109 
    

    随着数据的增加,准确率从 79.24%提高到 81.09%。

请随意微调超参数,就像我们在第 8 章中用人工神经网络预测股价一样,看看能否进一步提高分类性能。

摘要

在这一章中,我们致力于使用中枢神经系统对服装图像进行分类。我们首先详细解释了有线电视新闻网模型的各个组成部分,并了解了有线电视新闻网是如何受到我们视觉细胞工作方式的启发的。然后我们开发了一个美国有线电视新闻网模型来分类来自扎兰多的时尚 MNIST 服装图片。我们还讨论了数据增强和几种流行的图像增强方法。我们用 TensorFlow 中的 Keras 模块再次练习实现深度学习模型。

在下一章中,我们将关注另一种深度学习网络:循环神经网络 ( RNNs )。CNNs 和 RNNs 是两个最强大的深度神经网络,它们让深度学习在当今如此流行。

练习

  1. 如前所述,能否尝试微调 CNN 图像分类器,看看能否击败我们已经取得的成绩?
  2. 你也可以使用辍学和提前停止技术吗?

十三、将循环神经网络用于序列预测

在前一章中,我们重点介绍了卷积神经网络 ( CNNs )并将其用于处理图像相关任务。在这一章中,我们将探索循环神经网络 ( RNNs ),它们适用于顺序数据和时间相关数据,例如每日温度、DNA 序列和顾客随时间的购物交易。您将学习循环架构是如何工作的,并看到模型的变体。然后我们将研究它们的应用,包括情感分析和文本生成。最后,作为一个额外的部分,我们将介绍一个最新的顺序学习模型:Transformer。

我们将在本章中讨论以下主题:

  • RNNs 的顺序学习
  • 神经网络的机制和培训
  • 不同类型的无线网络
  • 长短期记忆神经网络
  • 情感分析的神经网络
  • 用于文本生成的无线网络
  • 自我关注和变压器模型

引入顺序学习

到目前为止,我们在本书中解决的机器学习问题都是与时间无关的。例如,在我们以前的方法下,广告点击率不依赖于用户的历史广告点击量;在人脸分类中,模型只接受当前人脸图像,而不接受以前的人脸图像。然而,生活中有很多情况取决于时间。比如在金融欺诈检测中,不能只看现在的交易;我们还应该考虑以前的交易,这样我们就可以根据它们的差异进行建模。另一个例子是词性 ( PoS )标注,我们给一个单词分配一个 PoS(动词、名词、副词等)。不要只关注给定的单词,我们必须看前面的单词,有时也要看后面的单词。

在像刚才提到的依赖于时间的情况下,当前输出不仅依赖于当前输入,还依赖于之前的输入;请注意,先前输入的长度不是固定的。用机器学习解决这样的问题叫做序列学习,或者序列建模。显然,与时间相关的事件叫做序列。除了在不相交的时间间隔内发生的事件(如金融交易、电话等),文本、语音和视频也是顺序数据。

你可能想知道为什么我们不能通过输入整个序列来以常规方式对序列数据建模。这可能非常有限,因为我们必须固定输入大小。一个问题是,如果一个重要事件位于固定窗口之外,我们将丢失信息。但是我们能不能用一个非常大的时间窗口?请注意,特征空间随窗口大小而增长。如果我们想在某个时间窗口覆盖足够多的事件,特征空间就会变得过大。因此,过度拟合可能是另一个问题。

我希望你现在明白为什么我们需要以不同的方式对顺序数据建模。在下一节中,我们将讨论用于现代序列学习的模型:RNNs。

通过实例学习 RNN 建筑

可以想象,rnn 因其循环机制而脱颖而出。我们将在下一节详细解释这一点。之后我们将讨论不同类型的无线网络,以及一些典型的应用。

循环机制

回想一下,在前馈网络(如香草神经网络和 CNNs)中,数据是单向移动的,从输入层到输出层。在 RNNs 中,循环架构允许数据循环回到输入层。这意味着数据不限于前馈方向。具体来说,在 RNN 的隐藏层中,来自先前时间点的输出将成为当前时间点的输入的一部分。下图说明了数据在 RNN 总体上是如何流动的:

图 13.1:RNN 的一般形式

这样的递归架构使得 RNNs 可以很好地处理顺序数据,包括时间序列(如每日温度、每日产品销售和临床脑电图记录)和一般的顺序连续数据(如句子中的单词、DNA 序列等)。以一款金融诈骗检测仪为例;前一个事务的输出特征进入当前事务的训练。最后,对一个事务的预测取决于它以前的所有事务。让我用数学和视觉的方式来解释这个循环机制。

假设我们有一些输入, x t 。这里, t 代表时间步长或顺序。在前馈神经网络中,我们简单假设不同 t 处的输入相互独立。我们表示一个隐藏层在一个时间步长的输出, t ,为ht=f(xt),其中 f 是隐藏层的抽象。

如下图所示:

图 13.2:前馈神经网络的一般形式

相反,RNN 中的反馈循环将先前状态的信息馈送到当前状态。一次一个 RNN 的隐藏层的输出 t ,可以表示为hT5=f(hT11】tT13】1,xT17】t)。如下图所示:

图 13.3:展开的循环层随时间的变化

相同的任务 f 在序列的每个元素上执行,输出 h t 依赖于先前计算生成的输出ht1。链状架构捕捉到了迄今为止计算出的“记忆”。这就是 RNNs 在处理顺序数据方面如此成功的原因。

此外,由于递归架构,rnn 在处理输入序列和/或输出序列的不同组合时也有很大的灵活性。在下一节中,我们将讨论基于输入和输出的不同类别的无线网络,包括以下内容:

  • 多对一
  • 一对多
  • 多对多(同步)
  • 多对多(非同步)

我们将从查看多对一 RNNs 开始。

多对一无线网络

RNN 最直观的类型可能是多对一的 T2。一个多对一 RNN 可以有输入序列,你想要多少个时间步长就有多少个,但是它在完成整个序列后只产生一个输出。下图描述了多对一 RNN 的总体结构:

图 13.4:多对一 RNN 的一般形式

这里, f 代表一个或多个循环隐藏层,其中一个单独的层从之前的时间步长中获取自己的输出。下面是三个隐藏层叠加的示例:

图 13.5:三个循环层叠加的例子

多对一 RNNs 广泛用于对顺序数据进行分类。情绪分析就是一个很好的例子,例如,RNN 阅读整个客户评论,并给情绪评分(正面、中立或负面情绪)。同样,我们也可以在新闻文章的主题分类中使用这种 RNNs。识别歌曲的流派是另一个应用,因为模型可以读取整个音频流。我们还可以使用多对一的神经网络来根据脑电图轨迹确定患者是否癫痫发作。

一对多无线网络

一对多rnn 与多对一 rnn 完全相反。它们只接收一个输入(不是一个序列)并产生一个输出序列。下图显示了典型的一对多 RNN:

图 13.6:一对多 RNN 的一般形式

同样, f 代表一个或多个重复出现的隐藏层。

请注意,这里的“一”并不意味着只有一个输入特征。这意味着输入来自一个时间步长,或者是与时间无关的。

一对多 rnn 通常用作序列发生器。例如,我们可以给定一个起始音符或/和一个流派来生成一首音乐。同样,我们也可以像专业编剧一样,用一对多的 rnn,用我们指定的起始词来写电影剧本。图像字幕是另一个有趣的应用:RNN 接收图像并输出图像的描述(一句话)。

多对多(同步)无线网络

第三种类型的 RNN,多对多(同步),允许输入序列中的每个元素都有一个输出。让我们看看数据在以下多对多(同步)RNN 中是如何流动的:

图 13.7:多对多(同步)RNN 的一般形式

如您所见,每个输出都是基于其相应的输入和所有先前的输出计算的。

这种类型的 RNN 的一个常见用途是时间序列预测,其中我们希望基于当前和先前观察到的数据在每个时间步长执行滚动预测。以下是一些时间序列预测的示例,我们可以在其中利用同步的多对多网络:

  • 商店每天的产品销售
  • 股票的每日收盘价
  • 工厂每小时的耗电量

它们也被广泛用于解决自然语言处理问题,包括词性标注、命名实体识别和实时语音识别。

多对多(非同步)无线网络

有时候,我们只需要在处理完整个输入序列后生成输出序列*。这是多对多 RNN 的非同步版本。*

关于多对多(非同步)RNN 的一般结构,请参考下图:

图 13.8:多对多(非同步)RNN 的一般形式

注意,输出序列的长度(上图中的 T y )可以不同于输入序列的长度(上图中的 T x )。这为我们提供了一些灵活性。

这种类型的 RNN 是机器翻译的首选模型。例如,在法英翻译中,模型首先用法语阅读一个完整的句子,然后产生一个用英语翻译的句子。多步提前预测是另一个流行的例子:有时,当给定过去一个月的数据时,我们被要求预测未来多天的销售额。

现在,您已经基于模型的输入和输出了解了四种类型的 RNN。

等等,一对一 RNNs 怎么样?没有这回事。一对一只是一个常规的前馈模型。

在本章的后面,我们将应用这些类型的 RNN 来解决项目,包括情感分析和单词生成。现在,让我们弄清楚 RNN 模型是如何训练的。

训练 RNN 模型

为了解释我们如何优化 RNN 的权重(参数),我们首先注释网络上的权重和数据,如下所示:

  • U 表示连接输入层和隐藏层的权重。
  • V 表示隐藏层和输出层之间的权重。请注意,为了简单起见,我们只使用了一个循环层。
  • W 表示重现层的权重;也就是反馈层。
  • x t 表示时间步长 t 的输入。
  • s t 表示时间步长 t 时的隐藏状态。
  • h t 表示时间步长 t 的输出。

接下来,我们通过三个时间步骤展开简单的 RNN 模型:t1、 tt + 1,如下所示:

图 13.9:展开循环层

我们将各层之间的数学关系描述如下:

  • 我们让 a 表示隐藏层的激活函数。在 RNNs 中,我们通常选择 tanh 或 ReLU 作为隐藏层的激活函数。
  • 给定当前输入, x t ,以及之前的隐藏状态,st1,我们计算当前隐藏状态, s t ,由st=a(UxT22】t+Ws 请随意再次阅读第八章*、用人工神经网络预测股价来复习你的神经网络知识。*
  • 以类似的方式,我们基于计算st1我们重复这一过程,直到 s 1 ,这取决于我们通常将 s 0 设置为全零。
  • 我们让 g 表示输出层的激活函数。如果我们想要执行二进制分类,它可以是 sigmoid 函数,用于多类分类的 softmax 函数,以及用于回归的简单线性函数(即无激活)。
  • 最后,我们计算时间步长 t的输出。

随着时间的推移,依赖关系处于隐藏状态(即 s t 依赖于st1st1依赖于st—2等等),递归层给网络带来了内存,网络从所有

正如我们对传统的神经网络所做的那样,我们应用反向传播算法来优化 RNNs 中的所有权重, UVW 。然而,您可能已经注意到,一个时间步长的输出间接依赖于所有先前的时间步长( h t 依赖于 s t ,而 s t 依赖于所有先前的时间步长)。因此,除了当前时间步长之外,我们还需要计算所有先前 t -1 时间步长的损失。因此,权重的梯度是这样计算的。例如,如果我们想要计算时间步长 t = 4 处的梯度,我们需要反推前四个时间步长( t = 3、 t = 2、 t = 1、 t = 0),并对这五个时间步长上的梯度求和。这个版本的反向传播算法叫做穿越时间反向传播 ( BPTT )。

递归体系结构使 rnn 能够从输入序列的一开始就捕获信息。这提高了序列学习的预测能力。你可能想知道普通的 RNNs 能否处理长序列。理论上可以,但由于渐变消失问题,实际上不能。消失的梯度意味着梯度会随着时间的推移变得非常小,这阻止了权重的更新。我将在下一节详细解释这一点,并介绍一种变体架构,长短期内存,它有助于解决这个问题。

用长短期记忆克服长期依赖

让我们从香草 RNNs 中的消失渐变问题开始。它是从哪里来的?回想一下,在反向传播期间,梯度随着 RNN 中的每个时间步长(即)而衰减;长输入序列中的早期元素对当前梯度的计算几乎没有贡献。这意味着普通的无线网络只能在短时间窗口内捕获时间相关性。然而,遥远的时间步长之间的相关性有时是预测的关键信号。RNN 变体,包括长短期记忆 ( LSTM )和门控循环单位 ( GRU )是专门设计用来解决需要学习长期依赖关系的问题。

我们将在本书中重点介绍 LSTM,因为它比 GRU 受欢迎得多。LSTM 比 GRU 早 10 年推出,也比 GRU 更成熟。如果您有兴趣了解更多关于 GRU 及其应用的信息,请随时查看由 Yuxi Hayden Liu (Packt Publishing)编写的《Python 深度学习体系结构〉实践》第 6 章〈T1〉、〈T2〉循环神经网络、〈T3〉》。

在 LSTM,我们使用光栅机制来处理长期依赖。它的魔力来自一个记忆单元和三个建立在重现细胞顶部的信息门。“门”这个词取自一个电路中的逻辑门(en.wikipedia.org/wiki/Logic_…)。它基本上是一个 sigmoid 函数,其输出值范围从 0 到 1。0 代表“关”逻辑,而 1 代表“开”逻辑。

下图描述了 LSTM 版本的递归单元,就在普通版本之后,以供比较:

图 13.10:普通 RNNs 与 LSTM RNNs 中的复发细胞

让我们从左到右详细看看 LSTM 循环细胞:

  • c t的记忆单位。它从输入序列的最开始就记忆信息。
  • “f”代表忘记门。它决定了要忘记多少来自先前记忆状态的信息,ct1,或者换句话说,要向前传递多少信息。让 W f 表示遗忘门与前一隐藏状态之间的权重,st1U f 表示遗忘门与当前输入之间的权重, x t
  • “I”代表输入门。它控制有多少信息从当前输入到通过。WT5】I 和UT9】I 分别是连接输入门到前一隐藏状态、stT16】1 和当前输入的权重,xT20】t。
  • “tanh”只是隐藏状态的激活函数。它就像香草 RNN 中的“a”。它的输出是根据当前输入计算的, x t ,连同相关的权重, U c ,先前的隐藏状态,st1,以及相应的权重, W c
  • “o”作为输出门。它定义了从内存中为整个循环细胞的输出提取了多少信息。一如既往, W oU o 分别是之前隐藏状态和当前输入的关联权重。

我们将这些组件之间的关系描述如下:

  • 在时间步长 t 处,忘记门 f 的输出被计算为
  • 在时间步长 t 处,输入门 i 的输出被计算为
  • 在时间步长 t 处,tanh 激活的输出*c’*被计算为
  • 在时间步长 t 处,输出门 o 的输出被计算为
  • 在时间步 t 使用更新存储单元 c t (此处为操作员。表示逐元素乘法)。同样,sigmoid 函数的输出值从 0 到 1。因此,遗忘门 f 和输入门 i 分别控制前一个记忆c*t1和当前记忆输入*c’*的数量,以进行结转。
  • 最后,我们更新隐藏状态, s t ,在时间步 t开始。这里,输出门 o 控制有多少更新的存储单元 c t 将被用作整个单元的输出。

一如既往,我们应用 BPTT 算法来训练 LSTM RNNs 中的所有权重,包括四组权重,分别为 UW ,与三个门和 tanh 激活功能相关联。通过学习这些权重,LSTM 网络以一种有效的方式明确地建模长期依赖关系。因此,在实践中,LSTM 是直接或默认的 RNN 模式。接下来,您将学习如何使用 LSTM 神经网络解决现实世界的问题。我们将从电影评论情绪分类开始。

用 RNNs 分析电影评论情绪

所以,我们的第一个 RNN 项目来了:电影评论情感。我们将以 IMDb(www.imdb.com/)电影评论数据集(ai.stanford.edu/~amaas/data…)为例。它包含 25,000 个用于培训的高极性电影评论,还有 25,000 个用于测试。每个评论都标记为 1(阳性)或 0(阴性)。我们将在以下三个部分构建基于 RNN 的电影情感分类器:分析和预处理电影评论数据开发简单的 LSTM 网络利用多个 LSTM 层提升性能。

分析和预处理数据

我们先从数据分析和预处理开始,如下:

  1. 我们从 TensorFlow 导入所有必要的模块:

    >>> import tensorflow as tf
    >>> from tensorflow.keras.datasets import imdb
    >>> from tensorflow.keras import layers, models, losses, optimizers
    >>> from tensorflow.keras.preprocessing.sequence import pad_sequences 
    
  2. Keras has a built-in IMDb dataset, so first, we load the dataset:

    >>> vocab_size = 5000
    >>> (X_train, y_train), (X_test, y_test) = \
                         imdb.load_data(num_words=vocab_size) 
    

    在这里,我们设置了词汇量,只保留这许多最频繁的单词。在本例中,这是数据集中出现频率最高的前 5,000 个单词。如果num_wordsNone,所有的字都会保留。

  3. Take a look at the training and testing data we just loaded:

    >>> print('Number of training samples:', len(y_train))
    Number of training samples: 25000
    >>> print('Number of positive samples', sum(y_train))
    Number of positive samples 12500
    >>> print('Number of test samples:', len(y_test))
    Number of test samples: 25000 
    

    训练集是完美平衡的,正负样本数量相同。

  4. Print a training sample, as follows:

    >>> print(X_train[0])
    [1, 14, 22, 16, 43, 530, 973, 1622, 1385, 65, 458, 4468, 66, 3941, 4, 173, 36, 256, 5, 25, 100, 43, 838, 112, 50, 670, 2, 9, 35, 480, 284, 5, 150, 4, 172, 112, 167, 2, 336, 385, 39, 4, 172, 4536, 1111, 17, 546, 38, 13, 447, 4, 192, 50, 16, 6, 147, 2025, 19, 14, 22, 4, 1920, 4613, 469, 4, 22, 71, 87, 12, 16, 43, 530, 38, 76, 15, 13, 1247, 4, 22, 17, 515, 17, 12, 16, 626, 18, 2, 5, 62, 386, 12, 8, 316, 8, 106, 5, 4, 2223, 2, 16, 480, 66, 3785, 33, 4, 130, 12, 16, 38, 619, 5, 25, 124, 51, 36, 135, 48, 25, 1415, 33, 6, 22, 12, 215, 28, 77, 52, 5, 14, 407, 16, 82, 2, 8, 4, 107, 117, 2, 15, 256, 4, 2, 7, 3766, 5, 723, 36, 71, 43, 530, 476, 26, 400, 317, 46, 7, 4, 2, 1029, 13, 104, 88, 4, 381, 15, 297, 98, 32, 2071, 56, 26, 141, 6, 194, 2, 18, 4, 226, 22, 21, 134, 476, 26, 480, 5, 144, 30, 2, 18, 51, 36, 28, 224, 92, 25, 104, 4, 226, 65, 16, 38, 1334, 88, 12, 16, 283, 5, 16, 4472, 113, 103, 32, 15, 16, 2, 19, 178, 32] 
    

    如您所见,原始文本已经被转换成一袋单词,每个单词都由一个整数表示。为了方便起见,整数值表示该词在数据集中出现的频率。例如,“1”代表最频繁的单词(“the”,你可以想象),而“10”代表第十个最频繁的单词。我们能找出单词是什么吗?让我们看看下一步。

  5. We use the word dictionary to map the integer back to the word it represents:

    >>> word_index = imdb.get_word_index()
    >>> index_word = {index: word for word, index in word_index.items()} 
    

    以第一次复习为例:

    >>> print([index_word.get(i, ' ') for i in X_train[0]])
    ['the', 'as', 'you', 'with', 'out', 'themselves', 'powerful', 'lets', 'loves', 'their', 'becomes', 'reaching', 'had', 'journalist', 'of', 'lot', 'from', 'anyone', 'to', 'have', 'after', 'out', 'atmosphere', 'never', 'more', 'room', 'and', 'it', 'so', 'heart', 'shows', 'to', 'years', 'of', 'every', 'never', 'going', 'and', 'help', 'moments', 'or', 'of', 'every', 'chest', 'visual', 'movie', 'except', 'her', 'was', 'several', 'of', 'enough', 'more', 'with', 'is', 'now', 'current', 'film', 'as', 'you', 'of', 'mine', 'potentially', 'unfortunately', 'of', 'you', 'than', 'him', 'that', 'with', 'out', 'themselves', 'her', 'get', 'for', 'was', 'camp', 'of', 'you', 'movie', 'sometimes', 'movie', 'that', 'with', 'scary', 'but', 'and', 'to', 'story', 'wonderful', 'that', 'in', 'seeing', 'in', 'character', 'to', 'of', '70s', 'and', 'with', 'heart', 'had', 'shadows', 'they', 'of', 'here', 'that', 'with', 'her', 'serious', 'to', 'have', 'does', 'when', 'from', 'why', 'what', 'have', 'critics', 'they', 'is', 'you', 'that', "isn't", 'one', 'will', 'very', 'to', 'as', 'itself', 'with', 'other', 'and', 'in', 'of', 'seen', 'over', 'and', 'for', 'anyone', 'of', 'and', 'br', "show's", 'to', 'whether', 'from', 'than', 'out', 'themselves', 'history', 'he', 'name', 'half', 'some', 'br', 'of', 'and', 'odd', 'was', 'two', 'most', 'of', 'mean', 'for', '1', 'any', 'an', 'boat', 'she', 'he', 'should', 'is', 'thought', 'and', 'but', 'of', 'script', 'you', 'not', 'while', 'history', 'he', 'heart', 'to', 'real', 'at', 'and', 'but', 'when', 'from', 'one', 'bit', 'then', 'have', 'two', 'of', 'script', 'their', 'with', 'her', 'nobody', 'most', 'that', 'with', "wasn't", 'to', 'with', 'armed', 'acting', 'watch', 'an', 'for', 'with', 'and', 'film', 'want', 'an'] 
    
  6. Next, we analyze the length of each sample (the number of words in each review, for example). We do so because all the input sequences to an RNN model must be the same length:

    >>> review_lengths = [len(x) for x in X_train] 
    

    绘制这些文档长度的分布,如下所示:

    >>> import matplotlib.pyplot as plt
    >>> plt.hist(review_lengths, bins=10)
    >>> plt.show() 
    

    分配结果见下图:

    图 13.11:检查长度分布

  7. As you can see, the majority of the reviews are around 200 words long. Next, we set 200 as the universal sequence length by padding shorter reviews with zeros and truncating longer reviews. We use the pad_sequences function from Keras to accomplish this:

    >>> maxlen = 200
    >>> X_train = pad_sequences(X_train, maxlen=maxlen)
    >>> X_test = pad_sequences(X_test, maxlen=maxlen) 
    

    接下来让我们看看输入序列的形状:

    >>> print('X_train shape after padding:', X_train.shape)
    X_train shape after padding: (25000, 200)
    >>> print('X_test shape after padding:', X_test.shape)
    X_test shape after padding: (25000, 200) 
    

让我们继续建立一个 LSTM 网络。

建立一个简单的 LSTM 网络

现在训练和测试数据集已经准备好了,我们可以构建我们的第一个 RNN 模型:

  1. 首先,我们修复随机种子并启动一个 Keras 顺序模型:

    >>> tf.random.set_seed(42)
    >>> model = models.Sequential() 
    
  2. Since our input sequences are word indices that are equivalent to one-hot encoded vectors, we need to embed them in dense vectors using the Embedding layer from Keras:

    >>> embedding_size = 32
    >>> model.add(layers.Embedding(vocab_size, embedding_size)) 
    

    这里,我们将由vocab_size=5000唯一单词标记组成的输入序列嵌入到大小为 32 的密集向量中。

    请随意重读最佳实践 14–使用带有神经网络的单词嵌入从文本数据中提取特征摘自第 11 章机器学习最佳实践

  3. Now here comes the recurrent layer, the LSTM layer specifically:

    >>> model.add(layers.LSTM(50)) 
    

    这里,我们只使用一个具有 50 个节点的递归层。

  4. 之后,我们添加输出层以及一个 sigmoid 激活函数,因为我们正在处理一个二进制分类问题:

    >>> model.add(layers.Dense(1, activation='sigmoid')) 
    
  5. 显示模型摘要,再次检查图层:

    >>> print(model.summary())
    Model: "sequential"
    _________________________________________________________________
    Layer (type)                 Output Shape              Param #   
    =================================================================
    embedding (Embedding)        (None, None, 32)          160000    
    _________________________________________________________________
    lstm (LSTM)                  (None, 50)                16600     
    _________________________________________________________________
    dense (Dense)                (None, 1)                 51        
    =================================================================
    Total params: 176,651
    Trainable params: 176,651
    Non-trainable params: 0
    _________________________________________________________________ 
    
  6. 接下来,我们用 Adam 优化器编译模型,并使用二进制交叉熵作为优化目标:

    >>> model.compile(loss='binary_crossentropy',
    ...               optimizer='adam',
    ...               metrics=['accuracy']) 
    
  7. 最后,我们训练三个时期的批量大小为 64 的模型:

    >>> batch_size = 64
    >>> n_epoch = 3
    >>> model.fit(X_train, y_train,
    ...           batch_size=batch_size,
    ...           epochs=n_epoch,
    ...           validation_data=(X_test, y_test))
    Train on 25000 samples, validate on 25000 samples
    Epoch 1/3
    391/391 [==============================] - 70s 178ms/step - loss: 0.4284 - accuracy: 0.7927 - val_loss: 0.3396 - val_accuracy: 0.8559
    Epoch 2/3
    391/391 [==============================] - 69s 176ms/step - loss: 0.2658 - accuracy: 0.8934 - val_loss: 0.3034 - val_accuracy: 0.8730
    Epoch 3/3
    391/391 [==============================] - 69s 177ms/step - loss: 0.2283 - accuracy: 0.9118 - val_loss: 0.3118 - val_accuracy: 0.8705 
    
  8. Using the trained model, we evaluate the classification accuracy on the testing set:

    >>> acc = model.evaluate(X_test, y_test, verbose = 0)[1]
    >>> print('Test accuracy:', acc)
    Test accuracy: 0.8705199956893921 
    

    我们获得了 87.05%的测试准确率。

堆叠多个 LSTM 层

现在,让我们尝试堆叠两个循环层。下图显示了如何堆叠两个循环层:

图 13.12:展开两个堆叠的循环层

让我们看看是否可以通过以下步骤构建多层 RNN 模型来超越以前的准确性:

  1. Initiate a new model and add an embedding layer, two LSTM layers, and an output layer:

    >>> model = models.Sequential()
    >>> model.add(layers.Embedding(vocab_size, embedding_size))
    >>> model.add(layers.LSTM(50, return_sequences=True, dropout=0.2))
    >>> model.add(layers.LSTM(50, dropout=0.2))
    >>> model.add(layers.Dense(1, activation='sigmoid')) 
    

    这里,第一个 LSTM 层带有return_sequences=True,因为我们需要将其整个输出序列馈送到第二个 LSTM 层。我们还为两个 LSTM 图层添加了 20%的缺失,以减少过度拟合,因为我们将有更多参数需要训练:

    >>> print(model.summary())
    Model: "sequential_1"
    _________________________________________________________________
    Layer (type)                 Output Shape              Param #   
    =================================================================
    embedding_1 (Embedding)      (None, None, 32)          160000    
    _________________________________________________________________
    lstm_1 (LSTM)                (None, None, 50)          16600     
    _________________________________________________________________
    lstm_2 (LSTM)                (None, 50)                20200     
    _________________________________________________________________
    dense_1 (Dense)              (None, 1)                 51        
    =================================================================
    Total params: 196,851
    Trainable params: 196,851
    Non-trainable params: 0
    _________________________________________________________________
    None 
    
  2. 同样,我们用 Adam 优化器以0.003学习速率

    >>> optimizer = optimizers.Adam(lr=0.003)
    >>> model.compile(loss='binary_crossentropy',
    ...               optimizer=optimizer,
    ...               metrics=['accuracy']) 
    

    编译模型

  3. 然后,我们训练 7 个时期的堆叠模型:

    >>> n_epoch = 7
    >>> model.fit(X_train, y_train,
    ...           batch_size=batch_size,
    ...           epochs=n_epoch,
    ...           validation_data=(X_test, y_test))
    Train on 25000 samples, validate on 25000 samples
    Epoch 1/7
    391/391 [==============================] - 139s 356ms/step - loss: 0.4755 - accuracy: 0.7692 - val_loss: 0.3438 - val_accuracy: 0.8511
    Epoch 2/7
    391/391 [==============================] - 140s 357ms/step - loss: 0.3272 - accuracy: 0.8631 - val_loss: 0.3407 - val_accuracy: 0.8573
    Epoch 3/7
    391/391 [==============================] - 137s 350ms/step - loss: 0.3042 - accuracy: 0.8782 - val_loss: 0.3436 - val_accuracy: 0.8580
    Epoch 4/7
    391/391 [==============================] - 136s 349ms/step - loss: 0.2468 - accuracy: 0.9028 - val_loss: 0.6771 - val_accuracy: 0.7860
    Epoch 5/7
    391/391 [==============================] - 137s 350ms/step - loss: 0.2201 - accuracy: 0.9117 - val_loss: 0.3273 - val_accuracy: 0.8684
    Epoch 6/7
    391/391 [==============================] - 137s 349ms/step - loss: 0.1867 - accuracy: 0.9278 - val_loss: 0.3352 - val_accuracy: 0.8736
    Epoch 7/7
    391/391 [==============================] - 138s 354ms/step - loss: 0.1586 - accuracy: 0.9398 - val_loss: 0.3335 - val_accuracy: 0.8756 
    
  4. Finally, we verify the test accuracy:

    >>> acc = model.evaluate(X_test, y_test, verbose=0)[1]
    >>> print('Test accuracy with stacked LSTM:', acc)
    Test accuracy with stacked LSTM: 0.8755999803543091 
    

    获得了 87.56%的较好测试精度。

至此,我们刚刚使用 RNNs 完成了评论情感分类项目。rnn 是多对一结构中的。在下一个项目中,我们将开发一个多对多结构下的 RNN 来写一部“小说”

用无线网络写你自己的战争与和平

在这个项目中,我们将研究一个有趣的语言建模问题——文本生成。

基于 RNN 的文本生成器可以写任何东西,这取决于我们输入的文本。训练文本可以来自小说,比如《T0》《权力的游戏》《T1》,莎士比亚的一首诗,或者《T2》《黑客帝国》《T3》的电影剧本。如果模型训练有素,生成的人工文本应该和原始文本相似(但不完全相同)。在这一部分,我们要用俄罗斯作家列夫·托尔斯泰的小说《RNNs》来写我们自己的《T4》《战争与和平》。随意在你喜欢的任何一本书上训练你自己的神经网络。

在构建训练集之前,我们将从数据采集和分析开始。之后,我们将建立并训练一个用于文本生成的 RNN 模型。

获取并分析训练数据

我推荐从目前不受版权保护的书籍中下载文本数据进行训练。古腾堡工程(www.gutenberg.org)是一个非常适合的地方。它提供超过 60,000 本版权已经过期的免费电子书。

原作品*《战争与和平》*可以从www.gutenberg.org/ebooks/2600下载,但注意会有一些清理,比如去掉目录中多余的开头部分“古腾堡工程电子书”,以及需要的纯文本 UTF-8 文件(www.gutenberg.org/files/2600/…)的多余附录“战争与和平古腾堡工程电子书的结尾”。所以,我们就不这样做了,直接从https://cs . Stanford . edu/people/karpathy/char-rnn/war peace _ input . txt下载清理后的文本文件。让我们开始吧:

  1. 首先,我们读取文件并将文本转换为小写:

    >>> training_file = 'warpeace_input.txt'
    >>> raw_text = open(training_file, 'r').read()
    >>> raw_text = raw_text.lower() 
    
  2. 然后,我们通过打印出前 200 个字符来快速查看训练文本数据:

    >>> print(raw_text[:200])
    "well, prince, so genoa and lucca are now just family estates of the 
    buonapartes. but i warn you, if you don't tell me that this means war,
    if you still try to defend the infamies and horrors perpetr 
    
  3. Next, we count the number of unique words:

    >>> all_words = raw_text.split()
    >>> unique_words = list(set(all_words))
    >>> print(f'Number of unique words: {len(unique_words)}')
    Number of unique words: 39830 
    

    然后,我们统计总的字符数:

    >>> n_chars = len(raw_text)
    >>> print(f'Total characters: {n_chars}')
    Total characters: 3196213 
    
  4. 从这 300 万个字符中,我们获得了唯一的字符,如下所示:

    >>> chars = sorted(list(set(raw_text)))
    >>> n_vocab = len(chars)
    >>> print(f'Total vocabulary (unique characters): {n_vocab}')
    Total vocabulary (unique characters): 57
    >>> print(chars)
    ['\n', ' ', '!', '"', "'", '(', ')', '*', ',', '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '=', '?', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'à', 'ä', 'é', 'ê', '\ufeff'] 
    

原始训练文本由 57 个独特的字符组成,并由近 40,000 个独特的单词组成。生成单词需要一步计算 40000 个概率,远比生成字符困难,后者只需要一步计算 57 个概率。因此,我们将一个字符视为一个标记,这里的词汇由 57 个字符组成。

那么,我们如何将字符输入 RNN 模型并生成输出字符呢?让我们在下一节看到。

构建 RNN 文本生成器的训练集

回想一下,在同步的“多对多”RNN 中,网络接收一个序列并同时产生一个序列;该模型捕捉序列中元素之间的关系,并基于学习到的模式再现新的序列。至于我们的文本生成器,我们可以输入固定长度的字符序列,并让它生成相同长度的序列,其中每个输出序列是从其输入序列移位的一个字符。以下示例将帮助您更好地理解这一点:

假设我们有一个原始文本样本“学习”,我们希望序列长度为 5。在这里,我们可以有一个输入序列“learn”和一个输出序列“earni”我们可以将它们放入网络,如下所示:

图 13.13:给 RNN 喂一套训练设备(“学习”、“恩尼”)

我们刚刚构建了一个训练样本(“learn”“earni”)。同样,从整个原始文本中构造训练样本,首先需要将原始文本拆分成固定长度的序列,X;然后,我们需要忽略原始文本的第一个字符,并将其拆分成相同长度的序列, Y 。来自 X 的序列是训练样本的输入,而来自 Y 的对应序列是样本的输出。假设我们有一个原始文本样本,“通过示例进行机器学习”,我们将序列长度设置为 5。我们将构建以下训练样本:

图 13.14:从“示例机器学习”构建的训练样本

这里,□表示空间。请注意,剩下的子序列“le”不够长,所以我们干脆放弃它。

我们还需要对输入和输出字符进行一次性编码,因为神经网络模型只接受数字数据。我们只需将 57 个唯一字符映射到从 0 到 56 的索引,如下所示:

>>> index_to_char = dict((i, c) for i, c in enumerate(chars))
>>> char_to_index = dict((c, i) for i, c in enumerate(chars))
>>> print(char_to_index)
{'\n': 0, ' ': 1, '!': 2, '"': 3, "'": 4, '(': 5, ')': 6, '*': 7, ',': 8, '-': 9, '.': 10, '/': 11, '0': 12, '1': 13, '2': 14, '3': 15, '4': 16, '5': 17, '6': 18, '7': 19, '8': 20, '9': 21, ':': 22, ';': 23, '=': 24, '?': 25, 'a': 26, 'b': 27, 'c': 28, 'd': 29, 'e': 30, 'f': 31, 'g': 32, 'h': 33, 'i': 34, 'j': 35, 'k': 36, 'l': 37, 'm': 38, 'n': 39, 'o': 40, 'p': 41, 'q': 42, 'r': 43, 's': 44, 't': 45, 'u': 46, 'v': 47, 'w': 48, 'x': 49, 'y': 50, 'z': 51, 'à': 52, 'ä': 53, 'é': 54, 'ê': 55, '\ufeff': 56} 

例如,字符“c”变成长度为 57 的向量,索引 28 中的“1”和所有其他索引中的“0”s;字符“h”变成长度为 57 的向量,在索引 33 中带有“1”,在所有其他索引中带有“0”s。

现在字符查找字典已经准备好了,我们可以构建整个训练集,如下所示:

>>> import numpy as np
>>> seq_length = 160
>>> n_seq = int(n_chars / seq_length) 

这里,我们将序列长度设置为160并获取n_seq训练样本。接下来,我们初始化训练输入和输出,它们都是形状(样本数、序列长度、特征维数):

>>> X = np.zeros((n_seq, seq_length, n_vocab))
>>> Y = np.zeros((n_seq, seq_length, n_vocab)) 

Keras 中的 RNN 模型要求输入和输出序列的形状符合形状(样本数、序列长度、特征维数)。

现在,对于每个n_seq样本,我们将“1”分配给存在相应字符的输入和输出向量的索引:

>>> for i in range(n_seq):
...     x_sequence = raw_text[i * seq_length : 
                              (i + 1) * seq_length]
...     x_sequence_ohe = np.zeros((seq_length, n_vocab))
...     for j in range(seq_length):
...             char = x_sequence[j]
...             index = char_to_index[char]
...             x_sequence_ohe[j][index] = 1.
...     X[i] = x_sequence_ohe
...     y_sequence = raw_text[i * seq_length + 1 : (i + 1) * 
                                                 seq_length + 1]
...     y_sequence_ohe = np.zeros((seq_length, n_vocab))
...     for j in range(seq_length):
...             char = y_sequence[j]
...             index = char_to_index[char]
...             y_sequence_ohe[j][index] = 1.
...     Y[i] = y_sequence_ohe 

接下来,看看构建的输入和输出示例的形状:

>>> X.shape
(19976, 160, 57)
>>> Y.shape
(19976, 160, 57) 

同样,每个样本(输入或输出序列)由 160 个元素组成。每个元素都是一个 57 维的一维热编码向量。

我们终于准备好了训练集,是时候构建和适应 RNN 模式了。让我们在接下来的两个部分中完成这项工作。

构建 RNN 文本生成器

在本节中,我们将构建一个 RNN,它有两个堆叠的循环层。对于复杂的问题,例如文本生成,这比具有单一循环层的 RNN 具有更强的预测能力。让我们开始吧:

  1. 首先,我们导入所有必要的模块,并固定一个随机种子:

    >>> import tensorflow as tf
    >>> from tensorflow.keras import layers, models, losses, optimizers
    >>> tf.random.set_seed(42) 
    
  2. 每个循环层包含 700 个单元,具有 0.4 的脱落率和 tanh 激活功能:

    >>> hidden_units = 700
    >>> dropout = 0.4 
    
  3. 我们指定了其他超参数,包括批次大小 100 和时期数量 300:

    >>> batch_size = 100
    >>> n_epoch= 300 
    
  4. Now, we create the RNN model, as follows:

    >>> model = models.Sequential()
    >>> model.add(layers.LSTM(hidden_units, input_shape=(None, n_vocab), return_sequences=True, dropout=dropout))
    >>> model.add(layers.LSTM(hidden_units, return_sequences=True, dropout=dropout))
    >>> model.add(layers.TimeDistributed(layers.Dense(n_vocab, activation='softmax'))) 
    

    有几件事值得研究:

    • return_sequences=True对于第一个递归层:第一个递归层的输出是一个序列,这样我们就可以把第二个递归层叠加在上面。
    • return_sequences=True对于第二递归层:第二递归层的输出是一个序列,启用多对多结构。
    • Dense(n_vocab, activation='softmax'):输出序列的每个元素都是一个热门的编码向量,因此 softmax 激活用于计算单个字符的概率。
    • TimeDistributed:由于递归层的输出是一个序列,Dense层不接受序列输入,所以TimeDistributed被用作适配器,使得Dense层可以应用于输入序列的每个元素。
  5. Next, we compile the network. As for the optimizer, we choose RMSprop with a learning rate of 0.001:

    >>> optimizer = optimizers.RMSprop(lr=0.001)
    >>> model.compile(loss="categorical_crossentropy", 
                      optimizer=optimizer) 
    

    这里,损失函数是多类交叉熵。

  6. 让我们总结一下刚刚构建的模型:

    >>> print(model.summary())  
    Model: "sequential"
    _________________________________________________________________
    Layer (type)                 Output Shape              Param #   
    =================================================================
    lstm (LSTM)                  (None, None, 700)         2122400   
    _________________________________________________________________
    lstm_1 (LSTM)                (None, None, 700)         3922800   
    _________________________________________________________________
    time_distributed (TimeDistri (None, None, 57)          39957     
    =================================================================
    Total params: 6,085,157
    Trainable params: 6,085,157
    Non-trainable params: 0
    _________________________________________________________________ 
    

至此,我们已经刚刚完成的构建,并准备训练模型。我们将在下一节中讨论这个问题。

训练 RNN 文本生成器

如模型总结所示,我们有 600 多万个参数需要训练。因此,建议在图形处理器上训练模型。如果你内部没有 GPU,可以使用谷歌 Colab 提供的免费 GPU。您可以按照ml-book.now.sh/free-gpu-fo…的教程进行设置。

此外,对于需要长时间训练的深度学习模型,设置一些回调是一个很好的做法,以便在训练期间跟踪模型的内部状态和性能。在我们的项目中,我们使用以下回调:

  • 模型检查点:这将在每个纪元后保存模型。如果在培训过程中出现意外的问题,您不必重新培训模型。您可以简单地加载保存的模型,然后从那里继续训练。
  • 提前止损:我们在第八章人工神经网络预测股价中介绍过。
  • 定期用最新的模型生成文本:这样做,我们可以看到生成的文本是多么的合理。

我们使用这三个回调来训练我们的 RNN 模型,如下所示:

  1. 首先,我们导入必要的模块:

    >>> from tensorflow.keras.callbacks import Callback, ModelCheckpoint, EarlyStopping 
    
  2. Then, we define the model checkpoint callback:

    >>> file_path =  
            "weights/weights_epoch_{epoch:03d}_loss_{loss:.4f}.hdf5"
    >>> checkpoint = ModelCheckpoint(file_path, monitor='loss', 
                       verbose=1, save_best_only=True, mode='min') 
    

    模型检查点将与由纪元号和训练损失组成的文件名一起保存。

  3. 之后,我们创建一个提前停止回调,如果连续 50 个时期验证损失没有减少,就停止训练:

    >>> early_stop = EarlyStopping(monitor='loss', min_delta=0, 
                                   patience=50, verbose=1, mode='min') 
    
  4. Next, we develop a helper function that generates text of any length, given a model:

    >>> def generate_text(model, gen_length, n_vocab, index_to_char):
    ...     """
    ...     Generating text using the RNN model
    ...     @param model: current RNN model
    ...     @param gen_length: number of characters we want to generate
    ...     @param n_vocab: number of unique characters
    ...     @param index_to_char: index to character mapping
    ...     @return: string of text generated
    ...     """
    ...     # Start with a randomly picked character
    ...     index = np.random.randint(n_vocab)
    ...     y_char = [index_to_char[index]]
    ...     X = np.zeros((1, gen_length, n_vocab))
    ...     for i in range(gen_length):
    ...         X[0, i, index] = 1.
    ...         indices = np.argmax(model.predict(
                       X[:, max(0, i - seq_length -1):i + 1, :])[0], 1)
    ...         index = indices[-1]
    ...         y_char.append(index_to_char[index])
    ...     return ''.join(y_char) 
    

    它从一个随机挑选的角色开始。然后,输入模型基于其先前生成的字符预测每个剩余的gen_length-1字符。

  5. Now, we define the callback class that generates text with the generate_text util function for every N epochs:

    >>> class ResultChecker(Callback):
    ...     def __init__(self, model, N, gen_length):
    ...         self.model = model
    ...         self.N = N
    ...         self.gen_length = gen_length
    ...
    ...     def on_epoch_end(self, epoch, logs={}):
    ...         if epoch % self.N == 0:
    ...             result = generate_text(self.model, 
                             self.gen_length, n_vocab, index_to_char)
    ...             print('\nMy War and Peace:\n' + result) 
    

    接下来,我们启动文本生成检查器回调:

    >>> result_checker = ResultChecker(model, 10, 500) 
    

    该模型将为每 10 个时代生成 500 个字符的文本。

  6. Now that all the callback components are ready, we can start training the model:

    >>> model.fit(X, Y, batch_size=batch_size, 
          verbose=1, epochs=n_epoch,callbacks=[
          result_checker, checkpoint, early_stop]) 
    

    我将在这里只演示 1、51、101 和 291 时代的结果:

    时代 1:

    Epoch 1/300
    200/200 [==============================] - 117s 584ms/step - loss: 2.8908
    My War and Peace:
    8 the tout to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to to t
    Epoch 00001: loss improved from inf to 2.89075, saving model to weights/weights_epoch_001_loss_2.8908.hdf5 
    

    纪元 51:

    Epoch 51/300
    200/200 [==============================] - ETA: 0s - loss: 1.7430
    My War and Peace:
    re and the same time the same time the same time he had not yet seen the first time that he was always said to him that the countess was sitting in the same time and the same time that he was so saying that he was saying that he was saying that he was saying that he was saying that he was saying that he was saying that he was saying that he was saying that he was saying that he was saying that he was saying that he was saying that he was saying that he was saying that he was saying that he was sa
    Epoch 00051: loss improved from 1.74371 to 1.74298, saving model to weights/weights_epoch_051_loss_1.7430.hdf5
    200/200 [==============================] - 64s 321ms/step - loss: 1.7430 
    

    纪元 101:

    Epoch 101/300
    200/200 [==============================] - ETA: 0s - loss: 1.6892
    My War and Peace:
    's and the same time and the same sonse of his life and her face was already in her hand.
    "what is it?" asked natasha. "i have not the post and the same to
    her and will not be able to say something to her and went to
    the door.
    "what is it?" asked natasha. "i have not the post and the same to her
    and that i shall not be able to say something to her and
    went on to the door.
    "what a strange in the morning, i am so all to say something to her,"
    said prince andrew, "i have not the post and the
    same
    Epoch 00101: loss did not improve from 1.68711
    200/200 [==============================] - 64s 321ms/step - loss: 1.6892 
    

    纪元 291:

    Epoch 291/300
    200/200 [==============================] - ETA: 0s - loss: 1.6136
    My War and Peace:
    à to the countess, who was sitting in the same way the sound of a sound of company servants were standing in the middle of the road.
    "what are you doing?" said the officer, turning to the princess with
    a smile.
    "i don't know what to say and want to see you."
    "yes, yes," said prince andrew, "i have not been the first to see you
    and you will be a little better than you are and we will be
    married. what a sin i want to see you."
    "yes, yes," said prince andrew, "i have not been the first to see yo
    Epoch 00291: loss did not improve from 1.61188
    200/200 [==============================] - 65s 323ms/step - loss: 1.6136 
    

在特斯拉 K80 GPU 上,每个纪元大约需要 60 秒。经过几个小时的训练,这个位于 RNN 的文本生成器可以写出一个真实有趣的《战争与和平》版本。这样,我们成功地使用了多对多类型的 RNN 来生成文本。

具有多对多结构的 RNN 是一种序列对序列(seq2seq)模型,它接收一个序列并输出另一个序列。一个典型的例子是机器翻译,将一种语言的单词序列转换成另一种语言的序列。state-of-the-art seq2seq 模型是 Transformer 模型,是谷歌大脑开发的。我们将在下一节简要讨论它。

使用 Transformer 模型促进语言理解

Transformer模型最早是在关注是你所需要的(arxiv.org/abs/1706.03…)中提出的。它可以有效地处理长期依赖,这在 LSTM 仍然是一个挑战。在本节中,我们将介绍 Transformer 的架构和构建模块,以及它最关键的部分:自我关注层。

探索变压器的架构

我们先来看看 Transformer 模型的高层架构(图片来自注意力是你需要的全部):

图 13.15:变压器架构

可以看到,变压器由两部分组成:编码器(左侧大矩形)和解码器(右侧大矩形)。编码器对输入序列进行加密。它有一个多头注意力层(我们接下来会谈到这一点)和一个常规前馈层。另一方面,解码器产生输出序列。它有一个屏蔽的多头注意力层,以及一个多头注意力层和一个常规前馈层。

在步骤 t 中,变压器模型输入步骤xT5】1、xT9】2、…、xt并输出步骤yT17】1、yT21】2、…、yT25】t-1 然后它预测 y t 。这与多对多的 RNN 模式没有什么不同。

多头关注层可能是您唯一觉得奇怪的地方,所以我们将在下一节中查看它。

理解自我关注

让我们讨论一下在下面的例子中自我关注层在转换器中是如何发挥关键作用的:

“我通过示例阅读了 Python 机器学习,它确实是一本很棒的书。”显然是指 Python 机器学习举例。当 Transformer 模型处理这句话的时候,自我关注会联想到 Python 机器学习举例。给定输入序列中的一个单词,自我注意允许模型在不同的注意力水平上观察序列中的其他单词,这有助于seq2seq任务中的语言理解和学习。

现在,让我们看看如何计算注意力分数。

如架构图所示,注意力层有三个输入向量:

  • 查询向量 Q ,表示序列中的查询词(即当前词)
  • 关键向量 K ,表示序列中的单个单词
  • 值向量 V ,也表示序列中的单个单词

这三个向量是在训练过程中训练的。

关注层的输出计算如下:

这里, d k 是关键向量的维数。以序列 python 机器学习举例为例;我们按照以下步骤计算第一个单词 python 的自我关注度:

  1. 我们计算序列中每个单词和单词 python 之间的点积。分别是 q 1kT8】1、qT12】1、kT16】2、qT20】1、kT24】3、qT28】1、 k 这里, q 1 是第一个词的查询向量, k 1k 5 分别是这五个词的关键向量。

  2. 我们通过除法和 softmax 激活来归一化结果点积:

  3. 然后,我们将得到的 softmax 向量乘以值向量,并对结果求和:

z 1 是序列中第一个单词 python 的自我关注得分。我们对序列中剩余的每个单词重复这个过程,以获得它的注意力得分。现在,你应该明白为什么这个叫做多头注意力:自我注意力不是只为一个词(一个步骤)计算的,而是为所有词(所有步骤)计算的。

然后,所有输出的注意力分数被连接并馈送到下游的常规前馈层。

在本节中,我们已经介绍了 Transformer 模型的主要概念。它已经成为自然语言处理中许多复杂问题的选择模型,如语音转文本、文本摘要和问答。通过增加注意力机制,Transformer 模型可以有效地处理顺序学习中的长期依赖关系。此外,它允许在训练过程中并行化,因为自我注意力可以针对单个步骤独立计算。

如果您有兴趣阅读更多内容,以下是使用 Transformer 取得的一些最新进展:

摘要

在这一章中,我们研究了两个自然语言处理项目:情感分析和使用神经网络的文本生成。我们首先详细解释了不同形式的输入和输出序列的循环机制和不同的 RNN 结构。您还学习了 LSTM 如何改进香草 RNNs。最后,作为奖励部分,我们介绍了 Transformer,这是一种最新的顺序学习模型。

在下一章中,我们将重点讨论第三类机器学习问题:强化学习。你将学习强化学习模式如何通过与环境互动来达到学习目标。

练习

  1. 使用双向递归层(自己学习足够容易),并将其应用到情感分析项目中。你能打败我们取得的成就吗?如果你想看一个例子,请阅读https://www . tensorflow . org/API _ docs/python/TF/keras/layers/双向
  2. 随意微调超参数,就像我们在第 8 章用人工神经网络预测股价一样,看看能否进一步提高分类性能。

十四、基于强化学习的复杂环境决策

在前一章中,我们重点介绍了用于顺序学习的 RNNs。本书的最后一章将讲述强化学习,这是本书开头提到的第三类机器学习任务。您将看到从经验中学习和通过与环境交互进行学习与之前介绍的有监督和无监督学习有何不同。

我们将在本章中讨论以下主题:

  • 为强化学习建立工作空间
  • 强化学习基础
  • 开放健身环境的模拟
  • 价值迭代和策略迭代算法
  • 政策评估和控制的蒙特卡罗方法
  • 问学习算法

设置工作环境

让我们开始设置工作环境,包括作为主框架的 PyTorch,以及 OpenAI Gym,这个工具包为你提供了各种环境来开发你的学习算法。

安装 PyTorch

py Torch(pytorch.org/)是脸书 AI 实验室在 Torch(torch.ch/)之上开发的新潮机器学习库。它提供了强大的计算图形和对图形处理器的高兼容性,以及简单友好的界面。PyTorch 在学术界迅速扩张,被越来越多的公司大量采用。下图(摘自horace.io/pytorch-vs-…)显示了 PyTorch 在顶级机器学习会议上的增长情况:

图 14.1:顶级机器学习会议中 PyTorch 论文的数量

在过去的一年里,在这些会议上,提到 PyTorch 的次数比 TensorFlow 还多。希望你有足够的动力和 PyTorch 一起工作。现在让我们看看如何正确安装它。

首先,在pytorch.org/get-started…页面的下表中,您可以为您的环境选择正确的配置:

图 14.2:使用系统配置安装 PyTorch

这里,我以本地运行的 Mac、Conda 和 Python 3.7(无 CUDA)为例,运行建议的命令行:

conda install pytorch torchvision -c pytorch 

接下来,您可以在 Python 中运行以下代码行来确认正确安装:

>>> import torch
>>> x = torch.empty(3, 4)
>>> print(x)
tensor([[7.8534e+34, 4.7418e+30, 5.9663e-02, 7.0374e+22],
        [3.5788e+01, 4.5825e-41, 4.0272e+01, 4.5825e-41],
        [0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00]]) 

这里,PyTorch 中的张量类似于 NumPy 中的 ndarrays,或者 TensorFlow 中的张量。我们刚刚创建了一个大小为 3 * 4 的张量。它是一个空矩阵,有一堆无意义的占位符浮动。同样,这与 NumPy 的空数组非常相似。

如果你想更熟悉 PyTorch,可以浏览官方教程pytorch.org/tutorials/#…中的入门章节。我建议你至少完成这两个:

到目前为止,我们已经成功建立了 PyTorch。让我们在下一节看看如何安装 OpenAI 健身房。

强化学习不限于 PyTorch。TensorFlow 总是一个不错的选择。在本书的最后一章学习趋势框架 PyTorch 是非常有益的。

安装 OpenAI 健身房

OpenAI 健身房(gym.openai.com/)是一个强大的开源工具包,用于开发和比较强化学习算法。它提供了多种环境来开发你的强化学习算法。是由专注于打造安全有益的人工通用智能 ( AGI )的非营利研究公司欧派(openai.com/)开发的。

安装健身房有两种方法。第一种是通过pip,如下:

pip install gym 

另一种方法是通过从 Git 存储库中克隆包并从那里安装它来从源构建:

git clone https://github.com/openai/gym
cd gym
pip install -e . 

安装后,您可以通过运行以下代码来检查可用的健身房环境:

>>> from gym import envs
>>> print(envs.registry.all())
dict_values([EnvSpec(Copy-v0), EnvSpec(RepeatCopy-v0), EnvSpec(ReversedAddition-v0), EnvSpec(ReversedAddition3-v0), EnvSpec(DuplicatedInput-v0), EnvSpec(Reverse-v0), EnvSpec(CartPole-v0), EnvSpec(CartPole-v1), EnvSpec(MountainCar-v0), EnvSpec(MountainCarContinuous-v0), EnvSpec(Pendulum-v0), EnvSpec(Acrobot-v1), EnvSpec(LunarLander-v2), EnvSpec(LunarLanderContinuous-v2), EnvSpec(BipedalWalker-v2), EnvSpec(BipedalWalkerHardcore-v2), EnvSpec(CarRacing-v0), EnvSpec(Blackjack-v0)
……
…… 

你可以在gym.openai.com/envs/看到完整的环境列表,包括步行、登月、赛车和雅达利游戏。随意玩健身房。

在对标不同的强化学习算法时,我们需要在标准化的环境中应用它们。健身房是一个拥有多种多样环境的完美场所。这类似于使用 MNIST、ImageNet 和汤森路透新闻等数据集作为有监督和无监督学习的基准。

健身房有一个易于使用的强化学习环境界面,我们可以编写代理与之交互。那么什么是强化学习呢?什么是特工?让我们在下一节看到。

用例子介绍强化学习

在这一章中,我将首先介绍强化学习的要素以及一个有趣的例子,然后将继续讨论我们如何衡量来自环境的反馈,并遵循解决强化学习问题的基本方法。

强化学习的要素

你可能在年轻的时候玩过超级马里奥(或索尼克)。在电子游戏中,你控制马里奥收集硬币,同时避开障碍物。如果马里奥撞到障碍物或掉进缺口,游戏就结束了。你会在游戏结束前尽可能多的得到硬币。

强化学习和超级马里奥游戏非常相似。强化学习就是学习做什么。它观察环境中的情况,并确定正确的行动,以便最大限度地获得数字奖励。以下是强化学习任务中的元素列表(我还将每个元素链接到超级马里奥和其他示例,以便更容易理解):

  • 环境:环境是任务或者模拟。在超级马里奥游戏中,游戏本身就是环境。在自动驾驶中,道路和交通就是环境。在 AlphaGo 下棋中,棋盘就是环境。环境的输入是从发送到代理的动作,输出是发送到代理的状态奖励
  • Agent:Agent 是根据强化学习模型采取动作的组件。它与环境交互并观察状态以输入模型。代理的目标是解决环境问题——找到一组最佳的行动来获得最大的回报。超级马里奥游戏中的代理是马里奥,自动驾驶汽车是用于自动驾驶的。
  • 动作:这是特工可能的动作。在强化学习任务中,当模型开始学习环境时,它通常是随机的。马里奥可能的动作包括左右移动、跳跃和蹲伏。
  • 状态:状态是对环境的观察。他们用数字的方式描述每一个时间步的情况。对于棋局,状态是棋盘上所有棋子的位置。对于超级马里奥,状态包括马里奥的坐标和时间框架中的其他元素。对于一个学习走路的机器人来说,它两条腿的位置就是状态。
  • 奖励:每次代理采取行动,都会收到来自环境的数字反馈。这个反馈叫做奖励。可以为正、负或零。例如,超级马里奥游戏中的奖励可以是+1(如果马里奥收集硬币),+2(如果他避开障碍物),-10(如果他碰到障碍物),或者 0(对于其他情况)。

下图总结了强化学习的过程:

图 14.3:强化学习过程

强化学习过程是一个迭代循环。一开始,代理从环境中观察初始状态 s 0 。然后代理采取行动, a 0 ,根据型号。代理移动后,环境现在处于新状态, s 1 ,并给出反馈奖励, R 1 。然后,代理采取一个动作,即 a 1 ,该动作由模型用输入 s 1R 1 计算得出。这个过程一直持续到终止、完成或永远。

强化学习模型的目标是使总回报最大化。那么如何计算总奖励呢?仅仅是通过总结所有时间步骤的奖励吗?让我们在下一节看到。

累积奖励

在时间步 t累计奖励(也称返回**)G1可以写成:**

这里, T 是终止时间步长或无穷大。 G t 表示在时间 t 采取行动att后的未来总奖励。在每个时间步骤 t 中,强化学习模型试图学习最佳可能的动作,以便最大化 G t

然而,在许多现实世界的情况下,事情并不是这样,我们只是简单地总结所有未来的回报。看看下面的例子:

股票 A 在第一天结束时上涨 6 美元,在第二天结束时下跌 5 美元。股票 B 在第一天下跌 5 美元,在第二天上涨 6 美元。两天后,两只股票都上涨了 1 美元。那么第一天开始你会买哪一个呢?很明显,股票 A 是因为你不会亏钱,如果你在第二天开始时卖掉它,你甚至可以获利 6 美元。

这两种股票的总回报是一样的,但是我们更喜欢股票 A,因为我们更关心即期回报,而不是远期回报。同样,在强化学习中,我们在遥远的未来对奖励进行折扣,折扣系数与时间范围相关联。更长的时间跨度对累积回报的影响应该更小。这是因为较长的时间跨度包含了更多不相关的信息,因此具有更高的方差。

我们定义了一个折扣系数,其值介于 0 和 1 之间。我们重写了包含折扣因素的累积奖励:

可以看到越大,折扣越小,反之亦然。如果,实际上没有折扣,模型基于所有未来奖励的总和来评估一个动作。如果,模型只关注即时奖励 R t +1

现在我们知道如何计算累计奖励,接下来要讲的是如何最大化。

强化学习方法

解决强化学习问题主要有两种方法,即寻找最优动作,使累积奖励最大化。一种是基于政策的方法,另一种是基于价值的方法。

一个策略是一个功能,将每个输入状态映射到一个动作:

它可以是确定性的,也可以是随机的:

  • 确定性:从输入状态到输出动作存在一对一映射
  • 随机:它给出了所有可能动作的概率分布

在基于策略的方法中,模型学习将每个输入状态映射到最佳动作的最优策略。

一个州的 V 定义为期望未来从该州收取的累计奖励:

在基于值的方法中,模型学习使输入状态的值最大化的最优值函数。换句话说,代理采取行动以达到实现最大值的状态。

在基于策略的算法中,模型从随机策略开始。然后计算该策略的价值函数。这个步骤叫做政策评估步骤。在此之后,基于价值函数找到了一个新的更好的策略。这是政策改进的一步。重复这两个步骤,直到找到最佳策略。而在基于值的算法中,模型从随机值函数开始。然后,它以迭代的方式找到一个新的和改进的值函数,直到它达到最优值函数。

我们了解到有两种主要的方法来解决强化学习问题。在下一节中,让我们看看如何分别以基于策略和基于值的方式,使用具体的算法,即动态规划方法,来解决一个具体的强化学习示例(FrozenLake)。

用动态规划求解 FrozenLake 环境

我们将在本节中重点介绍基于策略的和基于值的动态规划算法。但是让我们从模拟 FrozenLake 环境开始。

模拟冰湖环境

FrozenLake 是一个典型的 OpenAI 健身房环境,其中有离散的 T2 州。它是关于在网格中将代理从起始区块移动到目标区块,同时避免陷阱。电网要么是 4 * 4(gym.openai.com/envs/Frozen…,要么是 8 * 8(gym.openai.com/envs/Frozen…)。网格中有四种类型的图块:

  • S :起始牌。这是状态 0,并附带 0 奖励。
  • G :球门牌。它是 4 * 4 网格中的状态 15。它给予+1 奖励并终止一集。
  • F :冰冻的瓷砖。在 4 * 4 网格中,状态 1、2、3、4、6、8、9、10、13 和 14 是可行走的图块。它给出 0 奖励。
  • H :洞瓦。在 4 * 4 网格中,状态 5、7、11 和 12 是孔图块。它给出 0 奖励并终止一集。

在这里,意味着强化学习环境的模拟。它包含从初始状态到最终状态的状态列表,动作和奖励列表。在 4 * 4 FrozenLake 环境中,有 16 种可能的状态,因为代理可以移动到 16 个图块中的任何一个。有四种可能的动作:向左(0)、向下(1)、向右(2)和向上(3)。

这种环境的棘手之处在于,由于冰表面很滑,代理不会总是朝着它想要的方向移动,而是可以朝着任何其他可以行走的方向移动,或者在某些概率下保持不动。例如,它可能会向右移动,即使它打算向上移动。

现在,让我们按照以下步骤模拟 4 * 4 FrozenLake 环境:

  1. 要模拟任何 OpenAI 健身房环境,我们首先需要在表github.com/openai/gym/…中查找它的名字。在我们的例子中,我们得到“FrozenLake-v0”。

  2. We import the Gym library and create a FrozenLake instance:

    >>> import gym
    >>> env = gym.make("FrozenLake-v0")
    >>> n_state = env.observation_space.n
    >>> print(n_state)
    16
    >>> n_action = env.action_space.n
    >>> print(n_action)
    4 
    

    我们还获得了环境的维度。

  3. Every time we run a new episode, we need to reset the environment:

    >>> env.reset() 
    0 
    

    这意味着代理从状态 0 开始。同样,有 16 种可能的状态,0,1,…,15。

  4. We render the environment to display it:

    >>> env.render() 
    

    您将看到一个 4 * 4 矩阵,代表 FrozenLake 网格和代理所在的图块(状态 0):

    图 14.4:霜湖的初始状态

  5. Let's take a right action since it is walkable:

    >>> new_state, reward, is_done, info = env.step(2)
    >>> print(new_state)
    1
    >>> print(reward)
    0.0
    >>> print(is_done)
    False
    >>> print(info)
    {'prob': 0.3333333333333333} 
    

    代理向右移动到状态 1,概率为 33.33%,由于剧集尚未完成,因此获得 0 奖励。另请参见渲染结果:

    >>> env.render() 
    

    图 14.5:代理向右移动的结果

    您可能会得到完全不同的结果,因为代理可以以 33.33%的概率向下移动到状态 4,或者以 33.33%的概率停留在状态 0。

  6. Next, we define a function that simulates a FrozenLake episode under a given policy and returns the total reward (as an easy start, let's just assume discount factor ):

    >>> def run_episode(env, policy):
    ...     state = env.reset()
    ...     total_reward = 0
    ...     is_done = False
    ...     while not is_done:
    ...         action = policy[state].item()
    ...         state, reward, is_done, info = env.step(action)
    ...         total_reward += reward
    ...         if is_done:
    ...             break
    ...     return total_reward 
    

    这里,policy是 PyTorch 张量,.item()提取张量上某个元素的值。

  7. Now let's play around with the environment using a random policy. We will implement a random policy (where random actions are taken) and calculate the average total reward over 1,000 episodes:

    >>> n_episode = 1000
    >>> total_rewards = []
    >>> for episode in range(n_episode):
    ...     random_policy = torch.randint(high=n_action, size=(n_state,))
    ...     total_reward = run_episode(env, random_policy)
    ...     total_rewards.append(total_reward)
    ...
    >>> print(f'Average total reward under random policy: {sum(total_rewards)/n_episode}')
    Average total reward under random policy: 0.014 
    

    平均而言,如果我们采取随机行动,代理有 1.4%的机会达到目标。这告诉我们,解决 FrozenLake 环境并不像你想象的那么容易。

  8. As a bonus step, you can look into the transition matrix. The transition matrix contains probabilities of taking action a from state s then reaching . Take state 6 as an example:

    >>> print(env.env.P[6])
    {0: [(0.3333333333333333, 2, 0.0, False), (0.3333333333333333, 5, 0.0, True), (0.3333333333333333, 10, 0.0, False)], 1: [(0.3333333333333333, 5, 0.0, True), (0.3333333333333333, 10, 0.0, False), (0.3333333333333333, 7, 0.0, True)], 2: [(0.3333333333333333, 10, 0.0, False), (0.3333333333333333, 7, 0.0, True), (0.3333333333333333, 2, 0.0, False)], 3: [(0.3333333333333333, 7, 0.0, True), (0.3333333333333333, 2, 0.0, False), (0.3333333333333333, 5, 0.0, True)]} 
    

    返回字典 0、1、2、3 的键代表四种可能的动作。键值是与操作相关联的元组列表。元组的格式为(转移概率、新状态、奖励、是否为终端状态)。例如,如果代理打算从状态 6 采取动作 1(向下),它将以 33.33%的概率移动到状态 5 (H),并获得 0 奖励,剧集将因此结束;它将以 33.33%的概率移动到状态 10,并获得 0 奖励;它将以 33.33%的概率移动到状态 7 (H),并获得 0 奖励并终止该集。

我们在这一部分试验了随机策略,只有 1.4%的时间成功。但这让您为下一部分做好了准备,在下一部分中,我们将使用基于值的动态编程算法找到最佳策略,该算法称为值迭代算法

用数值迭代算法求解 FrozenLake

数值迭代是一种迭代算法。它以随机策略值 V 开始,然后基于贝尔曼最优性方程(en.wikipedia.org/wiki/Bellma…)迭代更新值,直到值收敛。

这些值通常很难完全收敛。因此,有两个收敛标准。一种是传递固定次数的迭代,比如 1000 次或 10000 次。另一种方法是指定一个阈值(如 0.0001 或 0.00001),如果所有值的变化都小于阈值,我们将终止该过程。

重要的是,在每次迭代中,它选择最大化策略值的操作,而不是取所有操作的期望值(平均值)。迭代过程可以表达如下:

这里,是最优值函数;表示通过采取动作 a 从状态 s 移动到状态的转移概率;而是状态下通过采取行动提供的奖励。

一旦我们获得了最优值,我们就可以很容易地相应地计算出最优策略:

让我们使用值迭代算法求解 FrozenLake 环境,如下所示:

  1. 首先我们设置 0.99 为折扣因子,0.0001 为收敛阈值:

    >>> gamma = 0.99
    >>> threshold = 0.0001 
    
  2. We develop the value iteration algorithm, which computes the optimal values:

    >>> def value_iteration(env, gamma, threshold):
    ...     """
    ...     Solve a given environment with value iteration algorithm
    ...     @param env: OpenAI Gym environment
    ...     @param gamma: discount factor
    ...     @param threshold: the evaluation will stop once values for all states are less than the threshold
    ...     @return: values of the optimal policy for the given environment
    ...     """
    ...     n_state = env.observation_space.n
    ...     n_action = env.action_space.n
    ...     V = torch.zeros(n_state)
    ...     while True:
    ...         V_temp = torch.empty(n_state)
    ...         for state in range(n_state):
    ...             v_actions = torch.zeros(n_action)
    ...             for action in range(n_action):
    ...                 for trans_prob, new_state, reward, _ in \
                                           env.env.P[state][action]:
    ...                     v_actions[action] += trans_prob * (
                                         reward + gamma * V[new_state])
    ...             V_temp[state] = torch.max(v_actions)
    ...         max_delta = torch.max(torch.abs(V - V_temp))
    ...         V = V_temp.clone()
    ...         if max_delta <= threshold:
    ...             break
    ...     return V 
    

    value_iteration 功能执行以下任务:

    • 从策略值全为 0 开始
    • 基于贝尔曼最优性方程更新值
    • 计算所有状态值的最大变化
    • 如果最大变化大于收敛阈值,则继续更新这些值
    • 否则,终止迭代过程并返回最后的值作为最优值
  3. 我们应用该算法来求解 FrozenLake 环境以及指定的参数:

    >>> V_optimal = value_iteration(env, gamma, threshold)
    Take a look at the resulting optimal values:
    >>> print('Optimal values:\n', V_optimal)
    Optimal values:
    tensor([0.5404, 0.4966, 0.4681, 0.4541, 0.5569, 0.0000, 0.3572, 0.0000, 0.5905, 0.6421, 0.6144, 0.0000, 0.0000, 0.7410, 0.8625, 0.0000]) 
    
  4. 因为我们有最优值,所以我们可以从这些值中提取最优策略。为此,我们开发了以下函数:

    >>> def extract_optimal_policy(env, V_optimal, gamma):
    ...     """
    ...     Obtain the optimal policy based on the optimal values
    ...     @param env: OpenAI Gym environment
    ...     @param V_optimal: optimal values
    ...     @param gamma: discount factor
    ...     @return: optimal policy
    ...     """
    ...     n_state = env.observation_space.n
    ...     n_action = env.action_space.n
    ...     optimal_policy = torch.zeros(n_state)
    ...     for state in range(n_state):
    ...         v_actions = torch.zeros(n_action)
    ...         for action in range(n_action):
    ...             for trans_prob, new_state, reward, _ in 
                                       env.env.P[state][action]:
    ...                 v_actions[action] += trans_prob * (
                               reward + gamma * V_optimal[new_state])
    ...         optimal_policy[state] = torch.argmax(v_actions)
    ...     return optimal_policy 
    
  5. Then we obtain the optimal policy based on the optimal values:

    >>> optimal_policy = extract_optimal_policy(env, V_optimal, gamma) 
    

    看看由此产生的最佳策略:

    >>> print('Optimal policy:\n', optimal_policy)
    Optimal policy:
    tensor([0., 3., 3., 3., 0., 3., 2., 3., 3., 1., 0., 3., 3., 2., 1., 3.]) 
    

    这意味着状态 0 下的最佳动作是 0(左),状态 1 下的 3(上)等。如果你看网格,这看起来不是很直观。但是请记住,网格很滑,代理可以向不同于期望方向的另一个方向移动。

  6. If you doubt that it is the optimal policy, you can run 1,000 episodes with the policy and gauge how good it is by checking the average reward, as follows:

    >>> n_episode = 1000
    >>> total_rewards = []
    >>> for episode in range(n_episode):
    ...     total_reward = run_episode(env, optimal_policy)
    ...     total_rewards.append(total_reward) 
    

    在这里,我们重用上一节中定义的run_episode函数。然后我们打印出平均奖励:

    >>> print('Average total reward under the optimal policy:', sum(total_rewards) / n_episode)
    Average total reward under the optimal policy: 0.75 
    

在由值迭代算法计算的最优策略下,代理有 75%的时间达到目标瓦片。我们可以用基于政策的方法做类似的事情吗?让我们在下一节看到。

用策略迭代算法求解 FrozenLake

策略迭代算法有两个组成部分,策略评估和策略改进。与值迭代类似,它从任意策略开始,然后是一系列迭代。

在每次迭代的策略评估步骤中,我们首先根据 Bellman 期望方程计算最新策略的值:

在策略改进步骤中,我们基于最新的策略值导出改进的策略,同样基于贝尔曼最优性方程:

这两个步骤重复进行,直到政策收敛。在收敛时,最新的策略及其值是最优策略和最优值。

让我们开发策略迭代算法,并使用它来解决 FrozenLake 环境,如下所示:

  1. We start with the policy_evaluation function that computes the values of a given policy:

    >>> def policy_evaluation(env, policy, gamma, threshold):
    ...  """
    ...     Perform policy evaluation
    ...     @param env: OpenAI Gym environment
    ...     @param policy: policy matrix containing actions and
            their probability in each state
    ...     @param gamma: discount factor
    ...     @param threshold: the evaluation will stop once values 
            for all states are less than the threshold
    ...     @return: values of the given policy
    ...  """
    ...     n_state = policy.shape[0]
    ...     V = torch.zeros(n_state)
    ...     while True:
    ...         V_temp = torch.zeros(n_state)
    ...         for state in range(n_state):
    ...             action = policy[state].item()
    ...             for trans_prob, new_state, reward, _ in \
                                         env.env.P[state][action]:
    ...                 V_temp[state] += trans_prob * (
                                         reward + gamma * V[new_state])
    ...         max_delta = torch.max(torch.abs–V - V_temp))
    ...         V = V_temp.clone()
    ...         if max_delta <= threshold:
    ...             break
    ...     return V 
    

    功能执行以下任务:

    • 用全 0 初始化策略值
    • 基于贝尔曼期望方程更新值
    • 计算所有状态值的最大变化
    • 如果最大变化大于阈值,则继续更新这些值
    • 否则,终止评估过程并返回最新值
  2. Next, we develop the second component, the policy improvement, in the following function:

    >>> def policy_improvement(env, V, gamma):
    ...  """"""
    ...     Obtain an improved policy based on the values
    ...     @param env: OpenAI Gym environment
    ...     @param V: policy values
    ...     @param gamma: discount factor
    ...     @return: the policy
    ...  """"""
    ...     n_state = env.observation_space.n
    ...     n_action = env.action_space.n
    ...     policy = torch.zeros(n_state)
    ...     for state in range(n_state):
    ...         v_actions = torch.zeros(n_action)
    ...         for action in range(n_action):
    ...             for trans_prob, new_state, reward, _ in 
                                          env.env.P[state][action]:
    ...                 v_actions[action] += trans_prob * (
                                      reward + gamma * V[new_state])
    ...         policy[state] = torch.argmax(v_actions)
    ...     return policy 
    

    它基于贝尔曼最优性方程,从输入的策略值中推导出新的更好的策略。

  3. With both components ready, we now develop the whole policy iteration algorithm:

    >>> def policy_iteration(env, gamma, threshold):
    ...  """
    ...     Solve a given environment with policy iteration algorithm
    ...     @param env: OpenAI Gym environment
    ...     @param gamma: discount factor
    ...     @param threshold: the evaluation will stop once values for all states are less than the threshold
    ...     @return: optimal values and the optimal policy for the given environment
    ...  """
    ...     n_state = env.observation_space.n
    ...     n_action = env.action_space.n
    ...     policy = torch.randint(high=n_action, 
                                   size=(n_state,)).float()
    ...     while True:
    ...         V = policy_evaluation(env, policy, gamma, threshold)
    ...         policy_improved = policy_improvement(env, V, gamma)
    ...         if torch.equal(policy_improved, policy):
    ...             return V, policy_improved
    ...         policy = policy_improved 
    

    该功能执行以下任务:

    • 初始化随机策略
    • 执行策略评估以更新策略值
    • 执行策略改进以生成新策略
    • 如果新策略与旧策略不同,则更新策略并运行另一次策略评估和改进迭代
    • 否则,终止迭代过程并返回最新的策略及其值
  4. 接下来,我们使用策略迭代来解决 FrozenLake 环境:

    >>> V_optimal, optimal_policy = policy_iteration(env, gamma, threshold) 
    
  5. Finally, we display the optimal policy and its values:

    >>> pri't('Optimal values'\n', V_optimal)
    Optimal values:
    tensor([0.5404, 0.4966, 0.4681, 0.4541, 0.5569, 0.0000, 0.3572, 0.0000, 0.5905, 0.6421, 0.6144, 0.0000, 0.0000, 0.7410, 0.8625, 0.0000])
    >>> pri't('Optimal policy'\n', optimal_policy)
    Optimal policy:
    tensor([0., 3., 3., 3., 0., 3., 2., 3., 3., 1., 0., 3., 3., 2., 1., 3.]) 
    

    我们得到了与值迭代算法相同的结果。

我们刚刚用策略迭代算法解决了 FrozenLake 环境。您可能想知道如何在值迭代和策略迭代算法之间进行选择。请看下表:

表 14.1:在策略迭代和值迭代算法之间进行选择

我们用动态规划方法解决了一个强化学习问题。它们需要一个完全已知的环境转换矩阵和奖励矩阵。而且,对于具有多种状态的环境,它们的可扩展性有限。在下一节中,我们将继续使用蒙特卡罗方法的学习之旅,该方法不需要环境的先验知识,并且可扩展性更强。

执行蒙特卡罗学习

蒙特卡洛(MC)-基于的强化学习是一种无模型方法,这意味着不需要已知的转移矩阵和奖励矩阵。在本节中,您将了解 21 点环境下的 MC 策略评估,并使用 MC 控制算法解决该环境。21 点是一个典型的过渡矩阵未知的环境。我们先来模拟一下二十一点的环境。

模拟 21 点环境

二十一点是一种流行的纸牌游戏。游戏有以下规则:

  • 玩家与庄家竞争,如果他们牌的总价值较高且不超过 21,则获胜。
  • 从 2 到 10 的牌的值从 2 到 10。
  • 卡 J、K 和 Q 的值为 10。
  • ace 的值可以是 1 或 11(称为“可用”ace)。
  • 开始时,双方都随机得到两张牌,但只有一张庄家的牌透露给玩家。玩家可以请求额外的卡(称为击中)或停止拥有更多的卡(称为)。在玩家叫牌之前,如果玩家的牌总数超过 21 张(称为半身像),玩家就会输。玩家坚持后,庄家继续抽牌,直到牌数总和达到 17。如果庄家的牌总数超过 21 张,玩家就赢了。如果双方都不失败,得分较高的一方将获胜,或者可能是平局。

健身房的 21 点环境(https://github . com/open ai/Gym/blob/master/Gym/envs/toy _ text/21 点. py )制定如下:

  • 一集的环境开始于每一方两张牌,并且只观察到庄家牌中的一张。
  • 如果有一场胜利或平局,一集就结束了。
  • 一集的最终奖励如果玩家赢了是+1,如果玩家输了是-1,如果有平局是 0。
  • 在每一回合中,玩家可以采取两个动作中的任何一个,击打(1)和击打(0)

现在,让我们模拟 21 点环境,探索其状态和动作:

  1. 首先创建一个Blackjack实例:

    >>> env = gym.make('Blackjack'v0') 
    
  2. Reset the environment:

    >>> env.reset()
    (7, 10, False) 
    

    它返回初始状态(三维向量):

    • 玩家当前点数(本例中为7)
    • 庄家的牌面点数(本例中为10)
    • 有没有可用的 ace(本例中为False)

    可用的王牌变量是True只有当玩家有一张可以算作 11 的王牌而不会造成半身像。如果玩家没有王牌,或者有王牌但是它爆了,这个状态变量就会变成False

    对于另一个状态示例(18,6,True),这意味着玩家有一张王牌被计为 11 和 7,并且庄家的透露牌值为 6。

  3. Let's now take some actions to see how the environment works. First, we take a hit action since we only have 7 points:

    >>> env.step(1)
    ((13, 10, False), 0.0, False, {}) 
    

    它返回一个状态(13, 10, False),一个 0 奖励,并且该集没有完成(如False)。

  4. 让我们再来一次因为我们只有 13 分:

    >>> env.step(1)
    ((19, 10, False), 0.0, False, {}) 
    
  5. We have 19 points and think it is good enough. Then we stop drawing cards by taking action stick (0):

    >>> env.step(0)
    ((19, 10, False), 1.0, True, {}) 
    

    庄家拿到一些牌,然后被抓。因此玩家获胜并获得+1 奖励。这集结束了。

随意玩转 21 点环境。一旦您对环境感到满意,您就可以进入下一部分,关于简单策略的 MC 策略评估。

执行蒙特卡罗策略评估

在上一节中,我们应用了动态规划来执行策略评估,这是策略的价值函数。然而,在大多数事先不知道转移矩阵的实际情况下,它是不起作用的。在这种情况下,我们可以使用 MC 方法评估价值函数。

为了估计价值函数,MC 方法使用经验平均回报代替预期回报(如在动态规划中)。计算经验平均收益有两种方法。一个是第一次访问,在所有剧集中,平均只返回一个州*的第一次出现的**。另一个是每次访问,它平均返回所有剧集中状态 s 的每次出现**。显然,第一次访问方法的计算量要少得多,因此更常用。在本章中,我将只介绍第一次访问方法。*****

**在这一节中,我们用一个简单的策略进行实验,我们不断添加新卡,直到总值达到 18(或者 19,或者 20,如果你喜欢的话)。我们对简单的政策进行首次访问 MC 评估,如下所示:

  1. We first need to define a function that simulates a Blackjack episode under the simple policy:

    >>> def run_episode(env, hold_score):
    ...     state = env.reset()
    ...     rewards = []
    ...     states = [state]
    ...     while True:
    ...         action = 1 if state[0] < hold_score else 0
    ...         state, reward, is_done, info = env.step(action)
    ...         states.append(state)
    ...         rewards.append(reward)
    ...         if is_done:
    ...             break
    ...     return states, rewards 
    

    在每集的每一轮中,如果当前分数小于hold_score或一根棍子,则代理将获得一次命中。

  2. In the MC settings, we need to keep track of states and rewards over all steps. And in first-visit value evaluation, we average returns only for the first occurrence of a state among all episodes. We define a function that evaluates the simple Blackjack policy with first-visit MC:

    >>> from collections import defaultdict
    >>> def mc_prediction_first_visit(env, hold_score, gamma, n_episode):
    ...     V = defaultdict(float)
    ...     N = defaultdict(int)
    ...     for episode in range(n_episode):
    ...         states_t, rewards_t = run_episode(env, hold_score)
    ...         return_t = 0
    ...         G = {}
    ...         for state_t, reward_t in zip(
                               states_t[1::-1], rewards_t[::-1]):
    ...             return_t = gamma * return_t + reward_t
    ...             G[state_t] = return_t
    ...         for state, return_t in G.items():
    ...             if state[0] <= 21:
    ...                 V[state] += return_t
    ...                 N[state] += 1
    ...     for state in V:
    ...         V[state] = V[state] / N[state]
    ...     return V 
    

    该功能执行以下任务:

    • 在带有功能run_episode的简单 21 点策略下运行n_episode
    • 对于每个集,计算每个州第一次访问的返回G
    • 对于每个状态,通过平均所有剧集的第一次返回来获得值
    • 返回结果值

    请注意,这里我们忽略玩家崩溃的状态,因为我们知道它们的值是-1。

  3. 我们指定hold_score18,折扣率为1作为一集 21 点足够短,将模拟 50 万集:

    >>> hold_score = 18
    >>> gamma = 1
    >>> n_episode = 500000 
    
  4. Now we plug in all variables to perform MC first-visit evaluation:

    >>> value = mc_prediction_first_visit(env, hold_score, gamma, n_episode) 
    

    然后,我们打印结果值:

    >>> print(value)
    defaultdict(<cla's 'fl'at'>, {(20, 6, False): 0.6923485653560042, (17, 5, False): -0.24390243902439024, (16, 5, False): -0.19118165784832453, (20, 10, False): 0.4326379146490474, (20, 7, False): 0.7686220540168588, (16, 6, False): -0.19249478804725503,
    ……
    ……
    (5, 9, False): -0.20612244897959184, (12, 7, True): 0.058823529411764705, (6, 4, False): -0.26582278481012656, (4, 8, False): -0.14937759336099585, (4, 3, False): -0.1680327868852459, (4, 9, False): -0.20276497695852536, (4, 4, False): -0.3201754385964912, (12, 8, True): 0.11057692307692307}) 
    

    我们刚刚计算了所有可能的 280 种状态的值:

    >>> print('Number of stat's:', len(value))
    Number of states: 280 
    

我们刚刚体验了在 21 点环境中使用 MC 方法在简单策略下计算 280 个状态的值。21 点环境的过渡矩阵事先并不知道。此外,如果我们采用动态规划方法,获得转移矩阵(大小为 280 * 280)将非常昂贵。在基于 MC 的解决方案中,我们只需要模拟一堆剧集并计算经验平均值。以类似的方式,我们将在下一节中搜索最佳策略。

执行策略蒙特卡罗控制

MC 控制用于为转移矩阵未知的环境寻找最优策略。MC 控制有两种类型,策略内和策略外。在政策方法中,我们执行政策,并对其进行迭代评估和改进;而在脱离策略的方法中,我们使用另一个策略生成的数据来训练最优策略。

在本节中,我们将重点关注政策方法。它的工作方式与策略迭代方法非常相似。它在以下两个阶段(评估和改进)之间迭代,直到收敛:

  • 在评估阶段,我们不是评估状态值,而是评估动作值,通常称为Q 值。Q 值 Q ( sa )是给定策略下,在状态 s 下采取动作 a 时,状态-动作对( sa )的值。评估可以以首次访问或每次访问的方式进行。
  • 在改进阶段,我们通过在每个状态分配最佳操作来更新策略:

现在,让我们按照以下步骤搜索具有策略上 MC 控制的最佳 21 点策略:

  1. We start with developing a function that executes an episode by taking the best actions under the given Q-values:

    >>> def run_episode(env, Q, n_action):
    ...     """
    ...     Run a episode given Q-values
    ...     @param env: OpenAI Gym environment
    ...     @param Q: Q-values
    ...     @param n_action: action space
    ...     @return: resulting states, actions and rewards for the entire episode
    ...     """
    ...     state = env.reset()
    ...     rewards = []
    ...     actions = []
    ...     states = []
    ...     action = torch.randint(0, n_action, [1]).item()
    ...     while True:
    ...         actions.append(action)
    ...         states.append(state)
    ...         state, reward, is_done, info = env.step(action)
    ...         rewards.append(reward)
    ...         if is_done:
    ...             break
    ...         action = torch.argmax(Q[state]).item()
    ...     return states, actions, rewards 
    

    这是改进阶段。具体来说,它执行以下任务:

    • 初始化一集
    • 采取随机行动作为探索的开始
    • 第一次动作后,根据给定的 Q 值表进行动作,即
    • 存储剧集中所有步骤的状态、动作和奖励,用于评估
  2. Next, we develop the on-policy MC control algorithm:

    >>> def mc_control_on_policy(env, gamma, n_episode):
    ...     """
    ...     Obtain the optimal policy with on-policy MC control method
    ...     @param env: OpenAI Gym environment
    ...     @param gamma: discount factor
    ...     @param n_episode: number of episodes
    ...     @return: the optimal Q-function, and the optimal policy
    ...     """
    ...     G_sum = defaultdict(float)
    ...     N = defaultdict(int)
    ...     Q = defaultdict(lambda: torch.empty(env.action_space.n))
    ...     for episode in range(n_episode):
    ...         states_t, actions_t, rewards_t = 
                           run_episode(env,  Q,  env.action_space.n)
    ...         return_t = 0
    ...         G = {}
    ...         for state_t, action_t, reward_t in zip(
                     states_t[::-1], actions_t[::-1],                                     rewards_t[::-1]):
    ...             return_t = gamma * return_t + reward_t
    ...             G[(state_t, action_t)] = return_t
    ...         for state_action, return_t in G.items():
    ...             state, action = state_action
    ...             if state[0] <= 21:
    ...                 G_sum[state_action] += return_t
    ...                 N[state_action] += 1
    ...                 Q[state][action] = 
                              G_sum[state_action] / N[state_action]
    ...     policy = {}
    ...     for state, actions in Q.items():
    ...         policy[state] = torch.argmax(actions).item()
    ...     return Q, policy 
    

    该功能执行以下任务:

    • 随机初始化 Q 值
    • 运行n_episode
    • 对于每一集,执行策略改进并获得训练数据;对结果状态、动作和奖励执行首次访问策略评估,并更新 Q 值
    • 最后,确定最优 Q 值和最优策略
  3. Now that the MC control function is ready, we compute the optimal policy:

    >>> gamma = 1
    >>> n_episode = 500000 
    >>> optimal_Q, optimal_policy = mc_control_on_policy(env, gamma, n_episode) 
    

    看看最佳策略:

    >>> print(optimal_policy)
    {(16, 8, True): 1, (11, 2, False): 1, (15, 5, True): 1, (14, 9, False): 1, (11, 6, False): 1, (20, 3, False): 0, (9, 6, False): 
    0, (12, 9, False): 0, (21, 2, True): 0, (16, 10, False): 1, (17, 5, False): 0, (13, 10, False): 1, (12, 10, False): 1, (14, 10, False): 0, (10, 2, False): 1, (20, 4, False): 0, (11, 4, False): 1, (16, 9, False): 0, (10, 8, 
    ……
    ……
    1, (18, 6, True): 0, (12, 2, True): 1, (8, 3, False): 1, (13, 3, True): 0, (4, 7, False): 1, (18, 8, True): 0, (6, 5, False): 1, (17, 6, True): 0, (19, 9, True): 0, (4, 4, False): 0, (14, 5, True): 1, (12, 6, True): 0, (4, 9, False): 1, (13, 4, True): 1, (4, 8, False): 1, (14, 3, True): 1, (12, 4, True): 1, (4, 6, False): 0, (12, 5, True): 0, (4, 2, False): 1, (4, 3, False): 1, (5, 4, False): 1, (4, 1, False): 0} 
    

你可能想知道这个最优策略是否真的是最优的,比之前的简单策略(保持在 18 点)更好。让我们分别在最优策略和简单策略下模拟 10 万次 21 点游戏:

  1. 我们从简单策略下模拟一集的函数开始:

    >>> def simulate_hold_episode(env, hold_score):
    ...     state = env.reset()
    ...     while True:
    ...         action = 1 if state[0] < hold_score else 0
    ...         state, reward, is_done, _ = env.step(action)
    ...         if is_done:
    ...             return reward 
    
  2. 接下来,我们在最优策略下进行模拟功能:

    >>> def simulate_episode(env, policy):
    ...     state = env.reset()
    ...     while True:
    ...         action = policy[state]
    ...         state, reward, is_done, _ = env.step(action)
    ...         if is_done:
    ...             return reward 
    
  3. We then run 100,000 episodes for both policies and keep track of their winning times:

    >>> n_episode = 100000
    >>> hold_score = 18
    >>> n_win_opt = 0
    >>> n_win_hold = 0
    >>> for _ in range(n_episode):
    ...     reward = simulate_episode(env, optimal_policy)
    ...     if reward == 1:
    ...         n_win_opt += 1
    ...     reward = simulate_hold_episode(env, hold_score)
    ...     if reward == 1:
    ...         n_win_hold += 1 
    

    我们将结果打印如下:

    >>> print(f'Winning probability:\nUnder the simple policy: {n_win_hold/n_episode}\nUnder the optimal policy: {n_win_opt/n_episode}')
    Winning probability:
    Under the simple policy: 0.39955
    Under the optimal policy: 0.42779 
    

    在最优策略下玩有 43%的胜算,而在简单策略下玩只有 40%的胜算。

在这一节中,我们用无模型算法 MC 学习来解决 21 点环境。在 MC 学习中,Q 值会一直更新到一集结束。这对于长流程来说可能是个问题。在下一节中,我们将讨论 Q 学习,它会更新每集每一步的 Q 值。你会看到它如何提高学习效率。

用 Q 学习算法求解出租车问题

q 学习也是无模型学习算法。它为一集的每一步更新 Q 功能。我们将演示如何使用 Q 学习来解决出租车环境。这是一个典型的情节相对较长的环境。因此,让我们首先模拟出租车环境。

模拟出租车环境

在出租车环境(gym.openai.com/envs/Taxi-v…)中,代理充当出租车司机,从一个地点接乘客,在目的地让乘客下车。

所有科目都在一个 5 * 5 的网格上。看看下面的例子:

图 14.6:出租车环境示例

某些颜色的瓷砖具有以下含义:

  • 黄色:空车位置(无乘客)
  • 蓝色:乘客所在位置
  • 紫色:乘客目的地
  • 绿色:有乘客的出租车位置

每集随机分配空车和乘客的起始位置以及乘客的目的地。

四个字母 R、Y、B 和 G 是唯一允许乘客上下车的四个位置。紫色的是目的地,蓝色的是乘客的位置。

出租车可以采取以下六种行动中的任何一种:

  • 0:向南移动
  • 1:向北移动
  • 2:向东移动
  • 3:向西移动
  • 4:接乘客
  • 5:让乘客下车

两块瓷砖之间有一根柱子“|”,防止出租车在两块瓷砖之间移动。

在每一步中,奖励遵循以下规则:

  • +20 用于将乘客送往目的地。这种情况下一集就结束了。而另一种一集就要结束的情况是有 200 步的时候。
  • -10 分,因为试图非法接送(不在 R、Y、B 或 G 中的任何一个上)。
  • -1 否则。

最后但同样重要的是,实际上有 500 种可能的状态:显然,出租车可以在 25 个瓦片中的任何一个上,乘客可以在 R、Y、B、G 中的任何一个上,或者在出租车内,目的地可以是 R、Y、B、G 中的任何一个;因此,我们有 25 * 5 * 4 = 500 种可能的状态。

现在让我们用环境来玩如下:

  1. First we create an instance of the Taxi environment:

    >>> env = gym.make('Taxi-v3')
    >>> n_state = env.observation_space.n
    >>> print(n_state)
    500
    >>> n_action = env.action_space.n
    >>> print(n_action)
    6 
    

    我们也知道状态是用 0 到 499 的整数表示的,有 6 种可能的动作。

  2. We reset the environment and render it:

    >>> env.reset() 
    262
    >>> env.render() 
    

    您将看到一个类似于下面的 5 * 5 网格:

    图 14.7:出租车环境的示例开始步骤

    乘客在蓝色 R 瓷砖上,目的地在紫色 y 上。

  3. Now let's go pick up the passenger by heading west for three tiles and north for two tiles (you will need to take different actions according to your initial state) then take the "pick-up" action:

    >>> print(env.step(3))
    (242, -1, False, {'prob': 1.0})
    >>> print(env.step(3))
    (222, -1, False, {'prob': 1.0})
    >>> print(env.step(3))
    (202, -1, False, {'prob': 1.0})
    >>> print(env.step(1))
    (102, -1, False, {'prob': 1.0})
    >>> print(env.step(1))
    (2, -1, False, {'prob': 1.0})
    >>> print(env.step(4))
    (18, -1, False, {'prob': 1.0}) 
    

    渲染环境:

    >>> env.render() 
    

    图 14.8:乘客在出租车内的状态示例

    出租车变绿,意味着乘客在出租车内。

  4. Now let's head to the destination by taking the "down" action four times (again, you will need to take your own set of actions) then executing a "drop-off":

    >>> print(env.step(0))
    (118, -1, False, {'prob': 1.0})
    >>> print(env.step(0))
    (218, -1, False, {'prob': 1.0})
    >>> print(env.step(0))
    (318, -1, False, {'prob': 1.0})
    >>> print(env.step(0))
    (418, -1, False, {'prob': 1.0})
    >>> print(env.step(5))
    (410, 20, True, {'prob': 1.0}) 
    

    最后,成功下车者将获得+20 奖励。

  5. 我们最终渲染环境:

    >>> env.render() 
    

图 14.9:乘客到达目的地的状态示例

可以采取一些随机动作,看看一个模型解决环境有多难。我们将在下一节讨论 Q 学习算法。

开发 Q 学习算法

Q-learning 是一种非策略学习算法,基于行为策略生成的数据优化 Q 值。行为策略是一种贪婪的策略,它采取行动为给定的州实现最高的回报。行为策略生成学习数据,目标策略(我们尝试优化的策略)基于以下等式更新 Q 值:

这里,是采取行动后的结果状态 a 从状态 sr 是关联奖励。表示行为策略产生最高 Q 值给定状态。最后,超参数分别是学习率和折扣因子。

从另一个策略产生的经验中学习,使 Q-learning 能够在一集的每一步中优化其 Q 值。我们从贪婪策略中获取信息,并使用这些信息立即更新目标值。

还有一点需要注意的是,目标策略是ε-贪婪的,这意味着它采取的随机动作的概率为(值从 0 到 1)采取的贪婪动作的概率为。ε-贪婪政策将开发探索相结合:在探索不同行动的同时,利用最佳行动。

现在是时候开发 Q 学习算法来解决 Taxi 环境了:

  1. We start with defining the epsilon-greedy policy:

    >>> def gen_epsilon_greedy_policy(n_action, epsilon):
    ...     def policy_function(state, Q):
    ...         probs = torch.ones(n_action) * epsilon / n_action
    ...         best_action = torch.argmax(Q[state]).item()
    ...         probs[best_action] += 1.0 - epsilon
    ...         action = torch.multinomial(probs, 1).item()
    ...         return action
    ...     return policy_function 
    

    给定|A|个可能的动作,以概率采取每个动作,以附加概率选择具有最高状态-动作值的动作。

  2. Now we create an instance of the epsilon-greedy-policy:

    >>> epsilon = 0.1
    >>> epsilon_greedy_policy = gen_epsilon_greedy_policy(env.action_space.n, epsilon) 
    

    这里,,就是勘探比。

  3. Next, we develop the Q-learning algorithm:

    >>> def q_learning(env, gamma, n_episode, alpha):
    ...     """
    ...     Obtain the optimal policy with off-policy Q-learning method
    ...     @param env: OpenAI Gym environment
    ...     @param gamma: discount factor
    ...     @param n_episode: number of episodes
    ...     @return: the optimal Q-function, and the optimal policy
    ...     """
    ...     n_action = env.action_space.n
    ...     Q = defaultdict(lambda: torch.zeros(n_action))
    ...     for episode in range(n_episode):
    ...         state = env.reset()
    ...         is_done = False
    ...         while not is_done:
    ...             action = epsilon_greedy_policy(state, Q)
    ...             next_state, reward, is_done, info = 
                                              env.step(action)
    ...             delta = reward + gamma * torch.max(Q[next_state]) - 
                                          Q[state][action]
    ...             Q[state][action] += alpha * delta
    ...             length_episode[episode] += 1 
    ...             total_reward_episode[episode] += reward
    ...             if is_done:
    ...                 break
    ...             state = next_state
    ...     policy = {}
    ...     for state, actions in Q.items():
    ...         policy[state] = torch.argmax(actions).item()
    ...     return Q, policy 
    

    我们首先初始化 Q 表。然后在每一集中,我们让代理遵循ε-贪婪策略采取行动,并基于非策略学习方程更新每一步的 Q 函数。我们运行n_episode集,最终获得最优策略和 Q 值。

  4. 然后我们启动两个变量来存储 1000 集的每一集的表现、集长(一集的步数)和总奖励:

    >>> n_episode = 1000
    >>> length_episode = [0] * n_episode
    >>> total_reward_episode = [0] * n_episode 
    
  5. Finally, we perform Q-learning to obtain the optimal policy for the Taxi problem:

    >>> gamma = 1
    >>> alpha = 0.4
    >>> optimal_Q, optimal_policy = q_learning(env, gamma, n_episode, alpha) 
    

    这里是折扣率,学习率

  6. After 1,000 episodes of learning, we plot the total rewards over episodes as follows:

    >>> import matplotlib.pyplot as plt
    >>> plt.plot(total_reward_episode)
    >>> plt.title('Episode reward over time')
    >>> plt.xlabel('Episode')
    >>> plt.ylabel('Total reward')
    >>> plt.ylim([-200, 20])
    >>> plt.show() 
    

    有关最终结果,请参考以下屏幕截图:

    图 14.10:剧集总奖励

    总奖励在学习中不断提高。而且 600 集后都在+5 左右。

  7. We also plot the lengths over episodes as follows:

    >>> plt.plot(length_episode)
    >>> plt.title('Episode length over time')
    >>> plt.xlabel('Episode')
    >>> plt.ylabel('Length')
    >>> plt.show() 
    

    有关最终结果,请参考以下屏幕截图:

图 14.11:剧集长度

可以看到,剧集长度从最大 200 集减少到 10 集左右,模型收敛在 600 集左右。这意味着经过训练,模型能够在 10 步左右解决问题。

在这一节中,我们用非政策问答学习解决了出租车问题。该算法通过学习贪婪策略产生的经验来优化每一步的 Q 值。

摘要

我们从设置工作环境开始这一章。之后,我们学习了强化学习的基础知识和一些例子。在探索了 FrozenLake 环境之后,我们使用了两种动态规划算法:值迭代和策略迭代来解决这个问题。我们谈到了蒙特卡罗学习,并将其用于 21 点环境中的值逼近和控制。最后,开发了 Q 学习算法,解决了出租车问题。

练习

  1. 能否尝试用价值迭代或策略迭代算法求解 8 * 8 FrozenLake 环境?
  2. 你能实现每次访问 MC 策略评估算法吗?
  3. 你能在 Q 学习算法中使用不同的探索比看看事情是如何变化的吗?**