原论文地址:https://arxiv.org/abs/1706.03762
一、概述
Transformer 模型的提出主要应用于机器翻译。简单来说将一种语言输入到机器翻译模型,然后输出它另一种语言的翻译。

如果进一步拆开看,我们可以看到 Transformer 有编码组件和解码组件,以及它们之间的连接

编码组件为编码器堆栈(论文中编码器数量为 6),解码组件中解码器数量和编码器数量一致。

编码器在结构上都是相同的(但它们不共享权重)。每一个都分为两个子层:Self-Attention 和 Feed Forward Neural Network 层。

编码器的输入首先经过自注意力层(帮助编码器在对特定单词进行编码时查看输入句子中的其他单词。在后面的叙述中详细叙述自注意力层)。自注意力层的输出被馈送到前馈神经网络。结构相同的前馈网络独立应用于每个位置。解码器同时也具有这两个层,但在它们之间是一个注意力层,帮助解码器专注于输入句子的相关部分(类似于 seq2seq 模型中的注意力)。

二、从一个 Tensor 开始
现在已经了解了模型的主要组件,现在开始研究各种向量/张量,以及它们如何在这些组件之间传递,以将训练模型的输入转化为输出。与 NLP 应用程序中的一般情况一样,首先使用 embedding 算法将每个输入单词转换为向量。(每个单词都 embedding 到一个大小为 512 的向量中)

embedding 仅发生在最底部的编码器中。所有编码器共有的抽象是它们的输入为一个大小为 512 的向量列表(在最底部的编码器中为 word embedding),在其他编码器中,它将是直接在下方的编码器的输出作为输入。这个列表的大小是可以设置的超参数(基本上它是我们训练数据集中最长句子的长度)。

可以看到 Transformer 的一个关键属性,那就是每个位置的单词在编码器中经过单独的路径。在自注意力层中,这些路径之间存在依赖关Å系。然而,前馈层没有这些依赖关系,因此各种路径可以在流过前馈层的同时并行执行。
编码器接收向量列表作为输入。它通过将这些向量传递给自注意层来处理这个列表,然后传递给前馈神经网络,然后将输出向上发送到下一个编码器。

三、自注意力机制
假设以下句子是我们要翻译的输入句子:
The animal didn't cross the street because it was too tired
这句话中的 it
指的是什么?它指的是 street
还是 animal
?这对人类来说是一个简单的问题,但对算法来说却不是那么简单。
当模型处理it
这个词时,自注意力机制 (self-attention) 允许它把it
和animal
联系起来。
当模型处理每个单词(输入序列中的每个位置)时,自注意力允许它查看输入序列中的其他位置以寻找有助于更好地编码该单词的线索。
允许 RNN 将其已处理的先前单词/向量的表示与当前正在处理的单词/向量结合起来。Self-attention 简单来说就是 Transformer 将对其它单词的“理解”融入我们当前正在处理的单词的一种办法。

当在编码器 #5(堆栈中的顶部编码器)中对单词 it
进行编码时,部分注意力机制专注于 The Animal
,并将其表示的一部分表示到 it
的编码中。
接下来先看看如何使用向量计算自注意力,然后继续看看它是如何使用矩阵实际实现的。计算自注意力的第一步是从每个编码器的输入向量(通常为每个词的 word embedding )中创建三个向量。因此,对于每个单词,创建一个 Query
向量、一个 Key
向量和一个 Value
向量。这些向量是通过将 embedding 乘以在训练过程中训练的三个矩阵来创建的。
值得注意的是,Q/K/V 的维度小于 embedding 向量。它们的维度是 64,而 embedding 和编码器输入/输出向量的维度是 512。它们不必更小,这是一种架构选择,可以使多头注意力(大部分)的计算保持不变。

那么 Query
,Key
, Value
向量分别代表什么呢?
计算自注意力的第二步是计算一个分数。假设我们正在计算本例中第一个单词 Thinking
的自注意力。需要根据这个词对输入句子的每个词进行评分。在某个位置对单词进行编码时,分数决定了对输入句子其他部分的关注程度。分数是通过正在评分的相应单词的 Q 向量与 K 向量的点积来计算的。因此,如果正在处理位置 #1 中单词的自注意力,第一个分数将是 q1 和 k1 的点积。第二个分数是 q1 和 k2 的点积。

第三步和第四步是将分数除以 8(论文中使用的 K 向量维度的平方根。这会导致梯度更稳定。默认为 8 ,也可设置为其他值),然后通过 softmax 操作传递结果。Softmax 将分数归一化,因此它们都是正数并且加起来为 1。

softmax 分数决定了每个单词在这个位置上的表达量。显然,当前位置的单词将具有最高的 softmax 分数,但有时关注与当前单词相关的另一个单词很有用。
第五步是将每个 V 向量乘以 softmax 分数(准备将它们相加)。这里的直观理解是保持想要关注的单词的值不变,并剔除不相关的单词(例如,将它们乘以 0.001 )
第六步是对加权值向量求和。这会在这个位置产生自注意力层的输出(对于第一个词)。

上述为自注意力计算。结果向量发送到前馈神经网络的向量。然而,在实际实现中,这种计算是以矩阵形式进行的,以便更快地处理。
第一步是计算uery, Key, 和 Value 矩阵。通过将 embedding 打包到矩阵 X 中,并将其乘以训练的权重矩阵(WQ、WK、WV)来做到这一点。

矩阵中的每一行对应于输入句子中的一个单词。可以看到 embedding 向量(512,或图中 4 个框)和 q/k/v 向量(64,或图中 3 个框)大小的差异
最后,由于处理的是矩阵,可以将步骤 2 到 6 合并到一个公式中来计算自注意力层的输出。

其实 Q、K、V 的作用可以用一句话概括:Q、K 是用来搬运信息的,而 K 本身就是从 token 当中搬运出来的信息,咱们来看一个例子,算法上不一定严谨,但一定有助于理解。
假如我们有下面这一句输入语料:
帅哥 Andy。
分词之后不难发现,“帅哥”这个定语是用来修饰 “Andy” 这个主语的。假设 “Andy” 这个主语所对应的 token 是一个高维空间 \(R^d\) 当中的一个点,当我们用 “帅哥” 这个形容词来修饰这个主语之后,那这个主语在空间 \(R^d\) 当中的位置相较于不被定语修饰的 token 的位置应该是有一定修改的,否则修饰和没修饰就没区别。
这个修改是怎么产生的?就是通过 \(Q_{Andy}\) 与 \(Q_{帅哥}\) 做内积(帅哥修饰的 Andy 的词语表示会受到上下文的影响变得不一样,也叫contexual word embedding)之后,与 \(V_{帅哥}\) 相乘并且加上 \(V_{Andy}\) 得来的,相当于 Q 和 K 决定了修改的幅度(幅度可正可负),而 V 用来决定修改的方向。
所以,“Andy” 这个主语应该位于空间 \(R^d\) 的某个子空间内,在这个子空间内不同的点对应的是被不同定语修饰的 “Andy”。
那当定语的信息已经被搬运给主语了之后,定语所对应的 token 实际上就没用了,那这时候定语所对应的 token 就应该在与其它 token 产生运算的时候被抵消掉,而这同样也是依靠 Q 和 K 来完成的。这个就是所谓的“基于上下文的词嵌入”(Context Word Embedding)。
V 比较好理解,同一个词在不同语境下代表的意思是不一样的,比如说 “Apple” 有时候是一个公司,而有时候是一种水果。
所以即便是相同的 token,在不同的语境下,也应该提取出不同的 V,这样才合理。
四、多头注意力机制
论文通过添加一种称为“multi-headed attention”(多头注意力)的机制进一步完善了自注意力层。这通过两种方式提高了注意力层的性能:
- 它扩展了模型关注不同位置的能力。是的,在上面的示例中, 包含一点其他编码,但它可能由实际单词本身主导。如果我们翻译
The animal didn’t cross the street because it was too tired
这样的句子会很有用,我们会想知道it
指的是哪个词。 - 它为注意力层提供了多个“representation subspaces”(表示子空间)。使用多头注意力,不仅有一个,而且还有多组 Q/K/V 权重矩阵(Transformer 使用 8 个注意力头,所以最终每个编码器/解码器都有 8 个集合) 。集合中的每一个都是随机初始化的。然后,在训练之后,每个集合用于将输入 embedding(或来自较低编码器/解码器的向量)投影到不同的表示子空间中。

通过多头注意力,为每个头维护单独的 Q/K/V 权重矩阵,从而产生不同的 Q/K/V 矩阵。正如之前所做的那样,将 X 乘以 WQ/WK/WV 矩阵以产生 Q/K/V 矩阵。
如果进行与上述相同的自注意力计算,只是使用不同的权重矩阵进行八次不同的计算,最终会得到八个不同的 Z 矩阵。

如何将多头注意力得到的多个结果 Z 输入到前馈层?连接这些矩阵,然后将它们乘以一个额外的权重矩阵 WO。

现在看看当我们在例句中编码 it
这个词时,不同的注意力头集中在哪里。

当对it
这个词进行编码时,一个注意力头最关注animal
,而另一个注意力头最关注tire
。从某种意义上说,模型对it
这个词的表示包含了一些表示animal
和tire
。
但是,如果将所有注意力都表示时,其可能会更难解释:

五、位置编码
目前为止所描述的,相比与 RNN 模型,Transformer 中缺少一种解释输入序列中单词顺序的方法。为了解决这个问题,Transformer 为每个输入 embedding 添加了一个向量。这些向量遵循模型学习的特定模式,这有助于确定每个单词的位置,或序列中不同单词之间的距离。直观理解为,一旦将这些值投影到 Q/K/V 向量中以及在点积注意力期间,将这些值添加到嵌入中会在 embedding 向量之间提供有意义的距离。

为了让模型了解单词的顺序,作者添加了位置编码向量——其值遵循特定的模式。
假设 embedding 的维度为 4,那么实际的位置编码将如下所示:

更多关于位置编码的也可以看我另一篇博文《Transformer 的绝对位置编码和相对位置编码》。

六、残差层
编码器架构中的一个细节是,每个编码器中的每个子层(self-attention,feed forward)周围都有一个残差连接,然后是层归一化步骤。

如果要可视化与 self attention 相关的向量和 layer-norm 操作,它看起来像这样:

这也适用于解码器的子层。如果想一个由 2 个堆叠编码器和解码器组成的 Transformer,它看起来像这样:

七、解码器
现在让我们来看看它们是如何协同工作的。编码器首先处理输入序列。然后将顶部编码器的输出转换为一组注意向量 K 和 V。这些将由每个解码器在其“encoder-decoder attention”层中使用,这有助于解码器将注意力集中在输入序列中的适当位置:

完成编码阶段后,开始解码阶段。解码阶段的每一步都从输出序列(本例中的英文翻译句子)中输出一个元素。
以下步骤重复该过程,直到出现一个特殊符号,表明转换器解码器已完成其输出。每一步的输出在下一个时间步被馈送到底部的解码器,解码器就像编码器一样传递它们的解码结果。就像对编码器输入所做的那样,将位置编码嵌入并添加到这些解码器输入中,以指示每个单词的位置。

解码器中的自注意力层的操作方式与编码器中的方式略有不同:
- 在解码器中,自注意力层只允许关注输出序列中较早的位置。这是通过在 self-attention 计算中的 softmax 步骤之前屏蔽未来位置(将它们设置为
-inf
)来完成的。 - “Encoder-Decoder Attention”层的工作方式与多头自注意力相似,不同之处在于它从其下方的层创建其 Q 矩阵,并从编码器堆栈的输出中获取 K 和 V 矩阵。
八、最终的全连接层和 Softmax 层
解码器堆栈输出一个浮点向量。其输入一个简单的全连接神经网络,它将解码器堆栈产生的向量投影到一个更大的向量中,称为 logits 向量。假设模型知道从训练数据集中学习到的 10,000 个独特的英语单词(模型的“输出词汇”)。这将使 logits 向量有 10,000 个单元格宽(每个单元格对应一个唯一单词的分数) 然后,softmax 层将这些分数转化为概率(全部为正数,且加起来为 1.0)。选择概率最高的单元格,并生成与其关联的单词作为该时间步的输出。

九、模拟训练和损失函数
在训练期间,未经训练的模型将通过完全相同的前向传递。由于是在一个带标签的训练数据集上训练它,可以将它的输出与实际正确的输出进行比较。为了形象化,假设输出词汇表只包含六个单词(a
、am
、i
、thanks
、student
和 <eos>
(“end of sentence”的缩写)) 。

一旦定义了的输出词汇表,就可以使用一个相同宽度的向量来表示我们词汇表中的每个单词。这也称为 one-hot 编码。因此,例如,可以使用以下向量表示单词 am
:

在训练阶段的第一步,正在通过一个简单的例子来训练它——将 merci
翻译成 thanks
。这意味着,希望输出是一个概率分布,表示 thanks
这个词。但是由于这个模型还没有经过训练,所以这还不太可能发生。

由于模型的参数(权重)都是随机初始化的,因此(未经训练的)模型会为每个单元格/单词生成具有任意值的概率分布(通常为正态分布)。可以将其与实际输出进行比较,然后使用反向传播调整所有模型的权重,以使输出更接近所需的输出
如何比较两个概率分布?最直观的理解两者相减的大小。(进一步了解可以查阅交叉熵和 KL 散度) 值得注意的是,这是一个过于简单的示例。更现实的是,通常会使用一个比一个词长的句子。例如——输入:je suis étudiant
,预期输出:i am a student
。这真正意味着希望模型能够连续输出概率分布,其中:
关于 KL 散度可以看我另一篇博文《从 R-Drop 到 KL 散度》。

- 每个概率分布都由一个宽度为 vocab_size 的向量表示(在示例中为 6,但更实际的是一个数字,例如 30,000 或 50,000)
- 第一个概率分布在与单词
i
相关的单元格中具有最高概率 - 第二个概率分布在与单词
am
相关的单元格中具有最高概率 - 依此类推,直到第五个输出分布指示
<end of sentence>
符号,它也有一个来自 10,000 个元素词汇表的单元格与之关联。

在足够大的数据集上训练模型足够的时间后,期望生成的概率分布如下所示:

现在,因为模型一次产生一个输出,可以假设模型正在从该概率分布中选择具有最高概率的单词并丢弃其余的单词。这是一种方法(称为贪婪解码)。另一种方法是保留前两个单词(例如,I
和 a
),然后在下一步中运行模型两次:假设第一个输出位置是单词 I
,另一次假设第一个输出位置是单词 a
,并且考虑到位置 #1和#2,无论哪个版本产生的错误更少。对 #2 和 #3 等位置重复此操作。这种方法称为 beam search ,在示例中,beam_size 为 2(意味着在任何时候,两个部分假设(未完成的翻译)都保存在内存中),top_beams 也是 2(意味着将返回两个翻译)。
总结
过去模型一直难以解决的是记忆力的问题,如果需要记录非常大的信息,模型就会变得很复杂,这就会导致过去的模型必须丢弃掉更远的信息。比如一段话有几百个字,可能过去的模型最多记录前后几十个字。
Transformer 最大的优势是在于其独特的自注意力机制,模拟人类的阅读习惯(关于这点可以看我另一篇博文《大脑是如何阅读文字的》)在读一句话时,大脑会先记住重要词汇,在一些新的论文中有指出甚至只需要加粗一些词语的开头几个字母会使得阅读更加容易。

从本质上理解,Attention 是从大量信息中有筛选出少量重要信息,并聚焦到这些重要信息上,忽略大多不重要的信息。权重越大越聚焦于其对应的 Value 值上,即权重代表了信息的重要性,而 Value 是其对应的信息。
得益于现代强大的 GPU 能力以及模型结构的设计,Transformer 有大吞吐量,大规模并行处理能力以及几乎能够处理任何序列数据,非常适合迁移到其它的领域如图片、声音等等。
此外 Transformer 还非常适合做上下文预测,自这模型之后文本生成领域也获得了突飞猛进的提升。
它的缺点也很明显:
- 位置信息编码可以改进
在使用词向量的过程中,会做如下假设:对词向量做线性变换,其语义可以在很大程度上得以保留,也就是说词向量保存了词语的语言学信息(词性、语义)。然而,位置编码在语义空间中并不具有这种可变换性,它相当于人为设计的一种索引。那么,将这种位置编码与词向量相加,就是不合理的,所以不能很好地表征位置信息。这个在后面的论文有逐步提出改进方案,如相对位置编码等。

从 self-attention 的原理上看,它能够真正意义上实现双向的信息编码,这也使得它能够并行计算,计算效率远超 rnn。但也正是如此,导致 Transformer 无法感知位置信息,因为 self-attention 的计算与顺序无关。为了弥补这一缺陷,原始的 Transformer 加入了位置编码,也就是强行叠加到 embedding 上,说强行可能不太恰当,但这确实值得怀疑。对于 Transformer 这样的重量级模型,在最开始加上一项,经过复杂的迭代,很难说它不会被衰减。
另一方面,Transfomer 中的绝对位置编码,也会对句子长度产生限制,模型需要提前开辟一块空间,用来计算每一个位置的编码。在 Transformer-XL、NEZHA 当中,作者使用相对位置编码,只关心词之间的相对位置关系,因此它不将受长度和空间限制,在计算注意力时生成即可。另外使用相对编码的好处也体现在下游,它能够建模更长的句子。
- 可解释性很差
目前关于 Transformer 为什么在很多领域很有效或者在某些领域又不是很有效的问题,仍然待研究。
- 模型很大
在 Transformer 后开始了模型对参数的时代,不断使用更高算力获得更大的模型,从而提升更高的分数。包括后来 Bert 等等仍然是可以看出有很大的冗余的,只能不断提升参数量、不断提升 batch-size 来获得更好的成绩。
- 结构可以优化
结构调整旨在直接对 Transformer 的每个模块进行修改,这个相对较难,需要基于大量的实验和对模型深刻的理解。例如,Transformer 最后的输出其实只用到了最后一层的信息,如果是 12 层模型的话,那么就相当于浪费了之前 11 层的输出结果。参照 attention 的思路,我们可以将每一层的结果都利用起来,最简单的做法就是将每层的输出求和,再通过一个 dense 层达到聚合的效果。
此外,虽然 Transform 从编码角度来看是双向的,但是在特定任务下,我们其实也需要单向的信息,因此可以想到,rnn 同样有可能替代 Transfomer 中的一些模板。同样道理,还有 cnn,善于捕捉局部信息,并且也能够并行计算,不妨是一个好的选择。
说到局部信息,Transformer 在计算注意力时使用的都是全局信息,这使得它无法对很长的句子或段落进行计算,因为每个位置都得和其它位置计算注意力,导致其复杂度将随着句子长度以平方级增长,从而内存不够。针对这一点目前也有不少优化,例如 Longformer、Lite Transformer 等。
在 Longformer 中,作者提出了局部注意力,使注意力计算在一个滑动窗口中进行,减缓了 Transformer 对长句理解能力不足的问题。
另外,很长一段时间内,BERT 在 NLG 任务上一直被诟病,这虽然不全是 Transformer 的锅,跟 BERT 的语言模型也有关系。但双向的 encoder+decoder 机制本身即不太符合生成的逻辑,对于生成任务,词之间的先后顺序还是非常重要的,即使 Transformer 的解码也能够逐字进行,但这不足以弥补其编码过程,从这一角度来看,Transformer 未必胜过 rnn。因此目前有很多针对 NLG 任务的改进措施,例如 UniLM、BART 等,前者设计了 Seq2Seq 掩码,后者在 decoder 部分用回了自回归的方式,即有损 encoder+自回归decoder,使模型更贴合生成任务。自回归的好处是不会破坏位置之间的先后(当然也因此不能够像Transformer那样双向)它基于传统的似然函数,即多个条件概率连乘,这直观上看更符合日常规律。
建议来自 京东科技风险算法与技术 团队。
- 对比学习可能会失效
这是一个偶然间发现的事情,似乎在双向的结构下,对比学习可能会获得非常差的效果。之前实验是在 Bert + T5 这样的双向结构上发现的,但不确定是为什么。
参考文献



主要贡献: Redflashing、砚安
整理:Qin
