Proximal Policy Optimization(PPO)算法原理及实现!

浏览: 7658

这两天看了一下李宏毅老师的强化学习课程的前两讲,主要介绍了Policy Gradient算法和Proximal Policy Optimization算法,在此整理总结一下。

视频地址:https://www.bilibili.com/video/av24724071/?p=4

1、PG算法回顾

在PG算法中,我们的Agent又被称为Actor,Actor对于一个特定的任务,都有自己的一个策略π,策略π通常用一个神经网络表示,其参数为θ。从一个特定的状态state出发,一直到任务的结束,被称为一个完整的eposide,在每一步,我们都能获得一个奖励r,一个完整的任务所获得的最终奖励被称为R。这样,一个有T个时刻的eposide,Actor不断与环境交互,形成如下的序列τ:

这样一个序列τ是不确定的,因为Actor在不同state下所采取的action可能是不同的,一个序列τ发生的概率为:

序列τ所获得的奖励为每个阶段所得到的奖励的和,称为R(τ)。因此,在Actor的策略为π的情况下,所能获得的期望奖励为:

而我们的期望是调整Actor的策略π,使得期望奖励最大化,于是我们有了策略梯度的方法,既然我们的期望函数已经有了,我们只要使用梯度提升的方法更新我们的网络参数θ(即更新策略π)就好了,所以问题的重点变为了求参数的梯度。梯度的求解过程如下:

上面的过程中,我们首先利用log函数求导的特点进行转化,随后用N次采样的平均值来近似期望,最后,我们将pθ展开,将与θ无关的项去掉,即得到了最终的结果。

所以,一个PG方法的完整过程如下:

我们首先采集数据,然后基于前面得到的梯度提升的式子更新参数,随后再根据更新后的策略在采集数据,再更新参数,如此循环进行。注意到图中的大红字only used once,因为在更新参数后,我们的策略已经变了,而先前的数据是基于更新参数前的策略得到的。

接下来讲两个PG方法的小tip:

增加一个基线
通过上面的介绍你可能发现了,PG方法在更新策略时,基本思想就是增加reward大的动作出现的概率,减小reward小的策略出现的概率。假设现在有一种情况,我们的reward在无论何时都是正的,对于没有采样到的动作,它的reward是0。因此,如果一个比较好的动作没有被采样到,而采样到的不好的动作得到了一个比较小的正reward,那么没有被采样到的好动作的出现概率会越来越小,这显然是不合适的,因此我们需要增加一个奖励的基线,让reward有正有负。
一般增加的基线是所获得奖励的平均值:

增加折扣因子
这个很容易理解,就像买股票一样,未来的1块钱的价值要小于当前1块钱的价值,因此未来的1块钱变成现在的价值,需要进行一定的折扣

使用优势函数

我们之前介绍的PG方法,对于同一个eposide中的所有数据,使用的奖励都是一样的,其实我们可以将其变为与st和at相关的。这里我们使用的是优势函数,即Qπ(st,at) - Vπ(st)。其中Qπ(st,at)可以使用从当前状态开始到eposide结束的奖励折现和,Vπ(st)可以通过一个critic来计算得到。

2、PPO算法原理简介

接着上面的讲,PG方法一个很大的缺点就是参数更新慢,因为我们每更新一次参数都需要进行重新的采样,这其实是中on-policy的策略,即我们想要训练的agent和与环境进行交互的agent是同一个agent;与之对应的就是off-policy的策略,即想要训练的agent和与环境进行交互的agent不是同一个agent,简单来说,就是拿别人的经验来训练自己。举个下棋的例子,如果你是通过自己下棋来不断提升自己的棋艺,那么就是on-policy的,如果是通过看别人下棋来提升自己,那么就是off-policy的:

那么为了提升我们的训练速度,让采样到的数据可以重复使用,我们可以将on-policy的方式转换为off-policy的方式。即我们的训练数据通过另一个Actor(对应的网络参数为θ'得到。这要怎么做呢?通过下面的思路:

通过这种方式,我们的p(x)和q(x)的分布不能差别太大,否则需要进行非常多次的采样,才能得到近似的结果:

如上图所示,很显然,在x服从p(x)分布时,f(x)的期望为负,此时我们从q(x)中来采样少数的x,那么我们采样到的x很有可能都分布在右半部分,此时f(x)大于0,我们很容易得到f(x)的期望为正的结论,这就会出现问题,因此需要进行大量的采样。

那么此时我们想要期望奖励最大化,则变为:

则梯度变为:

最后一项因为我们假设两个分布不能差太远,所以认为他们是相等的,为了求解方便,我们直接划掉。此时似然函数变为:

由梯度变为似然函数,使用的还是下面式子,大家可以自己手动算一下:

到这里,我们马上就要得到我们的PPO算法了,再坚持一下!

我们前面介绍了,我们希望θ和θ'不能差太远,这并不是说参数的值不能差太多,而是说,输入同样的state,网络得到的动作的概率分布不能差太远。得到动作的概率分布的相似程度,我们可以用KL散度来计算,将其加入PPO模型的似然函数中,变为:

在实际中,我们会动态改变对θ和θ'分布差异的惩罚,如果KL散度值太大,我们增加这一部分惩罚,如果小到一定值,我们就减小这一部分的惩罚,基于此,我们得到了PPO算法的过程:

PPO算法还有另一种实现方式,不将KL散度直接放入似然函数中,而是进行一定程度的裁剪:

上图中,绿色的线代表min中的第一项,即不做任何处理,蓝色的线为第二项,如果两个分布差距太大,则进行一定程度的裁剪。最后对这两项再取min,防止了θ更新太快。

3、PPO算法Tensorflow实现

本文的代码地址为:https://github.com/princewen/tensorflow_practice/tree/master/RL/Basic-PPO-Demo

参考的是莫烦老师的代码:https://github.com/MorvanZhou/Reinforcement-learning-with-tensorflow/blob/master/contents/12_Proximal_Policy_Optimization/simply_PPO.py

本文使用的是gym的强化学习环境,用的是钟摆垂直的这么一个环境,我们希望如下图所示的钟摆能够垂直:

关于本游戏的state,action以及reward,可以参考文献:https://blog.csdn.net/u013745804/article/details/78397106。状态是钟摆的角度以及角速度;动作是钟摆的控制力矩,动作维度只有一个维度。

好了,接下来介绍我们的实现代码:

创建环境

import gym
env = gym.make('Pendulum-v0').unwrapped

得到state、action、reward

这里我们首先得到state,action和即时奖励reward的序列,随后我们要计算折现的奖励和,get_v()是一个得到状态价值的函数。使用v(s) = r + gamma * v(s+1)不断进行循环。

for t in range(EP_LEN):    # in one episode
env.render()
a = ppo.choose_action(s) # 根据一个正态分布,选择一个action
s_, r, done, _ = env.step(a)
buffer_s.append(s)
buffer_a.append(a)
buffer_r.append((r+8)/8) # normalize reward, find to be useful
s = s_
ep_r += r

# update ppo
if (t+1) % BATCH == 0 or t == EP_LEN-1:
v_s_ = ppo.get_v(s_)
discounted_r = []
for r in buffer_r[::-1]:
v_s_ = r + GAMMA * v_s_
discounted_r.append(v_s_) # v(s) = r + gamma * v(s+1)
discounted_r.reverse()

bs, ba, br = np.vstack(buffer_s), np.vstack(buffer_a), np.array(discounted_r)[:, np.newaxis]
buffer_s, buffer_a, buffer_r = [], [], []
ppo.update(bs, ba, br)

定义critic计算优势函数

这里我们定义了一个critic来计算优势函数,状态价值定义了一个全联接神经网络来得到,而折扣奖励和我们之前已经计算过了:

#critic
with tf.variable_scope('critic'):
l1 = tf.layers.dense(self.tfs,100,tf.nn.relu)
self.v = tf.layers.dense(l1,1) # state-value
self.tfdc_r = tf.placeholder(tf.float32,[None,1],'discounted_r')
self.advantage = self.tfdc_r - self.v
self.closs = tf.reduce_mean(tf.square(self.advantage))
self.ctrain_op = tf.train.AdamOptimizer(C_LR).minimize(self.closs)

定义Actor

PPO里采用的是off-policy的策略,需要有一个单独的网络来收集数据,并用于策略的更新,同DQN的策略一样,我们定义了一个单独的网络,这个网络的参数是每隔一段时间由我们真正的Actor的参数复制过去的。

#actor
pi,pi_params = self._build_anet('pi',trainable=True)
oldpi,oldpi_params = self._build_anet('oldpi',trainable=False)
with tf.variable_scope('sample_action'):
self.sample_op = tf.squeeze(pi.sample(1),axis=0)
with tf.variable_scope('update_oldpi'):
self.update_oldpi_op = [oldp.assign(p) for p,oldp in zip(pi_params,oldpi_params)]

self.tfa = tf.placeholder(tf.float32,[None,A_DIM],'action')
self.tfadv = tf.placeholder(tf.float32,[None,1],'advantage')

而网络构建的代码如下,这里就比较神奇了,我们的Actor网络输出一个均值和方差,并返回一个由该均值和方差得到的正态分布,动作基于此正态分布进行采样:

def _build_anet(self,name,trainable):
with tf.variable_scope(name):
l1 = tf.layers.dense(self.tfs,100,tf.nn.relu,trainable=trainable)
mu = 2 * tf.layers.dense(l1,A_DIM,tf.nn.tanh,trainable=trainable)
sigma = tf.layers.dense(l1,A_DIM,tf.nn.softplus,trainable=trainable)
norm_dist = tf.distributions.Normal(loc=mu,scale=sigma) # 一个正态分布
params = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES,scope=name)
return norm_dist,params

损失计算

采用KL散度或者裁切的方式,损失计算方式不同:

with tf.variable_scope('loss'):
with tf.variable_scope('surrogate'):
ratio = pi.prob(self.tfa)/oldpi.prob(self.tfa)
surr = ratio * self.tfadv

if METHOD['name'] == 'kl_pen':
self.tflam = tf.placeholder(tf.float32,None,'lambda')
kl = tf.distributions.kl_divergence(oldpi,pi)
self.kl_mean = tf.reduce_mean(kl)
self.aloss = -tf.reduce_mean(surr-self.tflam * kl)

else:
self.aloss = -tf.reduce_mean(tf.minimum(surr,tf.clip_by_value(ratio,1.-METHOD['epsilon'],1.+METHOD['epsilon'])*self.tfadv))

好了,剩下的代码也很简单,相信大家一定能看懂啦,再夸赞一句,李宏毅老师讲的真棒!莫烦老师的代码简单易懂!

推荐 0
本文由 石晓文 创作,采用 知识共享署名-相同方式共享 3.0 中国大陆许可协议 进行许可。
转载、引用前需联系作者,并署名作者且注明文章出处。
本站文章版权归原作者及原出处所有 。内容为作者个人观点, 并不代表本站赞同其观点和对其真实性负责。本站是一个个人学习交流的平台,并不用于任何商业目的,如果有任何问题,请及时联系我们,我们将根据著作权人的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。

0 个评论

要回复文章请先登录注册