自注意力机制详解

1.1 注意力
先来看注意力机制,它的思想来自于网络搜索:有一个q(query),相当于你在搜索框里打的东西;有一个v(value),相当于网络上各种各样的内容;有一个k(key),它代表value所对应的关键词,和value是一一对应的关系。
注意力权重就高),反之同理。
(eg:q是“发烧了怎么办”,有个v是“xx退烧药”,其对应的k是“发烧、退烧…”,将q和k一对比,发现相似度高,所以搜索结果中会把“xx退烧药”放在前面。)
上面的过程很符合直觉,但是有个关键的技术性细节没有讲到:到底如何计算q和k的相似度呢?主要有两个方法:
简单粗暴的方法:把q和k丢进某个神经网络,让网络给你算一个相似度出来;
数学方法:对q和k做点积。接下来展开讲一下这个方法:
我们知道,无论什么样的数据,到了神经网络里面无非就是向量(矩阵)的形式。所以这里,我们可以将q,k,v都视为向量:

对q和两个k分别做点积(这里就隐含了一个要求,q和k的长度必须相等),得到两个注意力权重:

然后,用得到的注意力权重对v求加权:

可以发现,结果是一个向量,其长度=v的长度。
当然,在实操中q,k,v的个数不可能就一两个,因此接下来要做一下维度的扩展。首先,把k,v都增加到m个:

自然而然地,k,v就可以各自合并成矩阵K,V:

(d_k表示key的长度,d_v表示value的长度)
接下来还是重复之前的步骤,先将q和K相乘(和刚才一样,K要先转置):

我们得到了一个长度为m的向量a_1,其中每一个元素分别代表q对每一个(k,v)对的注意力权重。
接下来将向量a_1与V相乘:

得到的结果和刚才一样,还是一个长度为d_v的向量。
现在,把q增加到n个,那么最终的结果就会变成一个n\times d_v的矩阵。其中每一行是以Q中对应行作为q,对(k,v)做注意力计算得来的。
1.2 自注意力
以上,就是所谓的“注意力机制”。而自注意力机制呢,其实就是在注意力机制的基础上,加入了一个前提条件:q,k,v都是一个东西。
我们用一个NLP的例子来理解自注意力机制:
输入是一个句子长度(在这里是4)\times词典长度的矩阵,它代表“state of the art”这个句子:

我们将它复制三份,每一份分别对应着Q,K,V。然后和刚才一样,对Q和K做矩阵乘法:

得到一个4\times4的矩阵A,其中的元素,比如(3,2)代表的是第三个词“the”与第二个词“of”的相似度。
然后将A和V相乘:

结果是一个和输入同样形状的矩阵。但是,在输入中,每一行的意义很简单,就是代表了句子中的每一个词——而在这里,每一行的意义是:以句子中原来的那个词为前提,考虑这个词和句子中其他词的相关性,根据相关性计算出的所有词的信息的加权。
比如说,第一行是以“state”为前提,那么“state”肯定和自己相关性最大,其次也许是”art“,然后是”of“,”the“,那么第一行中的信息最主要就来自于state,还包含了一定”art“的信息,”of“,”the“的信息可能就没多少了。
(各个矩阵现实意义的理解非常重要,之后的mask操作会用到这块的知识。)
(做任何东西都不能只关注技术细节,更要关注现实意义。经济学是这样,深度学习也是如此。)
1.3 多头自注意力
自注意力的思想还是非常符合直觉的:从人类的角度来看,对于一个包含很多词的句子,我们在考虑每一个词时,不是光考虑这个词本身,更会考虑这个词和其他词的关系。
至于多头自注意力呢…呃,说实话,就没有这么优美了,并不是很好解释,所以我们直接进入技术细节。
(”多头“和”自“其实并不一定要挂钩,它们其实是两个不相干的概念,但这里为了方便就放到一起讲了)
首先,我们要决定”头“的数量,比如说8。
”头“是什么意思呢?你可以理解为,每有一个头,就做一次自注意力。
聪明的你肯定一下就会发现不对:我们刚才谈到的自注意力机制中,是没有任何可学习参数的。换言之,在输入序列给定的情况下,你无论做多少次自注意力,结果都只有一个。
因此在多头自注意力中,并不是直接把输入序列拿来做自注意力,而是对输入序列做3n次线性投影(n为头的数量),得到n组的(q,k,v)之后,再对各组做自注意力。
”线性投影“,看起来就不像人话,在实现中其实就是经过某个不带bias的线性层后得到的东西:
linear = nn.Linear(INPUT_SIZE, OUTPUT_SIZE, bias=False)
x = torch.ones([10, INPUT_SIZE])
y = linear(x)
# y就是x进行一次线性投影后的结果,其形状是(10, OUTPUT_SIZE)
既然是线性层,那就有可学习参数;有可学习参数,得到的各组(q,k,v)就不一样,进而做自注意力的结果就不一样。
1.4 带掩码的自注意力
Transformer做预测的流程如下(假设是一个机器翻译的任务):
把待翻译的句子喂给encoder,得到某个输出结果x';
将x'与<SOS>喂给decoder,得到翻译结果的第一个词y_1;
将x'与y_1喂给decoder,得到翻译结果的第二个词y_2;
…重复以上步骤,直到decoder输出<EOS>为止。
我们可以发现,这个流程和基于RNN的Seq2Seq差不多,都是串行的,也即每一次预测必须以上一次预测的结果为根据,只能一个词一个词地输出结果,没办法一次性输出整个结果句子。
但Transformer的一个特点,它训练的过程是并行的,也即我们会一次性把整个(真实的)翻译结果都输入decoder,然后让它一次性输出整个(预测的)翻译结果。
我们还是拿刚才自注意里面的图来讲解:
(decoder的输入按理应该有这个token,这里为了方便就不加了)

行代表的是注意力的”根据“,列代表的是注意力的”对象“。比如说,元素(1,2)就是”state“对”of“的注意力——等等,这里是不是有问题?
我们的模型最终是用来做预测的,不是光训练着玩的,而预测是根据前一个词预测后一个词,比如说,我们知道了第一个词是”state“,我们是在知道且仅知道它的情况下,去预测下一个词”of“。但是上面矩阵里面的元素(1,2)是”state“对”of“的注意力——这下就变成:我们在知道”of“的情况下去预测”of“,训练的意义就没了。
所以我们需要对这个矩阵做一个”掩码“操作,使得模型在预测某个词的时候,只能看到它之前的词。具体来说,就是把下面阴影部分的地方给”删掉“:

具体是如何删掉呢?就是把这些地方的值给替换成负无穷大(或者某一个很大的负数),这样在对这个矩阵做softmax操作后,这些地方的值就会变成0。(softmax这一环刚才没有提到,因为它只能算一个技术细节,对理解自注意力机制本身并不是很关键)