TensorFlow2-强化学习秘籍-全-
TensorFlow2 强化学习秘籍(全)
原文:
annas-archive.org/md5/ae4f6c3ed954fce75003dcfcae0c4977
译者:飞龙
前言
深度强化学习使得构建智能代理、产品和服务成为可能,这些代理不仅仅局限于计算机视觉或感知,而是能够执行各种操作。TensorFlow 2.x 是最新的主要版本,是最流行的深度学习框架,广泛用于开发和训练深度神经网络(DNNs)。
本书首先介绍了深度强化学习的基础知识以及 TensorFlow 2.x 的最新主要版本。接着,你将学习 OpenAI Gym、基于模型的强化学习和无模型强化学习,并学习如何开发基本的代理。然后,你将发现如何实现高级深度强化学习算法,例如演员-评论家、深度确定性策略梯度、深度 Q 网络、近端策略优化、深度递归 Q 网络和软演员-评论家算法,以训练你的强化学习代理。你还将通过构建加密货币交易代理、股票/股市交易代理和用于自动化任务完成的智能代理来探索强化学习在实际中的应用。最后,你将了解如何将深度强化学习代理部署到云端,并使用 TensorFlow 2.x 构建适用于 Web、移动和其他平台的跨平台应用。
本书结束时,你将通过易于遵循的简洁实现,掌握深度强化学习算法的核心概念,并能够使用 TensorFlow 2.x 从头开始进行实现。
本书的读者对象
本书适用于机器学习应用开发人员、人工智能和应用人工智能研究人员、数据科学家、深度学习从业者,以及具有强化学习基础知识的学生,他们希望从零开始使用 TensorFlow 2.x 构建、训练和部署自己的强化学习系统。
本书涵盖的内容
第一章,使用 TensorFlow 2.x 开发深度强化学习的构建模块,提供了关于如何开始使用强化学习环境、基于深度神经网络的强化学习代理、进化神经代理以及其他用于离散和连续动作空间强化学习应用的构建模块的实现方法。
第二章,实现基于价值的策略梯度和演员-评论家深度强化学习算法,包括实现基于价值迭代的学习代理的实现方法,并将强化学习中几个基础算法的实现分解为简单步骤,例如蒙特卡洛控制、SARSA 和 Q 学习、演员-评论家以及策略梯度算法。
第三章,实现高级强化学习算法,提供了实现完整的代理训练系统的简明方法,使用的算法包括深度 Q 网络(DQN)、双重深度 Q 网络(DDQN、DDDQN)、深度递归 Q 网络(DRQN)、异步优势演员-评论家(A3C)、近端策略优化(PPO)和深度确定性策略梯度(DDPG)强化学习算法。
第四章,现实世界中的强化学习 – 构建加密货币交易代理,展示了如何在自定义 RL 环境中实现并训练一个软演员-评论家代理,使用来自交易所(如 Gemini)的真实市场数据进行比特币和以太坊交易,涵盖了表格和视觉(图像)状态/观测以及离散和连续动作空间。
第五章,现实世界中的强化学习 – 构建股票/证券交易代理,介绍了如何训练先进的 RL 代理,通过使用视觉价格图表和/或表格票据数据等,在由真实股票市场交易所数据驱动的自定义 RL 环境中进行股票市场交易以获取利润。
第六章,现实世界中的强化学习 – 构建智能代理来完成您的待办事项,提供了构建、训练和测试基于视觉的 RL 代理的食谱,用于在网络上完成任务,帮助您自动化诸如点击网页上的弹出/确认对话框、登录各大网站、查找并预订最便宜的机票、清理电子邮件收件箱、在社交媒体上进行点赞/分享/转发以与粉丝互动等任务。
第七章,将深度强化学习代理部署到云端,包含了帮助您超越潮流的工具和细节,使用深度 RL 构建基于云的模拟即服务(Simulation-as-a-Service)和代理/机器人即服务(Agent/Bot-as-a-Service)程序。学习如何使用运行在云端的远程模拟器训练 RL 代理,如何打包 RL 代理的运行时组件,并通过部署您自己的交易机器人即服务将深度 RL 代理部署到云端。
第八章,加速深度 RL 代理开发的分布式训练,包含了通过利用 TensorFlow 2.x 的功能,使用深度神经网络模型的分布式训练加速深度 RL 代理开发的食谱。学习如何利用单台机器或集群机器上的多个 CPU 和 GPU 来扩展深度 RL 代理训练,并学习如何利用 Ray、Tune 和 RLLib 进行大规模加速训练。
第九章,在多个平台上部署深度 RL 代理,提供了可定制的模板,您可以用来构建和部署您自己深度 RL 应用程序,以满足您的使用案例。学习如何将 RL 代理模型导出为各种生产就绪格式进行服务/部署,例如 TensorFlow Lite、TensorFlow.js 和 ONNX,并学习如何利用 NVIDIA Triton 或构建您自己的解决方案来启动生产就绪的基于 RL 的 AI 服务。您还将学习如何将 RL 代理部署到移动和网页应用,并如何在您的 Node.js 应用中部署 RL 机器人。
为了最大程度地利用本书
本书中的代码在 Ubuntu 18.04 和 Ubuntu 20.04 上进行了广泛测试,如果安装了 Python 3.6+,则应该能在更高版本的 Ubuntu 上正常运行。安装了 Python 3.6+以及每个示例开头列出的必要 Python 包后,代码也应该能够在 Windows 和 macOS X 上顺利运行。
建议创建并使用名为 tfrl-cookbook 的 Python 虚拟环境来安装包并运行本书中的代码。推荐使用 Miniconda 或 Anaconda 进行 Python 虚拟环境管理。
如果你正在使用本书的数字版本,建议你自己输入代码,或者通过 GitHub 仓库访问代码(下节提供链接)。这样可以帮助你避免与代码复制粘贴相关的潜在错误。
强烈建议你为 GitHub 仓库加星并进行分叉,以便接收代码示例的更新和改进。我们鼓励你分享你所构建的内容,并与其他读者和社区互动,访问github.com/PacktPublishing/Tensorflow-2-Reinforcement-Learning-Cookbook/discussions
。
下载示例代码文件
你可以通过你的账户在www.packt.com下载本书的示例代码文件。如果你在其他地方购买了本书,你可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给你。
你可以通过以下步骤下载代码文件:
-
在www.packt.com登录或注册。
-
选择支持选项卡。
-
点击代码下载。
-
在搜索框中输入书名,并按照屏幕上的说明操作。
下载完成后,请确保使用以下最新版本的工具解压或提取文件夹:
-
Windows 版的 WinRAR/7-Zip
-
Zipeg/iZip/UnRarX(适用于 Mac)
-
Linux 版的 7-Zip/PeaZip
本书的代码包也托管在 GitHub 上,地址为github.com/PacktPublishing/Tensorflow-2-Reinforcement-Learning-Cookbook/
。如果代码有更新,将会在现有的 GitHub 仓库中进行更新。
我们还提供了其他代码包,来自我们丰富的图书和视频目录,访问github.com/PacktPublishing/
。快去看看吧!
下载彩色图片
我们还提供了一份 PDF 文件,包含了本书中使用的截图/图表的彩色图片。你可以在这里下载:static.packt-cdn.com/downloads/9781838982546_ColorImages.pdf
。
使用的约定
本书中使用了多种文本约定。
文本中的代码
:表示在配方中使用的代码词。例如:“我们将从在Actor
类中实现save
方法开始,以将 Actor 模型导出为 TensorFlow 的SavedModel
格式。”
一段代码设置如下:
def save(self, model_dir: str, version: int = 1):
actor_model_save_dir = os.path.join(model_dir, "actor", str(version), "model.savedmodel")
self.model.save(actor_model_save_dir, save_format="tf")
print(f"Actor model saved at:{actor_model_save_dir}")
当我们希望引起您对代码块中特定部分的注意时,相关行或项将设置为粗体:
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)
任何命令行输入或输出如下所示:
(tfrl-cookbook)praveen@desktop:~/tensorflow2-reinforcement-learning-cookbook/src/ch7-cloud-deploy-deep-rl-agents$ python 3_training_rl_agents_using_remote_sims.py
粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的文字通常会以这种方式显示。以下是一个示例:“点击打开现有项目选项,您将看到一个弹出窗口,要求您选择文件系统中的目录。导航到第九章的配方,并选择9.2_rl_android_app。”
提示或重要注意事项
显示如下。
联系我们
我们始终欢迎读者的反馈。
customercare@packtpub.com
。
勘误:尽管我们已尽最大努力确保内容的准确性,但错误有时仍会发生。如果您发现本书中的错误,请向我们报告。请访问www.packtpub.com/support/errata,选择您的书籍,点击“勘误提交表单”链接并填写详细信息。
copyright@packt.com
,并附上相关链接。
如果您有兴趣成为作者:如果您在某个领域有专长,并且有意写作或为书籍做贡献,请访问authors.packtpub.com。
书评
请留下评论。当您阅读并使用完本书后,为什么不在您购买该书的网站上留下评论呢?潜在的读者可以看到并根据您的客观意见做出购买决定,我们 Packt 也能了解您对我们产品的看法,而我们的作者也能看到您对他们书籍的反馈。谢谢!
欲了解有关 Packt 的更多信息,请访问packt.com。
第一章:第一章:使用 Tensorflow 2.x 开发深度强化学习的构建模块
本章提供了 深度强化学习(Deep RL)基础的实际和具体描述,充满了使用最新主要版本 TensorFlow 2.x 实现构建模块的食谱。它包括启动 RL 环境、OpenAI Gym、开发基于神经网络的智能体以及用于解决具有离散和连续值空间的深度 RL 应用的进化神经智能体的食谱。
本章讨论了以下食谱:
-
构建训练 RL 智能体的环境和奖励机制
-
为离散动作空间和决策问题实现基于神经网络的 RL 策略
-
为连续动作空间和连续控制问题实现基于神经网络的 RL 策略
-
使用 OpenAI Gym 进行 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 智能体的环境和奖励机制
本食谱将引导你完成构建 Gridworld 学习环境的步骤,以训练 RL 智能体。Gridworld 是一个简单的环境,其中世界被表示为一个网格。网格中的每个位置可以称为一个单元格。智能体在这个环境中的目标是找到一条通往目标状态的路径,类似于这里展示的网格:
图 1.1 – Gridworld 环境的截图
智能体的位置由网格中的蓝色单元格表示,目标和地雷/炸弹/障碍物的位置分别通过绿色和红色单元格在网格中表示。智能体(蓝色单元格)需要找到一条穿越网格到达目标(绿色单元格)的路径,同时避免踩到地雷/炸弹(红色单元格)。
准备工作
要完成这个食谱,你首先需要激活 tf2rl-cookbook
Python/Conda 虚拟环境并运行 pip install numpy gym
。如果以下导入语句没有问题,那么你可以开始了!
import copy
import sys
import gym
import numpy as np
现在我们可以开始了。
如何做到……
为了训练强化学习(RL)智能体,我们需要一个学习环境,类似于监督学习中使用的数据集。学习环境是一个模拟器,它为 RL 智能体提供观察,支持 RL 智能体通过执行动作来执行一组动作,并返回执行动作后获得的结果/新观察。
执行以下步骤来实现一个 Gridworld 学习环境,该环境表示一个简单的二维地图,具有不同颜色的单元格,表示智能体、目标、地雷/炸弹/障碍物、墙壁和空白空间在网格中的位置:
-
我们首先定义不同单元格状态与其颜色代码之间的映射关系,以便在 Gridworld 环境中使用:
EMPTY = BLACK = 0 WALL = GRAY = 1 AGENT = BLUE = 2 MINE = RED = 3 GOAL = GREEN = 4 SUCCESS = PINK = 5
-
接下来,使用 RGB 强度值生成颜色映射:
COLOR_MAP = { BLACK: [0.0, 0.0, 0.0], GRAY: [0.5, 0.5, 0.5], BLUE: [0.0, 0.0, 1.0], RED: [1.0, 0.0, 0.0], GREEN: [0.0, 1.0, 0.0], PINK: [1.0, 0.0, 1.0], }
-
现在让我们定义动作映射关系:
NOOP = 0 DOWN = 1 UP = 2 LEFT = 3 RIGHT = 4
-
然后,我们创建一个
GridworldEnv
类,并定义一个__init__
函数来定义必要的类变量,包括观察空间和动作空间:class GridworldEnv(): def __init__(self):
我们将在接下来的步骤中实现
__init__()
方法。 -
在这一步中,让我们使用网格单元格状态映射来定义 Gridworld 环境的布局:
self.grid_layout = """ 1 1 1 1 1 1 1 1 1 2 0 0 0 0 0 1 1 0 1 1 1 0 0 1 1 0 1 0 1 0 0 1 1 0 1 4 1 0 0 1 1 0 3 0 0 0 0 1 1 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 """
在之前的布局中,
0
表示空白单元格,1
表示墙壁,2
表示智能体的起始位置,3
表示地雷/炸弹/障碍物的位置,4
表示目标位置,这些是根据我们在第一步中定义的映射关系。 -
现在,我们准备定义 Gridworld RL 环境的观察空间:
self.initial_grid_state = np.fromstring( self.grid_layout, dtype=int, sep=" ") self.initial_grid_state = \ self.initial_grid_state.reshape(8, 8) self.grid_state = copy.deepcopy( self.initial_grid_state) self.observation_space = gym.spaces.Box( low=0, high=6, shape=self.grid_state.shape ) self.img_shape = [256, 256, 3] self.metadata = {"render.modes": ["human"]}
-
让我们定义动作空间,以及动作与智能体在网格中移动之间的映射关系:
self.action_space = gym.spaces.Discrete(5) self.actions = [NOOP, UP, DOWN, LEFT, RIGHT] self.action_pos_dict = { NOOP: [0, 0], UP: [-1, 0], DOWN: [1, 0], LEFT: [0, -1], RIGHT: [0, 1], }
-
现在让我们通过使用
get_state()
方法初始化智能体的起始状态和目标状态来完成__init__
函数(我们将在下一步实现该方法):(self.agent_start_state, self.agent_goal_state,) = \ self.get_state()
-
现在我们需要实现
get_state()
方法,该方法返回 Gridworld 环境的起始状态和目标状态:def get_state(self): start_state = np.where(self.grid_state == AGENT) goal_state = np.where(self.grid_state == GOAL) start_or_goal_not_found = not (start_state[0] \ and goal_state[0]) if start_or_goal_not_found: sys.exit( "Start and/or Goal state not present in the Gridworld. " "Check the Grid layout" ) start_state = (start_state[0][0], start_state[1][0]) goal_state = (goal_state[0][0], goal_state[1][0]) return start_state, goal_state
-
在这一步中,我们将实现
step(action)
方法来执行动作并获取下一个状态/观察、相关奖励以及是否结束回合:def step(self, action): """return next observation, reward, done, info""" action = int(action) info = {"success": True} done = False reward = 0.0 next_obs = ( self.agent_state[0] + \ self.action_pos_dict[action][0], self.agent_state[1] + \ self.action_pos_dict[action][1], )
-
接下来,指定奖励,最后返回
grid_state
、reward
、done
和info
:# Determine the reward if action == NOOP: return self.grid_state, reward, False, info next_state_valid = ( next_obs[0] < 0 or next_obs[0] >= \ self.grid_state.shape[0] ) or (next_obs[1] < 0 or next_obs[1] >= \ self.grid_state.shape[1]) if next_state_valid: info["success"] = False return self.grid_state, reward, False, info next_state = self.grid_state[next_obs[0], next_obs[1]] if next_state == EMPTY: self.grid_state[next_obs[0], next_obs[1]] = AGENT elif next_state == WALL: info["success"] = False reward = -0.1 return self.grid_state, reward, False, info elif next_state == GOAL: done = True reward = 1 elif next_state == MINE: done = True reward = -1 # self._render("human") self.grid_state[self.agent_state[0], self.agent_state[1]] = EMPTY self.agent_state = copy.deepcopy(next_obs) return self.grid_state, reward, done, info
-
接下来是
reset()
方法,它会在一个回合完成时(或者在请求重置环境时)重置 Gridworld 环境:def reset(self): self.grid_state = copy.deepcopy( self.initial_grid_state) (self.agent_state, self.agent_goal_state,) = \ self.get_state() return self.grid_state
-
为了以更易于人类理解的方式可视化 Gridworld 环境的状态,让我们实现一个渲染函数,将我们在第五步中定义的
grid_layout
转换为图像并显示它。至此,Gridworld 环境的实现将完成!def gridarray_to_image(self, img_shape=None): if img_shape is None: img_shape = self.img_shape observation = np.random.randn(*img_shape) * 0.0 scale_x = int(observation.shape[0] / self.grid_\ state.shape[0]) scale_y = int(observation.shape[1] / self.grid_\ state.shape[1]) for i in range(self.grid_state.shape[0]): for j in range(self.grid_state.shape[1]): for k in range(3): # 3-channel RGB image pixel_value = \ COLOR_MAP[self.grid_state[i, j]][k] observation[ i * scale_x : (i + 1) * scale_x, j * scale_y : (j + 1) * scale_y, k, ] = pixel_value return (255 * observation).astype(np.uint8) def render(self, mode="human", close=False): if close: if self.viewer is not None: self.viewer.close() self.viewer = None return img = self.gridarray_to_image() if mode == "rgb_array": return img elif mode == "human": from gym.envs.classic_control import \ rendering if self.viewer is None: self.viewer = \ rendering.SimpleImageViewer() self.viewer.imshow(img)
-
为了测试环境是否按预期工作,让我们添加一个
__main__
函数,该函数将在直接运行环境脚本时执行:if __name__ == "__main__": env = GridworldEnv() obs = env.reset() # Sample a random action from the action space action = env.action_space.sample() next_obs, reward, done, info = env.step(action) print(f"reward:{reward} done:{done} info:{info}") env.render() env.close()
-
一切就绪!Gridworld 环境已经准备好,我们可以通过运行脚本(
python envs/gridworld.py
)来快速测试它。将显示类似如下的输出:reward:0.0 done:False info:{'success': True}
还将显示以下 Gridworld 环境的渲染图:
图 1.2 – Gridworld
现在让我们看看它是如何工作的!
它是如何工作的…
第 5 步中如何实现…部分定义的grid_layout
表示学习环境的状态。Gridworld 环境定义了观察空间、动作空间和奖励机制,以实现env.render()
方法将环境的内部网格表示转换为图像并显示,以便进行视觉理解。
为离散动作空间和决策问题实现基于神经网络的 RL 策略
许多强化学习(RL)环境(无论是模拟的还是实际的)要求 RL 智能体从一系列动作中选择一个动作,换句话说,执行离散动作。虽然简单的线性函数可以用来表示此类智能体的策略,但它们通常无法扩展到复杂问题。像(深度)神经网络这样的非线性函数逼近器可以逼近任意函数,甚至是解决复杂问题所需的函数。
基于神经网络的策略网络是高级 RL 和深度 RL的关键构建块,并将适用于一般的离散决策问题。
在本节结束时,你将实现一个基于神经网络的策略智能体,该智能体使用TensorFlow 2.x并能够在Gridworld环境中执行动作,并且(经过很少或不需要修改)可以在任何离散动作空间环境中运行。
准备开始
激活tf2rl-cookbook
Python 虚拟环境,并运行以下命令来安装和导入相关包:
pip install --upgrade numpy tensorflow tensorflow_probability seaborn
import seaborn as sns
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
import tensorflow_probability as tfp
让我们开始吧。
如何实现…
我们将看看智能体在离散动作空间环境中可以使用的策略分布类型:
-
我们首先在 TensorFlow 2.x 中使用
tensorflow_probability
库创建一个二元策略分布:binary_policy = tfp.distributions.Bernoulli(probs=0.5) for i in range(5): action = binary_policy.sample(1) print("Action:", action)
前面的代码应输出如下内容:
Action: tf.Tensor([0], shape=(1,), dtype=int32) Action: tf.Tensor([1], shape=(1,), dtype=int32) Action: tf.Tensor([0], shape=(1,), dtype=int32) Action: tf.Tensor([1], shape=(1,), dtype=int32) Action: tf.Tensor([1], shape=(1,), dtype=int32)
重要提示
你得到的动作值将与这里显示的不同,因为它们是从伯努利分布中采样的,而这不是一个确定性过程。
-
让我们快速可视化二元策略分布:
# Sample 500 actions from the binary policy distribution sample_actions = binary_policy.sample(500) sns.distplot(sample_actions)
前面的代码将生成一个如图所示的分布图:
图 1.3 – 二元策略的分布图
-
在这一阶段,我们将实现一个离散的策略分布。一个单一离散变量的类别分布,具有k个有限类别,被称为多项分布。多项分布的推广是多次试验的多项分布,我们将使用它来表示离散策略分布:
action_dim = 4 # Dimension of the discrete action space action_probabilities = [0.25, 0.25, 0.25, 0.25] discrete_policy = tfp.distributions.Multinomial(probs=action_probabilities, total_count=1) for i in range(5): action = discrete_policy.sample(1) print(action)
前面的代码应输出类似以下内容:
tf.Tensor([[0\. 0\. 0\. 1.]], shape=(1, 4), dtype=float32) tf.Tensor([[0\. 0\. 1\. 0.]], shape=(1, 4), dtype=float32) tf.Tensor([[0\. 0\. 1\. 0.]], shape=(1, 4), dtype=float32) tf.Tensor([[1\. 0\. 0\. 0.]], shape=(1, 4), dtype=float32) tf.Tensor([[0\. 1\. 0\. 0.]], shape=(1, 4), dtype=float32)
-
接下来,我们将可视化离散的概率分布:
sns.distplot(discrete_policy.sample(1))
前面的代码将生成一个分布图,类似于这里显示的
discrete_policy
:图 1.4 – 离散策略的分布图
-
然后,计算离散策略的熵:
def entropy(action_probs): return -tf.reduce_sum(action_probs * \ tf.math.log(action_probs), axis=-1) action_probabilities = [0.25, 0.25, 0.25, 0.25] print(entropy(action_probabilities))
-
同时,实现一个离散策略类:
class DiscretePolicy(object): def __init__(self, num_actions): self.action_dim = num_actions def sample(self, actino_logits): self.distribution = tfp.distributions.Multinomial(logits=action_logits, total_count=1) return self.distribution.sample(1) def get_action(self, action_logits): action = self.sample(action_logits) return np.where(action)[-1] # Return the action index def entropy(self, action_probabilities): return – tf.reduce_sum(action_probabilities * tf.math.log(action_probabilities), axis=-1)
-
现在我们实现一个辅助方法,用于在给定环境中评估智能体:
def evaluate(agent, env, render=True): obs, episode_reward, done, step_num = env.reset(), 0.0, False, 0 while not done: action = agent.get_action(obs) obs, reward, done, info = env.step(action) episode_reward += reward step_num += 1 if render: env.render() return step_num, episode_reward, done, info
-
现在,让我们使用 TensorFlow 2.x 实现一个神经网络大脑类:
class Brain(keras.Model): def __init__(self, action_dim=5, input_shape=(1, 8 * 8)): """Initialize the Agent's Brain model Args: action_dim (int): Number of actions """ super(Brain, self).__init__() self.dense1 = layers.Dense(32, input_shape=\ input_shape, activation="relu") self.logits = layers.Dense(action_dim) def call(self, inputs): x = tf.convert_to_tensor(inputs) if len(x.shape) >= 2 and x.shape[0] != 1: x = tf.reshape(x, (1, -1)) return self.logits(self.dense1(x)) def process(self, observations): # Process batch observations using `call(inputs)` behind-the-scenes action_logits = \ self.predict_on_batch(observations) return action_logits
-
现在,让我们实现一个简单的智能体类,使用
DiscretePolicy
对象在离散环境中进行操作:class Agent(object): def __init__(self, action_dim=5, input_dim=(1, 8 * 8)): self.brain = Brain(action_dim, input_dim) self.policy = DiscretePolicy(action_dim) def get_action(self, obs): action_logits = self.brain.process(obs) action = self.policy.get_action( np.squeeze(action_logits, 0)) return action
-
现在,让我们在
GridworldEnv
中测试智能体:from envs.gridworld import GridworldEnv env = GridworldEnv() agent = Agent(env.action_space.n, env.observation_space.shape) steps, reward, done, info = evaluate(agent, env) print(f"steps:{steps} reward:{reward} done:{done} info:{info}") env.close()
这展示了如何实现策略。我们将在接下来的部分看到这一点是如何运作的。
它是如何工作的…
RL 智能体的核心组件之一是策略函数,它将观察与动作之间进行映射。形式上,策略是一个动作的分布,它规定了给定观察时选择某个动作的概率。
在智能体最多只能采取两个不同动作的环境中,例如,在二元动作空间中,我们可以使用伯努利分布来表示策略,其中采取动作 0 的概率由给出,采取动作 1 的概率由
给出,从而产生以下概率分布:
离散概率分布可以用来表示 RL 智能体的策略,当智能体可以在环境中采取k个可能的行动时。
从一般意义上讲,这种分布可以用来描述当随机变量可以取k个可能类别之一时的可能结果,因此也被称为类别分布。这是伯努利分布对 k 种事件的推广,因此是一个多项伯努利分布。
实现基于神经网络的 RL 策略,适用于连续动作空间和连续控制问题
强化学习已被用于许多控制问题中,取得了最先进的成果,不仅在像 Atari、围棋、国际象棋、将棋和星际争霸等各种游戏中,而且在现实世界的部署中,如暖通空调控制系统(HVAC)。
在动作空间是连续的环境中,意味着动作是实值的,需要使用实值的连续策略分布。当环境的动作空间包含实数时,可以使用连续概率分布来表示 RL 智能体的策略。从一般意义上讲,这种分布可以用来描述当随机变量可以取任何(实)值时,随机变量的可能结果。
一旦配方完成,你将拥有一个完整的脚本,控制一个在二维空间中驱车上坡的汽车,使用MountainCarContinuous
环境和连续动作空间。MountainCarContinuous
环境的截图如下:
图 1.5 – MountainCarContinuous 环境的截图
准备工作
激活tf2rl-cookbook
Conda Python 环境,并运行以下命令来安装和导入本食谱所需的 Python 包:
pip install --upgrade tensorflow_probability
import tensorflow_probability as tfp
import seaborn as sns
让我们开始吧。
如何实现……
我们将首先通过tensorflow_probability
库创建连续策略分布,并在此基础上构建必要的动作采样方法,以便为 RL 环境中给定的连续空间生成动作:
-
我们使用
tensorflow_probability
库在 TensorFlow 2.x 中创建一个连续的策略分布。我们将使用高斯/正态分布来创建一个在连续值上的策略分布:sample_actions = continuous_policy.sample(500) sns.distplot(sample_actions)
-
接下来,我们可视化一个连续的策略分布:
sample_actions = continuous_policy.sample(500) sns.distplot(sample_actions)
前面的代码将生成一个连续策略的分布图,类似于这里所示的图:
图 1.6 – 连续策略的分布图
-
现在,让我们使用高斯/正态分布实现一个连续策略分布:
mu = 0.0 # Mean = 0.0 sigma = 1.0 # Std deviation = 1.0 continuous_policy = tfp.distributions.Normal(loc=mu, scale=sigma) # action = continuous_policy.sample(10) for i in range(10): action = continuous_policy.sample(1) print(action)
前面的代码应当打印出类似于以下代码块中的内容:
tf.Tensor([-0.2527136], shape=(1,), dtype=float32) tf.Tensor([1.3262751], shape=(1,), dtype=float32) tf.Tensor([0.81889665], shape=(1,), dtype=float32) tf.Tensor([1.754675], shape=(1,), dtype=float32) tf.Tensor([0.30025303], shape=(1,), dtype=float32) tf.Tensor([-0.61728036], shape=(1,), dtype=float32) tf.Tensor([0.40142158], shape=(1,), dtype=float32) tf.Tensor([1.3219402], shape=(1,), dtype=float32) tf.Tensor([0.8791297], shape=(1,), dtype=float32) tf.Tensor([0.30356944], shape=(1,), dtype=float32)
重要提示
你得到的动作值将与这里显示的不同,因为它们是从高斯分布中采样的,而不是一个确定性的过程。
-
现在,让我们更进一步,实施一个多维连续策略。多元高斯分布可以用来表示多维连续策略。此类策略对于在具有多维、连续且实数值动作空间的环境中行动的智能体非常有用:
mu = [0.0, 0.0] covariance_diag = [3.0, 3.0] continuous_multidim_policy = tfp.distributions.MultivariateNormalDiag(loc=mu, scale_diag=covariance_diag) # action = continuous_multidim_policy.sample(10) for i in range(10): action = continuous_multidim_policy.sample(1) print(action)
前面的代码应当打印出类似以下内容:
tf.Tensor([[ 1.7003113 -2.5801306]], shape=(1, 2), dtype=float32) tf.Tensor([[ 2.744986 -0.5607129]], shape=(1, 2), dtype=float32) tf.Tensor([[ 6.696332 -3.3528223]], shape=(1, 2), dtype=float32) tf.Tensor([[ 1.2496299 -8.301748 ]], shape=(1, 2), dtype=float32) tf.Tensor([[2.0009246 3.557394 ]], shape=(1, 2), dtype=float32) tf.Tensor([[-4.491785 -1.0101566]], shape=(1, 2), dtype=float32) tf.Tensor([[ 3.0810184 -0.9008362]], shape=(1, 2), dtype=float32) tf.Tensor([[1.4185237 2.2145705]], shape=(1, 2), dtype=float32) tf.Tensor([[-1.9961193 -2.1251974]], shape=(1, 2), dtype=float32) tf.Tensor([[-1.2200387 -4.3516426]], shape=(1, 2), dtype=float32)
-
在继续之前,让我们可视化多维连续策略:
sample_actions = continuous_multidim_policy.sample(500) sns.jointplot(sample_actions[:, 0], sample_actions[:, 1], kind='scatter')
前面的代码将生成一个联合分布图,类似于这里所示的图:
图 1.7 – 多维连续策略的联合分布图
-
现在,我们准备实现连续策略类:
class ContinuousPolicy(object): def __init__(self, action_dim): self.action_dim = action_dim def sample(self, mu, var): self.distribution = \ tfp.distributions.Normal(loc=mu, scale=sigma) return self.distribution.sample(1) def get_action(self, mu, var): action = self.sample(mu, var) return action
-
下一步,让我们实现一个多维连续策略类:
import tensorflow_probability as tfp import numpy as np class ContinuousMultiDimensionalPolicy(object): def __init__(self, num_actions): self.action_dim = num_actions def sample(self, mu, covariance_diag): self.distribution = tfp.distributions.\ MultivariateNormalDiag(loc=mu, scale_diag=covariance_diag) return self.distribution.sample(1) def get_action(self, mu, covariance_diag): action = self.sample(mu, covariance_diag) return action
-
现在,让我们实现一个函数,在具有连续动作空间的环境中评估智能体,以评估每一回合的表现:
def evaluate(agent, env, render=True): obs, episode_reward, done, step_num = env.reset(), 0.0, False, 0 while not done: action = agent.get_action(obs) obs, reward, done, info = env.step(action) episode_reward += reward step_num += 1 if render: env.render() return step_num, episode_reward, done, info
-
我们现在准备在一个连续动作环境中测试智能体:
from neural_agent import Brain import gym env = gym.make("MountainCarContinuous-v0")Implementing a Neural-network Brain class using TensorFlow 2.x. class Brain(keras.Model): def __init__(self, action_dim=5, input_shape=(1, 8 * 8)): """Initialize the Agent's Brain model Args: action_dim (int): Number of actions """ super(Brain, self).__init__() self.dense1 = layers.Dense(32, input_shape=input_shape, activation="relu") self.logits = layers.Dense(action_dim) def call(self, inputs): x = tf.convert_to_tensor(inputs) if len(x.shape) >= 2 and x.shape[0] != 1: x = tf.reshape(x, (1, -1)) return self.logits(self.dense1(x)) def process(self, observations): # Process batch observations using `call(inputs)` # behind-the-scenes action_logits = \ self.predict_on_batch(observations) return action_logits
-
让我们实现一个简单的智能体类,利用
ContinuousPolicy
对象在连续动作空间环境中进行操作:class Agent(object): def __init__(self, action_dim=5, input_dim=(1, 8 * 8)): self.brain = Brain(action_dim, input_dim) self.policy = ContinuousPolicy(action_dim) def get_action(self, obs): action_logits = self.brain.process(obs) action = self.policy.get_action(*np.\ squeeze(action_logits, 0)) return action
-
最后一步,我们将在一个连续动作空间环境中测试智能体的性能:
from neural_agent import Brain import gym env = gym.make("MountainCarContinuous-v0") action_dim = 2 * env.action_space.shape[0] # 2 values (mu & sigma) for one action dim agent = Agent(action_dim, env.observation_space.shape) steps, reward, done, info = evaluate(agent, env) print(f"steps:{steps} reward:{reward} done:{done} info:{info}") env.close()
前面的脚本将调用
MountainCarContinuous
环境,渲染到屏幕上,并展示智能体在这个连续动作空间环境中的表现:
图 1.8 – MountainCarContinuous-v0 环境中代理的截图
接下来,让我们探索它是如何工作的。
如何操作…
我们为 RL 代理实现了一个使用高斯分布的连续值策略。高斯分布,也称为正态分布,是最广泛使用的实数分布。它通过两个参数表示:µ和σ。我们通过从分布中抽样,基于由以下公式给出的概率密度,生成这种策略的连续值动作:
多元正态分布将正态分布扩展到多个变量。我们使用这种分布来生成多维连续策略。
使用 OpenAI Gym 进行 RL 训练环境的工作
本篇提供了一个快速指南,帮助你快速启动并运行 OpenAI Gym 环境。Gym 环境和接口为训练 RL 代理提供了一个平台,是目前最广泛使用和接受的 RL 环境接口。
准备工作
我们将需要完整安装 OpenAI Gym,以便能够使用可用的环境。请按照github.com/openai/gym#id5
中列出的 Gym 安装步骤进行操作。
至少,你应该执行以下命令:
pip install gym[atari]
如何操作…
让我们从选择一个环境并探索 Gym 接口开始。你可能已经熟悉了前面食谱中用于创建 Gym 环境的基本函数调用。
你的步骤应该按以下格式排列:
-
让我们首先探索 Gym 中的环境列表:
#!/usr/bin/env python from gym import envs env_names = [spec.id for spec in envs.registry.all()] for name in sorted(env_names): print(name)
-
这个脚本将打印出通过你的 Gym 安装可用的所有环境的名称,按字母顺序排序。你可以使用以下命令运行这个脚本,查看已安装并可用的环境名称。你应该能看到列出的一长串环境。前几个环境在以下截图中提供,供你参考:
图 1.9 – 使用 openai-gym 包可用的环境列表
现在让我们来看一下如何运行其中一个 Gym 环境。
-
以下脚本将让你探索任何可用的 Gym 环境:
#!/usr/bin/env python import gym import sys def run_gym_env(argv): env = gym.make(argv[1]) # Name of the environment # supplied as 1st argument env.reset() for _ in range(int(argv[2])): env.render() env.step(env.action_space.sample()) env.close() if __name__ == "__main__": run_gym_env(sys.argv)
-
你可以将前面的脚本保存为
run_gym_env.py
,然后像这样运行脚本:Alien-v4 environment, which should look like the following screenshot:
图 1.10 – 使用 Alien-v4 1000 作为参数的 run_gym_env.py 样本输出
提示
你可以将Alien-v4
更改为上一步骤中列出的任何可用 Gym 环境。
如何操作…
以下表格总结了 Gym 环境的工作原理:
表 1.1 – Gym 环境接口概述
参见
您可以在这里找到有关 OpenAI Gym 的更多信息:gym.openai.com/
。
构建一个神经代理
本教程将指导您完成构建完整代理和代理-环境交互循环的步骤,这是任何强化学习应用程序的主要构建块。完成教程后,您将拥有一个可执行脚本,其中一个简单代理尝试在 Gridworld 环境中执行动作。您建立的代理可能会执行如下截图所示的操作:
图 1.11 – neural_agent.py 脚本输出的截图
准备工作
让我们开始通过激活tf2rl-cookbook
的 Conda Python 环境,并运行以下代码来安装和导入必要的 Python 模块:
pip install tensorflow gym tqdm # Run this line in a terminal
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
import gym
import envs
from tqdm import tqdm
如何操作…
我们将首先实现由 TensorFlow 2.x 驱动的 Brain 类,其基于神经网络实现:
-
让我们首先使用 TensorFlow 2.x 和 Keras 函数 API 初始化一个神经脑模型:
class Brain(keras.Model): def __init__(self, action_dim=5, input_shape=(1, 8 * 8)): """Initialize the Agent's Brain model Args: action_dim (int): Number of actions """ super(Brain, self).__init__() self.dense1 = layers.Dense(32, input_shape= \ input_shape, activation="relu") self.logits = layers.Dense(action_dim)
-
接下来,我们实现
Brain
类的call(…)
方法:def call(self, inputs): x = tf.convert_to_tensor(inputs) if len(x.shape) >= 2 and x.shape[0] != 1: x = tf.reshape(x, (1, -1)) return self.logits(self.dense1(x))
-
现在我们需要实现 Brain 类的
process()
方法,方便地对一批输入/观测进行预测:def process(self, observations): # Process batch observations using `call(inputs)` # behind-the-scenes action_logits = \ self.predict_on_batch(observations) return action_logits
-
现在让我们实现代理类的 init 函数:
class Agent(object): def __init__(self, action_dim=5, input_shape=(1, 8 * 8)): """Agent with a neural-network brain powered policy Args: brain (keras.Model): Neural Network based model """ self.brain = Brain(action_dim, input_shape) self.policy = self.policy_mlp
-
现在让我们为代理定义一个简单的策略函数:
def policy_mlp(self, observations): observations = observations.reshape(1, -1) # action_logits = self.brain(observations) action_logits = self.brain.process(observations) action = tf.random.categorical(tf.math.\ log(action_logits), num_samples=1) return tf.squeeze(action, axis=1)
-
然后,让我们为代理实现一个方便的
get_action
方法:def get_action(self, observations): return self.policy(observations)
-
现在让我们创建一个
learn()
的占位函数,将作为未来教程中 RL 算法实现的一部分:def learn(self, samples): raise NotImplementedError
这完成了我们基本代理的实现及所需的组成部分!
-
现在让我们在给定环境中为代理评估一个 episode:
def evaluate(agent, env, render=True): obs, episode_reward, done, step_num = env.reset(), 0.0, False, 0 while not done: action = agent.get_action(obs) obs, reward, done, info = env.step(action) episode_reward += reward step_num += 1 if render: env.render() return step_num, episode_reward, done, info
-
最后,让我们实现主函数:
if __name__ == "__main__": env = gym.make("Gridworld-v0") agent = Agent(env.action_space.n, env.observation_space.shape) for episode in tqdm(range(10)): steps, episode_reward, done, info = \ evaluate(agent, env) print(f"EpReward:{episode_reward:.2f}\ steps:{steps} done:{done} info:{info}") env.close()
-
按以下方式执行脚本:
python neural_agent.py
您应该看到 Gridworld 环境 GUI 弹出。这将显示代理在环境中的操作,看起来像以下截图所示:
图 1.12 – 神经代理在 Gridworld 环境中执行操作的截图
这提供了一个简单但完整的配方,用于构建一个代理和代理-环境交互循环。剩下的就是向learn()
方法添加您选择的 RL 算法,代理将开始智能地行动!
工作原理…
本教程汇集了构建完整代理-环境系统的必要组成部分。Brain
类实现了作为代理处理单元的神经网络,代理类利用Brain
类和一个简单的策略,在处理从环境接收到的观测后基于脑的输出选择动作。
我们将Brain
类实现为keras.Model
类的子类,这使我们能够为智能体的大脑定义一个自定义的基于神经网络的模型。__init__
方法初始化Brain
模型并使用Brain
模型定义所需的层,我们创建了两个__init__
方法,call(…)
方法也是一个必须由继承自keras.Model
类的子类实现的方法。call(…)
方法首先将输入转换为 TensorFlow 2.x 张量,然后将输入展平为形状为1 x total_number_of_elements
的输入张量。例如,如果输入数据的形状是 8 x 8(8 行 8 列),数据首先被转换为张量,形状展平为 1 x 8 * 8 = 1 x 64。然后,展平后的输入通过包含 32 个神经元和 ReLU 激活函数的 dense1 层进行处理。最后,logits 层处理来自前一层的输出,并产生 n 个输出,分别对应动作维度(n)。
predict_on_batch(…)
方法对作为参数提供的一批输入进行预测。与Keras的predict()
函数不同,这个函数假设提供的输入(观测值)正好是一批输入,因此将这批输入直接传递给网络,而不再进一步拆分输入数据。
然后,我们实现了Agent
类,并在智能体初始化函数中通过定义以下内容创建了一个Brain
类的对象实例:
self.brain = Brain(action_dim, input_shape)
在这里,input_shape
是大脑预计处理的输入形状,action_dim
是大脑预计输出的形状。智能体的策略被定义为来自前述配方的自定义DiscretePolicy
,以初始化智能体的策略。
智能体的策略函数policy_mlp
会展平输入观测值,并将其发送到智能体的大脑进行处理,得到action_logits
,即动作的未归一化概率。最终的动作是通过使用 TensorFlow 2.x 的categorical
方法从随机模块中获取的,该方法从给定的action_logits
(未归一化概率)中采样一个有效的动作。
重要提示
如果提供给predict_on_batch
函数的所有观测值无法在给定的 GPU 内存或 RAM(CPU)中容纳,则此操作可能导致 GPU 内存溢出(OOM)错误。
如果直接运行neural_agent.py
脚本,启动的主函数会创建一个 Gridworld-v0 环境实例,使用该环境的动作空间和观测空间初始化一个智能体,并开始对智能体进行 10 轮评估。
构建一个神经进化智能体
进化方法基于黑盒优化,也被称为无梯度方法,因为它不涉及梯度计算。本食谱将指导你实现一个简单的基于交叉熵的神经进化代理,使用 TensorFlow 2.x。
准备工作
激活 tf2rl-cookbook
Python 环境并导入以下运行此食谱所需的包:
from collections import namedtuple
import gym
import matplotlib.pyplot as plt
import numpy as np
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
from tqdm import tqdm
import envs
安装了所需的包后,我们准备开始了。
如何做到……
让我们将本章所学的所有内容结合起来,构建一个神经代理,通过进化过程改进其策略,以在 Gridworld 环境中导航:
-
让我们从
neural_agent.py
导入基本的神经代理和 Brain 类:from neural_agent import Agent, Brain from envs.gridworld import GridworldEnv
-
接下来,让我们实现一个方法,在给定的环境中滚动代理进行一轮,并返回
obs_batch
、actions_batch
和episode_reward
:def rollout(agent, env, render=False): obs, episode_reward, done, step_num = env.reset(), 0.0, False, 0 observations, actions = [], [] episode_reward = 0.0 while not done: action = agent.get_action(obs) next_obs, reward, done, info = env.step(action) # Save experience observations.append(np.array(obs).reshape(1, -1)) # Convert to numpy & reshape (8, 8) to (1, 64) actions.append(action) episode_reward += reward obs = next_obs step_num += 1 if render: env.render() env.close() return observations, actions, episode_reward
-
现在让我们测试轨迹滚动方法:
env = GridworldEnv() # input_shape = (env.observation_space.shape[0] * \ env.observation_space.shape[1], ) brain = Brain(env.action_space.n) agent = Agent(brain) obs_batch, actions_batch, episode_reward = rollout(agent, env)
-
现在,是时候验证使用滚动操作生成的经验数据是否一致了:
assert len(obs_batch) == len(actions_batch)
-
现在让我们滚动多个完整的轨迹以收集经验数据:
# Trajectory: (obs_batch, actions_batch, episode_reward) # Rollout 100 episodes; Maximum possible steps = 100 * 100 = 10e4 trajectories = [rollout(agent, env, render=True) \ for _ in tqdm(range(100))]
-
然后我们可以从经验数据样本中可视化奖励分布。我们还将在收集的经验数据中的期望奖励值的第 50 百分位处绘制一条红色竖线:
from tqdm.auto import tqdm import matplotlib.pyplot as plt %matplotlib inline sample_ep_rewards = [rollout(agent, env)[-1] for _ in \ tqdm(range(100))] plt.hist(sample_ep_rewards, bins=10, histtype="bar");
运行此代码将生成一个如下面图所示的图表:
图 1.13 – 期望奖励值的直方图
-
现在让我们创建一个用于存储轨迹的容器:
from collections import namedtuple Trajectory = namedtuple("Trajectory", ["obs", "actions", "reward"]) # Example for understanding the operations: print(Trajectory(*(1, 2, 3))) # Explanation: `*` unpacks the tuples into individual # values Trajectory(*(1, 2, 3)) == Trajectory(1, 2, 3) # The rollout(...) function returns a tuple of 3 values: # (obs, actions, rewards) # The Trajectory namedtuple can be used to collect # and store mini batch of experience to train the neuro # evolution agent trajectories = [Trajectory(*rollout(agent, env)) \ for _ in range(2)]
-
现在是时候为进化过程选择精英经验了:
def gather_elite_xp(trajectories, elitism_criterion): """Gather elite trajectories from the batch of trajectories Args: batch_trajectories (List): List of episode \ trajectories containing experiences (obs, actions,episode_reward) Returns: elite_batch_obs elite_batch_actions elite_reard_threshold """ batch_obs, batch_actions, batch_rewards = zip(*trajectories) reward_threshold = np.percentile(batch_rewards, elitism_criterion) indices = [index for index, value in enumerate( batch_rewards) if value >= reward_threshold] elite_batch_obs = [batch_obs[i] for i in indices] elite_batch_actions = [batch_actions[i] for i in \ indices] unpacked_elite_batch_obs = [item for items in \ elite_batch_obs for item in items] unpacked_elite_batch_actions = [item for items in \ elite_batch_actions for item in items] return np.array(unpacked_elite_batch_obs), \ np.array(unpacked_elite_batch_actions), \ reward_threshold
-
现在让我们测试精英经验收集例程:
elite_obs, elite_actions, reward_threshold = gather_elite_xp(trajectories, elitism_criterion=75)
-
现在让我们看一下如何实现一个帮助方法,将离散的动作索引转换为 one-hot 编码向量或动作概率分布:
def gen_action_distribution(action_index, action_dim=5): action_distribution = np.zeros(action_dim).\ astype(type(action_index)) action_distribution[action_index] = 1 action_distribution = \ np.expand_dims(action_distribution, 0) return action_distribution
-
现在是测试动作分布生成函数的时候了:
elite_action_distributions = np.array([gen_action_distribution(a.item()) for a in elite_actions])
-
现在,让我们使用 Keras 函数式 API 在 TensorFlow 2.x 中创建并编译神经网络大脑:
brain = Brain(env.action_space.n) brain.compile(loss="categorical_crossentropy", optimizer="adam", metrics=["accuracy"])
-
你现在可以按如下方式测试大脑训练循环:
elite_obs, elite_action_distributions = elite_obs.astype("float16"), elite_action_distributions.astype("float16") brain.fit(elite_obs, elite_action_distributions, batch_size=128, epochs=1);
这应该会产生以下输出:
1/1 [==============================] - 0s 960us/step - loss: 0.8060 - accuracy: 0.4900
提示
数值可能会有所不同。
-
下一大步是实现一个代理类,可以通过大脑初始化并在环境中行动:
class Agent(object): def __init__(self, brain): """Agent with a neural-network brain powered policy Args: brain (keras.Model): Neural Network based \ model """ self.brain = brain self.policy = self.policy_mlp def policy_mlp(self, observations): observations = observations.reshape(1, -1) action_logits = self.brain.process(observations) action = tf.random.categorical( tf.math.log(action_logits), num_samples=1) return tf.squeeze(action, axis=1) def get_action(self, observations): return self.policy(observations)
-
接下来,我们将实现一个帮助函数,用于评估给定环境中的代理:
def evaluate(agent, env, render=True): obs, episode_reward, done, step_num = env.reset(), 0.0, False, 0 while not done: action = agent.get_action(obs) obs, reward, done, info = env.step(action) episode_reward += reward step_num += 1 if render: env.render() return step_num, episode_reward, done, info
-
现在让我们测试代理评估循环:
env = GridworldEnv() agent = Agent(brain) for episode in tqdm(range(10)): steps, episode_reward, done, info = evaluate(agent, env) env.close()
-
下一步,让我们定义训练循环的参数:
total_trajectory_rollouts = 70 elitism_criterion = 70 # percentile num_epochs = 200 mean_rewards = [] elite_reward_thresholds = []
-
现在让我们创建
environment
、brain
和agent
对象:env = GridworldEnv() input_shape = (env.observation_space.shape[0] * \ env.observation_space.shape[1], ) brain = Brain(env.action_space.n) brain.compile(loss="categorical_crossentropy", optimizer="adam", metrics=["accuracy"]) agent = Agent(brain) for i in tqdm(range(num_epochs)): trajectories = [Trajectory(*rollout(agent, env)) \ for _ in range(total_trajectory_rollouts)] _, _, batch_rewards = zip(*trajectories) elite_obs, elite_actions, elite_threshold = \ gather_elite_xp(trajectories, elitism_criterion=elitism_criterion) elite_action_distributions = \ np.array([gen_action_distribution(a.item()) \ for a in elite_actions]) elite_obs, elite_action_distributions = \ elite_obs.astype("float16"), elite_action_distributions.astype("float16") brain.fit(elite_obs, elite_action_distributions, batch_size=128, epochs=3, verbose=0); mean_rewards.append(np.mean(batch_rewards)) elite_reward_thresholds.append(elite_threshold) print(f"Episode#:{i + 1} elite-reward-\ threshold:{elite_reward_thresholds[-1]:.2f} \ reward:{mean_rewards[-1]:.2f} ") plt.plot(mean_rewards, 'r', label="mean_reward") plt.plot(elite_reward_thresholds, 'g', label="elites_reward_threshold") plt.legend() plt.grid() plt.show()
这将生成一个如下面图所示的图表:
重要提示
期望奖励将有所不同,图表可能看起来不同。
图 1.14 – 平均奖励(实线,红色)和精英奖励阈值(虚线,绿色)的图
图中的实线表示神经进化代理获得的平均回报,虚线则表示用于确定精英的回报阈值。
它是如何工作的……
在每次迭代中,进化过程会展开或收集一系列轨迹,通过使用当前神经网络权重来构建经验数据,这些权重位于代理的大脑中。然后,会采用精英选择过程,基于该轨迹中获得的回报来挑选出前* k *百分位(精英标准)的轨迹/经验。接着,这些筛选出的经验数据将用于更新代理的大脑模型。这个过程会重复预设的迭代次数,从而使代理的大脑模型不断改进,并收集更多的回报。
另请参见
欲了解更多信息,我建议阅读 CMA 进化策略:教程:arxiv.org/pdf/1604.00772.pdf
。
第二章:第二章:实现基于值、基于策略和演员-评论员深度 RL 算法
本章提供了一种实际的方法,用于构建基于值、基于策略和基于演员-评论员算法的强化学习(RL)智能体。它包括实现基于值迭代的学习智能体的食谱,并将 RL 中几个基础算法的实现细节分解为简单的步骤。基于策略梯度的智能体和演员-评论员智能体使用最新版本的TensorFlow 2.x来定义神经网络策略。
本章将涵盖以下食谱:
-
构建用于训练 RL 智能体的随机环境
-
构建基于值的(RL)智能体算法
-
实现时序差分学习
-
为 RL 构建蒙特卡罗预测和控制算法
-
实现 SARSA 算法和 RL 智能体
-
构建 Q-learning 智能体
-
实现策略梯度
-
实现演员-评论员算法
开始吧!
技术要求
本书中的代码已在 Ubuntu 18.04 和 Ubuntu 20.04 上进行了广泛测试,并应当能在更高版本的 Ubuntu 上运行,只要安装了 Python 3.6+。在安装了 Python 3.6 以及每个食谱开头列出的必要 Python 包后,代码也应该可以在 Windows 和 Mac OS X 上运行。建议你创建并使用名为tf2rl-cookbook
的 Python 虚拟环境来安装包并运行本书中的代码。推荐安装 Miniconda 或 Anaconda 来进行 Python 虚拟环境管理。
每个章节中每个食谱的完整代码可以在这里找到:github.com/PacktPublishing/Tensorflow-2-Reinforcement-Learning-Cookbook
。
构建用于训练 RL 智能体的随机环境
要为现实世界训练 RL 智能体,我们需要随机的学习环境,因为现实世界问题本质上是随机的。本食谱将带你一步步构建一个迷宫学习环境来训练 RL 智能体。迷宫是一个简单的随机环境,世界被表示为一个网格。网格上的每个位置可以称为一个单元格。这个环境中智能体的目标是找到通往目标状态的道路。考虑下图中的迷宫,其中黑色单元格表示墙壁:
图 2.1 – 迷宫环境
智能体的位置初始化为位于迷宫的左上角单元格。智能体需要绕过网格,找到通往迷宫右上角目标单元格的路径,在此过程中收集最多数量的金币,同时避免碰到墙壁。目标位置、金币、墙壁和智能体的起始位置可以在环境代码中修改。
该环境中支持的四维离散动作如下:
-
0: 向上移动
-
1: 向下移动
-
2: 向左移动
-
3: 向右移动
奖励基于智能体在到达目标状态之前收集的硬币数量。由于环境具有随机性,环境执行的动作有 0.1 的概率发生“滑动”,即实际执行的动作会随机发生变化。滑动动作将是顺时针方向的动作(左 -> 上,上 -> 右,依此类推)。例如,当slip_probability=0.2
时,右移动作有 0.2 的概率会变成下移。
准备中
要完成此任务,您需要激活tf2rl-cookbook
Python/conda 虚拟环境,并运行pip install -r requirements.txt
。如果以下导入语句运行没有问题,那么您可以开始了:
import gym
import numpy as np
现在,我们可以开始了。
如何做……
学习环境是一个模拟器,提供 RL 智能体的观察,支持 RL 智能体可以执行的一组动作,并返回执行动作后得到的新观察结果。
按照以下步骤实现一个随机迷宫学习环境,表示一个简单的二维地图,单元格代表智能体的位置、目标、墙壁、硬币和空白区域:
-
我们将首先定义 MazeEnv 类和迷宫环境的地图:
class MazeEnv(gym.Env): def __init__(self, stochastic=True): """Stochastic Maze environment with coins,\ obstacles/walls and a goal state. """ self.map = np.asarray(["SWFWG", "OOOOO", "WOOOW", "FOWFW"])
-
接下来,将障碍物/墙壁放置在环境地图的适当位置:
self.dim = (4, 5) self.img_map = np.ones(self.dim) self.obstacles = [(0, 1), (0, 3), (2, 0), (2, 4), (3, 2), (3, 4)] for x in self.obstacles: self.img_map[x[0]][x[1]] = 0
-
让我们定义顺时针方向的滑动映射动作:
self.slip_action_map = { 0: 3, 1: 2, 2: 0, 3: 1, }
-
现在,让我们定义一个字典形式的查找表,将索引映射到迷宫环境中的单元格:
self.index_to_coordinate_map = { 0: (0, 0), 1: (1, 0), 2: (3, 0), 3: (1, 1), 4: (2, 1), 5: (3, 1), 6: (0, 2), 7: (1, 2), 8: (2, 2), 9: (1, 3), 10: (2, 3), 11: (3, 3), 12: (0, 4), 13: (1, 4), }
-
接下来,让我们定义反向查找,以便在给定索引时找到一个单元格:
self.coordinate_to_index_map = dict((val, key) for \ key, val in self.index_to_coordinate_map.items())
到此,我们已经完成了环境的初始化!
-
现在,让我们定义一个方法来处理迷宫中的硬币及其状态,其中 0 表示硬币未被智能体收集,1 表示硬币已被智能体收集:
def num2coin(self, n: int): coinlist = [ (0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1), (1, 1, 0), (1, 0, 1), (0, 1, 1), (1, 1, 1), ] return list(coinlist[n])
-
现在,让我们定义一个快速方法来执行与查找硬币的数字状态/值相反的操作:
def coin2num(self, v: List): if sum(v) < 2: return np.inner(v, [1, 2, 3]) else: return np.inner(v, [1, 2, 3]) + 1
-
接下来,我们将定义一个设置函数来设置环境的状态。对于值迭代等算法,这非常有用,因为每个状态都需要在环境中被访问,以便算法计算值:
def set_state(self, state: int) -> None: """Set the current state of the environment. Useful for value iteration Args: state (int): A valid state in the Maze env \ int: [0, 112] """ self.state = state
-
现在,是时候实现
step
方法了。我们将首先实现step
方法,并根据slip_probability
应用滑动动作:def step(self, action, slip=True): """Run one step into the Maze env Args: state (Any): Current index state of the maze action (int): Discrete action for up, down,\ left, right slip (bool, optional): Stochasticity in the \ env. Defaults to True. Raises: ValueError: If invalid action is provided as input Returns: Tuple : Next state, reward, done, _ """ self.slip = slip if self.slip: if np.random.rand() < self.slip_probability: action = self.slip_action_map[action]
-
继续实现
step
函数时,我们将根据执行的动作更新迷宫的状态:cell = self.index_to_coordinate_map[int(self.state / 8)] if action == 0: c_next = cell[1] r_next = max(0, cell[0] - 1) elif action == 1: c_next = cell[1] r_next = min(self.dim[0] - 1, cell[0] + 1) elif action == 2: c_next = max(0, cell[1] - 1) r_next = cell[0] elif action == 3: c_next = min(self.dim[1] - 1, cell[1] + 1) r_next = cell[0] else: raise ValueError(f"Invalid action:{action}")
-
接下来,我们将判断智能体是否已达到目标:
if (r_next == self.goal_pos[0]) and ( c_next == self.goal_pos[1] ): # Check if goal reached v_coin = self.num2coin(self.state % 8) self.state = 8 * self.coordinate_to_index_\ map[(r_next, c_next)] + self.state % 8 return ( self.state, float(sum(v_coin)), True, )
-
接下来,我们将处理当动作导致碰到障碍物/墙壁的情况:
else: if (r_next, c_next) in self.obstacles: # obstacle # tuple list return self.state, 0.0, False
-
您需要处理的最后一个情况是判断动作是否导致收集硬币:
else: # Coin locations v_coin = self.num2coin(self.state % 8) if (r_next, c_next) == (0, 2): v_coin[0] = 1 elif (r_next, c_next) == (3, 0): v_coin[1] = 1 elif (r_next, c_next) == (3, 3): v_coin[2] = 1 self.state = 8 * self.coordinate_to_index_map[(r_next, c_next)] + self.coin2num(v_coin) return ( self.state, 0.0, False, )
-
为了以一种对人类友好的方式可视化 Gridworld 的状态,让我们实现一个渲染函数,该函数将打印出迷宫环境当前状态的文本版本:
def render(self): cell = self.index_to_coordinate_map[int( self.state / 8)] desc = self.map.tolist() desc[cell[0]] = ( desc[cell[0]][: cell[1]] + "\x1b[1;34m" + desc[cell[0]][cell[1]] + "\x1b[0m" + desc[cell[0]][cell[1] + 1 :] ) print("\n".join("".join(row) for row in desc))
-
为了测试环境是否按预期工作, let’s 添加一个
__main__
函数,当环境脚本直接运行时会执行:if __name__ == "__main__": env = MazeEnv() obs = env.reset() env.render() done = False step_num = 1 action_list = ["UP", "DOWN", "LEFT", "RIGHT"] # Run one episode while not done: # Sample a random action from the action space action = env.action_space.sample() next_obs, reward, done = env.step(action) print( f"step#:{step_num} action:\ {action_list[action]} reward:{reward} \ done:{done}" ) step_num += 1 env.render() env.close()
-
至此,我们已经准备好了!迷宫环境已经准备好,我们可以通过运行脚本(
python envs/maze.py
)快速测试它。将显示类似于以下的输出:
图 2.2 – 迷宫环境的文本表示,突出显示并下划线当前状态
让我们看看它是如何工作的。
它是如何工作的…
我们的map
,如如何做…部分中的步骤 1所定义,表示学习环境的状态。迷宫环境定义了观察空间、动作空间和奖励机制,实现env.render()
方法将环境的内部网格表示转换为简单的文本/字符串网格,并打印出来以便于可视化理解。
构建基于价值的强化学习代理算法
基于价值的强化学习通过学习给定环境中的状态值函数或动作值函数来工作。此配方将向您展示如何为迷宫环境创建和更新价值函数,以获得最优策略。在模型无法使用的无模型强化学习问题中,学习价值函数,尤其是在低维状态空间的强化学习问题中,可能非常有效。
完成此配方后,您将拥有一个算法,可以根据价值函数生成以下最优动作序列:
图 2.3 – 基于价值的强化学习算法生成的最优动作序列,状态值通过喷气色彩图表示
让我们开始吧。
准备工作
要完成此配方,您需要激活tf2rl-cookbook
Python/conda 虚拟环境并运行pip install numpy gym
。如果以下导入语句运行没有问题,您就可以开始了:
import numpy as np
现在,我们可以开始了。
如何做…
让我们实现一个基于价值迭代的价值函数学习算法。我们将使用迷宫环境来实现并分析价值迭代算法。
按照以下步骤实施此配方:
-
从
envs.maze
导入迷宫学习环境:from envs.maze import MazeEnv
-
创建
MazeEnv
实例并打印观察空间和动作空间:env = MazeEnv() print(f"Observation space: {env.observation_space}") print(f"Action space: {env.action_space}")
-
让我们定义状态维度,以便初始化
state-values
、state-action values
和我们的策略:state_dim = env.distinct_states state_values = np.zeros(state_dim) q_values = np.zeros((state_dim, env.action_space.n)) policy = np.zeros(state_dim)
-
现在,我们准备实现一个函数,当给定环境中的状态和一个动作时,能够计算状态/动作值。我们将从声明
calculate_values
函数开始;我们将在接下来的步骤中完成实现:def calculate_values(state, action): """Evaluate Value function for given state and action Args: state (int): Valid (discrete) state in discrete \ `env.observation_space` action (int): Valid (discrete) action in \ `env.action_space` Returns: v_sum: value for given state, action """
-
下一步,我们将生成
slip_action
,这是一个基于学习环境随机性的随机动作:slip_action = env.slip_action_map[action]
-
在计算给定状态-动作对的值时,能够在执行动作前设置环境状态,以便观察奖励/结果是很重要的。迷宫环境提供了一个方便的
set_state
方法来设置当前的环境状态。让我们利用它,按所需的(输入)动作一步步执行环境:env.set_state(state) slip_next_state, slip_reward, _ = \ env.step(slip_action, slip=False)
-
我们需要一个环境中的转换列表,以便根据贝尔曼方程计算奖励。让我们创建一个
transitions
列表,并附加新获得的环境转换信息:transitions = [] transitions.append((slip_reward, slip_next_state, env.slip))
-
让我们通过状态和动作获取另一个转换,这一次不考虑随机性。我们可以通过不使用
slip_action
并将slip=False
来在迷宫环境中执行:env.set_state(state) next_state, reward, _ = env.step(action, slip=False) transitions.append((reward, next_state, 1 - env.slip))
-
只需再执行一步,即可完成
calculate_values
函数,那就是计算值:for reward, next_state, pi in transitions: v_sum += pi * (reward + discount * \ state_values[next_state]) return v_sum
-
现在,我们可以开始实现状态/动作值学习了。我们将从定义
max_iteration
超参数开始:# Define the maximum number of iterations per learning # step max_iteration = 1000
-
让我们使用价值迭代来实现
state-value
函数学习循环:for i in range(iters): v_s = np.zeros(state_dim) for state in range(state_dim): if env.index_to_coordinate_map[int(state / 8)]==\ env.goal_pos: continue v_max = float("-inf") for action in range(env.action_space.n): v_sum = calculate_values(state, action) v_max = max(v_max, v_sum) v_s[state] = v_max state_values = np.copy(v_s)
-
现在我们已经实现了
state-value
函数学习循环,接下来让我们继续实现action-value
函数:for state in range(state_dim): for action in range(env.action_space.n): q_values[state, action] = calculate_values(state, action)
-
计算出
action-value
函数后,我们离获得最优策略只差一步了。让我们去实现它吧!for state in range(state_dim): policy[state] = np.argmax(q_values[state, :])
-
我们可以使用以下代码行打印 Q 值(
state-action
值)和策略:print(f"Q-values: {q_values}") print("Action mapping:0 - UP; 1 - DOWN; 2 - LEFT; \ 3 - RIGHT") print(f"optimal_policy: {policy}")
-
最后一步,让我们可视化价值函数的学习和策略更新:
from value_function_utils import viusalize_maze_values viusalize_maze_values(q_values, env)
上述代码将生成以下图示,显示在学习过程中价值函数的进展以及策略更新:
![图 2.4 – 学习到的价值函数和策略的进展(从左到右,从上到下)图 2.4 – 学习到的价值函数和策略的进展(从左到右,从上到下)## 它是如何工作的……迷宫环境包含一个起始单元、一个目标单元,以及一些包含金币、墙壁和空地的单元。由于金币单元的不同性质,迷宫环境中有 112 个不同的状态。为了说明,当代理收集其中一个金币时,环境与代理收集另一个金币时的状态完全不同。这是因为金币的位置也很重要。q_values
(状态-动作值)是一个 112 x 4 的大矩阵,因此它会打印出一长串值。我们在这里不展示这些。第 14 步中的其他两个打印语句应产生类似以下的输出:
图 2.5 – 最优动作序列的文本表示
基于值迭代的值函数学习遵循贝尔曼方程,最优策略是通过从 Q 值函数中选择 Q/动作值最高的动作来获得的。
在图 2.4中,值函数使用喷射色图表示,而策略则通过绿色箭头表示。最初,状态的值几乎相等。随着学习进展,拥有硬币的状态比没有硬币的状态更有价值,指向目标的状态获得了一个非常高的值,只有略低于目标状态本身。迷宫中的黑色单元表示墙壁。箭头表示策略在给定迷宫单元格中的指导行动。随着学习的收敛,如右下方的图所示,策略达到了最优,指引代理在收集完所有硬币后到达目标。
重要说明
本书中的彩色版本图表可供下载。你可以在本书的前言部分找到这些图表的链接。
实现时间差学习
本食谱将引导你实现时间差(TD)学习算法。TD 算法使我们能够从代理体验的不完整回合中逐步学习,这意味着它们可以用于需要在线学习能力的问题。TD 算法在无模型强化学习(RL)环境中非常有用,因为它们不依赖于 MDP 转换或奖励的模型。为了更直观地理解 TD 算法的学习进展,本食谱还将展示如何实现 GridworldV2 学习环境,该环境在渲染时如下所示:
图 2.6 – 带有状态值和网格单元坐标的 GridworldV2 学习环境 2D 渲染
准备工作
要完成本食谱,你需要激活tf2rl-cookbook
Python/conda 虚拟环境,并运行pip install numpy gym
。如果以下导入语句没有问题,则可以开始了:
import gym
import matplotlib.pyplot as plt
import numpy as np
现在,我们可以开始了。
如何操作…
本食谱将包含两个组件,最后我们将把它们结合起来。第一个组件是 GridworldV2 的实现,第二个组件是 TD 学习算法的实现。让我们开始吧:
-
我们将首先实现 GridworldV2,然后定义
GridworldV2Eng
类:class GridworldV2Env(gym.Env): def __init__(self, step_cost=-0.2, max_ep_length=500, explore_start=False): self.index_to_coordinate_map = { "0": [0, 0], "1": [0, 1], "2": [0, 2], "3": [0, 3], "4": [1, 0], "5": [1, 1], "6": [1, 2], "7": [1, 3], "8": [2, 0], "9": [2, 1], "10": [2, 2], "11": [2, 3], } self.coordinate_to_index_map = { str(val): int(key) for key, val in self.index_to_coordinate_map.items() }
-
在此步骤中,你将继续实现
__init__
方法,并定义必要的值,这些值将定义 Gridworld 的大小、目标位置、墙壁位置以及炸弹的位置等:self.map = np.zeros((3, 4)) self.observation_space = gym.spaces.Discrete(1) self.distinct_states = [str(i) for i in \ range(12)] self.goal_coordinate = [0, 3] self.bomb_coordinate = [1, 3] self.wall_coordinate = [1, 1] self.goal_state = self.coordinate_to_index_map[ str(self.goal_coordinate)] # 3 self.bomb_state = self.coordinate_to_index_map[ str(self.bomb_coordinate)] # 7 self.map[self.goal_coordinate[0]]\ [self.goal_coordinate[1]] = 1 self.map[self.bomb_coordinate[0]]\ [self.bomb_coordinate[1]] = -1 self.map[self.wall_coordinate[0]]\ [self.wall_coordinate[1]] = 2 self.exploring_starts = explore_start self.state = 8 self.done = False self.max_ep_length = max_ep_length self.steps = 0 self.step_cost = step_cost self.action_space = gym.spaces.Discrete(4) self.action_map = {"UP": 0, "RIGHT": 1, "DOWN": 2, "LEFT": 3} self.possible_actions = \ list(self.action_map.values())
-
现在,我们可以继续定义
reset()
方法,该方法将在每个回合开始时调用,包括第一个回合:def reset(self): self.done = False self.steps = 0 self.map = np.zeros((3, 4)) self.map[self.goal_coordinate[0]]\ [self.goal_coordinate[1]] = 1 self.map[self.bomb_coordinate[0]]\ [self.bomb_coordinate[1]] = -1 self.map[self.wall_coordinate[0]]\ [self.wall_coordinate[1]] = 2 if self.exploring_starts: self.state = np.random.choice([0, 1, 2, 4, 6, 8, 9, 10, 11]) else: self.state = 8 return self.state
-
让我们实现一个
get_next_state
方法,这样我们就可以方便地获取下一个状态:def get_next_state(self, current_position, action): next_state = self.index_to_coordinate_map[ str(current_position)].copy() if action == 0 and next_state[0] != 0 and \ next_state != [2, 1]: # Move up next_state[0] -= 1 elif action == 1 and next_state[1] != 3 and \ next_state != [1, 0]: # Move right next_state[1] += 1 elif action == 2 and next_state[0] != 2 and \ next_state != [0, 1]: # Move down next_state[0] += 1 elif action == 3 and next_state[1] != 0 and \ next_state != [1, 2]: # Move left next_state[1] -= 1 else: pass return self.coordinate_to_index_map[str( next_state)]
-
这样,我们就可以准备实现
GridworldV2
环境的主要step
方法:def step(self, action): assert action in self.possible_actions, \ f"Invalid action:{action}" current_position = self.state next_state = self.get_next_state( current_position, action) self.steps += 1 if next_state == self.goal_state: reward = 1 self.done = True elif next_state == self.bomb_state: reward = -1 self.done = True else: reward = self.step_cost if self.steps == self.max_ep_length: self.done = True self.state = next_state return next_state, reward, self.done
-
现在,我们可以继续实现时序差分学习算法。我们首先通过初始化一个二维
numpy
数组来设置网格的状态值,然后设置目标位置和炸弹状态的值:def temporal_difference_learning(env, max_episodes): grid_state_values = np.zeros((len( env.distinct_states), 1)) grid_state_values[env.goal_state] = 1 grid_state_values[env.bomb_state] = -1
-
接下来,让我们定义折扣因子
gamma
、学习率alpha
,并将done
初始化为False
:# v: state-value function v = grid_state_values gamma = 0.99 # Discount factor alpha = 0.01 # learning rate done = False
-
现在,我们可以定义主要的外部循环,使其运行
max_episodes
次,在每个回合开始时重置环境的状态到初始状态:for episode in range(max_episodes): state = env.reset()
-
现在,是时候实现带有时序差分学习更新的内部循环一行代码了:
while not done: action = env.action_space.sample() # random policy next_state, reward, done = env.step(action) # State-value function updates using TD(0) v[state] += alpha * (reward + gamma * \ v[next_state] - v[state]) state = next_state
-
一旦学习已经收敛,我们希望能够可视化 GridwordV2 环境中每个状态的状态值。为此,我们可以利用
value_function_utils
中的visualize_grid_state_values
函数:visualize_grid_state_values(grid_state_values.reshape((3, 4)))
-
我们现在准备从主函数中运行
temporal_difference_learning
函数:if __name__ == "__main__": max_episodes = 4000 env = GridworldV2Env(step_cost=-0.1, max_ep_length=30) temporal_difference_learning(env, max_episodes)
-
上述代码将花费几秒钟时间进行
max_episodes
的时序差分学习。然后,它将生成一个类似于以下的图示:
图 2.7 – 渲染 GridworldV2 环境,网格单元的坐标和状态值根据右侧显示的尺度进行着色
它是如何工作的……
根据我们环境的实现,你可能已经注意到goal_state
位于(0, 3)
,而bomb_state
位于(1, 3)
。这基于网格单元的坐标、颜色和数值:
图 2.8 – 渲染 GridWorldV2 环境,带有初始状态值
状态被线性化,并使用一个整数表示 GridWorldV2 环境中每个 12 个不同状态。以下图示展示了网格状态的线性化渲染,帮助你更好地理解状态编码:
图 2.9 – 状态的线性化表示
现在我们已经了解了如何实现时序差分学习,接下来让我们开始构建蒙特卡洛算法。
构建蒙特卡洛预测和控制算法
这份食谱提供了构建 蒙特卡洛 预测与控制算法的基本材料,帮助你构建 RL 智能体。与时序差分学习算法类似,蒙特卡洛学习方法可以用来学习状态和动作值函数。蒙特卡洛方法没有偏差,因为它们从完整的回合中学习真实的经验,而没有近似预测。这些方法适用于需要良好收敛性特征的应用。以下图示展示了蒙特卡洛方法在 GridworldV2 环境中学习到的值:
图 2.10 – 蒙特卡洛预测的状态值(左)和状态-动作值(右)
准备就绪
要完成这个步骤,你需要激活 tf2rl-cookbook
Python/conda 虚拟环境,并运行 pip install -r requirements.txt
。如果以下导入语句能够顺利运行,那么你就可以开始了:
import numpy as np
现在,让我们开始。
如何实现…
我们将从实现 monte_carlo_prediction
算法并可视化 GridworldV2
环境中每个状态的学习值函数开始。之后,我们将实现 monte_carlo_control
算法,构建一个在 RL 环境中进行决策的智能体。
按照以下步骤进行:
-
让我们从导入语句开始,并导入必要的 Python 模块:
import numpy as np from envs.gridworldv2 import GridworldV2Env from value_function_utils import ( visualize_grid_action_values, visualize_grid_state_values, )
-
下一步是定义
monte_carlo_prediction
函数,并初始化所需的对象,如下所示:def monte_carlo_prediction(env, max_episodes): returns = {state: [] for state in \ env.distinct_states} grid_state_values = np.zeros(len( env.distinct_states)) grid_state_values[env.goal_state] = 1 grid_state_values[env.bomb_state] = -1 gamma = 0.99 # Discount factor
-
现在,让我们实现外层循环。外层循环在所有强化学习智能体训练代码中都很常见:
for episode in range(max_episodes): g_t = 0 state = env.reset() done = False trajectory = []
-
接下来是内层循环:
while not done: action = env.action_space.sample() # random policy next_state, reward, done = env.step(action) trajectory.append((state, reward)) state = next_state
-
我们现在拥有计算网格中状态值所需的所有信息:
for idx, (state, reward) in enumerate(trajectory[::-1]): g_t = gamma * g_t + reward # first visit Monte-Carlo prediction if state not in np.array(trajectory[::-1])\ [:, 0][idx + 1 :]: returns[str(state)].append(g_t) grid_state_values[state] = np.mean(returns[str(state)]) Let's visualize the learned state value function using the visualize_grid_state_values helper function from the value_function_utils script: visualize_grid_state_values(grid_state_values.reshape((3, 4)))
-
现在,是时候运行我们的蒙特卡洛预测器了:
if __name__ == "__main__": max_episodes = 4000 env = GridworldV2Env(step_cost=-0.1, max_ep_length=30) print(f"===Monte Carlo Prediction===") monte_carlo_prediction(env, max_episodes)
-
上述代码应生成一个图示,显示 GridworldV2 环境的渲染结果,以及状态值:
图 2.11 – 使用蒙特卡洛预测算法学习的 GridworldV2 状态值渲染图
-
让我们实现一个 epsilon-贪婪策略的函数:
def epsilon_greedy_policy(action_logits, epsilon=0.2): idx = np.argmax(action_logits) probs = [] epsilon_decay_factor = np.sqrt(sum([a ** 2 for a in \ action_logits])) if epsilon_decay_factor == 0: epsilon_decay_factor = 1.0 for i, a in enumerate(action_logits): if i == idx: probs.append(round(1 - epsilon + ( epsilon / epsilon_decay_factor), 3)) else: probs.append(round( epsilon / epsilon_decay_factor, 3)) residual_err = sum(probs) - 1 residual = residual_err / len(action_logits) return np.array(probs) - residual
-
现在,让我们进入 蒙特卡洛控制 算法的实现,用于强化学习。我们将从定义函数并为状态-动作值初始化初始值开始:
def monte_carlo_control(env, max_episodes): grid_state_action_values = np.zeros((12, 4)) grid_state_action_values[3] = 1 grid_state_action_values[7] = -1
-
让我们继续实现蒙特卡洛控制函数,通过初始化所有可能的状态-动作对的回报值:
possible_states = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"] possible_actions = ["0", "1", "2", "3"] returns = {} for state in possible_states: for action in possible_actions: returns[state + ", " + action] = []
-
作为下一步,让我们为每个回合定义外层循环,然后为回合中的每个步骤定义内层循环。通过这样做,我们可以收集经验轨迹,直到回合结束:
gamma = 0.99 for episode in range(max_episodes): g_t = 0 state = env.reset() trajectory = [] while True: action_values = \ grid_state_action_values[state] probs = epsilon_greedy_policy(action_values) action = np.random.choice(np.arange(4), \ p=probs) # random policy next_state, reward, done = env.step(action) trajectory.append((state, action, reward)) state = next_state if done: break
-
现在我们有了一个完整的内循环轨迹,我们可以实施蒙特卡洛控制更新,以更新状态-行动值:
for step in reversed(trajectory): g_t = gamma * g_t + step[2] Returns[str(step[0]) + ", " + \ str(step[1])].append(g_t) grid_state_action_values[step[0]][step[1]]= \ np.mean( Returns[str(step[0]) + ", " + \ str(step[1])] )
-
外部循环完成后,我们可以使用
value_function_utils
脚本中的visualize_grid_action_values
辅助函数来可视化状态-行动值:visualize_grid_action_values(grid_state_action_values
-
最后,让我们运行
monte_carlo_control
函数来学习 GridworldV2 环境中的状态-行动
值,并展示学习到的值:if __name__ == "__main__": max_episodes = 4000 env = GridworldV2Env(step_cost=-0.1, \ max_ep_length=30) print(f"===Monte Carlo Control===") monte_carlo_control(env, max_episodes)
前面的代码将生成如下所示的渲染结果:
图 2.12 – 显示每个网格状态下的四个行动值的 GridworldV2 环境渲染图
这就是本食谱的全部内容!
它是如何工作的……
用于周期性任务的蒙特卡洛方法直接从经验中学习,基于一个完整的样本回报在一个回合中获得的回报。基于第一次访问平均的蒙特卡洛预测算法估算价值函数如下:
图 2.13 – 蒙特卡洛预测算法
一旦智能体收集到一系列轨迹,我们可以使用蒙特卡洛控制算法中的过渡信息来学习状态-行动值函数。这可以被智能体用来在给定的 RL 环境中进行决策。
以下图展示了蒙特卡洛控制算法:
图 2.14 – 蒙特卡洛控制算法
学习到的状态-行动值函数的结果显示在图 2.12中,其中网格单元中的每个三角形表示在该网格状态下采取该方向行动的状态-行动值。三角形的底部朝向行动的方向。例如,图 2.12左上角的三角形,值为 0.44,表示在该网格状态下采取“向左”行动的状态-行动值。
实现 SARSA 算法和强化学习智能体
本食谱将展示如何实现状态-行动-奖励-状态-行动(SARSA)算法,以及如何使用 SARSA 算法开发和训练一个智能体,使其能够在强化学习环境中执行任务。SARSA 算法可以应用于无模型控制问题,并允许我们优化一个未知 MDP 的价值函数。
完成这个食谱后,你将得到一个工作中的强化学习(RL)智能体,当它在 GridworldV2 环境中运行时,将使用 SARSA 算法生成如下的状态-行动值函数:
图 2.15 – GridworldV2 环境渲染 – 每个三角形表示在该网格状态下执行该方向动作的动作值
准备工作
要完成此实例,你需要激活 tf2rl-cookbook
Python/conda 虚拟环境,并运行 pip install -r requirements.txt
。如果以下导入语句没有问题,就可以开始了:
import numpy as np
import random
现在,让我们开始吧。
如何做到…
让我们将 SARSA 学习更新实现为一个函数,并利用 epsilon-greedy 探索策略。将这两部分结合后,我们将拥有一个完整的智能体,用于在给定的强化学习环境中进行动作。在本实例中,我们将在 GridworldV2 环境中训练并测试智能体。
让我们一步一步开始实现:
-
首先,让我们定义一个函数来实现 SARSA 算法,并用零初始化状态-动作值:
def sarsa(env, max_episodes): grid_action_values = np.zeros((len( env.distinct_states), env.action_space.n))
-
现在,我们可以根据环境的配置更新目标状态和炸弹状态的值:
grid_action_values[env.goal_state] = 1 grid_action_values[env.bomb_state] = -1
-
让我们定义折扣因子
gamma
和学习率超参数alpha
。同时,为了方便起见,我们将grid_action_values
创建一个别名,命名为q
:gamma = 0.99 # discounting factor alpha = 0.01 # learning rate # q: state-action-value function q = grid_action_values
-
让我们开始一步一步地实现外循环:
for episode in range(max_episodes): step_num = 1 done = False state = env.reset() action = greedy_policy(q[state], 1)
-
现在,是时候实现 SARSA 学习更新步骤中的内循环了:
while not done: next_state, reward, done = env.step(action) step_num += 1 decayed_epsilon = gamma ** step_num # Doesn't have to be gamma next_action = greedy_policy(q[next_state], \ decayed_epsilon) q[state][action] += alpha * ( reward + gamma * q[next_state] \ [next_action] - q[state][action] ) state = next_state action = next_action
-
作为
sarsa
函数的最后一步,让我们可视化状态-动作值函数:visualize_grid_action_values(grid_action_values)
-
现在,我们将实现智能体将使用的 epsilon-greedy 策略:
def greedy_policy(q_values, epsilon): """Epsilon-greedy policy """ if random.random() >= epsilon: return np.argmax(q_values) else: return random.randint(0, 3)
-
最后,我们必须实现主函数并运行 SARSA 算法:
if __name__ == "__main__": max_episodes = 4000 env = GridworldV2Env(step_cost=-0.1, \ max_ep_length=30) sarsa(env, max_episodes)
执行后,将出现 GridworldV2 环境的渲染图,状态-动作值将如以下图所示:
图 2.16 – SARSA 算法在 GridworldV2 环境中的输出
它是如何工作的…
SARSA 是一种基于策略的时序差分学习控制算法。本实例使用 SARSA 算法来估计最优的状态-动作值。SARSA 算法可以总结如下:
图 2.17 – SARSA 算法
如你所见,这与 Q-learning 算法非常相似。当我们查看本章的下一个实例 构建 Q-learning 智能体 时,相似之处将更加清晰。
构建一个 Q-learning 智能体
本实例将展示如何构建一个Q-learning智能体。Q-learning 可以应用于无模型的强化学习问题。它支持离策略学习,因此为那些使用其他策略或其他智能体(甚至人类)收集的经验提供了实际解决方案。
完成本实例后,你将拥有一个有效的强化学习智能体,该智能体在 GridworldV2 环境中执行时,将使用 SARSA 算法生成以下状态-动作值函数:
图 2.18 – 使用 Q 学习算法获得的状态-动作值
准备就绪
要完成此配方,您需要激活 tf2rl-cookbook
Python/conda 虚拟环境,并运行 pip install -r requirements.txt
。如果以下导入语句没有问题,您就可以开始了:
import numpy as np
import random
现在,让我们开始。
如何实现…
让我们将 Q 学习算法作为一个函数实现,同时实现 epsilon-greedy 策略,以构建我们的 Q 学习代理。
让我们开始实现:
-
首先,让我们定义一个函数来实现 Q 学习算法,并将状态-动作值初始化为零:
def q_learning(env, max_episodes): grid_action_values = np.zeros((len(\ env.distinct_states), env.action_space.n))
-
现在我们可以根据环境配置更新目标状态和炸弹状态的值:
grid_action_values[env.goal_state] = 1 grid_action_values[env.bomb_state] = -1
-
让我们定义折扣因子
gamma
和学习率超参数alpha
。同时,让我们为grid_action_values
创建一个方便的别名,称其为q
:gamma = 0.99 # discounting factor alpha = 0.01 # learning rate # q: state-action-value function q = grid_action_values
-
让我们开始实现外部循环:
for episode in range(max_episodes): step_num = 1 done = False state = env.reset()
-
下一步,让我们实现带有 Q 学习更新的内部循环。同时,我们还将衰减在 epsilon-greedy 策略中使用的 epsilon:
while not done: decayed_epsilon = 1 * gamma ** step_num # Doesn't have to be gamma action = greedy_policy(q[state], \ decayed_epsilon) next_state, reward, done = env.step(action) # Q-Learning update grid_action_values[state][action] += alpha *( reward + gamma * max(q[next_state]) - \ q[state][action] ) step_num += 1 state = next_state
-
在
q_learning
函数的最后一步,让我们可视化状态-动作值函数:visualize_grid_action_values(grid_action_values)
-
接下来,我们将实现代理将使用的 epsilon-greedy 策略:
def greedy_policy(q_values, epsilon): """Epsilon-greedy policy """ if random.random() >= epsilon: return np.argmax(q_values) else: return random.randint(0, 3)
-
最后,我们将实现主函数并运行 SARSA 算法:
if __name__ == "__main__": max_episodes = 4000 env = GridworldV2Env(step_cost=-0.1, max_ep_length=30) q_learning(env, max_episodes)
执行时,将显示带有状态-动作值的 GridworldV2 环境渲染,如下图所示:
图 2.19 – 使用 Q 学习算法获得的动作值渲染的 GridworldV2 环境
它是如何工作的…
Q 学习算法涉及 Q 值更新,可以通过以下方程式总结:
这里,我们有以下内容:
-
是当前状态 s 和动作 a 的 Q 函数值。
-
用于从可能的下一步中选择最大值。
-
是代理的当前位置。
-
是当前动作。
-
是学习率。
-
是在当前状态下获得的奖励。
-
是 gamma(奖励衰减,折扣因子)。
-
是下一个状态。
-
是下一个状态下可用的动作,
。
如你现在可能已经能看出,Q-learning 和 SARSA 之间的区别仅在于如何计算下一状态和动作对的动作值/Q 值。在 Q-learning 中,我们使用 ,即 Q 函数的最大值,而在 SARSA 算法中,我们选择的是下一状态中所选择动作的 Q 值。这听起来可能很微妙,但因为 Q-learning 算法是通过对所有动作的最大值进行推断,而不仅仅是基于当前的行为策略进行推断,它可以直接学习最优策略。另一方面,SARSA 算法基于行为策略的探索参数(例如,ε-greedy 策略中的 ε 参数)学习近似最优策略。SARSA 算法比 Q-learning 算法具有更好的收敛性,因此更适用于在线学习或现实世界系统中的情况,或者即使有真实资源(时间和/或金钱)被投入,也比在模拟或模拟世界中训练更为合适。而 Q-learning 更适合在模拟中训练“最优”智能体,或者当资源(如时间/金钱)不那么昂贵时。
实现策略梯度
策略梯度算法是强化学习的基础,并且是许多高级 RL 算法的基础。这些算法直接优化最佳策略,相较于基于价值的算法,它们可以更快地学习。策略梯度算法对具有高维或连续动作空间的问题/应用有效。本教程将向你展示如何使用 TensorFlow 2.0 实现策略梯度算法。完成本教程后,你将能够在任何兼容的 OpenAI Gym 环境中训练 RL 智能体。
准备就绪
要完成这个教程,你需要激活tf2rl-cookbook
Python/conda 虚拟环境并运行pip install -r requirements.txt
。如果以下导入语句没有问题,那么你就准备好开始了:
import tensorflow as tf
import tensorflow_probability as tfp
from tensorflow import keras
from tensorflow.keras import layers
import numpy as np
import gym
现在,让我们开始吧。
如何实现…
这个教程有三个主要部分。第一个部分是应用策略函数,它将通过在 TensorFlow 2.x 中实现的神经网络来表示。第二部分是应用 Agent 类的实现,而最后一部分是应用训练函数,用于在给定的 RL 环境中训练基于策略梯度的智能体。
让我们逐步实现各个部分:
-
第一步是定义
PolicyNet
类。我们将定义模型,使其具有三层全连接或密集的神经网络层:class PolicyNet(keras.Model): def __init__(self, action_dim=1): super(PolicyNet, self).__init__() self.fc1 = layers.Dense(24, activation="relu") self.fc2 = layers.Dense(36, activation="relu") self.fc3 = layers.Dense(action_dim, activation="softmax")
-
接下来,我们将实现
call
函数,它将被调用来处理模型的输入:def call(self, x): x = self.fc1(x) x = self.fc2(x) x = self.fc3(x) return x
-
让我们还定义一个
process
函数,我们可以使用它来处理一批观测数据,并由模型进行处理:def process(self, observations): # Process batch observations using `call(x)` # behind-the-scenes action_probabilities = \ self.predict_on_batch(observations) return action_probabilities
-
定义好策略网络后,我们可以实现
Agent
类,它利用该策略网络,并使用优化器来训练模型:class Agent(object): def __init__(self, action_dim=1): """Agent with a neural-network brain powered policy Args: action_dim (int): Action dimension """ self.policy_net = PolicyNet( action_dim=action_dim) self.optimizer = tf.keras.optimizers.Adam( learning_rate=1e-3) self.gamma = 0.99
-
现在,让我们定义一个策略辅助函数,它接受一个观测作为输入,通过策略网络处理后返回动作作为输出:
def policy(self, observation): observation = observation.reshape(1, -1) observation = tf.convert_to_tensor(observation, dtype=tf.float32) action_logits = self.policy_net(observation) action = tf.random.categorical( tf.math.log(action_logits), num_samples=1) return action
-
让我们定义另一个辅助函数来从代理那里获取动作:
def get_action(self, observation): action = self.policy(observation).numpy() return action.squeeze()
-
现在,是时候定义策略梯度算法的学习更新了。让我们初始化
learn
函数,并为折扣奖励创建一个空列表:def learn(self, states, rewards, actions): discounted_reward = 0 discounted_rewards = [] rewards.reverse()
-
这是计算折扣奖励的正确位置,同时使用回合奖励作为输入:
for r in rewards: discounted_reward = r + self.gamma * \ discounted_reward discounted_rewards.append(discounted_reward) discounted_rewards.reverse()
-
现在,让我们实现计算策略梯度的关键步骤,并使用优化器更新神经网络策略的参数:
for state, reward, action in zip(states, discounted_rewards, actions): with tf.GradientTape() as tape: action_probabilities = \ self.policy_net(np.array([state]),\ training=True) loss = self.loss(action_probabilities, \ action, reward) grads = tape.gradient(loss, self.policy_net.trainable_variables) self.optimizer.apply_gradients( zip(grads, self.policy_net.trainable_variables) )
-
让我们实现前一步中提到的损失函数,以计算策略参数更新:
def loss(self, action_probabilities, action, reward): dist = tfp.distributions.Categorical( probs=action_probabilities, dtype=tf.float32 ) log_prob = dist.log_prob(action) loss = -log_prob * reward return loss
-
代理类完全实现后,我们可以继续实现代理训练函数。让我们从函数定义开始:
def train(agent: Agent, env: gym.Env, episodes: int, render=True): """Train `agent` in `env` for `episodes` Args: agent (Agent): Agent to train env (gym.Env): Environment to train the agent episodes (int): Number of episodes to train render (bool): True=Enable/False=Disable \ rendering; Default=True """
-
现在,让我们开始实现代理训练函数的外部循环:
for episode in range(episodes): done = False state = env.reset() total_reward = 0 rewards = [] states = [] actions = []
-
让我们继续实现内部循环,完成
train
函数:while not done: action = agent.get_action(state) next_state, reward, done, _ = \ env.step(action) rewards.append(reward) states.append(state) actions.append(action) state = next_state total_reward += reward if render: env.render() if done: agent.learn(states, rewards, actions) print("\n") print(f"Episode#:{episode} \ ep_reward:{total_reward}", end="\r")
-
最后,我们需要实现主函数:
if __name__ == "__main__": agent = Agent() episodes = 5000 env = gym.make("MountainCar-v0") train(agent, env, episodes) env.close()
上述代码将启动代理的训练过程(
render=True
),并展示代理在环境中进行的操作,即驾驶汽车向山上行驶。一旦代理经过足够多的训练轮次,你将看到代理成功地将汽车一路开上山顶,如下图所示:
图 2.20 – 策略梯度代理完成 MountainCar 任务
这就是本章节的全部内容!
它的工作原理…
我们使用了 TensorFlow 2.x 的 MountainCar
强化学习环境。策略梯度算法如图所示:
图 2.21 – 策略梯度算法
当你训练基于策略梯度的代理时,你会发现,尽管代理能够学习如何将汽车开上山,但这个过程可能非常漫长,或者它们可能会陷入局部最小值。这个基本版本的策略梯度有一些局限性。策略梯度是一种基于策略的算法,只能使用来自同一策略的经验/轨迹或回合转换,这个策略正在被优化。基本版本的策略梯度算法不能保证性能的单调提升,因为它可能会陷入局部最小值。
实现演员-评论员强化学习算法
Actor-critic 算法使我们能够结合基于值的方法和基于策略的方法进行强化学习——一个全能型的智能体。虽然策略梯度方法直接搜索并优化策略空间中的策略,导致更平滑的学习曲线和改进保证,但它们往往会卡在局部最大值(针对长期奖励优化目标)。基于值的方法不会卡在局部最优值,但它们缺乏收敛保证,像 Q-learning 这样的算法往往有较大的方差,并且样本效率较低。Actor-critic 方法结合了基于值的方法和基于策略梯度方法的优点。Actor-critic 方法的样本效率也更高。本教程将帮助你轻松实现一个基于 actor-critic 的强化学习智能体,使用 TensorFlow 2.x。在完成本教程后,你将能够在任何 OpenAI Gym 兼容的强化学习环境中训练 actor-critic 智能体。作为示例,我们将在 CartPole-V0 环境中训练该智能体。
准备工作
为了完成这个过程,你需要激活tf2rl-cookbook
的 Python/conda 虚拟环境,并运行pip install -r requirements.txt
。如果以下的导入语句没有问题,那么你就可以开始了:
import numpy as np
import tensorflow as tf
import gym
import tensorflow_probability as tfp
现在,开始吧。
如何实现…
这个过程主要有三个部分。第一部分是创建 actor-critic 模型,这将通过一个在 TensorFlow 2.x 中实现的神经网络表示。第二部分是实现 Agent 类,而最后一部分则是创建一个训练器函数,用于在给定的 RL 环境中训练基于策略梯度的智能体。
让我们一步一步地开始实现各个部分:
-
让我们从实现
ActorCritic
类开始:class ActorCritic(tf.keras.Model): def __init__(self, action_dim): super().__init__() self.fc1 = tf.keras.layers.Dense(512, \ activation="relu") self.fc2 = tf.keras.layers.Dense(128, \ activation="relu") self.critic = tf.keras.layers.Dense(1, \ activation=None) self.actor = tf.keras.layers.Dense(action_dim, \ activation=None)
-
在
ActorCritic
类中,我们需要做的最后一件事是实现call
函数,它执行神经网络模型的前向传递:def call(self, input_data): x = self.fc1(input_data) x1 = self.fc2(x) actor = self.actor(x1) critic = self.critic(x1) return critic, actor
-
定义了
ActorCritic
类后,我们可以继续实现Agent
类,并初始化一个ActorCritic
模型,连同一个优化器,用来更新 actor-critic 模型的参数:class Agent: def __init__(self, action_dim=4, gamma=0.99): """Agent with a neural-network brain powered policy Args: action_dim (int): Action dimension gamma (float) : Discount factor. Default=0.99 """ self.gamma = gamma self.opt = tf.keras.optimizers.Adam( learning_rate=1e-4) self.actor_critic = ActorCritic(action_dim)
-
接下来,我们必须实现智能体的
get_action
方法:def get_action(self, state): _, action_probabilities = \ self.actor_critic(np.array([state])) action_probabilities = tf.nn.softmax( action_probabilities) action_probabilities = \ action_probabilities.numpy() dist = tfp.distributions.Categorical( probs=action_probabilities, dtype=tf.float32 ) action = dist.sample() return int(action.numpy()[0])
-
现在,让我们实现一个函数,根据 actor-critic 算法计算 actor 的损失。这将推动 actor-critic 网络的参数更新,并使智能体不断改进:
def actor_loss(self, prob, action, td): prob = tf.nn.softmax(prob) dist = tfp.distributions.Categorical(probs=prob, dtype=tf.float32) log_prob = dist.log_prob(action) loss = -log_prob * td return loss
-
现在我们准备好实现 actor-critic 智能体的学习功能了:
def learn(self, state, action, reward, next_state, done): state = np.array([state]) next_state = np.array([next_state]) with tf.GradientTape() as tape: value, action_probabilities = \ self.actor_critic(state, training=True) value_next_st, _ = self.actor_critic( next_state, training=True) td = reward + self.gamma * value_next_st * \ (1 - int(done)) - value actor_loss = self.actor_loss( action_probabilities, action, td) critic_loss = td ** 2 total_loss = actor_loss + critic_loss grads = tape.gradient(total_loss, self.actor_critic.trainable_variables) self.opt.apply_gradients(zip(grads, self.actor_critic.trainable_variables)) return total_loss
-
现在,让我们定义训练函数,用于在给定的 RL 环境中训练智能体:
def train(agent, env, episodes, render=True): """Train `agent` in `env` for `episodes` Args: agent (Agent): Agent to train env (gym.Env): Environment to train the agent episodes (int): Number of episodes to train render (bool): True=Enable/False=Disable \ rendering; Default=True """ for episode in range(episodes): done = False state = env.reset() total_reward = 0 all_loss = [] while not done: action = agent.get_action(state) next_state, reward, done, _ = \ env.step(action) loss = agent.learn(state, action, reward, next_state, done) all_loss.append(loss) state = next_state total_reward += reward if render: env.render() if done: print("\n") print(f"Episode#:{episode} ep_reward:{total_reward}", end="\r")
-
最后一步是实现主函数,该函数将调用训练器来训练智能体,直到指定的训练轮次完成:
if __name__ == "__main__": env = gym.make("CartPole-v0") agent = Agent(env.action_space.n) num_episodes = 20000 train(agent, env, num_episodes)
一旦智能体得到了充分的训练,你将看到它能够很好地保持杆子在小车上平衡,如下图所示:
图 2.22 – 演员-评论员代理解决 CartPole 任务
它是如何工作的…
在这个例子中,我们使用 TensorFlow 2.x 的 Keras API 定义了一个基于神经网络的演员-评论员模型。在这个神经网络模型中,我们定义了两层全连接(密集)神经网络层,用于从输入中提取特征。这样产生了两个输出,分别对应演员和评论员的输出。评论员的输出是一个单一的浮动值,而演员的输出则表示给定强化学习环境中每个允许动作的 logits。
第三章:第三章:实现高级 RL 算法
本章提供了简短而清晰的食谱,帮助你使用 TensorFlow 2.x 从零开始实现先进的 强化学习(RL)算法和代理。包括构建 深度 Q 网络(DQN)、双重和决斗深度 Q 网络(DDQN,DDDQN)、深度递归 Q 网络(DRQN)、异步优势演员-评论家(A3C)、近端策略优化(PPO)和 深度确定性策略梯度(DDPG)的食谱。
本章将讨论以下食谱:
-
实现深度 Q 学习算法、DQN 和 Double-DQN 代理
-
实现决斗 DQN 代理
-
实现决斗双重 DQN 算法和 DDDQN 代理
-
实现深度递归 Q 学习算法和 DRQN 代理
-
实现异步优势演员-评论家算法和 A3C 代理
-
实现近端策略优化算法(Proximal Policy Optimization)和 PPO 代理
-
实现深度确定性策略梯度算法和 DDPG 代理
技术要求
书中的代码在 Ubuntu 18.04 和 Ubuntu 20.04 上经过广泛测试,并且如果安装了 Python 3.6+,应该可以在之后版本的 Ubuntu 上运行。安装 Python 3.6+ 以及之前章节所列的必要 Python 包后,代码也应该能够在 Windows 和 Mac OS X 上正常运行。建议创建并使用名为 tf2rl-cookbook
的 Python 虚拟环境来安装包并运行本书中的代码。推荐使用 Miniconda 或 Anaconda 安装 Python 虚拟环境进行管理。
每个章节中每个食谱的完整代码可以在此处获取:github.com/PacktPublishing/Tensorflow-2-Reinforcement-Learning-Cookbook
。
实现深度 Q 学习算法、DQN 和 Double-DQN 代理
DQN 代理使用深度神经网络学习 Q 值函数。DQN 已经证明自己是离散动作空间环境和问题中一种强大的算法,并且被认为是深度强化学习历史上的一个重要里程碑,当 DQN 掌握了 Atari 游戏时,成为了一个标志性的成果。
Double-DQN 代理使用两个相同的深度神经网络,它们的更新方式不同,因此权重也不同。第二个神经网络是从过去某一时刻(通常是上一轮)复制的主神经网络。
在本章节结束时,你将从零开始使用 TensorFlow 2.x 实现一个完整的 DQN 和 Double-DQN 代理,能够在任何离散动作空间的强化学习环境中进行训练。
让我们开始吧。
准备工作
要完成这个食谱,你首先需要激活 tf2rl-cookbook
Conda Python 虚拟环境,并运行 pip install -r requirements.txt
。如果以下导入语句没有问题,那么你就可以开始了!
import argparse
from datetime import datetime
import os
import random
from collections import deque
import gym
import numpy as np
import tensorflow as tf
from tensorflow.keras.layers import Dense, Input
现在我们可以开始了。
如何实现……
DQN 智能体包含几个组件,分别是DQN
类、Agent
类和train
方法。执行以下步骤,从零开始实现这些组件,构建一个完整的 DQN 智能体,使用 TensorFlow 2.x:
-
首先,让我们创建一个参数解析器来处理脚本的配置输入:
parser = argparse.ArgumentParser(prog="TFRL-Cookbook-Ch3-DQN") parser.add_argument("--env , default="CartPole-v0") parser.add_argument("--lr", type=float, default=0.005) parser.add_argument("--batch_size", type=int, default=256) parser.add_argument("--gamma", type=float, default=0.95) parser.add_argument("--eps", type=float, default=1.0) parser.add_argument("--eps_decay", type=float, default=0.995) parser.add_argument("--eps_min", type=float, default=0.01) parser.add_argument("--logdir", default="logs") args = parser.parse_args()
-
现在,让我们创建一个 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)
-
接下来,让我们实现一个
ReplayBuffer
类:class ReplayBuffer: def __init__(self, capacity=10000): self.buffer = deque(maxlen=capacity) def store(self, state, action, reward, next_state, done): self.buffer.append([state, action, reward, next_state, done]) def sample(self): sample = random.sample(self.buffer, args.batch_size) states, actions, rewards, next_states, done = \ map(np.asarray, zip(*sample)) states = np.array(states).reshape( args.batch_size, -1) next_states = np.array(next_states).\ reshape(args.batch_size, -1) return states, actions, rewards, next_states, done def size(self): return len(self.buffer)
-
现在是时候实现 DQN 类了,该类定义了 TensorFlow 2.x 中的深度神经网络:
class DQN: def __init__(self, state_dim, aciton_dim): self.state_dim = state_dim self.action_dim = aciton_dim self.epsilon = args.eps self.model = self.nn_model() def nn_model(self): model = tf.keras.Sequential( [ Input((self.state_dim,)), Dense(32, activation="relu"), Dense(16, activation="relu"), Dense(self.action_dim), ] ) model.compile(loss="mse", optimizer=Adam(args.lr)) return model
-
为了从 DQN 获取预测和动作,让我们实现
predict
和get_action
方法:def predict(self, state): return self.model.predict(state) def get_action(self, state): state = np.reshape(state, [1, self.state_dim]) self.epsilon *= args.eps_decay self.epsilon = max(self.epsilon, args.eps_min) q_value = self.predict(state)[0] if np.random.random() < self.epsilon: return random.randint(0, self.action_dim - 1) return np.argmax(q_value) def train(self, states, targets): self.model.fit(states, targets, epochs=1)
-
实现了其他组件后,我们可以开始实现我们的
Agent
类:class Agent: def __init__(self, env): self.env = env self.state_dim = \ self.env.observation_space.shape[0] self.action_dim = self.env.action_space.n self.model = DQN(self.state_dim, self.action_dim) self.target_model = DQN(self.state_dim, self.action_dim) self.update_target() self.buffer = ReplayBuffer() def update_target(self): weights = self.model.model.get_weights() self.target_model.model.set_weights(weights)
-
深度 Q 学习算法的核心是 q 学习更新和经验回放。让我们接下来实现它:
def replay_experience(self): for _ in range(10): states, actions, rewards, next_states, done=\ self.buffer.sample() targets = self.target_model.predict(states) next_q_values = self.target_model.\ predict(next_states).max(axis=1) targets[range(args.batch_size), actions] = ( rewards + (1 - done) * next_q_values * \ args.gamma ) self.model.train(states, targets)
-
下一步至关重要的是实现
train
函数来训练智能体:def train(self, max_episodes=1000): with writer.as_default(): # Tensorboard logging for ep in range(max_episodes): done, episode_reward = False, 0 observation = self.env.reset() while not done: action = \ self.model.get_action(observation) next_observation, reward, done, _ = \ self.env.step(action) self.buffer.store( observation, action, reward * \ 0.01, next_observation, done ) episode_reward += reward observation = next_observation if self.buffer.size() >= args.batch_size: self.replay_experience() self.update_target() print(f"Episode#{ep} Reward:{ episode_reward}") tf.summary.scalar("episode_reward", episode_reward, step=ep) writer.flush()
-
最后,让我们创建主函数以开始训练智能体:
if __name__ == "__main__": env = gym.make("CartPole-v0") agent = Agent(env) agent.train(max_episodes=20000)
-
要在默认环境(
CartPole-v0
)中训练 DQN 智能体,请执行以下命令:python ch3-deep-rl-agents/1_dqn.py
-
你还可以使用命令行参数在任何 OpenAI Gym 兼容的离散动作空间环境中训练 DQN 智能体:
python ch3-deep-rl-agents/1_dqn.py –env "MountainCar-v0"
-
现在,为了实现 Double DQN 智能体,我们必须修改
replay_experience
方法,以使用 Double Q 学习的更新步骤,如下所示:def replay_experience(self): for _ in range(10): states, actions, rewards, next_states, done=\ self.buffer.sample() targets = self.target_model.predict(states) next_q_values = \ self.target_model.predict(next_states)[ range(args.batch_size), np.argmax(self.model.predict( next_states), axis=1), ] targets[range(args.batch_size), actions] = ( rewards + (1 - done) * next_q_values * \ args.gamma ) self.model.train(states, targets)
-
最后,为了训练 Double DQN 智能体,保存并运行脚本,更新后的
replay_experience
方法,或者使用作为本书源代码一部分提供的脚本:python ch3-deep-rl-agents/1_double_dqn.py
让我们看看它是如何工作的。
它是如何工作的...
DQN 中的权重更新按以下 Q 学习方程进行:
这里, 是 DQN 参数(权重)的变化,s 是当前状态,a 是当前动作,s' 是下一个状态,w 代表 DQN 的权重,
是折扣因子,
是学习率,
表示由 DQN 预测的给定状态(s)和动作(a)的 Q 值,权重为
。
为了理解 DQN 智能体与 Double-DQN 智能体的区别,请对比第 8 步(DQN)和第 13 步(Double DQN)中的replay_experience
方法。你会注意到,关键区别在于计算next_q_values
。DQN 智能体使用预测的 Q 值的最大值(这可能是高估的),而 Double DQN 智能体使用两个不同神经网络的预测 Q 值。这种方法是为了避免 DQN 智能体高估 Q 值的问题。
实现对抗性 DQN 智能体
对抗性 DQN 智能体通过修改的网络架构显式地估计两个量:
-
状态值,V(s)
-
优势值,A(s, a)
状态值估计了处于状态 s 时的价值,优势值表示在状态 s 中采取行动 a 的优势。通过显式和独立地估计这两个数量,Dueling DQN 相较于 DQN 表现得更好。这个配方将带你逐步实现一个从零开始的 Dueling DQN 智能体,使用 TensorFlow 2.x。
准备工作
要完成这个配方,首先需要激活 tf2rl-cookbook
Conda Python 虚拟环境,并运行 pip install -r requirements.txt
。如果以下导入语句没有问题,则说明可以开始了!
import argparse
import os
import random
from collections import deque
from datetime import datetime
import gym
import numpy as np
import tensorflow as tf
from tensorflow.keras.layers import Add, Dense, Input
from tensorflow.keras.optimizers import Adam
现在我们可以开始了。
如何实现…
Dueling DQN 智能体由几个组件组成,即 DuelingDQN
类、Agent
类和 train
方法。按照以下步骤,从零开始实现这些组件,利用 TensorFlow 2.x 构建一个完整的 Dueling DQN 智能体:
-
作为第一步,让我们创建一个参数解析器,用于处理脚本的命令行配置输入:
parser = argparse.ArgumentParser(prog="TFRL-Cookbook-Ch3-DuelingDQN") parser.add_argument("--env", default="CartPole-v0") parser.add_argument("--lr", type=float, default=0.005) parser.add_argument("--batch_size", type=int, default=64) parser.add_argument("--gamma", type=float, default=0.95) parser.add_argument("--eps", type=float, default=1.0) parser.add_argument("--eps_decay", type=float, default=0.995) parser.add_argument("--eps_min", type=float, default=0.01) parser.add_argument("--logdir", default="logs") args = parser.parse_args()
-
为了在智能体训练过程中记录有用的统计信息,让我们创建一个 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)
-
接下来,让我们实现一个
ReplayBuffer
类:class ReplayBuffer: def __init__(self, capacity=10000): self.buffer = deque(maxlen=capacity) def store(self, state, action, reward, next_state, done): self.buffer.append([state, action, reward, next_state, done]) def sample(self): sample = random.sample(self.buffer, args.batch_size) states, actions, rewards, next_states, done = \ map(np.asarray, zip(*sample)) states = np.array(states).reshape( args.batch_size, -1) next_states = np.array(next_states).reshape( args.batch_size, -1) return states, actions, rewards, next_states, done def size(self): return len(self.buffer)
-
现在是时候实现 DuelingDQN 类,该类在 TensorFlow 2.x 中定义深度神经网络了:
class DuelingDQN: def __init__(self, state_dim, aciton_dim): self.state_dim = state_dim self.action_dim = aciton_dim self.epsilon = args.eps self.model = self.nn_model() def nn_model(self): backbone = tf.keras.Sequential( [ Input((self.state_dim,)), Dense(32, activation="relu"), Dense(16, activation="relu"), ] ) state_input = Input((self.state_dim,)) backbone_1 = Dense(32, activation="relu")\ (state_input) backbone_2 = Dense(16, activation="relu")\ (backbone_1) value_output = Dense(1)(backbone_2) advantage_output = Dense(self.action_dim)\ (backbone_2) output = Add()([value_output, advantage_output]) model = tf.keras.Model(state_input, output) model.compile(loss="mse", optimizer=Adam(args.lr)) return model
-
为了从 Dueling DQN 获取预测和动作,让我们实现
predict
、get_action
和train
方法:def predict(self, state): return self.model.predict(state) def get_action(self, state): state = np.reshape(state, [1, self.state_dim]) self.epsilon *= args.eps_decay self.epsilon = max(self.epsilon, args.eps_min) q_value = self.predict(state)[0] if np.random.random() < self.epsilon: return random.randint(0, self.action_dim - 1) return np.argmax(q_value) def train(self, states, targets): self.model.fit(states, targets, epochs=1)
-
现在我们可以开始实现
Agent
类:class Agent: def __init__(self, env): self.env = env self.state_dim = \ self.env.observation_space.shape[0] self.action_dim = self.env.action_space.n self.model = DuelingDQN(self.state_dim, self.action_dim) self.target_model = DuelingDQN(self.state_dim, self.action_dim) self.update_target() self.buffer = ReplayBuffer() def update_target(self): weights = self.model.model.get_weights() self.target_model.model.set_weights(weights)
-
Dueling Deep Q-learning 算法的关键在于 q-learning 更新和经验回放。接下来,让我们实现这些:
def replay_experience(self): for _ in range(10): states, actions, rewards, next_states, done=\ self.buffer.sample() targets = self.target_model.predict(states) next_q_values = self.target_model.\ predict(next_states).max(axis=1) targets[range(args.batch_size), actions] = ( rewards + (1 - done) * next_q_values * \ args.gamma ) self.model.train(states, targets)
-
下一个关键步骤是实现
train
函数来训练智能体:def train(self, max_episodes=1000): with writer.as_default(): for ep in range(max_episodes): done, episode_reward = False, 0 state = self.env.reset() while not done: action = self.model.get_action(state) next_state, reward, done, _ = \ self.env.step(action) self.buffer.put(state, action, \ reward * 0.01, \ next_state, done) episode_reward += reward state = next_state if self.buffer.size() >= args.batch_size: self.replay_experience() self.update_target() print(f"Episode#{ep} \ Reward:{episode_reward}") tf.summary.scalar("episode_reward",\ episode_reward, step=ep)
-
最后,让我们创建主函数来启动智能体的训练:
if __name__ == "__main__": env = gym.make("CartPole-v0") agent = Agent(env) agent.train(max_episodes=20000)
-
要在默认环境(
CartPole-v0
)中训练 Dueling DQN 智能体,请执行以下命令:python ch3-deep-rl-agents/2_dueling_dqn.py
-
你也可以在任何与 OpenAI Gym 兼容的离散动作空间环境中训练 DQN 智能体,使用命令行参数:
python ch3-deep-rl-agents/2_dueling_dqn.py –env "MountainCar-v0"
让我们来看它是如何工作的。
它是如何工作的…
Dueling-DQN 智能体在神经网络架构上与 DQN 智能体有所不同。
这些差异在下图中进行了总结:
图 3.1 – DQN 和 Dueling-DQN 的比较
DQN(图的上半部分)具有线性架构,预测一个单一的数量(Q(s, a)),而 Dueling-DQN 在最后一层有一个分叉,预测多个数量。
实现 Dueling Double DQN 算法和 DDDQN 智能体
Dueling Double DQN(DDDQN)结合了 Double Q-learning 和 Dueling 架构的优势。Double Q-learning 修正了 DQN 过高估计动作值的问题。Dueling 架构使用修改后的架构,分别学习状态值函数(V)和优势函数(A)。这种显式分离使算法能够更快学习,特别是在有许多动作可选且动作之间非常相似的情况下。Dueling 架构使智能体即使在一个状态下只采取了一个动作时也能进行学习,因为它可以更新和估计状态值函数,这与 DQN 智能体不同,后者无法从尚未采取的动作中学习。在完成这个食谱后,你将拥有一个完整的 DDDQN 智能体实现。
准备好了吗?
要完成这个食谱,你首先需要激活 tf2rl-cookbook
Conda Python 虚拟环境并运行 pip install -r requirements.txt
。如果以下导入语句没有问题,那么你就可以开始了!
import argparse
from datetime import datetime
import os
import random
from collections import deque
import gym
import numpy as np
import tensorflow as tf
from tensorflow.keras.layers import Add, Dense, Input
from tensorflow.keras.optimizers import Adam
我们准备好了,开始吧!
如何做……
DDDQN 智能体结合了 DQN、Double DQN 和 Dueling DQN 中的思想。执行以下步骤,从头开始实现这些组件,以便使用 TensorFlow 2.x 构建一个完整的 Dueling Double DQN 智能体:
-
首先,让我们创建一个参数解析器来处理脚本的配置输入:
parser = argparse.ArgumentParser(prog="TFRL-Cookbook-Ch3-DuelingDoubleDQN") parser.add_argument("--env", default="CartPole-v0") parser.add_argument("--lr", type=float, default=0.005) parser.add_argument("--batch_size", type=int, default=256) parser.add_argument("--gamma", type=float, default=0.95) parser.add_argument("--eps", type=float, default=1.0) parser.add_argument("--eps_decay", type=float, default=0.995) parser.add_argument("--eps_min", type=float, default=0.01) parser.add_argument("--logdir", default="logs") args = parser.parse_args()
-
接下来,让我们创建一个 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)
-
现在,让我们实现一个
ReplayBuffer
:class ReplayBuffer: def __init__(self, capacity=10000): self.buffer = deque(maxlen=capacity) def store(self, state, action, reward, next_state, done): self.buffer.append([state, action, reward, \ next_state, done]) def sample(self): sample = random.sample(self.buffer, \ args.batch_size) states, actions, rewards, next_states, done = \ map(np.asarray, zip(*sample)) states = np.array(states).reshape( args.batch_size, -1) next_states = np.array(next_states).\ reshape(args.batch_size, -1) return states, actions, rewards, next_states, \ done def size(self): return len(self.buffer)
-
现在是时候实现 Dueling DQN 类了,它将按照 Dueling 架构定义神经网络,后续我们会在此基础上添加 Double DQN 更新:
class DuelingDQN: def __init__(self, state_dim, aciton_dim): self.state_dim = state_dim self.action_dim = aciton_dim self.epsilon = args.eps self.model = self.nn_model() def nn_model(self): state_input = Input((self.state_dim,)) fc1 = Dense(32, activation="relu")(state_input) fc2 = Dense(16, activation="relu")(fc1) value_output = Dense(1)(fc2) advantage_output = Dense(self.action_dim)(fc2) output = Add()([value_output, advantage_output]) model = tf.keras.Model(state_input, output) model.compile(loss="mse", \ optimizer=Adam(args.lr)) return model
-
为了从 Dueling DQN 获取预测和动作,让我们实现
predict
和get_action
方法:def predict(self, state): return self.model.predict(state) def get_action(self, state): state = np.reshape(state, [1, self.state_dim]) self.epsilon *= args.eps_decay self.epsilon = max(self.epsilon, args.eps_min) q_value = self.predict(state)[0] if np.random.random() < self.epsilon: return random.randint(0, self.action_dim - 1) return np.argmax(q_value) def train(self, states, targets): self.model.fit(states, targets, epochs=1)
-
其他组件实现完成后,我们可以开始实现
Agent
类:class Agent: def __init__(self, env): self.env = env self.state_dim = \ self.env.observation_space.shape[0] self.action_dim = self.env.action_space.n self.model = DuelingDQN(self.state_dim, self.action_dim) self.target_model = DuelingDQN(self.state_dim, self.action_dim) self.update_target() self.buffer = ReplayBuffer() def update_target(self): weights = self.model.model.get_weights() self.target_model.model.set_weights(weights)
-
Dueling Double Deep Q-learning 算法的主要元素是 Q-learning 更新和经验回放。接下来我们将实现这些:
def replay_experience(self): for _ in range(10): states, actions, rewards, next_states, done=\ self.buffer.sample() targets = self.target_model.predict(states) next_q_values = \ self.target_model.predict(next_states)[ range(args.batch_size), np.argmax(self.model.predict( next_states), axis=1), ] targets[range(args.batch_size), actions] = ( rewards + (1 - done) * next_q_values * \ args.gamma ) self.model.train(states, targets)
-
下一个关键步骤是实现
train
函数来训练智能体:def train(self, max_episodes=1000): with writer.as_default(): for ep in range(max_episodes): done, episode_reward = False, 0 observation = self.env.reset() while not done: action = \ self.model.get_action(observation) next_observation, reward, done, _ = \ self.env.step(action) self.buffer.store( observation, action, reward * \ 0.01, next_observation, done ) episode_reward += reward observation = next_observation if self.buffer.size() >= args.batch_size: self.replay_experience() self.update_target() print(f"Episode#{ep} \ Reward:{episode_reward}") tf.summary.scalar("episode_reward", episode_reward, step=ep)
-
最后,让我们创建主函数来开始训练智能体:
if __name__ == "__main__": env = gym.make("CartPole-v0") agent = Agent(env) agent.train(max_episodes=20000)
-
要在默认环境(
CartPole-v0
)中训练 DQN 智能体,请执行以下命令:python ch3-deep-rl-agents/3_dueling_double_dqn.py
-
你还可以在任何兼容 OpenAI Gym 的离散动作空间环境中使用命令行参数训练 Dueling Double DQN 智能体:
python ch3-deep-rl-agents/3_dueling_double_dqn.py –env "MountainCar-v0"
它是如何工作的……
Dueling Double DQN 架构结合了 Double DQN 和 Dueling 架构引入的进展。
实现深度递归 Q-learning 算法和 DRQN 智能体
DRQN 使用递归神经网络来学习 Q 值函数。DRQN 更适合在部分可观察环境中进行强化学习。DRQN 中的递归网络层允许智能体通过整合时间序列的观察信息来进行学习。例如,DRQN 智能体可以推测环境中移动物体的速度,而无需任何输入的变化(例如,不需要帧堆叠)。在完成这个配方后,您将拥有一个完整的 DRQN 智能体,准备在您选择的强化学习环境中进行训练。
准备开始
要完成这个配方,您首先需要激活tf2rl-cookbook
Conda Python 虚拟环境,并运行pip install -r requirements.txt
。如果以下导入语句没有问题,那么您就准备好开始了!
import tensorflow as tf
from datetime import datetime
import os
from tensorflow.keras.layers import Input, Dense, LSTM
from tensorflow.keras.optimizers import Adam
import gym
import argparse
import numpy as np
from collections import deque
import random
让我们开始吧!
怎么做…
对抗双重 DQN 智能体结合了 DQN、双重 DQN 和对抗 DQN 的理念。执行以下步骤,从零开始实现这些组件,以使用 TensorFlow 2.x 构建完整的 DRQN 智能体:
-
首先,创建一个参数解析器来处理脚本的配置输入:
parser = argparse.ArgumentParser(prog="TFRL-Cookbook-Ch3-DRQN") parser.add_argument("--env", default="CartPole-v0") parser.add_argument("--lr", type=float, default=0.005) parser.add_argument("--batch_size", type=int, default=64) parser.add_argument("--time_steps", type=int, default=4) parser.add_argument("--gamma", type=float, default=0.95) parser.add_argument("--eps", type=float, default=1.0) parser.add_argument("--eps_decay", type=float, default=0.995) parser.add_argument("--eps_min", type=float, default=0.01) parser.add_argument("--logdir", default="logs") args = parser.parse_args()
-
让我们在智能体的训练过程中使用 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)
-
接下来,让我们实现一个
ReplayBuffer
:class ReplayBuffer: def __init__(self, capacity=10000): self.buffer = deque(maxlen=capacity) def store(self, state, action, reward, next_state,\ done): self.buffer.append([state, action, reward, \ next_state, done]) def sample(self): sample = random.sample(self.buffer, args.batch_size) states, actions, rewards, next_states, done = \ map(np.asarray, zip(*sample)) states = np.array(states).reshape( args.batch_size, -1) next_states = np.array(next_states).reshape( args.batch_size, -1) return states, actions, rewards, next_states, \ done def size(self): return len(self.buffer)
-
现在是时候实现定义深度神经网络的 DRQN 类了,使用的是 TensorFlow 2.x:
class DRQN: def __init__(self, state_dim, action_dim): self.state_dim = state_dim self.action_dim = action_dim self.epsilon = args.eps self.opt = Adam(args.lr) self.compute_loss = \ tf.keras.losses.MeanSquaredError() self.model = self.nn_model() def nn_model(self): return tf.keras.Sequential( [ Input((args.time_steps, self.state_dim)), LSTM(32, activation="tanh"), Dense(16, activation="relu"), Dense(self.action_dim), ] )
-
为了从 DRQN 获取预测和动作,让我们实现
predict
和get_action
方法:def predict(self, state): return self.model.predict(state) def get_action(self, state): state = np.reshape(state, [1, args.time_steps, self.state_dim]) self.epsilon *= args.eps_decay self.epsilon = max(self.epsilon, args.eps_min) q_value = self.predict(state)[0] if np.random.random() < self.epsilon: return random.randint(0, self.action_dim - 1) return np.argmax(q_value) def train(self, states, targets): targets = tf.stop_gradient(targets) with tf.GradientTape() as tape: logits = self.model(states, training=True) assert targets.shape == logits.shape loss = self.compute_loss(targets, logits) grads = tape.gradient(loss, self.model.trainable_variables) self.opt.apply_gradients(zip(grads, self.model.trainable_variables))
-
实现了其他组件后,我们可以开始实现我们的
Agent
类:class Agent: def __init__(self, env): self.env = env self.state_dim = \ self.env.observation_space.shape[0] self.action_dim = self.env.action_space.n self.states = np.zeros([args.time_steps, self.state_dim]) self.model = DRQN(self.state_dim, self.action_dim) self.target_model = DRQN(self.state_dim, self.action_dim) self.update_target() self.buffer = ReplayBuffer() def update_target(self): weights = self.model.model.get_weights() self.target_model.model.set_weights(weights)
-
除了我们在第 6 步中实现的 DRQN 类中的
train
方法外,深度递归 Q 学习算法的核心是 Q 学习更新和经验回放。接下来,让我们实现这一部分:def replay_experience(self): for _ in range(10): states, actions, rewards, next_states, done=\ self.buffer.sample() targets = self.target_model.predict(states) next_q_values = self.target_model.\ predict(next_states).max(axis=1) targets[range(args.batch_size), actions] = ( rewards + (1 - done) * next_q_values * \ args.gamma ) self.model.train(states, targets)
-
由于 DRQN 智能体使用递归状态,让我们实现
update_states
方法来更新智能体的递归状态:def update_states(self, next_state): self.states = np.roll(self.states, -1, axis=0) self.states[-1] = next_state
-
下一个关键步骤是实现
train
函数来训练智能体:def train(self, max_episodes=1000): with writer.as_default(): for ep in range(max_episodes): done, episode_reward = False, 0 self.states = np.zeros([args.time_steps, self.state_dim]) self.update_states(self.env.reset()) while not done: action = self.model.get_action( self.states) next_state, reward, done, _ = \ self.env.step(action) prev_states = self.states self.update_states(next_state) self.buffer.store( prev_states, action, reward * \ 0.01, self.states, done ) episode_reward += reward if self.buffer.size() >= args.batch_size: self.replay_experience() self.update_target() print(f"Episode#{ep} \ Reward:{episode_reward}") tf.summary.scalar("episode_reward", episode_reward, step=ep)
-
最后,让我们为智能体创建主要的训练循环:
if __name__ == "__main__": env = gym.make("Pong-v0") agent = Agent(env) agent.train(max_episodes=20000)
-
要在默认环境(
CartPole-v0
)中训练 DRQN 智能体,请执行以下命令:python ch3-deep-rl-agents/4_drqn.py
-
您还可以使用命令行参数在任何 OpenAI Gym 兼容的离散动作空间环境中训练 DQN 智能体:
python ch3-deep-rl-agents/4_drqn.py –env "MountainCar-v0"
它是如何工作的…
DRQN 智能体使用 LSTM 层,这为智能体增加了递归学习能力。LSTM 层在配方的第 5 步中添加到智能体的网络中。配方中的其他步骤与 DQN 智能体类似。
实现异步优势演员评论家算法和 A3C 智能体
A3C 算法在 Actor-Critic 类算法的基础上构建,通过使用神经网络来逼近演员(actor)和评论家(critic)。演员使用深度神经网络学习策略函数,而评论家则估计价值函数。算法的异步性质使得智能体能够从状态空间的不同部分进行学习,从而实现并行学习和更快的收敛。与使用经验回放记忆的 DQN 智能体不同,A3C 智能体使用多个工作线程来收集更多样本进行学习。在本配方的结尾,你将拥有一个完整的脚本,可以用来训练一个适用于任何连续动作值环境的 A3C 智能体!
准备开始
要完成这个配方,你首先需要激活 tf2rl-cookbook
Conda Python 虚拟环境并运行 pip install -r requirements.txt
。如果以下的导入语句没有问题,那就说明你已经准备好开始了!
import argparse
import os
from datetime import datetime
from multiprocessing import cpu_count
from threading import Thread
import gym
import numpy as np
import tensorflow as tf
from tensorflow.keras.layers import Input, Dense, Lambda
现在我们可以开始了。
如何实现…
我们将通过利用 Python 的多进程和多线程功能实现一个 异步优势演员评论家(A3C) 算法。以下步骤将帮助你从零开始使用 TensorFlow 2.x 实现一个完整的 A3C 智能体:
-
首先,让我们创建一个参数解析器,用来处理脚本的配置输入:
parser = argparse.ArgumentParser(prog="TFRL-Cookbook-Ch3-A3C") parser.add_argument("--env", default="MountainCarContinuous-v0") parser.add_argument("--actor-lr", type=float, default=0.001) parser.add_argument("--critic-lr", type=float, default=0.002) parser.add_argument("--update-interval", type=int, default=5) parser.add_argument("--gamma", type=float, default=0.99) parser.add_argument("--logdir", default="logs") args = parser.parse_args()
-
现在让我们创建一个 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)
-
为了统计全局回合数,让我们定义一个全局变量:
GLOBAL_EPISODE_NUM = 0
-
现在我们可以集中精力实现
Actor
类,它将包含一个基于神经网络的策略来在环境中执行动作:class Actor: def __init__(self, state_dim, action_dim, action_bound, std_bound): self.state_dim = state_dim self.action_dim = action_dim self.action_bound = action_bound self.std_bound = std_bound self.model = self.nn_model() self.opt = tf.keras.optimizers.Adam( args.actor_lr) self.entropy_beta = 0.01 def nn_model(self): state_input = Input((self.state_dim,)) dense_1 = Dense(32, activation="relu")\ (state_input) dense_2 = Dense(32, activation="relu")(dense_1) out_mu = Dense(self.action_dim, \ activation="tanh")(dense_2) mu_output = Lambda(lambda x: x * \ self.action_bound)(out_mu) std_output = Dense(self.action_dim, activation="softplus")(dense_2) return tf.keras.models.Model(state_input, [mu_output, std_output])
-
为了在给定状态下从演员获取动作,让我们定义
get_action
方法:def get_action(self, state): state = np.reshape(state, [1, self.state_dim]) mu, std = self.model.predict(state) mu, std = mu[0], std[0] return np.random.normal(mu, std, size=self.action_dim)
-
接下来,为了计算损失,我们需要计算策略(概率)密度函数的对数:
def log_pdf(self, mu, std, action): std = tf.clip_by_value(std, self.std_bound[0], self.std_bound[1]) var = std ** 2 log_policy_pdf = -0.5 * (action - mu) ** 2 / var\ - 0.5 * tf.math.log( var * 2 * np.pi ) return tf.reduce_sum(log_policy_pdf, 1, keepdims=True)
-
现在让我们使用
log_pdf
方法来计算演员损失:def compute_loss(self, mu, std, actions, advantages): log_policy_pdf = self.log_pdf(mu, std, actions) loss_policy = log_policy_pdf * advantages return tf.reduce_sum(-loss_policy)
-
作为
Actor
类实现的最后一步,让我们定义train
方法:def train(self, states, actions, advantages): with tf.GradientTape() as tape: mu, std = self.model(states, training=True) loss = self.compute_loss(mu, std, actions, advantages) grads = tape.gradient(loss, self.model.trainable_variables) self.opt.apply_gradients(zip(grads, self.model.trainable_variables)) return loss
-
定义好
Actor
类后,我们可以继续定义Critic
类:class Critic: def __init__(self, state_dim): self.state_dim = state_dim self.model = self.nn_model() self.opt = tf.keras.optimizers.Adam\ (args.critic_lr) def nn_model(self): return tf.keras.Sequential( [ Input((self.state_dim,)), Dense(32, activation="relu"), Dense(32, activation="relu"), Dense(16, activation="relu"), Dense(1, activation="linear"), ] )
-
接下来,让我们定义
train
方法和一个compute_loss
方法来训练评论家:def compute_loss(self, v_pred, td_targets): mse = tf.keras.losses.MeanSquaredError() return mse(td_targets, v_pred) 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
-
是时候基于 Python 的线程接口实现
A3CWorker
类了:class A3CWorker(Thread): def __init__(self, env, global_actor, global_critic, max_episodes): Thread.__init__(self) self.env = env self.state_dim = \ self.env.observation_space.shape[0] self.action_dim = self.env.action_space.shape[0] self.action_bound = self.env.action_space.high[0] self.std_bound = [1e-2, 1.0] self.max_episodes = max_episodes self.global_actor = global_actor self.global_critic = global_critic self.actor = Actor( self.state_dim, self.action_dim, self.action_bound, self.std_bound ) self.critic = Critic(self.state_dim) self.actor.model.set_weights( self.global_actor.model.get_weights()) self.critic.model.set_weights( self.global_critic.model.get_weights())
-
我们将使用 n 步时间差分 (TD) 学习更新。因此,让我们定义一个方法来计算 n 步 TD 目标:
def n_step_td_target(self, rewards, next_v_value, done): td_targets = np.zeros_like(rewards) cumulative = 0 if not done: cumulative = next_v_value for k in reversed(range(0, len(rewards))): cumulative = args.gamma * cumulative + \ rewards[k] td_targets[k] = cumulative return td_targets
-
我们还需要计算优势值。优势值的最简单形式很容易实现:
def advantage(self, td_targets, baselines): return td_targets - baselines
-
我们将把
train
方法的实现分为以下两个步骤。首先,让我们实现外部循环:def train(self): global GLOBAL_EPISODE_NUM while self.max_episodes >= GLOBAL_EPISODE_NUM: state_batch = [] action_batch = [] reward_batch = [] episode_reward, done = 0, False state = self.env.reset() while not done: # self.env.render() action = self.actor.get_action(state) action = np.clip(action, -self.action_bound, self.action_bound) next_state, reward, done, _ = \ self.env.step(action) state = np.reshape(state, [1, self.state_dim]) action = np.reshape(action, [1, 1]) next_state = np.reshape(next_state, [1, self.state_dim]) reward = np.reshape(reward, [1, 1]) state_batch.append(state) action_batch.append(action) reward_batch.append(reward)
-
在这一步,我们将完成
train
方法的实现:if len(state_batch) >= args.update_\ interval or done: states = np.array([state.squeeze() \ for state in state_batch]) actions = np.array([action.squeeze()\ for action in action_batch]) rewards = np.array([reward.squeeze()\ for reward in reward_batch]) next_v_value = self.critic.model.\ predict(next_state) td_targets = self.n_step_td_target( (rewards + 8) / 8, next_v_value, done ) advantages = td_targets - \ self.critic.model.predict(states) actor_loss = self.global_actor.train( states, actions, advantages) critic_loss = self.global_critic.\ train(states, td_targets) self.actor.model.set_weights(self.\ global_actor.model.get_weights()) self.critic.model.set_weights( self.global_critic.model.\ get_weights() ) state_batch = [] action_batch = [] reward_batch = [] episode_reward += reward[0][0] state = next_state[0] print(f"Episode#{GLOBAL_EPISODE_NUM}\ Reward:{episode_reward}") tf.summary.scalar("episode_reward", episode_reward, step=GLOBAL_EPISODE_NUM) GLOBAL_EPISODE_NUM += 1
-
A3CWorker
线程的 run 方法将是以下内容:def run(self): self.train()
-
接下来,让我们实现
Agent
类:class Agent: def __init__(self, env_name, num_workers=cpu_count()): env = gym.make(env_name) self.env_name = env_name self.state_dim = env.observation_space.shape[0] self.action_dim = env.action_space.shape[0] self.action_bound = env.action_space.high[0] self.std_bound = [1e-2, 1.0] self.global_actor = Actor( self.state_dim, self.action_dim, self.action_bound, self.std_bound ) self.global_critic = Critic(self.state_dim) self.num_workers = num_workers
-
A3C 智能体利用多个并发工作线程。为了更新每个工作线程以更新 A3C 智能体,以下代码是必要的:
def train(self, max_episodes=20000): workers = [] for i in range(self.num_workers): env = gym.make(self.env_name) workers.append( A3CWorker(env, self.global_actor, self.global_critic, max_episodes) ) for worker in workers: worker.start() for worker in workers: worker.join()
-
这样,我们的 A3C 智能体实现就完成了,接下来我们准备定义我们的主函数:
if __name__ == "__main__": env_name = "MountainCarContinuous-v0" agent = Agent(env_name, args.num_workers) agent.train(max_episodes=20000)
它是如何工作的…
简单来说,A3C 算法的核心可以通过以下步骤总结,每个迭代中都会执行这些步骤:
图 3.2 – A3C 智能体学习迭代中的更新步骤
步骤会从上到下重复进行,直到收敛。
实现邻近策略优化算法和 PPO 智能体
邻近策略优化(PPO)算法是在信任域策略优化(TRPO)的基础上发展而来的,通过将新策略限制在旧策略的信任区域内。PPO 通过使用一个剪切的替代目标函数简化了这一核心思想的实现,这个目标函数更容易实现,但仍然非常强大和高效。它是最广泛使用的强化学习算法之一,尤其适用于连续控制问题。在完成本教程后,你将构建一个 PPO 智能体,并能在你选择的强化学习环境中进行训练。
准备开始
为了完成本教程,你需要先激活tf2rl-cookbook
Conda Python 虚拟环境并运行pip install -r requirements.txt
。如果以下导入语句没有问题,你就准备好开始了!
import argparse
import os
from datetime import datetime
import gym
import numpy as np
import tensorflow as tf
from tensorflow.keras.layers import Dense, Input, Lambda
我们已经准备好开始了。
如何做...
以下步骤将帮助你从头开始使用 TensorFlow 2.x 实现一个完整的 PPO 智能体:
-
首先,创建一个参数解析器来处理脚本的配置输入:
parser = argparse.ArgumentParser(prog="TFRL-Cookbook-Ch3-PPO") parser.add_argument("--env", default="Pendulum-v0") parser.add_argument("--update-freq", type=int, default=5) parser.add_argument("--epochs", type=int, default=3) parser.add_argument("--actor-lr", type=float, default=0.0005) parser.add_argument("--critic-lr", type=float, default=0.001) 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()
-
接下来,我们创建一个 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)
-
我们现在可以集中精力实现
Actor
类,它将包含一个基于神经网络的策略来执行动作:class Actor: def __init__(self, state_dim, action_dim, action_bound, std_bound): self.state_dim = state_dim self.action_dim = action_dim self.action_bound = action_bound self.std_bound = std_bound self.model = self.nn_model() self.opt = \ tf.keras.optimizers.Adam(args.actor_lr) def nn_model(self): state_input = Input((self.state_dim,)) dense_1 = Dense(32, activation="relu")\ (state_input) dense_2 = Dense(32, activation="relu")\ (dense_1) out_mu = Dense(self.action_dim, activation="tanh")(dense_2) mu_output = Lambda(lambda x: x * \ self.action_bound)(out_mu) std_output = Dense(self.action_dim, activation="softplus")(dense_2) return tf.keras.models.Model(state_input, [mu_output, std_output])
-
为了从演员那里获得一个动作给定一个状态,先定义
get_action
方法:def get_action(self, state): state = np.reshape(state, [1, self.state_dim]) mu, std = self.model.predict(state) action = np.random.normal(mu[0], std[0], size=self.action_dim) action = np.clip(action, -self.action_bound, self.action_bound) log_policy = self.log_pdf(mu, std, action) return log_policy, action
-
接下来,为了计算损失,我们需要计算策略(概率)密度函数的对数:
def log_pdf(self, mu, std, action): std = tf.clip_by_value(std, self.std_bound[0], self.std_bound[1]) var = std ** 2 log_policy_pdf = -0.5 * (action - mu) ** 2 / var\ - 0.5 * tf.math.log( var * 2 * np.pi ) return tf.reduce_sum(log_policy_pdf, 1, keepdims=True)
-
现在我们使用
log_pdf
方法来计算演员损失:def compute_loss(self, log_old_policy, log_new_policy, actions, gaes): ratio = tf.exp(log_new_policy - \ tf.stop_gradient(log_old_policy)) gaes = tf.stop_gradient(gaes) clipped_ratio = tf.clip_by_value( ratio, 1.0 - args.clip_ratio, 1.0 + \ args.clip_ratio ) surrogate = -tf.minimum(ratio * gaes, \ clipped_ratio * gaes) return tf.reduce_mean(surrogate)
-
作为
Actor
类实现的最后一步,让我们定义train
方法:def train(self, log_old_policy, states, actions, gaes): with tf.GradientTape() as tape: mu, std = self.model(states, training=True) log_new_policy = self.log_pdf(mu, std, actions) loss = self.compute_loss(log_old_policy, log_new_policy, actions, gaes) grads = tape.gradient(loss, self.model.trainable_variables) self.opt.apply_gradients(zip(grads, self.model.trainable_variables)) return loss
-
定义好
Actor
类后,我们可以继续定义Critic
类:class Critic: def __init__(self, state_dim): self.state_dim = state_dim self.model = self.nn_model() self.opt = tf.keras.optimizers.Adam( args.critic_lr) def nn_model(self): return tf.keras.Sequential( [ Input((self.state_dim,)), Dense(32, activation="relu"), Dense(32, activation="relu"), Dense(16, activation="relu"), Dense(1, activation="linear"), ] )
-
接下来,定义
train
方法和compute_loss
方法来训练评论员:def compute_loss(self, v_pred, td_targets): mse = tf.keras.losses.MeanSquaredError() return mse(td_targets, v_pred) 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
-
现在是时候实现 PPO
Agent
类了:class Agent: def __init__(self, env): self.env = env self.state_dim = \ self.env.observation_space.shape[0] self.action_dim = self.env.action_space.shape[0] self.action_bound = self.env.action_space.high[0] self.std_bound = [1e-2, 1.0] self.actor_opt = \ tf.keras.optimizers.Adam(args.actor_lr) self.critic_opt = \ tf.keras.optimizers.Adam(args.critic_lr) self.actor = Actor( self.state_dim, self.action_dim, self.action_bound, self.std_bound ) self.critic = Critic(self.state_dim)
-
我们将使用广义优势估计(GAE)。让我们实现一个方法来计算 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
-
现在我们将拆分
train
方法的实现。首先,让我们实现外部循环:def train(self, max_episodes=1000): 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()
-
在这一步,我们将开始内循环(每个回合)实现,并在接下来的几个步骤中完成它:
while not done: # self.env.render() log_old_policy, action = \ self.actor.get_action(state) next_state, reward, done, _ = \ self.env.step(action) state = np.reshape(state, [1, self.state_dim]) action = np.reshape(action, [1, 1]) next_state = np.reshape(next_state, [1, self.state_dim]) reward = np.reshape(reward, [1, 1]) log_old_policy = \ np.reshape(log_old_policy, [1, 1]) state_batch.append(state) action_batch.append(action) reward_batch.append((reward + 8) / 8) old_policy_batch.append(log_old_policy)
-
在这一步,我们将使用 PPO 算法所做的价值预测来为策略更新过程做准备:
if len(state_batch) >= args.update_freq or done: states = np.array([state.\ squeeze() for state \ in state_batch]) actions = np.array( [action.squeeze() for action\ in action_batch] ) rewards = np.array( [reward.squeeze() for reward\ in 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(next_state) gaes, td_targets = \ self.gae_target( rewards, v_values, \ next_v_value, done )
-
在这一步,我们将实现 PPO 算法的策略更新步骤。这些步骤发生在内循环中,每当足够的智能体轨迹信息以采样经验批次的形式可用时:
actor_losses, critic_losses=[],[] for epoch in range(args.epochs): actor_loss =self.actor.train( old_policies, states,\ actions, gaes ) actor_losses.append( actor_loss) critic_loss = self.critic.\ train(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 )
-
作为
train
方法的最后一步,我们将重置中间变量,并打印出智能体获得的每个回合奖励的总结:state_batch = [] action_batch = [] reward_batch = [] old_policy_batch = [] episode_reward += reward[0][0] state = next_state[0] print(f"Episode#{ep} \ Reward:{episode_reward}") tf.summary.scalar("episode_reward", \ episode_reward, \ step=ep)
-
有了这些,我们的 PPO 智能体实现就完成了,接下来我们可以定义主函数来开始训练!
if __name__ == "__main__": env_name = "Pendulum-v0" env = gym.make(env_name) agent = Agent(env) agent.train(max_episodes=20000)
它是如何工作的……
PPO 算法使用截断来形成一个替代损失函数,并根据策略更新使用多个周期的 随机梯度下降/上升(SGD)优化。PPO 引入的截断减少了对策略的有效变化,从而提高了策略在学习过程中的稳定性。
PPO 智能体使用演员(Actor)根据最新的策略参数从环境中收集样本。第 15 步中定义的循环会从经验中采样一个小批量,并使用截断的替代目标函数在 n 个周期(通过 --epoch
参数传递给脚本)中训练网络。然后使用新的经验样本重复此过程。
实现深度确定性策略梯度算法和 DDPG 智能体
确定性策略梯度(DPG) 是一种演员-评论家强化学习算法,使用两个神经网络:一个用于估计动作价值函数,另一个用于估计最优目标策略。深度确定性策略梯度(DDPG)智能体建立在 DPG 的基础上,并且由于使用了确定性动作策略,相较于普通的演员-评论家智能体,它在效率上更高。通过完成这个食谱,你将获得一个强大的智能体,可以在多种强化学习环境中高效训练。
准备工作
要完成这个食谱,首先需要激活 tf2rl-cookbook
Conda Python 虚拟环境,并运行 pip install -r requirements.txt
。如果以下导入语句没有问题,那么你已经准备好开始了!
import argparse
import os
import random
from collections import deque
from datetime import datetime
import gym
import numpy as np
import tensorflow as tf
from tensorflow.keras.layers import Dense, Input, Lambda, concatenate
现在我们可以开始了。
如何做到这一点……
以下步骤将帮助你从零开始使用 TensorFlow 2.x 实现一个完整的 DDPG 智能体:
-
首先创建一个参数解析器来处理脚本的命令行配置输入:
parser = argparse.ArgumentParser(prog="TFRL-Cookbook-Ch3-DDPG") parser.add_argument("--env", default="Pendulum-v0") parser.add_argument("--actor_lr", type=float, default=0.0005) parser.add_argument("--critic_lr", type=float, default=0.001) parser.add_argument("--batch_size", type=int, default=64) parser.add_argument("--tau", type=float, default=0.05) parser.add_argument("--gamma", type=float, default=0.99) parser.add_argument("--train_start", type=int, default=2000) parser.add_argument("--logdir", default="logs") args = parser.parse_args()
-
让我们创建一个 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)
-
现在让我们实现一个经验回放内存:
class ReplayBuffer: def __init__(self, capacity=10000): self.buffer = deque(maxlen=capacity) def store(self, state, action, reward, next_state, done): self.buffer.append([state, action, reward, next_state, done]) def sample(self): sample = random.sample(self.buffer, args.batch_size) states, actions, rewards, next_states, done = \ map(np.asarray, zip(*sample)) states = np.array(states).reshape( args.batch_size, -1) next_states = np.array(next_states).\ reshape(args.batch_size, -1) return states, actions, rewards, next_states, \ done def size(self): return len(self.buffer)
-
我们现在可以集中精力实现
Actor
类,它将包含一个基于神经网络的策略进行操作:class Actor: def __init__(self, state_dim, action_dim, action_bound): self.state_dim = state_dim self.action_dim = action_dim self.action_bound = action_bound self.model = self.nn_model() self.opt = tf.keras.optimizers.Adam(args.actor_lr) def nn_model(self): return tf.keras.Sequential( [ Input((self.state_dim,)), Dense(32, activation="relu"), Dense(32, activation="relu"), Dense(self.action_dim, activation="tanh"), Lambda(lambda x: x * self.action_bound), ] )
-
为了根据状态从演员获取动作,让我们定义
get_action
方法:def get_action(self, state): state = np.reshape(state, [1, self.state_dim]) return self.model.predict(state)[0]
-
接下来,我们将实现一个预测函数来返回演员网络的预测结果:
def predict(self, state): return self.model.predict(state)
-
作为
Actor
类实现的最后一步,让我们定义train
方法:def train(self, states, q_grads): with tf.GradientTape() as tape: grads = tape.gradient( self.model(states), self.model.trainable_variables, -q_grads ) self.opt.apply_gradients(zip(grads, self.model.trainable_variables))
-
定义了
Actor
类后,我们可以继续定义Critic
类:class Critic: def __init__(self, state_dim, action_dim): self.state_dim = state_dim self.action_dim = action_dim self.model = self.nn_model() self.opt = \ tf.keras.optimizers.Adam(args.critic_lr) def nn_model(self): state_input = Input((self.state_dim,)) s1 = Dense(64, activation="relu")(state_input) s2 = Dense(32, activation="relu")(s1) action_input = Input((self.action_dim,)) a1 = Dense(32, activation="relu")(action_input) c1 = concatenate([s2, a1], axis=-1) c2 = Dense(16, activation="relu")(c1) output = Dense(1, activation="linear")(c2) return tf.keras.Model([state_input, action_input], output)
-
在此步骤中,我们将实现一个方法来计算 Q 函数的梯度:
def q_gradients(self, states, actions): actions = tf.convert_to_tensor(actions) with tf.GradientTape() as tape: tape.watch(actions) q_values = self.model([states, actions]) q_values = tf.squeeze(q_values) return tape.gradient(q_values, actions)
-
作为一个便捷方法,我们还可以定义一个
predict
函数来返回评论家网络的预测结果:def predict(self, inputs): return self.model.predict(inputs)
-
接下来,让我们定义
train
方法和compute_loss
方法来训练评论家:def train(self, states, actions, td_targets): with tf.GradientTape() as tape: v_pred = self.model([states, actions], 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
-
现在是时候实现 DDPG 的
Agent
类了:class Agent: def __init__(self, env): self.env = env self.state_dim = \ self.env.observation_space.shape[0] self.action_dim = self.env.action_space.shape[0] self.action_bound = self.env.action_space.high[0] self.buffer = ReplayBuffer() self.actor = Actor(self.state_dim, \ self.action_dim, self.action_bound) self.critic = Critic(self.state_dim, self.action_dim) self.target_actor = Actor(self.state_dim, self.action_dim, self.action_bound) self.target_critic = Critic(self.state_dim, self.action_dim) actor_weights = self.actor.model.get_weights() critic_weights = self.critic.model.get_weights() self.target_actor.model.set_weights( actor_weights) self.target_critic.model.set_weights( critic_weights)
-
现在让我们实现
update_target
方法,用目标网络的权重来更新演员和评论员网络的权重:def update_target(self): actor_weights = self.actor.model.get_weights() t_actor_weights = \ self.target_actor.model.get_weights() critic_weights = self.critic.model.get_weights() t_critic_weights = \ self.target_critic.model.get_weights() for i in range(len(actor_weights)): t_actor_weights[i] = ( args.tau * actor_weights[i] + \ (1 - args.tau) * t_actor_weights[i] ) for i in range(len(critic_weights)): t_critic_weights[i] = ( args.tau * critic_weights[i] + \ (1 - args.tau) * t_critic_weights[i] ) self.target_actor.model.set_weights( t_actor_weights) self.target_critic.model.set_weights( t_critic_weights)
-
接下来,让我们实现一个辅助方法来计算 TD 目标:
def get_td_target(self, rewards, q_values, dones): targets = np.asarray(q_values) for i in range(q_values.shape[0]): if dones[i]: targets[i] = rewards[i] else: targets[i] = args.gamma * q_values[i] return targets
-
确定性策略梯度算法的目的是向从确定性策略中采样的动作添加噪声。我们将使用奥恩斯坦-乌伦贝克(OU)过程来生成噪声:
def add_ou_noise(self, x, rho=0.15, mu=0, dt=1e-1, sigma=0.2, dim=1): return ( x + rho * (mu - x) * dt + sigma * \ np.sqrt(dt) * np.random.normal(size=dim) )
-
在这一步中,我们将使用经验回放来更新演员网络和评论员网络:
def replay_experience(self): for _ in range(10): states, actions, rewards, next_states, \ dones = self.buffer.sample() target_q_values = self.target_critic.predict( [next_states, self.target_actor.\ predict(next_states)] ) td_targets = self.get_td_target(rewards, target_q_values, dones) self.critic.train(states, actions, td_targets) s_actions = self.actor.predict(states) s_grads = self.critic.q_gradients(states, s_actions) grads = np.array(s_grads).reshape((-1, self.action_dim)) self.actor.train(states, grads) self.update_target()
-
利用我们实现的所有组件,我们现在准备将它们组合在一起,放入
train
方法中:def train(self, max_episodes=1000): with writer.as_default(): for ep in range(max_episodes): episode_reward, done = 0, False state = self.env.reset() bg_noise = np.zeros(self.action_dim) while not done: # self.env.render() action = self.actor.get_action(state) noise = self.add_ou_noise(bg_noise, \ dim=self.action_dim) action = np.clip( action + noise, -self.action_\ bound, self.action_bound ) next_state, reward, done, _ = \ self.env.step(action) self.buffer.store(state, action, \ (reward + 8) / 8, next_state, done) bg_noise = noise episode_reward += reward state = next_state if ( self.buffer.size() >= args.batch_size and self.buffer.size() >= \ args.train_start ): self.replay_experience() print(f"Episode#{ep} \ Reward:{episode_reward}") tf.summary.scalar("episode_reward", episode_reward, step=ep)
-
至此,我们的 DDPG 代理实现已经完成,我们准备定义主函数以开始训练!
if __name__ == "__main__": env_name = "Pendulum-v0" env = gym.make(env_name) agent = Agent(env) agent.train(max_episodes=20000)
它是如何工作的…
DDPG 代理估计两个量——Q 值函数和最优策略。DDPG 结合了 DQN 和 DPG 中介绍的思想。DDPG 除了采用 DQN 中引入的思想外,还使用了策略梯度更新规则,如第 14 步中定义的更新步骤所示。
第四章:第四章:现实世界中的强化学习——构建加密货币交易智能体
深度强化学习(深度 RL)智能体在解决现实世界中的挑战性问题时具有很大的潜力,并且存在许多机会。然而,现实世界中成功应用深度 RL 智能体的故事较少,除了游戏领域,主要是由于与 RL 智能体在实际部署中相关的各种挑战。本章包含了一些食谱,帮助你成功开发用于一个有趣且具有回报的现实世界问题的 RL 智能体:加密货币交易。本章的食谱包含了如何为加密货币交易实现自定义的、兼容 OpenAI Gym 的学习环境,这些环境支持离散和连续值的动作空间。此外,你还将学习如何为加密货币交易构建和训练 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
。
使用真实市场数据构建比特币交易强化学习平台
这个食谱将帮助你为智能体构建一个加密货币交易强化学习环境。这个环境模拟了基于来自 Gemini 加密货币交易所的真实数据的比特币交易所。在这个环境中,强化学习智能体可以进行买入/卖出/持有交易,并根据它的利润/亏损获得奖励,初始时智能体的交易账户中会有一笔现金余额。
准备工作
为完成这个食谱,请确保你使用的是最新版本。你需要激活 tf2rl-cookbook
Python/conda 虚拟环境。确保更新该环境,使其与本书代码库中的最新 conda 环境规范文件(tfrl-cookbook.yml
)匹配。如果以下 import
语句没有问题,那么你就可以开始了:
import os
import random
from typing import Dict
import gym
import numpy as np
import pandas as pd
from gym import spaces
现在,让我们开始吧!
如何做……
按照以下步骤学习如何实现 CryptoTradingEnv
:
-
让我们从导入所需的 Python 模块开始:
-
我们还将使用在
trading_utils.py
中实现的TradeVisualizer
类。我们将在实际使用时更详细地讨论这个类:from trading_utils import TradeVisualizer
-
为了方便配置加密货币交易环境,我们将设置一个环境配置字典。请注意,我们的加密货币交易环境已被配置好,能够基于来自 Gemini 加密货币交易所的真实数据进行比特币交易:
env_config = { "exchange": "Gemini", # Cryptocurrency exchange # (Gemini, coinbase, kraken, etc.) "ticker": "BTCUSD", # CryptoFiat "frequency": "daily", # daily/hourly/minutes "opening_account_balance": 100000, # Number of steps (days) of data provided to the # agent in one observation. "observation_horizon_sequence_length": 30, "order_size": 1, # Number of coins to buy per # buy/sell order }
-
让我们开始定义我们的
CryptoTradingEnv
类:class CryptoTradingEnv(gym.Env): def __init__(self, env_config: Dict = env_config): super(CryptoTradingEnv, self).__init__() self.ticker = env_config.get("ticker", "BTCUSD") data_dir = os.path.join(os.path.dirname(os.path.\ realpath(__file__)), "data") self.exchange = env_config["exchange"] freq = env_config["frequency"] if freq == "daily": self.freq_suffix = "d" elif freq == "hourly": self.freq_suffix = "1hr" elif freq == "minutes": self.freq_suffix = "1min"
-
我们将使用一个文件对象作为加密货币交易所的数据源。我们必须确保在加载/流式传输数据到内存之前,数据源是存在的:
self.ticker_file_stream = os.path.join( f"{data_dir}", f"{'_'.join([self.exchange, self.ticker, self.freq_suffix])}.csv", ) assert os.path.isfile( self.ticker_file_stream ), f"Cryptocurrency data file stream not found \ at: data/{self.ticker_file_stream}.csv" # Cryptocurrency exchange data stream. An offline # file stream is used. Alternatively, a web # API can be used to pull live data. self.ohlcv_df = pd.read_csv(self.ticker_file_\ stream, skiprows=1).sort_values(by="Date" )
-
代理账户中的初始余额通过
env_config
配置。让我们根据配置的值初始化初始账户余额:self.opening_account_balance = env_config["opening_account_balance"]
-
接下来,让我们使用 OpenAI Gym 库提供的标准空间类型定义来定义该加密货币交易环境的动作空间和观察空间:
# Action: 0-> Hold; 1-> Buy; 2 ->Sell; self.action_space = spaces.Discrete(3) self.observation_features = [ "Open", "High", "Low", "Close", "Volume BTC", "Volume USD", ] self.horizon = env_config.get( "observation_horizon_sequence_length") self.observation_space = spaces.Box( low=0, high=1, shape=(len(self.observation_features), self.horizon + 1), dtype=np.float, )
-
让我们定义代理在进行交易时将执行的交易订单大小:
self.order_size = env_config.get("order_size")
-
至此,我们已成功初始化环境!接下来,让我们定义
step(…)
方法。你会注意到,为了简化理解,我们使用了两个辅助成员方法:self.execute_trade_action
和self.get_observation
,简化了step(…)
方法的实现。我们将在稍后定义这些辅助方法,等到我们完成基本的 RL Gym 环境方法(step
、reset
和render
)的实现。现在,让我们看看step
方法的实现:def step(self, action): # Execute one step within the trading environment self.execute_trade_action(action) self.current_step += 1 reward = self.account_value - \ self.opening_account_balance # Profit (loss) done = self.account_value <= 0 or \ self.current_step >= len( self.ohlcv_df.loc[:, "Open"].values ) obs = self.get_observation() return obs, reward, done, {}
-
现在,让我们定义
reset()
方法,它将在每个 episode 开始时执行:def reset(self): # Reset the state of the environment to an # initial state self.cash_balance = self.opening_account_balance self.account_value = self.opening_account_balance self.num_coins_held = 0 self.cost_basis = 0 self.current_step = 0 self.trades = [] if self.viz is None: self.viz = TradeVisualizer( self.ticker, self.ticker_file_stream, "TFRL-Cookbook Ch4-CryptoTradingEnv", skiprows=1, # Skip the first line with # the data download source URL ) return self.get_observation()
-
下一步,我们将定义
render()
方法,它将为我们提供加密货币交易环境的视图,帮助我们理解发生了什么!在这里,我们将使用来自trading_utils.py
文件中的TradeVisualizer
类。TradeVisualizer
帮助我们可视化代理在环境中学习时的实时账户余额。该可视化工具还通过显示代理在环境中执行的买卖交易,直观地呈现代理的操作。以下是render()
方法输出的示例截图,供您参考:def render(self, **kwargs): # Render the environment to the screen if self.current_step > self.horizon: self.viz.render( self.current_step, self.account_value, self.trades, window_size=self.horizon, )
-
接下来,我们将实现一个方法,在训练完成后关闭所有可视化窗口:
def close(self): if self.viz is not None: self.viz.close() self.viz = None
-
现在,我们可以实现
execute_trade_action
方法,它在之前第 9 步的step(…)
方法中有所使用。我们将把实现过程分为三个步骤,每个步骤对应一个订单类型:Hold、Buy 和 Sell。我们先从 Hold 订单类型开始,因为它是最简单的。稍后你会明白为什么!def execute_trade_action(self, action): if action == 0: # Hold position return
-
实际上,在我们继续实现买入和卖出订单执行逻辑之前,我们需要实现另一个中间步骤。在这里,我们必须确定订单类型(买入或卖出),然后获取当前模拟时间下比特币的价格:
order_type = "buy" if action == 1 else "sell" # Stochastically determine the current price # based on Market Open & Close current_price = random.uniform( self.ohlcv_df.loc[self.current_step, "Open"], self.ohlcv_df.loc[self.current_step, "Close"], )
-
现在,我们准备好实现执行买入交易订单的逻辑,代码如下:
if order_type == "buy": allowable_coins = \ int(self.cash_balance / current_price) if allowable_coins < self.order_size: # Not enough cash to execute a buy order return # Simulate a BUY order and execute it at # current_price num_coins_bought = self.order_size current_cost = self.cost_basis * \ self.num_coins_held additional_cost = num_coins_bought * \ current_price self.cash_balance -= additional_cost self.cost_basis = (current_cost + \ additional_cost) / ( self.num_coins_held + num_coins_bought ) self.num_coins_held += num_coins_bought
-
让我们使用最新的买入交易更新
trades
列表:self.trades.append( { "type": "buy", "step": self.current_step, "shares": num_coins_bought, "proceeds": additional_cost, } )
-
下一步是实现执行卖出交易订单的逻辑:
elif order_type == "sell": # Simulate a SELL order and execute it at # current_price if self.num_coins_held < self.order_size: # Not enough coins to execute a sell # order return num_coins_sold = self.order_size self.cash_balance += num_coins_sold * \ current_price self.num_coins_held -= num_coins_sold sale_proceeds = num_coins_sold * \ current_price self.trades.append( { "type": "sell", "step": self.current_step, "shares": num_coins_sold, "proceeds": sale_proceeds, } )
-
为了完成我们的交易执行函数,我们需要添加几行代码来更新账户价值,一旦交易订单执行完毕:
if self.num_coins_held == 0: self.cost_basis = 0 # Update account value self.account_value = self.cash_balance + \ self.num_coins_held * \ current_price
-
到此为止,我们已经完成了一个由 Gemini 加密货币交易所提供的真实 BTCUSD 数据驱动的比特币交易强化学习环境的实现!让我们看看如何轻松创建环境并运行示例,而不是在这个环境中使用一个随机代理,所有这一切只需要六行代码:
if __name__ == "__main__": env = CryptoTradingEnv() obs = env.reset() for _ in range(600): action = env.action_space.sample() next_obs, reward, done, _ = env.step(action) env.render()
你应该能看到在
CryptoTradingEnv
环境中随机代理的示例。env.render()
函数应该产生类似以下的渲染:
图 4.2 – 展示 CryptoTradingEnv 环境的渲染,显示代理的当前账户余额以及买卖交易的执行情况
现在,让我们看看这一切是如何运作的。
它是如何工作的……
在这个配方中,我们实现了CryptoTradingEnv
函数,它提供了形状为(6, horizon + 1)的表格型观察数据,其中 horizon 可以通过env_config
字典进行配置。horizon 参数指定了时间窗口的持续时间(例如,3 天),即 Agent 在每次交易之前允许观察加密货币市场数据的时间长度。一旦 Agent 执行了允许的离散动作之一——0(保持)、1(买入)或 2(卖出)——相应的交易将在当前的加密货币(比特币)交易价格下执行,并且交易账户余额将随之更新。Agent 还将根据从本集开始的交易所获得的利润(或亏损)获得奖励。
使用价格图表构建以太坊交易强化学习平台
这个配方将教你如何为 RL 代理实现一个以太坊加密货币交易环境,提供视觉观察数据。Agent 将观察指定时间段内的价格图表,图表包含开盘价、最高价、最低价、收盘价和交易量信息,以便做出决策(保持、买入或卖出)。Agent 的目标是最大化其奖励,即如果你将 Agent 部署到你的账户进行交易时所能获得的利润!
准备就绪
为了完成这个食谱,确保你使用的是最新版本。你需要激活tf2rl-cookbook
Python/conda 虚拟环境。确保它会更新环境,使其匹配最新的 conda 环境规格文件(tfrl-cookbook.yml
),该文件可以在本食谱的代码库中找到。如果以下import
语句没有问题,你就可以开始了:
import os
import random
from typing import Dict
import cv2
import gym
import numpy as np
import pandas as pd
from gym import spaces
from trading_utils import TradeVisualizer
如何做到…
让我们遵循 OpenAI Gym 框架来实现我们的学习环境接口。我们将添加一些逻辑,模拟加密货币交易执行并适当地奖励智能体,因为这将有助于你的学习。
按照以下步骤完成你的实现:
-
让我们通过使用字典来配置环境:
env_config = { "exchange": "Gemini", # Cryptocurrency exchange # (Gemini, coinbase, kraken, etc.) "ticker": "ETHUSD", # CryptoFiat "frequency": "daily", # daily/hourly/minutes "opening_account_balance": 100000, # Number of steps (days) of data provided to the # agent in one observation "observation_horizon_sequence_length": 30, "order_size": 1, # Number of coins to buy per # buy/sell order }
-
让我们定义
CryptoTradingVisualEnv
类并从env_config
加载设置:class CryptoTradingVisualEnv(gym.Env): def __init__(self, env_config: Dict = env_config): """Cryptocurrency trading environment for RL agents The observations are cryptocurrency price info (OHLCV) over a horizon as specified in env_config. Action space is discrete to perform buy/sell/hold trades. Args: ticker(str, optional): Ticker symbol for the\ crypto-fiat currency pair. Defaults to "ETHUSD". env_config (Dict): Env configuration values """ super(CryptoTradingVisualEnv, self).__init__() self.ticker = env_config.get("ticker", "ETHUSD") data_dir = os.path.join(os.path.dirname(os.path.\ realpath(__file__)), "data") self.exchange = env_config["exchange"] freq = env_config["frequency"]
-
下一步,根据市场数据源的频率配置,加载来自输入流的加密货币交易所数据:
if freq == "daily": self.freq_suffix = "d" elif freq == "hourly": self.freq_suffix = "1hr" elif freq == "minutes": self.freq_suffix = "1min" self.ticker_file_stream = os.path.join( f"{data_dir}", f"{'_'.join([self.exchange, self.ticker, \ self.freq_suffix])}.csv", ) assert os.path.isfile( self.ticker_file_stream ), f"Cryptocurrency exchange data file stream \ not found at: data/{self.ticker_file_stream}.csv" # Cryptocurrency exchange data stream. An offline # file stream is used. Alternatively, a web # API can be used to pull live data. self.ohlcv_df = pd.read_csv(self.ticker_file_\ stream, skiprows=1).sort_values( by="Date" )
-
让我们初始化其他环境类变量,并定义状态和动作空间:
self.opening_account_balance = \ env_config["opening_account_balance"] # Action: 0-> Hold; 1-> Buy; 2 ->Sell; self.action_space = spaces.Discrete(3) self.observation_features = [ "Open", "High", "Low", "Close", "Volume ETH", "Volume USD", ] self.obs_width, self.obs_height = 128, 128 self.horizon = env_config.get(" observation_horizon_sequence_length") self.observation_space = spaces.Box( low=0, high=255, shape=(128, 128, 3), dtype=np.uint8, ) self.order_size = env_config.get("order_size") self.viz = None # Visualizer
-
让我们定义
reset
方法,以便(重新)初始化环境类变量:def reset(self): # Reset the state of the environment to an # initial state self.cash_balance = self.opening_account_balance self.account_value = self.opening_account_balance self.num_coins_held = 0 self.cost_basis = 0 self.current_step = 0 self.trades = [] if self.viz is None: self.viz = TradeVisualizer( self.ticker, self.ticker_file_stream, "TFRL-Cookbook\ Ch4-CryptoTradingVisualEnv", skiprows=1, ) return self.get_observation()
-
这个环境的关键特性是,智能体的观察是价格图表的图像,类似于你在人工交易员的计算机屏幕上看到的图表。这个图表包含闪烁的图形、红绿条和蜡烛!让我们定义
get_observation
方法,以返回图表屏幕的图像:def get_observation(self): """Return a view of the Ticker price chart as image observation Returns: img_observation(np.ndarray): Image of ticker candle stick plot with volume bars as observation """ img_observation = \ self.viz.render_image_observation( self.current_step, self.horizon ) img_observation = cv2.resize( img_observation, dsize=(128, 128), interpolation=cv2.INTER_CUBIC ) return img_observation
-
现在,我们将实现交易环境的交易执行逻辑。必须从市场数据流中提取以太坊加密货币(以美元计)的当前价格(在本例中为一个文件):
def execute_trade_action(self, action): if action == 0: # Hold position return order_type = "buy" if action == 1 else "sell" # Stochastically determine the current price # based on Market Open & Close current_price = random.uniform( self.ohlcv_df.loc[self.current_step, "Open"], self.ohlcv_df.loc[self.current_step, "Close"], )
-
如果智能体决定执行买入订单,我们必须计算智能体在单步中可以购买的以太坊代币/币的数量,并在模拟交易所执行“买入”订单:
# Buy Order allowable_coins = \ int(self.cash_balance / current_price) if allowable_coins < self.order_size: # Not enough cash to execute a buy order return # Simulate a BUY order and execute it at # current_price num_coins_bought = self.order_size current_cost = self.cost_basis * \ self.num_coins_held additional_cost = num_coins_bought * \ current_price self.cash_balance -= additional_cost self.cost_basis = \ (current_cost + additional_cost) / ( self.num_coins_held + num_coins_bought ) self.num_coins_held += num_coins_bought self.trades.append( { "type": "buy", "step": self.current_step, "shares": num_coins_bought, "proceeds": additional_cost, } )
-
相反,如果智能体决定卖出,以下逻辑将执行卖出订单:
# Simulate a SELL order and execute it at # current_price if self.num_coins_held < self.order_size: # Not enough coins to execute a sell # order return num_coins_sold = self.order_size self.cash_balance += num_coins_sold * \ current_price self.num_coins_held -= num_coins_sold sale_proceeds = num_coins_sold * \ current_price self.trades.append( { "type": "sell", "step": self.current_step, "shares": num_coins_sold, "proceeds": sale_proceeds, } )
-
让我们更新账户余额,以反映买卖交易的影响:
if self.num_coins_held == 0: self.cost_basis = 0 # Update account value self.account_value = self.cash_balance + \ self.num_coins_held * \ current_price
-
我们现在准备实现
step
方法:def step(self, action): # Execute one step within the trading environment self.execute_trade_action(action) self.current_step += 1 reward = self.account_value - \ self.opening_account_balance # Profit (loss) done = self.account_value <= 0 or \ self.current_step >= len( self.ohlcv_df.loc[:, "Open"].values ) obs = self.get_observation() return obs, reward, done, {}
-
让我们实现一个方法,将当前状态渲染为图像并显示到屏幕上。这将帮助我们理解智能体在学习交易时环境中发生了什么:
def render(self, **kwargs): # Render the environment to the screen if self.current_step > self.horizon: self.viz.render( self.current_step, self.account_value, self.trades, window_size=self.horizon, )
-
这就完成了我们的实现!让我们快速查看一下使用随机智能体的环境:
if __name__ == "__main__": env = CryptoTradingVisualEnv() obs = env.reset() for _ in range(600): action = env.action_space.sample() next_obs, reward, done, _ = env.step(action) env.render()
你应该看到示例随机智能体在
CryptoTradinVisualEnv
中执行的情况,其中智能体接收与此处所示相似的视觉/图像观察:
图 4.3 – 发送给学习智能体的示例观察
就这样,这个食谱完成了!
它是如何工作的……
在这个食谱中,我们实现了一个可视化的以太坊加密货币交易环境,提供图像作为代理的输入。图像包含了图表信息,如开盘、最高、最低、收盘和成交量数据。这个图表看起来就像一个人类交易员的屏幕,向代理提供当前市场的信号。
构建一个高级的加密货币交易平台为 RL 代理
如果我们不让代理只采取离散的动作,比如购买/卖出/持有预设数量的比特币或以太坊代币,而是让代理决定它想买或卖多少加密货币/代币呢?这正是这个食谱所要让你实现的功能,创建一个CryptoTradingVisualContinuousEnv
的 RL 环境。
准备工作
为了完成这个方案,你需要确保你拥有最新版本的内容。你需要激活tf2rl-cookbook
Python/conda 虚拟环境。确保你更新环境,以便它符合最新的 conda 环境规范文件(tfrl-cookbook.yml
),该文件可以在这个食谱的代码库中找到。如果以下的import
语句没有任何问题地运行,那么你就可以开始了:
import os
import random
from typing import Dict
import cv2
import gym
import numpy as np
import pandas as pd
from gym import spaces
from trading_utils import TradeVisualizer
怎么做…
这是一个复杂的环境,因为它使用高维图像作为观察输入,并允许执行连续的真实值动作。不过,由于你在本章中已经实现了前面几个食谱的经验,你很可能已经熟悉这个食谱的各个组成部分。
让我们开始吧:
-
首先,我们需要定义该环境允许的配置参数:
env_config = { "exchange": "Gemini", # Cryptocurrency exchange # (Gemini, coinbase, kraken, etc.) "ticker": "BTCUSD", # CryptoFiat "frequency": "daily", # daily/hourly/minutes "opening_account_balance": 100000, # Number of steps (days) of data provided to the # agent in one observation "observation_horizon_sequence_length": 30, }
-
让我们直接进入学习环境类的定义:
class CryptoTradingVisualContinuousEnv(gym.Env): def __init__(self, env_config: Dict = env_config): """Cryptocurrency trading environment for RL agents with continuous action space Args: ticker (str, optional): Ticker symbol for the crypto-fiat currency pair. Defaults to "BTCUSD". env_config (Dict): Env configuration values """ super(CryptoTradingVisualContinuousEnv, self).__init__() self.ticker = env_config.get("ticker", "BTCUSD") data_dir = os.path.join(os.path.dirname(os.path.\ realpath(__file__)), "data") self.exchange = env_config["exchange"] freq = env_config["frequency"] if freq == "daily": self.freq_suffix = "d" elif freq == "hourly": self.freq_suffix = "1hr" elif freq == "minutes": self.freq_suffix = "1min"
-
这一步很直接,因为我们只需要将市场数据从输入源加载到内存中:
self.ticker_file_stream = os.path.join( f"{data_dir}", f"{'_'.join([self.exchange, self.ticker, \ self.freq_suffix])}.csv", ) assert os.path.isfile( self.ticker_file_stream ), f"Cryptocurrency exchange data file stream \ not found at: data/{self.ticker_file_stream}.csv" # Cryptocurrency exchange data stream. An offline # file stream is used. Alternatively, a web # API can be used to pull live data. self.ohlcv_df = pd.read_csv( self.ticker_file_stream, skiprows=1).sort_values(by="Date" ) self.opening_account_balance = \ env_config["opening_account_balance"]
-
现在,让我们定义环境的连续动作空间和观察空间:
self.action_space = spaces.Box( low=np.array([-1]), high=np.array([1]), \ dtype=np.float ) self.observation_features = [ "Open", "High", "Low", "Close", "Volume BTC", "Volume USD", ] self.obs_width, self.obs_height = 128, 128 self.horizon = env_config.get( "observation_horizon_sequence_length") self.observation_space = spaces.Box( low=0, high=255, shape=(128, 128, 3), dtype=np.uint8, )
-
让我们定义环境中
step
方法的大致框架。接下来的步骤中我们将完成帮助方法的实现:def step(self, action): # Execute one step within the environment self.execute_trade_action(action) self.current_step += 1 reward = self.account_value - \ self.opening_account_balance # Profit (loss) done = self.account_value <= 0 or \ self.current_step >= len( self.ohlcv_df.loc[:, "Open"].values ) obs = self.get_observation() return obs, reward, done, {}
-
第一个帮助方法是
execute_trade_action
方法。接下来的几步实现应该很简单,因为前面几个食谱已经实现了在交易所按汇率买卖加密货币的逻辑:def execute_trade_action(self, action): if action == 0: # Indicates "HODL" action # HODL position; No trade to be executed return order_type = "buy" if action > 0 else "sell" order_fraction_of_allowable_coins = abs(action) # Stochastically determine the current price # based on Market Open & Close current_price = random.uniform( self.ohlcv_df.loc[self.current_step, "Open"], self.ohlcv_df.loc[self.current_step, "Close"], )
-
可以通过如下方式模拟交易所中的买入订单:
if order_type == "buy": allowable_coins = \ int(self.cash_balance / current_price) # Simulate a BUY order and execute it at # current_price num_coins_bought = int(allowable_coins * \ order_fraction_of_allowable_coins) current_cost = self.cost_basis * \ self.num_coins_held additional_cost = num_coins_bought * \ current_price self.cash_balance -= additional_cost self.cost_basis = (current_cost + \ additional_cost) / ( self.num_coins_held + num_coins_bought ) self.num_coins_held += num_coins_bought if num_coins_bought > 0: self.trades.append( { "type": "buy", "step": self.current_step, "shares": num_coins_bought, "proceeds": additional_cost, } )
-
同样地,卖出订单可以通过以下方式模拟:
elif order_type == "sell": # Simulate a SELL order and execute it at # current_price num_coins_sold = int( self.num_coins_held * \ order_fraction_of_allowable_coins ) self.cash_balance += num_coins_sold * \ current_price self.num_coins_held -= num_coins_sold sale_proceeds = num_coins_sold * \ current_price if num_coins_sold > 0: self.trades.append( { "type": "sell", "step": self.current_step, "shares": num_coins_sold, "proceeds": sale_proceeds, } )
-
一旦买入/卖出订单执行完毕,账户余额需要更新:
if self.num_coins_held == 0: self.cost_basis = 0 # Update account value self.account_value = self.cash_balance + \ self.num_coins_held * \ current_price
-
为了测试
CryptoTradingVisualcontinuousEnv
,你可以使用以下代码行来进行__main__
函数的测试:if __name__ == "__main__": env = CryptoTradingVisualContinuousEnv() obs = env.reset() for _ in range(600): action = env.action_space.sample() next_obs, reward, done, _ = env.step(action) env.render()
它是如何工作的…
CryptoTradingVisualcontinuousEnv
提供了一个强化学习环境,观察值是类似交易者屏幕的图像,并为代理提供了一个连续的、实值的动作空间。在这个环境中,动作是单维的、连续的且实值的,大小表示加密货币的购买/出售比例。如果动作为正(0 到 1),则解释为买入指令;如果动作为负(-1 到 0),则解释为卖出指令。这个比例值根据交易账户中的余额转换成可以买卖的加密货币数量。
使用强化学习训练加密货币交易机器人
Soft Actor-Critic(SAC)代理是目前最流行、最先进的强化学习代理之一,基于一个脱离策略的最大熵深度强化学习算法。这个配方提供了你从零开始构建 SAC 代理所需的所有组件,使用 TensorFlow 2.x,并使用来自 Gemini 加密货币交易所的真实数据来训练它进行加密货币(比特币、以太坊等)交易。
准备工作
为了完成这个配方,请确保你使用的是最新版本。你需要激活tf2rl-cookbook
的 Python/conda 虚拟环境。确保更新环境,使其与最新的 conda 环境规格文件(tfrl-cookbook.yml
)匹配,该文件可以在本配方的代码库中找到。如果以下import
语句没有问题,说明你可以开始操作了:
mport functools
import os
import random
from collections import deque
from functools import reduce
import imageio
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
from crypto_trading_continuous_env import CryptoTradingContinuousEnv
如何操作…
这个配方将指导你逐步实现 SAC 代理的过程,并帮助你在加密货币交易环境中训练代理,从而实现自动化的盈利机器!
让我们准备好,开始实现:
-
SAC 是一个演员-评论家代理,所以它有演员和评论家两个组件。让我们先定义使用 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
-
接下来,让我们定义评论家神经网络:
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
-
给定当前模型的权重和目标模型的权重,让我们实现一个快速的函数,利用
tau
作为平均因子,慢慢更新目标权重。这就像 Polyak 平均步骤: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)
-
我们现在准备初始化我们的 SAC 代理类:
class SAC(object): def __init__( self, env, 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, ): self.env = env self.state_shape = env.observation_space.shape # shape of observations self.action_shape = env.action_space.shape # number of actions self.action_bound = (env.action_space.high - \ env.action_space.low) / 2 self.action_shift = (env.action_space.high + \ env.action_space.low) / 2 self.memory = deque(maxlen=int(memory_cap))
-
作为下一步,我们将初始化演员网络,并打印演员神经网络的摘要:
# 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())
-
接下来,我们将定义两个评论家网络,并打印评论家神经网络的摘要:
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())
-
让我们初始化
alpha
温度参数和目标熵: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)
-
我们还将初始化 SAC 的其他超参数:
self.gamma = gamma # discount factor self.tau = tau # target model update self.batch_size = batch_size
-
这完成了 SAC 代理的
__init__
方法。接下来,我们将实现一个方法来(预)处理采取的动作: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
-
我们现在准备实现
act
方法,以便在给定状态下生成 SAC 代理的动作: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
-
为了将经验保存到回放记忆中,让我们实现
remember
函数:def remember(self, state, action, reward, next_state, done): state = state.reshape(-1) # Flatten state state = np.expand_dims(state, axis=0) next_state = next_state.reshape(-1) # Flatten next-state next_state = np.expand_dims(next_state, axis=0) self.memory.append([state, action, reward, next_state, done])
-
现在,让我们开始实现经验回放过程。我们将从初始化回放方法开始。我们将在接下来的步骤中完成回放方法的实现:
def replay(self): if len(self.memory) < self.batch_size: return samples = random.sample(self.memory, self.batch_size) s = np.array(samples).T states, actions, rewards, next_states, dones = [ np.vstack(s[i, :]).astype(np.float) for i in\ range(5) ]
-
让我们启动一个持久化的
GradientTape
函数,并开始累积梯度。我们通过处理动作并获取下一组动作和对数概率来实现这一点:with tf.GradientTape(persistent=True) as tape: # next state action log probs means, log_stds = self.actor(next_states) log_stds = tf.clip_by_value(log_stds, self.log_std_min, self.log_std_max) next_actions, log_probs = \ self.process_actions(means, log_stds)
-
这样,我们现在可以计算两个评论者网络的损失:
current_q_1 = self.critic_1([states, actions]) current_q_2 = self.critic_2([states, actions]) next_q_1 = self.critic_target_1([next_states, next_actions]) next_q_2 = self.critic_target_2([next_states, next_actions]) next_q_min = tf.math.minimum(next_q_1, next_q_2) state_values = next_q_min - self.alpha * \ log_probs target_qs = tf.stop_gradient( rewards + state_values * self.gamma * \ (1.0 - dones) ) critic_loss_1 = tf.reduce_mean( 0.5 * tf.math.square(current_q_1 - \ target_qs) ) critic_loss_2 = tf.reduce_mean( 0.5 * tf.math.square(current_q_2 - \ target_qs) )
-
当前的状态-动作对和由演员提供的对数概率可以通过以下方式计算:
means, log_stds = self.actor(states) log_stds = tf.clip_by_value(log_stds, self.log_std_min, self.log_std_max) actions, log_probs = \ self.process_actions(means, log_stds)
-
我们现在可以计算演员的损失并将梯度应用到评论者上:
current_q_1 = self.critic_1([states, actions]) current_q_2 = self.critic_2([states, actions]) current_q_min = tf.math.minimum(current_q_1, current_q_2) actor_loss = tf.reduce_mean(self.alpha * \ log_probs - current_q_min) if self.auto_alpha: alpha_loss = -tf.reduce_mean( (self.log_alpha * \ tf.stop_gradient(log_probs + \ self.target_entropy)) ) critic_grad = tape.gradient( critic_loss_1, self.critic_1.trainable_variables ) self.critic_optimizer_1.apply_gradients( zip(critic_grad, self.critic_1.trainable_variables) )
-
类似地,我们可以计算并应用演员的梯度:
critic_grad = tape.gradient( critic_loss_2, self.critic_2.trainable_variables ) # compute actor gradient self.critic_optimizer_2.apply_gradients( zip(critic_grad, self.critic_2.trainable_variables) ) actor_grad = tape.gradient( actor_loss, self.actor.trainable_variables ) # compute actor gradient self.actor_optimizer.apply_gradients( zip(actor_grad, self.actor.trainable_variables) )
-
现在,让我们将摘要记录到 TensorBoard:
# tensorboard info self.summaries["q1_loss"] = critic_loss_1 self.summaries["q2_loss"] = critic_loss_2 self.summaries["actor_loss"] = actor_loss if self.auto_alpha: # optimize temperature alpha_grad = tape.gradient(alpha_loss, [self.log_alpha]) self.alpha_optimizer.apply_gradients( zip(alpha_grad, [self.log_alpha])) self.alpha.assign(tf.exp(self.log_alpha)) # tensorboard info self.summaries["alpha_loss"] = alpha_loss
-
这完成了我们的经验回放方法。现在,我们可以继续
train
方法的实现。让我们从初始化train
方法开始。我们将在接下来的步骤中完成此方法的实现:def train(self, max_epochs=8000, random_epochs=1000, max_steps=1000, save_freq=50): 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) done, use_random, episode, steps, epoch, \ episode_reward = ( False, True, 0, 0, 0, 0, ) cur_state = self.env.reset()
-
现在,我们准备开始主训练循环。首先,让我们处理结束集的情况:
while epoch < max_epochs: if steps > max_steps: done = True if done: episode += 1 print( "episode {}: {} total reward, {} alpha, {} steps, {} epochs".format( episode, episode_reward, self.alpha.numpy(), steps, 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, self.env.reset(), 0, 0 if episode % save_freq == 0: self.save_model( "sac_actor_episode{}.h5".\ format(episode), "sac_critic_episode{}.h5".\ format(episode), )
-
每次进入环境时,SAC 代理学习需要执行以下步骤:
if epoch > random_epochs and \ len(self.memory) > self.batch_size: use_random = False action = self.act(cur_state, \ use_random=use_random) # determine action next_state, reward, done, _ = \ self.env.step(action[0]) # act on env # self.env.render(mode='rgb_array') self.remember(cur_state, action, reward, next_state, done) #add to memory self.replay() # train models through memory # replay update_target_weights( self.critic_1, self.critic_target_1, tau=self.tau ) # iterates target model update_target_weights(self.critic_2, self.critic_target_2, tau=self.tau) cur_state = next_state episode_reward += reward steps += 1 epoch += 1
-
处理完代理更新后,我们现在可以将一些有用的信息记录到 TensorBoard 中:
# Tensorboard update with summary_writer.as_default(): if len(self.memory) > self.batch_size: tf.summary.scalar( "Loss/actor_loss", self.summaries["actor_loss"], step=epoch ) tf.summary.scalar( "Loss/q1_loss", self.summaries["q1_loss"], step=epoch ) tf.summary.scalar( "Loss/q2_loss", self.summaries["q2_loss"], step=epoch ) if self.auto_alpha: tf.summary.scalar( "Loss/alpha_loss", self.summaries["alpha_loss"], step=epoch ) tf.summary.scalar("Stats/alpha", self.alpha, step=epoch) if self.auto_alpha: tf.summary.scalar("Stats/log_alpha", self.log_alpha, step=epoch) tf.summary.scalar("Stats/q_min", self.summaries["q_min"], step=epoch) tf.summary.scalar("Stats/q_mean", self.summaries["q_mean"], step=epoch) tf.summary.scalar("Main/step_reward", reward, step=epoch) summary_writer.flush()
-
作为我们
train
方法实现的最后一步,我们可以保存演员和评论者模型,以便在需要时恢复训练或从检查点重新加载:self.save_model( "sac_actor_final_episode{}.h5".format(episode), "sac_critic_final_episode{}.h5".format(episode), )
-
现在,我们将实际实现之前引用的
save_model
方法:def save_model(self, a_fn, c_fn): self.actor.save(a_fn) self.critic_1.save(c_fn)
-
让我们快速实现一个方法,从保存的模型中加载演员和评论者的状态,以便在需要时可以从之前保存的检查点恢复或继续:
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 代理,我们可以实现一个辅助方法:
def test(self, render=True, fps=30, filename="test_render.mp4"): cur_state, done, rewards = self.env.reset(), \ False, 0 video = imageio.get_writer(filename, fps=fps) while not done: action = self.act(cur_state, test=True) next_state, reward, done, _ = \ self.env.step(action[0]) cur_state = next_state rewards += reward if render: video.append_data( self.env.render(mode="rgb_array")) video.close() return rewards
-
这完成了我们的 SAC 代理实现。我们现在准备在
CryptoTradingContinuousEnv
中训练 SAC 代理:if __name__ == "__main__": gym_env = CryptoTradingContinuousEnv() sac = SAC(gym_env) # Load Actor and Critic from previously saved # checkpoints # sac.load_actor("sac_actor_episodexyz.h5") # sac.load_critic("sac_critic_episodexyz.h5") sac.train(max_epochs=100000, random_epochs=10000, save_freq=50) reward = sac.test() print(reward)
它是如何工作的…
SAC 是一种强大的 RL 算法,已证明在各种 RL 仿真环境中有效。SAC 不仅优化最大化每集奖励,还最大化代理策略的熵。您可以通过 TensorBoard 观察代理的学习进度,因为这个示例包括了记录代理进展的代码。您可以使用以下命令启动 TensorBoard:
tensorboard –-logdir=logs
上述命令将启动 TensorBoard。您可以通过浏览器在默认地址http://localhost:6006
访问它。这里提供了一个 TensorBoard 截图供参考:
图 4.4 – TensorBoard 截图,显示 SAC 代理在 CryptoTradingContinuousEnv 中的训练进度
这就是本章节的内容。祝你训练愉快!
第五章:第五章:现实世界中的强化学习——构建股票/股市交易智能体
基于软件的深度强化学习(深度 RL)智能体在执行交易策略时具有巨大的潜力,能够不知疲倦且毫无瑕疵地执行任务,且不受人类交易员常见的内存容量、速度、效率以及情绪干扰等限制。股票市场中的盈利交易涉及在考虑多个市场因素(如交易条件、宏观和微观市场条件、社会、政治和公司特定变化)后,仔细执行买入/卖出交易,并处理股票符号/代码。深度 RL 智能体在解决现实世界中的复杂问题时具有巨大潜力,并且存在大量的机会。
然而,由于与现实世界中 RL 智能体部署相关的各种挑战,目前只有少数成功的案例,展示了深度强化学习(RL)智能体在游戏以外的现实世界中的应用。本章包含的食谱将帮助你成功地开发用于另一个有趣且有回报的现实世界问题——股票市场交易的 RL 智能体。提供的食谱包含有关如何实现与 OpenAI Gym 兼容的股票市场交易学习环境的定制方法,这些环境包括离散和连续值的动作空间。此外,你还将学习如何为股票交易学习环境构建和训练 RL 智能体。
本章将涵盖以下食谱:
-
使用真实股票交易数据构建股票市场交易强化学习平台
-
使用价格图表构建股票市场交易强化学习平台
-
构建一个先进的股票交易强化学习平台,训练智能体模仿专业交易员
让我们开始吧!
技术要求
本书中的代码已在 Ubuntu 18.04 和 Ubuntu 20.04 上进行了广泛测试,如果安装了 Python 3.6+,则应适用于更新版本的 Ubuntu。安装了 Python 3.6+ 及所需的 Python 包(如每个章节开始时列出的包)后,代码也应能在 Windows 和 Mac OS X 上正常运行。你应该创建并使用名为 tf2rl-cookbook
的 Python 虚拟环境来安装包并运行本书中的代码。建议安装 Miniconda 或 Anaconda 以便进行 Python 虚拟环境管理。
每章中的完整代码可以在此处找到:github.com/PacktPublishing/Tensorflow-2-Reinforcement-Learning-Cookbook
。
使用真实股票交易数据构建股票市场交易强化学习平台
股票市场为任何人提供了一个高利润的机会,可以参与并赚取利润。虽然它易于接触,但并不是所有人都能持续获得盈利的交易,因为市场的动态性和情绪因素可能会影响人的决策。RL 代理将情绪因素排除在外,并可以训练成持续盈利的交易者。这个配方将教你如何实现一个股票市场交易环境,让你的 RL 代理学习如何使用真实的股市数据进行交易。当你训练足够后,你可以部署它们,让它们自动为你进行交易(并赚钱)!
准备开始
要完成此配方,请确保您拥有最新版本。您需要激活tf2rl-cookbook
的 Python/conda 虚拟环境。请确保更新环境,以便它与最新的 conda 环境规范文件(tfrl-cookbook.yml
)匹配,您可以在本烹饪书的代码库中找到该文件。如果以下import
语句运行没有任何问题,您就可以开始了:
import os
import random
from typing import Dict
import gym
import numpy as np
import pandas as pd
from gym import spaces
from trading_utils import TradeVisualizer
如何操作…
按照以下逐步流程来实现StockTradingEnv
:
-
让我们初始化环境的可配置参数:
env_config = { "ticker": "TSLA", "opening_account_balance": 1000, # Number of steps (days) of data provided to the # agent in one observation "observation_horizon_sequence_length": 30, "order_size": 1, # Number of shares to buy per # buy/sell order }
-
让我们初始化
StockTradingEnv
类并加载配置的股票交易代码的数据:class StockTradingEnv(gym.Env): def __init__(self, env_config: Dict = env_config): """Stock trading environment for RL agents Args: ticker (str, optional): Ticker symbol for the stock. Defaults to "MSFT". env_config (Dict): Env configuration values """ super(StockTradingEnv, self).__init__() self.ticker = env_config.get("ticker", "MSFT") data_dir = os.path.join(os.path.dirname(os.path.\ realpath(__file__)), "data") self.ticker_file_stream = os.path.join(f"{ data_dir}", f"{self.ticker}.csv")
-
让我们确保股票市场数据源存在,然后加载数据流:
assert os.path.isfile( self.ticker_file_stream ), f"Historical stock data file stream not found at: data/{self.ticker}.csv" # Stock market data stream. An offline file # stream is used. Alternatively, a web # API can be used to pull live data. # Data-Frame: Date Open High Low Close Adj-Close # Volume self.ohlcv_df = \ pd.read_csv(self.ticker_file_stream)
-
现在我们准备好定义观察空间和动作空间/环境,以便完成初始化函数的定义:
self.opening_account_balance = \ env_config["opening_account_balance"] # Action: 0-> Hold; 1-> Buy; 2 ->Sell; self.action_space = spaces.Discrete(3) self.observation_features = [ "Open", "High", "Low", "Close", "Adj Close", "Volume", ] self.horizon = env_config.get( "observation_horizon_sequence_length") self.observation_space = spaces.Box( low=0, high=1, shape=(len(self.observation_features), self.horizon + 1), dtype=np.float, ) self.order_size = env_config.get("order_size")
-
接下来,我们将实现一个方法,以便收集观察数据:
def get_observation(self): # Get stock price info data table from input # (file/live) stream observation = ( self.ohlcv_df.loc[ self.current_step : self.current_step + \ self.horizon, self.observation_features, ] .to_numpy() .T ) return observation
-
接下来,为了执行交易订单,我们需要确保相关逻辑已经就位。让我们现在添加这个:
def execute_trade_action(self, action): if action == 0: # Hold position return order_type = "buy" if action == 1 else "sell" # Stochastically determine the current stock # price based on Market Open & Close current_price = random.uniform( self.ohlcv_df.loc[self.current_step, "Open"], self.ohlcv_df.loc[self.current_step, "Close"], )
-
初始化完成后,我们可以添加买入股票的逻辑:
if order_type == "buy": allowable_shares = \ int(self.cash_balance / current_price) if allowable_shares < self.order_size: # Not enough cash to execute a buy order # return # Simulate a BUY order and execute it at # current_price num_shares_bought = self.order_size current_cost = self.cost_basis * \ self.num_shares_held additional_cost = num_shares_bought * \ current_price self.cash_balance -= additional_cost self.cost_basis = (current_cost + \ additional_cost) / ( self.num_shares_held + num_shares_bought ) self.num_shares_held += num_shares_bought self.trades.append( { "type": "buy", "step": self.current_step, "shares": num_shares_bought, "proceeds": additional_cost, } )
-
同样,我们现在可以添加卖出股票的逻辑:
elif order_type == "sell": # Simulate a SELL order and execute it at # current_price if self.num_shares_held < self.order_size: # Not enough shares to execute a sell # order return num_shares_sold = self.order_size self.cash_balance += num_shares_sold * \ current_price self.num_shares_held -= num_shares_sold sale_proceeds = num_shares_sold * current_price self.trades.append( { "type": "sell", "step": self.current_step, "shares": num_shares_sold, "proceeds": sale_proceeds, } )
-
最后,让我们更新账户余额:
# Update account value self.account_value = self.cash_balance + \ self.num_shares_held * \ current_price
-
我们现在准备好启动并检查新环境了:
if __name__ == "__main__": env = StockTradingEnv() obs = env.reset() for _ in range(600): action = env.action_space.sample() next_obs, reward, done, _ = env.step(action) env.render()
工作原理…
观察是股票价格信息(OHLCV),在env_config
中指定的时间范围内。动作空间是离散的,以便我们可以执行买入/卖出/持有操作。这是一个为 RL 代理设计的入门环境,旨在帮助其学习在股市中进行交易。祝您训练愉快!
使用价格图表构建股票市场交易 RL 平台
人类交易者会查看多个指标来分析和识别潜在交易。我们能否也允许代理通过查看价格蜡烛图来进行交易,而不是仅仅提供表格/CSV 表示?当然可以!这个配方将教你如何为你的 RL 代理构建一个视觉丰富的交易环境。
准备开始
为了完成本教程,请确保你使用的是最新版本。你需要激活tf2rl-cookbook
Python/conda 虚拟环境。确保更新环境,使其与最新的 conda 环境规范文件(tfrl-cookbook.yml
)匹配,该文件可以在本教程的代码库中找到。如果以下import
语句没有问题,那么你就准备好开始了:
import os
import random
from typing import Dict
import cv2
import gym
import numpy as np
import pandas as pd
from gym import spaces
from trading_utils import TradeVisualizer
如何实现……
让我们从配置环境开始。接下来,我们将带你完成实现的过程。在本教程结束时,你将构建一个完整的股票交易 RL 环境,允许智能体处理可视化的股票图表并做出交易决策。
让我们开始吧:
-
配置学习环境,具体如下:
env_config = { "ticker": "TSLA", "opening_account_balance": 100000, # Number of steps (days) of data provided to the # agent in one observation "observation_horizon_sequence_length": 30, "order_size": 1, # Number of shares to buy per # buy/sell order }
-
让我们实现
StockTradingVisualEnv
的初始化步骤:class StockTradingVisualEnv(gym.Env): def __init__(self, env_config: Dict = env_config): """Stock trading environment for RL agents Args: ticker (str, optional): Ticker symbol for the stock. Defaults to "MSFT". env_config (Dict): Env configuration values """ super(StockTradingVisualEnv, self).__init__() self.ticker = env_config.get("ticker", "MSFT") data_dir = os.path.join(os.path.dirname(os.path.\ realpath(__file__)), "data") self.ticker_file_stream = os.path.join( f"{data_dir}", f"{self.ticker}.csv") assert os.path.isfile( self.ticker_file_stream ), f"Historical stock data file stream not found\ at: data/{self.ticker}.csv" # Stock market data stream. An offline file # stream is used. Alternatively, a web # API can be used to pull live data. # Data-Frame: Date Open High Low Close Adj-Close # Volume self.ohlcv_df = \ pd.read_csv(self.ticker_file_stream)
-
让我们完成
__init__
方法的实现:self.opening_account_balance = \ env_config["opening_account_balance"] self.action_space = spaces.Discrete(3) self.observation_features = [ "Open", "High", "Low", "Close", "Adj Close", "Volume", ] self.obs_width, self.obs_height = 128, 128 self.horizon = env_config.get( "observation_horizon_sequence_length") self.observation_space = spaces.Box( low=0, high=255, shape=(128, 128, 3), dtype=np.uint8, ) self.order_size = env_config.get("order_size") self.viz = None # Visualizer
-
下一步是为环境定义
step
方法:def step(self, action): # Execute one step within the trading environment self.execute_trade_action(action) self.current_step += 1 reward = self.account_value - \ self.opening_account_balance # Profit (loss) done = self.account_value <= 0 or \ self.current_step >= len( self.ohlcv_df.loc[:, "Open"].values ) obs = self.get_observation() return obs, reward, done, {}
-
让我们实现前一步中用到的两个缺失的方法。为了实现
get_observation
方法,我们需要初始化TradeVisualizer
方法。鉴于此,让我们首先实现reset
方法:def reset(self): # Reset the state of the environment to an # initial state self.cash_balance = self.opening_account_balance self.account_value = self.opening_account_balance self.num_shares_held = 0 self.cost_basis = 0 self.current_step = 0 self.trades = [] if self.viz is None: self.viz = TradeVisualizer( self.ticker, self.ticker_file_stream, "TFRL-Cookbook Ch4-StockTradingVisualEnv", ) return self.get_observation()
-
现在,让我们继续实现
get_observation
方法:def get_observation(self): """Return a view of the Ticker price chart as image observation Returns: img_observation (np.ndarray): Image of ticker candle stick plot with volume bars as observation """ img_observation = \ self.viz.render_image_observation( self.current_step, self.horizon ) img_observation = cv2.resize( img_observation, dsize=(128, 128), interpolation=cv2.INTER_CUBIC ) return img_observation
-
是时候实现执行智能体交易动作的逻辑了。我们将把交易执行逻辑的实现分为接下来的三步:
def execute_trade_action(self, action): if action == 0: # Hold position return order_type = "buy" if action == 1 else "sell" # Stochastically determine the current stock # price based on Market Open & Close current_price = random.uniform( self.ohlcv_df.loc[self.current_step, "Open"], self.ohlcv_df.loc[self.current_step, \ "Close"], )
-
让我们实现执行“买入”订单的逻辑:
if order_type == "buy": allowable_shares = \ int(self.cash_balance / current_price) if allowable_shares < self.order_size: return num_shares_bought = self.order_size current_cost = self.cost_basis * \ self.num_shares_held additional_cost = num_shares_bought * \ current_price self.cash_balance -= additional_cost self.cost_basis = (current_cost + \ additional_cost)/ \ (self.num_shares_held +\ num_shares_bought) self.num_shares_held += num_shares_bought self.trades.append( { "type": "buy", "step": self.current_step, "shares": num_shares_bought, "proceeds": additional_cost, } )
-
现在,让我们处理“卖出”订单:
elif order_type == "sell": # Simulate a SELL order and execute it at # current_price if self.num_shares_held < self.order_size: # Not enough shares to execute a sell # order return num_shares_sold = self.order_size self.cash_balance += num_shares_sold * \ current_price self.num_shares_held -= num_shares_sold sale_proceeds = num_shares_sold * \ current_price self.trades.append( { "type": "sell", "step": self.current_step, "shares": num_shares_sold, "proceeds": sale_proceeds, } ) if self.num_shares_held == 0: self.cost_basis = 0 # Update account value self.account_value = self.cash_balance + \ self.num_shares_held * \ current_price
-
到这里,我们的实现就完成了!现在我们可以使用一个随机动作的智能体来测试环境:
if __name__ == "__main__": env = StockTradingVisualEnv() obs = env.reset() for _ in range(600): action = env.action_space.sample() next_obs, reward, done, _ = env.step(action) env.render()
它是如何工作的……
StockTradingVisualEnv
中的观测值是一个时间范围内的股票价格信息(OHLCV),如env_config
中所指定。动作空间是离散的,因此我们可以进行买入/卖出/持有交易。更具体地,动作具有以下含义:0->持有;1->买入;2->卖出。
下图展示了环境的运行情况:
图 5.1 – StockTradingVisualEnv 运行的示例截图
构建一个先进的股票交易 RL 平台,训练智能体模仿专业交易员
本教程将帮助你实现一个完整的股票交易环境,具有高维图像观测空间和连续动作空间,用于训练你的 RL 和深度 RL 智能体。这将使你能够使用强化学习(RL)构建智能交易机器人,进而模拟专业股票交易员的交易方式。像专业交易员一样,你训练的 RL 智能体将通过蜡烛图和价格线图的形式观察股票市场数据,并做出交易决策。一个经过良好训练的 RL 智能体,可能在不需要休息或佣金的情况下进行数千次盈利交易,与人类专业交易员不同,从而大大提高你的利润!
正在准备中
要完成此配方,请确保您拥有最新版本。您需要激活 tf2rl-cookbook
Python/conda 虚拟环境。确保更新环境,使其与最新的 conda 环境规范文件(tfrl-cookbook.yml
)匹配,该文件可以在本书的代码仓库中找到。如果以下的 import
语句没有问题,则表示您可以开始了:
import os
import random
from typing import Dict
import cv2
import gym
import numpy as np
import pandas as pd
from gym import spaces
from trading_utils import TradeVisualizer
如何实现…
到现在为止,通过在本章中完成之前的配方,你应该已经熟悉了实现的基本流程。按照这些步骤,你可以从零开始构建一个完整的股市交易环境,用于训练你的高级 RL 智能体:
-
让我们开始实现
StockTradingVisualContinuousEnv
:def __init__(self, env_config: Dict = env_config): """Stock trading environment for RL agents with continuous action space Args: ticker (str, optional): Ticker symbol for the stock. Defaults to "MSFT". env_config (Dict): Env configuration values """ super(StockTradingVisualContinuousEnv, self).__init__() self.ticker = env_config.get("ticker", "MSFT") data_dir = os.path.join(os.path.dirname(os.path.\ realpath(__file__)), "data") self.ticker_file_stream = os.path.join( f"{data_dir}", f"{self.ticker}.csv") assert os.path.isfile( self.ticker_file_stream ), f"Historical stock data file stream not found at: data/{self.ticker}.csv" self.ohlcv_df = \ pd.read_csv(self.ticker_file_stream)
-
让我们定义状态空间、动作空间和其他必要的变量,以完成
__init__
方法的实现:self.opening_account_balance = \ env_config["opening_account_balance"] # Action: 1-dim value indicating a fraction # amount of shares to Buy (0 to 1) or # sell (-1 to 0). The fraction is taken on the # allowable number of # shares that can be bought or sold based on the # account balance (no margin). self.action_space = spaces.Box( low=np.array([-1]), high=np.array([1]), dtype=np.float ) self.observation_features = [ "Open", "High", "Low", "Close", "Adj Close", "Volume", ] self.obs_width, self.obs_height = 128, 128 self.horizon = env_config.get( "observation_horizon_sequence_length") self.observation_space = spaces.Box( low=0, high=255, shape=(128, 128, 3), dtype=np.uint8, ) self.viz = None # Visualizer
-
接下来,我们来实现
get_observation
方法:def get_observation(self): """Return a view of the Ticker price chart as image observation Returns: img_observation (np.ndarray): Image of ticker candle stick plot with volume bars as observation """ img_observation = \ self.viz.render_image_observation( self.current_step, self.horizon ) img_observation = cv2.resize( img_observation, dsize=(128, 128), interpolation=cv2.INTER_CUBIC ) return img_observation
-
让我们初始化交易执行逻辑:
def execute_trade_action(self, action): if action == 0: # Indicates "Hold" action # Hold position; No trade to be executed return order_type = "buy" if action > 0 else "sell" order_fraction_of_allowable_shares = abs(action) # Stochastically determine the current stock # price based on Market Open & Close current_price = random.uniform( self.ohlcv_df.loc[self.current_step, "Open"], self.ohlcv_df.loc[self.current_step, "Close"], )
-
现在,我们准备好定义
"buy"
动作的行为了:if order_type == "buy": allowable_shares = \ int(self.cash_balance / current_price) # Simulate a BUY order and execute it at # current_price num_shares_bought = int( allowable_shares * \ order_fraction_of_allowable_shares ) current_cost = self.cost_basis * \ self.num_shares_held additional_cost = num_shares_bought * \ current_price self.cash_balance -= additional_cost self.cost_basis = (current_cost + \ additional_cost) / ( self.num_shares_held + num_shares_bought ) self.num_shares_held += num_shares_bought if num_shares_bought > 0: self.trades.append( { "type": "buy", "step": self.current_step, "shares": num_shares_bought, "proceeds": additional_cost, } )
-
类似地,我们可以定义
"sell"
动作的行为,并更新账户余额来完成该方法的实现:elif order_type == "sell": # Simulate a SELL order and execute it at # current_price num_shares_sold = int( self.num_shares_held * \ order_fraction_of_allowable_shares ) self.cash_balance += num_shares_sold * \ current_price self.num_shares_held -= num_shares_sold sale_proceeds = num_shares_sold * \ current_price if num_shares_sold > 0: self.trades.append( { "type": "sell", "step": self.current_step, "shares": num_shares_sold, "proceeds": sale_proceeds, } ) if self.num_shares_held == 0: self.cost_basis = 0 # Update account value self.account_value = self.cash_balance + \ self.num_shares_held * \ current_price
-
我们现在准备好实现
step
方法,该方法允许智能体在环境中执行一步操作:def step(self, action): # Execute one step within the environment self.execute_trade_action(action) self.current_step += 1 reward = self.account_value - \ self.opening_account_balance # Profit (loss) done = self.account_value <= 0 or \ self.current_step >= len( self.ohlcv_df.loc[:, "Open"].values ) obs = self.get_observation() return obs, reward, done, {}
-
接下来,让我们实现
reset()
方法,该方法将在每个训练回合开始时执行:def reset(self): # Reset the state of the environment to an # initial state self.cash_balance = self.opening_account_balance self.account_value = self.opening_account_balance self.num_shares_held = 0 self.cost_basis = 0 self.current_step = 0 self.trades = [] if self.viz is None: self.viz = TradeVisualizer( self.ticker, self.ticker_file_stream, "TFRL-Cookbook \ Ch4-StockTradingVisualContinuousEnv", ) return self.get_observation()
-
让我们通过实现
render
和close
方法来完成环境的实现:def render(self, **kwargs): # Render the environment to the screen if self.current_step > self.horizon: self.viz.render( self.current_step, self.account_value, self.trades, window_size=self.horizon, ) def close(self): if self.viz is not None: self.viz.close() self.viz = None
-
现在,是时候让你在上一章中构建的一个智能体来训练和测试这个真实数据支持的股市交易环境了。现在,让我们用一个简单的随机智能体来测试这个环境:
if __name__ == "__main__": env = StockTradingVisualContinuousEnv() obs = env.reset() for _ in range(600): action = env.action_space.sample() next_obs, reward, done, _ = env.step(action) env.render()
它是如何工作的……
为了模拟股市,必须使用真实的股市数据流。我们使用离线基于文件的数据流作为替代,避免了需要互联网连接和可能需要用户账户来获取市场数据的网页 API。该文件流包含标准格式的市场数据:日期、开盘、最高、最低、收盘、调整收盘和成交量。
智能体以蜡烛图的形式观察股市数据,下面的图像供你参考:
图 5.2 – StockTradingVisualContinuousEnvironment 的视觉观察
智能体的动作和学习进度可以从以下图像中看到,该图像是通过 render()
方法生成的:
图 5.3 – 实时账户余额和智能体在当前时间窗口内执行的交易动作的可视化
这就是本配方和本章的内容。祝训练愉快!
第六章:第六章:现实世界中的强化学习 – 构建智能体来完成您的待办事项
RL 智能体需要与环境进行互动,以进行学习和训练。为现实世界的应用训练 RL 智能体通常会面临物理限制和挑战。这是因为智能体在学习过程中可能会对它所操作的现实世界系统造成损害。幸运的是,现实世界中有许多任务并不一定面临这些挑战,且可以非常有用,帮助我们完成日常待办事项列表中的任务!
本章中的食谱将帮助您构建能够完成互联网上任务的 RL 智能体,这些任务包括响应烦人的弹窗、在网页上预订航班、管理电子邮件和社交媒体账户等等。我们可以在不使用随着时间变化而改变的 API,或者不依赖硬编码脚本的情况下完成这些任务,这些脚本可能会在网页更新后停止工作。您将通过使用鼠标和键盘训练智能体完成这些待办事项任务,就像人类一样!本章还将帮助您构建WebGym API,它是一个兼容 OpenAI Gym 的通用 RL 学习环境接口,您可以使用它将 50 多种网页任务转换为 RL 的训练环境,并训练自己的 RL 智能体。
本章将覆盖以下具体内容:
-
为现实世界的强化学习(RL)构建学习环境
-
构建一个 RL 智能体来完成网页上的任务 – 行动呼吁
-
构建一个视觉自动登录机器人
-
训练一个 RL 智能体来自动化旅行中的航班预订
-
训练一个 RL 智能体来管理您的电子邮件
-
训练一个 RL 智能体来自动化社交媒体账户管理
开始吧!
技术要求
本书中的代码已经在 Ubuntu 18.04 和 Ubuntu 20.04 上进行了广泛测试,这意味着它应该也能在较新版本的 Ubuntu 上运行,只要安装了 Python 3.6 及以上版本。安装了 Python 3.6+,以及每个食谱的准备部分中列出的必要 Python 包后,代码应该也可以在 Windows 和 Mac OSX 上正常运行。建议您创建并使用名为tf2rl-cookbook
的 Python 虚拟环境来安装本书中的包并运行代码。推荐使用 Miniconda 或 Anaconda 进行 Python 虚拟环境管理。您还需要在系统上安装 Chromium Chrome 驱动程序。在 Ubuntu 18.04+上,您可以使用sudo apt-get install chromium-chromedriver
命令进行安装。
每个食谱中的完整代码将在此处提供:github.com/PacktPublishing/Tensorflow-2-Reinforcement-Learning-Cookbook
。
为现实世界的强化学习(RL)构建学习环境
本教程将教你如何设置并构建基于 世界比特(WoB)的 OpenAI Gym 兼容学习平台 WebGym,用于训练 RL 代理完成全球 web 上的真实任务。WoB 是一个开放领域的平台,专为基于 web 的代理而设计。如需了解更多关于 WoB 的信息,请查看以下链接:proceedings.mlr.press/v70/shi17a/shi17a.pdf
。
WebGym 提供了供代理学习的环境,让代理以我们(人类)感知全球 web 的方式进行感知——通过我们显示器上呈现的像素。代理通过使用键盘和鼠标事件作为操作与环境进行互动。这使得代理能够像我们一样体验全球 web,这意味着我们不需要对代理进行任何额外修改,代理就能进行训练。这样,我们便能够训练能够直接与 web 页面和应用程序协作,完成现实世界任务的 RL 代理。
下图展示了一个 点击行动(CTA)任务的示例环境,在此环境中,任务是点击特定链接以进入下一个页面或步骤:
图 6.1 – 需要点击特定链接的示例 CTA 任务
另一个 CTA 任务的示例如以下图片所示:
图 6.2 – 需要选择并提交特定选项的示例 CTA 任务
让我们开始吧!
准备就绪
要完成本教程,你需要激活 tf2rl-cookbook
Python/conda 虚拟环境。确保更新环境,以使其与本教程代码库中的最新 conda 环境规范文件(tfrl-cookbook.yml
)匹配。WebGym 是构建在 miniwob-plusplus 基准之上的,后者也作为本书代码库的一部分提供,便于使用。
现在,让我们开始吧!
如何操作……
我们将通过定义自定义的 reset
和 step
方法来构建 WebGym。然后,我们将为训练环境定义状态和动作空间。首先,我们来看看 miniwob_env
模块的实现。让我们开始:
-
让我们首先导入必要的 Python 模块:
import os import gym from PIL import Image from miniwob.action import MiniWoBCoordClick from miniwob.environment import MiniWoBEnvironment
-
让我们指定导入本地
miniwob
环境的目录:cur_path_dir = \ os.path.dirname(os.path.realpath(__file__)) miniwob_dir = os.path.join(cur_path_dir, "miniwob", "html", "miniwob")
-
现在,我们可以开始创建
MiniWoBEnvironment
的子类。然后,我们可以调用父类的初始化函数来初始化环境并在配置miniwob
环境之前设置base_url
的值:class MiniWoBEnv(MiniWoBEnvironment, gym.Env): def __init__( self, env_name: str, obs_im_shape, num_instances: int = 1, miniwob_dir: str = miniwob_dir, seeds: list = [1], ): super().__init__(env_name) self.base_url = f"file://{miniwob_dir}" self.configure(num_instances=num_instances, seeds=seeds, base_url=self.base_url) # self.set_record_screenshots(True) self.obs_im_shape = obs_im_shape
-
现在是定制
reset(…)
方法的时候了。为了使环境可以随机化,我们将使用seeds
参数来接收随机种子。这可以用来生成随机的起始状态和任务,确保我们训练的代理不会对固定/静态网页产生过拟合:def reset(self, seeds=[1], mode=None, record_screenshots=False): """Forces stop and start all instances. Args: seeds (list[object]): Random seeds to set for each instance; If specified, len(seeds) must be equal to the number of instances. A None entry in the list = do not set a new seed. mode (str): If specified, set the data mode to this value before starting new episodes. record_screenshots (bool): Whether to record screenshots of the states. Returns: states (list[MiniWoBState]) """ miniwob_state = super().reset(seeds, mode, record_screenshots=True) return [ state.screenshot.resize(self.obs_im_shape, Image.ANTIALIAS) for state in miniwob_state ]
-
接下来,我们将重新定义
step(…)
方法。让我们分两步完成实现。首先,我们将定义该方法并添加文档字符串,解释方法的参数:def step(self, actions): """Applies an action on each instance and returns the results. Args: actions (list[MiniWoBAction or None]) Returns: tuple (states, rewards, dones, info) states (list[PIL.Image.Image]) rewards (list[float]) dones (list[bool]) info (dict): additional debug information. Global debug information is directly in the root level Local information for instance i is in info['n'][i] """
-
在这一步,我们将完成
step(…)
方法的实现:states, rewards, dones, info = \ super().step(actions) # Obtain screenshot & Resize image obs to match # config img_states = [ state.screenshot.resize(self.obs_im_shape) \ if not dones[i] else None for i, state in enumerate(states) ] return img_states, rewards, dones, info
-
这就完成了我们的
MiniWoBEnv
类实现!为了测试我们的类实现,并理解如何使用这个类,我们将编写一个简短的main()
函数:if __name__ == "__main__": env = MiniWoBVisualEnv("click-pie") for _ in range(10): obs = env.reset() done = False while not done: action = [MiniWoBCoordClick(90, 150)] obs, reward, done, info = env.step(action) [ob.show() for ob in obs if ob is not None] env.close()
-
你可以将前面的脚本保存为
miniwob_env.py
并执行它,查看一个随机智能体在样本环境中的表现。在接下来的几个步骤中,我们将扩展MiniWoBEnv
,以创建一个与 OpenAI Gym 兼容的学习环境接口。让我们首先创建一个名为envs.py
的新文件,并包括以下导入:import gym.spaces import numpy as np import string from miniwob_env import MiniWoBEnv from miniwob.action import MiniWoBCoordClick, MiniWoBType
-
对于第一个环境,我们将实现
MiniWoBVisualClickEnv
类:class MiniWoBVisualClickEnv(MiniWoBEnv): def __init__(self, name, num_instances=1): """RL environment with visual observations and touch/mouse-click action space Two dimensional, continuous-valued action space allows Agents to specify (x, y) coordinates on the visual rendering to click/ touch to interact with the world-of bits Args: name (str): Name of the supported \ MiniWoB-PlusPlus environment num_instances (int, optional): Number of \ parallel env instances. Defaults to 1. """ self.miniwob_env_name = name self.task_width = 160 self.task_height = 210 self.obs_im_width = 64 self.obs_im_height = 64 self.num_channels = 3 # RGB self.obs_im_size = (self.obs_im_width, \ self.obs_im_height) super().__init__(self.miniwob_env_name, self.obs_im_size, num_instances)
-
让我们在
__init__
方法中定义该环境的观察空间和动作空间:self.observation_space = gym.spaces.Box( 0, 255, (self.obs_im_width, self.obs_im_height, self.num_channels), dtype=int, ) self.action_space = gym.spaces.Box( low=np.array([0, 0]), high=np.array([self.task_width, self.task_height]), shape=(2,), dtype=int, )
-
接下来,我们将进一步扩展
reset(…)
方法,以提供与 OpenAI Gym 兼容的接口方法:def reset(self, seeds=[1]): """Forces stop and start all instances. Args: seeds (list[object]): Random seeds to set for each instance; If specified, len(seeds) must be equal to the number of instances. A None entry in the list = do not set a new seed. Returns: states (list[PIL.Image]) """ obs = super().reset(seeds) # Click somewhere to Start! # miniwob_state, _, _, _ = super().step( # self.num_instances * [MiniWoBCoordClick(10,10)] # ) return obs
-
下一个重要的部分是
step
方法。我们将分以下两步来实现它:def step(self, actions): """Applies an action on each instance and returns the results. Args: actions (list[(x, y) or None]); - x is the number of pixels from the left of browser window - y is the number of pixels from the top of browser window Returns: tuple (states, rewards, dones, info) states (list[PIL.Image.Image]) rewards (list[float]) dones (list[bool]) info (dict): additional debug information. Global debug information is directly in the root level Local information for instance i is in info['n'][i] """
-
为了完成
step
方法的实现,让我们先检查动作的维度是否符合预期,然后在必要时绑定动作。最后,我们必须在环境中执行一步:assert ( len(actions) == self.num_instances ), f"Expected len(actions)={self.num_instances}.\ Got {len(actions)}." def clamp(action, low=self.action_space.low,\ high=self.action_space.high): low_x, low_y = low high_x, high_y = high return ( max(low_x, min(action[0], high_x)), max(low_y, min(action[1], high_y)), ) miniwob_actions = \ [MiniWoBCoordClick(*clamp(action)) if action\ is not None else None for action in actions] return super().step(miniwob_actions)
-
我们可以使用描述性的类名将环境注册到 Gym 的注册表中:
class MiniWoBClickButtonVisualEnv(MiniWoBVisualClickEnv): def __init__(self, num_instances=1): super().__init__("click-button", num_instances)
-
最后,为了将环境本地注册到 OpenAI Gym 的注册表中,我们必须将环境注册信息添加到
__init__.py
文件中:import sys import os from gym.envs.registration import register sys.path.append(os.path.dirname(os.path.abspath(__file__))) _AVAILABLE_ENVS = { "MiniWoBClickButtonVisualEnv-v0": { "entry_point": \ "webgym.envs:MiniWoBClickButtonVisualEnv", "discription": "Click the button on a web page", } } for env_id, val in _AVAILABLE_ENVS.items(): register(id=env_id, entry_point=val.get("entry_point"))
这样,我们就完成了这个教程!
工作原理…
我们已经在MiniWoBEnv
中扩展了MiniWoB-plusplus
的实现,以便可以使用基于文件的网页来表示任务。我们进一步扩展了MiniWoBEnv
类,在MiniWoBVisualClickEnv
中提供了与 OpenAI Gym 兼容的接口:
为了清楚地了解 RL 智能体如何在这个环境中学习完成任务,请参考以下截图。在这里,智能体通过尝试不同的动作来理解任务的目标,在这个环境中,动作表现为点击网页上的不同区域(右侧由蓝色点表示)。最终,RL 智能体点击了正确的按钮,开始理解任务描述的含义,以及按钮的作用,因为它点击正确位置后得到了奖励:
图 6.3 – 可视化智能体在学习完成 CTA 任务时的动作
现在,到了继续下一个教程的时候!
构建一个 RL 智能体以在网页上完成任务——行动号召
本配方将教您如何实现一个 RL 训练脚本,使您能够训练一个 RL 代理来处理 OK
/Cancel
对话框,您需要点击以确认/取消弹出通知,以及 Click to learn more
按钮。在本配方中,您将实例化一个 RL 训练环境,该环境为包含 CTA 任务的网页提供视觉渲染。您将训练一个基于 近端策略优化(PPO)的深度 RL 代理,该代理使用 TensorFlow 2.x 实现,学习如何完成当前任务。
以下图片展示了来自随机化 CTA 环境(使用不同种子)的一组观察数据,以便您理解代理将要解决的任务:
图 6.4 – 来自随机化 CTA 环境的代理观察截图
让我们开始吧!
准备中
要完成此配方,您需要激活 tf2rl-cookbook
Python/conda 虚拟环境。确保更新该环境,使其与本食谱代码库中的最新 conda 环境规范文件(tfrl-cookbook.yml
)匹配。如果以下 import
语句能够顺利执行,那么您就准备好开始了:
import argparse
import os
from datetime import datetime
import gym
import numpy as np
import tensorflow as tf
from tensorflow.keras.layers import (Conv2D,Dense,Dropout,Flatten,Input,Lambda,MaxPool2D,)
import webgym # Used to register webgym environments
让我们开始吧!
如何做到…
在这个配方中,我们将实现一个完整的训练脚本,包括用于训练超参数配置的命令行参数解析。正如您从 import
语句中看到的,我们将使用 Keras 的 TensorFlow 2.x 函数式 API 来实现我们将在代理算法实现中使用的深度神经网络(DNNs)。
以下步骤将指导您完成实现:
-
让我们首先定义 CTA 代理训练脚本的命令行参数:
parser = argparse.ArgumentParser(prog="TFRL-Cookbook-Ch5-Click-To-Action-Agent") parser.add_argument("--env", default="MiniWoBClickButtonVisualEnv-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")
-
接下来,我们将创建一个 TensorBoard 日志记录器,以便记录和可视化 CTA 代理的实时训练进度:
args = parser.parse_args() 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)
-
在接下来的步骤中,我们将实现
Actor
类。然而,我们将首先实现__init__
方法:class Actor: def __init__(self, state_dim, action_dim, action_bound, std_bound): self.state_dim = state_dim self.action_dim = action_dim self.action_bound = np.array(action_bound) self.std_bound = std_bound self.weight_initializer = \ tf.keras.initializers.he_normal() self.eps = 1e-5 self.model = self.nn_model() self.model.summary() # Print a summary of the @ Actor model self.opt = \ tf.keras.optimizers.Nadam(args.actor_lr)
-
接下来,我们将定义表示代理模型的 DNN。我们将把 DNN 的实现拆分为多个步骤,因为它会稍长一些,涉及多个神经网络层的堆叠。作为第一个主要处理步骤,我们将通过堆叠卷积-池化-卷积-池化层来实现一个模块:
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) 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)
-
现在,我们将扁平化池化层的输出,以便开始使用带有丢弃层的全连接层或稠密层,生成我们期望的代理网络输出:
flat = Flatten()(pool2) 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) # action_dim[0] = 2 output_val = Dense( self.action_dim[0], activation="relu", kernel_initializer=self.weight_initializer, )(dropout2)
-
我们需要对预测值进行缩放和裁剪,以确保值被限定在我们期望的范围内。让我们使用 Lambda 层 来实现自定义的裁剪和缩放,如以下代码片段所示:
mu_output = Lambda( lambda x: tf.clip_by_value(x * \ self.action_bound, 1e-9, self.action_bound) )(output_val) std_output_1 = Dense( self.action_dim[0], activation="softplus", kernel_initializer=self.weight_initializer, )(dropout2) std_output = Lambda( lambda x: tf.clip_by_value( x * self.action_bound, 1e-9, \ self.action_bound / 2 ) )(std_output_1) return tf.keras.models.Model( inputs=obs_input, outputs=[mu_output, std_output], name="Actor" )
-
这完成了我们的
nn_model
实现。现在,让我们定义一个便捷函数,以便给定状态时获取一个动作: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) mu, std = self.model.predict(state_np) action = np.random.normal(mu, std + self.eps, \ size=self.action_dim).astype( "int" ) # Clip action to be between 0 and max obs screen # size action = np.clip(action, 0, self.action_bound) # 1 Action per instance of env; Env expects: # (num_instances, actions) action = (action,) log_policy = self.log_pdf(mu, std, action) return log_policy, action
-
现在,到了实现主要训练方法的时候。这个方法将更新 Actor 网络的参数:
def train(self, log_old_policy, states, actions, gaes): with tf.GradientTape() as tape: mu, std = self.model(states, training=True) log_new_policy = self.log_pdf(mu, std, actions) loss = self.compute_loss(log_old_policy, log_new_policy, actions, gaes) grads = tape.gradient(loss, self.model.trainable_variables) self.opt.apply_gradients(zip(grads, self.model.trainable_variables)) return loss
-
尽管我们在之前的
train
方法中使用了compute_loss
和log_pdf
,但我们还没有真正定义它们!让我们一个接一个地实现它们,从compute_loss
方法开始:def compute_loss(self, log_old_policy, log_new_policy, actions, gaes): # 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) ) gaes = tf.stop_gradient(gaes) clipped_ratio = tf.clip_by_value( ratio, 1.0 - args.clip_ratio, 1.0 + \ args.clip_ratio ) surrogate = -tf.minimum(ratio * gaes, \ clipped_ratio * gaes) return tf.reduce_mean(surrogate)
-
在这一步,我们将实现
log_pdf
方法:def log_pdf(self, mu, std, action): std = tf.clip_by_value(std, self.std_bound[0], self.std_bound[1]) var = std ** 2 log_policy_pdf = -0.5 * (action - mu) ** 2 / var\ - 0.5 * tf.math.log( var * 2 * np.pi ) return tf.reduce_sum(log_policy_pdf, 1, keepdims=True)
-
上一步已经完成了 Actor 的实现。现在是时候开始实现
Critic
类了:class Critic: def __init__(self, state_dim): self.state_dim = state_dim 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)
-
接下来是
Critic
类的神经网络模型。与 Actor 的神经网络模型类似,这也是一个 DNN。我们将把实现分为几个步骤。首先,来实现一个卷积-池化-卷积-池化模块: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) 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)
-
虽然我们可以堆叠更多的模块或层来加深神经网络,但对于我们当前的任务,DNN 中已经有足够的参数来学习如何在 CTA 任务中表现良好。让我们添加全连接层,这样我们就能最终产生状态条件下的动作值:
flat = Flatten()(pool2) 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) value = Dense( 1, activation="linear", \ kernel_initializer=self.weight_initializer )(dropout2)
-
让我们实现一个方法来计算 Critic 的学习损失,本质上是时间差学习目标与 Critic 预测的值之间的均方误差:
def compute_loss(self, v_pred, td_targets): mse = tf.keras.losses.MeanSquaredError() return mse(td_targets, v_pred)
-
让我们通过实现
train
方法来更新 Critic 的参数,从而最终完成Critic
类的实现: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
-
现在,我们可以利用 Actor 和 Critic 的实现来构建我们的 PPO 代理,使其能够处理高维(图像)观察。让我们从定义
PPOAgent
类的__init__
方法开始:class PPOAgent: def __init__(self, env): self.env = env self.state_dim = self.env.observation_space.shape self.action_dim = self.env.action_space.shape # Set action_bounds to be within the actual # task-window/browser-view of the Agent self.action_bound = [self.env.task_width, self.env.task_height] self.std_bound = [1e-2, 1.0] self.actor = Actor( self.state_dim, self.action_dim, self.action_bound, self.std_bound ) self.Critic = Critic(self.state_dim)
-
我们将使用广义优势估计(GAE)来更新我们的策略。所以,让我们实现一个方法来计算 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
-
我们来到了这个脚本的核心!让我们为深度 PPO 代理定义训练例程。我们将把实现分为多个步骤,以便于跟随。我们将从最外层的循环开始,这个循环必须为可配置的最大回合数运行:
def train(self, max_episodes=1000): 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
-
接下来,我们将实现遍历环境并通过检查环境中的
done
值来处理回合结束的逻辑:while not done: log_old_policy, 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}" ) done = np.all(dones) if done: next_state = prev_state else: prev_state = next_state state = np.array([np.array(s) for s\ in state]) next_state = np.array([np.array(s) \ for s in next_state]) reward = np.reshape(reward, [1, 1]) log_old_policy = np.reshape( log_old_policy, [1, 1]) state_batch.append(state) action_batch.append(action) reward_batch.append((reward + 8) / 8) old_policy_batch.append( log_old_policy)
-
接下来,我们将实现检查回合是否结束或是否需要更新的逻辑,并执行更新步骤:
if len(state_batch) >= \ args.update_freq or done: states = \ np.array([state.squeeze() \ for state in state_batch]) # Convert ([x, y],) to [x, y] actions = np.array([action[0] \ for action in action_batch]) rewards = np.array( [reward.squeeze() for reward\ in 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(next_state) gaes, td_targets = \ self.gae_target( rewards, v_values, \ next_v_value, done ) actor_losses, Critic_losses=[],[]
-
现在,我们已经有了更新的 GAE 目标,可以训练 Actor 和 Critic 网络,并记录损失和其他训练指标以便追踪:
for epoch in range(args.epochs): actor_loss = \ self.actor.train( old_policies, states, actions, gaes ) actor_losses.append( actor_loss) Critic_loss = \ self.Critic.train(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 ) state_batch = [] action_batch = [] reward_batch = [] old_policy_batch = [] episode_reward += reward[0][0] state = next_state[0]
-
最后,让我们实现
__main__
函数来训练 CTA 代理:if __name__ == "__main__": env_name = "MiniWoBClickButtonVisualEnv-v0" env = gym.make(env_name) cta_Agent = PPOAgent(env) cta_Agent.train()
这就完成了这个食谱!让我们简要回顾一下它是如何工作的。
它是如何工作的……
在这个食谱中,我们实现了一个基于 PPO 的深度强化学习代理,并提供了一个训练机制来开发 CTA 代理。请注意,为了简化起见,我们使用了一个环境实例,尽管代码可以扩展为更多环境实例,以加速训练。
为了理解智能体训练的进展,考虑以下一系列图像。在训练的初期阶段,当智能体试图理解任务及其目标时,智能体可能只是在执行随机动作(探索),甚至可能点击屏幕外,如以下截图所示:
图 6.5 – 智能体在初始探索阶段点击屏幕外(没有可见的蓝点)
随着智能体通过偶然发现正确的点击按钮开始取得进展,以下截图显示了智能体取得的一些进展:
图 6.6 – 深度 PPO 智能体在 CTA 任务中的进展
最后,当回合完成或结束(由于时间限制),智能体将接收到类似以下截图(左侧)所示的观察:
图 6.7 – 回合结束时的观察(左)和性能总结(右)
现在,是时候进入下一个教程了!
构建一个可视化自动登录机器人
想象一下,你有一个智能体或机器人,它观察你正在做的事情,并在你点击登录页面时自动帮你登录网站。虽然浏览器插件可以自动登录,但它们使用的是硬编码的脚本,只能在预先编程的网站登录网址上工作。那么,如果你有一个仅依赖于渲染页面的智能体——就像你自己完成任务一样——即使网址发生变化,或者你进入一个没有任何先前保存数据的新网站时,它仍然能够工作,这会有多酷呢?!这个教程将帮助你开发一个脚本,训练一个智能体在网页上登录!你将学习如何随机化、定制和增加智能体的通用性,使它能够在任何登录页面上工作。
随机化和定制任务的用户名和密码的示例如下图所示:
图 6.8 – 随机化用户登录任务的示例观察
让我们开始吧!
做好准备
为了完成这个教程,请确保你拥有最新版本。首先,你需要激活tf2rl-cookbook
Python/conda 虚拟环境。确保更新该环境,使其与本教程代码库中最新的 conda 环境规范文件(tfrl-cookbook.yml
)匹配。如果以下import
语句能顺利运行,那么你已经准备好开始了:
import argparse
import os
from datetime import datetime
import gym
import numpy as np
import tensorflow as tf
from tensorflow.keras.layers import (Conv2D,Dense,Dropout,Flatten,Input,Lambda,MaxPool2D,)
import webgym # Used to register webgym environments
让我们开始吧!
如何实现……
在这个教程中,我们将使用 PPO 算法实现基于深度强化学习的登录智能体。
让我们开始吧:
-
首先,让我们设置训练脚本的命令行参数和日志记录:
parser = argparse.ArgumentParser(prog="TFRL-Cookbook-Ch5-Login-Agent") parser.add_argument("--env", default="MiniWoBLoginUserVisualEnv-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() 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)
-
现在,我们可以直接跳入
Critic
类的定义:class Critic: def __init__(self, state_dim): self.state_dim = state_dim 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)
-
现在,让我们定义 Critic 模型的 DNN。我们将从实现一个由卷积-池化-卷积-池化组成的感知块开始。在随后的步骤中,我们将通过堆叠另一个感知块来增加网络的深度:
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) 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) value = Dense( 1, activation="linear", kernel_initializer=self.weight_initializer )(dropout2) return tf.keras.models.Model(inputs=obs_input, outputs=value, name="Critic")
-
为了完成我们的 Critic 实现,让我们定义
compute_loss
方法和update
方法,以便训练参数:def compute_loss(self, v_pred, td_targets): mse = tf.keras.losses.MeanSquaredError() return mse(td_targets, v_pred) 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
-
现在,我们可以开始实现
Actor
类。我们将在这一步初始化Actor
类,并在随后的步骤中继续实现:class Actor: def __init__(self, state_dim, action_dim, action_bound, std_bound): self.state_dim = state_dim self.action_dim = action_dim self.action_bound = np.array(action_bound) self.std_bound = std_bound self.weight_initializer = \ tf.keras.initializers.he_normal() self.eps = 1e-5 self.model = self.nn_model() self.model.summary() # Print a summary of the # Actor model self.opt = tf.keras.optimizers.Nadam( args.actor_lr)
-
我们将为我们的 Actor 使用与 Critic 实现中相似的 DNN 架构。因此,
nn_model
方法的实现将保持不变,除了最后几层,Actor 和 Critic 的实现将在此处有所不同。Actor 网络模型输出均值和标准差,这取决于动作空间的维度。另一方面,Critic 网络输出一个状态条件下的动作值,无论动作空间的维度如何。与 Critic 的 DNN 实现不同的层如下所示:# action_dim[0] = 2 output_val = Dense( self.action_dim[0], activation="relu", kernel_initializer=self.weight_initializer, )(dropout2) # Scale & clip x[i] to be in range [0, # action_bound[i]] mu_output = Lambda( lambda x: tf.clip_by_value(x * \ self.action_bound, 1e-9, self.action_bound) )(output_val) std_output_1 = Dense( self.action_dim[0], activation="softplus", kernel_initializer=self.weight_initializer, )(dropout2) std_output = Lambda( lambda x: tf.clip_by_value( x * self.action_bound, 1e-9, self.action_bound / 2 ) )(std_output_1) return tf.keras.models.Model( inputs=obs_input, outputs=[mu_output, std_output], name="Actor" )
-
让我们实现一些方法来计算 Actor 的损失和
log_pdf
:def log_pdf(self, mu, std, action): std = tf.clip_by_value(std, self.std_bound[0], self.std_bound[1]) var = std ** 2 log_policy_pdf = -0.5 * (action - mu) ** 2 / var\ - 0.5 * tf.math.log( var * 2 * np.pi ) return tf.reduce_sum(log_policy_pdf, 1, keepdims=True) def compute_loss(self, log_old_policy, log_new_policy, actions, gaes): # 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) ) gaes = tf.stop_gradient(gaes) clipped_ratio = tf.clip_by_value( ratio, 1.0 - args.clip_ratio, 1.0 + \ args.clip_ratio ) surrogate = -tf.minimum(ratio * gaes, clipped_ratio * gaes) return tf.reduce_mean(surrogate)
-
借助这些辅助方法,我们的训练方法实现变得更加简洁:
def train(self, log_old_policy, states, actions, gaes): with tf.GradientTape() as tape: mu, std = self.model(states, training=True) log_new_policy = self.log_pdf(mu, std, actions) loss = self.compute_loss(log_old_policy, log_new_policy, actions, gaes) grads = tape.gradient(loss, self.model.trainable_variables) self.opt.apply_gradients(zip(grads, self.model.trainable_variables)) return loss
-
最后,让我们实现一个方法,当给定一个状态作为输入时,它将从 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) mu, std = self.model.predict(state_np) action = np.random.normal(mu, std + self.eps, size=self.action_dim).astype( "int" ) # Clip action to be between 0 and max obs # screen size action = np.clip(action, 0, self.action_bound) # 1 Action per instance of env; Env expects: # (num_instances, actions) action = (action,) log_policy = self.log_pdf(mu, std, action) return log_policy, action
-
这完成了我们的 Actor 实现。现在,我们可以通过
PPOAgent
类的实现将 Actor 和 Critic 结合起来。由于之前的食谱中已经讨论了 GAE 目标计算,我们将跳过这一部分,专注于训练方法的实现:while not done: # self.env.render() log_old_policy, action = \ self.actor.get_action(state) next_state, reward, dones, _ = \ self.env.step(action) step_num += 1 # Convert action[2] from int idx to # char for verbose printing action_print = [] for a in action: # Map apply action_verbose = (a[:2], \ self.get_typed_char(a[2])) action_print.append( action_verbose) print( f"ep#:{ep} step#:{step_num} step_rew:{reward} \ action:{action_print} \ dones:{dones}" ) done = np.all(dones) if done: next_state = prev_state else: prev_state = next_state state = np.array([np.array(s) for \ s in state]) next_state = np.array([np.array(s) \ for s in next_state]) reward = np.reshape(reward, [1, 1]) log_old_policy = np.reshape( log_old_policy, [1, 1]) state_batch.append(state) action_batch.append(action) reward_batch.append((reward + 8) / 8) old_policy_batch.append(\ log_old_policy)
-
Agent 的更新是在预设的频率下执行的,频率依据收集的样本数量或每个回合结束时执行——以先到者为准:
if len(state_batch) >= \ args.update_freq or done: states = np.array([state.\ squeeze() for state\ in state_batch]) actions = np.array([action[0]\ for action in action_batch]) rewards = np.array( [reward.squeeze() for reward\ in 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(next_state) gaes, td_targets = \ self.gae_target( rewards, v_values, \ next_v_value, done) actor_losses, Critic_losses=[],[] for epoch in range(args.epochs): actor_loss = \ self.actor.train( old_policies, states, actions, gaes) actor_losses.append( actor_loss) Critic_loss = self.Critic.\ train(states, td_targets) Critic_losses.append( Critic_loss)
-
最后,我们可以运行
MiniWoBLoginUserVisualEnv-v0
并使用以下代码片段训练 Agent:if __name__ == "__main__": env_name = "MiniWoBLoginUserVisualEnv-v0" env = gym.make(env_name) cta_Agent = PPOAgent(env) cta_Agent.train()
这完成了我们的自动登录 Agent 脚本。现在是时候运行脚本,查看 Agent 的训练过程了!
它是如何工作的……
登录任务包括点击正确的表单字段并输入正确的用户名和/或密码。为了让 Agent 能够执行此操作,它需要掌握如何使用鼠标和键盘,以及处理网页视觉信息以理解任务和网页登录表单。通过足够的样本,深度 RL Agent 将学习一个策略来完成此任务。让我们来看一下 Agent 在不同阶段的进度状态快照。
以下图片显示了代理成功输入用户名并正确点击密码字段输入密码,但仍未完成任务:
图 6.9 – 训练有素的代理成功输入用户名,但未输入密码
在下图中,你可以看到代理已经学会输入用户名和密码,但它们并不完全正确,因此任务尚未完成:
图 6.10 – 代理输入用户名和密码,但输入错误
同样的代理,通过不同的检查点,在经历了几千次学习后,已经接近完成任务,如下图所示
图 6.11 – 一位训练有素的代理模型即将成功完成登录任务
现在你已经理解了代理是如何工作的及其行为,你可以根据自己的需求对其进行定制,并使用案例来训练代理自动登录任何你想要的自定义网站!
训练一个 RL 代理来自动化你的旅行机票预订
在本食谱中,你将学习如何基于 MiniWoBBookFlightVisualEnv
飞行预订环境实现一个深度 RL 代理:
图 6.12 – 来自随机化 MiniWoBBookFlightVisualEnv 环境的样本起始状态观察
让我们开始吧!
准备工作
为了完成这个食谱,你需要激活 tf2rl-cookbook
Python/conda 虚拟环境。确保更新环境,以便它与本食谱代码仓库中的最新 conda 环境规范文件 (tfrl-cookbook.yml
) 匹配。如果以下的 import
语句能正常运行,那么你就可以开始了:
import argparse
import os
import random
from collections import deque
from datetime import datetime
import gym
import numpy as np
import tensorflow as tf
from tensorflow.keras.layers import (Conv2D,Dense,Dropout, Flatten, Input,Lambda,MaxPool2D)
import webgym # Used to register webgym environments
如何做到……
在本食谱中,我们将实现一个完整的训练脚本,你可以定制并训练它来预订机票!
让我们开始吧:
-
首先,让我们将超参数暴露为可配置的参数,以便在训练脚本中使用:
parser = argparse.ArgumentParser( prog="TFRL-Cookbook-Ch5-SocialMedia-Mute-User-DDPGAgent" ) parser.add_argument("--env", default="Pendulum-v0") parser.add_argument("--actor_lr", type=float, default=0.0005) parser.add_argument("--Critic_lr", type=float, default=0.001) parser.add_argument("--batch_size", type=int, default=64) parser.add_argument("--tau", type=float, default=0.05) parser.add_argument("--gamma", type=float, default=0.99) parser.add_argument("--train_start", type=int, default=2000) parser.add_argument("--logdir", default="logs") args = parser.parse_args()
-
接下来,我们将设置 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)
-
我们将使用重放缓冲区来实现经验回放。让我们实现一个简单的
ReplayBuffer
类:class ReplayBuffer: def __init__(self, capacity=10000): self.buffer = deque(maxlen=capacity) def store(self, state, action, reward, next_state, done): self.buffer.append([state, action, reward, next_state, done]) def sample(self): sample = random.sample(self.buffer, args.batch_size) states, actions, rewards, next_states, done = \ map(np.asarray, zip(*sample)) states = \ np.array(states).reshape(args.batch_size, -1) next_states = np.array(next_states).\ reshape(args.batch_size, -1) return states, actions, rewards, next_states,\ done def size(self): return len(self.buffer)
-
让我们从实现
Actor
类开始:class Actor: def __init__(self, state_dim, action_dim, action_bound): self.state_dim = state_dim self.action_dim = action_dim self.action_bound = action_bound self.weight_initializer = \ tf.keras.initializers.he_normal() self.eps = 1e-5 self.model = self.nn_model() self.opt = tf.keras.optimizers.Adam( args.actor_lr)
-
Actor 的 DNN 模型将由两个感知块组成,每个感知块包含卷积-池化-卷积-池化层,正如我们之前的配方。我们将在这里跳过这一部分,直接查看
train
方法的实现。像往常一样,完整的源代码将会在本食谱的代码库中提供。让我们继续实现train
和predict
方法:def train(self, states, q_grads): with tf.GradientTape() as tape: grads = tape.gradient( self.model(states), self.model.trainable_variables, -q_grads ) self.opt.apply_gradients(zip(grads, \ self.model.trainable_variables)) def predict(self, state): return self.model.predict(state)
-
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) action = self.model.predict(state_np) # Clip action to be between 0 and max obs # screen size action = np.clip(action, 0, self.action_bound) # 1 Action per instance of env; Env expects: # (num_instances, actions) return action
-
这样,我们的
Actor
类就准备好了。现在,我们可以继续并实现Critic
类:class Critic: def __init__(self, state_dim, action_dim): self.state_dim = state_dim self.action_dim = action_dim self.weight_initializer = \ tf.keras.initializers.he_normal() self.model = self.nn_model() self.opt = \ tf.keras.optimizers.Adam(args.Critic_lr)
-
类似于
Actor
类的 DNN 模型,我们将重新使用之前食谱中的类似架构来构建Critic
类,包含两个感知块。你可以参考这个食谱的完整源代码或前一个食谱中的 DNN 实现,以获得完整性。让我们深入实现predict
和g_gradients
的计算,以计算 Q 函数:def predict(self, inputs): return self.model.predict(inputs) def q_gradients(self, states, actions): actions = tf.convert_to_tensor(actions) with tf.GradientTape() as tape: tape.watch(actions) q_values = self.model([states, actions]) q_values = tf.squeeze(q_values) return tape.gradient(q_values, actions)
-
为了更新我们的 Critic 模型,我们需要一个损失函数来驱动参数更新,并且需要一个实际的训练步骤来执行更新。在这一步中,我们将实现这两个核心方法:
def compute_loss(self, v_pred, td_targets): mse = tf.keras.losses.MeanSquaredError() return mse(td_targets, v_pred) def train(self, states, actions, td_targets): with tf.GradientTape() as tape: v_pred = self.model([states, actions], 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
-
现在是时候将 Actor 和 Critic 结合起来实现 DDPGAgent 了!让我们深入了解:
class DDPGAgent: def __init__(self, env): self.env = env self.state_dim = self.env.observation_space.shape self.action_dim = self.env.action_space.shape self.action_bound = self.env.action_space.high self.buffer = ReplayBuffer() self.actor = Actor(self.state_dim, self.action_dim, self.action_bound) self.Critic = Critic(self.state_dim, self.action_dim) self.target_actor = Actor(self.state_dim, self.action_dim, self.action_bound) self.target_Critic = Critic(self.state_dim, self.action_dim) actor_weights = self.actor.model.get_weights() Critic_weights = self.Critic.model.get_weights() self.target_actor.model.set_weights( actor_weights) self.target_Critic.model.set_weights (Critic_weights)
-
我们来实现一个方法,用于更新 Actor 和 Critic 的目标模型:
def update_target(self): actor_weights = self.actor.model.get_weights() t_actor_weights = \ self.target_actor.model.get_weights() Critic_weights = self.Critic.model.get_weights() t_Critic_weights = \ self.target_Critic.model.get_weights() for i in range(len(actor_weights)): t_actor_weights[i] = (args.tau * \ actor_weights[i] + (1 - args.tau) * \ t_actor_weights[i]) for i in range(len(Critic_weights)): t_Critic_weights[i] = (args.tau * \ Critic_weights[i] + (1 - args.tau) * \ t_Critic_weights[i]) self.target_actor.model.set_weights( t_actor_weights) self.target_Critic.model.set_weights( t_Critic_weights)
-
接下来,我们将实现一个方法,用于计算时序差分目标:
def get_td_target(self, rewards, q_values, dones): targets = np.asarray(q_values) for i in range(q_values.shape[0]): if dones[i]: targets[i] = rewards[i] else: targets[i] = args.gamma * q_values[i] return targets
-
因为我们使用的是确定性策略梯度且没有分布来从中采样,我们将使用一个噪声函数来在 Actor 网络预测的动作周围进行采样。Ornstein Uhlenbeck(OU)噪声过程是 DDPG 代理的一个流行选择。我们将在这里实现它:
def add_ou_noise(self, x, rho=0.15, mu=0, dt=1e-1, sigma=0.2, dim=1): return ( x + rho * (mu - x) * dt + sigma * \ np.sqrt(dt) * np.random.normal(size=dim))
-
接下来,我们将实现一个方法,用于重放 Replay Buffer 中的经验:
def replay_experience(self): for _ in range(10): states, actions, rewards, next_states, \ dones = self.buffer.sample() target_q_values = self.target_Critic.predict( [next_states, self.target_actor.predict(next_states)]) td_targets = self.get_td_target(rewards, target_q_values, dones) self.Critic.train(states, actions, td_targets) s_actions = self.actor.predict(states) s_grads = self.Critic.q_gradients(states, s_actions) grads = np.array(s_grads).reshape( (-1, self.action_dim)) self.actor.train(states, grads) self.update_target()
-
我们在代理(Agent)实现中的最后一个但最关键的任务是实现
train
方法。我们将把实现分为几个步骤。首先,我们将从最外层的循环开始,该循环必须运行至最多的回合数:def train(self, max_episodes=1000): with writer.as_default(): for ep in range(max_episodes): step_num, episode_reward, done = 0, 0,\ False state = self.env.reset() prev_state = state bg_noise = np.random.randint( self.env.action_space.low, self.env.action_space.high, self.env.action_space.shape, )
-
接下来,我们将实现内层循环,它将一直运行到回合结束:
while not done: # self.env.render() action = self.actor.get_action(state) noise = self.add_ou_noise(bg_noise,\ dim=self.action_dim) action = np.clip(action + noise, 0, \ self.action_bound).astype("int") next_state, reward, dones, _ = \ self.env.step(action) done = np.all(dones) if done: next_state = prev_state else: prev_state = next_state
-
我们还没有完成!我们仍然需要用代理收集到的新经验来更新我们的 Replay Buffer:
for (s, a, r, s_n, d) in zip(next_state,\ action, reward, next_state, dones): self.buffer.store(s, a, \ (r + 8) / 8, s_n, d) episode_reward += r step_num += 1 # 1 across # num_instances print(f"ep#:{ep} step#:{step_num} \ step_rew:{reward} \ action:{action} dones:{dones}") bg_noise = noise state = next_state
-
我们完成了吗?!差不多!我们只需要记得在 Replay Buffer 的大小大于我们用于训练的批量大小时重放经验:
if (self.buffer.size() >= args.batch_size and self.buffer.size() >= \ args.train_start): self.replay_experience() print(f"Episode#{ep} \ Reward:{episode_reward}") tf.summary.scalar("episode_reward", \ episode_reward, \ step=ep)
-
这就完成了我们的实现。现在,我们可以使用以下
__main__
函数启动在 Visual Flight Booking 环境中的 Agent 训练:if __name__ == "__main__": env_name = "MiniWoBBookFlightVisualEnv-v0" env = gym.make(env_name) Agent = DDPGAgent(env) Agent.train()
就这样!
它是如何工作的……
DDPG 代理从航班预订环境中收集一系列样本,在探索过程中利用这些经验,通过演员和评论员的更新来更新其策略参数。我们之前讨论的 OU 噪声允许代理在使用确定性行动策略的同时进行探索。航班预订环境相当复杂,因为它要求代理不仅掌握键盘和鼠标的操作,还需要通过查看任务描述的视觉图像(视觉文本解析)来理解任务,推断预期的任务目标,并按正确的顺序执行操作。以下截图展示了代理在完成足够多的训练回合后表现出的效果:
图 6.13 – 代理在不同学习阶段执行航班预订任务的截图
以下截图展示了代理在任务的最终阶段进展后的屏幕(尽管离完成任务还有很长距离):
图 6.14 – 代理完成航班预订任务的最终阶段的截图
完成这些步骤后,我们将进入下一个食谱!
训练一个 RL 代理来管理你的电子邮件
电子邮件已经成为许多人生活中不可或缺的一部分。一个普通的工作专业人士每天处理的电子邮件数量正逐日增加。虽然存在许多用于垃圾邮件控制的电子邮件过滤器,但如果有一个智能代理,可以执行一系列电子邮件管理任务,只需提供任务描述(通过文本或语音转文本的方式),且不受任何具有速率限制的 API 限制,岂不是很方便?在本食谱中,你将开发一个深度强化学习代理,并训练它执行电子邮件管理任务!以下图片展示了一组示例任务:
图 6.15 – 来自随机化 MiniWoBEmailInboxImportantVisualEnv 环境的一组观察样本
让我们深入细节!
准备开始
为了完成这个食谱,请确保你拥有最新版本。首先,你需要激活 tf2rl-cookbook
Python/conda 虚拟环境。确保更新该环境,以便它与本食谱代码库中的最新 conda 环境规范文件(tfrl-cookbook.yml
)匹配。如果以下 import
语句可以顺利运行,那么你就可以开始了:
import tensorflow as tf
from tensorflow.keras.layers import (
Conv2D,
Dense,
Dropout,
Flatten,
Input,
Lambda,
MaxPool2D,
)
import webgym # Used to register webgym environments
让我们开始吧!
如何操作…
按照以下步骤实现深度 RL 代理,并训练它来管理重要电子邮件:
-
首先,我们将定义一个
ArgumentParser
,以便从命令行配置脚本。有关可配置超参数的完整列表,请参考本食谱的源代码:parser = argparse.ArgumentParser( prog="TFRL-Cookbook-Ch5-Important-Emails-Manager-Agent" ) parser.add_argument("--env", default="MiniWoBEmailInboxImportantVisualEnv-v0")
-
接下来,让我们设置 TensorBoard 日志记录:
args = parser.parse_args() 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)
-
现在,我们可以初始化
Actor
类:class Actor: def __init__(self, state_dim, action_dim, action_bound, std_bound): self.state_dim = state_dim self.action_dim = action_dim self.action_bound = np.array(action_bound) self.std_bound = std_bound self.weight_initializer = \ tf.keras.initializers.he_normal() self.eps = 1e-5 self.model = self.nn_model() self.model.summary() # Print a summary of the # Actor model self.opt = \ tf.keras.optimizers.Nadam(args.actor_lr)
-
由于我们的电子邮件管理环境中的观察结果是视觉的(图像),我们需要为 Agent 的 Actor 提供感知能力。为此,我们必须使用基于卷积的感知块,如下所示:
def nn_model(self): obs_input = Input(self.state_dim) conv1 = Conv2D( filters=32, 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) 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=16, kernel_size=(3, 3), strides=(1, 1), padding="valid", activation="relu", )(pool3) pool4 = MaxPool2D(pool_size=(3, 3), strides=1)\ (conv4)
-
现在,我们准备将 DNN 输出展平,以生成我们希望从 Actor 输出的均值(mu)和标准差。首先,让我们添加展平层和全连接层:
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) # action_dim[0] = 2 output_val = Dense( self.action_dim[0], activation="relu", kernel_initializer=self.weight_initializer, )(dropout2)
-
我们现在准备定义我们 Actor 网络的最终层。这些层将帮助我们生成
mu
和std
,正如我们在前一步中讨论的那样:# Scale & clip x[i] to be in range [0, action_bound[i]] mu_output = Lambda( lambda x: tf.clip_by_value(x * \ self.action_bound, 1e-9, self.action_bound) )(output_val) std_output_1 = Dense( self.action_dim[0], activation="softplus", kernel_initializer=self.weight_initializer, )(dropout2) std_output = Lambda( lambda x: tf.clip_by_value( x * self.action_bound, 1e-9, \ self.action_bound / 2))(std_output_1) return tf.keras.models.Model( inputs=obs_input, outputs=[mu_output, \ std_output], name="Actor" )
-
这完成了我们 Actor 的 DNN 模型实现。为了实现剩余的方法并完成 Actor 类,请参考本食谱的完整代码,该代码可以在本食谱的代码库中找到。接下来,我们将专注于定义
Critic
类的接口:class Critic: def __init__(self, state_dim): self.state_dim = state_dim 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)
-
Critic 的 DNN 模型也基于与我们为
Actor
使用的相同的卷积神经网络架构。为了完整性,请参考本食谱的完整源代码,该代码可在本食谱的代码库中找到。我们将在这里实现损失计算和训练方法:def compute_loss(self, v_pred, td_targets): mse = tf.keras.losses.MeanSquaredError() return mse(td_targets, v_pred) 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
-
有了这些,我们现在可以定义我们的 Agent 类:
class PPOAgent: def __init__(self, env): self.env = env self.state_dim = self.env.observation_space.shape self.action_dim = self.env.action_space.shape # Set action_bounds to be within the actual # task-window/browser-view of the Agent self.action_bound = [self.env.task_width, \ self.env.task_height] self.std_bound = [1e-2, 1.0] self.actor = Actor( self.state_dim, self.action_dim, \ self.action_bound, self.std_bound ) self.Critic = Critic(self.state_dim)
-
上述代码应该对你来说是熟悉的,来自本章中前面的 Agent 实现。你可以基于我们之前的实现完成剩余的方法(和训练循环)。如果遇到困难,可以通过访问本食谱的代码库,查看完整源代码。我们现在将编写
__main__
函数,这样我们就可以在MiniWoBEmailInboxImportantVisualEnv
中训练 Agent。这将使我们能够看到 Agent 学习过程的实际表现:if __name__ == "__main__": env_name = "MiniWoBEmailInboxImportantVisualEnv-v0" env = gym.make(env_name) cta_Agent = PPOAgent(env) cta_Agent.train()
它是如何工作的…
PPO 代理使用卷积神经网络层来处理演员和评论家类中的高维视觉输入。PPO 算法通过使用一个替代损失函数来更新代理的策略参数,从而防止策略参数的剧烈更新。然后,它将策略更新保持在信任区域内,这使得它对超参数选择以及其他可能导致代理训练过程中不稳定的因素具有鲁棒性。电子邮件管理环境为深度 RL 代理提供了一个很好的顺序决策问题。首先,代理需要从收件箱中的一系列电子邮件中选择正确的电子邮件,然后执行所需的操作(例如标记电子邮件)。代理只能访问收件箱的视觉渲染,因此它需要提取任务规范的细节,解读任务规范,然后进行规划并执行操作!
以下是代理在学习不同阶段的表现截图(从不同的检查点加载):
图 6.16 – 展示代理学习进展的一系列截图
现在,让我们继续下一个教程!
训练一个 RL 代理来自动化你的社交媒体账号管理
在本教程结束时,你将构建一个完整的深度 RL 代理训练脚本,可以训练代理来执行社交媒体账号的管理任务!
以下图像展示了一系列(随机化的)来自环境的任务,我们将在其中训练代理:
图 6.17 – 一个示例社交媒体账号管理任务集,代理被要求解决这些任务
请注意,这个任务中有一个滚动条,代理需要学习如何使用它!与此任务相关的推文可能会被隐藏在屏幕的不可见区域,因此代理必须主动进行探索(通过上下滑动滚动条)才能继续!
准备开始
要完成这个教程,你需要激活 tf2rl-cookbook
Python/conda 虚拟环境。确保更新环境,使其与本教程代码库中的最新 conda 环境规范文件(tfrl-cookbook.yml
)匹配。如果以下的 import
语句能够正常运行,那么你就可以开始了:
import tensorflow as tf
from tensorflow.keras.layers import (Conv2D, Dense, Dropout, Flatten, Input, Lambda, MaxPool2D, concatenate,)
import webgym # Used to register webgym environments
如何做到这一点…
让我们从配置代理训练脚本开始。之后,你将看到如何完成实现。
让我们开始吧:
-
让我们直接进入实现!我们将从
ReplayBuffer
实现开始:class ReplayBuffer: def __init__(self, capacity=10000): self.buffer = deque(maxlen=capacity) def store(self, state, action, reward, next_state, done): self.buffer.append([state, action, reward, next_state, done]) def sample(self): sample = random.sample(self.buffer, args.batch_size) states, actions, rewards, next_states, done = \ map(np.asarray, zip(*sample)) states = \ np.array(states).reshape(args.batch_size, -1) next_states = np.array(next_states).\ reshape(args.batch_size, -1) return states, actions, rewards, next_states,\ done def size(self): return len(self.buffer)
-
接下来,我们将实现我们的
Actor
类:class Actor: def __init__(self, state_dim, action_dim, action_bound): self.state_dim = state_dim self.action_dim = action_dim self.action_bound = action_bound self.weight_initializer = \ tf.keras.initializers.he_normal() self.eps = 1e-5 self.model = self.nn_model() self.opt = \ tf.keras.optimizers.Adam(args.actor_lr)
-
下一个核心部分是我们演员的 DNN 定义:
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) 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)
-
根据任务的复杂性,我们可以修改(增加/减少)DNN 的深度。我们将通过将池化层的输出连接到带有丢弃层的全连接层开始:
flat = Flatten()(pool2) 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) # action_dim[0] = 2 output_val = Dense(self.action_dim[0], activation="relu", kernel_initializer= \ self.weight_initializer,)\ (dropout2) # Scale & clip x[i] to be in range # [0, action_bound[i]] mu_output=Lambda(lambda x: tf.clip_by_value(x *\ self.action_bound, 1e-9, \ self.action_bound))(output_val) return tf.keras.models.Model(inputs=obs_input, outputs=mu_output, name="Actor")
-
这完成了我们对 Actor 的 DNN 模型实现。现在,让我们实现一些方法来训练 Actor 并获取 Actor 模型的预测:
def train(self, states, q_grads): with tf.GradientTape() as tape: grads = tape.gradient(self.model(states),\ self.model.trainable_variables,\ -q_grads) self.opt.apply_gradients(zip(grads, \ self.model.trainable_variables)) def predict(self, state): return self.model.predict(state)
-
我们现在可以从我们的 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) action = self.model.predict(state_np) action = np.clip(action, 0, self.action_bound) return action
-
让我们开始实现 Critic。这里,我们需要实现我们所需要的智能体类:
class Critic: def __init__(self, state_dim, action_dim): self.state_dim = state_dim self.action_dim = action_dim self.weight_initializer = \ tf.keras.initializers.he_normal() self.model = self.nn_model() self.opt = \ tf.keras.optimizers.Adam(args.Critic_lr)
-
请注意,Critic 的模型是通过
self.nn_model()
初始化的。让我们在这里实现它: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) 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)
-
我们将通过将输出传递通过带有丢弃层的全连接层来完成 Critic 的 DNN 架构。这样,我们可以得到所需的动作值:
flat = Flatten()(pool2) 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) value = Dense(1, activation="linear", kernel_initializer= \ self.weight_initializer)\ (dropout2) return tf.keras.models.Model(inputs=obs_input, outputs=value, name="Critic")
-
现在,让我们实现
g_gradients
和compute_loss
方法。这应该是相当直接的:def q_gradients(self, states, actions): actions = tf.convert_to_tensor(actions) with tf.GradientTape() as tape: tape.watch(actions) q_values = self.model([states, actions]) q_values = tf.squeeze(q_values) return tape.gradient(q_values, actions) def compute_loss(self, v_pred, td_targets): mse = tf.keras.losses.MeanSquaredError() return mse(td_targets, v_pred)
-
最后,我们可以通过实现
predict
和train
方法来完成 Critic 的实现:def predict(self, inputs): return self.model.predict(inputs) def train(self, states, actions, td_targets): with tf.GradientTape() as tape: v_pred = self.model([states, actions],\ 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
-
我们现在可以利用 Actor 和 Critic 来实现我们的智能体:
class DDPGAgent: def __init__(self, env): self.env = env self.state_dim = self.env.observation_space.shape self.action_dim = self.env.action_space.shape self.action_bound = self.env.action_space.high self.buffer = ReplayBuffer() self.actor = Actor(self.state_dim, self.action_dim, self.action_bound) self.Critic = Critic(self.state_dim, self.action_dim) self.target_actor = Actor(self.state_dim, self.action_dim, self.action_bound) self.target_Critic = Critic(self.state_dim, self.action_dim) actor_weights = self.actor.model.get_weights() Critic_weights = self.Critic.model.get_weights() self.target_actor.model.set_weights( actor_weights) self.target_Critic.model.set_weights( Critic_weights)
-
接下来,我们将根据 DDPG 算法实现
update_target
方法:def update_target(self): actor_weights = self.actor.model.get_weights() t_actor_weights = \ self.target_actor.model.get_weights() Critic_weights = self.Critic.model.get_weights() t_Critic_weights = \ self.target_Critic.model.get_weights() for i in range(len(actor_weights)): t_actor_weights[i] = (args.tau * \ actor_weights[i] + \ (1 - args.tau) * \ t_actor_weights[i]) for i in range(len(Critic_weights)): t_Critic_weights[i] = (args.tau * \ Critic_weights[i] + \ (1 - args.tau) * \ t_Critic_weights[i]) self.target_actor.model.set_weights( t_actor_weights) self.target_Critic.model.set_weights( t_Critic_weights)
-
我们在这里不讨论
train
方法的实现。相反,我们将从外部循环的实现开始,然后在接下来的步骤中完成它:def train(self, max_episodes=1000): with writer.as_default(): for ep in range(max_episodes): step_num, episode_reward, done = 0, 0, \ False state = self.env.reset() prev_state = state bg_noise = np.random.randint( self.env.action_space.low, self.env.action_space.high, self.env.action_space.shape)
-
主要的内部循环实现如下:
while not done: action = self.actor.get_action(state) noise = self.add_ou_noise(bg_noise, dim=self.action_dim) action = np.clip(action + noise, 0, self.action_bound).astype("int") next_state, reward, dones, _ = \ self.env.step(action) done = np.all(dones) if done: next_state = prev_state else: prev_state = next_state for (s, a, r, s_n, d) in zip\ (next_state, action, reward, \ next_state, dones): self.buffer.store(s, a, \ (r + 8) / 8, s_n, d) episode_reward += r step_num += 1 # 1 across num_instances bg_noise = noise state = next_state if (self.buffer.size() >= args.batch_size and self.buffer.size() >= \ args.train_start): self.replay_experience() tf.summary.scalar("episode_reward", episode_reward, step=ep)
-
这完成了我们训练方法的实现。关于
replay_experience
、add_ou_noise
和get_td_targets
方法的实现,请参考本食谱的完整源代码,该代码可以在本食谱的代码库中找到。 -
让我们编写
__main__
函数,这样我们就可以开始在社交媒体环境中训练智能体:if __name__ == "__main__": env_name = "MiniWoBSocialMediaMuteUserVisualEnv-v0" env = gym.make(env_name) Agent = DDPGAgent(env) Agent.train()
它是如何工作的…
让我们直观地探索一下一个训练良好的智能体是如何在社交媒体管理任务中进展的。下图展示了智能体学习使用滚动条在这个环境中“导航”:
图 6.18 – 智能体学习使用滚动条进行导航
请注意,任务规范并未涉及任何与滚动条或导航相关的内容,智能体能够探索并发现它需要导航才能继续进行任务!下图展示了智能体在选择正确的推文后,点击了错误的操作;也就是点击了 Embed Tweet
而不是 Mute
按钮:
图 6.19 – 当目标是点击“静音”时,智能体点击了“嵌入推文”
在 9600 万个训练回合后,智能体已足够能够解决该任务。下图展示了智能体在评估回合中的表现(智能体是从检查点加载的)
图 6.20 – 从训练参数加载的代理即将成功完成任务
这就是本教程的内容以及本章的总结。祝你训练愉快!
第七章:第七章:将深度 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 智能体所需的最小运行时的详细信息。让我们直接进入细节:
-
首先,让我们实现演员组件,它将是一个 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
-
接下来,让我们实现评论家组件,它也将是一个 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
-
现在,让我们实现一个实用函数,用于在给定源 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)
-
现在,我们可以开始实现 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, ):
-
现在,让我们初始化智能体的状态/观测形状、动作形状、动作限制/边界,并初始化一个双端队列(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))
-
在这一步中,让我们定义并初始化演员组件:
# 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())
-
现在,让我们定义并初始化评论家组件:
# 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())
-
在这一步中,让我们根据
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)
-
让我们通过设置超参数并初始化用于 TensorBoard 日志记录的训练进度摘要字典来完成构造函数的实现:
# Set hyperparameters self.gamma = gamma # discount factor self.tau = tau # target model update self.batch_size = batch_size # Tensorboard self.summaries = {}
-
构造函数实现完成后,我们接下来实现
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
-
这一步非常关键。我们将实现
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
-
最后,让我们实现一些实用方法,用于从先前训练的模型中加载演员和评论家的模型权重:
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.py
、stock_trading_continuous_env.py
、trading_utils.py
以及我们在前几章中实现的其他自定义交易环境。你将在书籍的代码仓库中找到包含这些内容的 tradegym
模块。
如何操作…
我们的实现将包含两个核心模块——tradegym
服务器和 tradegym
客户端,这些模块是基于 OpenAI Gym HTTP API 构建的。本教程将重点介绍 HTTP 服务接口的定制和核心组件。我们将首先定义作为 tradegym
库一部分暴露的最小自定义环境集,然后构建服务器和客户端模块:
-
首先,确保
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”))
-
我们现在可以开始实现我们的
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
-
接下来,我们将导入
tradegym
模块,以便将可用的环境注册到 Gym 注册表中:sys.path.append(os.path.dirname(os.path.abspath(__file__))) import tradegym # Register tradegym envs with OpenAI Gym # registry
-
现在,让我们看看环境容器类的框架,并且有注释说明每个方法的作用。你可以参考本书代码仓库中
chapter7
下的完整实现。我们将从类定义开始,并在接下来的步骤中完成框架:class Envs(object): def __init__(self): self.envs = {} self.id_len = 8 # Number of chars in instance_id
-
在此步骤中,我们将查看两个有助于管理环境实例的辅助方法。它们支持查找和删除操作:
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"""
-
接下来,我们将查看其他一些有助于环境管理操作的方法:
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"""
-
本步骤中讨论的方法支持 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.)"""
-
有了前面的框架(和实现),我们可以通过Flask Python 库将这些操作暴露为 REST API。接下来,我们将讨论核心服务器应用程序的设置以及创建、重置和步骤方法的路由设置。让我们看看暴露端点处理程序的服务器应用程序设置:
app = Flask(__name__) envs = Envs()
-
现在我们可以查看
v1/envs
的 REST API 路由定义。它接受一个env_id
,该 ID 应该是一个有效的 Gym 环境 ID(如我们的自定义StockTradingContinuous-v0
或MountainCar-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)
-
接下来,我们将查看
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)
-
接下来,我们将查看
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)
-
对于
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)
-
接下来,我们将了解
tradegym
客户端的实现。完整实现可在本书代码仓库的chapter7
中找到tradegym_http_client.py
文件中。在本步骤中,我们将首先导入必要的 Python 模块,并在接下来的步骤中继续实现客户端封装器:import json import logging import os import requests import six.moves.urllib.parse as urlparse
-
客户端类提供了一个 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”})
-
在这里重复所有标准的 Gym HTTP 客户端方法并不是对本书有限空间的合理利用,因此我们将重点关注核心的封装方法,如
env_create
、env_reset
和env_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
-
在这一步中,我们将查看调用
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
-
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]
-
在其他客户端封装方法到位之后,我们可以实现
__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}”)
-
我们现在可以开始实际创建客户端实例并检查
tradegym
服务!首先,我们需要通过执行以下命令来启动tradegym
服务器:(tfrl-cookbook)praveen@desktop:~/tensorflow2-reinforcement-learning-cookbook/src/ch7-cloud-deploy-deep-rl-agents$ python tradegym_http_server.py
-
现在,我们可以通过在另一个终端运行以下命令来启动
tradegym
客户端:(tfrl-cookbook)praveen@desktop:~/tensorflow2-reinforcement-learning-cookbook/src/ch7-cloud-deploy-deep-rl-agents$ python tradegym_http_client.py
-
你应该会在你启动
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
。
让我们开始吧!
-
让我们先创建一个应用级别的子日志记录器,添加一个流处理器,并设置日志级别:
# 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)
-
接下来,让我们创建一个 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)
-
我们现在可以进入实现的核心部分。让我们从实现
__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)
-
接下来,让我们请求服务器创建我们想要的强化学习训练环境来训练我们的代理:
# Set up training environment env_id = “StockTradingContinuousEnv-v0” instance_id = client.env_create(env_id)
-
现在,让我们初始化我们的代理:
# 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)
-
我们现在准备好使用一些超参数来配置训练:
# 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, )
-
到此,我们已经准备好开始外部训练循环:
cur_state = client.env_reset(instance_id) # Start training while epoch < max_epochs: if steps > max_steps: done = True
-
现在,让我们处理当一个回合结束并且
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”, )
-
现在是关键步骤!让我们使用代理的
act
和train
方法,通过采取行动(执行动作)和使用收集到的经验来训练代理: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)
-
现在,让我们更新变量,为接下来的步骤做准备:
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()
-
这就完成了我们的训练循环。很简单,对吧?训练完成后,别忘了保存代理的模型,这样在部署时我们就可以使用已训练的模型:
agent.save_model( f”sac_actor_final_episode_{env_id}.h5”, \ f”sac_critic_final_episode_{env_id}.h5” )
-
你现在可以继续并使用以下命令运行脚本:
(tfrl-cookbook)praveen@desktop:~/tensorflow2-reinforcement-learning-cookbook/src/ch7-cloud-deploy-deep-rl-agents$ python 3_training_rl_agents_using_remote_sims.py
-
我们是不是忘了什么?客户端连接的是哪个模拟服务器?模拟服务器正在运行吗?!如果你在命令行看到一个类似以下的长错误信息,那么很可能是模拟服务器没有启动:
Failed to establish a new connection: [Errno 111] Connection refused’))
-
这次我们要做对!让我们通过使用以下命令启动
tradegym
服务器,确保我们的模拟服务器正在运行:(tfrl-cookbook)praveen@desktop:~/tensorflow2-reinforcement-learning-cookbook/src/ch7-cloud-deploy-deep-rl-agents$ python tradegym_http_server.py
-
我们现在可以使用以下命令启动代理训练脚本(与之前相同):
(tfrl-cookbook)praveen@desktop:~/tensorflow2-reinforcement-learning-cookbook/src/ch7-cloud-deploy-deep-rl-agents$ python 3_training_rl_agents_using_remote_sims.py
-
你应该看到类似以下内容的输出:
... 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
如何操作…
让我们专注于创建一个简单但完整的代理评估脚本:
-
首先,让我们导入用于训练环境的
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
-
接下来,让我们创建一个命令行参数解析器来处理命令行配置:
parser = ArgumentParser(prog=”TFRL-Cookbook-Ch7-Evaluating-RL-Agents”) parser.add_argument(“--agent”, default=”SAC”, help=”Name of Agent. Default=SAC”)
-
现在,让我们为
--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”, )
-
让我们还为
–-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”, )
-
现在,我们准备好完成参数解析:
args = parser.parse_args()
-
让我们从实现
__main__
方法开始,并在接下来的步骤中继续实现它。首先,我们从创建一个本地实例的 RL 环境开始,在该环境中我们将评估代理:if __name__ == “__main__”: # Create an instance of the evaluation environment env = gym.make(args.env)
-
现在,让我们初始化代理类。暂时我们只支持 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)
-
接下来,让我们加载训练好的代理模型:
# 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}”)
-
我们现在准备好使用测试环境中的训练模型来评估代理:
# 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}”)
-
现在,让我们尝试在
StockTradingContinuous-v0
环境中评估代理。请注意,股票交易环境中的市场数据源(data/MSFT.csv
和data/TSLA.csv
)可能与用于训练的市场数据不同!毕竟,我们想要评估的是代理学会如何交易!运行以下命令启动代理评估脚本:(tfrl-cookbook)praveen@desktop:~/tensorflow2-reinforcement-learning-cookbook/src/ch7-cloud-deploy-deep-rl-agents$ python 4_evaluating_rl_agents.py
-
根据你训练的代理表现如何,你会在控制台看到类似以下的输出(奖励值会有所不同):
... ================================================================================================== 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 以将代理容器化:
-
首先,让我们导入本章早些时候构建的
sac_agent_runtime
:sys.path.append(os.path.dirname(os.path.abspath(__file__))) from sac_agent_runtime import SAC
-
接下来,让我们为命令行参数创建一个处理程序,并将
--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”)
-
接下来,让我们添加参数,以便指定我们代理将要部署的主机服务器的 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”, )
-
接下来,让我们添加对指定包含训练好的代理模型的目录以及使用的特定模型版本的参数支持:
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”, )
-
作为支持的最终参数集,让我们添加允许指定基于训练模型配置的观测形状和动作空间规格的参数:
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,)” )
-
现在我们可以完成参数解析器,并开始实现
__main__
函数:args = parser.parse_args() if __name__ == “__main__”:
-
首先,让我们加载代理的运行时配置:
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, )
-
接下来,让我们创建一个代理实例,并从预训练模型中加载代理的演员和评论家网络的权重:
# 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}”)
-
现在我们可以使用 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()}
-
最后,我们只需要添加一行代码,当执行时启动 Flask 应用程序以启动服务:
# Launch/Run the Agent (http) service app.run(host=args.host_ip, port=args.host_port, debug=True)
-
我们的代理 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”
-
现在让我们安装一些必要的系统级软件包,并清理文件以节省磁盘空间:
RUN apt-get install -y wget git make cmake zlib1g-dev && rm -rf /var/lib/apt/lists/*
-
为了执行我们的代理运行时并安装所有必需的软件包,我们将使用 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
-
现在让我们将本章的源代码复制到容器中,并使用
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”
-
最后,我们只需为容器设置
ENTRYPOINT
和CMD
,当容器启动时,这些将作为参数传递给ENTRYPOINT
:ENTRYPOINT [ “conda”, “run”, “--no-capture-output”, “-n”, “tfrl-cookbook”, “python” ] CMD [ “5_packaging_rl_agents_for_deployment.py” ]
-
这完成了我们的 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
-
如果你是第一次运行前面的命令,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
-
对于涉及从磁盘进行 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...
-
最后,当 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
)。
我们开始吧:
-
首先使用以下命令构建包含交易机器人的 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
-
一旦 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
-
如果一切顺利,你应该会看到类似以下的控制台输出,表示机器人已启动并准备好执行操作:
...================================================================================================== 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 ...
-
现在你已经在本地(在你自己的服务器上)部署了交易机器人,接下来我们来创建一个简单的脚本,利用你构建的 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()}”)
-
你可以使用以下命令执行该脚本:
(tfrl-cookbook)praveen@desktop:~/tensorflow2-reinforcement-learning-cookbook/src/ch7-cloud-deploy-deep-rl-agents$python test_agent_service.py
-
请注意,您的机器人容器仍需要运行。一旦执行前述命令,你将看到类似以下的输出,表示在
/v1/act
端点接收到新的 POST 消息,并返回了 HTTP 响应状态 200,表示成功:172.17.0.1 - - [00/Mmm/YYYY hh:mm:ss] “POST /v1/act HTTP/1.1” 200 -
-
你还会注意到,测试脚本在其控制台窗口中会打印出类似以下的输出,表示它收到了来自交易机器人的一个操作:
Received Agent action:{‘action’: [[0.008385116065491426]]}
-
现在是将你的交易机器人部署到云平台的时候了,这样你或其他人就可以通过互联网访问它!正如在入门部分所讨论的,你在选择云服务提供商方面有多个选择,可以将你的 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
-
一旦安装了 Heroku CLI,你可以使用以下命令登录 Heroku 容器注册表:
heroku container:login
-
接下来,从包含代理的 Dockerfile 的目录运行以下命令;例如:
tfrl-cookbook)praveen@desktop:~/tensorflow2-reinforcement-learning-cookbook/src/ch7-cloud-deploy-deep-rl-agents$heroku create
-
如果你尚未登录 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:
-
登录后,你将看到类似如下的输出:
Creating salty-fortress-4191... done, stack is heroku-18 https://salty-fortress-4191.herokuapp.com/ | https://git.heroku.com/salty-fortress-4191.git
-
这就是你在 Heroku 上的容器注册表地址。你现在可以使用以下命令构建你的机器人容器并将其推送到 Heroku:
heroku container:push web
-
一旦该过程完成,你可以使用以下命令将机器人容器镜像发布到 Heroku 应用:
heroku container:release web
-
恭喜!你刚刚将你的机器人部署到了云端!你现在可以通过新的地址访问你的机器人,例如在之前代码中使用的示例地址
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 的策略。本章还提供了使用Ray、Tune 和 RLLib 框架训练分布式深度强化学习(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
文件。让我们开始:
-
让我们直接进入构建残差神经网络的模板:
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
-
使用上面的 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"""
-
接下来,让我们处理输入图像数据表示中的通道顺序。最常见的维度顺序是:
batch_size
xchannels
xwidth
xheight
或batch_size
xwidth
xheight
xchannels
。我们将处理这两种情况: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
-
现在,让我们对输入数据进行零填充,并应用初始层开始处理:
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)
-
现在是时候使用我们创建的
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,)
-
作为最终层,我们希望添加一个经过
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)
-
在 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
-
使用我们刚才讨论的 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)
-
定义好我们的模型后,我们可以跳到多 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
-
我们现在可以选择使用哪个数据集来运行我们的分布式训练管道。在这个食谱中,我们将使用
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
-
下一步需要你全神贯注!我们将选择分布式执行策略。TensorFlow 2.x 将许多功能封装成了一个简单的 API 调用,如下面所示:
strategy = tf.distribute.MirroredStrategy() print(f"Number of devices: { strategy.num_replicas_in_sync}")
-
在这一步中,我们将声明关键超参数,你可以根据机器的硬件(例如 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
-
在开始准备数据集之前,让我们实现一个预处理函数,该函数在将图像传递给神经网络之前执行操作。你可以添加你自己的自定义预处理操作。在这个食谱中,我们只需要首先将图像数据转换为
float32
,然后将图像像素值范围转换为[0, 1],而不是典型的[0, 255]区间:def preprocess(image, label): image = tf.cast(image, tf.float32) image /= 255 return image, label
-
我们已经准备好为训练和验证/测试创建数据集划分:
train_dataset = ( dataset_train.map(preprocess).cache().\ shuffle(BUFFER_SIZE).batch(BATCH_SIZE) ) eval_dataset = dataset_test.map(preprocess).batch( BATCH_SIZE)
-
我们已经到了这个食谱的关键步骤!让我们在分布式策略的范围内实例化并编译我们的模型:
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"], )
-
让我们还创建一些回调,用于将日志记录到 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 ), ]
-
有了这些,我们已经具备了使用分布式策略训练模型所需的一切。借助 Keras 用户友好的
fit()
API,它就像下面这样简单:model.fit(train_dataset, epochs=12, callbacks=callbacks)
-
当执行前面的行时,训练过程将开始。我们也可以使用以下几行手动保存模型:
path = "saved_model/" model.save(path, save_format="tf")
-
一旦我们保存了检查点,加载权重并开始评估模型就变得很容易:
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))
-
为了验证使用分布式策略训练的模型在有复制和没有复制的情况下都能正常工作,我们将在接下来的步骤中使用两种不同的方法加载并评估它。首先,让我们使用我们用来训练模型的(相同的)策略加载不带复制的模型:
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))
-
接下来,让我们在分布式执行策略的范围内加载模型,这将创建副本并评估模型:
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))
当你执行前面的两个代码块时,你会发现两种方法都会得到相同的评估准确度,这是一个好兆头,意味着我们可以在没有任何执行策略限制的情况下使用模型进行预测!
-
这完成了我们的食谱。让我们回顾一下并看看食谱是如何工作的。
它是如何工作的...
神经网络架构中的残差块应用了卷积滤波器,后接多个恒等块。具体来说,卷积块应用一次,接着是(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 地址来完成:
-
我们首先设置一个描述集群配置参数的配置项,指定我们希望在哪里训练模型。以下代码块已被注释掉,您可以根据集群设置编辑并取消注释,或者如果仅想在单机配置上尝试,可以保持注释状态:
# 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)
-
为了利用多台机器的配置,我们将使用 TensorFlow 2.x 的
MultiWorkerMirroredStrategy
:strategy = tf.distribute.experimental.MultiWorkerMirroredStrategy()
-
接下来,让我们声明训练的基本超参数。根据您的集群/计算机配置,随时调整批处理大小和
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
-
为了准备数据集,让我们实现两个快速的函数,用于规范化和增强输入图像:
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
-
为了简化操作并加快收敛速度,我们将继续使用 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))
-
为了使训练结果可重现,我们将使用固定的随机种子来打乱数据集:
tf.random.set_seed(22)
-
我们还没有准备好生成训练和验证/测试数据集。我们将使用前一步中声明的已知固定随机种子来打乱数据集,并对训练集应用数据增强:
train_dataset = ( train_dataset.map(augmentation) .map(normalize) .shuffle(NUM_TRAIN_SAMPLES) .batch(BS_PER_GPU * NUM_GPUS, drop_remainder=True) )
-
同样,我们将准备测试数据集,但我们不希望对测试图像进行随机裁剪!因此,我们将跳过数据增强,并使用标准化步骤进行预处理:
test_dataset = test_dataset.map(normalize).batch( BS_PER_GPU * NUM_GPUS, drop_remainder=True )
-
在我们开始训练之前,我们需要创建一个优化器实例,并准备好输入层。根据任务的需要,您可以使用不同的优化器,例如 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)
-
最后,我们准备在
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"], )
-
为了训练模型,我们使用简单而强大的 Keras API:
model.fit(train_dataset, epochs=NUM_EPOCHS)
-
一旦模型训练完成,我们可以轻松地保存、加载和评估:
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 代理,并使用深度卷积神经网络策略。让我们一步一步地开始实现:
-
我们将从导入实现这一食谱所需的模块开始:
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, )
-
我们将使用 OpenAI 的
procgen
环境。让我们也导入它:import procgen # Import & register procgen Gym envs
-
为了使这个食谱更易于配置和运行,让我们添加对命令行参数的支持,并配置一些有用的配置标志:
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()
-
让我们使用 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)
-
我们将首先在以下几个步骤中实现
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)
-
对于 Actor 的策略网络模型,我们将实现一个包含多个
Conv2D
和MaxPool2D
层的深度卷积神经网络。在这一步我们将开始实现,接下来的几步将完成它: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)
-
我们将添加更多的 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)
-
现在,我们可以使用一个扁平化层,并为策略网络准备输出头:
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)
-
作为构建策略网络神经模型的最后一步,我们将创建输出层并返回一个 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")
-
使用我们在前面步骤中定义的模型,我们可以开始处理状态/观察图像输入,并生成 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
-
接下来,为了计算驱动学习的替代损失,我们将实现
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)
-
接下来是一个核心方法,它将所有方法连接在一起以执行训练。请注意,这是每个副本的训练方法,我们将在后续的分布式训练方法中使用它:
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
-
为了实现分布式训练方法,我们将使用
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)
-
这就完成了我们的
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)
-
你一定注意到,我们在执行策略的作用域内创建了 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)
-
与我们的 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)
-
我们将添加值输出头,并将模型作为 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")
-
如你所记得,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)
-
与我们的 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
-
我们将通过实现
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 )
-
在实现了我们的
Actor
和Critic
类后,我们可以开始我们的分布式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())
-
接下来,我们将实现一个方法来计算广义优势估计(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
-
我们已经准备好开始我们的
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
-
现在,我们可以开始为每个回合执行的循环,直到回合结束:
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)
-
在每个回合内,如果我们达到了
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=\ [], []
-
在相同的执行上下文中,我们需要训练
Actor
和Critic
: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)
-
最后,我们需要重置跟踪变量并更新我们的回合奖励值:
state_batch = [] action_batch = [] reward_batch = [] old_policy_batch = [] episode_reward += reward state = next_state
-
这样,我们的分布式
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 的支持下获得的训练加速效果!
让我们回顾一下我们完成的工作以及配方如何工作的下一部分。
它是如何工作的...
我们实现了Actor
和Critic
类,其中 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
现在,让我们开始吧!
如何实现...
我们将逐个实现这些基础模块,从分布式参数服务器开始:
-
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
-
我们还将添加一个方法将权重保存到磁盘:
# 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’}.")
-
作为下一个构建块,我们将实现
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
-
接下来,我们将实现一个方法,将新经验存储到重放缓冲区:
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
-
为了从重放缓冲区采样一批经验数据,我们将实现一个方法,从重放缓冲区随机采样并返回一个包含采样经验数据的字典:
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])
-
这完成了我们的
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)
-
在代理初始化并加载完毕,环境实例也准备好后,我们可以开始我们的经验收集循环:
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
-
让我们处理
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)
-
最后,在回合结束时,使用参数服务器同步行为策略的权重:
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)
-
这完成了
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
-
我们的配方中的最后一个模块是
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, }
-
接下来,创建一个所需环境的实例,获取状态和观察空间,初始化 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"])
-
在这一步,我们将初始化
ParameterServer
类的实例和ReplayBuffer
类的实例:params_server = \ ParameterServer.remote(agent.actor.get_weights()) replay_buffer = ReplayBuffer.remote( config["obs_shape"], \ config["action_space"].shape, \ config["replay_size"] )
-
我们现在准备好运行已构建的模块了。我们将首先根据配置参数中指定的工作者数量,启动一系列
rollout
任务,这些任务将在分布式 ray 集群上启动rollout
过程:task_rollout = [ rollout.remote(params_server, replay_buffer, config) for i in range(config["num_workers"]) ]
rollout
任务将启动远程任务,这些任务将使用收集到的经验填充重放缓冲区。上述代码将立即返回,即使rollout
任务需要时间来完成,因为它是异步函数调用。 -
接下来,我们将启动一个可配置数量的学习者,在 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)
-
最后,让我们定义我们的入口点。我们将使用 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
这就完成了我们的实现!让我们在下一节简要讨论它的工作原理。
它是如何工作的……
我们构建了一个分布式的ParameterServer
、ReplayBuffer
、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 集群上启动训练,并逐步自定义训练流水线,以为你提供有用的食谱:
-
在 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).
-
让我们尝试在
coinrun
的procgen
环境中训练一个 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 一起使用。
-
我们将在这一步开始实现自定义模型(
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 )
-
接下来,让我们实现一个辅助方法来构建并返回一个简单的残差块:
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
-
让我们实现另一个方便的函数来构建多个残差块序列:
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
-
现在,我们可以开始实现
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)
-
在
__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
-
我们还将实现一个单行方法来重新调整值函数的输出:
def value_function(self): return tf.reshape(self._value, [-1])
这样,我们的
CustomModel
实现就完成了,并且可以开始使用了! -
我们将实现一个使用 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
-
在这一步,我们将把我们的自定义模型注册到 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()
-
我们将查看另一个快速食谱(
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
-
现在,让我们将自定义模型注册到 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")
-
现在,我们可以实现自定义训练循环,并根据需要在循环中加入任何步骤。我们将通过每隔 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
-
请注意,我们可以继续使用保存的检查点和 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)
-
最后,让我们关闭 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 代理训练。下章见!
第九章:第九章:在多个平台上部署深度强化学习代理
本章提供了将深度强化学习代理模型部署到桌面、Web、移动设备等应用中的教程。每个教程都是可定制的模板,你可以根据自己的用例构建和部署自己的深度强化学习应用。你还将学习如何导出强化学习代理模型,以便在各种生产环境中进行部署,支持的格式包括TensorFlow Lite、TensorFlow.js和ONNX,并了解如何利用 Nvidia Triton 启动生产就绪的基于强化学习的 AI 服务。
本章将涵盖以下教程:
-
使用 TensorFlow Lite 打包深度强化学习代理以便在移动设备和物联网设备上部署
-
在移动设备上部署强化学习代理
-
使用 TensorFlow.js 为 Web 和 Node.js 打包深度强化学习代理
-
将深度强化学习代理作为服务进行部署
-
为跨平台部署打包深度强化学习代理
技术要求
书中的代码在 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 Lite 打包深度强化学习代理以便在移动设备和物联网设备上部署
本教程将展示如何利用开源的TensorFlow Lite(TFLite)框架,在移动设备、物联网设备和嵌入式设备上部署深度强化学习代理。我们将实现一个完整的脚本,用于构建、训练和导出代理模型,你可以将其加载到移动设备或嵌入式设备中。我们将探索两种方法来生成 TFLite 模型。第一种方法是将代理模型保存并导出为 TensorFlow 的 SavedModel 文件格式,然后使用命令行转换器。第二种方法是通过 Python API 直接生成 TFLite 模型。
让我们开始吧!
准备工作
为了完成本教程,你需要首先激活 tf2rl-cookbook
Python/conda 虚拟环境。确保更新环境以匹配最新的 conda 环境规范文件(tfrl-cookbook.yml
),该文件位于本书代码库中。如果以下导入没有问题,那么你已经准备好开始了:
import argparse
import os
import sys
from datetime import datetime
import gym
import numpy as np
import procgen # Used to register procgen envs with Gym registry
import tensorflow as tf
from tensorflow.keras.layers import Conv2D, Dense, Dropout, Flatten, Input, MaxPool2D
现在,让我们开始吧!
如何操作...
在接下来的步骤中,为了节省空间,我们将重点介绍本食谱中特有的新功能。我们将展示模型保存和导出功能以及你可以采用的不同方式,并将Actor
、Critic
和Agent
模型的定义从以下步骤中省略,以节省空间。完整实现请参考书籍的代码仓库。
让我们开始吧:
-
首先,重要的是将 TensorFlow Keras 的后端设置为使用
float32
作为浮动值的默认表示,而不是默认的float64
:tf.keras.backend.set_floatx("float32")
-
接下来,让我们为传递给脚本的参数创建一个处理程序。我们还将为
--env
标志定义一个可供选择的训练环境选项列表:parser = argparse.ArgumentParser(prog="TFRL-Cookbook-Ch9-PPO-trainer-exporter-TFLite") parser.add_argument( "--env", default="procgen:procgen-coinrun-v0", choices=["procgen:procgen-bigfish", "procgen:procgen-bossfight", "procgen:procgen-caveflyer", "procgen:procgen-chaser", "procgen:procgen-climber", "procgen:procgen-coinrun", "procgen:procgen-dodgeball", "procgen:procgen-fruitbot", "procgen:procgen-heist", "procgen:procgen-jumper", "procgen:procgen-leaper", "procgen:procgen-maze", "procgen:procgen-miner", "procgen:procgen-ninja", "procgen:procgen-plunder", "procgen:procgen-starpilot", "Pong-v4", ], )
-
我们将添加一些其他参数,以便简化代理的训练和日志配置:
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()
-
让我们也设置日志记录,这样我们就可以使用 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)
-
对于第一种导出方法,我们将在以下步骤中为
Actor
、Critic
和Agent
类定义保存方法。我们将从Actor
类中的save
方法的实现开始,将 Actor 模型导出为 TensorFlow 的SavedModel
格式:def save(self, model_dir: str, version: int = 1): actor_model_save_dir = os.path.join( model_dir, "actor", str(version), \ "model.savedmodel" ) self.model.save(actor_model_save_dir, save_format="tf") print(f"Actor model saved at:\ {actor_model_save_dir}")
-
同样,我们将为
Critic
类实现save
方法,将 Critic 模型导出为 TensorFlow 的SavedModel
格式:def save(self, model_dir: str, version: int = 1): critic_model_save_dir = os.path.join( model_dir, "critic", str(version), \ "model.savedmodel" ) self.model.save(critic_model_save_dir, save_format="tf") print(f"Critic model saved at:{ critic_model_save_dir}")
-
现在,我们可以为
Agent
类添加一个save
方法,该方法将利用Actor
和Critic
的save
方法来保存 Agent 所需的两个模型:def save(self, model_dir: str, version: int = 1): self.actor.save(model_dir, version) self.critic.save(model_dir, version)
-
一旦执行了
save()
方法,它将生成两个模型(一个用于 Actor,一个用于 Critic),并将它们保存在文件系统中指定的目录里,目录结构和文件内容类似于下图所示:图 9.1 – PPO RL 代理的 TensorFlow SavedModel 目录结构和文件内容
-
一旦生成了
SavedModel
文件,我们可以使用tflite_convert
命令行工具,并指定 Actor 模型保存目录的位置。参考以下命令的示例:(tfrl-cookbook)praveen@desktop:~/tfrl-cookbook/ch9$tflite_convert \ --saved_model_dir=trained_models/ppo-procgen-coinrun/1/actor/model.savedmodel \ --output_file=trained_models/ppo-procgen-coinrun/1/actor/model.tflite
-
类似地,我们可以使用以下命令转换 Critic 模型:
(tfrl-cookbook)praveen@desktop:~/tfrl-cookbook/ch9$tflite_convert \ --saved_model_dir=trained_models/ppo-procgen-coinrun/1/critic/model.savedmodel \ --output_file=trained_models/ppo-procgen-coinrun/1/critic/model.tflite
万岁!现在我们有了 TFLite 格式的 Actor 和 Critic 模型,可以将它们与我们的移动应用一起发布。接下来,我们将看另一种方法,这种方法不需要我们(手动)切换到命令行来转换模型。
-
还有另一种方法可以将 Agent 模型导出为 TFLite 格式。我们将在以下步骤中实现这一方法,从
Actor
类的save_tflite
方法开始:def save_tflite(self, model_dir: str, version: int =\ 1): """Save/Export Actor model in TensorFlow Lite format""" actor_model_save_dir = os.path.join(model_dir,\ "actor", str(version)) model_converter = \ tf.lite.TFLiteConverter.from_keras_model( self.model) # Convert model to TFLite Flatbuffer tflite_model = model_converter.convert() # Save the model to disk/persistent-storage if not os.path.exists(actor_model_save_dir): os.makedirs(actor_model_save_dir) actor_model_file_name = os.path.join( actor_model_save_dir, "model.tflite") with open(actor_model_file_name, "wb") as \ model_file: model_file.write(tflite_model) print(f"Actor model saved in TFLite format at:\ {actor_model_file_name}")
-
同样地,我们将为
Critic
类实现save_tflite
方法:def save_tflite(self, model_dir: str, version: \ int = 1): """Save/Export Critic model in TensorFlow Lite format""" critic_model_save_dir = os.path.join(model_dir, "critic", str(version)) model_converter = \ tf.lite.TFLiteConverter.from_keras_model( self.model) # Convert model to TFLite Flatbuffer tflite_model = model_converter.convert() # Save the model to disk/persistent-storage if not os.path.exists(critic_model_save_dir): os.makedirs(critic_model_save_dir) critic_model_file_name = os.path.join( critic_model_save_dir, "model.tflite") with open(critic_model_file_name, "wb") as \ model_file: model_file.write(tflite_model) print(f"Critic model saved in TFLite format at:\ {critic_model_file_name}")
-
之后,Agent 类可以调用
save_tflite
方法,通过它自己的save_tflite
方法,像以下代码片段所示那样调用 Actor 和 Critic:def save_tflite(self, model_dir: str, version: \ int = 1): # Make sure `toco_from_protos binary` is on # system's PATH to avoid TFLite ConverterError toco_bin_dir = os.path.dirname(sys.executable) if not toco_bin_dir in os.environ["PATH"]: os.environ["PATH"] += os.pathsep + \ toco_bin_dir print(f"Saving Agent model (TFLite) to:{ model_dir}\n") self.actor.save_tflite(model_dir, version) self.critic.save_tflite(model_dir, version)
请注意,我们将当前(
tfrl-cookbook
)Python 环境的bin
目录添加到了系统的PATH
环境变量中,以确保 TFLite 转换器调用模型转换时能够找到toco_from_protos
二进制文件。 -
总结一下,我们可以最终确定
main
函数,实例化代理,并训练和保存模型为 TFLite 模型文件格式:if __name__ == "__main__": env_name = args.env env = gym.make(env_name) agent = PPOAgent(env) agent.train(max_episodes=1) # Model saving model_dir = "trained_models" agent_name = f"PPO_{env_name}" agent_version = 1 agent_model_path = os.path.join(model_dir, \ agent_name) agent.save_tflite(agent_model_path, agent_version)
这就是我们本次的示例。让我们回顾一下其中的一些重要细节,以便更好地理解这个示例。
它是如何工作的...
我们首先将 TensorFlow Keras 的后端设置为使用float32
作为浮动值的默认表示。这是因为否则 TensorFlow 将使用默认的float64
表示,而 TFLite 不支持这种表示(出于性能考虑),因为 TFLite 主要针对嵌入式和移动设备的运行。
然后,我们为--env
参数定义了一个选择列表。这一点非常重要,以确保环境的观察和动作空间与代理模型兼容。在这个示例中,我们使用了一个 PPO 代理,配有 Actor 和 Critic 网络,期望图像观察并在离散空间中产生动作。你可以将代理代码替换为之前章节中的 PPO 实现,这些实现使用不同的状态/观察空间和动作空间。你还可以完全替换成其他代理算法。你将在本章的代码库中找到一个附加示例,它导出了一个 DDPG 代理的 TFLite 模型。
我们讨论了两种保存并转换代理模型为 TFLite 格式的方法。第一种方法是先生成一个 TensorFlow 的 SavedModel 文件格式,然后使用tflite_convert
命令行工具将其转换为 TFLite 模型文件格式。第二种方法,我们使用了 TFLite 的 Python API,直接(在内存中)将代理的模型转换并保存为 TFLite(Flatbuffer)格式。我们利用了官方 TensorFlow 2.x Python 包中的TFLiteConverter
模块。下面的图示总结了通过 API 导出 RL 代理模型的不同方法:
图 9.2 – 将 TensorFlow 2.x 模型转换为 TensorFlow Lite Flatbuffer 格式
你可以在这里了解更多关于 TFLite 模型格式的信息:www.tensorflow.org/lite
。
是时候进入下一个示例了!
在移动设备上部署 RL 代理
移动设备是最具目标性的应用平台,因为它的用户覆盖面远远超过其他平台。根据www.alliedmarketresearch.com/mobile-application-market
,全球移动应用市场预计到 2026 年将达到 4073.2 亿美元。如此庞大的市场为基于 RL 的人工智能提供了许多机会。Android 和 iOS 是该领域的两大操作系统平台。虽然 iOS 是一个流行的平台,但开发 iOS 应用需要使用 Mac 电脑。因此,我们将使用 Android SDK 开发 Android 应用,这对于大多数人来说更容易访问。如果你是 iOS 应用开发者,可能可以将本教程的部分内容适配到你的应用中。
本教程提供了通过 TensorFlow Lite 框架将训练好的 RL 智能体模型部署到移动设备和/或物联网设备的方法。你还将获得一个示例 RL 乒乓球 Android 应用,可以将其作为测试平台来部署 RL 智能体,或开发你自己的创意和应用:
图 9.3 – RL 乒乓球应用在 Android 设备上的运行截图
让我们开始吧!
准备就绪
我们将使用 Android Studio 设置并开发示例 RL Android 应用程序。从官方网站下载并安装 Android Studio:developer.android.com/studio
。推荐使用默认安装位置。安装完成后,运行 Android Studio 启动Android Studio 设置向导。按照设置过程进行操作,并确保安装了最新的 Android SDK、Android SDK 命令行工具和 Android SDK 构建工具。
完成后运行应用程序时,你有两个选择:1. 在 Android 手机上运行 2. 在 Android 虚拟设备模拟器中运行。根据你的选择,按照以下设置说明操作:
-
在 Android 手机上运行:
a) 在 Android 设置中启用开发者选项和 USB 调试。详细操作说明请参阅:
developer.android.com/studio/debug/dev-options
。b) 如果你使用的是 Windows 系统,安装 Google USB 驱动程序:
developer.android.com/studio/run/win-usb
。c) 使用 USB 数据线将手机连接到计算机,如果出现提示,允许计算机访问你的手机。
d) 运行
adb devices
确保手机已被检测到。如果手机没有被检测到,确保驱动程序已安装并且手机上已启用 ADB 调试。你可以参考 Android 官方指南获取详细的操作说明:developer.android.com/studio/run/device#setting-up
。 -
在 Android 模拟器中运行:
a) 启动 Android Studio,点击 AVD Manager 图标并选择 Create Virtual Device。
b) 选择一个设备并点击 Next。
c) 选择一个 x86 或 x86_64 镜像,用于你想要模拟的 Android 版本,并完成过程。
d) 点击 Run,在 AVD Manager 工具栏中启动模拟器。
在你设置好设备后,进入 src/ch9-cross-platform-deployment
目录中的代码目录。你将看到一个示例 Android 应用,其目录结构和内容如下面的截图所示:
图 9.4 – 示例 Android 应用的目录结构和内容
一旦你有了示例代码库,继续阅读下一部分,看看如何准备我们的 RL agent 模型并构建应用。
如何操作...
我们将从 RL agent 模型的准备工作开始,然后构建一个简单的双人乒乓球应用,你可以与 agent 对战。按照这里列出的步骤操作:
-
使用本章前面讨论的食谱,将 RL agent(Actor)的模型导出为 TFLite 格式。例如,你可以运行前面的食谱来训练一个 PPO agent,用于
Pong-v4
环境,并使用生成的model.tflite
文件,位于trained_models/actor/1/
目录下。将模型文件放置在 Android 应用的app/src/assets/
目录下,如下图所示:图 9.5 – RL agent model.tflite 在 Android 应用 src 中的位置
-
编辑应用的
build.gradle
文件中的dependencies
部分,加入tensorflow-lite
依赖:dependencies { implementation fileTree(dir: 'libs', include: \ ['*.jar']) implementation 'org.tensorflow:tensorflow-lite:+' }
-
添加一个成员方法,从
assets
文件夹加载agent/model.tflite
并返回一个MappedByteBuffer
:MappedByteBuffer loadModelFile(AssetManager \ assetManager) throws IOException { AssetFileDescriptor fileDescriptor = \ assetManager.openFd("agent/model.tflite"); FileInputStream inputStream = new \ FileInputStream( fileDescriptor.getFileDescriptor()); FileChannel fileChannel = \ inputStream.getChannel(); long startOffset = \ fileDescriptor.getStartOffset(); long declaredLength = \ fileDescriptor.getDeclaredLength(); return fileChannel.map( FileChannel.MapMode.READ_ONLY, \ startOffset, declaredLength); }
-
我们现在可以像这样创建一个新的 TFLite 解释器:
interpreter = new Interpreter(loadModelFile(assetManager), new Interpreter.Options());
-
解释器已准备好。让我们准备输入数据。首先,根据我们从 agent 训练中了解的信息,定义一些常量:
static final int BATCH_SIZE = 1; static final int OBS_IMG_WIDTH = 160; static final int OBS_IMG_HEIGHT = 210; static final int OBS_IMG_CHANNELS = 3; // Image observation normalization static final int IMAGE_MEAN = 128; static final float IMAGE_STD = 128.0f;
-
现在,让我们实现一个方法,将
BitMap
格式的图像数据转换为ByteArray
:ByteBuffer convertBitmapToByteBuffer(Bitmap bitmap) { ByteBuffer byteBuffer; byteBuffer = ByteBuffer.allocateDirect(4 * \ BATCH_SIZE * OBS_IMG_WIDTH * \ OBS_IMG_HEIGHT * OBS_IMG_CHANNELS); byteBuffer.order(ByteOrder.nativeOrder()); int[] intValues = new int[OBS_IMG_WIDTH * \ OBS_IMG_HEIGHT]; bitmap.getPixels(intValues,0, bitmap.getWidth(),\ 0, 0, bitmap.getWidth(), bitmap.getHeight()); int pixel = 0; for (int i = 0; i < OBS_IMG_HEIGHT; ++i) { for (int j = 0; j < OBS_IMG_WIDTH; ++j) { final int val = intValues[pixel++]; byteBuffer.putFloat((((val >> 16) &\ 0xFF)-IMAGE_MEAN)/IMAGE_STD); byteBuffer.putFloat((((val >> 8) & \ 0xFF)-IMAGE_MEAN)/IMAGE_STD); byteBuffer.putFloat((((val) & 0xFF)-\ IMAGE_MEAN)/IMAGE_STD); } } return byteBuffer; }
-
我们现在可以将乒乓球游戏中的图像观察数据传入 Agent 模型,以获取动作:
ByteBuffer byteBuffer = convertBitmapToByteBuffer(bitmap); int[] action = new int[ACTION_DIM]; interpreter.run(byteBuffer, action);
这些就是本教程的所有主要成分!你可以将它们循环运行,以根据每个观察/游戏帧生成动作,或者根据你的需求进行定制!接下来,让我们看看如何使用 Android Studio 在 Android 设备上运行该应用。
-
启动 Android Studio。你将看到类似于下图的屏幕:
图 9.6 – Android Studio 欢迎屏幕
让我们继续进行下一步。
-
点击打开现有项目选项,弹出窗口会要求你选择文件系统中的目录。导航到你克隆的书籍代码仓库或你的分支文件夹,然后浏览到第九章中的这个配方文件夹,正如图示所示:
图 9.7 – 选择文件/项目界面以选择 RL Android 应用
你会注意到 Android Studio 已经识别出我们的应用,并显示带有 Android 图标的目录。
-
一旦你点击确定,Android Studio 将会打开并加载应用的代码,界面如下图所示:
图 9.8 – Android Studio 中加载 TFRL-Cookbook 的 RL 应用
到目前为止,一切顺利!
-
让我们通过点击构建菜单并选择构建项目来构建项目,如下图所示(或者直接按 Ctrl + F9):
图 9.9 – 使用 Make Project 选项构建 RL Android 应用
这个过程可能需要一些时间,你可能会在构建信息标签中看到有用的状态消息。
-
一旦构建过程完成,你会看到
.apk
文件,可以在 Android 设备上运行。 -
让我们继续使用运行菜单来运行应用,如下图所示:
图 9.11 – 在 Android Studio 中运行 RL 应用的 Run 菜单选项
此时,如果你已经将 Android 设备/手机连接到计算机,你可以在手机上启动该应用。否则,你可以使用 AVD 来模拟 Android 设备。
-
让我们从设备菜单中选择一个 AVD 设备来模拟,如下图所示:
图 9.12 – 选择 AVD 来模拟 Android 设备
我们现在已经准备好设备来运行这个应用。
-
让我们继续并启动/运行应用!你可以使用运行 'app'按钮,在运行菜单中,如下图所示:
图 9.13 – 运行 'app' 命令来启动应用
这应该会在 AVD 模拟器上启动应用(如果你选择了手机,则会在手机上启动)。
-
应用应该会在 Android 设备上启动,界面应该像下面的图示:
图 9.14 – TFRL-Cookbook RL 应用在 Android(模拟)设备上运行
恭喜!
这就完成了我们的配方。前往下一节以了解更多关于该配方的内容。
它是如何工作的...
在前一个配方中,我们看到如何将深度强化学习智能体的模型导出为 TFLite 格式。前一个配方生成了两个 model.tflite
文件,一个用于演员(Actor),另一个用于评论员(Critic)。
注意
你可以按照本书中之前讨论的配方,训练任何你选择的智能体算法,并使用本章中的配方 使用 TensorFlow Lite 为移动设备和物联网设备打包深度强化学习智能体 来获取在此配方中使用的演员 model.tflite
文件。
正如你可能记得在第三章中所提到的,在深度强化学习智能体上实现高级深度强化学习算法,演员组件负责根据学习到的策略生成动作,而评论员组件估计状态或状态-动作值。在部署强化学习智能体时,我们更关心的是智能体生成的动作,而不是预测的状态或状态-动作值。因此,在此配方中,我们仅使用智能体的演员模型进行部署。
我们首先通过更新应用的 gradle.build
文件来包含 TFLite 依赖项。接着我们添加了一个名为 loadModelFile
的方法来加载智能体的模型(model.tflite
)。此方法返回一个 MappedByteBuffer
对象,这是初始化 TFLite 解释器实例所需的。模型加载完成并且创建了 TFLite 解释器实例后,我们可以用有效的输入运行解释器来获得智能体的动作。为了确保输入的格式有效,我们将图像数据从 BitMap
格式转换为 ByteBuffer
格式。我们还根据用于训练强化学习智能体的环境的观察空间定义了图像观察的宽度、高度、通道数等。
在步骤 7中,智能体模型返回的动作可以用来驱动/移动,例如乒乓球游戏中的红色球拍,并且可以在循环中对每个新的观察重复之前的步骤,让智能体与自己或人类对战!
接着我们看到如何使用 Android Studio 启动应用,并最终完成该配方。希望你玩得开心!
当你准备好时,我们可以继续进行下一个配方。
使用 TensorFlow.js 打包深度强化学习(Deep RL)智能体以供网页和 Node.js 使用
JavaScript 是开发 Web 应用程序时的首选语言,因为它在前端和后端编程语言中都具有多功能性,可以通过 Web 浏览器或 Node.js 执行。能够在 Web 上运行 RL 智能体将为在 Web 应用程序中部署 RL 智能体开辟多个新路径。本示例将展示如何训练并导出 RL 智能体模型,将其转换为可以在 JavaScript 应用程序中使用的格式,这些应用程序可以直接在浏览器中运行或在 Node.js 环境中运行。TensorFlow.js (TF.js) 库允许我们使用 JavaScript 运行现有模型,甚至训练/重训新模型。我们将使用 tensorflowjs
Python 模块将我们的智能体模型导出为支持的格式,然后可以将其导入到基于 JavaScript 的 Web 或桌面(Node.js/Electron)应用中。我们将探讨两种将智能体模型导出为 TF.js 层格式的方法。
让我们开始!
准备工作
要完成此食谱,您首先需要激活 tf2rl-cookbook
Python/conda 虚拟环境。确保更新环境以匹配食谱代码库中的最新 conda 环境规范文件(tfrl-cookbook.yml
)。如果以下导入没有问题,则可以开始:
import argparse
import copy
import os
import random
from collections import deque
from datetime import datetime
import gym
import numpy as np
import tensorflow as tf
import tensorflowjs as tfjs
from tensorflow.keras.layers import (
Conv2D,
Dense,
Dropout,
Flatten,
Input,
Lambda,
MaxPool2D,
)
import webgym
现在,让我们开始吧!
如何实现...
在接下来的文本中,我们将通过专注于此食谱中特有的新增和重要部分来节省空间。我们将讲解模型保存和导出功能,以及如何通过不同方式实现,并将演员、评论员和智能体模型定义从以下步骤中省略,以节省空间。请参阅书中的代码库以获取完整的实现,包括训练和日志记录方法。
让我们开始吧:
-
让我们首先设置一个命令行参数解析器,方便自定义脚本:
parser = argparse.ArgumentParser( prog="TFRL-Cookbook-Ch9-DDPGAgent-TensorFlow.js-exporter" ) parser.add_argument("--env", default="MiniWoBSocialMediaMuteUserVisualEnv-v0") parser.add_argument("--actor_lr", type=float, default=0.0005) parser.add_argument("--critic_lr", type=float, default=0.001) parser.add_argument("--batch_size", type=int, default=64) parser.add_argument("--tau", type=float, default=0.05) parser.add_argument("--gamma", type=float, default=0.99) parser.add_argument("--train_start", type=int, default=2000) parser.add_argument("--logdir", default="logs") args = parser.parse_args()
-
我们还将设置日志记录,以便使用 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)
-
对于第一个导出方法,我们将在接下来的步骤中为
Actor
、Critic
和Agent
类定义save_h5
方法。我们将从实现Actor
类中的save_h5
方法开始,将演员模型导出为 Keras 的h5
格式:def save_h5(self, model_dir: str, version: int = 1): actor_model_save_dir = os.path.join( model_dir, "actor", str(version), "model.h5" ) self.model.save(actor_model_save_dir, \ save_format="h5") print(f"Actor model saved at:\ {actor_model_save_dir}")
-
同样,我们将为
Critic
类实现一个save
方法,将评论员模型导出为 Keras 的h5
格式:def save_h5(self, model_dir: str, version: int = 1): critic_model_save_dir = os.path.join( model_dir, "critic", str(version), "model.h5" ) self.model.save(critic_model_save_dir, \ save_format="h5") print(f"Critic model saved at:\ {critic_model_save_dir}")
-
现在我们可以为
Agent
类添加一个save
方法,该方法将利用演员和评论员的save
方法来保存智能体所需的两个模型:def save_h5(self, model_dir: str, version: int = 1): self.actor.save_h5(model_dir, version) self.critic.save_h5(model_dir, version)
-
一旦执行了
save_h5()
方法,save
方法将生成两个模型(一个用于演员,另一个用于评论员),并将它们保存在文件系统中指定的目录下,目录结构和文件内容如下图所示:图 9.15 – DDPG RL 代理的目录结构和文件内容,使用
save_h5
模型导出 -
一旦生成
.h5
文件,我们可以使用tensorflowjs_converter
命令行工具,并指定 Actor 模型的save
目录的位置。请参考以下命令作为示例:(tfrl-cookbook)praveen@desktop:~/tfrl-cookbook/ch9$tensorflowjs_converter --input_format keras \ actor/1/model.h5 \ actor/t1/model.tfjs
-
类似地,我们可以使用以下命令转换 Critic 模型:
(tfrl-cookbook)praveen@desktop:~/tfrl-cookbook/ch9$tensorflowjs_converter --input_format keras \ critic/1/model.h5 \ critic/t1/model.tfjs
太好了!我们现在拥有了 TF.js 层格式的 Actor 和 Critic 模型。我们将查看另一种方法,它不需要我们(手动)切换到命令行来转换模型。
-
还有另一种方法可以将代理模型导出为 TF.js 层格式。我们将在接下来的步骤中实现这一方法,从
Actor
类的save_tfjs
方法开始:def save_tfjs(self, model_dir: str, version: \ int = 1): """Save/Export Actor model in TensorFlow.js supported format""" actor_model_save_dir = os.path.join( model_dir, "actor", str(version), \ "model.tfjs" ) tfjs.converters.save_keras_model(self.model,\ actor_model_save_dir) print(f"Actor model saved in TF.js format at:\ {actor_model_save_dir}")
-
类似地,我们将实现
Critic
类的save_tfjs
方法:def save_tfjs(self, model_dir: str, version: \ int = 1): """Save/Export Critic model in TensorFlow.js supported format""" critic_model_save_dir = os.path.join( model_dir, "critic", str(version), \ "model.tfjs" ) tfjs.converters.save_keras_model(self.model,\ critic_model_save_dir) print(f"Critic model saved TF.js format \ at:{critic_model_save_dir}")
-
然后,
Agent
类可以通过其自己的save_tfjs
方法调用 Actor 和 Critic 的save_tfjs
方法,如下方代码片段所示:def save_tfjs(self, model_dir: str, version: \ int = 1): print(f"Saving Agent model to:{model_dir}\n") self.actor.save_tfjs(model_dir, version) self.critic.save_tfjs(model_dir, version)
-
当执行代理的
save_tfjs
方法时,将生成 Actor 和 Critic 模型的 TF.js 层格式,并将具有如图所示的目录结构和文件内容:图 9.16 – DDPG RL 代理的目录结构和文件内容,使用
save_tfjs
模型导出 -
总结一下,我们可以最终确定
main
函数来实例化代理,并直接使用 Python API 在 TF.js 层格式中训练并保存模型:if __name__ == "__main__": env_name = args.env env = gym.make(env_name) agent = PPOAgent(env) agent.train(max_episodes=1) # Model saving model_dir = "trained_models" agent_name = f"PPO_{env_name}" agent_version = 1 agent_model_path = os.path.join(model_dir, \ agent_name) # agent.save_h5(agent_model_path, agent_version) agent.save_tfjs(agent_model_path, agent_version)
-
现在,你可以将 TF.js 模型部署到你的 web 应用、Node.js 应用、Electron 应用或任何其他基于 JavaScript/TypeScript 的应用中。让我们在下一部分回顾一下我们在这个配方中使用的一些关键项。
它是如何工作的...
在这个配方中,我们使用了一个 DDPG 代理,其中包含期望图像观察并在连续空间中产生动作的 Actor 和 Critic 网络。你可以用本书前几章中使用不同状态/观察空间和动作空间的 DDPG 实现来替换代理代码。你也可以完全用不同的代理算法替换它。你会在本章的代码库中找到一个额外的配方,用于导出 PPO 代理的 TF.js 模型。
我们讨论了两种方法来保存和转换我们的代理模型为 TF.js 格式。第一种方法允许我们生成一个 Keras 模型,其格式为 H5,这是 HDF5 的缩写,全称是“层次化数据格式第五版文件格式”。然后我们使用 tensorflowjs_converter
命令行工具将其转换为 TF.js 模型。尽管每个模型仅需处理单个文件而且轻量化且易于处理,但与 SavedModel 文件格式相比,Keras HDF5 模型存在一些限制。具体来说,Keras HDF5 模型不包含自定义对象/层的计算图,因此在运行时需要这些自定义对象的 Python 类/函数定义来重建模型。此外,在我们在模型类定义之外添加损失项和指标的情况下(使用 model.add_loss()
或 model.add_metric()
),这些内容不会导出到 HDF5 模型文件中。
在第二种方法中,我们使用 tensorflowjs
Python 模块直接(内存中)将代理模型转换并保存为 TF.js 层格式。
您可以在此处了解有关 TF.js 的更多信息:www.tensorflow.org/js
。
现在是下一个食谱的时间!
将深度 RL 代理部署为服务
一旦您训练好您的 RL 代理来解决问题或业务需求,您将希望将其部署为一个服务 – 与将训练好的代理模型作为产品提供相比更为可能,这是因为包括可伸缩性和模型陈旧性限制在内的多种原因。如果您将其作为产品销售,您将希望有一种方法来更新代理模型的新版本,并且您不希望维护或支持多个版本或旧版本的代理。您将需要一个坚固且经过充分测试的机制,来将您的 RL 代理作为 AI 服务提供,允许可定制的运行时(不同的框架和 CPU/GPU 支持),轻松的模型升级,日志记录,性能监控等等。
为了满足所有这些需求,我们将使用 NVIDIA 的 Triton 服务器作为后端,为我们的代理提供服务。Triton 作为一个统一的推断框架,在生产中支持大规模部署的 AI 模型。它支持多种深度学习框架,包括 TensorFlow2、PyTorch、ONNX、Caffe2 等,还包括自定义框架,并提供了其他多个生产质量的功能和优化,如并发模型执行,动态批处理,日志记录,性能和健康监控等。
让我们开始我们的食谱!
准备工作
为了完成这个步骤,你首先需要激活tf2rl-cookbook
Python/conda 虚拟环境。确保更新该环境,以匹配本书代码仓库中最新的 conda 环境规范文件(tfrl-cookbook.yml
)。你还需要确保你的机器上安装了支持你 GPU 的最新 NVIDIA 驱动程序。你还需要在机器上安装 Docker。如果尚未安装 Docker,你可以按照官方说明在此链接进行安装:docs.docker.com/get-started/
。
现在,让我们开始吧!
如何操作…
在接下来的文字中,我们将节省篇幅,专注于我们将要构建的服务。我们将省略智能体训练脚本的内容,但你可以在本书的代码仓库中的ch9-cross-platform-deployment
找到这些脚本。
让我们开始吧:
-
首先,你需要训练、保存并导出你希望作为服务托管的智能体。你可以使用示例脚本
agent_trainer_saver.py
来训练 Webgym 环境套件中的一个任务的 PPO 智能体,使用以下命令:yy.mm format) that supports your NVIDIA GPU driver version. For example, if you have installed NVIDIA driver version 450.83 (find out by running nvidia-smi), then container versions built with CUDA 11.0.3 or lower, such as container version 20.09 or older, will work.
-
一旦你确定了合适的容器版本,例如
yy.mm
,你可以使用 Docker 通过以下命令拉取 NVIDIA Triton 服务器镜像:praveen@desktop:~$ docker pull nvcr.io/nvidia/tritonserver:yy.mm-py3
-
将
yy.mm
占位符更改为你所确定的版本。例如,要拉取容器版本 20.09,你需要运行以下命令:praveen@desktop:~$ docker pull nvcr.io/nvidia/tritonserver:20.09-py3
-
当你运行
agent_trainer_saver
脚本时,训练好的模型将存储在trained_models
目录中,目录结构和内容如下所示:图 9.17 – 导出训练模型的目录结构和内容
trained_models/actor
目录将是我们在 Triton 中提供服务时的模型仓库根目录。 -
我们现在准备好将智能体的动作作为服务进行提供了!要启动服务,请运行以下命令:
-v flag to point to the trained_models/actor folder on your serving machine. Also remember to update the yy.mm value to reflect your container version (20.3, for example).
-
如果你想从没有 GPU 的机器上提供智能体模型(不推荐),你可以简单地省略
–gpus=1
标志,指示 Triton 服务器仅使用 CPU 进行服务。命令将如下所示:$ docker run --shm-size=1g --ulimit memlock=-1 --ulimit stack=67108864 --rm -p8000:8000 -p8001:8001 -p8002:8002 -v/full/path/to/trained_models/actor:/models nvcr.io/nvidia/tritonserver:yy.mm-py3 tritonserver --model-repository=/models --strict-model-config=false --log-verbose=1
-
如果在提供智能体模型时遇到问题,请检查
trained_models/actor/config.pbtxt
文件,该文件描述了模型配置。虽然 Triton 可以自动从 TensorFlow 的 SavedModels 生成config.pbtxt
文件,但对于所有模型来说,它可能并不总是有效,尤其是自定义策略网络实现。如果你使用agent_trainer_saver
脚本导出训练好的 PPO 智能体,可以使用以下config.pbtxt
。我们将在接下来的几步中讨论模型配置:{ "name": "actor", "platform": "tensorflow_savedmodel", "backend": "tensorflow", "version_policy": { "latest": { "num_versions": 1 } }, "max_batch_size": 1,
-
我们将继续指定输入(状态/观察)空间/维度配置:
"input": [ { "name": "input_1", "data_type": "TYPE_FP64", "dims": [ 64, 64, 3 ], "format": "FORMAT_NHWC" } ],
-
接下来,我们将指定输出(动作空间):
"output": [ { "name": "lambda", "data_type": "TYPE_FP64", "dims": [ 2 ] }, { "name": "lambda_1", "data_type": "TYPE_FP64", "dims": [ 2 ] } ],
-
让我们还指定实例组、优化参数等:
"batch_input": [], "batch_output": [], "optimization": { "priority": "PRIORITY_DEFAULT", "input_pinned_memory": { "enable": true }, "output_pinned_memory": { "enable": true } }, "instance_group": [ { "name": "actor", "kind": "KIND_CPU", "count": 1, "gpus": [], "profile": [] } ],
-
config.pbtxt
文件所需的最终配置参数列表如下:"default_model_filename": "model.savedmodel", "cc_model_filenames": {}, "metric_tags": {}, "parameters": {}, "model_warmup": [] }
-
好极了!我们的代理服务已上线。此时,如果你希望将此服务提供给公共网络或网络上的用户,你可以在云端/远程服务器/VPS 上运行我们之前讨论的相同命令。让我们快速向服务器发送查询,以确保一切正常:
$curl -v localhost:8000/v2/health/ready
-
如果代理模型没有问题地提供服务,你将看到类似以下的输出:
... < HTTP/1.1 200 OK < Content-Length: 0 < Content-Type: text/plain
-
你也可以使用一个完整的示例客户端应用程序查询代理服务,以获取指定的动作。让我们快速设置运行 Triton 客户端所需的工具和库。你可以使用 Python pip 来安装依赖项,如下所示的命令片段:
$ pip install nvidia-pyindex $ pip install tritonclient[all]
-
可选地,为了能够运行性能分析器(
perf_analyzer
),你需要使用以下命令安装 libb64-dev 系统软件包:$ sudo apt update && apt install libb64-dev
-
现在你已经具备了运行示例 Triton 客户端应用程序所需的所有依赖项:
$ python sample_client_app.py
这就完成了我们的食谱!接下来,我们将进入下一部分,详细回顾我们在本食谱中完成的一些细节。
它是如何工作的……
我们的食谱包含三个部分:
-
训练、保存和导出;
-
部署;
-
启动客户端。
第一部分涵盖了代理的训练、保存和导出流程。在这一部分,我们首先选择了要训练的 RL 环境和代理算法。然后,我们利用本书前面讨论的多种训练策略之一来训练代理模型。接着,我们使用本章前面讨论的模型保存和导出方法,将训练好的代理模型导出为 TensorFlow 的 SavedModel 文件格式。如你所记,我们在保存和导出代理模型时遵循了特定的目录结构和文件命名规范。这一规范与 NVIDIA Triton 服务器所使用的模型仓库规范一致,因此我们导出的模型可以轻松地通过生产级 Triton 服务器提供服务。此外,这一组织方式还使得我们能够轻松地同时管理多个版本的代理模型。
在第二部分,我们看到如何使用 NVIDIA 的 Triton 服务器部署导出的代理模型。你可以在此了解更多关于 NVIDIA Triton 的信息:developer.nvidia.com/nvidia-triton-inference-server
。
我们看到使用生产级后端来提供代理服务是多么简单。我们可以轻松地在远程/云服务器或 VPS 上运行 Docker 容器,将此服务部署到网上。
最后,一旦服务启动,我们看到客户端如何通过向服务发送包含适当输入/观察数据的动作请求来使用该服务,数据来自测试环境。
本节的内容到此为止!让我们进入本章的最后一个食谱,以总结全文。
为跨平台部署打包深度强化学习(Deep RL)代理
尽管深度强化学习(Deep RL)在游戏领域(如 Atari、象棋、围棋、将棋)和模拟机器人方面取得了最大的成功,但现实世界中的应用正逐渐显现,其中深度强化学习代理显示出很大的潜力和价值。预计很快就会将深度强化学习代理部署到各种物理形式因素中,如嵌入式控制器、计算机、自动驾驶汽车、无人机以及其他机器人等。硬件处理器(如 CPU、GPU、TPU、FPGA、ASIC)、操作系统(如 Linux、Windows、OSX、Android)、架构(如 x86、ARM)和形式因素(如服务器、桌面、移动设备、物联网、嵌入式系统等)之间的差异使得部署过程充满挑战。本教程提供了如何利用 TensorFlow 2.x 框架的库、工具和实用程序的生态系统来打包适合部署到 Web、移动设备、物联网、嵌入式系统、机器人和桌面平台的深度强化学习代理模型的指南。
本教程提供了一个完整的脚本,用于构建、训练和打包一个深度强化学习代理,支持多种格式,可以使用 TensorFlow Serving、TensorFlow Hub、TensorFlow.js、TensorFlow Lite、NVIDIA Triton、ONNX、ONNX.js、Clipper 以及大多数为深度学习模型构建的服务框架进行部署/服务。
开始吧!
准备中
要完成本教程,首先需要激活 tf2rl-cookbook
Python/conda 虚拟环境。确保更新该环境,以匹配食谱代码仓库中的最新 conda 环境规范文件(tfrl-cookbook.yml
)。如果以下导入没有问题,说明你已经准备好开始了:
import argparse
import os
import sys
from datetime import datetime
import gym
import keras2onnx
import numpy as np
import procgen # Used to register procgen envs with Gym registry
import tensorflow as tf
import tensorflowjs as tfjs
from tensorflow.keras.layers import Conv2D, Dense, Dropout, Flatten, Input, MaxPool2D
现在,开始吧!
如何操作...
在接下来的内容中,我们将通过专注于本教程中独特的新功能来节省空间。我们将重点关注各种模型保存和导出功能,而将 Actor、Critic 和 Agent 的模型定义从以下步骤中省略,以节省空间。完整的实现可以参考书籍的代码仓库。我们将首先为 Actor 实现模型的保存/导出方法,然后在后续步骤中为 Critic 重复这些步骤,最后完成代理的实现。
开始吧:
-
首先,重要的是将 TensorFlow Keras 的后端设置为使用
float32
作为默认的浮动值表示,而不是默认的float64
:tf.keras.backend.set_floatx("float32")
-
我们将从以下几个步骤开始,逐步实现
Actor
的各种保存/导出方法。首先,我们将实现save
方法,将 Actor 模型保存并导出为 TensorFlow 的SavedModel
格式:def save(self, model_dir: str, version: int = 1): actor_model_save_dir = os.path.join( model_dir, "actor", str(version), \ "model.savedmodel" ) self.model.save(actor_model_save_dir, \ save_format="tf") print(f"Actor model saved at:\ {actor_model_save_dir}")
-
接下来,我们将向
Actor
类添加save_tflite
方法,以保存和导出 Actor 模型为 TFLite 格式:def save_tflite(self, model_dir: str, version: \ int = 1): """Save/Export Actor model in TensorFlow Lite format""" actor_model_save_dir = os.path.join(model_dir,\ "actor", str(version)) model_converter = \ tf.lite.TFLiteConverter.from_keras_model( self.model) # Convert model to TFLite Flatbuffer tflite_model = model_converter.convert() # Save the model to disk/persistent-storage if not os.path.exists(actor_model_save_dir): os.makedirs(actor_model_save_dir) actor_model_file_name = \ os.path.join(actor_model_save_dir, "model.tflite") with open(actor_model_file_name, "wb") as \ model_file: model_file.write(tflite_model) print(f"Actor model saved in TFLite format at:\ {actor_model_file_name}")
-
现在,让我们实现
save_h5
方法并将其添加到Actor
类中,以将 Actor 模型保存并导出为 HDF5 格式:def save_h5(self, model_dir: str, version: int = 1): actor_model_save_path = os.path.join( model_dir, "actor", str(version), "model.h5" ) self.model.save(actor_model_save_path, \ save_format="h5") print(f"Actor model saved at:\ {actor_model_save_path}")
-
接下来,我们将向
Actor
类添加save_tfjs
方法,以保存和导出 Actor 模型为 TF.js 格式:def save_tfjs(self, model_dir: str, version: \ int = 1): """Save/Export Actor model in TensorFlow.js supported format""" actor_model_save_dir = os.path.join( model_dir, "actor", str(version), \ "model.tfjs" ) tfjs.converters.save_keras_model(self.model, \ actor_model_save_dir) print(f"Actor model saved in TF.js format at:\ {actor_model_save_dir}")
-
作为最终变体,我们将向
Actor
类添加save_onnx
方法,用于以 ONNX 格式保存和导出 Actor 模型:def save_onnx(self, model_dir: str, version: \ int = 1): """Save/Export Actor model in ONNX format""" actor_model_save_path = os.path.join( model_dir, "actor", str(version), \ "model.onnx" ) onnx_model = keras2onnx.convert_keras( self.model, self.model.name) keras2onnx.save_model(onnx_model, \ actor_model_save_path) print(f"Actor model saved in ONNX format at:\ {actor_model_save_path}")
-
这就完成了
Actor
类的保存/导出方法!以类似的方式,让我们向Critic
类添加save
方法,以确保完整性。首先是save
方法,然后是后续步骤中的其他方法:def save(self, model_dir: str, version: int = 1): critic_model_save_dir = os.path.join( model_dir, "critic", str(version), \ "model.savedmodel" ) self.model.save(critic_model_save_dir, \ save_format="tf") print(f"Critic model saved at:\ {critic_model_save_dir}")
-
序列中的下一个方法是
save_tflite
方法,用于以 TFLite 格式保存和导出 Critic 模型:def save_tflite(self, model_dir: str, version: \ int = 1): """Save/Export Critic model in TensorFlow Lite format""" critic_model_save_dir = os.path.join(model_dir,\ "critic", str(version)) model_converter = \ tf.lite.TFLiteConverter.from_keras_model( self.model) # Convert model to TFLite Flatbuffer tflite_model = model_converter.convert() # Save the model to disk/persistent-storage if not os.path.exists(critic_model_save_dir): os.makedirs(critic_model_save_dir) critic_model_file_name = \ os.path.join(critic_model_save_dir, "model.tflite") with open(critic_model_file_name, "wb") as \ model_file: model_file.write(tflite_model) print(f"Critic model saved in TFLite format at:\ {critic_model_file_name}")
-
让我们在
Critic
类中实现save_h5
方法,以保存和导出 Critic 模型为 HDF5 格式:def save_h5(self, model_dir: str, version: int = 1): critic_model_save_dir = os.path.join( model_dir, "critic", str(version), "model.h5" ) self.model.save(critic_model_save_dir, \ save_format="h5") print(f"Critic model saved at:\ {critic_model_save_dir}")
-
接下来,我们将在
Critic
类中添加save_tfjs
方法,用于以 TF.js 格式保存和导出 Critic 模型:def save_tfjs(self, model_dir: str, version: \ int = 1): """Save/Export Critic model in TensorFlow.js supported format""" critic_model_save_dir = os.path.join( model_dir, "critic", str(version), \ "model.tfjs" ) tfjs.converters.save_keras_model(self.model,\ critic_model_save_dir) print(f"Critic model saved TF.js format at:\ {critic_model_save_dir}")
-
最终变体是
save_onnx
方法,它用于以 ONNX 格式保存和导出 Critic 模型:def save_onnx(self, model_dir: str, version: \ int = 1): """Save/Export Critic model in ONNX format""" critic_model_save_path = os.path.join( model_dir, "critic", str(version), \ "model.onnx" ) onnx_model = keras2onnx.convert_keras(self.model, self.model.name) keras2onnx.save_model(onnx_model, \ critic_model_save_path) print(f"Critic model saved in ONNX format at:\ {critic_model_save_path}")
-
这完成了对智能体
Critic
类的保存/导出方法的添加。接下来,我们可以向Agent
类添加相应的save
方法,这些方法将简单地调用Actor
和Critic
对象上的相应save
方法。让我们在接下来的两步中完成实现:def save(self, model_dir: str, version: int = 1): self.actor.save(model_dir, version) self.critic.save(model_dir, version) def save_tflite(self, model_dir: str, version: \ int = 1): # Make sure `toco_from_protos binary` is on # system's PATH to avoid TFLite ConverterError toco_bin_dir = os.path.dirname(sys.executable) if not toco_bin_dir in os.environ["PATH"]: os.environ["PATH"] += os.pathsep + \ toco_bin_dir print(f"Saving Agent model (TFLite) to:\ {model_dir}\n") self.actor.save_tflite(model_dir, version) self.critic.save_tflite(model_dir, version)
-
PPOAgent
类中的其余方法也很简单:def save_h5(self, model_dir: str, version: int = 1): print(f"Saving Agent model (HDF5) to:\ {model_dir}\n") self.actor.save_h5(model_dir, version) self.critic.save_h5(model_dir, version) def save_tfjs(self, model_dir: str, version: \ int = 1): print(f"Saving Agent model (TF.js) to:\ {model_dir}\n") self.actor.save_tfjs(model_dir, version) self.critic.save_tfjs(model_dir, version) def save_onnx(self, model_dir: str, version: \ int = 1): print(f"Saving Agent model (ONNX) to:\ {model_dir}\n") self.actor.save_onnx(model_dir, version) self.critic.save_onnx(model_dir, version)
-
这完成了我们对
Agent
类的实现!我们现在准备运行脚本来构建、训练和导出深度强化学习智能体模型!让我们实现main
函数,并调用我们在之前步骤中实现的所有save
方法:if __name__ == "__main__": env_name = args.env env = gym.make(env_name) agent = PPOAgent(env) agent.train(max_episodes=1) # Model saving model_dir = "trained_models" agent_name = f"PPO_{env_name}" agent_version = 1 agent_model_path = os.path.join(model_dir, \ agent_name) agent.save_onnx(agent_model_path, agent_version) agent.save_h5(agent_model_path, agent_version) agent.save_tfjs(agent_model_path, agent_version) agent.save_tflite(agent_model_path, agent_version)
-
现在是执行我们的脚本的时候了!请从书籍的代码库中拉取最新版本的配方,然后直接运行它!默认情况下,脚本将训练智能体一个回合,保存智能体模型,并将模型以多种格式导出,准备部署。脚本完成后,你将看到导出的模型及其目录结构和内容,类似于下图所示:
图 9.18 – PPO 深度强化学习智能体模型以各种格式导出,准备部署
这就是我们本章的最终配方!接下来,让我们快速回顾一下我们在此配方中涵盖的一些关键内容。
它是如何工作的…
我们首先设置 TensorFlow Keras 的后端,使用 float32
作为浮动值的默认表示。这是因为否则 TensorFlow 会使用默认的 float64
表示,而 TensorFlow Lite 不支持 float64
(出于性能原因),因为它面向嵌入式和移动设备运行。
在这个食谱中,我们使用了一个 PPO 代理,配备了期望图像观测并在离散空间中产生动作的 Actor 和 Critic 网络,专为 RL 环境设计,如 OpenAI 的程序化生成的 procgen 环境。你可以根据需要/应用,替换代理代码为前面章节中的 PPO 实现,这些实现使用不同的状态/观测空间和动作空间。你也可以完全替换代理,使用其他代理算法。
我们讨论了几种保存和导出代理模型的方法,利用 TensorFlow 2.x 生态系统提供的完整工具和库。以下图表总结了我们在本食谱中实现的各种导出选项:
图 9.19 – 本食谱中讨论的各种 RL 代理模型导出选项总结
这就是本食谱、这一章,乃至——更戏剧性地说——整本书的结束!在这本食谱中,我们讨论了如何利用 TensorFlow 2.x 框架和围绕它构建的工具和库生态系统,来构建 RL 的基础模块、环境、算法、代理和应用。我希望你在阅读这本书时有一段激动人心的旅程。
我迫不及待想看到你用本书中讨论的食谱构建/创造的内容。期待在github.com/PacktPublishing/Tensorflow-2-Reinforcement-Learning-Cookbook/discussions
的讨论页面上听到你的反馈。
期待在讨论/问题页面与你取得联系。祝你未来一切顺利!
第十章:你可能喜欢的其他书籍
如果你喜欢本书,可能对 Packt 的其他书籍感兴趣:
Python 强化学习精通
Enes Bilgin
ISBN: 978-1-83864-414-7
-
使用强化学习建模并解决复杂的顺序决策问题
-
培养对最先进的强化学习方法如何运作的深刻理解
-
使用 Python 和 TensorFlow 从零开始编写强化学习算法
-
使用 Ray 的 RLlib 包并行化和扩展你的强化学习实现
-
深入了解各种强化学习主题
-
理解不同强化学习方法之间的权衡
-
探索并解决在实际应用中实现强化学习的挑战
Python 深度强化学习 - 第二版
Sudharsan Ravichandiran
ISBN: 978-1-83921-068-6
-
理解核心强化学习概念,包括方法论、数学和代码
-
训练一个智能体使用 OpenAI Gym 解决 Blackjack、FrozenLake 等问题
-
训练智能体使用深度 Q 网络玩 Ms Pac-Man
-
学习基于策略、基于价值和演员-评论家方法
-
掌握 DDPG、TD3、TRPO、PPO 等背后的数学
-
探索新的领域,如分布式强化学习、元强化学习和反向强化学习
-
使用 Stable Baselines 训练智能体走路并玩 Atari 游戏
留下评论 - 让其他读者知道你的想法
请通过在购买网站上留下评论,与他人分享你对本书的想法。如果你在 Amazon 购买了本书,请在本书的 Amazon 页面上留下真实评论。这对于其他潜在读者非常重要,让他们能够看到并使用你的公正意见来做出购买决策,同时我们可以了解客户对产品的看法,作者也能看到他们与 Packt 合作创作的书籍的反馈。仅需几分钟时间,但对其他潜在客户、我们的作者以及 Packt 来说都非常宝贵。谢谢!