强化学习是什么?

强化学习(Reinforcement Learning, RL) 是机器学习的一个分支,目标是让智能体(agent)通过与环境(environment)的交互来学习最优的行为策略(policy),从而最大化某个累积回报(cumulative reward)

核心思想是通过试错和反馈的机制,找到在每个情境下的最优决策。

强化学习的优化目标通过选择策略来最大化累积奖励。 具体来说,智能体的目标是寻找一个最优策略 ,使得它在各个状态下的累积回报最大。形式上,可以用**价值函数(Value Function)或动作价值函数**(Q函数)来表示。

img

强化学习的要素

在强化学习中,我们通常将问题描述为一个**马尔可夫决策过程**(Markov Decision Process, MDP),它包含以下几个关键元素:

  1. 状态(State, S):智能体所处的环境状态,可能是游戏画面的一帧,也可能是机器人观测到的传感器数据。
  2. 动作(Action, A):智能体在给定状态下可以执行的一系列操作。
  3. 状态转移(Transition Dynamics, P):从当前状态s采取动作a后,会以一定的概率转移到下一个状态s’。
  4. 奖励(Reward, R):智能体在每个时刻(或每次转移)获得的即时回报,反映了该动作在此状态下的好坏。
  5. 折扣因子(Discount Factor, :用于平衡当前奖励和未来奖励的重要性,数值通常在 (0, 1] 之间。
  6. **轨迹(Trajectory):**在强化学习中,智能体从环境的初始状态开始,与环境交互直至到达终止状态所经历的一系列状态、动作以及相应的奖励就构成了一条完整的“轨迹”。
  7. **经验(Experience):**智能体可与环境交互多次,进行多次实验,形成多个轨迹。多个轨迹的集合被称为经验。
  8. **回报(Return):**强化学习的目标是学习一个好的策略,智能体按照这样的策略和环境交互,让累积奖励达到最大。这个累积奖励就是回报。通常现在的奖励和将来的奖励权重是不同的,比如今天奖励1万元和50年后奖励1万元,两者的价值大概率不同。因此会加上折扣因子。

其中折旧因子用于平衡当前奖励和未来奖励的重要性。如果没有折扣,那么回报就是从时间步开始直到回合结束所获得的总奖励。在有了回报概念后,我们知道,强化学习的目标是最大化期望回报

  1. 价值函数(Value Function)价值函数衡量的是“期望回报”。具体分为状态价值函数和动作价值函数。
  • 状态价值函数表示在状态 s 下,后续按照策略行动所能获得的期望回报
  • 动作价值函数表示在状态 s 下执行动作 a,并在之后按照策略行动所能获得的期望回报
  • V用来评价在某个状态下的策略表现好坏。Q用来评价在某个状态-动作对下的策略表现好坏。价值的估计至关重要,下文还会讨论。
  1. **优势函数(Advantage Function)****:优势函数度量的是,在给定状态 s 下,执行某个动作 a 比起在该状态的平均水平(即状态价值)好多少或差多少。它的常见形式是:
  • 如果 大于 0,说明在状态 s 下执行动作 a 要比该状态的平均策略价值要好。
  • 如果 小于 0,则说明这个动作比“平均”水平要差。
  1. 探索(Exploration)和学习(Learning):强化学习一般分为两个阶段。第一个是探索阶段,智能体先按照某些策略和环境进行交互,形成经验。第二个是学习阶段,智能体按照某些算法,从经验中学习,进而优化自己的策略
  2. 行为策略(Behavior Policy)和目标策略(Target Policy):行为策略是智能体在环境交互时实际执行的策略。目标策略是智能体最终想要学到的策略。两个策略相同就是on-policy,否则就是off-policy,这点我们在下文中会详细探讨。

A、Q、V的关系

优势函数(A)与值函数(V)、动作值函数(Q)的区别的联系:优势函数是Q和V的差值。A=Q-V

优势函数与值函数、动作值函数的区别:

  • 值函数(V):衡量一个状态的价值,反映了在某个状态下,智能体根据当前策略所能期望得到的总回报。
  • 动作值函数(Q):直接衡量在某状态下采取某动作后,智能体所能期望得到的总回报。
  • 优势函数(A):是一个动作对比的度量,表明选择某个动作相较于平均策略的回报增益。和V、Q不同的是,A提供了一个相对的评价。

估计值函数的三种方法:MC、TD、GAE

RL中,价值的估计非常重要。估计值函数的常见方法有三种:蒙特卡洛(MC)、时间差分(TD)、广义优势估计(GAE)。

它们在估计值函数时各有优缺点,具体在方差和偏差之间存在不同的权衡。

1. 蒙特卡洛方法(MC)

原理:蒙特卡洛方法通过在完整的轨迹上计算回报(即从当前状态开始到最终状态的累积奖励)来估计状态值函数或动作值函数。这个方法完全依赖于最终的回报,因此需要等待完整的路径(完整的Episode)来计算值。

优点

  • 不需要任何模型假设,直接依赖实际回报。
  • 在长期内是无偏的(即期望值是正确的),因为它直接使用实际的累积奖励。

缺点

  • 高方差:由于只使用每个Episode的最终回报,导致每个回报的估计可能存在较大波动,尤其是在奖励信号稀疏或变动较大的情况下,估计的方差较大。

总结:MC方法因为依赖完整的回报,所以它的估计有较大的方差,但没有偏差。

2. 时序差分方法(TD)

原理:时序差分方法则是通过递推更新(也称为bootstrapping)来估计值函数,它并不需要等待完整的Episode,而是根据每一步的即时反馈进行更新。TD方法将当前的估计值下一时刻的估计值进行比较,通过差分来更新当前状态的值。

优点

  • 较低的方差:由于TD方法使用每一步的即时反馈,它不依赖于完整Episode的回报,估计过程可以在更短时间内进行,因而方差较小。
  • 可以在线学习,不需要等待完整的Episode。

缺点

  • 高偏差:由于TD方法使用的是估计值而非真实回报,它会引入一定的偏差。特别是它依赖当前的估计来更新,因此如果初始估计有偏,后续的更新也会继承这个偏差。

总结:TD方法具有较低的方差,但它引入了偏差,因为它依赖于现有的估计,而非实际的回报。

3. 广义优势估计(GAE)

原理:GAE是一种折中方法,它结合了MC和TD的方法,旨在通过平衡方差和偏差来提高估计的稳定性和效率。GAE通过引入一个超参数 (类似于TD方法中的折扣因子),在计算优势函数时利用TD方法的部分信息,而不是完全依赖于真实的回报。

具体来说,GAE通过对TD误差进行加权平均来估计优势函数,从而减少单步TD误差带来的偏差,并控制方差的大小。其中, 表示每一步的TD误差, 是折扣因子, 是用于加权的超参数,控制了TD和MC的折中。

优点

  • 在方差和偏差之间取得了更好的折中。通过调节 ,GAE可以灵活控制偏差和方差之间的权衡。

缺点

  • 相较于纯粹的TD或MC,GAE需要更多的计算,因为它需要在每一步计算加权的TD误差。

总结:GAE通过加权TD误差,能够在MC的低偏差和TD的低方差之间取得一个折中。

一个简单的例子:假设我们有一个机器人学习在迷宫中导航。值函数表示从某个位置开始,机器人能获得多少总奖励。

  • MC方法:让机器人多次从起点走到终点,记录每次的总奖励,然后平均这些奖励来估计每个位置的值。这就像考试后看总分,但每次考试难度不同,所以分数波动大(高方差)。
  • TD方法:机器人每走一步就更新值估计。例如,从位置A走到位置B,获得奖励R,然后使用位置B的当前值来更新位置A的值。这就像在考试中每答一题就估计最终分数,但估计可能不准(有偏差),但波动小(低方差)。
  • GAE方法:结合多步的奖励和值估计,通过λ控制看多远。λ=0时纯TD,λ=1时纯MC。这就像在考试中不仅看当前题目的得分,还考虑后续题目的估计,但调整权重。

最后,总结它们的权衡:

  • MC:高方差,无偏差
  • TD:低方差,有偏差
  • GAE:可调偏差和方差

NLP中的RL

img

**状态S:**输入prompt

**动作A:**输出response(即LLM输出下一个token)

**奖励R:**根据prompt+response进行奖励模型打分

整体目标:给定prompt,调整policy,生成符合人类喜好(RM偏序信号)的response

回想一下我们对NLP任务做强化学习(RLHF)的目的:我们希望给模型一个prompt,让模型能生成符合人类喜好的response。再回想一下gpt模型做推理的过程:每个时刻 只产生一个token,即token是一个一个蹦出来的,先有上一个token,再有下一个token。

复习了这两点,现在我们可以更好解读上面这张图了:

  • 我们先喂给模型一个prompt,期望它能产出符合人类喜好的response
  • 在t时刻,模型根据上文,产出一个token,**这个token即对应着强化学习中的动作,我们记为A(t)。因此不难理解,在NLP语境下,强化学习任务的动作空间就对应着词表。
  • 在t时刻,模型产出token A(t)对应着的即时收益为R(t),总收益为(**复习一下, 蕴含着“即时收益”与“未来收益”两个内容)。这个收益即可以理解为“对人类喜好的衡量”。此刻,**模型的状态从S(t)变为S(t+1),也就是从“上文”变成“上文 + 新产出的token”
  • 在NLP语境下,智能体是语言模型本身,环境则对应着它产出的语料

经典强化学习算法的分类

img

RL中的on-policy 和 off-policy有什么区别?

在强化学习中,on-policyoff-policy 的区别,核心在于“数据采样的策略(Policy)与学习的策略是否相同”。

在这里插入图片描述

方面On-policyOff-policy
数据来源必须来自当前模型策略生成可以来自旧模型、人类写的样本或其他模型
典型方法PPO(RLHF 主流)Q-learning 风格、离线 RL
样本效率低(采样一次只能用一次)高(历史数据可反复利用)
稳定性更稳定,偏保守更高效,但不稳定性更大
应用场景RLHF 的 policy 微调奖励模型训练、offline RLHF、结合多源数据的对齐

RL中的online和offline有什么区别?

特性Online RL(在线)Offline RL(离线)
核心区别边交互边学习从固定数据集中学习
数据来源智能体自己实时生成预先收集的静态数据集
与环境交互,持续交互,学习期间零交互
别名交互式学习批量学习(Batch RL)
优点数据新鲜,适应性强,能主动探索安全,成本低,可利用历史数据
缺点试错成本高,可能不安全,采样效率可能低受数据质量限制,存在分布外(OOD)问题
类比学开车:亲自上路练习学开车:看驾驶教学录像

RLHF(RM+PPO)

RLHF 的全称是 Reinforcement Learning from Human Feedback,即基于人类反馈的强化学习

RLHF的整体流程是什么?

RLHF主要分为奖励模型训练和**近端策略优化(Proximal Policy Optimization, PPO)**两个步骤。

  • 奖励模型通过由人类反馈标注的偏好数据来学习人类的偏好,判断模型回复的有用性以及保证内容的无害性。奖励模型模拟了人类的偏好信息,能够不断地为模型的训练提供奖励信号。
  • 在获得奖励模型后,需要借助强化学习对语言模型继续进行微调。近端策略优化可以根据奖励模型获得的反馈 优化模型,通过不断的迭代,让模型探索和发现更符合人类偏好的回复策略。

RLHF一般包括三个步骤:

  1. 有监督微调(SFT):使用高质量的对话数据对预训练模型进行微调,得到一个初始的对话模型。
  2. 奖励模型训练(RM):使用人类标注的偏好数据训练一个奖励模型,能够对模型生成的回答进行评分。
  3. 强化学习微调(RL):使用强化学习算法(如PPO)对SFT模型进行微调,以最大化奖励模型的得分,同时防止模型偏离SFT模型太远。在第三步RL中,我们会同时使用Reference Model和Actor Model。

RLHF中有哪些数据集?

RLHF中有三种数据集,分别是:

  • 偏好数据集:用来训练奖励模型。
  • 提示数据集:在PPO流程中用来训练LLM。
  • 评估数据集:用来评估RLHF的效果。

RLHF中的四个重要角色

img

如上图,在RLHF-PPO阶段,一共有四个主要模型,分别是:

  • Actor Model:演员模型,这就是我们想要训练的目标语言模型
  • Critic Model:评论家模型,它的作用是预估总收益
  • Reward Model:奖励模型,它的作用是计算即时收益
  • Reference Model:参考模型,它的作用是在RLHF阶段给语言模型增加一些“约束”,防止语言模型训歪(朝不受控制的方向更新,效果可能越来越差)
  1. 策略模型(Policy Model,或Actor Model),生成模型回复。它就是RLHF训练希望产出的模型。在PPO训练中更新梯度。
  2. 奖励模型(Reward Model),输出奖励分数来评估回复质量的好坏。在PPO训练中不更新梯度。
  3. 评论模型(Critic Model,或Value Network),来预测回复的好坏,可以在训练过程中实时调整模型,选择对未来累积收益最大的行为。在PPO训练中更新梯度。
  4. **参考模型(Reference Model)**提供了一个 SFT 模型的备份,帮助模型不会出现过于极端的变化。在PPO训练中不更新梯度。

其中:

  • Actor/Critic Model在RLHF阶段是需要训练的(图中给这两个模型加了粗边,就是表示这个含义);而Reward/Reference Model参数冻结的。
  • Critic/Reward/Reference Model共同组成了一个“奖励-loss”计算体系(为了方便理解),我们综合它们的结果计算loss,用于更新Actor和Critic Model
Actor Model (演员模型)

正如前文所说,Actor就是我们想要训练的目标语言模型。我们一般用SFT阶段产出的SFT模型来对它做初始化。

img

我们的最终目的是让Actor模型能产生符合人类喜好的response。所以我们的策略是,先喂给Actor一条prompt (这里假设batch_size = 1,所以是1条prompt),让它生成对应的response。然后,我们再将“prompt + response"送入我们的“奖励-loss”计算体系中去算得最后的loss,用于更新actor。

Reference Model(参考模型)

**Reference Model(以下简称Ref模型)一般也用SFT阶段得到的SFT模型做初始化,在训练过程中,它的参数是冻结的。**Ref模型的主要作用是防止Actor”训歪”,那么它具体是怎么做到这一点的呢?

img

“防止模型训歪”换一个更详细的解释是:我们希望训练出来的Actor模型既能达到符合人类喜好的目的,又尽量让它和SFT模型不要差异太大。简言之,我们希望两个模型的输出分布尽量相似。那什么指标能用来衡量输出分布的相似度呢?我们自然而然想到了KL散度KL散度 = Actor模型的log_prob - Reference模型的log_prob

在这里插入图片描述

注:你可能已经注意到,按照KL散度的定义,这里写成log_probs - ref_log_probs更合适一些。但是如果你看过一些rlhf相关的论文的话,你可能记得在计算损失函数时,有一项 散度 (对这个有疑惑不要紧,我们马上在后文细说),即KL散度前带了负号,所以这里我写成ref_log_probs - log_probs这样的形式,更方便大家从直觉上理解这个公式。

现在,我们已经知道怎么利用Ref模型和KL散度来防止Actor训歪了。KL散度将在后续被用于loss的计算,我们在后文中会详细解释。

理解基础:log_prob
  1. 概率是什么?

当语言模型生成文本时,在每个位置,它都会从词汇表中选择一个词(token)。比如:

  • 输入:“今天天气”
  • 模型可能输出:“很好”(概率0.4)、“不错”(概率0.3)、“糟糕”(概率0.2)等

这里的0.4、0.3、0.2就是概率 - 模型认为各个选项的可能性。

  1. 为什么要用log_prob?

问题:概率值很小,连乘会数值下溢!

  • 假设生成10个token,每个概率0.1
  • 总概率 = 0.1 × 0.1 × … × 0.1 = 0.0000000001(非常小!)

解决方案:取对数!

  • log(0.1) = -2.3026
  • 10个token的总log_prob = -2.3026 × 10 = -23.026
  • 数值稳定,便于计算!

log_prob就是概率的对数值,是深度学习中的标准做法。

假设我们让Actor模型生成回答:“我喜欢RL”。Actor模型的log_probs:

输入prompt: "解释强化学习"
生成过程:
- "我"的log_prob: -1.2   (对应概率≈0.30)
- "喜"的log_prob: -0.8   (对应概率≈0.45)  
- "欢"的log_prob: -0.9   (对应概率≈0.41)
- "R"的log_prob: -2.1    (对应概率≈0.12)
- "L"的log_prob: -1.8    (对应概率≈0.17)
Critic Model(评论家模型)

Critic Model用于预测期望总收益 ,和Actor模型一样,它需要做参数更新。实践中,Critic Model的设计和初始化方式也有很多种,例如和Actor共享部分参数、从RW阶段的Reward Model初始化而来等等。我们讲解时,和deepspeed-chat的实现保持一致:从RW阶段的Reward Model初始化而来。

你可能想问:训练Actor模型我能理解,但我还是不明白,为什么要单独训练一个Critic模型用于预测收益呢?
这是因为,当我们在前文讨论总收益 (即时 + 未来)时,我们是站在上帝视角的,也就是客观存在的、真正的总收益。但是我们在训练模型时,就没有这个上帝视角加成了,也就是在t时刻,我们给不出客观存在的总收益**,我们只能训练一个模型去预测它。**

所以总结来说,在RLHF中,我们不仅要训练模型生成符合人类喜好的内容的能力(Actor),也要提升模型对人类喜好量化判断的能力(Critic)。这就是Critic模型存在的意义。我们来看看它的大致架构:

img

deepspeed-chat采用了Reward模型作为它的初始化,所以这里也按Reward模型的架构来简单画画它。你可以简单理解成,Reward/Critic模型和Actor模型的架构是很相似的(毕竟输入都一样),同时,它在最后一层增加了一个Value Head层,该层是个简单的线形层,用于将原始输出结果映射成单一的Vt值。

在图中,Vt表示Critic模型对t时刻及未来(response完成)的收益预估。

Reward Model(奖励模型)

Reward Model用于计算生成token 的即时收益,它就是RW阶段所训练的奖励模型,在RLHF过程中,它的参数是冻结的。

你可能想问:为什么Critic模型要参与训练,而同样是和收益相关的Reward模型的参数就可以冻结呢?
这是因为,Reward模型是站在上帝视角的。这个上帝视角有两层含义:

  • 第一点,Reward模型是经过和“估算收益”相关的训练的,因此在RLHF阶段它可以直接被当作一个能产生客观值的模型。
  • 第二点,Reward模型代表的含义就是“即时收益”,你的token已经产生,因此即时收益自然可以立刻算出。

通俗理解:

Reward Model

  1. 客观标准的守护者
  • Reward Model在RW阶段已经学会了"什么是好回答"
  • 它就像考试评分标准:一旦制定,在整个考试期间保持不变
  • 如果我们在RLHF过程中也更新Reward Model,就相当于在考试中不断修改评分标准,那学习目标就混乱了!
  1. 即时收益的计算器
  • Reward Model只看当前生成的文本,给出一个最终得分
  • 它回答:“这个回答质量如何?” → 给出分数
  • 不需要考虑"接下来会怎样",所以不需要随着Actor的变化而调整

Critic Model

  1. 价值预估的专家
  • Critic要预测:“从这个状态开始,期望能获得多少累积奖励?”
  • 它回答:“如果我现在处在对话的这个位置,预计最终能得多少分?”
  1. 需要适应变化的策略
  • 当Actor模型不断更新时,生成策略在变化
  • 同样的对话状态,不同策略下的未来收益预期完全不同
  • 所以Critic必须跟着训练,才能准确预估当前策略的价值

在这里插入图片描述

通俗的讲解一下:

  • 奖励模型(Reward Model)是预先训练好的,用于提供即时奖励,它不需要随着策略的更新而更新,因为人类的偏好标准是固定的。
  • Critic模型则需要随着Actor策略的更新而不断训练,以准确估计当前策略下的累积收益。

RLHF中的loss计算

到目前为止,我们已经基本了解了RLHF的训练框架,以及其中的四个重要角色(训练一个RLHF,有4个模型在硬件上跑,可想而知对存储的压力)。在本节中,我们一起来解读RLHF的loss计算方式。在解读中,我们会再一次理一遍RLHF的整体训练过程,填补相关细节。在这之后,我们就可以来看代码解析了。

在四个角色的讲解中,我们知道Actor和Critic模型都会做参数更新,所以我们的loss也分成2个:

  • **Actor loss:**用于评估Actor是否产生了符合人类喜好的结果,将作用于Actor的BWD上。
  • **Critic loss:**用于评估Critic是否正确预测了人类的喜好,将作用于Critic的BWD上。
Actor loss
(1)直观设计

在这里插入图片描述

(2)引入优势

在这里插入图片描述

(3)重新设计Rt

在这里插入图片描述

img

在这里插入图片描述

代码实践如下:

def compute_rewards(self, prompts, log_probs, ref_log_probs, reward_score,
                        action_mask):
        """
        reward_function:计算最终的reward分数
        复习一下几个相关参数的默认值:
        self.kl_ctl = 0.1
        self.clip_reward_value = 5
        
        对于batch中的某个prompt来说,它最终的reward分数为:
        (1) 先计算actor和ref_model的logit相似度: -self.kl_ctl * (log_probs - ref_log_probs)
            其实写成self.kl_ctl * (ref_log_probs - log_probs)更好理解些
            这个值越大,说明ref_model对actor生成的结果的认可度越高(即表明rlhf没有训歪),
            没有训歪的情况下我们也应该给模型一些奖励,这个奖励就是self.kl_ctl * (ref_log_probs - log_probs)
            
        (2)由于我们只取最后一个token对应位置的分数作为reward_score,因此我们只需要:
            self.kl_ctl * (ref_log_probs - log_probs)的最后一位 + reward_score
         
         (3) 同时我们对reward_score也做了大小限制,最大不超过self.clip_reward_value(超过统一给成self.clip_reward_value),
             最小不低于-self.clip_reward_value(低于统一给成-self.clip_reward_value)
        
         (4) 最后返回的rewards大小为:(batch_size, 各条数据的长度),对batch中的每条数据来说:
             - response的最后一位:self.kl_ctl * (ref_log_probs - log_probs)的最后一位 + reward_score
             - response的其余位置:self.kl_ctl * (ref_log_probs - log_probs)
        
        """

        kl_divergence_estimate = -self.kl_ctl * (log_probs - ref_log_probs)
        rewards = kl_divergence_estimate
        # ---------------------------------------------------------------------------------------------------
        # response开始的位置
        # (因为我们对prompt做过padding处理,因此batch中每个prompt长度一致,也就意味着每个response开始的位置一致)
        # (所以这里start是不加s的,只是一个int)
        # ---------------------------------------------------------------------------------------------------
        start = prompts.shape[1] - 1
        # ---------------------------------------------------------------------------------------------------
        # response结束的位置
        # (因为一个batch中,每个response的长度不一样,所以response的结束位置也不一样)
        # (所以这里end是加s的,ends的尺寸是(batch_size,)
        # ---------------------------------------------------------------------------------------------------
        ends = start + action_mask[:, start:].sum(1) + 1
        # ---------------------------------------------------------------------------------------------------
        # 对rewards_score做限制
        # ---------------------------------------------------------------------------------------------------
        reward_clip = torch.clamp(reward_score, -self.clip_reward_value,
                                  self.clip_reward_value)
        batch_size = log_probs.shape[0]
        for j in range(batch_size):
            rewards[j, start:ends[j]][-1] += reward_clip[j] # 

        return rewards
(4)重新设计优势

在这里插入图片描述

在这里插入图片描述

def get_advantages_and_returns(self, values, rewards, start):
        """
        Adopted from https://github.com/CarperAI/trlx/blob/main/trlx/models/modeling_ppo.py#L134
        
        没有引入GAE前的t时刻的优势值:
        detal_t = r_t + gamma * V_t+1 - V_t
        其中:
            - r_t表示t时刻的即时收益
            - V_t+1表示未来时刻的预期收益
            - r_t + gamma * V_t+1可理解成t时刻的实际预期收益
            - V_t可理解成t时刻的预估预期收益(是模型,例如critic model自己估算出来的)
        
        引入GAE后的t时刻的优势值:
        A_t = delta_t + gamma * lambda * A_t+1
        粗暴理解为在t时刻时,不仅考虑当下优势,还考虑了未来的优势
        为了知道A_t, 我们得知道A_t+1,所以在本算法中采取了从后往前做动态规划求解的方法,也即:
        假设T是最后一个时刻,则有A_T+1 = 0, 所以有: A_T = delta_T
        知道了A_T, 就可以依次往前倒推,把A_t-1, A_t-2之类都算出来了
        
        引入GAE后t时刻的实际预期收益
        returns_t = A_t + V_t
                  = delta_t + gamma * lambda * A_t+1 + V_t
                  = r_t + gamma * V_t+1 - V_t + gamma * lambda * A_t+1 + V_t
                  = r_t + gamma * (V_t+1 + lambda * A_t+1)
        
        注意,这里不管是advantages还是returns,都只算response的部分
        """
        
        # Adopted from https://github.com/CarperAI/trlx/blob/main/trlx/models/modeling_ppo.py#L134
        lastgaelam = 0
        advantages_reversed = []
        length = rewards.size()[-1]
        # 注意这里用了reversed,是采取从后往前倒推计算的方式
        for t in reversed(range(start, length)):
            nextvalues = values[:, t + 1] if t < length - 1 else 0.0
            delta = rewards[:, t] + self.gamma * nextvalues - values[:, t]
            lastgaelam = delta + self.gamma * self.lam * lastgaelam
            advantages_reversed.append(lastgaelam)
        advantages = torch.stack(advantages_reversed[::-1], dim=1) # 优势
        returns = advantages + values[:, start:] # 实际收益
        # values: 预期收益
        return advantages.detach(), returns
(5)PPO-epoch: 引入新约束

在这里插入图片描述

img

  • 第一步,我们准备一个batch的prompts
  • 第二步,我们将这个batch的prompts喂给Actor模型,让它生成对应的responses
  • 第三步,我们把prompt+responses喂给我们的Critic/Reward/Reference模型,让它生成用于计算actor/critic loss的数据,按照强化学习的术语,我们称这些数据为经验(experiences)。critic loss我们将在后文做详细讲解,目前我们只把目光聚焦到actor loss上
  • 第四步,我们根据这些经验,实际计算出actor/critic loss,然后更新Actor和Critic模型

这些步骤都很符合直觉,但是细心的你肯定发现了,文字描述中的第四步和图例中的第四步有差异:图中说,这一个batch的经验值将被用于n次模型更新,这是什么意思呢?

我们知道,在强化学习中,收集一个batch的经验是非常耗时的。对应到我们RLHF的例子中,收集一次经验,它要等四个模型做完推理才可以,正是因此,一个batch的经验,只用于计算1次loss,更新1次Actor和Critic模型,好像有点太浪费了。

所以,**我们自然而然想到,1个batch的经验,能不能用来计算ppo-epochs次loss,更新ppo-epochs次Actor和Critic模型?**简单写一下伪代码,我们想要:

# --------------------------------------------------------------
# 初始化RLHF中的四个模型
# --------------------------------------------------------------
actor, critic, reward, ref = initialize_models()

# --------------------------------------------------------------
# 训练
# --------------------------------------------------------------
# 对于每一个batch的数据
for i in steps: 
    # 先收集经验值
    exps = generate_experience(prompts, actor, critic, reward, ref)
    # 一个batch的经验值将被用于计算ppo_epochs次loss,更新ppo_epochs次模型
    # 这也意味着,当你计算一次新loss时,你用的是更新后的模型
    for j in ppo_epochs:
        actor_loss = cal_actor_loss(exps, actor)
        critic_loss = cal_critic_loss(exps, critic)
        
        actor.backward(actor_loss)
        actor.step()
        
        critc.backward(critic_loss)
        critic.step()

**而如果我们想让一个batch的经验值被重复使用ppo_epochs次,等价于我们想要Actor在这个过程中,模拟和环境交互ppo_epochs次。**举个例子:

  • 如果1个batch的经验值只使用1次,那么在本次更新完后,Actor就吃新的batch,正常和环境交互,产出新的经验值
  • 但如果1个batch的经验值被使用ppo_epochs次,在这ppo_epochs中,Actor是不吃任何新数据,不做任何交互的,所以我们只能让Actor“模拟”一下和环境交互的过程,吐出一些新数据出来。

在这里插入图片描述
在这里插入图片描述

整体代码如下:

def actor_loss_fn(self, logprobs, old_logprobs, advantages, mask):
        """
        logprobs: 实时计算的,response部分的prob(只有这个是随着actor实时更新而改变的)
        old_logprobs:老策略中,response部分的prob (这个是固定的,不随actor实时更新而改变)
        advantages: 老策略中,response部分每个token对应的优势(这个是固定的,不随actor实时更新而改变)
        mask:老策略中,response部分对应的mask情况这个是固定的,不随actor实时更新而改变)
        
        之所以要引入logprobs计算actor_loss,是因为我们不希望策略每次更新的幅度太大,防止模型训歪
        
        self.cliprange: 默认值是0.2
        """
        ## policy gradient loss
        # -------------------------------------------------------------------------------------
        # 计算新旧策略间的KL散度
        # -------------------------------------------------------------------------------------
        log_ratio = (logprobs - old_logprobs) * mask
        ratio = torch.exp(log_ratio)
        # -------------------------------------------------------------------------------------
        # 计算原始loss和截断loss
        # -------------------------------------------------------------------------------------
        pg_loss1 = -advantages * ratio
        pg_loss2 = -advantages * torch.clamp(ratio, 1.0 - self.cliprange, 1.0 + self.cliprange)
        pg_loss = torch.sum(torch.max(pg_loss1, pg_loss2) * mask) / mask.sum() # 最后是取每个非mask的response token的平均loss作为最终loss
        return pg_loss
(6)Actor loss小结

在这里插入图片描述

Critic loss

我们知道,1个batch产出的经验值,不仅被用来更新Actor,还被用来更新Critic。对于Critic loss,我们不再像Actor loss一样给出一个“演变过程”的解读,我们直接来看它最后的设计。

首先,在之前的解说中,你可能有这样一个印象:

在这里插入图片描述

在这里插入图片描述

代码如下:

def critic_loss_fn(self, values, old_values, returns, mask):
        """
        values: 实时critic跑出来的预估预期收益(是变动的,随着ppo epoch迭代而改变)
        old_values:老critic跑出来的预估预期收益(是固定值)
        returns:实际预期收益
        mask:response部分的mask
        
        self.cliprange_value = 0.2
        """
        ## value loss
        # 用旧的value去约束新的value
        values_clipped = torch.clamp(
            values,
            old_values - self.cliprange_value,
            old_values + self.cliprange_value,
        )
        if self.compute_fp32_loss:
            values = values.float()
            values_clipped = values_clipped.float()
        
        # critic模型的loss定义为(预估预期收益-实际预期收益)**2
        vf_loss1 = (values - returns)**2
        vf_loss2 = (values_clipped - returns)**2
        vf_loss = 0.5 * torch.sum(
            torch.max(vf_loss1, vf_loss2) * mask) / mask.sum() # 同样,最后也是把critic loss平均到每个token上
        return vf_loss

PPO的流程

PPO的实施流程如下:

  1. 环境采样:策略模型基于给定输入生成一系列的回复,奖励模型则对这些回复进行打分获得奖励。
  2. 优势估计:利用评论模型预测生成回复的未来累积奖励,并借助广义优势估计(Generalized Advantage Estimation,GAE)算法来估计优势函数,能够有助于更准确地评估每次行动的好处。
  3. 优化调整:使用优势函数来优化和调整策略模型,同时利用参考模型确保更新的策略不会有 太大的变化,从而维持模型的稳定性。

img

作为PPO流程的补充,参考下面这张图,可以划分为经验采样训练两个阶段。

第一阶段经验采样,也叫做rollout,就是滑跑的意思,在RL中可以理解为一次实验,也就是actor模型在当前环境下进行策略动作的过程,具体实现上就是actor模型根据prompt数据集进行generate生成response,然后根据prompt+response进行forward计算,得到logp/values/reward等元素,这里涉及到actor、reference、critic、reward 4个模型的推理过程

第二阶段就是训练流程,涉及到actor和critic两个模型,从计算loss来看,算法上是相互独立的,本质上是两个模型独立训练。

img

make_experience和rollout、Episode有什么区别与联系

在RLHF-PPO的语境下,这三个术语都涉及生成和收集与环境交互的数据,但侧重点略有不同:

  • make_experience:这是一个泛指的说法,描述了让模型与环境交互、生成经验数据的整个过程。它强调“制造经验”,也就是收集状态、动作、奖励等数据,为后续训练提供素材。
  • rollout:特指利用当前策略模型在环境中生成一段轨迹的过程。这段轨迹可以是完整的,也可以只是部分数据。Rollout更侧重于“展开”策略执行的具体过程,用来评估策略或计算优势等。
  • episode:指的是从环境的初始状态开始,到达到终止条件为止的一整段交互过程,是一种完整的轨迹。它是rollout的一种特殊情况,即rollout生成的是一个完整的体验序列。

联系与区别

  • 联系:三者都涉及通过策略与环境交互来收集数据。通常,make_experience的过程可能就是通过执行rollout来实现,而如果rollout生成的是从开始到结束的完整轨迹,那么它就构成了一个episode。
  • 区别:make_experience是一个更宽泛的概念,强调整个数据收集过程;rollout强调的是生成轨迹的动作,不一定要求轨迹完整;而episode专指完整的体验序列。

总的来说,make_experience包含了通过rollout生成经验,而rollout有时可能只是一部分episode,三者在数据生成过程中各有侧重。

DPO

DPO的基本原理:增加偏好样本的对数概率与减小非偏好样本响应的对数概率。它结合了动态加权机制,以避免仅使用概率比目标时遇到的模型退化问题。

img

DPO在优化人类偏好时避免了使用强化学习。现有RLHF方法通常会首先使用一个奖励模型(Reward Model)来拟合一个包含提示(Prompt)和人类对响应对(Response Pair)偏好的数据集,然后通过强化学习找到一个能够最大化该奖励模型的策略(Policy)。相比之下,DPO 直接以简单的分类目标优化最能满足偏好的策略,通过拟合一个隐式奖励模型,其对应的最优策略可以以闭式形式(Closed Form)提取出来。

GRPO

GRPO是对PPO的一种改进版本,属于online RL。它通过暴力采样求均值的方式替代了PPO中的Critic Model,同时保留了PPO中的重要性采样和裁剪机制。GRPO中冻结了Ref和RM 2个模型,仅需要训练Policy Model。

img

GRPO的目标函数:

img

DAPO

这是字节跳动seed团队对GRPO的重要改进。GRPO 面临四个主要问题:其单一剪切范围易导致低概率 Token 无法得到有效提升(熵坍塌),样本级别损失会弱化长序列 Token 的梯度贡献,一旦所有输出都是全对或全错则无梯度信号可用,且统一惩罚过长生成会引入噪声

DAPO 则通过“四大改进”逐一解决:

  • Clip-Higher 将剪切上下限分开以保留多样性;
  • Dynamic Sampling 过滤全对/全错样本保持有效梯度;
  • Token-Level Policy Gradient Loss 平衡长序列 Token 贡献;
  • Overlong Reward Shaping 为过长文本进行柔性惩罚或掩码,减少噪声干扰。

Reference

图解大模型RLHF系列之:人人都能看懂的PPO原理与源码解读 https://zhuanlan.zhihu.com/p/677607581

大模型中的强化学习 https://zhuanlan.zhihu.com/p/693582342

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐