【Transformer系列(5)】Transformer代码超详细解读(Pytorch)

news2024/11/22 1:12:18

前言 

前面几篇我们一起读了transformer的论文,更进一步了解了它的模型架构,这一篇呢,我们就来看看它是如何代码实现的!

(建议大家在读这一篇之前,先去看看上一篇模型结构讲解  这样可以理解更深刻噢!)

transformer代码有很多版本,本文是参考B站这位大佬改进后的代码进行解读,因为我也是刚开始学习,能力有限,如有不详实之处,大家可移步至文末的传送门去看大佬解读的更多细节嗷~


962f7cb1b48f44e29d9beb1d499d0530.gif​   🍀前期回顾

 【Transformer系列(1)】encoder(编码器)和decoder(解码器)

【Transformer系列(2)】注意力机制、自注意力机制、多头注意力机制、通道注意力机制、空间注意力机制超详细讲解
 

【Transformer系列(3)】《Attention Is All You Need》论文超详细解读(翻译+精读)


【Transformer系列(4)】Transformer模型结构超详细解读 


目录

前言 

🚀0.导入依赖库

🚀1. 数据预处理 

1.1 数据准备

1.1.1 训练集:句子输入部分

1.1.2 测试集:构建词表

1.2 数据构建 

1.2.1 实现一个minibatch迭代器

1.2.2 自定义一个MyDataSet去读取这些句子

🚀2.模型整体架构

2.1 超参数设置

2.2 整体架构

2.3 模型训练

🚀3. 编码器(Encoder)

3.1 Encoder Layer:单个编码器层

3.2 Encoder:编码器

3.3 Padding Mask:形成一个符号矩阵

🚀4. 解码器(Decoder)

4.1Decoder Layer:单个解码层 

4.2Decoder:解码器

4.3 Sequence Mask:屏蔽子序列的mask

🚀5. 位置编码(Position Embedding)

🚀6. 注意力机制(Attention)

6.1Scaled DotProduct Attention:缩放点积注意力机制

6.2MultiHead Attention:多头注意力机制

🚀7. 前馈神经网络(PoswiseFeedForward)

          7.1 实现方式1:Conv1d

 7.2 实现方式2:Linear

🚀0.导入依赖库

#======================0.导入依赖库=============================#
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt
import math
  • numpy:  科学计算库,提供了矩阵,线性代数,傅立叶变换等等的解决方案, 最常用的是它的N维数组对象
  • torch:   这是主要的Pytorch库。它提供了构建、训练和评估神经网络的工具
  • torch.nn:  torch下包含用于搭建神经网络的modules和可用于继承的类的一个子包
  • torch.optim:  优化器 Optimizer。主要是在模型训练阶段对模型可学习参数进行更新,常用优化器有 SGD,RMSprop,Adam等
  • matplotlib.pyplot:  matplotlib.pyplot 是一个命令型函数集合,它可以像使用 Matlab 一样使用 matplotlib,pyplot 中的每一个函数都会对画布图像作出相应的改变
  • math:  调用这个库进行数学运算

🚀1. 数据预处理 

本文以一个简单的德语到英语机器翻译任务Demo为例。

1.1 数据准备

1.1.1 训练集:句子输入部分

 # ===1.训练集(句子输入部分)===#
    sentences = ['ich mochte ein bier P', 'S i want a beer', 'i want a beer E']
  • 第一个德语句子 'ich mochte ein bier P'  --> 编码端的输入
  • 第二个英语句子 'S i want a beer' --> 解码端的输入
  • 第三个英语句子 'i want a beer E' --> 解码端的真实标签(答案)

可以通过这个图来理解一下:

P、S、E是什么?

  • P:pad字符。如果当前批次的数据量小于时间步数,将填写空白序列的符号。
  • S:Start。显示 解码 输入开始 的符号 
  • E:End。显示 解码 输出开始 的符号

1.1.2 测试集:构建词表

#===2.测试集(构建词表)===#
    
    # 编码端的词表
    src_vocab = {'P': 0, 'ich': 1, 'mochte': 2, 'ein': 3, 'bier': 4}
    src_vocab_size = len(src_vocab)  # src_vocab_size:实际情况下,它的长度应该是所有德语单词的个数

    # 解码端的词表
    tgt_vocab = {'P': 0, 'i': 1, 'want': 2, 'a': 3, 'beer': 4, 'S': 5, 'E': 6}
    tgt_vocab_size = len(tgt_vocab)  # 实际情况下,它应该是所有英语单词个数

词嵌入本身是一个look up查表的过程,因此需要构建词表:token及其索引。

现在的实际任务中,一般使用Huggingface Transformers库的Tokenizer等API直接获取。

其实编码端和解码端可以共用一个词表的。


1.2 数据构建 

1.2.1 实现一个minibatch迭代器

def make_batch(sentences):
    input_batch = [[src_vocab[n] for n in sentences[0].split()]] # 输入数据集
    output_batch = [[tgt_vocab[n] for n in sentences[1].split()]] # 输出数据集
    target_batch = [[tgt_vocab[n] for n in sentences[2].split()]] # 目标数据集
    return torch.LongTensor(input_batch), torch.LongTensor(output_batch), torch.LongTensor(target_batch)

这段代码主要是把字符串类型的文本转成词表索引,然后再把索引转成tensor类型。


1.2.2 自定义一个MyDataSet去读取这些句子

 ​ class MyDataSet(Data.Dataset):
     """自定义DataLoader"""
 ​
     def __init__(self, enc_inputs, dec_inputs, dec_outputs):
         super(MyDataSet, self).__init__()
         self.enc_inputs = enc_inputs
         self.dec_inputs = dec_inputs
         self.dec_outputs = dec_outputs
 ​
     def __len__(self):
         return self.enc_inputs.shape[0]
 ​
     def __getitem__(self, idx):
         return self.enc_inputs[idx], self.dec_inputs[idx], self.dec_outputs[idx]
 ​
 ​
 loader = Data.DataLoader(
     MyDataSet(enc_inputs, dec_inputs, dec_outputs), 2, True)

 我们需要在自定义的数据集类中继承Dataset类,同时还需要实现两个方法:

  • __len__方法:  能够实现通过全局的len()  方法获取其中的元素个数
  • __getitem__方法:  能够通过传入索引的方式获取数据,例如通过dataset[i]  获取其中的第i条数据

最后DataLoader进行封装:dataset,batch_size,shuffle(是否打乱)


🚀2.模型整体架构

2.1 超参数设置

    src_len = 5 # length of source 编码端的输入长度
    tgt_len = 5 # length of target 解码端的输入长度

    #===Transformer参数===#
    d_model = 512  # Embedding Size 每一个字符转化成Embedding的大小
    d_ff = 2048  # FeedForward dimension 前馈神经网络映射到多少维度
    d_k = d_v = 64  # dimension of K(=Q), V
    n_layers = 6  # number of Encoder of Decoder Layer  encoder和decoder的个数,这个设置的是6个encoder和decoder堆叠在一起(encoder和decoder的个数必须保持一样吗)
    n_heads = 8  # number of heads in Multi-Head Attention  多头注意力机制时,把头分为几个,这里说的是分为8个

重要参数:

  • src_len :  编码端的输入长度
  • tgt_len :   解码端的输入长度
  • d_model:   需要定义embeding 的维度,论文中设置的512
  • d_ff: FeedForward 层隐藏神经元个数,论文中设置的2048
  • d_k = d_v: Q、K、V 向量的维度,其中 Q 与 K 的维度必须相等,V 的维度没有限制,都设为 64
  • n_layers:  Encoder 和 Decoder 的个数,也就是图中的Nx
  • n_heads: 多头注意力中 head 的数量

2.2 整体架构

class Transformer(nn.Module):
    def __init__(self):
        super(Transformer, self).__init__()
        self.encoder=Encoder().to(device) # 编码层
        self.decoder=Decoder().to(device) # 解码层
       # 输出层,d_model是解码层每一个token输出维度的大小,之后会做一个tgt_vocab_size大小的softmax
        self.projection=nn.Linear(d_model,tgt_vocab_size,bias=False).to(device)
    
    # 实现函数
    def forward(self, enc_inputs,dec_inputs):
        """
         Transformers的输入:两个序列(编码端的输入,解码端的输入)
         enc_inputs: [batch_size, src_len]   形状:batch_size乘src_len
         dec_inputs: [batch_size, tgt_len]   形状:batch_size乘tgt_len
         """
        # tensor to store decoder outputs
        # outputs = torch.zeros(batch_size, tgt_len, tgt_vocab_size).to(self.device)

        # enc_outputs: [batch_size, src_len, d_model], enc_self_attns: [n_layers, batch_size, n_heads, src_len, src_len]
        # 经过Encoder网络后,得到的输出还是[batch_size, src_len, d_model]
        enc_outputs,enc_self_attns=self.encoder(enc_inputs)

        # dec_outputs: [batch_size, tgt_len, d_model], dec_self_attns: [n_layers, batch_size, n_heads, tgt_len, tgt_len], dec_enc_attn: [n_layers, batch_size, tgt_len, src_len]
        dec_outputs,dec_self_attns,dec_enc_attns=self.decoder(dec_inputs,enc_inputs,enc_outputs)

        # dec_outputs: [batch_size, tgt_len, d_model] -> dec_logits: [batch_size, tgt_len, tgt_vocab_size]
        dec_logits=self.projection(dec_outputs)
        return dec_logits.view(-1,dec_logits.size(-1)),enc_self_attns,dec_self_attns,dec_enc_attns

Transformer主要就是调用Encoder和Decoder。最后返回dec_logits的维度是[batch_size * tgt_len, tgt_vocab_size]

主要流程:

  • 输入文本进行词嵌入和位置编码,作为最终的文本嵌入;
  • 文本嵌入经过Encoder编码,得到注意力加权后输出的编码向量以及自注意力权重矩阵;
  • 然后将编码向量和样本的Ground trurh共同输入解码器,经过注意力加权等操作后输出最终的上下文向量,然后映射到词表大小的线性层上进行解码生成文本;
  • 最终返回代表预测结果的logits矩阵。

 主要参数:  

  • d_model:解码层每个token输出的维度大小,之后会做一个 tgt_vocab_size 大小的softmax

 d_model:预测一个德语单词被翻译成英语,它会对应为那个单词,这里输入就是一个单词在词表中的维度,这里的维度是512,所以在词表中一个单词的维度是512。

如果一句话有n个单词,那么在翻译的整个过程中就会调用n次这个全连接函数。

举个栗子:英语单词有100000个,那么这儿的tgt_vocab_size就是1000000个。到达这儿,就好像是一个分类任务,看这个单词属于这100000个类中的哪一个类,最后全连接分类的结果然后再进行一个softmax就会得到这100000个单词每个单词的概率。哪个单词的概率最大,那么我们就把这个德语单词翻译成那个单词。也就是我们这儿的projection就是那个德语单词被翻译成英语单词的词。

  • enc_inputs:编码端输入。形状为[batch_size, src_len],输出由自己的函数内部指定,想要什么指定输出什么,可以是全部tokens的输出,可以是特定每一层的输出;也可以是中间某些参数的输出
  • enc_outputs:编码端输出。编码端的输入(enc_inputs)通过编码端(encoder)流到编码端的输出(enc_outputs)
  • enc_self_attns:Q、K转置相乘后softmax的矩阵值,代表每个单词和其他单词的相关性
  • dec_outputs: 解码端输出,用于后续的linear映射
  • dec_self_attns: 类比于enc_self_attns是查看每个单词对解码端中输入的其余单词的相关性
  • dec_enc_attns:解码端中每个单词对encoder中每个单词的相关性
  • dec_logits.view: 进行view操作主要是为了适应后面的CrossEntropyLoss(交叉熵损失函数) API的参数要求

2.3 模型训练

model=Transformer().to(device) # 调用Transformer模型
criterion=nn.CrossEntropyLoss(ignore_index=0) # 交叉熵损失函数
optimizer=optim.SGD(model.parameters(),lr=1e-3,momentum=0.99)# 用Adam的话效果不好

for epoch in range(epochs):
    for enc_inputs,dec_inputs,dec_outputs in loader:
        """
            enc_inputs: [batch_size, src_len]
            dec_inputs: [batch_size, tgt_len]
            dec_outputs: [batch_size, tgt_len]
        """
        enc_inputs,dec_inputs,dec_outputs=enc_inputs.to(device),dec_inputs.to(device),dec_outputs.to(device)

        # outputs: [batch_size * tgt_len, tgt_vocab_size]
        outputs,enc_self_attns,dec_self_attns,dec_enc_attns=model(enc_inputs,dec_inputs)

        # dec_outputs.view(-1):[batch_size * tgt_len * tgt_vocab_size]
        loss=criterion(outputs,dec_outputs.view(-1))
        print('Epoch:', '%04d' % (epoch + 1), 'loss =', '{:.6f}'.format(loss))

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

这里的损失函数里面设置了一个参数ignore_index=0,因为"pad"这个单词的索引为0,这样设置以后,就不会计算"pad"的损失(因为本来"pad"也没有意义,不需要计算)


🚀3. 编码器(Encoder)

编码器(Encoder)由三个部分组成:输入多头注意力前馈神经网络

流程

  • 输入文本的索引tensor,经过词嵌入层得到词嵌入,然后和位置编码线性相加作为输入层的最终输出;
  • 随后,每一层的输出最为下一层编码块的输入,在每个编码块里进行注意力计算、前馈神经网络、残差连接、层归一化等操作;
  • 最终返回编码器最后一层的输出和每一层的注意力权重矩阵。

3.1 Encoder Layer:单个编码器层

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

结构如图所示:

# ---------------------------------------------------#
# EncoderLayer:包含两个部分,多头注意力机制和前馈神经网络
# ---------------------------------------------------#
class EncoderLayer(nn.Module):
    def __init__(self):
        super(EncoderLayer, self).__init__()
        self.enc_self_attn = MultiHeadAttention()
        self.pos_ffn = PoswiseFeedForwardNet()

    def forward(self, enc_inputs, enc_self_attn_mask):
        """
        下面这个就是做自注意力层,输入是enc_inputs,形状是[batch_size x seq_len_q x d_model],需要注意的是最初始的QKV矩阵是等同于这个
        输入的,去看一下enc_self_attn函数.
        """
        # enc_inputs to same Q,K,V
        enc_outputs, attn = self.enc_self_attn(enc_inputs, enc_inputs, enc_inputs, enc_self_attn_mask)
        # enc_outputs: [batch_size x len_q x d_model]
        enc_outputs = self.pos_ffn(enc_outputs)
        return enc_outputs, attn

Encoder Layer包含两个部分:多头注意力机制前馈神经网络 


3.2 Encoder:编码器

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

结构如图所示:

# -----------------------------------------------------------------------------#
# Encoder部分包含三个部分:词向量embedding,位置编码部分,自注意力层及后续的前馈神经网络
# -----------------------------------------------------------------------------#
class Encoder(nn.Module):
    def __init__(self):
        super(Encoder, self).__init__()
        # 这行其实就是生成一个矩阵,大小是: src_vocab_size * d_model
        self.src_emb = nn.Embedding(src_vocab_size, d_model)
        # 位置编码,这里是固定的正余弦函数,也可以使用类似词向量的nn.Embedding获得一个可以更新学习的位置编码
        self.pos_emb = PositionalEncoding(d_model)
        # 使用ModuleList对多个encoder进行堆叠,因为后续的encoder并没有使用词向量和位置编码,所以抽离出来;
        self.layers = nn.ModuleList([EncoderLayer() for _ in range(n_layers)])

    def forward(self, enc_inputs):
        """
        这里我们的enc_inputs形状是: [batch_size x source_len]
        """
        # 下面这行代码通过src_emb进行索引定位,enc_outputs输出形状是[batch_size, src_len, d_model]
        enc_outputs = self.src_emb(enc_inputs)

        # 这行是位置编码,把两者相加放到了pos_emb函数里面
        enc_outputs = self.pos_emb(enc_outputs.transpose(0, 1)).transpose(0, 1)

        # get_attn_pad_mask是为了得到句子中pad的位置信息,给到模型后面,在计算自注意力和交互注意力的时候去掉pad符号的影响
        enc_self_attn_mask = get_attn_pad_mask(enc_inputs, enc_inputs)
        enc_self_attns = []
        for layer in self.layers:
            # 去看EncoderLayer层函数
            enc_outputs, enc_self_attn = layer(enc_outputs, enc_self_attn_mask)
            enc_self_attns.append(enc_self_attn)
        return enc_outputs, enc_self_attns

进入Encoder后,首先进行Embedding,然后进行Positional Encoding。Embedding使用了nn.Embedding。n个Encoder Layer存放在nn.ModuleList()里的列表中。 

Encoder 部分包含三个部分:Word EmbeddingPosition EmbeddingMulti-Head Attention层及后续的Feed Forward层

  • Multi-Head Attention层:  主要就是进行attention的计算,QKV的矩阵运算都在这里。
  • Feed Forward层:  就是进行特征的提取,进行向前传播。

3.3 Padding Mask:形成一个符号矩阵

def get_attn_pad_mask(seq_q, seq_k):
    batch_size, len_q = seq_q.size()
    batch_size, len_k = seq_k.size()
    # eq(zero) is PAD token
    pad_attn_mask = seq_k.data.eq(0).unsqueeze(1)  # batch_size x 1 x len_k, one is masking
    # 最终得到的应该是一个最后n列为1的矩阵,即K的最后n个token为PAD。
    return pad_attn_mask.expand(batch_size, len_q, len_k)  # batch_size x len_q x len_k

padding mask的作用:

不同batch之间句子长度可以不一样,但是每个batch的长度必须是一样的:因此出现一个问题,不够长度需要加pad,使得其长度变成一样。

我们看一下这个图:

阴影部分是没有意义的,所以我们希望它是0,以便后续的softmax等操作。

padding mask的主要作用就是针对句子不够长的问题,我们加了 pad,因此需要对 pad 进行 遮掩mask

从代码角度来看:

这个函数最核心的一句代码是seq_k.data.eq(0),这句的作用是返回一个大小和seq_k一样的tensor,只不过里面的值只有True和False。如果seq_k某个位置的值等于0,那么对应位置就是True,否则即为False。

举个栗子:输入为seq_data = [1, 2, 3, 4, 0],seq_data.data.eq(0)就会返回[False, False, False, False, True]

【注意】由于在 Encoder 和 Decoder 中都需要进行 mask(和矩阵原大小一样,有问题的地方加负无穷) 操作,因此就无法确定这个函数的参数中 seq_len 的值,如果是在 Encoder 中调用的,seq_len 就等于 src_len;如果是在 Decoder 中调用的,seq_len 就有可能等于 src_len,也有可能等于 tgt_len(因为 Decoder 有两次 mask)。


🚀4. 解码器(Decoder)

上图红色框框为 Transformer 的 Decoder 结构,与 Encoder 相似,但是存在一些区别。

Decoder 包含两个 Multi-Head Attention 层。

  • 第一个 Multi-Head Attention 层采用了 Masked 操作。
  • 第二个 Multi-Head Attention 层的K, V矩阵使用 Encoder 编码信息矩阵C进行计算,而Q使用上一个 Decoder的输出计算

最后有一个 Softmax 层计算下一个翻译单词的概率。


4.1Decoder Layer:单个解码层 

Decoder模块由6个Decoder Layer组成,每个Decoder Layer结构完全一样,如图所示:

# -----------------------------------------------------------------------------#
# Decoder Layer包含了三个部分:解码器自注意力、“编码器-解码器”注意力、基于位置的前馈网络
# -----------------------------------------------------------------------------#
class DecoderLayer(nn.Module):
    def __init__(self):
        super(DecoderLayer, self).__init__()
        self.dec_self_attn = MultiHeadAttention()
        self.dec_enc_attn = MultiHeadAttention()
        self.pos_ffn = PoswiseFeedForwardNet()

    def forward(self, dec_inputs, enc_outputs, dec_self_attn_mask, dec_enc_attn_mask):
        dec_outputs, dec_self_attn = self.dec_self_attn(dec_inputs, dec_inputs, dec_inputs, dec_self_attn_mask)
        dec_outputs, dec_enc_attn = self.dec_enc_attn(dec_outputs, enc_outputs, enc_outputs, dec_enc_attn_mask)
        dec_outputs = self.pos_ffn(dec_outputs)
        return dec_outputs, dec_self_attn, dec_enc_attn

Decoder Layer包含了三个部分:解码器自注意力+“编码器-解码器”注意力+基于位置的前馈网络

每个Decoder Layer由三个子层连接结构组成:

  • 第一个子层连接结构:包括一个Multi-Head AttentionNorm层以及一个残差连接

在训练时,因为有目标数据可用,所以第一个Decoder LayerMulti-Head Attention的输入来自于目标数据,但是在测试时,已经没有目标数据可用了,那么,输入数据就来自于此前序列的Decoder 模块输出,没有预测过,那么就是起始标志的编码。同时,这里的注意力是自注意力,也就是说Q、K、V都来自于目标数据矩阵变化得来,然后计算注意力,另外,这里计算注意力值时,一定使用Mask操作。后续的5个Decoder Layer的输入数据是前一个Decoder Layer的输出。

  •  第二个子层连接结构:包括一个Multi-Head AttentionNorm层以及一个残差连接

Encoder的输出的结果将会作为K、V传入每一个Decoder Layer的第二个子层连接结构,而Q则是当前Decoder Layer的上一个子层连接结构的输出。注意,这里的Q、K、V已经不同源了,所以不再是自注意力机制。完成计算后,输出结果作为第三个子层连接结构的输入。

  • 第三个子层连接结构: 包括一个前馈全连接子层Norm层以及一个残差连接

完成计算后,输出结果作为输入进入下一个Decoder Layer。如果是最后一个Decoder Layer,那么输出结果就传入输出模块。


4.2Decoder:解码器

# -----------------------------------------------------------------------------#
# Decoder 部分包含三个部分:词向量embedding,位置编码部分,自注意力层及后续的前馈神经网络
# -----------------------------------------------------------------------------#
class Decoder(nn.Module):
    def __init__(self):
        super(Decoder, self).__init__()
        self.tgt_emb = nn.Embedding(tgt_vocab_size, d_model)
        self.pos_emb = PositionalEncoding(d_model)
        self.layers = nn.ModuleList([DecoderLayer() for _ in range(n_layers)])

    def forward(self, dec_inputs, enc_inputs, enc_outputs): # dec_inputs : [batch_size x target_len]
        dec_outputs = self.tgt_emb(dec_inputs)  # [batch_size, tgt_len, d_model]
        dec_outputs = self.pos_emb(dec_outputs.transpose(0, 1)).transpose(0, 1) # [batch_size, tgt_len, d_model]

        ## get_attn_pad_mask 自注意力层的时候的pad 部分
        dec_self_attn_pad_mask = get_attn_pad_mask(dec_inputs, dec_inputs)

        ## get_attn_subsequent_mask 这个做的是自注意层的mask部分,就是当前单词之后看不到,使用一个上三角为1的矩阵
        dec_self_attn_subsequent_mask = get_attn_subsequent_mask(dec_inputs)

        ## 两个矩阵相加,大于0的为1,不大于0的为0,为1的在之后就会被fill到无限小
        dec_self_attn_mask = torch.gt((dec_self_attn_pad_mask + dec_self_attn_subsequent_mask), 0)

        ## 这个做的是交互注意力机制中的mask矩阵,enc的输入是k,我去看这个k里面哪些是pad符号,给到后面的模型;注意哦,我q肯定也是有pad符号,但是这里我不在意的,之前说了好多次了哈
        dec_enc_attn_mask = get_attn_pad_mask(dec_inputs, enc_inputs)

        dec_self_attns, dec_enc_attns = [], []
        for layer in self.layers:
            dec_outputs, dec_self_attn, dec_enc_attn = layer(dec_outputs, enc_outputs, dec_self_attn_mask, dec_enc_attn_mask)
            dec_self_attns.append(dec_self_attn)
            dec_enc_attns.append(dec_enc_attn)
        return dec_outputs, dec_self_attns, dec_enc_attns

Decoder 部分包含三个部分:Word EmbeddingPosition EmbeddingMulti-Head Attention层及后续的Feed Forward层

Decoder Encoder类似,就是将6个Decoder Layer进行堆叠。第一个Decoder Layer接受目标数据作为输入,后续的Decoder使用前序一个Decoder Layer的输出作为输入,通过这种方式将6个Decoder Layer连接。最后一个Decoder Layer的输出将进入输出模块。 


4.3 Sequence Mask:屏蔽子序列的mask

屏蔽子序列的mask部分,这个函数就是用来表示Decoder的输入中哪些是未来词,使用一个上三角为1 的矩阵遮蔽未来词,让当前词看不到未来词。

img

def get_attn_subsequent_mask(seq):
    """
    seq: [batch_size, tgt_len]
    """
    attn_shape = [seq.size(0), seq.size(1), seq.size(1)]
    # attn_shape: [batch_size, tgt_len, tgt_len]
    subsequence_mask = np.triu(np.ones(attn_shape), k=1)  # 生成一个上三角矩阵
    subsequence_mask = torch.from_numpy(subsequence_mask).byte()
    return subsequence_mask  # [batch_size, tgt_len, tgt_len]

🚀5. 位置编码(Position Embedding)

Transformer 中需要使用Position Embedding 表示单词出现在句子中的位置。因为 Transformer 不采用 RNN 的结构,而是使用全局信息,因此是无法捕捉到序列顺序信息的,例如将K、V按行进行打乱,那么Attention之后的结果是一样的。但是序列信息非常重要,代表着全局的结构,因此必须将序列的分词相对或者绝对position信息利用起来。
 

class PositionalEncoding(nn.Module):
    def __init__(self, d_model, dropout=0.1, max_len=5000):
        super(PositionalEncoding, self).__init__()

        self.dropout = nn.Dropout(p=dropout)
		# 生成一个形状为[max_len,d_model]的全为0的tensor
        pe = torch.zeros(max_len, d_model)
        # position:[max_len,1],即[5000,1],这里插入一个维度是为了后面能够进行广播机制然后和div_term直接相乘
        # 注意,要理解一下这里position的维度。每个pos都需要512个编码。
        position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
        # 共有项,利用指数函数e和对数函数log取下来,方便计算
        div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))

        # 这里position * div_term有广播机制,因为div_term的形状为[d_model/2],即[256],符合广播条件,广播后两个tensor经过复制,形状都会变成[5000,256],*表示两个tensor对应位置处的两个元素相乘
        # 这里需要注意的是pe[:, 0::2]这个用法,就是从0开始到最后面,补长为2,其实代表的就是偶数位置赋值给pe
        pe[:, 0::2] = torch.sin(position * div_term)
        # 同理,这里是奇数位置
        pe[:, 1::2] = torch.cos(position * div_term)
        # 上面代码获取之后得到的pe:[max_len*d_model]

        # 下面这个代码之后,我们得到的pe形状是:[max_len*1*d_model]
        pe = pe.unsqueeze(0).transpose(0, 1)
		# 定一个缓冲区,其实简单理解为这个参数不更新就可以,但是参数仍然作为模型的参数保存
        self.register_buffer('pe', pe)  

    def forward(self, x):
        """
        x: [seq_len, batch_size, d_model]
        """
        # 这里的self.pe是从缓冲区里拿的
        # 切片操作,把pe第一维的前seq_len个tensor和x相加,其他维度不变
        # 这里其实也有广播机制,pe:[max_len,1,d_model],第二维大小为1,会自动扩张到batch_size大小。
        # 实现词嵌入和位置编码的线性相加
        x = x + self.pe[:x.size(0), :]
        return self.dropout(x)

位置编码的实现直接对照着公式写就行,上面这个代码只是其中一种实现方式。
【注意】pos代表的是单词在句子中的绝对索引位置,例如max_len是128,那么索引就是从0,1,2,…,127,假设d_model是512,即用一个512维tensor来编码一个索引位置,那么0<=2i<512,0<=i<=255,那么2i对应取值就是0,2,4…510,即偶数位置;2i+1的取值是1,3,5…511,即奇数位置。

最后的文本嵌入表征是词嵌入和位置编码相加得到。


🚀6. 注意力机制(Attention)

6.1Scaled DotProduct Attention:缩放点积注意力机制

class ScaledDotProductAttention(nn.Module):
    def __init__(self):
        super(ScaledDotProductAttention, self).__init__()

    def forward(self, Q, K, V, attn_mask):
        # 输入进来的维度分别是Q:[batch_size x n_heads x len_q x d_k]  K:[batch_size x n_heads x len_k x d_k]  V:[batch_size x n_heads x len_k x d_v]
        # matmul操作即矩阵相乘
        # [batch_size x n_heads x len_q x d_k] matmul [batch_size x n_heads x d_k x len_k] -> [batch_size x n_heads x len_q x len_k]
        scores = torch.matmul(Q, K.transpose(-1, -2)) / np.sqrt(d_k)

        # masked_fill_(mask,value)这个函数,用value填充源向量中与mask中值为1位置相对应的元素,
        # 要求mask和要填充的源向量形状需一致
        # 把被mask的地方置为无穷小,softmax之后会趋近于0,Q会忽视这部分的权重
        scores.masked_fill_(attn_mask, -1e9) # Fills elements of self tensor with value where mask is one.
        attn = nn.Softmax(dim=-1)(scores)
        context = torch.matmul(attn, V)
        # context:[batch_size,n_heads,len_q,d_k]
        # attn:[batch_size,n_heads,len_q,len_k]
        return context, attn

缩放点积注意力机制主要原理就是通过 Q 、K 计算出 scores,然后将 scores V 进行matmul操作,即矩阵相乘,这样得到每个单词的 context vector。

首先将 Q K 的转置相乘,相乘之后得到的 scores 还不能立刻进行 softmax,需要和 attn_mask 相加,把一些需要屏蔽的信息屏蔽掉,attn_mask 是一个仅由 True 和 False 组成的 tensor,并且一定会保证 attn_mask scores 的维度四个值相同(不然无法做对应位置相加)

mask 完了之后,就可以对 scores 进行 softmax 了。然后再与 V 相乘,得到 context。


6.2MultiHead Attention:多头注意力机制

与其只使用单独一个注意力汇聚, 我们可以用独立学习得到的h组(一般h=8)不同的线性投影来变换Q、K和V。

然后,这h组变换后的Q、K和V将并行地送到注意力汇聚中。 最后,将这h个注意力汇聚的输出拼接在一起, 并且通过另一个可以学习的线性投影进行变换, 以产生最终输出。 这种设计被称为多头注意力(multihead attention)。

class MultiHeadAttention(nn.Module):
    def __init__(self):
        super(MultiHeadAttention, self).__init__()
        # Wq,Wk,Wv其实就是一个线性层,用来将输入映射为Q、K、V
        # 这里输出是d_k * n_heads,因为是先映射,后分头。
        self.W_Q = nn.Linear(d_model, d_k * n_heads) 
        self.W_K = nn.Linear(d_model, d_k * n_heads)
        self.W_V = nn.Linear(d_model, d_v * n_heads)
        self.linear = nn.Linear(n_heads * d_v, d_model)
        self.layer_norm = nn.LayerNorm(d_model)

    def forward(self, Q, K, V, attn_mask):
        # attn_mask:[batch_size,len_q,len_k]
        # 输入的数据形状: Q: [batch_size x len_q x d_model], K: [batch_size x len_k x d_model], 
        # V: [batch_size x len_k x d_model]
        residual, batch_size = Q, Q.size(0)
        # (B, S, D) -proj-> (B, S, D) -split-> (B, S, H, W) -trans-> (B, H, S, W)

        # 分头;一定要注意的是q和k分头之后维度是一致的,所以一看这里都是d_k
        # q_s: [batch_size x n_heads x len_q x d_k]
        q_s = self.W_Q(Q).view(batch_size, -1, n_heads, d_k).transpose(1,2)
        # k_s: [batch_size x n_heads x len_k x d_k]
        k_s = self.W_K(K).view(batch_size, -1, n_heads, d_k).transpose(1,2)
        # v_s: [batch_size x n_heads x len_k x d_v]
        v_s = self.W_V(V).view(batch_size, -1, n_heads, d_v).transpose(1,2)

        # attn_mask:[batch_size x len_q x len_k] ---> [batch_size x n_heads x len_q x len_k]
        # 就是把pad信息复制n份,重复到n个头上以便计算多头注意力机制
        attn_mask = attn_mask.unsqueeze(1).repeat(1, n_heads, 1, 1)

        # 计算ScaledDotProductAttention
        # 得到的结果有两个:context: [batch_size x n_heads x len_q x d_v],
        # attn: [batch_size x n_heads x len_q x len_k]
        context, attn = ScaledDotProductAttention()(q_s, k_s, v_s, attn_mask)
        # 这里实际上在拼接n个头,把n个头的加权注意力输出拼接,然后过一个线性层,context变成
        # [batch_size,len_q,n_heads*d_v]。这里context需要进行contiguous,因为transpose后源tensor变成不连续的
        # 了,view操作需要连续的tensor。
        context = context.transpose(1, 2).contiguous().view(batch_size, -1, n_heads * d_v)
        output = self.linear(context)
        # 过残差、LN,输出output: [batch_size x len_q x d_model]和这一层的加权注意力表征向量
        return self.layer_norm(output + residual), attn

代码中有三处地方调用MultiHeadAttention():

  • Encoder Layer调用一次,传入的input_Qinput_Kinput_V全部都是enc_inputs
  • Decoder Layer中两次调用:
    • 第一次传入的全是dec_inputs
    • 第二次传入的分别是dec_outputs,enc_outputs,enc_outputs

这里需要注意一下:为啥都是d_k而不是d_q呢?

我们要注意的是q和k分头之后维度是一致的,所以这里都是dk


🚀7. 前馈神经网络(PoswiseFeedForward)

完成多头注意力计算后,考虑到此前一系列操作对复杂过程的拟合程度可能不足,所以通过增加全连接层来增强模型的拟合能力。 

有两种实现方式:一种是通过卷积的方式实现,一种是通过线性层实现。二者的区别除了原理上,还有代码细节上。

举个栗子:

  • 第一种卷积方式实现要求输入必须是[batch_size,channel,length],必须是三维tensor
  • 第二种线性层方式实现要求输入是[batch_size,*,d_model],可以有多个维度

7.1 实现方式1:Conv1d

class PoswiseFeedForwardNet(nn.Module):
    def __init__(self):
        super(PoswiseFeedForwardNet, self).__init__()
        self.conv1 = nn.Conv1d(in_channels=d_model, out_channels=d_ff, kernel_size=1)
        self.conv2 = nn.Conv1d(in_channels=d_ff, out_channels=d_model, kernel_size=1)
        self.layer_norm = nn.LayerNorm(d_model)

    def forward(self, inputs):
        residual = inputs # inputs : [batch_size, len_q, d_model]
        output = nn.ReLU()(self.conv1(inputs.transpose(1, 2)))
        output = self.conv2(output).transpose(1, 2)
        return self.layer_norm(output + residual)

 7.2 实现方式2:Linear

class PoswiseFeedForwardNet(nn.Module):
    def __init__(self):
        super(PoswiseFeedForwardNet, self).__init__()
        self.fc = nn.Sequential(
            nn.Linear(d_model, d_ff, bias=False),
            nn.ReLU(),
            nn.Linear(d_ff, d_model, bias=False))
        
    def forward(self, inputs):              # inputs: [batch_size, seq_len, d_model]
        residual = inputs
        output = self.fc(inputs)
        return nn.LayerNorm(d_model).(output + residual)   # [batch_size, seq_len, d_model]

这个方式比较好理解,就是做两次线性变换,残差连接后再跟一个Layer Norm

Layer Norm的作用:对x归一化,使x的均值为0,方差为1


以上就是transformer代码的解读。

更多详细解读还要看各位大佬的:

b站:Transformer代码(源码Pytorch版本)从零解读(Pytorch版本)_哔哩哔哩_bilibili

         手把手教你用Pytorch代码实现Transformer模型_哔哩哔哩_bilibili

CSDN:Transformer的PyTorch实现(超详细) 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/457546.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python 基础(十二):字典

❤️ 博客主页&#xff1a;水滴技术 &#x1f338; 订阅专栏&#xff1a;Python 入门核心技术 &#x1f680; 支持水滴&#xff1a;点赞&#x1f44d; 收藏⭐ 留言&#x1f4ac; 文章目录 一、声明字典1.1 使用 {} 声明字典1.2 使用 dict 函数声明字典1.3 声明一个空的字典 二…

【数据结构】AVLTree

1.AVL树的概念 二叉搜索树虽可以缩短查找的效率&#xff0c;但如果数据有序或接近有序二叉搜索树将退化为单支树&#xff0c;查找元素相当于在顺序表中搜索元素&#xff0c;效率低下。因此&#xff0c;两位俄罗斯的数G.M.Adelson-Velskii 和E.M.Landis在1962年 发明了一种解决上…

会话与会话技术(Session)

前言 Cookie将用户的信息保存在各自的浏览器中&#xff0c;并且可以在多次请求下实现数据的共享。但是如果当传递的信息较多时&#xff0c;Cookie技术会增大服务器程序处理的难度&#xff0c;因此&#xff0c;Servlet提供了另一种会话技术————Session&#xff0c;Session可…

跟李沐学AI——实用机器学习(入门版)

机器学习目录 2.1 探索性数据分析 2.2 数据清理 2.3 数据变换 2.4 特征工程 2.5 数据科学家的日常 Stanford University Practical machine learning 2.1 探索性数据分析 ​ 对目标的ftr数据进行处理&#xff0c;针对不同的信息做出不同的图形 输出数据集的行数和列数以及前十…

ArcGIS Pro地理空间数据处理完整工作流实训及python技术融合

GIS是利用电子计算机及其外部设备&#xff0c;采集、存储、分析和描述整个或部分地球表面与空间信息系统。简单地讲&#xff0c;它是在一定的地域内&#xff0c;将地理空间信息和 一些与该地域地理信息相关的属性信息结合起来&#xff0c;达到对地理和属性信息的综合管理。GIS的…

1、TI335x环境建立

记录裸机开发&#xff0c;TI A8系列处理器的AM335x过程&#xff0c;本次开发的是3352&#xff0c;在此基础上开发。 1、硬件准备&#xff1a; 已经测试调试ok的3352测试板&#xff0c;经过查看发现&#xff0c;am3352引出的下载接口是JTAG口&#xff0c;而我手里只有Jlink&…

Kubernetes---Pod调度、标签、配额、策略

静态pod 静态pod由user直接创建调用&#xff0c;不能迁移 由kebelet守护进程直接管理的pod&#xff0c;无需APIserver监管 kubelet监视每个静态pod 静态pod永远绑定到一个指定节点上的kubelet 静态pod spec不能引用其他API对象 静态pod配置路径/var/lib/kubelet/config.yaml里面…

CSS背景相关属性

一、背景颜色 属性名&#xff1a;background-color (bgc) 属性值&#xff1a;颜色取值&#xff1a;关键字&#xff0c;rgb表示法&#xff0c;rgba表示法&#xff0c;十六进制表示。 注&#xff1a; 背景颜色默认透明&#xff1a;rgba&#xff08;0&#xff0c;0&#xff0c…

C语言积锦

代码区&#xff1a;text 代码执行二进制码&#xff08;程序指令&#xff09; 具有共享、只读特性 数据区&#xff1a;1.初始化数据区data 2.未初始化数据区 bss 3.常量区 栈区&#xff1a;系统为每一个程序分配一个临时的空间 局部变量、函数信息、函数参数、数组。栈区大…

干货|Graphpad Prism也能做ERP图?So Easy!

Hello&#xff0c;大家好&#xff01; 这里是壹脑云科研圈&#xff0c;我是喵君姐姐~ 不知道你是否注意过这样一个现象。 在心理学大会报告的时候&#xff0c;专家经常会说一句话&#xff1a; 这个结果很漂亮&#xff01;&#xff01;&#xff01; 第一次听见的时候&#x…

K8s图形化管理工具Dasboard部署及使用

文章目录 一、Dashboard简介二、Dashboard部署安装三、配置Dashboard登入用户1、通过Token令牌登入2、通过kubeconfig文件登入 四、Dashboard创建容器 一、Dashboard简介 Kubernetes Dashboard是一个Web UI&#xff0c;用于管理Kubernetes集群中的应用程序和资源。它提供了一个…

使用wireshark抓包理解tcp协议和tls

首先下载安装wireshark 打开软件 1.选则自己连结的网络&#xff1b; 此时就会捕获的数据 2.加上端口过滤。 3.启动一个本地的http服务(这里采用的nodejs)&#xff1b; // server.js import koa from koa; const app new koa(); app.use(ctx > {ctx.body "hell…

托福听力专项 // Unit1 Listening for Main Ideas // Practice with Lectures // 共4篇

目录 Listening for Main Idea Lecture A a music class 单词 内容总结 Lecture B 单词 错题分析 Lecture C 单词 错题分析 Lecture D 单词 Listening for Main Idea Lecture A a music class 单词 evolve(v)to develop slowlyincorporate(v)to take in or includ…

Spring Boot使用(基础)

目录 1.Spring Boot是什么? 2.Spring Boot使用 2.1Spring目录介绍 2.2SpringBoot的使用 1.Spring Boot是什么? Spring Boot就是Spring脚手架,就是为了简化Spring开发而诞生的 Spring Boot的优点: 1.快速集成框架,提供了秒级继承各种框架,提供了启动添加依赖的功能 2.内…

修炼汇编语言第一章:汇编基础知识概述

目录 前言 一、汇编语言的组成 二&#xff1a;存储器 三&#xff1a;指令和数据 四&#xff1a;存储单元 五&#xff1a;CPU对存储器的读写 地址总线 控制总线 数据总线 前言 汇编语言是数据结构&#xff0c;操作系统&#xff0c;微机原理等重要课程的基础&#xff0…

【算法】冒泡排序

一.冒泡排序 主要思想&#xff1a; 反复交换相邻的元素&#xff0c;使较大的元素 逐渐冒泡到数组的末尾&#xff0c;从而实现排序的效果 实现过程&#xff1a; 1.遍历待排序数组&#xff0c;比较相邻的元素&#xff0c;如果前面的元素比后面的元素大&#xff0c; 就交换这两…

系统集成项目管理工程师 笔记(第八章:项目进度管理)

文章目录 8.1 规划项目进度管理 2938.1.1 规划项目进度管理的输入 2938.1.2 规划项目进度管理的工具与技术 2948.1.3 规划项目进度管理的输出 295 8.2 定义活动 2968.2.1 定义活动的输入 2968.2.2 定义活动的工具与技术 2968.2.3 定义活动的输出&#xff08;两清单、一属性&…

【深度学习】学习率与学习率衰减详解:torch.optim.lr_scheduler用法

【深度学习】学习率与学习率衰减详解&#xff1a;torch.optim.lr_scheduler用法 文章目录 【深度学习】学习率与学习率衰减详解&#xff1a;torch.optim.lr_scheduler用法1. 介绍1.1 学习率与学习率衰减 2. TensorFlow中的学习率衰减3. PyTorch中的学习率衰减2.1 optimizer 综述…

md/分类/信号领域/数字信号处理及MATLAB实现/频率调制(FM).md

文章目录 本文链接https://zh.wikipedia.org/wiki/频率调制用Python模拟FM/PM调制解调过程波形变化频率调制我的 本文链接 打死他 调频&#xff08;英语&#xff1a;Frequency Modulation&#xff0c;缩写&#xff1a;FM&#xff09;是一种以载波的瞬时频率变化来表示信息的方…

Java文件操作必备技能,10个小技巧让你快速掌握!

前言 在我们日常的开发中&#xff0c;文件操作是一个非常重要的主题。文件读写、文件复制、任意位置读写、缓存等技巧都是我们必须要掌握的。在这篇文章中&#xff0c;我将给你们介绍 10 个实用的文件操作技巧。 使用 try-with-resources 语句处理文件 IO 流&#xff0c;确保在…