赞
踩
① 将“softmax+交叉熵”推广到多标签分类问题
多分类问题引申到多标签分类问题(softmax+交叉熵)
作者苏剑林论述了将多分类任务下常用的softmax+CE的方式,推广到多标签任务,意图解决标签不均衡带来的一些问题。
② SGM,多标签分类的序列生成模型 将序列生成模型,运用到多标签分类任务上,序列生成模型,典型的就是seq2seq模型。
⑥ FGM对抗训练
最近参加某个NLP的比赛,因为NLP的问题接触的不是很多,所以在比赛过程中学习不少新知识和trick,在这里稍微记录以下,比赛用的是脱敏的数据,需要我们自己重新预训练模型,当然,用W2V,GLOVE等作为预训练模型+deep模型,也能达到还不错的成绩,但是上限不如bert高。
这里重点说下如何用huggingface的Transformers训练自己的模型,虽然官方是给了手册和教程的,但是大多是基于已有的预训练模型,但如何适用自己的语料重新训练自己的bert模型相关资料较少,这里自己实践后的过程记录下。
训练自己的bert模型,需要现在准备三样东西,分别是 语料(数据),分词器,模型。
用于训练bert模型的语料数据,常见的大规模数据在datasets里面都可以直接下载并加载,详细请参考资料⑤。对于自己的语料数据,或者脱敏的数据,我们需要自己处理下,这里建议每一行作为一个句子,词与词之间最好有分隔符。如下图Fig1所示:
Tonkenizer的作用,除了对文本进行分词外,还将每个词与相应的input_id对应,同时添加句子分隔符、mask掩码等,在Transformer中已经封装了常见的bert模型使用的分词器,如BertTokenizer,RobertaTokenizer等,可以直接使用,对文本进行分词并转化为对应的input_id,这里的id是与bert中embedding矩阵的索引号一一对应的。不同模型的Tokenizer略微有所区别,如下是加载预训练模型的tokenizer代码,详细可见资料③中的描述。
tokenizer = AutoTokenizer.from_pretrained('roberta-base')
如果我们的语料很小,想重新训练模型的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/')
上述的训练过程其实是针对的是byte级别的BPE编码,tokenizer保存为两个文件,分别是vocab.json
, merges.txt
,这个在Robert中会用到,,普通的bert只需要vocab。这两个文件,可以用官方封装好的Tokenizer直接加载,如下:
RobertaTokenizer(vocab_file='vocab.json',merges_file='merges.txt')
Robert中先将输入的所有tokens转化为merges.txt中对应的byte,再通过vocab.json中的字典进行byte到索引的转化。
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')
上述方法是最简单也是最常见的方式,如果你已经有了词汇表,甚至不需要去训练分词,可以直接用最后一句代码,加载你的词汇表,得到对应的tokenizer,里面默认添加了cls等特殊词。
通过tokenizer能得到对应的input_id,attention_mask等,其中input_id是最重要的,其他不一定需要。如下图Fig2:
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)
通过准备语料,到训练语料得到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"]
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/')
至此,自己训练的bert模型算是完成了,此后在加载自己训练和保存好的模型即可,然后进行各种下游任务的fine-tuning即可即可。加载的方式可以使用.from_pretrained()的方式。
封装自己的下游任务,和用torch写自己的nn.Model类似。这里同样给出了一个例子:
详细可以参考这里:Fine-tuning 的class 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
bert的返回部分,可以在资料④中查询,这里贴出了对返回值的说明,可以根据需要自己设置返回模型的哪些东西。
在比赛过程中学习了不少NLP的知识,比如NLP常用的FGM对抗训练,以及在多标签任务中,一些新颖的角度去做,如用seq2seq模型,softmax+BE 等方式,在训练方式上,可以采用LookAhead,Adamw,Warmup等。
这些trick能有效防止模型过拟合,提高模型预测的稳定性和准确性,在这次比赛中都取得了不错的效果。
比赛结束后,笔者也会将训练的代码放在Wisley 的 GitHub上,以及我自己在比赛中使用和尝试的一些trick,以便大家和自己日后学习和参考。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。