← 返回文章列表
数据工程

构建高质量 SFT 数据集的几个原则

2025-04-12 · 约 7 分钟读完

"数据质量比数据量更重要"——这话听了太多遍,但真正落地的时候,还是会本能地去堆数量。这篇文章记录我在做指令微调数据集时,实际用到的几个方法:数据清洗、多样性控制、难度分层。没有高深的技巧,都是反复踩坑之后留下来的实用做法。

原则一:先清洗,再扩量

很多人的第一反应是"数据不够就多采集"。但如果数据里充斥着重复条目、截断文本、格式异常,堆再多也只是在放大噪声。正确的顺序是:先把现有数据清洗干净,再考虑是否需要扩量。

去重

精确去重(hash 匹配)只能去掉完全相同的条目,对近似重复无效。比如同一个问题换个表述、同一段回答有几个字的差异,精确去重会全部放过。

更有效的方法是 MinHash LSH(局部敏感哈希)近似去重。思路是:把文本转成 n-gram,用 MinHash 生成签名,相似度超过阈值的认为是重复:

from datasketch import MinHash, MinHashLSH

def get_minhash(text, num_perm=128):
    m = MinHash(num_perm=num_perm)
    for word in text.split():
        m.update(word.encode("utf8"))
    return m

lsh = MinHashLSH(threshold=0.8, num_perm=128)
# 相似度 > 0.8 的视为重复,保留一条

实测下来,一个号称 100k 的指令数据集,去掉近似重复后通常只剩 60~70k 有效条目。

长度过滤

过滤掉以下两类:

格式校验

检查每条数据是否满足:instruction 字段非空、response 不是乱码、不包含未关闭的 HTML 标签或奇怪的转义字符。这步用正则表达式批量跑一遍,5 分钟的事。

原则二:多样性比数量更值钱

一个拥有 10 种场景各 1000 条的数据集,通常比只有 3 种场景共 5000 条的数据集效果更好。原因是模型的泛化能力来自于覆盖宽度,而不是某个场景的深度。

用 Embedding 可视化分布

把所有 instruction 用 sentence-transformers 编码成向量,然后用 UMAP 降到 2 维可视化。如果图上看到几个密集的"团",说明数据高度集中在几个话题,多样性不足。

from sentence_transformers import SentenceTransformer
import umap
import matplotlib.pyplot as plt

model = SentenceTransformer("paraphrase-multilingual-mpnet-base-v2")
embeddings = model.encode(instructions, show_progress_bar=True)

reducer = umap.UMAP(n_components=2, random_state=42)
coords = reducer.fit_transform(embeddings)

plt.scatter(coords[:, 0], coords[:, 1], alpha=0.3, s=5)
plt.title("Instruction Distribution")
plt.savefig("distribution.png", dpi=150)

看到分布之后,对密集区域做欠采样、对稀疏区域做补充,能让覆盖更均匀。

话题类别均衡

如果数据有类别标签(问答、代码、总结、翻译等),检查各类别的比例。占比超过 40% 的类别建议做欠采样,低于 5% 的补充数据或合成扩充。

原则三:难度分层,按课程投喂

直接把所有数据混在一起训练,效果通常不如按难度排序(Curriculum Learning)。原因是模型在训练早期如果碰到太难的样本,梯度很大、波动也大,容易走偏。

我用的分层标准很简单:

训练时前 1/3 epoch 只用简单样本,中间 1/3 混入中等,最后 1/3 全量训练。这个策略在多个实验里都带来了 eval 上 1~2 个百分点的提升,值得一试。

简单的难度估计方法: 用基座模型(不微调)对每条数据做一次推理,计算输出 token 的平均负对数似然(NLL)。NLL 越高,说明模型觉得这条数据越"难",可以用它做排序。

实用工具推荐


数据工程没有银弹,但这几个原则基本上是通用的。如果你在做 SFT 数据并遇到了具体问题,欢迎来聊。