Bert模型做多标签文本分类

参考链接

BERT模型的详细介绍

图解BERT模型:从零开始构建BERT

(强推)李宏毅2021春机器学习课程

我们现在来说,怎么把Bert应用到多标签文本分类的问题上。注意,本文的重点是Bert的应用,对多标签文本分类的介绍并不全面

单标签文本分类

对应单标签文本分类来说,例如二元的文本分类,我们首先用一层或多层LSTM提取文本序列特征,然后接一个dropout层防止过拟合,最后激活函数采用sigmoid,或者计算损失的时候使用sigmoid交叉熵损失函数。对于多元分类则激活函数采用softmax,其它没有差别

多标签文本分类

怎么从单标签分类问题拓展到多标签分类呢?

我们可以把二元分类的情况归并到多元分类

至少有以下两种方案(我懂的):

1,最后的全连接层以sigmoid作为激活函数,把每个神经元都当成是二元分类。另外,也可以直接把最后的全连接层改成n个全连接层,每个全连接层再接一个神经元做二元分类(激活函数是sigmoid),我认为二者本质上没有区别。

2,将多标签分类任务视作seq2seq的问题,对于给定的文本序列,生成不定长的标签序列。

这篇文章将介绍第一种方案。

首先我们先看看怎么使用Bert模型

下载transformers包,pip install transformers

如果是处理英文问题,并且不用统一大小写的话,可以按照下方链接下载

其次手动下载模型,下载bert-base-uncasedconfig.josn,vocab.txt,pytorch_model.bin三个文件

配置文件下载地址:https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-uncased-config.json

模型文件下载地址:https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-uncased-pytorch_model.bin

词汇表下载地址:https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-uncased-vocab.txt

下载完成后,按照config.json,vocab.txt,pytorch_model.bin重命名,放在bert-base-uncased文件夹下,此例中bert-base-uncased文件夹放置在项目根目录下

如果是处理中文任务,把链接中的bert-base-uncased替换成bert-base-chinese即可,存放文件夹名可根据习惯修改为相应模型的名称

下面的demo是基于中文bert演示的,真正的多标签分类的项目代码里用的是bert-base-uncased

导入包,加载预训练模型

import numpy as np
import torch
from transformers import BertTokenizer, BertConfig, BertForMaskedLM, BertForNextSentencePrediction
from transformers import BertModelmodel_name = "bert-base-chinese"
# a. 通过词典导入分词器
tokenizer = BertTokenizer.from_pretrained(model_name)
# b. 导入配置文件
model_config = BertConfig.from_pretrained(model_name)
# 修改配置
model_config.output_hidden_states = True
model_config.output_attentions = True
# 通过配置和路径导入模型
bert_model = BertModel.from_pretrained(model_name, config = model_config)

完成模型加载后,我们来看看Bert的输入输出

输入

假设我们输入了一句话是“我爱你,你爱我”,我们需要利用tokernizer做初步的embedding处理

sen_code = tokenizer.encode_plus("我爱你,你爱我")

得到的sen_code是这样的

{‘input_ids’: [101, 2769, 4263, 872, 102, 872, 4263, 2769, 102],

‘token_type_ids’: [0, 0, 0, 0, 0, 1, 1, 1, 1],

‘attention_mask’: [1, 1, 1, 1, 1, 1, 1, 1, 1]}

input_ids就是每个字符在字符表中的编号,101表示[CLS]开始符号,[102]表示[SEP]句子结尾分割符号。

token_type_ids是区分上下句的编码,上句全0,下句全1,用在Bert的句子预测任务上

attention_mask表示指定哪些词作为query进行attention操作,全为1表示self-attention,即每个词都作为query计算跟其它词的相关度

将input_ids转化回token

tokenizer.convert_ids_to_tokens(sen_code['input_ids'])
#output:['[CLS]', '我', '爱', '你', '[SEP]', '你', '爱', '我', '[SEP]']

Bert的输入是三个embedding的求和,token embedding,segment embedding和position embedding

# token embedding
tokens_tensor = torch.tensor([sen_code['input_ids']]) # 添加batch维度
# segment embedding
segments_tensors = torch.tensor([sen_code['token_type_ids']]) # 添加batch维度

输出

Bert是按照两个任务进行预训练的,分别是遮蔽语言任务(MLM)和句子预测任务。

我先简单解释一下这两个任务

遮蔽语言任务(Masked Language Model

对输入的语句中的字词 随机用 [MASK] 标签覆盖,然后模型对mask位置的单词进行预测。这个过程类似CBOW训练的过程,我们利用这个训练任务从而得到每个字符对应的embedding。特别的,[CLS]字符的embedding我们可以视为整个句子的embedding。我们可以理解为[CLS]字符跟句子中的其它字符都没有关系,能较为公平的考虑整个句子。

句子预测任务(NextSentence Prediction

该任务就是给定一篇文章中的两句话,判断第二句话在文本中是否紧跟在第一句话之后。如果我们训练的时候将问题和答案作为上下句作为模型输入,该任务也可以理解为判断问题和答案是否匹配

现在我们根据代码看看bert的输出

bert_model.eval()
with torch.no_grad():outputs = bert_model(tokens_tensor, token_type_ids = segments_tensors)encoded_layers = outputs   # outputs类型为tuple

最后一个隐藏层的输出,即遮蔽语言任务的输出,亦即每个字符的embedding

print("sequence output",encoded_layers[0].shape)
# sequence output torch.Size([1, 9, 768])

最后一个隐藏层的第一个输出[CLS]的embedding,然后进行pool操作的结果,所谓的pool操作就是接一个全连接层+tanh激活函数层。它可以作为整个句子的语义表示,但也有将所有向量的平均作为句子的表示的做法

print("pooled output",encoded_layers[1].shape)
# pooled output torch.Size([1, 768])

所有隐藏层的输出,hidden_states有13个元素,第一个是[CLS]的embedding,后面12个元素表示12个隐藏层的输出,对于seq2seq的任务,它们将作为decoder的输入

print("hidden_states",len(encoded_layers[2]),encoded_layers[2][0].shape)
# hidden_states 13 torch.Size([1, 9, 768])

attention分布,有12个元素,每个隐藏层的hidden_states经过self-attention层得到的attention分布,没有乘以V矩阵。因为是multi-head,一共有12个头,所以每个attention分布的维度是1x12x9x9(1是batch_size,9是序列长度)

print("attentions",len(encoded_layers[3]),encoded_layers[3][0].shape)
# attentions 12 torch.Size([1, 12, 9, 9])

要明白上面的输出为什么是那个意思,还是得看源码Bert代码详解(一)

模型构建

搞明白bert的输入输出之后我们就可以试着做fine-tune了,我们是要做多标签文本分类,根据第一个方案,我们首先提取出文本的特征,然后接全连接层,最后接一个sigmoid激活函数。

前面已经说过,pooled output就是表示bert得到的整个句子的语义特征,这正是我们需要的。将这个特征作为全连接层的输入即可。代码里面还定义了dropout层,这都是训练的常用技巧,防止过拟合

class BertForMultiLabel(BertPreTrainedModel):def __init__(self, config):super(BertForMultiLabel, self).__init__(config)self.bert = BertModel(config)self.dropout = nn.Dropout(config.hidden_dropout_prob)self.classifier = nn.Linear(config.hidden_size, config.num_labels)self.sigmoid = nn.Sigmoid()def forward(self, input_ids, token_type_ids=None, attention_mask=None, head_mask=None):outputs = self.bert(input_ids, token_type_ids,attention_mask,head_mask)pooled_output = outputs[1]pooled_output = self.dropout(pooled_output)logits = self.classifier(pooled_output)return self.sigmoid(logits)def unfreeze(self, start_layer, end_layer):def children(m):return m if isinstance(m, (list, tuple)) else list(m.children())def set_trainable_attr(m, b):m.trainable = bfor p in m.parameters():p.requires_grad = bdef apply_leaf(m, f):c = children(m)if isinstance(m, nn.Module):f(m)if len(c) > 0:for l in c:apply_leaf(l, f)def set_trainable(l, b):apply_leaf(l, lambda m: set_trainable_attr(m, b))set_trainable(self.bert, False)for i in range(start_layer, end_layer + 1):set_trainable(self.bert.encoder.layer[i], True)

定义损失函数,优化器和超参数

Bert原项目对训练使用了很多性能、显存消耗的优化技术,包括warmup,gradient accumulation,还有fp16,这些技术我暂时也没有全部搞懂,所以暂时抛弃部分优化技术,写一个最简单的优化器。AdamW是Bert预训练采用的优化算法,大家如果不懂可以去百度一下,我也不是很了解,所以就直接用了

# 定义超参数
batch_size = 8
lr = 2e-5
adam_epsilon = 1e-8
grad_clip = 1.0
start_layer = 11  #[0,11]
end_layer = 11      #[start_layer,11]# 定义损失函数
loss = nn.BCELoss()
# 定义优化器
optimizer = optim.AdamW(model.parameter(), lr=lr, eps=adam_epsilon)# 加载模型
model = BertForMultiLabel(config)
# 现在使用的Bert模型是12层,我们可以自由调节冻结bert模型的层数,当前是只训练最后一层
model.unfreeze(start_layer, end_layer)
model = model.cuda()

加载处理数据集

一个模型想要跑起来必然需要数据输入,Bert对参与训练的数据格式要求为input_ids, input_mask, segment_ids, label_ids。而原始的数据格式为string,label_ids

所以我们需要对数据做一些处理,为此我们定义一个BertProcessor类,这个类的主要方法为read_dataset和train_val_split。

注意我现在的做法和那些好的做法有很多差别,那些好的做法是基于优化的考虑,但我们现在暂时不用考虑这么多,把重心放在bert的使用和模型的成功训练上,优化做法读者可进一步研究。

先看类中部分代码,完整项目在最后

class BertProcessor:def __init__(self, vocab_path, do_lower_case, max_seq_length) -> None:self.tokenizer = BertTokenizer(vocab_path, do_lower_case)self.max_seq_length = max_seq_lengthdef get_input_ids(self, x):# 使用tokenizer对字符编码# 并将字符串填充或裁剪到max_seq_length的长度...def get_label_ids(self, x):# 合并标签为一个list...def read_dataset(self, file_path, train=True):data = pd.read_csv(file_path)if train:data['label_ids'] = data.iloc[:, 2:].apply(self.get_label_ids, axis=1)label_ids = torch.tensor(list(data['label_ids'].values))# 英文预处理,包括去除停用词,大小写转换,删除无关字符,拆解单词等等preprocessor = EnglishPreProcessor()tqdm.pandas(desc="english preprocess")data['comment_text'] = data['comment_text'].progress_apply(preprocessor)# 对每一个comment_text做encode操作tqdm.pandas(desc="convert tokens to ids")data['input_ids'] = data['comment_text'].progress_apply(self.get_input_ids)input_ids = torch.tensor(list(data['input_ids'].values), dtype=torch.int)input_mask = torch.ones(size=(len(data), self.max_seq_length), dtype=torch.int)segment_ids = torch.zeros(size=(len(data), self.max_seq_length), dtype=torch.int)if train:dataset = Data.TensorDataset(input_ids, input_mask, segment_ids, label_ids)else:dataset = Data.TensorDataset(input_ids, input_mask, segment_ids)return dataset

我想如果前面输入输出部分大家看懂的话,read_dataset函数很容易看懂

模型训练

有几点需要注意一下,为了使用gpu,需要调用cuda方法将数据转移到gpu上,然后在反向传播计算梯度后,需要做一个梯度裁剪,即当梯度超过grad_clip的时候就把梯度设为grad_clip

def train(model, train_iter, valid_iter, n_epoch, loss, optimizer):history = {'train_loss': [],'val_loss': [],'val_acc': []}for epoch in range(n_epoch):train_loss, n = 0.0, 0for input_ids, input_mask, segment_ids, label_ids in tqdm(train_iter):input_ids = input_ids.cuda()input_mask = input_mask.cuda()segment_ids = segment_ids.cuda()logits = model(input_ids, segment_ids, input_mask)l = loss(logits, label_ids.float().cuda())l.backward()clip_grad_norm_(model.parameters(), grad_clip)optimizer.step()optimizer.zero_grad()train_loss += l.item()train_loss = train_loss/nval_loss, val_acc, n = 0.0, 0.0, 0with torch.no_grad():for input_ids, input_mask, segment_ids, label_ids in valid_iter:input_ids = input_ids.cuda()input_mask = input_mask.cuda()segment_ids = segment_ids.cuda()logits = model(input_ids, segment_ids, input_mask)label_ids = label_ids.float().cuda()l = loss(logits, label_ids)val_loss += l.item()val_acc += (torch.where(logits > 0.5, 1, 0) == label_ids).min(axis=1)[0].sum()n += len(label_ids)val_acc = val_acc / nval_loss = val_loss / nprint("epoch %s train loss:%s val loss:%s" % (epoch + 1, train_loss, val_loss))history['train_loss'].append(train_loss)history['val_acc'].append(val_acc)history['val_loss'].append(val_loss)# save model checkpointmodel.save_pretrained("models%s" % (epoch + 1))return history

把损失曲线和准确率曲线绘制出来就是这样

plt.plot(range(len(history['train_loss'])), history['train_loss'], label="train loss")
plt.show()
plt.plot(range(len(history['val loss'])), history['val loss'], label="val loss")
plt.show()
plt.plot(range(len(history['val acc'])), history['val_acc'])
plt.show()

暂时没图,待添加。。。

补充陈述

事实上,当我们评估多标签分类的模型的时候,前面只考虑了总体的Accuracy这个指标,但是还有很多更详细的metric需要考虑。这个也交给大家去查阅资料吧。

为了便于理解,前面的实现非常的粗糙,摒弃了很多好的优化策略。大家可以看看这个repo里面的实现,我就是参考的这个代码。代码里面在读取数据时设置了缓存机制,方便再次运行的时候快速读取数据,然后模型保存,日志输出,训练性能显存优化,模型评估等方面都有更好的处理。

至于我这份代码,后续应该会逐渐改进,如果大家有需要可以评论或私信留下邮箱地址。

补充:没想到要代码的人还有点多,发到评论区置顶了

Bert模型做多标签文本分类相关推荐

  1. bert 是单标签还是多标签 的分类_搞定NLP领域的“变形金刚”!教你用BERT进行多标签文本分类...

    大数据文摘出品 来源:medium 编译:李雷.睡不着的iris.Aileen 过去的一年,深度神经网络的应用开启了自然语言处理的新时代.预训练模型在研究领域的应用已经令许多NLP项目的最新成果产生了 ...

  2. bert 是单标签还是多标签 的分类_搞定NLP领域的“变形金刚”!手把手教你用BERT进行多标签文本分类...

    大数据文摘出品 来源:medium 编译:李雷.睡不着的iris.Aileen 过去的一年,深度神经网络的应用开启了自然语言处理的新时代.预训练模型在研究领域的应用已经令许多NLP项目的最新成果产生了 ...

  3. 【NLP】BERT 模型与中文文本分类实践

    简介 2018年10月11日,Google发布的论文<Pre-training of Deep Bidirectional Transformers for Language Understan ...

  4. 多标签文本分类研究进展概述

    多标签文本分类研究进展概述 1.多标签文本分类的研究还有很大的提升空间. 2.多标签文本分类的基本流程,包括数据集获取.文本预处理.模型训练和预测结果: 3.多标签文本分类的方法:传统机器学习的方法和 ...

  5. 基于Transformers库的BERT模型:一个文本情感分类的实例解析

    简介 本文来讲述BERT应用的一个例子,采用预训练好的BERT模型来进行演示.BERT的库来源于Transformers,这是一个由PyTorch编写的库,其集成了多个NLP领域SOTA的模型,比如b ...

  6. 【多标签文本分类】MSML-BERT模型的层级多标签文本分类方法研究

    ·阅读摘要:   本文在BERT模型上,提出了利用多任务架构来解决层级多标签文本分类问题. ·参考文献:   [1] MSML-BERT模型的层级多标签文本分类方法研究 [0] 摘要   在摘要中,作 ...

  7. 【BERT-多标签文本分类实战】之五——BERT模型库的挑选与Transformers

    ·请参考本系列目录:[BERT-多标签文本分类实战]之一--实战项目总览 ·下载本实战项目资源:>=点击此处=< [1] BERT模型库   从BERT模型一经Google出世,到tens ...

  8. 【BERT-多标签文本分类实战】之二——BERT的地位与名词术语解释

    ·请参考本系列目录:[BERT-多标签文本分类实战]之一--实战项目总览 ·下载本实战项目资源:>=点击此处=< [注]本篇将从宏观上介绍bert的产生和在众多模型中的地位,以及与bert ...

  9. 【多标签文本分类】融合CNN-SAM与GAT的多标签文本分类模型

    ·阅读摘要:   在本文中,作者基于CNN.Attention.GAT提出CS-GAT模型,在一些通用数据集上,表现良好. ·参考文献:   [1] 融合CNN-SAM与GAT的多标签文本分类模型   ...

最新文章

  1. Java基础 - 面向对象 - 构造方法
  2. P4178 Tree (点分治)
  3. C++ inline函数和宏
  4. python测试用例怎么写_Python单元测试unittest的具体使用示例
  5. LaTeX indicator function
  6. 大型网站HTTPS 实践(一)| HTTPS 协议和原理
  7. 量子计算机 计算混沌,深入了解量子混沌可能是量子计算机的关键
  8. 云图说|云上应用监控神器——应用性能监控APM2.0
  9. [原创]如何写好SqlHelper 之终章
  10. 将Windows网络适配器共享网络的ip:192.168.137.1 改为其他IP
  11. udp数据包大小问题
  12. (8)USB协议 —— 高速模式握手过程
  13. android短信验证码自动填写
  14. 问题 B: 零基础学C/C++25——判断某整数是正整数、负整数还是零
  15. 汽车电瓶电压12V验证
  16. 关于电气人奋斗的故事
  17. GPU的发展史,GPU与CPU的关系是什么?为何现在GPU越来越重要?
  18. Android的Logcat命令详解:翻译Enabling logcat Logging
  19. 用阿里云托管服务器怎么托管_云托管就像圣诞老人的4种方式
  20. [译] Python 的打包现状(写于 2019 年)

热门文章

  1. Python字符串类型
  2. php制作医院预约电子就诊卡
  3. 原生js实现公历转农历
  4. mysql ddl ddm_ddm 排行_大智慧DDL DDM排序源码
  5. 计算机与科学技术暑期社会实践,2018年暑期社会实践活动|计算机科学技术学院、软件学院...
  6. 金立下马、美图卖身、锤子病重:华米OV们耍得一手好心机
  7. 正则——只能允许是汉字、拼音和数字的正则表达式
  8. dotNet MSIL中的一些不常见IL指令
  9. 【我的第一个目标检测课题】3、Retinanet网络的学习与实现+扩展
  10. 想要软件外包,企业开发APP和小程序如何才能不吃亏?