1、简介

论文原文:attention原文)

Transformer是2017年Google提出的, 当时主要是针对自然语言处理领域提出的。之前的RNN模型记忆长度有限且无法并行化,只有计算完时刻后的数据才能计算时刻的数据, 但Transformer都可以做到。在原论文中作者首先提出了self-attention的概念, 然后在此基础上提出Multi-Head Attention。本文针对self-attention以及multi-head attention的理论进行深入分析。


1、Self-Attention

论文中的Transformer模型如下图所示:

images

下面针对Self-Attention展开说明。为了方便理解, 假设输入的序列长度为22, 输入就是两个节点x1, x2, 然后通过input Embedding也就是图中的f(x), 将输入映射到a1, a2, 紧接着分别将a1, a2分别通过三个变换矩阵, , (这三个参数是可训练的, 是共享的), 得到对应的, , , (在源码中,这部分直接使用全连接层实现的, 这里为了方便理解)

images

其中,

q代表query, 后续会和每一个k进行匹配

k代表key, 后续会被每个qpp

v代表从a中提取得到的信息

后续q和k匹配的过程可以理解成计算二者的相关性, 相关性越大, 对应v的权重也越大。

假设a1 = (1, 1), a2 = (1, 0), , 那么,

transformers是可以并行计算的, 所以直接写成

同理可以得到,那么求得的就是原论文中的Q, 就是原论文中的K, 就是原论文中的V。接着拿q1和每个k进行match, 点乘操作, 接着除以得到对应的, 其中d代表向量的长度。 在本实例中等于2, 除以的原因在原论文中的解释是”进行点乘后的数值很大, 导致通过softmax后梯度变的很小”, 所以通过除以进行缩放, 比如计算:

同理拿去匹配所有的k能得到, 统一写成矩阵乘法形式:

然后对每一行即分别进行softmax处理得到, 这里的相当于计算得到针对每个v的权重, 到这里就完成了Attention(Q, K, V)公式中部分。

images

上面已经计算得到, 即针对每个v的权重, 接着进行加权得到最终的结果。

统一写成矩阵乘法形式

images

到这里, Self-Attention的内容就讲完了, 总结下来就是论文的一个公式。


2、Multi-Head Attention

下面看一下Multi-Head Attention模块, 实际使用中基本使用的还是Multi-Head Attention模块。原论文中说使用多头注意力机制能够联合来自不同head部分学习到的信息。

Multi-head attention allows the model to jointly attend to information from different representation subspaces at different positions.

首先还是和Self-Attention模块一样,将分别通过得到对应的, 然后再根据使用的head数据h进一步把得到的均分成h份。 比如下图假设h=2, 然后拆分成,那么就属于head1, 就属于head2。

images

这里,如果读过原论文, 会发现论文中写的是:通过映射得到每个head的

但是github上的源码就是简单的进行拆分, 其实也可以将设置成对应值来实现均分,比如下图中的Q通过就能得到均分后的

images

通过上述方法就能得到每个headi对应的, 接下来对每个head使用和Self-Attention相同的方法即可得到对应的结果。

images

然后将每个head得到的结果进行concat拼接, 比如下图中(head1得到的b1)和(head2得到的b1)拼接在一起, (head1得到的b2)和(head2得到的b2)拼接在一起。

images

接着将拼接后的结果通过(可学习的参数)进行融合, 融合后得到的结果b1, b2

images

到这里,Multi-Head Attention的内容就讲完了, 总结下来就是论文中的来个公式

images


3、Self-Attention与Multi-Head Attention计算量对比

原论文中作者说其实二者的计算量差不多。 Due to the reduced dimension of each head, the total computational cost is similar to that of single-head attention with full dimensionality.

下面做了一个简单的实验。

首先创建一个Self-Attention模块(单头)a1, 然后把proj变量置为identity(Identity对应的是Multi-Head Attention中最后那个的映射, 单头中是没有, 所以单头中identity表示不做任何操作)

再创建一个Multi-Head Attention模块(多头)a2, 然后设置8个head

创建一个随机变量, 注意shape

使用fvcore分别计算两个模块的FLOPS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# !/usr/bin/env python
# -*-coding:utf-8 -*-
"""
# @File : demo_flops.py
# @Time :
# @Author :
# @version :python 3.9
# @Software : PyCharm
# @Description:
"""
# ================【功能:】====================
import torch
from fvcore.nn import FlopCountAnalysis
from model_vit import Attention

def main():
# Self-Attention
a1 = Attention(dim=512, num_heads=1)
a1.proj = torch.nn.Identity() # remove Wo

# Multi-Head Attention
a2 = Attention(dim=512, num_heads=8)

t = (torch.rand(32, 1024, 512))

flops1 = FlopCountAnalysis(a1, t)
print("Self-Attention FLOPs:", flops1.total())

flops2 = FlopCountAnalysis(a2, t)
print("Multi-Head Attention FLOPs:", flops2.total())


if __name__ == '__main__':
main()

# Self-Attention FLOPs: 60,129,542,144
# Multi-Head Attention FLOPs: 68,719,476,736

从终端输出中发现二者的FLOPs差不多, Multi-Head Attention比Self-Attention略高一点

但其实二者的差异在最后的Wo上,如果把Multi-Head Attention的Wo也设置为Identity, 可以发现二者的FLOPs相等。


4、Position Embedding

其实上面讲的Self-Attention和Multi-Head Attention模块,在计算中没有考考位置信息。假设在Self-Attention模块中, 输入a1, a2, a3得到b1, b2, b3, 对于a1而言, a2和a3离它都是一样近的而且没有先后顺序。假设将输入的顺序改为a1, a3, a2, 对结果b1是没有任何影响的。

下面使用pytorch做一个实验, 首先创建一个Self-Attention模块, 注意这里在正向传播过程中直接传入QKV, 然后创建两个顺序不同的QKV变量t1和t2, 主要是将qkv的顺序换了以下, 分别将这两个变量输入Self-Attention进行正向传播。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# !/usr/bin/env python
# -*-coding:utf-8 -*-
"""
# @File : demo_pos_emb.py
# @Time :2023/11/4 17:15
# @Author :0399
# @version :python 3.9
# @Software : PyCharm
# @Description:
"""
# ================【功能:】====================
import torch
import torch.nn as nn

m = nn.MultiheadAttention(embed_dim=2, num_heads=1)

t1 = [[[1., 2.], # q1, k1, v1
[2., 3.], # q2, k2, v2
[3., 4.]]] # q3, k3, v3

t2 = [[[1., 2.], # q1, k1, v1
[3., 4.], # q3, k3, v3
[2., 3.]]] # q2, k2, v2

q, k, v = torch.as_tensor(t1), torch.as_tensor(t1), torch.as_tensor(t1)
print("result1: \n", m(q, k, v))


q, k, v = torch.as_tensor(t2), torch.as_tensor(t2), torch.as_tensor(t2)
print("result2: \n", m(q, k, v))

对比结果发现改变了顺序, 但是对于b1是没有影响的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
result1: 
(tensor([[[-0.2706, 0.7209],
[-0.2261, 1.0880],
[-0.1816, 1.4551]]], grad_fn=<AddBackward0>),
tensor([[[1.]],
[[1.]],
[[1.]]], grad_fn=<DivBackward0>))
result2:
(tensor([[[-0.2706, 0.7209],
[-0.1816, 1.4551],
[-0.2261, 1.0880]]], grad_fn=<AddBackward0>),
tensor([[[1.]],
[[1.]],
[[1.]]], grad_fn=<DivBackward0>))

为了引入位置信息, 论文中加入了位置编码position embedding, To this end, we add “positional encodings” to the input embeddings at the bottoms of the encoder and decoder stacks.

如下图所示,位置编码直接加在输入的a = {a1, a2, …an}中, 即pe={pe1, pe2, …pen}和a = {a1, a2, …an}拥有相同的维度大小。关于位置编码, 在论文中提出了两种方案, 一种是固定编码, 即论文中给出的sine and cosine functions方法,按照该方法计算出位置编码, 另外一种是可训练的位置编码, 作者尝试了两种方法发现结果差不多。

images


5、超参数对比

原论文中给出了一些超参数,如下表所示

images

N表示重复堆叠Transformer Block的次数

dmodel表示Multi-Head Attention输入输出token的维度

dff表示在MLP中隐层的节点个数

h表示head的个数

dk, dv表示在Multi-Head Attention中每个head的key(K)以及query(Q)的维度

pdrop表示dropout中的随机失活比例。