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

tech2025-04-29  8

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

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

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

一、基础游戏背景介绍

在这次代码实现中,为了体现与前面离散型输出的不同,我们特意选择了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 '', )

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

1.生成环境对象env2.生成PPO对象agent3.开始每一幕(Episode)循环4.首先重置agent存储的数据,将上一幕存储的数据清零5.得到环境的初始状态6.设置记录这一幕reward的变量7.开始这一幕中每一步(Step)的循环8.首先显示游戏窗口9.基于状态state,利用agent生成行动action10.将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中下载。

最新回复(0)