深度学习快速参考:6~10

2023-04-23 11:23:16 浏览数 (1)

六、超参数优化

使用深度神经网络的最大缺点之一是它们具有许多应优化的超参数,以使网络发挥最佳表现。 在前面的每个章节中,我们都遇到但没有涵盖超参数估计的挑战。 超参数优化是一个非常重要的话题。 在大多数情况下,这是一个未解决的问题,尽管我们不能涵盖本书的全部主题,但我认为它仍然值得一章。

在本章中,我将为您提供一些我认为是选择超参数的实用建议。 可以肯定的是,由于本章是基于我自己的经验,因此本章可能会有些偏颇和偏颇。 我希望经验会有所帮助,同时也带您进一步对该主题进行调查。

我们将在本章介绍以下主题:

  • 是否应该将网络架构视为超参数?
  • 我们应该优化哪些超参数?
  • 超参数优化策略

是否应该将网络架构视为超参数?

在构建最简单的网络时,我们必须对网络架构做出各种选择。 我们应该使用 1 个隐藏层还是 1,000 个? 每层应包含多少个神经元? 他们都应该使用relu激活函数还是tanh? 我们应该在每个隐藏层上还是仅在第一层上使用丢弃? 在设计网络架构时,我们必须做出许多选择。

在最典型的情况下,我们穷举搜索每个超参数的最佳值。 但是,要穷举搜索网络架构并不容易。 实际上,我们可能没有时间或计算能力。 我们很少看到研究人员通过穷举搜索来寻找最佳架构,因为选择的数量非常多,而且存在不只一个正确的答案。 取而代之的是,我们看到该领域的研究人员通过实验尝试建立已知的架构,以尝试创建新的新颖架构并改善现有架构。

因此,在介绍详尽搜索超参数的策略之前,让我们看一下两种推论出合理的,甚至不是最佳的网络架构的策略。

找到一个巨人然后站在他的肩膀上

沙特尔的伯纳德(Bernard of Chartres)被赋予了通过借鉴他人的发现来学习的概念。 但是,正是艾萨克·牛顿(Isaac Newton)说:“如果我进一步观察,那就是站在巨人的肩膀上。” 要明确的是,这正是我在这里建议的。

如果我要设计一个用于新的深度学习问题的网络架构,我要做的第一件事就是尝试找到一个令人满意的方式,以前已经解决了类似的问题。 尽管可能没有人能够解决您面临的任务,但可能存在类似的情况。

很可能存在几种可能的解决方案。 如果是这样,并且在时间允许的情况下,每次运行几次的平均结果可能会告诉您哪个运行效果最好。 当然,在这里我们发现自己很快进入了研究领域。

添加,直到过拟合,然后进行正则化

希望通过寻找类似问题的架构,您至少接近适合您的架构。 您如何做才能进一步优化网络架构?

  • 在多个实验运行中,添加层和/或神经元,直到您的网络开始针对问题过拟合。 在深度学习中,添加单元,直到您不再具有高偏差模型为止。
  • 一旦开始过拟合,您就会发现一些网络架构能够很好地拟合训练数据,甚至可能拟合得很好。 在这一点上,您应该集中精力通过使用丢弃,正则化,提早停止等方法来减少方差。

这种方法通常归因于著名的神经网络研究员 Geoffrey Hinton。 这是一个有趣的想法,因为它使过拟合不是要避免的事情,而是构建网络架构的良好第一步。

尽管没有规则可供我们选择最佳网络架构,并且可能存在许多最佳架构,但我发现这种策略在实践中对我来说非常有效。

实用建议

如果您对上述内容不太了解,我同意。 这对我也不是,我也不希望那样。 您当然可以在一组预定义的配置之间搜索最佳的网络架构,这也是正确的方法。 实际上,它可以说是更正确,更严格。 此过程旨在为您提供实用的建议,以帮助您在尽可能短的时间内达到最佳状态。

我们应该优化哪些超参数?

即使您遵循我的建议并选择了一个足够好的架构,您也可以并且仍然应该尝试在该架构中搜索理想的超参数。 我们可能要搜索的一些超参数包括:

  • 我们选择的优化器。 到目前为止,我一直在使用 Adam,但是 rmsprop 优化器或调整良好的 SGD 可能会更好。
  • 每个优化器都有一组我们可能需要调整的超参数,例如学习率,动量和衰减。
  • 网络权重初始化。
  • 神经元激活。
  • 正则化参数(例如丢弃概率)或 12 正则化中使用的正则化参数。
  • 批次大小。

如上所述,这不是详尽的清单。 当然,您可以尝试更多的选择,包括在每个隐藏层中引入可变数量的神经元,每层中丢弃概率的变化等等。 就像我们一直暗示的那样,超参数的可能组合是无限的。 这些选择也很可能并非独立于网络架构,添加和删除层可能会为这些超参数中的任何一个带来新的最佳选择。

超参数优化策略

在本章的这一点上,我们建议,在大多数情况下,尝试我们可能想尝试的每个超参数组合在计算上都是不可能的,或者至少是不切实际的。 深度神经网络肯定会花费很长时间进行训练。 尽管您可以并行处理问题并投入计算资源,但搜索超参数的最大限制可能仍然是时间。

如果时间是我们最大的限制,并且我们无法合理地探索拥有的所有可能性,那么我们将必须制定一种策略,使我们在拥有的时间内获得最大的效用。

在本节的其余部分,我将介绍一些用于超参数优化的常用策略,然后向您展示如何使用我最喜欢的两种方法在 Keras 中优化超参数。

通用策略

在所有机器学习模型中都有一套通用的超参数优化策略。 从总体上讲,这些策略包括:

  • 网格搜索
  • 随机搜索
  • 贝叶斯优化
  • 遗传算法
  • 机器学习的超参数

网格搜索只是尝试尝试所有事物,或者至少尝试离散事物,然后报告我们用蛮力找到的最佳超参数的最佳组合。 可以保证在我们确定的参数空间中找到最佳解决方案,以及其他较差的解决方案。

网格搜索对于深度学习并不是很实用。 除了最基本的深度神经网络,我们无法现实地探索所有可能参数的每个可能值。 使用随机搜索,我们从每个参数分布中随机抽样,并尝试其中的n,其中(n x每个示例训练时间)是我们愿意分配给这个问题的时间预算。

贝叶斯优化方法使用以前的观察结果来预测接下来要采样的超参数集。 尽管贝叶斯优化方法通常胜过蛮力技术,但目前的研究表明,与穷举方法相比,表现提升较小。 此外,由于贝叶斯方法取决于先前的经验,因此无论如何都不会令人尴尬地并行进行。

遗传算法是机器学习中非常有趣且活跃的研究领域。 但是,我目前的观点是,它们也不是深度神经网络参数优化的理想选择,因为它们再次依赖于先前的经验。

该领域中的一些最新研究着眼于训练神经网络,该神经网络可以预测给定网络架构的最佳参数。 可以参数化模型的模型的想法当然非常有趣,这是一个值得密切关注的地方。 这也可能是我们获得天网的方式。 只有时间证明一切。

在 scikit-learn 中使用随机搜索

使用 scikit-learn 可以轻松实现网格搜索和随机搜索。 在此示例中,我们将使用 Keras 的KerasClassifier类包装模型并使其与 scikit-learn API 兼容。 然后,我们将使用 scikit-learn 的RandomSearchCV类进行超参数搜索。

为此,我们将从稍微更改现在熟悉的模型构建函数开始。 我们将使用我们要搜索的超参数对其进行参数化,如以下代码所示:

代码语言:javascript复制
def build_network(keep_prob=0.5, optimizer='adam'):
    inputs = Input(shape=(784,), name="input")
    x = Dense(512, activation='relu', name="hidden1")(inputs)
    x = Dropout(keep_prob)(x)
    x = Dense(256, activation='relu', name="hidden2")(x)
    x = Dropout(keep_prob)(x)
    x = Dense(128, activation='relu', name="hidden3")(x)
    x = Dropout(keep_prob)(x)
    prediction = Dense(10, activation='softmax', name="output")(x)
    model = Model(inputs=inputs, outputs=prediction)
    model.compile(optimizer=optimizer, loss='categorical_crossentropy', 
                  metrics=["accuracy"])
    return model

在此示例中,我想搜索一个理想的丢弃值,并且我想尝试几个不同的优化器。 为了实现这一点,我需要将它们作为参数包含在函数中,以便可以通过我们的随机搜索方法对其进行更改。 当然,我们可以使用相同的方法来参数化和测试许多其他网络架构选择,但是我们在这里保持简单。

接下来,我们将创建一个函数,该函数返回一个字典,其中包含我们想搜索的所有可能的超参数及其值空间,如以下代码所示:

代码语言:javascript复制
def create_hyperparameters():
    batches = [10, 20, 30, 40, 50]
    optimizers = ['rmsprop', 'adam', 'adadelta']
    dropout = np.linspace(0.1, 0.5, 5)
    return {"batch_size": batches, "optimizer": optimizers, 
      "keep_prob": dropout}

剩下的就是使用RandomSearchCV将这两部分连接在一起。 首先,我们将模型包装到keras.wrappers.scikit_learn.KerasClassifier中,以便与 scikit-learn 兼容,如以下代码所示:

代码语言:javascript复制
model = KerasClassifier(build_fn=build_network, verbose=0)

接下来,我们将使用以下代码获得超参数字典:

代码语言:javascript复制
hyperparameters = create_hyperparameters()

然后,最后,我们将创建一个RandomSearchCV对象,该对象将用于搜索模型的参数空间,如以下代码所示:

代码语言:javascript复制
search = RandomizedSearchCV(estimator=model, param_distributions=hyperparameters, n_iter=10, n_jobs=1, cv=3, verbose=1)

拟合此RandomizedSearchCV对象后,它将从参数分布中随机选择值并将其应用于模型。 它将执行 10 次(n_iter=10),并且将尝试每种组合 3 次,因为我们使用了 3 倍交叉验证。 这意味着我们将总共拟合模型 30 次。 使用每次运行的平均准确率,它将返回最佳模型作为类属性.best_estimator,并且将返回最佳参数作为.best_params_

为了适合它,我们只需调用它的fit方法,就好像它是一个模型一样,如以下代码所示:

代码语言:javascript复制
search.fit(data["train_X"], data["train_y"])

print(search.best_params_)

在 Tesla K80 GPU 实例上,在上述网格上拟合第 5 章,“使用 Keras 进行多分类”所使用的 MNIST 模型。 在完成本节之前,让我们看一下搜索的一些输出,如以下代码所示:

代码语言:javascript复制
Using TensorFlow backend.
 Fitting 3 folds for each of 10 candidates, totalling 30 fits
tensorflow/core/common_runtime/gpu/gpu_device.cc:1030] Found device 0 with properties:
 name: Tesla K80 major: 3 minor: 7 memoryClockRate(GHz): 0.8235
 pciBusID: 0000:00:1e.0
 totalMemory: 11.17GiB freeMemory: 11.10GiB
tensorflow/core/common_runtime/gpu/gpu_device.cc:1120] Creating TensorFlow device (/device:GPU:0) -> (device: 0, name: Tesla K80, pci bus id: 0000:00:1e.0, compute capability: 3.7)
 [Parallel(n_jobs=1)]: Done 30 out of 30 | elapsed: 8.8min finished
 {'keep_prob': 0.20000000000000001, 'batch_size': 40, 'optimizer': 'adam'}

如您在此输出中看到的,在 10 次运行中,加粗的超参数似乎是表现最好的集合。 当然,我们当然可以运行更多的迭代,并且我们可能会找到一个更好的选择。 我们的预算仅由时间,耐心以及云帐户附带的信用卡决定。

Hyperband

Hyperband 是一项超参数优化技术,由 Lisha Li,Kevin Jamieson,Guilia DeSalvo,Afshin Rostamizadeh 和 Ameet Talwalker 于 2016 年在伯克利开发。 您可以在这里阅读他们的原始论文。

想象一下,就像我们在RandomSearchCV中所做的那样,随机采样许多潜在的超参数集。 完成RandomSearchCV后,它将选择一个单一的超参数配置作为其采样的最优值。 Hyperband 利用这样的思想,即即使经过少量迭代,最佳的超参数配置也可能会胜过其他配置。 Hyperband 中的乐队来自土匪,指的是基于多臂土匪技术(用于优化竞争选择之间的资源分配以优化表现为目标的技术)的勘探与开发。

使用 Hyperband,我们可以尝试一些可能的配置集(n),仅训练一次迭代。 作者将迭代一词留作多种可能的用途。 但是,我将周期作为迭代。 一旦完成第一个训练循环,就将根据表现对结果进行配置。 然后,对该列表的上半部分进行大量迭代的训练。 然后重复进行减半和剔除的过程,我们得到了一些非常小的配置集,我们将针对在搜索中定义的完整迭代次数进行训练。 与在每种可能的配置中搜索最大周期相比,此过程使我们在更短的时间内获得了最佳超参数集。

在本章的 GitHub 存储库中,我在hyperband.py中包括了hyperband算法的实现。 此实现主要源自 FastML 的实现,您可以在这个页面中找到。 要使用它,您需要首先实例化一个hyperband对象,如以下代码所示:

代码语言:javascript复制
from hyperband import Hyperband
hb = Hyperband(data, get_params, try_params)

Hyperband 构造器需要三个参数:

  • data:到目前为止,我在示例中一直在使用的数据字典
  • get_params:用于从我们正在搜索的超参数空间中采样的函数的名称
  • try_param:可用于评估n_iter迭代的超参数配置并返回损失的函数的名称

在下面的示例中,我实现了get_params以在参数空间中以统一的方式进行采样:

代码语言:javascript复制
def get_params():
    batches = np.random.choice([5, 10, 100])
    optimizers = np.random.choice(['rmsprop', 'adam', 'adadelta'])
    dropout = np.random.choice(np.linspace(0.1, 0.5, 10))
    return {"batch_size": batches, "optimizer": optimizers, 
      "keep_prob": dropout}

如您所见,所选的超参数配置将作为字典返回。

接下来,可以实现try_params以在超参数配置上针对指定的迭代次数拟合模型,如下所示:

代码语言:javascript复制
def try_params(data, num_iters, hyperparameters):
    model = build_network(keep_prob=hyperparameters["keep_prob"],
                           optimizer=hyperparameters["optimizer"])
    model.fit(x=data["train_X"], y=data["train_y"],
              batch_size=hyperparameters["batch_size"],
              epochs=int(num_iters))
    loss = model.evaluate(x=data["val_X"], y=data["val_y"], verbose=0)
    return {"loss": loss}

try_params函数返回一个字典,可用于跟踪任何数量的度量; 但是,由于它用于比较运行,因此需要损失。

通过在对象上调用.run()方法,hyperband对象将通过我们上面描述的算法运行。

代码语言:javascript复制
results = hb.run()

在这种情况下,results将是每次运行,其运行时间和测试的超参数的字典。 因为即使这种高度优化的搜索都需要花费大量时间,并且 GPU 时间也很昂贵,所以我将 MNIST 搜索的结果包括在本章的 GitHub 存储库的hyperband-output-mnist.txt中,可以在以下位置找到。

总结

超参数优化是从我们的深度神经网络获得最佳效果的重要一步。 寻找搜索超参数的最佳方法是机器学习研究的一个开放而活跃的领域。 尽管您当然可以将最新技术应用于自己的深度学习问题,但您需要在决策中权衡实现的复杂性和搜索运行时间。

有一些与网络架构有关的决策可以肯定地进行详尽地搜索,但是,如我上面提供的那样,一组启发式方法和最佳实践可能使您足够接近甚至减少搜索参数的数量。

最终,超参数搜索是一个经济问题,任何超参数搜索的第一部分都应考虑您的计算时间和个人时间预算,以试图找出最佳的超参数配置。

本章总结了深度学习的基础。 在下一章中,我们将从计算机视觉入手,介绍神经网络的一些更有趣和更高级的应用。

七、从头开始训练 CNN

深度神经网络彻底改变了计算机视觉。 实际上,我认为在最近几年中计算机视觉的进步已经使深层神经网络成为许多消费者每天使用的东西。 我们已经在第 5 章“使用 Keras 进行多分类”中使用计算机视觉分类器,其中我们使用了深度网络对手写数字进行分类。 现在,我想向您展示卷积层如何工作,如何使用它们以及如何在 Keras 中构建自己的卷积神经网络以构建更好,功能更强大的深度神经网络来解决计算机视觉问题。

我们将在本章介绍以下主题:

  • 卷积介绍
  • 在 Keras 中训练卷积神经网络
  • 使用数据增强

卷积介绍

经过训练的卷积层由称为过滤器的许多特征检测器组成,这些特征检测器在输入图像上滑动作为移动窗口。 稍后我们将讨论过滤器内部的内容,但现在它可能是一个黑匣子。 想象一个已经训练过的过滤器。 也许该过滤器已经过训练,可以检测图像中的边缘,您可能会认为这是黑暗与明亮之间的过渡。 当它经过图像时,其输出表示它检测到的特征的存在和位置,这对于第二层过滤器很有用。 稍微扩展一下我们的思想实验,现在想象第二个卷积层中的一个过滤器,它也已经被训练过了。 也许这个新层已经学会了检测直角,其中存在由上一层找到的两个边缘。 不断地我们去; 随着我们添加层,可以了解更多复杂的特征。 特征层次结构的概念对于卷积神经网络至关重要。 下图来自 Honglak Lee 等人的《使用卷积深度信念网络的无监督学习层次表示》[2011],非常好地说明了特征层次结构的概念:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v77D3C0B-1681567890351)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/c63ca1b1-5e19-423c-8174-d62c87d452bc.png)]

这是一种非常强大的技术,它比我们先前在 MNIST 上使用的深度学习flattenclassify方法具有多个优势。 我们将在短期内讨论这些内容,但首先让我们深入了解过滤器。

卷积层如何工作?

在上一节中,我说过卷积层是一组充当特征检测器的过滤器。 在我们深入探讨该架构之前,让我们回顾一下卷积实际上是什么的数学。

让我们首先手动将以下4 x 4矩阵与3 x 3矩阵卷积,我们将其称为过滤器。 卷积过程的第一步是获取过滤器与4 x 4矩阵的前九个框的按元素乘积:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WpmczkQM-1681567890352)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/47bb2d29-6a4a-46d5-8193-51c49ee62817.jpg)]

完成此操作后,我们将过滤器滑到一行上并执行相同的操作。 最后,我们将过滤器向下滑动,然后再次滑动。 卷积过程一旦完成,将使我们剩下2x2矩阵,如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wEka2Ahw-1681567890352)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/1ffeca84-f312-4324-bb86-19417a50f596.jpg)]

从技术上讲,这不是卷积,而是互相关。 按照惯例,我们将其称为卷积,并且就我们的目的而言,差异确实很小。

三维卷积

MNIST 是一个灰度示例,我们可以将每个图像表示为二维矩阵中从 0 到 255 的像素强度值。 但是,大多数时候,我们将使用彩色图像。 彩色图像实际上是三维矩阵,其中维是图像高度,图像宽度和颜色。 这将为图像中的每个像素生成一个矩阵,分别具有红色,蓝色和绿色值。

虽然我们先前展示的是二维过滤器,但我们可以通过在(高度,宽度,3(颜色))矩阵与3 x 3 x 3之间进行卷积来将其思想轻松转换为三个维度。 过滤。 最后,当我们在矩阵的所有三个轴上进行逐元素乘积运算时,仍然剩下二维输出。 提醒一下,这些高维矩阵通常称为张量,而我们正在做的就是使它们流动。

卷积层

之前我们已经讨论了由多个线性函数单元以及一些非线性(例如relu)组成的深度神经网络层。 在卷积层中,每个单元都是一个过滤器,结合了非线性。 例如,可以在 Keras 中定义卷积层,如下所示:

代码语言:javascript复制
from keras.layers import Conv2D
Conv2D(64, kernel_size=(3,3), activation="relu", name="conv_1")

在此层中,有 64 个独立的单元,每个单元都有3 x 3 x 3过滤器。 卷积操作完成后,每个单元都会像传统的完全连接层中那样为输出添加偏置和非线性(稍后会详细介绍该术语)。

在继续之前,让我们快速浏览一下示例的维度,以便确保我们都在同一页面上。 想象一下,我们有一个32 x 32 x 3的输入图像。 现在,我们将其与上述卷积层进行卷积。 该层包含 64 个过滤器,因此输出为30 x 30 x 64。 每个过滤器输​​出一个30 x 30矩阵。

卷积层的好处

因此,现在您希望对卷积层的工作原理有所了解,让我们讨论为什么我们要进行所有这些疯狂的数学运算。 为什么我们要使用卷积层而不是以前使用的普通层?

假设我们确实使用了普通层,以得到与之前讨论的相同的输出形状。 我们从32 x 32 x 3图像开始,所以总共有 3,072 个值。 我们剩下一个30 x 30 x 64矩阵。 总共有 57,600 个值。 如果我们要使用完全连接的层来连接这两个矩阵,则该层将具有 176,947,200 个可训练参数。 那是 1.76 亿。

但是,当我们使用上面的卷积层时,我们使用了 64 个3 x 3 x 3过滤器,这将导致 1,728 个可学习权重加 64 个偏差(总共 1,792 个参数)。

因此,显然卷积层需要的参数要少得多,但是为什么这很重要呢?

参数共享

由于过滤器是在整个图像中使用的,因此过滤器会学会检测特征,而不管其在图像中的位置如何。 事实证明,这非常有用,因为它为我们提供了平移不变性,这意味着我们可以检测到重要的内容,而不管其在整个图像中的朝向。

回想一下 MNIST,不难想象我们可能想检测 9 的循环,而不管它在照片中的位置如何。 提前思考,想象一个将图片分类为猫或汽车的分类器。 容易想象有一组过滤器可以检测出像汽车轮胎一样复杂的东西。 无论轮胎的方向在图像中的什么位置,检测该轮胎都是有用的,因为轮胎之类的东西强烈表明该图像不是猫(除非图像是驾驶汽车的猫)。

本地连接

过滤器由于其固定大小而着重于相邻像素之间的连通性。 这意味着他们将最强烈地学习本地特征。 当与其他过滤器以及层和非线性结合使用时,这使我们逐渐关注更大,更复杂的特征。 确实需要这种局部化特征的堆叠,这也是卷积层如此之大的关键原因。

池化层

除了卷积层,卷积神经网络通常使用另一种类型的层,称为池化层。 当添加卷积层时,使用池化层来减少卷积网络的维数,这会减少过拟合。 它们具有使特征检测器更坚固的附加好处。

池化层将矩阵划分为非重叠部分,然后通常在每个区域中采用最大值(在最大池化的情况下)。 可替代地,可以采用平均值。 但是,目前很少使用。 下图说明了此技术:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1zfRWe8l-1681567890353)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/020a3a99-27f2-41b0-b04f-7cb86e562db5.jpg)]

如我们所料,池化层在 Keras 中很容易实现。 以下代码可用于池化各层:

代码语言:javascript复制
from keras.layers import MaxPooling2D
pool1 = MaxPooling2D(pool_size=(2, 2), name="pool_1")

在这里,我们将池窗口定义为2 x 2

尽管我们之前没有讨论过填充,但是在某些架构中,通常将卷积层或池化层的输入填充为 0,以使输出尺寸等于输入。 Keras 的卷积层和池化层中的默认值都是有效填充,这意味着按惯例没有填充。 如果需要,参数padding="same"将应用填充。

批量标准化

批量规范化有助于我们的网络整体表现更好,学习速度更快。 批量规范化在应用中也很容易理解。 但是,为什么它起作用,仍然受到研究人员的争议。

使用批量归一化时,对于每个小批量,我们可以在每个非线性之后(或之前)对那个批量进行归一化,使其平均值为 0,单位方差。 这使每一层都可以从中学习标准化输入,从而使该层的学习效率更高。

批归一化层很容易在 Keras 中实现,本章的示例将在每个卷积层之后使用它们。 以下代码用于批量规范化:

代码语言:javascript复制
from keras.layers import BatchNormalization
x = BatchNormalization(name="batch_norm_1")

在 Keras 中训练卷积神经网络

现在我们已经介绍了卷积神经网络的基础知识,是时候构建一个了。 在本案例研究中,我们将面对一个众所周知的问题,即 CIFAR-10。 该数据集由 Alex Krizhevsky,Vinod Nair 和 Geoffrey Hinton 创建。

输入

CIFAR-10 数据集由属于 10 类的 60,000 张32 x 32彩色图像组成,每类 6,000 张图像。 我将使用 50,000 张图像作为训练集,使用 5,000 张图像作为验证集,并使用 5,000 张图像作为测试集。

卷积神经网络的输入张量层将为(N, 32, 32, 3),我们将像以前一样将其传递给build_network函数。 以下代码用于构建网络:

代码语言:javascript复制
def build_network(num_gpu=1, input_shape=None):
   inputs = Input(shape=input_shape, name="input")

输出

该模型的输出将是 0-9 之间的类别预测。 我们将使用与 MNIST 相同的 10 节点softmax。 令人惊讶的是,我们的输出层没有任何变化。 我们将使用以下代码来定义输出:

代码语言:javascript复制
output = Dense(10, activation="softmax", name="softmax")(d2)

成本函数和指标

在第 5 章中,我们使用分类交叉熵作为多分类器的损失函数。 这只是另一个多分类器,我们可以继续使用分类交叉熵作为我们的损失函数,并使用准确率作为度量。 我们已经开始使用图像作为输入,但是幸运的是我们的成本函数和指标保持不变。

卷积层

如果您开始怀疑此实现中是否会有任何不同之处,那就是这里。 我将使用两个卷积层,分别进行批量规范化和最大池化。 这将要求我们做出很多选择,当然我们以后可以选择作为超参数进行搜索。 不过,最好先让某些东西开始工作。 正如 Donald Knuth 所说,过早的优化是万恶之源。 我们将使用以下代码片段定义两个卷积块:

代码语言:javascript复制
# convolutional block 1
conv1 = Conv2D(64, kernel_size=(3,3), activation="relu", name="conv_1")(inputs)
batch1 = BatchNormalization(name="batch_norm_1")(conv1)
pool1 = MaxPooling2D(pool_size=(2, 2), name="pool_1")(batch1)

# convolutional block 2
conv2 = Conv2D(32, kernel_size=(3,3), activation="relu", name="conv_2")(pool1)
batch2 = BatchNormalization(name="batch_norm_2")(conv2)
pool2 = MaxPooling2D(pool_size=(2, 2), name="pool_2")(batch2)

因此,很明显,我们在这里有两个卷积块,它们由一个卷积层,一个批量规范化层和一个池化层组成。

在第一块中,我使用具有relu激活函数的 64 个3 x 3过滤器。 我使用的是有效(无)填充,跨度为 1。批量规范化不需要任何参数,并且实际上不是可训练的。 池化层使用2 x 2池化窗口,有效填充和跨度为 2(窗口尺寸)。

第二个块几乎相同。 但是,我将过滤器数量减半为 32。

尽管在该架构中有许多旋钮可以转动,但我首先要调整的是卷积的内核大小。 内核大小往往是一个重要的选择。 实际上,一些现代的神经网络架构(例如 Google 的 Inception)使我们可以在同一卷积层中使用多个过滤器大小。

全连接层

经过两轮卷积和合并后,我们的张量变得相对较小和较深。 在pool_2之后,输出尺寸为(n, 6, 6, 32)

我们希望在这些卷积层中提取此6 x 6 x 32张量表示的相关图像特征。 为了使用这些特征对图像进行分类,在进入最终输出层之前,我们将将该张量连接到几个完全连接的层。

在此示例中,我将使用 512 神经元完全连接层,256 神经元完全连接层以及最后的 10 神经元输出层。 我还将使用丢弃法来帮助防止过拟合,但只有一点点! 该过程的代码如下,供您参考:

代码语言:javascript复制
from keras.layers import Flatten, Dense, Dropout
# fully connected layers
flatten = Flatten()(pool2)
fc1 = Dense(512, activation="relu", name="fc1")(flatten)
d1 = Dropout(rate=0.2, name="dropout1")(fc1)
fc2 = Dense(256, activation="relu", name="fc2")(d1)
d2 = Dropout(rate=0.2, name="dropout2")(fc2)

我之前没有提到上面的flatten层。 flatten层完全按照其名称的含义执行。 将flattensn x 6 x 6 x 32张量flattens转换为n x 1152向量。 这将作为全连接层的输入。

Keras 中的多 GPU 模型

许多云计算平台可以提供包含多个 GPU 的实例。 随着我们模型的规模和复杂性的增长,您可能希望能够跨多个 GPU 并行化工作负载。 这在本机 TensorFlow 中可能涉及到一些过程,但是在 Keras 中,这只是一个函数调用。

正常构建模型,如以下代码所示:

代码语言:javascript复制
model = Model(inputs=inputs, outputs=output)

然后,我们借助以下代码将该模型传递给keras.utils.multi_gpu_model

代码语言:javascript复制
model = multi_gpu_model(model, num_gpu)

在此示例中,num_gpu是我们要使用的 GPU 的数量。

训练

将模型放在一起,并结合我们新的 CUDA GPU 功能,我们提出了以下架构:

代码语言:javascript复制
def build_network(num_gpu=1, input_shape=None):
    inputs = Input(shape=input_shape, name="input")

    # convolutional block 1
    conv1 = Conv2D(64, kernel_size=(3,3), activation="relu", 
      name="conv_1")(inputs)
    batch1 = BatchNormalization(name="batch_norm_1")(conv1)
    pool1 = MaxPooling2D(pool_size=(2, 2), name="pool_1")(batch1)

    # convolutional block 2
    conv2 = Conv2D(32, kernel_size=(3,3), activation="relu", 
      name="conv_2")(pool1)
    batch2 = BatchNormalization(name="batch_norm_2")(conv2)
    pool2 = MaxPooling2D(pool_size=(2, 2), name="pool_2")(batch2)

    # fully connected layers
    flatten = Flatten()(pool2)
    fc1 = Dense(512, activation="relu", name="fc1")(flatten)
    d1 = Dropout(rate=0.2, name="dropout1")(fc1)
    fc2 = Dense(256, activation="relu", name="fc2")(d1)
    d2 = Dropout(rate=0.2, name="dropout2")(fc2)

    # output layer
    output = Dense(10, activation="softmax", name="softmax")(d2)

    # finalize and compile
    model = Model(inputs=inputs, outputs=output)
    if num_gpu > 1:
        model = multi_gpu_model(model, num_gpu)
    model.compile(optimizer='adam', loss='categorical_crossentropy', 
      metrics=["accuracy"])
    return model

我们可以使用它来构建我们的模型:

代码语言:javascript复制
model = build_network(num_gpu=1, input_shape=(IMG_HEIGHT, IMG_WIDTH, CHANNELS))

然后,我们可以满足您的期望:

代码语言:javascript复制
model.fit(x=data["train_X"], y=data["train_y"],
          batch_size=32,
          epochs=200,
          validation_data=(data["val_X"], data["val_y"]),
          verbose=1,
          callbacks=callbacks)

在我们训练该模型时,您会注意到过拟合是一个紧迫的问题。 即使只有相对较小的两个卷积层,我们也已经有点过拟合了。

您可以从以下图形中看到过拟合的影响:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PefGxQt9-1681567890353)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/bc72fd8e-e789-444f-836f-7ad5e015642c.png)]

不足为奇,50,000 次观察不是很多数据,尤其是对于计算机视觉问题。 在实践中,计算机视觉问题得益于非常大的数据集。 实际上,Chen Sun 指出,附加数据倾向于以数据量的对数线性帮助计算机视觉模型。 不幸的是,在这种情况下,我们无法真正找到更多数据。 但是也许我们可以做些。 接下来让我们讨论数据增强。

使用数据增强

数据增强是一种将变换应用于图像并使用原始图像和变换后的图像进行训练的技术。 想象一下,我们有一个训练类,里面有一只猫:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HVAzkyxi-1681567890354)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/f2fc9117-dda1-40a5-b677-7b5bdaddb3fb.jpg)]

如果将水平翻转应用于此图像,我们将得到如下所示的内容:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hs4EybK2-1681567890354)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/9c828535-9310-4a38-9777-53ecf8041d54.jpg)]

当然,这是完全相同的图像,但是我们可以将原始图像和转换图像用作训练示例。 这不像我们训练中的两只猫那么好。 但是,它的确使我们可以告诉计算机,无论猫面对什么方向,猫都是猫。

在实践中,我们可以做的不仅仅是水平翻转。 当有意义时,我们也可以垂直翻转,移动和随机旋转图像。 这使我们能够人为地放大我们的数据集,并使它看起来比实际的更大。 当然,您只能将其推到目前为止,但这是在存在少量数据的情况下防止过拟合的一个非常强大的工具。

Keras ImageDataGenerator

不久前,进行图像增强的唯一方法是对转换进行编码,并将其随机应用于训练集,然后将转换后的图像保存在磁盘上(上下坡,在雪中)。 对我们来说幸运的是,Keras 现在提供了ImageDataGenerator类,可以在我们训练时即时应用转换,而无需手工编码转换。

我们可以通过实例化ImageDataGenerator来创建一个数据生成器对象,如下所示:

代码语言:javascript复制
def create_datagen(train_X):
    data_generator = ImageDataGenerator(
        rotation_range=20,
        width_shift_range=0.02,
        height_shift_range=0.02,
        horizontal_flip=True)
    data_generator.fit(train_X)
    return data_generator

在此示例中,我同时使用了移位,旋转和水平翻转。 我只使用很小的移位。 通过实验,我发现更大的变化太多了,而且我的网络实际上无法学到任何东西。 您的经验会随着您的问题而变化,但是我希望较大的图像更能容忍移动。 在这种情况下,我们使用 32 个像素的图像,这些图像非常小。

用生成器训练

如果您以前没有使用过生成器,则它就像迭代器一样工作。 每次调用ImageDataGenerator .flow()方法时,它都会产生一个新的训练小批量,并将随机变换应用于所馈送的图像。

Keras Model类带有.fit_generator()方法,该方法使我们可以使用生成器而不是给定的数据集:

代码语言:javascript复制
model.fit_generator(data_generator.flow(data["train_X"], data["train_y"], batch_size=32),
                    steps_per_epoch=len(data["train_X"]) // 32,
                    epochs=200,
                    validation_data=(data["val_X"], data["val_y"]),
                    verbose=1,
                    callbacks=callbacks)

在这里,我们用生成器替换了传统的xy参数。 最重要的是,请注意steps_per_epoch参数。 您可以从训练集中任意采样替换次数,并且每次都可以应用随机变换。 这意味着我们每个周期可以使用的迷你批数比数据还多。 在这里,我将仅根据观察得到的样本数量进行采样,但这不是必需的。 如果可以,我们可以并且应该将这个数字提高。

在总结之前,让我们看一下这种情况下图像增强的好处:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7RQrmNkK-1681567890355)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/fc4d1f15-9e77-4b74-bcb5-c783b63bf35f.png)]

如您所见,仅一点点图像增强确实帮助了我们。 不仅我们的整体精度更高,而且我们的网络过拟合的速度也慢得多。 如果您的计算机视觉问题只包含少量数据,那么图像增强就是您想要做的事情。

总结

在本章中,我们快速介绍了许多基础知识。 我们讨论了卷积层及其如何用于神经网络。 我们还介绍了批量规范化,池化层和数据增强。 最后,我们使用 Keras 从零开始训练卷积神经网络,然后使用数据增强对该网络进行改进。

我们还讨论了如何基于数据的渴望计算机视觉的深度神经网络问题。 在下一章中,我将向您展示迁移学习,这是我最喜欢的技术之一。 这将帮助您快速解决计算机视觉问题,并获得惊人的结果并且数据量更少。

八、将预训练的 CNN 用于迁移学习

迁移学习很棒。 实际上,在一本充满奇妙事物的书中,这可能是我必须告诉您的最奇妙的事物。 如果没有,那也许至少是我可以教给您的最有用和最实用的深度学习技术。 迁移学习可以帮助您解决深度学习问题,尤其是计算机视觉问题,而涉及问题范围的数据和数据却很少。 在本章中,我们将讨论什么是迁移学习,什么时候应该使用它,最后讨论如何在 Keras 中进行迁移学习。

我们将在本章介绍以下主题:

  • 迁移学习概述
  • 何时应使用迁移
  • 源/目标数量和相似性的影响
  • Keras 的迁移学习

迁移学习概述

在第 7 章和“卷积神经网络”中,我们训练了约 50,000 个观测值的卷积神经网络,并且由于网络和问题的复杂性,在开始训练的短短几个周期后,我们过拟合了。 如果您还记得的话,我曾评论说我们的训练集中有 50,000 个观察结果对于计算机视觉问题不是很大。 确实如此。 计算机视觉问题喜欢数据,而我们可以提供给他们的数据越多,它们的表现就越好。

我们可能认为计算机视觉技术最先进的深度神经网络通常在称为 ImageNet 的数据集上进行训练。 ImageNet数据集是包含 120 万张图像的 1,000 个分类器。 这还差不多! 如此庞大的数据集使研究人员能够构建真正复杂的深度神经网络,以检测复杂的特征。 当然,在 120 万张图像上训练有时具有 100 多个层的模型的价格很高。 训练可能需要数周和数月,而不是数小时。

但是,如果我们可以从一个最先进的,多层的,经过数百万张图像训练的网络开始,然后仅使用少量数据将该网络应用于我们自己的计算机视觉问题,该怎么办? 那就是迁移学习

要使用迁移学习,我们将执行以下步骤:

  1. 从训练非常大的复杂计算机视觉问题的模型开始; 我们将其称为我们的源域
  2. 删除网络的最后一层(softmax层),并可能删除其他完全连接的层
  3. 将最后几层替换为适合我们新问题的层,我们将其称为目标域
  4. 冻结所有已训练的层,使其权重不变
  5. 在目标域数据上训练网络

如果我们在这里停止,这通常被称为特征提取,因为我们正在使用在源域上训练的网络来提取目标域的视觉特征。 然后,我们使用栓接到该特征提取网络上的相对较小的神经网络来执行目标域任务。 根据我们的目标和数据集,这可能就足够了。

可选地,我们将通过解冻一些或所有冻结的层来微调整个网络,然后通常以很小的学习率再次进行训练。 我们将在短期内讨论何时使用微调,但是请确保我们涵盖了首先使用迁移学习的一些原因。

何时应使用迁移

当您的数据有限且存在解决类似问题的网络时,迁移学习会非常有效。 您可以使用迁移学习将最先进的网络和大量数据带入一个其他小的问题。 那么,什么时候应该使用迁移学习? 随时可以! 但是,我希望您首先考虑两个规定。 我们将在以下各节中讨论它们。

数据有限

关于计算机视觉和迁移学习,我最常被问到的问题是:我必须拥有多少张图像? 这是一个很难回答的问题,因为,正如我们将在下一节中看到的那样,更多通常更好。 一个更好的问题可能是:我可以使用几张图像来充分解决我的业务问题?

那么,我们的数据集有多有限? 尽管远非科学,但我已经建立了使用多达 2,000 张图像进行二分类任务的有用模型。 更简单的任务和更多样化的图像集通常可以在较小的数据集下获得更令人满意的结果。

根据经验,您至少需要几千张某类的图像,而通常最好使用 10 至 2 万张图像。

常见问题域

如果您的目标域至少与源域有些相似,那么迁移学习会很有效。 例如,假设您正在将图像分类为包含猫或狗。 有许多ImageNet训练有素的图像分类器非常适合用于此类型或问题。

相反,让我们想象我们的问题是将 CT 扫描或 MRI 归类为是否包含肿瘤。 此目标域与ImageNet源域非常不同。 这样,虽然使用迁移学习可能(并且可能会)有好处,但我们将需要更多的数据,并且可能需要进行一些微调才能使网络适应此目标域。

源/目标数量和相似性的影响

直到最近,很少有人研究数据量和源/目标域相似性对迁移学习表现的影响。 但是,这是一个对迁移学习的可用性很重要的主题,也是我撰写的主题。 在我的同事撰写的《调查数据量和域相似性对迁移学习应用的影响》中,对这些主题进行了一些实验。 这就是我们发现的东西。

更多数据总是有益的

Google 研究人员在《重新研究深度学习周期数据的不合理有效性》中进行的几次实验中,构建了一个内部数据集,其中包含 3 亿个观测值,显然比ImageNet大得多。 然后,他们在该数据集上训练了几种最先进的架构,从而使模型显示的数据量从 1000 万增加到 3000 万,1 亿,最后是 3 亿。 通过这样做,他们表明模型表现随用于训练的观察次数的对数线性增加,这表明在源域中,更多的数据总是有帮助。

但是目标域呢? 我们使用了一些类似于我们在迁移学习过程中可能使用的类型的数据集重复了 Google 实验,包括我们将在本章稍后使用的Dogs versus Cats数据集。 我们发现,在目标域中,模型的表现随用于训练的观察次数的对数线性增加,就像在源域中一样。 更多数据总是有帮助的。

源/目标域相似度

迁移学习的独特之处在于您担心源域和目标域之间的相似度。 经过训练以识别人脸的分类器可能不会轻易迁移到识别各种架构的目标领域。 我们进行了源和目标尽可能不同的实验,以及源和目标域非常相似的实验。 毫不奇怪,当迁移学习应用中的源域和目标域非常不同时,与相似时相比,它们需要更多的数据。 它们也需要更多的微调,因为当域在视觉上非常不同时,特征提取层需要大量的学习。

Keras 的迁移学习

与本书中的其他示例不同,在这里我们将需要涵盖目标域问题,源域问题以及我们正在使用的网络架构。 我们将从目标域的概述开始,这是我们要解决的问题。 然后,我们将介绍网络最初经过训练的源域,并简要介绍我们将使用的网络架构。 然后,我们将在本章的其余部分中将问题联系在一起。 我们需要分别考虑两个域,因为它们的大小和相似性与网络表现密切相关。 目标和源的类型越近,结果越好。

目标域概述

在本章的示例中,我将使用 Kaggle 的Dogs versus Cats数据集。 该数据集包含 25,000 张猫和狗的图像。 每个类别之间达到完美平衡,每个类别 12,500。 可以从这里下载数据集。

这是一个二分类问题。 每张照片都包含狗或猫,但不能同时包含两者。

该数据集由 Jeremy Elson 等人于 2007 年组装。 ,它目前托管在 www.kaggle.com 上。 它是完全免费下载和用于学术用途的,但是它确实需要一个 Kaggle 帐户并接受其最终用户许可。 一样,这是一个了不起的数据集,因此我在此处包括使用说明。

源域概述

我们将从在 ImageNet 上训练的深度神经网络开始。 如果您从“迁移学习概述”部分中回顾过,ImageNet是一个 1,000 类分类器,训练了大约 120 万张图像。 ImageNet数据集中都包含狗和猫的图像,因此在这种情况下,我们的目标域实际上与我们的源域非常相似。

源网络架构

我们将使用 Inception-V3 网络架构。 与您到目前为止在本书中所看到的相比,Inception 架构很有趣并且非常复杂。 如果您从第 7 章,“卷积神经网络”中回想起,我们必须围绕网络架构做出的决定之一就是选择过滤器大小。 对于每一层,我们必须决定是否应使用例如3 x 3过滤器,而不是5 x 5过滤器。 当然,也许根本就不需要另一次卷积。 也许像池化之类的东西可能更合适。 因此,如果我们在每一层都做所有事情,该怎么办? 这就是开始的动机。

该架构基于一系列模块或称为初始模块的构建块。 在每个初始模块中,先前的激活都赋予1 x 1卷积,3 x 3卷积,5 x 5卷积和最大池化层。 然后将输出连接在一起。

Inception-V3 网络由几个相互堆叠的 Inception 模块组成。 最后两层都完全连接,输出层是 1,000 个神经元 softmax。

通过使用keras.applications.inception_v3中的InceptionV3类,我们可以加载 Inception-V3 网络及其权重。 Keras 的网络动物园中有几个流行的网络,它们都位于keras.applications内部。 只需多一点工作就可以加载在 TensorFlow 中创建的模型。 也可以转换在其他架构中训练的模型,但这不在快速参考的范围之内。

要加载 Inception,我们只需要实例化一个InceptionV3对象,它是 Keras 模型,如以下代码所示:

代码语言:javascript复制
from keras.applications.inception_v3 import InceptionV3
base_model = InceptionV3(weights='imagenet', include_top=False)

您可能会注意到,我们在这里说了include_top=False,这表明我们不需要网络的顶层。 这免除了我们手动清除它们的工作。 第一次运行此代码时,它将下载 Inception-V3 网络架构并保存权重并将其缓存给我们。 现在,我们只需要添加我们自己的完全连接的层即可。

迁移网络架构

我们将用更适合我们的用例的全连接层替换最后两层。 由于我们的问题是二分类,因此我们将使用激活sigmoid的单个神经元替换输出层,如以下代码所示:

代码语言:javascript复制
# add a global spatial average pooling layer
x = base_model.output
x = GlobalAveragePooling2D()(x)
# let's add a fully-connected layer
x = Dense(1024, activation='relu')(x)
# and a logistic layer
predictions = Dense(1, activation='sigmoid')(x)

# this is the model we will train
model = Model(inputs=base_model.input, outputs=predictions)

注意,我们在这里使用GlobalAveragePooling2D层。 该层将前一层的 4D 输出平坦化为 2D 层,通过求平均将其适合于我们的全连接层。 通过指定pooling='avg' or 'max'来加载基本模型时,也可以完成此操作。 这是您如何处理此问题的电话。

至此,我们已经准备好训练网络。 但是,在执行此操作之前,我们需要记住冻结基本模型中的层,以免新的完全连接的层疯狂地试图学习时它们的权重不变。 为此,我们可以使用以下代码遍历各层并将其设置为不可训练:

代码语言:javascript复制
for layer in base_model.layers:
   layer.trainable = False

数据准备

我们将首先从 Kaggle 下载数据,然后将train.zip解压缩到本书的Chapter08目录中。 现在,您将拥有一个名为train/的目录,其中包含 25,000 张图像。 每个名称都将类似于cat.number.jpg

我们想移动这些数据,以便我们为训练,验证和测试创建单独的目录。 这些目录中的每一个都应具有猫和狗的目录。 这都是非常无聊且平凡的工作,因此,我创建了data_setup.py来为您执行此操作。 一旦运行它,数据将在本章的其余部分中全部格式化。

完成后,您将拥有一个具有以下结构的数据目录:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pn4o81VB-1681567890355)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/407018fd-63e4-450b-a1bd-640aad3b8e39.jpg)]

数据输入

快速浏览图像应使您确信我们的图像的分辨率和大小均不同。 正如您从第 7 章,“卷积神经网络”,所了解的那样,我们需要这些图像的大小与神经网络的输入张量一致。 这是一个非常现实的问题,您将经常面对计算机视觉任务。 虽然当然可以使用 ImageMagick 之类的程序来批量调整图像大小,但 Keras ImageDataGenerator类可用于快速调整图像大小,这就是我们要做的。

Inception-V3 期望299 x 299 x 3图像。 我们可以在数据生成器中指定此目标大小,如以下代码所示:

代码语言:javascript复制
train_datagen = ImageDataGenerator(rescale=1./255)
val_datagen = ImageDataGenerator(rescale=1./255)

train_generator = train_datagen.flow_from_directory(
    train_data_dir,
    target_size=(img_width, img_height),
    batch_size=batch_size,
    class_mode='binary')

validation_generator = val_datagen.flow_from_directory(
    val_data_dir,
    target_size=(img_width, img_height),
    batch_size=batch_size,
    class_mode='binary')

如果需要,我们当然可以在这里使用数据增强,但是我们实际上并不需要它。

我们在这里最有趣的事情可能是使用数据生成器的flow_from_directory()方法。 此方法采用一条路径,并根据该路径生成一批图像。 它为我们完成了将映像从磁盘中取出的所有工作。 由于它一次执行一批,因此即使不需要时,我们甚至不必将所有 50,000 个图像保留在 RAM 中。 很酷吧?

训练(特征提取)

对于此模型,我们将训练两次。 对于第一轮训练,我们将通过冻结网络的训练来进行 10 个周期的特征提取,仅调整完全连接的层权重,如我们在“迁移网络架构”部分中所讨论的。 然后,在下一部分中,我们将解冻某些层并再次进行训练,对另外 10 个周期进行微调,如以下代码所示:

代码语言:javascript复制
data_dir = "data/train/"
val_dir = "data/val/"
epochs = 10
batch_size = 30
model = build_model_feature_extraction()
train_generator, val_generator = setup_data(data_dir, val_dir)
callbacks_fe = create_callbacks(name='feature_extraction')
# stage 1 fit
model.fit_generator(
    train_generator,
    steps_per_epoch=train_generator.n // batch_size,
    epochs=epochs,
    validation_data=val_generator,
    validation_steps=val_generator.n // batch_size,
    callbacks=callbacks_fe,
    verbose=1)

scores = model.evaluate_generator(val_generator, steps=val_generator.n // batch_size)
print("Step 1 Scores: Loss: "   str(scores[0])   " Accuracy: "   str(scores[1]))

在前面的示例中,我们使用ImageDataGeneratorn属性来了解可用于生成器的图像总数,并将每个周期的步骤定义为该数目除以批量大小。

此代码的其余部分应该很熟悉。

如前所述,我们只需要训练大约 10 个周期。 现在,让我们看一下 TensorBoard 中的训练过程:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AvGlEeBz-1681567890355)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/ba99854e-0291-4744-b2ed-1b099411fa0c.png)]

如您所见,即使经过一个周期,网络的表现仍然非常好。 直到大约第 7 个阶段,我们都取得了非常微弱的表现提升。在第 7 个阶段,我们达到了最佳表现,导致 0.9828 的精度和 0.0547 的损失。

训练(微调)

为了微调网络,我们需要解冻一些冻结的层。 您可以解冻多少层,并且可以解冻任意数量的网络。 实际上,在大多数情况下,我们仅看到解冻最顶层的好处。 在这里,我仅解冻最后一个初始块,该块从图的249层开始。 以下代码描述了此技术:

代码语言:javascript复制
def build_model_fine_tuning(model, learning_rate=0.0001, momentum=0.9):
        for layer in model.layers[:249]:
            layer.trainable = False
        for layer in model.layers[249:]:
            layer.trainable = True
        model.compile(optimizer=SGD(lr=learning_rate, 
         momentum=momentum), loss='binary_crossentropy', metrics=
           ['accuracy'])
        return model

另请注意,我对随机梯度下降使用的学习率非常低,因此需要进行微调。 重要的是,此时应缓慢移动重物,以免在错误的方向上发生太大的飞跃。 我不建议使用adamrmsprop进行微调。 以下代码描述了微调机制:

代码语言:javascript复制
callbacks_ft = create_callbacks(name='fine_tuning')
# stage 2 fit
model = build_model_fine_tuning(model)
model.fit_generator(
 train_generator,
 steps_per_epoch=train_generator.n // batch_size,
 epochs=epochs,
 validation_data=val_generator,
 validation_steps=val_generator.n // batch_size,
 callbacks=callbacks_ft,
 verbose=2)

scores = model.evaluate_generator(val_generator, steps=val_generator.n // batch_size)
print("Step 2 Scores: Loss: "   str(scores[0])   " Accuracy: "   str(scores[1]))

我们可以再次查看 TensorBoard 图,以了解我们在进行微调后是否能得到任何收益:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NseTPTsp-1681567890356)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/7eeae209-356e-4f83-96fe-51e61bceb6e7.png)]

毫无疑问,我们的模型确实可以改进,但是只有很少的改进。 虽然规模很小,但您会注意到验证损失正在努力改善,并且可能显示出一些过拟合的迹象。

在这种情况下,微调几乎没有带来任何好处,但并非总是如此。 在此示例中,目标域和源域非常相似。 如前所述,由于源域和目标域不同,您从微调中获得的收益将增加。

总结

在本章中,我们介绍了迁移学习,并演示了如何使用在源域上进行预训练的网络如何极大地缩短训练时间,并最终改善我们的深度神经网络的表现。 我希望您喜欢这项技术,它是我的最爱之一,因为它非常实用,而且我通常会从中获得很好的效果。

在下一章中,我们将从计算机视觉过渡到可以记住先前输入的网络,使它们成为预测序列中下一项的理想选择。

九、从头开始训练 RNN

循环神经网络RNN)是为建模顺序数据而构建的一组神经网络。 在最后几章中,我们研究了使用卷积层从图像中学习特征。 当我们想从所有相关的值中学习特征时,循环层同样有用: x[t]x[t-1]x[t-2]x[t-3]

在本章中,我们将讨论如何将 RNN 用于时间序列问题,这无疑是涉及按时间或时间顺序排列的一系列数据点的问题。

我们将在本章介绍以下主题:

  • 循环神经网络介绍
  • 时间序列问题
  • 将 LSTM 用于时间序列预测

循环神经网络介绍

如果定义不清楚,我们来看一个例子:一个股票行情指示器,我们可以在其中观察股票价格随时间的变化,例如以下屏幕快照中的 Alphabet Inc.,这是时间序列的一个示例:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ktD8VExl-1681567890356)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/c8e78286-b120-4589-9bcc-791eeeb094d3.png)]

在下一章中,我们将讨论使用循环神经网络为语言建模,这是另一种类型的序列,即单词序列。 由于您正在阅读本书,因此无疑已经对语言顺序有了一些直觉。

如果您不熟悉时间序列,您可能想知道是否可以使用普通的多层感知器来解决时间序列问题。 您当然可以做到; 但是,实际上,使用循环网络几乎总是可以得到更好的结果。 也就是说,循环神经网络在序列建模方面还有其他两个优点:

  • 他们可以比普通的 MLP 更轻松地学习很长的序列
  • 他们可以处理不同长度的序列

当然,这给我们提出了一个重要的问题…

是什么使神经元循环?

循环神经网络具有循环,可以使信息从一个预测持续到下一个预测。 这意味着每个神经元的输出取决于网络的当前输入和先前的输出,如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XMtMMvl1-1681567890356)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/c3a17aee-28bb-49e5-b4fc-70be936df1ad.jpg)]

如果我们将这个图跨时间展平,它将看起来更像下图。 网络通知本身的想法是“循环”一词的来源,尽管作为 CS 专业,我始终将其视为循环神经网络。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ivQLfAZh-1681567890356)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/78f99d5b-c72c-4dd8-a83f-e000b7e711c5.jpg)]

在上图中,我们可以看到神经元A接受输入x[t0]并输出h[t0]在时间步 0 处。然后在时间步 1,神经元使用输入x[t1]以及来自其上一个时间步的信号来输出h[t1]。 现在在时间步骤 2,它认为它是输入x[t2]以及上一个时间步骤的信号,该信号可能仍包含时间步骤 0 的信息。我们继续这种方式,直到到达序列中的最后一个时间步,网络逐步增加其内存。

标准 RNN 使用权重矩阵将前一个时间步的信号与当前时间步的输入和隐藏权重矩阵的乘积混合。 在通过非线性函数(通常是双曲正切函数)进行馈送之前,将所有这些函数组合在一起。 对于每个时间步骤,它看起来像:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zbKJl19J-1681567890357)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/801cc47b-f4df-4766-b94a-f3937353c039.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M64UiXA4-1681567890357)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/b4034277-94c5-44ae-83e8-aec26521b29e.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9eAbgAZs-1681567890357)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/e391d033-316e-426f-bf52-c72785baffdb.png)]

此处t是前一个时间步输出和当前时间步输入的线性组合,均由权重矩阵WU进行参数化。 一旦计算出t,它就具有非线性函数,最常见的是双曲正切h[t]。 最后,神经元的输出o[t]h[t]与权重矩阵结合在一起,Va偏置,c偏置。

当您查看此结构时,请尝试想象一下一种情况,在该情况下,您很早就需要一些非常重要的信息。 随着序列的延长,重要的早期信息被遗忘的可能性就更高,因为新信号会轻易地压倒旧信息。 从数学上讲,单元的梯度将消失或爆炸。

这是标准 RNN 的主要缺点。 在实践中,传统的 RNN 难以按顺序学习真正的长期交互作用。 他们很健忘!

接下来,让我们看一下可以克服此限制的长短期内存网络。

长期短期记忆网络

每当需要循环网络时,长期短期记忆网络LSTM)都能很好地工作。 您可能已经猜到了,LSTM 在学习长期交互方面很出色。 实际上,这就是他们的设计意图。

LSTM 既可以积累先前时间步骤中的信息,又可以选择何时忘记一些不相关的信息,而选择一些新的更相关的信息。

例如,考虑序列In highschool I took Spanish. When I went to France I spoke French.。 如果我们正在训练一个网络来预测France一词,那么记住French并有选择地忘记Spanish是非常重要的,因为上下文已经发生了变化。 当序列的上下文发生变化时,LSTM 可以有选择地忘记某些事情。

为了完成这种选择性的长期记忆,LSTM 实现了一个“忘记门”,该门使 LSTM 成为了称为门控神经网络的神经网络家族的成员。 该遗忘门允许 LSTM 有选择地学习何时应从其长期存储器中丢弃信息。

LSTM 的另一个关键特性是内部自循环,使设备可以长期积累信息。 除了我们在 RNN 中看到的循环之外,还使用了该循环,可以将其视为时间步之间的外部循环。

相对于我们已经看到的其他神经元,LSTM 非常复杂,如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TLSAmTKX-1681567890358)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/0a5218b1-b99c-496a-bf84-f9d97c188a58.png)]

每个 LSTM 单元展开时,都有一个时间段t的输入,称为x[t],一个输出,称为o[t]以及从上一个时间步C[t-1]到下一个C[t]进行存储的存储器总线C

除这些输入外,该单元还包含多个门。 我们已经提到的第一个是忘记门,在图中标记为F[t]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5VbzHqmA-1681567890358)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/355dbba6-b10b-4cc7-a992-727d56fbc887.png)]

该门的输出(将在 0 和 1 之间)逐点乘以C[t-1]。 这允许门调节从C[t-1]C[t]的信息流。

下一个门,即输入门i[t]与函数候选C[t]结合使用。 候选C[t]学习可以添加到内存状态的向量。 输入门了解总线C中的哪些值得到更新。 下式说明i[t]和候选C[t]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q9gGhpdn-1681567890358)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/ec04eb9a-48f5-45fd-af5c-812bf60c1288.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RavsClGZ-1681567890358)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/fd532fce-cd9f-4f87-a814-45976a16593d.png)]

我们取i[t]和候选C[t]的点积,决定添加到总线C的对象, 使用F[t]决定要忘记什么之后,如以下公式所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QFeOhhQ3-1681567890358)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/c6d9fff9-d274-4af0-8ef2-69b3cd7c03bc.png)]

最后,我们将决定获取输出的内容。 输出主要来自内存总线C; 但是,它被另一个称为输出门的门过滤。 以下公式说明了输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HD75m9m5-1681567890363)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/57790f79-d20b-4cb4-a728-d25af7dab0bf.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nv0PwOvP-1681567890364)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/356fa44b-b999-4d7b-80b0-bfc55dc3d4d9.png)]

尽管很复杂,但 LSTM 在各种问题上都非常有效。 尽管存在 LSTM 的多个变体,但在大多数任务上仍基本认为该基本实现是最新技术。

这些任务之一是预测时间序列中的下一个值,这就是我们将在本章中使用的 LSTM。 但是,在我们开始将 LSTM 应用于时间序列之前,必须对时间序列分析和更传统的方法进行简短的复习。

时间上的反向传播

训练 RNN 要求反向传播的实现略有不同,即在整个时间(BPTT)中称为反向传播

与正常反向传播一样,BPTT 的目标是使用整体网络误差,通过梯度来调整每个神经元/单元对它们对整体误差的贡献的权重。 总体目标是相同的。

但是,当使用 BPTT 时,我们对误差的定义会稍有变化。 正如我们刚刚看到的,可以通过几个时间步长展开神经元循环。 我们关心所有这些时间步长的预测质量,而不仅仅是终端时间步长,因为 RNN 的目标是正确预测序列,因为逻辑单元误差定义为所有时间步长上展开的误差之和。

使用 BPTT 时,我们需要总结所有时间步骤中的误差。 然后,在计算完该总体误差后,我们将通过每个时间步的梯度来调整单元的权重。

这迫使我们明确定义将展开 LSTM 的程度。 在下面的示例中,您将看到这一点,当我们创建一组特定的时间步长时,将为每个观察值进行训练。

您选择反向传播的步骤数当然是超参数。 如果您需要从序列中很远的地方学习一些东西,显然您必须在序列中包括很多滞后。 您需要能够捕获相关期间。 另一方面,捕获太多的时间步长也不可取。 该网络将变得非常难以训练,因为随着时间的流逝,梯度会变得非常小。 这是前面几章中描述的梯度消失问题的另一个实例。

如您想象的那样,您可能想知道是否选择太大的时间步会使程序崩溃。 如果梯度驱动得太小以至于变为 NaN,那么我们将无法完成更新操作。 解决此问题的一种常见且简便的方法是在某些上下阈值之间固定梯度,我们将其称为梯度裁剪。 默认情况下,所有 Keras 优化器均已启用梯度剪切。 如果您的梯度被剪裁,则在该时间范围内网络可能不会学到很多东西,但是至少您的程序不会崩溃。

如果 BPTT 看起来确实令人困惑,请想象一下 LSTM 处于展开状态,其中每个时间步都有一个单元。 对于该网络结构,该算法实际上与标准反向传播几乎相同,不同之处在于所有展开的层均共享权重。

时间序列问题回顾

时间序列问题是涉及按时间顺序放置的一系列数据点的问题。 我们通常将这些数据点表示为一组:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NVwAIwE4-1681567890364)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/0969789c-bcfd-4c4a-b526-8e05a45d6ab1.png)]

通常,我们在时间序列分析中的目标是预测。 但是,使用时间序列当然还可以执行许多其他有趣的事情,而这不在本书的讨论范围之内。 预测实际上只是回归的一种特殊形式,我们的目标是根据给定的先前点x[t-1], ..., x[t-n]来预测某个点x[t]或点x[t], x[t 1], x[t 2], ..., x[t n]。 当时间序列自动关联时,我们可以执行此操作,这意味着数据点与其自身关联一个或多个时间上的点(称为滞后)。 自相关性越强,预测就越容易。

在许多书中,时间序列问题用y表示,而不是用x表示,以暗示我们通常关心预测给定自身的变量 y 的想法。

库存和流量

在计量经济学时间序列中,数量通常被定义为库存流量。 库存度量是指特定时间点的数量。 例如,2008 年 12 月 31 日的 SP500 的值是库存测量值。 流量测量是一段时间间隔内的速率。 美国股票市场从 2009 年到 2010 年的增长率是一种流量度量。

最经常进行预测时,我们会关注预测流量。 如果我们将预测想象为一种特定的回归,那么我们偏爱流量的第一个也是最明显的原因是,流量估计更有可能是插值而不是外推,而且插值几乎总是更安全。 此外,大多数时间序列模型都具有平稳性的假设。 固定时间序列是其统计属性(均值,方差和自相关)随时间恒定的序列。 如果我们使用一定数量的库存测量,则会发现大多数现实世界中的问题远非静止不动。

使用 LSTM 进行时间序列分析时,虽然没有假设(读取规则)需要平稳性,但根据实际经验,我发现对相对固定的数据进行训练的 LSTM 更加健壮。 使用 LSTM 进行时间序列预测时,几乎在所有情况下,一阶差分就足够了。

将库存数量转换为流量数量非常简单。 如果您具有n个点,则可以创建具有一阶差分的n-1流量测量值,其中,对于每个值t'[n],我们通过从t[n]中减去t[n-1]来进行计算,从而得出跨时间间隔的两次测量的变化率,如以下公式所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BVEEj6tY-1681567890364)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/335dae59-80a2-4afd-bc88-829c9306832d.png)]

例如,如果我们在三月份拥有价值 80 美元的股票,而在四月份突然价值 100 美元,则该股票的流率将为 20 美元。

一阶微分不能保证平稳的时间序列。 我们可能还需要删除季节或趋势。 趋势消除是专业预测员日常生活的重要组成部分。 如果我们使用传统的统计模型进行预测,则需要做更多的工作。 虽然我们没有涵盖这些内容的页面,但我们可能还需要执行二阶差分,季节性趋势下降或更多操作。 增强 Dickey-FullerADF)测试是一种统计测试,通常用于确定我们的时间序列是否实际上是静止的。 如果您想知道时间序列是否稳定,可以使用增强的 Dickey-Fuller 检验来检查。 但是,对于 LSTM,一阶微分通常可能就足够了。 只需了解网络最肯定会学习您数据集中剩余的季节和周期。

ARIMA 和 ARIMAX 预测

值得一提的是自回归综合移动平均值ARIMA)模型,因为它们传统上用于时间序列预测。 虽然我显然是深度神经网络的忠实拥护者(事实上,我写过关于它们的书),但我建议从 ARIMA 开始并逐步进行深度学习。 在许多情况下,ARIMA 的表现将优于 LSTM。 当数据稀疏时尤其如此。

从可能可行的最简单模型开始。 有时这将是一个深层的神经网络,但通常情况会更简单一些,例如线性回归或 ARIMA 模型。 该模型的复杂性应通过其提供的提升来证明,通常越简单越好。 尽管整本书中多次重申,但在时间序列预测中,这一说法比其他任何话题都更为真实。

ARIMA 模型是三个部分的组合。 AR,即自回归部分,是根据自身的自相关性对序列进行建模的部分。 MA 部分尝试对时间序列中的本地突发事件或冲击建模。 I 部分涵盖了差异,我们刚刚介绍了差异。 ARIMA 模型通常采用三个超参数pdq,分别对应于建模的自回归滞后的数量,微分度和模型的移动平均部分的顺序。

ARIMA 模型在 R 的auto.arima()和预测包中实现得很好,这可能是使用 R 语言的唯一很好的理由之一。

ARIMAX 模型允许在时间序列模型中包含一个或多个协变量。 您问这种情况下的协变量是多少? 这是一个附加时间序列,也与因变量相关,可用于进一步改善预测表现。

交易员的常见做法是尝试通过使用另一种商品的一个或多个滞后以及我们预测的商品的自回归部分来预测某些商品的价值。 在这种情况下,ARIMAX 模型将很有用。

如果您有许多具有复杂的高阶交互作用的协变量,那么您已进入 LSTM 的最佳预测时间序列。 在本书的开头,我们讨论了多层感知器如何对输入变量之间的复杂相互作用进行建模,从而为我们提供了自动特征工程,该工程提供了线性或逻辑回归的提升。 此属性可以继续使用 LSTM 进行具有许多输入变量的时间序列预测。

如果您想全面了解 ARIMA,ARIMAX 和时间序列预测,建议从 Rob J. Hyndman 的博客 Hyndsight 开始。

将 LSTM 用于时间序列预测

在本章中,我们将通过使用 2017 年 1 月至 5 月的比特币分钟价格来预测 2017 年 6 月美元的比特币分钟价格。我知道这听起来确实很赚钱,但是在您购买那条船之前,我建议您通读本章的最后; 说起来容易做起来难,甚至建模起来也容易。

即使我们能够使用这种模型在美元和比特币之间创造套利潜力(由于效率低下而导致两个市场之间的价格差异),但由于存在延迟,围绕比特币制定交易策略可能极其复杂。 在完成比特币交易中。 在撰写本文时,比特币交易的平均交易时间超过一个小时! 任何交易策略都应考虑这种“非流动性”。

和以前一样,本书的 Git 存储库中的Chapter09下提供了本章的代码。 文件data/bitcoin.csv包含数年的比特币价格。 基于以下假设,即前几年的市场行为与 2017 年加密货币流行后的行为无关,我们将仅使用几个月的价格信息作为模型。

数据准备

对于此示例,我们将不使用验证集,而是将测试集用作验证集。 在处理此类预测问题时,验证成为一项具有挑战性的工作,因为训练数据从测试数据中获取的越多,执行效果越差的可能性就越大。 另一方面,这并不能为过度安装提供太多保护。

为了使事情简单,在这里我们将只使用一个测试集,并希望最好。

在继续之前,让我们看一下将要进行的数据准备的总体流程。 为了使用此数据集训练 LSTM,我们需要:

  1. 加载数据集并将周期时间转换为熊猫日期时间。
  2. 通过对日期范围进行切片来创建训练和测试集。
  3. 差分我们的数据集。
  4. 将差异缩放到更接近我们的激活函数的程度。 我们将使用 -1 到 1,因为我们将使用tanh作为激活
  5. 创建一个训练集,其中每个目标x[t]都有一系列与之相关的滞后x[t-1], ..., x[t-n]。 在此训练集中,您可以将x[t]视为我们的典型因变量y。 滞后序列x[t-1], ..., x[t-n]可以看作是典型的X训练矩阵。

我将在接下来的主题中介绍每个步骤,并在进行过程中显示相关的代码。

加载数据集

从磁盘加载数据集是一项相当简单的工作。 如前所述,我们将按日期对数据进行切片。 为此,我们需要将数据集中的 Unix 周期时间转换为可分割的日期。 可以通过pandas to_datetime()方法轻松实现,如以下代码所示:

代码语言:javascript复制
def read_data():
    df = pd.read_csv("./data/bitcoin.csv")
    df["Time"] = pd.to_datetime(df.Timestamp, unit='s')
    df.index = df.Time
    df = df.drop(["Time", "Timestamp"], axis=1)
    return df

按日期切片和测试

现在,我们的数据帧已通过datetime时间戳编制索引,因此我们可以构造基于日期的切片函数。 为此,我们将定义一个布尔掩码,并使用该掩码选择现有的数据框。 虽然我们可以肯定地将其构造成一行,但我认为以这种方式阅读起来要容易一些,如以下代码所示:

代码语言:javascript复制
def select_dates(df, start, end):
    mask = (df.index > start) & (df.index <= end)
    return df[mask]

现在我们可以使用日期来获取数据框的某些部分,我们可以使用以下代码通过几次调用这些函数轻松地创建训练和测试数据框:

代码语言:javascript复制
df = read_data()
df_train = select_dates(df, start="2017-01-01", end="2017-05-31")
df_test = select_dates(df, start="2017-06-01", end="2017-06-30")

在使用这些数据集之前,我们需要对它们进行区别,如下所示。

差分时间序列

Pandas 数据框最初是为对时间序列数据进行操作而创建的,幸运的是,由于对数据集进行差分是时间序列中的一种常见操作,因此很容易内置。但是,根据良好的编码习惯,我们将围绕我们的一阶差分运算包装一个函数。 请注意,我们将用 0 填充无法进行一阶差分的所有空间。以下代码说明了此技术:

代码语言:javascript复制
def diff_data(df):
    df_diffed = df.diff()
    df_diffed.fillna(0, inplace=True)
    return df_diffed

通过差分数据集,我们将这个问题(库存问题)转移到了流量问题。 在比特币投放中,流量可能会很大,因为比特币的价值会在数分钟之间发生很大变化。 我们将通过缩放数据集来解决此问题。

缩放时间序列

在此示例中,我们将使用MinMaxScaler将每个差异数据点缩放为最小值为 -1 且最大值为 1 的比例。这将使我们的数据与双曲线正切函数(tanh ),这是我们针对该问题的激活函数。 我们将使用以下代码缩放系列:

代码语言:javascript复制
def scale_data(df, scaler=None):
    scaled_df = pd.DataFrame(index=df.index)
    if not scaler:
        scaler = MinMaxScaler(feature_range=(-1,1))
    scaled_df["Price"] = scaler.fit_transform(df.Close.values.reshape(-1,1))
    return scaler, scaled_df

请注意,此函数可以选择使用已经适合的缩放器。 这使我们能够将训练定标器应用到我们的测试仪上。

创建滞后的训练集

对于每个训练示例,给定一系列延迟x[t-1], ..., x[t-n],我们希望训练网络以预测值x[t]。 理想的延迟数是一个超参数,因此需要进行一些实验。

如前所述,以这种方式构造输入是 BPTT 算法的要求。 我们将使用以下代码来训练数据集:

代码语言:javascript复制
def lag_dataframe(data, lags=1):
    df = pd.DataFrame(data)
    columns = [df.shift(i) for i in range(lags, 0, -1)]
    columns.append(df)
    df = pd.concat(columns, axis=1)
    df.fillna(0, inplace=True)

    cols = df.columns.tolist()
    for i, col in enumerate(cols):
        if i == 0:
            cols[i] = "x"
        else:
            cols[i] = "x-"   str(i)

    cols[-1] = "y"
    df.columns = cols
    return df

例如,如果我们用lags = 3调用lag_dataframe,我们期望数据集返回x[t-1], x[t-2], x[t-3]。 我发现很难理解这样的滞后代码,因此,如果您也这样做,您并不孤单。 我建议运行它并建立一些熟悉的操作。

在选择数量滞后时,在将模型部署到生产环境时,您可能还需要考虑要等待多少个滞后才能做出预测。

输入形状

Keras 期望 LSTM 的输入是一个三维张量,看起来像:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6m2NQ2tR-1681567890365)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/e33c30d9-f1e9-4a50-94eb-df87a53faf1f.png)]

第一个维度显然是我们拥有的观测值的数量,并且我们可以预期。

第二维对应于使用lag_dataframe函数时我们选择的滞后次数。 这是我们要给 Keras 做出预测的时间步数。

第三维是该时间步中存在的特征数。 在我们的示例中,我们将使用一个,因为每个时间步只有一个特征,即该时间步的比特币价格。

在继续阅读之前,请仔细考虑此处定义三维矩阵给您的威力。 我们绝对可以将数百个其他时间序列作为预测该时间序列的特征。 通过这样做以及使用 LSTM,我们可以免费获得这些特征之间的特征工程。 正是这种功能使 LSTM 在金融领域变得如此令人兴奋。

对于当前的问题,我们将需要将二维矩阵转换为三维矩阵。 为此,我们将使用 NumPy 的便捷reshape函数,如以下代码所示:

代码语言:javascript复制
X_train = np.reshape(X_train.values, (X_train.shape[0], X_train.shape[1], 1))
X_test = np.reshape(X_test.values, (X_test.shape[0], X_test.shape[1], 1))

数据准备

在此示例中,我们做了很多转换。 在继续进行训练之前,我认为最好将两者结合起来。 如此处所示,我们将使用另一个函数将所有这些步骤联系在一起:

代码语言:javascript复制
def prep_data(df_train, df_test, lags):
    df_train = diff_data(df_train)
    scaler, df_train = scale_data(df_train)
    df_test = diff_data(df_test)
    scaler, df_test = scale_data(df_test, scaler)
    df_train = lag_dataframe(df_train, lags=lags)
    df_test = lag_dataframe(df_test, lags=lags)

    X_train = df_train.drop("y", axis=1)
    y_train = df_train.y
    X_test = df_test.drop("y", axis=1)
    y_test = df_test.y

    X_train = np.reshape(X_train.values, (X_train.shape[0], X_train.shape[1], 1))
    X_test = np.reshape(X_test.values, (X_test.shape[0], X_test.shape[1], 1))

    return X_train, X_test, y_train, y_test

此函数采用训练和测试数据帧,并应用差分,缩放和滞后代码。 然后,将这些数据帧重新调整为我们熟悉的Xy张量,以进行训练和测试。

现在,我们可以使用几行代码将这些转换粘合在一起,从而从加载数据到准备进行训练和测试,它们可以:

代码语言:javascript复制
LAGS=10
df = read_data()
df_train = select_dates(df, start="2017-01-01", end="2017-05-31")
df_test = select_dates(df, start="2017-06-01", end="2017-06-30")
X_train, X_test, y_train, y_test = prep_data(df_train, df_test, lags=LAGS)

这样,我们就可以开始训练了。

网络输出

我们的网络将输出一个单一值,该值是在前一分钟内给定分钟内比特流价格的缩放流量或预期变化。

我们可以使用单个神经元获得此输出。 该神经元可以在 Keras 密集层中实现。 它将多个 LSTM 神经元的输出作为输入,我们将在下一部分中介绍。 最后,此神经元的激活可以是tanh,因为我们已将数据缩放到与双曲正切函数相同的比例,如下所示:

代码语言:javascript复制
output = Dense(1, activation='tanh', name='output')(lstm2)

网络架构

我们的网络将使用两个 Keras LSTM 层,每个层具有 100 个 LSTM 单元:

代码语言:javascript复制
inputs = Input(batch_shape=(batch_shape, sequence_length, 
               input_dim), name="input")
lstm1 = LSTM(100, activation='tanh', return_sequences=True, 
             stateful=True, name='lstm1')(inputs)
lstm2 = LSTM(100, activation='tanh', return_sequences=False, 
             stateful=True, name='lstm2')(lstm1)
output = Dense(1, activation='tanh', name='output')(lstm2)

要特别注意return_sequences参数。 连接两个 LSTM 层时,您需要前一个 LSTM 层来输出序列中每个时间步的预测,以便下一个 LSTM 层的输入是三维的。 但是,我们的密集层仅需要二维输出即可预测其执行预测的确切时间步长。

有状态与无状态 LSTM

在本章的前面,我们讨论了 RNN 跨时间步长维护状态或内存的能力。

使用 Keras 时,可以用两种方式配置 LSTM,即有状态无状态

默认为无状态配置。 使用无状态 LSTM 配置时,每批 LSTM 单元存储器都会重置。 这使得批量大小成为非常重要的考虑因素。 当您正在学习的序列彼此不依赖时,无状态效果最佳。 下一个单词的句子级预测可能是何时使用无状态的一个很好的例子。

有状态配置会在每个周期重置 LSTM 单元存储器。 当训练集中的每个序列取决于其之前的序列时,最常使用此配置。 如果句子级别的预测对于无状态配置可能是一项好任务,那么文档级别的预测对于有状态模型可能是一项好任务。

最终,这种选择取决于问题,并且可能需要在测试每个选项时进行一些试验。

对于此示例,我已经测试了每个选项,并选择使用有状态模型。 当我们考虑问题的背景时,这可能不足为奇。

训练

尽管此时的情况似乎有很大不同,但是训练 LSTM 实际上与训练典型横截面问题的深度神经网络没有什么不同:

代码语言:javascript复制
LAGS=10
df = read_data()
df_train = select_dates(df, start="2017-01-01", end="2017-05-31")
df_test = select_dates(df, start="2017-06-01", end="2017-06-30")
X_train, X_test, y_train, y_test = prep_data(df_train, df_test, lags=LAGS)
model = build_network(sequence_length=LAGS)
callbacks = create_callbacks("lstm_100_100")
model.fit(x=X_train, y=y_train,
          batch_size=100,
          epochs=10,
          callbacks=callbacks)
model.save("lstm_model.h5")

在准备好数据之后,我们使用我们已经遍历的架构实例化一个网络,然后按预期对其进行拟合。

在这里,我使用的是有状态的 LSTM。 有状态 LSTM 的一个实际好处是,与无状态 LSTM 相比,它们倾向于在更少的时间进行训练。 如果要将其重构为无状态 LSTM,则在网络完成学习之前可能需要 100 个周期,而此处我们仅使用 10 个周期。

测量表现

在有状态的配置中经过 10 个星期之后,我们的损失已经停止改善,并且我们的网络也受到了良好的训练,如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i98E2GRB-1681567890365)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/e866b9d4-0486-4a35-8373-913744c3c687.png)]

我们拥有一个合适的网络,似乎已经学到了一些东西。 现在,我们可以对比特币的价格流做出某种预测。 如果我们能做好,我们所有人都会非常富有。 在去买那栋豪宅之前,我们可能应该测量一下模型的表现。

财务模型的最终检验是这个问题:“您愿意在上面花钱吗?”很难回答这个问题,因为在时间序列问题中衡量表现可能具有挑战性。

一种衡量表现的非常简单的方法是使用均方根误差来评估y_testX_test预测之间的差异。 我们最肯定可以做到这一点,如以下代码所示:

代码语言:javascript复制
RMSE = 0.0801932157201

0.08 是一个好分数吗? 让我们通过比较我们的预测与 6 月份比特币流量的实际值,开始对商品的调查。 这样做可能会使我们对模型的表现有直观的了解,这是我始终建议的一种做法:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i0PeThGF-1681567890365)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/92af7dd8-91fc-410a-bfc0-bbb0e36d4d6a.png)]

我们用绿色表示的预测有很多不足之处。 我们的模型已经学会了预测平均流量,但是在匹配完整信号方面确实做得很差。 甚至有可能我们只是在学习一种趋势,因为我们所做的努力不那么激烈。 我认为我们可能不得不把那栋豪宅推迟更长的时间,但是我们走了正确的道路。

考虑到我们的预测,即仅给出比特币的先前价值,该模型就可以解释尽可能多的比特币价格。 我们可能在建模时间序列的自回归部分方面做得相当不错。 但是,可能有许多不同的外部因素影响比特币的价格。 美元的价值,其他市场的动向,也许最重要的是,围绕比特币的嗡嗡声或信息流通,都可能在美元的价格中发挥重要作用。

这就是 LSTM 用于时间序列预测的功能真正发挥作用的地方。 通过添加附加的输入特征,所有这些信息都可以在某种程度上轻松地添加到模型中,希望可以解释越来越多的整个图片。

但是,让我再破一次您的希望。 对表现进行更彻底的调查还将包括考虑模型相对于某些幼稚模型所提供的提升。 此简单模型的典型选择可能包括称为随机游走模型,指数平滑模型的模型,或者可能使用朴素的方法,例如使用上一个时间步长作为当前时间步长的预测。 如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l6AU4cgv-1681567890366)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/afab68ad-33ef-49a9-8444-a4b7eeed2d8d.png)]

在此图中,我们将红色的预测与一个模型进行比较,在模型中,我们仅将前一分钟用作绿色的下一分钟的预测。 以蓝色表示的实际价格几乎完美地覆盖了这个朴素的模型。 我们的 LSTM 预测不如幼稚模型好。 仅使用最后一分钟的价格来预测当前分钟的价格会更好。 尽管我坚持认为我们走在正确的道路上,但在那艘船成为我们的船之前,我们还有很长的路要走。

对任何商品建模非常困难。 对于这种类型的问题,使用深度神经网络是可以肯定的,但是这个问题并不容易。 我加入了这个也许详尽的解释,以便如果您决定走这条路,便会明白自己的目标。

就是说,当您使用 LSTM 套利金融市场时,请记住给小费。

总结

在本章中,我们讨论了使用循环神经网络来预测序列中的下一个元素。 我们既涵盖了一般的 RNN,也涵盖了特定的 LSTM,我们专注于使用 LSTM 预测时间序列。 为了确保我们了解将 LSTM 用于时间序列的好处和挑战,我们简要回顾了时间序列分析的一些基础知识。 我们还花了几分钟讨论传统的时间序列模型,包括 ARIMA 和 ARIMAX。

最后,我们介绍了一个具有挑战性的用例,其中我们使用 LSTM 来预测比特币的价格。

在下一章中,我们将继续使用 RNN,现在将重点放在自然语言处理任务上,并介绍嵌入层的概念。

十、使用词嵌入从头开始训练 LSTM

到目前为止,我们已经看到了深度学习在结构化数据,图像数据甚至时间序列数据中的应用示例。 似乎唯一正确的方法是继续进行自然语言处理NLP)作为下一步。 机器学习和人类语言之间的联系非常有趣。 深度学习已像计算机视觉一样,以指数方式加快了该领域的发展速度。 让我们从 NLP 的简要概述开始,并在本章中将要完成的一些任务开始。

我们还将在本章中介绍以下主题:

  • 自然语言处理入门
  • 向量化文本
  • 词嵌入
  • Keras 嵌入层
  • 用于自然语言处理的一维 CNN
  • 文档分类的案例研究

自然语言处理入门

NLP 领域广阔而复杂。 从技术上讲,人类语言与计算机科学之间的任何交互都可能属于此类。 不过,为了便于讨论,我将 NLP 限于分析,理解,有时生成人类语言。

从计算机科学的起源开始,我们就对 NLP 着迷,因为它是通向强大人工智能的门户。 1950 年,艾伦·图灵(Alan Turing)提出了图灵测试,其中涉及一台计算机,它很好地模仿了一个人,使其与另一个人无法区分,以此作为机器智能的度量标准。 从那时起,我们一直在寻找帮助机器理解人类语言的聪明方法。 在此过程中,我们开发了语音到文本的转录,人类语言之间的自动翻译,文档的自动汇总,主题建模,命名实体标识以及各种其他用例。

随着我们对 NLP 的了解不断增长,我们发现 AI 应用在日常生活中变得越来越普遍。 聊天机器人作为客户服务应用已变得司空见惯,最近,它们已成为我们的个人数字助理。 在撰写本文时,我可以要求 Alexa 在我的购物清单中添加一些内容或演奏一些流畅的爵士乐。 自然语言处理以一种非常有趣和强大的方式将人类连接到计算机。

在本章中,我将专注于理解人类语言,然后使用这种理解进行分类。 我实际上将进行两个分类案例研究,一个涉及语义分析,另一个涉及文档分类。 这两个案例研究为深度学习的应用提供了巨大的机会,而且它们确实非常相似。

语义分析

语义分析从技术上讲是对语言含义的分析,但是通常当我们说语义分析时,我们是在谈论理解作者的感受。 语义分类器通常试图将某些话语分类为积极,消极,快乐,悲伤,中立等。

讽刺是我最喜欢的语言之一,这使这成为一个具有挑战性的问题。 人类语言中有许多微妙的模式,这些对于计算机学习来说是非常具有挑战性的。 但是挑战并不意味着没有可能。 只要有一个好的数据集,这个任务就很有可能实现。

要成功解决此类问题,需要一个好的数据集。 虽然我们当然可以在整个互联网上找到大量的人类对话,但其中大多数没有标签。 查找带标签的病例更具挑战性。 解决此问题的早期尝试是收集包含表情符号的 Twitter 数据。 如果一条推文中包含:),则认为该推文是肯定的。 这成为 Jimmy Lin 和 Alek Kolcz 在 Twitter 上的大规模机器学习中引用的知名表情符号技巧。

这种类型的分类器的大多数业务应用都是二元的,我们尝试在其中预测客户是否满意。 但是,那当然不是对这种语言模型的限制。 只要我们有用于此类事物的标签,我们就可以为其他音调建模。 我们甚至可能尝试衡量某人的声音或语言中的焦虑或困扰; 但是,解决音频输入超出了本章的范围。

进一步挖掘数据的尝试包括使用与正面和负面电影评论相关的语言以及与在线购物产品评论相关的语言。 这些都是很好的方法。 但是,在使用这些类型的数据源对来自不同域的文本进行分类时,应格外小心。 您可能会想到,电影评论或在线购买中使用的语言可能与 IT 帮助台客户支持电话中使用的语言完全不同。

当然,我们当然可以对情绪进行更多的分类。 在下一节中,我们将讨论文档分类的更一般的应用。

文档分类

文档分类与情感分析密切相关。 在这两种情况下,我们都使用文本将文档分类。 实际上,这只是改变的原因。 文档分类就是根据文档的类型对文档进行分类。 世界上最明显,最常见的文档分类系统是垃圾邮件过滤器,但它还有许多其他用途。

我最喜欢的文档分类用途之一是解决“联邦主义者论文”的原始作者的辩论。 亚历山大·汉密尔顿(Alexander Hamilton),詹姆斯·麦迪逊(James Madison)和约翰·杰伊(John Jay)在 1787 年和 1788 年以化名 Publius 出版了 85 篇文章,支持批准美国宪法。 后来,汉密尔顿提供了一份清单,详细列出了每篇论文的作者在 1804 年与亚伦·伯尔(Aaron Burr)进行致命的对决之前。麦迪逊(Madison)在 1818 年提供了自己的清单,这在作者身份上引起了争执,此后学者一直在努力解决。 虽然大多数人都同意有争议的作品是麦迪逊的作品,但是关于两者之间的合作仍存在一些理论。 将这 12 个有争议的文档归类为 Madison 还是 Hamilton,已经成为许多数据科学博客的不二之选。 正式而言,Glenn Fung 的论文《有争议的联邦主义者论文:通过凹面最小化进行 SVM 特征选择》 涵盖了相当严格的主题。

文档分类的最后一个示例可能是围绕了解文档的内容并规定操作。 想象一下一个分类器,它可能会读取有关法律案件的一些信息,例如请愿/投诉和传票,然后向被告提出建议。 然后,我们的假想系统可能会说:鉴于我在其他类似情况下的经验,您可能想解决

情感分析和文档分类是基于计算机理解自然语言的能力的强大技术。 但是,当然,这引出了一个问题,我们如何教计算机阅读?

向量化文本

机器学习模型(包括深度神经网络)吸收数字信息并产生数字输出。 自然语言处理的挑战自然就变成了将单词转换成数字。

我们可以通过多种方式将单词转换为数字。 所有这些方法都满足相同的目标,即将某些单词序列转换为数字向量。 有些方法比其他方法更好,因为有时进行转换时,翻译中可能会失去一些含义。

NLP 术语

让我们从定义一些通用术语开始,以便消除它们使用可能引起的任何歧义。 我知道,由于您可以阅读,因此您可能会对这些术语有所了解。 如果这看起来很古怪,我深表歉意,但是我保证,这将立即与我们接下来讨论的模型有关:

  • :我们将使用的大多数系统的原子元素。 尽管确实存在某些字符级模型,但我们今天不再讨论它们。
  • 句子:表达陈述,问题等的单词集合。
  • 文档:文档是句子的集合。 它可能是一个句子,或更可能是多个句子。
  • 语料库:文档的集合。

词袋模型

词袋BoW)模型是 NLP 模型,实际上忽略了句子结构和单词放置。 在“单词袋”模型中,我们将每个文档视为单词袋。 很容易想到这一点。 每个文档都是一个包含大量单词的容器。 我们忽略句子,结构以及哪个词排在前或后。 我们对文档中包含“非常”,“很好”和“不好”这两个词的事实感到关注,但是我们并不真正在意“好”而不是“坏”。

词袋模型很简单,需要相对较少的数据,并且考虑到该模型的朴素性,其运行效果非常好。

注意,这里使用模型表示表示。 我并不是在特定意义上指深度学习模型或机器学习模型。 相反,在这种情况下,模型是表示文本的一种方式。

给定一个由一组单词组成的文档,则需要定义一种策略来将单词转换为数字。 稍后我们将介绍几种策略,但首先我们需要简要讨论词干,词形化和停用词。

词干,词根去除和停用词

词干词根去除是两种不同但非常相似的技术,它们试图将每个单词还原为基本形式,从而简化了语言模型。 例如,如果要阻止猫的各种形式,我们将在此示例中进行转换:

代码语言:javascript复制
cat, cats, cat's, cats' -> cat

限制词法化和词干化之间的差异成为我们进行此转换的方式。 提取是通过算法完成的。 当应用于同一个单词的多种形式时,提取的根在大多数情况下应相同。 这个概念可以与词条反义化形成对比,词条反义化使用具有已知基础的词汇表并考虑如何使用该词。

词干处理通常比词条化处理快得多。 Porter 提取器在很多情况下都可以很好地工作,因此您可以将其作为提取的第一个安全选择。

停用词是在该语言中非常常见的词,但几乎没有语义。 典范示例是the一词。 我在上一句话中只使用了 3 次,但实际上只保留了一次意思。 通常,我们会删除停用词,以使输入内容更加稀疏。

大部分 BoW 模型都受益于词干,词根化和删除停用词。 有时,我们很快将要讨论的词嵌入模型也可以从词干提取或词义化中受益。 词嵌入模型很少会受益于停用词的删除。

计数和 TF-IDF 向量化

计数向量化和词频逆文档频率TF-IDF)是两种策略,将词袋转换成适合机器学习算法输入的特征向量。

计数向量化采用我们的一组单词,并创建一个向量,其中每个元素代表语料库词汇中的一个单词。 自然,一组文档中唯一单词的数量可能会很大,并且许多文档可能不包含语料库中存在的单词的任何实例。 在这种情况下,使用稀疏矩阵表示这些类型的字向量通常是非常明智的。 当一个单词出现一次或多次时,计数向量化器将简单地对该单词出现在文档中的次数进行计数,然后将该计数放置在代表该单词的位置。

使用计数向量化器,整个语料库可以表示为二维矩阵,其中每一行是一个文档,每一列是一个单词,然后每个元素就是该单词在文档中的计数。

在继续之前,让我们先看一个简单的例子。 想象一个具有两个文档的语料库:

代码语言:javascript复制
docA = "the cat sat on my face"
docB = "the dog sat on my bed"

语料库词汇为:

代码语言:javascript复制
{'bed', 'cat', 'dog', 'face', 'my', 'on', 'sat', 'the'}

因此,如果我们要为该语料库创建一个计数嵌入,它将看起来像这样:

bed

cat

dog

face

my

on

sat

the

文件 0

0

1

0

1

1

1

1

1

文件 1

1

0

1

0

1

1

1

1

这就是计数向量化。 这是我们工具箱中最简单的向量化技术。

计数向量化的问题在于我们使用了很多根本没有太多意义的单词。 实际上,英语中最常用的单词(the)占我们所讲单词的 7%,是第二个最受欢迎的单词(of)出现频率的两倍。 语言中单词的分布是幂律分布,这是称为 Zipf 定律的基础。 如果我们从计数中构造文档矩阵,那么最终得到的数字将包含很多信息,除非我们的目标是查看谁最经常使用the

更好的策略是根据单词在文档中的相对重要性对单词进行加权。 为此,我们可以使用 TF-IDF。

一个单词的 TF-IDF 分数是:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4nt8PXmp-1681567890366)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/cb6ce52c-4dee-4416-adb5-42cdfd30161e.png)]

在此公式中:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rhobkrlk-1681567890366)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/b1ca6787-9563-418b-aaa1-60a25fdc68c7.png)]

这个公式:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D5vpuQfq-1681567890366)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/cd6249c7-e42a-484b-90b3-4bf2de2f1300.png)]

如果我们要为同一语料库计算 TF-IDF 矩阵,它将看起来像这样:

bed

cat

dog

face

my

on

sat

the

文件 0

0

0.116

0

0.116

0

0

0

0

文件 1

0.116

0

0.116

0

0

0

0

0

您可能会注意到,通过对单词频率乘以逆文档频率进行加权,我们取消了所有文档中出现的单词,从而放大了不同的单词。 文件 0 全部关于猫和脸,而文件 1 全部关于狗和床。 这正是我们对许多分类器所要的。

词嵌入

词袋模型具有一些不理想的属性,值得注意的是。

我们之前研究过的词袋模型的第一个问题是它们没有考虑单词的上下文。 他们并没有真正考虑文档中单词之间存在的关系。

第二个相关问题是向量空间中单词的分配有些随意。 可能无法捕获有关语料库词汇中两个单词之间的关系的信息。 例如,虽然鳄鱼和鳄鱼都是相似的具有许多特征的生物,但已经学会处理鳄鱼的单词的模型几乎无法利用鳄鱼学到的知识(爬行动物学家讨厌邮件) 。

最后,由于语料库的词汇量可能很大,并且可能不会出现在所有文档中,因此 BoW 模型往往会产生非常稀疏的向量。

单词嵌入模型通过为每个单词学习一个向量来解决这些问题,其中每个语义相似的单词都映射到(嵌入)附近的点。 另外,与 BoW 模型相比,我们将在更小的向量空间中表示整个词汇表。 这提供了降维效果,并为我们提供了一个更小,更密集的向量,该向量可以捕获单词的语义值。

词嵌入模型在现实文档分类问题和语义分析问题中通常比词袋模型具有很大的提升,因为这种能力可以保留词相对于语料库中其他词的语义值。

一个简单的例子

如果您不熟悉单词嵌入,那么您现在可能会感到有些迷茫。 挂在那儿,它很快就会变得清晰起来。 让我们尝试一个具体的例子。

使用流行的单词嵌入模型word2vec,我们可以从单词cat开始,找到它的 384 元素向量,如以下输出代码所示:

代码语言:javascript复制
array([ 5.81600726e-01, 3.07168198e 00, 3.73339128e 00,
 2.83814788e-01, 2.79787600e-01, 2.29124355e 00,
 -2.14855480e 00, -1.22236431e 00, 2.20581269e 00,
 1.81546474e 00, 2.06929898e 00, -2.71712840e-01,...

我缩短了输出,但您明白了。 此模型中的每个单词都将转换为 384 个元素的向量。 可以对这些向量进行比较,以评估数据集中单词的语义相似性。

现在我们有了猫的向量,我将计算狗和蜥蜴的词向量。 我建议猫比蜥蜴更像狗。 我应该能够测量猫向量和狗向量之间的距离,然后测量猫向量和蜥蜴向量之间的距离。 尽管有许多方法可以测量向量之间的距离,但余弦相似度可能是单词向量最常用的方法。 在下表中,我们正在比较猫与狗和蜥蜴的余弦相似度:

dog

lizard

cat

0.74

0.63

不出所料,在我们的向量空间中,猫的含义比蜥蜴更接近狗。

通过预测学习单词嵌入

单词嵌入是通过使用专门为该任务构建的神经网络来计算的。 我将在这里介绍该网络的概述。 一旦计算了某些语料库的词嵌入,它们便可以轻松地重用于其他应用,因此使该技术成为迁移学习的候选者,类似于我们在第 8 章“使用预先训练的 CNN 的迁移学习”中介绍的技术。

当我们完成了对该词嵌入网络的训练后,我们网络中单个隐藏层的权重将成为我们词嵌入的查找表。 对于词汇表中的每个单词,我们将学习该单词的向量。

该隐藏层将包含比输入空间少的神经元,从而迫使网络学习输入层中存在的信息的压缩形式。 这种架构非常类似于自编码器。 但是,该技术围绕着一项任务,该任务帮助网络学习向量空间中每个单词的语义值。

我们将用来训练嵌入网络的任务是预测某些目标词出现在距训练词距离窗口内的概率。 例如,如果koala是我们的输入词,而marsupials是我们的目标词,则我们想知道这两个词彼此靠近的可能性。

此任务的输入层将是词汇表中每个单词的一个热编码向量。 输出层将是相同大小的softmax层,如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WMDRmnuD-1681567890367)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/ec71e9df-b735-4432-ab12-d31905f6d2c6.png)]

该网络导致隐藏层的形状为权重矩阵[词汇 x 神经元]。 例如,如果我们的语料库中有 20,000 个唯一单词,而隐藏层中有 300 个神经元,那么我们的隐藏层权重矩阵将为20,000 x 300。将这些权重保存到磁盘后,我们将拥有一个 300 元素向量,可用于代表每个词。 然后,在训练其他模型时,可以使用这些向量表示单词。

当然,除此以外,还有更多的训练词嵌入网络的方法,而我故意过分简化了快速参考样式。

如果您想了解更多信息,我建议您先阅读 Mikolov 等人的《单词和短语的分布式表示及其组成》。 本文介绍了一种流行的创建单词嵌入的方法,称为word2vec

通过计数学习单词嵌入

学习单词嵌入的另一种方法是通过计数。 用于词表示的全局向量GloVe 是 Pennington 等人创建的算法。

GloVe 通过创建单词共现的非常大的矩阵来工作。 对于某些语料库,这实际上是两个单词彼此相邻出现的次数的计数。 该算法的作者根据单词的接近程度来加权此计数,以使彼此接近的单词对每个计数的贡献更大。 一旦创建了这个共现矩阵,它将分解为一个较小的空间,从而生成一个单词 x 特征较大的矩阵。

有趣的是,word2vec和 GloVe 的结果非常相似,可以互换使用。 由 60 亿个单词的数据集预先构建的 GloVe 向量由斯坦福大学分发,是单词向量的常用来源。 本章稍后将使用 GloVe 向量。

从文本到文档

如果您一直在仔细阅读,您可能会注意到我尚未消除的鸿沟。 词嵌入模型为每个词创建一个向量。 相比之下,BoW 模型为每个文档创建一个向量。 那么,我们如何使用词嵌入模型进行文档分类呢?

一种幼稚的方法可能是获取文档中所有单词的向量并计算均值。 我们可能将此值解释为文档的平均语义值。 在实践中,通常使用此解决方案,并且可以产生良好的结果。 但是,它并不总是优于 BoW 嵌入模型。 考虑短语dog bites manman bites dog。 希望您会同意我的观点,这是两个截然不同的陈述。 但是,如果我们对它们的词向量进行平均,它们将具有相同的值。 这使我们提出了一些其他策略,可以用来设计文档中的特征,例如使用每个向量的均值,最大值和最小值。

Le 和 Mikolov 在《句子和文档的分布式表示》中提出了一种从单词到文档的更好的想法。 基于word2vec的思想,本文将段落标识符添加到我们描述的用于学习单词向量的神经网络的输入中。 使用文本中的单词以及文档 ID 可以使网络学习将可变长度文档嵌入向量空间中。 该技术称为 doc2vec,它可以很好地用作主题建模以及为模型创建输入特征的技术。

最后,许多深度学习框架都包含了嵌入层的概念。 嵌入层使您可以了解嵌入空间,这是网络正在执行的总体任务的一部分。 使用深度神经网络时,嵌入层可能是向量化文本的最佳选择。 接下来让我们看一下嵌入层。

Keras 嵌入层

Keras 嵌入层允许我们学习输入词的向量空间表示,就像我们在训练模型时在word2vec中所做的那样。 使用函数式 API,Keras 嵌入层始终是网络中的第二层,紧随输入层之后。

嵌入层需要以下三个参数:

  • input_dim:语料库的词汇量。
  • output_dim:我们要学习的向量空间的大小。 这将对应于word2vec隐藏层中神经元的数量。
  • input_length:我们将在每次观察中使用的文字数量。 在下面的示例中,我们将根据需要发送的最长文本使用固定大小,并将较小的文档填充为 0。

嵌入层将为每个输入文档输出 2D 矩阵,该矩阵包含input_length指定的每个单词的一个向量。

例如,我们可能有一个如下所示的嵌入层:

代码语言:javascript复制
Embedding(input_dim=10000, output_dim=128, input_length=10)

在这种情况下,该层的输出将是形状为10 x 128的 2D 矩阵,其中每个文档的 10 个单词将具有与之关联的 128 元素向量。

这样的单词序列可以作为 LSTM 的出色输入。 LSTM 层可以紧随嵌入层。 就像上一章一样,我们可以将嵌入层中的这 10 行视为 LSTM 的顺序输入。 在本章的第一个示例中,我将使用 LSTM,因此,如果您在未阅读第 9 章“从头开始训练 RNN”的情况下,则请花一点时间重新了解 LSTM 的操作,可以在此处找到。

如果我们想将嵌入层直接连接到密集层,则需要对其进行展平,但您可能不想这样做。 如果您有序列文本,通常使用 LSTM 是更好的选择。 我们还有另外一个有趣的选择。

用于自然语言处理的一维 CNN

回顾第 7 章,“从头开始训练 CNN”时,我们使用了卷积在图像区域上滑动窗口以学习复杂的视觉特征。 这使我们能够学习重要的局部视觉特征,而不管这些特征在图片中的位置,然后随着我们的网络越来越深入,逐步地学习越来越复杂的特征。 我们通常在 2D 或 3D 图像上使用3 x 35 x 5过滤器。 如果您对卷积层及其工作原理的理解感到生疏,则可能需要阅读第 7 章“从头开始训练 CNN”。

事实证明,我们可以对一系列单词使用相同的策略。 在这里,我们的 2D 矩阵是嵌入层的输出。 每行代表一个单词,并且该行中的所有元素都是其单词向量。 继续前面的示例,我们将有一个 10 x 128 的向量,其中连续有 10 个单词,每个单词都由 128 个元素的向量空间表示。 我们当然可以在这些单词上滑动过滤器。

卷积过滤器的大小针对 NLP 问题而改变。 当我们构建网络来解决 NLP 问题时,我们的过滤器将与单词向量一样宽。 过滤器的高度可以变化,通常在 2 到 5 之间。高度为 5 表示我们一次要在五个字上滑动过滤器。

事实证明,对于许多 NLP 问题,CNN 可以很好地运行,并且比 LSTM 快得多。 很难就何时使用 RNN/LSTM 和何时使用 CNN 给出确切的规则。 通常,如果您的问题需要状态,或者从很远的序列中学习到一些东西,那么使用 LSTM 可能会更好。 如果您的问题需要检测描述文本的特定单词集或文档的语义感觉,那么 CNN 可能会更快甚至更好地解决您的问题。

文档分类的案例研究

由于我已经提出了两种可行的文档分类方法,因此本章将包含两个单独的文档分类示例。 两者都将使用嵌入层。 一个将使用 LSTM,另一个将使用 CNN。

我们还将比较学习嵌入层与从其他人的权重开始采用迁移学习方法之间的表现。

这两个示例的代码都可以在本书的 Git 存储库中的Chapter10文件夹中找到。 某些数据和 GloVe 向量将需要分别下载。 有关说明,请参见代码中的注释。

Keras 嵌入层和 LSTM 的情感分析

本章的第一个案例研究将演示情绪分析。 在此示例中,我们将应用本章中学到的大多数内容。

我们将使用从互联网电影数据库IMDB)内置于 Keras 中的数据集。 该数据集包含 25,000 条电影评论,每条评论均按情感标记。 正面评论标记为 1,负面评论标记为 0。此数据集中的每个单词均已替换为标识该单词的整数。 每个评论都被编码为单词索引序列。

我们的目标是仅使用评论中的文字将电影评论分为正面评论或负面评论。

准备数据

因为我们使用的是内置数据集,所以 Keras 会处理大量的日常工作,这些工作涉及标记,词干,停用词以及将词标记转换为数字标记的工作。 keras.datasets.imbd将为我们提供一个列表列表,每个列表包含一个长度可变的整数序列,这些整数表示审阅中的单词。 我们将使用以下代码定义数据:

代码语言:javascript复制
def load_data(vocab_size):
    data = dict()
    data["vocab_size"] = vocab_size
    (data["X_train"], data["y_train"]), (data["X_test"], data["y_test"]) = 
    imdb.load_data(num_words=vocab_size)
    return data

我们可以通过调用load_data并为词汇表选择最大大小来加载数据。 在此示例中,我将使用 20,000 个单词作为词汇量。

如果需要手动执行此操作,以使示例代码可以解决您自己的问题,则可以使用keras.preprocessing.text.Tokenizer类,我们将在下一个示例中介绍该类。 我们将使用以下代码加载数据:

代码语言:javascript复制
data = load_data(20000)

下一步,我希望这些序列中的每个序列都具有相同的长度,并且我需要此列表列表为 2D 矩阵,其中每个评论是一行,每列是一个单词。 为了使每个列表大小相同,我将用 0 填充较短的序列。 我们稍后将使用的 LSTM 将学习忽略那些 0,这对于我们当然非常方便。

这种填充操作相当普遍,因此已内置在 Keras 中。 我们可以通过以下代码使用keras.preprocessing.sequence.pad_sequences完成此操作:

代码语言:javascript复制
def pad_sequences(data):
    data["X_train"] = sequence.pad_sequences(data["X_train"])
    data["sequence_length"] = data["X_train"].shape[1]
    data["X_test"] = sequence.pad_sequences(data["X_test"], maxlen=data["sequence_length"])
    return data

调用此函数会将列表列表转换为等长序列,并方便地将列表列表转换为 2D 矩阵,如下所示:

代码语言:javascript复制
data = pad_sequences(data)

输入和嵌入层架构

在上一章中,我们使用时间序列中的一组滞后训练了 LSTM。 在这里,我们的滞后实际上是序列中的单词。 我们将使用这些词来预测审阅者的情绪。 为了从单词序列到考虑这些单词的语义值的输入向量,我们可以使用嵌入层。

使用 Keras 函数式 API,嵌入层始终是网络中输入层之后的第二层。 让我们看一下这两层如何结合在一起:

代码语言:javascript复制
input = Input(shape=(sequence_length,), name="Input")
embedding = Embedding(input_dim=vocab_size, output_dim=embedding_dim,
                      input_length=sequence_length, name="embedding")(input)

我们的输入层需要知道序列长度,该长度与输入矩阵中的列数相对应。

嵌入层将使用输入层。 但是,它需要知道整体语料库词汇量,我们将这些词嵌入到的向量空间的大小以及序列长度。

我们定义的词汇量为 20,000 个单词,数据的序列长度为 2,494,并且指定的嵌入维数为 100。

将所有这些放在一起,嵌入层将从每个文件的 20,000 个输入热向量到每个文档的2,494 x 100 2D 矩阵,从而为序列中的每个单词嵌入向量空间。 随着模型的学习,嵌入层将不断学习。 很酷吧?

LSTM 层

我将在这里只使用一个 LSTM 层,只有 10 个神经元,如以下代码所示:

代码语言:javascript复制
lstm1 = LSTM(10, activation='tanh', return_sequences=False,
             dropout=0.2, recurrent_dropout=0.2, name='lstm1')(embedding)

为什么要使用这么小的 LSTM 层? 就像您将要看到的那样,该模型将因过拟合而陷入困境。 甚至只有 10 个 LSTM 单元也能很好地学习训练数据。 解决此问题的方法可能是添加数据,但实际上不能添加数据,因此保持网络结构简单是一个好主意。

这导致我们使用丢弃法。 我将在这一层同时使用丢弃法和经常性丢弃。 我们还没有谈论经常性丢弃的问题,所以让我们现在解决它。 以这种方式应用于 LSTM 层的常规过滤器将随机掩盖 LSTM 的输入。 循环丢弃会随机打开和关闭 LSTM 单元/神经元中展开的单元之间的内存。 与往常一样,丢弃是一个超参数,您需要搜索最佳值。

因为我们的输入是基于文档的,并且因为没有任何上下文,所以我们需要记住在文档之间,这是使用无状态 LSTM 的绝佳时机。

输出层

在此示例中,我们预测了二元目标。 和以前一样,我们可以使用具有单个 Sigmoid 神经元的密集层来完成此二分类任务:

代码语言:javascript复制
output = Dense(1, activation='sigmoid', name='sigmoid')(lstm1)

放在一起

现在,我们了解了组成部分,现在来看整个网络。 该网络显示在以下代码中,以供您参考:

代码语言:javascript复制
def build_network(vocab_size, embedding_dim, sequence_length):
    input = Input(shape=(sequence_length,), name="Input")
    embedding = Embedding(input_dim=vocab_size,  
       output_dim=embedding_dim, input_length=sequence_length, 
         name="embedding")(input)
    lstm1 = LSTM(10, activation='tanh', return_sequences=False,
       dropout=0.2, recurrent_dropout=0.2, name='lstm1')(embedding)
    output = Dense(1, activation='sigmoid', name='sigmoid')(lstm1)
    model = Model(inputs=input, outputs=output)
    model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
    return model

与其他二分类任务一样,我们可以使用二元交叉熵。 请注意,因为我们正在将 LSTM 层连接到密集层,所以我们需要将return_sequences设置为False,正如我们在第 9 章,“从头训练”中讨论的那样。

为了使这部分代码可重用,我们使词汇量,嵌入维数和序列长度可配置。 如果要搜索超参数,则还可能希望参数化dropoutrecurrent_dropout和 LSTM 神经元的数量。

训练网络

现在,我的情绪分析网络已经建立,现在该进行训练了:

代码语言:javascript复制
data = load_data(20000)
data = pad_sequences(data)
model = build_network(vocab_size=data["vocab_size"],
                      embedding_dim=100,
                      sequence_length=data["sequence_length"])

callbacks = create_callbacks("sentiment")

model.fit(x=data["X_train"], y=data["y_train"],
          batch_size=32,
          epochs=10,
          validation_data=(data["X_test"], data["y_test"]),
          callbacks=callbacks)

像这样将我所有的训练参数和数据保存在一个字典中,实际上只是一个样式问题,而与函数无关。 您可能希望单独处理所有事情。 我喜欢对所有内容使用字典,因为它使我无法来回传递大量参数。

由于我们使用的是无状态 LSTM,因此我们将在每个批次中重置单元存储器。 我的信念是,我们可以在不损失任何罚款的情况下重置文档之间的单元状态,因此批量大小实际上与表现有关。 我在这里使用了 32 个观察批,但是只要 GPU 内存允许,128 个观察批会产生相似的结果,并且表现会有所提高。

表现

从下面的屏幕截图中,让我们看一下我们的网络运行情况。 检查这些图时,请密切注意y轴上的刻度。 虽然挥杆动作看起来很戏剧性,但幅度并不大:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wxDmxQxv-1681567890367)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/d2911d59-77b8-4069-9bee-621999666e5d.png)]

这里首先要注意的是,在第 1 阶段,网络正在做的相当不错。 此后,它迅速开始过拟合。 总体而言,我认为我们的结果相当不错。 在第 1 阶段,我们会在验证集上正确预测约 86% 的时间的情绪。

尽管此案例研究涵盖了本章到目前为止已讨论的许多主题,但让我们再来看一个可以在嵌入层使用预训练的单词向量与我们学习的单词向量进行比较的地方。

有和没有 GloVe 的文档分类

在此示例中,我们将使用一个比较著名的文本分类问题,称为 news20。 在此问题中,我们获得了 19,997 个文档,每个文档都属于一个新闻组。 我们的目标是使用帖子的文本来预测该文本所属的新闻组。对于我们中间的千禧一代,新闻组是 Reddit 的先驱(但可能更接近伟大的 -Reddit 的曾祖父)。 这些新闻组涵盖的主题差异很大,包括政治,宗教和操作系统等主题,您应避免在礼貌的公司中讨论所有这些主题。 这些职位相当长,语料库中有 174,074 个独特的单词。

这次,我将构建模型的两个版本。 在第一个版本中,我们将使用嵌入层,并且将学习嵌入空间,就像在前面的示例中一样。 在第二个版本中,我将使用 GloVe 向量作为嵌入层的权重。 然后,我将花一些时间比较和对比这两种方法。

最后,在此示例中,我们将使用一维 CNN 代替 LSTM。

准备数据

当使用这样的文本文档时,可能需要很多平凡的代码才能使您到达想要的位置。 我将这个示例作为解决问题的一种方式。 一旦了解了这里发生的事情,就可以在将来的问题中重用其中的大部分内容并缩短开发时间,因此值得考虑。

以下函数将进入 20 个新闻组文本所在的顶级目录。 在该目录中,将有 20 个单独的目录,每个目录都有文件。 每个文件都是新闻组帖子:

代码语言:javascript复制
def load_data(text_data_dir, vocab_size, sequence_length, validation_split=0.2):
    data = dict()
    data["vocab_size"] = vocab_size
    data["sequence_length"] = sequence_length

    # second, prepare text samples and their labels
    print('Processing text dataset')

    texts = []  # list of text samples
    labels_index = {}  # dictionary mapping label name to numeric id
    labels = []  # list of label ids
    for name in sorted(os.listdir(text_data_dir)):
        path = os.path.join(text_data_dir, name)
        if os.path.isdir(path):
            label_id = len(labels_index)
            labels_index[name] = label_id
            for fname in sorted(os.listdir(path)):
                if fname.isdigit():
                    fpath = os.path.join(path, fname)
                    if sys.version_info < (3,):
                        f = open(fpath)
                    else:
                        f = open(fpath, encoding='latin-1')
                    t = f.read()
                    i = t.find('nn')  # skip header
                    if 0 < i:
                        t = t[i:]
                    texts.append(t)
                    f.close()
                    labels.append(label_id)
    print('Found %s texts.' % len(texts))
    data["texts"] = texts
    data["labels"] = labels
    return data

对于每个目录,我们将使用目录名称并将其添加到将其映射为数字的字典中。 这个数字将成为我们想要预测的值,我们的标签。 我们将把标签列表保留在data["labels"]中。

同样,对于文本,我们将打开每个文件,仅解析相关文本,而忽略有关谁在信息中张贴的垃圾邮件。 然后,我们将文本存储在data["texts"]中。 顺便说一句,删除标头中标识新闻组的部分非常重要。 那是作弊!

最后,我们剩下一个文本列表和一个相应的标签列表。 但是,此时,这些文本都是字符串。 我们需要做的下一件事是将这些字符串拆分为单词标记,将这些标记转换为数字标记,并填充序列,以使它们具有相同的长度。 这几乎是我们在前面的示例中所做的; 但是,在我们之前的示例中,数据已预先加标记。 我将使用此函数来完成任务,如以下代码所示:

代码语言:javascript复制
def tokenize_text(data):
    tokenizer = Tokenizer(num_words=data["vocab_size"])
    tokenizer.fit_on_texts(data["texts"])
    data["tokenizer"] = tokenizer
    sequences = tokenizer.texts_to_sequences(data["texts"])

    word_index = tokenizer.word_index
    print('Found %s unique tokens.' % len(word_index))

    data["X"] = pad_sequences(sequences, maxlen=data["sequence_length"])
    data["y"] = to_categorical(np.asarray(data["labels"]))
    print('Shape of data tensor:', data["X"].shape)
    print('Shape of label tensor:', data["y"].shape)

    # texts and labels aren't needed anymore
    data.pop("texts", None)
    data.pop("labels", None)
    return data

在这里,我们获取该文本列表,并使用keras.preprocessing.text.Tokenizer将其标记化。 之后,我们将它们填充为相等的长度。 最后,我们将数字标签转换为one_hot格式,就像 Keras 在其他多分类问题中一样。

我们几乎完成了数据处理。 但是,最后,我们需要获取文本和标签,然后将数据随机分成训练,验证和测试集,如以下代码所示。 我没有太多数据需要处理,因此我将在此处选择testval。 如果样本太小,可能无法很好地理解实际模型的表现,因此在执行此操作时要格外小心:

代码语言:javascript复制
def train_val_test_split(data):

    data["X_train"], X_test_val, data["y_train"],  y_test_val = train_test_split(data["X"],
                                                                                 data["y"],
                                                                                 test_size=0.2,
                                                                                 random_state=42)
    data["X_val"], data["X_test"], data["y_val"], data["y_test"] = train_test_split(X_test_val,
                                                                                    y_test_val,
                                                                                  test_size=0.25,
                                                                                 random_state=42)
    return data

加载预训练的单词向量

正如我刚才提到的,我将使用 Keras 嵌入层。 对于模型的第二个版本,我们将使用本章前面介绍的 GloVe 字向量来初始化嵌入层的权重。 为此,我们将需要从磁盘加载这些权重,并将它们放入合适的 2D 矩阵中,该层可用作权重。 我们将在这里介绍该操作。

下载 GloVe 向量时,您会发现在将下载文件解压缩到的目录中有几个文本文件。每个文件都对应一组单独的尺寸。 但是,在所有情况下,这些载体都是使用包含 60 亿个唯一单词的相同通用语料库开发的(因此标题为GloVe.6B)。 我将演示如何使用glove.6B.100d.txt文件。 在glove.6B.100d.txt中,每行都是单个单词向量。 在该行上,您将找到该单词和与其相关联的 100 维向量。 单词和向量的元素存储为文本,并用空格分隔。

为了使这些数据进入可用状态,我们将从磁盘加载开始。 然后,我们将线分为第一部分,单词和向量的元素。 完成此操作后,我们将向量转换为数组。 最后,我们将单词作为该值的键将数组作为值存储在字典中。 以下代码说明了此过程:

代码语言:javascript复制
def load_word_vectors(glove_dir):
    print('Indexing word vectors.')

    embeddings_index = {}
    f = open(os.path.join(glove_dir, 'glove.6B.100d.txt'),    
             encoding='utf8')
    for line in f:
        values = line.split()
        word = values[0]
        coefs = np.asarray(values[1:], dtype='float32')
        embeddings_index[word] = coefs
    f.close()

    print('Found %s word vectors.' % len(embeddings_index))
    return embeddings_index

运行此命令后,我们将有一个名为embeddings_index的字典,其中包含 GloVe 单词作为键,其向量作为值。 Keras 嵌入层需要 2D 矩阵作为输入,但是不需要字典,因此我们需要使用以下代码将字典操纵为矩阵:

代码语言:javascript复制
def embedding_index_to_matrix(embeddings_index, vocab_size, embedding_dim, word_index):
    print('Preparing embedding matrix.')

    # prepare embedding matrix
    num_words = min(vocab_size, len(word_index))
    embedding_matrix = np.zeros((num_words, embedding_dim))
    for word, i in word_index.items():
        if i >= vocab_size:
            continue
        embedding_vector = embeddings_index.get(word)
        if embedding_vector is not None:
            # words not found in embedding index will be all-zeros.
            embedding_matrix[i] = embedding_vector
    return embedding_matrix

我知道所有这些烦恼似乎都是可怕的,但确实如此,但是 GloVe 的作者在如何分配这些单词向量方面非常有心。 他们希望使使用任何一种编程语言的任何人都可以使用这些向量,为此,文本格式将受到人们的赞赏。 此外,如果您是一名实践中的数据科学家,您将习惯于此!

现在,我们将向量表示为 2D 矩阵,现在可以在 Keras 嵌入层中使用它们了。 我们的准备工作已经完成,所以现在让我们建立网络。

输入和嵌入层架构

我们在这里格式化 API 的方式与前面的示例稍有不同。 这种略有不同的结构将使在嵌入层中使用预训练向量更加容易。 我们将在以下各节中讨论这些结构性更改。

没有 GloVe 向量

让我们演示没有先训练词向量的embedding层的代码。 此代码应与上一个示例中的代码几乎相同:

代码语言:javascript复制
sequence_input = Input(shape=(sequence_length,), dtype='int32')
embedding_layer = Embedding(input_dim=vocab_size,
                            output_dim=embedding_dim,
                            input_length=sequence_length,
                            name="embedding")(sequence_input)

带有 GloVe 向量

现在,将其与包含以 2D 矩阵编码的预先训练的 GloVe 向量的代码进行比较:

代码语言:javascript复制
sequence_input = Input(shape=(sequence_length,), dtype='int32')
embedding_layer = Embedding(input_dim=vocab_size,
                            output_dim=embedding_dim,
                            weights=[embedding_matrix],
                            input_length=sequence_length,
                            trainable=False,
                            name="embedding")(sequence_input)

在大多数情况下,此代码看起来是等效的。 有两个主要区别:

  • 我们初始化层权重以包含在我们与weights=[embedding_matrix]组装的 GloVe 矩阵中。
  • 我们还将层设置为trainable=False。 这将阻止我们更新权重。 您可能希望以与微调权重相似的方式微调权重,该方式类似于我们在第 8 章“使用预训练的 CNN”进行的迁移学习中构建的 CNN,但是在大多数情况下, 不必要或没有帮助。

卷积层

对于一维卷积,层可以使用keras.layers.Conv1D。 我们将需要使用MaxPooling1D层以及Conv1D层,如以下代码所示:

代码语言:javascript复制
x = Conv1D(128, 5, activation='relu')(embedding_layer)
x = MaxPooling1D(5)(x)
x = Conv1D(128, 5, activation='relu')(x)
x = MaxPooling1D(5)(x)
x = Conv1D(128, 5, activation='relu')(x)
x = GlobalMaxPooling1D()(x)

对于Conv1D层,第一个整数参数是单元数,第二个是过滤器大小。 我们的过滤器只有一维,因此命名为 1D 卷积。 上例中的窗口大小为 5。

我正在使用的MaxPooling1D层也将使用 5 的窗口大小。相同的规则适用于一维实现中的池化层。

在最后一个卷积层之后,我们应用GlobalMaxPooling1D层。 该层是最大池化的特殊实现,它将获取最后一个Conv1D层(一个[batch x 35 x 128]张量)的输出,并跨时间步长将其合并到[batch x 128]。 这通常是在 NLP 网络中完成的,其目的类似于在基于图像的卷积网络中使用Flatten()层。 该层充当卷积层和密集层之间的桥梁。

输出层

此示例中的输出层看起来像其他任何多分类。 我在输出层之前也包括了一个密集层,如以下代码所示:

代码语言:javascript复制
x = Dense(128, activation='relu')(x)
preds = Dense(20, activation='softmax')(x)

放在一起

和以前一样,我们将在此处显示整个神经网络结构。 请注意,此结构适用于包含 GloVe 向量的模型版本:

代码语言:javascript复制
def build_model(vocab_size, embedding_dim, sequence_length, embedding_matrix):

    sequence_input = Input(shape=(sequence_length,), dtype='int32')
    embedding_layer = Embedding(input_dim=vocab_size,
                                output_dim=embedding_dim,
                                weights=[embedding_matrix],
                                input_length=sequence_length,
                                trainable=False,
                                name="embedding")(sequence_input)
    x = Conv1D(128, 5, activation='relu')(embedding_layer)
    x = MaxPooling1D(5)(x)
    x = Conv1D(128, 5, activation='relu')(x)
    x = MaxPooling1D(5)(x)
    x = Conv1D(128, 5, activation='relu')(x)
    x = GlobalMaxPooling1D()(x)
    x = Dense(128, activation='relu')(x)
    preds = Dense(20, activation='softmax')(x)
    model = Model(sequence_input, preds)
    model.compile(loss='categorical_crossentropy',
              optimizer='adam',
              metrics=['accuracy'])
    return model

我在这里再次使用adamcategorical_crossentropyaccuracy。 尽管本章介绍了许多新主题,但希望能看到保持不变的感觉会有些安慰。

训练

将所有代码放在一起,只需几行就可以完成训练,如以下代码所示:

代码语言:javascript复制
glove_dir = os.path.join(BASE_DIR, 'glove.6B')
text_data_dir = os.path.join(BASE_DIR, '20_newsgroup')
embeddings_index = load_word_vectors(glove_dir)

data = load_data(text_data_dir, vocab_size=20000, sequence_length=1000)
data = tokenize_text(data)
data = train_val_test_split(data)
data["embedding_dim"] = 100
data["embedding_matrix"] = embedding_index_to_matrix(embeddings_index=embeddings_index,
                                                     vocab_size=data["vocab_size"],
                                                     embedding_dim=data["embedding_dim"],
                                                     word_index=data["tokenizer"].word_index)

callbacks = create_callbacks("newsgroups-pretrained")
model = build_model(vocab_size=data["vocab_size"],
                    embedding_dim=data['embedding_dim'],
                    sequence_length=data['sequence_length'],
                    embedding_matrix=data['embedding_matrix'])

model.fit(data["X_train"], data["y_train"],
          batch_size=128,
          epochs=10,
          validation_data=(data["X_val"], data["y_val"]),
          callbacks=callbacks)

请注意,我们只训练 10 个周期,因此将这个问题的损失降到最低不会花很长时间。

表现

而我们在这里处于关键时刻。 让我们看看我的表现如何。 更重要的是,让我们将 GloVe 向量与该问题的学习向量进行比较。

以下屏幕截图中的橙色线对应于学习的嵌入层,蓝色线对应于 GloVe 向量:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-92djPc2c-1681567890367)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/dl-quick-ref/img/63265d98-f436-415a-ae52-1d53c78e5530.png)]

GloVe 预先训练的网络不仅学习得更快,而且在每个周期都表现得更好。 总体而言,这些网络似乎在学习文档分类任务方面做得很好。 大约在第五个周期之后,它们都开始过拟合。 但是,GloVe 模型比没有使用 GloVe 训练的网络更能防止过拟合。

通常,我建议尽可能在任何地方使用迁移学习。 图片和文字都是如此。

如果通过这些示例与我一起工作,我建议您对 LSTM 尝试同样的问题。 我认为使用 LSTM 时,您会发现该问题更加难以解决,并且难以解决过拟合问题。

总结

在本章中,我们以一般形式以及在情感分析的特定情况下研究了文档分类。 在此过程中,我们涵盖了很多 NLP 主题,包括 Word 袋模型,向量空间模型以及每个模型的相对优点。 我们还研究了使用 LSTM 和 1D 卷积进行文本分析。 最后,我们训练了两个单独的文档分类器,并通过实际示例应用了我们讨论的所有内容。

在下一章中,我们将讨论一个非常酷的自然语言模型,该模型将允许我们实际生成单词,称为序列到序列模型

0 人点赞