> 只用"通过"的样本定标,19 次修改全是降噪——而加了对抗样本之后,第一条被抓到的检查是死代码
一个论文可读性检查器的定标集是三篇真的中了稿的论文。 这个选择是对的,理由也很硬:一条会在已录用论文上报红的规则, 会被手动放行一次,然后永远被忽略(见 [没人过得去的闸门会被手动放行,而放行一次它就不再是闸门](/notes/unreachable-gates-get-overridden/))。
结论先行¶
一个论文可读性检查器的定标集是三篇真的中了稿的论文。 这个选择是对的,理由也很硬:一条会在已录用论文上报红的规则, 会被手动放行一次,然后永远被忽略(见 没人过得去的闸门会被手动放行,而放行一次它就不再是闸门)。
然后它在这个定标集上迭代了 19 次。19 次全部是降噪。
这不是巧合,而是定标集的结构决定的: 三篇通过的样本,只能告诉你"本不该响的响了"。 它在构造上无法告诉你"该响的没响"。 于是这个回路只有一个方向,没有任何东西挡着它被一路调到全绿。
补上另一侧的做法是一份对抗样本:一个故意踩满全部九项检查的
tripwire.tex,自检断言每一项都必须在它上面响。
已录用论文是天花板(STOPS ≤ 8),对抗样本是地板。
地板第一次跑就抓到一条死代码:captions 检查被放在"没有 teaser 图"
那条分支的 return 之后,所以最需要它的论文 —— 完全没有 teaser 图的那些 ——
恰好是唯一永远收不到它的。它还只看 §2 之前的第一张图,
后面每一张图的 caption 在每一篇论文里都没被检查过。
修好之后第一次跑,它在那篇 ICML 定标论文里找出一条六个词的 caption,
而那篇论文此前已经被这个工具跑过很多遍。
为什么¶
"用通过的样本做假阳性地板"是对的,而且必要。问题在于把它当成了全部。 一个检查器有两类错误,而一个只含正样本的集合只能观测一类。 更糟的是这种不对称会自我强化:每次修改都降噪 → 发现变少 → 看起来更可信 → 继续按同样的方式修。没有任何一步是错的,而终点是一片绿。 和 测不出失败的门禁比没有门禁更糟——它在制造信心 是一回事, 只是这里的闸门不是从一开始就坏,是被正确的流程慢慢磨哑的。
对抗样本之所以能抓到死代码,是因为它把"这条检查有没有被执行到" 变成了一个可断言的事实。真论文做不到这件事:一条检查在真论文上不响, 可以是论文没问题,也可以是代码走不到,两者的产物完全一样。
这里还有一个值得记的边界:"每条检查至少响一次"只能抓死代码,
抓不到"一条检查的两个分支里有一个烂了"。那需要把粒度降到子原因,
并且覆盖的单位是 (夹具, 参数) —— 见
对抗夹具的覆盖单位是"夹具 + 参数",不是夹具——第 27 条子原因只在加了一个 flag 之后才响。
怎么用¶
数一下你的改动历史是不是单向的。 这是最便宜的诊断,不需要读代码: 把一个检查器/规则集最近 N 次改动按"放宽"和"收紧"分类。 如果全在一侧,几乎可以肯定定标集只有一种符号的样本。
天花板和地板要分开的两组输入,不能是同一组。 通过样本定天花板(不许误报),对抗样本定地板(不许漏报)。 对抗样本是手写的、故意脏的、永远不会被修好的 —— 它不是测试数据, 是规格说明的可执行形式。
对抗样本要和检查项一起加。 一条新检查如果没有相应的触发点, 它在地板上就是不可见的;最好让自检直接拒绝这种情况(新加一个子原因 而没给它触发点 → 自检红)。
把"最需要这条检查的输入"单独想一遍。 上面那条死代码的危害不在于它不工作, 在于它不工作的范围正好是它的目标范围。 这和 判据的盲区正好落在最该示警的那批数据上,这是最糟的一种失效 是同一个形状: 盲区和目标重合是最糟的一种失效,而两者都不会报错。
出处¶
~/Tools/nonexpert-review:CALIBRATION.md 的「棘轮,和现在挡住它的那个地板」
一节(19 次改动、caption 检查位于 return 之后、只看第一张图、
修复后在 ICML 定标论文上的第一条发现)、fixtures/tripwire.tex、
自检在 ~/Tools/skill-backend-check.sh:1743-1776
(含"只降噪会把工具磨成一片绿"的原话)。