基于 Transformers 实现模型微调训练的主要流程,包括:

  • 数据集下载
  • 数据预处理
  • 训练超参数配置
  • 训练评估指标设置
  • 模型训练
  • 模型保存

一、 数据集下载

Hugging Face 是一个流行的机器学习库,它提供了许多用于自然语言处理(NLP)的工具和数据集。YelpReviewFull 数据集是 Hugging Face 提供的一个数据集,它包含了来自 Yelp 的用户评论,主要用于情感分析和文本分类任务。Yelp 是一个在线评论和评分网站,用户可以在上面对各种商家(如餐馆、酒店等)进行评论和评分。

数据集特点:

  • 评论内容‌:YelpReviewFull 数据集包含了大量的用户评论,这些评论涵盖了各种主题和情感。
  • 情感标签‌:每个评论都带有情感标签,通常分为正面(积极)或负面(消极)两种。
  • 文本多样性‌:数据集包含了不同主题的评论,例如餐饮、购物、服务等,这使得数据集非常适合于训练模型以处理多样化的文本数据。

Hugging Face 数据集下载: YelpReviewFull

数据集下载

from datasets import load_dataset

dataset = load_dataset("yelp_review_full")
# dataset = load_dataset("./model/datasets/yelp_review_full") #加载,已下载到本地的数据

print(dataset)

输出:

    DatasetDict({
        train: Dataset({
            features: ['label', 'text'],
            num_rows: 650000
        })
        test: Dataset({
            features: ['label', 'text'],
            num_rows: 50000
        })
    })

Yelp数据集总共有650,000个训练样本和50,000个测试样本,其中:

  • ‘label’: 对应于评论的分数(介于0和4之间)
  • ‘text’: 评论文本

输入:

dataset["train"][111]

输出:

{'label': 2,
 'text': "As far as Starbucks go, this is a pretty nice one.  The baristas are friendly and while I was here, a lot of regulars must have come in, because they bantered away with almost everyone.  The bathroom was clean and well maintained and the trash wasn't overflowing in the canisters around the store.  The pastries looked fresh, but I didn't partake.  The noise level was also at a nice working level - not too loud, music just barely audible.\\n\\nI do wish there was more seating.  It is nice that this location has a counter at the end of the bar for sole workers, but it doesn't replace more tables.  I'm sure this isn't as much of a problem in the summer when there's the space outside.\\n\\nThere was a treat receipt promo going on, but the barista didn't tell me about it, which I found odd.  Usually when they have promos like that going on, they ask everyone if they want their receipt to come back later in the day to claim whatever the offer is.  Today it was one of their new pastries for $1, I know in the summer they do $2 grande iced drinks with that morning's receipt.\\n\\nOverall, nice working or socializing environment.  Very friendly and inviting.  It's what I've come to expect from Starbucks, so points for consistency."}

加载 Yelp Review Full 数据集,随机展示num_examples个样本。

import random
import pandas as pd
import datasets
from IPython.display import display, HTML

def show_random_elements(dataset, num_examples=10):
    # 断言:确保请求的样本数量不超过数据集大小
    assert num_examples <= len(dataset), "Can't pick more elements than there are in the dataset."
    picks = []

    for _ in range(num_examples):
        pick = random.randint(0, len(dataset) - 1)
        # 随机选择不重复的样本索引
        while pick in picks:
            pick = random.randint(0, len(dataset) - 1)
        picks.append(pick)

    df = pd.DataFrame(dataset[picks])
    # 处理分类标签:将数字标签转换为对应的标签名称
    for column, type in dataset.features.items():
        if isinstance(type, datasets.ClassLabel): # 判断type是否是datasets.ClassLabel类型
            df[column] = df[column].transform(lambda i: type.names[i])
    display(HTML(df.to_html()))


show_random_elements(dataset["train"], num_examples=1)

输出:

labeltext
02 starI stopped in for dinner last night, craving their grits... the only thing that was any good during this visit were the fried eggs. I ordered the High Flyer again, my usual order at both locations, and it was a wreck. \n\nService was friendly and attentive, as usual. On a Friday night, there were two other tables seated, and they were finishing up when I sat down. Both Biscuits refuse to fully air condition the restaurant, so it was as tropical as the table cloths. \n\nThe food:\nThe eggs, cooked to over-well, were perfect. \nThe chicken sausage was over cooked and almost crunchy. So overly seasoned that I woke up at 1 am and it felt like my chest was on fire from indigestion.\nThe biscuit - well, I stated before how I feel about their biscuits. Too heavy and really just a way to get the delicious apple cranberry butter into my pie hole. But at least it wasn't burned.\nI substituted a buttermilk pancake for the oatmeal pancake and it was burned AGAIN. It's a pancake - if you burn it, throw it out and give me another one - it's not a steak, it's batter that probably costs less than a nickel. \nThe grits - my favorite grits - grits that make my mouth water every time I drive by this or the Park Rd location - were AWFUL. Overly cheesed and overly salty, clumps of grits were stuck together - BAD. \n\nAs my Pawpaw said, during his final days, after spending the weekend with a family member that will make you wish death would come soon, \"I ain't goin' back\".

二、预处理数据

对于长度不等的输入数据,使用填充(padding)和截断(truncation)策略来处理。Datasets 的 map 方法,支持一次性在整个数据集上应用预处理函数。

下面使用填充到最大长度的策略,处理整个数据集:

from transformers import AutoTokenizer
# 加载bert-base-cased中的分词
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
# 对于短数据进行填充(padding),对长数据进行阶段(truncation)
def tokenize_function(examples):
    return tokenizer(examples["text"], padding="max_length", truncation=True)
# 预处理数据
tokenized_datasets = dataset.map(tokenize_function, batched=True)
# 随机展示1条数据
show_random_elements(tokenized_datasets["train"], num_examples=1)

输出如下,其中:

  • input_ids: token_ids
  • token_type_ids: token_id 归属的句子编号
  • attention_mask: 指示哪些token需要被关注(注意力机制)
labeltextinput_idstoken_type_idsattention_mask
02 starHere's the deal: Long Lines - Overpriced - Good Service - Average Food.\n\nI would LOVE IT if Steve would actually eat in his own buffet so he could see what it has become. When we were here years past the quality of the food was so much better (but the service was lacking). Now it's the opposite-- the service is good but you can really notice the decline in the food (cuts of meat, etc). And, two small glasses of wine will set you back a good $25. \n\nI know it's the strip and I don't mind paying $40 a person IF the food is good (you'd pay that in a decent restaurant anywhere else). My problem is that the Wynn prides itself on quality... \n\nActually, to be honest I've decided I now officially hate buffets. I can't stand watching people stuffing themselves and coming back to their table with plates piled high with 50+ crab legs. Not to mention the amount of food that is wasted is ridiculous. \n\nPeople, what have we become? \n\nBTW, if you want a good buffet, skip the Wynn and head over to the new one at the Cosmopolitan Hotel.[101, 3446, 112, 188, 1103, 2239, 131, 3261, 12058, 118, 3278, 1643, 10835, 1181, 118, 2750, 2516, 118, 18098, 6702, 119, 165, 183, 165, 183, 2240, 1156, 149, 2346, 17145, 9686, 1191, 3036, 1156, 2140, 3940, 1107, 1117, 1319, 171, 9435, 2105, 1177, 1119, 1180, 1267, 1184, 1122, 1144, 1561, 119, 1332, 1195, 1127, 1303, 1201, 1763, 1103, 3068, 1104, 1103, 2094, 1108, 1177, 1277, 1618, 113, 1133, 1103, 1555, 1108, 11744, 114, 119, 1986, 1122, 112, 188, 1103, 3714, 118, 118, 1103, 1555, 1110, 1363, 1133, 1128, 1169, 1541, 4430, 1103, 6246, 1107, 1103, 2094, 113, 7484, 1104, 6092, ...][0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...][1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ...]
  • 数据抽样

随机抽取 1000 个数据样本,在 BERT 上进行小规模训练(基于 Pytorch Trainer):

small_train_dataset = tokenized_datasets["train"].shuffle(seed=42).select(range(1000))
small_eval_dataset = tokenized_datasets["test"].shuffle(seed=42).select(range(1000))

三、微调训练配置

1、加载 BERT 模型
from transformers import AutoModelForSequenceClassification
# 数据标签有5个
model = AutoModelForSequenceClassification.from_pretrained("bert-base-cased", num_labels=5)
2、训练超参数(TrainingArguments)

完整配置参数与默认值:传送门

源代码定义:传送门

from transformers import TrainingArguments

# 重要:模型权重保存路径(output_dir)
model_dir = "fine-turne-model/bert-base-cased-finetune-yelp"

# logging_steps 默认值为500,根据训练数据和步长,将其设置为100
training_args = TrainingArguments(output_dir=model_dir,
                                  per_device_train_batch_size=16,
                                  num_train_epochs=5,
                                  logging_steps=100)
# 完整的超参数配置
print(training_args)

输出:

TrainingArguments(
_n_gpu=1,
adafactor=False,
adam_beta1=0.9,
adam_beta2=0.999,
adam_epsilon=1e-08,
auto_find_batch_size=False,
bf16=False,
bf16_full_eval=False,
data_seed=None,
dataloader_drop_last=False,
dataloader_num_workers=0,
# 此处省略
)
3、训练过程中的指标评估(Evaluate)

Hugging Face Evaluate 库 支持使用一行代码,获得数十种不同领域(自然语言处理、计算机视觉、强化学习等)的评估方法。 当前支持完整评估指标:传送门

训练器(Trainer)在训练过程中不会自动评估模型性能。因此,需要向训练器传递一个函数来计算和报告指标。 Evaluate库提供了一个简单的准确率函数,可以使用evaluate.load函数加载:

import numpy as np
import evaluate

metric = evaluate.load("accuracy")

接着,调用 compute 函数来计算预测的准确率。在将预测传递给 compute 函数之前,将 logits 转换为预测值(所有Transformers 模型都返回 logits)。

def compute_metrics(eval_pred):
    logits, labels = eval_pred
    predictions = np.argmax(logits, axis=-1)
    return metric.compute(predictions=predictions, references=labels)
4、训练过程指标监控

通常,为了监控训练过程中的评估指标变化,我们可以在TrainingArguments指定evaluation_strategy参数,以便在 epoch 结束时报告评估指标。

from transformers import TrainingArguments, Trainer

training_args = TrainingArguments(output_dir=model_dir,
                                  evaluation_strategy="epoch", 
                                  per_device_train_batch_size=8,
                                  num_train_epochs=5,
                                  logging_steps=200)

参数详解:

  • output_dir=model_dir:模型权重保存的地址(重要)
  • evaluation_strategy="epoch":评估策略,每训练完一个epoch,进行一次评估
  • per_device_train_batch_size=8:GPU每次训练时候的batch_size大小
  • num_train_epochs=3:在训练过程中,一个 epoch 表示模型已经看到了训练数据集中的每一个样本一次。例如,如果有 1000 个训练样本,那么一个 epoch 就意味着模型会处理这 1000 个样本各一次。当设置 num_train_epochs=5 时,模型会完整地遍历整个训练数据集 5 次。足够的 epochs 数量可以让模型充分学习数据中的模式和特征。如果 epochs 太少,模型可能无法充分学习;如果 epochs 太多,可能导致过拟合
  • logging_steps=200:日志记录频率,便于监控训练过程
5、开始训练

实例化训练器(Trainer):

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=small_train_dataset,
    eval_dataset=small_eval_dataset,
    compute_metrics=compute_metrics,
)

trainer.train()

输出:

32%|███▏      | 200/625 [01:34<03:28,  2.04it/s]{'loss': 1.2236, 'grad_norm': 23.59279441833496, 'learning_rate': 3.408e-05, 'epoch': 1.6}
 64%|██████▍   | 400/625 [03:02<01:37,  2.32it/s]{'loss': 0.7053, 'grad_norm': 20.813697814941406, 'learning_rate': 1.808e-05, 'epoch': 3.2}
 96%|█████████▌| 600/625 [04:29<00:10,  2.32it/s]{'loss': 0.2977, 'grad_norm': 11.430902481079102, 'learning_rate': 2.08e-06, 'epoch': 4.8}
100%|██████████| 625/625 [04:41<00:00,  2.22it/s]
{'train_runtime': 281.4953, 'train_samples_per_second': 17.762, 'train_steps_per_second': 2.22, 'train_loss': 0.7188893650054932, 'epoch': 5.0}
TrainOutput(global_step=625, training_loss=0.7188893650054932, metrics={'train_runtime': 281.4953, 'train_samples_per_second': 17.762, 'train_steps_per_second': 2.22, 'total_flos': 1315590712320000.0, 'train_loss': 0.7188893650054932, 'epoch': 5.0})

由于logging_steps=200,所以每200次会记录训练数据。 num_train_epochs=5 ,模型会完整地遍历整个训练数据集 5 次,一共是5000条数据。 per_device_train_batch_size=8,5000/8 = 625批次。

使用100个样本进行测试:

small_test_dataset = tokenized_datasets["test"].shuffle(seed=64).select(range(100))
trainer.evaluate(small_test_dataset)

输出:

{'eval_loss': 1.4982781410217285, 'eval_accuracy': 0.57, 'eval_runtime': 1.737, 'eval_samples_per_second': 57.57, 'eval_steps_per_second': 7.484, 'epoch': 5.0}
6、保存模型和训练状态
  • 使用 trainer.save_model 方法保存模型,后续可以通过 from_pretrained() 方法重新加载
  • 使用 trainer.save_state 方法保存训练状态
trainer.save_model(model_dir)
trainer.save_state()
附录(完整代码)
'''根据需要设置数据和模型的下载地址
import os
# windows
os.environ['HF_HOME'] ='./model/'
os.environ['HF_HUB_CACHE'] = './model/hub/'
'''

from datasets import load_dataset
import random
import pandas as pd
import datasets
from IPython.display import display, HTML
from transformers import AutoTokenizer
import evaluate
import numpy as np
from transformers import TrainingArguments, Trainer
from transformers import AutoModelForSequenceClassification

def show_random_elements(dataset, num_examples=10):
    # 断言:确保请求的样本数量不超过数据集大小
    assert num_examples <= len(dataset), "Can't pick more elements than there are in the dataset."
    picks = []

    for _ in range(num_examples):
        pick = random.randint(0, len(dataset) - 1)
        # 随机选择不重复的样本索引
        while pick in picks:
            pick = random.randint(0, len(dataset) - 1)
        picks.append(pick)

    df = pd.DataFrame(dataset[picks])
    # 处理分类标签:将数字标签转换为对应的标签名称
    for column, type in dataset.features.items():
        if isinstance(type, datasets.ClassLabel): # 判断type是否是datasets.ClassLabel类型
            df[column] = df[column].transform(lambda i: type.names[i])
    # display(HTML(df.to_html())) #在jupyter lab上作为html展示
    print(df.to_string())

def tokenize_function(examples):
    return tokenizer(examples["text"], padding="max_length", truncation=True)

# 加载数据
dataset = load_dataset("yelp_review_full")
# 随机展示数据
#show_random_elements(dataset["train"])

# 预处理数据
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
tokenized_datasets = dataset.map(tokenize_function, batched=True)
# 随机展示数据
# show_random_elements(tokenized_datasets["train"], num_examples=1)

# 定义测试集和训练集
small_train_dataset = tokenized_datasets["train"].shuffle(seed=42).select(range(1000))
small_eval_dataset = tokenized_datasets["test"].shuffle(seed=42).select(range(1000))


# 加载模型,数据标签有5个
model = AutoModelForSequenceClassification.from_pretrained("bert-base-cased", num_labels=5)

# 设置微调后的模型存放地址 
model_dir = "fine-turne-model/bert-base-cased-finetune-yelp"
# logging_steps 默认值为500,根据我们的训练数据和步长,将其设置为100
training_args = TrainingArguments(output_dir=model_dir,
                                  per_device_train_batch_size=8,
                                  num_train_epochs=5,
                                  logging_steps=200)
# 设置模型评估参数
metric = evaluate.load("accuracy")
def compute_metrics(eval_pred):
    logits, labels = eval_pred
    predictions = np.argmax(logits, axis=-1)
    return metric.compute(predictions=predictions, references=labels)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=small_train_dataset,
    eval_dataset=small_eval_dataset,
    compute_metrics=compute_metrics,
)

print("训练开始")
print(trainer.train())

print("评估开始")
small_test_dataset = tokenized_datasets["test"].shuffle(seed=64).select(range(100))
print(trainer.evaluate(small_test_dataset))

输出:

训练开始
 32%|███▏      | 200/625 [01:34<03:28,  2.04it/s]{'loss': 1.2236, 'grad_norm': 23.59279441833496, 'learning_rate': 3.408e-05, 'epoch': 1.6}
 64%|██████▍   | 400/625 [03:02<01:37,  2.32it/s]{'loss': 0.7053, 'grad_norm': 20.813697814941406, 'learning_rate': 1.808e-05, 'epoch': 3.2}
 96%|█████████▌| 600/625 [04:29<00:10,  2.32it/s]{'loss': 0.2977, 'grad_norm': 11.430902481079102, 'learning_rate': 2.08e-06, 'epoch': 4.8}
100%|██████████| 625/625 [04:41<00:00,  2.22it/s]
{'train_runtime': 281.4953, 'train_samples_per_second': 17.762, 'train_steps_per_second': 2.22, 'train_loss': 0.7188893650054932, 'epoch': 5.0}
TrainOutput(global_step=625, training_loss=0.7188893650054932, metrics={'train_runtime': 281.4953, 'train_samples_per_second': 17.762, 'train_steps_per_second': 2.22, 'total_flos': 1315590712320000.0, 'train_loss': 0.7188893650054932, 'epoch': 5.0})
评估开始
100%|██████████| 13/13 [00:01<00:00,  8.16it/s]
{'eval_loss': 1.4982781410217285, 'eval_accuracy': 0.57, 'eval_runtime': 1.737, 'eval_samples_per_second': 57.57, 'eval_steps_per_second': 7.484, 'epoch': 5.0}
Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐