JavaScript 深度学习(三)(4)

简介: JavaScript 深度学习(三)

JavaScript 深度学习(三)(3)https://developer.aliyun.com/article/1516963

第十章:序列和文本的深度学习

本章包括

  • 顺序数据与非顺序数据有何不同
  • 哪些深度学习技术适用于涉及序列数据的问题
  • 如何在深度学习中表示文本数据,包括独热编码,多热编码和词嵌入
  • 什么是循环神经网络,以及为什么它们适合于顺序问题
  • 什么是一维卷积,以及为什么它是循环神经网络的一个有吸引力的替代品
  • 序列到序列任务的独特特性以及如何使用注意力机制来解决它们

本章重点介绍涉及序列数据的问题。序列数据的本质是其元素的排序。您可能已经意识到,我们之前已经处理过序列数据。具体来说,我们在第七章介绍的 Jena-weather 数据是序列数据。数据可以表示为数字数组的数组。外部数组的顺序当然很重要,因为测量是随着时间的推移而进行的。如果您改变外部数组的顺序——例如,上升的气压趋势变成下降的气压趋势——如果您尝试预测未来的天气,它就具有完全不同的含义。序列数据无处不在:股票价格,心电图(ECG)读数,软件代码中的字符串,视频的连续帧以及机器人采取的行动序列。将这些与非序列数据相对比,比如第三章中的鸢尾花:如果您改变这四个数字特征(萼片和花瓣的长度和宽度)的顺序并不重要。^([1])

¹

说服自己这确实是事实,练习本章末尾的练习 1。

本章的第一部分将介绍我们在第一章中提到的一种引人入胜的模型——循环神经网络(RNNs),它们专门设计用于从序列数据中学习。我们将理解循环神经网络的特殊特性,以及这些模型敏感于元素的排序和相关信息的直觉。

本章的第二部分将讨论一种特殊的序列数据:文本,这可能是最常见的序列数据(尤其是在网络环境中!)。我们将首先研究深度学习中如何表示文本以及如何在这些表示上应用循环神经网络。然后我们将转向一维卷积神经网络,并讨论它们为何也在处理文本时非常强大,以及它们如何对某些类型的问题是循环神经网络的有吸引力的替代品。

在本章的最后一部分,我们将进一步探讨比预测数字或类别稍微复杂一点的基于序列的任务。特别是,我们将涉及序列到序列的任务,这涉及从输入序列预测输出序列。我们将用一个例子来说明如何使用一种新的模型架构——注意机制来解决基本的序列到序列任务,这在基于深度学习的自然语言处理领域变得越来越重要。

通过本章结束时,您应该熟悉深度学习中常见类型的顺序数据,它们如何呈现为张量,以及如何使用 TensorFlow.js 编写基本的 RNN、1D 卷积网络和注意网络来解决涉及顺序数据的机器学习任务。

本章中您将看到的层和模型是本书中最复杂的。这是它们为顺序学习任务增强容量所付出的代价。即使我们努力以尽可能直观的方式呈现它们,配以图表和伪代码的帮助,您第一次阅读时可能会觉得其中一些很难理解。如果是这样,请尝试运行示例代码并完成章末提供的练习。根据我们的经验,实践经验使得内化复杂概念和架构变得更加容易,就像本章中出现的那些一样。

9.1. 天气预测的第二次尝试:引入 RNN

我们在第八章中为 Jena 天气问题构建的模型丢弃了顺序信息。在本节中,我们将告诉您为什么会这样,并且我们如何通过使用 RNN 将顺序信息带回来。这将使我们能够在温度预测任务中实现更准确的预测。

9.1.1. 为什么密集层无法建模顺序

由于我们在上一章节中已经详细描述了 Jena 天气数据集,所以在这里我们将仅简要讨论数据集和相关的机器学习任务。该任务涉及使用过去 10 天内一段时间内的 14 个天气仪器(如温度、气压和风速)的读数来预测从某一时刻开始的 24 小时后的温度。仪器读数以 10 分钟的固定间隔进行,但我们将其降采样 6 倍,以每小时一次,以便使模型大小和训练时间可管理。因此,每个训练示例都带有一个形状为[240, 14]的特征张量,其中 240 是 10 天内的时间步数,14 是不同天气仪器读数的数量。

在前一章的任务中,当我们尝试了一个线性回归模型和一个 MLP 时,我们使用了tf.layers.flatten层将 2D 输入特征展平为 1D(参见清单 8.2 和图 8.2)。展平步骤是必要的,因为线性回归器和 MLP 都使用了密集层来处理输入数据,而密集层要求每个输入示例的输入数据为 1D。这意味着所有时间步的信息以一种方式混合在一起,使得哪个时间步首先出现,接下来是哪个时间步,一个时间步距离另一个时间步有多远等等的重要性被抹去了。换句话说,当我们将形状为[240, 14]的 2D 张量展平为形状为[3360]的 1D 张量时,我们如何对 240 个时间步进行排序并不重要,只要我们在训练和推断之间保持一致即可。您可以在本章末尾的练习 1 中通过实验证实这一点。但从理论上讲,这种对数据元素顺序缺乏敏感性的缺点可以用以下方式理解。在密集层的核心是一组线性方程,每个方程都将每个输入特征值[x[1],x[2],…,x[n]]与来自核[k[1],k[2],…,k[n]]的可调系数相乘:

方程式 9.1.

图 9.1 提供了密集层的工作原理的可视化表示:从输入元素到层输出的路径在图形上对称,反映了方程式 9.1 中的数学对称性。当我们处理序列数据时,这种对称性是不可取的,因为它使模型对元素之间的顺序视而不见。

图 9.1. 密集层的内部架构。密集层执行的乘法和加法与其输入对称。与简单 RNN 层(图 9.2)相比,它通过引入逐步计算来打破对称性。请注意,我们假设输入只有四个元素,出于简单起见,省略了偏置项。此外,我们仅显示了密集层的一个输出单元的操作。其余的单元被表示为背景中的一堆模糊的框。

实际上,有一个简单的方法可以显示,我们基于密集层的方法(即 MLP,即使加入正则化)并没有很好地解决温度预测问题:将其准确性与我们从常识、非机器学习方法中获得的准确性进行比较。

我们所说的常识方法是什么?将温度预测为输入特征中的最后一个温度读数。简单地说,就假装从现在起 24 小时后的温度会与当前温度相同!这种方法是“直觉上合理”的,因为我们从日常经验中知道,明天的温度往往接近于今天的温度(也就是说,在同一天的同一时间)。这是一个非常简单的算法,并提供了一个合理的猜测,应该能击败所有其他类似简单的算法(例如,将温度预测为 48 小时前的温度)。

我们在 第八章 中使用的 tfjs-examples 的 jena-weather 目录提供了一个命令,用于评估这种常识方法的准确性:

git clone https://github.com/tensorflow/tfjs-examples.git
cd tfjs-examples/jena-weather
yarn
yarn train-rnn --modelType baseline

yarn train-rnn 命令调用了 train-rnn.js 脚本,并在基于 Node.js 的后端环境中执行计算。^([2]) 我们将在不久的将来回到这种操作模式,当我们探索 RNN 时。该命令应该会给出以下屏幕输出:

²

实现这种常识、非机器学习方法的代码位于 jena-weather/models.js 中名为 getBaselineMeanAbsoluteError() 的函数中。它使用 Dataset 对象的 forEachAsync() 方法来遍历验证子集的所有批次,计算每个批次的 MAE 损失,并累积所有损失以获得最终损失。

Commonsense baseline mean absolute error: 0.290331

因此,简单的非机器学习方法产生了约为 0.29(以归一化术语表示)的平均绝对预测误差,这与我们从 第八章 中 MLP 获得的最佳验证误差相当(见 图 8.4)。换句话说,MLP,无论是否进行正则化,都无法可靠地击败来自常识基线方法的准确性!

这样的观察在机器学习中并不少见:机器学习并不总是能够击败常识方法。为了击败它,机器学习模型有时需要通过超参数优化进行精心设计或调整。我们的观察还强调了在处理机器学习问题时创建非机器学习基准进行比较的重要性。当然,我们肯定要避免将所有的精力都浪费在构建一个甚至连一个简单且计算成本更低的基线都无法击败的机器学习算法上!我们能够在温度预测问题中击败基线吗?答案是肯定的,我们将依靠 RNN 来做到这一点。现在让我们来看看 RNN 如何捕捉和处理序列顺序。

9.1.2. RNNs 如何建模序列顺序

图 9.2 的 A 面通过使用一个简短的四项序列显示了 RNN 层的内部结构。有几种 RNN 层的变体,图表显示了最简单的变体,称为 SimpleRNN,并且在 TensorFlow.js 中可用作tf.layers.simpleRNN()工厂函数。我们稍后将在本章中讨论更复杂的 RNN 变体,但现在我们将专注于 SimpleRNN。

图 9.2. SimpleRNN 内部结构的“展开”(A 面)和“卷曲”(B 面)表示。卷曲视图(B 面)以更简洁的形式表示与展开视图相同的算法。它以更简洁的方式说明了 SimpleRNN 对输入数据的顺序处理。在面板 B 中的卷曲表示中,从输出(y)返回到模型本身的连接是这些层被称为循环的原因。与图 9.1 中一样,我们仅显示了四个输入元素,并简化了偏差项。

图表显示了输入的时间片段(x[1],x[2],x[3],…)是如何逐步处理的。在每一步中,x[i] 通过一个函数(f())进行处理,该函数表示为图表中心的矩形框。这产生了一个输出(y[i]),它与下一个输入片段(x[i][+1])结合,作为下一步 f() 的输入。重要的是要注意,即使图表显示了四个具有函数定义的单独框,它们实际上表示相同的函数。这个函数(f())称为 RNN 层的cell。在调用 RNN 层期间,它以迭代的方式使用。因此,可以将 RNN 层视为“在for循环中包装的 RNN 单元。”^([3])

³

引述于 Eugene Brevdo。

比较 SimpleRNN 的结构和密集层的结构(图 9.1),我们可以看到两个主要区别:

  • SimpleRNN 逐步处理输入元素(时间步)。这反映了输入的顺序性,这是密集层无法做到的。
  • 在 SimpleRNN 中,每个输入时间步的处理都会生成一个输出(y[i])。前一个时间步的输出(例如,y[1])在处理下一个时间步(例如 x[2])时由层使用。这就是 RNN 名称中“循环”部分的原因:来自先前时间步的输出会流回并成为后续时间步的输入。在诸如 dense、conv2d 和 maxPooling2d 之类的层类型中不会发生递归。这些层不涉及输出信息的回流,因此被称为前馈层。

由于这些独特的特征,SimpleRNN 打破了输入元素之间的对称性。它对输入元素的顺序敏感。如果重新排列顺序输入的元素,则输出将随之而改变。这使 SimpleRNN 与密集层有所区别。

图 9.2 的 B 面板是对简单循环神经网络的更抽象的表示。它被称为 rolled RNN 图表,与 A 面板中的 unrolled 图表相对应,因为它将所有时间步骤“卷”成一个循环。滚动图表很好地对应于编程语言中的 for 循环,这实际上是 TensorFlow.js 中实现 simpleRNN 和其他类型的 RNN 的方式。但是,与其显示真实的代码,不如看一下下面的简单 RNN 的伪代码,您可以将其视为图 9.2 中所示的 simpleRNN 结构的实现。这将帮助您专注于 RNN 层的工作原理的本质。

列表 9.1. simpleRNN 的内部计算的伪代码
y = 0                             ***1***
for x in input_sequence:          ***2***
  y = f(dot(W, x) + dot(U, y))    ***3***
  • 1 y 对应于图 9.2 中的 y。状态在开始时被初始化为零。
  • 2 x 对应于图 9.2 中的 x。for 循环遍历输入序列的所有时间步。
  • 3 W 和 U 分别是输入和状态的权重矩阵(即,回路回传并成为重复输入的输出)。这也是时间步骤 i 的输出成为时间步骤 i + 1 的状态(重复输入)的地方。

在列表 9.1 中,您可以看到时间步 i 的输出成为下一个时间步(下一个迭代)的“状态”。State 是 RNN 的一个重要概念。这是 RNN“记住”已经看过的输入序列步骤的方式。在 for 循环中,这个记忆状态与未来的输入步骤结合起来,并成为新的记忆状态。这使得 simpleRNN 能够根据之前序列中出现的元素来不同地处理相同的输入元素。这种基于记忆的敏感性是顺序处理的核心。作为一个简单的例子,如果您试图解码莫尔斯电码(由点和短划组成),则短划的含义取决于先前(以及之后)的点和短划的序列。另一个例子,在英语中,单词 last 可以根据之前的单词有完全不同的含义。

SimpleRNN 的命名适当,因为其输出和状态是相同的东西。稍后,我们将探索更复杂和更强大的 RNN 体系结构。其中一些具有输出和状态作为两个单独的东西;其他甚至具有多个状态。

关于 RNN 的另一件值得注意的事情是 for 循环使它们能够处理由任意数量的输入步骤组成的输入序列。这是通过将序列输入展平并将其馈送到密集层中无法完成的,因为密集层只能接受固定的输入形状。

此外,for 循环反映了 RNN 的另一个重要属性:参数共享。我们所说的是,相同的权重参数(WU)在所有时间步中都被使用。另一种选择是对每个时间步使用唯一的 W(和 U)值。这是不可取的,因为 1)它限制了 RNN 可以处理的时间步数,2)它导致可调参数数量的显著增加,这将增加计算量并增加训练期间过拟合的可能性。因此,RNN 层类似于 convnets 中的 conv2d 层,它们使用参数共享来实现高效计算并防止过拟合——尽管循环和 conv2d 层以不同的方式实现参数共享。虽然 conv2d 层利用了沿空间维度的平移不变性,但 RNN 层利用了沿时间维度的平移不变性。

图 9.2 显示了在推断时间(前向传播)中简单 RNN 中发生的情况。它并不显示在训练期间(后向传播)如何更新权重参数(WU)。然而,RNN 的训练遵循我们在 2.2.2 节(图 2.8)中介绍的相同反向传播规则——即从损失开始,回溯操作列表,取其导数,并通过它们累积梯度值。数学上,递归网络上的后向传播基本上与前向传播相同。唯一的区别是 RNN 层的反向传播沿时间倒退,在像 图 9.2 面板 A 中的展开图中。这就是为什么有时将训练 RNN 的过程称为时间反向传播(BPTT)。

SimpleRNN 的实现

关于 simpleRNN 和 RNN 总体的抽象思考已经足够了。现在让我们看看如何创建一个 simpleRNN 层并将其包含在模型对象中,这样我们就可以比以前更准确地预测温度了。清单 9.2 中的代码(从 jena-weather/train-rnn.js 中摘录)就是这样做的。尽管 simpleRNN 层的内部复杂性很高,但模型本身相当简单。它只有两层。第一层是 simpleRNN,配置为具有 32 个单元。第二个是使用默认线性激活生成温度的连续数值预测的密集层。请注意,因为模型以一个 RNN 开始,所以不再需要展平序列输入(与前一章中为同一问题创建 MLPs 时进行比较时)。实际上,如果我们在 simpleRNN 层之前放置一个 flatten 层,将会抛出错误,因为 TensorFlow.js 中的 RNN 层期望它们的输入至少是 3D(包括批处理维度)。

代码清单 9.2 创建用于温度预测问题的基于 simpleRNN 的模型
function buildSimpleRNNModel(inputShape) {
  const model = tf.sequential();
  const rnnUnits = 32;                       ***1***
  model.add(tf.layers.simpleRNN({            ***2***
    units: rnnUnits,
    inputShape
  }));
  model.add(tf.layers.dense({units: 1}));    ***3***
  return model;
}
  • 1 simpleRNN 层的硬编码单元数是通过超参数的手工调整得到的一个很好的值。
  • 2 模型的第一层是一个 simpleRNN 层。不需要对顺序输入进行展平,其形状为 [null, 240, 14]。
  • 3 我们用一个具有单个单元且默认线性激活函数的密集层来结束模型,这适用于回归问题。

要查看 simpleRNN 模型的运行情况,请使用以下命令:

yarn train-rnn --modelType simpleRNN --logDir /tmp/
  jean-weather-simpleRNN-logs

RNN 模型在后端环境中使用 tfjs-node 进行训练。由于基于 BPTT 的 RNN 训练涉及到大量计算,如果在资源受限的浏览器环境中训练相同的模型将会更加困难和缓慢,甚至不可能完成。如果您已经正确设置了 CUDA 环境,您可以在命令中添加 --gpu 标志来进一步提高训练速度。

前一个命令中的 --logDir 标志使得模型训练过程将损失值记录到指定的目录中。可以使用一个名为 TensorBoard 的工具在浏览器中加载并绘制损失曲线。图 9.3 是 TensorBoard 的一个截图。在 JavaScript 代码级别,通过使用指向日志目录的特殊回调函数来配置 tf.LayersModel.fit() 调用来实现这个功能。信息框 9.1 中包含了关于如何实现这一功能的进一步信息。

图 9.3 Jena-temperature-prediction 问题的 simpleRNN 模型的 MAE 损失曲线。该图是 TensorBoard 的一个截图,显示了基于 Node.js 进行的 simpleRNN 模型训练的日志。

使用 TensorBoard 回调函数在 Node.js 中监控长时间的模型训练

在 第八章 中,我们介绍了来自 tfjs-vis 库的回调函数,以帮助您在浏览器中监控 tf.LayersModel.fit() 的调用。然而,tfjs-vis 是仅适用于浏览器的库,不适用于 Node.js。在 tfjs-node(或 tfjs-node-gpu)中,默认情况下,tf.LayersModel.fit() 在终端中以进度条形式呈现,并显示损失和时间指标。虽然这种方式简洁明了而且信息量大,但文字和数字往往不如图形界面直观和吸引人。例如,对于模型训练后期我们经常寻找的损失值的微小变化,使用图表(具有适当的刻度和网格线)要比使用一段文本更容易发现。

幸运的是,一个名为 TensorBoard 的工具可以帮助我们在后端环境中完成这项工作。TensorBoard 最初是为 TensorFlow(Python)设计的,但 tfjs-node 和 tfjs-node-gpu 可以以兼容格式写入数据,这些数据可以被 TensorBoard 处理。要将损失和指标值记录到 TensorBoard 以用于 tf.LayersModel.fit()tf.LayersModel.fitDataset() 的调用中,请按照下列模式操作:

import * as tf from '@tensorflow/tfjs-node';
// Or '@tensorflow/tfjs-node-gpu'
     // ...
 await model.fit(xs, ys, {
   epochs,
   callbacks: tf.node.tensorBoard('/path/to/my/logdir')
 });
      // Or for fitDataset():
 await model.fitDataset(dataset, {
   epochs,
   batchesPerEpoch,
   callbacks: tf.node.tensorBoard('/path/to/my/logdir')
 });

这些调用会将损失值和在compile()调用期间配置的任何指标写入目录/path/to/my/logdir。要在浏览器中查看日志,

  1. 打开一个单独的终端。
  2. 使用以下命令安装 TensorBoard(如果尚未安装):pip install tensorboard
  3. 启动 TensorBoard 的后端服务器,并指向在回调创建过程中指定的日志目录:tensorboard --logdir /path/to/my/logdir
  4. 在 Web 浏览器中,导航至 TensorBoard 进程显示的 http:// URL。然后,类似于 figures 9.3 和 9.5 中显示的损失和指标图表将出现在 TensorBoard 的美观 Web UI 中。

listing 9.2 创建的 simpleRNN 模型的文本摘要如下:

Layer (type)                 Output shape              Param #
     =================================================================
     simple_rnn_SimpleRNN1 (Simpl [null,32]                 1504
     _________________________________________________________________
     dense_Dense1 (Dense)         [null,1]                  33
     =================================================================
Total params: 1537
     Trainable params: 1537
     Non-trainable params: 0
     _________________________________________________________________

它的权重参数明显少于我们之前使用的 MLP(1,537 与 107,585 相比,减少了 70 倍),但在训练过程中实现了更低的验证 MAE 损失(即更准确的预测)(0.271 与 0.289)。这种对温度预测误差的小但明显的减少突显了基于时间不变性的参数共享的强大力量以及 RNN 在学习诸如我们处理的天气数据之类的序列数据方面的优势。

您可能已经注意到,即使 simpleRNN 涉及相对少量的权重参数,与 MLP 等前馈模型相比,其训练和推断时间要长得多。这是 RNN 的一个主要缺点,即无法在时间步长上并行化操作。这种并行化是不可实现的,因为后续步骤依赖于先前步骤中计算的状态值(参见 figure 9.2 和 listing 9.1 中的伪代码)。如果使用大 O 符号表示,RNN 的前向传递需要 O(n)时间,其中n是输入时间步的数量。后向传递(BPTT)需要另外 O(n)时间。耶拿天气问题的输入未来包含大量(240)时间步,这导致了之前看到的较慢的训练时间。这也是为什么我们在 tfjs-node 而不是在浏览器中训练模型的主要原因。

RNN 的情况与 dense 和 conv2d 等前馈层形成鲜明对比。在这些层中,计算可以在输入元素之间并行化,因为对一个元素的操作不依赖于另一个输入元素的结果。这使得这些前馈层在执行它们的正向和反向传播时可以在 O(n)时间内花费较少的时间(在某些情况下接近 O(1)),借助 GPU 加速。在 section 9.2 中,我们将探索一些更多可并行化的序列建模方法,比如 1D 卷积。然而,熟悉 RNN 仍然是重要的,因为它们对于序列位置是敏感的,而 1D 卷积不是(稍后讨论)。

门控循环单元(GRU):一种更复杂的 RNN 类型

SimpleRNN 并不是 TensorFlow.js 中唯一的循环层。还有两个循环层可用:门控循环单元 (GRU^([4])) 和 LSTM(Long Short-Term Memory 的缩写^([5]))。在大多数实际应用中,你可能会想要使用这两种模型中的一种。SimpleRNN 对于大多数真实问题而言过于简单,尽管其计算成本更低并且其内部机制比 GRU 和 LSTM 更容易理解。但是,简单 RNN 存在一个主要问题:尽管理论上来说,simpleRNN 能够在时间 t 保留对于多个时间步长前的输入信息,但是在实践中,学习这种长期依赖关系非常困难。

Kyunghyun Cho 等人在 2014

Sepp Hochreiter 和 Jürgen Schmidhuber 在 1997 年发表的论文《Long Short-Term Memory》中提出了 LSTM 模型,这篇论文发表在《Neural Computation》杂志的第 9 卷第 8 期上,页码从 1735 至 1780。

这是由于梯度消失问题,这是一种类似于前馈网络深度很深时观察到的效应的影响:随着你向网络中添加越来越多的层,从损失函数向早期层反向传播的梯度大小会越来越小。因此,权重的更新也越来越小,直到网络最终变得无法训练。对于 RNN,大量的时间步骤在此问题中扮演了许多层的角色。GRU 和 LSTM 是为解决梯度消失问题而设计的 RNN,GRU 是两者中更简单的一种。让我们看看 GRU 是如何解决这个问题的。

与 simpleRNN 相比,GRU 具有更复杂的内部结构。图 9.4 显示了 GRU 的内部结构的滚动表示。与 simpleRNN 的相同滚动表示进行比较(图 9.2 的面板 B),它包含了更多的细节。输入 (x) 和输出 / 状态(按照 RNN 文献中的约定称为 h)通过 四个 等式生成新的输出 / 状态。相比之下,simpleRNN 仅涉及 一个 等式。这种复杂性也体现在 清单 9.3 中的伪代码中,可以将其视为 图 9.4 中机制的一种实现。为简单起见,我们省略了伪代码中的偏置项。

图 9.4 门控循环单元(GRU)的滚动表示,一种比 simpleRNN 更复杂、更强大的 RNN 层类型。这是一个滚动表示,与 图 9.2 中的面板 B 相似。请注意,我们为了简单起见,在等式中省略了偏置项。虚线表示了从 GRU 单元的输出 (h) 到下一个时间步的同一单元的反馈连接。

代码清单 9.3 Pseudo-code for a GRU layer
h = 0                                             ***1***
for x_i in input_sequence:                        ***2***
  z = sigmoid(dot(W_z, x) + dot(U_z, h))          ***3***
  r = sigmoid(dot(W_r, x) + dot(W_r, h))          ***4***
  h_prime = tanh(dot(W, x) + dot(r, dot(U, h)))   ***5***
  h = dot(1 - z, h) + dot(z, h_prime)             ***6***
  • 1 这是 图 9.4 中的 h。和 simpleRNN 一样,在最开始状态被初始化为零。
  • 2 这个 for 循环遍历输入序列的所有时间步。
  • 3 z 被称为更新门。
  • 4 r 被称为重置门。
  • 5 h_prime 是当前状态的临时状态。
  • 6 h_prime (当前临时状态) 和 h (上一个状态) 以加权方式结合(z 为权值)形成新状态。

在 GRU 的所有内部细节中,我们要强调两个最重要的方面:

  1. GRU 可以轻松地在许多时间步之间传递信息。这是通过中间量 z 实现的,它被称为更新门。由于更新门的存在,GRU 可以学习以最小的变化在许多时间步内传递相同的状态。特别地,在等式 (1 - z) · h + z · *h’ 中,如果 z 的值为 0,则状态 h 将简单地从当前时间步复制到下一个时间步。这种整体传递的能力对于 GRU 如何解决消失梯度问题至关重要。重置门 z 被计算为输入 x 和当前状态 h 的线性组合,然后经过一个 sigmoid 非线性函数。
  2. 除了更新门 z,GRU 中的另一个“门”被称为所谓的重置门r。像更新门 z 一样,r 被计算为对输入和当前状态 h 的线性组合进行 sigmoid 非线性函数操作。重置门控制有多少当前状态需要“遗忘”。特别地,在等式 tanh(W · x + r · U · h) 中,如果 r 的值变为 0,则当前状态 h 的影响被抹除;如果下游方程中的 (1 - z) 接近零,那么当前状态 h 对下一个状态的影响将被最小化。因此,rz 协同工作,使得 GRU 能够在适当条件下学习忘记历史或其一部分。例如,假设我们试图对电影评论进行正面或负面的分类。评论可能开始说“这部电影相当令人满意”,但评论过了一半后,又写到“然而,这部电影并不像其他基于类似观点的电影那么出色。” 在这一点上,应该大部分地忘记关于初始赞美的记忆,因为应该更多地权衡评论后部分对该评论最终情感分析结果的影响。

所以,这是 GRU 如何工作的一个非常粗糙和高层次的概述。要记住的重要事情是,GRU 的内部结构允许 RNN 学习何时保留旧状态,何时使用来自输入的信息更新状态。这种学习通过可调权重 W[z]U[z]W[r]W[r]WU 的更新体现出来(除了省略的偏置项)。

如果你一开始不明白所有细节,不要担心。归根结底,我们在最后几段中对 GRU 的直观解释并不那么重要。理解 GRU 如何以非常详细的层面处理序列数据并不是人类工程师的工作,就像理解卷积神经网络如何将图像输入转换为输出类别概率的细节并不是人类工程师的工作一样。细节是由神经网络在 RNN 结构数据所描述的假设空间中通过数据驱动的训练过程找到的。

要将 GRU 应用于我们的温度预测问题,我们构建一个包含 GRU 层的 TensorFlow.js 模型。我们用于此的代码(摘自 jena-weather/train-rnn.js)几乎与我们用于简单 RNN 模型的代码(代码清单 9.2)完全相同。唯一的区别是模型的第一层的类型(GRU 对比于简单 RNN)。

代码清单 9.4. 为 Jena 温度预测问题创建一个 GRU 模型
function buildGRUModel(inputShape) {
  const model = tf.sequential();
  const rnnUnits = 32;                      ***1***
  model.add(tf.layers.gru({                 ***2***
    units: rnnUnits,
    inputShape
  }));
  model.add(tf.layers.dense({units: 1}));   ***3***
  return model;
}
  • 1 硬编码的单元数是一个通过超参数手动调整而发现效果良好的数字。
  • 2 模型的第一层是一个 GRU 层。
  • 3 模型以具有单个单元和默认线性激活的密集层结束,用于回归问题。

要开始在 Jena 天气数据集上训练 GRU 模型,请使用

yarn train-rnn --modelType gru

图 9.5 显示了使用 GRU 模型获得的训练和验证损失曲线。它获得了约为 0.266 的最佳验证错误,这超过了我们在上一节中从简单 RNN 模型中获得的结果(0.271)。这反映了相较于简单 RNN,GRU 在学习序列模式方面具有更大的容量。在气象仪器读数中确实隐藏着一些序列模式,这些模式有助于提高温度的预测精度;这些信息被 GRU 捕捉到,但简单 RNN 没有。但这是以更长的训练时间为代价的。例如,在我们的一台机器上,GRU 模型的训练速度为每批 3,000 毫秒,而简单 RNN 的训练速度为每批 950 毫秒^([6])。但如果目标是尽可能准确地预测温度,那么这个代价很可能是值得的。

这些性能数字是从在 CPU 后端运行的 tfjs-node 获得的。如果你使用 tfjs-node-gpu 和 CUDA GPU 后端,你将获得两种模型类型的近似比例的加速。

图 9.5. 在温度预测问题上训练 GRU 模型的损失曲线。将其与简单 RNN 模型的损失曲线进行比较(图 9.3),注意 GRU 模型取得的最佳验证损失的小幅但真实的降低。

9.2. 为文本构建深度学习模型

我们刚刚研究的天气预测问题涉及顺序数值数据。但是最普遍的序列数据可能是文本而不是数字。在像英语这样以字母为基础的语言中,文本可以被视为字符序列或单词序列。这两种方法适用于不同的问题,并且在本节中我们将针对不同的任务使用它们。我们将在接下来的几节中介绍的文本数据的深度学习模型可以执行与文本相关的任务,例如

  • 给一段文本分配情感分数(例如,一个产品评论是积极的还是消极的)
  • 将一段文本按主题分类(例如,一篇新闻文章是关于政治、金融、体育、健康、天气还是其他)
  • 将文本输入转换为文本输出(例如,用于格式标准化或机器翻译)
  • 预测文本的即将出现的部分(例如,移动输入方法的智能建议功能)

此列表只是涉及文本的一小部分有趣的机器学习问题,这些问题在自然语言处理领域进行系统研究。尽管我们在本章中只是浅尝神经网络的自然语言处理技术,但这里介绍的概念和示例应该为你进一步探索提供了一个良好的起点(请参阅本章末尾的“进一步阅读资料”部分)。

请记住,本章中的深度神经网络都不真正理解文本或语言的人类意义。相反,这些模型可以将文本的统计结构映射到特定的目标空间,无论是连续情感分数、多类别分类结果还是新序列。这证明对于解决许多实际的、与文本相关的任务来说,这是足够的。自然语言处理的深度学习只是对字符和单词进行的模式识别,方式与基于深度学习的计算机视觉(第四章)对像素进行的模式识别类似。

在我们深入探讨为文本设计的深度神经网络之前,我们首先需要了解机器学习中的文本是如何表示的。

9.2.1. 机器学习中的文本表示:单热编码和多热编码

到目前为止,在本书中我们遇到的大部分输入数据都是连续的。例如,鸢尾花的花瓣长度在一定范围内连续变化;耶拿气象数据集中的天气仪读数都是实数。这些值可以直接表示为浮点型张量(浮点数)。但是,文本不同。文本数据以字符或单词的字符串形式出现,而不是实数。字符和单词是离散的。例如,在“j”和“k”之间没有类似于在 0.13 和 0.14 之间存在数字的东西。在这个意义上,字符和单词类似于多类分类中的类别(例如三种鸢尾花物种或 MobileNet 的 1,000 个输出类别)。文本数据在被馈送到深度学习模型之前需要被转换为向量(数字数组)。这个转换过程称为文本向量化

有多种文本向量化的方式。独热编码(如我们在第三章中介绍的)是其中之一。在英语中,根据划分标准,大约有 10,000 个最常用的单词。我们可以收集这 10,000 个单词并形成一个词汇表。词汇表中的唯一单词可以按照某种顺序排列(例如,按频率降序排列),以便为任何给定的单词分配一个整数索引。^([7]) 然后,每个英文单词都可以表示为一个长度为 10,000 的向量,其中只有对应索引的元素为 1,所有其余元素为 0。这就是该单词的独热向量化。图 9.6 的 A 面以图形方式展示了这一点。

一个显而易见的问题是:如果我们遇到一个落在这 10,000 词汇表之外的罕见单词怎么办?这是任何文本导向的深度学习算法所面临的实际问题。在实践中,我们通过向词汇表添加一个名为OOV的特殊项来解决这个问题。OOV 代表词汇表之外。因此,所有不属于词汇表的罕见单词都被归类为该特殊项,并将具有相同独热编码或嵌入向量。更复杂的技术有多个 OOV 桶,并使用哈希函数将罕见单词分配到这些桶中。

图 9.6. 一个单词的独热编码(向量化)(A 面)和一个句子作为一系列单词的独热编码(B 面)。C 面展示了与 B 面中相同句子的简化的多热编码。它是一种更简洁和可扩展的序列表示,但它丢弃了顺序信息。为了可视化,我们假设词汇表的大小只有 14。实际上,在深度学习中使用的英语单词的词汇量要大得多(数量级为数千或数万,例如,10,000)。

如果我们有一个句子而不是单个单词呢?我们可以为构成句子的所有单词获得独热向量,并将它们放在一起形成句子单词的二维表示(参见图 9.6 的面板 B)。这种方法简单而明确。它完美地保留了句子中出现的单词及其顺序的信息。^([8]) 然而,当文本变得很长时,向量的大小可能会变得非常大,以至于无法管理。例如,英语句子平均包含约 18 个单词。考虑到我们的词汇量为 10,000,仅表示一个句子就需要 180,000 个数字,这已经比句子本身占用的空间大得多了。更不用说一些与文本相关的问题涉及段落或整篇文章,其中包含更多的单词,会导致表示的大小和计算量急剧增加。

这假设没有 OOV(Out of Vocabulary)词。

解决这个问题的一种方法是将所有单词都包含在一个单一向量中,以便向量中的每个元素表示对应的单词是否出现在文本中。图 9.6 的面板 C 进行了说明。在这种表示中,向量的多个元素可以具有值 1。这就是为什么人们有时将其称为多热编码。多热编码具有固定长度(词汇量的大小),不管文本有多长,因此它解决了大小爆炸的问题。但这是以失去顺序信息为代价的:我们无法从多热向量中得知哪些单词先出现,哪些单词后出现。对于一些问题,这可能是可以接受的;对于其他问题,这是不可接受的。有更复杂的表示方法来解决大小爆炸问题,同时保留顺序信息,我们将在本章后面探讨。但首先,让我们看一个具体的与文本相关的机器学习问题,可以使用多热方法以合理的准确率解决。

9.2.2. 情感分析问题的首次尝试

我们将在第一个例子中使用互联网电影数据库(IMDb)数据集来应用机器学习到文本上。该数据集是 imdb.com 上大约 25,000 条电影评论的集合,每个评论都被标记为积极或消极。机器学习任务是二元分类:即给定的电影评论是积极的还是消极的。数据集是平衡的(50% 积极评论和 50% 消极评论)。正如你从在线评论中所期望的那样,示例的单词长度各不相同。有些评论只有 10 个单词,而另一些则可以长达 2,000 个单词。以下是一个典型评论的例子。此示例被标记为消极。数据集中省略了标点符号。

这部电影中的母亲对她的孩子太粗心了,以至于忽视了,我希望我对她和她的行为不要那么生气,因为否则我会享受这部电影的,她太过分了,我建议你快进到你看到她做的事情结束,还有,有没有人厌倦看到拍得这么黑暗的电影了,观众几乎看不到正在拍摄的东西,所以我们为什么看不到夜视了呢

数据被分为训练集和评估集,当您发出类似于模型训练命令时,它们会自动从网络下载并写入您的 tmp 目录

git clone https://github.com/tensorflow/tfjs-examples.git
    cd tfjs-examples/sentiment
    yarn
    yarn train multihot

如果您仔细检查 sentiment/data.js,您会发现它下载和读取的数据文件不包含实际的单词作为字符字符串。相反,这些文件中的单词以 32 位整数表示。虽然我们不会详细介绍该文件中的数据加载代码,但值得一提的是它执行了句子的多热向量化的部分,如下一列表所示。

列表 9.5. 从 loadFeatures() 函数对句子进行多热向量化
const buffer = tf.buffer([sequences.length, numWords]);   ***1***
     sequences.forEach((seq, i) => {                      ***2***
  seq.forEach(wordIndex => {                              ***3***
    if (wordIndex !== OOV_INDEX) {                        ***4***
      buffer.set(1, i, wordIndex);                        ***5***
    }
  });
});
  • 1 创建一个 TensorBuffer 而不是一个张量,因为我们将设置其元素值。缓冲区从全零开始。
  • 2 遍历所有例子,每个例子都是一个句子
  • 3 每个序列(句子)都是一个整数数组。
  • 4 跳过多热编码中的词汇表外(OOV)单词
  • 5 将缓冲区中的相应索引设置为 1。请注意,每个索引 i 可能有多个 wordIndex 值设置为 1,因此是多热编码。

多热编码的特征被表示为一个形状为 [numExamples, numWords] 的 2D 张量,其中 numWords 是词汇表的大小(在本例中为 10,000)。这种形状不受各个句子长度的影响,这使得这成为一个简单的向量化范例。从数据文件加载的目标的形状为 [numExamples, 1],包含负面和正面标签,分别表示为 0 和 1。

我们应用于多热数据的模型是一个 MLP。实际上,即使我们想要,由于多热编码丢失了顺序信息,也无法对数据应用 RNN 模型。我们将在下一节讨论基于 RNN 的方法。创建 MLP 模型的代码来自 sentiment/train.js 中的 buildModel() 函数,简化后的代码如下列表所示。

列表 9.6. 为多热编码的 IMDb 电影评论构建 MLP 模型
const model = tf.sequential();
model.add(tf.layers.dense({             ***1***
  units: 16,
  activation: 'relu',
  inputShape: [vocabularySize]          ***2***
}));
model.add(tf.layers.dense({
  units: 16,
  activation: 'relu'
}));
model.add(tf.layers.dense({
  units: 1,
  activation: 'sigmoid'                 ***3***
}));
  • 1 添加两个带有 relu 激活的隐藏密集层以增强表示能力
  • 2 输入形状是词汇表的大小,因为我们在这里处理多热向量化。
  • 3 为输出层使用 sigmoid 激活以适应二元分类任务

通过运行yarn train multihot --maxLen 500命令,可以看到模型达到大约 0.89 的最佳验证准确率。这个准确率还可以,明显高于机会的准确率(0.5)。这表明通过仅仅查看评论中出现的单词,可以在这个情感分析问题上获得一个相当合理的准确度。例如,像令人愉快崇高这样的单词与积极的评论相关联,而像糟糕乏味这样的单词与消极的评论相关联,并且具有相对较高的可靠性。当然,在许多情况下,仅仅看单词并不一定能得到正确的结论。举一个人为的例子,理解句子“别误会,我并不完全不同意这是一部优秀的电影”的真实含义需要考虑顺序信息——不仅是单词是什么,还有它们出现的顺序。在接下来的章节中,我们将展示通过使用一个不丢失顺序信息的文本向量化和一个能够利用顺序信息的模型,我们可以超越这个基准准确度。现在让我们看看词嵌入和一维卷积如何工作。

9.2.3. 文本的更高效表示:词嵌入

什么是词嵌入?就像一位热编码(图 9.6)一样,词嵌入是将一个单词表示为一个向量(在 TensorFlow.js 中是一个一维张量)的一种方式。然而,词嵌入允许向量的元素值被训练,而不是依据一个严格的规则进行硬编码,比如一热编码中的单词到索引映射。换句话说,当一个面向文本的神经网络使用词嵌入时,嵌入向量成为模型的可训练的权重参数。它们通过与模型的其他权重参数一样的反向传播规则进行更新。

这种情况在图 9.7 中示意。在 TensorFlow.js 中,可以使用tf.layer.embedding()层类型来执行词嵌入。它包含一个可训练的形状为[vocabularySize, embeddingDims]的权重矩阵,其中vocabularySize是词汇表中唯一单词的数量,embeddingDims是用户选择的嵌入向量的维度。每当给出一个单词,比如the,你可以使用一个单词到索引的查找表在嵌入矩阵中找到对应的行,该行就是你的单词的嵌入向量。请注意,单词到索引的查找表不是嵌入层的一部分;它是模型以外的一个单独的实体(例如,参见示例 9.9)

图 9.7. 描述嵌入矩阵工作原理的示意图。嵌入矩阵的每一行对应词汇表中的一个单词,每一列是一个嵌入维度。嵌入矩阵的元素值在图中以灰度表示,并随机选择。

如果你有一系列单词,就像图 9.7 中显示的句子一样,你需要按照正确的顺序重复这个查找过程,并将得到的嵌入向量堆叠成一个形状为[sequenceLength, embeddingDims]的二维张量,其中sequenceLength是句子中的单词数量。^([9]) 如果句子中有重复的单词(比如在图 9.7 中的例子中的the),这并不重要:只需让相同的嵌入向量在结果的二维张量中重复出现。

这种多词嵌入查找过程可以有效地使用tf.gather()方法进行,这就是 TensorFlow.js 中嵌入层在底层实现的方式。

单词嵌入为我们带来以下好处:

  • 它解决了使用独热编码的大小问题。embeddingDims通常比vocabularySize要小得多。例如,在我们即将在 IMDb 数据集上使用的一维卷积网络中,vocabularySize为 10,000,embeddingDims为 128。因此,在来自 IMDb 数据集的 500 字评论中,表示这个例子只需要 500 * 128 = 64k 个浮点数,而不是 500 * 10,000 = 5M 个数字,就像独热编码一样——这样的向量化更经济。
  • 通过不在乎词汇中单词的排序方式,并允许嵌入矩阵通过反向传播来进行训练,就像所有其他神经网络权重一样,单词嵌入可以学习单词之间的语义关系。意思相近的单词应该在嵌入空间中距离更近。例如,意思相近的单词,比如verytruly,它们的向量应该比那些意思更不同的单词的向量更接近,比如verybarely。为什么会这样?一个直观理解它的方式是意识到以下:假设你用意思相近的单词替换电影评论输入中的一些单词;一个训练良好的网络应该输出相同的分类结果。这只有当每一对单词的嵌入向量,它们是模型后续部分的输入,彼此之间非常接近时才会发生。
  • 也就是说,嵌入空间具有多个维度(例如,128)的事实应该允许嵌入向量捕获单词的不同方面。例如,可能会有一个表示词性的维度,其中形容词fast与另一个形容词(如warm)比与一个名词(如house)更接近。可能还有另一个维度编码单词的性别方面,其中像actress这样的词比一个男性意义的词(如actor)更接近另一个女性意义的词(如queen)。在下一节(见 info box 9.2),我们将向您展示一种可视化单词嵌入并探索它们在对 IMDb 数据集进行嵌入式神经网络训练后出现的有趣结构的方法。

Table 9.1 提供了一个更简洁的总结,概述了一热/多热编码和词嵌入这两种最常用的词向量化范式之间的差异。

Table 9.1. 比较两种词向量化范式:one-hot/multi-hot 编码和词嵌入
One-hot 或 multi-hot 编码 词嵌入
硬编码还是学习? 硬编码。 学习:嵌入矩阵是一个可训练的权重参数;这些值通常在训练后反映出词汇的语义结构。
稀疏还是密集? 稀疏:大多数元素为零;一些为一。 密集:元素取连续变化的值。
可扩展性 不可扩展到大词汇量:向量的大小与词汇量的大小成正比。 可扩展到大词汇量:嵌入大小(嵌入维度数)不必随词汇量的增加而增加。

JavaScript 深度学习(三)(5)https://developer.aliyun.com/article/1516965

相关实践学习
基于阿里云DeepGPU实例,用AI画唯美国风少女
本实验基于阿里云DeepGPU实例,使用aiacctorch加速stable-diffusion-webui,用AI画唯美国风少女,可提升性能至高至原性能的2.6倍。
相关文章
|
2天前
|
机器学习/深度学习 JavaScript 前端开发
【JS】深度学习JavaScript
【JS】深度学习JavaScript
6 2
|
1月前
|
机器学习/深度学习 JavaScript 前端开发
JavaScript 深度学习(五)(5)
JavaScript 深度学习(五)
11 0
|
1月前
|
机器学习/深度学习 JavaScript 前端开发
JavaScript 深度学习(五)(4)
JavaScript 深度学习(五)
26 0
|
1月前
|
存储 机器学习/深度学习 JavaScript
JavaScript 深度学习(五)(3)
JavaScript 深度学习(五)
25 0
|
1月前
|
机器学习/深度学习 并行计算 JavaScript
JavaScript 深度学习(五)(2)
JavaScript 深度学习(五)
32 0
|
1月前
|
机器学习/深度学习 人工智能 JavaScript
JavaScript 深度学习(五)(1)
JavaScript 深度学习(五)
28 0
|
1月前
|
机器学习/深度学习 前端开发 JavaScript
JavaScript 深度学习(四)(5)
JavaScript 深度学习(四)
15 1
|
1月前
|
机器学习/深度学习 存储 算法
JavaScript 深度学习(四)(4)
JavaScript 深度学习(四)
23 1
|
1月前
|
机器学习/深度学习 算法 JavaScript
JavaScript 深度学习(四)(3)
JavaScript 深度学习(四)
28 1
|
1月前
|
机器学习/深度学习 编解码 JavaScript
JavaScript 深度学习(四)(2)
JavaScript 深度学习(四)
24 1