1注意力机制介绍
1.1 注意力概念
- 我们观察事物时,之所以能够快速判断一种事物(当然允许判断是错误的), 是因为我们大脑能够很快把注意力放在事物最具有辨识度的部分从而作出判断,而并非是从头到尾的观察一遍事物后,才能有判断结果. 正是基于这样的理论,就产生了注意力机制.
1.2 注意力计算规则
-
它需要三个指定的输入Q(query), K(key), V(value), 然后通过计算公式得到注意力的结果, 这个结果代表query在key和value作用下的注意力表示. 当输入的Q=K=V时, 称作自注意力计算规则.
-
Q, K, V的比喻解释:
假如我们有一个问题: 给出一段文本,使用一些关键词对它进行描述! 为了方便统一正确答案,这道题可能预先已经给大家写出了一些关键词作为提示.其中这些给出的提示就可以看作是key, 而整个的文本信息就相当于是query,value的含义则更抽象,可以比作是你看到这段文本信息后,脑子里浮现的答案信息, 这里我们又假设大家最开始都不是很聪明,第一次看到这段文本后脑子里基本上浮现的信息就只有提示这些信息, 因此key与value基本是相同的,但是随着我们对这个问题的深入理解,通过我们的思考脑子里想起来的东西原来越多, 并且能够开始对我们query也就是这段文本,提取关键信息进行表示. 这就是注意力作用的过程, 通过这个过程, 我们最终脑子里的value发生了变化, 根据提示key生成了query的关键词表示方法,也就是另外一种特征表示方法. 刚刚我们说到key和value一般情况下默认是相同,与query是不同的,这种是我们一般的注意力输入形式, 但有一种特殊情况,就是我们query与key和value相同,这种情况我们称为自注意力机制,就如同我们的刚刚的例子, 使用一般注意力机制,是使用不同于给定文本的关键词表示它. 而自注意力机制, 需要用给定文本自身来表达自己,也就是说你需要从给定文本中抽取关键词来表述它, 相当于对文本自身的一次特征提取.
1.3 常见的注意力计算规则
-
将Q,K进行纵轴拼接, 做一次线性变化, 再使用softmax处理获得结果最后与V做张量乘法.
-
𝐴𝑡𝑡𝑒𝑛𝑡𝑖𝑜𝑛(𝑄,𝐾,𝑉)=𝑆𝑜𝑓𝑡𝑚𝑎𝑥(𝐿𝑖𝑛𝑒𝑎𝑟([𝑄,𝐾]))⋅𝑉
-
将Q,K进行纵轴拼接, 做一次线性变化后再使用tanh函数激活, 然后再进行内部求和, 最后使用softmax处理获得结果再与V做张量乘法.
-
𝐴𝑡𝑡𝑒𝑛𝑡𝑖𝑜𝑛(𝑄,𝐾,𝑉)=𝑆𝑜𝑓𝑡𝑚𝑎𝑥(𝑠𝑢𝑚(𝑡𝑎𝑛ℎ(𝐿𝑖𝑛𝑒𝑎𝑟([𝑄,𝐾]))))⋅𝑉
-
将Q与K的转置做点积运算, 然后除以一个缩放系数, 再使用softmax处理获得结果最后与V做张量乘法.
-
说明:当注意力权重矩阵和V都是三维张量且第一维代表为batch条数时, 则做bmm运算.bmm是一种特殊的张量乘法运算.
-
bmm运算演示:
如果参数1形状是(b × n × m), 参数2形状是(b × m × p), 则输出为(b × n × p)
input = torch.randn(10, 3, 4)
mat2 = torch.randn(10, 4, 5)
res = torch.bmm(input, mat2)
res.size()
torch.Size([10, 3, 5])
2 什么是注意力机制
- 注意力机制是注意力计算规则能够应用的深度学习网络的载体, 同时包括一些必要的全连接层以及相关张量处理, 使其与应用网络融为一体. 使用自注意力计算规则的注意力机制称为自注意力机制.
- 说明: NLP领域中, 当前的注意力机制大多数应用于seq2seq架构, 即编码器和解码器模型.
3 注意力机制的作用
- 在解码器端的注意力机制: 能够根据模型目标有效的聚焦编码器的输出结果, 当其作为解码器的输入时提升效果. 改善以往编码器输出是单一定长张量, 无法存储过多信息的情况.
- 在编码器端的注意力机制: 主要解决表征问题, 相当于特征提取过程, 得到输入的注意力表示. 一般使用自注意力(self-attention).
注意力机制在网络中实现的图形表示:
4 注意力机制实现步骤
4.1 步骤
- 第一步: 根据注意力计算规则, 对Q,K,V进行相应的计算.
- 第二步: 根据第一步采用的计算方法, 如果是拼接方法,则需要将Q与第二步的计算结果再进行拼接, 如果是转置点积, 一般是自注意力, Q与V相同, 则不需要进行与Q的拼接.
- 第三步: 最后为了使整个attention机制按照指定尺寸输出, 使用线性层作用在第二步的结果上做一个线性变换, 得到最终对Q的注意力表示.
4.2 代码实现
-
常见注意力机制的代码分析:
import torch import torch.nn as nn import torch.nn.functional as F class Attn(nn.Module): def __init__(self, query_size, key_size, value_size1, value_size2, output_size): """初始化函数中的参数有5个, query_size代表query的最后一维大小 key_size代表key的最后一维大小, value_size1代表value的导数第二维大小, value = (1, value_size1, value_size2) value_size2代表value的倒数第一维大小, output_size输出的最后一维大小""" super(Attn, self).__init__() # 将以下参数传入类中 self.query_size = query_size self.key_size = key_size self.value_size1 = value_size1 self.value_size2 = value_size2 self.output_size = output_size # 初始化注意力机制实现第一步中需要的线性层. self.attn = nn.Linear(self.query_size + self.key_size, value_size1) # 初始化注意力机制实现第三步中需要的线性层. self.attn_combine = nn.Linear(self.query_size + value_size2, output_size) def forward(self, Q, K, V): """forward函数的输入参数有三个, 分别是Q, K, V, 根据模型训练常识, 输入给Attion机制的 张量一般情况都是三维张量, 因此这里也假设Q, K, V都是三维张量""" # 第一步, 按照计算规则进行计算, # 我们采用常见的第一种计算规则 # 将Q,K进行纵轴拼接, 做一次线性变化, 最后使用softmax处理获得结果 attn_weights = F.softmax( self.attn(torch.cat((Q[0], K[0]), 1)), dim=1) # 然后进行第一步的后半部分, 将得到的权重矩阵与V做矩阵乘法计算, # 当二者都是三维张量且第一维代表为batch条数时, 则做bmm运算 attn_applied = torch.bmm(attn_weights.unsqueeze(0), V) # 之后进行第二步, 通过取[0]是用来降维, 根据第一步采用的计算方法, # 需要将Q与第一步的计算结果再进行拼接 output = torch.cat((Q[0], attn_applied[0]), 1) # 最后是第三步, 使用线性层作用在第三步的结果上做一个线性变换并扩展维度,得到输出 # 因为要保证输出也是三维张量, 因此使用unsqueeze(0)扩展维度 output = self.attn_combine(output).unsqueeze(0) return output, attn_weights
调用:
query_size = 32 key_size = 32 value_size1 = 32 value_size2 = 64 output_size = 64 attn = Attn(query_size, key_size, value_size1, value_size2, output_size) Q = torch.randn(1,1,32) K = torch.randn(1,1,32) V = torch.randn(1,32,64) out = attn(Q, K ,V) print(out[0]) print(out[1])
输出结果:
tensor([[[ 0.4477, -0.0500, -0.2277, -0.3168, -0.4096, -0.5982, 0.1548, -0.0771, -0.0951, 0.1833, 0.3128, 0.1260, 0.4420, 0.0495, -0.7774, -0.0995, 0.2629, 0.4957, 1.0922, 0.1428, 0.3024, -0.2646, -0.0265, 0.0632, 0.3951, 0.1583, 0.1130, 0.5500, -0.1887, -0.2816, -0.3800, -0.5741, 0.1342, 0.0244, -0.2217, 0.1544, 0.1865, -0.2019, 0.4090, -0.4762, 0.3677, -0.2553, -0.5199, 0.2290, -0.4407, 0.0663, -0.0182, -0.2168, 0.0913, -0.2340, 0.1924, -0.3687, 0.1508, 0.3618, -0.0113, 0.2864, -0.1929, -0.6821, 0.0951, 0.1335, 0.3560, -0.3215, 0.6461, 0.1532]]], grad_fn=<UnsqueezeBackward0>) tensor([[0.0395, 0.0342, 0.0200, 0.0471, 0.0177, 0.0209, 0.0244, 0.0465, 0.0346, 0.0378, 0.0282, 0.0214, 0.0135, 0.0419, 0.0926, 0.0123, 0.0177, 0.0187, 0.0166, 0.0225, 0.0234, 0.0284, 0.0151, 0.0239, 0.0132, 0.0439, 0.0507, 0.0419, 0.0352, 0.0392, 0.0546, 0.0224]], grad_fn=<SoftmaxBackward>)
5注意力机制原理
5.1 注意力机制示意图
Attention机制的工作原理并不复杂,我们可以用下面这张图做一个总结
5.2 Attention计算过程
- 阶段一: query 和 key 进行相似度计算,得到一个query 和 key 相关性的分值
- 阶段二: 将这个分值进行归一化(softmax),得到一个注意力的分布
- 阶段三: 使用注意力分布和 value 进行计算,得到一个融合注意力的更好的 value 值
为了更好的说明上面的情况, 我们通过注意力来做一个机器翻译(NMT) 的任务,机器翻译中,我们会使用 seq2seq 的架构,每个时间步从词典里生成一个翻译的结果。就像下面这张图一样.
在没有注意力之前,我们每次都是根据 Encoder 部分的输出结果来进行生成,提出注意力后,就是想在生成翻译结果时并不是看 Encoder 中所有的输出结果,而是先来看看想生成的这部分和哪些单词可能关系会比较大,关系大的我多借鉴些;关系小的,少借鉴些。就是这样一个想法,我们看看该如何操作。
- 这里为了生成单词,我们把 Decoder 部分输入后得到的向量作为 query;把 Encoder 部分每个单词的向量作为 key。首先我们先把 query 和 每一个单词进行点乘𝑠𝑐𝑜𝑟𝑒=𝑞𝑢𝑒𝑟𝑦⋅𝑘𝑒𝑦,得到相关性的分值;
- 有了这些分值后,我们对这些分值做一个𝑠𝑜𝑓𝑡𝑚𝑎𝑥 ,得到一个注意力的分布
- 有了这个注意力,我们就可以用它和 Encoder 的输出值 (value) 进行相乘,得到一个加权求和后的值,这个值就包含注意力的表示,我们用它来预测要生成的词。
5.3 Attention计算逻辑
当然,Attention 并不是只有这一种计算方式,后来还有很多人找到了各种各样的计算注意力的方法, 比如我们上面介绍的三种计算规则, 但是从本质上,它们都遵循着这个三步走的逻辑:
- query 和 key 进行相似度计算,得到一个query 和 key 相关性的分值
- 将这个分值进行归一化(softmax),得到一个注意力的分布
- 使用注意力分布和 value 进行计算,得到一个融合注意力的更好的 value 值
5.4 有无attention模型对比
1 无attention机制的模型
-
文本处理领域的Encoder-Decoder框架可以这么直观地去理解:可以把它看作适合处理由一个句子(或篇章)生成另外一个句子(或篇章)的通用处理模型。对于句子对,我们的目标是给定输入句子Source,期待通过Encoder-Decoder框架来生成目标句子Target。Source和Target可以是同一种语言,也可以是两种不同的语言。而Source和Target分别由各自的单词序列构成:𝑆𝑜𝑢𝑟𝑐𝑒=〈𝑋1,𝑋2⋯𝑋𝑚〉,𝑇𝑎𝑟𝑔𝑒𝑡=〈𝑦1,𝑦2⋯𝑦𝑛〉
-
encoder顾名思义就是对输入句子Source进行编码,将输入句子通过非线性变换转化为中间语义表示C:𝐶=𝐹(𝑋1,𝑋2⋯𝑋𝑚)
-
对于解码器Decoder来说,其任务是根据句子Source的中间语义表示C和之前已经生成的历史信息,y_1, y_2...y_i-1来生成i时刻要生成的单词y_i,𝑦𝑖=𝐺(𝐶,𝑦1,𝑦2⋯𝑦𝑖−1)
-
上述图中展示的Encoder-Decoder框架是没有体现出"注意力模型"的,所以可以把它看作是注意力不集中的分心模型。为什么说它注意力不集中呢?请观察下目标句子Target中每个单词的生成过程如下:𝑦1=𝑓(𝐶),𝑦2=𝑓(𝐶,𝑦1),𝑦3=𝑓(𝐶,𝑦1,𝑦2)
-
其中f是Decoder的非线性变换函数。从这里可以看出,在生成目标句子的单词时,不论生成哪个单词,它们使用的输入句子Source的语义编码C都是一样的,没有任何区别。
-
每个yi都依次这么产生,那么看起来就是整个系统根据输入句子Source生成了目标句子Target。如果Source是中文句子,Target是英文句子,那么这就是解决机器翻译问题的Encoder-Decoder框架;如果Source是一篇文章,Target是概括性的几句描述语句,那么这是文本摘要的Encoder-Decoder框架;如果Source是一句问句,Target是一句回答,那么这是问答系统或者对话机器人的Encoder-Decoder框架。由此可见,在文本处理领域,Encoder-Decoder的应用领域相当广泛。
-
问题点是: 语义编码C是由句子Source的每个单词经过Encoder 编码产生的,这意味着不论是生成哪个单词,还是,其实句子Source中任意单词对生成某个目标单词yi来说影响力都是相同的,这是为何说这个模型没有体现出注意力的缘由。这类似于人类看到眼前的画面,但是眼中却没有注意焦点一样.
2 有attention机制的模型
-
如果拿机器翻译来解释这个分心模型的Encoder-Decoder框架更好理解,比如输入的是英文句子:Tom chase Jerry,Encoder-Decoder框架逐步生成中文单词:"汤姆","追逐","杰瑞"。在翻译"杰瑞"这个中文单词的时候,分心模型里面的每个英文单词对于翻译目标单词"杰瑞"贡献是相同的,很明显这里不太合理,显然"Jerry"对于翻译成"杰瑞"更重要,但是分心模型是无法体现这一点的,这就是为何说它没有引入注意力的原因。
-
没有引入注意力的模型在输入句子比较短的时候问题不大,但是如果输入句子比较长,此时所有语义完全通过一个中间语义向量来表示,单词自身的信息已经消失,可想而知会丢失很多细节信息,这也是为何要引入注意力模型的重要原因。
-
上面的例子中,如果引入Attention模型的话,应该在翻译"杰瑞"的时候,体现出英文单词对于翻译当前中文单词不同的影响程度,比如给出类似下面一个概率分布值:(Tom,0.3)(Chase,0.2) (Jerry,0.5).每个英文单词的概率代表了翻译当前单词"杰瑞"时,注意力分配模型分配给不同英文单词的注意力大小。这对于正确翻译目标语单词肯定是有帮助的,因为引入了新的信息。
-
同理,目标句子中的每个单词都应该学会其对应的源语句子中单词的注意力分配概率信息。这意味着在生成每个单词的时候,原先都是相同的中间语义表示C会被替换成根据当前生成单词而不断变化的。理解Attention模型的关键就是这里,即由固定的中间语义表示C换成了根据当前输出单词来调整成加入注意力模型的变化的。增加了注意力模型的Encoder-Decoder框架理解起来如下图所示:
-
即生成目标句子单词的过程成了下面的形式:𝑦1=𝑓1(𝐶1),𝑦2=𝑓1(𝐶2,𝑦1),𝑦3=𝑓1(𝐶3,𝑦1,𝑦2)
-
而每个Ci可能对应着不同的源语句子单词的注意力分配概率分布,比如对于上面的英汉翻译来说,其对应的信息可能如下:𝐶𝑇𝑜𝑚=𝑔(0.6∗𝑓2(𝑇𝑜𝑚),0.2∗𝑓2(𝐶ℎ𝑎𝑠𝑒),0.2∗𝑓2(𝐽𝑒𝑟𝑟𝑦)),𝐶𝐶ℎ𝑎𝑠𝑒=𝑔(0.2∗𝑓2(𝑇𝑜𝑚),0.7∗𝑓2(𝐶ℎ𝑎𝑠𝑒),0.1∗𝑓2(𝐽𝑒𝑟𝑟𝑦)),𝐶𝐽𝑒𝑟𝑟𝑦=𝑔(0.3∗𝑓2(𝑇𝑜𝑚),0.2∗𝑓2(𝐶ℎ𝑎𝑠𝑒),0.5∗𝑓2(𝐽𝑒𝑟𝑟𝑦))
-
f2函数代表Encoder对输入英文单词的某种变换函数,比如如果Encoder是用的RNN模型的话,这个f2函数的结果往往是某个时刻输入后隐层节点的状态值;g代表Encoder根据单词的中间表示合成整个句子中间语义表示的变换函数,一般的做法中,g函数就是对构成元素加权求和,即下列公式
- Lx代表输入句子source的长度, a_ij代表在Target输出第i个单词时source输入句子中的第j个单词的注意力分配系数, 而hj则是source输入句子中第j个单词的语义编码, 假设Ci下标i就是上面例子所说的'汤姆', 那么Lx就是3, h1=f('Tom'), h2=f('Chase'),h3=f('jerry')分别输入句子每个单词的语义编码, 对应的注意力模型权值则分别是0.6, 0.2, 0.2, 所以g函数本质上就是加权求和函数, 如果形象表示的话, 翻译中文单词'汤姆'的时候, 数学公式对应的中间语义表示Ci的形成过程类似下图: