
复现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调到16beam size设为5gen_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.03CUDA版本11.8Python环境用的是conda管理的3.10。这个组合跑OPERA没有遇到底层冲突。下面是关键依赖版本建议大家直接盯死PyTorch 2.1.2torchvision 0.16.2transformers 4.36.2accelerate 0.25.0peft 0.7.1deepspeed 0.13.1bitsandbytes 0.41.3einops 0.7.0numpy 1.24.4opencv-python 4.8.1有个我自己当时忽略的坑numpy版本不能太高2.x版本会导致一些老代码里np.float报错。如果你在跑评估脚本时遇到AttributeError: module numpy has no attribute float不用怀疑就是版本问题。建议用这个顺序创建环境conda create -n opera python3.10 conda activate opera pip install torch2.1.2 torchvision0.16.2 --index-url https://download.pytorch.org/whl/cu118 pip install transformers4.36.2 accelerate0.25.0 peft0.7.1 pip install deepspeed0.13.1 bitsandbytes0.41.3 pip install einops numpy1.24.4 opencv-python4.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是2014annotations/captions_val2014.json——评估用的标准标注文件COCO的train2014和val2014图片因为某些生成和对比实验需要用到训练集图片做参考COCO数据集下载地址是cocodataset.org但是国内服务器直连经常会出现断流情况。我当时是用aria2分批下载的每批5个文件中断后可以续传。如果你在校园网或者公司内网可能还需要配置代理。图片下载下来大概13G标注文件不到1G建议先下载标注文件再下载val2014图片最后下train2014图片——因为评估只用valtrain是给训练流程用的。如果你只做评估可以先不下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_DEVICES0,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_weight0.5生成结果的流畅度比较好但CHAIRs指标比论文值略高说明惩罚力度不够部分幻觉token仍然进了候选序列。penalty_weight1.0最接近论文报告数值CHAIRs大约在45.3左右CHAIRi大约在12.8左右这个结果和论文表里的数字能够对得上。penalty_weight1.5CHAIRs确实还能再降一点但生成描述开始出现比较刻板的短语重复尤其是“a man and a woman”这种高频套话明显增多说明惩罚过猛导致解码偏向安全但平庸的路径。以我实验的感受来看论文选1.0不是随便定的而是平衡了幻觉指标和生成多样性之后的选择。建议优先从1.0起步再根据你的基座模型表现微调调整范围控制在0.8到1.2之间。4.3 评估指标CHAIR的计算逻辑与结果解读CHAIR是评估多模态幻觉的经典指标全称是Caption Hallucination Assessment with Image Relevance。它分两个层次CHAIRssentence-level统计所有生成句子中包含幻觉物体的句子比例。分子是包含幻觉物体的句子数分母是生成的句子总数。这个指标反映的是生成的描述有没有在句子层面“吹牛”。CHAIRiinstance-level统计生成描述中所有提到的物体实例里幻觉实例占的比例。它能反映出幻觉物体在所有出现物体中的占比比CHAIRs更严格。举个例子一张图片里只有一只狗模型生成了“a dog and a cat”那这条caption的CHAIRs算1存在幻觉物体catCHAIRi是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_port29500并设置deepspeed_config才能正常多卡并行。5.2 显存上限与bf16推理技巧OPERA在生成阶段要保存每层的注意力权重用于后续计算惩罚项这比普通推理更吃显存。之前在batch_size32、beam_size5的条件下24G显存直接OOM。我试过把batch_size降到8还是不够稳定最后用bf16配合gradient_checkpointing即便只是在推理阶段这个选项也会减少激活值缓存才勉强压到单卡可跑但速度很慢。后来我换成了双卡部署生成的batch_size才稳定在16。如果大家只有单卡建议把beam_size降到3——思路是让每次生成的候选减少相应降低注意力缓存的占用。虽然这会略微偏离论文默认参数但指标差异不大可以作为单卡环境下的降级方案。这里有一个很关键的细节论文里的beam_size5但测试时务必和论文保持一致。因为你最终要对比的是“复现结果”和“论文报告值”如果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_tokens512重复实验得到的典型数值配置CHAIRs句子级CHAIRi实例级备注原始论文报告未明确列出基线的同组对照以beam search基线为参照参考论文表1普通beam search复现约50.7%约15.0%使用官方llava推理OPERA解码penalty1.0约45.3%约12.8%稳定压过baselineOPERA解码penalty1.5约43.9%约11.7%流畅度略下降单卡fp16、beam_size3约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希望这份记录能帮你少踩几个坑、早一点看到属于自己的结果。