手撕PPO(一)论文框架
这份笔记是为了复现 PPO 整理的,按照采样、优势估计、损失计算、参数更新的顺序走一遍。公式记号尽量和 Schulman 的两篇原文(PPO 2017、GAE 2015)保持一致。这篇文章主要记 PPO 论文和实现里容易对不上的地方,代码复现部分下一篇再讲。
原论文链接:
[1707.06347] Proximal Policy Optimization Algorithms
前置知识
原本的策略梯度
DQN 是学一个“动作价值函数” $Q(s,a)$,然后选 $Q$ 最大的动作。Policy Gradient 是直接学一个策略 $\pi_\theta(a \mid s)$,让网络直接输出动作概率,或者输出连续动作分布的参数。
DQN 里网络做的是:输入 state,输出每个动作的 $Q$。训练目标是让 $Q$ 满足 Bellman 方程:
所以 DQN 本质上是:
- 先学“每个动作有多好”。
- 再根据 $Q$ 值选动作。
Policy Gradient 的思路
Policy Gradient 直接让神经网络表示策略:
它表示在状态 $s$ 下选择动作 $a$ 的概率。智能体从这个概率分布里采样动作:
所以 Policy Gradient 的核心是直接调整策略参数 $\theta$,让好动作出现的概率变大。
最基础的写法里,会用 $\log \pi_\theta(a_t \mid s_t) \times G_t$ 构造 policy objective。代码里对应的直觉就是:
1 | log_prob * return |
如果某个动作后面的回报高,就提高这个动作的 log_prob;如果回报低,就压低这个动作的 log_prob。
策略梯度表达式
论文里普通 policy gradient 的估计写成:
也就是说,策略梯度由两部分组成:
- $\nabla\theta \log \pi\theta(a_t \mid s_t)$:负责告诉网络当前动作概率该怎么变。它是当前动作的 log probability 对参数 $\theta$ 的梯度。
- $\hat{A}_t$:负责告诉网络这个动作到底好不好,以及好多少或者差多少。
这个公式给的是一个梯度向量 $\hat{g}$,也就是优化器真正需要的更新方向。
拆开看:
表示当前动作概率对参数 $\theta$ 的敏感方向,也就是当前概率对 $\theta$ 的梯度。
表示这个动作比预期好还是差,以及好多少或者差多少。
loss 表达式
策略梯度里真正想最大化的是 policy objective:
如果先不考虑 PPO 的 ratio 和 clip,最朴素的 policy loss 可以写成:
1 | policy_loss = -(log_prob * advantage).mean() |
负号是因为优化器默认做最小化,而策略目标本身是要最大化。这个符号后面 PPO 里也一样,policy objective 要最大化,代码里的 policy_loss 就要取负。
Policy Gradient / PPO 的 rollout buffer
Policy Gradient / PPO 的 rollout buffer,和 SARSA 里的 $(s,a,r,s’,a’)$ 很像,都是在记录“智能体真实和环境交互过的一段经历”。区别在于:SARSA 存一条 transition 就可以做 TD 更新;Policy Gradient 往往要存一整段轨迹,因为它要回头算这个动作后面到底带来了多少累计收益。
SARSA 存的是:
1 | s_t, a_t, r_t, s_{t+1}, a_{t+1} |
它用这些量学的是:
1 | 动作价值 Q(s,a) |
Policy Gradient / PPO 存的量会更多,最少也要有:
1 | s_t, a_t, r_t, done_t, log_prob_t, value_t |
其中 log_prob_t 是动作的对数概率。连续动作里,它通常是某个动作在高斯分布下的对数概率密度。这个量后面算 ratio 时会用到。
Policy Gradient 直接学的是:
1 | 策略 π(a|s) |
普通 PG 局限性
普通 Policy Gradient 最大的问题有三个。
第一,样本效率低。一批轨迹通常只更新一次,用完就扔,因为数据是由旧策略采来的,而策略一更新,数据就“过期”了。
第二,方差大。同一个动作可能因为环境随机性、初始状态不同,导致 return 差很多,所以训练不稳定。
第三,更新步子容易太大。如果对同一批数据多训练几轮,策略可能变化很大,导致原来的 log_prob 和现在的策略不匹配,性能可能直接崩。
PPO 论文第 2.1 节明确说,虽然对同一批轨迹上的普通 PG loss 做多步优化很诱人,但这样没有充分理论保证,经验上经常导致破坏性的大策略更新。
PPO 核心
这里看 PPO 论文里的两个图,也是 PPO 最主要的思想。

第一张图把单个样本的 clipped 目标画成关于 ratio $r$ 的曲线:
左右两幅分别对应 $A>0$ 和 $A<0$。红点都在 $r=1$ 处,也就是更新刚开始、新旧策略还一样时的位置。
$A>0$ 时(左图),动作好,想提高它的概率。目标随 $r$ 线性上升,但到 $r=1+\epsilon$ 之后变平。也就是说,就算把概率提得再高,目标也不再增加,没有动力把策略往这个方向推过头。注意 clip 只发生在右侧,也就是 $r$ 偏大的那一边;左侧 $r<1-\epsilon$ 仍然是斜线。
$A<0$ 时(右图),动作差,想降低它的概率。目标在 $r<1-\epsilon$ 这一段是平的,过了 $1-\epsilon$ 之后,随 $r$ 增大持续往下掉。这里 clip 发生在左侧,也就是 $r$ 偏小那一边;右侧没有下限,会一直惩罚下去。
把两幅图合起来看,规律是:clip 只在“朝有利方向更新过头”时把目标削平,去掉继续过度更新的动力;而“朝不利方向跑”的那一侧不设限,仍然保留梯度。

第二张图(PPO 论文 Figure 2)画的是 Hopper-v1 上第一次更新时,几个目标量随策略参数变化的曲线。
先说横坐标,它不是训练时间。横坐标是 linear interpolation factor,表示在参数空间里,从旧参数 $\theta{\text{old}}$(取值 0)到一次 PPO 更新后得到的新参数 $\theta{\text{new}}$(取值 1)之间做线性插值,并且向两边延伸。它是沿“这一次更新方向”切出来的一条一维剖面,用来观察如果沿这个方向走得更远会发生什么,跟训练步数和时间没关系。
四条曲线:
- $L^{\text{CPI}} = \hat{\mathbb{E}}_t[r_t A_t]$(橙):未截断的代理目标,一路往上涨没有上界,单看它会把策略推到任意远。
- $\hat{\mathbb{E}}_t[\text{clip}(r_t, 1-\epsilon, 1+\epsilon)A_t]$(绿):截断项,走到一定程度就饱和、压平。
- $L^{\text{CLIP}} = \hat{\mathbb{E}}_t[\min(r_t A_t,\ \text{clip}(r_t,1-\epsilon,1+\epsilon)A_t)]$(红):取 min 之后的真正目标,先升,在 1 稍微靠右的地方到达峰值,再往后开始下降。
- $\hat{\mathbb{E}}_t[\text{KL}_t]$(蓝):插值策略和旧策略之间的 KL 散度。
重点是红色的 $L^{\text{CLIP}}$ 有一个峰,而这个峰恰好落在 KL 约等于 0.02 的地方。也就是说,优化 $L^{\text{CLIP}}$ 自然会停在一个新旧策略差异不大的位置,而不像 $L^{\text{CPI}}$ 那样越走越高、把策略推飞。
为什么只 clip 不够,还要套一个 min
关键在于:如果目标写成纯 clip 版本:
那么一旦 $r$ 落进被截断的平台区,梯度就是零。不管这次更新是变好还是变坏,都没有信号。
举个会出问题的例子。设 $A>0$,这个动作是好的,应该提高概率。但某次更新反而把概率降下去了,$r$ 掉到 $1-\epsilon$ 以下。这时候希望有梯度把 $r$ 拉回来。可是纯 clip 在 $r<1-\epsilon$ 处取 $\text{clip}=1-\epsilon$,目标变成常数 $(1-\epsilon)A$,梯度为零,策略可以一直停在这个“变坏了”的位置而不受惩罚。
加上 min 就能修正。$A>0$、$r<1-\epsilon$ 时:
所以 $\min$ 取的是没被截断的 $rA$,梯度恢复,把 $r$ 往回拉。换句话说:
- 朝有利方向更新过头($A>0$ 且 $r>1+\epsilon$,或 $A<0$ 且 $r<1-\epsilon$):min 取截断项,目标变平,不奖励过度更新。
- 朝不利方向跑($A>0$ 却 $r$ 变小,或 $A<0$ 却 $r$ 变大):min 取未截断项,惩罚照常,梯度把策略拉回来。
所以 min 的本质是取两者里更“悲观”(更小)的那个,让 $L^{\text{CLIP}}$ 成为真实目标的一个下界。只 clip 会在不利方向上漏掉惩罚,min 把这个漏洞补上。
蓝线 KL 单调上升,是因为横坐标越大,插值出来的策略离 $\theta_{\text{old}}$ 越远,而 KL 度量的就是两个分布之间的差距,离得越远自然越大。
KL 升高代表新策略相对旧策略改得越来越多,也就是这一步走得越来越大。PPO 想要的是“改一点点、别一步迈太远”。问题在于 $L^{\text{CPI}}$(橙)会一直奖励继续往前走,对应 KL 无限制地涨上去,这正是不加约束的策略梯度容易训崩的原因。而 $L^{\text{CLIP}}$(红)涨到峰值后掉头向下,优化它不会把 KL 推到很大,最后停在 KL 约 0.02 这种温和的程度。这就是 clip 加 min 想达到的效果:不用显式的 KL 惩罚或信赖域约束,光靠对 ratio 截断就把策略更新幅度限制住。
具体细节
on-policy 还是 off-policy
这和 DQN 不一样。DQN 是 off-policy,可以把以前的经验存在 replay buffer 里,过很久之后还拿出来训练。因为 DQN 学的是 Bellman target,数据可以不是当前策略生成的。
PPO 是 on-policy。大致过程是:用当前策略采样,短期复用这批 rollout 做多轮 minibatch 更新,然后丢掉这批数据,再用新策略重新采样。
PPO 采样用的就是当前策略 $\pi_{\text{old}}$,这批数据只在当前一次更新里短期复用。采一批 rollout,用它训练 num_epochs 遍,然后丢掉,再用新策略采下一批。所以 PPO 没有 DQN 那种长期 replay buffer。
off-policy(如 DQN)允许旧数据长期存在 replay buffer 里随机取用;on-policy 则要求数据来自当前策略附近,不能复用很久以前的策略采的数据。PPO 之所以敢对同一批数据训练多个 epoch,靠的就是 ratio 加 clip 限制住了新旧策略的差异。
网络架构
PPO 是 actor-critic 结构,需要两个输出:
- Actor:输入状态 $st$,输出动作分布 $\pi\theta(a_t \mid s_t)$。
- Critic:输入状态 $st$,输出状态价值 $V\theta(s_t)$。
其中 $\pi\theta(a_t \mid s_t)$ 是当前策略在 $s_t$ 下选动作 $a_t$ 的概率,$V\theta(s_t)$ 是 critic 对从 $s_t$ 出发的未来折扣回报的估计。$\theta$ 是网络参数,actor 和 critic 可以完全分开,也可以共享一部分主干。
Rollout 采样
如果只看一条环境交互里最基础的 transition:
1 | s_t, a_t, r_t, s_{t+1} |
这只是最小的交互记录,对 PPO 来说不够。Policy Gradient 更关心的是这个动作对后续整段表现的贡献,所以它通常先收集一批完整或截断的轨迹,然后统一算 return / advantage,再更新网络。
PPO rollout 里一条训练样本更接近这样:
1 | s_t, a_t, r_t, done_t, log_prob_t, value_t |
其中 log_prob_t 是旧策略下动作 $at$ 的对数概率,value_t 是旧 critic 给出的 $V{\text{old}}(st)$。`s{t+1}有些实现不会单独存成nextobs,因为同一个环境下一步的obs[t + 1]就是它;rollout 最后还会额外 forward 一次得到last_value = V(s_T)`,用来给截断位置 bootstrap。这里要分清楚:环境交互的 transition 可以写成 $(s_t,a_t,r_t,s{t+1})$,但 PPO 训练用的 buffer 条目还必须带上旧策略的 log_prob、旧 critic 的 value 和 done。
多环境并行跑的时候是什么样,这个很重要。比如 rsl_rl 这类库,在机器人训练时通常都是多环境并行。
假设有:
1 | num_envs = 4 |
意思是 4 个环境同时独立运行,每个环境跑 5 步。这个时候 rollout buffer 不是一条长链,而是一个二维结构:时间维度 $T$ 乘环境维度 $N$。
用 buffer[t, env] 表示第 env 个环境在第 t 个采样步的数据,具体结构大概是:
1 | env0 env1 env2 env3 |
这里一共有 num_steps * num_envs = 5 * 4 = 20 条 transition。每个格子里不是只存一个 reward,而是存这一时刻训练后面需要用到的一组数据:
1 | obs[t, env] |
如果设并行环境数 $N$、每个环境采样步数 $T$,那么一轮更新前会收集 $N \times T$ 条 transition。例如 $N=4096$、$T=24$,一共是 98304 条样本。
单个环境、单个时间步的交互是这样一段过程:当前观测 $st$ 送进 actor,得到旧策略分布 $\pi{\text{old}}(\cdot \mid st)$;从中采样动作 $a_t$;同时记下这个动作在旧策略下的对数概率 $\log \pi{\text{old}}(at \mid s_t)$;critic 给出 $V{\text{old}}(st)$;环境执行动作,返回 $s{t+1}, r_t, \text{done}_t$。把这些一起写进 buffer:
1 | buffer.add({ |
实际采样是 $N$ 个环境同时跑,所以 buffer 天然带时间和环境两个维度:
1 | obs.shape = [num_steps, num_envs, obs_dim] |
带入上面的例子,如果 num_envs = 4、num_steps = 5:
1 | obs.shape = [5, 4, obs_dim] |
每个环境是独立的一条时间序列,reset 的时刻彼此不同,所以后面算优势时不能把环境维度搅在一起。
采样完成后,先计算 advantage 和 return。每一步都有自己的 advantage,这一步要按照时间顺序从后往前算,具体方法放在后面。注意在算 GAE 之前不能打乱 shuffle,也不能先展平。训练阶段才会把数据展平、打乱,然后分成 minibatch。
区分一下四个量
这四个量很容易混,先各自说清楚。
即时奖励 $r_t$ 是环境这一步直接给的,只看当前一步。在机器狗这类任务里,它往往是多个奖励项的加权和,比如速度跟踪、姿态稳定、能耗惩罚、关节速度惩罚、终止惩罚、存活奖励等。
回报 $G_t$ 是从当前时刻往后的折扣累计收益:
$G_t$ 是往未来无限步看的量,实际代码里不可能真的计算无限步。PPO 代码里的 returns 往往也不是完整的 Monte Carlo $G_t$。
因为 PPO 通常不会等完整 episode 结束,而是只采样固定长度 $T$。从 $s_t$ 到 $s_T$ 这段真实 reward 能看到,后面没采到的部分用 critic 补上:
这叫截断后的 bootstrapped return。它不是完整 episode 的真实 $G_t$,而是:
1 | 已看到的真实 reward |
论文式 (10) 里的 advantage 可以理解成:
价值 $V(s_t)$ 是 critic 对 $G_t$ 的预测,是网络估计出来的,不是环境给的。
优势 $A_t$ 最直观的定义是实际回报比 critic 预期高出多少:
$A_t > 0$ 说明这个动作之后的结果比预期好,应该提高它的概率;$A_t < 0$ 则相反。
从 $A_t = G_t - V(s_t)$ 推到 TD error
把 $Gt = r_t + \gamma G{t+1}$ 代进优势定义:
在右边加减一个 $\gamma V(s_{t+1})$:
后一个括号正好是 $A_{t+1}$,前一个括号定义为 TD error:
于是得到递推:
一路展开就是:
GAE
上面的展开方差很大。比较直观的理解是:如果用很长的未来收益来评价当前动作,那么后面的随机事件都会被算进 $A_t$ 里。于是当前动作可能本来差不多,但算出来的 advantage 一会儿很大,一会儿很小,甚至正负变来变去。
比如机器狗在某个状态 $s_t$ 做了一个动作 $a_t$,这个动作本身可能没问题。但是后面几十步里可能发生很多随机因素,比如接触状态变化、外部扰动、姿态偏移、某个环境提前 done。这些都会影响后面的 return。
GAE 在每一项 TD error 前面再乘一个 $\lambda$,用来在偏差和方差之间做权衡(Schulman et al. 2015):
其中 $\gamma$ 是折扣因子,常用 0.99;$\lambda$ 是 GAE 系数,常用 0.95;$\gamma\lambda$ 是后续 TD error 的衰减速度。
两个极端能帮助理解 $\lambda$ 的作用。$\lambda=0$ 时,$\hat{A}_t=\delta_t$,只看一步 TD,方差小但偏差大,严重依赖 critic。$\lambda=1$ 时,退回到接近 $\hat{A}_t=G_t-V(s_t)$ 的形式,偏差小但方差大。$0<\lambda<1$ 是折中。
GAE 不是固定的 n-step,而是从当前步一直累加 TD error,直到 rollout 片段结束或者 episode done。比如 $T=24$,第 0 步最多能用 $\delta0 \ldots \delta{23}$,第 23 步只能用 $\delta_{23}$;中间遇到 done 就在那里截断。
GAE 的代码递推
采样后已有 rewards、dones、values,形状都是 [num_steps, num_envs]。另外还需要 rollout 结束后那一个状态的价值 last_value = V(s_T),形状是 [num_envs]。
GAE 从后往前算:
1 | advantages = zeros([num_steps, num_envs]) |
对应的公式,考虑 done 之后是:
done 的作用是切断序列。某一步 done=True 时,next_non_terminal = 0,于是 $\delta_t = r_t - V(s_t)$,并且 $\hat{A}_t = \delta_t$。这样不会把 reset 之后新 episode 的回报错误地接到当前 episode 后面。
returns(critic 的训练目标)
PPO 里 critic 的训练目标通常叫 returns,代码里是:
1 | returns = advantages + values |
这里的 values 是 rollout 阶段存下来的 $V_{\text{old}}(s_t)$,所以完整写法是:
它直接来自 $A_t = G_t - V(s_t)$ 的移项 $G_t = A_t + V(s_t)$。注意这是从当前状态出发的累计收益目标,也叫 value target 或 $\lambda$-return,不是一步 reward。
三者放一起对比:
- $r_t$ 是即时奖励。
- $\text{returns}_t$ 是未来累计收益的训练目标,到时候要用它算 MSE 来更新 critic。
- $\hat{A}_t$ 是当前动作比预期好多少。
这里注意,算 TD error、GAE 和 returns 时,用的一直是 rollout 阶段 old critic 存下来的 value。
为什么 value loss 不会退化成 $\hat{A}^2$
训练时存在两个 value,要分清楚。old_values 是 rollout 时旧 critic 的估计,已经在 buffer 里,用来算 GAE 和 returns;new_values 是更新时当前 critic 重新 forward 出来的,用来算 value loss。
returns 必须用 old values 构造:
value loss 用 new values 算:
1 | new_values = critic(mb_obs) |
如果错误地写成:
1 | returns = advantages + new_values |
那么:
里面再也没有 critic 要拟合的信息,critic 学不到东西。正确的理解是:returns 是固定标签,new values 是当前预测,value loss 推着预测去靠近标签。
展平
GAE 和 returns 算完后,所有量还是 [num_steps, num_envs, ...]。训练前把时间和环境两个维度合并成一个 batch 维:
1 | batch_size = num_steps * num_envs |
例如 $24 \times 4096 = 98304$,展平后 b_advantages 和 b_returns 都是 [98304]。
要强调的是:GAE 阶段绝对不能提前展平。优势要沿着每个环境自己的时间序列从后往前算,必须先保持 [num_steps, num_envs],算完之后才展平成 [num_steps * num_envs]。
minibatch 更新
设 batch_size = 98304、minibatch_size = 16384、num_epochs = 5。每个 epoch 有 $98304 / 16384 = 6$ 个 minibatch,num_epochs = 5 表示同一批 rollout 数据反复训练 5 遍,所以一共 $5 \times 6 = 30$ 次 optimizer.step()。
每一遍 epoch 都重新打乱这 98304 条样本,再切成 6 个 minibatch,逐个更新。这批数据用完就丢掉,再用更新后的策略采下一批。
算 GAE 时它们是一条有先后关系的轨迹;算完之后进入训练,就被当成一堆可独立采样的样本。
当 batch 数量除不尽时,最后一个 minibatch 会比别的小。比如单环境 200 步、minibatch_size = 64,最后一块只剩 8 条。因为 loss 用的是 .mean(),样本少也不影响量纲,只是这一步梯度估计噪声大一点,对训练基本无影响。想更干净可以丢掉最后不满的那块(drop_last),或把 batch 大小选成能整除的数。
单个 minibatch 内部计算:网络更新步骤
取出一个 minibatch:
1 | mb_obs |
第一步,用当前 actor 重新算 log prob。注意 mb_actions 是 rollout 时旧策略采出来的动作,训练时不重新采样,而是拿当前新策略去评估同一个动作:
1 | dist = Normal(mean, std) |
这里注意:log_prob 是在算当前采样动作 action 在这个高斯动作分布下的对数概率密度。rollout 时 mb_actions 是从分布里采样出来的,但更新阶段不重新采样,也不对“采样”这个操作求导。更新时 mb_actions 被当成固定常量,梯度是通过 log_prob 对分布参数 $\mu$、$\sigma$ 的依赖传回去的。
高斯对数概率是:
这里 $a$ 是常量,$\mu$、$\sigma$ 是网络输出,所以求导是对 $\mu$、$\sigma$ 求,再传回 actor。这就是策略梯度的 score function(likelihood ratio)思路:不对随机采样求导,而是调整“产生这个动作的概率密度”。优势为正就抬高它的 log_prob,优势为负就压低。正因为不需要对采样反传,rollout 阶段才可以放心地 no_grad 和 detach。
另一条路线是 SAC / VAE 用的重参数化技巧:
它把随机性挪到与参数无关的 $\epsilon$ 上,让梯度直接穿过 $a$。PPO 不走这条路。
第二步,算 ratio。rollout 存的是 $\log \pi{\text{old}}(a_t \mid s_t)$,现在算出 $\log \pi\theta(a_t \mid s_t)$,所以:
$r_t > 1$ 表示新策略更倾向这个动作,$r_t < 1$ 表示更不倾向。
第三步,算 clipped policy 目标。原始 policy gradient 是 $r_t(\theta)\hat{A}_t$,PPO 对它做截断:
$\epsilon$ 常用 0.2。代码里 optimizer 默认最小化,所以取负号:
1 | surr1 = ratio * mb_advantages |
正负优势下 clip 的效果不同。$\hat{A}_t > 0$ 时应该提高动作概率,但 ratio 的有效增益被压在 $1+\epsilon$,$\epsilon=0.2$ 时就是 1.2。$\hat{A}_t < 0$ 时应该降低概率,但有效下降被压在 $1-\epsilon$,也就是 0.8。本质就是不让新策略一步偏离旧策略太远。
第四步,当前 critic 重新 forward 得到:
1 | new_values = critic(mb_obs) |
这是 critic 对这批 obs 的最新 $V(s)$ 预测。
第五步,算 value loss,标签是 mb_returns:
1 | value_loss = mean((new_values - mb_returns) ** 2) |
第六步,算 entropy bonus:
1 | entropy = new_dist.entropy() # 多维连续动作要 .sum(dim=-1),再 .mean() |
entropy 鼓励策略保持随机性,防止过早收敛到确定动作,维持探索。高斯熵有闭式解,只跟 $\sigma$ 有关、与 $\mu$ 无关:
entropy() 内部直接套这个公式,不需要采样。$\sigma$ 越大,熵越大,策略越随机;$\sigma$ 越小,策略越确定。$\sigma$ 是网络 fc_std 根据状态算出来的,所以 entropy 经由 $\sigma$ 到 fc_std 这一路带梯度,-c2 * entropy 反传时能更新到 actor 的参数。
std 有两种常见写法:本实现是 state-dependent std,$\sigma$ 随状态由网络输出,更灵活;很多经典 PPO 复现用 state-independent std,把 log_std 设成与状态无关的全局 nn.Parameter,所有状态共享,只有 $\mu$ 随状态变,更简单稳定。两种都可行,但要保证网络输出 shape、log_prob.sum(dim=-1) 和 entropy 的处理是一致的。
第七步,组合总损失。PPO 论文最大化的目标是:
代码里改成最小化,符号相应翻转:
1 | loss = policy_loss + c1 * value_loss - c2 * entropy.mean() |
符号可以这样记:优化器只会最小化 loss,所以想最大化的项加负号,想最小化的项加正号。policy 目标 $L^{\text{CLIP}}$ 要最大化,所以 policy_loss 取负;value 误差要最小化,所以是正号;entropy 要保持,也就是希望它大一点,所以取负。
其中 policy_loss 那个负号是对整个 $\min$ 取负,漏了会让策略朝反方向更新,reward 越训越低,是排查不收敛时第一个该看的地方。$c_1=0.5$、$c_2=0.01$ 不是推导出来的,是论文和主流实现的经验默认值,连续控制任务可以先按这个用,再看训练曲线调。






