【UCAS自然语言处理作业二】训练FFN, RNN, Attention机制的语言模型,并计算测试集上的PPL

本文主要是介绍【UCAS自然语言处理作业二】训练FFN, RNN, Attention机制的语言模型,并计算测试集上的PPL,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 前言
  • 前馈神经网络
    • 数据组织
    • Dataset
    • 网络结构
    • 训练
    • 超参设置
  • RNN
    • 数据组织&Dataset
    • 网络结构
    • 训练
    • 超参设置
  • 注意力网络
    • 数据组织&Dataset
    • 网络结构
      • Attention部分
      • 完整模型
    • 训练部分
    • 超参设置
  • 结果与分析
    • 训练集Loss
    • 测试集PPL

前言

本次实验主要针对前馈神经网络,RNN,以及基于注意力机制的网络学习语言建模任务,并在测试集上计算不同语言模型的PPL

  • PPL计算:我们采用teacher forcing的方式,给定ground truth context,让其预测next token,并将这些token的log probability进行平均,作为文本的PPL
  • CrossEntropyLoss:可以等价于PPL的计算,因此,我们将交叉熵损失作为ppl,具体原理可参考本人博客:如何计算文本的困惑度perplexity(ppl)_ppl计算_长命百岁️的博客-CSDN博客
  • 我们将数据分为训练集和测试集(后1000条)
  • 分词采用bart-base-chinese使用的tokenizer词表大小为21128。当然,也可以利用其他分词工具构建词表
  • 本文仅对重要的实验代码进行说明

前馈神经网络

数据组织

我们利用前馈神经网络,训练一个2-gram语言模型,即每次利用两个token来预测下一个token

def get_n_gram_data(self, data, n):res_data = []res_label = []if len(data) < n:raise VallueError("too short")start_idx = 0while start_idx + n <= len(data):res_data.append(data[start_idx: start_idx + n - 1])res_label.append(data[start_idx + n - 1])start_idx += 1return res_data, res_label
  • 该函数的输入是一个分词后的token_ids列表,输出是将这个ids分成不同的data, label
def get_data(path, n):res_data = []res_label = []tokenizer = BertTokenizer.from_pretrained('/users/nishiyu/ict/Models/bart-base-chinese')with open(path) as file:data = file.readlines()for sample in data:sample_data, sample_label = get_n_gram_data(tokenizer(sample, return_tensors='pt')['input_ids'][0], n)for idx in range(len(sample_data)):res_data.append(sample_data[idx])res_label.append(sample_label[idx])return res_data, res_label
  • 该函数对数据集中的每条数据进行分词,并得到对应的data, label
  • 值得注意的是,这样所有的输入/输出都是等长的,因此可以直接组装成batch

Dataset

class NGramDataset(Dataset):def __init__(self, data_path, window_size=3):self.data, self.label = get_data(data_path, window_size)def __len__(self):return len(self.data)def __getitem__(self, i):return self.data[i], self.label[i]
  • 通过window_size来指定n-gram
  • 每次访问返回datalabel

网络结构

class FeedForwardNNLM(nn.Module):def __init__(self, vocab_size, embedding_dim, window_size, hidden_dim):super(FeedForwardNNLM, self).__init__()self.embeddings = nn.Embedding(vocab_size, embedding_dim)self.e2h = nn.Linear((window_size - 1) * embedding_dim, hidden_dim)self.h2o = nn.Linear(hidden_dim, vocab_size)self.activate = F.reludef forward(self, inputs):embeds = self.embeddings(inputs).reshape([inputs.shape[0], -1])hidden = self.activate(self.e2h(embeds))output = self.h2o(hidden)return output
  • 网络流程:embedding层->全连接层->激活函数->线性层词表映射

训练

class Trainer():def __init__(self, args, embedding_dim, hidden_dim):self.args = argsself.model = FeedForwardNNLM(self.args.vocab_size, embedding_dim, args.window_size, hidden_dim)self.train_dataset = NGramDataset(self.args.train_data, self.args.window_size)self.train_dataloader = DataLoader(self.train_dataset, batch_size=args.batch_size, shuffle=True)self.test_dataset = NGramDataset(self.args.test_data, self.args.window_size)self.test_dataloader = DataLoader(self.test_dataset, batch_size=args.batch_size, shuffle=False)def train(self):self.model.train()device = torch.device('cuda')self.model.to(device)criterion = nn.CrossEntropyLoss()optimizer = Adam(self.model.parameters(), lr=5e-5)for epoch in range(args.epoch):total_loss = 0.0for step, batch in enumerate(self.train_dataloader):input_ids = batch[0].to(device)label_ids = batch[1].to(device)logits = self.model(input_ids)loss = criterion(logits, label_ids)loss.backward()optimizer.step()self.model.zero_grad()total_loss += lossprint(f'epoch: {epoch}, train loss: {total_loss / len(self.train_dataloader)}')self.evaluation()
  • 首先调用datasetdataloader对数据进行组织
  • 然后利用CrossEntropyLossAdam优化器(lr=5e-5)进行训练
  • 评估测试集效果

超参设置

def get_args():parser = argparse.ArgumentParser()# 添加命令行参数parser.add_argument('--vocab_size', type=int, default=21128)parser.add_argument('--train_data', type=str)parser.add_argument('--test_data', type=str)parser.add_argument('--window_size', type=int)parser.add_argument('--epoch', type=int, default=50)parser.add_argument('--batch_size', type=int, default=4096)args = parser.parse_args()return args
  • embedding_dim=128
  • hidden_dim=256
  • epoch = 150

RNN

数据组织&Dataset

RNN的数据组织比较简单,就是每一行作为一个输入就可以,不详细展开

网络结构

class RNNLanguageModel(nn.Module):def __init__(self, args, embedding_dim, hidden_dim):super(RNNLanguageModel, self).__init__()self.args = argsself.embeddings = nn.Embedding(self.args.vocab_size, embedding_dim)self.rnn = nn.RNN(embedding_dim, hidden_dim, batch_first=True)self.linear = nn.Linear(hidden_dim, self.args.vocab_size)def forward(self, inputs):embeds = self.embeddings(inputs)output, hidden = self.rnn(embeds)output = self.linear(output)return output
  • 网络流程:embedding->rnn网络->线性层词表映射
  • 这里RNN模型直接调用API

训练

自回归模型的训练是值得注意的

class Trainer():def __init__(self, args, embed_dim, head_dim):self.args = argsself.tokenizer = BertTokenizer.from_pretrained('/users/nishiyu/ict/Models/bart-base-chinese')self.model = RNNLanguageModel(args, embed_dim, head_dim)self.train_dataset = AttenDataset(self.args.train_data)self.train_dataloader = DataLoader(self.train_dataset, batch_size=args.batch_size, shuffle=True)self.test_dataset = AttenDataset(self.args.test_data)self.test_dataloader = DataLoader(self.test_dataset, batch_size=args.batch_size, shuffle=False)def train(self):self.model.to(self.args.device)criterion = nn.CrossEntropyLoss()optimizer = Adam(self.model.parameters(), lr=5e-5)for epoch in range(args.epoch):self.model.train()total_loss = 0.0for step, batch in enumerate(self.train_dataloader):tokens = self.tokenizer(batch, truncation=True, padding=True, max_length=self.args.max_len, return_tensors='pt').to(self.args.device)input_ids = tokens['input_ids'][:, :-1]label_ids = tokens['input_ids'][:, 1:].clone()pad_token_id = self.tokenizer.pad_token_idlabel_ids[label_ids == pad_token_id] = -100 logits = self.model(input_ids)loss = criterion(logits.view(-1, self.args.vocab_size), label_ids.view(-1))loss.backward()optimizer.step()self.model.zero_grad()total_loss += lossprint(f'epoch: {epoch}, train loss: {total_loss / len(self.train_dataloader)}')self.evaluation()
  • 与FFN不同的是,我们在需要数据的时候才进行分词

  • 注意到,数据集中不同数据的长度是不同的,我们想要将这些数据组织成batch,进行并行化训练,需要加padding。在训练过程中我们选择右padding

    input_ids = tokens['input_ids'][:, :-1]
    label_ids = tokens['input_ids'][:, 1:].clone()
    pad_token_id = self.tokenizer.pad_token_id
    label_ids[label_ids == pad_token_id] = -100 
    
    • 这四句是训练的核心代码,决定是否正确,从上往下分别是:
      • 组织输入:因为我们要预测下一个token,因此,输入最多就进行到倒数第二个token,所以不要最后一个
      • 组织label:因为我们要预测下一个token,因此作为label来说,不需要第一个token
      • 组织loss:对于padding部分的token,是不需要计算loss的,因此我们将padding部分对应的label_ids设置为-100,这是因为,损失函数默认id为-100的token为pad部分,不进行loss计算

超参设置

  • embedding_dim=512
  • hidden_dim=128
  • epoch=30
  • batch_size=12

注意力网络

数据组织&Dataset

与RNN完全相同,不进行介绍

网络结构

因为此网络比较重要,我之前也BART, GPT-2等模型的源码,因此我们选择自己写一个一层的decoder-only模型

  • 我们主要实现了自注意力机制
  • dropoutlayerNorm,残差链接等操作并没有关注

Attention部分

class SelfAttention(nn.Module):def __init__(self,args,embed_dim: int,num_heads: int,bias = True):super(SelfAttention, self).__init__()self.args = argsself.embed_dim = embed_dimself.num_heads = num_headsself.head_dim = embed_dim // num_headsself.scaling = self.head_dim**-0.5self.k_proj = nn.Linear(embed_dim, embed_dim, bias=bias)self.v_proj = nn.Linear(embed_dim, embed_dim, bias=bias)self.q_proj = nn.Linear(embed_dim, embed_dim, bias=bias)self.out_proj = nn.Linear(embed_dim, embed_dim, bias=bias)def _shape(self, tensor: torch.Tensor, seq_len: int, bsz: int):return tensor.view(bsz, seq_len, self.num_heads, self.head_dim).transpose(1, 2).contiguous()def forward(self, hidden_states: torch.Tensor):"""Input shape: Batch x seq_len x dim"""bsz, tgt_len, _ = hidden_states.size()# get query projquery_states = self.q_proj(hidden_states) * self.scaling# self_attentionkey_states = self._shape(self.k_proj(hidden_states), -1, bsz) # bsz, heads, seq_len, dimvalue_states = self._shape(self.v_proj(hidden_states), -1, bsz)proj_shape = (bsz * self.num_heads, -1, self.head_dim)query_states = self._shape(query_states, tgt_len, bsz).view(*proj_shape) # bsz_heads, seq_len, dimkey_states = key_states.reshape(*proj_shape)value_states = value_states.reshape(*proj_shape)attn_weights = torch.bmm(query_states, key_states.transpose(1, 2)) # bsz*head_num, tgt_len, src_lensrc_len = key_states.size(1)# causal_maskmask_value = torch.finfo(attn_weights.dtype).minmatrix = torch.ones(bsz * self.num_heads, src_len, tgt_len).to(self.args.device)causal_mask = torch.triu(matrix, diagonal=1)causal_weights = torch.where(causal_mask.byte(), mask_value, causal_mask.double())attn_weights += causal_weights# do not need attn_maskattn_probs = nn.functional.softmax(attn_weights, dim=-1)# get outputattn_output = torch.bmm(attn_probs, value_states)attn_output = attn_output.view(bsz, self.num_heads, tgt_len, self.head_dim)attn_output = attn_output.transpose(1, 2)attn_output = attn_output.reshape(bsz, tgt_len, self.embed_dim)attn_output = self.out_proj(attn_output)return attn_output
  • 首先我们定义了embed_dim, 多少个头,以及K,Q,V的映射矩阵
  • forward函数的输入是一个batch的embedding,流程如下
    • 将输入分别映射为K, Q, V, 并将尺寸转换为多头的形式,shape(bsz*num_heads, seq_len, dim)
    • 进行casual mask
      • 首先定义一个当前数据个数下的最小值,当一个数加上这个值再进行softmax,概率基本为0
      • 根据K, Q, V,得到一个分数矩阵attn_weights
      • 然后定义一个大小为bsz * self.num_heads, src_len, tgt_len的全1矩阵
      • 将该矩阵变成一个上三角矩阵,其中为1的地方,代表着当前位置需要进行mask,其他位置都是0
      • 对于矩阵中为1的地方,我们用定义的最小值来替换、
      • 将分数矩阵与mask矩阵相加,就实现了一个causal 分数矩阵
      • 然后进行softmax,通过V得到目标向量
    • 为什么没有对padding进行mask
      • 因为不需要,我们进行的是右padding,所以causal mask已经对后面的padding进行了mask
      • 另外,当真正的输入输出算完后,对于后面padding位置对应的输出,是不统计loss的,因此padding没有影响

完整模型

class AttentionModel(nn.Module):def __init__(self, args, embed_dim, head_num):super(AttentionModel, self).__init__()self.args = argsself.embeddings = nn.Embedding(self.args.vocab_size, embed_dim)self.p_embeddings = nn.Embedding(self.args.max_len, embed_dim)self.attention = SelfAttention(self.args, embed_dim, head_num)self.output = nn.Linear(embed_dim, self.args.vocab_size)def forward(self, input_ids, attn_mask):embeddings = self.embeddings(input_ids)position_embeddings = self.p_embeddings(torch.arange(0, input_ids.shape[1], device=self.args.device))embeddings = embeddings + position_embeddingsoutput = self.attention(embeddings, attn_mask)logits = self.output(output)return logits
  • 我们不仅做了embedding,还实现了position embedding

训练部分

训练阶段与RNN一直,也是组织输入,输出,以及loss

超参设置

  • embed_dim=512
  • num_head=8
  • epoch=30
  • batch_size=12

结果与分析

训练集Loss

  • FFN loss(最小值4.332110404968262)

    在这里插入图片描述

  • RNN loss(最小值4.00740385055542)

    在这里插入图片描述

  • Attention loss(最小值3.7037367820739746)

    在这里插入图片描述

测试集PPL

  • FFN(最小值4.401318073272705)

    在这里插入图片描述

  • RNN(最小值4.0991902351379395)

    在这里插入图片描述

  • Attention(最小值3.9784348011016846)

    在这里插入图片描述

从结果来看,无论是train loss, 还是test ppl,均有FFN>RNN>Attention的关系,且我们看到后两个模型还未完全收敛,性能仍有上升空间。

  • 尽管FFN的任务更简单,其性能仍最差,这是因为其模型结构过于简单
  • RNN与Attention任务一致,但性能更差
  • Attention性能最好,这些观察均符合基本认识

代码可见:ShiyuNee/Train-A-Language-Model-based-on-FFN-RNN-Attention (github.com)

这篇关于【UCAS自然语言处理作业二】训练FFN, RNN, Attention机制的语言模型,并计算测试集上的PPL的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/428190

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

大模型研发全揭秘:客服工单数据标注的完整攻略

在人工智能(AI)领域,数据标注是模型训练过程中至关重要的一步。无论你是新手还是有经验的从业者,掌握数据标注的技术细节和常见问题的解决方案都能为你的AI项目增添不少价值。在电信运营商的客服系统中,工单数据是客户问题和解决方案的重要记录。通过对这些工单数据进行有效标注,不仅能够帮助提升客服自动化系统的智能化水平,还能优化客户服务流程,提高客户满意度。本文将详细介绍如何在电信运营商客服工单的背景下进行

性能测试介绍

性能测试是一种测试方法,旨在评估系统、应用程序或组件在现实场景中的性能表现和可靠性。它通常用于衡量系统在不同负载条件下的响应时间、吞吐量、资源利用率、稳定性和可扩展性等关键指标。 为什么要进行性能测试 通过性能测试,可以确定系统是否能够满足预期的性能要求,找出性能瓶颈和潜在的问题,并进行优化和调整。 发现性能瓶颈:性能测试可以帮助发现系统的性能瓶颈,即系统在高负载或高并发情况下可能出现的问题

无人叉车3d激光slam多房间建图定位异常处理方案-墙体画线地图切分方案

墙体画线地图切分方案 针对问题:墙体两侧特征混淆误匹配,导致建图和定位偏差,表现为过门跳变、外月台走歪等 ·解决思路:预期的根治方案IGICP需要较长时间完成上线,先使用切分地图的工程化方案,即墙体两侧切分为不同地图,在某一侧只使用该侧地图进行定位 方案思路 切分原理:切分地图基于关键帧位置,而非点云。 理论基础:光照是直线的,一帧点云必定只能照射到墙的一侧,无法同时照到两侧实践考虑:关

作业提交过程之HDFSMapReduce

作业提交全过程详解 (1)作业提交 第1步:Client调用job.waitForCompletion方法,向整个集群提交MapReduce作业。 第2步:Client向RM申请一个作业id。 第3步:RM给Client返回该job资源的提交路径和作业id。 第4步:Client提交jar包、切片信息和配置文件到指定的资源提交路径。 第5步:Client提交完资源后,向RM申请运行MrAp

字节面试 | 如何测试RocketMQ、RocketMQ?

字节面试:RocketMQ是怎么测试的呢? 答: 首先保证消息的消费正确、设计逆向用例,在验证消息内容为空等情况时的消费正确性; 推送大批量MQ,通过Admin控制台查看MQ消费的情况,是否出现消费假死、TPS是否正常等等问题。(上述都是临场发挥,但是RocketMQ真正的测试点,还真的需要探讨) 01 先了解RocketMQ 作为测试也是要简单了解RocketMQ。简单来说,就是一个分

Andrej Karpathy最新采访:认知核心模型10亿参数就够了,AI会打破教育不公的僵局

夕小瑶科技说 原创  作者 | 海野 AI圈子的红人,AI大神Andrej Karpathy,曾是OpenAI联合创始人之一,特斯拉AI总监。上一次的动态是官宣创办一家名为 Eureka Labs 的人工智能+教育公司 ,宣布将长期致力于AI原生教育。 近日,Andrej Karpathy接受了No Priors(投资博客)的采访,与硅谷知名投资人 Sara Guo 和 Elad G

Retrieval-based-Voice-Conversion-WebUI模型构建指南

一、模型介绍 Retrieval-based-Voice-Conversion-WebUI(简称 RVC)模型是一个基于 VITS(Variational Inference with adversarial learning for end-to-end Text-to-Speech)的简单易用的语音转换框架。 具有以下特点 简单易用:RVC 模型通过简单易用的网页界面,使得用户无需深入了

【测试】输入正确用户名和密码,点击登录没有响应的可能性原因

目录 一、前端问题 1. 界面交互问题 2. 输入数据校验问题 二、网络问题 1. 网络连接中断 2. 代理设置问题 三、后端问题 1. 服务器故障 2. 数据库问题 3. 权限问题: 四、其他问题 1. 缓存问题 2. 第三方服务问题 3. 配置问题 一、前端问题 1. 界面交互问题 登录按钮的点击事件未正确绑定,导致点击后无法触发登录操作。 页面可能存在

科研绘图系列:R语言扩展物种堆积图(Extended Stacked Barplot)

介绍 R语言的扩展物种堆积图是一种数据可视化工具,它不仅展示了物种的堆积结果,还整合了不同样本分组之间的差异性分析结果。这种图形表示方法能够直观地比较不同物种在各个分组中的显著性差异,为研究者提供了一种有效的数据解读方式。 加载R包 knitr::opts_chunk$set(warning = F, message = F)library(tidyverse)library(phyl