学强化学习的同学到RLHF阶段,绕不开演员-评论家算法,也就是Actor-Critic。配合TD误差和策略梯度,正好是PPO那套更新逻辑的底层。很多人一看到这几个词,以为又冒出来一个新算法。其实RLHF不是把语言模型换成另一个强化学习模型,而是在已有奖励模型的基础上,用强化学习算法去微调策略模型。PPO就是目前最常用的一种策略优化器,它的框架本身就属于Actor-Critic。下面我会从RLHF的问题入手,拆解Actor-Critic的结构、TD误差的来龙去脉,以及actor和critic更新公式的推导过程,最后补一些实际训练时容易踩的坑。
1. RLHF里的策略优化为什么是Actor-Critic
1.1 RLHF没有重新发明强化学习算法
RLHF的完整流程一般包括四段:先有一个预训练语言模型,再做有监督指令微调,然后训练一个奖励模型,最后用强化学习算法根据奖励模型调整策略。前两步为后面的训练准备基础,第三步把人类偏好压缩成标量奖励,第四步才是真正的强化学习优化环节。
很多初学者会以为RLHF内部包含一种特殊的“RLHF算法”。实际上,RLHF是一个训练范式,它依赖的强化学习算法更多是PPO、REINFORCE等常见方法。PPO全称是近端策略优化,是Actor-Critic框架下的一种实现。所以理解RLHF,其实要先理解Actor-Critic,否则看PPO的loss时,根本不知道每个项从哪里来。
这个环节解决的问题很具体:奖励模型已经可以给一个回复打分了,但我们不能直接用梯度上升去更新语言模型。奖励模型不是语言模型生成器的可微函数。你输入文本,拿到标量,中间隔着离散token采样,整体不可导。所以只能把语言模型当成一个策略网络,通过采样生成回复,再用奖励分数和优势估计去调整分布。这就是强化学习在RLHF中的位置。
1.2 演员生成动作,评论家评估状态
先给Actor-Critic一个直观理解。actor是策略网络,在RLHF里就是被微调的语言模型,负责根据输入决定下一步生成什么token。critic是价值网络,负责预测当前状态最终能拿到多少未来奖励,输出是一个标量。actor像运动员,critic像教练。教练不会替运动员打球,但会根据场上情况判断一个动作是否值得做。
训练时,actor先采样动作,critic对该动作给出价值判断,两者结合起来更新。critic的价值估计能起到baseline的作用,减少actor策略梯度的方差。如果不使用baseline,actor更新时会非常不稳定。因为同一动作在不同轨迹中的回报差异很大,很容易把原本不错的动作误判成差动作。
在语言模型场景中,actor和critic的输入通常一样:一段prompt和已经生成的token序列。actor输出下一个token的概率分布,critic输出当前状态的价值。两者输出维度不同,这一点第一次接触代码时很容易搞混。
2. 把强化学习术语翻译成文本生成任务
2.1 状态、动作、奖励分别对应什么
在标准强化学习里,智能体每一步观察状态,选择动作,环境反馈奖励并转移状态。放到RLHF的文本生成任务中,每个元素都有对应关系。
- 状态s:对话历史加上模型已经生成的部分,例如“用户提问:...\n助手:...”。
- 动作a:在某个位置的下一步选择。如果按token级别拆分,动作就是下一个token;如果按句子级别拆分,动作就是完整回复。PPO实现中通常按token级别的概率计算,方便对齐损失函数。
- 奖励r:奖励模型给出的分数,表示这段回复被人类偏好的程度。中间每个token不一定都有奖励,通常整段回复结束时才给一个最终分数,中间步骤奖励为0。
- 折扣因子γ:控制未来奖励的衰减。因为中间奖励稀疏,γ一般取接近1的值,让模型把整段回复的结果回溯到每一步。
理解这个映射很重要。很多人写代码时不知道actor该输出什么、critic该输出什么,就是因为没有把token序列看作一个马尔可夫决策过程。
2.2 策略、状态价值、动作价值和优势
策略π_θ(a|s)就是语言模型在给定上下文时输出下一个token的概率分布。θ是模型参数。
状态价值V(s)表示从状态s开始,按照当前策略继续生成,到最后能获得的期望折扣回报。critic要逼近的正是这个V(s)。动作价值Q(s,a)则是在状态s下选择了动作a之后,再按当前策略继续生成,能获得的期望折扣回报。
有了这两个价值,就能定义优势函数:
$A(s,a) = Q(s,a) - V(s)$
直观含义是:在当前状态下选择动作a,相比于按当前策略的平均水平,到底好多少。如果优势大于0,说明这个动作值得提高概率;如果小于0,说明这个动作比平均差,应该降低概率。
状态价值和动作价值不一样。状态价值把当前状态下所有动作按策略概率加权平均,动作价值是针对某个确定动作的。实际计算中不需要显式估计Q,可以通过TD误差或GAE来逼近优势,这是下一部分的内容。
3. TD误差:连接“预测”和“现实”的桥梁
3.1 为什么不能一直等蒙特卡洛回报
在文本生成任务中,一个回复可能有几十到几百个token。如果采用蒙特卡洛方法,需要等整段回复生成完,拿到最终奖励,再把这个奖励当作每一步动作的回报。这样做的问题在于方差很大。同样的prompt,模型可能生成完全不同的回复风格,整段奖励波动也会很大。单次样本很难区分“这一步确实生成得好”和“这次只是随机运气好”。
蒙特卡洛的另一个问题是训练效率低。每一步都要等整条轨迹走完,无法一有反馈就更新。对于语言模型这种高维参数模型,等几百个token只更新一次,样本利用率很低。
TD方法,即时间差分方法,只需要一步或几步的真实奖励,然后用价值网络对下一状态的估计来补充剩余部分。这种“用预测更新预测”的做法被称为自举。它的方差比蒙特卡洛小,但会引入一定的估计偏差。Actor-Critic里大量使用TD类更新,就是想在偏差和方差之间找一个平衡点。
3.2 TD误差的数学定义
最基础的TD(0)更新使用下面的目标:
$V_{\text{target}}(s_t) = r_t + \gamma V(s_{t+1})$
其中,$V(s_{t+1})$是critic对下一状态的价值预测,$r_t$是当前一步拿到的即时奖励。TD误差定义为:
$\delta_t = r_t + \gamma V(s_{t+1}) - V(s_t)$
这个δ_t的含义是:当前价值估计,与“真实奖励+下一状态估计”之间的差。critic的训练目标就是让δ_t尽可能接近0。用均方误差表示:
$L_C = \frac{1}{2}\mathbb{E}\left[\left(r_t + \gamma V_{\text{target}}(s_{t+1}) - V_{\phi}(s_t)\right)^2\right]$
需要注意,当使用TD方法更新critic时,$r_t + \gamma V_{\text{target}}(s_{t+1})$这一部分通常不参与梯度回传,在PyTorch里需要做detach。否则critic的更新会让target也随参数变化,导致优化目标不固定,训练不稳定。
3.3 从TD误差到优势估计
动作价值Q(s_t,a_t)无法直接观测,但TD误差可以看作优势函数的一个估计。更准确地说,在策略评估收敛的条件下,TD误差的期望等于优势函数:
$A(s_t,a_t) \approx \mathbb{E}[\delta_t | s_t, a_t]$
因此,actor更新可以用δ_t代替优势函数A:
$\nabla J(\theta) \approx \mathbb{E}\left[\nabla_{\theta}\log\pi_{\theta}(a_t|s_t)\delta_t\right]$
在实际PPO实现中,通常不直接使用单步TD误差,而是使用GAE(广义优势估计),对多步TD误差进行加权平均。这样做既能保留单步更新效率,又能减少方差。
4. Actor和Critic的更新公式到底怎么推
4.1 Critic的梯度:让价值预测追TD target
critic的参数记为φ,输入状态,输出V_φ(s)。我们希望V_φ能逼近真实的状态价值,但在强化学习中并没有真实价值可以直接拿,只能用TD target当老师。
定义critic损失:
$L_C(\phi) = \frac{1}{2}\left( r_t + \gamma V_{\bar{\phi}}(s_{t+1}) - V_{\phi}(s_t) \right)^2$
这里$V_{\bar{\phi}}(s_{t+1})$表示用旧参数或detach后的目标网络计算,避免梯度同时流向两个时间步。
对φ求梯度:
$\nabla_{\phi} L_C = -\left( r_t + \gamma V_{\bar{\phi}}(s_{t+1}) - V_{\phi}(s_t) \right) \nabla_{\phi} V_{\phi}(s_t)$
更新方向是让$V_{\phi}(s_t)$向TD target靠近。训练初期critic loss会快速下降,之后缓慢逼近。如果critic loss一直不平滑或不停震荡,先检查value网络输出尺度、reward分数尺度和学习率。这三个因素比模型结构更容易让critic失控。
4.2 Actor的策略梯度:为什么会出现log概率
actor的目标是最大化期望回报:
$J(\theta) = \mathbb{E}{\tau \sim \pi{\theta}}\left[\sum_{t} r_t\right]$
直接对θ求梯度很麻烦,因为期望中的采样分布也依赖θ。策略梯度定理给出了一个便于采样的形式:
$\nabla_{\theta} J(\theta) = \mathbb{E}\left[\sum_t \nabla_{\theta}\log\pi_{\theta}(a_t|s_t) Q(s_t,a_t)\right]$
这里的logπ是关键。因为:
$\nabla_{\theta} \pi_{\theta}(a|s) = \pi_{\theta}(a|s) \nabla_{\theta}\log\pi_{\theta}(a|s)$
所以对概率取log后,梯度变成对动作的“指示”:如果一个动作带来的回报高,就朝提高其概率的方向走。采样时用策略分布生成一批动作,然后用上式估计梯度。这个估计是无偏的,但方差较高,所以需要引入baseline。
如果将Q(s,a)替换为优势A(s,a)=Q(s,a)-V(s),梯度变为:
$\nabla_{\theta} J(\theta) \approx \mathbb{E}\left[\nabla_{\theta}\log\pi_{\theta}(a_t|s_t) A(s_t,a_t)\right]$
减去V(s)不改变期望,因为对同一状态下所有动作,$\mathbb{E}{a\sim\pi}[\nabla{\theta}\log\pi_{\theta}(a|s)V(s)]=0$。这一点可以自己验算:对策略概率归一化条件求导,会得到这个期望为零。baseline的作用是降低方差,而不是改变优化方向。
4.3 Actor的损失函数和熵正则
用上面的梯度可以设计actor损失:
$L_A(\theta) = -\mathbb{E}\left[\log\pi_{\theta}(a_t|s_t) A(s_t,a_t)\right]$
因为梯度下降求的是最小值,所以这里加负号。如果优势为正,损失会倾向于提高对应动作的概率;如果优势为负,则降低对应动作概率。
为了防止模型过早陷入单一模式,很多实现还会加熵正则:
$L_{\text{total}} = L_A + c_{\text{ent}} \cdot \mathbb{E}\left[H(\pi_{\theta}(\cdot|s_t))\right]$
熵H大于0,最大化熵可以让token分布不要过于极端。判断熵是否合理,可以看训练中entropy是否下降过快。如果下降太快,模型可能已经开始过度自信,需要降低学习率或调整KL权重。
5. 从Actor-Critic到PPO,公式发生了什么变化
5.1 为什么RLHF里几乎都用PPO
标准Actor-Critic每次更新后都会改变策略分布,下一步采样就要重新开始。如果学习率大,策略变化幅度会很大,语言模型一旦被推到某个极端分布,后面很难拉回来。PPO在actor更新时加入概率比和clip限制,让每次更新幅度可控。
RLHF除了限制单次更新幅度,还经常引入一个参考策略模型。当前策略不能离参考策略太远,否则会丢失语言能力和指令理解能力。常用做法是在目标函数中加入KL惩罚项:
$-\beta \cdot KL(\pi_{\theta} | \pi_{\text{ref}})$
β控制让模型保持“像原来模型”的强度。β太小,模型会只顾奖励模型高分,输出可能出现重复、模式化或不符合事实的内容。β太大,优化效果不明显,奖励提升很慢。实际训练中经常要反复试这个系数。
5.2 PPO的actor目标具体长什么样
设旧策略为$\pi_{\theta_{old}}$,新策略为$\pi_{\theta}$。定义重要性采样比例:
$\rho_t = \frac{\pi_{\theta}(a_t|s_t)}{\pi_{\theta_{old}}(a_t|s_t)}$
用旧策略采样得到轨迹,再按这个比例估计新策略下的回报。PPO的actor目标为:
$L^{clip}(\theta) = \mathbb{E}\left[\min\left(\rho_t A_t, \operatorname{clip}(\rho_t, 1-\epsilon, 1+\epsilon) A_t\right)\right]$
其中ε一般取0.2附近,但具体以论文和实现为准。clip的作用是,当ρ在[1-ε,1+ε]范围内时,允许正常策略梯度;一旦超出范围,梯度被截断,防止一步更新过大。如果A_t大于0,梯度只允许提高到一定比例内;如果A_t小于0,也只允许降低到一定比例内。
在这个目标下,actor不再像普通Actor-Critic那样直接最大化对数概率乘优势,而是先限制概率比,再决定是否更新。这就是PPO比原始AC在稳定性上更好的关键。
5.3 整体loss组合和伪代码
一个典型的PPO整体损失可以写成:
$L = - L^{clip}(\theta) + c_1 L_C(\phi) - c_2 H(\pi_{\theta}) + \text{KL惩罚}$
每个实现的组合方式略有不同。不要直接背这个式子,要关注每项的作用:第一项提高好动作概率,第二项让价值估计更准,第三项保持多样性,KL惩罚防止跑偏。
下面是一个用于理解PPO更新结构的伪代码:
# 伪代码,用于理解PPO更新结构 for batch in dataloader: # 1. 用旧策略生成回复 sequences, old_log_probs, values = old_policy.sample(batch) # 2. 奖励模型打分 rewards = reward_model(sequences) # 3. 计算GAE优势 advantages = compute_gae(rewards, values) # 4. 多次更新 for _ in range(ppo_epochs): new_log_probs, new_values = policy(sequences) ratio = exp(new_log_probs - old_log_probs) clipped_ratio = clamp(ratio, 1 - eps, 1 + eps) loss = -min(ratio * advantages, clipped_ratio * advantages).mean() loss += value_coef * mse_loss(new_values, returns) loss -= entropy_coef * entropy(new_log_probs).mean() loss += kl_coef * kl_divergence(policy, ref_policy) loss.backward() optimizer.step()这是一个思路正确的简化版。实际RLHF代码里还要处理padding、token mask、长度归一化、分布式采样等问题,但在学习阶段先用这个框架理解就够了。伪代码中的eps、value_coef、entropy_coef、kl_coef都是超参数,跑实验时需要单独调整。
6. RLHF训练时最容易踩的坑和排查顺序
6.1 先把实验规模缩小
不要一上来就训练几十B模型或跑完整数据集。建议先选一个1B左右的模型,准备几百到几千条prompt,不做太多并行,只验证管线是否通。更推荐用已有小模型的RLHF样例代码起步,先跑通再换大模型。
运行前要确认三件事:actor能正常生成并返回log_prob;critic能输出和序列长度匹配的value;奖励模型能对完整回复返回标量分数。这三个条件不满足,后续优化无从谈起。输出shape对不上是最常见的启动错误。
资源方面,模型大小、batch size、序列长度三者共同决定显存占用。如果OOM,优先降低batch size和序列长度,而不是马上换机器。梯度累积可以弥补batch size减小带来的稳定性问题,但累积步数不要设置得太大,否则训练节奏会变得很慢。
6.2 第一次训练拆成三步
第一步,跑单条样本。输入一个prompt,采样生成一个回复,打印old_log_prob、value、reward、advantage,看数据尺度。如果reward在几十而value在0.1以下,critic更新会很难收敛,需要先做奖励归一化。
第二步,跑一个小batch,关掉更新或只更新几步。检查梯度是否存在,loss能否下降,有没有NaN。很多问题不是算法逻辑错,而是数值溢出。比如log_prob为-inf、advantage为NaN。出现这类情况,先检查输入长度和mask,不要先怀疑模型结构。
第三步,再开启完整更新周期。按照情况调整KL系数、clip范围、学习率。记录reward mean、entropy、KL、value loss这几个指标。它们比单看reward更有判断价值。reward只是最终结果,entropy和KL能告诉你策略是否已经走偏。
6.3 训练不稳定时的排查链路
出现以下情况,按这个顺序排查:
- reward一直涨,但生成质量明显变差。先看KL值是否过大或过小,再调小学习率或调大KL系数。不要继续加大batch size,这通常是策略崩溃的典型信号。
- critic loss不下降。先看value输出是否被初始化为0,reward是否过大,GAE的lambda是否需要调整。再把value loss和reward的scale打出来,如果量级差太远,需要归一化。
- 梯度爆炸或NaN。先看log_prob和advantage是否有nan,再看混合精度是否开启。多数时候是输入数据中有超长序列或空回复。
- 卡住不动。先确认不是死锁,看日志最后一条输出在哪个步骤,再检查进程数、端口和输出目录权限。这类问题通常是环境或工程问题,不是算法问题,不要一上来就改模型。
6.4 记住三件边界事
第一,能跑通小模型不等于能直接生产。大模型RLHF对训练策略、KL控制、奖励模型质量的要求完全不同。第二,默认参数只适合入门。PPO的epsilon、GAE的lambda、价值损失系数都要根据任务调整,不能照着抄一遍就期望稳定。第三,任何“reward提升”都不一定代表真实质量。奖励模型本身会过拟合,需要人工抽看生成结果,否则你优化的只是奖励模型的偏好。
7. 给自己建立一份推导检查清单
7.1 推导时要反复核对哪些点
每次看到一个新的AC变体,可以先把这些位置确认一遍,而不是直接闷头看loss代码:
- 符号量纲:动作是否对应actor的输出,状态是否对应critic的输入。两者不能混。
- 梯度来源:actor的更新是否来自log概率乘以优势;critic的更新是否来自TD误差平方的导数。
- target是否detach:如果TD target没有detach,critic可能在“追自己的影子”,训练目标会漂移。
- 优势是否归一化:很多实现在更新前会对advantage做标准化,训练会更稳定。
- clip是否作用于概率比,而不是直接裁剪advantage。这两者看起来像,结果完全不同。
- 熵正则的方向:最大化熵时,loss里通常带负号;写反了会加速收敛到单峰分布。
7.2 一份可操作的检查表
我建议在本地准备一个简单表格,记录你和标准实现的差异。每次跑新实验,先填这几项:模型规模、batch size、序列长度、kl系数、clip范围、GAE的lambda、学习率、价值损失系数。这样出现训练不稳定时,能快速定位是哪个参数改坏了。
也可以写一个小脚本,用固定seed在固定数据上跑几步,比较修改前后的loss曲线。如果不固定seed,可能只是采样波动,却会被误认为算法改进。这是强化学习实验里最常见的虚假信号。
7.3 推荐的学习顺序
如果你刚接触这部分,不要着急看懂完整RLHF代码。先用一个只有3个状态、2个动作的表格例子,手动算一次价值迭代;再实现一个最简单的Actor-Critic,在标准控制环境里跑一遍;然后回到RLHF代码,把AC和PPO的公式映射到每个tensor。
每次遇到新公式,用“符号翻译+维度验证+数值样例”三步来消化。先翻译成中文是什么,再看输入输出维度能不能对齐,最后用一个简单数字例子手算一遍。坚持几轮之后,再复杂的推导也不会怕。很多问题不是数学难,而是符号和实际tensor对不上。
最后留一个个人习惯:我会把每个新实验最先跑通的那份配置单独存一份,不覆盖。后面调参失败时,还能回到第一个稳定点重新看。强化学习训练本来就容易波动,保留一个确定性的起点,能省很多时间。