1.1.1 基于Transformer模型的机器翻译模型训练
在实现机器翻译(中英翻译为例)前,首先需要搜集大量的中英文对照数据,并进行机器翻译模型的训练,其模型训练代码如下:
#第15章/15.1.1/基于Transformer模型的机器翻译模型训练-第一部分
#导入相关的库
import math
import torch
import numpy as np
import torch.nn as nn
import torch.optim as optim
import torch.utils.data as Data
import torch.nn.functional as F
device = 'cpu' #使用cpu还是 GPU进行加速推理训练#device = 'cuda'
epochs = 100 #transformer模型训练的步数
#训练集
#这里为了演示随机输入了3个句子,当然真正的模型训练是需要大量的数据
sentences = [
#中文和英语的句子,单词个数不一定相同
#编码器输入 解码器输入 解码器输出
['我 爱 你
['你 好 吗
['人 工 智 能
]
#Padding 字符一般定义为0,其他的单词可以自行定义
#中文词库
src_vocab = {'P': 0, '我': 1, '爱': 2, '你': 3, '好': 4, '吗': 5, '人':6, '工':7, '智':8, '能':9}
src_idx2word = {i: w for i, w in enumerate(src_vocab)} #把单词的字典变成
#{0: 'P', 1: '我', 2: '爱', 3: '你', 4: '好', 5: '吗', 6: '人', 7: '工',
#8: '智', 9: '能'}
src_vocab_size = len(src_vocab) #输入编码器的词库数据长度
#英文词库
tgt_vocab = {'P': 0, 'i': 1, 'love': 2, 'you': 3, 'how': 4, 'are': 5, 'artificial':6,'intelligence':7, 'S': 8, 'E': 9, '.': 10}
idx2word = {i: w for i, w in enumerate(tgt_vocab)} #把单词的字典变成
#{0: 'P', 1: 'i', 2: 'love', 3: 'you', 4: 'how', 5: 'are', 6: 'artificial', #7: 'intelligence', 8: 'S', 9: 'E', 10: '.'}
tgt_vocab_size = len(tgt_vocab) #输入解码器的词库数据长度
src_len = 8 #编码器输入最大句子长度,长度不够的使用pad 代替
tgt_len = 7 #解码器输入最大句子长度,长度不够的使用pad 代替
#Transformer 超参数
d_model = 512 #词嵌入维度
d_ff = 2048 #前馈神经网络的维度
d_k = d_v = 64 #多头注意力机制维度
n_layers = 6 #模型搭建的层数
n_heads = 8 #多头注意力机制的头数
在初始化部分,主要是定义Transformer模型的一些超参数及输入给模型的数据集。当然要想真正训练一个机器翻译的大模型,需要搜集大量的训练数据集,而这里为了演示代码的运行过程,定义了几个中英翻译的句子。每个句子的中英文的数据按照列表类型保存起来,其每列表有三部分组成,例如['我 爱 你
(1)第一部分是中文数据“我 爱 你 P”,此数据会传递给Transformer模型的编码器,作为中文输入数据,其中字母“P”代表掩码字符。
(2)第二部分是对应的英文数据“S i love you .”,此数据会传递给Transformer模型的解码器,作为英文输入数据,其字母“S”代表单词start,Transformer模型碰到此单词“S”则说明模型需要开始预测下一个单词了。
(3)第三部分是英文数据“i love you . E”,此数据作为Transformer模型解码器的输出,其字母“E”,代表单词end,Transformer模型一旦碰到字母“E”,则说明模型需要结束预测。
得到训练集数据后,需要把每个汉字及每个英文单词都使用阿拉伯数字来代替以便后期进行词嵌入操作,例如“0”代表“P”,中文数据集中“1”代表“我”,英文数据集中“1”代表“i”等。需要把数据集按照数字依次排列,当然每个数字代表的数据集单词并不是固定的,只要确保不重复使用即可,其代码如下:
#第15章/15.1.1/基于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()]
#每次生成这一行sentence中encoder_input对应的id编码
for _ in range(src_len-len(enc_input)):
enc_input.append(0)
dec_input = [tgt_vocab[n] for n in sentences[i][1].split()]
#每次生成这一行sentence中decoder_input对应的id编码
for _ in range(tgt_len-len(dec_input)):
dec_input.append(0)
dec_output = [tgt_vocab[n] for n in sentences[i][2].split()]
#每次生成这一行sentence中decoder_output对应的id编码
for _ in range(tgt_len-len(dec_output)):
dec_output.append(0)
enc_inputs.append(enc_input)
dec_inputs.append(dec_input)
dec_outputs.append(dec_output)
return torch.LongTensor(enc_inputs), torch.LongTensor(dec_inputs), torch.LongTensor(dec_outputs)
#格式化输入的数据,把单词序列转换为数字序列,数据长度不够的使用0填充
enc_inputs, dec_inputs, dec_outputs = make_data(sentences)
#定义 data loader,方便模型的训练
class MyDataSet(Data.Dataset):
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),batch_size=3, shuffle=True)
数据集处理完成后,还需要把数据集中英文与中文单词都删除,以便获取完全数字的数据。make_data函数的功能便是输入中英文数据集,获取编码器的数字输入数据及解码器的输入输出数字数据。由于定义了编码器与解码器最大句子长度,因此此函数执行完成后,其输入数据长度不足定义的句子长度的,需要使用数字“0”进行填充。代码执行完成后,输出如下:
enc_inputs:
tensor([[1, 2, 3, 0, 0, 0, 0, 0],
[3, 4, 5, 0, 0, 0, 0, 0],
[6, 7, 8, 9, 0, 0, 0, 0]])
dec_inputs:
tensor([[ 8, 1, 2, 3, 10, 0, 0],
[ 8, 4, 5, 3, 10, 0, 0],
[ 8, 6, 7, 10, 0, 0, 0]])
dec_outputs:
tensor([[ 1, 2, 3, 10, 9, 0, 0],
[ 4, 5, 3, 10, 9, 0, 0],
[ 6, 7, 10, 9, 0, 0, 0]])
可以看到编码器与解码器的输入数据都被格式化成了数字数据,使用此数据就可以执行Transformer模型相关的功能模块代码了,其代码如下:
#第15章/15.1.1/基于Transformer模型的机器翻译模型训练-第三部分
#词嵌入
class Embeddings(nn.Module): #定义一个Embeddings类,继承自nn.Module
def __init__(self, vocab_size, d_model):
#初始化函数,输入词汇表大小和词向量维度
super(Embeddings, self).__init__() #调用父类的初始化函数
self.emb = nn.Embedding(vocab_size,d_model)
#定义一个nn.Embedding对象,用于词向量映射
def forward(self,x): #前向传播函数,输入x
return self.emb(x) #返回x的词向量映射结果
#位置编码#定义一个PositionalEncoding类,继承自nn.Module
class PositionalEncoding(nn.Module):
#初始化函数,输入词向量维度、DropOut率和最大序列长度
def __init__(self, d_model, DropOut=0.1, max_len=5000):
super(PositionalEncoding, self).__init__() #调用父类的初始化函数
#定义一个nn.DropOut对象,用于DropOut操作
self.DropOut = nn.DropOut(p=DropOut)
#初始化一个全零矩阵,用于位置编码
pe = torch.zeros(max_len, d_model)
#创建一个从0到max_len-1的位置向量
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
x = x + self.pe[:x.size(0), :] #将位置编码添加到x上
return self.DropOut(x) #应用DropOut操作并返回结果
#获取attention的pad mask,输入query序列和key序列pad mask
def get_attn_pad_mask(seq_q, seq_k):
batch_size, len_q = seq_q.size() #获取query序列的批次大小和序列长度
batch_size, len_k = seq_k.size() #获取key序列的批次大小和序列长度
#创建一个pad mask,1表示pad位置,0表示非pad位置
pad_attn_mask = seq_k.data.eq(0).unsqueeze(1)
#扩展pad mask到[batch_size, len_q, len_k]的形状
return pad_attn_mask.expand(batch_size, len_q, len_k)
#获取attention的subsequence mask,输入序列sequence mask
def get_attn_subsequence_mask(seq):
#获取attention的形状
attn_shape = [seq.size(0), seq.size(1), seq.size(1)]
#创建一个上三角矩阵,1表示可以attention,0表示不可以attention
subsequence_mask = np.triu(np.ones(attn_shape), k=1)
#将上三角矩阵转换为byte类型的tensor并返回
return torch.from_numpy(subsequence_mask).byte()
输入数据首先需要经过词嵌入与位置编码后,才可以传递给Transformer模型进行注意力机制的计算,这里并定义了pad_mask与sequence_mask矩阵的计算函数,方便计算编码器与解码器的掩码矩阵,然后搭建注意力机制计算代码,代码如下: