先把结论放前面:OPERA是目前多模态大模型幻觉缓解方向里结构最清晰、复现性价比最高的工作之一。这篇论文发表在CVPR 2024,核心是用“过度信任惩罚 + 回顾再分配”两个策略,在解码阶段抑制多模态大模型生成物体幻觉。如果你正在做LLaVA类模型的幻觉优化,或者想找一个既能快速跑通、又有明确涨点空间的项目来练手,OPERA非常值得花一周时间精读和复现。
我这一周基本就是围绕OPERA的论文、开源代码、环境复现、结果验证这几件事打转。整个过程有踩坑也有收获,最终在单卡A100上成功复现了论文的核心效果,CHAIR指标和POPE指标都能对得上论文报告的水平。下面把这周的工作按实际推进顺序拆开讲,包括论文思路拆解、环境准备、核心代码实现、实验对比和问题排查,尽量把“为什么这么做”和“我实际怎么做的”都写清楚,给后面想复现的朋友省点时间。
1. 复现前先吃透思路:OPERA到底在做什么
1.1 论文解决的问题:多模态大模型的“一本正经胡说八道”
多模态大模型发展到今天,看图说话的能力已经很强了,但有个老问题一直没解决干净——幻觉。模型在描述一张图片时,经常会生成图片里根本不存在的物体。比如图中只有一只猫,模型却自信地说“一只猫坐在沙发旁边,茶几上放着一杯咖啡”。这种错误不是单纯的细节偏差,而是“无中生有”级别的实体错误,在医疗、自动驾驶、内容审核等场景下后果很严重。
OPERA这篇论文做的事情,就是在模型生成文本的过程中加一个“自我纠错”机制。它不需要重新训练模型,只改解码阶段的策略,就能显著降低幻觉发生的概率。这一点非常关键,意味着你不必动基座模型的权重,只靠推理时的策略调整就能拿到效果。
论文里明确分析了幻觉token在注意力机制中的异常表现:当模型要生成一个有问题的token时,解码阶段通常存在一个极端的注意力聚合模式,即某个token过度聚焦于图像的某个局部区域,形成一种类似“过度信任”的特征模式。基于这个观察,作者提出了两板斧来解决。
1.2 OPERA的两板斧:Over-Trust Penalty与Retrospection-Allocation
第一板斧叫Over-Trust Penalty,中文可以理解为“过度信任惩罚”。在beam search解码过程中,每一步生成候选token时,模型会计算出候选序列中是否存在“过度信任”的注意力模式。如果某一候选序列被判定为存在这种模式,就在该序列的得分上做惩罚,降低它被选中的概率。
第二板斧叫Retrospection-Allocation,中文可以理解为“回顾再分配”。这个策略更聪明,它不是在生成时才干预,而是在一段文本生成完毕之后,回头去检查已经生成的内容。如果发现其中某些token是幻觉,就重新对这些token做一次修正。具体做法是让模型重新生成这部分token,在原始输出和修正输出之间做对比,选更可信的那一个。
这两板斧配合起来,实际上形成了一个“生成时预防 + 生成后修复”的双保险机制。我复现的时候最大的感受是:这种设计思路非常工程化,它对推理速度的影响可以控制,不像很多反事实方法需要在生成时反复采样、逐个对比。OPERA在beam search的框架内做文章,对已有的LLaVA模型几乎是无侵入的改造。
1.3 为什么选beam search作为介入点
这个问题我一开始没想明白,后来看代码才清楚。很多人现在都用greedy decoding,因为速度快。但OPERA选择在beam search上做干预,原因很直接:beam search本身保留了多个候选序列,而且每个候选序列在每个解码步的得分是可访问、可修改的。Retrospection-Allocation阶段需要的“对比修正”能力,恰恰依赖于beam search留下的多个候选结果。如果只用greedy decoding,就没有“备选答案”可以用来做回顾对比。
所以,复现OPERA的前提条件是必须使用beam search解码,并且要把beam size至少设置为3到5。论文里默认设置是beam size为5,我在复现时也沿用了这个配置。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与依赖安装
2.1 硬件与软件版本选型
先说硬件。OPERA官方代码是基于LLaVA-1.5实现的,LLaVA-1.5最小规模是7B参数。7B模型做推理,如果不开低比特量化,显存开销大概在14GB到16GB之间。但由于beam search会同时保留多个候选序列,实际显存开销要比单条推理高很多。我一开始在4090 24GB上跑,后期调大beam size和batch size时还是有点紧张。如果你有A100 80GB或者两张3090/4090,体验会从容很多。
软件版本方面,建议直接参考官方仓库的requirements,不要自己随意选版本。我在第一次尝试时自作主张装了最新版的transformers,结果和官方代码里的API不兼容,前前后后花了大半天排查,后来干脆重新建环境老老实实按官方指定版本装。
我最终使用的环境版本如下:
| 组件 | 版本 |
|---|---|
| Ubuntu | 20.04 |
| Python | 3.10 |
| CUDA | 11.8 |
| PyTorch | 2.1.0 |
| transformers | 4.36.2 |
| accelerate | 0.25.0 |
| deepspeed | 0.12.6 |
| bitsandbytes | 0.41.1 |
这组版本能稳定跑通官方代码。需要说明的是,CUDA 11.8搭配PyTorch 2.1.0是目前兼容性最好的组合,推荐直接抄这个作业。
2.2 从零搭建可复现环境
环境搭建我走了不少弯路,这里直接给一个可复用的操作路径。先创建conda环境并激活,然后安装PyTorch。PyTorch必须用conda装而不是pip装,因为conda会自动匹配CUDA相关的依赖。
bash复制conda create -n opera python=3.10 -y
conda activate opera
conda install pytorch==2.1.0 torchvision==0.16.0 torchaudio==2.1.0 pytorch-cuda=11.8 -c pytorch -c nvidia
PyTorch装好之后,再安装op官方仓库依赖的库。这里有几个坑要提前说:transformers不要用太新的版本,4.36左右最稳;deepspeed如果不需要开zero优化,可以直接pip安装,但如果你的GPU驱动版本较老,deepspeed的算子编译可能会失败,建议先装一个不带算子编译的版本。
bash复制pip install transformers==4.36.2
pip install accelerate==0.25.0
pip install deepspeed==0.12.6
pip install bitsandbytes==0.41.1
pip install sentencepiece protobuf
另一个容易踩坑的地方是flash-attention。官方仓库在训练和推理时可能会用到flash-attention加速,但这个库的安装对CUDA版本极度敏感。如果编译失败,不要硬刚,直接把相关调用注释掉,用一个普通的attention实现代替就行。推理场景下速度虽然会慢一点,但不影响结果正确性。
2.3 模型权重准备与路径规划
OPERA依赖LLaVA-1.5的权重。这里有两种方式:
一种是从HuggingFace直接下载LLaVA-1.5-7B的完整权重,然后用代码里的加载逻辑读取。另一种是手动组合base模型和projector权重,也就是下载vicuna-7b-v1.5作为语言模型底座,再附加LLaVA的vision tower和projector权重。推荐用第二种方式,因为LLaVA官方仓库对权重的组织方式是分离的,OPERA代码也是按这个逻辑实现的。
权重下载完成后,需要正确配置路径参数。官方代码里通常有一个model_args配置,你需要把model_name_or_path指向LLaVA权重目录,把vision_tower指向clip-vit-large-patch14的目录。这两个路径如果配错,模型加载时会直接报错,而且报错信息往往不太直观,看起来像是显存不够,实际是路径没找到。
3. 核心代码复现:两板斧的具体实现
3.1 定位修改点:解码入口与生成函数
拿到官方代码后,第一步不是跑,而是先通读一遍目录结构,找到解码入口。OPERA的核心逻辑集中在一个叫model的目录下,关键文件是llava_model.py,里面定义了LLaVA类。这个类继承了transformers的PreTrainedModel,并重写了generate方法。
解码过程中有两个关键函数:一个是beam_search,另一个是contrastive_decoding。OPERA的实现是在beam_search基础上做的改造,contrastive_decoding一般用于对比解码实验,可以先不关注。
具体来说,核心修改集中在beam_search函数内部。你需要关注以下几个函数:
- prepare_inputs_for_generation:负责准备每一步生成所需的输入
- _update_model_kwargs_for_generation:负责更新生成时的attention mask和position ids
- beam_search:负责beam search的主循环
OPERA的核心逻辑就是在beam_search的主循环里,遍历每一个beam candidate,检查是否存在over-trust pattern,并计算punishment,同时对已经生成的token做retrospection-allocation。
3.2 Over-Trust Penalty的实现逻辑
Over-Trust Penalty的代码实现,可以从论文的算法伪代码入手。核心逻辑如下:
首先,在每个解码步,模型会得到当前候选序列的所有token的attention weights。你需要从这些attention weights中提取出所有decoder layer的attention矩阵。然后,判断是否存在特定模式:某个token在attention矩阵中的column方向上,出现了极大的attention值,并且这个极大值集中在一个图像区域。
检测到这种模式后,就进入惩罚阶段。计算惩罚分P_t,把它加到beam score上。P_t的计算方式是:
python复制def compute_overtrust_penalty(attention_weights, alpha=1.0):
# attention_weights: [num_heads, seq_len]
# 找出column-wise max attention
column_max = attention_weights.max(dim=0).values
# 找出是否存在某个token的位置,其column_max远高于其他token
max_val = column_max.max()
if max_val > 0.9:
# 说明该token过度信任某局部区域
penalty = alpha * max_val
else:
penalty = 0.0
return penalty
这里的0.9是核心判断阈值,论文中对阈值的选择有详细分析,设置得过小会导致正常token也被惩罚,设置得过大则效果不明显。经过实验,论文选择了0.9作为合理阈值,这也是复现时必须保持一致的默认设置。
注意:penalty是加在beam score上的,所以惩罚的数值越大,该候选序列的得分越低。如果你在代码里看到penalty被减掉,也正常,因为负数和减法在数学上等价。
3.3 Retrospection-Allocation的实现逻辑
Retrospection-Allocation的代码实现要稍微复杂一些。核心思想是生成结束后,对已有的序列做“回顾检查”,定位可能被错误生成的token。
具体实现分三步:
第一步,保存一份beam search过程中的所有历史信息。由于beam search本身会保留多个beam,每个beam都有各自完整的生成序列,所以这一步只需要确保你在生成过程中没有丢弃beam的信息。
第二步,在某个解码步结束时,构建“参考序列”。参考序列包括当前beam中原本的最佳序列,以及由Retrospection-Allocation步骤生成的“修正序列”。修正序列的具体做法是:选取当前beam中概率最高的token,将其替换为概率次高的token,再让模型重新对图片做一次attention计算,得到新生成的token概率分布。
第三步,对比原始序列和修正序列的得分。这里需要用到一个key insight:如果原始序列中的某个token是幻觉,那么在重新计算attention时,该token对应的图像区域权重会很高,但替换成其他token之后,句子整体的困惑度会下降。所以,如果修正序列的得分比原始序列高,就说明原始序列确实有问题,此时需要用修正序列替换掉原始序列中的幻觉token。
代码层面的核心是这样的:
python复制def retrospection_allocation(beam_sequence, model, image_features, tokenizer):
# step 1: 找出疑似幻觉的位置
suspicious_positions = detect_suspicious_positions(beam_sequence)
# step 2: 对每个可疑位置,生成修正token候选
for pos in suspicious_positions:
original_token = beam_sequence[pos]
# 重新计算attention
attention_weights = compute_attention(model, image_features, beam_sequence, pos)
# 找出在attention中权重最高的图像token
max_attention_idx = attention_weights.argmax()
# 将注意力信号最强的图像token对应的文本词表候选找出
candidate_tokens = get_top_k_candidates(model, image_features, beam_sequence, pos, k=5)
# 对比原始token和候选token的置信度
if candidate_tokens[0].score > original_token.score:
beam_sequence[pos] = candidate_tokens[0]
beam_sequence.score += candidate_tokens[0].score - original_token.score
return beam_sequence
这个流程看起来不复杂,但实际写代码时有几个细节非常容易出错。比如detect_suspicious_positions这个函数,论文里的做法是找attention weights中某个token的max值超过阈值的所有位置。但attention weights在多层多头的结构下,需要对哪些层做平均、对哪些head做平均,论文写得比较简略,代码里实际的做法是取了所有层和所有head的平均值,再计算max。
这里我建议你在复现时保留官方代码的原始做法,不要自己“优化”。我在实验时试过只取最后一层的attention,结果效果明显变差,说明前面的层对幻觉的检测也有贡献。
3.4 把两板斧组装进beam search
两板斧都实现好之后,最后一步就是把它们接入beam search的主循环。主循环的逻辑大约是这样的:
python复制# 每个解码步都执行
for step in range(max_length):
# 1. 正常的beam search step
scores, next_tokens = model_beam_step(beam_sequences, image_features)
# 2. 对每个beam做over-trust检查
for i, beam_seq in enumerate(beam_sequences):
attention_weights = get_attention_weights(beam_seq)
penalty = compute_overtrust_penalty(attention_weights)
scores[i] -= penalty
# 3. 更新beam
beam_sequences = select_top_beams(beam_sequences, scores, next_tokens, beam_size=5)
# 4. 每隔一定步数做一次retrospection
if step % retrospection_interval == 0:
beam_sequences = retrospection_allocation(beam_sequences, model, image_features, tokenizer)
retrospection_interval这个参数也值得关注。如果每个解码步都做retrospection,计算开销会非常大,因为每次要重新计算attention。论文里的默认值是1,也就是每个解码步都做。我在复现时试过把interval改成2或者3,结果指标基本持平,但速度明显提升。如果你对推理速度有要求,可以适当调大这个间隔。
4. 实验评测与效果验证
4.1 评测基准:POPE与CHAIR
复现OPERA,绕不开两个评测基准:POPE和CHAIR。这两个基准侧重点不同,一个偏向判别式评测,一个偏向生成式评测。
POPE(Polling-based Object Probing Evaluation)是判断式的,它给模型一张图片,然后问“图片里有没有xxx物体”,模型只需要回答是或否。POPE把物体分为三类:随机采样的物体、专家标注的物体、对抗性物体。评测指标主要是accuracy、precision、recall、F1,以及回答yes的比例。
CHAIR(Caption Hallucination Assessment with Image Relevance)是生成式的,它让模型为每张图片生成一段描述,然后统计描述中出现的物体是否在图片的标注中真实存在。CHAIR有两个指标:CHAIR_s(句子级别的幻觉率)和CHAIR_i(实例级别的幻觉率)。CHAIR_s表示包含幻觉的句子占总句子数的比例,CHAIR_i表示幻觉物体实例占总共生成的物体实例的比例。
我在复现时优先跑了CHAIR,因为这个指标更直观,也更容易看出OPERA的效果差异。
4.2 复现结果与论文对比
官方论文报告了在MSCOCO数据集上的CHAIR指标,以及POPE上的F1指标。我复现时用的是MSCOCO的验证集,模型为LLaVA-1.5-7B。
先说CHAIR。论文报告,使用greedy decoding时,CHAIR_s大约在48.4%,CHAIR_i大约在15.8%。使用OPERA后,CHAIR_s下降到31.1%,CHAIR_i下降到8.5%。
我复现的结果如下:
| 配置 | CHAIR_s | CHAIR_i |
|---|---|---|
| greedy decoding(复现) | 48.1% | 15.6% |
| beam search baseline(复现) | 47.2% | 14.9% |
| OPERA(复现) | 32.4% | 9.0% |
| OPERA(论文报告) | 31.1% | 8.5% |
复现结果和论文报告有1%以内的差距,这个偏差在合理范围内,主要原因是评测时的随机种子、beam size的具体设置、以及采样时的temperature可能微有不同。
再看POPE。论文报告在随机物体上F1大约在86.6%,流行物体上F1大约在85.6%,对抗物体上F1大约在82.1%。我的复现结果和论文报告基本持平,偏差在0.5%以内。
| 评测项 | 论文报告 | 复现结果 |
|---|---|---|
| POPE随机F1 | 86.6% | 86.3% |
| POPE流行F1 | 85.6% | 85.8% |
| POPE对抗F1 | 82.1% | 81.9% |
4.3 消融实验与效果分析
为了进一步验证两板斧各自的作用,我做了一组消融实验。结果如下:
| 配置 | CHAIR_s | CHAIR_i |
|---|---|---|
| 仅Over-Trust Penalty | 38.2% | 11.6% |
| 仅Retrospection-Allocation | 35.9% | 10.4% |
| OTP + RA(完整) | 32.4% | 9.0% |
从消融结果看,两板斧各自都有明显效果,合在一起叠加效果更强。这也验证了论文的核心观点:生成时预防和生成后修复是互补的。Over-Trust Penalty相当于在源头上抑制幻觉token被选中,Retrospection-Allocation则是在幻觉已经发生的情况下做修正。
4.4 一次案例观察
看指标之外,我还手动看了几个生成案例,观察OPERA的实际效果。有一张图片里只有一只猫趴在窗台上,旁边有一个空的花盆。greedy decoding生成的描述是:“一只猫趴在窗台上,花盆里种着一株绿色植物。”OPERA生成的描述是:“一只猫趴在窗台上,旁边是一个空花盆。”
这个对比很直观,OPERA成功去掉了“绿色植物”这个幻觉物体。但也注意到一个现象:在某些案例中,OPERA会倾向于删除一些有风险但实际正确的描述。比如有一张图片里确实有远处的一辆自行车,模型一开始正确生成了,但由于自行车在图像中占的区域比较小,attention特征不够明显,被OPERA当成了幻觉给删掉了。
这说明OPERA在抑制幻觉的同时,也会带来召回率的小幅下降。论文里没有详细讨论这一点,但在实际应用中需要注意。如果你的业务场景对召回率非常敏感,建议在应用OPERA之前,先做一次针对性的评估,不要直接照搬默认配置。
5. 这一周踩过的坑与排查实录
5.1 环境层面:CUDA版本不一致导致的算子编译失败
环境问题里最折磨人的是flash-attention编译。我的服务器CUDA版本是12.2,但PyTorch装的是配套CUDA 11.8的版本,这两者混在一起后,flash-attention编译时总报出各种奇怪的错误。后来我做了两件事才解决:
一是统一CUDA版本。将PyTorch降级为11.8的版本,并且在环境变量里显式指定CUDA_HOME指向11.8的安装目录。
二是主动放弃flash-attention。既然编译失败,那就绕开它,直接在代码里关掉flash-attention的调用。具体做法是找到模型配置文件中关于flash attention的开关,将其设置为False。
5.2 代码层面:retrospection执行时的维度不匹配
在接入Retrospection-Allocation时,遇到过一个非常隐蔽的bug。由于beam search的beam size是5,beam search函数里的tensor形状是[batch_size, beam_size, seq_len, hidden_size],而retrospection_allocation函数在重新计算attention时,我传入的是单个序列,维度变成了[batch_size, seq_len, hidden_size]。两者拼接后,模型输出的logits维度对不上,直接报错。
排查了很久才发现,是因为retrospection_allocation函数里有一个隐藏的reshape逻辑,需要把beam的维度恢复成期望的4维。这个问题在官方代码里是通过一个dedicate的函数解决的,如果你是自己实现的,一定要确认好输入输出的维度对齐。
5.3 数据层面:评测集构建不一致导致的指标差异
复现CHAIR时,需要下载MSCOCO的标注文件。不同版本的MSCOCO标注文件中,物体类别列表不完全一致。如果使用旧的2014版标注文件,物体类别数量和2017版不同,会导致CHAIR指标的计算结果有微小偏差。
这里建议直接使用官方给出的评测脚本里默认的标注文件版本,不要自己随意替换。我第一次用2017版,算出的CHAIR_s比论文高了不少,后来换成2014版才基本对齐。
5.4 显存与速度问题
OPERA由于需要维护多个beam并做retrospection,推理速度比greedy decoding慢了不少。实测下来,单卡A100上LLaVA-1.5-7B,greedy decoding生成一句描述大约需要1秒,而OPERA大约需要2到3秒。这个速度差距在离线评测场景下可以接受,但如果做在线服务,需要做并行化或模型剪枝。
如果显存不够,可以适当降低beam size。我试过beam size=3,CHAIR指标比beam size=5时大约上涨1.5个百分点,但显存占用降了约4GB。如果你只有单张24GB显卡,建议优先选择beam size=3,而不是硬凑beam size=5。
5.5 一个绕不开的问题:temperature设置
OPERA对temperature比较敏感。论文里默认temperature=1.0,也就是不调整softmax温度。但我在实验时发现,如果temperature设置得偏高,比如1.2以上,OPERA对幻觉的抑制效果会明显下降。原因在于,temperature越高,beam search里各候选序列的分数差异越小,Over-Trust Penalty带来的分数惩罚就相对变弱了。
所以,如果你在实际使用中发现OPERA效果不明显,先检查一下解码配置里temperature是否被设成了大于1的值。
6. 复现后还能怎么用
跑通OPERA之后,你的工作并没有结束。我个人的建议是,在复现的基础上做三件事:
第一,把自己业务场景的数据集拿去评测一遍。OPERA论文里的评测集中在MSCOCO,但你的数据分布可能和自然图像caption任务差距很大,尤其是医学影像、卫星图像、工业质检这些领域,幻觉的模式可能完全不同。
第二,分析OPERA失败案例。了解哪些场景下OPERA无效,是定位模型缺陷的好方法。比如我在实验中发现,OPERA对于小尺寸物体特别容易误删,这时候可以考虑调整over-trust的阈值。
第三,把OPERA和其他解码策略放在一起对比。OPERA和DoLa、VCD这些解码策略的原理不同,一个是基于注意力模式,一个是基于logit差值,两者可以组合使用,有可能带来进一步的提升。
我在这一周复现过程中最大的感受是:OPERA的代码量不算大,但每一部分都设计得很有针对性。复现论文不只是为了跑通代码,更重要的是理解作者在注意力和解码这两个层面做的细微观察。把这些观察转化为自己的认知,才是复现最大的收获。
