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

    文章

    3648

    浏览量

    51712
  • 语言模型
    +关注

    关注

    0

    文章

    570

    浏览量

    11255
  • mask
    +关注

    关注

    0

    文章

    10

    浏览量

    3188

原文标题:2. 代码部分

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

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

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

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

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

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

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

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

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

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

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

    何进行YOLO模型转换?

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

    海思SD3403边缘计算AI数据训练概述

    AI数据训练:基于用户特定应用场景,用户采集照片或视频,通过AI数据训练工程师**(用户公司****员工)** ,进行特征标定后,将标定好的训练样本,通过AI
    发表于 04-28 11:11

    请问STM32WBA65如何进行matter的学习?

    STM32WBA65如何进行matter的学习?相关的支持都有哪些?有一个X-CUBE-MATTER,可是这个没有集成在STM32CubeMX中
    发表于 04-24 07:22

    何进行电磁干扰处理

    智慧华盛恒辉如何进行电磁干扰 一、引言 电磁干扰已成为一种重要的作战手段,用于削弱、瘫痪或混乱敌方的通信、控制和侦察系统。如何对敌方的装备进行电磁干扰,包括干扰原理、干扰方式、干扰策略以及干扰效果
    的头像 发表于 02-20 10:28 1181次阅读

    DLP4710EVM-LC如何进行烧录?

    DLP4710EVM-LC: 如何进行烧录
    发表于 02-20 08:07

    请问TIDA-00554的光谱模组在安装和调试阶段光机是如何进行校验的呢?

    你好,请问TIDA-00554的光谱模组在安装和调试阶段光机是如何进行校验的呢?比如光电探测器的调试、DMD微镜的调试以及光谱曲线的校正?如何保证多个光机之间的一致性呢?
    发表于 02-20 07:19

    DLPC7540EVM是否支持自定义的图像处理算法,以及如何进行算法的移植?

    是否支持自定义的图像处理算法,以及如何进行算法的移植?
    发表于 02-17 08:25

    请问多片ADS1255/6如何进行同步采集,使用1个SPI接口的情况下?

    请问多片ADS1255/6如何进行同步采集,使用1个SPI接口的情况下。
    发表于 02-12 07:17

    采用AFE0064和ADS8363加fpga结构,如何进行控制?

    采用AFE0064和ADS8363加fpga结构,详问如何进行控制?
    发表于 02-05 06:10

    请问做反射式血氧饱和度测量时如何进行标定呢?

    请问做反射式血氧饱和度测量时如何进行标定呢? 目前已完成透射式血氧饱和度测量仪的设计和实现,采用的Fluke的生命体征模拟仪Prosim8进行标定的,仪器有一个模拟手指,可以将指套式探头夹在模拟
    发表于 01-08 06:42

    请问ADS8689 AGND和DGND如何进行处理?

    请问ADS8689 AGND和DGND如何进行处理?是把AGND和DGND处理为同一个地,还是AGND和DGND通过0欧电阻进行连接?为什么?
    发表于 12-26 06:42