KL 正则化控制:reference policy 到底约束了什么¶
语言模型 RL 通常不从随机策略开始。预训练或 SFT policy 已经提供流畅性、知识与基本行为;在线优化只希望在目标分布附近改变排序。KL regularization 把这种“不要离开先验太远”的要求写进控制目标。
目标¶
对 prompt \(x\)、response \(y\) 与任务 reward \(r(x,y)\),常见目标为
对固定 \(x\) 取期望后,第二项就是
\(\beta\) 的量纲与 reward 尺度绑定。reward 整体乘 \(c\) 而 \(\beta\) 不变,会改变最优策略;所以 reward normalization、模型版本和 KL coefficient 不能分开记录。
闭式最优策略¶
对每个 \(x\),在 \(\sum_y\pi(y\mid x)=1\) 下做变分优化,可得
其中
这条闭式解默认可行策略对 reference policy 绝对连续,即
因此指数倾斜只能在 \(\pi_{\mathrm{ref}}\) 的 support 内重新分配概率;reference 概率为零的 response 不会被有限 reward“复活”。原始 softmax LM 通常有全词表 support,但 top-\(k\)、top-\(p\)、grammar mask 或其他受限解码会引入零概率,推导和实现都必须使用同一个可行动作集合。
重排后,
这条关系连接了在线 RLHF 与 DPO:同一 prompt 下比较 chosen/rejected 时,\(\log Z(x)\) 抵消。但闭式关系依赖特定 KL 正则化和 reward/preference 假设;DPO 不是对任意 RL 目标的代数替代。
Current、old、behavior 与 reference¶
语言模型 PPO 至少要区分四种 policy 身份:
| policy | 角色 |
|---|---|
| \(\pi_\theta^{\mathrm{train}}\) | learner 当前正在更新的 policy |
| \(\pi_{\mathrm{old}}^{\mathrm{train}}\) | 一轮更新开始时冻结的 training-side policy,用于 PPO update ratio |
| \(\mu^{\mathrm{rollout}}\) | 实际产生 token 的 behavior distribution,由 checkpoint、推理引擎与 sampling processor 共同决定 |
| \(\pi_{\mathrm{ref}}\) | 长期行为先验,用于 KL regularization |
PPO ratio
刻画 learner 在同一批样本上的 current–old update。同步且数值契约完全一致时,
异步队列、不同推理 kernel、量化、路由或采样处理器都可能破坏这个等式。此时 current 相对真实 behavior 的 direct ratio 是
不能把 \(\rho_t\) 直接改名为 behavior ratio。真正限制更新的是 PPO clipped surrogate、显式 trust region 或其他约束;ratio 本身只是一项统计量。reference KL
约束累计训练漂移。old、behavior 与 reference 偶尔从相同权重或 checkpoint 出发,也不具备相同语义。完整的三种 ratio 与 engine mismatch 见策略身份、训推分布与策略滞后。
Sequence KL 与 token KL¶
自回归分解给出
在 \(y\sim\pi\) 下取期望,sequence forward KL 可分解为策略访问前缀下的 token KL 之和。但工程估计有几种不同对象:
- 采样 token 的 log-ratio:对给定 prefix 只读被采样动作,是单动作 Monte Carlo estimator;计算便宜,但有动作采样方差,单样本可为负。在动作确实来自当前 \(\pi\) 时,它的条件期望才是该位置的 forward KL;
- 完整词表 KL:对给定 prefix 遍历全 vocabulary,可精确计算该位置的条件 KL,没有动作采样方差,但计算和显存代价更高;整条 rollout 的估计仍会随策略访问到的 prefix 变化;
- behavior-sampled log-ratio:若样本来自 \(\mu^{\mathrm{rollout}}\ne\pi_\theta\),一般不是当前策略 KL 的无偏估计;
- token mean:除以 response length 后不再等于 sequence KL,只是长度归一化诊断。
完整词表的 categorical KL 在每个 prefix 上精确求动作期望,因此理论上非负。下面的 policy_logits 与 reference_logits 必须使用同一 vocabulary 和 action support;mask 只决定哪些 prefix 进入 sequence 聚合。
import torch
import torch.nn.functional as F
def exact_token_kl(policy_logits, reference_logits, action_mask):
if (policy_logits.shape != reference_logits.shape
or policy_logits.shape[:-1] != action_mask.shape):
raise ValueError("policy, reference and prefix mask shapes must align")
reference_logits = reference_logits.detach()
mask = action_mask.bool()
if torch.any(mask.sum(-1) == 0):
raise ValueError("every sequence needs an action prefix")
active = mask[..., None]
if (torch.isnan(policy_logits) | torch.isposinf(policy_logits)).any() or (
torch.isnan(reference_logits) | torch.isposinf(reference_logits)).any():
raise ValueError("categorical logits cannot contain NaN or positive infinity")
policy_support = ~torch.isneginf(policy_logits)
reference_support = ~torch.isneginf(reference_logits)
if torch.any(active & policy_support & ~reference_support):
raise ValueError("reference support must cover active policy support")
if torch.any(mask & ~policy_support.any(-1)):
raise ValueError("every active prefix needs a finite action")
safe_policy = torch.where(active, policy_logits, torch.zeros_like(policy_logits))
safe_reference = torch.where(active, reference_logits, torch.zeros_like(reference_logits))
log_policy = F.log_softmax(safe_policy, dim=-1)
log_reference = F.log_softmax(safe_reference, dim=-1)
support = active & policy_support
log_ratio = torch.where(support, log_policy, 0.) - torch.where(support, log_reference, 0.)
token_kl = (torch.where(support, log_policy, -torch.inf).exp() * log_ratio).sum(-1)
return token_kl, token_kl.sum(-1), token_kl.sum(-1) / mask.sum(-1)
policy = torch.tensor([[[2., -torch.inf], [0., 2.], [9., -9.]]], requires_grad=True)
reference = torch.tensor([[[2., 0.], [1., 1.], [-9., 9.]]], requires_grad=True)
mask = torch.tensor([[True, True, False]])
token_kl, sequence_kl, mean_kl = exact_token_kl(policy, reference, mask)
assert torch.isfinite(token_kl).all() and token_kl[0, 0] > 0 and token_kl[0, 2] == 0
sequence_kl.sum().backward()
assert reference.grad is None and torch.isfinite(policy.grad).all()
mask 外的极端分布差不会进入聚合;mask 内若 reference 对 policy 的正概率动作给零 support,forward KL 应为无穷而非被 epsilon 悄悄截平。可执行的 sampled reference-KL 对照见强化学习手撕实现。
不要把任何平均 log-ratio 都命名为 kl。日志应写清采样分布、方向、词表是否完整以及 reduction。
采样 token estimator 的最小接口应同时返回 sequence sum 与 token mean,避免同一个变量名掩盖两种量。输入 policy_logp 和 reference_logp 必须在完全相同的 prefix、token ID 与词表 support 上重算;action_mask 排除 prompt、observation 和 padding。
import torch
def sampled_forward_kl(policy_logp, reference_logp, action_mask):
if not (policy_logp.shape == reference_logp.shape == action_mask.shape):
raise ValueError("log-probabilities and mask must align")
reference_logp = reference_logp.detach()
mask = action_mask.bool()
if torch.any(mask.sum(-1) == 0):
raise ValueError("every sequence needs a sampled action")
policy_finite = torch.isfinite(policy_logp[mask]).all()
reference_finite = torch.isfinite(reference_logp[mask]).all()
if not policy_finite or not reference_finite:
raise ValueError("sampled action log-probabilities must be finite")
token_log_ratio = torch.zeros_like(policy_logp)
token_log_ratio[mask] = policy_logp[mask] - reference_logp[mask]
sequence_sum = token_log_ratio.sum(-1)
token_mean = sequence_sum / mask.sum(-1)
return token_log_ratio, sequence_sum, token_mean
logp = torch.tensor([[torch.nan, -.2, -.7], [-torch.inf, -.1, -.4]], requires_grad=True)
ref = torch.tensor([[torch.nan, -.3, -.5], [torch.inf, -.1, -.4]], requires_grad=True)
mask = torch.tensor([[0, 1, 1], [0, 1, 1]], dtype=torch.bool)
token_lr, seq_kl, mean_kl = sampled_forward_kl(logp, ref, mask)
assert token_lr[:, 0].eq(0).all()
torch.testing.assert_close(seq_kl, torch.tensor([-.1, 0.]))
torch.testing.assert_close(mean_kl, seq_kl / 2)
seq_kl.sum().backward()
assert ref.grad is None and torch.isfinite(logp.grad).all()
单条序列的 sequence_sum 可以为负,这不违背 forward KL 非负性:非负性属于对当前策略动作和访问前缀取期望后的量。若样本来自 rollout behavior,必须先处理分布偏移,不能把这段代码的输出直接当作当前策略 KL。
Reward shaping 与 loss penalty¶
KL 可以进入 rollout reward:
也可以在 learner loss 中对当前 policy 直接加 penalty。上式描述“按真实 rollout distribution 收费”的版本;若实现改用 training-side old log-prob,它定义的是另一个 proxy,必须同时记录 engine mismatch 和 sampling processor。两种实现的估计对象、梯度路径和更新时机不同。
若 KL 作为 reward 进入 return,它会影响 critic target 与早期 advantage;若只作为当前 loss penalty,它不会以同样方式传播进 value。组合使用并非一定错误,但必须明确是否重复计费。
KL 不是安全边界¶
小平均 KL 不能保证:
- 每个 prompt 的行为都接近 reference;
- tail token 或少数状态没有剧烈变化;
- 不会出现 reward hacking;
- 输出事实性、安全性或工具权限得到保护。
平均 KL 可能被大量普通 token 稀释。至少同时看 prompt-level 分位数、response length、最大 token log-ratio、拒答/格式 slice 与独立任务指标。
\(\beta\) 的自适应¶
有些系统根据测得 KL 调整 \(\beta\):
这只是一个示意性的乘法 controller,不是 RLHF 系统统一采用的更新式,更不是理论自动保证。实际实现也可能使用线性比例更新、固定 horizon 或不同 clamp。测量噪声、policy lag 与长短 response 混合都可能让 \(\beta\) 振荡。应记录具体公式、controller interval、目标定义、clamp 与 restart 状态。
与最大熵 RL 的联系和区别¶
最大熵目标常写为
当 reference 在固定有限 support 上均匀时,负 KL 与 entropy 只差常数。若 action mask 或 grammar 让 support 随状态变化,这个“常数”也会随状态变化,不能无条件丢掉。语言模型 reference 又通常高度非均匀,携带语法、知识和风格先验。因此 RLHF 中的 KL 更像 relative entropy control,不是单纯鼓励随机性。探索强度仍由 sampling 与 policy entropy 共同决定。
验证¶
- \(\pi=\pi_{\mathrm{ref}}\) 时 sample log-ratio 为零。
- 共同修改 prompt token 不改变 response-only KL mask。
- 分别测试 sequence sum 与 token mean,避免长度约定暗中变化。
- old、behavior 与 reference 权重或 log-prob 交换时单元测试必须失败。
- reward rescale 后重新检查 \(\beta\) 与目标 KL。
- 按 prompt 和长度分层,而不只报告全局 mean。
实现见训练目标,偏好闭式关系见离线偏好优化,四种 policy 的训练契约见在线 RL。
Reference¶
- Ziegler et al., Fine-Tuning Language Models from Human Preferences
- Stiennon et al., Learning to Summarize with Human Feedback
- Ouyang et al., Training Language Models to Follow Instructions with Human Feedback
- Rafailov et al., Direct Preference Optimization
- Todorov, Linearly-Solvable Markov Decision Problems
- Peters, Mülling, and Altun, Relative Entropy Policy Search
- Haarnoja et al., Soft Actor-Critic: Off-Policy Maximum Entropy Deep Reinforcement Learning with a Stochastic Actor