Press "Enter" to skip to content

语言模型用于句子补全

使用语言模型选择最有可能的候选词来扩展英语句子的实际应用

Brett Jordan在Unsplash上的照片

与Naresh Singh共同撰写。

目录

引言问题陈述解决方案头脑风暴

  • 算法和数据结构
  • NLP(自然语言处理)
  • 深度学习(神经网络)

LSTM模型

  • 分词
  • PyTorch模型
  • 使用模型剪枝无效建议
  • 计算下一个单词的概率

Transformer模型

结论

引言

最近,像GPT这样的语言模型变得非常流行,并被用于各种文本生成任务,例如ChatGPT或其他对话型人工智能系统。这些语言模型非常庞大,参数通常超过数十亿个,并且需要大量的计算资源和资金来运行。

在英语语言模型的背景下,这些庞大的模型过度参数化,因为它们利用模型的参数来记忆和学习我们世界的各个方面,而不仅仅是对英语语言进行建模。如果我们有一个只需要模型理解语言及其结构的应用程序,我们可能只需使用一个更小的模型。

在这个笔记本中可以找到运行训练模型推理的完整代码。

问题陈述

假设我们正在构建一个滑动键盘系统,试图预测您在移动电话上输入的下一个单词。根据滑动模式的模式,用户的预期单词可能有很多可能性。然而,其中许多可能的单词并不是真正的英语单词,可以被排除。即使在这个初始的剪枝和排除步骤之后,仍然有许多候选词,我们需要从中选择一个作为用户的建议。

为了进一步剪枝候选词列表,我们可以使用基于深度学习的语言模型,该模型查看提供的上下文并告诉我们哪个候选词最有可能完成句子。

例如,如果用户输入了句子”I’ve scheduled this”,然后滑动模式如下所示:

语言模型用于句子补全 四海 第2张

那么,用户可能意思的一些英语单词有:

  1. messing
  2. meeting

然而,如果我们仔细考虑,由于句子前半部分有单词”scheduled”,用户可能更有可能是指”meeting”而不是”messing”。

根据我们目前所知,我们在编程上有哪些选择来进行剪枝?让我们在下面的部分中进行一些头脑风暴。

解决方案头脑风暴

算法和数据结构

使用第一原理,从一个数据语料库开始,找到一起出现的单词对,并训练一个马尔可夫模型,预测这对单词在句子中出现的概率。您会注意到这种方法存在两个重要问题。

  1. 空间利用:英语语言中有25万至100万个单词,不包括不断增长的大量专有名词。因此,任何传统软件解决方案都必须维护一个查找表,其中包含250k*250k = 6250亿个单词对,这有些过多。可能许多单词对并不经常出现,可以剪枝。即使剪枝后,仍然有很多需要关注的单词对。
  2. 完整性:编码一对单词的概率不能完全解决问题。例如,在仅查看最近的一对单词时,之前的句子上下文完全丢失。在句子”How is your day coming”中,如果您要检查”coming”之后的单词,您会看到很多以”coming”开头的单词对。这忽略了该单词之前的整个句子上下文。人们可以想象使用单词三元组等方法,但这会加剧上面提到的空间利用问题。

让我们将注意力转向一种利用英语语言特性的解决方案,看看是否可以帮助我们。

NLP(自然语言处理)

从历史上看,NLP(自然语言处理)的领域涉及理解句子的词性(POS),并利用这些信息进行修剪和预测决策。人们可以想象使用与每个词相关联的词性标签,确定句子中的下一个词是否有效。

然而,计算句子的词性本身就是一个复杂的过程,需要对语言有专门的理解,如NLTK词性标注页面所示。

接下来,让我们看一下一种基于深度学习的方法,它需要更多的标记数据,但不需要太多的语言专业知识来构建。

深度学习(神经网络)

NLP领域已经被深度学习的出现所颠覆。随着LSTM和Transformer基于语言模型的发明,解决方案往往涉及将一些高质量的数据提供给模型,并训练模型来预测下一个单词。

本质上,这就是GPT模型在做的。GPT(生成式预训练Transformer)模型被训练来预测给定句子前缀的下一个单词(标记)。

给定句子前缀“这是一个美好的”,模型可能会提供以下作为下一个单词的高概率预测:

  1. 一天
  2. 经验
  3. 世界
  4. 生活

以下单词可能有较低的完成句子前缀的概率:

  1. 红色
  2. 老鼠
  3. 线

Transformer模型架构是ChatGPT等系统的核心。然而,对于学习英语语言语义的更受限制的用例,我们可以使用更便宜的模型架构,如LSTM(长短期记忆)模型。

一个LSTM模型

让我们构建一个简单的LSTM模型,并训练它来预测给定一组标记的下一个标记。现在,你可能会问什么是标记。

标记化

通常对于语言模型来说,一个标记可以是

  1. 一个单个字符(或一个单个字节)
  2. 目标语言中的一个完整单词
  3. 介于1和2之间的某个东西。这通常被称为子词

将一个单个字符(或字节)映射到一个标记是非常受限制的,因为我们正在对该标记进行过多的上下文负载。这是因为例如,字符“c”出现在许多不同的单词中,为了预测我们在看到字符“c”后的下一个字符,我们需要仔细观察前导上下文。

将一个单个单词映射到一个标记也是有问题的,因为英语本身有25万到100万个单词。此外,当新单词添加到语言中时会发生什么?我们需要返回并重新训练整个模型以考虑这个新单词吗?

在2023年,子词标记化被认为是行业标准。它将经常一起出现的字节子字符串分配给唯一的标记。通常,语言模型具有几千个(例如4000)到几万个(例如60000)个唯一标记。确定什么构成一个标记的算法由BPE(字节对编码)算法确定。

要选择词汇表中唯一标记的数量(称为词汇表大小),我们需要注意以下几点:

  1. 如果我们选择的标记太少,我们又回到了每个字符一个标记的情况,模型很难学到有用的东西。
  2. 如果我们选择的标记太多,就会出现模型的嵌入表超过模型其余部分权重的情况,这样在受限环境中部署模型就变得困难。嵌入表的大小将取决于我们为每个标记使用的维度数量。通常使用大小为256、512、786等的标记嵌入维度。如果我们使用512个标记嵌入维度,并且有10万个标记,那么我们最终会得到一个使用200MiB内存的嵌入表。

因此,在选择词汇量时我们需要把握一个平衡。在这个例子中,我们选择了6600个标记,并使用6600的词汇量进行训练。接下来,让我们来看一下模型的定义本身。

PyTorch模型

模型本身非常简单。我们有以下层:

  1. 标记嵌入(词汇量=6600,嵌入维度=512),总大小约为15MiB(假设嵌入表的数据类型为4字节的float32)
  2. LSTM(层数=1,隐藏维度=786),总大小约为16MiB
  3. 多层感知器(786到3144到6600维度),总大小约为93MiB

完整的模型共有约31M个可训练参数,总大小约为120MiB。

语言模型用于句子补全 四海 第3张

以下是模型的PyTorch代码。

class WordPredictionLSTMModel(nn.Module):    def __init__(self, num_embed, embed_dim, pad_idx, lstm_hidden_dim, lstm_num_layers, output_dim, dropout):        super().__init__()        self.vocab_size = num_embed        self.embed = nn.Embedding(num_embed, embed_dim, pad_idx)        self.lstm = nn.LSTM(embed_dim, lstm_hidden_dim, lstm_num_layers, batch_first=True, dropout=dropout)        self.fc = nn.Sequential(            nn.Linear(lstm_hidden_dim, lstm_hidden_dim * 4),            nn.LayerNorm(lstm_hidden_dim * 4),            nn.LeakyReLU(),            nn.Dropout(p=dropout),            nn.Linear(lstm_hidden_dim * 4, output_dim),        )    #        def forward(self, x):        x = self.embed(x)        x, _ = self.lstm(x)        x = self.fc(x)        x = x.permute(0, 2, 1)        return x    ##

以下是使用torchinfo的模型摘要。

LSTM模型摘要

=================================================================Layer (type:depth-idx) Param #=================================================================WordPredictionLSTMModel - ├─Embedding: 1–1 3,379,200├─LSTM: 1–2 4,087,200├─Sequential: 1–3 - │ └─Linear: 2–1 2,474,328│ └─LayerNorm: 2–2 6,288│ └─LeakyReLU: 2–3 - │ └─Dropout: 2–4 - │ └─Linear: 2–5 20,757,000=================================================================Total params: 30,704,016Trainable params: 30,704,016Non-trainable params: 0=================================================================

解释准确率:在P100 GPU上用1200万英语句子进行约8小时的训练后,我们获得了4.03的损失,准确率为29%,前5位准确率为49%。这意味着模型能够正确预测下一个标记的准确率为29%,在训练集中,模型的前5个预测中有49%的准确率。

我们的成功指标应该是什么?虽然模型的前1位和前5位准确率并不令人印象深刻,但它们对于我们的问题并不那么重要。我们的候选词是适应滑动模式的一小组可能词汇。我们希望我们的模型能够选择一个理想的候选词来完成句子,使其在句法和语义上连贯。由于我们的模型通过训练数据学习语言的性质,我们希望它能给连贯的句子分配更高的概率。例如,如果我们有句子“The baseball player”和可能的候选完成词(“ran”,“swam”,“hid”),那么单词“ran”比其他两个更适合作为后续词。因此,如果我们的模型预测出的单词“ran”比其他单词有更高的概率,那对我们来说就可以了。

解释损失:4.03的损失意味着预测的负对数似然为4.03,这意味着正确预测下一个标记的概率为e^-4.03 = 0.0178,或者1/56。一个随机初始化的模型通常具有约为8.8的损失,即-log_e(1/6600),因为模型随机预测1/6600个标记(6600是词汇量)。虽然4.03的损失可能看起来不太好,但重要的是记住,训练后的模型比未经训练(或随机初始化)的模型好120倍。

接下来,让我们来看看如何使用这个模型来改进我们的滑动键盘的建议。

使用模型修剪无效建议

让我们来看一个真实的例子。假设我们有一个部分句子“I think”,用户在下面的蓝色滑动模式中滑动,从“o”开始,在字母“c”和“v”之间移动,然后在字母“e”和“v”之间结束。

语言模型用于句子补全 四海 第4张

这个滑动模式可能代表的一些可能单词是:

  1. Over
  2. Oct(十月的缩写)
  3. Ice
  4. I’ve(省略了撇号)

在这些建议中,最有可能的可能是“I’ve”。让我们将这些建议输入我们的模型并看看它的输出。

[I think] [I've] = 0.00087[I think] [over] = 0.00051[I think] [ice] = 0.00001[I think] [Oct] = 0.00000

等号后面的值是这个单词作为句子前缀的有效完成的概率。在这种情况下,我们可以看到单词“I’ve”被赋予了最高的概率。因此,它是跟在句子前缀“I think”后面的最可能的单词。

你可能会问的下一个问题是我们如何计算这些下一个单词的概率。让我们来看一下。

计算下一个单词的概率

为了计算一个单词作为句子前缀的有效完成的概率,我们以eval(推理)模式运行模型,并将分词后的句子前缀作为输入。我们还将单词分词后加上一个空格前缀。这是因为HuggingFace的预分词器会将以空格开头的单词拆分成多个标记,所以我们希望确保我们的输入与HuggingFace Tokenizers使用的分词策略一致。

假设候选单词由3个标记T0、T1和T2组成。

  1. 我们首先使用原始的分词后的句子前缀运行模型。对于最后一个标记,我们检查预测标记T0的概率。我们将其添加到“probs”列表中。
  2. 接下来,我们对前缀+T0进行预测,并检查标记T1的概率。我们将这个概率添加到“probs”列表中。
  3. 接下来,我们对前缀+T0+T1进行预测,并检查标记T2的概率。我们将这个概率添加到“probs”列表中。

“probs”列表包含了按顺序生成标记T0、T1和T2的概率。由于这些标记对应于候选单词的分词,我们可以将这些概率相乘,得到候选单词作为句子前缀的组合概率。

计算完成概率的代码如下所示。

 def get_completion_probability(self, input, completion, tok):      self.model.eval()      ids = tok.encode(input).ids      ids = torch.tensor(ids, device=self.device).unsqueeze(0)      completion_ids = torch.tensor(tok.encode(completion).ids, device=self.device).unsqueeze(0)      probs = []      for i in range(completion_ids.size(1)):          y = self.model(ids)          y = y[0,:,-1].softmax(dim=0)          # prob is the probability of this completion.          prob = y[completion_ids[0,i]]          probs.append(prob)          ids = torch.cat([ids, completion_ids[:,i:i+1]], dim=1)      #      return torch.tensor(probs)  #

下面我们可以看一些更多的例子。

[That ice-cream looks] [really] = 0.00709[That ice-cream looks] [delicious] = 0.00264[That ice-cream looks] [absolutely] = 0.00122[That ice-cream looks] [real] = 0.00031[That ice-cream looks] [fish] = 0.00004[That ice-cream looks] [paper] = 0.00001[That ice-cream looks] [atrocious] = 0.00000[Since we're heading] [toward] = 0.01052[Since we're heading] [away] = 0.00344[Since we're heading] [against] = 0.00035[Since we're heading] [both] = 0.00009[Since we're heading] [death] = 0.00000[Since we're heading] [bubble] = 0.00000[Since we're heading] [birth] = 0.00000[Did I make] [a] = 0.22704[Did I make] [the] = 0.06622[Did I make] [good] = 0.00190[Did I make] [food] = 0.00020[Did I make] [color] = 0.00007[Did I make] [house] = 0.00006[Did I make] [colour] = 0.00002[Did I make] [pencil] = 0.00001[Did I make] [flower] = 0.00000[We want a candidate] [with] = 0.03209[We want a candidate] [that] = 0.02145[We want a candidate] [experience] = 0.00097[We want a candidate] [which] = 0.00094[We want a candidate] [more] = 0.00010[We want a candidate] [less] = 0.00007[We want a candidate] [school] = 0.00003[This is the definitive guide to the] [the] = 0.00089[This is the definitive guide to the] [complete] = 0.00047[This is the definitive guide to the] [sentence] = 0.00006[This is the definitive guide to the] [rapper] = 0.00001[This is the definitive guide to the] [illustrated] = 0.00001[This is the definitive guide to the] [extravagant] = 0.00000[This is the definitive guide to the] [wrapper] = 0.00000[This is the definitive guide to the] [miniscule] = 0.00000[Please can you] [check] = 0.00502[Please can you] [confirm] = 0.00488[Please can you] [cease] = 0.00002[Please can you] [cradle] = 0.00000[Please can you] [laptop] = 0.00000[Please can you] [envelope] = 0.00000[Please can you] [options] = 0.00000[Please can you] [cordon] = 0.00000[Please can you] [corolla] = 0.00000[I think] [I've] = 0.00087[I think] [over] = 0.00051[I think] [ice] = 0.00001[I think] [Oct] = 0.00000[Please] [can] = 0.00428[Please] [cab] = 0.00000[I've scheduled this] [meeting] = 0.00077[I've scheduled this] [messing] = 0.00000

这些示例展示了单词在完成句子之前的概率。候选单词按概率递减的顺序排序。

由于Transformer模型正在逐渐取代LSTM和RNN模型用于基于序列的任务,让我们看一下相同目标的Transformer模型会是什么样子。

Transformer模型

基于Transformer的模型是一种非常流行的架构,用于训练语言模型以预测句子中的下一个单词。我们将使用的具体技术是因果关注机制。我们将仅使用PyTorch中的Transformer编码器层进行训练,使用因果关注。因果关注意味着我们将允许序列中的每个标记仅查看其之前的标记。这类似于仅在正向方向训练时单向LSTM层使用的信息。

语言模型用于句子补全 四海 第5张

我们将在这里看到的Transformer模型直接基于PyTorch中的nn.TransformerEncoder和nn.TransformerEncoderLayer。

import mathdef generate_src_mask(sz, device):    return torch.triu(torch.full((sz, sz), True, device=device), diagonal=1)#class PositionalEmbedding(nn.Module):    def __init__(self, sequence_length, embed_dim):        super().__init__()        self.sqrt_embed_dim = math.sqrt(embed_dim)        self.pos_embed = nn.Parameter(torch.empty((1, sequence_length, embed_dim)))        nn.init.uniform_(self.pos_embed, -1.0, 1.0)    #        def forward(self, x):        return x * self.sqrt_embed_dim + self.pos_embed[:,:x.size(1)]    ##class WordPredictionTransformerModel(nn.Module):    def __init__(self, sequence_length, num_embed, embed_dim, pad_idx, num_heads, num_layers, output_dim, dropout, norm_first, activation):        super().__init__()        self.vocab_size = num_embed        self.sequence_length = sequence_length        self.embed_dim = embed_dim        self.sqrt_embed_dim = math.sqrt(embed_dim)        self.embed = nn.Sequential(            nn.Embedding(num_embed, embed_dim, pad_idx),            PositionalEmbedding(sequence_length, embed_dim),            nn.LayerNorm(embed_dim),            nn.Dropout(p=0.1),        )        encoder_layer = nn.TransformerEncoderLayer(            d_model=embed_dim, nhead=num_heads, dropout=dropout, batch_first=True, norm_first=norm_first, activation=activation,        )        self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)        self.fc = nn.Sequential(            nn.Linear(embed_dim, embed_dim * 4),            nn.LayerNorm(embed_dim * 4),            nn.LeakyReLU(),            nn.Dropout(p=dropout),            nn.Linear(embed_dim * 4, output_dim),        )    #        def forward(self, x):        src_attention_mask = generate_src_mask(x.size(1), x.device)        x = self.embed(x)        x = self.encoder(x, is_causal=True, mask=src_attention_mask)        x = self.fc(x)        x = x.permute(0, 2, 1)        return x    ##

我们可以将这个模型替换为之前使用的LSTM模型,因为它的API是兼容的。相同数量的训练数据训练这个模型需要更长的时间,但性能相当。

Transformer模型对于长序列效果更好。在我们的情况下,我们有长度为256的序列。执行下一个单词完成所需的大部分上下文往往是局部的,所以我们在这里并不真正需要Transformer的强大能力。

结论

我们看到了如何使用基于LSTM(RNN)和Transformer模型的深度学习技术解决非常实际的自然语言处理问题。并非每个语言任务都需要使用数十亿参数的模型。需要对语言本身进行建模而不是记忆大量信息的专用应用可以使用更小的模型来处理,这些模型可以轻松部署并比目前常见的大型语言模型更高效。

除第一个图像外,所有图像均由作者创建。

Leave a Reply

Your email address will not be published. Required fields are marked *