第三部分:针对复杂问题的高级深度网络
自从卷积神经网络、LSTM 等模型问世以来,深度学习已经取得了长足的进展。亿级参数的 Transformer 模型在各方面表现都优于前面提到的模型。由于对更好的模型和快速开发机器学习模型的需求,跟踪和生产化机器学习模型是另一个备受关注的话题。
在第三部分中,我们首先讨论基于 RNN 的模型的一种更复杂的变体——序列到序列模型。然后我们将更详细地讨论基于 Transformer 的模型,并亲身感受它们在垃圾邮件分类和问答等任务中的运用。您还将学习如何利用像 Hugging Face 的 Transformers 这样的高级库快速实现解决方案。
接下来,您将学习如何使用 TensorBoard 跟踪模型的性能。您将学习如何轻松地在时间轴上可视化模型性能,以及性能分析等高级功能。最后,我们将介绍 TFX,这是一个标准化机器学习模型生产化的库。您将开发一个端到端管道,从数据到部署全面管理机器学习工作流程。
第十一章:序列到序列学习:第一部分
本章内容包括:
- 理解序列到序列数据
- 构建序列到序列机器翻译模型
- 训练和评估序列到序列模型
- 将训练的模型用于生成未见过的文本的翻译
在上一章中,我们探讨了使用深度递归神经网络解决自然语言处理任务的语言建模。在本章中,我们将进一步探讨如何使用递归神经网络解决更复杂的任务。我们将学习各种任务,其中任意长度的输入序列映射到另一个任意长度的序列。机器翻译是这种情况的一个非常适当的例子,它涉及将一种语言中的单词序列转换为另一种语言的单词序列。
此章节的主要目的是构建一个英德机器翻译器。我们首先需要下载一个机器翻译数据集,了解该数据集的结构并进行一些处理以准备好模型。接下来我们将定义一个可以将任意长的序列映射到另一个任意长的序列的机器翻译模型,这是一个基于编码器-解码器的模型,意味着有一个编码器将一个序列(例如一个英语短语)输出为一个潜在表示,并且有一个解码器来解码这个信息以生成目标序列(例如一个德语短语)。此模型的一个特殊特点是其能够内部将原始字符串转换为数值表示。因此,与我们在先前章节创建的其他自然语言处理模型相比,此模型更为全面。定义好模型后,我们将使用处理过的数据集进行训练,并评估其生成序列的每个单词的准确性以及 BLEU(双语评估研究)。BLEU 是一种比准确性更高级的度量,可以模拟人类评估翻译质量的方式。最后,我们将定义一个略微修改过的解码器,该解码器可以递归地生成单词(从一个初始种子开始),同时将前一个预测作为当前时间步的输入。在第一部分中,我们将讨论机器翻译数据,然后深入探讨建模。
11.1 理解机器翻译数据
您正在为前往德国的游客开发一项机器翻译服务。您找到了一份包含英语和德语文本的双语平行语料库(可在www.manythings.org/anki/deu-eng.zip
找到)。它在文本文件中并排包含英语文本和相应的德语翻译。这个想法是使用它来训练一个序列到序列模型,在这之前,您必须了解数据的组织方式,将其加载到内存中,并分析词汇量和序列长度。此外,您将处理文本,使其在德语翻译的开头具有特殊标记“sos”(表示“句子开始”)并在翻译的结尾具有“eos”(表示“句子结束”)。这些是重要的标记,在生成模型的翻译时将对我们有所帮助。
让我们首先下载数据集并对其进行浏览。您需要手动下载此数据集(可在www.manythings.org/anki/deu-eng.zip
找到),因为此网页不支持通过脚本进行自动检索。下载后,我们将提取包含数据的文本文件:
import os import requests import zipfile # Make sure the zip file has been downloaded if not os.path.exists(os.path.join('data','deu-eng.zip')): raise FileNotFoundError( "Uh oh! Did you download the deu-eng.zip from ➥ http:/ /www.manythings.org/anki/deu-eng.zip manually and place it in the ➥ Ch11/data folder?" ) else: if not os.path.exists(os.path.join('data', 'deu.txt')): with zipfile.ZipFile(os.path.join('data','deu-eng.zip'), 'r') as zip_ref: zip_ref.extractall('data') else: print("The extracted data already exists")
如果你打开文本文件,它将有以下条目:
Go. Geh. CC-BY 2.0 (France) Attribution: tatoeba.org ➥ #2877272 (CM) & #8597805 (Roujin) Hi. Hallo! CC-BY 2.0 (France) Attribution: tatoeba.org ➥ #538123 (CM) & #380701 (cburgmer) Hi. Grüß Gott! CC-BY 2.0 (France) Attribution: ➥ tatoeba.org #538123 (CM) & #659813 (Esperantostern) ... If someone who doesn't know your background says that you sound like ➥ a native speaker, ... . In other words, you don't really sound like ➥ a native speaker. Wenn jemand, der nicht weiß, woher man ➥ kommt, sagt, man erwecke doch den Eindruck, Muttersprachler zu sein, ➥ ... - dass man diesen Eindruck mit anderen Worten eigentlich nicht ➥ erweckt. CC-BY 2.0 (France) Attribution: tatoeba.org #953936 ➥ (CK) & #8836704 (Pfirsichbaeumchen) Doubtless there exists in this world precisely the right woman for ➥ any given man to marry and vice versa; ..., that probably, since ➥ the earth was created, the right man has never yet met the right ➥ woman. Ohne Zweifel findet sich auf dieser Welt zu jedem Mann ➥ genau die richtige Ehefrau und umgekehrt; ..., dass seit Erschaffung ➥ ebenderselben wohl noch nie der richtige Mann der richtigen Frau ➥ begegnet ist. CC-BY 2.0 (France) Attribution: tatoeba.org ➥ #7697649 (RM) & #7729416 (Pfirsichbaeumchen)
数据以制表符分隔的格式呈现,并具有<德语短语><制表符><英语短语><制表符><归属>格式。我们真正关心记录中的前两个以制表符分隔的值。一旦数据下载完成,我们就可以轻松地将数据加载到 pandas DataFrame 中。在这里,我们将加载数据,设置列名,并提取我们感兴趣的列:
import pandas as pd # Read the csv file df = pd.read_csv( os.path.join('data', 'deu.txt'), delimiter='\t', header=None ) # Set column names df.columns = ["EN", "DE", "Attribution"] df = df[["EN", "DE"]]
我们还可以通过以下方式计算 DataFrame 的大小
print('df.shape = {}'.format(df.shape))
这将返回
df.shape = (227080, 2)
注意:这里的数据会随着时间而更新。因此,您可能会得到与此处显示的略有不同的结果(例如,数据集大小,词汇量,词汇分布等)。
我们的数据集中有约 227,000 个示例。每个示例都包含一个英语短语/句子/段落和相应的德语翻译。我们将再进行一次清理步骤。看起来文本文件中的一些条目存在一些 Unicode 问题。这些问题对于 pandas 来说处理得很好,但对于一些下游 TensorFlow 组件来说会有问题。因此,让我们运行以下清理步骤来忽略数据中的这些问题行:
clean_inds = [i for i in range(len(df)) if b"\xc2" not in df.iloc[i]["DE"].encode("utf-8")] df = df.iloc[clean_inds]
让我们通过调用 df.head()(表 11.1)和 df.tail()(表 11.2)来分析一些示例。df.head()返回表 11.1 的内容,而 df.tail()生成表 11.2 的内容。
表 11.1 数据开头的一些示例
EN | DE | |
0 | Go. | Geh. |
1 | Hi. | Hallo! |
2 | Hi. | Grüß Gott! |
3 | Run! | Lauf! |
4 | Run. | Lauf! |
表 11.2 数据结尾的一些示例
EN | DE | |
227075 | Even if some by non-native speakers… | Auch wenn Sätze von Nichtmuttersprachlern mitu… |
227076 | 如果一个不了解你的背景的人… | 如果一个不了解你的背景的人… |
227077 | 如果一个不了解你的背景的人… | 如果一个陌生人告诉你要按照他们… |
227078 | 如果一个不了解你的背景的人… | 如果一个不知道你来自哪里的人… |
227079 | 这个世界上肯定存在… | 毫无疑问,这个世界上肯定存在… |
示例按长度排序,你可以看到它们从一个单词的示例开始,然后以大约 50 个单词的示例结束。我们将只使用来自该数据集的 50,000 个短语的样本来加快工作流程:
n_samples = 50000 df = df.sample(n=n_samples, random_state=random_seed)
我们设置随机种子为 random_seed=4321。
最后,我们将在德语翻译中引入两个特殊标记:sos 和 eos。sos 标记翻译的开始,eos 标记翻译的结束。正如您将看到的,这些标记在训练后生成翻译时起着重要作用。但为了在训练和推断(或生成)期间保持一致,我们将这些标记引入到所有示例中。可以使用以下方式轻松完成此操作:
start_token = 'sos' end_token = 'eos' df["DE"] = start_token + ' ' + df["DE"] + ' ' + end_token
SOS 和 EOS 标记
SOS 和 EOS 的选择只是一种便利,从技术上讲,它们可以由任何两个唯一的标记表示,只要它们不是语料库本身的词汇。使这些标记唯一是重要的,因为当从以前未见过的英文句子生成翻译时,它们起着重要作用。这些作用的具体细节将在后面的部分中讨论。
这是一个非常直接的转换。这将把短语“Grüß Gott!”转换为“sos Grüß Gott!eos”。接下来,我们将从我们抽样的数据中生成一个训练/验证/测试子集:
# Randomly sample 10% examples from the total 50000 randomly test_df = df.sample(n=n=int(n_samples/10), random_state=random_seed) # Randomly sample 10% examples from the remaining randomly valid_df = df.loc[~df.index.isin(test_df.index)].sample( n=n=int(n_samples/10), random_state=random_seed ) # Assign the rest to training data train_df = df.loc[~(df.index.isin(test_df.index) | ➥ df.index.isin(valid_df.index))]
我们将把数据的 10%保留为测试数据,另外 10%保留为验证数据,剩下的 80%作为训练数据。数据集将随机抽样(无替换)以得到数据集。然后我们继续分析文本数据集的两个重要特征,就像我们一遍又一遍地做的那样:词汇大小(列表 11.1)和序列长度(列表 11.2)。
列表 11.1 分析词汇大小
from collections import Counter en_words = train_df["EN"].str.split().sum() ❶ de_words = train_df["DE"].str.split().sum() ❷ n=10 ❸ def get_vocabulary_size_greater_than(words, n, verbose=True): """ Get the vocabulary size above a certain threshold """ counter = Counter(words) ❹ freq_df = pd.Series( ❺ list(counter.values()), index=list(counter.keys()) ).sort_values(ascending=False) if verbose: print(freq_df.head(n=10)) ❻ n_vocab = (freq_df>=n).sum() ❼ if verbose: print("\nVocabulary size (>={} frequent): {}".format(n, n_vocab)) return n_vocab print("English corpus") print('='*50) en_vocab = get_vocabulary_size_greater_than(en_words, n) print("\nGerman corpus") print('='*50) de_vocab = get_vocabulary_size_greater_than(de_words, n)
❶ 从英文单词中创建一个扁平化列表。
❷ 创建一个扁平化的德文单词列表。
❸ 获取出现次数大于或等于 10 次的单词的词汇大小。
❹ 生成一个计数器对象(即 dict word -> frequency)。
❺ 从计数器创建一个 pandas 系列,然后按最频繁到最不频繁排序。
❻ 打印最常见的单词。
❼ 获取至少出现 10 次的单词的计数。
这将返回
English corpus ================================================== Tom 9427 to 8673 I 8436 the 6999 you 6125 a 5680 is 4374 in 2664 of 2613 was 2298 dtype: int64 Vocabulary size (>=10 frequent): 2238 German corpus ================================================== sos 40000 eos 40000 Tom 9928 Ich 7749 ist 4753 nicht 4414 zu 3583 Sie 3465 du 3112 das 2909 dtype: int64 Vocabulary size (>=10 frequent): 2497
接下来,在以下函数中进行序列分析。
列表 11.2 分析序列长度
def print_sequence_length(str_ser): """ Print the summary stats of the sequence length """ seq_length_ser = str_ser.str.split(' ').str.len() ❶ print("\nSome summary statistics") ❷ print("Median length: {}\n".format(seq_length_ser.median())) ❷ print(seq_length_ser.describe()) ❷ print( "\nComputing the statistics between the 1% and 99% quantiles (to ➥ ignore outliers)" ) p_01 = seq_length_ser.quantile(0.01) ❸ p_99 = seq_length_ser.quantile(0.99) ❸ print( seq_length_ser[ (seq_length_ser >= p_01) & (seq_length_ser < p_99) ].describe() ❹ )
❶ 创建包含每个评论的序列长度的 pd.Series。
❷ 获取序列长度的中位数以及摘要统计信息。
❸ 获取给定标记(即 1%和 99%的百分位)的分位数。
❹ 打印定义的分位数之间的数据的摘要统计信息。
接下来,对数据调用此函数以获取统计信息:
print("English corpus") print('='*50) print_sequence_length(train_df["EN"]) print("\nGerman corpus") print('='*50) print_sequence_length(train_df["DE"])
这产生
English corpus ================================================== Some summary statistics Median length: 6.0 count 40000.000000 mean 6.360650 std 2.667726 min 1.000000 25% 5.000000 50% 6.000000 75% 8.000000 max 101.000000 Name: EN, dtype: float64 Computing the statistics between the 1% and 99% quantiles (to ignore outliers) count 39504.000000 mean 6.228002 std 2.328172 min 2.000000 25% 5.000000 50% 6.000000 75% 8.000000 max 14.000000 Name: EN, dtype: float64 German corpus ================================================== Some summary statistics Median length: 8.0 count 40000.000000 mean 8.397875 std 2.652027 min 3.000000 25% 7.000000 50% 8.000000 75% 10.000000 max 77.000000 Name: DE, dtype: float64 Computing the statistics between the 1% and 99% quantiles (to ignore outliers) count 39166.000000 mean 8.299035 std 2.291474 min 5.000000 25% 7.000000 50% 8.000000 75% 10.000000 max 16.000000 Name: DE, dtype: float64
接下来,让我们打印出两种语言的词汇量和序列长度参数:
print("EN vocabulary size: {}".format(en_vocab)) print("DE vocabulary size: {}".format(de_vocab)) # Define sequence lengths with some extra space for longer sequences en_seq_length = 19 de_seq_length = 21 print("EN max sequence length: {}".format(en_seq_length)) print("DE max sequence length: {}".format(de_seq_length))
这将返回
EN vocabulary size: 359 DE vocabulary size: 336 EN max sequence length: 19 DE max sequence length: 21
现在我们有了定义模型所需的语言特定参数。在下一节中,我们将看看如何定义一个能够在语言之间进行翻译的模型。
练习 1
您已经获得了以下格式的 pandas Series ser:
0 [a, b, c] 1 [d, e] 2 [f, g, h, i] ... dtype: object
编写一个名为 vocab_size(ser)的函数来返回词汇量。
11.2 编写英语-德语 seq2seq 机器翻译器
您有一个准备进入模型的干净数据集。您将使用一个序列到序列的深度学习模型作为机器翻译模型。它由两部分组成:一个编码器,用于生成英文(源)文本的隐藏表示,以及一个解码器,用于解码该表示以生成德文(目标)文本。编码器和解码器都是循环神经网络。此外,模型将接受原始文本,并使用 TensorFlow 提供的 TextVectorization 层将原始文本转换为令牌 ID。这些令牌 ID 将传递给一个嵌入层,该层将返回令牌 ID 的单词向量。
我们已经准备好并准备好使用的数据。现在让我们了解一下可以使用此数据的模型。序列到序列学习将任意长的序列映射到另一个任意长的序列。对于我们来说,这提出了一个独特的挑战,因为模型不仅需要能够消耗任意长度的序列,还需要能够生成任意长度的序列作为输出。例如,在机器翻译中,翻译通常比输入的单词少或多。因此,它们需要一种特殊类型的模型。这些模型被称为编码器-解码器或seq2seq(缩写为序列到序列)模型。
编码器-解码器模型实际上是两个不同的模型以某种方式相互连接起来。在概念上,编码器接受一个序列并产生一个上下文向量(或思考向量),其中嵌入了输入序列中的信息。解码器接受编码器产生的表示,并对其进行解码以生成另一个序列。由于两个部分(即编码器和解码器)分别在不同的事物上操作(即编码器消耗输入序列,而解码器生成输出序列),因此编码器-解码器模型非常适合解决序列到序列的任务。理解编码器和解码器的另一种方式是:编码器处理源语言输入(即要翻译的语言),解码器处理目标语言输入(即要翻译成的语言)。如图 11.1 所示。
图 11.1 编码器-解码器架构在机器翻译环境中的高级组件
特别地,编码器包含一个循环神经网络。我们将使用门控循环单元(GRU)模型。它通过输入序列并产生一个最终输出,这是在处理输入序列中的最后一个元素之后 GRU 单元的最终输出。
思想向量
思想向量 是由深度学习领域的泰斗杰弗里·亨滕(Geoffery Hinten)推广的一个术语,他从深度学习的起源就参与其中。思想向量指的是思想的向量化表示。生成准确的思想数值表示将彻底改变我们搜索文档或在网络上搜索(例如,谷歌)的方式。这类似于数值表示单词被称为 单词向量 的方式。在机器翻译的背景下,上下文向量可以称为思想向量,因为它在一个向量中捕捉了句子或短语的本质。
您可以在wiki.pathmind.com/thought-vectors
阅读更多关于此的信息。
接下来,我们有解码器,它也由一个 GRU 模型和几个密集层组成。密集层的目的是生成最终的预测(目标词汇中的一个词)。解码器中存在的密集层的权重在时间上是共享的。这意味着,正如 GRU 层在从一个输入移动到另一个输入时更新相同的权重一样,密集层在时间步上重复使用相同的权重。这个过程在图 11.2 中有所描述。
图 11.2 编码器和解码器模块中的特定组件。编码器有一个 GRU 层,解码器由一个或多个密集层后跟的 GRU 层组成,其权重在时间上是共享的。
到目前为止,在解决 NLP 任务时,将字符串标记转换为数值 ID 被认为是预处理步骤。换句话说,我们会执行标记到 ID 的转换,并将 ID 输入模型。但它并不一定要这样。我们可以定义更加灵活的模型,让这种文本处理在内部完成并学会解决任务。Keras 提供了一些层,可以插入到您的模型中,以使其更加端到端。tensorflow.keras.layers.experimental.preprocessing.TextVectorization 层就是这样一种层。让我们来看看这个层的用法。
TensorFlow 实战(五)(2)https://developer.aliyun.com/article/1522831