强化学习w/ Keras + OpenAI的实践:Actor-Critic模型

来源:互联网 发布:mac php环境 编辑:程序博客网 时间:2024/06/05 07:52

快速回顾

在上次的Keras/OpenAI教程中,讨论了一个非常基础的强化学习算法——DQN(深度Q网络)。这个“深度Q网络”是近几年刚出现的新兴事物,所以,如果你能够理解甚至运用这个算法,那就太了不起了。首先,还是快速地回顾一下取得的惊人成绩:一开始,对于一个好算法的开发几乎一概不知;而现在,已经能够探索的环境,并且完成试验了。

可以设身处地的想,这有点像让你玩一个既没有游戏规则,也没有最终目标的游戏,你只需要不停地玩,直到你赢了为止(感觉有点残忍)。不仅如此,你通过一系列动作所能达到的结果是无限的(连续观察空间)!然而,DQN通过逐步维护个更新的操作在处理这个看似不可能完成的任务时,其收敛速度快得惊人。

更复杂的环境

“MountainCar环境”到“Pendulum环境”的步骤与“CartPole环境”到“MountainCar环境”的步骤非常相似:正从一个离散的环境扩展为一个连续的环境。“Pendulum环境”有一个无限输入空间,这就意味着在任何给定的时间里,你能采取的行为数量是完全没有限制的。那么为什么DQN不再适用于这个环境了呢?DQN的执行难道不是完全独立于环境行为的结构吗?


比起MountainCar,给提供了一个无限输入空间的Pendulum所带来的挑战更大。

尽管它与具体的行为几乎没有关系,但DQN完全是建立在一个无限输出空间的基础之上的。想想是如何构建代码的:预测结果会给各个步骤中的各个可能发生的行为指定一个分数(在给定的现有环境下),然后网络会执行那个得分最高的行为。之前,通过给各个行为评分,已经减少了强化学习的问题。但是,假如有一个无限的输入空间,问题还能这么轻易地被解决吗?可能这个时候需要一个无限大的表格来跟踪记录所有的Q值!

这个“无限大的表格”似乎与理想的结构有很大的差距!

那么,应该如何完成这个看似不可能完成的任务呢?现在,正做着一些疯狂无比的事:不仅要玩一个完全没有指示和规则的游戏,而且这个游戏是无止境循环的!让来看一下DQN为什么会受到限制。

根源在于这个模型的构建方式:必须能够在每一步骤中更新在某一行为中的位置。这正是让模型预测Q值,而不是直接预测行为的原因。如果预测行为,那么就不知道在考虑预测结果的基础上如何更新模型,以及对未来的预测将接收到什么反馈。因此,最根源的问题在于,的模型似乎必须输出一个与所有行为相关的反馈的表格式计算。如果把这个模型分开,会怎么样呢?如果有两个独立的模型,一个模型输出预期行为(在连续空间内),另一个模型接收行为输入,由DQN中生成Q值,又会发生什么呢?这似乎对解决问题非常有帮助,事实上,这正是Actor-Critic模型的基础。

Actor-Critic模型的原理


与DQN不同的是,Actor-Critic模型有两个独立的网络,其中一个网络用于在给定的环境中对行为进行预测;另一个网络用于查找行为或环境的值。

正如在前面部分的介绍,整个Actor-Critic(AC)方法都是建立在两个相互作用的模型之上的。在强化学习(RL)和监督式学习领域,多个相互作用的神经络的重要性越来越突出。认识和熟悉这些网络架构,绝对是一次非常有价值的尝试:你将有能力理解,甚至规划出一些领域内先进的算法。

回到话题,顾名思义,Actor-Critic模型有两个组成部分——Actor和Critic。前者接收“环境”输入,并由此确定一个“最佳行为”,这实质上是DQN最普通的操作方式。后者扮演的是一个“评估者”的角色,它对“环境”输入的信息和“最佳行为”进行评分,这个分数代表着某一行为与环境的适应程度。

不妨想象一下,一个小女孩(actor)和她的父母(critic)站在一个操场上。孩子四处张望,观察在这个环境下所有可能玩的项目,比如说玩滑滑梯、荡秋千,或者是拔草。在这种情况下,不管是批评还是赞扬,都是父母根据女孩在这个环境下女孩的行为和表现所作出的评价。父母的评价和判断与所处的环境有非常密切的关系。

简要介绍:“链式法则”(任选)

在AC模型的原理中,你主要需要掌握的是支撑了绝大部分现代机器学习的一条法则——“链式法则”。毫不夸张地说,“链式法则”看似非常简单,但它将可能成为理解机器学习实践的最重要想法之一。事实上,如果你能直观地理解“链式法则”的概念,即使你不太擅长数学也完全没有关系。接下来我将快速地介绍一下“链式法则”。在下一个部分将会介绍AC模型的实际开发框架,并解释“链式法则”与AC模型开发的关系。


这是一个看似非常简单的概念,它有可能来自于你的第一堂微积分课程。由于其对算法的惊人加速,它构成了机器学习的现代基础。

从图上看,这个等式非常直观明了:分子或分母互相抵消。但是这个直观的等式有一个主要问题:等式的推导是反向的!重要的是要记住,数学与开发直观的符号一样,就像理解概念一样。所以,人们开发这个分数式的原因是“链式法则”与分数简化的原理非常相似。因此,那些企图仅通过这个符号来解释概念的人实际上都忽视了一个关键性步骤:为什么这个符号是适用的呢?求导过程为什么是这样的呢?


经典的弹簧例子实际上是观察运动中的“链式法则”的一种直观方法。

其中隐含的概念实际上并不比这个符号难理解。想象有几条绳子,绳子有一些固定的点被绑在一起,就像被串联起来的弹簧一样。假如你抓着这个弹簧系统的一端,你的目标是让另一端以10英尺/秒的速度震动。那么你可以以10英尺/秒的速度震动你抓住的一端,让它的震动传递到另一端。或者你可以连接一些以较低速率(比如5英尺/秒)震动的中间节点,这样,你就只需要以2英尺/秒的速率摇晃你所持的一端,因为你做的所有动作都会被传递到另一末端。其中的原理是:物理上的连接能够使动作从一端传递至另一端。注意!这里打的比方当然会与现实有些出入,但这主要是为了更直观、形象地理解链式法则。

同样的道理,如果有两个网络系统,其中一个系统的输出作为另一系统的输入。“震动”输出系统的参数自然会影响其输出,而这个输出的任何变化必定会被传递到管道的另一端。

AC模型概述

因此,必须开发一个与训练更为复杂的DQN有一些重合的ActorCritic模型。由于需要更多更高级的特性,所以必须利用Keras底层库的基础——Tensorflow。注意:你同样可以在Theano中执行,但是因为我从没研究过Theano,所以我没有将其代码包含在内。

AC模型的执行包括四个主要内容,与DQN的执行方法是一致的:

·      模型参数/设置

·      训练代码

·      预测代码

AC参数

首先,需要的是导入:

import gymimport numpy as np from keras.models import Sequential, Modelfrom keras.layers import Dense, Dropout, Inputfrom keras.layers.merge import Add, Multiplyfrom keras.optimizers import Adamimport keras.backend as K
import tensorflow as tf
import randomfrom collections import deque

 

AC参数与DQN参数非常类似,毕竟除去AC模型是两个独立单元,这个AC模型与DQN模型一样要完成一模一样的任务。为了保证网络能够顺利收敛,还继续使用了在DQN介绍帖中提到的“目标网络黑客”。唯一的新参数被称为“tau”,它与在下面案例中目标网络学习发生的变化有关:

class ActorCritic:    def __init__(self, env, sess):        self.env  = env        self.sess = sess
        self.learning_rate = 0.001        self.epsilon = 1.0        self.epsilon_decay = .995        self.gamma = .95        self.tau   = .125        self.memory = deque(maxlen=2000)

 

在接下来的训练部分,会对这个“tau”参数的使用作更详细的介绍,但简单来说,它的作用实质上是将“预测模型”慢慢地转换为“目标模型”。那么现在,就谈到了最有趣的部分:定义模型。正如前面所描述的,有两个单独的模型,每个模型都与其目标模型相关联。

从定义actor模型开始。Actor模型的目的在于,在给定的环境下,确定应采取的最佳行为。再次强调,这个任务给了数值数据,这意味着出了除了目前使用的全连接层,网络不需要、也没有多余的空间涵盖更多复杂的层。因此,actor模型几乎就是一系列从“环境观察”映射到“环境空间”的一点的全连接层:

def create_actor_model(self):        state_input = Input(shape=self.env.observation_space.shape)        h1 = Dense(24, activation='relu')(state_input)        h2 = Dense(48, activation='relu')(h1)        h3 = Dense(24, activation='relu')(h2)        output = Dense(self.env.action_space.shape[0],              activation='relu')(h3)                model = Model(input=state_input, output=output)        adam  = Adam(lr=0.001)        model.compile(loss="mse", optimizer=adam)        return state_input, model

 

主要的区别在于会归还给输入层一个reference。读完这个部分,你就会清楚这么做的原因。但简单来说,这是为了更好地区别处理actor模型的训练。

关于Actor模型,最棘手的问题就是决定如何训练它,而这也正是“链式法则”发挥作用的地方。但在讨论这个问题之前,来思考一下——为什么actor网络不同于标准的critic网络或DQN呢?是不是只需要执行DQN案例中一样的操作:根据当前的环境状态,以及根据基于当前和未来奖励的最佳行为,对模型进行匹配?如果有能力做到所有事情,那么这就不是问题了。但事实是对很对事情都无能为力,所以问题是确实存在的——由于现在的Q分数是在critic网络中单独计算的,究竟如何判断什么是“最佳行为”?

因此,为了克服这个问题,选择了另一个方法。并不是尝试找到“最佳行为”并不断地让模型与之适应,实际上是在“爬山坡”——梯度上升。“爬山坡”的说法主要是针对那些不太熟悉这个概念的人所做的一个比方,意思是,从你所处的角度,确定山坡最陡的方向,然后朝着这个方向一点点移动。换句话来说,“爬山坡”就是沿着一定的方向做一些很简单的事情,以尽力达到球体的最大值。你可能会想到很多情境来反驳这一方法,但是通常,它在现实情况下很有效。

因此,想利用这种方法来更新的actor模型。想判断出什么样的参数变化(actor模型中的参数)会导致Q值(由critic模型预测得出)的最大提升。因为actor模型输出行为,而critic模型则会根据环境状况和行为来作出评估,这样才能看到“链式法则”在其中发挥的作用。利用actor网络的输出作为的“中间链接”,会了解到如何改变actor参数才会导致最终的Q值的变化。

  self.actor_state_input, self.actor_model = \            self.create_actor_model()        _, self.target_actor_model = self.create_actor_model()
        self.actor_critic_grad = tf.placeholder(tf.float32,             [None, self.env.action_space.shape[0]])                 actor_model_weights = self.actor_model.trainable_weights        self.actor_grads = tf.gradients(self.actor_model.output,             actor_model_weights, -self.actor_critic_grad)        grads = zip(self.actor_grads, actor_model_weights)        self.optimize =  tf.train.AdamOptimizer(            self.learning_rate).apply_gradients(grads)

 

   在这里可以看到,保留了模型权重和输出(行为)之间的梯度。对于那些不熟悉Tensorflow的人来说,“占位符”实际上就是在你运行Tensorflow时输入数据的地方。在这里我就不过多地描述了,tensorflow.org教程中有非常详细的介绍。

转移到critic网络,面临的问题就完全相反了。也就是说,网络的定义稍微复杂了一些,而它的训练则相对简单了。Critic网络将环境状况和行为同时作为其输入,然后计算出相应的值。这样的操作是通过一系列全连接的层来完成的,其中中间有一个层把两个层合并起来,然后在结合到最终的Q值预测:

def create_critic_model(self):        state_input = Input(shape=self.env.observation_space.shape)        state_h1 = Dense(24, activation='relu')(state_input)        state_h2 = Dense(48)(state_h1)                action_input = Input(shape=self.env.action_space.shape)        action_h1    = Dense(48)(action_input)                merged    = Add()([state_h2, action_h1])        merged_h1 = Dense(24, activation='relu')(merged)        output = Dense(1, activation='relu')(merged_h1)        model  = Model(input=[state_input,action_input],             output=output)                adam  = Adam(lr=0.001)        model.compile(loss="mse", optimizer=adam)        return state_input, action_input, model

其中的关键在于,处理输入的方式与返还的内容之间存在不对称性。第一点,输入的处理方式。在环境状态输入上,比行为输入多设置了一个全连接层。这么做是因为这样的架构是专门为AC网络推荐的,但把全连接层都叠加在两个输入上的作用可能差不多。至于第二点,返还的内容,需要保留状态和行为输入,因为需要利用它们来更新actor网络:

 self.critic_state_input, self.critic_action_input, \            self.critic_model = self.create_critic_model()        _, _, self.target_critic_model = self.create_critic_model()
        self.critic_grads = tf.gradients(self.critic_model.output,             self.critic_action_input)                # Initialize for later gradient calculations        self.sess.run(tf.initialize_all_variables())

在这里,把缺失的梯度设置为需要计算的项目:与行为权重相关的输出Q。这在训练代码中是直接被调用的,正如接下来将看到的。

AC模型训练

这个区别于DQN的最后一个主要部分是“实际训练”。然而确实使用了相同的基础结构:从记忆中提取片段,并从中学习。因为有两种训练方法,所以把代码分成了不同的训练函数:

def train(self):        batch_size = 32        if len(self.memory) < batch_size:            return
        rewards = []        samples = random.sample(self.memory, batch_size)        self._train_critic(samples)        self._train_actor(samples)

现在来对这两种训练方法进行定义。然而,这与DQN非常类似,其实就是在寻找网络的未来回报及其训练。唯一的区别就是,是在“环境状态/行为”上进行训练的,而且使用了目标的critic模型,而不是actor模型,来预测未来回报:

    def _train_critic(self, samples):        for sample in samples:            cur_state, action, reward, new_state, done = sample            if not done:                target_action =                     self.target_actor_model.predict(new_state)                future_reward = self.target_critic_model.predict(                    [new_state, target_action])[0][0]                reward += self.gamma * future_reward            self.critic_model.fit([cur_state, action],                 reward, verbose=0)

幸运的是,之前已经完成了所有actor模型相关的工作!已经设置好了梯度在网络中的工作方式,所以现在只需要根据遇到的行为和环境状态来调用它:

def _train_actor(self, samples):        for sample in samples:            cur_state, action, reward, new_state, _ = sample            predicted_action = self.actor_model.predict(cur_state)            grads = self.sess.run(self.critic_grads, feed_dict={                self.critic_state_input:  cur_state,                self.critic_action_input: predicted_action            })[0]
            self.sess.run(self.optimize, feed_dict={                self.actor_state_input: cur_state,                self.actor_critic_grad: grads            })


就像前面所提到的,利用了目标模型。因此,必须在每一步都对其进行更新。更具体地说就是,以分数的形式将目标模型的值保留下来,然后将其更新。对actor模型和critic模型,都这么做,但本文只提供了actor模型的例子。

def _update_actor_target(self):        actor_model_weights  = self.actor_model.get_weights()        actor_target_weights =self.target_critic_model.get_weights()                for i in range(len(actor_target_weights)):            actor_target_weights[i] = actor_model_weights[i]        self.target_critic_model.set_weights(actor_target_weights

AC模型预测

AC模型预测与DQN预测没有任何区别,因此,在这里就不再介绍了。

def act(self, cur_state):  self.epsilon *= self.epsilon_decay  if np.random.random() < self.epsilon:   return self.env.action_space.sample()  return self.actor_model.predict(cur_state)

预测编码

AC的预测编码过程同样与之前的强化学习算法非常相似。只需要反复地试验和调用预测,然后记住并对其进行训练。

def main():    sess = tf.Session()    K.set_session(sess)    env = gym.make("Pendulum-v0")    actor_critic = ActorCritic(env, sess)        num_trials = 10000    trial_len  = 500        cur_state = env.reset()    action = env.action_space.sample()    while True:        env.render()        cur_state = cur_state.reshape((1,             env.observation_space.shape[0]))        action = actor_critic.act(cur_state)        action = action.reshape((1, env.action_space.shape[0]))                new_state, reward, done, _ = env.step(action)        new_state = new_state.reshape((1,             env.observation_space.shape[0]))                actor_critic.remember(cur_state, action, reward,             new_state, done)        actor_critic.train()                cur_state = new_state

完整代码

这里有使用AC模型对Pendulum-v0环境进行训练的完整代码。

 

"""solving pendulum using actor-critic model"""import gymimport numpy as np from keras.models import Sequential, Modelfrom keras.layers import Dense, Dropout, Inputfrom keras.layers.merge import Add, Multiplyfrom keras.optimizers import Adamimport keras.backend as Kimport tensorflow as tfimport randomfrom collections import deque# determines how to assign values to each state, i.e. takes the state# and action (two-input model) and determines the corresponding valueclass ActorCritic:def __init__(self, env, sess):self.env  = envself.sess = sessself.learning_rate = 0.001self.epsilon = 1.0self.epsilon_decay = .995self.gamma = .95self.tau   = .125# ===================================================================== ##                               Actor Model                             ## Chain rule: find the gradient of chaging the actor network params in  ## getting closest to the final value network predictions, i.e. de/dA    ## Calculate de/dA as = de/dC * dC/dA, where e is error, C critic, A act ## ===================================================================== #self.memory = deque(maxlen=2000)self.actor_state_input, self.actor_model = self.create_actor_model()_, self.target_actor_model = self.create_actor_model()self.actor_critic_grad = tf.placeholder(tf.float32, [None, self.env.action_space.shape[0]]) # where we will feed de/dC (from critic)actor_model_weights = self.actor_model.trainable_weightsself.actor_grads = tf.gradients(self.actor_model.output, actor_model_weights, -self.actor_critic_grad) # dC/dA (from actor)grads = zip(self.actor_grads, actor_model_weights)self.optimize = tf.train.AdamOptimizer(self.learning_rate).apply_gradients(grads)# ===================================================================== ##                              Critic Model                             ## ===================================================================== #self.critic_state_input, self.critic_action_input, \self.critic_model = self.create_critic_model()_, _, self.target_critic_model = self.create_critic_model()self.critic_grads = tf.gradients(self.critic_model.output, self.critic_action_input) # where we calcaulte de/dC for feeding above# Initialize for later gradient calculationsself.sess.run(tf.initialize_all_variables())# ========================================================================= ##                              Model Definitions                            ## ========================================================================= #def create_actor_model(self):state_input = Input(shape=self.env.observation_space.shape)h1 = Dense(24, activation='relu')(state_input)h2 = Dense(48, activation='relu')(h1)h3 = Dense(24, activation='relu')(h2)output = Dense(self.env.action_space.shape[0], activation='relu')(h3)model = Model(input=state_input, output=output)adam  = Adam(lr=0.001)model.compile(loss="mse", optimizer=adam)return state_input, modeldef create_critic_model(self):state_input = Input(shape=self.env.observation_space.shape)state_h1 = Dense(24, activation='relu')(state_input)state_h2 = Dense(48)(state_h1)action_input = Input(shape=self.env.action_space.shape)action_h1    = Dense(48)(action_input)merged    = Add()([state_h2, action_h1])merged_h1 = Dense(24, activation='relu')(merged)output = Dense(1, activation='relu')(merged_h1)model  = Model(input=[state_input,action_input], output=output)adam  = Adam(lr=0.001)model.compile(loss="mse", optimizer=adam)return state_input, action_input, model# ========================================================================= ##                               Model Training                              ## ========================================================================= #def remember(self, cur_state, action, reward, new_state, done):self.memory.append([cur_state, action, reward, new_state, done])def _train_actor(self, samples):for sample in samples:cur_state, action, reward, new_state, _ = samplepredicted_action = self.actor_model.predict(cur_state)grads = self.sess.run(self.critic_grads, feed_dict={self.critic_state_input:  cur_state,self.critic_action_input: predicted_action})[0]self.sess.run(self.optimize, feed_dict={self.actor_state_input: cur_state,self.actor_critic_grad: grads})            def _train_critic(self, samples):for sample in samples:cur_state, action, reward, new_state, done = sampleif not done:target_action = self.target_actor_model.predict(new_state)future_reward = self.target_critic_model.predict([new_state, target_action])[0][0]reward += self.gamma * future_rewardself.critic_model.fit([cur_state, action], reward, verbose=0)def train(self):batch_size = 32if len(self.memory) < batch_size:returnrewards = []samples = random.sample(self.memory, batch_size)self._train_critic(samples)self._train_actor(samples)# ========================================================================= ##                         Target Model Updating                             ## ========================================================================= #def _update_actor_target(self):actor_model_weights  = self.actor_model.get_weights()actor_target_weights = self.target_critic_model.get_weights()for i in range(len(actor_target_weights)):actor_target_weights[i] = actor_model_weights[i]self.target_critic_model.set_weights(actor_target_weights)def _update_critic_target(self):critic_model_weights  = self.critic_model.get_weights()critic_target_weights = self.critic_target_model.get_weights()for i in range(len(critic_target_weights)):critic_target_weights[i] = critic_model_weights[i]self.critic_target_model.set_weights(critic_target_weights)def update_target(self):self._update_actor_target()self._update_critic_target()# ========================================================================= ##                              Model Predictions                            ## ========================================================================= #def act(self, cur_state):self.epsilon *= self.epsilon_decayif np.random.random() < self.epsilon:return self.env.action_space.sample()return self.actor_model.predict(cur_state)def main():sess = tf.Session()K.set_session(sess)env = gym.make("Pendulum-v0")actor_critic = ActorCritic(env, sess)num_trials = 10000trial_len  = 500cur_state = env.reset()action = env.action_space.sample()while True:env.render()cur_state = cur_state.reshape((1, env.observation_space.shape[0]))action = actor_critic.act(cur_state)action = action.reshape((1, env.action_space.shape[0]))new_state, reward, done, _ = env.step(action)new_state = new_state.reshape((1, env.observation_space.shape[0]))actor_critic.remember(cur_state, action, reward, new_state, done)actor_critic.train()cur_state = new_stateif __name__ == "__main__":main()

阅读全文
0 0