DeepSeek-R1 技术报告

摘自:https://zhuanlan.zhihu.com/p/19744278380

Github: https://github.com/deepseek-ai/DeepSeek-R1

DeepSeek-R1:通过强化学习提升LLM的推理能力

DeepSeek-R1-Zero 采用大规模强化学习(RL)进行训练,无需预先进行监督微调(SFT),表现出显著的推理能力。在强化学习过程中,DeepSeek-R1-Zero 展现出多种卓越且新颖的推理特性。但该模型仍面临可读性不足语言混杂等问题。

为解决这些问题并进一步增强推理性能,研究团队开发了 DeepSeek-R1,该模型在进行强化学习前引入了多阶段训练和冷启动数据。

DeepSeek-R1 在推理任务上实现了与 OpenAI-o1-1217 相当的性能水平

为促进学术研究发展,研究团队开源了 DeepSeek-R1-Zero、DeepSeek-R1,以及基于 Qwen 和 Llama 架构从 DeepSeek-R1 知识蒸馏获得的六个稠密模型(1.5B、7B、8B、14B、32B、70B)。

引言

近年来,LLM技术发展迅速,不断缩小与AGI的差距。后训练技术已成为完整训练流程中的关键环节,证实能够提升推理任务准确率,实现社会价值观对齐,适应用户偏好,同时相较于预训练所需计算资源较少。在推理能力方面,OpenAI的o1系列模型首次通过延长Chain-of-Thought(CoT)推理过程引入了推理时扩展机制,在数学、编程和科学推理等多个推理任务中取得显著进展。

然而,如何实现有效的测试时扩展仍是学术界面临的重要课题。前期研究探索了多种方法,包括过程型奖励模型、强化学习以及蒙特卡洛树搜索和束搜索等算法。但这些方法均未能达到与OpenAI的o1系列模型相当的通用推理水平。

本研究采用纯RL方法提升语言模型的推理能力。研究旨在探索LLM在无监督数据条件下通过纯RL过程实现自我进化的推理能力潜力。

具体而言,研究选用DeepSeek-V3-Base作为基础模型,采用群组相对策略优化(GRPO)作为RL框架提升模型推理性能。在训练过程中,DeepSeek-R1-Zero自然形成了多种高效且创新的推理特征。经过数千轮RL迭代,DeepSeek-R1-Zero在推理基准测试中展现出优异性能。例如,在AIME 2024测试中,pass@1得分从15.6%提升至71.0%,采用majority voting机制后,得分进一步提高到86.7%,达到OpenAI-o1-0912的性能水平。

然而,DeepSeek-R1-Zero仍面临可读性不足、语言混杂等挑战。

为解决这些问题并进一步提升推理性能,研究团队开发了DeepSeek-R1模型,该模型整合了初始训练数据和多阶段训练流程。具体实施步骤包括:首先收集数千条初始训练数据用于DeepSeek-V3-Base模型的微调;随后进行推理强化学习训练;在RL过程接近收敛时,通过拒绝采样(rejection sampling)方法从RL检查点生成新的SFT数据,并结合DeepSeek-V3在写作、事实QA和自我认知等领域的监督数据重新训练DeepSeek-V3-Base模型;最后,使用新数据完成微调后的检查点进行额外的RL训练,综合考虑各类场景的提示词。

经过上述步骤,最终获得的DeepSeek-R1模型达到了与OpenAI-o1-1217相当的性能水平。

研究进一步探索了从DeepSeek-R1到较小dense模型的知识蒸馏。以Qwen2.5 32B为基础模型,直接从DeepSeek-R1进行知识蒸馏的效果优于直接应用RL训练,表明大型基础模型所发现的推理模式对提升推理能力具有关键作用。研究团队已开源蒸馏后的Qwen和Llama系列模型。

值得注意的是,14B蒸馏模型的性能显著超越了当前最先进的开源模型QwQ-32B-Preview,而32B和70B蒸馏模型则在稠密模型推理基准测试中创造了新的记录

主要贡献

后训练:基础模型的大规模强化学习应用

  • 本研究直接将RL应用于基础模型,无需将SFT作为前置步骤。这种方法使模型能够通过CoT探索复杂问题的解决方案,最终开发出DeepSeek-R1-Zero模型。DeepSeek-R1-Zero具备自我验证、反思和生成长CoT等能力,为学术界提供了重要研究成果。这是首个验证LLM推理能力可纯粹通过RL提升而无需SFT的开放研究,为该领域未来发展奠定基础
  • 研究提出了DeepSeek-R1的开发流程,包含两个RL阶段用于优化推理模式和人类偏好对齐,以及两个SFT阶段用于构建模型的推理和非推理基础能力。该流程将有助于行业开发更高性能的模型。

知识蒸馏:小型模型的性能提升

  • 研究表明大型模型的推理模式可通过知识蒸馏迁移至小型模型,其效果优于直接对小型模型进行RL训练。开源的DeepSeek-R1及其API将支持学术界开发更优秀的小型模型
  • 利用DeepSeek-R1生成的推理数据,研究团队对学术界广泛使用的多个稠密模型进行了微调。评估结果显示,经过知识蒸馏的小型dense模型在基准测试中表现优异。DeepSeek-R1-Distill-Qwen-7B在AIME 2024上达到55.5%的性能,超越QwQ-32B-Preview。DeepSeek-R1-Distill-Qwen-32B在AIME 2024、MATH-500和LiveCodeBench上分别达到72.6%、94.3%和57.2%的成绩,显著优于现有开源模型,达到与o1-mini相当的水平。研究团队已向学术界开源基于Qwen2.5和Llama3系列的1.5B、7B、8B、14B、32B和70B蒸馏检查点

研究方法

概述

传统研究主要依赖大规模监督数据提升模型性能。本研究证实,即使在无需监督微调(SFT)作为初始训练的情况下,通过大规模强化学习(RL)也能显著提升推理能力。此外,引入适量初始训练数据可进一步优化性能。后续章节将介绍:(1)DeepSeek-R1-Zero:直接对基础模型应用RL,无需任何SFT数据;(2)DeepSeek-R1基于经数千个长CoT样例微调的检查点进行RL训练;(3)将DeepSeek-R1的推理能力通过知识蒸馏迁移至小型稠密模型

DeepSeek-R1-Zero:基础模型的强化学习应用

前期相关研究表明强化学习在推理任务中具有显著效果。然而,这些研究高度依赖耗时的监督数据采集。本节探索LLM在无监督数据条件下通过纯强化学习实现推理能力自我进化的潜力。研究首先概述强化学习算法,随后展示实验结果,以期为学术界提供研究参考。

强化学习算法

群组相对策略优化(GRPO): 为优化RL训练成本,研究采用GRPO算法,摒弃了通常与策略模型规模相当的评论家模型,转而通过群组评分估计基线。具体而言,对每个问题 q ,GRPO从旧策略 πθold 采样输出组{ o1,o2,…,oG },通过最大化以下目标优化策略模型 πθ :

其中 ε 和 β 是超参数, Ai 是优势函数,使用组内每个输出对应的奖励组{ r1,r2,…,rG }计算得到:

奖励建模

奖励机制作为训练信号来源,决定RL的优化方向。DeepSeek-R1-Zero采用基于规则的双重奖励系统

  • 准确性奖励:评估响应正确性。如对确定性数学问题,要求模型以特定格式(如方框内)提供最终答案,实现基于规则的可靠验证。对LeetCode问题,则通过编译器基于预设测试用例生成反馈。
  • 格式奖励:要求模型将推理过程置于指定标签对内。研究未采用结果或过程神经奖励模型,原因在于神经奖励模型可能在大规模RL过程中产生奖励欺骗问题,且重训奖励模型需额外资源,增加训练流程复杂度。

训练模板

DeepSeek-R1-Zero的训练始于简洁指令模板的设计。

如表1所示,模板要求模型首先生成推理过程,随后给出最终答案。研究刻意将约束限定于结构格式,避免引入内容偏见(如强制反思推理或特定问题解决策略),以准确观测模型在RL过程中的自然演化。

DeepSeek-R1-Zero的性能分析、演化过程及关键突破

性能分析 图2记录了DeepSeek-R1-Zero在RL训练过程中AIME 2024基准测试的性能变化轨迹。

图2 | DeepSeek-R1-Zero训练过程中的AIME准确率变化。为确保评估稳定性,对每个问题采样16个响应并计算总体平均准确率。

数据显示,随着RL训练的深入,模型性能呈现稳定上升趋势。在AIME 2024测试中,平均pass@1得分从初始的15.6%显著提升至71.0%,达到OpenAI-o1-0912的性能水平,充分证实了RL算法在模型性能优化方面的有效性。

表2 | DeepSeek-R1-Zero与OpenAI o1模型在推理相关基准测试上的性能对比。

表2详细对比了DeepSeek-R1-Zero与OpenAI o1-0912模型在各类推理基准测试上的表现。结果表明,纯RL训练使DeepSeek-R1-Zero获得了出色的推理能力,无需借助监督微调数据,这证实了模型通过单一RL机制实现有效学习和泛化的能力。通过引入majority voting机制,模型性能得到进一步提升。例如,在AIME基准测试中,采用majority voting后性能从71.0%提升至86.7%,超越OpenAI-o1-0912。这种优异表现凸显了模型的基础能力和推理潜力。

演化过程分析 DeepSeek-R1-Zero的演化过程展示了RL在推理能力自主优化方面的显著效果。通过直接对基础模型实施RL训练,研究得以在无监督微调影响下观测模型进展。

图3 | 展示DeepSeek-R1-Zero在RL训练过程中训练集的平均响应长度变化,反映模型自主习得延长推理时间的能力。

如图3所示,模型的推理时长在训练过程中持续优化,这种进展源于模型的内生发展而非外部干预。DeepSeek-R1-Zero通过扩展测试计算时间,自然形成了解决复杂推理任务的能力。其计算规模从数百到数千个推理token不等,实现了深度的思维探索和优化。随着测试计算时间的延长,模型展现出复杂的行为特征,包括反思机制(重新评估先前推理步骤)和多元问题解决策略的探索。这些行为模式并非预设,而是源于模型与RL环境的交互作用,显著增强了其处理高难度任务的效率和准确性。

关键突破与局限性 研究过程中观察到模型出现重要突破,如表3所示,体现在中期版本中。

表3:记录DeepSeek-R1-Zero中期版本的重要突破,展示模型获得自主思考复核能力的过程,体现RL在模型能力提升方面的有效性。

此阶段,DeepSeek-R1-Zero习得了重新评估初始方法并延长思考时间的能力。这一进展不仅体现了模型推理能力的提升,也展示了RL在实现复杂学习成果方面的潜力。这种现象验证了RL的核心优势:通过适当的激励机制,促使模型自主发展高级问题解决策略。

然而,DeepSeek-R1-Zero仍存在若干局限性。尽管具备强大的推理能力和创新的推理行为,但在可读性和语言一致性方面仍面临挑战。为提高推理过程的可读性并促进开放社区交流,研究团队开发了DeepSeek-R1模型,该模型结合了RL和用户友好的初始训练数据。

DeepSeek-R1:基于冷启动的强化学习方法

基于DeepSeek-R1-Zero的成功实践,研究聚焦两个核心问题:

  1. 通过引入少量高质量数据作为冷启动,是否能够进一步提升推理性能或加速收敛?
  2. 如何开发既能生成清晰连贯的CoT,又具备强大通用能力的用户友好型模型?

为解决上述问题,研究团队设计了四阶段训练流程

冷启动机制

区别于DeepSeek-R1-Zero,DeepSeek-R1采用少量长CoT数据对模型进行预微调作为初始RL策略网络,以避免基础模型RL训练早期的不稳定性。数据收集采用多种方法:

  • 基于长CoT示例的少样本提示
  • 直接提示生成包含反思验证的详细答案
  • 整理DeepSeek-R1-Zero的规范化输出
  • 人工标注后处理优化

研究收集数千条冷启动数据用于DeepSeek-V3-Base的预训练。相较于DeepSeek-R1-Zero,冷启动数据具有以下优势:

  • 可读性增强:克服了DeepSeek-R1-Zero输出内容可读性差的局限。通过设计标准化输出模式,包括响应末尾的总结性内容,并筛除不符合阅读友好性要求的输出。输出采用|special_token|<reasoning_process>|special_token|<summary>格式,包含查询的推理过程和结果摘要。
  • 性能提升:基于人类认知模式优化的冷启动数据设计,展现出优于DeepSeek-R1-Zero的性能表现,验证了迭代训练对推理模型的优越性。

推理强化学习优化

完成冷启动数据预训练后,采用与DeepSeek-R1-Zero类似的大规模RL训练流程,重点提升模型在编码、数学、科学和逻辑等明确定义问题域的推理能力。在训练过程中发现Chain-of-Thought存在语言混杂现象,尤其是多语言提示场景下。为此引入语言一致性奖励机制,基于目标语言词占比计算。尽管消融实验显示该机制略微影响模型性能,但提升了人类使用体验。最终将任务准确率和语言一致性奖励合并计算总体奖励,持续RL训练直至模型在推理任务上收敛。

拒绝采样与监督微调

推理RL收敛后,利用检查点生成后续SFT数据。不同于专注推理的冷启动阶段,此阶段整合多领域数据以增强模型的写作、角色扮演等通用能力。具体实施如下:

推理数据构建 通过对RL训练检查点执行拒绝采样生成推理轨迹。扩展了评估机制,除规则型奖励外,引入基于DeepSeek-V3判断的生成式奖励模型。优化输出质量,过滤混杂语言、冗长段落和代码块。对每个提示词进行多样本采样,保留正确结果。最终获得约60万条推理训练样本。

非推理数据整合 在写作、事实QA、自我认知和翻译等领域,采用DeepSeek-V3流程和部分SFT数据。对复杂非推理任务,通过提示DeepSeek-V3生成前置CoT;对简单查询则直接响应。累计获取约20万条非推理训练样本。使用总计约80万样本数据对DeepSeek-V3-Base执行两轮微调。

全场景强化学习

优化人类偏好对齐,实施第二阶段RL训练,着重提升模型实用性、安全性和推理能力。采用多元奖励信号和多样化提示分布:

  • 推理数据:延续DeepSeek-R1-Zero方法,在数理逻辑领域应用规则型奖励
  • 通用数据:采用奖励模型捕捉复杂场景下的人类偏好
  • 实用性评估:专注于响应摘要,确保输出的实用性和相关性
  • 安全性保障:全面评估推理过程和摘要,识别并降低潜在风险

通过奖励信号和数据分布的系统整合,实现了推理能力和用户体验的均衡发展。

知识蒸馏:增强小型模型的推理能力

本研究采用DeepSeek-R1生成的80万训练样本,对Qwen和Llama等开源模型进行直接SFT微调,旨在将DeekSeek-R1的推理能力迁移至计算效率更高的小型模型。

实验结果表明,这种直接知识蒸馏方法显著提升小型模型的推理性能

研究选用的基础模型包括:Qwen2.5-Math-1.5B、Qwen2.5-Math-7B、Qwen2.5-14B、Qwen2.5-32B、Llama-3.1-8BLlama-3.3-70B-Instruct

选择Llama-3.3的原因在于其推理能力较Llama-3.1略有优势

蒸馏过程中仅采用SFT,未纳入RL阶段,尽管引入RL可能带来显著的性能提升。研究重点在于验证知识蒸馏技术的有效性,为后续学术界对RL优化的深入研究奠定基础。

实验设计与评估

研究采用多维度基准测试体系评估模型性能:

标准评估基准 8类16个评估标准如下所示:

  • 知识理解类:MMLU、MMLU-Redux、MMLU-Pro
  • 跨语言评估:C-Eval、CMMLU
  • 格式理解:IFEval
  • 长文本处理:FRAMES
  • 专业知识:GPQA Diamond
  • 事实问答:SimpleQA、C-SimpleQA
  • 编程能力评估: SWE-Bench Verified、Aider、LiveCodeBench、Codeforces
  • 数学能力测试: CNMO 2024、AIME 2024

除标准基准测试外,研究还使用LLM作为评估器评估模型在开放式生成任务上的表现。具体而言,遵循AlpacaEval 2.0Arena-Hard的原始配置,使用GPT-4-Turbo-1106作为成对比较的评估器。评估时仅输入最终摘要以避免长度偏差。对于蒸馏模型,报告其在AIME 2024、MATH-500、GPQA Diamond、Codeforces和LiveCodeBench上的代表性结果。

评估用prompt 不同的评估标准采用不同的prompt,具体如下所示:

  • 基础评估:采用simple evals框架标准prompt评估MMLU、DROP、GPQA Diamond和SimpleQA
  • 特殊处理: MMLU-Redux采用Zero-Eval prompt格式实现零样本评估,MMLU-Pro、C-Eval、CLUE-WSC将原少样本prompt改造为零样本形式
  • 编程评估: HumanEval-Mul覆盖8种主流编程语言,LiveCodeBench采用CoT格式,Codeforces基于10个Div.2竞赛题目与专家测试用例,SWE-Bench通过无代理框架验证

值得注意的是,DeepSeek-R1的输出在每个基准测试上限制为最多32,768个token。

基准模型 研究与多个强基准模型进行全面对比,包括DeepSeek-V3、Claude-Sonnet-3.5-1022、GPT-4o-0513、OpenAI-o1-miniOpenAI-o1-1217。鉴于在中国大陆访问OpenAI-o1-1217 API的限制,其性能数据来源于官方报告。对于蒸馏模型,额外与开源模型QwQ-32B-Preview进行比较。

生成配置 所有模型的最大生成长度设置为32K token。对需要采样的基准测试,采用0.6的temperature参数、0.95的top-p值,并为每个查询生成64个响应以估算pass@1。

DeepSeek-R1评估结果

表4 | DeepSeek-R1与其他代表性模型的比较。

在面向教育的知识基准测试(如MMLU、MMLU-Pro和GPQA Diamond)中,DeepSeek-R1相较于DeepSeek-V3展现出优越性能。这一进步主要归因于STEM相关问题准确率的提升,这得益于大规模RL带来的显著进步。

此外,DeepSeek-R1在依赖长文本理解的问答任务FRAMES上表现卓越,展示了其强大的文档分析能力。这凸显了推理模型在AI驱动的搜索和数据分析任务中的潜力

在事实性基准测试SimpleQA上,DeepSeek-R1的表现优于DeepSeek-V3,证明了其处理基于事实查询的能力。类似地,在该基准测试中也观察到OpenAI-o1超越GPT-4o的趋势。

然而,DeepSeek-R1在中文SimpleQA基准测试中的表现不如DeepSeek-V3,主要是由于安全性RL后倾向于拒绝回答某些查询。若不考虑安全性RL,DeepSeek-R1可以达到超过70%的准确率。

DeepSeek-R1在IF-Eval(一个用于评估模型遵循格式指令能力的基准测试)上也取得了令人瞩目的成果。这些改进可归因于在最终阶段的SFT和RL训练中引入了指令遵循数据。

此外,在AlpacaEval 2.0和ArenaHard上的出色表现表明DeepSeek-R1在写作任务和开放域问答方面具有优势。其显著优于DeepSeek-V3的表现凸显了大规模RL的泛化效益,不仅提升了推理能力,还改善了各个领域的性能。

而且DeepSeek-R1生成的摘要长度简洁,在ArenaHard上平均为689个token,在AlpacaEval 2.0上平均为2,218个字符。这表明DeepSeek-R1在基于GPT的评估中避免了引入长度偏差,进一步证实了其在多任务场景下的稳健性。

数学任务上,DeepSeek-R1展现出与OpenAI-o1-1217相当的性能,大幅超越其他模型。在LiveCodeBench和Codeforces等编码算法任务上也观察到类似趋势,其中注重推理的模型在这些基准测试中占据主导地位。

在面向工程的编码任务上,OpenAI-o1-1217在Aider上优于DeepSeek-R1,但在SWE Verified上表现相当。考虑到目前相关RL训练数据量仍然非常有限,研究团队认为DeepSeek-R1的工程性能将在下一版本中得到改善。

蒸馏模型评估

表5 | DeepSeek-R1蒸馏模型与其他可比模型在推理相关基准测试上的比较。

如表5所示,仅通过蒸馏DeepSeek-R1的输出,高效的DeepSeek-R1-7B(即DeepSeek-R1-Distill-Qwen-7B,以下类似缩写)就能在各方面超越GPT-4o-0513等非推理模型。

DeepSeek-R1-14B在所有评估指标上超越QwQ-32B-Preview,而DeepSeek-R1-32B和DeepSeek-R1-70B在大多数基准测试中显著超越o1-mini。这些结果展示了知识蒸馏的巨大潜力

此外,研究发现对这些蒸馏模型应用RL能带来显著的进一步提升。考虑到这值得进一步探索,此处仅呈现简单SFT蒸馏模型的结果。

讨论

蒸馏与强化学习对比

通过蒸馏DeepSeek-R1,小型模型能够取得出色的结果。然而,仍有一个问题待解答:

模型是否可以通过本文讨论的大规模RL训练而不依赖蒸馏来达到相当的性能?

为回答这个问题,研究团队对Qwen-32B-Base使用数学、代码和STEM数据进行了超过10K步的大规模RL训练,得到DeepSeek-R1-Zero-Qwen-32B。

如表6所示的实验结果表明,32B基础模型经过大规模RL训练后,达到了与QwQ-32B-Preview相当的性能。然而,从DeepSeek-R1蒸馏得到的DeepSeek-R1-Distill-Qwen-32B在所有基准测试中的表现都显著优于DeepSeek-R1-Zero-Qwen-32B

因此,可以得出两个结论:

首先,将更强大的模型蒸馏到较小的模型中可以产生优异的结果,而较小的模型依靠本文提到的大规模RL需要巨大的计算力,甚至可能无法达到蒸馏的性能水平

其次,虽然蒸馏策略既经济又有效,但要突破智能的边界可能仍需要更强大的基础模型和更大规模的强化学习

未成功的尝试

在开发DeepSeek-R1的早期阶段,研究也遇到了失败和挫折。在此分享这些失败经验以提供见解,但这并不意味着这些方法无法开发出有效的推理模型。

过程奖励模型(PRM)

PRM是一种合理的方法,可以引导模型采用更好的方法解决推理任务。然而,在实践中,PRM有三个主要限制可能阻碍其最终成功。

首先,在一般推理中明确定义细粒度步骤具有挑战性。其次,确定当前中间步骤是否正确是一项具有挑战性的任务。使用模型的自动标注可能无法产生令人满意的结果,而手动标注不利于规模化。第三,一旦引入基于模型的PRM,必然导致奖励欺骗,重新训练奖励模型需要额外的训练资源,并使整个训练流程变得复杂。

总之,虽然PRM在对模型生成的前N个响应重新排序或辅助引导搜索方面表现良好,但在实验中,相比其在大规模强化学习过程中引入的额外计算开销,其优势有限

蒙特卡洛树搜索(MCTS)

AlphaGoAlphaZero的启发,研究探索使用MCTS来增强测试时计算的可扩展性。这种方法包括将答案分解为更小的部分,使模型能够系统地探索解决方案空间。为此,提示模型生成多个标签,对应搜索所需的具体推理步骤。在训练方面,首先使用收集的提示通过预训练值模型引导的MCTS寻找答案。随后,使用产生的问答对来训练actor模型和值模型,不断改进过程。

然而,这种方法在扩大训练规模时遇到几个挑战。首先,与搜索空间相对明确的象棋不同,token生成呈现指数级更大的搜索空间。为解决这个问题,为每个节点设置最大扩展限制,但这可能导致模型陷入局部最优。其次,值模型直接影响生成质量,因为它指导搜索过程的每个步骤。训练细粒度值模型本质上是困难的,这使得模型难以迭代改进。虽然AlphaGo的核心成功依赖于训练值模型来逐步提升性能,但由于token生成的复杂性,这一原则在团队的设置中难以复制。

总之,虽然MCTS在与预训练值模型配对时可以改善推理性能,但通过自搜索迭代提升模型性能仍然是一个重大挑战

结论、局限性和未来工作

本文分享了通过RL增强模型推理能力的探索历程。DeepSeek-R1-Zero代表了一种不依赖冷启动数据的纯RL方法,在各种任务中取得了出色的表现。DeepSeek-R1通过结合冷启动数据和迭代RL微调展现出更强的性能,最终在多个任务上达到与OpenAI-o1-1217相当的水平。

研究进一步探索了将推理能力蒸馏到小型稠密模型的可能性。以DeepSeek-R1作为教师模型生成80万条数据,并对多个小型稠密模型进行微调。

结果令人鼓舞:DeepSeek-R1-Distill-Qwen-1.5B在数学基准测试中超越GPT-4o和Claude-3.5-Sonnet,在AIME上达到28.9%,在MATH上达到83.9%的成绩。其他稠密模型也取得了显著成果,大幅超越基于相同基础检查点的其他指令微调模型。

未来,计划在以下方向继续推进DeepSeek-R1的研究:

  • 通用能力:目前DeepSeek-R1在函数调用、多轮对话、复杂角色扮演和json输出等任务上的能力仍不及DeepSeek-V3。后续研究将探索如何利用长CoT增强这些领域的任务表现。
  • 语言混杂:DeepSeek-R1当前针对中文和英文进行了优化,在处理其他语言的查询时可能出现语言混杂问题。例如,即使查询使用非英文或中文的语言,DeepSeek-R1可能使用英语进行推理和响应。未来更新将着力解决这一限制。
  • 提示词工程:在评估DeepSeek-R1时发现,模型对prompt较为敏感。少样本提示会持续降低其性能。因此,建议用户直接描述问题并使用零样本设置指定输出格式以获得最佳结果
  • 软件工程任务:由于评估时间较长影响RL过程效率,大规模RL尚未在软件工程任务中广泛应用。因此,DeepSeek-R1在软件工程基准测试上相比DeepSeek-V3未显示出显著改进。未来版本将通过对软件工程数据实施拒绝采样或在RL过程中引入异步评估来提高效率。

LLM训练-对齐算法综述:RLHF, RLAIF, PPO, DPO and More

参考论文:A Comprehensive Survey of LLM Alignment Techniques: RLHF, RLAIF, PPO, DPO and More

一个完整的LLM训练过程包含以下几步:

  • Model Initialization:加载模型和处理器
  • 数据准备:解析数据集并设置其格式
  • 模型推理:将数据输入到模型中并获取输出
  • 梯度更新:根据损失函数更新模型参数

对齐(alignment)其作用就是让 LLM 与人类的价值观保持一致。在对齐 LLM 方面,基于人类反馈的强化学习(RLHF)是一种突破性的技术。该方法催生了 GPT-4、Claude 和 Gemini 等强大模型。RLHF 之后,人们也探索了多种多样的对齐 LLM 的方法。但是,此前还没有人全面总结对齐 LLM 与人类偏好的方法。

Salesforce 决定填补这一空白,于近日发布了一份 37 页的综述报告,其中按类别总结了现有的研究文献,并详细分析了各篇论文。

Introduction

这篇论文分为四大主题:奖励模型、反馈、强化学习(RL)、优化。每个主题又包含进一步的子主题,如图 1 所示。

xPO LLM 与人类偏好保持一致的 13 个分类方向

奖励模型的子主题包括:1. 显式奖励模型与隐式奖励模型;2. 逐点奖励模型与偏好模型;3. 响应层面的奖励与 token 层面的奖励;4. 负偏好优化。

反馈的子主题包括:1. 偏好反馈与二元反馈;2. 成对反馈与列表反馈;3. 人类反馈与 AI 反馈。

强化学习的子主题包括:1. 基于参考的强化学习与无参考的强化学习;2. 长度控制式强化学习;3. 强化学习中的不同分支;4. 在线策略强化学习与离线策略强化学习

优化的子主题包括:1. 在线 / 迭代式偏好优化与离线 / 非迭代式偏好优化;2. 分离 SFT 和对齐与合并 SFT 和对齐。

Individual Paper Reviews in Detail

1. RLHF/PPO

LLM 的预训练要用到大量来自不同来源的语料库,而这本身就无法确保这些数据集的质量。此外,LLM 的主要目标是预测下一个 token,这个目标与「有用且安全地遵从用户指令」的目标并不一致。因此,LLM 可能会输出不真实、有害或对用户无用的内容。本质上讲,这些模型并未与用户意图对齐。RLHF/PPO 的主要目标是在各种任务上对齐语言模型与用户意图,其做法是使用人类反馈来微调模型。有关这个主题的研究有很多。

2. RLAIF

获取人类偏好数据集的成本不低,因此基于人工智能反馈的强化学习(RLAIF)诞生了。此外,随着 LLM 的能力不断进步,所能收集到的 AI 偏好数据集的质量也不断提高,由此可提升 LLM 的对齐效果。

3.直接人类偏好优化

传统 RLHF 方法通常涉及到优化源自人类偏好的奖励函数。该方法虽有效,但也可能带来一些难题,比如增大计算复杂度以及在估计和优化奖励时需要考虑偏置 – 方差权衡。参阅论文《High-dimensional continuous control using generalized advantage estimation》。

近期有研究探索了其它一些旨在根据人类偏好(无需依赖某个标量的奖励信号)来直接优化 LLM 策略的方法。

这些方法的目标是通过更直接地使用偏好数据来简化对齐流程、降低计算开销以及实现更稳健的优化。通过将该问题描述为一个偏好优化问题,而不是奖励估计和最大化问题,这些方法能提供一种将语言模型与人类判断对齐的不同视角

4.token 级 DPO

使用 DPO 时,奖励会被一起分配给 prompt 和响应。相反,使用 MDP 时,奖励会被分配给各个动作。后续的两篇论文在 token 层面阐述了 DPO 并将其应用扩展到了 token 级的分析。

5.迭代式 / 在线 DPO

使用 DPO 时,会使用所有可用的偏好数据集来对齐 LLM。为了持续提升 LLM,应当实现迭代式 / 在线 DPO。这就引出了一个有趣的问题:如何高效地收集新的偏好数据集。下面两篇论文深入探讨了这一主题。

  • 自我奖励式语言模型,参阅论文《Self-rewarding language models》。
  • CRINGE,参阅论文《The cringe loss: Learning what language not to model》。

6.二元反馈

事实证明,收集偏好反馈比收集二元反馈(比如点赞或点踩)的难度大,因此后者可促进对齐过程的扩展。KTO 和 DRO 这两项研究关注的便是使用二元反馈来对齐 LLM。

  • KTO,Kahneman-Tversky 优化,参阅论文《KTO: Model alignment as prospect theoretic optimization》。
  • DRO,直接奖励优化,参阅论文《Offline regularised reinforcement learning for large language models alignment》。

7.融合 SFT 和对齐

之前的研究主要还是按顺序执行 SFT 和对齐,但事实证明这种方法很费力,并会导致灾难性遗忘。后续的研究有两个方向:一是将这两个过程整合成单一步骤;二是并行地微调两个模型,最终再进行融合。

  • ORPO,比值比偏好优化,参阅论文《ORPO: Monolithic preference optimization without reference model》。
  • PAFT,并行微调,参阅论文《PAFT: A parallel training paradigm for effective llm fine-tuning》。

8.长度控制式 DPO 和无参考 DPO

之前有研究表明,LLM 的输出往往过于冗长。为了解决这个问题,R-DPO 和 SimPO 的关注重心是在不影响生成性能的前提下实现对响应长度的控制。

此外,DPO 必需参考策略来确保已对齐模型不会与参考模型有太大偏差。相较之下,SimPO 和 RLOO 提出了一些方法,可以在不影响 LLM 效果的情况下消除对参考模型的需求

9.逐列表的偏好优化

之前在 PPO 和 DPO 方面的研究关注的是成对偏好,而 RLHF 方面的研究则是收集逐列表的偏好来加速数据收集过程,之后再将它们转换成成对偏好。尽管如此,为了提升 LLM 的性能,直接使用逐列表的数据集来执行偏好优化是可行的。以下三篇论文专门讨论了这种方法。

  • LiPO,逐列表偏好优化,参阅论文《LIPO: Listwise preference optimization through learning-to-rank》。
  • RRHF,参阅论文《RRHF: Rank responses to align language models with human feedback without tears》。
  • PRO,偏好排名优化,参阅论文《Preference ranking optimization for human alignment》。

10.负偏好优化

这些研究有一个共同前提:当前这一代 LLM 已经在翻译和总结等任务上超越了人类性能。因此,可以将 LLM 的输出视为期望响应,而无需依靠将人类标注的数据视为偏好响应;这样做是有好处的。反过来,不期望得到的响应依然也可被用于对齐 LLM,这个过程就是所谓的负偏好优化(NPO)。

  • NN,否定负例方法,参阅论文《Negating negatives: Alignment without human positive samples via distributional dispreference optimization》。
  • NPO,负例偏好优化,参阅论文《Negative preference optimization: From catastrophic collapse to effective unlearning》。
  • CPO,对比偏好优化,参阅论文《Contrastive preference optimization: Pushing the boundaries of llm performance in machine translation》。

11.纳什学习

之前的研究通常是使用逐点奖励和 BT 模型来得到成对偏好。但是,这种方法比不上直接成对偏好建模并且无法解决成对偏好中的不一致问题。为了克服这些局限,一些研究提出了纳什学习方法。

  • 根据人类反馈的纳什学习,参阅论文《Nash learning from human feedback》。
  • SPPO,自博弈偏好优化,参阅论文《A minimaximalist approach to reinforcement learning from human feedback》。
  • DNO,直接纳什优化,参阅论文《Direct nash optimization: Teaching language models to self-improve with general preferences》。

LLM 对齐(Alignment)方法:SFT、PPO、DPO 详细介绍

LLM(大语言模型)的对齐(Alignment)方法旨在让 AI 的输出更加符合人类预期,减少错误信息、有害内容或不准确的回答。主要总结LLM训练中的基本的对齐算法, 监督微调 (SFT)、直接偏好优化 (DPO) 和近端策略优化 (PPO)。

SFT(Supervised Fine-Tuning,监督微调)

监督微调(SFT)是 LLM 训练中的第一步,通过高质量的人工标注数据集对模型进行微调,使其具备基础的任务能力。SFT 是所有对齐方法的基础,如 RLHF、DPO 等都依赖于一个经过 SFT 训练的模型作为初始状态。

过程

  1. 数据准备:收集高质量的指令-响应(Instruction-Response)数据集,例如人类标注的数据或合成的数据。
  2. 模型微调:使用交叉熵损失(Cross-Entropy Loss)训练模型,使其学习提供与标注数据匹配的答案。
  3. 效果:使模型在常见任务(如问答、代码生成、对话等)中表现更好,提高其对指令的遵循能力。

给定输入 x(Prompt) 和目标输出 y(Response),模型的目标是最大化生成目标文本的概率:

其中:

  • Pθ​(yt​∣x,y<t​) 是 LLM 在给定上下文下预测下一个 token yt​ 的概率。
  • 训练时采用交叉熵损失(Cross Entropy Loss)来优化模型参数 θ。

SFT 仅依赖于人工标注数据,无法让模型学习偏好信息(比如不同回答的优劣)。无法动态调整:SFT 训练后,模型固定,难以针对用户反馈进行调整。缺乏探索性:模型只会学到训练数据中的模式,无法进行强化学习优化。

DPO(Direct Preference Optimization,直接偏好优化)

DPO(直接偏好优化)是一种比 RLHF 更简单、更高效的对齐方法。
它不需要训练奖励模型(RM)和使用强化学习(RL),而是直接优化 LLM,使其更符合人类偏好数据

准备偏好数据

  • 每个输入 Prompt 对应两个候选回答:一个优选(Preferred y+),一个劣选(Dispreferred y−)。
  • 例如:

Prompt: “如何写一封正式的电子邮件?”
Response 1 (优选): “在邮件中应保持正式语气,并包含称呼、正文和署名。”
Response 2 (劣选): “随便写就行了,不要太在意格式。”

优化 LLM 使其更倾向于优选回答

  • 计算优选回答的概率Pθ​(y+∣x) 和劣选回答的概率 Pθ​(y−∣x)。
  • 目标是优化模型,使得: Pθ(y+∣x)>Pθ(y−∣x)

损失函数: DPO 直接优化模型输出的偏好分布:LDPO=−log⁡σ(β(log⁡Pθ(y+∣x)−log⁡Pθ(y−∣x)))

其中:

  • σ(⋅) 是 Sigmoid 函数,确保优化方向正确。
  • β 是一个温度参数,控制学习速率。

PPO(Proximal Policy Optimization,近端策略优化)

PPO 是一种强化学习(RL)方法,通常用于 RLHF(基于人类反馈的强化学习)其目标是通过奖励模型(Reward Model, RM)指导 LLM 生成更符合人类偏好的内容

人类偏好数据(Human Preference Data)
人工标注的问答对,并标注哪个回答更优。

奖励模型(Reward Model, RM)
训练一个模型来预测人类对答案的偏好,帮助 LLM 学习优化目标。

强化学习(PPO 训练)
使用 RL 算法(如 PPO)优化 LLM,使其生成更符合人类期望的回答。

PPO 训练流程

  1. 初始化策略模型(LLM)
    • 采用 SFT 训练后的模型 作为 RLHF 训练的起点。
  2. 生成候选回答
    • LLM 生成多个答案,并让奖励模型(RM)进行打分。
  3. 计算奖励
    • 奖励模型对 LLM 的回答 y 计算奖励分数 R(y)。
  4. PPO 更新策略:计算策略更新梯度,优化 LLM 以生成更高奖励的答案。采用截断重要性采样(Clipped Importance Sampling) 控制更新幅度,防止策略崩溃。

PPO 公式

PPO 通过策略梯度(Policy Gradient)优化 LLM 的生成策略:

其中:

  • rt(θ) 是新旧策略的比率。
  • E 是期望,PPO 损失函数的平均值。
  • At​ 是优势函数(Advantage Function),衡量当前策略比旧策略好多少。
  • ϵ是超参数(通常取 0.2),限制更新幅度,防止训练不稳定。

优点

  • 能动态优化 LLM,使其不断学习更符合人类偏好的输出。
  • 能够处理长文本生成任务,适用于 GPT-4、Claude 这类大模型的对齐。

缺点

  • 计算成本高:训练需要采样大量数据,计算复杂。
  • 依赖奖励模型(RM):如果 RM 质量不好,PPO 可能会强化错误模式。
  • 训练不稳定:需要 carefully 选择超参数,避免梯度爆炸或模型崩溃。

奖励模型 RM

该阶段是RHLF的第一个阶段,训练得到一个rm模型用于rl阶段的模型打分,数据可以由人工标注(如 OpenAI 的 RLHF 训练)或合成数据(如基于规则的自动标注)生成。

RM 通常基于预训练的大型语言模型(如 GPT、LLaMA),然后添加一个奖励预测头(Reward Head),用于输出一个标量奖励分数。

例如,对于一个文本回答 R,RM 计算出:Rθ​(x,y)=s

其中:

  • x 是输入文本
  • y 是模型的回答
  • s 是 RM 计算出的偏好分数

为了让 RM 进行有效的偏好学习,通常使用 成对排名损失(Pairwise Ranking Loss) 训练 RM:L=−log⁡(σ(Rθ(x,ypreferred)−Rθ(x,ydispreferred)))

其中:

  • Rθ(x,ypreferred 是偏好回答的得分
  • Rθ(x,ydispreferred)是不被偏好的回答的得分
  • σ 是 Sigmoid 函数

目标
最大化更优答案次优答案的分数差,使 RM 更准确地预测人类偏好。

对比:

代码实现:

基于pytorch、deepspeed、transformers,代码:https://github.com/PKU-Alignment/align-anything/tree/main/align_anything/trainers/text_to_text

sft训练代码:

def loss(self, sft_batch: SupervisedBatch) -> dict[str, torch.Tensor]:
    """Loss function for supervised finetuning."""
    outputs = self.model(**self.infer_batch(sft_batch))
    return {'loss': outputs.loss}

def train_step(self, sft_batch: SupervisedBatch) -> dict[str, Any]:
    """Performs a single training step."""
    loss = self.loss(sft_batch)['loss']
    self.model.backward(loss)
    self.model.step()

    return {
        'train/loss': loss.item(),
        'train/lr': self.model.optimizer.param_groups[0]['lr'],
    }

dpo训练代码:

def compute_log_probs(
    self,
    model: AutoModelForCausalLM,
    batch: PreferenceBatch,
) -> torch.Tensor:
    """Compute log probabilities of given sequences."""
    # 获得所有可能输出的log概率,logits 表示每个 token 位置的 未归一化的概率分布
    logits = model(**self.infer_batch(batch)).logits
    device = logits.device
    input_ids = batch['input_ids']
    #取得每个样本的回复长度,用于截取模型输出
    batch_size = len(batch['meta_info']['response_lens'])
    logprob_list = []
    for idx in range(batch_size):
        response_length = batch['meta_info']['response_lens'][idx]
        # 去除填充 (PAD) token,避免计算无效 token 的概率。
        raw_input_id = strip_pad(input_ids[idx], self.tokenizer.pad_token_id)
        #只保留 回复部分的 logits,丢弃 prompt 部分。 
        logit = logits[idx][-response_length:].unsqueeze(0)
        input_id = raw_input_id[-response_length:].unsqueeze(0)
        #计算对应的better 和worse 序列token 对数概率
        log_p = gather_log_probabilities(logit[:, :-1], input_id[:, 1:])
        logprob_list.append(log_p.squeeze(0))
    # 不同样本的 log_probs 长度不同,使用 pad_sequence 进行 padding,补齐到相同长度。
    return torch.nn.utils.rnn.pad_sequence(
        logprob_list, batch_first=True, padding_value=0.0
    ).to(device)

def loss(  # pylint: disable=too-many-locals
    self,
    batch: PreferenceBatch,
) -> dict[str, torch.Tensor]:
    """Loss function for the DPO algorithm."""
   #计算当前模型 (self.model.module) 在 batch 上的 log 概率。
    sequence_log_probs = self.compute_log_probs(
        self.model.module,
        batch,
    )
# better_sequence_log_probs (用户偏好的回复)
# worse_sequence_log_probs (用户不喜欢的回复)
    (
        better_sequence_log_probs,  # size = (B, L - 1)
        worse_sequence_log_probs,  # size = (B, L - 1)
    ) = sequence_log_probs.chunk(chunks=2, dim=0)
# 计算参考模型 (self.reference_model.module) 的对数概率 (log_probs)。
# reference_model 通常是 原始未优化的模型,作为对比基准。
# torch.no_grad() 表示 不计算梯度,避免影响参考模型。
    with torch.no_grad():
        ref_sequence_log_probs = self.compute_log_probs(  # size = (2 * B, L - 1)
            self.reference_model.module,
            batch,
        )
        ref_better_sequence_log_probs, ref_worse_sequence_log_probs = (
            ref_sequence_log_probs.chunk(chunks=2, dim=0)
        )

    losses = []
    better_sample_rewards = []
    worse_sample_rewards = []

    batch_size = better_sequence_log_probs.size(0)
    for i in range(batch_size):
# 计算 更好/更差回复的总 log 概率(即累加 token 级别 log 概率)。
        better_log_prob = better_sequence_log_probs[i, :].sum(dim=-1)
        worse_log_prob = worse_sequence_log_probs[i, :].sum(dim=-1)
        ref_better_log_prob = ref_better_sequence_log_probs[i, :].sum(dim=-1)
        ref_worse_log_prob = ref_worse_sequence_log_probs[i, :].sum(dim=-1)
# 当前模型比参考模型更偏好 better 回复 的程度。
        better_log_ratio = better_log_prob - ref_better_log_prob
# 当前模型比参考模型更偏好 worse 回复 的程度。
        worse_log_ratio = worse_log_prob - ref_worse_log_prob
# 计算 better 和 worse 的 log 比值差
# 使用 -logsigmoid(x) 计算负对数 sigmoid 损失,优化模型使其更倾向 better 回复。
# logsigmoid 的性质:
# 如果 x 很大,logsigmoid(x) ≈ 0,意味着损失小,模型已经正确偏好 better response。
# 如果 x 很小或负,logsigmoid(x) ≈ x,意味着损失大,模型没有正确区分 better 和 worse,需要优化。
        losses.append(
            -F.logsigmoid(
                self.cfgs.train_cfgs.scale_coeff * (better_log_ratio - worse_log_ratio),
            ),
        )
        better_sample_rewards.append(
            self.cfgs.train_cfgs.scale_coeff * better_log_ratio.detach(),
        )
        worse_sample_rewards.append(self.cfgs.train_cfgs.scale_coeff * worse_log_ratio.detach())
    loss = torch.stack(losses).mean()  # size = ()
    better_sample_reward = torch.stack(better_sample_rewards)  # size = (B,)
    worse_sample_reward = torch.stack(worse_sample_rewards)  # size = (B,)
# 计算 奖励 (reward)、准确率 (accuracy) 和奖励间距 (margin)。
    reward = better_sample_reward + worse_sample_reward  # size = (B,)
    reward_accuracy = (better_sample_reward > worse_sample_reward).float().mean()  # size = ()
    reward_margin = better_sample_reward - worse_sample_reward  # size = (B,)

    return {
        'loss': loss,
        'reward': reward,
        'better_sample_reward': better_sample_reward,
        'worse_sample_reward': worse_sample_reward,
        'reward_accuracy': reward_accuracy,
        'reward_margin': reward_margin,
    }

def train_step(
    self,
    batch: PreferenceBatch,
) -> dict[str, Any]:
    """Perform a single training step for DPO."""
    loss_dict = self.loss(batch=batch)
    loss = loss_dict['loss']
    self.model.backward(loss)
    self.model.step()

    with torch.no_grad():
        reward = loss_dict['reward'].mean()
        better_sample_reward = loss_dict['better_sample_reward'].mean()
        worse_sample_reward = loss_dict['worse_sample_reward'].mean()
        reward_accuracy = loss_dict['reward_accuracy']
        reward_margin = loss_dict['reward_margin'].mean()

        loss = get_all_reduce_mean(loss)
        reward = get_all_reduce_mean(reward)
        better_sample_reward = get_all_reduce_mean(better_sample_reward)
        worse_sample_reward = get_all_reduce_mean(worse_sample_reward)
        reward_accuracy = get_all_reduce_mean(reward_accuracy)
        reward_margin = get_all_reduce_mean(reward_margin)

    return {
        'train/loss': loss.item(),
        'train/reward': reward.item(),
        'train/better_sample_reward': better_sample_reward.item(),
        'train/worse_sample_reward': worse_sample_reward.item(),
        'train/reward_accuracy': reward_accuracy.item(),
        'train/reward_margin': reward_margin.item(),
        'train/lr': self.model.optimizer.param_groups[0]['lr'],
    }

ppo训练代码:

#使用策略模型 (Actor Model) 生成文本,并返回其 input_ids 和 attention_mask。
def actor_step(self, mini_prompt_only_batch: PromptOnlyBatch) -> dict[str, Any]:
    infer_batch = self.infer_batch(mini_prompt_only_batch)
    actor_batch = copy.deepcopy(infer_batch)
    sequences = self.actor_model.module.generate(
        **infer_batch,
        generation_config=self.generation_config,
        synced_gpus=True,
        do_sample=True,
    )
    attention_mask = sequences.not_equal(self.tokenizer.pad_token_id)
    actor_batch['input_ids'] = sequences
    actor_batch['attention_mask'] = attention_mask

    return actor_batch
# 计算奖励值 (reward) 和对抗奖励值 (reward_values)。 
def reward_model_step(self, actor_batch: PromptOnlyBatch) -> dict[str, Any]:
        reward_batch = copy.deepcopy(actor_batch)
        if self.reward_tokenizer is not self.tokenizer:
            reward_tokenize_output = batch_retokenize(
                actor_batch['input_ids'],
                src_tokenizer=self.tokenizer,
                dest_tokenizer=self.reward_tokenizer,
                skip_special_tokens=True,
                device=self.args.device,
            )
            reward_batch['input_ids'] = reward_tokenize_output['input_ids']
            reward_batch['attention_mask'] = reward_tokenize_output['attention_mask']
        reward_infer_batch = self.reward_infer_batch(reward_batch)
        reward_batch['reward'] = self.reward_model(**reward_infer_batch).end_scores.squeeze(dim=-1)
        critic_infer_batch = self.reward_infer_batch(actor_batch)
        scores = self.reward_critic_model(**critic_infer_batch).scores
        reward_batch['reward_values'] = scores.squeeze(dim=-1)[:, :-1]

        return reward_batch
#冻结模型参数,避免影响训练,采样多个 mini-batch,生成文本,计算奖励,计算 log 概率 (log_probs),计算参考模型的 log 概率 (ref_log_probs)
  @torch.no_grad()
    def rollout(self, prompt_only_batch: PromptOnlyBatch) -> list[dict[str, Any]]:
        """Rollout a batch of experiences."""
        # freeze the model for rolling out
        self.set_train(mode=False)

        total_batch_size = prompt_only_batch['input_ids'].size(0)
        micro_batch_size = int(self.cfgs.train_cfgs.per_device_train_batch_size)
        micro_inference_batches = []
        micro_training_batches = []
        mini_batch = {}
        for i in range(0, total_batch_size, micro_batch_size):

            mini_batch = {
                key: prompt_only_batch[key][i : i + micro_batch_size] for key in prompt_only_batch
            }

            # actor generation
            actor_batch = self.actor_step(mini_batch)
            # reward model and reward critic model scoring
            reward_batch = self.reward_model_step(actor_batch)
            # calculate the log probabilities
            logits = self.actor_model(**actor_batch).logits
            ref_logits = self.actor_reference_model(**actor_batch).logits
            log_probs = gather_log_probabilities(logits[:, :-1], actor_batch['input_ids'][:, 1:])
            ref_log_probs = gather_log_probabilities(
                ref_logits[:, :-1], actor_batch['input_ids'][:, 1:]
            )

            micro_training_batch = {}
            micro_training_batch['prompt_idx'] = mini_batch['input_ids'].size(-1) - 1
            micro_training_batch['log_probs'] = log_probs
            micro_training_batch['ref_log_probs'] = ref_log_probs
            micro_training_batch['reward'] = reward_batch['reward']
            micro_training_batch['reward_values'] = reward_batch['reward_values']

            mini_batch['input_ids'] = reward_batch['input_ids']
            mini_batch['attention_mask'] = actor_batch['attention_mask']
            # add rollout results to the batches
            micro_inference_batches.append(mini_batch)
            micro_training_batches.append(micro_training_batch)

        # unfreeze the model for training
        self.set_train()

        return micro_inference_batches, micro_training_batches

#计算策略梯度损失
# 计算 PPO 损失函数:
# ratios = exp(new_log_probs - old_log_probs)(新旧策略比)。
# 裁剪 ratios 避免策略剧烈变化(PPO 关键)。
# return -masked_mean(surrogate, mask):最大化优势 𝐴𝑡
   
def actor_loss_fn(
        self,
        log_probs: torch.Tensor,  # size = (B, L - S)
        old_log_probs: torch.Tensor,  # size = (B, L - S)
        advantages: torch.Tensor,  # size = (B, L - S)
        mask: torch.BoolTensor,  # size = (B, L - S)
    ) -> torch.Tensor:  # size = ()
        # size = (B, L - S)
        ratios = torch.exp(log_probs - old_log_probs)
        surrogate1 = advantages * ratios
        surrogate2 = advantages * torch.clamp(
            ratios,
            1.0 - self.clip_range_ratio,
            1.0 + self.clip_range_ratio,
        )
        surrogate = torch.minimum(surrogate1, surrogate2)
        return -masked_mean(surrogate, mask)  # size = ()
#  rl_step函数是训练过程中使用强化学习(RL)更新策略的一步。在PPo算法中,rl_step是用来更新策略网络(actor)和价值网络(critic)的一部分。具体来说,这个函数通过计算强化学习损失(actor loss和critic loss),并通过反向传播优化这两个网络。
# reward_critic_model 评估奖励函数的 价值估计,用于计算 优势函数 𝐴𝑡不是直接计算奖励,而是估算未来可能获得的奖励。主要用于时间差分(TD learning)更新策略,类似于 价值函数。

def rl_step(
        self, inference_batch: dict[str, torch.Tensor], training_batch: dict[str, torch.Tensor]
    ) -> dict[str, Any]:
        """Perform a single update step with RL loss."""
        old_log_probs = training_batch['log_probs']
        ref_log_probs = training_batch['ref_log_probs']
        reward = training_batch['reward']
        old_reward_values = training_batch['reward_values']
        start = training_batch['prompt_idx']

        input_ids = inference_batch['input_ids']
        attention_mask = inference_batch['attention_mask']

        sequence_mask = attention_mask[:, 1:]

        with torch.no_grad():
            old_rewards = self.add_kl_divergence_regularization(
                reward,
                old_log_probs,
                ref_log_probs,
                sequence_mask,
            )
            reward_advantages, reward_returns = self.get_advantages_and_returns(
                old_reward_values,
                old_rewards,
                sequence_mask,
                start,
            )

        logits = self.actor_model(**inference_batch, use_cache=False).logits
        log_probs = gather_log_probabilities(logits[:, :-1], input_ids[:, 1:])
        actor_loss = self.actor_loss_fn(
            log_probs[:, start:],
            old_log_probs[:, start:],
            reward_advantages,
            sequence_mask[:, start:],
        )
        self.actor_model.backward(actor_loss)
        self.actor_model.step()

        reward_values = self.reward_critic_model(**inference_batch).scores
        reward_values = reward_values.squeeze(dim=-1)[:, :-1]
        reward_critic_loss = self.critic_loss_fn(
            reward_values[:, start:],
            old_reward_values[:, start:],
            reward_returns,
            sequence_mask[:, start:],
        )
        self.reward_critic_model.backward(reward_critic_loss)
        self.reward_critic_model.step()

        with torch.no_grad():
            mask = sequence_mask[:, start:]
            kl_divergence = ((old_log_probs - ref_log_probs)[:, start:] * mask).sum(dim=-1).mean()
            mean_generated_length = mask.sum(dim=-1).float().mean()
            max_generated_length = mask.sum(dim=-1).float().max()

            reward = reward.mean()
            reward_with_kl_penalty = (old_rewards[:, start:] * mask).sum(dim=-1).mean()
            reward_advantage = masked_mean(reward_advantages, mask)
            reward_return = masked_mean(reward_returns, mask)
            reward_value = masked_mean(reward_values[:, start:], mask)

            actor_loss = get_all_reduce_mean(actor_loss)
            reward_critic_loss = get_all_reduce_mean(reward_critic_loss)
            reward = get_all_reduce_mean(reward)
            reward_with_kl_penalty = get_all_reduce_mean(reward_with_kl_penalty)
            reward_advantage = get_all_reduce_mean(reward_advantage)
            reward_return = get_all_reduce_mean(reward_return)
            reward_value = get_all_reduce_mean(reward_value)
            kl_divergence = get_all_reduce_mean(kl_divergence)
            mean_generated_length = get_all_reduce_mean(mean_generated_length)
            max_generated_length = get_all_reduce_max(max_generated_length)

        dist.barrier()

        return {
            'train/actor_loss': actor_loss.item(),
            'train/reward_critic_loss': reward_critic_loss.item(),
            'train/reward': reward.item(),
            'train/reward_with_kl_penalty': reward_with_kl_penalty.item(),
            'train/reward_advantage': reward_advantage.item(),
            'train/reward_return': reward_return.item(),
            'train/reward_value': reward_value.item(),
            'train/kl_divergence': kl_divergence.item(),
            'train/actor_lr': self.actor_model.optimizer.param_groups[0]['lr'],
            'train/reward_critic_lr': self.reward_critic_model.optimizer.param_groups[0]['lr'],
            'train/mean_generated_length': mean_generated_length.item(),
            'train/max_generated_length': max_generated_length.item(),
        }

    def ptx_step(self, ptx_batch: dict[str, torch.Tensor]) -> dict[str, Any]:
        """Perform a single update step with PTX loss."""
        ptx_loss = self.actor_model(**self.infer_batch(ptx_batch)).loss
        self.actor_model.backward(self.ptx_coeff * ptx_loss)
        self.actor_model.step()
        ptx_loss = get_all_reduce_mean(ptx_loss)
        return {
            'train/ptx_loss': ptx_loss.item(),
        }

    def train(self) -> None:
        """Train the model."""
        self.logger.print('***** Running training *****')

        progress_bar = tqdm(
            total=self.total_training_steps,
            desc=f'Training 1/{self.cfgs.train_cfgs.epochs} epoch',
            position=0,
            leave=True,
            disable=not is_main_process(),
        )

        if self.cfgs.data_cfgs.eval_datasets:
            self.logger.print('\n***** Evaluating at the beginning *****')
            self.eval()

        num_prompt_only_batches = len(self.prompt_only_dataloader)
        num_ptx_batches = len(self.ptx_dataloader)
        num_ptx_replicas = (num_prompt_only_batches + num_ptx_batches - 1) // num_ptx_batches
        for epoch in range(int(self.cfgs.train_cfgs.epochs)):
            for prompt_only_batch, ptx_batch in zip(
                self.prompt_only_dataloader,
                itertools.chain.from_iterable([self.ptx_dataloader] * num_ptx_replicas),
            ):
                inference_batches, training_batches = self.rollout(prompt_only_batch)

                if self.use_ptx:
                    ptx_batches = self.split_ptx_micro_batches(ptx_batch)
                else:
                    ptx_batches = [None for _ in range(len(inference_batches))]
                torch.cuda.empty_cache()

                for _ in range(self.cfgs.train_cfgs.update_iters):
                    for inference_batch, training_batch, ptx_batch in zip(
                        inference_batches, training_batches, ptx_batches
                    ):
                        rl_info = self.rl_step(inference_batch, training_batch)

                        torch.cuda.empty_cache()
                        self.logger.log(rl_info, step=self.global_step)
                        if self.use_ptx:
                            ptx_info = self.ptx_step(ptx_batch)
                            torch.cuda.empty_cache()
                            self.logger.log(ptx_info, step=self.global_step)

                        self.global_step += 1
                        progress_bar.set_description(
                            f'Training {epoch + 1}/{self.cfgs.train_cfgs.epochs} epoch '
                            f'(reward {rl_info["train/reward"]:.4f})',
                        )
                        progress_bar.update(1)

                        if self.global_step % self.cfgs.logger_cfgs.save_interval == 0:
                            self.logger.print(f'Saving checkpoint at step {self.global_step} ...')
                            self.save(tag=self.global_step)
                            self.logger.print('Checkpoint saved.')

                        if (
                            self.cfgs.data_cfgs.eval_datasets
                            and self.cfgs.train_cfgs.eval_strategy == 'steps'
                            and self.global_step % self.cfgs.train_cfgs.eval_interval == 0
                        ):
                            self.logger.print(
                                f'\n***** Evaluating at step {self.global_step} *****',
                            )
                            self.eval()

RM奖励模型训练代码:

    def loss(
        self,
        batch: PreferenceBatch,
    ) -> dict[str, torch.Tensor]:
        """Loss function for the reward model."""
        (
            better_input_ids,  # size = (B, L)
            worse_input_ids,  # size = (B, L)
        ) = batch[
            'input_ids'
        ].chunk(chunks=2, dim=0)
        assert better_input_ids.size(0) == worse_input_ids.size(0), 'batch size mismatch!'

# scores:一般来说,这代表模型在每个时间步骤(或输入分段)上的奖励得分,通常是一个形状为 (B, L, 1) 的张量,其中 B 是批量大小,L 是输入序列的长度,1 是奖励得分的维度。
#end_scores:通常表示输入序列的结束阶段的奖励得分,这可能是在整个序列处理完成后,模型计算出的最终奖励。
        output = self.model(**self.infer_batch(batch))
        scores = output.scores
        end_scores = output.end_scores
        higher_rewards, lower_rewards = scores.squeeze(dim=-1).chunk(chunks=2, dim=0)
        higher_end_reward, lower_end_reward = end_scores.squeeze(dim=-1).chunk(chunks=2, dim=0)

        loss = -F.logsigmoid(higher_end_reward - lower_end_reward).mean()

        if self.cfgs.train_cfgs.regularization > 0.0:
            loss = (
                loss
                + self.cfgs.train_cfgs.regularization
                * torch.stack([lower_end_reward, higher_end_reward]).square().mean()
            )

        accuracy = (higher_end_reward > lower_end_reward).float().mean()  # size = ()
        return {
            'loss': loss,  # size = ()
            'higher_end_reward': higher_end_reward,  # size = (B,)
            'lower_end_reward': lower_end_reward,  # size = (B,)
            'higher_rewards': higher_rewards,  # size = (B, L)
            'lower_rewards': lower_rewards,  # size = (B, L)
            'accuracy': accuracy,  # size = ()
        }

    def train_step(
        self,
        batch: PreferenceBatch,
    ) -> dict[str, Any]:
        """Perform a single training step."""
        loss_dict = self.loss(batch)
        loss = loss_dict['loss']
        self.model.backward(loss)
        self.model.step()

        accuracy = loss_dict['accuracy']

        loss = get_all_reduce_mean(loss)
        accuracy = get_all_reduce_mean(accuracy)

        return {
            'train/loss': loss.item(),
            'train/accuracy': accuracy.item(),
            'train/lr': self.model.optimizer.param_groups[0]['lr'],
        }

相关论文:

  • KTO,Kahneman-Tversky 优化,参阅论文《KTO: Model alignment as prospect theoretic optimization》。
  • DRO,直接奖励优化,参阅论文《Offline regularised reinforcement learning for large language models alignment》。
  • SimPO,简单偏好优化,参阅论文《SimPO: Simple preference optimization with a reference-free reward》