2026/8/14 7:12:04

大模型RLHF核心算法PPO详解:从原理到工程实践

大模型RLHF核心算法PPO详解:从原理到工程实践 1. 项目概述从SFT到RLHF为什么需要PPO如果你已经跟着大模型微调的路线走了一段时间大概率已经玩过监督微调SFT也听说过人类反馈强化学习RLHF是让模型“对齐”人类价值观、生成更优质回答的关键一步。但当你真正开始研究RLHF的实现时会发现一个绕不开的名字近端策略优化Proximal Policy Optimization, PPO。它几乎是当前所有主流大模型如GPT系列、Claude、Llama 2-Chat等进行RLHF阶段时那个在幕后默默工作的核心算法引擎。简单来说PPO解决的是一个“精细操控”的难题。在SFT阶段我们给模型看了很多“标准答案”它学会了模仿。但在RLHF阶段我们不再提供标准答案而是提供一个“奖励模型”Reward Model, RM让它给模型生成的每一个回答打分。模型的目标变成了调整自己的参数使得生成的回答能获得尽可能高的奖励分。这听起来很像强化学习RL里智能体在环境中通过试错学习以获得奖励的场景。没错大模型的RLHF本质上就是一个超大规模的强化学习问题其中模型自身就是需要被优化的“策略”。那么为什么非得是PPO而不是其他更经典的RL算法比如策略梯度或者TRPO呢核心原因在于稳定性、效率和工程友好性的绝佳平衡。大模型参数动辄百亿、千亿一次不好的更新可能导致模型“崩溃”比如开始输出乱码或毫无意义的重复文本训练成本极高。PPO通过引入一个“近端”惩罚项严格限制了每次参数更新的幅度确保新策略不会偏离旧策略太远从而像给训练过程装上了“护栏”和“减震器”让这个庞然大物能够稳定、平滑地朝着获得更高奖励的方向进化。理解PPO你就握住了打开大模型对齐与能力精调黑盒的一把关键钥匙。2. PPO核心思想拆解给“巨兽”套上缰绳要理解PPO为何有效我们需要先看看它要解决的前置问题。在传统的策略梯度方法中我们直接计算期望奖励关于策略参数的梯度然后沿着梯度方向更新。但对于大模型这种高维、复杂的策略这种“蒙眼狂奔”式的更新非常不稳定学习率设大了一步就可能让模型性能暴跌且难以恢复这种现象称为“策略崩溃”。2.1 信任区域与重要性采样PPO的思想根源来自于更早的信任区域策略优化TRPO。TRPO的核心洞见是我们不希望新策略π_θ和旧策略π_θ_old差别太大。它通过一个复杂的数学约束KL散度约束来明确地定义一个“信任区域”保证更新在这个安全的区域内进行。TRPO理论很漂亮但实现起来需要计算二阶导数海森矩阵并求解约束优化问题对于大模型来说计算成本太高工程实现也颇为繁琐。PPO的聪明之处在于它用了一个计算上更轻量、但效果相近的“软约束”来达成类似“信任区域”的效果。它不再硬性要求KL散度必须小于某个阈值而是修改了优化的目标函数。PPO主要使用了两种变体PPO-Penalty和PPO-Clip。目前大模型训练中几乎清一色使用的是PPO-Clip因为它更简单不需要动态调整惩罚系数。PPO-Clip的目标函数看起来是这样的L^{CLIP}(θ) E_t [ min( r_t(θ) * A_t, clip(r_t(θ), 1-ε, 1ε) * A_t ) ]这个公式初看有点唬人我们来拆解一下r_t(θ)重要性采样比率这是关键。r_t(θ) π_θ(a_t|s_t) / π_θ_old(a_t|s_t)。它衡量了新策略和旧策略对某个具体行动在NLG中就是生成某个词元的概率比值。如果比值是1说明新旧策略看法一致大于1说明新策略更倾向于这个行动小于1则相反。A_t优势函数这个值告诉我们在某个状态下采取某个行动比平均情况好多少或差多少。它通常由奖励R_t减去一个基线比如价值函数V(s)得到。正的优势意味着这个行动是好的应该被鼓励负的则意味着应该被抑制。clip操作这是PPO的“灵魂”。它把重要性采样比率r_t(θ)限制在区间[1-ε, 1ε]内。ε是一个超参数通常设为0.1或0.2。这个min和clip的组合实现了什么效果当优势A_t为正时行动是好的我们希望提高这个行动的概率即让r_t(θ)增大。但PPO说可以增大但不能超过1ε。如果r_t(θ)本来就会增长到超过1ε那么clip函数会把它拉回到1ε此时clip(...)*A_t会小于r_t(θ)*A_tmin函数就会选择这个更小的值作为目标。这相当于给正向更新的幅度设置了一个上限防止模型因为某个行动看起来特别好就过度优化从而偏离旧策略太远。当优势A_t为负时行动是差的我们希望降低这个行动的概率即让r_t(θ)减小。同样PPO限制它不能低于1-ε。如果r_t(θ)会减小到低于1-εclip函数会把它抬升到1-εmin函数会选择这个更大的负得少一点的值。这相当于给负向更新的幅度设置了一个下限防止模型因为某个行动看起来差就彻底否定它造成策略剧烈震荡。实操心得你可以把ε想象成更新步长的“保险丝”。ε越小策略更新越保守训练越稳定但可能学习速度慢ε越大更新越激进学习可能更快但崩溃风险增加。在大模型训练中通常从0.1或0.2开始这是一个经过大量实践检验的、比较安全的起点。2.2 PPO在大模型RLHF中的工作流在大模型的语境下PPO的各个组件是这样映射的状态s_t当前已生成的文本序列即上下文。行动a_t在词汇表中选择下一个词元token。策略π被训练的大语言模型本身。它接收状态上下文输出行动的概率分布下一个词元的概率。奖励r_t由奖励模型RM给出的标量分数。注意在文本生成中奖励通常是稀疏的即只在生成完整个序列或达到某个停止标记时才给出一个总奖励而不是每个生成步骤都有奖励。这就需要通过广义优势估计GAE等技术来将稀疏的最终奖励合理地分配折现到每个生成步骤上从而计算出每一步的优势A_t。轨迹从初始提示prompt开始到生成结束如遇到eos标记所产生的一个完整文本序列。所以一个简化的PPO训练循环是这样的用当前策略模型Actor对一批提示prompts进行采样生成完整的回复。用奖励模型RM对这些回复进行打分得到奖励值。用旧策略模型一个副本参数固定和价值函数模型Critic可选但常用来计算生成轨迹中每个时间步的优势A_t。根据PPO-Clip目标函数计算关于策略模型Actor参数的梯度并更新Actor。通常也会用一个损失函数来更新价值函数模型Critic使其能更准确地预测状态价值从而更好地估计优势。重复这个过程。注意事项在实际的大模型RLHF实现如DeepSpeed-Chat、TRL等库中为了节省显存步骤1中生成的文本及其中间隐藏状态等会被缓存起来。在后续的PPO更新步骤中会多次例如2-4次利用这批缓存的数据进行小批量minibatch的梯度更新这被称为多个PPO epochs。这能更充分地利用一次前向传播生成的数据提升样本效率。3. 实操要点构建你的第一个大模型PPO训练循环理解了原理我们来看看如何动手搭建一个最简化的PPO训练流程。这里我们不会涉及分布式训练等复杂工程而是聚焦于核心逻辑。假设我们已经有了一个经过SFT的模型Actor、一个训练好的奖励模型RM和一个用于估算状态价值Value的模型Critic通常可以和Actor共享主干网络但有一个独立的输出头。3.1 数据准备与模型初始化首先我们需要一批提示词prompts。这些提示词应该具有多样性覆盖你想要模型学习应对的各种场景。import torch from transformers import AutoTokenizer, AutoModelForCausalLM from torch.utils.data import DataLoader # 1. 加载模型和分词器 actor_model_name your_sft_model_path # 经过SFT的模型 critic_model_name your_sft_model_path # Critic可以和Actor初始化相同 reward_model_name your_rm_model_path # 训练好的奖励模型 tokenizer AutoTokenizer.from_pretrained(actor_model_name) actor_model AutoModelForCausalLM.from_pretrained(actor_model_name) critic_model AutoModelForCausalLM.from_pretrained(critic_model_name) # 注意需要额外配置一个价值头value head reward_model AutoModelForCausalLM.from_pretrained(reward_model_name) # RM通常也是一个LM最后一个token的隐藏状态接一个标量输出层 # 添加价值头一个线性层将隐藏状态映射为标量价值 class ValueHead(torch.nn.Module): def __init__(self, hidden_size): super().__init__() self.linear torch.nn.Linear(hidden_size, 1) def forward(self, hidden_states): return self.linear(hidden_states).squeeze(-1) # 假设我们给critic_model加上value_head critic_model.value_head ValueHead(critic_model.config.hidden_size) # 设置padding token if tokenizer.pad_token is None: tokenizer.pad_token tokenizer.eos_token # 2. 准备提示数据 prompts [ 请解释一下机器学习中的过拟合现象。, 用Python写一个快速排序函数。, 简述气候变化的主要原因。, # ... 更多提示 ] prompt_dataset ... # 将prompts封装成Dataset dataloader DataLoader(prompt_dataset, batch_size4, shuffleTrue)3.2 核心训练循环步骤详解接下来是PPO训练的核心部分。我们将它分解为几个关键函数。步骤A生成回复Rolloutdef generate_rollout(actor_model, prompts, tokenizer, max_length128): 使用当前的策略模型Actor生成回复。 inputs tokenizer(prompts, return_tensorspt, paddingTrue, truncationTrue, max_length512).to(actor_model.device) with torch.no_grad(): # 使用采样sampling而非贪婪解码以增加探索性 outputs actor_model.generate( **inputs, max_new_tokensmax_length, do_sampleTrue, top_p0.9, # 使用核采样nucleus sampling temperature0.7, pad_token_idtokenizer.pad_token_id, eos_token_idtokenizer.eos_token_id, return_dict_in_generateTrue, output_scoresTrue, # 我们需要每个时间步的logits来计算概率 ) sequences outputs.sequences # 提取生成的文本部分去掉输入提示 input_lengths inputs[input_ids].shape[1] response_ids sequences[:, input_lengths:] # 收集每个生成步骤的logits用于后续计算概率 logits torch.stack(outputs.scores, dim1) # [batch_size, seq_len, vocab_size] return response_ids, logits, sequences, input_lengths注意这里output_scoresTrue会返回每个生成步骤的logits这对计算动作概率至关重要但也会显著增加内存消耗。在生产环境中可能需要更高效的方式。步骤B计算奖励和优势这是RLHF-PPO中最复杂也最关键的一步。def compute_rewards_and_advantages(response_ids, prompts, reward_model, critic_model, tokenizer, gamma0.99, lam0.95): 计算每个生成token的奖励和优势。 为了简化我们假设奖励模型只在序列末尾给出一个总奖励。 实际中可能使用序列级奖励或更复杂的每步奖励。 batch_size, seq_len response_ids.shape device response_ids.device # 1. 计算奖励稀疏奖励场景 # 将提示和回复拼接送给奖励模型打分 full_texts [p tokenizer.decode(r, skip_special_tokensTrue) for p, r in zip(prompts, response_ids)] with torch.no_grad(): rm_inputs tokenizer(full_texts, return_tensorspt, paddingTrue, truncationTrue, max_length1024).to(device) rm_outputs reward_model(**rm_inputs) # 假设奖励模型的输出是最后一个token的隐藏状态经过一个线性层得到的标量 # 这里需要根据你的RM实际结构调整 rewards rm_outputs.logits[:, -1].squeeze(-1) # [batch_size] # 2. 计算价值Critic的预测 # 同样将提示和回复拼接让Critic为每个位置state预测价值 with torch.no_grad(): # 我们需要为整个序列包括提示和回复的每个位置计算价值 # 一种常见做法是将完整的sequences输入Critic获取每个位置对应的价值 # 这里为了简化我们假设已经有了完整的sequences和对应的input_lengths # 我们需要用Critic对sequences做一次前向传播 critic_inputs tokenizer([tokenizer.decode(s, skip_special_tokensFalse) for s in sequences], return_tensorspt, paddingTrue, truncationTrue, max_length1024).to(device) critic_outputs critic_model(**critic_inputs, output_hidden_statesTrue) last_hidden_states critic_outputs.hidden_states[-1] # [batch_size, total_seq_len, hidden_size] values critic_model.value_head(last_hidden_states) # [batch_size, total_seq_len] # 只取回复部分对应的价值 response_values values[:, input_lengths: input_lengths seq_len] # [batch_size, seq_len] # 3. 计算优势使用GAE - Generalized Advantage Estimation advantages torch.zeros_like(response_values).to(device) last_advantage 0 # 从后向前计算 for t in reversed(range(seq_len)): if t seq_len - 1: # 最后一个时间步下一个状态的价值为0终止状态 next_value 0 delta rewards - response_values[:, t] else: next_value response_values[:, t1] # 稀疏奖励下只有最后一步有非零奖励中间步骤的即时奖励r_t为0 delta 0 gamma * next_value - response_values[:, t] advantages[:, t] delta gamma * lam * last_advantage last_advantage advantages[:, t] # 4. 计算回报Returns returns advantages response_values # 将优势归一化一个稳定训练的小技巧 advantages (advantages - advantages.mean()) / (advantages.std() 1e-8) return rewards, response_values, advantages, returns核心解析GAE广义优势估计是这里的关键。它平滑地结合了不同步数的优势估计从1步到n步在偏差和方差之间取得了很好的平衡。参数lamλ控制了权衡λ1时高方差但低偏差λ0时退化为单步TD误差。通常设为0.95左右。步骤C执行PPO更新现在我们有了一批数据状态、动作、旧概率、优势、价值可以开始更新Actor和Critic了。def ppo_update(actor_model, critic_model, optimizer_actor, optimizer_critic, response_ids, old_logprobs, advantages, returns, values, clip_epsilon0.2, value_coef0.5, entropy_coef0.01, ppo_epochs4, batch_size2): 执行多轮PPO更新。 # 将数据转换为一个大的数据集 total_samples response_ids.shape[0] * response_ids.shape[1] # 我们需要将三维数据展平为二维 [batch_size * seq_len, ...] flat_response_ids response_ids.reshape(-1) flat_old_logprobs old_logprobs.reshape(-1) flat_advantages advantages.reshape(-1) flat_returns returns.reshape(-1) flat_values values.reshape(-1) # 创建索引用于随机小批量采样 indices torch.randperm(total_samples) for epoch in range(ppo_epochs): for start in range(0, total_samples, batch_size): end start batch_size batch_indices indices[start:end] batch_response_ids flat_response_ids[batch_indices] batch_old_logprobs flat_old_logprobs[batch_indices] batch_advantages flat_advantages[batch_indices] batch_returns flat_returns[batch_indices] batch_values flat_values[batch_indices] # --- 更新 Critic (价值函数) --- # Critic的目标是让预测的价值更接近实际回报 current_values critic_model.value_head(...) # 需要根据batch_indices重新计算当前价值这里简化了实际的数据流 # 实际中我们需要用当前的critic_model对对应的状态再做一次前向传播 # 为了示例我们假设 current_values 已经计算好 critic_loss torch.nn.functional.mse_loss(current_values, batch_returns) optimizer_critic.zero_grad() critic_loss.backward() torch.nn.utils.clip_grad_norm_(critic_model.parameters(), max_norm0.5) optimizer_critic.step() # --- 更新 Actor (策略) --- # 需要计算当前策略下采取这些动作的对数概率 # 这通常需要将batch_response_ids对应的token及其上下文再次输入actor_model # 获取当前策略的logits然后计算对数概率 # logits actor_model(...).logits # [batch_size, seq_len, vocab_size] # 假设我们取出了对应位置的logits并计算了当前对数概率 batch_curr_logprobs # 实际计算较为复杂需要索引对齐 ratio torch.exp(batch_curr_logprobs - batch_old_logprobs) # PPO-Clip 目标函数 surr1 ratio * batch_advantages surr2 torch.clamp(ratio, 1.0 - clip_epsilon, 1.0 clip_epsilon) * batch_advantages actor_loss -torch.min(surr1, surr2).mean() # 添加熵奖励鼓励探索防止策略过早收敛到单一模式 # 需要计算当前策略的熵entropy -sum(p * log(p)) # 假设我们计算出了当前批次的平均熵 batch_entropy entropy_bonus -entropy_coef * batch_entropy total_actor_loss actor_loss entropy_bonus optimizer_actor.zero_grad() total_actor_loss.backward() torch.nn.utils.clip_grad_norm_(actor_model.parameters(), max_norm0.5) optimizer_actor.step()重要提示上面的更新代码是高度简化的概念演示。在实际中batch_curr_logprobs和batch_entropy的计算需要非常小心地处理数据对齐和模型前向传播这通常是RLHF实现中最容易出错的地方。成熟的库如TRL会妥善处理这些细节。3.3 整合训练循环最后我们将上述步骤整合到一个训练循环中。# 超参数配置 ppo_config { clip_epsilon: 0.2, gamma: 0.99, lam: 0.95, ppo_epochs: 4, batch_size: 32, # PPO更新时的小批量大小 lr_actor: 1e-6, # Actor学习率通常非常小 lr_critic: 1e-6, max_response_length: 128, num_rollout_steps: 128, # 每次收集多少步的数据 } # 初始化优化器 optimizer_actor torch.optim.Adam(actor_model.parameters(), lrppo_config[lr_actor]) optimizer_critic torch.optim.Adam(critic_model.parameters(), lrppo_config[lr_critic]) # 训练循环 for epoch in range(total_epochs): for batch_prompts in dataloader: # 1. 生成阶段 (Rollout Phase) response_ids, logits, sequences, input_lengths generate_rollout( actor_model, batch_prompts, tokenizer, ppo_config[max_response_length] ) # 2. 评估阶段 (Evaluation Phase) # 计算旧策略下每个动作的对数概率用于重要性采样 # logits是actor_model在生成时产生的 probs torch.nn.functional.softmax(logits, dim-1) # 获取实际生成的token的概率 # response_ids shape: [batch_size, seq_len] # 我们需要为每个batch每个位置取出对应token的prob # 这是一个高级索引操作 action_probs probs.gather(dim-1, indexresponse_ids.unsqueeze(-1)).squeeze(-1) old_logprobs torch.log(action_probs 1e-10) # 防止log(0) # 计算奖励、价值、优势、回报 rewards, values, advantages, returns compute_rewards_and_advantages( response_ids, batch_prompts, reward_model, critic_model, tokenizer, gammappo_config[gamma], lamppo_config[lam] ) # 3. 优化阶段 (Optimization Phase) ppo_update( actor_model, critic_model, optimizer_actor, optimizer_critic, response_ids, old_logprobs, advantages, returns, values, clip_epsilonppo_config[clip_epsilon], ppo_epochsppo_config[ppo_epochs], batch_sizeppo_config[batch_size] ) # 4. 可选定期保存模型和日志 if step % log_interval 0: print(fEpoch {epoch}, Step {step}, Avg Reward: {rewards.mean().item():.4f})这个循环勾勒出了PPO训练的核心骨架。在实际应用中你需要处理大量的工程细节例如高效的注意力掩码、梯度检查点gradient checkpointing以节省显存、混合精度训练、分布式数据并行等。4. 常见陷阱与实战调试技巧即使理解了原理和流程第一次跑PPO训练也极易踩坑。下面是我在实践中总结的几个关键问题和应对策略。4.1 奖励飙升或崩溃KL散度失控现象训练初期奖励分数快速上升到一个不合理的极高值或者模型输出迅速退化成一堆无意义的重复字符奖励崩溃。根因这是PPO训练中最经典的问题。模型发现了奖励模型的“漏洞”或偏好开始过度优化生成一些对人类无意义但对RM来说能骗得高分的文本例如以“这是一个非常好的回答因为...”开头的大量自夸式文本。同时策略相对于初始SFT模型的KL散度急剧增大意味着模型已经“跑偏”太远。解决方案KL惩罚KL Penalty在PPO的目标函数中直接加入一个KL散度惩罚项。修改后的目标函数为L L^{CLIP} - β * KL(π_θ || π_ref)其中π_ref通常是初始的SFT模型。系数β需要仔细调整。自适应KL控制像OpenAI在最初的InstructGPT论文中那样动态调整β。设定一个目标KL散度值如d_targ如果当前批次的平均KL散度高于目标则增大β以加强惩罚如果低于目标则减小β。奖励裁剪Reward Clipping对奖励模型输出的原始奖励值进行裁剪例如限制在[-10, 10]的区间内防止个别极端奖励值主导优化方向。使用参考模型Reference Model在计算重要性采样比率r_t(θ)时分母不是用上一步的旧策略而是始终用一个固定的、参数冻结的初始SFT模型即参考模型。这能更稳定地将策略锚定在合理的分布附近。这是目前很多实现如TRL的默认做法。实操心得我强烈建议始终使用参考模型并配合一个较小的、固定的KL惩罚系数如β0.01到0.1。同时密切监控训练日志中的kl_divergence、reward和reward_kl_penalized即加了KL惩罚后的奖励这几个指标。一个健康的训练曲线应该是reward_kl_penalized缓慢而稳定地上升同时kl_divergence保持在一个较低的水平例如0.5到5之间具体取决于任务。4.2 训练不稳定损失剧烈震荡现象Actor或Critic的损失值在迭代中上蹿下跳没有收敛趋势。根因学习率过高、批次大小太小、梯度爆炸、或优势估计不准确。解决方案微小的学习率大模型的RLHF学习率必须非常小。对于百亿参数模型Actor和Critic的学习率通常在1e-6到5e-6量级。可以从1e-6开始尝试。梯度裁剪Gradient Clipping这是稳定训练的生命线。务必对Actor和Critic的梯度范数进行裁剪通常设置max_norm0.5或1.0。增大批次大小Batch Size在硬件允许的范围内尽可能使用大的批次大小。这能提供更稳定的梯度估计。如果显存不足可以累积梯度Gradient Accumulation。优势归一化Advantage Normalization如前文代码所示对每个批次内的优势进行归一化减去均值除以标准差这是一个标准且有效的技巧。价值函数预训练Value Function Pretraining在正式PPO训练前先用一些数据例如SFT数据或初始生成的数据单独训练Critic一段时间让它学会预测回报的基线。这能提供更准确的优势估计加速PPO的稳定。4.3 模型“遗忘”或“胡说八道”现象模型在追求高奖励的过程中忘记了在SFT阶段学到的语言能力和事实知识开始生成语法错误、事实错误或完全脱离上下文的回答。根因奖励模型可能没有对语言流畅性和事实准确性给予足够的奖励信号导致模型为了“讨好”RM而牺牲了基础能力。解决方案在奖励中融入监督信号一种有效的方法是在最终奖励中混合一个监督微调SFT损失。即总损失 PPO损失 λ * SFT损失。这个SFT损失计算的是当前模型在旧数据SFT数据或高质量问答对上的负对数似然。这相当于给模型一个“锚点”提醒它不要忘记基本功。参数λ需要权衡。使用更好的奖励模型根本原因在于奖励模型的质量。确保你的奖励模型是在高质量、多样化的偏好数据上训练的并且经过了充分的校准能够同时衡量“有用性”、“诚实性”和“无害性”。课程学习Curriculum Learning不要一开始就用很难的提示或很强的奖励信号。可以从简单的提示和温和的KL惩罚开始随着训练进行逐步增加任务的复杂性或调整超参数。4.4 显存爆炸与计算效率现象即使模型参数量不大PPO训练也很快耗尽显存。根因PPO需要同时存储多个模型Actor, Critic, Reference Model, Reward Model在显存中并且在前向和反向传播中需要保存大量的中间激活值用于计算概率比率和优势。解决方案模型共享让Actor和Critic共享Transformer主干网络只使用不同的头部LM Head和Value Head。这能大幅减少参数量和显存占用。梯度检查点Gradient Checkpointing在Transformer层中启用梯度检查点用计算时间换显存空间。这对于长序列生成至关重要。使用优化库不要从头造轮子。使用像TRLTransformer Reinforcement Learning或DeepSpeed-Chat这样的专门库。它们经过了高度优化集成了模型共享、梯度检查点、混合精度训练、甚至ZeRO-3等分布式优化技术。例如使用trl.PPOTrainer可以省去上面绝大部分的繁琐实现。离线经验回放如果在线生成数据太慢可以考虑先用一个策略生成大量(prompt, response)对并计算好奖励和优势存储起来然后用这些固定数据进行多轮PPO更新。但这可能会引入偏差因为数据不再来自当前策略。5. 超越基础PPO的变体与前沿探索PPO是当前RLHF的事实标准但研究社区并未止步。了解这些变体有助于你理解未来的发展方向。5.1 PPO-ptx防止知识遗忘的利器这是OpenAI在InstructGPT中使用的技术全称是“PPO with pre-training gradient mixing”。如前所述它在PPO目标函数中混合了预训练或SFT的目标。具体来说它从预训练数据集中采样一批文本计算当前模型在这些文本上的语言建模损失并将该损失的梯度与PPO损失的梯度按一定比例混合。这被证明能非常有效地防止模型在追求对齐时发生“灾难性遗忘”。如果你发现模型在RLHF后常识和语言能力下降这是首要尝试的改进。5.2 新一代策略优化算法DPO及其家族PPO虽然强大但其训练流程复杂需要维护多个模型进行多阶段训练且对超参数敏感。近年来直接偏好优化Direct Preference Optimization, DPO异军突起。DPO的核心思想非常巧妙它绕过了奖励模型建模和复杂的强化学习循环直接利用偏好数据来优化策略模型。它推导出一个损失函数使得优化后的策略模型能够隐式地符合一个最优的奖励函数。DPO的训练像SFT一样简单只需一个模型和一批(chosen, rejected)的偏好对但效果常能与PPO媲美因此迅速流行。随后出现的身份策略优化Identity Policy Optimization, IPO和KTOKahneman-Tversky Optimization等都在DPO的基础上进行了改进旨在解决DPO可能存在的过拟合问题或利用不同类型的反馈数据。对于资源有限的研究者或开发者从DPO开始尝试RLHF是一个更低门槛的选择。5.3 探索与利用的再平衡熵奖励与采样温度在PPO的目标函数中我们加入了熵奖励entropy_bonus来鼓励探索。但在文本生成中探索生成多样化的词和利用生成高奖励的词的平衡非常微妙。除了调整entropy_coef系数另一个关键控制杆是生成时的采样温度Temperature。在训练阶段Rollout使用较高的温度如0.7-1.0可以增加探索让模型尝试更多样的回复从而收集到更丰富的数据。在评估或部署阶段则使用较低的温度如0.2-0.5以获得更确定、更优质的输出。5.4 多目标优化平衡有用、诚实与无害真实的RLHF rarely是单一奖励的优化。我们通常希望模型同时做到有用Helpful、诚实Honest和无害Harmless即所谓的“HHH”原则。一种实践方法是训练多个奖励模型分别对应不同的维度然后在PPO训练中将多个奖励加权求和作为总奖励。例如R_total α * R_helpfulness β * R_truthfulness γ * R_harmlessness。调整这些权重α, β, γ就是在塑造模型的不同性格倾向。这要求你的偏好标注数据也需要从多个维度进行标注。调试一个PPO训练就像驾驶一辆高性能但敏感的赛车你需要同时关注多个仪表盘奖励、KL散度、损失、生成样本质量并微调多个控制杆学习率、裁剪系数、熵系数、KL系数。它没有银弹参数需要根据你的具体模型、数据和奖励函数进行大量的实验和耐心调整。每一次训练崩溃后的日志分析都是你更深入理解语言模型如何“学习”和“思考”的宝贵机会。当你看到模型生成的回答从生硬刻板逐渐变得自然、有用且符合预期时那种成就感无疑是驱动你继续深入这个领域的最大动力。