news 2026/11/3 2:35:13

决策数据怎么造?Laya 三类原语 choice/score/noul 的标注规范与避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
决策数据怎么造?Laya 三类原语 choice/score/noul 的标注规范与避坑

决策数据怎么造?Laya 三类原语 choice/score/noul 的标注规范与避坑

Laya 是一个「不做生成、只做决策」的非自回归 System 1 决策引擎:输入一段结构化状态(邮件、工单、对话片段、JSON)和一组类型化问题,一次前向传播直接输出带校准概率的类型化答案,不产出任何自由文本。正因如此,它的微调数据与传统大模型训练语料有本质差异——你不需要「提示词—标准回答」的问答对,需要的是「状态 + 类型化问题 + 结构化答案」的决策记录。答案长什么样、边界怎么划、理由写在哪,直接决定了微调后模型在 typed-decisions 基准上的表现:同一个基准上,微调后的 checkpoint 准确率 0.766,而基础英文 checkpoint 只有 0.362,差距几乎全部来自数据侧。

这篇文章以仓库源码(rl_common.py、rl_agent_api.py、rl_agent_config.json、eval/results.json)为准,拆解 choice/score/noul 三类原语的标注规格、边界样本与「理由合理性」字段的落法,以及标注质量的一致性与去偏问题。

先读懂三类原语的解码语义,再谈标注

标注规范的第一课,是搞清楚模型最终会怎么解读你给的答案。Laya 的决策头在序列渲染时,把每个选项放在一个独立的[MASK]标记上,最后对这些标记打分并 softmax;三种原语在 rl_common.py 的render_options()中各有固定的渲染方式:

def render_options(q: Dict) -> List[str]: t, crit = q["t"], q.get("crit") if t == "choice": return [k if not v else "%s: %s" % (k, v) for k, v in crit.items()] if t == "score": return ["level %d: %s" % (i, c) for i, c in enumerate(crit)] crit = crit or {} return ["false: " + (crit.get("false") or "no, the statement does not hold"), "true: " + (crit.get("true") or "yes, the statement holds")]

这条渲染函数定义了三条硬性标注规范:

  • choice(类别选择):criteria是一个字典,key 是机器读的标签,value 是给模型看的语义描述,渲染为"key: description"。推理时返回 argmax 标签和全标签概率分布(rl_agent_api.py 中answers[qid] = {"type": "choice", "choice": keys[int(p.argmax())], "probabilities": {...}})。接口同时兼容criteria以列表形式传入,此时自动转为{c: None}——列表里的元素会成为裸标签,没有描述。
  • score(序数评分):criteria是一个有序列表,渲染为"level i: <描述>"。它的解码方式与 choice 完全不同——不是取 argmax,而是算概率加权期望:score = (np.arange(k) * p).sum(),同时附上legend: {str(i): c}。这意味着 score 天然是一个连续值,标注时「刻度之间的语义间距」必须大致均匀,因为模型学的是序数分布,不是离散类别。
  • noul(布尔判断):只有两个选项,且顺序被硬编码为false:在前、true:在后(p[1]即「是」的概率)。标注时不需要你指定正反标签——渲染顺序是写死的,你在criteria里只能提供false/true两端的描述文本,且都有默认值兜底。

三类原语的标注差异:标签、刻度与答案形态

理解了解码语义,标注差异就清晰了。训练记录的真实数据形态见 rl_common.py 的encode_record():一条记录包含state与一个qs数组,每个问题项是{"t": 类型, "ins": 指令, "crit": 标准, "y": 答案索引, "soft": 可选软标签}:

{ "state": {"from": "user@acme.com", "subject": "Duplicate charge on invoice #4411", "body": "Hi, we were billed twice for March. Please refund the duplicate today."}, "qs": [ {"t": "choice", "ins": "Which department should handle this request?", "crit": {"billing": "invoices, payments, refunds", "technical": "bugs, outages, system errors", "sales": "pricing, new contracts", "other": "everything else"}, "y": 0}, {"t": "score", "ins": "How urgent is this request?", "crit": ["not urgent", "soon", "critical deadline or blocking issue"], "y": 2}, {"t": "noul", "ins": "Does the user threaten to cancel or leave?", "y": 1} ] }

choice:标签命名与描述的信息量

choice 是三类原语中标注自由度最高、也最容易出问题的一类。README 的 typed-decisions 基准显示,微调后 checkpoint 按原语的准确率为 noul 0.857、choice 0.733、score 0.723——choice 明显弱于 noul,一个重要原因就是 choice 的答案空间由标注者自定义。

两个具体坑:

  1. 标签偏见:key 本身是模型可见的文本。"other"、"misc"、"something_else"这类标签,语义上几乎不含信息,模型很容易把它们当成「兜底垃圾筒」而过度分配概率。经验是兜底类也给出描述("other": "everything else"),并且把描述写成与正类并列的判据。
  2. 描述即隐式理由:choice 的 value 字段本质上是「这个选项成立的理由」。标注时应该写「什么样的状态会命中它」的判据,而不是「它是什么意思」的定义。"billing": "invoices, payments, refunds"之所以有效,是因为它覆盖了状态中出现的证据词。

另外注意选项预算:build_sequence()中每个选项文本先截断到 48 token,若总长超出head_max_len(英文 checkpoint 192、typed-decisions 256,见两个 rl_agent_config.json),则均等收缩每个选项至max(4, (head_max_len - 16) // n)。也就是说选项描述不是越长越好——过长会被硬截断,且高基数选项会互相挤占预算(README 实测 77 个标签时每个标签只剩 3–4 token,Banking77 上准确率跌到 0.425)。选项数超过 20 时,要么提高head_max_len,要么拆成两阶段 coarse-to-fine 的层级 choice。

score:序数刻度的间距与软标签

score 是所有原语里最微妙的一个。它在训练时的奖励是 log + spherical 再加上 ranked probability score(rl_common.py 的proper_reward(),对qtype == score额外减去w_rps * rps),RPS 直接惩罚预测 CDF 与目标 CDF 的平方距离——所以模型学的是整个序数分布,而推理时又输出加权期望。这带来一条硬约束:

刻度之间的语义距离要均匀。如果把["不紧急", "有点急", "非常急", "十万火急"]标成 0/1/2/3,模型会把它们当等距处理;实际业务里「有点急」到「非常急」的差距可能远大于「不紧急」到「有点急」。标注时应该为每个 level 写独立的判据描述,让相邻 level 的边界可判定,而不是靠标注者的主观感觉排序。

当边界确实模糊时,用软标签:encode_record()里target = list(q["soft"]) if q.get("soft") else one_hot(q["y"]),即支持soft字段直接给一个分布作为目标。这在多人标注意见分裂时特别有用——不要强行把 60/40 的分歧压成二选一,直接标成软分布,与 RLCD 的校准目标天然一致。

noul:最大的坑是「跟随选项标签」

README 的 Honest Limits 里明确记录了一个已知缺陷:noul 可能跟随选项标签而不是状态(issue #156)。noul的渲染是"false: ..."/"true: ...",在英文 checkpoint 上,这对标签本身的权重可能压倒状态内容,导致对明显正向的输入返回高置信度的「否」。这是典型的标注侧问题在推理侧暴露。

规避方式在 README 中给出:把 noul 改写为两选项 choice,用中性 key 加你的正反措辞作为描述:

{"type": "choice", "instructions": "Is this review positive?", "criteria": {"A": "yes, the review is positive", "B": "no, the review is negative"}}

在 rl_agent_config.json 的温度表里也能看到 noul 的特殊性:noul:2的校准温度是 1.983,是普通 choice(3–5 选项 1.76)之上的最大值——二元判断的输出分布天生更尖锐,标注时要特别注意正负样本的平衡,避免模型把「默认否」当成捷径。

边界样本与「理由合理性」字段的设计

传统 NLP 微调常给答案配一段「解释文本」,但 Laya 不做生成,它的「理由」载体是选项描述(criteria)与状态本身的构成。源码里没有独立的 reason 字段,理由合理性是通过三个机制体现的:

第一,criteria 描述就是可执行的判据。决策头对每个选项渲染[MASK] opt并在其上打分,模型实际在做的是「状态证据 vs 选项判据」的匹配。所以标注边界样本时,调整的不是标签,而是判据的措辞——把边界样本命中的证据词写进对应选项的描述里,能直接改变决策边界。

第二,状态侧结构(serialize_state)。rl_common.py 的serialize_state()会把 dict 状态json.dumps成字符串再进入序列。对邮件类状态,README 的推荐形态是{"from", "subject", "body"}结构化字段。标注时状态字段的命名要有区分度——subject与body各司其职,模型需要在「主题行证据」与「正文证据」之间做权衡,这正是边界决策的信息来源。

第三,指令与状态的 token 预算分配。build_sequence()的序列结构是:

[CLS] <type> instructions [SEP] [MASK] opt0 [MASK] opt1 ... [SEP] state [SEP]

指令文本被截断到head_ids[:max(8, opt_budget)],状态最多用max_len - head_max_len(英文 512-192≈320 token)。所以:指令要短而准(一句话说清判断目标),状态要保留与决策相关的全部证据。长文档场景则要走laya-multilingual的max_len=8192通道(README 实测 4000 token 内 16–18/20 正确,超过后波动明显)——标注长文档时同样要注意状态截断位置,truncate_left=True的对话场景会从左侧截断,即保留最新对话。

标注质量控制:一致性、去偏与校准

Laya 的训练与评估体系对标注质量非常敏感,仓库提供了完整的质量观测闭环(eval/results.json、eval/results.md)。

一致性的量化基线

typed-decisions 基准给出了两个关键参照:teacher 自我一致天花板 0.735,逐题多数类基线 0.461。微调 checkpoint 的 0.766 能超过 teacher 天花板——这意味着 400 个 case、2000 个决策的标注集一致性极高,标注分歧没有拖后腿。反过来,如果自己的标注集里「同一个状态、两个标注者给出不同标签」的比例超过一定阈值,模型训练时就会被互相矛盾的梯度撕扯。落地做法是:先做一小批双人标注,用一致性系数(如 Cohen's kappa)当准入门槛,分歧样本要么进soft软标签,要么开评审会统一判据。

去偏:三个可量化的观测点

  1. 训练期的选项打乱:encode_record()中,训练时非 score 问题会rng.shuffle(order)打乱选项顺序再构造序列——这强制模型不能依赖「选项位置」这类捷径,只能学语义。标注侧对应的纪律是:标签命名不要带序号感(option_a、option_b这类 key 会削弱打乱的去偏效果,因为序号信息仍残留在文本里)。
  2. 置信度与 ECE 的分裂检测:eval_in 23024 题 13 个任务族的整体 ECE 是 0.030,而零样本 4 个任务族(2400 题)的整体 ECE 是 0.204——标注过的领域校准优秀、未标注领域校准崩塌,这是「过拟合于标注风格」的直接信号。分族看更明显:in-task 的 sentiment and rating 准确率只有 0.442、ECE 高达 0.438,response quality scoring 准确率 0.581,而 intent and routing 达到 0.991——主观评分类任务的标注一致性天然差,这类数据必须给软标签或更多标注者投票,否则模型学到的就是一个方差极大的分布。
  3. 零样本评估必须单独建集:eval/results.md 明确 zero-shot 任务族「held out of training entirely」。去偏的正确验证方式是保留整族任务不进训练,看零样本准确率与 ECE——而不是随机抽 10% 的样本做 holdout,那只能验证记忆,验证不了泛化。

左图是 eval_in 上的可靠性校准(ECE 0.030):蓝线与理想校准线几乎贴合,仅高置信区间略偏上——标注一致性高的数据,训出来的概率分布才敢信。右图风险-覆盖曲线(AURC 0.083)说明按置信度筛样本可以稳定控制错误率,这正是生产环境置信度门控(gating)能用的前提。

校准是标注质量的下游放大器

即便数据标得好,模型出厂也是过自信的。README 记录了基座 checkpoint 的原始 ECE 高达 0.466,按(问题类型, 选项数)分桶拟合一个温度后降到 0.081。温度表存在 rl_agent_config.json 的temperature_by_options里,值得注意choice:11+的温度是 0.1006——高基数选项的原始分布过于平坦,需要剧烈锐化才能恢复区分度。这是高基数 choice 标注时的又一个隐形成本:选项越多,每个标签的标注质量要求越高,因为校准环节已经无法靠温度补救信息量不足的问题。

对比零样本曲线(整体 ECE 0.204)与上面的 eval_in 曲线(0.030),校准差距就是标注覆盖度的差距。如果某个任务族的零样本 ECE 显著偏高,说明该族数据在标注集里欠采样——优先补它,而不是加温度。

标注避坑清单

把上面的证据收敛成一张可直接对照的检查表:

  1. choice:key 要短、语义中性(别带序号);value 写「命中判据」而非「名词解释」;兜底类也写描述;选项 ≤ 20 个,超过就分层或调head_max_len;选项描述控制在 48 token 内,别写长句。
  2. score:level 语义间距均匀;每个 level 有独立判据;边界模糊用soft软标签;别把 score 当 5 分类用——它训练时吃 RPS 奖励,输出是期望值。
  3. noul:警惕跟随false:/true:标签的已知缺陷;正负样本要平衡;不放心就改写成两选项 choice 并用中性 key。
  4. 边界样本:调整证据(状态字段)与判据(criteria 描述)的措辞,而不是强行改标签;指令保持一句话;状态保留全部决策证据,长文档注意 8192 与截断方向。
  5. 质量控制:小批双人标注先过一致性门槛;分歧样本走soft;零样本评估按任务族整体 holdout;每族都盯 accuracy 与 ECE 双指标,主观评分族(sentiment、response quality)要多人投票。

数据决定天花板,模型只负责逼近它。Laya 的 0.362 → 0.766 之差,就是标注规范的差距——把 choice/score/noul 的语义、边界和一致性管住,微调只是最后一步。

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!