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在同一个英语句子内单词间产生的联系。
图1 可视化Self Attention实例
图2 可视化Self Attention实例
从两张图可以看出,Self Attention可以捕获同一个句子中单词之间的一些句法特征(比如图1展示的有一定距离的短语结构)或者语义特征(比如图2展示的its的指代对象Law)。
很明显,引入Self Attention后会更容易捕获句子中长距离的相互依赖的特征,因为如果是RNN或者LSTM,需要依次序序列计算,对于远距离的相互依赖的特征,要经过若干时间步步骤的信息累积才能将两者联系起来,而距离越远,有效捕获的可能性越小。
但是Self Attention在计算过程中会直接将句子中任意两个单词的联系通过一个计算步骤直接联系起来,所以远距离依赖特征之间的距离被极大缩短,有利于有效地利用这些特征。除此外,Self Attention对于增加计算的并行性也有直接帮助作用。这是为何Self Attention逐渐被广泛使用的主要原因。
如果把Attention机制从Encoder-Decoder框架中剥离,并进一步做抽象,可以更容易看懂Attention机制的本质思想。
Attention机制的本质思想
我们可以这样来看待Attention机制:将Source中的构成元素想象成是由一系列的
其中,=||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计算权重系数,第一个过程细分为两个阶段:
二个过程根据权重系数对Value进行加权求和。
这样,可以将Attention的计算过程抽象为如图10展示的三个阶段。
图10 三阶段计算Attention过程
在第一个阶段,可以引入不同的函数和计算机制,根据Query和某个,计算两者的相似性或者相关性,最常见的方法包括:求两者的向量点积、求两者的向量Cosine相似性或者通过再引入额外的神经网络来求值,即如下方式:
第一阶段产生的分值根据具体产生的方法不同其数值取值范围也不一样,第二阶段引入类似SoftMax的计算方式对第一阶段的得分进行数值转换,一方面可以进行归一化,将原始计算分值整理成所有元素权重之和为1的概率分布;另一方面也可以通过SoftMax的内在机制更加突出重要元素的权重。即一般采用如下公式计算:
第二阶段的计算结果即为对应的权重系数,然后进行加权求和即可得到Attention数值:
通过如上三个阶段的计算,即可求出针对Query的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是我们模型训练过程学习到的合适的参数。上述操作即可简化为矩阵形式
同样,可以多叠加几层self-attention,用同样的操作不同的QKV矩阵由C变成CC,变成CCC,这就是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)
For this tutorial, we start with 3 inputs, each with dimension 4.
Input 1: [1, 0, 1, 0]
Input 2: [0, 2, 0, 2]
Input 3: [1, 1, 1, 1]
Every input must have three representations (see diagram below). These representations are called key (orange), query (red), and value (purple). For this example, let’s take that we want these representations to have a dimension of 3. Because every input has a dimension of 4, this means each set of the weights must have a shape of 4×3.
(the dimension of value is also the dimension of the output.)
In order to obtain these representations, every input (green) is multiplied with a set of weights for keys, a set of weights for querys (I know that’s not the right spelling), and a set of weights for values. In our example, we ‘initialise’ the three sets of weights as follows.
Weights for key:
[[0, 0, 1],
[1, 1, 0],
[0, 1, 0],
[1, 1, 0]]
Weights for query:
[[1, 0, 1],
[1, 0, 0],
[0, 0, 1],
[0, 1, 1]]
Weights for value:
[[0, 2, 0],
[0, 3, 0],
[1, 0, 3],
[1, 1, 0]]
PS: In a neural network setting, these weights are usually small numbers, initialised randomly using an appropriate random distribution like Gaussian, Xavier and Kaiming distributions.
Now that we have the three sets of weights, let’s actually obtain the key, query and value representations for every input.
Key representation for Input 1:
[0, 0, 1]
[1, 0, 1, 0] x [1, 1, 0] = [0, 1, 1]
[0, 1, 0]
[1, 1, 0]
Use the same set of weights to get the key representation for Input 2:
[0, 0, 1]
[0, 2, 0, 2] x [1, 1, 0] = [4, 4, 0]
[0, 1, 0]
[1, 1, 0]
Use the same set of weights to get the key representation for Input 3:
[0, 0, 1]
[1, 1, 1, 1] x [1, 1, 0] = [2, 3, 1]
[0, 1, 0]
[1, 1, 0]
1. A faster way is to vectorise the above key operations:
[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]
2. Let’s do the same to obtain the value representations for every input:
[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]
3. finally the query representations:
[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]
PS: In practice, a bias vector may be added to the product of matrix multiplication.
To obtain attention scores, we start off with taking a dot product between Input 1’s query (red) with all keys (orange), including itself. Since there are 3 key representations (because we have 3 inputs), we obtain 3 attention scores (blue).
[0, 4, 2]
[1, 0, 2] x [1, 4, 3] = [2, 4, 4]
[1, 0, 1]
we only use the query from Input 1. Later we’ll work on repeating this same step for the other querys.
PS: The above operation is known as dot product attention, one of the several score functions. Other score functions include scaled dot product and additive/concat.
Take the softmax across these attention scores (blue).
softmax([2, 4, 4]) = [0.0, 0.5, 0.5]
The softmaxed attention scores for each input (blue) is multiplied with its corresponding value (purple). This results in 3 alignment vectors (yellow). In this tutorial, we’ll refer to them as weighted values.
1: 0.0 * [1, 2, 3] = [0.0, 0.0, 0.0]
2: 0.5 * [2, 8, 0] = [1.0, 4.0, 0.0]
3: 0.5 * [2, 6, 3] = [1.0, 3.0, 1.5]
Take all the weighted values (yellow) and sum them element-wise:
[0.0, 0.0, 0.0]
+ [1.0, 4.0, 0.0]
+ [1.0, 3.0, 1.5]
-----------------
= [2.0, 7.0, 1.5]
The resulting vector [2.0, 7.0, 1.5] (dark green) is Output 1, which is based on the query representation from Input 1 interacting with all other keys, including itself.
Query 与 Key 的纬度一定要相同,因为两者需要进行点积相乘, 然而, Value的纬度可以与Q, K的纬度不一样
The resulting output will consequently follow the dimension of value.
import tensorflow as tf
x = [
[1, 0, 1, 0], # Input 1
[0, 2, 0, 2], # Input 2
[1, 1, 1, 1] # Input 3
]
x = tf.Variable(x, dtype=tf.float32)
w_key = [
[0, 0, 1],
[1, 1, 0],
[0, 1, 0],
[1, 1, 0]
]
w_query = [
[1, 0, 1],
[1, 0, 0],
[0, 0, 1],
[0, 1, 1]
]
w_value = [
[0, 2, 0],
[0, 3, 0],
[1, 0, 3],
[1, 1, 0]
]
w_key = tf.Variable(w_key, dtype=tf.float32)
w_query = tf.Variable(w_query, dtype=tf.float32)
w_value = tf.Variable(w_value, dtype=tf.float32)
keys = x @ w_key
querys = x @ w_query
values = x @ w_value
print(keys)
# tensor([[0., 1., 1.],
# [4., 4., 0.],
# [2., 3., 1.]])
print(querys)
# tensor([[1., 0., 2.],
# [2., 2., 2.],
# [2., 1., 3.]])
print(values)
# tensor([[1., 2., 3.],
# [2., 8., 0.],
# [2., 6., 3.]])
attn_scores = querys @ tf.transpose(keys, perm=[1, 0]) # [[1, 4]
print(attn_scores)
# tensor([[ 2., 4., 4.], # attention scores from Query 1
# [ 4., 16., 12.], # attention scores from Query 2
# [ 4., 12., 10.]]) # attention scores from Query 3
attn_scores_softmax = tf.nn.softmax(attn_scores)
print(attn_scores_softmax)
# tensor([[6.3379e-02, 4.6831e-01, 4.6831e-01],
# [6.0337e-06, 9.8201e-01, 1.7986e-02],
# [2.9539e-04, 8.8054e-01, 1.1917e-01]])
# For readability, approximate the above as follows
attn_scores_softmax = [
[0.0, 0.5, 0.5],
[0.0, 1.0, 0.0],
[0.0, 0.9, 0.1]
]
attn_scores_softmax = tf.Variable(attn_scores_softmax)
print(attn_scores_softmax)
print(attn_scores_softmax)
print(values)
outputs = tf.matmul(attn_scores_softmax, values)
print(outputs)
tf.Tensor(
[[1. 2. 3.]
[2. 8. 0.]
[2. 6. 3.]], shape=(3, 3), dtype=float32)
tf.Tensor(
[[2. 7. 1.5 ]
[2. 8. 0. ]
[2. 7.7999997 0.3 ]], shape=(3, 3), dtype=float32)
weighted_values = values[:,None] * tf.transpose(attn_scores_softmax, perm=[1, 0])[:,:,None]
print(weighted_values)
# tensor([[[0.0000, 0.0000, 0.0000],
# [0.0000, 0.0000, 0.0000],
# [0.0000, 0.0000, 0.0000]],
#
# [[1.0000, 4.0000, 0.0000],
# [2.0000, 8.0000, 0.0000],
# [1.8000, 7.2000, 0.0000]],
#
# [[1.0000, 3.0000, 1.5000],
# [0.0000, 0.0000, 0.0000],
# [0.2000, 0.6000, 0.3000]]])
outputs = tf.reduce_sum(weighted_values, axis=0)
print(outputs)
# tensor([[2.0000, 7.0000, 1.5000], # Output 1
# [2.0000, 8.0000, 0.0000], # Output 2
# [2.0000, 7.8000, 0.3000]]) # Output 3
Attention机制只是一种思想,可以用到很多任务上,Attention机制比较适合有以下特点的任务:
1)长文本任务,document级别,因为长文本本身所携带的信息量比较大,可能会带来信息过载问题,很多任务可能只需要用到其中一些关键信息(比如文本分类),所以Attention机制用在这里正适合capture这些关键信息。
2)涉及到两段的相关文本,可能会需要对两段内容进行对齐,找到这两段文本之间的一些相关关系。比如机器翻译,将英文翻译成中文,英文和中文明显是有对齐关系的,Attention机制可以找出,在翻译到某个中文字的时候,需要对齐到哪个英文单词。又比如阅读理解,给出问题和文章,其实问题中也可以对齐到文章相关的描述,比如“什么时候”可以对齐到文章中相关的时间部分。
3)任务很大部分取决于某些特征。我举个例子,比如在AI+法律领域,根据初步判决文书来预测所触犯的法律条款,在文书中可能会有一些罪名判定,而这种特征对任务是非常重要的,所以用Attention来capture到这种特征就比较有用。(CNN也可以)
下面介绍我了解到的一些task,其中机器翻译、摘要生成、图文互搜属于seq2seq任务,需要对两段内容进行对齐,文本蕴含用到前提和假设两段文本,阅读理解也用到了文章和问题两段文本,文本分类、序列标注和关系抽取属于单文本Attention的做法。
1)机器翻译:encoder用于对原文建模,decoder用于生成译文,attention用于连接原文和译文,在每一步翻译的时候关注不同的原文信息。
2)摘要生成:encoder用于对原文建模,decoder用于生成新文本,从形式上和机器翻译都是seq2seq任务,但是从任务特点上看,机器翻译可以具体对齐到某几个词,但这里是由长文本生成短文本,decoder可能需要capture到encoder更多的内容,进行总结。
3)图文互搜:encoder对图片建模,decoder生成相关文本,在decoder生成每个词的时候,用attention机制来关注图片的不同部分。
4)文本蕴含:判断前提和假设是否相关,attention机制用来对前提和假设进行对齐。
5)阅读理解:可以对文本进行self attention,也可以对文章和问题进行对齐。
6)文本分类:一般是对一段句子进行attention,得到一个句向量去做分类。
7)序列标注:Deep Semantic Role Labeling with Self-Attention,这篇论文在softmax前用到了self attention,学习句子结构信息,和利用到标签依赖关系的CRF进行pk。
8)关系抽取:也可以用到self attention
https://zhuanlan.zhihu.com/p/47282410
https://www.jianshu.com/p/27514668a1a3
https://www.jianshu.com/p/c3e1a9c04204
https://towardsdatascience.com/illustrated-self-attention-2d627e33b20a