PyTorch-Ignite PyTorch-Ignite

使用IMDb评论进行文本分类的Transformers

在本教程中,我们将使用PyTorch-Ignite对Transformers库中的模型进行微调,用于文本分类。我们将遵循微调预训练模型教程来进行文本预处理,并定义模型、优化器和数据加载器。

然后我们将使用Ignite来:

  • 训练和评估模型
  • 计算指标
  • 设置实验和监控模型

根据教程,我们将使用 IMDb电影评论数据集来将评论分类为正面或负面。

必需的依赖项

!pip install pytorch-ignite transformers datasets

在我们深入之前,我们将使用 manual_seed来播种一切。

from ignite.utils import manual_seed

manual_seed(42)

基本设置

接下来,我们将按照教程加载我们的数据集和分词器来预处理数据。

数据预处理

from datasets import load_dataset

raw_datasets = load_dataset("imdb")
from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
def tokenize_function(examples):
    return tokenizer(examples["text"], padding="max_length", truncation=True)

tokenized_datasets = raw_datasets.map(tokenize_function, batched=True)

我们即将结束关于PyTorch特定指令的教程。在这里,我们从原始数据集中提取了一个更大的子集。由于我们在开始时已经对所有内容进行了种子设置,因此我们不需要再提供种子。

tokenized_datasets = tokenized_datasets.remove_columns(["text"])
tokenized_datasets = tokenized_datasets.rename_column("label", "labels")
tokenized_datasets.set_format("torch")

small_train_dataset = tokenized_datasets["train"].shuffle().select(range(5000))
small_eval_dataset = tokenized_datasets["test"].shuffle().select(range(5000))

数据加载器

from torch.utils.data import DataLoader

train_dataloader = DataLoader(small_train_dataset, shuffle=True, batch_size=8)
eval_dataloader = DataLoader(small_eval_dataset, batch_size=8)

模型

from transformers import AutoModelForSequenceClassification

model = AutoModelForSequenceClassification.from_pretrained("bert-base-cased", num_labels=2)

优化器

from transformers import AdamW

optimizer = AdamW(model.parameters(), lr=5e-5)

LR 调度器

我们将使用内置的Ignite替代方案linear调度器,即 PiecewiseLinear。我们还将增加epoch的数量。

from ignite.contrib.handlers import PiecewiseLinear

num_epochs = 10
num_training_steps = num_epochs * len(train_dataloader)

milestones_values = [
        (0, 5e-5),
        (num_training_steps, 0.0),
    ]
lr_scheduler = PiecewiseLinear(
        optimizer, param_name="lr", milestones_values=milestones_values
    )

设置设备

import torch

device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
model.to(device)

创建训练器

Ignite的 Engine 允许用户定义一个 process_function 来处理给定的一批数据。这个函数应用于数据集的所有批次。这是一个可以应用于训练和验证模型的通用类。一个 process_function 有两个参数 enginebatch

教程中处理一批训练数据的代码如下:

for batch in train_dataloader:
    batch = {k: v.to(device) for k, v in batch.items()}
    outputs = model(**batch)
    loss = outputs.loss
    loss.backward()

    optimizer.step()
    lr_scheduler.step()
    optimizer.zero_grad()
    progress_bar.update(1)

因此我们将定义一个process_function(下面称为train_step)来执行上述任务:

  • model 设置为训练模式。
  • batch中的项目移动到device
  • 执行前向传播并生成 output
  • 提取损失。
  • 使用损失执行反向传播,以计算模型参数的梯度。
  • 使用梯度和优化器优化模型参数。

最后,我们选择返回loss,以便我们可以利用它进行进一步处理。

你还会注意到我们没有在train_step中更新lr_schedulerprogress_bar。这是因为Ignite会自动处理这些,我们将在本教程的后面部分看到。

def train_step(engine, batch):  
    model.train()
    
    batch = {k: v.to(device) for k, v in batch.items()}
    outputs = model(**batch)
    loss = outputs.loss
    loss.backward()

    optimizer.step()
    optimizer.zero_grad()

    return loss

然后我们通过将train_step附加到训练引擎来创建一个模型trainer。稍后,我们将使用trainer来循环遍历训练数据集num_epochs次。

from ignite.engine import Engine

trainer = Engine(train_step)

我们之前定义的lr_scheduler是一个处理程序。

Handlers 可以是任何类型的函数(lambda函数、类方法等)。除此之外,Ignite提供了几个内置的处理程序以减少冗余代码。我们将这些处理程序附加到引擎上,引擎在特定的事件触发时执行。这些事件可以是任何情况,比如迭代的开始或一个周期的结束。这里是内置事件的完整列表。

因此,我们将通过add_event_handler()lr_scheduler(处理器)附加到trainerengine)上,以便它可以在Events.ITERATION_STARTED(迭代开始时)自动触发。

from ignite.engine import Events

trainer.add_event_handler(Events.ITERATION_STARTED, lr_scheduler)

这是我们没有在train_step()中包含lr_scheduler.step()的原因。

进度条

接下来我们创建了一个 Ignite 的 ProgessBar() 实例,并将其附加到训练器上,以替换 progress_bar.update(1)

from ignite.contrib.handlers import ProgressBar

pbar = ProgressBar()

我们可以简单地跟踪进度:

pbar.attach(trainer)

或者也可以跟踪 trainer(或 train_step)的输出:

pbar.attach(trainer, output_transform=lambda x: {'loss': x})

创建评估器

类似于训练process_function,我们设置了一个函数来评估单批的训练/验证/测试数据。

model.eval()
for batch in eval_dataloader:
    batch = {k: v.to(device) for k, v in batch.items()}
    with torch.no_grad():
        outputs = model(**batch)

    logits = outputs.logits
    predictions = torch.argmax(logits, dim=-1)
    metric.add_batch(predictions=predictions, references=batch["labels"])

以下是evaluate_step()的作用:

  • 将模型设置为评估模式。
  • batch中的项目移动到device
  • 使用 torch.no_grad(),后续步骤将不计算任何梯度。
  • 在模型上执行前向传播,以计算outputs来自batch
  • 获取真实的 predictions 来自 logits(正类和负类的概率)。

最后,我们返回预测值和实际标签,以便我们可以计算指标。

你会注意到我们没有在evaluate_step()中计算指标。这是因为Ignite提供了内置的metrics,我们稍后可以将其附加到引擎上。

注意: Ignite 建议将指标附加到评估器而不是训练器上,因为在训练过程中模型参数不断变化,最好在静态模型上评估模型。这一点很重要,因为训练和评估的函数有所不同。训练返回一个单一的标量损失。评估返回 y_predy,因为该输出用于计算整个数据集的每批次指标。

Ignite中的所有指标都需要y_predy作为附加到Engine的函数的输出。

def evaluate_step(engine, batch):
    model.eval()

    batch = {k: v.to(device) for k, v in batch.items()}
    with torch.no_grad():
        outputs = model(**batch)

    logits = outputs.logits
    predictions = torch.argmax(logits, dim=-1)

    return {'y_pred': predictions, 'y': batch["labels"]}

下面我们创建两个引擎,一个训练评估器和一个验证评估器。train_evaluatorvalidation_evaluator 使用相同的函数,但它们有不同的用途,我们将在本教程的后面看到。

train_evaluator = Engine(evaluate_step)
validation_evaluator = Engine(evaluate_step)

附加指标

🤗 教程定义了一个用于评估的指标,准确率:

metric= load_metric("accuracy")

我们可以轻松地将Ignite内置的Accuracy()指标附加到train_evaluatorvalidation_evaluator上。我们还需要指定指标名称(如下面的accuracy)。在内部,它将使用y_predy来计算准确率。

from ignite.metrics import Accuracy

Accuracy().attach(train_evaluator, 'accuracy')
Accuracy().attach(validation_evaluator, 'accuracy')

日志指标

现在我们将定义自定义处理程序(函数)并将它们附加到训练过程的各个Events上。

以下两个函数都实现了类似的任务。它们打印了在数据集上运行的evaluator的结果。log_training_results()在训练评估器和训练数据集上执行此操作,而log_validation_results()在验证评估器和验证数据集上执行此操作。另一个区别是这些函数在训练器引擎中的附加方式。

第一种方法涉及使用装饰器,语法简单 - @ trainer.on(Events.EPOCH_COMPLETED),意味着装饰的函数将被附加到训练器上,并在每个周期结束时调用。

第二种方法涉及使用trainer的add_event_handler方法 - trainer.add_event_handler(Events.EPOCH_COMPLETED, custom_function)。这实现了与上述相同的结果。

@trainer.on(Events.EPOCH_COMPLETED)
def log_training_results(engine):
    train_evaluator.run(train_dataloader)
    metrics = train_evaluator.state.metrics
    avg_accuracy = metrics['accuracy']
    print(f"Training Results - Epoch: {engine.state.epoch}  Avg accuracy: {avg_accuracy:.3f}")
    
def log_validation_results(engine):
    validation_evaluator.run(eval_dataloader)
    metrics = validation_evaluator.state.metrics
    avg_accuracy = metrics['accuracy']
    print(f"Validation Results - Epoch: {engine.state.epoch}  Avg accuracy: {avg_accuracy:.3f}")

trainer.add_event_handler(Events.EPOCH_COMPLETED, log_validation_results)

早停

现在我们将为训练过程设置一个EarlyStopping处理程序。EarlyStopping需要一个score_function,允许用户定义停止训练的任何标准。在这种情况下,如果验证集的损失在2个epochs(patience)内没有减少,训练过程将提前停止。

from ignite.handlers import EarlyStopping

def score_function(engine):
    val_accuracy = engine.state.metrics['accuracy']
    return val_accuracy

handler = EarlyStopping(patience=2, score_function=score_function, trainer=trainer)
validation_evaluator.add_event_handler(Events.COMPLETED, handler)

模型检查点

最后,我们希望保存最佳的模型权重。因此,我们将使用 Ignite 的 ModelCheckpoint 处理程序在每个 epoch 结束时对模型进行检查点保存。这将创建一个 models 目录,并保存 2 个最佳模型(n_saved),前缀为 bert-base-cased

from ignite.handlers import ModelCheckpoint

checkpointer = ModelCheckpoint(dirname='models', filename_prefix='bert-base-cased', n_saved=2, create_dir=True)
trainer.add_event_handler(Events.EPOCH_COMPLETED, checkpointer, {'model': model})

开始训练!

接下来,我们将运行训练器进行10个周期,并监控结果。下面我们可以看到ProgessBar打印每次迭代的损失,并按照我们在自定义函数中指定的方式打印训练和验证的结果。

trainer.run(train_dataloader, max_epochs=num_epochs)

就是这样!我们已经成功训练并评估了一个用于文本分类的Transformer。