关于人工智能:如何生成文本-通过-Transformers-用不同的解码方法生成文本

6次阅读

共计 12750 个字符,预计需要花费 32 分钟才能阅读完成。

简介

近年来,随着以 OpenAI GPT2 模型 为代表的基于数百万网页数据训练的大型 Transformer 语言模型的衰亡,凋谢域语言生成畛域吸引了越来越多的关注。凋谢域中的条件语言生成成果令人印象粗浅,典型的例子有: GPT2 在独角兽话题上的精彩续写,XLNet 以及 应用 CTRL 模型生成受控文本 等。促成这些停顿的除了 transformer 架构的改良和大规模无监督训练数据外,更好的解码办法 也施展了不可或缺的作用。

本文简述了不同的解码策略,同时向读者展现了如何应用风行的 transformers 库轻松实现这些解码策略!

下文中的所有性能均可用于 自回归 语言生成工作 (点击 此处 回顾)。简略温习一下, 自回归 语言生成是基于如下假如: 一个文本序列的概率分布能够合成为每个词基于其上文的条件概率的乘积。

\(P(w_{1:T} | W_0 ) = \prod_{t=1}^T P(w_{t} | w_{1: t-1}, W_0) \text{, 其中} w_{1: 0} = \emptyset, \)

上式中,\(W_0 \) 是初始 上下文 单词序列。文本序列的长度 \(T \) 通常时变的,并且对应于工夫步 \(t=T \)。\(P(w_{t} | w_{1: t- 1}, W_{0}) \) 的词表中已蕴含 终止符 (End Of Sequence,EOS)。transformers 目前已反对的自回归语言生成工作包含 GPT2XLNetOpenAi-GPTCTRLTransfoXLXLMBartT5 模型,并反对 PyTorch 和 TensorFlow (>= 2.0) 两种框架!

咱们会介绍目前最罕用的解码办法,次要有 贪婪搜寻 (Greedy search)波束搜寻 (Beam search)Top-K 采样 (Top-K sampling) 以及 Top-p 采样 (Top-p sampling)

在此之前,咱们先疾速装置一下 transformers 并把模型加载进来。本文咱们用 GPT2 模型在 TensorFlow 2.1 中进行演示,但 API 和应用 PyTorch 框架是一一对应的。

!pip install -q git+https://github.com/huggingface/transformers.git
!pip install -q tensorflow==2.1
import tensorflow as tf
from transformers import TFGPT2LMHeadModel, 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)

贪婪搜寻

贪婪搜寻在每个工夫步 \(t \) 都简略地抉择概率最高的词作为以后输入词: \(w_t = argmax_{w}P(w | w_{1:t-1}) \),如下图所示。

从单词 \(\text{“The”} \) 开始,算法在第一步贪婪地抉择条件概率最高的词 \(\text{“nice”} \) 作为输入,依此往后。最终生成的单词序列为 \(\text{“The”}, \text{“nice”}, \text{“woman”} \),其联结概率为 \(0.5 \times 0.4 = 0.2 \)。

上面,咱们输出文本序列 \((\text{“I”}, \text{“enjoy”}, \text{“walking”}, \text{“with”}, \text{“my”}, \text{“cute”}, \text{“dog”}) \) 给 GPT2 模型,让模型生成下文。咱们以此为例看看如何在 transformers 中应用贪婪搜寻:

# encode context the generation is conditioned on
input_ids = tokenizer.encode('I enjoy walking with my cute dog', return_tensors='tf')

# generate text until the output length (which includes the context length) reaches 50
greedy_output = model.generate(input_ids, max_length=50)

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 if I'll

好,咱们曾经用 GPT2 生成了第一个短文本😊。依据上文生成的单词是正当的,但模型很快开始输入反复的文本!这在语言生成中是一个十分广泛的问题,在贪婪搜寻和波束搜寻中仿佛更是如此 – 详见 Vijayakumar 等人,2016 和 Shao 等人,2017 的论文。

贪婪搜寻的次要毛病是它错过了暗藏在低概率词前面的高概率词,如上图所示:

条件概率为 \(0.9 \) 的单词 \(\text{“has”} \) 暗藏在单词 \(\text{“dog”} \) 前面,而 \(\text{“dog”} \) 因为在 t=1 时条件概率值只排第二所以未被抉择,因而贪婪搜寻会错过序列 \(\text{“The”}, \text {“dog”}, \text{“has”} \)。

幸好咱们能够用波束搜寻来缓解这个问题!

波束搜寻

波束搜寻通过在每个工夫步保留最可能的 num_beams 个词,并从中最终抉择出概率最高的序列来升高失落潜在的高概率序列的危险。以 num_beams=2 为例:

在工夫步 1,除了最有可能的假如 \((\text{“The”}, \text{“nice”}) \),波束搜寻还跟踪第二可能的假如 \((\text{“The”}, \text{“dog”}) \)。在工夫步 2,波束搜寻发现序列 \((\text{“The”}, \text{“dog”}, \text{“has”}) \) 概率为 \(0.36\),比 \((\text{“The”}, \text{“nice”}, \text{“woman”}) \) 的 \(0.2 \) 更高。太棒了,在咱们的例子中它曾经找到了最有可能的序列!

波束搜寻个别都会找到比贪婪搜寻概率更高的输入序列,但仍不保障找到全局最优解。

让咱们看看如何在 transformers 中应用波束搜寻。咱们设置 num_beams > 1early_stopping=True 以便在所有波束达到 EOS 时间接完结生成。

# activate beam search and early_stopping
beam_output = model.generate(
    input_ids,
    max_length=50,
    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 if I'll

尽管后果比贪婪搜寻更晦涩,但输入中依然蕴含反复。一个简略的补救措施是引入 n-grams (即间断 n 个词的词序列) 惩办,该办法是由 Paulus 等人 (2017) 和 Klein 等人 (2017) 引入的。最常见的 n-grams 惩办是确保每个 n-gram 都只呈现一次,办法是如果看到以后候选词与其上文所组成的 n-gram 曾经呈现过了,就将该候选词的概率设置为 0。

咱们能够通过设置 no_repeat_ngram_size=2 来试试,这样任意 2-gram 不会呈现两次:

# set no_repeat_ngram_size to 2
beam_output = model.generate(
    input_ids,
    max_length=50,
    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 take a break

不错,看起来好多了!咱们看到生成的文本曾经没有反复了。然而,n-gram 惩办应用时必须审慎,如一篇对于 纽约 这个城市的文章就不应应用 2-gram 惩办,否则,城市名称在整个文本中将只呈现一次!

波束搜寻的另一个重要个性是咱们可能比拟概率最高的几个波束,并抉择最合乎咱们要求的波束作为最终生成文本。

transformers 中,咱们只需将参数 num_return_sequences 设置为需返回的概率最高的波束的数量,记得确保 num_return_sequences <= num_beams

# set return_num_sequences > 1
beam_outputs = model.generate(
    input_ids,
    max_length=50,
    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 take a break
1: 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 get back to
2: 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 take a break
3: 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 get back to
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 time for me to take a step

如咱们所见,五个波束彼此之间仅有大量差异 —— 这在仅应用 5 个波束时难能可贵。

凋谢域文本生成的钻研人员最近提出了几个理由来阐明对该畛域而言波束搜寻可能不是最佳计划:

  • 在机器翻译或摘要等工作中,因为所需生成的长度或多或少都是可预测的,所以波束搜寻成果比拟好 – 参见 Murray 等人 (2018) 和 Yang 等人 (2018) 的工作。但凋谢域文本生成状况有所不同,其输入文本长度可能会有很大差别,如对话和故事生成的输入文本长度就有很大不同。
  • 咱们曾经看到波束搜寻已被证实存在反复生成的问题。在故事生成这样的场景中,很难用 n-gram 或其余惩办来管制,因为在“不反复”和最大可反复 n-grams之间找到一个好的折衷须要大量的微调。
  • 正如 Ari Holtzman 等人 (2019) 所论证的那样,高质量的人类语言并不遵循最大概率法令。换句话说,作为人类,咱们心愿生成的文本能让咱们感到惊喜,而可预测的文本使人感觉无聊。论文作者画了一个概率图,很好地展现了这一点,从图中能够看出人类文本带来的惊喜度比波束搜寻好不少。

因而,让咱们开始玩点刺激的,引入一些随机性🤪。

采样

在其最根本的模式中,采样意味着依据以后条件概率分布随机抉择输入词 \(w_t \) :

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

持续应用上文中的例子,下图可视化了应用采样生成文本的过程。

很显著,应用采样办法时文本生成自身不再是 确定性的。单词 \(\text{“car”} \) 从条件概率分布 \(P(w | \text{“The”}) \) 中采样而得,而 \(\text{“drives”} \) 则采样自 \(P(w | \text{“The”}, \text{“car”}) \)。

transformers 中,咱们设置 do_sample=True 并通过设置 top_k=0 停用 Top-K 采样 (稍后具体介绍)。在下文中,为便于复现,咱们会固定 random_seed=0,但你能够在本人的模型中随便更改 random_seed

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

# activate sampling and deactivate top_k by setting top_k sampling to 0
sample_output = model.generate(
    input_ids,
    do_sample=True,
    max_length=50,
    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. He just gave me a whole new hand sense."But it seems that the dogs have learned a lot from teasing at the local batte harness once they take on the outside."I take

有意思!生成的文本看起来不错 – 但仔细观察会发现它不是很连贯。3-grams new hand senselocal batte harness 十分奇怪,看起来不像是人写的。这就是对单词序列进行采样时的大问题: 模型通常会产生不连贯的乱码,参见 Ari Holtzman 等人 (2019) 的论文。

缓解这一问题的一个技巧是通过升高所谓的 softmax 的“温度”使散布 \(P(w|w_{1:t-1} \) 更平缓。而升高“温度”,实质上是减少高概率单词的似然并升高低概率单词的似然。

将温度利用到于咱们的例子中后,后果如下图所示。

\(t=1 \) 时刻单词的条件散布变得更加平缓,简直没有机会抉择单词 \(\text{“car”} \) 了。

让咱们看看如何通过设置 temperature=0.7 来冷却生成过程:

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

# use temperature to decrease the sensitivity to low probability candidates
sample_output = model.generate(
    input_ids,
    do_sample=True,
    max_length=50,
    top_k=0,
    temperature=0.7
)

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 be at home too much. I also find it a bit weird when I'm out shopping. I am always away from my house a lot, but I do have a few friends

好,奇怪的 n-gram 变少了,当初输入更连贯了!尽管温度能够使散布的随机性升高,但极限条件下,当“温度”设置为 \(0 \) 时,温度缩放采样就进化成贪婪解码了,因而会遇到与贪婪解码雷同的问题。

Top-K 采样

Fan 等人 (2018) 的论文介绍了一种简略但十分弱小的采样计划,称为 Top-K 采样。在 Top-K 采样中,概率最大的 K 个词会被选出,而后这 K 个词的概率会被从新归一化,最初就在这从新被归一化概率后的 K 个词中采样。GPT2 采纳了这种采样计划,这也是它在故事生成这样的工作上取得成功的起因之一。

咱们将上文例子中的候选单词数从 3 个单词扩大到 10 个单词,以更好地阐明 Top-K 采样。

设 \(K = 6\),即咱们将在两个采样步的采样池大小限度为 6 个单词。咱们定义 6 个最有可能的词的汇合为 \(V_{\text{top-K}} \)。在第一步中,\(V_{\text{top-K}} \) 仅占总概率的大概三分之二,但在第二步,它简直占了全副的概率。同时,咱们能够看到在第二步该办法胜利地打消了那些奇怪的候选词 \((\text{“not”}, \text{“the”}, \text{“small”}, \text{“told”}) \)。

咱们以设置 top_k=50 为例看下如何在 transformers 库中应用 Top-K:

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

# set top_k to 50
sample_output = model.generate(
    input_ids,
    do_sample=True,
    max_length=50,
    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. 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.

We hope you'll find this story interesting!

I am from

相当不错!该文本能够说是迄今为止生成的最 “像人” 的文本。当初还有一个问题,Top-K 采样不会动静调整从须要概率分布 \(P(w|w_{1:t-1} \) 中选出的单词数。这可能会有问题,因为某些散布可能是十分尖利 (上图中右侧的散布),而另一些可能更平坦 (上图中左侧的散布),所以对不同的散布应用同一个绝对数 K 可能并不普适。

在 \(t=1 \) 时,Top-K 将 \((\text{“people”}, \text{“big”}, \text{“house”}, \text{“cat”}) \) 排出了采样池,而这些词仿佛是正当的候选词。另一方面,在 \(t=2 \) 时,该办法却又把不太适合的 \((\text{“down”}, \text{“a”}) \) 纳入了采样池。因而,将采样池限度为固定大小 K 可能会在散布比拟尖利的时候产生胡说八道,而在散布比拟平坦的时候限度模型的创造力。这一发现促使 Ari Holtzman 等人 (2019) 创造了 Top-p – 或 – 采样。

Top-p (核) 采样

Top-p 中,采样不只是在最有可能的 K 个单词中进行,而是在累积概率超过概率 p 的最小单词集中进行。而后在这组词中重新分配概率品质。这样,词集的大小 (又名 汇合中的词数) 能够依据下一个词的概率分布动静减少和缩小。好吧,说的很啰嗦,一图胜千言。

假如 \(p=0.92 \),Top-p 采样对单词概率进行降序排列并累加,而后抉择概率和首次超过 \(p=92% \) 的单词集作为采样池,定义为 \(V_{\text{top-p}} \)。在 \(t=1 \) 时 \(V_{\text{top-p}} \) 有 9 个词,而在 \(t=2 \) 时它只须要抉择前 3 个词就超过了 92%。其实很简略吧!能够看出,在单词比拟不可预测时,它保留了更多的候选词, \(P(w | \text{“The”}) \),而当单词仿佛更容易预测时,只保留了几个候选词, \(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
tf.random.set_seed(0)

# deactivate top_k sampling and sample only from 92% most likely words
sample_output = model.generate(
    input_ids,
    do_sample=True,
    max_length=50,
    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. He will never be the same. I watch him play.


Guys, my dog needs a name. Especially if he is found with wings.


What was that? I had a lot o

太好了,这看起来跟人类写的差不多了,尽管还不算齐全是。

尽管从实践上讲,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
tf.random.set_seed(0)

# set top_k = 50 and set top_p = 0.95 and num_return_sequences = 3
sample_outputs = model.generate(
    input_ids,
    do_sample=True,
    max_length=50,
    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. 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
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
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."So as a result, I've got more work in my time," he said.

很酷,当初你领有了所有能够在 transformers 里用模型来帮你写故事的工具了!

总结

在凋谢域语言生成场景中,作为最新的解码办法,top-ptop-K 采样于传统的 贪婪 波束 搜寻相比,仿佛能产生更晦涩的文本。但,最近有更多的证据表明 贪婪 波束 搜寻的显著缺点 – 次要是生成反复的单词序列 – 是由模型 (特地是模型的训练形式) 引起的,而不是解码办法, 参见 Welleck 等人 (2019) 的论文。此外,如 Welleck 等人 (2020) 的论文所述,看起来 top-Ktop-p 采样也会产生反复的单词序列。

在 Welleck 等人 (2019) 的论文中,作者表明,依据人类评估,在调整训练指标后,波束搜寻相比 Top-p 采样能产生更晦涩的文本。

凋谢域语言生成是一个疾速倒退的钻研畛域,而且通常状况下这里没有放之四海而皆准的办法,因而必须理解哪种办法最适宜本人的特定场景。

好的方面是, 能够在 transfomers 中尝试所有不同的解码办法 🤗。

以上是对如何在 transformers 中应用不同的解码办法以及凋谢域语言生成的最新趋势的简要介绍。

十分欢送大家在 Github 代码库 上提供反馈和问题。

如果想要体验下用模型生成故事的乐趣,能够拜访咱们的 web 利用 Writing with Transformers。

感激为本文做出奉献的所有人: Alexander Rush、Julien Chaumand、Thomas Wolf、Victor Sanh、Sam Shleifer、Clément Delangue、Yacine Jernite、Oliver Åstrand 和 John de Wasseige。

附录

generate 办法还有几个注释未提及的参数,这里咱们简要解释一下它们!

  • min_length 用于强制模型在达到 min_length 之前不生成 EOS。这在摘要场景中应用得比拟多,但如果用户想要更长的文本输入,也会很有用。
  • repetition_penalty 可用于对生成反复的单词这一行为进行惩办。它首先由 Keskar 等人 (2019) 引入,在 Welleck 等人 (2019) 的工作中,它是训练指标的一部分。它能够十分无效地避免反复,但仿佛对模型和用户场景十分敏感,其中一个例子见 Github 上的 探讨。
  • attention_mask 可用于屏蔽填充符。
  • pad_token_idbos_token_ideos_token_id: 如果模型默认没有这些 token,用户能够手动抉择其余 token id 来示意它们。

更多信息,请查阅 generate 函数 手册。


英文原文: https://hf.co/blog/how-to-generate

原文作者: Patrick von Platen

译者: Matrix Yao (姚伟峰),英特尔深度学习工程师,工作方向为 transformer-family 模型在各模态数据上的利用及大规模模型的训练推理。

审校 / 排版: zhongdongy (阿东)

正文完
 0