LLM中PPO、DPO以及GRPO

LLM中PPO、DPO以及GRPO 2025年Deepseek推出了Deepseek-R1模型一下子把LLM和强化学习推到一个新的热度。作为nlp的从业人员经历过Bert、GPT2、ChatGPT以及GPT-4O和R1等模型见证了模型从不会思考到具有一定的推理和思考能力的变迁。记得2022年的时候也是采用PPO强化学习的算法用于对话策略学习实现了一个任务型问答机器人。时至今日LLM领域强化学习算法一步一步的进化升级从RLHF中的PPO到后来的DPO又到今年的GRPO以及更新更好的一些算法模型训练效率越来越高、模型效果也越来越好强化学习在LLM中的地位也越来越高。之前也有计划写一点强化学的相关内容由于一些这样那样的原因(我懒理解不到位)这个规划就一直搁浅了接着Deepseek-r1模型大火看了好多遍PPO、DPO以及GRPO相关的文章和代码后我觉得还是要写一篇强化学习相关的内容记录一下这个知识以防忘记。本文就LLM中的PPO、DPO以及GRPO算法进行解析增强自己对它们的理解由于水平有限难免出现一些错误如果有错轻喷评论区指出就好。一、强化学习的基础概念为了更好的理解强化学习以及后续的各种算法先从强化学习中的整体流程和基础概念出发。下图是引至《深度强化学习基础研究和应用》一书第2章节形象的描述了强化学习的组成和场景示例1、基础概念和流程强化学习中重要的组成部分智能体、环境、动作、状态和奖励智能体强化学习的主体、与环境进行交互负责动作或者执行策略环境强化学习的基本元素与智能体交互的对象可以理解为规律规则或者规则的组合动作空间智能体能够采取的行为的集合。比方说上图围棋游戏中的在某个点下子。状态空间当前环境信息的所有集合奖励智能体采取动作后环境给出的反馈有正有负、有大有小强化学习的目标让智能体学习到一种模式或者策略使得它与环境长期或者指定的时间段内交互得到的奖励累积最大化2、重要关键词强化学习中有很多稀奇古怪、容易混淆的概念下面就把我认为比较重要的关键词列举出来并做简单的说明解释和分析。怎么来实现强化学习的目标呢有两种路线一种是策略优化直接优化策略一种是价值优化通过价值的大小来间接的选择最优动作策略优化(policy-optimization)直接优化策略(神经网络参数化建模)策略给出动作状态的条件概率。学习的过程中把从初始状态开始累积的奖励期望最大化作为优化目标对参数就偏导就得到梯度。特点是——高方差、低偏差。方差是指策略优化过程中建模的神经网络更新的方向和幅度在不同的训练数据上的波动情况偏差是指学习的策略和期望的策略的差异程度。方差高是由于和环境交互轨迹(动作选择或者状态转移等)具有随机性——训练数据差异较大因此神经网络更新的方向和幅度必然不稳定波动比较大由于是直接对神经网络建模的策略进行学习只要采样够多学习充分最后得到的策略和我们期望的策略就是差异就是比较小的。价值优化(value-optimization)先学习价值函数——状态或者状态-动作对的长期期望收益然后策略就是根据价值函数来间接选择最优动作。所以优化还是优化价值函数没有策略函数也就是没有显式的对策略进行建模(用一个神经网络参数化建模策略)。特点是——低方差、高偏差。这里的方差是指价值函数的价值估计(具体的价值大小)在不同的训练数据上的波动情况偏差是指估计价值和真实价值或者间接的策略和真实的策略之间的差异。方差低是由于它的价值估计算法采用了即时奖励和下一状态的估计主要的波动就在即时奖励上被局部化而即时奖励的波动不大所以整体方差就比较低偏差主要是由于价值的估计本身就带有系统误差——估计的就不准确以及再间接的指导策略选择动作进一步的加大误差所以偏差就更大了。学习方式可以分为在线以及离线这两种方式on-line在线学习主要是指智能体和环境交互过程中实时生成训练数据并利用这些数据来更新策略。比如我们本文中关注的算法PPO和GPRO等学习过程中都是先和环境交互输入prompt得到response和reward组成训练数据然后进行即时训练。可以简单的理解训练过程中模型有没有生成数据生成了就是在线学习。off-line离线学习顾名思义就是离线不是实时和环境交互获取训练数据而是利用预先收集好的数据进行训练和学习。DPO算法就是离线学习的算法模型的prompt和response都是提前收集好了没有在训练过程中实时的生成。从数据生成使用的策略也就是行为策略和学习优化的目标策略是否相同的角度强化学习可以分为on-policy(同策算法)和off-police(异策算法)on-policy行为策略和目标策略是同一个策略那么它就是on-policy的算法。行为策略怎理解呢就是强化学习过程中数据的生成采用的是什么策略。PPO算法中数据生成使用的是Actor这个模型同时优化的目标策略也是Actor这个模型所以简单来讲它就是on-policy算法。严格的来讲我赞同PPO是on-policy和off-policy的算法PPO的优化目标策略实现的时候会使用收集到的数据更新多个epoch在第一个epoch可以认为行为策略和目标策略是同一个策略这个时候就是on-policy后面的几个epoch就是off-policy了模型参数已经更新过了它不是原来生成数据的策略了。有更多不同的理解可以参考问题——在强化学习中为什么TRPO和PPO算法属于On-Policy的算法off-policy行为策略和目标策略不是同一个策略那么它就是off-policy算法。具体是算法有Q-Learning下面看一个Q-Learning算法示例图(强化学习中的奇怪概念(一)——On-policy与off-policy 小错 高赞文章)图中可以明确的看到收集数据的行为策略(蓝色框框)是1-贪心策略而目标策略(红色框框 arg max)是完整的贪心策略它们两个不是同一个策略因此Q-Learning就是off-policy的算法。3、NLP中的强化学习上文给出了一个具体是强化学习的应用场景示例那就是围棋游戏。它的智能体、环境、状态、动作分别如下定义智能体下围棋的策略或者电脑环境围棋的规律或者规则智能体与其交互状态是指当前棋盘黑白子的坐标信息以及没有棋子的坐标信息动作智能体围棋落子的坐标信息在NLP中这些概念或者名字以及流程和NLP又是怎么对应的呢在NLP中使用强化学习的目的就是让模型生成符合我们人类喜好、毒害性少、帮助性好这样最优的文本。gpt类模型采用自回归解码方式给定输入预测下一个token然后把历史输入和生成的token作为新的输入预测新的一个token。按照上述步骤一直进行下去直到满足结束条件。如图prompt输入模型后就会生成下一个token(图中的蓝色框框)。输入的prompt为上文在强化学习中可以视为状态生成的token视为动作那么此时可以得到及时奖励(根据当前上下文当前生成这个token的好坏的评估)以及总收益当前时刻的即时奖励以及未来时刻的总收益并且智能体、环境、状态以及动作等概念可以按照如下理解智能体想要训练的大语言模型环境评估模型生成的文本的规则或者规律状态当前的上文动作词表对应的空间模型要生成的一个token当然还有一些疑问就是NLP中使用强化学习和到底是怎么计算的在后面的章节中我们会给出解释。二、RLHF中的PPO2022年11月OpenAI发布了ChatGPT引发了全民对 LLM 的应用、探索、学习和创新的热潮同时强化学习在 LLM 中持续深入它通过反馈优化模型生成更贴合人类喜好的文本。其中PPO算法首次在文本大模型中成功应用为 LLM 优化开辟新路径。下文我们就一起看看RLHF以及它的PPO是什么样的。1、RLHF算法流程和关键模型RLHF在LLM火起来可以追溯到OpenAI的instructGPT——Training language models to follow instructions with human feedback该论文详细的介绍了RLHF的训练流程、涉及到的具体模型数量以及使用的强化学习的算法。方法示例图如下第一步收集示范数据对GPT3进行微调。——SFT第二步收集对比数据训练一个奖励模型。——reward model第三步使用PPO强化学习算法结合奖励模型进行学习——rl PPO用一个模型流程图来表示上述过程第一阶段SFT只有一个模型更新第二阶段的Reward Model的训练也只有一个模型更新最后的PPO算法阶段需要涉及到4个模型分别是Actor Model就是我们训练的目标模型训练完成后这个模型就可以用来部署进行推理了。参数需要更新虚线框的模型。Reference Model其实就算第一阶段微调好的预训练模型它的作用就是给Actor Model训练更新的时候一个约束让它不要太奔放跑得太歪了。这个模型是冻结的不参与参数更新。Critic Model一般的RL方案中这个模型初始化就是第二阶段训练好的Rewrad模型它的作用是评价给定prompt已生成的token的价值也就是上面提到的注意它包含了当前时刻的奖励以及未来所有时刻的奖励并且这是一个预估的值对未来的预估可靠性有待商榷。参数也需要更新虚线框的模型。Reward Model奖励模型顾名思义就是给出当前生成的token的奖励——它是即时奖励理论上是准确的模型冻结参数不更新。另外注意这里其实有个细节的地方奖励模型训练的时候对比的是整个response1和response2的好坏。推理的时候把promptresponse通过Linear_head有[L,D]的logits转化为[L]这里的每个分数都可以使用吗很明显只有最后一个token的分数是准确的我们人类标注数据也是看的prompt完整的response而不是prompt部分response做排序这个也排序不了因为不知道未来的token是什么样的。——简而言之就是reward模型的分数只有最后一个token是准确的其他位置给出的分数都是不太准确的。业界主流的实现方案也是只使用最后一个token的Reward Model分数作为奖励其他的位置则使用另外的方案来获取奖励。当然也有一些工作直接尝试使用Reward Model给出的每个位置的分数或者认为每个token都是一样的重要因此把一个平均分数分配给所有的token。后面代码阅读的时候可以看看主流的方案是怎么处理的。2、Loss函数reward model loss这个loss函数公式字面上的意思就是针对一个prompt-X 标注了K(4K9)个response它们之间有排序关系构建为K*(K-1)/2个样本对形如(X,Yw,Yl)使得XYw和XYl输入到模型后得出的分值只差经过sigmoid函数之后取对数对所有的样本求期望(也就是求一个平均值)加上负号loss函数要求这个值越小越好。含义就是模型能力具备区分不同答案的好坏区分得越开越好不同的答案之间的奖励差值越大越好。具体的伪代码如下for epoch in range(epochs): total_loss 0 for batch in dataloader: x, y_w, y_l batch r_theta_yw model(xy_w) r_theta_yl model(xy_l) diff r_theta_yw - r_theta_yl log_prob torch.log(sigmoid(diff)) loss -torch.mean(log_prob) optimizer.zero_grad() loss.backward() optimizer.step()同样可以看看LLM-Factory这个比较流行的模型训练库RM模型训练的loss是怎么计算的代码如下override def compute_loss( self, model: PreTrainedModel, inputs: dict[str, torch.Tensor], return_outputs: bool False, **kwargs ) - Union[torch.Tensor, tuple[torch.Tensor, list[torch.Tensor]]]: rCompute pairwise loss. The first n examples are chosen and the last n examples are rejected. Subclass and override to inject custom behavior. Note that the first element will be removed from the output tuple. See: https://github.com/huggingface/transformers/blob/v4.40.0/src/transformers/trainer.py#L3842 _, _, values model(**inputs, output_hidden_statesTrue, return_dictTrue, use_cacheFalse) batch_size inputs[input_ids].size(0) // 2 chosen_masks, rejected_masks torch.split(inputs[attention_mask], batch_size, dim0) chosen_rewards, rejected_rewards torch.split(values, batch_size, dim0) chosen_scores chosen_rewards.gather(dim-1, index(chosen_masks.sum(dim-1, keepdimTrue) - 1)) rejected_scores rejected_rewards.gather(dim-1, index(rejected_masks.sum(dim-1, keepdimTrue) - 1)) chosen_scores, rejected_scores chosen_scores.squeeze(), rejected_scores.squeeze() loss -torch.nn.functional.logsigmoid(chosen_scores.float() - rejected_scores.float()).mean() if return_outputs: return loss, (loss, chosen_scores, rejected_scores) else: return loss很明显的可以看到这个代码和上图的损失函数公式是对齐的没有比较难理解的实现。注意这里的样本对的组织和处理方式有两种策略1、把同一个prompt的构建的样本对放到一个batch中——这个是严格的遵循论文的目标函数和损失函数。优势是batch内能够学习到数据的排序关系感觉数据的多样性有点少因为batch内的数据同质性验证、相关性过高2、是把不同的prompt构建的样本对放到同一个batch中——这样每一次的更新就和论文的公式不太一样这里的优势就是数据在一个batch内多样性好这样的训练方式会使得模型最后的泛化性能比较好。业界训练RM模型使用的方案都是采用第二种样本组织和处理的方式为的就是增强模型的泛化性能。RLHF中的目标函数上文也提到过RLHF中PPO的训练涉及到4个模型其中有两个模型是非冻结可以学习的就是actor model和critic model那么上述目标函数就是一定会把loss应用或者说关联到这个两个模型上面。简单理解一下这个目标函数的意义针对一个输入x改变学习模型的参数然它的输出y使得RM模型的(x,y)的奖励值与新模型和老模型的概率比(这个比值一定是大于1的)的对数的差值最大化。——所以就要取r(x,y)更大的同时新模型不能偏离原始的SFT模型太远。如何来限制或者调整y的生成呢PPO强化学习算法框架中其优化目标是使得累积折扣回报期望最大化采用的是GAE来指导限制Actor模型的学习。Actor模型的目标函数表示新老模型的概率比这里需要用到actor和referenc模型的logtis——概率比*相对优势越大越好。目标函数梯度如下这里的优势含义就是评估选择的动作相对其他平均动作到底有多好或者多坏这里的优势函数的公式中也做了类似未来折扣计算(当前时刻的真实值未来所有时刻的估计)——那么对动作的好坏评估就更加准确了而计算需要用到奖励模型的输出、critic模型的输出。到这里所有的模型都参与到整个策略的更新了。总结1、为了让积累的折扣回报期望越大采用GAE来指导actor模型的学习让它产生好的动作的概率更大坏的动作概率更小——学习的模型是actor模型奖励模型、参考模型、评价模型都是间接的参与到模型学习优化中。2、为了让优化计算时候的基线更准确率critic 模型需要进行学习优化对未来的价值预测更加准确辅助actor的学习让actor模型学习的更准确。对应于具体的PPO算法中目标函数如下DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models公式1中具体说的是求期望(对所有的问题q,和对应生成o)后面的公式韩式——对o中的每个token新旧模型相对gae优势求平均整体就是 对token的新旧模型相对gae平均优势求期望。注意到这个是求token级别的。另外一个注意点是RM模型最后一个token是有奖励的也就是非EOS的token 奖励R为0所有根据下面的公式是可以计算每一个时刻的(每个token)的优势。最后一个token是有奖励的也就是非EOS的token 奖励R为0——这个是一个强假设或者说工程上的妥协现在的方案中这样假设中间token奖励为0同时结合critic 模型来预测每个token的价值一起计算优势模型的效果是可以的。但是理论上这样的假设是不合理的一个序列中每个字都应该有不同的价值而不是简简单单的只给最后一个字给于奖励其他的为0。从工程上来说最后一个token是有奖励的也就是非EOS的token 奖励R为0是最容易实现的成本最低如果要给其他的token给出一个合适的奖励目前是无法做到的或者说很难做到。从上文可知actor模型和critic模型都是需要学习的下文来看看具体的各自的loss实现actor lossdef compute_policy_loss( old_log_probs: torch.Tensor, log_probs: torch.Tensor, advantages: torch.Tensor, response_mask: torch.Tensor, clip_ratio_low: float, clip_ratio_high: float, clip_ratio_dual: float, ) - Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: Compute the policy loss. Args: old_log_prob: (torch.Tensor) shape: (bs, response_length) log_prob: (torch.Tensor) shape: (bs, response_length) advantages: (torch.Tensor) shape: (bs, response_length) response_mask: (torch.Tensor) shape: (bs, response_length) clip_ratio_low: (float) The lower clip range used in PPO. See https://arxiv.org/abs/1707.06347 clip_ratio_high: (float) The higher clip range used in DAPO. See https://arxiv.org/pdf/2503.14476 clip_ratio_dual: (float) The dual clip range used in Dual-clip PPO. See Returns: pg_loss: a scalar torch.Tensor policy gradient loss computed via PPO pg_clipfrac_higher: (float) a float number indicating the fraction of policy gradient loss being clipped to a higher value pg_clipfrac_lower: (float) a float number indicating the fraction of policy gradient loss being clipped to a lower value ppo_kl: (float) a float number indicating the mean KL divergence between the old policy and the new policy negative_approx_kl log_probs - old_log_probs # clamp the ratio before exp to avoid nan # see: https://github.com/pytorch/pytorch/issues/10729 ratio torch.exp(negative_approx_kl) clipped_ratio torch.exp( torch.clamp(negative_approx_kl, np.log(1.0 - clip_ratio_low), np.log(1.0 clip_ratio_high)) ) pg_loss -advantages * ratio pg_loss2 -advantages * clipped_ratio pg_loss3 -advantages * clip_ratio_dual clipped_pg_loss_higher torch.max(pg_loss, pg_loss2) # clip if pg_loss pg_loss2 pg_clipfrac_higher (pg_loss pg_loss2).float() clipped_pg_loss_lower torch.min(clipped_pg_loss_higher, pg_loss3) # clip if pg_loss pg_loss3 and adv 0 final_pg_loss torch.where(advantages 0, clipped_pg_loss_lower, clipped_pg_loss_higher) pg_clipfrac_lower (clipped_pg_loss_higher pg_loss3).float() * (advantages 0).float() final_pg_loss VF.masked_mean(final_pg_loss, response_mask) pg_clipfrac_higher VF.masked_mean(pg_clipfrac_higher, response_mask) pg_clipfrac_lower VF.masked_mean(pg_clipfrac_lower, response_mask) ppo_kl VF.masked_mean(-negative_approx_kl, response_mask)和Actor模型的目标函数印证这里的实现多了一些处理pg_loss2就已经实现了标准的PPO的actor loss后续的处理就是对截断的上下界以及小于0的处理防止优势小于0的时候梯度出现问题。critic lossdef compute_value_loss( vpreds: torch.Tensor, returns: torch.Tensor, values: torch.Tensor, action_mask: torch.Tensor, cliprange_value: float, ) - Tuple[torch.Tensor, float]: Compute the value loss. Args: vpreds (torch.FloatTensor): Predicted values of the value head, shape (batch_size, response_length) returns: (torch.FloatTensor): Ground truth returns, shape (batch_size, response_length) values (torch.FloatTensor): Old values of value head, shape (batch_size, response_length) action_mask: (torch.Tensor) shape: (bs, response_length) cliprange_value: (float) The clip range for value net used in PPO Returns: vf_loss: a scalar (torch.FloatTensor): value function loss vf_clipfrac: a float The ratio of vf being clipped vpredclipped torch.clamp(vpreds, values - cliprange_value, values cliprange_value) vf_loss1 torch.square(vpreds - returns) vf_loss2 torch.square(vpredclipped - returns) vf_loss 0.5 * VF.masked_mean(torch.max(vf_loss1, vf_loss2), action_mask) # clip if vf_loss1 vf_loss2 vf_clipfrac VF.masked_mean((vf_loss1 vf_loss2).float(), action_mask) return vf_loss, vf_clipfrac它是一个L2也就是MSE均方误差损失函数是critic模型当前的value(状态价值)对return(也就是Q值当前的动作价值)的拟合上述实现是verl中的源码添加了一些clip的策略让模型训练更稳定效果更好。3、训练流程上述训练流程图是阅读了verl中ppo训练流程得到的过程示意图。图中还有一个细节没有画出来。就是log_probs的生成中是有old_log_probs和ref_log_probs其中ref_log_probs就是使用参考模型来生成计算的作用就是用来限制actor模型更新离参考模型太远了更新步子不要太大。注意到训练的数据是没有label的只需要有prompt没有预先收集好的response让模型自己去探索生成使用rewardmodel 的奖励来指导模型参数的更新。三、低成本的DPOPPO作为一个强化学习的算法 LLM 推理和思考能力的提升具有开创性意义但是它具有明显的不足。1、训练超参及其敏感时不时就训练异常需要反复调整训练超参2、最明显的就是资源消耗很大训练一个目标LLM需要使用Actor、Critic、reference以及reward等4个模型显存消耗很大。为了找到一条低消耗能让模型具有一定的推理和思考的能力2023年研究者们对PPO进行了简化提出了DPO来进行LLM的RLHF的训练不严格的来说这个算法其实就是一个带有强化学习和对比学习思想的SFT。1、关键模型DPO训练中使用到的模型就比之前的PPO要少很多了只需要一个训练的Acotr模型和一个reference模型。架构如下图其中reference模型是要冻结的参数不可更新而actor模型是要冻结的。注意到模型的训练数据必须是同一个promptchosen_response和promptrejected_response目标就是希望模型经过训练后能够尽可能的选择输出chosen_response(符合人类喜好的或者偏向的)2、Loss函数上述论文的截图中给出了标准DPO损失函数的公式函数就是最小化新老模型在正负样本概率比值的对数与新老模型在负样本概率比值的对数的差的对数的相反数——进一步理解由于对数函数单调递增它就是最大化新模型在正样本概率比值的对数与与新老模型在负样本概率比值的对数的差——拉大正样本和负样本之间的概率的对数差值。这个就是DPO的目标让模型朝着输出正样本的方向去学习。从实际上来说上述的loss函数的目标是不会总是达成的。上面的loss函数做转化后就是下图中的公式(使用对数函数变形和加减法的结合)显示dpo的目标可以等价于让新模型的正负样本的概率比值 比 老模型的正负样本比值要大就能降低loss。但是这个loss降低可能是新模型 正负样本的绝对概率都变小或者变大(只要相对比值比老模型大——老模式是0.6/0.3,新模型是0.3/0.1这样log3-log2 大于0 降低loss)这种情况下模型的输出是什么呢很有可能就是模型原始分布中的其他response。实际训练过程中也可以观察到模型loss降低但是模型不怎么输出正样本有很多时候生成其他的样本更有的时候loss升高了效果不变坏。也是这个新模型正负样本绝对概率变化导致的。接下来看看trl框架中dpo_trainer中的loss实现def dpo_loss( self, chosen_logps: torch.FloatTensor, rejected_logps: torch.FloatTensor, ref_chosen_logps: torch.FloatTensor, ref_rejected_logps: torch.FloatTensor, ) - tuple[torch.FloatTensor, torch.FloatTensor, torch.FloatTensor]: Compute the DPO loss for a batch of policy and reference model log probabilities. Args: chosen_logps (torch.FloatTensor): Log probabilities of the model for the chosen responses. Shape: (batch_size,). rejected_logps (torch.FloatTensor): Log probabilities of the model for the rejected responses. Shape: (batch_size,). ref_chosen_logps (torch.FloatTensor): Log probabilities of the reference model for the chosen responses. Shape: (batch_size,). ref_rejected_logps (torch.FloatTensor): Log probabilities of the reference model for the rejected responses. Shape: (batch_size,). Returns: A tuple of three tensors: (losses, chosen_rewards, rejected_rewards). The losses tensor contains the DPO loss for each example in the batch. The chosen_rewards and rejected_rewards tensors contain the rewards for the chosen and rejected responses, respectively. device self.accelerator.device # Get the log ratios for the chosen and rejected responses chosen_logratios chosen_logps.to(device) - (not self.reference_free) * ref_chosen_logps.to(device) rejected_logratios rejected_logps.to(device) - (not self.reference_free) * ref_rejected_logps.to(device) if self.f_divergence_type FDivergenceType.ALPHA_DIVERGENCE.value: # The alpha-divergence formula: (1 - u^-alpha) / alpha # The divergence difference between the chosen and rejected sample is: # (1 - u[w]^-alpha) / alpha - (1 - u[l]^-alpha) / alpha # (u[l]^-alpha - u[w]^-alpha) / alpha # where u[w] and u[l] are the policy/reference probability ratios # for the chosen and rejected samples, respectively. alpha_coef FDivergenceConstants.ALPHA_DIVERGENCE_COEF_DEFAULT if self.f_divergence_params and FDivergenceConstants.ALPHA_DIVERGENCE_COEF_KEY in self.f_divergence_params: alpha_coef float(self.f_divergence_params[FDivergenceConstants.ALPHA_DIVERGENCE_COEF_KEY]) logits (cap_exp(rejected_logratios * -alpha_coef) - cap_exp(chosen_logratios * -alpha_coef)) / alpha_coef else: logratios chosen_logps - rejected_logps if self.reference_free: ref_logratios torch.tensor([0], dtypelogratios.dtype, devicelogratios.device) else: ref_logratios ref_chosen_logps - ref_rejected_logps logratios logratios.to(self.accelerator.device) ref_logratios ref_logratios.to(self.accelerator.device) logits logratios - ref_logratios if self.f_divergence_type FDivergenceType.JS_DIVERGENCE.value: # The js-divergence formula: log(2 * u / (1 u)) # The divergence difference between the chosen and rejected sample is: # log(2 * u[w] / (1 u[w])) - log(2 * u[l] / (1 u[l])) # log(u[w]) - log(u[l]) - (log(1 u[w]) - log(1 u[l])) # where u[w] and u[l] are the policy/reference probability ratios # for the chosen and rejected samples, respectively. logits - F.softplus(chosen_logratios) - F.softplus(rejected_logratios) # The beta is a temperature parameter for the DPO loss, typically something in the range of 0.1 to 0.5. # We ignore the reference model as beta - 0. The label_smoothing parameter encodes our uncertainty about the # labels and calculates a conservative DPO loss. if self.loss_type sigmoid: losses ( -F.logsigmoid(self.beta * logits) * (1 - self.label_smoothing) - F.logsigmoid(-self.beta * logits) * self.label_smoothing ) elif self.loss_type robust: losses ( -F.logsigmoid(self.beta * logits) * (1 - self.label_smoothing) F.logsigmoid(-self.beta * logits) * self.label_smoothing ) / (1 - 2 * self.label_smoothing) elif self.loss_type exo_pair: # eqn (16) of the EXO paper: https://huggingface.co/papers/2402.00856 import math if self.label_smoothing 0: self.label_smoothing 1e-3 losses (self.beta * logits).sigmoid() * ( F.logsigmoid(self.beta * logits) - math.log(1 - self.label_smoothing) ) (-self.beta * logits).sigmoid() * (F.logsigmoid(-self.beta * logits) - math.log(self.label_smoothing)) elif self.loss_type hinge: losses torch.relu(1 - self.beta * logits) elif self.loss_type ipo: # eqn (17) of the paper where beta is the regularization parameter for the IPO loss, denoted by tau in the paper. losses (logits - 1 / (2 * self.beta)) ** 2 elif self.loss_type bco_pair: chosen_logratios chosen_logps - ref_chosen_logps rejected_logratios rejected_logps - ref_rejected_logps chosen_rewards self.beta * chosen_logratios rejected_rewards self.beta * rejected_logratios rewards torch.cat((chosen_rewards, rejected_rewards), 0).mean().detach() self.running.update(rewards) delta self.running.mean losses -F.logsigmoid((self.beta * chosen_logratios) - delta) - F.logsigmoid( -(self.beta * rejected_logratios - delta) ) elif self.loss_type sppo_hard: # In the paper (https://huggingface.co/papers/2405.00675), SPPO employs a soft probability approach, # estimated using the PairRM score. The probability calculation is conducted outside of the trainer class. # The version described here is the hard probability version, where P in Equation (4.7) of Algorithm 1 is # set to 1 for the winner and 0 for the loser. a chosen_logps - ref_chosen_logps b rejected_logps - ref_rejected_logps losses (a - 0.5 / self.beta) ** 2 (b 0.5 / self.beta) ** 2 elif self.loss_type nca_pair: chosen_rewards (chosen_logps - ref_chosen_logps) * self.beta rejected_rewards (rejected_logps - ref_rejected_logps) * self.beta losses ( -F.logsigmoid(chosen_rewards) - 0.5 * F.logsigmoid(-chosen_rewards) - 0.5 * F.logsigmoid(-rejected_rewards) ) elif self.loss_type aot_pair: chosen_logratios chosen_logps - ref_chosen_logps rejected_logratios rejected_logps - ref_rejected_logps chosen_logratios_sorted, _ torch.sort(chosen_logratios, dim0) rejected_logratios_sorted, _ torch.sort(rejected_logratios, dim0) delta chosen_logratios_sorted - rejected_logratios_sorted losses ( -F.logsigmoid(self.beta * delta) * (1 - self.label_smoothing) - F.logsigmoid(-self.beta * delta) * self.label_smoothing ) elif self.loss_type aot: logratios chosen_logps - rejected_logps ref_logratios ref_chosen_logps - ref_rejected_logps logratios_sorted, _ torch.sort(logratios, dim0) ref_logratios_sorted, _ torch.sort(ref_logratios, dim0) delta logratios_sorted - ref_logratios_sorted losses ( -F.logsigmoid(self.beta * delta) * (1 - self.label_smoothing) - F.logsigmoid(-self.beta * delta) * self.label_smoothing ) elif self.loss_type apo_zero: # Eqn (7) of the APO paper (https://huggingface.co/papers/2408.06266) # Use this loss when you believe the chosen outputs are better than your models default output losses_chosen 1 - F.sigmoid(self.beta * chosen_logratios) # Increase chosen likelihood losses_rejected F.sigmoid(self.beta * rejected_logratios) # Decrease rejected likelihood losses losses_chosen losses_rejected elif self.loss_type apo_down: # Eqn (8) of the APO paper (https://huggingface.co/papers/2408.06266) # Use this loss when you believe the chosen outputs are worse than your models default output. # Decrease chosen likelihood and decrease rejected likelihood more losses_chosen F.sigmoid(self.beta * chosen_logratios) losses_rejected 1 - F.sigmoid(self.beta * (chosen_logratios - rejected_logratios)) losses losses_chosen losses_rejected elif self.loss_type discopop: # Eqn (5) of the DiscoPOP paper (https://huggingface.co/papers/2406.08414) # This loss was discovered with LLM discovery logratios chosen_logps - rejected_logps ref_logratios ref_chosen_logps - ref_rejected_logps logits logratios - ref_logratios logits logits * self.beta # Modulate the mixing coefficient based on the log ratio magnitudes log_ratio_modulation torch.sigmoid(logits / self.args.discopop_tau) logistic_component -F.logsigmoid(logits) exp_component torch.exp(-logits) # Blend between logistic and exponential component based on log ratio modulation losses logistic_component * (1 - log_ratio_modulation) exp_component * log_ratio_modulation上述代码是完整的实现里面包含了很多种对新老模型对数概率差值的处理方案我们只看最标准的sigmoid方案把核心的代码切出来看如下logratios chosen_logps - rejected_logps ref_logratios ref_chosen_logps - ref_rejected_logps logits logratios - ref_logratios losses ( -F.logsigmoid(self.beta * logits) * (1 - self.label_smoothing) - F.logsigmoid(-self.beta * logits) * self.label_smoothing )这个论文的公式就是完全一一对应的只不过加了smooth处理这个正则化的处理就是防止模型对样本对的概率差值太绝对了让模型有更强的容错。四、R1类思考模型的GRPODPO算法简化了PPO训练成本它比较擅长让模型不学习到人类不希望模型生成的知识但是模型的思考和推理能力并没有提升。2025年Deepseek 在其Deepseek v3基座模型的基础上使用强化学习的方式进行RL训练发布了带有思考过程的Deepseek R1模型因为它的思考和推理效果惊艳、有质的提升同时开源训练方案以及训练方法确实相对以前的方法简单有效迅速引发广泛关注。一时间DeepSeek R1 热度飙升在全球 AI 领域掀起热潮成为现象级产品。其使用的强化学习算法GRPO以及训练方案都值得学习。1、关键模型和算法流程2、Loss函数的理解以及代码阅读论文中的loss函数DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models另外deepseek的论文中DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning这个损失函数和之前PPO的损失函数大同小异都是token级别的核心的不同点有2个a、grpo的损失中是有分组的概念的同一个prompt生成的多个response都是同一个组的因此损失函数的公式有有G以及O有下标i——表示这个token是那个分组的(注意也有t这个t表示的是一个response中的具体位置的token)b、优势Ait的含义也是不同的这个是组内的相对优势(相对组内其他序列而言比组内序列的平均要好多少——而PPO则是想到critic模型当前状态的生成要好多少)同样有下标i和t——这里也是值得注意的这个Ai虽然有下标t但是并不是代表没给token的优势不同它们是一样的通过上图公式3可以看出A是通过分组的序列计算的因此同一个序列的所有token优势是相同的之所以有个下标t感觉是为了loss函数的公式形式上统一更好区分和PPO的不同。其他的地方不用怎么解读了就是需要注意A的计算方式公式3中给出明确的含义和计算方式其中的r就是我们的reward_function给出来的。3、训练流程简单总结一下PPO和GRPO的对比图解大模型RLHF系列之人人都能看懂的PPO原理与源码解读Direct Preference Optimization: Your Language Model is Secretly a Reward Model简单例子说明 DPO 为什么可能表现不好DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language ModelsDeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning