简介
在《【NLP】多标签分类》系列的上一篇文章中,我们深入探讨了三种机器学习方法:Binary
Relevance (BR)、Classifier Chains (CC) 以及 Label Powerset
(LP),旨在解决多标签分类的挑战。这些方法各展所长,为我们提供了不同角度解析和处理多标签问题的视角。继先前对这些机器学习方法的详尽分析之后,本篇文章转向更为先进的解决策略——专注于序列生成方法,并以Transformer模型的一种变体,即T5预训练模型为核心,进行实验探索。
本文将不仅详细介绍如何利用T5模型对多标签分类任务进行微调,而且还将通过实验对比,展现其相较于之前讨论的传统方法在性能上的优势和潜在应用价值。通过精心设计的实验和深入的结果分析,揭示序列生成方法特别是Transformer架构的强大能力和灵活性。
个人博客与相关链接
本文相关代码和数据集已同步上传github: issey_Kaggle/MultiLabelClassification
at main · iceissey/issey_Kaggle (github.com)
本文代码(Notebook)已公布至kaggle: Transformer-Multi-Label-Classification
(kaggle.com)
博主个人博客链接:issey的博客 -
愿无岁月可回首
实验数据与任务说明
数据来源:Multi-Label
Classification Dataset (kaggle.com)
任务说明:
背景:NLP——多标签分类数据集。
内容:该数据集包含6个不同的标签(计算机科学、物理学、数学、统计学、定量生物学、定量金融),用于根据摘要和标题对研究论文进行分类。
标签列中的值1表示该标签属于该论文,每篇论文可以有多个标签为1。
模型介绍
Transformer模型自从2017年由Vaswani等人在论文《Attention Is All You
Need》中首次提出以来,已经证明了其在多种自然语言处理任务上的强大能力。尽管本文不会深入讲解Transformer的详细架构及其组成模块,我们仍然强烈推荐感兴趣的读者参考原始论文以获得全面的理解。
Transformer的创新之处在于其独特的自注意力机制,使其能够在处理文本时更有效地捕捉长距离依赖关系。这一特性不仅提高了处理速度,还提升了模型对文本的理解深度,打开了自然语言处理领域的新篇章。以下是Transformer在NLP领域的一些关键应用:
文本分类 :Transformer能够理解复杂的文本结构和语义,使其在文本分类任务上表现优异,包括情感分析、主题识别等。
机器翻译 :由于其强大的语言模型能力,Transformer模型已成为机器翻译领域的主导技术,提供了更加流畅和准确的翻译结果。
文本摘要 :Transformer模型能够理解和提取文本的关键信息,生成准确且连贯的摘要,无论是抽取式还是生成式摘要。
问答系统 :利用其深度理解能力,Transformer能够从大量文本中提取答案,为问答系统提供强有力的支持。
语言生成 :Transformer的变体,如GPT系列,已经展示了在生成文本、编写代码等任务上的卓越能力,推动了创造性文本生成和自动编程的新发展。
Hugging Face
在深入探讨如何将Transformer模型应用于多标签分类任务之前,让我们先了解一下Hugging
Face。作为一个致力于推进机器学习技术民主化的开源社区和公司,Hugging
Face为研究者和开发者们提供了丰富的预训练模型库及相关工具,极大地简化了NLP任务的开发流程。
官网链接:Hugging Face – The AI
community building the future.
作为一个广泛使用的Python库,Hugging
Face的Transformers库集合了数百种预训练的Transformer模型,支持轻松应用于文本分类、文本生成、问答等多种NLP任务。该库的一个主要优势是其提供了统一的接口,让不同的Transformer模型,比如BERT、GPT-2、RoBERTa等,在几乎不需修改代码的情况下就能互相替换使用。
社区支持和资源
Hugging
Face不仅提供预训练模型,还维护着一个充满活力的社区,社区成员在此分享经验、解决方案及最佳实践。这样的平台为初学者和专家提供了交流与学习的机会,进一步推动了NLP领域的发展。更进一步,Hugging
Face也提供了模型共享平台,允许研究者和开发者上传及分享自己训练的模型,进一步增强了社区资源。
预训练模型的应用
对于多标签分类任务而言,Hugging
Face的Transformers库开辟了一个既简单又强大的途径,以便利用最先进的模型。用户可根据自身任务需求选择合适的预训练模型,并通过微调(fine-tuning)的方式使其适应具体的多标签分类任务,从而大幅度降低了模型开发和训练的时间及资源消耗。在接下来的部分中,我们会详细展示这一过程的实现,包括模型的选择、数据准备、训练以及性能评估等关键步骤。
T5模型(Text-To-Text
Transfer Transformer)
本节将介绍我们在本次实验中使用的预训练模型T5,全称为Text-To-Text
Transfer
Transformer。T5模型以其创新性著称,其设计理念是将所有自然语言处理(NLP)任务转化为一个统一的文本到文本的格式。这种独特的通用性使得T5成为解决多标签分类等复杂任务的理想选择。
T5的核心理念
T5模型的设计核心在于将各种NLP任务统一到一个简单的框架中:接受文本输入并产生文本输出。这意味着无论是进行文本分类、翻译,还是处理更为复杂的多标签分类和问答任务,T5模型都以相同的方法处理,极大地提升了模型的灵活性和适用范围。
T5的架构和训练方法
T5遵循了经典的Encoder-Decoder架构,但在训练策略上进行了创新。它首先在大量文本数据上进行预训练,掌握语言的广泛知识,然后在特定任务的数据集上进行微调(fine-tuning)。这种结合预训练和微调的方法使T5在许多NLP任务上取得了卓越的表现。
T5在多标签分类任务中的运用
在多标签分类任务中,T5模型将任务视为一个文本到文本的转换问题:它将文章内容作为输入,并输出一系列的标签作为分类结果。这种方法简化了任务的处理流程,并允许T5利用其预训练阶段学到的丰富语言知识,以提升任务的处理效率和分类准确性。
实验步骤
本实验的主要步骤包括:1)数据预处理。2)模型训练与测试。3)结果转化与评估。
数据预处理
正如前一节所述,T5模型以序列生成的形式处理任务,即接收文本输入并产生文本输出。因此,我们需要将原始数据转换成符合这一格式的形式,以便模型能够有效处理。以下是我们的原始数据格式示例:
为了将这些数据转换为适合序列生成任务的格式,我们需要将标签(即标记为1的类别)转化为一串文本标签,如下所示:
示例:
注意:标签之间可以使用其他符号进行隔开,本例中使用的是分号(;)。我们的目标是将标记为1的标签拼接成一条文本数据,以便模型可以将这些标签作为生成任务的一部分来处理。
模型训练与测试
模型选择:
模型名称:T5-Small
模型链接:google-t5/t5-small ·
Hugging Face
参数设置
1 2 3 batch_size = 16 epochs = 5 learning_rate = 2e-5
结果转化与评估
在使用T5模型完成多标签分类任务后,我们会得到模型生成的文本序列作为输出。这些输出序列以文本形式列出了预测的标签,例如:
为了对模型的性能进行评估,并使用我们在上篇文章中介绍的多标签分类评估方法,必须先将这些文本格式的标签转换回原始数据的格式,即将每个标签对应到它们各自的分类列上,并用0或1表示其是否被预测为该类。转换后的格式如下所示:
在这个转换过程中,我们首先将每个预测的标签字符串分割为单独的标签(在本例中,我们使用分号";"作为分隔符)。然后,我们检查每个原始标签列,并将其与分割后的标签进行匹配,如果预测中包含某个标签,则在相应的列中标记为1;如果不包含,则标记为0。这样,我们就能得到一个与原始数据格式相匹配的矩阵,便于我们采用上篇文章中介绍的评估方法来量化模型的性能。
代码与实验
该部分强烈建议搭配Kaggle使用,见"相关链接"部分。(如果觉得有帮助,可以顺便点个赞谢谢)
数据预处理
将原始的多标签分类数据集转换为适用于T5模型的格式。具体来说,我们将文章的标题和摘要合并为一个单独的文本输入,并将标记为1的多个标签合并为一个分号分隔的标签字符串。最终,这一预处理步骤将生成一个清晰的文本到文本格式,为T5模型的训练做好准备。
DataPreprocessing.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import pandas as pd"""准备数据""" input_csv = "../../../archive/train.csv" data = pd.read_csv(input_csv) print (len (data))label_columns = data.columns[-6 :] print (label_columns)data['text' ] = data['TITLE' ] + " " + data['ABSTRACT' ] print (data['text' ].head())data['labels' ] = data[label_columns].apply(lambda x: '; ' .join(x.index[x == 1 ]), axis=1 ) print (data['labels' ])preprocessed_data = data[['text' , 'labels' ]] print (preprocessed_data.head())output_path = "../../../archive/preprocessed_data.csv" preprocessed_data.to_csv(output_path, index=False )
模型训练与预测
本次仍然使用了Pytorch以及Pytorch lightning作为实验框架,关于Pytorch
lightning的使用方法请自行查阅官网。
Pytorch lightning: Welcome to ⚡ PyTorch
Lightning — PyTorch Lightning 2.2.1 documentation
该部分对应文件名:Transformer.py
自定义批处理函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def collate_fn (batch ): """ 自定义批处理函数 """ texts = [item['text' ] for item in batch] labels = [item['labels' ] for item in batch] encoding = tokenizer(texts, padding=True , truncation=True , max_length=512 , return_tensors="pt" ) with tokenizer.as_target_tokenizer(): labels_encoding = tokenizer(labels, padding=True , truncation=True , max_length=512 , return_tensors="pt" ) labels_encoding["input_ids" ][labels_encoding["input_ids" ] == tokenizer.pad_token_id] = -100 return { 'input_ids' : encoding['input_ids' ], 'attention_mask' : encoding['attention_mask' ], 'labels' : labels_encoding['input_ids' ] }
自定义Dataset
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class T5Dataset (Dataset ): """自定义数据集""" def __init__ (self, dataset ): self.dataset = dataset def __getitem__ (self, idx ): item = self.dataset[idx] return { 'text' : item['text' ], 'labels' : item['labels' ] } def __len__ (self ): return len (self.dataset)
自定义LightningModule
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 class T5FineTuner (pl.LightningModule): """自定义LightningModule""" def __init__ (self, train_dataset, val_dataset, test_dataset, learning_rate=2e-5 ): super (T5FineTuner, self).__init__() self.validation_loss = [] self.model = T5ForConditionalGeneration.from_pretrained('t5-small' ) self.learning_rate = learning_rate self.train_dataset = train_dataset self.val_dataset = val_dataset self.test_dataset = test_dataset self.prediction = [] def forward (self, input_ids, attention_mask, labels=None ): output = self.model(input_ids=input_ids, attention_mask=attention_mask, labels=labels) return output def configure_optimizers (self ): return AdamW(self.model.parameters(), lr=self.learning_rate) def train_dataloader (self ): train_loader = DataLoader(dataset=self.train_dataset, batch_size=batch_size, collate_fn=collate_fn, shuffle=True ) return train_loader def val_dataloader (self ): val_loader = DataLoader(dataset=self.val_dataset, batch_size=batch_size, collate_fn=collate_fn, shuffle=False ) return val_loader def test_dataloader (self ): test_loader = DataLoader(dataset=self.test_dataset, batch_size=batch_size, collate_fn=collate_fn, shuffle=False ) return test_loader def training_step (self, batch, batch_idx ): input_ids = batch['input_ids' ] attention_mask = batch['attention_mask' ] labels = batch['labels' ] output = self(input_ids, attention_mask, labels) loss = output.loss self.log('train_loss' , loss, prog_bar=True , logger=True , on_step=True , on_epoch=True ) return loss def validation_step (self, batch, batch_idx ): input_ids = batch['input_ids' ] attention_mask = batch['attention_mask' ] labels = batch['labels' ] output = self(input_ids, attention_mask, labels) loss = output.loss self.log('val_loss' , loss, prog_bar=False , logger=True , on_step=True , on_epoch=True ) return loss def test_step (self, batch, batch_idx ): input_ids = batch['input_ids' ] attention_mask = batch['attention_mask' ] self.model.eval () generated_ids = self.model.generate(input_ids=input_ids, attention_mask=attention_mask) generated_texts = [tokenizer.decode(generated_id, skip_special_tokens=True , clean_up_tokenization_spaces=True ) for generated_id in generated_ids] self.prediction.extend(generated_texts)
训练与预测函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 def test (model, fast_run ): trainer = pl.Trainer(fast_dev_run=fast_run) trainer.test(model) test_result = model.prediction for text in test_result[:10 ]: print (text) return test_result def train (fast_run ): checkpoint_callback = ModelCheckpoint( monitor='val_loss' , dirpath='../../archive/log/T5FineTuner_checkpoints' , filename='Models-{epoch:02d}-{val_loss:.2f}' , save_top_k=1 , mode='min' ) log_dir = "../../archive/log" logger = TensorBoardLogger(save_dir=log_dir, name="T5FineTuner_logs" ) trainer = pl.Trainer(max_epochs=epochs, log_every_n_steps=10 , accelerator='gpu' , devices="auto" , fast_dev_run=fast_run, callbacks=[checkpoint_callback], logger=logger) model = T5FineTuner(train_dataset, valid_dataset, test_dataset, learning_rate) trainer.fit(model) return model
保存结果以及任务启动
当在Kaggle上进行操作时,请注意,直接使用load_dataset
函数从CSV文件加载数据集可能会导致错误。为了避免这个问题,推荐先使用pandas库将CSV文件读入为DataFrame,之后再将其转换为适合模型训练的格式。具体的代码实现和操作可以参考Kaggle笔记本中的相关部分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 def save_to_csv (test_dataset, predictions, filename="../../archive/test_predictions.csv" ): with open (filename, mode='w' , newline='' , encoding='utf-8' ) as file: writer = csv.writer(file) writer.writerow(['text' , 'true_labels' , 'pred_labels' ]) for item, pred_label in zip (test_dataset, predictions): text = item['text' ] true_labels = item['labels' ] writer.writerow([text, true_labels, pred_label]) if __name__ == '__main__' : data = load_dataset('csv' , data_files={'train' : '../../archive/preprocessed_data.csv' })["train" ] train_testvalid = data.train_test_split(test_size=0.3 , seed=42 ) test_valid = train_testvalid['test' ].train_test_split(test_size=0.5 , seed=42 ) train_dataset = train_testvalid['train' ] valid_dataset = test_valid['train' ] test_dataset = test_valid['test' ] print ("Training set size:" , len (train_dataset)) print ("Validation set size:" , len (valid_dataset)) print ("Test set size:" , len (test_dataset)) train_dataset = T5Dataset(train_dataset) valid_dataset = T5Dataset(valid_dataset) test_dataset = T5Dataset(test_dataset) tokenizer = T5Tokenizer.from_pretrained('t5-small' ) train_dataloader = DataLoader(dataset=train_dataset, batch_size=batch_size, collate_fn=collate_fn, shuffle=True , drop_last=True ) for i, batch in enumerate (train_dataloader): print (f"Batch {i + 1 } " ) print ("Input IDs:" , batch['input_ids' ]) print ("Input IDs shape:" , batch['input_ids' ].shape) print ("Attention Mask:" , batch['attention_mask' ]) print ("Attention Mask shape:" , batch['attention_mask' ].shape) print ("Labels:" , batch['labels' ]) print ("\n" ) if i == 0 : break fast_run = True model = train(fast_run) pre_texts = test(model, fast_run) save_to_csv(test_dataset, pre_texts)
结果转化与模型评估
Estimate.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 import pandas as pdfrom sklearn.preprocessing import MultiLabelBinarizerfrom sklearn.metrics import precision_score, recall_score, f1_score, accuracy_scoreimport numpy as npdef convert_labels (label_str ): return label_str.split(';' ) if label_str else [] def clean_label (label ): return label.strip() file_path = "../../archive/test_predictions.csv" data = pd.read_csv(file_path) true_labels = [convert_labels(label_str) for label_str in data['true_labels' ]] pred_labels = [convert_labels(label_str) for label_str in data['pred_labels' ]] true_labels_cleaned = [list (map (clean_label, label_list)) for label_list in true_labels] pred_labels_cleaned = [list (map (clean_label, label_list)) for label_list in pred_labels] mlb = MultiLabelBinarizer() mlb.fit(true_labels_cleaned + pred_labels_cleaned) y_true = mlb.transform(true_labels_cleaned) y_pred = mlb.transform(pred_labels_cleaned) print ("Transformer(T5) Accuracy =" , accuracy_score(y_true, y_pred))print ("Transformer(T5) Precision (micro-average) =" , precision_score(y_true, y_pred, average='micro' ))print ("Transformer(T5) Recall (micro-average) =" , recall_score(y_true, y_pred, average='micro' ))print ("Transformer(T5) F1 Score (micro-average) =" , f1_score(y_true, y_pred, average='micro' ))print ("\nAnother way to calculate accuracy:" )column_accuracies = np.mean(y_true == y_pred, axis=0 ) column_accuracy_with_labels = list (zip (mlb.classes_, column_accuracies)) mean_column_accuracy = np.mean(column_accuracies) for acc in column_accuracy_with_labels: print (acc) print ("Average accuracy = " , mean_column_accuracy)
实验结果
本节汇总并比较了上篇和下篇文章中各种实验的结果。我们采用了几种不同的算法来处理多标签分类问题,包括Binary
Relevance(BR)与Random Forest组合、Classifier Chains(CC)与Random
Forest组合、Label Powerset(LP)与Random
Forest组合、LP与SVM组合,以及使用了Transformer(T5)模型的序列生成方法。
通过对比准确率(Accuracy)、微观精确度(Precision_micro)、微观召回率(Recall_micro)和微观F1分数(F1_micro)这四个关键性能指标,我们发现:
使用基于Random
Forest的BR、CC和LP方法可以得到相对较好的预测性能。
当LP与SVM组合使用时,性能有所提高,特别是在召回率和F1分数方面。
最为显著的是,Transformer(T5)模型尤其在准确率、召回率和F1分数上达到了最高值。
具体数值如下所示:
BR(RandomForest)
0.4477
0.8038
0.4978
0.6149
CC(RandomForest)
0.4787
0.8012
0.5277
0.6363
LP(RandomForest)
0.5349
0.7179
0.5889
0.6470
LP(SVM)
0.5914
0.7368
0.7245
0.7306
Transformer(T5)
0.6427
0.7994
0.7840
0.7916
从结果中我们可以得出结论,Transformer模型在处理复杂的多标签分类任务时,展现出了其强大的能力。这也表明了序列到序列模型,在NLP领域的广泛应用潜力和有效性。