• NLP实战9:Transformer实战-单词预测


    目录

    一、定义模型

    二、加载数据集

    三、初始化实例

    四、训练模型

    五、评估模型


    🍨 本文为[🔗365天深度学习训练营]内部限免文章(版权归 *K同学啊* 所有)
    🍖 作者:[K同学啊]

    模型结构图:

     📌 本周任务:
    ●理解文中代码逻辑并成功运行
    ●自定义输入一段英文文本进行预测(拓展内容,可自由发挥)

    数据集介绍:

    这是一个关于使用 Transformer 模型来预测文本序列中下一个单词的教程示例。

    本文使用的是Wikitext-2数据集,WikiText 英语词库数据(The WikiText Long Term Dependency Language Modeling Dataset)是一个包含1亿个词汇的英文词库数据,这些词汇是从Wikipedia的优质文章和标杆文章中提取得到,包括WikiText-2和WikiText-103两个版本,相比于著名的 Penn Treebank (PTB) 词库中的词汇数量,前者是其2倍,后者是其110倍。每个词汇还同时保留产生该词汇的原始文章,这尤其适合当需要长时依赖(longterm dependency)自然语言建模的场景。

    以下是关于Wikitext-2数据集的一些详细介绍:
    1数据来源:Wikitext-2数据集是从维基百科抽取的,包含了维基百科中的文章文本。
    2数据内容:Wikitext-2数据集包含维基百科的文章内容,包括各种主题和领域的信息。这些文章是经过预处理和清洗的,以提供干净和可用于训练的文本数据。
    3数据规模:Wikitext-2数据集的规模相对较小。它包含了超过2,088,628个词标记(token)的文本,以及其中1,915,997个词标记用于训练,172,430个词标记用于验证和186,716个词标记用于测试。
    4数据格式:Wikitext-2数据集以纯文本形式进行存储,每个文本文件包含一个维基百科文章的内容。文本以段落和句子为单位进行分割。
    5用途:Wikitext-2数据集通常用于语言建模任务,其中模型的目标是根据之前的上下文来预测下一个词或下一个句子。此外,该数据集也可以用于其他文本生成任务,如机器翻译、摘要生成等。

    一、定义模型

    1. from tempfile import TemporaryDirectory
    2. from typing import Tuple
    3. from torch import nn, Tensor
    4. from torch.nn import TransformerEncoder, TransformerEncoderLayer
    5. from torch.utils.data import dataset
    6. import math,os,torch
    7. device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    8. print(device)

    cuda

    1. class TransformerModel(nn.Module):
    2. def __init__(self, ntoken: int, d_model: int, nhead: int, d_hid: int,
    3. nlayers: int, dropout: float = 0.5):
    4. super().__init__()
    5. self.model_type = 'Transformer'
    6. self.pos_encoder = PositionalEncoding(d_model, dropout)
    7. # 定义编码器层
    8. encoder_layers = TransformerEncoderLayer(d_model, nhead, d_hid, dropout)
    9. # 定义编码器,pytorch将Transformer编码器进行了打包,这里直接调用即可
    10. self.transformer_encoder = TransformerEncoder(encoder_layers, nlayers)
    11. self.embedding = nn.Embedding(ntoken, d_model)
    12. self.d_model = d_model
    13. self.linear = nn.Linear(d_model, ntoken)
    14. self.init_weights()
    15. # 初始化权重
    16. def init_weights(self) -> None:
    17. initrange = 0.1
    18. self.embedding.weight.data.uniform_(-initrange, initrange)
    19. self.linear.bias.data.zero_()
    20. self.linear.weight.data.uniform_(-initrange, initrange)
    21. def forward(self, src: Tensor, src_mask: Tensor = None) -> Tensor:
    22. """
    23. Arguments:
    24. src : Tensor, 形状为 [seq_len, batch_size]
    25. src_mask: Tensor, 形状为 [seq_len, seq_len]
    26. Returns:
    27. 输出的 Tensor, 形状为 [seq_len, batch_size, ntoken]
    28. """
    29. src = self.embedding(src) * math.sqrt(self.d_model)
    30. src = self.pos_encoder(src)
    31. output = self.transformer_encoder(src, src_mask)
    32. output = self.linear(output)
    33. return output

    定义位置编码器PositionalEncoding,用于在Transformer模型中为输入的序列添加位置编码

    1. class PositionalEncoding(nn.Module):
    2. def __init__(self, d_model: int, dropout: float = 0.1, max_len: int = 5000):
    3. super().__init__()
    4. self.dropout = nn.Dropout(p=dropout)
    5. # 生成位置编码的位置张量
    6. position = torch.arange(max_len).unsqueeze(1)
    7. # 计算位置编码的除数项
    8. div_term = torch.exp(torch.arange(0, d_model, 2) * (-math.log(10000.0) / d_model))
    9. # 创建位置编码张量
    10. pe = torch.zeros(max_len, 1, d_model)
    11. # 使用正弦函数计算位置编码中的奇数维度部分
    12. pe[:, 0, 0::2] = torch.sin(position * div_term)
    13. # 使用余弦函数计算位置编码中的偶数维度部分
    14. pe[:, 0, 1::2] = torch.cos(position * div_term)
    15. self.register_buffer('pe', pe)
    16. def forward(self, x: Tensor) -> Tensor:
    17. """
    18. Arguments:
    19. x: Tensor, 形状为 [seq_len, batch_size, embedding_dim]
    20. """
    21. # 将位置编码添加到输入张量
    22. x = x + self.pe[:x.size(0)]
    23. # 应用 dropout
    24. return self.dropout(x)

    二、加载数据集

    本教程用于torchtext生成 Wikitext-2 数据集。在此之前,你需要安装下面的包:
    pip install portalocker
    pip install torchdata
    batchify()将数据排列成batch_size列。如果数据没有均匀地分成batch_size列,则数据将被修剪以适合。例如,以字母表作为数据(总长度为 26)和batch_size=4,我们会将字母表分成长度为 6 的序列,从而得到 4 个这样的序列。

    image.png

    1. from torchtext.datasets import WikiText2
    2. from torchtext.data.utils import get_tokenizer
    3. from torchtext.vocab import build_vocab_from_iterator
    4. # 从torchtext库中导入WikiText2数据集
    5. train_iter = WikiText2(split='train')
    6. # 获取基本英语的分词器
    7. tokenizer = get_tokenizer('basic_english')
    8. # 通过迭代器构建词汇表
    9. vocab = build_vocab_from_iterator(map(tokenizer, train_iter), specials=[''])
    10. # 将默认索引设置为''
    11. vocab.set_default_index(vocab[''])
    12. def data_process(raw_text_iter: dataset.IterableDataset) -> Tensor:
    13. """将原始文本转换为扁平的张量"""
    14. data = [torch.tensor(vocab(tokenizer(item)),
    15. dtype=torch.long) for item in raw_text_iter]
    16. return torch.cat(tuple(filter(lambda t: t.numel() > 0, data)))
    17. # 由于构建词汇表时"train_iter"被使用了,所以需要重新创建
    18. train_iter, val_iter, test_iter = WikiText2()
    19. # 对训练、验证和测试数据进行处理
    20. train_data = data_process(train_iter)
    21. val_data = data_process(val_iter)
    22. test_data = data_process(test_iter)
    23. # 检查是否有可用的CUDA设备,将设备设置为GPU或CPU
    24. device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    25. def batchify(data: Tensor, bsz: int) -> Tensor:
    26. """将数据划分为 bsz 个单独的序列,去除不能完全容纳的额外元素。
    27. 参数:
    28. data: Tensor, 形状为``[N]``
    29. bsz : int, 批大小
    30. 返回:
    31. 形状为 [N // bsz, bsz] 的张量
    32. """
    33. seq_len = data.size(0) // bsz
    34. data = data[:seq_len * bsz]
    35. data = data.view(bsz, seq_len).t().contiguous()
    36. return data.to(device)
    37. # 设置批大小和评估批大小
    38. batch_size = 20
    39. eval_batch_size = 10
    40. # 将训练、验证和测试数据进行批处理
    41. train_data = batchify(train_data, batch_size) # 形状为 [seq_len, batch_size]
    42. val_data = batchify(val_data, eval_batch_size)
    43. test_data = batchify(test_data, eval_batch_size)

    data.view(bsz, seq_len).t().contiguous()详解如下:

    • data.view(bsz, seq_len):使用view方法将数据张量进行重塑,将其形状调整为(bsz, seq_len),其中bsz是批大小,seq_len是序列长度。
    • .t():使用.t()方法对重塑后的张量进行转置操作,将原来的行转换为列,原来的列转换为行。这是因为在自然语言处理任务中,通常我们希望对一个批次中的多个句子进行并行处理,因此需要将句子排列为批次维度在前,序列维度在后的形式。
    • .contiguous():使用.contiguous()方法确保转置后的张量在内存中是连续存储的。在进行一些操作时,如转换为某些特定类型的张量或进行高效的计算,需要保证张量的内存布局是连续的。
    1. bptt = 35
    2. # 获取批次数据
    3. def get_batch(source: Tensor, i: int) -> Tuple[Tensor, Tensor]:
    4. """
    5. 参数:
    6. source: Tensor,形状为 ``[full_seq_len, batch_size]``
    7. i : int, 当前批次索引
    8. 返回:
    9. tuple (data, target),
    10. - data形状为 [seq_len, batch_size],
    11. - target形状为 [seq_len * batch_size]
    12. """
    13. # 计算当前批次的序列长度,最大为bptt,确保不超过source的长度
    14. seq_len = min(bptt, len(source) - 1 - i)
    15. # 获取data,从i开始,长度为seq_len
    16. data = source[i:i+seq_len]
    17. # 获取target,从i+1开始,长度为seq_len,并将其形状转换为一维张量
    18. target = source[i+1:i+1+seq_len].reshape(-1)
    19. return data, target

    三、初始化实例

    1. ntokens = len(vocab) # 词汇表的大小
    2. emsize = 200 # 嵌入维度
    3. d_hid = 200 # nn.TransformerEncoder 中前馈网络模型的维度
    4. nlayers = 2 #nn.TransformerEncoder中的nn.TransformerEncoderLayer层数
    5. nhead = 2 # nn.MultiheadAttention 中的头数
    6. dropout = 0.2 # 丢弃概率
    7. # 创建 Transformer 模型,并将其移动到设备上
    8. model = TransformerModel(ntokens,
    9. emsize,
    10. nhead,
    11. d_hid,
    12. nlayers,
    13. dropout).to(device)

    四、训练模型

    我们将CrossEntropyLoss与SGD(随机梯度下降)优化器结合使用。学习率最初设置为 5.0 并遵循StepLR。在训练期间,我们使用nn.utils.clip_grad_norm_来防止梯度爆炸。

    1. import time
    2. criterion = nn.CrossEntropyLoss() # 定义交叉熵损失函数
    3. lr = 5.0 # 学习率
    4. # 使用随机梯度下降(SGD)优化器,将模型参数传入优化器
    5. optimizer = torch.optim.SGD(model.parameters(), lr=lr)
    6. # 使用学习率调度器,每隔1个epoch,将学习率按0.95的比例进行衰减
    7. scheduler = torch.optim.lr_scheduler.StepLR(optimizer, 1.0, gamma=0.95)
    8. def train(model: nn.Module) -> None:
    9. model.train() # 开启训练模式
    10. total_loss = 0.
    11. log_interval = 200 # 每隔200个batch打印一次日志
    12. start_time = time.time()
    13. num_batches = len(train_data) // bptt # 计算总的batch数量
    14. for batch, i in enumerate(range(0, train_data.size(0) - 1, bptt)):
    15. data, targets = get_batch(train_data, i) # 获取当前batch的数据和目标
    16. output = model(data) # 前向传播
    17. output_flat = output.view(-1, ntokens)
    18. loss = criterion(output_flat, targets) # 计算损失
    19. optimizer.zero_grad() # 梯度清零
    20. loss.backward() # 反向传播计算梯度
    21. torch.nn.utils.clip_grad_norm_(model.parameters(), 0.5) # 对梯度进行裁剪,防止梯度爆炸
    22. optimizer.step() # 更新模型参数
    23. total_loss += loss.item() # 累加损失值
    24. if batch % log_interval == 0 and batch > 0:
    25. lr = scheduler.get_last_lr()[0] # 获取当前学习率
    26. # 计算每个batch的平均耗时
    27. ms_per_batch = (time.time() - start_time) * 1000 / log_interval
    28. cur_loss = total_loss / log_interval # 计算平均损失
    29. ppl = math.exp(cur_loss) # 计算困惑度
    30. # 打印日志信息
    31. print(f'| epoch {epoch:3d} | {batch:5d}/{num_batches:5d} batches | '
    32. f'lr {lr:02.2f} | ms/batch {ms_per_batch:5.2f} | '
    33. f'loss {cur_loss:5.2f} | ppl {ppl:8.2f}')
    34. total_loss = 0 # 重置损失值
    35. start_time = time.time() # 重置起始时间
    36. def evaluate(model: nn.Module, eval_data: Tensor) -> float:
    37. model.eval() # 开启评估模式
    38. total_loss = 0.
    39. with torch.no_grad():
    40. for i in range(0, eval_data.size(0) - 1, bptt):
    41. data, targets = get_batch(eval_data, i) # 获取当前batch的数据和目标
    42. seq_len = data.size(0) # 序列长度
    43. output = model(data) # 前向传播
    44. output_flat = output.view(-1, ntokens)
    45. total_loss += seq_len * criterion(output_flat, targets).item() # 计算总损失
    46. return total_loss / (len(eval_data) - 1) # 返回平均损失

    math.exp(cur_loss)是使用数学模块中的 exp() 函数来计算当前损失对应的困惑度值。在这个上下文中,cur_loss 是当前的平均损失值,math.exp() 函数会将其作为指数的幂次,返回 e 的 cur_loss 次方。这个操作是为了计算困惑度(Perplexity),困惑度是一种评估语言模型好坏的指标,通常用于衡量模型对于给定输入数据的预测能力。困惑度越低,表示模型的预测能力越好。

    1. best_val_loss = float('inf') # 初始最佳验证损失为无穷大
    2. epochs = 1 # 训练的总轮数
    3. with TemporaryDirectory() as tempdir: # 创建临时目录来保存最佳模型参数
    4. # 最佳模型参数的保存路径
    5. best_model_params_path = os.path.join(tempdir, "best_model_params.pt")
    6. for epoch in range(1, epochs + 1): # 遍历每个epoch
    7. epoch_start_time = time.time() # 记录当前epoch开始的时间
    8. train(model) # 进行模型训练
    9. val_loss = evaluate(model, val_data) # 在验证集上评估模型性能,计算验证损失
    10. val_ppl = math.exp(val_loss) # 计算困惑度
    11. elapsed = time.time() - epoch_start_time # 计算当前epoch的耗时
    12. print('-' * 89)
    13. # 打印当前epoch的信息,包括耗时、验证损失和困惑度
    14. print(f'| end of epoch {epoch:3d} | time: {elapsed:5.2f}s | '
    15. f'valid loss {val_loss:5.2f} | valid ppl {val_ppl:8.2f}')
    16. print('-' * 89)
    17. if val_loss < best_val_loss: # 如果当前验证损失比最佳验证损失更低
    18. best_val_loss = val_loss # 更新最佳验证损失
    19. # 保存当前模型参数为最佳模型参数
    20. torch.save(model.state_dict(), best_model_params_path)
    21. scheduler.step() # 更新学习率
    22. # 加载最佳模型参数,即加载在验证集上性能最好的模型
    23. model.load_state_dict(torch.load(best_model_params_path))

    五、评估模型

    1. test_loss = evaluate(model, test_data)
    2. test_ppl = math.exp(test_loss)
    3. print('=' * 89)
    4. print(f'| End of training | test loss {test_loss:5.2f} | '
    5. f'test ppl {test_ppl:8.2f}')
    6. print('=' * 89)

  • 相关阅读:
    二、准备开发与调试环境
    全面预算管理软件
    HTML学生中秋节日网页设计模板 DIV布局大学生中秋节网页作业制作 八月十五中秋静态网页成品代码下载 中秋节日网页设计作品
    [C++随想录] 模版进阶
    prometheus starting - 相识
    RK3568驱动指南|第五期-中断-第49章 中断线程化实验
    NFT Insider #73:淡马锡将领投Animoca Brands新一轮1亿美元融资
    基于 Amazon API Gatewy 的跨账号跨网络的私有 API 集成
    【计网】(六)传输层(TCP、UDP、可靠传输、流量控制......)
    【Rust 指南】并发编程|无畏并发的原因
  • 原文地址:https://blog.csdn.net/m0_62237233/article/details/132031810