当前位置: 首页 > article >正文

【NLP】注意力机制

目录

一、认识注意力机制

1.1 常见注意力计算规则

1.2 注意力机制的作用

1.3 注意力机制代码实现

二、注意力机制原理

2.1 attention计算过程 

2.2 attention的计算逻辑

2.3 有无attention模型对比

2.3.1 无attention机制的模型

2.3.2 有attention机制的模型

三、Self-attention

3.1 认识Self-attention

3.2 Self-attention 和 attention 的使用方法

3.3 Self-attention 机制代码实现


一、认识注意力机制

人类观察事物时,之所以能够快速判断一种事物(当然允许判断是错误的),是因为大脑能够很快把注意力放在事物最具有辨识度的部分从而作出判断,而并非是从头到尾的观察一遍事物后,才能有判断结果。正是基于这样的理论,就产生了注意力机制

需要三个指定的输入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相同,这种情况称为自注意力机制。就如上面的例子,使用一般注意力机制,是使用不同于给定文本的关键词表示它。而自注意力机制需要用给定文本自身来表达自己,即需要从给定文本中抽取关键词来表述自己, 相当于对文本自身的一次特征提取

1.1 常见注意力计算规则

  • 将Q,K进行纵轴拼接,做一次线性变化,再使用softmax处理,最后与V做张量乘法

  • 将Q,K进行纵轴拼接,做一次线性变化后再使用tanh函数激活,然后再进行内部求和,最后使用softmax处理,再与V做张量乘法

  • Q与K的转置做点积运算,然后除以一个缩放系数,再使用softmax处理,最后与V做张量乘法

注意:当注意力权重矩阵和V都是三维张量且第一维代表为batch条数时,则做bmm运算。bmm是一种特殊的张量乘法运算

import torch

inputs = torch.randn(10, 3, 4)
weights = torch.randn(10, 4, 5)
result = torch.bmm(inputs, weights)
print(result.size())
# torch.Size([10, 3, 5])

1.2 注意力机制的作用

注意力机制是注意力计算规则能够应用的深度学习网络的载体,同时包括一些必要的全连接层以及相关张量处理,使其与应用网络融为一体。使用自注意力计算规则的注意力机制称为自注意力机制

NLP领域中,当前的注意力机制大多数应用于seq2seq架构,即编码器和解码器模型

  • 解码器端的注意力机制:能够根据模型目标有效的聚焦编码器的输出结果,当其作为解码器的输入时提升效果,改善以往编码器输出是单一定长张量,无法存储过多信息的情况
  • 编码器端的注意力机制:主要解决表征问题,相当于特征提取过程,得到输入的注意力表示,一般使用自注意力(self-attention)

1.3 注意力机制代码实现

  • 第一步:根据注意力计算规则,对Q,K,V进行相应的计算
  • 第二步:根据第一步采用的计算方法。若是拼接方法,则需将Q与第二步的计算结果再进行拼接;若是转置点积,一般是自注意力,Q与V相同,则不需要进行与Q的拼接
  • 第三步:最后为了使整个attention机制按照指定尺寸输出使用,线性层作用在第二步的结果上做一个线性变换,得到最终对Q的注意力表示
import torch
import torch.nn as nn

class attention(nn.Module):
    def __init__(self, query_size, key_size, value_size1, value_size2, output_size):
        super(attention, 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.line1 = nn.Linear(self.query_size + key_size, self.value_size1)
        self.line2 = nn.Linear(self.query_size + value_size2, self.output_size)

    def forward(self, Q, K, V):
        attention_weight = torch.softmax(self.line1(torch.cat((Q[0], K[0]), 1)), dim=1)
        # check shape
        print(Q[0].shape, K[0].shape, (torch.cat((Q[0], K[0]), 1)).shape)
        # torch.Size([1, 32]) torch.Size([1, 32]) torch.Size([1, 64])
        print(self.line1(torch.cat((Q[0], K[0]), 1)).shape)
        # torch.Size([1, 32])
        print(attention_weight.shape)
        # torch.Size([1, 32])

        attention_applied = torch.bmm(attention_weight.unsqueeze(0), V)
        # check shape
        print(attention_weight.unsqueeze(0).shape)
        # torch.Size([1, 1, 32])
        print(attention_applied.shape)
        # torch.Size([1, 1, 64])

        output = torch.cat((Q[0], attention_applied[0]), 1)
        # check shape
        print(output.shape)
        # torch.Size([1, 96])

        output = self.line2(output).unsqueeze(0)
        # check shape
        print(output.shape)
        # torch.Size([1, 1, 64])

        return output, attention_weight


def main():
    query_size = 32
    key_size = 32
    value_size1 = 32
    value_size2 = 64
    output_size = 64
    attn = attention(query_size, key_size, value_size1, value_size2, output_size)
    # (batch_size, seq_len, hidden_size)
    # 批量数, 输入序列长度, 每个词向量的维度
    Q = torch.randn(1, 1, 32)
    K = torch.randn(1, 1, 32)
    V = torch.randn(1, 32, 64)
    out, weights = attn(Q, K ,V)
    print(out)
    print(weights)

if __name__ == "__main__":
    main()

# tensor([[[ 0.0659, -0.2032, -0.1200,  0.3103,  0.1267, -0.2573,  0.0595,
#           -0.3809,  0.6900,  0.5539, -0.1737, -0.3153,  0.7032, -0.6192,
#           -0.0120,  0.4132,  0.1484,  0.3911, -0.0876,  0.3548, -0.2247,
#           -0.3236,  0.2761, -0.1817, -0.2472, -0.3050,  0.4670,  0.3442,
#           -0.0092,  0.5283,  0.0881,  0.2219, -0.7051, -0.0028,  0.5049,
#            0.7083, -0.2809,  0.3218, -0.3225,  0.1372,  0.3596,  0.0069,
#           -0.1422, -0.0494,  0.4049, -0.0856,  0.5200, -0.0793, -0.0608,
#           -0.0135,  0.3282, -0.6138,  0.0643, -0.6000, -0.1060,  0.4633,
#            0.1958, -0.2890,  0.3448, -0.2266,  0.1201,  0.3016,  0.7245,
#            0.2607]]], grad_fn=<UnsqueezeBackward0>)
# tensor([[0.0564, 0.0397, 0.0270, 0.0137, 0.0302, 0.0242, 0.0452, 0.0154, 0.0244,
#          0.0204, 0.0363, 0.0151, 0.0079, 0.0592, 0.0556, 0.0196, 0.0188, 0.1013,
#          0.0348, 0.0679, 0.0289, 0.0183, 0.0196, 0.0123, 0.0113, 0.0516, 0.0051,
#          0.0378, 0.0405, 0.0109, 0.0426, 0.0080]], grad_fn=<SoftmaxBackward0>)

二、注意力机制原理

2.1 attention计算过程 

  • 阶段一:query 和 key 进行相似度计算,得到一个query 和 key 相关性的分值
  • 阶段二:将这个分值进行归一化(softmax),得到一个注意力的分布
  • 阶段三:使用注意力分布和 value 进行计算,得到一个融合注意力的更好的 value 值

机器翻译中,会使用 seq2seq 的架构,每个时间步从词典里生成一个翻译结果。如下图:

在没有注意力之前,每次都是根据 Encoder 部分的输出结果来进行生成,提出注意力后,就是想在生成翻译结果时并不是看 Encoder 中所有的输出结果,而是先来看看想生成的这部分和哪些单词可能关系会比较大,关系大的多借鉴些,关系小的少借鉴些

  • 为了生成单词,把 Decoder 部分输入后得到的向量作为 query;把 Encoder 部分每个单词的向量作为 key。先把 query 和每一个单词进行点乘 score = query⋅key,得到相关性的分值
  • 有了这些分值后,对这些分值做一个softmax,得到一个注意力的分布
  • 有了这个注意力,就可以用其和 Encoder 的输出值 (value) 进行相乘,得到一个加权求和后的值,这个值就包含注意力的表示,用来预测要生成的词

2.2 attention的计算逻辑

attention 并不是只有这一种计算方式,后来还有很多人找到了各种各样的计算注意力的方法。比如上面介绍的三种计算规则,但是从本质上都遵循着这个三步走的逻辑

  • query 和 key 进行相似度计算,得到一个query 和 key 相关性的分值
  • 将这个分值进行归一化(softmax),得到一个注意力的分布
  • 使用注意力分布和 value 进行计算,得到一个融合注意力的更好的 value 值

2.3 有无attention模型对比

2.3.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都是一样的,没有任何区别

每个 y_{i} 都依次这么产生,那么看起来就是整个系统根据输入句子Source生成了目标句子Target。若Source是中文句子,Target是英文句子,那么这就是解决机器翻译问题的 Encoder-Decoder 框架;若Source是一篇文章,Target是概括性的几句描述语句,那么这是文本摘要的 Encoder-Decoder 框架;若Source是一句问句,Target是一句回答,那么这是问答系统或者对话机器人的 Encoder-Decoder 框架

存在问题

语义编码C是由句子Source的每个单词经过 Encoder 编码产生的,这意味着不论是生成哪个单词,其实句子Source中任意单词对生成某个目标单词 y_{i} 来说影响力都是相同的,类似于人类看到眼前的画面,但是眼中却没有注意焦点一样

2.3.2 有attention机制的模型

假设在机器翻译中,比如输入的是英文句子:Tom chase Jerry。Encoder-Decoder框架逐步生成中文单词:"汤姆","追逐","杰瑞"。在翻译"杰瑞"这个中文单词的时候,分心模型里面的每个英文单词对于翻译目标单词"杰瑞"贡献是相同的,显然"Jerry"对于翻译成"杰瑞"更重要,但是分心模型是无法体现这一点的

没有引入注意力的模型在输入句子比较短的时候问题不大,但若输入句子比较长,此时所有语义完全通过一个中间语义向量来表示,单词自身的信息已经消失,可想而知会丢失很多细节信息,这也是为何要引入注意力模型的重要原因

上面的例子中,若引入Attention模型的话,应该在翻译"杰瑞"时,体现出英文单词对于翻译当前中文单词不同的影响程度,比如给出类似下面一个概率分布值:(Tom,0.3)(Chase,0.2) (Jerry,0.5)。每个英文单词的概率代表了翻译当前单词"杰瑞"时,注意力分配模型分配给不同英文单词的注意力大小。这对于正确翻译目标语单词肯定是有帮助的,因为引入了新的信息

同理,目标句子中的每个单词都应该学会其对应的源语句子中单词的注意力分配概率信息。这意味着在生成每个单词的时候,原先都是相同的中间语义表示C会被替换成根据当前生成单词而不断变化的。即由固定的中间语义表示C换成了根据当前输出单词来调整成加入注意力模型的变化的。增加了注意力模型的Encoder-Decoder框架理解起来如下图所示:

即生成目标句子单词的过程成了下面的形式:

而每个 C_{i} 可能对应着不同的源语句子单词的注意力分配概率分布

f2 函数代表 Encoder 对输入英文单词的某种变换函数,比如若Encoder是用的RNN模型的话,这个 f2 函数的结果往往是某个时刻输入后隐层节点的状态值;g 代表 Encoder 根据单词的中间表示合成整个句子中间语义表示的变换函数,一般的做法中,g函数就是对构成元素加权求和,如下:

Lx代表输入句子Source的长度,a_{ij} 代表在 Target 输出第 i 个单词时 Source 输入句子中的第 j 个单词的注意力分配系数,而 h_{j} 则是 Source 输入句子中第 j 个单词的语义编码

假设 C_{i} 下标 i 就是上面例子所说的"汤姆",那么Lx就是3,h1=f('Tom'),h2=f('Chase'),h3=f('jerry') 分别输入句子每个单词的语义编码,对应的注意力模型权值则分别是0.6、0.2、0.2, 所以 g 函数本质上就是加权求和函数。翻译中文单词"汤姆"时,数学公式对应的中间语义表示 C_{i} 的形成过程类似下图:

三、Self-attention

3.1 认识Self-attention

Self-attention 就本质上是一种特殊的 attention。这种应用在transformer中最重要的结构之一。attention机制能够找到子序列和全局的attention的关系,即找到权重值w_{i}。Self-attention 相对于 attention 的变化,其实就是寻找权重值w_{i}过程不同

为了能够产生输出的向量y_{i},self-attention 其实是对所有的输入做了一个加权平均的操作,这个公式和上面的attention是一致的

j 代表整个序列的长度,并且 j 个权重的相加之和等于1。值得一提的是,w_{ij} 并不是一个需要神经网络学习的参数,是来源于 x_{i} 和 x_{j} 的之间的计算的结果(w_{ij} 的计算发生了变化)。最简单的一种计算方式,就是使用点积的方式

x_{i} 和 x_{j} 是一对输入和输出。对于下一个输出的向量y_{i+1},有一个全新的输入序列和一个不同的权重值。这个点积的输出的取值范围在负无穷和正无穷之间,所以要使用一个 softmax 将其映射到[0,1]之间,且确保对于整个序列而言和为1

3.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 这种结构更加的普遍,在很多任务作为降维、特征表示、特征交叉等功能尝试着应用,很多时候效果都不错

3.3 Self-attention 机制代码实现

import torch

# 准备输入
datas = [[1,0,1,1],[0,2,0,2],[1,1,1,1]]
inputs = torch.tensor(datas, dtype=torch.float32)

# 初始化参数
key_weights = [[0, 0, 1],[1, 1, 0],[0, 1, 0],[1, 1, 0]]
query_weights = [[1, 0, 1],[1, 0, 0],[0, 0, 1],[0, 1, 1]]
value_weights = [[0, 2, 0],[0, 3, 0],[1, 0, 3],[1, 1, 0]]
key_weights = torch.tensor(key_weights, dtype=torch.float32)
query_weights = torch.tensor(query_weights, dtype=torch.float32)
value_weights = torch.tensor(value_weights, dtype=torch.float32)

# 获取key、value、query
keys = inputs @ key_weights
querys = inputs @ query_weights
values = inputs @ value_weights
print("Keys: \n", keys)
print("Querys: \n", querys)
print("Values: \n", values)

# 计算 attention score(假设缩放系数为1)
attntion_scores = querys @ keys.T
print(attntion_scores)
# 计算softmax
attntion_scores_softmax = torch.softmax(attntion_scores, dim=-1)
print(attntion_scores_softmax)

# value乘attntion_scores_softmax
weighted_values = values[:,None] * attntion_scores_softmax.T[:,:,None]
print(weighted_values)
# 给value加权求和获取output
outputs = weighted_values.sum(dim=0)
print(outputs)


http://www.kler.cn/a/556349.html

相关文章:

  • buu-[OGeek2019]babyrop-好久不见41
  • Mac arm架构使用 Yarn 全局安装 Vue CLI
  • Brave132编译指南 Linux篇 - Brave简介(一)
  • SkyWalking快速入门
  • ubuntu追加path环境变量
  • 使用爬虫按关键字搜索亚马逊商品:实战指南
  • selenium环境搭建
  • DeepSeek 与网络安全:AI 在网络安全领域的应用与挑战
  • 天 锐 蓝盾终端安全管理系统:办公U盘拷贝使用管控限制
  • 上下文感知 AI Agent 将赋予我们的“超能力”
  • 2后端JAVA:下载数据库数据到EXCEL表格?代码
  • [Android]使用AlarmManager设置周期性任务
  • [C++]使用纯opencv部署yolov12目标检测onnx模型
  • idea任意版本的安装
  • 第1章:LangChain4j的聊天与语言模型
  • rk3588/3576板端编译程序无法运行视频推理
  • OpenBMC:BmcWeb实例化App
  • 清影2.0(AI视频生成)技术浅析(五):音频处理技术
  • AI Agent Service Toolkit:一站式大模型智能体开发套件
  • C++与Python:两种编程语言的区别