【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 = 0
        while 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 += 1
            return 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.relu

    def 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 = args
        self.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.0
            for 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 += loss
            print(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 = args
        self.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 = args
        self.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.0
            for 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_id
                label_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 += loss
            print(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 = args
        self.embed_dim = embed_dim
        self.num_heads = num_heads
        self.head_dim = embed_dim // num_heads
        self.scaling = self.head_dim**-0.5

        self.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 proj
        query_states = self.q_proj(hidden_states) * self.scaling
       
        # self_attention
        key_states = self._shape(self.k_proj(hidden_states), -1, bsz) # bsz, heads, seq_len, dim
        value_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, dim
        key_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_len

        src_len = key_states.size(1)
        # causal_mask
        mask_value = torch.finfo(attn_weights.dtype).min
        matrix = 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_mask
        attn_probs = nn.functional.softmax(attn_weights, dim=-1)

        # get output
        attn_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 = args
        self.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_embeddings
        output = 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)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/189533.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

不同品牌的手机可以则哪一个你投屏到电视?

如果你使用AirDroid Cast的TV版&#xff0c;苹果手机可以通过airPlay或无线投屏方式&#xff0c;将屏幕同步到电视屏幕&#xff1b;多个品牌的安卓手机可以通过无线投屏投射到电视。而且无线投屏不限制距离&#xff0c;即使是远程投屏也可以实现。 打开AirDroid Cast的TV版&…

鸿蒙(HarmonyOS)应用开发——装饰器

简介 ArkTS是HarmonyOS优选的主力应用开发语言。它在TypeScript&#xff08;简称TS&#xff09;的基础上&#xff0c;扩展了声明式UI、状态管理等相应的能力&#xff0c;让开发者可以以更简洁、更自然的方式开发高性能应用。TS是JavaScript&#xff08;简称JS&#xff09;的超…

shiro的前后端分离模式

shiro的前后端分离模式 前言&#xff1a;在上一篇《shiro的简单认证和授权》中介绍了shiro的搭建&#xff0c;默认情况下&#xff0c;shiro是通过设置cookie&#xff0c;使前端请求带有“JSESSION”cookie&#xff0c;后端通过获取该cookie判断用户是否登录以及授权。但是在前…

【开源】基于Vue和SpringBoot的木马文件检测系统

项目编号&#xff1a; S 041 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S041&#xff0c;文末获取源码。} 项目编号&#xff1a;S041&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 数据中心模块2.2 木马分类模块2.3 木…

JavaScript 表达式

JavaScript 表达式 目录 JavaScript 表达式 一、赋值表达式 二、算术表达式 三、布尔表达式 四、字符串表达式 表达式是一个语句的集合&#xff0c;计算结果是个单一值。 在JavaScript中&#xff0c;常见的表达式有4种&#xff1a; &#xff08;1&#xff09;赋值表达式…

Postman接口自动化测试之——批量参数化(参数文件)

接口请求中的参数引用格式&#xff1a;{{参数名}} 参数文件只适用于集合中。 创建参数文件 以记事本举例&#xff0c;也可以使用其他编辑器&#xff1b;第一行参数名&#xff0c;用半角逗号&#xff08;英文逗号&#xff09;隔开&#xff0c;第二行为参数值&#xff0c;一样…

栈详解(C语言)

文章目录 写在前面1 栈的定义2 栈的初始化3 数据入栈4 数据出栈5 获取栈顶元素6 获取栈元素个数7 判断栈是否为空8 栈的销毁 写在前面 本片文章详细介绍了另外两种存储逻辑关系为 “一对一” 的数据结构——栈和队列中的栈&#xff0c;并使用C语言实现了数组栈。 栈C语言实现源…

Java基于springoot开发的企业招聘求职网站

演示视频&#xff1a; https://www.bilibili.com/video/BV1xw411n7Tu/?share_sourcecopy_web&vd_source11344bb73ef9b33550b8202d07ae139b 技术&#xff1a;springootmysqlvuejsbootstrappoi制作word模板 主要功能&#xff1a;求职者可以注册发布简历&#xff0c;选择简…

Redis面试题:redis做为缓存,mysql的数据如何与redis进行同步呢?(双写一致性)

目录 强一致性&#xff1a;延迟双删&#xff0c;读写锁。 弱一致性&#xff1a;使用MQ或者canal实现异步通知 面试官&#xff1a;redis做为缓存&#xff0c;mysql的数据如何与redis进行同步呢&#xff1f;&#xff08;双写一致性&#xff09; 候选人&#xff1a;嗯&#xff…

抖音生态融合:开发与抖音平台对接的票务小程序

为了更好地服务用户需求&#xff0c;将票务服务与抖音平台结合&#xff0c;成为了一个创新的方向。通过开发票务小程序&#xff0c;用户可以在抖音平台上直接获取相关活动的票务信息&#xff0c;完成购票、预订等操作&#xff0c;实现了线上线下的有机连接。 一、开发过程 1…

赢麻了!义乌一个村有5000个网红,有人年收租就300万!

#义乌一村电商年成交额超300亿# ,在中国&#xff0c;电商行业的发展可谓是日新月异&#xff0c;而位于浙江省义乌市的江北下朱村&#xff0c;正是这股潮流的一个典型代表。这个村子&#xff0c;处处弥漫着“直播”的气息&#xff0c;仿佛每个人都在为这个新兴行业助力。 江北下…

网安融合新进展:Check Point+七云网络联合研发,加固大型企业边缘、分支侧安全

AI 爆火、万物互联&#xff0c;底层需要更灵活的网络设施提供支撑。据国际分析机构 Gartner 预测&#xff0c;到 2024 年&#xff0c;SD-WAN&#xff08;软件定义的广域网&#xff09;使用率将达到 60%。不过边缘和终端兴起&#xff0c;未经过数据中心的流量也在成为新的安全风…

居家适老化设计第三十一条---卫生间水龙头

以上产品图片均来源于淘宝 侵权联系删除 居家适老化中&#xff0c;水龙头是一个非常重要的设备。水龙头的选择应该考虑到老年人的特点和需求。首先&#xff0c;水龙头的操作应该简单方便&#xff0c;老年人手部灵活性可能不如年轻人&#xff0c;因此水龙头应该设计成易于转动和…

torch.nn.batchnorm1d,torch.nn.batchnorm2d,torch.nn.LayerNorm解释:

批量归一化是一种加速神经网络训练和提升模型泛化能力的技术。它对每个特征维度进行标准化处理&#xff0c;即调整每个特征的均值和标准差&#xff0c;使得它们的分布更加稳定。 Batch Norm主要是为了让输入在激活函数的敏感区。所以BatchNorm层要加在激活函数前面。 1.torch.…

处理分类问题的不平衡数据的 5 种技术

一、介绍 分类问题在机器学习领域很常见。正如我们所知&#xff0c;在分类问题中&#xff0c;我们试图通过研究输入数据或预测变量来预测类标签&#xff0c;其中目标或输出变量本质上是分类变量。 如果您已经处理过分类问题&#xff0c;那么您一定遇到过以下情况&#xff1a;其…

【Go实现】实践GoF的23种设计模式:备忘录模式

上一篇&#xff1a;【Go实现】实践GoF的23种设计模式&#xff1a;命令模式 简单的分布式应用系统&#xff08;示例代码工程&#xff09;&#xff1a;https://github.com/ruanrunxue/Practice-Design-Pattern–Go-Implementation 简介 相对于代理模式、工厂模式等设计模式&…

PyQt6把QTDesigner生成的UI文件转成python源码,并运行

锋哥原创的PyQt6视频教程&#xff1a; 2024版 PyQt6 Python桌面开发 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili2024版 PyQt6 Python桌面开发 视频教程(无废话版) 玩命更新中~共计18条视频&#xff0c;包括&#xff1a;2024版 PyQt6 Python桌面开发 视频教程(无废话版…

1.1 C语言之入门:使用Visual Studio Community 2022运行hello world

1.1 使用Visual Studio Community 2022运行c语言的hello world 一、下载安装Visual Studio Community 2022 与 新建项目二、编写c helloworld三、编译、链接、运行 c helloworld1. 问题记录&#xff1a;无法打开源文件"stdio.h"2. 问题记录&#xff1a;调试和执行按钮…

Pinctrl子系统和GPIO子系统

Pinctrl子系统&#xff1a; 借助Princtr子系统来设置一个Pin的复用和电气属性&#xff1b; pinctrl子系统主要做的工作是&#xff1a;1. 获取设备树中的PIN信息&#xff1b;2.根据获取到的pin信息来设置的Pin的复用功能&#xff1b;3.根据获取到的pin信息去设置pin的电气特性…

vue+elementui如何实现在表格中点击按钮预览图片?

效果图如上&#xff1a; 使用el-image-viewer 重点 &#xff1a; 引入 import ElImageViewer from "element-ui/packages/image/src/image-viewer"; <template><div class"preview-table"><el-table border :data"tableData" …