算法实战篇(二),Tensorflow实现Actor-Critic框架下的经典PPO算法

本篇是我们算法实战的第二篇,针对的是我们在“基础算法篇(六),基于AC框架的PPO算法”中提出的相关算法,具体算法中部分参考了莫烦老师的相关代码,在这里向莫烦老师表示感谢。

Tensorflow实现Actor-Critic框架下的经典PPO算法

  • 一、基础游戏背景介绍
  • 二、主函数
  • 三、Agent类
    • (一)PPO类的初始化函数
    • (二)建立Critic深度神经网络(价值网络)
      • 1. 价值网络的建立
      • 2. 更新方法的定义
    • (三)建立Actor深度神经网络(策略网络)
      • 1. 策略网络的建立
      • 2. 更新方法的定义
    • (四)利用策略网络(Actor)生成行为
    • (五)存储与更新数据
    • (六)利用数据更新策略网络(Actor)和价值网络(Critic)
  • 总结

一、基础游戏背景介绍

在这次代码实现中,为了体现与前面离散型输出的不同,我们特意选择了Gym中的转动杆游戏,如下图:
算法实战篇(二),Tensorflow实现Actor-Critic框架下的经典PPO算法_第1张图片

游戏的目标就是给转动杆一个力,最终让它能够稳定的立起来。这个游戏与DQN中使用的“活动杆小车”游戏不同的是,这个游戏的输入action_space是一个Box(1,)类型,即一个float的连续值。同时,这个游戏的输出observation_space是一个Box(3,)类型,是三个float的数组。
其他关于编程环境的搭建,请大家参考“番外篇,强化学习基础环境搭建”
下面我们正式进入我们的程序。

二、主函数

同样的,让我们先看main()函数:

def main():
    # first, create the envrioment of 'Pendulum-v0'
    # the game Pendulum-v0's observation_space is Box(3,), it means the observation has three var and each one is float
    # the action_space is Box(1,), it means the action has one var and is float
    # the output of the game is continuous, not discrete
    env = gym.make(ENV_NAME).unwrapped
    # second, create the PPO agent. it is based the AC arch, so it has two type of network
    # the critic which give the value of the state
    # the actor which give the action
    agent = PPO(env.observation_space, env.action_space)
    for episode in range(EPISODES):
        # every episode reset the memory
        agent.resetMemory()
        # get the initial state
        state = env.reset()
        # this is the total reward
        ep_r = 0
        for step in range(STEPS):
            # show the game window
            env.render()
            # output the action
            action = agent.Choose_Action(state)
            # process the action and get the info
            next_state, reward, done, info = env.step(action)
            # store the date to update the model
            agent.Store_Data(state, action, reward, next_state, done)
            # train the agent every BATCH_SIZE
            if (step + 1) % BATCH_SIZE == 0 or step == STEPS - 1:
                agent.Train(next_state)
            # set the new state
            state = next_state
            # record the reward
            ep_r += reward
            if step == STEPS - 1:
                agent.UpdateActorParameters()
        # caculate the total reward in every episode
        if episode == 0:
            all_ep_r.append(ep_r)
        else:
            all_ep_r.append(all_ep_r[-1] * 0.9 + ep_r * 0.1)
        print(
            'Ep: %i' % episode,
            "|Ep_r: %i" % ep_r,
            ("|Lam: %.4f" % METHOD['lam']) if METHOD['name'] == 'kl_pen' else '',
        )

上述主函数主要分为以下几步(针对每一行代码):

  • 1.生成环境对象env
  • 2.生成PPO对象agent
  • 3.开始每一幕(Episode)循环
  • 4.首先重置agent存储的数据,将上一幕存储的数据清零
  • 5.得到环境的初始状态
  • 6.设置记录这一幕reward的变量
  • 7.开始这一幕中每一步(Step)的循环
  • 8.首先显示游戏窗口
  • 9.基于状态state,利用agent生成行动action
  • 10.将action输入环境,得到下一个状态next_state和实时收益reward等数据
  • 11.存储相关数据
  • 12.如果满足训练条件,则进行agent的训练,用梯度下降更新相关Critic和Actor网络参数
  • 13.将最新得到的下一个状态,改为现有状态
  • 14.判断是否是最后一个step,如果是,则执行两个actor参数的拷贝更新,否则继续执行步循环,直到走完设计的步数
  • 15.后续的代码是记录每个Episode的总得分,并将其存储和打印出来

我们可以看出,从主函数角度来讲,与DQN的函数基本一致,区别在于这里是每个Episode直接打印结果。
下面,我们来详细介绍基于Actor-Critic框架的PPO类。

三、Agent类

在“基础算法篇(六),基于AC框架的PPO算法”中我们介绍了相关算法,这里我们是严格按照算法进行的实现,其中网络构建了两类三个:

  • 价值网络(Critic),一个网络,输入是环境的状态,输出是这个状态的价值;
  • 策略网络(Actor),两个网络,一个用来与环境交互,另一个进行参数更新,这种方式主要用来解决经典PG算法中的“采集数据不能够重复使用的问题”。

下面我们详细介绍相关代码实现部分。

(一)PPO类的初始化函数

PPO类的初始化主要是导入观测空间和行动空间的大小,并根据这两个量生成相应的策略和价值网络,具体代码如下:

def __init__(self, observation_space, action_space):
    # the state is the input vector of network, in the game of 'Pendulum-v0', it has three dimensions
    self.state_dim = observation_space.shape[0]
    # the action is the output vector and  in the game of 'Pendulum-v0', it has one dimensions
    self.action_dim = action_space.shape[0]
    # it is the input, which come from the env
    self.state_input = tf.placeholder(tf.float32, [None, self.state_dim], 'state')
    # create the network to represent the state value
    self.Create_Critic()
    # create two networks to output the action, and update the networks
    self.Create_Actor_with_two_network()
    # Init session
    self.sess = tf.Session()
    self.sess.run(tf.global_variables_initializer())

具体每一步的含义,我在代码中做了注释。我们在这里可以看到,创建价值网络和创建策略网络使用了两个独立的函数,下面我们具体来讲各个函数情况。

(二)建立Critic深度神经网络(价值网络)

我们在“基础算法篇(六),基于AC框架的PPO算法”第一节中介绍了,Actor-Critic框架是通过在策略梯度中引入价值函数,实现两个网络的结合。因此,价值网络主要是对状态价值的评估,同时它也需要在不断的训练中通过梯度下降来更新参数,下面我们详细介绍两部分的代码。

1. 价值网络的建立

我们在这里建立的价值网络,输入是游戏的状态,输出是状态的价值,中间包含两个隐藏层,具体代码如下:

# first, create the parameters of networks
W1 = self.weight_variable([self.state_dim, 100])
b1 = self.bias_variable([100])
W2 = self.weight_variable([100, 50])
b2 = self.bias_variable([50])
W3 = self.weight_variable([50, self.action_dim])
b3 = self.bias_variable([self.action_dim])
# second, create the network with two hidden layers
# hidden layer one
h_layer_one = tf.nn.relu(tf.matmul(self.state_input, W1) + b1)
# hidden layer two
h_layer_two = tf.nn.relu(tf.matmul(h_layer_one, W2) + b2)
# the output of current_net
self.v = tf.matmul(h_layer_two, W3) + b3

其中self.state_dim为输入状态,self.v即为价值网络的输出。

2. 更新方法的定义

对于价值网络的更新,我们在这里使用优势函数作为其损失,具体代码如下:

# the input of discounted reward
self.tfdc_r = tf.placeholder(tf.float32, [None, 1], 'discounted_r')
# the advantage value, use to update the critic network
self.advantage = self.tfdc_r - self.v
# the loss of the network
self.closs = tf.reduce_mean(tf.square(self.advantage))
# the training method of critic
self.ctrain_op = tf.train.AdamOptimizer(Critic_LR).minimize(self.closs)

其中self.tfdc_r为我们后面需要计算的折扣收益,self.ctrain_op为Critic网络的更新操作,在后续进行更新时,输入相关参数,调用这一操作即可。

(三)建立Actor深度神经网络(策略网络)

我们在“基础算法篇(六),基于AC框架的PPO算法”中介绍了,为了解决原有策略梯度(PG)算法中数据不能够重复利用的问题,我们使用了Importance Sampling的思路,即利用一个网络与环境进行交互,而另外一个网络负责进行更新,在这里我们就实现了pi和oldpi两个网络,并定义了相关的更新策略。

1. 策略网络的建立

我们建立的pi和oldpi两个网络中,其中oldpi负责与环境进行交互,pi负责进行参数更新,具体代码如下:

# create the actor that update the parameters
pi, pi_params = self.build_actor_net('pi', trainable=True)
# create the actor that interact with env
oldpi, oldpi_params = self.build_actor_net('oldpi', trainable=False)
# sample the action from the distribution
with tf.variable_scope('sample_action'):
     self.sample_from_oldpi = tf.squeeze(oldpi.sample(1), axis=0)

这里构建网络的具体方法,我们借鉴了莫烦老师的代码,也是构建了包含两个隐藏层的网络,具体代码如下:

# the function that create the actor network
# it has two hidden layers
# the method of creating actor is different  from the critic
# the output of network is a distribution
def build_actor_net(self, name, trainable):
    with tf.variable_scope(name):
         l1 = tf.layers.dense(self.state_input, 100, tf.nn.relu, trainable=trainable)
         l2 = tf.layers.dense(l1, 50, tf.nn.relu, trainable=trainable)
         mu = 2 * tf.layers.dense(l2, self.action_dim, tf.nn.tanh, trainable=trainable)
         sigma = tf.layers.dense(l2, self.action_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

由这里可以看出,这个网络与DQN中离散输出不同的是,这个网络输出的其实是一个分布norm_dist(其中mu是这个分布的均值,sigma为这个分布的方差),因此最终的网络输出则是从这个分布中随机sample一个值即可。

2. 更新方法的定义

根据上面的代码我们可以看出,pi是可以更新的,具体的更新逻辑我们这里也借鉴了莫烦老师的思路,做了PPO和PPO2两种实现,代码如下:

# the actions in memory
self.tfa = tf.placeholder(tf.float32, [None, self.action_dim], 'action')
# the advantage value
self.tfadv = tf.placeholder(tf.float32, [None, 1], 'advantage')
with tf.variable_scope('loss'):
     with tf.variable_scope('surrogate'):
          # the ration between the pi and oldpi, this is importance sampling part
          ratio = pi.prob(self.tfa) / (oldpi.prob(self.tfa) + 1e-5)
          # the surrogate
          surr = ratio * self.tfadv
     # this is the method of PPO
     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:  # this is the method of PPO2
         self.aloss = -tf.reduce_mean(tf.minimum(
              surr,
              tf.clip_by_value(ratio, 1. - METHOD['epsilon'], 1. + METHOD['epsilon']) * self.tfadv)
              )
# define the method of training actor
with tf.variable_scope('atrain'):
     self.atrain_op = tf.train.AdamOptimizer(Actor_LR).minimize(self.aloss)

上面的代码中,其中self.tfa为输入的行动序列,self.tfadv为输入的优势函数,在具体的损失计算中,首先按照Importance Sampling的思路计算出相关目标函数,然后再判断是使用PPO算法还是使用PPO2算法,最终形成用来更新的损失函数self.aloss
最后,定义了Actor的更新操作self.atrain_op,这个与Critic一样,在更新时输入参数,然后调用即可。

(四)利用策略网络(Actor)生成行为

前面讲了,利用策略网络网络生成行为,就是利用self.sample_from_oldpi操作,从oldpi网络的输出分布中sample一个值出来即可,具体代码如下:

# output the action with state, the output is from oldpi
def Choose_Action(self, s):
    s = s[np.newaxis, :]
    a = self.sess.run(self.sample_from_oldpi, {self.state_input: s})[0]
    return np.clip(a, -2, 2)

这里将输出值限定在 ( − 2 , 2 ) (-2,2) (2,2)的范围内。

(五)存储与更新数据

我们在这里使用的存储策略在每个Episode开始时初始化存储列表,然后在Episode的每个Step都存储数据,具体代码如下:

 # reset the memory in every episode
def resetMemory(self):
    self.buffer_s, self.buffer_a, self.buffer_r = [], [], []

# store the data of every steps
def Store_Data(self, state, action, reward, next_state, done):
    self.buffer_s.append(state)
    self.buffer_a.append(action)
    self.buffer_r.append(reward)

上面两个函数,是在主函数main()中相关位置进行调用的。

(六)利用数据更新策略网络(Actor)和价值网络(Critic)

最后,我们讲一下两个网络的更新操作,我们的策略是每隔BATCH_SIZE步进行一次更新,具体的更新代码如下:

# the train function that update the network
def Train(self, next_state):
    # caculate the discount reward
    v_s_ = self.get_v(next_state)
    discounted_r = []
    for r in self.buffer_r[::-1]:
        v_s_ = r + GAMMA * v_s_
        discounted_r.append(v_s_)
    discounted_r.reverse()
    bs, ba, br = np.vstack(self.buffer_s), np.vstack(self.buffer_a), np.array(discounted_r)[:, np.newaxis]
    # this the main function of update
    self.update(bs, ba, br)

上面是更新的主函数,在这里我们首先利用价值网络获得下一状态的价值,然后计算每一步的折扣收益,然后将相关数据输入update函数,进行更新,update函数代码如下:

# the function that update the actor and critic
def update(self, s, a, r):
    adv = self.sess.run(self.advantage, {self.state_input: s, self.tfdc_r: r})
    # update actor
    if METHOD['name'] == 'kl_pen':
       for _ in range(ACTOR_UPDATE_TIMES):
           _, kl = self.sess.run(
                [self.atrain_op, self.kl_mean],
                {self.state_input: s, self.tfa: a, self.tfadv: adv, self.tflam: METHOD['lam']})
           if kl > 4 * METHOD['kl_target']:  # this in in google's paper
                break
       if kl < METHOD['kl_target'] / 1.5:  # adaptive lambda, this is in OpenAI's paper
            METHOD['lam'] /= 2
       elif kl > METHOD['kl_target'] * 1.5:
            METHOD['lam'] *= 2
       METHOD['lam'] = np.clip(METHOD['lam'], 1e-4, 10)  # sometimes explode, this clipping is my solution
    else:  # clipping method, find this is better (OpenAI's paper)
       [self.sess.run(self.atrain_op, {self.state_input: s, self.tfa: a, self.tfadv: adv}) for _ in range(ACTOR_UPDATE_TIMES)]
    # update critic
    [self.sess.run(self.ctrain_op, {self.state_input: s, self.tfdc_r: r}) for _ in range(CRITIC_UPDATE_TIMES)]

上面的代码中,首先计算优势函数,然后判断是PPO还是PPO2,如果是PPO,还要对KL散度的参数进行计算,之后再调用self.atrain_op进行策略网络pi的更新,最后再调用self.ctrain_op进行价值网络的更新。
最后还要说明一点的是,我们这里设计的是每个Episode的最后一步,将pi网络的参数更新到oldpi网络上,具体代码如下:

# ths dunction the copy the pi's parameters to oldpi
def UpdateActorParameters(self):
    self.sess.run(self.update_oldpi_from_pi)

总结

本篇介绍了Actor-Critic框架下的经典PPO算法相关代码实现部分,如果大家感兴趣,完整的代码可以从我的Github中下载。

你可能感兴趣的:(强化学习笔记,强化学习,人工智能)