对于 Bert 来说,用于文本分类是最常见的,并且准确率也很高。本文将会对 bert 用于文本分类来做详细的介绍。
对于不同的数据,需要导入不同的预训练模型。
预训练模型下载地址:Models - Hugging Face
本文使用的是中文数据集,因此需要选择中文的预训练模型:bert-base-chinese at main
BertModel 主要为 transformer encoder 结构,包含三个部分:
注意:BertModel 也可以配置为 Decoder
图1 bert 模型初始化/结构
Bert文本分类模型常见做法为将bert最后一层输出的第一个token位置(CLS位置)当作句子的表示,后接全连接层进行分类。
Bert 模型可以用于不同的场景,在文本分类,实体识别等场景的输入是不同的。
对于文本分类,其最主要的有两个参数:input_ids,attention_mask
图2 bert 模型输入
input_ids:经过 tokenizer 分词后的 subword 对应的下标列表;
attention_mask:在 self-attention 过程中,这一块 mask 用于标记 subword 所处句子和 padding 的区别,将 padding 部分填充为 0;
该模型的输出也是有多个,但是只有一个是用于文本分类的
bert的输出结果的其中四个维度:
1、last_hidden_state:shape是(batch_size, sequence_length, hidden_size),hidden_size=768,它是模型最后一层输出的隐藏状态。
2、pooler_output:shape是(batch_size, hidden_size),在通过用于辅助预训练任务的层进行进一步处理后,序列的第一个token(classification token)的最后一层的隐藏状态。例如。对于 BERT 系列模型,这会在通过线性层和 tanh 激活函数处理后返回分类标记。线性层权重在预训练期间从下一个句子预测(分类)目标进行训练。
3、hidden_states:shape是(batch_size, sequence_length, hidden_size),这是输出的一个可选项,如果输出,
需要指定`output_hidden_states=True` is passed or when `config.output_hidden_states=True`
它的第一个元素是embedding,其余元素是各层的输出。每层输出的模型隐藏状态加上可选的初始嵌入输出。
4、attentions:shape是(batch_size,num_heads,sequence_length,sequence_length),这也是输出的一个可选项,如果输出,
需要指定`output_attentions=True` is passed or when `config.output_attentions=True`
它的元素是每一层的注意力权重(softmax),用于计算self-attention heads的加权平均值。
我们是微调模式,需要获取bert最后一个隐藏层的输出输入到下一个全连接层,所以取第一个维度,也就是hiden_outputs.pooler_output
图3 bert 模型的输出
数据格式
# 读取文件
all_data = open(file, "r", encoding="utf-8").read().split("\n")
# 得到所有文本、所有标签、句子的最大长度
texts, labels, max_length = [], [], []
for data in all_data:
if data:
text, label = data.split("\t")
max_length.append(len(text))
texts.append(text)
labels.append(label)
# 取出一条数据并截断长度
text = self.all_text[index][:self.max_len]
label = self.all_label[index]
# 分词
text_id = self.tokenizer.tokenize(text)
# 加上起始标志
text_id = ["[CLS]"] + text_id
# 编码
token_id = self.tokenizer.convert_tokens_to_ids(text_id)
# 掩码 -》
mask = [1] * len(token_id) + [0] * (self.max_len + 2 - len(token_id))
# 编码后 -》长度一致
token_ids = token_id + [0] * (self.max_len + 2 - len(token_id))
# str -》 int
label = int(label)
# 转化成tensor
token_ids = torch.tensor(token_ids)
mask = torch.tensor(mask)
label = torch.tensor(label)
return (token_ids, mask), label
class MyModel(nn.Module):
def __init__(self):
super(MyModel, self).__init__()
self.args = parsers()
self.device = "cuda:0" if self.args.device else "cpu"
# 加载 bert 中文预训练模型
self.bert = BertModel.from_pretrained(self.args.bert_pred)
# 让 bert 模型进行微调(参数在训练过程中变化)
for param in self.bert.parameters():
param.requires_grad = True
# 全连接层
self.linear = nn.Linear(self.args.num_filters, self.args.class_num)
def forward(self, x):
input_ids, attention_mask = x[0].to(self.device), x[1].to(self.device)
hidden_out = self.bert(input_ids, attention_mask=attention_mask,
output_all_encoded_layers=False) # 控制是否输出所有encoder层的结果
# shape (batch_size, hidden_size)
pred = self.linear(hidden_out.pooler_output)
# 返回预测结果
return pred
for epoch in range(args.epochs):
loss_sum, count = 0, 0
model.train()
for batch_index, (batch_text, batch_label) in enumerate(train_dataloader):
batch_label = batch_label.to(device)
pred = model(batch_text)
loss = loss_fn(pred, batch_label)
opt.zero_grad()
loss.backward()
opt.step()
loss_sum += loss
count += 1
# 打印内容
if len(train_dataloader) - batch_index <= len(train_dataloader) % 1000 and count == len(train_dataloader) % 1000:
msg = "[{0}/{1:5d}]\tTrain_Loss:{2:.4f}"
logging.info(msg.format(epoch + 1, batch_index + 1, loss_sum / count))
loss_sum, count = 0.0, 0
if batch_index % 1000 == 999:
msg = "[{0}/{1:5d}]\tTrain_Loss:{2:.4f}"
logging.info(msg.format(epoch + 1, batch_index + 1, loss_sum / count))
loss_sum, count = 0.0, 0
bert 文本分类