- 参考链接:
PPO 方法介绍
PPO 的目标
- PPO目标定义
$$
\begin{aligned}
\max_{\theta_\text{new}} \quad &\mathbb{E}_{s \sim \rho_{\pi_{\theta_\text{old}}}, a \sim \pi_{\theta_\text{old}}}\left[\frac{\pi_{\theta_\text{new}}(a|s)}{\pi_{\theta_\text{old}}(a|s)} A_{\pi_{\theta_\text{old}}}(s,a)\right] \\
&\text{s.t. } \quad \quad \mathbb{E}_{s \sim \rho_{\pi_{\theta_\text{old}}}} \left[D_{\text{KL}}(\pi_{\theta_\text{old}}, \pi_{\theta_\text{new}})\right] \le \delta
\end{aligned}
$$ - PPO目标详细推导见RL——TRPO-PPO-目标函数基础推导
PPO-Penalty
- 又名PPO-惩罚
$$
\begin{aligned}
\max_{\theta}&\ \ \mathbb{E}_{s \sim \rho_{\pi_{\theta_\text{old}}}, a \sim \pi_{\theta_\text{old}}}\left[\frac{\pi_\theta(a|s)}{\pi_{\theta_{\text{old}}}(a|s)}A_{\theta_{\text{old}}}(s,a) - \beta D_{KL}(\pi_{\theta_{\text{old}}}(\cdot|s), \pi_\theta(\cdot|s))\right]
\end{aligned}
$$
PPO-Clip
- 又名PPO截断
$$
\begin{aligned}
\max_\theta&\ \ \mathbb{E}_{s\sim \rho_{\theta_{\text{old}}},a\sim q(a|s)}\min\left(\color{blue}{\frac{\pi_\theta(a|s)}{q(a|s)}A_{\theta_{\text{old}}}(s,a)}, \color{red}{clip\left(\frac{\pi_\theta(a|s)}{q(a|s)}, 1-\epsilon, 1+\epsilon\right)A_{\theta_{\text{old}}}(a,s)}\right)
\end{aligned}
$$ - 理论上,以上采样分布可以是任意分布 ,实际上使用Old策略效果更好,样本利用率也更高,所以常用的PPO目标一般会如下定义:
$$
\begin{aligned}
\max_\theta&\ \ \mathbb{E}_{s \sim \rho_{\pi_{\theta_\text{old}}}, a \sim \pi_{\theta_\text{old}}}\min\left(\color{blue}{\frac{\pi_\theta(a|s)}{\pi_{\theta_{\text{old}}}(a|s)}A_{\theta_{\text{old}}}(s,a)}, \color{red}{clip\left(\frac{\pi_\theta(a|s)}{\pi_{\theta_{\text{old}}}(a|s)}, 1-\epsilon, 1+\epsilon\right)A_{\theta_{\text{old}}}(a,s)}\right)
\end{aligned}
$$- 以上目标是 \(\max_\theta\),实际实现时会在令损失函数等于负的目标函数
- 令 \(r(\theta) = \frac{\pi_\theta(a|s)}{\pi_{\theta_{\text{old}}}(a|s)} \),则有:
$$
\begin{aligned}
\max_\theta&\ \ \mathbb{E}_{s \sim \rho_{\pi_{\theta_\text{old}}}, a \sim \pi_{\theta_\text{old}}}\min\left(\color{blue}{r(\theta)A_{\theta_{\text{old}}}(s,a)}, \color{red}{clip\left(r(\theta), 1-\epsilon, 1+\epsilon\right)A_{\theta_{\text{old}}}(a,s)}\right)
\end{aligned}
$$
PPO-Clip 进阶讨论
- 副标题:PPO-Clip的损失函数究竟在做什么?为什么需要使用 \(\min\) 操作?
- 参考链接:如何理解 PPO-CLIP 目标函数中的 clip 和 min 操作?过犹不及论 - Finch的文章 - 知乎
- PPO的目标定义如下:
$$
\begin{aligned}
\max_\theta&\ \ \mathbb{E}_{s \sim \rho_{\pi_{\theta_\text{old}}}, a \sim \pi_{\theta_\text{old}}}\min\left(\color{blue}{\frac{\pi_\theta(a|s)}{\pi_{\theta_{\text{old}}}(a|s)}A_{\theta_{\text{old}}}(s,a)}, \color{red}{clip\left(\frac{\pi_\theta(a|s)}{\pi_{\theta_{\text{old}}}(a|s)}, 1-\epsilon, 1+\epsilon\right)A_{\theta_{\text{old}}}(a,s)}\right)
\end{aligned}
$$- 注意: 发生截断时, \((1+\epsilon)A_{\theta_{\text{old}}}(a,s)\) 或 \((1-\epsilon)A_{\theta_{\text{old}}}(a,s)\) 对策略参数 \(\theta\) 的梯度为 0 :
- 常数 \((1+\epsilon)\) 和 \((1+\epsilon)\) 对策略参数的梯度为 0
- \(A_{\theta_{\text{old}}}(a,s)\) 本身与旧策略有关(但本质上也只是按照旧策略与环境交互得到 Reward 而已,也不会回传梯度到旧策略),与当前策略参数 \(\theta\) 无关,对策略参数 \(\theta\) 的梯度为 0;
- \(A_{\theta_{\text{old}}}(a,s)\) 计算优势函数时用到了价值网络,这也与当前策略参数 \(\theta\) 无关,对策略参数 \(\theta\) 的梯度为 0(特别地,\(A_{\theta_{\text{old}}}(a,s)\) 是经过 stop_gradient 得到的,也不会影响价值网络的参数)
- 注意: 发生截断时, \((1+\epsilon)A_{\theta_{\text{old}}}(a,s)\) 或 \((1-\epsilon)A_{\theta_{\text{old}}}(a,s)\) 对策略参数 \(\theta\) 的梯度为 0 :
- \(clip + \min\) 操作讨论 ,关于 \(\min\left(\color{blue}{r(\theta)A_{\theta_{\text{old}}}(s,a)}, \color{red}{clip\left(r(\theta), 1-\epsilon, 1+\epsilon\right)A_{\theta_{\text{old}}}(a,s)}\right)\),由于 \(A_{\theta_{\text{old}}}(a,s) > 0\) 是有正有负的(\(A=Q-V\) 的加权平均),对于某个样本来说:
- 当 \(A_{\theta_{\text{old}}}(a,s) > 0\) 时,要提升目标动作概率 \(\pi_\theta(a|s)\) :
- 若 \(r(\theta) > 1+\epsilon\),则说明相对原始策略 \(\pi_{\theta_\text{old}(a|s)}\),\(\pi_\theta(a|s)\) 已经提升够多了 ,不希望再继续提升(偏离原始策略太多容易不稳定),\(clip + \min\) 操作可以将这个样本的目标值截断为 \((1+\epsilon)A_{\theta_{\text{old}}}(a,s)\),这与策略参数 \(\theta\) 无关 ,不会有梯度回传,即该样本相当于被废弃了(不考虑求均值会用到样本数量)
- 若 \(r(\theta) \leq 1+\epsilon\),则目标动作概率 \(\pi_\theta(a|s)\) 还小 ,可以正常更新以提升该动作的概率(问题:这里其实是无法控制目标概率更新的幅度的,更新后的真实值可能超过 \(1+\epsilon\) 这个阈值)
- 当 \(A_{\theta_{\text{old}}}(a,s) < 0\) 时,要降低目标动作概率 \(\pi_\theta(a|s)\) :
- 若 \(r(\theta) < 1-\epsilon\),则说明相对原始策略 \(\pi_{\theta_\text{old}(a|s)}\),\(\pi_\theta(a|s)\) 已经降低的够多了 ,不希望再继续降低(偏离原始策略太多容易不稳定),\(clip + \min\) 操作可以将这个样本的目标值截断为 \((1-\epsilon)A_{\theta_{\text{old}}}(a,s)\),这与策略参数 \(\theta\) 无关,不会有梯度回传,即该样本相当于被废弃了(不考虑求均值会用到样本数量)
- 若 \(r(\theta) \geq 1+\epsilon\),则目标动作概率 \(\pi_\theta(a|s)\) 较大 ,可以正常更新以降低该动作的概率(问题:这里其实是无法控制目标概率更新的幅度的,更新后的真实值可能小于 \(1-\epsilon\) 这个阈值)
- 思考:由于 \(r(\theta) > 1+\epsilon\) 的样本动作概率不许继续提升(但可以被降低),\(r(\theta) < 1-\epsilon\) 的样本动作概率不许继续降低(但可以提升),所以整体来说,所有动作的概率都倾向于维持 \(r(\theta) \in [1-\epsilon, 1+\epsilon]\) 之间(只是倾向于,不能完全保证)
- 严格One-Step更新下,损失函数可做如下简化:此时旧策略采样的样本仅更新一次模型即丢弃(即epoch=1,且一次更新完所有参数,batch_size足够大),且立刻会将新策略的更新同步到旧策略上,保证每次更新模型前新旧策略完全一致 ,则无需使用Clip操作和min操作(因为 \(r(\theta)=1\)),PPO目标函数将可以简化为如下形式(注意,虽然此时 \(r(\theta) = \frac{\pi_\theta(a|s)}{\pi_{\theta_{\text{old}}}(a|s)}=1\),但是必须保留 \(r(\theta)\),因为梯度传导需要分子 \(\pi_\theta(a|s)\)):
$$
\begin{aligned}
\max_\theta&\ \ \mathbb{E}_{s \sim \rho_{\pi_{\theta_\text{old}}}, a \sim \pi_{\theta_\text{old}}}\left(\color{blue}{\frac{\pi_\theta(a|s)}{\pi_{\theta_{\text{old}}}(a|s)}A_{\theta_{\text{old}}}(s,a)}\right)
\end{aligned}
$$- 建议epoch大于1的理由1 :One-Step更新样本效率低,如果设置的学习率过大又会引发不稳定,所以建议还是多次epoch更新,增加样本利用率
- 建议epoch大于1的理由2 :epoch大于1的多次更新还有补救回调的作用,当策略已经更新偏离旧策略太多时,PPO损失函数保证可以有机会被拉回来
- 补充讨论:实际上,严格One-Step更新下,PPO降级为普通PG ,更详细的讨论见附录
- 补充:Simplified PPO-Clip Objective中有PPO简化的推导,能更加清晰的显示PPO的 \(clip + \min\) 操作
- 当 \(A_{\theta_{\text{old}}}(a,s) > 0\) 时,要提升目标动作概率 \(\pi_\theta(a|s)\) :
- 一些博客(比如:Visualize the Clipped Surrogate Objective Function)有关于Clip仅限制了单边的讨论(有效性有待商榷):
- 当 \(A_{\theta_{\text{old}}}(a,s) > 0\) 时, \(r(\theta)\) 在Clip后的生效范围在 \(r(\theta) \in [0,1+\epsilon]\),也就是 \(1-\epsilon\) 边界会失效(问题 :此时失效是正常的吧,因为此时 \(A_{\theta_{\text{old}}}(a,s) > 0\),我们要提升目标动作概率,当前的策略目标动作概率越小,我们越应该提升,这反而是PPO的设计:为了使得整体策略不偏离旧策略太远?)
- 当 \(A_{\theta_{\text{old}}}(a,s) < 0\) 时, \(r(\theta)\) 在Clip后的生效范围在 \(r(\theta) \in [1-\epsilon,+\infty]\),也就是 \(1+\epsilon\) 边界会失效(同上问题 :此时失效是正常的吧,因为此时 \(A_{\theta_{\text{old}}}(a,s) < 0\),我们要降低目标动作概率,当前的策略目标动作概率越大,我们越应该降低,这反而是PPO的设计:为了使得整体策略不偏离旧策略太远?)
- 改进方案(有效性有待商榷) :在on-policy的设定下,我们认为策略新旧策略的比值 \(r(\theta)\) 不会太大,一般不会出现问题,但是off-policy设定下,可能会出现问题,所以需要再加一层Clip(参考自:Visualize the Clipped Surrogate Objective Function)
$$
\begin{aligned}
\max_\theta&\ \ \mathbb{E}_{s \sim \rho_{\pi_{\theta_\text{old}}}, a \sim \pi_{\theta_\text{old}}}\max\left( \color{red}{\eta A_{\theta_{\text{old}}}(a,s)}, \min\left(\color{blue}{r(\theta)A_{\theta_{\text{old}}}(s,a)}, \color{red}{clip\left(r(\theta), 1-\epsilon, 1+\epsilon\right)A_{\theta_{\text{old}}}(a,s)}\right)\right)
\end{aligned}
$$- 其中 \(\color{red}{\eta}\) 是超参数(理解:一般会比较大,比如 5 或 10 等?)
- 注:综合上面的问题,这里的结论不一定合理 ,这种做法的有效性还有待商榷,因为 Clip 不是在控制每一个epoch更新后的 \(r(\theta)\),而是根据更新前的 \(r(\theta)\) 判断是否要继续更新对应的状态动作对
- 如果从比值过大可能是对应异常值导致来看(比如模型推理错误导致异常值),这里确实可以做一下截断
- 否则使用保留梯度的截断更合适(至少保留部分梯度),详情见附录
- 特别说明:理论上来说,上述的 \(\max\left( \color{red}{\eta A_{\theta_{\text{old}}}(a,s)}, \cdot\right)\) 只会在 \(A_{\theta_{\text{old}}}(a,s) < 0\) 时生效,因为 \(A_{\theta_{\text{old}}}(a,s) > 0\) 时会被更小的上界 \(1+\epsilon\) 提前 Clip 掉(相当于 \(\color{red}{\eta}\) 没有生效)
PPO 网络更新
- Critic 网络更新(原始论文中未明确给出 Critic 网络更新的公式,实际上 Critic 网络的更新有直接使用真实折扣奖励作为目标值、TD-Error 作为损失函数和使用GAE 作为目标值等版本,这里给出TD-Error 作为损失函数的形式,更多详情见后面的章节单独讨论)
$$
Loss_{\text{critic}} = \sum (r_t + \gamma V^{\bar{w}}(s_{t+1}) - V^{w}(s_{t})) ^ 2
$$- 这里虽然使用 Target V 网络表达,但实际上PPO一般不需要使用 Target V 网络
- 这里V值拟合的目标是策略 \(\pi_\theta\) 对应的V值 \(V^{\pi_\theta}\)
- \(r_t = r(s_t, a_t)\vert_{a_t \sim \pi_\theta(\cdot|s_t)}\),训练用的整个轨迹链路都是从策略 \(\pi_\theta\) 采样得到的
- Actor网络更新
$$
Loss_{\text{actor}} = - \mathbb{E}_{s \sim \rho_{\pi_{\theta_\text{old}}}, a \sim \pi_{\theta_\text{old}}}\min\left(\color{blue}{r(\theta)A_{\theta_{\text{old}}}(s,a)}, \color{red}{clip\left(r(\theta), 1-\epsilon, 1+\epsilon\right)A_{\theta_{\text{old}}}(a,s)}\right)
$$- 其中: \(r(\theta) = \frac{\pi_\theta(a|s)}{\pi_{\theta_{\text{old}}}(a|s)} \)
PPO Critic 网络更新的其他方式
- 损失函数形式的选择 :在PPO算法中,Critic网络(即价值函数网络)的更新通常采用均方误差(MSE)损失或Huber Loss(也称为 Smooth_l1_loss),通过梯度下降来最小化价值函数的预测误差
- PPO中Critic的核心更新公式为 MSE损失等,但目标值 \( V_{\text{target} } \) 的计算方式可能因具体实现而异(单步TD、GAE等)。实际代码中通常结合以下步骤:
- 从经验缓冲区采样数据 \((s_t, r_t, s_{t+1})\)
- 计算目标值 \( V_{\text{target} } \)(如单步TD或GAE)
- 最小化 \( L_{\text{critic} } \) 更新Critic参数 \( \theta \)
- Critic 损失函数(MSE) :
$$
L_{\text{critic} } = \frac{1}{2} \mathbb{E}_{(s_t) \sim \text{batch} } \left[ \left( V_{\theta}(s_t) - V_{\text{target} }(s_t) \right)^2 \right]
$$- \( V_\theta(s_t) \) 是当前Critic网络的输出,\( \theta \) 为网络参数
- \( V_{\text{target} }(s_t) \) 是目标值,可以有多种实现,具体实现见下文
可选目标1:基于 TD 误差的更新(Temporal Difference Learning)
Critic的目标是拟合状态值函数 \( V^\pi(s) \),通过TD误差计算当前值函数的预测与目标值的差异
TD目标值(单步) :
$$
V_{\text{target} }(s_t) = r_t + \gamma V_{\text{old} }(s_{t+1})
$$- \( \gamma \) 是折扣因子
- \( V_{\text{old} } \) 是旧Critic网络的输出(稳定训练),需要注意这里不是Target网络,而是PPO每个episode可能会更新多个epoch ,需要保证更新过程中Critic网络的学习目标是不变的,即每个epoch中 ,目标值\( V_{\text{target} }(s_t) \) 始终是不变的 ,实现时,确保目标值是提前计算得到的即可,具体实现可以如下(参考自动手学强化学习实现):
1
2
3
4
5
6
7
8
9# ...
td_target = rewards + self.gamma * self.critic(next_states) * (1 - dones)
# ...
for _ in range(self.epochs):
critic_loss = torch.mean(F.mse_loss(self.critic(states), td_target.detach()))
self.critic_optimizer.zero_grad()
critic_loss.backward()
self.critic_optimizer.step()
# ...
此时的Critic 损失函数相当于:
$$
L_{\text{critic} } = \frac{1}{2} \mathbb{E}_{(s_t) \sim \text{batch} } \left[ \left( \color{red}{r_t + \gamma V_{\text{old} }(s_{t+1})} - V_{\theta}(s_t)\right)^2 \right]
$$
可选目标2:广义优势估计(GAE)结合多步TD
- 当使用GAE(Generalized Advantage Estimation)时,Critic的目标值会引入多步TD误差的加权平均,进一步减少方差
- GAE 优势函数 :
$$
A_t^{\text{GAE}(\gamma, \lambda)} = \sum_{k=0}^{T-t} (\gamma \lambda)^k \delta_{t+k}
$$- 其中 \( \delta_t = r_t + \gamma V_{\text{old} }(s_{t+1}) - V_{\text{old} }(s_t) \) 是单步TD误差,\( \lambda \) 是GAE超参数
- Critic 目标值(GAE) :
$$
V_{\text{target} }(s_t) = A_t^{\text{GAE} } + V_{\text{old} }(s_t)
$$ - 此时的Critic 损失函数相当于:
$$
L_{\text{critic} } = \frac{1}{2} \mathbb{E}_{(s_t) \sim \text{batch} } \left[ \left( \color{red}{A_t^{\text{GAE} } + V_{\text{old} }(s_t)} - V_{\theta}(s_t)\right)^2 \right]
$$
可选目标3:折扣真实奖励
- 对于状态 \( s_t \),其目标值 \( V_{\text{target}}(s_t) \) 是从 \( t \) 时刻开始到回合结束的累计折扣奖励:
$$
V_{\text{target}}(s_t) = \sum_{k=0}^{T-t} \gamma^k r_{t+k}
$$- \( T \) 是回合终止时间步,
- \( \gamma \) 是折扣因子(如0.99),
- \( r_{t+k} \) 是 \( t+k \) 时刻的即时奖励
- 此时的Critic损失函数相当于:
$$
L_{\text{critic} } = \frac{1}{2} \mathbb{E}_{(s_t) \sim \text{batch} } \left[ \left( \color{red}{\sum_{k=0}^{T-t} \gamma^k r_{t+k}} - V_{\theta}(s_t)\right)^2 \right]
$$
其他优化:目标网络(Target Network,一般PPO不需要)
- 为稳定训练,Critic的目标值 \( V_{\text{target} } \) 可能通过慢更新的目标网络计算(类似DQN):
$$
V_{\text{target} }(s_t) = r_t + \gamma V_{\bar{\theta}}(s_{t+1})
$$- 其中 \( \bar{\theta} \) 是目标网络参数,通过Polyak平均更新(一种加权平均方法,即RL中常说的软更新):
$$
\theta^- \leftarrow \tau \theta + (1 - \tau) \bar{\theta} \quad (\tau \ll 1)
$$
- 其中 \( \bar{\theta} \) 是目标网络参数,通过Polyak平均更新(一种加权平均方法,即RL中常说的软更新):
PPO 的一些实践说明
- 常用的形式是 PPO-Clip 形式,实践中效果更好
- 一般来说 PPO 需要使用 Target V 网络,使用 Target V 网络会导致收敛较慢
- Critic 网络的损失函数可以归回方法中常用的
smooth_l1_loss(即huber_loss),以减少异常值带来的影响 - PPO-Clip 中一般设置 \(\epsilon=0.2\)
- PPO原始论文中,每次采样到的数据会作 K 次 epochs,且不同游戏使用的次数不同,在 Mujoco 中使用 \(epochs=10\),Roboschool 中使用 \(epochs=15\),Atari 中使用 \(epochs=3\)
PPO 连续动作实现离散动作的实现主要区别
模型建模
策略网络 :连续动作需要使用 \(\mu_\theta,\sigma_\theta\) 表示均值和方差,连续分布下,每个动作的概率理论上都是0,但借助概率密度函数的含义,可以通过计算
采样方式 :采样时需要创建分布来采样,由于不需要梯度回传,所以不需要使用重参数法
Critic网络 :由于离散连续场景都用V网络,仅仅评估状态下的价值即可,与动作无关,连续动作处理不需要特殊修改
新旧策略比值计算方式不同
离线动作按照推导中的实现即可
1
2
3
4def compute_surrogate_obj(self, states, actions, advantage, old_log_probs, actor): # 计算策略目标
log_probs = torch.log(actor(states).gather(1, actions))
ratio = torch.exp(log_probs - old_log_probs)
return torch.mean(ratio * advantage)连续动作需要使用概率密度函数来实现
1
2
3
4
5
6def compute_surrogate_obj(self, states, actions, advantage, old_log_probs, actor):
mu, std = actor(states)
action_dists = torch.distributions.Normal(mu, std)
log_probs = action_dists.log_prob(actions) # 返回\log(f(actions)),f为概率密度函数
ratio = torch.exp(log_probs - old_log_probs) # 这里可以直接用于算概率之间的比值理论是概率密度函数的含义
return torch.mean(ratio * advantage) # 注意torch内部实现这里的梯度可以回传到actor网络上(基于参数mu,std可以运算得到log_prob,所以梯度可以回传)
PPO 的训练技巧
- 参考:影响PPO算法性能的10个关键技巧(附PPO算法简洁Pytorch实现)
- 亲自测试实践总结(主要以 ‘CartPole-v0’ 环境测试):
- Advantage Normalization 能让 Critic Loss 和 Policy Loss 都更加平滑(比较稳定),但是对整体回报不一定有收益,某些情况下还出现了波动(待确定原因)
- State Normalization 会严重拖慢训练速度,实际测试时发现一个有趣的现象:使用 State Normalization 技巧后,会导致策略先逐步收敛到最优策略再突然下降,降幅很大且不再恢复,状态和策略都陷入了崩溃状态(结论是 State Normalization 技巧要慎用)
- Orthogonal Initialization 一般都会有正向的效果,虽然某些场景下不一定提升很大
- Reward Scaling 会导致策略不稳定(甚至无法收敛)
- 增加 Policy Entropy 会导致收敛不稳定,且超参数很敏感
- 附上述实验的代码:
>>>点击展开折叠内容...
1
Advantage Normalization
最早出自The Mirage of Action-Dependent Baselines in Reinforcement Learning,对 Advantage Function 进行归一化,用于提升 PG 方法的性能
具体方法:减去均值除以方差
理解:
- 归一化 将 Advantage 的均值强制设为 0:
- 这意味着在当前的 Batch 中,大约有一半的动作会被认为是“好动作”(\(A>0\),增加概率),另一半是“坏动作”(\(A<0\),减少概率)
- 这能有效防止 Policy 总是往一个方向跑(例如 Reward 全是正数时),显著加快收敛
- 归一化 将方差设为 1
- 这使得 Loss 的量级不会因为 Reward 的绝对数值大小而剧烈波动,使得超参数(如 Learning Rate)更容易调节
- 归一化 将 Advantage 的均值强制设为 0:
实现方案:
- 方案一:Batch Advantage Normalization(BAN),对当前 Batch 的所有 Advantage 求均值和方差
- 方案二:Mini-Batch Advantage Normalization(MBAN),仅对当前 Mini-Batch Advantage Normalization
实践中,BAN 效果最好,MBAN 效果次之,不使用任何 AN 效果最差;
- 理解,方案一和方案二并未限定具体采样的轨迹是多少个,但是主要思路是尽量在更多的样本上统计均值和方差,减少波动,这样效果更好些
关于 MBAN,ppo-implementation-details博客中有详细实现
问题:GAE 中还需要做 Advantage Normalization 吗?是否是在计算 GAE 之前做归一化?
- 回答,可以做,是在 GAE 之后做
在一个带 GAE 和 Advantage Normalization 的 PPO 的一个训练迭代中,流程通常如下:
- 第一步:采集数据(Rollout) :Agent 与环境交互,采集一定步数的数据。假设采集了 \(N\) 个步骤(例如 2048 步)
- 计算 GAE :利用这 \(N\) 个数据,计算出每一个时间步的 Advantage值,得到一个向量 \(\mathbf{A} = [A_1, A_2, …, A_N]\)
- 计算统计量(非滑动) :直接计算这 \(N\) 个数据的均值和标准差:
$$ \mu_{batch} = \frac{1}{N} \sum_{i=1}^{N} A_i $$
$$ \sigma_{batch} = \sqrt{\frac{1}{N} \sum_{i=1}^{N} (A_i - \mu_{batch})^2} $$ - 执行归一化 :
$$ A_{norm}^{(i)} = \frac{A_i - \mu_{batch}}{\sigma_{batch} + \epsilon} $$ - Mini-batch 训练 :将归一化后的数据打乱(Shuffle),切分成多个小批次(Mini-batch)进行 SGD 更新
为什么不用滑动平均?
- Advantage 的定义是相对的 :Advantage \(A(s,a) = Q(s,a) - V(s)\) 衡量的是动作 \(a\) 比“平均表现”好多少
- 分布漂移(Non-stationarity) :随着 Policy 的更新,Agent 的能力在变,Value Function 也在变
- 上一次迭代算出的 Advantage 分布与当前迭代的分布可能完全不同
- 如果使用历史数据的滑动平均,会引入过时的统计信息,导致对当前策略评估的偏差
- Advantage Normalization 的核心目的是为了降低方差(Variance Reduction) 并确保 Policy Gradient 的更新幅度在不同 Batch 间保持稳定,而不是为了将数据缩放到某个固定的物理尺度
代码示例
- 类似 开源框架 Stable Baseline3 的伪代码逻辑如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15# 采集数据并计算 GAE (在 rollout_buffer 中完成)
rollout_buffer.compute_returns_and_advantage(...)
# 在准备训练数据时进行归一化
# 注意:这里是对整个 buffer 的 advantage 进行操作
advantages = rollout_buffer.advantages
if self.normalize_advantage:
# 直接计算当前 buffer 的均值和标准差
advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)
# 之后再进行 mini-batch 循环更新
for epoch in range(n_epochs):
for rollout_data in rollout_buffer.get(batch_size):
# 使用已经归一化好的 advantages 计算 loss
pass
State Normalization
对状态做归一化
State Normalization 的核心在于,与环境交互的过程中,维护一个动态的关于所有经历过的所有 State 的 Mean 和 Std, 然后对当前的获得的 State 做normalization
经过 Normalization 后的 State 符合
Mean=0,Std=1的正态分布,用这样的状态作为神经网络的输入,更有利于神经网络的训练采用滑动增量更新的方式(详细证明见附录):
- 均值:\(\mu_{\text{new}} = \mu_{\text{old}} + \frac{1}{n}(x-\mu_{\text{old}})\)
- 方差中间变量:\(S_{\text{new}} = S_{\text{old}} + (x-\mu_{\text{old}})\cdot(x-\mu_{\text{new}})\)
- 注意这个值除以 \(n\) 才是方差
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31class RunningMeanStd:
# Dynamically calculate mean and std
def __init__(self, shape): # shape:the dimension of input data
self.n = 0
self.mean = np.zeros(shape)
self.S = np.zeros(shape)
self.std = np.sqrt(self.S)
def update(self, x):
x = np.array(x)
self.n += 1
if self.n == 1:
self.mean = x
self.std = x
else:
old_mean = self.mean.copy()
self.mean = old_mean + (x - old_mean) / self.n
self.S = self.S + (x - old_mean) * (x - self.mean)
self.std = np.sqrt(self.S / self.n )
class Normalization:
def __init__(self, shape):
self.running_ms = RunningMeanStd(shape=shape)
def __call__(self, x, update=True):
# Whether to update the mean and std,during the evaluating,update=Flase
if update:
self.running_ms.update(x)
x = (x - self.running_ms.mean) / (self.running_ms.std + 1e-8)
return x在模型实现时,状态归一化这个函数是添加到策略网络和状态网络层的输入端的,实现 Demo 如下:
1
2
3
4
5
6
7
8
9def forward(self, x):
if config.use_state_norm:
norm_x = []
for i in range(x.size(0)): # 逐个动作归一化
norm_x.append(torch.tensor(self.state_norm(x[i]), dtype=torch.float32))
x = torch.stack(norm_x, dim=0)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
return self.fc3(x)
Reward Normalization
- 与 State Normalization 的方案,动态维护所有获得过的 Reward 的 Mean 和 Std,然后再对当前的 Reward 做 Normalization
- 问题:是对单次 Reward 做了 Normalization 吗?如果 Reward 已经做了 Normalization,GAE 中实际上就已经使用了标准化后的 Reward 了,Advantage 是否还需要做呢?
- 常常被替换为 Reward Scaling
- 虽然 Advantage 不用滑动平均,但 Reward Normalization 通常是使用滑动平均的
- Reward Normalization :
- 目的是让环境反馈的 Reward 尺度统一(比如不管是股票涨跌的金额,还是游戏得分,都缩放到 1 左右)
- 因为环境的物理属性是不变的,所以用滑动平均(Running Mean/Std)来估计环境 Reward 的全局统计特性是合理的
- Advantage Normalization :是对“优势”的归一化,是策略更新内部的一个数值稳定技巧,只关注当前批次
- Reward Normalization :
Reward Scaling
- 相关论文:PPO-Implementation matters in deep policy gradients A case study on PPO and TRPO
- Reward Scaling 与 Reward Normalization 的区别在于,Reward Scaling 是动态计算一个 standard deviation of a rolling discounted sum of the rewards,然后只对当前的 reward 除以这个 std(不减去均值?)
- Reward Normalization 和 Reward Scaling 二选一即可,建议使用 Reward Scaling 而不是 Reward Normalization 即可
- 代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class RewardScaling:
def __init__(self, shape, gamma):
self.shape = shape # reward shape=1
self.gamma = gamma # discount factor
self.running_ms = RunningMeanStd(shape=self.shape)
self.R = np.zeros(self.shape)
def __call__(self, x):
self.R = self.gamma * self.R + x
self.running_ms.update(self.R)
x = x / (self.running_ms.std + 1e-8) # Only divided std
return x
def reset(self): # When an episode is done,we should reset 'self.R'
self.R = np.zeros(self.shape)
Policy Entropy(Entropy Bonus)
- 在 Actor 的 Loss 中增加一项策略熵,在最大化收益的同时,最大化策略熵,增加探索性(理解:同时有正则的作用)
- 但是增加以后会新增加一个新的超参数,且模型对该参数很敏感
Learning Rate Decay
- 学习率逐步衰减
- 代码实现
1
2
3
4
5
6
7def lr_decay(self, total_steps):
lr_a_now = self.lr_a * (1 - total_steps / self.max_train_steps)
lr_c_now = self.lr_c * (1 - total_steps / self.max_train_steps)
for p in self.optimizer_actor.param_groups:
p['lr'] = lr_a_now
for p in self.optimizer_critic.param_groups:
p['lr'] = lr_c_now
Gradient Clip
- 梯度裁剪
- 代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13# Update actor
self.optimizer_actor.zero_grad()
actor_loss.mean().backward()
if self.use_grad_clip: # Trick 7: Gradient clip
torch.nn.utils.clip_grad_norm_(self.actor.parameters(), 0.5)
self.optimizer_actor.step()
# Update critic
self.optimizer_critic.zero_grad()
critic_loss.backward()
if self.use_grad_clip: # Trick 7: Gradient clip
torch.nn.utils.clip_grad_norm_(self.critic.parameters(), 0.5)
self.optimizer_critic.step()
Orthogonal Initialization
- 正交初始化(Orthogonal Initialization)是为了防止在训练开始时出现梯度消失、梯度爆炸等问题所提出的一种神经网络初始化方式。具体的方法分为两步:
- 用均值为 0,标准差为1的高斯分布初始化权重矩阵
- 对这个权重矩阵进行奇异值分解,得到两个正交矩阵,取其中之一作为该层神经网络的权重矩阵
- 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49# orthogonal init
def orthogonal_init(layer, gain=1.0):
nn.init.orthogonal_(layer.weight, gain=gain)
nn.init.constant_(layer.bias, 0)
class Actor_Gaussian(nn.Module):
def __init__(self, args):
super(Actor_Gaussian, self).__init__()
self.max_action = args.max_action
self.fc1 = nn.Linear(args.state_dim, args.hidden_width)
self.fc2 = nn.Linear(args.hidden_width, args.hidden_width)
self.mean_layer = nn.Linear(args.hidden_width, args.action_dim)
self.log_std = nn.Parameter(torch.zeros(1, args.action_dim)) # We use 'nn.Paremeter' to train log_std automatically
if args.use_orthogonal_init:
print("------use_orthogonal_init------")
orthogonal_init(self.fc1)
orthogonal_init(self.fc2)
orthogonal_init(self.mean_layer, gain=0.01)
def forward(self, s):
s = torch.tanh(self.fc1(s))
s = torch.tanh(self.fc2(s))
mean = self.max_action * torch.tanh(self.mean_layer(s)) # [-1,1]->[-max_action,max_action]
return mean
def get_dist(self, s):
mean = self.forward(s)
log_std = self.log_std.expand_as(mean) # To make 'log_std' have the same dimension as 'mean'
std = torch.exp(log_std) # The reason we train the 'log_std' is to ensure std=exp(log_std)>0
dist = Normal(mean, std) # Get the Gaussian distribution
return dist
class Critic(nn.Module):
def __init__(self, args):
super(Critic, self).__init__()
self.fc1 = nn.Linear(args.state_dim, args.hidden_width)
self.fc2 = nn.Linear(args.hidden_width, args.hidden_width)
self.fc3 = nn.Linear(args.hidden_width, 1)
if args.use_orthogonal_init:
print("------use_orthogonal_init------")
orthogonal_init(self.fc1)
orthogonal_init(self.fc2)
orthogonal_init(self.fc3)
def forward(self, s):
s = torch.tanh(self.fc1(s))
s = torch.tanh(self.fc2(s))
v_s = self.fc3(s)
return v_s
Adam Optimizer Epsilon Parameter
- 实践中,从官方默认值 1e-8 改成 1e-5
- 原因?
Tanh Activation Function
- 将 ReLU 换成 tanh 激活函数
- 建议 PPO 算法默认使用激活函数
- 原因?
Value Clipping
核心目标:为了训练的稳定性,对价值进行裁剪
实现方式1:Stable Baselines3 的 PPO 实现中有如下代码 /ppo/ppo.py#L234-L244
1
2
3
4
5
6
7
8
9
10
11if self.clip_range_vf is None:
# No clipping
values_pred = values
else:
# Clip the difference between old and new value
# NOTE: this depends on the reward scaling
values_pred = rollout_data.old_values + th.clamp(
values - rollout_data.old_values, -clip_range_vf, clip_range_vf
)
# Value loss using the TD(gae_lambda) target
value_loss = F.mse_loss(rollout_data.returns, values_pred)- 从代码可以看出,Stable Baselines3 认为只要是当前价值网络的预估值超过旧网络预估值的情况,都是异常情况,则当前样本不置信,将当前样本的损失设置为固定值(通过Clip实现,此时该样本对价值网络参数的梯度为0,相当于丢弃样本了)
实现方式2:一些地方也看到过类似表达(最早是在RLHF中看到的)
1
2
3
4
5
6def critic_loss_fn(self, values, old_values, returns, mask):
values_clipped = torch.clamp(values, old_values - clip_range_vf, old_values + clip_range_vf)
vf_loss1 = (values - returns) ** 2
vf_loss2 = (values_clipped - returns) ** 2
vf_loss = torch.mean(torch.max(vf_loss1, vf_loss2) * mask) / mask.sum()
return vf_loss- 忽略上面的
mask部分,是语言模型训来特有的 - 这种实现的基本思路是:如果 Clip 后的价值与真实值的误差(MSE)大于当前策略,则使用 Clip 后的,此时该样本的 Loss 对价值网络参数的梯度为0,相当于丢弃样本了
- 理解
max操作:当策略误差已经比 Clip 后的值对应的误差还要小了,就不要使用该误差了(也就是说当前样本的误差相对上一轮已经小了一定大小了,不需要继续更新了,这种做法称为保守更新),只有 MSE 误差相对原始价值网络对应的误差变小时,才丢弃样本
- 忽略上面的
附录-均值方差滑动更新公式证明
均值更新推导
- 均值更新推导详情:
$$
\begin{align}
\mu_n &= \frac{1}{n}\sum_{i=1}^n x_i \\
&= \frac{1}{n}(\sum_{i=1}^{n-1} x_i + x_n) \\
&= \frac{1}{n}(\sum_{i=1}^{n-1} x_i + x_n) \\
&= \frac{1}{n}((n-1)\cdot\frac{1}{n-1}\sum_{i=1}^{n-1} x_i + x_n) \\
&= \frac{1}{n}((n-1)\mu_{n-1} + x_n) \\
&= \frac{1}{n}(n\mu_{n-1} + x_n - \mu_{n-1}) \\
&= \mu_{n-1} + \frac{1}{n}(x_n - \mu_{n-1})
\end{align}
$$
方差更新推导
- 将方差中间变量 \(S_n\) 展开(这里 \(S_n\) 除以n才是方差)有:
$$
\begin{align}
S_n &= \sum_{i=1}^{n} (x_i - \mu_n)^2 \\
&= \sum_{i=1}^{n-1} (x_i - \mu_n)^2 + (x_n - \mu_n)^2 \\
&= \sum_{i=1}^{n-1} (x_i - \mu_{n-1} + \mu_{n-1} - \mu_n)^2 + (x_n - \mu_n)^2 \\
&= \sum_{i=1}^{n-1} (x_i - \mu_{n-1})^2 + 2(\mu_{n-1} - \mu_{n})\sum_{i=1}^{n-1}(x_i - \mu_{n-1}) + (n-1)(\mu_{n-1} -\mu_n)^2 + (x_n - \mu_n)^2 \\
&= S_{n-1} + 2(\mu_{n-1} - \mu_{n})(\sum_{i=1}^{n-1} x_i - (n-1)\mu_{n-1}) + (n-1)(\mu_{n-1} -\mu_n)^2 + (x_n - \mu_n)^2 \\
&= S_{n-1} + 2(\mu_{n-1} - \mu_{n})((n-1)\mu_{n-1} - (n-1)\mu_{n-1}) + (n-1)(\mu_{n-1} -\mu_n)^2 + (x_n - \mu_n)^2 \\
&= S_{n-1} + 2(\mu_{n-1} - \mu_{n})\cdot 0 + (n-1)(\mu_{n-1} -\mu_n)^2 + (x_n - \mu_n)^2 \\
&= S_{n-1} + (n-1)(\mu_{n-1} -\mu_n)^2 + (x_n - \mu_n)^2 \\
&= S_{n-1} + (n-1)(\mu_{n-1} -\mu_n)(\mu_{n-1} -\mu_n) + (x_n - \mu_n)^2 \\
&= S_{n-1} + ((n-1)\mu_{n-1} -(n-1)\mu_n)(\mu_{n-1} -\mu_n) + (x_n - \mu_n)^2 \\
&= S_{n-1} + (n\mu_{n} - x_n -(n-1)\mu_n)(\mu_{n-1} -\mu_n) + (x_n - \mu_n)^2 \\
&= S_{n-1} + (\mu_{n} - x_n)(\mu_{n-1} -\mu_n) + (x_n - \mu_n)^2 \\
&= S_{n-1} + (x_n - \mu_{n})(\mu_n - \mu_{n-1}) + (x_n - \mu_n)^2 \\
&= S_{n-1} + (x_n - \mu_{n})(\mu_n - \mu_{n-1} + x_n - \mu_n)\\
&= S_{n-1} + (x_n - \mu_{n})(x_n - \mu_{n-1})\\
\end{align}
$$ - 最终有: \(S_n = S_{n+1} + (x_n - \mu_{n})(x_n - \mu_{n-1})\)
DPPO(Distributed PPO)
- DPPO是PPO的分布式版本,引入了分布式计算的概念,允许多个计算节点(或智能体)并行地与环境交互,收集数据,并将这些数据用于更新全局模型
- 分布式架构不仅加快了数据收集的速度,还提高了算法处理大规模并行任务的能力,使得学习过程更加高效
附录:一次采样仅更新一次的 PPO 讨论
- 问题 :一次采样仅更新一次的 PPO 下,此时是 on-policy 的场景,为什么损失函数看起来和普通的 PG 不相等?(注:可以回顾一下,普通 PG 的更新公式是严格按照 on-policy 更新推导的,二者理应相等)
- 回答 :其实此时两者的更新公式是一致的
- 补充说明 :一次采样仅更新一次的 PPO 场景下,此时旧策略采样的样本仅更新一次模型即丢弃(即
epoch=1,且一次更新完所有参数,batch_size 足够大),且立刻会将新策略的更新同步到旧策略上,保证每次更新模型前新旧策略完全一致
普通策略梯度更新公式
- 普通策略梯度的更新为:
$$\theta \leftarrow \theta + \alpha \nabla_\theta \log \pi_\theta(a_t|s_t) G_t^n$$ - 以上梯度更新对应的损失函数为:
$$ Loss(\theta) = - \log \pi_\theta(a_t|s_t) G_t^n $$ - 进一步求导有:
$$\nabla_\theta Loss(\theta) = - \frac{\nabla_\theta \pi_\theta(a_t|s_t)}{\pi_\theta(a_t|s_t)} G_t^n$$ - 注意,以上更新使用的 \(s_t,a_t,G_t^n\) 等均来源于当前策略,由于此时有 \(\pi_\theta = \pi_{\theta_\text{old}}\),所以上面的更新公式也可以写成
$$\nabla_\theta Loss(\theta) = - \color{blue}{\frac{\nabla_\theta \pi_\theta(a_t|s_t)}{\pi_{\theta_\text{old}}(a_t|s_t)}} \color{red}{G_t^n}$$
PPO 简化后的更新公式
- 一次采样仅更新一次的PPO,其简化后的损失函数为:
$$ Loss(\theta) = - \mathbb{E}_{s \sim \rho_{\pi_{\theta_\text{old}}}, a \sim \pi_{\theta_\text{old}}}\left(\frac{\pi_\theta(a|s)}{\pi_{\theta_{\text{old}}}(a|s)}A_{\theta_{\text{old}}}(s,a)\right)$$ - 将期望形式转换为采样后有:
$$ Loss(\theta) = - \frac{\pi_\theta(a|s)}{\pi_{\theta_{\text{old}}}(a|s)}A_{\theta_{\text{old}}}(s,a)$$ - 进一步求导有:
$$ \nabla_\theta Loss(\theta) = - \color{blue}{\frac{\nabla_\theta \pi_\theta(a|s)}{\pi_{\theta_{\text{old}}}(a|s)}}\color{red}{A_{\theta_{\text{old}}}(s,a)}$$- 注:此时有 \(\pi_\theta = \pi_{\theta_\text{old}}\)
总体对比
- 总结来看,针对一次采样一次更新的 PPO,简化后的更新公式与普通 PG 完全相同,更新使用的样本 \((s,a)\) 也都是从当前策略采样的(即 on-policy 场景),两者的唯一区别是梯度权重选择:
- 简化 PPO:基于当前策略采样得到的 GAE 估计 \(A_{\theta_{\text{old}}}(s,a)\)
- 普通 PG:基于当前策略采样的蒙特卡罗收益 \(G_t^n\),除了 REINFORCE 外,其他的方法实际也常用Q函数或者优势函数 A 等估计值来替代,故简化后的 PPO 实际上可以看做是梯度权重是 GAE 的普通 PG 算法
附录:Dual-Clip PPO
Dual-Clip PPO 核心方法总结
- 原始论文链接:(Dual-Clip PPO)Mastering Complex Control in MOBA Games with Deep Reinforcement Learning, 2020 AAAI, Tencent
- Dual-Clip PPO 是腾讯 AI Lab 为解决 MOBA 1v1 游戏(如《王者荣耀》)中深度强化学习训练难题而提出的 PPO(Proximal Policy Optimization)改进算法 ,核心目标是在大规模离线训练场景下,解决传统 PPO 因策略偏差过大导致的收敛不稳定问题,适配 MOBA 游戏庞大的状态空间(约 \(10^{600}\))与动作空间(约 \(10^{18000}\))
- Dual-Clip PPO 跟前面章节中提到的 Visualize the Clipped Surrogate Objective Function 本质是一个事情,这里借助腾讯的文章,进行一些更详细的说明和探讨
- 注:该博客引用了另一篇文章 Towards Delivering a Coherent Self-Contained Explanation of Proximal Policy Optimization, 20210815 的最早时间晚于 腾讯的论文之后
设计背景:传统 PPO 的局限性
- 在 MOBA 1v1 游戏的大规模分布式训练中,传统 PPO 面临两大核心问题:
- 策略偏差过大 :训练数据来自多源历史策略(如不同训练阶段的 AI 自对弈轨迹),这些轨迹与当前训练的目标策略 \(\pi_{\theta}\) 差异显著,导致概率比 \(r_t(\theta) = \frac{\pi_{\theta}(a_t|s_t)}{\pi_{\theta_{old} }(a_t|s_t)}\) 可能异常巨大
- 方差失控 :当优势函数估计值 \(\hat{A}_t < 0\)(即动作 \(a_t\) 为“劣势动作”)时,巨大的 \(r_t(\theta)\) 会使 \(r_t(\theta) \cdot \hat{A}_t\) 产生无界负向偏差,导致策略更新方向混乱,训练难以收敛
核心改进逻辑:双重裁剪机制
- 传统 PPO :仅通过单一裁剪(Clip)限制概率比 \(r_t(\theta)\) 的范围(\(1-\epsilon \leq r_t(\theta) \leq 1+\epsilon\)),以避免策略更新幅度过大;
- Dual-Clip PPO :在传统 PPO 的基础上,针对 \(\hat{A}_t < 0\) 的场景增加第二重裁剪 ,通过下界限制 \(r_t(\theta) \cdot \hat{A}_t\) 的负向偏差,具体逻辑如下:
- 第一重裁剪(继承传统 PPO) :对概率比 \(r_t(\theta)\) 进行范围约束,确保策略更新“贴近”历史策略,避免突变:
\(clip(r_t(\theta), 1-\epsilon, 1+\epsilon)\),其中 \(\epsilon\) 为超参数(实验中设为 0.2) - 第二重裁剪(新增) :当 \(\hat{A}_t < 0\) 时,对 \(r_t(\theta) \cdot \hat{A}_t\) 增加下界 \(c \cdot \hat{A}_t\)(\(c > 1\) 为超参数,实验中设为 3),限制其负向偏差的最大值
- 第一重裁剪(继承传统 PPO) :对概率比 \(r_t(\theta)\) 进行范围约束,确保策略更新“贴近”历史策略,避免突变:
- 这一改进的核心逻辑是:即使 \(r_t(\theta)\) 异常大,“劣势动作”的损失也不会无限制减小,避免策略被极端样本误导
- 但这也限制了超过 reference 组过高的动作概率朝下更新,使用软性的梯度缩放可能会更好(即降低这个比例,同时保持梯度继续更新)
数学表达:目标函数定义
- Dual-Clip PPO 的核心是优化以下目标函数,通过“先 min 后 max”的双重裁剪实现稳定更新:
- 当 \(\hat{A}_t < 0\) 时,目标函数为:
$$
\hat{\mathbb{E} }_t\left[ \max\left( \min\left( r_t(\theta) \cdot \hat{A}_t,\ clip(r_t(\theta), 1-\epsilon, 1+\epsilon) \cdot \hat{A}_t \right),\ c \cdot \hat{A}_t \right) \right]
$$- \(\hat{\mathbb{E} }_t\):对批量样本的经验期望;
- \(r_t(\theta)\):当前策略与历史策略的概率比;
- \(\hat{A}_t\):优势函数估计值(衡量动作 \(a_t\) 相对平均水平的优劣);
- \(\epsilon = 0.2\)、\(c = 3\):实验验证的最优超参数,平衡探索与收敛
关键优势:适配 MOBA 训练需求
- 注:Dual-Clip PPO 针对 MOBA 1v1 游戏的训练特性,解决了传统算法的核心痛点,论文中提到的以下具体优势主要针对这个场景
- 保证收敛稳定性 :通过第二重裁剪限制负向偏差,即使在多源离线数据(如百万级 CPU 生成的自对弈轨迹)场景下,也能避免策略更新“失控”,实验中 AI 训练 80 小时后 Elo 评分趋于稳定(达到职业选手水平)
- 适配大规模分布式训练 :支持“离线数据生成-在线模型训练”解耦的系统架构(如 60 万 CPU 生成样本、1064 块 GPU 训练),无需依赖在线实时采样,大幅提升训练效率(单 GPU 每秒处理 8 万样本)
- 兼容多标签动作 decoupling :MOBA 游戏的动作需拆解为“按钮类型(如技能/移动)+ 目标单位(如敌方英雄/小兵)”等独立标签,Dual-Clip PPO 可与“控制依赖解耦”策略结合,对每个动作标签独立优化,同时保证整体策略收敛
实际效果展示
- 在《王者荣耀》1v1 模式的实验中,Dual-Clip PPO 是 AI 击败职业选手的关键组件之一:
- 与传统 PPO 相比,Dual-Clip PPO 使训练收敛时间缩短约 20% ,且 AI 在与职业选手的 BO5 对战中胜率达 100%(如法师英雄貂蝉 3:0 击败联赛顶尖法师选手);
- 结合“动作掩码(Action Mask)”“目标注意力(Target Attention)”等策略后,AI 在 2100 场公开对战中胜率达 99.81%,验证了其在复杂控制场景下的有效性
思考
- 适用于 off-policy 场景,且需要较为严重的 off-policy 才需要
- 改成保留梯度的截断方式理论会更合适,既可以避免异常点,又可以保证已经过高的动作概率得以被惩罚
- 注:截止到 2025 年,快手发布的 OneRec 也提出了一种方法 Early Clipped GRPO, 其中用到的方法就是本文提到的保留梯度的截断方式