目录
[🍔 注意力机制原理](#🍔 注意力机制原理)
[1.1 注意力机制示意图](#1.1 注意力机制示意图)
[1.2 Attention计算过程](#1.2 Attention计算过程)
[1.3 Attention计算逻辑](#1.3 Attention计算逻辑)
[1.4 有无Attention模型对比](#1.4 有无Attention模型对比)
[1.4.1 无Attention机制的模型](#1.4.1 无Attention机制的模型)
[1.4.2 有Attention机制的模型](#1.4.2 有Attention机制的模型)
[🍔 Self-attention演变过程](#🍔 Self-attention演变过程)
[2.1 Self-attention介绍](#2.1 Self-attention介绍)
[2.2 Self-attention和Attention使用方法](#2.2 Self-attention和Attention使用方法)
[2.3 Self attention机制的代码实现](#2.3 Self attention机制的代码实现)
[2.3.1 Self-attetion实现步骤](#2.3.1 Self-attetion实现步骤)
[2.3.2 代码解析](#2.3.2 代码解析)
学习目标
🍀 知道注意力机制的工作流程
🍀 理解三个阶段过程
🍀 理解attention和self-attention之间的不同点
🍀 理解attention和self-attention的应用场景
🍔 注意力机制原理
1.1 注意力机制示意图
Attention机制的工作原理并不复杂,我们可以用下面这张图做一个总结
1.2 Attention计算过程
- 阶段一: query 和 key 进行相似度计算,得到一个query 和 key 相关性的分值
- 阶段二: 将这个分值进行归一化(softmax),得到一个注意力的分布
- 阶段三: 使用注意力分布和 value 进行计算,得到一个融合注意力的更好的 value 值
为了更好的说明上面的情况, 我们通过注意力来做一个机器翻译(NMT) 的任务,机器翻译中,我们会使用 seq2seq 的架构,每个时间步从词典里生成一个翻译的结果。就像下面这张图一样.
在没有注意力之前,我们每次都是根据 Encoder 部分的输出结果来进行生成,提出注意力后,就是想在生成翻译结果时并不是看 Encoder 中所有的输出结果,而是先来看看想生成的这部分和哪些单词可能关系会比较大,关系大的我多借鉴些;关系小的,少借鉴些。就是这样一个想法,我们看看该如何操作。
- 这里为了生成单词,我们把 Decoder 部分输入后得到的向量作为 query;把 Encoder 部分每个单词的向量作为 key。首先我们先把 query 和 每一个单词进行点乘score=query⋅key,得到相关性的分值。
- 有了这些分值后,我们对这些分值做一个softmax ,得到一个注意力的分布。
- 有了这个注意力,我们就可以用它和 Encoder 的输出值 (value) 进行相乘,得到一个加权求和后的值,这个值就包含注意力的表示,我们用它来预测要生成的词。
这个过程我们可以看看一个动图的事例理解一下:
动图演示
1.3 Attention计算逻辑
当然,Attention 并不是只有这一种计算方式,后来还有很多人找到了各种各样的计算注意力的方法, 比如我们上面介绍的三种计算规则, 但是从本质上,它们都遵循着这个三步走的逻辑:
- query 和 key 进行相似度计算,得到一个query 和 key 相关性的分值
- 将这个分值进行归一化(softmax),得到一个注意力的分布
- 使用注意力分布和 value 进行计算,得到一个融合注意力的更好的 value 值
1.4 有无Attention模型对比
1.4.1 无Attention机制的模型
- 文本处理领域的Encoder-Decoder框架可以这么直观地去理解:可以把它看作适合处理由一个句子(或篇章)生成另外一个句子(或篇章)的通用处理模型。对于句子对,我们的目标是给定输入句子Source,期待通过Encoder-Decoder框架来生成目标句子Target。Source和Target可以是同一种语言,也可以是两种不同的语言。而Source和Target分别由各自的单词序列构成:
- encoder顾名思义就是对输入句子Source进行编码,将输入句子通过非线性变换转化为中间语义表示C:
- 对于解码器Decoder来说,其任务是根据句子Source的中间语义表示C和之前已经生成的历史信息,y_1, y_2...y_i-1来生成i时刻要生成的单词y_i
- 上述图中展示的Encoder-Decoder框架是没有体现出"注意力模型"的,所以可以把它看作是注意力不集中的分心模型。为什么说它注意力不集中呢?请观察下目标句子Target中每个单词的生成过程如下:
-
其中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来说影响力都是相同的,这是为何说这个模型没有体现出注意力的缘由。这类似于人类看到眼前的画面,但是眼中却没有注意焦点一样.
1.4.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框架理解起来如下图所示:
- 即生成目标句子单词的过程成了下面的形式:
- 而每个Ci可能对应着不同的源语句子单词的注意力分配概率分布,比如对于上面的英汉翻译来说,其对应的信息可能如下:
- 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的形成过程类似下图:
🍔 Self-attention演变过程
2.1 Self-attention介绍
Self-attention就本质上是一种特殊的attention。这种应用在transformer中最重要的结构之一。前面我们介绍了attention机制,它能够帮我们找到子序列和全局的attention的关系,也就是找到权重值wi。Self-attention向对于attention的变化,其实就是寻找权重值的wi过程不同。下面我们来看看self-attention的运算过程。
- 为了能够产生输出的向量yi ,self-attention其实是对所有的输入做了一个加权平均的操作,这个公式和上面的attention是一致的。
- j代表整个序列的长度,并且j个权重的相加之和等于1。值得一提的是,这里的 wij并不是一个需要神经网络学习的参数,它是来源于xi和xj的之间的计算的结果(这里wij的计算发生了变化)。它们之间最简单的一种计算方式,就是使用点积的方式。
xi和xj是一对输入和输出。对于下一个输出的向量yi+1,我们有一个全新的输入序列和一个不同的权重值。
- 这个点积的输出的取值范围在负无穷和正无穷之间,所以我们要使用一个softmax把它映射到 [0,1]之间,并且要确保它们对于整个序列而言的和为1。
- 以上这些就是self-attention最基本的操作.
2.2 Self-attention和Attention使用方法
根据他们之间的重要区别, 可以区分在不同任务中的使用方法:
- 在神经网络中,通常来说你会有输入层(input),应用激活函数后的输出层(output),在RNN当中你会有状态(state)。如果attention (AT) 被应用在某一层的话,它更多的是被应用在输出或者是状态层上,而当我们使用self-attention(SA),这种注意力的机制更多的实在关注input上。
- Attention (AT) 经常被应用在从编码器(encoder)转换到解码器(decoder)。比如说,解码器的神经元会接受一些AT从编码层生成的输入信息。在这种情况下,AT连接的是**两个不同的组件**(component),编码器和解码器。但是如果我们用**SA**,它就不是关注的两个组件,它只是在关注你应用的**那一个组件**。那这里他就不会去关注解码器了,就比如说在Bert中,使用的情况,我们就没有解码器。
- SA可以在一个模型当中被多次的、独立的使用(比如说在Transformer中,使用了18次;在Bert当中使用12次)。但是,AT在一个模型当中经常只是被使用一次,并且起到连接两个组件的作用。
- SA比较擅长在一个序列当中,寻找不同部分之间的关系 。比如说,在词法分析的过程中,能够帮助去理解不同词之间的关系。AT却更擅长寻找两个序列之间的关系,比如说在翻译任务当中,原始的文本和翻译后的文本。这里也要注意,在翻译任务重,SA也很擅长,比如说Transformer。
- AT可以连接两种不同的模态,比如说图片和文字。SA更多的是被应用在同一种模态上,但是如果一定要使用SA来做的话,也可以将不同的模态组合成一个序列,再使用SA。
- 其实有时候大部分情况,SA这种结构更加的general,在很多任务作为降维、特征表示、特征交叉等功能尝试着应用,很多时候效果都不错。
2.3 Self attention机制的代码实现
2.3.1 Self-attetion实现步骤
- 里我们实现的注意力机制是现在比较流行的点积相乘的注意力机制
- self-attention机制的实现步骤
- 第一步: 准备输入
- 第二步: 初始化参数
- 第三步: 获取key,query和value
- 第四步: 给input计算attention score
- 第五步: 计算softmax
- 第六步: 给value乘上score
- 第七步: 给value加权求和获取output
2.3.2 代码解析
python
import torch
import torch.nn as nn
import torch.nn.functional as F
class SelfAttention(nn.Module):
def __init__(self, embed_size, heads):
# 初始化Self-Attention模块
# param embed_size: 嵌入向量的大小
# param heads: 多头注意力机制中的头数
super(SelfAttention, self).__init__()
self.embed_size = embed_size
self.heads = heads
self.head_dim = embed_size // heads # 每个头的维度大小
assert (
self.head_dim * heads == embed_size
), "Embedding size needs to be divisible by heads"
# 初始化三个线性层,分别用于生成key, query, value
self.values = nn.Linear(self.head_dim, embed_size, bias=False)
self.keys = nn.Linear(self.head_dim, embed_size, bias=False)
self.queries = nn.Linear(self.head_dim, embed_size, bias=False)
self.fc_out = nn.Linear(embed_size, embed_size) # 输出层
def forward(self, inputs):
# 前向传播
# param inputs: 输入的序列数据
# return: 注意力加权后的输出
batch_size = inputs.shape[0]
seq_len = inputs.shape[1]
# 获取key,query和value
# 首先将输入数据重塑为多头的形状,然后分别通过三个线性层
inputs = inputs.reshape(batch_size, seq_len, self.heads, self.head_dim)
values = self.values(inputs)
keys = self.keys(inputs)
queries = self.queries(inputs)
# 计算attention score
# 使用点积来计算每个query和所有key之间的相似度
energy = torch.einsum("nqhd,nkhd->nhqk", [queries, keys])
# 计算softmax
# 在最后一个维度(即seq_len维度)上应用softmax,得到注意力权重
attention = torch.softmax(energy / (self.embed_size ** (1 / 2)), dim=3)
# 给value乘上score
# 使用注意力权重对value进行加权
weighted_values = torch.einsum("nhql,nlhd->nqhd", [attention, values])
# 给value加权求和获取output
# 将多头的结果重塑回原始的序列形状,并通过输出层
output = weighted_values.reshape(batch_size, seq_len, self.embed_size)
output = self.fc_out(output)
return output
if __name__ == "__main__":
embed_size = 8 # 嵌入向量的大小
heads = 2 # 多头注意力机制中的头数
seq_len = 5 # 序列长度
batch_size = 3 # 批量大小
# 随机初始化输入数据
inputs = torch.randn(batch_size, seq_len, embed_size)
# 创建一个Self-Attention实例
attention = SelfAttention(embed_size, heads)
# 前向传播
output = attention(inputs)
print(output.shape)
💘若能为您的学习之旅添一丝光亮,不胜荣幸💘
🐼期待您的宝贵意见,让我们共同进步共同成长🐼