模型微调数据集怎么制作成JSONL格式

文章导读
把微调数据集做成 JSONL,动作本身不复杂:把每条训练样本序列化成一行独立的 JSON 对象,按 UTF-8 写进文本文件,行与行之间不共享结构。真正容易翻车的是字段约定,JSON 语法本身反而不常出问题。同一批数据,字段写成 messages 还是 prompt / completion,直接决定训练脚本能不能直接加载、chat template 会不会自动补上特殊 token。因此先对齐全套
📋 目录
  1. A 先确认当前场景和目标
  2. B 按这个顺序处理
  3. C 改完后怎么验证
  4. D 容易忽略的边界
A A

把微调数据集做成 JSONL,动作本身不复杂:把每条训练样本序列化成一行独立的 JSON 对象,按 UTF-8 写进文本文件,行与行之间不共享结构。真正容易翻车的是字段约定,JSON 语法本身反而不常出问题。同一批数据,字段写成 messages 还是 prompt / completion,直接决定训练脚本能不能直接加载、chat template 会不会自动补上特殊 token。因此先对齐全套约定,再批量序列化,比先转格式再回头改要省事得多。

判断方向:先确认训练框架读取的字段结构(多轮用 messages、单轮用 instruction-input-output 或 prompt-completion),再用脚本逐条序列化,最后逐行回读校验。JSONL 的硬性要求是一行一个完整 JSON 对象、UTF-8 无 BOM、行内不出现裸换行。边界:字段名与 chat template 不匹配时,文件语法再正确,训练效果也不会符合预期,这时应改数据侧,而不是改框架侧。

先确认当前场景和目标

动手前先回答几个问题,答案不同,产出的 JSONL 结构就不同。

  • 任务是单轮指令、多轮对话,还是分类或抽取类任务?多轮一般用 messages 数组,单轮在多数框架里可以用 instruction / input / output 三段式。
  • 训练框架和加载器对字段名有硬要求吗?有的加载器只认固定键名,有的允许用参数映射,建议先翻一下手头训练配置里数据字段相关的设置项。
  • chat template 由谁负责拼接?如果 tokenizer 会在训练时自动套模板,数据集里就不要再手工塞 <|im_start|> 这类标记,否则会重复。
  • 原始数据现在是什么形态:CSV、Excel、数据库导出、还是一堆散装 txt?这决定了清洗步骤放在序列化之前还是之后。
  • 数据量大概多少、单条最长多少字符?这关系到用流式逐行处理还是允许整表读进内存。

如果以上任何一条还不确定,先取 20 条样本手工做成一个样例文件,把这个样例交给训练框架跑通,再去批量转换。这个顺序能避免批量转换完才发现整体结构不对。

按这个顺序处理

建议按“定结构、洗数据、序列化、落盘、划分”这个顺序推进,每一步都能单独检查。

模型微调数据集怎么制作成JSONL格式
  1. 先定一条样例记录,把它写进一个只有一行的样例文件,作为后续所有样本的结构基准。
  2. 清洗原始数据:去掉问答为空或只有空白字符的样本、去掉完全重复的条目、按目标模型的上下文长度处理超长样本。超长样本是截断还是丢弃,需要结合任务确认,多轮对话里直接截断容易把最后一轮 assistant 回复切掉。
  3. 写转换脚本,逐条序列化并写入新文件,不要在原文件上覆盖写。
  4. 划分训练集和验证集,划分应在去重之后做,避免同源样本同时出现在两边。

下面是一个通用的转换骨架,字段名和过滤条件按自己的数据替换:

import json

def to_record(item):
    return {
        "messages": [
            {"role": "user", "content": item["question"].strip()},
            {"role": "assistant", "content": item["answer"].strip()},
        ]
    }

with open("raw.jsonl", encoding="utf-8") as fin, \
     open("train.jsonl", "w", encoding="utf-8", newline="\n") as fout:
    for line in fin:
        item = json.loads(line)
        q = item.get("question", "").strip()
        a = item.get("answer", "").strip()
        if not q or not a:
            continue
        fout.write(json.dumps(to_record(item), ensure_ascii=False) + "\n")

几个要点:ensure_ascii=False 让中文保持可读,便于人工抽查;json.dumps 会把字段内部的换行转义成 \n,所以内容里有换行也不会破坏“一行一对象”;写入时显式指定 newline="\n",避免在 Windows 环境下写出 CRLF。转换后文件形如:

{"messages":[{"role":"user","content":"把这句话翻成英文:今天下雨"},{"role":"assistant","content":"It is raining today."}]}
{"messages":[{"role":"user","content":"用一句话解释梯度下降"},{"role":"assistant","content":"沿着损失函数下降最快的方向逐步调整参数。"}]}

如果框架要求单轮三段式,把 to_record 的返回改成 {"instruction": q, "input": "", "output": a} 即可,其余流程不变。

模型微调数据集怎么制作成JSONL格式

改完后怎么验证

验证分三层:语法层、结构层、语义层。

语法层逐行回读,确认每一行都能被解析、没有空行:

import json

count = 0
with open("train.jsonl", encoding="utf-8") as f:
    for i, line in enumerate(f, 1):
        if not line.strip():
            print("空行:", i)
            continue
        obj = json.loads(line)
        assert obj["messages"][-1]["role"] == "assistant"
        count += 1
print("有效样本数:", count)

预期信号是没有异常抛出,且打印出的有效样本数与清洗后应保留的条数一致。

模型微调数据集怎么制作成JSONL格式

结构层检查首字节是否带 BOM(读到 \ufeff 就说明有),抽查若干条样本的字段是否齐全、role 顺序是否是 user 开头、assistant 结尾。

语义层取一两条样本,走一次 tokenizer 的 chat template 渲染,看渲染出来的字符串是否符合预期:有没有重复的特殊标记、有没有多出一个空的 system 段、结束符位置是否正确。再把渲染结果和原始文本对照,确认内容没有被转义或截断。最后人工抽读十几条,重点看标点、代码块、表格这类容易在转换中被破坏的内容。

如果训练框架提供了 dry run 或数据预览命令,先用小样本子集跑一次,比直接上全集更容易定位问题。

容易忽略的边界

  • 编码与换行:UTF-8 带 BOM、CRLF 行尾、文件末尾多出空行,都可能让加载器报错或静默跳过。用 newline="\n" 写、用 encoding="utf-8" 读,能规避大部分这类问题。
  • 嵌套 JSON 被二次转义:如果原数据里某个字段本身就是 JSON 字符串,直接塞进去会变成带反斜杠的字符串。需要先 json.loads 解析再放入,或者明确保留成字符串,取决于框架期望。
  • 空 assistant 回复:某些清洗流程会把空回复保留下来,训练时这类样本只会教模型学会立刻停止输出,通常应过滤。
  • 大文件内存:几十万条以上时,逐行流式读写比整表 read() 再处理更稳妥,上面的骨架已经是流式写法。
  • 训练/验证集泄漏:同一条样本的不同改写版本如果同时进入两边,验证指标会偏乐观,建议先去重再划分。
  • 回退建议:转换前保留原始数据副本,输出到新文件名而不是覆盖;一旦发现结构不对,直接丢弃输出文件重跑脚本,比在已转换的文件上做修补更可控。如果框架字段要求与现有数据差距过大,优先在脚本里加一层映射,而不是改动原始数据源。