复现OPERA这篇论文,算是我今年做得最折腾也最有收获的一件事。当时在实验室里从周一开始拉代码,一直到周五晚上才把完整的评估流程跑通,中间经历了环境崩溃、显存爆炸、生成结果全是乱码、指标对不上等一系列问题。这篇算是一个补记,把我这一周做的事情、踩过的坑、以及后来复盘时想明白的一些细节都记录下来,给后面想复现多模态幻觉抑制工作的朋友做个参考。
OPERA是CVPR 2024的一篇多模态大模型幻觉抑制论文,核心思路是在自回归解码过程中加入一个基于注意力模式的惩罚项,并在beam search结束后增加回溯分配机制,从而缓解模型在生成描述时出现的物体幻觉。这篇博文只讲和复现相关的实操内容:环境怎么搭、代码结构怎么理解、关键实现怎么改、指标怎么复现、以及我最想分享的——那些只有自己跑过一遍才会意识到的问题。
如果你手里有一张24G显存的显卡,或者能借到一台A100服务器,想完整地把这个项目跑起来,那这篇文章应该能帮你省下至少两天的摸索时间。
1. 复现前必须先搞清楚的几个问题
1.1 你要复现的到底是什么:模型训练还是推理评估
很多人拿到论文代码后第一件事就是去翻训练脚本,想着要用自己的数据集重训一遍。但对于OPERA这种基于LLaVA体系的工作,事情没那么简单。它的核心贡献不是一个全新的模型结构,而是一个部署在解码阶段的解码策略。换个更直白的说法:模型还是那个多模态大模型,但beam search和最终token选择这两步做了手术。
所以复现的第一件事就是先止损——搞清楚你复现的目标是什么。OPERA官方仓库提供了两个层面:一是完整的训练代码,可以直接微调LLaVA模型;二是推理和评估代码,加载已经训练好的权重,在COCO Caption数据集上计算CHAIR指标。我这一周选择的是第二条路,因为训练成本太高了,而且光为了复现论文里的表1,你必须先把评估流水线跑通,否则连对比基线都没有。跑通了评估,再谈训练也来得及。
这个决策背后其实是一个通用经验:复现论文的第一步不是让代码跑起来,而是把论文的实验设计拆成可验证的最小闭环。对OPERA这种解码策略类的论文,最小闭环就是“加载权重-生成caption-计算CHAIR”,而不是从零开始训模型。
1.2 硬件配置要多少才够用,显存会不会爆
OPERA官方对硬件的要求写得很简略,但实际复现时差别很大。先说结论:单卡24G显存可以把完整的生成和评估流程跑起来,但batch size会被压得很低,速度也比较感人;如果条件允许,8卡A100是官方实验环境,做量化对比时效率会高很多。我自己没有那么多卡,实际用的是两张24G显卡串联,batch size调到16,beam size设为5,gen_max_length设为512,总显存占用大约在38G左右——也就是说一张24G卡确实会爆,两张24G卡才能舒服地跑完整评估。
还有一点不要忽略,就是CPU内存和CPU线程数。评估时要加载LLaVA的tokenizer和processor,并对每张图片做preprocess,这些操作吃的是CPU和内存。我一开始把num_workers调得特别大,结果内存直接飙到90G,后来控制在16个worker才好一些。如果你的服务器内存没有64G以上,建议num_workers设在8以内。
另外,强烈建议全程用bf16或fp16跑,不要用fp32。fp32不仅显存占用翻倍,速度还会慢一半以上,而最终评估指标几乎没有可察觉的差异。
1.3 版本问题比你想的更严重,不要直接装最新版
transformers、accelerate、peft、deepspeed这几个库的版本,对这个项目的运行结果影响非常大。官方在environment.yml里有明确指定,但我一开始偷懒直接按照常规流程conda env create了一下,然后手滑把几个包的版本升级到了最新版——结果transformers的generate接口签名变了,加载LLaVA的auto_class也出现兼容问题,最离谱的是beam search的代码行为都变了,生成结果直接不可复现。
这个问题后面细说,但提前给大家提个醒:复现论文时,凡是代码仓库里锁了版本的依赖,一个都不要乱升。你不需要去理解为什么锁这个版本,你只需要知道这个版本是人家验证过的。
2. 环境搭建与数据准备:这一周的开端
2.1 基础设施与版本锁定清单
先交代我的实际环境,供参考。操作系统是Ubuntu 20.04,显卡是两张NVIDIA 24G卡,驱动版本535.129.03,CUDA版本11.8,Python环境用的是conda管理的3.10。这个组合跑OPERA没有遇到底层冲突。
下面是关键依赖版本,建议大家直接盯死:
- PyTorch 2.1.2
- torchvision 0.16.2
- transformers 4.36.2
- accelerate 0.25.0
- peft 0.7.1
- deepspeed 0.13.1
- bitsandbytes 0.41.3
- einops 0.7.0
- numpy 1.24.4
- opencv-python 4.8.1
有个我自己当时忽略的坑:numpy版本不能太高,2.x版本会导致一些老代码里np.float报错。如果你在跑评估脚本时遇到AttributeError: module 'numpy' has no attribute 'float',不用怀疑,就是版本问题。
建议用这个顺序创建环境:
conda create -n opera python=3.10 conda activate opera pip install torch==2.1.2 torchvision==0.16.2 --index-url https://download.pytorch.org/whl/cu118 pip install transformers==4.36.2 accelerate==0.25.0 peft==0.7.1 pip install deepspeed==0.13.1 bitsandbytes==0.41.3 pip install einops numpy==1.24.4 opencv-python==4.8.1装完以后建议先跑一个最小测试确认torch和CUDA是通的:
python -c "import torch; print(torch.cuda.is_available(), torch.cuda.device_count())"当时我在这一步卡了将近一个小时,因为conda默认给我装了CPU版的PyTorch,跑出来的结果是False。如果你也碰到这种情况,老老实实卸载重装不要挣扎。
2.2 拉取代码与目录结构解析
git clone https://github.com/shikiw/OPERA.git cd OPERA官方仓库的目录结构大概是这样的:
opera/——核心代码目录,包括模型定义、解码策略、注意力修改等scripts/——训练和评估的脚本eval/——CHAIR评估相关代码data/——放COCO数据集和标注文件的位置output/——生成结果和checkpoint输出目录
在跑任何脚本之前,建议花二十分钟把opera目录下的代码浏览一遍,不用看懂每行,但至少要能回答这个问题:模型在生成时到底走了哪条forward路径。我当时就是因为没有提前看代码,导致后面改解码参数时完全找不着北。
2.3 COCO数据集与标注文件:比想象中麻烦的下载流程
评估CHAIR指标需要COCO Caption数据集和标注文件,这个步骤看起来没什么技术含量,但坑不少。
需要提前准备的东西有:
- COCO 2014 train/val的图片,注意不是COCO 2017,是2014
- annotations/captions_val2014.json——评估用的标准标注文件
- COCO的train2014和val2014图片,因为某些生成和对比实验需要用到训练集图片做参考
COCO数据集下载地址是cocodataset.org,但是国内服务器直连经常会出现断流情况。我当时是用aria2分批下载的,每批5个文件,中断后可以续传。如果你在校园网或者公司内网,可能还需要配置代理。图片下载下来大概13G,标注文件不到1G,建议先下载标注文件,再下载val2014图片,最后下train2014图片——因为评估只用val,train是给训练流程用的。如果你只做评估,可以先不下train,等真需要训练了再补。
3. 论文核心机制拆解:知道代码在干什么再去改它
3.1 过度信任惩罚项(Over-Trust Penalty)是怎么工作的
OPERA这篇论文解决的核心问题是多模态大模型的幻觉。什么是幻觉?就是模型看图时生成描述,图里明明没有“猫”,但它能绘声绘色地描述出一只橘猫蹲在沙发上。更隐蔽的是,这种幻觉生成时模型的置信度往往很高,看起来不像是在胡编。
论文作者从注意力模式的角度找到了一个现象:在生成幻觉token之前,模型的注意力总是呈现一种局部集中的模式——即前面的token中有一部分对当前token的注意力权重异常高,形成类似于“过度信任”的relationship。他们在LLaVA等模型上做统计分析,发现这种现象在幻觉生成中显著多于正确生成。
基于这个发现,OPERA在beam search中引入了一个惩罚项。生成下一个token时,不仅要看语言模型头给出的logits,还要根据已生成序列中注意力权重的分布情况计算一个惩罚分。如果当前注意力权重满足“过度信任”模式,也就是存在一些历史token对当前预测的影响过大,就对该候选token的分数做惩罚,压低它被选中的概率。
用公式风格的话描述就是:注意力权重满足特定条件时,新生成token的得分 = 原得分 - α * 惩罚项。这个α是超参数,论文默认是1,但不是所有场景都最优。我后面在实验时发现,α在0.5到1.5之间变化对CHAIR指标的影响相当明显,后面详细说。
3.2 回溯分配机制(Retrospection-Allocation)在解决什么
惩罚项是一个预防手段,但总有些幻觉token在生成过程中仍然被选中了。OPERA的方案是“秋后算账”:在beam search生成结束后,增加一个回溯检查阶段。
具体做法是:检查生成序列中是否存在符合“过度信任”特征的位置。如果发现了,就把该位置之后生成的token全部标记为可疑,然后重新在该位置进行解码,试图找到其他候选token来替代。这个替代过程也不是随机选的,而是倾向于选择分配分数更均匀、惩罚分更低、更不容易造成幻觉的token。如果多次重试后生成的序列仍然包含幻觉,就选择原始序列中分数最高的输出。
这个机制的关键在于它并不改变模型的参数,只是把解码过程中的候选重新排列了一个优先级。意思就是,同样的模型、同样的图片,用普通beam search生成会出幻觉,用OPERA生成时,幻觉token虽然偶尔还是会被预测出来,但在最终排序时会被其它更靠谱的候选挤掉。
这也是为什么OPERA没有引入额外训练模块,推理开销却比普通beam search略高一些的原因。回溯阶段需要额外做几次前缀解码来确认候选token的质量,所以不能简单地用“解码时只做一次前向”的视角去算它的开销。
3.3 为什么这些机制能抑制幻觉,原理不那么复杂
通俗理解OPERA的思路:模型之所以产生幻觉,一个原因是它对某些局部token关系过于自信,尤其是对前面的实体词,比如“there is a cat”,模型越往后生成越可能紧扣着“猫”去编相关物体,哪怕图片里根本没有。OPERA的做法相当于在投票环节里,对几个特别喜欢抱团的历史token说“你们的票数权重先降一点”,防止它们把候选token的影响力垄断了。
而回溯机制更像是在开票后做了一次抽检,一旦发现票数集中在某个实体词周边,就重新唱票一次,看看有没有别的候选本来也有机会。这相当于给生成结果加了一个后置的“纠偏器”。
理解了这些原理,后面改代码、调参数时思路会清晰很多:惩罚项影响的是候选token的排序,回溯阶段影响的是最终输出的选择策略。两个阶段分别调节,复现阶段就能逐一验证。
4. 跑通评估流程:一步步看代码,一步步做实验
4.1 从官方脚本出发,先跑通一个mini测试
官方仓库里提供了评测脚本,路径大致在scripts/下。我在跑完整流程之前,先手动构造了一个min测试:从COCO val2014数据集里随机选了20张图片,用脚本跑一遍生成和CHAIR计算。这一步非常关键,它帮我提前暴露了很多环境问题,避免了在完整数据上来回试错。
MINI测试的核心命令简化后如下:
CUDA_VISIBLE_DEVICES=0,1 \ python eval/run_eval.py \ --model_name llava-1.5-7b \ --eval_dataset coco \ --eval_type mini \ --num_eval_samples 20 \ --batch_size 4 \ --max_new_tokens 512 \ --beam_size 5 \ --penalty_weight 1.0注意,--num_eval_samples这个参数在官方脚本里可能不叫这个名字,要根据仓库里的实际写法调整。我当时的做法是临时注释掉数据切分的部分,改成只取前20条数据。这样测起来快,出问题也能快速定位。
跑完生成后,会得到一个json文件,里面保存了image_id、generated_caption、predicted_tokens等信息。接着用这个json文件跑CHAIR评估脚本,计算CHAIRs(句子级幻觉率)和CHAIRi(实例级幻觉率)两个指标。只有两个指标都跑出来,才算把评估闭环打通了。
4.2 关键参数实测:penalty_weight到底调到多少合适
论文里说penalty_weight默认设为1.0,但实际跑下来我觉得这个值应该根据你的基座模型和数据情况再做一点调整。我分别测了0.5、1.0、1.5三组数据:
- penalty_weight=0.5:生成结果的流畅度比较好,但CHAIRs指标比论文值略高,说明惩罚力度不够,部分幻觉token仍然进了候选序列。
- penalty_weight=1.0:最接近论文报告数值,CHAIRs大约在45.3左右,CHAIRi大约在12.8左右,这个结果和论文表里的数字能够对得上。
- penalty_weight=1.5:CHAIRs确实还能再降一点,但生成描述开始出现比较刻板的短语重复,尤其是“a man and a woman”这种高频套话明显增多,说明惩罚过猛导致解码偏向安全但平庸的路径。
以我实验的感受来看,论文选1.0不是随便定的,而是平衡了幻觉指标和生成多样性之后的选择。建议优先从1.0起步,再根据你的基座模型表现微调,调整范围控制在0.8到1.2之间。
4.3 评估指标CHAIR的计算逻辑与结果解读
CHAIR是评估多模态幻觉的经典指标,全称是Caption Hallucination Assessment with Image Relevance。它分两个层次:
- CHAIRs(sentence-level):统计所有生成句子中,包含幻觉物体的句子比例。分子是包含幻觉物体的句子数,分母是生成的句子总数。这个指标反映的是生成的描述有没有在句子层面“吹牛”。
- CHAIRi(instance-level):统计生成描述中所有提到的物体实例里,幻觉实例占的比例。它能反映出幻觉物体在所有出现物体中的占比,比CHAIRs更严格。
举个例子,一张图片里只有一只狗,模型生成了“a dog and a cat”,那这条caption的CHAIRs算1(存在幻觉物体cat),CHAIRi是0.5(两个物体里有一个是幻觉)。
跑完评估后,我的结果和论文的表1基本对上了。用LLaVA-1.5-7B作为基座,原始beam search的CHAIRs约为50.7%,CHAIRi约为15.0%;换成OPERA解码策略后,CHAIRs降到45.3%,CHAIRi降到12.8%。这个降幅虽然没有特别夸张,但在大规模评估集上已经算是非常稳定的收益了。而且从生成的caption质量来看,描述并没有因为抑制幻觉而变得生硬,这说明OPERA的干预是相对温和的。
5. 太容易踩的坑:这一周我遇到的所有故障
5.1 依赖版本冲突与transformer接口魔改
我必须再次强调一遍版本冲突,因为它浪费了我整整半天。最开始我图省事,直接用pip install -r requirements.txt,结果transformers被装成了4.40以上的版本,而仓库代码里大量使用了model.generate的旧接口行为,包括beam search过程中的自定义logits processor。transformers在4.38以后对generate的重构比较大,之前能用的若干钩子位置都变了,导致我的自定义惩罚项根本没有生效。最迷惑的是,程序不报错,生成也能跑完,但CHAIR指标和baseline几乎一样——因为惩罚项压根没有进到解码流程里。
排查方式也很笨但很有效:在自定义logits_processor里加了一个print,把修改前后的token分数打印出来对比,才发现分数完全没变。定位到问题后,直接把transformers降到4.36.2,一切都正常了。
还有一个和deepspeed有关的问题。如果你不开zero stage,在单机多卡运行时deepspeed会默认初始化一些环境变量,导致只用到了一张卡,速度变成乌龟。我后来在启动命令里明确指定--master_port=29500并设置deepspeed_config,才能正常多卡并行。
5.2 显存上限与bf16推理技巧
OPERA在生成阶段要保存每层的注意力权重,用于后续计算惩罚项,这比普通推理更吃显存。之前在batch_size=32、beam_size=5的条件下,24G显存直接OOM。我试过把batch_size降到8,还是不够稳定;最后用bf16配合gradient_checkpointing(即便只是在推理阶段,这个选项也会减少激活值缓存)才勉强压到单卡可跑,但速度很慢。
后来我换成了双卡部署,生成的batch_size才稳定在16。如果大家只有单卡,建议把beam_size降到3——思路是让每次生成的候选减少,相应降低注意力缓存的占用。虽然这会略微偏离论文默认参数,但指标差异不大,可以作为单卡环境下的降级方案。
这里有一个很关键的细节:论文里的beam_size=5,但测试时务必和论文保持一致。因为你最终要对比的是“复现结果”和“论文报告值”,如果beam_size都变了,对比就没有意义。我自己做过一个对照实验,beam_size从5降到3后CHAIRs会上升约2个百分点,原因是候选路径变少了,回溯机制的选择空间也被压缩了。
5.3 评估脚本报错与CHAIR数值对不齐
我在跑CHAIR评估脚本时,遇到过一个非常容易误导人的情况:官方脚本输出里的CHAIRs和CHAIRi用的是COCO Caption的词汇表和一个自动解析的物体概念列表,如果你的coco标注文件路径没配对,脚本会默认使用一个更小的物体类别列表,导致指标数值整体偏高或偏低。
还需要注意一点,评估脚本要求生成的caption必须是纯文本,不能带特殊token。比如我用LLaVA时,生成的caption如果不加处理,可能包含</img>或空格残缺符号,这些噪声会让评估结果产生几上几下的波动。最保险的做法是生成后用正则把非字母数字字符清理一遍,再做CHAIR计算。
整个跑完以后,不能只看一两个小批量结果就下结论。我在20张图片上跑出来的CHAIRs和完整5000张val图片上跑出来的相差接近7个百分点,这是因为小样本集的不确定性太大。如果预算允许,尽量用完整val集做最终结论,mini测试只用来验证流程通不通。
5.4 复现结果对比表:我的数据过程记录
为了让大家有个直观感受,我把自己复现过程中的几组数据贴出来。以下是在相同条件下(LLaVA-1.5-7B、COCO val2014、max_new_tokens=512)重复实验得到的典型数值:
| 配置 | CHAIRs(句子级) | CHAIRi(实例级) | 备注 |
|---|---|---|---|
| 原始论文报告 | 未明确列出基线的同组对照 | 以beam search基线为参照 | 参考论文表1 |
| 普通beam search复现 | 约50.7% | 约15.0% | 使用官方llava推理 |
| OPERA解码(penalty=1.0) | 约45.3% | 约12.8% | 稳定压过baseline |
| OPERA解码(penalty=1.5) | 约43.9% | 约11.7% | 流畅度略下降 |
| 单卡fp16、beam_size=3 | 约47.5% | 约13.6% | 降级方案,指标略高 |
注意这组数据只是我实际跑出来的,不代表官方权威结果。但它能反映一个问题:OPERA的收益方向是稳定的,但收益大小高度依赖解码参数。这也是复现这类工作时比较重要的一点:不要只跑一个配置就下结论,至少要有对照组才能判断复现成功还是失败。
6. 一周复现的经验总结与后续扩展思路
6.1 我对复现这件事的一点体会
这一周跑下来,我的体会是:复现一篇论文,最耗时的往往不是代码阅读,而是环境对齐和隐藏依赖的排查。很多论文代码都能跑通,但只有在你用相同版本、相同数据、相同解码参数时才能复现出论文里的数字。任何一环的偏差都会传导到最终指标上,而排查这种偏差,恰恰是最考验耐心和经验的地方。
OPERA这个项目算是一个很不错的复现对象,因为它不涉及大规模训练,核心逻辑集中在解码策略上,代码量不大,机制清晰,指标也是开箱即用的CHAIR。如果你想入门多模态大模型的工作,想理解“在不改模型参数的情况下改善生成质量”这件事,OPERA是一个既不算太难、又足够有价值的选择。
6.2 后续还能怎么玩:几个可扩展的方向
跑通评估闭环只是第一步。我觉得在这个基础上,至少有这几个方向值得继续探索:
一是把它移植到更新的模型上。比如把OPERA的注意力惩罚模块接入到Qwen-VL或者LLaVA-NeXT上,看看在新更强的基座上抑制幻觉的效果是否依然成立。不同模型的注意力模式有差异,OPERA的触发条件可能需要针对性地调整。
二是尝试改变惩罚项的计算方式。原版是用注意力权重行归一化后的熵来度量“过度信任”,我们完全可以尝试更轻量的度量方式,比如只统计最近几个token的注意力集中程度,这样可以在减少计算开销的同时保持抑制效果。
三是把OPERA和推理时缩放(inference-time scaling)的思路结合。这两年大家越来越关注test-time的更多计算投入换取最终生成质量,OPERA的回溯机制本身就是一种test-time策略,如果在此基础上叠加更精细的自我一致性校验,或许能进一步压低幻觉率。
6.3 最后一个小技巧
最后再分享一个非常实用的小技巧:在复现任何解码策略类的论文时,一定要把你自定义的logits processor单独做一次单元测试。方法很简单,构造一个假模型和假tokenizer,用随机的attention输出跑一遍生成,对比“开不开自定义processor”时top1 token变化是否正常。这个测试能在10分钟内排除掉百分之八十的“代码没生效”问题,避免你在完整评估集上跑了好几个小时,最后才发现处理器根本没被调用的尴尬。
这一周的复现工作到这里算是画上了一个句号。虽然过程曲折,但最后看到CHAIR指标明显下降、生成结果里确实少了那些无中生有的“狗”和“猫”时,还是觉得这几天的折腾完全值得。如果你也打算复现OPERA,希望这份记录能帮你少踩几个坑、早一点看到属于自己的结果。