强化学习的几个主要方法(策略梯度、PPO、REINFORCE实现等)(下)
由于平台字数限制,上文:https://blog.csdn.net/ooblack/article/details/144198538
4. PPO算法
近端策略优化(proximal policy optimization,PPO)算法是OpenAI的默认强化学习算法,在RLHF中也用到了这个算法,说明这个算法还是很强的,那它到底解决了一个什么问题呢?
先介绍两个概念,同策略学习(On-Policy Learning)和异策略学习(Off-Policy Learning),在强化学习里面,要学习的是一个智能体。如果要学习的智能体和与环境交互的智能体是相同的,我们称之为同策略。如果要学习的智能体和与环境交互的智能体不是相同的,我们称之为异策略。
PPO的主要解决的问题是什么呢?
无论是A2C算法还是REINFOCE算法,都有一个问题,数据在采样完成,更新模型一次之后就丢弃了,前文不止一次强调过,数据采样是很昂贵且费事的,只用一次就丢显然是不合理的,但是数据用一次之后模型已经更新了,旧的数据是旧的模型产生的不能用来更新新的模型,PPO就是用来解决这个问题的。
它的思路是这样的,我们用梯度下降去降低损失,由于学习率的问题,我们可能方向对了,但是步子迈小了,所以用旧数据再去更新一次,再迈一步,但是直觉告诉我们不能一直用旧数据,因为模型参数多迭代几次之后可能跟产生旧数据的旧模型完全不一样了,PPO最大的贡献就在于它告诉模型什么时候旧数据不能再用了,定量分析了新模型与旧模型的差距。
这里先介绍重要性采样的概念。
4.1 重要性采样
假设我们有一个函数 f ( x ) f(x) f(x),要计算从分布 p p p 采样 x x x,再把 x x x 代入 f f f,得到 f ( x ) f(x) f(x)。我们该怎么计算 f ( x ) f(x) f(x) 的期望值呢?假设我们不能对分布 p p p 做积分,但可以从分布 p p p 采样一些数据 x i x^i xi。把 x i x^i xi 代入 f ( x ) f(x) f(x),取它的平均值,就可以近似 f ( x ) f(x) f(x) 的期望值。
E x ∼ p [ f ( x ) ] ≈ 1 N ∑ i = 1 N f ( x i ) \mathbb{E}_{x \sim p}[f(x)] \approx \frac{1}{N} \sum_{i=1}^{N} f(x^i) Ex∼p[f(x)]≈N1i=1∑Nf(xi)
现在有另外一个问题,假设我们不能从分布 p p p 采样数据,只能从另外一个分布 q q q 采样数据 x i x^i xi, q q q 可以是任何分布。如果我们从 q q q 采样 x i x^i xi,就不能使用刚刚的方法。因为刚刚的方法需要从 p p p里面采样。
但是我硬要从 q q q里面采,神奇的数学会给出解决方案:
E x ∼ p [ f ( x ) ] = ∫ f ( x ) p ( x ) d x = ∫ f ( x ) p ( x ) q ( x ) q ( x ) d x = E x ∼ q [ f ( x ) p ( x ) q ( x ) ] \begin{align} \mathbb{E}_{x \sim p}[f(x)] &= \int f(x)p(x)dx \\ &= \int f(x) \frac{p(x)}{q(x)} q(x) dx \\&= \mathbb{E}_{x \sim q} \left[ f(x) \frac{p(x)}{q(x)} \right] \end{align} Ex∼p[f(x)]=∫f(x)p(x)dx=∫f(x)q(x)p(x)q(x)dx=Ex∼q[f(x)q(x)p(x)]
我们就可以写成对 q q q里面所采样出来的 x 取期望值。我们从 q q q里面采样 x x x,再计算 f ( x ) p ( x ) q ( x ) f(x){\frac{p(x)}{q(x)}} f(x)q(x)p(x),再取期望值。所以就算我们不能从 p p p里面采样数据,但只要能从 q q q里面采样数据,就可以计算从 p p p采样 x x x代入 f f f以后的期望值。
其中 f ( x ) p ( x ) q ( x ) f(x){\frac{p(x)}{q(x)}} f(x)q(x)p(x)称作重要性权重。它主要用来修正两个分布之间的差异,但是它也不是万能的,通过上面的式子我们能看出它只能保证两种采样方法的期望相同,没说方差相同,如果方差巨大的话,也达不到我们想要的效果,两个分布的方差如下( Var [ X ] = E [ X 2 ] − ( E [ X ] ) 2 \text{Var}[X] = \mathbb{E}[X^2] - \left(\mathbb{E}[X]\right)^2 Var[X]=E[X2]−(E[X])2)
Var x ∼ p [ f ( x ) ] = E x ∼ p [ f ( x ) 2 ] − ( E x ∼ p [ f ( x ) ] ) 2 Var x ∼ q [ f ( x ) p ( x ) q ( x ) ] = E x ∼ q [ ( f ( x ) p ( x ) q ( x ) ) 2 ] − ( E x ∼ q [ f ( x ) p ( x ) q ( x ) ] ) 2 = E x ∼ p [ f ( x ) 2 p ( x ) q ( x ) ] − ( E x ∼ p [ f ( x ) ] ) 2 \begin{align*} \text{Var}_{x \sim p}[f(x)] &= \mathbb{E}_{x \sim p} \left[ f(x)^2 \right] - \left(\mathbb{E}_{x \sim p}[f(x)]\right)^2 \\ \text{Var}_{x \sim q} \left[f(x) \frac{p(x)}{q(x)} \right] &= \mathbb{E}_{x \sim q} \left[ \left(f(x) \frac{p(x)}{q(x)}\right)^2 \right] - \left(\mathbb{E}_{x \sim q} \left[ f(x) \frac{p(x)}{q(x)}\right] \right)^2 \\ &= \mathbb{E}_{x \sim p} \left[ f(x)^2 \frac{p(x)}{q(x)} \right] - \left(\mathbb{E}_{x \sim p}[f(x)]\right)^2 \end{align*} Varx∼p[f(x)]Varx∼q[f(x)q(x)p(x)]=Ex∼p[f(x)2]−(Ex∼p[f(x)])2=Ex∼q[(f(x)q(x)p(x))2]−(Ex∼q[f(x)q(x)p(x)])2=Ex∼p[f(x)2q(x)p(x)]−(Ex∼p[f(x)])2
可以看到对 q q q分布采样的方差比对 p p p分布采样的方差多乘了一个 p ( x ) q ( x ) \frac{p(x)}{q(x)} q(x)p(x),所以就一件事, p p p和 q q q的分布要尽量相似用这个方法才能有一个很好的效果。下面举个例子来说明这一点:

例如,当 p ( x ) p(x) p(x) 和 q ( x ) q(x) q(x) 差距很大时,就会有问题。如图所示,假设蓝线是 p ( x ) p(x) p(x) 的分布,绿线是 q ( x ) q(x) q(x) 的分布,红线是 f ( x ) f(x) f(x)。如果我们要计算 f ( x ) f(x) f(x) 的期望值,从分布 p ( x ) p(x) p(x) 做采样,显然 E x ∼ p [ f ( x ) ] \mathbb E_{x \sim p}[f(x)] Ex∼p[f(x)] 是负的。这是因为左边区域 p ( x ) p(x) p(x) 的概率很高,所以采样会到这个区域,而 f ( x ) f(x) f(x) 在这个区域是负的,所以理论上这一项算出来会是负的。
接下来我们改成从 q ( x ) q(x) q(x) 采样,因为 q ( x ) q(x) q(x) 在右边区域的概率比较高,所以如果我们的采样点不够多,可能只会采样到右侧。如果我们只采样到右侧,可能 E x ∼ q [ f ( x ) p ( x ) q ( x ) ] \mathbb E_{x \sim q}[f(x)\frac{p(x)}{q(x)}] Ex∼q[f(x)q(x)p(x)] 是正的。我们这边采样到这些点,去计算它们的 f ( x ) p ( x ) q ( x ) f(x)\frac{p(x)}{q(x)} f(x)q(x)p(x) 都是正的。我们采样到这些点都是正的,取期望值以后也是正的,这是因为采样的次数不够多。假设我们采样次数很少,只能采样到右边。左边虽然概率很低,但也有可能被采样到。假设我们好不容易采样到左边的点,因为左边的点的 p ( x ) p(x) p(x) 和 q ( x ) q(x) q(x) 差是很多的,这边 p ( x ) p(x) p(x) 很大, q ( x ) q(x) q(x) 很小。 f ( x ) f(x) f(x) 好不容易终于采样到一个负的,这个负的就会被乘上一个非常大的权重,这样就可以平衡刚才那边一直采样到正的值的情况。最终我们算出这一项的期望值,终究还是负的。但前提是我们要采样足够多次,这件事情才会发生。但有可能采样次数不够多, E x ∼ p [ f ( x ) ] \mathbb E_{x \sim p}[f(x)] Ex∼p[f(x)] 与 E x ∼ q [ f ( x ) p ( x ) q ( x ) ] \mathbb E_{x \sim q}[f(x)\frac{p(x)}{q(x)}] Ex∼q[f(x)q(x)p(x)] 可能就有很大的差距,这就是重要性采样的问题。
有了重要性采样,就可以使用旧模型参数 θ ′ {\theta'} θ′采集到的数据来更新新的模型参数 θ \theta θ,旧模型记作 π θ \pi_{\theta} πθ,新模型记作 π θ ′ \pi_{\theta'} πθ′。
梯度公式:
∇ R ˉ θ = E τ ∼ p θ ( τ ) [ p θ ( τ ) p θ ′ ( τ ) R ( τ ) ∇ log p θ ( τ ) ] \nabla \bar{R}_{\theta} = \mathbb{E}_{\tau \sim p^{\theta}(\tau)} \left[ \frac{p^{\theta}(\tau)}{p^{\theta'}(\tau)} R(\tau) \nabla \log p^{\theta}(\tau) \right] ∇Rˉθ=Eτ∼pθ(τ)[pθ′(τ)pθ(τ)R(τ)∇logpθ(τ)]
这个公式是由REINFORCE的梯度公式加了一个重要性权重变换过来的。
状态动作对概率分开算:
E ( s t , a t ) ∼ π θ [ A θ ( s t , a t ) ∇ log p θ ( a t ∣ s t ) n ] \mathbb{E}_{(s_t, a_t) \sim \pi_{\theta}} \left[ A^{\theta}(s_t, a_t) \nabla \log p_{\theta}(a_t|s_t)^n \right] E(st,at)∼πθ[Aθ(st,at)∇logpθ(at∣st)n]
这个式子表示在状态-动作对$ (s_t, a_t) $服从策略 π θ π_θ πθ 分布下的期望。其中 A θ ( s t , a t ) A^{\theta}(s_t, a_t) Aθ(st,at)表示的就是优势函数,之前在A2C算法里替换的那个,这里讨论的是重要性采样,继续使用 A θ ( s t , a t ) A^{\theta}(s_t, a_t) Aθ(st,at)来表示,最后在PPO算法里换成时序差分误差即可。
我们用状态-动作对$ (s_t, a_t) $服从策略 π θ ′ π_{\theta'} πθ′ 分布下的期望加上重要性权重 f ( x ) p ( x ) q ( x ) f(x){\frac{p(x)}{q(x)}} f(x)q(x)p(x)来近似$ (s_t, a_t) 在 在 在π_θ$下的分布,即:
E ( s t , a t ) ∼ π θ ′ [ p θ ( s t , a t ) p θ ′ ( s t , a t ) A θ ( s t , a t ) ∇ log p θ ( a t n ∣ s t n ) n ] \mathbb{E}_{(s_t, a_t) \sim \pi_{\theta'}} \left[ \frac{p_{\theta}(s_t, a_t)}{p_{\theta'}(s_t, a_t)} A^{\theta}(s_t, a_t) \nabla \log p_{\theta}(a_t^n|s_t^n)^n \right] E(st,at)∼πθ′[pθ′(st,at)pθ(st,at)Aθ(st,at)∇logpθ(atn∣stn)n]
由于:
p θ ( s t , a t ) = p θ ( a t ∣ s t ) p θ ( s t ) p θ ′ ( s t , a t ) = p θ ′ ( a t ∣ s t ) p θ ′ ( s t ) p_{\theta}(s_t, a_t) = p_{\theta}(a_t|s_t)p_{\theta}(s_t) \\ p_{\theta'}(s_t, a_t) = p_{\theta'}(a_t|s_t)p_{\theta'}(s_t) pθ(st,at)=pθ(at∣st)pθ(st)pθ′(st,at)=pθ′(at∣st)pθ′(st)
因此:
E ( s t , a t ) ∼ π θ ′ [ p θ ( a t ∣ s t ) p θ ( s t ) p θ ′ ( a t ∣ s t ) p θ ′ ( s t ) A θ ′ ( s t , a t ) ∇ log p θ ( a t n ∣ s t n ) n ] \mathbb{E}_{(s_t, a_t) \sim \pi_{\theta'}} \left[ \frac{p_{\theta}(a_t|s_t)p_{\theta}(s_t)}{p_{\theta'}(a_t|s_t)p_{\theta'}(s_t)} A^{\theta'}(s_t, a_t) \nabla \log p_{\theta}(a_t^n|s_t^n)^n \right] E(st,at)∼πθ′[pθ′(at∣st)pθ′(st)pθ(at∣st)pθ(st)Aθ′(st,at)∇logpθ(atn∣stn)n]
假设模型是 θ {\theta} θ的时候,我们看到 s t s_t st的概率,与模型是 θ ′ θ' θ′的时候,我们看到 s t s_t st的概率是一样的,即 p θ ( s t ) = p θ ′ ( s t ) {p_{\theta}(s_t)} = {p_{\theta'}(s_t)} pθ(st)=pθ′(st)。因此可得:
E ( s t , a t ) ∼ π θ ′ [ p θ ( a t ∣ s t ) p θ ′ ( a t ∣ s t ) A θ ′ ( s t , a t ) ∇ log p θ ( a t n ∣ s t n ) n ] \mathbb{E}_{(s_t, a_t) \sim \pi_{\theta'}} \left[ \frac{p_{\theta}(a_t|s_t)}{p_{\theta'}(a_t|s_t)} A^{\theta'}(s_t, a_t) \nabla \log p_{\theta}(a_t^n|s_t^n)^n \right] E(st,at)∼πθ′[pθ′(at∣st)pθ(at∣st)Aθ′(st,at)∇logpθ(atn∣stn)n]
为什么可以这样假设呢?有很多种解释,有的说差的不远,有的说环境状态与动作关联性不是很强,也有的说随机系统怎么怎么滴,反正也不用纠结了,不这么干这个算法很难进行下去,这么做效果好就对了!
根据之前策略梯度算法里面的套路:
∇ J θ ′ ( θ ) = E ( s t , a t ) ∼ π θ ′ [ ∇ p θ ( a t ∣ s t ) p θ ′ ( a t ∣ s t ) A θ ′ ( s t , a t ) ] \nabla J^{\theta'}(\theta) = \mathbb{E}_{(s_t, a_t) \sim \pi_{\theta'}} \left[ \frac{\nabla p_{\theta}(a_t|s_t)}{p_{\theta'}(a_t|s_t)} A^{\theta'}(s_t, a_t) \right] ∇Jθ′(θ)=E(st,at)∼πθ′[pθ′(at∣st)∇pθ(at∣st)Aθ′(st,at)]
那我们要优化的目标函数就是:
J θ ′ ( θ ) = E ( s t , a t ) ∼ π θ ′ [ p θ ( a t ∣ s t ) p θ ′ ( a t ∣ s t ) A θ ′ ( s t , a t ) ] J^{\theta'}(\theta) = \mathbb{E}_{(s_t, a_t) \sim \pi_{\theta'}} \left[ \frac{p_{\theta}(a_t|s_t)}{p_{\theta'}(a_t|s_t)} A^{\theta'}(s_t, a_t) \right] Jθ′(θ)=E(st,at)∼πθ′[pθ′(at∣st)pθ(at∣st)Aθ′(st,at)]
我们本来是要在 θ \theta θ上进行采样来更新 θ \theta θ的,但是现在变成在 θ ′ \theta' θ′上采样来更新 θ \theta θ了,那就由同策略学习变成了异策略学习。
以上就是重要性采样。它有一个显而易见的问题,就是新旧策略差距过大的话,旧策略采出来的数据意义就不是很大了,PPO算法就是为了解决这一问题。
4.2近端策略优化(PPO)
为了不使 p θ ( a t ∣ s t ) p_{\theta}(a_t|s_t) pθ(at∣st)和 p θ ′ ( a t ∣ s t ) p_{\theta'}(a_t|s_t) pθ′(at∣st)差距过大,就是让两个概率模型差距不那么大,之前我们是用交叉熵来衡量两个概率模型的差距,在这里我们使用相对熵,也就是KL散度来衡量这种差距。
KL散度公式:
D K L ( P ∣ ∣ Q ) = ∑ x P ( x ) log P ( x ) Q ( x ) D_{KL}(P||Q) = \sum_x P(x) \log \frac{P(x)}{Q(x)} DKL(P∣∣Q)=x∑P(x)logQ(x)P(x)
D K L ( P ∣ ∣ Q ) D_{KL}(P||Q) DKL(P∣∣Q)表示KL散度,KL散度量化了如果用 Q Q Q来表示 P P P,需要付出多少额外的信息代价。
既然我们可以量化两个模型之间的差距,模型之间的差距又不能过大,就顺理成章的能想到把量化过后的模型之间的差距当成一个正则项加到目标函数里面去不就行了吗,事实上PPO算法也是这样做的。
PPO算法的优化目标(损失函数):
J P P O θ ′ ( θ ) = J θ ′ ( θ ) − β K L ( θ , θ ′ ) J^{\theta'}_{PPO}(\theta) = J^{\theta'}(\theta) - \beta KL(\theta, \theta') \\ JPPOθ′(θ)=Jθ′(θ)−βKL(θ,θ′)
J θ ′ ( θ ) = E ( s t , a t ) ∼ π θ ′ [ p θ ( a t ∣ s t ) p θ ′ ( a t ∣ s t ) A θ ′ ( s t , a t ) ] J^{\theta'}(\theta) = \mathbb{E}_{(s_t, a_t) \sim \pi_{\theta'}} \left[ \frac{p_{\theta}(a_t|s_t)}{p_{\theta'}(a_t|s_t)} A^{\theta'}(s_t, a_t) \right] Jθ′(θ)=E(st,at)∼πθ′[pθ′(at∣st)pθ(at∣st)Aθ′(st,at)]
PPO 有一个前身:信任区域策略优化(trust region policy optimization,TRPO)。TRPO 可表示为:
J T R P O θ ′ ( θ ) = E ( s t , a t ) ∼ π θ ′ [ p θ ( a t ∣ s t ) p θ ′ ( a t ∣ s t ) A θ ′ ( s t , a t ) ] , s.t. K L ( θ , θ ′ ) < δ J^{\theta'}_{TRPO}(\theta) = \mathbb{E}_{(s_t, a_t) \sim \pi_{\theta'}} \left[ \frac{p_{\theta}(a_t|s_t)}{p_{\theta'}(a_t|s_t)} A^{\theta'}(s_t, a_t) \right], \text{ s.t. } KL(\theta, \theta') < \delta JTRPOθ′(θ)=E(st,at)∼πθ′[pθ′(at∣st)pθ(at∣st)Aθ′(st,at)], s.t. KL(θ,θ′)<δ
TRPO看起来和PPO很接近了,但是它把KL散度当成一种约束了,这种约束在基于梯度优化里面很难实现它,但是如果像PPO那样直接当成一个正则项,那么梯度优化实现它就会非常简单。
PPO算法还有两个变体。
4.2.1 近端策略优化惩罚(PPO-penalty、PPO1)
在实现PPO算法的时候一般使用前一个训练的迭代得到的演员的参数 θ k \theta^k θk与环境做大量交互,得到大量数据,来更新 θ \theta θ,就可以写成这样:
J P P O θ k ( θ ) = J θ k ( θ ) − β K L ( θ , θ k ) J^{\theta^k}_{PPO}(\theta) = J^{\theta^k}(\theta) - \beta KL(\theta, \theta^k)\\ JPPOθk(θ)=Jθk(θ)−βKL(θ,θk) J θ k ( θ ) ≈ ∑ ( s t , a t ) p θ ( a t ∣ s t ) p θ k ( a t ∣ s t ) A θ k ( s t , a t ) J^{\theta^k}(\theta) \approx \sum_{(s_t, a_t)} \frac{p_{\theta}(a_t|s_t)}{p_{\theta^k}(a_t|s_t)} A^{\theta^k}(s_t, a_t) Jθk(θ)≈(st,at)∑pθk(at∣st)pθ(at∣st)Aθk(st,at)
里面有个超参数 β \beta β有点不好控制,我们可以给KL散度设置一个最大值,一个最小值,当KL散度大于最大值的时候,就说明后面的罚没有发挥作用,就是正则项作用太小了,就增大 β \beta β提升KL散度的影响,当KL散度小于最小值的时候,就说明罚的太重了,后面一项的效果太强了,就减小 β \beta β削弱KL散度的影响。
因此我们称之为自适应KL惩罚(adaptive KL penalty)。
4.2.2 近端策略优化裁剪(PPO-clip、PPO2)
如果觉得计算 KL 散度太复杂了,还有一个 PPO2算法,PPO2 即近端策略优化裁剪。近端策略优化裁剪的目标函数里面没有 KL 散度,其要最大化的目标函数为
J P P O θ k ( θ ) ≈ ∑ ( s t , a t ) min ( p θ ( a t ∣ s t ) p θ k ( a t ∣ s t ) A θ k ( s t , a t ) , clip ( p θ ( a t ∣ s t ) p θ k ( a t ∣ s t ) , 1 − ϵ , 1 + ϵ ) A θ k ( s t , a t ) ) J^{\theta^k}_{PPO}(\theta) \approx \sum_{(s_t, a_t)} \min \left( \frac{p_{\theta}(a_t|s_t)}{p_{\theta^k}(a_t|s_t)} A^{\theta^k}(s_t, a_t), \right. \\ \left. \text{clip} \left(\frac{p_{\theta}(a_t|s_t)}{p_{\theta^k}(a_t|s_t)}, 1-\epsilon, 1+\epsilon \right) A^{\theta^k}(s_t, a_t) \right) JPPOθk(θ)≈(st,at)∑min(pθk(at∣st)pθ(at∣st)Aθk(st,at),clip(pθk(at∣st)pθ(at∣st),1−ϵ,1+ϵ)Aθk(st,at))
第二项前面有一个裁剪(clip)函数,裁剪函数是指,在括号里面有3项,如果第一项小于第二项,那就输出 1 − ε 1−ε 1−ε;第一项如果大于第三项,那就输出 1 + ε 1+ε 1+ε。
ε ε ε是一个超参数,是我们要调整的,可以设置成 0.1 或 0.2 。
剪裁函数图像:

看到PPO2的目标函数就不如之前的式子好理解了,那这个到底是啥意思呢,不用KL散度怎么知道新模型和旧模型之间的差距呢?

再重新审视一下剪裁函数
观察上面两幅图,绿色的线指的是 p θ ( a t ∣ s t ) p θ k ( a t ∣ s t ) \frac{p_{\theta}(a_t|s_t)}{p_{\theta^k}(a_t|s_t)} pθk(at∣st)pθ(at∣st),蓝色的线指的是被剪裁之后的样子。
- A θ k ( s t , a t ) > 0 A^{\theta^k}(s_t, a_t) > 0 Aθk(st,at)>0表示当前状态动作对是好的。希望它的概率,也就是 p θ ( a t ∣ s t ) p_{\theta}(a_t|s_t) pθ(at∣st)越大越好,但是也不能太大,它与 p θ ′ ( a t ∣ s t ) p_{\theta'}(a_t|s_t) pθ′(at∣st)的比值不能超过 1 + ε 1+ε 1+ε,如果超过了就会遇到我们之前的方差问题,红色的线就是指的是目标函数。
- A θ k ( s t , a t ) < 0 A^{\theta^k}(s_t, a_t) < 0 Aθk(st,at)<0表示当前状态动作对是不好的。同理,希望它的概率,也就是 p θ ( a t ∣ s t ) p_{\theta}(a_t|s_t) pθ(at∣st)越小越好,但是也不能太小,它与 p θ ′ ( a t ∣ s t ) p_{\theta'}(a_t|s_t) pθ′(at∣st)的比值不能低于 1 − ε 1-ε 1−ε,如果低于了就会遇到我们之前的方差问题,红色的线就是指的是目标函数。
4.3 PPO算法的实现
这里我们选择实现的是近端策略优化裁剪,也就是PPO2,PPO算法是策略梯度算法的一种,为了将其效果最大化,可以结合A2C算法一起使用,那就是Critic网络和A2C一样,A2C算法中Actor我们用PPO的优化目标来代替A2C算法中的目标,同时我们采用时时序差分的方法来做采样,把优势函数换成时序差分误差即可,损失函数就变成了以下:
J P P O θ k ( θ ) ≈ ∑ ( s t , a t ) min ( p θ ( a t ∣ s t ) p θ k ( a t ∣ s t ) δ t n , clip ( p θ ( a t ∣ s t ) p θ k ( a t ∣ s t ) , 1 − ϵ , 1 + ϵ ) δ t n ) J^{\theta^k}_{PPO}(\theta) \approx \sum_{(s_t, a_t)} \min \left( \frac{p_{\theta}(a_t|s_t)}{p_{\theta^k}(a_t|s_t)} \delta_t^n, \right. \\ \left. \text{clip} \left(\frac{p_{\theta}(a_t|s_t)}{p_{\theta^k}(a_t|s_t)}, 1-\epsilon, 1+\epsilon \right) \delta_t^n \right) JPPOθk(θ)≈(st,at)∑min(pθk(at∣st)pθ(at∣st)δtn,clip(pθk(at∣st)pθ(at∣st),1−ϵ,1+ϵ)δtn)
其中 δ t n = r t n + V π ( s t + 1 n ) − V π ( s t n ) :TD-error。 \delta_t^n = r_t^n + V_{\pi}(s_{t+1}^n) - V_{\pi}(s_t^n) \text{:TD-error。} δtn=rtn+Vπ(st+1n)−Vπ(stn):TD-error。
但是在PPO的实现里面我们一般不直接这么做,而是像套娃一样用了另一种叫做GAE(Generalized Advantage Estimation)的误差来取代TD-error,就像最开始我们用回报 G t G_t Gt来取代单步奖励 r t r_t rt那样, G t G_t Gt的公式是这样的:
G t = r t + 1 + γ r t + 2 + γ 2 r t + 3 + . . . + γ T − t − 1 r T G_t = r_{t+1} + \gamma r_{t+2} + \gamma^2 r_{t+3} + ... + \gamma^{T-t-1} r_T Gt=rt+1+γrt+2+γ2rt+3+...+γT−t−1rT
也就是:
G t = ∑ i = t + 1 T γ i − t − 1 r i G_t = \sum_{i=t+1}^T \gamma^{i-t-1} r_i Gt=i=t+1∑Tγi−t−1ri
那GAE是怎么做的呢?
A t G A E = ∑ l = 0 ∞ ( γ λ ) l δ t ( l ) A^{GAE}_t = \sum_{l=0}^{\infty} (\gamma \lambda)^l \delta^{(l)}_t AtGAE=l=0∑∞(γλ)lδt(l)
也可以写成
A t G A E = ∑ l = 0 T − t − 1 ( γ λ ) l δ t + l A^{GAE}_t = \sum_{l=0}^{T-t-1} (\gamma \lambda)^l \delta_{t+l} AtGAE=l=0∑T−t−1(γλ)lδt+l
这样看就非常清楚了,其中 δ t = r t + V π ( s t + 1 ) − V π ( s t ) :TD-error。 \delta_t = r_t + V_{\pi}(s_{t+1}) - V_{\pi}(s_t) \text{:TD-error。} δt=rt+Vπ(st+1)−Vπ(st):TD-error。
-
当 λ = 0 λ=0 λ=0,GAE等价于单步 TD,偏差低但方差高。
-
当 λ = 1 λ=1 λ=1,GAE近似于蒙特卡洛回报(Monte Carlo Return),方差低但偏差高。
-
调整 λ λ λ参数可以在偏差和方差之间找到一个更合适的折中。
-
γ γ γ是折扣因子,控制对未来奖励的关注程度。
用GAE来替代之前的 A θ k ( s t , a t ) A^{\theta^k}(s_t, a_t) Aθk(st,at),那公式就可以写成:
J P P O θ k ( θ ) ≈ ∑ ( s t , a t ) min ( p θ ( a t ∣ s t ) p θ k ( a t ∣ s t ) A t , n G A E , clip ( p θ ( a t ∣ s t ) p θ k ( a t ∣ s t ) , 1 − ϵ , 1 + ϵ ) A t , n G A E ) J^{\theta^k}_{PPO}(\theta) \approx \sum_{(s_t, a_t)} \min \left( \frac{p_{\theta}(a_t|s_t)}{p_{\theta^k}(a_t|s_t)} A^{GAE}_{t,n}, \right. \\ \left. \text{clip} \left(\frac{p_{\theta}(a_t|s_t)}{p_{\theta^k}(a_t|s_t)}, 1-\epsilon, 1+\epsilon \right) A^{GAE}_{t,n} \right) JPPOθk(θ)≈(st,at)∑min(pθk(at∣st)pθ(at∣st)At,nGAE,clip(pθk(at∣st)pθ(at∣st),1−ϵ,1+ϵ)At,nGAE)
智能体的任务和之前的A2C算法里面的任务是一样的。
import gym
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
from torch.distributions import Categorical
import matplotlib.pyplot as pltclass Config:def __init__(self):# PPO特定参数self.clip_epsilon = 0.2 # PPO裁剪系数,用于限制策略更新的幅度self.ppo_epochs = 10 # 每次更新的PPO轮数self.batch_size = 64 # 小批量训练的大小self.gae_lambda = 0.95 # GAE参数,用于计算优势函数# 基础参数self.hidden_dim = 64 # 神经网络隐藏层维度self.lr = 0.0003 # 学习率self.gamma = 0.99 # 折扣因子self.max_episodes = 5000 # 最大训练回合数self.max_steps = 500 # 每个回合最大步数self.seed = 42 # 随机种子self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 设备选择(GPU/CPU)self.entropy_coef = 0.01 # 熵正则化系数,用于鼓励探索# 分离Actor和Critic网络
class Actor(nn.Module):def __init__(self, state_dim, action_dim):super(Actor, self).__init__()# 定义Actor网络结构self.network = nn.Sequential(nn.Linear(state_dim, 64), # 输入层到隐藏层nn.Tanh(), # 激活函数nn.Linear(64, action_dim), # 隐藏层到输出层nn.Softmax(dim=-1) # 输出层使用Softmax得到动作概率分布)def forward(self, state):# 前向传播,返回动作概率分布return self.network(state)class Critic(nn.Module):def __init__(self, state_dim):super(Critic, self).__init__()# 定义Critic网络结构self.network = nn.Sequential(nn.Linear(state_dim, 64), # 输入层到隐藏层nn.Tanh(), # 激活函数nn.Linear(64, 1) # 隐藏层到输出层,输出状态价值)def forward(self, state):# 前向传播,返回状态价值return self.network(state)class PPO:def __init__(self, state_dim, action_dim, config):self.config = configself.actor = Actor(state_dim, action_dim) # 初始化Actor网络self.critic = Critic(state_dim) # 初始化Critic网络self.actor_optimizer = optim.Adam(self.actor.parameters(), lr=config.lr) # Actor优化器self.critic_optimizer = optim.Adam(self.critic.parameters(), lr=config.lr) # Critic优化器def get_action(self, state):with torch.no_grad():state = torch.FloatTensor(state) # 转换状态为张量action_probs = self.actor(state) # 获取动作概率分布dist = Categorical(action_probs) # 创建类别分布action = dist.sample() # 采样动作action_log_prob = dist.log_prob(action) # 获取动作的对数概率return action.item(), action_log_prob.item() # 返回动作和对数概率def compute_gae(self, values, rewards, dones, next_value):advantages = []gae = 0for t in reversed(range(len(rewards))):if t == len(rewards) - 1:next_val = next_value # 最后一步的下一个值else:next_val = values[t + 1] # 其他步骤的下一个值delta = rewards[t] + self.config.gamma * next_val * (1 - dones[t]) - values[t] # TD误差gae = delta + self.config.gamma * self.config.gae_lambda * (1 - dones[t]) * gae # 计算GAEadvantages.insert(0, gae) # 插入到优势列表开头advantages = torch.FloatTensor(advantages) # 转换为张量return advantagesdef update(self, states, actions, old_log_probs, rewards, dones, next_state):states = torch.FloatTensor(np.array(states)) # 转换状态为张量actions = torch.LongTensor(actions) # 转换动作为张量old_log_probs = torch.FloatTensor(old_log_probs) # 转换旧的对数概率为张量# 计算GAEwith torch.no_grad():values = self.critic(states).squeeze() # 获取当前状态的价值next_value = self.critic(torch.FloatTensor(next_state)).item() # 获取下一个状态的价值advantages = self.compute_gae(values.tolist(), rewards, dones, next_value) # 计算优势returns = advantages + values # 计算回报,即优势+状态价值,因为advantages是基于TD误差的是相对值,所以需要加上状态价值得到绝对值,来稳定训练advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8) # 标准化优势# PPO更新多轮for _ in range(self.config.ppo_epochs):for idx in range(0, len(states), self.config.batch_size):batch_states = states[idx:idx + self.config.batch_size] # 获取小批量状态batch_actions = actions[idx:idx + self.config.batch_size] # 获取小批量动作batch_old_log_probs = old_log_probs[idx:idx + self.config.batch_size] # 获取小批量旧的对数概率batch_advantages = advantages[idx:idx + self.config.batch_size] # 获取小批量优势batch_returns = returns[idx:idx + self.config.batch_size] # 获取小批量回报# 计算新的动作概率action_probs = self.actor(batch_states)dist = Categorical(action_probs)new_log_probs = dist.log_prob(batch_actions)# 计算熵entropy = dist.entropy().mean()# 计算比率和裁剪后的目标ratio = torch.exp(new_log_probs - batch_old_log_probs)surr1 = ratio * batch_advantagessurr2 = torch.clamp(ratio, 1-self.config.clip_epsilon, 1+self.config.clip_epsilon) * batch_advantages# 计算Actor损失(加入熵正则化项)actor_loss = -torch.min(surr1, surr2).mean() - self.config.entropy_coef * entropy# 计算Critic损失critic_values = self.critic(batch_states).view(-1) # 使用 view(-1) 替代 squeeze()critic_loss = nn.MSELoss()(critic_values, batch_returns)# 更新Actor网络self.actor_optimizer.zero_grad()actor_loss.backward()self.actor_optimizer.step()# 更新Critic网络self.critic_optimizer.zero_grad()critic_loss.backward()self.critic_optimizer.step()def train_ppo():config = Config() # 初始化配置env = gym.make("CartPole-v1") # 创建环境state_dim = env.observation_space.shape[0] # 获取状态维度action_dim = env.action_space.n # 获取动作维度agent = PPO(state_dim, action_dim, config) # 初始化PPO智能体rewards = [] # 用于存储每个回合的奖励for episode in range(config.max_episodes):state, _ = env.reset() # 重置环境states, actions, rewards_buffer, log_probs, dones = [], [], [], [], [] # 初始化存储变量total_reward = 0 # 初始化总奖励for step in range(config.max_steps):action, log_prob = agent.get_action(state) # 获取动作和对数概率next_state, reward, done, _, _ = env.step(action) # 执行动作states.append(state) # 存储状态actions.append(action) # 存储动作rewards_buffer.append(reward) # 存储奖励log_probs.append(log_prob) # 存储对数概率dones.append(done) # 存储是否结束total_reward += reward # 累加奖励state = next_state # 更新状态if done:breakagent.update(states, actions, log_probs, rewards_buffer, dones, next_state) # 更新智能体rewards.append(total_reward) # 记录每个回合的奖励print(f"Episode {episode + 1}, Total Reward: {total_reward}") # 打印当前回合的奖励if total_reward >= 500:print("Solved!") # 如果总奖励达到500,表示问题解决breakenv.close() # 关闭环境# 绘制reward曲线plt.plot(rewards)plt.title('Training Rewards')plt.xlabel('Episode')plt.ylabel('Reward')plt.show()if __name__ == "__main__":train_ppo()
结果:

可以看到,两百次采样以内就能达到目标,获得奖励(500)