第六章 利用深度Q学习来实现最优控制的智能体

前言

在前一章中,我们实现了一个智能代理,它使用Q- learning在大约7分钟的时间内在双核笔记本电脑CPU上从头开始解决山地车问题。在本章中,我们将实现一个高级版本的Q-learning,称为深度Q-learning,它可以用来解决几个离散控制问题,这些问题比山地车问题要复杂得多。离散控制问题是将行动空间离散为有限个数的值的(序列)决策问题。在前一章中,学习agent使用一个二维状态空间矢量作为输入,其中包含小车的位置和速度信息,以采取最优控制行动。在本章中,我们将看到如何实现一个学习代理,它将(屏幕上)视觉图像作为输入,并学习采取最优控制行动。这和我们解决问题的方法很接近,不是吗?我们人类不会通过计算物体的位置和速度来决定下一步该做什么。我们只是观察发生了什么,然后学习采取行动,随着时间的推移改善,最终完全解决问题。

这一章将指导你如何逐步构建一个更好的代理,通过使用最近出版的方法逐步改进我们的q学习代理实现与深度神经网络函数近似的稳定q学习。在本章结束时,您将学会如何实现和训练一个深度q学习代理,该代理观察屏幕上的像素,并使用Gym环境玩Atari游戏,并获得相当好的分数!我们还将讨论如何在学习过程中可视化和比较代理的性能。您将看到相同的代理算法是如何在几个不同的Atari游戏上训练的,并且代理仍然能够很好地学习玩游戏。如果你不能等待看到在行动或如果你想看到的东西,瞥见你将开发在潜水之前,你可以查看本章互动-文件夹下的代码从这本书的代码库并尝试pre-trained代理在多个Atari游戏!关于如何运行预先训练过的代理的说明可以在ch6/README.md文件中找到。

本章有很多技术细节为你配备足够的背景知识和你理解的循序渐进的过程改善基本的q学习算法,建立一个更加有能力和智能代理基于q学习的深处,还有一些模块和工具需要训练和测试代理以一种系统化的方式。以下是本章将涉及的较高级别主题的大纲:

  • 改进Q-learning agent的各种方法,包括以下几种:动作值函数的神经网络近似;经验回放;探索计划。
  • 使用PyTorch实现深度卷积神经网络的动作-值函数近似
  • 利用目标网络稳定深度q网络
  • 使用TensorBoard记录和监视PyTorch代理的学习性能
  • 参数与配置管理
  • Atari Gym环境
  • 训练深度Q-learner玩Atari游戏

让我们从第一个主题开始,看看我们如何从上一章结束的地方开始,继续朝着更有能力和更智能的代理前进。

改进的Q-learning代理

在最后一章中,我们回顾了Q-learning算法并实现了Q_Learner类。对于山地车环境,我们使用形状为51x51x3的多维数组来表示动作值函数。注意,我们将状态空间离散为NUM_DISCRETE_BINS配置参数给出的固定数量的bins(我们使用50)。我们本质上用低维、离散的表示将观测量子化或近似化,以减少n维数组中可能元素的数量。通过这种对观察/状态空间的离散化,我们将汽车的可能位置限制在50个固定位置的集合内,汽车的可能速度限制在50个固定值的集合内。任何其他位置或速度值都将近似于这些固定值中的一个。因此,当汽车实际处于不同的位置时,代理可能接收到相同的位置值。对于某些环境,这可能是个问题。施动者可能没有足够的知识来区分从悬崖上掉下来和站在悬崖边缘以便向前跳跃。在下一节中,我们将研究如何使用更强大的函数近似器来表示动作值函数,而不是简单的n维数组/表,因为它有其局限性。

利用神经网络近似q函数

神经网络作为通用函数逼近器被证明是有效的。事实上,有一个普遍的近似定理,说明一个单一的隐含层前馈神经网络可以近似任何封闭和有界的连续函数。这基本上意味着即使是简单的(浅的)神经网络也可以近似几个函数。你可以使用一个具有固定权重/参数的简单神经网络来近似任何函数,这种感觉是不是太好了?它实际上是正确的,除了一个注意事项,使它不能在任何地方使用。虽然单个隐层神经网络可以用有限的参数集逼近任何函数,但我们并没有一个普遍保证的方法来学习那些最能代表任何函数的参数。您将看到研究人员已经能够使用神经网络来近似几个复杂而有用的函数。如今,无处不在的智能手机中内置的大部分智能都是由(高度优化的)神经网络驱动的。根据照片中的人物、地点和背景自动将照片组织成相册的几个性能最好的系统,识别你的面孔和声音的系统,或自动为你撰写电子邮件回复的系统,都是由神经网络提供动力的。即使是最先进的技术——从谷歌Assistant等语音助手那里生成类似人类的真实声音——也由神经网络提供支持。

谷歌Assistant目前使用Deepmind开发的WaveNet和WaveNet2进行文本-语音(TTS)合成,这比目前开发的任何其他TTS系统都要现实得多。

我希望这足以激励您使用神经网络来近似Q函数!在这一节中,我们将首先用浅的(不是深的)单隐层神经网络近似q函数,并使用它来解决著名的Cart Pole问题。虽然神经网络是强大的函数近似器,但我们将看到,为了强化学习问题,训练哪怕是一层神经网络来近似q函数也不是件容易的事。我们将研究一些用神经网络近似来改进q学习的方法,在本章后面的章节中,我们将研究如何使用具有更强表示能力的深度神经网络来近似q函数。

让我们首先回顾一下Q_Learner类的__init__(…)方法,我们在前一章中实现过:

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函数初始化为多维NumPy数组的地方。在下面几节中,我们将看到如何用更强大的神经网络表示代替NumPy数组表示。

使用PyTorch来实现浅层Q网络

在本节中,我们将开始使用PyTorch的神经网络模块实现一个简单的神经网络,然后看看如何使用它来替换基于多维数组的Q动作值表类函数。

让我们从神经网络实现开始。下面的代码演示了如何使用PyTorch实现一个单层感知器(SLP):

class SLP(torch.nn.Module):
    """
    A Single Layer Perceptron (SLP) class to approximate functions
    """
    def __init__(self, input_shape, output_shape, device=torch.device("cpu")):
        """
        :param input_shape: Shape/dimension of the input
        :param output_shape: Shape/dimension of the output
        :param device: The device (cpu or cuda) that the SLP should use to store the inputs for the forward pass
        """
        super(SLP, self).__init__()
        self.device = device
        self.input_shape = input_shape[0]
        self.hidden_shape = 40
        self.linear1 = torch.nn.Linear(self.input_shape, self.hidden_shape)
        self.out = torch.nn.Linear(self.hidden_shape, output_shape)

    def forward(self, x):
        x = torch.from_numpy(x).float().to(self.device)
        x = torch.nn.functional.relu(self.linear1(x))
        x = self.out(x)
        return x

SLP类使用torch.nn实现了一个单层神经网络,在输入层和输出层之间有40个隐藏单元。线性类,并使用矫正线性单元(ReLU或ReLU)作为激活函数。这段代码可以在本书的代码库中找到ch6/ function_approator /perceptron.py。数字40没什么特别的,所以你可以随意改变神经网络中隐藏单位的数量。

实现Shallow_Q_Learner

然后,我们可以修改Q_Learner类,使用这个SLP来表示Q-函数。请注意,我们将不得不修改Q_Learner类学习(…)方法以及计算损失的梯度对得到的权重和backpropagate他们更新和优化神经网络的权值,以改善其核反应能量表示接近实际值。我们还将稍微修改get_action(…)方法,以便通过神经网络向前传递q值。下面用星号显示了Shallow_Q_Learner类的代码以及对Q_Learner类实现所做的更改,以便您一目了然地看到它们之间的差异:

import torch from function_approximator.perceptron 
import SLP
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 Shallow_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 = SLP(self.obs_shape, self.action_shape)
        self.Q_optimizer = torch.optim.Adam(self.Q.parameters(), lr=1e-5)
        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]
        td_error = torch.nn.functional.mse_loss(self.Q(obs)[action], td_target)
        #self.Q[discretized_obs][action] += self.alpha * td_error
        self.Q_optimizer.zero_grad() 
        td_error.backward() 
        self.Q_optimizer.step()

这里讨论Shallow_Q_Learner类实现,只是为了便于您理解如何实现基于神经网络的q函数近似,以取代传统的表式q学习实现。

Experience replay

在大多数环境中,代理接收到的信息不是独立的和相同分布的(i.i.d)。这意味着agent接收到的观察结果与它之前接收到的观察结果以及它将接收到的下一个观察结果紧密相关。这是可以理解的,因为通常情况下,agent在典型的强化学习环境中解决的问题是顺序的。结果表明,神经网络在样本辨识的情况下收敛效果较好。

经验重放还支持重用代理的过去经验。神经网络的更新,特别是低学习率,需要几个反向传播和优化步骤,以收敛到良好的值。利用过去的经验数据,特别是小批量更新神经网络,有助于q -网络的收敛,使其更接近真实动作值。

实现the experience memory

让我们实现一个经验记忆类来存储代理收集的经验。在此之前,让我们加深对经验的理解。在强化学习问题表示使用马尔可夫决策过程(MDP),在第二章,我们讨论了强化学习和强化学习,表示一个经验是有效的数据结构,包括观察的时间步,这个观察后的行动,该行动的奖励了,下观察(或国家)的环境转换到由于代理人的行动。还应该包含“done”布尔值,它表示这个特定的下一个观察是否标记了该集的结束。让我们使用来自collections库的Python命名元组来表示这样的数据结构,如下面的代码片段所示:

from collections import namedtuple 
Experience = namedtuple("Experience", ['obs', 'action', 'reward', 'next_obs', 'done'])

namedtuple数据结构可以方便地使用name属性(如’obs’、'action’等)而不是数字索引(如0、1等)访问元素。

现在,我们可以使用刚刚创建的体验数据结构来实现体验记忆类。为了弄清楚我们需要在体验记忆类中实现哪些方法,让我们考虑一下我们以后将如何使用它。

首先,我们希望能够在agent收集的体验记忆中存储新的体验。然后,当我们想要重放以更新q功能时,我们想要从体验记忆中批量取样或检索体验。因此,本质上,我们需要一个可以存储新体验的方法,以及一个可以对单个或一批体验进行抽样的方法。

让我们深入体验内存的实现,从初始化方法开始,我们用所需的容量初始化内存,如下所示:

class ExperienceMemory(object):
    """
    A cyclic/ring buffer based Experience Memory implementation
    """
    def __init__(self, capacity=int(1e6)):
        """

        :param capacity: Total capacity (Max number of Experiences)
        :return:
        """
        self.capacity = capacity
        self.mem_idx = 0  # Index of the current experience
        self.memory = []

mem_idx成员变量将用于指向当前的写入头或索引位置,当新体验到达时,我们将在那里存储它们。
“循环缓冲区”还有其他名称,您可能听说过:“循环缓冲区”、“环缓冲区”和“循环队列”。它们都表示相同的底层数据结构,使用类似环形的固定大小的数据表示。

接下来,我们来看看store方法的实现:

    def store(self, experience):
        """

        :param experience: The Experience object to be stored into the memory
        :return:
        """
        if self.mem_idx < self.capacity:
            # Extend the memory and create space
            self.memory.append(None)
        self.memory[self.mem_idx % self.capacity] = experience
        self.mem_idx += 1

很简单,对吧?我们将体验存储在mem_idx,就像我们讨论的那样。
下一段代码是我们的sample方法实现:

    def sample(self, batch_size):
        """

        :param batch_size:  Sample batch_size
        :return: A list of batch_size number of Experiences sampled at random from mem
        """
        assert batch_size <= len(self.memory), "Sample batch_size is more than available exp in mem"
        return random.sample(self.memory, batch_size)

在前面的代码中,我们使用Python的随机库从体验记忆中随机地统一采样体验。我们还将实现一个简单的get_size助手方法,我们将使用它来找出在经验记忆中已经存储了多少经验:

    def get_size(self):
        """

        :return: Number of Experiences stored in the memory
        """
        return len(self.memory)

接下来,我们将看看如何回放从体验记忆中采样的体验,以更新代理的q -函数。

给Q-learner类实现the replay experience方法

因此,我们为代理实现了一个存储系统,使用一个整洁的循环缓冲区来存储它过去的经验。在这一节中,我们将看看如何在q学习者的课堂中使用体验记忆来重放体验。

下面的代码片段实现了replay_experience方法,该方法展示了我们如何从经验记忆中取样,并调用即将实现的方法,让代理从取样的一批经验中学习:

    def replay_experience(self, batch_size = None):
        batch_size = batch_size if batch_size is not None else self.params['replay_batch_size']
        experience_batch = self.memory.sample(batch_size)
        self.learn_from_batch_experience(experience_batch)
        self.training_steps_completed += 1  # Increment the number of training batch steps complemented

在像SARSA这样的在线学习方法中,行为值评估在agent与环境交互的每一步之后都会更新。通过这种方式,更新将传播代理刚刚经历的信息。如果agent不经常经历一些事情,这些更新可能会让agent忘记这些经历,当agent在将来遇到类似情况时,可能会导致糟糕的性能。这是不可取的,特别是对于有许多参数(或权值),需要调整到正确的值集的神经网络。这是使用经验记忆的主要动机之一,并在更新Q动作价值评估时重现过去的经验。现在我们将实现learn_from_batch_experience方法,该方法扩展了我们在前一章中实现的learn方法,以便从一批经验中学习,而不是从单一的经验中学习。下面是该方法的实现:

  def learn_from_batch_experience(self, experiences):
        batch_xp = Experience(*zip(*experiences))
        obs_batch = np.array(batch_xp.obs) / 255.0  # Scale/Divide by max limit of obs's dtype. 255 for uint8
        action_batch = np.array(batch_xp.action)
        reward_batch = np.array(batch_xp.reward)
        # Clip the rewards
        if self.params["clip_rewards"]:
            reward_batch = np.sign(reward_batch)
        next_obs_batch = np.array(batch_xp.next_obs) / 255.0  # Scale/Divide by max limit of obs' dtype. 255 for uint8
        done_batch = np.array(batch_xp.done)

        if self.params['use_target_network']:
            #if self.training_steps_completed % self.params['target_network_update_freq'] == 0:
            if self.step_num % self.params['target_network_update_freq'] == 0:
                # The *update_freq is the Num steps after which target net is updated.
                # A schedule can be used instead to vary the update freq.
                self.Q_target.load_state_dict(self.Q.state_dict())
            td_target = reward_batch + ~done_batch * \
                np.tile(self.gamma, len(next_obs_batch)) * \
                self.Q_target(next_obs_batch).max(1)[0].data.cpu().numpy()
        else:
            td_target = reward_batch + ~done_batch * \
                np.tile(self.gamma, len(next_obs_batch)) * \
                self.Q(next_obs_batch).detach().max(1)[0].data.cpu().numpy()

        td_target = torch.from_numpy(td_target).to(device)
        action_idx = torch.from_numpy(action_batch).to(device)
        td_error = torch.nn.functional.mse_loss( self.Q(obs_batch).gather(1, action_idx.view(-1, 1)),
                                                       td_target.float().unsqueeze(1))

        self.Q_optimizer.zero_grad()
        td_error.mean().backward()
        writer.add_scalar("DQL/td_error", td_error.mean(), self.step_num)
        self.Q_optimizer.step()

该方法接收一批(或小批)的经验,首先分别提取观察批、行动批、奖励批和下一批观察批,以便在后续步骤中单独使用它们。done_batch表示每一个经历下一个观察是否为一个插曲的结束。然后,我们计算时间差异(TD)误差与最大行动,这是q学习目标。注意,我们将td_target计算中的第二项与~done_batch相乘。

它负责为终端状态指定零值。因此,如果next_obs_batch中的一个特定的next_obs是终端,那么第二项将变成0,结果就是td_target = rewards_batch。

然后我们计算td_target(目标q值)和q网络预测的q值之间的均方误差。我们将此误差作为引导信号,并将其反向传播到神经网络中的所有节点,然后进行优化步骤更新参数/权值以使误差最小化。

回顾一下贪心行为策略

在前一章中,我们讨论了贪婪的行动选择策略,该策略根据代理的行动值估计以1-e的概率采取最佳行动,并以epsilon给出的概率采取随机行动,。 Epsilon是一个超参数,可以根据实验调整到一个很好的值。 一个较高的值意味着代理的行为将是随机的,一个较低的值意味着代理的行为将更有可能利用它已经知道的环境,而不会试图探索。 我应该通过采取从未尝试过的行动来探索更多吗? 或者我应该利用我已经知道的东西,并对我的知识采取最好的行动,这可能是有限的? 这就是强化学习体所面临的探索开发困境。

直观地说,在代理学习过程的初始阶段,有一个非常高的值(最大值为1.0)是很有帮助的,这样代理就可以通过采取大多数随机动作来探索状态空间。 一旦它获得了足够的经验,并对环境有了更好的理解,降低价值就会让代理更经常地根据它认为是最好的行为采取行动。 它将是有用的,有一个实用功能,照顾不同的价值,对吗? 让我们在下一节中实现这样的函数。

实现epsilon衰变时间表

我们可以线性地(或减少)该值(在下面的左侧图中)、指数地(在下面的右侧图中)或使用其他一些衰减计划。 线性和指数附表是勘探参数最常用的衰减附表
在这里插入图片描述
在前面的图中,您可以看到epsilon(探索)值是如何随着不同的调度方案而变化的(左图上是线性的,右图上是指数的)。 在前面的图表中显示的衰减时间表使用epsilon_max(开始)值为1,线性情况下epsilon_min(最终)值为0.01,指数情况下使用exp(-1000000/2000),两者在10000次事件后都保持epsilon_min的恒定值。

下面的代码实现了线性衰减时间表,我们将用于我们的Deep_Q_Learning代理实现来玩Atari游戏:

class LinearDecaySchedule(object):
    def __init__(self, initial_value, final_value, max_steps):
        assert initial_value > final_value, "initial_value should be > final_value"
        self.initial_value = initial_value
        self.final_value = final_value
        self.decay_factor = (initial_value - final_value) / max_steps

    def __call__(self, step_num):
        current_value = self.initial_value - self.decay_factor * step_num
        if current_value < self.final_value:
            current_value = self.final_value
        return current_value

if __name__ == "__main__":
    import matplotlib.pyplot as plt
    epsilon_initial = 1.0
    epsilon_final = 0.05
    MAX_NUM_EPISODES = 10000
    MAX_STEPS_PER_EPISODE = 300
    linear_sched = LinearDecaySchedule(initial_value = epsilon_initial,
                                    final_value = epsilon_final,
                                    max_steps = MAX_NUM_EPISODES * MAX_STEPS_PER_EPISODE)
    epsilon = [linear_sched(step) for step in range(MAX_NUM_EPISODES * MAX_STEPS_PER_EPISODE)]
    plt.plot(epsilon)
    plt.show()

实现深度Q学习代理

在本节中,我们将讨论如何将我们的浅Q学习器扩展到一个更复杂和强大的基于深度Q学习器的代理,该代理可以学习基于原始视觉图像输入的行为,我们将在本章的末尾使用该代理来训练那些玩好Atari游戏的代理。 请注意,您可以在任何具有离散动作空间的学习环境中训练这种深度Q学习代理。 Atari游戏环境是我们将在本书中使用的一类有趣的环境。

我们将从一个深卷积Q网络实现开始,并将其纳入我们的Q学习。 然后,我们将看到如何使用目标Q网络技术来提高深度Q学习者的稳定性。 然后,我们将结合我们到目前为止讨论的所有技术,将我们的深度Q学习代理的全面实现结合起来。

在PyTorch中实现一个深度卷积Q网络

让我们实现一个3层深度卷积神经网络(CNN),它以Atari游戏屏幕像素为输入,并输出该特定游戏的每个可能动作的动作值,这是在OpenAIGym环境中定义的。 以下代码是CNN类的代码:

import torch


class CNN(torch.nn.Module):
    def __init__(self, input_shape, output_shape, device=torch.device("cpu")):
        """
        A Convolution Neural Network (CNN) class to approximate functions with visual/image inputs

        :param input_shape:  Shape/dimension of the input image. Assumed to be resized to C x 84 x 84
        :param output_shape: Shape/dimension of the output.
        :param device: The device (cpu or cuda) that the CNN should use to store the inputs for the forward pass
        """
        #  input_shape: C x 84 x 84
        super(CNN, self).__init__()
        self.device = device
        self.layer1 = torch.nn.Sequential(
            torch.nn.Conv2d(input_shape[0], 32, kernel_size=8, stride=4, padding=0),
            torch.nn.ReLU()
        )
        self.layer2 = torch.nn.Sequential(
            torch.nn.Conv2d(32, 64, kernel_size=3, stride=2, padding=0),
            torch.nn.ReLU()
        )
        self.layer3 = torch.nn.Sequential(
            torch.nn.Conv2d(64, 64, kernel_size=3, stride=1, padding=0),
            torch.nn.ReLU()
        )
        self.out = torch.nn.Linear(64 * 7 * 7, output_shape)

正如您所看到的,很容易向神经网络添加更多的层。 我们可以使用一个更深的网络,它有三层以上,但它将以需要更多的计算能力和时间为代价。 在深度强化学习中,特别是在函数逼近的Q学习中,没有证明的收敛保证。 因此,我们应该确保我们的代理的实现足够好,以便在使用更深的神经网络来提高Q/value-函数表示的容量之前很好地学习和取得进展。

使用目标Q网络来稳定代理的学习

一种简单的冻结Q网络的固定步骤,然后使用它生成Q学习目标来更新深Q网络的参数的技术被证明是相当有效的减少振荡和稳定Q学习与神经网络近似。 这种技术相对简单,但它对稳定的学习非常有帮助。

实现将是简单明了的。 我们必须对我们现有的深度Q学习类进行两个更改或更新:

  • 创建一个目标Q网络,并定期与原始Q网络同步/更新它
  • 使用目标Q网络生成Q学习目标

为了比较代理在目标Q网络和不使用目标Q网络的情况下是如何执行的,您可以使用我们在本章前面的章节中开发的参数管理器和日志记录和可视化工具验证启用目标Q网络的性能增益。

我们将首先添加一个新的类成员,名为Q_target,我们可以添加到我们的深度Q学习类的__init__方法中。 下面的代码片段显示了代码行,我们在前面声明Self之后添加新成员。 deep_Q_learner.py脚本中的DQN:
在这里插入图片描述
然后,我们可以修改我们前面实现的learn_from_batch_experience方法,使用目标Q网络创建Q学习目标。 下面的代码片段显示了我们第一个实现中粗体字体的更改:

    def learn_from_batch_experience(self, experiences):
        batch_xp = Experience(*zip(*experiences))
        obs_batch = np.array(batch_xp.obs) / 255.0  # Scale/Divide by max limit of obs's dtype. 255 for uint8
        action_batch = np.array(batch_xp.action)
        reward_batch = np.array(batch_xp.reward)
        # Clip the rewards
        if self.params["clip_rewards"]:
            reward_batch = np.sign(reward_batch)
        next_obs_batch = np.array(batch_xp.next_obs) / 255.0  # Scale/Divide by max limit of obs' dtype. 255 for uint8
        done_batch = np.array(batch_xp.done)

        if self.params['use_target_network']:
            #if self.training_steps_completed % self.params['target_network_update_freq'] == 0:
            if self.step_num % self.params['target_network_update_freq'] == 0:
                # The *update_freq is the Num steps after which target net is updated.
                # A schedule can be used instead to vary the update freq.
                self.Q_target.load_state_dict(self.Q.state_dict())
            td_target = reward_batch + ~done_batch * \
                np.tile(self.gamma, len(next_obs_batch)) * \
                self.Q_target(next_obs_batch).max(1)[0].data.cpu().numpy()
        else:
            td_target = reward_batch + ~done_batch * \
                np.tile(self.gamma, len(next_obs_batch)) * \
                self.Q(next_obs_batch).detach().max(1)[0].data.cpu().numpy()

        td_target = torch.from_numpy(td_target).to(device)
        action_idx = torch.from_numpy(action_batch).to(device)
        td_error = torch.nn.functional.mse_loss( self.Q(obs_batch).gather(1, action_idx.view(-1, 1)),
                                                       td_target.float().unsqueeze(1))

        self.Q_optimizer.zero_grad()
        td_error.mean().backward()
        writer.add_scalar("DQL/td_error", td_error.mean(), self.step_num)
        self.Q_optimizer.step()

这就完成了我们的目标Q-网络实现。

我们如何知道代理是否受益于我们在前面章节中讨论的目标Q网络和其他改进? 在下一节中,我们将研究如何记录和可视化代理的性能,以便我们可以监视和计算我们讨论的改进是否实际导致更好的性能。

记录和可视化代理的学习过程

我们现在有一个学习代理,它使用神经网络来学习Q值并更新自己,以更好地完成任务。 代理人需要一段时间才能学会明智的行事。 我们怎么知道探员在一个特定的时间发生了什么? 我们怎么知道代理人是在进步还是只是装傻? 我们如何看待和衡量代理的进度与时间? 我们应该坐着等训练结束吗? 不。 你不觉得应该有更好的办法吗?

是的,还有! 实际上,对于我们,代理的开发人员来说,能够观察代理是如何执行的,以便弄清楚实现是否有问题,或者某些超参数是否对代理学习任何东西太差,这一点非常重要。 我们已经有了日志记录的初步版本,并看到了代理的学习是如何与使用print语句生成的控制台输出一起进行的。 这让我们第一手地了解了集号、集奖励、最大奖励等等,但它更像是一个给定时间的快照。 我们希望能够看到进展的历史,看看代理的学习是否会随着学习误差的减少而收敛,等等。 这将使我们能够朝着正确的方向思考,更新我们的实现或调整参数,以提高代理的学习性能。

TensorFlow深度学习库提供了一个名为TensorBoard的工具。 它是一个强大的工具来可视化神经网络图,绘制定量指标,如学习错误,奖励,等等,随着训练的进展。 它甚至可以用来可视化图像和其他一些有用的数据类型。 它使我们的深度学习算法实现更容易理解、识别和调试。 在下一节中,我们将看到如何使用TensorBoard来记录和可视化代理的进度。

使用tensorboard和可视化PyTorch RL代理的进度

虽然TensorBoard是TensorFlow深度学习图书馆发布的工具,但它本身是一种灵活的工具,可以与PyTorch等其他深度学习图书馆一起使用。 基本上,TensorBoard工具从日志文件中读取TensorFlow事件摘要数据,并定期更新可视化和绘图。 幸运的是,我们有一个名为tensorboardX的库,它提供了一个方便的接口来创建Tensorboard可以处理的事件。 这样,我们就可以很容易地从代理培训代码中生成适当的事件,以便记录和可视化代理的学习过程是如何进行的。 这个库的使用非常简单明了。 我们导入tensorboardX,并创建一个具有所需日志文件名的摘要写入对象。 然后,我们可以使用Summary Writer对象添加新的标量(以及其他受支持的数据),将新的数据点添加到将定期更新的绘图中。 下面的屏幕截图是一个例子,说明TensorBoard的输出将是什么样的,我们将在代理培训脚本中登录这样的信息,以可视化它的进展:
在这里插入图片描述
在前面的截图中,右下角最大的标题为main/mean_ep_reward的情节显示了代理是如何随着时间的推移逐步获得更高和更高的奖励的。 在前面截图中的所有绘图中,x轴显示训练步骤的数量,y轴具有记录的数据的值,这是由每个绘图的标题所表示的。

现在,我们知道如何记录和可视化代理的性能,因为它是培训。 但是,仍然存在一个问题,即我们如何将代理与本章前面章节中讨论的一个或多个改进进行比较。 我们讨论了几个改进,每个改进都增加了新的超参数。 为了管理各种超参数,并方便地打开和关闭改进和配置,在下一节中,我们将讨论如何通过构建一个简单的参数管理类来实现这一点。

管理超参数和配置参数

正如您可能注意到的,我们的代理有几个超参数,如学习速率、伽马、epsilon启动/最小值等。 对于代理和环境,也有几个配置参数,我们希望能够轻松地修改和运行,而不是搜索代码来查找该参数的定义位置。 有一个简单而好的方法来管理这些参数也有助于我们想要自动化培训过程或运行参数扫描或其他方法来调优和找到对代理工作的最佳参数集。

在下面的两个小节中,我们将研究如何使用JSON文件以易于使用的方式指定参数和超参数,并实现一个参数管理器类来处理这些外部可配置的参数以更新代理和环境配置。

使用JSON文件轻松配置参数

在实现我们的参数管理器之前,让我们了解一下我们的参数配置JSON文件什么样子。 下面是参数.json文件的一个片段,我们将使用它来配置代理和环境的参数。 Java Script Object Notation(JSON)文件是一种方便的、可读的数据表示格式。 我们将在本章后面的章节中讨论这些参数的含义。 目前,我们将集中讨论如何使用这样的文件来指定或更改代理和环境使用的参数:
在这里插入图片描述

参数管理器

您喜欢刚才看到的参数配置文件示例吗? 希望你做到了。 在本节中,我们将实现一个参数管理器,它将帮助我们加载、获取和设置这些参数。

我们将首先创建一个名为ParamsManger的Python类,它使用JSONPython库从params_file读取的参数字典初始化Params成员变量,如下所示:
在这里插入图片描述
然后,我们将实施一些对我们方便的方法。 我们将从返回从JSON文件中读取的参数的整个字典的get_params方法开始:
在这里插入图片描述
有时,我们可能只想得到与代理对应的参数或与我们初始化代理或环境时可以传入的环境对应的参数。 由于我们在上一节中看到的参数.json文件中整齐地分离了代理和环境参数,实现非常简单,如下所示:

def get_env_params(self):
	 """ 
	 Returns the environment configuration parameters 
	 :return: A dictionary of configuration parameters used for the environment 
	 """ 
	 return self.params['env'] 
def get_agent_params(self):
	""" 
	Returns the hyper-parameters and configuration parameters used by the agent 
	:return: A dictionary of parameters used by the agent
	 """ 
	return self.params['agent']

我们还将实现另一种更新代理参数的简单方法,以便在启动培训脚本时也可以从命令行提供/读取代理参数:
在这里插入图片描述
前面的参数管理器实现以及一个简单的测试过程可以在本书的代码存储库中的ch6/utils/params_manager.py上获得。 在下一节中,我们将巩固到目前为止我们讨论和实现的所有技术,以建立一个完整的基于深度Q学习的代理。

一个完整的深度Q学习器,以解决复杂的问题与原始像素输入

从本章开始,我们就实现了几种额外的技术和实用工具来改进代理。 在这一节中,我们将把我们迄今讨论过的所有改进和实用工具合并成一个统一的deep_Q_Learner.py脚本。 我们将使用这个统一的代理脚本在下一节中对Atari Gym的环境进行培训,并观察代理提高其性能,并随着时间的推移获得越来越多的分数。

下面的代码是使用我们在本章前面几节中开发的以下特性的统一版本:

  • Experience memory
  • Experience replay to learn from (mini) batches of experience
  • Linear epsilon decay schedule
  • Target network for stable learning
  • Parameter management using JSON files
  • Performance visualization and logging using TensorBoard
from datetime import datetime
from argparse import ArgumentParser
import gym
import torch
import random
import numpy as np

import environment.atari as Atari
import environment.utils as env_utils
from utils.params_manager import ParamsManager
from utils.decay_schedule import LinearDecaySchedule
from utils.experience_memory import Experience, ExperienceMemory
import utils.weights_initializer
from function_approximator.perceptron import SLP
from function_approximator.cnn import CNN
from tensorboardX import SummaryWriter

args = ArgumentParser("deep_Q_learner")
args.add_argument("--params-file", help="Path to the parameters json file. Default is parameters.json",
                  default="parameters.json", metavar="PFILE")
args.add_argument("--env", help="ID of the Atari environment available in OpenAI Gym.Default is SeaquestNoFrameskip-v4",
                  default="SeaquestNoFrameskip-v4", metavar="ENV")
args.add_argument("--gpu-id", help="GPU device ID to use. Default=0", default=0, type=int, metavar="GPU_ID")
args.add_argument("--render", help="Render environment to Screen. Off by default", action="store_true", default=False)
args.add_argument("--test", help="Test mode. Used for playing without learning. Off by default", action="store_true",
                  default=False)
args.add_argument("--record", help="Enable recording (video & stats) of the agent's performance",
                  action="store_true", default=False)
args.add_argument("--recording-output-dir", help="Directory to store monitor outputs. Default=./trained_models/results",
                  default="./trained_models/results")
args = args.parse_args()

params_manager= ParamsManager(args.params_file)
seed = params_manager.get_agent_params()['seed']
summary_file_path_prefix = params_manager.get_agent_params()['summary_file_path_prefix']
summary_file_path= summary_file_path_prefix + args.env+ "_" + datetime.now().strftime("%y-%m-%d-%H-%M")
writer = SummaryWriter(summary_file_path)
# Export the parameters as json files to the log directory to keep track of the parameters used in each experiment
params_manager.export_env_params(summary_file_path + "/" + "env_params.json")
params_manager.export_agent_params(summary_file_path + "/" + "agent_params.json")
global_step_num = 0
use_cuda = params_manager.get_agent_params()['use_cuda']
# new in PyTorch 0.4
device = torch.device("cuda:" + str(args.gpu_id) if torch.cuda.is_available() and use_cuda else "cpu")
torch.manual_seed(seed)
np.random.seed(seed)
if torch.cuda.is_available() and use_cuda:
    torch.cuda.manual_seed_all(seed)


class Deep_Q_Learner(object):
    def __init__(self, state_shape, action_shape, params):
        """
        self.Q is the Action-Value function. This agent represents Q using a Neural Network
        If the input is a single dimensional vector, uses a Single-Layer-Perceptron else if the input is 3 dimensional
        image, use a Convolutional-Neural-Network
        :param state_shape: Shape (tuple) of the observation/state
        :param action_shape: Shape (number) of the discrete action space
        :param params: A dictionary containing various Agent configuration parameters and hyper-parameters
        """
        self.state_shape = state_shape
        self.action_shape = action_shape
        self.params = params
        self.gamma = self.params['gamma']  # Agent's discount factor
        self.learning_rate = self.params['lr']  # Agent's Q-learning rate
        self.best_mean_reward = - float("inf") # Agent's personal best mean episode reward
        self.best_reward = - float("inf")
        self.training_steps_completed = 0  # Number of training batch steps completed so far

        if len(self.state_shape) == 1:  # Single dimensional observation/state space
            self.DQN = SLP
        elif len(self.state_shape) == 3:  # 3D/image observation/state
            self.DQN = CNN

        self.Q = self.DQN(state_shape, action_shape, device).to(device)
        self.Q.apply(utils.weights_initializer.xavier)

        self.Q_optimizer = torch.optim.Adam(self.Q.parameters(), lr=self.learning_rate)
        if self.params['use_target_network']:
            self.Q_target = self.DQN(state_shape, action_shape, device).to(device)
        # self.policy is the policy followed by the agent. This agents follows
        # an epsilon-greedy policy w.r.t it's Q estimate.
        self.policy = self.epsilon_greedy_Q
        self.epsilon_max = params["epsilon_max"]
        self.epsilon_min = params["epsilon_min"]
        self.epsilon_decay = LinearDecaySchedule(initial_value=self.epsilon_max,
                                    final_value=self.epsilon_min,
                                    max_steps= self.params['epsilon_decay_final_step'])
        self.step_num = 0

        self.memory = ExperienceMemory(capacity=int(self.params['experience_memory_capacity']))  # Initialize an Experience memory with 1M capacity

    def get_action(self, observation):
        observation = np.array(observation)  # Observations could be lazy frames. So force fetch before moving forward
        observation = observation / 255.0  # Scale/Divide by max limit of obs' dtype. 255 for uint8
        if len(observation.shape) == 3: # Single image (not a batch)
            if observation.shape[2] < observation.shape[0]:  # Probably observation is in W x H x C format
                # NOTE: This is just an additional check. The env wrappers are taking care of this conversion already
                # Reshape to C x H x W format as per PyTorch's convention
                observation = observation.reshape(observation.shape[2], observation.shape[1], observation.shape[0])
            observation = np.expand_dims(observation, 0)  # Create a batch dimension
        return self.policy(observation)

    def epsilon_greedy_Q(self, observation):
        # Decay Epsilon/exploration as per schedule
        writer.add_scalar("DQL/epsilon", self.epsilon_decay(self.step_num), self.step_num)
        self.step_num +=1
        if random.random() < self.epsilon_decay(self.step_num) and not self.params["test"]:
            action = random.choice([i for i in range(self.action_shape)])
        else:
            action = np.argmax(self.Q(observation).data.to(torch.device('cpu')).numpy())
        return action

    def learn(self, s, a, r, s_next, done):
        # TD(0) Q-learning
        if done:  # End of episode
            td_target = reward + 0.0  # Set the value of terminal state to zero
        else:
            td_target = r + self.gamma * torch.max(self.Q(s_next))
        td_error = td_target - self.Q(s)[a]
        # Update Q estimate
        #self.Q(s)[a] = self.Q(s)[a] + self.learning_rate * td_error
        self.Q_optimizer.zero_grad()
        td_error.backward()
        self.Q_optimizer.step()

    def learn_from_batch_experience(self, experiences):
        batch_xp = Experience(*zip(*experiences))
        obs_batch = np.array(batch_xp.obs) / 255.0  # Scale/Divide by max limit of obs's dtype. 255 for uint8
        action_batch = np.array(batch_xp.action)
        reward_batch = np.array(batch_xp.reward)
        # Clip the rewards
        if self.params["clip_rewards"]:
            reward_batch = np.sign(reward_batch)
        next_obs_batch = np.array(batch_xp.next_obs) / 255.0  # Scale/Divide by max limit of obs' dtype. 255 for uint8
        done_batch = np.array(batch_xp.done)

        if self.params['use_target_network']:
            #if self.training_steps_completed % self.params['target_network_update_freq'] == 0:
            if self.step_num % self.params['target_network_update_freq'] == 0:
                # The *update_freq is the Num steps after which target net is updated.
                # A schedule can be used instead to vary the update freq.
                self.Q_target.load_state_dict(self.Q.state_dict())
            td_target = reward_batch + ~done_batch * \
                np.tile(self.gamma, len(next_obs_batch)) * \
                self.Q_target(next_obs_batch).max(1)[0].data.cpu().numpy()
        else:
            td_target = reward_batch + ~done_batch * \
                np.tile(self.gamma, len(next_obs_batch)) * \
                self.Q(next_obs_batch).detach().max(1)[0].data.cpu().numpy()

        td_target = torch.from_numpy(td_target).to(device)
        action_idx = torch.from_numpy(action_batch).to(device)
        td_error = torch.nn.functional.mse_loss( self.Q(obs_batch).gather(1, action_idx.view(-1, 1)),
                                                       td_target.float().unsqueeze(1))

        self.Q_optimizer.zero_grad()
        td_error.mean().backward()
        writer.add_scalar("DQL/td_error", td_error.mean(), self.step_num)
        self.Q_optimizer.step()

    def replay_experience(self, batch_size = None):
        batch_size = batch_size if batch_size is not None else self.params['replay_batch_size']
        experience_batch = self.memory.sample(batch_size)
        self.learn_from_batch_experience(experience_batch)
        self.training_steps_completed += 1  # Increment the number of training batch steps complemented

    def save(self, env_name):
        file_name = self.params['save_dir'] + "DQL_" + env_name + ".ptm"
        agent_state = {
    
    "Q": self.Q.state_dict(),
                       "best_mean_reward": self.best_mean_reward,
                       "best_reward": self.best_reward};
        torch.save(agent_state, file_name)
        print("Agent's state saved to ", file_name)

    def load(self, env_name):
        file_name = self.params['load_dir'] + "DQL_" + env_name + ".ptm"
        agent_state = torch.load(file_name, map_location= lambda storage, loc: storage)
        self.Q.load_state_dict(agent_state["Q"])
        self.Q.to(device)
        self.best_mean_reward = agent_state["best_mean_reward"]
        self.best_reward = agent_state["best_reward"]
        print("Loaded Q model state from", file_name,
              " which fetched a best mean reward of:", self.best_mean_reward,
              " and an all time best reward of:", self.best_reward)


if __name__ == "__main__":
    env_conf = params_manager.get_env_params()
    env_conf["env_name"] = args.env
    # In test mode, let the end of the game be the end of episode rather than ending episode at the end of every life.
    # This helps to report out the (mean and max) episode rewards per game (rather than per life!)
    if args.test:
        env_conf["episodic_life"] = False
    # Specify the reward calculation type used for printing stats at the end of every episode.
    # If "episode_life" is true, the printed stats (reward, mean reward, max reward) are per life. If "episodic_life"
    # is false, the printed stats/scores are per game in Atari environments
    rew_type = "LIFE" if env_conf["episodic_life"] else "GAME"

    # If a custom useful_region configuration for this environment ID is available, use it if not use the Default
    custom_region_available = False
    for key, value in env_conf['useful_region'].items():
        if key in args.env:
            env_conf['useful_region'] = value
            custom_region_available = True
            break
    if custom_region_available is not True:
        env_conf['useful_region'] = env_conf['useful_region']['Default']

    print("Using env_conf:", env_conf)
    atari_env = False
    for game in Atari.get_games_list():
        if game.replace("_", "") in args.env.lower():
            atari_env = True
    if atari_env:
        env = Atari.make_env(args.env, env_conf)
    else:
        print("Given environment name is not an Atari Env. Creating a Gym env")
        # Resize the obs to w x h (84 x 84 by default) and then reshape it to be in the C x H x W format
        env = env_utils.ResizeReshapeFrames(gym.make(args.env))

    if args.record:  # If monitor is enabled, record stats and video of agent's performance
        env = gym.wrappers.Monitor(env, args.recording_output_dir, force=True)

    observation_shape = env.observation_space.shape
    action_shape = env.action_space.n
    agent_params = params_manager.get_agent_params()
    agent_params["test"] = args.test
    agent = Deep_Q_Learner(observation_shape, action_shape, agent_params)

    episode_rewards = list()
    prev_checkpoint_mean_ep_rew = agent.best_mean_reward
    num_improved_episodes_before_checkpoint = 0  # To keep track of the num of ep with higher perf to save model
    print("Using agent_params:", agent_params)
    if agent_params['load_trained_model']:
        try:
            agent.load(env_conf["env_name"])
            prev_checkpoint_mean_ep_rew = agent.best_mean_reward
        except FileNotFoundError:
            print("WARNING: No trained model found for this environment. Training from scratch.")

    #for episode in range(agent_params['max_num_episodes']):
    episode = 0
    while global_step_num <= agent_params['max_training_steps']:
        obs = env.reset()
        cum_reward = 0.0  # Cumulative reward
        done = False
        step = 0
        #for step in range(agent_params['max_steps_per_episode']):
        while not done:
            if env_conf['render'] or args.render:
                env.render()
            action = agent.get_action(obs)
            next_obs, reward, done, info = env.step(action)
            #agent.learn(obs, action, reward, next_obs, done)
            agent.memory.store(Experience(obs, action, reward, next_obs, done))

            obs = next_obs
            cum_reward += reward
            step += 1
            global_step_num +=1

            if done is True:
                episode += 1
                episode_rewards.append(cum_reward)
                if cum_reward > agent.best_reward:
                    agent.best_reward = cum_reward
                if np.mean(episode_rewards) > prev_checkpoint_mean_ep_rew:
                    num_improved_episodes_before_checkpoint += 1
                if num_improved_episodes_before_checkpoint >= agent_params["save_freq_when_perf_improves"]:
                    prev_checkpoint_mean_ep_rew = np.mean(episode_rewards)
                    agent.best_mean_reward = np.mean(episode_rewards)
                    agent.save(env_conf['env_name'])
                    num_improved_episodes_before_checkpoint = 0
                print("\nEpisode#{} ended in {} steps. Per {} stats: reward ={} ; mean_reward={:.3f} best_reward={}".
                      format(episode, step+1, rew_type, cum_reward, np.mean(episode_rewards), agent.best_reward))
                writer.add_scalar("main/ep_reward", cum_reward, global_step_num)
                writer.add_scalar("main/mean_ep_reward", np.mean(episode_rewards), global_step_num)
                writer.add_scalar("main/max_ep_rew", agent.best_reward, global_step_num)
                # Learn from batches of experience once a certain amount of xp is available unless in test only mode
                if agent.memory.get_size() >= 2 * agent_params['replay_start_size'] and not args.test:
                    agent.replay_experience()

                break
    env.close()
    writer.close()

前面的代码,以及使用我们将在下一节中讨论的Atari包装器所需的一些额外更改,可在本书的代码存储库中的ch6/deep_Q_Learner.py上查阅。 在我们完成关于Atari Gym环境的下一节之后,我们将在deep_Q_Learner.py中使用代理实现来训练Atari游戏中的代理,并最终看到它们的性能。

Atari Gym 环境

在第四章“探索健身房及其功能”中,我们查看了健身房中可用的各种环境列表,包括Atari游戏类别,并使用脚本列出了计算机上可用的所有健身房环境。 我们还研究了环境名称的命名,特别是对于Atari游戏。 在本节中,我们将使用Atari环境,并了解如何使用Gym环境包装器自定义环境。 以下是9个不同Atari环境的9张截图的拼贴:
在这里插入图片描述

定制Atari Gym环境

有时,我们可能想改变观察被环境发回的方式,或者改变奖励的规模,这样我们的代理就可以更好地学习或过滤掉一些信息,然后代理才收到它们,或者改变环境在屏幕上呈现的方式。 到目前为止,我们一直在开发和定制我们的代理,使其在环境中发挥良好的作用。 在环境如何以及如何发送回代理的问题上有一些灵活性是不是很好,这样我们就可以自定义代理如何学习操作了? 幸运的是,在Gym环境包装器的帮助下,Gym库使环境发送的信息易于扩展或自定义。 包装器接口允许我们在以前的例程之上以层的形式子类和添加例程。 我们可以将自定义处理语句添加到Gym环境类的以下一个或多个方法中:

在这里插入图片描述
根据我们希望对环境进行的定制,我们可以决定要扩展哪些方法。 例如,如果我们想改变观察的形状/大小,我们可以扩展_step和_reset方法。 在下一小节中,我们将看到如何使用包装器接口自定义Atari Gym环境。

实现自定义Gym环境包装

在这一节中,我们将看到一些对Gym Atari环境特别有用的Gym环境包装。 我们将在本节中实现的大多数包装器也可以与其他环境一起使用,以提高代理的学习性能。

下表提到了将在下一节中实现的包装器列表,并简要描述了每个包装器,以给您一个概述:

在这里插入图片描述

奖励剪报

不同的问题或环境提供不同范围的奖励值。 例如,我们在上一章中看到,在MountainCarv0环境中,代理每次步骤都得到-1的奖励,直到插曲终止,无论代理以何种方式移动汽车。 在CartPolev0环境中,代理每次步骤获得1的奖励,直到插曲终止。 在像MS Pac-Man这样的Atari游戏环境中,如果代理吃掉一个鬼魂,它将得到多达1600的奖励。 我们可以开始看到奖励的大小以及奖励的场合在不同的环境和学习问题上有很大的不同。 如果我们的深度Q学习代理算法必须解决这些问题,而不试图微调超参数以独立地为每个环境工作,我们必须对不同的奖励尺度做一些事情。 这正是奖励裁剪背后的直觉,在这种直觉中,我们将奖励剪辑为-1、0或1,这取决于从环境中获得的实际奖励的标志。 这样,我们就限制了奖励的大小,这些奖励可以在不同的环境中变化很大。 我们可以实现这种简单的奖励裁剪技术,并通过继承从健身房应用到我们的环境。 奖励Wrapper类并修改奖励(…)函数,如下代码片段所示:

在这里插入图片描述

将奖励裁剪到(-1,0,1)的技术对Atari游戏很有效。 但是,很高兴知道,这可能不是普遍处理不同奖励大小和频率的环境的最佳技术。 裁剪奖励值改变了代理的学习目标,有时可能导致代理学习的策略与期望的不同。

预处理Atari屏幕图像帧

Atari Gym环境产生的观测结果通常具有210x160x3的形状,它表示宽度为210像素和高度为160像素的RGB(颜色)图像。 虽然在210x160x3的原始分辨率下的彩色图像具有更多的像素,因此具有更多的信息,但事实证明,通常情况下,随着分辨率的降低,更好的性能是可能的。 较低的分辨率意味着代理在每一步处理的数据较少,这意味着更快的培训时间,特别是在您和我拥有的消费级计算硬件上。

让我们创建一个预处理管道,它将获取原始观测图像(Atari屏幕)并执行以下操作:在这里插入图片描述
我们可以在屏幕上找出没有任何关于代理环境的有用信息的区域。

最后,我们调整图像的尺寸为84x84。 我们可以选择一个不同的数字,除了84,只要它包含合理的像素数量。 然而,有一个平方矩阵(如84x84或80x80)是有效的,因为卷积操作(例如,与CUDA)被优化为这样的平方输入:

def process_frame_84(frame, conf):
    frame = frame[conf["crop1"]:conf["crop2"] + 160, :160]
    frame = frame.mean(2)
    #frame = frame.astype(np.float32)
    #frame *= (1.0 / 255.0)
    frame = cv2.resize(frame, (84, conf["dimension2"]))
    frame = cv2.resize(frame, (84, 84))
    frame = np.reshape(frame, [1, 84, 84])
    return frame


class AtariRescale(gym.ObservationWrapper):
    def __init__(self, env, env_conf):
        gym.ObservationWrapper.__init__(self, env)
        self.observation_space = Box(0, 255, [1, 84, 84], dtype=np.uint8)
        self.conf = env_conf

    def observation(self, observation):
        return process_frame_84(observation, self.conf)

请注意,对于一个数据类型为numpy.Float32的观测帧,分辨率为84x84像素,需要4字节,我们需要大约4x84x84=28,224字节。 从体验内存部分可以回忆到,一个体验对象包含两个帧(一个用于观察,另一个用于下一个观察),这意味着我们需要2x28,224=56,448字节(2个字节用于操作4字节用于奖励)。 这56,448个字节(或0.056448MB)可能看起来不多,但如果您考虑到使用1e6(百万)顺序的经验内存容量是典型的,您可能会意识到我们需要大约1e6x0.056448MB=56,448MB或56.448GB! 这意味着我们将需要56.448GB的RAM,仅用于容量为100万次体验的体验内存!

您可以进行几次内存优化,以减少培训代理所需的RAM。 使用较小的体验内存是减少某些游戏中内存占用的一种简单方法。 在某些环境中,拥有更大的经验内存将帮助代理更快地学习。 一种减少内存占用的方法是在存储时不缩放帧(除以255),这需要浮点表示(numpy.Float32),而是将帧存储为numpy.uint8,这样我们只需要1字节而不是每像素4字节,这将有助于将内存需求减少4倍。 然后,当我们想使用存储的经验在我们的转发到网络到深Q网络,以获得Q值预测,我们可以缩放图像在0.0到1.0范围内。

使观测值标准化

在某些情况下,将观测值标准化可以帮助收敛速度。 最常用的规范化过程涉及两个步骤:

  • 使用平均减法进行零集中
  • 使用标准差进行缩放

本质上,以下是标准化过程:

(x - numpy.mean(x)) / numpy.std(x)

在前面的过程中,x是观察。 请注意,还使用其他规范化过程,这取决于所需的规范化值的范围。 例如,如果我们希望规范化后的值在0到1之间,我们可以使用以下方法:

(x - numpy.min(x)) / (numpy.max(x) - numpy.min(x))

在前面的过程中,我们不是减去平均值,而是减去最小值,除以最大值和最小值之间的差值。 这样,观测/x中的最小值被归一化为0,最大值被归一化为1。

或者,如果我们希望归一化后的值介于-1和1之间,则可以使用以下内容:

2 * (x - numpy.min(x)) / (numpy.max(x) - numpy.min(x)) - 1

在我们的环境归一化包装器实现中,我们将使用第一种方法,其中我们使用平均减法和标度对观测数据进行零中心,使用观测中数据的标准差。 事实上,我们将更进一步,计算到目前为止我们收到的所有观测数据的运行均值和标准差,以便根据代理到目前为止观察到的观测数据的分布对观测数据进行规范化。 这更合适,因为来自同一环境的不同观测之间可能存在较高的方差。 下面是我们讨论的规范化包装器的实现代码:

class NormalizedEnv(gym.ObservationWrapper):
    def __init__(self, env=None):
        gym.ObservationWrapper.__init__(self, env)
        self.state_mean = 0
        self.state_std = 0
        self.alpha = 0.9999
        self.num_steps = 0

    def observation(self, observation):
        self.num_steps += 1
        self.state_mean = self.state_mean * self.alpha + \
            observation.mean() * (1 - self.alpha)
        self.state_std = self.state_std * self.alpha + \
            observation.std() * (1 - self.alpha)

        unbiased_mean = self.state_mean / (1 - pow(self.alpha, self.num_steps))
        unbiased_std = self.state_std / (1 - pow(self.alpha, self.num_steps))

        return (observation - unbiased_mean) / (unbiased_std + 1e-8)

我们从环境中获得的作为观察的图像帧(即使在我们的预处理包装器之后)已经在相同的尺度上(0-255或0.0到1.0)。 在这种情况下,规范化过程中的缩放步骤可能不是很有帮助。 一般情况下,这个包装器对其他环境类型是有用的,也没有被观察到对来自Gym环境(如Atari)的已经缩放的图像观测的性能有害。

随机无操作复位

当环境被重置时,代理通常从相同的初始状态开始,因此在重置时接收相同的观察。 代理可能会记住或习惯在一个游戏级别的开始状态,以至于他们可能开始表现不佳,他们开始在一个稍微不同的位置或游戏级别。 有时,它被发现有助于随机化初始状态,例如抽样不同的初始状态,从代理开始插曲。 为了实现这一点,我们可以添加一个Gym包装器,在重置后发送第一个观察之前执行随机数的“no-op。 健身房图书馆用于Atari环境的Atari2600的Arcade学习环境支持一个“NOOP”或“不操作”动作,在Gym库中被编码为一个值为0的动作。 因此,在将观察结果返回到代理之前,我们将用随机数的动作=0来进入环境,如下所示:

class NoopResetEnv(gym.Wrapper):
    def __init__(self, env, noop_max=30):
        """Sample initial states by taking random number of no-ops on reset.
        No-op is assumed to be action 0.
        """
        gym.Wrapper.__init__(self, env)
        self.noop_max = noop_max
        self.noop_action = 0
        assert env.unwrapped.get_action_meanings()[0] == 'NOOP'

    def reset(self):
        """ Do no-op action for a number of steps in [1, noop_max]."""
        self.env.reset()
        noops = random.randrange(1, self.noop_max + 1)  # pylint: disable=E1101
        assert noops > 0
        obs = None
        for _ in range(noops):
            obs, _, done, _ = self.env.step(self.noop_action)
        return obs

    def step(self, ac):
        return self.env.step(ac)

启动复位

一些Atari游戏要求玩家按下开火键开始游戏。 有些游戏要求在每一个生命丢失后按下“火”按钮。 更多的时候,这不,这是唯一的使用消防按钮! 虽然我们意识到这一点可能显得微不足道,但强化学习代理有时很难自己弄清楚这一点。 他们不可能学习到这一点。 事实上,他们能够找出游戏中许多隐藏的小故障或模式,而这些都是人类从未发现过的! 例如,在Qbert的游戏中,一个使用进化策略(这是一种受遗传算法启发的黑盒式学习策略)训练的代理想出了一种独特的方法,它可以继续接收分数,永远不会让游戏结束! 你知道探员能得分多少吗? ~1,000,000! 他们只能得到这么多,因为游戏是人为地重置由于一个时间限制。你能试着在Qbert的比赛中得分那么多吗? 你可以在这里看到代理得分:https://www.youtube.com/watch?v=meE5aaRJ0Zs.

关键不在于特工们如此聪明地把这些事情都搞清楚。 他们肯定可以,但大多数时候,这损害了代理在合理的时间内所能取得的进展。 当我们需要一个单一的代理来处理几种不同的游戏(一次一个)时,这一点尤其正确)。 我们最好从更简单的假设开始,在我们能够使用更简单的假设训练代理很好地发挥作用之后,使它们更加复杂。

因此,我们将实现一个 FireResetEnv Gym包装器,它将按下每个重置上的火灾按钮,并为代理启动环境。 代码的实现如下:

class FireResetEnv(gym.Wrapper):
    def __init__(self, env):
        """Take action on reset for environments that are fixed until firing."""
        gym.Wrapper.__init__(self, env)
        assert env.unwrapped.get_action_meanings()[1] == 'FIRE'
        assert len(env.unwrapped.get_action_meanings()) >= 3

    def reset(self):
        self.env.reset()
        obs, _, done, _ = self.env.step(1)
        if done:
            self.env.reset()
        obs, _, done, _ = self.env.step(2)
        if done:
            self.env.reset()
        return obs

    def step(self, ac):
        return self.env.step(ac)

史诗般的生活

在许多游戏中,包括Atari游戏,玩家可以玩不止一个生命。

Deepmind观察、使用和报告说,当生命丧失时终止一集有助于代理人更好地学习。 必须指出,意图是向代理人表明,失去生命是一件坏事。 在这种情况下,当插曲终止时,我们不会重置环境,而是继续,直到游戏实际上结束,然后我们重置环境。 如果我们在每一次生命损失后重新设置游戏,我们将限制代理的接触观察和经验,这些观察和经验可以只用一次生命来收集,这通常对代理的学习性能不利。

为了实现我们刚才讨论的内容,我们将使用EpisodicLifeEnv类,它在生命丢失时标志着一集的结束,并在游戏结束时重置环境,如下面的代码片段所示:

class EpisodicLifeEnv(gym.Wrapper):
    def __init__(self, env):
        """Make end-of-life == end-of-episode, but only reset on true game over.
        Done by DeepMind for the DQN and co. since it helps value estimation.
        """
        gym.Wrapper.__init__(self, env)
        self.lives = 0
        self.was_real_done = True

    def step(self, action):
        obs, reward, done, info = self.env.step(action)
        self.was_real_done = True
        # check current lives, make loss of life terminal,
        # then update lives to handle bonus lives
        lives = info['ale.lives']
        if lives < self.lives and lives > 0:
            # for Qbert sometimes we stay in lives == 0 condition for a few frames
            # so its important to keep lives > 0, so that we only reset once
            # the environment advertises done.
            done = True
            self.was_real_done = False
        self.lives = lives
        return obs, reward, done, info

    def reset(self):
        """Reset only when lives are exhausted.
        This way all states are still reachable even though lives are episodic,
        and the learner need not know about any of this behind-the-scenes.
        """
        if self.was_real_done:
            obs = self.env.reset()
            self.lives = 0
        else:
            # no-op step to advance from terminal/lost life state
            obs, _, _, info = self.env.step(0)
            self.lives = info['ale.lives']
        return obs

最大和跳帧

Gym库提供的环境没有框架在他们的ID,我们在第四章,探索健身房及其特点,我们讨论了健身房环境的命名。 正如您可能从我们在第4章“探索健身房及其特性”中的讨论中回顾的那样,默认情况下,如果环境名称中没有确定性或无Frameskip的存在,则发送给环境的操作将在n帧的持续时间内重复执行,其中n是从(2,3,4)中统一采样的)。 如果我们想以特定的速度遍历环境,我们可以使用ID中没有Frameskip的Gym Atari环境,这将通过底层环境,而不会对步骤持续时间进行任何更改。 步速率,在这种情况下,是1/60秒,这是60帧每秒。 然后,我们可以根据我们的选择自定义跳过的环境,跳过速率(K)以特定的速率步进。 这种自定义步骤/跳频率的实现如下:

class MaxAndSkipEnv(gym.Wrapper):
    def __init__(self, env=None, skip=4):
        """Return only every `skip`-th frame"""
        gym.Wrapper.__init__(self, env)
        # most recent raw observations (for max pooling across time steps)
        self._obs_buffer = deque(maxlen=2)
        self._skip = skip

    def step(self, action):
        total_reward = 0.0
        done = None
        for _ in range(self._skip):
            obs, reward, done, info = self.env.step(action)
            self._obs_buffer.append(obs)
            total_reward += reward
            if done:
                break

        max_frame = np.max(np.stack(self._obs_buffer), axis=0)
        return max_frame, total_reward, done, info

    def reset(self):
        """Clear past frame buffer and init. to first obs. from inner env."""
        self._obs_buffer.clear()
        obs = self.env.reset()
        self._obs_buffer.append(obs)
        return obs

请注意,我们也在取跳过的帧上的像素值的最大值,并将其作为观察发送,而不是完全忽略跳过的所有中间图像帧。

包装Gym环境

最后,我们将应用我们根据使用 parameters.JSON文件指定的环境配置开发的前面的包装器:

def make_env(env_id, env_conf):
    env = gym.make(env_id)
    if 'NoFrameskip' in env_id:
        assert 'NoFrameskip' in env.spec.id
        env = NoopResetEnv(env, noop_max=30)
        env = MaxAndSkipEnv(env, skip=env_conf['skip_rate'])

    if env_conf['episodic_life']:
        env = EpisodicLifeEnv(env)

    try:
        if 'FIRE' in env.unwrapped.get_action_meanings():
            env = FireResetEnv(env)
    except AttributeError:
        pass

    env = AtariRescale(env, env_conf['useful_region'])

    if env_conf['normalize_observation']:
        env = NormalizedEnv(env)

    env = FrameStack(env, env_conf['num_frames_to_stack'])

    #if env_conf['clip_reward']:  # Reward clipping is done by the agent using the agent's params
    #    env = ClipRewardEnv(env)
    return env

我们前面讨论过的所有环境包装器都在本书的代码存储库中的ch6/environment/atari.py中实现并可用。

训练深度Q学习者玩Atari游戏

我们在这一章中经历了几个新的技术。现在开始有趣的部分,你可以让你的代理人自己训练玩几个Atari游戏,看看他们是如何进步的。 我们的深Q学习者的伟大之处在于,我们可以使用相同的代理来训练和玩任何Atari游戏!

在本节的最后,您应该能够使用我们的深度Q学习代理来观察屏幕上的像素,并通过向Atari健身房环境发送操纵杆命令来采取行动,就像下面的屏幕截图中所示:
在这里插入图片描述

汇集了一个全面的深层Q学习者

现在是时候将我们讨论过的所有技术组合成一个全面的实现,利用所有这些技术来获得最大的性能。 我们将使用我们在上一节中使用几个有用的Gym环境包装器创建的environment.atari模块。 让我们看看代码大纲,了解代码的结构:

您将注意到,为了简洁起见,删除了代码的某些部分,并将其替换为.,表示该部分中的代码已折叠/隐藏。 您可以在本书的代码库ch6/deep_Q_Learner.py中找到完整代码的最新版本。

#!/usr/bin/env python
# Deep Q-learning agent implemented using PyTorch | Praveen Palanisamy
# Chapter 6, Hands-on Intelligent Agents with OpenAI Gym, 2018

from datetime import datetime
from argparse import ArgumentParser
import gym
import torch
import random
import numpy as np

import environment.atari as Atari
import environment.utils as env_utils
from utils.params_manager import ParamsManager
from utils.decay_schedule import LinearDecaySchedule
from utils.experience_memory import Experience, ExperienceMemory
import utils.weights_initializer
from function_approximator.perceptron import SLP
from function_approximator.cnn import CNN
from tensorboardX import SummaryWriter

args = ArgumentParser("deep_Q_learner")
args.add_argument("--params-file", help="Path to the parameters json file. Default is parameters.json",
                  default="parameters.json", metavar="PFILE")
args.add_argument("--env", help="ID of the Atari environment available in OpenAI Gym.Default is SeaquestNoFrameskip-v4",
                  default="SeaquestNoFrameskip-v4", metavar="ENV")
args.add_argument("--gpu-id", help="GPU device ID to use. Default=0", default=0, type=int, metavar="GPU_ID")
args.add_argument("--render", help="Render environment to Screen. Off by default", action="store_true", default=False)
args.add_argument("--test", help="Test mode. Used for playing without learning. Off by default", action="store_true",
                  default=False)
args.add_argument("--record", help="Enable recording (video & stats) of the agent's performance",
                  action="store_true", default=False)
args.add_argument("--recording-output-dir", help="Directory to store monitor outputs. Default=./trained_models/results",
                  default="./trained_models/results")
args = args.parse_args()

params_manager= ParamsManager(args.params_file)
seed = params_manager.get_agent_params()['seed']
summary_file_path_prefix = params_manager.get_agent_params()['summary_file_path_prefix']
summary_file_path= summary_file_path_prefix + args.env+ "_" + datetime.now().strftime("%y-%m-%d-%H-%M")
writer = SummaryWriter(summary_file_path)
# Export the parameters as json files to the log directory to keep track of the parameters used in each experiment
params_manager.export_env_params(summary_file_path + "/" + "env_params.json")
params_manager.export_agent_params(summary_file_path + "/" + "agent_params.json")
global_step_num = 0
use_cuda = params_manager.get_agent_params()['use_cuda']
# new in PyTorch 0.4
device = torch.device("cuda:" + str(args.gpu_id) if torch.cuda.is_available() and use_cuda else "cpu")
torch.manual_seed(seed)
np.random.seed(seed)
if torch.cuda.is_available() and use_cuda:
    torch.cuda.manual_seed_all(seed)


class Deep_Q_Learner(object):
    def __init__(self, state_shape, action_shape, params):
        """
        self.Q is the Action-Value function. This agent represents Q using a Neural Network
        If the input is a single dimensional vector, uses a Single-Layer-Perceptron else if the input is 3 dimensional
        image, use a Convolutional-Neural-Network

        :param state_shape: Shape (tuple) of the observation/state
        :param action_shape: Shape (number) of the discrete action space
        :param params: A dictionary containing various Agent configuration parameters and hyper-parameters
        """
        self.state_shape = state_shape
        self.action_shape = action_shape
        self.params = params
        self.gamma = self.params['gamma']  # Agent's discount factor
        self.learning_rate = self.params['lr']  # Agent's Q-learning rate
        self.best_mean_reward = - float("inf") # Agent's personal best mean episode reward
        self.best_reward = - float("inf")
        self.training_steps_completed = 0  # Number of training batch steps completed so far

        if len(self.state_shape) == 1:  # Single dimensional observation/state space
            self.DQN = SLP
        elif len(self.state_shape) == 3:  # 3D/image observation/state
            self.DQN = CNN

        self.Q = self.DQN(state_shape, action_shape, device).to(device)
        self.Q.apply(utils.weights_initializer.xavier)

        self.Q_optimizer = torch.optim.Adam(self.Q.parameters(), lr=self.learning_rate)
        if self.params['use_target_network']:
            self.Q_target = self.DQN(state_shape, action_shape, device).to(device)
        # self.policy is the policy followed by the agent. This agents follows
        # an epsilon-greedy policy w.r.t it's Q estimate.
        self.policy = self.epsilon_greedy_Q
        self.epsilon_max = params["epsilon_max"]
        self.epsilon_min = params["epsilon_min"]
        self.epsilon_decay = LinearDecaySchedule(initial_value=self.epsilon_max,
                                    final_value=self.epsilon_min,
                                    max_steps= self.params['epsilon_decay_final_step'])
        self.step_num = 0

        self.memory = ExperienceMemory(capacity=int(self.params['experience_memory_capacity']))  # Initialize an Experience memory with 1M capacity

    def get_action(self, observation):
        observation = np.array(observation)  # Observations could be lazy frames. So force fetch before moving forward
        observation = observation / 255.0  # Scale/Divide by max limit of obs' dtype. 255 for uint8
        if len(observation.shape) == 3: # Single image (not a batch)
            if observation.shape[2] < observation.shape[0]:  # Probably observation is in W x H x C format
                # NOTE: This is just an additional check. The env wrappers are taking care of this conversion already
                # Reshape to C x H x W format as per PyTorch's convention
                observation = observation.reshape(observation.shape[2], observation.shape[1], observation.shape[0])
            observation = np.expand_dims(observation, 0)  # Create a batch dimension
        return self.policy(observation)

    def epsilon_greedy_Q(self, observation):
        # Decay Epsilon/exploration as per schedule
        writer.add_scalar("DQL/epsilon", self.epsilon_decay(self.step_num), self.step_num)
        self.step_num +=1
        if random.random() < self.epsilon_decay(self.step_num) and not self.params["test"]:
            action = random.choice([i for i in range(self.action_shape)])
        else:
            action = np.argmax(self.Q(observation).data.to(torch.device('cpu')).numpy())
        return action

    def learn(self, s, a, r, s_next, done):
        # TD(0) Q-learning
        if done:  # End of episode
            td_target = reward + 0.0  # Set the value of terminal state to zero
        else:
            td_target = r + self.gamma * torch.max(self.Q(s_next))
        td_error = td_target - self.Q(s)[a]
        # Update Q estimate
        #self.Q(s)[a] = self.Q(s)[a] + self.learning_rate * td_error
        self.Q_optimizer.zero_grad()
        td_error.backward()
        self.Q_optimizer.step()

    def learn_from_batch_experience(self, experiences):
        batch_xp = Experience(*zip(*experiences))
        obs_batch = np.array(batch_xp.obs) / 255.0  # Scale/Divide by max limit of obs's dtype. 255 for uint8
        action_batch = np.array(batch_xp.action)
        reward_batch = np.array(batch_xp.reward)
        # Clip the rewards
        if self.params["clip_rewards"]:
            reward_batch = np.sign(reward_batch)
        next_obs_batch = np.array(batch_xp.next_obs) / 255.0  # Scale/Divide by max limit of obs' dtype. 255 for uint8
        done_batch = np.array(batch_xp.done)

        if self.params['use_target_network']:
            #if self.training_steps_completed % self.params['target_network_update_freq'] == 0:
            if self.step_num % self.params['target_network_update_freq'] == 0:
                # The *update_freq is the Num steps after which target net is updated.
                # A schedule can be used instead to vary the update freq.
                self.Q_target.load_state_dict(self.Q.state_dict())
            td_target = reward_batch + ~done_batch * \
                np.tile(self.gamma, len(next_obs_batch)) * \
                self.Q_target(next_obs_batch).max(1)[0].data.cpu().numpy()
        else:
            td_target = reward_batch + ~done_batch * \
                np.tile(self.gamma, len(next_obs_batch)) * \
                self.Q(next_obs_batch).detach().max(1)[0].data.cpu().numpy()

        td_target = torch.from_numpy(td_target).to(device)
        action_idx = torch.from_numpy(action_batch).to(device)
        td_error = torch.nn.functional.mse_loss( self.Q(obs_batch).gather(1, action_idx.view(-1, 1)),
                                                       td_target.float().unsqueeze(1))

        self.Q_optimizer.zero_grad()
        td_error.mean().backward()
        writer.add_scalar("DQL/td_error", td_error.mean(), self.step_num)
        self.Q_optimizer.step()

    def replay_experience(self, batch_size = None):
        batch_size = batch_size if batch_size is not None else self.params['replay_batch_size']
        experience_batch = self.memory.sample(batch_size)
        self.learn_from_batch_experience(experience_batch)
        self.training_steps_completed += 1  # Increment the number of training batch steps complemented

    def save(self, env_name):
        file_name = self.params['save_dir'] + "DQL_" + env_name + ".ptm"
        agent_state = {
    
    "Q": self.Q.state_dict(),
                       "best_mean_reward": self.best_mean_reward,
                       "best_reward": self.best_reward};
        torch.save(agent_state, file_name)
        print("Agent's state saved to ", file_name)

    def load(self, env_name):
        file_name = self.params['load_dir'] + "DQL_" + env_name + ".ptm"
        agent_state = torch.load(file_name, map_location= lambda storage, loc: storage)
        self.Q.load_state_dict(agent_state["Q"])
        self.Q.to(device)
        self.best_mean_reward = agent_state["best_mean_reward"]
        self.best_reward = agent_state["best_reward"]
        print("Loaded Q model state from", file_name,
              " which fetched a best mean reward of:", self.best_mean_reward,
              " and an all time best reward of:", self.best_reward)


if __name__ == "__main__":
    env_conf = params_manager.get_env_params()
    env_conf["env_name"] = args.env
    # In test mode, let the end of the game be the end of episode rather than ending episode at the end of every life.
    # This helps to report out the (mean and max) episode rewards per game (rather than per life!)
    if args.test:
        env_conf["episodic_life"] = False
    # Specify the reward calculation type used for printing stats at the end of every episode.
    # If "episode_life" is true, the printed stats (reward, mean reward, max reward) are per life. If "episodic_life"
    # is false, the printed stats/scores are per game in Atari environments
    rew_type = "LIFE" if env_conf["episodic_life"] else "GAME"

    # If a custom useful_region configuration for this environment ID is available, use it if not use the Default
    custom_region_available = False
    for key, value in env_conf['useful_region'].items():
        if key in args.env:
            env_conf['useful_region'] = value
            custom_region_available = True
            break
    if custom_region_available is not True:
        env_conf['useful_region'] = env_conf['useful_region']['Default']

    print("Using env_conf:", env_conf)
    atari_env = False
    for game in Atari.get_games_list():
        if game.replace("_", "") in args.env.lower():
            atari_env = True
    if atari_env:
        env = Atari.make_env(args.env, env_conf)
    else:
        print("Given environment name is not an Atari Env. Creating a Gym env")
        # Resize the obs to w x h (84 x 84 by default) and then reshape it to be in the C x H x W format
        env = env_utils.ResizeReshapeFrames(gym.make(args.env))

    if args.record:  # If monitor is enabled, record stats and video of agent's performance
        env = gym.wrappers.Monitor(env, args.recording_output_dir, force=True)

    observation_shape = env.observation_space.shape
    action_shape = env.action_space.n
    agent_params = params_manager.get_agent_params()
    agent_params["test"] = args.test
    agent = Deep_Q_Learner(observation_shape, action_shape, agent_params)

    episode_rewards = list()
    prev_checkpoint_mean_ep_rew = agent.best_mean_reward
    num_improved_episodes_before_checkpoint = 0  # To keep track of the num of ep with higher perf to save model
    print("Using agent_params:", agent_params)
    if agent_params['load_trained_model']:
        try:
            agent.load(env_conf["env_name"])
            prev_checkpoint_mean_ep_rew = agent.best_mean_reward
        except FileNotFoundError:
            print("WARNING: No trained model found for this environment. Training from scratch.")

    #for episode in range(agent_params['max_num_episodes']):
    episode = 0
    while global_step_num <= agent_params['max_training_steps']:
        obs = env.reset()
        cum_reward = 0.0  # Cumulative reward
        done = False
        step = 0
        #for step in range(agent_params['max_steps_per_episode']):
        while not done:
            if env_conf['render'] or args.render:
                env.render()
            action = agent.get_action(obs)
            next_obs, reward, done, info = env.step(action)
            #agent.learn(obs, action, reward, next_obs, done)
            agent.memory.store(Experience(obs, action, reward, next_obs, done))

            obs = next_obs
            cum_reward += reward
            step += 1
            global_step_num +=1

            if done is True:
                episode += 1
                episode_rewards.append(cum_reward)
                if cum_reward > agent.best_reward:
                    agent.best_reward = cum_reward
                if np.mean(episode_rewards) > prev_checkpoint_mean_ep_rew:
                    num_improved_episodes_before_checkpoint += 1
                if num_improved_episodes_before_checkpoint >= agent_params["save_freq_when_perf_improves"]:
                    prev_checkpoint_mean_ep_rew = np.mean(episode_rewards)
                    agent.best_mean_reward = np.mean(episode_rewards)
                    agent.save(env_conf['env_name'])
                    num_improved_episodes_before_checkpoint = 0
                print("\nEpisode#{} ended in {} steps. Per {} stats: reward ={} ; mean_reward={:.3f} best_reward={}".
                      format(episode, step+1, rew_type, cum_reward, np.mean(episode_rewards), agent.best_reward))
                writer.add_scalar("main/ep_reward", cum_reward, global_step_num)
                writer.add_scalar("main/mean_ep_reward", np.mean(episode_rewards), global_step_num)
                writer.add_scalar("main/max_ep_rew", agent.best_reward, global_step_num)
                # Learn from batches of experience once a certain amount of xp is available unless in test only mode
                if agent.memory.get_size() >= 2 * agent_params['replay_start_size'] and not args.test:
                    agent.replay_experience()
                    print('..............')
                break
    env.close()
    writer.close()

启动训练进程

我们现在已经为深度Q学习者整理了所有的片段,并准备培训代理! 一定要从本书的代码库中检查出/拉/下载最新的代码。

您可以从Atari环境列表中选择任何环境,并使用以下命令训练我们开发的代理:

:~/HOIAWOG/ch6$ python deep_Q_learner.py --env “ENV_ID”

在前面的命令中,ENV_ID是Atari Gym环境的名称/ID。 例如,如果您想在没有帧跳过的pong环境中训练代理,您将运行以下命令:

:~/HOIAWOG/ch6$ python deep_Q_learner.py --env “PongNoFrameskip-v4”

默认情况下,训练日志将保存到./logs/DQL_{ENV}_{T},其中{ENV}是环境的名称,{T}是运行代理时获得的时间戳。 如果使用以下命令启动Tensor Board实例:

:~/HOIAWOG/ch6$ tensorboard --logdir=logs/

默认情况下,我们的deep_Q_learner.py脚本将使用parameters.JSON文件,其位于与读取可配置参数值的脚本相同的目录中。 可以使用命令行-params-file参数重写不同的参数配置文件。

如果在 parameters.JSON文件中load_trained_model参数设置为true,如果为所选环境保存的模型可用,我们的脚本将尝试用它以前学习过的模型初始化代理,以便它可以从它离开的地方恢复,而不是从头开始训练。

测试你的深Q学习者在Atari游戏中的表现

感觉很棒,不是吗? 你现在已经开发了一个代理,可以学习玩任何Atari游戏,并获得更好的自己! 一旦您让您的代理在任何Atari游戏上接受培训,您就可以使用脚本的测试模式来根据到目前为止的学习来测试代理的性能。 您可以在deep_q_learner.py脚本中使用–test参数来启用测试模式。 启用环境渲染也很有用,这样您就可以直观地看到(除了控制台上打印的奖励之外)代理是如何执行的。 例如,您可以使用以下命令在SeaquestAtari游戏中测试代理:

:~/HOIAWOG/ch6$ python deep_Q_learner.py --env “Seaquest- v0” --test --render

你会看到海奎斯特游戏窗口上来和代理显示它的技能!

关于测试模式需要注意的几点如下:

  • 测试模式将关闭代理的学习例程。 因此,代理不会在测试模式下学习或更新自己。 此模式仅用于测试受过训练的代理是如何执行的。 如果您想了解代理在学习过程中是如何执行的,您可以只使用–render选项而不使用–test选项。
  • 测试模式假设您选择的环境的已训练模型存在于trained_models文件夹中。 否则,一个新生的代理人,没有任何事先的知识,将开始从头开始玩游戏。 此外,既然学习是禁用的,你就不会看到代理的改进!

现在,轮到你外出、实验、复习和比较我们在不同Atari健身房环境中实现的代理的性能,看看代理能得分多少! 如果你训练一个代理在游戏中玩得很好,你可以通过从你的叉子打开这本书的代码存储库的拉请求来展示和分享它给其他读者。 你将在页面上被选中!

一旦你对我们开发的代码库感到满意,你就可以用它做几个实验。 例如,您可以通过简单地更改参数来关闭目标Q网络或增加/减少体验内存/重放批处理大小。 使用非常方便的Tensor Board仪表板进行JSON文件和性能比较。

总结

我们开始这一章的宏伟目标是开发智能学习代理,可以在Atari游戏中获得很大的分数。 我们通过实施几种技术来改进我们在上一章中开发的Q-学习者,在这方面取得了逐步的进展。 我们首先学习了如何使用神经网络逼近Q动值函数,并通过实际实现一个浅层神经网络来解决著名的CartPole问题,使我们的学习具体化。 然后,我们实现了经验记忆和经验回放,使代理能够从(Mini)随机抽样的批次经验中学习,这些经验有助于通过打破代理交互之间的相关性,并通过代理先前经验的批次回放来提高样本效率来提高性能。 然后,我们重新讨论了epsilon-贪婪的行动选择政策,并实施了一个衰变时间表,以减少基于时间表的探索,让代理更多地依赖于它的学习。

然后,我们研究了如何使用基于Py Torch的学习代理的Tensor Board的日志记录和可视化功能,以便我们能够以简单和直观的方式观察代理的培训进度。 我们还实现了一个整洁的小参数管理器类,它使我们能够使用外部易读JSON文件配置代理的超参数和其他配置参数。

在我们得到了一个良好的基线和有用的实用工具实现之后,我们开始了深入Q学习器的实现。 我们首先在PyTorch中实现了一个深卷积神经网络,然后用它来表示代理的Q(动作值)函数。 然后,我们看到实现使用目标Q网络的想法是多么容易,该目标Q网络已知用于稳定代理的Q学习过程。 然后,我们将我们的基于深度Q学习的代理组合在一起,它可以仅仅基于来自Gym环境的原始像素观测来学习操作。

然后,我们把我们的眼睛和手放在Atari Gym的环境上,并研究了几种使用Gym环境包装器定制Gym环境的方法。 我们还讨论了Atari环境中的几个有用的包装器,并具体实现了包装器来剪辑奖励,预处理观察图像帧,对整个采样观察分布进行归一化观察,在重置时发送随机NOOP操作以采样不同的启动状态,按重置上的Fire按钮,并以自定义速率跳帧。 我们终于看到了如何将这些都整合到一个全面的代理培训代码库中,并对代理进行任何Atari游戏的培训,并在Tensorboard上看到进度摘要。 我们还研究了如何保存状态,并从以前保存的状态恢复对代理的培训,而不是从头开始重新运行培训。 最后,我们看到了我们实施和培训的代理的改进性能。

我们希望你在这整个章节中有很多乐趣。 我们将在下一章中研究和实现一种不同的算法,它可以用于采取更复杂的行动,而不是一组离散的按钮按下,以及我们如何使用它来训练代理在模拟中自主控制汽车!

猜你喜欢

转载自blog.csdn.net/weixin_42990464/article/details/112169640
今日推荐