PaperReading:Reinforcement Learning
paperreading
本文字数:9.1k 字 | 阅读时长 ≈ 37 min

PaperReading:Reinforcement Learning

paperreading
本文字数:9.1k 字 | 阅读时长 ≈ 37 min

基本概念

在讲解强化学习算法之前,需要先了解一些基本概念,方便我们后续算法的理解

1. 蒙特卡洛算法

蒙特卡罗(Monte Carlo Method)算法,其核心思想是利用随机变量的统计特性,通过大量随机试验对问题的解进行近似。其实说白了就是通过大量实验来拟合真实的期望

\[ \text{期望} \, \mathbb{E}[f(X)] = \int f(x) p(x) dx \approx \frac{1}{N} \sum_{i=1}^N f(x_i) \]

\(f(x)\) 是目标函数,\(p(x)\) 是概率密度函数。即 \(p(x)\) 理解成“哪些 \(x\) 更容易出现”,把 \(f(x)\) 理解成“当 \(x\) 出现时,数值是多少”,\(N\) 是采样次数。

2. 回报、状态价值函数和动作价值函数

强化学习中,智能体(agent)会不断与环境(environment)交互。在时间步 \(t\),智能体处于状态 \(s_t\),根据策略 \(\pi\) 选择动作 \(a_t\);环境执行动作后返回即时奖励 \(r_t\),并转移到下一个状态 \(s_{t+1}\):

\[ s_t \xrightarrow{a_t} (r_t,s_{t+1}) \]

不过,智能体不能只关心当前一步的奖励。例如,在迷宫中,某一步可能暂时没有奖励,却能让智能体在几步之后到达终点。因此,我们需要把当前及未来的奖励合在一起考虑。

从时间步 \(t\) 开始,一直到 episode 结束所获得的折扣奖励之和称为 return(回报),记作 \(G_t\):

\[ G_t = \sum_{k=0}^{T-t-1}\gamma^k r_{t+k} =r_t+\gamma r_{t+1}+\gamma^2r_{t+2}+\cdots \]

其中,\(\gamma\in[0,1]\) 是折扣因子:\(\gamma\) 越接近 1,智能体越重视长期奖励;\(\gamma\) 越小,智能体越关注近期奖励。 这是因为 \(\gamma\) 越接近 1,未来奖励衰减得越慢,智能体更愿意为了远期的高奖励暂时牺牲眼前收益;\(\gamma\) 越接近 0,未来奖励衰减得越快,智能体主要关注当前或近期奖励;当 \(\gamma=0\) 时,\(G_t=r_t\),智能体完全不考虑未来奖励;当 \(\gamma=1\) 时,所有时间步的奖励权重相同,不进行折扣。

这里需要先区分三个容易混淆的概念:

为什么价值函数要取期望?因为策略选择动作和环境状态转移都可能具有随机性。即使从同一个状态出发,每次走出的轨迹也可能不同,因此得到的 \(G_t\) 也可能不同。价值函数要衡量的不是某一次“运气好或不好”的结果,而是长期来看平均能获得多少回报。

状态价值函数

状态价值函数 \(V_\pi(s)\) 表示:智能体当前处于状态 \(s\),之后一直按照策略 \(\pi\) 行动,预计能够获得多少未来折扣回报。

\[ V_\pi(s)= \mathbb{E}_\pi \left[ G_t \mid s_t=s \right] \]

这里的期望主要来自两个方面:策略 \(\pi\) 选择动作时可能具有随机性以及环境在执行动作后可能随机转移到不同状态。

因此,\(V_\pi(s)\) 衡量的是状态 \(s\) 在策略 \(\pi\) 下的整体价值,可以通俗地理解为:当前处于这个状态,之后按照当前策略行动,未来平均能获得多少回报。 当这个期望无法计算时,就可以使用前面提到的蒙特卡洛方法:从状态 \(s\) 出发采样多条轨迹,再对这些轨迹的 return 求平均来获得了。

动作价值函数

动作价值函数,也称为状态—动作价值函数,记为 \(Q_\pi(s,a)\)。它表示:智能体当前处于状态 \(s\),首先执行指定动作 \(a\),之后继续按照策略 \(\pi\) 行动时,预计能够获得多少未来折扣回报。其定义为:

\[ Q_\pi(s,a)= \mathbb{E}_\pi \left[ G_t \mid s_t=s,\ a_t=a \right] \]

与状态价值函数相比,动作价值函数额外指定了当前时刻采取的动作 \(a\)。需要注意的是,只有当前动作被固定为 \(a\),从下一个时间步开始,智能体仍然按照策略 \(\pi\) 选择后续动作。因此,\(Q_\pi(s,a)\) 可以通俗地理解为:在当前状态下,先采取这个动作,未来平均能获得多少回报。

状态价值函数与动作价值函数的区别

二者最关键的区别是:是否指定当前要执行的动作。 换句话说,\(Q_\pi(s,a)\) 是先分别评价每个动作,而 \(V_\pi(s)\) 是站在当前策略的角度,对这些动作价值做一次综合评价。他们的关系如下,可以理解为状态价值就是所有动作价值按照策略概率计算的加权平均:

\[ V_\pi(s)= \sum_a \pi(a\mid s)Q_\pi(s,a) \]

3. 为什么要对未来奖励进行折扣?

4. 用一个迷宫例子串起来

下面通过一个完整的迷宫例子,统一理解即时奖励 \(r_t\)、return \(G_t\)、动作价值 \(Q_\pi(s,a)\) 和状态价值 \(V_\pi(s)\)。

假设机器人当前位于迷宫中的一个路口,状态记为 \(s_0\)。机器人可以选择“向左”或“向右”,当前策略为:

\[ \pi(\text{向左}\mid s_0)=0.7 \quad \pi(\text{向右}\mid s_0)=0.3 \]

选择向左:假设机器人选择向左后,在连续四个时间步中获得的奖励分别为:

令折扣因子为 \(\gamma=0.9\),则这条实际轨迹从时间步 0 开始的折扣回报为:

\[ \begin{aligned} G_0^{\text{左}} &=r_0+\gamma r_1+\gamma^2r_2+\gamma^3r_3 \\ &=1+0.9\times2+0.9^2\times3+0.9^3\times4 \\ &=1+1.8+2.43+2.916 \\ &=8.146 \end{aligned} \]

这里的 \(G_0^{\text{左}}=8.146\) 表示:在这一次实际轨迹中,机器人从当前位置向左走后,获得的未来折扣奖励总和为 8.146。由于这里假设环境是确定的,向左走总会得到相同的后续奖励,因此:

\[ Q_\pi(s_0,\text{向左})=8.146 \]

它表示:机器人处于状态 \(s_0\) 时,先选择向左,之后继续按照策略 \(\pi\) 行动,预计能够获得的折扣回报为 8.146。

选择向右:假设机器人选择向右后,连续四个时间步获得的奖励分别为:

则向右这条轨迹的折扣回报为:

\[ \begin{aligned} G_0^{\text{右}} &=0+0.9\times1+0.9^2\times1+0.9^3\times1 \\ &=0+0.9+0.81+0.729 \\ &=2.439 \end{aligned} \]

同样,由于这里假设环境是确定的,因此:

\[ Q_\pi(s_0,\text{向右})=2.439 \]

这表示:机器人处于状态 \(s_0\) 时,先选择向右,预计能够获得的折扣回报为 2.439。

而之前我们提到过,状态价值等于各个动作价值按照策略概率计算的加权平均:

\[ V_\pi(s_0)= \sum_a \pi(a\mid s_0)Q_\pi(s_0,a) \]

代入当前策略和两个动作的价值:

\[ \begin{aligned} V_\pi(s_0) &= 0.7\times Q_\pi(s_0,\text{向左}) \mathrel{+} 0.3\times Q_\pi(s_0,\text{向右}) \\ &= 0.7\times8.146+0.3\times2.439 \\ &= 5.7022+0.7317 \\ &= 6.4339 \end{aligned} \]

因此:

\[ V_\pi(s_0)=6.4339 \]

它表示:机器人当前处于状态 \(s_0\),如果按照策略 \(\pi\) 选择动作并继续行动,未来平均能够获得 6.4339 的折扣回报。

后面的 Policy Gradient 正是利用采样得到的 \(G_t\) 来判断动作的好坏,并进一步调整策略 \(\pi_\theta(a\mid s)\)。

1. Policy Gradient

1.1 什么是 Policy Gradient?

Policy Gradient(策略梯度) 是强化学习(Reinforcement Learning)中直接对策略函数进行优化的一类方法。其目标是:直接最大化策略参数 \(\theta\) 下,agent 的期望总回报:

\[ J(\theta) = \mathbb{E}_{\tau \sim \pi_\theta} [R(\tau)] \]

注意:\(R(\tau)\) 表示整条轨迹 \(\tau\) 的折扣累计回报,即从轨迹起点开始计算的 \(G_0\)。

1.2 策略梯度的基本思想

Policy Gradient 则通过直接建模策略 \(\pi_\theta(a|s)\) 来优化策略,通常用策略网络(也就是一个神经网络)来输出动作,这个动作与环境进行交互并输出 reward 来衡量当前动作的好坏,最后我们根据得到的 reward 来优化策略网络。

我们希望优化一个策略 \(\pi_\theta(a_t | s_t)\),使得在这个策略下,执行一条轨迹(一个完整 episode)后获得的总奖励最大:

\[ J(\theta) = \mathbb{E}_{\pi_\theta} [\sum_t r_t] \]

我们希望求 \(J(\theta)\) 对策略参数的梯度,以便进行优化。由于 \(J(\theta)\) 是一个期望,直接求梯度不方便。于是我们借助 score function trick(也叫 log-derivative trick),有:

\[ \nabla_\theta \mathbb{E}_{x \sim p_\theta(x)}[f(x)] = \mathbb{E}_{x \sim p_\theta(x)} [f(x) \nabla_\theta \log p_\theta(x)] \]

将这个技巧应用于 \(J(\theta)\),可以推导出:

\[ \nabla_\theta J(\theta) = \mathbb{E}_{\tau \sim \pi_\theta} \left[ \sum_t \nabla_\theta \log \pi_\theta(a_t | s_t) \cdot G_t \right] \]

其中:

1.3 标准策略梯度算法

知道这个整体的大致流程之后,我们看一下一个完整的 REINFORCE 算法训练过程。这个算法的核心思想是:根据动作带来的回报调整其被选择的概率,回报越高,动作概率被增强得越明显;当回报为负时,则会降低该动作被选择的概率。

算法流程

  1. 初始化策略网络参数 \(\theta\)。它的作用是输入一个状态 \(s\),输出一个动作 \(a\) 的概率分布。
  2. 循环训练:重复下面的过程,直到达到训练轮数或策略收敛:

算法总结

算法通过蒙特卡洛方法估计策略梯度,并沿该方向更新参数以期获得更高回报。优缺点如下

为解决这些问题,后续发展出了 Actor-Critic、PPO 等方法,引入基线、价值函数等技巧以降低方差、提高效率。但理解 REINFORCE 是掌握高级算法的基础。

举个具体例子加深理解

回合制游戏策略梯度完整计算小游戏

首先我们采样一条轨迹 \(\tau\),假设有 3 个时间步,状态 \(s_t\)、动作 \(a_t\)、奖励 \(r_t\),如下,然后接下来,我们计算每个时间步的梯度 \(\nabla_\theta \log \pi_\theta(a_t | s_t)\),假设策略网络输出的概率如下表所示:

\(t\) 状态 \(s_t\) 动作 \(a_t\) 奖励 \(r_t\) 回报 \(G_t\)(假设不折扣 \(\gamma=1\)) 策略概率 \(p_t\) \(\log \pi_\theta(a_t\mid s_t)\)
0 起点 向右 (1) 0 \(r_0 + r_1 + r_2 = 0 + 0 + 1 = 1\) 0.5 -0.693
1 前进一步 向右 (1) 0 \(r_1 + r_2 = 0 + 1 = 1\) 0.6 -0.511
2 接近终点 向右 (1) 1 \(r_2 = 1\) 0.7 -0.357

计算累计 loss:\(L = -\sum_t \log \pi_\theta(a_t|s_t) G_t\),逐步代入结果如下
\[ \begin{align*} \text{loss} &= -\big( (-0.693) \cdot 1 + (-0.511) \cdot 1 + (-0.357) \cdot 1 \big) \\ &= -\big( -0.693 - 0.511 - 0.357 \big) \\ &= -(-1.561) \\ &= 1.561 \end{align*} \]

代码形式写出来

# 假设策略网络输出的概率
p0, p1, p2 = 0.5, 0.6, 0.7

# 计算 log 概率
log_p0 = torch.log(torch.tensor(p0))  # ≈ -0.693
log_p1 = torch.log(torch.tensor(p1))  # ≈ -0.511
log_p2 = torch.log(torch.tensor(p2))  # ≈ -0.357

# 每一步的 return
G0, G1, G2 = 1.0, 1.0, 1.0

# loss
loss = -(log_p0 * G0 + log_p1 * G1 + log_p2 * G2)
print(loss.item())  # ≈ 1.561

这里的 \(p_0,p_1,p_2\) 是固定数值,因此这段代码只用来验证 loss 的计算结果。实际训练时,概率由策略网络输出,再通过 loss.backward()optimizer.step() 更新网络参数,完整过程见下面的 CartPole 例子。

为什么这个 loss 会鼓励动作概率更高?从 \(\nabla_\theta L = - \sum_t G_t \nabla_\theta \log \pi_\theta(a_t|s_t)\) 公式可以看出:

下面是一个完整的 Policy Gradient 的例子。代码与上面的公式基本对应:action_probs 对应 \(\pi_\theta(a_t|s_t)\),log_probs 对应 \(\log \pi_\theta(a_t|s_t)\),returns_tensor 对应 \(G_t\),policy_loss 对应策略损失 \(L\)。为了使训练更稳定,代码会在每批轨迹内对 \(G_t\) 进行标准化。

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.distributions import Bernoulli
from itertools import count
import gymnasium as gym
from datetime import datetime


class PolicyNet(nn.Module):
    def __init__(self):
        super(PolicyNet, self).__init__()
        self.fc1 = nn.Linear(4, 24)
        self.fc2 = nn.Linear(24, 36)
        self.fc3 = nn.Linear(36, 1)  # 动作 1(向右)的概率

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = torch.sigmoid(self.fc3(x))
        return x


def main():
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

    # Parameters
    num_episodes = 5000
    batch_size = 5
    learning_rate = 0.01
    gamma = 0.99

    # Create environment
    env = gym.make('CartPole-v1')
    policy_net = PolicyNet().to(device)
    optimizer = torch.optim.RMSprop(policy_net.parameters(), lr=learning_rate)

    # 保存一批 trajectories
    states_pool = []
    actions_pool = []
    rewards_pool = []

    running_reward = 0
    running_loss = 0
    best_reward = 0
    start_time = datetime.now()

    print("\nTraining started...")
    print("Episode | Duration | Reward | Running |  Loss  | Time")
    print("-" * 65)

    success_count = 0
    for episode in range(num_episodes):
        state, _ = env.reset()
        state = torch.from_numpy(state).float().to(device)
        episode_reward = 0
        episode_loss = 0
        episode_steps = 0

        # 单独保存一个 episode 的轨迹
        episode_states = []
        episode_actions = []
        episode_rewards = []


        for t in count():
            action_prob = policy_net(state)
            action_dist = Bernoulli(action_prob)
            action = action_dist.sample()
            action = int(action.item())

            next_state, reward, terminated, truncated, _ = env.step(action)
            done = terminated or truncated
            episode_reward += reward

            # Append transition to current episode
            episode_states.append(state)
            episode_actions.append(torch.tensor([float(action)], dtype=torch.float32).to(device))
            episode_rewards.append(reward)

            state = torch.from_numpy(next_state).float().to(device)
            episode_steps += 1

            if done:
                break

        # 将当前 episode 数据加入全局 pool
        states_pool.append(episode_states)
        actions_pool.append(episode_actions)
        rewards_pool.append(episode_rewards)

        # 每 batch_size 回合更新一次 policy
        if (episode + 1) % batch_size == 0:
            # 计算每个时间步的折扣累计回报 G_t
            all_returns = []
            for episode_rewards in rewards_pool:
                G_t = 0
                episode_returns = []
                for reward in reversed(episode_rewards):
                    G_t = reward + gamma * G_t
                    episode_returns.insert(0, G_t)
                all_returns.extend(episode_returns)

            # 标准化 return,降低梯度方差
            returns_tensor = torch.tensor(all_returns, dtype=torch.float32, device=device)
            returns_tensor = (returns_tensor - returns_tensor.mean()) / (returns_tensor.std(unbiased=False) + 1e-8)

            # 批量化反向传播
            optimizer.zero_grad()

            # 扁平化拼接所有状态和动作
            flat_states = [state for episode_states in states_pool for state in episode_states]
            flat_actions = [action for episode_actions in actions_pool for action in episode_actions]
            states_tensor = torch.stack(flat_states).to(device)         # shape: [N, 4]
            actions_tensor = torch.cat(flat_actions).to(device)         # shape: [N]  # 注意 actions 是 [0] or [1] 的 float32

            # 模型预测 & 构建伯努利分布
            action_probs = policy_net(states_tensor).squeeze(1)         # shape: [N]
            action_dist = Bernoulli(action_probs)

            # L = -E[G_t * log pi_theta(a_t|s_t)]
            log_probs = action_dist.log_prob(actions_tensor)            # shape: [N]
            policy_loss = -(log_probs * returns_tensor).mean()

            # 反向传播
            policy_loss.backward()
            optimizer.step()
            episode_loss = policy_loss.item()

            # 清空池子
            states_pool = []
            actions_pool = []
            rewards_pool = []

        # Update metrics
        running_reward = 0.95 * running_reward + 0.05 * episode_reward
        running_loss = 0.95 * running_loss + 0.05 * episode_loss

        if episode_reward > best_reward:
            best_reward = episode_reward

        if episode % 2 == 0:
            elapsed_time = datetime.now() - start_time
            print(f"{episode:7d} | {episode_steps:8d} | {episode_reward:6.1f} |  {running_reward:7.1f} |  {episode_loss:4.2f}  | {elapsed_time}")
        if running_reward > 195:
            success_count += 1
            if success_count >= 10:   # 确保稳定性
                print(f"\nSolved in {episode} episodes!")
                break
        else:
            success_count = 0

    print(f"\nTraining finished!")
    print(f"Best reward: {best_reward:.1f}")
    print(f"Final running reward: {running_reward:.1f}")
    print(f"Total training time: {datetime.now() - start_time}")
    env.close()


if __name__ == '__main__':
    main()

2. PPO

2.1 什么是 PPO?

PPO(Proximal Policy Optimization,近端策略优化) 是一种改进的策略梯度算法。它在更新策略时限制新策略与旧策略之间的变化幅度,避免策略在一次更新中发生过大的改变,从而提高训练的稳定性。

PPO 的核心目标函数为:

\[ L^{\mathrm{CLIP}}(\theta)= \mathbb{E}_t \left[ \min \left( \rho_t(\theta)\hat{A}_t,\, \operatorname{clip} \left( \rho_t(\theta),1-\epsilon,1+\epsilon \right) \hat{A}_t \right) \right] \]

其中:

新旧策略概率比定义为:

\[ \rho_t(\theta)= \frac{\pi_\theta(a_t\mid s_t)} {\pi_{\theta_{\mathrm{old}}}(a_t\mid s_t)} \]

这里使用 \(\rho_t(\theta)\) 表示概率比,是为了避免与即时奖励 \(r_t\) 混淆。概率比可以直观地理解为:

PPO 的基本思想可以概括为:

如果一个动作优于平均水平,就提高它被选择的概率;如果一个动作劣于平均水平,就降低它被选择的概率。但无论提高还是降低,都不能在一次更新中改变得过多。

2.2 PPO 的训练流程

PPO 的训练过程主要包括初始化、数据收集、优势估计和网络优化四个阶段。

初始化

  1. 初始化策略网络参数 \(\theta\),即 Actor。
  2. 初始化价值网络参数 \(\phi\),即 Critic。
  3. 设置相关超参数,例如:学习率 \(\alpha\);折扣因子 \(\gamma\);GAE 参数 \(\lambda\);裁剪系数 \(\epsilon\);每批数据的优化轮数 \(K\);mini-batch 大小。

Actor 和 Critic 可以是两个独立的网络,也可以共享一部分底层网络,使用不同预测头输出结果:

主训练循环:对于每一轮训练,依次执行以下步骤。

1. 使用旧策略收集数据

使用旧策略 \(\pi_{\theta_{\mathrm{old}}}\) 与环境交互,直到收集到预设数量的时间步,例如 2048 个时间步。在每个时间步 \(t\):

  1. 根据策略 \(\pi_{\theta_{\mathrm{old}}}(a_t\mid s_t)\) 采样动作 \(a_t\);
  2. 在环境中执行动作 \(a_t\);
  3. 获得即时奖励 \(r_t\) 和下一个状态 \(s_{t+1}\);
  4. 记录当前 episode 是否结束的标志 \(d_t\);
  5. 保存旧策略下该动作的 log probability;
  6. 保存 Critic 对当前状态的价值估计。

通常需要保存以下数据,这些数据共同构成一批 rollout

\[ (s_t,a_t,r_t,s_{t+1},d_t) \quad \log\pi_{\theta_{\mathrm{old}}}(a_t\mid s_t) \quad V_{\phi_{\mathrm{old}}}(s_t) \]

2. 计算优势值和价值目标

使用收集到的奖励和 Critic 预测的状态价值,计算每个时间步的优势估计 \(\hat{A}_t\)。PPO 中通常使用 GAE(Generalized Advantage Estimation,广义优势估计)计算优势值,以平衡偏差和方差。随后,根据优势值构造 Critic 的学习目标:

\[ V_{\\mathrm{target},t}= \hat{A}_t+V_{\phi_{\mathrm{old}}}(s_t) \]

\(\hat{A}_t\) 用于训练 Actor,\(V_{\mathrm{target},t}\) 用于训练 Critic。

3. 对同一批数据进行多轮优化

将 rollout 数据随机打乱,并划分为多个 mini-batch。随后对同一批 rollout 数据进行 \(K\) 轮优化。对于每个 mini-batch,需要进行以下计算。新策略下动作的概率比为:

\[ \begin{aligned} \rho_t(\theta) &= \frac{\pi_\theta(a_t\mid s_t)} {\pi_{\theta_{\mathrm{old}}}(a_t\mid s_t)} \\ &= \exp \left( \log\pi_\theta(a_t\mid s_t) - \log\pi_{\theta_{\mathrm{old}}}(a_t\mid s_t) \right) \end{aligned} \]

PPO 的策略目标为:

\[ L^{\mathrm{CLIP}}(\theta) = \mathbb{E}_t \left[ \min \left( \rho_t(\theta)\hat{A}_t,\, \operatorname{clip} \left( \rho_t(\theta),1-\epsilon,1+\epsilon \right) \hat{A}_t \right) \right] \]

由于深度学习优化器通常执行最小化,因此实际使用的策略损失为:

\[ L^{\mathrm{policy}}(\theta) = -L^{\mathrm{CLIP}}(\theta) \]

Critic 的价值函数损失通常使用均方误差:

\[ L^{\mathrm{value}}(\phi) = \frac{1}{2} \mathbb{E}_t \left[ \left( V_\phi(s_t)-V_{\mathrm{target},t} \right)^2 \right] \]

为了避免策略过早变得过于确定,PPO 通常还会加入熵奖励:

\[ H(\pi_\theta) = \mathbb{E}_t \left[ H\left(\pi_\theta(\cdot\mid s_t)\right) \right] \]

策略分布的熵越大,说明动作分布越分散,策略通常具有更强的探索能力。

最终的总损失可以写成:

\[ L(\theta,\phi) = L^{\mathrm{policy}}(\theta) + c_vL^{\mathrm{value}}(\phi) - c_eH(\pi_\theta) \]

\(c_v\):价值函数损失系数,\(c_e\):熵奖励系数。最后,计算总损失对参数 \(\theta\) 和 \(\phi\) 的梯度,并使用 Adam 等优化器更新 Actor 和 Critic。

4. 重新收集数据

完成 \(K\) 轮优化后,当前 rollout 数据会被丢弃。随后使用更新后的策略重新与环境交互,收集下一批数据。因此,PPO 虽然可以对同一批 rollout 数据重复训练多轮,但它仍然属于 on-policy 算法。它只能有限地重复使用当前策略附近采集的数据,不能像典型的 off-policy 算法那样长期使用任意历史数据。

PPO 的整体流程可以概括为:

使用当前策略收集 rollout
        ↓
保存奖励、旧动作概率和旧价值预测
        ↓
计算 TD 误差、GAE 和价值目标
        ↓
将 rollout 划分为多个 mini-batch
        ↓
对同一批数据优化 K 轮
        ↓
丢弃当前 rollout
        ↓
使用更新后的策略重新收集数据

PPO 的核心在于:通过裁剪目标函数,在鼓励策略改进的同时,限制一次更新的幅度,从而提高策略训练的稳定性。

2.3 简单代码实现

下面是 PPO 策略损失的核心代码片段:

# 当前策略输出动作概率分布
probs = policy_net(states)
dist = torch.distributions.Categorical(probs)

# 当前策略下,已采样动作的 log probability
new_log_probs = dist.log_prob(actions)

# 新旧策略概率比
ratio = torch.exp(new_log_probs - old_log_probs)

# 未裁剪目标
surrogate_1 = ratio * advantages

# 裁剪后的目标
surrogate_2 = torch.clamp(
    ratio,
    1.0 - epsilon,
    1.0 + epsilon
) * advantages

# PPO 最大化目标,代码中转化为最小化负目标
policy_loss = -torch.min(
    surrogate_1,
    surrogate_2
).mean()

optimizer.zero_grad()
policy_loss.backward()
optimizer.step()

其中:

这段代码只展示了 Actor 的策略损失,没有包含 Critic 损失、熵奖励、GAE 计算和多轮 mini-batch 优化。

2.4 PPO 优化目标

看完整个 PPO 流程后,一个关键问题是:为什么需要设计这样的裁剪目标函数?PPO 主要解决的是策略梯度更新不稳定的问题。传统策略梯度方法可以使用下面的目标更新策略:

\[ L^{\mathrm{PG}}(\theta) = \mathbb{E}_t \left[ \log\pi_\theta(a_t\mid s_t)\hat{A}_t \right] \]

其中,\(\hat{A}_t\) 表示动作 \(a_t\) 相对于状态 \(s_t\) 下平均表现的优势。

在最简单的 REINFORCE 中,可以直接使用折扣回报 \(G_t\) 作为策略更新信号,相当于将基线设为 0。引入价值函数作为基线后,则可以使用:

\[ \hat{A}_t = G_t-V(s_t) \]

但是,如果学习率过大,或者在同一批数据上更新次数过多,新策略可能迅速偏离采集数据时的旧策略。这会带来两个问题:

  1. rollout 中的动作是由旧策略采样的。当新策略变化过大时,这批数据将不再能够准确反映新策略的行为。
  2. 一次过大的更新可能破坏原本已经学到的有效策略,造成性能骤降。

TRPO 的解决方法

TRPO(Trust Region Policy Optimization,信任区域策略优化)通过 KL 散度直接约束新旧策略之间的差异:

\[ D_{\mathrm{KL}} \left( \pi_{\theta_{\mathrm{old}}} \Vert \pi_\theta \right) \leq\delta \]

其优化目标可以表示为:

\[ \max_\theta \mathbb{E}_t \left[ \frac{\pi_\theta(a_t\mid s_t)} {\pi_{\theta_{\mathrm{old}}}(a_t\mid s_t)} \hat{A}_t \right] \]

同时满足:

\[ D_{\mathrm{KL}} \left( \pi_{\theta_{\mathrm{old}}} \Vert \pi_\theta \right) \leq\delta \]

TRPO 能够有效限制策略变化,但其优化过程较复杂,通常需要近似二阶优化。

PPO 使用裁剪代理目标,以更简单的一阶优化方式近似实现类似的保守更新:

\[ L^{\mathrm{CLIP}}(\theta) = \mathbb{E}_t \left[ \min \left( \rho_t(\theta)\hat{A}_t,\, \operatorname{clip} \left( \rho_t(\theta),1-\epsilon,1+\epsilon \right) \hat{A}_t \right) \right] \]

目标函数中的两个部分分别为:

PPO 对两者取较小值,相当于选择一个更加保守的策略更新收益。

当优势为正时

假设:

\[ \hat{A}_t>0 \]

说明当前动作优于平均水平,因此希望提高该动作的概率,即:

\[ \rho_t(\theta)>1 \]

但是,当概率比超过:

\[ 1+\epsilon \]

继续提高该动作的概率,不会继续增大裁剪目标。此时,PPO 不再鼓励策略沿这个方向进一步大幅更新。

当优势为负时

假设:

\[ \hat{A}_t<0 \]

说明当前动作劣于平均水平,因此希望降低该动作的概率,即:

\[ \rho_t(\theta)<1 \]

但是,当概率比低于:

\[ 1-\epsilon \]

继续降低该动作的概率,也不会继续改善裁剪目标。PPO 同样不再鼓励策略沿这个方向进一步大幅更新。

可以总结为:

优势值 期望的更新方向 PPO 限制的情况
\(\hat{A}_t>0\) 提高动作概率 概率提高过多,即 \(\rho_t>1+\epsilon\)
\(\hat{A}_t<0\) 降低动作概率 概率降低过多,即 \(\rho_t<1-\epsilon\)

需要注意的是,clip 并不是简单地将所有概率比强制限制在 \([1-\epsilon,1+\epsilon]\) 内。

它限制的是:

当策略已经沿着正确方向更新得足够多时,不再继续奖励更大的更新幅度。

如果策略朝错误的方向变化,例如优势为正的动作概率反而降低,PPO 仍然会对这种变化进行惩罚。

2.5 为什么要引入 Critic 网络?

Critic 网络的主要作用是估计状态价值,从而为 Actor 提供更加稳定、更加准确的学习信号。

如果没有 Critic,REINFORCE 可以直接使用折扣回报 \(G_t\) 更新策略:

\[ \nabla_\theta J(\theta) \approx \sum_t G_t \nabla_\theta \log\pi_\theta(a_t\mid s_t) \]

但是,直接使用 \(G_t\) 存在两个主要问题。

1. 回报的方差较高

\(G_t\) 包含从当前时间步开始的所有未来奖励,因此会受到以下随机性的影响:

因此,即使当前动作完全相同,不同轨迹得到的 \(G_t\) 也可能存在较大差异,导致策略梯度估计波动较大。

2. 缺少状态相关的比较基准

仅知道一个动作获得了多少回报,并不能判断这个动作相对于当前状态下的平均水平是否足够好。

例如,在某个状态下:

虽然回报 10 是正数,但该动作实际上低于平均水平,不应该提高其概率。因此,引入优势函数:

\[ A^\pi(s_t,a_t) = Q^\pi(s_t,a_t)-V^\pi(s_t) \]

其中:

如果:

\[ A^\pi(s_t,a_t)>0 \]

说明动作优于平均水平,应提高其概率。

如果:

\[ A^\pi(s_t,a_t)<0 \]

说明动作劣于平均水平,应降低其概率。

PPO 通常不直接训练一个 \(Q\) 网络,而是使用 Critic 估计状态价值:

\[ V_\phi(s) \approx V^\pi(s) \]

随后,利用奖励和 Critic 的价值预测计算优势估计 \(\hat{A}_t\)。

Critic 可以理解为一个评价者:

Actor 负责选择动作,Critic 负责判断当前状态通常能够获得多少回报,从而帮助判断本次动作是否优于平均水平。

对比项 无 Critic 有 Critic
策略更新信号 折扣回报 \(G_t\) 优势估计 \(\hat{A}_t\)
评价方式 本次轨迹获得多少回报 当前动作比状态平均水平好多少
方差 通常较高 通常较低
稳定性 梯度波动较大 相对更加稳定
额外网络 不需要价值网络 需要 Critic 估计 \(V(s)\)

引入 Critic 并不会改变最终目标,智能体仍然希望最大化期望回报。Critic 只是为 Actor 提供了一个方差更低的训练信号。

2.6 PPO 的优势函数与 GAE

在 PPO 中,Actor 根据优势函数更新策略。

状态价值函数、动作价值函数和优势函数分别定义为:

\[ V^\pi(s_t) = \mathbb{E}_\pi \left[ G_t\mid s_t \right] \]

\[ Q^\pi(s_t,a_t) = \mathbb{E}_\pi \left[ G_t\mid s_t,a_t \right] \]

\[ A^\pi(s_t,a_t) = Q^\pi(s_t,a_t)-V^\pi(s_t) \]

优势函数衡量的是:

在状态 \(s_t\) 下,执行动作 \(a_t\) 相对于按照当前策略选择动作的平均表现好多少。

但是,在实际训练中,真实的 \(Q^\pi\) 和 \(V^\pi\) 都是未知的,只能通过采样和神经网络进行估计。

TD 误差

Critic 使用神经网络估计状态价值:

\[ V_\phi(s_t) \approx V^\pi(s_t) \]

基于 Critic 的价值预测,可以定义单步 TD 误差:

\[ \delta_t = r_t + \gamma V_\phi(s_{t+1}) - V_\phi(s_t) \]

如果需要考虑 episode 是否结束,可以写成:

\[ \delta_t = r_t + \gamma(1-d_t)V_\phi(s_{t+1}) - V_\phi(s_t) \]

其中:

TD 误差可以理解为:

当前实际奖励加上下一个状态的预测价值,与当前状态原有预测价值之间的差值。

如果 \(\delta_t>0\),说明这一步的实际结果比 Critic 原先预期的更好;如果 \(\delta_t<0\),说明实际结果比预期更差。

单步 TD 误差只观察一步之后的状态,方差通常较低,但较为依赖 Critic 的预测,因此可能带来一定偏差。

蒙特卡洛优势估计

另一种简单方法是使用完整折扣回报减去状态价值:

\[ \hat{A}_t = G_t-V_\phi(s_t) \]

这种方法使用完整轨迹得到的回报,偏差相对较低,但会受到未来所有奖励和状态转移随机性的影响,因此方差较高。

GAE

GAE(Generalized Advantage Estimation,广义优势估计)在单步 TD 和蒙特卡洛估计之间进行平衡。

其定义为:

\[ \hat{A}_t^{\mathrm{GAE}(\gamma,\lambda)} = \sum_{l=0}^{\infty} (\gamma\lambda)^l \delta_{t+l} \]

展开后为:

\[ \hat{A}_t = \delta_t + \gamma\lambda\delta_{t+1} + (\gamma\lambda)^2\delta_{t+2}+\cdots \]

实际代码中通常使用反向递推计算:

\[ \hat{A}_t = \delta_t + \gamma\lambda(1-d_t)\hat{A}_{t+1} \]

其中,\(\lambda\in[0,1]\) 用于控制偏差和方差之间的权衡。

\[ \hat{A}_t=\delta_t \]

此时退化为单步 TD 误差,通常具有较低方差,但更加依赖 Critic 的预测,偏差可能较高。

GAE 会考虑更多未来时间步的信息。在完整轨迹和终止状态正确处理的情况下,它接近蒙特卡洛优势估计,通常偏差较低,但方差较高。

实际训练中,\(\lambda\) 经常取:

\[ \lambda=0.95 \]

在计算出优势后,可以构造 Critic 的价值目标:

\[ V_{\mathrm{target},t} = \hat{A}_t + V_{\phi_{\mathrm{old}}}(s_t) \]

Critic 通过最小化以下损失学习价值目标:

\[ L^{\mathrm{value}}(\phi) = \frac{1}{2} \mathbb{E}_t \left[ \left( V_\phi(s_t)-V_{\mathrm{target},t} \right)^2 \right] \]

实际实现中,通常还会对一个 batch 内的优势值进行标准化:

\[ \hat{A}_t \leftarrow \frac{ \hat{A}_t-\operatorname{mean}(\hat{A}) }{ \operatorname{std}(\hat{A})+\varepsilon } \]

优势标准化不是 PPO 理论公式中的必要部分,但通常有助于提高数值稳定性。

GAE 的主要作用包括:

最后,可以通过下面的表格理解 PPO 中三个主要价值函数之间的关系:

函数 符号 PPO 中的处理方式
状态价值函数 \(V^\pi(s)\) 由 Critic 网络估计,输出近似值 \(V_\phi(s)\)
动作价值函数 \(Q^\pi(s,a)\) 通常不单独训练 Q 网络,而是通过奖励和状态价值间接体现
优势函数 \(A^\pi(s,a)=Q^\pi(s,a)-V^\pi(s)\) 使用 GAE 得到优势估计 \(\hat{A}_t\),用于更新 Actor

PPO 可以用一句话概括:

PPO 使用 Critic 和 GAE 判断一个动作相对于平均水平好多少,再根据新旧策略概率比调整该动作的概率,并通过裁剪目标限制策略一次更新得过远。

Sep 06, 2026
Aug 01, 2026