用Python标准库按提示词分组划分数据:让同组样本留在同一个集合
从格式检查走到数据划分
JSONL能够正常解析,说明数据过了格式这一关;准备训练集和验证集时,还需要考虑样本之间的关系。例如同一个问题有多个答案版本,如果直接逐行随机划分,它们可能分别进入训练集和验证集。这样评估时就可能遇到训练中见过的问题,结果不容易解释。
本文给出一个小型分组划分实现:用instruction和input组成提示词分组键,再通过固定规则将整组分配给train或validation。代码只依赖Python标准库。本次在本地Python 3.11中用8条自建记录运行验证,没有上传业务数据,也没有据此声称模型效果提升。
先把分组规则讲清楚
这里把instruction和input相同的记录归为一组,output不参与分组。原因是同一道问题的不同答案仍应保持在同一侧。缺少input时按空字符串处理;显式传入null则报错,避免把缺失与错误值混为一谈。
为了处理常见的文本表示差异,示例统一CRLF、CR换行为LF,执行Unicode NFC规范化,并去掉字段首尾空白。它不会合并内部连续空格、改变大小写、翻译文字或识别语义近似问题。这些选择都是本工具的规则,不是任何云平台的数据规范。对于代码、诗歌、缩进敏感文本,首尾空白本身也可能有意义,此时应调整normalize函数,重新生成整个划分版本。
组键不能简单写成instruction + input。否则“a”和“bc”与“ab”和“c”会拼接出相同字符串。代码使用JSON数组序列化保留两个字段的边界,再把固定版本标记salt和组键一起编码后计算SHA-256。
完整实现
保存为group_split.py。plan返回分配结果和原始记录,不写入文件、不修改输入记录,也不调用任何外部服务。
"""Deterministic prompt grouping; no network and no file mutation."""
import hashlib
import json
import unicodedata
def normalize(value):
if not isinstance(value, str):
raise TypeError("instruction and input must be strings")
value = value.replace("\r\n", "\n").replace("\r", "\n")
return unicodedata.normalize("NFC", value).strip()
def group_key(record):
if not isinstance(record, dict):
raise TypeError("record must be an object")
instruction = normalize(record["instruction"])
if not instruction:
raise ValueError("instruction must not be empty")
return json.dumps([instruction, normalize(record.get("input", ""))],
ensure_ascii=False, separators=(",", ":"))
def assign(record, validation_percent=20, salt="split-v1"):
if type(validation_percent) is not int or not 0 <= validation_percent <= 100:
raise ValueError("validation_percent must be an integer in 0..100")
payload = json.dumps([salt, group_key(record)], ensure_ascii=False,
separators=(",", ":")).encode("utf-8")
digest = hashlib.sha256(payload).digest()
bucket = int.from_bytes(digest[:8], "big") % 100
return "validation" if bucket < validation_percent else "train"
def plan(records, validation_percent=20, salt="split-v1"):
return [(assign(r, validation_percent, salt), r) for r in records]
validation_percent为0到100之间的整数。把摘要前8字节转换为整数后映射到100个桶,小于阈值的组进入validation。salt在这里是公开的划分版本标记,不是密码。为了复现结果,应记录salt、分组字段、规范化规则、代码版本及输入数据版本。不要使用Python内置hash作为跨进程稳定分配的依据。
同一规则下,记录重新排序不会改变其所属集合;新增其他组也不会改变已有组的归属。改变比例、salt或规范化规则则可能改变已有分配,因此这些参数应和实验记录一起保存。
最小用法与检查方法
from group_split import plan, group_key
rows = [
{"instruction": "Summarize", "input": "A", "output": "one"},
{"instruction": "Summarize", "input": "A", "output": "another"},
{"instruction": "Classify", "input": "B", "output": "six"},
]
assigned = plan(rows, validation_percent=20, salt="split-v1")
train_keys = {group_key(r) for label, r in assigned if label == "train"}
valid_keys = {group_key(r) for label, r in assigned if label == "validation"}
assert train_keys.isdisjoint(valid_keys)
for label, record in assigned:
print(label, record["instruction"])
如果要处理JSONL,可以先逐行解析并完成字段校验,再把记录交给assign。本文的plan为了便于演示会返回一个列表,空间开销随记录数增加;大文件可逐条调用assign,分别写到新建的输出文件。正式导出时还应考虑出错后的临时文件清理和原子发布,不能把只写到一半的文件当作完整训练输入。
本次自建样例验证了什么
8条样例包含:同一提示词的两个不同答案、首尾空白与CRLF差异、NFC等价的两种重音字符表示,以及三个不同提示词。按上述规则形成5个分组。实际输出标签按输入顺序为:
validation, validation, validation, train,
train, train, train, validation
断言确认前3条同组且同侧,第4、5条同组且同侧;倒序输入后每个组的归属不变;新增另一组后原有组归属不变。比例设为0时全部为train,设为100时全部为validation;负数、超过100、浮点数和布尔值均被拒绝。测试还检查了字段拼接歧义、空instruction、非字符串input和输入对象未被修改。
这里4条进入验证集,并不等于程序承诺20%的记录数。阈值作用于组的摘要桶,有限样本和大小不均的分组会让实际记录比例偏离配置值。如果业务必须严格满足数量或标签分布,需要在整组约束下设计额外的分配策略,并保存最终清单。
适用边界
相同提示词分组只能解决明确规则下的一类交叉问题。来自同一篇文档、同一会话或同一对象的不同问题,仍可能被分到两侧;若这些关联是评估关注点,应优先用文档ID、会话ID或对象ID作为分组依据。近重复内容需要另外检测;按时间评估未来表现时,也不适合直接用这种无时间约束的划分代替时间切分。
本示例不检查output是否为有效标注,不处理重复JSON字段,也不替代数据格式预检和人工抽查。计算哈希也不意味着匿名化,尤其是提示词来自一个很小的可猜测集合时。分享实验记录时不要顺带公开原始问题、答案或敏感标识。
可以先做格式校验,再确定业务上的独立分组单位,最后检查两侧组键是否相交,并统计实际条数、标签分布和长短文本分布。这样的记录比只保存一句“按8:2切分”更容易复现。
参考:Python官方文档《hashlib — Secure hashes and message digests》与《json — JSON encoder and decoder》。本文代码与文字由AI辅助编写,示例使用自建数据实测,未包含真实账号、密钥或训练样本。
- 点赞
- 收藏
- 关注作者
评论(0)