本文将剖析Transformer关于注意力机制的几个技术要点,包括encoder端的自注意力机制、decoder端的masked自注意力机制(训练阶段需要,推理阶段也需要)以及decoder端的交叉注意力机制等,而架构的其它部分将简单略过。搞清楚了这些技术要点,我们就能对Transformer的整体架构有一个清晰的认知,并为灵活地使用该模型打下基础。
关注公众号,更方便地接收最新内容。
encoder端的多头自注意力
多头注意力机制实际上只是自注意机制的“重复”,所以核心在于讲解自注意力机制。
自注意力机制
自注意力,通俗点说是利用每个词自己的信息去得到与其它词的关联,具体做法是计算两个词向量的点积,用点积的结果来反映一个词对另一个的关注度。
相比之下如LSTM中的注意力机制就可以被称为外部注意力,因为它使用的是可训练的参数来决定对于数据的关注程度,比如参数值为0,那么就不会利用到这个数据,即不关注,参数值为0.8,则0.8作为系数与某个词向量相乘,就相当于很大程度地利用了这个词向量的信息,当然,这里只是举个直观的例子。“自”和“外”这两个字很形象地表达出了这两种注意力机制的区别。
一个有n个词的句子,每个词的加上了位置信息的词向量作为矩阵的一行,这样就形成了一个句子的词向量矩阵。我们的讨论主要基于矩阵讲解,这样内容更简洁。
自注意力机制的核心公式:
下面展示计算过程:
将输入映射成了Q、K、V矩阵,这3个矩阵的每一行就对应着输入的每个词的q、k、v向量,比如“我在看书”中的4个词,“我”的q向量与“在”的k向量点积结果是(经过softmax)0.3,就说明“我”这个词对“在”这个词注意力是0.3,然后这个结果就与“在”的v向量相乘,以此类推,最终做完注意力计算后,“我”的新的表示可能就是“0.2×我+0.3×在+0.2×看+0.3×书”。
下面进行公式讲解:
因为点积的结果可能有正有负有0,而且是绝对数值,不好表达一个词对另一个词注意力的相对大小,所以结果经过softmax(分别对每一行)变成0~1之间的值,于是,一个词对于其它词的注意力就是一个和为1的概率分布,这样直观的注意力表示形式下,我们就会说“这个词对那个词注意力很高,有80%”。分母上的d_k也就是Q或K行向量的长度,目的是在做softmax之前把点积结果缩放得更小一些,以免数值太大落在softmax的平缓区域导致梯度消失。
于是,从一个句子的带有位置信息的词向量矩阵输入模型,到做完一个自注意力机制,完整的计算流程如下:
多头自注意力机制
Transformer中使用的是多头自注意力机制,过程非常简单,只是对输入X做了多组的上述的自注意力机制,每组的参数是独立的,目的是有更丰富的空间来表达句子的特征信息。把每组的结果连接起来,再做一个线性映射得到最终结果。
每一个layer的输出的形状和输入的形状一样的另一个作用是用于残差连接,如Transformer的结构图所示,每一个layer后都有一个residual connection和norm。
decoder端的masked自注意力机制
如1中所述多头自注意力只是自注意力机制的组合,所以我们只按单个的注意力机制来讲。Transformer推理时利用当前词预测下一个词,实际上就是个多分类任务,输出的是一个词表长度的向量,向量的每个位置对应一个词的概率值。这个词表中的词还包含一些特殊符号,比如代表句子开始和结束的bos和eos 。
一次输出一个词,然后把这个词加入到decoder的已存在的输入序列后面,用这个新加入的词来预测下一个词,这种操作叫做自回归,无论是对话任务还是翻译任务,给定不同的输入,输出是不定长的,所以采取这样的方式,就可以应对各种长度的答案,预测到结束标识符或者达到限定的最大长度,就结束输出。如果是设计一次性输出整个答案的模型,显然我们遇到的第一个问题就是如何设计输出的结构,以应对不同长度的输出。
例如,训练模型时,输入“I love China”希望模型输出“我爱中国”。作为有监督学习,我们的标签如何构造呢?
bos | 我 | 爱 | 中 | 国 |
---|---|---|---|---|
我 | 爱 | 中 | 国 | eos |
“bos 我爱中国”作为输入,“我爱中国 eos”作为标签,即“我”是“bos”的标签,...“eos”是“国的标签”。由句子开始标志“bos”要让模型预测出“我”,即预测结果和“我”计算损失值,...有“国”要预测出句子结束标志“eos”,即对“国”的预测结果和“eos”计算损失值。换言之,由当前词预测下一个词,下一个词就作为当前词的标签。
因为训练阶段decoder的输入是已知的,而我们的目的是训练模型用当前词预测下一个词的能力,所以我们在计算注意力分数后,就把当前词之后的词的注意力分数置为负无穷,这样经过softmax函数后这个位置提供的概率值就是0了,就实现了“掩盖”后面的词的目的了。
这个功能的实现非常简单,以“bos 我爱中国”作为decoder的输入,经过QK^T计算就会得到一个5x5的注意力分数矩阵,但是我们不想让“bos”注意到其之后的词,不想让“我”注意到其之后的词...
那么可以构造一个和注意力分数矩阵同样形状的下三角矩阵作为 mask:
1 | 0 | 0 | 0 | 0 |
---|---|---|---|---|
1 | 1 | 0 | 0 | 0 |
1 | 1 | 1 | 0 | 0 |
1 | 1 | 1 | 1 | 0 |
1 | 1 | 1 | 1 | 1 |
于是这个mask为0的位置的索引就可以用来把注意力分数矩阵的该位置置为负无穷,经过softmax后这个位置的概率就会变成0。这样就起到了mask的作用。
attention_scores = attention_scores.masked_fill(mask == 0, float('-inf'))
masked自注意力机制的内容就讲完了。
在推理过程中,和训练过程一样,decoder的输入在第一层做注意力机制时也是要masked,这一点,网上的很多教程都写错了,对大家照成了误导。 一般搞错的人会认为,推理过程后面的信息都没有产生,哪来mask的必要,实际上,decoder第一次输入“bos”,模型预测出“我”,第二次输入“bos 我”,进行计算注意力分数时,“我”当然应该利用前面的词“bos”,但是“bos”不应该利用后面的词“我”,这是因为,在训练过程中,“bos”就是在看不到“我”的情况下参与模型训练的,推理时的mask操作应和训练时完全相同,否则就会出现data shift数据偏移**,也就是模型推理结果可能是“乱七八糟的”。
回想一下Transformer模型的计算过程,如果推理时不做mask,decoder输入“bos”,模型预测出“我”,没有问题,第二次输入“bos 我”,然后进行注意力机制,不做mask,“bos”和“我”都互相包含了对方的信息,在第二个decoder的block中,然后“bos”和“我”又会做自注意力机制的计算,此时,“我”得到的“bos”的信息是已经包含了“我”的信息的“bos”,这和训练阶段的“bos”是完全不一样的,因为训练过程始终有mask,不论经过多少次的各种注意力机制的计算,“bos”这个词始终看不到后面的词,也就是不会把后面词的信息融合到自己的词向量中,所以, 在训练阶段得到的参数作用在推理时差异很大的数据上,模型“胡言乱语”是很有可能的。
另一个角度来看,可知,每当利用最后一个词预测出一个新词时(模型虽然表现为每次只预测一个词,实际上之前已经预测出来的词在最后一步,也就是做分类之前一直是参与计算的,只是做分类时只对最后一个词向量分类,没有重复对之前的词向量分类,因为如果推理时保持和训练一样的计算过程即做mask的话,之前已经预测出来的词在之后预测新词时的分类结果是一样的,不会因为新预测出一个词时,之前的词就变了),如果推理时不使用mask,就会发现每当新预测一个词,之前已经预测出来的内容也变了,请读者根据计算过程来理解这个现象。
decoder中的交叉注意力机制
我们知道,encoder和decoder的block都会重复多次,比如Transformer原论文中的6次,那么encoder最终的输出,也就是第6个block的输出走向哪里呢?最终输出假如是X_out,形状是(m,d_model),m是句子中词的个数,d_model是词向量的长度。X_out会被送到每一个decoder的block的交叉注意力层使用,流程如下,请结合Transformer的流程图理解:
- decoder的masked注意力机制层输出结果d_1到交叉注意力层(这个d_1当然也会输出到更后面的残差连接处,这里不赘述);encoder的最终输出X_out也输送给交叉注意力层
- 在交叉注意力层做的注意力机制逻辑和encoder的注意力机制一样,只不过是用参数矩阵W(Q)映射自己decoder的上一层输出d_1形成Q;用参数矩阵W(K)和W(V)映射来自encoder的输出得到K和V,然后做的事情就和encoder的注意力机制一样了。这样做的目的是从encoder的输入,比如“I love China”中提取信息,然后在结合自己的“bos”这个词,使得模型能够学到“I love China”这句英文对应的中文第一个字是“我”,利用“bos 我”结合“I love China”的信息可以学到翻译后的第二个字是“爱”,依次类推。这就是交叉注意力的含义。