精通 TensorFlow 1.x:6~10

2023-04-23 11:29:49 浏览数 (3)

六、TensorFlow 和 Keras 中的 RNN

在涉及有序数据序列的问题中,例如时间序列预测自然语言处理,上下文对于预测输出非常有价值。可以通过摄取整个序列而不仅仅是最后一个数据点来确定这些问题的上下文。因此,先前的输出成为当前输入的一部分,并且当重复时,最后的输出结果是所有先前输入的结果以及最后一个输入。 循环神经网络RNN)架构是用于处理涉及序列的机器学习问题的解决方案。

循环神经网络RNN)是一种用于处理顺序数据的专用神经网络架构。顺序数据可以是一段时间内的观察序列,如时间序列数据,或字符序列,单词和句子,如文本数据。

标准神经网络的一个假设是,输入数据的排列方式是一个输入不依赖于另一个输入。然而,对于时间序列数据和文本数据,该假设不成立,因为序列中稍后出现的值通常受到之前出现的值的影响。

为了实现这一目标,RNN 通过以下方式扩展了标准神经网络:

  • 通过在计算图中添加循环或循环,RNN 增加了将一个层的输出用作相同或前一层的输入的特性。
  • RNN 添加存储器单元以存储可在当前计算中使用的先前输入和输出。

在本章中,我们将介绍以下有关 RNN 的主题:

  • 简单的循环神经网络
  • RNN 变种
  • LSTM
  • GRU
  • TensorFlow 中的 RNN
  • Keras 中的 RNN
  • Keras 中用于 MNIST 数据的 RNN

接下来的两章将介绍在 TensorFlow 和 Keras 中为时间序列和文本(NLP)数据构建 RNN 模型的实际示例。

简单循环神经网络

这是一个带循环的简单神经网络:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3qbF9y6w-1681566456865)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/fe89a986-6079-4b70-b184-39f9b4b5e8bb.png)]RNN Network

在该图中,神经网络N接受输入x[t]以产生输出y[t]。由于循环,在下一步t 1,输入y[t]和输入x[t 1]产生输出y[t 1]。在数学上,我们将其表示为以下等式:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OZnLtkeR-1681566456866)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/e6eb9d40-90e7-4850-bc5b-dc5fd44fa73f.png)]

当我们展开循环时,RNN 架构在时间步t1看起来如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ljxKhhaR-1681566456867)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/af2b19d9-701b-402e-8fbc-383fe1b045ba.png)]

随着时间步长的发展,这个循环在时间步骤 5 展开如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wF7hYIs7-1681566456867)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/3166287c-3e95-4248-9324-c17d7e4054b7.png)]

在每个时间步骤,使用相同的学习函数φ(·)和相同的参数,wb

输出y并不总是在每个时间步产生。相反,在每个时间步产生输出h,并且对该输出h应用另一个激活函数以产生输出y。 RNN 的等式现在看起来像这样:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TFeVS97S-1681566456868)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/1109a666-62de-4eac-928c-0888b0db98fa.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kxc54JhZ-1681566456868)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/eb66e53f-2c5e-4d4d-96b1-0d6dfd768ba6.png)]

其中,

  • w(hx)是连接到隐藏层的x输入的权重向量
  • w(hh)是来自前一时间步的h的值的权重向量
  • w(yh)是连接隐藏层和输出层的层的权重向量
  • 用于h[t]的函数通常是非线性函数,例如 tanh 或 ReLU

在 RNN 中,在每个时间步使用相同的参数w(hx), w(hh), w(yh), b(h), b(y)。这一事实大大减少了我们需要学习的基于序列模型的参数数量。

由此, RNN 在时间步t5如下展开,假设输出y仅在时间步t5产生:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YjC4Bma8-1681566456868)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/e3ba641c-7ad9-439e-87d6-f465c428d146.png)]

简单的 RNN 由 Elman 在 1990 年引入,因此它也被称为 Elman 网络。然而,简单的 RNN 无法满足我们今天的处理需求,因此我们将在下一节中了解 RNN 的变体。

阅读 Elman 的原始研究论文,了解 RNN 架构的起源:

代码语言:javascript复制
J. L. Elman, Finding Structure in Time, Cogn. Sci., vol. 14, no. 2, pp. 179–211, 1990.

RNN 变种

RNN 架构已经以多种方式扩展,以适应某些问题中的额外需求,并克服简单 RNN 模型的缺点。我们列出了下面的 RNN 架构的一些主要扩展。

  • 双向 RNNBRNN)用于输出依赖于序列的前一个和后一个元素的情况。 BRNN 通过堆叠两个 RNN(称为前向和后向层)来实现,输出是前向和后向层 RNN 的隐藏状态的结果。在前向层中,存储器状态 h 从时间步长t流向时间步长t 1,并且在后向层中,存储器状态从时间步长t流出。到时间步t-1。两个层在时间步t时采用相同的输入x[t],但它们在时间步t共同产生输出。
  • 深双向 RNNDBRNN)通过添加多个层进一步扩展 BRNN。 BRNN 在时间维度上隐藏了层或单元。但是,通过堆叠 BRNN,我们可以在 DBRNN 中获得分层表示。其中一个显着差异是,在 BRNN 中,我们对同一层中的每个单元使用相同的参数,但在 DBRNN 中,我们对每个堆叠层使用不同的参数。
  • 长短期记忆LSTM)网络通过使用涉及多个非线性函数而不是一个简单非线性函数的架构来扩展 RNN 隐藏状态。 LSTM 由称为单元的黑盒组成,取三个输入:时间t-1的工作记忆(h[t-1]),当前输入(x[t])和时间t-1的长期记忆(c[t-1]),并产生两个输出:更新的工作记忆(h[t])和长期记忆(c[t])。单元使用称为门的函数来决定从记忆中选择性地保存和擦除的内容。我们在下面的部分中详细描述了 LSTM。

阅读以下关于 LSTM 的研究论文,以获得有关 LSTM 起源的更多信息:

代码语言:javascript复制
S. Hochreiter and J. Schmidhuber, Long Short-Term Memory, Neural Comput., vol. 9, no. 8, pp. 1735–1780, 1997.http://www.bioinf.jku.at/publications/older/2604.pdf
  • 门控循环单元GRU)网络是 LSTM 的简化变体。 结合遗忘和输入的功能,在更简单的更新门中进行门控。它还将隐藏状态和单元状态组合成一个单一状态。因此,与 LSTM 相比,GRU 在计算上更便宜。 我们在下面的部分中详细描述了 GRU。

阅读以下研究论文以探索 GRU 的更多细节:

代码语言:javascript复制
K. Cho, B. van Merrienboer, C. Gulcehre, D. Bahdanau, F. Bougares, H. Schwenk, and Y. Bengio, Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation, 2014.https://arxiv.org/abs/1406.1078

J. Chung, C. Gulcehre, K. Cho, and Y. Bengio, Empirical Evaluation of Gated Recurrent Neural Networks on Sequence Modeling, pp. 1–9, 2014. https://arxiv.org/abs/1412.3555
  • seq2seq 模型将编码器 - 解码器架构与 RNN 架构相结合。在 Seq2Seq 架构中,模型训练数据序列,例如文本数据或时间序列数据,然后该模型用于生成输出序列。例如,在英文文本上训练模型,然后从模型生成西班牙文本。 Seq2Seq 模型由编码器和解码器模型组成,它们都使用 RNN 架构构建。可以堆叠 Seq2Seq 模型以构建分层多层模型。

LSTM 网络

当 RNN 在很长的数据序列上进行训练时,梯度往往变得非常大或非常小,它们会消失到几乎为零。 长短期记忆LSTM)网络通过添加用于控制对过去信息的访问的门,来解决消失/爆炸梯度问题。 LSTM 概念最初由 Hochreiter 和 Schmidhuber 在 1997 年引入。

阅读以下关于 LSTM 的研究论文,以获得有关 LSTM 起源的更多信息:

代码语言:javascript复制
S. Hochreiter and J. Schmidhuber, Long Short-Term Memory, Neural Comput., vol. 9, no. 8, pp. 1735–1780, 1997. http://www.bioinf.jku.at/publications/older/2604.pdf

在 RNN 中,使用重复使用的学习函数φ的单个神经网络层,而在 LSTM 中,使用由四个主要函数组成的重复模块。构建 LSTM 网络的模块称为单元。 LSTM 单元通过选择性地学习或擦除信息,有助于在长序列通过时更有效地训练模型。组成单元的函数也称为门,因为它们充当传入和传出单元的信息的网守。

LSTM 模型有两种记忆:

  • h(隐藏状态)表示的工作记忆
  • c(单元状态)表示的长期记忆。

单元状态或长期记忆仅在两个线性相互作用下从一个单元流向另一个单元。 LSTM 将信息添加到长期记忆中,或通过门从长期记忆中删除信息。

下图描绘了 LSTM 单元:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lpig1Mys-1681566456869)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/5cd7f05b-1514-45d1-948f-b023de11377d.png)]The LSTM Cell

通过 LSTM 单元中的门的内部流动如下:

  1. 遗忘门(或记忆门)f()h[t-1]x[t]按照以下等式作为输入流向f()门: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z90pEAxO-1681566456869)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/925e0a35-3fef-4e56-a3b9-852ee9704a88.png)] 遗忘门的功能是决定忘记哪些信息以及要记住哪些信息。这里使用sigmoid激活函数,因此输出 1 表示信息被转移到单元内的下一步骤,输出 0 表示信息被选择性地丢弃。
  2. 输入门(或保存门)i()h[t-1]x[t]按照以下等式作为输入流向i()门: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EiTFBTIr-1681566456869)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/9b60f5ae-c253-4d41-8896-c3a94db45567.png)] 输入门的功能是决定是保存还是丢弃输入。输入功能还允许单元了解要保留或丢弃的候选存储器的哪个部分。
  3. 候选长期记忆:候选长期记忆由h[t-1]x[t]使用激活函数计算,主要是tanh,按照下式: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kqlqNwGF-1681566456870)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/338bafaf-9c59-40d7-9aa4-29b96248c40b.png)]
  4. 接下来,组合前面的三个计算以得到更新长期记忆,由c[t]表示,如下式所示: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-64p0sy5j-1681566456870)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/f244e66e-0b70-49e7-b1c3-dade32143d29.png)]
  5. 输出门(或聚焦/关注门)o()h[t-1]x[t]按照以下等式作为输入流向o()门: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Aq1iycWF-1681566456870)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/b32dd2e9-bafa-41ae-ab11-0dfbd95f3a02.png)] 输出门的功能是决定多少信息可用于更新工作记忆。
  6. 接下来,工作记忆h[t]从长期记忆c[t]和焦点/注意力向量更新,如下式所示: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UTI1H7H3-1681566456871)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/9e296cf4-d44b-4d7c-b6a5-aa4f7afce542.png)] 其中φ(·)是激活函数,通常是tanh

GRU 网络

LSTM 网络的计算成本很高,因此,研究人员发现了一种几乎同样有效的 RNN 配置,称为门控循环单元GRU)架构。

在 GRU 中,不使用工作和长期记忆,只使用一种记忆,用h(隐藏状态)表示。 GRU 单元通过复位更新门,将信息添加到此状态存储器,或从该状态存储器中删除信息。

下图描绘了 GRU 单元(说明如下图):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-skqtBX4M-1681566456871)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/cb5734c0-dc19-4d5d-bf98-eb448f083dd2.png)]The GRU Cell

GRU 单元中通过门的内部流量如下:

  1. 更新门u():输入h[t-1]x[t]按照以下公式流向u()门: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0OVUBrEQ-1681566456871)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/772bddac-9209-47d8-a1c4-eb66fe59fe1f.png)]
  2. 复位门r():输入h[t-1]x[t]按照以下公式流向r()门: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1QFTESE7-1681566456872)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/a0f59f6c-94df-4742-bd5f-5ac3d81540cf.png)]
  3. 候选状态记忆:候选长期记忆是根据r()门,h[t-1]x[t]的输出计算出来的,按照下列公式: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WF2wwZP8-1681566456872)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/7ce40c11-c131-493c-8ccf-7759863540c0.png)]
  4. 接下来,组合前面的三个计算以得到更新的状态存储器,由h[t],表示,如下式所示: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jjw0XbEF-1681566456872)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/519ccb8b-513c-4b1a-a1d7-9f48d6c2577b.png)]

阅读以下研究论文以探索 GRU 的更多细节:

代码语言:javascript复制
K. Cho, B. van Merrienboer, C. Gulcehre, D. Bahdanau, F. Bougares, H. Schwenk, and Y. Bengio, Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation, 2014. https://arxiv.org/abs/1406.1078

J. Chung, C. Gulcehre, K. Cho, and Y. Bengio, Empirical Evaluation of Gated Recurrent Neural Networks on Sequence Modeling, pp. 1–9, 2014. https://arxiv.org/abs/1412.3555

TensorFlow RNN

在低级 TensorFlow 库中创建 RNN 模型的基本工作流程与 MLP 几乎相同:

  • 首先创建形状的输入和输出占位符(None, #TimeSteps, #Features)(批量大小, #TimeSteps, #Features)
  • 从输入占位符中,创建一个长度为#TimeSteps的列表,其中包含形状的张量(None, #特征)(批量大小, #特征)
  • tf.rnn.rnn_cell模块创建所需 RNN 类型的单元
  • 使用先前创建的单元和输入张量列表来创建静态或动态 RNN
  • 创建输出权重和偏差变量,并定义损失和优化器函数
  • 对于所需的周期数,使用损失和优化器函数训练模型

这个基本工作流程将在下一章的示例代码中演示。让我们看看可用于支持先前工作流程的各种类。

TensorFlow RNN 单元类

tf.nn.rnn_cell模块包含以下用于在 TensorFlow 中创建不同类型单元的类:

描述

BasicRNNCell

提供 RNN 单元的实现

BasicLSTMCell

提供 LSTM RNN 单元的实现,基于这个页面

LSTMCell

提供 LSTM RNN 单元,基于这个页面和这个页面

GRUCell

提供 GRU RNN 单元,基于这个页面

MultiRNNCell

提供由连续连接的多个简单单元组成的 RNN 单元

tf.contrib.rnn模块提供以下额外的类用于在 TensorFlow 中创建不同类型的单元:

描述

LSTMBlockCell

提供块 LSTM RNN 单元,基于这个页面

LSTMBlockFusedCell

提供块融合 LSTM RNN 单元,基于这个页面

GLSTMCell

提供分组 LSTM 单元,基于这个页面

GridLSTMCell

提供网格 LSTM RNN 单元,基于这个页面

GRUBlockCell

提供块 GRU RNN 单元,基于这个页面

BidirectionalGridLSTMCell

仅在频率上而不是在时间上提供双向网格 LSTM

NASCell

提供神经架构搜索 RNN 单元,基于这个页面

UGRNNCell

提供更新门 RNN 单元,基于这个页面

TensorFlow RNN 模型构建类

TensorFlow 提供了从 RNN 单元对象创建 RNN 模型的类。静态 RNN 类在编译时为时间步骤添加展开的单元,而动态 RNN 类在运行时添加展开的单元用于时间步长。

  • tf.nn.static_rnn
  • tf.nn.static_state_saving_rnn
  • tf.nn.static_bidirectional_rnn
  • tf.nn.dynamic_rnn
  • tf.nn.bidirectional_dynamic_rnn
  • tf.nn.raw_rnn
  • tf.contrib.rnn.stack_bidirectional_dynamic_rnn

TensorFlow RNN 单元包装器类

TensorFlow 还提供包装其他单元类的类:

  • tf.contrib.rnn.LSTMBlockWrapper
  • tf.contrib.rnn.DropoutWrapper
  • tf.contrib.rnn.EmbeddingWrapper
  • tf.contrib.rnn.InputProjectionWrapper
  • tf.contrib.rnn.OutputProjectionWrapper
  • tf.contrib.rnn.DeviceWrapper
  • tf.contrib.rnn.ResidualWrapper

有关 TensorFlow 中 RNN 的最新文档,请访问此链接。

Keras 中的 RNN

与 TensorFlow 相比,在 Keras 中创建 RNN 要容易得多。正如您在第 3 章中学到的,Keras 提供了用于创建循环网络的函数式和顺序 API。要构建 RNN 模型,您必须从kera.layers.recurrent模块添加层。 Keras 在keras.layers.recurrent模块中提供以下类型的循环层:

  • SimpleRNN
  • LSTM
  • GRU

有状态模型

Keras 循环层还支持 RNN 模型,可在批次之间保存状态。您可以通过将stateful参数作为True传递来创建有状态 RNN,LSTM 或 GRU 模型。对于有状态模型,为输入指定的批量大小必须是固定值。在有状态模型中,从训练批次中学到的隐藏状态将重新用于下一批。如果您想在训练期间的某个时刻重置记忆,可以通过调用model.reset_states()layer.reset_states()函数使用额外的代码来完成。

我们将在下一章中看到使用 Keras 构建 RNN 的示例。

有关 Keras 循环层的最新文档可在此链接中找到。

RNN 的应用领域

RNN 更频繁使用的一些应用领域如下:

  • 自然语言模型:RNN 模型已用于自然语言处理(NLP),用于自然语言理解和自然语言生成任务。在 NLP 中,RNN 模型被给予一系列单词并且它预测另一个单词序列。因此,训练的模型可用于生成单词序列,称为文本生成的字段。例如,生成故事和剧本。 NLP 的另一个领域是语言翻译,其中给定一种语言的一系列单词,该模型预测另一种语言的单词序列。
  • 语音和语音识别:RNN 模型非常适用于构建模拟音频数据的模型。在语音识别中,RNN 模型被给予音频数据并且它预测一系列语音片段。它可用于训练模型以识别语音命令,甚至用于与基于语音的聊天机器人的对话。
  • 图像/视频描述或字幕生成:RNN 模型可与 CNN 结合使用,以生成图像和视频中找到的元素的描述。这些描述也可用于生成图像和视频的标题。
  • 时间序列数据:最重要的是,RNN 对时间序列数据非常有用。大多数传感器和系统生成时间顺序很重要的数据。 RNN 模型非常适合于查找模式和预测此类数据。

通过此链接了解有关 RNN 的更多信息:

http://karpathy.github.io/2015/05/21/rnn-effectiveness/

http://colah.github.io/posts/2015-08-Understanding-LSTMs/

http://www.wildml.com/2015/09/recurrent-neural-networks-tutorial-part-1-introduction-to-rnns/

https://r2rt.com/written-memories-understanding-deriving-and-extending-the-lstm.html

Keras 中的用于 MNIST 数据的 RNN

虽然 RNN 主要用于序列数据,但它也可用于图像数据。我们知道图像具有最小的两个维度 - 高度和宽度。现在将其中一个维度视为时间步长,将其他维度视为特征。对于 MNIST,图像大小为28 x 28像素,因此我们可以将 MNIST 图像视为具有 28 个时间步长,每个时间步长具有 28 个特征。

我们将在下一章中提供时间序列和文本数据的示例,但让我们为 Keras 中的 MNIST 构建和训练 RNN,以快速浏览构建和训练 RNN 模型的过程。

您可以按照 Jupyter 笔记本中的代码ch-06_RNN_MNIST_Keras

导入所需的模块:

代码语言:javascript复制
import keras
from keras.models import Sequential
from keras.layers import Dense, Activation
from keras.layers.recurrent import SimpleRNN
from keras.optimizers import RMSprop
from keras.optimizers import SGD

获取 MNIST 数据并将数据从 1D 中的 784 像素转换为 2D 中的28 x 28像素:

代码语言:javascript复制
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets(os.path.join(datasetslib.datasets_root,
                                               'mnist'), 
                                  one_hot=True)
X_train = mnist.train.images
X_test = mnist.test.images
Y_train = mnist.train.labels
Y_test = mnist.test.labels
n_classes = 10
n_classes = 10
X_train = X_train.reshape(-1,28,28)
X_test = X_test.reshape(-1,28,28)

在 Keras 构建SimpleRNN模型:

代码语言:javascript复制
# create and fit the SimpleRNN model
model = Sequential()
model.add(SimpleRNN(units=16, activation='relu', input_shape=(28,28)))
model.add(Dense(n_classes))
model.add(Activation('softmax'))

model.compile(loss='categorical_crossentropy',
              optimizer=RMSprop(lr=0.01),
              metrics=['accuracy'])
model.summary()

该模型如下:

代码语言:javascript复制
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
simple_rnn_1 (SimpleRNN)     (None, 16)                720       
_________________________________________________________________
dense_1 (Dense)              (None, 10)                170       
_________________________________________________________________
activation_1 (Activation)    (None, 10)                0         
=================================================================
Total params: 890
Trainable params: 890
Non-trainable params: 0
_________________________________________________________________

训练模型并打印测试数据集的准确率:

代码语言:javascript复制
model.fit(X_train, Y_train,
          batch_size=100, epochs=20)

score = model.evaluate(X_test, Y_test)
print('nTest loss:', score[0])
print('Test accuracy:', score[1])

我们得到以下结果:

代码语言:javascript复制
Test loss: 0.520945608187
Test accuracy: 0.8379

总结

在本章中,我们了解了循环神经网络(RNN)。我们了解了 RNN 的各种变体,并详细描述了其中的两个:长短期记忆(LSTM)网络和门控循环单元(GRU)网络。我们还描述了可用于在 TensorFlow 和 Keras 中构建 RNN 单元,模型和层的类。我们构建了一个简单的 RNN 网络,用于对 MNIST 数据集的数字进行分类。

在下一章中,我们将学习如何构建和训练时间序列数据的 RNN 模型。

七、TensorFlow 和 Keras 中的用于时间序列数据的 RNN

时间序列数据是一系列值,以不同的时间间隔记录或测量。作为序列,RNN 架构是从这些数据训练模型的最佳方法。在本章中,我们将使用示例时间序列数据集来展示如何使用 TensorFlow 和 Keras 构建 RNN 模型。

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

  • 航空公司乘客(airpass)时间序列数据集:
    • 描述和下载数据集
    • 可视化数据集
  • 在 TensorFlow 中预处理 RNN 的数据集
  • TensorFlow 中用于时间序列数据的 RNN:
    • TensorFlow 中的SimpleRNN
    • TensorFlow 中的 LSTM
    • TensorFlow 中的 GRU
  • 在 Keras 中为 RNN 预处理数据集
  • Keras 中用于时间序列数据的 RNN:
    • Keras 的SimpleRNN
    • Keras 的 LSTM
    • Keras 的 GRU

让我们从了解样本数据集开始。

您可以按照 Jupyter 笔记本中的代码ch-07a_RNN_TimeSeries_TensorFlow

航空公司乘客数据集

为了简洁起见,我们选择了一个名为国际航空公司乘客(航空通票)的非常小的数据集。该数据包含从 1949 年 1 月到 1960 年 12 月的每月总乘客数量。数据集中的数字是指数千的数量。该数据集最初由 Box 和 Jenkins 在 1976 年的工作中使用。它作为 时间序列数据集库TSDL)的一部分与 Rob Hyndman 教授的各种其他时间序列数据集一起收集。在澳大利亚莫纳什大学。后来,TSDL 被转移到 DataMarket。

您可以从此链接下载数据集。

加载 airpass 数据集

我们将数据集保存为数据集根目录(~/datasets)中ts-data文件夹中的 CSV 文件,并使用以下命令将数据加载到 pandas 数据框中:

代码语言:javascript复制
filepath = os.path.join(datasetslib.datasets_root,
                        'ts-data',
                        'international-airline-passengers-cleaned.csv'
                       ) 
dataframe = pd.read_csv(filepath,usecols=[1],header=0)
dataset = dataframe.values
dataset = dataset.astype(np.float32)

从 NumPy 数组中的数据框中提取值并转换为np.float32

代码语言:javascript复制
dataset = dataframe.values
dataset = dataset.astype(np.float32)

可视化 airpass 数据集

让我们看一下数据集的外观:

代码语言:javascript复制
plt.plot(dataset,label='Original Data')
plt.legend()
plt.show()

airpass数据集的图如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xsq0rwa0-1681566456872)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/305f4a94-92a4-4164-9ec2-473cc7225413.png)]Airline Passengers Dataset

用于 TensorFlow RNN 模型的数据集预处理

为了使其为学习模型做好准备,通过应用 MinMax 缩放来正则化数据集,该缩放使数据集值介于 0 和 1 之间。您可以尝试根据数据的性质对数据应用不同的缩放方法。

代码语言:javascript复制
# normalize the dataset
scaler = skpp.MinMaxScaler(feature_range=(0, 1))
normalized_dataset = scaler.fit_transform(dataset)

我们使用自己开发的实用函数将数据集拆分为训练和测试数据集。必须拆分数据而不对数据集进行混洗,因为改组数据集会破坏序列。维护数据序列对于训练时间序列模型非常重要。

代码语言:javascript复制
train,test=tsu.train_test_split(normalized_dataset,train_size=0.67)

然后我们将训练和测试数据集转换为有监督的机器学习集。让我们试着理解监督学习集的含义。假设我们有一系列数据:1,2,3,4,5。我们想要了解生成数据集的概率分布。为了做到这一点,我们可以假设时间步长t的值是从时间步长t-1tk的值的结果,其中k是窗口大小。为简化起见,假设窗口大小为 1。因此,时间步长t的值(称为输入特征)是时间步长值t-1的结果,被称为目标。让我们重复一遍所有时间步骤,我们得到下表:

输入值或特征

输出值或目标

1

2

2

3

3

4

4

5

我们展示的示例只有一个变量值,它将转换为特征和目标。当目标值取决于一个变量时,它被称为单变量时间序列。同样的逻辑可以应用于多变量时间序列,其中目标取决于多个变量。我们使用x来表示输入特征,使用y来表示输出目标。

考虑到这一背景,为了将airpass数据转换为监督机器学习数据,我们设置了以下超参数:

  1. 设置用于学习或预测下一个时间步的过去时间步数:
代码语言:javascript复制
n_x=1  
  1. 设置学习或预测的未来时间步长的数量:
代码语言:javascript复制
n_y=1 
  1. 设置用于学习的x变量的数量;由于当前示例是单变量的,因此设置为 1:
代码语言:javascript复制
n_x_vars = 1  
  1. 设置要预测的y变量的数量;由于当前示例是单变量的,因此设置为 1:
代码语言:javascript复制
n_y_vars = 1  
  1. 最后,我们通过应用本节开头所述的逻辑将训练和测试数据集转换为XY集:
代码语言:javascript复制
X_train, Y_train, X_test, Y_test = tsu.mvts_to_xy(train,
                                  test,n_x=n_x,n_y=n_y)

现在数据已经过预处理并可以输入到我们的模型中,让我们使用 TensorFlow 准备一个SimpleRNN模型。

TensorFlow 中的简单 RNN

在 TensorFlow 中定义和训练简单 RNN 的工作流程如下:

  1. 定义模型的超参数:
代码语言:javascript复制
state_size = 4
n_epochs = 100
n_timesteps = n_x 
learning_rate = 0.1

这里新的超参数是state_sizestate_size表示 RNN 单元的权重向量的数量。

  1. 为模型定义XY参数的占位符。X占位符的形状为(batch_size, number_of_input_timesteps, number_of_inputs)Y占位符的形状为(batch_size, number_of_output_timesteps, number_of_outputs)。对于batch_size,我们使用None,以便我们以后可以输入任意大小的批次。
代码语言:javascript复制
X_p = tf.placeholder(tf.float32, [None, n_timesteps, n_x_vars], 
    name='X_p') 
Y_p = tf.placeholder(tf.float32, [None, n_timesteps, n_y_vars], 
    name='Y_p')
  1. 将输入占位符X_p转换为长度等于时间步数的张量列表,在此示例中为n_x或 1:
代码语言:javascript复制
# make a list of tensors of length n_timesteps
rnn_inputs = tf.unstack(X_p,axis=1)
  1. 使用tf.nn.rnn_cell.BasicRNNCell创建一个简单的 RNN 单元:
代码语言:javascript复制
cell = tf.nn.rnn_cell.BasicRNNCell(state_size)
  1. TensorFlow 提供static_rnndynamic_rnn便利方法(以及其他方法)分别创建静态和动态 RNN。创建静态 RNN:
代码语言:javascript复制
rnn_outputs, final_state = tf.nn.static_rnn(cell, 
                                            rnn_inputs,
                                            dtype=tf.float32
                                           )

静态 RNN 在编译时创建单元,即展开循环。动态 RNN 创建单元,即在运行时展开循环 。在本章中,我们仅展示了static_rnn的示例,但是一旦获得静态 RNN 的专业知识,就应该探索dynamic_rnn

static_rnn方法采用以下参数:

  • cell:我们之前定义的基本 RNN 单元对象。它可能是另一种单元,我们将在本章中进一步看到。
  • rnn_inputs:形状(batch_size, number_of_inputs)的张量列表。
  • dtype:初始状态和预期输出的数据类型。
  1. 定义预测层的权重和偏差参数:
代码语言:javascript复制
W = tf.get_variable('W', [state_size, n_y_vars])
b = tf.get_variable('b', [n_y_vars], 
    initializer=tf.constant_initializer(0.0))
  1. 将预测层定义为密集线性层:
代码语言:javascript复制
predictions = [tf.matmul(rnn_output, W)   b 
                for rnn_output in rnn_outputs]
  1. 输出 Y 是张量的形状;将其转换为张量列表:
代码语言:javascript复制
y_as_list = tf.unstack(Y_p, num=n_timesteps, axis=1)
  1. 将损失函数定义为预测标签和实际标签之间的均方误差:
代码语言:javascript复制
mse = tf.losses.mean_squared_error
losses = [mse(labels=label, predictions=prediction) 
          for prediction, label in zip(predictions, y_as_list)
         ]
  1. 将总损失定义为所有预测时间步长的平均损失:
代码语言:javascript复制
total_loss = tf.reduce_mean(losses)
  1. 定义优化器以最小化total_loss
代码语言:javascript复制
optimizer = tf.train.AdagradOptimizer(learning_rate).minimize(total_loss)
  1. 现在我们已经定义了模型,损耗和优化器函数,让我们训练模型并计算训练损失:
代码语言:javascript复制
with tf.Session() as tfs:
    tfs.run(tf.global_variables_initializer())
    epoch_loss = 0.0
    for epoch in range(n_epochs):
        feed_dict={X_p: X_train.reshape(-1, n_timesteps, 
                                        n_x_vars), 
                   Y_p: Y_train.reshape(-1, n_timesteps, 
                                        n_x_vars)
                  }
        epoch_loss,y_train_pred,_=tfs.run([total_loss,predictions,
                                optimizer], feed_dict=feed_dict)
    print("train mse = {}".format(epoch_loss))

我们得到以下值:

代码语言:javascript复制
train mse = 0.0019413739209994674
  1. 让我们在测试数据上测试模型:
代码语言:javascript复制
feed_dict={X_p: X_test.reshape(-1, n_timesteps,n_x_vars), 
           Y_p: Y_test.reshape(-1, n_timesteps,n_y_vars)
          }
test_loss, y_test_pred = tfs.run([total_loss,predictions], 
                                 feed_dict=feed_dict
                                )
print('test mse = {}'.format(test_loss))
print('test rmse = {}'.format(math.sqrt(test_loss)))

我们在测试数据上得到以下 mse 和 rmse(均方根误差):

代码语言:javascript复制
test mse = 0.008790395222604275
test rmse = 0.09375710758446143

这非常令人印象深刻。

这是一个非常简单的例子,只用一个变量值预测一个时间步。在现实生活中,输出受到多个特征的影响,并且需要预测不止一个时间步。后一类问题被称为多变量多时间步进预测问题。这些问题是使用循环神经网络进行更好预测的积极研究领域。

现在让我们重新调整预测和原始值并绘制原始值(请在笔记本中查找代码)。

我们得到以下绘图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GlhgKgww-1681566456873)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/b83815e7-8ee4-469a-a23a-8a71b9c46b30.png)]

令人印象深刻的是,在我们的简单示例中,预测数据几乎与原始数据相匹配。对这种准确预测的一种可能解释是,单个时间步的预测基于来自最后一个时间步的单个变量的预测,因此它们总是在先前值的附近。

尽管如此,前面示例的目的是展示在 TensorFlow 中创建 RNN 的方法。现在让我们使用 RNN 变体重新创建相同的示例。

TensorFlow 中的 LSTM

由于爆炸和消失梯度的问题,简单的 RNN 架构并不总是有效,因此使用了改进的 RNN 架构,例如 LSTM 网络。 TensorFlow 提供 API 来创建 LSTM RNN 架构。

在上一节中展示的示例中,要将简单 RNN 更改为 LSTM 网络,我们所要做的就是更改单元类型,如下所示:

代码语言:javascript复制
cell = tf.nn.rnn_cell.LSTMCell(state_size)

其余代码保持不变,因为 TensorFlow 会为您在 LSTM 单元内创建门。

笔记本ch-07a_RNN_TimeSeries_TensorFlow中提供了 LSTM 模型的完整代码。

然而,对于 LSTM,我们必须运行 600 个周期的代码才能使结果更接近基本 RNN。原因是 LSTM 需要学习更多参数,因此需要更多的训练迭代。对于我们的简单示例,它似乎有点过分,但对于较大的数据集,与简单的 RNN 相比,LSTM 显示出更好的结果。

具有 LSTM 架构的模型的输出如下:

代码语言:javascript复制
train mse = 0.0020806745160371065
test mse = 0.01499235536903143
test rmse = 0.12244327408653947

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-72endx7a-1681566456878)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/5dd03142-e42f-4e1c-bf60-19e31475b853.png)]

TensorFlow 中的 GRU

要将最后一节中的 LSTM 示例更改为 GRU 网络, 按如下方式更改单元类型,TensorFlow 将为您处理其余部分:

代码语言:javascript复制
cell = tf.nn.rnn_cell.GRUCell(state_size)

笔记本ch-07a_RNN_TimeSeries_TensorFlow中提供了 GRU 模型的完整代码。

对于小airpass数据集,GRU 在相同数量的周期中表现出更好的表现。在实践中,GRU 和 LSTM 表现出相当的表现。就执行速度而言,与 LSTM 相比,GRU 模型训练和预测更快。

GRU 模型的完整代码在 Jupyter 笔记本中提供。GRU 模型的结果如下:

代码语言:javascript复制
train mse = 0.0019633215852081776
test mse = 0.014307591132819653
test rmse = 0.11961434334066987

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ckbMoSmQ-1681566456879)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/2d9ac55a-7f30-406b-934d-3cba2d9c6a91.png)]

我们鼓励您探索 TensorFlow 中可用的其他选项来创建循环神经网络。现在让我们在 TensorFlow 的一个高级库中尝试相同的示例。

对于下一节,您可以按照 Jupyter 笔记本中的代码ch-07b_RNN_TimeSeries_Keras

用于 Keras RNN 模型的数据集预处理

与使用较低级别 TensorFlow 类和方法构建相比,在 Keras 中构建 RNN 网络要简单得多。对于 Keras,我们预先处理数据,如前面部分所述,以获得受监督的机器学习时间序列数据集:X_train, Y_train, X_test, Y_test

从这里开始,预处理有所不同。对于 Keras,输入必须是(samples, time steps, features)形状。当我们将数据转换为监督机器学习格式时,在重塑数据时,我们可以将时间步长设置为 1,从而将所有输入时间步长作为特征,或者我们可以设置时间步长为实际的时间步数,从而为每个时间步长提供特征集。换句话说,我们之前获得的X_trainX_test数据集可以重新整形为以下方法之一:

方法 1:n时间步长与1特征:

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

方法 2:1时间步长n特征:

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

在本章中,我们将对特征大小为 1 的数据集进行整形,因为我们只使用一个变量作为输入:

代码语言:javascript复制
# reshape input to be [samples, time steps, features]
X_train = X_train.reshape(X_train.shape[0], X_train.shape[1],1)
X_test = X_test.reshape(X_test.shape[0], X_train.shape[1], 1)

Keras 中的简单 RNN

通过添加具有内部神经元数量和输入张量形状的SimpleRNN层,可以在 Keras 中轻松构建 RNN 模型,不包括样本维数。以下代码创建,编译和拟合SimpleRNN

代码语言:javascript复制
# create and fit the SimpleRNN model
model = Sequential()
model.add(SimpleRNN(units=4, input_shape=(X_train.shape[1], 
    X_train.shape[2])))
model.add(Dense(1))
model.compile(loss='mean_squared_error', optimizer='adam')
model.fit(X_train, Y_train, epochs=20, batch_size=1)

由于我们的数据集很小,我们使用batch_size为 1 并训练 20 次迭代,但对于较大的数据集,您需要调整这些和其他超参数的值。

该模型的结构如下:

代码语言:javascript复制
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
simple_rnn_1 (SimpleRNN)     (None, 4)                 24        
_________________________________________________________________
dense_1 (Dense)              (None, 1)                 5         
=================================================================
Total params: 29
Trainable params: 29
Non-trainable params: 0

训练的结果如下:

代码语言:javascript复制
Epoch 1/20
95/95 [==============================] - 0s - loss: 0.0161     
Epoch 2/20
95/95 [==============================] - 0s - loss: 0.0074     
Epoch 3/20
95/95 [==============================] - 0s - loss: 0.0063     
Epoch 4/20
95/95 [==============================] - 0s - loss: 0.0051     

-- epoch 5 to 14 removed for the sake of brevity --

Epoch 14/20
95/95 [==============================] - 0s - loss: 0.0021     
Epoch 15/20
95/95 [==============================] - 0s - loss: 0.0020     
Epoch 16/20
95/95 [==============================] - 0s - loss: 0.0020     
Epoch 17/20
95/95 [==============================] - 0s - loss: 0.0020     
Epoch 18/20
95/95 [==============================] - 0s - loss: 0.0020         
Epoch 19/20
95/95 [==============================] - 0s - loss: 0.0020     
Epoch 20/20
95/95 [==============================] - 0s - loss: 0.0020     

损失从 0.0161 开始,平稳在 0.0020。让我们做出预测并重新调整预测和原件。我们使用 Keras 提供的函数来计算均方根误差:

代码语言:javascript复制
from keras.losses import mean_squared_error as k_mse
from keras.backend import sqrt as k_sqrt
import keras.backend as K

# make predictions
y_train_pred = model.predict(X_train)
y_test_pred = model.predict(X_test)

# invert predictions
y_train_pred = scaler.inverse_transform(y_train_pred)
y_test_pred = scaler.inverse_transform(y_test_pred)

#invert originals
y_train_orig = scaler.inverse_transform(Y_train)
y_test_orig = scaler.inverse_transform(Y_test)

# calculate root mean squared error
trainScore = k_sqrt(k_mse(y_train_orig[:,0],
                          y_train_pred[:,0])
                   ).eval(session=K.get_session())
print('Train Score: {0:.2f} RMSE'.format(trainScore))

testScore = k_sqrt(k_mse(y_test_orig[:,0],
                         y_test_pred[:,0])
                  ).eval(session=K.get_session())
print('Test Score: {0:.2f} RMSE'.format(testScore))

我们得到以下结果:

代码语言:javascript复制
Train Score: 23.27 RMSE
Test Score: 54.13 RMSE

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-21Swd4gs-1681566456879)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/93bcf886-fc39-4e77-8fa0-d72af7ac310b.png)]

我们可以看到,这不像我们在 TensorFlow 部分得到的那样完美;但是,这种差异是因为超参数值。我们留给您尝试不同的超参数值来调整此 Keras 模型以获得更好的结果。

Keras 中的 LSTM

创建 LSTM 模型只需添加 LSTM 层而不是SimpleRNN层,如下所示:

代码语言:javascript复制
model.add(LSTM(units=4, input_shape=(X_train.shape[1], X_train.shape[2])))

模型结构如下所示:

代码语言:javascript复制
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
lstm_1 (LSTM)                (None, 4)                 96        
_________________________________________________________________
dense_1 (Dense)              (None, 1)                 5         
=================================================================
Total params: 101
Trainable params: 101
Non-trainable params: 0
_________________________________________________________________

笔记本ch-07b_RNN_TimeSeries_Keras中提供了 LSTM 模型的完整代码。

由于 LSTM 模型具有更多需要训练的参数,对于相同数量的迭代(20 个周期),我们得到更高的误差分数。我们留给您探索周期和其他超参数的各种值,以获得更好的结果:

代码语言:javascript复制
Train Score: 32.21 RMSE
Test Score: 84.68 RMSE

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7ybVvBfm-1681566456879)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/b64ddef8-5b68-44b8-842e-779177ef1557.png)]

Keras 中的 GRU

使用 TensorFlow 和 Keras 的一个优点是它们可以轻松创建模型。与 LSTM 一样,创建 GRU 模型只需添加 GRU 层而不是 LSTM 或SimpleRNN层,如下所示:

代码语言:javascript复制
model.add(GRU(units=4, input_shape=(X_train.shape[1], X_train.shape[2])))

模型结构如下:

代码语言:javascript复制
Layer (type)                 Output Shape              Param #   
=================================================================
gru_1 (GRU)                  (None, 4)                 72        
_________________________________________________________________
dense_1 (Dense)              (None, 1)                 5         
=================================================================
Total params: 77
Trainable params: 77
Non-trainable params: 0

笔记本ch-07b_RNN_TimeSeries_Keras中提供了 GRU 模型的完整代码。

正如预期的那样,GRU 模型显示出与 LSTM 几乎相同的表现,我们让您尝试使用不同的超参数值来优化此模型:

代码语言:javascript复制
Train Score: 31.49 RMSE
Test Score: 92.75 RMSE

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VuVFWQeq-1681566456880)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/f5e8871e-1511-408f-ab58-0b3e3b79644d.png)]

总结

时间序列数据是基于序列的数据,因此 RNN 模型是从时间序列数据中学习的相关架构。在本章中,您学习了如何使用 TensorFlow(一个低级库)和 Keras(一个高级库)创建不同类型的 RNN 模型。我们只介绍了SimpleRNN,LSTM 和 GRU,但您应该探索可以使用 TensorFlow 和 Keras 创建的许多其他 RNN 变体。

在下一章中,我们将使用当前章节和前几章中构建的基础为各种自然语言处理NLP)任务创建文本数据的 RNN 模型。

八、TensorFlow 和 Keras 中的用于文本数据的 RNN

文本数据可以被视为一系列字符,单词,句子或段落。 循环神经网络RNN)已被证明是非常有用的序列神经网络结构。为了将神经网络模型应用于自然语言处理NLP)任务,文本被视为单词序列。事实证明,这对于 NLP 任务非常成功,例如:

  • 问题回答
  • 会话智能体或聊天机器人
  • 文本分类
  • 情感分析
  • 图像标题或描述文本生成
  • 命名实体识别
  • 语音识别和标记

NLP 与 TensorFlow 深度学习技术是一个广阔的领域,很难在一章中捕捉到。因此,我们尝试使用 Tensorflow 和 Keras 为您提供该领域中最流行和最重要的示例。一旦掌握了本章的内容,不要忘记探索和试验 NLP 的其他领域。

在本章中,我们将了解以下主题:

  • 词向量表示
  • 为 word2vec 模型准备数据
  • TensorFlow 和 Keras 中的 SkipGram 模型
  • 使用 t-SNE 可视化单词嵌入
  • TensorFlow 和 Keras 中使用 LSTM 模型的文本生成示例

词向量表示

为了从文本数据中学习神经网络模型的参数,首先,我们必须将文本或自然语言数据转换为可由神经网络摄取的格式。神经网络通常以数字向量的形式摄取文本。将原始文本数据转换为数字向量的算法称为字嵌入算法。

一种流行的字嵌入方法是我们在 MNIST 图像分类中看到的单热编码。假设我们的文本数据集由 60,000 个字典单词组成。然后,每个单词可以由具有 60,000 个元素的单热编码向量表示,其中除了表示具有值 1 的该单词的一个元素之外,所有其他元素具有零值。

然而,单热编码方法有其缺点。首先,对于具有大量单词的词汇,单热词向量的维数变得非常大。其次,人们无法找到与单热编码向量的单词相似性。例如,假设猫和小猫的向量分别为[1 0 0 0 0 0][0 0 0 0 0 1]。这些向量没有相似之处。

还有其他基于语料库的方法,用于将基于文本的语料库转换为数字向量,例如:

  • 单词频率 - 反向文档频率(TF-IDF)
  • 潜在语义分析(LSA)
  • 主题建模

最近,用数值向量表示单词的焦点已转移到基于分布假设的方法,这意味着具有相似语义含义的单词倾向于出现在类似的上下文中。

两种最广泛使用的方法称为 word2vec 和 GloVe。我们将在本章中使用 word2vec 进行练习。正如我们在前一段中所了解到的,单热编码给出了语料库字典中单词总数大小的维数。使用 word2vec 创建的单词向量的维度要低得多。

word2vec 系列模型使用两种架构构建:

  • CBOW:训练模型以学习给定上下文词的中心词的概率分布。因此,给定一组上下文单词,模型以您在高中语言课程中所做的填空方式预测中心单词。 CBOW 架构最适用于具有较小词汇表的数据集。
  • SkipGram:训练模型以学习给定中心词的上下文词的概率分布。因此,给定一个中心词,模型以您在高中语言课程中完成的句子方式预测语境词。

例如,让我们考虑一下这句话:

代码语言:javascript复制
Vets2data.org is a non-profit for educating the US Military Veterans Community on Artificial Intelligence and Data Science.

在 CBOW 架构中,给出单词MilitaryCommunity,模型学习单词Veterans的概率,并在 SkipGram 架构中,给出单词Veterans,模型学习单词MilitaryCommunity的概率。

word2vec 模型以无监督的方式从文本语料库中学习单词向量。文本语料库分为成对的上下文单词和目标单词。虽然这些对是真正的对,但是伪对是用随机配对的上下文词和上下文词生成的,因此在数据中产生噪声。训练分类器以学习用于区分真对和假对的参数。该分类器的参数成为 word2vec 模型或单词向量。

关于 word2vec 理论背后的数学和理论的更多信息可以从以下论文中学到:

代码语言:javascript复制
Mikolov, T., I. Sutskever, K. Chen, G. Corrado, and J. Dean. Distributed Representations of Words and Phrases and Their Compositionality. _Advances in Neural Information Processing Systems_, 2013, pp. 3111–3119.

Mikolov, T., K. Chen, G. Corrado, and J. Dean. Efficient Estimation of Word Representations in Vector Space. _arXiv_, 2013, pp. 1–12.

Rong, X. word2vec Parameter Learning Explained. _arXiv:1411.2738_, 2014, pp. 1–19.

Baroni, M., G. Dinu, and G. Kruszewski. Don’t Count, Predict! A Systematic Comparison of Context-Counting vs. Context-Predicting Semantic Vectors. 2014.

您应该使用 GloVe 和 word2vec 练习并应用适用于您的文本数据的方法。

有关 GLoVe 算法的更多信息可以从以下文章中学习:

代码语言:javascript复制
Pennington, J., R. Socher, and C. Manning. GloVe: Global Vectors for Word Representation. 2014.

让我们通过在 TensorFlow 和 Keras 中创建单词向量来理解 word2vec 模型。

您可以按照 Jupyter 笔记本中的下几节的代码ch-08a_Embeddings_in_TensorFlow_and_Keras

用于 word2vec 模型的数据准备

我们将使用流行的 PTB 和 text8 数据集进行演示。

PennTreebankPTB)数据集是在 UPenn 进行的 Penn Treebank 项目的副产品。 PTB 项目团队在华尔街日报三年的故事中提取了大约一百万字,并以 Treebank II 风格对其进行了标注。 PTB 数据集有两种形式: 基本示例,大小约为 35 MB, 高级示例,大小约为 235 MB。我们将使用由 929K 字组成的简单数据集进行训练,73K 字用于验证,82K 字用于测试。建议您浏览高级数据集。有关 PTB 数据集的更多详细信息,请访问此链接 。

可以从此链接下载 PTB 数据集。

text8 数据集是一个较短的清理版本的大型维基百科数据转储,大小约为 1GB。有关如何创建 text8 数据集的过程,请参见此链接。

text8 数据集可以从此链接下载。

使用我们的自定义库datasetslib中的load_data代码加载数据集:

load_data()函数执行以下操作:

  1. 如果数据集的 URL 在本地不可用,它将从数据集的 URL 下载数据存档。
  2. 由于PTB数据有三个文件,它首先从训练文件中读取文本,而对于text8,它从归档中读取第一个文件。
  3. 它将训练文件中的单词转换为词汇表,并为每个词汇单词分配一个唯一的数字,单词 ID,将其存储在集合word2id中,并准备反向词典,这样我们就可以从 ID 中查找单词,并将其存储在集合id2word中。
  4. 它使用集合word2id将文本文件转换为 ID 序列。
  5. 因此,在load_data的末尾,我们在训练数据集中有一系列数字,在集合id2word中有一个 ID 到字的映射。

让我们看一下从 text8 和 PTB 数据集加载的数据:

加载和准备 PTB 数据集

首先导入模块并加载数据如下::

代码语言:javascript复制
from datasetslib.ptb import PTBSimple
ptb = PTBSimple()
# downloads data, converts words to ids, converts files to a list of ids
ptb.load_data() 
print('Train :',ptb.part['train'][0:5])
print('Test: ',ptb.part['test'][0:5])
print('Valid: ',ptb.part['valid'][0:5])
print('Vocabulary Length = ',ptb.vocab_len)

每个数据集的前五个元素以及词汇长度打印如下:

代码语言:javascript复制
Train : [9970, 9971, 9972, 9974, 9975]
Test:  [102, 14, 24, 32, 752]
Valid:  [1132, 93, 358, 5, 329]
Vocabulary Length =  10000

我们将上下文窗口设置为两个单词并获得 CBOW 对:

代码语言:javascript复制
ptb.skip_window=2
ptb.reset_index_in_epoch()
# in CBOW input is the context word and output is the target word
y_batch, x_batch = ptb.next_batch_cbow() 

print('The CBOW pairs : context,target')
for i in range(5 * ptb.skip_window):
    print('(', [ptb.id2word[x_i] for x_i in x_batch[i]],
          ',', y_batch[i], ptb.id2word[y_batch[i]], ')')

输出是:

代码语言:javascript复制
The CBOW pairs : context,target
( ['aer', 'banknote', 'calloway', 'centrust'] , 9972 berlitz )
( ['banknote', 'berlitz', 'centrust', 'cluett'] , 9974 calloway )
( ['berlitz', 'calloway', 'cluett', 'fromstein'] , 9975 centrust )
( ['calloway', 'centrust', 'fromstein', 'gitano'] , 9976 cluett )
( ['centrust', 'cluett', 'gitano', 'guterman'] , 9980 fromstein )
( ['cluett', 'fromstein', 'guterman', 'hydro-quebec'] , 9981 gitano )
( ['fromstein', 'gitano', 'hydro-quebec', 'ipo'] , 9982 guterman )
( ['gitano', 'guterman', 'ipo', 'kia'] , 9983 hydro-quebec )
( ['guterman', 'hydro-quebec', 'kia', 'memotec'] , 9984 ipo )
( ['hydro-quebec', 'ipo', 'memotec', 'mlx'] , 9986 kia )

现在让我们看看 SkipGram 对:

代码语言:javascript复制
ptb.skip_window=2
ptb.reset_index_in_epoch()
# in SkipGram input is the target word and output is the context word
x_batch, y_batch = ptb.next_batch()

print('The SkipGram pairs : target,context')
for i in range(5 * ptb.skip_window):
    print('(',x_batch[i], ptb.id2word[x_batch[i]],
        ',', y_batch[i], ptb.id2word[y_batch[i]],')')

输出为:

代码语言:javascript复制
The SkipGram pairs : target,context
( 9972 berlitz , 9970 aer )
( 9972 berlitz , 9971 banknote )
( 9972 berlitz , 9974 calloway )
( 9972 berlitz , 9975 centrust )
( 9974 calloway , 9971 banknote )
( 9974 calloway , 9972 berlitz )
( 9974 calloway , 9975 centrust )
( 9974 calloway , 9976 cluett )
( 9975 centrust , 9972 berlitz )
( 9975 centrust , 9974 calloway )

加载和准备 text8 数据集

现在我们使用 text8 数据集执行相同的加载和预处理步骤:

代码语言:javascript复制
from datasetslib.text8 import Text8
text8 = Text8()
text8.load_data() 
# downloads data, converts words to ids, converts files to a list of ids
print('Train:', text8.part['train'][0:5])
print('Vocabulary Length = ',text8.vocab_len)

我们发现词汇长度大约是 254,000 字:

代码语言:javascript复制
Train: [5233, 3083, 11, 5, 194]
Vocabulary Length =  253854

一些教程通过查找最常用的单词或将词汇量大小截断为 10,000 个单词来操纵此数据。 但是,我们使用了 text8 数据集的第一个文件中的完整数据集和完整词汇表。

准备 CBOW 对:

代码语言:javascript复制
text8.skip_window=2
text8.reset_index_in_epoch()
# in CBOW input is the context word and output is the target word
y_batch, x_batch = text8.next_batch_cbow() 

print('The CBOW pairs : context,target')
for i in range(5 * text8.skip_window):
    print('(', [text8.id2word[x_i] for x_i in x_batch[i]],
          ',', y_batch[i], text8.id2word[y_batch[i]], ')')

输出是:

代码语言:javascript复制
The CBOW pairs : context,target
( ['anarchism', 'originated', 'a', 'term'] , 11 as )
( ['originated', 'as', 'term', 'of'] , 5 a )
( ['as', 'a', 'of', 'abuse'] , 194 term )
( ['a', 'term', 'abuse', 'first'] , 1 of )
( ['term', 'of', 'first', 'used'] , 3133 abuse )
( ['of', 'abuse', 'used', 'against'] , 45 first )
( ['abuse', 'first', 'against', 'early'] , 58 used )
( ['first', 'used', 'early', 'working'] , 155 against )
( ['used', 'against', 'working', 'class'] , 127 early )
( ['against', 'early', 'class', 'radicals'] , 741 working )

准备 SkipGram 对:

代码语言:javascript复制
text8.skip_window=2
text8.reset_index_in_epoch()
# in SkipGram input is the target word and output is the context word
x_batch, y_batch = text8.next_batch()

print('The SkipGram pairs : target,context')
for i in range(5 * text8.skip_window):
    print('(',x_batch[i], text8.id2word[x_batch[i]],
        ',', y_batch[i], text8.id2word[y_batch[i]],')')

输出为:

代码语言:javascript复制
The SkipGram pairs : target,context
( 11 as , 5233 anarchism )
( 11 as , 3083 originated )
( 11 as , 5 a )
( 11 as , 194 term )
( 5 a , 3083 originated )
( 5 a , 11 as )
( 5 a , 194 term )
( 5 a , 1 of )
( 194 term , 11 as )
( 194 term , 5 a )

准备小验证集

为了演示该示例,我们创建了一个包含 8 个单词的小型验证集,每个单词是从单词中随机选择的,其中单词 ID 在 0 到10 x 8之间。

代码语言:javascript复制
valid_size = 8
x_valid = np.random.choice(valid_size * 10, valid_size, replace=False)
print(x_valid)

作为示例,我们将以下内容作为验证集:

代码语言:javascript复制
valid:  [64 58 59 4 69 53 31 77]

我们将使用此验证集通过打印五个最接近的单词来演示嵌入一词的结果。

TensorFlow 中的 SkipGram 模型

现在我们已经准备好了训练和验证数据,让我们在 TensorFlow 中创建一个 SkipGram 模型。

我们首先定义超参数:

代码语言:javascript复制
batch_size = 128
embedding_size = 128
skip_window = 2
n_negative_samples = 64
ptb.skip_window=2
learning_rate = 1.0
  • batch_size是要在单个批次中输入算法的目标和上下文单词对的数量
  • embedding_size是每个单词的单词向量或嵌入的维度
  • ptb.skip_window是在两个方向上的目标词的上下文中要考虑的词的数量
  • n_negative_samples是由 NCE 损失函数生成的负样本数,本章将进一步说明

在一些教程中,包括 TensorFlow 文档中的一个教程,还使用了一个参数num_skips。在这样的教程中,作者选择了num_skips(目标,上下文)对。例如,如果skip_window是 2,那么对的总数将是 4,如果num_skips被设置为 2,则只有两对将被随机选择用于训练。但是,我们考虑了所有的对以便保持训练练习简单。

定义训练数据的输入和输出占位符以及验证数据的张量:

代码语言:javascript复制
inputs = tf.placeholder(dtype=tf.int32, shape=[batch_size])
outputs = tf.placeholder(dtype=tf.int32, shape=[batch_size,1])
inputs_valid = tf.constant(x_valid, dtype=tf.int32)

定义一个嵌入矩阵,其行数等于词汇长度,列等于嵌入维度。该矩阵中的每一行将表示词汇表中一个单词的单词向量。使用在 -1.0 到 1.0 之间均匀采样的值填充此嵌入矩阵。

代码语言:javascript复制
# define embeddings matrix with vocab_len rows and embedding_size columns
# each row represents vectore representation or embedding of a word
# in the vocbulary

embed_dist = tf.random_uniform(shape=[ptb.vocab_len, embedding_size],
                               minval=-1.0,maxval=1.0)
embed_matrix = tf.Variable(embed_dist,name='embed_matrix')

使用此矩阵,定义使用tf.nn.embedding_lookup()实现的嵌入查找表。tf.nn.embedding_lookup()有两个参数:嵌入矩阵和输入占位符。 查找函数返回inputs占位符中单词的单词向量。

代码语言:javascript复制
# define the embedding lookup table
# provides the embeddings of the word ids in the input tensor
embed_ltable = tf.nn.embedding_lookup(embed_matrix, inputs)

embed_ltable也可以解释为输入层顶部的嵌入层。接下来,将嵌入层的输出馈送到 softmax 或噪声对比估计(NCE)层。 NCE 基于一个非常简单的想法,即训练基于逻辑回归的二分类器,以便从真实和嘈杂数据的混合中学习参数。

TensorFlow 文档进一步详细描述了 NCE。

总之,基于 softmax 损失的模型在计算上是昂贵的,因为在整个词汇表中计算概率分布并对其进行归一化。基于 NCE 损耗的模型将其减少为二分类问题,即从噪声样本中识别真实样本。

NCE 的基本数学细节可以在以下 NIPS 论文中找到:使用噪声对比估计高效学习词嵌入,作者 Andriy Mnih 和 Koray Kavukcuoglu。该论文可从此链接获得。

tf.nn.nce_loss()函数在求值计算损耗时自动生成负样本:参数num_sampled设置为等于负样本数(n_negative_samples)。此参数指定要绘制的负样本数。

代码语言:javascript复制
# define noise-contrastive estimation (NCE) loss layer
nce_dist = tf.truncated_normal(shape=[ptb.vocab_len, embedding_size],
                               stddev=1.0 /
                               tf.sqrt(embedding_size * 1.0)
                               )
nce_w = tf.Variable(nce_dist)
nce_b = tf.Variable(tf.zeros(shape=[ptb.vocab_len]))

loss = tf.reduce_mean(tf.nn.nce_loss(weights=nce_w,
                                     biases=nce_b,
                                     inputs=embed_ltable,
                                     labels=outputs,
                                     num_sampled=n_negative_samples,
                                     num_classes=ptb.vocab_len
                                     )
                      )

接下来,计算验证集中的样本与嵌入矩阵之间的余弦相似度:

  1. 为了计算相似性得分,首先,计算嵌入矩阵中每个单词向量的 L2 范数。
代码语言:javascript复制
# Compute the cosine similarity between validation set samples
# and all embeddings.
norm = tf.sqrt(tf.reduce_sum(tf.square(embed_matrix), 1, 
                             keep_dims=True))
normalized_embeddings = embed_matrix / norm
  1. 查找验证集中的样本的嵌入或单词向量:
代码语言:javascript复制
embed_valid = tf.nn.embedding_lookup(normalized_embeddings, 
                                     inputs_valid)
  1. 通过将验证集的嵌入与嵌入矩阵相乘来计算相似性得分。
代码语言:javascript复制
similarity = tf.matmul(
    embed_valid, normalized_embeddings, transpose_b=True)

这给出了具有(valid_sizevocab_len)形状的张量。张量中的每一行指的是验证词和词汇单词之间的相似性得分。

接下来,定义 SGD 优化器,学习率为 0.9,历时 50 个周期。

代码语言:javascript复制
n_epochs = 10
learning_rate = 0.9
n_batches = ptb.n_batches(batch_size)
optimizer = tf.train.GradientDescentOptimizer(learning_rate)
            .minimize(loss)

对于每个周期:

  1. 逐批在整个数据集上运行优化器。
代码语言:javascript复制
ptb.reset_index_in_epoch()
for step in range(n_batches):
    x_batch, y_batch = ptb.next_batch()
    y_batch = dsu.to2d(y_batch,unit_axis=1)
    feed_dict = {inputs: x_batch, outputs: y_batch}
    _, batch_loss = tfs.run([optimizer, loss], feed_dict=feed_dict)
    epoch_loss  = batch_loss
  1. 计算并打印周期的平均损失。
代码语言:javascript复制
 epoch_loss = epoch_loss / n_batches 
 print('n','Average loss after epoch ', epoch, ': ', epoch_loss)
  1. 在周期结束时,计算相似性得分。
代码语言:javascript复制
similarity_scores = tfs.run(similarity)
  1. 对于验证集中的每个单词,打印具有最高相似性得分的五个单词。
代码语言:javascript复制
top_k = 5 
for i in range(valid_size):
    similar_words = (-similarity_scores[i,:])
                    .argsort()[1:top_k   1]
    similar_str = 'Similar to {0:}:'
                    .format(ptb.id2word[x_valid[i]])
    for k in range(top_k):
        similar_str = '{0:} {1:},'.format(similar_str, 
                        ptb.id2word[similar_words[k]])
    print(similar_str)

最后,在完成所有周期之后,计算可在学习过程中进一步利用的嵌入向量:

代码语言:javascript复制
final_embeddings = tfs.run(normalized_embeddings)

完整的训练代码如下:

代码语言:javascript复制
n_epochs = 10
learning_rate = 0.9
n_batches = ptb.n_batches_wv()
optimizer = tf.train.GradientDescentOptimizer(learning_rate).minimize(loss)

with tf.Session() as tfs:
    tf.global_variables_initializer().run()
    for epoch in range(n_epochs):
        epoch_loss = 0
        ptb.reset_index()
        for step in range(n_batches):
            x_batch, y_batch = ptb.next_batch_sg()
            y_batch = nputil.to2d(y_batch, unit_axis=1)
            feed_dict = {inputs: x_batch, outputs: y_batch}
            _, batch_loss = tfs.run([optimizer, loss], feed_dict=feed_dict)
            epoch_loss  = batch_loss
        epoch_loss = epoch_loss / n_batches
        print('nAverage loss after epoch ', epoch, ': ', epoch_loss)

        # print closest words to validation set at end of every epoch
        similarity_scores = tfs.run(similarity)
        top_k = 5
        for i in range(valid_size):
            similar_words = (-similarity_scores[i, :]
                             ).argsort()[1:top_k   1]
            similar_str = 'Similar to {0:}:'.format(
                ptb.id2word[x_valid[i]])
            for k in range(top_k):
                similar_str = '{0:} {1:},'.format(
                    similar_str, ptb.id2word[similar_words[k]])
            print(similar_str)
    final_embeddings = tfs.run(normalized_embeddings)

这是我们分别在第 1 和第 10 周期之后得到的输出:

代码语言:javascript复制
Average loss after epoch  0 :  115.644006802
Similar to we: types, downturn, internal, by, introduce,
Similar to been: said, funds, mcgraw-hill, street, have,
Similar to also: will, she, next, computer, 's,
Similar to of: was, and, milk, dollars, $,
Similar to last: be, october, acknowledging, requested, computer,
Similar to u.s.: plant, increase, many, down, recent,
Similar to an: commerce, you, some, american, a,
Similar to trading: increased, describes, state, companies, in,

Average loss after epoch  9 :  5.56538496033
Similar to we: types, downturn, introduce, internal, claims,
Similar to been: exxon, said, problem, mcgraw-hill, street,
Similar to also: will, she, ssangyong, audit, screens,
Similar to of: seasonal, dollars, motor, none, deaths,
Similar to last: acknowledging, allow, incorporated, joint, requested,
Similar to u.s.: undersecretary, typically, maxwell, recent, increase,
Similar to an: banking, officials, imbalances, americans, manager,
Similar to trading: describes, increased, owners, committee, else,

最后,我们运行 5000 个周期的模型并获得以下结果:

代码语言:javascript复制
Average loss after epoch  4999 :  2.74216903135
Similar to we: matter, noted, here, classified, orders,
Similar to been: good, precedent, medium-sized, gradual, useful,
Similar to also: introduce, england, index, able, then,
Similar to of: indicator, cleveland, theory, the, load,
Similar to last: dec., office, chrysler, march, receiving,
Similar to u.s.: label, fannie, pressures, squeezed, reflection,
Similar to an: knowing, outlawed, milestones, doubled, base,
Similar to trading: associates, downturn, money, portfolios, go,

尝试进一步运行,最多 50,000 个周期,以获得更好的结果。

同样,我们在 50 个周期之后使用 text8 模型得到以下结果:

代码语言:javascript复制
Average loss after epoch  49 :  5.74381046423
Similar to four: five, three, six, seven, eight,
Similar to all: many, both, some, various, these,
Similar to between: with, through, thus, among, within,
Similar to a: another, the, any, each, tpvgames,
Similar to that: which, however, although, but, when,
Similar to zero: five, three, six, eight, four,
Similar to is: was, are, has, being, busan,
Similar to no: any, only, the, another, trinomial,

t-SNE 和单词嵌入可视化

让我们可视化我们在上一节中生成的单词嵌入。 t-SNE 是在二维空间中显示高维数据的最流行的方法。我们将使用 scikit-learn 库中的方法,并重用 TensorFlow 文档中给出的代码,来绘制我们刚学过的词嵌入的图形。

TensorFlow 文档中的原始代码可从此链接获得。

以下是我们如何实现该程序:

  1. 创建tsne模型:
代码语言:javascript复制
tsne = TSNE(perplexity=30, n_components=2,
            init='pca', n_iter=5000, method='exact')
  1. 将要显示的嵌入数限制为 500,否则,图形变得非常难以理解:
代码语言:javascript复制
n_embeddings = 500
  1. 通过调用tsne模型上的fit_transform()方法并将final_embeddings的第一个n_embeddings作为输入来创建低维表示。
代码语言:javascript复制
low_dim_embeddings = tsne.fit_transform(
    final_embeddings[:n_embeddings, :])
  1. 找到我们为图表选择的单词向量的文本表示:
代码语言:javascript复制
labels = [ptb.id2word[i] for i in range(n_embeddings)]
  1. 最后,绘制嵌入图:
代码语言:javascript复制
plot_with_labels(low_dim_embeddings, labels)

我们得到以下绘图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yn5sZmOZ-1681566456880)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/16116250-d5d1-4e33-943a-f9c465a7edbf.png)]t-SNE visualization of embeddings for PTB data set

同样,从 text8 模型中,我们得到以下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Isd1Q9Zw-1681566456880)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/652fb621-e9a3-48bb-8270-ee1073f69df3.png)]t-SNE visualization of embeddings for text8 data set

Keras 中的 SkipGram 模型

使用 Keras 的嵌入模型的流程与 TensorFlow 保持一致。

  • 在 Keras 函数式或顺序模型中创建网络架构
  • 将目标和上下文单词的真实性对提供给网络
  • 查找目标和上下文单词的单词向量
  • 执行单词向量的点积来获得相似性得分
  • 将相似性得分通过 sigmoid 层以将输出作为真或假对

现在让我们使用 Keras 函数式 API 实现这些步骤:

  1. 导入所需的库:
代码语言:javascript复制
from keras.models import Model
from keras.layers.embeddings import Embedding
from keras.preprocessing import sequence
from keras.preprocessing.sequence import skipgrams
from keras.layers import Input, Dense, Reshape, Dot, merge
import keras

重置图,以便清除以前在 Jupyter 笔记本中运行的任何后续效果:

代码语言:javascript复制
# reset the jupyter buffers
tf.reset_default_graph()
keras.backend.clear_session()
  1. 创建一个验证集,我们将用它来打印我们的模型在训练结束时找到的相似单词:
代码语言:javascript复制
valid_size = 8
x_valid = np.random.choice(valid_size * 10, valid_size, replace=False)
print('valid: ',x_valid)
  1. 定义所需的超参数:
代码语言:javascript复制
batch_size = 1024
embedding_size = 512 
n_negative_samples = 64
ptb.skip_window=2
  1. 使用keras.preprocessing.sequence中的make_sampling_table()函数创建一个大小等于词汇长度的样本表。接下来,使用keras.preprocessing.sequence中的函数skipgrams()生成上下文和目标词对以及表示它们是真对还是假对的标签。
代码语言:javascript复制
sample_table = sequence.make_sampling_table(ptb.vocab_len)
pairs, labels= sequence.skipgrams(ptb.part['train'],
        ptb.vocab_len,window_size=ptb.skip_window,
        sampling_table=sample_table)
  1. 让我们打印一些使用以下代码生成的伪造和真实对:
代码语言:javascript复制
print('The SkipGram pairs : target,context')
for i in range(5 * ptb.skip_window):
    print(['{} {}'.format(id,ptb.id2word[id])  
        for id in pairs[i]],':',labels[i])

对配对如下:

代码语言:javascript复制
The SkipGram pairs : target,context
['547 trying', '5 to'] : 1
['4845 bargain', '2 <eos>'] : 1
['1705 election', '198 during'] : 1
['4704 flows', '8117 gun'] : 0
['13 is', '37 company'] : 1
['625 above', '132 three'] : 1
['5768 pessimistic', '1934 immediate'] : 0
['637 china', '2 <eos>'] : 1
['258 five', '1345 pence'] : 1
['1956 chrysler', '8928 exercises'] : 0
  1. 从上面生成的对中拆分目标和上下文单词,以便将它们输入模型。将目标和上下文单词转换为二维数组。
代码语言:javascript复制
x,y=zip(*pairs)
x=np.array(x,dtype=np.int32)
x=dsu.to2d(x,unit_axis=1)
y=np.array(y,dtype=np.int32)
y=dsu.to2d(y,unit_axis=1)
labels=np.array(labels,dtype=np.int32)
labels=dsu.to2d(labels,unit_axis=1)
  1. 定义网络的架构。正如我们所讨论的,必须将目标和上下文单词输入网络,并且需要从嵌入层中查找它们的向量。因此,首先我们分别为目标和上下文单词定义输入,嵌入和重塑层:
代码语言:javascript复制
# build the target word model
target_in = Input(shape=(1,),name='target_in')
target = Embedding(ptb.vocab_len,embedding_size,input_length=1,
            name='target_em')(target_in)
target = Reshape((embedding_size,1),name='target_re')(target)

# build the context word model
context_in = Input((1,),name='context_in')
context = Embedding(ptb.vocab_len,embedding_size,input_length=1,
            name='context_em')(context_in)
context = Reshape((embedding_size,1),name='context_re')(context)
  1. 接下来,构建这两个模型的点积,将其输入 sigmoid 层以生成输出标签:
代码语言:javascript复制
# merge the models with the dot product to check for 
# similarity and add sigmoid layer
output = Dot(axes=1,name='output_dot')([target,context])
output = Reshape((1,),name='output_re')(output)
output = Dense(1, activation='sigmoid',name='output_sig')(output)
  1. 从我们刚刚创建的输入和输出模型构建函数式模型:
代码语言:javascript复制
# create the functional model for finding word vectors
model = Model(inputs=[target_in,context_in],outputs=output)
model.compile(loss='binary_crossentropy', optimizer='adam')
  1. 此外,在给定输入目标词的情况下,构建一个模型,用于预测与所有单词的相似性:
代码语言:javascript复制
# merge the models and create model to check for cosine similarity
similarity = Dot(axes=0,normalize=True,
            name='sim_dot')([target,context])
similarity_model = Model(inputs=[target_in,context_in],
            outputs=similarity)

让我们打印模型摘要:

代码语言:javascript复制
__________________________________________________________________________
Layer (type)               Output Shape          Param #     Connected to                     
==========================================================================
target_in (InputLayer)     (None, 1)          0                                            
__________________________________________________________________________
context_in (InputLayer)    (None, 1)          0                                            
__________________________________________________________________________
target_em (Embedding)      (None, 1, 512)     5120000     target_in[0][0]                  
__________________________________________________________________________
context_em (Embedding)     (None, 1, 512)     5120000     context_in[0][0]                 
__________________________________________________________________________
target_re (Reshape)        (None, 512, 1)     0           target_em[0][0]                  
__________________________________________________________________________
context_re (Reshape)       (None, 512, 1)     0           context_em[0][0]                 
__________________________________________________________________________
output_dot (Dot)           (None, 1, 1)       0           target_re[0][0]                  
                                                          context_re[0][0]                 
__________________________________________________________________________
output_re (Reshape)        (None, 1)          0           output_dot[0][0]                 
__________________________________________________________________________
output_sig (Dense)         (None, 1)          2           output_re[0][0]                  
==========================================================================
Total params: 10,240,002
Trainable params: 10,240,002
Non-trainable params: 0
__________________________________________________________________________
  1. 接下来,训练模型。我们只训练了 5 个周期,但你应该尝试更多的周期,至少 1000 或 10,000 个周期。

请记住,这将需要几个小时,因为这不是最优化的代码。 欢迎您使用本书和其他来源的提示和技巧进一步优化代码。

代码语言:javascript复制
n_epochs = 5
batch_size = 1024
model.fit([x,y],labels,batch_size=batch_size, epochs=n_epochs)

让我们根据这个模型发现的单词向量打印单词的相似度:

代码语言:javascript复制
# print closest words to validation set at end of training
top_k = 5
y_val = np.arange(ptb.vocab_len, dtype=np.int32)
y_val = dsu.to2d(y_val,unit_axis=1)
for i in range(valid_size):
    x_val = np.full(shape=(ptb.vocab_len,1),fill_value=x_valid[i], 
            dtype=np.int32)
    similarity_scores = similarity_model.predict([x_val,y_val])
    similarity_scores=similarity_scores.flatten()
    similar_words = (-similarity_scores).argsort()[1:top_k   1]
    similar_str = 'Similar to {0:}:'.format(ptb.id2word[x_valid[i]])
    for k in range(top_k):
        similar_str = '{0:} {1:},'.format(similar_str, 
                        ptb.id2word[similar_words[k]])
    print(similar_str)

我们得到以下输出:

代码语言:javascript复制
Similar to we: rake, kia, sim, ssangyong, memotec,
Similar to been: nahb, sim, rake, punts, rubens,
Similar to also: photography, snack-food, rubens, nahb, ssangyong,
Similar to of: isi, rake, memotec, kia, mlx,
Similar to last: rubens, punts, memotec, sim, photography,
Similar to u.s.: mlx, memotec, punts, rubens, kia,
Similar to an: memotec, isi, ssangyong, rake, sim,
Similar to trading: rake, rubens, swapo, mlx, nahb,

到目前为止,我们已经看到了如何使用 TensorFlow 及其高级库 Keras 创建单词向量或嵌入。现在让我们看看如何使用 TensorFlow 和 Keras 来学习模型并将模型应用于一些与 NLP 相关的任务的预测。

TensorFlow 和 Keras 中的 RNN 模型和文本生成

文本生成是 NLP 中 RNN 模型的主要应用之一。针对文本序列训练 RNN 模型,然后通过提供种子文本作为输入来生成文本序列。让我们试试 text8 数据集。

让我们加载 text8 数据集并打印前 100 个单词:

代码语言:javascript复制
from datasetslib.text8 import Text8
text8 = Text8()
# downloads data, converts words to ids, converts files to a list of ids
text8.load_data() 
print(' '.join([text8.id2word[x_i] for x_i in text8.part['train'][0:100]]))

我们得到以下输出:

代码语言:javascript复制
anarchism originated as a term of abuse first used against early working class radicals including the diggers of the english revolution and the sans culottes of the french revolution whilst the term is still used in a pejorative way to describe any act that used violent means to destroy the organization of society it has also been taken up as a positive label by self defined anarchists the word anarchism is derived from the greek without archons ruler chief king anarchism as a political philosophy is the belief that rulers are unnecessary and should be abolished although there are differing

在我们的笔记本示例中,我们将数据加载剪切为 5,000 字的文本,因为较大的文本需要高级技术,例如分布式或批量,我们希望保持示例简单。

代码语言:javascript复制
from datasetslib.text8 import Text8
text8 = Text8()
text8.load_data(clip_at=5000) 
print('Train:', text8.part['train'][0:5])
print('Vocabulary Length = ',text8.vocab_len)

我们看到词汇量现在减少到 1,457 个单词。

代码语言:javascript复制
Train: [  8 497   7   5 116]
Vocabulary Length =  1457

在我们的示例中,我们构造了一个非常简单的单层 LSTM。为了训练模型,我们使用 5 个单词作为输入来学习第六个单词的参数。输入层是 5 个字,隐藏层是具有 128 个单元的 LSTM 单元,最后一层是完全连接的层,其输出等于词汇量大小。由于我们正在演示这个例子,我们没有使用单词向量,而是使用非常简单的单热编码输出向量。

一旦模型被训练,我们用 2 个不同的字符串作为生成更多字符的种子来测试它:

  • random5:随机选择 5 个单词生成的字符串。
  • first5:从文本的前 5 个单词生成的字符串。
代码语言:javascript复制
random5 = np.random.choice(n_x * 50, n_x, replace=False)
print('Random 5 words: ',id2string(random5))
first5 = text8.part['train'][0:n_x].copy()
print('First 5 words: ',id2string(first5))

我们看到种子串是:

代码语言:javascript复制
Random 5 words:  free bolshevik be n another
First 5 words:  anarchism originated as a term

对于您的执行,随机种子字符串可能不同。

现在让我们首先在 TensorFlow 中创建 LSTM 模型。

TensorFlow 中的 LSTM 和文本生成

您可以在 Jupyter 笔记本ch-08b_RNN_Text_TensorFlow中按照本节的代码进行操作。

我们使用以下步骤在 TensorFlow 中实现文本生成 LSTM:

  1. 让我们为xy定义参数和占位符:
代码语言:javascript复制
batch_size = 128
n_x = 5 # number of input words
n_y = 1 # number of output words
n_x_vars = 1 # in case of our text, there is only 1 variable at each timestep
n_y_vars = text8.vocab_len
state_size = 128
learning_rate = 0.001
x_p = tf.placeholder(tf.float32, [None, n_x, n_x_vars], name='x_p') 
y_p = tf.placeholder(tf.float32, [None, n_y_vars], name='y_p')

对于输入,我们使用单词的整数表示,因此n_x_vars是 1。对于输出,我们使用单热编码值,因此输出的数量等于词汇长度。

  1. 接下来,创建一个长度为n_x的张量列表:
代码语言:javascript复制
x_in = tf.unstack(x_p,axis=1,name='x_in')
  1. 接下来,从输入和单元创建 LSTM 单元和静态 RNN 网络:
代码语言:javascript复制
cell = tf.nn.rnn_cell.LSTMCell(state_size)
rnn_outputs, final_states = tf.nn.static_rnn(cell, x_in,dtype=tf.float32)
  1. 接下来,我们定义最终层的权重,偏差和公式。最后一层只需要为第六个单词选择输出,因此我们应用以下公式来仅获取最后一个输出:
代码语言:javascript复制
# output node parameters
w = tf.get_variable('w', [state_size, n_y_vars], initializer= tf.random_normal_initializer)
b = tf.get_variable('b', [n_y_vars], initializer=tf.constant_initializer(0.0))
y_out = tf.matmul(rnn_outputs[-1], w)   b
  1. 接下来,创建一个损失函数和优化器:
代码语言:javascript复制
loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(
        logits=y_out, labels=y_p))
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate)
            .minimize(loss)
  1. 创建我们可以在会话块中运行的准确率函数,以检查训练模式的准确率:
代码语言:javascript复制
n_correct_pred = tf.equal(tf.argmax(y_out,1), tf.argmax(y_p,1))
accuracy = tf.reduce_mean(tf.cast(n_correct_pred, tf.float32))
  1. 最后,我们训练模型 1000 个周期,并每 100 个周期打印结果。此外,每 100 个周期,我们从上面描述的种子字符串打印生成的文本。

LSTM 和 RNN 网络需要对大量数据集进行大量周期的训练,以获得更好的结果。 请尝试加载完整的数据集并在计算机上运行 50,000 或 80,000 个周期,并使用其他超参数来改善结果。

代码语言:javascript复制
n_epochs = 1000
learning_rate = 0.001
text8.reset_index_in_epoch()
n_batches = text8.n_batches_seq(batch_size=batch_size,n_tx=n_x,n_ty=n_y)
n_epochs_display = 100

with tf.Session() as tfs:
    tf.global_variables_initializer().run()

    for epoch in range(n_epochs):
        epoch_loss = 0
        epoch_accuracy = 0
        for step in range(n_batches):
            x_batch, y_batch = text8.next_batch_seq(batch_size=batch_size,
                                n_tx=n_x,n_ty=n_y)
            y_batch = dsu.to2d(y_batch,unit_axis=1)
            y_onehot = np.zeros(shape=[batch_size,text8.vocab_len],
                        dtype=np.float32)
            for i in range(batch_size):
                y_onehot[i,y_batch[i]]=1

            feed_dict = {x_p: x_batch.reshape(-1, n_x, n_x_vars), 
                         y_p: y_onehot}
            _, batch_accuracy, batch_loss = tfs.run([optimizer,accuracy,
                                            loss],feed_dict=feed_dict)
            epoch_loss  = batch_loss
            epoch_accuracy  = batch_accuracy

        if (epoch 1) % (n_epochs_display) == 0:
            epoch_loss = epoch_loss / n_batches
            epoch_accuracy = epoch_accuracy / n_batches
            print('nEpoch {0:}, Average loss:{1:}, Average accuracy:{2:}'.
                    format(epoch,epoch_loss,epoch_accuracy ))

            y_pred_r5 = np.empty([10])
            y_pred_f5 = np.empty([10])

            x_test_r5 = random5.copy()
            x_test_f5 = first5.copy()
            # let us generate text of 10 words after feeding 5 words
            for i in range(10):
                for x,y in zip([x_test_r5,x_test_f5],
                               [y_pred_r5,y_pred_f5]):
                    x_input = x.copy()
                    feed_dict = {x_p: x_input.reshape(-1, n_x, n_x_vars)}
                    y_pred = tfs.run(y_out, feed_dict=feed_dict)
                    y_pred_id = int(tf.argmax(y_pred, 1).eval())
                    y[i]=y_pred_id
                    x[:-1] = x[1:]
                    x[-1] = y_pred_id
            print(' Random 5 prediction:',id2string(y_pred_r5))
            print(' First 5 prediction:',id2string(y_pred_f5))

结果如下:

代码语言:javascript复制
Epoch 99, Average loss:1.3972469369570415, Average accuracy:0.8489583333333334
  Random 5 prediction: labor warren together strongly profits strongly supported supported co without
  First 5 prediction: market own self free together strongly profits strongly supported supported

Epoch 199, Average loss:0.7894854595263799, Average accuracy:0.9186197916666666
  Random 5 prediction: syndicalists spanish class movements also also anarcho anarcho anarchist was
  First 5 prediction: five civil association class movements also anarcho anarcho anarcho anarcho

Epoch 299, Average loss:1.360412875811259, Average accuracy:0.865234375
  Random 5 prediction: anarchistic beginnings influenced true tolstoy tolstoy tolstoy tolstoy tolstoy tolstoy
  First 5 prediction: early civil movement be for was two most most most

Epoch 399, Average loss:1.1692512730757396, Average accuracy:0.8645833333333334
  Random 5 prediction: including war than than revolutionary than than war than than
  First 5 prediction: left including including including other other other other other other

Epoch 499, Average loss:0.5921860883633295, Average accuracy:0.923828125
  Random 5 prediction: ever edited interested interested variety variety variety variety variety variety
  First 5 prediction: english market herbert strongly price interested variety variety variety variety

Epoch 599, Average loss:0.8356450994809469, Average accuracy:0.8958333333333334
  Random 5 prediction: management allow trabajo trabajo national national mag mag ricardo ricardo
  First 5 prediction: spain prior am working n war war war self self

Epoch 699, Average loss:0.7057955612738928, Average accuracy:0.8971354166666666
  Random 5 prediction: teachings can directive tend resist obey christianity author christianity christianity
  First 5 prediction: early early called social called social social social social social

Epoch 799, Average loss:0.772875706354777, Average accuracy:0.90234375
  Random 5 prediction: associated war than revolutionary revolutionary revolutionary than than revolutionary revolutionary
  First 5 prediction: political been hierarchy war than see anti anti anti anti

Epoch 899, Average loss:0.43675946692625683, Average accuracy:0.9375
  Random 5 prediction: individualist which which individualist warren warren tucker benjamin how tucker
  First 5 prediction: four at warren individualist warren published considered considered considered considered

Epoch 999, Average loss:0.23202441136042276, Average accuracy:0.9602864583333334
  Random 5 prediction: allow allow trabajo you you you you you you you
  First 5 prediction: labour spanish they they they movement movement anarcho anarcho two

生成的文本中的重复单词是常见的,并且应该更好地训练模型。虽然模型的准确率提高到 96%,但仍然不足以生成清晰的文本。尝试增加 LSTM 单元/隐藏层的数量,同时在较大的数据集上运行模型以获取大量周期。

现在让我们在 Keras 建立相同的模型:

Keras 中的 LSTM 和文本生成

您可以在 Jupyter 笔记本ch-08b_RNN_Text_Keras中按照本节的代码进行操作。

我们在 Keras 实现文本生成 LSTM,步骤如下:

  1. 首先,我们将所有数据转换为两个张量,张量x有五列,因为我们一次输入五个字,张量y只有一列输出。我们将y或标签张量转换为单热编码表示。

请记住,在大型数据集的实践中,您将使用 word2vec 嵌入而不是单热表示。

代码语言:javascript复制
# get the data
x_train, y_train = text8.seq_to_xy(seq=text8.part['train'],n_tx=n_x,n_ty=n_y)
# reshape input to be [samples, time steps, features]
x_train = x_train.reshape(x_train.shape[0], x_train.shape[1],1)
y_onehot = np.zeros(shape=[y_train.shape[0],text8.vocab_len],dtype=np.float32)
for i in range(y_train.shape[0]):
    y_onehot[i,y_train[i]]=1
  1. 接下来,仅使用一个隐藏的 LSTM 层定义 LSTM 模型。由于我们的输出不是序列,我们还将return_sequences设置为False
代码语言:javascript复制
n_epochs = 1000
batch_size=128
state_size=128
n_epochs_display=100

# create and fit the LSTM model
model = Sequential()
model.add(LSTM(units=state_size,
                input_shape=(x_train.shape[1], x_train.shape[2]),
                return_sequences=False
                )
          )
model.add(Dense(text8.vocab_len))
model.add(Activation('softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam')
model.summary()

该模型如下所示:

代码语言:javascript复制
Layer (type)                 Output Shape              Param #   
=================================================================
lstm_1 (LSTM)                (None, 128)               66560     
_________________________________________________________________
dense_1 (Dense)              (None, 1457)              187953    
_________________________________________________________________
activation_1 (Activation)    (None, 1457)              0         
=================================================================
Total params: 254,513
Trainable params: 254,513
Non-trainable params: 0
_________________________________________________________________
  1. 对于 Keras,我们运行一个循环来运行 10 次,在每次迭代中训练 100 个周期的模型并打印文本生成的结果。以下是训练模型和生成文本的完整代码:
代码语言:javascript复制
for j in range(n_epochs // n_epochs_display):
     model.fit(x_train, y_onehot, epochs=n_epochs_display,
                         batch_size=batch_size,verbose=0)
     # generate text
     y_pred_r5 = np.empty([10])
     y_pred_f5 = np.empty([10])
     x_test_r5 = random5.copy()
     x_test_f5 = first5.copy()
     # let us generate text of 10 words after feeding 5 words
     for i in range(10):
         for x,y in zip([x_test_r5,x_test_f5],
                        [y_pred_r5,y_pred_f5]):
             x_input = x.copy()
             x_input = x_input.reshape(-1, n_x, n_x_vars)
             y_pred = model.predict(x_input)[0]
             y_pred_id = np.argmax(y_pred)
             y[i]=y_pred_id
             x[:-1] = x[1:]
             x[-1] = y_pred_id
     print('Epoch: ',((j 1) * n_epochs_display)-1)
     print(' Random5 prediction:',id2string(y_pred_r5))
     print(' First5 prediction:',id2string(y_pred_f5))
  1. 输出并不奇怪,从重复单词开始,模型有所改进,但是可以通过更多 LSTM 层,更多数据,更多训练迭代和其他超参数调整来进一步提高。
代码语言:javascript复制
Random 5 words: free bolshevik be n another 
First 5 words: anarchism originated as a term

预测的输出如下:

代码语言:javascript复制
Epoch: 99 
    Random5 prediction: anarchistic anarchistic wrote wrote wrote wrote wrote wrote wrote wrote 
    First5 prediction: right philosophy than than than than than than than than 

Epoch: 199 
    Random5 prediction: anarchistic anarchistic wrote wrote wrote wrote wrote wrote wrote wrote 
    First5 prediction: term i revolutionary than war war french french french french 

Epoch: 299 
    Random5 prediction: anarchistic anarchistic wrote wrote wrote wrote wrote wrote wrote wrote 
    First5 prediction: term i revolutionary revolutionary revolutionary revolutionary revolutionary revolutionary revolutionary revolutionary 

Epoch: 399 
    Random5 prediction: anarchistic anarchistic wrote wrote wrote wrote wrote wrote wrote wrote 
    First5 prediction: term i revolutionary labor had had french french french french 

Epoch: 499 
    Random5 prediction: anarchistic anarchistic amongst wrote wrote wrote wrote wrote wrote wrote 
    First5 prediction: term i revolutionary labor individualist had had french french french 

Epoch: 599 
    Random5 prediction: tolstoy wrote tolstoy wrote wrote wrote wrote wrote wrote wrote     First5 prediction: term i revolutionary labor individualist had had had had had 

Epoch: 699 
    Random5 prediction: tolstoy wrote tolstoy wrote wrote wrote wrote wrote wrote wrote     First5 prediction: term i revolutionary labor individualist had had had had had 

Epoch: 799 
    Random5 prediction: tolstoy wrote tolstoy tolstoy tolstoy tolstoy tolstoy tolstoy tolstoy tolstoy 
    First5 prediction: term i revolutionary labor individualist had had had had had 

Epoch: 899 
    Random5 prediction: tolstoy wrote tolstoy tolstoy tolstoy tolstoy tolstoy tolstoy tolstoy tolstoy 
    First5 prediction: term i revolutionary labor should warren warren warren warren warren 

Epoch: 999 
    Random5 prediction: tolstoy wrote tolstoy tolstoy tolstoy tolstoy tolstoy tolstoy tolstoy tolstoy 
    First5 prediction: term i individualist labor should warren warren warren warren warren

如果您注意到我们在 LSTM 模型的输出中有重复的单词用于文本生成。虽然超参数和网络调整可以消除一些重复,但还有其他方法可以解决这个问题。我们得到重复单词的原因是模型总是从单词的概率分布中选择具有最高概率的单词。这可以改变以选择诸如在连续单词之间引入更大可变性的单词。

总结

在本章中,我们学习了单词嵌入的方法,以找到更好的文本数据元素表示。随着神经网络和深度学习摄取大量文本数据,单热表示和其他单词表示方法变得低效。我们还学习了如何使用 t-SNE 图来可视化文字嵌入。我们使用简单的 LSTM 模型在 TensorFlow 和 Keras 中生成文本。类似的概念可以应用于各种其他任务,例如情感分析,问答和神经机器翻译。

在我们深入研究先进的 TensorFlow 功能(如迁移学习,强化学习,生成网络和分布式 TensorFlow)之前,我们将在下一章中看到如何将 TensorFlow 模型投入生产。

九、TensorFlow 和 Keras 中的 CNN

卷积神经网络CNN)是一种特殊的前馈神经网络,在其架构中包含卷积和汇聚层。也称为 ConvNets,CNN 架构的一般模式是按以下顺序包含这些层:

  1. 完全连接的输入层
  2. 卷积,池化和全连接层的多种组合
  3. 完全连接的输出层,具有 softmax 激活函数

CNN 架构已被证明在解决涉及图像学习的问题(例如图像识别和对象识别)方面非常成功。

在本章中,我们将学习与卷积网络相关的以下主题:

  • 理解卷积
  • 理解池化
  • CNN 架构模式 - LeNet
  • 用于 MNIST 数据集的 LeNet
    • 使用 TensorFlow 和 MNIST 的 LeNet
    • 使用 Keras 和 MNIST 的 LeNet
  • 用于 CIFAR 数据集的 LeNet
    • 使用 TensorFlow 和 CIFAR10 的 LeNet CNN
    • 使用 Keras 和 CIFAR10 的 LeNet CNN

让我们从学习卷积网络背后的核心概念开始。

理解卷积

卷积是 CNN 架构背后的核心概念。简单来说,卷积是一种数学运算,它结合了两个来源的信息来产生一组新的信息。具体来说,它将一个称为内核的特殊矩阵应用于输入张量,以产生一组称为特征图的矩阵。可以使用任何流行的算法将内核应用于输入张量。

生成卷积矩阵的最常用算法如下:

代码语言:javascript复制
N_STRIDES = [1,1]
1. Overlap the kernel with the top-left cells of the image matrix.
2. Repeat while the kernel overlaps the image matrix:
    2.1 c_col = 0
    2.2 Repeat while the kernel overlaps the image matrix:
        2.1.1 set c_row = 0        2.1.2 convolved_scalar = scalar_prod(kernel, overlapped cells)
        2.1.3 convolved_matrix(c_row,c_col) = convolved_scalar
        2.1.4 Slide the kernel down by N_STRIDES[0] rows.
        2.1.5 c_row = c_row    1
    2.3 Slide the kernel to (topmost row, N_STRIDES[1] columns right)
    2.4 c_col = c_col   1

例如,我们假设核矩阵是2 x 2矩阵,输入图像是3 x 3矩阵。下图逐步显示了上述算法:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QpWK3vWN-1681566456881)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/12748d18-c1b3-49c4-8376-208a077c6116.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FGhtdwSV-1681566456881)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/27bc51c5-60bc-4cd0-b030-7253ca8072f6.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jAMVVCNt-1681566456881)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/1f39630a-7dcf-4fd2-8f38-e5cc1123125c.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BEgeoOWZ-1681566456882)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/d613bdcd-7ecd-425d-9941-003500a98adf.png)]

在卷积操作结束时,我们得到以下特征图:

-6

-8

-12

-14

在上面的示例中,与卷积的原始输入相比,生成的特征映射的大小更小。通常,特征图的大小减小(内核大小减 1)。因此,特征图的大小为:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JQwhxlhv-1681566456882)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/0bc241d0-3c55-481f-8687-fb8da66ffa09.png)]

三维张量

对于具有额外深度尺寸的三维张量,您可以将前面的算法视为应用于深度维度中的每个层。将卷积应用于 3D 张量的输出也是 2D 张量,因为卷积运算添加了三个通道。

步幅

数组N_STRIDES中的步长是您想要将内核滑过的行或列的数字。在我们的例子中,我们使用了 1 的步幅。如果我们使用更多的步幅,那么特征图的大小将根据以下等式进一步减小:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NIAh3g4P-1681566456882)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/6db06a54-de55-4fc3-a0c5-485fcf4b35b2.png)]

填充

如果我们不希望减小特征映射的大小,那么我们可以在输入的所有边上使用填充,使得特征的大小增加填充大小的两倍。使用填充,可以按如下方式计算特征图的大小:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j9smkKYX-1681566456883)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/f3a81e63-5d7a-4e84-9c0b-bf2eb6e1f284.png)]

TensorFlow 允许两种填充:SAMEVALIDSAME填充意味着添加填充,使输出特征图与输入特征具有相同的大小。 VALID填充意味着没有填充。

应用前面提到的卷积算法的结果是特征图,是原始张量的滤波版本。例如,特征图可能只有从原始图像中过滤出的轮廓。因此,内核也称为过滤器。对于每个内核,您将获得单独的 2D 特征图。

根据您希望网络学习的特征,您必须应用适当的过滤器来强调所需的特征。 但是,使用 CNN,模型可以自动了解哪些内核在卷积层中最有效。

TensorFlow 中的卷积运算

TensorFlow 提供实现卷积算法的卷积层。例如,具有以下签名的tf.nn.conv2d()操作:

代码语言:javascript复制
tf.nn.conv2d(
  input,
  filter,
  strides,
  padding,
  use_cudnn_on_gpu=None,
  data_format=None,
  name=None
)

inputfilter表示形状[batch_size, input_height, input_width, input_depth]的数据张量和形状[filter_height, filter_width, input_depth, output_depth]的核张量。内核张量中的output_depth表示应该应用于输入的内核数量。strides张量表示每个维度中要滑动的单元数。如上所述,padding是有效的或相同的。

您可以在此链接中找到有关 TensorFlow 中可用卷积操作的更多信息

您可以在此链接中找到有关 Keras 中可用卷积层的更多信息

此链接提供了卷积的详细数学解释:

http://colah.github.io/posts/2014-07-Understanding-Convolutions/

http://ufldl.stanford.edu/tutorial/supervised/FeatureExtractionUsingConvolution/

http://colah.github.io/posts/2014-07-Understanding-Convolutions/

卷积层或操作将输入值或神经元连接到下一个隐藏层神经元。每个隐藏层神经元连接到与内核中元素数量相同数量的输入神经元。所以在前面的例子中,内核有 4 个元素,因此隐藏层神经元连接到输入层的 4 个神经元(3×3 个神经元中)。在我们的例子中,输入层的 4 个神经元的这个区域被称为 CNN 理论中的感受域

卷积层具有每个内核的单独权重和偏差参数。权重参数的数量等于内核中元素的数量,并且只有一个偏差参数。内核的所有连接共享相同的权重和偏差参数。因此在我们的例子中,将有 4 个权重参数和 1 个偏差参数,但如果我们在卷积层中使用 5 个内核,则总共将有5 x 4个权重参数和5 x 1个偏差参数(每个特征图 4 个权重,1 个偏差)。

理解池化

通常,在卷积操作中,应用几个不同的内核,这导致生成若干特征映射。因此,卷积运算导致生成大尺寸数据集。

例如,将形状为3 x 3 x 1的内核应用于具有28 x 28 x 1像素形状的图像的 MNIST 数据集,可生成形状为26 x 26 x 1的特征映射。如果我们在其中应用 32 个这样的滤波器卷积层,则输出的形状为32 x 26 x 26 x 1,即形状为26 x 26 x 1的 32 个特征图。

与形状为28 x 28 x 1的原始数据集相比,这是一个庞大的数据集。因此,为了简化下一层的学习,我们应用池化的概念。

聚合是指计算卷积特征空间区域的聚合统计量。两个最受欢迎的聚合统计数据是最大值和平均值。应用最大池化的输出是所选区域的最大值,而应用平均池的输出是区域中数字的平均值。

例如,假设特征图的形状为3 x 3,池化区域形状为2 x 2。以下图像显示了使用[1, 1]的步幅应用的最大池操作:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hTXGCIMk-1681566456883)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/c4bea549-7ee7-491c-9ad7-37ccb55dbf2e.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gaA4GOmx-1681566456883)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/4cea41c9-7a13-4e8d-bbec-d7d36ecf69b5.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-agOQ9oob-1681566456883)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/efdcb6bb-da73-40ea-ab75-88008b176d48.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mkB9yDTv-1681566456884)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/4c1f8231-e6d4-4bde-9cdf-2332accbb66d.png)]

在最大池操作结束时,我们得到以下矩阵:

5

6

8

9

通常,池化操作应用非重叠区域,因此步幅张量和区域张量被设置为相同的值。

例如,TensorFlow 具有以下签名的max_pooling操作:

代码语言:javascript复制
max_pool(
  value,
  ksize,
  strides,
  padding,
  data_format='NHWC',
  name=None
)

value表示形状[batch_size, input_height, input_width, input_depth]的输入张量。对矩形形状区域ksize执行合并操作。这些区域被形状strides抵消。

您可以在此链接中找到有关 TensorFlow 中可用的池化操作的更多信息

有关 Keras 中可用池化的更多信息,请访问此链接

此链接提供了池化的详细数学说明

CNN 架构模式 - LeNet

LeNet 是实现 CNN 的流行架构模式。在本章中,我们将学习如何通过按以下顺序创建层来构建基于 LeNet 模式的 CNN 模型:

  1. 输入层
  2. 卷积层 1,它产生一组特征映射,具有 ReLU 激活
  3. 池化层 1 产生一组统计聚合的特征映射
  4. 卷积层 2,其产生一组特征映射,具有 ReLU 激活
  5. 池化层 2 产生一组统计聚合的特征映射
  6. 完全连接的层,通过 ReLU 激活来展平特征图
  7. 通过应用简单线性激活产生输出的输出层

LeNet 系列模型由 Yann LeCun 及其研究员介绍。有关 LeNet 系列模型的更多详细信息,请访问此链接。

Yann LeCun 通过此链接维护 LeNet 系列模型列表。

用于 MNIST 数据的 LeNet

您可以按照 Jupyter 笔记本中的代码ch-09a_CNN_MNIST_TF_and_Keras

准备 MNIST 数据到测试和训练集:

代码语言:javascript复制
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets(os.path.join('.','mnist'), one_hot=True)
X_train = mnist.train.images
X_test = mnist.test.images
Y_train = mnist.train.labels
Y_test = mnist.test.labels

TensorFlow 中的用于 MNIST 的 LeNet CNN

在 TensorFlow 中,应用以下步骤为 MNIST 数据构建基于 LeNet 的 CNN 模型:

  1. 定义超参数,以及 x 和 y 的占位符(输入图像和输出标签) :
代码语言:javascript复制
n_classes = 10 # 0-9 digits
n_width = 28
n_height = 28
n_depth = 1
n_inputs = n_height * n_width * n_depth # total pixels
learning_rate = 0.001
n_epochs = 10
batch_size = 100
n_batches = int(mnist.train.num_examples/batch_size)

# input images shape: (n_samples,n_pixels)
x = tf.placeholder(dtype=tf.float32, name="x", shape=[None, n_inputs]) 
# output labels
y = tf.placeholder(dtype=tf.float32, name="y", shape=[None, n_classes])

将输入 x 重塑为形状(n_samples, n_width, n_height, n_depth)

代码语言:javascript复制
x_ = tf.reshape(x, shape=[-1, n_width, n_height, n_depth])
  1. 使用形状为4 x 4的 32 个内核定义第一个卷积层,从而生成 32 个特征图。
  • 首先,定义第一个卷积层的权重和偏差。我们使用正态分布填充参数:
代码语言:javascript复制
layer1_w = tf.Variable(tf.random_normal(shape=[4,4,n_depth,32],
            stddev=0.1),name='l1_w')
layer1_b = tf.Variable(tf.random_normal([32]),name='l1_b')
  • 接下来,用tf.nn.conv2d函数定义卷积层。函数参数stride定义了内核张量在每个维度中应该滑动的元素。维度顺序由data_format确定,可以是'NHWC''NCHW'(默认为'NHWC')。 通常,stride中的第一个和最后一个元素设置为 1。函数参数padding可以是SAMEVALIDSAME padding表示输入将用零填充,以便在卷积后输出与输入的形状相同。使用tf.nn.relu()函数添加relu激活:
代码语言:javascript复制
layer1_conv = tf.nn.relu(tf.nn.conv2d(x_,layer1_w,
                                      strides=[1,1,1,1],
                                      padding='SAME'
                                     )   
                         layer1_b 
                        )
  • 使用tf.nn.max_pool()函数定义第一个池化层。参数ksize表示使用2×2×1个区域的合并操作,参数stride表示将区域滑动2×2×1个像素。因此,区域彼此不重叠。由于我们使用max_pool,池化操作选择2 x 2 x 1区域中的最大值:
代码语言:javascript复制
layer1_pool = tf.nn.max_pool(layer1_conv,ksize=[1,2,2,1],
                 strides=[1,2,2,1],padding='SAME')

第一个卷积层产生 32 个大小为28 x 28 x 1的特征图,然后池化成32 x 14 x 14 x 1的数据。

  1. 定义第二个卷积层,它将此数据作为输入并生成 64 个特征图。
  • 首先,定义第二个卷积层的权重和偏差。我们用正态分布填充参数:
代码语言:javascript复制
layer2_w = tf.Variable(tf.random_normal(shape=[4,4,32,64],
           stddev=0.1),name='l2_w')
layer2_b = tf.Variable(tf.random_normal([64]),name='l2_b')
  • 接下来,用tf.nn.conv2d函数定义卷积层:
代码语言:javascript复制
layer2_conv = tf.nn.relu(tf.nn.conv2d(layer1_pool,
                                      layer2_w,
                                      strides=[1,1,1,1],
                                      padding='SAME'
                                     )   
                          layer2_b
                        )
  • tf.nn.max_pool函数定义第二个池化层:
代码语言:javascript复制
layer2_pool = tf.nn.max_pool(layer2_conv,
                             ksize=[1,2,2,1],
                             strides=[1,2,2,1],
                             padding='SAME'
                            )

第二卷积层的输出形状为64×14×14×1,然后池化成64×7×7×1的形状的输出。

  1. 在输入 1024 个神经元的完全连接层之前重新整形此输出,以产生大小为 1024 的扁平输出:
代码语言:javascript复制
layer3_w = tf.Variable(tf.random_normal(shape=[64*7*7*1,1024],
                       stddev=0.1),name='l3_w')
layer3_b = tf.Variable(tf.random_normal([1024]),name='l3_b')
layer3_fc = tf.nn.relu(tf.matmul(tf.reshape(layer2_pool,
            [-1, 64*7*7*1]),layer3_w)   layer3_b)
  1. 完全连接层的输出馈入具有 10 个输出的线性输出层。我们在这一层没有使用 softmax,因为我们的损失函数自动将 softmax 应用于输出:
代码语言:javascript复制
layer4_w = tf.Variable(tf.random_normal(shape=[1024, n_classes],
                                        stddev=0.1),name='l)
layer4_b = tf.Variable(tf.random_normal([n_classes]),name='l4_b')
layer4_out = tf.matmul(layer3_fc,layer4_w) layer4_b

这创建了我们保存在变量model中的第一个 CNN 模型:

代码语言:javascript复制
model = layer4_out

鼓励读者探索具有不同超参数值的 TensorFlow 中可用的不同卷积和池操作符。

为了定义损失,我们使用tf.nn.softmax_cross_entropy_with_logits函数,对于优化器,我们使用AdamOptimizer函数。您应该尝试探索 TensorFlow 中可用的不同优化器函数。

代码语言:javascript复制
entropy = tf.nn.softmax_cross_entropy_with_logits(logits=model, labels=y)
loss = tf.reduce_mean(entropy)
optimizer = tf.train.AdamOptimizer(learning_rate).minimize(loss)

最后,我们通过迭代n_epochs来训练模型,并且在n_batches上的每个周期列中,每批batch_size的大小:

代码语言:javascript复制
with tf.Session() as tfs:
    tf.global_variables_initializer().run()
    for epoch in range(n_epochs):
        total_loss = 0.0
        for batch in range(n_batches):
            batch_x,batch_y = mnist.train.next_batch(batch_size)
            feed_dict={x:batch_x, y: batch_y}
            batch_loss,_ = tfs.run([loss, optimizer],
                                   feed_dict=feed_dict)
            total_loss  = batch_loss 
        average_loss = total_loss / n_batches
        print("Epoch: {0:04d} loss = {1:0.6f}".format(epoch,average_loss))
    print("Model Trained.")

    predictions_check = tf.equal(tf.argmax(model,1),tf.argmax(y,1))
    accuracy = tf.reduce_mean(tf.cast(predictions_check, tf.float32))
    feed_dict = {x:mnist.test.images, y:mnist.test.labels}
    print("Accuracy:", accuracy.eval(feed_dict=feed_dict))

我们得到以下输出:

代码语言:javascript复制
Epoch: 0000   loss = 1.418295
Epoch: 0001   loss = 0.088259
Epoch: 0002   loss = 0.055410
Epoch: 0003   loss = 0.042798
Epoch: 0004   loss = 0.030471
Epoch: 0005   loss = 0.023837
Epoch: 0006   loss = 0.019800
Epoch: 0007   loss = 0.015900
Epoch: 0008   loss = 0.012918
Epoch: 0009   loss = 0.010322
Model Trained.
Accuracy: 0.9884

现在,与我们在前几章中看到的方法相比,这是一个非常好的准确率。从图像数据中学习 CNN 模型是不是很神奇?

Keras 中的用于 MNIST 的 LeNet CNN

让我们重新审视具有相同数据集的相同 LeNet 架构,以在 Keras 中构建和训练 CNN 模型:

  1. 导入所需的 Keras 模块:
代码语言:javascript复制
import keras
from keras.models import Sequential
from keras.layers import Conv2D,MaxPooling2D, Dense, Flatten, Reshape
from keras.optimizers import SGD
  1. 定义每个层的过滤器数量:
代码语言:javascript复制
n_filters=[32,64]
  1. 定义其他超参数:
代码语言:javascript复制
learning_rate = 0.01
n_epochs = 10
batch_size = 100
  1. 定义顺序模型并添加层以将输入数据重新整形为形状(n_width,n_height,n_depth)
代码语言:javascript复制
model = Sequential()
model.add(Reshape(target_shape=(n_width,n_height,n_depth), 
                  input_shape=(n_inputs,))
         )
  1. 使用4 x 4内核过滤器,SAME填充和relu激活添加第一个卷积层:
代码语言:javascript复制
model.add(Conv2D(filters=n_filters[0],kernel_size=4, 
                 padding='SAME',activation='relu') 
         )
  1. 添加区域大小为2 x 2且步长为2 x 2的池化层:
代码语言:javascript复制
model.add(MaxPooling2D(pool_size=(2,2),strides=(2,2)))
  1. 以与添加第一层相同的方式添加第二个卷积和池化层:
代码语言:javascript复制
model.add(Conv2D(filters=n_filters[1],kernel_size=4, 
                 padding='SAME',activation='relu') 
         )
model.add(MaxPooling2D(pool_size=(2,2),strides=(2,2)))
  1. 添加层以展平第二个层的输出和 1024 个神经元的完全连接层,以处理展平的输出:
代码语言:javascript复制
model.add(Flatten())
model.add(Dense(units=1024, activation='relu'))
  1. 使用softmax激活添加最终输出层:
代码语言:javascript复制
model.add(Dense(units=n_outputs, activation='softmax'))
  1. 使用以下代码查看模型摘要:
代码语言:javascript复制
model.summary()

该模型描述如下:

代码语言:javascript复制
Layer (type)                 Output Shape              Param #   
=================================================================
reshape_1 (Reshape)          (None, 28, 28, 1)         0         
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 28, 28, 32)        544       
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 14, 14, 32)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 14, 14, 64)        32832     
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 7, 7, 64)          0         
_________________________________________________________________
flatten_1 (Flatten)          (None, 3136)              0         
_________________________________________________________________
dense_1 (Dense)              (None, 1024)              3212288   
_________________________________________________________________
dense_2 (Dense)              (None, 10)                10250     
=================================================================
Total params: 3,255,914
Trainable params: 3,255,914
Non-trainable params: 0
_________________________________________________________________
  1. 编译,训练和评估模型:
代码语言:javascript复制
model.compile(loss='categorical_crossentropy',
              optimizer=SGD(lr=learning_rate),
              metrics=['accuracy'])
model.fit(X_train, Y_train,batch_size=batch_size,
          epochs=n_epochs)
score = model.evaluate(X_test, Y_test)
print('nTest loss:', score[0])
print('Test accuracy:', score[1])

我们得到以下输出:

代码语言:javascript复制
Epoch 1/10
55000/55000 [===================] - 267s - loss: 0.8854 - acc: 0.7631   
Epoch 2/10
55000/55000 [===================] - 272s - loss: 0.2406 - acc: 0.9272   
Epoch 3/10
55000/55000 [===================] - 267s - loss: 0.1712 - acc: 0.9488   
Epoch 4/10
55000/55000 [===================] - 295s - loss: 0.1339 - acc: 0.9604   
Epoch 5/10
55000/55000 [===================] - 278s - loss: 0.1112 - acc: 0.9667   
Epoch 6/10
55000/55000 [===================] - 279s - loss: 0.0957 - acc: 0.9714   
Epoch 7/10
55000/55000 [===================] - 316s - loss: 0.0842 - acc: 0.9744   
Epoch 8/10
55000/55000 [===================] - 317s - loss: 0.0758 - acc: 0.9773   
Epoch 9/10
55000/55000 [===================] - 285s - loss: 0.0693 - acc: 0.9790   
Epoch 10/10
55000/55000 [===================] - 217s - loss: 0.0630 - acc: 0.9804
Test loss: 0.0628845927377
Test accuracy: 0.9785

准确率的差异可归因于我们在这里使用 SGD 优化器这一事实,它没有实现我们用于 TensorFlow 模型的AdamOptimizer提供的一些高级功能。

用于 CIFAR10 数据的 LeNet

现在我们已经学会了使用 TensorFlow 和 Keras 的 MNIST 数据集构建和训练 CNN 模型,让我们用 CIFAR10 数据集重复练习。

CIFAR-10 数据集包含 60,000 个32x32像素形状的 RGB 彩色图像。图像被平均分为 10 个不同的类别或类别:飞机,汽车,鸟,猫,鹿,狗,青蛙,马,船和卡车。 CIFAR-10 和 CIFAR-100 是包含 8000 万个图像的大图像数据集的子集。 CIFAR 数据集由 Alex Krizhevsky,Vinod Nair 和 Geoffrey Hinton 收集和标记。数字 10 和 100 表示​​图像类别的数量。

有关 CIFAR 数据集的更多详细信息,请访问此链接:

http://www.cs.toronto.edu/~kriz/cifar.html

http://www.cs.toronto.edu/~kriz/learning-features-2009-TR.pdf

我们选择了 CIFAR 10,因为它有 3 个通道,即图像的深度为 3,而 MNIST 数据集只有一个通道。 为了简洁起见,我们将详细信息留给下载并将数据拆分为训练和测试集,并在本书代码包中的datasetslib包中提供代码。

您可以按照 Jupyter 笔记本中的代码ch-09b_CNN_CIFAR10_TF_and_Keras

我们使用以下代码加载和预处理 CIFAR10 数据:

代码语言:javascript复制
from datasetslib.cifar import cifar10
from datasetslib import imutil
dataset = cifar10()
dataset.x_layout=imutil.LAYOUT_NHWC
dataset.load_data()
dataset.scaleX()

加载数据使得图像采用'NHWC'格式,使数据变形(number_of_samples, image_height, image_width, image_channels)。我们将图像通道称为图像深度。图像中的每个像素是 0 到 255 之间的数字。使用 MinMax 缩放来缩放数据集,以通过将所有像素值除以 255 来标准化图像。

加载和预处理的数据在数据集对象变量中可用作dataset.X_traindataset.Y_traindataset.X_testdataset.Y_test

TensorFlow 中的用于 CIFAR10 的卷积网络

我们保持层,滤波器及其大小与之前的 MNIST 示例中的相同,增加了一个正则化层。由于此数据集与 MNIST 相比较复杂,因此我们为正则化目的添加了额外的丢弃层:

代码语言:javascript复制
tf.nn.dropout(layer1_pool, keep_prob)

在预测和评估期间,占位符keep_prob设置为 1。这样我们就可以重复使用相同的模型进行训练以及预测和评估。

有关 CIFAR10 数据的 LeNet 模型的完整代码在笔记本ch-09b_CNN_CIFAR10_TF_and_Keras中提供。

在运行模型时,我们得到以下输出:

代码语言:javascript复制
Epoch: 0000   loss = 2.115784
Epoch: 0001   loss = 1.620117
Epoch: 0002   loss = 1.417657
Epoch: 0003   loss = 1.284346
Epoch: 0004   loss = 1.164068
Epoch: 0005   loss = 1.058837
Epoch: 0006   loss = 0.953583
Epoch: 0007   loss = 0.853759
Epoch: 0008   loss = 0.758431
Epoch: 0009   loss = 0.663844
Epoch: 0010   loss = 0.574547
Epoch: 0011   loss = 0.489902
Epoch: 0012   loss = 0.410211
Epoch: 0013   loss = 0.342640
Epoch: 0014   loss = 0.280877
Epoch: 0015   loss = 0.234057
Epoch: 0016   loss = 0.195667
Epoch: 0017   loss = 0.161439
Epoch: 0018   loss = 0.140618
Epoch: 0019   loss = 0.126363
Model Trained.
Accuracy: 0.6361

与我们在 MNIST 数据上获得的准确率相比,我们没有获得良好的准确率。通过调整不同的超参数并改变卷积和池化层的组合,可以实现更好的准确率。我们将其作为挑战,让读者探索并尝试不同的 LeNet 架构和超参数变体,以实现更高的准确率。

Keras 中的用于 CIFAR10 的卷积网络

让我们在 Keras 重复 LeNet CNN 模型构建和 CIFAR10 数据训练。我们保持架构与前面的示例相同,以便轻松解释概念。在 Keras 中,丢弃层添加如下:

代码语言:javascript复制
model.add(Dropout(0.2))

用于 CIFAR10 CNN 模型的 Keras 中的完整代码在笔记本ch-09b_CNN_CIFAR10_TF_and_Keras中提供。

在运行模型时,我们得到以下模型描述:

代码语言:javascript复制
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_1 (Conv2D)            (None, 32, 32, 32)        1568      
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 16, 16, 32)        0         
_________________________________________________________________
dropout_1 (Dropout)          (None, 16, 16, 32)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 16, 16, 64)        32832     
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 8, 8, 64)          0         
_________________________________________________________________
dropout_2 (Dropout)          (None, 8, 8, 64)          0         
_________________________________________________________________
flatten_1 (Flatten)          (None, 4096)              0         
_________________________________________________________________
dense_1 (Dense)              (None, 1024)              4195328   
_________________________________________________________________
dropout_3 (Dropout)          (None, 1024)              0         
_________________________________________________________________
dense_2 (Dense)              (None, 10)                10250     
=================================================================
Total params: 4,239,978
Trainable params: 4,239,978
Non-trainable params: 0
_________________________________________________________________

我们得到以下训练和评估结果:

代码语言:javascript复制
Epoch 1/10
50000/50000 [====================] - 191s - loss: 1.5847 - acc: 0.4364   
Epoch 2/10
50000/50000 [====================] - 202s - loss: 1.1491 - acc: 0.5973   
Epoch 3/10
50000/50000 [====================] - 223s - loss: 0.9838 - acc: 0.6582   
Epoch 4/10
50000/50000 [====================] - 223s - loss: 0.8612 - acc: 0.7009   
Epoch 5/10
50000/50000 [====================] - 224s - loss: 0.7564 - acc: 0.7394   
Epoch 6/10
50000/50000 [====================] - 217s - loss: 0.6690 - acc: 0.7710   
Epoch 7/10
50000/50000 [====================] - 222s - loss: 0.5925 - acc: 0.7945   
Epoch 8/10
50000/50000 [====================] - 221s - loss: 0.5263 - acc: 0.8191   
Epoch 9/10
50000/50000 [====================] - 237s - loss: 0.4692 - acc: 0.8387   
Epoch 10/10
50000/50000 [====================] - 230s - loss: 0.4320 - acc: 0.8528   
Test loss: 0.849927025414
Test accuracy: 0.7414

再次,我们将其作为挑战,让读者探索并尝试不同的 LeNet 架构和超参数变体,以实现更高的准确率。

总结

在本章中,我们学习了如何使用 TensorFlow 和 Keras 创建卷积神经网络。我们学习了卷积和池化的核心概念,这是 CNN 的基础。我们学习了 LeNet 系列架构,并为 MNIST 和 CIFAR 数据集创建,训练和评估了 LeNet 族模型。 TensorFlow 和 Keras 提供了许多卷积和池化层和操作。鼓励读者探索本章未涉及的层和操作。

在下一章中,我们将继续学习如何使用自编码器架构将 TensorFlow 应用于图像数据。

十、TensorFlow 和 Keras 中的自编码器

自编码器是一种神经网络架构,通常与无监督学习,降维和数据压缩相关联。自编码器通过使用隐藏层中较少数量的神经元来学习产生与输入层相同的输出。这允许隐藏层以较少数量的参数学习输入的特征。使用较少数量的神经元来学习输入数据的特征的这个过程反过来减少了输入数据集的维度。

自编码器架构有两个阶段:编码器和解码器。在编码器阶段,模型学习表示具有较小维度的压缩向量的输入,并且在解码器阶段,模型学习将压缩向量表示为输出向量。损失计算为输出和输入之间的熵距离,因此通过最小化损失,我们学习将输入编码成能够产生输入的表示的参数,以及另一组学习参数。

在本章中,您将学习如何使用 TensorFlow 和 Keras 在以下主题中创建自编码器架构:

  • 自编码器类型
  • TensorFlow 和 Keras 中的栈式自编码器
  • 在 TensorFlow 和 Keras 中对自编码器进行去噪
  • TensorFlow 和 Keras 中的变分自编码器

自编码器类型

自编码器架构可以在各种配置中找到,例如简单自编码器,稀疏自编码器,去噪自编码器和卷积自编码器。

  • 简单自编码器:在简单的自编码器中,与输入相比,隐藏层具有较少数量的节点或神经元。例如,在 MNIST 数据集中,784 个特征的输入可以连接到 512 个节点的隐藏层或 256 个节点,其连接到 784 特征输出层。因此,在训练期间,仅由 256 个节点学习 784 个特征。 简单自编码器也称为欠完整自编码器。 简单的自编码器可以是单层或多层。通常,单层自编码器在生产中表现不佳。多层自编码器具有多个隐藏层,分为编码器和解码器分组。编码器层将大量特征编码为较少数量的神经元,然后解码器层将学习的压缩特征解码回原始特征或减少数量的特征。多层自编码器被称为栈式自编码器
  • 稀疏自编码器:在稀疏自编码器中,添加正则化项作为惩罚,因此,与简单自编码器相比,表示变得更稀疏。
  • 去噪自编码器(DAE):在 DAE 架构中,输入带有随机噪声。 DAE 重新创建输入并尝试消除噪音。 DAE 中的损失函数将去噪重建输出与原始未损坏输入进行比较。
  • 卷积自编码器(CAE):前面讨论过的自编码器使用全连接层,这种模式类似于多层感知机模型。我们也可以使用卷积层而不是完全连接或密集层。当我们使用卷积层来创建自编码器时,它被称为卷积自编码器。作为一个例子,我们可以为 CAE 提供以下层: 输入 -> 卷积 -> 池化 -> 卷积 -> 池化 -> 输出 第一组卷积和池化层充当编码器,将高维输入特征空间减少到低维特征空间。第二组卷积和池化层充当解码器,将其转换回高维特征空间。
  • 变分自编码器(VAE):变分自编码器架构是自编码器领域的最新发展。 VAE 是一种生成模型,即它产生概率分布的参数,从中可以生成原始数据或与原始数据非常相似的数据。 在 VAE 中,编码器将输入样本转换为潜在空间中的参数,使用该参数对潜在点进行采样。然后解码器使用潜点重新生成原始输入数据。因此,在 VAE 中学习的重点转移到最大化输入数据的概率,而不是试图从输入重建输出。

现在让我们在以下部分中在 TensorFlow 和 Keras 中构建自编码器。我们将使用 MNIST 数据集来构建自编码器。自编码器将学习表示具有较少数量的神经元或特征的 MNIST 数据集的手写数字。

您可以按照 Jupyter 笔记本中的代码ch-10_AutoEncoders_TF_and_Keras

像往常一样,我们首先使用以下代码读取 MNIST 数据集:

代码语言:javascript复制
from tensorflow.examples.tutorials.mnist.input_data import input_data
dataset_home = os.path.join(datasetslib.datasets_root,'mnist') 
mnist = input_data.read_data_sets(dataset_home,one_hot=False)

X_train = mnist.train.images
X_test = mnist.test.images
Y_train = mnist.train.labels
Y_test = mnist.test.labels

pixel_size = 28

我们从训练和测试数据集中提取四个不同的图像及其各自的标签:

代码语言:javascript复制
while True:
     train_images,train_labels = mnist.train.next_batch(4)
     if len(set(train_labels))==4:
        break
while True:
     test_images,test_labels = mnist.test.next_batch(4)
     if len(set(test_labels))==4:
        break

现在让我们看看使用 MNIST 数据集构建自编码器的代码。

您可以按照 Jupyter 笔记本中的代码ch-10_AutoEncoders_TF_and_Keras

TensorFlow 中的栈式自编码器

在 TensorFlow 中构建栈式自编码器模型的步骤如下:

  1. 首先,定义超参数如下:
代码语言:javascript复制
learning_rate = 0.001
n_epochs = 20
batch_size = 100
n_batches = int(mnist.train.num_examples/batch_size)
  1. 定义输入(即特征)和输出(即目标)的数量。输出数量与输入数量相同:
代码语言:javascript复制
# number of pixels in the MNIST image as number of inputs
n_inputs = 784
n_outputs = n_inputs
  1. 定义输入和输出图像的占位符:
代码语言:javascript复制
x = tf.placeholder(dtype=tf.float32, name="x", shape=[None, n_inputs])
y = tf.placeholder(dtype=tf.float32, name="y", shape=[None, n_outputs])
  1. 添加编码器和解码器层的神经元数量为[512,256,256,512]
代码语言:javascript复制
# number of hidden layers
n_layers = 2
# neurons in each hidden layer
n_neurons = [512,256]
# add number of decoder layers:
n_neurons.extend(list(reversed(n_neurons)))
n_layers = n_layers * 2
  1. 定义wb参数:
代码语言:javascript复制
w=[]
b=[]

for i in range(n_layers):
    w.append(tf.Variable(tf.random_normal([n_inputs 
                  if i==0 else n_neurons[i-1],n_neurons[i]]),
                         name="w_{0:04d}".format(i) 
                        )
            ) 
    b.append(tf.Variable(tf.zeros([n_neurons[i]]),
                         name="b_{0:04d}".format(i) 
                        )
            ) 
w.append(tf.Variable(tf.random_normal([n_neurons[n_layers-1] 
                    if n_layers > 0 else n_inputs,n_outputs]),
                     name="w_out"
                    )
        )
b.append(tf.Variable(tf.zeros([n_outputs]),name="b_out"))
  1. 构建网络并为每个层使用 sigmoid 激活函数:
代码语言:javascript复制
# x is input layer
layer = x
# add hidden layers
for i in range(n_layers):
layer = tf.nn.sigmoid(tf.matmul(layer, w[i])   b[i])
# add output layer
layer = tf.nn.sigmoid(tf.matmul(layer, w[n_layers])   b[n_layers]) 
model = layer
  1. 使用mean_squared_error定义loss函数,使用AdamOptimizer定义optimizer函数:
代码语言:javascript复制
mse = tf.losses.mean_squared_error
loss = mse(predictions=model, labels=y)
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate)
optimizer = optimizer.minimize(loss)
  1. 训练模型并预测traintest集的图像:
代码语言:javascript复制
with tf.Session() as tfs:
    tf.global_variables_initializer().run()
    for epoch in range(n_epochs):
        epoch_loss = 0.0
        for batch in range(n_batches):
            X_batch, _ = mnist.train.next_batch(batch_size)
            feed_dict={x: X_batch,y: X_batch}
            _,batch_loss = tfs.run([optimizer,loss], feed_dict)
            epoch_loss  = batch_loss 
        if (epoch==9) or (epoch==0):
            average_loss = epoch_loss / n_batches
            print('epoch: {0:04d} loss = {1:0.6f}'
                  .format(epoch,average_loss))
    # predict images using trained autoencoder model 
    Y_train_pred = tfs.run(model, feed_dict={x: train_images})
    Y_test_pred = tfs.run(model, feed_dict={x: test_images})
  1. 我们看到以下输出,因为损失在 20 个周期后显着减少:
代码语言:javascript复制
epoch: 0000   loss = 0.156696
epoch: 0009   loss = 0.091367
epoch: 0019   loss = 0.078550
  1. 现在模型已经过训练,让我们显示训练模型中的预测图像。我们写了一个辅助函数display_images来帮助我们显示图像:
代码语言:javascript复制
import random

# Function to display the images and labels
# images should be in NHW or NHWC format
def display_images(images, labels, count=0, one_hot=False):
    # if number of images to display is not provided, then display all the images
    if (count==0):
        count = images.shape[0]

    idx_list = random.sample(range(len(labels)),count)
    for i in range(count):
        plt.subplot(4, 4, i 1)
        plt.title(labels[i])
        plt.imshow(images[i])
        plt.axis('off')
    plt.tight_layout()
    plt.show()

使用此函数,我们首先显示训练集中的四个图像和自编码器预测的图像。

第一行表示实际图像,第二行表示生成的图像:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2RJ7nvWt-1681566456884)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/38171092-c4ce-46a5-8ad6-b85fcdcb4cc6.png)]

生成的图像有一点点噪音,可以通过更多训练和超参数调整来消除。现在预测训练集图像并不神奇,因为我们在这些图像上训练了自编码器,因此它知道它们。让我们看一下预测测试集图像的结果。 第一行表示实际图像,第二行表示生成的图像:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z0aMV2ce-1681566456885)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/865dec56-0438-4c15-902b-4533b7f21ea3.png)]

哇!经过训练的自编码器能够生成相同的数字,只有从 768 中学到的 256 个特征。生成的图像中的噪声可以通过超参数调整和更多训练来改善。

Keras 中的栈式自编码器

现在让我们在 Keras 中构建相同的自编码器。

我们使用以下命令清除笔记本中的图,以便我们可以构建一个新图,该图不会占用上一个会话或图中的任何内存:

tf.reset_default_graph() keras.backend.clear_session()

  1. 首先,我们导入 keras 库并定义超参数和层:
代码语言:javascript复制
import keras
from keras.layers import Dense
from keras.models import Sequential

learning_rate = 0.001
n_epochs = 20
batch_size = 100
n_batches = int(mnist.train.num_examples/batch_sizee
# number of pixels in the MNIST image as number of inputs
n_inputs = 784
n_outputs = n_i
# number of hidden layers
n_layers = 2
# neurons in each hidden layer
n_neurons = [512,256]
# add decoder layers:
n_neurons.extend(list(reversed(n_neurons)))
n_layers = n_layers * 2
  1. 接下来,我们构建一个顺序模型并为其添加密集层。对于更改,我们对隐藏层使用relu激活,为最终层使用linear激活:
代码语言:javascript复制
model = Sequential()

# add input to first layer
model.add(Dense(units=n_neurons[0], activation='relu', 
    input_shape=(n_inputs,)))

for i in range(1,n_layers):
    model.add(Dense(units=n_neurons[i], activation='relu'))

# add last layer as output layer
model.add(Dense(units=n_outputs, activation='linear'))
  1. 现在让我们显示模型摘要以查看模型的外观:
代码语言:javascript复制
model.summary()

该模型在五个密集层中共有 1,132,816 个参数:

代码语言:javascript复制
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_1 (Dense)              (None, 512)               401920    
_________________________________________________________________
dense_2 (Dense)              (None, 256)               131328    
_________________________________________________________________
dense_3 (Dense)              (None, 256)               65792     
_________________________________________________________________
dense_4 (Dense)              (None, 512)               131584    
_________________________________________________________________
dense_5 (Dense)              (None, 784)               402192    
=================================================================
Total params: 1,132,816
Trainable params: 1,132,816
Non-trainable params: 0
_________________________________________________________________
  1. 让我们用上一个例子中的均方损失编译模型:
代码语言:javascript复制
model.compile(loss='mse',
    optimizer=keras.optimizers.Adam(lr=learning_rate),
    metrics=['accuracy'])

model.fit(X_train, X_train,batch_size=batch_size,
    epochs=n_epochs)

在 20 个周期,我们能够获得 0.0046 的损失,相比之前我们得到的 0.078550:

代码语言:javascript复制
Epoch 1/20
55000/55000 [==========================] - 18s - loss: 0.0193 - acc: 0.0117    
Epoch 2/20
55000/55000 [==========================] - 18s - loss: 0.0087 - acc: 0.0139 
...
...
...   
Epoch 20/20
55000/55000 [==========================] - 16s - loss: 0.0046 - acc: 0.0171

现在让我们预测并显示模型生成的训练和测试图像。第一行表示实际图像,第二行表示生成的图像。以下是 t 降雨设置图像:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AS8016kR-1681566456885)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/f3177e47-afdc-4f5d-a574-257d22403286.png)]

以下是测试集图像:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K6mvwrO3-1681566456885)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/2f99a937-f842-4d85-96ad-1c41e2e5d396.png)]

这是我们在能够从 256 个特征生成图像时实现的非常好的准确率。

TensorFlow 中的去噪自编码器

正如您在本章的第一部分中所了解的那样,可以使用去噪自编码器来训练模型,以便它们能够从输入到训练模型的图像中去除噪声:

  1. 出于本示例的目的,我们编写以下辅助函数来帮助我们为图像添加噪声:
代码语言:javascript复制
def add_noise(X):
    return X   0.5 * np.random.randn(X.shape[0],X.shape[1])
  1. 然后我们为测试图像添加噪声并将其存储在单独的列表中:
代码语言:javascript复制
test_images_noisy = add_noise(test_images)

我们将使用这些测试图像来测试我们的去噪模型示例的输出。

  1. 我们按照前面的例子构建和训练去噪自编码器,但有一点不同:在训练时,我们将噪声图像输入到输入层,我们用非噪声图像检查重建和去噪误差,如下面的代码所示:
代码语言:javascript复制
X_batch, _ = mnist.train.next_batch(batch_size)
X_batch_noisy = add_noise(X_batch)
feed_dict={x: X_batch_noisy, y: X_batch}
_,batch_loss = tfs.run([optimizer,loss], feed_dict=feed_dict)

笔记本ch-10_AutoEncoders_TF_and_Keras中提供了去噪自编码器的完整代码。

现在让我们首先显示从 DAE 模型生成的测试图像;第一行表示原始的非噪声测试图像,第二行表示生成的测试图像:

代码语言:javascript复制
display_images(test_images.reshape(-1,pixel_size,pixel_size),test_labels)
display_images(Y_test_pred1.reshape(-1,pixel_size,pixel_size),test_labels)

上述代码的结果如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wFhS6PL7-1681566456886)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/843e4c1a-9600-4c6c-8300-42cd8a5191cc.png)]

接下来,当我们输入噪声测试图像时,我们显示生成的图像:

代码语言:javascript复制
display_images(test_images_noisy.reshape(-1,pixel_size,pixel_size),
    test_labels)
display_images(Y_test_pred2.reshape(-1,pixel_size,pixel_size),test_labels)

上述代码的结果如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9Z56aP92-1681566456886)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/5f949726-6ef6-4407-8656-47d45e26fc25.png)]

那太酷了!!该模型学习了图像并生成了几乎正确的图像,即使是非常嘈杂的图像。通过适当的超参数调整可以进一步提高再生质量。

Keras 中的去噪自编码器

现在让我们在 Keras 中构建相同的去噪自编码器。

由于 Keras 负责按批量大小喂养训练集,我们创建了一个嘈杂的训练集作为我们模型的输入:

代码语言:javascript复制
X_train_noisy = add_noise(X_train)

Keras 中 DAE 的完整代码在笔记本ch-10_AutoEncoders_TF_and_Keras中提供。

DAE Keras 模型如下所示:

代码语言:javascript复制
Layer (type)                 Output Shape              Param #   
=================================================================
dense_1 (Dense)              (None, 512)               401920    
_________________________________________________________________
dense_2 (Dense)              (None, 256)               131328    
_________________________________________________________________
dense_3 (Dense)              (None, 256)               65792     
_________________________________________________________________
dense_4 (Dense)              (None, 512)               131584    
_________________________________________________________________
dense_5 (Dense)              (None, 784)               402192    
=================================================================
Total params: 1,132,816
Trainable params: 1,132,816
Non-trainable params: 0

由于 DAE 模型很复杂,为了演示,我们不得不将周期数增加到 100 来训练模型:

代码语言:javascript复制
n_epochs=100

model.fit(x=X_train_noisy, y=X_train,
     batch_size=batch_size,
     epochs=n_epochs,
     verbose=0)

Y_test_pred1 = model.predict(test_images)
Y_test_pred2 = model.predict(test_images_noisy)

打印生成的图像:

代码语言:javascript复制
display_images(test_images.reshape(-1,pixel_size,pixel_size),test_labels)
display_images(Y_test_pred1.reshape(-1,pixel_size,pixel_size),test_labels)

第一行是原始测试图像,第二行是生成的测试图像:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-izyL2fUI-1681566456886)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/888247bf-9fb1-4fec-aec1-73b4fac5bc85.png)]

代码语言:javascript复制
display_images(test_images_noisy.reshape(-1,pixel_size,pixel_size),
    test_labels)
display_images(Y_test_pred2.reshape(-1,pixel_size,pixel_size),test_labels)

第一行是噪声测试图像,第二行是生成的测试图像:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sQJXhItW-1681566456887)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/11853759-8390-41a8-9990-83d7b37828de.png)]

正如我们所看到的,去噪自编码器可以很好地从噪声版本的图像中生成图像。

TensorFlow 中的变分自编码器

变分自编码器是自编码器的现代生成版本。让我们为同一个前面的问题构建一个变分自编码器。我们将通过提供来自原始和嘈杂测试集的图像来测试自编码器。

我们将使用不同的编码风格来构建此自编码器,以便使用 TensorFlow 演示不同的编码风格:

  1. 首先定义超参数:
代码语言:javascript复制
learning_rate = 0.001
n_epochs = 20
batch_size = 100
n_batches = int(mnist.train.num_examples/batch_size)
# number of pixels in the MNIST image as number of inputs
n_inputs = 784
n_outputs = n_inputs
  1. 接下来,定义参数字典以保存权重和偏差参数:
代码语言:javascript复制
params={}
  1. 定义每个编码器和解码器中隐藏层的数量:
代码语言:javascript复制
n_layers = 2
# neurons in each hidden layer
n_neurons = [512,256] 
  1. 变分编码器中的新增加是我们定义潜变量z的维数:
代码语言:javascript复制
n_neurons_z = 128 # the dimensions of latent variables
  1. 我们使用激活tanh
代码语言:javascript复制
activation = tf.nn.tanh
  1. 定义输入和输出占位符:
代码语言:javascript复制
x = tf.placeholder(dtype=tf.float32, name="x", 
                    shape=[None, n_inputs]) 
y = tf.placeholder(dtype=tf.float32, name="y", 
                    shape=[None, n_outputs])
  1. 定义输入层:
代码语言:javascript复制
# x is input layer
layer = x
  1. 定义编码器网络的偏差和权重并添加层。变分自编码器的编码器网络也称为识别网络或推理网络或概率编码器网络:
代码语言:javascript复制
for i in range(0,n_layers):
    name="w_e_{0:04d}".format(i)
    params[name] = tf.get_variable(name=name, 
        shape=[n_inputs if i==0 else n_neurons[i-1],
        n_neurons[i]], 
        initializer=tf.glorot_uniform_initializer()
        )
    name="b_e_{0:04d}".format(i)
    params[name] = tf.Variable(tf.zeros([n_neurons[i]]),
        name=name
        )
    layer = activation(tf.matmul(layer,
        params["w_e_{0:04d}".format(i)]
        )   params["b_e_{0:04d}".format(i)]
        )
  1. 接下来,添加潜在变量的均值和方差的层:
代码语言:javascript复制
name="w_e_z_mean"
params[name] = tf.get_variable(name=name,
    shape=[n_neurons[n_layers-1], n_neurons_z],
    initializer=tf.glorot_uniform_initializer()
    )
name="b_e_z_mean"
params[name] = tf.Variable(tf.zeros([n_neurons_z]),
    name=name
    )
z_mean = tf.matmul(layer, params["w_e_z_mean"])   
            params["b_e_z_mean"]
name="w_e_z_log_var"
params[name] = tf.get_variable(name=name,
    shape=[n_neurons[n_layers-1], n_neurons_z],
    initializer=tf.glorot_uniform_initializer()
    )
name="b_e_z_log_var"
params[name] = tf.Variable(tf.zeros([n_neurons_z]),
    name="b_e_z_log_var"
    )

z_log_var = tf.matmul(layer, params["w_e_z_log_var"])   
            params["b_e_z_log_var"]
  1. 接下来,定义表示与z方差的变量相同形状的噪声分布的epsilon变量:
代码语言:javascript复制
epsilon = tf.random_normal(tf.shape(z_log_var), 
    mean=0, 
    stddev=1.0,
    dtype=tf.float32, 
    name='epsilon'
    )
  1. 根据均值,对数方差和噪声定义后验分布:
代码语言:javascript复制
z = z_mean   tf.exp(z_log_var * 0.5) * epsilon 
  1. 接下来,定义解码器网络的权重和偏差,并添加解码器层。变分自编码器中的解码器网络也称为概率解码器或生成器网络。
代码语言:javascript复制
# add generator / probablistic decoder network parameters and layers
layer = z

for i in range(n_layers-1,-1,-1):
name="w_d_{0:04d}".format(i)
    params[name] = tf.get_variable(name=name, 
    shape=[n_neurons_z if i==n_layers-1 else n_neurons[i 1],
    n_neurons[i]], 
    initializer=tf.glorot_uniform_initializer()
    )
name="b_d_{0:04d}".format(i)
params[name] = tf.Variable(tf.zeros([n_neurons[i]]),
    name=name 
    )
layer = activation(tf.matmul(layer, params["w_d_{0:04d}".format(i)])   
    params["b_d_{0:04d}".format(i)])
  1. 最后,定义输出层:
代码语言:javascript复制
name="w_d_z_mean"
params[name] = tf.get_variable(name=name,
    shape=[n_neurons[0],n_outputs],
    initializer=tf.glorot_uniform_initializer()
    )
name="b_d_z_mean"
    params[name] = tf.Variable(tf.zeros([n_outputs]),
    name=name
    )
name="w_d_z_log_var" 
params[name] = tf.Variable(tf.random_normal([n_neurons[0],
    n_outputs]),
    name=name
    )
name="b_d_z_log_var"
params[name] = tf.Variable(tf.zeros([n_outputs]),
    name=name
    )
layer = tf.nn.sigmoid(tf.matmul(layer, params["w_d_z_mean"])   
    params["b_d_z_mean"])

model = layer
  1. 在变异自编码器中,我们有重建损失和正则化损失。将损失函数定义为重建损失和正则化损失的总和:
代码语言:javascript复制
rec_loss = -tf.reduce_sum(y * tf.log(1e-10   model)   (1-y) 
                            * tf.log(1e-10   1 - model), 1)
reg_loss = -0.5*tf.reduce_sum(1   z_log_var - tf.square(z_mean) 
                                - tf.exp(z_log_var), 1) 
loss = tf.reduce_mean(rec_loss reg_loss)
  1. 根据AdapOptimizer定义优化器函数:
代码语言:javascript复制
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate)
            .minimize(loss)
  1. 现在让我们训练模型并从非噪声和噪声测试图像生成图像:
代码语言:javascript复制
with tf.Session() as tfs:
    tf.global_variables_initializer().run()
    for epoch in range(n_epochs):
        epoch_loss = 0.0
        for batch in range(n_batches):
            X_batch, _ = mnist.train.next_batch(batch_size)
            feed_dict={x: X_batch,y: X_batch}
            _,batch_loss = tfs.run([optimizer,loss], 
                            feed_dict=feed_dict)
            epoch_loss  = batch_loss
        if (epoch==9) or (epoch==0):
            average_loss = epoch_loss / n_batches
            print("epoch: {0:04d} loss = {1:0.6f}"
                      .format(epoch,average_loss))

# predict images using autoencoder model trained
Y_test_pred1 = tfs.run(model, feed_dict={x: test_images})
Y_test_pred2 = tfs.run(model, feed_dict={x: test_images_noisy})

我们得到以下输出:

代码语言:javascript复制
epoch: 0000   loss = 180.444682
epoch: 0009   loss = 106.817749
epoch: 0019   loss = 102.580904

现在让我们显示图像:

代码语言:javascript复制
display_images(test_images.reshape(-1,pixel_size,pixel_size),test_labels)
display_images(Y_test_pred1.reshape(-1,pixel_size,pixel_size),test_labels)

结果如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OTodRSdE-1681566456887)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/121f4c1e-cabc-4b93-a3d4-28aa4b0c9180.png)]

代码语言:javascript复制
display_images(test_images_noisy.reshape(-1,pixel_size,pixel_size),
    test_labels)
display_images(Y_test_pred2.reshape(-1,pixel_size,pixel_size),test_labels)

结果如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A0fqjURB-1681566456887)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/50581981-df44-410a-a655-05e58413ea89.png)]

同样,可以通过超参数调整和增加学习量来改善结果。

Keras 中的变分自编码器

在 Keras 中,构建变分自编码器更容易,并且代码行更少。 Keras 变分自编码器最好使用函数式风格构建。到目前为止,我们已经使用了在 Keras 中构建模型的顺序样式,现在在这个例子中,我们将看到在 Keras 中构建 VAE 模型的函数式风格。在 Keras 建立 VAE 的步骤如下:

  1. 定义隐藏层和潜在变量层中的超参数和神经元数量:
代码语言:javascript复制
import keras
from keras.layers import Lambda, Dense, Input, Layer
from keras.models import Model
from keras import backend as K

learning_rate = 0.001
batch_size = 100
n_batches = int(mnist.train.num_examples/batch_size)
# number of pixels in the MNIST image as number of inputs
n_inputs = 784
n_outputs = n_inputs
# number of hidden layers
n_layers = 2
# neurons in each hidden layer
n_neurons = [512,256]
# the dimensions of latent variables
n_neurons_z = 128
  1. 构建输入层:
代码语言:javascript复制
x = Input(shape=(n_inputs,), name='input')
  1. 构建编码器层,以及潜在变量的均值和方差层:
代码语言:javascript复制
# build encoder
layer = x
for i in range(n_layers):
    layer = Dense(units=n_neurons[i], activation='relu',name='enc_{0}'.format(i))(layer)

z_mean = Dense(units=n_neurons_z,name='z_mean')(layer)
z_log_var = Dense(units=n_neurons_z,name='z_log_v')(layer)
  1. 创建噪声和后验分布:
代码语言:javascript复制
# noise distribution
epsilon = K.random_normal(shape=K.shape(z_log_var), 
          mean=0,stddev=1.0)

# posterior distribution
z = Lambda(lambda zargs: zargs[0]   K.exp(zargs[1] * 0.5) * epsilon,
    name='z')([z_mean,z_log_var])
  1. 添加解码器层:
代码语言:javascript复制
# add generator / probablistic decoder network layers
layer = z
for i in range(n_layers-1,-1,-1):
    layer = Dense(units=n_neurons[i], activation='relu',
            name='dec_{0}'.format(i))(layer)
  1. 定义最终输出层:
代码语言:javascript复制
y_hat = Dense(units=n_outputs, activation='sigmoid',
        name='output')(layer)
  1. 最后,从输入层和输出层定义模型并显示模型摘要:
代码语言:javascript复制
model = Model(x,y_hat)
model.summary()

我们看到以下摘要:

代码语言:javascript复制
_________________________________________________________________________
Layer (type)         Output Shape          Param #     Connected to                     
=========================================================================
input (InputLayer)   (None, 784)           0                                            
_________________________________________________________________________
enc_0 (Dense)        (None, 512)           401920      input[0][0]                      
_________________________________________________________________________
enc_1 (Dense)        (None, 256)           131328      enc_0[0][0]                      
_________________________________________________________________________
z_mean (Dense)       (None, 128)           32896       enc_1[0][0]                      
_________________________________________________________________________
z_log_v (Dense)      (None, 128)           32896       enc_1[0][0]                      
_________________________________________________________________________
z (Lambda)           (None, 128)           0           z_mean[0][0]                     
                                                       z_log_v[0][0]                    
_________________________________________________________________________
dec_1 (Dense)        (None, 256)           33024       z[0][0]                          
_________________________________________________________________________
dec_0 (Dense)        (None, 512)           131584      dec_1[0][0]                      
_________________________________________________________________________
output (Dense)       (None, 784)           402192      dec_0[0][0]                      
=========================================================================
Total params: 1,165,840
Trainable params: 1,165,840
Non-trainable params: 0
_________________________________________________________________________
  1. 定义一个计算重建和正则化损失之和的函数:
代码语言:javascript复制
def vae_loss(y, y_hat):
    rec_loss = -K.sum(y * K.log(1e-10   y_hat)   (1-y) * 
                K.log(1e-10   1 - y_hat), axis=-1)
    reg_loss = -0.5 * K.sum(1   z_log_var - K.square(z_mean) - 
                K.exp(z_log_var), axis=-1) 
    loss = K.mean(rec_loss reg_loss)
    return loss
  1. 使用此损失函数来编译模型:
代码语言:javascript复制
model.compile(loss=vae_loss,
    optimizer=keras.optimizers.Adam(lr=learning_rate))
  1. 让我们训练 50 个周期的模型并预测图像,正如我们在前面的部分中所做的那样:
代码语言:javascript复制
n_epochs=50
model.fit(x=X_train_noisy,y=X_train,batch_size=batch_size,
    epochs=n_epochs,verbose=0)
Y_test_pred1 = model.predict(test_images)
Y_test_pred2 = model.predict(test_images_noisy)

让我们显示结果图像:

代码语言:javascript复制
display_images(test_images.reshape(-1,pixel_size,pixel_size),test_labels)
display_images(Y_test_pred1.reshape(-1,pixel_size,pixel_size),test_labels)

我们得到如下结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-obGpqp5N-1681566456888)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/9ced2741-a51f-4bcc-951d-2d73e22a1c89.png)]

代码语言:javascript复制
display_images(test_images_noisy.reshape(-1,pixel_size,pixel_size),
    test_labels)
display_images(Y_test_pred2.reshape(-1,pixel_size,pixel_size),test_labels)

我们得到以下结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7wR09F8w-1681566456888)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/mastering-tf-1x-zh/img/2bf0dce8-3567-4713-baf2-c20cd2178af3.png)]

这很棒!!生成的图像更清晰,更清晰。

总结

自编码器是无监督数据学习的绝佳工具。它们通常用于降低维数,因此数据可以用较少数量的特征来表示。在本章中,您了解了各种类型的自编码器。我们使用 TensorFlow 和 Keras 练习构建三种类型的自编码器:栈式自编码器,去噪自编码器和变分自编码器。我们使用 MNIST 数据集作为示例。

在最后的章节中,您学习了如何使用 TensorFlow 和 Keras 构建各种机器学习和深度学习模型,例如回归,分类,MLP,CNN,RNN 和自编码器。在下一章中,您将了解 TensorFlow 和 Keras 的高级功能,这些功能允许我们将模型投入生产。

1 人点赞