深度学习-自然语言处理(NLP)-Pytorch:Transformer模型(使用官方模块)构建【根据torch提供的模块搭建自己的Transformer模型】【使用BERT对编码器端参数进行初始化】

在这里插入图片描述

一、Transformer模型各组件构成

1、Embedding Layer(文本嵌入层)

实例化官方"文本嵌入层"

self.embedding_fn = nn.Embedding(ntoken, ninp)  # 实例化官方"文本嵌入层"

2、Positional Encoding(位置编码器)

实例化自定义"位置编码器"

self.pos_encoder = PositionalEncoding(ninp, dropout)  # 实例化自定义"位置编码器"

3、编码器层

实例化官方"编码器层"

encoder_layers = TransformerEncoderLayer(ninp, nhead, nhid, dropout)  # 实例化官方"编码器层"

4、编码器

实例化官方"编码器"

self.transformer_encoder = TransformerEncoder(encoder_layers, nlayers)  # 实例化官方"编码器"

5、解码器

实例化官方"线性层"作为解码器

self.decoder = nn.Linear(ninp, ntoken)  # 实例化官方"线性层"作为解码器

二、根据torch.nn提供的模块搭建自己的Transformer模型01

Encoder部分使用Transformer模型的Encoder,Decoder部分使用自定义的Decoder

import math
import time
import copy
import torch
import torch.nn as nn
from torch.nn import TransformerEncoder, TransformerEncoderLayer
import torchtext  # 包含经典文本数据集以及相关工具包【它是torch工具中处理NLP问题的常用数据处理包】
from torchtext.data.utils import get_tokenizer  # torchtext中的数据处理工具, get_tokenizer用于英文分词

BATCH_SIZE = 10  # 每批次的句子数量
MAX_LEN = 35  # 每个句子的长度


class TransformerModel(nn.Module):
    def __init__(self, ntoken, ninp, nhead, nhid, nlayers, dropout=0.5):
        super(TransformerModel, self).__init__()
        self.model_type = 'Transformer'
        self.ninp = ninp
        self.src_mask = None
        # 实例化Transformer中的各个组件
        self.embedding_fn = nn.Embedding(num_embeddings=ntoken, embedding_dim=ninp)  # 实例化官方"文本嵌入层"
        self.pos_encoder = PositionalEncoding(ninp, dropout)  # 实例化官方"位置编码器"
        # d_model: the number of expected features in the input (required)
        # nhead: the number of heads in the multiheadattention models (required)
        # dim_feedforward: the dimension of the feedforward network model (default=2048).
        encoder_layers = TransformerEncoderLayer(d_model=ninp, nhead=nhead, dim_feedforward=nhid, dropout=dropout)  # 实例化官方"编码器层"
        self.encoder = TransformerEncoder(encoder_layer=encoder_layers, num_layers=nlayers)  # 实例化官方"编码器"
        self.decoder = nn.Linear(ninp, ntoken)  # 实例化官方"线性层"作为解码器
        self.init_weights()

    def _generate_square_subsequent_mask(self, sz):
        mask = (torch.triu(torch.ones(sz, sz)) == 1).transpose(0, 1)
        mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))
        return mask

    def init_weights(self):
        initrange = 0.1
        self.embedding_fn.weight.data.uniform_(-initrange, initrange)
        self.decoder.weight.data.uniform_(-initrange, initrange)
        self.decoder.bias.data.zero_()

    def forward(self, src):
        if self.src_mask is None or self.src_mask.size(0) != len(src):
            device = src.device
            mask = self._generate_square_subsequent_mask(len(src)).to(device)
            self.src_mask = mask

        src = self.embedding_fn(src) * math.sqrt(self.ninp)
        src = self.pos_encoder(src)
        output = self.encoder(src, self.src_mask)
        output = self.decoder(output)
        return output



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 = x + self.pe[:x.size(0), :]
        return self.dropout(x)




# 第一步: 导入wikiText-2数据集并作基本处理【wikiText-2是Wikitext-103 的子集,主要用于测试小型数据集的语言模型训练效果】
# 创建语料域, 语料域是存放语料的数据结构【最终获得一个Field对象<torchtext.data.field.Field object at 0x7fc42a02e7f0>】
# 它的四个参数代表给存放语料(或称作文本)施加的作用【tokenize: 使用get_tokenizer("basic_english")获得一个分割器对象,分割方式按照文本为基础英文进行分割; init_token: 给文本施加的起始符 <sos>; eos_token: 给文本施加的终止符<eos>;lower为True,代表存放的文本字母全部转为小写.
Field = torchtext.legacy.data.Field(tokenize=get_tokenizer("basic_english"), init_token='<sos>', eos_token='<eos>', lower=True)
train_txt, val_txt, test_txt = torchtext.legacy.datasets.WikiText2.splits(Field)  # 使用 torchtext 的数据集方法导入WikiText2数据,并切分为对应训练文本, 验证文本,测试文本, 并对这些文本施加刚刚创建的语料域.
print("len(train_txt.examples[0].text) = {0}----train_txt.examples[0].text[:10] = {1}".format(len(train_txt.examples[0].text), train_txt.examples[0].text[:10]))  # 通过examples[0].text取出文本对象进行查看【所有单词的列表】
print("len(val_txt.examples[0].text) = {0}----val_txt.examples[0].text[-10:] = {1}".format(len(val_txt.examples[0].text), val_txt.examples[0].text[-10:]))  # 通过examples[0].text取出文本对象进行查看【所有单词的列表】
print("len(test_txt.examples[0].text) = {0}----test_txt.examples[0].text[:10] = {1}".format(len(test_txt.examples[0].text), test_txt.examples[0].text[:10]))  # 通过examples[0].text取出文本对象进行查看【所有单词的列表】
Field.build_vocab(train_txt)  # 将训练集文本数据构建一个vocab对象【这样可以使用vocab对象的stoi方法统计文本共包含的不重复词汇总数】
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")  # 然后选择设备cuda或者cpu  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")    device = torch.device("cpu")

# 第二步: 构建用于模型输入的批次化数据
batch_size = BATCH_SIZE  # 训练/测试数据的 batch_size【每次训练的句子数量】
max_len = MAX_LEN  # 令子长度允许的最大值max_len为 MAX_LEN


def batchify(data, batch_size):  # batchify函数用于将文本数据映射成连续数字张量, 并转换成指定的样式【data: 文本数据(train_txt, val_txt, test_txt), batch_size: 每次模型更新参数的数据量】
    data = Field.numericalize([data.examples[0].text])  # 使用 Field 的 numericalize方法将单词映射成对应的连续数字.
    print("batchify---->data.shape = {0}".format(data.shape))
    nbatch = data.size(0) // batch_size  # 接着用数据词汇总数除以 batch_size,取整数得到一个nbatch代表需要多少次batch后能够遍历完所有数据
    data = data.narrow(0, 0, nbatch * batch_size)  # 使用narrow方法(参数1:进行裁剪的维度;参数2/3:裁剪的起始结束位置)对不规整的剩余数据进行删除,最后不能形成 batch_size 个的一组数据被删除【第一个参数是代表横轴删除还是纵轴删除, 0为横轴,1为纵轴; 第二个和第三个参数代表保留开始轴到结束轴的数值.类似于切片】
    data = data.view(batch_size, -1).t().contiguous()  # 使用view方法对data进行矩阵变换,矩阵经过transpose操作后一定要使用.contiguous()来保证后续可以继续进行view/reshape操作【最终形状:行数代表nbatch; 列数代表batch_size】 data = data.view(-1, batch_size)
    return data.to(device)  # 最后将数据分配在指定的设备上


train_data = batchify(train_txt, batch_size)  # torch.Size([208670, 10])
print("batchify---->train_data.shape = {0}----train_data[:2,:] = \n{1}".format(train_data.shape, train_data[:2, :]))  # torch.Size([208670, 10])
val_data = batchify(val_txt, batch_size)  # torch.Size([21817, 10])
print("batchify---->val_data.shape = {0}----val_data[:2,:] = \n{1}".format(val_data.shape, val_data[:2, :]))
test_data = batchify(test_txt, batch_size)  # torch.Size([24621, 10])
print("batchify---->test_data.shape = {0}----val_data[:2,:] = \n{1}".format(test_data.shape, val_data[:2, :]))


# 用于获得每个批次合理大小的源数据和目标数据【source_data:通过batchify得到的train_data/val_data/test_data; batch_index: 是具体的批次次数(0~nbatch)】
def get_data_of_this_batch(source_data, batch_index):
    nbatch = source_data.size(0)
    sentence_len = min(max_len, nbatch - 1 - batch_index)  # 首先我们确定句子长度, 它将是在 max_len和 len(source) - 1 - batch_index 中最小值【前面的批次中都会是max_len的值, 只不过最后一个批次中, 句子长度可能不够 max_len, 因此会变为 len(source) - 1 - batch_index 的值】
    source_data_of_this_batch = source_data[batch_index:batch_index + sentence_len]  # 语言模型训练的源数据的第 batch_index 批数据【是 batchify 的结果source的切片source[batch_index:batch_index+seq_len]】
    target_data_of_this_batch = source_data[batch_index + 1:batch_index + 1 + sentence_len].view(-1)  # 根据语言模型训练的语料规定, 它的目标数据是源数据向后移动一位【因为最后目标数据的切片会越界, 因此使用view(-1)来保证形状正常】
    return source_data_of_this_batch, target_data_of_this_batch  # 形状分别为:torch.Size([35, 10])、torch.Size([350])


print("get_data_of_this_batch测试---->source_data_of_this_batch.shape = get_data_of_this_batch(test_data, 1)[0].shape = {0}||||target_data_of_this_batch.shape = get_data_of_this_batch(test_data, 1)[1].shape = {1}".format(get_data_of_this_batch(test_data, 1)[0].shape, get_data_of_this_batch(test_data, 1)[1].shape))
print("get_data_of_this_batch测试---->source_data_of_this_batch.shape = get_data_of_this_batch(test_data, 24603)[0].shape = {0}||||target_data_of_this_batch.shape = get_data_of_this_batch(test_data, 24603)[1].shape = {1}".format(get_data_of_this_batch(test_data, 24603)[0].shape, get_data_of_this_batch(test_data, 24603)[1].shape))

# 第三步: 构建训练函数、评估函数
ntokens = len(Field.vocab.stoi)  # 通过TEXT.vocab.stoi方法获得不重复词汇总数
embedding_size = 200  # 词嵌入大小为200
ff_middle_dim = 200  # 前馈全连接层的节点数
nlayers = 2  # 编码器层的数量
nhead = 2  # 多头注意力机制的头数
dropout = 0.2  # 置0比率
lr = 5.0  # 学习率初始值定为5.0
# 实例化Tranformer模型
model = TransformerModel(ntoken=ntokens, ninp=embedding_size, nhead=nhead, nhid=ff_middle_dim, nlayers=nlayers, dropout=dropout).to(device)  # 将参数输入到TransformerModel中
# 实例化损失函数
criterion = nn.CrossEntropyLoss()  # 关于损失函数, 我们使用nn自带的交叉熵损失
# 实例化梯度下降优化器
optimizer = torch.optim.SGD(model.parameters(), lr=lr)  # 优化器选择torch自带的SGD随机梯度下降方法, 并把lr传入其中
# 实例化学习率调整器
scheduler = torch.optim.lr_scheduler.StepLR(optimizer=optimizer, step_size=1.0, gamma=0.95)  # 定义学习率调整方法, 使用torch自带的lr_scheduler, 将优化器传入其中【step_size:表示每几个epoch更新一次学习率; gamma:表示学习率衰减速率】


# 训练函数
def train(epoch):
    model.train()  # 模型开启训练模式
    total_loss = 0.  # 定义初始损失为0
    start_time = time.time()  # 获得当前时间
    for batch, batch_index in enumerate(range(0, train_data.size(0) - 1, max_len)):  # 开始遍历批次数据
        source_data_of_this_batch, target_data_of_this_batch = get_data_of_this_batch(train_data, batch_index)  # 通过get_batch获得源数据和目标数据
        optimizer.zero_grad()  # 设置优化器初始梯度为0梯度
        output = model(source_data_of_this_batch)  # 将数据装入model得到输出
        loss = criterion(output.view(-1, ntokens), target_data_of_this_batch)  # 将输出和目标数据传入损失函数对象
        loss.backward()  # 损失进行反向传播以获得总的损失
        torch.nn.utils.clip_grad_norm_(model.parameters(), 0.5)  # 使用nn自带的clip_grad_norm_方法进行梯度规范化, 防止出现梯度消失或爆炸
        optimizer.step()  # 模型参数进行更新
        total_loss += loss.item()  # 将每层的损失相加获得总的损失
        log_interval = 200  # 日志打印间隔定为200
        if batch % log_interval == 0 and batch > 0:  # 如果batch是200的倍数且大于0,则打印相关日志
            cur_loss = total_loss / log_interval  # 平均损失为总损失除以log_interval
            elapsed = time.time() - start_time  # 需要的时间为当前时间减去开始时间
            # 打印轮数, 当前批次和总批次, 当前学习率, 训练速度(每豪秒处理多少批次),平均损失, 以及困惑度【困惑度是衡量语言模型的重要指标, 它的计算方法就是对交叉熵平均损失取自然对数的底数】
            print('| epoch {:3d} | {:5d}/{:5d} nbatches | lr {:02.2f} | ms/batch {:5.2f} | loss {:5.2f} | ppl {:8.2f}'.format(epoch, batch, len(train_data) // max_len, scheduler.get_lr()[0], elapsed * 1000 / log_interval, cur_loss, math.exp(cur_loss)))
            total_loss = 0  # 每个批次结束后, 总损失归0
            start_time = time.time()  # 开始时间取当前时间


# 评估函数
def evaluate(model, data_source):  # 评估阶段包括验证和测试【model:为每轮训练产生的模型, data_source:代表验证或测试数据集】
    model.eval()  # 模型开启评估模式
    total_loss = 0  # 总损失归0
    with torch.no_grad():  # 因为评估模式模型参数不变, 因此反向传播不需要求导, 以加快计算
        for i in range(0, data_source.size(0) - 1, max_len):  # 与训练过程相同, 但是因为过程不需要打印信息, 因此不需要batch数
            data, targets = get_data_of_this_batch(data_source, i)  # 首先还是通过通过get_data_of_this_batch获得验证数据集的源数据和目标数据
            output = model(data)  # 通过eval_model获得输出
            output_flat = output.view(-1, ntokens)  # 对输出形状扁平化, 变为全部词汇的概率分布
            total_loss += criterion(output_flat, targets).item()  # 获得评估过程的总损失
            cur_loss = total_loss / ((data_source.size(0) - 1) / max_len)  # 计算平均损失
    return cur_loss  # 返回平均损失


# 第四步: 进行训练和评估
best_val_loss = float("inf")  # 首先初始化最佳验证损失,初始值为无穷大
epochs = 2  # 定义训练轮数
best_model = None  # 定义最佳模型变量, 初始值为None
for epoch in range(1, epochs + 1):  # 使用for循环遍历轮数
    print("=" * 100, "epoch={0}".format(epoch), "=" * 100)
    epoch_start_time = time.time()  # 首先获得轮数开始时间
    print("-" * 50, "epoch={0}:开始训练".format(epoch), "-" * 50)
    train(epoch)  # 调用训练函数
    print("-" * 50, "epoch={0}:开始评估".format(epoch), "-" * 50)
    val_loss = evaluate(model, val_data)  # 该轮训练后我们的模型参数已经发生了变化【将模型和评估数据传入到评估函数中】
    print('=' * 100)
    print('| end of epoch {:3d} | time: {:5.2f}s | valid loss {:5.2f} | valid ppl {:8.2f}'.format(epoch, (time.time() - epoch_start_time), val_loss, math.exp(val_loss)))  # 打印每轮的评估日志,分别有轮数,耗时,验证损失以及验证困惑度
    print('=' * 100)
    # 比较哪一轮损失最小,赋值给best_val_loss,并取该损失下的模型为best_model
    if val_loss < best_val_loss:
        best_val_loss = val_loss
        best_model = copy.deepcopy(model)  # 使用深拷贝,拷贝最优模型
    scheduler.step()  # 每轮都会对优化方法的学习率做调整

# 第五步:最优模型测试
test_loss = evaluate(best_model, test_data)  # 我们仍然使用evaluate函数,这次它的参数是best_model以及测试数据
print('=' * 100)
print('| End of testing | test loss {:5.2f} | test ppl {:8.2f}'.format(test_loss, math.exp(test_loss)))  # 打印测试日志,包括测试损失和测试困惑度
print('=' * 100)

打印结果:

len(train_txt.examples[0].text) = 2086708----train_txt.examples[0].text[:10] = ['<eos>', '=', 'valkyria', 'chronicles', 'iii', '=', '<eos>', '<eos>', 'senjō', 'no']

len(val_txt.examples[0].text) = 218177----val_txt.examples[0].text[-10:] = ['=', '=', 'television', 'roles', '=', '=', '=', '<eos>', '<eos>', '<eos>']

len(test_txt.examples[0].text) = 246217----test_txt.examples[0].text[:10] = ['<eos>', '=', 'robert', '<unk>', '=', '<eos>', '<eos>', 'robert', '<unk>', 'is']

batchify---->data.shape = torch.Size([2086708, 1])

batchify---->train_data.shape = torch.Size([208670, 10])----train_data[:2,:] = 
tensor([[    3,  1849,     7,     5,     4,     0,     6,     4,     6,    65],
        [   12,    13,   458,  1045, 19094,   147,     0,  2280,    58,  2438]],
       device='cuda:0')

batchify---->data.shape = torch.Size([218177, 1])

batchify---->val_data.shape = torch.Size([21817, 10])----val_data[:2,:] = 
tensor([[    3,     5,   481,     4,  1358,  2903,     4,     8,     9,     3],
        [   12,    82,     9,  8885,     6,    10, 21738,   780,  2156,     4]],
       device='cuda:0')

batchify---->data.shape = torch.Size([246217, 1])

batchify---->test_data.shape = torch.Size([24621, 10])----val_data[:2,:] = 
tensor([[    3,     5,   481,     4,  1358,  2903,     4,     8,     9,     3],
        [   12,    82,     9,  8885,     6,    10, 21738,   780,  2156,     4]],
       device='cuda:0')

get_data_of_this_batch测试---->source_data_of_this_batch.shape = get_data_of_this_batch(test_data, 1)[0].shape = torch.Size([35, 10])||||target_data_of_this_batch.shape = get_data_of_this_batch(test_data, 1)[1].shape = torch.Size([350])

get_data_of_this_batch测试---->source_data_of_this_batch.shape = get_data_of_this_batch(test_data, 24603)[0].shape = torch.Size([17, 10])||||target_data_of_this_batch.shape = get_data_of_this_batch(test_data, 24603)[1].shape = torch.Size([170])
==================================================================================================== epoch=1 ====================================================================================================
-------------------------------------------------- epoch=1:开始训练 --------------------------------------------------
C:\Program_Files_AI\Anaconda3531\lib\site-packages\torch\optim\lr_scheduler.py:370: UserWarning: To get the last learning rate computed by the scheduler, please use `get_last_lr()`.
| epoch   1 |   200/ 5962 nbatches | lr 5.00 | ms/batch 18.45 | loss  8.05 | ppl  3130.39
  "please use `get_last_lr()`.", UserWarning)
| epoch   1 |   400/ 5962 nbatches | lr 5.00 | ms/batch 16.41 | loss  6.77 | ppl   874.83
| epoch   1 |   600/ 5962 nbatches | lr 5.00 | ms/batch 16.38 | loss  6.38 | ppl   592.72
| epoch   1 |   800/ 5962 nbatches | lr 5.00 | ms/batch 17.71 | loss  6.32 | ppl   554.65
| epoch   1 |  1000/ 5962 nbatches | lr 5.00 | ms/batch 16.52 | loss  6.21 | ppl   499.60
| epoch   1 |  1200/ 5962 nbatches | lr 5.00 | ms/batch 16.59 | loss  6.18 | ppl   483.96
| epoch   1 |  1400/ 5962 nbatches | lr 5.00 | ms/batch 16.53 | loss  6.16 | ppl   473.69
| epoch   1 |  1600/ 5962 nbatches | lr 5.00 | ms/batch 16.36 | loss  6.16 | ppl   471.81
| epoch   1 |  1800/ 5962 nbatches | lr 5.00 | ms/batch 16.54 | loss  6.11 | ppl   450.17
| epoch   1 |  2000/ 5962 nbatches | lr 5.00 | ms/batch 16.51 | loss  6.13 | ppl   460.26
| epoch   1 |  2200/ 5962 nbatches | lr 5.00 | ms/batch 16.80 | loss  6.06 | ppl   430.46
| epoch   1 |  2400/ 5962 nbatches | lr 5.00 | ms/batch 17.00 | loss  6.02 | ppl   412.08
| epoch   1 |  2600/ 5962 nbatches | lr 5.00 | ms/batch 16.94 | loss  6.06 | ppl   427.63
| epoch   1 |  2800/ 5962 nbatches | lr 5.00 | ms/batch 16.81 | loss  6.01 | ppl   405.53
| epoch   1 |  3000/ 5962 nbatches | lr 5.00 | ms/batch 16.86 | loss  5.97 | ppl   393.41
| epoch   1 |  3200/ 5962 nbatches | lr 5.00 | ms/batch 16.63 | loss  5.99 | ppl   400.89
| epoch   1 |  3400/ 5962 nbatches | lr 5.00 | ms/batch 16.67 | loss  6.05 | ppl   424.81
| epoch   1 |  3600/ 5962 nbatches | lr 5.00 | ms/batch 17.17 | loss  5.97 | ppl   390.73
| epoch   1 |  3800/ 5962 nbatches | lr 5.00 | ms/batch 16.83 | loss  5.96 | ppl   385.97
| epoch   1 |  4000/ 5962 nbatches | lr 5.00 | ms/batch 17.27 | loss  5.92 | ppl   372.53
| epoch   1 |  4200/ 5962 nbatches | lr 5.00 | ms/batch 16.95 | loss  6.01 | ppl   408.44
| epoch   1 |  4400/ 5962 nbatches | lr 5.00 | ms/batch 16.53 | loss  5.99 | ppl   399.39
| epoch   1 |  4600/ 5962 nbatches | lr 5.00 | ms/batch 17.31 | loss  6.04 | ppl   420.58
| epoch   1 |  4800/ 5962 nbatches | lr 5.00 | ms/batch 16.70 | loss  5.94 | ppl   381.56
| epoch   1 |  5000/ 5962 nbatches | lr 5.00 | ms/batch 16.97 | loss  5.97 | ppl   393.05
| epoch   1 |  5200/ 5962 nbatches | lr 5.00 | ms/batch 16.86 | loss  5.86 | ppl   350.92
| epoch   1 |  5400/ 5962 nbatches | lr 5.00 | ms/batch 17.08 | loss  5.99 | ppl   400.30
| epoch   1 |  5600/ 5962 nbatches | lr 5.00 | ms/batch 17.28 | loss  5.98 | ppl   394.06
| epoch   1 |  5800/ 5962 nbatches | lr 5.00 | ms/batch 16.86 | loss  5.89 | ppl   359.91
-------------------------------------------------- epoch=1:开始评估 --------------------------------------------------
====================================================================================================
| end of epoch   1 | time: 103.76s | valid loss  5.84 | valid ppl   344.20
====================================================================================================
==================================================================================================== epoch=2 ====================================================================================================
-------------------------------------------------- epoch=2:开始训练 --------------------------------------------------
| epoch   2 |   200/ 5962 nbatches | lr 4.51 | ms/batch 17.43 | loss  5.93 | ppl   377.39
| epoch   2 |   400/ 5962 nbatches | lr 4.51 | ms/batch 17.62 | loss  5.86 | ppl   350.71
| epoch   2 |   600/ 5962 nbatches | lr 4.51 | ms/batch 17.09 | loss  5.70 | ppl   300.16
| epoch   2 |   800/ 5962 nbatches | lr 4.51 | ms/batch 17.16 | loss  5.80 | ppl   330.66
| epoch   2 |  1000/ 5962 nbatches | lr 4.51 | ms/batch 17.32 | loss  5.78 | ppl   324.58
| epoch   2 |  1200/ 5962 nbatches | lr 4.51 | ms/batch 17.59 | loss  5.79 | ppl   326.33
| epoch   2 |  1400/ 5962 nbatches | lr 4.51 | ms/batch 17.16 | loss  5.82 | ppl   335.88
| epoch   2 |  1600/ 5962 nbatches | lr 4.51 | ms/batch 18.03 | loss  5.85 | ppl   345.64
| epoch   2 |  1800/ 5962 nbatches | lr 4.51 | ms/batch 17.47 | loss  5.82 | ppl   335.38
| epoch   2 |  2000/ 5962 nbatches | lr 4.51 | ms/batch 17.41 | loss  5.86 | ppl   350.73
| epoch   2 |  2200/ 5962 nbatches | lr 4.51 | ms/batch 17.53 | loss  5.79 | ppl   326.66
| epoch   2 |  2400/ 5962 nbatches | lr 4.51 | ms/batch 17.88 | loss  5.77 | ppl   321.60
| epoch   2 |  2600/ 5962 nbatches | lr 4.51 | ms/batch 17.70 | loss  5.79 | ppl   327.80
| epoch   2 |  2800/ 5962 nbatches | lr 4.51 | ms/batch 17.67 | loss  5.77 | ppl   319.31
| epoch   2 |  3000/ 5962 nbatches | lr 4.51 | ms/batch 18.16 | loss  5.74 | ppl   312.56
| epoch   2 |  3200/ 5962 nbatches | lr 4.51 | ms/batch 17.60 | loss  5.77 | ppl   318.99
| epoch   2 |  3400/ 5962 nbatches | lr 4.51 | ms/batch 17.70 | loss  5.85 | ppl   346.36
| epoch   2 |  3600/ 5962 nbatches | lr 4.51 | ms/batch 17.84 | loss  5.73 | ppl   308.12
| epoch   2 |  3800/ 5962 nbatches | lr 4.51 | ms/batch 17.70 | loss  5.75 | ppl   312.82
| epoch   2 |  4000/ 5962 nbatches | lr 4.51 | ms/batch 17.52 | loss  5.68 | ppl   292.11
| epoch   2 |  4200/ 5962 nbatches | lr 4.51 | ms/batch 17.61 | loss  5.79 | ppl   326.95
| epoch   2 |  4400/ 5962 nbatches | lr 4.51 | ms/batch 18.21 | loss  5.82 | ppl   336.27
| epoch   2 |  4600/ 5962 nbatches | lr 4.51 | ms/batch 18.23 | loss  5.81 | ppl   335.29
| epoch   2 |  4800/ 5962 nbatches | lr 4.51 | ms/batch 17.64 | loss  5.73 | ppl   307.74
| epoch   2 |  5000/ 5962 nbatches | lr 4.51 | ms/batch 17.79 | loss  5.75 | ppl   315.08
| epoch   2 |  5200/ 5962 nbatches | lr 4.51 | ms/batch 17.74 | loss  5.64 | ppl   280.63
| epoch   2 |  5400/ 5962 nbatches | lr 4.51 | ms/batch 17.80 | loss  5.78 | ppl   323.40
| epoch   2 |  5600/ 5962 nbatches | lr 4.51 | ms/batch 17.96 | loss  5.78 | ppl   323.46
| epoch   2 |  5800/ 5962 nbatches | lr 4.51 | ms/batch 18.06 | loss  5.68 | ppl   294.41
-------------------------------------------------- epoch=2:开始评估 --------------------------------------------------
====================================================================================================
| end of epoch   2 | time: 108.56s | valid loss  5.76 | valid ppl   317.85
====================================================================================================
====================================================================================================
| End of testing | test loss  5.69 | test ppl   296.01
====================================================================================================

三、根据torch.nn提供的模块搭建自己的Transformer模型02

Encoder部分使用Transformer模型的Encoder,Decoder部分使用Transformer模型的Decoder

import math
import time
import copy
import torch
import torch.nn as nn
from torch.nn import TransformerEncoder, TransformerEncoderLayer, TransformerDecoder, TransformerDecoderLayer
import torchtext  # 包含经典文本数据集以及相关工具包【它是torch工具中处理NLP问题的常用数据处理包】
from torchtext.data.utils import get_tokenizer  # torchtext中的数据处理工具, get_tokenizer用于英文分词

BATCH_SIZE = 10  # 每批次的句子数量
MAX_LEN = 35  # 每个句子的长度


class TransformerModel(nn.Module):
    def __init__(self, ntoken, ninp, nhead, nhid, nlayers, dropout=0.5):
        super(TransformerModel, self).__init__()
        self.model_type = 'Transformer'
        self.ninp = ninp
        self.src_mask = None
        # 实例化Transformer中的各个组件
        self.embedding_fn = nn.Embedding(num_embeddings=ntoken, embedding_dim=ninp)  # 实例化官方"文本嵌入层"
        self.pos_encoder = PositionalEncoding(ninp, dropout)  # 实例化官方"位置编码器"
        # d_model: the number of expected features in the input (required)
        # nhead: the number of heads in the multiheadattention models (required)
        # dim_feedforward: the dimension of the feedforward network model (default=2048).
        encoder_layers = TransformerEncoderLayer(d_model=ninp, nhead=nhead, dim_feedforward=nhid, dropout=dropout)  # 实例化官方"编码器层"
        self.encoder = TransformerEncoder(encoder_layer=encoder_layers, num_layers=nlayers)  # 实例化官方"编码器"
        decoder_layers = TransformerDecoderLayer(d_model=ninp, nhead=nhead, dim_feedforward=nhid, dropout=dropout)  # 实例化官方"解码器层"
        self.decoder = TransformerDecoder(decoder_layer=decoder_layers, num_layers=nlayers)
        self.init_weights()

    def _generate_square_subsequent_mask(self, sz):
        mask = (torch.triu(torch.ones(sz, sz)) == 1).transpose(0, 1)
        mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))
        return mask

    def init_weights(self):
        initrange = 0.1
        self.embedding_fn.weight.data.uniform_(-initrange, initrange)
        # self.decoder.bias.data.zero_()
        # self.decoder.weight.data.uniform_(-initrange, initrange)

    def forward(self, src):
        if self.src_mask is None or self.src_mask.size(0) != len(src):
            device = src.device
            mask = self._generate_square_subsequent_mask(len(src)).to(device)
            self.src_mask = mask

        src = self.embedding_fn(src) * math.sqrt(self.ninp)
        src = self.pos_encoder(src)
        output = self.encoder(src, self.src_mask)
        output = self.decoder(output)
        return output



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 = x + self.pe[:x.size(0), :]
        return self.dropout(x)


# 第一步: 导入wikiText-2数据集并作基本处理【wikiText-2是Wikitext-103 的子集,主要用于测试小型数据集的语言模型训练效果】
# 创建语料域, 语料域是存放语料的数据结构【最终获得一个Field对象<torchtext.data.field.Field object at 0x7fc42a02e7f0>】
# 它的四个参数代表给存放语料(或称作文本)施加的作用【tokenize: 使用get_tokenizer("basic_english")获得一个分割器对象,分割方式按照文本为基础英文进行分割; init_token: 给文本施加的起始符 <sos>; eos_token: 给文本施加的终止符<eos>;lower为True,代表存放的文本字母全部转为小写.
Field = torchtext.legacy.data.Field(tokenize=get_tokenizer("basic_english"), init_token='<sos>', eos_token='<eos>', lower=True)
train_txt, val_txt, test_txt = torchtext.legacy.datasets.WikiText2.splits(Field)  # 使用 torchtext 的数据集方法导入WikiText2数据,并切分为对应训练文本, 验证文本,测试文本, 并对这些文本施加刚刚创建的语料域.
print("len(train_txt.examples[0].text) = {0}----train_txt.examples[0].text[:10] = {1}".format(len(train_txt.examples[0].text), train_txt.examples[0].text[:10]))  # 通过examples[0].text取出文本对象进行查看【所有单词的列表】
print("len(val_txt.examples[0].text) = {0}----val_txt.examples[0].text[-10:] = {1}".format(len(val_txt.examples[0].text), val_txt.examples[0].text[-10:]))  # 通过examples[0].text取出文本对象进行查看【所有单词的列表】
print("len(test_txt.examples[0].text) = {0}----test_txt.examples[0].text[:10] = {1}".format(len(test_txt.examples[0].text), test_txt.examples[0].text[:10]))  # 通过examples[0].text取出文本对象进行查看【所有单词的列表】
Field.build_vocab(train_txt)  # 将训练集文本数据构建一个vocab对象【这样可以使用vocab对象的stoi方法统计文本共包含的不重复词汇总数】
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")  # 然后选择设备cuda或者cpu  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")    device = torch.device("cpu")

# 第二步: 构建用于模型输入的批次化数据
batch_size = BATCH_SIZE  # 训练/测试数据的 batch_size【每次训练的句子数量】
max_len = MAX_LEN  # 令子长度允许的最大值max_len为 MAX_LEN


def batchify(data, batch_size):  # batchify函数用于将文本数据映射成连续数字张量, 并转换成指定的样式【data: 文本数据(train_txt, val_txt, test_txt), batch_size: 每次模型更新参数的数据量】
    data = Field.numericalize([data.examples[0].text])  # 使用 Field 的 numericalize方法将单词映射成对应的连续数字.
    print("batchify---->data.shape = {0}".format(data.shape))
    nbatch = data.size(0) // batch_size  # 接着用数据词汇总数除以 batch_size,取整数得到一个nbatch代表需要多少次batch后能够遍历完所有数据
    data = data.narrow(0, 0, nbatch * batch_size)  # 使用narrow方法(参数1:进行裁剪的维度;参数2/3:裁剪的起始结束位置)对不规整的剩余数据进行删除,最后不能形成 batch_size 个的一组数据被删除【第一个参数是代表横轴删除还是纵轴删除, 0为横轴,1为纵轴; 第二个和第三个参数代表保留开始轴到结束轴的数值.类似于切片】
    data = data.view(batch_size, -1).t().contiguous()  # 使用view方法对data进行矩阵变换,矩阵经过transpose操作后一定要使用.contiguous()来保证后续可以继续进行view/reshape操作【最终形状:行数代表nbatch; 列数代表batch_size】 data = data.view(-1, batch_size)
    return data.to(device)  # 最后将数据分配在指定的设备上


train_data = batchify(train_txt, batch_size)  # torch.Size([208670, 10])
print("batchify---->train_data.shape = {0}----train_data[:2,:] = \n{1}".format(train_data.shape, train_data[:2, :]))  # torch.Size([208670, 10])
val_data = batchify(val_txt, batch_size)  # torch.Size([21817, 10])
print("batchify---->val_data.shape = {0}----val_data[:2,:] = \n{1}".format(val_data.shape, val_data[:2, :]))
test_data = batchify(test_txt, batch_size)  # torch.Size([24621, 10])
print("batchify---->test_data.shape = {0}----val_data[:2,:] = \n{1}".format(test_data.shape, val_data[:2, :]))


# 用于获得每个批次合理大小的源数据和目标数据【source_data:通过batchify得到的train_data/val_data/test_data; batch_index: 是具体的批次次数(0~nbatch)】
def get_data_of_this_batch(source_data, batch_index):
    nbatch = source_data.size(0)
    sentence_len = min(max_len, nbatch - 1 - batch_index)  # 首先我们确定句子长度, 它将是在 max_len和 len(source) - 1 - batch_index 中最小值【前面的批次中都会是max_len的值, 只不过最后一个批次中, 句子长度可能不够 max_len, 因此会变为 len(source) - 1 - batch_index 的值】
    source_data_of_this_batch = source_data[batch_index:batch_index + sentence_len]  # 语言模型训练的源数据的第 batch_index 批数据【是 batchify 的结果source的切片source[batch_index:batch_index+seq_len]】
    target_data_of_this_batch = source_data[batch_index + 1:batch_index + 1 + sentence_len].view(-1)  # 根据语言模型训练的语料规定, 它的目标数据是源数据向后移动一位【因为最后目标数据的切片会越界, 因此使用view(-1)来保证形状正常】
    return source_data_of_this_batch, target_data_of_this_batch  # 形状分别为:torch.Size([35, 10])、torch.Size([350])


print("get_data_of_this_batch测试---->source_data_of_this_batch.shape = get_data_of_this_batch(test_data, 1)[0].shape = {0}||||target_data_of_this_batch.shape = get_data_of_this_batch(test_data, 1)[1].shape = {1}".format(get_data_of_this_batch(test_data, 1)[0].shape, get_data_of_this_batch(test_data, 1)[1].shape))
print("get_data_of_this_batch测试---->source_data_of_this_batch.shape = get_data_of_this_batch(test_data, 24603)[0].shape = {0}||||target_data_of_this_batch.shape = get_data_of_this_batch(test_data, 24603)[1].shape = {1}".format(get_data_of_this_batch(test_data, 24603)[0].shape, get_data_of_this_batch(test_data, 24603)[1].shape))

# 第三步: 构建训练函数、评估函数
ntokens = len(Field.vocab.stoi)  # 通过TEXT.vocab.stoi方法获得不重复词汇总数
embedding_size = 200  # 词嵌入大小为200
ff_middle_dim = 200  # 前馈全连接层的节点数
nlayers = 2  # 编码器层的数量
nhead = 2  # 多头注意力机制的头数
dropout = 0.2  # 置0比率
lr = 5.0  # 学习率初始值定为5.0
# 实例化Tranformer模型
model = TransformerModel(ntoken=ntokens, ninp=embedding_size, nhead=nhead, nhid=ff_middle_dim, nlayers=nlayers, dropout=dropout).to(device)  # 将参数输入到TransformerModel中
# 实例化损失函数
criterion = nn.CrossEntropyLoss()  # 关于损失函数, 我们使用nn自带的交叉熵损失
# 实例化梯度下降优化器
optimizer = torch.optim.SGD(model.parameters(), lr=lr)  # 优化器选择torch自带的SGD随机梯度下降方法, 并把lr传入其中
# 实例化学习率调整器
scheduler = torch.optim.lr_scheduler.StepLR(optimizer=optimizer, step_size=1.0, gamma=0.95)  # 定义学习率调整方法, 使用torch自带的lr_scheduler, 将优化器传入其中【step_size:表示每几个epoch更新一次学习率; gamma:表示学习率衰减速率】


# 训练函数
def train(epoch):
    model.train()  # 模型开启训练模式
    total_loss = 0.  # 定义初始损失为0
    start_time = time.time()  # 获得当前时间
    for batch, batch_index in enumerate(range(0, train_data.size(0) - 1, max_len)):  # 开始遍历批次数据
        source_data_of_this_batch, target_data_of_this_batch = get_data_of_this_batch(train_data, batch_index)  # 通过get_batch获得源数据和目标数据
        optimizer.zero_grad()  # 设置优化器初始梯度为0梯度
        output = model(source_data_of_this_batch)  # 将数据装入model得到输出
        loss = criterion(output.view(-1, ntokens), target_data_of_this_batch)  # 将输出和目标数据传入损失函数对象
        loss.backward()  # 损失进行反向传播以获得总的损失
        torch.nn.utils.clip_grad_norm_(model.parameters(), 0.5)  # 使用nn自带的clip_grad_norm_方法进行梯度规范化, 防止出现梯度消失或爆炸
        optimizer.step()  # 模型参数进行更新
        total_loss += loss.item()  # 将每层的损失相加获得总的损失
        log_interval = 200  # 日志打印间隔定为200
        if batch % log_interval == 0 and batch > 0:  # 如果batch是200的倍数且大于0,则打印相关日志
            cur_loss = total_loss / log_interval  # 平均损失为总损失除以log_interval
            elapsed = time.time() - start_time  # 需要的时间为当前时间减去开始时间
            # 打印轮数, 当前批次和总批次, 当前学习率, 训练速度(每豪秒处理多少批次),平均损失, 以及困惑度【困惑度是衡量语言模型的重要指标, 它的计算方法就是对交叉熵平均损失取自然对数的底数】
            print('| epoch {:3d} | {:5d}/{:5d} nbatches | lr {:02.2f} | ms/batch {:5.2f} | loss {:5.2f} | ppl {:8.2f}'.format(epoch, batch, len(train_data) // max_len, scheduler.get_lr()[0], elapsed * 1000 / log_interval, cur_loss, math.exp(cur_loss)))
            total_loss = 0  # 每个批次结束后, 总损失归0
            start_time = time.time()  # 开始时间取当前时间


# 评估函数
def evaluate(model, data_source):  # 评估阶段包括验证和测试【model:为每轮训练产生的模型, data_source:代表验证或测试数据集】
    model.eval()  # 模型开启评估模式
    total_loss = 0  # 总损失归0
    with torch.no_grad():  # 因为评估模式模型参数不变, 因此反向传播不需要求导, 以加快计算
        for i in range(0, data_source.size(0) - 1, max_len):  # 与训练过程相同, 但是因为过程不需要打印信息, 因此不需要batch数
            data, targets = get_data_of_this_batch(data_source, i)  # 首先还是通过通过get_data_of_this_batch获得验证数据集的源数据和目标数据
            output = model(data)  # 通过eval_model获得输出
            output_flat = output.view(-1, ntokens)  # 对输出形状扁平化, 变为全部词汇的概率分布
            total_loss += criterion(output_flat, targets).item()  # 获得评估过程的总损失
            cur_loss = total_loss / ((data_source.size(0) - 1) / max_len)  # 计算平均损失
    return cur_loss  # 返回平均损失


# 第四步: 进行训练和评估
best_val_loss = float("inf")  # 首先初始化最佳验证损失,初始值为无穷大
epochs = 2  # 定义训练轮数
best_model = None  # 定义最佳模型变量, 初始值为None
for epoch in range(1, epochs + 1):  # 使用for循环遍历轮数
    print("=" * 100, "epoch={0}".format(epoch), "=" * 100)
    epoch_start_time = time.time()  # 首先获得轮数开始时间
    print("-" * 50, "epoch={0}:开始训练".format(epoch), "-" * 50)
    train(epoch)  # 调用训练函数
    print("-" * 50, "epoch={0}:开始评估".format(epoch), "-" * 50)
    val_loss = evaluate(model, val_data)  # 该轮训练后我们的模型参数已经发生了变化【将模型和评估数据传入到评估函数中】
    print('=' * 100)
    print('| end of epoch {:3d} | time: {:5.2f}s | valid loss {:5.2f} | valid ppl {:8.2f}'.format(epoch, (time.time() - epoch_start_time), val_loss, math.exp(val_loss)))  # 打印每轮的评估日志,分别有轮数,耗时,验证损失以及验证困惑度
    print('=' * 100)
    # 比较哪一轮损失最小,赋值给best_val_loss,并取该损失下的模型为best_model
    if val_loss < best_val_loss:
        best_val_loss = val_loss
        best_model = copy.deepcopy(model)  # 使用深拷贝,拷贝最优模型
    scheduler.step()  # 每轮都会对优化方法的学习率做调整

# 第五步:最优模型测试
test_loss = evaluate(best_model, test_data)  # 我们仍然使用evaluate函数,这次它的参数是best_model以及测试数据
print('=' * 100)
print('| End of testing | test loss {:5.2f} | test ppl {:8.2f}'.format(test_loss, math.exp(test_loss)))  # 打印测试日志,包括测试损失和测试困惑度
print('=' * 100)

四、

运用 BERT 预训练模型对 Encoder 端的Transformer 参数进行初始化

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页