0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

如何进行MLM训练

深度学习自然语言处理 来源:CSDN 作者:常鸿宇 2022-08-13 10:54 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

1. 关于MLM

1.1 背景

作为 Bert 预训练的两大任务之一,MLMNSP 大家应该并不陌生。其中,NSP 任务在后续的一些预训练任务中经常被嫌弃,例如 Roberta 中将 NSP 任务直接放弃,Albert 中将 NSP 替换成了句子顺序预测。

这主要是因为 NSP 作为一个分类任务过于简单,对模型的学习并没有太大的帮助,而 MLM 则被多数预训练模型保留下来。由 Roberta的实验结果也可以证明,Bert 的主要能力应该是来自于 MLM 任务的训练。

Bert为代表的预训练语言模型是在大规模语料的基础上训练以获得的基础的学习能力,而实际应用时,我们所面临的语料或许具有某些特殊性,这就使得重新进行 MLM 训练具有了必要性。

1.2 如何进行MLM训练

1.2.1 什么是MLM

MLM 的训练,在不同的预训练模型中其实是有所不同的。今天介绍的内容以最基础的 Bert 为例。

Bert的MLM是静态mask,而在后续的其他预训练模型中,这一策略通常被替换成了动态mask。除此之外还有 whole word mask 的模型,这些都不在今天的讨论范围内。

所谓 mask language model 的任务,通俗来讲,就是将句子中的一部分token替换掉,然后根据句子的剩余部分,试图去还原这部分被mask的token

1.2.2 如何Mask

mask 的比例一般是15%,这一比例也被后续的多数模型所继承,而在最初BERT 的论文中,没有对这一比例的界定给出具体的说明。在我的印象中,似乎是知道后来同样是Google提出的 T5 模型的论文中,对此进行了解释,对 mask 的比例进行了实验,最终得出结论,15%的比例是最合理的(如果我记错了,还请指正)。

15%的token选出之后,并不是所有的都替换成[mask]标记符。实际操作是:

  • 从这15%选出的部分中,将其中的80%替换成[mask];
  • 10%替换成一个随机的token;
  • 剩下的10%保留原来的token。

这样做可以提高模型的鲁棒性。这个比例也可以自己控制。

到这里可能有同学要问了,既然有10%保留不变的话,为什么不干脆只选择15%*90% = 13.5%的token呢?如果看完后面的代码,就会很清楚地理解这个问题了。

先说结论:因为 MLM 的任务是将选出的这15%的token全部进行预测,不管这个token是否被替换成了[mask],也就是说,即使它被保留了原样,也还是需要被预测的

2. 代码部分

2.1 背景

介绍完了基础内容之后,接下来的内容,我将基于 transformers 模块,介绍如何进行 mask language model 的训练。

其实 transformers 模块中,本身是提供了 MLM 训练任务的,模型都写好了,只需要调用它内置的 trainerdatasets模块即可。感兴趣的同学可以去 huggingface 的官网搜索相关教程。

然而我觉得 datasets 每次调用的时候都要去写数据集的py文件,对arrow的数据格式不熟悉的话还很容易出错,而且 trainer 我觉得也不是很好用,任何一点小小的修改都挺费劲(就是它以为它写的很完备,考虑了用户的所有需求,但是实际上有一些冗余的部分)。

所以我就参考它的实现方式,把它的代码拆解,又按照自己的方式重新组织了一下。

2.2 准备工作

首先在写核心代码之前,先做好准备工作。
import 所有需要的模块:

import os
import json
import copy
from tqdm.notebook import tqdm

import torch
from torch.optim import AdamW
from torch.utils.data import DataLoader, Dataset
from transformers import BertForMaskedLM, BertTokenizerFast

然后写一个config类,将所有参数集中起来:

class Config:
    def __init__(self):
        pass
    
    def mlm_config(
        self, 
        mlm_probability=0.15, 
        special_tokens_mask=None,
        prob_replace_mask=0.8,
        prob_replace_rand=0.1,
        prob_keep_ori=0.1,
    ):
        """
        :param mlm_probability: 被mask的token总数
        :param special_token_mask: 特殊token
        :param prob_replace_mask: 被替换成[MASK]的token比率
        :param prob_replace_rand: 被随机替换成其他token比率
        :param prob_keep_ori: 保留原token的比率
        """
        assert sum([prob_replace_mask, prob_replace_rand, prob_keep_ori]) == 1,                 ValueError("Sum of the probs must equal to 1.")
        self.mlm_probability = mlm_probability
        self.special_tokens_mask = special_tokens_mask
        self.prob_replace_mask = prob_replace_mask
        self.prob_replace_rand = prob_replace_rand
        self.prob_keep_ori = prob_keep_ori
        
    def training_config(
        self,
        batch_size,
        epochs,
        learning_rate,
        weight_decay,
        device,
    ):
        self.batch_size = batch_size
        self.epochs = epochs
        self.learning_rate = learning_rate
        self.weight_decay = weight_decay
        self.device = device
        
    def io_config(
        self,
        from_path,
        save_path,
    ):
        self.from_path = from_path
        self.save_path = save_path

接着就是设置各种配置:

config = Config()
config.mlm_config()
config.training_config(batch_size=4, epochs=10, learning_rate=1e-5, weight_decay=0, device='cuda:0')
config.io_config(from_path='/data/BERTmodels/huggingface/chinese_wwm/', 
                 save_path='./finetune_embedding_model/mlm/')

最后创建BERT模型。注意,这里的 tokenizer 就是一个普通的 tokenizer,而BERT模型则是带了下游任务的 BertForMaskedLM,它是 transformers 中写好的一个类,

bert_tokenizer = BertTokenizerFast.from_pretrained(config.from_path)
bert_mlm_model = BertForMaskedLM.from_pretrained(config.from_path)

2.3 数据集

因为舍弃了datasets这个包,所以我们现在需要自己实现数据的输入了。方案就是使用 torchDataset 类。这个类一般在构建 DataLoader 的时候,会与一个聚合函数一起使用,以实现对batch的组织。而我这里偷个懒,就没有写聚合函数,batch的组织方法放在dataset中进行。

在这个类中,有一个 mask tokens 的方法,作用是从数据中选择出所有需要mask 的token,并且采用三种mask方式中的一个。这个方法是从transformers 中拿出来的,将其从类方法转为静态方法测试之后,再将其放在自己的这个类中为我们所用。仔细阅读这一段代码,也就可以回答1.2.2 中提出的那个问题了。

取batch的原理很简单,一开始我们将原始数据deepcopy备份一下,然后每次从中截取一个batch的大小,这个时候的当前数据就少了一个batch,我们定义这个类的长度为当前长度除以batch size向下取整,所以当类的长度变为0的时候,就说明这一个epoch的所有step都已经执行结束,要进行下一个epoch的训练,此时,再将当前数据变为原始数据,就可以实现对epoch的循环了。

class TrainDataset(Dataset):
    """
    注意:由于没有使用data_collator,batch放在dataset里边做,
    因而在dataloader出来的结果会多套一层batch维度,传入模型时注意squeeze掉
    """
    def __init__(self, input_texts, tokenizer, config):
        self.input_texts = input_texts
        self.tokenizer = tokenizer
        self.config = config
        self.ori_inputs = copy.deepcopy(input_texts)
        
    def __len__(self):
        return len(self.input_texts) // self.config.batch_size
    
    def __getitem__(self, idx):
        batch_text = self.input_texts[: self.config.batch_size]
        features = self.tokenizer(batch_text, max_length=512, truncation=True, padding=True, return_tensors='pt')
        inputs, labels = self.mask_tokens(features['input_ids'])
        batch = {"inputs": inputs, "labels": labels}
        self.input_texts = self.input_texts[self.config.batch_size: ]
        if not len(self):
            self.input_texts = self.ori_inputs
        
        return batch
        
    def mask_tokens(self, inputs):
        """
        Prepare masked tokens inputs/labels for masked language modeling: 80% MASK, 10% random, 10% original.
        """
        labels = inputs.clone()
        # We sample a few tokens in each sequence for MLM training (with probability `self.mlm_probability`)
        probability_matrix = torch.full(labels.shape, self.config.mlm_probability)
        if self.config.special_tokens_mask is None:
            special_tokens_mask = [
                self.tokenizer.get_special_tokens_mask(val, already_has_special_tokens=True) for val in labels.tolist()
            ]
            special_tokens_mask = torch.tensor(special_tokens_mask, dtype=torch.bool)
        else:
            special_tokens_mask = self.config.special_tokens_mask.bool()

        probability_matrix.masked_fill_(special_tokens_mask, value=0.0)
        masked_indices = torch.bernoulli(probability_matrix).bool()
        labels[~masked_indices] = -100  # We only compute loss on masked tokens

        # 80% of the time, we replace masked input tokens with tokenizer.mask_token ([MASK])
        indices_replaced = torch.bernoulli(torch.full(labels.shape, self.config.prob_replace_mask)).bool() & masked_indices
        inputs[indices_replaced] = self.tokenizer.convert_tokens_to_ids(self.tokenizer.mask_token)

        # 10% of the time, we replace masked input tokens with random word
        current_prob = self.config.prob_replace_rand / (1 - self.config.prob_replace_mask)
        indices_random = torch.bernoulli(torch.full(labels.shape, current_prob)).bool() & masked_indices & ~indices_replaced
        random_words = torch.randint(len(self.tokenizer), labels.shape, dtype=torch.long)
        inputs[indices_random] = random_words[indices_random]

        # The rest of the time (10% of the time) we keep the masked input tokens unchanged
        return inputs, labels

然后取一些用于训练的语料,格式很简单,就是把所有文本放在一个list里边,注意长度不要超过512个token,不然多出来的部分就浪费掉了。可以做适当的预处理。

[
"这是一条文本",
"这是另一条文本",
...,
]

然后构建dataloader:

train_dataset = TrainDataset(training_texts, bert_tokenizer, config)
train_dataloader = DataLoader(train_dataset)

2.4 训练

构建一个训练方法,输入参数分别是我们实例化好的待训练模型,数据集,还有config:

def train(model, train_dataloader, config):
    """
    训练
    :param model: nn.Module
    :param train_dataloader: DataLoader
    :param config: Config
    ---------------
    ver: 2021-11-08
    by: changhongyu
    """
    assert config.device.startswith('cuda') or config.device == 'cpu', ValueError("Invalid device.")
    device = torch.device(config.device)
    
    model.to(device)
    
    if not len(train_dataloader):
        raise EOFError("Empty train_dataloader.")
        
    param_optimizer = list(model.named_parameters())
    no_decay = ["bias", "LayerNorm.bias", "LayerNorm.weight"]
    optimizer_grouped_parameters = [
        {"params": [p for n, p in param_optimizer if not any(nd in n for nd in no_decay)], "weight_decay": 0.01},
        {"params": [p for n, p in param_optimizer if any(nd in n for nd in no_decay)], "weight_decay": 0.0}]
    
    optimizer = AdamW(params=optimizer_grouped_parameters, lr=config.learning_rate, weight_decay=config.weight_decay)
    
    for cur_epc in tqdm(range(int(config.epochs)), desc="Epoch"):
        training_loss = 0
        print("Epoch: {}".format(cur_epc+1))
        model.train()
        for step, batch in enumerate(tqdm(train_dataloader, desc='Step')):
            input_ids = batch['inputs'].squeeze(0).to(device)
            labels = batch['labels'].squeeze(0).to(device)
            loss = model(input_ids=input_ids, labels=labels).loss
            
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            model.zero_grad()
            training_loss += loss.item()
        print("Training loss: ", training_loss)

调用它训练几轮:

train(model=bert_mlm_model, train_dataloader=train_dataloader, config=config)

2.5 保存和加载

使用过预训练模型的同学应该都了解,普通的bert有两项输出,分别是:

  • 每一个token对应的768维编码结果;
  • 以及用于表征整个句子的句子特征。

其中,这个句子特征是由模型中的一个 Pooler 模块对原句池化得来的。可是这个Pooler的训练,并不是由 MLM 任务来的,而是由 NSP任务中来的。

由于没有 NSP 任务,所以无法对 Pooler 进行训练,故而没有必要在模型中加入 Pooler。所以在保存的时候需要分别保存 embedding和 encoder, 加载的时候也需要分别读取 embedding 和 encoder,这样训练出来的模型拿不到 CLS 层的句子表征。如果需要的话,可以手动pooling 。

torch.save(bert_mlm_model.bert.embeddings.state_dict(), os.path.join(config.save_path, 'bert_mlm_ep_{}_eb.bin'.format(config.epochs)))
torch.save(bert_mlm_model.bert.encoder.state_dict(), os.path.join(config.save_path, 'bert_mlm_ep_{}_ec.bin'.format(config.epochs)))

加载的话,也是实例化完bert模型之后,用bert的 embedding 组件和 encoder 组件分别读取这两个权重文件即可。

到这里,本期内容就全部结束了,希望看完这篇博客的同学,能够对 Bert 的基础原理有更深入的了解。

审核编辑 :李倩


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 模型
    +关注

    关注

    1

    文章

    3874

    浏览量

    52341
  • 语言模型
    +关注

    关注

    0

    文章

    575

    浏览量

    11372
  • mask
    +关注

    关注

    0

    文章

    10

    浏览量

    3247

原文标题:2. 代码部分

文章出处:【微信号:zenRRan,微信公众号:深度学习自然语言处理】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    AD9269的芯片手册ADC的时钟信号在芯片内部和AVDD连接,如何进行修改

    ,发现ADC的时钟信号在芯片内部和AVDD连接,如何进行修改,需要在FPGA和ADC之间加一个时钟分配芯片么,该芯片需要用模拟电源供电么?
    发表于 05-21 08:29

    对于设备上的旧固件如何进行备份和恢复?

    对于设备上的旧固件,如何进行备份和恢复?
    发表于 12-12 08:23

    在使用CW32L083系列微控制器时如何进行系统复位和看门狗定时器配置?

    在使用CW32L083系列微控制器时,如何进行系统复位和看门狗定时器配置?
    发表于 12-10 06:46

    单片机如何进行加解密钥操作,一般使用哪种形式,具体流程是什么样子的?

    目前单片机如何进行加解密钥操作,一般使用哪种形式,具体流程是什么样子的?
    发表于 12-04 06:09

    L083最低功耗是多少,应该如何进行低功耗设计?有哪些注意事项?

    L083最低功耗是多少,应该如何进行低功耗设计?有哪些注意事项?
    发表于 11-12 07:29

    恩智浦i.MX RT1180 MCU如何进入Boundary Scan模式

    本文重点介绍RT1180如何进入Boundary Scan模式,通过Jtag来进行板级硬件测试的过程。遵循IEEE1149.1中的测试访问端口和BoundaryScan体系结构的标准。
    的头像 发表于 10-22 09:50 3789次阅读
    恩智浦i.MX RT1180 MCU如<b class='flag-5'>何进</b>入Boundary Scan模式

    在Ubuntu20.04系统中训练神经网络模型的一些经验

    模型。 我们使用MNIST数据集,训练一个卷积神经网络(CNN)模型,用于手写数字识别。一旦模型被训练并保存,就可以用于对新图像进行推理和预测。要使用生成的模型进行推理,可以按照以下步
    发表于 10-22 07:03

    何进行声音定位?

    文章主要介绍了如何利用一种简单的TDOA算法进行声音点位,并使用数据采集卡进行声音定位的实验。
    的头像 发表于 09-23 15:47 2276次阅读
    如<b class='flag-5'>何进行</b>声音定位?

    2KW逆变侧功率管的损耗如何进行计算详细公式免费下载

    本文档的主要内容详细介绍的是2KW逆变侧功率管的损耗如何进行计算详细公式免费下载。
    发表于 08-29 16:18 34次下载

    何进行YOLO模型转换?

    我目前使用的转模型代码如下 from ultralytics import YOLOimport cv2import timeimport nncaseimport# 加载预训练的YOLO模型
    发表于 08-14 06:03

    在对庐山派K230的SD卡data文件夹进行删除和新件文件夹时无法操作,且训练时线程异常,怎么解决?

    1.我的SD卡可以正常启动,也可以收到来自于庐山派摄像头拍摄的照片,SD卡有data和sdcard分区 2.但是一旦进行删除或写入,就会断开,我一开始在data/data/images这个目录
    发表于 08-01 08:03

    OpenHarmony2025年度竞赛训练营重磅开启

      OpenHarmony2025年度竞赛训练营       活动介绍 OpenHarmony竞赛训练营 旨在引导高校学生进行OpenHarmony产学研用,培养更多应用型人才和产业需求有效链接
    的头像 发表于 07-16 11:51 1122次阅读

    使用 ai cude 里面自带的案例训练UI显示异常的原因?怎么解决?

    案例的配置是默认的,显示训练ui更改显示异常
    发表于 06-23 06:21

    k210在线训练的算法是yolo5吗?

    k210在线训练的算法是yolo5吗
    发表于 06-16 08:25

    OCR识别训练完成后给的是空压缩包,为什么?

    OCR识别 一共弄了26张图片,都标注好了,点击开始训练,显示训练成功了,也将压缩包发到邮箱了,下载下来后,压缩包里面是空的 OCR图片20几张图太少了。麻烦您多添加点,参考我们的ocr识别训练数据集 请问
    发表于 05-28 06:46