从0开始transformer代码理解(附带调试和个人原理理解)

代码来源

本次代码来源自github https://github.com/graykode/nlp-tutorial
里面的5.1 transformer代码

本文目录

  • 代码来源
  • 第一步 数据准备(从main函数开始)
    • make_batch函数
  • **Transformer 主体函数定义代码**
  • Encoder层
    • 词向量维度嵌入
    • 掩码部分实现
    • 多层encoder叠加
      • **多头自注意力层**
    • 前馈神经网络、
  • Decodr部分
    • mask操作
    • 交叉注意力层实现
    • 前馈神经网络
  • 最后一部
  • 全部代码

第一步 数据准备(从main函数开始)

首先这里是自定义了三句话,分别是给到encoder的输入和decoder的输入还有测试的输入

sentences = ['ich mochte ein bier P', 'S i want a beer', 'i want a beer E']

然后把这些通过词向量编号变成,对应的向量。

#对应的词向量编号
    src_vocab = {'P': 0, 'ich': 1, 'mochte': 2, 'ein': 3, 'bier': 4}
    src_vocab_size = len(src_vocab)

    tgt_vocab = {'P': 0, 'i': 1, 'want': 2, 'a': 3, 'beer': 4, 'S': 5, 'E': 6}
    number_dict = {i: w for i, w in enumerate(tgt_vocab)}
    tgt_vocab_size = len(tgt_vocab)
    src_len = 5 # 我们训练的时候的源序列长度
    tgt_len = 5 # 测试集语句的长度

    d_model = 512  # Embedding Size  词嵌入的维度 可以理解为一个词有512个维度 它被分解成了具有一个512维度特征的向量 更浅显易懂的说就是把“我”这个词和512种不同的词作比较从而得到向量
    d_ff = 2048  # 这个先不管 是前馈神经网络所需要提高的维度
    d_k = d_v = 64  # 特征k v 的特征维度
    n_layers = 6  # Encoder 和 Decoder 的数量 也就是大N
    n_heads = 8  # 把qkv所需要分的头数  但是要注意 n_heads * d_k = d_model 为什么?因为输入的d_model被划分成8个子空间(也就是头数)这意味着每个头输出的维度是64计算完成后拼接起来

经过make_batch函数我们得到encoder和decoder的向量

enc_inputs, dec_inputs, target_batch = make_batch(sentences)

那他们是什么形状的呢?如下图 看到一句话是[1,5]也就是
在这里插入图片描述

make_batch函数

其实很简单 这个函数跑就是把词对应的编号一 一和我们的句子对应起来 比如S对应5. 以此类推

#用于将句子转换为批量数据
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)

那么接下来就来到重要的模型定义和训练

    model = Transformer()#  Transformer模型

    criterion = nn.CrossEntropyLoss()#交叉熵损失函数
    optimizer = optim.Adam(model.parameters(), lr=0.001)#    优化器

    for epoch in range(20):
        optimizer.zero_grad()
        outputs, enc_self_attns, dec_self_attns, dec_enc_attns = model(enc_inputs, dec_inputs)
        loss = criterion(outputs, target_batch.contiguous().view(-1))
        print('Epoch:', '%04d' % (epoch + 1), 'cost =', '{:.6f}'.format(loss))
        loss.backward()
        optimizer.step()

Transformer 主体函数定义代码

怎么说呢?主体代码就是根据transformer上面的图一样的,不会的可以看我上一篇博客。开始是encoder层然后是decoder层,最后用一个线性层降维得到我们所需要的结果

class Transformer(nn.Module):
    # 初始化函数
    def __init__(self):
        # 调用父类的初始化函数
        super(Transformer, self).__init__()
        # 定义编码器
        self.encoder = Encoder()
        # 定义解码器
        self.decoder = Decoder()
        # 定义一个线性层,将d_model个神经元的编码器输出转化为目标词汇表大小个神经元的输出,不添加偏置
        self.projection = nn.Linear(d_model, tgt_vocab_size, bias=False)
    # 前向传播函数
    def forward(self, enc_inputs, dec_inputs):
        # 调用编码器
        enc_outputs, enc_self_attns = self.encoder(enc_inputs)
        # 调用解码器
        dec_outputs, dec_self_attns, dec_enc_attns = self.decoder(dec_inputs, enc_inputs, enc_outputs)#enc_inputs是告诉解码端那些是pad符号
        # 调用线性层 调整输出形状  就是把我们的decoder输出的d_model维度映射到词汇表维度  在本例子种是512->5
        dec_logits = self.projection(dec_outputs) # dec_logits : [batch_size x src_vocab_size x tgt_vocab_size]
        # 返回解码  这里是方便我们调用损失函数  因为我们的形状是[batch_size x src_vocab_size x tgt_vocab_size]  所以batch * src_size是我们的总样本数 方便进行交叉熵计算
        return dec_logits.view(-1, dec_logits.size(-1)), enc_self_attns, dec_self_attns, dec_enc_attns

Encoder层

词向量维度嵌入

ok 进入我们transformer的第一步解释我们的encodr层,因为transformer里面的第一步是输入到encoder里面
**这里解释一下整个原理 ,我们的encoder必须是先加上位置编码 - - > 然后进行多头注意力机制(其中有用norm操作进行残差连接) --> 前馈神经网络层 ** 简单的三步却让人一生无法释怀!

我将分开讲,首先是定义,很简单,我想说的是这个词嵌入层,可能新手不太懂(比如我),那么你可以简单的理解为维度转换,这里就是把维度为[1,5,5]的数据变成[1,5,512] 我添加了打印代码,调试结果在下面。然后就是pos_emb 这个就是相当与把我们需要训练的数据加上下标,具体就是根据我们的编码表(这里是直接把我们的编码表输入了)获得对应的嵌入。

    def __init__(self):
        super(Encoder, self).__init__()
        # 定义编码器中的词嵌入层
        self.src_emb = nn.Embedding(src_vocab_size, d_model)
        # 定义编码器中的位置编码层,从预训练的编码表中获取
        self.pos_emb = nn.Embedding.from_pretrained(get_sinusoid_encoding_table(src_len+1, d_model),freeze=True)
        # 定义编码器中的层
        self.layers = nn.ModuleList([EncoderLayer() for _ in range(n_layers)])

在这里插入图片描述

掩码部分实现

OK进行下一步
这一步很重要!因为我们在训练的时候要要求我们的序列是等长的,但是很多时候我们的词汇并不是 这样,有长有短,所以我们必须用一些特定符号进行标记,这里我们用PAD来说明,但是填充的时候肯定要标记呀,不然把PAD也拿去训练那不是搞笑了吗。

        # 获取编码器自注意力时的mask  注意我们的输入都是enc_inputs
        enc_self_attn_mask = get_attn_pad_mask(enc_inputs, enc_inputs)

在这里插入图片描述

所以这个函数是这样的
我们的输入都是enc_inputs 为什么?** 这个问题就要从注意力点积开始了。注意力计算的核心操作是对查询(Q)和键(K)之间的点积。假设查询的维度是 [batch_size, len_q, d_k],键的维度是 [batch_size, len_k, d_k],点积结果的维度将会是 [batch_size, len_q, len_k]。
掩码矩阵的维度需要与这个点积结果的维度匹配,以便正确地应用掩码。因此,掩码矩阵的维度必须是 [batch_size, len_q, len_k]。**

def get_attn_pad_mask(seq_q, seq_k):#enc_inputs, enc_inputs 告诉后面的句子后面的层 那些是被pad符号填充的  pad的目的是让batch里面的每一行长度一致
    # 获取seq_q和seq_k的batch_size和长度
    batch_size, len_q = seq_q.size()
    batch_size, len_k = seq_k.size()
    # 创建一个全为0的mask矩阵,维度为batch_size x len_k
    pad_attn_mask = seq_k.data.eq(0).unsqueeze(1)  # batch_size x 1 x len_k(=len_q), one is maskingtensor([[[False, False, False, False,  True]]])  True代表为pad符号
    # 扩展mask矩阵,使其维度为batch_size x len_q x len_k
    a=pad_attn_mask.expand(batch_size, len_q, len_k)
    #tensor([[[False, False, False, False,  True],
        #  [False, False, False, False,  True],
        #  [False, False, False, False,  True],
        #  [False, False, False, False,  True],
        #  [False, False, False, False,  True]]])
    return pad_attn_mask.expand(batch_size, len_q, len_k)  # batch_size x len_q x len_k  重复len_q次
    

做完这些,最后就到了encoder的叠加,也就是N个encoder的叠加,把上一层的输出传给下一层
把上一个encoder的输出变为为下一个encoder的输入,还有encoder的掩码也要传递给下一个encoder,因为它们的掩码是通用的。

        # 遍历编码器中的每一层
        for layer in self.layers:
            # 将输出传递给下一层 
            enc_outputs, enc_self_attn = layer(enc_outputs, enc_self_attn_mask)
            # 将编码器自注意力添加到自注意力列表中 这个列表主要是最后的展示show 没什么用
            enc_self_attns.append(enc_self_attn)
        # 返回编码器的输出和自注意力列表

多层encoder叠加

那有人会问 ,我们的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_outputs, attn = self.enc_self_attn(enc_inputs, enc_inputs, enc_inputs, enc_self_attn_mask)  # encoder的输入的qkv特征都是来自一个input所以一样
        enc_outputs = self.pos_ffn(enc_outputs)  # enc_outputs: [batch_size x len_q x d_model]
        return enc_outputs, attn

多头自注意力层

``这个实现的代码就是通过给的input然后乘以权重矩阵Wq Wk Wv来获得 我们的qkv特征,当然我们再这里需要分头,所谓分头就是把我们的d_model特征分成n_head*d_q 或d_v 或d_k个头,然后进行注意力计算达到我们原文描述的效果。在下面注解种有。

class MultiHeadAttention(nn.Module):
    def __init__(self):#通过线形层映射一个qkv的特征矩阵
        super(MultiHeadAttention, self).__init__()
        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):
        # 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分成q_1 q_2.。。。
        q_s = self.W_Q(Q).view(batch_size, -1, n_heads, d_k).transpose(1,2)  # q_s: [batch_size x n_heads x len_q x d_k]
        k_s = self.W_K(K).view(batch_size, -1, n_heads, d_k).transpose(1,2)  # k_s: [batch_size x n_heads x len_k x d_k]
        v_s = self.W_V(V).view(batch_size, -1, n_heads, d_v).transpose(1,2)  # v_s: [batch_size x n_heads x len_k x d_v]
        print("W_Q", self.W_Q.weight)
        print("W_K", self.W_K.weight)
        print("W_V", self.W_V.weight)

当然对于分的每个头,我们都需要知道他们的掩码(也就是pad符号)所以我们对我们的掩码也进行分头(效果如下图)

 attn_mask = attn_mask.unsqueeze(1).repeat(1, n_heads, 1, 1) # attn_mask : [batch_size x n_heads x len_q x len_k]  对每个头我们都要知道他们的qkv特征的pad填充在哪所以也要分头

在这里插入图片描述
然后是注意力点积计算部分

		context, attn = ScaledDotProductAttention()(q_s, k_s, v_s, attn_mask)

函数ScaledDotProductAttention根据原文给的公式计算,我的上一篇博客有
这里注意一个问题,这里可能会问为什么不能先和v乘再进行softmax 因为qk乘以得到的结果矩阵和v维度不一样 再者我们假如乘完再softmax最后会失去其注意力机制的意义 首先因为乘以完v后我们会维度不匹配,scores 的形状是 [batch_size, seq_len, seq_len],V 的形状是 [batch_size, seq_len, d_v]。直接相乘在数学上是行不通的,因为矩阵乘法要求维度匹配。再者原文中只有这qk的公式。

class ScaledDotProductAttention(nn.Module):#这里是注意力机制的计算部分 
    def __init__(self):
        super(ScaledDotProductAttention, self).__init__()

    def forward(self, Q, K, V, attn_mask):
        scores = torch.matmul(Q, K.transpose(-1, -2)) / np.sqrt(d_k) # scores : [batch_size x n_heads x len_q(=len_k) x len_k(=len_q)]  做qk乘法后除以维度
        scores.masked_fill_(attn_mask, -1e9) # Fills elements of self tensor with value where mask is one. 把pad符号的值设置为负无穷  因为设置负无穷可以让softmax的值都为0
        attn = nn.Softmax(dim=-1)(scores) # 归一化 这里可能会问为什么不能先和v乘再进行softmax  因为qk乘以得到的结果矩阵和v维度不一样 再者我们假如乘完再softmax最后会失去其注意力机制的意义 因为我们要
        context = torch.matmul(attn, V)#[1,8,5,5]x[1,8,5,64]=[1,8,5,64] 计算矩阵乘法
        return context, attn

在这里插入图片描述
在这里插入图片描述

然后是把得到的context进行合并用于前馈神经网络降维后再进行残差链接


        context = context.transpose(1, 2).contiguous().view(batch_size, -1, n_heads * d_v) # context: [batch_size x len_q x n_heads * d_v]这里d_v是64
        output = self.linear(context)#线性层把512->512便于残差链接
        return self.layer_norm(output + residual), attn # output: [batch_size x len_q x d_model]残差链接

前馈神经网络、

  self.pos_ffn = PoswiseFeedForwardNet()  # 位置前馈神经网络

这里主要是对我们的通道进行缩放注解中有

 class PoswiseFeedForwardNet(nn.Module):#对应图的feedforward层  前馈神经网络增强非线性表达能力 具体就是将其低维度卷积->高纬度 用ReLU激活函数->还原低纬度
 def __init__(self):
     super(PoswiseFeedForwardNet, self).__init__()
     self.conv1 = nn.Conv1d(in_channels=d_model, out_channels=d_ff, kernel_size=1)#512->2048
     self.conv2 = nn.Conv1d(in_channels=d_ff, out_channels=d_model, kernel_size=1)#2048->512
     self.layer_norm = nn.LayerNorm(d_model)#顺手进行norm操作
#一维卷积 (nn.Conv1d) 需要输入的数据形状为 [batch_size, in_channels, length]。具体来说:

# batch_size:批次大小,表示输入样本的数量。
# in_channels:输入通道数,表示输入特征的维度。
# length:输入序列的长度。
 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)

Decodr部分

说到Dcoder 其实Dcoder和Encoder的代码差不多 也就是多了交叉注意力机制。但是Dcoder的输入我们需要注意,他们分别是encoder的输出和输入还有我们额decoder的输入。这个时候疑惑,这里的代码为什么还需要encoder的输入?其实这里的输入就是为了再交叉注意力计算的时候给出encoder 的掩码位置便于计算而已。

     # 调用解码器
     dec_outputs, dec_self_attns, dec_enc_attns = self.decoder(dec_inputs, enc_inputs, enc_outputs)#enc_inputs是告诉解码端那些是pad符号

初始化代码就是词嵌入和位置编码和encoder一样

class Decoder(nn.Module):
 def __init__(self):
     super(Decoder, self).__init__()
     self.tgt_emb = nn.Embedding(tgt_vocab_size, d_model)
     self.pos_emb = nn.Embedding.from_pretrained(get_sinusoid_encoding_table(tgt_len+1, d_model),freeze=True)
     self.layers = nn.ModuleList([DecoderLayer() for _ in range(n_layers)])

mask操作

因为在我们训练的时候需要蒙住操作,具体原理如下图
比如输入S的时候只能看到S看不到卷起来 输入卷的时候只能看到S和卷看不到起来
所以形成一个上三角矩阵。

请添加图片描述
代码如下 核心代码就是最后一句获得我们的mask矩阵

 def forward(self, dec_inputs, enc_inputs, enc_outputs): # dec_inputs : [batch_size x target_len]
     dec_outputs = self.tgt_emb(dec_inputs) + self.pos_emb(torch.LongTensor([[5,1,2,3,4]]))#和encoder的输入一样,需要加上位置编码
     dec_self_attn_pad_mask = get_attn_pad_mask(dec_inputs, dec_inputs)#解码端的自注意力mask 也就是告诉输入的pad符号不参与自注意力计算 并且告诉哪里是pad符号
     dec_self_attn_subsequent_mask = get_attn_subsequent_mask(dec_inputs)#得到decoder输入的上三角mask矩阵

在这里插入图片描述

当和pad矩阵两者相加就会得到我们要的矩阵大于0的部分置为1小于等于0 的部分置为0
代码如下

     dec_self_attn_mask = torch.gt((dec_self_attn_pad_mask + dec_self_attn_subsequent_mask), 0)#当和pad矩阵两者相加就会得到我们要的矩阵大于0的部分置为1  小于等于0 的部分置为0

交叉注意力层实现

我们知道交叉注意力 层我们需要encoder的qk和我们decoder的v来进行qkv多头注意力矩阵计算矩阵计算

交叉注意力层的mask矩阵 它的输入Quer来自于Masked Multi-Head Attention的输出,Keys和Values来自于Encoder中最后一层的输出。所以dec_input输入的是q enc_inputs输入的是k v

dec_enc_attn_mask = get_attn_pad_mask(dec_inputs, enc_inputs)#交叉注意力层的mask矩阵  它的输入Quer来自于Masked Multi-Head Attention的输出,Keys和Values来自于Encoder中最后一层的输出。所以dec_input输入的是q enc_inputs输入的是k v

然后我们就是decoder的层进行叠加6次,其实只要注意交叉注意力层就行了,但是我们要注意我们的输入要输入encoder的输出enc_outputs和decoder的输出dec_outputs,还有交叉注意力的掩码dec_enc_attn_mask和,自注意力掩码dec_self_attn_mask

     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

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

前馈神经网络

其实encoder和decoder的前馈神经网络是一样的,都是维度变换压缩然后512->2048 ->512然后进行残差链接。具体可以看我的代码注释

最后一部

就是计算损失…,用测试集测试,将返回的掩码进行展示
为什么返回掩码集合就是以下这样

   print('first head of last state enc_self_attns')
   showgraph(enc_self_attns)

   print('first head of last state dec_self_attns')
   showgraph(dec_self_attns)

   print('first head of last state dec_enc_attns')
   showgraph(dec_enc_attns)

全部代码

# %%
# code by Tae Hwan Jung(Jeff Jung) @graykode, Derek Miller @dmmiller612
# Reference : https://github.com/jadore801120/attention-is-all-you-need-pytorch
#           https://github.com/JayParks/transformer
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt

# S: Symbol that shows starting of decoding input
# E: Symbol that shows starting of decoding output
# P: Symbol that will fill in blank sequence if current batch data size is short than time steps

# 定义一个函数,用于将句子转换为批量数据
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)

# 定义一个函数,用于获取正弦编码表
def get_sinusoid_encoding_table(n_position, d_model):
   # 定义一个计算角度的函数
   def cal_angle(position, hid_idx):
       # 计算位置除以10000的幂次方,再乘以2乘以隐藏层索引除以d_model
       return position / np.power(10000, 2 * (hid_idx // 2) / d_model)
   # 定义一个获取位置角度向量的函数
   def get_posi_angle_vec(position):
       # 返回一个列表,列表的元素为计算得到的的角度
       return [cal_angle(position, hid_j) for hid_j in range(d_model)]

   # 创建一个n_position位置的正弦编码表
   sinusoid_table = np.array([get_posi_angle_vec(pos_i) for pos_i in range(n_position)])
   # 将正弦编码表的偶数列(dim 2i)设置为sin
   sinusoid_table[:, 0::2] = np.sin(sinusoid_table[:, 0::2])  # dim 2i
   # 将正弦编码表的奇数列(dim 2i+1)设置为cos
   sinusoid_table[:, 1::2] = np.cos(sinusoid_table[:, 1::2])  # dim 2i+1
   # 返回FloatTensor类型的正弦编码表
   return torch.FloatTensor(sinusoid_table)

def get_attn_pad_mask(seq_q, seq_k):#enc_inputs, enc_inputs 告诉后面的句子后面的层 那些是被pad符号填充的  pad的目的是让batch里面的每一行长度一致
   # 获取seq_q和seq_k的batch_size和长度
   batch_size, len_q = seq_q.size()
   batch_size, len_k = seq_k.size()
   # 创建一个全为0的mask矩阵,维度为batch_size x len_k  增加第一个维度,len_k个元素都是0
   pad_attn_mask = seq_k.data.eq(0).unsqueeze(1)  # batch_size x 1 x len_k(=len_q), one is maskingtensor([[[False, False, False, False,  True]]])  True代表为pad符号
   # 扩展mask矩阵,使其维度为batch_size x len_q x len_k
   a=pad_attn_mask.expand(batch_size, len_q, len_k)#tensor([[[False, False, False, False,  True],
       #  [False, False, False, False,  True],
       #  [False, False, False, False,  True],
       #  [False, False, False, False,  True],
       #  [False, False, False, False,  True]]])
   return pad_attn_mask.expand(batch_size, len_q, len_k)  # batch_size x len_q x len_k  重复len_q次
   

def get_attn_subsequent_mask(seq):
   attn_shape = [seq.size(0), seq.size(1), seq.size(1)]#创建三角矩阵维度
   subsequent_mask = np.triu(np.ones(attn_shape), k=1)#创建一个上三角为1的矩阵,为什么要这样 因为我们的mask是 比如[S我爱我家] 从S开始看就是说看S的时候只能看S 看到我的时候只能看过S和我。。。就是单词掩码
   subsequent_mask = torch.from_numpy(subsequent_mask).byte()
   return subsequent_mask

class ScaledDotProductAttention(nn.Module):#这里是注意力机制的计算部分 
   def __init__(self):
       super(ScaledDotProductAttention, self).__init__()

   def forward(self, Q, K, V, attn_mask):
       scores = torch.matmul(Q, K.transpose(-1, -2)) / np.sqrt(d_k) # scores : [batch_size x n_heads x len_q(=len_k) x len_k(=len_q)]  做qk乘法后除以维度
       scores.masked_fill_(attn_mask, -1e9) # Fills elements of self tensor with value where mask is one. 把pad符号的值设置为负无穷  因为设置负无穷可以让softmax的值都为0
       attn = nn.Softmax(dim=-1)(scores) # 归一化 这里可能会问为什么不能先和v乘再进行softmax  因为qk乘以得到的结果矩阵和v维度不一样 再者我们假如乘完再softmax最后会失去其注意力机制的意义 因为我们要
       context = torch.matmul(attn, V)#[1,8,5,5]x[1,8,5,64]=[1,8,5,64] 计算点积
       return context, attn

class MultiHeadAttention(nn.Module):
   def __init__(self):#通过线形层映射一个qkv的特征矩阵
       super(MultiHeadAttention, self).__init__()
       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):
       # 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分成q_1 q_2.。。。
       q_s = self.W_Q(Q).view(batch_size, -1, n_heads, d_k).transpose(1,2)  # q_s: [batch_size x n_heads x len_q x d_k]
       k_s = self.W_K(K).view(batch_size, -1, n_heads, d_k).transpose(1,2)  # k_s: [batch_size x n_heads x len_k x d_k]
       v_s = self.W_V(V).view(batch_size, -1, n_heads, d_v).transpose(1,2)  # v_s: [batch_size x n_heads x len_k x d_v]
       print("W_Q", self.W_Q.weight)
       print("W_K", self.W_K.weight)
       print("W_V", self.W_V.weight)
        
       

       attn_mask = attn_mask.unsqueeze(1).repeat(1, n_heads, 1, 1) # attn_mask : [batch_size x n_heads x len_q x len_k]  对每个头我们都要知道他们的qkv特征的pad填充在哪所以也要分头

       # context: [batch_size x n_heads x len_q x d_v], attn: [batch_size x n_heads x len_q(=len_k) x len_k(=len_q)]
       context, attn = ScaledDotProductAttention()(q_s, k_s, v_s, attn_mask)
       context = context.transpose(1, 2).contiguous().view(batch_size, -1, n_heads * d_v) # context: [batch_size x len_q x n_heads * d_v]这里d_v是64
       output = self.linear(context)#线性层把512->512便于残差链接
       return self.layer_norm(output + residual), attn # output: [batch_size x len_q x d_model]残差链接

class PoswiseFeedForwardNet(nn.Module):#对应图的feedforward层  前馈神经网络增强非线性表达能力 具体就是将其低维度卷积->高纬度 用ReLU激活函数->还原低纬度
   def __init__(self):
       super(PoswiseFeedForwardNet, self).__init__()
       self.conv1 = nn.Conv1d(in_channels=d_model, out_channels=d_ff, kernel_size=1)#512->2048
       self.conv2 = nn.Conv1d(in_channels=d_ff, out_channels=d_model, kernel_size=1)#2048->512
       self.layer_norm = nn.LayerNorm(d_model)#顺手进行norm操作
#一维卷积 (nn.Conv1d) 需要输入的数据形状为 [batch_size, in_channels, length]。具体来说:

# batch_size:批次大小,表示输入样本的数量。
# in_channels:输入通道数,表示输入特征的维度。
# length:输入序列的长度。
   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)

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_outputs, attn = self.enc_self_attn(enc_inputs, enc_inputs, enc_inputs, enc_self_attn_mask)  # enc_inputs to same Q,K,V
       enc_outputs = self.pos_ffn(enc_outputs)  # enc_outputs: [batch_size x len_q x d_model]
       return enc_outputs, attn

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

class Encoder(nn.Module):
   def __init__(self):
       super(Encoder, self).__init__()
       # 定义编码器中的词嵌入层
       self.src_emb = nn.Embedding(src_vocab_size, d_model)
       # 定义编码器中的位置编码层,从预训练的sinusoid编码表中获取
       self.pos_emb = nn.Embedding.from_pretrained(get_sinusoid_encoding_table(src_len+1, d_model),freeze=True)
       # 定义编码器中的层
       self.layers = nn.ModuleList([EncoderLayer() for _ in range(n_layers)])

   def forward(self, enc_inputs): # enc_inputs : [batch_size x source_len]
       # 将输入的词转换为词嵌入
       a=self.src_emb(enc_inputs)
       print(a)
       print(a.shape)
       enc_outputs = self.src_emb(enc_inputs) + self.pos_emb(torch.LongTensor([[1,2,3,4,0]]))
       print("enc_outputs:",enc_outputs)

       # 获取编码器自注意力时的mask
       enc_self_attn_mask = get_attn_pad_mask(enc_inputs, enc_inputs)
       enc_self_attns = []
       # 初始化编码器自注意力列表
       print("enc_self_attn_mask:",enc_self_attn_mask.shape)

       # 遍历编码器中的每一层
       for layer in self.layers:
           # 将输出传递给下一层
           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

class Decoder(nn.Module):
   def __init__(self):
       super(Decoder, self).__init__()
       self.tgt_emb = nn.Embedding(tgt_vocab_size, d_model)
       self.pos_emb = nn.Embedding.from_pretrained(get_sinusoid_encoding_table(tgt_len+1, d_model),freeze=True)
       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) + self.pos_emb(torch.LongTensor([[5,1,2,3,4]]))#和encoder的输入一样,需要加上位置编码
       dec_self_attn_pad_mask = get_attn_pad_mask(dec_inputs, dec_inputs)#解码端的自注意力mask 也就是告诉输入的pad符号不参与自注意力计算 并且告诉哪里是pad符号
       dec_self_attn_subsequent_mask = get_attn_subsequent_mask(dec_inputs)#得到decoder输入的上三角mask矩阵
       dec_self_attn_mask = torch.gt((dec_self_attn_pad_mask + dec_self_attn_subsequent_mask), 0)#当和pad矩阵两者相加就会得到我们要的矩阵大于0的部分置为1  小于等于0 的部分置为0

       dec_enc_attn_mask = get_attn_pad_mask(dec_inputs, enc_inputs)#交叉注意力层的mask矩阵  它的输入Quer来自于Masked Multi-Head Attention的输出,Keys和Values来自于Encoder中最后一层的输出。所以dec_input输入的是q enc_inputs输入的是k v

       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
# 定义Transformer类,继承自nn.Module
class Transformer(nn.Module):
   # 初始化函数
   def __init__(self):
       # 调用父类的初始化函数
       super(Transformer, self).__init__()
       # 定义编码器
       self.encoder = Encoder()
       # 定义解码器
       self.decoder = Decoder()
       # 定义一个线性层,将d_model个神经元的编码器输出转化为目标词汇表大小个神经元的输出,不添加偏置
       self.projection = nn.Linear(d_model, tgt_vocab_size, bias=False)
   # 前向传播函数
   def forward(self, enc_inputs, dec_inputs):
       # 调用编码器
       enc_outputs, enc_self_attns = self.encoder(enc_inputs)
       # 调用解码器
       dec_outputs, dec_self_attns, dec_enc_attns = self.decoder(dec_inputs, enc_inputs, enc_outputs)#enc_inputs是告诉解码端那些是pad符号
       # 调用线性层
       dec_logits = self.projection(dec_outputs) # dec_logits : [batch_size x src_vocab_size x tgt_vocab_size]
       # 返回解码 logits
       return dec_logits.view(-1, dec_logits.size(-1)), enc_self_attns, dec_self_attns, dec_enc_attns

def showgraph(attn):
   attn = attn[-1].squeeze(0)[0]
   attn = attn.squeeze(0).data.numpy()
   fig = plt.figure(figsize=(n_heads, n_heads)) # [n_heads, n_heads]
   ax = fig.add_subplot(1, 1, 1)
   ax.matshow(attn, cmap='viridis')
   ax.set_xticklabels(['']+sentences[0].split(), fontdict={'fontsize': 14}, rotation=90)
   ax.set_yticklabels(['']+sentences[2].split(), fontdict={'fontsize': 14})
   plt.show()

if __name__ == '__main__':
   sentences = ['ich mochte ein bier P', 'S i want a beer', 'i want a beer E']

   # Transformer Parameters
   # Padding Should be Zero
   src_vocab = {'P': 0, 'ich': 1, 'mochte': 2, 'ein': 3, 'bier': 4}
   src_vocab_size = len(src_vocab)

   tgt_vocab = {'P': 0, 'i': 1, 'want': 2, 'a': 3, 'beer': 4, 'S': 5, 'E': 6}
   number_dict = {i: w for i, w in enumerate(tgt_vocab)}
   tgt_vocab_size = len(tgt_vocab)

   src_len = 5 # length of source
   tgt_len = 5 # length of target

   d_model = 512  # Embedding Size  词嵌入的维度 可以理解为一个词有512个维度 它被分解成了具有一个512维度特征的向量 更浅显易懂的说就是把“我”这个词和512种不同的词作比较从而得到向量
   d_ff = 2048  # 这个先不管 是前馈神经网络所需要提高的维度
   d_k = d_v = 64  # 特征k v 的特征维度
   n_layers = 6  # number of Encoder of Decoder Layer
   n_heads = 8  # 把qkv所需要分的头数  但是要注意 n_heads * d_k = d_model 为什么?因为输入的d_model被划分成8个子空间(也就是头数)这意味着每个头输出的维度是64计算完成后拼接起来

   model = Transformer()#  Transformer模型

   criterion = nn.CrossEntropyLoss()#交叉熵损失函数
   optimizer = optim.Adam(model.parameters(), lr=0.001)#    优化器

   enc_inputs, dec_inputs, target_batch = make_batch(sentences)
   print("sentences"+str(sentences))

   print("make_batch:"+str(enc_inputs.shape))
   print("make_batch:"+str(enc_inputs))
   print("make_batch:"+str(dec_inputs.shape))
   print("make_batch:"+str(dec_inputs))
   for epoch in range(20):
       optimizer.zero_grad()
       outputs, enc_self_attns, dec_self_attns, dec_enc_attns = model(enc_inputs, dec_inputs)
       loss = criterion(outputs, target_batch.contiguous().view(-1))
       print('Epoch:', '%04d' % (epoch + 1), 'cost =', '{:.6f}'.format(loss))
       loss.backward()
       optimizer.step()

   # Test
   predict, _, _, _ = model(enc_inputs, dec_inputs)
   predict = predict.data.max(1, keepdim=True)[1]
   print(sentences[0], '->', [number_dict[n.item()] for n in predict.squeeze()])

   print('first head of last state enc_self_attns')
   showgraph(enc_self_attns)

   print('first head of last state dec_self_attns')
   showgraph(dec_self_attns)

   print('first head of last state dec_enc_attns')
   showgraph(dec_enc_attns)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux运维之需掌握的基本Linux命令

前言:本博客仅作记录学习使用,部分图片出自网络,如有侵犯您的权益,请联系删除 目录 一、SHELL 二、执行命令 三、常用系统工作命令 四、系统状态检测命令 五、查找定位文件命令 六、文本文件编辑命令 七、文件目录管理命令…

echarts--Tree的label上添加图片

使用echarts的rich富文本,配合lable的formatter去实现 主要代码:label里 rich: {img1: {backgroundColor: {image: Cloudy,},height: 40}},formatter: function (param) {var res "";res {img1|} param.name;return res;}, 如果想要哪一节…

Python 生成Md文件带超链 和 PDF文件 带分页显示内容

software.md # -*- coding: utf-8 -*- import os f open("software.md", "w", encoding"utf-8") f.write(内部测试版2024 MD版\n) for root, dirs, files in os.walk(path): dax os.path.basename(root)if dax "":print("空白…

大模型补贴政策来了!!!

广州琶洲人工智能与数字经济试验区管理委员会 广州市海珠区科技工业商务和信息化局关于印发广州市海珠区建设人工智能大模型应用示范区实施细则的通知 各有关单位: 为进一步促进海珠区人工智能大模型产业发展,加快建设人工智能大模型应用示范区&#xf…

昇思MindSpore学习总结八——模型保存与加载

在训练网络模型的过程中,实际上我们希望保存中间和最后的结果,用于微调(fine-tune)和后续的模型推理与部署,接下来将介绍如何保存与加载模型。 1.构建模型 import numpy as np import mindspore from mindspore impo…

【RocketMQ】记录一次RocketMQ消费延迟问题排查思路

文章目录 背景问题排查Consumer负载均衡机制订阅关系的一致 背景 业务团队反馈使用我提供的RocketMQ集群,上游生产的消息,部分消息,消费程序需要等1分钟,甚至几分钟后,才能收到。 问题排查 见怪不怪,大部…

TapData 医疗美容行业数字化白皮书上线

随着颜值经济的发展,人们追求“美”的热情也不断高涨,而作为近年来被资本频频下注的赛道之一,医疗美容逐渐走进大众视野。 一方面,社会对外貌的重视程度不断提升,越来越多的人愿意通过医疗美容来提升自身形象&#xf…

后端加前端Echarts画图示例全流程(折线图,饼图,柱状图)

本文将带领读者通过一个完整的Echarts画图示例项目,演示如何结合后端技术(使用Spring Boot框架)和前端技术(使用Vue.js或React框架)来实现数据可视化。我们将实现折线图、饼图和柱状图三种常见的数据展示方式&#xff…

【python】PyQt5事件机制、定时器原理分析和实战演练

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,…

哨兵1SAR空间数据包协议数据单元文档(五)

《哨兵1SAR空间数据包协议数据单元》文档对数据包的结构进行了详细描述,并提供了用户数据的格式和解码算法。 原文链接: 哨兵1SAR空间数据包协议数据单元文档英文版 同系列中的其他文章篇链接: 哨兵1SAR空间数据包协议数据单元文档(一) 哨兵1…

亚太万人eVTOL展!2024深圳eVTOL将于9月登陆鹏城

2024年以来,北京、上海等十多个省市,先后发布了鼓励低空经济发展的行动方案,其中,eVTOL(电动垂直起降航空器)成为低空经济最火热的细分赛道。2023年,中国eVTOL产业规模达9.8亿元,同比…

【C++】相机标定源码笔记- RGB 相机与 ToF 深度传感器校准类

类的设计目标是为了实现 RGB 相机与 ToF 深度传感器之间的高精度校准,从而使两种类型的数据能够在同一个坐标框架内被整合使用。这在很多场景下都是非常有用的,比如在3D重建、增强现实、机器人导航等应用中,能够提供更丰富的场景信息。 -----…

64位Office API声明语句第120讲

跟我学VBA,我这里专注VBA, 授人以渔。我98年开始,从源码接触VBA已经20余年了,随着年龄的增长,越来越觉得有必要把这项技能传递给需要这项技术的职场人员。希望职场和数据打交道的朋友,都来学习VBA,利用VBA,起码可以提高…

工厂模式之简单工厂模式

文章目录 工厂模式工厂模式分为工厂模式的角色简单工厂模式案例代码定义一个父类,三个子类定义简单工厂客户端使用输出结果 工厂模式 工厂模式属于创造型的模式,用于创建对象。 工厂模式分为 简单工厂模式:定义一个简单工厂类,根…

Vue3的模板语法插值表达式用法

在template中输入“5 3” &#xff0c;是没有运算能力的&#xff0c;只会把字符直接显示出来&#xff0c;代码如下&#xff1a; <template><view>这是demo</view><view>5 3</view><navigator open-type"navigateBack"><vi…

永劫无间国服延迟高、报错、卡顿的处理措施一览

永劫无间国服延迟高、报错、卡顿怎么办&#xff1f;快速解决办法分享 第一个办法&#xff1a;改善延迟 如果是一直遇到永劫无间国服延迟高、报错、卡顿的问题&#xff0c;重启游戏也不管用的话&#xff0c;那应该就是网络问题&#xff0c;玩家可以启动雷神&#xff0c;让其快速…

服了!DELETE 同一行记录也会造成死锁!!

1 问题背景 “哥们&#xff0c;又双叒叕写了个死锁&#xff0c;秀啊&#xff01;&#x1f60f;” 就算是经常写死锁的同学看到估计都会有点懵&#xff0c;两条一模一样的 DELETE 语句怎么会产生死锁呢&#xff1f; 2 MySQL 锁回顾 看到这里的靓仔肯定对 MySQL 的锁非常了解&…

红酒与建筑:品味历史与艺术的交汇

在时间的长河中&#xff0c;红酒与建筑都是人类智慧的结晶&#xff0c;它们各自承载着历史的厚重与艺术的韵味。当这两者交汇时&#xff0c;仿佛是一场穿越时空的对话&#xff0c;将我们带入一个既古老又现代、既深沉又温柔的世界。今天&#xff0c;就让我们一起走进这个奇妙的…

企业消费采购成本和员工体验如何实现“鱼和熊掌“的兼得?

有企业说企业消费采购成本和员工体验的关系好比是“鱼和熊掌”&#xff0c;无法兼得&#xff1f; 要想控制好成本就一定要加强管控&#xff0c;但是加强管控以后&#xff0c;就会很难让员工获得满意的体验度。如果不加以管控&#xff0c;员工自由度增加了&#xff0c;往往就很难…

为什么要在成像应用中使用图像采集卡?

达到最大产量是工业和工厂自动化的关键标准之一。提高传感器分辨率和帧速率有助于实现这一目标&#xff0c;但也使带宽达到极限&#xff0c;并提出了新的传输问题。当前高带宽接口(如10GigE、相机直接与PC连接和嵌入式系统)的实现促使成像应用的许多用户询问如何以最佳配置最优…