解码的生成多样性

是大模型呀

Posted by BY J on August 14, 2023

解码的生成多样性

原文出处:

对于自回归文本生成任务中的预训练模型,每一个框架都有对应的generate生成方法,一般封装在各自的GnenerationMixin class中。

这个类对外提供generate()方法,可以用于:

  • greedy decoding: 当num_beams=1且do_sample=False,调用greedy_search()方法
  • multinomial: 当num_beams=1且do_sample=True,调用sample()方法
  • beam-search: 当num_beams > 1且do_sample=False, 调用beam_search()方法
  • beam-search multinomial: 当 num_beams > 1且do_sample=True,调用beam_sample()方法
  • diverse beam-search: 当num_beams > 1且num_beam_groups > 1 调用 group_beam_search()方法
  • constrained beam-search: 当constraints ≠ None或force_words_ids ≠ None, 调用constrained_beam_search()方法

笔者在实际操作过程中,发现一些参数对实际生成效果会有不少的影响,所以在讲解具体的参数之前,先回顾下主要的解码算法原理。

自回归语言生成任务

自回归语言生成任务基于如下假设: 一个文本序列的概率分布可以分解为每个词基于其上文的条件概率的乘积。

上式中, 是初始上下文单词序列。文本序列的长度 通常是变的,并且对应于时间步 。$$P(w_{t} w_{1:t-1}, w_0)$$的词表中已包含终止符(EOS)。

Greedy Search(贪心搜索)

贪心搜索在每个时间步都简单的选择概率最高的词作为当前输出词: $$w_t = argmax_{w} P(w w_{1:t-1})$$,如下图所示

Untitled

从单词开始,算法在第一步贪心的选择条件概率最高的词作为输出,依次往后。最终生成的单词序列为,其联合概率为

下面,我们输入文本序列给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

根据上文生成的单词是合理的,但模型很快开始输出重复文本!这在语言生成中是一个非常普遍的问题,在贪心搜索和波束搜索中更是如此-详见论文1与论文2

贪心搜索的主要缺点是它错过了隐藏在低概率词后面的高概率词,如上图所示:

条件概率为的单词隐藏在单词的后面,而因为在时条件概率值只排第二所以未被选择,因此贪心搜索会错过序列

幸好可以用波束搜索来缓解这个问题!

波束搜索

波束搜索通过在每个时间步保留最可能的nums_beams个词,并从中最终选择出概率最高的序列来降低丢失潜在的高概率序列的风险。以num_beams=2为例:

Untitled

在时间步1,除了最有可能的假设,波束搜索还跟踪第二可能的假设在时间步2,波束搜索发现序列概率为,比更高。在此例中,它已经找到了最有可能的序列。

波束搜索一般都会找到比贪心搜索概率更高的输出序列,但仍不保证找到全局最优解。

我们设置num_beams > 1和early_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个词的词序列)惩罚,该方法是由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)所论证的那样,高质量的人类语言并不遵循最大概率法则。换句话说,作为人类,我们希望生成的文本能让我们感到惊喜,而可预测的文本使人感到无聊。论文作者画了一个概率图,很好的展示了这一点,从图中可以看出人类文本带来的惊喜度比波束搜索好不少。

Untitled

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

采样

在其最基本的形式中,采样意味着根据当前条件概率分布随机选择输出词

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

Untitled

很明显,使用采样方法时文本生成本身不再是确定性的。单词从条件概率分布$$P(w \text{The})\text{drives}P(w \text{The}\space\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 scene和local batte harness 非常奇怪,看起来不像是人写的。这就是对单词序列进行采样时的大问题:模型通常会产生不连贯的乱码,参见Ari Holtzman(2019)的论文。

缓解这一问题的一个技巧是通过降低所谓的softmax的温度使分布 $$P(w w_{1:t-1})$$ 更陡峭。而降低“温度”,本质上是增加高概率单词的似然并降低低概率单词的似然。

将温度应用到于我们的例子中后,结果如下图所示:

Untitled

的时刻单词的条件分布变得更加陡峭,几乎没有机会选择单词了。

让我们看看如何通过设置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))

可以看到,奇怪的n-gram变少了,现在输出更连贯了。虽然温度可以使分布的随机性降低,但极限条件下,当“温度”设置为0时,温度缩放采样就退化成贪心解码了,因此会遇到与贪心解码相同的问题。

Top-K采样

Fan(2018)等人的论文介绍了一种简单但非常强大的采样方案,成为Top-K采样。在Top-K采样中,概率最大的K个单词会被选出,然后这K个词的概率会被重新归一化,最后就在这重新被归一化概率后的K个词中采样。GPT2采用了这种方案,这也是它在故事生成这样的任务上取得成功的原因之一。

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

Untitled

,即我们将在两个采样步的采样池大小限制为6个单词。我们定义6个最有可能的词的集合为。在第一步中,仅占概率的大约三分之二,但在第二步,它几乎占了全部的概率。同时,我们可以看到在第二步该方法成功的消除了那些奇怪的候选词

我们设置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$$可能并不普适。

时,Top-K将排出了采样池,而这些词似乎是合理的候选词。另一方面,在时,该方法却又把不太合适的纳入了采样池。因此,将采样池限制为固定大小K可能会在分布比较尖锐的时候产生胡言乱语,而在分布比较平坦的时候限制模型的创造力。这一发现促使Ari Holtzman(2019)发明了Top-P或核-采样。

Top-p(核)采样

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

Untitled

假设,Top-p采样对单词概率进行降序排列并累加,然后选择概率和首次超过%的单词集作为采样池,定义为。在有9个词,而在时它只需要选择前3个词就超过了%。其实很简单吧!可以看出,在单词比较不可预测时,它保留了更多的候选词,如$$P(w \text{The})P(w \text{The},\text{car})$$。

我们可以通过设置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.

总结

在开放域语言生成场景中,作为最新的解码方法, top-p 和 top-K 采样于传统的 贪心 和 波束 搜索相比,似乎能产生更流畅的文本。但,最近有更多的证据表明 贪心 和 波束 搜索的明显缺陷 - 主要是生成重复的单词序列 - 是由模型 (特别是模型的训练方式) 引起的,而不是解码方法, 参见 Welleck 等人 (2019) 的论文。此外,如 Welleck 等人 (2020) 的论文所述,看起来 top-K 和 top-p 采样也会产生重复的单词序列。

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

开放域语言生成是一个快速发展的研究领域,而且通常情况下这里没有放之四海而皆准的方法,因此必须了解哪种方法最适合自己的特定场景。

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

以上是对如何在 transformers 中使用不同的解码方法以及开放域语言生成的最新趋势的简要介绍。

附录

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 函数 手册