BERT模型入门系列(三):Self-Attention详解

5,753 阅读6分钟

在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模型计算权重 [公式] 只依赖 [公式][公式] 的相关性,而忽略了输入信息的位置信息,因此在单独使用的时候一般需要加入位置编码信息来进行修正。

参考:

zhuanlan.zhihu.com/p/47282410

jalammar.github.io/illustrated…

arxiv.org/abs/1706.03…

towardsdatascience.com/illustrated…

google/trax

《神经网络与深度学习》

《机器阅读理解:算法与实践》