模型微调
1.Transformer 模型
- 什么是自然语言处理?NLP 是语言学和机器学习交叉领域,专注于理解与人类语言相关的一切,NLP 任务的目标不仅是单独理解单个单词,而且是能够理解这些单词的上下文
- 常见的 NLP 任务,对整个句子进行分类,对句子中每个词进行分类,生成文本内容,从文本中提取答案,从输入文本生成新句子
2.使用 Transformers
transformers 库的目标是提供一个统一的 API 接口,通过它可以加载/训练和保存任何 transformer 模型
tokenizer API 是 pipeline()函数的重要组成部分,负责第一步和最后一步的处理,将文本转换到神经网络的输入以及在需要时将其转换回文本
transformers 库中 pipeline 处理的三个步骤:使用 tokenizer 进行预处理,通过模型传递输入,后处理;模型无法直接处理原始文本,因此该管道的第一步是将文本转换为模型能够理解的数字,tensor (张量)
tokenizer, 基于单词的 tokenization无法表示相似词的相似性,基于字符的 tokenization 失去了单词的含义,基于子词的 tokenization 结合了前面二者的优点
使用 (注意 tensor 必须是矩形,一维向量(张量)长度一致)
tokenize示例
Python
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
sequence = "Using a Transformer network is simple"
tokens = tokenizer.tokenize(sequence)
ids = tokenizer.convert_tokens_to_ids(tokens)
decoded_string = tokenizer.decode(ids)
print(tokens)
print(ids)
print(decoded_string)
print(f"词汇表大小: {len(tokenizer.vocab)}")
print(f"词汇表内容示例: {list(tokenizer.vocab.keys())[:20]}")
# 结果
"""
['Using', 'a', 'Trans', '##former', 'network', 'is', 'simple']
[7993, 170, 13809, 23763, 2443, 1110, 3014]
Using a Transformer network is simple
词汇表大小: 28996
词汇表内容示例: ['##up', 'applying', '##ample', 'replaced', 'jumper', 'destroyed', 'convened', 'iPhone', 'therefore', '##master', '##本', 'Bosnia', '##utile', 'spaced', '##roon', 'tensions', 'brows', '##ogan', 'altitude', 'Karnataka']
"""
"""
模型识别文字的方式?
BERT使用WordPiece分词算法,流程如下:
1. 分词: 将文本拆分成子词单元
- 常见词保持完整:Using, a, network, is, simple
- 长词拆成前缀+后缀:Transformer → Trans + ##former(##表示后缀)
2. 映射ID: 每个token对应词汇表中的一个固定ID
3. 解码: ID序列还原为文本
模型认识的文字范围
bert-base-cased的词汇表大小是28996个token。
词汇表内容:
- 英文常见单词和子词
- 标点符号
- 特殊token:[CLS], [SEP], [UNK], [PAD], [MASK]
限制:未知文字会被标记为[UNK](unknown)。例如中文输入会产生大量[UNK],因为词汇表主要是英文。
词汇表存储位置?
tokenizer.json │ JSON格式,model.vocab 字段包含 {token: id} 映射
tokenizer.json 结构
{
"model": {
"vocab": {"[PAD]": 0, "[unused1]": 1, ..., "Using": 7993, ...},
"unk_token": "[UNK]",
"continuing_subword_prefix": "##"
},
"normalizer": {...}, // 文本规范化规则
"pre_tokenizer": {...}, // 预分词规则
"decoder": {...}, // 解码规则
"added_tokens": [...] // 新增的特殊token
}
tokenizer.json 是HuggingFace统一格式(包含完整分词配置)
tokenizer.json 是模型的“语言门户”。没有它,模型即使有万亿参数,也无法理解你输入的任何一个字。
"""- 从 tokenizer 到模型
Python
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
sequences = ["I've been waiting for a HuggingFace course my whole life.", "So have I!"]
# tokenizer()函数内部自动做了填充(保证不同句子分词后的 token 列表长度一致)
# tokenizer()函数内部自动做了截断(保证输入不会超过模型可以接受的最大 token 数量)
# tokenizer()函数内部自动做了张量转换,先把句子分词,再把分词映射为数字,再把数字转换为 pytorch tensor,因为模型只接受张量 tensor 输入
tokens = tokenizer(sequences, padding=True, truncation=True, return_tensors="pt")
print(tokens)
output = model(**tokens)
print(output)
"""
{'input_ids':
tensor([[ 101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607, 2026, 2878, 2166, 1012, 102],
[ 101, 2061, 2031, 1045, 999, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[ 101, 7592, 2088, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]),
'token_type_ids':
tensor([[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]]),
'attention_mask':
tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])}
SequenceClassifierOutput(loss=None,
logits=tensor([[-1.5607, 1.6123],
[-3.6183, 3.9137],
[-3.9943, 4.3083]],
grad_fn=<AddmmBackward0>), hidden_states=None, attentions=None)
"""3.微调一个预训练模型
- 如何使用自己的数据集微调预训练模型呢?从模型中心(hub)加载大型数据集;使用高级 Trainer API 微调一个模型;自定义训练过程;利用 Accelerate 库在所有分布式设备上轻松运行自定义训练过程;
1. 从模型中心加载数据集
Python
# 加载 GLUE 基准测试中的 MRPC(微软研究释义语料库)数据集
# MRPC 是一个句子对分类任务,判断两个句子是否语义相同( paraphrase 或 not paraphrase)
from datasets import load_dataset
# load_dataset() 从 HuggingFace Hub 下载并加载数据集
# "glue" 是基准测试名称,"mrpc" 是具体子任务
raw_datasets = load_dataset("glue", "mrpc")
print(raw_datasets) # 打印数据集结构,包含 train、validation、test 三个子集
# 查看训练集中的第 15 条样本
# 每条样本包含:sentence1、sentence2、label(0=不相同,1=相同)、idx
print(raw_datasets["train"][15])
# 查看验证集中的第 87 条样本
print(raw_datasets["validation"][87])
#结果
"""
DatasetDict({
train: Dataset({
features: ['sentence1', 'sentence2', 'label', 'idx'],
num_rows: 3668
})
validation: Dataset({
features: ['sentence1', 'sentence2', 'label', 'idx'],
num_rows: 408
})
test: Dataset({
features: ['sentence1', 'sentence2', 'label', 'idx'],
num_rows: 1725
})
})
label == 0 表示两句非同义,label == 1 表示两句同义
{'sentence1': 'Rudder was most recently senior vice president for the Developer & Platform Evangelism Business .', 'sentence2': 'Senior Vice President Eric Rudder , formerly head of the Developer and Platform Evangelism unit , will lead the new entity .', 'label': 0, 'idx': 16}
{'sentence1': 'However , EPA officials would not confirm the 20 percent figure .', 'sentence2': 'Only in the past few weeks have officials settled on the 20 percent figure .', 'label': 0, 'idx': 812}
"""2. 预处理数据集 + 动态填充
- 预处理数据集的作用是,将数据集提前处理好,生成数字(模型只能读懂数字)保存到内存中(增量文件关联到原数据集文件)
- 动态填充,预处理之后的数据集一批一批加载到内存的同时进行动态填充,以此来保证每批数据的长度统一(模型的要求),同时可以只填充该批的最长长度,比较省空间
Python
from datasets import load_dataset
from transformers import AutoTokenizer
# 加载的映射(索引),数据不进内存
raw_datasets = load_dataset("glue", "mrpc")
print(raw_datasets)
"""
DatasetDict({
train: Dataset({
features: ['sentence1', 'sentence2', 'label', 'idx'],
num_rows: 3668
})
validation: Dataset({
features: ['sentence1', 'sentence2', 'label', 'idx'],
num_rows: 408
})
test: Dataset({
features: ['sentence1', 'sentence2', 'label', 'idx'],
num_rows: 1725
})
})
"""
checkpoint = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
# 输入句子数据进行分词,生成模型可以读懂的数字数据
ids1 = tokenizer("This is the first sentence.", "This is the second one.")
print(ids1)
"""
{'input_ids': [101, 2023, 2003, 1996, 2034, 6251, 1012, 102, 2023, 2003, 1996, 2117, 2028, 1012, 102],
'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1],
'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}
"""
# 把数字转换回分词文字
t1 = tokenizer.convert_ids_to_tokens(ids1["input_ids"])
# 多句话都在同一个 input_ids列表中了,token_type_ids的值用来区分每一句话(有些模型有token_type_ids,取决于模型预训练时是否用过)
print(t1)
"""
['[CLS]', 'this', 'is', 'the', 'first', 'sentence', '.', '[SEP]', 'this', 'is', 'the', 'second', 'one', '.', '[SEP]']
"""
# 预处理训练数据集
# 首先raw_datasets = load_dataset("glue", "mrpc")方式下载的数据集会保存在本地磁盘,以 Apache Arrow 格式
# 以这种方式将磁盘中的数据集所有数据加载到内存中,可能内存会不够用
# tokenized_dataset = tokenizer(raw_datasets["train"]["sentence1"], raw_datasets["train"]["sentence2"], padding=True, truncation=True)
def tokenize_function(example):
# 之前直接 tokenizer(raw_datasets["train"]["sentence1"]) 报错,是因为 Arrow 类型不被识别。
# 而 .map() 方法内部会自动处理类型转换,把 Arrow 数据转成 Python 原生类型再传给你的函数,所以 example["sentence1"] 实际上已经是 list[str],tokenizer 可以正常处理。
return tokenizer(example["sentence1"], example["sentence2"], truncation=True)
# 将数据集分成多个 batch(默认每批 1000条),对每个 batch 调用tokenize_function,将返回的 tokenization 结果合并到原始数据集中得到新的数据集
tokenized_datasets = raw_datasets.map(tokenize_function, batched=True)
print(tokenized_datasets)
print(tokenized_datasets.cache_files)
"""
DatasetDict({
train: Dataset({
features: ['sentence1', 'sentence2', 'label', 'idx', 'input_ids', 'token_type_ids', 'attention_mask'],
num_rows: 3668
})
validation: Dataset({
features: ['sentence1', 'sentence2', 'label', 'idx', 'input_ids', 'token_type_ids', 'attention_mask'],
num_rows: 408
})
test: Dataset({
features: ['sentence1', 'sentence2', 'label', 'idx', 'input_ids', 'token_type_ids', 'attention_mask'],
num_rows: 1725
})
})
{'train': [{'filename': '/Users/<account>/.cache/huggingface/datasets/glue/mrpc/0.0.0/bcdcba79d07bc864c1c254ccfcedcce55bcc9a8c/cache-9c65154dc04d15c8.arrow'}],
'validation': [{'filename': '/Users/<account>/.cache/huggingface/datasets/glue/mrpc/0.0.0/bcdcba79d07bc864c1c254ccfcedcce55bcc9a8c/cache-c476afed73355cef.arrow'}],
'test': [{'filename': '/Users/<account>/.cache/huggingface/datasets/glue/mrpc/0.0.0/bcdcba79d07bc864c1c254ccfcedcce55bcc9a8c/cache-49ddf8863e6bf95c.arrow'}]}
"""
# 取预处理后的数据集的前 8 行数据,并去除idx,sentence1,sentence2键
samples = tokenized_datasets["train"][:8]
samples = {k: v for k, v in samples.items() if k not in ["idx", "sentence1", "sentence2"]}
print([len(x) for x in samples["input_ids"]])
"""
结果如下,长度不统一
[50, 59, 47, 67, 59, 50, 62, 32]
"""
# 动态填充
# 因为每个句子长度不同,需要填充到统一长度,该工具会在一个 batch 内找到最长的那条,把其他短的补 0(padding token)到相同长度,这样比预先固定长度更省内存
from transformers import DataCollatorWithPadding
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)
# 进行动态填充,长度统一
batch2 = data_collator(samples)
print({k: v.shape for k, v in batch2.items()})
"""
{'input_ids': torch.Size([8, 67]),
'token_type_ids': torch.Size([8, 67]),
'attention_mask': torch.Size([8, 67]),
'labels': torch.Size([8])}
"""3. 使用 Trainer API 微调一个模型
- transformers 库提供了 Trainer 类,可以帮助你在数据集上微调任何预训练模型
Python
from datasets import load_dataset
from transformers import AutoTokenizer, DataCollatorWithPadding
# 加载数据集
# glue 是一个英文语言理解基准测试平台,包含多个 NLP 任务,用于评估预训练语言模型的理解能力。就像 AI 模型的"考试系统",包含多道" 题目"(子任务)。
# mrpc 是 GLUE 中的一个子任务,微软研究院发布的句子对数据集。 任务类型:二分类 — 判断两个句子是否表达相同含义(是否是 paraphrase)
raw_datasets = load_dataset("glue", "mrpc")
checkpoint = "bert-base-uncased"
# 加载指定模型的分词器
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)
# 数据batch动态填充工具
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)
"""
测试每一步流程
from transformers import TrainingArguments
from transformers import AutoModelForSequenceClassification
from transformers import Trainer
import numpy as np
import evaluate
# 训练参数
raining_args = TrainingArguments("test-trainer", num_train_epochs=1, learning_rate=5e-4, per_device_train_batch_size=4)
# 模型,num_labels=2指定分类任务的类别数量
model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)
# 训练
trainer = Trainer(model,
training_args,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["validation"],
data_collator=data_collator,
processing_class=tokenizer)
trainer.train()
# 评估,使用数据集中的验证集对模型进行评估
predictions = trainer.predict(tokenized_datasets["validation"])
print(predictions.predictions.shape, predictions.label_ids.shape)
# 将模型输出的 logits 逻辑值浮点数转化为可以与标签进行比较的预测值,返回数组中最大值所在的索引,`axis=-1`沿最后一个维度(即类别维度)寻找最大值
preds = np.argmax(predictions.predictions, axis=-1)
# 将上一步生成的预测值和真实标签值进行对比计算准确率和 F1
metric = evaluate.load("glue", "mrpc")
metric.compute(predictions=preds, references=predictions.label_ids)
"""
from transformers import TrainingArguments
from transformers import AutoModelForSequenceClassification
from transformers import Trainer
import numpy as np
import evaluate
# 总结上述评估步骤,给出总的评估配置
def compute_metrics(eval_preds):
metric = evaluate.load("glue", "mrpc")
logits, labels = eval_preds
predictions = np.argmax(logits, axis=-1)
return metric.compute(predictions=predictions, references=labels)
training_args = TrainingArguments("test-trainer", eval_strategy="epoch")
model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)
trainer = Trainer(model,
training_args,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["validation"],
data_collator=data_collator,
processing_class=tokenizer,
compute_metrics=compute_metrics, )
# ========== Baseline 评估(微调前)==========
print("Baseline 评估(微调前的原始模型)")
baseline_val = trainer.evaluate(tokenized_datasets["validation"])
baseline_test = trainer.evaluate(tokenized_datasets["test"])
print(f"Validation 集 baseline: accuracy={baseline_val['eval_accuracy']:.4f}, f1={baseline_val['eval_f1']:.4f}")
print(f"Test 集 baseline: accuracy={baseline_test['eval_accuracy']:.4f}, f1={baseline_test['eval_f1']:.4f}")
"""
Validation 集 baseline: accuracy=0.6838, f1=0.8122
Test 集 baseline: accuracy=0.6649, f1=0.7987
"""
# ========== 开始微调训练 ==========
print("开始微调训练...")
trainer.train()
"""
# 1.评估输出(带 eval_ 前缀)来源: Trainer.evaluate() 或每个 epoch 结束时的自动评估
# 触发时机: eval_strategy="epoch" 设置后,每个 epoch 结束时自动运行
# 数据来源: validation 集
# 2. 训练输出(无 eval_ 前缀)
# 来源: Trainer.train() 内部的训练循环
# 触发时机: 每隔一定步数(默认每 500 步)
# 数据来源: 训练 batch 的数据 【grad_norm │ 梯度的范数(用于监控梯度爆炸)】
{'eval_loss': '0.4013', 'eval_model_preparation_time': '0.0012', 'eval_accuracy': '0.8309', 'eval_f1': '0.886', 'eval_runtime': '5.73', 'eval_samples_per_second': '71.2', 'eval_steps_per_second': '8.901', 'epoch': '1'}
{'loss': '0.4941', 'grad_norm': '12.77', 'learning_rate': '3.188e-05', 'epoch': '1.089'}
{'eval_loss': '0.5849', 'eval_model_preparation_time': '0.0012', 'eval_accuracy': '0.8333', 'eval_f1': '0.8859', 'eval_runtime': '5.802', 'eval_samples_per_second': '70.33', 'eval_steps_per_second': '8.791', 'epoch': '2'}
'loss': '0.2593', 'grad_norm': '0.1277', 'learning_rate': '1.373e-05', 'epoch': '2.179'}
{'eval_loss': '0.7269', 'eval_model_preparation_time': '0.0012', 'eval_accuracy': '0.8578', 'eval_f1': '0.899', 'eval_runtime': '5.461', 'eval_samples_per_second': '74.71', 'eval_steps_per_second': '9.338', 'epoch': '3'}
{'train_runtime': '320.2', 'train_samples_per_second': '34.37', 'train_steps_per_second': '4.301', 'train_loss': '0.3005', 'epoch': '3'}
"""
# ========== 微调后评估 ==========
print("微调后评估")
after_val = trainer.evaluate(tokenized_datasets["validation"])
after_test = trainer.evaluate(tokenized_datasets["test"])
print(f"Validation 集: accuracy={after_val['eval_accuracy']:.4f}, f1={after_val['eval_f1']:.4f}")
print(f"Test 集: accuracy={after_test['eval_accuracy']:.4f}, f1={after_test['eval_f1']:.4f}")
"""
Validation 集: accuracy=0.8578, f1=0.8990
Test 集: accuracy=0.8458, f1=0.8871
"""
# ========== 效果对比总结 ==========
print("微调效果对比总结")
print(
f"Validation 集提升: accuracy +{after_val['eval_accuracy'] - baseline_val['eval_accuracy']:.4f}, f1 +{after_val['eval_f1'] - baseline_val['eval_f1']:.4f}")
print(
f"Test 集提升: accuracy +{after_test['eval_accuracy'] - baseline_test['eval_accuracy']:.4f}, f1 +{after_test['eval_f1'] - baseline_test['eval_f1']:.4f}")
"""
Validation 集提升: accuracy +0.1740, f1 +0.0867
Test 集提升: accuracy +0.1809, f1 +0.0884
1. 相对提升幅度
从你的 baseline 到微调后:
┌─────────────────────┬──────────┬────────┬───────┬────────────┐
│ 指标 │ Baseline │ 微调后 │ 提升 │ 相对提升率 │
├─────────────────────┼──────────┼────────┼───────┼────────────┤
│ Validation accuracy │ ~0.66 │ ~0.83 │ +0.17 │ +26% │
├─────────────────────┼──────────┼────────┼───────┼────────────┤
│ Validation f1 │ ~0.80 │ ~0.89 │ +0.09 │ +11% │
└─────────────────────┴──────────┴────────┴───────┴────────────┘
相对提升率 = 提升 / baseline,accuracy 提升了 26%,这是明显的。
2. 与任务基准对比
MRPC 任务的历史水平:
┌───────────────────────┬──────────┬─────────┐
│ 模型 │ Accuracy │ F1 │
├───────────────────────┼──────────┼─────────┤
│ 原始 BERT(未微调) │ ~60-70% │ ~75-80% │
├───────────────────────┼──────────┼─────────┤
│ 微调后 BERT-base │ 84-85% │ 88-89% │
├───────────────────────┼──────────┼─────────┤
│ 更强模型(RoBERTa等) │ ~90% │ ~92% │
├───────────────────────┼──────────┼─────────┤
│ 人类水平 │ ~90% │ ~92% │
└───────────────────────┴──────────┴─────────┘
你的微调结果(accuracy ~83%, f1 ~89%)接近 BERT-base 的典型水平,说明微调有效。
3. 判断提升大小的标准
┌──────────┬──────────────────┐
│ 提升幅度 │ 评价 │
├──────────┼──────────────────┤
│ < 1% │ 微小,可能不显著 │
├──────────┼──────────────────┤
│ 1-5% │ 小但有意义 │
├──────────┼──────────────────┤
│ 5-10% │ 中等,值得注意 │
├──────────┼──────────────────┤
│ 10-20% │ 较大,明显改进 │
├──────────┼──────────────────┤
│ > 20% │ 显著提升 │
└──────────┴──────────────────┘
你的 +17% accuracy 属于较大提升。
4. 为什么 baseline 较低
原始 BERT 模型的分类头是随机初始化的,所以 baseline 表现接近随机猜测(二分类约 50%)。微调后分类头被训练,才能发挥作用。
"""4. 一个完整的训练过程
- 在不使用transformers库 Trainer 类的情况下实现一样的训练步骤和效果
Python
# 在不使用 Trainer 类的情况下实现一样的训练步骤和效果
from datasets import load_dataset
from transformers import AutoTokenizer, DataCollatorWithPadding
raw_datasets = load_dataset("glue", "mrpc")
checkpoint = "bert-base-uncased"
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"])
# 将列名 label 重命名为 labels,因为模型默认的输入是 labels
tokenized_datasets = tokenized_datasets.rename_column("label", "labels")
# 设置数据集的格式,使其返回 pytorch 张量而不是列表
tokenized_datasets.set_format("torch")
print(tokenized_datasets["train"].column_names)
"""
['labels', 'input_ids', 'token_type_ids', 'attention_mask']
"""
from torch.utils.data import DataLoader
# 定义训练数据加载器
train_dataloader = DataLoader(
tokenized_datasets["train"], shuffle=True, batch_size=8, collate_fn=data_collator
)
print(len(train_dataloader), len(tokenized_datasets["train"]))
"""
# 第一个返回的是 batch 数量(注意每 batch 是 8 条数据),第二个返回的是总样本数量3668条,训练完所有 459个 batch 算作一次 epoch
459 3668
"""
# 定义评估数据加载器
eval_dataloader = DataLoader(
tokenized_datasets["validation"], batch_size=8, collate_fn=data_collator
)
# 快速验证数据处理中有没有错误,可以检验其中的一个 batch
for batch in train_dataloader:
print({k: v.shape for k, v in batch.items()})
break
"""
{'labels': torch.Size([8]), 'input_ids': torch.Size([8, 65]), 'token_type_ids': torch.Size([8, 65]), 'attention_mask': torch.Size([8, 65])}
"""
from transformers import AutoModelForSequenceClassification
model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)
# 传入一个 batch 的数据到模型中测试一下,batch 中有 labels 时,transformer 模型都将返回这个 batch 的 loss
for batch in train_dataloader:
outputs = model(**batch)
print(outputs.loss, outputs.logits.shape)
print(outputs)
break
"""
tensor(0.7678, grad_fn=<NllLossBackward0>) torch.Size([8, 2])
SequenceClassifierOutput(loss=tensor(0.7678, grad_fn=<NllLossBackward0>),
logits=tensor([[ 0.4507, -0.3374],
[ 0.4436, -0.3329],
[ 0.4424, -0.3326],
[ 0.4530, -0.3285],
[ 0.4412, -0.3299],
[ 0.4282, -0.3515],
[ 0.4315, -0.3450],
[ 0.4229, -0.3282]], grad_fn=<AddmmBackward0>), hidden_states=None, attentions=None)
"""
# 训练过程
# 1 epoch:
# ├── batch 1: 8 样本 → 前向传播 → 计算损失 → 反向传播 → 更新权重
# ├── batch 2: 8 样本 → 前向传播 → 计算损失 → 反向传播 → 更新权重
# ├── ...
# ├── batch 458: 8 样本 → 前向传播 → 计算损失 → 反向传播 → 更新权重
# └── batch 459: 4 样本 → 前向传播 → 计算损失 → 反向传播 → 更新权重
# ↑
# 这就是 1 个 epoch 完成
# 一个 Batch 是一起训练的
#
# 所有样本同时进行前向传播、计算损失、反向传播。
#
# 训练过程
# 一个 batch (8条数据):
#
# 前向传播:
# ┌─────────────────────────────────────┐
# │ 数据1 ─┐ │
# │ 数据2 ─┤ │
# │ 数据3 ─┼──→ 模型 ──→ 8个输出 ──→ 1个loss │
# │ ... ─┤ (并行计算) │
# │ 数据8 ─┘ │
# └─────────────────────────────────────┘
# ↓
# 反向传播: 更新一次参数
#
# 代码验证
#
# for batch in train_dataloader:
# # batch 包含 8 条数据,形状 [8, 81]
# outputs = model(**batch) # 8条数据一起前向传播
#
# print(outputs.logits.shape) # torch.Size([8, 2]) - 8条数据的预测结果
# print(outputs.loss) # 一个标量 - 8条数据的平均损失
#
# loss.backward() # 基于这个平均损失反向传播
# optimizer.step() # 更新一次参数
#
# 对比两种方式
#
# ┌────────────┬───────────────────────┬────────────────────────────┐
# │ 方式 │ 参数更新次数 │ 特点 │
# ├────────────┼───────────────────────┼────────────────────────────┤
# │ Batch 训练 │ 3668/8 = 459 次/epoch │ 并行计算,速度快,梯度稳定 │
# ├────────────┼───────────────────────┼────────────────────────────┤
# │ 逐条训练 │ 3668 次/epoch │ 串行计算,慢,梯度波动大 │
# └────────────┴───────────────────────┴────────────────────────────┘
#
# 为什么一起训练
#
# # 损失是 batch 内所有样本的平均
# loss = (loss_样本1 + loss_样本2 + ... + loss_样本8) / 8
#
# # 优点:
# # 1. GPU 并行计算,速度快
# # 2. 平均梯度更稳定,训练更平滑
# # 3. 充分利用 GPU 显存带宽
#
# 总结
#
# ┌──────────┬────────────────────────────────────┐
# │ 概念 │ 说明 │
# ├──────────┼────────────────────────────────────┤
# │ Batch 内 │ 8 条数据同时计算,产生一个平均损失 │
# ├──────────┼────────────────────────────────────┤
# │ 参数更新 │ 每个 batch 结束后更新一次 │
# ├──────────┼────────────────────────────────────┤
# │ 1 epoch │ 参数更新 459 次(= batch 数量) │
# └──────────┴────────────────────────────────────┘
# 优化器(使用 AdamW)和学习率调度器
# AdamW vs Adam:
# - Adam:经典自适应学习率优化器
# - AdamW:带权重衰减的 Adam,正则化效果更好,是 BERT 训练的标准选择
from torch.optim import AdamW
# model.parameters()是模型所有可训练参数,lr=5e-5学习率 0.00005(BERT 常用的小学习率)
optimizer = AdamW(model.parameters(), lr=5e-5)
# 学习率调度器
from transformers import get_scheduler
num_epoches = 3
num_training_steps = num_epoches * len(train_dataloader)
print(num_training_steps)
"""
1377
# 训练步数 = epoch 数量 * batch 批次数(一个 batch 全丢进去训练一次)= 3 * 459 = 1377【总训练步数 1377 整个训练过程参数更新 1377 次】
"""
# "linear" # 调度策略
# 学习率变化曲线(linear策略):
#
# 学习率
# │
# 5e-5 ─────┐
# │ \
# │ \
# │ \
# │ \
# │ \
# │ \
# ─────└──────└────→ 训练步数
# 0
# 1377
#
# 对比不同调度策略:
#
# ┌────────────────────────┬──────────────┬──────────────────────┐
# │ 策略 │ 曲线形状 │ 特点 │
# ├────────────────────────┼──────────────┼──────────────────────┤
# │ "linear" │ 线性下降到0 │ BERT常用,简单有效 │
# ├────────────────────────┼──────────────┼──────────────────────┤
# │ "cosine" │ 余弦曲线下降 │ 平滑,后期下降慢 │
# ├────────────────────────┼──────────────┼──────────────────────┤
# │ "cosine_with_restarts" │ 余弦重启 │ 多次周期,适合长训练 │
# ├────────────────────────┼──────────────┼──────────────────────┤
# │ "constant" │ 保持不变 │ 学习率始终5e-5 │
# └────────────────────────┴──────────────┴──────────────────────┘
# num_warmup_steps=0 # 预热步数,设置为 0 则不使用预热,预热的作用: 训练初期学习率从小逐渐增大,避免一开始大学习率破坏预训练权重。
# num_training_steps=num_training_steps # 总训练步数
lr_scheduler = get_scheduler(
"linear",
optimizer=optimizer,
num_warmup_steps=0,
num_training_steps=num_training_steps
)
# 总结
#
# ┌────────────────────┬──────────────────────────────────────┐
# │ 组件 │ 作用 │
# ├────────────────────┼──────────────────────────────────────┤
# │ AdamW │ 优化器,决定如何更新参数 │
# ├────────────────────┼──────────────────────────────────────┤
# │ num_training_steps │ 总训练步数 = epoch × batch数 │
# ├────────────────────┼──────────────────────────────────────┤
# │ lr_scheduler │ 学习率调度器,让学习率随训练逐渐降低 │
# └────────────────────┴──────────────────────────────────────┘
#
# 为什么需要学习率调度?
# - 训练初期:需要较大学习率快速学习
# - 训练后期:需要小学习率精细调整,避免震荡
# 访问 GPU 设置,目的是加快训练(不过我这台机器目前没有 cuda😭)
# import torch
# device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
# model.to(device)
# 训练循环♻️
# 使用 tqdm 库,在训练步骤数上添加一个进度条
from tqdm.auto import tqdm
progress_bar = tqdm(range(num_training_steps))
# 设置训练模式
# ┌───────────────┬────────────────────────────────────────┐
# │ 模式 │ 作用 │
# ├───────────────┼────────────────────────────────────────┤
# │ model.train() │ 训练模式,启用Dropout、BatchNorm更新 │
# ├───────────────┼────────────────────────────────────────┤
# │ model.eval() │ 评估模式,关闭Dropout、固定BatchNorm │
# └───────────────┴────────────────────────────────────────┘
model.train()
for epoch in range(num_epoches):
for batch in train_dataloader:
# 使用 GPU 加速,因为本机配置问题,暂不用
# batch = {k: v.to(device) for k, v in batch.items()}
# 前向传播
# **是 Python 的字典解包语法,将字典的键值对展开为函数的关键字参数
outputs = model(**batch)
# 输出一个 batch 的平均损失值
loss = outputs.loss
# 反向传播,计算梯度(累加到现有梯度)
loss.backward()
# 用梯度更新参数
optimizer.step()
# 更新学习率
lr_scheduler.step()
# 清零梯度,准备下一轮
optimizer.zero_grad()
# 训练完一个 batch 更新进度条
progress_bar.update(1)
# 上述训练循环不会告诉我们任何关于模型目前的状态,我们需要为此添加一个评估循环
# 评估循环
# evaluate 库,这是 Hugging Face 开发的评估库(原名 datasets.metrics),专门用于计算机器学习模型的各种评估指标。
import evaluate, torch
# "glue" │ 基准测试名称(GLUE 是一个包含多个 NLP 任务的评测基准)
# "mrpc" │ 具体任务名称(MRPC = Microsoft Research Paraphrase Corpus)
# 加载 GLUE 基准测试中的 MRPC 任务的评估指标。MRPC 是微软研究院的同义改写语料库,用于判断两个句子是否表达相同意思。指标会计算准确率和 F1 分数。
metric = evaluate.load("glue", "mrpc")
# 设置模型的训练模式为评估模式
model.eval()
for batch in eval_dataloader:
# batch = {k: v.to(device) for k, v in batch.items()}
# 遍历数据批次。torch.no_grad() 禁用梯度计算,节省内存并加速推理。
with torch.no_grad():
outputs = model(**batch)
logits = outputs.logits
# 提取模型的原始输出,然后用 argmax 取最后一维的最大值索引,得到预测类别(0 或 1)。【dim=-1 就是对每一行找最大值的列索引】
predictions = torch.argmax(logits, dim=-1)
# 将预测结果和真实标签累积到指标对象中。
metric.add_batch(predictions=predictions, references=batch["labels"])
# 打印评估结果
print(metric.compute())
"""
{'accuracy': 0.875, 'f1': 0.9122203098106713}
"""
# 微调模型保存
model.save_pretrained("./test-trainer")
tokenizer.save_pretrained("./test-trainer")
# 显式清理资源,避免 Python 退出时崩溃
import gc
del model, optimizer, lr_scheduler
del train_dataloader, eval_dataloader
del tokenized_datasets, raw_datasets
gc.collect()
gc.collect()
print("训练完成,资源已清理")
"""
# 评估示例数据:
labels = torch.tensor([1, 0])
完整示例:
batch = {
"input_ids": torch.tensor([
[101, 2023, 2003, ...],
# 样本0的输入token
[101, 2054, 2856, ...]
# 样本1的输入token
]),
"attention_mask": torch.tensor([
[1, 1, 1, ...],
[1, 1, 0, ...]
]),
"labels": torch.tensor([1, 0])
}
# ↑ ↑
# │ └── 样本1的真实标签:0(不是同义改写)
# └───── 样本0的真实标签:1(是同义改写)
predictions和labels的对应关系?
predictions = torch.tensor([1, 0]) # 模型预测
labels = torch.tensor([1, 0]) # 真实标签
# 对比:
# 样本0:预测1,真实1 → 正确 ✓
# 样本1:预测0,真实0 → 正确 ✓
# 这批样本全部预测正确
另一个例子(有错误预测):
predictions = torch.tensor([1, 0, 1, 0]) # 模型预测
labels = torch.tensor([1, 1, 1, 0]) # 真实标签
# 对比:
# 样本0:预测1,真实1 → 正确 ✓
# 样本1:预测0,真实1 → 错误 ✗
# 样本2:预测1,真实1 → 正确 ✓
# 样本3:预测0,真实0 → 正确 ✓
MRPC
标签含义:
- 0 = 两个句子不是同义改写
- 1 = 两个句子是同义改写
"""4.使用Accelerate 加速你的训练循环
- 之前定义的训练循环在单个 CPU 或者 GPU 上运行良好,通过使用 Accelerate 库,只需进行一些调整,我们就可以在多个 GPU 和 TPU 上启用分布式训练
- 手动训练循环的完整代码
Python
from torch.optim import AdamW
from transformers import AutoModelForSequenceClassification, get_scheduler
model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)
optimizer = AdamW(model.parameters(), lr=3e-5)
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
model.to(device)
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,
)
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)- 加上 accelerate库之后的代码(-表示要删除的代码,+表示新增的代码)
Python
+ from accelerate import Accelerator
from transformers import AutoModelForSequenceClassification, get_scheduler
+ accelerator = Accelerator()
model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)
optimizer = AdamW(model.parameters(), lr=3e-5)
- device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
- model.to(device)
+ 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
)
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()
+ accelerator.backward(loss)
optimizer.step()
lr_scheduler.step()
optimizer.zero_grad()
progress_bar.update(1)5.分享你的模型和分词器
使用预训练模型
模型中心 hub 选择合适的模型很简单,只需几行代码即可在任何下游库中使用它
从 hub上拉模型下来使用
Python
# 导入 pipeline 函数,这是 Hugging Face 提供的高层 API,用于快速加载预训练模型
from transformers import pipeline
# - 创建一个"填空"类型的 pipeline
# - model="camembert-base" 指定使用 CamemBERT 模型,这是一个专门针对法语训练的 BERT 模型
camembert_fill_mask = pipeline("fill-mask", model="camembert-base")
# 输入法语句子,其中 <mask> 是需要预测的词位
results = camembert_fill_mask("Le camembert est <mask> :)")
# 输出预测结果,通常返回一个列表,包含多个候选词及其概率
print(results)
"""
[{'score': 0.49091655015945435, 'token': 7200, 'token_str': 'délicieux', 'sequence': 'Le camembert est délicieux :)'},
{'score': 0.10557064414024353, 'token': 2183, 'token_str': 'excellent', 'sequence': 'Le camembert est excellent :)'},
{'score': 0.03453359007835388, 'token': 26202, 'token_str': 'succulent', 'sequence': 'Le camembert est succulent :)'},
{'score': 0.03303172439336777, 'token': 528, 'token_str': 'meilleur', 'sequence': 'Le camembert est meilleur :)'},
{'score': 0.030076900497078896, 'token': 1654, 'token_str': 'parfait', 'sequence': 'Le camembert est parfait :)'}]
"""
# 还可以使用模型架构实例化 checkpoint,建议使用 Auto*类,因为它们在设计时不依赖模型架构
from transformers import AutoTokenizer, AutoModelForMaskedLM
tokenizer = AutoTokenizer.from_pretrained("camembert-base")
model = AutoModelForMaskedLM.from_pretrained("camembert-base")- 分享预训练的模型
- huggingface客户端方式推送到 hub 仓库
bash
# Install the Hugging Face CLI
brew install hf
# (optional) Login with your Hugging Face credentials
hf auth login
# Push your model files
hf upload <your-account>/<your repository name> .- python代码方式推送
Python
from huggingface_hub import login, upload_folder
# (optional) Login with your Hugging Face credentials
login('<token-with-write-access>')
# Push your model files
upload_folder(folder_path=".", repo_id="<your-account>/<your repository name>", repo_type="model")- git 方式推送
bash
# Make sure git-xet is installed (https://hf.co/docs/hub/git-xet)
git xet install
git clone https://huggingface.co/<your-account>/<your repository name>
# You'll be prompted for your HF credentials
git push- 构建模型卡片
- hub网页端进行编辑
6.参考文档
- huggingface上著名的 llm学习课程,中文版链接:https://huggingface.co/learn/llm-course/zh-CN/chapter0/1