手搓Transformer代码(简易版)复现

环境配置

1.创建环境

2.下包 

math、torch、numpy

代码流程

1.基本设置

1.导包

import math
import torch
import numpy as np
import torch.nn as nn
import torch.optim as optim
import torch.utils.data as Data

2.基本设置

#设置设备为cuda
device = 'cuda'

# 周期为50
epochs = 50

2.设置数据集及处理

1.设置手动数据集

定义了一个训练数据集sentence,其中包含了三对中文到英语的句子。每个句子都被分成了三个部分:enc_input编码器的输入,即中文句子),dec_input解码器的输入,包含了一个开始符号S和英语句子),dec_output解码器的输出,包含了英语句子和一个结束符号E)。

# 训练集
sentences = [
    # 中文和英语的单词个数不要求相同
    # enc_input                dec_input           dec_output
    ['我 有 一 个 好 朋 友 P', 'S I have a good friend .', 'I have a good friend . E'],
    ['我 有 零 个 女 朋 友 P', 'S I have zero girl friend .', 'I have zero girl friend . E'],
    ['我 有 一 个 男 朋 友 P', 'S I have a boy friend .', 'I have a boy friend . E']
]

为中文(源语言)和英语(目标语言)分别创建了词汇表src_vocabtgt_vocab
src_idx2word为一个字典,将单词映射到唯一的索引。同时创建了索引到单词的映射src_idx2wordidx2word,以及计算词汇表的大小src_vocab_sizetgt_vocab_size

# 中文和英语的单词要分开建立词库
# Padding Should be Zero
src_vocab = {'P': 0, '我': 1, '有': 2, '一': 3,
             '个': 4, '好': 5, '朋': 6, '友': 7, '零': 8, '女': 9, '男': 10}
src_idx2word = {i: w for i, w in enumerate(src_vocab)}
src_vocab_size = len(src_vocab)

tgt_vocab = {'P': 0, 'I': 1, 'have': 2, 'a': 3, 'good': 4,
             'friend': 5, 'zero': 6, 'girl': 7,  'boy': 8, 'S': 9, 'E': 10, '.': 11}
idx2word = {i: w for i, w in enumerate(tgt_vocab)}
tgt_vocab_size = len(tgt_vocab)

源语言和目标语言句子的最大长度src_lentgt_len

d_model:模型中嵌入向量的维度,也是位置编码的维度。

d_ff:前馈神经网络中间层的维度。

d_kd_v:注意力机制中查询(Q)和键(K)、值(V)的维度。

n_layers:编码器和解码器中层的数量。

n_heads:多头注意力机制中的头数。

src_len = 8  # (源句子的长度)enc_input max sequence length
tgt_len = 7  # dec_input(=dec_output) max sequence length

# Transformer Parameters
d_model = 512  # Embedding Size(token embedding和position编码的维度)
# FeedForward dimension (两次线性层中的隐藏层 512->2048->512,线性层是用来做特征提取的),当然最后会再接一个projection层
d_ff = 2048
d_k = d_v = 64  # dimension of K(=Q), V(Q和K的维度需要相同,这里为了方便让K=V)
n_layers = 6  # number of Encoder of Decoder Layer(Block的个数)
n_heads = 8  # number of heads in Multi-Head Attention(有几套头)

2.处理加工数据集

定义了一个函数make_data,其目的是将句子中的单词转换成对应的数字序列,这些数字序列可以用于训练机器翻译模型。

函数接受一个参数sentence,这是一个列表,其中包含了多个句子,每个句子都是一个包含三个字符串的列表:中文句子(编码器输入)、带有开始符号的英文句子(解码器输入)、以及英文句子(解码器输出)。

首先初始化了三个空列表enc_inputsdec_inputsdec_outputs,用于存储转换后的数字序列。然后,它遍历sentence中的每个句子,并将每个句子的单词转换成对应的数字。这是通过使用之前定义的src_vocabtgt_vocab词汇表来完成的,这些词汇表将单词映射到唯一的整数索引。

对于每个句子,代码执行以下操作:

  • enc_input:将中文句子中的每个单词转换为其在src_vocab中的索引,并将结果存储为一个列表。
  • dec_input:将带有开始符号的英文句子中的每个单词转换为其在tgt_vocab中的索引,并将结果存储为一个列表。
  • dec_output:将英文句子中的每个单词(不包括开始符号)转换为其在tgt_vocab中的索引,并将结果存储为一个列表。

然后,使用extend方法将这些转换后的列表添加到enc_inputsdec_inputsdec_outputs中。最后,函数返回三个torch.LongTensor对象,这些对象包含了所有转换后的数字序列,可以用于训练PyTorch模型。

在函数调用make_data(sentences)之后,enc_inputsdec_inputsdec_outputs将包含转换后的数字序列,这些序列可以用于训练Transformer模型进行机器翻译。

def make_data(sentences):
    """把单词序列转换为数字序列"""
    enc_inputs, dec_inputs, dec_outputs = [], [], []
    for i in range(len(sentences)):
 
        enc_input = [[src_vocab[n] for n in sentences[i][0].split()]]
        dec_input = [[tgt_vocab[n] for n in sentences[i][1].split()]]
        dec_output = [[tgt_vocab[n] for n in sentences[i][2].split()]]

        #[[1, 2, 3, 4, 5, 6, 7, 0], [1, 2, 8, 4, 9, 6, 7, 0], [1, 2, 3, 4, 10, 6, 7, 0]]
        enc_inputs.extend(enc_input)
        #[[9, 1, 2, 3, 4, 5, 11], [9, 1, 2, 6, 7, 5, 11], [9, 1, 2, 3, 8, 5, 11]]
        dec_inputs.extend(dec_input)
        #[[1, 2, 3, 4, 5, 11, 10], [1, 2, 6, 7, 5, 11, 10], [1, 2, 3, 8, 5, 11, 10]]
        dec_outputs.extend(dec_output)

    return torch.LongTensor(enc_inputs), torch.LongTensor(dec_inputs), torch.LongTensor(dec_outputs)


enc_inputs, dec_inputs, dec_outputs = make_data(sentences)

3.构建Transformer模型

1.全部代码

# ====================================================================================================
# Transformer模型

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)

        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
        div_term = torch.exp(torch.arange(
            0, d_model, 2).float() * (-math.log(10000.0) / d_model))
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        pe = pe.unsqueeze(0).transpose(0, 1)
        self.register_buffer('pe', pe)

    def forward(self, x):
        """
        x: [seq_len, batch_size, d_model]
        """
        x = x + self.pe[:x.size(0), :]
        return self.dropout(x)


def get_attn_pad_mask(seq_q, seq_k):
    # pad mask的作用:在对value向量加权平均的时候,可以让pad对应的alpha_ij=0,这样注意力就不会考虑到pad向量
    """这里的q,k表示的是两个序列(跟注意力机制的q,k没有关系),例如encoder_inputs (x1,x2,..xm)和encoder_inputs (x1,x2..xm)
    encoder和decoder都可能调用这个函数,所以seq_len视情况而定
    seq_q: [batch_size, seq_len]
    seq_k: [batch_size, seq_len]
    seq_len could be src_len or it could be tgt_len
    seq_len in seq_q and seq_len in seq_k maybe not equal
    """
    batch_size, len_q = seq_q.size()  # 这个seq_q只是用来expand维度的
    batch_size, len_k = seq_k.size()
    # eq(zero) is PAD token
    # 例如:seq_k = [[1,2,3,4,0], [1,2,3,5,0]]
    # [batch_size, 1, len_k], True is masked
    pad_attn_mask = seq_k.data.eq(0).unsqueeze(1)
    # [batch_size, len_q, len_k] 构成一个立方体(batch_size个这样的矩阵)
    return pad_attn_mask.expand(batch_size, len_q, len_k)


def get_attn_subsequence_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]

2.位置编码PositionalEncoding

  这段代码定义了一个名为PositionalEncoding的PyTorch模块,用于对Transformer模型中的序列进行位置编码

  Transformer模型是一种基于自注意力机制的模型,它在对序列进行处理时,需要一种方法来保留序列中单词的顺序信息。位置编码就是用来添加这种顺序信息的

class PositionalEncoding(nn.Module):
'''
定义了一个名为PositionalEncoding的PyTorch神经网络模块,它继承自nn.Module。
d_model(嵌入维度),dropout( dropout概率),和max_len(最大序列长度)作为参数
创建了一个nn.Dropout实例,用于在训练过程中随机地将一些节点置零,以防止过拟合。
'''
    def __init__(self, d_model, dropout=0.1, max_len=5000):
        super(PositionalEncoding, self).__init__()
        self.dropout = nn.Dropout(p=dropout)
'''
二维张量pe,其大小为max_len乘以d_model
初始化所有值为0。这个张量将用于存储位置编码
'''
        pe = torch.zeros(max_len, d_model)
'''
一维张量position,其包含了从0到max_len的所有整数。
使用unsqueeze方法增加了一个维度,使其形状变为(max_len, 1)
可以与d_model维度进行广播运算。
'''
        position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
'''
计算了一个除数div_term,通过对d_model个元素的一维张量应用指数函数得到的。
这个张量的元素是从0开始,每次增加2(只计算偶数位置),
并且乘以(-math.log(10000.0) / d_model)。
这个除数将用于正弦和余弦函数的计算。
'''
        div_term = torch.exp(torch.arange(
            0, d_model, 2).float() * (-math.log(10000.0) / d_model))
'''
计算位置编码的正弦和余弦部分
将它们存储在pe张量的偶数和奇数位置上
每个位置的编码都是由正弦和余弦函数的值组成的
'''
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
'''
首先在pe的最前面增加了一个维度,使其形状变为(1, max_len, d_model)
然后交换了第一维和第二维的位置,使其形状变为(max_len, 1, d_model)
这样做的目的是为了在后续的加法操作中能够正确地进行广播。
'''
        pe = pe.unsqueeze(0).transpose(0, 1)
'''
将pe张量注册为一个模型缓冲区
pe张量就会被视为模型的一部分,可以在模型的保存和加载操作中被保存和恢复。
'''
        self.register_buffer('pe', pe)
'''
PositionalEncoding模块的前向传播函数
它接收一个形状为(seq_len, batch_size, d_model)的输入张量x
并将其与pe张量的相应部分相加
然后,应用dropout正则化,并返回结果。这样,输入序列的每个位置都添加了相应的位置编码。
'''
    def forward(self, x):
        """
        x: [seq_len, batch_size, d_model]
        """
        x = x + self.pe[:x.size(0), :]
        return self.dropout(x)

3.自注意力机制

定义了一个名为ScaledDotProductAttention的PyTorch模块,它实现了自注意力机制(Scaled Dot-Product Attention)的一种变体,这是Transformer模型中的一个关键组成部分

class ScaledDotProductAttention(nn.Module):
'''
定义了一个名为ScaledDotProductAttention的类
继承自nn.Module。在初始化方法中,没有添加任何额外的层或参数
因为自注意力机制本身不需要任何额外的参数。
'''
    def __init__(self):
        super(ScaledDotProductAttention, self).__init__()
'''
这个方法定义了自注意力机制的前向传播过程。它接收四个张量作为输入:
Q:查询张量,形状为[batch_size, n_heads, len_q, d_k]。
K:键张量,形状为[batch_size, n_heads, len_k, d_k]。
V:值张量,形状为[batch_size, n_heads, len_v, d_v],其中len_v等于len_k。
attn_mask:注意力掩码张量,形状为[batch_size, n_heads, seq_len, seq_len],用于屏蔽某些位置的注意力。
scores张量是通过将Q和K张量进行矩阵乘法并除以d_k的平方根得到的,这样可以确保注意力分数的计算不会过拟合
'''
    def forward(self, Q, K, V, attn_mask):
        scores = torch.matmul(Q, K.transpose(-1, -2)) / \
            np.sqrt(d_k)  # scores : [batch_size, n_heads, len_q, len_k]
'''
masked_fill函数将attn_mask张量中值为1的位置对应的scores张量中的元素填充为-1e9。这样做是为了确保在这些位置上的注意力分数被忽略。
'''
        scores.masked_fill_(attn_mask, -1e9)
'''
attn张量是通过将scores张量在最后一个维度上应用nn.Softmax函数得到的
这将注意力分数转换为概率分布。
'''
        attn = nn.Softmax(dim=-1)(scores)  # 对最后一个维度(v)做softmax
'''
context张量是通过将attn张量与V张量进行矩阵乘法得到的,
这相当于对每个查询位置应用了注意力分布。
'''
        context = torch.matmul(attn, V)
        return context, attn

4.多头注意力机制

定义了一个名为 MultiHeadAttention 的 PyTorch 模块,用于实现多头注意力机制。 Transformer 架构中的一个关键组件,可以用于编码器(Encoder)的自注意力(Self-Attention)、解码器(Decoder)的掩蔽自注意力(Masked Self-Attention)以及编码器-解码器(Encoder-Decoder)的注意力机制。

class MultiHeadAttention(nn.Module):
    """这个Attention类可以实现:
    Encoder的Self-Attention
    Decoder的Masked Self-Attention
    Encoder-Decoder的Attention
    输入:seq_len x d_model
    输出:seq_len x d_model
    """
'''
在初始化函数中,定义了四个全连接层
分别用于生成查询(Q)、键(K)、值(V)以及合并多头注意力的输出。
'''
    def __init__(self):
        super(MultiHeadAttention, self).__init__()
        self.W_Q = nn.Linear(d_model, d_k * n_heads, bias=False)  # q,k必须维度相同,不然无法做点积
        self.W_K = nn.Linear(d_model, d_k * n_heads, bias=False)
        self.W_V = nn.Linear(d_model, d_v * n_heads, bias=False)
        # 这个全连接层可以保证多头attention的输出仍然是seq_len x d_model
        self.fc = nn.Linear(n_heads * d_v, d_model, bias=False)
'''
前向传播函数定义了输入inputQ,K,V和注意力掩码attn_mask
同时保存了输入 input_Q 的残差连接residual和批处理大小input_Q.size(0)
'''
    def forward(self, input_Q, input_K, input_V, attn_mask):
        """
        input_Q: [batch_size, len_q, d_model]
        input_K: [batch_size, len_k, d_model]
        input_V: [batch_size, len_v(=len_k), d_model]
        attn_mask: [batch_size, seq_len, seq_len]
        """
        residual, batch_size = input_Q, input_Q.size(0)
'''
对输入 input_Q、input_K 和 input_V 进行线性变换,
然后将它们重新塑造和转置以适应多头注意力的结构
'''
        Q = self.W_Q(input_Q).view(batch_size, -1, n_heads, d_k).transpose(1, 2)
        K = self.W_K(input_K).view(batch_size, -1, n_heads, d_k).transpose(1, 2)
        V = self.W_V(input_V).view(batch_size, -1, n_heads, d_v).transpose(1, 2)

'''
将注意力掩码从三维扩展到四维以匹配多头注意力的结构
context: [batch_size, n_heads, len_q, d_v], attn: [batch_size, n_heads, len_q, len_k]
'''
        context, attn = ScaledDotProductAttention()(Q, K, V, attn_mask)


'''
下面将不同头的输出向量拼接在一起
context: [batch_size, n_heads, len_q, d_v] -> [batch_size, len_q, n_heads * d_v]
调用 ScaledDotProductAttention 函数来计算注意力权重和上下文向量
'''
        context = context.transpose(1, 2).reshape(
            batch_size, -1, n_heads * d_v)

'''
这个全连接层可以保证多头attention的输出仍然是seq_len x d_model
'''
        output = self.fc(context)  # [batch_size, len_q, d_model]
        return nn.LayerNorm(d_model).to(device)(output + residual), attn





5.位置前馈网络

定义了一个名为 PoswiseFeedForwardNet 的 PyTorch 模块。

实现了一个位置前馈网络,这是 Transformer 模型中的另一个关键组件。

位置前馈网络在每个位置上应用相同的全连接网络,对序列中的每个元素进行相同的非线性变换

class PoswiseFeedForwardNet(nn.Module):
'''
在初始化函数中,定义了一个顺序模型 fc
它包含两个全连接层和一个 ReLU 激活函数
第一个全连接层将输入维度从 d_model 变换到 d_ff
第二个全连接层将维度从 d_ff 变换回 d_model
两个全连接层都不使用偏置。
'''

    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)
        )

'''
在前向传播函数中
首先,保存了输入 inputs 的残差连接
然后,将输入通过定义的顺序模型 fc 进行处理
最后,将处理后的输出与残差连接相加,并通过一个层归一化(Layer Normalization)层
这个层归一化层的目的是对每个样本的特征进行归一化处理,以提高模型的稳定性和性能。最终输出仍然是 [batch_size, seq_len, d_model] 维度。
'''
    def forward(self, inputs):
        """
        inputs: [batch_size, seq_len, d_model]
        """
        residual = inputs
        output = self.fc(inputs)
        # [batch_size, seq_len, d_model]
        return nn.LayerNorm(d_model).to(device)(output + residual)

6.编码器层EncoderLayer

定义了一个名为 EncoderLayer 的 PyTorch 模块

它代表了 Transformer 模型中编码器(Encoder)的一个层级

每个编码器层包含一个多头自注意力(Self-Attention)机制和一个位置前馈网络(Position-wise Feed-Forward Network)。

'''
在初始化函数中
创建了 MultiHeadAttention 和 PoswiseFeedForwardNet 的实例
分别用于实现多头自注意力和位置前馈网络。
'''
class EncoderLayer(nn.Module):
    def __init__(self):
        super(EncoderLayer, self).__init__()
        self.enc_self_attn = MultiHeadAttention()
        self.pos_ffn = PoswiseFeedForwardNet()
'''
前向传播函数定义了输入 enc_inputs 和自注意力掩码 enc_self_attn_mask
enc_inputs 是编码器的输入序列
enc_self_attn_mask 用于在自注意力机制中屏蔽填充(padding)或序列中的后续位置,以保持自回归性质。
'''
    def forward(self, enc_inputs, enc_self_attn_mask):
        """
        enc_inputs: [batch_size, src_len, d_model]
        enc_self_attn_mask: [batch_size, src_len, src_len]  # mask矩阵(pad mask or sequence mask)
        """
'''
调用 MultiHeadAttention 模块来计算自注意力
由于是自注意力,所以查询(Q)、键(K)和值(V)都来自同一个输入 enc_inputs
注意力掩码 enc_self_attn_mask 也被传递以实现掩蔽效果。
'''
        # enc_outputs: [batch_size, src_len, d_model], attn: [batch_size, n_heads, src_len, src_len]
        # 第一个enc_inputs * W_Q = Q
        # 第二个enc_inputs * W_K = K
        # 第三个enc_inputs * W_V = V
        enc_outputs, attn = self.enc_self_attn(enc_inputs, enc_inputs, enc_inputs,
                                               enc_self_attn_mask)  # enc_inputs to same Q,K,V(未线性变换前)


7.解码器层模块DecoderLayer

定义了一个 DecoderLayer 类,这是一个神经网络层

用于实现基于注意力机制(Attention)的解码器(Decoder)

这个类是典型的用于 Transformer 模型中的解码器层。

初始化类及三个字模块

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()
'''
DecoderLayer 继承自 nn.Module,这是所有 PyTorch 神经网络模块的基类。
在 __init__ 方法中,定义了三个子模块:
self.dec_self_attn:多头自注意力机制,用于解码器自身的输入。
self.dec_enc_attn:多头注意力机制,用于解码器和编码器之间的交互。
self.pos_ffn:位置前馈网络(Position-wise Feed-Forward Network),通常包括两个线性变换和一个激活函数。
'''

前向传播过程

'''
参数说明
dec_inputs:解码器的输入,形状为 [batch_size, tgt_len, d_model]。
enc_outputs:编码器的输出,形状为 [batch_size, src_len, d_model]。
dec_self_attn_mask:解码器自注意力的掩码,形状为 [batch_size, tgt_len, tgt_len]。
dec_enc_attn_mask:解码器-编码器注意力的掩码,形状为 [batch_size, tgt_len, src_len]。
'''
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)  
# 这里的Q,K,V全是Decoder自己的输入
    dec_outputs, dec_enc_attn = self.dec_enc_attn(dec_outputs, enc_outputs, enc_outputs,
                                                  dec_enc_attn_mask)  
# Attention层的Q(来自decoder) 和 K,V(来自encoder)
    # [batch_size, tgt_len, d_model]
    dec_outputs = self.pos_ffn(dec_outputs)
    # dec_self_attn, dec_enc_attn这两个是为了可视化的
    return dec_outputs, dec_self_attn, dec_enc_attn

8.编码器类Encoder

定义了一个 Transformer 模型中的编码器(Encoder)类

该类由词嵌入层位置编码层多个编码层(Encoder Layer)组成。

初始化类

class Encoder(nn.Module):
'''
Encoder 类继承自 nn.Module。
在初始化方法 __init__ 中,定义了三个主要组件:
src_emb:用于将输入的单词序列src_vocab_size映射到 d_model 维的嵌入空间,是一个嵌入层。
pos_emb:位置编码层,用于为嵌入向量添加位置信息。位置编码是固定的,不需要训练。
layers:包含多个编码层的列表,每个编码层由 EncoderLayer 类实例化。

'''
    def __init__(self):
        super(Encoder, self).__init__()
        self.src_emb = nn.Embedding(src_vocab_size, d_model) 
        # 词嵌入层
        self.pos_emb = PositionalEncoding(d_model) 
        # 位置编码层,Transformer中位置编码时是固定的,不需要学习
        self.layers = nn.ModuleList([EncoderLayer() for _ in range(n_layers)]) 
        # 编码层列表

前向传播过程

def forward(self, enc_inputs):
'''
使用初始化的src_emb方法
将输入的单词序列enc_inputs通过嵌入层映射到 d_model 维的向量表示。
'''
    enc_outputs = self.src_emb(enc_inputs)  # [batch_size, src_len, d_model]
'''
将嵌入后的向量添加位置编码 使用pos_emb方法添加位置编码
由于位置编码需要输入的形状为 [src_len, batch_size, d_model]
所以进行了转置操作,添加位置编码后再转置回来。
'''
    enc_outputs = self.pos_emb(enc_outputs.transpose(0, 1)).transpose(0, 1)  
'''
生成自注意力的掩码矩阵enc_self_attn_mask,用于在计算自注意力时屏蔽填充位置(padding)
'''
    # [batch_size, src_len, d_model]
    enc_self_attn_mask = get_attn_pad_mask(enc_inputs, enc_inputs) 
'''
初始化自注意力权重enc_self_attns
依次通过每个编码层layers,每个编码层接收上一个编码层的输出和自注意力掩码作为输入,输出编码后的结果 enc_outputs 和自注意力权重 enc_self_attn。
将每个编码层的自注意力权重保存在 enc_self_attns 列表中,用于可视化。
'''
    # [batch_size, src_len, src_len]
    enc_self_attns = []
    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

9.解码器类Decoder

定义了 Transformer 模型中的解码器(Decoder)类。解码器由词嵌入层位置编码层多个解码层(Decoder Layer)组成

初始化方法

class Decoder(nn.Module):
'''
Decoder 类继承自 nn.Module。
在初始化方法 __init__ 中,定义了三个主要组件:
tgt_emb:用于将目标序列的单词tgt_vocab_size嵌入到 d_model 维的嵌入空间。
pos_emb:位置编码层positionalEncoding,用于为嵌入向量添加位置信息。
位置编码是固定的,不需要训练。
layers:包含多个解码层的列表,每个解码层由 DecoderLayer 类实例化
'''
    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, tgt_len]
    enc_inputs: [batch_size, src_len]
    enc_outputs: [batch_size, src_len, d_model]   # 用于 Encoder-Decoder 注意力层
  """
'''
将目标序列dec_input通过嵌入层映射到 d_model 维的向量表示。
'''
    dec_outputs = self.tgt_emb(dec_inputs)  # [batch_size, tgt_len, d_model]
'''
为嵌入后的向量添加位置编码pos_emb方法。
位置编码层的输入和输出的第一个维度是时间步(即序列长度),
因此需要进行转置操作,添加位置编码后再转置回来,并将结果转移到指定的设备(如 GPU)
'''
    dec_outputs = self.pos_emb(dec_outputs.transpose(0, 1)).transpose(0, 1).to(device) 
 # [batch_size, tgt_len, d_model]
'''
生成自注意力的掩码矩阵dec_self_attn_pad_mask
包括填充掩码dec_self_attn_pad_mask和子序列掩码dec_self_attn_subsequence_mask
填充掩码用于屏蔽填充位置(padding)
子序列掩码用于确保当前时刻看不到未来的信息。
'''
    dec_self_attn_pad_mask = get_attn_pad_mask(dec_inputs, dec_inputs).to(device) 
# [batch_size, tgt_len, tgt_len]
    dec_self_attn_subsequence_mask = get_attn_subsequence_mask(dec_inputs).to(device) 
# [batch_size, tgt_len, tgt_len]
    dec_self_attn_mask = torch.gt((dec_self_attn_pad_mask + dec_self_attn_subsequence_mask), 0).to(device) 
# [batch_size, tgt_len, tgt_len]
'''
生成 Encoder-Decoder 注意力掩码dec_enc_attn_mask
用于屏蔽编码器输出中的填充位置。
'''
    dec_enc_attn_mask = get_attn_pad_mask(dec_inputs, enc_inputs).to(device)
 # [batch_size, tgt_len, src_len]
'''
依次通过每个解码层
每个解码层接收上一个解码层的输出和注意力掩码作为输入
输出解码后的结果 dec_outputs 和自注意力权重 dec_self_attn 
解码器-编码器注意力权重 dec_enc_attn。

将每个解码层的自注意力权重和解码器-编码器注意力权重
保存在 dec_self_attns 和 dec_enc_attns 列表中,用于可视化
'''
    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

10.完整的Transformer模型

定义了一个完整的 Transformer 模型,包括编码器(Encoder)解码器(Decoder)和一个线性投影层

实例化和初始模型

class Transformer(nn.Module):
'''
初始化部分:
self.encoder:实例化并初始化编码器encoder。
self.decoder:实例化并初始化解码器decoder。
self.projection:一个线性投影层Linear,用于将解码器的输出投影到目标词汇表的维度。
'''
    def __init__(self):
        super(Transformer, self).__init__()
        self.encoder = Encoder().to(device)
        self.decoder = Decoder().to(device)
        self.projection = nn.Linear(d_model, tgt_vocab_size, bias=False).to(device)
def forward(self, enc_inputs, dec_inputs):
    """
    解码器输入的维度和编码器输入的维度
    enc_inputs: [batch_size, src_len]
    dec_inputs: [batch_size, tgt_len]
    """

    '''
    将enc_inputs传入编码器
    得到编码器的输出 enc_outputs 和自注意力权重 enc_self_attns。
    '''
    enc_outputs, enc_self_attns = self.encoder(enc_inputs)

    '''
    将 dec_inputs、enc_inputs 和 enc_outputs 传入解码器
    得到解码器的输出 dec_outputs、自注意力权重 dec_self_attns、解码器-编码器注意力权重 dec_enc_attns。
    '''
    dec_outputs, dec_self_attns, dec_enc_attns = self.decoder(dec_inputs, enc_inputs, enc_outputs)
    '''
    将解码器的输出dec_outputs通过线性投影层projection
    映射到目标词汇表的维度,得到 dec_logits
    返回形状为 [batch_size * tgt_len, tgt_vocab_size] 的 dec_logits
    以及编码器和解码器的注意力权重。
    '''
    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 模型,并将其移动到指定的设备(例如 GPU)
使用交叉熵损失函数CrossEntropyLoss,并ignore_index忽略索引为 0 的位置(即填充位置 pad)
使用随机梯度下降(SGD)优化器,学习率为 1e-3,动量为 0.99
提到使用 Adam 优化器效果不好,因此选择了 SGD
'''
model = Transformer().to(device)
criterion = nn.CrossEntropyLoss(ignore_index=0)
optimizer = optim.SGD(model.parameters(), lr=1e-3, momentum=0.99)

4.模型训练过程

1.Transomer模型训练过程

定义了模型训练的过程,使用了一个数据加载器 loader 来迭代数据并更新模型参数。

'''
epochs 表示训练的总轮数。在每一轮训练中,都会遍历整个数据集一次
'''
for epoch in range(epochs):
'''
loader 是一个数据加载器,每次迭代会返回一个批次的输入和输出数据。
dec_outputs:解码器的输出,形状为 [batch_size, tgt_len],用于计算损失.
enc_inputs:编码器的输入,形状为 [batch_size, src_len]。
dec_inputs:解码器的输入,形状为 [batch_size, tgt_len]。
'''
    for enc_inputs, dec_inputs, dec_outputs in loader:
'''
将编码器输入、解码器输入和解码器输出数据移动到指定的计算设备上
'''
        enc_inputs, dec_inputs, dec_outputs = enc_inputs.to(device), dec_inputs.to(device), dec_outputs.to(device)
'''
前向传播计算输出
调用模型的前向传播方法,得到模型的输出 outputs 以及注意力权重。
outputs:形状为 [batch_size * tgt_len, tgt_vocab_size] 的预测结果。
enc_self_attns:编码器的自注意力权重。
dec_self_attns:解码器的自注意力权重。
dec_enc_attns:解码器-编码器的注意力权重。
'''
        outputs, enc_self_attns, dec_self_attns, dec_enc_attns = model(enc_inputs, dec_inputs)
'''
计算损失值函数
使用交叉熵损失函数criterion计算损失。
outputs:模型的预测输出,形状为 [batch_size * tgt_len, tgt_vocab_size]。
dec_outputs.view(-1):真实的目标输出标签,形状为 [batch_size * tgt_len]。
'''
        loss = criterion(outputs, dec_outputs.view(-1))
'''
打印当前 epoch 的编号和对应的损失值。
'''
        print('Epoch:', '%04d' % (epoch + 1), 'loss =', '{:.6f}'.format(loss))
'''
optimizer.zero_grad():清除上一次计算的梯度。
loss.backward():计算当前损失的梯度。
optimizer.step():根据计算的梯度更新模型的参数。
'''
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

2.贪心算法过程

定义了一个贪心解码器 greedy_decoder,用于在推理阶段逐步生成目标序列。

def greedy_decoder(model, enc_input, start_symbol):
    """
    贪心解码器。
    对于简化操作,贪心解码器是 K=1 时的 Beam Search。在推理时
因为我们不知道目标序列的输入,
    所以我们尝试逐词生成目标输入,然后将其输入到 Transformer 中。
    :param model: Transformer 模型
    :param enc_input: 编码器输入
    :param start_symbol: 起始符号。在这个例子中是 'S',对应索引 4
    :return: 生成的目标输入
    """

编码部分

'''
对编码器输入进行编码
得到编码器输出 enc_outputs 和编码器自注意力权重 enc_self_attns。
'''
enc_outputs, enc_self_attns = model.encoder(enc_input)

初始化解码器输入

'''
初始化一个空的解码器输入 tensor
形状为 [1, 0]
数据类型与编码器输入相同
'''
dec_input = torch.zeros(1, 0).type_as(enc_input.data)

贪心解码循环

'''
terminal:解码循环的终止条件。
next_symbol:下一个要生成的符号,初始化为起始符号 start_symbol。
'''
terminal = False
next_symbol = start_symbol
while not terminal:
'''
将当前生成的 next_symbol 拼接到解码器输入序列 dec_input 的末尾
'''
    dec_input = torch.cat([dec_input.to(device), torch.tensor([[next_symbol]], dtype=enc_input.dtype).to(device)], -1)
'''
将更新后的解码器输入、编码器输入和编码器输出传入解码器,
得到解码器输出 dec_outputs。
'''
    dec_outputs, _, _ = model.decoder(dec_input, enc_input, enc_outputs)
'''
将解码器输出通过线性投影层映射到目标词汇表的维度
'''
    projected = model.projection(dec_outputs)
'''
选择概率最高的词作为下一个要生成的词 next_word。
更新 next_symbol 为 next_word。
'''
    prob = projected.squeeze(0).max(dim=-1, keepdim=False)[1]
    next_word = prob.data[-1]
    next_symbol = next_word
'''
如果生成的 next_symbol 是终止符号 'E',
则设置 terminal 为 True,终止解码循环。
'''
    if next_symbol == tgt_vocab["E"]:
        terminal = True

返回生成的序列

'''
返回生成的目标序列 greedy_dec_predict
跳过起始符号 start_symbol(即取 dec_input 的所有列,跳过第一列)
'''
greedy_dec_predict = dec_input[:, 1:]
return greedy_dec_predict

5.设计示例

展示了如何使用训练好的 Transformer 模型在预测阶段进行翻译,将中文句子翻译成英文句子

sentences = [
    # enc_input                dec_input           dec_output
    ['我 有 一 个 男 朋 友 P', '', '']
]
'''
make_data 函数将 sentences 转换为适合模型输入的数据格式,
生成 enc_inputs、dec_inputs 和 dec_outputs。
使用 Data.DataLoader 创建一个数据加载器 test_loader,用于迭代测试数据。
使用 next(iter(test_loader)) 获取一个批次的输入数据 enc_inputs
'''
enc_inputs, dec_inputs, dec_outputs = make_data(sentences)
test_loader = Data.DataLoader(MyDataSet(enc_inputs, dec_inputs, dec_outputs), 2, True)
enc_inputs, _, _ = next(iter(test_loader))
print()
print("="*30)
print("利用训练好的Transformer模型将中文句子'我 有 零 个 女 朋 友' 翻译成英文句子: ")

'''
遍历每个输入句子 enc_inputs。
使用 greedy_decoder 函数对每个句子进行翻译:
model:训练好的 Transformer 模型。
enc_inputs[i].view(1, -1).to(device):将当前输入句子调整形状并移动到设备上。
start_symbol=tgt_vocab["S"]:设置起始符号为 S。
打印输入句子 enc_inputs[i] 及其翻译结果 greedy_dec_predict。
使用 src_idx2word 和 idx2word 字典将索引转换为单词并打印翻译前后的句子
'''
for i in range(len(enc_inputs)):
    greedy_dec_predict = greedy_decoder(model, enc_inputs[i].view(1, -1).to(device), start_symbol=tgt_vocab["S"])
    print(enc_inputs[i], '->', greedy_dec_predict.squeeze())
    print([src_idx2word[t.item()] for t in enc_inputs[i]], '->', [idx2word[n.item()] for n in greedy_dec_predict.squeeze()])

6.结果展示

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值