本篇是我们算法实战的第二篇,针对的是我们在“基础算法篇(六),基于AC框架的PPO算法”中提出的相关算法,具体算法中部分参考了莫烦老师的相关代码,在这里向莫烦老师表示感谢。
在这次代码实现中,为了体现与前面离散型输出的不同,我们特意选择了Gym中的转动杆游戏,如下图:
游戏的目标就是给转动杆一个力,最终让它能够稳定的立起来。这个游戏与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 '',
)
上述主函数主要分为以下几步(针对每一行代码):
我们可以看出,从主函数角度来讲,与DQN的函数基本一致,区别在于这里是每个Episode直接打印结果。
下面,我们来详细介绍基于Actor-Critic框架的PPO类。
在“基础算法篇(六),基于AC框架的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())
具体每一步的含义,我在代码中做了注释。我们在这里可以看到,创建价值网络和创建策略网络使用了两个独立的函数,下面我们具体来讲各个函数情况。
我们在“基础算法篇(六),基于AC框架的PPO算法”第一节中介绍了,Actor-Critic框架是通过在策略梯度中引入价值函数,实现两个网络的结合。因此,价值网络主要是对状态价值的评估,同时它也需要在不断的训练中通过梯度下降来更新参数,下面我们详细介绍两部分的代码。
我们在这里建立的价值网络,输入是游戏的状态,输出是状态的价值,中间包含两个隐藏层,具体代码如下:
# 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即为价值网络的输出。
对于价值网络的更新,我们在这里使用优势函数作为其损失,具体代码如下:
# 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网络的更新操作,在后续进行更新时,输入相关参数,调用这一操作即可。
我们在“基础算法篇(六),基于AC框架的PPO算法”中介绍了,为了解决原有策略梯度(PG)算法中数据不能够重复利用的问题,我们使用了Importance Sampling的思路,即利用一个网络与环境进行交互,而另外一个网络负责进行更新,在这里我们就实现了pi和oldpi两个网络,并定义了相关的更新策略。
我们建立的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一个值即可。
根据上面的代码我们可以看出,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一样,在更新时输入参数,然后调用即可。
前面讲了,利用策略网络网络生成行为,就是利用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()中相关位置进行调用的。
最后,我们讲一下两个网络的更新操作,我们的策略是每隔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中下载。