前阵子整理VLM视觉问答报告的时候,我让某个开源VLM看一眼湿地图,它斩钉截铁地回答“画面里有一只斑马”,而那张照片里只有水鸟和枯草。这不是偶然抽风,而是多模态大模型最典型的“一本正经瞎编”:模型对物体存在性的判断,经常不是来自图像证据,而是来自语言先验。多模态幻觉检测这几年被反复提起,POPE评测是绕不开的一个基准,也是CVPR 26这个投稿周期里高频出现的基线方法。这篇东西不打算泛泛讲概念,而是想聊清楚三件事:POPE到底在测哪种幻觉、它的负样本怎么设计、我拿它复现和训模型时踩过的坑。
如果你正在做视觉语言模型(VLM)的评测、微调,或者只是被自己模型的“睁眼说瞎话”折磨过,这篇内容应该对你有用。
1. 为什么多模态幻觉检测成了这轮VLM评测的必答题
1.1 模型一本正经说“有”,图里明明没有
看几个开放式问答的结果,你会发现幻觉不是小概率事件。问“图片里有什么”,模型能流畅地输出一大段描述,里面有大量物体名称是图片里找不到的。更麻烦的是它撒谎撒得很连贯,比如图片是空旷的停车场,模型说“路边停着一辆红色轿车”,连颜色和位置都给你脑补好了。这种错误放到内容审核、自动标注、无人车感知报告这类场景里,风险是实打实的。
我最早做VLM评估时,习惯直接看生成文本里有没有出现错误物体。后来发现这个思路局限很大。生成式回答自由度太高,同一个画面模型可能这次说有路灯、下次说有垃圾桶,随机性很强。你很难判断它的稳定行为是什么。换句话说,幻觉是系统性的,评测方法却把它当成偶发错误来抓,自然抓不准。
1.2 幻觉的根源不在“模型故意撒谎”,而在三处设计缺陷
很多刚接触的人会问:模型为什么非要“看到”不存在的物体?从我的角度看,根因不是模型道德有问题,而是三个设计环节共同作用的结果。
第一是视觉编码信息不足。绝大多数VLM会把图像切patch后压成固定数量token,再喂给语言模型。一张高清图可能被压缩到几十个token,猫的耳朵、车的反光镜这些细节在压缩过程中就丢了。模型没看清,只能靠猜。第二是文本先验太强。语言模型部分在预训练阶段见过海量“草坪上有狗、餐桌上有盘子”这类共现句子,回答“有”在统计上更顺势,于是当视觉证据模糊时,它倾向于顺着常见共现关系填空。第三是训练数据里的图像-文本对并不总是精确的,不少图文对是alt文本或自动生成描述,里面本身就包含图里没有的对象,模型等于被数据带坏了。
这三个因素叠加,幻觉就不是某个prompt触发的问题,而是模型默认行为的一部分。评测这类问题,必须构造能反复触发、能量化统计的测试协议,POPE做的就是这件事。
1.3 生成式评测的盲区:CHAIR为什么不够用
在POPE被广泛讨论之前,大家常用的还有CHAIR这类基于生成描述的评测方法。思路是让模型看图写一段描述,然后统计描述里的物体词有多少是图片里不存在的。设计上很直观,但实操起来有明确瓶颈。
CHAIR依赖一个物体词表,判断某个词是否出现在图里时,需要调目标检测模型或人工标注来确认。模型如果不说名词,改说“那里有一团棕色的东西”,词表匹配就失效了。而且一张图一次生成只能得到一份描述,生成采样一变更结果就变,复现性差。POPE把开放生成问题改成是非判断问题,一个图像配一个明确对象,模型必须回答有或没有,结果变成结构化统计,就稳定多了。
2. POPE评测:轮询式探测如何把幻觉变成可量化指标
2.1 任务定义:把开放式生成变成是非判断
POPE(Polling-based Object Probing Evaluation)的核心思路可以概括成一句话:别再让模型自由发挥了,直接轮询式地问它“图里有没有X”,然后统计回答质量。任务流程不复杂:给定一张测试图像,挑一个物体名称,构造“Is there a/an [object] in the image?”这样一个二选一问题,让模型回答Yes或No。把所有问题汇总,按二分类方式统计指标。
这样设计有一个很实际的好处:可控性强。模型回答空间被锁死,评测方知道ground truth,可以精确算出它在多少问题上把不存在的物体说成存在。相对于生成式评测,这种判别式评测不需要做词表匹配,也不需要复杂的语义对齐,跑起来又快又稳。我后来在模型迭代里把它当回归测试用,每次改动视觉编码器或训练数据,就全量跑一遍POPE,看F1有没有掉,非常方便。
2.2 三组负样本采样策略:random、popular、adversarial
POPE最关键的细节在负样本怎么选。选得太简单,模型靠语言先验就能答对,测不出真实力;选得太难,指标又可能过于苛刻。POPE给出的方案是把负样本分成三档:
- random negative:完全随机地从类别集合里挑一个图里不存在的物体,比如给一张办公室图配“斑马”,这类错误比较离谱,大部分模型都能拒绝。
- popular negative:挑那些在训练语料里高频出现、和图像内容可能无关的物体,比如“沙发”“手机”“汽车”。模型见过这些词够多,语言先验会强烈倾向说有,更具迷惑性。
- adversarial negative:挑与图中真实物体有强相关性、经常共同出现的物体。比如图里是餐桌和椅子,问题问“有没有餐叉”。这种负样本最接近真实使用场景,也最容易骗出幻觉。
三档难度是递增的。一般评测会把positive样本(图里真实存在的物体)算一组,negative按三种策略各算一组,数量大致对齐。看到某个模型adversarial组F1明显低于random组,基本可以判断它的幻觉集中在语义共现场景里,光靠随机问话测不出来。
2.3 评测指标:准确率之外还要看F1和幻觉率走向
POPE报告指标通常包括准确率、精确率、召回率和F1。初次接触的人容易只看准确率。我建议重点盯F1,因为这里的类不平衡问题不太严重,但模型回答策略很会“作弊”。
比如一个模型无论问什么都答Yes,它会在positive问题上全对,召回率很高;但它把所有negative都答成Yes,精确率会非常难看,F1会被拉下来。反过来,一个极端保守的模型全都答No,准确率可能也不低,但召回率很惨。只看准确率,这两个方向的劣化都容易被平均掉。我习惯同时跑一张包含四列的统计表,再额外算一个“拒真率”或者叫幻觉率:模型对negative问题答Yes的比例。这个数字直观反映“瞎编”有多严重,汇报给非技术同事也更好懂。
3. 手把手复现POPE:采样、提问模板与打分的完整流程
3.1 数据准备:从标注到POPE提问清单
复现POPE的第一步是构造提问清单。通常从公开目标检测数据集的验证集里抽取一小批图片,比如400到600张。对每张图,从标注里挑出确实存在的对象作为positive;然后按前文三种策略各生成若干不存在的对象作为negative。把每条数据整理成相同的JSON结构,大致是图片路径、对象名、问题、标准答案。
{ "image": "val2014/000000123456.jpg", "object": "person", "question": "Is there a person in the image?", "answer": "yes" }我个人建议把positive和negative分开存放,后面统计时能分别看分歧。另一个容易忽略的点是:对象名必须和提问冠词统一,否则会产生很多无效的语法噪声。比如“apple”用“an”,“person”用“a”,格式不一致会影响模型对问题的语义解析,进而在小规模样本上影响零点几个点的指标波动。
3.2 评测脚本封装:一个能跑不同VLM的循环
跑POPE不需要复杂的训练代码,核心就是一个前向推理循环。我把评测脚本封装成一个函数:输入图像路径和问题文本,返回“Yes”或“No”。最稳的做法是直接从模型输出的logits里取Yes和No两个token的概率做比较,而不是用greedy decode后再匹配文本,后者容易受“Yes.”和“yes”这类格式差异干扰。
下面是我在实际项目里用的简化版本,思路可以直接抄:
import json import torch from PIL import Image def load_query_items(path): with open(path, "r", encoding="utf-8") as f: return [json.loads(line) for line in f] YES_TOKEN_ID = ... # 根据tokenizer确定 NO_TOKEN_ID = ... # 根据tokenizer确定 def poll_one_sample(model, tokenizer, processor, image_path, question): image = Image.open(image_path).convert("RGB") inputs = processor(text=question, images=image, return_tensors="pt") inputs = {k: v.to(model.device) for k, v in inputs.items()} with torch.no_grad(): outputs = model(**inputs) logits = outputs.logits[0, -1, :] yes_prob = torch.softmax(logits, dim=-1)[YES_TOKEN_ID].item() no_prob = torch.softmax(logits, dim=-1)[NO_TOKEN_ID].item() return "yes" if yes_prob > no_prob else "no" def run_pope(model, tokenizer, processor, query_file): items = load_query_items(query_file) results = [] for item in items: pred = poll_one_sample(model, tokenizer, processor, item["image"], item["question"]) results.append({ "object": item["object"], "gold": item["answer"], "pred": pred, "split": item.get("split", "") }) return results注意,不同VLM的processor在图片预处理上有差异,有的默认会resize到固定尺寸,有的会做中心裁剪。评测前务必确认测试图的真实输入分辨率,否则同一个POPE问题集在不同实现上跑出来的指标会微妙地不一样。这个问题后面还会展开。
3.3 结果统计与表格解读的注意事项
拿到results列表后,直接按二分类统计即可。无论模型内部结构多复杂,对外就是gold和pred两组字段。统计时我习惯分成positive、random negative、popular negative、adversarial negative四个子集分别算指标,再算summary。子集指标比总表有用得多,因为你能看到模型是在“常见物体”上被带偏,还是在“共现物体”上被带偏。
假设一张结果表长这样:
| 子集 | Accuracy | Precision | Recall | F1 |
|---|---|---|---|---|
| positive | 94.6 | 88.3 | 97.2 | 92.5 |
| random neg | 96.1 | 95.0 | 91.0 | 92.9 |
| popular neg | 87.3 | 89.1 | 80.4 | 84.5 |
| adversarial neg | 71.8 | 76.2 | 65.3 | 70.4 |
看到adversarial组F1只有70.4,基本可以断定这个模型的幻觉高发区是“共现物体”。如果你只报了总准确率大概能在88左右,看起来很体面,但分布拆开就露馅。汇报时把这张子集表一起给出,比单说一个数字诚实得多。
4. 从评测走向修正:降低VLM幻觉的四个生效层级
4.1 解码层:对比解码与logit抑制的不一样用法
POPE跑出来F1低,最直觉的修复路径是改解码,因为改动最小、不用重新训练。一个常见思路是对比解码:把图像的原始视觉输入和经过某种扰动(比如模糊、裁剪)的视觉输入分别送进模型,得到两组分布,然后将它们做差分放大。这样做的逻辑是,如果模型回答主要靠语言先验,那么扰动视觉信号后预测分布不会差太多;如果回答真的依赖图像证据,扰动后分布会明显变化。把这两组分布的差异利用好,能让模型更依赖可靠视觉证据、少依赖先验。
另一个更实用的做法是logit抑制。在解码阶段直接压低“Yes”这个token的生成概率,或者在POPE这类二选一场景里给No token加一个小的偏置项。实测下来这个操作能很快压制幻觉,但也有副作用:模型会变得过于保守,把图里真实存在但不太突出的物体也拒掉。所以我会固定跑POPE看F1的平衡点,偏置调太大时positive组Recall会掉,那时候就得回落。
4.2 视觉编码层:分辨率、token密度和投影层的影响
解码层的修复终究是修修补补,我更常做的是改视觉侧。POPE的negative问题上,模型之所以“没看见也说有”,很多时候是因为视觉侧给语言模型的证据太稀薄了。不少VLM默认把输入图缩到336甚至更小分辨率,然后下采样成少量视觉token。这个信息瓶颈非常致命。
我做过一组对照实验:同一份VLM权重,只把输入图分辨率从336提到672,同时把视觉token数从64个提高到两百多个,POPE的adversarial组F1涨了差不多6个点。原理不复杂,猫的胡须、红绿灯的位置这些小颗粒度线索是需要token容量来承载的。把更高分辨率和多尺度特征引入投影层,是比解码层更治本的一类手段,代价只是显存和延迟。
4.3 训练层:用偏向“拒认”的数据做偏好优化
想从根本上改变模型的“世界观”,还是得动训练。目前最主流的方向是偏好优化:采集大量图像-对象二元组,把“图里真实出现并正确回答有”当成偏好回答,把“图里不存在但模型倾向于说有”当成拒绝回答,用偏好评测方式约束模型。
实操上,我会先跑POPE收集一批模型答错的negative样本,把这些错误样本做成negative answer,构造偏好对去微调。这个闭环非常有效:POPE是评测器,也是数据挖掘器。只靠通用数据做偏好优化,模型可能只是在通用对话上变稳了;针对性用POPE的badcase做训练,修复效率高得多。需要注意的是数据量和训练步数别过大,否则模型容易“矫枉过正”,出现对真实对象也过度拒认的问题。
4.4 推理层:自检与追问路线值不值得用
训练改完后,还可以在推理时加一层自检。比如第一次问“图里有没有X”,模型回答Yes;再换一种问法反向追认:“If you cannot clearly see X, answer No. Is there X?”有多轮自证的VLM明显会更谨慎。代价是推理成本约翻倍,每张图要跑两次前向。对实时性要求不高的离线批处理场景,这个方案性价比很高。
但我不建议把自检当成常态策略。它治标不治本,而且不同VLM对二次追问的响应模式差异巨大,有的模型会来回横跳。我的习惯是:先用POPE定位模型的问题集中在哪一组负样本,如果adversarial组差,优先考虑数据处理和视觉编码增强;自检只作为上线前的最后一层防御。
5. POPE的边界在哪里:踩坑记录与实战限制
5.1 语言先验对F1的污染,是我第一个发现的坑
复现POPE时我犯过的最典型错误是:看到popular negative组F1还挺高,就以为模型幻觉不严重。后来把提问对象换成那些在语言预训练里高频出现、但在测试图像里基本不会出现的物体,比如“电视”“床”,模型的Yes倾向立刻上来了。
这说明模型很多情况下不是“看到”了物体,而是“听到”了高概率名词就条件反射。语言先验强的模型在POPE的popular组上天然占便宜,评测时如果不区分报告,会高估模型真实的视觉 grounding 能力。所以现在我看结果,优先看adversarial组和positive组的比值,两个都很高才说明模型是真正在看图回答。
5.2 采样数字、提问措辞、随机种子都会改写排名
POPE看起来流程简单,但细节决定结论。同一份模型,用不同随机种子从类别里采样negative,F1可能有1到2个点的波动。提问措辞更敏感,改“Is there a cat?”为“Can you see a cat?”或“Does the image contain a cat?”,模型的预测就会变化,因为不同VLM对“see”和“contain”的语感差异很大。
我踩过最尴尬的坑是拿着同一模型比较两个公开实现,一个用模板A,一个用模板B,结果排名直接反转。后来形成的工作习惯是:自己固定一套模板,所有模型都跑同一份提问清单,绝不混用。复现别人的POPE数字前,先确认它的采样策略、负样本数量、提问模板三件套是不是和自己的假设一致,不一致时先对齐变量再谈对比。
5.3 静态测试集合的陈旧感:POPE之后还需要什么
POPE的优点是把幻觉量化,缺点也明显:它依赖固定类别集合和固定提问方式,测不到模型在开放词汇、长尾物体上的幻觉情况。比如我上线一个新场景,里面有一堆工业零件,POPE里根本没有这些类别,那无论它分数多好,都不能给我任何安全感。这种情况下我倾向于补充动态评测:拿目标检测器在真实业务图片上跑一批标签,动态生成POPE风格问题,这样测试集合能跟着业务走,而不是永远躺在公开数据集的标注里。
另外,POPE测的是“物体存在性”这一种幻觉,但VLM还会产生属性幻觉(颜色、材质、数量)和关系幻觉(左边、上面、是A的B)。这些不能硬套POPE。我现在的做法是把POPE当“幻觉回归测试”,每次模型迭代后必跑,同时为属性幻觉再单独建一套小评测集,两者互相补位。
如果你刚接触这个方向,我的建议是先别急着调模型,老老实实把POPE在自己关心的模型上复现一遍,把子集指标拆开看。先搞清楚模型到底是在random组犯傻,还是在adversarial组犯傻,再决定下一步改解码、改分辨率还是改训练数据。有了这个评测闭环,后面所有改动才有依据,不然就是在跟一个看不见的误差赛跑。