Transformer 机器翻译任务(上)

架构解析:

                                                                        Transformer 架构

1.输入部分实现:

        输入部分包含两部分: 源文本嵌入层及其位置编码器、目标文本嵌入层及其位置编码器

        文本嵌入层作用: 无论是源文本还是目标文本嵌入,都是将文本中的词汇的数字表示转化为向量表示。

#导入包
import torch
import torch.nn as nn
import math
from torch.autograd import Variable
#变量封装函数
#定义Embeddings类实现文本嵌入层,这里s说明两个一摸一样的嵌入层,他们共享参数
class Embeddings(nn.Module):
    def __init__(self,d_model,vocab):
    #类的初始化函数,两个参数:d_model是词嵌入的维度,vocab是词表大小
        #用super的方式继承nn.Module的初始化函数
        super(Embeddings,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:因为Embedding层是首层,所以代表输入给模型的文本通过词汇映射后的张量
    #将x参数传给self.lut并与根号下self.d_model相乘作为结果返回
        return self.lut(x)*math.sqrt(self.d_model)
#nn.Embedding 演示
embedding = nn.Embedding(10,3)
input=torch.LongTensor([[1,2,4,5],[4,3,2,9]])
print(embedding(input))

结果:

tensor([[[ 0.5698, -1.2743, -0.6155],
         [-0.4725, -0.0029,  1.8145],
         [ 0.9761,  0.0535, -2.0612],
         [ 0.5406, -0.3778,  0.8490]],

        [[ 0.9761,  0.0535, -2.0612],
         [ 1.0493,  0.5042,  0.4432],
         [-0.4725, -0.0029,  1.8145],
         [-0.5530,  0.9282,  1.1277]]], grad_fn=<EmbeddingBackward>)

#实例化参数
d_model = 512  #词嵌入维度512  每个词被映射为512维度的向量
vocab = 1000 #词表大小1000

#输入参数
#输入x是一个使用variable的长整型张量,形状2*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=",embr.shape)

 结果:

embr= tensor([[[-22.4642,  -3.6376,   6.6192,  ...,  21.8646,   6.2595,  25.8007],
         [ -6.5625,  43.3326,   5.7480,  ...,  56.3417,  22.4026,   5.1603],
         [ -5.2708,  25.8822,  -8.2533,  ...,  11.3690,  -4.4162,  27.2251],
         [-21.2326,  20.4468,  15.3034,  ...,  -6.3335, -10.0722, -12.4905]],

        [[-12.3713, -11.4310,  -3.3242,  ...,  -4.9082, -45.4644,   3.7045],
         [ 25.3261,  -8.0496, -14.6015,  ...,   2.2214,  13.5650,  -6.0152],
         [ 25.2979, -27.1811, -25.2218,  ...,  -3.1814,   8.5051, -26.9446],
         [ 32.1590,  21.6688,   7.9504,  ..., -11.7885,  -0.6783,  -0.8048]]],
       grad_fn=<MulBackward0>)
embr= torch.Size([2, 4, 512])

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

#定义位置编码器
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 = nn.Dropout(p=dropout)

        #初始化一个位置矩阵,他是0阵,矩阵大小为max_len * d_model
        pe = torch.zeros(max_len,d_model)

        #初始化一个绝对位置矩阵,在这里,词汇的绝对位置用他的索引表示
        #所以我们首先是使用arange方法获得一个连续自然数向量,然后再使用upsqueeze方法扩展向量维度
        #又因为传入参数为1,代表矩阵扩展的方向,会使一个向量变成max_len*1的矩阵
        position = torch.arange(0, max_len).unsqueeze(1)  #二维张量

        #绝对位置矩阵初始化后,接下来就是将位置信息加入到位置编码矩阵当中
        #最简单的思路就是先将max_len*1的绝对位置矩阵转化为max_len*d_model的形状,然后覆盖原来初始位置的位置编码矩阵
        #要做这些变化,就需要一个1*d_model形状的变换矩阵div_term,我们对这个变换矩阵除了形状之外还希望能够将自然数的绝对位置编码缩放成足够小的数字
        #有利于在之后的梯度下降中可以更好的收敛
        #首先使用arrange获得一个自然数矩阵,然后只初始化一半的即1*d_model/2的矩阵,这是为什么?
        #看作是初始化两次,每次初始化的变换矩阵都会做不同的处理,第一次初始化的变换矩阵用于sin给偶数列赋值,第二次初始化的变换矩阵用于cos给奇数列赋值
        #并把这两个矩阵分别填充在位置编码矩阵的奇数和偶数位置上面,形成最终的位置编码矩阵
        div_term = torch.exp(torch.arange(0,d_model,2)*-(math.log(10000.0)/d_model))
        #所有的行在偶数列都用sin来给赋值
        pe[:, 0::2] = torch.sin(position * div_term)
        #所有的行在奇数列都用cos来给赋值
        pe[:, 1::2] = torch.cos(position * div_term)

        #位置编码矩阵为pe,且是一个二维矩阵,要想和embedding输入(一个三维张量)相加
        #就必须对其扩展维度,对第一个维度进行扩展,类似增加batch_size,成为三维矩阵
        pe = pe.unsqueeze(0)

        #最后把pe位置编码模型注册成模型的buffer,
        #我们把buffer认为是对模型效果是有帮助的,但是并不是模型的超参数或者参数,训练过程中不需要随着优化步骤进行更新的增益对象
        #注册之后可以在模型保存后重加载时和模型参数一起被加载,通过self.pe调用它
        #'pe'为名字 d_model的值确定,pe矩阵就固定了
        self.register_buffer('pe',pe)

    def forward(self, x):
        #forward的参数为x,表示文本序列的词嵌入表示
        #再相加之前对pe做一些工作,将三维张量是第二维(句子最大长度)的那一列将切片到输入的x的第二位相同即x.size(1)
        #我们默认max_len为5000实在是太大,很难有一个句子包含5000个词汇,所以要进行pe与输入张量的适配,实际上用不到那么多,所以我们对pe进行截取
        #最后Variable进行封装,使其与x样式相同,但是他是不需要进行梯度求解的,因此将requires_grad设置成false
        x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False)
        #最后使用self.dropout对象进行丢弃操作,并返回结果
        return self.dropout(x)
#实例化参数
#词嵌入维度
d_model = 512
#置0比率
dropout = 0.2
#句子最大长度 
max_len = 60

#输入参数
#输入x是Embedding层的输出的张量,形状是2*4*512
x = embr

#调用
pe = PositionalEncoding(d_model,dropout,max_len)
pe_result = pe(x)
print("pe_result==",pe_result)
#绘制词汇向量中特征的分布曲线
import matplotlib.pyplot as plt
import numpy as np
#创建画布 15*5
plt.figure(figsize=(15,5))
#实例化PositionalEncoding类得到的pe对象,输入参数是20和0
pe = PositionalEncoding(20,0)  #d_model=20  dropout=0
#然后将pe传入Variable封装的tensor,这样pe会直接执行forward对象
#且这个tensor里的数值都是0,被处理后相当于位置编码张量,展示pe
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.legend(["dim %d"%p for p in [4,5,6,7]])

# 必须有这个,要不然无法显示
plt.show()

 输出效果分析:

        每条颜色曲线代表某一个词汇中的特征在不同位置的含义

        保证同一个词汇随着所在位置不同它对应的位置嵌入向量会发生变化

        正弦波和余弦波的值域范围都是[-1,1],很好的控制了嵌入数值的大小,有利益梯度的快速计算

2.编码器部分实现:

        编码器部分:

                由N个编码器层堆叠组成

                每个编码器都由两个子层连接结构组成

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

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

2.1 掩码张量

                在transformer中,掩码张量主要用于attention中,有一些生成的attention张量中的值计  算是通过一个句子完整语义(包括未来信息)得到的,未来信息被看到是因为训练过程会把整个输出结果一次性进行Embedding,但是理论上解码器的输出并不是一次就能产生结果的,而是通过上一次综合结果得出,因此未来结果会被提前利用,所以我们需要进行遮挡。

#掩码张量代码
def subsequent_mask(size):
    '''生成向后的掩码张量,参数size是掩码张量最后两个维度的大小,他的最后两维形成一个方针'''
    #在函数中,首先生成掩码张量的形状
    attn_shape = (1,size,size)  #1是扩充维度

    #然后使用np.ones方法向这个形状添加1元素,形成上三角阵,最后为了节省空间
    #再使用其中的数据类型变成无符号8位整型unit8
    subsequent_mask = np.triu(np.ones(attn_shape),k = 1).astype('uint8') #k=0沿主对角线向右上移一位(数据不改变),然后执行清0,实现三角阵

    #最后将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
sm = subsequent_mask(size)
# print("sm==",sm)

#掩码张量可视化
plt.figure(figsize=(5,5))
plt.imshow(subsequent_mask(20)[0])
plt.show()

 输出效果分析:

        黄色部分代表是1,被遮挡,紫色表示没有被遮挡的信息,横坐标代表词汇的位置,纵坐标代表可以查看的位置

     

2.2注意力机制

Attention(Q,K,V) = softmax(\frac{QK^{T}}{\sqrt[]{d_k}})V

假如我们有一个问题:给出一段文本,使用一些关键词对它进行描述!

为了方便统一正确答案,这道题可能预先已经给大家写出了一些关键词作为提示.其中这些给出的提示就可以看作key。而整个的文本信息就相当于是query,value的含义则更抽象,可以比作是你看到这段文本信息后,脑子里浮现的答案信息。

这里我们又假设大家最开始都不是很聪明,第一次看到这段文本后脑子里基本上浮现的信息就只有提示这些信息,因此key与value基本是相同的,但是随着我们对这个问题的深入理解,通过我们的思考脑子里想起来的东西原来并且能够开始对我们query也就是这段文本,提取关键信息进行表示.这就是注意力作用的过程,通过这个过1我们最终脑子里的value发生了变化,
根据提示key生成了query的关键词表示方法,也就是另外一种特征表示方法。
刚刚我们说到key和value一般情况下默认是相同,与query是不同的,这种是我们一般的注意力输入形式,但有一种特殊情况,就是我们query与key和value相同,这种情况我们称为自注意力机制,就如同我们的刚刚的使用一般注意力机制,是使用不同于给定文本的关键词表示它.而自注意力机制,
需要用给定文本自身来表达自己,也就是说你需要从给定文本中抽取关键词来表述它,相当于对文本自身的一次特征提取。

#注意力机制
import torch.nn.functional as F
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)
    #按照注意力公式,将query与key的转置相乘,这里面key是将最后两个维度进行转置,再除以缩放系数
    ##得到注意力得分张量scores
    scores = torch.matmul(query,key.transpose(-2,-1)) / math.sqrt(d_k)
    #接着判断是否使用掩码张量
    if mask is not None :
        #使用tensor的masked_fill方法,将掩码张量和scores张量每个位置一一比较,如果掩码张量不是空就需要使用mask
        #则对应的scores张量用-1e9这个值来替换,如下演示
        scores = scores.masked_fill(mask == 0,-1e9)
    #对scores的最后一维进行softmax操作,使用F.softmax方法,第一个参数是softmax对象,第二个是归一化的方式
    #这样获得最终的注意力张量
    #F.sofrmax(x,dim)作用:根据不同的dim规则来做归一化操作。x指的是输入的张量,dim指的是归一化的方式。
    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

#实例化分析

#调用

#我们输入的query,key,value相同,等于位置编码的输出pe_result

query = key = value = pe_result #自注意力机制

#print("query.shape",query.shape) #torch.Size([2, 4, 512])

#调用

attn,p_attn = attention(query,key,value)

# print("attn:",attn)

# print("attn.shape:",attn.shape) #torch.Size([2, 4, 512])

# print("p_attn:",p_attn)

# print("p_attn.shape:",p_attn.shape)  #torch.Size([2, 4, 4])

#带mask的输入参数

#mask为一个2*4*4的零张量

mask = Variable(torch.zeros(2,4,4))

#调用

attn,p_attn = attention(query,key,value,mask=mask)

print("attn:",attn)

print("attn.shape:",attn.shape) #torch.Size([2, 4, 512])

print("p_attn:",p_attn)

print("p_attn.shape:",p_attn.shape)  #torch.Size([2, 4, 4])

 2.3多头注意力机制

从多头注意力的结构图中,貌似这个所谓的多个头就是指多组线性变换层,其实并不是,我只有使用了一组线性变化层,即三个变换张量对Q,K,V分别进行线性变换,这些变换不会改变原有张量的尺寸,因此每个变换矩阵都是方阵,得到输出结果后,多头的作用才开始显现,每个头开始从词义层面分割输出的张量,也就是每个头都想获得一组Q,K,V进行注意力机制的计算,但是句子中的每个词的表示只获得一部分,也就是只分割了最后一维的词嵌入向量.这就是所谓的多头,将每个头的获得的输入送到注意力机制中,就形成多头注意力机制。

多头注意力机制的作用:
·这种结构设计能让每个注意力机制去优化每个词汇的不同特征部分,从而均衡同一种注意力机制可能产生的偏差,让词义拥有来自更多元的表达,实验表明可以从而提升模型效果。

#用于深度拷贝的copy工具包
import copy  #让a和b分别占用不同的内存,虽然值一样,因此改变a就  不会改变b

#首先需要定义克隆函数,因为在多头注意力机制的实现中,用到多个结构相同的线性层.
#我们将使用clone函数将他们一同初始化在一个网络层列表对象中.之后的结构中也会用到该函数.
def clones(module, N):
    '''用干牛成相同网络层的克降函数,它的参数module表示要克隆的目标网络层,N代表需要克隆的数量
    在函数中,我们通过for循环对module进行N次深度拷贝,使其每个module成为独立的层,
    然后将其放在nn.ModuleList类型的列表中存放.'''
    return nn.ModuleList( [copy. deepcopy(module) for _ in range(N)])  
    #for _in range(n)和for each in range(n)是一样的,只不过_在下面不会用到,这里的_可以替换成任何符合规定的字符串
#我们使用一个类来实现多头注意力机制的处理
class MultiHeadedAttention(nn.Module):
    def __init__(self, head,embedding_dim,dropout=0.1):
        '''在类的初始化时,会传入三个参数,head代表头数,embedding_dim代表词嵌入的维度'''
        '''dropout代表进行dropout操作时置0比率,默认是0.1.'''
        super(MultiHeadedAttention,self).__init__()

        #在函数中,首先使用了一个测试中常用的assert语句,判断h是否能被d_model整除,#这是因为我们之后要给每个头分配等量的词特征.也就是embedding_dim/head个.
        assert embedding_dim % head ==0
        #得到每个头获得的分割词向量维度d_k
        self.d_k = embedding_dim // head
        
        #传入头数h
        self.head = head

        #然后获得线性层对象,并且通过nn的lineral实例化,它的内部矩阵是一个embedding_dim x embedding_dim(方阵),然后使用clones拷贝四次
        #四次是因为Q,K,V各自需要一个线性层,并且拼接的矩阵还需要一个线性层(h和concat相连)
        self.linears = clones(nn.Linear(embedding_dim,embedding_dim),4)

        #self.att初始化为None,它代表最后得到最后的注意力张量,现在还没有结果所以为None 
        self.att = None

        #最后就是一个self.dropout对象,它通过nn中的Dropout实例化而来,置0比率为传进来的参数
        self.dropout = nn.Dropout(p=dropout)

    def forward(self, query,key,value, mask=None) :
        '''前向逻辑函数,它的输入参数有四个,前三个就是注意力机制需要的Q,K,V,
        最后一个是注意力机制中可能需要的mask掩码张量,默认是None. '''
        
        #如果存在掩码张量mask
        if mask is not None:
            #使用unsqueeze拓展维度,代表多头中的第n头
            mask = mask.unsqueeze(0)

        #接着,我们获得一个batch_size的变量,他是query尺寸的第1个数字,代表有多少个样本.
        batch_size = query.size(0)
        #之后就进入多头处理环节
        #首先利用zip将输入QKV与三个线性层组到一起,然后使用for循环,将输入QKV分别传到线性层中,
        #做完线性变换后,开始为每个头分割输入,这里使用view方法对线性变换的结果进行维度重塑,多增加另一个维度,将embedding 拆分成head*d_k
        #这样就意味着每个头可以获得一部分词特征组成的句子,其中的-1代表自适应维度,
        #计算机会根据这种变换自动计算这里的值.然后对第二维和第三维进行转置操作,
        #为了让代表句子长度维度(-1)和词向量维度(self.d_k)能够相邻,这样注意力机制才能找到词义与句子位置的关系,
        #从attention函数中可以看到,利用的是原始输入的倒数第一和第二维.这样我们就得到了每个头的输入
        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))]  #只利用了self.linears前三个值  每次取出一个model和x,model是一个方阵的全连接线性层,x是query, key,value三个输入
        
        #view()对已知的进行reshape  将embedding 拆分成head*d_k
        #zip()函数用于将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表。在Python 3.x 中为了减少内存,zip()返回的是一个对象。如需展示列表,需手动 list() 转换。
        
        #得到每个头的输入后,接下来就是将他们传入到attention中,
        #这里直接调用我们之前实现的attention函数.同时也将mask和dropout传入其中.
        x,self.attn = attention(query,key,value,mask=mask,dropout=self.dropout)  #self.attn 权值矩阵

        #通过多头注意力计算后,我们就得到了每个头计算结果组成的4维张量,我们需要将其转换为输入的形状以方便后续计算
        #因此这里开始进行第一步处理环节的逆操作,先对第二和第三维进行转置,然后使用contiguous方法
        #这个方法的作用就是能够让转置后的张量应用view方法,否则将无法直接使用,
        #所以,下一步就是使用view重塑形状,变成和输入形状相同(三维).
        x = x.transpose(1,2).contiguous().view(batch_size,-1,self.head*self.d_k)

        #最后使用线性层列表中的最后一个线性层列表对输入进行线性变换得到最终的多头注意力结构的输出.
        return self.linears[-1](x)
#实例化若干参数
head = 8
embedding_dim=512
dropout = 0.2

#若干输出参数初始化
query = key = value = pe_result  #自注意力机制

mask = Variable(torch.zeros(8,4,4))

mha = MultiHeadedAttention(head,embedding_dim,dropout)
mha_result = mha(query,key,value,mask)
print("mha_result",mha_result)
print("mha_result.shape",mha_result.shape)

结果:

mha_result tensor([[[  0.6555,   6.7892,   2.0045,  ...,  -3.1804,  -8.4493,  -1.4751],
         [  2.7332,   9.8476,   0.9385,  ...,  -0.9942,  -1.6430,   3.6372],
         [  1.5154,   8.0234,   4.8857,  ...,  -4.7212,  -6.4956,  -1.6924],
         [  0.0775,   1.0627,  -0.1773,  ...,  -3.8392,  -3.8443,  -1.4026]],

        [[ -3.9523,   3.6303, -10.8047,  ...,   1.5843,   5.7099,  -1.9001],
         [ -0.6468,   2.3565,  -7.8420,  ...,  -2.1634,   3.5382,  -5.4170],
         [ -2.5982,   5.0443, -14.7125,  ...,   0.0869,   8.9312,  -4.0839],
         [ -4.2254,  -0.5809,  -7.0227,  ...,   1.6144,   7.4018,  -4.9653]]],
       grad_fn=<AddBackward0>)
mha_result.shape torch.Size([2, 4, 512])

2.4前馈全连接层

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

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

#前馈全连接层
#通过类PositionwiseFeedForward来实现
class PositionwiseFeedForward(nn . Module):
    def __init__(self, d_model, d_ff, dropout=0.1):
        '''初始化函数有3个输入参数分别是d_model, d_ff,和dropout=0.1,第一个d_model是线性层的输入维度也就是第二个线性层的输出维度
        因为我们希望输入通过前馈全连接层后输入和输出的维度不变.第二个参数d_ff就是第二个线性层的输入维度和第一个线性层的输出维度 最后一个是dropout置0比率'''
        super(PositionwiseFeedForward,self).__init__()
        
        #首先按照我们预期使用nn实例化了两个线性层对象,self.w1和self.w2
        #它们的参数分别是_model, d_ff和d_ff, d_model
        self.w1 = nn.Linear(d_model, d_ff)
        self.w2 = nn.Linear(d_ff, d_model)
        #然后使用nn的Dropout实例化了对象self.dropout
        self.dropout = nn.Dropout(dropout)
    def forward(self,x):
        #输入参数为x,代表来自上一层的输出
        #首先经过第一个线性层,然后使用Funtional中relu函数进行激活,
        #之后再使用dropout进行随机置0,最后通过第二个线性层w2,返回最终结果
        return self.w2(self.dropout(F.relu(self.w1(x))))
#实例化参数
d_model = 512
#线性变化的维度
d_ff = 64
dropout= 0.2
#输入参数
x = mha_result
#调用
ff = PositionwiseFeedForward(d_model, d_ff, dropout)
ff_result = ff(x)
print("ff_result:",ff_result)
print("ff_result.shape:",ff_result.shape)

结果:

ff_result: tensor([[[-0.8521,  1.5820, -1.3818,  ..., -0.8904,  0.1466,  0.8077],
         [-1.1178,  0.9740, -1.4030,  ..., -2.2666, -0.1584,  0.4578],
         [-1.2843,  0.7624, -0.1395,  ..., -1.0660,  0.1130,  0.6395],
         [-0.3657,  1.1799, -2.0446,  ..., -0.6277,  0.6724,  0.1956]],

        [[-1.0424,  2.0287, -0.1249,  ..., -1.0606,  0.2618, -2.6139],
         [ 0.5075,  0.7507, -0.5367,  ..., -1.5217, -0.7423, -1.7025],
         [ 0.8786,  1.6887,  0.8623,  ...,  0.4138, -1.1363, -1.1891],
         [-0.5024,  1.1897, -1.1081,  ..., -1.1718, -1.6968, -0.3837]]],
       grad_fn=<AddBackward0>)
ff_result.shape: torch.Size([2, 4, 512])

2.5规范化层

 规范化层的作用:
它是所有深层网络模型都需要的标准网络层,因为随着网络层数的增加,通过多层的计算后参数可能开始出现过大或过小的情况,这样可能会导致学习过程出现异常,模型可能收敛非常的慢.因此都会在一定层数后接规范化层进行数值的规范化,使其特征数值在合理范围内.

#通过LayerNorm实现规范化层的类
class LayerNorm(nn.Module):
    def __init__( self, features,eps=1e-6):
        '''初始化承数有两个参数,一个是features,表示词嵌入的维度,
        另一个是eps它是一个足够小的数,在规范化公式的分母中出现,防止分母为0.默认是1e-6. '''
        super(LayerNorm,self).__init__()

        #根据features的形状初始化两个参数张量a2,和b2,第一个初始化为1张量,也就是里面的元素都是1
        #第二个初始化为0张量,也就是里面的元素都是0。这两个张量就是规范化层的参数
        #因为直接对上一层得到的结果做规范化公式计算,将改变结果的正常表征,因此需要有参数作为调节因子
        #使其即能满足规范化要求,又能不改变针对目标的表征.最后使用nn.parameter封装,代表他们是模型的参数
        self.a2 = nn.Parameter(torch.ones(features))
        self.b2 = nn.Parameter(torch.zeros(features))
        #把eps传到类中
        self.eps = eps
    def forward( self, x):
        '''输入参数x代表来自上一层的输出
        #在函数中,首先对输入变量x求其最后一个维度的均值,并保持输出维度与输入维度一致.
        #接着再求最后一个维度的标准差,然后就是根据规范化公式,用x减去均值除以标准差获得规范化的结果
        #最后对结果乘以我们的缩放参数,即a2,*号代表同型点乘,即对应位置进行乘法操作,加上位移参数b2'''
        mean = x.mean(-1,keepdim=True)  #keepdim=True 保持输出维度与输入维度一致
        std = x.std(-1,keepdim=True)
        return self.a2 *(x -mean)/ (std+self.eps) + self.b2  #减去均值 除以方差

#实例化参数
feature = d_model = 512
eps = 1e-6

#输入x来自前馈全连接层的输出
x = ff_result

#调用
ln = LayerNorm(feature,eps)
ln_result = ln(x)
print("ln_result",ln_result)
print("ln_result.shape",ln_result.shape)

 结果:

ln_result tensor([[[-2.3174e+00,  5.4942e-01, -1.0672e+00,  ...,  1.5668e+00,
          -3.8290e-01,  1.0758e+00],
         [-2.3772e+00,  1.5186e+00, -2.9492e-01,  ...,  1.4196e+00,
          -6.9411e-01,  1.6383e+00],
         [-2.7022e+00,  1.0273e+00, -3.2748e-01,  ...,  2.2802e+00,
           1.1571e+00,  2.0433e+00],
         [-1.6436e+00,  4.0382e-01, -8.1419e-04,  ...,  2.2132e+00,
           1.1884e+00,  9.6908e-01]],

        [[-5.7286e-01, -4.7734e-01,  1.9316e-01,  ...,  1.1173e+00,
          -5.5429e-01,  3.3576e-01],
         [-7.8134e-01,  2.2175e-01, -8.3406e-01,  ...,  1.9158e+00,
          -1.3819e+00,  1.3129e+00],
         [-8.1944e-01, -6.1010e-01, -2.6352e-02,  ...,  1.3342e+00,
          -9.0744e-01,  9.8563e-01],
         [-8.8902e-01, -3.2065e-03, -4.0896e-01,  ...,  2.3518e+00,
          -8.0326e-01,  7.1091e-01]]], grad_fn=<AddBackward0>)
ln_result.shape torch.Size([2, 4, 512])

2.6 子层连接结构:
输入到每个子层以及规范化层的过程中,还使用了残差链接(跳跃连接),因此我们把这一部分结构整体叫做子层连接(代表子层及其链接结构),在每个编码器层中,都有两个子层,这两个子层加上周围的链接结构就形成了两个子层连接结构.
 

#使用SublayerConnection来实现子层连接结构的类
class SublayerConnection(nn. Module):
    def __init__(self,size,dropout=0.1):
        '''
        它输入参数有两个,size以及dropout,size一般是都是词嵌入维度的大小,
        dropout本身是对模型结构中的节点数进行随机抑制的比率,
        又因为节点被抑制等效就是该节点的输出都是0,
        因此也可以把dropout看作是对输出矩阵的随机置0
        '''
        super(SublayerConnection,self).__init__()
        
         #实例化了规范化对象self.norm
        self.norm = LayerNorm(size)

        #又使用nn中预定义的droupout实例化一个self.dropout对象.
        self.dropout = nn.Dropout(p=dropout)

    def forward(self,x,sublayer):
        '''
        前向逻辑函数中,x是接收上一个层或者子层的输入作为第一个参数,
        将该子层连接中的子层函数作为第二个参数
        '''
        #我们首先对输出x进行规范化,然后将结果传给子层处理,之后再对子层进行dropout操作,
        #随机停止一些网络中神经元的作用,来防止过拟合.最后还有一个add操作,
        #因为存在跳跃连接,所以是将输入x与dropout后的子层输出结果相加作为最终的子层连接输出.
        return x + self.dropout(sublayer(self.norm(x)) )
#实例化参函数
d_model = 512
#线性变化的维度
head = 8
dropout= 0.2
size = 512

#输入参数
#令x为位置编码器输出
x = pe_result
mask = Variable(torch.zeros(8,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.shape:",sc_result.shape)

结果:

sc_result: tensor([[[-8.2046e+01,  4.1766e+01, -2.4870e+00,  ...,  6.7852e+00,
           6.9423e+00,  2.7157e+01],
         [-1.3258e-01, -3.4396e-01,  3.0957e-03,  ..., -5.2584e+00,
           7.4701e+01, -2.2027e+01],
         [ 2.6139e-01,  4.9363e+01,  2.4006e+01,  ...,  1.0152e+01,
           9.1406e-01, -1.8293e+01],
         [-3.0573e-02, -0.0000e+00, -2.9709e+01,  ..., -2.5909e+00,
          -9.2513e-02,  5.6313e+01]],

        [[-2.8320e+01, -3.1821e-01, -1.5847e+01,  ...,  1.0334e+01,
           1.8174e+01, -2.3099e+01],
         [-2.3792e+01,  8.9626e+00, -3.9537e+00,  ...,  0.0000e+00,
          -3.0325e+01,  6.0573e-02],
         [ 0.0000e+00, -8.9084e+00,  3.8686e+01,  ..., -1.4855e+01,
           5.1187e+01,  1.9176e-01],
         [ 4.6820e+00,  2.7350e+00, -4.2182e+00,  ..., -2.0125e-01,
           1.9034e+01, -3.3674e+01]]], grad_fn=<AddBackward0>)
sc_result.shape: torch.Size([2, 4, 512])

 2.7编码器层        

        作用:作为编码器的组成单元,每个编码器完成一次对输入的特征提取过程,即为编码过程。

#使用EncoderLayer类实现编码器层
class EncoderLayer(nn. Module) :
    def __init__(self,size,self_attn,feed_forward,dropout):
        '''
        它的初始化函数参数有四个:
        第一个是size,其实就是我们词嵌入维度的大小,它也将作为我们编码
        第二个self_attn,之后我们将传入多头自注意力子层实例化对象,并且是自注意力机制,
        第三个是feed_froward,之后我们将传入前馈全连接层实例化对象,最后一个是置0比率dropoout
        '''
        super(EncoderLayer, self).__init__()

        # 首先将self_attn和feed_forward传入其中.
        self.self_attn = self_attn
        self.feed_forward = feed_forward
        # 如图所示,编码器层中有两个子层连接结构,所以使用clones函数进行克隆
        self.sublayer = clones(SublayerConnection(size, dropout),2)#把size传入其中 一个Encoder有两个残差连接结构
        self.size = size

    def forward(self, x, mask):
        '''forward风的由有两个输入余数y和ma&k分代丰上―目的输出和塔E改是moc '''
        #里面就是按照结构图左侧的流程.首先通过第一个子层连接结构,其中包含多头自注意力子层,#然后通过第二个子层连接结构,其中包含前馈全连接子层.最后返回结果.
        x = self.sublayer[0](x,lambda x: self.self_attn(x,x,x,mask))
        return self.sublayer[1](x,self.feed_forward)
#实例化参数
size = 512
head = 8
d_model =512

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(8,4,4))

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

输出:

el_result: tensor([[[ 1.5426e+01,  5.1705e+01,  1.7282e+01,  ...,  1.4393e+01,
          -2.2006e-01,  2.7038e+01],
         [-1.7512e+01,  4.5033e+00, -1.1306e-01,  ..., -3.6896e+01,
          -1.5540e+01, -3.1290e+01],
         [ 8.8698e+00, -2.8196e+00, -5.5794e+01,  ...,  9.1394e+00,
           4.7457e-02, -6.1556e+00],
         [ 7.6480e-01, -1.5347e+01, -3.0350e+00,  ..., -2.1523e+00,
          -2.4606e+01,  4.8328e+01]],

        [[-3.1572e+01,  3.0826e+01, -4.3435e+00,  ..., -4.0304e-01,
           3.3149e+01, -3.3336e+01],
         [-3.9945e+01, -1.8512e-01,  1.0413e+00,  ..., -2.8906e+00,
          -1.5209e+00, -1.9260e+01],
         [-2.1865e-01, -6.1427e+00, -8.9143e+00,  ..., -2.9637e-01,
           6.3482e+01,  4.2338e+01],
         [-2.7011e+01,  1.6092e-01,  1.1102e-01,  ..., -1.1949e-01,
          -5.2112e+01, -2.2194e+01]]], grad_fn=<AddBackward0>)
el_result.shape: torch.Size([2, 4, 512])

2.8 编码器 

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

#使用Encoder类来实现编码器
class Encoder(nn.Module):
    def __init__(self,layer,N):
        #初始化函数的两个参数分别代表编码器层和编码器层的个数
        super(Encoder,self).__init__()
        #首先使用clones函数克隆N个编码器放在self.layer中
        self.layers = clones(layer, N)
        #在初始化一个规范化层,他将作用在编码器层后面
        self.norm =  LayerNorm(layer.size)

    def forward(self, x, mask):
        #forward函数的输入和编码器相同,x代表上一层的输出,mask代表掩码张量
        #首先就是对我们克隆的编码器进行循环,每次都会得到一个新的x
        #这个循环的过程,就相当于输出的x经过N个编码器层的处理
        #最后在经过规范化层的对象self.norm进行处理,最后返回结果
        for layer in self.layers:
            x = layer(x,mask)
        return self.norm(x)
#实例化参数
size = 512
head = 8
d_model =512
x = pe_result
d_ff = 64
c = copy.deepcopy  #copy.copy()是浅拷贝,只拷贝父对象,不会拷贝对象的内部的子对象。copy.deepcopy()是深拷贝,会拷贝对象及其子对象
dropout = 0.2
attn = MultiHeadedAttention(head, d_model)
ff = PositionwiseFeedForward(d_model, d_ff, dropout)
mask = Variable(torch.zeros(8,4,4))
N = 8
layer = EncoderLayer(size,c(attn),c(ff),dropout)  #c(attn) 深度拷贝一个注意力机制的对象

#调用
en = Encoder(layer,N)
en_result = en(x,mask)

print("en_result:",en_result)
print("en_result.shape:",en_result.shape)

结果:

en_result: tensor([[[ 0.9467, -0.1019, -1.2170,  ...,  0.3434,  1.4108, -0.5786],
         [ 0.3837,  1.3165,  0.7040,  ..., -0.1623, -1.8425,  1.1943],
         [-0.3850,  0.0327,  1.3240,  ...,  0.1747,  1.8380, -1.2386],
         [-0.1095,  1.3437,  1.4893,  ..., -0.6668,  0.4197,  0.0159]],

        [[-1.4486, -0.6303,  1.3219,  ..., -0.0162,  2.1937,  1.2729],
         [ 0.1721, -1.6509,  1.5538,  ...,  2.1295,  0.7422,  0.0251],
         [ 0.6134,  0.5718,  0.8994,  ..., -0.0147, -0.1076,  0.2205],
         [-2.6184, -1.5628, -0.6892,  ...,  2.0367, -0.9032, -1.0209]]],
       grad_fn=<AddBackward0>)
en_result.shape: torch.Size([2, 4, 512])

3.解码器部分实现:

        解码器部分:

                由N个解码器层堆叠组成

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

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

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

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

        说明:解码器层中的各个部分,如多头注意力机制,规范化层,前馈全连接网络以及残差结构都和编码器中的实现相同

3.1 解码器层

作用:作为解码器层根据给定输入输入向目标方向进行特征提取操作,即为解码过程。编码器是根据给定的输入提取特征。

假设你有一本菜谱,这本菜谱有很多章节,每个章节都是文字描写如何烹饪不同的菜。现在我们讲所有文字输入到Transformer当中,注意此时STransformeregNet接收到的只是文字信息,它并不知道这本菜谱的构成。Encoder所做的就是它能理解这本菜谱的结构,对其中的内容进行抽象,形成一些高阶的信息(比如描述做了什么菜),并将这些信息对应到一个简化的空间里。Decoder对这些简化的高阶信息进行理解与编译,并且对相同语义相近的文字进行分类并对应到具体的菜式上面(比如:凉菜,面食,鱼,肉等)。这样以来,仅从文字上,我们就可以大概知道这是描述哪个菜式了,可能是凉菜,也可能是面食。换句话说,你不仅能获得菜式信息,并且可以知道这些菜式对应的是哪一个章节,哪些字句,是不是很棒?

#使用DecoderLayer的类实现解码器层
class DecoderLayer(nn.Module):
    def __init__(self,size, self_attn,src_attn,feed_forward, dropout):
        '''初始化参数的参数本5个,
        第一个是size.代表词嵌入的维度大小,同时也代表解码器层的尺寸,
        第二个是self_attn,多头自注意力对象,也就是说这个注意力机制需要Q=K=V,
        第三个是src_attn,多头注意力对象,这里Q!=K=V,
        第四个是前馈全连接层对象,
        最后就是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函数克隆三个子层连接对象.
        self.sublayer = clones(SublayerConnection(size, dropout),3)

    def forward(self, x, memory,source_mask,target_mask):
        '''forward函数中的参数有4个,
        第一个是来自上一层的输入x,
        第二个是来自编码器层的语义存储变量mermory, #编码输出的张量就是memory 编码器的输出代表编码器对于原始文本的语义抽取
        以及源数据掩码张量source_mask和目标数据掩码张量target_mask.
        '''
        #将memory表示成m方便之后使用
        m = memory

        #将x传入第一个子层结构,第一个子层结构的输入分别是x和self-attn函数,因为是自注意力机制,所以Q=K=V
        #最后一个参数是目标数据掩码张量,这时要对目标数据进行遮掩,因为此时模型可能还没有生成任何目标数据
        #比如在解码器准备生成第一个字符或词汇时,我们其实已经传入了第一个字符以便计算损失,
        #但是我们不希望在生成第一个字符时模型能利用这个信息,因此我们会将其遮掩,同样生成第二个字符或词汇时
        #模型只能使用第一个字符或词汇信息,第二个字符以及之后的信息都不允许被模型使用.
        x = self.sublayer[0](x,lambda x: self.self_attn(x, x, x, target_mask))

        #接着进入第二个子层,这个子层中常规的注意力机制,q是输入x;k,v是编码层输出memory
        #同样也传入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)
#实例化参数
#类的实例化参数与解码器层类似,相比多出了src_attn,但是和self_attn是同一个类.head = 8
size = 512
d_model = 512
d_ff =64
dropout = 0.2
self_attn = src_attn = MultiHeadedAttention(head, d_model,dropout)  #只是为了简单演示
#前馈全连接层也和之前相同
ff = PositionwiseFeedForward(d_model,d_ff,dropout)

#输入参数
#x是来自目标数据的词嵌入表示,但形式和源数据的词嵌入表示相同,这里使用per充当.
x = pe_result
#memory是来自编码器的输出
memory = en_result
#实际中source_mask和target_mask并不相同,这里为了方便计算使他们都为mask
mask = Variable(torch.zeros(8,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)

 结果:

tensor([[[-6.4884e-02,  4.7482e-01, -5.9966e+01,  ..., -4.2434e-01,
           3.9713e+01, -2.2336e+01],
         [ 3.7793e+01, -4.4837e+01,  2.1298e-01,  ..., -1.8226e+01,
           5.3589e+01,  2.1755e+01],
         [-2.0116e+01, -2.1733e-01, -2.0253e+01,  ...,  8.5459e+00,
          -4.3410e+01,  4.4864e+00],
         [-7.1186e+01, -5.9868e+01,  9.6210e+00,  ...,  9.6792e+00,
          -6.9601e+00,  1.0633e+01]],

        [[ 3.9183e-02,  1.9656e+01, -5.3197e+01,  ..., -1.9261e-01,
          -1.4693e-01, -5.9193e+00],
         [-2.2901e+01, -1.7759e+00,  2.5482e+01,  ..., -6.9718e-02,
          -4.4508e+01, -4.5777e+01],
         [ 2.2915e+01,  3.0768e-01, -1.9758e+01,  ..., -1.5927e+01,
           1.6447e+01, -2.0908e+01],
         [-1.7846e+01, -3.4286e+01, -6.6995e+00,  ...,  5.9093e+00,
          -8.3544e+00, -1.0876e+01]]], grad_fn=<AddBackward0>)
torch.Size([2, 4, 512])

 3.2 解码器

        作用:根据解码器的结果以及上一次预测的结果,对下一次可能出现的值进行特征表示

#使用类Decoder来实现解码器
class Decoder(nn.Module) :
    def __init__(self, layer,N):
        '''
        初始化函数的参数有两个第一个就是解码器层1ayer,第二个是解码器层个数N
        '''
        super(Decoder, self).__init__()
        #首先使用clones方法克隆了N个layer,然后实例化了一个规范化层.
        #因为数据走过了所有的解码器层后最后要做规范化处理.
        self.layers = clones(layer,N)
        self.norm = LayerNorm(layer.size)

    def forward(self, x,memory,source_mask,target_mask):
        '''forward函数中的参数有4个.x代表目标数据的嵌入表示,memory是编码器层的输出,
        以及源数据掩码张量source_mask和目标数据掩码张量target_mask'''

        #然后就是对每个层进行循环,当然这个循环就是变量x通过每一个层的处理,
        #得出最后的结果,再进行一次规范化返回即可.
        for layer in self.layers:
            x = layer(x, memory,source_mask, target_mask)
        return self.norm(x)
#实例化参数
#分别是解码器层layer和解码器层的个数Nsize = 512
d_model = 512
head = 8
d_fI = 64
dropout = 0.2
c =copy.deepcopy
attn = MultiHeadedAttention(head,d_model)
ff = PositionwiseFeedForward(d_model, d_ff, dropout)
layer = DecoderLayer(d_model, c(attn) , c(attn), c(ff), dropout)  #c(attn) , c(attn), c(ff)进行深度复制,在内存中占用不同地址,互不干扰
N= 8

#输入参数:
#输入参数与解码器层的输入参数相同
x = pe_result
memory = en_result
mask = Variable(torch.zeros(8,4,4))
source_mask = target_mask = mask

#调用
de = Decoder(layer,N)
de_result = de(x,memory,source_mask,target_mask)
print(de_result)
print(de_result.shape)

结果:

tensor([[[ 0.0680,  0.3388,  0.3694,  ..., -1.4013, -0.9994, -0.0787],
         [-2.3353,  0.7284, -0.0312,  ..., -1.5196, -0.9640,  0.0024],
         [ 0.1278, -1.1433, -0.3626,  ...,  0.2243, -1.1858,  0.7119],
         [ 1.5512,  2.1713,  1.6186,  ..., -0.2259,  0.1864, -0.3596]],

        [[ 0.6581, -0.0125,  0.7877,  ..., -0.0308, -0.8433, -0.1474],
         [ 2.2406, -1.1255,  1.9449,  ...,  1.5241, -0.4336, -0.0262],
         [ 1.4591,  1.1773, -0.2203,  ...,  0.4344,  1.9302,  2.2946],
         [-0.0497, -0.9295,  1.1276,  ..., -1.2434,  0.4723, -1.2626]]],
       grad_fn=<AddBackward0>)
torch.Size([2, 4, 512])

4.输出部分实现

        输出包含: 线性层和softmax层

        线性层作用:通过对上一步的线性变化得到指定的输出,也就是转化维度的作用

        softmax层作用:使最后一维向量中的数字缩放到0-1的概率内,并满足他们的和为1

# nn.functional工具包装载了网络层中那些只进行计算,而没有参数的层
import torch.nn.functional as F
#将线性层和softmax计算层一起实现,因为二者的共同目标是生成最后的结构
#因此把类的名字叫做Generator,生成器类

class Generator(nn. Module):
    def __init__(self, dnodel,vocab_size):
        '''初始化函数的输入参数有两个. d model代表词嵌入维度vocab_size代表词表大小'''
        super(Generator, self).__init__()

        #首先就是使用nn中的预定义线性层进行实例化,得到一个对象self.project等待使用,
        #这个线性层的参数有两个,就是初始化函数传进来的两个参数:d_model,vocab_size
        self.project = nn.Linear(d_model,vocab_size)

    def forward(self, x):
        '''前向逻辑函数中输入是一层的输出张最×'''
        #在函数中,首先使用上一步得到的self.project对x进行线性变化,
        #然后使用F中已经实现的log_softmax进行的softmax处理.
        #在这里之所以使用log_softmax是因为和我们这个pytorch版本的损失函数实现有关,在其他版本
        #log_softmax就是对softmax的结果又取了对数,因为对数函数是单调递增函数,
        #因此对最终我们取最大的概率值没有影响.最后返回结果即可.
        return F.log_softmax(self.project(x), dim=-1)
#实例化参数
#词嵌入维度
d_model = 512
vocab_size = 1000 #c词表大小

#输入参数
#输入x是上一层网络的输出,我们使用来自解码器的输出
x = de_result

#调用
gen = Generator(d_model,vocab_size)
gen_result = gen(x)
print("gen_result",gen_result)
print("gen_result.shape",gen_result.shape)

结果:

gen_result tensor([[[-7.3721, -7.6565, -7.4275,  ..., -6.9557, -7.2538, -6.2844],
         [-6.8687, -7.8624, -8.4473,  ..., -5.9359, -7.4111, -7.2476],
         [-6.1762, -6.6304, -6.0642,  ..., -6.0703, -8.1359, -7.8970],
         [-7.6870, -8.0672, -7.7164,  ..., -7.7060, -6.9981, -6.1902]],

        [[-6.5913, -7.7518, -7.6995,  ..., -6.5857, -6.4197, -7.0835],
         [-7.9634, -7.4495, -6.8146,  ..., -6.8459, -7.3948, -7.7325],
         [-7.0604, -7.3653, -7.3081,  ..., -6.8293, -6.8729, -6.5208],
         [-6.7996, -5.3331, -6.8227,  ..., -6.9652, -7.7622, -6.9104]]],
       grad_fn=<LogSoftmaxBackward>)
gen_result.shape torch.Size([2, 4, 1000]) 

4 模型构建 

#使用EncoderDecoder类来实现编码器-解码器结构
class EncoderDecoder(nn . Module):
    def __init__(self,encoder,decoder,source_embed,target_embed,generator):
        '''
        初始化函数中有5个参数,分别是编码器对象,解码器对象,
        源数据嵌入函数,目标数据嵌入函数,以及输出部分的类别生成器对象
        '''
        super (EncoderDecoder, self). __init__()#将参数传入到类中
        self.encoder = encoder
        self.decoder = decoder
        self.src_embed = source_embed
        self.tgt_embed = target_embed
        self.generator = generator
    def forward(self,source,target,source_mask, target_mask):
        '''
        在forward函数中,有四个参数,source代表源数据,target代表目标数据,
        以及源数据掩码张量source_mask和目标数据掩码张量target_mask
        '''
        # 在函数中,将source,source_mask传入编码函数,得到结果后,
        #与source_mask,target和target_mask一同传给解码函数.
        return self.decode (self.encode(source,source_mask),source_mask,target,target_mask) #self.encode函数和 self.decode函数在后面
    def encode(self, source,source_mask):
        '''编码函数,以source和source_mask为参数'''
        #使用src_embed对source做处理,然后和source_mask一起传给self.encoder
        return self.encoder(self.src_embed(source),source_mask)  #self.src_embed(source)是完成word embedding
    
    def decode(self, memory,source_mask, target, target_mask):
        '''解码函数 以memory即编码器的输出( self.encoder(self.src_embed(source),source_mask)). 
        source mask. taraet. taraet mask为参数'''
        #使用tgt_embed对target做处理,然后和source_mask,target_mask,memory一起传给se
        return self.decoder(self.tgt_embed(target), memory,source_mask,target_mask)
#实例化参数
vocab_size = 1000
d_model = 512
encoder = en
decoder = de
source_embed = nn.Embedding(vocab_size,d_model)
target_embed = nn.Embedding(vocab_size,d_model)
generator = gen


#假设源数据与目标数据相同,实际中并不相同
source = target = Variable(torch.LongTensor([[100,2,421,508],[491,998,1,221]]))
#假设src_mask与tgt_mask相同,实际中并不相同
source_mask = target_mask = Variable(torch.zeros(8,4,4))

#调用
ed = EncoderDecoder(encoder,decoder,source_embed,target_embed,generator)
ed_result = ed(source,target,source_mask, target_mask)
print(ed_result)
print(ed_result.shape)

结果:

tensor([[[ 2.1606, -1.0481, -0.8626,  ..., -2.7846,  0.0970, -0.1060],
         [ 1.6086, -1.6404, -0.9891,  ..., -3.1672,  0.7472, -0.4660],
         [ 2.1196, -1.8632, -0.7462,  ..., -2.6502, -0.2004, -0.2501],
         [ 1.0647, -1.2531, -0.5254,  ..., -2.5127, -0.2024, -0.5688]],

        [[-0.3991,  0.0507, -0.6292,  ..., -0.1926, -1.7896, -0.1960],
         [ 0.2242,  0.0077, -0.8503,  ..., -0.3392, -1.4279, -0.6860],
         [-0.6804,  0.7188, -0.3762,  ..., -1.2633, -1.3671, -0.8991],
         [-0.2715,  0.5633, -0.8857,  ..., -0.6518, -0.7866, -0.4109]]],
       grad_fn=<AddBackward0>)
torch.Size([2, 4, 512])

5. 构建可训练模型

#Tansformer模型构建过程的代码分析:
def make_model(source_vocab,target_vocab,N=6,d_model=512,d_ff=2048,head=8,dropout=0.1):
    '''
    该函数用来构建模型,有7个参数,分别是源数据特征(词汇)总数,目标数据特征(词汇)总数,
    编码器和解码器堆叠数,词向量映射维度,前馈全连接网络中变换矩阵的维度,
    多头注意力结构中的多头数,以及置零比率dropout.  #d_ff优化模型参数的时候需要用,作为超参数
    '''
    # 首先得到一个深度拷贝命令,接下来很多结构都需要进行深度拷贝,
    #来保证他们彼此之间相互独立,不受干扰.
    c = copy.deepcopy

    #实例化了多头注意力类,得到对象attn
    attn = MultiHeadedAttention( head, d_model)

    #然后实例化前馈全连接类,得到对象ff
    ff = PositionwiseFeedForward(d_model, d_ff, dropout)

    #实例化位置编码类,得到对象position
    position = PositionalEncoding( d_model,dropout)

    #根据结构图,最外层是EncoderDecoder,在EncoderDecoder中,
    #分别是编码器层,解码器层,源数据Embedding层和位置编码组成的有序结构,
    #目标数据Embedding层和位置编码组成的有序结构,以及类别生成器层.
    #在编码器层中有attention子层以及前馈全连接子层,
    #在解码器层中有两个attention子层以及前馈全连接层.
    model = EncoderDecoder(Encoder(EncoderLayer(d_model, c(attn),c(ff),dropout),N), #编码器对象
                           Decoder(DecoderLayer(d_model, c(attn), c(attn),c(ff),dropout),N),#解码器对象
                           nn.Sequential(Embeddings(d_model,source_vocab),c(position)),#源数据嵌入函数
                           nn.Sequential(Embeddings(d_model,target_vocab),(position)),#目标数据嵌入函数
                           Generator( d_model, target_vocab)) #输出部分的类别生成器对象
    #nn.Sequential() 可以允许将整个容器视为单个模块(即相当于把多个模块封装成一个模块),
    #forward()方法接收输入之后,nn.Sequential()按照内部模块的顺序自动依次计算并输出结果。
    #模型结构完成后,接下来就是初始化模型中的参数,比如线性层中的变换矩阵
    #这里一但判断参数的维度大于1,则会将其初始化成一个服从均匀分布的矩阵,
    #初始化作用:如果初始化值很小,那么随着层数的传递,方差就会趋于0,此时输入值 也变得越来越小,在sigmoid上就是在0附近,接近于线性,失去了非线性
    #如果初始值很大,那么随着层数的传递,方差会迅速增加,此时输入值变得很大,而sigmoid在大输入值写倒数趋近于0,反向传播时会遇到梯度消失的问题
    for p in model.parameters(): #模型model参数parameters
        if p.dim() >1:
            nn.init.xavier_uniform(p)
    return model

#实例化参数
#输入参数:
source_vocab = 11
target_vocab = 11
N= 6
# 其他参数都使用默认值
#调用:
if __name__ == '__main__' :
    res = make_model(source_vocab, target_vocab,N)
    print("res:",res)

结果:

res: EncoderDecoder(
  (encoder): Encoder(
    (layers): ModuleList(
      (0): EncoderLayer(
        (self_attn): MultiHeadedAttention(
          (linears): ModuleList(
            (0): Linear(in_features=512, out_features=512, bias=True)
            (1): Linear(in_features=512, out_features=512, bias=True)
            (2): Linear(in_features=512, out_features=512, bias=True)
            (3): Linear(in_features=512, out_features=512, bias=True)
          )
          (dropout): Dropout(p=0.1, inplace=False)
        )
        (feed_forward): PositionwiseFeedForward(
          (w1): Linear(in_features=512, out_features=2048, bias=True)
          (w2): Linear(in_features=2048, out_features=512, bias=True)
          (dropout): Dropout(p=0.1, inplace=False)
        )
        (sublayer): ModuleList(
          (0): SublayerConnection(
            (norm): LayerNorm()
            (dropout): Dropout(p=0.1, inplace=False)
          )
          (1): SublayerConnection(
            (norm): LayerNorm()
            (dropout): Dropout(p=0.1, inplace=False)
          )
        )
      )
      (1): EncoderLayer(
        (self_attn): MultiHeadedAttention(
          (linears): ModuleList(
            (0): Linear(in_features=512, out_features=512, bias=True)
            (1): Linear(in_features=512, out_features=512, bias=True)
            (2): Linear(in_features=512, out_features=512, bias=True)
            (3): Linear(in_features=512, out_features=512, bias=True)
          )
          (dropout): Dropout(p=0.1, inplace=False)
        )
        (feed_forward): PositionwiseFeedForward(
          (w1): Linear(in_features=512, out_features=2048, bias=True)
          (w2): Linear(in_features=2048, out_features=512, bias=True)
          (dropout): Dropout(p=0.1, inplace=False)
        )
        (sublayer): ModuleList(
          (0): SublayerConnection(
            (norm): LayerNorm()
            (dropout): Dropout(p=0.1, inplace=False)
          )
          (1): SublayerConnection(
            (norm): LayerNorm()
            (dropout): Dropout(p=0.1, inplace=False)
          )
        )
      )
      (2): EncoderLayer(
        (self_attn): MultiHeadedAttention(
          (linears): ModuleList(
            (0): Linear(in_features=512, out_features=512, bias=True)
            (1): Linear(in_features=512, out_features=512, bias=True)
            (2): Linear(in_features=512, out_features=512, bias=True)
            (3): Linear(in_features=512, out_features=512, bias=True)
          )
          (dropout): Dropout(p=0.1, inplace=False)
        )
        (feed_forward): PositionwiseFeedForward(
          (w1): Linear(in_features=512, out_features=2048, bias=True)
          (w2): Linear(in_features=2048, out_features=512, bias=True)
          (dropout): Dropout(p=0.1, inplace=False)
        )
        (sublayer): ModuleList(
          (0): SublayerConnection(
            (norm): LayerNorm()
            (dropout): Dropout(p=0.1, inplace=False)
          )
          (1): SublayerConnection(
            (norm): LayerNorm()
            (dropout): Dropout(p=0.1, inplace=False)
          )
        )
      )
      (3): EncoderLayer(
        (self_attn): MultiHeadedAttention(
          (linears): ModuleList(
            (0): Linear(in_features=512, out_features=512, bias=True)
            (1): Linear(in_features=512, out_features=512, bias=True)
            (2): Linear(in_features=512, out_features=512, bias=True)
            (3): Linear(in_features=512, out_features=512, bias=True)
          )
          (dropout): Dropout(p=0.1, inplace=False)
        )
        (feed_forward): PositionwiseFeedForward(
          (w1): Linear(in_features=512, out_features=2048, bias=True)
          (w2): Linear(in_features=2048, out_features=512, bias=True)
          (dropout): Dropout(p=0.1, inplace=False)
        )
        (sublayer): ModuleList(
          (0): SublayerConnection(
            (norm): LayerNorm()
            (dropout): Dropout(p=0.1, inplace=False)
          )
          (1): SublayerConnection(
            (norm): LayerNorm()
            (dropout): Dropout(p=0.1, inplace=False)
          )
        )
      )
      (4): EncoderLayer(
        (self_attn): MultiHeadedAttention(
          (linears): ModuleList(
            (0): Linear(in_features=512, out_features=512, bias=True)
            (1): Linear(in_features=512, out_features=512, bias=True)
            (2): Linear(in_features=512, out_features=512, bias=True)
            (3): Linear(in_features=512, out_features=512, bias=True)
          )
          (dropout): Dropout(p=0.1, inplace=False)
        )
        (feed_forward): PositionwiseFeedForward(
          (w1): Linear(in_features=512, out_features=2048, bias=True)
          (w2): Linear(in_features=2048, out_features=512, bias=True)
          (dropout): Dropout(p=0.1, inplace=False)
        )
        (sublayer): ModuleList(
          (0): SublayerConnection(
            (norm): LayerNorm()
            (dropout): Dropout(p=0.1, inplace=False)
          )
          (1): SublayerConnection(
            (norm): LayerNorm()
            (dropout): Dropout(p=0.1, inplace=False)
          )
        )
      )
      (5): EncoderLayer(
        (self_attn): MultiHeadedAttention(
          (linears): ModuleList(
            (0): Linear(in_features=512, out_features=512, bias=True)
            (1): Linear(in_features=512, out_features=512, bias=True)
            (2): Linear(in_features=512, out_features=512, bias=True)
            (3): Linear(in_features=512, out_features=512, bias=True)
          )
          (dropout): Dropout(p=0.1, inplace=False)
        )
        (feed_forward): PositionwiseFeedForward(
          (w1): Linear(in_features=512, out_features=2048, bias=True)
          (w2): Linear(in_features=2048, out_features=512, bias=True)
          (dropout): Dropout(p=0.1, inplace=False)
        )
        (sublayer): ModuleList(
          (0): SublayerConnection(
            (norm): LayerNorm()
            (dropout): Dropout(p=0.1, inplace=False)
          )
          (1): SublayerConnection(
            (norm): LayerNorm()
            (dropout): Dropout(p=0.1, inplace=False)
          )
        )
      )
    )
    (norm): LayerNorm()
  )

...

6.模型基本运行

        目标:完成一个小的copy任务的基本测试工作 

copy任务介绍:
任务描述:针对数字序列进行学习,学习的最终目标是使输出与输入的序列相同.如输入[1,5,8,9,3],输出也是[1,5,8,9,3].
任务意义: copy任务在模型基础测试中具有重要意义,因为copy操作对于模型来讲是一条明显规律,因此模型能否在短时间内,小数据集中学会它,可以帮助我们断定模型所有过程是否正常,是否已具备基本学习能力.


使用copy任务进行模型基本测试的四步曲:
第一步:构建数据集生成器
第二步:获得Transformer模型及其优化器和损失函数

第三步:运行模型进行训练和评估
第四步:使用模型进行贪婪解码

6.1 构建数据集生成器 

# 导入工具包Batch,它能够对原始样本数据生成对应批次的掩码张量
from pyitcast.transformer_utils import Batch

def data_generator(V,batch,num_batch) :
    '''该函数用干随机生成copy任务的数据,它的三个输入参数是V︰随机生成数字的最大值+1,
    batch:每次输送给模型更新一次参数的数据量,num_batch:一共输送num_batch次完成一轮
    '''
    #使用for循环遍历nbatches
    for i in range (num_batch):
        #在循环中使用np的random.randint方法随机生成[1,V)的整数,
        #分布在(batch,10)形状的矩阵中,然后再把numpy形式转换称torch中的tensor.
        data = torch.from_numpy(np.random.randint(1,V,size=(batch,10)))

        #接着使数据矩阵中的第一列数字都为1,这一列也就成为了起始标志列,
        #当解码器进行第一次解码的时候,会使用起始标志列作为输入.
        data[:, 0] = 1

        #因为是copy任务,所有source与target是完全相同的,且数据样本作用变量不需要求梯度
        #因此requires_grad设置为False
        source = Variable (data,requires_grad=False)
        target = Variable ( data,requires_grad=False)

        #使用Batch对source和target进行对应批次的掩码张量生成,最后使用yield返回一个生成器对象
        yield Batch( source, target) #yield 是一个类似 return 的关键字,迭代一次遇到yield时就返回yield后面(右边)的值。重点是:下一次迭代时,从上一次迭代遇到的yield后面的代码(下一行)开始执行。
#将生成B-10的整数
V = 11
#每次喂给模型20个数据进行参数更新
batch = 20
#连续喂3日次完成全部数据的遍历,也就是1轮
num_batch = 30

#调用:
if __name__ == '__main__' :
    res = data_generator (V, batch,num_batch)
    print("res:",res)

结果:

res: <generator object data_generator at 0x00000226795EC9E0>

6.2 获得Transformer模型及其优化器和损失函数

#获得Transformer模型及其优化器和损失函数
#导入优化器工具包ge _std_opt,该工具用于获得标准的针对Transformer模型的优化器
#该标准优化器基于Adam优化器,使其对序列到序列的任务更有效.
from pyitcast.transformer_utils import get_std_opt
#导入标签平滑工具包,该工具用于标签平滑,标签平滑的作用就是小幅度的改变原有标签值的值域#因为在理论上即使是人工的标注数据也可能并非完全正确,
#会受到一些外界因素的影响而产生一些微小的偏差
#因此使用标签平滑来弥补这种偏差,减少模型对某一条规律的绝对认知,以防止过拟合
from pyitcast.transformer_utils import LabelSmoothing

#导入损失计算工具包,该工具能够使用标签平滑后的结果进行损失的计算,
#损失的计算方法可以认为是交叉嫡损失函数.
from pyitcast.transformer_utils import SimpleLossCompute


#使用make_model获得model
model = make_model(V,V,N=2)
#使用get_std_opt获得模型优化器
model_optimizer = get_std_opt(model)
#使用LabelSmoothing获得标签平滑对象
criterion = LabelSmoothing(size=V, padding_idx=0,smoothing=0.0)
#使用SimpleLossCompute获得利用标签平滑结果的损失计算方法
loss = SimpleLossCompute(model.generator, criterion,model_optimizer)
#使用LabelSmoothing实例化一个crit对象.
#第一个参数size代表目标数据的词汇总数,也是模型最后一层得到张量的最后一维大小#这里是5说明目标词汇总数是5个.第二个参数padding_idx表示要将那些tensor中的数字
#替换成0,一般padding_idx=0表示你进行替换.第三个参数smoothing,表示标签的平滑程度#如原来标签的表示值为1,则平滑后它的值域变为[1-smoothing,1+smoothing] .
crit = LabelSmoothing(size=5,padding_idx=0,smoothing=0.5)
#假定一个任意的模型最后输出预测结果和真实结果
predict = Variable(torch.FloatTensor([[0, 0.2,0.7,0.1,0],
                                        [ 0,0.2,0.7,0.1,0 ] ,
                                        [0,0.2,0.7,0.1,0]]))
#标签的表示值是0,1,2
target = Variable(torch. LongTensor([2,1,0]))
#将predict,target传入到对象中
crit(predict,target)
#绘制标签平滑图像
plt.imshow(crit.true_dist)
plt.show()

 标签平滑图像分析:
我们目光集中在黄色小方块上,它相对于横坐标横跨的值域就是标签平滑后的正向平滑值域,我们可以看到大致是从0.5到2.5.
它相对于纵坐标横跨的值域就是标签平滑后的负向平滑值域,我们可以看到大致是从-0.5到1.5,总的值域空间由原来的[0,2]变成了[-0.5,2.5].

6.3 模型训练

#导入模型单轮训练工具包run_epoch,该工具将对模型使用给定的损失函数计算方法进行单轮参染更新.#并打印每轮参数更新的损失结果.
from pyitcast.transformer_utils import run_epoch
def run(model,loss,epochs=10):
    '''
    模型训练函数共有三个参数, model代表将要进行训练的模型
    loss代表使用的损失计算方法 epochs代表模型训练的轮数
    '''
    #遍历轮数
    for epoch in range ( epochs) :
        #模型使用训练模式,所有参数将被更新model.train()
        #训练时,batch_size是20
        run_epoch(data_generator(V,8,20), model,loss)
        #模型使用评估模式,参数将不会变化
        model.eval()
        #评估时,batch_size是5
        run_epoch(data_generator(V,8,5),model,loss)
#输入参数
#进行10轮训练
epochs = 10

#调用:
if __name__ == '__main__' :
    run(model,loss)

结果:

Epoch Step: 1 Loss: 3.500957 Tokens per Sec: 368.571381
Epoch Step: 1 Loss: 2.636759 Tokens per Sec: 444.564026
Epoch Step: 1 Loss: 2.428092 Tokens per Sec: 426.184845
Epoch Step: 1 Loss: 2.173620 Tokens per Sec: 466.073334
Epoch Step: 1 Loss: 2.247338 Tokens per Sec: 421.227356
Epoch Step: 1 Loss: 1.879305 Tokens per Sec: 496.500763
Epoch Step: 1 Loss: 1.597706 Tokens per Sec: 473.709625
Epoch Step: 1 Loss: 1.734927 Tokens per Sec: 494.792847
Epoch Step: 1 Loss: 1.642253 Tokens per Sec: 498.212708
Epoch Step: 1 Loss: 1.725536 Tokens per Sec: 498.226257
Epoch Step: 1 Loss: 1.663171 Tokens per Sec: 498.225433
Epoch Step: 1 Loss: 1.502501 Tokens per Sec: 483.219391
Epoch Step: 1 Loss: 1.597845 Tokens per Sec: 493.115967
Epoch Step: 1 Loss: 1.490417 Tokens per Sec: 488.112976
Epoch Step: 1 Loss: 1.486048 Tokens per Sec: 488.112183
Epoch Step: 1 Loss: 1.108046 Tokens per Sec: 480.008484
Epoch Step: 1 Loss: 1.254724 Tokens per Sec: 486.477875
Epoch Step: 1 Loss: 1.015205 Tokens per Sec: 508.782501
Epoch Step: 1 Loss: 0.974149 Tokens per Sec: 496.498718
Epoch Step: 1 Loss: 1.017743 Tokens per Sec: 505.178894

6.4使用模型进行贪婪解码

    #第四步:使用模型进行贪婪解码
    #模型进入测试模式
    model.eval()

    #假定的输入张量
    source = Variable(torch.LongTensor([[ 1,3,2,5,4,6,7,8,9,10]]))

    #定义源数据掩码张量,因为元素都是1,在我们这里1代表不遮掩
    #因此相当于对源数据没有任何遮掩.
    source_mask = Variable (torch.ones(1,1,10))  #10是因为输入张量是10个

    #最后将model,src,src_mask,解码的最大长度限制max_len,默认为10#以及起始标志数字,默认为1,我们这里使用的也是1
    result = greedy_decode(model,source,source_mask,max_len=10,start_symbol=1)
    #贪婪预测,即在每个阶段采用得分最高的项。也就是最有可能的项。算法速度快,但是准确度不高。最后输出的是该项对应的索引值,之后在字典中查找对应的单词,就能得到恢复后的单词序列。
    print(result)

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值