Bert系列:基于Bert微调快速实现多标签文本分类

多标签文本分类定义和应用场景

文本分类是指对形如文章,新闻,舆情,评论,用户输入的词条等自然语言文本数据,根据某个业务维度进行自动归类的技术。
多标签分类是指文本可以被归类为一种或者多种不同的类目下,同一个文本实例可以有多个类别标签。相比于多元分类(文本只能归属于一类),多标签文本分类在实际的场景中更为常见,例如

  • 1.新闻根据主题归类,比如某篇描述国际冲突的新闻既可以归为国际政治,也可以归类到军事主题。
  • 2.舆情根据事件类型归类,比如某篇描述土地污染的與情,涉及政府,工厂,开发商,业主等多方,该舆情可以被归类为环境污染处罚,重大事故损失,房企暴雷,消费者维权等多个事件类型。
  • 3.用户输入根据意图归类,例如在用户在APP的搜索栏输入的短语包括“变形金刚”,用户的意图既可以是指电影,也可能是游戏,玩具。

Bert微调网络结构简述

在前文Bert系列:Bert、Transformer、预训练模型、微调 简单入门介绍中介绍了Bert是基于Transformer的编码器和海量文本训练出的预训练模型,他通过学习完型填空和上下句预测的任务来获得类似人类一样对通用语义的理解能力。在文本分类场景,只需要将原始文本输入到Bert中,就可以利用到预训练的token embedding知识以及Bert的self Attention结构直接将文本转化为对应的特征向量,其中向量的第一个位置[CLS]单独用来给下游分类任务使用的,比如文本分类,情感分类,一对文本的相似匹配分类等等,Bert对下游分类任务的微调网络示意图如下

下游任务为分类的Bert微调网路结构

只需要将输入[CLS]+句子+[SEP]传入到BERT中,然后取出[CLS]位置的向量做分类就行,微调任务需要学习的参数包括最后一层的分类器,Bert中的token embedding,位置编码,Self Attention中的线性映射,以及各种全连接等。


多标签分类的损失函数

对于多元文本分类,文本只能属于唯一一类,此时通常采用Softmax交叉熵,而对于多标签分类场景,文本可以属于多个类别,此时Softmax交叉熵不再适用,一般采用每个类别的Sigmoid交叉熵的形式。令一共最大有n个候选类别,某条样本只属于其中的k个类别,则当n>>k时会出现类别不均衡问题,此时损失会受到(n-k)类稀疏部分的影响,导致有价值的k类别损失被平均掉,使得模型收敛缓慢。
令一共10个类别,一个批次下2条样本各命中其中的1个类别,用PyTorch来验证下类别不均衡问题的存在

>>> import torch
>>> 
>>> y_pred = torch.tensor([[0.1] * 10, [0.1] * 10])
>>> y_true = torch.tensor([[1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]])
>>> 
>>> criterion1 = torch.nn.BCELoss(reduction="none")
>>> loss1 = criterion1(y_pred, y_true)
>>> criterion2 = torch.nn.BCELoss(reduction="mean")
>>> loss2 = criterion2(y_pred, y_true)
>>> 
>>> loss1
tensor([[2.3026, 0.1054, 0.1054, 0.1054, 0.1054, 0.1054, 0.1054, 0.1054, 0.1054,
         0.1054],
        [0.1054, 2.3026, 0.1054, 0.1054, 0.1054, 0.1054, 0.1054, 0.1054, 0.1054,
         0.1054]])
>>> loss2
tensor(0.3251)

loss1表明多标签分类的Sigmoid交叉熵会对每个类别位置计算一次loss,最后采用所有位置的均值作为该批次的最终损失,在该案例中剩下的9个位置的类别损失占了大部分的项数,导致实际命中的类别损失不明显,对比下二分类中交叉熵损失,仅拿出命中类目的预测值和实际标签值如下

>>> y_pred = torch.tensor([0.1, 0.1])
>>> y_true = torch.tensor([1.0, 1.0])
>>> 
>>> criterion1 = torch.nn.BCELoss(reduction="none")
>>> loss1 = criterion1(y_pred, y_true)
>>> criterion2 = torch.nn.BCELoss(reduction="mean")
>>> loss2 = criterion2(y_pred, y_true)
>>> 
>>> loss1
tensor([2.3026, 2.3026])
>>> loss2
tensor(2.3026)

二分类中交叉熵损失隔绝了剩余稀疏多标签的影响,该批次的最终损失为2.3026,明显高于多标签的0.3251。这种方式等同于Softmax交叉熵,代码如下

>>> y_pred = torch.tensor([[0.1] * 10, [0.1] * 10])
>>> y_true = torch.tensor([[1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]])
>>> 
>>> criterion1 = torch.nn.CrossEntropyLoss(reduction="none")
>>> loss1 = criterion1(y_pred, y_true)
>>> criterion2 = torch.nn.CrossEntropyLoss(reduction="mean")
>>> loss2 = criterion2(y_pred, y_true)
>>> 
>>> loss1
tensor([2.3026, 2.3026])
>>> loss2
tensor(2.3026)

该种形式只会对实际标签为1的位置求损失,公式如下,其中S为输出得分,P为每个位置的实际onehot标签值

Softmax+交叉熵

因此在多标签文本分类中采用逐位的sigmoid交叉熵需要额外对样本做均衡调整,另一种方式是采用Softmax交叉熵变形。

作者从交叉熵公式的本质出发,将上面公式只定格在有意义的P(t)=1的位置,Softmax转化为目标预测值和其他位置预测值的差,形成logsumexp的形式,其中St代表目标标签为1的预测得分,Si为其他标签为0位置的预测得分

目标预测值和其他位置预测值的差

而logsumexp实际上是max函数的光滑近似,进一步推导如下

Softmax交叉熵近似

显然期望loss越小就是希望所有Sn-St的最大值最小化,如果St刚好是最大预测值,则St-St为最大值,此时loss接近为0,否则loss为一个正数,大小和最大预测值Sn和St的差距相关。
同理作者将把这种本质思想迁移到多标签分类上来,在单标签场景期望的是St比其他位置Sn都大,而多标签场景改为正例集合中所有得分都大于每个正例所对应的负例集合中的每个得分,公式如下

多标签分类Softmax

其中1为当i和j相同时的结果,作者额外引入了一个S0得分代表阈值,期望所有负例得分都能小于S0,所有正例得分都能大于S0,虽然这个log里面有4项,但是本质只是增加了一系列两两得分相减,最终还是被一个全局的max压缩拿到最大值作为loss。
上式可以把1拆开做因式分解,进一步转化为

多标签分类Softmax推导

如果指定阈值为0,那么就简化为

多标签分类Softmax简化

此公式本质上是比对所有标类别得分与非目标类别得分的差距,最后使用logsumexp聚合为最大值,自动隔绝了非最大值的影响,不存在类别不均衡问题。PyTorch实现代码如下

def multi_label_categorical_cross_entropy(y_true, y_pred):
    """
    :param y_true: [batch_size, cate_size]
    :param y_pred: [batch_size, cate_size]
    :return:
    """
    y_pred = (1 - 2 * y_true) * y_pred  # 预测得分将正例乘以-1,负例乘以1
    y_pred_neg = y_pred - y_true * 1e12  # 预测得分neg集合,将正例变为负无穷,负例保持不变
    y_pred_pos = y_pred - (1 - y_true) * 1e12  # 预测得分pos集合,将负例变为负无穷,正例保持不变
    zeros = torch.zeros_like(y_pred[..., :1])
    y_pred_neg = torch.cat([y_pred_neg, zeros], dim=-1)  # 0阈值
    y_pred_pos = torch.cat([y_pred_pos, zeros], dim=-1)
    neg_loss = torch.logsumexp(y_pred_neg, dim=-1)
    pos_loss = torch.logsumexp(y_pred_pos, dim=-1)
    return torch.mean(neg_loss + pos_loss)

其中第一行将预测得分将正例乘以-1,负例乘以1,目的是得到前项负例集合中的Si和后项正例集合中的-Sj,第二第三行代码负责将neg和pos集合位置的得分挑选出来,对非该集合的得分赋予一个负无穷大的值,使得e的指数次方接近于0从而对logsumexp没有影响,加入zeros的目的是构造前后两项log中的1,最终将两个logsumexp相加求平均即可获得整个批次的多分类Softmax交叉熵损失。


Bert多标签文本分类在PyTorch下的实现

采用新闻主题分类,一共94个类别,单条样本如下

text:'青岛市 大气 11月29日,莱西市政府组织店埠镇政府、环保局对信访件反映的问题进行了调查核实,有关情况如下: 经现场调查,该锅炉位于店埠镇政府院内北侧,检查时锅炉正在运行,未配套相应的污染防治设施,燃烧废气直接排放。'
label:['破坏生态环保']

(1)数据准备

基于PyTorch的Dataset,DataLoader快速将数据改造为批次格式,使用huggingface的Bert分词器将文本编码为数值id

def collate_fn(data):
    texts, labels = [], []
    for d in data:
        texts.append(d[0])
        one_hot_label = [0] * LABEL_NUM
        for label in d[1]:
            one_hot_label[LABEL2ID[label]] = 1
        labels.append(one_hot_label)
    # truncate, padding, token, convert_ids
    encode = TOKENIZER.batch_encode_plus(texts, truncation=True, max_length=PRE_TRAIN_CONFIG.max_position_embeddings,
                                         return_tensors="pt", padding=True)
    input_ids = encode["input_ids"].to(DEVICE)
    attention_mask = encode["attention_mask"].to(DEVICE)
    token_type_ids = encode["token_type_ids"].to(DEVICE)
    labels = torch.FloatTensor(labels).to(DEVICE)

    return input_ids, attention_mask, token_type_ids, labels

train_data, val_data, test_data = random_split(total_data, (train_size, val_size, test_size))
train_loader = DataLoader(train_data, shuffle=True, drop_last=False, batch_size=32, collate_fn=collate_fn)

(2)模型构建

定义PyTorch网络模块,调用Bert预训练模型输出每个位置词的embedding,拿到第一个位置[CLS]用于分类,加一层线性层Linear得到每个类别的得分。

PRE_TRAIN = AutoModel.from_pretrained(PRE_TRAIN_PATH)

class Model(nn.Module):
    def __init__(self):
        super(Model, self).__init__()
        self.pre_train = PRE_TRAIN
        self.linear = nn.Linear(in_features=PRE_TRAIN_CONFIG.hidden_size, out_features=LABEL_NUM)
        nn.init.xavier_normal_(self.linear.weight.data)

    def forward(self, input_ids, attention_mask, token_type_ids):
        cls_emb = self.pre_train(input_ids, attention_mask)[0][:, 0, :]
        linear_out = self.linear(cls_emb)  # [None, 94]
        return linear_out  # [None, 94]

(3)损失函数

模型损失直接前文实现的multi_label_categorical_cross_entropy函数

prob = model(input_ids, attention_mask, token_type_ids)
loss = multi_label_categorical_cross_entropy(labels, prob)

如果采用二分类交叉熵的形式需要在model中对输出加上sigmoid,同时采用BCELoss来进行计算损失

    ...
    def forward(self, input_ids, attention_mask, token_type_ids):
        cls_emb = self.pre_train(input_ids, attention_mask)[0][:, 0, :]
        linear_out = self.linear(cls_emb)  # [None, 94]
        return torch.sigmoid(linear_out)  # [None, 94]

criterion = nn.BCELoss(reduction="mean")
loss = criterion(prob, labels)

(4)模型训练

采用每50步验证集连续10次f1值不上升作为早停条件,在学习率模块添加预热学习率,差分学习率等技巧

    model = Model().to(DEVICE)
    epochs = 20
    pre_train_lr = 3e-5
    optimizer, schedule = get_optimizer_schedule(model, pre_train_lr=pre_train_lr, other_lr=pre_train_lr * 100,
                                                 warm_step=50, total_step=len(train_loader) * epochs,
                                                 weight_decay=0.001)
    handler = train_handler(model, model_path="./topic_cls/model.bin", acc_name="f1", num_windows=10)
    early_stop_flag = False

    for epoch in range(epochs):
        for step, (input_ids, attention_mask, token_type_ids, labels) in enumerate(train_loader):
            model.train()
            optimizer.zero_grad()
            prob = model(input_ids, attention_mask, token_type_ids)
            loss = multi_label_categorical_cross_entropy(labels, prob)
            loss.backward()
            optimizer.step()
            schedule.step()
            step += 1
            print("epoch: {}, step: {}, loss: {}".format(epoch + 1, step, loss.item()))
            if step % 50 == 0:
                loss_val, acc_val, pre_val, recall_val, f1_val, report = eval_metrics(model, val_loader)
                print("[evaluation] loss: {} acc: {} pre: {} recall: {} f1: {}".format(loss_val, acc_val, pre_val,
                                                                                       recall_val, f1_val))
                # print(report)
                handler.get_step_metrics(f1_val)
                if handler.early_stop():
                    early_stop_flag = True
                    print("early stop...")
                    break
        if early_stop_flag:
            break

(5)模型效果对比

分别采用逐位二分类交叉熵损失和多标签Softmax交叉熵两种损失来训练模型,早停后最佳模型在测试集合的表现如下

损失函数策略逐位sigmoid交叉熵多标签softmax交叉熵
loss0.00460.3361
accuracy0.89430.9155
precision0.93710.9463
recall0.94170.9594
f10.93940.9528

在该数据集上使用多标签Softmax交叉熵的效果明显优于逐位二分类交叉熵。
从训练过程来看,每50步记录一次验集的f1提升情况,使用多标签Softmax交叉熵的模型能在训练早期快速收敛达到0.5以上的f1,而使用逐位二分类交叉熵的模型在第一个验证步长上f1还是停留在0,需要更多步长的训练才能稳定收敛到较高的f1水平。

验证集每50步的f1值

(6)模型预测应用

构建一个预测Predictor类,封装文本输出的预处理和模型调用预测, 在输出层加入sigmoid来获取每个类别的置信度。

class Predictor:
    def __init__(self):
        self.model = Model()
        self.model.load_state_dict(torch.load(os.path.join(ROOT_PATH, "./topic_cls/model.bin")))
        self.max_length = PRE_TRAIN_CONFIG.max_position_embeddings
        self.tokenizer = TOKENIZER
        self.device = "cpu"
        self.model.to(self.device)
        self.model.eval()
        self.id2label = ID2LABEL

    def preprocess(self, texts: list):
        encode = self.tokenizer.batch_encode_plus(texts, padding=True, truncation=True, max_length=self.max_length,
                                                  return_tensors="pt")
        input_ids = encode["input_ids"].to(self.device)
        attention_mask = encode["attention_mask"].to(self.device)
        token_type_ids = encode["token_type_ids"].to(self.device)
        return input_ids, attention_mask, token_type_ids

    def get_topic_type(self, texts):
        assert isinstance(texts, list), "输入非文本集合"
        res = [[] for _ in range(len(texts))]
        input_ids, attention_mask, token_type_ids = self.preprocess(texts)
        with torch.no_grad():
            sigmoid_out = torch.sigmoid(self.model(input_ids, attention_mask, token_type_ids)).detach().cpu().numpy()
            binary_out = (sigmoid_out > 0.5).astype(float)
            axis_1, axis_2 = np.where(binary_out == 1)
            for i, j in zip(axis_1, axis_2):
                res[i].append({"topic_type": self.id2label[j], "conf": sigmoid_out[i, j]})

        return res

输入一批文本预测主题类别如下

>>> predictor = Predictor()
>>> texts = ["青岛市 大气 11月29日,莱西市政府组织店埠镇政府、环保局对信访件反映的问题进行了调查核实,有关情况如下: 经现场调查,该锅炉位于店埠镇政府院内北侧,检查时锅炉正在运行,未配套相应的污染防治设施,燃烧废气直接排放。"]
>>> for i in predictor.get_event_type(texts):
>>>   print(i)

>>> [{'event_type': '破坏生态环保', 'conf': 0.99510044}]

最后的最后

感谢你们的阅读和喜欢,我收藏了很多技术干货,可以共享给喜欢我文章的朋友们,如果你肯花时间沉下心去学习,它们一定能帮到你。

因为这个行业不同于其他行业,知识体系实在是过于庞大,知识更新也非常快。作为一个普通人,无法全部学完,所以我们在提升技术的时候,首先需要明确一个目标,然后制定好完整的计划,同时找到好的学习方法,这样才能更快的提升自己。

这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

一、全套AGI大模型学习路线

AI大模型时代的学习之旅:从基础到前沿,掌握人工智能的核心技能!

img

二、640套AI大模型报告合集

这套包含640份报告的合集,涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师,还是对AI大模型感兴趣的爱好者,这套报告合集都将为您提供宝贵的信息和启示。

img

三、AI大模型经典PDF籍

随着人工智能技术的飞速发展,AI大模型已经成为了当今科技领域的一大热点。这些大型预训练模型,如GPT-3、BERT、XLNet等,以其强大的语言理解和生成能力,正在改变我们对人工智能的认识。 那以下这些PDF籍就是非常不错的学习资源。

img

四、AI大模型商业化落地方案

img
这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

  • 11
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值