多标签文本分类

多标签文本分类是指一个输入文本样本对应有多种标签。本文是一个训练多标签文本分类任务的实例:

训练过程

main.py

导入相关的库:

# coding=utf-8
import os
# os.environ["CUDA_VISIBLE_DEVICES"] = "2"
os.environ["WANDB_DISABLED"] = "true"  # 禁用wandb
'''
from huggingface_hub import snapshot_download
# 将预训练模型下载到指定目录(cache_dir参数指定保存目录)
snapshot_download(repo_id='bert-base-chinese', cache_dir='.')
'''

import json
import torch
from sklearn.preprocessing import MultiLabelBinarizer
from transformers import AutoModelForSequenceClassification, AutoTokenizer, DataCollatorWithPadding, TrainingArguments
from custom_trainer import CustomTrainer
from datasets import load_dataset

tokenizer及数据加载:

tokenizer = AutoTokenizer.from_pretrained('models--bert-base-chinese')
data = load_dataset("json", data_files='preprocessed_data/labeled_data.json')

利用MultiLabelBinarizer处理标签,利用MultiLabelBinarizer处理标签的好处是:在后续利用MultiLabelBinarizer相关方法向模型传入label时,模型会自动识别为多标签模型(在模型的全连接层使用sigmoid函数而不是softmax函数)

label_encoder = MultiLabelBinarizer()
label_encoder = label_encoder.fit(data['train']['labels'])

保存一个标签ID到标签名的映射文件,便于后续得到测试文本对应的标签名

save_batch = 64  # batch size
epochs = 100     # epoch num
if not os.path.exists(f"model_save_epochs{epochs}_batch{save_batch}"):
    os.mkdir(f"model_save_epochs{epochs}_batch{save_batch}")
    
with open(f"model_save_epochs{epochs}_batch{save_batch}/labelmap.json", 'w', encoding='utf-8') as fw:
    json.dump({'LABEL_' + str(ind): label_name for ind, label_name in enumerate(label_encoder.classes_)}, fw,
              ensure_ascii=False, indent=1)

将加载到的数据集分割成训练集和验证集

data = data['train'].train_test_split(test_size=0.1, shuffle=True)

构造模型的输入输出数据

def preprocess_function(examples):
    model_inputs = tokenizer(examples['text'], max_length=300, truncation=True) 
    labels = [labels for labels in examples['labels']]
    model_inputs['labels'] = label_encoder.transform(labels).astype(float)  # 将标签名转换成标签id
    return model_inputs


data = data.map(preprocess_function, batched=True, batch_size=100)

加载预训练模型

model = AutoModelForSequenceClassification.from_pretrained('models--bert-base-chinese',
                                                           num_labels=label_encoder.classes_.size, # 标签类别数
                                                           # problem_type 指明模型训练类型为多标签文本分类,但在实际测试时发现:
                                                           # 即使不设置此参数,模型也仍使用的是多标签文本分类方式训练
                                                           # 可能是因为前面用MultiLabelBinarizer处理标签后,模型识别出要训练多标签文本分类
                                                           problem_type='multi_label_classification'
                                                           )

使用huggingface/Datasets方式加载数据时,可以用DataCollator达到批处理的效果。这里可能会有读者好奇,数据并没有做填充处理,后面如何以batch输入模型进行训练。事实上,在tokenizer时做填充处理是没有问题的。但是如果在这个阶段中做填充,会将所有数据的长度都填充到300,如果训练时一个batch中的数据中都是短文本,那数据中将有大量的填充值,影响计算效率。该如何解决这一问题呢?

我们可以让其在取出一个batch的数据之后再根据batch内数据的最大长度进行填充,例如用Dataloader加载数据,我们可以指定collate_fn,但是这个函数需要我们自行实现。在transformers中,我们则可以使用DataCollatorWithPadding,实例化该类并在Trainer中指定即可。

data_collator = DataCollatorWithPadding(tokenizer=tokenizer)

获取模型训练参数:

args = TrainingArguments(
    learning_rate=2e-5,
    per_device_train_batch_size=save_batch,
    per_device_eval_batch_size=128,
    num_train_epochs=epochs,
    weight_decay=0.01,
    output_dir=f"model_save_epochs{epochs}_batch{save_batch}",
    logging_steps=10,
    evaluation_strategy="epoch",
    save_strategy="epoch",
    save_total_limit=3,
    load_best_model_at_end=True,
    metric_for_best_model="eval_loss",
    fp16=True,
    disable_tqdm=False,
)

自定义验证

def compute_metrics(eval_pred):
    predictions, labels = eval_pred
    # BCEWithLogitsLoss 用于计算多标签损失,后文将具体介绍
    loss = torch.nn.BCEWithLogitsLoss()(torch.Tensor(predictions), torch.tensor(labels))
    return {'eval_loss': loss.cpu().numpy()}

进行训练:

 # CustomTrainer中重写了compute_loss()函数,目的是检验模型训练过程中的损失计算结果是否与BCEWithLogitsLoss计算结果一致
 # 经测试,结果是一致的,大家可以直接使用Trainer进行训练即可
trainer = CustomTrainer( 
  model,
  args=args,
  train_dataset=data["train"],
  eval_dataset=data["test"],
  tokenizer=tokenizer,
  compute_metrics=compute_metrics,
  data_collator=DataCollatorWithPadding(tokenizer=tokenizer)
)

trainer.train()

custom_trainer.py

from transformers import Trainer
import torch
class CustomTrainer(Trainer):
    def compute_loss(self, model, inputs, return_outputs=False):
        """
        How the loss is computed by Trainer. By default, all models return the loss in the first element.

        Subclass and override for custom behavior.
        """
        labels = inputs.get('labels')
        outputs = model(**inputs)
        logits = outputs.get('logits')
        loss = torch.nn.BCEWithLogitsLoss()(logits, labels)
        return (loss, outputs) if return_outputs else loss

pipeline.py

from transformers import pipeline
import json

with open('model_save_epochs100_batch1/labelmap.json')as fr:
    label_id2name = json.load(fr)

with open('raw_data/diseasecontent.json', 'r', encoding='utf-8') as fr:
    texts = [text.strip() for text in json.load(fr)if text.strip("000").strip()]

text = ["", "",]
classifier = pipeline(
    task="text-classification", model="model_save_epochs100_batch1/checkpoint-325809",
    function_to_apply='sigmoid', top_k=10,  device=0  # return_all_scores=True
)
res = [] # 保存结果
for text in texts:
    labels = []
    for ite in classifier(text, truncation=True, padding=True, max_length=512):  
        if ite['score'] > 0.5:
            labels.append(label_id2name[ite['label']])
        else:
            break

    res.append({'text': text, 'labels': labels})

Pytorch之BCELoss和BCEWithLogitsLoss

BCELoss是
$$
-\frac{1}{n}\sum (y_{n} \times ln X_{n} + (1-y_{n})\times ln(1-X_{n}))
$$
其中y是标签(target),x是模型输出的值(sigmoid(logits))。

所以对于给定

logits = [[-0.4089, -1.2471, 0.5907], 
          [-0.4897, -0.8267, -0.7349], 
          [0.5241, -0.1246, -0.4751]]

x = [[0.3992 , 0.2232, 0.6435 ],
	[0.3800 , 0.3044, 0.3241], 
	[0.6281, 0.4689, 0.3834]]
	
y = [[0, 1, 1],
    [0, 0, 1], 
    [1, 0, 1]]

所以其计算过程

第一行:
第一列 0 × ln ⁡ 0.3992 + ( 1 − 0 ) × ln ⁡ ( 1 − 0.3992 ) = − 0.5095
第二列 1 × ln ⁡ 0.2232 + ( 1 − 1 ) × ln ⁡ ( 1 − 0.2232 ) = − 1.4997
第三列 1 × ln ⁡ 0.6435 + ( 1 − 1 ) × ln ⁡ ( 1 − 0.6435 ) = − 0.4408
第二行:
第一列 0 × ln ⁡ 0.3800 + ( 1 − 0 ) × ln ⁡ ( 1 − 0.3800 ) = − 0.4780
第二列 0 × ln ⁡ 0.3044 + ( 1 − 0 ) × ln ⁡ ( 1 − 0.3044 ) = − 0.3630
第三列 1 × ln ⁡ 0.3241 + ( 1 − 1 ) × ln ⁡ ( 1 − 0.3241 ) = − 1.1267
第三行:
第一列 1 × ln ⁡ 0.6281 + ( 1 − 1 ) × ln ⁡ ( 1 − 0.6281 ) = − 0.4651
第二列 0 × ln ⁡ 0.4689 + ( 1 − 0 ) × ln ⁡ ( 1 − 0.4689 ) = − 0.6328
第三列 1 × ln ⁡ 0.3834 + ( 1 − 1 ) × ln ⁡ ( 1 − 0.3834 ) = − 0.9587

$$
-\frac{-0.5095 - 1.4997 - 0.4408}{3} = 0.8167
$$
$$
-\frac{-0.4780 - 0.3630 - 1.1267 }{3} = 0.6559
$$

$$
-\frac{-0.4651 - 0.6328 - 0.9587}{3} = 0.6855
$$

故而BCE损失为:
$$
BCELoss \approx \frac{-0.8167 - 0.6559- 0.9587}{3} = 0.7194
$$

利用BCELoss代码实现如下:

from torch import nn
import torch
m = nn.Sigmoid()
loss = nn.BCELoss()
logits = torch.Tensor([[-0.4089, -1.2471, 0.5907], 
                       [-0.4897, -0.8267, -0.7349], 
                       [0.5241, -0.1246, -0.4751]])
target = torch.Tensor([[0, 1, 1], 
                       [0, 0, 1], 
                       [1, 0, 1]])
pre = m(logits)
output = loss(pre, target)
print(output)

# 输出为:0.7193

利用BCEWithLogitsLoss实现:

print(nn.BCEWithLogitsLoss()(logits, target))
# 输出为:0.7193

BCEWithLogitsLoss() 中包含了sigmoid过程

BCELoss参考链接:https://blog.csdn.net/qq_22210253/article/details/85222093

posted @ 2022-10-18 17:23  teanon  阅读(1341)  评论(1编辑  收藏  举报