transformer结构解析--学习笔记

认识transformer架构

  1. 输入部分实现
  2. 编码器部分实现        

           2.1掩码张量

           2.2注意力机制

           2.3多头注意力机制

           2.4前馈全连接层

           2.5规范化层

           2.6子层连接机构

           2.7编码器层

           2.8编码器

3.解码器部分实现

        3.1解码器层

        3.2解码器

4.输出部分实现

5.模型构建

6.模型基本测试运行

输入部分包括:源文本嵌层和位置编码器

文本嵌入层的作用:无论是源文本嵌入还是目标文本嵌入,都是为了将文本词汇的数字表示转变为向量表示,希望在这样的高维空间表示词汇间的关系

文本嵌入层的代码表示:

#导入必备的工具包
import  torch
#预定义的网络层torch.nn,
import torch.nn as nn
#数学计算工具包
import math
#torch变量封装函数Variable
from torch.autograd import Variable
#定义Embedings类来实现文本嵌入层,这里s代表两个一摸一样的嵌入层,它们共享参数
#该类继承nn.Module,这样就有标准层的一些功能,
"""****************输入部分实现**************"""
class Embedding(nn.Module):
    def __init__(self,d_model,vocab):
        """类的初始化函数,有两个函数d_model:指词的维度,vocab:指词表的大小,"""
        """接着使用super的方式指明继承nn.module的初始化函数,我们自己实现的所有层都会这样去继承super(Embedding,self).__init__()"""
        super(Embedding, self).__init__()
    #之后就是调用nn中的预定义层Embedding,获得一个词的嵌入对象self.lut
        self.lut=nn.Embedding(vocab,d_model)
        #最后就是将d_model传入类中
        self.d_model=d_model

    #前馈神经网络
    def forward(self,x):
        """可以将其理解为该层的前向传播逻辑,所有层都会有此函数
        当传给该类的实例化对象参数时,自动调用该类该函数
        参数x:因为Embeddeding层是首层,所以代表输入给模型的文本通过词汇映射后的张量
        """

        #将x传给self.lut并与根号下self.d_model相乘作为结果返回
        return self.lut(x)*math.sqrt(self.d_model)

#测试Embedding层的功能
d_model=512
vocab=1000
x=Variable(torch.LongTensor([[100,2,421,508],[491,998,1,221]]))
emb=Embedding(d_model,vocab)
embr=emb(x)
print("embr:",embr)
print(embr.shape)

位置编码器的作用:因为再transformer的编码器结构中,并没有针对词汇位置信息的处理,因此需要在Embedding层后加入位置编码器,将词汇位置不同可能会产生不同语义的信息加入到词嵌入张量中,以弥补位置信息的缺失。

位置编码器的代码分析:

构建位置编码器的类
class PostitionalEncoding(nn.Module):
    def __init__(self,d_model,dropout,max_len=5000):
        # d_model:代表词嵌入的维度
        # dropout:代表drouput的置零比率
        # max_len:代表每隔句子的最大长度
        super(PostitionalEncoding, self).__init__()

        #实例Drouput层
        self.dropout=nn.Dropout(p=dropout)
        #初始化一个位置编码矩阵,大小是max_len*d_model
        pe=torch.zeros(max_len,d_model)
        #初始化一个绝对位置矩阵,max_len*1
        position=torch.arange(0,max_len).unsqueeze(1)

        #定义一个变化矩阵div_term,跳跃式的初始化

        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.unsqueeze(0)
        self.register_buffer('pe',pe)

    def forward(self,x): #x代表的是Embedding层的输出
        #x:代表文本序列的词嵌入表示
        #首先明确pe的编码太长了,将第二个维度,也就是max_len对应的那个维度缩小为x的句子大小
        x=x+Variable(self.pe[:,:x.size(1)],requires_grad=False)
        return self.dropout(x)

#位置编码器的测试部分
d_model=512
dropout=0.1
max_len=60

x = embr
pe=PostitionalEncoding(d_model,dropout,max_len)
pe_result=pe(x)
print(pe_result)
print("pe_result:",pe_result.shape)

2.编码器部分实现

编码器部分:1.有N个编码器层堆叠而成;2.每个编码器层由两个子层连接结构组曾=成;3.第一个子层链接结构包括一个多头注意力子层和规范化层以及一个残差连接;4.第二个子层连接结构包括一个前馈全连接子层和规范化层以及一个残差连接

2.1掩码张量

在transformer中,掩码张量的主要作用在应用attention时,有一些生成的attention张量中的值计算有可能已知量未来信息而得到的,未来信息被看到是因为训练时会把整个输出结果都是一次性进行Embeeding,但是解码器的输出却不是一次性就能产生最终结果的,因为为了防止发生这样的情况,我们会进行遮掩。

生成掩码张量的函数

#生成掩码张量的函数
def subsenquent_mask(size):
   """生成向后遮掩的掩码张量,参数size是掩码张量最后两个维度的大小,它的最后两维形成一个方阵"""
   # 在函数中,首先定义掩码张量的形状
   attn_shape=(1,size,size)
   #然后在使用np.ones方法向这个形状中添加1元素,形成上三角形,最后为了节省空间,再使用其中的数据类型变为无符号8位整形unit8
   subsenquent_mask=np.triu(np.ones(attn_shape),k=1).astype("uint8")
   #最后将numpy类型转化为torch中的tensor,内部做一个1-的操作
   #在这个其实是做了一个三角形的反转,subsenquent_mask每个元素都会减1
   #如果是0,subsenquent_mask中的该位置由0变为1
   #如果是1,subsenquent_mask中的该位置由1变为0
   return torch.from_numpy(1-subsenquent_mask)#torch.from_numpy()方法把数组转换成张量,且二者共享内存,对张量进行修改比如重新赋值,那么原始数组也会相应发生改变。

#测试掩码张量函数的部分
size=5
sm=subsenquent_mask(size)
print(sm)
print(sm.shape)
plt.figure(figsize=(5,5))
plt.imshow(subsenquent_mask(20)[0])
"""plt.imshow()函数负责对图像进行处理,并显示其格式,但是不能显示。其后跟着plt.show()才能显示出来。"""
plt.show()

2.2注意力机制

需要三个指定的输入Q(query),K(key),V(value),然后通过公式得到注意力的计算结果,这个结果代表query在key和Value作用下的表示,而

这个具体的计算规则有很多种,这里只介绍一种。

注意力计算规则的代码分析:
########注意力计算规则的代码分析:
def attention(query,key,value,mask=None,dropout=None):
    #query,key,value:代表注意力的三个输入张量
    #mask:掩码张量
    #dropout:传入的Dropout实例化对象
    #首先将query的最后一个维度提取出来,代表的是词嵌入的维度
    d_k=query.size(-1)
    #按照注意力的计算公式,将query和key的转置进行矩阵乘法,然后除以缩放系数
    scores=torch.matmul(query,key.transpose(-2,-1))/math.sqrt(d_k)

    #判断是否使用掩码张量
    if mask is not None:
        #利用masked_fill方法,将掩码张量和0进行位置的意义比较,如果等于0,替换成一个非常小的数,
        scores=scores.masked_fill(mask==0,-1e9)

    #对scores的最后一个维度上进行sofxmax操作
    p_attn=F.softmax(scores,dim=-1)
    #判断是否使用dropout操作
    if dropout is not None:
        p_attn=dropout(p_attn)

    #最后一步完成p_attn和value张量的乘法,并返回query的注意力表示
    return torch.matmul(p_attn,value),p_attn

#测试注意力计算部分的代码
query=key=value=pe_result
mask=Variable(torch.zeros(2,4,4))
# print("mask:",mask)
attn,p_attn=attention(query,key,value,mask=mask)
print(attn.shape)
print("attn:",attn)
print(p_attn.shape)
print("p_attn:",p_attn)

2.3多头注意力机制

学习并实现了多头注意力机制的类:MutiheadedAttinetion

        因为多头注意力机制中需要使用多个相同的线性层,首先实现了克隆函数clones。

        clones函数的输入是module,N 分别代表克隆的目标层,克隆的个数

        clones函数的输出是装有N个克隆层的Module列表。

        接着实现MutiheadedAttention类,它的初始化函数输入是h,d_model,dropout分别代表投头数,词嵌入维度和置零比率

        它的实例化对象输入是Q,K,V以及掩码张量mask注意力

        它的实例化对象输出是通过多头注意力机制处理的Q的注意力表示

实现代码:

#实现克隆函数,因为在多头注意力机制下,要用到多个结构相同的线性层
#需要使用clone函数将他们一同初始化到一个网络层列表对象中
def clones(module,N):
    #module:代表要克隆的网络目标层。
    # N:将module克隆几个
    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操作时,置零的比率
        super(MultiHeadedAttention, self).__init__()
        #要确认一个事实:多头的数量head需要整除嵌入词的维度embedding_dim
        assert embedding_dim %head==0
        #获得每个头获得的词向量的维度
        self.d_k=embedding_dim//head
        self.head=head
        self.embedding_dim=embedding_dim
        #获得线形层,要获得4个,分别是Q,K,V以及最终的输出线形层
        self.linears=clones(nn.Linear(embedding_dim,embedding_dim),4)
        #初始化注意力张量
        self.attn=None
        #初始化drouput对象
        self.dropout=nn.Dropout(p=dropout)


    def forward(self,query,key,value,mask=None):
        #queryy,key,value是注意力机制的三个输入张量,mask代表掩码张量
        #首先判断是否使用掩码张量
        if mask is not None:
            #使用unsqueeze将掩码张量进行维度扩充,代表多头中的第n个头
            mask=mask.unsqueeze(1)

        #得到batch_size
        batch_size=query.size(0)
    #首先使用zip将网络层和输入数据连接在一起,模型的输出利用view和reansponse进行维度和形状的转换
        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,self.attn=attention(query,key,value,mask=mask,dropout=self.dropout)

        #得到每个头的计算结果是4维张量,需要进行形状的转换
        #前面已经将1,2两个维度进行过转置,在这里必须要重新转置回来,经历了transponse()转职后,必须使用contiguous方法,否则无法使用view()方法
        x=x.transpose(1,2).contiguous().view(batch_size,-1,self.head*self.d_k)
        #最后将x输入线形层列表中的最后一个线形层中进行处理,
        return self.linears[-1](x)

    #实例化入肝参数
head=8
embedding_dim=512
dropout=0.2



#测试多头注意力机制函数的测试代码

#若干输入参数的初始化
query=key=value=pe_result
mask=Variable(torch.zeros(2,4,4))
mha=MultiHeadedAttention(head,embedding_dim,dropout)
mha_result=mha(query,key,value,mask=mask)
print(mha_result)
print(mha_result.shape)

2.4前馈全连接层

前馈全连接层:在transformer中前馈全连接层就是具有两层线性层的全连接网络

作用:考虑注意力机制可能对复杂过程的拟合度不够,通过增加两层网络来增强模型的能力

实现前馈全连接层的类:PositionWiseFeedForward

它的实例化参数为d_model,d_ff,dropout,分别代表嵌入维度,线性变换维度,置零比率

它的输入参数x,表示上层的输出

它的输出是经过2层线性网络变换的特征表示

实现代码:

#构建前馈全连接网络类
class PositionWiseFeedForward(nn.Module):
    def __init__(self,d_model,d_ff,dropout=0.1):
        #d_model:代表词嵌入的维度,同时也是两个线形层的输入维度和输出维度
        #d_ff:代表 第一个线性层的输出维度,和第二个线形成层的输入维度
        #dropout:经过Dropout层处理时,随机置零的比率
        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):
        #x:代表来自上一层的输出
        #首先将x送入到第一个线性层网络,然后经理relu函数激活,再经理dropout层的处理
        #最后送入到第二个线形层
        return self.w2(self.dropout(F.relu(self.w1(x))))

#测试部分代码
d_model=512
d_ff=64
dropout=0.1
x=mha_result
ff=PositionWiseFeedForward(d_model,d_ff,dropout)
ff_result=ff(x)
print(ff_result)
print(ff_result.shape)

2.5规范化层与子层连接层

 代码演示:

#构建规范化层的类
class LayerNorm(nn.Module):
    def __init__(self,features,eps=1e-6):
        #features:代表词嵌入的维度
        #eps:一个足够小的整数,用来在规范化计算公式的分母中,防止除零操作
        super(LayerNorm, self).__init__()
        #初始化两个参数张量a2,b2,用来对 结果进行规范化操作
        #将其用nn.Parameter进行封装,代表他们也是模型中的参数
        self.a2=nn.Parameter(torch.ones(features))
        self.b2=nn.Parameter(torch.zeros(features))
        self.eps=eps

    def forward(self,x):
        #首先对x进行最后一个维度上的均值操作,同时保持输出维度和输入维度一致
        mean=x.mean(-1,keepdim=True)  #-1代表最后一个维度

        #接着对x进行最后一个维度上的求标准差的操作,同时保持输出维度和输入维度一致
        std=x.std(-1,keepdim=True)
        #按照规范化公式进行计算并返回
        return self.a2*(x-mean)/(std+self.eps)+self.b2

#测试部分代码
features=d_model=512
eps=1e-6
x=ff_result
ln=LayerNorm(features,eps)
ln_result=ln(x)
# print(ln_result)
# print(ln_result.shape)




#构建子层连接结构的类
class SublayerConnection(nn.Module):
    def __init__(self,szie,dropout):
        #size:代表词嵌入的维度
        #dropout:进行dropout操作的置零比率
        super(SublayerConnection, self).__init__()
        #实例化一个规范层的对象
        self.norm=LayerNorm(size)
        #实例化一个dropout对象
        self.dropout=nn.Dropout(p=dropout)
        self.size=size


    def forward(self,x,sublayer):
        #x;代表上一层传入的张量
        #sublayer:该子层连接中子层函数
        #首先将x进行规范化,然后送入到子层函数中处理,处理结果进入dropout层,最后进行残差连接
        return x+self.dropout(sublayer(self.norm(x)))


#测试部分的代码
size=d_model=512
head=8
dropout=0.1
x=pe_result
mask=Variable(torch.zeros((2,4,4)))
self_attn=MultiHeadedAttention(head,d_model)
sublayer=lambda x:self_attn(x,x,x,mask)
sc=SublayerConnection(size,dropout)
sc_result=sc(x,sublayer)
print("sc_result:",sc_result)
print("sc_result:",sc_result.shape)

2.7编码器层

 编码器层的作用:作为编码器的组成部分,每个编码器层完成了一次对输入的特征提取过程,即编码过程

 学习并实现了编码器层的类:EncoderLayer

  •  类的初始化函数共有 4个,size:词嵌入维度的大小,第二个self_attn,第三个是feed_forward,之后我们将传入前馈神经全连接层实例化对象,最后一个是置0比率dropout
  • 实例化对象的输入参数有2个,x代表来自上一层的输出,mask代表掩码张量
  • 它的输出代表经过整个编码层的特征表示

代码部分:

#构建编码器层的类
class EncoderLayer(nn.Module):
    def __init__(self,size,self_attn,feed_forward,dropout):
        #size:代表词嵌入的维度
        #self_attn:代表传入的多头自注意力子层的实例化对象
        #feed_forward:代表全连接层实例化对象
        #dropout:进行dropout操作的置零比率
        super(EncoderLayer, self).__init__()
        #将两个实例化对象和参数传入类
        self.self_attn=self_attn
        self.feed_forward=feed_forward
        self.size=size
        #编码器层中有两个子层连接结构,使用clones函数进行操作
        self.sublayer=clones(SublayerConnection(size,dropout),2)

    def forward(self,x,mask):
    #x:代表上一层的传入张量
    #mask:代表掩码张量
    #首先让x经过第一个子层连接结构,内部包含多头自注意力机制子层
    #再让张量经过第二个子层连接结构,其中包含前馈神经网络
        x=self.sublayer[0](x,lambda x:self.self_attn(x,x,x,mask))
        return self.sublayer[1](x,self.feed_forward)

#测试部分代码
size=d_model=512
head=8
d_ff=64
x=pe_result
dropout=0.2
self_attn=MultiHeadedAttention(head,d_model)
ff=PositionWiseFeedForward(d_model,d_ff,dropout)
mask=Variable(torch.zeros(2,4,4))

el=EncoderLayer(size,self_attn,ff,dropout)
el_result=el(x,mask)
print("el:result:",el_result)
print("el_result:",el_result.shape)

 2.8编码器

编码器用于对输入进行指定的特征提取过程,也成为编码,由N个编码器层堆叠而成

学习并实现了编码器层的类:Encoder

  • 类的初始化函数参数由两个,layer和N,代表编码器层和编码器层的个数
  • forward函数也有两个,和编码器层的forward相同,x代表上一层的输出,mask代表掩码张量
  • 编码器层的输出就是Transformer中编码器的特征提取表示,它将成为解码器的输入的一部分

代码部分:

#构建编码器类Encoder
class Encoder(nn.Module):
    def __init__(self,layer,N):
        #layer:代表编码器层
        #N:代表编码器中有几个layer
        super(Encoder, self).__init__()
        #首先使用clones函数克隆N个编码器层放置在self.layers中
        self.layers=clones(layer,N)
        #初始化一个规范化层,作用在编辑器的后面
        self.norm=LayerNorm(layer.size)

    def forward(self,x,mask):
        #x:代表上一层输出的张量
        #mask:代表掩码张量
        #让x经过N个编码器层的处理,最后再经过规范化层输出就可以了
        for layer in self.layers:
            x=layer(x,mask)
            return self.norm(x)

#测试代码
size=d_model=512
d_ff=64
head=8
c=copy.deepcopy
attn=MultiHeadedAttention(head,d_model)
ff=PositionWiseFeedForward(d_model,d_ff,dropout)
dropout=0.2
layer=EncoderLayer(size,c(attn),c(ff),dropout)
N=8
mask=Variable(torch.zeros(2,4,4))
en=Encoder(layer,N)
en_result=en(x,mask)
print("en_result:",en_result)
print(en_result.shape)

3.解码器部分实现

         3.1解码器层

每个解码器层由三个子层连接结构组成

第一个子层连接包括一个多头自注意力子层和规范化层以及一个残差连接

第二个子层连接结构包括一个多头注意力子层和规范化层以及一个残差连接

第三个子层连接结构包括一个前馈全连接子层和规范化层以及一个残差连接

结构图:

代码部分:

#构建解码器层类
class Decoderlayer(nn.Module):
    def __init__(self,self_attn,src_attn,feed_forward,dropout):
        #size:代表词嵌入的维度
        #self_attn:代表多头自注意力机制的对象
        #src_attn:代表常规的注意力机制的对象
        #feed_forward:代表前馈全连接层的对象
        #dropout:代表dropout的置零比率
        super(Decoderlayer, self).__init__()
        #将参数传入类中
        self.size=size
        self.self_attn=self_attn
        self.src_attn=src_attn
        self.feed_forward=feed_forward
        self.dropout=dropout

        #按照解码器层的结构图,使用clones函数克隆3个子层连接对象
        self.sublayer=clones(SublayerConnection(size,dropout),3)
    def forward(self,x,memory,source_mask,target_mask):
        #x:代表上一层输入的张量
        #memory:代表编码的语义存储张量
        #source_mask:源数据的掩码张量
        #target_mask:目标数据的掩码张量
        m=memory
        #第一步让x经历第一个子层,多头自注意力机制的子层
        ##采用target_mask:为了将解码时未来的信息进行遮掩,
        x=self.sublayer[0](x,lambda x:self.self_attn(x,x,x,mask))
        #第二步让x经历第二个子层,常规的注意力机制的子层,Q!=k=v
        #采用source_mask:为了遮蔽掉对信息无用的数据
        x=self.sublayer[1](x,lambda x:self.src_attn(x,m,m,source_mask))
        #第三步让经历第三个子层,前馈全连接层
        return self.sublayer[2](x,self.feed_forward)

#代码测试部分

size=d_model=512
head=8
d_ff=64
dropout=0.2
self_attn=MultiHeadedAttention(head,d_model,dropout)
ff=PositionWiseFeedForward(d_model,d_ff,dropout)
x=pe_result
memory=en_result
mask=Variable(torch.zeros(2,4,4))
source_mask=target_mask=mask
dl=Decoderlayer(size,self_attn,src_attn,ff,dropout)
dl_result=dl(x,memory,source_mask,target_mask)
print(dl_result)
print(dl_result.shape)

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值