AIGC标识 Constitutional AI 数据清洗:一次去重,为什么把训练目标改了?

把多轮修订结果整理成训练数据时,一段清洗代码很容易显得无害:

pairs = list(dict.fromkeys(pairs))

它保留顺序,也没有丢掉任何“不同的答案”。但它仍可能改变训练目标:重复样本的次数,本来就是经验分布的一部分。

这个问题可以用 Constitutional AI 的修订数据来拆解。本文只讨论一个可验证的数据准备组件,不复现完整训练,也不以结构检查代替安全性评估。

先分清:修订过程和训练目标不是一回事

在 Constitutional AI 论文 §3.1 的监督阶段,模型产生批评和修订,再用各轮修订构造监督数据。论文还混入 helpfulness 样本;这里单独分析修订这一支。

假设一个提示 Q 的初稿是 D0,修订轨迹如下:

第 1 轮:D0 → A
第 2 轮: A → A
第 3 轮: A → A
第 4 轮: A → B

监督目标:A、A、A、B
去重之后:A、B

第二、三轮没有改变答案文本,不代表数据记录不存在。这四条记录可能有不同的原则或批评;是否保留它们,需要训练数据规范决定。

这里采用的练习契约是“每轮保留一个目标”。因此输出必须是四个 (Q, revision),不是两个唯一答案,也不是只取最终答案 B。这个精确保留策略是练习约定;论文提到使用各轮修订,不应据此推断它公布了所有去重和字符串清洗细节。

一个不用训练大模型就能算清的反例

固定同一个提示,把完整答案简化成 A、B 两个类别。设模型给 A 的概率是 q,给 B 的概率是 1−q;每条样本等权,最小化平均负对数似然。

保留次数:L(q) = −¾ log(q) −¼ log(1−q)
唯一答案:L(q) = −½ log(q) −½ log(1−q)

第一种目标的最优点是 q=0.75,第二种是 q=0.50。去重并非只是节约存储,它改变了两个答案的相对权重。

下表是同一套候选概率在两种目标下的损失,单位为自然对数:

q = P(A) 原始四条记录 去重后的两条记录
0.50 0.693147 0.693147
0.75 0.562335 0.836988

这不是一次语言模型实验的指标,而是一个两类别玩具模型的精确计算。真实 SFT 还受序列长度、token 掩码、损失归一化、采样器和其他数据混合比例影响;“三条记录”不自动等于实际训练中恰好三倍的梯度贡献。

下面的 Python 可以直接复算:

import math
from collections import Counter

def mean_nll(targets, q):
    if not targets or not 0 < q < 1:
        raise ValueError("need samples and 0 < q < 1")
    if any(target not in ("A", "B") for target in targets):
        raise ValueError("unknown target")
    return -sum(math.log(q if y == "A" else 1 - q)
                for y in targets) / len(targets)

targets = ["A", "A", "A", "B"]
unique = list(dict.fromkeys(targets))
print(Counter(targets))
for q in (0.50, 0.75):
    print(f"{q:.2f}: {mean_nll(targets, q):.6f}, "
          f"{mean_nll(unique, q):.6f}")

assert mean_nll(targets, .75) < mean_nll(targets, .50)
assert mean_nll(unique, .50) < mean_nll(unique, .75)

如果目的确实是压缩存储,可以保留计数权重:A:3, B:1。按计数加权后再除以总计数,能在这个等权、逐样本平均的目标上恢复原始损失。只保存唯一文本而丢掉计数,则无法恢复。

数据准备函数应当验证什么

一个小而明确的契约,比“智能清洗”更适合审查:

  1. 首轮 source 必须精确等于初稿。
  2. 后续 source 必须精确等于上一轮 revision。
  3. principle 必须在允许的原则 ID 集合中。
  4. 每轮都产出原始提示和修订答案,保留顺序、重复、空字符串及空白。

在输入字段类型已经符合契约的前提下,实现如下:

def revision_training_pairs(prompt, initial, principles, rounds):
    allowed = set(principles)
    current = initial
    pairs = []
    for item in rounds:
        if item["source"] != current or item["principle"] not in allowed:
            raise ValueError("broken revision chain or unknown principle")
        current = item["revision"]
        pairs.append((prompt, current))
    return pairs

rounds = []
source = "D0"
for answer in ["A", "A", "A", "B"]:
    rounds.append(dict(source=source, principle="clarity",
                       critique="Review wording", revision=answer))
    source = answer

assert revision_training_pairs("Q", "D0", ["clarity"], rounds) == [
    ("Q", "A"), ("Q", "A"), ("Q", "A"), ("Q", "B")
]
assert revision_training_pairs("Q", "D0", [], []) == []

blank = [dict(source=" x ", principle="p", critique="not a target",
              revision="")]
assert revision_training_pairs(" Q ", " x ", ["p"], blank) == [(" Q ", "")]

for bad in (
    dict(source="wrong", principle="p", critique="c", revision="y"),
    dict(source="x", principle="unknown", critique="c", revision="y"),
):
    try:
        revision_training_pairs("Q", "x", ["p"], [bad])
    except ValueError:
        pass
    else:
        raise AssertionError("invalid chain was accepted")

注意,critique 解释修改理由,但不是这里的监督答案。source 负责把修订串起来,也不代替与答案配对的原始提示。

空字符串是否应进入真实训练,是上游质量策略的问题。这里保留它,是为了让数据准备组件忠实执行契约,避免暗中掺入另一套过滤策略。如果需要丢弃空答案,应增加可审计的过滤阶段和计数,而不是靠 if revision: 顺手删除。

同理,.strip() 也不总是无害。代码缩进、Markdown 代码块和精确匹配任务中的空白可能有意义;清洗规则应与训练目标、序列化格式共同制定。

什么时候应该去重?

误采集、日志重放或爬虫重复导致的重复记录,完全可能需要删除。问题在于,应先识别“重复”的来源和计数单位,再决定处理方式。

至少要回答:去重键是完整 (prompt, revision),还是只有 revision?重复来自不同修订轮,还是同一个事件被重复导入?训练希望按轮、按提示,还是按唯一答案加权?若去重后保留权重,采样器是否真的使用了它?

只按答案文本跨提示去重尤其危险:两个不同问题都回答“是”,显然不是同一条条件训练样本。相反,即使保留所有修订,修订轮数较多的提示也可能获得更高总权重;如果目标是每个提示等权,还需要显式归一化。

我把这个组件放进自己维护的 TensorDrill:构建 Constitutional AI 修订训练对。配套反馈学习专题可以交互检查修订链。练习只验证数据组织规则,不判断答案是否无害,也不训练模型。

数据清洗前,先把“重复行是否代表重复权重”写进契约。否则,代码看起来更干净,优化的却可能已经是另一个目标。

参考资料

posted @ 2026-10-22 13:06  dijefff  阅读(4)  评论(0)    收藏  举报