层层剖析,让你彻底搞懂Self-Attention、MultiHead-Attention和Masked-Attention的机制和原理


中文 | English

本文内容

本文基于李宏毅老师对 Self-Attention 的讲解,进行理解和补充,并结合Pytorch代码,最终目的是使得自己和各位读者更好的理解Self-Attention

李宏毅Self-Attention链接: https://www.youtube.com/watch?v=hYdO9CscNes PPT链接见视频下方

通过本文的阅读,你可以获得以下知识:

  1. 什么是Self-Attention,为什么要用Self-Attention
  2. Self-Attention是如何做的
  3. Self-Attention是如何设计的
  4. Self-Attention公式的细节
  5. MultiHead Attention
  6. Masked Attention

一、Self-Attention

1.1. 为什么要使用Self-Attention

假设现在一有个词性标注(POS Tags)的任务,例如:输入I saw a saw(我看到了一个锯子)这句话,目标是将每个单词的词性标注出来,最终输出为N, V, DET, N(名词、动词、定冠词、名词)。 在这里插入图片描述

这句话中,第一个saw为动词,第二个saw(锯子)为名词。如果想做到这一点,就需要保证机器在看到一个向量(单词)时,要同时考虑其上下文,并且,要能判断出上下文中每一个元素应该考虑多少。例如,对于第一个saw,要更多的关注I,而第二个saw,就应该多关注a

这个时候,就要Attention机制来提取这种关系:如果一个任务的输入是一个Sequence(一排向量),而且各向量之间有一定关系,那么就要利用Attention机制来提取这种关系

1.2. 直观的感受下Self-Attention


在这里插入图片描述
该图描述了Self-Attention的使用。Self-Attention接受一个Sequence(一排向量,可以是输入,也可以是前面隐层的输出),然后Self-Attention输出一个长度相同的Sequence,该Sequence的每个向量都充分考虑了上下文。 举个例子,输入是Isawasaw,对应向量为:

I=[100],  saw=[010],  a=[001],  saw=[010]

在经过Self-Attention层之后,可能就变成了这样:

I=[0.70.280.02],  saw=[0.340.650.01],  a=[0.20.20.6],  saw=[0.010.50.49]

对于第一个saw,它除了自身外,还要考虑 0.34I;对于第二个saw,它要考虑0.49a

1.3. Self-Attenion是如何考虑上下文的


在这里插入图片描述
如图所示,每个输入都会和其他输入计算一个相关性分数,然后基于该分数,输出包含上下文信息的新向量

对于上图,a1需要与 a1,a2,a3,a4 分别计算相关性分数 α1,1,α1,2,α1,3,α1,4需要和自己也计算一下), α 的分数越高,表示两个向量的相关度越高

计算好 α1, 后,就可以求出新的包含上下文信息的向量 b1,假设 α1,1=5,α1,2=2,α1,3=1,α1,4=2,则:

b1=iα1,iai=5a1+2a2+1a3+2a4

同理,对于 b2,首先计算权重 α2,1,α2,2,α2,3,α2,4 , 然后进行加权求和

如果按照上面这个式子做,还有两个问题:

  1. α 之和不为1,这样会将输入向量放大或缩小
  2. 直接用输入向量ai去乘的话,拟合能力不够好

对于问题1,通常的做法是将 α 过一个Softmax(当然也可以选择其他的方式)

对于问题2,通常是将 ai 乘个矩阵(该矩阵是训练出来的),然后生成 vi ,然后用 vi 去乘 α

1.4. 如何计算相关性分数 α

首先,复习下向量相乘。两个向量相乘(做内积),公式为:ab=|a||b|cosθ , 通过公式可以很容易得出结论:

  • 两个向量夹角越小(越接近),其内积越大,相关性越高。反之,两个向量夹角越大,相关性越差,如果夹角为90°,两向量垂直,内积为0,无相关性

通过上面的结论,很容易想到,要计算 a1a2 的相关性,直接做内积即可,即 α1,2=a1a2 。 但如果直接这样,显然不好,例如,句子I saw a sawsawsaw相关性一定很高(两个一样的向量夹角为0),这样不就错了嘛。

为了解决上面这个问题,Self-Attention又额外“训练”了两个矩阵 WqWk

  • Wq 负责对“主角”进行线性变化,将其变换为 q,称为query
  • Wk 负责对“配角”进行线性变化,将其变换为 k,称为key

有了WqWk,我们就可以计算 a1a2 的相关分数 α1,2了,即:

α1,2=q1k2=(Wqa1)(Wka2)

上面这些内容可以汇总成如下图:
在这里插入图片描述
要计算 a1(主角)与 a1,a2,a3,a4(配角)的相关度,需要经历如下几步:

  1. 通过 Wq ,计算 q1
  2. 通过 Wk,计算 k1,k2,k3,k4
  3. 通过 qk , 计算 α1,1,α1,2,α1,3,α1,4

上图并没有把 k1 画出来,但实际计算的时候,需要计算 k1,即需要计算 a1和其自身的相关分数。

1.5. 将 α 归一化

还记得上面提到的,α之和不为1,所以,在上面得到了 α1, 后,还需要过一下Softmax,将α1,进行归一化。如下图:


在这里插入图片描述

最终,会将归一化后的 α1, 作为 a1 与其它向量的相关分数。 同理,a2,a3,... 向量与其他向量的相关分数也这么求。

不一定非要用Softmax,你开心想用什么都行,说不定效果还不错,也不一定非要归一化。 只是通常是这么做的

1.6. 整合上述内容

求出了相关分数 α,就可以进行加权求和计算出包含上下文信息的向量 b 了。还记得上面提到过,如果直接用 aα 进行加权求和,泛化性不够好,所以需要对 a 进行线性变换,得到向量 v,所以Self-Attention还需要训练一个矩阵 Wv 用于对 a 进行线性变化,即:

v1=Wva1        v2=Wva2         v3=Wva3           v4=Wva4

然后就可用 vα 进行加权求和,得到 b 了。

b1=iα1,ivi=α1,1v1+α1,2v2+α1,3v3+α1,4v4

将求 b1 的整个过程可以归纳为下图:


在这里插入图片描述
用更正式的话描述一下整个过程:

有一组输入序列 I=(a1,a2,,an),其中 ai 为向量, 将序列 I 通过Self-Attention,可以将其转化为另外一个序列 O=(b1,b2,,bn),其中向量 bi 是由向量 ai 结合其上下文得出的bi 的求解过程如下:

  1. 求出查询向量 qi, 公式为 qi=Wqai
  2. 求出 k1,k2,,kn,公式为 kj=Wkaj
  3. 求出 αi,1,αi,2,,αi,n , 公式为 αi,j=qikj
  4. αi,1,αi,2,,αi,n 进行归一化得到 αi,1,αi,2,,αi,n,公式为 αi,j=Softmax(αi,j;αi,)=exp(αi,j)/texp(αi,t)
  5. 求出向量v1,v2,,vn, 公式为: vj=Wvaj
  6. 求出 bi, 公式为 bi=jαi,jvj

其中,Wq,Wk,Wv 都是训练出来的


到这里Self-Attention的面纱已经揭开,但还没有结束,因为上面的步骤如果写成代码,需要大量的for循环,显然效率太低,所以需要进行向量化,能合并成向量的合成向量,能合并成矩阵的合成矩阵

1.7. 向量化

向量a 的矩阵化,假设列向量 ai 维度为 d,显然可以将输入转化为矩阵 I,公式为:

Id×n=(a1,a2,,an)

接下来定义 Wq,Wk,Wv 矩阵,其中WqWk的矩阵维度必须一致,为dk×d,而Wv的矩阵维度为dv×d,其中 dkdv 都是需要调的超参数(一般与词向量的维度 d 保持一致)dk 只影响过程,但 dv 会影响结果,即 dv 是Attention的输出向量 b 的维度。 定义好 Wq 的维度后,就可以将 q 矩阵化了,

向量 q 的矩阵化,公式为:

Qdk×n=(q1,q2,,qn)=Wdk×dqId×n

同理,向量k的矩阵化,公式为:

Kdk×n=(k1,k2,,kn)=WkI

同理,向量v的矩阵化,公式为:

Vdv×n=(v1,v2,,vn)=WvI

得到了矩阵QK,那么就很容易得出相关分数 α 的矩阵了,

相关分数 α 的矩阵为

An×n=[α1,1α2,1αn,1α1,2α2,2αn,2α1,nα2,nαn,n]=KTQ=[k1Tk2TknT](q1,q2,,qn)

我的定义 ki 是列向量,所以要转置一下

进一步,α 的矩阵为

An×n=softmax(A)=[α1,1α2,1αn,1α1,2α2,2αn,2α1,nα2,nαn,n]

A 有了,V 有了,那就可以对输出向量 b 进行矩阵化了,

输出向量b的矩阵化,公式为:

Odv×n=(b1,b2,,bn)=Vdv×nAn×n=(v1,v2,,vn)[α1,1α2,1αn,1α1,2α2,2αn,2α1,nα2,nαn,n]

将上面全部整合起来,就可以的到,整合后的公式

O=Attention(Q,K,V)=Vsoftmax(KTQ)

如果你看过其他文章,你应该会看到真正的最终公式如下:

 Attention (Q,K,V)=softmax(QKTdk)V

其实我们的公式和这个公式只差了一个转置和 dk 。转置不比多说,就是表示方式不同。

原公式的Q,K,V以及输出O,对应我们公式的 QT,KT,VTOT

1.8. dk是什么,为什么要除以 dk

首先,dk是Q和K矩阵的行维度,也就是上面的 Qdk×d中的 dk 。而矩阵相乘会放大原有矩阵的标准差,放大的倍数约为dk,为了将标准差缩放回原来的大小,所以要除以 dk

例如,假设 Qn×dkKn×dk 的均值为0,标准差为1。则矩阵 QKT 的均值为0,标准差为 dk,矩阵相乘使得其标准差放大了 dk

矩阵的均值就是把所有的元素加起来除以元素数量,方差同理。

可以通过以下代码验证这个结论(数学不好,只能通过实验验证结论了,哭):

```python
Q = np.random.normal(size=(123, 456)) # 生成均值为0,标准差为1的 Q和K
K = np.random.normal(size=(123, 456))
print("Q.std=%s, K.std=%s, \nQ·K^T.std=%s, Q·K^T/√d.std=%s" 
      % (Q.std(), K.std(), 
         Q.dot(K.T).std(), Q.dot(K.T).std() / np.sqrt(456)))
```
Q.std=0.9977961671085275, K.std=1.0000574599289282,
Q·K^T.std=21.240017020263437, Q·K^T/√d.std=0.9946549289466212

通过输出可以看到,Q和K的标准差都为1,但是两矩阵相乘后,标准差却变为了 21.24, 通过除以 dk,标准差又重新变为了 1

再看另一个例子,该例子Q和K的标准差是随机的,更符合真实的情况:

```python
Q = np.random.normal(loc=1.56, scale=0.36, size=(123, 456)) # 生成均值为随机,标准差为随机的 Q和K
K = np.random.normal(loc=-0.34, scale=1.2, size=(123, 456))
print("Q.std=%s, K.std=%s, \nQ·K^T.std=%s, Q·K^T/√d.std=%s" 
      % (Q.std(), K.std(), 
         Q.dot(K.T).std(), Q.dot(K.T).std() / np.sqrt(456)))
```
Q.std=0.357460640868945, K.std=1.204536717914841, 
Q·K^T.std=37.78368871510589, Q·K^T/√d.std=1.769383337989377

可以看到,最开始Q的标准差为 0.35, K的标准差为 1.20,结果矩阵相乘后标准差达到了 37.78, 经过缩放后,标准差又回到了1.76

1.9. 代码实战:Pytorch定义SelfAttention模型

接下来使用Pytorch来定义SelfAttention模型,这里使用原论文中的公式:

 Attention (Q,K,V)=softmax(QKTdk)V

这里为了使代码定义逻辑更清晰,下面我将各个部分的维度标记出来:

On×dv= Attention (Qn×dk,Kn×dk,Vn×dv)=softmax(Qn×dkKdk×nTdk)Vn×dv=An×nVn×dv

其中,各个变量定义为:

  • n:input_num,输入向量的数量,例如,你一句话包含20个单词,则该值为20
  • dk:dimension of K,Q和K矩阵的行维度(超参数,需要自己调,一般和输入向量维度 d 一致即可),该值决定了线性层的宽度。
  • dv:dimension of V,V矩阵的行维度,该值为输出向量的维度(超参数,需要自己调,一般取值和输入向量维度 d 保持一致)。

上述公式中,Q,K,V是通过矩阵 Wq,Wk,Wv和输入向量 I 计算出来的,而一般对于要训练的矩阵,代码中一般使用线性层来表示,详情可参考:Pytorch nn.Linear的基本用法,所以最终 Q 矩阵的计算公式为:

Qn×dk=In×dWd×dkq        (2)

K,V 矩阵同理。其中

  • d:input_vector_dim: 输入向量的维度,例如你将单词编码为了10维的向量,则该值为10

有了公式(1)和(2),就可以定义SelfAttention模型了,代码如下:

```python
class SelfAttention(nn.Module):
    def __init__(self, input_vector_dim: int, dim_k=None, dim_v=None):
        """
        初始化SelfAttention,包含如下关键参数:
        input_vector_dim: 输入向量的维度,对应上述公式中的d,例如你将单词编码为了10维的向量,则该值为10
        dim_k: 矩阵W^k和W^q的维度
        dim_v: 输出向量的维度,即b的维度,例如,经过Attention后的输出向量b,如果你想让他的维度为15,则该值为15,若不填,则取input_vector_dim
        """
        super(SelfAttention, self).__init__()

        self.input_vector_dim = input_vector_dim
        # 如果 dim_k 和 dim_v 为 None,则取输入向量的维度
        if dim_k is None:
            dim_k = input_vector_dim
        if dim_v is None:
            dim_v = input_vector_dim

        """
        实际写代码时,常用线性层来表示需要训练的矩阵,方便反向传播和参数更新
        """
        self.W_q = nn.Linear(input_vector_dim, dim_k, bias=False)
        self.W_k = nn.Linear(input_vector_dim, dim_k, bias=False)
        self.W_v = nn.Linear(input_vector_dim, dim_v, bias=False)

        # 这个是根号下d_k
        self._norm_fact = 1 / np.sqrt(dim_k)

    def forward(self, x):
        """
        进行前向传播:
        x: 输入向量,size为(batch_size, input_num, input_vector_dim)
        """
        # 通过W_q, W_k, W_v矩阵计算出,Q,K,V
        # Q,K,V矩阵的size为 (batch_size, input_num, output_vector_dim)
        Q = self.W_q(x)
        K = self.W_k(x)
        V = self.W_v(x)

        # permute用于变换矩阵的size中对应元素的位置,
        # 即,将K的size由(batch_size, input_num, output_vector_dim),变为(batch_size, output_vector_dim,input_num)
        # 0,1,2 代表各个元素的下标,即变换前,batch_size所在的位置是0,input_num所在的位置是1
        K_T = K.permute(0, 2, 1)

        # bmm是batch matrix-matrix product,即对一批矩阵进行矩阵相乘
        # bmm详情参见:https://pytorch.org/docs/stable/generated/torch.bmm.html
        atten = nn.Softmax(dim=-1)(torch.bmm(Q, K_T) * self._norm_fact)

        # 最后再乘以 V
        output = torch.bmm(atten, V)

        return output

```

接下来使用一下,定义50个为一批(batch_size=50),输入向量维度为3, 一次输入5个向量,欲经过Attention层后,编码成5个4维的向量

```python
model = SelfAttention(3, 5, 4)
model(torch.Tensor(50,5,3)).size()
```
torch.Size([50, 5, 4])

Attention模型一般作为整体模型的一部分,是套在其他模型中使用的,最经典的莫过于Transformer



二. MultiHead Attention 和 Masked Attention

MultiHead Attention 和 Masked Attention 请参考下篇MultiHead-Attention和Masked-Attention的机制和原理


参考资料

李宏毅Self-Attention: https://www.youtube.com/watch?v=hYdO9CscNes

超详细图解Self-Attention: https://zhuanlan.zhihu.com/p/410776234

Pytorch nn.Linear的基本用法:https://blog.csdn.net/zhaohongfei_358/article/details/122797190

极简翻译模型Demo,彻底理解Transformer:https://zhuanlan.zhihu.com/p/360343417

annotated-transformer:https://github.com/harvardnlp/annotated-transformer/

Next Post Previous Post
No Comment
Add Comment
comment url