质量与多样性的兼得之策
引言
最近做 post-training 筛数据时反复遇到一个需求: 想从一个大池子里挑一个小子集, 既要分高, 又要在某些维度上分散, 比如 repo、tag、语言、时间。
本篇博客主要介绍一下我的思路, 其实把这个看成一个前AI时代的组合优化问题, 想要得到一个优解并没有那么难。数据的多样性不应该是一个"先采样什么什么, 看看分布, 再升降采样"这样的"人类启发式优化", 而是能够给出优性证明的稳定可复现流程。
基于一些数据之类的敏感性, 这个工具不会开源, 让你的Claude Code看完自己复现一个(doge
多样性需求
先说几个的场景, 直观感受一下"质量 + 多样性"到底在说什么:
- PR 池筛 SFT 数据: 1000 万 PR 里挑 1 万, 每条都打过质量分。直接 top-k 的结果是名额大头来自前几个仓库 — 模型只学会"那几个仓库的风格", 对其他仓库基本不泛化
- 多语言代码 SFT: 想要 Python / TypeScript / Go / Rust 比例大致均衡。某些语言的样本质量分普遍高, 直接按分排会几乎全是 Python
- 难度 benchmark 抽样: 简单/中等/困难三档各占 30%, 但模型在中等档输出更长更整齐, 质量分会更高, 直接选会偏到中等
- 去重 + 高分: 选 top-k 之后发现里面有一对 embedding 几乎重合; 或者同一 repo 三天内的两条PR 改了同一个文件
- 覆盖长尾: 数据集有 200 个 task tag, 不希望某些 tag 一 条都不选
这些场景的共同特征: 每条数据本身有个分, 但不能只看分。某种"结构"在里面 — 同 repo / 同 tag / 同时间窗 / 同语义簇 — 决定了我们要让候选在这个结构上散开。
把需求抽象一下
把上面这些场景翻译成数学, 我发现它们能整理成三类偏好:
1. 一元偏好 — 单条数据本身的加减分 "带 testing tag 的多加 0.3 分"、"质量分本身"、"主动学习里的不确定度奖励" — 这些都是 per-item 一个 delta, 跟其他条无关。
2. 多元对称偏好 — 同组数量的某种函数 "同 repo 不要太多"、"长尾 tag 至少保 3 条"、"label 比例 4:3:3" — 都是先按某个 key 分组, 然后对每组的"被选中数量"算一个罚或奖。关键在对称: 在同一组里, 哪几条被选中并不重要, 只看选了几条。这个对称性是它能写得简洁的根本原因 — 条数据展开成两两关系是 项, 但因为对称, 折叠成"每组的数量函数"就只跟组数有关。
3. 二元偏好 — 两条之间的关系 "embedding 余弦 > 0.9 的两条不要一起选"、"同 repo 3 天内的两条互斥"、"n-gram 重叠太大互斥" — 这些是真两两的关系, 没办法折叠成"组数量"。
第一类几乎零成本, 直接加进基础分。麻烦的是第二、三类 — 它们都是 上的非线性 / 高阶项, 不做妥协的话求解会爆炸。
设计选择: 哪里做妥协, 为什么是合理的
第一类一元偏好几乎零成本, 直接累加进基础分就行。麻烦的是第二、三类 — 它们都是 上的非线性 / 高阶项, 不做妥协的话求解会爆炸。和 Claude Code 脑暴一番后, 我做了三个非平凡的限制。这些限制让算法可解, 同时我认为它们恰好踩中了业务需求的结构。
限制 1: 二元偏好只支持稀疏图
二元偏好理论上是 的矩阵。如果稠密, , 不管什么算法都跑不动 ( 时矩阵都装不下)。我的限制是 — 平均每条最多跟常数条相关。
业务上其实非常合理: 实际场景里"哪两条要互斥"几乎都来自一些稀疏化的关系 — embedding 的 kNN (每条只跟最近 K 条相关), 时间窗 (每条只跟前后几天相关), LSH bucket (每条 只跟同 bucket 的几条相关)。"全 N*N 矩阵"这种在工程上本来也算不出来。
好处: 稀疏二元可以按连通分量切开, 每个分量独立求解 — 用并查集把禁忌图分成若干组, 组与组之间没有耦合。 时大部分分量就一个点, 这一档是 的; 真正费力的只有少数大分量 (典型 ), 用分支定界或小型 LP 处理。
限制 2: 多元偏好只支持组数量的对称函数
多元偏好理论上是 上的一般 set function 。这种太一般, 求解需要枚举所有子集。我的限制是只支持"按 key 分组, 对每组数量算一个标量函数"。
业务上借用了上面"对称性"的观察: 大部分多元需求 ("repo 散开" / "tag 比例" / "长尾覆盖") 在同一组内对哪几条被选中是无差别的。把这种对称结构显式地表达出来, 求解就从 量级降到 量级。
限制 3: 组数量函数限制成凹
"组里选越多越亏"的形状有很多种 — 凸的、凹的、阶梯的、V 形的。我只支持凹的。
这个限制纯粹是为了让对偶松弛紧 — 凹函数有切线上界, 切线斜率可以折进每条数据的有效得分, 组与组之间的耦合就消失了, 拉格朗日松弛紧, ratio 证明成立。
需要先把"凹"这个词说清楚, 因为它跟"递增递减"是两码事 — 凹 = 二阶差分 = 图像永远不"鼓起来", 跟函数本身是涨是跌没关系。直观说就是 marginal increment 不增。
业务上常见的形状几乎都是凹的:
| 业务含义 | 凹? | |
|---|---|---|
| 二次罚: 每多一条罚得更狠 | ✓ (二阶差分 ) | |
| 饱和罚: marginal 罚趋稳 | ✓ | |
| 双向贴近 (TargetDistribution) | ✓ ( 形抛物线) | |
| 覆盖饱和: 至少 条之后边际收益清零 | ✓ (分段线性) | |
| 熵最大化 | ✓ |
不凹的典型反例: 阶梯硬约束 ( 时 0, 超过给 ) — 在阈值 处先平后跌, 中间出现凸弯。
从偏好到算子
把上面的抽象写成代码, 引擎只需要识别三个算子:
| 算子 | 数学 | 用例 |
|---|---|---|
UnaryPreference | "给带 testing tag 的多加 0.3 分" | |
VariancePreference | , 凹 | "同 repo 不要太多"、"label 比例 4:3:3" |
SparseBinaryPreference |