当前位置:   article > 正文

使用huggingface的Transformers预训练自己的bert模型+FineTuning_huggingface预训练一个bert

huggingface预训练一个bert

将“softmax+交叉熵”推广到多标签分类问题
多分类问题引申到多标签分类问题(softmax+交叉熵)
作者苏剑林论述了将多分类任务下常用的softmax+CE的方式,推广到多标签任务,意图解决标签不均衡带来的一些问题。

SGM,多标签分类的序列生成模型 将序列生成模型,运用到多标签分类任务上,序列生成模型,典型的就是seq2seq模型。

关于transformers库中不同模型的Tokenizer说明

transformers手册
Transfomers Github

datasets手册

FGM对抗训练

最近参加某个NLP的比赛,因为NLP的问题接触的不是很多,所以在比赛过程中学习不少新知识和trick,在这里稍微记录以下,比赛用的是脱敏的数据,需要我们自己重新预训练模型,当然,用W2V,GLOVE等作为预训练模型+deep模型,也能达到还不错的成绩,但是上限不如bert高。

这里重点说下如何用huggingfaceTransformers训练自己的模型,虽然官方是给了手册和教程的,但是大多是基于已有的预训练模型,但如何适用自己的语料重新训练自己的bert模型相关资料较少,这里自己实践后的过程记录下。
训练自己的bert模型,需要现在准备三样东西,分别是 语料(数据),分词器,模型。

一、语料数据

用于训练bert模型的语料数据,常见的大规模数据在datasets里面都可以直接下载并加载,详细请参考资料⑤。对于自己的语料数据,或者脱敏的数据,我们需要自己处理下,这里建议每一行作为一个句子,词与词之间最好有分隔符。如下图Fig1所示:
在这里插入图片描述

Fig1 训练语料文本.txt(词与词之间用空格分割,一行表示一句话,由于是脱敏数据,所以词语均由数字表示)

二、分词器Tokenizer

Tonkenizer的作用,除了对文本进行分词外,还将每个词与相应的input_id对应,同时添加句子分隔符、mask掩码等,在Transformer中已经封装了常见的bert模型使用的分词器,如BertTokenizer,RobertaTokenizer等,可以直接使用,对文本进行分词并转化为对应的input_id,这里的id是与bert中embedding矩阵的索引号一一对应的。不同模型的Tokenizer略微有所区别,如下是加载预训练模型的tokenizer代码,详细可见资料③中的描述。

tokenizer = AutoTokenizer.from_pretrained('roberta-base')
  • 1

如果我们的语料很小,想重新训练模型的embedding层,就需要制作自己的Tonkenizer,以缩小词汇表的规模。这里是官方给出的一个训练自己tokenizer的代码:

from tokenizers import Tokenizer
from tokenizers.decoders import ByteLevel as ByteLevelDecoder
from tokenizers.models import BPE
from tokenizers.normalizers import Lowercase, NFKC, Sequence
from tokenizers.pre_tokenizers import ByteLeve
from tokenizers.trainers import BpeTrainer

# 1、创建一个空的字节对编码模型
tokenizer = Tokenizer(BPE())

#2、启用小写和unicode规范化,序列规范化器Sequence可以组合多个规范化器,并按顺序执行
tokenizer.normalizer = Sequence([
    NFKC(),
    Lowercase()
])
#3、标记化器需要一个预标记化器,负责将输入转换为ByteLevel表示。
tokenizer.pre_tokenizer = ByteLevel()

# 4、添加解码器,将token令牌化的输入恢复为原始的输入
tokenizer.decoder = ByteLevelDecoder()
# 5、初始化训练器,给他关于我们想要生成的词汇表的详细信息
trainer = BpeTrainer(vocab_size=858, show_progress=True, initial_alphabet=ByteLevel.alphabet())
# 6、开始训练我们的语料
tokenizer.train(files=["./tmp/all_data_txt.txt"], trainer=trainer)
# 最终得到该语料的Tonkernize,查看下词汇大小
print("Trained vocab size: {}".format(tokenizer.get_vocab_size()))
# 保存训练的tokenizer
tokenizer.model.save('./my_token/')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

上述的训练过程其实是针对的是byte级别的BPE编码,tokenizer保存为两个文件,分别是vocab.json, merges.txt ,这个在Robert中会用到,,普通的bert只需要vocab。这两个文件,可以用官方封装好的Tokenizer直接加载,如下:

RobertaTokenizer(vocab_file='vocab.json',merges_file='merges.txt')
  • 1

Robert中先将输入的所有tokens转化为merges.txt中对应的byte,再通过vocab.json中的字典进行byte到索引的转化。

  • 对于大多数bert而言,其实只需要vocab即可,所以这里有一种更简单的方式来实现试用自己语料的Tokenizer.
import tokenizers
# 创建分词器
bwpt = tokenizers.BertWordPieceTokenizer()
filepath = "../excel2txt.txt" # 语料文件
#训练分词器
bwpt.train(
    files=[filepath],
    vocab_size=50000, # 这里预设定的词语大小不是很重要
    min_frequency=1,
    limit_alphabet=1000
)
# 保存训练后的模型词表
bwpt.save_model('./pretrained_models/')
#output: ['./pretrained_models/vocab.txt']

# 加载刚刚训练的tokenizer
tokenizer=BertTokenizer(vocab_file='./pretrained_models/vocab.txt')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

上述方法是最简单也是最常见的方式,如果你已经有了词汇表,甚至不需要去训练分词,可以直接用最后一句代码,加载你的词汇表,得到对应的tokenizer,里面默认添加了cls等特殊词。
通过tokenizer能得到对应的input_id,attention_mask等,其中input_id是最重要的,其他不一定需要。如下图Fig2:
在这里插入图片描述

Fig2 tokenizer转换(将输入文本转化成对应的id)

三、模型

Transformers提供了多种的bert模型接口用于使用,我们只需下载对应的模型配置和权重即可。甚至不需要自己手动下载,程序会自动下载对应模型权重和配置,这在资料④中写的非常清楚。 需要注意的是,如果我们自己重新训练了tokenizer,需要更改model的embedding矩阵,比如之前的embedding矩阵是213000x678,而我们的词汇大小只有863,所以embedding的大小要改成863x678,即可,这个可以在config中更改,模型也提供了更改的属性方法。如下面的代码例子:

from transformers import (
    CONFIG_MAPPING,MODEL_FOR_MASKED_LM_MAPPING, AutoConfig,
    AutoModelForMaskedLM,
    AutoTokenizer,DataCollatorForLanguageModeling,HfArgumentParser,Trainer,TrainingArguments,set_seed,
)
# 自己修改部分配置参数
config_kwargs = {
    "cache_dir": None,
    "revision": 'main',
    "use_auth_token": None,
#      "hidden_size": 512,
#     "num_attention_heads": 4,
    "hidden_dropout_prob": 0.2,
#     "vocab_size": 863 # 自己设置词汇大小
}
# 将模型的配置参数载入
config = AutoConfig.from_pretrained('./tmp/bert-base-case/', **config_kwargs)
# 载入预训练模型
model = AutoModelForMaskedLM.from_pretrained(
            '../tmp/bert-base-case/',
            from_tf=bool(".ckpt" in 'roberta-base'), # 支持tf的权重
            config=config,
            cache_dir=None, 
            revision='main',
            use_auth_token=None,
        )
model.resize_token_embeddings(len(tokenizer))
#output:Embedding(863, 768, padding_idx=1)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

训练自己的预训练模型

通过准备语料,到训练语料得到tokenizer,再到模型的加载,经过这几步,我们就可以预训练我们自己的bert了,预训练采用的是MLM模型,在训练之前,需要制作好训练的数据集,这里根据资料⑤,总结了数据生成器的制作代码如下。

from datasets import load_dataset,Dataset
# data_files接收字典或者list,值为语料路径
datasets=load_dataset('text',data_files={'train':'./tmp/all_data_txt.txt',"validation":"./tmp/all_data_txt.txt"})
column_names = datasets["train"].column_names
text_column_name = "text" if "text" in column_names else column_names[0]
# 将我们刚刚加载好的datasets ,通过tokenizer做映射,得到input_id,也就是实际输入模型的东西。
def tokenize_function(examples):
    # Remove empty lines
    examples["text"] = [line for line in examples["text"] if len(line) > 0 and not line.isspace()]
    return tokenizer(
        examples["text"], 
        padding="max_length", # 进行填充
        truncation=True, # 进行截断
        max_length=100, # 设置句子的长度
        # We use this option because DataCollatorForLanguageModeling (see below) is more efficient when it
        # receives the `special_tokens_mask`.
        return_special_tokens_mask=True,
    )
tokenized_datasets = datasets.map(
    tokenize_function,
    batched=True,
    num_proc=None,
    remove_columns=[text_column_name],
    load_from_cache_file=False,
)
# 得到训练集和验证集
train_dataset = tokenized_datasets["train"]
eval_dataset = tokenized_datasets["validation"]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

Transformer还提供了更方便的数据迭代器接口LineByLineTextDataset,要求语料文本的每行表示一句话。也就是我们语料数据准备阶段建议的格式形式。
这里以nezha模型为例,采用MLM的方式预训练自己的语料的完整代码:
模型下载:NeZha_Chinese_PyTorch

import os
import csv
from transformers import  BertTokenizer, WEIGHTS_NAME,TrainingArguments
from model.modeling_nezha import NeZhaForSequenceClassification,NeZhaForMaskedLM
from model.configuration_nezha import NeZhaConfig
import tokenizers
import torch
from datasets import load_dataset,Dataset 
from transformers import (
    CONFIG_MAPPING,
    MODEL_FOR_MASKED_LM_MAPPING,
    AutoConfig,
    AutoModelForMaskedLM,
    AutoTokenizer,
    DataCollatorForLanguageModeling,
    HfArgumentParser,
    Trainer,
    TrainingArguments,
    set_seed,
    LineByLineTextDataset
)
## 制作自己的tokenizer
bwpt = tokenizers.BertWordPieceTokenizer()
filepath = "../excel2txt.txt" # 和本文第一部分的语料格式一致
bwpt.train(
    files=[filepath],
    vocab_size=50000,
    min_frequency=1,
    limit_alphabet=1000
)
bwpt.save_model('./pretrained_models/') # 得到vocab.txt

## 加载tokenizer和模型
model_path='../tmp/nezha/'
token_path='./pretrained_models/vocab.txt'
tokenizer =  BertTokenizer.from_pretrained(token_path, do_lower_case=True)
config=NeZhaConfig.from_pretrained(model_path)
model=NeZhaForMaskedLM.from_pretrained(model_path, config=config)
model.resize_token_embeddings(len(tokenizer))

# 通过LineByLineTextDataset接口 加载数据 #长度设置为128, # 这里file_path于本文第一部分的语料格式一致
train_dataset=LineByLineTextDataset(tokenizer=tokenizer,file_path='../tmp/all_data_txt.txt',block_size=128) 
# MLM模型的数据DataCollator
data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=True, mlm_probability=0.15)
# 训练参数
pretrain_batch_size=64
num_train_epochs=300
training_args = TrainingArguments(
    output_dir='./outputs/', overwrite_output_dir=True, num_train_epochs=num_train_epochs, learning_rate=6e-5,
    per_device_train_batch_size=pretrain_batch_size,, save_total_limit=10)# save_steps=10000
# 通过Trainer接口训练模型
trainer = Trainer(
    model=model, args=training_args, data_collator=data_collator, train_dataset=train_dataset)

# 开始训练
trainer.train(True)
trainer.save_model('./outputs/')

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58

Fine-tuning

至此,自己训练的bert模型算是完成了,此后在加载自己训练和保存好的模型即可,然后进行各种下游任务的fine-tuning即可即可。加载的方式可以使用.from_pretrained()的方式。
封装自己的下游任务,和用torch写自己的nn.Model类似。这里同样给出了一个例子:
详细可以参考这里:Fine-tuningclass MyModel(nn.Module)部分。
也可以参考NeZha_Chinese_PyTorch,model部分的modeling_nezha.py代码,里面提供了nezha对多种下游任务的封装模型。
这是个多分类的示例:

class MyModel(nn.Module):
    def __init__(self, freeze_bert=False, model_name='bert-base-chinese', hidden_size=768, num_classes=2):
        super(MyModel, self).__init__()
        # 返回输出的隐藏层
        self.bert = AutoModel.from_pretrained(model_name, output_hidden_states=True, return_dict=True)

        if freeze_bert:
            for p in self.bert.parameters():
                p.requires_grad = False

        self.fc = nn.Sequential(
            nn.Dropout(p=0.5),
            nn.Linear(hidden_size*4, num_classes, bias=False),
        )
  
    def forward(self, input_ids, attn_masks, token_type_ids):
    	#其实只需要input_ids也可以
        outputs = self.bert(input_ids, token_type_ids=token_type_ids, attention_mask=attn_masks)
        # 将最后输入的隐藏层后四个进行拼接
        hidden_states = torch.cat(tuple([outputs.hidden_states[i] for i in [-1, -2, -3, -4]]), dim=-1) # [bs, seq_len, hidden_dim*4]
        first_hidden_states = hidden_states[:, 0, :] # [bs, hidden_dim*4] # 这里取的是cls的embedding表示,
        #它代表了一句话的embedding
        logits = self.fc(first_hidden_states)
        return logits
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

bert的返回部分,可以在资料④中查询,这里贴出了对返回值的说明,可以根据需要自己设置返回模型的哪些东西。

在这里插入图片描述

Fig3 bert模型返回值说明

  • transformers同时支持tf于torch两种深度学习框架,这里有个采用tf.keras封装训练的例子,感觉用keras训练就更方便了,可以参考下。Transformers的tf.keras训练

总结

在比赛过程中学习了不少NLP的知识,比如NLP常用的FGM对抗训练,以及在多标签任务中,一些新颖的角度去做,如用seq2seq模型,softmax+BE 等方式,在训练方式上,可以采用LookAhead,Adamw,Warmup等。
这些trick能有效防止模型过拟合,提高模型预测的稳定性和准确性,在这次比赛中都取得了不错的效果。
比赛结束后,笔者也会将训练的代码放在Wisley 的 GitHub上,以及我自己在比赛中使用和尝试的一些trick,以便大家和自己日后学习和参考。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/249562
推荐阅读
相关标签
  

闽ICP备14008679号