强化学习实战:从 Q-Learning 到 PPO 与 RLHF

强化学习让智能体在试错中学习决策,本文系统讲解马尔可夫决策过程与贝尔曼方程、价值迭代与策略迭代、时序差分与 Q-Learning、DQN 的神经网络近似与经验回放、策略梯度与 REINFORCE、Actor-Critic 与优势函数、PPO 的裁剪目标与信赖域思想、连续动作空间的 DDPG/SAC、奖励设计与稀疏奖励的解法,以及 RLHF 与 RLVR 在大模型对齐中的强化学习应用与训练稳定性工程实践。

监督学习告诉你「这个输入对应什么答案」,强化学习只告诉你「刚才那步做得有多好」。在没有标准答案、只有延迟奖励的场景里——下棋、机器人控制、大模型对齐——强化学习是唯一的选择。

强化学习的问题设定与 MDP

强化学习的数学语言是马尔可夫决策过程(MDP),由五元组 (S, A, P, R, γ) 定义。

MDP 的五个要素

  • 状态空间 S:环境的所有可能状态,如棋盘局面。
  • 动作空间 A:智能体可执行的动作,如落子位置。离散或连续。
  • 转移概率 P(s’|s,a):在状态 s 执行动作 a 后到达 s' 的概率。
  • 奖励函数 R(s,a):即时反馈,是学习唯一的信号来源。
  • 折扣因子 γ ∈ [0,1):未来奖励的衰减系数,γ=0.99 表示「近处奖励更重要」。

马尔可夫性是整个框架的基石:下一状态只依赖当前状态与动作,与更早的历史无关。真实问题往往不满足,需要用帧堆叠或 RNN 把历史编码进状态。

回报与价值函数

回报(Return) 是从当前时刻起的折扣累计奖励:

G_t = r_{t+1} + γ r_{t+2} + γ² r_{t+3} + ... = Σ_{k=0}^∞ γ^k r_{t+k+1}

两个核心价值函数:

  • 状态价值 V^π(s):从状态 s 出发,按策略 π 行动能拿到的期望回报。
  • 动作价值 Q^π(s,a):在状态 s 先执行动作 a,之后按 π 行动能拿到的期望回报。

V 回答「这个局面好不好」,Q 回答「在这个局面做这个动作好不好」。几乎所有算法都在估计这两个函数之一。

贝尔曼方程

价值函数满足自洽的贝尔曼方程:

V^π(s) = Σ_a π(a|s) Σ_{s'} P(s'|s,a) [ R(s,a) + γ V^π(s') ]
Q^π(s,a) = Σ_{s'} P(s'|s,a) [ R(s,a) + γ Σ_{a'} π(a'|s') Q^π(s',a') ]

贝尔曼方程的递归结构是所有强化学习算法的计算基础:它把一个无穷和问题变成了一步加未来的迭代。

价值迭代与策略迭代

知道贝尔曼方程后,最朴素的做法是动态规划:反复迭代直到价值收敛。

价值迭代

价值迭代直接对最优价值函数做迭代:

V_{k+1}(s) = max_a Σ_{s'} P(s'|s,a) [ R(s,a) + γ V_k(s') ]

每一步都取「让价值最大的动作」,迭代到 V 不再变化即收敛。收敛后,最优策略就是每步选 argmax_a 的动作。

import numpy as np

def value_iteration(P, R, gamma=0.99, tol=1e-6):
    # P: [S, A, S] 转移概率  R: [S, A] 奖励
    S, A = R.shape
    V = np.zeros(S)
    while True:
        Q = R + gamma * P.dot(V)          # [S, A]
        V_new = Q.max(axis=1)
        if np.abs(V_new - V).max() < tol:
            return V_new, Q.argmax(axis=1)
        V = V_new

策略迭代

策略迭代交替做两步:

  1. 策略评估:在当前策略下,迭代算出 V^π。
  2. 策略改进:对每个状态取 argmax_a Q^π(s,a),得到新策略。

两者对比:

维度价值迭代策略迭代
每轮计算一次扫描完整评估 + 一次改进
收敛轮数多少
实现复杂度低中
适用状态空间小状态空间小

两者都要求已知转移概率 P。真实问题中 P 通常未知,这就引出了无模型(Model-Free)方法。

动态规划也有硬伤:它需要遍历所有状态,状态数一多(如围棋的 10^170 种局面)就完全不可行。更致命的是它假设环境模型已知,而现实中机器人不知道执行动作后世界的精确转移概率。

时序差分与 Q-Learning

时序差分(Temporal Difference, TD) 是无模型强化学习的核心思想:不用等回合结束算完整回报,而是用下一时刻的估计来更新当前估计——自举(Bootstrapping)。

TD 误差与更新

TD 误差是「实际观察到的奖励 + 下一状态估计」与「当前估计」之差:

δ_t = r_{t+1} + γ V(s_{t+1}) - V(s_t)
V(s_t) ← V(s_t) + α · δ_t

α 是学习率。这个更新不需要知道 P,只需要一次实际交互得到的 (s, a, r, s')。

Q-Learning 的更新规则

Q-Learning 是离线策略(Off-Policy)的经典算法,更新规则:

Q(s,a) ← Q(s,a) + α [ r + γ max_{a'} Q(s',a') - Q(s,a) ]

注意 max_{a'}——它假设下一步会选最优动作,与当前实际使用的策略无关。这就是「离线策略」的含义:可以用探索性策略收集数据,却学习最优策略。

import numpy as np

def q_learning(env, episodes=5000, alpha=0.1, gamma=0.99, eps=0.1):
    Q = np.zeros((env.observation_space.n, env.action_space.n))
    for _ in range(episodes):
        s, _ = env.reset()
        done = False
        while not done:
            if np.random.rand() < eps:
                a = env.action_space.sample()      # ε-贪心探索
            else:
                a = Q[s].argmax()
            s2, r, term, trunc, _ = env.step(a)
            done = term or trunc
            target = r if done else r + gamma * Q[s2].max()
            Q[s, a] += alpha * (target - Q[s, a])
            s = s2
    return Q

探索与利用的权衡

ε-贪心 是最简单的探索策略:以 ε 概率随机动作,1-ε 概率选当前最优。更精细的方案:

  • 衰减 ε:训练初期多探索,后期收敛到利用。
  • UCB:给不确定性高的动作加分,a = argmax[Q(a) + c√(ln t / N(a))]。
  • 玻尔兹曼探索:按 softmax(Q/τ) 采样,温度 τ 控制随机程度。

DQN 与经验回放

Q-Learning 用表格存 Q,状态一多就爆表。DQN(Deep Q-Network) 用神经网络近似 Q(s,a),并在 2015 年首次让智能体从像素输入直接学会玩 Atari 游戏。

函数近似与损失

DQN 把 Q(s,a) 参数化为网络 Q_θ,损失是最优贝尔曼方程的平方误差:

L(θ) = E_{(s,a,r,s')~D} [ ( r + γ max_{a'} Q_{θ^-}(s',a') - Q_θ(s,a) )² ]

θ^- 是目标网络的参数,定期从 θ 同步。没有目标网络时,目标值 r + γ max Q 也在随参数变动,训练会发散——这是 DQN 最关键的工程技巧之一。

经验回放

经验回放(Experience Replay) 把交互得到的 (s,a,r,s') 存进一个缓冲区,训练时随机采样 mini-batch。它解决两个问题:

  • 样本相关性:连续交互的样本高度相关,直接训练会破坏 SGD 的独立同分布假设。随机采样打破相关性。
  • 样本效率:一条经验可以被多次复用,而不是用完即弃。
import random
from collections import deque
import torch

class ReplayBuffer:
    def __init__(self, capacity=100000):
        self.buf = deque(maxlen=capacity)

    def push(self, s, a, r, s2, done):
        self.buf.append((s, a, r, s2, done))

    def sample(self, batch_size):
        batch = random.sample(self.buf, batch_size)
        s, a, r, s2, d = zip(*batch)
        return (torch.as_tensor(s), torch.as_tensor(a),
                torch.as_tensor(r, dtype=torch.float32),
                torch.as_tensor(s2), torch.as_tensor(d, dtype=torch.float32))

Double DQN 与 Dueling DQN

原始 DQN 有一个系统性偏差:max_{a'} Q(s',a') 会高估真实价值,因为取最大值的动作恰好是被噪声抬高的那个。改进方案:

变体核心改进解决的问题
Double DQN用在线网络选动作、目标网络估值Q 值高估
Dueling DQN拆成 V(s) 与优势 A(s,a)状态价值与动作优势解耦
Prioritized Replay按 TD 误差加权采样重要样本利用率
Noisy Net网络参数加噪声做探索替代 ε-贪心

Double DQN 的改动只有一行:把 max_{a'} Q_{θ^-}(s',a') 改成 Q_{θ^-}(s', argmax_{a'} Q_θ(s',a'))。

策略梯度与 REINFORCE

价值方法在连续动作空间里很别扭——max_a Q(s,a) 需要对动作做优化。策略梯度直接参数化策略 π_θ(a|s),用梯度上升最大化期望回报。

策略梯度定理

策略梯度的核心公式:

∇_θ J(θ) = E_{τ~π_θ} [ Σ_t ∇_θ log π_θ(a_t|s_t) · G_t ]

直觉解释:回报高于平均的动作,提高它的概率;回报低的,降低它。∇log π 指示了「增大该动作概率」的方向,G_t 是权重。

REINFORCE 算法

最简单的策略梯度算法,用完整回合的回报做权重:

import torch

def reinforce(policy, env, optimizer, gamma=0.99, episodes=1000):
    for _ in range(episodes):
        log_probs, rewards = [], []
        s, _ = env.reset()
        done = False
        while not done:
            logits = policy(torch.as_tensor(s, dtype=torch.float32))
            dist = torch.distributions.Categorical(logits=logits)
            a = dist.sample()
            log_probs.append(dist.log_prob(a))
            s, r, term, trunc, _ = env.step(a.item())
            rewards.append(r)
            done = term or trunc

        # 从后往前算折扣回报
        G, returns = 0, []
        for r in reversed(rewards):
            G = r + gamma * G
            returns.insert(0, G)
        returns = torch.tensor(returns)
        returns = (returns - returns.mean()) / (returns.std() + 1e-8)

        loss = -torch.stack([lp * G for lp, G in zip(log_probs, returns)]).sum()
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

returns 的标准化(减均值除标准差)是关键技巧:它把「绝对回报」变成「相对回报」,显著降低梯度方差。

方差与基线

REINFORCE 的致命弱点是方差极大:同一策略跑两次,回报可能天差地别,梯度噪声淹没了信号。引入基线 b(s) 降低方差:

∇_θ J ≈ E [ Σ_t ∇_θ log π_θ(a_t|s_t) · (G_t - b(s_t)) ]

只要基线不依赖动作 a,它就不改变梯度的期望,只降低方差。最常用的基线就是状态价值函数 V(s)——「这个局面平均能拿多少分」。减去它后,权重变成了「比平均好多少」,这正是优势函数。

Actor-Critic 与优势函数

Actor-Critic 把两者结合:Actor 是策略网络(决定做什么),Critic 是价值网络(评价做得好不好)。

优势函数

优势函数定义为:

A(s,a) = Q(s,a) - V(s)

它衡量「在状态 s 下做动作 a,比平均水平好多少」。用优势替代回报作为策略梯度的权重:

∇_θ J = E [ ∇_θ log π_θ(a|s) · A(s,a) ]

优势为正就提高该动作概率,为负就降低。这比原始回报的方差小得多。

GAE 优势估计

GAE(Generalized Advantage Estimation) 用一组 TD 误差的指数加权和估计优势,是 PPO 的标配:

δ_t = r_t + γ V(s_{t+1}) - V(s_t)
A_t^GAE = Σ_{l=0}^∞ (γλ)^l δ_{t+l}

λ 在偏差与方差之间权衡:λ=0 退化为单步 TD(低方差高偏差),λ=1 退化为蒙特卡洛(高方差低偏差)。实践中 λ=0.95 是常用值。

def compute_gae(rewards, values, dones, gamma=0.99, lam=0.95):
    advantages, gae = [], 0
    values = values + [0]                    # 末尾补 0
    for t in reversed(range(len(rewards))):
        delta = rewards[t] + gamma * values[t + 1] * (1 - dones[t]) - values[t]
        gae = delta + gamma * lam * (1 - dones[t]) * gae
        advantages.insert(0, gae)
    return advantages

顺带一提 A2C(Advantage Actor-Critic) 是同步版的 A3C:并行跑 N 个环境,每步收集所有环境的经验,算 GAE 后一次性更新。它比单环境训练稳定得多,是理解 Actor-Critic 的最佳起点。

PPO 与信赖域方法

策略梯度的根本风险是更新步太大:策略一旦崩坏,收集到的数据全废,很难恢复。TRPO 用 KL 散度约束更新幅度,但二阶优化实现复杂。PPO 用一个巧妙的裁剪目标,把信赖域约束变成一阶可优化。

裁剪目标函数

PPO 定义概率比 r_t(θ) = π_θ(a_t|s_t) / π_{θ_old}(a_t|s_t),目标函数:

L^CLIP(θ) = E [ min( r_t(θ) A_t , clip(r_t(θ), 1-ε, 1+ε) A_t ) ]

直觉:当优势为正(该动作好),提高其概率,但比值最多涨到 1+ε;当优势为负,降低概率,但比值最多降到 1-ε。超出范围后梯度归零,策略不会走太远。

def ppo_loss(new_log_probs, old_log_probs, advantages, clip_eps=0.2):
    ratio = torch.exp(new_log_probs - old_log_probs)
    surr1 = ratio * advantages
    surr2 = torch.clamp(ratio, 1 - clip_eps, 1 + clip_eps) * advantages
    return -torch.min(surr1, surr2).mean()

PPO 完整训练循环

for iteration in range(num_iters):
    # 1) 用当前策略收集一批轨迹
    rollout = collect_rollout(policy, envs, steps=2048)
    advantages = compute_gae(rollout.rewards, rollout.values,
                             rollout.dones, gamma=0.99, lam=0.95)
    returns = advantages + rollout.values

    # 2) 在同一批数据上多轮更新
    for epoch in range(10):
        for batch in rollout.minibatches(64):
            logp, entropy, value = policy.evaluate(batch.obs, batch.acts)
            pg = ppo_loss(logp, batch.old_logp, batch.advantages)
            vf = F.mse_loss(value, batch.returns)
            loss = pg + 0.5 * vf - 0.01 * entropy.mean()
            optimizer.zero_grad()
            loss.backward()
            nn.utils.clip_grad_norm_(policy.parameters(), 0.5)
            optimizer.step()

PPO 的五个关键超参:clip_eps=0.2、gamma=0.99、lam=0.95、entropy_coef=0.01、value_coef=0.5。这套默认值在多数任务上都能跑,是它成为「默认算法」的原因。

与 TRPO 的对比

维度TRPOPPO
约束方式KL 硬约束裁剪软约束
优化阶数二阶(共轭梯度)一阶(SGD)
实现复杂度高低
样本效率略高略低
工业采用度低极高

连续动作空间与确定性策略

机器人控制、自动驾驶的动作是连续值,argmax_a Q(s,a) 不再可用。

DDPG 与确定性策略

DDPG(Deep Deterministic Policy Gradient) 用一个确定性策略网络 μ_θ(s) 直接输出动作,Critic 评价 Q(s, μ_θ(s))。策略的梯度通过 Critic 反传:

∇_θ J = E [ ∇_a Q(s,a) |_{a=μ_θ(s)} · ∇_θ μ_θ(s) ]

DDPG 借用了 DQN 的两大技巧:经验回放与目标网络(这里用软更新 θ^- ← τθ + (1-τ)θ^-)。

SAC 与最大熵

SAC(Soft Actor-Critic) 在奖励里加入策略熵:

J(π) = Σ_t E [ r(s_t,a_t) + α H(π(·|s_t)) ]

熵项鼓励策略保持随机性,天然解决探索问题,且对超参更鲁棒。SAC 是当前连续控制任务的默认首选。

class GaussianPolicy(nn.Module):
    def __init__(self, obs_dim, act_dim, hidden=256):
        super().__init__()
        self.net = nn.Sequential(nn.Linear(obs_dim, hidden), nn.ReLU(),
                                 nn.Linear(hidden, hidden), nn.ReLU())
        self.mu = nn.Linear(hidden, act_dim)
        self.log_std = nn.Linear(hidden, act_dim)

    def forward(self, obs):
        h = self.net(obs)
        std = self.log_std(h).clamp(-20, 2).exp()
        return self.mu(h), std

    def sample(self, obs):
        mu, std = self(obs)
        dist = torch.distributions.Normal(mu, std)
        x = dist.rsample()                      # 重参数化采样
        action = torch.tanh(x)                  # 压缩到 [-1, 1]
        log_prob = dist.log_prob(x) - torch.log(1 - action.pow(2) + 1e-6)
        return action, log_prob.sum(-1, keepdim=True)

离散与连续的选型

场景动作空间推荐算法
游戏、推荐离散DQN / PPO
机器人关节连续SAC / TD3
大模型 token离散且巨大PPO / GRPO
混合动作混合分层或参数化

奖励设计与稀疏奖励

强化学习最大的工程难点不是算法,而是奖励函数。

奖励塑形

奖励塑形(Reward Shaping) 在原始奖励上叠加辅助信号,引导学习:

r'(s,a,s') = r(s,a,s') + γ Φ(s') - Φ(s)

Φ 是势函数。这个形式的塑形(称为势能塑形)在理论上保证不改变最优策略,是最安全的设计。

稀疏奖励的解法

当奖励只在成功时给出(如「机器人把方块放进盒子」),学习信号极其稀少。常用解法:

  • HER(Hindsight Experience Replay):把失败轨迹的目标替换成实际达成的状态,从失败中造出成功样本。
  • 好奇心驱动:用预测误差作为内在奖励,鼓励探索新状态。
  • 课程学习:从简单任务逐步过渡到困难任务。
  • 模仿学习预热:先用专家示范做行为克隆,再强化学习微调。

HER 的实现要点是重标记:取轨迹中后续某个时刻实际达成的状态作为新的目标,重算每步奖励,于是失败的轨迹也能贡献正样本。

RLHF 与 RLVR 中的强化学习应用

大模型对齐让强化学习重新成为焦点:用人类的偏好,而不是标注的答案,来优化语言模型。

RLHF 的三阶段流程

  1. 监督微调(SFT):用人工示范数据微调基座模型。
  2. 训练奖励模型(RM):收集人类对模型输出的偏好排序,训练一个打分模型。
  3. PPO 优化:用奖励模型作为奖励信号,PPO 优化语言模型。

PPO 阶段的四个模型:Actor(待优化)、Critic(估计价值)、Reward Model(打分)、Reference Model(算 KL 惩罚)。奖励里加 KL 惩罚是防止模型偏离基座太远:

R_total = R_rm(x, y) - β · KL( π_θ(y|x) || π_ref(y|x) )

实现上,奖励模型只给序列末尾一个标量分,而 KL 是逐 token 计算的,两者相加后按 token 维度广播到整条序列。

RLVR 与可验证奖励

RLVR(Reinforcement Learning with Verifiable Rewards) 用可程序化验证的奖励替代人类偏好模型——数学题对答案、代码跑单元测试、格式用正则校验。它绕开了奖励模型的偏差,是推理模型训练的核心手段。

# RLVR 的奖励:答案正确 +1,格式合规 +0.1,其余 0
def verify_reward(response, gold):
    answer = extract_boxed(response)          # 提取 \boxed{...} 中的答案
    if answer is None:
        return 0.0
    r = 1.0 if answer.strip() == gold.strip() else 0.0
    if has_valid_format(response):
        r += 0.1
    return r

GRPO(Group Relative Policy Optimization) 是 RLVR 常用的算法变体:对同一问题采样一组回答,用组内平均分作为基线,省掉了 Critic 网络。

A_i = (r_i - mean(r_1..r_G)) / std(r_1..r_G)

组内相对优势天然消除了问题难度的影响——难题组和易题组的基线各自归一化。

奖励黑客与对策

奖励模型只是人类偏好的代理,优化过度就会钻空子:

  • 长度膨胀:模型学会「长回答得分高」,输出冗长废话。对策是长度惩罚或长度归一化。
  • 谄媚(Sycophancy):迎合用户观点而非说真话。对策是用对抗数据训练奖励模型。
  • 格式刷分:只学会输出格式标记而内容空洞。对策是格式分设上限。

训练稳定性与工程实践

强化学习以「难调」著称,以下是踩坑最多的几条。

常见失稳模式

  • 价值估计爆炸:Q 值随训练无界增长。对策:梯度裁剪、目标网络、价值裁剪、降低学习率。
  • 熵坍塌:策略过早收敛到确定性,失去探索。对策:熵正则系数别调太小。
  • 奖励归一化缺失:不同任务奖励尺度差异大,同一套超参失效。对策:用 running mean/std 归一化奖励。

奖励归一化推荐用增量式更新的均值方差估计(Welford 算法),在线更新不保留历史样本,避免内存随训练增长。

并行环境与吞吐

强化学习的瓶颈常在环境交互而非梯度计算。提升吞吐的手段:

  • 向量化环境:同时跑 16~64 个环境实例,把交互并行化。
  • 异步收集:收集与训练在不同进程,用队列通信。
  • 环境步进加速:用 C++ 实现或直接操作状态数组,跳过渲染。

向量化环境(如 gymnasium.vector.SyncVectorEnv)一次 step 返回 N 个环境的转移,把交互吞吐提升一到两个数量级。

复现性与评估

强化学习结果的方差极大,同一个算法换随机种子可能差 30%。评估纪律:

  • 多个随机种子:至少 5 个种子,报告均值与标准差。
  • 评估用确定性策略:训练用随机策略,评估用均值动作。
  • 固定评估环境:单独一组固定种子的评估环境,避免评估方差。
  • 报告训练曲线:单个最终数字毫无意义,要看曲线是否稳定。

总结

强化学习的主线是「在不知道环境模型的前提下,用交互数据估计价值或直接优化策略」:MDP 与贝尔曼方程给出问题框架,价值迭代解决模型已知的小问题,Q-Learning 与 DQN 用 TD 误差加经验回放解决无模型离散控制,策略梯度与 Actor-Critic 解决连续与随机策略,PPO 用裁剪目标把不稳定的策略更新变成可用的工程算法。大模型时代,RLHF 与 RLVR 把奖励从「环境给出」变成了「人类偏好」或「程序验证」,但底层仍是同一套 PPO 骨架。落地的第一性原理是:奖励函数设计比算法选择更重要,训练稳定性比收敛速度更重要——绝大多数强化学习项目失败在奖励与评估,而不是网络结构。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「ai」更多文章

  1. GPU 共享与调度:MPS、MIG 与多租户隔离
  2. 异构推理硬件:ROCm、Intel 与国产 NPU 适配实践
  3. 前缀缓存与语义缓存:KV 复用与重复计算消除