其实我们之前有用这种方法,就是我们训出来的翻译小模型不懂latte这个词,也不懂点咖啡。so,我们准备了一批语料来强训它,让它记住。
大语言模型(LLM)微调(Fine-tuning)流水线:
利用 Hugging Face 的生态(transformers 和 datasets),走完了从“准备数据 -> 处理数据 -> 配置参数 -> 启动训练 -> 保存模型”的全过程。它的核心目的是:拿一个已经有基础能力的预训练模型(GPT-2),用大量的中文维基百科数据让它继续学习(Next-Token Prediction),从而掌握(或增强)生成连贯中文文本的能力。
load_dataset
load_dataset 是 Hugging Face datasets 库中最核心、也是使用频率最高的“魔法函数”。你可以把它理解为大模型时代的数据集版 git clone 或 pip install
dataset = load_dataset("wikimedia/wikipedia", "20231101.zh", split="train", cache_dir="./hf_cache")
一、 它是怎么从 huggingface.co 拉取数据的?
当你执行 load_dataset("wikimedia/wikipedia", ...) 时,底层其实经历了一场精密的协同:
- API 寻址:函数首先会向 Hugging Face Hub 的 API 发送请求,查找名为
wikimedia/wikipedia的仓库。 - 下载脚本或元数据:它不仅会查找数据文件,还会寻找仓库里的
dataset_script.py(数据集处理脚本)或者直接寻找 Parquet / JSONL 格式的底层数据文件。 - 本地缓存(核心机制):下载的数据会存入你指定的
cache_dir(如果没有指定,默认存放在~/.cache/huggingface/datasets/)。 - Arrow 内存映射(黑科技):Hugging Face 极其聪明地使用了 Apache Arrow 格式来将数据序列化到本地硬盘,并通过内存映射(Memory-Mapping)技术读取。
- 这意味着什么? 即使你加载了一个 100GB 的维基百科数据集,你的电脑内存也不会爆炸!因为它不会把数据全塞进 RAM 里,而是放在硬盘上,用到哪一行才读取哪一行,速度极快。
二、 核心参数全解析
以代码为例:
dataset = load_dataset(
path="wikimedia/wikipedia",
name="20231101.zh",
split="train",
cache_dir="./hf_cache"
)
| 参数名 | 类型 | 详细解释 |
|---|---|---|
path | str | 数据集的路径或 ID。格式通常是 组织名/数据集名(如 "wikimedia/wikipedia"),也可以直接是本地文件夹路径。 |
name | str | 数据集的具体配置(子集)。很多大型数据集包含多种语言或多个子任务。比如维基百科有几百种语言,"20231101.zh" 就是告诉它:“我只要 2023 年 11 月 1 日版本的中文数据”。 |
split | str | 数据切分。指定你要加载 train、test 还是 validation。高级用法: 你还可以切片,比如 split="train[:10%]",这样它只会拉取前 10% 的数据,非常适合本地快速调试! |
cache_dir | str | 本地缓存路径。强烈建议指定一个空间充足的磁盘目录。下载过一次后,下次运行代码会瞬间完成(直接读缓存)。 |
streaming | bool | (补充参数) 默认为 False。如果你的硬盘不够大(比如只有 50G,但数据有 500G),设为 True。它会变成“流式传输”,边下载边训练,完全不占本地硬盘。 |
三、 拉回来的数据结构到底是什么样的?
拉回来的结构完全取决于你是否使用了 split 参数。
结构 A:不加 split 参数(返回 DatasetDict)
如果你这样写:dataset = load_dataset("glue", "sst2")
它返回的是一个 DatasetDict(数据集字典),包含所有的数据分支。结构如下:
Python
DatasetDict({
'train': Dataset({
features: ['sentence', 'label', 'idx'],
num_rows: 67349
}),
'validation': Dataset({
features: ['sentence', 'label', 'idx'],
num_rows: 872
}),
'test': Dataset({
features: ['sentence', 'label', 'idx'],
num_rows: 1821
})
})
此时你必须通过 dataset['train'] 才能取到真正的训练集。
结构 B:加了 split 参数(返回单个 Dataset)
如果你这样写:dataset = load_dataset("glue", "sst2", split="train")
它会跳过字典层级,直接返回一个 Dataset 对象。结构如下:
Python
Dataset({
features: ['sentence', 'label', 'idx'],
num_rows: 67349
})
在这个结构下,数据表现得既像列表,又像字典,本质上是一张列式存储的数据表,类似exel:
按行访问(像列表):
Python
dataset[0] # 返回:{'sentence': 'hide new secretions from the parental units ', 'label': 0, 'idx': 0}按列访问(像字典):
Python
dataset['sentence'][:3] # 返回前三句话的列表:['hide new...', 'contains no wit...', 'that loves its...']
features 就是这张数据表的“表头(列名)”和“数据类型说明书”。
你看到的 features: ['sentence', 'label', 'idx'] 是为了举例,随便借用的一个情感分类数据集(SST-2)的表头。我们可以把它翻译成 Excel 表格来看:
| idx (索引号) | sentence (句子内容) | label (标签/答案) |
|---|---|---|
| 0 | “这部电影太棒了!” | 1 (代表积极) |
| 1 | “剧本写得一塌糊涂。” | 0 (代表消极) |
回到维基百科大模型训练
对于要跑的中文维基百科数据集 ("wikimedia/wikipedia"),它的原始 features(表头)其实长这样:
'id':文章的唯一编号'url':文章的维基百科网址'title':文章标题(比如“人工智能”)'text':文章的全部正文内容(这是你训练 GPT-2 最需要的一列)
map 函数到底施了什么魔法?
之后代码里写了这一步:
tokenized_datasets = dataset.map(tokenize_function, batched=True)
当你执行完这个 map 映射后,不仅仅是里面的文字被转换成了数字,整个数据集的 features(表头)也被扩充了!
执行完之后,你的表头里会多出三列:
'input_ids':被分词器(Tokenizer)切好并转成数字的 Token 列表。'attention_mask':用来告诉模型哪里是真实文字(1),哪里是凑数的填充符(0)。'labels':你手动复制出来的答案列,用来计算模型的预测误差(Loss)。
所以处理完后,你真实的 features 其实变成了:['id', 'url', 'title', 'text', 'input_ids', 'attention_mask', 'labels']。
为什么 features (列名)极其重要?
在 Hugging Face 的生态里,底层的 PyTorch Trainer 是个“睁眼瞎”。当它把数据喂给 GPT-2 模型时,它只认特定名字的列。
它会自动去你的 features 里寻找名为 input_ids、attention_mask 和 labels 的列,并把这些列的矩阵直接扔进 GPU 里的模型层。而那些它不认识的列(比如 'url'、'title'、'text'),在送入模型前会被框架自动忽略并剔除,以免引起代码报错。
这也是为什么在编写 tokenize_function 的时候,必须要老老实实地生成 input_ids 和 labels 的原因——因为这是你和模型之间约定的“暗号”。
三种训练集的区别:
1. Train (训练集) = 平时的课后作业 📚
- 用途:用来给模型“学习”和“更新大脑记忆”的。
- 过程:学生(模型)做作业,做完一对标准答案(计算 Loss),发现做错了,就在脑子里调整自己的解题思路(更新模型权重参数)。
- 特点:数据量最大,通常占总数据的 80% 左右。模型会反复看这些题(Epoch),直到熟练掌握。
2. Validation (验证集) = 周末的模拟考试 📝
- 用途:用来测试学生当下的真实水平,并帮助老师(你)调整复习策略(调整超参数)。
- 过程:每学完一章(跑完一定步数的 Train),就让学生做一套没见过的模拟卷。做完后核对分数,但不讲解错题。也就是说,模型绝对不能通过验证集来更新权重参数!
- 为什么需要它? 如果学生平时作业全对,但一到模拟考就考砸,说明他只是“死记硬背”了作业答案(这在机器学习里叫过拟合 Overfitting)。这时候你作为老师,就知道要调整策略了(比如让他停止复习,或者加大惩罚力度
weight_decay)。在代码里,eval_strategy='epoch'就是让模型每学完一轮,就用 Validation 测一下当前水平。
3. Test (测试集) = 最终的高考 (或者期末大考) 🎓
- 用途:用来给模型做最终的实力定级,一锤定音。
- 过程:真正的高考题,绝对保密。在模型完全训练好、并且通过验证集选出了你认为“最佳”的版本后,才能拿测试集出来考它。
- 特点:测出来的成绩,就是你要写进论文、或者汇报给老板的最终成绩。考完之后,无论成绩好坏,都不能再回去修改模型了。如果考砸了,说明你的整个训练流程有问题,必须推倒重来。
💡 总结与实战对应
| 概念 | 拟人化比喻 | 在模型训练中的实际作用 | 是否更新模型权重? |
|---|---|---|---|
| Train | 平时作业 | 真正驱动模型学习、改变参数的数据。 | 是 (Yes) |
| Validation | 模拟考 | 监控训练过程,防止死记硬背,调整超参数。 | 否 (No) |
| Test | 最终高考 | 训练全部结束后,客观评估模型的最终能力。 | 否 (No) |
在之后的 GPT-2 代码里:
维基百科默认把所有数据都打包成了一坨(当作 Train)。你之前加的那句 train_test_split(test_size=0.05),其实就是从海量的“平时作业”里,强行抽出了 5% 封存起来,当做了“模拟考卷”(也就是代码里的 eval_dataset),这样你在训练时,系统就能帮你监控 GPT-2 是不是真的学会了说中文!
正式开始吧
那加载数据集的代码:
from datasets import load_dataset
from transformers import AutoTokenizer
# Load dataset
# dataset = load_dataset('wikitext', 'wikitext-2-raw-v1')
dataset = load_dataset("wikimedia/wikipedia", "20231101.zh", split="train", cache_dir="./hf_cache")
# Load tokenizer
tokenizer = AutoTokenizer.from_pretrained('gpt2')
tokenizer.pad_token = tokenizer.eos_token
# Tokenize the dataset
def tokenize_function(examples):
inputs = tokenizer(examples['text'], truncation=True, padding='max_length', max_length=128)
inputs['labels'] = inputs['input_ids'].copy()
return inputs
tokenized_datasets = dataset.map(tokenize_function, batched=True)
加载数据集并训练的代码
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, Trainer, TrainingArguments
from datasets import load_dataset
# Check if GPU is available
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")
# Load dataset
dataset = load_dataset("wikimedia/wikipedia", "20231101.zh", split="train", cache_dir="./hf_cache")
dataset = dataset.train_test_split(test_size=0.05)
# Load tokenizer and model
tokenizer = AutoTokenizer.from_pretrained('gpt2')
model = AutoModelForCausalLM.from_pretrained('gpt2').to(device)
# Set the EOS token as the padding token
tokenizer.pad_token = tokenizer.eos_token
# Tokenize the dataset
def tokenize_function(examples):
inputs = tokenizer(examples['text'], truncation=True, padding='max_length', max_length=128)
inputs['labels'] = inputs['input_ids'].copy()
return inputs
tokenized_datasets = dataset.map(tokenize_function, batched=True)
# Define training arguments
training_args = TrainingArguments(
output_dir='./results',
eval_strategy='epoch',
num_train_epochs=1,
per_device_train_batch_size=4,
per_device_eval_batch_size=4,
warmup_steps=500,
weight_decay=0.01,
logging_dir='./logs'
)
# Initialize Trainer
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_datasets['train'],
# eval_dataset=tokenized_datasets['validation'],
eval_dataset=tokenized_datasets['test']
)
# Train the model
trainer.train()
# 自动读取之前的训练进度
#trainer.train(resume_from_checkpoint=True)
# 指定之前的进度
#trainer.train(resume_from_checkpoint='./results/checkpoint-5000')
# save the model and tokenizer explicitly
model_output_dir = './model'
model.save_pretrained(model_output_dir)
tokenizer.save_pretrained(model_output_dir)
实际验收:
import argparse
from transformers import AutoTokenizer, AutoModelForCausalLM
def get_model_parameters(model):
total_params = sum(p.numel() for p in model.parameters())
return total_params
def main(input_text):
# Load the tokenizer and model from the saved directory
model_path = './results/model'
tokenizer = AutoTokenizer.from_pretrained(model_path)
model = AutoModelForCausalLM.from_pretrained(model_path)
# Calculate the Number of Parameters in the model being used for inference
total_params = get_model_parameters(model)
print(f"Total number of paramerers: {total_params}")
# Prepare the input text you want to generate predictions for
inputs = tokenizer(input_text, return_tensors='pt')
# Generate Text
outputs = model.generate(**inputs, max_length=50, num_return_sequences=1)
# Decode the generated text
generated_text = tokenizer.decode(outputs[0], skip_special_tokens=True)
print(generated_text)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Generate text using a trained model.")
parser.add_argument('input_text', type=str, help="The input text to generate from.")
args = parser.parse_args()
main(args.input_text)
具体用法:
python inference.py "Can you tell me a story"