RL 是 post-training 里比较重要的一个部分。开个坑,记录下 RL 相关的基础知识。
跑个 demo 看下 RL 的基本概念
RL 本质是在 Actor 和 Env 的持续交互中,通过 action 和 state 变化来不断迭代 policy/reward/critic 等模型。
下面的例子用一个左右移动的场景(目标是移动到 2)来举例(下面代码基本都有注释,highlight 几个点):
- reward model: 这个场景下 reward 会比较简单,直接通过 state 来判断。为了引入 critic values,这里在 state=2 的 reward 是最大的,但是故意在 state=1 的 reward 设置为最小。所以如果 actor 只关心 reward,那永远走不到 2。
- reward 分为确定性的 reward 和非确定性的 reward。如何定义好 reward(尤其是过程中的 reward)很关键;
- critic model:为了避免 actor 只看 reward(短期收益),引入 critic values(长期价值),长期价值本质上是未来所有潜在 reward 的压缩。所以每个 step 的真实价值应该是 reward + gamma * critic-value。
- critic value 也分两种情况,一种是可枚举的,比如这个场景,我可以遍历所有的情况,然后不断更新 critic value;另一种是不可枚举的,预估的;
- policy 最后输出的是一个 logprobs,(1, T),T 是 action 的维度,然后加入一定的随机性,这里和 LLM 的 temperature 是类似的。
- Training 训练过程
- rollout:actor 每走一步或者几步,产生的 trajectory 可以认为是一次 rollout,包括 state/action/logprobs/reward/critic-values 等多个信息。
- reward model 一般不参与 RL 训练。
- critic model 更新:
- 计算一个 advantage,公式是
adv = step["reward"] + gamma * V[s_next] - V[s],- adv > 0:说明 V[s] 估计小了;adv < 0:说明 V[s] 估计大了;
- 用这个 delta 值去训模型
- 计算一个 advantage,公式是
- policy model 更新:
- 对于 policy model,adv 的值表示这个行为应该被鼓励还是抑制,从而影响下次输出的 logprobs
- 这是一个最简单的 demo,reward/critic/policy 都直接用 python dict 来表示,实际的训练过程,根据不同的方法论会更加的复杂...
import numpy as np
import random
# 环境的描述(-2 到 2 共有 5 个点)
states = [-2, -1, 0, 1, 2]
# 动作集合(左,不动,右)
actions = [-1, 0, 1]
# 执行 action
def transition(s, a):
return max(-2, min(2, s + a))
# reward model 定义
def reward(s):
if s == 2:
return 10
if s == 1:
return -5
return -1
# policy logits: state -> action logits
policy_logits = {
s: np.zeros(len(actions)) for s in states
}
def softmax(x):
e = np.exp(x - np.max(x))
return e / e.sum()
# policy model,
def policy(state):
probs = softmax(policy_logits[state])
return probs
V = {s: 0.0 for s in states}
gamma = 0.9
trajectory = []
# 一次 rollout
state = 0
for t in range(100):
probs = policy(state)
action_idx = np.random.choice(len(actions), p=probs)
action = actions[action_idx]
logprob = np.log(probs[action_idx] + 1e-8)
next_state = transition(state, action)
r = reward(next_state)
trajectory.append({
"state": state,
"action": action,
"action_idx": action_idx,
"reward": r,
"logprob": logprob
})
state = next_state
# 计算 advantage, 作为 policy model 的输入
advantages = []
for step in trajectory:
s = step["state"]
a = step["action"]
s_next = transition(s, a)
td_target = step["reward"] + gamma * V[s_next]
advantage = td_target - V[s]
advantages.append(advantage)
# 利用 advantages 更新 policy model
lr = 0.1
for step, adv in zip(trajectory, advantages):
s = step["state"]
a_idx = step["action_idx"]
logp = step["logprob"]
# policy gradient: ∇ logπ(a|s) * advantage
policy_logits[s][a_idx] += lr * adv
# 利用 advantages 更新 critic model
alpha = 0.1
for step, adv in zip(trajectory, advantages):
s = step["state"]
V[s] += alpha * adv
for i, step in enumerate(trajectory):
print(
f"t={i}, s={step['state']}, a={step['action']}, "
f"r={step['reward']}, logp={step['logprob']:.3f}, "
f"adv={advantages[i]:.3f}"
)
- 除了上面的 demo 之外,还有一个 reference model,目的是约束,放置模型训歪。不过我现在还没理解这个是否是必要的,如果是为了约束,那在 reward 和 critic model 里也能起到类似的作用,多加一个模型反而让整体复杂度和稳定性风险又高一个量级。
- Reference 模型可能来自于初始化的 policy model,为了防止 policy model 跑的太偏;(利用 KL 散度来定义 loss)
OpenRLHF / Slime / VeRL
- OpenRLHF 里各个严谨的公式推导参考:https://zhuanlan.zhihu.com/p/7461863937
- OpenRLHF 的源码解读可以看:https://github.com/OpenRLHF/OpenRLHF ,主要讲的是不同的 model(比如 inference 和 training)如何利用 Ray 来做分组。
- 之前读了一部分 Slime 的源码,读下来和 OpenRLHF 的思路是类似的;
Slime
// TODO 源码解读