找回密码
 立即注册
首页 业界区 科技 从零开始:PPO 微调大模型实战(基于 PyTorch) ...

从零开始:PPO 微调大模型实战(基于 PyTorch)

凳舒 2026-1-23 02:05:13
<h1 id="从零开始ppo-微调大模型实战基于-pytorch">从零开始:PPO 微调大模型实战(基于 PyTorch)</h1>
<h2 id="ppo-真正难的不是算法本身">PPO 真正难的,不是算法本身</h2>
<p>如果你已经看过一些 PPO 的原理文章,大概率会有过这种感觉:</p>
<p>好像每个字都认识,但真让我自己写代码,脑子还是一片空白。</p>
<p>这其实挺正常的。<br>
至少我第一次准备动手写 PPO 的时候,也是这种状态。</p>
<p>问题不在你,而在 PPO 本身。</p>
<p>在论文里,PPO 看起来是一个干净利落的算法;<br>
但一旦落到工程里,它立刻变成了一整条系统链路:</p>
<ul>
<li>模型自己生成内容</li>
<li>用 reward model 打分</li>
<li>再算 KL 约束</li>
<li>再算 advantage</li>
<li>然后还要小心翼翼地更新多轮</li>
</ul>
<p>任何一个地方写错,都不一定会立刻报错,但后果可能很严重:</p>
<ul>
<li>loss 看起来很正常,但模型能力在悄悄退化</li>
<li>reward 一路往上走,输出却越来越奇怪</li>
<li>训练能跑、日志也漂亮,但结果完全不可复现</li>
</ul>
<p>所以这篇文章我只想做一件事:</p>
<p>不用任何黑盒框架,从零用 PyTorch 跑通一版不容易翻车的 PPO 微调。</p>
<p>它不追求最优,也不炫技。<br>
目标只有一个:工程上尽量安全。</p>
<p>
<img alt="21" loading="lazy" data-src="https://img2024.cnblogs.com/blog/3755179/202601/3755179-20260121121206930-523786811.png" >
</p>
<p>PPO 微调整体数据流图</p>
<h2 id="开始之前你需要准备什么以及别对第一版抱太大幻想">开始之前:你需要准备什么(以及别对第一版抱太大幻想)</h2>
<p>在真正写 PPO 代码之前,我建议你先确认三件事。</p>
<p>第一,你的 base model 已经做过 SFT<br>
PPO 并不是用来教模型“怎么回答问题”的,它更像是在微调模型的行为边界。</p>
<p>第二,你手里有一个能打分的 Reward Model<br>
它不需要特别聪明,只要稳定、一致、别太极端,就已经够用了。</p>
<p>第三,也是最重要的一点<br>
你得接受一个现实:</p>
<p>第一版 PPO 的目标不是效果炸裂,而是模型没被你训坏。</p>
<p>很多失败的 PPO 项目,问题并不出在算法上,而是工程师一开始就太着急,想“一步调到最优”。</p>
<h2 id="ppo-微调的整体工程结构先把全局图放在脑子里">PPO 微调的整体工程结构(先把全局图放在脑子里)</h2>
<p>在写任何代码之前,先在脑子里有一张全局图,会让你少踩很多坑。</p>
<p>在大模型场景下,一次完整的 PPO iteration,通常包括这些步骤:</p>
<ul>
<li>用当前 policy 生成 response</li>
<li>用 reward model 给 response 打分</li>
<li>计算当前 policy 和 reference policy 之间的 KL</li>
<li>把 reward 和 KL 合成一个总 reward</li>
<li>根据总 reward 估计 advantage</li>
<li>用 PPO loss 小步更新模型参数</li>
</ul>
<p>如果一定要打个比方,我更愿意这样理解 PPO:</p>
<p>它就像给策略梯度拴了一根安全绳。<br>
你可以往上爬,但不允许一步跨得太狠。</p>
<p>
<img alt="22" loading="lazy" data-src="https://img2024.cnblogs.com/blog/3755179/202601/3755179-20260121121155024-848183920.png" >
</p>
<p>Policy / Reward / Reference / Value 关系图</p>
<h3 id="第一步准备模型与-tokenizer为什么一定要保留-ref-model">第一步:准备模型与 tokenizer(为什么一定要保留 ref model)</h3>
  1. <code>from transformers import AutoModelForCausalLM, AutoTokenizer
  2. model = AutoModelForCausalLM.from_pretrained("your_sft_model")
  3. ref_model = AutoModelForCausalLM.from_pretrained("your_sft_model")
  4. tokenizer = AutoTokenizer.from_pretrained("your_sft_model")
  5. ref_model.eval()
  6. for p in ref_model.parameters():
  7.     p.requires_grad = False
  8. </code>
复制代码
<p>这里有一个我必须强调的工程原则:</p>
<p>reference model 是 PPO 的“底线”。</p>
<p>没有 ref model,你会遇到很多非常隐蔽的问题:</p>
<ul>
<li>reward model 再强,也迟早会被模型钻空子</li>
<li>模型输出会慢慢偏离正常语言分布</li>
<li>原本 SFT 学到的能力,会在不知不觉中被破坏</li>
</ul>
<p>说实话,我见过的不少 PPO 事故,追根溯源,几乎都能回到这一点:<br>
ref model 被弱化了,甚至被“顺手一起训了”。</p>
<h3 id="第二步生成-responseppo-不稳定的第一个源头">第二步:生成 response(PPO 不稳定的第一个源头)</h3>
<p>和 supervised learning 不一样,PPO 的训练样本并不是现成的数据集,而是模型自己生成的。</p>
  1. <code>def generate_response(model, prompt_ids, max_new_tokens=128):
  2.     with torch.no_grad():
  3.         output = model.generate(
  4.             input_ids=prompt_ids,
  5.             max_new_tokens=max_new_tokens,
  6.             do_sample=True,
  7.             top_p=0.9,
  8.             temperature=1.0
  9.         )
  10.     return output
  11. </code>
复制代码
<p>这里有几个非常现实、但经常被忽略的点:</p>
<ul>
<li>sampling 的随机性,本质上就是 PPO 的探索噪声</li>
<li>temperature 太低,模型几乎学不到新东西</li>
<li>temperature 太高,reward 的方差会直接炸掉</li>
</ul>
<p>如果是第一次跑 PPO,我的建议很保守:</p>
<ul>
<li>temperature 设成 1.0</li>
<li>top_p 用 0.9</li>
<li>先别碰 beam search</li>
</ul>
<h3 id="第三步reward--kl最容易看起来对其实用错的地方">第三步:Reward + KL(最容易“看起来对,其实用错”的地方)</h3>
<h4 id="reward-model-到底要多准">Reward Model 到底要多“准”?</h4>
<p>很多人一上来就会纠结:</p>
<p>Reward Model 一定要非常准吧?</p>
<p>但在 PPO 里,一个更现实的结论是:</p>
<p>reward 的排序性,远比绝对值重要。</p>
<p>工程上我更关心的是:</p>
<ul>
<li>reward 分布别太尖</li>
<li>不要大量 0 / 1 极值</li>
<li>reward 有没有无意中偏向长度或格式</li>
</ul>
<h4 id="kl-的作用说白了就是别把模型性格改没了">KL 的作用,说白了就是“别把模型性格改没了”</h4>
<p>KL penalty 在 PPO 中真的不是装饰品。</p>
<p>它更像是一根保险丝,用来防止模型在 reward 的驱动下“性格突变”。</p>
  1. <code>def compute_kl(logits, ref_logits):
  2.     log_probs = torch.log_softmax(logits, dim=-1)
  3.     ref_log_probs = torch.log_softmax(ref_logits, dim=-1)
  4.     kl = torch.sum(
  5.         torch.exp(log_probs) * (log_probs - ref_log_probs),
  6.         dim=-1
  7.     )
  8.     return kl.mean()
  9. </code>
复制代码
<p>几个很实在的工程经验:</p>
<ul>
<li>KL 通常只算 response 部分</li>
<li>KL 的数值尺度会随着词表大小变化</li>
<li>KL 一定要进监控,不然你根本不知道模型在不在“飘”</li>
</ul>
<p>
<img alt="23" loading="lazy" data-src="https://img2024.cnblogs.com/blog/3755179/202601/3755179-20260121121141836-528517409.png" >
</p>
<p>KL 曲线随训练步数变化</p>
<h3 id="第四步为什么不能直接用-rewardadvantage-的直觉解释">第四步:为什么不能直接用 reward?(Advantage 的直觉解释)</h3>
<p>在 PPO 里,reward 更像“结果”,而 advantage 更像“方向”。</p>
<p>最简单、但在工程上能用的 advantage 写法是:</p>
  1. <code>advantage = total_reward - total_reward.mean()
  2. </code>
复制代码
<p>它看起来确实有点粗糙,但解决了一个很关键的问题:</p>
<ul>
<li>不让所有样本一起无脑推模型</li>
<li>强调“相对更好”的行为</li>
</ul>
<p>这里有个重要认知:</p>
<p>第一版 PPO,真的不需要一个很完美的 value model。</p>
<h3 id="第五步ppo-loss别被-loss-曲线骗了">第五步:PPO loss(别被 loss 曲线骗了)</h3>
  1. <code>ratio = torch.exp(new_logprob - old_logprob)
  2. clipped_ratio = torch.clamp(ratio, 1-eps, 1+eps)
  3. loss = -torch.mean(
  4.     torch.min(ratio * advantage, clipped_ratio * advantage)
  5. )
  6. </code>
复制代码
<p>在工程里你一定要知道:</p>
<ul>
<li>loss 降得慢,不等于训练失败</li>
<li>loss 很平,也不代表模型没在学</li>
<li>PPO 的 loss 曲线,不能用 supervised learning 的思路去看</li>
</ul>
<p>真正有价值的信号,其实是:</p>
<ul>
<li>KL 有没有失控</li>
<li>reward 是不是稳步提升</li>
</ul>
<h3 id="第六步完整-ppo-更新循环贴近真实-gpu-训练">第六步:完整 PPO 更新循环(贴近真实 GPU 训练)</h3>
  1. <code>for batch in prompts:
  2.     response = generate_response(model, batch)
  3.    
  4.     reward = reward_model(response)
  5.     kl = compute_kl(
  6.         model_logits(response),
  7.         ref_model_logits(response)
  8.     )
  9.    
  10.     total_reward = reward - beta * kl
  11.     advantage = total_reward - total_reward.mean()
  12.    
  13.     for _ in range(ppo_epochs):
  14.         loss = ppo_loss(...)
  15.         loss.backward()
  16.         optimizer.step()
  17.         optimizer.zero_grad()
  18. </code>
复制代码
<p>一些很“工程”的建议:</p>
<ul>
<li>PPO epoch 别太多,4 已经很激进了</li>
<li>gradient clipping 基本是必选项</li>
<li>advantage 最好 batch 内单独算</li>
</ul>
<p>如果你不想一开始就手写所有 PPO 细节,LLaMA-Factory online 已经把 PPO + KL + Reward 的完整流程封装好,用它先跑一条“参考答案”,再回头对照自己的 PyTorch 实现,会省很多时间。</p>
<h2 id="训练中我最关心的几个监控信号">训练中我最关心的几个监控信号</h2>
<p>真正成熟的 PPO 训练,看的从来不只是一个 loss。</p>
<p>至少要包括这些:</p>
<ul>
<li>KL divergence</li>
<li>reward 的均值和方差</li>
<li>response 的平均长度</li>
<li>logprob 的分布变化</li>
<li>固定 prompt 下的输出变化</li>
<li>人工抽样的主观质量</li>
</ul>
<p>如果只能选一个重点盯:</p>
<p>盯 KL。</p>
<h2 id="一些常见翻车现场基本都是真实踩过的坑">一些常见翻车现场(基本都是真实踩过的坑)</h2>
<ul>
<li>reward 涨得很快,但模型开始胡说<br>
通常是 KL 太小,加上 reward 太单一</li>
<li>模型输出越来越短<br>
先看看 reward 有没有无意中惩罚长度</li>
<li>模型开始反复输出模板句<br>
很可能是 reward model 偏向了某种模式</li>
<li>模型几乎不动<br>
要么 KL 太大,要么学习率被你压得太死</li>
</ul>
<h2 id="进阶但仍然安全的改进方向">进阶但仍然安全的改进方向</h2>
<p>当你已经能稳定跑通 PPO 之后,可以再考虑这些事情:</p>
<ul>
<li>KL 的自适应调节</li>
<li>response length normalization</li>
<li>reward clipping</li>
<li>加 value head(但一定要非常谨慎)</li>
</ul>
<p>顺序真的很重要:</p>
<p>永远先稳,再谈强。</p>
<h2 id="写在最后我现在怎么看-ppo">写在最后:我现在怎么看 PPO</h2>
<p>如果只总结三点经验,那会是这样:</p>
<p>第一,PPO 的核心不是把 reward 拉到多高,而是控制变化幅度</p>
<p>第二,KL 是 PPO 的灵魂,而不是可选项</p>
<p>第三,一版 PPO 是否成功,最现实的标准只有一个:<br>
模型还像不像一个正常模型</p>
<p>在真实工程里,很多团队都会选择这样的路径:<br>
先用 LLaMA-Factory online 跑通一版稳定的 PPO,对齐整体流程,再把关键模块逐步迁移到自研的 PyTorch 实现中。这条路不一定最优,但通常最稳。</p><br>来源:程序园用户自行投稿发布,如果侵权,请联系站长删除<br>免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册