数据集是中文的酒店评论,共有 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'(.)+', r'', 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('我以前去过其他的酒店,服务特别好,但是在这家酒店就差很多')
预测结果:
输入: 酒店设施还可以,总体我觉得不错 预测: 好评 输入: 什么垃圾地方,连个矿泉水都没有,下次再也不来了 预测: 差评 输入: 一进酒店,有简单的装饰,中式的简单装修,进入标间,两张床,白色的床单,必备的台灯,还有塑料拖鞋。 预测: 差评 输入: 非常安静,非常赞地理位置:离地铁站很近很方便,前台小姐姐,服务态度很好,很贴心 预测: 好评 输入: 我以前去过其他的酒店,服务特别好,但是在这家酒店就差很多 预测: 差评

冀公网安备13050302001966号