自然语言处理

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

单词嵌入

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

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

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

  1. vocabulary_size = 50000
  2. embedding_size = 150
  3. embeddings = tf.Variable(
  4. 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()函数来获取相应的嵌入:

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

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

提示

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

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

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

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

单词嵌入 - 图1

图 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 教程的实现所需的全部工具。 将它们取出,并训练你自己的英法翻译器吧!