精通 Transformers(二)(1)https://developer.aliyun.com/article/1510690
第五章:文本分类的语言模型微调
在本章中,我们将学习如何配置预训练模型进行文本分类,并如何对其进行微调以适应任何文本分类的下游任务,例如情感分析或多类分类。我们还将讨论如何处理句对和回归问题,涵盖一个实现。我们将使用 GLUE 等知名数据集,以及我们自己的定制数据集。然后,我们将利用 Trainer 类,该类处理了训练和微调过程的复杂性。
首先,我们将学习如何使用 Trainer 类进行单句二元情感分类微调。然后,我们将使用原生 PyTorch 进行情感分类的训练,而不使用 Trainer 类。在多类分类中,将考虑超过两个类别。我们将有七个类别分类微调任务要执行。最后,我们将训练一个文本回归模型,以预测具有句子对的数值。
本章将涵盖以下主题:
- 文本分类简介
- 对单句二元分类微调 BERT 模型
- 使用原生 PyTorch 训练分类模型
- 使用自定义数据集对 BERT 进行多类分类微调
- 对句对回归进行 BERT 的微调
- 利用
run_glue.py
对模型进行微调
技术要求
我们将使用 Jupyter Notebook 运行我们的编程练习。您需要 Python 3.6+。确保已安装以下软件包:
sklearn
- Transformers 4.0+
datasets
所有本章编程练习的笔记本将在以下 GitHub 链接上提供:github.com/PacktPublishing/Mastering-Transformers/tree/main/CH05
。
查看以下链接以观看代码演示视频:
文本分类简介
文本分类(也称为文本分类)是将文档(句子、Twitter 帖子、书籍章节、电子邮件内容等)映射到预定义列表(类别)中的一种方式。在具有正负标签的两类情况下,我们称之为二元分类 - 更具体地说,是情感分析。对于多于两个类别的情况,我们称之为多类分类,其中类别是相互排斥的,或者称之为多标签分类,其中类别不是相互排斥的,这意味着一个文档可以获得多个标签。例如,一篇新闻文章的内容可能同时涉及体育和政治。除了这种分类之外,我们可能希望对文档进行范围为[-1,1]的评分或在[1-5]范围内对其进行排名。我们可以用回归模型解决这种问题,其中输出的类型是数值而不是分类。
幸运的是,变换器架构使我们能够高效地解决这些问题。对于句对任务,如文档相似性或文本蕴涵,输入不是单一句子,而是两个句子,如下图所示。我们可以评分两个句子在语义上相似的程度,或者预测它们是否在语义上相似。另一个句对任务是文本蕴涵,其中问题定义为多类分类。在 GLUE 基准测试中,两个序列被消耗:蕴含/矛盾/中性:
图 5.1 – 文本分类方案
让我们通过微调预训练的 BERT 模型开始我们的训练过程,针对一个常见问题:情感分析。
为单句二元分类微调 BERT 模型
在本节中,我们将讨论如何使用流行的IMDb 情感
数据集,通过微调预训练的 BERT 模型进行情感分析。使用 GPU 可以加快我们的学习过程,但如果您没有这样的资源,您也可以通过 CPU 进行微调。让我们开始吧:
- 要了解并保存当前设备的信息,我们可以执行以下代码行:
from torch import cuda device = 'cuda' if cuda.is_available() else 'cpu'
- 我们将在这里使用
DistilBertForSequenceClassification
类,它是从DistilBert
类继承而来,顶部有一个特殊的序列分类头。我们可以利用这个分类头来训练分类模型,其中默认类别数为2
:
from transformers import DistilBertTokenizerFast, DistilBertForSequenceClassification model_path= 'distilbert-base-uncased' tokenizer = DistilBertTokenizerFast.from_pre-trained(model_path) model = \ DistilBertForSequenceClassification.from_pre-trained(model_path, id2label={0:"NEG", 1:"POS"}, label2id={"NEG":0, "POS":1})
- 注意传递给模型的两个参数称为
id2label
和label2id
,用于推理。或者,我们可以实例化一个特定的config
对象并将其传递给模型,如下所示:
config = AutoConfig.from_pre-trained(....) SequenceClassification.from_pre-trained(.... config=config)
- 现在,让我们选择一个名为
IMDB Dataset
的流行情感分类数据集。原始数据集包含两组数据:25,000 个训练示例和 25 个测试示例。我们将数据集分成测试集和验证集。请注意,数据集的前一半示例为正面,而后一半的示例都为负面。我们可以按以下方式分布示例:
from datasets import load_dataset imdb_train= load_dataset('imdb', split="train") imdb_test= load_dataset('imdb', split="test[:6250]+test[-6250:]") imdb_val= \ load_dataset('imdb', split="test[6250:12500]+test[-12500:-6250]")
- 让我们检查数据集的形状:
>>> imdb_train.shape, imdb_test.shape, imdb_val.shape ((25000, 2), (12500, 2), (12500, 2))
- 您可以根据计算资源的情况从数据集中取出一小部分。对于较小的部分,您应该运行以下代码,选择 4,000 个示例进行训练,1,000 个进行测试,以及 1,000 个进行验证,如下所示:
imdb_train= load_dataset('imdb', split="train[:2000]+train[-2000:]") imdb_test= load_dataset('imdb', split="test[:500]+test[-500:]") imdb_val= load_dataset('imdb', split="test[500:1000]+test[-1000:-500]")
- 现在,我们可以将这些数据集通过
tokenizer
模型,使它们准备好进行训练:
enc_train = imdb_train.map(lambda e: tokenizer( e['text'], padding=True, truncation=True), batched=True, batch_size=1000) enc_test = imdb_test.map(lambda e: tokenizer( e['text'], padding=True, truncation=True), batched=True, batch_size=1000) enc_val = imdb_val.map(lambda e: tokenizer( e['text'], padding=True, truncation=True), batched=True, batch_size=1000)
- 让我们看看训练集的样子。注意力掩码和输入 ID 是由分词器添加到数据集中的,以便 BERT 模型进行处理:
import pandas as pd pd.DataFrame(enc_train)
- 输出如下:
图 5.2 – 编码后的训练数据集
此时,数据集已准备好用于训练和测试。Trainer
类(TFTrainer
用于 TensorFlow)和TrainingArguments
类(TFTrainingArguments
用于 TensorFlow)将帮助我们处理训练的许多复杂性。我们将在TrainingArguments
类中定义我们的参数集,然后将其传递给Trainer
对象。
让我们定义每个训练参数的作用:
表 1 - 不同训练参数定义表 - 若要获取更多信息,请查看
TrainingArguments
的 API 文档,或在 Python notebook 中执行以下代码:
TrainingArguments?
- 虽然像 LSTM 这样的深度学习架构需要许多 epoch,有时超过 50 个,但对于基于 transformer 的微调,由于迁移学习,我们通常会满足于 3 个 epoch 的数量。大部分时间,这个数量已经足够进行微调,因为预训练模型在预训练阶段已经学到了很多关于语言的知识,通常需要大约 50 个 epoch。要确定正确的 epoch 数量,我们需要监控训练和评估损失。我们将学习如何在第十一章中跟踪训练,注意力可视化和实验追踪。
- 对于许多下游任务问题,这将足够用。在训练过程中,我们的模型检查点将被保存在
./MyIMDBModel
文件夹中,每 200 步保存一次:
from transformers import TrainingArguments, Trainer training_args = TrainingArguments( output_dir='./MyIMDBModel', do_train=True, do_eval=True, num_train_epochs=3, per_device_train_batch_size=32, per_device_eval_batch_size=64, warmup_steps=100, weight_decay=0.01, logging_strategy='steps', logging_dir='./logs', logging_steps=200, evaluation_strategy= 'steps', fp16= cuda.is_available(), load_best_model_at_end=True )
- 在实例化
Trainer
对象之前,我们将定义compute_metrics()
方法,它可以帮助我们监控训练过程中特定指标的进展,如 Precision、RMSE、Pearson 相关性、BLEU 等。文本分类问题(如情感分类或多类分类)大多使用微平均或宏平均 F1 进行评估。而宏平均方法平等对待每个类别,微平均对每个文本或每个标记的分类决策平等对待。微平均等于模型正确决策的次数与总决策次数的比率。而宏平均方法计算每个类别的 Precision、Recall 和 F1 的平均分数。对于我们的分类问题,宏平均更方便进行评估,因为我们希望给每个标签平等的权重,如下所示:
from sklearn.metrics import accuracy_score, Precision_Recall_fscore_support def compute_metrics(pred): labels = pred.label_ids preds = pred.predictions.argmax(-1) Precision, Recall, f1, _ = \ Precision_Recall_fscore_support(labels, preds, average='macro') acc = accuracy_score(labels, preds) return { 'Accuracy': acc, 'F1': f1, 'Precision': Precision, 'Recall': Recall }
- 我们几乎已经准备好开始训练过程。现在,让我们实例化
Trainer
对象并启动它。Trainer
类是一个非常强大和优化的工具,用于组织 PyTorch 和 TensorFlow(TFTrainer
用于 TensorFlow)的复杂训练和评估过程,这得益于transformers
库:
trainer = Trainer( model=model, args=training_args, train_dataset=enc_train, eval_dataset=enc_val, compute_metrics= compute_metrics )
- 最后,我们可以开始训练过程:
results=trainer.train()
- 前面的调用开始记录指标,我们将在第十一章,注意力可视化和实验跟踪中更详细地讨论这些内容。整个 IMDb 数据集包括 25,000 个训练示例。使用批量大小为 32,我们有 25K/32 约等于 782 个步骤,并且对于 3 个时期还有 2,346 个步骤(782 x 3),如下所示的进度条显示:
图 5.3 – Trainer 对象生成的输出 Trainer
对象保留了验证损失最小的检查点。它选择了步骤 1,400 处的检查点,因为该步骤的验证损失最小。让我们在三个(训练/测试/验证)数据集上评估最佳检查点:
>>> q=[trainer.evaluate(eval_dataset=data) for data in [enc_train, enc_val, enc_test]] >>> pd.DataFrame(q, index=["train","val","test"]).iloc[:,:5]
- 输出如下:
图 5.4 – 分类模型在训练/验证/测试数据集上的性能 - 干得好!我们成功完成了训练/测试阶段,并获得了 92.6 的准确度和 92.6 的宏平均 F1 值。为了更详细地监视您的训练过程,您可以调用高级工具,如 TensorBoard。这些工具会解析日志,并使我们能够跟踪各种指标以进行全面分析。我们已经在
./logs
文件夹下记录了性能和其他指标。只需在我们的 Python 笔记本中运行tensorboard
函数就足够了,如下面的代码块所示(我们将在第十一章中详细讨论 TensorBoard 和其他监控工具的可视化和实验跟踪):
%reload_ext tensorboard %tensorboard --logdir logs
- 现在,我们将使用模型进行推理以检查其是否正常工作。让我们定义一个预测函数来简化预测步骤,如下所示:
def get_prediction(text): inputs = tokenizer(text, padding=True,truncation=True, max_length=250, return_tensors="pt").to(device) outputs = \ model(inputs["input_ids"].to(device),inputs["attention_mask"].to(device)) probs = outputs[0].softmax(1) return probs, probs.argmax()
- 现在,运行模型进行推理:
>>> text = "I didn't like the movie it bored me " >>> get_prediction(text)[1].item() 0
- 我们在这里得到的是
0
,表示的是负面。我们已经定义了哪个 ID 表示哪个标签。我们可以使用这种映射方案来获取标签。或者,我们可以将所有这些乏味的步骤简单地传递给一个专用的 API,即 Pipeline,这是我们已经熟悉的。在实例化之前,让我们保存最佳模型以进行进一步的推理:
model_save_path = "MyBestIMDBModel" trainer.save_model(model_save_path) tokenizer.save_pre-trained(model_save_path)
- Pipeline API 是使用预训练模型进行推理的简便方法。我们从保存模型的位置加载模型并将其传递给 Pipeline API,其余工作由其完成。我们可以跳过保存步骤,而是直接将
model
和tokenizer
对象在内存中传递给 Pipeline API。如果这样做,将获得相同的结果。 - 如下面的代码所示,当我们执行二元分类时,需要将 Pipeline 的任务名称参数指定为
sentiment-analysis
:
>>> from transformers import pipeline, \ DistilBertForSequenceClassification, DistilBertTokenizerFast >>> model = \ DistilBertForSequenceClassification.from_pre-trained("MyBestIMDBModel") >>> tokenizer= \ DistilBertTokenizerFast.from_pre-trained("MyBestIMDBModel") >>> nlp= pipeline("sentiment-analysis", model=model, tokenizer=tokenizer) >>> nlp("the movie was very impressive") Out: [{'label': 'POS', 'score': 0.9621992707252502}] >>> nlp("the text of the picture was very poor") Out: [{'label': 'NEG', 'score': 0.9938313961029053}]
- Pipeline 知道如何处理输入,并某种方式学会了哪个 ID 表示哪个(
POS
或NEG
)标签。它还产生类别概率。
干得好!我们已经使用Trainer
类为 IMDb 数据集微调了情感预测模型。在接下来的部分中,我们将使用原生 PyTorch 进行相同的二元分类培训。我们还将使用其他数据集。
使用原生 PyTorch 训练分类模型
Trainer
类非常强大,我们要感谢 HuggingFace 团队提供了这样一个有用的工具。然而,在本节中,我们将从头开始微调预训练模型,以了解其内部运行原理。让我们开始吧:
- 首先,让我们加载用于微调的模型。我们将在这里选择
DistilBERT
,因为它是 BERT 的一个小型、快速和廉价版本:
from transformers import DistilBertForSequenceClassification model = DistilBertForSequenceClassification.from_pre-trained('distilbert-base-uncased')
- 要对任何模型进行微调,我们需要将其设置为训练模式,如下所示:
model.train()
- 现在,我们必须加载分词器:
from transformers import DistilBertTokenizerFast tokenizer = DistilBertTokenizerFast.from_pre-trained('bert-base-uncased')
- 由于
Trainer
类已经为我们组织好了整个过程,我们在之前的 IMDb 情感分类练习中没有处理优化和其他训练设置。现在,我们需要自己实例化优化器。在这里,我们必须选择AdamW
,它是 Adam 算法的一个实现,但修复了权重衰减。最近的研究表明,AdamW
产生的训练损失和验证损失比使用 Adam 训练的模型更好。因此,在许多 transformer 训练过程中,它是一个广泛使用的优化器:
from transformers import AdamW optimizer = AdamW(model.parameters(), lr=1e-3)
- 要从头开始设计微调过程,我们必须了解如何实现单步前向传播和反向传播。我们可以通过 transformer 层传递一个批次并获得输出,该输出由分词器生成的
input_ids
和attention_mask
组成,并使用真实标签计算损失。正如我们所看到的,输出包含loss
和logits
两部分。现在,loss.backward()
通过使用输入和标签评估模型来计算张量的梯度。optimizer.step()
执行单个优化步骤并使用计算的梯度更新权重,这称为反向传播。当我们很快将所有这些行放入一个循环中时,我们还将添加optimizer.zero_grad()
,它清除所有参数的梯度。在循环开始时调用这一点非常重要;否则,我们可能会积累多个步骤的梯度。输出的第二个张量是logits。在深度学习的上下文中,logits(logistic units 的缩写)是神经架构的最后一层,由实数作为预测值组成。在分类的情况下,logits 需要通过 softmax 函数转换为概率。否则,它们只是用于回归的标准化值。 - 如果我们想要手动计算损失,我们就不能将标签传递给模型。由于这个原因,模型只产生 logits,而不计算损失。在下面的示例中,我们正在手动计算交叉熵损失:
from torch.nn import functional labels = torch.tensor([1,0,1]) outputs = model(input_ids, attention_mask=attention_mask) loss = functional.cross_entropy(outputs.logits, labels) loss.backward() optimizer.step() loss Output: tensor(0.6101, grad_fn=<NllLossBackward>)
- 有了这个,我们学会了如何将批量输入通过网络的前向方向在单个步骤中进行传递。现在,是时候设计一个循环,以批量迭代整个数据集来训练模型进行多个 epochs。为此,我们将首先设计
Dataset
类。它是torch.Dataset
的子类,继承成员变量和函数,并实现__init__()
和__getitem()__
抽象函数:
from torch.utils.data import Dataset class MyDataset(Dataset): def __init__(self, encodings, labels): self.encodings = encodings self.labels = labels def __getitem__(self, idx): item = {key: torch.tensor(val[idx]) for key, val in self.encodings.items()} item['labels'] = torch.tensor(self.labels[idx]) return item def __len__(self): return len(self.labels)
- 通过拿取另一个情感分析数据集 SST-2 数据集,即斯坦福情感树库 v2(SST2)来对情感分析的模型进行微调。我们还将加载 SST-2 的相应度量进行评估,如下所示:
import datasets from datasets import load_dataset sst2= load_dataset("glue","sst2") from datasets import load_metric metric = load_metric("glue", "sst2")
- 我们将相应地提取句子和标签:
texts=sst2['train']['sentence'] labels=sst2['train']['label'] val_texts=sst2['validation']['sentence'] val_labels=sst2['validation']['label']
- 现在,我们可以通过标记器传递数据集并实例化
MyDataset
对象,使 BERT 模型可以与它们一起工作:
train_dataset= MyDataset(tokenizer(texts, truncation=True, padding=True), labels) val_dataset= MyDataset(tokenizer(val_texts, truncation=True, padding=True), val_labels)
- 让我们实例化一个
Dataloader
类,它提供了通过加载顺序迭代数据样本的接口。这也有助于批处理和内存固定:
from torch.utils.data import DataLoader train_loader = DataLoader(train_dataset, batch_size=16, shuffle=True) val_loader = DataLoader(val_dataset, batch_size=16, shuffle=True)
- 以下行检测设备并适当地定义
AdamW
优化器:
from transformers import AdamW device = \ torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu') model.to(device) optimizer = AdamW(model.parameters(), lr=1e-3)
- 到目前为止,我们知道如何实现前向传播,这是我们处理一批示例的地方。在这里,批量数据通过神经网络的前向方向进行传递。在单个步骤中,每层从第一层到最后一层都由批量数据处理,根据激活函数,传递到下一层。为了在多个 epochs 中遍历整个数据集,我们设计了两个嵌套循环:外部循环是为了 epochs,而内部循环是为了每批次的步骤。内部部分由两个块组成;一个用于训练,另一个用于评估每个 epochs。您可能已经注意到,我们在第一个训练循环中调用了
model.train()
,当移动第二个评估块时,我们调用了model.eval()
。这很重要,因为我们使模型处于训练和推理模式。 - 我们已经讨论了内部块。注意,我们通过相应的
metric
对象跟踪模型的性能:
for epoch in range(3): model.train() for batch in train_loader: optimizer.zero_grad() input_ids = batch['input_ids'].to(device) attention_mask = batch['attention_mask'].to(device) labels = batch['labels'].to(device) outputs = \ model(input_ids, attention_mask=attention_mask, labels=labels) loss = outputs[0] loss.backward() optimizer.step() model.eval() for batch in val_loader: input_ids = batch['input_ids'].to(device) attention_mask = batch['attention_mask'].to(device) labels = batch['labels'].to(device) outputs = \ model(input_ids, attention_mask=attention_mask, labels=labels) predictions=outputs.logits.argmax(dim=-1) metric.add_batch( predictions=predictions, references=batch["labels"], ) eval_metric = metric.compute() print(f"epoch {epoch}: {eval_metric}") OUTPUT: epoch 0: {'accuracy': 0.9048165137614679} epoch 1: {'accuracy': 0.8944954128440367} epoch 2: {'accuracy': 0.9094036697247706}
- 做得好!我们已经对模型进行了微调,并获得了大约 90.94 的准确度。剩下的流程,如保存、加载和推理,将类似于我们在
Trainer
类中所做的。
有了这个,我们已经完成了二元分类。在下一节中,我们将学习如何为非英语语言实现多类分类模型。
精通 Transformers(二)(3)https://developer.aliyun.com/article/1510692