Bert模型做多标签⽂本分类
Bert模型做多标签⽂本分类
我们现在来说,怎么把Bert应⽤到多标签⽂本分类的问题上。注意,本⽂的重点是Bert的应⽤,对多标签⽂本分类的介绍并不全⾯
单标签⽂本分类
对应单标签⽂本分类来说,例如⼆元的⽂本分类,我们⾸先⽤⼀层或多层LSTM提取⽂本序列特征,然后接⼀个dropout层防⽌过拟合,最后激活函数采⽤sigmoid,或者计算损失的时候使⽤sigmoid交叉熵损失函数。对于多元分类则激活函数采⽤softmax,其它没有差别
多标签⽂本分类
怎么从单标签分类问题拓展到多标签分类呢?
我们可以把⼆元分类的情况归并到多元分类
⾄少有以下两种⽅案(我懂的):
1,最后的全连接层以sigmoid作为激活函数,把每个神经元都当成是⼆元分类。另外,也可以直接把最后的全连接层改成n个全连接层,每个全连接层再接⼀个神经元做⼆元分类(激活函数是sigmoid),我认为⼆者本质上没有区别。
2,将多标签分类任务视作q2q的问题,对于给定的⽂本序列,⽣成不定长的标签序列。
这篇⽂章将介绍第⼀种⽅案。
⾸先我们先看看怎么使⽤Bert模型
下载transformers包,pip install transformers
其次⼿动下载模型,下载bert-ba-uncad的config.josn,,pytorch_model.bin三个⽂件
下载完成后,按照config.josn,,pytorch_model.bin重命名,放在bert-ba-chine⽂件夹下,此例中bert-ba-chine ⽂件夹放置在项⽬根⽬录下
导⼊包,加载预训练模型
import numpy as np
import torch
from transformers import BertTokenizer, BertConfig, BertForMaskedLM, BertForNextSentencePrediction
from transformers import BertModel
model_name ="bert-ba-chine"
# 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处理
n_code = de_plus("我爱你,你爱我")
得到的n_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]}
厦门万达广场
emdinput_ids就是每个字符在字符表中的编号,101表⽰[CLS]开始符号,[102]表⽰[SEP]句⼦结尾分割符号。
token_type_ids是区分上下句的编码,上句全0,下句全1,⽤在Bert的句⼦预测任务上
attention_mask表⽰指定哪些词作为query进⾏attention操作,全为1表⽰lf-attention,即每个词都作为query计算跟其它词的相关度将input_ids转化回token
#output:['[CLS]', '我', '爱', '你', '[SEP]', '你', '爱', '我', '[SEP]']
Bert的输⼊是三个embedding的求和,token embedding,gment embedding和position embedding
# token embedding
tokens_tensor = sor([n_code['input_ids']])# 添加batch维度
# gment embedding
gments_tensors = sor([n_code['token_type_ids']])# 添加batch维度
输出
Bert是按照两个任务进⾏预训练的,分别是遮蔽语⾔任务(MLM)和句⼦预测任务。
我先简单解释⼀下这两个任务
遮蔽语⾔任务(Masked Language Model)
对输⼊的语句中的字词 随机⽤ [MASK] 标签覆盖,然后模型对mask位置的单词进⾏预测。这个过程类似CBOW训练的过程,我们利⽤这个训练任务从⽽得到每个字符对应的embedding。特别的,[CLS]字符的embedding我们可以视为整个句⼦的embedding。我们可以理解为[CLS]字符跟句⼦中的其它字符都没有关系,能较为公平的考虑整个句⼦。
句⼦预测任务(NextSentence Prediction)
该任务就是给定⼀篇⽂章中的两句话,判断第⼆句话在⽂本中是否紧跟在第⼀句话之后。如果我们训练的时候将问题和答案作为上下句作为模型输⼊,该任务也可以理解为判断问题和答案是否匹配
现在我们根据代码看看bert的输出
bert_model.eval()
_grad():
outputs = bert_model(tokens_tensor, token_type_ids = gments_tensors)
encoded_layers = outputs # outputs类型为tuple
最后⼀个隐藏层的输出,即遮蔽语⾔任务的输出,亦即每个字符的embedding
print("quence output",encoded_layers[0].shape)
# quence output torch.Size([1, 9, 768])
考虑全部隐藏层的第⼀个输出,然后进⾏pool操作的结果,所谓的pool操作就是接⼀个全连接层+tanh激活函数层。它可以作为整个句⼦的语义表⽰,但也有将所有单词的平均作为句⼦的表⽰的做法brave
print("pooled output",encoded_layers[1].shape)
# pooled output torch.Size([1, 768])
所有隐藏层的输出,hidden_states有13个元素,第⼀个是[CLS]的embedding,后⾯12个元素表⽰12个隐藏层的输出,对于q2q 的任务,它们将作为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经过lf-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的输⼊输出之后我们就可以试着做fine-tune了,我们是要做多标签⽂本分类,根据第⼀个⽅案,我们⾸先提取出⽂本的特征,然后接全连接层,最后接⼀个sigmoid激活函数。
前⾯已经说过,pooled output就是表⽰bert得到的整个句⼦的语义特征,这正是我们需要的。将这个特征作为全连接层的输⼊即可。代码⾥⾯还定义了dropout层,这都是训练的常⽤技巧,防⽌过拟合
在线读英语class BertForMultiLabel(BertPreTrainedModel):
def__init__(lf, config):
super(BertForMultiLabel, lf).__init__(config)
lf.bert = BertModel(config)
lf.dropout = nn.Dropout(config.hidden_dropout_prob)
lf.classifier = nn.Linear(config.hidden_size, config.num_labels)
lf.sigmoid = nn.Sigmoid()
def forward(lf, input_ids, token_type_ids=None, attention_mask=None, head_mask=None):
outputs = lf.bert(input_ids, token_type_ids,attention_mask,head_mask)
pooled_output = outputs[1]
pooled_output = lf.dropout(pooled_output)
logits = lf.classifier(pooled_output)
return lf.sigmoid(logits)
def unfreeze(lf, start_layer, end_layer):
def children(m):
unequivocalreturn m if isinstance(m,(list,tuple))el list(m.children())
def t_trainable_attr(m, b):
for p in m.parameters():
def 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 t_trainable(l, b):
apply_leaf(l,lambda m: t_trainable_attr(m, b))
t_trainable(lf.bert,Fal)kind of
for i in range(start_layer, end_layer +1):
t_trainable(der.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)
clotomodel = model.cuda()
加载处理数据集
⼀个模型想要跑起来必然需要数据输⼊,Bert对参与训练的数据格式要求为input_ids, input_mask, gment_ids, label_ids。⽽原始的数据格式为string,label_ids
所以我们需要对数据做⼀些处理,为此我们定义⼀个BertProcessor类,这个类的主要⽅法为read_datat和train_val_split。
注意我现在的做法和那些好的做法有很多差别,那些好的做法是基于优化的考虑,但我们现在暂时不
⽤考虑这么多,把重⼼放在bert的使⽤和模型的成功训练上,优化做法读者可进⼀步研究。
先看类中部分代码,完整项⽬在最后
class BertProcessor:
def__init__(lf, vocab_path, do_lower_ca, max_q_length)->None:
lf.max_q_length = max_q_length
def get_input_ids(lf, x):
# 使⽤tokenizer对字符编码
wcdma
# 并将字符串填充或裁剪到max_q_length的长度
...
四级作文万能句子
def get_label_ids(lf, x):
# 合并标签为⼀个list
...
def read_datat(lf, file_path, train=True):
data = pd.read_csv(file_path)
if train:
data['label_ids']= data.iloc[:,2:]._label_ids, axis=1)
label_ids = sor(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")
pimp
data['input_ids']= data['comment_text'].progress__input_ids)
input_ids = sor(list(data['input_ids'].values), dtype=torch.int)
input_mask = s(size=(len(data), lf.max_q_length), dtype=torch.int)
gment_ids = s(size=(len(data), lf.max_q_length), dtype=torch.int)
if train:
datat = Data.TensorDatat(input_ids, input_mask, gment_ids, label_ids)
el:
datat = Data.TensorDatat(input_ids, input_mask, gment_ids)
return datat
我想如果前⾯输⼊输出部分⼤家看懂的话,read_datat函数很容易看懂
模型训练
有⼏点需要注意⼀下,为了使⽤gpu,需要调⽤cuda⽅法将数据转移到gpu上,然后在反向传播计算梯度后,需要做⼀个梯度裁剪,即当梯度超过grad_clip的时候就把梯度设为grad_clip