跳到主要内容
0.2%的参数,41.7%的F1,和一个漂亮的测量点

0.2%的参数,41.7%的F1,和一个漂亮的测量点

P99
P99

· 阅读约 7 分钟

上周arXiv上挂了一篇没怎么引起注意的论文(8月17号,Khatri,题目叫《Probing the Prefill: Detecting Code Vulnerabilities via Latent Activations》)。做法一句话能说完:把几个通用LLM——Granite-4.1-8B、Qwen3.5-9B、Gemma-4-12B——跑完prefill,在最后一个token的激活上挂一个13.4M到16M参数的MLP探针,训练它判断当前的C/C++函数有没有漏洞。主模型全程冻结,反向传播不碰主模型一根手指头。

这个测量点选得非常聪明。prefill最后一个token,是模型读完全部代码、准备吐出第一个生成token的那个瞬间——如果"这个函数里有没有漏洞"这个信息真的存在于内部表示里,那个位置是信号最干净的时刻。再早一点,上下文没读完,函数信息残缺;再晚一点,生成已经开始,连续性、风格、语言模型的先验全混了进来。单论"取哪个位置代表'模型对这段代码的理解'",这个选择几乎没有更好的答案。对做探针的人来说,这是一个教科书级别的实验设计。

论文自己最漂亮的数字也出在这里:在Devign基准上,这个0.2%参数量的探针拿到68.8%的F1,超过了已发表微调分类器的最佳结果(67.9% F1),而它只读激活、不训练主模型。我看着这个数字愣了一下,差点就把"探针干翻微调"当成了这篇论文的结论。

然后我看到了另一个数字:四个基准的平均F1,41.7%。

先说个结论:平均41.7%这个数字本身不奇怪——它说明Devign之外那几个基准上,探针的成绩比Devign掉得多。事实也确实如此:在Big-Vul、Draper VDISC、PrimeVul三个更难、类别更不平衡的基准上,探针明显落后于已有最佳结果。四个基准里一个过线、三个不过线,"平均40多分"就这么来的。

我把这个落差反复看了几遍,心里挺复杂。一方面,我理解作者把这个"过线"拿出来当卖点——在一个公开基准上、用极小的探针、冻结模型,压过了已有论文里最好的微调分类器,这在探针文献里确实少见。但另一方面,这个结果只在一个基准上成立,而且另外三个基准上输得还挺明显。以"跨语言、跨通用漏洞形态的表示中存在漏洞信息"这个范围去总结结论,目前的数据支撑是:在一个C/C++函数级漏洞基准上成立。这个边界比论文摘要给读者留下的印象要窄,而且窄不少。

这时候想先交代一句我的阅读范围:我只把摘要和实验数据认真读了,正文里的训练细节——probe层数、训练集划分方式、是否做了多次运行时方差统计——我没看完。下面的判断基于数字本身,如果正文里有我没看到的消融,我后面会回来更正。

接着拆那个"超过微调"的数字。68.8%对67.9%,差了0.9个点。这篇论文没有给方差信息——至少我看到的摘要和结果段落里没有。如果你在一个数据集上把同一个探针跑十次,F1的浮动范围完全可能到±1.5甚至更大。0.9个点的领先,在没有多次运行分布的情况下,就是一个没有任何置信区间的裸数字,它可以是"真的强于微调",也可以是"这次运气好了一点"。这还不是最要命的。

最要命的是:那个67.9%,作者说来自"已发表的微调分类器最佳结果"。我用什么划分方式、用什么模型、什么训练预算、多少样本,才得到这个67.9%?如果两个数字来自不同的Devign数据划分,那它们根本没有可比性。跨论文对比F1,划分不同、类别平衡不同、评估口径不同,数字直接拿出来比高低,是我做实测时最忌讳的事情之一。这篇论文如果正文里没有交代这个基线的具体训练条件,那"超过"就只是一句字面的陈述,压不住验证。

我还没想通的另一个点是:为什么探针是MLP,而不是线性探针?

探针参数只有13.4M到16M,相对主模型是0.2%不到,这是小,但它不是线性分类器——它是一个有非线性层的MLP。如果漏洞信息在线性可分的方向上就存在,用一层线性探针就能提取出来,这个结论会干净很多:信息存在,而且是以一种模型表示里很"直白"的方式存在。用了MLP,等于默认信息不是线性可分的,探针需要多一层非线性变换才能把它捞出来。那就出现一个问题:在Devign上那个68.8%——或者说平均41.7%的分——有多少是"激活确实携带了漏洞语义",有多少是MLP额外的那几个非线性层,把数据集里与漏洞高度相关、但跟漏洞没有因果关系的语法痕迹一起学了进去?

这里我有点私人偏见。我以前做过很小的探针实验,踩过最大的坑是:你很兴奋地发现activation space里有一个能完美区分正负样本的方向,并且探针分数很高,于是以为"模型内部理解了这个概念"。不一定的。探针揭示的是"信息存在",不是"模型在生成时用到了这个信息"。你找到一条能区分"漏洞/非漏洞"的方向,并不意味着模型在续写代码时会沿着这个方向做决策。那两个问题隔着一整条推理链,而探针实验只回答前一个。论文的措辞是"模型对代码的表示反映漏洞状态"——这一步比"信息存在"强,但又没强到"模型真的在按照漏洞知识推理"的程度。它对证据等级的把握其实是在中间那层,这层站得住,只是别被"探针超越微调"这种标题带到更远的地方去。

另外一个让我皱眉的地方是"0.2%"这个修辞。它比的是参数规模,但很容易被读成"0.2%的成本"。参数少只是省内存和训练算力,部署时仍然要把每个函数跑一遍完整的prefill才能拿到那个token的激活——你把一个轻量级漏洞检查器塞进CI,每次提交都要过一个8B模型的前向,这个成本根本不是linter级别的。而且探针还需要标注数据训练,微调分类器也需要标注数据训练——两边在数据成本上其实一样。所以"0.2%的成本打败100%微调"这个印象,是换算不出来的。正确的说法只能是:在参数规模上,它确实非常非常小。

好了,现在把话说均匀一点。这不是一篇坏论文——恰恰相反,实验框架是很好的:四个模型横跨不同规模,固定了测量点,固定了探针类型,保持主模型冻结。它回答的是一个我喜欢的问题:"在免费的通用编码模型里,用一个小探针能从激活中分辨出函数级漏洞吗?"回答是:在一个基准上可以,在另外三个还不能。这个答案既比"大模型能检测漏洞"更具体,也比"探针没用"更接近真实情况。

但"提供一个早期证据"这个结论,我用起来会谨慎一些。如果把这个证据摆在我办公桌上,我会给它贴的标签是:在Devign这个条件下,方向大致对;超出这个条件,证据不够。等作者放出探针权重和完整的实验脚本——如果会放的话——我会把它跑在我自己攒的那组CVE样本上,重点测两种东西:第一,探针的分数在变量重命名、删除注释、格式化变化之后掉多少;第二,它能不能跨出C/C++,在别的语言上还认得漏洞。这两件事是我目前最想看到的消融。如果格式化变体就把它打穿了,那Devign上那个68.8%就很好解释:探针学到的可能是那个数据集里漏洞样本所共有的表面特征,不是漏洞的语义。如果它扛得住,那我上面这一整段怀疑都可以收回,我会专门写一篇纠偏。

到时候数字对不上,欢迎来找我。