Self Attention和Multi-Head Attention的原理和实现

引言

    使用深度学习做NLP的方法,一般是将单词转换为词向量序列,然后通过神经网络编码或者说提取这些词向量中的特征信息,继而根据不同任务进行不同的操作。提取特征的第一个方法是使用卷积神经网络,RNN结构简单,非常适合序列建模,但是缺点是无法并行运算,使得运算速度非常低。提取特征的第二个方法是使用卷积神经网络,但是效果并不是很好。第三个方法是使用注意力机制,注意力机制可以并行的提取序列特征。

    Attention机制本质上是人类视觉的注意力机制。人们视觉在感知东西的时候一般不会是一个场景从到头看到尾每次全部都看,而往往是根据需求观察注意特定的一部分。而且当人们发现一个场景经常在某部分出现自己想观察的东西时,人们会进行学习在将来再出现类似场景时把注意力放到该部分上。

    《基于Attention的自动标题生成》介绍了Attention机制如何应用到“编码-解码”架构中用于自然语言生成,那里面的Attention被称为“Encoder-Decoder Attention”,同时提到了Attention计算可以抽象为查询过程。简单的说,Self Attention就是Q、K、V均为同一个输入向量映射而来的Encoder-Decoder Attention,Multi-Head Attention同时计算多个Attention,并最终得到合并结果。

Self Attention原理

    self attention有什么优点呢,这里引用谷歌论文《Attention Is All You Need》里面说的,第一是计算复杂度小,第二是可以大量的并行计算,第三是可以更好的学习远距离依赖。Attention的计算公式如下:

0.png

下面一步步分解self attention的计算过程(图来自https://jalammar.github.io/illustrated-transformer/):

输入单词表示向量,比如可以是词向量。

把输入向量映射到q、k、v三个变量,如下图:

1.png

比如上图X1和X2分别是Thinking和Machines这两个单词的词向量,q1和q2被称为查询向量,k称为键向量,v称为值向量。Wq,Wk,Wv都是随机初始化的映射矩阵。

计算Attention score,即某个单词的查询向量和各个单词对应的键向量的匹配度,匹配度可以通过加法或点积得到。图如下:

2.png

减小score,并将score转换为权重。

其中dk是q k v的维度。score可以通过点积和加法得到,当dk较小时,这两种方法得到的结果很相似。但是点积的速度更快和省空间。但是当dk较大时,加法计算score优于点积结果没有除以dk^0.5的情况。原因可能是:the dot products grow large in magnitude, pushing the softmax function into regions where it has extremely small gradients。所以要先除以dk^0.5,再进行softmax。

权重乘以v,并求和。

4.png


最终的结果z就是x1这个单词的Attention向量。

Self Attention代码实现


    使用Keras自定义self attention层,代码如下:

    

from keras import initializers

from keras import activations

from keras import backend as K

from keras.engine.topology import Layer

 

class MySelfAttention(Layer):

    

    def __init__(self,output_dim,kernel_initializer='glorot_uniform',**kwargs):

        self.output_dim=output_dim

        self.kernel_initializer = initializers.get(kernel_initializer)

        super(MySelfAttention,self).__init__(**kwargs)

        

    def build(self,input_shape):

        self.W=self.add_weight(name='W',

             shape=(3,input_shape[2],self.output_dim),

             initializer=self.kernel_initializer,

             trainable=True)

        self.built = True

        

    def call(self,x):

        q=K.dot(x,self.W[0])

        k=K.dot(x,self.W[1])

        v=K.dot(x,self.W[2])

        #print('q_shape:'+str(q.shape))

        e=K.batch_dot(q,K.permute_dimensions(k,[0,2,1]))#把k转置,并与q点乘

        e=e/(self.output_dim**0.5)

        e=K.softmax(e)

        o=K.batch_dot(e,v)

        return o

        

    def compute_output_shape(self,input_shape):

        return (input_shape[0],input_shape[1],self.output_dim)

Multi-Head Attention原理

    不同的随机初始化映射矩阵Wq,Wk,Wv可以将输入向量映射到不同的子空间,这可以让模型从不同角度理解输入的序列。因此同时几个Attention的组合效果可能会优于单个Attenion,这种同时计算多个Attention的方法被称为Multi-Head Attention,或者多头注意力

    每个“Head”都会产生一个输出向量z,但是我们一般只需要一个,因此还需要一个矩阵把多个合并的注意力向量映射为单个向量。图示如下:

from keras import initializers

from keras import activations

from keras import backend as K

from keras.engine.topology import Layer

 

 

class MyMultiHeadAttention(Layer):

    def __init__(self,output_dim,num_head,kernel_initializer='glorot_uniform',**kwargs):

        self.output_dim=output_dim

        self.num_head=num_head

        self.kernel_initializer = initializers.get(kernel_initializer)

        super(MyMultiHeadAttention,self).__init__(**kwargs)

        

    def build(self,input_shape):

        self.W=self.add_weight(name='W',

           shape=(self.num_head,3,input_shape[2],self.output_dim),

           initializer=self.kernel_initializer,

           trainable=True)

        self.Wo=self.add_weight(name='Wo',

           shape=(self.num_head*self.output_dim,self.output_dim),

           initializer=self.kernel_initializer,

           trainable=True)

        self.built = True

        

    def call(self,x):

        q=K.dot(x,self.W[0,0])

        k=K.dot(x,self.W[0,1])

        v=K.dot(x,self.W[0,2])

        e=K.batch_dot(q,K.permute_dimensions(k,[0,2,1]))#把k转置,并与q点乘

        e=e/(self.output_dim**0.5)

        e=K.softmax(e)

        outputs=K.batch_dot(e,v)

        for i in range(1,self.W.shape[0]):

            q=K.dot(x,self.W[i,0])

            k=K.dot(x,self.W[i,1])

            v=K.dot(x,self.W[i,2])

            #print('q_shape:'+str(q.shape))

            e=K.batch_dot(q,K.permute_dimensions(k,[0,2,1]))#把k转置,并与q点乘

            e=e/(self.output_dim**0.5)

            e=K.softmax(e)

            #print('e_shape:'+str(e.shape))

            o=K.batch_dot(e,v)

            outputs=K.concatenate([outputs,o])

        z=K.dot(outputs,self.Wo)

        return z

        

    def compute_output_shape(self,input_shape):

        return (input_shape[0],input_shape[1],self.output_dim)

pytorch实现muti-head-attention

image.png

image.png


            
            

本博客源码Github地址:

https://github.com/zeus-y/

请随手给个star,谢谢!

打赏

评论