第九章:文本生成方法的崛起
在前几章中,我们讨论了不同的方法和技术来开发和训练生成模型。特别是在第六章“使用 GAN 生成图像”中,我们讨论了生成模型的分类以及介绍了显式和隐式类。在整本书中,我们的重点一直是在视觉空间中开发生成模型,利用图像和视频数据集。深度学习在计算机视觉领域的发展以及易于理解性是引入这样一个专注介绍的主要原因。
然而,在过去几年中,自然语言处理(NLP)或文本数据处理受到了极大的关注和研究。文本不只是另一种无结构类型的数据;其背后还有更多东西超出了表面所见。文本数据代表了我们的思想、想法、知识和交流。
在本章和下一章中,我们将专注于理解与 NLP 和文本数据的生成模型相关的概念。我们将在本章中涵盖与文本数据生成模型相关的不同概念、架构和组件,重点关注以下主题:
- 传统表示文本数据方式的简要概述
- 分布式表示方法
- 基于 RNN 的文本生成
- LSTM 变体和文本卷积
我们将介绍不同架构的内部工作原理和使文本生成用例成为可能的关键贡献。我们还将构建和训练这些架构,以更好地理解它们。读者还应该注意,虽然我们将在第九章“文本生成方法的崛起”和第十章“NLP 2.0:使用 Transformer 生成文本”中深入研究关键贡献和相关细节,但这些模型中的一些非常庞大,无法在常规硬件上进行训练。我们将在必要时利用某些高级 Python 包,以避免复杂性。
本章中呈现的所有代码片段都可以直接在 Google Colab 中运行。由于篇幅原因,未包含依赖项的导入语句,但读者可以参考 GitHub 存储库获取完整的代码:github.com/PacktPublishing/Hands-On-Generative-AI-with-Python-and-TensorFlow-2
。
在我们深入建模方面之前,让我们先了解如何表示文本数据。
表示文本
语言是我们存在中最复杂的方面之一。我们使用语言来传达我们的思想和选择。每种语言都有一个叫做字母表的字符列表,一个词汇表和一组叫做语法的规则。然而,理解和学习一门语言并不是一项微不足道的任务。语言是复杂的,拥有模糊的语法规则和结构。
文本是语言的一种表达形式,帮助我们交流和分享。这使得它成为研究的完美领域,以扩展人工智能可以实现的范围。文本是一种无结构数据,不能直接被任何已知算法使用。机器学习和深度学习算法通常使用数字、矩阵、向量等进行工作。这又引出了一个问题:我们如何为不同的与语言相关的任务表示文本?
词袋
正如我们之前提到的,每种语言都包括一个定义的字符列表(字母表),这些字符组合在一起形成单词(词汇表)。传统上,词袋(BoW)一直是表示文本信息的最流行方法之一。
词袋是将文本转换为向量形式的一种简单灵活的方法。这种转换不仅有助于从原始文本中提取特征,还使其适用于不同的算法和架构。正如其名称所示,词袋表示模型将每个单词作为一种基本的度量单位。词袋模型描述了在给定文本语料库中单词的出现情况。为了构建一个用于表示的词袋模型,我们需要两个主要的东西:
- 词汇表:从要分析的文本语料库中已知单词的集合。
- 出现的度量:根据手头的应用/任务选择的东西。例如,计算每个单词的出现次数,称为词频,就是一种度量。
与词袋模型相关的详细讨论超出了本章的范围。我们正在呈现一个高级概述,作为在本章后面引入更复杂主题之前的入门。
词袋模型被称为“袋子”,以突显其简单性和我们忽略出现次数的任何排序的事实。换句话说,词袋模型舍弃了给定文本中单词的任何顺序或结构相关信息。这听起来可能是一个很大的问题,但直到最近,词袋模型仍然是表示文本数据的一种流行和有效的选择。让我们快速看几个例子,了解这种简单方法是如何工作的。
“有人说世界将在火中终结,有人说在冰中。从我尝到的欲望中,我同意那些赞成火的人。”
Fire and Ice by Robert Frost. We'll use these few lines of text to understand how the BoW model works. The following is a step-by-step approach:
- 定义词汇表:
首先且最重要的步骤是从我们的语料库中定义一个已知单词列表。为了便于理解和实际原因,我们现在可以忽略大小写和标点符号。因此,词汇或唯一单词为 {some, say, the, world, will, end, in, fire, ice, from, what, i, have, tasted, of, desire, hold, with, those, who, favour}。
这个词汇表是一个包含 26 个词中的 21 个唯一单词的语料库。 - 定义出现的度量:一旦我们有了词汇集,我们需要定义如何衡量词汇中每个单词的出现次数。正如我们之前提到的,有很多种方法可以做到这一点。这样的一个指标就是简单地检查特定单词是否存在。如果单词不存在,则使用 0,如果存在则使用 1。因此,句子“some say ice”可以得分为:
- some: 1
- say: 1
- the: 0
- world: 0
- will: 0
- end: 0
- in: 0
- fire: 0
- ice: 1
- 因此,总向量看起来像[1, 1, 0, 0, 0, 0, 0, 0, 1]。
多年来已经发展了一些其他指标。最常用的指标是:
- 词频
- TF-IDF,如第七章,使用 GAN 进行风格转移
- 哈希化
这些步骤提供了词袋模型如何帮助我们将文本数据表示为数字或向量的高层次概述。诗歌摘录的总体向量表示如下表所示:
图 9.1:词袋表示
矩阵中的每一行对应诗歌中的一行,而词汇表中的唯一单词构成了列。因此,每一行就是所考虑文本的向量表示。
改进此方法的结果涉及一些额外的步骤。这些优化与词汇和评分方面有关。管理词汇非常重要;通常,文本语料库的规模会迅速增大。处理词汇的一些常见方法包括:
- 忽略标点符号
- 忽略大小写
- 移除常见单词(或停用词)如 a, an, the, this 等
- 使用单词的词根形式的方法,如stop代替stopping。词干提取和词形还原是两种这样的方法
- 处理拼写错误
我们已经讨论了不同的评分方法以及它们如何帮助捕捉某些重要特征。词袋模型简单而有效,是大多数自然语言处理任务的良好起点。然而,它存在一些问题,可以总结如下:
- 缺失的上下文:
正如我们之前提到的,词袋模型不考虑文本的排序或结构。通过简单地丢弃与排序相关的信息,向量失去了捕捉基础文本使用上下文的机会。例如,“我肯定”和“我怀疑我肯定”这两句话将具有相同的向量表示,然而它们表达了不同的思想。扩展词袋模型以包括 n-gram(连续词组)而不是单个词确实有助于捕捉一些上下文,但在非常有限的范围内。 - 词汇和稀疏向量:
随着语料库的规模增加,词汇量也在增加。管理词汇量大小所需的步骤需要大量的监督和手动工作。由于这种模型的工作方式,大量的词汇导致非常稀疏的向量。稀疏向量对建模和计算需求(空间和时间)造成问题。激进的修剪和词汇管理步骤在一定程度上确实有所帮助,但也可能导致重要特征的丢失。
在这里,我们讨论了词袋模型如何帮助将文本转换为向量形式,以及这种设置中的一些问题。在下一节,我们将转向一些更多涉及的表示方法,这些方法缓解了一些这些问题。
分布式表示
词袋模型是将单词转换为向量形式的易于理解的方法。这个过程通常被称为向量化。虽然这是一种有用的方法,但在捕获上下文和与稀疏相关的问题方面,词袋模型也有它的局限性。由于深度学习架构正在成为大多数空间的事实上的最先进系统,显而易见的是我们应该在 NLP 任务中也利用它们。除了前面提到的问题,词袋模型的稀疏和大(宽)向量是另一个可以使用神经网络解决的方面。
一种处理稀疏问题的简单替代方案是将每个单词编码为唯一的数字。继续上一节的示例,“有人说冰”,我们可以将“有人”赋值为 1,“说”赋值为 2,“冰”赋值为 3,以此类推。这将导致一个密集的向量,[1, 2, 3]。这是对空间的有效利用,并且我们得到了所有元素都是完整的向量。然而,缺失上下文的限制仍然存在。由于数字是任意的,它们几乎不能单独捕获任何上下文。相反,将数字任意映射到单词并不是非常可解释的。
可解释性是 NLP 任务的重要要求。对于计算机视觉用例,视觉线索足以成为理解模型如何感知或生成输出的良好指标(尽管在那方面的量化也是一个问题,但我们现在可以跳过它)。对于 NLP 任务,由于文本数据首先需要转换为向量,因此重要的是理解这些向量捕获了什么,以及模型如何使用它们。
在接下来的章节中,我们将介绍一些流行的向量化技术,尝试捕捉上下文,同时限制向量的稀疏性。请注意,还有许多其他方法(例如基于 SVD 的方法和共现矩阵)也有助于向量化文本数据。在本节中,我们将只涉及那些有助于理解本章后续内容的方法。
Word2vec
英国牛津词典大约有 60 万个独特的单词,并且每年都在增长。然而,这些单词并非独立的术语;它们彼此之间存在一些关系。word2vec 模型的假设是学习高质量的向量表示,以捕获上下文。这更好地总结了 J.R. 菲斯的著名引文:“你可以通过它搭配的伙伴来认识一个词”。
在他们名为“Vector Space 中单词表示的高效估计”的工作中,Mikolov 等人¹介绍了两种学习大型语料库中单词向量表示的模型。Word2Vec 是这些模型的软件实现,属于学习这些嵌入的迭代方法。与一次性考虑整个语料库不同,这种方法尝试迭代地学习编码每个单词的表示及其上下文。学习词表示作为密集上下文向量的这一概念并不新鲜。这最初是由 Rumelhart 等人于 1990²年提出的。他们展示了神经网络如何学习表示,使类似的单词最终处于相同的聚类中。拥有捕获某种相似性概念的单词向量形式的能力是非常强大的。让我们详细看看 word2vec 模型是如何实现这一点的。
连续词袋 (CBOW) 模型
连续词袋模型是我们在上一节讨论的词袋模型的扩展。该模型的关键方面是上下文窗口。上下文窗口被定义为沿着句子移动的固定大小的滑动窗口。中间的词称为目标,窗口内的左右术语称为上下文术语。CBOW 模型通过给定其上下文术语来预测目标术语。
例如,让我们考虑一句参考句子,“some say the world will end in fire”。如果我们的窗口大小为 4,目标术语为world,那么上下文术语将会是{say, the}和{will, end}。模型的输入是形式为(上下文术语,目标术语)的元组,然后将其通过神经网络学习嵌入向量。
这个过程如下图所示:
图 9.2:连续词袋模型设置
如前面的图表所示,上下文术语,表示为,被作为输入传递给模型,以预测目标术语,表示为w[t]。CBOW 模型的整体工作可以解释如下:
- 对于大小为V的词汇表,定义了大小为C的上下文窗口。C可以是 4、6 或任何其他大小。我们还定义了两个矩阵W和W’来生成输入和输出向量。矩阵W是VxN,而W’是NxV的维度。N是嵌入向量的大小。
- 上下文术语()和目标术语(y)被转化为独热编码(或标签编码),并且训练数据以元组的形式准备:(,y)。
- 我们对上下文向量进行平均以得到 。
- 最终的输出评分向量 z 是平均向量 v’ 和输出矩阵 W’ 的点积。
- 输出评分向量经过 softmax 函数转换为概率值;也就是说,y’ = softmax(z),其中 y’ 应该对应词汇表中的一个术语。
- 最终的目标是训练神经网络,使得 y’ 和实际目标 y 尽可能接近。
作者建议使用诸如交叉熵之类的成本函数来训练网络并学习这样的嵌入。
skip-gram 模型
skip-gram 模型是该论文中用于学习词嵌入的第二个变体。本质上,该模型的工作方式与 CBOW 模型完全相反。换句话说,在 skip-gram 的情况下,我们输入一个词(中心/目标词),预测上下文术语作为模型的输出。让我们用之前的例子进行说明,“some say the world will end in fire”。在这里,我们将用 world 作为输入术语,并训练一个模型以高概率预测 {say, the, will, end},作为上下文术语。
下图显示了 skip-gram 模型;如预期的那样,这是我们在 图 9.2 中讨论的 CBOW 设置的镜像:
图 9.3:skip-gram 模型设置
skip-gram 模型的逐步工作可以解释如下:
- 对于一个大小为 V 的词汇表,定义一个大小为 C 的上下文窗口。C 可以是 4、6 或其他任意大小。我们还定义了两个矩阵 W 和 W’,分别用于生成输入向量和输出向量。矩阵 W 是 VxN 的,而 W’ 的维度是 NxV。N 是嵌入向量的大小。
- 生成中心词 x 的独热编码表示。
- 通过 x 和 W 的点积来获取 x 的词嵌入表示。嵌入表示为 v = W.x。
- 我们通过将 W’ 和 v 的点积得到输出评分向量 z;也就是说,z = W’.v。
- 评分向量通过 softmax 层转换为输出概率,生成 y’。
- 最终的目标是训练神经网络,使得 y’ 和实际上的上下文 y 尽可能接近。
在 skip-gram 的情况下,对于任何给定的中心词,我们有多个输入-输出训练对。该模型将所有上下文术语都视为同等重要,无论它们与上下文窗口中的中心词之间的距离如何。这使我们能够使用交叉熵作为成本函数,并假设具有强条件独立性。
为了改善结果并加快训练过程,作者们引入了一些简单但有效的技巧。负采样、噪声对比估计和分层 softmax等概念是一些被利用的技术。要详细了解 CBOW 和 skip-gram,请读者阅读 Mikolov 等人引用的文献¹,作者在其中详细解释了每个步骤。
nltk to clean up this dataset and prepare it for the next steps. The text cleanup process is limited to lowercasing, removing special characters, and stop word removal only:
# import statements and code for the function normalize_corpus # have been skipped for brevity. See corresponding # notebook for details. cats = ['alt.atheism', 'sci.space'] newsgroups_train = fetch_20newsgroups(subset='train', categories=cats, remove=('headers', 'footers', 'quotes')) norm_corpus = normalize_corpus(newsgroups_train.data) gensim to train a skip-gram word2vec model:
# tokenize corpus tokenized_corpus = [nltk.word_tokenize(doc) for doc in norm_corpus] # Set values for various parameters embedding_size = 32 # Word vector dimensionality context_window = 20 # Context window size min_word_count = 1 # Minimum word count sample = 1e-3 # Downsample setting for frequent words sg = 1 # skip-gram model w2v_model = word2vec.Word2Vec(tokenized_corpus, size=embedding_size, window=context_window, min_count =min_word_count, sg=sg, sample=sample, iter=200)
只需几行代码,我们就可以获得我们词汇表的 word2vec 表示。检查后,我们发现我们的词汇表中有 19,000 个独特单词,并且我们为每个单词都有一个向量表示。以下代码片段显示了如何获得任何单词的向量表示。我们还将演示如何获取与给定单词最相似的单词:
# get word vector w2v_model.wv['sun']
array([ 0.607681, 0.2790227, 0.48256198, 0.41311446, 0.9275479, -1.1269532, 0.8191313, 0.03389674, -0.23167856, 0.3170586, 0.0094937, 0.1252524, -0.5247988, -0.2794391, -0.62564677, -0.28145587, -0.70590997, -0.636148, -0.6147065, -0.34033248, 0.11295943, 0.44503215, -0.37155458, -0.04982868, 0.34405553, 0.49197063, 0.25858226, 0.354654, 0.00691116, 0.1671375, 0.51912665, 1.0082873 ], dtype=float32)
# get similar words w2v_model.wv.most_similar(positive=['god'])
[('believe', 0.8401427268981934), ('existence', 0.8364629149436951), ('exists', 0.8211747407913208), ('selfcontradictory', 0.8076522946357727), ('gods', 0.7966105937957764), ('weak', 0.7965559959411621), ('belief', 0.7767481803894043), ('disbelieving', 0.7757835388183594), ('exist', 0.77425217628479), ('interestingly', 0.7742466926574707)]
前述输出展示了sun这个单词的 32 维向量。我们还展示了与单词god最相似的单词。我们可以清楚地看到,诸如 believe、existence 等单词似乎是最相似的,这是合乎逻辑的,考虑到我们使用的数据集。对于感兴趣的读者,我们在对应的笔记本中展示了使用 TensorBoard 的 3 维向量空间表示。TensorBoard 表示帮助我们直观地理解嵌入空间,以及这些向量是如何相互作用的。
GloVe
word2vec 模型有助于改进各种自然语言处理任务的性能。在相同的动力下,另一个重要的实现叫做 GloVe 也出现了。GloVe 或全局词向量表示于 2014 年由 Pennington 等人发表,旨在改进已知的单词表示技术。³
正如我们所见,word2vec 模型通过考虑词汇表中单词的局部上下文(定义的窗口)来工作。即使这是非常有效的,但还不够完善。单词在不同上下文中可能意味着不同的东西,这要求我们不仅要理解局部上下文,还要理解全局上下文。GloVe 试图在学习单词向量的同时考虑全局上下文。
有一些经典的技术,例如潜在语义分析(LSA),这些技术基于矩阵分解,在捕获全局上下文方面做得很好,但在向量数学等方面做得不太好。
GloVe 是一种旨在学习更好的词表示的方法。GloVe 算法包括以下步骤:
- 准备一个词共现矩阵X,使得每个元素x[i][j]表示单词i在单词j上下文中出现的频率。GloVe 使用了两个固定尺寸的窗口,这有助于捕捉单词之前和之后的上下文。
- 共现矩阵X使用衰减因子进行更新,以惩罚上下文中距离较远的术语。衰减因子定义如下:,其中 offset 是考虑的单词的距离。
- 然后,我们将准备 GloVe 方程如下软约束条件:
这里,w[i]是主要单词的向量,w[j]是上下文单词的向量,b[i],b[j]是相应的偏差项。 - 最后一步是使用前述约束条件来定义成本函数,其定义如下:
这里,f是一个加权函数,定义如下:
该论文的作者使用获得了最佳结果。
类似于 word2vec 模型,GloVe 嵌入也取得了良好的结果,作者展示了 GloVe 胜过 word2vec 的结果。他们将此归因于更好的问题表述和全局上下文的捕捉。
在实践中,这两种模型的性能差不多。由于需要更大的词汇表来获得更好的嵌入(对于 word2vec 和 GloVe),对于大多数实际应用情况,预训练的嵌入是可用且被使用的。
预训练的 GloVe 向量可以通过多个软件包获得,例如spacy
。感兴趣的读者可以探索spacy
软件包以获得更多详情。
FastText
Word2Vec 和 GloVe 是强大的方法,在将单词编码为向量空间时具有很好的特性。当涉及到获取在词汇表中的单词的向量表示时,这两种技术都能很好地工作,但对于词汇表之外的术语,它们没有明确的答案。
在 word2vec 和 GloVe 方法中,单词是基本单位。这一假设在 FastText 实现中受到挑战和改进。FastText 的单词表示方面基于 2017 年 Bojanowski 等人的论文使用子词信息丰富化词向量。⁴ 该工作将每个单词分解为一组 n-grams。这有助于捕捉和学习字符组合的不同向量表示,与早期技术中的整个单词不同。
例如,如果我们考虑单词“India”和n=3用于 n-gram 设置,则它将将单词分解为{, }。符号“<”和“>”是特殊字符,用于表示原始单词的开始和结束。这有助于区分,它代表整个单词,和超出词汇表的术语的嵌入。这可以通过添加和平均所需 n-gram 的向量表示来实现。
FastText 在处理可能有大量新/词汇表之外术语的用例时,被显示明显提高性能。FastText 是由Facebook AI Research(FAIR)的研究人员开发的,这应该不足为奇,因为在 Facebook 等社交媒体平台上生成的内容是巨大且不断变化的。
随着它的改进,也有一些缺点。由于这种情况下的基本单位是一个 n-gram,因此训练/学习这种表示所需的时间比以前的技术更长。 n-gram 方法还增加了训练这种模型所需的内存量。然而,论文的作者指出,散列技巧在一定程度上有助于控制内存需求。
为了便于理解,让我们再次利用我们熟悉的 Python 库gensim
。我们将扩展上一节中 word2vec 模型练习所使用的相同数据集和预处理步骤。以下片段准备了 FastText 模型对象:
# Set values for various parameters embedding_size = 32 # Word vector dimensionality context_window = 20 # Context window size min_word_count = 1 # Minimum word count sample = 1e-3 # Downsample setting for frequent words sg = 1 # skip-gram model ft_model = FastText(tokenized_corpus, size=embedding_size, window=context_window, min_count = min_word_count, sg=sg, sample=sample, iter=100)
word2vec 模型无法返回单词"sunny"的矢量表示,因为它不在训练词汇表中。以下片段显示了 FastText 仍能生成矢量表示的方法:
# out of vocabulary ft_model.wv['sunny']
array([-0.16000476, 0.3925578, -0.6220364, -0.14427347, -1.308504, 0.611941, 1.2834805, 0.5174112, -1.7918613, -0.8964722, -0.23748468, -0.81343293, 1.2371198 , 1.0380564, -0.44239333, 0.20864521, -0.9888209, 0.89212966, -1.1963437, 0.738966, -0.60981965, -1.1683533, -0.7930039, 1.0648874, 0.5561004, -0.28057176, -0.37946936, 0.02066167, 1.3181996, 0.8494686, -0.5021836, -1.0629338], dtype=float32)
这展示了 FastText 如何改进基于 word2vec 和 GloVe 的表示技术。我们可以轻松处理词汇表之外的术语,同时确保基于上下文的密集表示。
现在让我们利用这个理解来开发文本生成模型。
文本生成和 LSTM 的魔法
在前几节中,我们讨论了不同的表示文本数据的方法,以使其适合不同的自然语言处理算法使用。在本节中,我们将利用这种对文本表示的理解来构建文本生成模型。
到目前为止,我们已经使用由不同种类和组合的层组成的前馈网络构建了模型。这些网络一次处理一个训练示例,这与其他训练样本是独立的。我们说这些样本是独立同分布的,或IID。语言,或文本,有点不同。
正如我们在前几节中讨论的,单词根据它们被使用的上下文而改变它们的含义。换句话说,如果我们要开发和训练一种语言生成模型,我们必须确保模型理解其输入的上下文。
循环神经网络(RNNs)是一类允许先前输出用作输入的神经网络,同时具有记忆或隐藏单元。对先前输入的意识有助于捕捉上下文,并使我们能够处理可变长度的输入序列(句子很少长度相同)。下图显示了典型的 RNN,既实际形式又展开形式:
图 9.4:一个典型的 RNN
如图 9.4所示,在时间 t[1],输入 x[1] 生成输出 y[1]。在时间 t[2],x[2] 和 y[1](前一个输出)一起生成输出 y[2],以此类推。与 typcial feedforward 网络不同,其中的每个输入都独立于其他输入,RNN 引入了前面的输出对当前和将来的输出的影响。
RNN 还有一些不同的变体,即门控循环单元(GRUs)和长短期记忆(LSTMs)。之前描述的原始 RNN 在自回归环境中工作良好。但是,它在处理更长上下文窗口(梯度消失)时存在问题。GRUs 和 LSTMs 通过使用不同的门和记忆单元来尝试克服此类问题。LSTMs 由 Hochreiter 和 Schmidhuber 于 1997 年引入,可以记住非常长的序列数据中的信息。LSTMs 由称为输入、输出和遗忘门的三个门组成。以下图表显示了这一点。
图 9.5:LSTM 单元的不同门
有关 LSTMs 的详细了解,请参阅colah.github.io/posts/2015-08-Understanding-LSTMs/
。
现在,我们将重点介绍更正式地定义文本生成任务。
语言建模
基于 NLP 的解决方案非常有效,我们可以在我们周围看到它们的应用。最突出的例子是手机键盘上的自动完成功能,搜索引擎(Google,Bing 等)甚至文字处理软件(如 MS Word)。
自动完成是一个正式概念称为语言建模的常见名称。简单来说,语言模型以某些文本作为输入上下文,以生成下一组单词作为输出。这很有趣,因为语言模型试图理解输入的上下文,语言结构和规则,以预测下一个单词。我们经常在搜索引擎,聊天平台,电子邮件等上使用它作为文本完成工具。语言模型是 NLP 的一个完美实际应用,并展示了 RNN 的威力。在本节中,我们将致力于建立对 RNN 基于语言模型的文本生成的理解以及训练。
让我们从理解生成训练数据集的过程开始。我们可以使用下面的图像来做到这一点。该图像描绘了一个基于单词的语言模型,即以单词为基本单位的模型。在同样的思路下,我们可以开发基于字符,基于短语甚至基于文档的模型:
图 9.6:用于语言模型的训练数据生成过程
正如我们之前提到的,语言模型通过上下文来生成接下来的单词。这个上下文也被称为一个滑动窗口,它在输入的句子中从左到右(从右到左对于从右往左书写的语言)移动。 图 9.6中的滑动窗口跨越三个单词,作为输入。每个训练数据点的对应输出是窗口后面紧跟的下一个单词(或一组单词,如果目标是预测下一个短语)。因此,我们准备我们的训练数据集,其中包括({上下文词汇}, 下一个单词)这种形式的元组。滑动窗口帮助我们从训练数据集中的每个句子中生成大量的训练样本,而无需显式标记。
然后使用这个训练数据集来训练基于 RNN 的语言模型。在实践中,我们通常使用 LSTM 或 GRU 单元来代替普通的 RNN 单元。我们之前讨论过 RNN 具有自动回归到先前时间步的数值的能力。在语言模型的上下文中,我们自动回归到上下文词汇,模型产生相应的下一个单词。然后我们利用时间反向传播(BPTT)通过梯度下降来更新模型权重,直到达到所需的性能。我们在 第三章,深度神经网络的构建模块中详细讨论了 BPTT。
现在我们对语言模型以及准备训练数据集和模型设置所涉及的步骤有了一定的了解。现在让我们利用 TensorFlow 和 Keras 来实现其中一些概念。
实践操作:字符级语言模型
我们在之前的部分中讨论了语言建模的基础知识。在这一部分中,我们将构建并训练自己的语言模型,但是有一点不同。与之前部分的讨论相反,在这里,我们将在字符级别而不是词级别工作。简单来说,我们将着手构建一个模型,将少量字符作为输入(上下文)来生成接下来的一组字符。选择更细粒度的语言模型是为了便于训练这样的模型。字符级语言模型需要考虑的词汇量(或独特字符的数量)要比词级语言模型少得多。
为了构建我们的语言模型,第一步是获取一个数据集用作训练的来源。古腾堡计划是一项志愿者工作,旨在数字化历史著作并提供免费下载。由于我们需要大量数据来训练语言模型,我们将选择其中最大的一本书,列夫·托尔斯泰的 战争与和平。该书可在以下网址下载:
以下代码片段载入了作为我们源数据集的书籍内容:
datafile_path = r'warpeace_2600-0.txt' # Load the text file text = open(datafile_path, 'rb').read().decode(encoding='utf-8') print ('Book contains a total of {} characters'.format(len(text)))
Book contains a total of 3293673 characters
vocab = sorted(set(text)) print ('{} unique characters'.format(len(vocab)))
108 unique characters
下一步是准备我们的数据集用于模型。正如我们在表示文本部分讨论的那样,文本数据被转换为向量,使用词表示模型。一种方法是首先将它们转换为独热编码向量,然后使用诸如 word2vec 之类的模型将其转换为密集表示。另一种方法是首先将它们转换为任意数值表示,然后在 RNN-based 语言模型的其余部分中训练嵌入层。在这种情况下,我们使用了后一种方法,即在模型的其余部分一起训练一个嵌入层。
下面的代码片段准备了单个字符到整数映射的映射:
char2idx = {u:i for i, u in enumerate(vocab)} idx2char = np.array(vocab) text_as_int = np.array([char2idx[c] for c in text]) print('{') for char,_ in zip(char2idx, range(20)): print(' {:4s}: {:3d},'.format(repr(char), char2idx[char])) print(' ...\n}')
{ '\n': 0, '\r': 1, ' ' : 2, '!' : 3, ...
如你所见,每个唯一的字符都映射到一个整数;例如,\n
映射为 0,!
映射为 3,依此类推。
Python 与 TensorFlow2 生成式 AI(四)(2)https://developer.aliyun.com/article/1512053