PyTorch教程-9.3. 语言模型

电子说

1.2w人已加入

描述

在9.2 节中,我们将看到如何将文本序列映射到标记中,其中这些标记可以被视为一系列离散的观察结果,例如单词或字符。假设文本序列中的标记长度T依次是 x1,x2,…,xT. 语言模型的目标是估计整个序列的联合概率:

(9.3.1)P(x1,x2,…,xT),

其中可以应用第 9.1 节中的统计工具。

语言模型非常有用。例如,一个理想的语言模型将能够自行生成自然文本,只需一次绘制一个标记即可 xt∼P(xt∣xt−1,…,x1). 与使用打字机的猴子完全不同,从这种模型中出现的所有文本都将作为自然语言传递,例如英文文本。此外,只需在先前的对话片段上调节文本,就足以生成有意义的对话。显然,我们离设计这样一个系统还有很长的路要走,因为它需要理解文本,而不仅仅是生成语法合理的内容。

尽管如此,语言模型即使在其有限的形式下也能提供很好的服务。例如,“to recognize speech”和“to wreck a nice beach”这两个短语听起来非常相似。这可能会导致语音识别中出现歧义,这很容易通过一种语言模型来解决,该模型拒绝将第二种翻译认为是古怪的。同样,在文档摘要算法中,值得知道“狗咬人”比“人咬狗”更频繁,或者“我想吃奶奶”是一个相当令人不安的陈述,而“我想吃,奶奶”要温和得多。

 

import torch
from d2l import torch as d2l

 

 

from mxnet import np, npx
from d2l import mxnet as d2l

npx.set_np()

 

 

from jax import numpy as jnp
from d2l import jax as d2l

 

 

No GPU/TPU found, falling back to CPU. (Set TF_CPP_MIN_LOG_LEVEL=0 and rerun for more info.)

 

 

import tensorflow as tf
from d2l import tensorflow as d2l

 

9.3.1. 学习语言模型

显而易见的问题是我们应该如何对文档甚至一系列标记进行建模。假设我们在单词级别标记文本数据。让我们从应用基本概率规则开始:

(9.3.2)P(x1,x2,…,xT)=∏t=1TP(xt∣x1,…,xt−1).

例如,包含四个单词的文本序列的概率为:

(9.3.3)P(deep,learning,is,fun)=P(deep)P(learning∣deep)P(is∣deep,learning)P(fun∣deep,learning,is).

9.3.1.1. 马尔可夫模型和n-克

在9.1节的序列模型分析中,我们将马尔可夫模型应用到语言建模中。序列上的分布满足一阶马尔可夫性质,如果 P(xt+1∣xt,…,x1)=P(xt+1∣xt). 更高的阶数对应更长的依赖关系。这导致我们可以应用一些近似值来对序列建模:

(9.3.4)P(x1,x2,x3,x4)=P(x1)P(x2)P(x3)P(x4),P(x1,x2,x3,x4)=P(x1)P(x2∣x1)P(x3∣x2)P(x4∣x3),P(x1,x2,x3,x4)=P(x1)P(x2∣x1)P(x3∣x1,x2)P(x4∣x2,x3).

涉及一个、两个和三个变量的概率公式通常分别称为一元模型、二元模型和三元模型。为了计算语言模型,我们需要计算单词的概率和给定前几个单词的单词的条件概率。请注意,此类概率是语言模型参数。

9.3.1.2. 词频

在这里,我们假设训练数据集是一个大型文本语料库,例如所有维基百科词条、古腾堡计划和网络上发布的所有文本。可以根据训练数据集中给定单词的相对单词频率计算单词的概率。例如,估计P^(deep)可以计算为任何以单词“deep”开头的句子的概率。一种不太准确的方法是计算“deep”这个词的所有出现次数,然后除以语料库中的单词总数。这工作得很好,特别是对于频繁出现的单词。继续,我们可以尝试估计

(9.3.5)P^(learning∣deep)=n(deep, learning)n(deep),

在哪里n(x)和n(x,x′)分别是单个词和连续词对的出现次数。不幸的是,估计单词对的概率有点困难,因为“深度学习”的出现频率要低得多。特别是,对于一些不常见的单词组合,可能很难找到足够多的出现次数来获得准确的估计。正如第 9.2.5 节中的实证结果所表明的那样,对于三词组合及以上,情况会变得更糟。将有许多我们可能不会在我们的数据集中看到的似是而非的三词组合。除非我们提供一些解决方案来分配此类单词组合的非零计数,否则我们将无法在语言模型中使用它们。如果数据集很小或者单词非常罕见,我们可能连一个都找不到。

9.3.1.3. 拉普拉斯平滑

一种常见的策略是执行某种形式的拉普拉斯平滑。解决方案是为所有计数添加一个小常数。表示为n 训练集中的单词总数和m唯一单词的数量。该解决方案有助于单例,例如,通过

(9.3.6)P^(x)=n(x)+ϵ1/mn+ϵ1,P^(x′∣x)=n(x,x′)+ϵ2P^(x′)n(x)+ϵ2,P^(x″∣x,x′)=n(x,x′,x″)+ϵ3P^(x″)n(x,x′)+ϵ3.

这里ϵ1,ϵ2, 和ϵ3是超参数。拿ϵ1例如:当 ϵ1=0, 没有应用平滑;什么时候ϵ1 接近正无穷大,P^(x)接近均匀概率1/m. 以上是其他技术可以实现的相当原始的变体 (Wood等人,2011 年)。

不幸的是,由于以下原因,这样的模型很快就会变得笨拙。首先,如第 9.2.5 节所述 ,许多n-grams 很少出现,这使得拉普拉斯平滑不适合语言建模。其次,我们需要存储所有计数。第三,这完全忽略了文字的意思。例如,“cat”和“feline”应该出现在相关的语境中。很难将此类模型调整到其他上下文,而基于深度学习的语言模型非常适合将这一点考虑在内。最后,长单词序列几乎肯定是新颖的,因此简单地计算以前见过的单词序列频率的模型在这方面肯定表现不佳。因此,我们将在本章的其余部分重点介绍使用神经网络进行语言建模。

9.3.2. 困惑

接下来,让我们讨论如何衡量语言模型的质量,这将在后续部分中用于评估我们的模型。一种方法是检查文本有多令人惊讶。一个好的语言模型能够用高精度的标记来预测我们接下来会看到什么。考虑不同语言模型提出的短语“It is raining”的以下延续:

“外面下雨了”

“香蕉树下雨了”

“正在下雨 piouw;kcj pwepoiut”

就质量而言,示例 1 显然是最好的。言辞合情合理,逻辑连贯。虽然它可能不能完全准确地反映出哪个词在语义上跟随(“在旧金山”和“在冬天”将是完全合理的扩展),但该模型能够捕捉到哪个词跟随在后面。示例 2 通过生成无意义的扩展而变得相当糟糕。尽管如此,至少该模型已经学会了如何拼写单词以及单词之间的某种程度的相关性。最后,示例 3 表明训练有素的模型无法正确拟合数据。

我们可以通过计算序列的可能性来衡量模型的质量。不幸的是,这是一个难以理解和比较的数字。毕竟,较短的序列比较长的序列更有可能出现,因此评估托尔斯泰的巨著《战争与和平》中的模型将不可避免地产生比圣埃克苏佩里的中篇小说《小王子》小得多的可能性。缺少的是相当于平均值。

信息论在这里派上用场。我们在介绍 softmax 回归时定义了熵、惊奇和交叉熵(第 4.1.3 节)。如果我们想压缩文本,我们可以询问在给定当前标记集的情况下预测下一个标记。更好的语言模型应该能让我们更准确地预测下一个标记。因此,它应该允许我们在压缩序列时花费更少的比特。所以我们可以通过对所有数据进行平均的交叉熵损失来衡量它n序列的标记:

(9.3.7)1n∑t=1n−log⁡P(xt∣xt−1,…,x1),

在哪里P由语言模型给出,并且xt是在时间步观察到的实际标记t从序列。这使得不同长度文档的性能具有可比性。由于历史原因,自然语言处理领域的科学家更喜欢使用一种叫做困惑度的量。简而言之,它是(9.3.7)的指数:

(9.3.8)exp⁡(−1n∑t=1nlog⁡P(xt∣xt−1,…,x1)).

困惑度可以最好地理解为我们在决定下一步选择哪个标记时所拥有的实际选择数量的几何平均值。让我们看一些案例:

在最好的情况下,模型总是完美地将目标标记的概率估计为 1。在这种情况下,模型的困惑度为 1。

在最坏的情况下,模型总是预测目标标记的概率为 0。在这种情况下,困惑度为正无穷大。

在基线上,该模型预测词汇表中所有可用标记的均匀分布。在这种情况下,困惑度等于词汇表中唯一标记的数量。事实上,如果我们要在不进行任何压缩的情况下存储序列,这将是我们对它进行编码所能做的最好的事情。因此,这提供了一个重要的上限,任何有用的模型都必须击败它。

9.3.3. 分区序列

我们将使用神经网络设计语言模型,并使用困惑度来评估模型在给定文本序列中的当前标记集的情况下预测下一个标记的能力。在介绍该模型之前,我们假设它一次处理一小批具有预定义长度的序列。现在的问题是如何随机读取输入序列和目标序列的小批量。

假设数据集采用一系列的形式T中的令牌索引corpus。我们将把它分成子序列,其中每个子序列有n令牌(时间步长)。为每个时期迭代(几乎)整个数据集的所有标记并获得所有可能的长度 -n子序列,我们可以引入随机性。更具体地说,在每个时代的开始,丢弃第一个d令牌,在哪里d∈[0,n)是随机均匀采样的。然后将序列的其余部分划分为 m=⌊(T−d)/n⌋子序列。表示为 xt=[xt,…,xt+n−1]长度-n 从令牌开始的子序列xt在时间步t. 所结果的m分区子序列是 xd,xd+n,…,xd+n(m−1).每个子序列将用作语言模型的输入序列。

对于语言建模,目标是根据我们目前看到的标记预测下一个标记,因此目标(标签)是原始序列,移动一个标记。任何输入序列的目标序列xt是xt+1有长度n.

语言模型

图 9.3.1从分割的长度为 5 的子序列中获得 5 对输入序列和目标序列。

图 9.3.1显示了获得 5 对输入序列和目标序列的示例n=5和d=2.

 

@d2l.add_to_class(d2l.TimeMachine) #@save
def __init__(self, batch_size, num_steps, num_train=10000, num_val=5000):
  super(d2l.TimeMachine, self).__init__()
  self.save_hyperparameters()
  corpus, self.vocab = self.build(self._download())
  array = torch.tensor([corpus[i:i+num_steps+1]
            for i in range(len(corpus)-num_steps)])
  self.X, self.Y = array[:,:-1], array[:,1:]

 

 

@d2l.add_to_class(d2l.TimeMachine) #@save
def __init__(self, batch_size, num_steps, num_train=10000, num_val=5000):
  super(d2l.TimeMachine, self).__init__()
  self.save_hyperparameters()
  corpus, self.vocab = self.build(self._download())
  array = np.array([corpus[i:i+num_steps+1]
            for i in range(len(corpus)-num_steps)])
  self.X, self.Y = array[:,:-1], array[:,1:]

 

 

@d2l.add_to_class(d2l.TimeMachine) #@save
def __init__(self, batch_size, num_steps, num_train=10000, num_val=5000):
  super(d2l.TimeMachine, self).__init__()
  self.save_hyperparameters()
  corpus, self.vocab = self.build(self._download())
  array = jnp.array([corpus[i:i+num_steps+1]
            for i in range(len(corpus)-num_steps)])
  self.X, self.Y = array[:,:-1], array[:,1:]

 

 

@d2l.add_to_class(d2l.TimeMachine) #@save
def __init__(self, batch_size, num_steps, num_train=10000, num_val=5000):
  super(d2l.TimeMachine, self).__init__()
  self.save_hyperparameters()
  corpus, self.vocab = self.build(self._download())
  array = tf.constant([corpus[i:i+num_steps+1]
            for i in range(len(corpus)-num_steps)])
  self.X, self.Y = array[:,:-1], array[:,1:]

 

为了训练语言模型,我们将在小批量中随机抽取输入序列和目标序列对。以下数据加载器每次从数据集中随机生成一个小批量。参数 batch_size指定每个小批量中子序列示例的数量,并且num_steps是以标记为单位的子序列长度。

 

@d2l.add_to_class(d2l.TimeMachine) #@save
def get_dataloader(self, train):
  idx = slice(0, self.num_train) if train else slice(
    self.num_train, self.num_train + self.num_val)
  return self.get_tensorloader([self.X, self.Y], train, idx)

 

正如我们在下面看到的,可以通过将输入序列移动一个标记来获得一小批目标序列。

 

data = d2l.TimeMachine(batch_size=2, num_steps=10)
for X, Y in data.train_dataloader():
  print('X:', X, 'nY:', Y)
  break

 

 

X: tensor([[ 0, 5, 10, 14, 6, 15, 20, 10, 16, 15],
    [ 5, 10, 7, 7, 6, 19, 6, 15, 4, 6]])
Y: tensor([[ 5, 10, 14, 6, 15, 20, 10, 16, 15, 0],
    [10, 7, 7, 6, 19, 6, 15, 4, 6, 0]])

 

 

data = d2l.TimeMachine(batch_size=2, num_steps=10)
for X, Y in data.train_dataloader():
  print('X:', X, 'nY:', Y)
  break

 

 

X: [[14. 6. 15. 21. 0. 14. 26. 0. 5. 6.]
 [ 0. 2. 0. 7. 16. 22. 19. 21. 9. 0.]]
Y: [[ 6. 15. 21. 0. 14. 26. 0. 5. 6. 2.]
 [ 2. 0. 7. 16. 22. 19. 21. 9. 0. 5.]]

 

 

data = d2l.TimeMachine(batch_size=2, num_steps=10)
for X, Y in data.train_dataloader():
  print('X:', X, 'nY:', Y)
  break

 

 

X: [[13 10 14 10 21 20 0 22 20 0]
 [ 0 14 16 23 6 20 0 10 15 21]]
Y: [[10 14 10 21 20 0 22 20 0 21]
 [14 16 23 6 20 0 10 15 21 6]]

 

 

data = d2l.TimeMachine(batch_size=2, num_steps=10)
for X, Y in data.train_dataloader():
  print('X:', X, 'nY:', Y)
  break

 

 

X: tf.Tensor(
[[13 26 0 21 24 16 0 5 10 14]
 [22 20 15 6 20 20 0 14 16 23]], shape=(2, 10), dtype=int32)
Y: tf.Tensor(
[[26 0 21 24 16 0 5 10 14 6]
 [20 15 6 20 20 0 14 16 23 6]], shape=(2, 10), dtype=int32)

 

9.3.4. 总结与讨论

语言模型估计文本序列的联合概率。对于长序列,n-grams 通过截断依赖关系提供了一个方便的模型。然而,有很多结构但没有足够的频率来通过拉普拉斯平滑有效地处理不常见的单词组合。因此,我们将在后续部分重点介绍神经语言建模。为了训练语言模型,我们可以在小批量中随机抽取输入序列和目标序列对。训练结束后,我们将使用 perplexity 来衡量语言模型的质量。

语言模型可以随着数据大小、模型大小和训练计算量的增加而扩展。大型语言模型可以通过给定输入文本指令预测输出文本来执行所需的任务。正如我们稍后将讨论的(例如, 第 11.9 节),目前,大型语言模型构成了跨不同任务的最先进系统的基础。

9.3.5. 练习

假设有100,000训练数据集中的单词。一个四文库需要存储多少词频和多词邻频?

你将如何模拟对话?

您还能想到哪些其他方法来读取长序列数据?

考虑我们在每个纪元开始时丢弃前几个标记的均匀随机数的方法。

它真的会导致文档序列的完美均匀分布吗?

你必须做些什么才能使事情变得更加统一?

如果我们想让一个序列示例是一个完整的句子,这在小批量抽样中会引入什么样的问题?我们如何解决这个问题?

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分