Transformer(Encoder编码器-Decoder解码器、多头注意力机制、多头自注意力机制、掩码张量、前馈全连接层、...
时间:2023-08-07 23:07:00
日萌社
人工智能AI:Keras PyTorch MXNet TensorFlow PaddlePaddle 深度学习实战(不定期更新)
Encoder编码器-Decoder解码器框架 Attention注意力机制
Pytorch:Transformer(Encoder编码器-Decoder解码器、多头注意力机制、多头自注意力机制、掩码张量、接层、标准化层、子层连接结构、pyitcast) part1
Pytorch:Transformer(Encoder编码器-Decoder解码器、多头注意力机制、多头自注意力机制、掩码张量、接层、标准化层、子层连接结构、pyitcast) part2
Pytorch:解码器端的Attention注意机制,seq2seq实现英译法任务的模型架构
BahdanauAttention注意机制,LuongAttention注意力机制
BahdanauAttention注意机制:基于seq2seq从西班牙语到英语的机器翻译任务,解码器Attention注意机制,seq2seq模型架构
图片描述生成任务,利用迁移学习实现图片描述生成过程,CNN编码器 RNN解码器(GRU)模型架构,BahdanauAttention注意机制,解码器端Attention注意力机制
注意机制,bmm运算
注意力机制 SENet、CBAM
机器翻译 MXNet(使用含注意力机制的编码器-解码器,即 Encoder编码器-Decoder解码器框架 Attention注意机制)
基于Seq2Seq中文聊天机器人编程实践(Encoder编码器-Decoder解码器框架 Attention注意机制)
基于Transformer文本情感分析编程实践(Encoder编码器-Decoder解码器框架 Attention注意力机制 Positional Encoding位置编码)
注:本文基于:Transformer文本情感分析编程实践(Encoder编码器-Decoder解码器框架 Attention注意力机制 Positional Encoding位置编码) 本文实现了Transformer的Model类型模型,实际上是改造过的特别版Transformer,因为Transformer的Model只实现了类型模型Encoder编码器, 没有相应的实现Decoder由于目前的解码器,Transformer的Model类型模型处理分类任务, 所以这里只用了Encoder编码器提取特性,最后,通过全连接层网络拟合分类。
建议使用下面文章的代码linux环境运行,window它可能无法运行
Pytorch:Transformer(Encoder编码器-Decoder解码器、多头注意力机制、多头自注意力机制、掩码张量、接层、标准化层、子层连接结构、pyitcast) part1
Pytorch:Transformer(Encoder编码器-Decoder解码器、多头注意力机制、多头自注意力机制、掩码张量、接层、标准化层、子层连接结构、pyitcast) part2
实现输入部分_1.py
""" 文本嵌入层的代码分析 """ """ 输入部分包括: 1.源文本嵌入层及其位置编码器 2.编码器的目标文本嵌入层及其位置 """ """ 1.输入部分 嵌入层: 嵌入层类Embeddings: Embeddings(单词嵌入维度, 词表大小) 嵌入层最终得到一个三维通过Embedding层的[batch size, 最大长度的句子max_len, 单词嵌入维度]单词嵌入张量。 1.embedding = nn.Embedding(词表大小, 单词嵌入维度) 创建Embedding嵌入层。 2.output = embedding(input) 批量句子中的单词索引值传入Embedding嵌入层,输出单词索引对应的词嵌入维度的权重向量。 2维[batch size, 最大长度的句子max_len]的input传入Embedding输出输出嵌入层[batch size, 最大长度的句子max_len, 单词嵌入维度]output。 3.output * math.sqrt(词嵌入维度) math.sqrt(词嵌入维度)作为Embedding嵌入层输出output缩放系数。 3.output * math.sqrt(词嵌入维度) math.sqrt(词嵌入维度)作为Embedding嵌入层输出output缩放系数。 该sqrt结果值越大,上述两者相乘的结果值就越大sqrt结果值越小,两者相乘的结果值越小。 2.输入部分 位置编码器: 位置编码层类PositionalEncoding: PositionalEncoding(单词嵌入维度, Dropout0丢弃比, 默认值max_len=5000) 位置编码层最终得到一个三维通过Embedding编码层的层和位置[batch size, 最大长度的句子max_len, 单词嵌入维度]单词嵌入张量。 1.pe = torch.zeros(max_len句子的最大长度, 单词嵌入维度) 二维矩阵初始化位置编码,shape为[max_len句子的最大长度, 单词嵌入维度]。 2.position = torch.arange(0., max_len).unsqueeze(1) 将绝对位置编码的2维矩阵初始化为一个shape为[max_len句子最大长度, 1]2D矩阵,元素值从0到0max_len-1。 torch.arange(0., max_len):得到一个1维shape为[max_len]元素值从0到0max_len-1。 torch.arange(0., max_len).unsqueeze(1)获得2Dshape为[max_len, 1]矩阵,元素值从0到0max_len-1。 arange获得连续自然数的一维方法shape为[max_len]向量:torch.arange(0., 3) 得到 tensor([0., 1., 2.])。 torch.arange(0., 3).unsqueeze(1)得到tensor([[0.], [1.], [2.]]),shape为[max_len, 1] 3.div_term = torch.exp(torch.arange(0., 单词嵌入维度, 2) * -(math.log(10000.0) / 单词嵌入维度)) 生成一个shape为[单词嵌入维度数除以2]的1维向量,向量中的每个数值都很小。 每个极小的向量值都有一个后缀e-0X,从e-01、e-02、e-03、e-04...不等。 每个极小的向量值都有一个后缀e-0X,从e-01、e-02、e-03、e-04...不等。e-01:10^(-1)即0.1。 1.torch.arange(0., 单词嵌入维度, 2): 生成一个shape为[字嵌入维度数除以2]的1维向量。 比如 torch.arange(0., 10, 2)得tensor([0., 2., 4., 6., 8.]) 2.-(math.log(10000.0) / 词嵌入维度): 作为缩放系数系数。首先,除法运算得到一个非常小的正值小数,然后取反得负数。 2维矩阵的作用是在绝对位置编码position将自然数缩放入足够小的数字中,有助于在随后的梯度下降过程中更快地收敛。 比如 math.log(10000.0) / 512 得到一个小的正值小数 0.017988946039015984 3.torch.arange(0., 单词嵌入维度, 2) * -(math.log(10000.0) / 词嵌入维度): 生成一个shape为[词嵌入维度数除以2]的1维向量,向量中每个数为极小的数值。 每个极小的向量值都有一个后缀e-0X,从e-01、e-02、e-03、e-04...不等。 每个极小的向量值都有一个后缀e-0X,从e-01、e-02、e-03、e-04...不等。 e-01:10^(-1)即0.1。 4.pe[:, 0::2] = torch.sin(position *div_term)
pe[:, 1::2] = torch.cos(position * div_term)
位置编码的2维矩阵中的数值为自然数经过缩放后的足够小的数值,有助于在之后的梯度下降过程中更快的收敛。
位置编码的2维矩阵的shape为[max_len句子最大长度, 词嵌入维度]。
sin正弦波和cos余弦波的值域范围都是1到-1很好的控制了嵌入数值的大小, 有助于梯度的快速计算。
1.position:shape为[max_len句子最大长度, 1]的2维矩阵,元素值为从0到max_len-1。
2.div_term:shape为[词嵌入维度数除以2]的1维向量,向量中每个数为极小的数值。
3.position * div_term:[max_len句子最大长度, 1] * [词嵌入维度数除以2] 得 [max_len句子最大长度, 词嵌入维度数除以2]
4.pe[:, 0::2] = sin正弦函数([max_len句子最大长度, 词嵌入维度数除以2])
把数据填充到位置编码的2维矩阵中的偶数列上
5.pe[:, 1::2] = cos余弦函数([max_len句子最大长度, 词嵌入维度数除以2])
把数据填充到位置编码的2维矩阵中的奇数列上
5.pe = pe.unsqueeze(0)
位置编码的2维矩阵增加一个维度,从[max_len句子最大长度, 词嵌入维度]变成[1, max_len句子最大长度, 词嵌入维度]。
变成3维后的位置编码矩阵目的是和经过Embedding后输出的三维矩阵进行相加。
6.register_buffer('pe', pe)
把3维的pe位置编码矩阵注册成模型的buffer。
我们把它认为是对模型效果有帮助的,但是却不是模型结构中超参数或者参数,不需要随着优化步骤进行更新的增益对象。
注册之后我们就可以在模型保存后重新加载时和模型结构与参数一同被加载。
7.embedding = embedding + Variable(pe[:, :embedding.size(1)], requires_grad=False)
三维的embedding嵌入词向量和三维的位置编码矩阵pe进行相加,最终得到经过了Embedding层和位置编码的嵌入词向量。
embedding:3维的[batch size, 句子最大长度max_len, 词嵌入维度]的批量文本序列的嵌入词向量。
pe:3维的[1, max_len句子最大长度, 词嵌入维度]的位置编码矩阵。
embedding.size(1):句子最大长度max_len。
pe[:, :embedding.size(1)]:
等同于pe[:, :x.size(1), :]的写法。
因为位置编码矩阵pe的第二维max_len句子最大长度的默认值为5000,一般已经大于实际可能的句子长度。
因此需要从位置编码矩阵pe中进行切片取出和embedding的最大句子长度max_len相同的第二维行数,其他维度的数量不变。
requires_grad=False:因为位置编码矩阵pe不需要进行梯度求解的,因此把requires_grad设置成false,最后使用Variable进行封装。
8.dropout(x)
经过了Embedding层和位置编码的嵌入词向量最终还要经过“p=丢弃率”的Dropout层。
"""
#======================================= 嵌入层 =========================================#
"""
文本嵌入层的作用:
无论是源文本嵌入还是目标文本嵌入,都是为了将文本中词汇的数字表示转变为向量表示,
希望在这样的高维空间捕捉词汇间的关系.
"""
# 导入必备的工具包
import torch
# 预定义的网络层torch.nn, 工具开发者已经帮助我们开发好的一些常用层,
# 比如,卷积层, lstm层, embedding层等, 不需要我们再重新造轮子.
import torch.nn as nn
# 数学计算工具包
import math
# torch中变量封装函数Variable.
from torch.autograd import Variable
# 定义Embeddings类来实现文本嵌入层,这里s说明代表两个一模一样的嵌入层, 他们共享参数.
# 该类继承nn.Module, 这样就有标准层的一些功能, 这里我们也可以理解为一种模式, 我们自己实现的所有层都会这样去写.
class Embeddings(nn.Module):
def __init__(self, d_model, vocab):
"""类的初始化函数, 有两个参数, d_model: 指词嵌入的维度, vocab: 指词表的大小."""
# 接着就是使用super的方式指明继承nn.Module的初始化函数, 我们自己实现的所有层都会这样去写.
super(Embeddings, self).__init__()
"""创建Embedding嵌入层:embedding = nn.Embedding(词表大小, 词嵌入维度)"""
# 之后就是调用nn中的预定义层Embedding, 获得一个词嵌入对象self.lut
self.lut = nn.Embedding(vocab, d_model) #nn.Embedding(词表大小, 词嵌入维度)
# 最后就是将d_model传入类中
self.d_model = d_model #词嵌入维度
"""
1.embedding = nn.Embedding(词表大小, 词嵌入维度)
创建Embedding嵌入层。
2.output = embedding(input)
批量句子中的单词索引值传入Embedding嵌入层,输出单词索引对应的词嵌入维度的权重向量。
2维[batch size, 句子最大长度max_len]的input传入Embedding嵌入层输出3维[batch size, 句子最大长度max_len, 词嵌入维度]的output。
3. output * math.sqrt(词嵌入维度)
math.sqrt(词嵌入维度)作为Embedding嵌入层输出output的缩放系数。
该sqrt结果值越大那么上述两者相乘的结果值就越大,该sqrt结果值越小那么上述两者相乘的结果值就越小。
"""
def forward(self, x):
"""可以将其理解为该层的前向传播逻辑,所有层中都会有此函数
当传给该类的实例化对象参数时, 自动调用该类函数
参数x: 因为Embedding层是首层, 所以代表输入给模型的文本通过词汇映射后的张量"""
"""
embedding(input) * math.sqrt(词嵌入维度)
math.sqrt(词嵌入维度)在此处相当于缩放系数,该sqrt结果值越大那么上述两者相乘的结果值就越大,
该sqrt结果值越小那么上述两者相乘的结果值就越小。
"""
# 将x传给self.lut并与根号下self.d_model相乘作为结果返回
return self.lut(x) * math.sqrt(self.d_model)
# #实例化参数:
# # 词嵌入维度是512维
# d_model = 512
# # 词表大小是1000
# vocab = 1000
# #输入参数:
# # 输入x是一个使用Variable封装的长整型张量, 形状是2 x 4
# x = Variable(torch.LongTensor([[100,2,421,508],[491,998,1,221]]))
# #调用:
# emb = Embeddings(d_model, vocab)
# embr = emb(x)
# print("embr:", embr)
# print("embr.shape:", embr.shape) #torch.Size([2, 4, 512])
#============================ 位置编码器 =========================================#
"""
位置编码器的作用:
因为在Transformer的编码器结构中, 并没有针对词汇位置信息的处理,因此需要在Embedding层后加入位置编码器,
将词汇位置不同可能会产生不同语义的信息加入到词嵌入张量中, 以弥补位置信息的缺失.
"""
"""
RuntimeError: exp_vml_cpu not implemented for 'Long'
把 div_term = torch.exp(torch.arange(0, d_model, 2) * -(math.log(10000.0) / d_model))
修改为 div_term = torch.exp(torch.arange(0., d_model, 2) * -(math.log(10000.0) / d_model))
就是int值的0修改为Float值的0.
RuntimeError: expected device cpu and dtype Float but got device cpu and dtype Long
把 position = torch.arange(0, max_len).unsqueeze(1)
修改为 position = torch.arange(0., max_len).unsqueeze(1)
就是int值的0修改为Float值的0.
"""
# 定义位置编码器类, 我们同样把它看做一个层, 因此会继承nn.Module
class PositionalEncoding(nn.Module):
def __init__(self, d_model, dropout, max_len=5000):
"""位置编码器类的初始化函数, 共有三个参数, 分别是d_model: 词嵌入维度,
dropout: 置0比率, max_len: 每个句子的最大长度"""
super(PositionalEncoding, self).__init__()
# 实例化nn中预定义的Dropout层, 并将dropout传入其中, 获得对象self.dropout
self.dropout = nn.Dropout(p=dropout)
""" 初始化一个位置编码矩阵:pe = torch.zeros(max_len句子最大长度, 词嵌入维度) """
# 初始化一个位置编码矩阵, 它是一个0阵,矩阵的大小是max_len x d_model.
pe = torch.zeros(max_len, d_model)
# 初始化一个绝对位置矩阵, 在我们这里,词汇的绝对位置就是用它的索引去表示.
# 所以我们首先使用arange方法获得一个连续自然数向量,然后再使用unsqueeze方法拓展向量维度使其成为矩阵,
# 又因为参数传的是1,代表矩阵拓展的位置,会使向量变成一个max_len x 1 的矩阵,
position = torch.arange(0., max_len).unsqueeze(1)
print("position.shape:",position.shape) # [max_len句子最大长度, 1]
# 绝对位置矩阵初始化之后,接下来就是考虑如何将这些位置信息加入到位置编码矩阵中,
# 最简单思路就是先将max_len x 1的绝对位置矩阵, 变换成max_len x d_model形状,然后覆盖原来的初始位置编码矩阵即可,
# 要做这种矩阵变换,就需要一个1xd_model形状的变换矩阵div_term,我们对这个变换矩阵的要求除了形状外,
# 还希望它能够将自然数的绝对位置编码缩放成足够小的数字,有助于在之后的梯度下降过程中更快的收敛。
# 这样我们就可以开始初始化这个变换矩阵了。
# 首先使用arange获得一个自然数矩阵, 但是细心的同学们会发现, 我们这里并没有按照预计的一样初始化一个1xd_model的矩阵,
# 而是有了一个跳跃,只初始化了一半即1xd_model/2 的矩阵。 为什么是一半呢,其实这里并不是真正意义上的初始化了一半的矩阵,
# 我们可以把它看作是初始化了两次,而每次初始化的变换矩阵会做不同的处理,
# 第一次初始化的变换矩阵分布在正弦波上,第二次初始化的变换矩阵分布在余弦波上,
# 并把这两个矩阵分别填充在位置编码矩阵的偶数和奇数位置上,组成最终的位置编码矩阵。
div_term = torch.exp(torch.arange(0., d_model, 2) * -(math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
# 这样我们就得到了位置编码矩阵pe, pe现在还只是一个二维矩阵,要想和embedding的输出(一个三维张量)相加,
# 就必须拓展一个维度,所以这里使用unsqueeze拓展维度.
pe = pe.unsqueeze(0)
print("pe.shape",pe.shape) #([1, 60, 512]
# 最后把pe位置编码矩阵注册成模型的buffer,什么是buffer呢,
# 我们把它认为是对模型效果有帮助的,但是却不是模型结构中超参数或者参数,不需要随着优化步骤进行更新的增益对象.
# 注册之后我们就可以在模型保存后重加载时和模型结构与参数一同被加载.
self.register_buffer('pe', pe)
"""
位置编码层类:PositionalEncoding(词嵌入维度, Dropout的置0丢弃比率, 默认值max_len=5000)
1.pe = torch.zeros(max_len句子最大长度, 词嵌入维度)
初始化一个位置编码的2维矩阵,shape为[max_len句子最大长度, 词嵌入维度]。
2.position = torch.arange(0., max_len).unsqueeze(1)
初始化一个绝对位置编码的2维矩阵,为一个shape为[max_len句子最大长度, 1]的2维矩阵,元素值为从0到max_len-1。
torch.arange(0., max_len):得到一个1维的shape为[max_len]的向量,元素值为从0到max_len-1。
torch.arange(0., max_len).unsqueeze(1):得到一个2维shape为[max_len, 1]的矩阵,元素值为从0到max_len-1。
arange方法获得一个连续自然数的一维的shape为[max_len]向量:torch.arange(0., 3) 得到 tensor([0., 1., 2.])。
torch.arange(0., 3).unsqueeze(1)得到tensor([[0.], [1.], [2.]]),shape为[max_len, 1]
3.div_term = torch.exp(torch.arange(0., 词嵌入维度, 2) * -(math.log(10000.0) / 词嵌入维度))
生成一个shape为[词嵌入维度数除以2]的1维向量,向量中每个数为极小的数值。
向量中每个极小的数值都带一个后缀e-0X,从e-01、e-02、e-03、e-04、...不等。e-01:10^(-1)即0.1。
1.torch.arange(0., 词嵌入维度, 2):
生成一个shape为[词嵌入维度数除以2]的1维向量。
比如 torch.arange(0., 10, 2)得tensor([0., 2., 4., 6., 8.])
2.-(math.log(10000.0) / 词嵌入维度):
该负数作为一个缩放系数。首先除法运算得到一个很小的正值小数然后取反得负数。
作用为将绝对位置编码的2维矩阵position中的自然数缩放成足够小的数字,有助于在之后的梯度下降过程中更快的收敛。
比如 math.log(10000.0) / 512 得一个很小的正值小数 0.017988946039015984
3.torch.arange(0., 词嵌入维度, 2) * -(math.log(10000.0) / 词嵌入维度):
生成一个shape为[词嵌入维度数除以2]的1维向量,向量中每个数为极小的数值。
向量中每个极小的数值都带一个后缀e-0X,从e-01、e-02、e-03、e-04、...不等。
e-01:10^(-1)即0.1。
4.pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
位置编码的2维矩阵中的数值为自然数经过缩放后的足够小的数值,有助于在之后的梯度下降过程中更快的收敛。
位置编码的2维矩阵的shape为[max_len句子最大长度, 词嵌入维度]。
sin正弦波和cos余弦波的值域范围都是1到-1很好的控制了嵌入数值的大小, 有助于梯度的快速计算。
1.position:shape为[max_len句子最大长度, 1]的2维矩阵,元素值为从0到max_len-1。
2.div_term:shape为[词嵌入维度数除以2]的1维向量,向量中每个数为极小的数值。
3.position * div_term:[max_len句子最大长度, 1] * [词嵌入维度数除以2] 得 [max_len句子最大长度, 词嵌入维度数除以2]
4.pe[:, 0::2] = sin正弦函数([max_len句子最大长度, 词嵌入维度数除以2])
把数据填充到位置编码的2维矩阵中的偶数列上.
5.pe[:, 1::2] = cos余弦函数([max_len句子最大长度, 词嵌入维度数除以2])
把数据填充到位置编码的2维矩阵中的奇数列上
5.pe = pe.unsqueeze(0)
位置编码的2维矩阵增加一个维度,从[max_len句子最大长度, 词嵌入维度]变成[1, max_len句子最大长度, 词嵌入维度]。
变成3维后的位置编码矩阵目的是和经过Embedding后输出的三维矩阵进行相加。
6.register_buffer('pe', pe)
把3维的pe位置编码矩阵注册成模型的buffer。
我们把它认为是对模型效果有帮助的,但是却不是模型结构中超参数或者参数,不需要随着优化步骤进行更新的增益对象。
注册之后我们就可以在模型保存后重新加载时和模型结构与参数一同被加载。
7.embedding = embedding + Variable(pe[:, :embedding.size(1)], requires_grad=False)
三维的embedding嵌入词向量和三维的位置编码矩阵pe进行相加,最终得到经过了Embedding层和位置编码的嵌入词向量。
embedding:3维的[batch size, 句子最大长度max_len, 词嵌入维度]的批量文本序列的嵌入词向量。
pe:3维的[1, max_len句子最大长度, 词嵌入维度]的位置编码矩阵。
embedding.size(1):句子最大长度max_len。
pe[:, :embedding.size(1)]:
等同于pe[:, :x.size(1), :]的写法。
因为位置编码矩阵pe的第二维max_len句子最大长度的默认值为5000,一般已经大于实际可能的句子长度。
因此需要从位置编码矩阵pe中进行切片取出和embedding的最大句子长度max_len相同的第二维行数,其他维度的数量不变。
requires_grad=False:因为位置编码矩阵pe不需要进行梯度求解的,因此把requires_grad设置成false,最后使用Variable进行封装。
8.dropout(x)
经过了Embedding层和位置编码的嵌入词向量最终还要经过“p=丢弃率”的Dropout层。
"""
def forward(self, x):
# print("self.pe[:, :x.size(1)]",self.pe[:, :x.size(1)].shape) #torch.Size([1, 4, 512])
# print("self.pe[:, :x.size(1)]",self.pe[:, :x.size(1), :].shape) #torch.Size([1, 4, 512])
"""forward函数的参数是x, 表示文本序列的词嵌入表示"""
# 在相加之前我们对pe做一些适配工作, 将这个三维张量的第二维也就是句子最大长度的那一维将切片到与输入的x的第二维相同即x.size(1),
# 因为我们默认max_len为5000一般来讲实在太大了,很难有一条句子包含5000个词汇,所以要进行与输入张量的适配.
# 最后使用Variable进行封装,使其与x的样式相同,但是它是不需要进行梯度求解的,因此把requires_grad设置成false.
x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False)
# 最后使用self.dropout对象进行'丢弃'操作, 并返回结果.
return self.dropout(x)
"""
Variable(torch.zeros(...), requires_grad=True/False)
1.torch.autograd.Variable是autograd的核心类,Variable整合了Tensor,并整合了反向传播的相关实现。
2.Variable包含3个属性:
data:保存了Tensor,是本体真实数据。
grad:保存了上述data属性值的梯度值,与data属性的形状一致。
grad_fn:指向Function对象,用于反向传播时的梯度计算之用。
3.requires_grad
1.requires_grad=True:在反向传播时会对所封装的Tensor数据进行梯度求解。
2.requires_grad=False:在反向传播时不会对所封装的Tensor数据进行梯度求解。
3.比如输入的样本数据和目标标签数据使用Variable进行封装的同时而又不需要求梯度时,那么便可以设置requires_grad=False,
那么它是不需要进行梯度求解的。
"""
# # 实例化参数:
# # 词嵌入维度是512维
# d_model = 512
# # 置0比率为0.1
# dropout = 0.1
# # 句子最大长度
# max_len = 60
# # 输入参数:
# # 输入x是Embedding层的输出的张量, 形状是2 x 4 x 512
# x = embr #torch.Size([2, 4, 512])
# Variable containing:
# ( 0 ,.,.) =
# 35.9321 3.2582 -17.7301 ... 3.4109 13.8832 39.0272
# 8.5410 -3.5790 -12.0460 ... 40.1880 36.6009 34.7141
# -17.0650 -1.8705 -20.1807 ... -12.5556 -34.0739 35.6536
# 20.6105 4.4314 14.9912 ... -0.1342 -9.9270 28.6771
#
# ( 1 ,.,.) =
# 27.7016 16.7183 46.6900 ... 17.9840 17.2525 -3.9709
# 3.0645 -5.5105 10.8802 ... -13.0069 30.8834 -38.3209
# 33.1378 -32.1435 -3.9369 ... 15.6094 -29.7063 40.1361
# -31.5056 3.3648 1.4726 ... 2.8047 -9.6514 -23.4909
# [torch.FloatTensor of size 2x4x512]
# # 调用:
# # 位置编码层类:PositionalEncoding(词嵌入维度, Dropout的置0丢弃比率, 默认值max_len=5000)
# pe = PositionalEncoding(d_model, dropout, max_len)
# pe_result = pe(x)
# print("pe_result:", pe_result)
"""
>>> torch.arange(0., 5)
tensor([0., 1., 2., 3., 4.])
>>> torch.arange(0., 5).shape
torch.Size([5])
>>> torch.arange(0., 5).unsqueeze(1).shape
torch.Size([5, 1])
"""
"""
>>> a = torch.arange(0., 10, 2)
>>> print(a) #tensor([0., 2., 4., 6., 8.])
tensor([0., 2., 4., 6., 8.])
>>> print(a.shape)
torch.Size([5])
>>> math.log(10000.0) / 512
0.017988946039015984
>>> torch.exp(torch.arange(0., 10, 2) * -(math.log(10000.0) / 512))
tensor([1.0000, 0.9647, 0.9306, 0.8977, 0.8660])
"""
#=========================绘制词汇向量中特征的分布曲线=========================#
"""
绘制词汇向量中特征的分布曲线
每条颜色的曲线代表某一个词汇中的特征在不同位置的含义.
保证同一词汇随着所在位置不同它对应位置嵌入向量会发生变化.
正弦波和余弦波的值域范围都是1到-1这又很好的控制了嵌入数值的大小, 有助于梯度的快速计算.
"""
import matplotlib.pyplot as plt
import numpy as np
"""
1.pe = PositionalEncoding(词嵌入维度, Dropout的置0丢弃比率, 默认值max_len=5000)
位置编码层类PositionalEncoding(词嵌入维度, Dropout的置0丢弃比率, 默认值max_len=5000)
位置编码层最终得到一个三维的经过了Embedding层和位置编码层的[batch size, 句子最大长度max_len, 词嵌入维度]的词嵌入张量。
2.embedding = Variable(torch.zeros(1, 100, 20))
值全为0的三维的[batch size, 句子最大长度max_len, 词嵌入维度]的词嵌入张量
3.output = pe(embedding)
传入值全为0的三维的词嵌入张量,那么经过位置编码层的output输出值仅为包含位置编码信息的三维张量,实际并不包含Embedding层的词嵌入信息。
4.plt.plot(np.arange(100), y[0, :, 4:8].data.numpy())
定义画布的横纵坐标, 横坐标到100的长度,纵坐标是某一个词汇中的词嵌入维特征在不同句子长度下对应的值。
保证同一词汇随着所在位置不同它对应位置嵌入向量会发生变化。
因为词嵌入维度总共有20维, 我们这里只查看词嵌入维度的4、5、6、7维的值。
"""
# # 创建一张15 x 5大小的画布
# plt.figure(figsize=(15, 5))
#
# # 实例化PositionalEncoding类得到pe对象, 输入参数是20和0
# # PositionalEncoding(词嵌入维度, Dropout的置0丢弃比率, 默认值max_len=5000)
# pe = PositionalEncoding(20, 0)
#
# # 然后向pe传入被Variable封装的tensor, 这样pe会直接执行forward函数,
# # 且这个tensor里的数值都是0, 被处理后相当于位置编码张量
# y = pe(Variable(torch.zeros(1, 100, 20)))
#
# # 然后定义画布的横纵坐标, 横坐标到100的长度, 纵坐标是某一个词汇中的某维特征在不同长度下对应的值
# # 因为总共有20维之多, 我们这里只查看4,5,6,7维的值.
# plt.plot(np.arange(100), y[0, :, 4:8].data.numpy())
# #查看所有维的值
# # plt.plot(np.arange(100), y[0, :, :].data.numpy())
#
# # 在画布上填写维度提示信息
# plt.legend(["dim %d"%p for p in [4,5,6,7]])
# plt.show()
掩码张量_2.py
"""
编码器部分:
1.由N个编码器层堆叠而成
2.每个编码器层由两个子层连接结构组成
3.第一个子层连接结构包括一个多头自注意力子层和规范化层以及一个残差连接
4.第二个子层连接结构包括一个前馈全连接子层和规范化层以及一个残差连接
"""
"""
掩码张量
1.掩代表遮掩,码就是我们张量中的数值,它的尺寸不定,里面一般只有1和0的元素,代表位置被遮掩或者不被遮掩,
至于是0位置被遮掩还是1位置被遮掩可以自定义,因此它的作用就是让另外一个张量中的一些数值被遮掩,
也可以说被替换, 它的表现形式是一个张量.
2.在transformer中, 掩码张量的主要作用应用在attention注意力计算中的注意力得分张量scores上。
在生成注意力得分张量scores中的值的其计算过程有可能是已知了未来信息而计算得到的,
而未来信息被看到是因为训练时会把整个输出结果都一次性进行Embedding,
但是理论上解码器的的输出却不是一次就能产生最终结果的,而是一次次通过上一次结果综合得出的,
因此,未来的信息可能被提前利用,所以我们会对attention注意力计算中的注意力得分张量scores中的值进行遮掩。
3.掩码张量的可视化输出效果分析
通过观察可视化掩码张量中的方阵, 值为1的部分为黄色代表信息会被遮掩, 值为0的部分为紫色代表信息不会被遮掩。
以矩阵的从左上角到右下角的对角线所在的值和在对角线左下方的所有值均置为1,在对角线右上方的所有值均置为0。
横坐标代表每个目标词汇的位置, 纵坐标代表可查看的目标单词的位置;
在横坐标0的位置向纵轴上望过去一列都是黄色的, 代表全部信息会被遮掩,说明第一个目标单词还没有产生。
在横坐标1的位置向纵轴上望过去一列中纵坐标0的位置为紫色代表信息不会被遮掩,
说明第一个目标单词已经产生,其他后面位置的单词看不到, 以此类推。
在横坐标2的位置向纵轴上望过去一列中纵坐标0、1的位置为紫色代表信息不会被遮掩,
说明第一、二个目标单词已经产生,其他后面位置的单词看不到, 以此类推。
4.scores = scores.masked_fill(mask == 0, -1e9)
掩码张量mask中值为1的部分对应到注意力得分张量scores中相同位置的值则保持不变,代表注意力得分张量scores中的该部分信息不会被遮掩。
掩码张量mask中值为0的部分对应到注意力得分张量scores中相同位置的值则全置为-1e9极小值,代表注意力得分张量scores中的该部分信息会被遮掩。
通过掩码张量mask得知, 值为1的部分代表信息不会被遮掩, 值为0的部分代表信息会被遮掩。
5.源输入数据的掩码张量
进行源数据遮掩的原因并非是抑制信息泄漏,而是遮蔽掉对结果没有意义的字符而产生的注意力值,
把注意力集中在源数据的某一部分,不让源数据的注意力被分散,以此提升模型效果和训练速度。
6.目标数据的掩码张量
形状为[batch size, 句子最大长度max_len, 句子最大长度max_len]。
目标数据掩码张量用于对目标数据进行遮掩,因为此时模型可能还没有生成任何目标数据。
比如在解码器准备生成第一个字符或词汇时,我们其实已经传入了第一个字符''以便计算损失,
但是我们不希望在生成第一个字符时模型能利用这个信息,因此我们会将目标数据的第一个字符遮掩。
同样生成第二个字符或词汇时,模型只能使用目标数据的第一个字符或词汇信息,
不允许模型使用第二个字符以及之后的信息。
目标数据的掩码张量只能看到当前单词前面的单词,但看不到当前单词和后面的单词。
比如预测第三个单词,只能第一第二个单词,看不到当前第三个单词和后面的单词。
比如目标语句[我,爱,北京,天安门],那么解码器预测"我"时,解码器输入的第一个单词是''以便计算损失,
那么只看到第一个单词'',看不到"我"。预测"爱"时,解码器输入的第二个单词是'我',
那么只看到['','我'],看不到"爱"。
7.Bert: 判断一句话是否完整!
source: 一句话
target: 这句话的最后一个标点符号
next_char == '.?!'
next_char == ''
"""
# 导入必备的工具包
import torch
import numpy as np
import matplotlib.pyplot as plt
"""
mask掩码张量的形状可以定义为[batch size, 句子最大长度max_len, 句子最大长度max_len],
因为mask需要和注意力得分张量scores进行匹配形状:scores.masked_fill(mask == 0, -1e9)
1.attn_shape = (1, size, size)
定义掩码张量的形状为[1, size, size],最后两个维度形成一个方阵,即[1, 句子最大长度max_len, 句子最大长度max_len]。
该掩码张量的目的为向后遮掩。
2.subsequent_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8')
1.np.ones(attn_shape):掩码张量中全部值初始化为1。
2.np.triu的k=1:
以矩阵的从左上角到右下角的对角线整体向右上方移动一格作为标准线,标准线所在的值和在标准线右上方的所有值原封不动,
而在标准线左下方的所有值均置为0。
3.np.triu(初始化值为1掩码张量, k=1):
以掩码张量的从左上角到右下角的对角线整体向右上方移动一格作为标准线,标准线所在的值和在标准线右上方的所有值原封不动均为1,
而在标准线左下方的所有值均置为0,形成右上方值全为1的三角阵,其他位置值全为0。
4.astype('uint8'):为了节约空间,使其中的数据类型变为unit8(无符号8位整型)
3.torch.from_numpy(1 - subsequent_mask)
掩码张量中的0值变成1,1值变成0。
即以矩阵的从左上角到右下角的对角线所在的值和在对角线左下方的所有值均置为1,在对角线右上方的所有值均置为0。
通过可视化掩码张量得知, 值为1的部分为黄色代表信息会被遮掩, 值为0的部分为紫色代表信息不会被遮掩。
1.1 - subsequent_mask
表示1减去subsequent_mask掩码张量中的任何值,相当于掩码张量中的0值变成1,1值变成0。
原理是1减掩码张量中的0值等于1,1减掩码张量中的1值等于0。
2.torch.from_numpy(numpy类型变量):把numpy的ndarray 转换为 pytorch的tensor
tensor类型变量.numpy():把pytorch的tensor 转换为 numpy的ndarray
"""
def subsequent_mask(size):
"""生成向后遮掩的掩码张量, 参数size是掩码张量最后两个维度的大小, 它的最后两维形成一个方阵"""
# 在函数中, 首先定义掩码张量的形状
attn_shape = (1, size, size)
# 然后使用np.ones方法向这个形状中添加1元素,形成上三角阵, 最后为了节约空间,
# 再使其中的数据类型变为无符号8位整形unit8
subsequent_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8')
# 最后将numpy类型转化为torch中的tensor, 内部做一个1 - 的操作,
# 在这个其实是做了一个三角阵的反转, subsequent_mask中的每个元素都会被1减,
# 如果是0, subsequent_mask中的该位置由0变成1
# 如果是1, subsequent_mask中的该位置由1变成0
return torch.from_numpy(1 - subsequent_mask)
# #输入实例:
# # 生成的掩码张量的最后两维的大小
# size = 5
# #调用
# mask = subsequent_mask(size)
# print("mask:", mask)
# # tensor([[[1, 0, 0, 0, 0],
# # [1, 1, 0, 0, 0],
# # [1, 1, 1, 0, 0],
# # [1, 1, 1, 1, 0],
# # [1, 1, 1, 1, 1]]], dtype=torch.uint8)
#
# plt.figure(figsize=(5,5))
# plt.imshow(subsequent_mask(20)[0])
# plt.show()
"""
np.tril(input, k=-1/0/1)
k=-1:以矩阵的从左上角到右下角的对角线整体向左下方移动一格作为标准线,标准线所在的值和在标准线左下方的所有值原封不动,而在标准线右上方的所有值均置为0。
k=0:以矩阵的从左上角到右下角的对角线作为标准线,标准线所在的值和在标准线左下方的所有值原封不动,而在标准线右上方的所有值均置为0。
k=1:以矩阵的从左上角到右下角的对角线整体向右上方移动一格作为标准线,标准线所在的值和在标准线左下方的所有值原封不动,而在标准线右上方的所有值均置为0。
np.triu(input, k=-1/0/1)
k=-1:以矩阵的从左上角到右下角的对角线整体向左下方移动一格作为标准线,标准线所在的值和在标准线右上方的所有值原封不动,而在标准线左下方的所有值均置为0。
k=0:以矩阵的从左上角到右下角的对角线作为标准线,标准线所在的值和在标准线右上方的所有值原封不动,而在标准线左下方的所有值均置为0。
k=1:以矩阵的从左上角到右下角的对角线整体向右上方移动一格作为标准线,标准线所在的值和在标准线右上方的所有值原封不动,而在标准线左下方的所有值均置为0。
---------------------------------------------------------------------
>>> np.triu([[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]], k=-1)
array([[ 1, 2, 3, 4],
[ 5, 6, 7, 8],
[ 0, 10, 11, 12],
[ 0, 0, 15, 16]])
>>> np.triu([[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]], k=0)
array([[ 1, 2, 3, 4],
[ 0, 6, 7, 8],
[ 0, 0, 11, 12],
[ 0, 0, 0, 16]])
>>> np.triu([[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]], k=1)
array([[ 0, 2, 3, 4],
[ 0, 0, 7, 8],
[ 0, 0, 0, 12],
[ 0, 0, 0, 0]])
---------------------------------------------------------------------
>>> np.tril([[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]], k=-1)
array([[ 0, 0, 0, 0],
[ 5, 0, 0, 0],
[ 9, 10, 0, 0],
[13, 14, 15, 0]])
>>> np.tril([[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]], k=0)
array([[ 1, 0, 0, 0],
[ 5, 6, 0, 0],
[ 9, 10, 11, 0],
[13, 14, 15, 16]])
>>> np.tril([[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]], k=1)
array([[ 1, 2, 0, 0],
[ 5, 6, 7, 0],
[ 9, 10, 11, 12],
[13, 14, 15, 16]])
---------------------------------------------------------------------
>>> np.triu([[1,2,3],[4,5,6],[7,8,9],[10,11,12]], k=-1)
array([[ 1, 2, 3],
[ 4, 5, 6],
[ 0, 8, 9],
[ 0, 0, 12]])
>>> np.triu([[1,2,3],[4,5,6],[7,8,9],[10,11,12]], k=0)
array([[ 1, 2, 3],
[ 0, 5, 6],
[ 0, 0, 9],
[ 0, 0, 0]])
>>> np.triu([[1,2,3],[4,5,6],[7,8,9],[10,11,12]], k=1)
array([[ 0, 2, 3],
[ 0, 0, 6],
[ 0, 0, 0],
[ 0, 0, 0]])
"""
注意力机制_3.py
"""
1.注意力:
我们观察事物时,之所以能够快速判断一种事物(当然允许判断是错误的),
是因为我们大脑能够很快把注意力放在事物最具有辨识度的部分从而作出判断,
而并非是从头到尾的观察一遍事物后,才能有判断结果. 正是基于这样的理论,就产生了注意力机制.
2.注意力计算规则:
它需要三个指定的输入Q(query), K(key), V(value), 然后通过公式得到注意力的计算结果,
这个结果代表query在key和value作用下的表示. 而这个具体的计算规则有很多种, 我这里只介绍我们用到的这一种.
3.注意力机制:
注意力机制是注意力计算规则能够应用的深度学习网络的载体, 除了注意力计算规则外,
还包括一些必要的全连接层以及相关张量处理, 使其与应用网络融为一体.
使用自注意力计算规则的注意力机制称为自注意力机制.
4.Q, K, V的比喻解释:
Q是一段准备被概括的文本; K是给出的提示; V是大脑中的对提示K的延伸。当Q=K=V时, 称作自注意力机制。
假如我们有一个问题: 给出一段文本query,使用一些关键词对它进行描述!
为了方便统一正确答案,这道题可能预先已经给大家写出了一些关键词作为提示。其中这些给出的提示信息就可以看作是key,
而整个的文本信息就相当于是query。而value的含义则更抽象,可以比作是你看到这段文本信息后,脑子里浮现的答案信息。
这里我们又假设大家最开始都不是很聪明,第一次看到这段文本后脑子里基本上浮现的信息就只有提示信息key,
因此此时可以认为key与value基本是相同的,但是随着我们对这个问题的深入理解,通过我们的不断思考,脑子里浮现的答案信息越来越多,
并且能够开始对这段文本也就是query开始进行提取关键信息进行表示,那么这就是注意力作用的过程。
通过这个过程,我们最终脑子里的value已经和一开始的value发生了变化。
根据提示信息key生成了query的关键词表示方法,也就是一种特征表示方法。
刚刚我们说到key和value一般情况下默认是相同,与query是不同的,这种是我们一般的注意力输入形式,
就如同我们的刚刚的例子,使用一般注意力机制,是使用不同于给定文本query的关键词来表示它。
但有一种特殊情况,也就是query与key和value相同,这种情况我们称为自注意力机制,
而自注意力机制,需要用给定文本query自身来表达自己,也就是说你需要从给定文本中抽取关键词来表述它, 相当于对文本query自身的一次特征提取。
5.注意力计算规则的函数: attention
它的输入就是Q,K,V以及mask和dropout, mask用于掩码, dropout用于随机置0.
它的输出有两个, query的注意力表示以及注意力张量.
"""
import torch
import math
import torch.nn.functional as F
# torch中变量封装函数Variable.
from torch.autograd import Variable
from day08.输入部分实现_1 import PositionalEncoding
from day08.输入部分实现_1 import Embeddings
from day08.掩码张量_2 import subsequent_mask
"""
tensor变量.masked_fill(掩码张量mask == 0, -1e9)
1.掩码张量mask中值为1的部分对应到tensor变量中相同位置的值则保持不变,代表信息不会被遮掩。
而掩码张量mask中值为0的部分对应到tensor变量中相同位置的值则全置为-1e9极小值,代表信息会被遮掩。
subsequent_mask返回的掩码张量mask中0值的位置对应到tensor变量中同样的位置上的值均会被置换为-1e9极小值,代表信息会被遮掩。
2.编码器输入的掩码张量
定义源数据编码器输入的初始化掩码张量, 因为元素都是1, 在我们这里1代表不遮掩,因此相当于对源数据没有任何遮掩。
source_mask = Variable(torch.ones(1, 1, 10))
3.通过掩码张量mask得知, 值为1的部分代表信息不会被遮掩, 值为0的部分代表信息会被遮掩。
# tensor([[[1, 0, 0, 0, 0],
# [1, 1, 0, 0, 0],
# [1, 1, 1, 0, 0],
# [1, 1, 1, 1, 0],
# [1, 1, 1, 1, 1]]], dtype=torch.uint8)
-------------------------------------------------------------------------
attention(query, key, value, mask=None, dropout=None)
如果为自注意机制,那么Q(query)=K(key)=V(value)
Q(query):经过了Embedding层和位置编码层的[batch size, 句子最大长度max_len, 词嵌入维度]的词嵌入张量。
K(key)和V(value):Q是一段准备被概括的文本,那么K是给出的提示; V是大脑中的对提示K的延伸。
mask:掩码张量mask
dropout:nn.Dropout(p=置0丢弃率)
dropout本身是对模型结构中的节点数进行随机抑制的比率,
又因为节点被抑制等效就是该节点的输出都是0,因此也可以把dropout看作是对输出矩阵的随机置0的比率。
-------------------------------------------------------------------------
如果为自注意机制,那么Q(query)=K(key)=V(value),
Q/K/V均为经过了Embedding层和位置编码层的[batch size, 句子最大长度max_len, 词嵌入维度]的词嵌入张量。
1.scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
1.K(key)的转置为:[batch size, 词嵌入维度, 句子最大长度max_len]
2.Q(query)和K(key)的转置进行点积:
那么 [batch size, 句子最大长度max_len, 词嵌入维度] * [batch size, 词嵌入维度, 句子最大长度max_len],
得到 [batch size, 句子最大长度max_len, 句子最大长度max_len]
2.matmul(p_attn, value)
即 p_attn 和 Q(query)进行点积运算,
那么 [batch size, 句子最大长度max_len, 句子最大长度max_len] * [batch size, 句子最大长度max_len, 词嵌入维度],
得到 [batch size, 句子最大长度max_len, 词嵌入维度]
-------------------------------------------------------------------------
1.d_k = query.size(-1):d_k即为 词嵌入维度
Q(query):经过了Embedding层和位置编码层的[batch size, 句子最大长度max_len, 词嵌入维度]的词嵌入张量
query.size(-1):取出词嵌入维度
2.scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
Q(query)和K(key)的转置进行点积乘的结果再除以缩放系数sqrt(d_k),这种计算方法也称为缩放点积注意力计算,得到注意力得分张量scores
1.key.transpose(-2, -1):最后两个维度之间互换,即K(key)进行转置
2.matmul(query, key.transpose(-2, -1)):Q(query)和K(key)的转置进行点积乘
3.math.sqrt(d_k):作为缩放系数,d_k(词嵌入维度)进行开根号
3.scores = scores.masked_fill(mask == 0, -1e9)
掩码张量mask中值为1的部分对应到注意力得分张量scores中相同位置的值则保持不变,代表注意力得分张量scores中的该部分信息不会被遮掩。
掩码张量mask中值为0的部分对应到注意力得分张量scores中相同位置的值则全置为-1e9极小值,代表注意力得分张量scores中的该部分信息会被遮掩。
通过掩码张量mask得知, 值为1的部分代表信息不会被遮掩, 值为0的部分代表信息会被遮掩。
源输入数据的掩码张量
进行源数据遮掩的原因并非是抑制信息泄漏,而是遮蔽掉对结果没有意义的字符而产生的注意力值,
把注意力集中在源数据的某一部分,不让源数据的注意力被分散,以此提升模型效果和训练速度。
目标数据的掩码张量
形状为[batch size, 句子最大长度max_len, 句子最大长度max_len]。
目标数据掩码张量用于对目标数据进行遮掩,因为此时模型可能还没有生成任何目标数据。
比如在解码器准备生成第一个字符或词汇时,我们其实已经传入了第一个字符''以便计算损失,
但是我们不希望在生成第一个字符时模型能利用这个信息,因此我们会将目标数据的第一个字符遮掩。
同样生成第二个字符或词汇时,模型只能使用目标数据的第一个字符或词汇信息,
不允许模型使用第二个字符以及之后的信息。
目标数据的掩码张量只能看到当前单词前面的单词,但看不到当前单词和后面的单词。
比如预测第三个单词,只能第一第二个单词,看不到当前第三个单词和后面的单词。
比如目标语句[我,爱,北京,天安门],那么解码器预测"我"时,解码器输入的第一个单词是''以便计算损失,
那么只看到第一个单词'',看不到"我"。预测"爱"时,解码器输入的第二个单词是'我',
那么只看到['','我'],看不到"爱"。
4.p_attn = F.softmax(scores, dim = -1)
对注意力得分张量scores的最后一个维度的值进行转换为概率值
5.p_attn = dropout(p_attn)
对p_attn张量中的值按照丢弃率为百分之多少的进行置为0
6.torch.matmul(p_attn, value), p_attn
注意力计算的最后一步:p_attn张量和V(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)
# print("d_k:",d_k) #64
# 按照注意力公式, 将query与key的转置相乘, 这里面key是将最后两个维度进行转置,
# 再除以缩放系数根号下d_k, 这种计算方法也称为缩放点积注意力计算.
# 得到注意力得分张量scores
scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
# print("scores.shape",scores.shape) #torch.Size([2, 8, 4, 4])
# 接着判断是否使用掩码张量
if mask is not None:
# 使用tensor的masked_fill方法, 将掩码张量和scores张量每个位置一一比较, 如果掩码张量处为0
# 则对应的scores张量用-1e9这个值来替换, 如下演示
scores = scores.masked_fill(mask == 0, -1e9)
# print("scores.shape:", scores.shape) #torch.Size([2, 4, 4])
# print("scores:",scores)
# tensor([[[1.4670e+04, -1.0000e+09, -1.0000e+09, -1.0000e+09],
# [-7.1877e+02, 1.3407e+04, -1.0000e+09, -1.0000e+09],
# [-7.7895e+02, 1.1335e+03, 1.3097e+04, -1.0000e+09],
# [-1.8603e+02, -5.8361e+02, 1.7998e+02, 1.1442e+04]],
#
# [[1.1710e+04, -1.0000e+09, -1.0000e+09, -1.0000e+09],
# [4.9352e+02, 1.3066e+04, -1.0000e+09, -1.0000e+09],
# [-7.1906e+02, 6.3984e+02, 1.3662e+04, -1.0000e+09],
# [6.2098e+02, 3.5394e+02, -5.2597e+02, 1.3532e+04]]],
# grad_fn= < MaskedFillBackward0 >)
# 对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
# #实例化参数:
# # 词嵌入维度是512维
# d_model = 512
# # 词表大小是1000
# vocab = 1000
# #输入参数:
# # 输入x是一个使用Variable封装的长整型张量, 形状是2 x 4
# x = Variable(torch.LongTensor([[100,2,421,508],[491,998,1,221]]))
# #调用:
# emb = Embeddings(d_model, vocab)
# embr = emb(x)
# # print("embr:", embr)
# # print("embr.shape:", embr.shape) #torch.Size([2, 4, 512])
#
# # 实例化参数:
# # 词嵌入维度是512维
# d_model = 512
# # 置0比率为0.1
# dropout = 0.1
# # 句子最大长度
# max_len = 60
# # 输入参数:
# # 输入x是Embedding层的输出的张量, 形状是2 x 4 x 512
# x = embr #torch.Size([2, 4, 512])
# pe = PositionalEncoding(d_model, dropout, max_len)
# pe_result = pe(x)
# # print("pe_result:", pe_result)
# print("pe_result.shape:", pe_result.shape) #torch.Size([2, 4, 512])
#===================================================
#输入参数:
# 我们令输入的query, key, value都相同, 位置编码的输出
# query = key = value = pe_result
#调用:
# attn, p_attn = attention(query, key, value)
# print("attn:", attn)
# print("p_attn:", p_attn)
#===================================================
#带有mask的输入参数:
# query = key = value = pe_result
# # 令mask为一个2x4x4的零张量
# mask = Variable(torch.zeros(2, 4, 4))
# #调用:
# attn, p_attn = attention(query, key, value, mask=mask)
# print("attn:", attn)
# print("p_attn:", p_attn)
#===================================================
# #输入实例:
# # 生成的掩码张量的最后两维的大小
# size = 4
# #调用
# mask = subsequent_mask(size)
# print("subsequent_mask.shape:", mask.shape) #torch.Size([1, 4, 4])
# print("subsequent_mask:", mask)
# # tensor([[[1, 0, 0, 0],
# # [1, 1, 0, 0],
# # [1, 1, 1, 0],
# # [1, 1, 1, 1]]], dtype=torch.uint8)
# #带有mask的输入参数:
# query = key = value = pe_result
# # print("pe_result.shape",pe_result.shape) #torch.Size([2, 4, 512])
# #调用:
# attn, p_attn = attention(query, key, value, mask=mask)
# print("attn.shape:", attn.shape) #torch.Size([2, 4, 512])
# print("p_attn.shape:", p_attn.shape) #torch.Size([2, 4, 4])
# print("attn:", attn)
# print("p_attn:", p_attn)
#===================================================
"""
>>> input = Variable(torch.randn(5, 5))
>>> input
Variable containing:
2.0344 -0.5450 0.3365 -0.1888 -2.1803
1.5221 -0.3823 0.8414 0.7836 -0.8481
-0.0345 -0.8643 0.6476 -0.2713 1.5645
0.8788 -2.2142 0.4022 0.1997 0.1474
2.9109 0.6006 -0.6745 -1.7262 0.6977
[torch.FloatTensor of size 5x5]
>>> mask = Variable(torch.zeros(5, 5))
>>> mask
Variable containing:
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
0 0 0 0 0
[torch.FloatTensor of size 5x5]
>>> input.masked_fill(mask == 0, -1e9)
Variable containing:
-1.0000e+09 -1.0000e+09 -1.0000e+09 -1.0000e+09 -1.0000e+09
-1.0000e+09 -1.0000e+09 -1.0000e+09 -1.0000e+09 -1.0000e+09
-1.0000e+09 -1.0000e+09 -1.0000e+09 -1.0000e+09 -1.0000e+09
-1.0000e+09 -1.0000e+09 -1.0000e+09 -1.0000e+09 -1.0000e+09
-1.0000e+09 -1.0000e+09 -1.0000e+09 -1.0000e+09 -1.0000e+09
[torch.FloatTensor of size 5x5]
----------------------------------------
mask = subsequent_mask(size)
print("mask:", mask)
# tensor([[[1, 0, 0, 0, 0],
# [1, 1, 0, 0, 0],
# [1, 1, 1, 0, 0],
# [1, 1, 1, 1, 0],
# [1, 1, 1, 1, 1]]], dtype=torch.uint8)
input = Variable(torch.randn(5, 5))
print("input:", input)
# input: tensor([[ 0.7355, -1.7779, -0.9368, 0.8789, 0.6941],
# [-1.3478, -0.8884, 0.1631, 1.3854, 0.1796],
# [-0.6873, -0.7180, 0.1878, 0.7216, 0.3455],
# [ 0.9504, -0.4424, 1.5504, 0.1826, -2.4970],
# [ 0.2762, 0.7762, 0.1749, -0.9793, -0.1307]])
scores = input.masked_fill(mask == 0, -1e9)
print("scores:", scores)
# scores: tensor([[[ 7.3554e-01, -1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09],
# [-1.3478e+00, -8.8837e-01, -1.0000e+09, -1.0000e+09, -1.0000e+09],
# [-6.8731e-01, -7.1800e-01, 1.8781e-01, -1.0000e+09, -1.0000e+09],
# [ 9.5039e-01, -4.4245e-01, 1.5504e+00, 1.8260e-01, -1.0000e+09],
# [ 2.7617e-01, 7.7617e-01, 1.7487e-01, -9.7926e-01, -1.3075e-01]]])
scores = input.masked_fill(mask == 0, 100)
print("scores:", scores)
# scores: tensor([[[ -0.6732, 100.0000, 100.0000, 100.0000, 100.0000],
# [ -0.5004, 0.2380, 100.0000, 100.0000, 100.0000],
# [ 0.6119, -0.3316, -1.3848, 100.0000, 100.0000],
# [ -0.2672, 0.3029, 0.8813, 1.5983, 100.0000],
# [ -0.3998, -0.9995, 2.9381, -0.4206, -0.7675]]])
"""
多头注意力机制_4.py
"""
多头注意力机制:
从多头注意力的结构图中,貌似这个所谓的多个头就是指多组线性变换层,其实并不是,只有使用了一组线性变化层,
即三个变换张量对Q,K,V分别进行线性变换,这里对Q,K,V分别进行线性变换操作并不会改变原有张量的尺寸,因此每个变换矩阵都是方阵N*N的,
得到输出结果后,多头的作用才开始显现,每个头开始从词义层面分割输出的张量,
也就是每个头都想获得一组Q,K,V进行注意力机制的计算,每个头只获得句子中的每个词的一部分表示,
也就是只分割了最后一维的词嵌入向量. 这就是所谓的多头,将每个头的获得的输入送到注意力机制中, 就形成多头注意力机制.
多头注意力机制的作用:
这种结构设计能让每个注意力机制去优化每个词汇的不同特征部分,从而均衡同一种注意力机制可能产生的偏差,
让词义拥有来自更多元的表达,实验表明可以从而提升模型效果.
"""
# 用于深度拷贝的copy工具包
import copy
import torch
import torch.nn as nn
# torch中变量封装函数Variable.
from torch.autograd import Variable
from day08.注意力机制_3 import attention
from day08.输入部分实现_1 import PositionalEncoding
from day08.输入部分实现_1 import Embeddings
# 首先需要定义克隆函数, 因为在多头注意力机制的实现中, 用到多个结构相同的线性层.
# 我们将使用clone函数将他们一同初始化在一个网络层列表对象中. 之后的结构中也会用到该函数.
def clones(module, N):
"""用于生成相同网络层的克隆函数, 它的参数module表示要克隆的目标网络层, N代表需要克隆的数量"""
# 在函数中, 我们通过for循环对module进行N次深度拷贝, 使其每个module成为独立的层,
# 然后将其放在nn.ModuleList类型的列表中存放.
return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])
"""
MultiHeadedAttention 多头注意力机制
1.Q(query)、K(key)、V(value)分别对应一个Linear线性层,多头注意力结构中的最后一个输出层也为Linear线性层,
多头注意力结构一共需要4个Linear线性层,每个Linear线性层结构相同但内存上互相独立。
2.多头注意力中的多头定义为head,每个头都分配等量的“单词在词嵌入维度上的”特征信息,
即每个头分别从词嵌入维度的词向量中所分配得到的等量特征信息,因此需要保证 embedding_dim // head 能整除。
3.输入到attention函数中进行多头注意力计算的传入参数Q(query)、K(key)、V(value)的形状均为[batch size, head, 句子最大长度max_len, d_k],
第三维的句子长度维度max_len和第四维的词向量维度d_k相邻,这样多头注意力机制才能找到词义与句子位置的关系。
从attention函数中可以看到,利用的正是query/key/value四维输入的第三维和第四维。
第二维的head代表每个头,那么每个头分别拥有所有每个单词(第三维)的一部分等量的词向量特征信息(第四维),
也就是说这样我们就得到了每个头(第二维)的词向量特征信息(第三维和第四维)。
1.MultiHeadedAttention(head, embedding_dim, dropout=0.1)
head:多头注意力中的多头
embedding_dim:词嵌入维度/词向量维度
dropout=0.1:置0比率,默认值0.1
dropout本身是对模型结构中的节点数进行随机抑制的比率,
又因为节点被抑制等效就是该节点的输出都是0,因此也可以把dropout看作是对输出矩阵的随机置0的比率。
2.MultiHeadedAttention的__init__
1.assert embedding_dim % head == 0
d_k = embedding_dim // head
assert用于断言。判断词嵌入维度embedding_dim能否整除多头注意力中的多头head。
因为后面需要给多头head中的每个头都分配等量的“单词在词嵌入维度上的”特征信息,
也就是每个头都分配有 embedding_dim / head 的等量特征信息。
比如head=8,embedding_dim=512,那么每个头都分配得到512/8=64维的词向量维度的特征信息。
2.其他
head = head:多头注意力中的多头head数量
attn = None:初始化注意力张量p_attn
dropout = nn.Dropout(p=置0丢弃率)
dropout本身是对模型结构中的节点数进行随机抑制的比率,
又因为节点被抑制等效就是该节点的输出都是0,因此也可以把dropout看作是对输出矩阵的随机置0的比率。
3.linears = nn.ModuleList([copy.deepcopy(nn.Linear(embedding_dim, embedding_dim)) for _ in range(指定生成的线性层数量)])
import copy:深度拷贝的copy工具包
因为在多头注意力机制的实现中, 需要用到多个结构相同但内存上互相独立的线性层。
那么通过深拷贝的方式复制出多个结构相同但内存上互相独立的线性层。
并且最后将多个线性层都一同初始化封装到同一个nn.ModuleList网络层列表对象中。
1.通过nn.Linear(输入维度,输出维度)创建线性层对象,线性层内部拥有一个可用于对输入张量进行维度变换的矩阵,
该矩阵形状为"输入维度X输出维度",输入张量与变换矩阵进行相乘后线性层便输出指定输出维度的张量。
2.nn.Linear(embedding_dim词向量维度, embedding_dim词向量维度)
线性层内部拥有一个“embedding_dim * embedding_dim”的变换矩阵,可用于对输入张量进行维度