<jupyter_start><jupyter_text>使用 RNN 和注意力的自然语言处理 当艾伦图灵在 1950 年想象他著名的图灵测试时，他的目标是评估机器匹配人类智能的能力。他本可以测试很多东西，例如识别图片中的猫、下棋、作曲或逃离迷宫的能力，但有趣的是，他选择了一项语言任务。更具体地说，他设计了一个聊天机器人，能够欺骗对话者认为它是人类。这个测试确实有它的弱点：一组硬编码规则可以愚弄毫无戒心或天真的人类（例如，机器可以针对某些关键字给出模糊的预定义答案；它可以假装在开玩笑或喝醉了，以通过其最奇怪的答案；或者它可以通过用自己的问题回答难题来逃避难题），并且人类智能的许多方面都被完全忽略了（例如，解释面部表情等非语言交流的能力，或学习手动任务的能力）。但该测试确实强调了一个事实，即掌握语言可以说是智人最大的认知能力。我们可以建造一台可以读写自然语言的机器吗？ 自然语言任务的常见方法是使用循环神经网络。因此，我们将继续探索 RNN（在第 15 章中介绍），从字符 RNN 开始，经过训练以预测句子中的下一个字符。这将允许我们生成一些原始文本，在这个过程中我们将看到如何在一个很长的序列上构建一个 TensorFlow 数据集。我们将首先使用一个无状态的 RNN（它在每次迭代中学习文本的随机部分，没有关于文本其余部分的任何信息），然后我们将构建一个有状态的 RNN（它保留训练迭代之间的隐藏状态并继续阅读哪里它停止了，允许它学习更长的模式）。接下来，我们将构建一个 RNN 来执行情感分析（例如，阅读电影评论并提取评分者对电影的感觉），这次将句子视为单词序列，而不是字符。然后我们将展示如何使用 RNN 来构建能够执行神经机器翻译 (NMT) 的编码器-解码器架构。为此，我们将使用 TensorFlow Addons 项目提供的 seq2seq API。 在本章的第二部分，我们将研究注意力机制。 顾名思义，这些是神经网络组件，它们学会选择模型的其余部分在每个时间步应该关注的输入部分。 首先，我们将看到如何使用注意力来提高基于 RNN 的编码器-解码器架构的性能，然后我们将完全放弃 RNN，看看一个非常成功的仅注意力架构，称为 Transformer。 最后，我们将看看 2018 年和 2019 年 NLP 的一些最重要的进步，包括非常强大的语言模型，例如 GPT-2 和 BERT，它们都基于 Transformers。让我们从一个简单而有趣的模型开始，它可以像莎士比亚一样写作（嗯，有点）。  使用字符 RNN 生成莎士比亚文本 在 2015 年一篇名为“循环神经网络的不合理有效性”的著名博客文章中，<NAME> 展示了如何训练 RNN 来预测句子中的下一个字符。 然后可以使用这个 Char-RNN 生成新颖的文本，一次一个字符。 这是 Char-RNN 模型在对莎士比亚的所有作品进行训练后生成的一小部分文本示例：```PANDARUS:Alas, I think he shall be come approached and the dayWhen little srain would be attain’d into being never fed,And who is but a chain and subjects of his death,I should not sleep.``` 不完全是杰作，但令人印象深刻的是，该模型仅通过学习预测句子中的下一个字符就能够学习单词、语法、正确的标点符号等。 让我们从创建数据集开始，一步一步地看看如何构建 Char-RNN。  创建训练数据集 首先，让我们下载莎士比亚的所有作品，使用 Keras 方便的 get_file() 函数并从 Andrej Karpathy 的 Char-RNN 项目下载数据：<jupyter_code>shakespeare_url = "https://homl.info/shakespeare" # shortcut URL

filepath = keras.utils.get_file("shakespeare.txt", shakespeare_url)

with open(filepath) as f:
    shakespeare_text = f.read()<jupyter_output>time: 3.57 ms (started: 2021-08-21 04:40:54 +08:00)<jupyter_text>接下来，我们必须将每个字符编码为一个整数。 一种选择是创建一个自定义的预处理层，就像我们在第 13 章中所做的那样。但在这种情况下，使用 Keras 的 Tokenizer 类会更简单。 首先，我们需要为文本拟合一个分词器：它会找到文本中使用的所有字符，并将每个字符映射到不同的字符 ID，从 1 到不同字符的数量（它不是从 0 开始，所以我们 可以使用该值进行屏蔽，正如我们将在本章后面看到的那样）：<jupyter_code>tokenizer = keras.preprocessing.text.Tokenizer(char_level=True)
tokenizer.fit_on_texts([shakespeare_text])<jupyter_output>time: 246 ms (started: 2021-08-21 04:40:57 +08:00)<jupyter_text>我们设置 `char_level=True` 以获得字符级编码而不是默认的词级编码。 请注意，此标记器默认将文本转换为小写（但如果您不想要，可以设置 `lower=False`）。 现在分词器可以将一个句子（或一个句子列表）编码为一个字符 ID 列表并返回，它会告诉我们有多少不同的字符以及文本中的字符总数：<jupyter_code>tokenizer.texts_to_sequences(["First"])
tokenizer.sequences_to_texts([[20, 6, 9, 8, 3]])
max_id = len(tokenizer.word_index) # number of distinct characters
max_id
dataset_size = tokenizer.document_count # total number of characters
dataset_size
import numpy as np
np.sum(list(tokenizer.word_counts.values()))<jupyter_output><empty_output><jupyter_text>让我们对全文进行编码，以便每个字符都由其 ID 表示（我们减去 1 以获得从 0 到 38 的 ID，而不是从 1 到 39）：<jupyter_code>[encoded] = np.array(tokenizer.texts_to_sequences([shakespeare_text])) - 1
encoded.shape<jupyter_output><empty_output><jupyter_text>在继续之前，我们需要将数据集拆分为训练集、验证集和测试集。 我们不能只打乱文本中的所有字符，那么如何拆分顺序数据集？  如何拆分顺序数据集 避免训练集、验证集和测试集之间的任何重叠非常重要。 例如，我们可以将前 90% 的文本作为训练集，接下来的 5% 作为验证集，最后的 5% 作为测试集。 在这些集合之间留出空隙也是一个好主意，以避免段落重叠超过两个集合的风险。 在处理时间序列时，您通常会按时间划分：例如，您可能将 2000 年至 2012 年作为训练集，将 2013 年至 2015 年作为验证集，将 2016 年至 2018 年作为测试集 设置。 但是，在某些情况下，您可能可以沿其他维度拆分，这将为您提供更长的训练时间。 例如，如果您拥有 10,000 家公司从 2000 年到 2018 年的财务状况数据，您或许可以将这些数据拆分到不同的公司。 但是，这些公司中的许多公司很可能会高度相关（例如，整个经济部门可能会共同上升或下降），并且如果您在训练集和测试集上关联了公司，则您的测试集将不会那么有用 ，因为它对泛化误差的度量将具有乐观偏差。 因此，跨时间分割通常更安全——但这隐含地假设 RNN 过去（在训练集中）可以学习的模式在未来仍然存在。 换句话说，我们假设时间序列是平稳的（至少在广义上是这样）。 对于许多时间序列，这个假设是合理的（例如，化学反应应该没问题，因为化学定律不会每天都在变化），但对于许多其他时间序列来说却不是（例如，众所周知，金融市场不是静止的，因为模式随着时间的推移而消失） 一旦交易者发现它们并开始利用它们）。 为了确保时间序列确实足够平稳，您可以在验证集上绘制模型随时间的误差：如果模型在验证集的第一部分比在最后一部分的表现好得多，那么时间序列可能不会 足够稳定，你最好在更短的时间跨度内训练模型。 简而言之，将时间序列分成训练集、验证集和测试集并不是一项微不足道的任务，如何完成将在很大程度上取决于手头的任务。现在回到莎士比亚！ 让我们将前 90% 的文本作为训练集（保留其余的用于验证集和测试集），并创建一个 tf.data.Dataset 将一个一个地返回该集中的每个字符：<jupyter_code>train_size = dataset_size * 90 // 100

dataset = tf.data.Dataset.from_tensor_slices(encoded[:train_size])<jupyter_output>time: 2.17 ms (started: 2021-08-21 04:41:18 +08:00)<jupyter_text>将序列数据集切分成多个窗口 训练集现在由超过一百万个字符的单个序列组成，因此我们不能直接在其上训练神经网络：RNN 将相当于具有超过一百万层的深度网络，我们将有一个 （很长）实例来训练它。 相反，我们将使用数据集的 window() 方法将这个长字符序列转换为许多较小的文本窗口。 数据集中的每个实例都是整个文本的一个相当短的子串，RNN 只会在这些子串的长度上展开。 这称为随时间截断的反向传播。 让我们调用 window() 方法来创建一个短文本窗口的数据集：<jupyter_code>n_steps = 100
window_length = n_steps + 1 # target = input shifted 1 character ahead

dataset = dataset.window(window_length, shift=1, drop_remainder=True)<jupyter_output>time: 1.91 ms (started: 2021-08-21 04:41:21 +08:00)<jupyter_text>window---将输入元素的（嵌套）组合成窗口（嵌套）的数据集。“窗口”是大小为 `size` 的平面元素的有限数据集（如果没有足够的输入元素来填充窗口，并且 `drop_remainder` 评估为 `False`，则可能更少）。`shift` 参数确定窗口在每次迭代中移动的输入元素的数量。 如果窗口和元素都从 0 开始编号，则窗口 `k` 中的第一个元素将是输入数据集的元素 `k * shift`。 特别是，第一个窗口的第一个元素将始终是输入数据集的第一个元素。`stride` 参数确定输入元素的步幅，而 `shift` 参数确定窗口的移位。--- 您可以尝试调整 n_steps：在较短的输入序列上训练 RNN 会更容易，但是 RNN 当然无法学习比 n_steps 更长的任何模式，所以不要让它太小。<jupyter_code>i=0
for window in dataset:
    i+=1
    print(list(window.as_numpy_iterator()))
    if i>=3:
        break<jupyter_output>time: 9.47 ms (started: 2021-08-21 04:41:24 +08:00)<jupyter_text>默认情况下，window() 方法创建不重叠的窗口，但为了获得尽可能大的训练集，我们使用 shift=1 以便第一个窗口包含字符 0 到 100，第二个窗口包含字符 1 到 101，依此类推。 为确保所有窗口的长度恰好为 101 个字符（这将允许我们创建批次而无需进行任何填充），我们设置 drop_remainder=True（否则最后 100 个窗口将包含 100 个字符、99 个字符，依此类推到 1 个字符）。 window() 方法创建一个包含窗口的数据集，每个窗口也表示为一个数据集。它是一个嵌套数据集，类似于列表列表。当您想通过调用其数据集方法（例如，对它们进行混洗或批处理）来转换每个窗口时，这很有用。但是，我们不能直接使用嵌套数据集进行训练，因为我们的模型需要张量作为输入，而不是数据集。因此，我们必须调用 flat_map() 方法：它将嵌套数据集转换为平面数据集（不包含数据集的数据集）。例如，假设 {1, 2, 3} 表示包含张量 1、2 和 3 序列的数据集。如果将嵌套数据集 {{1, 2}, {3, 4, 5, 6}} 展平，你会得到平面数据集 {1, 2, 3, 4, 5, 6}。此外， flat_map() 方法将一个函数作为参数，它允许您在展平之前转换嵌套数据集中的每个数据集。例如，如果您将函数 lambda ds: ds.batch(2) 传递给 flat_map()，那么它会将嵌套数据集 {{1, 2}, {3, 4, 5, 6}} 转换为平面数据集{[1, 2], [3, 4], [5, 6]}：这是一个大小为 2 的张量数据集。考虑到这一点，我们准备扁平化我们的数据集：<jupyter_code>dataset = dataset.flat_map(lambda window: window.batch(window_length))
dataset<jupyter_output><empty_output><jupyter_text>请注意，我们在每个窗口上调用 batch(window_length)：由于所有窗口都具有该长度，因此我们将为每个窗口获得一个张量。 现在数据集包含每个 101 个字符的连续窗口。 由于梯度下降在训练集中的实例是独立且同分布的情况下效果最好（参见第 4 章），我们需要对这些窗口进行打乱。 然后我们可以批处理窗口并将输入（前 100 个字符）与目标（最后一个字符）分开：<jupyter_code>batch_size = 32
dataset = dataset.shuffle(10000).batch(batch_size)
dataset = dataset.map(lambda windows: (windows[:, :-1], windows[:, 1:]))<jupyter_output>time: 40.4 ms (started: 2021-08-21 04:41:39 +08:00)<jupyter_text>图 16-1 总结了迄今为止讨论的数据集准备步骤（显示的窗口长度为 11 而不是 101，批大小为 3 而不是 32）。 ![](https://tva1.sinaimg.cn/large/008i3skNgy1gtnwmnlbttj61gs0nwn0s02.jpg) 正如第 13 章所讨论的，分类输入特征通常应该被编码，通常是作为 one-hot 向量或作为嵌入。 在这里，我们将使用 one-hot 向量对每个字符进行编码，因为不同的字符很少（只有 39 个）：<jupyter_code>dataset = dataset.map(lambda X_batch, Y_batch: (tf.one_hot(X_batch, depth=max_id), Y_batch))<jupyter_output>time: 13 ms (started: 2021-08-21 04:41:41 +08:00)<jupyter_text>最后，我们只需要添加预取：<jupyter_code>dataset = dataset.prefetch(1)<jupyter_output>time: 1.41 ms (started: 2021-08-21 04:41:43 +08:00)<jupyter_text>就是这样！ 准备数据集是最困难的部分。 现在让我们创建模型。  构建和训练 Char-RNN 模型 为了根据前 100 个字符预测下一个字符，我们可以使用具有 2 个 GRU 层的 RNN，每个层有 128 个单位，输入（dropout）和隐藏状态（recurrent_dropout）都有 20% 的 dropout。 如果需要，我们可以稍后调整这些超参数。 输出层是一个时间分布的 Dense 层，就像我们在第 15 章看到的那样。这次这个层必须有 39 个单位（max_id），因为文本中有 39 个不同的字符，我们想输出每个可能字符的概率（ 在每个时间步）。 每个时间步的输出概率总和应为 1，因此我们将 softmax 激活函数应用于 Dense 层的输出。 然后我们可以使用“sparse_categorical_crossentropy”损失和 Adam 优化器来编译这个模型。 最后，我们准备好在几个时期内训练模型（这可能需要很多小时，具体取决于您的硬件）：<jupyter_code>model = keras.models.Sequential([
    keras.layers.GRU(128, 
                     return_sequences=True, 
                     input_shape=[None, max_id],
                     dropout=0.2, 
#                      recurrent_dropout=0.2
                    ),
    keras.layers.GRU(128, 
                     return_sequences=True,
                     dropout=0.2, 
#                      recurrent_dropout=0.2
                    ),
    keras.layers.TimeDistributed(keras.layers.Dense(max_id,activation="softmax"))
])

model.compile(loss="sparse_categorical_crossentropy", optimizer="adam")

history = model.fit(dataset, epochs=20)
model.summary()<jupyter_output>Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
gru (GRU)                    (None, None, 128)         64896     
_________________________________________________________________
gru_1 (GRU)                  (None, None, 128)         99072     
_________________________________________________________________
time_distributed (TimeDistri (None, None, 39)          5031      
=================================================================
Total params: 168,999
Trainable params: 168,999
Non-trainable params: 0
_________________________________________________________________
time: 3.07 ms (started: 2021-08-21 04:38:58 +08:00)<jupyter_text>使用 Char-RNN 模型 现在我们有一个模型可以预测莎士比亚所写文本中的下一个字符。 为了给它提供一些文本，我们首先需要像之前一样对其进行预处理，所以让我们为此创建一个小函数：<jupyter_code>def preprocess(texts):
    X = np.array(tokenizer.texts_to_sequences(texts)) - 1
    return tf.one_hot(X, max_id)<jupyter_output>time: 933 µs (started: 2021-08-21 04:44:59 +08:00)<jupyter_text>现在让我们使用模型来预测一些文本中的下一个字母：<jupyter_code>X_new = preprocess(["How are yo"])
Y_pred = model.predict_classes(X_new)
tokenizer.sequences_to_texts(Y_pred + 1)[0][-1] # 1st sentence, last char<jupyter_output><empty_output><jupyter_text>成功！ 模型猜对了。 现在让我们使用这个模型来生成新文本。<jupyter_code>dir(tf.keras.utils)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.12023060796645703, 'fraction_numerical': 0.04979035639412998, 'mean_word_length': 7.595495495495496, 'pattern_counts': {'":': 0, '<': 52, '<?xml version=': 0, '>': 53, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '15', 'dir': 'jupyter-structured-clean-dedup', 'id': '135910', 'license': 'Apache-2.0', 'n_tokens_mistral': 6357, 'n_tokens_neox': 6669, 'n_words': 585, 'path': 'ch16.ipynb', 'repo_name': 'ustchope/handson-ml2', 'size': '9549'}