跳到主要内容

数据标注与数据飞轮

本节定位

模型效果的上限,很多时候不是由训练技巧决定的,而是由数据决定的。

尤其是大模型微调里,常见的情况不是:

  • 方法不够新

而是:

  • 标注口径不一致
  • 正负样本边界含糊
  • 线上失败样本没有回流

所以这节课要解决的是一个更根本的问题:

怎样把“数据越来越好”做成一条持续运转的飞轮。

学习目标

  • 理解标注不只是“打标签”,而是在定义任务边界
  • 知道如何设计标签体系、标注规范和质检流程
  • 学会用一致性指标和难例筛选来检查数据质量
  • 理解数据飞轮是如何把线上失败样本转成下一轮训练资产

一、为什么说“数据标注”本质上是在定义任务?

1.1 标签不是文员工作,而是产品决策

假设你的任务是“客服回复质量分类”。

如果你只给标注员一个标签名:

  • 好回复
  • 坏回复

那每个人心里的标准都不同:

  • 有人看礼貌
  • 有人看是否解决问题
  • 有人看是否符合政策

最后模型学到的就会是一锅混合标准。

所以标签真正应该回答的是:

  • 什么算对
  • 什么算错
  • 边界情况怎么判

1.2 类比:模型不是在学标签名,而是在学你背后的规则

你可以把每一条标注数据想成:

  • 一个经过人类裁决的案例

模型看到的不是“safe/unsafe”这几个字,
而是你通过大量案例暗中表达的判断标准。

因此如果规则本身模糊,
模型不可能学得清楚。

1.3 为什么很多微调项目会卡在这里?

因为团队常常会高估“标签名”的清晰度,低估“标注规范”的重要性。

真正能稳定提升数据质量的,往往不是标注平台,而是:

  • 标签定义
  • 正反例
  • 边界例
  • 复核机制

二、先把标签体系设计清楚

2.1 标签尽量和业务动作对应

一个好标签体系,最好能自然映射到后续动作。

例如在客服审核任务里,
比起只分:

更实用的标签可能是:

  • correct_and_polite
  • correct_but_too_brief
  • policy_violation
  • hallucinated_promise

因为这样的标签更利于后续:

  • 错误分析
  • 数据补充
  • 定向微调

2.2 边界样本一定要单独写规则

新人最容易忽略的是:

  • 明显正例
  • 明显负例

通常不难标。

真正难的是:

  • 部分正确
  • 语气礼貌但事实错
  • 拒答方向对,但措辞生硬

这些边界例如果不写清楚,
一致性一定会掉。

2.3 什么时候该做分类标签,什么时候该做偏好对比?

如果你的任务重点是:

  • 明确类别
  • 明确是否违规

分类标签通常更自然。

如果你的任务重点是:

  • 两个回答谁更好
  • 风格谁更符合预期

偏好对比往往更稳定。

也就是说:

  • 分类更适合“绝对标准”
  • 偏好更适合“相对优劣”

三、先跑一个真正有用的数据质检脚本

下面这段代码会做三件现实中非常常用的事:

  1. 计算两个标注员的一致率
  2. 计算 Cohen's kappa
  3. 找出需要进入下一轮复核或补标的样本
from collections import Counter

records = [
{
"id": 1,
"text": "可以先去重置密码,再尝试重新登录。",
"label_a": "good",
"label_b": "good",
"model_confidence": 0.93,
},
{
"id": 2,
"text": "你自己去查吧。",
"label_a": "bad",
"label_b": "bad",
"model_confidence": 0.91,
},
{
"id": 3,
"text": "已经发货也一定可以秒退。",
"label_a": "bad",
"label_b": "good",
"model_confidence": 0.52,
},
{
"id": 4,
"text": "订单完成后可在发票中心申请开票。",
"label_a": "good",
"label_b": "good",
"model_confidence": 0.51,
},
{
"id": 5,
"text": "我不确定是否支持改地址,建议联系人工客服确认。",
"label_a": "good",
"label_b": "bad",
"model_confidence": 0.47,
},
]


def agreement_rate(labels_a, labels_b):
matches = sum(a == b for a, b in zip(labels_a, labels_b))
return matches / len(labels_a)


def cohens_kappa(labels_a, labels_b):
n = len(labels_a)
observed = agreement_rate(labels_a, labels_b)

counter_a = Counter(labels_a)
counter_b = Counter(labels_b)
all_labels = sorted(set(labels_a) | set(labels_b))
expected = sum(
(counter_a[label] / n) * (counter_b[label] / n)
for label in all_labels
)

if expected == 1:
return 1.0
return (observed - expected) / (1 - expected)


labels_a = [row["label_a"] for row in records]
labels_b = [row["label_b"] for row in records]

print("agreement =", round(agreement_rate(labels_a, labels_b), 3))
print("kappa =", round(cohens_kappa(labels_a, labels_b), 3))

needs_review = [
row for row in records
if row["label_a"] != row["label_b"] or row["model_confidence"] < 0.6
]

needs_review = sorted(needs_review, key=lambda row: row["model_confidence"])
print("\nreview queue:")
for row in needs_review:
print(
f"id={row['id']} confidence={row['model_confidence']:.2f} "
f"labels=({row['label_a']}, {row['label_b']}) text={row['text']}"
)

3.1 为什么这段代码不是“废示例”?

因为它对应的是数据团队每天都会做的三件事:

  • 看标注员是否一致
  • 看模型在哪些样本上最不确定
  • 把争议样本拉出来重点复核

如果你只盯“总样本量”,却不看这些信息,
数据质量会很容易停留在表面。

3.2 为什么 agreement 还不够?

因为有时类别非常不平衡。
例如 90% 样本都属于 good
那两个标注员即便都很偷懒,也能得到看起来很高的一致率。

这就是为什么很多团队还会看:

  • Cohen's kappa

它会尝试扣除“碰巧一致”的成分。

3.3 为什么低置信度样本要进复核队列?

因为这类样本往往意味着:

  • 模型拿不准
  • 规则边界模糊
  • 或者样本本身比较脏

它们正是下一轮数据增益最大的地方。


四、什么叫“数据飞轮”?

4.1 最小闭环长什么样?

一个典型的数据飞轮通常是:

  1. 模型上线
  2. 收集失败样本
  3. 清洗和去重
  4. 复标或补标
  5. 加入下一轮训练集
  6. 再评估、再上线

飞轮的重点不是“循环”这两个字,
而是每一轮回来的数据都更贴近真实问题。

4.2 为什么线上失败样本特别值钱?

因为它们往往具备两个特点:

  • 来自真实用户
  • 正好打中当前系统最薄弱的地方

和人工凭空编一批样本相比,
这种数据更有针对性。

4.3 飞轮最怕什么?

最怕三件事:

  • 失败样本收不上来
  • 收上来后没人分类归因
  • 归因后没有进入下一轮训练或评估

如果只收集、不回流,
那就不叫飞轮,只是积压。


五、怎样把飞轮做得更稳?

5.1 先做失败类型分桶

把线上问题分成几类,常常比单纯堆样本更有效。

例如:

  • 格式错误
  • 幻觉
  • 政策违规
  • 过度拒答
  • 漏关键字段

这样下一轮你就知道该补哪一类数据。

5.2 再做去重和代表性采样

真实线上数据很容易重复。
如果用户大量重复问同一类问题,你不应该机械地把所有样本都塞回训练集。

更好的做法通常是:

  • 去掉近重复
  • 保留代表性样本
  • 给稀有但高风险问题更高优先级

5.3 别忘了版本管理

每一轮数据最好都记清楚:

  • 从哪里来
  • 为什么加入
  • 属于哪一类错误
  • 是否已经人工复核

否则到后面你会很难回答:

这次提升到底是因为方法变了,还是因为数据变了?


六、标注规范到底该写到什么程度?

6.1 至少要有正例、反例和边界例

一个好规范通常至少包含:

  • 标签定义
  • 适用条件
  • 明确正例
  • 明确反例
  • 易混边界例

6.2 最好能回答“为什么”

如果规范只写:

  • 遇到这种情况打 bad

但没写为什么,
标注员在遇到相似但不完全相同的情况时就会摇摆。

6.3 规范本身也要迭代

随着项目推进,你会不断发现:

  • 旧规则覆盖不到的新场景
  • 原标签过粗
  • 两个标签容易混淆

这时要更新的,不只是数据,
还有规范本身。


七、这些误区特别容易踩

7.1 误区一:先大量标,再说规则

规则没定清楚就大规模开标,
通常会导致返工量极大。

7.2 误区二:只盯一致率,不看争议原因

一致率低只是表象。
更重要的是知道:

  • 是规范不清
  • 还是样本太脏
  • 或者标签体系本身就不合理

7.3 误区三:把飞轮理解成“不断加更多数据”

飞轮不是纯堆量,
而是不断把最有价值的失败样本转成高质量训练资产。


八、小结

这一节最重要的结论是:

数据标注不是微调前的一道杂务,而是任务定义、质量控制和持续迭代能力的核心。

真正有生命力的数据体系,通常同时具备三件事:

  1. 规则清楚
  2. 质检到位
  3. 失败样本能稳定回流

当这三件事都成立时,
你的模型质量才会出现持续、可解释的提升。


练习

  1. 为一个你熟悉的任务设计 3 到 5 个标签,并写出每个标签的正例和反例。
  2. 参考本节代码,自己手动构造一批双人标注数据,算一下一致率和 kappa。
  3. 想一想:你的项目里哪些线上失败样本最值得优先回流到训练集?
  4. 如果两个标注员总在同一类样本上争议,你会先改规范、改标签体系,还是直接投票裁决?为什么?