本节摘要:官方 README 里最显眼的一句诚实声明:底座检查点不做微调直接使用,效果接近随机猜。本节先解释成因——laya 的检查点是 BERT 级编码器(第 2 章),参数量与预训练目标决定了它没有大模型那种广域先验;再划定例外——简单二分类、选项语义差异极大的任务可能勉强可用,但「勉强可用」不是上线理由;然后给一份部署前的最小验证集自测清单:五十到两百道题、覆盖真实分布、按类型分桶判定;最后用两个误用案例说明「laya 是微调框架,不是开箱模型」这句话的分量。这一节是全书的守门员:跳过它,后面所有章节都建在流沙上。
第 2 章介绍过三个检查点的底座:laya 421M 基于 ModernBERT-large,laya-multilingual 322M 基于 mmBERT。两个数字先摆出来(官方口径):421M 与 322M 的参数量,比主流生成式大模型小三个数量级。
| 角度 | 生成式大模型 | laya 底座检查点 |
|---|---|---|
| 参数量 | 数十亿到数千亿 | 322M~421M(官方口径) |
| 预训练目标 | 广域语言建模,见过海量任务形态 | 编码器表征,任务形态靠微调注入 |
| zero-shot 表现 | 靠指令泛化,多数任务可用 | 近随机(官方诚实声明) |
| 微调后表现 | 提升有限(已经很强) | 质变:typed-decisions 0.362→0.766(官方口径) |
关键在第二行:生成式大模型的广域先验是在预训练里「顺便」获得的,而 laya 的设计哲学是把这些先验全部让渡出去,换取毫秒级延迟与 CPU 可跑的部署形态(第 0.1 节的三轴)。所以近随机不是缺陷被藏起来了,而是架构选择的直接代价——官方把它写在 README 显眼处,本节只是把这句话翻译成工程语言。
两条能力曲线的分歧(示意,非精确数值) 效果 │ 生成式大模型(预训练即高峰) │ ─────────────────── │ ────┘ │ ────┘ laya(微调后才起飞) │ ──┘ ────────────── │ ──┘ ────┘ │随机线──┴──────────────────────▶ 投入(微调数据/轮次) └───────────────────────────── 结论:两条曲线的交叉点之后,laya 用小得多的算力拿到任务内的精度
实践中确实存在底座勉强能用的任务,条件大致三条:输出空间极小(是/否两档);选项之间语义差异极大(「合规」与「涉嫌违法」而不是「较好」与「略好」);输入文本短且模式固定。三条全中的任务,底座可能略高于随机。
但「略高于随机」依然不够上线资格,原因有二:其一,略高于随机意味着错误仍然过半,而决策场景的错误是有业务代价的(第 7.3 节的代价矩阵);其二,你无法区分「模型懂这个任务」和「模型碰巧在这批样本上蒙对」——没有验证集,这两个假设不可分辨。所以例外任务也要走本节第三部分的清单,只是验证集可以小一号。
最小验证集的目标不是全面评测(那是《Evals 实战:LLM 评测工程》第 07 章《概率系统评测》的领域),而是在上线前用最小成本回答「微调后的 laya 在我的任务上及格了吗」。
# min_eval.py —— 最小验证集自测:按类型分桶统计准确率与弃权率 import json from collections import defaultdict def run_min_eval(model, eval_path, pass_threshold=0.85): buckets = defaultdict(lambda: {"hit": 0, "total": 0, "abstain": 0}) with open(eval_path, encoding="utf-8") as fh: for line in fh: item = json.loads(line) pred = model.predict(item["context"], item["question"], item["options"]) bucket = buckets[item["type"]] # 按问题类型分桶 bucket["total"] += 1 if pred.get("abstained"): # min_confidence 弃权单独计数 bucket["abstain"] += 1 elif pred["choice"] == item["label"]: bucket["hit"] += 1 report, all_pass = {}, True for type_name, stat in buckets.items(): acc = stat["hit"] / stat["total"] ok = acc >= pass_threshold # 阈值按业务代价定(第 7.3 节) all_pass &= ok report[type_name] = { "样本数": stat["total"], "准确率": round(acc, 3), "弃权率": round(stat["abstain"] / stat["total"], 3), "及格": "是" if ok else "否", } return {"分桶报告": report, "全部及格": all_pass}
清单本体(标注为工程建议,非官方要求):
| 清单项 | 规模建议(示意) | 判定 |
|---|---|---|
| 题目总量 | 50~200 道 | 覆盖真实分布,别只挑难题 |
| 类型分桶 | 每桶至少 20 道 | 有任何一桶不及格就不上线 |
| 标注来源 | 业务标注而非模型生成 | 防止「模型考自己」 |
| 与微调集隔离 | 严格不重叠 | 重叠即作弊,分数无效 |
| 弃权率上限 | 按业务定(例如 10%) | 弃权爆表说明校准或阈值有问题 |
| 位置偏差抽查 | 抽 10 道做打乱重测(第 10.2 节) | 概率随位置剧烈漂移要预警 |
案例一:当通用分类器上线。团队拿到 laya,没做任何微调,直接接进内容审核流,理由是「BERT 应该天然懂分类」。两周后抽检发现错误率过半——这正是底座近随机的表现。正确路径是:圈定审核流里最狭窄的一个子任务(例如「是否涉人身攻击」),构造验证集确认底座不行,微调(第 5 章),再过一遍本节清单。
案例二:拿演示效果当生产效果。演示时用十个精选样本,微调后全对,于是直接放量。问题在于十个样本说明不了分布——长尾问题、混合语言、奇怪措辞都没出现。最小验证集的 50~200 道就是为了把「精选」挤掉,让长尾有机会暴露。
两个案例的共同教训就是本节标题的另一半:laya 是微调框架,不是开箱模型。它的价值主张(第 5.3 节的两个案例)全部建立在「你的数据 + 它的速度」上,跳过第一步就没有后面的故事。
| 问题 | 排查顺序 | 处置 |
|---|---|---|
| 微调后验证集仍不及格 | 数据量是否太少(几百条以下,示意量级)→ 标注是否有冲突(同类样本不同标签)→ 题面是否歧义(人工重读 20 条错题) | 先清标注再谈数据量;题面歧义的题拆改写 |
| 到底多少条微调数据起步 | 看任务封闭度:类目越少、措辞越规整,所需越少(数百条即可见效果,示意量级) | 从封闭子任务起步攒数据,再逐步放开 |
| 能不能先拿底座跑着攒数据 | 可以,但只当「采集期」不当生产 | 底座判定结果落盘为弱标注候选,人工抽检后再入训练集 |
| 验证集为什么不能顺便看微调效果 | 微调集看效果等于开卷考试,分数无意义 | 验证集与微调集物理隔离,文件名与流程上都要隔离 |
| 简单二分类也不达标 | 检查是不是把多个判断塞进了一道题(先分情绪再分紧急度) | 任务拆分:一道题只问一件事 |
| 及格线怎么定 | 不是拍 0.9:看错误代价(第 7.3 节代价矩阵)与人工兜底成本 | 代价高的桶线就高;个别难桶单独设线 |
排查的总原则与第 10.2、10.3 节一致:先怀疑数据与流程,再怀疑模型——微调框架的故障九成在喂数据的那一环。
下一节解决上线后的第一类可信度问题:同样的题目、同样的选项,只因为顺序不同,概率分布就变了——位置偏差怎么测、怎么补。