如何生成文本:使用不同的解码方法生成Transformers的文本

发布于2020年3月1日
在 GitHub 上更新
Open In Colab

注意:2023年7月编辑,包含最新参考文献和示例。

引言

近年来,随着在数百万网页上训练的基于Transformer的大型语言模型的兴起,例如OpenAI的ChatGPT和Meta的LLaMA,开放式语言生成引起了越来越多的关注。条件开放式语言生成的结果令人印象深刻,它们被证明能够泛化到新任务处理代码,或者接受非文本数据作为输入。除了改进的Transformer架构和海量无监督训练数据,更好的解码方法也发挥了重要作用。

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

以下所有功能都可用于自回归语言生成(此处是回顾)。简而言之,自回归语言生成基于以下假设:词序列的概率分布可以分解为条件下一个词分布的乘积

P(w1:TW0)=t=1TP(wtw1:t1,W0) ,with w1:0=, P(w_{1:T} | W_0 ) = \prod_{t=1}^T P(w_{t} | w_{1: t-1}, W_0) \text{ ,with } w_{1: 0} = \emptyset,

其中W0W_0是初始的上下文词序列。词序列的长度TT通常是动态确定的,并且对应于从P(wtw1:t1,W0)P(w_{t} | w_{1: t-1}, W_{0})生成EOS token的时间步t=Tt=T

我们将介绍当前最主要的解码方法:贪婪搜索束搜索采样

让我们快速安装transformers并加载模型。我们将使用PyTorch中的GPT2进行演示,但该API与TensorFlow和JAX完全相同。

!pip install -q transformers
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch

torch_device = "cuda" if torch.cuda.is_available() else "cpu"

tokenizer = AutoTokenizer.from_pretrained("gpt2")

# add the EOS token as PAD token to avoid warnings
model = AutoModelForCausalLM.from_pretrained("gpt2", pad_token_id=tokenizer.eos_token_id).to(torch_device)

贪婪搜索

贪婪搜索是最简单的解码方法。它在每个时间步tt选择概率最高的词作为下一个词:wt=argmaxwP(ww1:t1)w_t = argmax_{w}P(w | w_{1:t-1})。下图展示了贪婪搜索。

greedy search

从词"The",\text{"The"},开始,算法贪婪地选择概率最高的下一个词"nice"\text{"nice"},以此类推,最终生成的词序列是("The","nice","woman")(\text{"The"}, \text{"nice"}, \text{"woman"}),总概率为0.5×0.4=0.20.5 \times 0.4 = 0.2

接下来我们将使用GPT2在上下文("I","enjoy","walking","with","my","cute","dog")(\text{"I"}, \text{"enjoy"}, \text{"walking"}, \text{"with"}, \text{"my"}, \text{"cute"}, \text{"dog"})上生成词序列。让我们看看如何在transformers中使用贪婪搜索

# encode context the generation is conditioned on
model_inputs = tokenizer('I enjoy walking with my cute dog', return_tensors='pt').to(torch_device)

# generate 40 new tokens
greedy_output = model.generate(**model_inputs, max_new_tokens=40)

print("Output:\n" + 100 * '-')
print(tokenizer.decode(greedy_output[0], skip_special_tokens=True))
Output:
----------------------------------------------------------------------------------------------------
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.

I'm not sure

好的!我们已经用GPT2生成了第一个短文本😊。上下文后的生成词是合理的,但模型很快就开始重复自己!这是语言生成中一个非常常见的问题,在贪婪搜索和束搜索中似乎更是如此——请参阅Vijayakumar et al., 2016Shao et al., 2017

然而,贪婪搜索的主要缺点是它会错过隐藏在低概率词后面的高概率词,如我们上面的草图所示

"has"\text{"has"}的条件概率高达0.90.9,却隐藏在条件概率仅次于最高概率的词"dog"\text{"dog"}后面,因此贪婪搜索错过了词序列"The","dog","has"\text{"The"}, \text{"dog"}, \text{"has"}

幸运的是,我们有束搜索来缓解这个问题!

束搜索

束搜索通过在每个时间步保留最有可能的num_beams个假设,并最终选择整体概率最高的假设,从而降低了错过隐藏高概率词序列的风险。让我们以num_beams=2为例进行说明

beam search

在时间步1,除了最有可能的假设("The","nice")(\text{"The"}, \text{"nice"})之外,束搜索还跟踪次可能的假设("The","dog")(\text{"The"}, \text{"dog"})。在时间步2,束搜索发现词序列("The","dog","has")(\text{"The"}, \text{"dog"}, \text{"has"})的概率为0.360.36,高于("The","nice","woman")(\text{"The"}, \text{"nice"}, \text{"woman"})0.20.2。太棒了,它找到了我们玩具示例中最有可能的词序列!

束搜索总能找到比贪婪搜索更高概率的输出序列,但不能保证找到最有可能的输出。

让我们看看如何在transformers中使用束搜索。我们将num_beams > 1并设置early_stopping=True,这样当所有束假设都达到EOS标记时,生成就完成了。

# activate beam search and early_stopping
beam_output = model.generate(
    **model_inputs,
    max_new_tokens=40,
    num_beams=5,
    early_stopping=True
)

print("Output:\n" + 100 * '-')
print(tokenizer.decode(beam_output[0], skip_special_tokens=True))
Output:
----------------------------------------------------------------------------------------------------
I enjoy walking with my cute dog, but I'm not sure if I'll ever be able to walk with him again.

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

虽然结果可以说更流畅,但输出仍然包含相同词序列的重复。一种可用的补救措施是引入N-gram(即N个词的词序列)惩罚,如Paulus et al. (2017)Klein et al. (2017)所介绍的。最常见的N-gram惩罚确保没有N-gram重复出现两次,方法是手动将可能创建已见过N-gram的下一个词的概率设置为0。

让我们通过设置no_repeat_ngram_size=2来尝试一下,这样就不会出现重复的2-gram

# set no_repeat_ngram_size to 2
beam_output = model.generate(
    **model_inputs,
    max_new_tokens=40,
    num_beams=5,
    no_repeat_ngram_size=2,
    early_stopping=True
)

print("Output:\n" + 100 * '-')
print(tokenizer.decode(beam_output[0], skip_special_tokens=True))
Output:
----------------------------------------------------------------------------------------------------
I enjoy walking with my cute dog, but I'm not sure if I'll ever be able to walk with him again.

I've been thinking about this for a while now, and I think it's time for me to

很好,看起来好多了!我们可以看到重复不再出现。然而,n-gram惩罚必须谨慎使用。一篇关于城市纽约的文章不应该使用2-gram惩罚,否则城市名称只会出现在整个文本中一次!

束搜索的另一个重要特点是,我们可以在生成后比较排名靠前的光束,并选择最符合我们目的的生成光束。

transformers中,我们只需将参数num_return_sequences设置为应返回的最高分光束的数量。但请确保num_return_sequences <= num_beams

# set return_num_sequences > 1
beam_outputs = model.generate(
    **model_inputs,
    max_new_tokens=40,
    num_beams=5,
    no_repeat_ngram_size=2,
    num_return_sequences=5,
    early_stopping=True
)

# now we have 3 output sequences
print("Output:\n" + 100 * '-')
for i, beam_output in enumerate(beam_outputs):
  print("{}: {}".format(i, tokenizer.decode(beam_output, skip_special_tokens=True)))
Output:
----------------------------------------------------------------------------------------------------
0: I enjoy walking with my cute dog, but I'm not sure if I'll ever be able to walk with him again.

I've been thinking about this for a while now, and I think it's time for me to
1: I enjoy walking with my cute dog, but I'm not sure if I'll ever be able to walk with her again.

I've been thinking about this for a while now, and I think it's time for me to
2: I enjoy walking with my cute dog, but I'm not sure if I'll ever be able to walk with him again.

I've been thinking about this for a while now, and I think it's a good idea to
3: I enjoy walking with my cute dog, but I'm not sure if I'll ever be able to walk with him again.

I've been thinking about this for a while now, and I think it's time to take a
4: I enjoy walking with my cute dog, but I'm not sure if I'll ever be able to walk with him again.

I've been thinking about this for a while now, and I think it's a good idea.

可以看出,这五个光束假设彼此之间只有细微差别——在使用仅五个光束时,这并不令人意外。

在开放式生成中,有几个原因被提出,说明为什么束搜索可能不是最佳选择

  • 束搜索在所需的生成长度或多或少可预测的任务中非常有效,例如机器翻译或摘要——参见Murray 等人 (2018)Yang 等人 (2018)。但这不适用于开放式生成,其中所需的输出长度可能变化很大,例如对话和故事生成。

  • 我们已经看到束搜索严重受到重复生成的影响。这在故事生成中尤其难以通过 N-gram 或其他惩罚进行控制,因为在抑制重复和重复相同 N-gram 的循环之间找到一个好的权衡需要大量的微调。

  • Ari Holtzman 等人 (2019)所论证的,高质量的人类语言不遵循高概率下一个词的分布。换句话说,作为人类,我们希望生成的文本能让我们感到惊喜,而不是无聊/可预测。作者通过绘制模型对人类文本的概率与束搜索所做的对比,很好地展示了这一点。

alt
text

所以,让我们停止无聊,引入一些随机性🤪。

采样

最基本的采样形式,意味着根据其条件概率分布随机选择下一个词wtw_t

wtP(ww1:t1) w_t \sim P(w|w_{1:t-1})

以上述为例,下图展示了采样时的语言生成。

sampling search

很明显,使用采样进行语言生成不再是确定性的。词("car")(\text{"car"})是从条件概率分布P(w"The")P(w | \text{"The"})中采样的,接着从P(w"The","car")P(w | \text{"The"}, \text{"car"})中采样了("drives")(\text{"drives"})

transformers中,我们设置do_sample=True并通过top_k=0禁用Top-K采样(稍后会详细介绍)。接下来,我们将为演示目的固定随机种子。您可以随意更改set_seed参数以获得不同的结果,或将其删除以实现非确定性。

# set seed to reproduce results. Feel free to change the seed though to get different results
from transformers import set_seed
set_seed(42)

# activate sampling and deactivate top_k by setting top_k sampling to 0
sample_output = model.generate(
    **model_inputs,
    max_new_tokens=40,
    do_sample=True,
    top_k=0
)

print("Output:\n" + 100 * '-')
print(tokenizer.decode(sample_output[0], skip_special_tokens=True))
Output:
----------------------------------------------------------------------------------------------------
I enjoy walking with my cute dog for the rest of the day, but this had me staying in an unusual room and not going on nights out with friends (which will always be wondered for a mere minute or so at this point).

有意思!文本看起来还不错——但仔细观察,它不太连贯,听起来不像人类写的。这就是采样词序列的一大问题:模型经常生成不连贯的胡言乱语,参看Ari Holtzman 等人 (2019)

一个技巧是通过降低softmax的所谓temperature来使分布P(ww1:t1)P(w|w_{1:t-1})变得更尖锐(增加高概率词的可能性,降低低概率词的可能性)。

将温度应用于我们上面的示例,可能看起来如下所示。

sampling temp search

步骤t=1t=1的条件下一个词分布变得更加尖锐,几乎没有机会选择词("car")(\text{"car"})

让我们看看如何通过设置temperature=0.6来降低库中分布的温度

# set seed to reproduce results. Feel free to change the seed though to get different results
set_seed(42)

# use temperature to decrease the sensitivity to low probability candidates
sample_output = model.generate(
    **model_inputs,
    max_new_tokens=40,
    do_sample=True,
    top_k=0,
    temperature=0.6,
)

print("Output:\n" + 100 * '-')
print(tokenizer.decode(sample_output[0], skip_special_tokens=True))
Output:
----------------------------------------------------------------------------------------------------
I enjoy walking with my cute dog, but I don't like to chew on it. I like to eat it and not chew on it. I like to be able to walk with my dog."

So how did you decide

好的。奇怪的n-gram减少了,输出现在更连贯了一些!虽然应用温度可以使分布的随机性降低,但在极限情况下,当设置temperature0\to 0时,温度标度采样会等同于贪婪解码,并会遇到与之前相同的问题。

Top-K 采样

Fan 等人 (2018)提出了一种简单但非常强大的采样方案,称为Top-K采样。在Top-K采样中,过滤掉K个最有可能的下一个词,并将概率质量重新分配给这K个词。GPT2采用了这种采样方案,这也是它在故事生成中取得成功的原因之一。

我们将上述示例中用于两个采样步骤的词范围从3个词扩展到10个词,以便更好地说明Top-K采样。

Top K sampling

在设置K=6K = 6后,在两个采样步骤中,我们将采样池限制为6个词。虽然在第一步中,定义为Vtop-KV_{\text{top-K}}的6个最有可能的词仅占总概率质量的约三分之二,但在第二步中几乎包含了所有概率质量。尽管如此,我们看到它成功地消除了第二步中那些相当奇怪的候选词(“not",“the",“small",“told")(\text{``not"}, \text{``the"}, \text{``small"}, \text{``told"})

让我们看看如何在库中使用Top-K,通过设置top_k=50

# set seed to reproduce results. Feel free to change the seed though to get different results
set_seed(42)

# set top_k to 50
sample_output = model.generate(
    **model_inputs,
    max_new_tokens=40,
    do_sample=True,
    top_k=50
)

print("Output:\n" + 100 * '-')
print(tokenizer.decode(sample_output[0], skip_special_tokens=True))
Output:
----------------------------------------------------------------------------------------------------
I enjoy walking with my cute dog for the rest of the day, but this time it was hard for me to figure out what to do with it. (One reason I asked this for a few months back is that I had a

一点也不坏!这段文本可以说是迄今为止最像人类的文本了。然而,Top-K 采样有一个问题,就是它不能动态地调整从下一个词概率分布P(ww1:t1)P(w|w_{1:t-1})中过滤的词数。这可能会有问题,因为有些词可能来自一个非常尖锐的分布(上图右侧的分布),而另一些词则来自一个更平坦的分布(上图左侧的分布)。

在步骤t=1t=1中,Top-K消除了采样("people","big","house","cat")(\text{"people"}, \text{"big"}, \text{"house"}, \text{"cat"})的可能性,这些词看起来是合理的候选词。另一方面,在步骤t=2t=2中,该方法将可以说是不合适的词("down","a")(\text{"down"}, \text{"a"})包含在词采样池中。因此,将采样池限制为固定大小K可能会导致模型为尖锐分布产生胡言乱语,并限制模型对平坦分布的创造力。这种直觉促使Ari Holtzman 等人 (2019)创建了Top-p-或nucleus-采样。

Top-p (nucleus) 采样

Top-p采样中,不是仅从最有可能的K个词中采样,而是从累积概率超过概率p的最小词集合中选择。然后,概率质量将重新分配到这个词集合中。通过这种方式,词集合的大小(即集合中词的数量)可以根据下一个词的概率分布动态增加和减少。好吧,这说得太多了,让我们可视化一下。

Top p sampling

设置p=0.92p=0.92后,Top-p采样会选择最少数量的词,使其累积概率共同超过p=92%p=92\%的概率质量,定义为Vtop-pV_{\text{top-p}}。在第一个示例中,这包含了9个最可能的词,而在第二个示例中,它只需要选择前3个词就能超过92%。实际上很简单!可以看出,在下一个词不太可预测的情况下,它保留了广泛的词汇,例如P(w"The”)P(w | \text{"The''}),而在下一个词看起来更可预测的情况下,它只保留了少数词,例如P(w"The","car")P(w | \text{"The"}, \text{"car"})

好的,是时候在transformers中查看了!我们通过设置0 < top_p < 1来激活Top-p采样

# set seed to reproduce results. Feel free to change the seed though to get different results
set_seed(42)

# set top_k to 50
sample_output = model.generate(
    **model_inputs,
    max_new_tokens=40,
    do_sample=True,
    top_p=0.92,
    top_k=0
)

print("Output:\n" + 100 * '-')
print(tokenizer.decode(sample_output[0], skip_special_tokens=True))
Output:
----------------------------------------------------------------------------------------------------
I enjoy walking with my cute dog for the rest of the day, but this had me staying in an unusual room and not going on nights out with friends (which will always be my yearning for such a spacious screen on my desk

太棒了,这听起来像是人类写的。嗯,也许还没完全达到。

虽然理论上 Top-p 看起来比 Top-K 更优雅,但两种方法在实践中都表现良好。Top-p 也可以与 Top-K 结合使用,这样可以避免非常低排名的词,同时允许一些动态选择。

最后,为了获得多个独立采样的输出,我们可以再次将参数num_return_sequences设置为> 1

# set seed to reproduce results. Feel free to change the seed though to get different results
set_seed(42)

# set top_k = 50 and set top_p = 0.95 and num_return_sequences = 3
sample_outputs = model.generate(
    **model_inputs,
    max_new_tokens=40,
    do_sample=True,
    top_k=50,
    top_p=0.95,
    num_return_sequences=3,
)

print("Output:\n" + 100 * '-')
for i, sample_output in enumerate(sample_outputs):
  print("{}: {}".format(i, tokenizer.decode(sample_output, skip_special_tokens=True)))
Output:
----------------------------------------------------------------------------------------------------
0: I enjoy walking with my cute dog for the rest of the day, but this time it was hard for me to figure out what to do with it. When I finally looked at this for a few moments, I immediately thought, "
1: I enjoy walking with my cute dog. The only time I felt like walking was when I was working, so it was awesome for me. I didn't want to walk for days. I am really curious how she can walk with me
2: I enjoy walking with my cute dog (Chama-I-I-I-I-I), and I really enjoy running. I play in a little game I play with my brother in which I take pictures of our houses.

太棒了,现在你应该掌握所有工具,让你的模型用transformers编写你的故事了!

结论

作为即时解码方法,在开放式语言生成中,top-ptop-K采样似乎比传统的贪婪搜索和搜索产生更流畅的文本。有证据表明,贪婪搜索和搜索的明显缺陷——主要是生成重复的词序列——是由模型(特别是模型的训练方式)造成的,而不是解码方法造成的,参见Welleck et al. (2019)。此外,如Welleck et al. (2020)所示,top-Ktop-p采样似乎也存在生成重复词序列的问题。

Welleck et al. (2019)中,作者通过人类评估表明,在调整模型训练目标后,束搜索可以生成比 Top-p 采样更流畅的文本。

开放式语言生成是一个快速发展的研究领域,通常情况下没有一刀切的方法,因此必须根据具体用例看哪种方法效果最好。

幸运的是,可以在transfomers🤗中尝试所有不同的解码方法——您可以在这里查看可用方法的概述。

感谢所有为这篇博客文章做出贡献的人:Alexander Rush、Julien Chaumand、Thomas Wolf、Victor Sanh、Sam Shleifer、Clément Delangue、Yacine Jernite、Oliver Åstrand 和 John de Wasseige。

附录

generate 已经发展成为一种高度可组合的方法,其标志可以从许多本博客文章未涵盖的方向操纵生成文本。以下是一些有用的页面可供参考:

如果您觉得我们的文档难以导航,并且无法轻松找到您要查找的内容,请在此 GitHub issue中给我们留言。您的反馈对于我们未来的方向至关重要!🤗

社区

注册登录 发表评论