做自己的数据集

微调数据该长什么样、从零整理的流程、质量比数量重要,以及怎么传上 Hub。

作者 David更新于 第 13 篇(共 19 篇)

要微调,先有数据。这一篇讲微调数据的标准格式、怎么从手上的原始材料整理出来、以及一条最该记住的经验:质量远比数量重要。

微调数据的两种格式

① 对话格式(推荐)

{"messages": [
  {"role": "system", "content": "你是一个简洁的客服助手。"},
  {"role": "user", "content": "怎么退货?"},
  {"role": "assistant", "content": "在订单页点「申请退货」,填写原因后系统会生成退货单号。"}
]}

一行一条,存成 .jsonl。这是当前的事实标准——trl 的 SFTTrainer 直接支持,会自动套用模型的 chat template。

多轮对话就是多几组 user/assistant:

{"messages": [
  {"role": "user", "content": "怎么退货?"},
  {"role": "assistant", "content": "在订单页点「申请退货」。"},
  {"role": "user", "content": "退款多久到账?"},
  {"role": "assistant", "content": "审核通过后 3~5 个工作日原路退回。"}
]}

② 指令格式(Alpaca 风格)

{"instruction": "把下面这句话改写得更正式", "input": "这事儿挺急的", "output": "此事较为紧急。"}

老一些,但仍常见。新项目建议直接用对话格式——它能表达多轮,也和推理时的实际形态一致。

训练格式必须和推理时一致。 用单轮指令格式训出来的模型,拿去做多轮对话时表现会明显下滑——它没见过「上文里有好几轮」这种输入。

同理,训练时带 system prompt、推理时不带(或反过来),效果也会打折。先想清楚上线时怎么用,再按那个样子准备数据。

从原始材料到数据集

假设你有一堆公司文档,想微调一个问答助手。

第一步:抽取成结构化

import json, pathlib

rows = []
for p in pathlib.Path("docs").glob("**/*.md"):
    text = p.read_text(encoding="utf-8")
    rows.append({"source": str(p), "text": text})

with open("raw.jsonl", "w", encoding="utf-8") as f:
    for r in rows:
        f.write(json.dumps(r, ensure_ascii=False) + "\n")

ensure_ascii=False 别忘了,否则中文会变成 \uXXXX 转义,文件体积翻几倍且不可读。

第二步:生成问答对

手写几千条不现实,常见做法是用一个强模型批量生成,人工抽检:

from vllm import LLM, SamplingParams

llm = LLM(model="Qwen/Qwen3-32B", max_model_len=8192)

PROMPT = """根据下面的文档片段,生成 3 个用户可能问的问题及对应答案。
答案必须完全来自文档,不要编造。输出 JSON 数组,每项含 question 与 answer。

文档:
{doc}"""

prompts = [[{"role": "user", "content": PROMPT.format(doc=r["text"][:3000])}] for r in rows]
outs = llm.chat(prompts, SamplingParams(temperature=0.3, max_tokens=1024))

用 vLLM 的离线批量推理,几千条几分钟就跑完。

合成数据必须人工抽检。 模型会编造文档里没有的内容(尤其在文档片段信息不足时),这些幻觉会被原样学进去,而且训完很难发现。

最低限度:随机抽 100 条逐个核对。错误率超过 5% 就要改提示词或换更强的模型重来。

另外注意生成模型的许可——有些商业模型的服务条款禁止用其输出训练竞品模型。用开源模型生成没这个问题。

第三步:清洗

from datasets import load_dataset

ds = load_dataset("json", data_files="qa.jsonl", split="train")

ds = ds.filter(lambda x: 10 < len(x["answer"]) < 2000)       # 去掉太短太长
ds = ds.filter(lambda x: "抱歉" not in x["answer"][:10])      # 去掉拒答
ds = ds.filter(lambda x: x["question"].strip().endswith(("?", "?")))

去重(简单精确去重):

seen = set()
def dedup(x):
    key = x["question"].strip()
    if key in seen:
        return False
    seen.add(key)
    return True

ds = ds.filter(dedup)

去重的收益常被低估。 合成数据里重复问法极多(「怎么退货」「如何退货」「退货流程是什么」),重复样本会让模型在这些点上过拟合。

精确去重是底线。更进一步可以用向量模型做语义去重:相似度 > 0.95 的只留一条。

第四步:转成对话格式

def to_messages(x):
    return {"messages": [
        {"role": "user", "content": x["question"]},
        {"role": "assistant", "content": x["answer"]},
    ]}

ds = ds.map(to_messages, remove_columns=ds.column_names)
ds = ds.train_test_split(test_size=0.05, seed=42)
ds["train"].to_json("train.jsonl", force_ascii=False)

留 5% 做验证集——训练时用来看有没有过拟合,见训练实操。

质量比数量重要

这是整篇最该记住的一句。

1000 条精挑细选的数据,通常胜过 10 万条粗糙数据。

这不是玄学:LoRA 微调调整的参数量本来就少,它学的是「风格与格式」而非「新知识」。少量高质量样本足以定型;大量含噪样本反而把噪声一起学了。

实践路径:

  1. 先做 500~1000 条,训一版看效果。
  2. 效果不好,先看数据(抽查 50 条),而不是加量。
  3. 确认数据没问题再加量,每次翻倍。

一上来就堆 10 万条,出了问题根本查不出是哪批数据的锅。

判断数据质量的几个信号:

检查怎么看
答案是否真的回答了问题抽查 50 条
长度分布是否合理plt.hist([len(a) for a in answers])
有没有大量重复去重前后条数比
格式是否统一有没有的答案带 Markdown、有的不带
有没有敏感信息手机号、邮箱、身份证、内部密钥

上传前必须过一遍敏感信息。 从内部文档生成的数据集里混进真实姓名、电话、密钥,传到公开仓库就收不回来了(Git 有历史)。

import re
PATTERNS = [r"\d{11}", r"[\w.]+@[\w.]+", r"sk-[A-Za-z0-9]{20,}"]

def has_pii(x):
    t = json.dumps(x, ensure_ascii=False)
    return any(re.search(p, t) for p in PATTERNS)

suspects = ds.filter(has_pii)
print(f"可疑条数:{len(suspects)}")

正则只是初筛,发现可疑就人工看。

传到 Hub

from datasets import load_dataset

ds = load_dataset("json", data_files={"train": "train.jsonl", "test": "test.jsonl"})
ds.push_to_hub("your-name/my-qa-dataset", private=True)

先传 private=True,确认没问题再改公开。

命令行方式:

hf upload --repo-type dataset your-name/my-qa-dataset ./data

写数据集卡片

仓库根目录的 README.md,带 YAML 头:

---
license: cc-by-4.0
language: [zh]
task_categories: [question-answering]
size_categories: [1K<n<10K]
---

# 客服问答数据集

## 来源
基于公司公开文档,用 Qwen3-32B 生成问答对,人工抽检 200 条(准确率 96%)。

## 格式
对话格式 JSONL,每条含单轮 user/assistant。

## 已知局限
- 覆盖范围限于退换货与物流,不含支付相关问题。
- 合成数据,可能残留少量事实性错误。

「已知局限」这一节最有价值,也最常被省略。 半年后你自己回来看,或者别人要用,最需要知道的就是「这份数据不适合干什么」。

写清楚来源与生成方式,也是学术与合规上的基本要求。

小结

  • 微调数据用对话格式 JSONL({"messages": [...]}),是当前标准。
  • 训练格式必须和推理时一致——单轮训出来的模型做多轮会明显变差。
  • 用强模型批量生成 + 人工抽检 100 条,错误率超 5% 就重来。
  • 去重收益很大,重复问法会导致局部过拟合。
  • 1000 条精数据胜过 10 万条粗数据;效果不好先看数据,别急着加量。
  • 写文件记得 ensure_ascii=False。
  • 上传前扫一遍 PII,公开仓库的历史收不回来。
  • 数据集卡片里写清来源与已知局限。

下一篇:LoRA 微调。

本页目录