
训练过大模型的朋友应该都有过这种体会指令微调做完模型语法通顺了、知识也记住了可面对那些“没有标准答案”的开放问题时它给出来的东西总像是教科书在说话——正确但没有人味。你让它解释一个概念它能列三四个要点你让它推荐一本书它能给你报一堆出版信息。你挑不出大错就是觉得差口气。这个差口气本质上就是从“学会知识”到“理解偏好”之间的那道坎。我在这条坎上折腾了很久最后真正把体验拉上去的是引入了强化学习里的PPO算法。这篇文章想和你聊聊为什么大模型微调不只是把知识喂进去更要让模型学会“人的偏好”以及PPO到底是怎么一环扣一环把“偏好”变成可优化目标的包含我自己在实操中反复调整过的参数、踩过的坑和排查思路。1. 微调的新分水岭知识灌入和偏好对齐是两条路1.1 “学会知识”和“理解偏好”本质是两种不同任务很多人以为大模型微调就是把更多数据喂进去模型自然就越变越聪明。这句话在预训练阶段基本成立到了微调阶段就不成立了。预训练和SFT学的是“字面上最可能出现的下一个词”本质上做的是条件概率估计。模型学会的是“这段文本里哪些表达是常见的、合理的”但并不知道“用户更喜欢哪种表达”。我习惯用一个比喻SFT是让一个学生读了很多范文他写出来的东西语法没毛病结构四平八稳但他是照着范文集的模板在拼接他不知道什么样的表达才真正打动读者。偏好对齐要解决的就是后面这件事。在标注数据里我们通常只能记录“人对某个答案给了高分”这种绝对值很难学。因为每个人对“好”的标准都不一样甚至同一个人今天和明天的标准也不一样。可如果让模型比较两个候选答案判定“哪一个更好”这个信号就稳定得多。于是就有了奖励模型、PM、PPO这套链路目的不是继续灌知识而是让模型在多个同样“正确”的答案里学会挑出或者说生成那个更符合人类偏好的答案。这个区别是理解整篇内容的起点。不要觉得“我已经做了SFT微调就结束了”SFT打底偏好对齐才是决定产品体验上限的部分。1.2 为什么偏偏是PPO成了主流方案既然要让模型学会偏好最自然的思路就是把“生成答案”看作一次决策然后用强化学习去优化。但语言模型的动作空间特别大——词表轻轻松松几万个token而且一条回复动辄几百上千个token如果用朴素策略梯度每一步更新都容易产生巨大的方差训练动不动就崩。PPO进入视野是因为它做了两件非常重要的事一是用“重要性采样”让旧策略采集的数据得以复用二是通过裁剪目标函数限制每次更新的幅度不让策略在一步之间跑偏太远。这两点让大规模语言模型的强化学习变得可控制、可复现。所以哪怕后来出现了各种新的对齐思路主流的开源对齐库默认实现里最稳定、最常用的一支仍然是PPO。从我自己的实践感受来说PPO不是听起来那么“高大上”它本质上是一个工程上非常务实的折中方案。它有数学上严格的目标函数又留了很多旋钮让人在工程上调节比如KL系数、GAE的lambda、裁剪范围。也正是这些旋钮让“训练能跑起来”和“训练能跑出效果”变成两件完全不同的事。2. 读懂PPO前先读懂RLHF的完整链路2.1 第一阶段SFT让模型先学会规范表达偏好对齐不是从零开始训一个模型它的起点通常是一个已经做过指令微调的模型。这一步我们叫SFT全称是监督式微调。SFT阶段用的数据是人工整理的“指令-回答”对模型的任务很简单看到指令尽量模仿标准答案的输出。它学的是一种“说话的基本礼仪”比如要能理解问题、回答要连贯、不能跑题、格式要规范。这里有一个容易被低估的点SFT数据的质量决定了后面RLHF阶段的天花板。如果SFT阶段模型就见过很多胡说八道、结构混乱的回答后面PPO再怎么调也难把它拽回一个体面的表达上来。因为PPO里的奖励模型本身也是基于一个已经比较“会说话”的模型去训练的如果基座就乱七八糟奖励模型也会学得很混乱。所以我的建议是进入RLHF之前至少先花精力把SFT数据清洗干净。宁可少而精不要多而杂。SFT阶段“学会知识”的意义在于让模型有足够的能力储备后面才有空间去谈“偏好”。2.2 第二阶段奖励模型把人类的“好恶”变成可优化分数有了SFT模型之后下一步是训练奖励模型。它的职责是给定一个问题prompt和一个回答response输出一个标量分数代表“这个回答有多符合人类偏好”。这里有个训练细节要留意奖励模型通常不是直接让人给绝对分数而是让人做“对比”——同一个问题下模型生成两个候选回答标注者判断谁更好。数据里就会记录成chosen更优回答和rejected较差回答。训练时奖励模型给chosen的打分要比给rejected的打分高损失函数用的是排序损失直观理解起来就是让“赢家”和“输家”的分数差距拉开。实际训练时奖励模型往往是一个SFT模型但在最后一层加一个线性层输出scalar。这样做的好处是能复用模型已有的语言理解能力只需要额外学习“如何评价”即可。常见做法是初始化自SFT模型或同规模预训练模型。我自己训练奖励模型时最深刻的体会是它特别容易过拟合。因为训练集里同一个prompt往往只有两到三个对比回答模型很快就能记住“哪个回答该得高分”一到新的prompt上就瞎给分。所以奖励模型的泛化能力直接决定了PPO阶段会不会南辕北辙。2.3 第三阶段PPO让模型在偏好空间里寻找最优策略奖励模型训练好以后进入最核心的部分——用PPO优化策略模型。策略模型就是我们最终要上线服务的模型它由SFT模型初始化。训练时我们要同时保留这么几个模型策略模型actor、参考模型reference、奖励模型reward和价值模型critic。整个流程可以粗略理解为给策略模型一批prompt让它生成回答然后用奖励模型给回答整体打个分这个分数再分摊到每个token上变成“好”或“坏”的强化信号。PPO通过这个信号去更新策略让模型学会生成更高分的回答。你可能会问为什么不让奖励模型一训练完直接拿最好的回答做SFT算了这想法很直觉但问题在于我们不可能穷举所有可能的回答。奖励模型定义了一个“偏好空间”我们需要在这个空间里搜索找到那些分数高、又符合人类表达习惯的区域。PPO做的就是这种搜索而且它搜得很谨慎每一步改动都不太大避免从一个合理区域跳到另一个不合理区域。3. 从数学直觉到代码骨架PPO在语言模型里的落地细节3.1 重要性采样为什么旧策略生成的数据还能反复用聊PPO之前必须先把重要性采样这个概念讲明白。传统的策略梯度方法每次更新策略后都要重新互动采样因为新策略下的数据分布已经变了。可大模型每做一次生成都要跑一次前向成本极其昂贵。如果每次PPO更新都要重新采一批数据训练会被慢到怀疑人生。PPO的巧妙之处在于它允许你用旧策略采到的数据去估计新策略的梯度只要乘上一个重要性比率做修正。这个比率通常写成r(θ) πθ(a|s) / π_old(a|s)意思是“新策略生成这个token的概率”除以“旧策略生成这个token的概率”。如果两条策略完全一样比率就是1如果新策略在某条数据上概率变高了比率就大于1变低了就小于1。但直接拿这个比率乘优势函数训练仍然可能炸。所以PPO对这个比率做了裁剪把r限制在[1-ε, 1ε]之间常见的ε取0.2。目标是L min(r * A, clip(r, 1-ε, 1ε) * A)当优势A是正的这个token值得鼓励我们就希望r不要太大免得一步跨太远当A是负的我们又希望r别太小免得新策略一下子把某个旧动作的概率压到零。这个clip机制就像一个减速带是PPO训练稳定性的核心来源。3.2 GAE与优势估计给每个token公平打分语言模型一次生成几百个token但奖励模型往往只给整条回复一个整体分数。这里就涉及一个问题哪些token该为这个分数负责答案是很难精确知道。这个时候我们就要用GAE全称是广义优势估计来做一种“功劳分摊”。GAE的核心参数有折扣因子γ和GAE系数λ。γ的含义是“当前时刻对未来奖励的看重程度”在PPO语言模型里经常取1.0或0.99意味着我们不太想对后续的奖励打折λ控制的是优势估计的偏差和方差之间的取舍取0.95是比较常见的选择。实际训练时价值模型critic会给每个token预测一个“基线价值”表示“从这个位置出发期望能获得多少奖励”。真实奖励和基线价值之间的差就是每个token的优势。如果一个token之后模型说出了特别讨喜的话它会多分到一份功劳如果后续回答明显变差前面的token也要担责。这套机制让模型知道开头的“嗯嗯”、中段的转折、结尾的总结都对最终评价有影响。3.3 KL散度约束与参考模型不让模型跑飞PPO更新策略时有一个天然风险模型为了讨好奖励模型可能会走捷径生成一些语法上不对、但得分极高的奇怪文本。为了防止这种情况我们通常会保留一个冻结的参考模型也就是SFT阶段的模型。每生成一个token策略模型输出的概率分布会和参考模型的概率分布计算KL散度把这个KL值作为惩罚项从奖励里扣掉。如果你之前没接触过KL可以把它理解成“新模型和旧模型说话的差距有多大”。如果策略模型突然说出一种特别怪但得分特别高的表达KL会迅速变大奖励被扣掉模型就意识到这条路走不通。如果KL系数β设置太大模型会唯唯诺诺不敢偏离参考模型半步学习效果接近于零设置太小又会放飞自我。我自己习惯用自适应KL调节设定一个目标KL区间比如[0.01, 0.03]当实际KL超过上限就把β调大低于下限就把β调小。这样可以在“学新东西”和“保持稳定”之间找到一个动态平衡。千万不要把β设成固定值就不管了训练过程中分布变化很大固定值几乎必然失效。3.4 一个简化的PPO训练循环伪代码说了这么多理论最后落到代码上。下面是一个极简的伪代码不依赖任何具体框架但足以看出一次PPO迭代里到底发生了什么# 伪代码示意一次PPO训练迭代 # 1. 采样阶段用当前策略模型生成回答 for batch in dataloader: queries batch[prompt] responses, logprobs actor.generate(queries) # 新策略的token对数概率 ref_logprobs ref_model.compute_logprobs(queries, responses) # 参考模型的概率 # 2. 计算奖励 rewards reward_model.score(queries, responses) # 整条回答一个分数 kl compute_kl(logprobs, ref_logprobs) # 逐token KL散度 rewards rewards - beta * kl.sum(dim-1) # 减去KL惩罚 # 3. 估计优势 values critic_model.compute_values(queries, responses) # token级基线 advantages gae(rewards, values, gamma1.0, lam0.95) # 优势估计 returns advantages values # 用于更新critic # 4. 多次PPO更新复用采样数据 for _ in range(ppo_epochs): new_logprobs actor.compute_logprobs(queries, responses.detach()) ratio torch.exp(new_logprobs - logprobs.detach()) pg_loss -torch.min( ratio * advantages.detach(), torch.clamp(ratio, 1 - clip_eps, 1 clip_eps) * advantages.detach() ).mean() value_loss mse_loss(critic_model.compute_values(...), returns.detach()) update_actor(pg_loss) update_critic(value_loss)这段代码如果不是自己跑过一次很容易觉得无所谓真正跑起来就会知道每个环节都有可能成为瓶颈。后面我就把实操中容易出坑的地方拆开讲。4. 实操指南从零跑通一个大模型PPO对齐实验4.1 组件的显存规划PPO整个链路里至少同时存在四份模型这是和SFT最大的不同。SFT只要一份模型就能训练PPO却需要组件作用说明Actor策略模型我们最终要用的模型由SFT模型初始化会被更新Reference参考模型计算KL惩罚一般直接复用SFT模型冻结Reward奖励模型给回答打分单独训练的RM冻结Critic价值模型估计token级基线通常和Actor同样结构但输出为标量显存估算上一份7B模型在BF16下大约占14GB四份就是56GB还要算上优化器状态、梯度、激活值单张A100级别的卡才勉强够用。很多人第一次跑PPO失败不是算法问题而是显存直接崩了。解决思路有几种一是用LoRA只训练Actor部分的低秩适配器其他模型全部冻结并开启梯度检查点二是把actor和critic合并成同一个主干模型只在上面挂两个不同的输出头省一份主干显存三是用小批量加梯度累积牺牲速度换内存。我自己比较推荐一开始先用LoRA跑通小规模的实验比如1B模型或7B模型把训练脚本、数据、超参都验证一遍再上大规模。不要一上来就直接拿几十B的模型跑PPO调试成本太高。4.2 准备数据与训练奖励模型PPO阶段需要的prompt数据可以复用SFT阶段的一部分指令但要注意多样性。不要只准备单一风格的提问比如全是“请解释一下XX概念”因为模型会在这些固定prompt上快速过拟合奖励模型也会跟着学歪。奖励模型的数据格式一般是这样的{ prompt: 请用一句话向我推荐一本适合下班后读的书, chosen: 如果你想在晚上放松又不想完全失去思考的乐趣这本书的短篇故事节奏很合适随时翻开都行。, rejected: 《某书名》作者是某某出版社是某某出版时间是某年。这本书受到很多读者关注。 }你看chosen和rejected在语法上没有哪一句话是彻底错的但chosen明显更有人味、更贴合“适合下班后读”这个场景这就是人类偏好的微妙之处。训练奖励模型时需要注意这个东西极其容易过拟合。我一般会把验证集单独留出来每次训练一个epoch就跑到验证集上看排序准确率准确率到80%左右就停再训就只会在训练集上刷分。另外也可以考虑给同一个prompt多配几组对比样本这比单纯增加prompt数量更能提升泛化能力。4.3 核心超参配置与训练循环因为开源库版本差异较大这里我只给出我习惯用的参数区间大家可以作为起始参考再按自己的模型规模调整超参我常用的值说明Actor学习率1e-6 ~ 3e-6比SFT低一个数量级Critic学习率5e-6 ~ 1e-5价值模型可以学得快一点Clip ε0.2常用值基本不用动GAE λ0.95平滑优势估计γ1.0不折扣最终奖励PPO更新次数3~4每次采样后复用几次KL目标0.01 ~ 0.03自适应KL的核心批大小32~128按显存决定训练循环的顺序不要搞错。先rollout用当前策略生成回答再统一计算奖励和KL然后计算优势和value target最后做多轮PPO更新。我见过很多人把reward计算放进ppo更新循环里导致每个mini-batch都会重新调用奖励模型不仅慢而且梯度计算还容易出问题。更新完策略模型之后我习惯立即做一次小规模的抽样生成人工看一眼回答质量而不是只看训练日志。训练日志里loss下降、reward上升都不如真实对话样例来得直接。这个习惯帮我避开了好几次“日志很漂亮、生成很垃圾”的危机。4.4 训练进程监控跑PPO不能像SFT那样只看loss曲线。我的做法是同时盯三个东西策略模型得分、平均KL散度、每个batch的生成样例。策略模型得分指的是奖励模型给新生成回答的平均分理想情况是缓慢上升上升过快往往意味着在走捷径。平均KL散度保持在一个可控区间如果突然飙升说明模型行为开始异常。生成样例则是最直观的“报警器”生成里开始出现重复、乱码、敏感内容时KLD通常都还没完全反映出来但人眼一眼就能看出不对。我建议把这三个量打包成一个固定的日志结构每个一定步数打印一次不要只留Loss。等训练出问题时再回头查很多关键线索早就被淹没在历史日志里了。5. 常见问题排查与避坑实录5.1 reward hacking奖励越刷越高效果越来越差这是PPO训练里最经典的问题我也是吃了大亏才彻底明白。现象是训练日志里奖励分数一路暴涨看着非常振奋但把模型拿出来和人对聊发现它开始用奇怪的方式触发奖励模型。比如回答里塞一堆长难句或者反复使用某些高概率关键词甚至生成结构诡异但得分很高的文本。本质原因是奖励模型不是完美的它只是对人类偏好的近似PPO又特别擅长找近似函数的漏洞。解决思路有几种调大KL惩罚的β让模型不能离SFT初始模型太远。降低PPO更新次数从4次降到2次缩小搜索半径。给奖励模型的score加一些噪声或随机扰动破坏那种“一招鲜”的触发模式。检查奖励模型的测试集准确率如果训练集准确率很高测试集很低先回去修奖励模型。5.2 KL突然失控和生成崩溃训练到一半KL散度突然从0.02跳到几十生成内容变成无意义字符这多半是策略模型某一步更新过大导致的。最常见的原因是某个batch里奖励特别极端或者critic对某个token的价值估计严重偏离导致优势值异常大。我的排查顺序是先看是不是单条数据的问题把那个batch里reward最高的几条样本打印出来通常能找到“毒样本”再看critic loss有没有发散如果value function震荡优势估计就不可能靠谱。如果确认是critic问题可以试试降低critic学习率或者对奖励做标准化比如把reward减均值除标准差。如果确认是策略问题立刻降低PPO更新次数和actor学习率别再硬撑。还有一个土办法把KL系数临时调大十倍先让模型“冷静下来”。5.3 训练速度慢到怀疑人生PPO比SFT慢是正常的因为它要做四次前向actor生成、ref计算、reward计算、critic计算再叠加多轮更新。但如果你发现训练慢到不可接受可能不是算法问题而是工程问题。一个常见的坑是actor在rollout阶段开启了梯度计算白白多算了一轮反向。rollout生成不需要梯度应该完全放在torch.no_grad()下。另一个坑是参考模型和策略模型没有共用分词器缓存导致重复padding。再一个坑是use_cache被错误关闭生成时缓存没有打开那一字一句地解码会非常难受。显存不够的时候优先考虑把critic和actor合并参数或者用LoRA。LoRA还有个额外的好处是能显著降低模型状态更新时的通信成本训练速度会快不少。5.4 效果评估模型到底变好了没有训练结束不代表模型就真的变好了。我需要做几组对比SFT模型 vs PPO后的模型同样的prompt跑一遍人工盲评。注意不要只看奖励模型打分因为奖励模型是PPO自己的优化目标确实可能被绕过。更稳妥的做法是准备一个“外部偏好集”这个数据集在训练奖励模型时没有见过。用这个集上的对决赛果来判断最好能和真实的用户反馈相结合。我自己的评估流程是准备20到30条覆盖场景的高质量prompt请几个朋友按“更有帮助、更自然、更喜欢”三个维度打分。这里有一些主观性没关系我们关注的是相对提升不需要绝对精确。如果PPO版本在这个小样本盲测里没有明显优势我会选择保留SFT版本RLHF不是非要不可调不好不如不调。最后再分享一个我个人的体会PPO是那种“原理不复杂但手感很重要”的训练范式。第一次跑通你可能觉得也不过如此等你把reward hacking、KL失控、critic震荡这些问题一个个踩完才会真正明白那些超参旋钮到底是干什么用的。不要指望一次跑出完美结果做对齐本身就是一场和模型、奖励模型、数据质量三方之间的拉锯战。如果你正在跑我的建议是从小模型小批次开始把日志监控和人工抽评的习惯养成再考虑大规模训练。这个顺序省下来的时间和显卡成本远比你想象中多。