Transformer实战(15)——使用PyTorch微调Transformer语言模型

Transformer实战(15)------使用PyTorch微调Transformer语言模型

    • [0. 前言](#0. 前言)
    • [1. 从零开始微调预训练模型](#1. 从零开始微调预训练模型)
    • [2. 单步前向传播和反向传播](#2. 单步前向传播和反向传播)
    • [3. 训练循环](#3. 训练循环)
    • 小结
    • 系列链接

0. 前言

在本节中,我们将全面剖析预训练 Transformer 模型的微调过程。相比于依赖高级 API (如 Trainer )的便捷封装,本节聚焦于使用 PyTorch 手动构建训练管道------包括模型加载、优化器配置、前向传播、反向传播、损失计算以及自定义数据集与数据加载器的实现。通过对单步前向与反向传播的演示,再到完整的 epoch 循环与验证流程,将深入理解 AdamW 优化器在 Transformer 微调中的优势,以及如何将批数据高效地送入模型进行训练和评估。

1. 从零开始微调预训练模型

接下来,我们将从零开始微调预训练模型,以了解其背后的运行机制。

(1) 首先,加载模型进行微调。本节我们选择 DistilBert,它是BERT的一个轻量、快速的版本:

python 复制代码
from transformers import DistilBertForSequenceClassification
model = DistilBertForSequenceClassification.from_pretrained('distilbert-base-uncased')

(2) 要对模型进行微调,需要将其设置为训练模式:

python 复制代码
model.train()

(3) 接下来,加载分词器:

python 复制代码
from transformers import DistilBertTokenizerFast
tokenizer = DistilBertTokenizerFast.from_pretrained('bert-base-uncased')

(4) 由于 Trainer 类为我们组织了整个过程,因此在之前的IMDb 情感分类模型训练中,我们并未涉及优化和其他训练设置。在本节中,我们需要自己实例化优化器,选择 AdamW 优化器,它是 Adam 算法的一种改进实现,解决了权重衰减问题。研究表明,AdamW 比使用 Adam 训练的模型能够产生更好的训练损失和验证损失优化表现,因此在 Transformer 训练过程中被广泛使用:

python 复制代码
from transformers import AdamW
optimizer = AdamW(model.parameters(), lr=1e-3)

2. 单步前向传播和反向传播

为了从零开始设计微调过程,我们必须理解如何实现单步前向传播和反向传播。将单个批次的数据通过 Transformer 层并获取输出,即前向传播。然后,使用输出和真实标签计算损失,并根据损失更新模型权重,即反向传播。

(1) 接收一个批次中的三个句子及其对应的标签,并执行前向传播。最后,模型会自动计算损失:

python 复制代码
import torch
texts= ["this is a good example","this is a bad example","this is a good one"]
labels= [1,0,1]
labels = torch.tensor(labels).unsqueeze(0)
encoding = tokenizer(texts, return_tensors='pt', padding=True, truncation=True, max_length=512)
input_ids = encoding['input_ids']
attention_mask = encoding['attention_mask']
outputs = model(input_ids, attention_mask=attention_mask, labels=labels)
loss = outputs.loss
loss.backward()
optimizer.step()
outputs
# SequenceClassifierOutput(loss=tensor(0.6841, grad_fn=<NllLossBackward0>), logits=tensor([[ 0.0428,  0.1283],
#        [ 0.0652,  0.2260],
#        [-0.0192,  0.1239]], grad_fn=<AddmmBackward0>), hidden_states=None, attentions=None)

模型接受由分词器生成的 input_idsattention_mask 作为输入,并使用真实标签计算损失。输出包含损失和 logitsloss.backward() 会通过使用输入和标签对模型进行评估来计算张量的梯度。optimizer.step() 执行一次优化步骤,并使用已计算的梯度更新权重,这就是反向传播。将这些代码放入一个循环中时,还会添加 optimizer.zero_grad() 清除所有参数的梯度。在循环开始时调用这一操作非常重要,否则可能会积累多个步骤的梯度。输出的第二个张量是 logits,在深度学习中,logits(logistic units) 是神经网络结构的最后一层,由实数预测值组成。在分类问题中,logits 需要通过 softmax 函数转换为概率;而对于回归任务,则只需进行简单的归一化处理。

(2) 模型只会输出 logits,而不会计算损失。以下示例展示了如何手动计算交叉熵损失:

python 复制代码
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
# tensor(0.5494, grad_fn=<NllLossBackward0>)

3. 训练循环

通过以上内容,我们了解了如何将批数据通过网络进行单步前向传播。接下来,设计循环以批次形式遍历整个数据集,进行多个 epoch 的训练。

(1) 首先实现 Dataset,它是 torch.Dataset 的子类,继承了成员变量和函数,并实现了 __init__()__getitem__() 这两个抽象函数:

python 复制代码
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)

(2) 接下来,使用 sst2 (Stanford Sentiment Treebank v2) 数据集,来微调模型进行情感分析。我们还将加载与 sst2 对应的评估指标:

python 复制代码
import datasets
from datasets import load_dataset
sst2= load_dataset("glue","sst2")
from evaluate import load
metric = load("glue", "sst2")

(3) 提取句子和对应的标签:

python 复制代码
texts=sst2['train']['sentence']
labels=sst2['train']['label']
val_texts=sst2['validation']['sentence']
val_labels=sst2['validation']['label']

(4) 将数据集通过分词器处理,并实例化 MyDataset 对象,使 DistiBert 模型能够处理这些数据:

python 复制代码
train_dataset= MyDataset(tokenizer(texts, truncation=True, padding=True), labels)
val_dataset=  MyDataset(tokenizer(val_texts, truncation=True, padding=True), val_labels)

(5) 接下来,实例化一个 DataLoader 类,它提供了按加载顺序遍历数据样本的接口,也有助于批处理和内存固定:

python 复制代码
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)

(6) 检测设备并定义 AdamW 优化器:

python 复制代码
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-5)

(7) 我们已经了解了如何实现前向传播,在这个过程中,批数据通过神经网络通过神经网络的每一层,从第一个层到最后一个层,依次处理,并经过激活函数后传递到下一个层。为了在多个 epoch 中遍历整个数据集,需要嵌套循环:外循环用于 epoch,而内循环则用于处理每个批次。每个 epoch 由两个模块组成;一个用于训练,另一个用于评估。在训练循环中调用了 model.train(),而在评估循环中调用了 model.eval()。通过相应的指标对象来跟踪模型的表现:

python 复制代码
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}")

微调后的模型,达到了大约 90.94% 的准确率:

python 复制代码
epoch 0: {'accuracy': 0.9048165137614679}
epoch 1: {'accuracy': 0.8944954128440367}
epoch 2: {'accuracy': 0.9094036697247706}

小结

本节通过 DistilBert 的微调,介绍了使用 PyToch 从加载预训练模型、手动构建优化器,到实现前向与反向传播、管理梯度,展示了使用 PyTorch 微调 Transformer 语言模型的完整流程。我们演示了如何定义 DatasetDataLoader,并利用 SST-2 数据集完成多轮训练与评估,最终取得约 90.94% 的验证准确率。

系列链接

Transformer实战(1)------词嵌入技术详解
Transformer实战(2)------循环神经网络详解
Transformer实战(3)------从词袋模型到Transformer:NLP技术演进
Transformer实战(4)------从零开始构建Transformer
Transformer实战(5)------Hugging Face环境配置与应用详解
Transformer实战(6)------Transformer模型性能评估
Transformer实战(7)------datasets库核心功能解析
Transformer实战(8)------BERT模型详解与实现
Transformer实战(9)------Transformer分词算法详解
Transformer实战(10)------生成式语言模型 (Generative Language Model, GLM)
Transformer实战(11)------从零开始构建GPT模型
Transformer实战(12)------基于Transformer的文本到文本模型
Transformer实战(13)------从零开始训练GPT-2语言模型
Transformer实战(14)------微调Transformer语言模型用于文本分类