当前位置:AIGC资讯 > AIGC > 正文

AIGC入门(一) 从零开始搭建Transformer!(上)

前言

我记得曾经看过一篇综述,综述里曾这样讲过:

多模态使用Transformer作为基石模型的一个原因之一,就是因为它能够很好地统一视觉(ViT、DiT)和文本,并且无限制地扩大其参数。这是一个在工程上很有作用也很有实际意义的事情。

笔者曾经狠狠地读过论文原文,看过无数多的解析,然而只大致知道其中的数学原理,大致是什么流程,哪个模块要干些啥,哪种设计是什么意思,但是从来都是迷迷糊糊颠来倒去的。网上的代码讲解一大堆,但是都乱七八糟的,很多还不全面。

现在,在准备一些事情的同时,笔者决定好好地去解析一下Transformer相关的论文源码了!

笔者便决定狠狠地从头写一遍transformer的相关代码(造轮子),这也能督促自己从更深刻的角度去了解Transformer的思想。

因此,笔者将这篇文章的叙述重点放在了代码上,故很多的介绍重点都在代码的注释里。为了更好的阅读体验,请最好用电脑阅读!

目标:我们要使用我们的transformer来完成一个简单的翻译任务。

话不多说,马上开始!

(注意:我们的这个只是一个学习级别的Demo,因此数据集、模型参数、训练批次都不会很大。主要是从代码入手,跑通整个transformer的架构。因此最终的效果请不要介意。)

这篇文章旨在从代码级别简略介绍Transformer的结构特征。中间会穿插一些知识点方便记忆。

本篇文章代码参考了The Annotated Transformer。笔者在此基础上做了较大改动,将一些比较难懂的部分拆开来重新梳理了一遍,并且将原先的简单copy任务换成了论文原文的翻译任务。并使得代码阅读的难易度更偏向小白。受文章字数限制,本文分上下两篇。

获得本文章已完成的相关代码请点击这里,请自己整理任意文本(chinese.txt)到文本(english.txt)的数据集。参考网站在这里。

这期文章面向的是想要入门Transformer的新手,希望这篇文章对你有所帮助。大佬可以指出其中的错误,感谢不尽!另外,用电脑阅读的效果比用手机阅读的效果要好上不少。

最后完成的代码组成结构如下。我们将逐个文件开始依次入手编写。

一共是六个文件和一个小型自建数据集。

一、基本层级的实现(Layers.py)

首先,附上代码所需的相关包:

import torch 
import numpy as np 
import torch.nn as nn 
import torch.nn.functional as F 
import math 
import copy from torch.autograd 
import Variable

然后,让我们来看看论文原图给出的架构:

Attention Is All You Need:附图1,模型架构。

接下来,就是一步步拆解这个结构流程啦。

1、Embedding层的实现

首先是这个部分:即输入的Input Embedding层。这是一个比较常见的结构。

红框部分

############################ 嵌入层;################################# 
class embeddings(nn.Module): def __init__(self,d_model:int,vocab:int): 
    # embedding层一共有vocab个词语,每个词语的维度为d_model个,一共是vocab * d_model个数字组成的权重矩阵; 
    super(embeddings,self).__init__() # 获得父类的成员函数; 
    self.embedding = nn.Embedding(vocab,d_model) 
    self.d_model = d_model def forward(self,x): 
    # 之所以需要乘上维度的平方,是防止数据太小,经过后面的PE叠加后无影响了; 
    return self.embedding(x) * math.sqrt(self.d_model)

在这里,一个词语经过embedding后,其维度会变成我们规定的 𝑑𝑚𝑜𝑑𝑒𝑙 维。一个长为 𝑥 的句子(我们可以把它的形状看作为 (𝑥,1) ),在经过embedding后,会变成一个 (𝑥,𝑑𝑚𝑜𝑑𝑒𝑙) 形状的矩阵。

知识点(01):为什么要使用embedding?
回答:使用embedding可以将稀疏表达变为稠密表达,降低表征一个词所需要的向量空间维度。(大白话:其实就是one-hot在面对大词表的时候太过于稀疏了,而且维度太大了,因此使用embedding来获得一种较低维度的表示)

知识点(02):这里为什么要乘上 𝑑_𝑚𝑜𝑑𝑒𝑙 ?
回答:由Embedding初始化计算公式得:其输出的方差将随着 𝑑_𝑚𝑜𝑑𝑒𝑙 的增大而减小。这样会使得输出的波动范围较小。因此需要对其进行一定的缩放,避免其无法受到影响,也更方便训练。

Embedding 是经过了 One-Hot 的全连接层。这一模块更详细的解释:什么是词向量?如何得到词向量?Embedding 快速解读 | 鲁老师 (lulaoshi.info)。

2、Position Encoding层的实现

即这个部分:

位置编码部分。

其代码实现如下:

############################ PE位置编码;############################
class Positional_Encoding(nn.Module):
    def __init__(self,d_model:int,dropout:float,len_position=500):
        super(Positional_Encoding,self).__init__()
        
        # 在训练阶段按概率p随即将输入的张量元素随机归零,常用的正则化器,用于防止网络过拟合;
        self.dropout = nn.Dropout(p=dropout)
        self.PE = torch.zeros(len_position,d_model)
        # 生成的PE维度为(len_position,d_model),有d_model个不同相位的正弦函数用来编码;
        # 每一个位置的对应编码为512维度的向量;

        # .arange()返回大小为(len_position)维度的向量,值为增长的step;.unsqueeze(1)在位置1处增加了一个维度;
        position = torch.arange(0.,len_position).unsqueeze(1) #维度为(5000 * 1);
        div_term = torch.exp(torch.arange(0., d_model, 2) * (-(math.log(10000.0) / d_model)))#维度为(1 * 5000);
         # position * div_term维度为(5000 * 1)*(1 * 512)=(5000 * 512);
        self.PE[:,0::2] = torch.sin(position * div_term) # 偶数位置使用sin编码;
        self.PE[:,1::2] = torch.cos(position * div_term) # 基数位置使用cos编码;

        #在最初始的维度插上batch;利用广播机制平衡维度;
        self.PE = self.PE.unsqueeze(0)
        # self.register_buffer("PE",self.PE)

    def forward(self,x):
        x = x + Variable(self.PE[:,:x.size(1)])#转化为Variable,就不需要求导了;此时添加上了位置编码;
        return self.dropout(x)

论文原文里的位置编码的公式长这样:

𝑃𝐸(𝑝𝑜𝑠,2𝑖)=𝑠𝑖𝑛(𝑝𝑜𝑠/10000^(2𝑖/𝑑𝑚𝑜𝑑𝑒𝑙)),𝑝𝑜𝑠=1,2,3...

𝑃𝐸(𝑝𝑜𝑠,2𝑖+1)=𝑐𝑜𝑠(𝑝𝑜𝑠/10000^(2𝑖/𝑑𝑚𝑜𝑑𝑒𝑙)),𝑝𝑜𝑠=1,2,3...

知识点(03):为什么要使用位置编码?
回答:Transformer模型与传统RNN不同,它不会按照序列的顺序处理数据,而是一次性处理整个序列。这种处理方式虽然提高了效率,但也意味着模型无法直接从输入中获取位置信息。位置编码是为了补充这一缺失的位置信息。它通过给每个单词添加一个独特的编码,这样模型就能够区分单词在序列中的位置。

知识点(04):位置编码为什么使用正弦余弦?
回答:这种方法可以确保每个位置编码唯一,并且能够很容易地扩展到比训练时序列更长的情况。

知识点(05):现在位置编码有什么改进吗?
回答:旋转位置编码(Rotary Position Embedding)(现在比较流行的大模型都用的这种)。

3、Multi-Head Attention层

多头注意力机制层,也是最重要的一层。

多头注意力机制层。

############################ 多头注意力即多层注意力机制。#####################################
#######原先输入的词维度为512的将通过三个投影矩阵投影到更小的维度;############################
class MultiHeadedAttention(nn.Module): 
    def __init__(self, head_num:int, d_model, dropout=0.1):
        super(MultiHeadedAttention,self).__init__()
        self.d_k = d_model // head_num #d_k为输出模型大小的维度;
        self.head_num = head_num #多头的数目;
        self.dropout = dropout
        self.Linears = clones(nn.Linear(d_model,d_model),4) # 定义四个投影矩阵;
        self.Attention = None
        self.Dropout = nn.Dropout(p=dropout)
    
    def forward(self,query,key,value,mask=None):
        nbatches = query.size(0)

        seq_q_len = query.size(1)
        seq_k_len = key.size(1)
        seq_v_len = value.size(1)

        if mask is not None:
            mask = mask.unsqueeze(1) # 给mas添加一个维度,并设置其值为1;

        # 分别进行线性变换;
        query = self.Linears[0](query)
        key = self.Linears[1](key)
        value = self.Linears[2](value)

        # 重塑512维度为head_num*d_k;
        query = query.view(nbatches,seq_q_len,self.head_num,self.d_k)
        key = key.view(nbatches,seq_k_len,self.head_num,self.d_k)
        value = value.view(nbatches,seq_v_len,self.head_num,self.d_k)

        # 将与头有关的维度放在前面,方便后续注意力层进行操作;
        query = query.transpose(1,2)
        key = key.transpose(1,2)
        value = value.transpose(1,2)

        # 经过注意力层,返回softmax(qk/)sqrt(d)*v;
        x,self.attn = Attention(query,key,value,mask=mask,dropout=self.Dropout)

        x = x.transpose(1,2).contiguous() # 将多头相关的维度交换回来;
        x = x.view(nbatches,-1,self.head_num * self.d_k) # 将维度重塑为512维;
        return self.Linears[-1](x) # concat掉。

这里的clones() 是一个复制多个层的函数。长这样:

def clones(module, N):# 用来拷贝多个层;
    return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])

我们可以通过forward看到,在这一层,我们经过Embedding和PE处理后的数据会被进行这样的操作:

复制三份,并改名为query、key和value。(自注意力机制:这三个东西都是一样的东西!)

每一份分别进入一个线性层self.Linears[i]() ,即所谓的“投影矩阵”;

每一份将进行reshape(即view()),将512维的数据转换为 ℎ𝑒𝑎𝑑×𝑑𝑘 的维度数据(如果 ℎ𝑒𝑎𝑑=8 ,则 𝑑𝑘=512÷ℎ𝑒𝑎𝑑=64 。假如输入为(10,5,512) ,输出即为 (10,5,8,64) 。)

每一份数据交换维度:将 ℎ𝑒𝑎𝑑 相关的维度放在前面,词数相关维度放后面。例:把(10,5,8,64) 变成 (10,8,5,64) 。这样是为了方便Attention层的处理。

将这三份数据送进Attention层计算注意力矩阵。

将获得的最终数据(以上面为例,最终数据维度为(10,8,5,64) )的 ℎ𝑒𝑎𝑑 维度还原回来( 重新变成(10,5,8,64) ),并重塑为512维度的数据((10,5,512) )。

再经过一个线性层。

Attention层的代码实现如下:

############################ 注意力层; ############################
def Attention(query,key,value,mask=None,dropout=None):
    d_k = query.size(-1) # 获取最后一个维度;
    
    # 对其进行相乘;多头的维度保持不变,使用softmax(qk/sqrt(d))*v公式进行计算;
    scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
    if mask is not None: # 如果有mask就使用,当需要制作解码器的时候使用;
        scores = scores.masked_fill_(mask == 0, -1e9) 
    p_attn = F.softmax(scores, dim=-1)  # 获取注意力分数图;

    if dropout is not None: 
        p_attn = dropout(p_attn)
    
    # 返回计算的数值和注意力分数矩阵;
    return torch.matmul(p_attn, value), p_attn 

实际上,就是将三个矩阵的其中两个相乘,并且取了一个softmax用来做概率的归一,获得的这个叫attention score。相乘的作用是为了获得每个词语对于每个词语之间的注意力权重(即影响了多少)。

知识点(06):Transformer模型中的多头注意力机制是什么?
回答:多头注意力机制是一种将自注意力机制扩展到多个子空间的方法。在Transformer模型中,多头注意力机制用于将输入序列分别映射到多个子空间(实际上就是reshape了一下),然后在每个子空间中计算自注意力(实际上就是拿reshape后的数据去用公式计算了一下),最后将多个子空间的输出拼接起来(又reshape回来了)得到最终的输出。

知识点(07):Transformer为什么要对Q/K/V使用不同的投影矩阵?
回答:K和Q的点乘是为了得到一个attention score矩阵,用来对V进行提纯。K和Q使用了不同的投影矩阵来计算,可以理解为是在不同空间上的投影。正因为有了这种不同空间的投影,增加了表达能力,这样计算得到的attention score矩阵的泛化能力更高(记忆一个泛化能力更好就行了,因为它可学习的参数更多了)。

知识点(08):为什么在进行softmax之前需要对attention除以 𝑑𝑘 的平方根?
回答:如果 𝑑𝑘 的维数太大,则会使得softmax内的取值的数字更偏向于0或1,这样在训练的过程中可能会出现梯度消失的现象,因此需要除以 𝑑𝑘 的平方根。另外为什么是平方根:假设向量Q,K满足各分量独立同分布,均值为0,方差为1,那么QK点积均值为0,方差就是 𝑑𝑘 。从统计学计算,若果让QK点积的方差控制在1,需要将其除以 𝑑𝑘 的平方根,这样可以使得经过softmax后的数据变得更加平滑。

4、LayerNorm层

Layer Norm其实就是对每一个数据本身去做标准化处理,即也就是针对单个样本的不同特征做标准化。他在结构图中长这样:

LayerNorm层。

它的代码如下:

############################ LayerNorm层; ############################
class LayerNorm(nn.Module):
    def __init__(self,features,eps=1e-6):
        super(LayerNorm,self).__init__()
        # features=(int)512,用来给nn.Parameter生成可训练参数的维度;
        self.a_2 = nn.Parameter(torch.ones(features))
        self.b_2 = nn.Parameter(torch.zeros(features))
        self.eps = eps

    def forward(self,x):
        # 此时的x维度为(nBatches,句子长度,512);
        # 对512维度的部分求均值,keepdim保持输出维度相同;
        mean = x.mean(-1,keepdim=True)
        
        # 对512维度的部分求标准差,keepdim保持输出维度相同;eps保持标准差不为零;
        std = x.std(-1,keepdim=True) + self.eps 
        return self.a_2 * (x-mean)/std + self.b_2

知识点(09):为什么使用LayerNorm?
回答:与批归一化(Batch Normalization, BN)不同,LayerNorm是在单个样本的所有特征上进行归一化,而不是在一个批次的同一特征上。这使得LayerNorm不受批次大小的限制,特别适用于批次大小不一或者需要处理变长序列的场景。

知识点(10):使用LayerNorm的优势?
回答:LayerNorm有助于稳定神经网络的训练,加快收敛速度,并提高模型的泛化能力。

5、Feed Forward层

其实就是普通的线性层。他在结构图中的这个位置:

又叫前馈层。

它的代码实现十分清晰且清楚:

############################ FFN层; ############################
class PositionWiseFeedForward(nn.Module):
    def __init__(self, d_model, d_Hidden, dropout=0.1):
        super(PositionWiseFeedForward,self).__init__()
        self.linear_1 = nn.Linear(d_model,d_Hidden)
        self.linear_2 = nn.Linear(d_Hidden,d_model)
        self.dropout = nn.Dropout(p=dropout)

    def forward(self,x):
        x = self.linear_1(x)
        x = F.relu(x)
        x = self.dropout(x)
        x = self.linear_2(x)
        return x

知识点(11):前馈层的作用是?
回答:具体来说,Feed Forward层的作用是对Multi-Head Attention层的输出进行进一步的特征提取和变换。它通过线性变换将数据映射到高维空间,然后再映射回低维空间,这个过程中引入的非线性激活函数有助于增强模型的表达能力和学习复杂特征的能力(其实,一般的MLP的作用也就是进行特征提取)。

6、残差连接

就是普通的残差连接。在结构图里的这个位置:

残差连接。

他的代码如下:

############################ SubLayerConnection层; ############################
class SubLayerConnection(nn.Module):
    def __init__(self,d_model,dropout=0.1):
        super(SubLayerConnection,self).__init__()
        self.LayerNorm = LayerNorm(d_model)
        self.dropout = nn.Dropout(dropout)

    def forward(self,x,sublayer):
        # 采用残差结构;
        return x + self.dropout(sublayer(self.LayerNorm(x)))

注意:我们为了使得代码简洁,特意将LayerNorm放在了残差连接层的这个地方。但是我们注意到这里是先经过LayerNorm,然后再经过注意力层/FFN的。这也是harvard这个组经过实验后的一点小小改动。

知识点(12):残差链接的作用是?
回答:残差连接的作用是帮助信息在网络层之间直接传递。残差连接通过将输入直接加到子层的输出上,这样做可以减轻梯度消失的问题,保留输入信息,减轻训练负担,并提高模型性能。

接下来,就要开始组装整个encoder块和decoder块了。

二、Encoder和Decoder的组装(Encoder_Decoder.py)

总所周知,最原始的Transformer论文中提及的,一共有两个组成部分,一个是Encoder,另外一个是Decoder。

前者负责将我们的输入的序列转化为隐藏表示,而后者则根据此反过来推理出我们需要的语句。

为了方便理解,我将Encoder和Decoder的代码其放到了另外一个py文件中。

这个文件所需包如下:

import Layers as L #导入上一部分的文件。
import torch
import numpy as np
import torch.nn as nn
import torch.nn.functional as F
import copy

下面将详细介绍一下代码的实现。

1、Encoder的组装

让我们看看图:这其实就是一个搭积木的问题而已!我们已经有了所有的子层结构,接下来就是积木怎么搭建了。

单个Encoder块。

现在,我们的数据经过了Embedding和PE,接下来要输入进去单个的Encoder块了:

######################## 构建单个Encoder块; ###########################
class EncoderLayer(nn.Module):
    def __init__(self, d_model, self_attn, feed_forward, dropout=0.1):
        # d_model是输入维度数据;
        super(EncoderLayer,self).__init__()
        self.self_attn = self_attn #自注意力层;
        self.feed_forward = feed_forward # FFN层;
        self.sublayer = L.clones(L.SubLayerConnection(d_model,dropout),2) # 两个残差链接结构;
        self.d_model = d_model
    
    def forward(self, x, mask=None):
        # 第一个残差结构,通过自注意力机制输入;其中经过了多头注意力层 + LN层,并通过残差结构连接;
        x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask)) 
        # 第二个残差结构,通过FFN;其中经过了FFN层,并通过残差结构连接;
        x = self.sublayer[1](x, self.feed_forward)
        return x

有了单个的Encoder块,只要我们有N个Encoder块组装在一起,就能够构成一整个Encoder了:

######################## 整个Encoder; ###########################
class Encoder(nn.Module):
    def __init__(self, EncodeLayer, num_of_EncodeLayer:int):
        # d_model是输入维度数据;
        super(Encoder, self).__init__()
        self.EncodeLayers = L.clones(EncodeLayer,num_of_EncodeLayer)
        self.LayerNorm = L.LayerNorm(EncodeLayer.d_model) # 为d_model维;
    
    def forward(self, x, mask=None):
        for encodelayer in self.EncodeLayers:
            x = encodelayer(x, mask)
            x = self.LayerNorm(x)
        return x

简单而清晰!接下来就是Decoder的组装。

2、Decoder的组装

让我们看看图:Decoder的结构相比于其他的结构而言有一些小小的改动:即第二个多头注意力的query和value是Encoder的输出。

单个Decoder块。

还是一样,让我们先来组装一个单独的Decoder块:

######################## 构建单个Decoder块; ###########################
class DecoderLayer(nn.Module): # 单独一个Decoder层;
    def __init__(self, d_model, self_attn, src_attn, 
      feed_forward, dropout):
      # size = d_model=512;
        super(DecoderLayer, self).__init__()
        self.d_model = d_model
        self.self_attn = self_attn
        self.src_attn = src_attn
        self.feed_forward = feed_forward
        self.sublayer = L.clones(L.SubLayerConnection(d_model, dropout), 3)

    def forward(self, x, memory, src_mask, tgt_mask):
        m = memory
        # 来自Encoder的query与value:用作解码序列的query与value。
        x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, tgt_mask))
        x = self.sublayer[1](x, lambda x: self.self_attn(x, m, m, src_mask))
        return self.sublayer[2](x, self.feed_forward)

在这里,我们在前向传播的过程中增加了一个变量memory。这个变量是从Encoder的输出得来的,因此我们决定将他作为一个外来的变量添加进来。

有了单独的一个Decoder块,接下来就可以组装成一整个Decoder了:

######################## 构建整个Decoder; ###########################
class Decoder(nn.Module):
    def __init__(self, layer, N):
        super(Decoder, self).__init__()
        self.decodelayers = L.clones(layer, N)
        self.layerNorm = L.LayerNorm(layer.d_model)

    def forward(self, x, memory, src_mask, tgt_mask):
        for layer in self.decodelayers:
            x = layer(x, memory, src_mask, tgt_mask)
            x = self.layerNorm(x)
        return x 

我们需要注意一点:在Decoder中,就必须使用掩码了(Mask)。因此,我们需要在这里将Mask的表示一并给出。下面是实现mask的代码:

######################## 构建mask的上三角阵; ###########################
def subsequent_mask(size):
    "Mask out subsequent positions."
    attn_shape = (1, size, size) # (1, 10, 10)
    subsequent_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8')
    # triu生成一个三角矩阵,k对应的维度的对角线往下的元素均为0,上三角的元素均为1; 
    return torch.from_numpy(subsequent_mask) == 0 #反转矩阵:让元素反着来;

mask实际上就是一个对角线及以下全部为1的,上半三角全部为0的矩阵。其中值为0或False的位置表示被遮蔽,而值为1或True的位置表示不被遮蔽。这个稍微结合一下Attention Map实现的原理就明白了。后续将会在Batch的构造那一小节中给出更详细的解释。

让我们看看这个函数的输出:

print(subsequent_mask(3))

他的结果如下:

>>>tensor([[[ True, False, False],  
            [ True,  True, False],  
            [ True,  True,  True]]])

知识点(13):mask的作用是?
回答:在解码器中,为了防止预测当前单词时看到未来的单词,会使用Attention Mask来屏蔽后续位置。这样,在计算注意力分布时,模型只会关注当前和之前的位置。

到这里为止,整个Encoder和Decoder的组装就完成了!但是,我们注意到,并不是所有层我们都构建完成了。Decoder输出后,还需要经过最后一个生成头来预测概率。因此我们需要继续添加一个小层。

3、Generator输出头

这个部分就是一个简单的线性层。他将会输出词表大小的数据,用来预测下一个词的概率。这个部分在整个结构中如下所示:

一个预测最终词概率的输出头。

它的代码如下:

######################## 构建整个输出头; ###########################
class Generator(nn.Module):
    def __init__(self, d_model, vocab):
        super(Generator, self).__init__()
        self.output = nn.Linear(d_model, vocab) # 输入是d_model的维度,输出是词表大小;

    def forward(self, x):
        return F.log_softmax(self.output(x), dim=-1) # 使用softmax修正概率;

注意:输出头输出的是下一个词的概率!他仍然是需要参考词表找到概率最大或相对较大的那个词的。也正是这个原因,很多大佬都称呼其为一个“概率预测模型”。

不过,这里为什么使用的是log_softmax而不是softmax?因为在计算softmax时,由于涉及指数运算,当输入值很大时可能会导致数值溢出。log_softmax通过先取对数再进行softmax运算,有效避免了这个问题。

如此一来,整个模型的细分结构我们都很清晰的给他表示了!接下来就是整个模型的组装。

三、整个模型的组装

组建整个模型了!让我们先用一整个类来将整个模型笼罩在里面:

######################## 构建整个大模型的编码-解码结构; ###########################
class EncoderDecoder(nn.Module):
    def __init__(self, encoder, decoder, src_embed, tgt_embed, generator):
        super(EncoderDecoder, self).__init__()
        self.encoder = encoder
        self.decoder = decoder
        self.src_embed = src_embed # 最开始经过词嵌入和PE模块的编码序列;
        self.tgt_embed = tgt_embed # 最后结果的经过词嵌入和PE模块的编码序列;
        self.generator = generator # 输出头;

    def forward(self, src, tgt, src_mask, tgt_mask):
        src = self.src_embed(src) # 序列经过词嵌入层和PE编码;
        src = self.encoder(src, src_mask) # 序列经过编码器;
        embed_tgt = self.tgt_embed(tgt) # 经过词嵌入层的tgt;
         # src_mask是第一层多头注意力机制的mask,tgt_mask是第二层多头注意力机制的mask;
        Decoder_result = self.decoder(embed_tgt, src, src_mask, tgt_mask)
        output = self.generator(Decoder_result)
        return output

为了方便理解,我将上面变量和在模型中的实际应用摆放在一起:

然后,我们使用一个函数来构建模型:

######################## 构建模型; ###########################
def make_model(src_vocab, tgt_vocab, N=2, d_model=512, d_ff=2048, h=8, dropout=0.1):
    # src_vocab = Input需要构建的语言词表大小;
    # tgt_vocab = Output输入需要构建的语言词表大小;
    # d_model = 经过embedding后的扩充维度大小;
    # d_ff = 在ffn时候的隐藏层大小;
    # h = 头大小;
    
    c = copy.deepcopy # 深度,这里使用deepcopy防止因指针指向同一片地方而导致发生的干扰。
    attn = L.MultiHeadedAttention(h, d_model) #多头注意力层;
    ff = L.PositionWiseFeedForward(d_model, d_ff, dropout) # FFN层;
    position = L.Positional_Encoding(d_model, dropout) # 位置编码层;

    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(L.embeddings(d_model, src_vocab), c(position)), # nn.Sequential实现模型的顺序连接;
        nn.Sequential(L.embeddings(d_model, tgt_vocab), c(position)),
        Generator(d_model, tgt_vocab)) #调用__init__()进行初始化;

    # 对所有层参数使用Xavier初始化;
    for p in model.parameters():
        if p.dim() > 1:
            nn.init.xavier_uniform_(p)
    return model

如此这般,我们便从一开始的单个简单层到整个模型的搭建,全部都大功告成!接下来,就是Loss的设计和训练了!

受字数限制,分上下两篇。下篇链接在此。https://blog.csdn.net/alxws/article/details/140004508?spm=1001.2014.3001.5502

更新时间 2024-07-02