DLAI-GRPO-大模型强化微调笔记-全-
DLAI GRPO 大模型强化微调笔记(全)
001:课程介绍 🎯

在本课程中,我们将学习一种名为强化微调的技术。这是一种利用强化学习来提升大型语言模型在需要多步推理任务上性能的训练方法,例如解决数学问题或生成代码。通过引导模型逐步思考,强化微调能让模型自主发现复杂任务的解决方案,而不是像传统监督学习那样依赖已有的示例。
这种方法能让你用比成功监督微调所需数据量少得多的训练数据(可能只需几十个例子)来让模型适应复杂的任务。我们很高兴地介绍本课程的讲师:Predbase公司的联合创始人兼首席技术官Travis,以及高级机器学习工程师兼机器学习负责人Arnav。他们都曾与许多客户密切合作,使用强化微调解决实际业务问题。

在课程中,我们将通过一个有趣的例子来探索强化微调的工作原理:训练一个小型语言模型玩Wordle游戏。这是一个流行的猜词游戏,玩家需要在六次或更少的尝试中猜出一个五个字母的单词。

我们将从提示一个拥有25.7亿参数的模型玩这个游戏开始,分析其表现,并开发一个奖励函数来帮助模型学习如何随时间推移做得更好。
这个奖励函数是GRPO算法的核心组件。GRPO,即组相对策略优化,是由DeepSeek开发的一种用于执行推理任务强化学习的学习算法。在GRPO中,语言模型会对单个提示生成多个响应,然后使用一个基于可验证指标(如正确的格式或可运行的代码)的奖励函数来评估这些响应。
使用奖励函数是GRPO与其他强化学习算法(如依赖人类反馈或复杂多模态系统来分配奖励的PPO或DPO)的关键区别。
在为Wordle示例开发奖励函数后,你将学习一些编写优秀奖励函数的通用原则,这些原则可应用于广泛的问题。我们还将探讨如何避免奖励黑客行为,即模型学会了最大化奖励但并未真正解决问题的行为。
接下来,我们将仔细研究在强化微调过程中损失是如何计算的。你将看到,GRPO算法中看似复杂的过程(如损失函数中的裁剪和KL散度),一旦用代码实现,其实比想象中更简单。
最后,课程结束时,你将了解如何使用Predbase API,结合你自己的数据和自定义奖励函数来执行强化微调。

推理能力强的语言模型是许多智能体系统的关键组成部分,而强化微调能让较小的模型在智能体工作流程中表现出色。围绕大型语言模型的这一能力,人们充满了兴奋。同时,强化学习本身也是一种非常强大且重要的技术,对许多人来说仍然非常神秘。因此,现在是学习强化学习工作原理以及如何用它来微调你自己的定制推理模型的绝佳时机。相信你会发现学习这些内容非常有收获。

在上一节中,我们介绍了课程的整体目标和强化微调的基本概念。接下来,在下一节视频中,我们将学习强化微调与监督微调之间的主要区别。
002:强化学习简介 🧠
在本节课中,我们将要学习强化学习如何帮助大型语言模型通过实验和接收结果反馈来学习新任务。你将看到这个过程与监督微调有何不同,并直观地了解最重要的强化学习算法是如何工作的。
从监督微调到强化学习



上一节我们介绍了监督微调的基本概念,本节中我们来看看强化学习如何提供一种不同的训练范式。
传统上,我们通过一个称为监督微调的过程来教LLM完成分类、命名实体识别和代码生成等任务。首先,我们收集一个带标签的数据集,即一组提示词和响应对,以展示我们希望LLM学习的行为。然后,在训练期间,每个示例都会经过两个步骤:在前向传播中,模型为给定的提示词生成输出。接着,在反向传播中,我们将模型的输出与正确的响应进行比较,计算误差,并更新模型的权重以减少该误差。当我们对数千个类似示例重复这些步骤时,模型就学会了期望的行为。
监督微调的关键在于它通过演示来教导模型。例如,我们可以向模型展示一组数学问题及其最终答案,它将学会生成这些输出的模式,即使对于以前从未见过的类似数学问题也是如此。对于更复杂的任务,你可以在答案旁边包含推理步骤。通过这种方式构建数据集,你可以同时教导模型两个方面:第一是输出格式,即如何使用标签将思考过程与最终答案分开;第二是这种逐步推理的能力。这个系统教导模型如何生成从提示词到期望解决方案的逻辑链。

然而,尽管监督微调擅长许多任务,但它确实有一些局限性。为了获得良好的质量改进,通常需要数千个高质量的标记示例供模型学习,而这些示例的收集可能更加困难和昂贵。另一个常见的问题是过拟合现象,即模型过于完美地学习了数据中的模式,而在未见过的示例上表现不佳。这些局限性指向了对一种训练方法的需求,这种方法可以减少对大量标记数据的依赖,减轻过拟合,同时仍能引导模型朝向期望的行为。

强化学习核心概念 🐕
一种这样的替代方法是强化学习,模型通过与环境交互并优化奖励信号来学习,而不是模仿固定的标记示例。为了更好地理解这个想法,让我们仔细看看这个例子。
在这个例子中,你的小狗可以采取许多不同的行动。它可以选择坐在一个地方,可以选择打滚,或者在你扔出棍子时选择去捡回来。小狗从所有可以采取的行动中学习到,当它实际捡回棍子并交还给你时,它会得到零食作为奖励,而不是一直坐在原地。在这个例子中,小狗是智能体。捡棍子是小狗采取的行动。零食是从环境中获得的奖励。小狗观察到的结果是,带回棍子会得到零食,而不是其他行动。
那么,这个想法如何实际转化为LLM训练呢?我们可以从一个示例开始,例如来自环境的提示词,并将其输入给作为智能体的语言模型。然后,语言模型采取一个行动,即生成一系列标记作为其响应。我们可以评估这个响应,并提供一个分数,作为对该行动的奖励。这个分数可以基于质量、人类偏好或像准确性这样的自动化指标。然后,模型可以使用这个奖励作为反馈来调整其权重,从而学会为不同的输入提示词最大化其奖励。这个过程可以在新示例甚至相同示例上重复,模型将继续优化其权重以获得更高的奖励。

主流强化学习算法
以下是两种主流的基于人类反馈的强化学习方法。
基于人类反馈的强化学习
那么,我们如何实际实施这样的训练过程呢?一种被证明极其有效的方法是基于人类反馈的强化学习。这正是驱动ChatGPT等模型的核心过程。RLHF工作流程包含四个步骤。
在步骤1中,我们向语言模型发送一个提示词,并使用基于温度的采样方法生成多个候选响应。在步骤2中,我们要求标注者将这些响应从最好到最差进行排序。这产生了一个偏好排序数据集。在步骤3中,我们训练一个单独的奖励模型来学习预测这些人类偏好。它接收一个提示词和响应对作为输入,并输出一个分数来指示该响应的好坏。最后,在步骤4中,我们使用像PPO这样的强化学习算法对原始LLM进行微调。对于每个提示词,语言模型生成一个响应,奖励模型对其进行评分,然后更新语言模型的权重,以增加产生高评分输出的可能性。当我们对数百个提示词重复此步骤时,它就学会了生成能产生高分并与人类偏好一致的响应。
直接偏好优化

另一种日益流行的强化学习算法是直接偏好优化。与RLHF类似,它也使用人类偏好数据。但它不是首先训练一个单独的奖励模型,而是直接在人类偏好对上微调LLM。让我们看看它是如何做到的。
我们以与RLHF相同的过程开始,将提示词传递给LLM并采样候选响应。然而,在这种情况下,我们只采样两个不同的响应A和B。接下来,我们可以通过要求标注者告诉我们他们更喜欢两个响应中的哪一个来获取人类反馈。这通常在各种应用中使用点赞或点踩来完成,但也有其他收集方式。然后,这些偏好被用来创建一个偏好数据集,该数据集包含一个提示词、被选中的响应以及同一提示词下被拒绝的响应。最后,我们可以使用DPO算法来更新模型的权重,以生成具有更高人类偏好的响应。
训练算法本身背后的思想非常简单:对于每个提示词,比较模型对偏好响应和被拒绝响应的概率分布,看看它更可能生成哪一个。然后我们调整权重,使得模型对偏好响应的概率上升,对被拒绝响应的概率下降。

GRPO:一种新的替代方案
RLHF和DPO都依赖于人类偏好标签,而不是标准答案,但它们在标签格式、成本和风险上有所不同。RLHF需要对许多候选响应进行完整排序以训练奖励模型,并且还需要将模型的多个副本加载到内存中,导致非常高的计算和内存开销。相比之下,DPO使用简单的偏好对,通过不需要奖励模型来减少计算负载,但仍然需要大量带注释的比较数据来学习细微的偏好差异。
然而,这两种方法都没有教导模型全新的任务。它们只是引导模型朝向人类偏好的行为。为了克服对大型偏好数据集的依赖,DeepSeek团队提出了一种新的替代方法,称为组相对策略优化。这是DeepSeek R1背后的算法。


GRPO算法通过依赖我们可以定义的可编程奖励函数,绕过了对任何人类偏好标签的需求。其核心训练循环有三个步骤。与RLHF类似,我们首先向语言模型发送提示词并采样多个候选响应。接下来,我们可以编写一个或多个可编程奖励函数,这些函数接收每个提示词和响应对作为输入,并输出一个分数。例如,你可以检查输出的格式或其正确性。如果这些函数编写得当,生成的响应将获得一系列分数。然后,GRPO算法将每个候选的奖励视为训练信号。它提升组内得分高于平均水平的响应的生成概率,并降低得分低于平均水平的响应的生成概率。通过重复这个循环,GRPO直接在您关心的奖励函数上微调模型,而无需收集偏好数据,从而在人类标签稀缺或成本高昂时也能实现强化微调。关于奖励函数和GRPO训练算法的更多细节,我们将在本课程的其余部分进行介绍。
总结

本节课中我们一起学习了强化学习的基本原理及其在大型语言模型微调中的应用。我们回顾了监督微调的局限性,并探讨了强化学习如何通过奖励信号引导模型学习。我们介绍了两种主流方法:基于人类反馈的强化学习和直接偏好优化,并了解了它们各自的优缺点。最后,我们介绍了组相对策略优化这一新方法,它通过可编程奖励函数减少了对人类标注数据的依赖。理解这些基础概念是后续深入学习GRPO等具体算法的关键。
003:强化微调的优势 🎯
在本节课中,我们将探讨将强化学习作为一种微调技术能为你的工作带来哪些益处,以及哪些任务最适合使用这种训练方法。我们将具体分析 GRPO 在实践中带来的优势,并帮助你判断何时应该选择强化微调。
强化微调的优势
上一节我们介绍了强化学习的基本工作原理,本节中我们来看看 GRPO 作为微调技术所具备的具体优势。
GRPO 在实践中具有以下主要优势:
- 无需标注数据:该方法不需要预先标注好的数据。你只需要一种验证输出正确性的方法,例如通过可编程的奖励函数、使用大语言模型作为评判员,或本课程中将讨论的其他方法。
- 数据效率高且可扩展:它可以从少至 1 个示例开始工作,并随着训练过程中向模型展示的提示词数量的增加而有效扩展。
- 比监督微调更灵活:GRPO 在训练过程中主动从反馈中学习,而不是从一个固定的标注数据集中学习。这使得它更加灵活。
- 促进推理能力提升:正因如此,它能使推理模型有机地发现解决复杂问题的更好策略,通过改进其内部的思维链来实现。

实践案例:代码翻译
在 Crbase,我们想看看 GRPO 训练的模型在诸如代码翻译这样的艰巨现实任务上表现如何。
我们使用基于 GRPO 构建的强化微调方法,成功创建了一个最先进的 Triton 内核生成模型。该模型从一个开源的 320 亿参数指令模型微调而来,其表现超越了 Cloth 3.7、Thinking Deep C1 甚至 OpenAI 的 o1 模型。
这个结果强调了,使用可编程奖励的强化微调可以将大语言模型的能力推送到远超监督学习或基于偏好的训练方法的水平。
何时使用强化微调?
那么,你究竟应该在何时使用强化微调呢?它在以下三种情况下效果显著:

以下是三种最适合使用强化微调的场景:
- 没有标注数据时:当你没有标注数据,但可以验证模型输出的正确性时,例如代码生成或具有确定性输出的简单智能体工作流。
- 标注数据有限时:当你有标注数据,但数量不足以进行有效的监督微调时。这通常指少于 1000 个标注示例的情况。
- 思维链推理能提升性能时:当任务性能可以通过思维链推理得到提升时。
思维链推理是一个过程,你要求模型在给出最终答案之前,先输出一些描述其思考过程的词元。事实证明,那些应用思维链后性能得到提升的任务,也非常适合使用强化微调。
适合强化微调的任务示例
哪些任务非常适合强化微调呢?有很多,以下是三个典型例子:
以下是三个非常适合强化微调的任务类型:
- 数学问题求解:在这种情况下,强化学习让模型生成并验证详细的解题步骤,并不断优化其思维链,直到计算正确为止。
- 代码生成与调试:这也是强化微调的一个绝佳用例。模型通过针对测试用例或代码规范进行评分来学习,从而生成正确、地道的代码,并迭代地修复错误。
- 逻辑与多步推理任务:例如智能体工作流。当一个任务需要一系列决策时,强化微调鼓励模型进行自我批判,并根据最终结果改进每一步。

在以上每种场景中,从程序化或基于竞赛的奖励中主动学习的能力,解锁了远比静态监督微调更丰富、更可靠的行为模式。
决策流程:如何选择?
如果你正在决定是否使用强化微调,可以遵循以下决策流程:
以下是帮助你选择微调方法的决策步骤:
- 检查标注数据量:
- 如果拥有充足的标注数据(例如超过 10 万行),监督微调通常是获得一个好模型的最快路径。
- 如果标注数据量中等(例如少于 10 万行,但大约有 1000 行),你需要自问:思维链或其他推理提示是否能提升初始性能?
- 如果能,那么强化微调可以通过奖励正确的推理步骤来放大这些推理收益。
- 如果不能,你很可能从使用监督微调中获益最多。
- 考虑任务可验证性:
- 如果你没有标注数据,则应考虑任务的可验证性。
- 如果你可以验证输出并为其分配一个分数,那么你可以使用带有可编程奖励函数的强化微调。
- 然而,如果你的任务不可验证,你可能需要使用其他算法,如通过首先收集偏好标签来进行 DPO 或 RLHF。
总结与预告

本节课中,我们一起学习了强化微调相较于传统方法的优势,了解了它最适合的应用场景和任务类型,并掌握了一个帮助你做出技术选型的决策流程。

在下一课中,我们将演示如何使用 GRPO 来训练一个玩 Wordle 游戏的模型。尽管 Wordle 是一个游戏,但它为探索 GRPO 算法的每一个组成部分提供了一个理想的沙盒环境,并能让你亲眼看到为何这种方法在强化微调中表现出色。
004:大型语言模型能掌握 Wordle 吗?🧩



在本节课中,我们将介绍 Wordle 游戏,并将其作为 GRPO 方法的一个贯穿始终的示例。Wordle 是一个简单的游戏,但它需要规划、假设检验和逐步推理才能玩得好。这使其成为一个绝佳的例子,用以观察大型语言模型如何学习规划、分析反馈,并通过强化微调逐步改进其策略。
游戏规则回顾 🎯
上一节我们介绍了 Wordle 作为 GRPO 的示例。本节中,我们来看看 Wordle 的具体规则。
游戏目标是在最多 6 次猜测中识别出一个秘密的 5 字母单词。每次猜测后,你会收到关于猜测中每个字母的反馈。绿色表示字母正确且位置正确。黄色表示字母出现在单词中,但位置不同。灰色表示该字母完全不出现在单词中。
由于我们需要将游戏信息输入给语言模型,我们将用文本符号来表示这些颜色:用对勾 ✓ 表示绿色,用短横线 - 表示黄色,用叉号 ✗ 表示灰色。
将 Wordle 构建为强化学习问题 💻
现在,让我们进入代码,看看如何将 Wordle 构建为一个强化微调问题。
首先,我们需要导入一些必要的包。我们将通过指定不同的基础 URL 来配置 OpenAI SDK,使其指向一个模型托管服务。在本节课中,我们将使用 Gemma-2-7b-instruct 模型。
初始化客户端后,我们可以使用 transformers 包加载与该模型关联的分词器。
加载分词器后,让我们设置系统提示词,我们将把它传递给模型以进行 Wordle 游戏。系统提示词包含几个关键部分。第一部分是告诉语言模型它正在玩 Wordle 这个猜词游戏。第二部分侧重于给出我们刚刚讨论的三条游戏规则。第三部分告诉模型它将如何接收反馈。
在给出这些基本信息后,我们还会提供一个秘密单词以及猜测和反馈的示例。例如,我们给出秘密单词是 brisk,并假设模型猜测了 storm,我们会以每个字母对应一个符号的格式给出反馈。在这个例子中,S 在单词 brisk 中,但位置错误,所以我们给它一个短横线 -,而 O、T、M 完全不在单词中。
最后,我们会告诉模型我们想要的响应格式。具体来说,我们将要求它使用思维链推理来解释其思考过程,并将思考过程放在 <think> 标签内,然后将猜测的单词放在 <guess> 标签内返回。
定义辅助类和方法 🛠️
接下来,我们将定义一些辅助类和方法。
我们可以导入一些额外的依赖项来帮助定义。我们将定义一个枚举(Enum),用于指示猜测中每个字母的反馈。我们还将定义一个名为 GuessWithFeedback 的数据类,它包含一个字符串类型的猜测(guess)和一个反馈属性(feedback),反馈是一个枚举对象的列表。我们还会定义一个包装器,它的作用是将猜测和反馈转换为可以添加到提示词中的字符串。
现在,我们有了表示反馈的方法,接下来需要定义一个方法,帮助我们将所有这些反馈捕获到一个可以传递给模型的用户提示词中。我们将始终以基础提示词“请猜测一个新的5字母单词”开始,然后使用过去的猜测列表,从 GuessWithFeedback 对象创建这些反馈字符串,并在用户提示词中返回。
接下来,我们需要一种方法来捕获系统提示词、带有反馈的用户提示词,并为模型的逐步推理提供一个简短的开场白。
我们将定义这个包含系统提示词、完整渲染的用户提示词和开场白的消息对象。然后,我们将使用分词器,用正确的聊天模板标记来格式化它,以便模型以它期望的格式接收。
最后,我们将定义一个 generate_stream 函数,它接收一个提示词和一个可选的适配器 ID。这将调用 OpenAI 的 completions.create 端点,并传入提示词、温度、最大令牌数等参数,然后在生成时流式传输输出。需要注意的是,我们将温度设置为 0 以产生确定性响应,因为我们正在评估模型的质量。
查看格式化数据与模型表现 📊
现在我们已经定义了这些辅助方法,让我们看看格式化后的提示词数据是什么样子。
假设我们想要猜测的秘密单词是 craft。到目前为止,模型已经进行了两次猜测:crane 和 crash。我们可以创建 GuessWithFeedback 类的实例,其中包含猜测以及每个字母的详细反馈。当我们将其传入 render_prompt 方法时,我们会看到我们的提示词包含了与系统提示词相同的内容,以及格式化的反馈和开始猜测的开场白。
接下来,我们可以看看当我们将这个提示词发送给基础模型时会发生什么。
基础模型理解了很多反馈:C 和 R 在正确位置,而 N、E、S 和 H 不在。然而,它决定重复其最初的猜测 crane。这是一个相当不理想的猜测。
现在,我们可以看看微调模型在相同提示词上的表现。注意,我们在这里传入了一个适配器 ID,它指向我们使用将在本课程其余部分继续探索的强化微调过程训练的模型的权重。我们使用一种称为 LoRA 的技术对模型进行了微调,它允许我们仅添加和更新一小部分低秩适配器权重,而不是修改基础模型中的所有权重。
当它开始生成响应时,我们可以看到它理解了 C 和 R 在正确位置,而 N 和 E 不在单词中。同样,它也理解了单词 crash 的反馈。接着,它逐步思考可能的单词并排除它们。在生成了这一长串思维链后,它根据剩余的所有条件,决定 craft 是一个最优的猜测。微调模型实际上利用过去的反馈,在三次猜测中正确猜出了我们的秘密单词。
模拟完整游戏流程 🎮
现在我们已经了解了基础模型和微调模型在单轮中的表现,我们可以尝试模拟整个游戏。
为此,我们可以定义两个有用的辅助方法。get_feedback 方法将猜测和秘密单词作为输入,并使用上面定义的标准为猜测中的每个字母分配反馈。如果字母在确切位置匹配,我们给它一个正确符号 ✓;如果它在字母列表中但位置错误,我们给它一个短横线 -;如果它完全不在单词中,我们将其标记为错误字母 ✗。然后,它将返回这些单独反馈的列表作为输出。
我们还可以定义一个函数来逐轮模拟游戏玩法,我们称之为 next_turn。它接收三个属性作为输入:过去的猜测列表、秘密单词和一个可选的适配器 ID。它首先获取过去的猜测列表,并生成我们上面看到的渲染后的提示词。接着,它发送给模型以生成输出。一旦我们有了响应,我们将使用正则表达式匹配来提取 <guess> 标签之间的单词。如果正则匹配成功,我们就得到了模型的猜测。然后,我们可以使用上面定义的 get_feedback 方法为其分配反馈。我们可以将其添加到过去的猜测列表中,并继续这个过程。最后,这个函数将打印此时所有的过去猜测。如果猜测与秘密单词匹配,则将其标记为成功。如果我们进行了超过 6 次猜测,则说明模型没有成功。
对比基础模型与微调模型 🤖
定义了所有这些辅助方法后,让我们进入游戏玩法的核心部分。
对于游戏,我们将猜测一个秘密单词 brick,这对模型来说是一个相当容易猜测的单词。我们将从一个空的过去猜测历史开始,并将适配器 ID 设置为空,以便首先使用基础模型进行猜测。
接下来,我们可以调用 next_turn 函数,传入过去的猜测、秘密单词和适配器 ID,看看它会输出什么。
对于第一次猜测,模型认为猜测一个包含常见元音和辅音的常用单词是个好主意,因此它猜测了单词 grain,并相应地得到了一些反馈。让我们看看它在下次猜测中如何整合反馈。
如果你查看模型在第二次猜测时的思维链,我们可以看到它利用了一些反馈,例如 R 在正确位置,但它也错误地得出结论认为 C、A、N 和 E 完全不在单词中。如果我们阅读思维链的其余部分,我们会看到它决定随机猜测,并猜了单词 brick,因此它猜对了这个单词。
现在,让我们看看微调模型对于同一个秘密单词的表现如何。再次,我们定义我们的秘密单词,将过去的猜测设置为空列表,但这次我们将适配器 ID 设置为上面看到的同一个模型。然后,我们可以像之前一样调用 next_turn 函数。
微调模型决定它想选择一个首猜单词,它需要包含常见字母、有元音且重复字母最少。它提出了一组合理的候选词,如 arise、stare 或 crane,然后决定 stare 是一个很好的首猜,因为它包含很多常见字母。对于这个猜测,它收到了以下反馈。让我们看看它如何在下次猜测中利用其反馈。
模型首先分析其第一次猜测,并正确地了解到 S、T、A 和 E 不在秘密单词中。它还承认 R 在单词中,但位置错误。基于此,它想出了一个策略,思考它尚未尝试过的常见字母,然后思考如何使用这些信息。基于它知道 R 在单词中但位置错误的事实,它认为 R 可能应该在第二个位置,并想出了一个可能的单词列表。接着,它排除了像 print 这样的单词,因为它知道 P 不在单词中。随着它继续思考,它决定 proud 是一个好的猜测,因为它测试了多个新字母,这将排除很多单词。
对于猜测 proud,它了解到 R 确实在第二个位置,但 O、U、D 和 P 不在单词中。现在,如果你思考一下,它的猜测 stare 和 proud 已经测试了五个元音中的四个(A、E、O、U)。所以下一次猜测,它实际上应该尝试包含字母 I 的猜测。让我们看看它在第三次猜测中是否这样做了。
再次,它从分析前两次猜测开始,并尝试利用这些信息来思考符合以下模式的单词:? r ? ? ?。它列出了一个候选单词列表。它还正确地排除了无效的单词(太短或太长),最终得出结论,只有三个有效选项:brick、drink 和 grind 是下次猜测的良好候选。它决定选择 brick,因为它引入了我们尚未测试过的新字母。然后,它花了一点时间根据初始反馈的所有标准验证这个猜测是有效的。事实证明,brick 确实是正确的猜测。
与基础模型相比,你会注意到微调模型迭代地思考了其推理过程,并采用了更具战略性的方法来解 Wordle 游戏。这实际上是强化微调的好处之一,因为我们要求模型在提供响应之前先输出其思维链,它可以在训练过程中学习如何迭代地改进,以得出更合理的推理来获得良好的结果。
总结与展望 📝
本节课中,我们一起学习了如何将 Wordle 游戏构建为一个强化微调问题。我们回顾了游戏规则,定义了必要的辅助类和方法来格式化提示词和模拟游戏流程。通过对比基础模型和经过 LoRA 微调的模型,我们观察到微调模型能够进行更系统、更具战略性的逐步推理,从而更有效地利用反馈并猜出单词。
这是一个很好的时机,可以尝试其他秘密单词,看看基础模型和微调模型如何比较,特别是为了更好地理解微调模型如何在其努力猜测秘密单词的过程中产生一致且合理的推理。
当你完成后,可以在下一节课中与 Travis 一起,他将向你展示如何为 Wordle 游戏定义奖励函数。


005:奖励函数设计 🎯

在本节课中,我们将学习如何设计奖励函数,以驱动强化微调过程。我们将看到奖励如何转化为优势值,从而在学习过程中引导模型产生更好的结果。
上一节我们介绍了如何指示大型语言模型玩 Wordle 游戏。本节中,我们来看看如何设计奖励函数。
开始实践
让我们进入 Notebook 开始实践。首先导入所需的依赖项。
import torch

本节课我们将使用 PyTorch。接下来,创建我们将用于提示的基础模型部署。本节课的基础模型是 Qwen2-7B-Instruct 模型。我们将其定义为一个变量。
设计奖励函数
一种直接的奖励函数设计方法是使用简单的二元成功或失败信号。这为正确答案分配奖励值 1,为错误答案分配奖励值 0。
这类似于监督微调领域中的情况,即模型试图获得一个真实的标准答案。
def binary_reward(guess, secret_word):
return 1 if guess == secret_word else 0
实践示例
现在让我们看看这个奖励函数在一些示例猜测中如何工作。假设我们的秘密单词是 pound,并且模型在此之前已经猜了几个词:crane、blonde,最后是 found。
我们这里有一个名为 GuessWithFeedback 的辅助类,它本质上接收我们的猜测和秘密单词作为输入,然后存储关于哪些字母正确、哪些错误、哪些位置错误等信息。
现在,我们获取所有这些过去的猜测,并尝试从我们的模型生成一个新的猜测。
我们将调用生成函数,将过去的猜测转换为完全渲染的提示,获取响应,然后从该响应中提取出猜测。最后,我们将使用上面定义的 Wordle 奖励函数对最终猜测进行评分,看看我们得到了什么。
在这种情况下,模型猜测了 gone,获得了 0 的奖励。这意味着从学习过程的角度来看,这个猜测是完全错误的。
从奖励到学习
现在,让我们简要讨论一下这些奖励函数最终如何转化为学习过程。在强化学习中,奖励函数向智能体提供关于其实现目标情况的反馈。这些奖励是在采取某些行动后分配的数值,表示结果的可取程度。
我们最终要做的是,获取模型针对特定提示做出的所有不同猜测,然后找出哪些猜测相对更好。智能体的目标是随着时间的推移最大化其总体奖励。
这种学习需要两个要素:一是需要生成响应的多样性,二是最终需要导致奖励的多样性。之所以如此,是因为我们确定一个响应相对于另一个响应的相对可取性的方式,是通过一种称为“优势”的东西。
以下是计算优势的公式:
优势 = (奖励 - 平均奖励) / 标准差
我们在这里所做的就是,获取为特定提示计算的所有奖励,然后计算一个归一化值,即减去平均值再除以标准差。最终得到一个以 0 为中心的良好数值。
在代码中,它看起来像这样的函数:
def compute_advantages(rewards):
mean_reward = torch.mean(rewards)
std_reward = torch.std(rewards)
if std_reward == 0:
advantages = torch.zeros_like(rewards)
else:
advantages = (rewards - mean_reward) / std_reward
return advantages
让我们看一个快速示例,了解这个优势计算是如何工作的。假设有一些假的奖励分数,范围从 0 到 1,中间有一些值如 0.2、0.4、0.5 等。让我们看看优势值是什么样的。
可以看到,优势值以 0 为中心。对于那些处于中间的奖励,优势值下降到负值;对于相对较低的奖励,优势值按比例上升。这表明,从学习的角度来看,我们将阻止模型生成那些得分为 0 的响应,并鼓励模型生成更多看起来像产生这些高奖励值的响应。
可视化奖励与优势
让我们可视化现有奖励函数在 Wordle 任务上的奖励和优势。我们将定义一个函数来打印猜测表格。对于每个响应和奖励函数,我们将获取猜测、获取奖励,并打印显示这些值的表格。
让我们进行几次猜测,计算奖励和优势,并在此处渲染表格。
我们可以看到,对于秘密单词 pound,我们进行了八次不同的猜测:crane、tower、sort、food 等。在每种情况下,这些猜测都不是单词 pound,因此奖励为 0,结果优势也为 0。因此,从 GRPO 算法的角度来看,这些奖励实际上不会导致任何学习。
引入部分奖励
尽管目前所有猜测都获得 0 奖励,但并非所有猜测都同样错误。有些猜测包含正确位置上的正确字母。例如,可以看到 news 中的 N、OU 等字母,它们确实在正确的位置上;in 是正确的字母但位置错误。因此,可以说这比像 Crane 这样的猜测更好,因为后者在正确位置上的正确字母要少得多。
这表明二元奖励函数可能过于严格。相反,我们可以引入一个部分奖励系统,根据正确性和位置准确性,为更接近目标单词的猜测分配更高的奖励。
让我们引入一个新的分配部分奖励的奖励函数。
首先,我们将比较猜测的长度与秘密单词的长度。如果它们长度不同,我们将直接返回奖励 0,从而在方向上阻止模型进行任何字母数量不正确的猜测。
接下来,我们将获取秘密单词中所有有效字母的集合,然后逐个迭代猜测中和秘密单词中的每个字母并进行比较。
如果秘密字母和猜测字母匹配,那么我们处于字母正确且位置正确的情况,我们将给予 0.2 的奖励。
如果字母在单词中但位置错误,那么我们将给予 0.1 的分数。
否则,我们将不给予任何奖励。
这意味着,对于一个给定的五个字母的单词,如果每个字母都在正确的位置,它将获得总计 1 的奖励。而对于介于两者之间的情况,我们将有部分奖励,正确位置上的正确字母可能导致 0.2 或 0.4 等分数。因此,我们应该希望看到从这个过程中获得的奖励分数有一些变化。
def partial_credit_reward(guess, secret_word):
if len(guess) != len(secret_word):
return 0.0
reward = 0.0
secret_letters = set(secret_word)
for g_char, s_char in zip(guess, secret_word):
if g_char == s_char:
reward += 0.2 # 正确字母,正确位置
elif g_char in secret_letters:
reward += 0.1 # 正确字母,错误位置
return reward
尝试新的奖励函数
让我们尝试将新的部分奖励函数应用于之前的秘密单词,并使用我们的模型尝试创建一些猜测。
正如我们将在这里看到的,即使有部分奖励,这个过程也严重依赖于为给定提示获得良好的不同响应多样性。我们控制这种多样性的方式是通过一个称为“温度”的参数。虽然也存在其他采样参数,但温度是我们可以使用的最常见的参数之一。
在这里,我们将看看如果将温度设置为 0 会发生什么,这意味着模型将始终为每个提示选择概率最高的猜测。
不出所料,当我们将温度设置为 0 时,我们基本上创建了一个确定性采样过程,因此每次模型都猜测相同的东西。在这种情况下,单词 frown 获得了 0.2 的奖励,但由于它每次都猜 frown,我们又回到了优势本身全为零的情况。
在光谱的另一端,我们可以尝试用高温(如这里的 1.3)生成响应,这应该会引入更多的变化。
使用较高的温度值已成功地导致生成的奖励分数有更多种类。因此,我们现在看到了我们所希望的那种优势变化。但我们也看到,猜测总体上平均比贪婪采样时更差。我们看到更多猜测为空白的情况,这意味着模型实际上从未成功生成猜测。
因此,这最终意味着,虽然这里会发生一些方向性的学习,因为我们可以计算优势,但整体学习过程会更慢,因为猜测质量本身通常相当低。
寻找平衡
因此,我们想要做的是找到一种方法来平衡这两个极端,这意味着在这里设置一个合理的温度值,比如 0.7。
现在我们可以看到,我们终于开始得到更像我们所希望的东西。猜测通常倾向于具有正确的字母数,它们都是有效的单词,其中一些比另一些更好。我们看到奖励分数有一些变化,优势也有一些变化。总的来说,我们预计这将开始推动我们的模型学习猜测更有可能获得更高奖励的单词,从而更有可能最终在下一课中猜对单词。
在下一课中,我们将看看其他奖励函数的例子,这些函数可用于评估更软性的标准,这些标准有时更主观,或在学习过程中更依赖于人类的价值判断。
总结

本节课中,我们一起学习了如何为强化微调设计奖励函数。我们探讨了二元奖励和部分奖励系统,了解了奖励如何通过计算优势值来指导模型学习。我们还看到了温度参数在控制响应多样性方面的重要作用,以及如何在确定性和高多样性之间找到平衡,以促进有效的学习过程。
006:基于LLM作为评判者的奖励函数设计 🧠


在本节课中,我们将学习如何为一个更主观的任务——总结财报电话会议记录——编写奖励函数。你将看到如何利用大型语言模型作为人类判断的代理,并在结果难以通过代码验证的情况下,创建能够产生学习信号的奖励函数。
导入依赖
首先,我们导入必要的标准依赖库。
加载数据集与任务定义
上一节我们介绍了奖励函数的基本概念,本节中我们来看看一个具体的应用场景。我们将使用一个与之前不同的用例:总结财报电话会议记录。
我们从Huggingface加载这个数据集,并查看一个示例记录。可以看到,这些记录通常非常冗长。为了本任务的目的,我们假设目标是创建一个对金融分析师有用的摘要,他们只希望了解基于财报电话会议的公司健康状况的关键要点。
构建摘要生成提示
以下是构建生成摘要提示的步骤:
- 定义一个简单的提示,例如:“生成以下财报电话会议记录中信息的简洁摘要。仅用摘要回应,不要包含任何无关文本。”
- 将记录作为变量插入提示中。
- 定义一个函数,该函数接收记录和希望生成的样本数量作为输入,并生成摘要。
我们使用OpenAI API兼容的SDK,将提示转换为聊天API格式,并设置温度参数为0.9以确保一定的随机性,从而生成摘要。
生成的摘要比原始记录短得多,但仍包含一些不必要的语言,例如“以下是财报电话会议记录的简洁摘要”。对于我们的金融分析师来说,其中一些内容可能并非必需。
设计奖励函数:LLM作为评判者
下一步是思考如何构建一个奖励函数,以引导生成的摘要更符合分析师工作的需求。
一种方法是使用LLM作为分析师判断的代理,尝试在1到10的范围内对摘要进行评分,然后将最终分数用作奖励函数得分。
以下是实现此奖励函数的关键步骤:
- 设计提示,要求模型根据记录和摘要,在1到10的范围内评分(1代表非常差,10代表非常好),并在特定标签内输出最终分数。
- 将上述逻辑封装成一个函数,该函数接收记录、摘要和一个评判模型(例如GPT-4-mini)作为输入,并返回一个浮点数值。
- 函数内部将提示、记录和摘要转换为聊天格式的消息。
- 调用评判模型生成一个响应(温度设为0以获得其认为的最佳响应)。
- 使用正则表达式从响应中提取最终分数,转换为整数,然后除以10以得到0到1之间的归一化值。
- 如果过程中出现任何错误,则返回分数0。
应用此评判奖励函数到我们的摘要和记录上,模型会提供一些推理过程(可用于审核其判断是否合理),并给出最终分数,例如0.9。
扩展评估与发现问题
现在,让我们尝试扩展到8个不同的样本,而不是仅一个,以了解评判模型给出的奖励分数的多样性。
我们为原始记录生成了8个不同的摘要,然后使用评判模型根据上面编写的奖励函数对每个摘要进行评分。
评分结果普遍较高(0.8, 0.7等),但重要的是,它从未明确指出任何摘要存在特别严重的问题,也从未给出满分。这是以这种直接方式使用LLM作为评判者的一个普遍问题:它倾向于认为事物总体良好,因为它不想被明确指出错误。这对我们来说是个问题,因为我们希望模型对特定回答的好坏有非常明确的意见,以便更清晰地指导学习过程,使其朝着我们希望的方向发展。
改进方法:基于客观测验的奖励
如何解决这个问题?一种思路是尝试将其建立在更客观的基础上。
我们不再直接问模型“你觉得这个摘要怎么样?”,而是尝试基于记录中我们认为对金融分析师最相关的信息生成一个多项选择测验。例如,问题可以是“第一季度的每股收益是多少?”,并附上选项A、B、C、D以及末尾的答案。
因为所有我们关心的信息都在原始记录中,所以对于LLM来说,构建这个测验应该是一个相对直接和客观的任务。然后,在学习过程中,我们可以参考这个测验来给摘要评分,看看摘要是否保留了测验中涉及的所有信息。这是一位客户为其摘要问题提出的技术。
实现结构化测验生成
我们可以利用LLM普遍支持的结构化生成功能,使用Pydantic模式定义我们想要的输出结构。
以下是定义测验结构的关键类:
Question类:表示单个测验问题。- 属性:
question_text(问题文本),options(选项列表),answer(正确答案的索引)。 - 辅助函数:
shuffle_options(打乱选项顺序),render(将问题渲染为字符串)。
- 属性:
Quiz类:包装一系列问题。- 属性:
questions(问题列表)。 - 辅助函数:
shuffle_all(打乱所有问题的选项),to_string(将整个测验打印为字符串)。
- 属性:
我们定义一个create_quiz辅助函数,它接收记录字符串作为输入,使用提示要求模型从中生成测验,并调用completions.parse API,传入Quiz作为响应格式,温度设为0.7以生成不同变体。得到Quiz对象后,我们会打乱每个问题的所有选项顺序。这样做是为了抵消模型在放置正确答案位置上的潜在偏见(例如,经常放在B选项),使其更随机。
使用摘要进行测验并评分
生成了测验之后,我们需要编写辅助函数,让评判模型使用摘要来回答这个测验。
以下是实现步骤:
- 定义提示:“使用提供的记录摘要来回答以下测验。必须回答所有10个问题。如果不知道,请回答0。”
- 将测验字符串和摘要插入提示中。
- 调用评判模型(温度设为0)获取其答案列表。
- 解析响应(预期为括号包围的答案列表),例如
[‘A’, ‘C’, ‘0’, …]。
最后,我们需要对take_quiz函数输出的答案进行评分。
我们编写score_quiz_answers辅助函数:
- 输入:答案列表和测验对象。
- 首先进行完整性检查,确保答案数量与测验问题数量一致。
- 然后遍历每个答案和对应的问题,如果匹配,则增加正确计数。
- 最终得分是正确回答数除以测验问题总数。
评估改进后的奖励函数
现在,让我们在之前生成的所有摘要上运行这个基于测验的评分流程。
我们遍历每个摘要,让其参加测验,记录答案,然后使用评分函数计算得分。
结果显示,我们基于测验的方法在得分(以及由此计算出的优势度)上提供了相当不错的多样性。因此,我们可以预期从这个过程中获得良好的学习效果,因为存在多样化的奖励和优势信号。
总结与展望
本节课中,我们一起学习了如何为总结财报电话会议记录这个主观任务设计奖励函数。我们首先尝试了直接使用LLM作为评判者进行评分,但发现了其评分偏高、区分度不足的问题。接着,我们引入了一种更客观的改进方法:让LLM基于原始记录生成一个关键信息测验,然后通过评估摘要回答该测验的准确率来获得奖励分数。这种方法能产生更具区分度的奖励信号,更有利于指导模型学习。

在下一节课中,我们将更仔细地研究这个特定用例,并思考我们的奖励模型可能被利用以鼓励不良行为(即所谓的“奖励黑客”)的一些方式。然后,在之后的课程中,我们将回到如何将所有内容整合到一个损失函数中的想法。
007:奖励攻击与缓解策略

概述
在本节课中,我们将要学习强化学习中一个常见的问题——奖励攻击。我们将探讨在文本摘要任务中奖励攻击可能的表现形式,并学习如何通过修改奖励函数来惩罚模型的不良行为,从而引导模型生成真正符合我们要求的摘要。


奖励攻击问题
上一节我们介绍了如何通过问答测试来评估摘要质量。本节中我们来看看一个潜在的风险:奖励攻击。
奖励攻击是指模型学会了一种策略,能够最大化其获得的奖励,但实际上并未执行我们期望它完成的任务。在摘要任务中,这可能表现为模型为了获得高分而忽略“简洁”的要求。
我们继续使用收益电话会议记录摘要任务。首先,我们使用与之前相同的提示词生成八个摘要,并设置温度参数为0.9以确保输出多样性。然后,我们查看这些摘要在问答测试中的得分。
然而,我们可能忽略了一个问题:如果将原始文本本身作为“摘要”提交给测试,它会得多少分?结果发现,原始文本本身获得了满分。这意味着,如果奖励函数仅仅基于测试得分,那么模型生成原始文本就是最优策略。这为强化学习过程创造了一个反常的激励:尽管目标是生成简洁摘要,但模型实际上因保留了更多原文信息而受到奖励。长此以往,模型可能会学会“欺骗”系统,忽略提示中的简洁性要求,直接返回原文以优化奖励。
设计惩罚机制
那么,我们如何缓解这个问题呢?我们可以设计一个新的奖励函数,将我们关心的“简洁性”属性考虑进去。

以下是不同生成摘要的长度。除了测试得分有差异外,摘要的长度也存在显著差异。有些摘要约900字符,有些则长达1300字符。但如果我们查看原始文本的长度,会发现它大约有21000字符,这远超出理想摘要的长度。我们必须阻止模型生成如此长的内容。
因此,我们引入一个新的奖励函数,其目的是惩罚那些过长、超出我们定义的“简洁摘要”范围的输出。这个函数是一个惩罚项,其值为负数,我们称之为“长度惩罚奖励”。
长度惩罚奖励函数公式:
def length_penalty_reward(response, target_length=1024, max_penalty=-10):
length = len(response)
if length <= target_length:
return 0 # 无惩罚
else:
# 惩罚随长度超出目标而增加,最高为 max_penalty
penalty = max_penalty * min(1, (length - target_length) / target_length)
return penalty
该函数计算响应的字符长度,并与我们设定的摘要最大合理长度(1024字符)进行比较。如果摘要长度小于目标长度,则返回0(无惩罚)。否则,惩罚值将随着文本长度超出目标长度的比例而增加,最高惩罚为-10。
让我们看看这个长度惩罚对原始文本(如果被模型生成)的影响。由于原始文本超过20000字符,它将受到最高惩罚-10,这应该能极大地阻止模型生成这种长度的摘要。
整合奖励函数
现在,让我们回到最初生成的摘要,看看长度惩罚奖励对它们各自的影响。
我们可以看到,对于最短的摘要(941字符,低于1024的目标),它获得了0奖励(即无惩罚),这是该函数可能的最高奖励。而对于最长的摘要(1365字符),它获得了最负的奖励,这也转化为最低的优势值。
现在,让我们将这两个不同的奖励函数整合成一个最终的总奖励函数。

总奖励函数公式:
def total_reward(completion, quiz_function, length_penalty_function):
quiz_score = quiz_reward(completion) # 假设的问答测试奖励函数
length_penalty = length_penalty_reward(completion)
total = quiz_score + length_penalty
return total
在这个案例中,我们将来自长度惩罚函数的奖励惩罚与问答测试奖励直接相加,得到最终奖励。
我们可以计算并可视化长度奖励与测试奖励之间的关系。在图表右上角,以深绿色标记的响应同时具有最高的长度奖励和最高的测试奖励,因此它拥有最高的总优势值。在强化学习过程中,模型将被引导去生成更多类似这样的响应。
相比之下,图中有一组响应,它们的测试奖励非常相似(大约在0.6到0.65之间),但由于长度惩罚不同,它们的优势值差异很大。最左侧的响应因过长而受到严重惩罚,尽管其测试奖励与右侧一个表现不错的响应相同,但总体优势却很低。
总结
本节课中我们一起学习了奖励攻击的概念及其在文本摘要任务中的具体表现。我们了解到,如果奖励函数设计不当,模型可能会学会通过“作弊”来获取高分,而非真正完成我们设定的任务。
为了缓解这个问题,我们引入了长度惩罚奖励函数,对过长的摘要输出进行惩罚。通过将测试奖励与长度惩罚相加,我们构建了一个更全面的总奖励函数。这个新函数能够有效地区分那些虽然测试得分高但过于冗长的“坏”摘要,以及真正既准确又简洁的“好”摘要,从而引导模型的学习方向。


引入此类惩罚有助于减轻奖励攻击的影响,避免模型陷入虽然技术上获得高奖励、但最终并未完成我们期望任务(即生成简洁摘要)的失败模式。在下一节课中,我们将把这些优势值计算与最终的学习过程结合起来,展示它们如何通过损失函数的计算转化为模型参数的更新。
008:GRPO 中的损失计算 📉

在本节课中,我们将深入探讨 GRPO 算法中损失函数是如何计算的。这是驱动语言模型在训练过程中从其“实验”中学习的关键步骤。我们将把复杂的公式分解为四个核心组成部分,并通过代码实现来理解其运作机制。

概述
上一节我们介绍了如何分配奖励和计算优势值。本节中,我们来看看 GRPO 算法如何利用这些信息来计算损失,从而指导模型参数的更新。GRPO 的损失函数虽然看起来复杂,但可以分解为四个主要部分:策略损失、优势值、裁剪目标和 KL 散度。
损失函数的四个组成部分
以下是 GRPO 损失函数的四个核心组件:
-
策略损失:这代表了带适配器的模型与不带适配器的模型之间,对每个输出令牌的概率分布之比。其核心是衡量策略模型相对于参考模型对生成序列的偏好变化。
- 公式:
ratio = exp(log_prob_policy - log_prob_ref) - 直观理解:如果
ratio > 1,说明策略模型比参考模型更倾向于生成该令牌;如果ratio < 1,则相反。
- 公式:
-
优势值:这是我们之前从奖励函数计算得到的值。它用于缩放策略损失,告诉模型哪些生成了高奖励的令牌应该被加强,哪些生成了低奖励的令牌应该被抑制。
-
裁剪目标:为了防止任何单个训练步骤中产生过大的损失值,从而确保训练稳定性。它通过限制
ratio的波动范围来实现。- 代码:
clipped_ratio = torch.clamp(ratio, 1-epsilon, 1+epsilon) - 参数:
epsilon通常设为0.2。
- 代码:
-
KL 散度:这是一个惩罚项,用于确保在训练过程中,我们正在优化的策略模型不会偏离原始的参考模型(基线模型)太远。这有助于保留模型原有的知识,防止“灾难性遗忘”。
- 公式:
kl_div = (log_prob_policy - log_prob_ref) * ratio - log(ratio) + 1
- 公式:
代码实现:准备模型与输入
现在,让我们看看这个损失函数在代码中是如何实现的。首先,我们需要初始化模型并准备数据。
我们将导入必要的库,并初始化一个名为 baby_llama 的基础模型及其分词器。在 GRPO 中,我们使用两个模型:
- 参考模型:冻结的、未添加 LoRA 权重的原始模型。
- 策略模型:在基础模型上添加了可训练的 LoRA 适配器的模型,这是我们实际要优化的对象。
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
# 初始化基础模型和分词器
model_name = “baby_llama”
base_model = AutoModelForCausalLM.from_pretrained(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name)
# 创建参考模型(冻结)
ref_model = AutoModelForCausalLM.from_pretrained(model_name)
for param in ref_model.parameters():
param.requires_grad = False
# 创建策略模型(添加LoRA)
from peft import LoraConfig, get_peft_model
lora_config = LoraConfig(
r=8, # LoRA秩
lora_alpha=32,
target_modules=[“q_proj”, “v_proj”], # 在哪些层添加LoRA
lora_dropout=0.1,
bias=“none”,
task_type=“CAUSAL_LM”
)
policy_model = get_peft_model(base_model, lora_config)
接下来,我们定义两个辅助函数来准备模型输入和计算对数概率。
def prepare_inputs(prompt, completion, tokenizer):
"""将提示词和补全内容转换为模型输入张量。"""
# 分词
prompt_tokens = tokenizer(prompt, return_tensors=“pt”).input_ids
completion_tokens = tokenizer(completion, return_tensors=“pt”).input_ids
# 合并提示和补全
input_ids = torch.cat([prompt_tokens, completion_tokens], dim=1)
# 创建注意力掩码(全部为1,表示所有令牌都参与计算)
attention_mask = torch.ones_like(input_ids)
# 创建补全掩码(仅对补全部分的令牌计算损失)
prompt_len = prompt_tokens.shape[1]
total_len = input_ids.shape[1]
completion_mask = torch.zeros(total_len)
completion_mask[prompt_len:] = 1
return input_ids, attention_mask, completion_mask
def compute_log_probs(model, input_ids, attention_mask):
"""计算模型对每个输出令牌的对数概率。"""
with torch.no_grad():
outputs = model(input_ids=input_ids, attention_mask=attention_mask)
logits = outputs.logits # 模型输出的原始分数
# 计算对数概率:log_softmax
log_probs = torch.nn.functional.log_softmax(logits, dim=-1)
# 我们关心的是模型实际生成的令牌的概率
# 通过 gather 操作获取每个位置对应令牌的对数概率
gathered_log_probs = log_probs[:, :-1, :].gather(dim=-1, index=input_ids[:, 1:].unsqueeze(-1)).squeeze(-1)
return gathered_log_probs
实现基础 GRPO 损失函数
有了上述准备,我们可以开始实现 GRPO 损失函数。首先从最核心的策略损失开始。
def compute_grpo_loss_basic(policy_model, ref_model, tokenizer, prompt, completion, advantage):
"""计算基础的GRPO损失(不含裁剪和KL散度)。"""
# 1. 准备输入
input_ids, attn_mask, comp_mask = prepare_inputs(prompt, completion, tokenizer)
# 2. 计算策略模型和参考模型的对数概率
log_probs_policy = compute_log_probs(policy_model, input_ids, attn_mask)
log_probs_ref = compute_log_probs(ref_model, input_ids, attn_mask)
# 3. 计算比率 (ratio)
ratio = torch.exp(log_probs_policy - log_probs_ref) # 核心公式
# 4. 计算策略损失:比率 * 优势值
policy_loss_per_token = ratio * advantage
# 优化器通常最小化损失,但我们要最大化奖励,所以取负号
policy_loss_per_token = -policy_loss_per_token
# 5. 仅对补全部分的令牌计算平均损失
# comp_mask[1:] 是因为 gathered_log_probs 比 input_ids 少一个时间步
loss = (policy_loss_per_token * comp_mask[1:]).sum() / comp_mask[1:].sum()
return loss
# 示例调用
prompt = “The quick brown fox jumps over the”
completion = “ lazy dog”
advantage = 2.0 # 假设奖励函数给出的优势值
loss = compute_grpo_loss_basic(policy_model, ref_model, tokenizer, prompt, completion, advantage)
print(f“基础策略损失: {loss.item()}”)
注意:在训练的第一步,策略模型和参考模型是完全相同的,因此 ratio 全为 1。此时,损失值完全由优势值决定 (loss = -advantage)。这强调了设计能产生多样化奖励分数的奖励函数的重要性。如果所有优势值都是 0,训练将无法启动。
添加裁剪目标
基础损失函数可能导致某些令牌的 ratio 极大或极小,造成训练不稳定。裁剪目标通过限制 ratio 的范围来解决这个问题。
def compute_grpo_loss_with_clip(policy_model, ref_model, tokenizer, prompt, completion, advantage, epsilon=0.2):
"""计算包含裁剪目标的GRPO损失。"""
# 准备输入和计算对数概率(同上)
input_ids, attn_mask, comp_mask = prepare_inputs(prompt, completion, tokenizer)
log_probs_policy = compute_log_probs(policy_model, input_ids, attn_mask)
log_probs_ref = compute_log_probs(ref_model, input_ids, attn_mask)
ratio = torch.exp(log_probs_policy - log_probs_ref)
# 计算未裁剪的损失
unclipped_loss = ratio * advantage
# 计算裁剪后的比率及对应的损失
clipped_ratio = torch.clamp(ratio, 1.0 - epsilon, 1.0 + epsilon)
clipped_loss = clipped_ratio * advantage
# 取最小值,防止过大的更新
policy_loss_per_token = torch.min(unclipped_loss, clipped_loss)
policy_loss_per_token = -policy_loss_per_token # 取负以最大化奖励
# 计算平均损失
loss = (policy_loss_per_token * comp_mask[1:]).sum() / comp_mask[1:].sum()
return loss
# 示例调用
loss_clip = compute_grpo_loss_with_clip(policy_model, ref_model, tokenizer, prompt, completion, advantage, epsilon=0.2)
print(f“带裁剪的损失: {loss_clip.item()}”)
裁剪操作作用于每个令牌的 ratio 上,而不是最终的损失值。这确保了训练更新的平滑性。
添加 KL 散度惩罚
最后,我们引入 KL 散度项,以防止策略模型过度偏离参考模型,从而保留原始模型的有用知识。
def compute_grpo_loss_full(policy_model, ref_model, tokenizer, prompt, completion, advantage, epsilon=0.2, beta=0.1):
"""计算完整的GRPO损失(包含裁剪和KL散度)。"""
# 准备输入和计算对数概率
input_ids, attn_mask, comp_mask = prepare_inputs(prompt, completion, tokenizer)
log_probs_policy = compute_log_probs(policy_model, input_ids, attn_mask)
log_probs_ref = compute_log_probs(ref_model, input_ids, attn_mask)
ratio = torch.exp(log_probs_policy - log_probs_ref)
delta = log_probs_policy - log_probs_ref # 用于计算KL散度
# 1. 计算策略损失(带裁剪)
unclipped_loss = ratio * advantage
clipped_ratio = torch.clamp(ratio, 1.0 - epsilon, 1.0 + epsilon)
clipped_loss = clipped_ratio * advantage
policy_loss_per_token = torch.min(unclipped_loss, clipped_loss)
# 2. 计算每个令牌的KL散度惩罚
# KL散度公式: (log_prob_policy - log_prob_ref) * ratio - torch.log(ratio) + 1
kl_div_per_token = delta * ratio - torch.log(ratio) + 1
# 3. 组合损失:策略损失 - beta * KL散度
# 取负号,因为我们要最大化(策略损失 - KL惩罚)
total_loss_per_token = -(policy_loss_per_token - beta * kl_div_per_token)
# 4. 计算平均损失
loss = (total_loss_per_token * comp_mask[1:]).sum() / comp_mask[1:].sum()
return loss
# 示例调用
loss_full = compute_grpo_loss_full(policy_model, ref_model, tokenizer, prompt, completion, advantage, epsilon=0.2, beta=0.1)
print(f“完整GRPO损失(含KL散度): {loss_full.item()}”)
参数 beta 的作用:
beta控制 KL 散度惩罚的强度。beta = 0:忽略 KL 散度,模型可以自由探索,但可能遗忘原有知识。beta = 0.1(常用):在学习新任务和保留旧知识之间取得良好平衡。beta = 0.5或更高:强烈要求模型行为接近参考模型,适用于希望保持高度通用性的场景。
KL 散度项的行为是:当策略模型对某个令牌的置信度远高于参考模型时(delta 为正且大),惩罚增长较慢;当策略模型置信度远低于参考模型时(delta 为负且大),惩罚会迅速增加,从而强力纠正模型的偏离。
总结
本节课中,我们一起学习了 GRPO 算法损失函数的完整构成与实现。我们将其分解为四个关键部分:
- 策略损失:通过比率计算策略模型相对于参考模型的行为变化。
- 优势值缩放:根据奖励调整损失,鼓励高奖励行为。
- 裁剪目标:通过限制比率变化范围来稳定训练。
- KL 散度惩罚:防止策略模型过度偏离原始参考模型,保留已有知识。

尽管强化学习的损失函数初看复杂,但其核心思想与预训练或监督微调中的损失函数相似,关键区别在于每个样本(或令牌)都根据其获得的奖励(优势值)进行加权。裁剪和 KL 散度则像是“刹车系统”,防止训练过程中出现过于激进的更新。如今,许多公司提供了实现这些算法的训练系统,使得开发者无需从头实现这些复杂细节。在下一节,我们将看到如何利用这些服务来微调一个真实的世界模型。
009:整合训练 Wordle 模型 🧩

在本节课中,我们将学习如何整合之前介绍的所有概念,使用 GRPO 方法训练一个能够玩 Wordle 游戏的模型。我们将通过 Parabase SDK 设置一个强化微调任务,并比较不同模型的性能。最后,我们还将探讨如何结合监督微调来进一步提升模型效果。
设置训练流程
上一节我们介绍了奖励函数和 GRPO 损失函数的细节,本节中我们来看看如何将这些组件整合起来,训练一个玩 Wordle 的模型。
我们将使用 Parabase SDK 来设置一个强化微调任务。首先,需要定义系统提示词和用户提示词,这与第 2 和第 3 课的内容一致。系统提示词用于设定游戏规则、反馈格式以及有效回复的示例。用户提示词则包含当前游戏状态、之前的猜测、收到的反馈以及生成新猜测的明确指令。
定义好提示词后,我们将完整的提示词传递给一个 12.57B 参数的指令微调模型,并使用基于温度的采样方法生成 16 个候选回复。



# 伪代码示例:生成候选回复
prompt = system_prompt + user_prompt
candidate_responses = model.generate(prompt, num_candidates=16, temperature=0.8)
评估与奖励
生成了候选猜测后,下一步是使用三个不同的奖励函数对每个猜测进行评分。这些函数比之前看到的更为复杂,是我们在迭代改进模型以学习玩 Wordle 游戏的过程中开发的。
以下是三个奖励函数的简要说明:
- 输出格式检查:确保模型的回复包含正确的
<think>和<guess>标签,并且输出的是一个有效的五字母英文单词。 - 用户先前反馈:评估新猜测在多大程度上合理利用了之前尝试的反馈,奖励那些在先前线索基础上进行逻辑构建的猜测。
- 猜测反馈:根据一个猜测在排除可能性方面的有效性进行评分。一个猜测能从不正确的单词集合中排除的潜在单词越多,奖励越高。
如果您想了解这些函数的具体实现,请查看本课相关的 .py 文件。
最后,我们使用这些奖励分数来计算优势值,应用裁剪以防止训练不稳定,并计算 GRPO 损失来逐步更新模型。这个循环过程推动模型朝着更具策略性和更成功的 Wordle 玩法发展。
代码实现:在 Parabase 中构建训练任务
现在您已经了解了使用 RF 训练 Wordle 模型的路线图,让我们深入代码,看看如何在 Parabase 中构建它。
首先,我们需要导入必要的库和配置类。
import parabase as pb
from parabase.config import GRPOConfig, RewardFunctionsConfig
from datasets import load_dataset
接下来,由于我们通过 Parabase 进行训练,您需要注册一个 Parabase 账户,并通过提供 API 令牌登录 SDK。
pb.login(api_token="your_api_token_here")
登录后,第一步是加载用于 GRPO 训练的数据集。我们可以从 Hugging Face 加载一个数据集。
dataset = load_dataset("parabase/wordle-grpo")
该数据集包含了从过往 Wordle 游戏中选取的一组种子五字母单词,并由 GPT-3.7 等强大模型模拟游戏过程生成。我们丢弃了模型产生的最终输出,但保留了其在寻找解决方案过程中产生的中间猜测。
然后,我们可以直接从 Pandas DataFrame 将此数据上传到 Parabase。
pb.datasets.upload_from_pandas(df=dataset.to_pandas(), name="wordle_training_data")
上传数据后,下一步是创建一个新的代码仓库。一个仓库类似于 GitHub 仓库,但您可以用它在平台上跟踪所有的训练实验。我们将创建一个名为 “wordle” 的仓库。
创建好仓库并上传数据后,我们现在可以设置训练运行了。
在 GRPO 中,我们需要定义奖励函数。这已在我们的 .py 文件中完成,因此我们拥有了 guess_value、output_format_check 和 users_previous_feedback 这些奖励函数。
设置好奖励函数后,我们现在可以定义要运行的微调任务。微调任务由四部分组成:一个定义训练内容和奖励函数的配置、一个数据集、仓库以及可选的描述。
让我们放大查看定义 GRPO 训练运行配置的 GRPOConfig。
grpo_config = GRPOConfig(
base_model="Qwen2-57B-Instruct",
reward_functions=RewardFunctionsConfig(
runtime={"dependencies": ["pandas"]}, # 指定运行时依赖
functions={
"output_format": output_format_check_func,
"user_feedback": users_previous_feedback_func,
"guess_feedback": guess_feedback_func
}
),
sampling_params={
"max_tokens": 4096,
"temperature": 0.7,
"top_k": 50
},
num_generations=16
)
我们可以指定基础模型为 Qwen2-57B-Instruct。然后使用 RewardFunctionsConfig 定义我们的奖励函数集合,它包含两个可设置的属性:runtime 和 functions(一个从人类可读名称到实际函数定义的映射)。奖励函数在 Parabase 服务器上执行,因此如果它们需要可选的依赖项(如 pandas 或 OpenAI 的 LLM 作为评判),则需要在 runtime 配置中指定。
定义好奖励函数后,我们还可以设置可选的采样参数,例如最大令牌数、温度、top-k 采样等。在本例中,我们希望给模型足够的令牌来展开其思维链,因此将 max_tokens 设置为 4096。最后,我们可以根据计算预算将 num_generations 设置为 8 或 16。
一旦我们的微调任务设置完成,我们就可以运行单元格来启动训练任务。
模型性能评估
我们使用上述设置训练了一个玩 Wordle 的模型。现在,让我们看看这个模型在一组它从未见过的游戏上的表现。
我们在 10 局 Wordle 游戏上对闭源和开源模型进行了基准测试,具体测量了两个指标:模型能够解决的游戏数量,以及在已解决游戏中的平均猜测次数。
我们发现:
- GPT-4o-mini 只能解决 1 局游戏。
- Claude-3.5-Sonnet 能够解决大约 8/10 的游戏,这相当不错。
- Claude-3.7-Sonnet-Thinking 能够解决所有 10 局游戏,平均猜测次数少于 4 次,但这仅在我们给予它 8000 个令牌的“思考”预算时才成立。
- 基础的 Qwen 模型未能解决任何一局游戏。
- 当我们使用 GRPO 进行强化微调后,Qwen 模型解决了 3/10 的游戏,在已解决的游戏中平均猜测次数为 4 次。对于这个规模的模型来说,这实际上非常惊人,清楚地展示了纯奖励驱动优化带来的策略性游戏能力和效率提升。
结合监督微调以获得最佳效果
我们还可以结合监督微调和强化微调,以获得两全其美的效果。
第一步:我们首先让 Claude-3.7-Sonnet 玩 35 局 Wordle 游戏,并捕获它为每个中间猜测生成的推理轨迹。这些“提示-完成”对构成了我们的监督微调数据集,它教会模型如何以逻辑方式逐步思考其猜测。由此产生的 SFT 检查点为我们提供了进一步优化的强大初始化,本质上是一个模仿良好推理的模型。
第二步:我们使用这个 SFT 模型作为 GRPO 的起点。我们将运行与之前描述的相同的强化微调过程:生成补全、用奖励函数评分、计算优势值并更新模型。这将产生我们最终的 GRPO 检查点,它不仅优化了模仿推理,还优化了更高效地解决 Wordle 的能力。
通过将监督微调与强化微调相结合,我们的 Qwen2-57B 模型现在能够正确解决 7/10 的游戏,其性能提升超过 2 倍。
关于 GRPO 和强化学习需要记住的一点是,它是一种同策略算法。它用于帮助模型发掘自身知识,以便在下游任务中表现得更好。当您使用强大模型的输出来进行 SFT,然后使用 GRPO 来精炼这些知识时,我们经常发现,小型模型实际上能够在同一任务上击败这些更大的模型。
如果您有兴趣使用 SFT 或结合 SFT 和 GRPO 来训练模型,我们在 Parabase 的笔记本末尾提供了相关代码。
总结


本节课中,我们一起学习了如何将奖励函数、GRPO 损失和训练流程整合起来,使用 Parabase SDK 训练一个玩 Wordle 游戏的模型。我们看到了如何设置训练任务、评估模型性能,并发现了结合监督微调预热阶段可以显著提升最终模型的效果。这个过程展示了如何通过奖励驱动的优化,让语言模型学会执行复杂的、基于策略的任务。
010:总结 🎯

在本节课中,我们将对使用GRPO进行大型语言模型强化微调(RFT)的整个课程内容进行回顾与总结。
恭喜你完成了本课程的学习。
你已掌握了许多内容,从GRPO强化学习的详细基础,到创建奖励函数以引导大型语言模型在复杂任务上取得良好性能的艺术与科学。
为RFT设计奖励函数非常灵活。由于你需要从头开始构建这些函数,因此有很大的空间可以融入你自己的领域知识。
在Pratabase,我们与许多客户合作,他们正在为其业务问题构思有趣且富有创意的RFT解决方案,包括你在课程早期看到的用于文本摘要的quizta奖励函数。
如果你有兴趣进行更深入的学习,Pratabase网站上提供了更多关于RFT的学习资源。
并且,由于这项技术仍处于起步阶段,我们非常乐意听取你探索的任何用例。
我们希望你喜欢这门课程,并迫不及待想看到你的构建成果。

总结

本节课中,我们一起回顾了整个课程的核心内容。我们学习了GRPO强化学习的基础,探讨了如何设计与构建有效的奖励函数来引导模型行为,并了解了该技术在现实业务场景中的应用潜力。记住,设计奖励函数是一个融合了领域知识与创造力的过程。随着技术的不断发展,期待你能运用所学,探索并构建出属于自己的解决方案。


浙公网安备 33010602011771号