当前位置:网站首页>注意力机制
注意力机制
2022-08-04 08:40:00 【幽影相随】
注意力
我们观察事物时,之所以能够快速判断一种事物(当然允许判断是错误的), 是因为我们大脑能够很快把注意力放在事物最具有辨识度的部分从而作出判断,而并非是从头到尾的观察一遍事物后,才能有判断结果. 正是基于这样的理论,就产生了注意力机制.
注意力计算规则
它需要三个指定的输入Q(query), K(key), V(value), 然后通过公式得到注意力的计算结果, 这个结果代表query在key和value作用下的表示. 而这个具体的计算规则有很多种, 我这里只介绍我们用到的这一种.
我们这里使用的注意力的计算规则:
Q, K, V的比喻解释
假如我们有一个问题: 给出一段文本,使用一些关键词对它进行描述!
为了方便统一正确答案,这道题可能预先已经给大家写出了一些关键词作为提示.其中这些给出的提示就可以看作是key,
而整个的文本信息就相当于是query,value的含义则更抽象,可以比作是你看到这段文本信息后,脑子里浮现的答案信息,
这里我们又假设大家最开始都不是很聪明,第一次看到这段文本后脑子里基本上浮现的信息就只有提示这些信息,
因此key与value基本是相同的,但是随着我们对这个问题的深入理解,通过我们的思考脑子里想起来的东西原来越多,
并且能够开始对我们query也就是这段文本,提取关键信息进行表示. 这就是注意力作用的过程, 通过这个过程,
我们最终脑子里的value发生了变化,
根据提示key生成了query的关键词表示方法,也就是另外一种特征表示方法.
刚刚我们说到key和value一般情况下默认是相同,与query是不同的,这种是我们一般的注意力输入形式,
但有一种特殊情况,就是我们query与key和value相同,这种情况我们称为自注意力机制,就如同我们的刚刚的例子,
使用一般注意力机制,是使用不同于给定文本的关键词表示它. 而自注意力机制,
需要用给定文本自身来表达自己,也就是说你需要从给定文本中抽取关键词来表述它, 相当于对文本自身的一次特征提取.
注意力机制
注意力机制是注意力计算规则能够应用的深度学习网络的载体, 除了注意力计算规则外, 还包括一些必要的全连接层以及相关张量处理, 使其与应用网络融为一体. 使用自注意力计算规则的注意力机制称为自注意力机制.
注意力机制在网络中实现的图形表示:
注意力计算规则的代码分析:
def attention(query, key, value, mask=None, dropout=None):
"""注意力机制的实现, 输入分别是query, key, value, mask: 掩码张量, dropout是nn.Dropout层的实例化对象, 默认为None"""
# 在函数中, 首先取query的最后一维的大小, 一般情况下就等同于我们的词嵌入维度, 命名为d_k
d_k = query.size(-1)
# 按照注意力公式, 将query与key的转置相乘, 这里面key是将最后两个维度进行转置, 再除以缩放系数根号下d_k, 这种计算方法也称为缩放点积注意力计算.
# 得到注意力得分张量scores
scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
# 接着判断是否使用掩码张量
if mask is not None:
# 使用tensor的masked_fill方法, 将掩码张量和scores张量每个位置一一比较, 如果掩码张量处为0
# 则对应的scores张量用-1e9这个值来替换, 如下演示
scores = scores.masked_fill(mask == 0, -1e9)
# 对scores的最后一维进行softmax操作, 使用F.softmax方法, 第一个参数是softmax对象, 第二个是目标维度.
# 这样获得最终的注意力张量
p_attn = F.softmax(scores, dim = -1)
# 之后判断是否使用dropout进行随机置0
if dropout is not None:
# 将p_attn传入dropout对象中进行'丢弃'处理
p_attn = dropout(p_attn)
# 最后, 根据公式将p_attn与value张量相乘获得最终的query注意力表示, 同时返回注意力张量
return torch.matmul(p_attn, value), p_attn
多头注意力机制
从多头注意力的结构图中,貌似这个所谓的多个头就是指多组线性变换层,其实并不是,我只有使用了一组线性变化层,即三个变换张量对Q,K,V分别进行线性变换,这些变换不会改变原有张量的尺寸,因此每个变换矩阵都是方阵,得到输出结果后,多头的作用才开始显现,每个头开始从词义层面分割输出的张量,也就是每个头都想获得一组Q,K,V进行注意力机制的计算,但是句子中的每个词的表示只获得一部分,也就是只分割了最后一维的词嵌入向量. 这就是所谓的多头,将每个头的获得的输入送到注意力机制中, 就形成多头注意力机制.
多头注意力机制的作用
这种结构设计能让每个注意力机制去优化每个词汇的不同特征部分,从而均衡同一种注意力机制可能产生的偏差,让词义拥有来自更多元的表达,实验表明可以从而提升模型效果.
多头注意力机制的代码实现
# 用于深度拷贝的copy工具包
import copy
# 首先需要定义克隆函数, 因为在多头注意力机制的实现中, 用到多个结构相同的线性层.
# 我们将使用clone函数将他们一同初始化在一个网络层列表对象中. 之后的结构中也会用到该函数.
def clones(module, N):
"""用于生成相同网络层的克隆函数, 它的参数module表示要克隆的目标网络层, N代表需要克隆的数量"""
# 在函数中, 我们通过for循环对module进行N次深度拷贝, 使其每个module成为独立的层,
# 然后将其放在nn.ModuleList类型的列表中存放.
return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])
# 我们使用一个类来实现多头注意力机制的处理
class MultiHeadedAttention(nn.Module):
def __init__(self, head, embedding_dim, dropout=0.1):
"""在类的初始化时, 会传入三个参数,head代表头数,embedding_dim代表词嵌入的维度, dropout代表进行dropout操作时置0比率,默认是0.1."""
super(MultiHeadedAttention, self).__init__()
# 在函数中,首先使用了一个测试中常用的assert语句,判断h是否能被d_model整除,
# 这是因为我们之后要给每个头分配等量的词特征.也就是embedding_dim/head个.
assert embedding_dim % head == 0
# 得到每个头获得的分割词向量维度d_k
self.d_k = embedding_dim // head
# 传入头数h
self.head = head
# 然后获得线性层对象,通过nn的Linear实例化,它的内部变换矩阵是embedding_dim x embedding_dim,然后使用clones函数克隆四个,
# 为什么是四个呢,这是因为在多头注意力中,Q,K,V各需要一个,最后拼接的矩阵还需要一个,因此一共是四个.
self.linears = clones(nn.Linear(embedding_dim, embedding_dim), 4)
# self.attn为None,它代表最后得到的注意力张量,现在还没有结果所以为None.
self.attn = None
# 最后就是一个self.dropout对象,它通过nn中的Dropout实例化而来,置0比率为传进来的参数dropout.
self.dropout = nn.Dropout(p=dropout)
def forward(self, query, key, value, mask=None):
"""前向逻辑函数, 它的输入参数有四个,前三个就是注意力机制需要的Q, K, V, 最后一个是注意力机制中可能需要的mask掩码张量,默认是None. """
# 如果存在掩码张量mask
if mask is not None:
# 使用unsqueeze拓展维度
mask = mask.unsqueeze(0)
# 接着,我们获得一个batch_size的变量,他是query尺寸的第1个数字,代表有多少条样本.
batch_size = query.size(0)
# 之后就进入多头处理环节
# 首先利用zip将输入QKV与三个线性层组到一起,然后使用for循环,将输入QKV分别传到线性层中,
# 做完线性变换后,开始为每个头分割输入,这里使用view方法对线性变换的结果进行维度重塑,多加了一个维度h,代表头数,
# 这样就意味着每个头可以获得一部分词特征组成的句子,其中的-1代表自适应维度,
# 计算机会根据这种变换自动计算这里的值.然后对第二维和第三维进行转置操作,
# 为了让代表句子长度维度和词向量维度能够相邻,这样注意力机制才能找到词义与句子位置的关系,
# 从attention函数中可以看到,利用的是原始输入的倒数第一和第二维.这样我们就得到了每个头的输入.
query, key, value = \
[model(x).view(batch_size, -1, self.head, self.d_k).transpose(1, 2)
for model, x in zip(self.linears, (query, key, value))]
# 得到每个头的输入后,接下来就是将他们传入到attention中,
# 这里直接调用我们之前实现的attention函数.同时也将mask和dropout传入其中.
x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)
# 通过多头注意力计算后,我们就得到了每个头计算结果组成的4维张量,我们需要将其转换为输入的形状以方便后续的计算,
# 因此这里开始进行第一步处理环节的逆操作,先对第二和第三维进行转置,然后使用contiguous方法,
# 这个方法的作用就是能够让转置后的张量应用view方法,否则将无法直接使用,
# 所以,下一步就是使用view重塑形状,变成和输入形状相同.
x = x.transpose(1, 2).contiguous().view(batch_size, -1, self.head * self.d_k)
# 最后使用线性层列表中的最后一个线性层对输入进行线性变换得到最终的多头注意力结构的输出.
return self.linears[-1](x)
测试
# 注意力机制
# 我们令输入的query, key, value都相同, 位置编码的输出
query = key = value = pe_result
attn, p_attn = encoder.attention(query, key, value)
print("attn:", attn)
print(attn.size())
print("p_attn:", p_attn)
print(p_attn.size())
# 头数head
head = 8
# 词嵌入维度embedding_dim
embedding_dim = 512
# 置零比率dropout
dropout = 0.2
# 假设输入的Q,K,V仍然相等
query = value = key = pe_result
# 输入的掩码张量mask
mask = Variable(torch.zeros(8, 4, 4))
mha = encoder.MultiHeadedAttention(head, embedding_dim, dropout)
mha_result = mha(query, key, value, mask)
print("mha_result:", mha_result)
attn: tensor([[[-13.8824, -10.0891, -27.5858, ..., -0.1623, 13.4148, 7.5599],
[ 2.1640, 51.0298, -0.2015, ..., 21.2529, -41.9738, 0.0000],
[ 6.9812, -7.6297, 15.6535, ..., 13.0467, 7.1040, 12.4104],
[ 14.9699, 2.1783, 37.2174, ..., -53.7662, 12.4649, 2.9366]],
[[ -8.2613, 6.2006, 16.9781, ..., 35.4672, 22.1663, 0.0000],
[-40.9224, 17.7519, 9.1471, ..., -27.1193, -30.0732, -34.5008],
[-17.2694, 1.3170, -10.1614, ..., -3.7508, -16.7412, 7.0938],
[ -1.5082, -5.4363, 2.8118, ..., 0.0000, -24.9139, -54.3199]]],
grad_fn=<UnsafeViewBackward0>)
torch.Size([2, 4, 512])
p_attn: tensor([[[1., 0., 0., 0.],
[0., 1., 0., 0.],
[0., 0., 1., 0.],
[0., 0., 0., 1.]],
[[1., 0., 0., 0.],
[0., 1., 0., 0.],
[0., 0., 1., 0.],
[0., 0., 0., 1.]]], grad_fn=<SoftmaxBackward0>)
torch.Size([2, 4, 4])
mha_result: tensor([[[-1.4132, 2.2245, -1.1098, ..., -3.5604, 1.7258, -1.4194],
[-2.6828, 4.9929, 1.4352, ..., -2.0582, 3.6597, -3.0089],
[-1.4100, 7.6690, -2.5672, ..., -3.0528, 5.4415, -2.5700],
[-0.6725, 2.1622, 0.4773, ..., -7.1450, -1.4593, 2.2417]],
[[-1.1598, -1.8696, -1.5388, ..., 1.3303, -3.0106, 1.9627],
[ 3.2437, -1.5602, 0.8261, ..., 0.5034, -0.9025, -1.0680],
[-0.3674, -4.7591, -0.4724, ..., 2.7614, -2.6698, 1.1591],
[ 2.1000, 0.1836, 0.5179, ..., -0.2727, -1.9056, -1.6426]]],
grad_fn=<ViewBackward0>)
边栏推荐
猜你喜欢

(三)DDD上下文映射图——老师,我俩可是纯洁的男女关系!

高等代数_证明_对称矩阵属于不同特征值的特征向量正交

Yolov5更换主干网络之《旷视轻量化卷积神经网络ShuffleNetv2》

ShuffleNet v2 network structure reproduction (Pytorch version)

IDEA引入类报错:“The file size (2.59 MB) exceeds the configured limit (2.56MB)

Typora_Markdown_图片标题(题注)

【Attention】Dual Attention(DANet) & Fully Attention(FLA)

YOLOv5应用轻量级通用上采样算子CARAFE

经典动态规划问题的递归实现方法——LeetCode39 组合总和

推荐几种可以直接翻译PDF英文文献的方法
随机推荐
【UE虚幻引擎】UE5实现动态导航样条线绘制
研究性学习专题 3_LL(1)语法分析设计原理与实现
秒懂大模型 | 3步搞定AI写摘要
js异步变同步、同步变异步
高等代数_证明_幂等矩阵一定能够相似对角化
(三)DDD上下文映射图——老师,我俩可是纯洁的男女关系!
通过GBase 8c Platform安装数据库集群时报错
金仓数据库KingbaseES客户端编程接口指南-JDBC(10. JDBC 读写分离最佳实践)
2022的七夕,奉上7个精美的表白代码,同时教大家改源码快速自用
2022-08-02 分析RK817 输出32k clock PMIC_32KOUT_WIFI给WiFi模块 clock 注册devm_clk_hw_register
高等代数_证明_对称矩阵一定能够相似对角化
[Computer recording screen] How to use bandicam to record the game setting graphic tutorial
YOLOv5应用轻量级通用上采样算子CARAFE
Libpq 是否支持读写分离配置
spark算子讲解
yuv420sp转jpg
在安装GBase 8c数据库的时候,报错显示“Host ips belong to different cluster”。这是为什么呢?有什么解决办法?
The sorting algorithm including selection, bubble, and insertion
关于Oracle RAC 11g重建磁盘组的问题
25.时间序列预测实战