Self Attention原理
self attention有什么优点呢,这里引用谷歌论文《Attention Is All You Need》里面说的,第一是计算复杂度小,第二是可以大量的并行计算,第三是可以更好的学习远距离依赖。Attention的计算公式如下:
下面一步步分解self attention的计算过程(图来自https://jalammar.github.io/illustrated-transformer/):
- 输入单词表示向量,比如可以是词向量。
- 把输入向量映射到q、k、v三个变量,如下图:比如上图X1和X2分别是Thinking和Machines这两个单词的词向量,q1和q2被称为查询向量,k称为键向量,v称为值向量。Wq,Wk,Wv都是随机初始化的映射矩阵。
- 计算Attention score,即某个单词的查询向量和各个单词对应的键向量的匹配度,匹配度可以通过加法或点积得到。图如下:
- 减小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,并求和。最终的结果z就是x1这个单词的Attention向量。当同时计算所有单词的Attention时,图示如下:1. 将输入词向量转换为Q、K、V.2. 直接计算Z
Self Attention代码实现
使用Keras自定义self attention层,代码如下:
from keras import initializersfrom keras import activationsfrom keras import backend as Kfrom 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,但是我们一般只需要一个,因此还需要一个矩阵把多个合并的注意力向量映射为单个向量。图示如下:
Multi-Head Attention代码实现
还是使用Keras实现multi-head attention,代码如下:
from keras import initializersfrom keras import activationsfrom keras import backend as Kfrom 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)