title | thumbnail | authors | translators | ||||||
---|---|---|---|---|---|---|---|---|---|
基于 Transformers 的编码器-解码器模型 |
/blog/assets/05_encoder_decoder/thumbnail.png |
|
|
!pip install transformers==4.2.1
!pip install sentencepiece==0.1.95
Vaswani 等人在其名作 Attention is all you need 中首创了基于 transformer 的编码器-解码器模型,如今已成为自然语言处理 (natural language processing,NLP) 领域编码器-解码器架构的事实标准。
最近基于 transformer 的编码器-解码器模型训练这一方向涌现出了大量关于预训练目标函数的研究,例如 T5、Bart、Pegasus、ProphetNet、Marge等,但它们所使用的网络结构并没有改变。
本文的目的是详细解释如何用基于 transformer 的编码器-解码器架构来对序列到序列(sequence-to-sequence) 问题进行建模。我们将重点关注有关这一架构的数学知识以及如何对该架构的模型进行推理。在此过程中,我们还将介绍 NLP 中序列到序列模型的一些背景知识,并将基于 transformer 的编码器-解码器架构分解为 编码器 和 解码器 这两个部分分别讨论。我们提供了许多图例,并把基于 transformer 的编码器-解码器模型的理论与其在 🤗 transformers 推理场景中的实际应用二者联系起来。请注意,这篇博文不解释如何训练这些模型 —— 我们会在后续博文中涵盖这一方面的内容。
基于 transformer 的编码器-解码器模型是表征学习和模型架构这两个领域多年研究成果的结晶。本文简要介绍了神经编码器-解码器模型的历史,更多背景知识,建议读者阅读由 Sebastion Ruder 撰写的这篇精彩博文。此外,建议读者对自注意力(self-attention)架构有一个基本了解,可以阅读 Jay Alammar 的这篇博文复习一下原始 transformer 模型。
截至本文撰写时,🤗 transformers 库已经支持的编码器-解码器模型有:T5、Bart、MarianMT 以及 Pegasus,你可以从这儿获取相关信息。
本文分 4 个部分:
- 背景 - 简要回顾了神经编码器-解码器模型的历史,重点关注基于 RNN 的模型。
- 编码器-解码器 - 阐述基于 transformer 的编码器-解码器模型,并阐述如何使用该模型进行推理。
- 编码器 - 阐述模型的编码器部分。
- 解码器 - 阐述模型的解码器部分。
每个部分都建立在前一部分的基础上,但也可以单独阅读。
自然语言生成 (natural language generation,NLG)是 NLP 的一个子领域,其任务一般可被建模为序列到序列问题。这类任务可以定义为寻找一个模型,该模型将输入词序列映射为目标词序列,典型的例子有摘要和翻译。在下文中,我们假设每个单词都被编码为一个向量表征。因此,$n$ 个输入词可以表示为
因此,序列到序列问题可以表示为找到一个映射
$$ f: \mathbf{X}{1:n} \to \mathbf{Y}{1:m} $$
Sutskever 等(2014)的工作指出,深度神经网络(deep neural networks,DNN)“尽管灵活且强大,但只能用于拟合输入和输出维度均固定的映射。”
因此,要用使用 DNN 模型
2014 年,Cho 等人和 Sutskever 等人提出使用完全基于递归神经网络(recurrent neural networks,RNN)的编码器-解码器模型来解决序列到序列任务。与 DNN 相比,RNN 支持输出可变数量的目标向量。下面,我们深入了解一下基于 RNN 的编码器-解码器模型的功能。
在推理过程中,RNN 编码器通过连续更新其隐含状态
然后,我们用
下面,我们进一步解释一下。从数学角度讲,解码器定义了给定隐含状态
根据贝叶斯法则,上述分布可以分解为每个目标向量的条件分布的积,如下所示:
$$ p_{\theta_{dec}}(\mathbf{Y}{1:m} |\mathbf{c}) = \prod{i=1}^{m} p_{\theta_{\text{dec}}}(\mathbf{y}i | \mathbf{Y}{0: i-1}, \mathbf{c}) $$
因此,如果模型架构可以在给定所有前驱目标向量的条件下对下一个目标向量的条件分布进行建模的话:
$$ p_{\theta_{\text{dec}}}(\mathbf{y}i | \mathbf{Y}{0: i-1}, \mathbf{c}), \forall i \in {1, \ldots, m}$$
那它就可以通过简单地将所有条件概率相乘来模拟给定隐藏状态
那么基于 RNN 的解码器架构如何建模 $p_{\theta_{\text{dec}}}(\mathbf{y}i | \mathbf{Y}{0: i-1}, \mathbf{c})$ 呢?
从计算角度讲,模型按序将前一时刻的内部隐含状态 $\mathbf{c}{i-1}$ 和前一时刻的目标向量 $\mathbf{y}{i-1}$ 映射到当前内部隐含状态
$$ f_{\theta_{\text{dec}}}(\mathbf{y}{i-1}, \mathbf{c}{i-1}) \to \mathbf{l}_i, \mathbf{c}_i$$
此处,$\mathbf{c}_0$ 为 RNN 编码器的输出。随后,对 logit 向量
$$ p(\mathbf{y}i | \mathbf{l}i) = \textbf{Softmax}(\mathbf{l}i), \text{ 其中 } \mathbf{l}i = f{\theta{\text{dec}}}(\mathbf{y}{i-1}, \mathbf{c}{\text{prev}})$$
更多有关 logit 向量及其生成的概率分布的详细信息,请参阅脚注
目标向量序列 $\mathbf{Y}{1:m}$ 的概率空间非常大,因此在推理时,必须借助解码方法 ${}^5$ 对 $p{\theta_{dec}}(\mathbf{Y}_{1:m} |\mathbf{c})$ 进行采样才能高效地生成最终的目标向量序列。
给定某解码方法,在推理时,我们首先从分布 $p_{\theta_{\text{dec}}}(\mathbf{y}i | \mathbf{Y}{0: i-1}, \mathbf{c})$ 中采样出下一个输出向量;接着,将其添加至解码器输入序列末尾,让解码器 RNN 继续从
$p_{\theta_{\text{dec}}}(\mathbf{y}{i+1} | \mathbf{Y}{0: i}, \mathbf{c})$ 中采样出下一个输出向量
基于 RNN 的编码器-解码器模型的一个重要特征是需要定义一些特殊向量,如
上图中,我们将编码器 RNN 编码器展开,并用绿色表示;同时,将解码器 RNN 展开,并用红色表示。
英文句子 I want to buy a car
,表示为
为了生成第一个目标向量,将
最终采样出第一个目标词
依此类推,一直到第 6 步,此时从 $\mathbf{l}6$ 中采样出 $\text{EOS}$,解码完成。输出目标序列为 $\mathbf{Y}{1:6} = {\mathbf{y}_1, \ldots, \mathbf{y}_6}$, 即上文中的 “Ich will ein Auto kaufen”。
综上所述,我们通过将分布 $p(\mathbf{Y}{1:m} | \mathbf{X}{1:n})$ 分解为
$$ p_{\theta_{\text{enc}}, \theta_{\text{dec}}}(\mathbf{Y}{1:m} | \mathbf{X}{1:n}) = \prod_{i=1}^{m} p_{\theta_{\text{enc}}, \theta_{\text{dec}}}(\mathbf{y}i | \mathbf{Y}{0: i-1}, \mathbf{X}{1:n}) = \prod{i=1}^{m} p_{\theta_{\text{dec}}}(\mathbf{y}i | \mathbf{Y}{0: i-1}, \mathbf{c}), \text{ 其中 } \mathbf{c}=f_{\theta_{enc}}(X) $$
在推理过程中,利用高效的解码方法可以自回归地生成目标序列
基于 RNN 的编码器-解码器模型席卷了 NLG 社区。2016 年,谷歌宣布用基于 RNN 的编码器-解码器单一模型完全取代其原先使用的的含有大量特征工程的翻译服务(参见 此处)。
然而,基于 RNN 的编码器-解码器模型存在两个主要缺陷。首先,RNN 存在梯度消失问题,因此很难捕获长程依赖性,参见 Hochreiter 等(2001)的工作。其次,RNN 固有的循环架构使得在编码时无法进行有效的并行化,参见 Vaswani 等(2017)的工作。
2017 年,Vaswani 等人引入了 transformer 架构,从而催生了基于 transformer 的编码器-解码器模型。
与基于 RNN 的编码器-解码器模型类似,基于 transformer 的编码器-解码器模型由一个编码器和一个解码器组成,且其编码器和解码器均由残差注意力模块(residual attention blocks) 堆叠而成。基于 transformer 的编码器-解码器模型的关键创新在于:残差注意力模块无需使用循环结构即可处理长度
回忆一下,要解决序列到序列问题,我们需要找到输入序列 $\mathbf{X}{1:n}$ 到变长输出序列 $\mathbf{Y}{1:m}$ 的映射。我们看看如何使用基于 transformer 的编码器-解码器模型来找到这样的映射。
与基于 RNN 的编码器-解码器模型类似,基于 transformer 的编码器-解码器模型定义了在给定输入序列 $\mathbf{X}{1:n}$ 条件下目标序列 $\mathbf{Y}{1:m}$ 的条件分布:
$$ p_{\theta_{\text{enc}}, \theta_{\text{dec}}}(\mathbf{Y}{1:m} | \mathbf{X}{1:n}) $$
基于 transformer 的编码器部分将输入序列 $\mathbf{X}{1:n}$ 编码为隐含状态序列 $\mathbf{\overline{X}} {1:n}$,即:
$$ f_{\theta_{\text{enc}}}: \mathbf{X}{1:n} \to \mathbf{\overline{X}}{1:n} $$
然后,基于 transformer 的解码器负责建模在给定隐含状态序列 $\mathbf{\overline{ X}}{1:n}$ 的条件下目标向量序列 $\mathbf{Y}{1:m}$ 的概率分布:
$$ p_{\theta_{dec}}(\mathbf{Y}{1:m} | \mathbf{\overline{X}}{1:n})$$
根据贝叶斯法则,该序列分布可被分解为每个目标向量 $\mathbf{y}i$ 在给定隐含状态 $\mathbf{\overline{X} }{1:n}$ 和其所有前驱目标向量
$$ p_{\theta_{dec}}(\mathbf{Y}{1:m} | \mathbf{\overline{X}}{1:n}) = \prod_{i=1}^{m} p_{\theta_{\text{dec}}}(\mathbf{y}i | \mathbf{Y}{0: i-1}, \mathbf{\overline{X}}_{1:n}) $$
因此,在生成 $\mathbf{y}i$ 时,基于 transformer 的解码器将隐含状态序列 $\mathbf{\overline{X}}{1:n}$ 及其所有前驱目标向量 $\mathbf{Y}{0 :i-1}$ 映射到 logit 向量 $\mathbf{l}i$。 然后经由 softmax 运算对 logit 向量 $\mathbf{l}i$ 进行处理,从而生成条件分布 $p{\theta{\text{dec}}}(\mathbf{y} i | \mathbf{Y}{0: i-1}, \mathbf{\overline{X}}{1:n})$。这个流程跟基于 RNN 的解码器是一样的。然而,与基于 RNN 的解码器不同的是,在这里,目标向量 $\mathbf{y}i$ 的分布是显式(或直接)地以其所有前驱目标向量 $\mathbf{y} 0, \ldots, \mathbf{y}{i-1}$ 为条件的,稍后我们将详细介绍。此处第 0 个目标向量 $\mathbf{y}0$ 仍表示为 $\text{BOS}$ 向量。有了条件分布 $p{\theta{\text{dec}}}(\mathbf{y}i | \mathbf{Y}{0: i-1}, \mathbf{\overline{X} }{1:n})$,我们就可以自回归生成输出了。至此,我们定义了可用于推理的从输入序列 $\mathbf{X}{1:n}$ 到输出序列
我们可视化一下使用基于 transformer 的编码器-解码器模型自回归地生成序列的完整过程。
上图中,绿色为基于 transformer 的编码器,红色为基于 transformer 的解码器。与上一节一样,我们展示了如何将表示为
首先,编码器将完整的输入序列 $\mathbf{X}{1:7}$ = "I want to buy a car"(由浅绿色向量表示)处理为上下文相关的编码序列 $\mathbf{\overline{X}}{1:7}$。这里上下文相关的意思是,举个例子,$\mathbf{\overline{x}}_4$ 的编码不仅取决于输入
接下来,输入编码
$$ p_{\theta_{enc, dec}}(\mathbf{y} | \mathbf{y}0, \mathbf{X}{1:7}) = p_{\theta_{enc, dec}}(\mathbf{y} | \text{BOS}, \text{I want to buy a car EOS}) = p_{\theta_{dec}}(\mathbf{y} | \text{BOS}, \mathbf{\overline{X}}_{1:7}) $$
然后,从该分布中采样出第一个目标向量
再采样一次,生成目标向量
这里有一点比较重要,我们仅在第一次前向传播时用编码器将 $\mathbf{X}{1:n}$ 映射到 $\mathbf{\overline{X}}{ 1:n}$。从第二次前向传播开始,解码器可以直接使用之前算得的编码
可以看出,仅在步骤
在 🤗 transformers 库中,这一自回归生成过程是在调用 .generate()
方法时在后台完成的。我们用一个翻译模型来实际体验一下。
from transformers import MarianMTModel, MarianTokenizer
tokenizer = MarianTokenizer.from_pretrained("Helsinki-NLP/opus-mt-en-de")
model = MarianMTModel.from_pretrained("Helsinki-NLP/opus-mt-en-de")
# create ids of encoded input vectors
input_ids = tokenizer("I want to buy a car", return_tensors="pt").input_ids
# translate example
output_ids = model.generate(input_ids)[0]
# decode and print
print(tokenizer.decode(output_ids))
输出:
<pad> Ich will ein Auto kaufen
.generate()
接口做了很多事情。首先,它将 input_ids
传递给编码器。然后,它将一个预定义的标记连同已编码的 input_ids
一起传递给解码器(在使用 MarianMTModel
的情况下,该预定义标记为
我们在附录中加入了一个代码片段,展示了如何“从头开始”实现一个简单的生成方法。如果你想要完全了解自回归生成的幕后工作原理,强烈建议阅读附录。
总结一下:
- 基于 transformer 的编码器实现了从输入序列 $\mathbf{X}{1:n}$ 到上下文相关的编码序列 $\mathbf{\overline{X}}{1 :n}$ 之间的映射。
- 基于 transformer 的解码器定义了条件分布 $p_{\theta_{\text{dec}}}(\mathbf{y}i | \mathbf{Y}{0: i-1}, \mathbf{ \overline{X}}_{1:n})$。
- 给定适当的解码机制,可以自回归地从 $p_{\theta_{\text{dec}}}(\mathbf{y}i | \mathbf{Y}{0: i-1}, \mathbf{\overline{X}}{1:n}), \forall i \in {1, \ldots, m}$ 中采样出输出序列 $\mathbf{Y}{1:m}$。
太好了,现在我们已经大致了解了基于 transformer 的编码器-解码器模型的工作原理。下面的部分,我们将更深入地研究模型的编码器和解码器部分。更具体地说,我们将确切地看到编码器如何利用自注意力层来产生一系列上下文相关的向量编码,以及自注意力层如何实现高效并行化。然后,我们将详细解释自注意力层在解码器模型中的工作原理,以及解码器如何通过交叉注意力层以编码器输出为条件来定义分布 $p_{\theta_ {\text{dec}}}(\mathbf{y}i | \mathbf{Y}{0: i-1}, \mathbf{\overline{X}}_{1:n})$。在此过程中,基于 transformer 的编码器-解码器模型如何解决基于 RNN 的编码器-解码器模型的长程依赖问题的答案将变得显而易见。
"Helsinki-NLP/opus-mt-en-de"
的解码参数。可以看到,其使用了 num_beams=6
的波束搜索。
如前一节所述,基于 transformer 的编码器将输入序列映射到上下文相关的编码序列:
$$ f_{\theta_{\text{enc}}}: \mathbf{X}{1:n} \to \mathbf{\overline{X}}{1:n} $$
仔细观察架构,基于 transformer 的编码器由许多残差注意力模块堆叠而成。每个编码器模块都包含一个双向自注意力层,其后跟着两个前馈层。这里,为简单起见,我们忽略归一化层(normalization layer)。此外,我们不会深入讨论两个前馈层的作用,仅将其视为每个编码器模块
我们对编码器如何将输入序列 "I want to buy a car EOS" 变换为上下文编码序列
这一过程进行一下可视化。与基于 RNN 的编码器类似,基于 transformer 的编码器也在输入序列最后添加了一个 EOS,以提示模型输入向量序列已结束
上图中的基于 transformer 的编码器由三个编码器模块组成。我们在右侧的红框中详细列出了第二个编码器模块的前三个输入向量:$\mathbf{x}_1$,$\mathbf {x}_2$ 及
可以看出,自注意力层的每个输出向量
我们更深入了解一下双向自注意力的工作原理。编码器模块的输入序列 key
向量 value
向量 query
向量
请注意,对每个输入向量 query
、key
和 value
向量后,将每个 query
向量 key
向量 key
向量与 query
向量 value
向量 value
向量的加权和 value
向量的权重与 key
向量 $\mathbf{k}_1, \ldots, \mathbf{k}n$ 之间的余弦相似度成正比,其数学公式为 $\textbf{Softmax}(\mathbf{K}{1:n}^\intercal \mathbf{q}_j)$,如下文的公式所示。关于自注意力层的完整描述,建议读者阅读这篇博文或原始论文。
好吧,又复杂起来了。我们以上例中的一个 query
向量为例图解一下双向自注意层。为简单起见,本例中假设我们的基于 transformer 的解码器只有一个注意力头 config.num_heads = 1
并且没有归一化层。
图左显示了上个例子中的第二个编码器模块,右边详细可视化了第二个输入向量 query
向量 query
向量),value
向量 key
向量 query
向量 $\mathbf{q}2$ 与所有 key
向量的转置(即 $\mathbf{K}{1:7}^{\intercal}$)相乘,随后进行 softmax 操作以产生自注意力权重。 自注意力权重最终与各自的 value
向量相乘,并加上输入向量
为了进一步理解双向自注意力层的含义,我们假设以下句子:“房子很漂亮且位于市中心,因此那儿公共交通很方便”。 “那儿”这个词指的是“房子”,这两个词相隔12个字。在基于 transformer 的编码器中,双向自注意力层运算一次,即可将“房子”的输入向量与“那儿”的输入向量相关联。相比之下,在基于 RNN 的编码器中,相距 12 个字的词将需要至少 12 个时间步的运算,这意味着在基于 RNN 的编码器中所需数学运算与距离呈线性关系。这使得基于 RNN 的编码器更难对长程上下文表征进行建模。此外,很明显,基于 transformer 的编码器比基于 RNN 的编码器-解码器模型更不容易丢失重要信息,因为编码的序列长度相对输入序列长度保持不变,即 $\textbf{len }(\mathbf{X}{1:n}) = \textbf{len}(\mathbf{\overline{X}}{1:n}) = n$,而 RNN 则会将
除了更容易学到长程依赖外,我们还可以看到 transformer 架构能够并行处理文本。从数学上讲,这是通过将自注意力机制表示为 query
、key
和 value
的矩阵乘来完成的:
$$\mathbf{X''}{1:n} = \mathbf{V}{1:n} \text{Softmax}(\mathbf{Q}{1:n}^\intercal \mathbf{K}{1:n}) + \mathbf{X'}_{1:n} $$
输出
太好了,现在我们应该对 a) 基于 transformer 的编码器模型如何有效地建模长程上下文表征,以及 b) 它们如何有效地处理长序列向量输入这两个方面有了比较好的理解了。
现在,我们写一个 MarianMT
编码器-解码器模型的编码器部分的小例子,以验证这些理论在实践中行不行得通。
from transformers import MarianMTModel, MarianTokenizer
import torch
tokenizer = MarianTokenizer.from_pretrained("Helsinki-NLP/opus-mt-en-de")
model = MarianMTModel.from_pretrained("Helsinki-NLP/opus-mt-en-de")
embeddings = model.get_input_embeddings()
# create ids of encoded input vectors
input_ids = tokenizer("I want to buy a car", return_tensors="pt").input_ids
# pass input_ids to encoder
encoder_hidden_states = model.base_model.encoder(input_ids, return_dict=True).last_hidden_state
# change the input slightly and pass to encoder
input_ids_perturbed = tokenizer("I want to buy a house", return_tensors="pt").input_ids
encoder_hidden_states_perturbed = model.base_model.encoder(input_ids_perturbed, return_dict=True).last_hidden_state
# compare shape and encoding of first vector
print(f"Length of input embeddings {embeddings(input_ids).shape[1]}. Length of encoder_hidden_states {encoder_hidden_states.shape[1]}")
# compare values of word embedding of "I" for input_ids and perturbed input_ids
print("Is encoding for `I` equal to its perturbed version?: ", torch.allclose(encoder_hidden_states[0, 0], encoder_hidden_states_perturbed[0, 0], atol=1e-3))
输出:
Length of input embeddings 7. Length of encoder_hidden_states 7
Is encoding for `I` equal to its perturbed version?: False
我们比较一下输入词嵌入的序列长度(即 embeddings(input_ids)
,对应于 $\mathbf{X}{1:n}$)和 encoder_hidden_states
的长度(对应于$\mathbf{\overline{X}}{1:n}$)。同时,我们让编码器对单词序列 "I want to buy a car" 及其轻微改动版 "I want to buy a house" 分别执行前向操作,以检查第一个词 "I" 的输出编码在更改输入序列的最后一个单词后是否会有所不同。
不出意外,输入词嵌入和编码器输出编码的长度,即 $\textbf{len}(\mathbf{X}{1:n})$ 和 $\textbf{len }(\mathbf{\overline{X}}{1:n})$,是相等的。同时,可以注意到当最后一个单词从 "car" 改成 "house" 后,$\mathbf{\overline{x}}_1 = \text{"I"}$ 的编码输出向量的值也改变了。因为我们现在已经理解了双向自注意力机制,这就不足为奇了。
顺带一提,自编码模型(如 BERT)的架构与基于 transformer 的编码器模型是完全一样的。 自编码模型利用这种架构对开放域文本数据进行大规模自监督预训练,以便它们可以将任何单词序列映射到深度双向表征。在 Devlin 等(2018) 的工作中,作者展示了一个预训练 BERT 模型,其顶部有一个任务相关的分类层,可以在 11 个 NLP 任务上获得 SOTA 结果。你可以从此处 找到 🤗 transformers 支持的所有自编码模型。
如编码器-解码器部分所述,基于 transformer 的解码器定义了给定上下文编码序列条件下目标序列的条件概率分布:
$$ p_{\theta_{dec}}(\mathbf{Y}{1: m} | \mathbf{\overline{X}}{1:n}) $$
根据贝叶斯法则,在给定上下文编码序列和每个目标变量的所有前驱目标向量的条件下,可将上述分布分解为每个目标向量的条件分布的乘积:
$$ p_{\theta_{dec}}(\mathbf{Y}{1:m} | \mathbf{\overline{X}}{1:n}) = \prod_{i=1}^{m} p_{\theta_{dec}}(\mathbf{y}i | \mathbf{Y}{0: i-1}, \mathbf{\overline{X}}_{1:n}) $$
我们首先了解一下基于 transformer 的解码器如何定义概率分布。基于 transformer 的解码器由很多解码器模块堆叠而成,最后再加一个线性层(即 “LM 头”)。这些解码器模块的堆叠将上下文相关的编码序列 $\mathbf{\overline{X}}{1:n}$ 和每个目标向量的前驱输入 $\mathbf{Y}{0:i-1}$(这里 $\mathbf{y}0$ 为 BOS)映射为目标向量的编码序列 $\mathbf{\overline{Y} }{0:i-1}$。然后,“LM 头”将目标向量的编码序列 $\mathbf{\overline{Y}}{0:i-1}$ 映射到 logit 向量序列 $\mathbf {L}{1:n} = \mathbf{l}_1, \ldots, \mathbf{l}_n$, 而每个 logit 向量$\mathbf{l}_i$ 的维度即为词表的词汇量。这样,对于每个
$$p_{\theta_{dec}}(\mathbf{y}i | \mathbf{Y}{0: i-1}, \mathbf{\overline{X}}_{1:n}), \forall i \in {1, \ldots, n}$$
“LM 头” 即为词嵌入矩阵的转置,即
$$ p_{\theta_{dec}}(\mathbf{y} | \mathbf{\overline{X}}{1:n}, \mathbf{Y}{0:i-1})$$ $$ = \text{Softmax}(f_{\theta_{\text{dec}}}(\mathbf{\overline{X}}{1:n}, \mathbf{Y}{0:i-1}))$$ $$ = \text{Softmax}(\mathbf{W}{\text{emb}}^{\intercal} \mathbf{\overline{y}}{i-1})$$ $$ = \text{Softmax}(\mathbf{l}_i) $$
总结一下,为了对目标向量序列 $\mathbf{Y}{1: m}$ 的条件分布建模,先在目标向量 $\mathbf{Y}{1: m-1}$ 前面加上特殊的
$$ p_{\theta_{dec}}(\mathbf{Y}{1:m} | \mathbf{\overline{X}}{1:n}) = \prod_{i=1}^{m} p_{\theta_{dec}}(\mathbf{y}i | \mathbf{Y}{0: i-1}, \mathbf{\overline{X}}_{1:n}).$$
与基于 transformer 的编码器不同,在基于 transformer 的解码器中,其输出向量 $\mathbf{\overline{y}}{i-1}$ 应该能很好地表征下一个目标向量(即 $\mathbf{y}i$),而不是输入向量本身(即 $\mathbf{y}{i-1}$)。此外,输出向量 $\mathbf{\overline{y}}{i-1}$ 应基于编码器的整个输出序列
好,我们仍以英语到德语翻译为例可视化一下基于 transformer 的解码器。
我们可以看到解码器将 $\mathbf{Y}{0:5}$: "BOS"、"Ich"、"will"、"ein"、"Auto"、"kaufen"(图中以浅红色显示)和 "I"、"want"、"to"、"buy"、"a"、"car"、"EOS"(即 $\mathbf{\overline{X}}{1:7}$(图中以深绿色显示))映射到 logit 向量
因此,对每个
$$ p_{\theta_{dec}}(\mathbf{y} | \text{BOS}, \mathbf{\overline{X}}{1:7}), $$ $$ p{\theta_{dec}}(\mathbf{y} | \text{BOS Ich}, \mathbf{\overline{X}}{1:7}), $$ $$ \ldots, $$ $$ p{\theta_{dec}}(\mathbf{y} | \text{BOS Ich will ein Auto kaufen}, \mathbf{\overline{X}}_{1:7}) $$
总条件概率如下:
其可表示为以下乘积形式:
$$ p_{\theta_{dec}}(\text{Ich} | \text{BOS}, \mathbf{\overline{X}}{1:7}) \times \ldots \times p{\theta_{dec}}(\text{EOS} | \text{BOS Ich will ein Auto kaufen}, \mathbf{\overline{X}}_{1:7}) $$
图右侧的红框显示了前三个目标向量
与双向自注意一样,在单向自注意中,query
向量 $\mathbf{q}0, \ldots, \mathbf{q}{m-1}$(如下图紫色所示),key
向量 $\mathbf{k}0, \ldots, \mathbf{k}{m-1}$(如下图橙色所示),和 value
向量 $\mathbf{v }0, \ldots, \mathbf{v}{m-1}$(如下图蓝色所示)均由输入向量 $\mathbf{y'}0, \ldots, \mathbf{ y'}{m-1}$(如下图浅红色所示)映射而来。然而,在单向自注意力中,每个 query
向量 key
向量进行比较(即 value
向量并加权求和。
我们将单向自注意力总结如下:
$$\mathbf{y''}i = \mathbf{V}{0: i} \textbf{Softmax}(\mathbf{K}_{0: i}^\intercal \mathbf{q}_i) + \mathbf{y'}_i$$
请注意,key
和 value
向量的索引范围都是 key
向量的索引范围。
下图显示了上例中输入向量
可以看出
那么,为什么解码器使用单向自注意力而不是双向自注意力这件事很重要呢?如前所述,基于 transformer 的解码器定义了从输入向量序列 $\mathbf{Y}{0: m-1}$ 到其下一个解码器输入的 logit 向量的映射,即 $\mathbf{L}{1:m}$。举个例子,输入向量
这显然是不对的,因为这样的话,基于 transformer 的解码器永远不会学到在给定所有前驱词的情况下预测下一个词,而只是对所有
太棒了!现在我们可以转到连接编码器和解码器的层 - 交叉注意力机制!
交叉注意层将两个向量序列作为输入:单向自注意层的输出 $\mathbf{Y''}{0: m-1}$ 和编码器的输出 $\mathbf{\overline{X}}{1:n}$。与自注意力层一样,query
向量 $\mathbf{q}0, \ldots, \mathbf{q}{m-1}$ 是上一层输出向量 $\mathbf{Y''}{0: m-1}$ 的投影。而 key
和 value
向量 $\mathbf{k}0, \ldots, \mathbf{k}{n-1}$、$\mathbf{v}0, \ldots, \mathbf {v}{n-1}$ 是编码器输出向量 $\mathbf{\overline{X}}{1:n}$ 的投影。定义完 key
、value
和 query
向量后,将 query
向量 key
向量进行比较,并用各自的得分对相应的 value
向量进行加权求和。这个过程与双向自注意力对所有
$$ \mathbf{y'''}i = \mathbf{V}{1:n} \textbf{Softmax}(\mathbf{K}_{1: n}^\intercal \mathbf{q}_i) + \mathbf{y''}_i $$
注意,key
和 value
向量的索引范围是
我们用上例中输入向量
我们可以看到 query
向量 query
向量 key
向量 $\mathbf{k}_1, \ldots, \mathbf{k}7$(黄色)进行比较,这里的 key
向量对应于编码器对其输入 $\mathbf{X}{1:n}$ = "I want to buy a car EOS" 的上下文相关向量表征。这将 "Ich" 的向量表征与所有编码器输入向量直接关联起来。最后,将注意力权重乘以 value
向量
所以,直观而言,到底发生了什么?每个输出向量 $\mathbf{y'''}i$ 是由所有从编码器来的 value
向量($\mathbf{v}{1}, \ldots, \mathbf{v }_7$ )的加权和与输入向量本身 query
投影与 来自编码器的 $\mathbf{k}_j$ 越相关,其对应的
酷!现在我们可以看到这种架构的每个输出向量 $\mathbf{y'''}i$ 取决于其来自编码器的输入向量 $\mathbf{\overline{X}}{1 :n}$ 及其自身的输入向量 key
向量 value
向量 $\mathbf{v}1, \ldots, \mathbf{v}n $ 的投影矩阵 $\mathbf{W}^{\text{cross}}{k}$ 和 $\mathbf{W}^{\text{cross}}{v}$ 都是与 value
向量 为什么基于 transformer 的解码器没有远程依赖问题而基于 RNN 的解码器有
这一问题的答案已经很显然了。因为每个解码器 logit 向量直接依赖于每个编码后的输出向量,因此比较第一个编码输出向量和最后一个解码器 logit 向量只需一次操作,而不像 RNN 需要很多次。
总而言之,单向自注意力层负责基于当前及之前的所有解码器输入向量建模每个输出向量,而交叉注意力层则负责进一步基于编码器的所有输入向量建模每个输出向量。
为了验证我们对该理论的理解,我们继续上面编码器部分的代码,完成解码器部分。
from transformers import MarianMTModel, MarianTokenizer
import torch
tokenizer = MarianTokenizer.from_pretrained("Helsinki-NLP/opus-mt-en-de")
model = MarianMTModel.from_pretrained("Helsinki-NLP/opus-mt-en-de")
embeddings = model.get_input_embeddings()
# create token ids for encoder input
input_ids = tokenizer("I want to buy a car", return_tensors="pt").input_ids
# pass input token ids to encoder
encoder_output_vectors = model.base_model.encoder(input_ids, return_dict=True).last_hidden_state
# create token ids for decoder input
decoder_input_ids = tokenizer("<pad> Ich will ein", return_tensors="pt", add_special_tokens=False).input_ids
# pass decoder input ids and encoded input vectors to decoder
decoder_output_vectors = model.base_model.decoder(decoder_input_ids, encoder_hidden_states=encoder_output_vectors).last_hidden_state
# derive embeddings by multiplying decoder outputs with embedding weights
lm_logits = torch.nn.functional.linear(decoder_output_vectors, embeddings.weight, bias=model.final_logits_bias)
# change the decoder input slightly
decoder_input_ids_perturbed = tokenizer("<pad> Ich will das", return_tensors="pt", add_special_tokens=False).input_ids
decoder_output_vectors_perturbed = model.base_model.decoder(decoder_input_ids_perturbed, encoder_hidden_states=encoder_output_vectors).last_hidden_state
lm_logits_perturbed = torch.nn.functional.linear(decoder_output_vectors_perturbed, embeddings.weight, bias=model.final_logits_bias)
# compare shape and encoding of first vector
print(f"Shape of decoder input vectors {embeddings(decoder_input_ids).shape}. Shape of decoder logits {lm_logits.shape}")
# compare values of word embedding of "I" for input_ids and perturbed input_ids
print("Is encoding for `Ich` equal to its perturbed version?: ", torch.allclose(lm_logits[0, 0], lm_logits_perturbed[0, 0], atol=1e-3))
输出:
Shape of decoder input vectors torch.Size([1, 5, 512]). Shape of decoder logits torch.Size([1, 5, 58101])
Is encoding for `Ich` equal to its perturbed version?: True
我们首先比较解码器词嵌入层的输出维度 embeddings(decoder_input_ids)
(对应于 $\mathbf{Y}{0: 4}$,这里 <pad>
对应于 BOS 且 "Ich will das" 被分为 4 个词)和 lm_logits
(对应于 $\mathbf{L}{1:5}$)的维度。此外,我们还通过解码器将单词序列 “<pad>
Ich will ein” 和其轻微改编版 “<pad>
Ich will das” 与 encoder_output_vectors
一起传递给解码器,以检查对应于 "Ich" 的第二个 lm_logit 在仅改变输入序列中的最后一个单词("ein" -> "das")时是否会有所不同。
正如预期的那样,解码器输入词嵌入和 lm_logits 的输出,即 $\mathbf{Y}{0: 4}$ 和 $\mathbf{L}{ 1:5}$ 的最后一个维度不同。虽然序列长度相同(=5),但解码器输入词嵌入的维度对应于 model.config.hidden_size
,而 lm_logit
的维数对应于词汇表大小 model.config.vocab_size
。其次,可以注意到,当将最后一个单词从 "ein" 变为 "das",$\mathbf{l}_1 = \text{"Ich"}$ 的输出向量的值不变。鉴于我们已经理解了单向自注意力,这就不足为奇了。
最后一点,自回归模型,如 GPT2,与删除了交叉注意力层的基于 transformer 的解码器模型架构是相同的,因为纯自回归模型不依赖任何编码器的输出。因此,自回归模型本质上与自编码模型相同,只是用单向注意力代替了双向注意力。这些模型还可以在大量开放域文本数据上进行预训练,以在自然语言生成 (NLG) 任务中表现出令人印象深刻的性能。在 Radford 等(2019)的工作中,作者表明预训练的 GPT2 模型无需太多微调即可在多种 NLG 任务上取得达到 SOTA 或接近 SOTA 的结果。你可以在此处获取所有 🤗 transformers 支持的自回归模型的信息。
好了!至此,你应该已经很好地理解了基于 transforemr 的编码器-解码器模型以及如何在 🤗 transformers 库中使用它们。
非常感谢 Victor Sanh、Sasha Rush、Sam Shleifer、Oliver Åstrand、Ted Moskovitz 和 Kristian Kyvik 提供的宝贵反馈。
如上所述,以下代码片段展示了如何为基于 transformer 的编码器-解码器模型编写一个简单的生成方法。在这里,我们使用 torch.argmax
实现了一个简单的贪心解码法来对目标向量进行采样。
from transformers import MarianMTModel, MarianTokenizer
import torch
tokenizer = MarianTokenizer.from_pretrained("Helsinki-NLP/opus-mt-en-de")
model = MarianMTModel.from_pretrained("Helsinki-NLP/opus-mt-en-de")
# create ids of encoded input vectors
input_ids = tokenizer("I want to buy a car", return_tensors="pt").input_ids
# create BOS token
decoder_input_ids = tokenizer("<pad>", add_special_tokens=False, return_tensors="pt").input_ids
assert decoder_input_ids[0, 0].item() == model.config.decoder_start_token_id, "`decoder_input_ids` should correspond to `model.config.decoder_start_token_id`"
# STEP 1
# pass input_ids to encoder and to decoder and pass BOS token to decoder to retrieve first logit
outputs = model(input_ids, decoder_input_ids=decoder_input_ids, return_dict=True)
# get encoded sequence
encoded_sequence = (outputs.encoder_last_hidden_state,)
# get logits
lm_logits = outputs.logits
# sample last token with highest prob
next_decoder_input_ids = torch.argmax(lm_logits[:, -1:], axis=-1)
# concat
decoder_input_ids = torch.cat([decoder_input_ids, next_decoder_input_ids], axis=-1)
# STEP 2
# reuse encoded_inputs and pass BOS + "Ich" to decoder to second logit
lm_logits = model(None, encoder_outputs=encoded_sequence, decoder_input_ids=decoder_input_ids, return_dict=True).logits
# sample last token with highest prob again
next_decoder_input_ids = torch.argmax(lm_logits[:, -1:], axis=-1)
# concat again
decoder_input_ids = torch.cat([decoder_input_ids, next_decoder_input_ids], axis=-1)
# STEP 3
lm_logits = model(None, encoder_outputs=encoded_sequence, decoder_input_ids=decoder_input_ids, return_dict=True).logits
next_decoder_input_ids = torch.argmax(lm_logits[:, -1:], axis=-1)
decoder_input_ids = torch.cat([decoder_input_ids, next_decoder_input_ids], axis=-1)
# let's see what we have generated so far!
print(f"Generated so far: {tokenizer.decode(decoder_input_ids[0], skip_special_tokens=True)}")
# This can be written in a loop as well.
输出:
Generated so far: Ich will ein
在这个示例代码中,我们准确地展示了正文中描述的内容。我们在输入 "I want to buy a car" 前面加上
在实践中,我们会使用更复杂的解码方法来采样 lm_logits
。你可以参考这篇博文了解更多的解码方法。
英文原文: https://huggingface.co/blog/encoder-decoder 原文作者:Patrick von Platen 译者: Matrix Yao (姚伟峰),英特尔深度学习工程师,工作方向为 transformer-family 模型在各模态数据上的应用及大规模模型的训练推理。