重拾基础  - PPO

重拾基础 - PPO

Proximal Policy Optimization (PPO)

背景

Proximal Policy Optimization,简称PPO,即近端策略优化,是对Policy Graident,即策略梯度的一种改进算法。PPO的核心精神在于,通过一种被称之为Importce Sampling的方法,将Policy Gradient中On-policy的训练过程转化为Off-policy,即从在线学习转化为离线学习,某种意义上与基于值迭代算法中的Experience Replay有异曲同工之处。通过这个改进,训练速度与效果在实验上相较于Policy Gradient具有明显提升。


Policy Gradient

Policy Gradient是一种基于策略迭代的强化学习算法,不同于基于值迭代的DQN、Double-DQN、Duling-DQN通过间接地估计动作-状态值函数来学习的过程,Policy Gradient直接地通过采样状态、动作、奖励,然后期望直接最大化奖励的期望。PPO与PG都希望最大化奖励的期望,当采样足够充分时,奖励的期望可以近似为N回合的奖励的平均值:

\bar{R}{\theta} = \sum_{\tau} R(\tau) P(\tau \lvert \theta) \approx \frac{1}{N} \sum^{N}_{n=1} R(\tau^{n})

上式中的第n回合的奖励值之和 R(\tau^n) 被定义为如下形式:

R(\tau) = \sum^{T}_{t=1} r_t

在前篇专门介绍Policy Gradient文章中,已经详细地推导了关于 \nabla \bar{R}_{\theta} 的计算方法,所以在这里的具体推导过程将略过,最后关于其计算公式将有如下形式:

\nabla \bar{R}_{\theta} = \frac{1}{N} \sum^{N}_{n=1} \sum^{T_n}_{t=1} R(\tau^n) \nabla \log p(a_t \lvert s_t, \theta)

本质上是最小化N回合采样出的动作与网络输出的动作的交叉熵的基础上乘以 R(\tau^n) ,奖励值给了梯度下降的方向,推导出了 \nabla \bar{R}_{\theta} ,其实就已经可以根据梯度下降法反向传播改进网络进行训练了,但是通常情况下我们会根据具体的问题对其做一些修正。


Actor-Critic Model

R(\tau^n) 的修正通常情况下是必须的,也是有意义的,符合直觉的。以CartPole-v0与MountainCar-v0,即小车倒立杆和过山车游戏为例,每一个状态采取的动作对整个回合的奖励和是不同的,对于小车倒立杆问题而言,初始的几个状态采取的动作直接决定了杆是否会很快地倒,所以直觉地他们更加重要,而对于过山车问题而言,在小车即将爬上山时的这些状态采取的动作直接决定了小车能不能爬上山,所以直觉地他们更加重要。

这将引入我们的第一个改进,对于小车倒立杆问题而言,我们需要针对每一个状态、动作元组对 R(\tau^n) 进行如下替换:

R(\tau^n) \rightarrow \sum^{T_n}_{t=t^{\prime}} \gamma^{t} r^{n}_{t}

这样原来的梯度公式将会被改写为以下形式:

\nabla \bar{R}_{\theta} = \frac{1}{N} \sum^{N}_{n=1} \sum^{T_n}_{t=1} \sum^{T_n}_{t=t^{\prime}} \gamma^{t} r^{n}_{t} \nabla \log p(a_t \lvert s_t, \theta)

但是这样还存在一个称之为Overestimate,即过估计的问题。因为在实际情况中,我们的状态-动作采样通常是不充分的,这会导致一些一些动作或者状态几乎不会被采样,这样在进行梯度下降训练网络时,在这些状态对应的动作将可能被极大的放大或者缩小。由于输出层是soft-max,这些概率会此消彼长,这显然不是我们想看到的。所以我们需要做第二个改进:引入Baseline,通常可能是一个待调整的常超参数,或者Critic,通常是一个待训练的网络。

如果引入的是一个Critic,这样的模型将会被称之为Actor-Critic Model,即演员-评论家模型,而N回合平均奖励值的梯度将会被改写为以下形式:

\nabla \bar{R}_{\theta} = \frac{1}{N} \sum^{N}_{n=1} \sum^{T_n}_{t=1} A^{\theta}(a_t \lvert s_t) \nabla \log p(a_t \lvert s_t, \theta)

在一次训练过程中,我们会按顺序同时更新这两个网络,目前这样的模型已经被广泛使用,并在实验上证明了较好的效果。


Importance Sampling

在前面提到,PPO的一个核心改进是将Policy Gradient中On-policy的训练过程转化为Off-policy,即从在线学习转化为离线学习,这个转化过程被称之为Importance Sampling,是一种数学手段。如果我们有连续随机变量X,它的概率密度函数记作 p(x) ,则 f(x) 的期望通过如下公式计算:

E_{x \sim p} \left[ f(x) \right] = \int^{}_{} f(x)p(x)dx

若我们对于连续随机变量X,有另一个概率密度函数记作$q(x)$,那么他们将有以下关系:

E_{x \sim p} \left[ f(x) \right] = \int f(x) \cdot p(x)dx = \int f(x) \frac{p(x)}{q(x)} \cdot q(x) dx = E_{x \sim q} \left[ f(x) \frac{p(x)}{q(x)} \right]

在上式中最右边的项中, \frac{p(x)}{q(x)} 被称之为Importance Weight,类比到我们的问题, f(x)A^{\theta}(a_t \lvert s_t) ,而 \frac{p(x)}{q(x)} ,则是新老策略对于当前状态采取当前动作对应的概率之比,这句话比较费解,更加具体一些,对于小车倒立杆为例,动作是离散的,在网络的输出是一组离散的概率分布,以这个概率分布选择动作,这个动作在新老策略中,在当前状态中都对应了一个概率值, \frac{p(x)}{q(x)} 即是他们的比值。

通过这一操作,在采样充分的情况下,我们可以认为:

E_{x \sim p} \left[ f(x) \right] = E_{x \sim q} \left[ f(x) \frac{p(x)}{q(x)} \right]


Proximal Policy Optimization

最终我们将推导出PPO,Importance Sampling将给我们将On-policy的训练过程转化为Off-policy以基础,即我们可以通过老策略,即 q(x) 进行充分采样,然后改进新策略 p(x) ,这个过程可以在一回合重复N次,而不再是1次,这样大幅度减少了原始PG算法在线学习进行采样状态-动作-奖励元组对时间,同时保证了训练效果,而N回合平均奖励值的梯度也将被改写为以下形式:

\nabla \bar{R}_{\theta} = \frac{1}{N} \sum^{N}_{n=1} \sum^{T_n}_{t=1} \frac{p_{\theta}(a_t \lvert s_t)}{p_{\theta^{\prime}}(a_t \lvert s_t)} A^{\theta}(a_t \lvert s_t) \nabla \log p(a_t \lvert s_t, \theta)

在实际训练过程中,会有一个对 \frac{p_{\theta}(a_t \lvert s_t)}{p_{\theta^{\prime}}(a_t \lvert s_t)} 的clip的操作:

clip(\frac{p_{\theta}(a_t \lvert s_t)}{p_{\theta^{\prime}}(a_t \lvert s_t)}, 1 - \epsilon, 1 + \epsilon)

相当于一个正则化的操作,其中 \epsilon 是一个可调整的超参数,至此,PPO也就介绍完了。


Experiment


# coding=utf-8

import tensorflow as tf
import numpy as np
import gym
import sys

sys.path.append('..')

from base.model import BaseRLModel

class Agent(BaseRLModel):

    def __init__(self, session, env, a_space, s_space, **options):
        super(Agent, self).__init__(session, env, a_space, s_space, **options)

        self._init_input()
        self._init_nn()
        self._init_op()
        self._init_saver()

        self.a_buffer = []
        self.s_buffer = []
        self.r_buffer = []
        self.a_p_r_buffer = []

        self.session.run(tf.global_variables_initializer())

    def _init_input(self, *args):
        with tf.variable_scope('input'):
            self.s = tf.placeholder(tf.float32, [None, self.s_space], name='s')
            self.a = tf.placeholder(tf.int32, [None, ], name='a')
            self.r = tf.placeholder(tf.float32, [None, ], name='r')
            self.adv = tf.placeholder(tf.float32, [None, ], name='adv')
            self.a_p_r = tf.placeholder(tf.float32, [None, ], name='a_p_r')

    def _init_nn(self, *args):
        self.advantage, self.value = self._init_critic_net('critic_net')
        self.a_prob_eval, self.a_logits_eval = self._init_actor_net('eval_actor_net')
        self.a_prob_target, self.a_logits_target = self._init_actor_net('target_actor_net', trainable=False)

    def _init_op(self):
        with tf.variable_scope('critic_loss_func'):
            # loss func.
            self.c_loss_func = tf.losses.mean_squared_error(labels=self.r, predictions=self.value)
        with tf.variable_scope('critic_optimizer'):
            # critic optimizer.
            self.c_optimizer = tf.train.AdamOptimizer(self.learning_rate).minimize(self.c_loss_func)
        with tf.variable_scope('update_target_actor_net'):
            # Get eval w, b.
            params_e = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope='eval_actor_net')
            params_t = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope='target_actor_net')
            self.update_target_a_op = [tf.assign(t, e) for t, e in zip(params_t, params_e)]
        with tf.variable_scope('actor_loss_func'):
            # one hot a.
            a_one_hot = tf.one_hot(self.a, self.a_space)
            # cross entropy.
            cross_entropy = tf.nn.softmax_cross_entropy_with_logits_v2(labels=a_one_hot, logits=self.a_logits_eval)
            # loss func.
            self.a_loss_func = tf.reduce_mean(cross_entropy * self.adv * self.a_p_r)
        with tf.variable_scope('actor_optimizer'):
            self.a_optimizer = tf.train.AdamOptimizer(self.learning_rate).minimize(self.a_loss_func)

    def _init_actor_net(self, scope, trainable=True):
        with tf.variable_scope(scope):
            # Kernel initializer.
            w_initializer = tf.random_normal_initializer(0.0, 0.01)
            # First dense.
            f_dense = tf.layers.dense(self.s, 32, tf.nn.relu, trainable=trainable, kernel_initializer=w_initializer)
            # Second dense.
            s_dense = tf.layers.dense(f_dense, 32, tf.nn.relu, trainable=trainable, kernel_initializer=w_initializer)
            # Action logits.
            a_logits = tf.layers.dense(s_dense, self.a_space, trainable=trainable, kernel_initializer=w_initializer)
            # Action prob.
            a_prob = tf.nn.softmax(a_logits)
            return a_prob, a_logits

    def _init_critic_net(self, scope):
        with tf.variable_scope(scope):
            # Kernel initializer.
            w_initializer = tf.random_normal_initializer(0.0, 0.01)
            # First dense.
            f_dense = tf.layers.dense(self.s, 64, tf.nn.relu, kernel_initializer=w_initializer)
            # Value.
            value = tf.layers.dense(f_dense, 1)
            value = tf.reshape(value, [-1, ])
            # Advantage.
            advantage = self.r - value
            return advantage, value

    def predict(self, s):
        # Calculate a eval prob.
        a_prob_eval, a_prob_target = self.session.run([self.a_prob_eval, self.a_prob_target], {self.s: [s]})
        # Calculate action prob ratio between eval and target.
        a_p_r = np.max(a_prob_eval) / np.max(a_prob_target)
        self.a_p_r_buffer.append(a_p_r)
        return np.random.choice(range(a_prob_eval.shape[1]), p=a_prob_eval.ravel())

    def snapshot(self, s, a, r, _):
        self.a_buffer.append(a)
        self.s_buffer.append(s)
        self.r_buffer.append(r)

    def train(self):
        self.session.run(self.update_target_a_op)
        # Copy r_buffer
        r_buffer = self.r_buffer
        # Init r_tau
        r_tau = 0
        # Calculate r_tau
        for index in reversed(range(0, len(r_buffer))):
            r_tau = r_tau * self.gamma + r_buffer[index]
            self.r_buffer[index] = r_tau
        # Calculate adv.
        adv_buffer = self.session.run(self.advantage, {self.s: self.s_buffer, self.r: self.r_buffer})
        # Minimize loss.
        self.session.run([self.a_optimizer, self.c_optimizer], {
            self.adv: adv_buffer,
            self.s: self.s_buffer,
            self.a: self.a_buffer,
            self.r: self.r_buffer,
            self.a_p_r: self.a_p_r_buffer,
        })
        self.s_buffer = []
        self.a_buffer = []
        self.r_buffer = []
        self.a_p_r_buffer = []

    def run(self):
        if self.mode == 'train':
            for episode in range(self.train_episodes):
                s, r_episode = self.env.reset(), 0
                while True:
                    if episode > 200:
                        self.env.render()
                    a = self.predict(s)
                    s_n, r, done, _ = self.env.step(a)
                    if done:
                        r = -5
                    r_episode += r
                    self.snapshot(s, a, r, s_n)
                    s = s_n
                    if done:
                        break
                self.train()
                if episode % 25 == 0:
                    self.logger.warning('Episode: {} | Rewards: {}'.format(episode, r_episode))
                    self.save()
        else:
            for episode in range(self.eval_episodes):
                s, r_episode = self.env.reset()
                while True:
                    a = self.predict(s)
                    s_n, r, done, _ = self.env.step(a)
                    r_episode += r
                    s = s_n
                    if done:
                        break

Running

# Make env.
env = gym.make('CartPole-v0')
env.seed(1)
env = env.unwrapped
# Init session.
session = tf.Session()
# Init agent.
agent = Agent(session, env, env.action_space.n, env.observation_space.shape[0], **{
    'model_name': 'PolicyGradient',
})
agent.run()

结尾

目前观察,PPO在小车倒立杆问题上的收敛速度几倍于PG与一票基于值迭代的方法,让我非常惊讶。

编辑于 2018-08-11

文章被以下专栏收录