20 帧画面,模型答了 20 次,每一次都答同一个答案。准确率 10/20。
正常人到这里就该关掉窗口了:一个二选一的任务,它连抛硬币都不如——抛硬币起码还会换个面。
可我把它的原始概率导出来画了条 ROC,AUC 0.949。
这两个数字是同一批调用、同一个模型、同一批帧跑出来的。下面讲怎么会这样,以及为什么这事在你测任何一个"读分"型决策模型时都会发生。想直接拿判据的看第 2 章,那是一个加法和一个减法;想看我自己在这上面翻的两次车,第 3 章和第 5 章。
1. 先说清楚在测什么
有一类玩法最近被包成开源项目到处发:不让大模型生成句子,只让它做一次前向,然后直接读几个候选词的概率,谁高选谁。设 max_tokens=1,要一份 top logprobs,对着 A/B/C 几个字母做 softmax,完事。快,因为一个 token 都不用解码。
我拿它干一件具体的事:给模型看一帧游戏画面——就是那个上下管道中间穿缝的小鸟——问它现在该拍翅膀还是该等。只喂图,不给任何坐标数值。候选两个词:flap 和 wait。
先说我翻的第一次车,因为它比后面所有数字都重要。
我最初问的是"鸟在缝的上方还是下方"。去偏之后 512×640 分辨率下 19/20,p50 87ms,三档分辨率延迟都压在 80ms 上下。我当时挺高兴。
他看完说:问在上在下,是你替它做了半步推理。
对。真正的任务是出动作,不是描述位置。"在缝下方"到"该拍翅膀"这一跳是我脑子里替它跳的,题面直接把答案的一半送出去了。一个评测如果把中间结论当成问题问出去,量的就不是你要的那个能力。 换成裸问 flap / wait,就掉到了开头那个 10/20。
2. 偏置量:一个加法,一个减法
掉到 10/20 之后我没直接判死,去看了 logprob。数字长这样:
- 真答案是 flap 的帧上,
flap比wait领先 +3.24 nats - 真答案是 wait 的帧上,
flap仍然领先 +0.59 nats
第二行是关键。该等的时候它也说拍,只是没那么使劲说。
把这两个数拆成两半,读者拿自己的数据也能算:
偏置量 offset = (领先幅度₊ + 领先幅度₋) ÷ 2
分离度 sep = (领先幅度₊ − 领先幅度₋) ÷ 2
这不是什么经验规律,就是一个加法和一个减法。代进去:
offset = (3.24 + 0.59) ÷ 2 = 1.915 nats
sep = (3.24 − 0.59) ÷ 2 = 1.325 nats
判据一句话:offset 比 sep 大,argmax 必定全倒向一边。 因为把整条分数轴平移 1.915,而两类中心距离原点只有 1.325,两坨点会被一起推过 0 这条线。实测 20/20 全答 flap,和这个不等式对上了。
offset 还能翻译成人话。过一下 sigmoid:
1 ÷ (1 + e^−1.915) = 87.2%
这个模型在还没看图的时候,就已经有 87% 的倾向想说 flap。 它有口头禅。
而 sep = 1.325 nats 意味着看完图之后,它确实能把两类推开——换算过去大约是 79% 对 21% 的赔率差。信号是真的,只是被口头禅整个推到了同一侧。
所以修法是把那条线挪到该在的地方,换模型没用。我拿 20 帧标定了一个阈值,剩下 30 帧全新测试:27/30,AUC 0.949。同一个模型,同一批调用,没改一个字的提示词。
3. 第二次翻车:它爱的可能是字母 A
在"上方/下方"那一轮,我跑出过另一组更难看的数:
| 选项映射 | 正确数 |
|---|---|
| A = 上方 | 11 / 12 |
| A = 下方 | 3 / 12 |
同一批帧,同一个问题,只把两个选项调了个位置,准确率从 92% 塌到 25%。
这个也能算。把两次实验的"选 A 率"平均出来:
选A率 = [正确率(A=X) + (1 − 正确率(A=Y))] ÷ 2
= [11/12 + 9/12] ÷ 2 = 83.3%
偏离 0.5 多少,就是字母偏置多少。这里偏了 33 个点。两轮平均准确率 58.3%,而选 A 率 83.3%——后者离 50% 更远,说明主导它输出的是字母的位置,不是画面的内容。
顺带说一句,这是为什么"选项反序重测"必须是读分模型的默认动作。只做一遍,你根本分不清模型在看题还是在背 A。
4. 换一个商用云端 API,偏置方向反了
这时候很容易得出一个结论:本地小模型不行,人家云端的专用决策服务没这毛病。
我当时确实顺口说了一句"人家接口内部做了校准"。他反问:你怎么知道?
我不知道。那句话是从"它返回的是归一化概率,用起来顺手"脑补出来的,没有任何证据。收回,当场测。
同样那 30 帧的数值状态,丢给一个商用云端决策 API,裸 argmax:
总计 21 / 30 (70%)
该 wait 的 15 帧:15 / 15 全对
该 flap 的 15 帧:6 / 15
70% 看着像个能用的模型。拆开看,它对一类全对、对另一类基本全错——它在押 wait,方向跟本地模型正好相反。把判定阈值挪到 0.3 附近,28/30。
所以两边都有口头禅,只是一个爱说 flap、一个爱说 wait。方向不可预测,存在与否可以预测——只要是单次前向读分,就先假设它有偏置。 校准这一层属于连接层,换哪个引擎都得做一遍,做完都受益。
5. 拆穿别人的数字
上面那个 21/30 是一件武器。
一份 benchmark 报"二分类准确率 70%",你什么都不知道。同样是 70%,可能是两类各对 70%,也可能是一类 100% 另一类 40%。后面这种情况下模型根本没在判断,它在押注,而测试集恰好一半一半,押注就能白捡 50 分。
判据:
看任何一份二分类/多选成绩,先问两件事:
① 两类的 recall 各是多少?差超过 20 个点,先当偏置处理,别当能力读。
② argmax 的输出分布是什么?全倒向一边,准确率这个数就已经作废了。
这两个问题都不需要你重跑实验,对方的结果文件里就有。我这次的 15/15 对 6/15,两类 recall 差了整整 60 个点——准确率 70% 和"押注一边 + 测试集正好对半开"给出的数字完全长得一样,只有混淆矩阵能把它们分开。
反过来,这也是为什么报 AUC 比报准确率诚实。AUC 只看排序,不受那条判定线摆在哪里影响。我这个模型 argmax 准确率 50%、AUC 0.949,两个数一起摆出来,读者立刻知道"能力在、阈值歪"。只报前者是埋没,只报后者是吹牛。
6. 这次实验的毛病
不标出来的数字,你应该默认不信。这几条我自己先说:
- 样本太小。 20 帧标定 + 30 帧测试。AUC 0.949 这个数的置信区间很宽,别拿它当精确值,只能读作"排序能力明显存在"。
- 帧是合成的,不是真实截图。 我画的是干净的合成画面,真实渲染里有背景、有抖动、有粒子,偏置量大概率要重测。
- 阈值在同一分布上标的。 换分辨率、换场景、换模型版本,这个 0.3 或者那个 offset 全部作废,要重标。这不是一次性校准。
- 只测了两个模型。 一个本地、一个云端,方向相反。n=2 支持"这类模型普遍有偏置"这个猜想,不足以证明它。
- "在上还是在下" 19/20 那个数别拿去用。 上一章说过了,那个问法自带半个答案。留在这里只是为了说明我怎么发现问题的。
- 延迟只测了单路 p50 87ms,没测并发下的分布。
7. 照着做的清单
下次拿到一个读分型决策模型,按这个顺序走:
- 先看 argmax 的输出分布,再看准确率。 全倒向一边,准确率这个数没有信息量,直接跳到第 3 步。
- 把选项对调,重跑一遍。 算两轮平均的选 A 率,偏离 50% 多少就是字母偏置多少。这一步花不了十分钟,能省掉后面所有的自我怀疑。
- 导出两类的 logprob 差,算 offset 和 sep。 offset 大于 sep,argmax 必然废,别再调提示词了,去调阈值。
- 留一小批标阈值,剩下的当测试集。 标定集和测试集不能重叠,否则 AUC 是你自己发给自己的奖。
- 报 AUC,同时报两类各自的 recall。 单一准确率在这类任务上是最没信息量的报法。
- 换引擎重来一遍。 偏置方向不跨模型迁移,本地爱说 A 不代表云端也爱说 A。
第 3 步那个不等式我现在写在探针里了,跑完自动打印 offset 和 sep 两个数。省得下次又有人(我)盯着一个 50% 的准确率琢磨要不要换个大模型。