① 将“softmax+交叉熵”推广到多标签分类问题
多分类问题引申到多标签分类问题(softmax+交叉熵)
作者苏剑林论述了将多分类任务下常用的softmax+CE的方式,推广到多标签任务,意图解决标签不均衡带来的一些问题。
② SGM,多标签分类的序列生成模型 将序列生成模型,运用到多标签分类任务上,序列生成模型,典型的就是seq2seq模型。
③ 关于transformers库中不同模型的Tokenizer说明
④ transformers手册
Transfomers Github
⑤ datasets手册
⑥ 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,以便大家和自己日后学习和参考。