承接上一篇内容:我们拆解了 MiniMind 底层核心架构,吃透了 RMSNorm、SwiGLU、RoPE 三大组件的工程实现与优化逻辑。现在,终于轮到 LLM 最关键的「粮草」------ 数据工程。
行业里流传一句话:「LLM 效果三分靠模型,七分靠数据」。对 MiniMind 这种超小模型来说,数据的质量、格式、适配性更是决定性因素 ------26M 参数的模型,根本经不起低质量数据的「误导」。

本篇聚焦「数据全流程」,纯实操视角 + 源码落地:
- 亲手训练 MiniMind 专属 Tokenizer,理解 6400 词表的设计逻辑;
- 拆解预训练 / SFT/DPO 三类核心数据集的格式、清洗规则、加载流程;
- 解决数据下载慢、格式错误、适配失败等新手高频坑;
- 教你适配自定义数据集,为后续训练自己的垂域小模型铺路。
建议打开 MiniMind 源码
trainer/train_tokenizer.py和utils/data_utils.py对照阅读,边看边实操!开源项目地址:https://github.com/jingyaogong/minimind
一、先明确:数据工程对小模型的核心意义
小模型和大模型的「数据需求」完全不同:
- 大模型:靠海量数据「堆知识」,哪怕有部分噪声,也能靠参数规模稀释;
- 小模型:参数有限,必须「精准投喂」------ 高质量、高相关性、格式统一的数据,才能让每一个参数都发挥价值。
MiniMind 的数据工程核心目标:用最少的数据,实现最优的效果,具体体现在三点:
- Tokenizer 轻量化:6400 词表,避免词嵌入层占用过多参数;
- 数据集精简:预训练 / SFT/DPO 数据均经过二次清洗,去重去噪;
- 格式统一:所有数据统一为 JSONL 格式,降低训练脚本复杂度。
二、核心部分 1:Tokenizer 训练 ------ 给小模型造一本「专属词典」
2.1 Tokenizer 是什么?(极简理解)
Tokenizer 本质是「语言词典 + 编码规则」:
- 把自然语言(如「你好,MiniMind」)拆分成模型能理解的「Token 序列」(如
[1024, 3, 5678]); - 训练时,模型学习的是 Token 之间的关联,而非直接学习文字;
- 词典(词表)的大小、拆分规则,直接影响模型的编码效率和参数量。
2.2 MiniMind 为什么不用现成 Tokenizer?偏要自定义训练?
市面上主流 Tokenizer(如 Llama3、Qwen2)的词表都在 32k 以上,对小模型来说有两个致命问题:
- 参数量浪费:词嵌入层参数 = 词表大小 × 嵌入维度(如 32k×512=16.38M),占 MiniMind2-Small 总参数(26M)的 63%,导致模型「头重脚轻」;
- 编码效率低:大词表适合多语言、复杂文本,MiniMind 聚焦中文对话,无需冗余词汇,小词表编码更快、更精准。
MiniMind 自定义 Tokenizer 的核心设计:
- 词表大小:6400(仅为 Llama3 的 1/20);
- 训练算法:SentencePiece(Unigram 模型),适合中文短文本拆分;
- 核心目标:平衡「编码压缩率」和「模型参数量」。
2.3 亲手训练 MiniMind 风格 Tokenizer(实操步骤)
步骤 1:准备训练数据
训练 Tokenizer 不需要海量数据,100MB-1GB 高质量文本即可。推荐用 MiniMind 预训练数据的子集:
# 从预训练数据中抽取前 10 万条文本(足够训练小词表)
head -n 100000 ./dataset/pretrain_hq.jsonl > ./dataset/tokenizer_train_data.jsonl
步骤 2:运行 Tokenizer 训练脚本(源码解析)
MiniMind 训练 Tokenizer 的核心代码在 trainer/train_tokenizer.py,简化后关键逻辑如下:
python
import sentencepiece as spm
import json
def train_minimind_tokenizer():
# 1. 读取训练数据(提取 JSONL 中的 text 字段)
train_text = []
with open("./dataset/tokenizer_train_data.jsonl", "r", encoding="utf-8") as f:
for line in f:
data = json.loads(line)
train_text.append(data["text"])
# 2. 写入临时文本文件(SentencePiece 要求输入为纯文本)
with open("./dataset/tokenizer_temp.txt", "w", encoding="utf-8") as f:
f.write("\n".join(train_text))
# 3. 配置 SentencePiece 训练参数
spm.SentencePieceTrainer.Train(
input="./dataset/tokenizer_temp.txt",
model_prefix="minimind_tokenizer", # 输出模型前缀
vocab_size=6400, # 词表大小(核心参数)
model_type="unigram", # 模型类型(Unigram 适合中文)
character_coverage=0.9995, # 字符覆盖度(保证生僻字被包含)
max_sentence_length=1024, # 最大句子长度
pad_id=0, # PAD token ID
bos_id=1, # 句子开始 ID(MiniMind 用 <|im_start|>)
eos_id=2, # 句子结束 ID(MiniMind 用 <|im_end|>)
unk_id=3, # 未知词 ID
user_defined_symbols=["<|im_start|>", "<|im_end|>", "<tool_call>", ""] # 自定义特殊 Token(适配对话/工具调用)
)
print("Tokenizer 训练完成!生成 minimind_tokenizer.model 和 minimind_tokenizer.vocab")
if __name__ == "__main__":
train_minimind_tokenizer()
步骤 3:执行训练命令
# 进入 trainer 目录
cd trainer
# 运行训练脚本
python train_tokenizer.py
步骤 4:验证 Tokenizer 效果
训练完成后,会生成两个文件:minimind_tokenizer.model(模型文件)和 minimind_tokenizer.vocab(词表文件)。用以下代码测试编码效果:
import sentencepiece as spm
# 加载 Tokenizer
tokenizer = spm.SentencePieceProcessor()
tokenizer.Load("minimind_tokenizer.model")
# 测试编码
text = "你好!我是 MiniMind,2 小时就能训练完成。"
tokens = tokenizer.EncodeAsIds(text)
tokens_text = tokenizer.EncodeAsPieces(text)
print("Token ID 序列:", tokens)
print("Token 文本序列:", tokens_text)
print("原始文本:", tokenizer.DecodeIds(tokens))
Token ID 序列: [1024, 3, 5678, 123, 456, 789, ...]
Token 文本序列: ['▁你好', '!', '▁我是', '▁Mini', 'Mind', ',', ...]
原始文本: 你好!我是 MiniMind,2 小时就能训练完成。
2.4 Tokenizer 优化关键技巧(小模型专属)
- 词表大小不能太小:低于 4000 会导致「字符级拆分」(如「 MiniMind」拆成「M」「i」「n」),编码效率极低;
- 保留高频特殊符号 :对话场景必须加入
<|im_start|><|im_end|>,工具调用场景加入<tool_call>; - 中文优先:训练数据以中文为主,避免英文词汇占用过多词表空间(MiniMind 中文 Token 占比 85%+);
- 避免冗余词汇:过滤低频词(出现次数 <5),减少词表体积。
三、核心部分 2:全数据集处理 ------ 预训练 / SFT/DPO 三阶段适配
MiniMind 支持三类核心数据集,分别对应训练的不同阶段,格式和处理逻辑各有侧重。我们逐一拆解「格式要求 + 清洗规则 + 加载代码」。
3.1 预训练数据:让模型「学知识」
3.1.1 核心作用
预训练是无监督学习,目标是让模型学习语言规律、常识、基础概念(比如「杭州是浙江的省会」「1+1=2」),相当于给模型「喂墨水」。
3.1.2 数据格式(JSONL)
MiniMind 预训练数据统一为单字段 JSONL(每行一个样本):
{"text": "如何摆脱拖延症?治愈拖延症并不容易,但以下建议可能有所帮助:1. 分解任务;2. 设定截止时间;3. 减少干扰。"}
{"text": "光合作用是植物利用光能将二氧化碳和水转化为有机物,并释放氧气的过程。"}
3.1.3 数据清洗核心规则(MiniMind 实战标准)
预训练数据质量直接决定模型「知识储备」,MiniMind 对 pretrain_hq.jsonl 的清洗步骤:
- 去重:基于文本哈希去重,避免重复内容导致模型过拟合;
- 去噪声 :过滤含乱码、特殊符号(如
@#$%^&*)、广告的文本; - 长度筛选:保留字符数 <512 的文本(小模型算力有限,长文本训练效率低);
- 质量过滤:过滤无意义文本(如「啊啊啊啊啊」「123456」);
- 编码统一:全部转为 UTF-8,避免中文乱码。
3.1.4 加载代码(utils/data_utils.py 核心片段)
python
import json
from torch.utils.data import Dataset
class PretrainDataset(Dataset):
def __init__(self, data_path, tokenizer, max_seq_len=512):
self.data = []
# 读取 JSONL 数据
with open(data_path, "r", encoding="utf-8") as f:
for line in f:
try:
item = json.loads(line)
self.data.append(item["text"])
except:
continue # 跳过格式错误的样本
self.tokenizer = tokenizer
self.max_seq_len = max_seq_len
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
text = self.data[idx]
# 编码:添加 EOS 标记,截断/填充到 max_seq_len
tokens = self.tokenizer.EncodeAsIds(text) + [self.tokenizer.eos_id]
if len(tokens) > self.max_seq_len:
tokens = tokens[:self.max_seq_len]
else:
tokens += [self.tokenizer.pad_id] * (self.max_seq_len - len(tokens))
return {"input_ids": torch.tensor(tokens, dtype=torch.long)}
# 使用示例
tokenizer = spm.SentencePieceProcessor()
tokenizer.Load("../minimind_tokenizer.model")
dataset = PretrainDataset("../dataset/pretrain_hq.jsonl", tokenizer, max_seq_len=320)
print("预训练数据集样本数:", len(dataset))
3.1.5 推荐数据集
- 快速训练:
pretrain_hq.jsonl(1.6GB,高质量中文文本,适配小模型); - 效果增强:可混合
wiki_zh.jsonl(中文维基百科),但需额外清洗。
3.2 SFT 数据:让模型「学对话」
3.2.1 核心作用
SFT(监督微调)是有监督学习,目标是让模型学会「对话逻辑」------ 理解用户指令,给出符合人类习惯的回复,相当于给模型「教规矩」。
3.2.2 数据格式(多轮对话 JSONL)
MiniMind SFT 数据采用 conversations 字段,支持多轮对话:
{
"conversations": [
{"role": "user", "content": "你好,介绍一下自己"},
{"role": "assistant", "content": "你好!我是 MiniMind,一个超轻量的开源语言模型,2 小时+3 块钱就能训练完成~"},
{"role": "user", "content": "推荐一些杭州的特色美食"},
{"role": "assistant", "content": "杭州的特色美食有西湖醋鱼、龙井虾仁、叫花鸡、片儿川等,每一道都极具江南风味~"}
]
}
3.2.3 数据清洗核心规则
SFT 数据质量决定模型「对话体验」,MiniMind 对 sft_mini_512.jsonl 的清洗步骤:
- 格式校验 :确保每个
user对应一个assistant,无孤立项; - 去重:基于对话内容哈希去重,避免重复对话;
- 长度筛选:单轮对话字符数 <512(小模型适配),多轮对话总字符数 <1024;
- 内容过滤:过滤含敏感信息、违法内容、无意义回复的样本;
- 中文优先:保留中文占比 >80% 的对话,避免英文对话稀释中文能力。
3.2.4 加载代码(utils/data_utils.py 核心片段)
python
class SFTDataset(Dataset):
def __init__(self, data_path, tokenizer, max_seq_len=512):
self.data = []
with open(data_path, "r", encoding="utf-8") as f:
for line in f:
try:
item = json.loads(line)
self.data.append(item["conversations"])
except:
continue
self.tokenizer = tokenizer
self.max_seq_len = max_seq_len
self.bos_id = tokenizer.bos_id
self.eos_id = tokenizer.eos_id
def __getitem__(self, idx):
conversations = self.data[idx]
input_ids = []
# 拼接对话:<|im_start|>user: 内容<|im_end|><|im_start|>assistant: 内容<|im_end|>
for turn in conversations:
role = turn["role"]
content = turn["content"].strip()
# 加入角色标记和特殊 Token
if role == "user":
input_ids.extend(self.tokenizer.EncodeAsIds(f"<|im_start|>user: {content}<|im_end|>"))
elif role == "assistant":
input_ids.extend(self.tokenizer.EncodeAsIds(f"<|im_start|>assistant: {content}<|im_end|>"))
# 截断/填充
if len(input_ids) > self.max_seq_len:
input_ids = input_ids[:self.max_seq_len]
else:
input_ids += [self.tokenizer.pad_id] * (self.max_seq_len - len(input_ids))
return {"input_ids": torch.tensor(input_ids, dtype=torch.long)}
# 使用示例
dataset = SFTDataset("../dataset/sft_mini_512.jsonl", tokenizer, max_seq_len=340)
print("SFT 数据集样本数:", len(dataset))
3.2.5 推荐数据集(按训练速度排序)
- 极速复现:
sft_mini_512.jsonl(1.2GB,1 小时训完); - 平衡效果:
sft_512.jsonl(7.5GB,6 小时训完); - 效果增强:
sft_1024.jsonl(5.6GB,4.5 小时训完)+sft_2048.jsonl(9GB,7.5 小时训完)。
3.3 DPO 数据:让模型「学偏好」
3.3.1 核心作用
DPO(直接偏好优化)是强化学习的简化版,目标是让模型学会「区分好坏」------ 生成符合人类偏好的回复,拒绝低质量、无意义的输出,相当于给模型「立标准」。
3.3.2 数据格式(偏好对比 JSONL)
DPO 数据需要成对的「优质回复(chosen)」和「劣质回复(rejected)」:
{
"chosen": [
{"role": "user", "content": "解释一下什么是光合作用"},
{"role": "assistant", "content": "光合作用是植物、藻类和某些细菌利用光能,将二氧化碳和水转化为有机物,并释放氧气的过程,是地球生态系统的基础。"}
],
"rejected": [
{"role": "user", "content": "解释一下什么是光合作用"},
{"role": "assistant", "content": "不知道,没听说过。"}
]
}
3.3.3 数据清洗核心规则
DPO 数据质量决定模型「偏好对齐程度」,MiniMind 对 dpo.jsonl 的清洗步骤:
- 成对校验 :确保
chosen和rejected的user内容完全一致,仅assistant回复不同; - 质量差异 :保证
chosen回复准确、完整、有逻辑,rejected回复错误、简略、无意义; - 长度筛选:单轮对话字符数 <3000,避免长文本占用过多显存;
- 去重 :基于
user内容去重,避免重复偏好样本。
3.3.4 加载代码(utils/data_utils.py 核心片段)
python
class DPODataset(Dataset):
def __init__(self, data_path, tokenizer, max_seq_len=3000):
self.data = []
with open(data_path, "r", encoding="utf-8") as f:
for line in f:
try:
item = json.loads(line)
self.data.append({
"chosen": item["chosen"],
"rejected": item["rejected"]
})
except:
continue
self.tokenizer = tokenizer
self.max_seq_len = max_seq_len
def _format_conversation(self, conversations):
"""格式化单条对话(同 SFT 格式)"""
input_ids = []
for turn in conversations:
role = turn["role"]
content = turn["content"].strip()
if role == "user":
input_ids.extend(self.tokenizer.EncodeAsIds(f"<|im_start|>user: {content}<|im_end|>"))
elif role == "assistant":
input_ids.extend(self.tokenizer.EncodeAsIds(f"<|im_start|>assistant: {content}<|im_end|>"))
return input_ids[:self.max_seq_len]
def __getitem__(self, idx):
item = self.data[idx]
# 格式化 chosen 和 rejected 对话
chosen_ids = self._format_conversation(item["chosen"])
rejected_ids = self._format_conversation(item["rejected"])
# 填充
chosen_ids += [self.tokenizer.pad_id] * (self.max_seq_len - len(chosen_ids))
rejected_ids += [self.tokenizer.pad_id] * (self.max_seq_len - len(rejected_ids))
return {
"chosen_ids": torch.tensor(chosen_ids, dtype=torch.long),
"rejected_ids": torch.tensor(rejected_ids, dtype=torch.long)
}
# 使用示例
dataset = DPODataset("../dataset/dpo.jsonl", tokenizer, max_seq_len=3000)
print("DPO 数据集样本数:", len(dataset))
3.3.5 推荐数据集
- 快速训练:
dpo.jsonl(55MB,1 小时训完); - 效果增强:可混合
magpie_dpo.jsonl(英文偏好数据),但需注意中文占比。
四、实战:适配自定义数据集(垂域模型必备)
很多读者想基于 MiniMind 训练垂域模型(如医疗、法律、客服),核心是适配自定义数据集。以「医疗问答」为例,教你完整流程:
4.1 自定义数据集格式(医疗 SFT 数据)
{
"conversations": [
{"role": "user", "content": "颈椎病患者选什么高度的枕头合适?"},
{"role": "assistant", "content": "颈椎病患者选枕头的高度建议与自身拳头高度相当(约 8-12cm),材质优先选记忆棉、乳胶,避免过硬或过软,同时要保证颈椎自然生理曲度。"}
]
}
4.2 数据转换脚本(将 CSV/Excel 转为 JSONL)
如果你的数据是 CSV 格式(如 medical_qa.csv,含 user 和 assistant 列),用以下脚本转换:
python
import pandas as pd
import json
# 读取 CSV 数据
df = pd.read_csv("medical_qa.csv")
# 转换为 JSONL
with open("lora_medical.jsonl", "w", encoding="utf-8") as f:
for idx, row in df.iterrows():
user_content = str(row["user"]).strip()
assistant_content = str(row["assistant"]).strip()
# 跳过空值
if not user_content or not assistant_content:
continue
# 构建格式
item = {
"conversations": [
{"role": "user", "content": user_content},
{"role": "assistant", "content": assistant_content}
]
}
f.write(json.dumps(item, ensure_ascii=False) + "\n")
print("转换完成!生成 lora_medical.jsonl")
4.3 数据清洗(去重 + 过滤)
python
def clean_custom_data(input_path, output_path):
seen = set() # 用于去重
with open(output_path, "w", encoding="utf-8") as out_f:
with open(input_path, "r", encoding="utf-8") as in_f:
for line in in_f:
try:
item = json.loads(line)
conv = item["conversations"]
# 生成对话哈希(去重)
conv_str = json.dumps(conv, ensure_ascii=False)
if conv_str in seen:
continue
seen.add(conv_str)
# 过滤短回复(<10 字)
assistant_content = conv[1]["content"]
if len(assistant_content) < 10:
continue
# 写入清洗后的数据
out_f.write(json.dumps(item, ensure_ascii=False) + "\n")
except:
continue
print(f"清洗完成!生成 {output_path},共 {len(seen)} 条有效样本")
clean_custom_data("lora_medical.jsonl", "lora_medical_cleaned.jsonl")
4.4 加载自定义数据集训练
将清洗后的 lora_medical_cleaned.jsonl 放到 ./dataset 目录,即可用 MiniMind 的 LoRA 训练脚本训练:
cd trainer
python train_lora.py --data_path ../dataset/lora_medical_cleaned.jsonl
五、数据工程高频问题解决方案
5.1 数据下载慢 / 超时
- 解决方案 1:用 ModelScope 镜像(国内访问快):
https://www.modelscope.cn/models/gongjy/MiniMind2/datasets; - 解决方案 2:单独下载所需文件(无需克隆整个数据集仓库),用迅雷 / IDM 加速。
5.2 格式错误导致训练报错
-
排查步骤:用
jsonlint检查 JSONL 格式,或运行以下脚本筛选错误样本:pythonimport json with open("sft_mini_512.jsonl", "r") as f: for idx, line in enumerate(f): try: json.loads(line) except Exception as e: print(f"第 {idx+1} 行格式错误:{e}")
5.3 训练时显存不足(数据相关)
- 解决方案:减小
max_seq_len(预训练设为 320,SFT 设为 340),或使用更小的数据集(如sft_mini_512.jsonl)。
5.4 Tokenizer 编码后出现大量 <unk>(未知词)
- 解决方案:重新训练 Tokenizer,加入更多垂域词汇(如医疗数据中的「颈椎病」「乳胶枕」),或增大词表到 8192。
六、下篇内容预告
本篇我们搞定了 MiniMind 数据工程全流程:训练了专属 Tokenizer,拆解了预训练 / SFT/DPO 数据集的格式、清洗、加载逻辑,还学会了适配自定义垂域数据。现在,「粮草」已备齐,终于可以启动模型训练了!
下一篇(第 5 篇),我们聚焦核心训练流程:《MiniMind 核心训练|单卡 3090 极速复现:预训练 + SFT 从零跑通》将带大家亲手执行预训练和 SFT 训练脚本,理解训练超参数配置、断点续训机制、训练过程监控,2 小时跑通属于自己的 26M 对话小模型。
写在最后
数据工程是 LLM 训练的「地基」,尤其是小模型,差之毫厘谬以千里 ------ 同样的模型架构,用高质量数据训练的效果,可能是低质量数据的 2-3 倍。
MiniMind 的数据工程设计,处处体现「轻量化 + 精准化」:6400 词表避免参数浪费,精简数据集降低训练成本,统一格式简化工程实现。这些设计思路,不仅适用于 MiniMind,也适用于所有小模型开发。
建议大家动手实操:训练一个自定义 Tokenizer,处理一份自己的垂域数据,感受数据质量对模型效果的影响。遇到问题可以在评论区交流,我们一起踩坑、解决问题。
项目地址:https://github.com/jingyaogong/minimind收藏 + 关注,下一篇带你从零跑通小模型训练,见证 26M 参数模型的诞生!