hsn-intl-agt-oaigym-merge-1

85 阅读54分钟

OpenAI Gym 智能体实用指南(二)

原文:annas-archive.org/md5/e4fd128cf9b93e0f7a542b053330517a

译者:飞龙

协议:CC BY-NC-SA 4.0

第七章:创建自定义 OpenAI Gym 环境 - CARLA 驾驶模拟器

在第一章中,我们查看了 OpenAI Gym 环境目录中可用的各种学习环境类别。接着,我们在第五章,实现你的第一个学习智能体 - 解决 Mountain Car 问题中探讨了环境列表及其命名法,并提前了解了其中的一些内容。我们还开发了智能体来解决 Mountain Car、Cart Pole 问题以及一些 Atari 游戏环境。到现在为止,你应该已经很好地理解了 OpenAI Gym 提供的各种环境类型和变种。通常,在我们学会如何开发自己的智能体之后,我们希望将这种知识和技能应用于开发智能体来解决新的问题,解决我们已经面临的问题,甚至是我们感兴趣的问题。例如,你可能是一个游戏开发者,想为游戏角色增加智能行为,或者是一个机器人工程师,想将人工智能引入到你的机器人中,或者你可能是一个自动驾驶工程师,想将强化学习应用于自动驾驶。你也许是一个喜欢修理的小工具爱好者,想将一件小工具变成一个智能的物联网(IoT)设备,或者你甚至可能是一个医疗行业的专业人士,想通过机器学习提高实验室的诊断能力。应用的潜力几乎是无限的。

我们选择 OpenAI Gym 作为我们的学习环境的原因之一,是因为它简洁而标准的接口将环境的类型和特性与环境-智能体接口解耦。在本章中,我们将探讨如何根据个人或专业需求创建你自己的环境。这将使你能够在自己的设计或问题上使用我们在前几章中开发的智能体实现、训练和测试脚本、参数管理器以及日志记录和可视化程序。

了解 Gym 环境的构成

任何与 Gym 兼容的环境都应该继承 gym.Env 类,并实现 reset 和 step 方法,以及 observation_space 和 action_space 属性和特性。还有机会实现其他可选方法,增加我们自定义环境的附加功能。下表列出了并描述了其他可用的方法:

方法功能描述
observation_space环境返回的观察值的形状和类型。
action_space环境接受的动作的形状和类型。
reset()用于在每个 episode 开始或结束时重置环境的常规操作。
step(...)用于计算推进环境、仿真或游戏至下一步所需的信息的程序。该程序包括在环境中应用所选的动作、计算奖励、生成下一次观察,并判断回合是否结束。
_render()(可选)此项渲染 Gym 环境的状态或观察结果。
_close()(可选)此项用于关闭 Gym 环境。
_seed(可选)此项为 Gym 环境的随机函数提供一个自定义种子,使得环境在给定种子下能以可复现的方式运行。
_configure(可选)此项启用额外的环境配置。

创建自定义 Gym 环境实现的模板

基于我们已经讨论的 Gym 环境结构,我们现在将展示一个名为 CustomEnv 的自定义环境类实现的基本版本,它将是 gym.Env 的子类,并实现所需的必要方法和参数,以使其成为一个与 Gym 兼容的环境。以下是最小实现的模板:

import gym

class CustomEnv(gym.Env):
    """
    A template to implement custom OpenAI Gym environments

    """

    metadata = {'render.modes': ['human']}
    def __init__(self):
        self.__version__ = "0.0.1"
        # Modify the observation space, low, high and shape values according to your custom environment's needs
        self.observation_space = gym.spaces.Box(low=0.0, high=1.0, shape=(3,))
        # Modify the action space, and dimension according to your custom environment's needs
        self.action_space = gym.spaces.Box(4)

    def step(self, action):
        """
        Runs one time-step of the environment's dynamics. The reset() method is called at the end of every episode
        :param action: The action to be executed in the environment
        :return: (observation, reward, done, info)
            observation (object):
                Observation from the environment at the current time-step
            reward (float):
                Reward from the environment due to the previous action performed
            done (bool):
                a boolean, indicating whether the episode has ended
            info (dict):
                a dictionary containing additional information about the previous action
        """

        # Implement your step method here
        #   - Calculate reward based on the action
        #   - Calculate next observation
        #   - Set done to True if end of episode else set done to False
        #   - Optionally, set values to the info dict
        # return (observation, reward, done, info)

    def reset(self):
        """
        Reset the environment state and returns an initial observation

        Returns
        -------
        observation (object): The initial observation for the new episode after reset
        :return:
        """

        # Implement your reset method here
        # return observation

    def render(self, mode='human', close=False):
        """

        :param mode:
        :return:
        """
        return

在我们完成环境类的实现后,我们应当将其注册到 OpenAI Gym 注册表中,以便可以像之前使用 Gym 环境一样通过 gym.make(ENV_NAME) 创建环境实例。

注册自定义环境到 OpenAI Gym

CustomEnv class we implemented is as follows:
from gym.envs.registration import register

register(
    id='CustomEnv-v0',
    entry_point='custom_environments.envs:CustomEnv',
)

我们将在本章后面使用此模板来创建一个使用非常复杂的驾驶模拟器的自定义 Gym 环境。

创建一个与 OpenAI Gym 兼容的 CARLA 驾驶模拟环境

CARLA 是一个基于 UnrealEngine4 游戏引擎构建的驾驶模拟环境,相较于一些竞争对手,CARLA 提供了更为真实的渲染效果。你可以在其官方网站 carla.org 阅读更多关于 CARLA 模拟器的信息。在本节中,我们将探讨如何创建一个与 OpenAI Gym 兼容的自定义汽车驾驶环境,以训练我们的学习代理。这个环境相当复杂,需要 GPU 支持才能运行——与我们之前见过的其他 Gym 环境不同。一旦你理解了如何为 CARLA 创建一个兼容 Gym 的自定义环境接口,你就能获得足够的信息,来为任何复杂的自定义环境开发接口。

CARLA 的最新版本是 CARLA 0.8.2。虽然大多数(如果不是全部的话)核心环境接口,尤其是PythonClient库,可能保持不变,但未来可能会有变更,这需要对自定义环境实现进行调整。如果发生这种情况,本书的代码库将相应更新,以支持 CARLA 的新版。在进行本章的工作时,你可能需要确保使用本书代码库中的最新版本代码(这也是订阅 GitHub 通知的另一个原因)。不过,本章讨论的自定义环境实现构建模块将普遍适用,并将指导你定义兼容 OpenAI Gym 接口的自定义环境。自定义 CARLA 环境接口的完整代码可以在本书的代码库中的ch7/carla-gym找到。

在我们开始一个兼容 Gym 的 CARLA 环境之前,让我们先看一下 CARLA 模拟器。因此,让我们首先下载 CARLA 发布的二进制文件。在接下来的部分,我们将使用VER_NUM表示版本号,因此在运行以下命令之前,请确保将VER_NUM文本替换为你正在使用的版本号:

  1. 首先,在你的主目录中创建一个名为software的文件夹,可以使用以下 bash 命令:
mkdir ~/software && cd ~/software
  1. 使用官方发布页面上的链接下载 CARLA 的 Linux 二进制发布版本(CARLA_VER_NUM.tar.gz),该页面地址为github.com/carla-simulator/carla/releases/tag/VER_NUM。(版本 0.8.2 的直接链接是:drive.google.com/open?id=1ZtVt1AqdyGxgyTm69nzuwrOYoPUn_Dsm。)然后,将其解压到~/software目录下。你现在应该在~/software/CARLA_VER_NUM文件夹中看到一个名为CarlaUE4.sh的文件。

  2. 使用以下命令将CARLA_SERVER环境变量设置为指向你计算机上的CarlaUE4.sh

export CARLA_SERVER=~/software/CARLA_VER_NUM/CarlaUE4.sh

现在你准备好测试运行 CARLA 驾驶模拟器了!只需执行$CARLA_SERVER,或者直接执行~/software/CARLA_VER_NUM/CarlaUE4.sh。对于 CARLA 0.8.2 版本,命令将是~/software/CARLA_0.8.2/CarlaUE4.sh。此时,你应该能看到一个 CARLA 模拟器的界面,具体如以下截图所示:

上一张截图显示的是车辆(代理)在 CARLA 的一个起始位置。下面的截图显示的是车辆在 CARLA 环境中的另一个起始位置:

一旦车辆初始化完成,你应该能够使用键盘上的wasd键来控制车辆。w键将使汽车向前行驶,a键将使汽车向左转,其他操作你大概能猜出来!

现在让我们继续,并开始实现我们的 Gym 兼容 CARLA 环境的配置和初始化。

配置与初始化

我们将首先定义一些特定于环境的配置参数,并简要了解场景配置。然后,我们将开始 CarlaEnv 类的初始化过程,该类将继承自 Gym.Env 类。

配置

首先,让我们使用字典定义环境的配置参数列表,如下所示:

# Default environment configuration
ENV_CONFIG = {
    "enable_planner": True,
    "use_depth_camera": False,
    "discrete_actions": True,
    "server_map": "/Game/Maps/" + city,
    "scenarios": [scenario_config["Lane_Keep_Town2"]],
    "framestack": 2, # note: only [1, 2] currently supported
    "early_terminate_on_collision": True,
    "verbose": False,
    "render_x_res": 800,
    "render_y_res": 600,
    "x_res": 80,
    "y_res": 80
}

scenario_config 定义了若干在创建各种驾驶场景时有用的参数。场景配置描述在 scenarios.json 文件中,该文件可以在本书的代码库中找到,路径为 ch7/carla-gym/carla_gym/envs/scenarios.json

初始化

__init__ 方法中,我们定义了初始化参数以及动作和状态空间,正如我们在上一节中看到的那样,这些是必需的。实现非常直接,如下所示:

def __init__(self, config=ENV_CONFIG):
        self.config = config
        self.city = self.config["server_map"].split("/")[-1]
        if self.config["enable_planner"]:
            self.planner = Planner(self.city)

        if config["discrete_actions"]:
            self.action_space = Discrete(len(DISCRETE_ACTIONS))
        else:
            self.action_space = Box(-1.0, 1.0, shape=(2,))
        if config["use_depth_camera"]:
            image_space = Box(
                -1.0, 1.0, shape=(

                    config["y_res"], config["x_res"],
                    1 * config["framestack"]))
        else:
            image_space = Box(
                0.0, 255.0, shape=(
                    config["y_res"], config["x_res"],
                    3 * config["framestack"]))
        self.observation_space = Tuple(
            [image_space,
             Discrete(len(COMMANDS_ENUM)),  # next_command
             Box(-128.0, 128.0, shape=(2,))])  # forward_speed, dist to goal

        self._spec = lambda: None
        self._spec.id = "Carla-v0"

        self.server_port = None
        self.server_process = None
        self.client = None
        self.num_steps = 0
        self.total_reward = 0
        self.prev_measurement = None
        self.prev_image = None
        self.episode_id = None
        self.measurements_file = None
        self.weather = None
        self.scenario = None
        self.start_pos = None
        self.end_pos = None
        self.start_coord = None
        self.end_coord = None
        self.last_obs = None

实现 reset 方法

正如你可能已经注意到的,在每一集开始时,我们调用 Gym 环境的 reset 方法。对于 CARLA 环境,我们希望通过 CARLA 客户端更新 CARLA 服务器,以重新启动场景。

那么,让我们继续开始实现 reset 方法。

使用 CarlaSettings 对象自定义 CARLA 仿真

当我们开始一个新的一集时,我们希望能够配置起始状态(代理或车辆的起始位置)、目标状态(代理或车辆的预定目的地)、场景的复杂度(通过场景中的车辆或行人数量来衡量)、观测的类型和来源(配置在车辆上的传感器)等。

CARLA 项目使用服务器-客户端架构管理 UE4 环境与外部配置和控制之间的接口,因此有两个服务器。

对于 CARLA 环境,我们可以使用 CarlaSettings 对象或 CarlaSettings.ini 文件配置环境的起始状态、目标状态、复杂度级别以及传感器源。

现在,让我们创建一个 CarlaSettings 对象并配置一些设置,如下所示:

settings = CarlaSettings()  # Initialize a CarlaSettings object with default values
settings.set(
            SynchronousMode=True,
            SendNonPlayerAgentsInfo=True,  # To receive info about all other objs
            NumberOfVehicles=self.scenario["num_vehicles"],
            NumberOfPedestrians=self.scenario["num_pedestrians"],
            WeatherId=self.weather)
SynchronousMode to True to enable the synchronous mode, in which the CARLA server halts the execution of each frame until a control message is received. Control messages are based on the actions the agent takes and are sent through the CARLA client.

向 CARLA 中的车辆添加摄像头和传感器

要在 CARLA 环境中添加 RGB 彩色摄像头,请使用以下代码:

# Create a RGB Camera Object
camera1 = Camera('CameraRGB')
# Set the RGB camera image resolution in pixels
camera1.set_image_size(640, 480)
# Set the camera/sensor position relative to the car in meters
camera1.set_positions(0.25, 0, 1.30)
# Add the sensor to the Carla Settings object
settings.add_sensor(camera1)

你还可以使用以下代码片段添加深度测量传感器或摄像头:

# Create a depth camera object that can provide us the ground-truth depth of the driving scene
camera2 = Camera("CameraDepth",PostProcessing="Depth")
# Set the depth camera image resolution in pixels
camera2.set_image_size(640, 480)
# Set the camera/sensor position relative to the car in meters
camera2.set_position(0.30, 0, 1.30)
# Add the sensor to the Carla settings object
settings.add_sensor(camera)Setting up the start and end positions in the scene for the Carla Simulation

要向 CARLA 环境中添加 LIDAR,请使用以下代码:

# Create a LIDAR object. The default LIDAR supports 32 beams
lidar = Lidar('Lidar32')
# Set the LIDAR sensor's specifications
lidar.set(
    Channels=32,  # Number of beams/channels
    Range=50,     # Range of the sensor in meters
    PointsPerSecond=1000000,  # Sample rate
    RotationFrequency=10,  # Frequency of rotation
    UpperFovLimit=10,  # Vertical field of view upper limit angle
    LowerFovLimit=-30) # Vertical field of view lower limit angle
# Set the LIDAR position & rotation relative to the car in meters
lidar.set_position(0, 0, 2.5)
lidar.set_rotation(0, 0, 0)
# Add the sensor to the Carla settings object
settings.add_sensor(lidar)

一旦我们根据期望的驾驶仿真配置创建了一个 CARLA 设置对象,我们可以将其发送到 CARLA 服务器,以设置环境并启动仿真。

一旦我们将 CARLA 设置对象发送到 CARLA 服务器,服务器会响应一个场景描述对象,其中包含可用于自我驾驶车辆的起始位置,如下所示:

scene = self.client.load_settings(settings)
available_start_spots = scene.player_start_spots

我们现在可以为主车或自车选择一个特定的起始位置,或者随机选择一个起始点,如以下代码片段所示:

start_spot = random.randint(0, max(0, available_start_spots))

我们还可以将这个起始点偏好发送到服务器,并使用以下代码片段请求启动新的一集:

self.client.start_episode(start_spot)

请注意,前一行是一个阻塞函数调用,它将在 CARLA 服务器实际启动本集之前阻塞动作。

现在,我们可以从这个起始位置开始,逐步进行,直到本集的结束。在下一节中,我们将看到我们需要实现 CARLA 环境的step()方法,这个方法用于逐步推进环境,直到本集结束:

def _reset(self):
        self.num_steps = 0
        self.total_reward = 0
        self.prev_measurement = None
        self.prev_image = None
        self.episode_id = datetime.today().strftime("%Y-%m-%d_%H-%M-%S_%f")
        self.measurements_file = None

        # Create a CarlaSettings object. This object is a wrapper around
        # the CarlaSettings.ini file. Here we set the configuration we
        # want for the new episode.
        settings = CarlaSettings()
        # If config["scenarios"] is a single scenario, then use it if it's an array of scenarios, randomly choose one and init
        self.config = update_scenarios_parameter(self.config)

        if isinstance(self.config["scenarios"],dict):
            self.scenario = self.config["scenarios"]
        else: #ininstance array of dict
            self.scenario = random.choice(self.config["scenarios"])
        assert self.scenario["city"] == self.city, (self.scenario, self.city)
        self.weather = random.choice(self.scenario["weather_distribution"])
        settings.set(
            SynchronousMode=True,
            SendNonPlayerAgentsInfo=True,
            NumberOfVehicles=self.scenario["num_vehicles"],
            NumberOfPedestrians=self.scenario["num_pedestrians"],
            WeatherId=self.weather)
        settings.randomize_seeds()

        if self.config["use_depth_camera"]:
            camera1 = Camera("CameraDepth", PostProcessing="Depth")
            camera1.set_image_size(
                self.config["render_x_res"], self.config["render_y_res"])
            camera1.set_position(30, 0, 130)
            settings.add_sensor(camera1)

        camera2 = Camera("CameraRGB")
        camera2.set_image_size(
            self.config["render_x_res"], self.config["render_y_res"])
        camera2.set_position(30, 0, 130)
        settings.add_sensor(camera2)

        # Setup start and end positions
        scene = self.client.load_settings(settings)
        positions = scene.player_start_spots
        self.start_pos = positions[self.scenario["start_pos_id"]]
        self.end_pos = positions[self.scenario["end_pos_id"]]
        self.start_coord = [
            self.start_pos.location.x // 100, self.start_pos.location.y // 100]
        self.end_coord = [
            self.end_pos.location.x // 100, self.end_pos.location.y // 100]
        print(
            "Start pos {} ({}), end {} ({})".format(
                self.scenario["start_pos_id"], self.start_coord,
                self.scenario["end_pos_id"], self.end_coord))

        # Notify the server that we want to start the episode at the
        # player_start index. This function blocks until the server is ready
        # to start the episode.
        print("Starting new episode...")
        self.client.start_episode(self.scenario["start_pos_id"])

        image, py_measurements = self._read_observation()
        self.prev_measurement = py_measurements
        return self.encode_obs(self.preprocess_image(image), py_measurements)

为 CARLA 环境实现step()函数

一旦我们通过将 CARLA 设置对象发送到 CARLA 服务器并调用client.start_episode(start_spot)来初始化 CARLA 模拟器,驾驶模拟就会开始。然后,我们可以使用client.read_data()方法获取在给定步骤下模拟产生的数据。我们可以通过以下代码行来实现这一点:

measurements, sensor_data = client.read_data()

访问相机或传感器数据

我们可以通过返回的sensor_data对象的data属性,在任何给定的时间步获取传感器数据。要获取 RGB 摄像头帧,请输入以下代码:

rgb_image = sensor_data['CameraRGB'].data

rgb_image是一个 NumPy n 维数组,您可以像通常访问和操作 NumPy n 维数组一样访问和操作它。

例如,要访问 RGB 摄像头图像在(x, y)图像平面坐标处的像素值,可以使用以下代码行:

pixel_value_at_x_y = rgb_image[X, Y]

要获取深度摄像头帧,请输入以下代码:

depth_image = sensor_data['CameraDepth'].data

发送动作以控制 CARLA 中的代理

我们可以通过向 CARLA 服务器发送所需的转向、油门、刹车、手刹和倒车(档)命令,来控制 CARLA 中的汽车。下表展示了汽车在 CARLA 中将遵循的命令的值、范围和描述:

命令/动作名称值类型,范围描述
转向Float,[-1.0,+1.0]标准化转向角度
油门Float,[0.0,1.0]标准化油门输入
刹车Float,[0.0,1.0]标准化刹车输入
手刹Boolean,真/假这告诉汽车是否启用手刹(True)或不启用(False
倒车Boolean,真/假这告诉汽车是否处于倒档(True)或不是(False

如 CARLA 文档中所述,实际的转向角度将取决于车辆。例如,默认的 Mustang 车辆的最大转向角度为 70 度,这在车辆的前轮 UE4 蓝图文件中有所定义。这些是控制 CARLA 中车辆所需的五个不同命令。在这五个命令中,三者(转向、油门和刹车)是实值浮动点数。尽管它们的范围限制在-1 到+1 或 0 到 1 之间,但可能的(唯一)数值组合却是庞大的。例如,如果我们使用单精度浮动点表示油门值,该值位于 0 到 1 之间,那么总共有种可能的不同值,这意味着油门命令有 1,056,964,608 种不同的可能值。刹车命令也是如此,因为它的值也位于 0 到 1 之间。转向命令的可能浮动值大约是其余两个命令的两倍,因为它位于-1 到+1 之间。由于单个控制消息由五个命令中每个命令的一个值组成,因此不同动作(或控制消息)的数量是每个命令唯一值的乘积,其大致顺序如下:

如你所见,这会生成一个巨大的动作空间,这可能对于一个深度学习代理来说是一个非常困难的问题,因为它需要回归到这样一个巨大的动作空间。因此,让我们简化动作空间,定义两种不同的动作空间——一种是连续空间,另一种是离散空间,这对于应用不同的强化学习算法很有用。例如,基于深度 Q 学习的算法(没有自然化优势函数)只能在离散动作空间中工作。

CARLA 中的连续动作空间

在驾驶过程中,我们通常不会同时加速和刹车;因为 CARLA 中的动作空间是连续的,而且代理将在每一步执行一个动作,所以可能只需要一个命令来同时处理加速和减速。现在,我们将油门和刹车命令合并为一个命令,其值范围从-1 到+1,其中-1 到 0 的范围用于刹车命令,0 到 1 的范围用于油门或加速命令。我们可以使用以下命令来定义它:

action_space = gym.space.Box(-1.0, 1.0, shape=2(,))

action[0]代表转向命令,action[1]代表我们合并的油门和刹车命令的值。目前,我们将hand_brakereverse都设为 False。接下来,我们将查看如何定义一个离散的动作空间,以便选择我们希望代理执行的动作。

CARLA 中的离散动作空间

我们已经看到完整的动作空间非常大(大约是 )。你可能玩过那种只需要使用四个方向键或游戏手柄来控制速度和车头方向(汽车指向的方向)的游戏,那么为什么我们不能要求代理以类似的方式来控制汽车呢?好吧,这就是离散化动作空间的想法。虽然我们无法对汽车进行精确控制,但我们可以确保离散化后的空间能在模拟环境中提供良好的控制。

让我们从使用与连续动作空间情况类似的约定开始——在连续动作空间中,我们使用一个浮点值来表示油门(加速)和刹车(减速)动作,从而在内部使用二维有界空间。这意味着在这种情况下,动作空间可以定义如下:

action_space = gym.spaces.Discrete(NUM_DISCRETE_ACTIONS)

如你所见,NUM_DISCRETE_ACTONS 等于可用动作的数量,我们将在本节稍后定义。

然后,我们将使用二维有界空间对该空间进行离散化,并将其作为离散动作空间暴露给代理。为了在保持控制车辆的同时最小化可能的动作数量,我们使用以下动作列表:

动作索引动作描述动作数组值
0滑行[0.0, 0.0]
1向左转[0.0, -0.5]
2向右转[0.0, 0.5]
3向前[1.0, 0.0]
4刹车[-0.5, 0.0]
5左转并加速[1.0, -0.5]
6向右转并加速[1.0, 0.5]
7向左转并减速[-0.5, -0.5]
8向右转并减速[-0.5, 0.5]

现在,让我们在 carla_env 实现脚本中定义前述的离散动作集合,并展示如下:

DISCRETE_ACTIONS = {
    0: [0.0, 0.0],    # Coast
    1: [0.0, -0.5],   # Turn Left 
    2: [0.0, 0.5],    # Turn Right
    3: [1.0, 0.0],    # Forward
    4: [-0.5, 0.0],   # Brake
    5: [1.0, -0.5],   # Bear Left & accelerate
    6: [1.0, 0.5],    # Bear Right & accelerate
    7: [-0.5, -0.5],  # Bear Left & decelerate
    8: [-0.5, 0.5],   # Bear Right & decelerate
}

向 CARLA 模拟服务器发送动作

现在我们已经定义了 CARLA Gym 环境的动作空间,我们可以查看如何将我们定义的连续或离散动作转换为 CARLA 模拟服务器接受的值。

由于我们在连续和离散动作空间中都遵循了相同的二维有界动作值约定,我们可以使用以下代码片段将动作转换为转向、油门和刹车命令:

throttle = float(np.clip(action[0], 0, 1)
brake = float(np.abs(np.cllip(action[0], -1, 0)
steer = float(p.clip(action[1], -1, 1)
hand_brake = False
reverse = False

如你所见,这里 action[0] 表示油门和刹车,而 action[1] 表示转向角度。

我们将利用 CARLA PythonClient 库中 CarlaClient 类的实现来处理与 CARLA 服务器的通信。如果你想了解如何使用协议缓冲区处理与服务器的通信,可以查看 ch7/carla-gym/carla_gym/envs/carla/client.pyCarlaClient 类的实现。

要在 CARLA 环境中实现奖励函数,请输入以下代码:

def calculate_reward(self, current_measurement):
        """
        Calculate the reward based on the effect of the action taken using the previous and the current measurements
        :param current_measurement: The measurement obtained from the Carla engine after executing the current action
        :return: The scalar reward
        """
        reward = 0.0

        cur_dist = current_measurement["distance_to_goal"]

        prev_dist = self.prev_measurement["distance_to_goal"]

        if env.config["verbose"]:
            print("Cur dist {}, prev dist {}".format(cur_dist, prev_dist))

        # Distance travelled toward the goal in m
        reward += np.clip(prev_dist - cur_dist, -10.0, 10.0)

        # Change in speed (km/hr)
        reward += 0.05 * (current_measurement["forward_speed"] - self.prev_measurement["forward_speed"])

        # New collision damage
        reward -= .00002 * (
            current_measurement["collision_vehicles"] + current_measurement["collision_pedestrians"] +
            current_measurement["collision_other"] - self.prev_measurement["collision_vehicles"] -
            self.prev_measurement["collision_pedestrians"] - self.prev_measurement["collision_other"])

        # New sidewalk intersection
        reward -= 2 * (
            current_measurement["intersection_offroad"] - self.prev_measurement["intersection_offroad"])

        # New opposite lane intersection
        reward -= 2 * (
            current_measurement["intersection_otherlane"] - self.prev_measurement["intersection_otherlane"])

        return reward

确定 CARLA 环境中剧集的结束条件

我们已经实现了meta hod来计算奖励,并定义了允许的动作、观察和自定义 CARLA 环境的重置方法。根据我们的自定义 Gym 环境创建模板,这些是我们需要实现的必要方法,用于创建与 OpenAI Gym 接口兼容的自定义环境。

虽然这是真的,但还有一件事需要我们处理,以便代理能够持续与我们的环境交互。记得我们在第五章中开发 Q-learning 代理时,实现你的第一个学习代理——解决山地车问题,针对山地车环境,环境在 200 步后会自动重置?或者在杠杆杆环境中,当杆子低于某个阈值时环境会重置?再比如在 Atari 游戏中,当代理失去最后一条命时,环境会自动重置?是的,我们需要关注决定何时重置环境的例程,目前我们在自定义 CARLA Gym 环境实现中缺少这一部分。

尽管我们可以选择任何标准来重置 CARLA Gym 环境,但有三点需要考虑,如下所示:

  • 当主机或代理控制的自驾车与其他车辆、行人、建筑物或路边物体发生碰撞时,这可能是致命的(类似于 Atari 游戏中失去生命的情况)

  • 当主机或自驾车达到目的地或终点目标时

  • 当超出时间限制时(类似于我们在山地车 Gym 环境中的 200 时间步限制)

我们可以使用这些条件来形成决定一集结束的标准。确定.step(...)返回的done变量值的伪代码如下(完整代码可以在书籍的代码仓库ch7/carla-gym/carla_gym/envs/中找到):

# 1\. Check if a collision has occured
m = measurements_from_carla_server
collided = m["collision_vehicles"] > 0 or m["collision_pedestrians"] > 0 or m["collision_other"] > 0

# 2\. Check if the ego/host car has reached the destination/goal
planner = carla_planner
goal_reached = planner["next_command"] == "REACHED_GOAL"

# 3\. Check if the time-limit has been exceeded
time_limit = scenario_max_steps_config
time_limit_exceeded = num_steps > time_limit

# Set "done" to True if either of the above 3 criteria becomes true
done = collided or goal_reached or time_limit_exceeded

我们现在已经完成了创建基于 CARLA 驾驶模拟器的自定义 Gym 兼容环境所需的所有组件!在接下来的部分中,我们将测试这个环境,并最终看到它的实际效果。

测试 CARLA Gym 环境

为了方便测试我们环境实现的基础部分,我们将实现一个简单的main()例程,这样我们就可以像运行脚本一样运行环境。这将帮助我们验证基本接口是否已正确设置,以及环境的实际表现如何!

CarlaEnv and runs five episodes with a fixed action of going forward. The ENV_CONFIG action, which we created during initialization, can be changed to use discrete or continuous action spaces, as follows:
# Part of https://github.com/PacktPublishing/Hands-On-Intelligent-Agents-with-OpenAI-Gym/ch7/carla-gym/carla_gym/envs/carla_env.py
if __name__ == "__main__":
    for _ in range(5):
        env = CarlaEnv()
        obs = env.reset()
        done = False
        t = 0
        total_reward = 0.0
        while not done:
            t += 1
            if ENV_CONFIG["discrete_actions"]:
                obs, reward, done, info = env.step(3) # Go Forward
            else:
                obs, reward, done, info = env.step([1.0, 0.0]) # Full throttle, zero steering angle
            total_reward += reward
            print("step#:", t, "reward:", round(reward, 4), "total_reward:", round(total_reward, 4), "done:", done)

现在,开始测试我们刚刚创建的环境吧!请记住,CARLA 需要 GPU 才能平稳运行,并且系统环境变量CARLA_SERVER需要定义,并指向你系统中的CarlaUE4.sh文件。一旦准备好,你可以通过在rl_gym_book conda 环境中运行以下命令来测试我们创建的环境:

(rl_gym_book) praveen@ubuntu:~/rl_gym_book/ch7$ python carla-gym/carla_gym/envs/carla_env.py

上述命令应该会打开一个小的 CARLA 模拟器窗口,并初始化在 carla_env.py 脚本中使用的场景配置。这应该类似于以下截图:

如你所见,默认情况下,车辆被脚本设置为直行。请注意,carla_env.py 脚本还会产生控制台输出,显示当前环境中的时间步、计算的瞬时奖励、该回合的总奖励以及 done(True 或 False)的值,这些对测试我们的环境非常有用。当车辆开始前进时,你应该看到奖励值在增加!

控制台输出如下:

现在,你的自定义 CARLA Gym 环境已经运行了!你可以使用 ch7/carla-gym/carla_gym/envs/scenarios.json 文件中的定义创建多个不同的驾驶场景。然后,你可以为这些场景创建新的自定义 CARLA 环境,注册后可以使用常见的 gym.make(...) 命令来使用它们,例如 gym.make("Carla-v0")

本书代码库中的代码负责通过我们之前在本章讨论的方法进行环境注册,并将其注册到 OpenAI Gym 注册表中。现在,你可以使用 OpenAI Gym 创建我们构建的自定义环境的实例。

以下截图展示了你可以用来测试自定义 Gym 环境的 Python 命令:

就是这样!其余部分和其他 Gym 环境类似。

总结

在本章中,我们逐步讲解了自定义 Gym 环境的实现,从一个模板开始,搭建了一个 OpenAI Gym 环境的基本结构,并提供了所有必要的接口供智能体使用。我们还探讨了如何在 Gym 注册表中注册自定义环境实现,使得我们可以使用熟悉的 gym.make(ENV_NAME) 命令来创建现有环境的实例。接着,我们学习了如何为基于开源驾驶模拟器 CARLA 的 UnrealEngine 创建一个与 Gym 兼容的环境实现。然后,我们快速走过了安装和运行 CARLA 所需的步骤,并开始逐步实现 CarlaEnv 类,详细涵盖了实现与 OpenAI Gym 兼容的自定义环境所涉及的重要细节。

在下一章,我们将从零开始构建一个高级智能体,通过实践示例,最终使用我们在本章创建的自定义 CARLA 环境来训练一个可以独立驾驶汽车的智能体!

第八章:使用深度演员-评论家算法实现智能-自动驾驶汽车代理

在第六章,实现一个用于最优控制的智能代理,使用深度 Q 学习,我们实现了使用深度 Q 学习的代理来解决涉及离散动作或决策的问题。我们看到它们如何被训练来玩视频游戏,比如 Atari 游戏,就像我们一样:看着游戏屏幕并按下游戏手柄/摇杆上的按钮。我们可以使用这样的代理在给定有限的选择集的情况下,做出最佳选择、做决策或执行动作,其中可能的决策或动作数量是有限的,通常较少。有许多现实世界的问题可以通过能够学习执行最优离散动作的代理来解决。我们在第六章中看到了些例子,实现一个用于最优离散控制的智能代理,使用深度 Q 学习

在现实世界中,还有其他类别的问题和任务要求执行的动作是低级的,并且是连续值而不是离散的。例如,一个智能温控系统或恒温器需要能够对内部控制电路进行精细调整,以维持房间的指定温度。控制动作信号可能包括一个连续值的实数(例如1.456)来控制供暖、通风和空调HVAC)系统。再考虑一个例子,我们希望开发一个智能代理来自动驾驶汽车。人类驾驶汽车时,通过换挡、踩油门或刹车踏板以及转向来操控汽车。虽然当前的档位是五到六个值中的一个,取决于汽车的变速系统,但如果一个智能软件代理必须执行所有这些动作,它必须能够为油门(加速器)、刹车(刹车)和转向产生连续值的实数。

在像这些例子中,当我们需要代理执行连续值的动作时,我们可以使用基于策略梯度的演员-评论家方法来直接学习和更新代理的策略,而不是像在第六章中看到的深度 Q 学习代理那样通过状态和/或动作值函数来进行学习,实现一个用于最优离散控制的智能代理,使用深度 Q 学习。在本章中,我们将从演员-评论家算法的基础开始,逐步构建我们的代理,同时训练它使用 OpenAI Gym 环境解决各种经典的控制问题。我们将把代理构建到能够在 CARLA 驾驶模拟环境中驾驶汽车,使用我们在上一章中实现的自定义 Gym 接口。

深度 n 步优势演员-评论者算法

在我们基于深度 Q 学习的智能代理实现中,我们使用深度神经网络作为函数逼近器来表示动作值函数。然后代理根据值函数提出策略。特别地,我们在实现中使用了 -贪婪算法。因此,我们理解最终代理必须知道在给定观测/状态时采取什么行动是好的。而不是对状态/行动函数进行参数化或逼近,然后根据该函数导出策略,我们可以直接参数化策略吗?是可以的!这正是策略梯度方法的精确思想。

在接下来的小节中,我们将简要介绍基于策略梯度的学习方法,然后转向结合价值和基于策略的学习的演员-评论者方法。然后,我们将看一些扩展到演员-评论者方法,已被证明能提高学习性能的方法。

策略梯度

在基于策略梯度的方法中,策略可以通过使用带参数的神经网络表示,例如 ,目标是找到最佳参数集 。这可以直观地看作是一个优化问题,我们试图优化策略的目标,以找到表现最佳的策略。代理策略的目标是什么?我们知道,代理应该在长期内获得最大的奖励,以完成任务或实现目标。如果我们能数学化地表述这个目标,我们可以使用优化技术找到最佳策略,供代理根据给定任务遵循。

我们知道状态值函数  告诉我们从状态  开始,按照策略  直到本集结束的预期回报。它告诉我们身处状态  有多好。因此,一个良好的策略在环境中起始状态的值应较高,因为它代表了在该状态下执行策略  直至本集结束时的预期/平均/总体价值。起始状态值越高,遵循策略的代理可以获得的总长期奖励也越高。因此,在一个情节性环境中——环境即一个情节,即具有终端状态——我们可以根据起始状态的值来衡量策略的优劣。数学上,这样的目标函数可以写成如下形式:

但如果环境不是序列性的呢?这意味着它没有终止状态,并且一直持续下去。在这种环境中,我们可以使用遵循当前策略时所访问的状态的平均值 。从数学角度来看,平均值目标函数可以表示为以下形式:

这里, 是  对应的马尔可夫链的平稳分布,表示遵循策略  时访问状态  的概率。

我们还可以使用在这种环境中每个时间步获得的平均奖励,这可以通过以下方程式在数学上表示:

这本质上是当智能体根据策略  采取行动时可以获得的奖励的期望值,可以简写为如下形式:

为了使用梯度下降法优化此策略目标函数,我们将对方程关于  进行求导,找到梯度,进行反向传播,并执行梯度下降步骤。从之前的方程中,我们可以写出如下公式:

让我们通过展开项并进一步简化,求解前面方程对  的导数。按照以下方程从左到右的顺序,理解得出结果所涉及的一系列步骤:

为了理解这些方程以及如何将策略梯度  等同于似然比 ,我们先回顾一下我们的目标是什么。我们的目标是找到策略的最优参数集 ,使得遵循该策略的智能体能够在期望中获得最大奖励(即平均奖励)。为了实现这个目标,我们从一组参数开始,然后不断更新这些参数,直到我们达到最优参数集。为了确定在参数空间中需要更新哪些方向,我们利用策略  对参数  的梯度指示的方向。我们先从前面方程中的第二项 (这是第一项  按定义得到的结果)开始:

是在策略  下,采取动作  在状态  中获得的步长奖励的期望值的梯度。根据期望值的定义,可以将其写成如下的和式:

我们将研究似然比技巧,在此上下文中用于将该方程转化为一种使计算可行的形式。

似然比技巧

由  表示的策略假设在其非零时为可微分函数,但计算策略相对于 theta 的梯度, ,可能并不直接。我们可以在两边同时乘以和除以策略 ,得到以下公式:

从微积分中,我们知道函数的对数的梯度是该函数相对于其本身的梯度,数学表达式如下:

因此,我们可以将策略相对于其参数的梯度写成以下形式:

这在机器学习中被称为似然比技巧,或对数导数技巧。

策略梯度定理

由于策略  是一个概率分布函数,它描述了给定状态和参数  下的动作概率分布,根据定义,跨状态和动作的双重求和项可以表示为通过奖励  在分布  上的得分函数的期望值。这在数学上等价于以下公式:

请注意,在前面的方程中,  是采取行动  从状态  获得的步奖励。

策略梯度定理通过用长期行动值  替换即时步奖励 ,对这种方法进行了推广,可以写成如下形式:

这个结果非常有用,并且形成了多个策略梯度方法变体的基础。

通过对策略梯度的理解,我们将在接下来的几节中深入探讨演员-评论员算法及其变体。

演员-评论员算法

让我们从演员-评论员架构的图示表示开始,如下图所示:

如名称和前面的图示所示,演员-评论员算法有两个组成部分。演员负责在环境中执行动作,即根据对环境的观察并根据代理的策略采取行动。演员可以被视为策略的持有者/制定者。另一方面,评论员负责估计状态值、状态-动作值或优势值函数(取决于所使用的演员-评论员算法的变体)。让我们考虑一个例子,其中评论员试图估计动作值函数 。如果我们使用一组参数 w 来表示评论员的参数,评论员的估计值可以基本写成:

将真实的动作值函数替换为评论家对动作值函数的近似(在策略梯度定理部分的最后一个方程),从上一节的策略梯度定理结果中得到以下近似的策略梯度:

实际操作中,我们进一步通过使用随机梯度上升(或者带负号的下降)来逼近期望值。

优势演员-评论家算法

动作值演员-评论家算法仍然具有较高的方差。我们可以通过从策略梯度中减去基准函数 *B(s)*来减少方差。一个好的基准是状态值函数, 。使用状态值函数作为基准,我们可以将策略梯度定理的结果重写为以下形式:

我们可以定义优势函数 为以下形式:

当与基准一起用于前述的策略梯度方程时,这将给出演员-评论家策略梯度的优势:

回顾前几章,值函数的 1 步时序差分(TD)误差 给出如下:

如果我们计算这个 TD 误差的期望值,我们将得到一个方程,它类似于我们在第二章中看到的动作值函数的定义,强化学习与深度强化学习。从这个结果中,我们可以观察到,TD 误差实际上是优势函数的无偏估计,正如从左到右推导出的这个方程所示:

有了这个结果和本章迄今为止的方程组,我们已经具备了足够的理论基础,可以开始实现我们的代理!在进入代码之前,让我们先理解算法的流程,以便在脑海中对其有一个清晰的图像。

最简单的(一般/基础)优势演员-评论家算法包括以下步骤:

  1. 初始化(随机)策略和价值函数估计。

  2. 对于给定的观察/状态 ,执行当前策略 规定的动作 

  3. 基于得到的状态 和通过 1 步 TD 学习方程获得的奖励 ,计算 TD 误差:

  4. 通过根据 TD 误差调整状态的动作概率来更新演员!

    • 如果  > 0,增加采取动作 的概率,因为 是一个好的决策,并且效果很好

    • 如果  < 0,则减少采取动作 的概率,因为 导致了代理的表现不佳

  5. 通过调整其对的估计值,使用 TD 误差更新评论员:

    • ,其中是评论员的学习率
  6. 将下一个状态设置为当前状态,并重复步骤 2。

n 步优势演员-评论员算法

在优势演员-评论员算法部分,我们查看了实现该算法的步骤。在第 3 步中,我们注意到需要基于 1 步回报(TD 目标)计算 TD 误差。这就像让智能体在环境中迈出一步,然后根据结果计算评论员估计值的误差,并更新智能体的策略。这听起来直接且简单,对吧?但是,是否有更好的方法来学习和更新策略呢?正如你从本节标题中可能猜到的那样,思路是使用 n 步回报,与基于 1 步回报的 TD 学习相比,n 步回报使用了更多的信息来学习和更新策略。n 步 TD 学习可以看作是一个广义版本,而在前一节中讨论的演员-评论员算法中使用的 1 步 TD 学习是 n 步 TD 学习算法的特例,n=1。我们来看一个简短的示例,以理解 n 步回报的计算,然后实现一个 Python 方法来计算 n 步回报,我们将在智能体实现中使用它。

n 步回报

n 步回报是一个简单但非常有用的概念,已知能够为多种强化学习算法提供更好的性能,不仅仅是优势演员-评论员算法。例如,目前在57款游戏的 Atari 套件上表现最好的算法,显著超越第二名的算法,便使用了 n 步回报。我们实际上会在第十章中讨论这个智能体算法,名为 Rainbow,探索学习环境的景观:Roboschool, Gym-Retro, StarCraft-II, DMLab

首先,我们需要对 n 步回报过程有一个直观的理解。我们使用以下图示来说明环境中的一步。假设智能体在时间 t=1 时处于状态,并决定采取动作,这导致环境在时间 t=t+1=1+1=2 时过渡到状态,同时智能体获得奖励

我们可以使用以下公式计算 1 步 TD 回报:

这里,是根据价值函数(评论员)对状态的价值估计。实质上,智能体采取一步,并利用所接收到的回报以及智能体对下一个/结果状态的价值估计的折扣值来计算回报。

如果我们让智能体继续与环境交互更多的步数,智能体的轨迹可以用以下简化的图示表示:

该图展示了智能体与环境之间的 5 步交互。采用与前一段中 1 步回报计算类似的方法,我们可以使用以下公式计算 5 步回报:

然后,我们可以在优势演员-评论员算法的步骤 3 中使用此作为 TD 目标,以提高智能体的性能。

你可以通过在任意 Gym 环境中运行 ch8/a2c_agent.py 脚本,设置parameters.json文件中的learning_step_thresh参数为 1(用于 1 步回报)和 5 或 10(用于 n 步回报),来比较具有 1 步回报和 n 步回报的优势演员-评论员智能体的性能。

例如,你可以运行

(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch8$python a2c_agent.py --env Pendulum-v0 使用learning_step_thresh=1,通过以下命令使用 Tensorboard 监控其性能

(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch8/logs$tensorboard --logdir=., 然后在大约一百万步后,你可以比较使用learning_step_thresh=10训练的智能体的性能。请注意,训练后的智能体模型将保存在~/HOIAWOG/ch8/trained_models/A2_Pendulum-v0.ptm。你可以在开始第二次运行之前重命名它或将其移动到不同的目录,以便从头开始训练!

为了使概念更加明确,让我们讨论如何在步骤 3 中以及在优势演员-评论员算法中使用它。我们将首先使用 n 步回报作为 TD 目标,并使用以下公式计算 TD 误差(算法的步骤 3):

然后,我们将按照前一小节中讨论的算法步骤 4 更新评论员。接着,在步骤 5 中,我们将使用以下更新规则来更新评论员:

然后,我们将继续进行算法的步骤 6,使用来自的 5 步过渡,直到,并计算 5 步回报,然后重复更新的过程。

实现 n 步回报计算

如果我们暂停片刻并分析一下发生了什么,你可能会发现我们可能没有充分利用这条 5 步长的轨迹。从状态 开始的代理 5 步长轨迹提供了信息,但我们最终只学到了一个新信息,这仅仅是关于 来更新演员和评论员()。其实,我们可以通过使用相同的 5 步轨迹来计算轨迹中每个状态值的更新,并根据轨迹结束的 n 值使学习过程更加高效。例如,在一个简化的轨迹表示中,如果我们考虑状态 ,并将轨迹的前视部分包含在气泡中,如下图所示:

我们可以使用气泡中的信息来提取状态 的 TD 学习目标。在这种情况下,由于从 处只能获得一步信息,我们将计算 1 步回报,如下方程所示:

如我们之前讨论过的,我们可以使用该值作为方程中的 TD 目标,以获得另一个 TD 误差值,并利用第二个值来更新演员和 ,以及之前的更新()。现在,我们又为代理提供了一个新的学习信息!

如果我们应用相同的直觉,考虑状态 ,并将轨迹的前视部分包含在气泡中,如下图所示:

我们可以使用气泡中的信息来提取 的 TD 学习目标。在这种情况下,从 获得了两种信息,因此我们将使用以下方程计算 2 步回报:

如果我们看一下这个方程和前一个方程,我们可以观察到  和 之间的关系,公式如下所示:

这为代理提供了另一个学习的信息。同样,我们可以从这条轨迹中提取更多的信息。将相同的概念扩展到  和 ,我们可以得到以下关系:

同样,简而言之,我们可以观察到以下内容:

最后,我们还可以观察到以下内容:

简而言之,我们可以从轨迹中的最后一步开始,计算 n 步回报直到轨迹结束,然后回到上一部来使用之前计算的值计算回报。

实现过程直接且简单,建议自己尝试实现。这些内容提供如下,供参考:

def calculate_n_step_return(self, n_step_rewards, final_state, done, gamma):
        """
        Calculates the n-step return for each state in the input-trajectory/n_step_transitions
        :param n_step_rewards: List of rewards for each step
        :param final_state: Final state in this n_step_transition/trajectory
        :param done: True rf the final state is a terminal state if not, False
        :return: The n-step return for each state in the n_step_transitions
        """
        g_t_n_s = list()
        with torch.no_grad():
            g_t_n = torch.tensor([[0]]).float() if done else self.critic(self.preproc_obs(final_state)).cpu()
            for r_t in n_step_rewards[::-1]: # Reverse order; From r_tpn to r_t
                g_t_n = torch.tensor(r_t).float() + self.gamma * g_t_n
                g_t_n_s.insert(0, g_t_n) # n-step returns inserted to the left to maintain correct index order
            return g_t_n_s

深度 n 步优势演员-评论家算法

我们观察到演员-评论家算法结合了基于价值的方法和基于策略的方法。评论家估计价值函数,演员遵循策略,我们研究了如何更新演员和评论家。通过我们在第六章中,使用深度 Q 学习实现最优离散控制的智能代理的经验,我们自然产生了使用神经网络来逼近价值函数,从而代替评论家的想法。我们还可以使用神经网络来表示策略!,在这种情况下,参数!是神经网络的权重。使用深度神经网络来逼近演员和评论家的方法,正是深度演员-评论家算法的核心思想。

实现深度 n 步优势演员评论家代理

我们已经准备好所有实现深度 n 步优势演员-评论家(A2C)代理所需的背景信息。让我们先看看代理实现过程的概述,然后直接进入实际的实现过程。

以下是我们 A2C 代理的高级流程:

  1. 初始化演员和评论家的网络。

  2. 使用演员的当前策略从环境中收集 n 步经验并计算 n 步回报。

  3. 计算演员和评论家的损失。

  4. 执行随机梯度下降优化步骤以更新演员和评论家的参数。

  5. 从第 2 步开始重复。

我们将在一个名为DeepActorCriticAgent的 Python 类中实现该代理。你可以在本书的代码仓库中的第八章找到完整的实现:ch8/a2c_agent.py。我们将使该实现具有灵活性,以便我们可以轻松扩展它,进一步实现批处理版本,并且还可以实现异步版本的 n 步优势演员-评论家代理。

初始化演员和评论家的网络

DeepActorCriticAgent类的初始化很直接。我们将快速浏览它,然后查看我们如何实际定义和初始化演员和评论家的网络。

代理的初始化函数如下所示:

class DeepActorCriticAgent(mp.Process):
    def __init__(self, id, env_name, agent_params):
        """
        An Advantage Actor-Critic Agent that uses a Deep Neural Network to represent it's Policy and the Value function
        :param id: An integer ID to identify the agent in case there are multiple agent instances
        :param env_name: Name/ID of the environment
        :param agent_params: Parameters to be used by the agent
        """
        super(DeepActorCriticAgent, self).__init__()
        self.id = id
        self.actor_name = "actor" + str(self.id)
        self.env_name = env_name
        self.params = agent_params
        self.policy = self.multi_variate_gaussian_policy
        self.gamma = self.params['gamma']
        self.trajectory = [] # Contains the trajectory of the agent as a sequence of Transitions
        self.rewards = [] # Contains the rewards obtained from the env at every step
        self.global_step_num = 0
        self.best_mean_reward = - float("inf") # Agent's personal best mean episode reward
        self.best_reward = - float("inf")
        self.saved_params = False # Whether or not the params have been saved along with the model to model_dir
        self.continuous_action_space = True #Assumption by default unless env.action_space is Discrete

你可能会想知道为什么agent类继承自multiprocessing.Process类。虽然在我们的第一个代理实现中,我们将一个代理运行在一个进程中,但我们可以利用这个灵活的接口,启用并行运行多个代理,从而加速学习过程。

接下来我们将介绍使用 PyTorch 操作定义的神经网络实现的演员和评论员。按照与我们在第六章的深度 Q 学习智能体相似的代码结构,使用深度 Q 学习实现最优离散控制的智能体,在代码库中你会看到我们使用一个名为function_approximator的模块来包含我们的基于神经网络的函数逼近器实现。你可以在本书代码库中的ch8/function_approximator文件夹下找到完整的实现。

由于一些环境具有较小且离散的状态空间,例如Pendulum-v0MountainCar-v0CartPole-v0环境,我们还将实现浅层版本的神经网络,并与深层版本一同使用,以便根据智能体训练/测试所用的环境动态选择合适的神经网络。当你查看演员的神经网络示例实现时,你会注意到在shallowdeep函数逼近器模块中都有一个名为Actor的类和一个不同的类叫做DiscreteActor。这是为了通用性,方便我们根据环境的动作空间是连续的还是离散的,让智能体动态选择和使用最适合表示演员的神经网络。为了实现智能体的完整性和通用性,你还需要了解另外一个变体:我们实现中的shallowdeep函数逼近器模块都有一个ActorCritic类,它是一个单一的神经网络架构,既表示演员又表示评论员。这样,特征提取层在演员和评论员之间共享,神经网络中的不同头(最终层)用于表示演员和评论员。

有时,实现的不同部分可能会令人困惑。为了帮助避免困惑,以下是我们基于神经网络的演员-评论员实现中各种选项的总结:

模块/类描述目的/用例
1. function_approximator.shallow用于演员-评论员表示的浅层神经网络实现。具有低维状态/观察空间的环境。
1.1 function_approximator.shallow.Actor前馈神经网络实现,输出两个连续值:mu(均值)和 sigma(标准差),用于基于高斯分布的策略表示。低维状态/观察空间和连续动作空间。
1.2 function_approximator.shallow.DiscreteActor前馈神经网络,为动作空间中的每个动作输出一个 logit。低维状态/观察空间和离散动作空间。
1.3 function_approximator.shallow.Critic前馈神经网络,输出一个连续值。用于表示评论员/值函数,适用于低维状态/观测空间的环境。
1.4 function_approximator.shallow.ActorCritic前馈神经网络,输出高斯分布的均值(mu)和标准差(sigma),以及一个连续值。用于表示同一网络中的演员(actor)和评论员(critic),适用于低维状态/观测空间的环境。也可以将其修改为离散的演员-评论员网络。
2. function_approximator.deep深度神经网络实现,用于演员(actor)和评论员(critic)表示。适用于具有高维状态/观测空间的环境。
2.1 function_approximator.deep.Actor深度卷积神经网络,输出基于高斯分布的策略表示的均值(mu)和标准差(sigma)。高维状态/观测空间和连续动作空间。
2.2 function_approximator.deep.DiscreteActor深度卷积神经网络,为动作空间中的每个动作输出一个 logit 值。高维状态/观测空间和离散动作空间。
2.3 function_approximator.deep.Critic深度卷积神经网络,输出一个连续值。用于表示评论员/值函数,适用于高维状态/观测空间的环境。
2.4 function_approximator.deep.ActorCritic深度卷积神经网络,输出高斯分布的均值(mu)和标准差(sigma),以及一个连续值。用于表示同一网络中的演员(actor)和评论员(critic),适用于高维状态/观测空间的环境。也可以将其修改为离散的演员-评论员网络。

现在让我们看看run()方法的第一部分,在这里我们根据环境的状态和动作空间的类型,以及根据先前表格中状态空间是低维还是高维的不同,来初始化演员(actor)和评论员(critic)网络:

from function_approximator.shallow import Actor as ShallowActor
from function_approximator.shallow import DiscreteActor as ShallowDiscreteActor
from function_approximator.shallow import Critic as ShallowCritic
from function_approximator.deep import Actor as DeepActor
from function_approximator.deep import DiscreteActor as DeepDiscreteActor
from function_approximator.deep import Critic as DeepCritic

def run(self):
        self.env = gym.make(self.env_name)
        self.state_shape = self.env.observation_space.shape
        if isinstance(self.env.action_space.sample(), int): # Discrete action space
            self.action_shape = self.env.action_space.n
            self.policy = self.discrete_policy
            self.continuous_action_space = False

        else: # Continuous action space
            self.action_shape = self.env.action_space.shape[0]
            self.policy = self.multi_variate_gaussian_policy
        self.critic_shape = 1
        if len(self.state_shape) == 3: # Screen image is the input to the agent
            if self.continuous_action_space:
                self.actor= DeepActor(self.state_shape, self.action_shape, device).to(device)
            else: # Discrete action space
                self.actor = DeepDiscreteActor(self.state_shape, self.action_shape, device).to(device)
            self.critic = DeepCritic(self.state_shape, self.critic_shape, device).to(device)
        else: # Input is a (single dimensional) vector
            if self.continuous_action_space:
                #self.actor_critic = ShallowActorCritic(self.state_shape, self.action_shape, 1, self.params).to(device)
                self.actor = ShallowActor(self.state_shape, self.action_shape, device).to(device)
            else: # Discrete action space
                self.actor = ShallowDiscreteActor(self.state_shape, self.action_shape, device).to(device)
            self.critic = ShallowCritic(self.state_shape, self.critic_shape, device).to(device)
        self.actor_optimizer = torch.optim.Adam(self.actor.parameters(), lr=self.params["learning_rate"])
        self.critic_optimizer = torch.optim.Adam(self.critic.parameters(), lr=self.params["learning_rate"])

使用当前策略收集 n 步经验

下一步是执行所谓的rollouts,即使用当前策略让代理收集n步转移。这个过程本质上是让代理与环境进行交互,并生成新的经验,通常表示为一个包含状态、动作、奖励以及下一个状态的元组,简写为(![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c6563d92c99d4947bd7134747b2528ef~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1770718636&x-signature=PiVn5fzoERuCysikbXU4qpeH5fA%3D), ![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0d1188427cea4c34a9c4843093654acf~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1770718636&x-signature=pj7qZnFxCDQPrgeIJVC0kSDWoPo%3D), ![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fbae1358c9ff4215b66c5b9a60662d95~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1770718636&x-signature=Mp5bBDKRkM5%2BDHGDtz1r1jW6xxU%3D), ![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f1cf02ad1f754b0e9fd3ab9873122069~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1770718636&x-signature=gVPuInbK%2FuXqKqVq0L5YLLEXxhg%3D)),如下图所示:

在前面的示意图中,代理将用如下方式填充它的self.trajectory,该列表包含五个转移:[T1, T2, T3, T4, T5]

在我们的实现中,我们将使用稍微修改过的转换表示法,以减少冗余的计算。我们将使用以下定义来表示转换:

Transition = namedtuple("Transition", ["s", "value_s", "a", "log_prob_a"])

这里,s 是状态,value_s 是评论家对状态 s 的值的预测,a 是采取的行动,log_prob_a 是根据演员/代理当前策略采取行动 a 的概率的对数。

我们将使用之前实现的 calculate_n_step_return(self, n_step_rewards, final_state, done, gamma) 方法,基于包含每个步骤获得的标量奖励值的 n_step_rewards 列表和用于计算评论家对轨迹中最后/最终状态的估计值的 final_state,来计算 n 步回报,正如我们在 n 步回报计算部分中讨论过的那样。

计算演员和评论家的损失

从我们之前讨论的 n 步深度演员-评论家算法的描述中,你可能记得评论家是通过神经网络来表示的,它试图解决一个问题,这个问题类似于我们在第六章中看到的,使用深度 Q 学习实现最优离散控制的智能体,即表示值函数(类似于我们在本章中使用的动作值函数,但稍微简单一些)。我们可以使用标准的均方误差MSE)损失,或者使用平滑的 L1 损失/Huber 损失,这些损失是基于评论家的预测值和在前一步计算的 n 步回报(TD 目标)计算的。

对于演员(actor),我们将使用通过策略梯度定理得到的结果,特别是优势演员-评论家版本,在这种方法中,优势值函数被用来指导演员策略的梯度更新。我们将使用 TD_error,它是优势值函数的无偏估计。

总结来说,评论家和演员的损失如下:

  • critic_loss = MSE(, critic_prediction)

  • actor_loss = log() * TD_error

在捕获了主要的损失计算方程后,我们可以通过 calculate_loss(self, trajectory, td_targets) 方法在代码中实现它们,以下是代码片段的示例:

def calculate_loss(self, trajectory, td_targets):
        """
        Calculates the critic and actor losses using the td_targets and self.trajectory
        :param td_targets:
        :return:
        """
        n_step_trajectory = Transition(*zip(*trajectory))
        v_s_batch = n_step_trajectory.value_s
        log_prob_a_batch = n_step_trajectory.log_prob_a
        actor_losses, critic_losses = [], []
        for td_target, critic_prediction, log_p_a in zip(td_targets, v_s_batch, log_prob_a_batch):
            td_err = td_target - critic_prediction
            actor_losses.append(- log_p_a * td_err) # td_err is an unbiased estimated of Advantage
            critic_losses.append(F.smooth_l1_loss(critic_prediction, td_target))
            #critic_loss.append(F.mse_loss(critic_pred, td_target))
        if self.params["use_entropy_bonus"]:
            actor_loss = torch.stack(actor_losses).mean() - self.action_distribution.entropy().mean()
        else:
            actor_loss = torch.stack(actor_losses).mean()
        critic_loss = torch.stack(critic_losses).mean()

        writer.add_scalar(self.actor_name + "/critic_loss", critic_loss, self.global_step_num)
        writer.add_scalar(self.actor_name + "/actor_loss", actor_loss, self.global_step_num)

        return actor_loss, critic_loss

更新演员-评论家模型

在我们计算了演员和评论家的损失后,学习过程的下一步也是最后一步,是基于损失更新演员和评论家的参数。由于我们使用了强大的 PyTorch 库,它自动处理部分微分、误差反向传播和梯度计算,因此在使用前面步骤的结果时,代码实现简单而直接,如下所示的代码示例所示:

def learn(self, n_th_observation, done):
        td_targets = self.calculate_n_step_return(self.rewards, n_th_observation, done, self.gamma)
        actor_loss, critic_loss = self.calculate_loss(self.trajectory, td_targets)

        self.actor_optimizer.zero_grad()
        actor_loss.backward(retain_graph=True)
        self.actor_optimizer.step()

        self.critic_optimizer.zero_grad()
        critic_loss.backward()
        self.critic_optimizer.step()

        self.trajectory.clear()
        self.rewards.clear()

保存/加载、日志记录、可视化和监控工具

在前面的章节中,我们走访了智能体学习算法实现的核心部分。除了这些核心部分,还有一些实用函数,我们将使用它们在不同的学习环境中训练和测试智能体。我们将重用在第六章中已经开发的组件,使用深度 Q 学习实现最优离散控制的智能体,例如utils.params_manager,以及save()load()方法,它们分别用于保存和加载智能体训练好的大脑或模型。我们还将使用日志工具来记录智能体的进展,以便使用 Tensorboard 进行良好的可视化和快速查看,还可以用于调试和监控,以观察智能体的训练过程中是否存在问题。

有了这些,我们就可以完成 n 步优势演员-评论员智能体的实现!你可以在ch8/a2c_agent.py文件中找到完整的实现。在我们看看如何训练智能体之前,在下一节中,我们将快速了解一下可以应用于深度 n 步优势智能体的一个扩展,以使它在多核机器上表现得更好。

扩展 - 异步深度 n 步优势演员-评论员

我们可以对智能体实现做的一个简单扩展是,启动多个智能体实例,每个实例都有自己的学习环境实例,并以异步方式返回它们从中学到的更新,也就是说,它们在有可用更新时发送,而无需进行时间同步。这种算法通常被称为 A3C 算法,A3C 是异步优势演员-评论员的缩写。

这个扩展背后的动机之一来源于我们在第六章中学到的内容,使用深度 Q 学习实现最优离散控制的智能体,特别是使用经验回放内存。我们的深度 Q 学习智能体在加入经验回放内存后,学习效果显著提升,本质上它帮助解决了序列决策问题中的依赖性问题,并使智能体能从过去的经验中提取更多的信息。类似地,使用多个并行运行的演员-学习者实例的想法,也被发现有助于打破过渡之间的相关性,并有助于探索环境状态空间的不同部分,因为每个演员-学习者进程都有自己的策略参数和环境实例来进行探索。一旦并行运行的智能体实例有一些更新需要发送回来,它们会将这些更新发送到一个共享的全局智能体实例,这个全局实例作为其他智能体实例同步的新参数来源。

我们可以使用 Python 的 PyTorch 多进程库来实现这个扩展。没错!你猜对了。这正是我们在实现中让DeepActorCritic代理一开始就继承了torch.multiprocessing.Process的原因,这样我们就可以在不进行重大代码重构的情况下将这个扩展添加到其中。如果你有兴趣,可以查看书籍代码仓库中的ch8/README.md文件,获取更多关于探索这一架构的资源。

我们可以轻松扩展a2c_agent.py中 n 步优势演员-评论员代理的实现,来实现同步深度 n 步优势演员-评论员代理。你可以在ch8/async_a2c_agent.py中找到异步实现。

训练一个智能和自主驾驶的代理

现在我们已经具备了实现本章目标所需的所有部分,目标是组建一个智能的自主驾驶代理,并训练它在我们在上一章中开发的、使用 Gym 接口构建的真实感 CARLA 驾驶环境中自主驾驶。代理的训练过程可能需要一段时间。根据你用来训练代理的机器硬件,训练可能需要从几个小时(例如Pendulum-v0CartPole-v0和一些 Atari 游戏等简单环境)到几天(例如 CARLA 驾驶环境等复杂环境)。为了先了解训练过程以及在训练期间如何监控进展,我们将从一些简单的示例开始,逐步走过训练和测试代理的整个过程。然后,我们将展示如何轻松地将其转移到 CARLA 驾驶环境中,进一步进行训练。

训练和测试深度 n 步优势演员-评论员代理

因为我们的代理实现是通用的(如上一节第 1 步中通过表格讨论的那样),我们可以使用任何具有 Gym 兼容接口的学习环境来训练/测试代理。你可以在本书初期章节中讨论的各种环境中进行实验并训练代理,下一章我们还会讨论一些更有趣的学习环境。别忘了我们的自定义 CARLA 自动驾驶环境!

我们将选择一些环境作为示例,逐步展示如何启动训练和测试过程,帮助你开始自己的实验。首先,更新你从书籍代码仓库 fork 的代码,并cdch8文件夹,这里存放着本章的代码。像往常一样,确保激活为本书创建的 conda 环境。完成这些后,你可以使用a2c_agent.py脚本启动 n 步优势演员评论员代理的训练过程,示例如下:

(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch8$ python a2c_agent --env Pendulum-v0

你可以将Pendulum-v0替换为任何在你的机器上设置好的兼容 Gym 的学习环境名称。

这应该会启动代理的训练脚本,它将使用~/HOIAWOG/ch8/parameters.json文件中指定的默认参数(你可以更改这些参数以进行实验)。如果有可用的已训练代理的大脑/模型,它还会从~/HOIAWOG/ch8/trained_models目录加载适用于指定环境的模型,并继续训练。对于高维状态空间环境,例如 Atari 游戏或其他环境,其中状态/观察是场景的图像或屏幕像素,将使用我们在之前章节中讨论的深度卷积神经网络,这将利用你机器上的 GPU(如果有的话)来加速计算(如果你希望禁用此功能,可以在parameters.json文件中将use_cuda = False)。如果你的机器上有多个 GPU,并且希望在不同的 GPU 上训练不同的代理,可以通过--gpu-id标志指定 GPU 设备 ID,要求脚本在训练/测试时使用特定的 GPU。

一旦训练过程开始,你可以通过从logs目录运行以下命令来启动tensorboard,监控代理的进程:

(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch8/logs$ tensorboard --logdir .

在使用上述命令启动tensorboard后,你可以访问http://localhost:6006网页来监控代理的进展。这里提供了示例截图供你参考;这些截图来自两次 n 步优势演员-评论员代理的训练运行,使用了不同的n步值,并使用了parameters.json文件中的learning_step_threshold参数:

演员-评论员(使用单独的演员和评论员网络):

    • Pendulum-v0 ; n-step (learning_step_threshold = 100)

2.  - Pendulum-v0; n-step (learning_step_threshold = 5)

  • 比较 1(100 步 AC,绿色)和 2(5 步 AC,灰色)在Pendulum-v0上进行 1000 万步训练:

训练脚本还会将训练过程的摘要输出到控制台。如果你想可视化环境,以查看代理正在做什么或它是如何学习的,可以在启动训练脚本时在命令中添加--render标志,如下所示:

(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch8$ python a2c_agent --env CartPole-v0 --render

如你所见,我们已经达到一个阶段,你只需一条命令即可开始训练、记录并可视化代理的表现!到目前为止,我们已经取得了非常好的进展。

您可以在相同的环境或不同的环境上使用不同参数集运行多个代理实验。前面的示例被选择来展示其在更简单的环境中的表现,以便您可以轻松地运行全长实验并重现和比较结果,无论您可能拥有的硬件资源如何。作为本书代码库的一部分,针对一些环境提供了训练代理脑/模型,以便您可以快速启动并在测试模式下运行脚本,查看训练代理在任务中的表现。它们可以在您 fork 的书籍代码库的ch8/trianed_models文件夹中找到,或者在此处的上游源:github.com/PacktPublishing/Hands-On-Intelligent-Agents-with-OpenAI-Gym/tree/master/ch8/trained_models。您还会在书籍代码库中找到其他资源,例如其他环境中的学习曲线插图和代理在各种环境中表现的视频剪辑,供您参考。

一旦您准备好测试代理,可以使用您自己训练的代理的脑模型或使用预训练的代理脑,您可以使用--test标志来表示您希望禁用学习并在测试模式下运行代理。例如,要在LunarLander-v2环境中测试代理,并且打开学习环境的渲染,您可以使用以下命令:

(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch8$ python a2c_agent --env LunarLander-v2 --test --render

我们可以互换地使用我们讨论的异步代理作为我们基础代理的扩展。由于两种代理实现都遵循相同的结构和配置,我们可以通过仅使用async_a2c_agent.py脚本来轻松切换到异步代理训练脚本。它们甚至支持相同的命令行参数,以简化我们的工作。当使用async_a2c_agent.py脚本时,您应确保根据您希望代理使用的进程数或并行实例数在parameters.json文件中设置num_agents参数。例如,我们可以使用以下命令在BipedalWalker-v2环境中训练我们的异步代理版本:

(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch8$ python async_a2c_agent --env BipedalWalker-v2 

正如您可能已经意识到的那样,我们的代理实现能够学习在各种不同的环境中行动,每个环境都有其自己的任务集,以及它们自己的状态、观察和行动空间。正是这种多功能性使得基于深度强化学习的代理变得流行,并适合解决各种问题。现在我们已经熟悉了训练过程,我们终于可以开始训练代理驾驶汽车,并跟随 CARLA 驾驶模拟器中的车道行驶。

在 CARLA 驾驶模拟器中训练代理驾驶汽车。

让我们开始在 CARLA 驾驶环境中训练一个代理!首先,确保你的 GitHub 分支是最新的,已经与上游主分支同步,这样你就能获得来自书籍仓库的最新代码。由于我们在上一章创建的 CARLA 环境与 OpenAI Gym 接口兼容,因此使用 CARLA 环境进行训练就像使用任何其他 Gym 环境一样简单。你可以使用以下命令训练 n 步优势演员-评论家代理:

(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch8$ python a2c_agent --env Carla-v0

这将启动代理的训练过程,像我们之前看到的那样,进度摘要将被打印到控制台窗口,并且日志会被写入logs文件夹,你可以使用tensorboard查看它们。

在训练过程的初期阶段,你会注意到代理驾驶汽车像疯了一样!

经过几小时的训练,你会看到代理能够控制汽车,成功地沿着道路行驶,同时保持在车道内并避免撞到其他车辆。你可以在ch8/trained_models文件夹中找到一个经过训练的自动驾驶代理模型,方便你快速让代理进行测试驾驶!你还可以在书籍的代码仓库中找到更多资源和实验结果,帮助你的学习和实验。祝你实验愉快!

总结

在本章中,我们从基础开始,动手实践了基于演员-评论家架构的深度强化学习代理。我们从策略梯度方法的介绍开始,逐步讲解了表示策略梯度优化的目标函数、理解似然比技巧,最后推导出策略梯度定理。接着,我们了解了演员-评论家架构如何利用策略梯度定理,并使用演员组件表示代理的策略,使用评论家组件表示状态/动作/优势值函数,具体实现取决于架构的实现方式。在对演员-评论家架构有了直观的理解之后,我们进入了 A2C 算法,并讨论了其中涉及的六个步骤。然后,我们通过图示讨论了 n 步回报的计算,并展示了如何在 Python 中轻松实现 n 步回报计算方法。接着,我们逐步实现了深度 n 步优势演员-评论家代理。

我们还讨论了如何使实现变得灵活和通用,以适应各种不同的环境,这些环境可能具有不同的状态、观察和动作空间维度,并且可能是连续的或离散的。接着,我们探讨了如何在不同进程中并行运行多个智能体实例,以提高学习性能。在最后一部分,我们走过了训练智能体过程中涉及的步骤,一旦智能体训练完成,我们可以使用--test--render标志来测试智能体的表现。我们从简单的环境开始,以便熟悉训练和监控过程,最终实现了本章的目标——在 CARLA 驾驶模拟器中训练一个智能体,使其能够自主驾驶一辆车!希望你在阅读这一相对较长的章节时学到了很多东西。到此为止,你已经积累了理解和实现本章以及第六章中两大类高性能学习智能体算法的经验,《使用深度 Q 学习实现最优离散控制的智能体》。在下一章,我们将探索新的有前景的学习环境,在这些环境中,你可以训练自定义智能体,并开始朝着下一个层次取得进展。

第九章:探索学习环境的全景 - Roboschool、Gym-Retro、StarCraft-II、DeepMindLab

在你不断积累经验的过程中,已经走过了很长一段路,目的是通过动手实践构建智能体,解决各种具有挑战性的问题。在前几章中,我们探讨了 OpenAI Gym 中提供的几个环境。在本章中,我们将超越 Gym,看看一些其他开发完善的环境,供你训练智能体或进行实验。

在我们查看其他提供良好学习环境的开源库,以帮助开发智能体之前,先来看一下最近添加到 OpenAI Gym 库中的一类环境。如果你像我一样对机器人技术感兴趣,你一定会非常喜欢这个。没错!它就是机器人环境类,提供了许多非常有用的环境,用于机器人臂执行抓取、滑动、推动等操作。这些机器人环境基于 MuJoCo 引擎,你可能还记得在第三章, *《OpenAI Gym 和深度强化学习入门》*中提到过,MuJoCo 引擎需要付费许可,除非你是学生,并且仅用于个人或课堂用途。以下截图总结了这些机器人环境,包括每个环境的名称和简要描述,供你参考,如果你有兴趣探索这些问题:

与 Gym 接口兼容的环境

在本节中,我们将深入探讨与 Gym 接口完全兼容的环境。你应该能够在这些环境中使用我们在前几章中开发的任何智能体。让我们开始,看看一些非常有用且充满前景的学习环境。

Roboschool

Roboschool (https://github.com/openai/roboschool) 提供了几个用于仿真控制机器人的环境。它由 OpenAI 发布,且这些环境与我们在本书中使用的 OpenAI Gym 环境接口相同。Gym 的基于 MuJoCo 的环境提供了丰富多样的机器人任务,但 MuJoCo 在免费试用期过后需要许可证。Roboschool 提供了八个环境,这些环境与 MuJoCo 环境非常相似,这是一个好消息,因为它提供了一个免费的替代方案。除了这八个环境,Roboschool 还提供了几个新的、具有挑战性的环境。

以下表格展示了 MuJoCo Gym 环境与 Roboschool 环境之间的快速对比:

简要描述MuJoCo 环境Roboschool 环境
让一只单腿 2D 机器人尽可能快地向前跳跃Hopper-v2RoboschoolHopper-v1
让一个 2D 机器人行走Walker2d-v2RoboschoolWalker2d-v1
让一个四足 3D 机器人行走Ant-v2RoboschoolAnt-v1
让一个双足 3D 机器人尽可能快地行走而不摔倒Humanoid-v2RoboschoolHumanoid-v1

以下表格提供了 Roboschool 库中可用环境的完整列表,包括它们的状态空间和动作空间,供您快速参考:

环境 IDRoboschool 环境观察空间动作空间
RoboschoolInvertedPendulum-v1Box(5,)Box(1,)
RoboschoolInvertedPendulumSwingup-v1Box(5,)Box(1,)
RoboschoolInvertedDoublePendulum-v1Box(9,)Box(1,)
RoboschoolReacher-v1Box(9,)Box(2,)
RoboschoolHopper-v1Box(15,)Box(3,)
RoboschoolWalker2d-v1Box(22,)Box(6,)
RoboschoolHalfCheetah-v1Box(26,)Box(6,)
RoboschoolAnt-v1Box(28,)Box(8,)
RoboschoolHumanoid-v1Box(44,)Box(17,)
RoboschoolHumanoidFlagrun-v1Box(44,)Box(17,)
RoboschoolHumanoidFlagrunHarder-v1Box(44,)Box(17,)
RoboschoolPong-v1Box(13,)Box(2,)

快速入门指南:设置和运行 Roboschool 环境

Roboschool 环境使用开源的 Bulletphysics 引擎,而不是专有的 MuJoCo 引擎。让我们快速浏览一个 Roboschool 环境,以便您了解如何使用 Roboschool 库中的任何环境,如果您觉得它对您的工作有帮助的话。首先,我们需要在rl_gym_book conda 环境中安装 Roboschool Python 库。由于该库依赖于多个组件,包括 Bulletphysics 引擎,因此安装过程涉及几个步骤,具体步骤可以在官方 Roboschool GitHub 仓库中找到:github.com/openai/roboschool。为了简化操作,您可以使用本书代码仓库中的脚本ch9/setup_roboschool.sh,该脚本会自动编译和安装Roboschool库。请按照以下步骤运行该脚本:

  1. 激活rl_gym_book conda 环境,使用source activate rl_gym_book

  2. 使用cd ch9导航到ch9文件夹。

  3. 确保脚本的执行权限已设置为chmod a+x setup_roboschool.sh

  4. 使用sudo运行脚本:./setup_roboschool.sh

这应该会安装所需的系统依赖项,获取并编译兼容的 bullet3 物理引擎源代码;将 Roboschool 源代码拉取到您主目录下的software文件夹;最后在rl_gym_book conda 环境中编译、构建并安装 Roboschool 库。如果设置成功完成,您将在控制台看到以下信息:

Setup completed successfully. You can now import roboschool and use it. If you would like to \test the installation, you can run: python ~/roboschool/agent_zoo/demo_race2.py"

您可以使用以下命令运行一个快速入门演示脚本:

`(rl_gym_book) praveen@ubuntu:~$ python ~/roboschool/agent_zoo/demo_race2.py`

这将启动一场有趣的机器人竞赛,您将看到一只跳跃者、半猎豹和类人机器人进行比赛!有趣的是,每个机器人都由基于强化学习训练的策略控制。比赛将看起来像这个快照:

安装完成后,您可以创建一个 Roboschool 环境,并使用我们在前面章节中开发的代理来训练并在这些环境中运行。

您可以使用本章代码仓库中的run_roboschool_env.py脚本 github.com/PacktPublishing/Hands-On-Intelligent-Agents-with-OpenAI-Gym/tree/master/ch9 来查看任何 Roboschool 环境。例如,要查看RoboschoolInvertedDoublePendulum-v1环境,您可以运行以下脚本:

(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch9$python run_roboschool_env.py --env RoboschoolInvertedDoublePendulum-v1

您可以使用前面表格中列出的任何其他 Roboschool 环境名称,以及在发布时提供的新的 Roboschool 环境。

Gym retro

Gym Retro (github.com/openai/retro) 是 OpenAI 于 2018 年 5 月 25 日发布的一个相对较新的 Python 库 (blog.openai.com/gym-retro/),作为一个用于开发游戏玩法强化学习算法的研究平台。尽管在 OpenAI Gym 中有一个包含 60 多个游戏的 Atari 游戏合集,但可用的游戏总数有限。Gym Retro 支持使用为多个控制台/复古游戏平台开发的游戏,例如任天堂的 NES、SNES、Game Boy 控制台、世嘉 Genesis 和世嘉 Master System 等。通过使用 Libretro API 的模拟器,这一切成为可能:

Gym Retro 提供了便捷的包装器,将超过 1000 款视频游戏转化为兼容 Gym 接口的学习环境!是不是很棒!多个新的学习环境,但接口保持一致,这样我们就可以轻松地训练和测试我们到目前为止开发的代理,无需对代码做任何必要的修改……

为了感受在 Gym Retro 中使用环境的简易性,我们暂时把安装步骤放在一边,快速看看安装后如何创建一个新的 Gym Retro 环境的代码:

import retro
env = retro.make(game='Airstriker-Genesis', state='Level1')

这段代码片段将创建一个env对象,该对象具有与我们之前看到的所有 Gym 环境相同的接口和方法,例如step(...)reset()render()

Gym Retro 的快速入门指南

让我们通过使用以下命令,快速安装预构建的二进制文件并试用 Gym Retro 库:

(rl_gym_book) praveen@ubuntu:~/rl_gym_book/ch9$ pip install gym-retro

安装成功后,我们可以通过以下脚本快速查看其中一个可用的 Gym Retro 环境:

#!/usr/bin/env python
import retro

if __name__ == '__main__':
   env = retro.make(game='Airstriker-Genesis', state='Level1')
    obs = env.reset()
    while True:
        obs, rew, done, info = env.step(env.action_space.sample())
        env.render()
        if done:
            obs = env.reset()

运行此脚本后,将会弹出一个窗口,显示《Airstriker》游戏并展示飞船执行随机动作。游戏窗口将呈现如下图所示:

在我们继续之前,值得注意的是,包含整个游戏数据的ROM只读内存)文件并不是所有游戏都可以免费获得。一些非商业性的游戏机游戏的 ROM 文件,如《Airstriker》(在前面的脚本中使用的游戏)、《Fire》、Dekadrive、Automaton、《Fire》、《Lost Marbles》等,已包含在 Gym Retro 库中,可以免费使用。其他游戏,如《刺猬索尼克》系列(《刺猬索尼克》、《刺猬索尼克 2》、《刺猬索尼克 3 & Knuckles》),需要购买 ROM 文件以合法使用,可以通过像Steam这样的平台进行购买。这对于希望在此类环境中开发算法的爱好者、学生以及其他热衷者来说,是一个障碍。但至少这个障碍相对较小,因为在 Steam 上购买《刺猬索尼克》的 ROM 大约需要 1.69 美元。一旦你拥有了游戏的 ROM 文件,Gym Retro 库提供了一个脚本,允许你将这些文件导入到库中,方法如下:

(rl_gym_book) praveen@ubuntu:~/rl_gym_book/ch9$ python -m retro.import /PATH/TO/YOUR/ROMs/DIRECTORY
OpenAI Universe 

请注意,在创建新的 Gym Retro 环境时,我们需要提供游戏名称以及游戏状态retro.make(game='游戏名称', state='状态名称')

若要获取可用 Gym Retro 环境的列表,可以运行以下命令:

(rl_gym_book) praveen@ubuntu:~/rl_gym_book/ch9$ python -c "import retro; retro.list_games()"

若要获取可用游戏状态的列表,可以运行以下 Python 脚本:

#!/usr/bin/evn python
import retro
for game in retro.list_games():
    print(game, retro.list_states(game))

到目前为止,我们已经熟悉了 Gym Retro 库。接下来,分析一下这个库在我们已经看到和使用的内容之外,提供了哪些优势或新特性。首先,Gym Retro 库使用了比 Atari 主机更新的游戏主机(如 SEGA Genesis)。做个对比,SEGA Genesis 游戏主机的 RAM 是 Atari 主机的 500 倍,这使得它能够提供更好的视觉效果和更广泛的控制选项。这为我们提供了相对复杂的学习环境,并且为我们的智能体提供了一些更复杂的任务和挑战,供它们学习和解决。其次,这些主机游戏中的几个是渐进性的,游戏的复杂度通常随着每一关的提升而增加,而且各关卡在某些方面(例如目标、物体外观、物理效果等)具有许多相似之处,同时在其他方面(如布局、新物体等)也提供了多样性。这样的训练环境,随着难度逐步增加的关卡,帮助开发能够学习解决一般任务的智能体,而不仅仅是特定的任务或环境(如监督学习中的过拟合)。智能体能够学会将自己在一个关卡中的技能和知识转移到下一个关卡,进而转移到另一个游戏中。这个领域正在积极研究,通常被称为课程学习、分阶段学习或渐进式进化。毕竟,我们最终的目标是开发能够学习解决一般任务的智能体,而不仅仅是训练中给定的具体任务。Gym Retro 库提供了一些有用的、仅限于游戏的环境来促进此类实验和研究。

其他开源基于 Python 的学习环境

在本节中,我们将讨论一些最近的基于 Python 的学习环境,这些环境为智能体开发提供了良好的平台,但不一定具有与 Gym 兼容的环境接口。虽然它们没有提供与 Gym 兼容的接口,但本节中讨论的这些环境经过精心挑选,确保要么已经有 Gym 的包装器(使其与 Gym 接口兼容),要么它们很容易实现,可以用来测试和实验我们在本书中开发的智能体。正如你所猜测的那样,未来将有更多的优秀基于 Python 的智能体开发学习环境出现,因为这一领域目前正在积极研究。本书的代码库将提供新环境的信息和快速入门指南,一旦这些新环境发布,你可以在本书的 GitHub 仓库中注册获取更新通知。在接下来的子章节中,我们将讨论一些现成的、有前景的学习环境,供大家使用。

星际争霸 II - PySC2

《星际争霸 II》非常受欢迎,实际上是有史以来最成功的实时战略游戏之一,全球有数百万人在玩这款游戏。它甚至有一个世界锦标联赛(wcs.starcraft2.com/en-us/)!游戏环境相当复杂,主要目标是建立一个军队基地,管理经济,防守基地,并摧毁敌人。玩家从第三人称视角控制基地和军队。如果你不熟悉《星际争霸》,你应该先观看几场在线比赛,以便了解游戏的复杂性和快速节奏。

人类要想在这款实时战略游戏中表现出色,需要大量的练习(甚至需要几个月,实际上职业玩家需要数年训练),计划和快速反应。尽管软件代理可以在每帧中按下多个软件按钮,做出相当快速的动作,但行动速度并不是唯一决定胜利的因素。代理还需要进行多任务处理和微管理军队单位,并最大化得分,这比 Atari 游戏复杂几个数量级。

《星际争霸 II》的制作公司暴雪发布了《星际争霸 II》API,提供了与《星际争霸 II》游戏接口的必要钩子,使得玩家可以不受限制地控制游戏。这为开发我们所追求的智能代理提供了新的可能性。暴雪甚至为在 AI 和机器学习许可下公开使用该环境提供了单独的最终用户许可协议EULA)!对于像暴雪这样从事游戏制作和销售的公司来说,这一举措非常受欢迎。暴雪将StarCraft2SC2)客户端协议实现开源,并提供了 Linux 安装包,以及多个附加组件,如地图包,用户可以从他们的 GitHub 页面免费下载,链接为github.com/Blizzard/s2client-proto。除此之外,Google DeepMind 还开源了他们的 PySC2 库,通过 Python 暴露了 SC2 客户端接口,并提供了一个封装,使其成为一个强化学习(RL)环境。

以下截图展示了 PySC2 的用户界面,右侧是可供代理作为观察的特征层,左侧是游戏场景的简化概览:

如果你对这些类型的环境感兴趣,尤其是如果你是游戏开发者,你可能也对 Dota 2 环境感兴趣。Dota 2 是一款实时战略游戏,和 StarCraft II 类似,由两队五名玩家进行对战,每个玩家控制一个英雄角色。你可以了解更多关于 OpenAI 如何开发一个由五个神经网络代理组成的团队,他们学会了团队协作,单日内进行 180 年游戏量的训练,学习如何克服多个挑战(包括高维连续状态和动作空间以及长期的决策时间),而这一切都发生在自我对战中!你可以在 blog.openai.com/openai-five… 阅读更多关于五代理团队的内容。

StarCraft II PySC2 环境设置和运行的快速入门指南

我们将展示如何快速设置并开始使用 StarCraft II 环境。和往常一样,请使用代码库中的 README 文件获取最新的操作说明,因为链接和版本可能会发生变化。如果你还没有这么做,请为该书的代码库添加星标并关注,以便收到关于更改和更新的通知。

下载 StarCraft II Linux 包

github.com/Blizzard/s2… 下载 StarCraft 游戏的最新 Linux 包,并将其解压到硬盘上的 ~/StarCraftII 目录。例如,要将版本 4.1.2 下载到你的~/StarCraftII/文件夹中,可以使用以下命令:

wget http://blzdistsc2-a.akamaihd.net/Linux/SC2.4.1.2.60604_2018_05_16.zip -O ~/StarCraftII/SC2.4.1.2.zip

让我们将文件解压到~/StarCraftII/目录:

unzip ~/StarCraftII/SC2.4.1.2.zip -d ~/StarCraftII/

请注意,正如下载页面所述,这些文件是受密码保护的,密码是'iagreetotheeula

通过输入该命令,暴雪确保我们同意接受其 AI 和机器学习许可协议,详情请见下载页面。

下载 SC2 地图

我们需要 StarCraft II 地图包和迷你游戏包才能开始。

github.com/Blizzard/s2client-proto#map-packs下载地图包

解压到你的~/StarCraftII/Maps目录。

作为示例,我们将使用以下命令下载 2018 年第二赛季发布的梯子地图:

wget http://blzdistsc2-a.akamaihd.net/MapPacks/Ladder2018Season2_Updated.zip -O ~/StarCraftII/Maps/Ladder2018S2.zip

让我们将地图解压到~/StarCraftII/Maps目录:

unzip ~/StarCraftII/Maps/Ladder2018S2.zip -d ~/StarCraftII/Maps/

接下来,我们将下载并解压迷你游戏地图文件:

wget https://github.com/deepmind/pysc2/releases/download/v1.2/mini_games.zip -O ~/StarCraftII/Maps/mini_games1.2.zip

unzip ~/StarCraftII/Maps/mini_games1.2.zip -d ~/StarCraftII/Maps

安装 PySC2

让我们安装 PySC2 库以供 RL 环境接口使用,并安装所需的依赖项。这个步骤会很简单,因为 PySC2 库在 PyPi 上有提供 Python 包:

(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch9$ pip install pysc2

玩 StarCraft II 或运行示例代理

为了测试安装是否成功并查看 StarCraft II 学习环境的样子,你可以使用以下命令在 Simple64 地图或 CollectMineralShards 地图上快速启动一个随机行为的代理:

(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch9$ python -m pysc2.bin.agent --map Simple64

你也可以加载环境中的其他可用地图。例如,以下命令加载 CollectMineralShards 地图:

(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch9$ python -m pysc2.bin.agent --map CollectMineralShards

这应该会弹出一个 UI,显示随机智能体所采取的动作,帮助你了解有效的动作是什么,并帮助你可视化智能体在环境中行动时的情况。

若要自己玩这个游戏,PySC2 提供了一个人类智能体接口,这对于调试(如果你感兴趣,也可以用来玩游戏!)非常有用。以下是运行并亲自玩游戏的命令:

(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch9$ python -m pysc2.bin.play --map Simple64

你还可以运行一个示例智能体,该智能体的脚本任务是收集矿物碎片,这是游戏中的一项任务,使用以下命令:

(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch9$ python -m pysc2.bin.agent --map CollectMineralShards --agent pysc2.agents.scripted_agent.CollectMineralShards

请关注本书的代码库,获取新的智能体源代码以及训练和测试具有高级技能的新智能体的说明。你还可以自定义我们在上一章中开发的智能体,学习如何玩《星际争霸 II》。如果你这样做了,请向本书的代码库提交一个拉取请求,给作者发封邮件,或者大声说出来,让大家知道你做了哪些很酷的事情!

DeepMind lab

DeepMind Lab (github.com/deepmind/lab) 是一个 3D 学习环境,提供了一系列具有挑战性任务的环境,例如通过迷宫进行 3D 导航和解谜。它是基于一系列开源软件构建的,包括著名的《Quake III Arena》。

环境接口与我们在本书中广泛使用的 Gym 接口非常相似。为了让你了解环境接口的实际样子,可以查看以下代码片段:

import deepmind_lab
num_steps = 1000
config = {  
    'width': 640,
    'height': 480,
    'fps': 30
}
...
env = deepmind_lab.Lab(level, ['RGB_INTERLEAVED'], config=config, renderer='software')

for step in range(num_steps) 
if done:
    env.reset()
obs = env.observations()
action = agent.get_action(...)
reward = env.step(action, num_steps=1)
done = not env.is_running()

这段代码虽然与 OpenAI Gym 接口不是一一兼容,但提供了一个非常相似的接口。

DeepMind Lab 学习环境接口

我们将简要讨论 DeepMind Lab (DM Lab) 的环境接口,以便你能够熟悉它,看到它与 OpenAI Gym 接口的相似之处,并开始在 DM Lab 环境中进行智能体实验!

reset(episode=-1, seed=None)

这类似于我们在 Gym 接口中看到的 reset() 方法,但与 Gym 环境不同,DM Lab 的 reset 方法调用不会返回观察结果。我们稍后会看到如何获取观察结果,所以现在我们讨论的是 DM Lab 的 reset(episode=-1, seed=None) 方法。它将环境重置为初始状态,并且需要在每一集的结尾调用,以便创建新的集。可选的 episode 参数接受一个整数值,用于指定特定集中的关卡。如果未设置 episode 值,或者将其设置为 -1,则关卡将按数字顺序加载。seed 参数也是可选的,用于为环境的随机数生成器设置种子,以便实现可重复性。

step(action, num_steps=1)

这与 Gym 接口的step(action)方法类似,但与reset(...)方法一样,调用该方法不会返回下一个观测(或奖励、结束状态、信息)。调用此方法会将环境推进num_steps帧,且在每一帧中执行由action定义的动作。这种动作重复的行为在我们希望相同的动作在连续四帧左右应用时非常有用,事实上,几位研究人员发现这种方法有助于学习。有一些 Gym 环境包装器可以实现这种动作重复行为。

observations()

这是我们在调用reset(...)step(action)之后用来接收来自 DM Lab 环境的观测的方法。该方法返回一个 Python 字典对象,其中包含我们从环境的可用类型列表中指定的每种类型的观测。例如,如果我们希望将环境的RGBD红绿蓝深度)信息作为观测类型,我们可以在初始化环境时通过'RGBD'键指定,随后可以通过相同的'RGBD'键从返回的观测字典中检索该信息。下面是一个简单的示例来说明这一点:

env = deepmind_lab.Lab('tests/empty_room_test', ['RGBD'])
env.reset()
obs = env.observations()['RGBD']

DM Lab 环境还支持其他类型的观测。我们可以使用observation_spec()来获取支持的观测类型列表,我们将在稍后详细讨论。

is_running()

这个方法类似于(但意义相反)Gym 接口的step(action)方法返回的done布尔值。

当环境完成一个回合或停止运行时,该方法将返回False。只要环境仍在运行,它将返回True

observation_spec()

这个方法类似于我们在 Gym 环境中使用的env.observation_space()。该方法返回一个列表,指定了 DM Lab 环境支持的所有可用观测。它还包括有关与关卡相关的自定义观测的规格说明。

这些规格包含了如果在观测列表中指定了该规格名称时将返回的张量或字符串的名称、类型和形状(例如之前提到的'RGBD'示例)。例如,以下代码片段列出了列表中将返回的两个项,帮助你了解规格内容:

{
    'dtype': <type 'numpy.uint8'>, ## Array data type
    'name': 'RGBD',                ## Name of observation.
    'shape': (4, 180, 320)         ## shape of the array. (Heights, Width, Colors)
}

{    
    'name': 'RGB_INTERLEAVED', ## Name of observation.
    'dtype': <type 'numpy.uint8'>, ## Data type array.     
    'shape': (180, 320, 3) ## Shape of array. (Height, Width, Colors)
}

为了快速理解如何使用这个方法,让我们看看以下代码行和输出:

import deepmind_lab
import pprint
env = deepmind_lab.Lab('tests/empty_room_test', [])
observation_spec = env.observation_spec()
pprint.pprint(observation_spec)
# Outputs:
[{'dtype': <type 'numpy.uint8'>, 'name': 'RGB_INTERLEAVED', 'shape': (180, 320, 3)},
 {'dtype': <type 'numpy.uint8'>, 'name': 'RGBD_INTERLEAVED', 'shape': (180, 320, 4)},
 {'dtype': <type 'numpy.uint8'>, 'name': 'RGB', 'shape': (3, 180, 320)},
 {'dtype': <type 'numpy.uint8'>, 'name': 'RGBD', 'shape': (4, 180, 320)},
 {'dtype': <type 'numpy.uint8'>, 'name': 'BGR_INTERLEAVED', 'shape': (180, 320, 3)},
 {'dtype': <type 'numpy.uint8'>, 'name': 'BGRD_INTERLEAVED', 'shape': (180, 320, 4)},
 {'dtype': <type 'numpy.float64'>, 'name': 'MAP_FRAME_NUMBER', 'shape': (1,)},
 {'dtype': <type 'numpy.float64'>, 'name': 'VEL.TRANS', 'shape': (3,)},
 {'dtype': <type 'numpy.float64'>, 'name': 'VEL.ROT', 'shape': (3,)},
 {'dtype': <type 'str'>, 'name': 'INSTR', 'shape': ()},
 {'dtype': <type 'numpy.float64'>, 'name': 'DEBUG.POS.TRANS', 'shape': (3,)},
 {'dtype': <type 'numpy.float64'>, 'name': 'DEBUG.POS.ROT', 'shape': (3,)},
 {'dtype': <type 'numpy.float64'>, 'name': 'DEBUG.PLAYER_ID', 'shape': (1,)},
# etc...

action_spec()

类似于observation_spec()action_spec()方法返回一个列表,其中包含空间中每个元素的最小值、最大值和名称。minmax值分别代表动作空间中相应元素可以设置的最小值和最大值。这个列表的长度等于动作空间的维度/形状。这类似于我们在 Gym 环境中使用的env.action_space

以下代码片段让我们快速了解调用此方法时返回值的样子:

import deepmind_lab
import pprint

env = deepmind_lab.Lab('tests/empty_room_test', [])
action_spec = env.action_spec()
pprint.pprint(action_spec)
# Outputs:
# [{'max': 512, 'min': -512, 'name': 'LOOK_LEFT_RIGHT_PIXELS_PER_FRAME'},
#  {'max': 512, 'min': -512, 'name': 'LOOK_DOWN_UP_PIXELS_PER_FRAME'},
#  {'max': 1, 'min': -1, 'name': 'STRAFE_LEFT_RIGHT'},
#  {'max': 1, 'min': -1, 'name': 'MOVE_BACK_FORWARD'},
#  {'max': 1, 'min': 0, 'name': 'FIRE'},
#  {'max': 1, 'min': 0, 'name': 'JUMP'},
#  {'max': 1, 'min': 0, 'name': 'CROUCH'}]

num_steps()

这个工具方法就像一个计数器,它计算自上次调用reset()以来环境执行的帧数。

fps()

这个工具方法返回每秒钟实际(墙钟)执行的帧数(或环境步数)。这个方法对于跟踪环境的执行速度以及代理从环境中采样的速度非常有用。

events()

这个工具方法可以在调试时非常有用,因为它返回自上次调用reset()step(...)以来发生的事件列表。返回的元组包含一个名称和一组观察数据。

close()

与 Gym 环境中的close()方法类似,这个方法也会关闭环境实例并释放底层资源,例如 Quake III Arena 实例。

设置和运行 DeepMind Lab 的快速入门指南

在前面简短讨论了 DeepMind Lab 环境接口之后,我们已经准备好亲自体验这个学习环境。在接下来的子章节中,我们将逐步介绍设置 DeepMind Lab 并运行一个示例代理的过程。

设置和安装 DeepMind Lab 及其依赖项

DeepMind Lab 库使用 Bazel 作为构建工具,而 Bazel 又依赖于 Java。书中的代码库有一个脚本,可以帮助你轻松设置 DeepMind Lab。你可以在章节 9 文件夹下找到这个脚本,路径是github.com/PacktPublishing/Hands-On-Intelligent-Agents-with-OpenAI-Gym/tree/master/ch9。你可以使用以下命令运行该脚本:

(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch9$./setup_deepmindlab.sh

这个脚本可能需要一些时间来完成,但它会自动安装所有必要的包和库,包括 Bazel 及其依赖项,并为你设置好一切。

玩游戏、测试随机行为代理或训练自己的代理!

安装完成后,你可以通过运行以下命令,使用键盘输入测试游戏:

(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch9$ cd deepmindlab

(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch9/deepmindlab$ bazel run :game -- --level_script=tests/empty_room_test

你还可以通过以下命令,在随机行为代理的帮助下进行测试:

(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch9/deepmindlab$ bazel run :python_random_agent --define graphics=sdl -- --length=5000

要开始自己的代理开发,你可以使用已经配置好的示例代理脚本,它可以与 DeepMind Lab 环境进行交互。你可以在~/HOIAWOG/ch9/deepmindlab/python/random_agent.py找到该脚本。要开始训练这个代理,你可以使用以下命令:

(rl_gym_book) praveen@ubuntu:~/HOIAWOG/ch9/deepmindlab$ bazel run :python_random_agent

概述

在本章中,我们探讨了几个有趣且有价值的学习环境,了解了它们的界面设置,甚至通过每个环境的快速入门指南和书中代码库中的设置脚本亲自操作了这些环境。我们首先查看了与我们现在非常熟悉的 OpenAI Gym 接口兼容的环境。具体来说,在这一类别中,我们探索了 Roboschool 和 Gym Retro 环境。

我们还查看了其他有用的学习环境,这些环境不一定具备与 Gym 兼容的环境接口,但它们拥有非常相似的 API,因此我们可以轻松地调整我们的智能体代码,或在学习环境周围实现一个包装器,使其兼容 Gym API。具体来说,我们探索了著名的实时战略游戏《星际争霸 II》环境和 DeepMind Lab 环境。我们还简要地提到了 DOTA2 环境,该环境曾被用于训练单一智能体和一个智能体团队,由 OpenAI 训练,这个团队成功击败了业余人类玩家,甚至一些职业电竞团队,在 DOTA 2 比赛中取得胜利。

我们观察了每个学习环境库中提供的不同任务和环境,并尝试了一些示例,以熟悉这些环境,同时了解如何使用我们在前几章中开发的智能体来训练并解决这些相对较新的学习环境中的挑战性任务。

第十章:探索学习算法领域 - DDPG(演员-评论家),PPO(策略梯度),Rainbow(基于值的方法)

在上一章中,我们讨论了几种有前景的学习环境,你可以用来训练智能体解决不同的任务。在第七章,创建自定义 OpenAI Gym 环境——CARLA 驾驶模拟器,我们还展示了如何创建自己的环境,以解决你可能感兴趣的任务或问题,借助智能和自主软件代理。这为你提供了完成后可以继续深入的方向,以探索和尝试本书中讨论的所有环境、任务和问题。按此思路,在本章中,我们将讨论几种有前景的学习算法,它们将成为你智能代理开发工作中的未来参考。

到目前为止,在本书中,我们已经详细介绍了实现智能代理的逐步过程,这些代理能够学习改进并解决离散决策/控制问题(第六章,使用深度 Q 学习实现最优离散控制的智能代理)和连续动作/控制问题(第八章,使用深度演员-评论家算法实现智能自主汽车驾驶代理)。这些内容为开发这样的学习代理提供了良好的起点。希望之前的章节为你展示了一个能够学习改进并应对手头任务或问题的自主智能软件代理/系统的整体框架。我们还审视了开发、训练和测试这些复杂系统时,有助于的整体管道及其有用的工具和常规(如日志记录、可视化、参数管理等)。我们看到两类主要算法:基于深度 Q 学习(及其扩展)和深度演员-评论家(及其扩展)的深度强化学习算法。它们是良好的基准算法,实际上,直到今天,它们仍然是该领域最新研究论文中的参考。这一研究领域近年来活跃发展,提出了若干新的算法。一些算法在样本复杂度上表现更好,即智能体在达到某一性能水平之前,从环境中收集的样本数。一些其他算法具有稳定的学习特性,并且在足够的时间内能够找到最优策略,适用于大多数问题且几乎不需要调参。还引入了几种新的架构,如 IMPALA 和 Ape-X,它们使得高可扩展的学习算法实现成为可能。

我们将快速了解这些有前景的算法、它们的优点以及潜在的应用类型。我们还将查看这些算法为我们已有的知识添加的关键组件的代码示例。这些算法的示例实现可在本书的代码库中找到,位于ch10文件夹,github.com/PacktPublishing/Hands-On-Intelligent-Agents-with-OpenAI-Gym

深度确定性策略梯度

深度确定性策略梯度DDPG)是一种离策略、无模型、演员-评论家算法,基于确定性策略梯度DPG)定理(proceedings.mlr.press/v32/silver1…)。与基于深度 Q 学习的方法不同,基于演员-评论家的策略梯度方法不仅适用于离散动作空间的问题/任务,也能轻松应用于连续动作空间。

核心概念

在第八章,使用深度演员-评论家算法实现智能自动驾驶汽车代理,我们带你走过了策略梯度定理的推导,并为引入上下文,复现了以下内容:

你可能还记得,我们考虑的策略是一个随机函数,给定状态s)和参数()为每个动作分配一个概率。在确定性策略梯度中,随机策略被一个确定性策略所替代,该策略为给定的状态和参数集指定了一个固定策略!。简而言之,DPG 可以通过以下两个方程表示:

这是策略目标函数:

这里,是由参数化的确定性策略,r(s,a)是执行动作a在状态 s 下的奖励函数,而是该策略下的折扣状态分布。

确定性策略目标函数的梯度已在之前链接的论文中证明为:

我们现在看到熟悉的动作-价值函数项,通常称为评论员。DDPG 基于这个结果,并使用深度神经网络表示动作-价值函数,就像我们在第六章中做的那样,使用深度 Q 学习实现智能最优离散控制代理,以及其他一些修改来稳定训练。具体而言,使用了 Q 目标网络(就像我们在第六章中讨论的那样,使用深度 Q 学习实现智能最优离散控制代理),但是现在这个目标网络是缓慢更新的,而不是在几个更新步骤后保持固定并更新它。DDPG 还使用经验重放缓冲区,并使用噪声版本的 ,用方程式 表示,以鼓励探索作为策略。 是确定性的。

DDPG 有一个扩展版本叫做 D4PG,简称为分布式分布式 DDPG。我猜你可能在想:DPG -> DDPG -> {缺失?} -> DDDDPG。没错!缺失的部分就是你需要实现的内容。

D4PG 算法对 DDPG 算法做了四个主要改进,如果你有兴趣,可以简要列出如下:

  • 分布式评论员(评论员现在估计 Q 值的分布,而不是给定状态和动作的单一 Q 值)

  • N 步回报(类似于我们在第八章中使用的实现智能自动驾驶代理,使用深度演员-评论员算法,使用了 n 步 TD 回报,而不是通常的 1 步回报)

  • 优先经验重放(用于从经验重放记忆中抽样经验)

  • 分布式并行演员(利用 K 个独立的演员并行收集经验并填充经验重放记忆)

邻近策略优化(Proximal Policy Optimization)

邻近策略优化(PPO) 是一种基于策略梯度的方法,是已经被证明既稳定又具有可扩展性的算法之一。事实上,PPO 是 OpenAI Five 团队使用的算法,该团队的代理与多位人类 DOTA II 玩家对战并获胜,这一点我们在前一章中讨论过。

核心概念

在策略梯度方法中,算法通过执行回合(rollouts)来收集状态转移和(可能的)奖励样本,并使用梯度下降更新策略的参数,以最小化目标函数。其思想是不断更新参数以改进策略,直到获得一个较好的策略。为了提高训练的稳定性,信任域策略优化TRPO)算法对策略更新施加了Kullback-LieblerKL)散度约束,确保策略在与旧策略的比较中不会在一步中更新得过多。TRPO 是 PPO 算法的前身。让我们简要讨论一下 TRPO 算法中使用的目标函数,以便更好地理解 PPO。

脱策略学习

正如我们所知,在脱策略学习的情况下,代理遵循一个与其试图优化的策略不同的行为策略。提醒一下,我们在第六章中讨论过的 Q-learning,使用深度 Q-learning 实现智能代理进行最优离散控制,以及一些扩展,都是脱策略算法。我们用 来表示行为策略。那么,我们可以将代理的目标函数写为状态访问分布和动作的总优势,如下所示:

这里, 是更新前的策略参数, 是旧策略参数下的状态访问概率分布。我们可以在内层求和式中对项进行乘除操作,使用行为策略 ,其目的是使用重要性采样来考虑到状态转移是通过行为策略 进行采样的:

与前一方程相比,前述方程中变化的项用红色表示。

我们可以将之前的求和表示为一个期望,如下所示:

在策略学习中

在策略学习的情况下,行为策略和目标策略是相同的。因此,自然地,当前策略(更新前)是代理用来收集样本的策略,表示为 ,这就是行为策略,因此目标函数变为:

与前一方程相比,前述方程中变化的项用红色表示。

TRPO 通过信任域约束优化前述目标函数,使用以下方程给出的 KL 散度度量:

这是确保新策略更新不会与当前策略相差过大的约束条件。尽管 TRPO 背后的理念简洁且直观,但其实现和梯度更新涉及复杂性。PPO 通过使用一个裁剪的替代目标简化了这一方法,且既有效又简单。让我们通过算法背后的数学原理深入理解 PPO 的核心概念。假设在给定状态 s 下采取动作 a 的新策略与旧策略的概率比定义如下:

将这个代入我们之前讨论的 TRPO 的在线策略目标函数方程中,得到的目标函数如下:

简单地移除 KL 散度约束将导致不稳定,因为可能会出现大量的参数更新。PPO 通过强制! 落在区间!内,施加了这个约束,其中!是一个可调超参数。实际上,PPO 中使用的目标函数取原始参数值和裁剪版本中的最小值,其数学描述如下所示:

这导致了一个稳定的学习目标,并且策略单调地改善。

Rainbow

Rainbow (arxiv.org/pdf/1710.02298.pdf) 是基于 DQN 的一种离策略深度强化学习算法。我们在第六章,使用深度 Q 学习实现最优离散控制的智能代理中研究并实现了深度 Q 学习(DQN)以及一些 DQN 的扩展。DQN 算法已经有了几个扩展和改进。Rainbow 结合了其中六个扩展,显示出它们的组合效果更佳。Rainbow 是一个最先进的算法,目前在所有 Atari 游戏中保持着最高分记录。如果你想知道为什么这个算法叫做 Rainbow,很可能是因为它结合了七个(即彩虹的颜色数)Q 学习算法的扩展,具体包括:

  • DQN

  • 双重 Q 学习

  • 优先经验回放

  • 对抗网络

  • 多步学习/n 步学习

  • 分布式 RL

  • 噪声网络

核心概念

Rainbow 结合了 DQN 和六个已被证明能解决原始 DQN 算法局限性的扩展。我们将简要回顾这六个扩展,了解它们如何为整体性能提升做出贡献,并使 Rainbow 在 Atari 基准测试中占据了榜首,同时也证明了它们在 OpenAI Retro 大赛中的成功。

DQN

到现在为止,你应该已经非常熟悉 DQN,因为我们在第六章,使用深度 Q-Learning 实现最优离散控制的智能体中逐步实现了深度 Q-learning 智能体,在那里我们详细讨论了 DQN 以及它如何通过深度神经网络函数逼近、回放记忆和目标网络扩展标准 Q-learning。让我们回顾一下我们在第六章,使用深度 Q-Learning 实现最优离散控制的智能体中使用的 Q-learning 损失:

这基本上是 TD 目标和 DQN 的 Q 估计之间的均方误差,正如我们在第六章,使用深度 Q-Learning 实现最优离散控制的智能体中所提到的,其中 是缓慢变化的目标网络, 是主 Q 网络。

双重 Q-Learning

在双重 Q-Learning 中,有两个动作值/Q 函数。我们将它们称为 Q1 和 Q2。双重 Q-Learning 的理念是将动作选择与价值估计解耦。也就是说,当我们想更新 Q1 时,我们根据 Q1 选择最佳动作,但使用 Q2 来找到选定动作的价值。类似地,当 Q2 被更新时,我们基于 Q2 选择动作,但使用 Q1 来确定选定动作的价值。实际上,我们可以使用主 Q 网络 作为 Q1,使用缓慢变化的目标网络 作为 Q2,从而得到以下双重 Q-Learning 损失方程(与 DQN 方程的差异以红色显示):

更改损失函数的动机是,Q-learning 会受到过度估计偏差的影响,这可能会损害学习。过度估计的原因是最大值的期望大于或等于期望的最大值(通常是不等式成立),这源于 Q-learning 算法和 DQN 中的最大化步骤。双重 Q-Learning 所引入的变化已被证明可以减少有害的过度估计,从而改善性能,相较于 DQN 更具优势。

优先经验回放

在我们实现深度 Q 学习的第六章,使用深度 Q 学习实现智能代理进行最优离散控制中,我们使用经验回放内存来存储和检索采样的过渡经验。在我们的实现中,以及在 DQN 算法中,来自回放内存缓冲区的经验是均匀采样的。直观地说,我们希望更频繁地采样这些经验,因为有很多内容需要学习。优先经验回放根据以下方程,以相对最后遇到的绝对 TD 误差的概率 进行过渡采样:

在这里, 是一个超参数,决定分布的形状。这确保我们采样那些 Q 值预测与正确值相差较大的过渡。实际上,新的过渡会以最高优先级插入回放内存,以表示最近过渡经验的重要性。

对战网络

对战网络(Dueling networks)是一种为基于值的强化学习设计的神经网络架构。名称*对战(dueling)*来源于该架构的主要特点,即它有两条计算流,一条用于值函数,另一条用于优势函数。下面的图示来自一篇研究论文(arxiv.org/pdf/1511.06581.pdf),展示了对战网络架构(图中底部的网络)与典型 DQN 架构(图中顶部的网络)之间的对比:

编码特征的卷积层是由值流和优势流共享的,并通过一个特殊的聚合函数进行合并,正如在论文中所讨论的,它对应于以下的动作值分解:

,和  分别是值流、共享卷积编码器和优势流的参数,  是它们的连接。

多步学习/n 步学习

在第八章,使用深度演员-评论家算法实现智能自主汽车驾驶代理中,我们实现了 n 步返回 TD 方法,并讨论了如何使用前视多步目标替代单步 TD 目标。我们可以将这个 n 步返回与 DQN 一起使用,这本质上是该扩展的思想。回想一下,从状态 的截断 n 步返回给出如下:

使用这个方程,可以定义 DQN 的多步变体,以最小化以下损失(与 DQN 方程的差异以红色显示):

这个方程展示了 DQN 中引入的变化。

分布式强化学习

分布式强化学习方法(arxiv.org/abs/1707.06887)是学习如何近似回报的分布,而不是期望(平均)回报。分布式强化学习方法提出使用放置在离散支持集上的概率质量来建模这些分布。本质上,这意味着与其试图建模给定状态下的单一动作值,不如为每个动作给定状态下寻找一个动作值的分布。虽然不深入细节(因为那需要大量背景信息),我们将看看此方法对强化学习的一个关键贡献,即分布式贝尔曼方程的公式化。如你从本书前几章回忆的那样,使用一步贝尔曼备份的动作值函数可以通过如下方式返回:

在分布式贝尔曼方程的情况下,标量量  被随机变量  所替代,从而得到如下方程:

由于该量不再是标量,因此更新方程需要比单纯将下一状态-动作值的折扣值加到步进回报中更加小心地处理。分布式贝尔曼方程的更新步骤可以通过以下图示轻松理解(从左到右的各个阶段):

在前面的插图中,下一状态的动作值分布用红色显示在左侧,接着通过折扣因子  (中间)进行缩放,最终通过  平移该分布,从而得到分布贝尔曼更新。在更新之后,由前一更新操作得到的目标分布  通过最小化  和  之间的交叉熵损失,投影到当前分布的支持集  上。

在此背景下,你可以简要浏览分布式强化学习论文中的 C51 算法伪代码,它已经集成到 Rainbow 智能体中:

噪声网络

如果你还记得,我们在第六章《使用深度 Q 学习实现智能代理进行最优离散控制》中使用了-贪心策略,基于深度 Q 网络学习到的动作值来选择动作,这基本上意味着大多数时候都会选择给定状态下的最高动作值对应的动作,除了在一小部分时间(即非常小的概率)内,代理会选择一个随机动作。这可能会阻止代理探索更多的奖励状态,特别是当它已经收敛的动作值不是最优的动作值时。使用-贪心策略进行探索的局限性,在 Atari 游戏《Montezuma's Revenge》的表现中显而易见,在该游戏中,必须以正确的方式执行一系列动作才能获得第一个奖励。为了克服这一探索限制,Rainbow 使用了喧嚣网络的思想——这是一种 2017 年提出的简单但有效的方法。

喧嚣网络的主要思想是线性神经网络层的一个喧嚣版本,它结合了确定性流和喧嚣流,如以下方程所示,针对线性神经网络层的情况:

在这里, 是喧嚣层的参数,它们与 DQN 的其他参数一起通过梯度下降进行学习。 代表元素级的乘积操作, 是均值为零的随机噪声。我们可以在 DQN 实现中用喧嚣线性层代替普通的线性层,这将具有更好的探索优势。由于 是可学习的参数,网络可以学会忽略喧嚣流。随着时间的推移,对于每个神经元来说,喧嚣流会在状态空间的不同部分以不同的速率衰减,从而通过一种自我退火的形式实现更好的探索。

Rainbow 代理的实现将所有这些方法结合起来,达到了最先进的成果,并且在 57 款 Atari 游戏的测试中,比其他任何方法的表现都要优秀。总体来说,Rainbow 代理与以前最佳表现的算法在 Atari 游戏综合基准测试中的表现,如下图所示:

从图中可以清楚地看出,Rainbow 代理所包含的方法在 57 款不同的 Atari 游戏中带来了显著的性能提升。

优势和应用的快速总结

Rainbow 代理的一些关键优势总结如下,供您快速参考:

  • 融合了过去几年中 Q-learning 的多个显著扩展

  • 在 Atari 基准测试中取得了最先进的结果。

  • 使用适当调整的 n 值的 n 步目标通常能加速学习。

  • 与其他 DQN 变体不同,Rainbow 智能体可以在经验回放内存中收集的帧数减少 40% 的情况下开始学习。

  • 在不到 10 小时(700 万帧)内,在单 GPU 机器上匹配了 DQN 的最佳表现。

Rainbow 算法已成为离散控制问题中最受追捧的智能体,尤其适用于动作空间小且离散的场景。它在其他游戏环境中也非常成功,例如 Gym-Retro,特别是经过调优的 Rainbow 智能体在 2018 年 OpenAI Retro 比赛中获得了第二名。该比赛是一个迁移学习竞赛,任务是学习在某些关卡中玩复古的 Genesis 游戏《刺猬索尼克》、《刺猬索尼克 II》和《刺猬索尼克与纳克尔斯》,然后能够在未经过训练的其他关卡中表现良好。考虑到在典型的强化学习环境中,智能体通常在相同环境中训练和测试,Retro 比赛衡量了学习算法从先前经验中泛化学习的能力。总的来说,Rainbow 智能体是任何具有离散动作空间的强化学习问题/任务的最佳初步选择。

总结

作为本书的最后一章,本章总结了当前在该领域内最先进的关键学习算法。我们了解了三种不同最先进算法的核心概念,每种算法都有其独特的元素和分类(演员-评论家/基于策略/基于价值函数)。

具体来说,我们讨论了深度确定性策略梯度算法,这是一种演员-评论家架构方法,采用确定性策略,而非通常的随机策略,并在多个连续控制任务中取得了良好的表现。

接下来我们研究了 PPO 算法,这是一种基于策略梯度的方法,采用了 TRPO 目标的剪辑版本,并学习到一个单调提升且稳定的策略,在像 DOTA II 这样的高维环境中取得了成功。

最后,我们看到了 Rainbow 算法,这是一种基于价值的方法,结合了多个扩展到非常流行的 Q-learning 算法的技术,即 DQN、双重 Q-learning、优先经验回放、对抗网络、多步学习/n 步学习、分布式强化学习和噪声网络层。Rainbow 智能体在 57 款 Atari 基准游戏中取得了显著更好的表现,并且在 OpenAI Retro 比赛中的迁移学习任务上表现也非常出色。

现在,我们进入了这本书的最后一段!希望您在阅读过程中享受了旅程,学到了很多,并获得了实现智能代理算法以及在您选择的学习环境/问题上训练和测试代理所需的许多实际技能的机会。您可以使用书籍代码库中的问题追踪系统报告代码问题,或者如果您想进一步讨论某个主题,或者需要任何额外的参考资料/指引来进入下一个阶段。