tf2-rl-cb-merge-3

50 阅读32分钟

TensorFlow2 强化学习秘籍(四)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

第七章:第七章:将深度 RL 代理部署到云端

云计算已成为基于 AI 的产品和解决方案的事实上的部署平台。深度学习模型在云端运行变得越来越普遍。然而,由于各种原因,将基于强化学习的代理部署到云端仍然非常有限。本章包含了帮助你掌握工具和细节的配方,让你走在前沿,构建基于深度 RL 的云端 Simulation-as-a-Service 和 Agent/Bot-as-a-Service 应用。

本章具体讨论了以下配方:

  • 实现 RL 代理的运行时组件

  • 构建作为服务的 RL 环境模拟器

  • 使用远程模拟器服务训练 RL 代理

  • 测试/评估 RL 代理

  • 打包 RL 代理以进行部署 – 一个交易机器人

  • 将 RL 代理部署到云端 – 一个交易机器人即服务

技术要求

本书中的代码在 Ubuntu 18.04 和 Ubuntu 20.04 上经过广泛测试,如果安装了 Python 3.6+,该代码应该能在更新版本的 Ubuntu 上运行。如果安装了 Python 3.6+ 和前面列出的必要 Python 包,代码也应该能在 Windows 和 macOS X 上运行。建议创建并使用名为 tf2rl-cookbook 的 Python 虚拟环境来安装包并运行本书中的代码。建议使用 Miniconda 或 Anaconda 进行 Python 虚拟环境管理。

每章每个配方的完整代码可以在这里找到:github.com/PacktPublishing/Tensorflow-2-Reinforcement-Learning-Cookbook

实现 RL 代理的运行时组件

在前几章中,我们已经讨论了几个代理算法的实现。你可能已经注意到,在之前的章节(特别是 第三章实现高级深度 RL 算法)中的一些配方里,我们实现了 RL 代理的训练代码,其中有些部分的代理代码是有条件执行的。例如,经验回放的例程只有在满足某些条件(比如回放记忆中的样本数量)时才会运行,等等。这引出了一个问题:在一个代理中,哪些组件是必需的,特别是当我们不打算继续训练它,而只是执行一个已经学到的策略时?

本配方将帮助你将 Soft Actor-Critic (SAC) 代理的实现提炼到一组最小的组件——这些是你的代理运行时绝对必要的组件。

让我们开始吧!

做好准备

为了完成这个食谱,首先需要激活tf2rl-cookbook的 Python/conda 虚拟环境。确保更新环境以匹配食谱代码库中的最新 conda 环境规格文件(tfrl-cookbook.yml)。WebGym 建立在 miniWob-plusplus 基准之上(github.com/stanfordnlp/miniwob-plusplus),该基准也作为本书代码库的一部分提供,便于使用。如果以下的import语句没有问题,你就可以开始了:

import functools
from collections import deque
import numpy as np
import tensorflow as tf
import tensorflow_probability as tfp
from tensorflow.keras.layers import Concatenate, Dense, Input
from tensorflow.keras.models import Model
from tensorflow.keras.optimizers import Adam
tf.keras.backend.set_floatx(“float64”)

现在,让我们开始吧!

如何做到这一点…

以下步骤提供了实现 SAC 智能体所需的最小运行时的详细信息。让我们直接进入细节:

  1. 首先,让我们实现演员组件,它将是一个 TensorFlow 2.x 模型:

    def actor(state_shape, action_shape, units=(512, 256, 64)):
        state_shape_flattened = \
            functools.reduce(lambda x, y: x * y, state_shape)
        state = Input(shape=state_shape_flattened)
        x = Dense(units[0], name=”L0”, activation=”relu”)\
                 (state)
        for index in range(1, len(units)):
            x = Dense(units[index], name=”L{}”.format(index),
                      activation=”relu”)(x)
        actions_mean = Dense(action_shape[0], \
                             name=”Out_mean”)(x)
        actions_std = Dense(action_shape[0], 
                             name=”Out_std”)(x)
        model = Model(inputs=state, outputs=[actions_mean,
                      actions_std])
        return model
    
  2. 接下来,让我们实现评论家组件,它也将是一个 TensorFlow 2.x 模型:

    def critic(state_shape, action_shape, units=(512, 256, 64)):
        state_shape_flattened = \
            functools.reduce(lambda x, y: x * y, state_shape)
        inputs = [Input(shape=state_shape_flattened), \
                  Input(shape=action_shape)]
        concat = Concatenate(axis=-1)(inputs)
        x = Dense(units[0], name=”Hidden0”, \
                  activation=”relu”)(concat)
        for index in range(1, len(units)):
            x = Dense(units[index], \
                      name=”Hidden{}”.format(index), \
                      activation=”relu”)(x)
        output = Dense(1, name=”Out_QVal”)(x)
        model = Model(inputs=inputs, outputs=output)
        return model
    
  3. 现在,让我们实现一个实用函数,用于在给定源 TensorFlow 2.x 模型的情况下更新目标模型的权重:

    def update_target_weights(model, target_model, tau=0.005):
        weights = model.get_weights()
        target_weights = target_model.get_weights()
        for i in range(len(target_weights)):  
        # set tau% of target model to be new weights
            target_weights[i] = weights[i] * tau + \
                                target_weights[i] * (1 - tau)
        target_model.set_weights(target_weights)
    
  4. 现在,我们可以开始实现 SAC 智能体的运行时类。我们将把实现分为以下几个步骤。让我们从类的实现开始,并在这一步中定义构造函数的参数:

    class SAC(object):
        def __init__(
            self,
            observation_shape,
            action_space,
            lr_actor=3e-5,
            lr_critic=3e-4,
            actor_units=(64, 64),
            critic_units=(64, 64),
            auto_alpha=True,
            alpha=0.2,
            tau=0.005,
            gamma=0.99,
            batch_size=128,
            memory_cap=100000,
        ):
    
  5. 现在,让我们初始化智能体的状态/观测形状、动作形状、动作限制/边界,并初始化一个双端队列(deque)来存储智能体的记忆:

            self.state_shape = observation_shape  # shape of 
            # observations
            self.action_shape = action_space.shape  # number 
            # of actions
            self.action_bound = \
                (action_space.high - action_space.low) / 2
            self.action_shift = \
                (action_space.high + action_space.low) / 2
            self.memory = deque(maxlen=int(memory_cap))
    
  6. 在这一步中,让我们定义并初始化演员组件:

            # Define and initialize actor network
            self.actor = actor(self.state_shape, 
                               self.action_shape, 
                               actor_units)
            self.actor_optimizer = \
                Adam(learning_rate=lr_actor)
            self.log_std_min = -20
            self.log_std_max = 2
            print(self.actor.summary())
    
  7. 现在,让我们定义并初始化评论家组件:

            # Define and initialize critic networks
            self.critic_1 = critic(self.state_shape, 
                                   self.action_shape, 
                                   critic_units)
            self.critic_target_1 = critic(self.state_shape,
                                          self.action_shape,
                                          critic_units)
            self.critic_optimizer_1 = \
                Adam(learning_rate=lr_critic)
            update_target_weights(self.critic_1, 
                                  self.critic_target_1, 
                                  tau=1.0)
            self.critic_2 = critic(self.state_shape, 
                                   self.action_shape, 
                                   critic_units)
            self.critic_target_2 = critic(self.state_shape,
                                          self.action_shape,
                                          critic_units)
            self.critic_optimizer_2 = \
                Adam(learning_rate=lr_critic)
            update_target_weights(self.critic_2, 
                                  self.critic_target_2, 
                                  tau=1.0)
            print(self.critic_1.summary())
    
  8. 在这一步中,让我们根据auto_alpha标志来初始化 SAC 智能体的温度和目标熵:

            # Define and initialize temperature alpha and 
            # target entropy
            self.auto_alpha = auto_alpha
            if auto_alpha:
                self.target_entropy = \
                    -np.prod(self.action_shape)
                self.log_alpha = tf.Variable(0.0, 
                                            dtype=tf.float64)
                self.alpha = tf.Variable(0.0, 
                                         dtype=tf.float64)
                self.alpha.assign(tf.exp(self.log_alpha))
                self.alpha_optimizer = \
                    Adam(learning_rate=lr_actor)
            else:
                self.alpha = tf.Variable(alpha, 
                                         dtype=tf.float64)
    
  9. 让我们通过设置超参数并初始化用于 TensorBoard 日志记录的训练进度摘要字典来完成构造函数的实现:

            # Set hyperparameters
            self.gamma = gamma  # discount factor
            self.tau = tau  # target model update
            self.batch_size = batch_size
            # Tensorboard
            self.summaries = {}
    
  10. 构造函数实现完成后,我们接下来实现process_action函数,该函数接收智能体的原始动作并处理它,使其可以被执行:

        def process_actions(self, mean, log_std, test=False, 
        eps=1e-6):
            std = tf.math.exp(log_std)
            raw_actions = mean
            if not test:
                raw_actions += tf.random.normal(shape=mean.\
                               shape, dtype=tf.float64) * std
            log_prob_u = tfp.distributions.Normal(loc=mean,
                             scale=std).log_prob(raw_actions)
            actions = tf.math.tanh(raw_actions)
            log_prob = tf.reduce_sum(log_prob_u - \
                         tf.math.log(1 - actions ** 2 + eps))
            actions = actions * self.action_bound + \
                      self.action_shift
            return actions, log_prob
    
  11. 这一步非常关键。我们将实现act方法,该方法将以状态作为输入,生成并返回要执行的动作:

        def act(self, state, test=False, use_random=False):
            state = state.reshape(-1)  # Flatten state
            state = np.expand_dims(state, axis=0).\
                                         astype(np.float64)
            if use_random:
                a = tf.random.uniform(
                    shape=(1, self.action_shape[0]), 
                           minval=-1, maxval=1, 
                           dtype=tf.float64
                )
            else:
                means, log_stds = self.actor.predict(state)
                log_stds = tf.clip_by_value(log_stds, 
                                            self.log_std_min,
                                            self.log_std_max)
                a, log_prob = self.process_actions(means,
                                                   log_stds, 
                                                   test=test)
            q1 = self.critic_1.predict([state, a])[0][0]
            q2 = self.critic_2.predict([state, a])[0][0]
            self.summaries[“q_min”] = tf.math.minimum(q1, q2)
            self.summaries[“q_mean”] = np.mean([q1, q2])
            return a
    
  12. 最后,让我们实现一些实用方法,用于从先前训练的模型中加载演员和评论家的模型权重:

        def load_actor(self, a_fn):
            self.actor.load_weights(a_fn)
            print(self.actor.summary())
        def load_critic(self, c_fn):
            self.critic_1.load_weights(c_fn)
            self.critic_target_1.load_weights(c_fn)
            self.critic_2.load_weights(c_fn)
            self.critic_target_2.load_weights(c_fn)
            print(self.critic_1.summary())
    

到此为止,我们已经完成了所有必要的 SAC RL 智能体运行时组件的实现!

它是如何工作的…

在这个食谱中,我们实现了 SAC 智能体的基本运行时组件。运行时组件包括演员和评论家模型定义、一个从先前训练的智能体模型中加载权重的机制,以及一个智能体接口,用于根据状态生成动作,利用演员的预测并处理预测生成可执行动作。

对于其他基于演员-评论家的 RL 智能体算法,如 A2C、A3C 和 DDPG 及其扩展和变体,运行时组件将非常相似,甚至可能是相同的。

现在是时候进入下一个教程了!

将 RL 环境模拟器构建为服务

本教程将引导你将你的 RL 训练环境/模拟器转换为一个服务。这将使你能够提供模拟即服务(Simulation-as-a-Service)来训练 RL 代理!

到目前为止,我们已经在多种环境中使用不同的模拟器训练了多个 RL 代理,具体取决于要解决的任务。训练脚本使用 OpenAI Gym 接口与在同一进程中运行的环境或在不同进程中本地运行的环境进行通信。本教程将引导你完成将任何 OpenAI Gym 兼容的训练环境(包括你自定义的 RL 训练环境)转换为可以本地或远程部署为服务的过程。构建并部署完成后,代理训练客户端可以连接到模拟服务器,并远程训练一个或多个代理。

作为一个具体的例子,我们将使用我们的 tradegym 库,它是我们在前几章中为加密货币和股票交易构建的 RL 训练环境的集合,并通过 RESTful HTTP 接口 将它们暴露出来,以便训练 RL 代理。

开始吧!

准备工作

要完成本教程,你需要首先激活 tf2rl-cookbook Python/conda 虚拟环境。确保更新该环境,使其与最新的 conda 环境规范文件(tfrl-cookbook.yml)保持一致,该文件在食谱代码仓库中。

我们还需要创建一个新的 Python 模块,名为 tradegym,其中包含 crypto_trading_env.pystock_trading_continuous_env.pytrading_utils.py 以及我们在前几章中实现的其他自定义交易环境。你将在书籍的代码仓库中找到包含这些内容的 tradegym 模块。

如何操作…

我们的实现将包含两个核心模块——tradegym 服务器和 tradegym 客户端,这些模块是基于 OpenAI Gym HTTP API 构建的。本教程将重点介绍 HTTP 服务接口的定制和核心组件。我们将首先定义作为 tradegym 库一部分暴露的最小自定义环境集,然后构建服务器和客户端模块:

  1. 首先,确保 tradegym 库的 __init__.py 文件中包含最基本的内容,以便我们可以导入这些环境:

    import sys
    import os
    from gym.envs.registration import register
    sys.path.append(os.path.dirname(os.path.abspath(__file__)))
    _AVAILABLE_ENVS = {
        “CryptoTradingEnv-v0”: {
            “entry_point”: \
              “tradegym.crypto_trading_env:CryptoTradingEnv”,
            “description”: “Crypto Trading RL environment”,
        },
        “StockTradingContinuousEnv-v0”: {
            “entry_point”: “tradegym.stock_trading_\
                 continuous_env:StockTradingContinuousEnv”,
            “description”: “Stock Trading RL environment with continous action space”,
        },
    }
    for env_id, val in _AVAILABLE_ENVS.items():
        register(id=env_id, entry_point=val.get(
                                         “entry_point”))
    
  2. 我们现在可以开始实现我们的 tradegym 服务器,命名为 tradegym_http_server.py。我们将在接下来的几个步骤中完成实现。让我们首先导入必要的 Python 模块:

    import argparse
    import json
    import logging
    import os
    import sys
    import uuid
    import numpy as np
    import six
    from flask import Flask, jsonify, request
    import gym
    
  3. 接下来,我们将导入 tradegym 模块,以便将可用的环境注册到 Gym 注册表中:

    sys.path.append(os.path.dirname(os.path.abspath(__file__)))
    import tradegym  # Register tradegym envs with OpenAI Gym 
    # registry
    
  4. 现在,让我们看看环境容器类的框架,并且有注释说明每个方法的作用。你可以参考本书代码仓库中chapter7下的完整实现。我们将从类定义开始,并在接下来的步骤中完成框架:

    class Envs(object):
        def __init__(self):
            self.envs = {}
            self.id_len = 8  # Number of chars in instance_id
    
  5. 在此步骤中,我们将查看两个有助于管理环境实例的辅助方法。它们支持查找和删除操作:

        def _lookup_env(self, instance_id):
            """Lookup environment based on instance_id and 
               throw error if not found"""
        def _remove_env(self, instance_id):
            """Delete environment associated with 
               instance_id"""
    
  6. 接下来,我们将查看其他一些有助于环境管理操作的方法:

        def create(self, env_id, seed=None):
            """Create (make) an instance of the environment 
               with `env_id` and return the instance_id"""
        def list_all(self):
            """Return a dictionary of all the active 
               environments with instance_id as keys"""
        def reset(self, instance_id):
            """Reset the environment pointed to by the 
               instance_id"""
    
        def env_close(self, instance_id):
            """Call .close() on the environment and remove 
               instance_id from the list of all envs"""
    
  7. 本步骤中讨论的方法支持 RL 环境的核心操作,并且这些方法与核心 Gym API 一一对应:

        def step(self, instance_id, action, render):
            """Perform a single step in the environment 
               pointed to by the instance_id and return 
               observation, reward, done and info"""
        def get_action_space_contains(self, instance_id, x):
            """Check if the given environment’s action space 
               contains x"""
        def get_action_space_info(self, instance_id):
            """Return the observation space infor for the 
               given environment instance_id"""
        def get_action_space_sample(self, instance_id):
            """Return a sample action for the environment 
               referred by the instance_id"""
        def get_observation_space_contains(self, instance_id, 
        j):
            """Return true is the environment’s observation 
               space contains `j`. False otherwise"""
        def get_observation_space_info(self, instance_id):
            """Return the observation space for the 
               environment referred by the instance_id"""
        def _get_space_properties(self, space):
            """Return a dictionary containing the attributes 
               and values of the given Gym Spce (Discrete, 
               Box etc.)"""
    
  8. 有了前面的框架(和实现),我们可以通过Flask Python 库将这些操作暴露为 REST API。接下来,我们将讨论核心服务器应用程序的设置以及创建、重置和步骤方法的路由设置。让我们看看暴露端点处理程序的服务器应用程序设置:

    app = Flask(__name__)
    envs = Envs()
    
  9. 现在我们可以查看v1/envs的 REST API 路由定义。它接受一个env_id,该 ID 应该是一个有效的 Gym 环境 ID(如我们的自定义StockTradingContinuous-v0MountainCar-v0,这些都可以在 Gym 注册表中找到),并返回一个instance_id

    @app.route(“/v1/envs/”, methods=[“POST”])
    def env_create():
        env_id = get_required_param(request.get_json(), 
                                    “env_id”)
        seed = get_optional_param(request.get_json(), 
                                   “seed”, None)
        instance_id = envs.create(env_id, seed)
        return jsonify(instance_id=instance_id)
    
  10. 接下来,我们将查看v1/envs/<instance_id>/reset的 HTTP POST 端点的 REST API 路由定义,其中<instance_id>可以是env_create()方法返回的任何 ID:

    @app.route(“/v1/envs/<instance_id>/reset/”, 
               methods=[“POST”])
    def env_reset(instance_id):
        observation = envs.reset(instance_id)
        if np.isscalar(observation):
            observation = observation.item()
        return jsonify(observation=observation)
    
  11. 接下来,我们将查看v1/envs/<instance_id>/step端点的路由定义,这是在 RL 训练循环中最可能被调用的端点:

    @app.route(“/v1/envs/<instance_id>/step/”,
               methods=[“POST”])
    def env_step(instance_id):
        json = request.get_json()
        action = get_required_param(json, “action”)
        render = get_optional_param(json, “render”, False)
        [obs_jsonable, reward, done, info] = envs.step(instance_id, action, render)
        return jsonify(observation=obs_jsonable, 
                       reward=reward, done=done, info=info)
    
  12. 对于tradegym服务器上剩余的路由定义,请参考本书的代码仓库。我们将在tradegym服务器脚本中实现一个__main__函数,用于在执行时启动服务器(稍后我们将在本教程中使用它来进行测试):

    if __name__ == “__main__”:
        parser = argparse.ArgumentParser(description=”Start a
                                        Gym HTTP API server”)
        parser.add_argument(“-l”,“--listen”, help=”interface\
                            to listen to”, default=”0.0.0.0”)
        parser.add_argument(“-p”, “--port”, default=6666, \
                            type=int, help=”port to bind to”)
        args = parser.parse_args()
        print(“Server starting at: “ + \
               “http://{}:{}”.format(args.listen, args.port))
        app.run(host=args.listen, port=args.port, debug=True)
    
  13. 接下来,我们将了解tradegym客户端的实现。完整实现可在本书代码仓库的chapter7中找到tradegym_http_client.py文件中。在本步骤中,我们将首先导入必要的 Python 模块,并在接下来的步骤中继续实现客户端封装器:

    import json
    import logging
    import os
    import requests
    import six.moves.urllib.parse as urlparse
    
  14. 客户端类提供了一个 Python 封装器,用于与tradegym HTTP 服务器进行接口交互。客户端类的构造函数接受服务器的地址(IP 和端口信息)以建立连接。让我们看看构造函数的实现:

    class Client(object):
        def __init__(self, remote_base):
            self.remote_base = remote_base
            self.session = requests.Session()
            self.session.headers.update({“Content-type”: \
                                         “application/json”})
    
  15. 在这里重复所有标准的 Gym HTTP 客户端方法并不是对本书有限空间的合理利用,因此我们将重点关注核心的封装方法,如env_createenv_resetenv_step,这些方法将在我们的代理训练脚本中广泛使用。有关完整实现,请参阅本书的代码库。让我们看一下用于在远程tradegym服务器上创建 RL 仿真环境实例的env_create封装方法:

        def env_create(self, env_id):
            route = “/v1/envs/”
            data = {“env_id”: env_id}
            resp = self._post_request(route, data)
            instance_id = resp[“instance_id”]
            return instance_id
    
  16. 在这一步中,我们将查看调用reset方法的封装方法,它通过tradegym服务器在env_create调用时返回的唯一instance_id来操作特定的环境:

        def env_reset(self, instance_id):
            route = “/v1/envs/{}/reset/”.format(instance_id)
            resp = self._post_request(route, None)
            observation = resp[“observation”]
            return observation
    
  17. tradegym客户端的Client类中最常用的方法是step方法。让我们看一下它的实现,应该对你来说很简单:

        def env_step(self, instance_id, action, 
        render=False):
            route = “/v1/envs/{}/step/”.format(instance_id)
            data = {“action”: action, “render”: render}
            resp = self._post_request(route, data)
            observation = resp[“observation”]
            reward = resp[“reward”]
            done = resp[“done”]
            info = resp[“info”]
            return [observation, reward, done, info]
    
  18. 在其他客户端封装方法到位之后,我们可以实现__main__例程来连接到tradegym服务器,并调用一些方法作为示例,以测试一切是否按预期工作。让我们编写__main__例程:

    if __name__ == “__main__”:
        remote_base = “http://127.0.0.1:6666”
        client = Client(remote_base)
        # Create environment
        env_id = “StockTradingContinuousEnv-v0”
        # env_id = “CartPole-v0”
        instance_id = client.env_create(env_id)
        # Check properties
        all_envs = client.env_list_all()
        logger.info(f”all_envs:{all_envs}”)
        action_info = \
            client.env_action_space_info(instance_id)
        logger.info(f”action_info:{action_info}”)
        obs_info = \
            client.env_observation_space_info(instance_id)
        # logger.info(f”obs_info:{obs_info}”)
        # Run a single step
        init_obs = client.env_reset(instance_id)
        [observation, reward, done, info] = \
            client.env_step(instance_id, 1, True)
        logger.info(f”reward:{reward} done:{done} \
                      info:{info}”)
    
  19. 我们现在可以开始实际创建客户端实例并检查tradegym服务!首先,我们需要通过执行以下命令来启动tradegym服务器:

    (tfrl-cookbook)praveen@desktop:~/tensorflow2-reinforcement-learning-cookbook/src/ch7-cloud-deploy-deep-rl-agents$ python tradegym_http_server.py
    
  20. 现在,我们可以通过在另一个终端运行以下命令来启动tradegym客户端:

    (tfrl-cookbook)praveen@desktop:~/tensorflow2-reinforcement-learning-cookbook/src/ch7-cloud-deploy-deep-rl-agents$ python tradegym_http_client.py
    
  21. 你应该会在你启动tradegym_http_client.py脚本的终端中看到类似以下的输出:

    all_envs:{‘114c5e8f’: ‘StockTradingContinuousEnv-v0’, ‘6287385e’: ‘StockTradingContinuousEnv-v0’, ‘d55c97c0’: ‘StockTradingContinuousEnv-v0’, ‘fd355ed8’: ‘StockTradingContinuousEnv-v0’}
    action_info:{‘high’: [1.0], ‘low’: [-1.0], ‘name’: ‘Box’, ‘shape’: [1]}
    reward:0.0 done:False info:{}
    

这就完成了整个流程!让我们简要回顾一下它是如何工作的。

它是如何工作的……

tradegym服务器提供了一个环境容器类,并通过 REST API 公开环境接口。tradegym客户端提供了 Python 封装方法,通过 REST API 与 RL 环境进行交互。

Envs类充当tradegym服务器上实例化的环境的管理器。它还充当多个环境的容器,因为客户端可以发送请求创建多个(相同或不同的)环境。当tradegym客户端使用 REST API 请求tradegym服务器创建一个新环境时,服务器会创建所请求环境的实例并返回一个唯一的实例 ID(例如:8kdi4289)。从此时起,客户端可以使用实例 ID 来引用特定的环境。这使得客户端和代理训练代码可以同时与多个环境进行交互。因此,tradegym服务器通过 HTTP 提供一个 RESTful 接口,充当一个真正的服务。

准备好下一个流程了吗?让我们开始吧。

使用远程模拟器服务训练 RL 代理

在这个教程中,我们将探讨如何利用远程模拟器服务来训练我们的代理。我们将重用前面章节中的 SAC 代理实现,并专注于如何使用远程运行的强化学习模拟器(例如在云端)作为服务来训练 SAC 或任何强化学习代理。我们将使用前一个教程中构建的tradegym服务器为我们提供强化学习模拟器服务。

让我们开始吧!

准备就绪

为了完成这个教程,并确保你拥有最新版本,你需要先激活tf2rl-cookbook Python/conda 虚拟环境。确保更新环境,以匹配食谱代码库中的最新 conda 环境规范文件(tfrl-cookbook.yml)。如果以下import语句没有问题,说明你已经准备好开始了:

import datetime
import os
import sys
import logging
import gym.spaces
import numpy as np
import tensorflow as tf
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from tradegym_http_client import Client
from sac_agent_base import SAC

让我们直接进入正题。

怎么做……

我们将实现训练脚本的核心部分,并省略命令行配置及其他非必要功能,以保持脚本简洁。我们将命名脚本为3_training_rl_agents_using_remote_sims.py

让我们开始吧!

  1. 让我们先创建一个应用级别的子日志记录器,添加一个流处理器,并设置日志级别:

    # Create an App-level child logger
    logger = logging.getLogger(“TFRL-cookbook-ch7-training-with-sim-server”)
    # Set handler for this logger to handle messages
    logger.addHandler(logging.StreamHandler())
    # Set logging-level for this logger’s handler
    logger.setLevel(logging.DEBUG)
    
  2. 接下来,让我们创建一个 TensorFlow SummaryWriter来记录代理的训练进度:

    current_time = datetime.datetime.now().strftime(“%Y%m%d-%H%M%S”)
    train_log_dir = os.path.join(“logs”, “TFRL-Cookbook-Ch4-SAC”, current_time)
    summary_writer = tf.summary.create_file_writer(train_log_dir)
    
  3. 我们现在可以进入实现的核心部分。让我们从实现__main__函数开始,并在接下来的步骤中继续实现。首先设置客户端,使用服务器地址连接到模拟服务:

    if __name__ == “__main__”:
        # Set up client to connect to sim server
        sim_service_address = “http://127.0.0.1:6666”
        client = Client(sim_service_address)
    
  4. 接下来,让我们请求服务器创建我们想要的强化学习训练环境来训练我们的代理:

        # Set up training environment
        env_id = “StockTradingContinuousEnv-v0”
        instance_id = client.env_create(env_id)
    
  5. 现在,让我们初始化我们的代理:

        # Set up agent
        observation_space_info = \
            client.env_observation_space_info(instance_id)
        observation_shape = \
            observation_space_info.get(“shape”)
        action_space_info = \
            client.env_action_space_info(instance_id)
        action_space = gym.spaces.Box(
            np.array(action_space_info.get(“low”)),
            np.array(action_space_info.get(“high”)),
            action_space_info.get(“shape”),
        )
        agent = SAC(observation_shape, action_space)
    
  6. 我们现在准备好使用一些超参数来配置训练:

        # Configure training
        max_epochs = 30000
        random_epochs = 0.6 * max_epochs
        max_steps = 100
        save_freq = 500
        reward = 0
        done = False
        done, use_random, episode, steps, epoch, \
        episode_reward = (
            False,
            True,
            0,
            0,
            0,
            0,
        )
    
  7. 到此,我们已经准备好开始外部训练循环:

        cur_state = client.env_reset(instance_id)
        # Start training
        while epoch < max_epochs:
            if steps > max_steps:
                done = True
    
  8. 现在,让我们处理当一个回合结束并且done被设置为True的情况:

            if done:
                episode += 1
                logger.info(
                    f”episode:{episode} \
                     cumulative_reward:{episode_reward} \
                     steps:{steps} epochs:{epoch}”)
                with summary_writer.as_default():
                    tf.summary.scalar(“Main/episode_reward”, 
                                episode_reward, step=episode)
                    tf.summary.scalar(“Main/episode_steps”,
                                       steps, step=episode)
                summary_writer.flush()
                done, cur_state, steps, episode_reward = (
                    False, 
                client.env_reset(instance_id), 0, 0,)
                if episode % save_freq == 0:
                    agent.save_model(
                        f”sac_actor_episode{episode}_\
                          {env_id}.h5”,
                        f”sac_critic_episode{episode}_\
                          {env_id}.h5”,
                    )
    
  9. 现在是关键步骤!让我们使用代理的acttrain方法,通过采取行动(执行动作)和使用收集到的经验来训练代理:

            if epoch > random_epochs:
                use_random = False
            action = agent.act(np.array(cur_state), 
                               use_random=use_random)
            next_state, reward, done, _ = client.env_step(
                instance_id, action.numpy().tolist()
            )
            agent.train(np.array(cur_state), action, reward,
                        np.array(next_state), done)
    
  10. 现在,让我们更新变量,为接下来的步骤做准备:

            cur_state = next_state
            episode_reward += reward
            steps += 1
            epoch += 1
            # Update Tensorboard with Agent’s training status
            agent.log_status(summary_writer, epoch, reward)
            summary_writer.flush()
    
  11. 这就完成了我们的训练循环。很简单,对吧?训练完成后,别忘了保存代理的模型,这样在部署时我们就可以使用已训练的模型:

        agent.save_model(
            f”sac_actor_final_episode_{env_id}.h5”, \
            f”sac_critic_final_episode_{env_id}.h5”
        )
    
  12. 你现在可以继续并使用以下命令运行脚本:

    (tfrl-cookbook)praveen@desktop:~/tensorflow2-reinforcement-learning-cookbook/src/ch7-cloud-deploy-deep-rl-agents$ python 3_training_rl_agents_using_remote_sims.py
    
  13. 我们是不是忘了什么?客户端连接的是哪个模拟服务器?模拟服务器正在运行吗?!如果你在命令行看到一个类似以下的长错误信息,那么很可能是模拟服务器没有启动:

    Failed to establish a new connection: [Errno 111] Connection refused’))
    
  14. 这次我们要做对!让我们通过使用以下命令启动tradegym服务器,确保我们的模拟服务器正在运行:

    (tfrl-cookbook)praveen@desktop:~/tensorflow2-reinforcement-learning-cookbook/src/ch7-cloud-deploy-deep-rl-agents$ python tradegym_http_server.py
    
  15. 我们现在可以使用以下命令启动代理训练脚本(与之前相同):

    (tfrl-cookbook)praveen@desktop:~/tensorflow2-reinforcement-learning-cookbook/src/ch7-cloud-deploy-deep-rl-agents$ python 3_training_rl_agents_using_remote_sims.py
    
  16. 你应该看到类似以下内容的输出:

    ...
    Total params: 16,257
    Trainable params: 16,257
    Non-trainable params: 0
    __________________________________________________________________________________________________
    None
    episode:1 cumulative_reward:370.45421418744525 steps:9 epochs:9
    episode:2 cumulative_reward:334.52956448599605 steps:9 epochs:18
    episode:3 cumulative_reward:375.27432450733943 steps:9 epochs:27
    episode:4 cumulative_reward:363.7160827166332 steps:9 epochs:36
    episode:5 cumulative_reward:363.2819222532322 steps:9 epochs:45
    ...
    

这就完成了我们用于通过远程仿真训练 RL 代理的脚本!

它是如何工作的…

到目前为止,我们一直直接使用gym库与仿真器交互,因为我们在代理训练脚本中运行 RL 环境仿真器。虽然对于依赖 CPU 的本地仿真器,这样做已经足够,但随着我们开始使用高级仿真器,或使用我们没有的仿真器,甚至在我们不想运行或管理仿真器实例的情况下,我们可以利用我们在本章之前配方中构建的客户端包装器,连接到像tradegym这样的 RL 环境,它们公开了 REST API 接口。在这个配方中,代理训练脚本利用tradegym客户端模块与远程tradegym服务器进行交互,从而完成 RL 训练循环。

有了这些,让我们继续下一个配方,看看如何评估之前训练过的代理。

测试/评估 RL 代理

假设你已经使用训练脚本(前一个配方)在某个交易环境中训练了 SAC 代理,并且你有多个版本的训练代理模型,每个模型都有不同的策略网络架构或超参数,或者你对其进行了调整和自定义以提高性能。当你想要部署一个代理时,你肯定希望选择表现最好的代理,对吧?

本配方将帮助你构建一个精简的脚本,用于在本地评估给定的预训练代理模型,从而获得定量性能评估,并在选择合适的代理模型进行部署之前比较多个训练模型。具体来说,我们将使用本章之前构建的tradegym模块和sac_agent_runtime模块来评估我们训练的代理模型。

让我们开始吧!

准备工作

要完成此配方,首先需要激活tf2rl-cookbook的 Python/conda 虚拟环境。确保更新环境以匹配最新的 conda 环境规范文件(tfrl-cookbook.yml),该文件位于食谱的代码库中。如果以下import语句没有问题,说明你已经准备好开始了:

#!/bin/env/python
import os
import sys
from argparse import ArgumentParser
import imageio
import gym

如何操作…

让我们专注于创建一个简单但完整的代理评估脚本:

  1. 首先,让我们导入用于训练环境的tradegym模块和 SAC 代理运行时:

    sys.path.append(os.path.dirname(os.path.abspath(__file__)))
    import tradegym  # Register tradegym envs with OpenAI Gym registry
    from sac_agent_runtime import SAC
    
  2. 接下来,让我们创建一个命令行参数解析器来处理命令行配置:

    parser = ArgumentParser(prog=”TFRL-Cookbook-Ch7-Evaluating-RL-Agents”)
    parser.add_argument(“--agent”, default=”SAC”, help=”Name of Agent. Default=SAC”)
    
  3. 现在,让我们为--env参数添加支持,以指定 RL 环境 ID,并为–-num-episodes添加支持,以指定评估代理的回合数。让我们为这两个参数设置一些合理的默认值,这样即使没有任何参数,我们也能运行脚本进行快速(或者说是懒惰?)测试:

    parser.add_argument(
        “--env”,
        default=”StockTradingContinuousEnv-v0”,
        help=”Name of Gym env. Default=StockTradingContinuousEnv-v0”,
    )
    parser.add_argument(
        “--num-episodes”,
        default=10,
        help=”Number of episodes to evaluate the agent.\
              Default=100”,
    )
    
  4. 让我们还为–-trained-models-dir添加支持,用于指定包含训练模型的目录,并为–-model-version标志添加支持,用于指定该目录中的特定模型版本:

    parser.add_argument(
        “--trained-models-dir”,
        default=”trained_models”,
        help=”Directory contained trained models. Default=trained_models”,
    )
    parser.add_argument(
        “--model-version”,
        default=”episode100”,
        help=”Trained model version. Default=episode100”,
    )
    
  5. 现在,我们准备好完成参数解析:

    args = parser.parse_args()
    
  6. 让我们从实现__main__方法开始,并在接下来的步骤中继续实现它。首先,我们从创建一个本地实例的 RL 环境开始,在该环境中我们将评估代理:

    if __name__ == “__main__”:
        # Create an instance of the evaluation environment
        env = gym.make(args.env)
    
  7. 现在,让我们初始化代理类。暂时我们只支持 SAC 代理,但如果你希望支持本书中讨论的其他代理,添加支持非常容易:

        if args.agent != “SAC”:
            print(f”Unsupported Agent: {args.agent}. Using \
                    SAC Agent”)
            args.agent = “SAC”
        # Create an instance of the Soft Actor-Critic Agent
        agent = SAC(env.observation_space.shape, \
                    env.action_space)
    
  8. 接下来,让我们加载训练好的代理模型:

        # Load trained Agent model/brain
        model_version = args.model_version
        agent.load_actor(
            os.path.join(args.trained_models_dir, \
                         f”sac_actor_{model_version}.h5”)
        )
        agent.load_critic(
            os.path.join(args.trained_models_dir, \
                         f”sac_critic_{model_version}.h5”)
        )
        print(f”Loaded {args.agent} agent with trained \
                model version:{model_version}”)
    
  9. 我们现在准备好使用测试环境中的训练模型来评估代理:

        # Evaluate/Test/Rollout Agent with trained model/
        # brain
        video = imageio.get_writer(“agent_eval_video.mp4”,\
                                    fps=30)
        avg_reward = 0
        for i in range(args.num_episodes):
            cur_state, done, rewards = env.reset(), False, 0
            while not done:
                action = agent.act(cur_state, test=True)
                next_state, reward, done, _ = \
                                    env.step(action[0])
                cur_state = next_state
                rewards += reward
                if render:
                    video.append_data(env.render(mode=\
                                                ”rgb_array”))
            print(f”Episode#:{i} cumulative_reward:\
                    {rewards}”)
            avg_reward += rewards
        avg_reward /= args.num_episodes
        video.close()
        print(f”Average rewards over {args.num_episodes} \
                episodes: {avg_reward}”)
    
  10. 现在,让我们尝试在StockTradingContinuous-v0环境中评估代理。请注意,股票交易环境中的市场数据源(data/MSFT.csvdata/TSLA.csv)可能与用于训练的市场数据不同!毕竟,我们想要评估的是代理学会如何交易!运行以下命令启动代理评估脚本:

    (tfrl-cookbook)praveen@desktop:~/tensorflow2-reinforcement-learning-cookbook/src/ch7-cloud-deploy-deep-rl-agents$ python 4_evaluating_rl_agents.py
    
  11. 根据你训练的代理表现如何,你会在控制台看到类似以下的输出(奖励值会有所不同):

    ...
    ==================================================================================================
    Total params: 16,257
    Trainable params: 16,257
    Non-trainable params: 0
    __________________________________________________________________________________________________
    None
    Loaded SAC agent with trained model version:episode100
    Episode#:0 cumulative_reward:382.5117154452246
    Episode#:1 cumulative_reward:359.27720004181674
    Episode#:2 cumulative_reward:370.92829808499664
    Episode#:3 cumulative_reward:341.44002189086007
    Episode#:4 cumulative_reward:364.32631211784394
    Episode#:5 cumulative_reward:385.89219327764476
    Episode#:6 cumulative_reward:365.2120387185878
    Episode#:7 cumulative_reward:339.98494537310785
    Episode#:8 cumulative_reward:362.7133769241483
    Episode#:9 cumulative_reward:379.12388043270073
    Average rewards over 10 episodes: 365.1409982306931
    ...
    

就这些!

工作原理……

我们初始化了一个 SAC 代理,只使用了通过sac_agent_runtime模块评估代理所需的运行时组件,并加载了先前训练好的模型版本(包括演员和评论家模型),这些都可以通过命令行参数进行自定义。然后,我们使用tradegym库创建了一个StockTradingContinuousEnv-v0环境的本地实例,并评估了我们的代理,以便获取累积奖励作为评估训练代理模型性能的量化指标。

既然我们已经知道如何评估并选择表现最好的代理,让我们进入下一个步骤,了解如何打包训练好的代理以进行部署!

打包强化学习代理以便部署——一个交易机器人

这是本章的一个关键步骤,我们将在这里讨论如何将代理打包,以便我们可以将其作为服务部署到云端(下一个步骤!)。我们将实现一个脚本,该脚本将我们的训练好的代理模型并将act方法暴露为一个 RESTful 服务。接着,我们会将代理和 API 脚本打包成一个Docker容器,准备好部署到云端!通过本步骤,你将构建一个准备好部署的 Docker 容器,其中包含你的训练好的强化学习代理,能够创建并提供 Agent/Bot-as-a-Service!

让我们深入了解细节。

准备工作

要完成这个步骤,你需要首先激活tf2rl-cookbook的 Python/conda 虚拟环境。确保更新环境,以便与 cookbook 代码库中的最新 conda 环境规格文件(tfrl-cookbook.yml)匹配。如果以下import语句没有问题,你就可以进行下一步,设置 Docker 环境:

import os
import sys
from argparse import ArgumentParser
import gym.spaces
from flask import Flask, request
import numpy as np

对于这个食谱,您需要安装 Docker。请按照官方安装说明为您的平台安装 Docker。您可以在docs.docker.com/get-docker/找到相关说明。

如何操作……

我们将首先实现脚本,将代理的act方法暴露为 REST 服务,然后继续创建 Dockerfile 以将代理容器化:

  1. 首先,让我们导入本章早些时候构建的sac_agent_runtime

    sys.path.append(os.path.dirname(os.path.abspath(__file__)))
    from sac_agent_runtime import SAC
    
  2. 接下来,让我们为命令行参数创建一个处理程序,并将--agent作为第一个支持的参数,以便指定我们想要使用的代理算法:

    parser = ArgumentParser(
        prog=”TFRL-Cookbook-Ch7-Packaging-RL-Agents-For-Cloud-Deployments”
    )
    parser.add_argument(“--agent”, default=”SAC”, help=”Name of Agent. Default=SAC”)
    
  3. 接下来,让我们添加参数,以便指定我们代理将要部署的主机服务器的 IP 地址和端口。现在我们将设置并使用默认值,当需要时可以从命令行更改它们:

    parser.add_argument(
        “--host-ip”,
        default=”0.0.0.0”,
        help=”IP Address of the host server where Agent  
              service is run. Default=127.0.0.1”,
    )
    parser.add_argument(
        “--host-port”,
        default=”5555”,
        help=”Port on the host server to use for Agent 
              service. Default=5555”,
    )
    
  4. 接下来,让我们添加对指定包含训练好的代理模型的目录以及使用的特定模型版本的参数支持:

    parser.add_argument(
        “--trained-models-dir”,
        default=”trained_models”,
        help=”Directory contained trained models. \
              Default=trained_models”,
    )
    parser.add_argument(
        “--model-version”,
        default=”episode100”,
        help=”Trained model version. Default=episode100”,
    )
    
  5. 作为支持的最终参数集,让我们添加允许指定基于训练模型配置的观测形状和动作空间规格的参数:

    parser.add_argument(
        “--observation-shape”,
        default=(6, 31),
        help=”Shape of observations. Default=(6, 31)”,
    )
    parser.add_argument(
        “--action-space-low”, default=[-1], help=”Low value \
         of action space. Default=[-1]”
    )
    parser.add_argument(
        “--action-space-high”, default=[1], help=”High value\
         of action space. Default=[1]”
    )
    parser.add_argument(
        “--action-shape”, default=(1,), help=”Shape of \
        actions. Default=(1,)”
    )
    
  6. 现在我们可以完成参数解析器,并开始实现__main__函数:

    args = parser.parse_args()
    if __name__ == “__main__”:
    
  7. 首先,让我们加载代理的运行时配置:

        if args.agent != “SAC”:
            print(f”Unsupported Agent: {args.agent}. Using \
                    SAC Agent”)
            args.agent = “SAC”
        # Set Agent’s runtime configs
        observation_shape = args.observation_shape
        action_space = gym.spaces.Box(
            np.array(args.action_space_low),
            np.array(args.action_space_high),
            args.action_shape,
        )
    
  8. 接下来,让我们创建一个代理实例,并从预训练模型中加载代理的演员和评论家网络的权重:

        # Create an instance of the Agent
        agent = SAC(observation_shape, action_space)
        # Load trained Agent model/brain
        model_version = args.model_version
        agent.load_actor(
            os.path.join(args.trained_models_dir, \
                         f”sac_actor_{model_version}.h5”)
        )
        agent.load_critic(
            os.path.join(args.trained_models_dir, \
                         f”sac_critic_{model_version}.h5”)
        )
        print(f”Loaded {args.agent} agent with trained model\
                 version:{model_version}”)
    
  9. 现在我们可以使用 Flask 设置服务端点,这将和以下代码行一样简单。请注意,我们在/v1/act端点暴露了代理的act方法:

        # Setup Agent (http) service
        app = Flask(__name__)
        @app.route(“/v1/act”, methods=[“POST”])
        def get_action():
            data = request.get_json()
            action = agent.act(np.array(data.get(
                               “observation”)), test=True)
            return {“action”: action.numpy().tolist()}
    
  10. 最后,我们只需要添加一行代码,当执行时启动 Flask 应用程序以启动服务:

        # Launch/Run the Agent (http) service
        app.run(host=args.host_ip, port=args.host_port, 
                debug=True)
    
  11. 我们的代理 REST API 实现已经完成。现在我们可以集中精力为代理服务创建一个 Docker 容器。我们将通过指定基础镜像为nvidia/cuda:*来开始实现 Dockerfile,这样我们就能获得必要的 GPU 驱动程序,以便在部署代理的服务器上使用 GPU。接下来的代码行将放入名为Dockerfile的文件中:

    FROM nvidia/cuda:10.1-cudnn7-devel-ubuntu18.04
    # TensorFlow2.x Reinforcement Learning Cookbook
    # Chapter 7: Deploying Deep RL Agents to the cloud
    LABEL maintainer=”emailid@domain.tld”
    
  12. 现在让我们安装一些必要的系统级软件包,并清理文件以节省磁盘空间:

    RUN apt-get install -y wget git make cmake zlib1g-dev && rm -rf /var/lib/apt/lists/*
    
  13. 为了执行我们的代理运行时并安装所有必需的软件包,我们将使用 conda Python 环境。所以,让我们继续按照说明下载并在容器中设置miniconda

    ENV PATH=”/root/miniconda3/bin:${PATH}”
    ARG PATH=”/root/miniconda3/bin:${PATH}”
    RUN apt-get update
    RUN wget \
        https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh \
        && mkdir /root/.conda \
        && bash Miniconda3-latest-Linux-x86_64.sh -b \
        && rm -f Miniconda3-latest-Linux-x86_64.sh
    # conda>4.9.0 is required for `--no-capture-output`
    RUN conda update -n base conda
    
  14. 现在让我们将本章的源代码复制到容器中,并使用tfrl-cookbook.yml文件中指定的软件包列表创建 conda 环境:

    ADD . /root/tf-rl-cookbook/ch7
    WORKDIR /root/tf-rl-cookbook/ch7
    RUN conda env create -f “tfrl-cookbook.yml” -n “tfrl-cookbook”
    
  15. 最后,我们只需为容器设置ENTRYPOINTCMD,当容器启动时,这些将作为参数传递给ENTRYPOINT

    ENTRYPOINT [ “conda”, “run”, “--no-capture-output”, “-n”, “tfrl-cookbook”, “python” ]
    CMD [ “5_packaging_rl_agents_for_deployment.py” ]
    
  16. 这完成了我们的 Dockerfile,现在我们准备通过构建 Docker 容器来打包我们的代理。你可以运行以下命令,根据 Dockerfile 中的指令构建 Docker 容器,并为其打上你选择的容器镜像名称。让我们使用以下命令:

    (tfrl-cookbook)praveen@desktop:~/tensorflow2-reinforcement-learning-cookbook/src/ch7-cloud-deploy-deep-rl-agents$docker build -f Dockerfile -t tfrl-cookbook/ch7-trading-bot:latest
    
  17. 如果你是第一次运行前面的命令,Docker 可能需要花费较长时间来构建容器。之后的运行或更新将会更快,因为中间层可能已经在第一次运行时被缓存。当一切顺利时,你会看到类似下面的输出(注意,由于我之前已经构建过容器,因此大部分层已经被缓存):

    Sending build context to Docker daemon  1.793MB
    Step 1/13 : FROM nvidia/cuda:10.1-cudnn7-devel-ubuntu18.04
     ---> a3bd8cb789b0
    Step 2/13 : LABEL maintainer=”emailid@domain.tld”
     ---> Using cache
     ---> 4322623c24c8
    Step 3/13 : ENV PATH=”/root/miniconda3/bin:${PATH}”
     ---> Using cache
     ---> e9e8c882662a
    Step 4/13 : ARG PATH=”/root/miniconda3/bin:${PATH}”
     ---> Using cache
     ---> 31d45d5bcb05
    Step 5/13 : RUN apt-get update
     ---> Using cache
     ---> 3f7ed3eb3c76
    Step 6/13 : RUN apt-get install -y wget git make cmake zlib1g-dev && rm -rf /var/lib/apt/lists/*
     ---> Using cache
     ---> 0ffb6752f5f6
    Step 7/13 : RUN wget     https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh     && mkdir /root/.conda     && bash Miniconda3-latest-Linux-x86_64.sh -b     && rm -f Miniconda3-latest-Linux-x86_64.sh
     ---> Using cache
    
  18. 对于涉及从磁盘进行 COPY/ADD 文件操作的层,指令将会执行,因为它们无法被缓存。例如,你会看到以下来自第 9 步的步骤会继续执行而不使用任何缓存。即使你已经构建过容器,这也是正常的:

    Step 9/13 : ADD . /root/tf-rl-cookbook/ch7
     ---> ed8541c42ebc
    Step 10/13 : WORKDIR /root/tf-rl-cookbook/ch7
     ---> Running in f5a9c6ad485c
    Removing intermediate container f5a9c6ad485c
     ---> 695ca00c6db3
    Step 11/13 : RUN conda env create -f “tfrl-cookbook.yml” -n “tfrl-cookbook”
     ---> Running in b2a9706721e7
    Collecting package metadata (repodata.json): ...working... done
    Solving environment: ...working... done...
    
  19. 最后,当 Docker 容器构建完成时,你将看到类似以下的消息:

    Step 13/13 : CMD [ “2_packaging_rl_agents_for_deployment.py” ]
     ---> Running in 336e442b0218
    Removing intermediate container 336e442b0218
     ---> cc1caea406e6
    Successfully built cc1caea406e6
    Successfully tagged tfrl-cookbook/ch7:latest
    

恭喜你成功打包了 RL 代理,准备部署!

它是如何工作的…

我们利用了本章前面构建的 sac_agent_runtime 来创建和初始化一个 SAC 代理实例。然后我们加载了预训练的代理模型,分别用于演员和评论员。之后,我们将 SAC 代理的 act 方法暴露为一个 REST API,通过 HTTP POST 端点来接受观察值作为 POST 消息,并将动作作为响应返回。最后,我们将脚本作为 Flask 应用启动,开始服务。

在本食谱的第二部分,我们将代理应用程序 actioa-serving 打包为 Docker 容器,并准备好进行部署!

我们现在即将将代理部署到云端!继续下一部分食谱,了解如何操作。

将 RL 代理部署到云端——作为服务的交易机器人

训练 RL 代理的终极目标是利用它在新的观察值下做出决策。以我们的股票交易 SAC 代理为例,到目前为止,我们已经学会了如何训练、评估并打包表现最佳的代理模型来构建交易机器人。虽然我们集中在一个特定的应用场景(自动交易机器人),但你可以看到,根据本书前几章中的食谱,如何轻松地更改训练环境或代理算法。本食谱将指导你通过将 Docker 容器化的 RL 代理部署到云端并运行作为服务的机器人。

正在准备中

要完成这个教程,你需要访问像 Azure、AWS、GCP、Heroku 等云服务,或其他支持托管和运行 Docker 容器的云服务提供商。如果你是学生,可以利用 GitHub 的学生开发者套餐(education.github.com/pack),该套餐从 2020 年起为你提供一些免费福利,包括 100 美元的 Microsoft Azure 信用或作为新用户的 50 美元 DigitalOcean 平台信用。

有很多指南讲解如何将 Docker 容器推送到云端并作为服务部署/运行。例如,如果你有 Azure 账户,可以参照官方指南:docs.microsoft.com/en-us/azure/container-instances/container-instances-quickstart

本指南将带你通过多种选项(CLI、门户、PowerShell、ARM 模板和 Docker CLI)来部署基于 Docker 容器的代理服务。

如何操作……

我们将首先在本地部署交易机器人并进行测试。之后,我们可以将其部署到你选择的云服务上。作为示例,本教程将带你通过将其部署到 Heroku 的步骤(heroku.com)。

我们开始吧:

  1. 首先使用以下命令构建包含交易机器人的 Docker 容器。请注意,如果你之前已经按照本章的其他教程构建过容器,那么根据缓存的层和对 Dockerfile 所做的更改,以下命令可能会更快地执行完毕:

    (tfrl-cookbook)praveen@desktop:~/tensorflow2-reinforcement-learning-cookbook/src/ch7-cloud-deploy-deep-rl-agents$docker build -f Dockerfile -t tfrl-cookbook/ch7-trading-bot:latest
    
  2. 一旦 Docker 容器成功构建,我们可以使用以下命令启动机器人:

    (tfrl-cookbook)praveen@desktop:~/tensorflow2-reinforcement-learning-cookbook/src/ch7-cloud-deploy-deep-rl-agents$docker run -it -p 5555:5555 tfrl-cookbook/ch7-trading-bot
    
  3. 如果一切顺利,你应该会看到类似以下的控制台输出,表示机器人已启动并准备好执行操作:

    ...==================================================================================================
    Total params: 16,257
    Trainable params: 16,257
    Non-trainable params: 0
    __________________________________________________________________________________________________
    None
    Loaded SAC agent with trained model version:episode100
     * Debugger is active!
     * Debugger PIN: 604-104-903
    ...
    
  4. 现在你已经在本地(在你自己的服务器上)部署了交易机器人,接下来我们来创建一个简单的脚本,利用你构建的 Bot-as-a-Service。创建一个名为test_agent_service.py的文件,内容如下:

    #Simple test script for the deployed Trading Bot-as-a-Service
    import os
    import sys
    import gym
    import requests
    sys.path.append(os.path.dirname(os.path.abspath(__file__)))
    import tradegym  # Register tradegym envs with OpenAI Gym # registry
    host_ip = “127.0.0.1”
    host_port = 5555
    endpoint = “v1/act”
    env = gym.make(“StockTradingContinuousEnv-v0”)
    post_data = {“observation”: env.observation_space.sample().tolist()}
    res = requests.post(f”http://{host_ip}:{host_port}/{endpoint}”, json=post_data)
    if res.ok:
        print(f”Received Agent action:{res.json()}”)
    
  5. 你可以使用以下命令执行该脚本:

    (tfrl-cookbook)praveen@desktop:~/tensorflow2-reinforcement-learning-cookbook/src/ch7-cloud-deploy-deep-rl-agents$python test_agent_service.py
    
  6. 请注意,您的机器人容器仍需要运行。一旦执行前述命令,你将看到类似以下的输出,表示在/v1/act端点接收到新的 POST 消息,并返回了 HTTP 响应状态 200,表示成功:

    172.17.0.1 - - [00/Mmm/YYYY hh:mm:ss] “POST /v1/act HTTP/1.1200 -
    
  7. 你还会注意到,测试脚本在其控制台窗口中会打印出类似以下的输出,表示它收到了来自交易机器人的一个操作:

    Received Agent action:{‘action’: [[0.008385116065491426]]}
    
  8. 现在是将你的交易机器人部署到云平台的时候了,这样你或其他人就可以通过互联网访问它!正如在入门部分所讨论的,你在选择云服务提供商方面有多个选择,可以将你的 Docker 容器镜像托管并部署 RL 代理 Bot-as-a-Service。我们将以 Heroku 为例,它提供免费托管服务和简便的命令行界面。首先,你需要安装 Heroku CLI。按照 devcenter.heroku.com/articles/heroku-cli 上列出的官方说明为你的平台(Linux/Windows/macOS X)安装 Heroku CLI。在 Ubuntu Linux 上,我们可以使用以下命令:

    sudo snap install --classic heroku
    
  9. 一旦安装了 Heroku CLI,你可以使用以下命令登录 Heroku 容器注册表:

    heroku container:login
    
  10. 接下来,从包含代理的 Dockerfile 的目录运行以下命令;例如:

    tfrl-cookbook)praveen@desktop:~/tensorflow2-reinforcement-learning-cookbook/src/ch7-cloud-deploy-deep-rl-agents$heroku create
    
  11. 如果你尚未登录 Heroku,你将被提示登录:

    Creating app... !
         Invalid credentials provided.
     ›   Warning: heroku update available from 7.46.2 to 
    7.47.0.
    heroku: Press any key to open up the browser to login or q to exit:
    
  12. 登录后,你将看到类似如下的输出:

    Creating salty-fortress-4191... done, stack is heroku-18
    https://salty-fortress-4191.herokuapp.com/ | https://git.heroku.com/salty-fortress-4191.git
    
  13. 这就是你在 Heroku 上的容器注册表地址。你现在可以使用以下命令构建你的机器人容器并将其推送到 Heroku:

    heroku container:push web
    
  14. 一旦该过程完成,你可以使用以下命令将机器人容器镜像发布到 Heroku 应用:

    heroku container:release web
    
  15. 恭喜!你刚刚将你的机器人部署到了云端!你现在可以通过新的地址访问你的机器人,例如在之前代码中使用的示例地址 salty-fortress-4191.herokuapp.com/。你应该能够向你的机器人发送观察数据,并获取机器人的回应动作!恭喜你成功部署了你的 Bot-as-a-Service!

我们现在准备好结束本章内容了。

它是如何工作的……

我们首先通过使用 docker run 命令并指定将本地端口 5555 映射到容器的端口 5555,在你的机器上本地构建并启动了 Docker 容器。这将允许主机(你的机器)使用该端口与容器通信,就像它是机器上的本地端口一样。部署后,我们使用了一个测试脚本,该脚本利用 Python 的 request 库创建了一个带有观察值示例数据的 POST 请求,并将其发送到容器中的机器人。我们观察到机器人如何通过命令行的状态输出响应请求,并返回成功的回应,包含机器人的交易动作。

然后我们将相同的容器与机器人一起部署到云端(Heroku)。成功部署后,可以通过 Heroku 自动创建的公共 herokuapp URL 在网络上访问机器人。

这完成了本章的内容和食谱!希望你在整个过程中感到愉快。下章见。

第八章:第八章:加速深度强化学习代理开发的分布式训练

训练深度强化学习代理解决任务需要大量的时间,因为其样本复杂度很高。对于实际应用,快速迭代代理训练和测试周期对于深度强化学习应用的市场就绪度至关重要。本章中的配方提供了如何利用 TensorFlow 2.x 的能力,通过分布式训练深度神经网络模型来加速深度强化学习代理开发的说明。讨论了如何在单台机器以及跨机器集群上利用多个 CPU 和 GPU 的策略。本章还提供了使用RayTuneRLLib 框架训练分布式深度强化学习Deep RL)代理的多个配方。

具体来说,本章包含以下配方:

  • 使用 TensorFlow 2.x 构建分布式深度学习模型 – 多 GPU 训练

  • 扩展规模与范围 – 多机器、多 GPU 训练

  • 大规模训练深度强化学习代理 – 多 GPU PPO 代理

  • 为加速训练构建分布式深度强化学习的构建模块

  • 使用 Ray、Tune 和 RLLib 进行大规模深度强化学习(Deep RL)代理训练

技术要求

本书中的代码在 Ubuntu 18.04 和 Ubuntu 20.04 上经过广泛测试,且如果安装了 Python 3.6+,应该也能在之后版本的 Ubuntu 上运行。只要安装了 Python 3.6+ 以及所需的 Python 包(每个配方开始前都会列出),代码也应该能够在 Windows 和 Mac OSX 上正常运行。建议创建并使用名为 tf2rl-cookbook 的 Python 虚拟环境来安装本书中所需的包并运行代码。推荐使用 Miniconda 或 Anaconda 来管理 Python 虚拟环境。

每个配方的完整代码可以在此获取:github.com/PacktPublishing/Tensorflow-2-Reinforcement-Learning-Cookbook

使用 TensorFlow 2.x 进行分布式深度学习模型训练 – 多 GPU 训练

深度强化学习利用深度神经网络进行策略、价值函数或模型表示。对于高维观察/状态空间,例如图像或类似图像的观察,通常会使用卷积神经网络CNN)架构。虽然 CNN 强大且能训练适用于视觉控制任务的深度强化学习策略,但在强化学习的设置下,训练深度 CNN 需要大量时间。本配方将帮助你了解如何利用 TensorFlow 2.x 的分布式训练 API,通过多 GPU 训练深度残差网络ResNets)。本配方提供了可配置的构建模块,你可以用它们来构建深度强化学习组件,比如深度策略网络或价值网络。

让我们开始吧!

准备工作

要完成这个食谱,你需要首先激活 tf2rl-cookbook Python/conda 虚拟环境。确保更新环境,以匹配食谱代码库中最新的 conda 环境规范文件(tfrl-cookbook.yml)。拥有一台(本地或云端)配备一个或多个 GPU 的机器将对这个食谱有帮助。我们将使用 tensorflow_datasets,如果你使用 tfrl-cookbook.yml 来设置/更新了你的 conda 环境,它应该已经安装好了。

现在,让我们开始吧!

如何实现...

本食谱中的实现基于最新的官方 TensorFlow 文档/教程。接下来的步骤将帮助你深入掌握 TensorFlow 2.x 的分布式执行能力。我们将使用 ResNet 模型作为大模型的示例,它将从分布式训练中受益,利用多个 GPU 加速训练。我们将讨论构建 ResNet 的主要组件的代码片段。完整的实现请参考食谱代码库中的 resnet.py 文件。让我们开始:

  1. 让我们直接进入构建残差神经网络的模板:

    def resnet_block(
        input_tensor, size, kernel_size, filters, stage, \
         conv_strides=(2, 2), training=None
    ):
        x = conv_building_block(
            input_tensor,
            kernel_size,
            filters,
            stage=stage,
            strides=conv_strides,
            block="block_0",
            training=training,
        )
        for i in range(size - 1):
            x = identity_building_block(
                x,
                kernel_size,
                filters,
                stage=stage,
                block="block_%d" % (i + 1),
                training=training,
            )
        return x
    
  2. 使用上面的 ResNet 块模板,我们可以快速构建包含多个 ResNet 块的 ResNet。在本书中,我们将实现一个包含一个 ResNet 块的 ResNet,你可以在代码库中找到实现了多个可配置数量和大小的 ResNet 块的 ResNet。让我们开始并在接下来的几个步骤中完成 ResNet 的实现,每次集中讨论一个重要的概念。首先,让我们定义函数签名:

    def resnet(num_blocks, img_input=None, classes=10, training=None):
        """Builds the ResNet architecture using provided 
           config"""
    
  3. 接下来,让我们处理输入图像数据表示中的通道顺序。最常见的维度顺序是:batch_size x channels x width x heightbatch_size x width x height x channels。我们将处理这两种情况:

        if backend.image_data_format() == "channels_first":
            x = layers.Lambda(
                lambda x: backend.permute_dimensions(x, \
                    (0, 3, 1, 2)), name="transpose"
            )(img_input)
            bn_axis = 1
        else:  # channel_last
            x = img_input
            bn_axis = 3
    
  4. 现在,让我们对输入数据进行零填充,并应用初始层开始处理:

        x = tf.keras.layers.ZeroPadding2D(padding=(1, 1), \
                                         name="conv1_pad")(x)
        x = tf.keras.layers.Conv2D(16,(3, 3),strides=(1, 1),
                             padding="valid",
                             kernel_initializer="he_normal",
                             kernel_regularizer= \
                                tf.keras.regularizers.l2(
                                     L2_WEIGHT_DECAY), 
                             bias_regularizer= \
                                 tf.keras.regularizers.l2(
                                     L2_WEIGHT_DECAY), 
                                                            name="conv1",)(x)
        x = tf.keras.layers.BatchNormalization(axis=bn_axis,
                 name="bn_conv1", momentum=BATCH_NORM_DECAY,
                 epsilon=BATCH_NORM_EPSILON,)\
                      (x, training=training)
        x = tf.keras.layers.Activation("relu")(x)
    
  5. 现在是时候使用我们创建的 resnet_block 函数来添加 ResNet 块了:

        x = resnet_block(x, size=num_blocks, kernel_size=3,
            filters=[16, 16], stage=2, conv_strides=(1, 1),
            training=training,)
        x = resnet_block(x, size=num_blocks, kernel_size=3,
            filters=[32, 32], stage=3, conv_strides=(2, 2),
            training=training)
        x = resnet_block(x, size=num_blocks, kernel_size=3,
            filters=[64, 64], stage=4, conv_strides=(2, 2),
            training=training,)
    
  6. 作为最终层,我们希望添加一个经过 softmax 激活的 Dense(全连接)层,节点数量等于任务所需的输出类别数:

    x = tf.keras.layers.GlobalAveragePooling2D(
                                         name="avg_pool")(x)
        x = tf.keras.layers.Dense(classes,
            activation="softmax",
            kernel_initializer="he_normal",
            kernel_regularizer=tf.keras.regularizers.l2(
                 L2_WEIGHT_DECAY), 
            bias_regularizer=tf.keras.regularizers.l2(
                 L2_WEIGHT_DECAY), 
            name="fc10",)(x)
    
  7. 在 ResNet 模型构建函数中的最后一步是将这些层封装为一个 TensorFlow 2.x Keras 模型,并返回输出:

        inputs = img_input
        # Create model.
        model = tf.keras.models.Model(inputs, x, name=f"resnet{6 * num_blocks + 2}")
        return model
    
  8. 使用我们刚才讨论的 ResNet 函数,通过简单地改变块的数量,构建具有不同层深度的深度残差网络变得非常容易。例如,以下是可能的:

    resnet_mini = functools.partial(resnet, num_blocks=1)
    resnet20 = functools.partial(resnet, num_blocks=3)
    resnet32 = functools.partial(resnet, num_blocks=5)
    resnet44 = functools.partial(resnet, num_blocks=7)
    resnet56 = functools.partial(resnet, num_blocks=9)
    
  9. 定义好我们的模型后,我们可以跳到多 GPU 训练代码。本食谱中的剩余步骤将引导你完成实现过程,帮助你利用机器上的所有可用 GPU 加速训练 ResNet。让我们从导入我们构建的 ResNet 模块以及 tensorflow_datasets 模块开始:

    import os
    import sys
    import tensorflow as tf
    import tensorflow_datasets as tfds
    if "." not in sys.path:
        sys.path.insert(0, ".")
    import resnet
    
  10. 我们现在可以选择使用哪个数据集来运行我们的分布式训练管道。在这个食谱中,我们将使用dmlab数据集,该数据集包含在 DeepMind Lab 环境中,RL 代理通常观察到的图像。根据你训练机器的 GPU、RAM 和 CPU 的计算能力,你可能想使用一个更小的数据集,比如CIFAR10

    dataset_name = "dmlab"  # "cifar10" or "cifar100"; See tensorflow.org/datasets/catalog for complete list
    # NOTE: dmlab is large in size; Download bandwidth and # GPU memory to be considered
    datasets, info = tfds.load(name="dmlab", with_info=True,
                               as_supervised=True)
    dataset_train, dataset_test = datasets["train"], \
                                  datasets["test"]
    input_shape = info.features["image"].shape
    num_classes = info.features["label"].num_classes
    
  11. 下一步需要你全神贯注!我们将选择分布式执行策略。TensorFlow 2.x 将许多功能封装成了一个简单的 API 调用,如下面所示:

    strategy = tf.distribute.MirroredStrategy()
    print(f"Number of devices: {
               strategy.num_replicas_in_sync}")
    
  12. 在这一步中,我们将声明关键超参数,你可以根据机器的硬件(例如 RAM 和 GPU 内存)进行调整:

    num_train_examples = info.splits["train"].num_examples
    num_test_examples = info.splits["test"].num_examples
    BUFFER_SIZE = 1000  # Increase as per available memory
    BATCH_SIZE_PER_REPLICA = 64
    BATCH_SIZE = BATCH_SIZE_PER_REPLICA * \
                      strategy.num_replicas_in_sync
    
  13. 在开始准备数据集之前,让我们实现一个预处理函数,该函数在将图像传递给神经网络之前执行操作。你可以添加你自己的自定义预处理操作。在这个食谱中,我们只需要首先将图像数据转换为float32,然后将图像像素值范围转换为[0, 1],而不是典型的[0, 255]区间:

    def preprocess(image, label):
        image = tf.cast(image, tf.float32)
        image /= 255
        return image, label
    
  14. 我们已经准备好为训练和验证/测试创建数据集划分:

    train_dataset = (
        dataset_train.map(preprocess).cache().\
            shuffle(BUFFER_SIZE).batch(BATCH_SIZE)
    )
    eval_dataset = dataset_test.map(preprocess).batch(
                                                 BATCH_SIZE)
    
  15. 我们已经到了这个食谱的关键步骤!让我们在分布式策略的范围内实例化并编译我们的模型:

    with strategy.scope():
        # model = create_model()
        model = create_model("resnet_mini")
        tf.keras.utils.plot_model(model, 
                                 to_file="./slim_resnet.png", 
                                 show_shapes=True)
        model.compile(
            loss=\
              tf.keras.losses.SparseCategoricalCrossentropy(
                  from_logits=True),
            optimizer=tf.keras.optimizers.Adam(),
            metrics=["accuracy"],
        )
    
  16. 让我们还创建一些回调,用于将日志记录到 TensorBoard,并在训练过程中检查点保存我们的模型参数:

    checkpoint_dir = "./training_checkpoints"
    checkpoint_prefix = os.path.join(checkpoint_dir, 
                                     "ckpt_{epoch}")
    callbacks = [
        tf.keras.callbacks.TensorBoard(
            log_dir="./logs", write_images=True, \
            update_freq="batch"
        ),
        tf.keras.callbacks.ModelCheckpoint(
            filepath=checkpoint_prefix, \
            save_weights_only=True
        ),
    ]
    
  17. 有了这些,我们已经具备了使用分布式策略训练模型所需的一切。借助 Keras 用户友好的fit()API,它就像下面这样简单:

    model.fit(train_dataset, epochs=12, callbacks=callbacks)
    
  18. 当执行前面的行时,训练过程将开始。我们也可以使用以下几行手动保存模型:

    path = "saved_model/"
    model.save(path, save_format="tf")
    
  19. 一旦我们保存了检查点,加载权重并开始评估模型就变得很容易:

    model.load_weights(tf.train.latest_checkpoint(checkpoint_dir))
    eval_loss, eval_acc = model.evaluate(eval_dataset)
    print("Eval loss: {}, Eval Accuracy: {}".format(eval_loss, eval_acc))
    
  20. 为了验证使用分布式策略训练的模型在有复制和没有复制的情况下都能正常工作,我们将在接下来的步骤中使用两种不同的方法加载并评估它。首先,让我们使用我们用来训练模型的(相同的)策略加载不带复制的模型:

    unreplicated_model = tf.keras.models.load_model(path)
    unreplicated_model.compile(
        loss=tf.keras.losses.\
             SparseCategoricalCrossentropy(from_logits=True),
        optimizer=tf.keras.optimizers.Adam(),
        metrics=["accuracy"],
    )
    eval_loss, eval_acc = unreplicated_model.evaluate(eval_dataset)
    print("Eval loss: {}, Eval Accuracy: {}".format(eval_loss, eval_acc))
    
  21. 接下来,让我们在分布式执行策略的范围内加载模型,这将创建副本并评估模型:

    with strategy.scope():
        replicated_model = tf.keras.models.load_model(path)
        replicated_model.compile(
            loss=tf.keras.losses.\
             SparseCategoricalCrossentropy(from_logits=True),
            optimizer=tf.keras.optimizers.Adam(),
            metrics=["accuracy"],
        )
        eval_loss, eval_acc = \
            replicated_model.evaluate(eval_dataset)
        print("Eval loss: {}, \
              Eval Accuracy: {}".format(eval_loss, eval_acc))
    

    当你执行前面的两个代码块时,你会发现两种方法都会得到相同的评估准确度,这是一个好兆头,意味着我们可以在没有任何执行策略限制的情况下使用模型进行预测!

  22. 这完成了我们的食谱。让我们回顾一下并看看食谱是如何工作的。

它是如何工作的...

神经网络架构中的残差块应用了卷积滤波器,后接多个恒等块。具体来说,卷积块应用一次,接着是(size - 1)个恒等块,其中 size 是一个整数,表示卷积-恒等块的数量。恒等块实现了跳跃连接或短路连接,使得输入可以绕过卷积操作直接通过。卷积块则包含卷积层,后接批量归一化激活,再接一个或多个卷积-批归一化-激活层。我们构建的resnet模块使用这些卷积和恒等构建块来构建一个完整的 ResNet,并且可以通过简单地更改块的数量来配置不同大小的网络。网络的大小计算公式为6 * num_blocks + 2

一旦我们的 ResNet 模型准备好,我们使用tensorflow_datasets模块生成训练和验证数据集。TensorFlow 数据集模块提供了几个流行的数据集,如 CIFAR10、CIFAR100 和 DMLAB,这些数据集包含图像及其相关标签,用于分类任务。所有可用数据集的列表可以在此找到:tensorflow.org/datasets/catalog

在这个食谱中,我们使用了tf.distribute.MirroredStrategy的镜像策略进行分布式执行,它允许在一台机器上使用多个副本进行同步分布式训练。即使是在多副本的分布式执行下,我们发现使用回调进行常规的日志记录和检查点保存依然如预期工作。我们还验证了加载保存的模型并运行推理进行评估在有或没有复制的情况下都能正常工作,这使得模型在训练过程中使用了分布式执行策略后,依然具有可移植性,不会因增加任何额外限制而受影响!

是时候进入下一个食谱了!

扩展与扩展 – 多机器,多 GPU 训练

为了在深度学习模型的分布式训练中实现最大规模,我们需要能够跨 GPU 和机器利用计算资源。这可以显著减少迭代或开发新模型和架构所需的时间,从而加速您正在解决的问题的进展。借助 Microsoft Azure、Amazon AWS 和 Google GCP 等云计算服务,按小时租用多台 GPU 配备的机器变得更加容易且普遍。这比搭建和维护自己的多 GPU 多机器节点更经济。这个配方将提供一个快速的演练,展示如何使用 TensorFlow 2.x 的多工作节点镜像分布式执行策略训练深度模型,基于官方文档,您可以根据自己的使用场景轻松定制。在本配方的多机器多 GPU 分布式训练示例中,我们将训练一个深度残差网络(ResNet 或 resnet)用于典型的图像分类任务。相同的网络架构也可以通过对输出层进行轻微修改,供 RL 智能体用于其策略或价值函数表示,正如我们将在本章后续的配方中看到的那样。

让我们开始吧!

准备工作

要完成此配方,您首先需要激活tf2rl-cookbook Python/conda 虚拟环境。确保更新环境,以匹配配方代码仓库中的最新 conda 环境规范文件(tfrl-cookbook.yml)。为了运行分布式训练管道,建议设置一个包含两个或更多安装了 GPU 的机器的集群,可以是在本地或云实例中,如 Azure、AWS 或 GCP。虽然我们将要实现的训练脚本可以利用集群中的多台机器,但并不绝对需要设置集群,尽管推荐这样做。

现在,让我们开始吧!

如何做到这一点...

由于此分布式训练设置涉及多台机器,我们需要一个机器之间的通信接口,并且要能够寻址每台机器。这通常通过现有的网络基础设施和 IP 地址来完成:

  1. 我们首先设置一个描述集群配置参数的配置项,指定我们希望在哪里训练模型。以下代码块已被注释掉,您可以根据集群设置编辑并取消注释,或者如果仅想在单机配置上尝试,可以保持注释状态:

    # Uncomment the following lines and fill worker details 
    # based on your cluster configuration
    # tf_config = {
    #    "cluster": {"worker": ["1.2.3.4:1111", 
                     "localhost:2222"]},
    #    "task": {"index": 0, "type": "worker"},
    # }
    # os.environ["TF_CONFIG"] = json.dumps(tf_config)
    
  2. 为了利用多台机器的配置,我们将使用 TensorFlow 2.x 的 MultiWorkerMirroredStrategy

    strategy = tf.distribute.experimental.MultiWorkerMirroredStrategy()
    
  3. 接下来,让我们声明训练的基本超参数。根据您的集群/计算机配置,随时调整批处理大小和 NUM_GPUS 值:

    NUM_GPUS = 2
    BS_PER_GPU = 128
    NUM_EPOCHS = 60
    HEIGHT = 32
    WIDTH = 32
    NUM_CHANNELS = 3
    NUM_CLASSES = 10
    NUM_TRAIN_SAMPLES = 50000
    BASE_LEARNING_RATE = 0.1
    
  4. 为了准备数据集,让我们实现两个快速的函数,用于规范化和增强输入图像:

    def normalize(x, y):
        x = tf.image.per_image_standardization(x)
        return x, y
    def augmentation(x, y):
        x = tf.image.resize_with_crop_or_pad(x, HEIGHT + 8, 
                                             WIDTH + 8)
        x = tf.image.random_crop(x, [HEIGHT, WIDTH, 
                                     NUM_CHANNELS])
        x = tf.image.random_flip_left_right(x)
        return x, y
    
  5. 为了简化操作并加快收敛速度,我们将继续使用 CIFAR10 数据集,这是官方 TensorFlow 2.x 示例中用于训练的,但在您探索时可以自由选择其他数据集。一旦选择了数据集,我们就可以生成训练集和测试集:

    (x, y), (x_test, y_test) = \
          keras.datasets.cifar10.load_data()
    train_dataset = tf.data.Dataset.from_tensor_slices((x,y))
    test_dataset = \
        tf.data.Dataset.from_tensor_slices((x_test, y_test))
    
  6. 为了使训练结果可重现,我们将使用固定的随机种子来打乱数据集:

    tf.random.set_seed(22)
    
  7. 我们还没有准备好生成训练和验证/测试数据集。我们将使用前一步中声明的已知固定随机种子来打乱数据集,并对训练集应用数据增强:

    train_dataset = (
        train_dataset.map(augmentation)
        .map(normalize)
        .shuffle(NUM_TRAIN_SAMPLES)
        .batch(BS_PER_GPU * NUM_GPUS, drop_remainder=True)
    )
    
  8. 同样,我们将准备测试数据集,但我们不希望对测试图像进行随机裁剪!因此,我们将跳过数据增强,并使用标准化步骤进行预处理:

    test_dataset = test_dataset.map(normalize).batch(
        BS_PER_GPU * NUM_GPUS, drop_remainder=True
    )
    
  9. 在我们开始训练之前,我们需要创建一个优化器实例,并准备好输入层。根据任务的需要,您可以使用不同的优化器,例如 Adam:

    opt = keras.optimizers.SGD(learning_rate=0.1, 
                               momentum=0.9)
    input_shape = (HEIGHT, WIDTH, NUM_CHANNELS)
    img_input = tf.keras.layers.Input(shape=input_shape)
    
  10. 最后,我们准备在 MultiMachineMirroredStrategy 的作用域内构建模型实例:

    with strategy.scope():
        model = resnet.resnet56(img_input=img_input, 
                                classes=NUM_CLASSES)
        model.compile(
            optimizer=opt,
            loss="sparse_categorical_crossentropy",
            metrics=["sparse_categorical_accuracy"],
        )
    
  11. 为了训练模型,我们使用简单而强大的 Keras API:

    model.fit(train_dataset, epochs=NUM_EPOCHS)
    
  12. 一旦模型训练完成,我们可以轻松地保存、加载和评估:

    12.1 保存

    model.save(path, save_format="tf")
    # 12.2 Load
    loaded_model = tf.keras.models.load_model(path)
    loaded_model.compile(
        loss=tf.keras.losses.\
            SparseCategoricalCrossentropy(from_logits=True),
        optimizer=tf.keras.optimizers.Adam(),
        metrics=["accuracy"],
    )
    # 12.3 Evaluate
    eval_loss, eval_acc = loaded_model.evaluate(eval_dataset)
    

这完成了我们的教程实现!让我们在下一部分总结我们实现了什么以及它是如何工作的。

它是如何工作的...

对于使用 TensorFlow 2.x 的任何分布式训练,需要在集群中每一台(虚拟)机器上设置 TF_CONFIG 环境变量。这些配置值将告知每台机器关于角色和每个节点执行任务所需的训练信息。您可以在这里阅读更多关于 TensorFlow 2.x 分布式训练中使用的TF_CONFIG配置的详细信息:cloud.google.com/ai-platform/training/docs/distributed-training-details

我们使用了 TensorFlow 2.x 的 MultiWorkerMirroredStrategy,这是一种与本章前面教程中使用的 Mirrored Strategy 类似的策略。这种策略适用于跨机器的同步训练,每台机器可能拥有一个或多个 GPU。所有训练模型所需的变量和计算都会在每个工作节点上进行复制,就像 Mirrored Strategy 一样,并且使用分布式收集例程(如 all-reduce)来汇总来自多个分布式节点的结果。训练、保存模型、加载模型和评估模型的其余工作流程与我们之前的教程相同。

准备好下一个教程了吗?让我们开始吧。

大规模训练深度强化学习代理 – 多 GPU PPO 代理

一般来说,RL 代理需要大量的样本和梯度步骤来进行训练,这取决于状态、动作和问题空间的复杂性。随着深度强化学习(Deep RL)的发展,计算复杂度也会急剧增加,因为代理使用的深度神经网络(无论是用于 Q 值函数表示,策略表示,还是两者都有)有更多的操作和参数需要分别执行和更新。为了加速训练过程,我们需要能够扩展我们的深度 RL 代理训练,以利用可用的计算资源,如 GPU。这个食谱将帮助你利用多个 GPU,以分布式的方式训练一个使用深度卷积神经网络策略的 PPO 代理,在使用OpenAI 的 procgen库的程序生成的 RL 环境中进行训练。

让我们开始吧!

准备工作

要完成这个食谱,首先你需要激活tf2rl-cookbook Python/conda 虚拟环境。确保更新环境,以匹配食谱代码库中的最新 conda 环境规格文件(tfrl-cookbook.yml)。虽然不是必需的,但建议使用具有两个或更多 GPU 的机器来执行此食谱。

现在,让我们开始吧!

如何做...

我们将实现一个完整的食谱,允许以分布式方式配置训练 PPO 代理,并使用深度卷积神经网络策略。让我们一步一步地开始实现:

  1. 我们将从导入实现这一食谱所需的模块开始:

    import argparse
    import os
    from datetime import datetime
    import gym
    import gym.wrappers
    import numpy as np
    import tensorflow as tf
    from tensorflow.keras.layers import (
        Conv2D,
        Dense,
        Dropout,
        Flatten,
        Input,
        MaxPool2D,
    )
    
  2. 我们将使用 OpenAI 的procgen环境。让我们也导入它:

    import procgen  # Import & register procgen Gym envs
    
  3. 为了使这个食谱更易于配置和运行,让我们添加对命令行参数的支持,并配置一些有用的配置标志:

    parser = argparse.ArgumentParser(prog="TFRL-Cookbook-Ch9-Distributed-RL-Agent")
    parser.add_argument("--env", default="procgen:procgen-coinrun-v0")
    parser.add_argument("--update-freq", type=int, default=16)
    parser.add_argument("--epochs", type=int, default=3)
    parser.add_argument("--actor-lr", type=float, default=1e-4)
    parser.add_argument("--critic-lr", type=float, default=1e-4)
    parser.add_argument("--clip-ratio", type=float, default=0.1)
    parser.add_argument("--gae-lambda", type=float, default=0.95)
    parser.add_argument("--gamma", type=float, default=0.99)
    parser.add_argument("--logdir", default="logs")
    args = parser.parse_args()
    
  4. 让我们使用 TensorBoard 摘要写入器进行日志记录:

    logdir = os.path.join(
        args.logdir, parser.prog, args.env, \
        datetime.now().strftime("%Y%m%d-%H%M%S")
    )
    print(f"Saving training logs to:{logdir}")
    writer = tf.summary.create_file_writer(logdir)
    
  5. 我们将首先在以下几个步骤中实现Actor类,从__init__方法开始。你会注意到我们需要在执行策略的上下文中实例化模型:

    class Actor:
        def __init__(self, state_dim, action_dim, 
        execution_strategy):
            self.state_dim = state_dim
            self.action_dim = action_dim
            self.execution_strategy = execution_strategy
            with self.execution_strategy.scope():
                self.weight_initializer = \
                    tf.keras.initializers.he_normal()
                self.model = self.nn_model()
                self.model.summary()  # Print a summary of
                # the Actor model
                self.opt = \
                    tf.keras.optimizers.Nadam(args.actor_lr)
    
  6. 对于 Actor 的策略网络模型,我们将实现一个包含多个Conv2DMaxPool2D层的深度卷积神经网络。在这一步我们将开始实现,接下来的几步将完成它:

        def nn_model(self):
            obs_input = Input(self.state_dim)
            conv1 = Conv2D(
                filters=64,
                kernel_size=(3, 3),
                strides=(1, 1),
                padding="same",
                input_shape=self.state_dim,
                data_format="channels_last",
                activation="relu",
            )(obs_input)
            pool1 = MaxPool2D(pool_size=(3, 3), \
                              strides=1)(conv1)
    
  7. 我们将添加更多的 Conv2D - Pool2D 层,以根据任务的需求堆叠处理层。在这个食谱中,我们将为 procgen 环境训练策略,该环境在视觉上较为丰富,因此我们将堆叠更多的层:

           conv2 = Conv2D(
                filters=32,
                kernel_size=(3, 3),
                strides=(1, 1),
                padding="valid",
                activation="relu",
            )(pool1)
            pool2 = MaxPool2D(pool_size=(3, 3), strides=1)\
                        (conv2)
            conv3 = Conv2D(
                filters=16,
                kernel_size=(3, 3),
                strides=(1, 1),
                padding="valid",
                activation="relu",
            )(pool2)
            pool3 = MaxPool2D(pool_size=(3, 3), strides=1)\
                        (conv3)
            conv4 = Conv2D(
                filters=8,
                kernel_size=(3, 3),
                strides=(1, 1),
                padding="valid",
                activation="relu",
            )(pool3)
            pool4 = MaxPool2D(pool_size=(3, 3), strides=1)\
                        (conv4)
    
  8. 现在,我们可以使用一个扁平化层,并为策略网络准备输出头:

           flat = Flatten()(pool4)
            dense1 = Dense(
                16, activation="relu", \
                   kernel_initializer=self.weight_initializer
            )(flat)
            dropout1 = Dropout(0.3)(dense1)
            dense2 = Dense(
                8, activation="relu", \
                   kernel_initializer=self.weight_initializer
            )(dropout1)
            dropout2 = Dropout(0.3)(dense2)
    
  9. 作为构建策略网络神经模型的最后一步,我们将创建输出层并返回一个 Keras 模型:

            output_discrete_action = Dense(
                self.action_dim,
                activation="softmax",
                kernel_initializer=self.weight_initializer,
            )(dropout2)
            return tf.keras.models.Model(
                inputs=obs_input, 
                outputs = output_discrete_action, 
                name="Actor")
    
  10. 使用我们在前面步骤中定义的模型,我们可以开始处理状态/观察图像输入,并生成 logits(未归一化的概率)以及 Actor 将采取的动作。让我们实现一个方法来完成这个任务:

        def get_action(self, state):
            # Convert [Image] to np.array(np.adarray)
            state_np = np.array([np.array(s) for s in state])
            if len(state_np.shape) == 3:
                # Convert (w, h, c) to (1, w, h, c)
                state_np = np.expand_dims(state_np, 0)
            logits = self.model.predict(state_np)  
            # shape: (batch_size, self.action_dim)
            action = np.random.choice(self.action_dim, 
                                      p=logits[0])
            # 1 Action per instance of env; Env expects:
            # (num_instances, actions)
            # action = (action,)
            return logits, action
    
  11. 接下来,为了计算驱动学习的替代损失,我们将实现compute_loss方法:

        def compute_loss(self, old_policy, new_policy, 
        actions, gaes):
            log_old_policy = tf.math.log(tf.reduce_sum(
                                       old_policy * actions))
            log_old_policy = tf.stop_gradient(log_old_policy)
            log_new_policy = tf.math.log(tf.reduce_sum(
                                       new_policy * actions))
            # Avoid INF in exp by setting 80 as the upper 
            # bound since,
            # tf.exp(x) for x>88 yeilds NaN (float32)
            ratio = tf.exp(
                tf.minimum(log_new_policy - \
                           tf.stop_gradient(log_old_policy),\
                           80)
            )
            clipped_ratio = tf.clip_by_value(
                ratio, 1.0 - args.clip_ratio, 1.0 + \
                args.clip_ratio
            )
            gaes = tf.stop_gradient(gaes)
            surrogate = -tf.minimum(ratio * gaes, \
                                    clipped_ratio * gaes)
            return tf.reduce_mean(surrogate)
    
  12. 接下来是一个核心方法,它将所有方法连接在一起以执行训练。请注意,这是每个副本的训练方法,我们将在后续的分布式训练方法中使用它:

        def train(self, old_policy, states, actions, gaes):
            actions = tf.one_hot(actions, self.action_dim)  
            # One-hot encoding
            actions = tf.reshape(actions, [-1, \
                                 self.action_dim])  
            # Add batch dimension
            actions = tf.cast(actions, tf.float64)
            with tf.GradientTape() as tape:
                logits = self.model(states, training=True)
                loss = self.compute_loss(old_policy, logits, 
                                         actions, gaes)
            grads = tape.gradient(loss, 
                              self.model.trainable_variables)
            self.opt.apply_gradients(zip(grads, 
                             self.model.trainable_variables))
            return loss
    
  13. 为了实现分布式训练方法,我们将使用tf.function装饰器来实现一个 TensorFlow 2.x 函数:

        @tf.function
        def train_distributed(self, old_policy, states,
                              actions, gaes):
            per_replica_losses = self.execution_strategy.run(
                self.train, args=(old_policy, states, 
                                  actions, gaes))
            return self.execution_strategy.reduce(
                tf.distribute.ReduceOp.SUM, \
                    per_replica_losses, axis=None)
    
  14. 这就完成了我们的Actor类实现,接下来我们将开始实现Critic类:

    class Critic:
        def __init__(self, state_dim, execution_strategy):
            self.state_dim = state_dim
            self.execution_strategy = execution_strategy
            with self.execution_strategy.scope():
                self.weight_initializer = \
                    tf.keras.initializers.he_normal()
                self.model = self.nn_model()
                self.model.summary()  
                # Print a summary of the Critic model
                self.opt = \
                    tf.keras.optimizers.Nadam(args.critic_lr)
    
  15. 你一定注意到,我们在执行策略的作用域内创建了 Critic 的价值函数模型实例,以支持分布式训练。接下来,我们将开始在以下几个步骤中实现 Critic 的神经网络模型:

        def nn_model(self):
            obs_input = Input(self.state_dim)
            conv1 = Conv2D(
                filters=64,
                kernel_size=(3, 3),
                strides=(1, 1),
                padding="same",
                input_shape=self.state_dim,
                data_format="channels_last",
                activation="relu",
            )(obs_input)
            pool1 = MaxPool2D(pool_size=(3, 3), strides=2)\
                        (conv1)
    
  16. 与我们的 Actor 模型类似,我们将有类似的 Conv2D-MaxPool2D 层的堆叠,后面跟着带有丢弃的扁平化层:

            conv2 = Conv2D(filters=32, kernel_size=(3, 3),
                strides=(1, 1),
                padding="valid", activation="relu",)(pool1)
            pool2 = MaxPool2D(pool_size=(3, 3), strides=2)\
                        (conv2)
            conv3 = Conv2D(filters=16,
                kernel_size=(3, 3), strides=(1, 1),
                padding="valid", activation="relu",)(pool2)
            pool3 = MaxPool2D(pool_size=(3, 3), strides=1)\
                        (conv3)
            conv4 = Conv2D(filters=8, kernel_size=(3, 3),
                strides=(1, 1), padding="valid",
                activation="relu",)(pool3)
            pool4 = MaxPool2D(pool_size=(3, 3), strides=1)\
                        (conv4)
            flat = Flatten()(pool4)
            dense1 = Dense(16, activation="relu", 
                           kernel_initializer =\
                               self.weight_initializer)\
                           (flat)
            dropout1 = Dropout(0.3)(dense1)
            dense2 = Dense(8, activation="relu", 
                           kernel_initializer = \
                               self.weight_initializer)\
                           (dropout1)
            dropout2 = Dropout(0.3)(dense2)
    
  17. 我们将添加值输出头,并将模型作为 Keras 模型返回,以完成我们 Critic 的神经网络模型:

            value = Dense(
                1, activation="linear", 
                kernel_initializer=self.weight_initializer)\
                (dropout2)
            return tf.keras.models.Model(inputs=obs_input, \
                                         outputs=value, \
                                         name="Critic")
    
  18. 如你所记得,Critic 的损失是预测的时间差目标与实际时间差目标之间的均方误差。让我们实现一个计算损失的方法:

        def compute_loss(self, v_pred, td_targets):
            mse = tf.keras.losses.MeanSquaredError(
                     reduction=tf.keras.losses.Reduction.SUM)
            return mse(td_targets, v_pred)
    
  19. 与我们的 Actor 实现类似,我们将实现一个每个副本的train方法,然后在后续步骤中用于分布式训练:

        def train(self, states, td_targets):
            with tf.GradientTape() as tape:
                v_pred = self.model(states, training=True)
                # assert v_pred.shape == td_targets.shape
                loss = self.compute_loss(v_pred, \
                               tf.stop_gradient(td_targets))
            grads = tape.gradient(loss, \
                           self.model.trainable_variables)
            self.opt.apply_gradients(zip(grads, \
                           self.model.trainable_variables))
            return loss
    
  20. 我们将通过实现train_distributed方法来完成Critic类的实现,该方法支持分布式训练:

        @tf.function
        def train_distributed(self, states, td_targets):
            per_replica_losses = self.execution_strategy.run(
                self.train, args=(states, td_targets)
            )
            return self.execution_strategy.reduce(
                tf.distribute.ReduceOp.SUM, \
                per_replica_losses, axis=None
            )
    
  21. 在实现了我们的ActorCritic类后,我们可以开始我们的分布式PPOAgent实现。我们将分几个步骤实现PPOAgent类。让我们从__init__方法开始:

    class PPOAgent:
        def __init__(self, env):
            """Distributed PPO Agent for image observations 
            and discrete action-space Gym envs
            Args:
                env (gym.Env): OpenAI Gym I/O compatible RL 
                environment with discrete action space
            """
            self.env = env
            self.state_dim = self.env.observation_space.shape
            self.action_dim = self.env.action_space.n
            # Create a Distributed execution strategy
            self.distributed_execution_strategy = \
                         tf.distribute.MirroredStrategy()
            print(f"Number of devices: {self.\
                    distributed_execution_strategy.\
                    num_replicas_in_sync}")
            # Create Actor & Critic networks under the 
            # distributed execution strategy scope
            with self.distributed_execution_strategy.scope():
                self.actor = Actor(self.state_dim, 
                                self.action_dim, 
                                tf.distribute.get_strategy())
                self.critic = Critic(self.state_dim, 
                                tf.distribute.get_strategy())
    
  22. 接下来,我们将实现一个方法来计算广义优势估计GAE)的目标:

        def gae_target(self, rewards, v_values, next_v_value,
        done):
            n_step_targets = np.zeros_like(rewards)
            gae = np.zeros_like(rewards)
            gae_cumulative = 0
            forward_val = 0
            if not done:
                forward_val = next_v_value
            for k in reversed(range(0, len(rewards))):
                delta = rewards[k] + args.gamma * \
                  forward_val - v_values[k]
                gae_cumulative = args.gamma * \
                  args.gae_lambda * gae_cumulative + delta
                gae[k] = gae_cumulative
                forward_val = v_values[k]
                n_step_targets[k] = gae[k] + v_values[k]
            return gae, n_step_targets
    
  23. 我们已经准备好开始我们的train(…)方法。我们将把这个方法的实现分为以下几个步骤。让我们设置作用域,开始外循环,并初始化变量:

        def train(self, max_episodes=1000):
            with self.distributed_execution_strategy.scope():
                with writer.as_default():
                    for ep in range(max_episodes):
                        state_batch = []
                        action_batch = []
                        reward_batch = []
                        old_policy_batch = []
                        episode_reward, done = 0, False
                        state = self.env.reset()
                        prev_state = state
                        step_num = 0
    
  24. 现在,我们可以开始为每个回合执行的循环,直到回合结束:

                          while not done:
                            self.env.render()
                            logits, action = \
                                 self.actor.get_action(state)
                            next_state, reward, dones, _ = \
                                        self.env.step(action)
                            step_num += 1
                            print(f"ep#:{ep} step#:{step_num} 
                                    step_rew:{reward} \
                                    action:{action} \
                                    dones:{dones}",end="\r",)
                            done = np.all(dones)
                            if done:
                                next_state = prev_state
                            else:
                                prev_state = next_state
                            state_batch.append(state)
                            action_batch.append(action)
                            reward_batch.append(
                                            (reward + 8) / 8)
                            old_policy_batch.append(logits)  
    
  25. 在每个回合内,如果我们达到了update_freq或者刚刚到达了结束状态,我们需要计算 GAE 和 TD 目标。让我们添加相应的代码:

                             if len(state_batch) >= \
                             args.update_freq or done:
                                states = np.array(
                                    [state.squeeze() for \
                                     state in state_batch])
                                actions = \
                                    np.array(action_batch)
                                rewards = \
                                    np.array(reward_batch)
                                old_policies = np.array(
                                    [old_pi.squeeze() for \
                                 old_pi in old_policy_batch])
                                v_values = self.critic.\
                                        model.predict(states)
                                next_v_value = self.critic.\
                                   model.predict(
                                       np.expand_dims(
                                           next_state, 0))
                                gaes, td_targets = \
                                     self.gae_target(
                                         rewards, v_values,
                                         next_v_value, done)
                                actor_losses, critic_losses=\
                                                       [], []   
    
  26. 在相同的执行上下文中,我们需要训练ActorCritic

                                   for epoch in range(args.\
                                   epochs):
                                    actor_loss = self.actor.\
                                      train_distributed(
                                         old_policies,
                                         states, actions,
                                         gaes)
                                    actor_losses.\
                                      append(actor_loss)
                                    critic_loss = self.\
                                    critic.train_distributed(
                                       states, td_targets)
                                    critic_losses.\
                                       append(critic_loss)
                                # Plot mean actor & critic 
                                # losses on every update
                                tf.summary.scalar(
                                    "actor_loss", 
                                     np.mean(actor_losses), 
                                     step=ep)
                                tf.summary.scalar(
                                     "critic_loss", 
                                      np.mean(critic_losses), 
                                      step=ep) 
    
  27. 最后,我们需要重置跟踪变量并更新我们的回合奖励值:

    
                                state_batch = []
                                action_batch = []
                                reward_batch = []
                                old_policy_batch = []
                            episode_reward += reward
                            state = next_state 
    
  28. 这样,我们的分布式main方法就完成了,来完成我们的配方:

    if __name__ == "__main__":
        env_name = "procgen:procgen-coinrun-v0"
        env = gym.make(env_name, render_mode="rgb_array")
        env = gym.wrappers.Monitor(env=env, 
                            directory="./videos", force=True)
        agent = PPOAgent(env)
        agent.train()
    

    配方完成了!希望你喜欢这个过程。你可以执行这个配方,并通过 TensorBoard 日志观看进度,以查看你在更多 GPU 的支持下获得的训练加速效果!

让我们回顾一下我们完成的工作以及配方如何工作的下一部分。

它是如何工作的...

我们实现了ActorCritic类,其中 Actor 使用深度卷积神经网络表示策略,而 Critic 则使用类似的深度卷积神经网络表示其价值函数。这两个模型都在分布式执行策略的范围内实例化,使用了self.execution_strategy.scope()构造方法。

procgen 环境(如 coinrun、fruitbot、jumper、leaper、maze 等)是视觉上(相对)丰富的环境,因此需要较深的卷积层来处理视觉观察。因此,我们为 Actor 的策略网络使用了深度 CNN 模型。为了在多个 GPU 上使用多个副本进行分布式训练,我们首先实现了单副本训练方法(train),然后使用Tensorflow.function在副本间运行,并将结果进行汇总得到总损失。

最后,在分布式环境中训练我们的 PPO 智能体时,我们通过使用 Python 的with语句进行上下文管理,将所有训练操作都纳入分布式执行策略的范围,例如:with self.distributed_execution_strategy.scope()

该是进行下一个配方的时候了!

用于加速训练的分布式深度强化学习基础模块

本章之前的配方讨论了如何使用 TensorFlow 2.x 的分布式执行 API 来扩展深度强化学习训练。理解了这些概念和实现风格后,训练使用更高级架构(如 Impala 和 R2D2)的深度强化学习智能体,需要像分布式参数服务器和分布式经验回放这样的 RL 基础模块。本章将演示如何为分布式 RL 训练实现这些基础模块。我们将使用 Ray 分布式计算框架来实现我们的基础模块。

让我们开始吧!

准备工作

要完成这个配方,首先需要激活tf2rl-cookbook的 Python/conda 虚拟环境。确保更新环境以匹配食谱代码仓库中的最新 conda 环境规范文件(tfrl-cookbook.yml)。为了测试我们在这个配方中构建的基础模块,我们将使用基于书中早期配方实现的 SAC 智能体的self.sac_agent_base模块。如果以下import语句能正常运行,那么你准备开始了:

import pickle
import sys
import fire
import gym
import numpy as np
import ray
if "." not in sys.path:
    sys.path.insert(0, ".")
from sac_agent_base import SAC

现在,让我们开始吧!

如何实现...

我们将逐个实现这些基础模块,从分布式参数服务器开始:

  1. ParameterServer类是一个简单的存储类,用于在分布式训练环境中共享神经网络的参数或权重。我们将实现这个类作为 Ray 的远程 Actor:

    @ray.remote
    class ParameterServer(object):
        def __init__(self, weights):
            values = [value.copy() for value in weights]
            self.weights = values
        def push(self, weights):
            values = [value.copy() for value in weights]
            self.weights = values
        def pull(self):
            return self.weights
        def get_weights(self):
            return self.weights
    
  2. 我们还将添加一个方法将权重保存到磁盘:

        # save weights to disk
        def save_weights(self, name):
            with open(name + "weights.pkl", "wb") as pkl:
                pickle.dump(self.weights, pkl)
            print(f"Weights saved to {name + 
                                      ‘weights.pkl’}.")
    
  3. 作为下一个构建块,我们将实现ReplayBuffer,它可以被分布式代理集群使用。我们将在这一步开始实现,并在接下来的几步中继续:

    @ray.remote
    class ReplayBuffer:
        """
        A simple FIFO experience replay buffer for RL Agents
        """
        def __init__(self, obs_shape, action_shape, size):
            self.cur_states = np.zeros([size, obs_shape[0]],
                                        dtype=np.float32)
            self.actions = np.zeros([size, action_shape[0]],
                                     dtype=np.float32)
            self.rewards = np.zeros(size, dtype=np.float32)
            self.next_states = np.zeros([size, obs_shape[0]],
                                         dtype=np.float32)
            self.dones = np.zeros(size, dtype=np.float32)
            self.idx, self.size, self.max_size = 0, 0, size
            self.rollout_steps = 0
    
  4. 接下来,我们将实现一个方法,将新经验存储到重放缓冲区:

        def store(self, obs, act, rew, next_obs, done):
            self.cur_states[self.idx] = np.squeeze(obs)
            self.actions[self.idx] = np.squeeze(act)
            self.rewards[self.idx] = np.squeeze(rew)
            self.next_states[self.idx] = np.squeeze(next_obs)
            self.dones[self.idx] = done
            self.idx = (self.idx + 1) % self.max_size
            self.size = min(self.size + 1, self.max_size)
            self.rollout_steps += 1
    
  5. 为了从重放缓冲区采样一批经验数据,我们将实现一个方法,从重放缓冲区随机采样并返回一个包含采样经验数据的字典:

        def sample_batch(self, batch_size=32):
            idxs = np.random.randint(0, self.size, 
                                     size=batch_size)
            return dict(
                cur_states=self.cur_states[idxs],
                actions=self.actions[idxs],
                rewards=self.rewards[idxs],
                next_states=self.next_states[idxs],
                dones=self.dones[idxs])
    
  6. 这完成了我们的ReplayBuffer类的实现。现在我们将开始实现一个方法来进行rollout,该方法本质上是使用从分布式参数服务器对象中提取的参数和探索策略在 RL 环境中收集经验,并将收集到的经验存储到分布式重放缓冲区中。我们将在这一步开始实现,并在接下来的步骤中完成rollout方法的实现:

    @ray.remote
    def rollout(ps, replay_buffer, config):
        """Collect experience using an exploration policy"""
        env = gym.make(config["env"])
        obs, reward, done, ep_ret, ep_len = env.reset(), 0, \
                                              False, 0, 0
        total_steps = config["steps_per_epoch"] * \
                       config["epochs"]
        agent = SAC(env.observation_space.shape, \
                    env.action_space)
        weights = ray.get(ps.pull.remote())
        target_weights = agent.actor.get_weights()
        for i in range(len(target_weights)):  
        # set tau% of target model to be new weights
            target_weights[i] = weights[i]
        agent.actor.set_weights(target_weights)
    
  7. 在代理初始化并加载完毕,环境实例也准备好后,我们可以开始我们的经验收集循环:

        for step in range(total_steps):
            if step > config["random_exploration_steps"]:
                # Use Agent’s policy for exploration after 
                `random_exploration_steps`
                a = agent.act(obs)
            else:  # Use a uniform random exploration policy
                a = env.action_space.sample()
            next_obs, reward, done, _ = env.step(a)
            print(f"Step#:{step} reward:{reward} \
                    done:{done}")
            ep_ret += reward
            ep_len += 1
    
  8. 让我们处理max_ep_len配置的情况,以指示回合的最大长度,然后将收集的经验存储到分布式重放缓冲区中:

            done = False if ep_len == config["max_ep_len"]\
                     else done
            # Store experience to replay buffer
            replay_buffer.store.remote(obs, a, reward, 
                                       next_obs, done)
    
  9. 最后,在回合结束时,使用参数服务器同步行为策略的权重:

            obs = next_obs
            if done or (ep_len == config["max_ep_len"]):
                """
                Perform parameter sync at the end of the 
                trajectory.
                """
                obs, reward, done, ep_ret, ep_len = \
                                 env.reset(), 0, False, 0, 0
                weights = ray.get(ps.pull.remote())
                agent.actor.set_weights(weights)
    
  10. 这完成了rollout方法的实现,我们现在可以实现一个运行训练循环的train方法:

    @ray.remote(num_gpus=1, max_calls=1)
    def train(ps, replay_buffer, config):
        agent = SAC(config["obs_shape"], \
                    config["action_space"])
        weights = ray.get(ps.pull.remote())
        agent.actor.set_weights(weights)
        train_step = 1
        while True:
            agent.train_with_distributed_replay_memory(
                ray.get(replay_buffer.sample_batch.remote())
            )
            if train_step % config["worker_update_freq"]== 0:
                weights = agent.actor.get_weights()
                ps.push.remote(weights)
            train_step += 1
    
  11. 我们的配方中的最后一个模块是main函数,它将迄今为止构建的所有模块整合起来并执行。我们将在这一步开始实现,并在剩下的步骤中完成。让我们从main函数的参数列表开始,并将参数捕获到配置字典中:

    def main(
        env="MountainCarContinuous-v0",
        epochs=1000,
        steps_per_epoch=5000,
        replay_size=100000,
        random_exploration_steps=1000,
        max_ep_len=1000,
        num_workers=4,
        num_learners=1,
        worker_update_freq=500,
    ):
        config = {
            "env": env,
            "epochs": epochs,
            "steps_per_epoch": steps_per_epoch,
            "max_ep_len": max_ep_len,
            "replay_size": replay_size,
            "random_exploration_steps": \
                 random_exploration_steps,
            "num_workers": num_workers,
            "num_learners": num_learners,
            "worker_update_freq": worker_update_freq,
        }
    
  12. 接下来,创建一个所需环境的实例,获取状态和观察空间,初始化 ray,并初始化一个随机策略-演员-评论家(Stochastic Actor-Critic)代理。注意,我们初始化的是一个单节点的 ray 集群,但你也可以使用节点集群(本地或云端)来初始化 ray:

        env = gym.make(config["env"])
        config["obs_shape"] = env.observation_space.shape
        config["action_space"] = env.action_space
        ray.init()
        agent = SAC(config["obs_shape"], \
                    config["action_space"])
    
  13. 在这一步,我们将初始化ParameterServer类的实例和ReplayBuffer类的实例:

        params_server = \
            ParameterServer.remote(agent.actor.get_weights())
        replay_buffer = ReplayBuffer.remote(
            config["obs_shape"], \
            config["action_space"].shape, \
            config["replay_size"]
        )
    
  14. 我们现在准备好运行已构建的模块了。我们将首先根据配置参数中指定的工作者数量,启动一系列rollout任务,这些任务将在分布式 ray 集群上启动rollout过程:

        task_rollout = [
            rollout.remote(params_server, replay_buffer, 
                           config)
            for i in range(config["num_workers"])
        ]
    

    rollout任务将启动远程任务,这些任务将使用收集到的经验填充重放缓冲区。上述代码将立即返回,即使rollout任务需要时间来完成,因为它是异步函数调用。

  15. 接下来,我们将启动一个可配置数量的学习者,在 ray 集群上运行分布式训练任务:

        task_train = [
            train.remote(params_server, replay_buffer, 
                         config)
            for i in range(config["num_learners"])
        ]
    

    上述语句将启动远程训练过程,并立即返回,尽管train函数在学习者上需要一定时间来完成。

    We will wait for the tasks to complete on the main thread before exiting:
        ray.wait(task_rollout)
        ray.wait(task_train)
    
  16. 最后,让我们定义我们的入口点。我们将使用 Python Fire 库来暴露我们的main函数,并使其参数看起来像是一个支持命令行参数的可执行文件:

    if __name__ == "__main__":
        fire.Fire(main)
    

    使用前述的入口点,脚本可以从命令行配置并启动。这里提供一个示例供你参考:

    (tfrl-cookbook)praveen@dev-cluster:~/tfrl-cookbook$python 4_building_blocks_for_distributed_rl_using_ray.py main --env="MountaincarContinuous-v0" --num_workers=8 --num_learners=3
    

这就完成了我们的实现!让我们在下一节简要讨论它的工作原理。

它是如何工作的……

我们构建了一个分布式的ParameterServerReplayBuffer、rollout worker 和 learner 进程。这些构建模块对于训练分布式 RL 代理至关重要。我们使用 Ray 作为分布式计算框架。

在实现了构建模块和任务后,在main函数中,我们在 Ray 集群上启动了两个异步的分布式任务。task_rollout启动了(可配置数量的)rollout worker,而task_train启动了(可配置数量的)learner。两个任务都以分布式方式异步运行在 Ray 集群上。rollout workers 从参数服务器拉取最新的权重,并将经验收集并存储到重放内存缓冲区中,同时,learners 使用从重放内存中采样的经验批次进行训练,并将更新(且可能改进的)参数集推送到参数服务器。

是时候进入本章的下一个,也是最后一个教程了!

使用 Ray、Tune 和 RLLib 进行大规模深度强化学习(Deep RL)代理训练

在之前的教程中,我们初步了解了如何从头实现分布式 RL 代理训练流程。由于大多数用作构建模块的组件已成为构建深度强化学习训练基础设施的标准方式,我们可以利用一个现有的库,该库维护了这些构建模块的高质量实现。幸运的是,选择 Ray 作为分布式计算框架使我们处于一个有利位置。Tune 和 RLLib 是基于 Ray 构建的两个库,并与 Ray 一起提供,提供高度可扩展的超参数调优(Tune)和 RL 训练(RLLib)。本教程将提供一套精选步骤,帮助你熟悉 Ray、Tune 和 RLLib,从而能够利用它们来扩展你的深度 RL 训练流程。除了文中讨论的教程外,本章的代码仓库中还有一系列额外的教程供你参考。

让我们开始吧!

准备工作

要完成这个教程,你首先需要激活tf2rl-cookbook的 Python/conda 虚拟环境。确保更新环境以匹配最新的 conda 环境规范文件(tfrl-cookbook.yml),该文件位于教程代码仓库中。当你使用提供的 conda YAML 规范来设置环境时,Ray、Tune 和 RLLib 将会被安装在你的tf2rl-cookbook conda 环境中。如果你希望在其他环境中安装 Tune 和 RLLib,最简单的方法是使用以下命令安装:

 pip install ray[tune,rllib]

现在,开始吧!

如何实现……

我们将从快速和基本的命令与食谱开始,使用 Tune 和 RLLib 在 ray 集群上启动训练,并逐步自定义训练流水线,以为你提供有用的食谱:

  1. 在 OpenAI Gym 环境中启动 RL 代理的典型训练和指定算法名称和环境名称一样简单。例如,要在 CartPole-v4 Gym 环境中训练 PPO 代理,你只需要执行以下命令:

    --eager flag is also specified, which forces RLLib to use eager execution (the default mode of execution in TensorFlow 2.x).
    
  2. 让我们尝试在coinrunprocgen环境中训练一个 PPO 代理,就像我们之前的一个食谱一样:

    (tfrl-cookbook) praveen@dev-cluster:~/tfrl-cookbook$rllib train --run PPO --env "procgen:procgen-coinrun-v0" --eager
    

    你会注意到,前面的命令会失败,并给出以下(简化的)错误:

        ValueError: No default configuration for obs shape [64, 64, 3], you must specify `conv_filters` manually as a model option. Default configurations are only available for inputs of shape [42, 42, K] and [84, 84, K]. You may alternatively want to use a custom model or preprocessor.
    

    这是因为,如错误所示,RLLib 默认支持形状为(42,42,k)或(84,84,k)的观察值。其他形状的观察值将需要自定义模型或预处理器。在接下来的几个步骤中,我们将展示如何实现一个自定义神经网络模型,使用 TensorFlow 2.x Keras API 实现,并且可以与 ray RLLib 一起使用。

  3. 我们将在这一步开始实现自定义模型(custom_model.py),并在接下来的几步中完成它。在这一步,让我们导入必要的模块,并实现一个辅助方法,以返回具有特定滤波深度的 Conv2D 层:

    from ray.rllib.models.tf.tf_modelv2 import TFModelV2
    import tensorflow as tf
    def conv_layer(depth, name):
        return tf.keras.layers.Conv2D(
            filters=depth, kernel_size=3, strides=1, \
            padding="same", name=name
        )
    
  4. 接下来,让我们实现一个辅助方法来构建并返回一个简单的残差块:

    def residual_block(x, depth, prefix):
        inputs = x
        assert inputs.get_shape()[-1].value == depth
        x = tf.keras.layers.ReLU()(x)
        x = conv_layer(depth, name=prefix + "_conv0")(x)
        x = tf.keras.layers.ReLU()(x)
        x = conv_layer(depth, name=prefix + "_conv1")(x)
        return x + inputs
    
  5. 让我们实现另一个方便的函数来构建多个残差块序列:

    def conv_sequence(x, depth, prefix):
        x = conv_layer(depth, prefix + "_conv")(x)
        x = tf.keras.layers.MaxPool2D(pool_size=3, \
                                      strides=2,\
                                      padding="same")(x)
        x = residual_block(x, depth, prefix=prefix + \
                           "_block0")
        x = residual_block(x, depth, prefix=prefix + \
                           "_block1")
        return x
    
  6. 现在,我们可以开始实现CustomModel类,作为 RLLib 提供的 TFModelV2 基类的子类,以便轻松地与 RLLib 集成:

    class CustomModel(TFModelV2):
        """Deep residual network that produces logits for 
           policy and value for value-function;
        Based on architecture used in IMPALA paper:https://
           arxiv.org/abs/1802.01561"""
        def __init__(self, obs_space, action_space, 
        num_outputs, model_config, name):
            super().__init__(obs_space, action_space, \
                             num_outputs, model_config, name)
            depths = [16, 32, 32]
            inputs = tf.keras.layers.Input(
                            shape=obs_space.shape,
                            name="observations")
            scaled_inputs = tf.cast(inputs, 
                                    tf.float32) / 255.0
            x = scaled_inputs
            for i, depth in enumerate(depths):
                x = conv_sequence(x, depth, prefix=f"seq{i}")
            x = tf.keras.layers.Flatten()(x)
            x = tf.keras.layers.ReLU()(x)
            x = tf.keras.layers.Dense(units=256,
                                      activation="relu", 
                                      name="hidden")(x)
            logits = tf.keras.layers.Dense(units=num_outputs,
                                           name="pi")(x)
            value = tf.keras.layers.Dense(units=1, 
                                          name="vf")(x)
            self.base_model = tf.keras.Model(inputs, 
                                            [logits, value])
            self.register_variables(
                                   self.base_model.variables)
    
  7. __init__方法之后,我们需要实现forward方法,因为它没有被基类(TFModelV2)实现,但却是必需的:

        def forward(self, input_dict, state, seq_lens):
            # explicit cast to float32 needed in eager
            obs = tf.cast(input_dict["obs"], tf.float32)
            logits, self._value = self.base_model(obs)
            return logits, state
    
  8. 我们还将实现一个单行方法来重新调整值函数的输出:

        def value_function(self):
            return tf.reshape(self._value, [-1])
    

    这样,我们的CustomModel实现就完成了,并且可以开始使用了!

  9. 我们将实现一个使用 ray、Tune 和 RLLib 的 Python API 的解决方案(5.1_training_using_tune_run.py),这样你就可以在使用它们的命令行工具的同时,也能利用该模型。让我们将实现分为两步。在这一步,我们将导入必要的模块并初始化 ray:

    import ray
    import sys
    from ray import tune
    from ray.rllib.models import ModelCatalog
    if not "." in sys.path:
        sys.path.insert(0, ".")
    from custom_model import CustomModel
    ray.init()  # Can also initialize a cluster with multiple 
    #nodes here using the cluster head node’s IP
    
  10. 在这一步,我们将把我们的自定义模型注册到 RLLib 的ModelCatlog中,然后使用它来训练一个带有自定义参数集的 PPO 代理,其中包括强制 RLLib 使用 TensorFlow 2 的framework参数。我们还将在脚本结束时关闭 ray:

    # Register custom-model in ModelCatalog
    ModelCatalog.register_custom_model("CustomCNN", 
                                        CustomModel)
    experiment_analysis = tune.run(
        "PPO",
        config={
            "env": "procgen:procgen-coinrun-v0",
            "num_gpus": 0,
            "num_workers": 2,
            "model": {"custom_model": "CustomCNN"},
            "framework": "tf2",
            "log_level": "INFO",
        },
        local_dir="ray_results",  # store experiment results
        #  in this dir
    )
    ray.shutdown()
    
  11. 我们将查看另一个快速食谱(5_2_custom_training_using_tune.py)来定制训练循环。我们将把实现分为以下几个步骤,以保持简洁。在这一步,我们将导入必要的库并初始化 ray:

    import sys
    import ray
    import ray.rllib.agents.impala as impala
    from ray.tune.logger import pretty_print
    from ray.rllib.models import ModelCatalog
    if not "." in sys.path:
        sys.path.insert(0, ".")
    from custom_model import CustomModel
    ray.init()  # You can also initialize a multi-node ray 
    # cluster here
    
  12. 现在,让我们将自定义模型注册到 RLLib 的ModelCatalog中,并配置IMPALA 代理。我们当然可以使用任何其他的 RLLib 支持的代理,如 PPO 或 SAC:

    # Register custom-model in ModelCatalog
    ModelCatalog.register_custom_model("CustomCNN", 
                                        CustomModel)
    config = impala.DEFAULT_CONFIG.copy()
    config["num_gpus"] = 0
    config["num_workers"] = 1
    config["model"]["custom_model"] = "CustomCNN"
    config["log_level"] = "INFO"
    config["framework"] = "tf2"
    trainer = impala.ImpalaTrainer(config=config,
                            env="procgen:procgen-coinrun-v0")
    
  13. 现在,我们可以实现自定义训练循环,并根据需要在循环中加入任何步骤。我们将通过每隔 n(100) 代(epochs)执行一次训练步骤并保存代理的模型来保持示例循环的简单性:

    for step in range(1000):
        # Custom training loop
        result = trainer.train()
        print(pretty_print(result))
        if step % 100 == 0:
            checkpoint = trainer.save()
            print("checkpoint saved at", checkpoint
    
  14. 请注意,我们可以继续使用保存的检查点和 Ray tune 的简化 run API 来训练代理,如此处示例所示:

    # Restore agent from a checkpoint and start a new 
    # training run with a different config
    config["lr"] =  ray.tune.grid_search([0.01, 0.001])"]
    ray.tune.run(trainer, config=config, restore=checkpoint)
    
  15. 最后,让我们关闭 Ray 以释放系统资源:

    ray.shutdown()
    

这就完成了本次配方!在下一节中,让我们回顾一下我们在本节中讨论的内容。

它是如何工作的...

我们发现了 Ray RLLib 简单但有限的命令行界面中的一个常见限制。我们还讨论了解决方案,以克服第 2 步中的失败情况,在此过程中需要自定义模型来使用 RLLib 的 PPO 代理训练,并在第 9 步和第 10 步中实现了该方案。

尽管第 9 步和第 10 步中讨论的解决方案看起来很优雅,但它可能无法提供您所需的所有自定义选项或您熟悉的选项。例如,它将基本的 RL 循环抽象了出来,这个循环会遍历环境。我们从第 11 步开始实现了另一种快速方案,允许自定义训练循环。在第 12 步中,我们看到如何注册自定义模型并将其与 IMPALA 代理一起使用——IMPALA 代理是基于 IMPortance 加权 Actor-Learner 架构的可扩展分布式深度强化学习代理。IMPALA 代理的演员通过传递状态、动作和奖励的序列与集中式学习器通信,在学习器中进行批量梯度更新,而与之对比的是基于(异步)Actor-Critic 的代理,其中梯度被传递到一个集中式参数服务器。

如需更多关于 Tune 的信息,可以参考 docs.ray.io/en/master/tune/user-guide.html 上的 Tune 用户指南和配置文档。

如需更多关于 RLLib 训练 API 和配置文档的信息,可以参考 docs.ray.io/en/master/rllib-training.html

本章和配方已完成!希望您通过所获得的新技能和知识,能够加速您的深度 RL 代理训练。下章见!