在你自己的决策上微调 Laya
在你自己的决策上微调 Laya
在 typed-decisions 基准上,基础 checkpoint 零样本的得分接近随机 —— 0.36 和 0.35,对 0.318 的 随机基线而言 —— 而微调后的 checkpoint 在同一批 2,000 个决策上达到 0.766,高于 TypeSafe Jev 已发布的 0.727,也高于 0.735 的教师自一致上限。微调承载了大部分价值,公开的 微调 notebook 在 Kaggle 免费的 2xT4 GPU 上跑完整个循环:构建数据集、用 RLCD 训练、拟合校准温度、评估,并把 结果推到 Hub。这一页讲解那个 notebook,并指出当数据换成你自己的之后,哪些部分仍然关键。
另一个完整示例 —— 在单张 16 GB GPU、不花钱调 API 的情况下训练一个浏览器智能体决策头 —— 在 把 Laya 微调成浏览器智能体决策头。
notebook 按顺序做了什么
| # | 步骤 | 发生的事 |
|---|---|---|
| 1 | 环境 | 断言两张 T4 GPU 都可见且已分配 |
| 2 | 安装 | laya、transformers、datasets 以及训练依赖 |
| 3 | 预处理 | 1,200 个训练用例(6,000 个类型化决策)变成带软目标的 tokenized 条目,写成磁盘文件供两个 DDP rank 使用 |
| 4 | 训练 | 在 torchrun --nproc_per_node=2 下跑 train_ddp.py,四个 epoch |
| 5 | 校准 | 每种类型一个温度,在训练前就留出的切片上拟合(在训练脚本内部,最后一个 epoch 之后) |
| 6 | 评估 | 微调后的 checkpoint 回答官方 test 划分 —— 400 个用例、2,000 个决策 —— 并给出每用例延迟 |
| 7 | 指标 | 准确率、软准确率、Brier、ECE、score MAE、档位内一位、KL/TV 和延迟分位数;一张与 Jev 和教师上限正面比较的表 |
| 8 | 发布 | (可选)用这次运行自己的数字生成一张模型卡,把文件夹上传到 Hub |
| 9 | 报告 | benchmark_report.json,含指标表和各工作流的准确率 |
Kaggle 设置:Accelerator 选 GPU T4 x2,Internet 设 On。输出落在
/kaggle/working/laya_finetuned_typed_decisions。
训练配方
RLCD 在基准的金标分布上训练,而不是在硬标签上:每个条目都带上教师给每个选项分配的概率, 损失的两半都读这个目标 ——
- 一个策略梯度项,作用在采样得到的带噪 logit 投影上(GRPO 风格:每个条目四个样本,探索 噪声从 0.4 退火到 0.1),由适当的评分规则给奖励(球面 0.75,排序概率 1.0);
- 一个全权重的软交叉熵项,对着同一个分布。
notebook 为 16 GB 显卡设置的旋钮:
| epochs | 4 |
| 有效批大小 | 64 个序列(每微批 8 个,2 张 GPU,4 步累积) |
| 学习率 | encoder 2.5e-5,head 1e-4 —— AdamW,余弦调度 |
| 内存 | fp16 autocast,encoder 和 head 上开梯度 checkpointing,梯度范数裁剪 1.0 |
| 序列预算 | max_len 1024,head_max_len 256,max_tokens_per_batch 4096 |
2xT4 上的运行时间,演示是分钟级,真实数据是小时级:演示的 6,000 个决策大约 4–6 分钟,而对约 30k 个问题跑四个 epoch 大约 4–5 小时。
要把它指向你的数据,替换那两个 load_dataset 调用,并保持行 schema:每个用例带 state、
questions 和 gold(教师对每个问题给出的概率),预处理器把它们变成条目。问题类型是
choice、score 和 noul;任何你能用它们对某个状态表达出来的东西都可以。
校准是这次运行的一部分
这是照搬循环时最容易被丢掉的一步,而一旦有人用置信度做门控,它就是关键。
notebook 在把训练数据分片到各个 rank 之前,先切出一份校准切片(最多 400 个条目,或 10%, 固定种子,每个 rank 上完全一样)。在运行已经训练过的条目上拟合温度,测的是拟合本身而不是校准 —— 模型在那些条目上几乎必然正确,优化器没有什么需要软化,于是返回一个退化的 scale。
最后一个 epoch 之后,rank 0 用 LBFGS 在 log 温度上拟合每种问题类型一个温度(choice、
score、noul),钳制在 [0.1, 10](切片不足十个条目时为 1.0,拟合抛异常时为 1.2)。
这些值作为 temperature 写进 rl_agent_config.json,notebook 在同一次写入里移除任何继承来的
temperature_by_options:那些旧的分桶值在推理时优先,会静默盖掉新的拟合。
温度缩放不改变 argmax —— 也不改变准确率;变的是置信度。checkpoint 按交付状态是过度自信的, 所以依赖任何阈值之前先拟合,并在留出数据上评估结果,再声称有改进。配置持久化的回归测试不需要 下载或训练就能跑:
python tests/test_calibration_persistence.py
信任它之前先评估
评估是对官方测试划分的一次完整遍历:400 个用例、2,000 个决策,横跨 Agent Trace
Observability、Customer Service、Invoice Processing 和 Security Incidents。它计算准确率、软
准确率、Brier、ECE(通过 laya.common.ece_score)、score MAE、档位内一位和延迟分位数,然后
构建一张正面比较表,其中的参考行是固定的:
| model | kind | accuracy | ECE |
|---|---|---|---|
| TypeSafe Jev 1.13.0 | general | 0.727 | 0.144 |
| ModernBERT-base (149M) | specialist | 0.646 | 0.179 |
| Teacher Self-Agreement | ceiling | 0.735 | — |
| Laya (published checkpoint) | fine-tuned | 0.766 | — |
你自己运行里的 Laya 行也以同样方式计算 —— notebook 用这次运行自己的数字重建那张表。两个值得
照搬的习惯:把你关心的切片(一种语言、一个工作流)留在留出数据里,并把校准和准确率一起报告,
因为训练信号是一个分布,而不只是一个标签。等你有了数字,仓库的
Discussions 是分享它们的地方;基准和已知
限制在仓库根目录的 BENCHMARKS.md 里。
推到 Hub
发布那一格是循环的最后一公里,它刻意做得无聊:
- 在 Kaggle 里放一个可写的
HF_TOKEN(Add-ons → Secrets)。缺了它那一格会抛出错误,并给出 确切的指示。 - 设置目标仓库 —— 交付的那一格默认用项目自己命名空间里的一个名字,所以运行前先改掉它。
- 运行它。它写出一张模型卡,卡上的数字来自这次运行的对比表,然后上传
model.safetensors、encoder/、tokenizer/、rl_agent_config.json、那张卡和基准报告。
结果加载起来和任何其他 checkpoint 一样 —— 没有微调专用的 API:
import laya
agent = laya.load("your-org/your-checkpoint") # the repo you just pushed
result = agent.predict(state, questions)
一个滚动更新的 checkpoint_latest/ 在每个 epoch 后会被覆盖,所以 Kaggle 超时或 OOM 的代价是
一个 epoch,而不是整次运行。
要当心什么
- 循环的好坏取决于目标。 RLCD 在你的问题上模仿教师的分布;训练前(或训练的同时)收集教师 置信度,并把它们的质量当作上限。
- 校准切片小是故意的。 最多 400 个条目或 10% —— 够拟合三个按类型的标量,不够用来验证。 自己留出一份评估数据。
- 你的标签必须能放进三种原语。 如果你的决策不是 choice、不是量表、也不是是非概率,先把 它塑造成其中一种。两个尖锐的边缘已经有记录:选项多会削弱置信度的选择 (#394),强制选择的否定可能跟着问题走 而不是跟着状态走(#377)。
- 要交付配置,不只是权重。 被移除的
temperature_by_options就是那种如果在一个照搬的配置 里存活下来、就会静默让校准失效的东西。