在2018年BERT模型横空出世后,各种各样的BERT衍生模型ALBERT, SpanBERT, DistilBERT, SesameBERT, SemBERT, SciBERT, BioBERT, MobileBERT, TinyBERT 和 CamemBERT像雨后春笋一般冒出来。这些五花八门的bert模型有没有共同之处呢?有的!
答案就是self-attention。self-attention是什么呢?他背后的数学逻辑是怎么样的呢?这就是今天我们所要探讨的问题。这篇文章的主要目的是整体过一下self-attention模型涉及到的数学计算。
一、什么是self-Attention模型? 为什么需要self-attention模型?
在之前介绍过了attention模型,对attention模型了解,如果你觉得self-Attention和attention类型,那么恭喜你答对了,在概念上和一些数学运算上,这两个模型有着很多相同得概念。
我们的人类的语言是一个变长的向量序列,在我们进行机器翻译的时候,通常会用循环神经网络(RNN)进行上下文编码,RNN是以线性方式传递单词信息的神经网络模型(即每个单词需要逐个输入进行处理)。RNN的这种线性处理的方式带来了两个问题:
**1、训练速度受限,**由于RNN天然的顺序结构,在训练时都是以线性方式处理,无法并行化,所以训练速度会受限
**2、处理长文本能力弱,**RNN在处理单词时,当前处理单词信息的状态会传递给下一个单词,一个单词的信息会随着距离的增加而衰减,在文本特别长的时候,靠前部分的单词和靠后部分的单词机会没有有效的状态传递。但是在一些长文本中,需要知道上下文才能知道单词的含义,如“I arrived at the bank after crossing the river“,这里bank是银行还是河岸呢,这就需要联系上下文,当看到river之后就应该知道这里bank很大概率指的是河岸。在RNN中就需要一步步的顺序处理从bank到river的所有词语,而当它们相距较远时RNN的效果常常较差。
为了解决这两个问题,我们就需要使用self-attention模型了。
二、self-attention计算过程是怎么样的?
在计算self-attention的过程中,每一个单词都会经过Embedding,得到词向量 ,对于每一个输入
,首先要通过线性映射到三个不同的空间,得到的是三个矩阵
、
、
。其中,
是
线性映射到
的参数矩阵。
是在训练过程中得到的参数,我们先有一个概念,在后面通过代码看看这几个参数矩阵是怎么来的。
self-attention主要参数 图片来源:jalammar.github.io/illustrated…
先来看看self-attention模型的计算过程。
self-attention计算过程
假设输入的序列为 输出的序列为
,那么,self-attention计算可以分为三个步骤
1、计算Q(查询向量Quey)、K(键向量)、Value(值向量)
2、计算注意力权重,这里使用点积来作为注意力打分函数
可以简写为:
其中, 表示查询向量
或者键向量
的维度
3、计算输出向量序列
其中, 为输出和输入向量序列的位置,
表示第j个输入到第n个输出关注的权重
上面的描述可能还是比较抽象,我们用一个例子来看看具体的计算过程。
在我们的例子中,我们初始化 为如下的值:
参数初始化:
参数矩阵
[[0, 0, 1],
[1, 1, 0],
[0, 1, 0],
[1, 1, 0]]
参数矩阵
[[1, 0, 1],
[1, 0, 0],
[0, 0, 1],
[0, 1, 1]]
参数矩阵
[[0, 2, 0],
[0, 3, 0],
[1, 0, 3],
[1, 1, 0]]
注: 一般使用_Gaussian, Xavier_ 和 _Kaiming_随机分布初始化。在训练开始之前完成这些初始化工作。tensor2tensor是google开源的框架,里面实现了attention、self-attention、bert等模型,不过这个模型已经过时了,google开发了trax作为tensor2tensor的替代品,我们这儿就看看trax在self-attention模型里面关于Q、K、V是怎么处理的。
为方便理解,删掉了一些代码,只留下主干。
# trax/layers/attention.py
def AttentionQKV(d_feature, n_heads=1, dropout=0.0, mode='train',
cache_KV_in_predict=False, q_sparsity=None,
result_sparsity=None):
# 构造q、k、v处理层,是一个d_feature个神经元的全连接层
k_processor = core.Dense(d_feature)
v_processor = core.Dense(d_feature)
if q_sparsity is None:
q_processor = core.Dense(d_feature)
return cb.Serial(
cb.Parallel(
q_processor,
k_processor,
v_processor,
),
PureAttention( # pylint: disable=no-value-for-parameter
n_heads=n_heads, dropout=dropout, mode=mode),
result_processor
)
其中core.Dense构造了一个全连接层,全连接层会调用init_weights_and_state函数初始化权重
初始化隐藏层权重
# trax/layers/core.py
def init_weights_and_state(self, input_signature):
shape_w = (input_signature.shape[-1], self._n_units)
shape_b = (self._n_units,)
# 随机初始化隐藏层的权重
rng_w, rng_b = fastmath.random.split(self.rng, 2)
w = self._kernel_initializer(shape_w, rng_w)
if self._use_bfloat16:
w = w.astype(jnp.bfloat16)
if self._use_bias:
b = self._bias_initializer(shape_b, rng_b)
if self._use_bfloat16:
b = b.astype(jnp.bfloat16)
self.weights = (w, b)
else:
self.weights = w
输入X:
X1=[1,0,1,0]
x2=[0,2,0,2]
x3=[1,1,1,1]
第一步:计算
#计算Q
[1, 0, 1]
[1, 0, 1, 0] [1, 0, 0] [1, 0, 2]
[0, 2, 0, 2] x [0, 0, 1] = [2, 2, 2]
[1, 1, 1, 1] [0, 1, 1] [2, 1, 3]
#计算K
[0, 0, 1]
[1, 0, 1, 0] [1, 1, 0] [0, 1, 1]
[0, 2, 0, 2] x [0, 1, 0] = [4, 4, 0]
[1, 1, 1, 1] [1, 1, 0] [2, 3, 1]
#计算V
[0, 2, 0]
[1, 0, 1, 0] [0, 3, 0] [1, 2, 3]
[0, 2, 0, 2] x [1, 0, 3] = [2, 8, 0]
[1, 1, 1, 1] [1, 1, 0] [2, 6, 3]
query、key、value计算
第二步:计算注意力权重
我们按照点积的方式计算注意力权重,计算注意力权重的公式如下:
首先计算注意力权重,通过计算K的转置矩阵和Q的点积得到。
[1, 0, 2] [0, 4, 2] [2, 4, 4]
[2, 2, 2] x [1, 4, 3] = [4, 16, 12]
[2, 1, 3] [1, 0, 1] [4, 12, 10]
其中, 表示查询向量
或者键向量
的维度,在这里,
= 3, 为了计算方便,我们只取一位小数,那么√_3_=1.7。
所以,根据 计算可以得到:
[1.2, 2.4, 2.4]
[2.4, 9.4, 7.1]
[2.4, 7.1, 5.9]
最后,我们计算 ,得到注意力权重矩阵
# 注意力权重矩阵
[0.1, 0.4, 0.4]
[0.0, 0.9, 0.0]
[0.0, 0.7, 0.2]
注意力权重计算
对于查询Q来说,不同的键值K都有不同的注意力权重,例如:对于输入 ,键值为
,对应的注意力权重分别为0.1、0.4、0.4。
第三步:计算输出向量序列
计算输出向量序列的公式如下:
其中, 为输出和输入向量序列的位置,
表示第j个输入到第n个输出关注的权重
h1 = [1, 2, 3] * 0.1 + [2, 8, 0] * 0.4 + [2, 6, 3] * 0.4
= [1.7, 5.8, 1.5]
h2 = [1, 2, 3] * 0.0 + [2, 8, 0] * 0.9 + [2, 6, 3] * 0.0
= [1.8, 7.2, 0]
h3 = [1, 2, 3] * 0.0 + [2, 8, 0] * 0.7 + [2, 6, 3] * 0.2
= [1.8, 6.8, 0.6]
self-attention计算过程
三、多头注意力机制:
self-attention模型可以看作在一个线性投影空间建立输入X中不同向量之间的交互关系,为了提取更多的交互信息,我们可以使用多头注意力(Multi-Head self-attention),在多个不同的投影空间中捕捉不同的交互信息。
多头注意力机制是self-attention的扩展,对于输入x,使用多头注意力,设使用的head的数量为n,那么,会把输入的向量x划分成n个独立的向量,每向量都使用self-attention计算注意力权重,完成之后在进行合并。
在我们下一篇讲解的transformer模型用到的就是多头注意力机制。
self-attention模型计算权重 只依赖
和
的相关性,而忽略了输入信息的位置信息,因此在单独使用的时候一般需要加入位置编码信息来进行修正。
参考:
jalammar.github.io/illustrated…
towardsdatascience.com/illustrated…
《神经网络与深度学习》
《机器阅读理解:算法与实践》