准备在云端 Mac 跑一次 MLX 微调时,先别启动训练。把训练集与验证集上传后,最值得先做的是数据验收:某行不是完整 JSON、回答为空,或同一段对话同时进入两个集合,都可能让一次实验失去比较价值。下面用 Python 标准库建立一道可重复执行的检查,不下载模型,也不依赖训练环境。
先约定输入,不让脚本猜格式
本文处理纯文本对话,每行一个对象,仅含 messages 字段;每条消息仅含 role、content。允许开头出现一条 system,随后必须按 user、assistant 交替,最后以回答结束。多模态内容、工具调用和连续同角色消息不在本次契约内。
这是一套项目验收规则,不代表所有 MLX 训练工具都只接受这种结构。执行前要核对当前工具的数据入口;如果源数据采用其他字段,先写显式转换步骤,不要让检查器静默丢字段。
{"messages":[{"role":"user","content":"解释训练集与验证集的区别。"},{"role":"assistant","content":"训练集用于更新参数,验证集用于观察未参与更新的数据表现。"}]}
把两个集合保存为 train.jsonl、valid.jsonl,使用 UTF-8、无 BOM 编码。空行算错误;文件最后一个换行符不算额外空行。原始文件只读保存,修订另存一份,方便追查数据变化。
用标准库逐行拦截格式问题
先运行 python3 --version 确认解释器可用,再将下面代码保存为 check_dataset.py。单行限制设为 256 KiB,这是本项目的输入保护阈值,不是模型上下文容量。脚本按二进制限量读取,避免超长行先被完整装入内存。
import hashlib
import json
import sys
LIMIT = 256 * 1024
seen = {}
def unique_object(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValueError("duplicate object key")
result[key] = value
return result
def reject_constant(value):
raise ValueError("nonstandard JSON constant")
def validate(obj):
if not isinstance(obj, dict) or set(obj) != {"messages"}:
raise ValueError("expected messages-only object")
messages = obj["messages"]
if not isinstance(messages, list):
raise ValueError("messages must be an array")
roles = []
for message in messages:
if not isinstance(message, dict):
raise ValueError("message must be an object")
if set(message) != {"role", "content"}:
raise ValueError("unexpected message fields")
role = message["role"]
content = message["content"]
if not isinstance(role, str):
raise ValueError("role must be text")
if not isinstance(content, str) or not content.strip():
raise ValueError("content must be nonempty text")
roles.append(role)
body = roles[1:] if roles[:1] == ["system"] else roles
if len(body) < 2 or len(body) % 2:
raise ValueError("expected complete conversation pairs")
if body != ["user", "assistant"] * (len(body) // 2):
raise ValueError("invalid role order")
return messages
def inspect(path):
count = 0
with open(path, "rb") as stream:
while True:
raw = stream.readline(LIMIT + 1)
if not raw:
break
count += 1
try:
if len(raw) > LIMIT:
raise ValueError("line exceeds byte limit")
obj = json.loads(
raw.decode("utf-8"),
object_pairs_hook=unique_object,
parse_constant=reject_constant,
)
messages = validate(obj)
canonical = json.dumps(
messages, ensure_ascii=False,
sort_keys=True, separators=(",", ":"),
).encode("utf-8")
digest = hashlib.sha256(canonical).hexdigest()
if digest in seen:
raise ValueError(
"duplicate conversation; first at " + seen[digest]
)
seen[digest] = f"{path}:{count}"
except (ValueError, RecursionError) as error:
raise ValueError(f"{path}:{count}: {error}") from error
if count == 0:
raise ValueError(f"{path}: empty dataset")
return count
if __name__ == "__main__":
if len(sys.argv) != 3:
raise SystemExit("usage: check_dataset.py TRAIN VALID")
try:
counts = [inspect(path) for path in sys.argv[1:]]
except (OSError, ValueError) as error:
print(str(error), file=sys.stderr)
raise SystemExit(1)
print(json.dumps({"train": counts[0], "valid": counts[1]}))
把重复检测放在两个集合之间
依次传入训练集与验证集,脚本共享同一份摘要索引。因此,集合内部重复和跨集合重复都会返回失败,并显示第一次出现的位置。它遇到首个错误就停止,修复后重新执行,不输出整段对话内容。
python3 check_dataset.py train.jsonl valid.jsonl
摘要计算前会排序对象键、去掉 JSON 排版差异,但保留消息顺序和正文字符。调整缩进或键顺序无法绕过检测;正文增加空格则可能得到不同摘要。脚本不会擅自统一大小写或改写 Unicode,以免改变代码、公式及原文含义。
精确去重不等于消除数据泄漏。同一文档拆出的不同问答、问题相同而回答改写的样本,仍可能跨集合泄漏。
有来源文档编号时,应先按文档分组切分,再生成对话。没有来源编号时,至少补做问题重复检查和抽样复核,不要把摘要检查通过当成验证集独立性的证明。摘要索引随样本量增长;超大数据集应改用磁盘索引。
分词长度必须另做一次验收
字节数只用于控制异常输入,不能换算为固定 token 数。中文、代码、表情的编码与分词比例不同,聊天模板还会添加角色标记和其他特殊 token。
长度检查应使用目标模型随附的分词器,按照训练工具实际采用的聊天模板处理整段对话,再统计 token。记录模型修订标识、模板、最大序列长度及超限条数;不要只分别计算各条 content 后相加。
超限样本要明确选择拒绝、拆分或截断。采用截断时,检查是否把回答全部截掉,以及损失掩码是否仍覆盖有效回答;有效训练 token 为零的样本必须拒绝。这里不提供通用截断命令,因为不同训练入口的模板与掩码行为并不相同。
用反例验收检查器,再接入训练
先复制样本构造四种反例:空回答、角色顺序错误、重复对象键,以及跨文件重复对话。逐项运行,确认都返回退出码 1,并能定位文件与行号。还要用两条不同的合法对话验证成功路径,避免检查器始终报错却无人发现。
接入实验脚本时,用 && 或显式判断退出码连接后续训练步骤,不能失败后继续执行。固定数据后保存两份文件的 SHA-256、样本数量、长度检查结果与检查器版本;本次训练只读取这份已验收副本。
验收的终点不是一句“数据正常”,而是可以重新执行的证据:相同输入得到相同结论,错误能定位,集合切分有依据,长度处理有记录。完成这些,再开始消耗训练时间。
常见问题
JSONL 检查通过后,可以直接认定数据适合 MLX 微调吗?
不能。脚本只验收约定的消息结构与精确重复;还要用所选模型的分词器和聊天模板检查长度,并核对当前训练工具接受的数据格式。
调整 JSON 对象的键顺序能避开重复检测吗?
不能。脚本先对对象键排序再计算 SHA-256,相同消息内容即使键顺序不同也会被识别;正文措辞变化和语义近似不在检测范围内。
按实验时长租用 Mac
独享 Mac mini M4,16GB 统一内存 / 256GB SSD。按天、周、月、季预付,到期不续即停机。模型与运行环境由用户自行准备,实际可用状态以控制台实时返回为准。
按天租用 Mac