如何用不同解码方法实现Transformer语言生成

近年来,随着在数百万个网页上训练的大型transformer-based语言模型的兴起,人们对开放式语言生成的兴趣越来越浓厚。

条件开放式语言生成的成果令人印象深刻,除了改进的transformer架构和大量的无监督训练数据,好的解码方法也起到了重要的作用。

这篇博客文章简要概述了不同的解码策略,更重要的是展示了如何轻松使用流行的transformer 库实现它们!


原文如下:

自回归语言生成基于一个假设,即一个单词序列的概率分布可以分解为条件下一个单词分布的乘积:

如何用不同解码方法实现Transformer语言生成

W0W0 是初始上下文字序列。单词序列的长度TT通常是即时确定的,并且与时间步长t=T对应,EOS令牌是根据P(wt∣w1:t−1,W0)生成的。

在PyTorch和Tensorflow >= 2.0中,GPT2、XLNet、OpenAi-GPT、CTRL、TransfoXL、XLM、Bart、T5都可以使用自回归语言生成!

我们将介绍目前最著名的解码方法,主要有贪心搜索(greedy search)、集束搜索(beam search)、Top-K采样和Top-p采样。

快速安装transformers 并加载模型。我们将在Tensorflow 2.1中使用GPT2进行演示,但是PyTorch的API是1对1的。


!pip install -q git+https://github.com/huggingface/transformers.git

!pip install -q tensorflow==2.1

importtensorflow astf

fromtransformers importTFGPT2LMHeadModel, GPT2Tokenizer

tokenizer = GPT2Tokenizer.from_pretrained("gpt2")

# add the EOS token as PAD token to avoid warnings

model=TFGPT2LMHeadModel.from_pretrained("gpt2", pad_token_id=tokenizer.eos_token_id)


Greedy Search(贪心搜索)


贪心搜索只是在每个timestep t中选择概率最高的单词作为下一个单词: wt = argmaxwP(w∣w1:t-1)。下面的示意图展示了贪心搜索:

如何用不同解码方法实现Transformer语言生成

算法从单词“the”开始,选择概率最高的下一个单词“nice”,以此类推,最终生成的单词序列是“the”、“nice”、“woman”,总体概率为0.5×0.4=0.2

接下来,我们将在上下文中使用GPT2生成单词序列(“I”、“enjoy”、“walking”、“with”、“my”、“cute”、“dog”)。来看看贪心搜索是如何在transformers中使用的:

# encode context the generation is conditioned on

<code>input_ids = tokenizer.encode('I enjoy walking with my cute dog', return_tensors='tf')/<code>
<code> /<code>
<code># generate text until the output length (which includes the context length) reaches 50/<code>
<code>greedy_output = model.generate(input_ids, max_length=50)/<code>
<code> /<code>
<code>print("Output:\\n" + 100 * '-')/<code>
<code>print(tokenizer.decode(greedy_output[0], skip_special_tokens=True))/<code>
<code>
<code>I enjoy walking with my cute dog, but I'm not sure if I'll ever be able to walk with my dog. I'm not sure if I'll ever be able to walk with my dog./<code>
<code> /<code>

I'm not sure if I'll

我们使用GPT2生成了第一个简短文本。根据上下文生成的单词是合理的,但是模型很快就会开始重复!一般来说,这是语言生成中一个非常普遍的问题,在贪心和波束搜索中似乎更是如此。


贪心搜索的主要缺点是,它错过了隐藏在低概率单词后面的高概率单词。


如上图所示:单词“has”的条件概率高达0.9,隐藏在单词“dog”后面,而“dog”的条件概率仅次于“dog”,因此贪婪搜索会漏掉单词序列“The”、“dog”、“has”。

还好,我们有了集束搜索(beam search)来缓解这个问题!

beam search(集束搜索)

集束搜索通过在每个time step保留最可能的num_beams ,并最终选择总体概率最高的假设,从而降低了丢失隐藏的高概率单词序列的风险。


让我们用num_beams = 2进行说明:

如何用不同解码方法实现Transformer语言生成


在time step 11,除了最可能的假设“ The”,“ woman”之外,集束搜索还跟踪第二个最可能的假设“ The”、“ dog”。在time step 22,集束搜索发现单词序列"The", "dog", "has"的概率比“ The”,“ nice”,“ woman”(0.2)高0.36。很好,它发现了示例中最可能的单词序列!

集束搜索将始终找到比贪心搜索具有更高概率的输出序列,但不能保证找到最有可能的输出。

让我们看看如何在transformers中使用集束搜索。我们设置num_beam > 1和early_stop =True,这样当所有集束假设到达EOS令牌时,生成就完成了。


<code># activate beam search and early_stopping/<code>
<code>beam_output = model.generate(/<code>
<code>    input_ids,/<code>
<code>    max_length=50,/<code>
<code>    num_beams=5,/<code>
<code>    early_stopping=True/<code>
<code>)/<code>
<code> /<code>
<code>print("Output:\\n" + 100 * '-')/<code>
<code>print(tokenizer.decode(beam_output[0], skip_special_tokens=True))/<code>
<code>----------------------------------------------------------------------------------------------------/<code>
<code>I enjoy walking with my cute dog, but I'm not sure if I'll ever be able to walk with him again./<code>
<code> /<code>

I'm not sure if I'll ever be able to walk with him again. I'm not sure if I'll


虽然结果可能更流畅,但输出仍然包含相同单词序列的重复。


一个简单的补救方法是引入n-g(也就是n-g),是由Paulus和Klein等人提出的惩罚。最常见的n-grams惩罚是手动设置下一个单词出现的概率为0,以确保n-grams不会出现两次。


通过设置no_repeat_ngram_size=2来尝试一下,这样就不会出现两次2-gram:


# set no_repeat_ngram_size to 2

<code>beam_output = model.generate(/<code>
<code>    input_ids,/<code>
<code>    max_length=50,/<code>
<code>    num_beams=5,/<code>
<code>    no_repeat_ngram_size=2,/<code>
<code>    early_stopping=True/<code>
<code>)/<code>
<code> /<code>
<code>print("Output:\\n" + 100 * '-')/<code>
<code>print(tokenizer.decode(beam_output[0], skip_special_tokens=True))/<code>
<code>----------------------------------------------------------------------------------------------------/<code>
<code>I enjoy walking with my cute dog, but I'm not sure if I'll ever be able to walk with him again./<code>
<code> /<code>
<code>I've been thinking about this for a while now, and I think it's time for me to take a break/<code> 


不错,看起来好多了!重复不再出现。然而,必须谨慎使用n-grams惩罚。在一篇关于纽约市的文章不应该使用2-gram的惩罚或,否则,全文中城市名称只会出现一次!


集束搜索的另一个重要特性是,可以比较生成后的顶部集束,并选择最适合目标的集束。


在transformers中,我们简单地将参数num_return_sequences设置为应返回的最高分集束的数量。确保num_return_sequences <= num_beam !


# set return_num_sequences > 1

<code>beam_outputs = model.generate(/<code>
<code>    input_ids,/<code>
<code>    max_length=50,/<code>
<code>    num_beams=5,/<code>
<code>    no_repeat_ngram_size=2,/<code>
<code>    num_return_sequences=5,/<code>
<code>    early_stopping=True/<code>
<code>)/<code>
<code> /<code>
<code># now we have 3 output sequences/<code>
<code>print("Output:\\n" + 100 * '-')/<code>
<code>for i, beam_output in enumerate(beam_outputs):/<code>
<code>  print("{}: {}".format(i, tokenizer.decode(beam_output, skip_special_tokens=True)))/<code>
<code>----------------------------------------------------------------------------------------------------/<code>
<code>0: I enjoy walking with my cute dog, but I'm not sure if I'll ever be able to walk with him again./<code>
<code> /<code>
<code>I've been thinking about this for a while now, and I think it's time for me to take a break/<code>
<code>1: I enjoy walking with my cute dog, but I'm not sure if I'll ever be able to walk with him again./<code>
<code> /<code>
<code>I've been thinking about this for a while now, and I think it's time for me to get back to/<code>
<code>2: I enjoy walking with my cute dog, but I'm not sure if I'll ever be able to walk with her again./<code>
<code> /<code>
<code>I've been thinking about this for a while now, and I think it's time for me to take a break/<code>
<code>3: I enjoy walking with my cute dog, but I'm not sure if I'll ever be able to walk with her again./<code>
<code> /<code>
<code>I've been thinking about this for a while now, and I think it's time for me to get back to/<code>
<code>4: I enjoy walking with my cute dog, but I'm not sure if I'll ever be able to walk with him again./<code>


<code>I've been thinking about this for a while now, and I think it's time for me to take a step/<code>


可以看出,当只使用5个集束时,5个集束假设之间的差别很小。

在开放式的生成中,关于集束可能搜索不是最好选择的问题,有几个原因最近被提出来了。

集束搜索可以很好地完成任务——在机器翻译或摘要中,期望生成的长度或多或少是可预测的。但这并不是开放式的生成,期望输出的长度可能会有很大的变化,例如对话和故事生成。

我们已经看到,集束搜索严重地受到重复生成的影响,这在故事生成过程中更加难以控制,因为要在强制的“不重复”和重复的n-grams 之间找到一个平衡需要大量的微调。

正如Ari Holtzman等人所指出的,高质量的人类语言并不遵循高概率的单词分布。


换句话说,作为人类,我们希望生成的文本能给我们带来惊喜,而不是枯燥/可预测的。作者通过绘制概率图很好地展示了这一点,一个模型将给出人类文本和集束搜索的结果。


如何用不同解码方法实现Transformer语言生成


因此,让我们引入一些随机性。

Sampling(采样)


在最基本的形式中,采样是指根据其条件概率分布随机选择下一个单词wt:


如何用不同解码方法实现Transformer语言生成


以上面的示例为例,下图可视化了采样时的语言生成。


如何用不同解码方法实现Transformer语言生成

很明显,使用抽样的语言生成不再是确定性的。单词“car”由条件概率分布P(w∣“The”)进行采样,然后由P(w∣“The”,“car”)进行采样。

在transformer中,我们设置do_sample=True,并通过top_k=0停止使用Top-K采样(稍后会详细介绍)。在接下来的代码中,我们将修复random_seed=0,以便进行演示。你可以随意更改random_seed来使用模型。

<code>tf.random.set_seed(0)/<code>

# set seed to reproduce results. Feel free to change the seed though to get different results

<code># activate sampling and deactivate top_k by setting top_k sampling to 0/<code>
<code>sample_output = model.generate(/<code>
<code>    input_ids,/<code>
<code>    do_sample=True,/<code>
<code>    max_length=50,/<code>
<code>    top_k=0/<code>
<code>)/<code>
<code>print("Output:\\n" + 100 * '-')/<code>
<code>print(tokenizer.decode(sample_output[0], skip_special_tokens=True))/<code>
<code>----------------------------------------------------------------------------------------------------/<code>
<code>I enjoy walking with my cute dog. He just gave me a whole new hand sense."/<code>
<code> /<code>
<code>But it seems that the dogs have learned a lot from teasing at the local batte harness once they take on the outside./<code>
<code>"I take/<code>
<code>"I take/<code>


文字似乎还不错,但通过仔细观察,它并不是很连贯。3-grams new hand和local batte harness非常怪异。在对单词序列进行采样时,模型通常会产生不连贯的乱码,这是一个大问题。


一个诀窍是通过降低softmax温度,使分布P(w∣w1:t−1)变得更清晰(增加高概率单词的可能性,降低低概率单词的可能性)。


对我们的示例应用温度的说明如下所示。


如何用不同解码方法实现Transformer语言生成


步骤t=1的条件下的单词分布变得更加清晰,几乎没有机会选择单词“car”。


让我们看看如何通过设置来冷却库中的分布


# set seed to reproduce results. Feel free to change the seed though to get different results

<code>tf.random.set_seed(0)/<code>
<code># use temperature to decrease the sensitivity to low probability candidates/<code>
<code>sample_output = model.generate(/<code>
<code>    input_ids,/<code>
<code>    do_sample=True,/<code>
<code>    max_length=50,/<code>
<code>    top_k=0,/<code>
<code>    temperature=0.7/<code>
<code>)/<code>
<code>print("Output:\\n" + 100 * '-')/<code>
<code>print(tokenizer.decode(sample_output[0], skip_special_tokens=True))/<code>
<code>----------------------------------------------------------------------------------------------------/<code>
<code>I enjoy walking with my cute dog, but I don't like to be at home too much. I also/<code>


n-grams减少了,输出也更连贯了!虽然应用温度可以使分布的随机性降低,但在其限制下,当将温度$ \\设置为0$时,按比例进行的温度采样就等于贪心解码,会出现和以前一样的问题。

Top-K抽样


Fan等人提出了一种简单但非常强大的抽样方案,称为Top-K抽样。


在Top-K抽样中,对K个最有可能的下一个单词进行过滤,并将概率质量重新分配到下一个单词中。GPT2采用了这种抽样方案,这是它成功生成故事的原因之一。


为了更好地说明Top-K抽样,我们将上面示例中两个抽样步骤所使用的单词范围从3个单词扩展到10个单词。


如何用不同解码方法实现Transformer语言生成


设置K=6后,在这两个采样步骤中,我们将采样池限制为6个单词。最有可能的6个单词,定义为Vtop-K,这只占整个概率质量的2 / 3,它包括第二步中几乎所有的概率质量。尽管如此,我们可以看到,在第二个采样步骤中,它成功地排除了“not”、“the”、“small”等奇怪的候选项。


来看看Top-K如何在库中使用,通过设置top_k=50:

<code># set seed to reproduce results. Feel free to change the seed though to get different results/<code>
<code>tf.random.set_seed(0)/<code>
<code># set top_k to 50/<code>
<code>sample_output = model.generate(/<code>
<code>    input_ids,/<code>
<code>    do_sample=True,/<code>
<code>    max_length=50,/<code>
<code>    top_k=50/<code>
<code>)/<code>
<code>print("Output:\\n" + 100 * '-')/<code>
<code>print(tokenizer.decode(sample_output[0], skip_special_tokens=True))/<code>
<code>----------------------------------------------------------------------------------------------------/<code>
<code>I enjoy walking with my cute dog. It's so good to have an environment where your dog is available to share with you and we'll be taking care of you./<code>
<code>We hope you'll find this story interesting!/<code>
<code>I am from/<code>


一点也不差!该文本可以说是迄今为止最人性化的文本。尽管使用Top-K采样时需要关注的一个问题是,它不能动态调整从下一个单词概率分布P(w∣w1:t-1)中过滤的单词数量。这可能是有问题的,


因为有些单词可能是从一个非常明显的分布(上图中右侧的分布)中采样的,而另一些单词则是从更平坦的分布中采样的(上图左侧的分布)。

在步骤t=1中,Top-K排除了对“people”、“big”、“house”、“cat” 进行抽样可能性,这些似乎都是合理的候选者。另一方面,在步骤t=2中,该方法将可能不合适的单词“down”、“a”包含在单词样本库中。

因此,因此,将样本池限制为固定大小K可能会危害模型以产生乱序的尖峰分布,并限制模型用于平面分布的创造力。这种直觉促使Ari Holtzman等人创建了Top-p或nucleus采样。


Top-p (nucleus) sampling


在Top-p抽样中, 不是仅从最有可能的K个单词中进行抽样,而是从累积概率超过概率p的最小单词集进行抽样,然后将概率质量重新分配到这组单词中。这样,单词集合的大小(也就是集合中单词的数量)可以根据下一个单词的概率分布动态地增加和减少。


如何用不同解码方法实现Transformer语言生成


在设定p=0.92的情况下,Top-p抽样选取的最小字数超过p=92的概率质量,定义为Vtop-p。在第一个例子中,它包含了9个最可能的单词,而在第二个例子中,它只需要选择前3个单词就可以超过92%。实际上很简单!可以看出,它保留了大量的单词,而下一个单词的可预测性可能较差,例如P(w∣the);


只有几个词时,下一个词似乎更容易预测,例如P(w∣the”,“car”)。

好了,该用transformers检查一下了!通过设置0


# set seed to reproduce results. Feel free to change the seed though to get different results

<code>tf.random.set_seed(0)/<code>
<code># deactivate top_k sampling and sample only from 92% most likely words/<code>
<code>sample_output = model.generate(/<code>
<code>    input_ids,/<code>
<code>    do_sample=True,/<code>
<code>    max_length=50,/<code>
<code>    top_p=0.92,/<code>
<code>    top_k=0/<code>
<code>)/<code>
<code>print("Output:\\n" + 100 * '-')/<code>
<code>print(tokenizer.decode(sample_output[0], skip_special_tokens=True))/<code>
<code>----------------------------------------------------------------------------------------------------/<code>
<code>I enjoy walking with my cute dog. He will never be the same. I watch him play./<code>
<code>Guys, my dog needs a name. Especially if he is found with wings. /<code>
<code>What was that? I had a lot of/<code>


虽然在理论上,Top-p似乎比Top-K更好,但两种方法在实践中都很有效。Top-p也可以与Top-K结合使用,Top-K可以避免排名很低的单词,同时可以进行一些动态选择。

最后,要获得多个独立采样的输出,可以再次设置参数num_return_sequences> 1:


# set seed to reproduce results. Feel free to change the seed though to get different results

<code>tf.random.set_seed(0)/<code>
<code># set top_k = 50 and set top_p = 0.95 and num_return_sequences = 3/<code>
<code>sample_outputs = model.generate(/<code>
<code>    input_ids,/<code>
<code>    do_sample=True,/<code>
<code>    max_length=50,/<code>
<code>    top_k=50,/<code>
<code>    top_p=0.95,/<code>
<code>    num_return_sequences=3/<code>
<code>)/<code>
<code>print("Output:\\n" + 100 * '-')/<code>
<code>for i, sample_output in enumerate(sample_outputs):/<code>
<code>  print("{}: {}".format(i, tokenizer.decode(sample_output, skip_special_tokens=True)))/<code>
<code>----------------------------------------------------------------------------------------------------/<code>
<code>0: I enjoy walking with my cute dog. It's so good to have the chance to walk with a dog. But I have this problem with the dog and how he's always looking at us and always trying to make me see that I can do something/<code>
<code>1: I enjoy walking with my cute dog, she loves taking trips to different places on the planet, even in the desert! The world isn't big enough for us to travel by the bus with our beloved pup, but that's where I find my love/<code>
<code>2: I enjoy walking with my cute dog and playing with our kids," said David J. Smith, director of the Humane Society of the US./<code>
<code>"So as a result, I've got more work in my time," he said./<code>


很好,现在有了所有工具,可以让模型用transformers来写故事了!

在开放式语言生成中,top-p和top-K抽样作为一种特殊的译码方法,似乎比传统的贪心搜索和集束搜索能产生更流畅的文本。


不过,最近有更多的证据表明,贪心搜索和集束搜索的明显缺陷(主要是产生重复的单词序列)是由模型(尤其是模型的训练方式)而不是解码方法造成的。此外,如Welleck等人所示,top-K和top-p抽样也会产生重复的单词序列。

在Welleck等人的研究中,作者表明,根据人类的评估,在适应模型的训练目标时,集束搜索可以比Top-p抽样产生更流畅的文本。

开放式语言生成是一个快速发展的研究领域,通常情况下,这里没有一种千篇一律的方法,因此人们必须了解哪种方法在特定的用例中最有效。

你可以尝试使用transfomers中的所有不同解码方法。

简短介绍了如何在transfomers中使用不同的解码方法以及开放式语言生成的最新趋势。

附录

上面没有提到生成方法的两个附加参数。在这里简要地解释一下:

  • 在达到min_length之前,min_length可以用来强制模型不产生EOS令牌(=不完成句子)。摘要中经常使用它,但是如果用户想要更长的输出,通常可以使用它。

  • repetition_penalty可用于惩罚已经生成的单词或属于上下文的单词。它最初是由Kesker等人(2019年)提出的,也在Welleck等人(2019年)的训练目标中使用。它可以非常有效地防止重复,但似乎对不同的模型和用例非常敏感。

  • attention_mask可用于屏蔽填充的令牌

  • pad_token_id、bos_token_id、eos_token_id:如果模型默认没有这些令牌,用户可以手动选择其他令牌id来表示它们。

原文链接:

https://huggingface.co/blog/how-to-generate

"


分享到:


相關文章: