关于人工智能:在Transformers-中使用约束波束搜索引导文本生成

3次阅读

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

引言

本文假如读者曾经相熟文本生成畛域波束搜寻相干的背景常识,具体可参见博文 如何生成文本: 通过 Transformers 用不同的解码办法生成文本。

与一般的波束搜寻不同,束缚 波束搜寻容许咱们管制所生成的文本。这很有用,因为有时咱们确切地晓得输入中须要蕴含什么。例如,在机器翻译工作中,咱们可能通过查字典曾经晓得哪些词必须蕴含在最终的译文中; 而在某些特定的场合中,尽管某几个词对于语言模型而言差不多,但对最终用户而言可能却相差很大。这两种状况都能够通过容许用户通知模型最终输入中必须蕴含哪些词来解决。

这事儿为什么这么难

然而,这个事件操作起来并不容易,它要求咱们在生成过程中的 某个时刻 在输入文本的 某个地位 强制生成某些特定子序列。

假如咱们要生成一个句子 S,它必须依照先 \(t_ 1 \) 再 \(t_ 1 \) 的程序蕴含短语 \(p_1={ t_1, t_2} \)。以下定义了咱们心愿生成的句子 \(S \):

\(S_{冀望} = {s_1, s_2, …, s_k, t_1, t_2, s_{k+1}, …, s_n } \)

问题是波束搜寻是逐词输入文本的。咱们能够大抵将波束搜寻视为函数 \(B(\mathbf{s}_{0:i}) = s_{i+1} \),它依据以后生成的序列 \(\mathbf{s}_{0:i} \) 预测下一时刻 \(i+ 1 \) 的输入。然而这个函数在任意时刻 \(i < k \) 怎么晓得,将来的某个时刻 \(k \) 必须生成某个指定词?或者当它在时刻 \(i=k \) 时,它如何确定以后那个指定词的最佳地位,而不是将来的某一时刻 \(i>k \)?

如果你同时有多个不同的束缚怎么办?如果你想同时指定应用短语 \(p_1={t_1, t_2} \) 短语 \(p_2={ t_3, t_4, t_5, t_6} \) 怎么办?如果你心愿模型在两个短语之间 任选一个 怎么办?如果你想同时指定应用短语 \(p_1 \) 以及短语列表 \({p_{21}, p_{22}, p_{23}} \) 中的任一短语怎么办?

上述需要在理论场景中是很正当的需要,下文介绍的新的束缚波束搜寻性能能够满足所有这些需要!

咱们会先简要介绍一下新的 束缚波束搜寻 能够做些什么,而后再深刻介绍其原理。

例 1: 指定蕴含某词

假如咱们要将 "How old are you?" 翻译成德语。它对应两种德语表白,其中 "Wie alt bist du?" 是非正式场合的表白,而 "Wie alt sind Sie?" 是正式场合的表白。

不同的场合,咱们可能偏向于不同的表白,但咱们如何通知模型呢?

应用传统波束搜寻

咱们先看下如何应用 传统波束搜寻 来实现翻译。

!pip install -q git+https://github.com/huggingface/transformers.git
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM

tokenizer = AutoTokenizer.from_pretrained("t5-base")
model = AutoModelForSeq2SeqLM.from_pretrained("t5-base")

encoder_input_str = "translate English to German: How old are you?"

input_ids = tokenizer(encoder_input_str, return_tensors="pt").input_ids

outputs = model.generate(
    input_ids,
    num_beams=10,
    num_return_sequences=1,
    no_repeat_ngram_size=1,
    remove_invalid_values=True,
)

print("Output:\n" + 100 *'-')
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
Output:
----------------------------------------------------------------------------------------------------
Wie alt bist du?

应用束缚波束搜寻

然而如果咱们想要一个正式的表白而不是非正式的表白呢?如果咱们曾经先验地晓得输入中必须蕴含什么,咱们该如何 将其 注入到输入中呢?

咱们能够通过 model.generate()force_words_ids 参数来实现这一性能,代码如下:

tokenizer = AutoTokenizer.from_pretrained("t5-base")
model = AutoModelForSeq2SeqLM.from_pretrained("t5-base")

encoder_input_str = "translate English to German: How old are you?"

force_words = ["Sie"]

input_ids = tokenizer(encoder_input_str, return_tensors="pt").input_ids
force_words_ids = tokenizer(force_words, add_special_tokens=False).input_ids

outputs = model.generate(
    input_ids,
    force_words_ids=force_words_ids,
    num_beams=5,
    num_return_sequences=1,
    no_repeat_ngram_size=1,
    remove_invalid_values=True,
)

print("Output:\n" + 100 *'-')
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
Output:
----------------------------------------------------------------------------------------------------
Wie alt sind Sie?

如你所见,当初咱们能用咱们对输入的先验常识来领导文本的生成。以前咱们必须学生成一堆候选输入,而后手动从中挑选出合乎咱们要求的输入。当初咱们能够间接在生成阶段做到这一点。

例 2: 析取式束缚

在下面的例子中,咱们晓得须要在最终输入中蕴含哪些单词。这方面的一个例子可能是在神经机器翻译过程中联合应用字典。

然而,如果咱们不晓得要应用哪种 词形_呢,咱们可能心愿应用单词 rain 但对其不同的词性没有偏好,即 ["raining", "rained", "rains", ...] 是等概的。更个别地,很多状况下,咱们可能并不刻板地心愿 _逐字母统一,此时咱们心愿划定一个范畴由模型去从中抉择最合适的。

反对这种行为的束缚叫 析取式束缚 (Disjunctive Constraints),其容许用户输出一个单词列表来疏导文本生成,最终输入中仅须蕴含该列表中的 至多一个 词即可。

上面是一个混合应用上述两类束缚的例子:

from transformers import GPT2LMHeadModel, GPT2Tokenizer

model = GPT2LMHeadModel.from_pretrained("gpt2")
tokenizer = GPT2Tokenizer.from_pretrained("gpt2")

force_word = "scared"
force_flexible = ["scream", "screams", "screaming", "screamed"]

force_words_ids = [tokenizer([force_word], add_prefix_space=True, add_special_tokens=False).input_ids,
    tokenizer(force_flexible, add_prefix_space=True, add_special_tokens=False).input_ids,
]

starting_text = ["The soldiers", "The child"]

input_ids = tokenizer(starting_text, return_tensors="pt").input_ids

outputs = model.generate(
    input_ids,
    force_words_ids=force_words_ids,
    num_beams=10,
    num_return_sequences=1,
    no_repeat_ngram_size=1,
    remove_invalid_values=True,
)

print("Output:\n" + 100 *'-')
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
print(tokenizer.decode(outputs[1], skip_special_tokens=True))
Setting `pad_token_id` to `eos_token_id`:50256 for open-end generation.

Output:
----------------------------------------------------------------------------------------------------
The soldiers, who were all scared and screaming at each other as they tried to get out of the
The child was taken to a local hospital where she screamed and scared for her life, police said.

如你所见,第一个输入里有 "screaming",第二个输入里有 "screamed",同时它们都原原本本地蕴含了 "scared"。留神,其实 ["screaming", "screamed", ...] 列表中不用肯定是同一单词的不同词形,它能够是任何单词。应用这种形式,能够满足咱们只须要从候选单词列表中抉择一个单词的利用场景。

传统波束搜寻

以下是传统 波束搜寻 的一个例子,摘自之前的 博文:

与贪婪搜寻不同,波束搜寻会保留更多的候选词。上图中,咱们每一步都展现了 3 个最可能的预测词。

num_beams=3 时,咱们能够将第 1 步波束搜寻示意成下图:

波束搜寻不像贪婪搜寻那样只抉择 "The dog",而是容许将 "The nice""The car" 留待进一步思考

下一步,咱们会为上一步创立的三个分支别离预测可能的下一个词。

尽管咱们 考查 了显著多于 num_beams 个候选词,但在每步完结时,咱们只会输入 num_beams 个最终候选词。咱们不能始终分叉,那样的话,beams 的数目将在 \(n \) 步后变成 \(\text{beams}^{n \) 个,最终变成指数级的增长 (当波束数为 \( 10 \) 时,在 \(10 \) 步之后就会变成 \(10,000,000,000 \) 个分支!)。

接着,咱们反复上述步骤,直到满足停止条件,如生成 <eos> 标记或达到 max_length。整个过程能够总结为: 分叉、排序、剪枝,如此往返。

束缚波束搜寻

束缚波束搜寻试图通过在每一步生成过程中 _注入_所需词来满足束缚。

假如咱们试图指定输入中须蕴含短语 "is fast"

在传统波束搜寻中,咱们在每个分支中找到 k 个概率最高的候选词,以供下一步应用。在束缚波束搜寻中,除了执行与传统波束搜寻雷同的操作外,咱们还会试着把束缚词加进去,以 _看看咱们是否能尽量满足束缚_。图示如下:

上图中,咱们最终候选词除了包含像 "dog""nice" 这样的高概率词之外,咱们还把 "is" 塞了进去,以尽量满足生成的句子中须含 "is fast" 的束缚。

第二步,每个分支的候选词抉择与传统的波束搜寻大部分相似。惟一的不同是,与下面第一步一样,束缚波束搜寻会在每个新分叉上持续强加束缚,把满足束缚的候选词强加进来,如下图所示:

组 (Banks)

在探讨下一步之前,咱们停下来思考一下上述办法的缺点。

在输入中横蛮地强制插入束缚短语 is fast 的问题在于,大多数状况下,你最终会失去像下面的 The is fast 这样的无意义输入。咱们须要解决这个问题。你能够从 huggingface/transformers 代码库中的这个 问题 中理解更多无关这个问题及其复杂性的深刻探讨。

组办法通过在满足束缚和产生正当输入两者之间获得均衡来解决这个问题。

咱们把所有候选波束依照其 满足了多少步束缚 分到不同的组中,其中组 \(n \) 里蕴含的是 满足了 \(n \) 步束缚的波束列表。而后咱们依照程序轮流抉择各组的候选波束。在上图中,咱们先从组 2 (Bank 2) 中抉择概率最大的输入,而后从组 1 (Bank 1) 中抉择概率最大的输入,最初从组 0 (Bank 0) 中抉择最大的输入; 接着咱们从组 2 (Bank 2) 中抉择概率次大的输入,从组 1 (Bank 1) 中抉择概率次大的输入,依此类推。因为咱们应用的是 num_beams=3,所以咱们只需执行上述过程三次,就能够失去 ["The is fast", "The dog is", "The dog and"]

这样,即便咱们 强制 模型思考咱们手动增加的束缚词分支,咱们仍然会跟踪其余可能更有意义的高概率序列。只管 The is fast 齐全满足束缚,但这并不是一个有意义的短语。侥幸的是,咱们有 "The dog is""The dog and" 能够在将来的步骤中应用,心愿在未来这会产生更有意义的输入。

图示如下 (以上例的第 3 步为例):

请留神,上图中不须要强制增加 "The is fast",因为它曾经被蕴含在概率排序中了。另外,请留神像 "The dog is slow""The dog is mad" 这样的波束实际上是属于组 0 (Bank 0) 的,为什么呢?因为只管它蕴含词 "is",但它不可用于生成 "is fast",因为 fast 的位子曾经被 slowmad 占掉了,也就杜绝了后续能生成 "is fast" 的可能性。从另一个角度讲,因为 slow 这样的词的退出,该分支 满足束缚的进度 被重置成了 0。

最初请留神,咱们最终生成了蕴含束缚短语的正当输入: "The dog is fast"

起初咱们很放心,因为自觉地增加束缚词会导致呈现诸如 "The is fast" 之类的无意义短语。然而,应用基于组的轮流抉择办法,咱们最终隐式地解脱了无意义的输入,优先选择了更正当的输入。

对于 Constraint 类的更多信息及自定义束缚

咱们总结下要点。每一步,咱们都一直地纠缠模型,强制增加束缚词,同时也跟踪不满足束缚的分支,直到最终生成蕴含所需短语的正当的高概率序列。

在实现时,咱们的次要办法是将每个束缚示意为一个 Constraint 对象,其目标是跟踪满足束缚的进度并通知波束搜寻接下来要生成哪些词。只管咱们能够应用 model.generate() 的关键字参数 force_words_ids,但应用该参数时后端理论产生的状况如下:

from transformers import AutoTokenizer, AutoModelForSeq2SeqLM, PhrasalConstraint

tokenizer = AutoTokenizer.from_pretrained("t5-base")
model = AutoModelForSeq2SeqLM.from_pretrained("t5-base")

encoder_input_str = "translate English to German: How old are you?"

constraints = [
    PhrasalConstraint(tokenizer("Sie", add_special_tokens=False).input_ids
    )
]

input_ids = tokenizer(encoder_input_str, return_tensors="pt").input_ids

outputs = model.generate(
    input_ids,
    constraints=constraints,
    num_beams=10,
    num_return_sequences=1,
    no_repeat_ngram_size=1,
    remove_invalid_values=True,
)

print("Output:\n" + 100 *'-')
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
Output:
----------------------------------------------------------------------------------------------------
Wie alt sind Sie?

你甚至能够定义一个本人的束缚并将其通过 constraints 参数输出给 model.generate()。此时,你只须要创立 Constraint 形象接口类的子类并遵循其要求即可。你能够在 此处 的 Constraint 定义中找到更多信息。

咱们还能够尝试其余一些有意思的束缚 (尚未实现,兴许你能够试一试!) 如 OrderedConstraintsTemplateConstraints 等。目前,在最终输入中束缚短语间是无序的。例如,后面的例子一个输入中的束缚短语程序为 scared -> screaming,而另一个输入中的束缚短语程序为 screamed -> scared。如果有了 OrderedConstraints,咱们就能够容许用户指定束缚短语的程序。TemplateConstraints 的性能更小众,其束缚能够像这样:

starting_text = "The woman"
template = ["the", "","School of","", "in"]

possible_outputs == [
   "The woman attended the Ross School of Business in Michigan.",
   "The woman was the administrator for the Harvard School of Business in MA."
]

或是这样:

starting_text = "The woman"
template = ["the", "","", "University", "","in"]

possible_outputs == ["The woman attended the Carnegie Mellon University in Pittsburgh.",]
impossible_outputs == ["The woman attended the Harvard University in MA."]

或者,如果用户不关怀两个词之间应该隔多少个词,那仅用 OrderedConstraint 就能够了。

总结

束缚波束搜寻为咱们提供了一种将内部常识和需要注入文本生成过程的灵便办法。以前,没有一个简略的办法可用于通知模型 1. 输入中须要蕴含某列表中的词或短语,其中 2. 其中有一些是可选的,有些必须蕴含的,这样 3. 它们能够最终生成至在正当的地位。当初,咱们能够通过综合应用 Constraint 的不同子类来齐全管制咱们的生成!

该新个性次要基于以下论文:

  • Guided Open Vocabulary Image Captioning with Constrained Beam Search
  • Fast Lexically Constrained Decoding with Dynamic Beam Allocation for Neural Machine Translation
  • Improved Lexically Constrained Decoding for Translation and Monolingual Rewriting
  • Guided Generation of Cause and Effect

与上述这些工作一样,还有许多新的钻研正在摸索如何应用内部常识 (例如 KG (Knowledge Graph)、KB (Knowledge Base) ) 来领导大型深度学习模型输入。咱们心愿束缚波束搜寻性能成为实现此目标的无效办法之一。

感激所有为此性能提供领导的人: Patrick von Platen 参加了从 初始问题 探讨到 最终 PR 的全过程,还有 Narsil Patry,他们二位对代码进行了具体的反馈。

本文应用的图标来自于 Freepik – Flaticon。


英文原文: https://hf.co/blog/constrained-beam-search

原文作者: Chan Woo Kim

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

审校 / 排版: zhongdongy (阿东)

正文完
 0