NLP —— 注意力机制(Pytorch代码实现)
一、注意力机制概念
Seq2Seq是啥,它和RNN、LSTM、GRU的关系和区别是啥?
1- Seq2Seq是一种模型架构,用于将一个序列映射到另一个序列。典型结构是 Encoder-Decoder(编码器-解码器)
2- RNN、LSTM、GRU是神经元内部结构
3- Seq2Seq模型的编码器和解码器通常由 RNN、LSTM 或 GRU 构成
4- 可以将Seq2Seq理解为房子,RNN、LSTM、GRU理解为建房子的砖
没有注意力机制之前存在的弊端:
弊端1: 如果翻译的句子很长很复杂,比如直接一篇文章输进去,模型的计算量很大,并且模型的准确率下降严重
弊端2: 没有考虑词与词之间的相关性,导致翻译效果比较差
结构


注意力机制的作用:就是为了解决上面提到的“信息利用不细致的问题”
当Decoder解码器在翻译内容的时候:
要翻译生成welcome的时候 -> 让解码器多关注编码器端的【欢迎】对应的信息
要翻译生成to的时候 -> 让编码器多关注编码器端的【来】对应的信息
...
这样就比只靠一个没有任何区分、不会发生变化的中间语义张量C,进行文本翻译的效果要更好
1 - Encoder:编码器,负责对输入的文本语义进行【理解】
v0、v1、v2 输入数据的词向量
h0、h1、h2 编码器各个时间步的隐藏状态
Embedding 编码器端的词嵌入层,将词索引变成词向量
2 - C 中间语义张量C,也可以叫做专属信息包
编码器中的GRU会把h0、h1、h2打包成一个总的信息包,再传递给到下游的编码器
3 - Decoder :解码器,负责生成/翻译得到文本【主生成】
解码器拿到中间语义张量C以后,结合自己的Embedding词嵌入层,从初始输入_Go开始进行文本内容的生成或者翻译的工作,使用GRU翻译得到英文
s0、s1、s2、s3:解码器端各个时间步的隐藏状态
注意力机制分类及实现
概念
1.软注意力:也称之为全局注意。注意力权重值分布在0-1之间,关注所有的词汇,但是不同词汇根据权重大小关注的程度不一样。
2.硬注意力: 也称之为局部注意。注意力权重值是0或者1,只关注哪些重要的部分,忽略次要的部分
3.自注意力: 也称之为内注意。通过输入项内部的"表决"来决定应该关注哪些输入项.
=========================================================================
二、加了Attention的Encoder-Decoder框架

1、专属信息包C如何来的:权重值和对应的隐藏状态进行乘法,然后累加求和

2、为什么要有注意力机制?
当Decoder解码器生成/翻译得到某个词的时候,应该关注编码器端
中的哪些词。例如:翻译【汤姆】的时候,应该多关注【Tom】。
3、如何得到注意力概率分布
注意力分配权重的计算过程
① 计算相似性
使用相似性计算工程,对比Decoder上一个时间步的隐藏状态和Encoder中每个单词的隐藏状态计算相似性,看谁更重要。
②将相似性转成权重矩阵
使用softmax激活函数,将上面的相似性转为权重(概率),并且权重值之和为1
③ 加权求和
使用第2步的权重矩阵与Encoder中每个单词的隐藏状态进行乘法和加法运算,得到专属学习包
三、Attention机制的本质思想

绝大多数情况下 Key和Value 完全一样,张量形状和张量数字。
Key、Value来至编码器端隐藏状态线性变换后的结果
当前Q = K = V 的时候是自注意力机制。(后面会专门写一篇)
四、Pytorch中注意力机制计算原理

1-6步 专属信息包里面的计算步骤
① Q和K进行张量拼接
② 通过线性变化 nn.Linear计算 Q和K的相似度
③ 将相似性转换权重矩阵
④ 权重矩阵和V进行矩阵乘法运算,得到专属信息包
⑤ Q 和 C 进行张量拼接
⑥ 将拼接后的张量使用 线性变化 nn.Linear调整形状
注意力计算公式
计算规则前提
1- 当Q、K、V相等, 称作自注意力
2- 当Q、K、V不相等时,称为一般注意力(包括软注意力、硬注意力)
3- 自注意力 和 一般注意力的区别,只是数据层面(也就是QKV是否相等)的区别
4- 具体计算得到 自注意力 和 一般注意力,有如下的3种公式。目前使用最多的是第1个和第3个。
注意:
1: 不要将注意力的三种类别(软注意力、硬注意力、自注意力) 和 注意力计算的3个公式搞混
2: 也就是下面的3个公式,即能够算软注意力,也能够算自注意力
3: Transformer框架中使用的是第3个公式算自注意力,原因是除以根号dk
三种规则方法
-
公式1: 将Q和K进行纵轴拼接,然后经过线性变换,再经过Softmax进行处理得到权重,最后和V进行相乘

-
公式2:将Q和K进行纵轴拼接,接着经过一次线性变化,然后进过tanh激活函数处理,再进行sum求和,再经过softmax进行处理得到权重,最后和V进行张量的乘法

-
公式3:将Q和K的转置进行点乘,然后除以一个缩放系数,再经过softmax进行处理得到权重,最后和V进行张量的乘法

注意:dk是k的维度,等于词向量的维度 或者 隐藏状态维度
防止矩阵点乘运算结果过大,导致Softmax计算后概率值出现极端值,极大或极小。避免模型对这些极值学习的不好
K为什么要转置?
① K和V的形状是相同的,假设K的形状是[N个词, M的隐藏状态]
② Q是单个时间步的隐藏状态,形状是[1, M的隐藏状态]
③ 为了能和V进行矩阵乘法运算,那么K需要转置
举例:

①
K和V的张量形状 【batch_size, seq_len, hidden_size】
K和V:【2,5,8】 2条句子,每个句子5个词,向量维度8
Q: query,张量形状[batch_size,num_layers,hidden_size] -> [2,1,8]
Q和K目前不能直接拼接,需要将Q的形状 由 [2,1,8] -> [2,5,8]
②
nn.Linear(in_features=16,out_features=1)
使用线性层对QK拼接后的结果计算相似性 nn.Linear -> 【2,5,1】
为什么是1? 2条句子,5个词。Q要和每个K计算权重->每个词都要算1个权重, 权重10 = 2 * 5 * 1
③
【2,5,1】 -> 【2,1,5】 ?
因为V 是【2,5,8】 要做矩阵乘法运算。【2,5,1】 不能直接相乘
调换维度后 ->. softmax 得到权重矩阵
④
【2,1,5】@ 【2,5,8】 -> 【2,1,8】
⑤
Q和C进行张量拼接
【2,1,8】拼接 【2,1,8】 -> 【2,1,16】
⑥
对拼接后的张量进行形状调整,以满足Decoder端GRU输入数据的要求 nn.Linear(in_features=16,out_features=8)
使用nn.linear调整形状
【2,1,16】 -> 【2,1,8】
代码
import torch
import torch.nn as nn
from torch import Tensor
"""
自定义 模型
"""
class MyAttn(nn.Module):
def __init__(self,q_size,k_size,c_size,output_size):
"""
:param q_size: Q:最后一个维度的大小
:param k_size: k:最后一个维度的大小
:param c_size: 专属信息包最后一个维度大小
:param output_size: Q和C拼接后 -> 经过线性调整后的状态 -> 张量中最后一个维度的大小
"""
# 1 初始化父类
super().__init__()
# 2 设置属性值
self.q_size = q_size
self.k_size = k_size
self.c_size = c_size
self.output_size = output_size
# 3 搭建神经网络
# 构建第2步 和 第6步的线性层
# 为什么 out_features = 1 根据图例 拼接后最后一个维度是1
self.qk_linear = nn.Linear(in_features=self.q_size+self.k_size,out_features=1)
self.qc_linear = nn.Linear(in_features=self.q_size+self.c_size,out_features=self.c_size)
def forward(self,Q:Tensor,K:Tensor,V:Tensor):
"""
前向传播
:param Q: 如图【2,1,8】 【batch_size,num_layers,hidden_size】
:param K: 【2,5,8】 【batch_size,seq_len,hidden_size】
:param V: 和K一致
:return: 输出 output 和 权重 weight
"""
# 1
# Q和K 拼接
# 第2维 维度不同 改变形状
seq_len = K.shape[-2]
# 1.1
# Q: [2,1,8] -> Q_expand [2,5,8]
Q_expand = Q.expand(size=(-1,seq_len,-1))
print(f"Q-->{Q_expand.shape}")
print(f"Q_expand-->{Q_expand.shape}")
# 1.2
# Q_expand [2,5,8] K [2,5,8] -> qk_cat = [2,5,16]
qk_cat = torch.cat((Q_expand,K),dim=-1)
# 2
# 经过线性层计算相似性
# [2,5,16] -> [2,5,1]
qk_cat_score = self.qk_linear(qk_cat)
# 3
# 相似性 转为 权重矩阵
# [2,5,1] -> [2,1,5] 为了和C 矩阵相乘
qk_cat_score = qk_cat_score.transpose(dim0=1,dim1=2)
weighted_mat = torch.softmax(qk_cat_score,dim=-1)
# 4
# 加权求和: 权重矩阵 和 V 进行矩阵乘法,计算得到专属信息包C。
# weighted_mat [2,1,5];V [2,5,8] -> [2,1,8]
C = torch.bmm(weighted_mat,V)
# 5
# Q和C 拼接
# Q [2,1,8];C [2,1,8] -> [2,1,16]
qc_cat = torch.cat((Q,C),dim=-1)
# 6
# 线性调整形状
# [2,1,16] -> [2,1,8]
qc_cat_score = self.qc_linear(qc_cat)
return qc_cat_score,weighted_mat
if __name__ == "__main__":
# 1.定义基础变量
batch_size = 2 # 2条句子
seq_len = 5 # 每条句子5个词
hidden_size = 8 # 词向量和隐藏状态维度相同
num_layers = 1 # 隐藏层层数
# 2- 初始化Q、K、V
"""
为什么Q的形状中间是1?
答:当前只针对Decoder解码器端一个时间步。当前时刻拿一个Q,去和Encoder全部位置算相似性
"""
Q = torch.randn(size=(batch_size,num_layers,hidden_size)) # Decoder端当前时间步的隐藏状态
K = torch.randn(size=(batch_size,seq_len,hidden_size)) # Encoder端多个词的隐藏状态拼接后的
V = torch.randn(size=(batch_size,seq_len,hidden_size)) # Encoder端多个词的隐藏状态拼接后的
# 3- 计算注意力
query_size = hidden_size
key_size = hidden_size
c_size = hidden_size
output_size = hidden_size
attn_model = MyAttn(q_size=query_size,
k_size=key_size,
c_size=c_size,
output_size=output_size)
output,attn_weight = attn_model(Q,K,V)
print(f"权重值{attn_weight}")
print(f"权重值和{attn_weight.sum()}")

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)