数据集是中文的酒店评论,共有 50216 + 12555 条评论,前者是训练集,后者是验证集。clean_data 函数是对评论做的一些简单的处理。train_data 的数据对象为:
DatasetDict({ train: Dataset({ features: ['review', '__index_level_0__', 'labels', 'input_ids', 'token_type_ids', 'attention_mask'], num_rows: 50216 }) valid: Dataset({ features: ['review', '__index_level_0__', 'labels', 'input_ids', 'token_type_ids', 'attention_mask'], num_rows: 12555 }) })
我们最终会将数据序列化到 data/senti-dataset 文件中,数据文件结构如下:
senti-dataset/ ├── dataset_dict.json ├── test │ ├── dataset.arrow │ ├── dataset_info.json │ └── state.json ├── train │ ├── dataset.arrow │ ├── dataset_info.json │ └── state.json └── valid ├── dataset.arrow ├── dataset_info.json └── state.json
完整代码如下:
from datasets import load_dataset from datasets import Dataset import pandas as pd import zhconv import re from transformers import BertTokenizer def clean_data(inputs: str): # 繁体转简体 inputs = zhconv.convert(inputs, 'zh-hans') # 大写转小写 inputs = inputs.lower() # 删除 "免费注册 网站导航..." start = inputs.find('免费注册') if start != -1: inputs = inputs[:start] # 去除除了中文、数字、字母、逗号、句号、问号 # inputs = inputs.replace(',', ',') # inputs = inputs.replace('!', '!') # inputs = inputs.replace('?', '?') # inputs = inputs.replace('.', '。') inputs = re.sub(r'[^\u4e00-\u9fa50-9a-z]', ' ', inputs) # 替换连续重复 inputs = re.sub(r'(.)\1+', r'\1', inputs) # 去除多余空格 inputs = ' '.join(inputs.split()) return inputs def preprocess(): tokenizer = BertTokenizer.from_pretrained('bert-base-chinese') train_data = pd.read_csv('data/online_shopping_10_cats.csv') train_data = train_data[['label', 'review']] train_data = train_data.dropna() # DataFrame 转换为 Dataset 对象 train_data = Dataset.from_pandas(train_data) # 处理数据 # 注意: Trainer 要求输入的数据带有 labels 标签 train_data = train_data.map(lambda x: {'labels': x['label'], 'review': clean_data(x['review'])}) # 过滤空数据 train_data = train_data.filter(lambda x: len(x['review']) != 0) # map 函数会将返回的字典并到原来的字典中 train_data = train_data.map( lambda x: tokenizer(x['review'], truncation=True, padding='max_length', max_length=256), batched=True) # 删除某些列 train_data = train_data.remove_columns(['label']) # 分割数据集 train_data = train_data.train_test_split(test_size=0.2) train_data['valid'] = train_data.pop('test') print(train_data) # 存储数据 train_data.save_to_disk('data/senti-dataset') if __name__ == '__main__': preprocess()
我们在 bert-base-chinese 中文预训练模型的基础上进行微调,以适应在新的数据集-中文酒店评论上进行文本分类。在这里我们使用 Train 类来完成中文评论分类模型的训练。我们训练时,只训练下游任务的参数部分。
from datasets import load_from_disk from transformers import BertForSequenceClassification from transformers import TrainingArguments from transformers import Trainer from sklearn.metrics import accuracy_score, f1_score, recall_score, precision_score from sklearn.metrics import precision_recall_fscore_support
1. TrainingArguments
Trainer 要求我们传递一些训练相关的参数,即: 构建 TrainingArguments 对象,其具体参数如下:
- output_dir 参数为必须添加的参数,它主要指定了模型训练过程中需要输出的内容,例如:保存 checkpoint 的路径
- per_device_train_batch_size 参数设置 batch_size 大小,由于 Bert 模型很大,如果在 GPU 训练时出现 Cuda 内存分配失败,则应该相应调低该值,我的显卡为 RTX 2060,显存大小为 6G,在训练时就报错了,所以,我把这个参数设置为 2
- overwrite_output_dir 参数表示覆盖输出目录中的内容
- num_train_epochs 参数表示训练的 Epoch 数量
- learning_rate 这个不用说了,设置学习率
- eval_steps 参数表示每次训练多少步时,使用设置的验证集进行一次评估。我这里设置的是 10000,这是因为我的总的迭代次数是 19 万多,设置太小,评估过于频繁了,这个根据自己情况设置就好
- save_steps 参数表示训练迭代多少次,也就是训练多少个 batch 保存一次模型
- logging_dir 表示日志的输出目录
- metric_for_best_model 因为我们在训练过程中会输出多个 checkpoint,这个参数用来指定评估最优模型的指标是什么,我们这里使用的时 precision,如果不指定的话,默认是根据 loss 值来评定最优模型
2. Trainer
Trainer 是 Transformers 封装的用于模型训练的高级 API,可以帮助我们快速搭建训练过程。训练机器配置为:centos7.8 + torch1.8.2 + cuda11 + 10代i5 + 16G固态内存 + RTX 2060 显卡,训练时间大概在 3 小时 30 分钟左右。
我们设置的训练参数如下:
- 训练轮数:20
- 每 2000 次迭代存储一次模型
- 初始学习率设置为 2e-5
- 优化方法使用的是 AdamW
- 每次迭代送入模型的数据是 32 个
- 每次迭代更新一次梯度
- 模型存储目录为当前的 model 目录下
完整训练代码如下:
def train(): # 1. 读取数据集 data = load_from_disk('data/senti-dataset') # 将数据类型转换为张量 data.set_format('pytorch', columns=['labels', 'input_ids', 'token_type_ids', 'attention_mask']) # 训练集 train_data = data['train'].remove_columns(['review', '__index_level_0__']) # 验证集 # valid_data = data['valid'].remove_columns(['review', '__index_level_0__']) # 2. 训练器 train_model = BertForSequenceClassification.from_pretrained( 'bert-base-chinese', num_labels=2) train_model.to('cuda') # 固定基础编码器的参数 for param in train_model.base_model.parameters(): param.requires_grad = False train_args = TrainingArguments(output_dir='model', overwrite_output_dir=True, save_steps=2000, per_device_train_batch_size=32, num_train_epochs=20, learning_rate=2e-5, evaluation_strategy='no', # 训练期间不进行评估 # eval_steps=1000, logging_strategy='epoch', logging_dir='logs', gradient_accumulation_steps=1, # 每n个step更新一次参数 metric_for_best_model='accuracy') trainer = Trainer(train_model, train_args, train_dataset=train_data) # 训练模型 trainer.train() # 存储模型 trainer.save_model('model/checkpoint-final')
训练过程输出如下:
******************* Running training ******************* Num examples = 50216 Num Epochs = 20 Instantaneous batch size per device = 32 Total train batch size (w. parallel, distributed & accumulation) = 32 Gradient Accumulation steps = 1 Total optimization steps = 31400 ******************************************************* {'loss': 0.5807, 'learning_rate': 1.9e-05, 'epoch': 1.0} {'loss': 0.4634, 'learning_rate': 1.8e-05, 'epoch': 2.0} {'loss': 0.4168, 'learning_rate': 1.7e-05, 'epoch': 3.0} {'loss': 0.3925, 'learning_rate': 1.6e-05, 'epoch': 4.0} {'loss': 0.3779, 'learning_rate': 1.5e-05, 'epoch': 5.0} {'loss': 0.366, 'learning_rate': 1e-05, 'epoch': 6.0} {'loss': 0.3604, 'learning_rate': 1e-05, 'epoch': 7.0} {'loss': 0.355, 'learning_rate': 1e-05, 'epoch': 8.0} {'loss': 0.3507, 'learning_rate': 1e-05, 'epoch': 9.0} {'loss': 0.3486, 'learning_rate': 1e-05, 'epoch': 10.0} {'loss': 0.344, 'learning_rate': 9e-06, 'epoch': 11.0} {'loss': 0.3427, 'learning_rate': 8e-06, 'epoch': 12.0} {'loss': 0.341, 'learning_rate': 7e-06, 'epoch': 13.0} {'loss': 0.3393, 'learning_rate': 6e-06, 'epoch': 14.0} {'loss': 0.3404, 'learning_rate': 5e-06, 'epoch': 15.0} {'loss': 0.3395, 'learning_rate': 4e-06, 'epoch': 16.0} {'loss': 0.3386, 'learning_rate': 3e-06, 'epoch': 17.0} {'loss': 0.3367, 'learning_rate': 2e-06, 'epoch': 18.0} {'loss': 0.3361, 'learning_rate': 1e-06, 'epoch': 19.0} {'loss': 0.34, 'learning_rate': 0.0, 'epoch': 20.0}
3. 训练日志
另外,模型训练结束后会在 logging_dir 目录下输出 events.out.tfevents文件,该文件可以通过 tensorboard 可视化工具来查看输出日志,命令为:
tensorboard --logdir=./logs --port=8008
我们在前面的训练过程中,共产生多个 checkpoint, 分别如下:
checkpoint-10000 checkpoint-18000 checkpoint-24000 checkpoint-4000 checkpoint-12000 checkpoint-2000 checkpoint-26000 checkpoint-6000 checkpoint-14000 checkpoint-20000 checkpoint-28000 checkpoint-8000 checkpoint-16000 checkpoint-22000 checkpoint-30000 checkpoint-final
我们接下来,使用测试集分别在不同的 checkpoint 下去评估下模型的准确率、精度、召回率,还有 f1-score。
1. 模型评估
在我电脑上,使用所有的 12555 测试集评估一次大概需要 2 分钟多,我们共有 16 个模型,评估所有的模型需要 35 分钟左右。
完成模型评估代码如下:
def evaluate(model_path, valid_data): # 评估模型 test_model = BertForSequenceClassification.from_pretrained(model_path, num_labels=2) test_model.to('cuda') with torch.no_grad(): all_y_true = [] all_y_pred = [] def eval(inputs): outputs = test_model(**inputs) y_true = inputs['labels'] y_pred = torch.argmax(outputs.logits, dim=-1) all_y_true.extend(y_true.cpu().numpy().tolist()) all_y_pred.extend(y_pred.cpu().numpy().tolist()) valid_data.map(eval, batched=True, batch_size=32) # 评估预测结果 accuracy = accuracy_score(all_y_true, all_y_pred) precis_0 = precision_score(all_y_true, all_y_pred, pos_label=0) precis_1 = precision_score(all_y_true, all_y_pred, pos_label=1) recall_0 = recall_score(all_y_true, all_y_pred, pos_label=0) recall_1 = recall_score(all_y_true, all_y_pred, pos_label=1) fscore_0 = f1_score(all_y_true, all_y_pred, pos_label=0) fscore_1 = f1_score(all_y_true, all_y_pred, pos_label=1) print('------测试集报告------') print('accuracy: %.5f' % accuracy) print('--------------------') print('precis_0: %.5f' % precis_0) print('precis_1: %.5f' % precis_1) print('--------------------') print('recall_0: %.5f' % recall_0) print('recall_1: %.5f' % recall_1) print('--------------------') print('fscore_0: %.5f' % fscore_0) print('fscore_1: %.5f' % fscore_1) print('--------------------') def evaluate_models(): data = load_from_disk('data/senti-dataset') data.set_format('pytorch', columns=['labels', 'input_ids', 'token_type_ids', 'attention_mask'], device='cuda') valid_data = data['valid'].remove_columns(['review', '__index_level_0__']) print('数据总量:', valid_data.num_rows) # 1. 获得模型路径 model_path_list = glob.glob('model/*') for model_path in model_path_list: model_name = model_path.replace('model/', '') print('-' * 20 + model_name + '-' * 20) evaluate(model_path, valid_data)
评估函数输出结果如下:
accuracy: 0.86874 -------------------- precis_0: 0.86322 precis_1: 0.87424 -------------------- recall_0: 0.87268 recall_1: 0.86488 -------------------- fscore_0: 0.86793 fscore_1: 0.86954 -------------------- --------------------checkpoint-8000-------------------- accuracy: 0.87360 -------------------- precis_0: 0.86443 precis_1: 0.88294 -------------------- recall_0: 0.88268 recall_1: 0.86472 -------------------- fscore_0: 0.87346 fscore_1: 0.87374 -------------------- --------------------checkpoint-10000-------------------- accuracy: 0.87686 -------------------- precis_0: 0.86737 precis_1: 0.88655 -------------------- recall_0: 0.88638 recall_1: 0.86756 -------------------- fscore_0: 0.87677 fscore_1: 0.87695 -------------------- --------------------checkpoint-12000-------------------- accuracy: 0.87814 -------------------- precis_0: 0.87215 precis_1: 0.88412 -------------------- recall_0: 0.88284 recall_1: 0.87354 -------------------- fscore_0: 0.87746 fscore_1: 0.87880 -------------------- --------------------checkpoint-14000-------------------- accuracy: 0.88037 -------------------- precis_0: 0.87308 precis_1: 0.88772 -------------------- recall_0: 0.88687 recall_1: 0.87402 -------------------- fscore_0: 0.87992 fscore_1: 0.88081 -------------------- --------------------checkpoint-16000-------------------- accuracy: 0.88220 -------------------- precis_0: 0.87224 precis_1: 0.89238 -------------------- recall_0: 0.89234 recall_1: 0.87228 -------------------- fscore_0: 0.88218 fscore_1: 0.88222 -------------------- --------------------checkpoint-18000-------------------- accuracy: 0.88284 -------------------- precis_0: 0.87631 precis_1: 0.88939 -------------------- recall_0: 0.88832 recall_1: 0.87748 -------------------- fscore_0: 0.88227 fscore_1: 0.88339 -------------------- --------------------checkpoint-20000-------------------- accuracy: 0.88371 -------------------- precis_0: 0.87677 precis_1: 0.89070 -------------------- recall_0: 0.88977 recall_1: 0.87780 -------------------- fscore_0: 0.88322 fscore_1: 0.88420 -------------------- --------------------checkpoint-22000-------------------- accuracy: 0.88491 -------------------- precis_0: 0.87363 precis_1: 0.89652 -------------------- recall_0: 0.89686 recall_1: 0.87323 -------------------- fscore_0: 0.88509 fscore_1: 0.88472 -------------------- --------------------checkpoint-24000-------------------- accuracy: 0.88586 -------------------- precis_0: 0.87622 precis_1: 0.89570 -------------------- recall_0: 0.89557 recall_1: 0.87638 -------------------- fscore_0: 0.88579 fscore_1: 0.88593 -------------------- --------------------checkpoint-26000-------------------- accuracy: 0.88515 -------------------- precis_0: 0.87322 precis_1: 0.89747 -------------------- recall_0: 0.89799 recall_1: 0.87260 -------------------- fscore_0: 0.88543 fscore_1: 0.88486 -------------------- --------------------checkpoint-28000-------------------- accuracy: 0.88530 -------------------- precis_0: 0.87163 precis_1: 0.89958 -------------------- recall_0: 0.90056 recall_1: 0.87039 -------------------- fscore_0: 0.88586 fscore_1: 0.88474 -------------------- --------------------checkpoint-30000-------------------- accuracy: 0.88546 -------------------- precis_0: 0.87318 precis_1: 0.89818 -------------------- recall_0: 0.89879 recall_1: 0.87244 -------------------- fscore_0: 0.88580 fscore_1: 0.88513 -------------------- --------------------checkpoint-final-------------------- accuracy: 0.88530 -------------------- precis_0: 0.87349 precis_1: 0.89751 -------------------- recall_0: 0.89799 recall_1: 0.87291 -------------------- fscore_0: 0.88557 fscore_1: 0.88504 --------------------
2. 模型预测
在评估完整之后,我们选择在测试集上相对较好的 checkpoint-24000,随机输入一些评论,来查看预测的结果。
def predict(text): # 任务模型 test_model = BertForSequenceClassification.from_pretrained('model/checkpoint-24000', num_labels=2) # test_model.to('cuda') # 分词器 tokenizer = BertTokenizer.from_pretrained('bert-base-chinese') # 数据处理 inputs = clean_data(text) # 数据标注 inputs = tokenizer(inputs, truncation=True, max_length=256, padding='max_length', return_tensors='pt') # 模型计算 outputs = test_model(**inputs) # 计算结果 y_pred = torch.argmax(outputs.logits, dim=-1) print('输入:', text) print('预测:', '好评' if y_pred.item() == 1 else '差评') if __name__ == '__main__': predict('酒店设施还可以,总体我觉得不错') predict('什么垃圾地方,连个矿泉水都没有,下次再也不来了') predict('一进酒店,有简单的装饰,中式的简单装修,进入标间,两张床,白色的床单,必备的台灯,还有塑料拖鞋。') predict('非常安静,非常赞地理位置:离地铁站很近很方便,前台小姐姐,服务态度很好,很贴心') predict('我以前去过其他的酒店,服务特别好,但是在这家酒店就差很多')
预测结果:
输入: 酒店设施还可以,总体我觉得不错 预测: 好评 输入: 什么垃圾地方,连个矿泉水都没有,下次再也不来了 预测: 差评 输入: 一进酒店,有简单的装饰,中式的简单装修,进入标间,两张床,白色的床单,必备的台灯,还有塑料拖鞋。 预测: 差评 输入: 非常安静,非常赞地理位置:离地铁站很近很方便,前台小姐姐,服务态度很好,很贴心 预测: 好评 输入: 我以前去过其他的酒店,服务特别好,但是在这家酒店就差很多 预测: 差评