其实我们之前有用这种方法,就是我们训出来的翻译小模型不懂latte这个词,也不懂点咖啡。so,我们准备了一批语料来强训它,让它记住。

大语言模型(LLM)微调(Fine-tuning)流水线

利用 Hugging Face 的生态(transformersdatasets),走完了从“准备数据 -> 处理数据 -> 配置参数 -> 启动训练 -> 保存模型”的全过程。它的核心目的是:拿一个已经有基础能力的预训练模型(GPT-2),用大量的中文维基百科数据让它继续学习(Next-Token Prediction),从而掌握(或增强)生成连贯中文文本的能力。

load_dataset

load_dataset 是 Hugging Face datasets 库中最核心、也是使用频率最高的“魔法函数”。你可以把它理解为大模型时代的数据集版 git clonepip install

dataset = load_dataset("wikimedia/wikipedia", "20231101.zh", split="train", cache_dir="./hf_cache")

一、 它是怎么从 huggingface.co 拉取数据的?

当你执行 load_dataset("wikimedia/wikipedia", ...) 时,底层其实经历了一场精密的协同:

  1. API 寻址:函数首先会向 Hugging Face Hub 的 API 发送请求,查找名为 wikimedia/wikipedia 的仓库。
  2. 下载脚本或元数据:它不仅会查找数据文件,还会寻找仓库里的 dataset_script.py(数据集处理脚本)或者直接寻找 Parquet / JSONL 格式的底层数据文件。
  3. 本地缓存(核心机制):下载的数据会存入你指定的 cache_dir(如果没有指定,默认存放在 ~/.cache/huggingface/datasets/)。
  4. Arrow 内存映射(黑科技):Hugging Face 极其聪明地使用了 Apache Arrow 格式来将数据序列化到本地硬盘,并通过内存映射(Memory-Mapping)技术读取。
    • 这意味着什么? 即使你加载了一个 100GB 的维基百科数据集,你的电脑内存也不会爆炸!因为它不会把数据全塞进 RAM 里,而是放在硬盘上,用到哪一行才读取哪一行,速度极快。

二、 核心参数全解析

以代码为例:

dataset = load_dataset(
    path="wikimedia/wikipedia", 
    name="20231101.zh", 
    split="train", 
    cache_dir="./hf_cache"
)
参数名类型详细解释
pathstr数据集的路径或 ID。格式通常是 组织名/数据集名(如 "wikimedia/wikipedia"),也可以直接是本地文件夹路径。
namestr数据集的具体配置(子集)。很多大型数据集包含多种语言或多个子任务。比如维基百科有几百种语言,"20231101.zh" 就是告诉它:“我只要 2023 年 11 月 1 日版本的中文数据”。
splitstr数据切分。指定你要加载 traintest 还是 validation高级用法: 你还可以切片,比如 split="train[:10%]",这样它只会拉取前 10% 的数据,非常适合本地快速调试!
cache_dirstr本地缓存路径。强烈建议指定一个空间充足的磁盘目录。下载过一次后,下次运行代码会瞬间完成(直接读缓存)。
streamingbool(补充参数) 默认为 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:

  1. 按行访问(像列表):

    Python

    dataset[0] 
    # 返回:{'sentence': 'hide new secretions from the parental units ', 'label': 0, 'idx': 0}
    
  2. 按列访问(像字典):

    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(表头)也被扩充了!

执行完之后,你的表头里会多出三列

  1. 'input_ids':被分词器(Tokenizer)切好并转成数字的 Token 列表。
  2. 'attention_mask':用来告诉模型哪里是真实文字(1),哪里是凑数的填充符(0)。
  3. 'labels':你手动复制出来的答案列,用来计算模型的预测误差(Loss)。

所以处理完后,你真实的 features 其实变成了:['id', 'url', 'title', 'text', 'input_ids', 'attention_mask', 'labels']

为什么 features (列名)极其重要?

在 Hugging Face 的生态里,底层的 PyTorch Trainer 是个“睁眼瞎”。当它把数据喂给 GPT-2 模型时,它只认特定名字的列

它会自动去你的 features 里寻找名为 input_idsattention_masklabels 的列,并把这些列的矩阵直接扔进 GPU 里的模型层。而那些它不认识的列(比如 'url''title''text'),在送入模型前会被框架自动忽略并剔除,以免引起代码报错。

这也是为什么在编写 tokenize_function 的时候,必须要老老实实地生成 input_idslabels 的原因——因为这是你和模型之间约定的“暗号”。

三种训练集的区别:

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"