第四章:数据集准备——从原始数据到可训练样本

做LoRA微调,说白了就是让模型学会你给它的新知识。那新知识从哪来?当然是从数据里来。我见过太多同学一上来就调参数、改配置,结果跑起来才发现数据格式不对,白白浪费半天时间。嗯,这一章我们就来把数据准备这块彻底讲透。

4.1 数据集格式要求:别让模型“看不懂”

LoRA微调的数据格式,其实没那么玄乎。主流框架(比如Hugging Face的Transformers)通常支持两种格式:

  • JSON格式:每条数据一个字典,字段名自己定
  • CSV格式:表格形式,列名对应字段

我个人习惯用JSON,因为结构清晰,嵌套起来也方便。举个例子,如果你在做对话任务,数据大概长这样:

{
  "instruction": "请解释什么是LoRA",
  "input": "",
  "output": "LoRA(Low-Rank Adaptation)是一种高效的模型微调方法..."
}

这里要注意一个坑:字段名必须和你的训练脚本里定义的一致。我曾经有个项目,数据里写的是“question”和“answer”,但脚本里读的是“instruction”和“output”,结果跑了一轮才发现——嗯,白跑了。

⚠️ 避坑指南:我曾经因为字段名大小写不一致("Output" vs "output"),排查了整整两个小时。建议统一用小写+下划线命名。

4.2 加载本地数据集:从硬盘到内存

数据准备好了,怎么喂给模型?最常用的工具是Hugging Face的datasets库。你想想看,它就像一个数据管家,帮你处理各种格式。

加载本地JSON文件的代码很简单:

from datasets import load_dataset

dataset = load_dataset("json", data_files="my_data.json")
print(dataset)
# 输出:DatasetDict({"train": Dataset({...})})

如果你有多个文件,可以这样:

dataset = load_dataset("json", data_files={
    "train": "train.json",
    "test": "test.json"
})

我在项目中遇到过一个问题:数据文件太大,内存直接爆了。这时候可以用streaming=True参数,让数据按需加载,不一次性全读进内存。

💡 小技巧:如果数据量超过1GB,建议用streaming=True。虽然训练时会慢一点,但至少不会OOM。

4.3 数据预处理Pipeline:把“生肉”变成“熟饭”

原始数据通常不能直接喂给模型。你需要做一系列预处理,比如分词、截断、填充。我习惯把这套流程叫做“数据预处理Pipeline”。

一个典型的Pipeline包含这几步:

  1. 分词:把文本转成token ID
  2. 截断:超过最大长度的部分切掉
  3. 填充:不足长度的补上padding token
  4. 添加特殊标记:比如[CLS]、[SEP]等

代码实现大概是这样的:

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese")

def preprocess_function(examples):
    # 拼接指令和输入
    inputs = [f"{inst} {inp}" for inst, inp in zip(examples["instruction"], examples["input"])]
    # 分词
    model_inputs = tokenizer(
        inputs,
        max_length=512,
        truncation=True,
        padding="max_length"
    )
    # 标签就是输出
    model_inputs["labels"] = tokenizer(
        examples["output"],
        max_length=512,
        truncation=True,
        padding="max_length"
    )["input_ids"]
    return model_inputs

# 应用到整个数据集
tokenized_dataset = dataset.map(preprocess_function, batched=True)

这里有个细节:batched=True表示批量处理,速度会快很多。我第一次写的时候没加这个参数,处理10万条数据跑了快半小时——加了之后,3分钟搞定。

🔑 核心要点:预处理函数必须返回一个字典,key是模型能识别的字段名(如input_ids、attention_mask、labels)。

4.4 Train/Test拆分:别让模型“作弊”

数据预处理完了,接下来要划分训练集和测试集。为什么?因为你需要一个“考官”来检验模型学得怎么样。如果训练和测试用同一批数据,那模型就是“开卷考试”,分数再高也没意义。

我常用的拆分比例是8:2或9:1。代码很简单:

# 方法一:直接用datasets的split方法
split_dataset = tokenized_dataset["train"].train_test_split(test_size=0.2)
train_dataset = split_dataset["train"]
test_dataset = split_dataset["test"]

# 方法二:手动指定随机种子,保证可复现
split_dataset = tokenized_dataset["train"].train_test_split(
    test_size=0.2,
    seed=42
)

你可能会问:为什么要固定随机种子?因为有一次我调试代码,每次跑出来的结果都不一样,排查了半天才发现是数据拆分不一致导致的。嗯,从那以后我每次都会设seed=42

⚠️ 注意:如果你的数据有类别标签,记得用stratify_by_column参数做分层抽样,保证训练集和测试集的类别分布一致。

4.5 数据加载器构建:让训练“流水线”跑起来

最后一步,把处理好的数据包装成PyTorch的DataLoader。说白了,就是给模型建一个“传送带”,一批一批地送数据。

from torch.utils.data import DataLoader
from transformers import DataCollatorForSeq2Seq

# 数据整理器:自动处理padding和batch
data_collator = DataCollatorForSeq2Seq(
    tokenizer=tokenizer,
    model=model,
    padding=True
)

# 构建DataLoader
train_dataloader = DataLoader(
    train_dataset,
    batch_size=8,          # 每批8条
    shuffle=True,          # 打乱顺序
    collate_fn=data_collator
)

test_dataloader = DataLoader(
    test_dataset,
    batch_size=8,
    shuffle=False          # 测试集不需要打乱
)

这里batch_size的选择很有讲究。我刚开始做的时候,觉得越大越好,直接设了64,结果显存爆了。后来慢慢试,发现8~16对于大多数LoRA任务来说是个比较稳妥的范围。

💡 经验之谈:如果显存不够,可以试试梯度累积(gradient accumulation)。比如batch_size=4,梯度累积步数=2,效果相当于batch_size=8,但显存占用少一半。

知识体系总览

说了这么多,我们来画个图,把整个数据准备的流程串起来:

LoRA微调数据准备全流程 原始数据 JSON/CSV格式 加载数据集 load_dataset() 预处理Pipeline 分词/截断/填充 Train/Test拆分 8:2比例 数据加载器构建 DataLoader + collator ✅ 可训练数据 整个流程环环相扣,每一步都影响最终效果

你看,从原始数据到可训练样本,一共就五步。每一步都不难,但每一步都有坑。我刚开始做LoRA微调的时候,就是在数据准备这块栽了好几次跟头。后来总结出一句话:数据准备花的时间,会在训练阶段加倍还给你

📌 本章小结
  • 数据格式:JSON最灵活,字段名要统一
  • 加载数据:用datasets库,大文件用streaming
  • 预处理:分词+截断+填充,三步走
  • 拆分:8:2比例,固定随机种子
  • DataLoader:batch_size从8开始试

专注资料整理