[深度学习] 自然语言处理 --- Self-Attention(一) 基本介绍_小墨鱼的专栏-CSDN博客
https://zengwenqi.blog.csdn.net/article/details/102680781[深度学习] 自然语言处理 --- Self-Attention(二) 动画与代码演示_小墨鱼的专栏-CSDN博客
https://zengwenqi.blog.csdn.net/article/details/115906518[深度学习] 自然语言处理 --- Self-Attention(三) 知识点与源码解析_小墨鱼的专栏-CSDN博客
https://zengwenqi.blog.csdn.net/article/details/115917038
一 Self Attention
Self Attention也经常被称为intra Attention(内部Attention),最近一年也获得了比较广泛的使用,比如Google最新的机器翻译模型内部大量采用了Self Attention模型。
在一般任务的Encoder-Decoder框架中,输入Source和输出Target内容是不一样的,比如对于英-中机器翻译来说,Source是英文句子,Target是对应的翻译出的中文句子,Attention机制发生在Target的元素Query和Source中的所有元素之间。
而Self Attention顾名思义,指的不是Target和Source之间的Attention机制,而是Source内部元素之间或者Target内部元素之间发生的Attention机制,也可以理解为Target=Source这种特殊情况下的注意力计算机制。其具体计算过程是一样的,只是计算对象发生了变化而已,所以此处不再赘述其计算过程细节。
如果是常规的Target不等于Source情形下的注意力计算,其物理含义正如上文所讲,比如对于机器翻译来说,本质上是目标语单词和源语单词之间的一种单词对齐机制。那么如果是Self Attention机制,一个很自然的问题是:通过Self Attention到底学到了哪些规律或者抽取出了哪些特征呢?或者说引入Self Attention有什么增益或者好处呢?我们仍然以机器翻译中的Self Attention来说明,图1和图2是可视化地表示Self Attention在同一个英语句子内单词间产生的联系。
从两张图可以看出,Self Attention可以捕获同一个句子中单词之间的一些句法特征(比如图1展示的有一定距离的短语结构)或者语义特征(比如图2展示的its的指代对象Law)。
很明显,引入Self Attention后会更容易捕获句子中长距离的相互依赖的特征,因为如果是RNN或者LSTM,需要依次序序列计算,对于远距离的相互依赖的特征,要经过若干时间步步骤的信息累积才能将两者联系起来,而距离越远,有效捕获的可能性越小。
但是Self Attention在计算过程中会直接将句子中任意两个单词的联系通过一个计算步骤直接联系起来,所以远距离依赖特征之间的距离被极大缩短,有利于有效地利用这些特征。除此外,Self Attention对于增加计算的并行性也有直接帮助作用。这是为何Self Attention逐渐被广泛使用的主要原因。
二 Attention机制的本质思想
如果把Attention机制从Encoder-Decoder框架中剥离,并进一步做抽象,可以更容易看懂Attention机制的本质思想。
Attention机制的本质思想
我们可以这样来看待Attention机制:将Source中的构成元素想象成是由一系列的<Key,Value>数据对构成,此时给定Target中的某个元素Query,通过计算Query和各个Key的相似性或者相关性,得到每个Key对应Value的权重系数,然后对Value进行加权求和,即得到了最终的Attention数值。所以本质上Attention机制是对Source中元素的Value值进行加权求和,而Query和Key用来计算对应Value的权重系数。即可以将其本质思想改写为如下公式:
其中,=||Source||代表Source的长度,公式含义即如上所述。
上文所举的机器翻译的例子里,因为在计算Attention的过程中,Source中的Key和Value合二为一,指向的是同一个东西,也即输入句子中每个单词对应的语义编码,所以可能不容易看出这种能够体现本质思想的结构。
当然,从概念上理解,把Attention仍然理解为从大量信息中有选择地筛选出少量重要信息并聚焦到这些重要信息上,忽略大多不重要的信息,这种思路仍然成立。聚焦的过程体现在权重系数的计算上,权重越大越聚焦于其对应的Value值上,即权重代表了信息的重要性,而Value是其对应的信息。
从图中可以引出另外一种理解,也可以将Attention机制看作一种软寻址(Soft Addressing):Source可以看作存储器内存储的内容,元素由地址Key和值Value组成,当前有个Key=Query的查询,目的是取出存储器中对应的Value值,即Attention数值。通过Query和存储器内元素Key的地址进行相似性比较来寻址,之所以说是软寻址,指的不像一般寻址只从存储内容里面找出一条内容,而是可能从每个Key地址都会取出内容,取出内容的重要性根据Query和Key的相似性来决定,之后对Value进行加权求和,这样就可以取出最终的Value值,也即Attention值。所以不少研究人员将Attention机制看作软寻址的一种特例,这也是非常有道理的。
至于Attention机制的具体计算过程,如果对目前大多数方法进行抽象的话,可以将其归纳为两个过程:
第一个过程是根据Query和Key计算权重系数,第一个过程细分为两个阶段:
- 第一个阶段根据Query和Key计算两者的相似性或者相关性;
- 第二个阶段对第一阶段的原始分值进行归一化处理;
二个过程根据权重系数对Value进行加权求和。
这样,可以将Attention的计算过程抽象为如图10展示的三个阶段。
图10 三阶段计算Attention过程
在第一个阶段,可以引入不同的函数和计算机制,根据Query和某个,计算两者的相似性或者相关性,最常见的方法包括:求两者的向量点积、求两者的向量Cosine相似性或者通过再引入额外的神经网络来求值,即如下方式:
第一阶段产生的分值根据具体产生的方法不同其数值取值范围也不一样,第二阶段引入类似SoftMax的计算方式对第一阶段的得分进行数值转换,一方面可以进行归一化,将原始计算分值整理成所有元素权重之和为1的概率分布;另一方面也可以通过SoftMax的内在机制更加突出重要元素的权重。即一般采用如下公式计算:
第二阶段的计算结果即为
对应的权重系数,然后进行加权求和即可得到Attention数值:
通过如上三个阶段的计算,即可求出针对Query的Attention数值,目前绝大多数具体的注意力机制计算方法都符合上述的三阶段抽象计算过程。
三 Self Attention模型与实现
通过上述对Attention本质思想的梳理,我们可以更容易理解本节介绍的Self Attention模型。
有了query,key,value概念之后,就比较好理解Self-Attention
1. 输入的词汇(翻译中一句话分成的一组词)都要embedding成一个固定长度的向量x才输入模型的。即对于一句话的所有词,组成了一个输入矩阵X。
2. 随机生成3个矩阵Q,K,V对应query,key,value
3. 对于一个输入x,用x点乘Q得到query,用x点乘K得到key, 用x点乘V得到value
对于一句话中的所有x,都可以得到对应的query,key,value
每个x,都可以用自己的query去和其他key计算score,然后用该score和对应的其他value来计算自己的注意力向量C。经过这样的计算,x变成了C。
上图中的z即为C。对于self-attention来讲,Q(Query), K(Key), V(Value)三个矩阵均来自同一输入,为了防止其结果过大,会除以一个尺度标度.
如果将输入的所有向量合并为矩阵形式,则所有query, key, value向量也可以合并为矩阵形式表示
其中 WQ, WK, WV是我们模型训练过程学习到的合适的参数。上述操作即可简化为矩阵形式
4. 同样可以多叠加几层self-attention,用同样的操作不同的QKV矩阵由C变成CC,变成CCC,这就是mutil-head self-attention。
self-attention像是一种向量转换。x变为c,维度没变,值变了。而同时,这种转变又蕴含了x与上下文x之间的关系。rnn也可以实现由x变为另一个向量,同时也考虑了上下文关系,但是,他存在循环神经网络的弊端,无法并行。而self-attention组成的transformer则可以实现并行运算。即,他不需要等待下一个状态h计算出来再计算C,而是直接通过QKV矩阵和当前x计算所得。
那QKV怎么得到?随机初始,训练所得。
tf.keras实现自定义网络层。需要实现以下三个方法:(注意input_shape是包含batch_size项的
)
build(input_shape)
: 这是你定义权重的地方。这个方法必须设self.built = True
,可以通过调用super([Layer], self).build()
完成。call(x)
: 这里是编写层的功能逻辑的地方。你只需要关注传入call
的第一个参数:输入张量,除非你希望你的层支持masking。compute_output_shape(input_shape)
: 如果你的层更改了输入张量的形状,你应该在这里定义形状变化的逻辑,这让Keras能够自动推断各层的形状。
#! -*- coding: utf-8 -*-
import tensorflow.keras.backend as K
import tensorflow as tf
class Position_Embedding(tf.keras.layers.Layer):
def __init__(self, size=None, mode='sum', **kwargs):
self.size = size # 必须为偶数
self.mode = mode
super(Position_Embedding, self).__init__(**kwargs)
def call(self, x):
if (self.size == None) or (self.mode == 'sum'):
self.size = int(x.shape[-1])
batch_size, seq_len = K.shape(x)[0], K.shape(x)[1]
position_j = 1. / K.pow(10000., 2 * K.arange(self.size / 2, dtype='float32') / self.size)
position_j = K.expand_dims(position_j, 0)
position_i = K.cumsum(K.ones_like(x[:, :, 0]), 1) - 1 # K.arange不支持变长,只好用这种方法生成
position_i = K.expand_dims(position_i, 2)
position_ij = K.dot(position_i, position_j)
position_ij = K.concatenate([K.cos(position_ij), K.sin(position_ij)], 2)
if self.mode == 'sum':
return position_ij + x
elif self.mode == 'concat':
return K.concatenate([position_ij, x], 2)
def compute_output_shape(self, input_shape):
if self.mode == 'sum':
return input_shape
elif self.mode == 'concat':
return (input_shape[0], input_shape[1], input_shape[2] + self.size)
class Attention(tf.keras.layers.Layer):
def __init__(self, output_dim, **kwargs):
self.output_dim = output_dim
super(Attention, self).__init__(**kwargs)
def build(self, input_shape):
self.WQ = self.add_weight(name='WQ',
shape=(input_shape[-1], self.output_dim),
initializer='glorot_uniform',
trainable=True)
self.WK = self.add_weight(name='WK',
shape=(input_shape[-1], self.output_dim),
initializer='glorot_uniform',
trainable=True)
self.WV = self.add_weight(name='WV',
shape=(input_shape[-1], self.output_dim),
initializer='glorot_uniform',
trainable=True)
super(Attention, self).build(input_shape)
def call(self, x):
# 对Q、K、V做线性变换
Q_seq = K.dot(x, self.WQ)
K_seq = K.dot(x, self.WK)
V_seq = K.dot(x, self.WV)
print("\n")
print("--"*25)
print("Q_seq.shape: ", Q_seq.shape)
print("K.permute_dimensions(K_seq, [0, 2, 1]).shape: ",K.permute_dimensions(K_seq, [0, 2, 1]).shape)
QK = K.batch_dot(Q_seq, K.permute_dimensions(K_seq, [0, 2, 1]))
QK = QK / K.int_shape(x)[-1] ** 0.5
QK = K.softmax(QK)
print("QK.shape: ",QK.shape)
Z_seq = K.batch_dot(QK, V_seq)
print("Z_seq.shape: ",Z_seq.shape)
print("=="*25)
return Z_seq
def compute_output_shape(self, input_shape):
return (input_shape[0], input_shape[1], self.output_dim)
from __future__ import print_function
import tensorflow as tf
import tensorflow.keras.datasets.imdb as imdb
import tensorflow.keras.preprocessing.sequence as sequence
from attention import Position_Embedding, Attention
max_features = 10000
maxlen = 80
batch_size = 32
print('Loading data...')
(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=max_features)
print(len(x_train), 'train sequences')
print(len(x_test), 'test sequences')
print('Pad sequences (samples x time)')
x_train = sequence.pad_sequences(x_train, maxlen=maxlen)
x_test = sequence.pad_sequences(x_test, maxlen=maxlen)
print('x_train shape:', x_train.shape)
print('x_test shape:', x_test.shape)
S_inputs = tf.keras.layers.Input(shape=(None,), dtype='int32')
embeddings = tf.keras.layers.Embedding(max_features, 128)(S_inputs)
# 增加Position_Embedding能轻微提高准确率
embeddings = Position_Embedding()(embeddings)
O_seq = Attention(8)(embeddings)
O_seq = tf.keras.layers.GlobalAveragePooling1D()(O_seq)
O_seq = tf.keras.layers.Dropout(0.5)(O_seq)
outputs = tf.keras.layers.Dense(1, activation='sigmoid')(O_seq)
model = tf.keras.Model(inputs=S_inputs, outputs=outputs)
print(model.summary())
# try using different optimizers and different optimizer configs
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
print('Train...')
model.fit(x_train, y_train,
batch_size=batch_size,
epochs=1,
validation_data=(x_test, y_test))
score, acc = model.evaluate(x_test, y_test, batch_size=batch_size)
print('Test score:', score)
print('Test accuracy:', acc)
muti-head步骤,直白的解释就是将上面的Scaled Dot-Product Attention步骤重复执行,然后将每次执行的结果拼接起来,需要注意的是每次重复执行Scaled Dot-Product Attention步骤的参数并不共享。
#! -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function
import tensorflow as tf
import tensorflow.keras.layers as layers
import tensorflow.keras.backend as K
class Position_Embedding(layers.Layer):
def __init__(self, size=None, mode='sum', **kwargs):
self.size = size # 必须为偶数
self.mode = mode
super(Position_Embedding, self).__init__(**kwargs)
def call(self, x):
if (self.size == None) or (self.mode == 'sum'):
self.size = int(x.shape[-1])
batch_size, seq_len = K.shape(x)[0], K.shape(x)[1]
position_j = 1. / K.pow(10000., 2 * K.arange(self.size / 2, dtype='float32') / self.size)
position_j = K.expand_dims(position_j, 0)
position_i = K.cumsum(K.ones_like(x[:, :, 0]), 1) - 1 # K.arange不支持变长,只好用这种方法生成
position_i = K.expand_dims(position_i, 2)
position_ij = K.dot(position_i, position_j)
position_ij = K.concatenate([K.cos(position_ij), K.sin(position_ij)], 2)
if self.mode == 'sum':
return position_ij + x
elif self.mode == 'concat':
return K.concatenate([position_ij, x], 2)
def compute_output_shape(self, input_shape):
if self.mode == 'sum':
return input_shape
elif self.mode == 'concat':
return (input_shape[0], input_shape[1], input_shape[2] + self.size)
class Attention(layers.Layer):
def __init__(self, head_num, head_size, **kwargs):
self.head_num = head_num
self.head_size = head_size
self.output_dim = head_num * head_size
super(Attention, self).__init__(**kwargs)
def build(self, input_shape):
self.WQ = self.add_weight(name='WQ',
shape=(input_shape[-1], self.output_dim),
initializer='glorot_uniform',
trainable=True)
self.WK = self.add_weight(name='WK',
shape=(input_shape[-1], self.output_dim),
initializer='glorot_uniform',
trainable=True)
self.WV = self.add_weight(name='WV',
shape=(input_shape[-1], self.output_dim),
initializer='glorot_uniform',
trainable=True)
super(Attention, self).build(input_shape)
def call(self, x):
# 对Q、K、V做线性变换
print("\n")
print("--"*25)
Q_seq = K.dot(x, self.WQ)
Q_seq = K.reshape(Q_seq, (-1, K.shape(Q_seq)[1], self.head_num, self.head_size))
Q_seq = K.permute_dimensions(Q_seq, (0, 2, 1, 3))
print("Q_seq.shape: ", Q_seq.shape)
K_seq = K.dot(x, self.WK)
K_seq = K.reshape(K_seq, (-1, K.shape(K_seq)[1], self.head_num, self.head_size))
K_seq = K.permute_dimensions(K_seq, (0, 2, 1, 3))
print("K_seq.shape: ", K_seq.shape)
V_seq = K.dot(x, self.WV)
V_seq = K.reshape(V_seq, (-1, K.shape(V_seq)[1], self.head_num, self.head_size))
V_seq = K.permute_dimensions(V_seq, (0, 2, 1, 3))
print("V_seq.shape: ", V_seq.shape)
# 计算内积,然后softmax
QK_seq = tf.matmul(Q_seq, K.permute_dimensions(K_seq, (0, 1, 3, 2))) / self.head_size ** 0.5
QK_seq = K.softmax(QK_seq)
print("QK_seq.shape: ", QK_seq.shape)
Z_seq = tf.matmul(QK_seq, V_seq)
Z_seq = K.permute_dimensions(Z_seq, (0, 2, 1, 3))
Z_seq = K.reshape(Z_seq, (-1, K.shape(Z_seq)[1], self.output_dim))
print("Z_seq.shape: ", Z_seq.shape)
print("-="*25)
return Z_seq
def compute_output_shape(self, input_shape):
return (input_shape[0], input_shape[0], self.output_dim)
imdb测试代码里修改成
O_seq = Attention(2, 8)(embeddings)
四 Attention任务
下面介绍我了解到的一些task,其中机器翻译、摘要生成、图文互搜属于seq2seq任务,需要对两段内容进行对齐,文本蕴含用到前提和假设两段文本,阅读理解也用到了文章和问题两段文本,文本分类、序列标注和关系抽取属于单文本Attention的做法。
1)机器翻译:encoder用于对原文建模,decoder用于生成译文,attention用于连接原文和译文,在每一步翻译的时候关注不同的原文信息。
2)摘要生成:encoder用于对原文建模,decoder用于生成新文本,从形式上和机器翻译都是seq2seq任务,但是从任务特点上看,机器翻译可以具体对齐到某几个词,但这里是由长文本生成短文本,decoder可能需要capture到encoder更多的内容,进行总结。
3)图文互搜:encoder对图片建模,decoder生成相关文本,在decoder生成每个词的时候,用attention机制来关注图片的不同部分。
4)文本蕴含:判断前提和假设是否相关,attention机制
Attention机制只是一种思想,可以用到很多任务上,Attention机制比较适合有以下特点的任务:
1)长文本任务,document级别,因为长文本本身所携带的信息量比较大,可能会带来信息过载问题,很多任务可能只需要用到其中一些关键信息(比如文本分类),所以Attention机制用在这里正适合capture这些关键信息。
2)涉及到两段的相关文本,可能会需要对两段内容进行对齐,找到这两段文本之间的一些相关关系。比如机器翻译,将英文翻译成中文,英文和中文明显是有对齐关系的,Attention机制可以找出,在翻译到某个中文字的时候,需要对齐到哪个英文单词。又比如阅读理解,给出问题和文章,其实问题中也可以对齐到文章相关的描述,比如“什么时候”可以对齐到文章中相关的时间部分。
3)任务很大部分取决于某些特征。我举个例子,比如在AI+法律领域,根据初步判决文书来预测所触犯的法律条款,在文书中可能会有一些罪名判定,而这种特征对任务是非常重要的,所以用Attention来capture到这种特征就比较有用。(CNN也可以)
用来对前提和假设进行对齐。
5)阅读理解:可以对文本进行self attention,也可以对文章和问题进行对齐。
6)文本分类:一般是对一段句子进行attention,得到一个句向量去做分类。
7)序列标注:Deep Semantic Role Labeling with Self-Attention,这篇论文在softmax前用到了self attention,学习句子结构信息,和利用到标签依赖关系的CRF进行pk。
8)关系抽取:也可以用到self attention
总结
-
总的来说,attention的机制就是一个加权求和的机制,只要我们使用了加权求和,不管你是怎么花式加权,花式求和,只要你是根据了已有信息计算的隐藏状态的加权和求和,那么就是使用了attention,而所谓的self attention就是仅仅在句子内部做加权求和(区别与seq2seq里面的decoder对encoder的隐藏状态做的加权求和)。
-
self attention我个人认为作用范围更大一点,而key-value其实是对attention进行了一个更广泛的定义罢了,我们前面的attention都可以套上key-value attention,比如很多时候我们是把k和v都当成一样的算来,做self的时候还可能是quey=key=value。
BERT模型入门系列(三):Self-Attention详解 - 掘金
Attention机制详解(二)——Self-Attention与Transformer - 知乎
NLP-Word Embedding-Attention机制 - 简书
https://towardsdatascience.com/illustrated-self-attention-2d627e33b20a