在RLHF(基于人类反馈的强化学习)训练中,很多人会遇到一个奇怪的现象:奖励分数持续上涨,但模型生成的内容质量却越来越差,甚至出现重复、胡言乱语或恶意迎合奖励模型的行为。这不是训练代码写错了,而是RLHF最核心的难题之一——奖励模型过优化。本文将围绕这个现象展开,重点讲解KL散度约束如何缓解这一问题。

什么是奖励模型过优化
奖励模型本质上是一个代理目标。人类标注员对模型的输出进行偏好排序,奖励模型学习这些偏好后给出一个标量分数,用来近似人类的真实偏好。问题在于,奖励模型只是一个容量有限的神经网络,它学到的评分函数与人类真实偏好之间存在偏差。
当策略模型通过PPO等算法不断最大化这个分数时,它找到的往往是奖励模型的漏洞,而不是人类真正喜欢的行为。经典的实验结论是Goodhart定律的直接体现:当某个代理指标被过度优化时,它与真实目标的关联就会失效甚至反转。在RLHF中,表现为策略输出越来越长、堆砌格式化符号、过度使用特定句式,这些特征可能是奖励模型在训练数据中学到的“高分信号”,但对人类来说毫无价值。
更麻烦的是,奖励模型通常只在参考模型(一般是SFT模型)的输出分布上训练过。一旦策略模型偏离这个分布太远,进入了奖励模型的训练数据从未覆盖的区域,奖励分数就完全失去了可信度。此时分数越高,模型行为越离谱,训练曲线看起来一切正常,实际上已经崩坏。
KL散度约束的原理与作用
解决过优化的核心手段,是在奖励目标中加入KL散度惩罚项。KL散度衡量两个概率分布的差异,在RLHF中具体指当前策略与参考模型(SFT模型)输出分布之间的差异:
import torch.nn.functional as F
def compute_kl_penalty(policy_logps, ref_logps):
# 计算策略模型与参考模型在每个token上的KL散度
# policy_logps: 当前策略的对数概率
# ref_logps: 参考模型的对数概率
kl = policy_logps * (policy_logps.exp() / ref_logps.exp()).log()
return kl.sum(dim=-1).mean()数学形式上,最终的优化目标变成了:最大化奖励分数,同时减去一个KL惩罚项。直觉很好理解:策略模型可以尝试提升奖励,但每偏离参考模型一步,都要付出代价。当偏移带来的奖励增益小于KL惩罚时,策略就不再继续偏离,从而被“锚定”在参考模型附近。
这个设计有两个关键作用。第一,它把策略的探索范围限制在奖励模型相对可信的区域内,因为参考模型的输出分布正是奖励模型训练数据的主要来源。第二,它起到了正则化的效果,防止策略为了 exploiting 奖励模型的偏差而走向极端。实践中,KL约束往往是决定RLHF训练成败的最重要超参数之一。
需要注意的是,KL方向不能搞反。RLHF中用的是策略对参考模型的KL,衡量的是当前策略相对参考的偏离程度,两个方向的KL散度数值不同,含义也不同,写反了会导致约束失效或训练崩溃。
在PPO中实现KL约束的完整方案
接下来给出一个简化的训练框架,展示KL惩罚如何嵌入PPO流程。这里采用将KL惩罚直接加到奖励信号上的做法,这也是InstructGPT等工作中采用的方式:
import torch
def ppo_step(policy_model, ref_model, reward_model, batch, kl_coef=0.1):
# 1. 生成响应并记录对数概率
outputs, old_logps = policy_model.generate_and_score(batch)
with torch.no_grad():
ref_logps = ref_model.score(batch, outputs)
base_reward = reward_model(batch, outputs) # 奖励模型打分
# 2. 计算KL惩罚并合成最终奖励
kl_penalty = (old_logps.exp() / ref_logps.exp()).log()
rewards = base_reward - kl_coef * kl_penalty.detach()
# 3. PPO更新循环:多轮epoch迭代策略
for _ in range(ppo_epochs):
new_logps, values = policy_model.evaluate(batch, outputs)
ratio = (new_logps - old_logps).exp()
advantages = compute_gae(rewards, values)
# PPO裁剪损失 + 价值函数损失
actor_loss = -torch.min(
ratio * advantages,
ratio.clamp(0.8, 1.2) * advantages
).mean()
value_loss = F.mse_loss(values, returns)
total_loss = actor_loss + 0.5 * value_loss
total_loss.backward()
optimizer.step()除了把KL加进奖励,另一种做法是直接在损失函数上加KL正则项,两者效果接近,实现上略有差异。加在奖励里可以让价值网络也感知到惩罚信号,训练更稳定;加在损失里实现更简单,适合快速验证。无论哪种方式,KL系数的取值都至关重要。
KL系数调节与训练监控技巧
KL系数通常在0.01到0.2之间搜索起步。系数太小,约束形同虚设,过优化照样发生;系数太大,策略几乎不动,奖励提升缓慢,训练等于白做。一个实用的经验做法是自适应调节:设定一个KL目标值(比如0.1),每个训练步监控实际KL散度,若超过目标值就增大系数,低于目标值就减小系数。trl等开源库已经内置了这种自适应机制。
除了调系数,训练监控同样重要。只看奖励曲线远远不够,必须同时监控KL散度的变化趋势和人工抽检生成样本。几个典型的预警信号:奖励快速上涨而KL同步暴涨,说明策略在钻空子;KL趋近于零且奖励不涨,说明系数过大;生成文本出现明显的重复循环或格式异常,说明过优化已经发生。建议每几百步保存一批样本人工评估,这是发现问题的最可靠手段。
最后还可以配合一些工程手段:限制训练的epoch数量或总步数,在过优化爆发前停止;使用更大的奖励模型或更高质量、更多样化的偏好数据,从源头减少奖励模型的漏洞;对生成长度做归一化,避免策略靠堆长文本骗分。KL约束不是银弹,但它和这些手段组合起来,基本能覆盖RLHF训练不稳定的大部分场景。