NLP —— Hugging Face A full Training代码实现

我们将使用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)的行为在训练和评估模式下可能不同,如果这些层在不同的设备上表现不一致,也会影响最终的准确率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值