本节摘要:最经济的取数路线是用现成的开源指令集。本节先给四把尺子——质量、规模、许可证、语言——再横向对比三个主流候选:OpenHermes 2.5(约 100 万条、多源混合、质量口碑好)、UltraChat(约 150 万轮多轮对话、由强模型生成)、SmolTalk(HuggingFace 为小模型 SFT 调配的混料,也是 nanochat 当前 master 的示例选择)。选定之后过一遍不可省略的清洗管线:格式校验(2.1 的契约)→ 精确去重 + 近似去重(MinHash 类)→ 规则质量过滤 → 长度截断,附可跑的管线代码。中心信条:开源数据"开箱即用"是指许可证,不是指质量。
阅读完本节,你应当能够:
| 尺子 | 问什么 | 为什么重要 |
|---|---|---|
| 质量 | 回答是否正确、格式是否统一、是否含机器翻译腔/残留水印 | 小模型没有"消化坏数据"的冗余容量,坏答案会被直接背下来 |
| 规模 | 条数够不够(SFT 通常数万~数十万条起) | 太少学不会格式,太多重复则过拟合模板腔 |
| 许可证 | 数据集 license 是否允许训练与再分发 | 商用/再分发的前提,发布模型前必查 |
| 语言 | 中英占比与中文质量 | 英文为主的数据训出的模型中文手感差——第 8 章中文改造的核心动因 |
💡 质量优先于规模是 SFT 数据的主旋律:数万条高质量样本的效果通常好过百万条平庸混合(社区多份复现报告的一致结论,如 Tulu/LIMA 一系研究的核心发现——"少量高质量指令数据足以对齐",属学术共识级结论而非精确数字)。
| 数据集 | 规模(数据集卡口径) | 形态 | 特点 | 许可证(以数据集卡为准) |
|---|---|---|---|---|
| OpenHermes 2.5 | ~100 万条 | 以单轮为主 | 多源混合、质量口碑好,社区 SFT 常客 | 开放(允许训练用途) |
| UltraChat | ~150 万轮 | 多轮对话 | 强模型生成的多轮语料,练指代与角色恒定 | 开放研究用途 |
| SmolTalk | ~100 万条 | 混合(含工具/推理类子集) | HuggingFace 为小模型 SFT 调配;nanochat 当前 master 的示例选择 | 开放(含子集,注意各源约束) |
nanochat 与这三者的关系(两个时间层,均需以官方仓库 README 为准):发布帖时代(2025-10)SFT 数据由官方自备的一批问答对话构成;当前 master 的 SFT 示例数据换用了 SmolTalk。本书教学上不受绑定——三条数据里任一条(或混合)都能跑通第 3 章,方法完全一致。
选型速查:
# clean_sft_data.py —— 开源指令集清洗管线(写法示意) import json import re import hashlib MAX_TOKENS = 2048 # 与 1.1 的长度预算对齐(p95 截断) def norm_key(content: str) -> str: """精确去重键:压空白后取哈希。""" return hashlib.md5(re.sub(r"\s+", "", content).encode()).hexdigest() def rule_filter(obj: dict) -> bool: """规则质量过滤:拦最常见的不合格样本。""" for m in obj["messages"]: c = m["content"] if len(c) < 2: # 空洞回答 return False if c.lower().startswith(("as an ai", "作为一个语言模型")): return False # 模型免责腔,小模型学了只会复读 if "http" in c and len(c) < 60: # 纯链接回答 return False # 回答与提问完全相同 = 自问自答脏样本 pairs = [(u, a) for u, a in zip(obj["messages"], obj["messages"][1:]) if u["role"] == "user" and a["role"] == "assistant"] return all(u["content"] != a["content"] for u, a in pairs) def char_budget(obj: dict) -> bool: """长度预算的粗筛(字符级,token 级在打包时精确截断)。""" return sum(len(m["content"]) for m in obj["messages"]) <= MAX_TOKENS * 3 def pipeline(src: str, dst: str) -> None: seen, kept = set(), 0 with open(src, encoding="utf-8") as f, open(dst, "w", encoding="utf-8") as out: for line in f: try: obj = json.loads(line) except json.JSONDecodeError: continue # step0:JSON 层直接丢 if not rule_filter(obj) or not char_budget(obj): continue # step2:规则过滤 key = norm_key(obj["messages"][-1]["content"]) # step1:按末轮回答精确去重 if key in seen: continue seen.add(key) out.write(json.dumps(obj, ensure_ascii=False) + "\n") # step3:截断在打包时做 kept += 1 print(f"清洗完成:保留 {kept} 条") if __name__ == "__main__": pipeline("data_sft/raw.jsonl", "data_sft/train.jsonl")
四步各自的取舍:
check_messages.py,契约不过的样本直接出局。⚠️ 近似去重(MinHash/SimHash 类)在十万条以上规模时值得加:精确哈希抓不到"改了几个字"的近重复。Python 里可用
datasketch库的 MinHashLSH;本书 124M 实验的量级下,精确去重 + 规则过滤已够用。
清洗后的合理量级(示意参考,非硬指标):
| 模型规模 | 建议清洗后条数 | 训练轮数(第 3 章用) |
|---|---|---|
| 124M | 2 万~10 万条 | 2~3 个 epoch |
| 1B+ | 10 万~100 万条 | 1~2 个 epoch |
产出验收两条:wc -l data_sft/train.jsonl 看条数落在上表区间;随机抽 20 条人眼看质量(人工抽检不可省略——所有自动指标都可能集体失灵,眼睛不会)。
开源集覆盖不到的领域(比如中文垂直场景)怎么办?2.3 节换上自造数据的产线——让强模型替你写教材。