Task4-基于深度学习的文本分类3-基于Bert预训练和微调进行文本分类
因为天池这个比赛的数据集是脱敏的,无法利用其它已经预训练好的模型,所以需要针对这个数据集自己从头预训练一个模型。
我们利用Huggingface的transformer包,按照自己的需求从头开始预训练一个模型,然后将该模型应用于下游任务。
完整代码见:NLP-hands-on/天池-零基础入门NLP at main · ifwind/NLP-hands-on (github.com)
注意:利用Huggingface做预训练需要安装wandb包,如果报错可参考 :[wandb.errors.UsageError: api_key not configured (no-tty). call wandb.login(key=[your_api_key\])_](https://blog.csdn.net/hhhhhhhhhhwwwwwwwwww/article/details/116124285)
预训练模型
利用Huggingface的transformer包进行预训练主要包括以下几个步骤:
用数据集训练Tokenizer;
加载数据及数据预处理;
设定预训练模型参数,初始化预训练模型;
设定训练参数,加载训练器;
训练并保存模型。
用数据集训练Tokenizer
Tokenizer是分词器,分词方式有很多种,可以按照空格直接切分、也可以在按词组划分等,可以查看HuggingFace关于tokenizers 的官方文档。
Huggingface中,Tokenizer的训练方式为:
根据tokenizers.models
实例化一个Tokenizer
对象tokenizer
,
从tokenizers.trainers
中选模型相应的训练器实例化,得到trainer
;
从tokenizers.pre_tokenizers
选定一个预训练分词器,对tokenizer
的预训练分词器实例化;
利用tokenizer.train()
结合trainer
对语料(注意,语料为一行一句 )进行训练;
利用tokenizer.save()
保存tokenizer
。
因为天池这个比赛的数据集是脱敏的,词都是用数字进行表示,没有办法训练wordpiece等复杂形式的分词器,只能用空格分隔,在wordlevel进行分词。
因此,我们利用tokenizers.models
中的WordLevel
模型,对应tokenizers.trainers
中的WordLevelTrainer
,选择预训练分词器为Whitespace
训练分词器。
另外,在训练Tokenizer时,可以利用上全部的语料(包括训练集和最终的测试集)。
完整代码如下:
import joblibfrom config import *import pandas as pdfrom sklearn.model_selection import train_test_splitfrom sklearn.model_selection import StratifiedKFoldimport osdef data_preprocess (): rawdata = pd.read_csv(data_file, sep='\t' , encoding='UTF-8' ) import re rawdata['words' ]=rawdata['text' ].apply(lambda x: re.sub('3750|900|648' ,"" ,x)) del rawdata['text' ] final_test_data = pd.read_csv(final_test_data_file, sep='\t' , encoding='UTF-8' ) final_test_data['words' ] = final_test_data['text' ].apply(lambda x: re.sub('3750|900|648' ,"" ,x)) del final_test_data['text' ] all_value= rawdata['words' ].append(final_test_data['words' ]) all_value.columns=['text' ] all_value.to_csv('../alldata.csv' ,index=False ) data_preprocess() from tokenizers import Tokenizerfrom tokenizers.models import BPE,WordLeveltokenizer= Tokenizer(WordLevel(unk_token="[UNK]" )) from tokenizers.trainers import BpeTrainer,WordLevelTrainertrainer = WordLevelTrainer(special_tokens=["[UNK]" , "[CLS]" , "[SEP]" , "[PAD]" , "[MASK]" ]) from tokenizers.pre_tokenizers import Whitespacetokenizer.pre_tokenizer = Whitespace() tokenizer.train(['../alldata.csv' ], trainer) tokenizer.mask_token='[MASK]' tokenizer.save("../tokenizer-my-Whitespace.json" )
加载数据及数据预处理
在预训练模型时,利用的是普通的、不带标签的句子,因此,在预训练模型时,同样采用全部的语料(包括训练集和最终的测试集,也就是前面的alldata.csv
)。
使用Huggingface的datasets包的load_dataset函数加载数据,这里用的是csv的数据格式,关于其他格式的数据可以参考之前的一篇博客:加载数据 。
输入模型之前,还需要对句子进行分词,转换成word id,此外,还需要padding长度不足的句子、获取对padding部分掩码的矩阵等等操作,这些由tokenizer进行处理,如果还需要其他预处理操作,都可以通过定义一个函数,将预处理操作封装起来,然后利用dataset.map()进行处理。
另外我们还在这里实例化了一个数据收集器data_collator ,将经预处理的输入分batch再次处理后喂给模型。这是为了告诉Trainer
如何从预处理的输入数据中构造batch。
from transformers import PreTrainedTokenizerFasttokenizer = PreTrainedTokenizerFast(tokenizer_file="tokenizer-my-Whitespace.json" ) tokenizer.mask_token='[MASK]' tokenizer.pad_token='[PAD]' from datasets import load_datasetdataset = load_dataset('csv' , data_files={'train' :'alldata.csv' },cache_dir='Bert_pre_train\\' ) def preprocess_function (examples ): return tokenizer(examples['text' ], truncation=True ,max_length=512 ) encoded_dataset = dataset.map (preprocess_function, batched=True ) from transformers import DataCollatorForLanguageModelingdata_collator = DataCollatorForLanguageModeling( tokenizer=tokenizer, mlm=True , mlm_probability=0.15 )
设定预训练模型参数,初始化预训练模型
这里选择了XLNet模型作为预训练模型的基础架构,然后根据调整模型的参数,也可以选 Roberta、ALBert等模型,参考官方文档 进行配置。
from transformers import RobertaConfig,AlbertConfig,XLNetConfigconfig_kwargs = { "d_model" : 512 , "n_head" : 4 , "vocab_size" : tokenizer.get_vocab_size(), "embedding_size" :64 , "bi_data" :True , "n_layer" :8 } config = XLNetConfig(**config_kwargs) from transformers import RobertaForMaskedLM,AlbertForMaskedLM,XLNetLMHeadModelmodel = XLNetLMHeadModel(config=config)
设定训练参数,加载训练器
from transformers import Trainer, TrainingArgumentstraining_args = TrainingArguments( output_dir="./BERT" , overwrite_output_dir=True , num_train_epochs=1 , per_gpu_train_batch_size=12 , save_steps=10_000 , save_total_limit=2 , prediction_loss_only=True , ) trainer = Trainer( model=model, args=training_args, data_collator=data_collator, train_dataset=encoded_dataset["train" ] )
训练并保存模型
trainer.train() trainer.save_model("./BERT" )
微调模型进行分类
主要包括以下几个步骤:
训练集划分;
数据预处理;
加载预训练模型、设置微调参数;
微调训练下游任务模型并保存。
训练集划分
去掉可能的标点符号,并把当前竞赛给的训练集划分为三个部分:训练集、验证集、测试集 。其中,训练集用于训练,验证集用于调参,测试集用于评估线下和线上的模型效果。
这里首先用train_test_split(注意使用分层抽样)把训练集划分为训练集和测试集(9:1),然后再将训练集进一步划分为训练集和开发集(9:1)。
import joblibfrom config import *import pandas as pdfrom sklearn.model_selection import train_test_splitfrom sklearn.model_selection import StratifiedKFoldimport osdef data_preprocess (): rawdata = pd.read_csv(data_file, sep='\t' , encoding='UTF-8' ) import re rawdata['words' ]=rawdata['text' ].apply(lambda x: re.sub('3750|900|648' ,"" ,x)) del rawdata['text' ] if os.path.exists(test_index_file) and os.path.exists(train_index_file): test_index=joblib.load(test_index_file) train_index=joblib.load(train_index_file) else : rawdata.reset_index(inplace=True , drop=True ) X = list (rawdata.index) y = rawdata['label' ] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1 , stratify=y) test_index = {'X_test' : X_test, 'y_test' : y_test} joblib.dump(test_index, 'test_index.pkl' ) train_index = {'X_train' : X_train, 'y_train' : y_train} joblib.dump(train_index, 'train_index.pkl' ) train_x=rawdata.loc[train_index['X_train' ]] train_y=rawdata.loc[train_index['X_train' ]]['label' ].values X_train, X_test, y_train, y_test = train_test_split(train_x, train_y, test_size=0.1 , stratify=train_y) X_train.columns=['label' , 'text' ] X_train.to_csv('train_data.csv' ,index=False ) X_test.columns=['label' , 'text' ] X_test.to_csv('dev_data.csv' ,index=False ) test_x=rawdata.loc[test_index['X_test' ]] test_x.columns=['label' , 'text' ] test_x.to_csv('test_data.csv' ,index=False )
数据预处理
和预训练模型时一致,加载数据集后还需要对数据进行预处理,注意上一步中加入了label
这个字段,作为句子的分类标签。
Huggingface封装了模型的有效字段,如果字段名称对不上会在trainer
的self.get_train_dataloader()
去掉无效字段,有效字段可以在debug模式下,在transformer
的trainer.py
的self.args.remove_unused_columns
中查看。
from datasets import load_datasetdataset = load_dataset('csv' , data_files={'train' :'train_data.csv' , 'dev' :'dev_data.csv' , 'test' :'test_data.csv' },cache_dir='fine-tune\\' ) from transformers import PreTrainedTokenizerFasttokenizer = PreTrainedTokenizerFast(tokenizer_file="tokenizer-my-Whitespace.json" ) tokenizer.mask_token='[MASK]' tokenizer.pad_token='[PAD]' def preprocess_function (examples ): return tokenizer(examples['text' ], truncation=True ,max_length=512 ) encoded_dataset = dataset.map (preprocess_function, batched=True )
加载预训练模型
model_checkpoint = "BERT" num_labels = 14 from transformers import XLNetForSequenceClassification, TrainingArguments, Trainermodel = XLNetForSequenceClassification.from_pretrained(model_checkpoint, num_labels=num_labels)
设置微调参数
metric_name = "acc" args = TrainingArguments( "test-glue" , evaluation_strategy = "epoch" , save_strategy = "epoch" , learning_rate=2e-5 , per_device_train_batch_size=batch_size, per_device_eval_batch_size=batch_size*4 , num_train_epochs=5 , weight_decay=0.01 , load_best_model_at_end=True , metric_for_best_model=metric_name, save_steps=10_000 , save_total_limit=2 , ) from datasets import load_metricimport numpy as npdef compute_metrics (eval_pred ): metric = load_metric('f1' ) predictions, labels = eval_pred predictions = np.argmax(predictions, axis=1 ) return metric.compute(predictions=predictions, references=labels,average='macro' ) trainer = Trainer( model, args, train_dataset=encoded_dataset["train" ], eval_dataset=encoded_dataset["dev" ], tokenizer=tokenizer, compute_metrics=compute_metrics )
微调训练下游任务模型并保存
trainer.train() trainer.save_model("./test-glue" )
评估模型
这里先用之前训练过程中保存的test-glue/checkpoint-13500
模型进行评估,看一下训练效果,在预训练1个epoch的预训练模型下微调1个epoch,F1可以达到91.39左右。
model_checkpoint = "test-glue/checkpoint-13500" num_labels = 14 from transformers import XLNetForSequenceClassification, TrainingArguments, Trainermodel = XLNetForSequenceClassification.from_pretrained(model_checkpoint, num_labels=num_labels) batch_size = 12 metric_name = "acc" args = TrainingArguments( "test-glue" , evaluation_strategy = "epoch" , save_strategy = "epoch" , learning_rate=2e-5 , per_device_train_batch_size=batch_size, per_device_eval_batch_size=batch_size*4 , num_train_epochs=5 , weight_decay=0.01 , load_best_model_at_end=True , metric_for_best_model=metric_name, save_steps=10_000 , save_total_limit=2 , ) from datasets import load_metricimport numpy as npdef compute_metrics (eval_pred ): metric = load_metric('f1' ) predictions, labels = eval_pred predictions = np.argmax(predictions, axis=1 ) return metric.compute(predictions=predictions, references=labels,average='macro' ) trainer = Trainer( model, args, train_dataset=encoded_dataset["train" ], eval_dataset=encoded_dataset["dev" ], tokenizer=tokenizer, compute_metrics=compute_metrics ) trainer.evaluate()
预训练1个epoch的XLNet预训练模型+5个epoch的微调,F1线上0.9324
预训练1个epoch的XLNet预训练模型+5个epoch的XLNet+2*LSTM+Attention微调模型,F1线上0.9442
预训练5个epoch的XLNet预训练模型+5个epoch的XLNet+2*LSTM+Attention微调模型,F1线上0.9493
参考资料
ALBERT — transformers 4.11.3 documentation (huggingface.co)
BERT相关——(5)Pre-train Model | 冬于的博客 (ifwind.github.io)
BERT实战——(1)文本分类 | 冬于的博客 (ifwind.github.io)
阅读源码-理解pytorch_pretrained_bert中BertTokenizer工作方式_枪枪枪的博客-CSDN博客
NLP学习1 - 使用Huggingface Transformers框架从头训练语言模型 - 简书 (jianshu.com)