Transformer(三)---编码器部分实现
目录
一、编码器介绍
二、掩码张量
三、注意力机制
四、多头注意力机制
五、前馈全连接层
六、规范化层
七、子层连接结构
八、编码器层
九、编码器
一、编码器介绍
编码器部分:由N个编码器层堆叠而成。每个编码器层由两个子层连接结构组成,第一个子层连接结构包括一个多头子注意力子层和规范化层以及一个残差连接;第二个子层连接结构包括一个前馈全连接子层和规范化层以及一个残差连接。
二、掩码张量
掩码张量介绍:掩代表遮掩,码就是我们张量中的数值,它的尺寸不定,里面一般只有1和0的元素,代表位置被遮掩或者不被遮掩,至于是0位置被遮掩还是1位置被遮掩可以自定义,因此它的作用就是让另外一个张量中的一些数值被遮掩,也可以说被替换, 它的表现形式是一个张量.
掩码张量作用:在transformer中, 掩码张量的主要作用在应用attention(将在下一小节讲解)时,有一些生成的attention张量中的值计算有可能已知了未来信息而得到的,未来信息被看到是因为训练时会把整个输出结果都一次性进行Embedding,但是理论上解码器的的输出却不是一次就能产生最终结果的,而是一次次通过上一次结果综合得出的,因此,未来的信息可能被提前利用. 所以,我们会进行遮掩. 关于解码器的有关知识将在后面的章节中讲解.
上三角矩阵和np.triu函数演示:
# nn.triu()函数功能介绍
# def triu(m, k)# m:表示一个矩阵# K:表示对角线的起始位置(k取值默认为0)# return: 返回函数的上三角矩阵
import numpy as np
def dm_test_nptriu():# 测试产生上三角矩阵print(np.triu([[1, 1, 1, 1, 1],[2, 2, 2, 2, 2],[3, 3, 3, 3, 3],[4, 4, 4, 4, 4],[5, 5, 5, 5, 5]], k=1))print(np.triu([[1, 1, 1, 1, 1],[2, 2, 2, 2, 2],[3, 3, 3, 3, 3],[4, 4, 4, 4, 4],[5, 5, 5, 5, 5]], k=0))print(np.triu([[1, 1, 1, 1, 1],[2, 2, 2, 2, 2],[3, 3, 3, 3, 3],[4, 4, 4, 4, 4],[5, 5, 5, 5, 5]], k=-1))
dm_test_nptriu()
生成掩码函数:
import torch
# 下三角矩阵:生成字符时,希望模型不要使用当前字符和后面的字符。# 使用遮掩mask,防止未来的信息可能被提前利用# 实现方法:1 - 上三角矩阵
def subsequent_mask(size):# 产生上三角矩阵 产生一个方阵subsequent_mask = np.triu(m=np.ones((1,size,size)),k=1).astype('uint8')# 返回下三角矩阵 torch.from_numpy(1-subsequent_mask)return torch.from_numpy(1-subsequent_mask)
def dm_test_subsequent_mask():# 产生5*5的下三角矩阵size = 5sm = subsequent_mask(size)print('下三角矩阵--->\n', sm)
dm_test_subsequent_mask()
tensor.mask_fill函数演示:
input = torch.randn(5,5)
mask = subsequent_mask(5) # 生成下三角矩阵
input.masked_fill(mask==0,-1e9)
可见,主对角线以上的元素都被-inf替代。
三、注意力机制
我们这里使用的注意力的计算规则:
import torch.nn.functional as F
def attention(query,key,value,mask=None,dropout=None):# query,key,value:代表注意力的三个输入张量# mask:代表掩码张量# dropout:传入的dropout实例化对象# 1 求查询张量特征尺寸大小d_k = query.size()[-1]# 2 求查询张量q的权重分布scores q@k^T/math.sqrt(d_k)# [2,4,512]@[2,512,4]-->[2,4,4]scores = torch.matmul(query,key.transpose(-2,-1))/math.sqrt(d_k)# 3 是否对权重分布scores 进行 masked_fillif mask is not None:# 根据mask矩阵0的位置 对scores矩阵对应位置进行掩码scores = scores.masked_fill(mask==0,-1e9)# 4 求查询张量q的权重分布p_attn = F.softmax(scores,dim=-1)# 5 是否对p_attn进行dropoutif dropout is not None:p_attn = dropout(p_attn)# 返回 查询张量q的注意力结果表示 bmm-matmul运算 ,注意力查询张量q的权重分布p_attn# [2,4,4]*[2,4,512]-->[2,4,512]return torch.matmul(p_attn,value),p_attn
四、多头注意力机制
将模型分为多个头, 可以形成多个子空间, 让模型去关注不同方面的信息, 最后再将各个方面的信息综合起来得到更好的效果。这种结构设计能让每个注意力机制去优化每个词汇的不同特征部分,从而均衡同一种注意力机制可能产生的偏差,让词义拥有来自更多元的表达,实验表明可以从而提升模型效果.
import copy
# 深度copy模型 输入模型对象和copy的个数 存储到模型列表中
def clones(module,N):return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])
class MultiHeadAttention(nn.Module):def __init__(self,head,embedding_dim,dropout=0.1):super(MultiHeadAttention,self).__init__()# 确认数据特征能否被整除 eg:特征尺寸512 % 头数8assert embedding_dim % head == 0# 计算每个头特征尺寸 特征尺寸//头数8 = 64self.d_k = embedding_dim // head# 多少头数self.head = head# 四个线性层self.linears = clones(nn.Linear(embedding_dim,embedding_dim),4)# 注意力权重分布self.attn = None# dropout层self.dropout = nn.Dropout(p=dropout)def forward(self,query,key,value,mask=None):# 若使用掩码,则掩码增加一个维度 [8,4,4]--->[1,8,4,4]if mask is not None:mask = mask.unsqueeze(0)# 求数据多少行 eg:[2,4,512] 则batch_size=2batch_size = query.size(0)# 数据形状变化[2,4,512]--->[2,4,8,64]--->[2,8,4,64]# 4代表4个单词 8代表8个头 让句子长度4和句子特征64靠在一起 更有利于捕捉句子特征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))]# 注意力结果表示x形状[2,8,4,64] 注意力权重attn形状:[2,8,4,4]x,self.attn = attention(query,key,value,mask=mask,dropout=self.dropout)# 数据形状变化 [2,8,4,64] -->[2,4,8,64]--->[2,4,512]# transpose(1,2) 操作可能会使张量变为不连续的,因此在调用 view 方法之前,需要先用 contiguous() 确保张量在内存中连续,这样 view 才能正常工作。x = x.transpose(1,2).contiguous().view(batch_size,-1,self.head*self.d_k)# 返回最后变化后的结果 [2,4,512]--->[2,4,512]return self.linears[-1](x)
五、前馈全连接层
在Transformer中前馈全连接层就是具有两层线性层的全连接网络.
前馈全连接层的作用:考虑注意力机制可能对复杂过程的拟合程度不够, 通过增加两层网络来增强模型的能力。
class PositionwiseFeedForward(nn.Module):def __init__(self,d_model,d_ff,dropout=0.1):# d_model 第一个线性层的输入维度# d_ff 第二个线性层的输出维度super(PositionwiseFeedForward,self).__init__()self.w1 = nn.Linear(d_model,d_ff)self.w2 = nn.Linear(d_ff,d_model)self.dropout = nn.Dropout(p=dropout)def forward(self,x):return self.w2(self.dropout(F.relu(self.w1(x))))
六、规范化层
它是所有深层网络模型都需要的标准网络层,因为随着网络层数的增加,通过多层的计算后参数可能开始出现过大或过小的情况,这样可能会导致学习过程出现异常,模型可能收敛非常的慢。因此都会在一定层数后接规范化层进行数值的规范化,使其特征数值在合理范围内。
class LayerNorm(nn.Module):def __init__(self,features,eps=1e-6):super(LayerNorm,self).__init__()# 定义 a2 规范化层的系数 y=kx+b中的kself.a2 = nn.Parameter(torch.ones(features))# 定义b2 规范化层的系数 y=kx+b中的bself.b2 = nn.Parameter(torch.zeros(features))self.eps = epsdef forward(self,x):# 对数据求均值 保持形状不变# [2,4,512] ---> [2,4,1]mean = x.mean(-1,keepdim=True)# 对数据求方差 保持形状不变# [2,4,512] ---> [2,4,1]std = x.std(-1,keepdim=True)# 对数据进行标准化变换 反向传播可学习参数a2,b2y = self.a2*(x-mean)/(std+self.eps)+self.b2return y
LayerNorm(批归一化)和BatchNorm(层归一化)的区别:
BatchNorm(批归一化)和 LayerNorm(层归一化)都是深度学习中常用的归一化技术,用于加速模型训练、缓解梯度消失问题并提高模型稳定性。
-
BatchNorm:更适合计算机视觉任务(如 CNN),因为图像数据的同一通道在不同样本间具有相似的分布特性,批次统计量能有效代表全局分布。
-
LayerNorm:更适合自然语言处理任务(如 Transformer),因为文本序列长度可变,且不同样本的特征分布差异较大,使用单样本统计量更合理。
七、子层连接结构
如图所示,输入到每个子层以及规范化层的过程中,还使用了残差链接(跳跃连接),因此我们把这一部分结构整体叫做子层连接(代表子层及其连接结构),在每个编码器层中,都有两个子层,这两个子层加上周围的链接结构就形成了两个子层连接结构.
class SublayerConnection(nn.Module):def __init__(self,size,dropout=0.1):super(SublayerConnection,self).__init__()self.norm = LayerNorm(size)self.dropout = nn.Dropout(dropout)def forward(self,x,sublayer):# x 代表数据# sublayer 函数入口地址 子层函数(前馈全连接层或者注意力机制层函数的入口地址)# 方式1 self.norm()->sublayer()->self.dropout() + xmyres = x+self.dropout(sublayer(self.norm(x)))# 方式2 sublayer()-->self.norm()--->self.dropout()+x# myres = x+self.dropout(self.norm(sublayer(x)))return myres
八、编码器层
作为编码器的组成单元, 每个编码器层完成一次对输入的特征提取过程, 即编码过程.
class EncoderLayer(nn.Module):def __init__(self,size,self_attn,feed_forward,dropout):super(EncoderLayer,self).__init__()# 实例化多头自注意力层对象self.self_attn = self_attn# 前馈全连接层对象self.feed_forward = feed_forward# size词嵌入维度512self.size = size# clones两个子层连接结构 self.sublayers = clones(SublayerConnection(size,dropout),2)def forward(self,x,mask):# 第一个子层连接结构x = self.sublayers[0](x,lambda x:self.self_attn(x,x,x,mask))# 第二个子层连接结构x = self.sublayers[1](x,self.feed_forward)return x
九、编码器
编码器用于对输入进行指定的特征提取过程, 也称为编码, 由N个编码器层堆叠而成.
class Encoder(nn.Module):def __init__(self,layer,N):super(Encoder,self).__init__()# 实例化多个编码层对象self.layers = clones(layer,N)# 实例化规范化层self.norm = LayerNorm(layer.size)def forward(self,x,mask):for layer in self.layers:x = layer(x,mask)return self.norm(x)
def dm_test_Encoder():size = 512head = 8d_model = 512vocab = 1000d_ff = 64x = torch.LongTensor([[100,2,421,508],[491,998,1,221]])emb = Embeddings(vocab,d_model)embr = emb(x)dropout = 0.2max_len = 60x = embr # [2,4,512]pe = PositionalEncoding(d_model,dropout,max_len)pe_result = pe(x) # 位置编码x = pe_resultmask = torch.zeros(8,4,4)# 实例化多头子注意力机制类对象self_attn = MultiHeadAttention(head,d_model)# 实例化前馈全连接层对象ff = PositionwiseFeedForward(d_model,d_ff,dropout)c=copy.deepcopy# 实例化编码层对象my_encoderlayer = EncoderLayer(size,c(self_attn),c(ff),dropout)# maskmask = torch.zeros(8,4,4)N = 6 # 实例化编码器对象en = Encoder(my_encoderlayer,N)en_result = en(x,mask)print('en_result.shape',en_result.shape,en_result)
dm_test_Encoder()