ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

多模态大模型三阶段持续提升:SFT、RL与UPT的完整实践指南

多模态大模型三阶段持续提升:SFT、RL与UPT的完整实践指南

看到这个标题的时候我正好在梳理多模态大模型的微调实验清单,然后整个人就顿了一下。2025年机器学习顶会上这篇标题被截断的工作,前半段信息量已经足够大了:First SFT, Second RL, Third UPT: Continual Improving Multi-Modal LLM Reasoning via Unsu...。做多模态大模型(Multi-Modal LLM,下面简称MLLM)的人,看到SFT和RL这两个缩写都会会心一笑,这是当下对齐技术的两板斧;真正让人停下来琢磨的,是UPT,以及后面被截断的Unsu。如果是Unsupervised,那这篇文章讲的就是一条完整的三阶段持续提升管道:用有监督微调打底,用强化学习对齐偏好,最后用无监督信号继续涨点。我想把这个管道拆开揉碎,结合我实际复现过程中的经验和踩坑记录,给你一份能直接拿去用的参考。这个内容适合正在做MLLM微调、对齐、推理能力提升的工程师和研究者,也适合刚入门、想搞清楚SFT、RL、偏好学习之间到底是什么关系的同学。

1. 标题里的“三步走”到底说了什么

1.1 为什么一步到位不行

先聊一个最基础的问题:为什么不能只做SFT,或者只做RL,非要分三步?

我的理解是这样的。SFT(Supervised Fine-Tuning)解决的问题是让模型“会说”和“会听”。你给它一批问题-答案对,让它学会期望的作答格式、语气、知识范围。但问题是,SFT的输出质量上限由采集的标注数据决定。标注数据里有什么,模型就会到什么水平;标注数据里没有的推理步骤,模型很难自己长出来。

RL(Reinforcement Learning)解决的是“在答案空间里挑选更优解”。它不教模型具体的句子,而是给定一组候选答案,通过奖励信号告诉模型哪个更好、哪个更差,然后让模型往好的方向偏移。问题在于,纯RL直接跑在SFT模型上,非常容易把模型带到“奖励黑客”的方向——模型学会了钻空子,输出自相矛盾的长篇大论,表面上符合奖励函数,实际推理质量一塌糊涂。

UPT(结合标题前半段的语境,我更倾向把它理解为Unsupervised Preference Tuning,无监督偏好调优)解决的,是当外部标注资源和人工反馈都枯竭之后,模型还能不能继续提升的问题。它通过模型自身产生多样性数据、自行构造偏好信号,在没有任何人工标注的情况下完成下一轮自我优化。

所以这三步不是简单的“按顺序跑一遍”,而是每一层都在解决上一层的遗留问题。SFT先给模型一个稳定的行为基座,RL在基座上做偏好对齐,UPT在数据匮乏阶段用自生成信号做持续优化。这就是标题里Continual Improving的含义:并不是跑完第三阶段就结束,而是三个阶段可以在不同数据块上循环使用。

1.2 三步各自的定位

我习惯用一句话概括三步:

  • SFT让模型“毕业”,RL让模型“拿高分”,UPT让模型“在没有新题目的情况下自己刷题”。

这句话虽然不那么严谨,但能帮你快速想清楚什么时候该用哪一步。

具体到技术层面,三者的训练对象和损失函数也不同。SFT用的是标准的交叉熵损失,监督信号是完整的参考答案;RL用的是策略梯度或隐式奖励优化,监督信号是偏好对(chosen和rejected);UPT本质上也是偏好学习,但它的偏好对不是人工标注的,而是由模型自己采样、自己评判得到的。讽刺的是,整条链路里最贵的不是RL,而是UPT的数据构建逻辑——你要自己设计一套可信任的“伪偏好信号”,否则第三阶段会把模型带偏。

我在复现这个思路时也深刻体会到,很多时候模型提升的瓶颈不再是模型结构或者训练技巧,而是数据从哪里来。SFT靠人工,RL靠人类偏好,UPT靠算法自生成,这三个阶段的演进本质上是“数据来源从外部转向内部”的演进。

2. 第一步SFT:先把“说人话”的地基打好

2.1 数据构造才是真正的门槛

如果你真的打算复现这套三步走,我建议你把三分之二的精力压在SFT阶段的数据准备上。原因很简单:RL和UPT的质量上限,完全取决于SFT之后模型的初始状态。SFT阶段如果模型连基本的图文对应关系都不稳定,后面任何强化学习都是空中楼阁。

多模态SFT数据和纯文本SFT数据有一个本质区别:每条训练样本里都有图像和文本两个模态,而图像信息必须经过视觉编码器变成向量,再和文本token拼接。这就带来两个实际问题。

第一,数据的图像-文本对齐质量必须严格把关。我做数据清洗的时候发现,公开数据集里不少样本存在图文错配和OCR乱码问题。你看着两条样本差不多,其实一张图配了错误的文本,模型学着学着就把“图和文有什么关系”这个最根本的事情学歪了。我的做法是先跑一个小的视觉问答预检模型,把所有训练样本过滤一遍,凡是预检模型答非所问的一律剔除,这个操作大概会过滤掉5%到10%的脏数据,但换来的是训练稳定性的明显提升。

第二,CoT(Chain-of-Thought)数据要比单步答案数据更值得投入。这里有个很关键的细节——推理能力提升的核心并不是“多算几步”,而是模型能看到中间推理过程。你可以在SFT阶段给模型灌输大量带有完整思维链的答案,让它在RL阶段输出的候选答案本身就具有“先推理、后结论”的结构。我实际测试过,SFT数据里CoT占比在60%以上时,后面RL阶段的收敛速度和稳定性都会有可感知的改善。

2.2 训练细节与参数底座

SFT阶段的训练参数配置,取决于你用的是全参微调还是LoRA。我的个人建议是做多模态推理项目时,优先尝试LoRA,尤其是当你的显存资源有限的时候。

我以一个7B规模的MLLM为例,给出一个实测过的基础配置参考(注意这只是参考,不是标准答案):

参数项推荐值或推荐做法备注
训练方式LoRA (rank=64, alpha=128)显存压力小,后续RL阶段便于切换
学习率1e-5,带warmup (前3%步数)高学习率容易让视觉编码器剧烈漂移
批次大小128条样本(gradient accumulation凑)多模态样本短序列多,实际tokens看batch
训练轮数2~3轮再多容易过拟合指令格式
视觉编码器冻结或极低学习率(1e-7)更新视觉编码器收益不稳定
最大序列长度2048~4096长图中包含多个OCR块时尤其注意
损失计算范围仅计算Answer部分token不计算图像token和系统提示token

实操的时候有几个细节很容易被忽略。

第一个事:大部分开源MLLM的模板里会有类似<image>是什么?这样的指令分隔,训练时务必确保loss mask正确,否则模型会把prompt部分也背下来,到了推理阶段就会在回复里复述用户输入,非常呆。

第二个事:多模态数据里同一张图可能被多次使用,分别搭配不同的问题。我在数据组织上会把同一张图的所有变体放在同一个批次里,而不是打散到全局。这样可以减少同一个视觉编码在不同batch里反复正向反向传播的浪费,训练速度会快一些。

第三个事:不要一上来就冻结视觉编码器不更新。对于复杂图表类数据,视觉编码器完全不更新的话,OCR位置信息和图表结构信息可能提取得不够。我建议的做法是前三分之一的训练步数冻结视觉编码器,后半程解锁,并用主学习率的千分之一来更新它。这个策略在多个数据集上都验证有效。

3. 第二步RL:用偏好信号让模型学会“挑好答案”

3.1 PPO还是DPO,这是个成本问题

进入RL阶段,第一个绕不开的决策就是:用PPO还是DPO。很多刚接触这个方向的同学会在这一步纠结很久,我给出一个比较务实的判断标准——训练资源和稳定性。

PPO需要同时加载策略模型、参考模型、奖励模型和价值模型,至少是四份模型副本,显存开销非常夸张。好处是你可以用任何形式的奖励信号,包括过程奖励、结果奖励、甚至手工规则奖励;坏处是超参数敏感,KL系数和GAE参数稍微不合适,模型输出就开始崩,表现为吐乱码、重复短语、情绪空洞。

DPO的最大优点是把偏好学习简化为一个分类式目标,直接对偏好对(chosen和rejected)做优化,不需要单独的奖励模型。它只需要策略模型和参考模型两份权重,训练机制也更接近SFT,稳定性好很多。代价是你必须得有高质量的偏好对数据——这一点在多模态领域尤其稀缺。

我的个人建议是:如果你手头只有几万条偏好对,而且数据质量没有把握,选DPO;如果你有明确的奖励函数定义,比如数学题想用最终答案对错作为奖励,那可以考虑PPO,但要为调参留出时间。

3.2 多模态场景下的奖励设计

多模态RL和纯文本RL相比,有一个额外的难点:模型的输入一半是图像token,一半是文本token,当你只对文本输出计算奖励时,模型很难通过梯度把“文本不好”这个信号传导到视觉编码层。换句话说,图像理解部分很难被RL信号直接修正,所以RL阶段的大部分收益来自优化模型的“语言组织策略”和“推理路径选择”,而不是视觉感知能力。

这个认知很重要,它影响你的数据设计。我实际做下来,效果最好的偏好对样本不是单纯的“答案错vs答案对”,而是“推理路径混乱但答案碰巧对”和“推理路径清晰且答案对”的对比。也就是说,偏好对应该重点体现推理过程的优劣,而不是只看最终结果的正确性。这也是为什么我前面强调SFT阶段要灌CoT数据——有了CoT,RL阶段才能对“推理链的每一步选择”构造出有价值的偏好信号。

具体训练中,我推荐使用DPO损失,其中超参数β(温度系数)通常取0.1到0.3之间。β太大会让模型对偏好差异过于激进,导致输出变得极端;β太小则优化效果不明显。如果你实验中发现训练完成后模型输出明显变长、但逻辑密度下降,多半是β偏大。此时把β从0.2降为0.1,往往能缓解“话痨化”问题。

另外,有一个我曾经踩过的坑:DPO训练的时候梯度同时作用在chosen和rejected两条输出上。如果rejected输出特别短,而chosen输出特别长,模型为了迎合偏好,会在长度维度上钻空子——疯狂加长输出。所以我在构造偏好对的时候,会做一个长度匹配:chosen和rejected的token长度尽量控制在1.2倍以内。这一点对最终的稳定性影响非常大。

4. 第三步UPT:没有新标注,怎么继续涨点

4.1 UPT的两种理解方式

UPT这个缩写在学术圈里其实并不像SFT和RL那样有公认定义,结合这个标题的语境,我倾向于认为它是Unsupervised Preference Tuning。但如果作者实际写的是Unified Preference Training,逻辑上也说得通。不管是哪种,核心思想都是统一的:在偏好数据不依赖人工标注的情况下,让模型自己生成、自己评价、自己更新。

这里需要强调的是,UPT并不等于“无监督学习”。它依然是一种偏好学习,只是偏好对来源于模型自身的采样。这种“用自己当老师”的做法,在纯文本推理领域已经有不少变体,比如拒绝采样微调、自一致性蒸馏等。而放到多模态场景下,难点在于自生成数据的质量怎么判断——因为图像理解错误和文本推理错误经常纠缠在一起。

4.2 自生成偏好数据的流程

我复现这个思路时,把UPT的完整流程拆成了四个环节,下面逐一展开。

  • 第一环:采样。选取一批未标注的图文输入,让当前模型以较高温度(我常用temperature=0.8或1.0)采样多条输出,一般每条输入采4到8条。
  • 第二环:筛选。这步是核心,也是最需要工程功力的。对多模态推理任务而言,我采用的方法是先找个可计算的客观结果做粗筛。比如数学题以最终答案是否匹配为第一道门,把答案错误的样本直接丢弃;然后对答案正确的多个输出,用模型自身计算自一致性分数,逻辑上越一致的输出越可能是高质量推理链。
  • 第三环:组对。把经过筛选的高质量输出作为chosen,把那些答案错误、但长度相似且在指令格式上“看起来还行”的输出作为rejected,构造出偏好对。
  • 第四环:更新。使用DPO或者KTO之类的偏好优化算法,在新的自生成偏好对上对模型进行一轮轻量更新。

这个流程本质上是在人为制造一个“答案已知、过程未知”的训练环境,让模型从自己过去的尝试中学习。这里我不推荐直接用答案正确性作为唯一筛选标准,因为多模态推理中经常出现“最终答案对但推理过程有严重幻觉”的情况,比如把橙色的苹果说成是香蕉,最后绕回一个正确数字。所以筛选阶段,我在答案匹配之外还会用一个简单的幻觉检测启发式:让模型在回答之前先描述图像中的关键元素,然后检查描述是否和实际标签匹配。这个操作虽然朴素,但确实能过滤掉不少“碰巧答对”的样本。

UPT还有一个容易被忽略的技术要点:别忘了冻结参考模型。无论你是用DPO还是其他偏好优化方法,参考模型都必须保持在更新前的状态,否则偏好优化目标就变成了移动靶。我在早期实验中偷懒没有冻结参考模型,结果训练第一个epoch后就开始出现严重的不稳定,输出来回摇摆,损失曲线像心电图,后来老老实实冻结才恢复正常。

4.3 持续改进的机制:防止模型学完就忘

既然标题强调的是Continual Improving,那UPT阶段就必须考虑灾难性遗忘的问题。我在实际操作中发现,单纯在自生成数据上重复做UPT,模型会在两三轮之后出现一种症状:新任务的理解越来越顺,但是之前SFT阶段学会的基础指令遵循能力开始退化。比如你问一个图像里有几把椅子,它可能答对了,但问“请用JSON格式输出标题和描述”的时候,又忘记了JSON格式要求。

解决这个问题的办法不复杂,叫数据重放。具体操作上,我每一轮UPT都会混入20%左右的历史训练数据,其中大部分来自SFT阶段的核心指令数据。这些数据不参与偏好构造,只是在每个batch里作为普通监督样本出现,稳住模型的全局能力。这个比例我试过10%和30%,20%左右在我的设置下效果最好,既能有效防遗忘,又不至于把UPT优化方向带偏。

还有一个细节:UPT阶段更新幅度一定要小。我用的学习率只有SFT阶段的十分之一到五分之一,即1e-6到2e-6之间。如果你发现UPT之后模型输出的基础能力没有变化,那大概率是学习率太低;如果你发现模型开始变得“油腔滑调”,那大概率是学习率太高,或者偏好对质量不过关。这种“微调量级”的把控,可能是UPT阶段最核心的工程判断。

5. 多模态推理训练中的实操避坑清单

5.1 图像Token与文本Token的隐形冲突

多模态模型训练里有一个很隐蔽的问题:图像token的数量往往远大于文本token。一张高分辨率图片切成patch之后,可能产生数百甚至上千的视觉token,而一道数学题的问题文本可能只有几十个token。当这两部分拼接进同一个序列时,attention的计算开销会严重偏向图像侧,而反向传播时梯度也主要集中在图像token上。

这在RL阶段尤其危险。因为DPO这类偏好优化的目标是文本输出,如果视觉token占比过高,模型会把大量容量都花在“更精细地看图像”上,而对文本输出的优化不足。我建议在数据准备期就对图像分辨率做一个统一策略:不是所有任务都需要超高分辨率,简单的视觉问答任务可以适当降采样,只有OCR密集或图表密集的任务才保留高分辨率。把高分辨率图像的占比控制在30%到40%,训练效率和最终效果都会更好。

5.2 训练稳定性与显存占用

以7B MLLM做RL阶段训练为例,我的显存经验数据是这样的:使用LoRA + 8卡80G显存,DPO训练可以比较舒服地容纳单卡batch size 2到4;如果用PPO,那显存直接翻倍还多,经常需要把batch size压到1,模型并行和梯度累积会拖慢整个训练节奏。

为了在有限显存里提升训练效率,我从实践中总结出两个有效手段。第一,用FlashAttention-2,并且手动开启梯度检查点,这样能省下不少激活值显存;第二,把视觉编码器彻底冻结,只优化LLM部分的LoRA参数。视觉编码器的参数量很大,冻结之后不仅显存压力小,反向传播也快了很多。代价是模型无法通过RL阶段改善视觉感知,但就像前面说的,RL本来也不擅长改视觉感知,这个代价可以接受。

5.3 评测陷阱与数据泄漏

最后说一个很多文章不会提、但实践中一定会遇到的问题:评测数据泄漏。当你使用公开的多模态推理数据集来验证三步走的提升效果时,一定要检查数据集是否出现在你的SFT训练数据里。我在一次实验中,SFT阶段不小心混入了评测集的同源数据,结果评测分数暴涨十几个点,我还高兴了两天,后来做消融时发现是泄漏,白高兴一场。

我的排查手段很原始但很有效:把评测数据的文本部分和训练数据做一次模糊匹配,不能只做精确匹配,因为很多公开数据集存在转写噪声。匹配比例超过3%,就要考虑从训练数据里剔除对应样本,或者换一套不相干的评测集。

另外一个评测陷阱是多模态模型对Prompt格式特别敏感。同一道题,用不同的系统提示或者不同的图像占位符格式,得分差异可能非常大。我的习惯是评测时固定一套统一的模板,并且和SFT训练时的模板保持一致,这样才能相对公平地对比不同阶段的提升幅度。

6. 一个可复现的最小管道

6.1 硬件与框架选择

如果你看完前面的分析,想自己动手复现一套,我建议不要一开始就用最大规模的模型。我的经验是:用7B到8B规模的MLLM做验证,跑通管道后再考虑升级。硬件上8卡80G是最舒服的配置,如果资源紧张,4卡也可以,但batch size和训练速度会明显受限。

框架方面我用的是HuggingFace TRL来管理SFT和DPO训练,视觉编码部分用现成的MLLM库加载预训练权重,DeepSpeed做ZeRO-3并行。选这套组合是因为它迭代速度快、社区兼容性好,遇到问题容易搜到解决方案。如果你团队里已经有人熟练使用其他RL框架,那也没必要换,核心训练逻辑都是通用的。

6.2 核心训练流程伪代码

下面是我这套三步走管道的最小化伪代码,可以当作代码骨架来参考。

# stage 1: SFT model = load_multimodal_llm("base-mllm-7b") train_dataset = load_sft_data("sft_mixed.jsonl") # 图文指令数据 trainer = SFTTrainer( model=model, train_dataset=train_dataset, args=training_args(sft), # lr 1e-5, lora_rank 64, epochs 2 formatting_func=apply_multimodal_template, ) trainer.train() model.save_pretrained("stage1_sft_model") # stage 2: RL (DPO) ref_model = load_multimodal_llm("stage1_sft_model") policy_model = load_multimodal_llm("stage1_sft_model") dpo_dataset = load_dpo_pairs("preference_pairs.jsonl") # chosen / rejected dpo_trainer = DPOTrainer( model=policy_model, ref_model=ref_model, train_dataset=dpo_dataset, beta=0.15, args=training_args(dpo), # lr 1e-6, epochs 1 ) dpo_trainer.train() policy_model.save_pretrained("stage2_rl_model") # stage 3: UPT policy_model = load_multimodal_llm("stage2_rl_model") ref_model_for_upt = load_multimodal_llm("stage2_rl_model") # 冻结 unlabeled_queries = load_unlabeled_inference_data("unlabeled_queries.jsonl") new_pairs = construct_preference_pairs( policy_model, unlabeled_queries, temperature=0.8, num_samples=6, answer_match=True, self_consistency=True, ) replay_data = load_sft_data("sft_core_sample.jsonl") # 20%重放 upt_trainer = DPOTrainer( model=policy_model, ref_model=ref_model_for_upt, train_dataset=new_pairs, beta=0.1, args=training_args(upt), # lr 1e-6, epochs 1 ) upt_trainer.train() # 每个batch混入重放数据,用普通SFT损失稳住基础能力 policy_model.save_pretrained("stage3_upt_model")

这套流程看着不长,但每次跑一轮完整的三步走,以我的经验,数据处理和清洗占的时间能到总耗时的一半以上。所以不要小看数据环节。

6.3 值得记录的经验数字

跑完这套流程后,我记录了几个比较敏感的数字,可以给你做参照。SFT阶段3万条左右的高质量图文指令样本,可以支撑起7B模型的指令遵循能力;RL偏好对数量不需要太多,8000到1万条高质量偏好对就能看到明显效果;UPT阶段自生成偏好对推荐做到1.2万条以上,同时混入20%的历史SFT核心数据做重放,这样能保证持续提升的同时不遗忘旧能力。

如果经过一轮完整三步走,多模态推理评测集上的得分提升不到2个百分点,我首先会怀疑SFT阶段的数据质量,多半是CoT占比太低或者脏数据太多。把SFT阶段重做一遍,比继续硬调RL和UPT超参数更划算。这个判断在我多次实验中都被验证了。

我个人在实际操作中最深的体会是:这三个阶段的真正分界线不是算法,而是数据策略。SFT阶段你为数据付费,RL阶段你为偏好付费,UPT阶段你开始为自己的模型判断力付费。越往后走,工程里最值钱的东西越不是显卡和代码,而是你设计“伪偏好信号”时的那点判断力。这也是为什么同一套三步走管道,不同团队跑出来的效果差异能非常大——大家卡的坑根本不在一处。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进