注意力机制:当我们看一个图像或者听一段音频时,会根据自己的需求,集中注意力在关键元素上,以获取相关信息。
同样地,注意力机制中的模型也会根据输入的不同部分,给它们不同的权重,并集中注意力在最具有代表性的元素上,以提高模型的性能。
具体来说,注意力机制通常包括以下几个步骤:
映射输入:将输入(如文本、图像等)映射到一个向量空间中。
计算注意力权重:根据注意力模型的不同实现方式,计算每个输入元素的权重,以表示它在当前任务中的重要性。
加权求和:根据计算得到的注意力权重,对输入进行加权求和,以产生最终的输出。
Self-Attention 是一种“自我关注”机制,用于提取输入序列中不同位置之间的相关性。它的基本思想是:对于任意一个元素(如单词、句子等),通过对其它元素的表示进行线性变换,得到一个新的表示,反映了该元素与其它元素之间的联系。Self-Attention 通常包括以下几个步骤:
映射输入:将输入序列中每个元素(如单词)映射到一个高维向量空间中,并形成一个矩阵X,其中每行代表一个元素的向量表示。
计算注意力权重:对于矩阵X中的每个元素,都可以利用一个查询向量Q、一个键向量K和一个值向量V,计算出它与其他所有元素之间的相似度(或者相关度),并将这些相似度作为权重,加权求和得到输出。
加权求和:根据计算得到的注意力权重,对值向量V进行加权求和,产生最终的输出。
Transformer结构组件和逻辑
重点:自注意力、多头注意力、交叉注意力
1、输入嵌入层(Input Embedding):输入数据的预处理,计算每个单词的表示向量X,它由单词的Embedding和单词位置的Embedding相加得到。
(1)单词Embedding计算
将每个单词映射到连续向量空间中, 即映射为一个1乘d的向量表示(矩阵)。
那么n个单词的输入就变成了n*d的矩阵(向量)。
(因为注意力机制需要输入是向量格式,两个向量的点乘表示两个向量的相似度或者说相关性)
单词Embedding有以下几种方法:
使用预训练的词嵌入模型:如 Word2Vec、GloVe、FastText 和 BERT 等,通过大规模的文本语料库学习单词的向量表示。
训练自定义的词嵌入模型:使用诸如 Word2Vec 或 Skip-gram 模型这样的算法,在你自己的数据集上训练单词Embedding。
使用深度学习模型中的嵌入层:使用深度学习模型(如RNN、CNN或 Transformer模型)的嵌入层来学习单词 Embedding。
此外,有两个词汇表(相当于字典,对每个字/单词做了索引),分别对应源语言和目标语言,初始化词汇表得到词元list。
(2)位置Embedding计算
因为词袋模型的单词向量表示无法提取单词的位置特征,而句子的意思跟单词在句子的位置也有关系,所以往单词的向量表示中加入位置编码。位置Embedding可以通过训练得到,也可以使用某种公式计算得到。在 Transformer 中采用了后者,计算公式如下:
其中,pos 表示单词在句子中的位置,d 表示 PE的维度 (与词 Embedding 一样),2i 表示偶数的维度,2i+1 表示奇数维度 (即 2i≤d, 2i+1≤d)。
2、自注意力Self-Attention以及相关计算
自注意力中,Q、K、V的物理意义是一样的,都表示同一个句子中不同token组成的矩阵。
(1)计算Q、K、V向量:为输入序列的每个单词计算三个向量:查询向量(Query)、键向量(Key)和值向量(Value)。
这些向量通常是通过将单词的嵌入向量(Embedding Vector)输入到一个线性变换层得到的。
Q、K、V使用不同的权重矩阵Wq, Wk,Wv来计算,可以理解为是在不同空间上的投影。
正因为有了这种不同空间的投影,增加了表达能力。这些权重矩阵正是模型要训练学习的参数。
矩阵中的每一行表示一个token的word embedding向量。假设一个句子单词长度是6,embedding维度是512,那么Q、K、V都是(6,512)的矩阵。
(2)计算Q、K的点积(注意力分数):
计算Query向量与Key向量的点积,得到一个分数(attention score矩阵)。这个分数反映了任意两个单词的关联程度,或者说任两个token的相似性。
(3)Softmax函数归一化(注意力权重):这些分数会经过一个Softmax函数进行归一化,得到每个单词的注意力权重(一个百分比,可以理解为概率)。
这些权重表示了在理解当前单词时,应该给予序列中其他单词多大的关注。
(4)注意力权重加权求和(加权和向量):
这些注意力权重与对应的Value向量进行加权求和,得到一个加权和向量。这个加权和向量会被用作当前单词的新表示,包含了更丰富的上下文信息。
因为attention score矩阵已经很难表示原来的句子了,而V还代表着原来的句子,所以用attention score矩阵来对V进行提炼,得到的是一个加权后的结果。
单词表示向量矩阵 经过6个Encoder block 后可以得到句子所有单词的编码信息矩阵 C(每一个Encoder block输出的矩阵维度与输入完全一致)。
3、多头注意力层 Multi-Head Attention
Multi-Head Attention就是使用多组WQ 、WK、WV得到多组Query,Keys,Values,然后每组分别计算得到一个Z矩阵,最后将得到的多个Z矩阵进行拼接。
Multi-Head Attention增强学习能力(多组不同的线性变换即所谓的多头),由多个 Self-Attention 组合形成的。
论文中8个输出矩阵 Z1 到 Z8 之后,Multi-Head Attention 将它们拼接在一起 (Concat),然后传入一个Linear层,得到 Multi-Head Attention 最终的输出Z。
4、残差连接和层规一化(Add & Norm )
add&norm组件是由残差连接和紧随其后的层规一化组成,用来提升训练的稳定性。
从示意图看出,所有子层(模型)输出前都经过add&norm组件,才用作下一层的输入。
1)残差连接
Add指某一层模型的输入x加上该层模型的输出(就是x+f(x),后续把和作为归一化函数的输入)
用意:残差连接引入输入直接到输出的通路,便于梯度回传,缓解在优化过程中由于网络过深引起的梯度消失问题。
为什么这么做?添加了残差块后,网络的输出就变成h(X)=F(X)+X,
(一般的设计模型时不知道最优需要几层网络,为了保证设计多出来的几层网络可进行恒等映射F(x)=x,把网络的输出转变成h(X)=F(X)+X,只要训练让F(X)=0就可以了,而训练变成0比变成X容易)
总之,ResNet残差连接解决神经网络退化,梯度消失的问题
2)层归一化
Norm指 Layer Normalization(层归一化) 是基于特征维度进行规范化,将数据进行标准化(乘以缩放系数、加上平移系数,保留其非线性能力。就是将每一层神经元的输入都转成均值方差都一样的。
计算公式
LN(x)=α(x−μσ)+βLN(x)=α(x−μσ)+β
对输入数据进行Normalize归一化的目的有二:
1.能够加快训练的速度,可以加快收敛。
2.提高训练的稳定性。
例如,对于MultiHeadAttention层:
LayerNorm(X + MultiHeadAttention(X))
对于FeedForward层:
LayerNorm(X+FeedForward(X))
Layer Normalization(LN)与Batch Normalization(BN)的区别:
LN是在同一个样本中不同神经元之间进行归一化,而BN是在同一个batch中不同样本之间的同一位置的神经元之间进行归一化。
5、位置感知的前馈网络(position-wise)一个2层全连接网络。
与人类大脑能够并行处理信息的能力类似,Transformer模型中的每一个注意力子层都包含一个前馈网络FFN。
FFN层被放置在每个注意力层的后面,用来对注意力权重进行非线性转换或对特征进行映射,通过使用线性变换和非线性激活函数,
使模型对注意力层的输出进行进一步的转换和特征提取,从而增强模型的表征能力,提供更多的非线性和学习能力。
FFN 就是一个简单的二层全连接网络,其中第一层的激活函数为ReLU。对于输入序列中每个位置上的向量x:
FFN(x)=max(0,xW1+b1)W2+b2
等价
FFN(x)=Relu(xW1+b1)W2+b2FFN(x)=Relu(xW1+b1)W2+b2
这些全连接层是逐位置(position-wise)的,这意味着每个序列位置都会经过相同的线性变换,再用非线性激活函数Relu,再线性变换。
这里的x就是Multi-Head Attention的输出Z,假如Z是(2,64)维的矩阵,W1是(64,1024),其中W2与W1维度相反(1024,64),那么按照上面的公式:
FFN(Z)=(2,64)x(64,1024)x(1024,64)=(2,64),我们发现维度没有发生变化,这两层网络就是为了将输入的Z映射到更加高维的空间中(2,64)x(64,1024)=(2,1024),
然后通过非线性函数ReLU进行筛选,筛选完后再变回原来的维度。然后经过Add&Normalize,输入下一个encoder中,经过6个encoder后输入到decoder。
需要注意的是:在同一层上(这里指的是Encoder或Decoder内的层),每个位置上的向量x所对应的参数W1、W2、b1、b2都是相同的,但是层与层之间的参数是不同的。
前馈层功能:
线性变换1:使用可学习的权重矩阵将输入表示投影到更高维度的空间中。
非线性激活:第一个线性变换的输出通过非线性激活函数(例如ReLU)传递。这引入了模型的非线性,使其能够捕捉数据中的复杂模式和关系。
线性变换2:激活函数的输出然后通过另一个可学习的权重矩阵投影回原始的维度空间中。
6、编码器(N个EncoderBlock串联)
每个EncoderBlock包含两个子层:多头自注意力和基于位置的前馈网络,这两个子层都使用了残差连接和层规一化。
(1)Multi-Head Attention
(2)Feed Forward
(3)Add & Norm
Encoder block 接收输入矩阵X(n,d),并输出一个矩阵O(n,d)。通过多个 Encoder block 叠加就可以组成 Encoder。
n表示单词数量,d表示词嵌入向量的维度,如512.
所有编码器都会接收到一个大小为 512 的向量列表,即词嵌入向量(加入了位置编码),其他编码器接收的是上一个编码器的输出。
最后一个 Encoder block 输出的矩阵就是编码信息矩阵 C,这一矩阵后续会用到 Decoder 中。
每个查询都会关注所有的键值对并生成一个注意力输出。 由于Q、K和V来自同一组输入,故称为Self-Attention。
7、解码器(N个DecoderBlock串联)
每个 Decoder block 结构:
1) 掩码多头注意力
每个DecoderBlock的第一个自注意力子层额外增加了注意力掩码,屏蔽未知单词,只能看到已经翻译的单词和当前单词。
2) 交叉注意力
每一个DecoderBlock比EncoderBlock多一个多头注意力,并且使用交叉注意力(Cross-attention)方法,同时接收来自编码器端的输出以及前一个掩码注意力层的输出。
Key和Value是使用编码器的输出进行投影的,Query是通过解码器前一层的输出进行投影所得。
3)位置感知的前馈网络(position-wise)
计算逻辑详解:
(1)第一个 Multi-Head Attention 层采用了 Masked 操作。
第一步,掩码操作,Mask 的作用是只能使用之前的信息。
第二步,和之前的 Self-Attention 一样,通过输入矩阵X计算得到Q,K,V矩阵。然后计算Q积K。
第三步,Q积K之后需要进行 Softmax,计算 attention score,我们在 Softmax 之前需要使用Mask矩阵遮挡住每一个单词之后的信息。
第四步,使用第三步得到的attention score矩阵与矩阵V相乘,得到输出 Z。
第五步,通过上述步骤就可以得到一个 Mask Self-Attention 的输出矩阵,然后和 Encoder 类似,通过 Multi-Head Attention 拼接多个输出,然后计算得到第一个 Multi-Head Attention 的输出Z,Z与输入X维度一样。
(2)第二个 Multi-Head Attention 层的K, V矩阵使用 Encoder 的编码信息矩阵C(Encoder中最后一层的输出)做参进行计算,而Q使用前一个 Decoder block 的输出计算(就是所谓交叉注意力)。
主要的区别在于其中 Self-Attention 的 K, V矩阵不是使用 上一个 Decoder block 的输出计算的,而是使用 Encoder 的编码信息矩阵 C 作为输入参数。
这样做的好处是在 Decoder 的时候,每一位单词都可以利用到 Encoder 所有单词的信息 (这些信息无需 Mask)。
第一个Masked Multi-Head Attention是为了得到之前已经预测输出的信息,
第二个Multi-Head Attention是通过当前的输入与经过encoder提取过的特征向量之间的关系来预测输出。
(3)最后有一个 Softmax层计算下一个翻译单词的概率。
Decoder block 最后的部分是利用 Softmax 预测下一个单词,在之前的网络层我们可以得到一个最终的输出 Z
实现代码:
输入数据预处理,需要安装nltk
pip install nltk
需要下载语料库,安装nltk_data
下载地址:https://gitee.com/iceliooo/nltk_data
import nltk
# 下载reuters语料库,这里可能下载不了,
nltk.download()
requirements.txt
torch==2.0.1
torchvision==0.15.2
onnx==1.14.0
onnxruntime==1.15.1
pycocotools==2.0.7
PyYAML==6.0.1
scipy==1.13.0
onnxslim==0.1.31
onnxruntime-gpu==1.18.0
gradio==4.31.5
opencv-python==4.9.0.80
psutil==5.9.8
py-cpuinfo==9.0.0
huggingface-hub==0.23.2
safetensors==0.4.3
tqdm~=4.67.1
ultralytics~=8.1.34
numpy~=1.26.4
matplotlib~=3.9.4
pillow~=10.4.0
pandas~=2.2.3
scikit-learn~=1.6.0
seaborn~=0.13.2
thop~=0.1.1-2209072238
requests~=2.32.3
nltk~=3.9.1
目录结构
import numpy as np
import torch
from collections import Counter
from nltk import word_tokenize
from torch.autograd import Variable
UNK=1
PAD=1
BATCH_SIZE=128
DEVICE='cpu'
PAD = 0 # 填充元素
def subsequent_mask(size):
attn_shape = (1, size, size)
# attn_shape =(1, 11, 11)
subsequent_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8')
# np.ones(attn_shape) 会生成一个形状为 attn_shape 的全为1的矩阵。
# np.triu() 函数将这个矩阵转换为上三角矩阵。
# 参数 k 控制了主对角线以上的偏移量。当 k=0 时,生成的是包含主对角线在内的上三角矩阵;当 k=1 时,生成的是主对角线以上偏移一个单位的上三角矩阵。
# 返回一个右上角(不含主对角线)为全False,左下角(含主对角线)为全True的subsequent_mask矩阵
# torch.from_numpy(subsequent_mask) 将 NumPy 数组 subsequent_mask 转换为 PyTorch 张量。== 0 表示对张量中的每个元素进行逐元素比较,检查是否等于0。
# 最终返回的是一个布尔类型的张量,其中每个元素都是与0比较的结果,即True或False。即等于0为True,等于1为False。
return torch.from_numpy(subsequent_mask) == 0
def seq_padding(X, padding=PAD):
"""
按批次(batch)对数据填充、长度对齐
"""
# 计算该批次各条样本语句长度
Length = [len(x) for x in X]
# 获取该批次样本中语句长度最大值
MaxLength = max(Length)
# 遍历该批次样本,如果语句长度小于最大长度,则用padding填充
return np.array([
np.concatenate([x, [padding] * (MaxLength - len(x))]) if len(x) < MaxLength else x for x in X
])
class Batch:
"""
批次类
1. 输入序列(源)
2. 输出序列(目标)
3. 构造掩码
"""
def __init__(self, src, trg=None, pad=PAD):
# 将src、trg转为tensor格式,将数据放到设备上并规范成整数类型
src = torch.from_numpy(src).to(DEVICE).long()
# src =tensor([[ 2, 16, 17, 18, 4, 3, 0],[ 2, 5, 6, 7, 8, 4, 3]])
trg = torch.from_numpy(trg).to(DEVICE).long()
# trg =tensor([[ 2, 22, 23, 24, 25, 26, 4, 3, 0, 0, 0], [ 2, 6, 7, 8, 9, 10, 11, 12, 13, 4, 3]])
self.src = src
# self.src= tensor([[ 2, 16, 17, 18, 4, 3, 0],[ 2, 5, 6, 7, 8, 4, 3]])
# (src != pad):这部分代码会生成一个与输入序列 src 形状相同的布尔张量,其中真实单词的位置为 True,填充部分的位置为 False。并在seq length前面增加一维,形成维度为 1×seq length 的矩阵
self.src_mask = (src != pad).unsqueeze(-2)
"""self.src_mask=tensor([[[ True, True, True, True, True, True, False]],
[[ True, True, True, True, True, True, True]]])"""
# self.src_mask.shape= torch.Size([2, 1, 7])
# 如果输出目标不为空,则需要对解码器使用的目标语句进行掩码
if trg is not None:
# 解码器使用的目标输入部分
self.trg = trg[:, : -1] # 去除最后一列
# self.trg=tensor([[ 2, 22, 23, 24, 25, 26, 4, 3, 0, 0],[ 2, 6, 7, 8, 9, 10, 11, 12, 13, 4]])
# 解码器训练时应预测输出的目标结果
self.trg_y = trg[:, 1:] # 去除第一列的
# self.trg_y=tensor([[22, 23, 24, 25, 26, 4, 3, 0, 0, 0],[ 6, 7, 8, 9, 10, 11, 12, 13, 4, 3]])
# 将目标输入部分进行注意力掩码
self.trg_mask = self.make_std_mask(self.trg, pad)
# 生成一个大小为self.trg.size(-1)的掩码矩阵,下三角为True,上三角为False
# 将应输出的目标结果中真实的词数进行统计
self.ntokens = (self.trg_y != pad).data.sum()
# self.ntokens=tensor(17)
# 掩码操作
# 这个操作的目的是在训练解码器时,确保解码器在预测当前时间步的词时只能依赖之前的词,而不能依赖未来的词
@staticmethod
def make_std_mask(tgt, pad):
"Create a mask to hide padding and future words."
tgt_mask = (tgt != pad).unsqueeze(-2)
# 这部分代码会生成一个与目标序列 tgt 形状相同的布尔张量,其中真实单词的位置为 True,填充部分的位置为 False。并在倒数第二维度上添加一个新维度。
# tgt_mask =tensor([[[True, True, True, True, True, True, True, True, True, True, True]]])
tgt_mask = tgt_mask & Variable(subsequent_mask(tgt.size(-1)).type_as(tgt_mask.data))
# subsequent_mask(tgt.size(-1)):生成一个大小为tgt.size(-1)的下三角形的矩阵,上三角部分为0,下三角部分和对角线为1。
# Variable(...).type_as(tgt_mask.data):将生成的下三角形矩阵转换为与目标序列掩码相同的数据类型和设备类型。
# (tgt_mask & ...): 对上面生成的下三角形矩阵与目标序列掩码进行逻辑与操作,得到最终的目标序列掩码。
return tgt_mask
class PrepareData:
def __init__(self, train_file, dev_file):
# 读取数据、分词
self.train_en, self.train_cn = self.load_data(train_file)
self.dev_en, self.dev_cn = self.load_data(dev_file)
# 构建词表
self.en_word_dict, self.en_total_words, self.en_index_dict = \
self.build_dict(self.train_en)
self.cn_word_dict, self.cn_total_words, self.cn_index_dict = \
self.build_dict(self.train_cn)
# 单词映射为索引
self.train_en, self.train_cn = self.word2id(self.train_en, self.train_cn, self.en_word_dict, self.cn_word_dict)
self.dev_en, self.dev_cn = self.word2id(self.dev_en, self.dev_cn, self.en_word_dict, self.cn_word_dict)
# 划分批次、填充、掩码
self.train_data = self.split_batch(self.train_en, self.train_cn, BATCH_SIZE)
self.dev_data = self.split_batch(self.dev_en, self.dev_cn, BATCH_SIZE)
def load_data(self, path):
"""
读取英文、中文数据
对每条样本分词并构建包含起始符和终止符的单词列表
形式如:en = [['BOS', 'i', 'love', 'you', 'EOS'], ['BOS', 'me', 'too', 'EOS'], ...]
cn = [['BOS', '我', '爱', '你', 'EOS'], ['BOS', '我', '也', '是', 'EOS'], ...]
"""
en = []
cn = []
with open(path, mode="r", encoding="utf-8") as f:
for line in f.readlines():
sent_en, sent_cn = line.strip().split("\t")
sent_en = sent_en.lower()
# sent_cn = cht_to_chs(sent_cn)
sent_en = ["BOS"] + word_tokenize(sent_en) + ["EOS"]
# 中文按字符切分
sent_cn = ["BOS"] + [char for char in sent_cn] + ["EOS"]
en.append(sent_en)
cn.append(sent_cn)
return en, cn
def build_dict(self, sentences, max_words=5e4):
"""
构造分词后的列表数据
构建单词-索引映射(key为单词,value为id值)
"""
# 统计数据集中单词词频
word_count = Counter([word for sent in sentences for word in sent])
# 按词频保留前max_words个单词构建词典
# 添加UNK和PAD两个单词
ls = word_count.most_common(int(max_words))
total_words = len(ls) + 2
word_dict = {w[0]: index + 2 for index, w in enumerate(ls)}
word_dict['UNK'] = UNK
word_dict['PAD'] = PAD
# 构建id2word映射
index_dict = {v: k for k, v in word_dict.items()}
return word_dict, total_words, index_dict
def word2id(self, en, cn, en_dict, cn_dict, sort=True):
"""
将英文、中文单词列表转为单词索引列表
`sort=True`表示以英文语句长度排序,以便按批次填充时,同批次语句填充尽量少
"""
length = len(en)
# 单词映射为索引
out_en_ids = [[en_dict.get(word, UNK) for word in sent] for sent in en]
out_cn_ids = [[cn_dict.get(word, UNK) for word in sent] for sent in cn]
# 按照语句长度排序
def len_argsort(seq):
"""
传入一系列语句数据(分好词的列表形式),
按照语句长度排序后,返回排序后原来各语句在数据中的索引下标
"""
return sorted(range(len(seq)), key=lambda x: len(seq[x]))
# 按相同顺序对中文、英文样本排序
if sort:
# 以英文语句长度排序
sorted_index = len_argsort(out_en_ids)
out_en_ids = [out_en_ids[idx] for idx in sorted_index]
out_cn_ids = [out_cn_ids[idx] for idx in sorted_index]
return out_en_ids, out_cn_ids
def split_batch(self, en, cn, batch_size, shuffle=True):
"""
划分批次
`shuffle=True`表示对各批次顺序随机打乱
"""
# 每隔batch_size取一个索引作为后续batch的起始索引
idx_list = np.arange(0, len(en), batch_size)
# 起始索引随机打乱
if shuffle:
np.random.shuffle(idx_list)
# 存放所有批次的语句索引
batch_indexs = []
for idx in idx_list:
"""
形如[array([4, 5, 6, 7]),
array([0, 1, 2, 3]),
array([8, 9, 10, 11]),
...]
"""
# 起始索引最大的批次可能发生越界,要限定其索引
batch_indexs.append(np.arange(idx, min(idx + batch_size, len(en))))
# 构建批次列表
batches = []
for batch_index in batch_indexs:
# 按当前批次的样本索引采样
batch_en = [en[index] for index in batch_index]
batch_cn = [cn[index] for index in batch_index]
# 对当前批次中所有语句填充、对齐长度
# 维度为:batch_size * 当前批次中语句的最大长度
batch_cn = seq_padding(batch_cn)
batch_en = seq_padding(batch_en)
# 将当前批次添加到批次列表
# Batch类用于实现注意力掩码
batches.append(Batch(batch_en, batch_cn))
return batches
if __name__ == '__main__':
DEV_FILE = '../data/en-cn/train.txt' # 训练集
TRAIN_FILE = "../data/en-cn/dev.txt" # 验证集
data = PrepareData(TRAIN_FILE, DEV_FILE) # 实例化类
"""类的一些成员变量
print(data.en_word_dict) #文件里所有英文组成一个字典。{'BOS': 2, 'EOS': 3, '.': 4, 'i': 5, 'the': 6, 'you': 7, 'to': 8, ......}
print(data.cn_word_dict) #文件里所有中文组成一个字典。{'BOS': 2, 'EOS': 3, '。': 4, '我': 5, '的': 6, '了': 7, ......}
print(data.cn_total_words)
print(data.en_total_words)
print(data.en_index_dict) #{2: 'BOS', 3: 'EOS', 4: '.', 5: 'i', 6: 'the', 7: 'you', 8: 'to', 9: 'a', 10: '?',。....}
print(data.cn_index_dict)
print(data.dev_data)
"""
print(data.dev_cn)#[[2, 5, 1273, 7, 4, 3], [2, 5, 93, 7, 924, 34, 1, 4, 3], [2, 5, 61, 153, 694, 479, 4, 3], [2, 15, 47, 51, 48, 4, 3],....]
#print(data.train_data) # 里面包含很多个batch,每个batch里有数据信息
# for index, batch in enumerate(data.train_data):
# print(batch.src) # 转为tensor的batch_en(每句话的英文单词在字典的索引构成的列表,且填充好)
# print(batch.trg) # 转为tensor的batch_cn(每句话的中文单词在字典的索引构成的列表,且填充好)
# print(batch.src_mask) # batch_en的掩码
# print(batch.trg_mask) # batch_cn的掩码
#
可能遇到的错误和解决
https://blog.csdn.net/lu_rong_qq/article/details/143409795
模型定义和训练:
import torch
import torch.nn as nn
import torch.optim as optim
import torch.utils.data as data
import math
import copy
import torch.nn.functional as F
from npl.PrepareData import PrepareData
from npl.parser import args
""" 多头注意力
MultiHeadAttention类封装了在Transformer模型中常用的多头注意力机制。它处理将输入分割成多个注意力头,对每个头应用注意力,
然后将结果组合。通过这样做,模型可以在不同尺度上捕获输入数据中的各种关系,提高模型的表达能力。
forward 过程:
1 应用线性变换:首先使用初始化中定义的权重将查询(Q)、键(K)和值(V)通过线性变换。
2 分割头:使用split_heads方法将转换后的Q、K、V分割成多个头。
3 应用缩放点积注意力:在分割的头上调用scaled_dot_product_attention方法。注意力分数是通过取查询(Q)和键(K)的点积,然后通过键的维度(d_k)的平方根进行缩放来计算的。
4 组合头:使用combine_heads方法将每个头的结果组合回单个张量。
5 应用输出变换:最后,组合的张量通过输出线性变换。
"""
class MultiHeadAttention(nn.Module):
def __init__(self, d_model, num_heads):
super(MultiHeadAttention, self).__init__()
# 确保模型维度(d_model)可以被注意力头数整除
assert d_model % num_heads == 0, "d_model必须能被num_heads整除"
# 初始化维度
self.d_model = d_model # 模型的维度
self.num_heads = num_heads # 注意力头的数量
self.d_k = d_model // num_heads # 每个头的键、查询和值的维度
# 用于转换输入的线性层
self.W_q = nn.Linear(d_model, d_model) # 查询转换
self.W_k = nn.Linear(d_model, d_model) # 键转换
self.W_v = nn.Linear(d_model, d_model) # 值转换
self.W_o = nn.Linear(d_model, d_model) # 输出转换
# 缩放点积注意力:scaled_dot_product_attention
def scaled_dot_product_attention(self, Q, K, V, mask=None):
# 计算注意力分数 (乘除法是缩放)
# 点积 这里,注意力分数是通过取查询(Q)和键(K)的点积,然后通过键的维度(d_k)的平方根进行缩放来计算的。根据论文公式
attn_scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)
# 应用掩码:如果提供了掩码,则将其应用于注意力分数以掩盖特定值(如填充的注意力)。
if mask is not None:
attn_scores = attn_scores.masked_fill(mask == 0, -1e9)
# 应用softmax以获得注意力概率,(计算注意力权重:注意力分数通过softmax函数传递,以将它们转换为总和为1的概率。)
attn_probs = torch.softmax(attn_scores, dim=-1)
# 计算输出:乘以值以获得最终输出,注意力的最终输出是通过将注意力权重乘以值(V)来计算的。
output = torch.matmul(attn_probs, V)
return output
def split_heads(self, x):
# 重塑输入以进行多头注意力
batch_size, seq_length, d_model = x.size()
return x.view(batch_size, seq_length, self.num_heads, self.d_k).transpose(1, 2)
def combine_heads(self, x):
# 将多个头重新组合成原始形状
batch_size, _, seq_length, d_k = x.size()
return x.transpose(1, 2).contiguous().view(batch_size, seq_length, self.d_model)
def forward(self, Q, K, V, mask=None):
# 应用线性变换并分割头
Q = self.split_heads(self.W_q(Q))
K = self.split_heads(self.W_k(K))
V = self.split_heads(self.W_v(V))
# 执行缩放点积注意力
attn_output = self.scaled_dot_product_attention(Q, K, V, mask)
# 组合头并应用输出变换
output = self.W_o(self.combine_heads(attn_output))
return output
class PositionwiseFeedForward(nn.Module):
"Implements FFN equation."
def __init__(self, d_model, d_ff, dropout=0.1):
super(PositionwiseFeedForward, self).__init__()
self.w_1 = nn.Linear(d_model, d_ff)
self.w_2 = nn.Linear(d_ff, d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
return self.w_2(self.dropout(self.w_1(x).relu()))
"""
位置感知前馈网络 (由两个带有ReLU激活函数的线性层组成)
PositionWiseFeedForward类定义了一个位置感知的前馈神经网络,由两个带有ReLU激活函数的线性层组成。
在Transformer模型的上下文中,这个前馈网络分别且相同地应用于每个位置。它有助于转换由注意力机制在Transformer中学习到的特征。
"""
class PositionWiseFeedForward(nn.Module):
def __init__(self, d_model, d_ff,dropout=0.1):
"""
参数:
:param d_model: 模型输入和输出的维度。
:param d_ff: 前馈网络中内层的维度。
self.fc1和self.fc2: 两个全连接(线性)层,输入和输出维度由d_model和d_ff定义。
self.relu: ReLU(修正线性单元)激活函数,在两个线性层之间引入非线性。
"""
super(PositionWiseFeedForward, self).__init__()
self.fc1 = nn.Linear(d_model, d_ff) # 定义两个模型组件fc1、fc2
self.fc2 = nn.Linear(d_ff, d_model) # 定义两个模型组件fc1、fc2
self.relu = nn.ReLU() # 定义激活函数
self.dropout = nn.Dropout(dropout)
def forward(self, x):
"""
x: 前馈网络的输入。
输入首先通过第一个线性层(fc1)。self.fc1()等价于 self.fc1.forward()
self.relu(...): fc1的输出然后通过ReLU激活函数。(ReLU将所有负值替换为零,为模型引入非线性。)
self.fc2(...): 激活的输出然后通过第二个线性层(fc2),产生最终输出。
"""
return self.fc2(self.dropout(self.relu(self.fc1.forward(x))))
"""
位置编码函数,根据论文公式
d_model: 模型输入的维度。
max_seq_length: 预先计算位置编码的最大序列长度。
前向方法简单地将位置编码添加到输入x。
它使用pe的前x.size(1)个元素,以确保位置编码与x的实际序列长度相匹配。
最后,将pe注册为缓冲区,这意味着它将是模块状态的一部分,但不会被视为可训练参数。
"""
class PositionalEncoding(nn.Module):
def __init__(self, d_model, max_seq_length):
"""
初始参数
:param d_model: 模型输入的维度。
:param max_seq_length: 预先计算位置编码的最大序列长度。
"""
super(PositionalEncoding, self).__init__()
# 用零填充的张量,将填充位置编码。
pe = torch.zeros(max_seq_length, d_model)
# 包含序列中每个位置的位置索引的张量。
position = torch.arange(0, max_seq_length, dtype=torch.float).unsqueeze(1)
# 用于以特定方式缩放位置索引的项。
div_term = torch.exp(torch.arange(0, d_model, 2).float() * -(math.log(10000.0) / d_model))
# 正弦函数应用于pe的偶数索引,余弦函数应用于奇数索引。
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
# 最后,将pe注册为缓冲区,这意味着它将是模块状态的一部分,但不会被视为可训练参数。
self.register_buffer('pe', pe.unsqueeze(0))
def forward(self, x):
return x + self.pe[:, :x.size(1)]
"""
EncoderLayer类定义了Transformer编码器的单层。它封装了多头自注意力机制,然后是位置感知前馈神经网络,带有残差连接、层归一化和dropout。
forward 处理步骤:
自注意力:输入x通过多头自注意力机制传递。
加法 & 归一化(自注意力后):注意力输出添加到原始输入(残差连接),然后是dropout和使用norm1的归一化。
前馈网络:前一步的输出通过位置感知前馈网络传递。
加法 & 归一化(前馈后):与步骤2类似,前馈输出添加到该阶段的输入(残差连接),然后是dropout和使用norm2的归一化。
输出:处理后的张量作为编码器层的输出返回。
"""
class EncoderLayer(nn.Module):
def __init__(self, d_model, num_heads, d_ff, dropout):
"""
初始参数:
:param d_model: 输入的维度。
:param num_heads: 多头注意力中的注意力头数。
:param d_ff: 位置感知前馈网络中内层的维度。
:param dropout: 用于正则化的dropout率。0.1
组件:
self.self_attn: 多头自注意力机制。
self.feed_forward: 位置感知前馈神经网络。
self.norm1和self.norm2: 层归一化,应用于平滑层的输入。
self.dropout: Dropout层,用于通过在训练期间随机将一些激活设置为零来防止过拟合。
"""
super(EncoderLayer, self).__init__()
self.self_attn = MultiHeadAttention(d_model, num_heads)
self.feed_forward = PositionWiseFeedForward(d_model, d_ff)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x, mask):
"""
:param x: 编码器层的输入。
:param mask: 可选掩码,用于忽略输入的某些部分。
:return:
下面 完全根据论文写
"""
attn_output = self.self_attn(x, x, x, mask) # 计算多头注意力
x = self.norm1(x + self.dropout(attn_output)) # 残差连接Add(输入+输出)和层归一化
ff_output = self.feed_forward(x) # 位置前馈网络
x = self.norm2(x + self.dropout(ff_output)) # 残差连接Add(输入+输出)和层归一化
return x
"""
DecoderLayer类定义了Transformer解码器的单层。它由多头自注意力机制、多头交叉注意力机制(关注编码器的输出)、位置感知前馈神经网络以及相应的残差连接、
层归一化和dropout层组成。这种组合使解码器能够根据编码器的表示生成有意义的输出,同时考虑目标序列和源序列。与编码器一样,通常会堆叠多个解码器层以形成完整的解码器部分。
forward
处理步骤:
目标序列上的自注意力:输入x通过自注意力机制处理。
加法 & 归一化(自注意力后):自注意力的输出添加到原始x,然后是dropout和使用norm1的归一化。
编码器输出上的交叉注意力:前一步归一化输出通过交叉注意力机制处理,该机制关注编码器的输出enc_output。
加法 & 归一化(交叉注意力后):交叉注意力的输出添加到该阶段的输入,然后是dropout和使用norm2的归一化。
前馈网络:前一步的输出通过前馈网络传递。
加法 & 归一化(前馈后):前馈输出添加到该阶段的输入,然后是dropout和使用norm3的归一化。
输出:处理后的张量作为解码器层的输出返回。
"""
class DecoderLayer(nn.Module):
def __init__(self, d_model, num_heads, d_ff, dropout):
"""
参数:
:param d_model: 输入的维度。
:param num_heads: 多头注意力中的注意力头数。
:param d_ff: 前馈网络中内层的维度。
:param dropout: 用于正则化的dropout率。
组件:
self.self_attn: 目标序列的多头自注意力机制。
self.cross_attn: 多头注意力机制,用于关注编码器的输出。
self.feed_forward: 位置感知前馈神经网络。
self.norm1, self.norm2, self.norm3: 层归一化组件。
self.dropout: 用于正则化的dropout层。
"""
super(DecoderLayer, self).__init__()
self.self_attn = MultiHeadAttention(d_model, num_heads)
self.cross_attn = MultiHeadAttention(d_model, num_heads)
self.feed_forward = PositionWiseFeedForward(d_model, d_ff)
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
self.norm3 = nn.LayerNorm(d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x, enc_output, src_mask, tgt_mask):
"""
输入:
:param x: 解码器层的输入。
:param enc_output: 对应编码器的输出(用于交叉注意力步骤)。
:param src_mask: 源掩码,用于忽略编码器输出的某些部分。
:param tgt_mask: 目标掩码,用于忽略解码器输入的某些部分,遮挡后面的单词。
:return:
"""
attn_output = self.self_attn(x, x, x, tgt_mask) # masked 多头注意力(参数:Q,K,V,MASK)
x = self.norm1(x + self.dropout(attn_output)) # 残差连接Add(输入+输出)和层归一化
# 同时接收来自编码器端的输出以及前一个掩码注意力层的输出,称为交叉注意力。
# Key和Value是使用编码器的输出进行投影的,Query是通过解码器前一层的输出进行投影所得。
attn_output = self.cross_attn(x, enc_output, enc_output, src_mask)
x = self.norm2(x + self.dropout(attn_output)) # 残差连接Add(输入+输出)和层归一化
ff_output = self.feed_forward(x) # 前馈net
x = self.norm3(x + self.dropout(ff_output)) # 残差连接Add(输入+输出)和层归一化
return x
"""
定义参数和组件的模型
Transformer类汇集了Transformer模型的各个组件,包括嵌入、位置编码、编码器层和解码器层。它为训练和推理提供了方便的接口,封装了多头注意力、前馈网络和层归一化的复杂性。
这个实现遵循标准的Transformer架构,适用于机器翻译、文本摘要等序列到序列任务。
"""
class Transformer(nn.Module):
"""
构造函数采用以下参数:
src_vocab_size: 源词汇表大小。 5000
tgt_vocab_size: 目标词汇表大小。5000
d_model: 模型嵌入的维度。 512
num_heads: 多头注意力机制中的注意力头数。 8
num_layers: 编码器和解码器的层数。 6
d_ff: 前馈网络中内层的维度。 2048
max_seq_length: 位置编码的最大序列长度。 100
dropout: 用于正则化的dropout率。 0.1
"""
def __init__(self, src_vocab_size, tgt_vocab_size, d_model, num_heads, num_layers, d_ff, max_seq_length, dropout):
super(Transformer, self).__init__()
self.encoder_embedding = nn.Embedding(src_vocab_size, d_model) # 源序列的嵌入层。
self.decoder_embedding = nn.Embedding(tgt_vocab_size, d_model) # 目标序列的嵌入层。
self.positional_encoding = PositionalEncoding(d_model, max_seq_length) # 位置编码组件。
# 编码器层的列表。
self.encoder_layers = nn.ModuleList([EncoderLayer(d_model, num_heads, d_ff, dropout) for _ in range(num_layers)])
# 解码器层的列表。
self.decoder_layers = nn.ModuleList([DecoderLayer(d_model, num_heads, d_ff, dropout) for _ in range(num_layers)])
# 将输出映射到目标词汇表大小的最终全连接(线性)层。输出函数
self.fc = nn.Linear(d_model, tgt_vocab_size)
self.dropout = nn.Dropout(dropout) # Dropout层。
def generate_mask(self, src, tgt):
src_mask = (src != 0).unsqueeze(1).unsqueeze(2)
tgt_mask = (tgt != 0).unsqueeze(1).unsqueeze(3)
seq_length = tgt.size(1)
# triu() diagonal=1 右移一位的上三角矩阵
nopeak_mask = (1 - torch.triu(torch.ones(1, seq_length, seq_length), diagonal=1)).bool()
tgt_mask = tgt_mask & nopeak_mask
return src_mask, tgt_mask
# forward
# 方法定义了Transformer的前向传递,采用源和目标序列,并产生输出预测。
# 输入嵌入和位置编码:首先使用各自的嵌入层嵌入源和目标序列,然后添加它们的位置编码。
# 编码器层:源序列通过编码器层传递,最终编码器输出表示处理过的源序列。
# 解码器层:目标序列和编码器的输出通过解码器层传递,得到解码器的输出。
# 最终线性层:解码器的输出使用全连接(线性)层映射到目标词汇表大小。
# 输出:最终输出是一个张量,表示模型对目标序列的预测。
#
def forward(self, src, tgt):
src_mask, tgt_mask = self.generate_mask(src, tgt)
src_embedded = self.dropout(self.positional_encoding(self.encoder_embedding(src)))
tgt_embedded = self.dropout(self.positional_encoding(self.decoder_embedding(tgt)))
enc_output = src_embedded
for enc_layer in self.encoder_layers:
enc_output = enc_layer(enc_output, src_mask)
dec_output = tgt_embedded
for dec_layer in self.decoder_layers:
dec_output = dec_layer(dec_output, enc_output, src_mask, tgt_mask)
output = self.fc(dec_output)
return output
"""
训练模型
超参数:这些值定义了Transformer模型的架构和行为:
src_vocab_size, tgt_vocab_size: 源和目标序列的词汇表大小,均设置为5000。
d_model: 模型嵌入的维度,设置为512。
num_heads: 多头注意力机制中的注意力头数,设置为8。
num_layers: 编码器和解码器的层数,设置为6。
d_ff: 前馈网络中内层的维度,设置为2048。
max_seq_length: 位置编码的最大序列长度,设置为200。
dropout: 用于正则化的dropout率,设置为0.1。
"""
if __name__ == '__main__':
TRAIN_FILE = '../data/en-cn/train.txt' # 训练集
DEV_FILE = "../data/en-cn/dev.txt" # 验证集
save_file = '../weights/model.pt' # 模型保存路径
d_model = 512 # 单词向量模型嵌入的维度,设置为512
num_heads = 8 # 多头注意力机制中的注意力头数,设置为8
num_layers = 6 # 编码器和解码器的层数,设置为6
d_ff = 2048 # 前馈网络中内层的维度,设置为2048
max_seq_length = 200 # 位置编码的最大序列长度,设置为200
dropout = 0.1 # 用于正则化的dropout率,设置为0.1
PAD = 0 # padding占位符的索引
UNK = 1 # 未登录词标识符的索引
epochs = 5 # 训练轮数
device = 'cpu'
data = PrepareData(TRAIN_FILE, DEV_FILE) # 处理训练数据,数据路径
tgt_vocab = len(data.cn_word_dict)
src_vocab = len(data.en_word_dict)
args.src_vocab = len(data.en_word_dict)
args.tgt_vocab = len(data.cn_word_dict)
args.save_file = save_file
print("src_vocab %d" % args.src_vocab)
print("tgt_vocab %d" % args.tgt_vocab)
print(">>>>>>> start train")
model = Transformer(src_vocab_size=src_vocab, tgt_vocab_size=tgt_vocab,
d_model=d_model, num_heads=num_heads,
num_layers=num_layers, d_ff=d_ff, max_seq_length=max_seq_length, dropout=dropout)
criterion = nn.CrossEntropyLoss(ignore_index=0)
optimizer = optim.Adam(model.parameters(), lr=0.0001, betas=(0.9, 0.98), eps=1e-9)
for epoch in range(args.epochs):
model.train()
total_tokens = 0.
total_loss = 0.
tokens = 0.
for i, batch in enumerate(data.train_data): # 获得batch块及其索引
optimizer.zero_grad() # 清除上一次迭代的梯度
#
x = model(batch.src, batch.trg)
# x= F.log_softmax(nn.Linear(d_model, tgt_vocab).forward(x1), dim=-1)
print(x)
# 损失通过将数据重塑为一维张量并使用交叉熵损失函数来计算。 ---- 计算误差
loss = criterion(x.contiguous().view(-1, x.size(-1)), batch.trg_y.contiguous().view(-1))
total_loss += loss # 计算所有batch的总体损失。
total_tokens += batch.ntokens # batch.ntokens:每个batch里有多少个组数据。这里记录所有batch里样本的总数。
loss.backward() # 计算损失相对于模型参数的梯度。 ---- 计算梯度
optimizer.step() # 使用计算出的梯度更新模型的参数。---- 更新
print(f"Epoch: {epoch+1}, Loss: {loss.item()}") # 打印当前周期号和该周期的损失值。
if i % 50 == 1:
tokens = 0 # 将50个batch的样本数清零。
# model.eval()
# print('>>>>> Evaluate')
# with torch.no_grad():
# loss = criterion(x.contiguous().view(-1, x.size(-1)), batch.trg_y.contiguous().view(-1))
# print('<<<<< Evaluate loss: %f' % loss)
torch.save(model.state_dict(), args.save_file)
命令参数解析辅助类:
import argparse
import torch
parser = argparse.ArgumentParser()
parser.add_argument('--train-file', default='../data/train.txt')
parser.add_argument('--dev-file', default='../data/dev.txt')
parser.add_argument('--UNK', default=0, type=int)
parser.add_argument('--PAD', default=1, type=int)
# TODO 常改动参数
parser.add_argument('--type', default='train') # 默认是训练模式, 若传递 "evaluate" 则对 dev数据集进行预测输出
parser.add_argument('--gpu', default=3, type=int) # gpu 卡号
parser.add_argument('--epochs', default=5, type=int) # 训练轮数
parser.add_argument('--layers', default=2, type=int) # transformer层数
parser.add_argument('--h-num', default=8, type=int) # multihead attention hidden层数
parser.add_argument('--batch-size', default=64, type=int)
parser.add_argument('--d-model', default=256, type=int)
parser.add_argument('--d-ff', default=1024, type=int)
parser.add_argument('--dropout', default=0.1, type=float)
parser.add_argument('--max-length', default=60, type=int)
parser.add_argument('--save-file', default='save/model.pt') # 模型保存位置
args = parser.parse_args()
device = torch.device(f"cuda:{args.gpu}" if torch.cuda.is_available() else "cpu")
args.device = device
源码和样本数据下载:https://github.com/hinesboy/transformer-simple.git
模型定义原理一样,写法略有不同。
归一化、掩码(mask)以及Transformer模型中的多头注意力机制,都是深度学习中非常重要的概念。
归一化
在神经网络中,归一化通常指的是将输入数据标准化到某个特定范围(如0和1之间),但更常见的做法是对数据进行标准化(standardization),即将数据转换为均值为0、方差为1的分布。这有助于提高模型的训练速度和稳定性,尤其是在使用梯度下降法时。
掩码(Mask)
在Transformer模型中,掩码的使用非常关键,主要有两种类型:padding mask 和 sequence mask。
Padding Mask:
在处理变长序列时,需要对输入序列进行填充,使得每个批次的输入具有相同的长度。填充通常用0来表示,这些填充的位置并不携带有用信息。
为了确保模型在注意力计算中不关注这些填充位置,padding mask会将这些位置的值加上一个非常大的负数(如负无穷),这样在经过softmax之后,这些位置的注意力权重将接近于0,确保它们不会影响模型的学习。
Sequence Mask:
在解码阶段,为了确保模型在生成当前时刻的输出时不能看到未来的信息,我们需要使用sequence mask。具体来说,对于时间步t,解码器的输出只能依赖于t之前的输出,而不能依赖于t之后的输出。
实现方法是构建一个上三角矩阵,上三角的值全为0,表示在计算注意力时,只允许关注当前时间步及之前的时间步,未来的信息被掩盖。
Transformer中的多头注意力
在Transformer模型中,Encoder和Decoder的多头注意力机制略有不同:
Encoder的多头注意力:
只需要使用padding mask,确保填充位置不会影响注意力计算。
Decoder的多头注意力:
需要同时使用padding mask和sequence mask。第一个Masked Multi-Head Attention用于获取之前已预测的输出信息,确保模型能够利用历史信息进行预测。
第二个Multi-Head Attention则是通过当前的输入信息与Encoder输出的特征向量进行交互,以生成下一个时间步的输出。
通过使用掩码,Transformer能够有效地处理变长序列并避免信息泄露。归一化则有助于提高训练的效率和效果。
多头注意力机制具有几个优点:
并行化:通过同时关注输入序列的不同部分,多头注意力显著加快了计算速度,使其比传统的注意力机制更加高效。
增强表示:每个注意力头都关注输入序列的不同方面,使模型能够捕捉各种模式和关系。这导致输入的表示更丰富、更强大,增强了模型理解和生成文本的能力。
改进泛化性:多头注意力使模型能够关注序列内的局部和全局依赖关系,从而提高了跨不同任务和领域的泛化性。
软注意力是一种用来衡量数据重要性的机制。在计算机中,软注意力通过一些数学函数来计算这个“关注”的程度,比如 softmax 或 sigmoid。
这是一种可以预测的方式,主要用在以下三种类型的注意力机制中:
通道注意力:它根据每个特征通道(或滤镜)来计算得分,根据不同的特征图来做判断。
空间注意力:这是针对图像的具体区域进行关注,而不是通道。它在目标检测、语义分割和人员重新识别等任务中非常有用。
自注意力:这个机制会比较输入数据中不同部分之间的关联。自注意力根据输入数据中两个部分的相似度来计算得分。
软注意力的方法通常通过软函数(如 softmax 和 sigmoid)来加权输入数据的不同部分。这种方式是可预测且可微的,这意味着可以通过反向传播进行训练。
通道、空间和时序注意力可以看作是作用于不同域的机制。
硬注意力和软注意力的主要区别在于硬注意力的随机性。硬注意力只选择一个特定的区域来关注,而不是像软注意力那样平均分配注意力。硬注意力的主要类型包括:
贝叶斯注意力 (Bayesian Attention):这种注意力使用贝叶斯统计模型来决定哪部分应该得到关注。就像你在做实验时,用概率来推断某个结果的可能性。经常用于解决视觉问题,因为它可以帮助模型在不确定的情况下做出更好的决策。
强化学习注意力 (Reinforced Attention):强化学习是一种基于奖励和惩罚的学习方式。利用这种方式来训练模型,帮助它选择最重要的部分。
高斯注意力 (Gaussian Attention):高斯注意力使用一种叫做 2D 高斯核的数学工具来计算注意力得分。
多模态注意力机制是一种用来处理多种不同类型数据的方法,比如文本和图像。它的主要特点是能够在不同模式之间生成注意力,也就是让模型知道在一堆不同数据中关注哪些部分。
常见的多模态注意力机制包括:
交叉注意力 (Cross Attention):这个机制通过比较不同模式之间的相关性来生成注意力得分。
Perceiver 模型:这是一个基于 Transformer 的多模态模型,专门处理大规模、多模态数据。它可以同时处理不同类型的数据,如文本、图像和音频。
多模态注意力机制的好处是它可以让模型从不同角度理解数据,帮助模型更全面地做出决策。