文本分类任务基本流程

文本分类任务基本流程

前言

文本来源于笔者在openmmlab工程团队的知识分享。

文本分类是自然语言处理领域的一项基本任务,它涉及将给定文本分配到一个或多个预定义的类别,根据输出的不同分为单标签多分类(一个句子对应一个标签,如某一个新闻文本属于经济新闻或娱乐新闻等)和多标签分类(一个句子对应多个标签,比如一个新闻文本既属于经济新闻又属于娱乐新闻)。本文将以单标签多分类任务为例,简单介绍一下用pytorch实现BERT中文文本分类的基本流程。

任务流程

文本分类流程

文本分类的基本流程一般是文本->文本预处理->特征抽取->分类器训练->分类器评估->预测。首先需要将文本进行清洗处理成结构化的数据,比如每一条文本处理成sentence+'\t'+label的形式,然后采用不同的word embedding方法将文本转换成向量,然后构建分类模型,将文本向量输入模型进行训练,每一个epoch后进行evaluation验证,最后进行预测。

流程相关概念介绍

词嵌入

在CV任务中,输入往往是图像,对于一个图像而言,图像本身就是由像素点矩阵构成的,RGB通道的图像更是三维的矩阵,本身就含有大量的特征信息,而且能被计算机直接理解。而NLP任务的输入是句子,计算机无法直接理解句子,所以所有nlp任务的第一步也是相当重要的一步就是将单词或者句子转化成向量,这个过程称为词嵌入(word embedding)

简单来说每一个nlp模型都需要一个词表,通过词表先将一句话转化成一个编号向量,对于中文nlp任务还涉及到一个粒度的问题,有的模型是词粒度(每一个词转化成一个编号),有的模型是字粒度(每一个字作为一个编号),词表的每一个元素称为一个token。

str2index

如图,根据不同的处理规则和词表,可以将句子转化成词表对应的下标向量(如果出现不存在的token一般会用一个UKN(unknown)token来表示),将句子转化成下标向量后,下一步是采用不同的特征提取方式将每一个token转化成一个向量,这样就可以达到将计算机无法理解的句子转化成计算机可以理解的向量的效果。根据词嵌入的原理大致可以细分成以下几种方法,具体方法原理可以查看对应的链接。

静态词嵌入是指,每一个token对应的向量是固定的,静态词向量的维度一般设置在300,即每一个token会被转变成一个300维的固定的词向量;动态词嵌入会根据词语所在的句子,通过BERT等预训练模型动态的生成词向量,比如“苹果公司推出了新产品”和“我今天吃了一个苹果”两个句子中的“苹果”通过动态词嵌入生成的向量是不一样的。动态词向量更能够学习到文本的上下文语义,极大的提升了nlp任务的效果。

预训练模型

预训练模型是一种深度学习模型,这种模型首先在大规模数据集上进行了预先训练,以学习文本或图像通用的特征或表示,然后在各种下游任务中进行微调,以适应特定的任务需求,从而极大地减少了训练时间和所需的数据量。

评价指标

混淆矩阵

confusion matrix

混淆矩阵也称误差矩阵,是表示精度评价的一种标准格式,用n行n列的矩阵形式来表示。混淆矩阵(confusion matrix)是可视化工具,混淆矩阵的每一列代表了预测类别,每一列的总数表示预测为该类别的数据的数目;每一行代表了数据的真实归属类别,每一行的数据总数表示该类别的数据实例的数目。

准确率Acc

准确率Accuracy就是所有预测正确的所有样本除以总样本,通常来说越接近1越好。准确率的使用有一定的局限性。比如当样本不平衡时,假设有99个负例,一个正例。如果模型将这些样本全部预测成了负例,准确率为99%,这显然是不合理的,此时准确率就失去了衡量模型性能的作用。在这种情况下,可以使用查准率和查全率,这两个指标对长尾数据集的模型评价更有说服力。

在上图总共100个样本,我们一共预测对了15 + 15 + 45=75个样本,所以准确率=75/100 = 75%。

查准率Precision

精确率是正确分类为正类的样本数占所有被预测为正类的样本数的比例,精确率关注的是模型预测类别的准确性。

上图中对于A来说,共有24个样本被预测成A,但是其中只有15个样本真实是A,所以A的查准率=15/24=62.5%

查全率Recall

查全率是正确分类为正类的样本数占所有实际为正类的样本数的比例,查全率关注的是模型识别类别的能力。

上图中对于A来说,共有20个样本A,但是其中只有15个样本被模型识别成了A,所以A的查全率=15/20=62.5%

F1分数F1-score

F1分数是精确率和召回率的调和平均值,用于综合考虑精确率和召回率。当我们想要在精确率和召回率之间取得平衡时,F1分数是一个很好的指标。F1的计算公式为

F1

常见超参数及模型相关概念

  • device:在模型训练中,输入数据和模型放置的设备(cpu/gpu)
  • batch_size:在模型训练中,由于显存限制的原因,往往不能把全量的数据输入模型进行训练,所以需要把数据切分成更小的bacth,以batch为单位输入模型进行训练,batch_size指每一个batch包含多少条数据
  • pad_size:在NLP任务中,我们通常需要处理的是不同长度的文本数据,为了使输入数据能够在神经网络中进行并行计算,我们需要将所有序列统一成相同的长度。pad_size就是设定一个固定的长度,对于长度较小的文本后面添加特殊token(通常是[PAD]),对于长度较大的文本直接截断,从而实现长度的统一。
  • epoch:轮次,每次把所有batch都输入到模型训练一次即为一次epoch。
  • weight_decay:权重衰减,通过在Loss函数后加一个正则化项,通过使权重减小的方式,一定减少模型过拟合的问题。
  • learning_rate:学习率,控制我们要多大程度调整网络的权重,以符合梯度损失,值越低沿着梯度下降越慢,越高下降越快。
  • bert_lr_ratio:由于bert等预训练模型的拟合能力过强,所以一般而言非bert层的学习率需要是bert层的5-10倍,bert_lr_ratio用来控制bert层学习率的百分比,一般设置为0.2。
  • patience:为了防止模型过拟合和资源浪费,如果模型训练过程中,持续patience个epoch,模型的效果还没有提升,我们就认为此时模型已经拟合,就会提前停止。
  • dropout:dropout是指在神经网络的训练过程中随机地将某些神经元的输出设置为0。这样做的目的是为了防止过拟合,即模型过于依赖训练数据,无法泛化到新的数据上。通过随机地丢弃一部分神经元,dropout可以减少神经网络中神经元之间的复杂关系,从而提高模型的鲁棒性和泛化能力。在测试时,不再对神经元进行随机丢弃,而是将所有神经元都保留下来。
  • mask:因为我们会对文本做PAD处理,对于模型而言[PAD]是没有意义的,所以需要知道encode后的token哪些是真实文本,哪些是[PAD],我们会用一组0/1向量来标识非[PAD]token,这个向量称为mask向量
  • Special mark:在预训练模型中,有一些特殊的token用来表示特殊的意思,常见的包括:
    • [PAD]:表示填充的token。
    • [CLS]:放在每一个句子的首位,表示句子级别的特征。
    • [SEP]:放在句子的尾位,表示一个句子结束。在句子对任务学习中用来区分不同的句子。
    • [UNK]:表示词表里没有的token。
  • optimizer:优化器就是在深度学习反向传播过程中,指引损失函数(目标函数)的各个参数往正确的方向更新合适的大小,使得更新后的各个参数让损失函数(目标函数)值不断逼近全局最小。简单来说就是让loss变成最小的算法。

模型训练

超参数的设置

首先需要设置模型训练相关的超参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 各项参数设置
dataset = 'data/THUCnews'
labels_name = [w.strip() for w in open(os.path.join(dataset, 'class.txt'), 'r', encoding='utf8').readlines()]
pad_size = 32
device = torch.device('cpu') # cpu
# device = torch.device('cuda') # 设备gpu
batch_size = 4
bert_path = 'bert-base-chinese'
num_classes = len(labels_name)
weight_decay = 0.02
num_epochs = 30
learning_rate = 5e-5
bert_lr_ratio = 0.2
dropout = 0.1
patience = 6
save_path = 'saved_dict/bert.ckpt' # 模型保存地址

数据预处理

根据原始数据集采用适当的办法将数据集处理成文本和标签一一对应的格式,便于后续的数据读取。只要处理成一一对应的形式即可,比如将每一条文本和标签用\t拼接,后续可以很方便的处理。

data process

数据读取

首先读取数据,将文本和对应的label对应起来。本文是用一个用一个二维的tuple保存单条数据,然后存到list中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from tqdm import tqdm
import os

# 读取数据集
def data_loader(file_path):
contents = []
with open(file_path, 'r', encoding='UTF-8') as f:
for line in tqdm(f):
lin = line.strip()
if not lin:
continue
content, label = lin.split('\t')
contents.append((content, label))
return contents

train_data = data_loader(os.path.join(dataset, 'train.txt'))
dev_data = data_loader(os.path.join(dataset, 'dev.txt'))
test_data = data_loader(os.path.join(dataset, 'test.txt'))

print(test_data[:5])
print(labels_name)

data detail

数据编码

读取数据后,需要对数据进行encode,nlp里的预训练模型在使用上可以粗暴地理解简单粗暴地认为通常包含两部分,一个是tokenizer,用来将句子转化为一个一个token,另一部分就是预训练模型本身,可以将每一个token特征提取成一个n维的向量,比如bert类预训练模型一般转化后的向量是768维。

下图中调用了transformers包的BertTokenizer方法,加载bert预训练的tokenizer后对数据进行编码,下图中的各项参数含义如下:

  • text:要编码的文本,字符串类型。
  • max_length:编码后的序列的最大长度。如果输入文本长度超过了 max_length,则根据 truncation_strategy 参数截断或填充到指定长度。默认为 None,表示不限制序列长度。
  • truncation:一个布尔值,表示是否截断输入文本以适合指定的序列长度。如果为 True,则输入文本长度超过 max_length 时将被截断。如果为 False,则不进行截断操作。默认为 True
  • truncation_strategy:一个字符串,表示截断策略。有两个可选值:“longest_first” 和 “only_first”。如果选择 “longest_first”,则将文本截断为长度等于 max_length 的最长子串;如果选择 “only_first”,则将文本截断为开头的长度等于 max_length 的子串。默认为 “longest_first”。
  • add_special_tokens:一个布尔值,表示是否在序列的开头和结尾添加特殊的标记,如 [CLS][SEP]。默认为 True
  • pad_to_max_length:一个布尔值,表示是否填充序列以达到指定的 max_length。如果为 True,则在序列末尾添加特殊的填充标记,使序列长度达到 max_length。如果为 False,则不进行填充操作。默认为 True

对于经过pad后的句子,我们还需要构造一个mask向量用来标识哪些token是pad,这样每一条数据就会变成一个三维tuple(tokenizer后的句子向量,mask句子向量,标签label)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from transformers import BertTokenizer, BertModel
# 数据encode
def encode_data(tokenizer, data):
data_encoded = []
for text, label in tqdm(data, total=len(data)):
inputs = tokenizer.encode(text=text, max_length=pad_size, truncation=True,
truncation_strategy='longest_first',
add_special_tokens=True, pad_to_max_length=True)
data_encoded.append((inputs, [1 if x != 0 else 0 for x in inputs], int(label)))
return data_encoded

tokenizer = BertTokenizer.from_pretrained(bert_path)

train_data = encode_data(tokenizer, train_data)
dev_data = encode_data(tokenizer, dev_data)
test_data = encode_data(tokenizer, test_data)

print(train_data[0])

encode

随后我们需要进行输入的包装,一方面是将刚才生成的向量转化为张量Tensor,然后放到设置的device上进行模型训练(矩阵运算),另一方面是将数据切分成一个个batch。常见的工具类有torch.utils.data下的 TensorDataset DataLoader。这两个方法往往是组合起来使用,TensorDataset用来整理数据,DataLoader用来切分和读取数据。

具体来说下图中的zip(*train_data)])的作用是将之前的tuple list按列展开重新组合,之前的数据格式是[(input_id, mask, label),...]现在就会变成[(input_id1,input_id2,...),(mask1,mask2,...),(label1,label2,...)]然后遍历每一个元素,将元素转化成张量后放到device上;然后用DataLoader将其包装成迭代器,下图中的各参数含义如下:

  • dataset: 数据集对象,通常是 TensorDataset 类的实例。
  • batch_size: 每个批次的数据量大小。
  • shuffle: 是否在每个epoch之前对数据进行随机打乱。
  • drop_last: 是否将最后一个不足一个批次大小的数据扔掉。

我们可以发现每一个句子经过tokenizer后都会变成101开头和102结尾,句子pad部分都会变成0,这三个下标其实就是对应bert预训练模型词表中的[CLS]、[SEP][PAD]

1
2
3
4
5
6
7
8
9
10
11
from torch.utils.data import TensorDataset, DataLoader
import torch
# 迭代器包装
train_dataset = TensorDataset(*[torch.LongTensor(x).to(device) for x in zip(*train_data)])
dev_dataset = TensorDataset(*[torch.LongTensor(x).to(device) for x in zip(*dev_data)])
test_dataset = TensorDataset(*[torch.LongTensor(x).to(device) for x in zip(*test_data)])
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=False, drop_last=False)
dev_loader = DataLoader(dev_dataset, batch_size=batch_size, shuffle=False, drop_last=False)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False, drop_last=False)
test_iter = iter(test_loader)
print(next(test_iter))

data loader

网络搭建

数据处理部分结束后,我们需要搭建网络,如果不关心模型内部的运算过程,实际上我们在使用深度学习网络的时候只需要知道每一个网络的输入输出是什么shape(形状),shape的每一个维度代表什么含义即可。

如图,对于下面这个示例句子,经过encode之后输入bert预训练模型,输出由两部分,第一部分包含这个句子每一个token的特征张量,第二部分是句子的[[CLS]token](https://aicarrier.feishu.cn/docx/DcIYd0xjIoS7k1x9GcwcdScJnlC#part-UYw0drPzNokAPJxjdJbcVBoJnWd)的特征。对于句子级别的文本分类,我们只需要关注[CLS]token即可,经过bert模型后句子会变成[sample_number,768]的形状,然后通过一个全连接层将768维压缩到需要分类的类别(图中是5),这样句子最终会变成[sample_number,label_number]的形状,此时第二维可以看做句子属于不同类别的概率,取概率最高的label作为预测结果即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from transformers import BertTokenizer, BertModel
import torch
import torch.nn as nn

# 用预训练的BERT模型进行初始化
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
model = BertModel.from_pretrained('bert-base-chinese')
# 输入句子
sentence = "这是一个示例句子。"
# 使用BERT Tokenizer对句子进行编码
input_ids = tokenizer.encode(text=sentence)
input_ids = torch.LongTensor(input_ids).unsqueeze(0)

# 使用BERT模型获取词嵌入
with torch.no_grad():
last_hidden_states,cls_states = model(input_ids, return_dict=False)
lc = nn.Linear(768, 5)
out = lc(cls_states)

# print(last_hidden_states)
print(input_ids) # tokenizer后的句子张量
print(last_hidden_states.shape) # bert输出的句子embedding张量,shape第一维表示句子,第二维表示token,第三维表示token的特征维度
print(cls_states.shape) # bert输出的[CLS]的张量,shape第一维表示句子,第二维表示特征维度
print(out.shape) # [CLS]张量经过全连接层后的输出,输出每一个label的概率

model demo

回到本文,基于bert的文本分类网络搭建实际上也只需要两层即可,第一层bert层做特征抽取,第二层linear层做label输出,整体网络结构图如下。__init__部分是网络初始化各个层,forward部分是网络进行计算的流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import torch.nn as nn

# 模型网络搭建
class Model(nn.Module):
def __init__(self, bert_path, hidden_size, num_classes):
super(Model, self).__init__()
self.bert = BertModel.from_pretrained(bert_path) # 加载预训练模型
for param in self.bert.parameters():
param.requires_grad = True # 在训练中更新bert预训练模型的权重
self.fc = nn.Linear(hidden_size, num_classes) # 全连接层分类

def forward(self, x):
context = x[0] # 输入的句子
mask = x[1] # 对padding部分进行mask,和句子一个size,padding部分用0表示,如:[1, 1, 1, 1, 0, 0]
_, pooled = self.bert(context, attention_mask=mask, return_dict=False)
out = self.fc(pooled)
return out

# 初始化模型
model = Model(bert_path, 768, num_classes).to(device)
print(model.parameters)

优化器设置

在模型开始前还需要初始化优化器,优化器简单来讲就是让模型训练的过程中loss尽可能的逼近全局最小的算法,目前一般用AdamW优化器比较多。下面代码是对于使用了bert类型预训练模型的优化器设置,一般来讲是固定的,他的作用是把bert层的学习率设置的低一点,然后对于有权重衰减层设置权重衰减,没有权重衰减层设置权重衰减为0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from transformers import AdamW

def get_optimizer(model):
param_optimizer = list(model.named_parameters())
no_decay = ['bias', 'LayerNorm.bias', 'LayerNorm.weight']
bert_param_ids = list(map(id, param_optimizer))
no_weight_decay_params = [x[1] for x in filter(
lambda name_w: any(nwd in name_w[0] for nwd in no_decay), model.named_parameters())]
no_weight_decay_param_ids = list(map(id, [x[1] for x in no_weight_decay_params]))
bert_base_params = filter(lambda p: id(p) in bert_param_ids and id(p) not in no_weight_decay_param_ids,
model.parameters())
bert_no_weight_decay_params = filter(lambda p: id(p) in bert_param_ids and id(p) in no_weight_decay_param_ids,
model.parameters())
base_no_weight_decay_params = filter(
lambda p: id(p) not in bert_param_ids and id(p) in no_weight_decay_param_ids,
model.parameters())
base_params = filter(lambda p: id(p) not in bert_param_ids and id(p) not in no_weight_decay_param_ids,
model.parameters())
params = [{"params": bert_base_params, "lr": learning_rate * bert_lr_ratio},
{"params": bert_no_weight_decay_params, "lr": learning_rate * bert_lr_ratio,
"weight_decay": 0.0},
{"params": base_no_weight_decay_params, "lr": learning_rate, "weight_decay": 0.0},
{"params": base_params, "lr": learning_rate}]

# 设置AdamW优化器
optimizer = AdamW(params, lr=learning_rate, weight_decay=weight_decay)

return optimizer

optimizer = get_optimizer(model)

模型训练

初始化优化器,搭建完模型网路结构后就可以开始模型训练了。一般来说模型训练的过程也比较固定,模型输入->模型输出->loss计算->反向传播->优化器step->模型评估->模型保存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
import numpy as np
import torch.nn.functional as F
from sklearn import metrics
from sklearn.metrics import f1_score

def evaluate(model, data_iter):
model.eval()
loss_total = 0
predict_all = np.array([], dtype=int)
labels_all = np.array([], dtype=int)
with torch.no_grad():
for step, batch in tqdm(data_iter):
batch = [x.to(device) for x in batch]
outputs = model((batch[0], batch[1]))
# print(labels)
loss = F.cross_entropy(outputs, batch[-1])
loss_total += loss
labels = batch[-1].data.cpu().numpy()
predic = torch.max(outputs.data, 1)[1].cpu().numpy()
labels_all = np.append(labels_all, labels)
predict_all = np.append(predict_all, predic)

acc = metrics.accuracy_score(labels_all, predict_all)
f1 = f1_score(labels_all, predict_all, average='macro')
report = metrics.classification_report(labels_all, predict_all, target_names=labels_name, digits=4)
confusion = metrics.confusion_matrix(labels_all, predict_all)

return acc, f1, loss_total / (len(data_iter) + 1e-10), report, confusion

def test(model, test_iter):
# test
model.load_state_dict(torch.load(save_path))
model.eval()
test_acc, test_f1, test_loss, test_report, test_confusion = evaluate(model, test_iter)
msg = 'Test Loss: {0:>5.2}, Test Acc: {1:>6.2%}, Test F1:{2:>6.2%}'
logger.info(msg.format(test_loss, test_acc, test_f1))
logger.info("Precision, Recall and F1-Score...")
logger.info(test_report)
logger.info("Confusion Matrix...")
logger.info(test_confusion)

def train(model, train_iter, dev_iter, test_iter, optimizer):
model.train()
dev_best_f1 = float('-inf')
last_improve_epoch = 0
model.train()
for epoch in range(num_epochs):
logger.info('Epoch [{}/{}]'.format(epoch + 1, num_epochs))

# 记录变量
train_labels_all = np.array([], dtype=int)
train_predicts_all = np.array([], dtype=int)
train_loss_list = []
t = tqdm(train_iter, leave=False, total=len(train_iter), desc='Training')
for step, batch in enumerate(t):
batch = [x.to(device) for x in batch]
model.train()
model.zero_grad()
outputs = model((batch[0], batch[1]))
loss = F.cross_entropy(outputs, batch[-1])
train_loss_list.append(loss.item())
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) # 梯度裁剪
optimizer.step()
# 真实标签和预测标签

predicts = torch.max(outputs.data, 1)[1].cpu()
labels_train = batch[-1].cpu().data.numpy()
train_labels_all = np.append(train_labels_all, labels_train)
train_predicts_all = np.append(train_predicts_all, predicts)

# 训练集评估
train_loss = sum(train_loss_list) / (len(train_loss_list) + 1e-10)
train_acc = metrics.accuracy_score(train_labels_all, train_predicts_all)
train_f1 = metrics.f1_score(train_labels_all, train_predicts_all, average='macro')

dev_acc, dev_f1, dev_loss, report, confusion = evaluate(model, dev_iter)
msg = 'Train Loss: {0:>5.6}, Train Acc: {1:>6.4%}, Train F1: {2:>6.4%}, Val Loss: {3:>5.4}, Val Acc: {4:>6.4%}, Val F1: {5:>6.4%}'
logger.info(msg.format(train_loss, train_acc, train_f1, dev_loss, dev_acc, dev_f1))
logger.info("Precision, Recall and F1-Score...")
logger.info(report)
logger.info("Confusion Matrix...")
logger.info(confusion)

if dev_f1 > dev_best_f1:
dev_best_f1 = dev_f1
torch.save(model.state_dict(), save_path)
last_improve_epoch = epoch

if epoch - last_improve_epoch > patience:
logger.info("No optimization for a long time, auto-stopping...")
break

test(model, test_iter)

train(model,train_loader,dev_loader,test_loader,optimizer)

训练结果

文本的实验结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
023-04-21 16:08:10,560 - bert_cls.py[line:205] - INFO: No optimization for a long time, auto-stopping...
2023-04-21 16:08:15,798 - bert_cls.py[line:149] - INFO: Test Loss: 0.52, Test Acc: 92.00%, Test F1:92.00%
2023-04-21 16:08:15,798 - bert_cls.py[line:150] - INFO: Precision, Recall and F1-Score...
2023-04-21 16:08:15,798 - bert_cls.py[line:151] - INFO:
precision recall f1-score support

体育 0.9542 0.9580 0.9561 500
娱乐 0.8650 0.9480 0.9046 500
家居 0.9502 0.8780 0.9127 500
彩票 0.9801 0.9860 0.9831 500
房产 0.8776 0.9320 0.9040 500
教育 0.9434 0.9660 0.9545 500
时尚 0.9584 0.9220 0.9399 500
时政 0.8945 0.9160 0.9051 500
星座 0.9841 0.9920 0.9880 500
游戏 0.9467 0.9240 0.9352 500
社会 0.9251 0.9140 0.9195 500
科技 0.8264 0.8760 0.8505 500
股票 0.8592 0.8180 0.8381 500
财经 0.9300 0.8500 0.8882 500

accuracy 0.9200 7000
macro avg 0.9211 0.9200 0.9200 7000
weighted avg 0.9211 0.9200 0.9200 7000

2023-04-21 16:08:15,798 - bert_cls.py[line:152] - INFO: Confusion Matrix...
2023-04-21 16:08:15,798 - bert_cls.py[line:153] - INFO:
[[479 13 0 5 0 0 1 1 0 0 1 0 0 0]
[ 6 474 1 2 0 2 4 2 2 2 3 1 0 1]
[ 0 8 439 0 25 1 2 2 2 2 2 12 3 2]
[ 6 1 0 493 0 0 0 0 0 0 0 0 0 0]
[ 1 3 6 0 466 0 1 1 0 1 4 6 6 5]
[ 0 2 0 0 1 483 1 2 3 1 5 2 0 0]
[ 1 17 7 0 7 2 461 1 0 0 2 2 0 0]
[ 1 4 0 0 5 5 2 458 0 1 8 7 5 4]
[ 0 0 0 0 0 1 3 0 496 0 0 0 0 0]
[ 3 9 1 0 1 1 4 1 0 462 1 15 2 0]
[ 4 5 1 1 1 11 2 7 0 0 457 10 1 0]
[ 0 8 1 1 6 3 0 10 0 12 8 438 12 1]
[ 0 1 3 0 14 2 0 21 0 6 0 25 409 19]
[ 1 3 3 1 5 1 0 6 1 1 3 12 38 425]]

模型预测和gradio应用简单构建

训练好模型后,如果想使用模型进行预测,步骤其实和训练类似,而且不需要反向传播和任何的评价指标统计,整体流程一般是输入句子->encode编码->模型输入输出->格式化输出。这地方需要注意的是,如果训练保存模型的设备和加载模型的设备不一致,需要在torch.load(save_path, map_location=device)的时候用map_location参数来把二者统一起来,不然会产生模型读取错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import os
from transformers import BertTokenizer, BertModel
import torch
import torch.nn as nn
import gradio as gr

class Model(nn.Module):
def __init__(self, bert_path, hidden_size, num_classes):
super(Model, self).__init__()
self.bert = BertModel.from_pretrained(bert_path)
for param in self.bert.parameters():
param.requires_grad = True
self.fc = nn.Linear(hidden_size, num_classes)

def forward(self, x):
context = x[0] # 输入的句子
mask = x[1] # 对padding部分进行mask,和句子一个size,padding部分用0表示,如:[1, 1, 1, 1, 0, 0]
_, pooled = self.bert(context, attention_mask=mask, return_dict=False)
out = self.fc(pooled)
return out

dataset = 'data/THUCnews'
labels_name = [w.strip() for w in open(os.path.join(dataset, 'class.txt'), 'r', encoding='utf8').readlines()]
device = torch.device('cpu') # 设备cpu
bert_path = 'bert-base-chinese'
num_classes = len(labels_name)
save_path = 'saved_dict/bert.ckpt'
pad_size = 32

# 用预训练的BERT模型进行初始化
tokenizer = BertTokenizer.from_pretrained(bert_path)
model = Model(bert_path, 768, num_classes).to(device)

# 加载模型权重。
model.load_state_dict(torch.load(save_path, map_location=device))
model.eval()

def predict(sentence):
inputs = tokenizer.encode(text=sentence, max_length=pad_size, truncation=True,
truncation_strategy='longest_first',
add_special_tokens=True, pad_to_max_length=True)
data_encode = (inputs, [1 if x != 0 else 0 for x in inputs])
input_id = torch.LongTensor(data_encode[0]).unsqueeze(0).to(device)
input_mask = torch.LongTensor(data_encode[1]).unsqueeze(0).to(device)

with torch.no_grad():
outputs = model((input_id, input_mask))
predicts = torch.max(outputs.data, 1)[1].cpu()

return labels_name[predicts[0]]

# gradio封装
iface = gr.Interface(fn=predict, inputs="text", outputs="text")
iface.launch()

相关代码

https://github.com/leidaoyu/text_classification_demo


文本分类任务基本流程
https://shouldbeldy.github.io/2023/04/25/文本分类任务基本流程/
作者
Daoyu Lei
发布于
2023年4月25日
许可协议