我们将使用Hugging Face提供的transformer大模型实现一个完整的训练过程
一、数据
1、导包
from datasets import load_dataset
from transformers import AutoTokenizer, DataCollatorWithPadding
from torch.utils.data import DataLoader
from transformers import AutoModelForSequenceClassification, AdamW, get_scheduler
import torch
from tqdm.auto import tqdm # 进度条
import evaluate
(1)AutoTokenizer
功能:AutoTokenizer是一个自动化工具,能够根据给定的模型名称或路径,自动选择并加载适当的分词器(tokenizer)。这意味着你可以轻松使用 Hugging Face 预训练模型而无需手动指定具体的分词器类型。
主要用途:AutoTokenizer可以对文本进行分词、编码(将单词转换为对应的ID)、解码(将ID转换为文本)、填充(padding)和截断(truncation)等操作。它会根据模型的要求自动处理这些步骤。
(2)DataCollatorWithPadding
功能:DataCollatorWithPadding 是一个用于将不同长度的输入序列整理成批次的工具。它会在批处理时自动填充短于最大长度的序列,以确保所有序列在输入到模型时具有相同的长度。
主要用途:在自然语言处理任务中,文本的长度通常是不一致的。这个数据整理器可以将输入序列填充到相同的长度,从而使它们可以被批量处理,提高模型训练的效率。
(3)AutoModelForSequenceClassification、AdamW、get_scheduler
AutoModelForSequenceClassification:用于加载适合文本分类任务的预训练模型。它根据输入的模型名称或路径,自动选择和加载适合的模型架构
AdamW:用于模型训练的优化器,具备权重衰减的功能,常用于 NLP 任务。
get_scheduler:用于创建学习率调度器,动态调整优化器的学习率以优化模型训练过程。通常是在训练过程中逐步减小学习率以提高模型的收敛性
2、准备数据
raw_datasets = load_dataset("glue", "mrpc") # MRPC是一个用于评估文本重述paraphrase检测能力的数据集
checkpoint = "bert-base-uncased" # bert-base-uncased是BERT模型的基础版本,具有12层Transformer编码器,110M参数。
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
checkpoint使用BERT模型的基础版本,然后将其传入tokenizer标记器,从而把输入的句子转换成model能够理解的输入格式;MRPC(Microsoft Research Paraphrase Corpus)是用于判断句子对是否同义的任务。
3、数据预处理
(1) 定义分词函数
def tokenize_function(example):
return tokenizer(example["sentence1"], example["sentence2"], truncation=True)
tokenize_function接受的参数example通常是一个字典,truncation=True表示如果输入的句子长度超过模型允许的最大长度,分词器将截断句子。
下面是调用该函数的一个示例:
example = {
"sentence1": "I love you more than you know.",
"sentence2": "I think there are no limits but the sky."
}
调用tokenize_function函数会得到如下输出:
{'input_ids': [101, 1045, 2293, 2017, 2062, 2084, 2017, 2113, 1012, 102, 1045, 2228, 2045, 2024, 2053, 6537, 2021, 1996, 3712, 1012, 102],
'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}
inputs_ids是句子的ID表示,attention_mask指示哪些 token 是实际输入(1)哪些是填充(0)
(2)数据处理
tokenized_datasets = raw_datasets.map(tokenize_function, batched=True)
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)
tokenized_datasets = tokenized_datasets.remove_columns(["sentence1", "sentence2", "idx"])
tokenized_datasets = tokenized_datasets.rename_column("label", "labels")
tokenized_datasets.set_format("torch")
·tokenized_datasets = raw_datasets.map(tokenize_function, batched=True)
把原始数据进行分词处理,map方法用于对数据集的每一条记录应用给定的函数,batched=True让分词函数一次接收多个输入示例(而不是逐个示例),这样可以提高处理效率。
·data_collator = DataCollatorWithPadding(tokenizer=tokenizer)
主要功能是处理不同长度的输入序列,将不同长度的序列填充到相同的长度
·tokenized_datasets = tokenized_datasets.remove_columns(["sentence1", "sentence2", "idx"])
在训练过程中,我们输入的参数通常不需要“sentence1”“sentence2”和“idx”,只有labels是必要的,移除这三个参数可以简化数据集,减少内存占用
我们在上述操作进行完成后,可以输出一下结果进行验证:
print(tokenized_datasets["train"].column_names)
结果为:
['input_ids', 'attention_mask', 'labels']
二、模型
1、定义
model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)
outputs = model(**batch)
(1)加载BERT模型,num_labels=2表示这是一个二分类任务
(2)**运算符可以解包字典,由于每个batch都是字典类型,包含inputs_ids,attention_mask和labels,所以**batch相当于进行了如下操作:
outputs = model(input_ids=batch['input_ids'], attention_mask=batch['attention_mask'], labels=batch['labels'])
我们可以对output的loss和logits形状进行输出
print(outputs.loss, outputs.logits.shape)
得到如下结果:
tensor(0.6931, grad_fn=<NllLossBackward0>)
torch.Size([8, 2])
(3)下面我们做一系列准备,包括优化器、学习率调度器的设置等
optimizer = AdamW(model.parameters(), lr=5e-5)
num_epochs = 3 # 总训练周期数,意味着整个数据集被遍历3次
num_training_steps = num_epochs * len(train_dataloader)
# num_training_steps 的计算公式是总周期数乘以每个周期的批次数,这将给出模型训练过程中的总步骤数
lr_scheduler = get_scheduler(
"linear", # "linear" 表示线性衰减学习率,随着训练的进行,学习率将线性减小。
optimizer=optimizer,
num_warmup_steps=0,
num_training_steps=num_training_steps,
)
同样,不妨看看总共需要多少steps
print(num_training_steps)
2、迁移到GPU
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
model.to(device)
print(device)
3、训练
在训练开始之前,我们设置一个进度条来直观感受训练速度:
progress_bar = tqdm(range(num_training_steps))
然后就是非常经典的训练过程,唯一要注意的小点是最后一句更新进度条:
model.train()
for epoch in range(num_epochs):
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)
4、评估
加载 GLUE 数据集中的 MRPC 评估指标:
metric = evaluate.load("glue", "mrpc")
设置成评估模式,进行评估:
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"])
(1)logits是用于分类的最终输出值,它们是未经过激活函数(如softmax)的原始预测分数,代表了模型对于每个类别的信心分数。这些分数可以是任意的实数值,正值、负值都可以,模型使用这些值来进行决策。
(2) predictions = torch.argmax(logits, dim=-1)
torch.argmax的功能是,计算每个样本在所有类别中的最大值索引。对于一个函数f(x),argmax(f(x))是求当f(x)取到最大值时x的值
对于多类别分类任务, 通常logits是一个形状为(batch_size,num_classes)的张量,其中batch_size是批次大小,num_classes是类别数。dim=-1表示沿着最后一个维度(即类别维度)进行操作
(3) metric.add_batch(predictions=predictions, references=batch["labels"])
predictions是刚刚计算出的模型预测类别,references=batch["labels"]是当前批次的真实标签。
最后,打印结果就好了
print(metric.compute())
三、完整代码
from datasets import load_dataset
from transformers import AutoTokenizer, DataCollatorWithPadding
from torch.utils.data import DataLoader
from transformers import AutoModelForSequenceClassification, AdamW, get_scheduler
import torch
from tqdm.auto import tqdm
import evaluate
raw_datasets = load_dataset("glue", "mrpc") # MRPC是一个用于评估文本重述paraphrase检测能力的数据集
checkpoint = "bert-base-uncased" # bert-base-uncased是BERT模型的基础版本,具有12层Transformer编码器,110M参数。
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
def tokenize_function(example):
return tokenizer(example["sentence1"], example["sentence2"], truncation=True)
tokenized_datasets = raw_datasets.map(tokenize_function, batched=True)
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)
tokenized_datasets = tokenized_datasets.remove_columns(["sentence1", "sentence2", "idx"])
tokenized_datasets = tokenized_datasets.rename_column("label", "labels")
tokenized_datasets.set_format("torch")
print(tokenized_datasets["train"].column_names)
train_dataloader = DataLoader(
tokenized_datasets["train"], shuffle=True, batch_size=8, collate_fn=data_collator
)
eval_dataloader = DataLoader(
tokenized_datasets["validation"], batch_size=8, collate_fn=data_collator
)
for batch in train_dataloader:
print({k: v.shape for k, v in batch.items()})
break
model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)
outputs = model(**batch)
print(outputs.loss, outputs.logits.shape)
optimizer = AdamW(model.parameters(), lr=5e-5)
num_epochs = 3 # 总训练周期数,意味着整个数据集被遍历3次
num_training_steps = num_epochs * len(train_dataloader)
# num_training_steps 的计算公式是总周期数乘以每个周期的批次数,这将给出模型训练过程中的总步骤数
lr_scheduler = get_scheduler(
"linear", # "linear" 表示线性衰减学习率,随着训练的进行,学习率将线性减小。
optimizer=optimizer,
num_warmup_steps=0,
num_training_steps=num_training_steps,
)
print(num_training_steps)
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
model.to(device)
print(device)
progress_bar = tqdm(range(num_training_steps))
model.train() # 设置模型为训练模式
for epoch in range(num_epochs):
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)
metric = evaluate.load("glue", "mrpc")
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"])
print(metric.compute())
accuracy如下:
{'accuracy': 0.8455882352941176, 'f1': 0.8923076923076922}
四、改进与优化
由于笔者使用的是macOS系统,模型在CPU上的训练速度会相当慢。macOS 12及以上的系统已经可以支持在MPS设备上使用GPU加速,所以对代码作出如下修改(注释#++++++++++为需要添加的行):
from datasets import load_dataset
from transformers import AutoTokenizer, DataCollatorWithPadding
from torch.utils.data import DataLoader
from transformers import AutoModelForSequenceClassification, AdamW, get_scheduler
import torch
from tqdm.auto import tqdm
import evaluate
from accelerate import Accelerator # ++++++++++
raw_datasets = load_dataset("glue", "mrpc") # MRPC是一个用于评估文本重述paraphrase检测能力的数据集
checkpoint = "bert-base-uncased" # BERT模型的基础版本,具有12层Transformer编码器,110M参数
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
def tokenize_function(example):
return tokenizer(example["sentence1"], example["sentence2"], truncation=True)
tokenized_datasets = raw_datasets.map(tokenize_function, batched=True)
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)
tokenized_datasets = tokenized_datasets.remove_columns(["sentence1", "sentence2", "idx"])
tokenized_datasets = tokenized_datasets.rename_column("label", "labels")
tokenized_datasets.set_format("torch")
print(tokenized_datasets["train"].column_names)
train_dataloader = DataLoader(
tokenized_datasets["train"], shuffle=True, batch_size=8, collate_fn=data_collator
)
eval_dataloader = DataLoader(
tokenized_datasets["validation"], batch_size=8, collate_fn=data_collator
)
accelerator = Accelerator() # ++++++++++
model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)
optimizer = AdamW(model.parameters(), lr=5e-5)
train_dataloader, eval_dataloader, model, optimizer = accelerator.prepare(
train_dataloader, eval_dataloader, model, optimizer
) # ++++++++++
num_epochs = 3 # 总训练周期数
num_training_steps = num_epochs * len(train_dataloader)
lr_scheduler = get_scheduler(
"linear",
optimizer=optimizer,
num_warmup_steps=0,
num_training_steps=num_training_steps,
)
print(num_training_steps)
# 使用 MPS 设备(如果可用)
device = torch.device("mps") if torch.backends.mps.is_available() else torch.device("cpu") # ++++++++++
model.to(device)
print(device)
progress_bar = tqdm(range(num_training_steps))
model.train()
for epoch in range(num_epochs):
for batch in train_dataloader:
batch = {k: v.to(device) for k, v in batch.items()}
outputs = model(**batch)
loss = outputs.loss
accelerator.backward(loss) # ++++++++++
optimizer.step()
lr_scheduler.step()
optimizer.zero_grad()
progress_bar.update(1)
metric = evaluate.load("glue", "mrpc")
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"])
print(metric.compute())
我们可以做个对比:
1、使用MPS
2、使用CPU
显然,MPS的速度是CPU的两倍多
我CPU炸了……
你可能会注意到用CPU训练的准确率略高于MPS,这并不意味着前者更优。准确率可能受浮点数运算精度、数值稳定性等因素影响;某些层(如Batch Normalization和Dropout)的行为在训练和评估模式下可能不同,如果这些层在不同的设备上表现不一致,也会影响最终的准确率。