第五章 实现你的第一个学习代理-解决山地车的问题

前言

干得好,走到这一步!在前面的章节中,我们很好地介绍了OpenAI Gym,它的特性,以及如何在你自己的程序中安装、配置和使用它。我们还讨论了强化学习的基础知识以及什么是深度强化学习,并建立了PyTorch深度学习库来开发深度强化学习应用程序。在本章中,您将开始开发您的第一个学习代理!你将开发一个智能代理,它将学习如何解决山地车的问题。逐渐在接下来的章节中,我们将解决日益具有挑战性的问题得到更舒适OpenAI健身发展中强化学习算法解决问题。我们将开始本章通过了解山车的问题,这已成为一种流行在强化学习和社会最优控制问题。我们将开发从头学习代理,然后训练它解决山车问题使用Gym的山车环境。我们将终于看到代理的进展并简要如何看待的方式我们可以改善剂用它来解决更复杂的问题。我们将在这一章讨论的主题如下:

  • 了解山车问题
  • 实现基于代理的强化学习来解决山车问题
  • 在Gym中训练强化学习代理
  • 测试代理的性能

理解山车问题

对于任何强化学习问题,无论我们使用何种学习算法,有关该问题的两个基本定义都是重要的。它们是状态空间和动作空间的定义。我们在本书前面提到过,状态和动作空间可以是离散的,也可以是连续的。通常,在大多数问题中,状态空间由连续值组成,并表示为向量、矩阵或张量(多维矩阵)。与连续值问题和环境相比,具有离散动作空间的问题和环境相对容易。在这本书中,我们将为一些问题和环境开发混合了状态空间和动作空间组合的学习算法,这样当你开始自己开发智能代理和算法时,你就可以轻松地处理任何此类变化。

让我们首先通过高层次的描述来理解山地车问题,然后再看看山地车环境的状态和行动空间。

山车问题和环境

在山地车Gym的环境中,一辆车是在一个一维的轨道上,位于两座山之间。我们的目标是让车靠右行驶上山;然而,即使以最高速度行驶,汽车的引擎也不够强劲,无法上山。因此,要想取得成功,就必须反复推波助澜。简而言之,山地车的问题就是把动力不足的车开到山顶。

在你实现你的代理算法之前,它将极大地帮助你理解环境、问题、状态和行动空间。我们如何找出运动室内山地车环境的状态和活动空间?我们已经从第四章了解了如何做到这一点,探索健身房及其特点。我们编写了一个名为get_observation_action_space的脚本。它将打印环境的状态、观察和操作空间,其名称将作为第一个参数传递给脚本。让我们用以下命令要求它打印MountainCar-v0环境的空格:

:~/rl_gym_book/ch4$ python get_observation_action_space.py 'MountainCar-v0'

上述命令将产生如下输出:
在这里插入图片描述
从这个输出,我们可以看到状态和观测空间是一个二维的盒子,而行动空间是三维的和离散的。

如果你想复习一下盒子和离散空间的含义,你可以快速翻到第4章,探索Gym及其特征,我们在Gym部分讨论了这些空间及其含义。理解它们是很重要的。

下表总结了状态和动作空间类型、描述和允许值的范围,供参考:
在这里插入图片描述
举个例子,汽车从-0.6到-0.4的随机位置出发,速度为零,目标是到达右边的山顶,也就是0。5的位置。(从技术上讲,汽车可以超过0.5,达到0.6,这也是考虑的。)在到达目标位置(0.5)之前,环境将在每一次步骤中发送-1作为奖励。环境会终止这一插曲。当汽车到达0.5位置或所走的步数达到200时,done变量将等于True。

从零开始实现Q-learning

在本节中,我们将逐步开始实现我们的智能代理。我们将使用NumPy库和OpenAI Gym库中的MountainCar-V0环境来实现著名的Q-learning算法。
让我们回顾一下我们在第4章中使用的强化学习Gym锅板代码,探索Gym及其特点,如下:

import gym
env = gym.make("Qbert-v0")
MAX_NUM_EPISODES = 10
MAX_STEPS_PER_EPISODE = 500
for episode in range(MAX_NUM_EPISODES):
    obs = env.reset()
    for step in range(MAX_STEPS_PER_EPISODE):
        env.render()
        action = env.action_space.sample()# Sample random action. This will be replaced by our agent's action when we start developing the agent algorithms
        next_state, reward, done, info = env.step(action) # Send the action to the environment and receive the next_state, reward and whether done or not
        obs = next_state

        if done is True:
            print("\n Episode #{} ended in {} steps.".format(episode, step+1))
            break

这段代码是开发我们的强化学习代理的一个很好的起点(也就是样板!)。我们首先将环境名从Qbert-v0更改为MountainCar-v0。注意,在前面的脚本中,我们设置了MAX_STEPS_PER_EPISODE。这是代理在情节结束之前可以采取的步骤或操作的数量。这在持续的、永久的或循环的环境中可能很有用,因为环境本身不会结束情节。这里,我们为代理设置了一个限制,以避免无限循环。然而,OpenAI Gym中定义的大多数环境都有一个插曲终止条件,一旦满足其中任何一个条件,env.step(…)函数返回的done变量将被设置为True。我们在上一节中看到,对于我们感兴趣的山地车问题,如果汽车到达目标位置(0.5)或所走的步数达到200,环境将终止插曲。因此,我们可以进一步简化这个样板代码,使其像下面这样用于Mountain Car环境:

import gym
env = gym.make("MountainCar-v0")
MAX_NUM_EPISODES = 5000

for episode in range(MAX_NUM_EPISODES):
    done = False
    obs = env.reset()
    total_reward = 0.0 # To keep track of the total reward obtained in each episode
    step = 0
    while not done:
        env.render()
        action = env.action_space.sample()# Sample random action. This will be replaced by our agent's action when we start developing the agent algorithms
        next_state, reward, done, info = env.step(action) # Send the action to the environment and receive the next_state, reward and whether done or not
        total_reward += reward
        step += 1
        obs = next_state

    print("\n Episode #{} ended in {} steps. total_reward={}".format(episode, step+1, total_reward))
env.close()

如果您运行前面的脚本,您将看到山地车环境出现在一个新的窗口中,汽车随机左右移动1000集。你还会在每个章节的末尾看到章节编号、所采取的步骤以及所获得的总奖励,如下图所示:
在这里插入图片描述
示例输出应该类似于下面的截图:
在这里插入图片描述
您应该记得,在我们前面的小节中,代理每走一步就得到-1的奖励,而MountainCar-v0环境将在200步后终止插曲;这就是为什么你的代理有时会得到-200的总奖励!毕竟,行为人没有思考或从之前的行为中学习就采取了随机行动。理想情况下,我们希望代理确定如何以最少的步数到达山顶(靠近旗子、接近、位于或超过位置0.5)。别担心——我们将在本章的最后构建这样一个智能代理!
让我们看一下Q-learning部分。

回顾Q-learning

在第二章,强化学习和深度强化学习中,我们讨论了SARSA和q学习算法。这两种算法都提供了一种更新动作值函数估计的系统方法。特别地,我们看到Q-learning是一种off-policy学习算法,它根据agent的策略,将当前状态和动作的动作值估计更新到后续状态中可获得的最大动作值。我们也看到Q-learning更新由以下公式给出:
在这里插入图片描述
在下一节中,我们将在Python中实现一个Q_Learner类,它实现这个学习更新规则以及其他必要的函数和方法。

使用Python和Numpy来实现Q-learning代理

让我们通过实现Q_Learner类来开始实现Q-learning代理。这个类的主要方法如下:

  • init(self, env)
  • discretize(self, obs)
  • get_action(self, obs)
  • learn(self, obs, action, reward, next_obs)

稍后您将发现,这里的方法是常见的,并且存在于我们将在本书中实现的几乎所有代理中。这使得您很容易掌握它们,因为这些方法将会被反复使用(经过一些修改)。

对于一般的代理实现来说,discretize()函数不是必需的,但是当状态空间很大且连续时,最好将该空间离散为可计数的容器或值范围,以简化表示。这也减少了Q-learning算法需要学习的值的数量,因为它现在只需要学习有限的值集,可以用表格格式或使用n维数组来简洁地表示,而不是复杂的函数。此外,用于最优控制的Q-learnig算法对q值的表表示也能保证收敛。

定义超参数

在Q_Learner类声明之前,我们将初始化几个有用的超参数。下面是我们将在Q_Learner实现中使用的超参数:

  • EPSILON_MIN: 这是我们希望代理在执行贪心策略时使用的最小值。
  • MAX_NUM_EPISODES: 我们希望代理与环境交互的最大集数。
  • STEPS_PER_EPISODE: 这是每一集的步数。这可能是环境中每集允许的最大步骤数,也可能是我们想要基于时间预算限制的自定义值。允许每个插曲有更多的步骤意味着每个插曲可能需要更长的时间才能完成,在非终止的环境中,直到达到这个限制,环境才会被重置,即使代理被困在相同的位置。
  • ALPHA: 这是我们希望agent使用的学习速率。这是前一节列出的q学习更新方程中的alpha值。有些算法会随着训练的进行而改变学习速率。
  • GAMMA: 这是代理人将用来计入未来奖励的折扣系数。这个值对应于上一节Q-learning更新方程中的gamma值。
  • NUM_DISCRETE_BINS: 这是状态空间将被离散到的值的箱数。对于山地车环境,我们将状态空间离散化为30个箱子。你可以使用更高或更低的值。

请注意,MAX_NUM_EPISODES和STEPS_PER_EPISODE已经在本章前面的某个章节中介绍的样板代码中定义了。

这些超参数在Python代码中像这样定义,带有一些初始值:

EPSILON_MIN = 0.005 
max_num_steps = MAX_NUM_EPISODES * STEPS_PER_EPISODE 
EPSILON_DECAY = 500 * EPSILON_MIN / max_num_steps 
ALPHA = 0.05 # Learning rate
GAMMA = 0.98 # Discount factor 
NUM_DISCRETE_BINS = 30 # Number of bins to Discretize each observation dim

实现Q_learner类的__init__方法

接下来,让我们看看Q_Learner类的成员函数定义。init(self, env)函数接受环境的实例,env,作为输入参数,初始化的尺寸/形状观测空间和操作空间,同时也决定了参数离散化基于NUM_DISCRETE_BINS我们组的观测空间。init(self, env)函数也初始化函数Q NumPy阵列,基于离散观测空间的形状和动作空间维度。init(self, env)的实现很简单,因为我们只初始化代理所需的值。这是我们的实现:

class Q_Learner(object):
    def __init__(self, env):
        self.obs_shape = env.observation_space.shape
        self.obs_high = env.observation_space.high
        self.obs_low = env.observation_space.low
        self.obs_bins = NUM_DISCRETE_BINS  # Number of bins to Discretize each observation dim
        self.bin_width = (self.obs_high - self.obs_low) / self.obs_bins
        self.action_shape = env.action_space.n
        # Create a multi-dimensional array (aka. Table) to represent the
        # Q-values
        self.Q = np.zeros((self.obs_bins + 1, self.obs_bins + 1,
                           self.action_shape))  # (51 x 51 x 3)
        self.alpha = ALPHA  # Learning rate
        self.gamma = GAMMA  # Discount factor
        self.epsilon = 1.0

实现Q_learner类的discretize方法

让我们花点时间来理解我们是如何离散化观测空间的。离散观察空间(通常是度量空间)的最简单且有效的方法是将值的范围划分为一组称为bins的有限值。值的跨度/范围由空间中每个维度的最大可能值和最小可能值之间的差给出。一旦我们计算跨度,我们可以用它除以我们决定的NUM_DISCRETE_BINS来获得箱子的宽度。我们在__init__函数中计算了bin宽度,因为它不会随着每一次新的观察而改变。离散化(self, obs)函数接收每一个新的函数,应用离散化步骤在离散化空间中寻找观测值所属的对象。就像这样做一样简单:

(obs - self.obs_low) / self.bin_width

我们希望它属于任何一个bins(而不是介于两者之间的某个位置);因此,我们将前面的代码转换为一个integer:

((obs - self.obs_low) / self.bin_width).astype(int)

最后,我们将这个离散的观察结果返回为一个元组。所有这些操作都可以在一行Python代码中编写,像这样:

def discretize(self, obs): 
	return tuple(((obs - self.obs_low) / self.bin_width).astype(int))

实现Q_learner的get_action方法

我们希望探员在观察后采取行动。get_action(self, obs)是我们定义的函数,用于在obs中给定一个观察结果来生成一个动作。最广泛使用的动作选择策略是贪心策略,它根据agent的估计以(高)概率为1-的方式采取最佳动作,并以(小)概率由epsilon给出的方式采取随机动作。我们使用NumPy的random模块中的random()方法来实现epsilon-greedy策略,如下所示:

 def get_action(self, obs):
        discretized_obs = self.discretize(obs)
        # Epsilon-Greedy action selection
        if self.epsilon > EPSILON_MIN:
            self.epsilon -= EPSILON_DECAY
        if np.random.random() > self.epsilon:
            return np.argmax(self.Q[discretized_obs])
        else:  # Choose a random action
            return np.random.choice([a for a in range(self.action_shape)])

实现Q_learner类的学习方法

正如您可能已经猜到的,这是Q_Learner类最重要的方法,它可以神奇地学习q值,从而使代理能够在一段时间内采取智能行动!最好的部分是它实现起来并不复杂!它仅仅是我们之前看到的Q-learning更新方程的实现。当我说它很容易实现时,不要相信我?!好的,下面是学习函数的实现:

    def learn(self, obs, action, reward, next_obs):
        discretized_obs = self.discretize(obs)
        discretized_next_obs = self.discretize(next_obs)
        td_target = reward + self.gamma * np.max(self.Q[discretized_next_obs])
        td_error = td_target - self.Q[discretized_obs][action]
        self.Q[discretized_obs][action] += self.alpha * td_error

我们本可以在一行代码中编写Q学习更新规则,像这样:

self.Q[discretized_obs][action] += self.alpha * (reward + self.gamma * np.max(self.Q[discretized_next_obs]) - self.Q[discretized_obs][action])

但是,在单独的一行中计算每一项会使它更容易阅读和理解。

完整实现Q_learner类

如果我们把所有的方法实现放在一起,我们会得到这样的代码片段:

import gym
import numpy as np

MAX_NUM_EPISODES = 50000
STEPS_PER_EPISODE = 200 #  This is specific to MountainCar. May change with env
EPSILON_MIN = 0.005
max_num_steps = MAX_NUM_EPISODES * STEPS_PER_EPISODE
EPSILON_DECAY = 500 * EPSILON_MIN / max_num_steps
ALPHA = 0.05  # Learning rate
GAMMA = 0.98  # Discount factor
NUM_DISCRETE_BINS = 30  # Number of bins to Discretize each observation dim


class Q_Learner(object):
    def __init__(self, env):
        self.obs_shape = env.observation_space.shape
        self.obs_high = env.observation_space.high
        self.obs_low = env.observation_space.low
        self.obs_bins = NUM_DISCRETE_BINS  # Number of bins to Discretize each observation dim
        self.bin_width = (self.obs_high - self.obs_low) / self.obs_bins
        self.action_shape = env.action_space.n
        # Create a multi-dimensional array (aka. Table) to represent the
        # Q-values
        self.Q = np.zeros((self.obs_bins + 1, self.obs_bins + 1,
                           self.action_shape))  # (51 x 51 x 3)
        self.alpha = ALPHA  # Learning rate
        self.gamma = GAMMA  # Discount factor
        self.epsilon = 1.0

    def discretize(self, obs):
        return tuple(((obs - self.obs_low) / self.bin_width).astype(int))

    def get_action(self, obs):
        discretized_obs = self.discretize(obs)
        # Epsilon-Greedy action selection
        if self.epsilon > EPSILON_MIN:
            self.epsilon -= EPSILON_DECAY
        if np.random.random() > self.epsilon:
            return np.argmax(self.Q[discretized_obs])
        else:  # Choose a random action
            return np.random.choice([a for a in range(self.action_shape)])

    def learn(self, obs, action, reward, next_obs):
        discretized_obs = self.discretize(obs)
        discretized_next_obs = self.discretize(next_obs)
        td_target = reward + self.gamma * np.max(self.Q[discretized_next_obs])
        td_error = td_target - self.Q[discretized_obs][action]
        self.Q[discretized_obs][action] += self.alpha * td_error

我们已经准备好智能体了。你可能会问,我们下一步该做什么?我们应该在Gym里训练智能体!在下一节中,我们将介绍训练过程。

在Gym中训练强化学习代理

培训Q-learning代理的过程对您来说可能已经很熟悉了,因为它有许多与我们之前使用的样板代码相同的代码行,也有类似的结构。我们现在不再从环境的操作空间中选择随机操作,而是使用agent.get_action(obs)方法从代理中获取操作。我们也会打电话给代理。将agent的动作发送到环境并收到反馈后,学习(obs, action, reward, next_obs)方法。培训功能如下:

def train(agent, env):
    best_reward = -float('inf')
    for episode in range(MAX_NUM_EPISODES):
        done = False
        obs = env.reset()
        total_reward = 0.0
        while not done:
            action = agent.get_action(obs)
            next_obs, reward, done, info = env.step(action)
            agent.learn(obs, action, reward, next_obs)
            obs = next_obs
            total_reward += reward
        if total_reward > best_reward:
            best_reward = total_reward
        print("Episode#:{} reward:{} best_reward:{} eps:{}".format(episode,
                                     total_reward, best_reward, agent.epsilon))
    # Return the trained policy
    return np.argmax(agent.Q, axis=2)

测试和记录智能体的性能

一旦我们让智能体在Gym训练,我们希望能够衡量他的学习情况。为了做到这一点,我们让智能体做了一个测试。就像在学校一样!test(agent、env、policy)获取代理对象、环境实例和代理的策略,以测试代理在环境中的性能,并返回一个完整插曲的总报酬。它类似于我们前面看到的train(agent, env)函数,但它不让agent学习或更新其Q值估计:

def test(agent, env, policy):
    done = False
    obs = env.reset()
    total_reward = 0.0
    while not done:
        action = policy[agent.discretize(obs)]
        next_obs, reward, done, info = env.step(action)
        obs = next_obs
        total_reward += reward
    return total_reward

请注意,test(agent, env, policy)函数评估代理在一集中的表现,并返回代理在该集中获得的总奖励。我们想要衡量代理在几集中的表现如何,以得到一个很好的衡量代理的实际表现。此外,Gym提供了一个称为monitor的方便包装函数,以视频文件的形式记录agent的进度。下面的代码片段演示了如何测试和记录代理在1,000集上的性能,并将记录的代理的动作作为视频文件保存在环境中的gym_monitor_path目录中:

if __name__ == "__main__":
    env = gym.make('MountainCar-v0')
    agent = Q_Learner(env)
    learned_policy = train(agent, env)
    # Use the Gym Monitor wrapper to evalaute the agent and record video
    gym_monitor_path = "./gym_monitor_output"
    env = gym.wrappers.Monitor(env, gym_monitor_path, force=True)
    for _ in range(1000):
        test(agent, env, learned_policy)
    env.close()

简单完整的Q-learner实现用来解决山车问题

在这一节中,我们将把整个代码放到一个Python脚本中,以初始化环境,启动代理的培训过程,获取训练好的策略,测试代理的性能,并记录它在环境中的行为!

#!/usr/bin/env/ python
"""
q_learner.py
An easy-to-follow script to train, test and evaluate a Q-learning agent on the Mountain Car
problem using the OpenAI Gym. |Praveen Palanisamy
# Chapter 5, Hands-on Intelligent Agents with OpenAI Gym, 2018
"""
import gym
import numpy as np

MAX_NUM_EPISODES = 2000
STEPS_PER_EPISODE = 200 #  This is specific to MountainCar. May change with env
EPSILON_MIN = 0.005
max_num_steps = MAX_NUM_EPISODES * STEPS_PER_EPISODE
EPSILON_DECAY = 500 * EPSILON_MIN / max_num_steps
ALPHA = 0.05  # Learning rate
GAMMA = 0.98  # Discount factor
NUM_DISCRETE_BINS = 30  # Number of bins to Discretize each observation dim


class Q_Learner(object):
    def __init__(self, env):
        self.obs_shape = env.observation_space.shape
        self.obs_high = env.observation_space.high
        self.obs_low = env.observation_space.low
        self.obs_bins = NUM_DISCRETE_BINS  # Number of bins to Discretize each observation dim
        self.bin_width = (self.obs_high - self.obs_low) / self.obs_bins
        self.action_shape = env.action_space.n
        # Create a multi-dimensional array (aka. Table) to represent the
        # Q-values
        self.Q = np.zeros((self.obs_bins + 1, self.obs_bins + 1,
                           self.action_shape))  # (51 x 51 x 3)
        self.alpha = ALPHA  # Learning rate
        self.gamma = GAMMA  # Discount factor
        self.epsilon = 1.0

    def discretize(self, obs):
        return tuple(((obs - self.obs_low) / self.bin_width).astype(int))

    def get_action(self, obs):
        discretized_obs = self.discretize(obs)
        # Epsilon-Greedy action selection
        if self.epsilon > EPSILON_MIN:
            self.epsilon -= EPSILON_DECAY
        if np.random.random() > self.epsilon:
            return np.argmax(self.Q[discretized_obs])
        else:  # Choose a random action
            return np.random.choice([a for a in range(self.action_shape)])

    def learn(self, obs, action, reward, next_obs):
        discretized_obs = self.discretize(obs)
        discretized_next_obs = self.discretize(next_obs)
        td_target = reward + self.gamma * np.max(self.Q[discretized_next_obs])
        td_error = td_target - self.Q[discretized_obs][action]
        self.Q[discretized_obs][action] += self.alpha * td_error

def train(agent, env):
    best_reward = -float('inf')
    for episode in range(MAX_NUM_EPISODES):
        done = False
        obs = env.reset()
        total_reward = 0.0
        while not done:
            action = agent.get_action(obs)
            next_obs, reward, done, info = env.step(action)
            agent.learn(obs, action, reward, next_obs)
            obs = next_obs
            total_reward += reward
        if total_reward > best_reward:
            best_reward = total_reward
        print("Episode#:{} reward:{} best_reward:{} eps:{}".format(episode,
                                     total_reward, best_reward, agent.epsilon))
    # Return the trained policy
    return np.argmax(agent.Q, axis=2)


def test(agent, env, policy):
    done = False
    obs = env.reset()
    total_reward = 0.0
    while not done:
        action = policy[agent.discretize(obs)]
        next_obs, reward, done, info = env.step(action)
        obs = next_obs
        total_reward += reward
    return total_reward


if __name__ == "__main__":
    env = gym.make('MountainCar-v0')
    agent = Q_Learner(env)
    learned_policy = train(agent, env)
    # Use the Gym Monitor wrapper to evalaute the agent and record video
    gym_monitor_path = "./gym_monitor_output"
    env = gym.wrappers.Monitor(env, gym_monitor_path, force=True)
    for _ in range(1000):
        test(agent, env, learned_policy)
    env.close()


如果你让代理学习的时间足够长,你就会看到代理不断改进和学习,以越来越少的步骤到达山顶。

总结

在这一章里我们学到了很多。更重要的是,我们实现了一个agent,它能在7分钟左右的时间里聪明地解决山地车的问题!

我们从了解著名的山地车问题开始,并观察环境、观察空间、状态空间和奖励是如何在健身房的山地车v0环境中设计的。我们重新访问了上一章中使用的reinforcement learning Gym样板代码,并对其进行了一些改进,这些改进也可以在本书的代码存储库中获得。

然后我们为我们的Q-learning代理定义了超参数,并开始从头开始实现Q-learning算法。我们首先实现了代理的初始化函数来初始化代理的内部状态变量,包括使用NumPy n维数组的Q值表示。然后采用离散化方法对状态空间进行离散化;get_action(…)方法基于贪心策略选择操作;最后是learn(…)函数,它实现了Q-learning更新规则,形成了agent的核心。我们看到了它有多简单。

我希望您在实现这个代理并观看它在Gym解决山地车问题时玩得很开心!我们将在下一章进入高级方法来解决各种更有挑战性的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

点PY

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值