【翻译】Sklearn与TensorFlow机器学习实用指南 —— 第14章 循环神经网络(下)

浏览: 1982

作者:ApacheCN【翻译】  Python机器学习爱好者

Python爱好者社区专栏作者

GitHub:https://github.com/apachecn/hands_on_Ml_with_Sklearn_and_TF


在多个 GPU 上分布式部署深度 RNN 网络

Dropout 的应用

对于深层深度 RNN,在训练集上很容易过拟合。Dropout 是防止过拟合的常用技术。

可以简单的在 RNN 层之前或之后添加一层 Dropout 层,但如果需要在 RNN 层之间应用 Dropout 技术就需要DropoutWrapper

下面的代码中,每一层的 RNN 的输入前都应用了 Dropout,Dropout 的概率为 50%。

keep_prob = 0.5

cell = tf.contrib.rnn.BasicRNNCell(num_units=n_neurons)
cell_drop = tf.contrib.rnn.DropoutWrapper(cell, input_keep_prob=keep_prob)
multi_layer_cell = tf.contrib.rnn.MultiRNNCell([cell_drop]*n_layers)
rnn_outputs, states = tf.nn.dynamic_rnn(multi_layer_cell, X, dtype=tf.float32)

同时也可以通过设置output_keep_prob来在输出应用 Dropout 技术。

然而在以上代码中存在的主要问题是,Dropout 不管是在训练还是测试时都起作用了,而我们想要的仅仅是在训练时应用 Dropout。

很不幸的是DropoutWrapper不支持is_training这样一个设置选项。因此必须自己写 Dropout 包装类,或者创建两个计算图,一个用来训练,一个用来测试。后则可通过如下面代码这样实现。

import sys
is_training  = (sys.argv[-1] == "train")

X = tf.placeholder(tf.float32, [None, n_steps, n_inputs])
y = tf.placeholder(tf.float32, [None, n_steps, n_outputs])
cell = tf.contrib.rnn.BasicRNNCell(num_units=n_neurons)
if is_training:
   cell = tf.contrib.rnn.DropoutWrapper(cell, input_keep_prob=keep_prob)
multi_layer_cell = tf.contrib.rnn.MultiRNNCell([cell]*n_layers)
rnn_outpus, status = tf.nn.dynamic_rnn(multi_layer_cell, X, dtype=tf.float32)

[...] # bulid the rest of the graph
init = tf.global_variables_initializer()
saver = tf.train.Saver()

with tf.Session() as sess:
   if is_training:
       init.run()
       for iteration in range(n_iterations):
           [...] # train the model
       save_path = saver.save(sess, "/tmp/my_model.ckpt")
   else:
       saver.restore(sess, "/tmp/my_model.ckpt")
       [...] # use the model

通过以上的方法就能够训练各种 RNN 网络了。然而对于长序列的 RNN 训练还言之过早,事情会变得有一些困难。

那么我们来探讨一下究竟这是为什么和怎么应对呢?

长时训练的困难

在训练长序列的 RNN 模型时,那么就需要把 RNN 在时间维度上展开成很深的神经网络。正如任何深度神经网络一样,其面临着梯度消失/爆炸的问题,使训练无法终止或收敛。

很多之前讨论过的缓解这种问题的技巧都可以应用在深度展开的 RNN 网络:好的参数初始化方式,非饱和的激活函数(如 ReLU),批量规范化(Batch Normalization), 梯度截断(Gradient Clipping),更快的优化器。

即便如此, RNN 在处理适中的长序列(如 100 输入序列)也在训练时表现的很慢。

最简单和常见的方法解决训练时长问题就是在训练阶段仅仅展开限定时间步长的 RNN 网络,一种称为截断时间反向传播的算法。

在 TensorFlow 中通过截断输入序列来简单实现这种功能。例如在时间序列预测问题上可以在训练时减小n_steps来实现截断。理所当然这种方法会限制模型在长期模式的学习能力。一种变通方案时确保缩短的序列中包含旧数据和新数据,从而使模型获得两者信息(如序列同时包含最近五个月的数据,最近五周的和最近五天的数据)。

问题时如何确保从去年的细分类中获取的数据有效性呢?这期间短暂但重要的事件对后世的影响,甚至时数年后这种影响是否一定要考虑在内呢(如选举结果)?这种方案有其先天的不足之处。

在长的时间训练过程中,第二个要面临的问题时第一个输入的记忆会在长时间运行的 RNN 网络中逐渐淡去。确实,通过变换的方式,数据穿流在 RNN 网络之中,每个时间步长后都有一些信息被抛弃掉了。那么在一定时间后,第一个输入实际上会在 RNN 的状态中消失于无形。

比如说,你想要分析长篇幅的影评的情感类别,影评以"I love this movie"开篇,并辅以各种改善影片的一些建议。试想一下,如果 RNN 网络逐渐忘记了开头的几个词,RNN 网络的判断完全有可能会对影评断章取义。

为了解决其中的问题,各种能够携带长时记忆的神经单元的变体被提出。这些变体是有效的,往往基本形式的神经单元就不怎么被使用了。

首先了解一下最流行的一种长时记忆神经单元:长短时记忆神经单元 LSTM。

LSTM 单元

长短时记忆单元在 1997 年由 S.H. 和 J.S. 首次提出 [3],并在接下来的几年内经过 A.G,H.S[4],W.Z [5] 等数位研究人员的改进逐渐形成。如果把 LSTM 单元看作一个黑盒,从外围看它和基本形式的记忆单元很相似,但 LSTM 单元会比基本单元性能更好,收敛更快,能够感知数据的长时依赖。TensorFlow 中通过BasicLSTMCell实现 LSTM 单元。

[3]: "Long Short-Term Memory," S.Hochreiter and J.Schmidhuber(1997)

[4]: "Long Short-Term Memory Recurrent Neural Network Architectures for Large Scale Acoustic Modeling," H.Sak et al.(2014)

[5]: "Recurrent Neural Network Regularization," W.Zaremba et al.(2015)

lstm_cell = tf.contrib.rnn.BasicLSTMCell(num_units=n_neurons)

LSTM 单元的工作机制是什么呢?在图 14-13 中展示了基本 LSTM 单元的结构。

简要来说,LSTM 单元能够学习到识别重要输入(输入门作用),存储进长时状态,并保存必要的时间(遗忘门功能),并学会提取当前输出所需要的记忆。

这也解释了 LSTM 单元能够在提取长时序列,长文本,录音等数据中的长期模式的惊人成功的原因。

公式 14-3 总结了如何计算单元的长时状态,短时状态,和单个输入情形时每单位步长的输出(小批量的方程形式与单输入的形式相似)。


窥孔连接

lstm_cell = tf.contrib.rnn.LSTMCell(num_units=n_neurons, use_peepholes=True)

在众多 LSTM 变体中,一个特别流行的变体就是 GRU 单元。

GRU 单元

门控循环单元(图 14-14)在 2014 年的 K.Cho et al. 的论文中提出,并且此文也引入了前文所述的编解码网络。

门控循环单元是 LSTM 单元的简化版本,能实现同样的性能,这也说明了为什么它能越来越流行。简化主要在一下几个方面:


公式 14-4 总结了如何计算单个输入情形时每单位步的单元的状态。

在 TensoFlow 中创建 GRU 单元很简单:

gru_cell = tf.contrib.rnn.GRUCell(n_units=n_neurons)

LSTM 或 GRU 单元是近年来 RNN 成功背后的主要原因之一,特别是在自然语言处理(NLP)中的应用。

自然语言处理

现在,大多数最先进的 NLP 应用(如机器翻译,自动摘要,解析,情感分析等),现在(至少一部分)都基于 RNN。 在最后一节中,我们将快速了解机器翻译模型的概况。 TensorFlow 的很厉害的 Word2Vec 和 Seq2Seq 教程非常好地介绍了这个主题,所以你一定要阅读一下。

单词嵌入

在我们开始之前,我们需要选择一个词的表示形式。 一种选择可以是,使用单热向量表示每个词。 假设你的词汇表包含 5 万个单词,那么第n个单词将被表示为 50,000 维的向量,除了第n个位置为 1 之外,其它全部为 0。 然而,对于如此庞大的词汇表,这种稀疏表示根本就不会有效。 理想情况下,你希望相似的单词具有相似的表示形式,这使得模型可以轻松地将所学的关于单词的只是,推广到所有相似单词。 例如,如果模型被告知"I drink milk"是一个有效的句子,并且如果它知道"milk"接近于"water",而不同于"shoes",那么它会知道"I drink water" 也许是一个有效的句子,而"I drink shoes"可能不是。 但你如何提出这样一个有意义的表示呢?

最常见的解决方案是,用一个相当小且密集的向量(例如 150 维)表示词汇表中的每个单词,称为嵌入,并让神经网络在训练过程中,为每个单词学习一个良好的嵌入。 在训练开始时,嵌入只是随机选择的,但在训练过程中,反向传播会自动更新嵌入,来帮助神经网络执行任务。 通常这意味着,相似的词会逐渐彼此靠近,甚至最终以一种相当有意义的方式组织起来。 例如,嵌入可能最终沿着各种轴分布,它们代表性别,单数/复数,形容词/名词。 结果可能真的很神奇。

在TensorFlow中,首先需要创建一个变量来表示词汇表中每个词的嵌入(随机初始化):

vocabulary_size = 50000
embedding_size = 150
embeddings = tf.Variable(
   tf.random_uniform([vocabulary_size, embedding_size], -1.0, 1.0))

现在假设你打算将句子"I drink milk"提供给你的神经网络。 你应该首先对句子进行预处理并将其分解成已知单词的列表。 例如,你可以删除不必要的字符,用预定义的标记词(如"[UNK]")替换未知单词,用"[NUM]"替换数字值,用"[URL]"替换 URL 等。 一旦你有了一个已知单词列表,你可以在字典中查找每个单词的整数标识符(从 0 到 49999),例如[72,3335,288]。 此时,你已准备好使用占位符将这些单词标识符提供给 TensorFlow,并应用embedding_lookup()函数来获取相应的嵌入:

train_inputs = tf.placeholder(tf.int32, shape=[None])  # from ids...
embed = tf.nn.embedding_lookup(embeddings, train_inputs)  # ...to embeddings

一旦你的模型习得了良好的词嵌入,它们实际上可以在任何 NLP 应用中高效复用:毕竟,"milk"依然接近于"water",而且不管你的应用是什么,它都不同于"shoes"。 实际上,你可能需要下载预训练的单词嵌入,而不是训练自己的单词嵌入。 就像复用预训练层(参见第 11 章)一样,你可以选择冻结预训练嵌入(例如,使用trainable=False创建嵌入变量),或者让反向传播为你的应用调整它们。 第一种选择将加速训练,但第二种选择可能会产生稍高的性能。

提示


对于表示可能拥有大量不同值的类别属性,嵌入也很有用,特别是当值之间存在复杂的相似性的时候。 例如,考虑职业,爱好,菜品,物种,品牌等。

你现在拥有了实现机器翻译系统所需的几乎所有的工具。 现在我们来看看它吧。


用于机器翻译的编解码器网络

让我们来看看简单的机器翻译模型,它将英语句子翻译成法语(参见图 14-15)。

图 14-15:简单的机器翻译模型

英语句子被送进编码器,解码器输出法语翻译。 请注意,法语翻译也被用作解码器的输入,但后退了一步。 换句话说,解码器的输入是它应该在前一步输出的字(不管它实际输出的是什么)。 对于第一个单词,提供了表示句子开始的标记("<go>")。 解码器预期以序列末尾标记(EOS)结束句子("<eos>")。

请注意,英语句子在送入编码器之前会反转。 例如,"I drink milk""milk drink I"相反。这确保了英语句子的开头将会最后送到编码器,这很有用,因为这通常是解码器需要翻译的第一个东西。

每个单词最初由简单整数标识符表示(例如,单词"milk"为 288)。 接下来,嵌入查找返回词的嵌入(如前所述,这是一个密集的,相当低维的向量)。 这些词的嵌入是实际送到编码器和解码器的内容。

在每个步骤中,解码器输出输出词汇表(即法语)中每个词的得分,然后 Softmax 层将这些得分转换为概率。 例如,在第一步中,单词"Je"有 20% 的概率,"Tu"有 1% 的概率,以此类推。 概率最高的词会输出。 这非常类似于常规分类任务,因此你可以使用softmax_cross_entropy_with_logits()函数来训练模型。

请注意,在推断期间(训练之后),你不再将目标句子送入解码器。 相反,只需向解码器提供它在上一步输出的单词,如图 14-16 所示(这将需要嵌入查找,它未在图中显示)。

图 14-16:在推断期间,将之前的输出单词提供为输入

好的,现在你有了大方向。 但是,如果你阅读 TensorFlow 的序列教程,并查看rnn/translate/seq2seq_model.py中的代码(在 TensorFlow 模型中),你会注意到一些重要的区别:

  • 首先,到目前为止,我们已经假定所有输入序列(编码器和解码器的)具有恒定的长度。但显然句子长度可能会有所不同。有几种方法可以处理它 - 例如,使用static_rnn()dynamic_rnn()函数的sequence_length参数,来指定每个句子的长度(如前所述)。然而,教程中使用了另一种方法(大概是出于性能原因):句子分到长度相似的桶中(例如,句子的单词 1 到 6 分到一个桶,单词 7 到 12 分到另一个桶,等等),并且使用特殊的填充标记(例如"<pad>")来填充较短的句子。例如,"I drink milk"变成"<pad> <pad> <pad> milk drink I",翻译成"Je bois du lait <eos> <pad>"。当然,我们希望忽略任何 EOS 标记之后的输出。为此,本教程的实现使用target_weights向量。例如,对于目标句子"Je bois du lait <eos> <pad>",权重将设置为[1.0,1.0,1.0,1.0,1.0,0.0](注意权重 0.0 对应目标句子中的填充标记)。简单地将损失乘以目标权重,将消除对应 EOS 标记之后的单词的损失。

  • 其次,当输出词汇表很大时(就是这里的情况),输出每个可能的单词的概率将会非常慢。 如果目标词汇表包含 50,000 个法语单词,则解码器将输出 50,000 维向量,然后在这样的大向量上计算 softmax 函数,计算量将非常大。 为了避免这种情况,一种解决方案是让解码器输出更小的向量,例如 1,000 维向量,然后使用采样技术来估计损失,而不必对目标词汇表中的每个单词计算它。 这种采样 Softmax 技术是由 SébastienJean 等人在 2015 年提出的。在 TensorFlow 中,你可以使用sampled_softmax_loss()函数。

  • 第三,教程的实现使用了一种注意力机制,让解码器能够窥视输入序列。 注意力增强的 RNN 不在本书的讨论范围之内,但如果你有兴趣,可以关注机器翻译,机器阅读和图像说明的相关论文。

  • 最后,本教程的实现使用了tf.nn.legacy_seq2seq模块,该模块提供了轻松构建各种编解码器模型的工具。 例如,embedding_rnn_seq2seq()函数会创建一个简单的编解码器模型,它会自动为你处理单词嵌入,就像图 14-15 中所示的一样。 此代码可能会很快更新,来使用新的tf.nn.seq2seq模块。

你现在拥有了,了解所有 seq2seq 教程的实现所需的全部工具。 将它们取出,并训练你自己的英法翻译器吧!

练习

  1. 你能想象 seq2seq RNN 的几个应用吗? seq2vec 的 RNN 呢?vex2seq 的 RNN 呢?

  2. 为什么人们使用编解码器 RNN 而不是简单的 seq2seq RNN 来自动翻译?

  3. 如何将卷积神经网络与 RNN 结合,来对视频进行分类?

  4. 使用dynamic_rnn()而不是static_rnn()构建 RNN 有什么好处?

  5. 你如何处理长度可变的输入序列? 那么长度可变输出序列呢?

  6. 在多个 GPU 上分配深层 RNN 的训练和执行的常见方式是什么?

  7. Hochreiter 和 Schmidhuber 在其关于 LSTM 的文章中使用了嵌入式 Reber 语法。 它们是产生字符串,如"BPBTSXXVPSEPE"的人造语法。查看 Jenny Orr 对此主题的不错的介绍。 选择一个特定的嵌入式 Reber 语法(例如 Jenny Orr 页面上显示的语法),然后训练一个 RNN 来确定字符串是否遵循该语法。 你首先需要编写一个函数,该函数能够生成训练批量,包含大约 50% 遵循语法的字符串,以及 50% 不遵循的字符串。

  8. 解决“How much did it rain? II”(下雨下了多久 II)Kaggle 比赛。 这是一个时间序列预测任务:它为你提供极化雷达值的快照,并要求预测每小时降水量。 Luis Andre Dutra e Silva 的采访对他在比赛中获得第二名的技术,提供了一些有趣的见解。 特别是,他使用了由两个 LSTM 层组成的 RNN。

  9. 通过 TensorFlow 的 Word2Vec 教程来创建单词嵌入,然后通过 Seq2Seq 教程来训练英法翻译系统。

附录 A 提供了这些练习的答案。

Python爱好者社区历史文章大合集:

Python爱好者社区历史文章列表(每周append更新一次)

福利:文末扫码立刻关注公众号,“Python爱好者社区”,开始学习Python课程:

关注后在公众号内回复“课程”即可获取:

小编的Python入门免费视频课程!!!

【最新免费微课】小编的Python快速上手matplotlib可视化库!!!

崔老师爬虫实战案例免费学习视频。

陈老师数据分析报告制作免费学习视频。

玩转大数据分析!Spark2.X+Python 精华实战课程免费学习视频。

推荐 0
本文由 Python爱好者社区 创作,采用 知识共享署名-相同方式共享 3.0 中国大陆许可协议 进行许可。
转载、引用前需联系作者,并署名作者且注明文章出处。
本站文章版权归原作者及原出处所有 。内容为作者个人观点, 并不代表本站赞同其观点和对其真实性负责。本站是一个个人学习交流的平台,并不用于任何商业目的,如果有任何问题,请及时联系我们,我们将根据著作权人的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。

0 个评论

要回复文章请先登录注册