PyTorch-1-x-强化学习秘籍-全-

PyTorch 1.x 强化学习秘籍(全)

原文:zh.annas-archive.org/md5/863e6116b9dfbed5ea6521a90f2b5732

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

强化学习兴起的原因在于它通过学习在环境中采取最优行动来最大化累积奖励的概念,从而革新了自动化。

PyTorch 1.x 强化学习菜谱向您介绍了重要的强化学习概念,以及在 PyTorch 中实现算法的方法。本书的每一章都将引导您了解不同类型的强化学习方法及其在行业中的应用。通过包含真实世界示例的配方,您将发现在动态规划、蒙特卡洛方法、时间差分与 Q-learning、多臂老丨虎丨机、函数逼*、深度 Q 网络和策略梯度等强化学习技术领域提升知识和熟练度是多么有趣而易于跟随的事情。有趣且易于跟随的示例,如 Atari 游戏、21 点、Gridworld 环境、互联网广告、Mountain Car 和 Flappy Bird,将让您在实现目标之前保持兴趣盎然。

通过本书,您将掌握流行的强化学习算法的实现,并学习将强化学习技术应用于解决其他实际问题的最佳实践。

本书适合对象

寻求在强化学习中快速解决不同问题的机器学习工程师、数据科学家和人工智能研究人员会发现本书非常有用。需要有机器学习概念的先验知识,而对 PyTorch 的先前经验将是一个优势。

本书内容概述

第一章,使用 PyTorch 入门强化学习,是本书逐步指南开始的地方,为那些希望开始学习使用 PyTorch 进行强化学习的读者提供了指导。我们将建立工作环境并熟悉使用 Atari 和 CartPole 游戏场景的强化学习环境。本章还将涵盖几种基本的强化学习算法的实现,包括随机搜索、爬山法和策略梯度。最后,读者还将有机会复习 PyTorch 的基础知识,并为即将到来的学习示例和项目做好准备。

第二章,马尔可夫决策过程与动态规划,从创建马尔可夫链和马尔可夫决策过程开始,后者是大多数强化学习算法的核心。然后,我们将介绍两种解决马尔可夫决策过程(MDP)的方法,即值迭代和策略迭代。通过实践策略评估,我们将更加熟悉 MDP 和贝尔曼方程。我们还将逐步演示如何解决有趣的硬币翻转赌博问题。最后,我们将学习如何执行动态规划以扩展学习能力。

第三章,蒙特卡洛方法进行数值估计,专注于蒙特卡洛方法。我们将从使用蒙特卡洛估算 pi 值开始。接着,我们将学习如何使用蒙特卡洛方法预测状态值和状态-动作值。我们将展示如何训练一个代理程序在 21 点中获胜。此外,我们将通过开发各种算法探索在线策略的第一次访问蒙特卡洛控制和离线蒙特卡洛控制。还将涵盖带有 epsilon-greedy 策略和加权重要性采样的蒙特卡洛控制。

第四章,时间差分与 Q 学习,首先建立了 CliffWalking 和 Windy Gridworld 环境场地,这些将在时间差分和 Q 学习中使用。通过我们的逐步指南,读者将探索用于预测的时间差分,并且会通过 Q 学习获得实际控制经验,以及通过 SARSA 实现在线策略控制。我们还将处理一个有趣的项目,出租车问题,并展示如何使用 Q 学习和 SARSA 算法解决它。最后,我们将涵盖 Double Q-learning 算法作为额外的部分。

第五章,解决多臂赌博问题,涵盖了多臂赌博算法,这可能是强化学习中最流行的算法之一。我们将从创建多臂赌博问题开始。我们将看到如何使用四种策略解决多臂赌博问题,包括 epsilon-greedy 策略、softmax 探索、上置信度界算法和 Thompson 采样算法。我们还将处理一个十亿美元的问题,在线广告,展示如何使用多臂赌博算法解决它。最后,我们将开发一个更复杂的算法,上下文赌博算法,并用它来优化显示广告。

第六章,使用函数逼*扩展学习,专注于函数逼*,并将从设置 Mountain Car 环境场地开始。通过我们的逐步指南,我们将讨论为什么使用函数逼*而不是表查找,并且通过 Q 学习和 SARSA 等现有算法融入函数逼*的实际经验。我们还将涵盖一个高级技术,即使用经验重放进行批处理。最后,我们将介绍如何使用本章学到的内容来解决 CartPole 问题。

第七章,《行动中的深度 Q 网络》,涵盖了深度 Q 学习或深度 Q 网络DQN),被认为是最现代的强化学习技术。我们将逐步开发一个 DQN 模型,并了解经验回放和目标网络在实践中使深度 Q 学习发挥作用的重要性。为了帮助读者解决雅达利游戏问题,我们将演示如何将卷积神经网络融入到 DQN 中。我们还将涵盖两种 DQN 变体,分别为双重 DQN 和对战 DQN。我们将介绍如何使用双重 DQN 调优 Q 学习算法。

第八章,《实施策略梯度和策略优化》,专注于策略梯度和优化,并首先实施 REINFORCE 算法。然后,我们将基于 ClifWalking 开发带基准线的 REINFORCE 算法。我们还将实施 actor-critic 算法,并应用它来解决 ClifWalking 问题。为了扩展确定性策略梯度算法,我们从 DQN 中应用技巧,并开发深度确定性策略梯度。作为一个有趣的体验,我们训练一个基于交叉熵方法的代理来玩 CartPole 游戏。最后,我们将谈论如何使用异步 actor-critic 方法和神经网络来扩展策略梯度方法。

第九章,《毕业项目 - 使用 DQN 玩 Flappy Bird》带领我们进行一个毕业项目 - 使用强化学习玩 Flappy Bird。我们将应用本书中学到的知识来构建一个智能机器人。我们将专注于构建一个 DQN,调优模型参数,并部署模型。让我们看看鸟在空中能飞多久。

为了从本书中获得最大收益

寻求强化学习中不同问题的快速解决方案的数据科学家、机器学习工程师和人工智能研究人员会发现这本书很有用。需要有机器学习概念的先前接触,而 PyTorch 的先前经验并非必需,但会是一个优势。

下载示例代码文件

您可以从您在 www.packt.com 的账户中下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问 www.packtpub.com/support 并注册,以直接通过电子邮件接收文件。

您可以按照以下步骤下载代码文件:

  1. www.packt.com 登录或注册。

  2. 选择“支持”选项卡。

  3. 点击“代码下载”。

  4. 在搜索框中输入书名,然后按照屏幕上的说明操作。

下载文件后,请确保您使用最新版本的解压工具解压或提取文件夹:

  • Windows 系统使用 WinRAR/7-Zip

  • Mac 系统使用 Zipeg/iZip/UnRarX

  • Linux 系统使用 7-Zip/PeaZip

The code bundle for the book is also hosted on GitHub at github.com/PacktPublishing/PyTorch-1.x-Reinforcement-Learning-Cookbook. In case there's an update to the code, it will be updated on the existing GitHub repository.

We also have other code bundles from our rich catalog of books and videos available at github.com/PacktPublishing/. Check them out!

Download the color images

We also provide a PDF file that has color images of the screenshots/diagrams used in this book. You can download it here: static.packt-cdn.com/downloads/9781838551964_ColorImages.pdf.

Conventions used

There are a number of text conventions used throughout this book.

CodeInText:指示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 用户名。例如:"所谓的empty并不意味着所有元素都有Null值。"

A block of code is set as follows:

>>> def random_policy():
...     action = torch.multinomial(torch.ones(n_action), 1).item()
...     return action

Any command-line input or output is written as follows:

conda install pytorch torchvision -c pytorch

Bold: 表示新术语、重要词汇或屏幕上看到的词语。例如,菜单或对话框中的单词以这种方式出现在文本中。例如:"这种方法称为随机搜索,因为每次试验中权重都是随机选择的,希望通过大量试验找到最佳权重。"

Warnings or important notes appear like this.

Tips and tricks appear like this.

Sections

In this book, you will find several headings that appear frequently (Getting ready, How to do it..., How it works..., There's more..., and See also).

To give clear instructions on how to complete a recipe, use these sections as follows:

Getting ready

This section tells you what to expect in the recipe and describes how to set up any software or any preliminary settings required for the recipe.

How to do it...

This section contains the steps required to follow the recipe.

How it works...

This section usually consists of a detailed explanation of what happened in the previous section.

There's more...

This section consists of additional information about the recipe in order to make you more knowledgeable about the recipe.

See also

This section provides helpful links to other useful information for the recipe.

Get in touch

Feedback from our readers is always welcome.

General feedback: 如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并发送邮件至customercare@packtpub.com联系我们。

勘误:尽管我们已竭尽全力确保内容准确性,但错误难免会发生。如果您在本书中发现错误,请向我们报告,我们将不胜感激。请访问www.packtpub.com/support/errata,选择您的书籍,点击“勘误提交表格”链接,并填写详细信息。

盗版:如果您在互联网上发现我们作品的任何形式的非法复制,请向我们提供位置地址或网站名称,我们将不胜感激。请通过copyright@packt.com与我们联系,并附上材料链接。

如果您有兴趣成为作者:如果您在某个专题上有专业知识,并且有意写作或为书籍做贡献,请访问authors.packtpub.com

评论

请留下您的评论。在阅读并使用本书后,为何不在您购买它的网站上留下评论呢?潜在的读者可以看到并使用您的客观意见来作出购买决策,我们在 Packt 可以了解您对我们产品的看法,而我们的作者可以看到您对他们书籍的反馈。谢谢!

要了解更多关于 Packt 的信息,请访问packt.com

第一章:开始使用强化学习和 PyTorch

我们开始了实用强化学习和 PyTorch 之旅,使用基本但重要的强化学习算法,包括随机搜索、爬山和策略梯度。我们将从设置工作环境和 OpenAI Gym 开始,通过 Atari 和 CartPole 游乐场熟悉强化学习环境。我们还将逐步演示如何开发算法来解决 CartPole 问题。此外,我们将回顾 PyTorch 的基础知识,并为即将进行的学习示例和项目做准备。

本章包含以下操作:

  • 设置工作环境

  • 安装 OpenAI Gym

  • 模拟 Atari 环境

  • 模拟 CartPole 环境

  • 回顾 PyTorch 的基本原理

  • 实现和评估随机搜索策略

  • 开发爬山算法

  • 开发策略梯度算法

设置工作环境

让我们开始设置工作环境,包括正确版本的 Python 和 Anaconda,以及作为本书主要框架的 PyTorch。

Python 是本书中实现所有强化学习算法和技术的语言。在本书中,我们将使用 Python 3,具体来说是 3.6 或以上版本。如果您仍在使用 Python 2,现在是切换到 Python 3 的最佳时机,因为 Python 2 将在 2020 年后不再受支持。不过过渡非常顺利,不必担心。

Anaconda是一个开源的 Python 发行版(www.anaconda.com/distribution/),用于数据科学和机器学习。我们将使用 Anaconda 的包管理器conda来安装 Python 包,以及pip

PyTorchpytorch.org/),主要由 Facebook AI Research(FAIR)组开发,是基于 Torch(torch.ch/)的流行机器学习库。PyTorch 中的张量取代了 NumPy 的ndarrays,提供了更多的灵活性和与 GPU 的兼容性。由于强大的计算图和简单友好的接口,PyTorch 社区每天都在扩展,越来越多的技术巨头也在采用它。

让我们看看如何正确设置所有这些组件。

如何操作…

我们将从安装 Anaconda 开始。如果您的系统已经运行 Python 3.6 或 3.7 的 Anaconda,则可以跳过此步骤。否则,您可以按照以下操作系统的说明安装,链接如下:

设置完成后,可以随意使用 PyTorch 进行实验。要验证你是否正确设置了 Anaconda 和 Python,请在 Linux/Mac 的终端或 Windows 的命令提示符中输入以下命令(从现在起,我们将简称为终端):

python

它将显示你的 Python Anaconda 环境。你应该看到类似以下截图:

如果未提到 Anaconda 和 Python 3.x,请检查系统路径或 Python 运行路径。

下一步要做的是安装 PyTorch。首先,前往 pytorch.org/get-started/locally/,然后从以下表格中选择你的环境描述:

这里我们以 MacCondaPython 3.7 以及在本地运行(没有 CUDA)作为示例,并在终端中输入生成的命令行:

conda install pytorch torchvision -c pytorch

要确认 PyTorch 是否正确安装,请在 Python 中运行以下代码:

>>> import torch
>>> x = torch.empty(3, 4)
>>> print(x)
tensor([[ 0.0000e+00,  2.0000e+00, -1.2750e+16, -2.0005e+00],
 [ 9.8742e-37,  1.4013e-45, 9.9222e-37,  1.4013e-45],
 [ 9.9220e-37,  1.4013e-45, 9.9225e-37,  2.7551e-40]])

如果显示了一个 3 x 4 的矩阵,则表示 PyTorch 安装正确。

现在我们已成功设置好工作环境。

它的工作原理...

我们刚刚在 PyTorch 中创建了一个大小为 3 x 4 的张量。它是一个空矩阵。所谓的 empty 并不意味着所有元素都是 Null 的值。相反,它们是一堆没有意义的浮点数,被视为占位符。用户需要稍后设置所有的值。这与 NumPy 的空数组非常相似。

还有更多...

有些人可能会质疑安装 Anaconda 和使用 conda 管理包的必要性,因为使用 pip 安装包很容易。事实上,conda 是比 pip 更好的打包工具。我们主要使用 conda 有以下四个理由:

  • 它能很好地处理库依赖关系:使用 conda 安装包会自动下载其所有依赖项。但是,使用 pip 则会导致警告并中止安装。

  • 它能优雅地解决包的冲突:如果安装一个包需要另一个特定版本的包(比如说 2.3 或之后的版本),conda 将自动更新另一个包的版本。

  • 它能轻松创建虚拟环境:虚拟环境是一个自包含的包目录树。不同的应用程序或项目可以使用不同的虚拟环境。所有虚拟环境彼此隔离。建议使用虚拟环境,这样我们为一个应用程序所做的任何操作都不会影响我们的系统环境或任何其他环境。

  • 它也与 pip 兼容:我们仍然可以在 conda 中使用 pip,使用以下命令:

conda install pip

另请参见

如果你对学习使用 conda 感兴趣,请随意查看以下资源:

如果你想更加熟悉 PyTorch,可以查看官方教程中的入门部分,位于pytorch.org/tutorials/#getting-started。我们建议至少完成以下内容:

安装 OpenAI Gym

设置工作环境后,我们现在可以安装 OpenAI Gym。在不使用 OpenAI Gym 的情况下,您无法进行强化学习,该工具为您提供了多种环境,用于开发学习算法。

OpenAI (openai.com/) 是一家致力于构建安全的人工通用智能AGI)并确保其造福于人类的非营利性研究公司。OpenAI Gym 是一个强大且开源的工具包,用于开发和比较强化学习算法。它为多种强化学习仿真和任务提供接口,涵盖从步行到登月,从汽车赛车到玩 Atari 游戏的各种场景。查看gym.openai.com/envs/获取完整的环境列表。我们可以使用诸如 PyTorch、TensorFlow 或 Keras 等任何数值计算库编写代理,与 OpenAI Gym 环境进行交互。

如何做到...

有两种方法可以安装 Gym。第一种是使用pip,如下所示:

pip install gym

对于conda用户,请记住在使用pip安装 Gym 之前,首先在conda中安装pip,使用以下命令:

conda install pip

这是因为截至 2019 年初,Gym 尚未正式在conda中提供。

另一种方法是从源代码构建:

  1. 首先,直接从其 Git 仓库克隆该包:
git clone https://github.com/openai/gym
  1. 转到下载的文件夹,并从那里安装 Gym:
cd gym
pip install -e .

现在你可以开始了。随意尝试使用gym玩耍。

  1. 您还可以通过输入以下代码行来检查可用的gym环境:
>>> from gym import envs
>>> print(envs.registry.all())
dict_values([EnvSpec(Copy-v0), EnvSpec(RepeatCopy-v0), EnvSpec(ReversedAddition-v0), EnvSpec(ReversedAddition3-v0), EnvSpec(DuplicatedInput-v0), EnvSpec(Reverse-v0), EnvSpec(CartPole-v0), EnvSpec(CartPole-v1), EnvSpec(MountainCar-v0), EnvSpec(MountainCarContinuous-v0), EnvSpec(Pendulum-v0), EnvSpec(Acrobot-v1), EnvSpec(LunarLander-v2), EnvSpec(LunarLanderContinuous-v2), EnvSpec(BipedalWalker-v2), EnvSpec(BipedalWalkerHardcore-v2), EnvSpec(CarRacing-v0), EnvSpec(Blackjack-v0)
...
...

如果您正确安装了 Gym,这将为您提供一个环境的长列表。我们将在下一个示例模拟 Atari 环境中尝试其中的一些。

它是如何运行的...

与使用简单的pip方法安装 Gym 相比,第二种方法在您想要添加新环境和修改 Gym 本身时提供更大的灵活性。

还有更多内容...

也许你会想为什么我们需要在 Gym 的环境中测试强化学习算法,因为我们实际工作中的环境可能会大不相同。你会想起强化学习并不对环境做出太多假设,但通过与环境的交互来更多地了解它。此外,在比较不同算法的性能时,我们需要将它们应用于标准化的环境中。Gym 是一个完美的基准测试,涵盖了许多多功能和易于使用的环境。这与我们在监督和无监督学习中经常使用的数据集类似,如 MNIST、Imagenet、MovieLens 和 Thomson Reuters News。

另请参阅

查看官方 Gym 文档,请访问gym.openai.com/docs/

模拟 Atari 环境

要开始使用 Gym,让我们玩一些 Atari 游戏。

Atari 环境(gym.openai.com/envs/#atari)是各种 Atari 2600 视频游戏,如 Alien、AirRaid、Pong 和 Space Race。如果你曾经玩过 Atari 游戏,这个步骤应该很有趣,因为你将玩一个 Atari 游戏,Space Invaders。然而,一个代理将代表你行动。

如何做...

让我们按照以下步骤模拟 Atari 环境:

  1. 第一次运行任何atari环境时,我们需要通过在终端中运行以下命令安装atari依赖项:
pip install gym[atari]

或者,如果你在上一个步骤中使用了第二种方法来安装 gym,你可以运行以下命令代替:

pip install -e '.[atari]'
  1. 安装完 Atari 依赖项后,我们在 Python 中导入gym库:
>>> import gym
  1. 创建一个SpaceInvaders环境的实例:
>>> env = gym.make('SpaceInvaders-v0')
  1. 重置环境:
>>> env.reset()
 array([[[ 0,  0, 0],
         [ 0, 0,  0],
         [ 0, 0,  0],
         ...,
         ...,
         [80, 89, 22],
         [80, 89, 22],
         [80, 89, 22]]], dtype=uint8)

正如你所见,这也会返回环境的初始状态。

  1. 渲染环境:
>>> env.render()
True

你会看到一个小窗口弹出,如下所示:

如你从游戏窗口看到的,飞船从三条生命(红色飞船)开始。

  1. 随机选择一个可能的动作并执行它:
>>> action = env.action_space.sample()
>>> new_state, reward, is_done, info = env.step(action)

step()方法返回在执行动作后发生的事情,包括以下内容:

  • 新状态:新的观察。

  • 奖励:与该动作在该状态下相关联的奖励。

  • 是否完成:指示游戏是否结束的标志。在SpaceInvaders环境中,如果飞船没有更多生命或者所有外星人都消失了,这将为True;否则,它将保持为False

  • 信息:与环境相关的额外信息。这是关于当前剩余生命的数量。这在调试时非常有用。

让我们来看看is_done标志和info

>>> print(is_done)
False
>>> print(info)
{'ale.lives': 3}

现在我们渲染环境:

>>> env.render()
 True

游戏窗口变成了以下样子:

在游戏窗口中你不会注意到太大的差异,因为飞船只是移动了一下。

  1. 现在,让我们创建一个while循环,让代理尽可能执行多个动作:
>>> is_done = False
>>> while not is_done:
...     action = env.action_space.sample()
...     new_state, reward, is_done, info = env.step(action)
...     print(info)
...     env.render()
{'ale.lives': 3}
True
{'ale.lives': 3}
True
……
……
{'ale.lives': 2}
True
{'ale.lives': 2}
True
……
……
{'ale.lives': 1}
True
{'ale.lives': 1}
True

同时,您会看到游戏正在运行,飞船不断移动和射击,外星人也是如此。观看起来也挺有趣的。最后,当游戏结束时,窗口如下所示:

正如您所见,我们在这个游戏中得了 150 分。您可能会得到比这更高或更低的分数,因为代理执行的动作是随机选择的。

我们还确认最后一条信息中没有剩余的生命:

>>> print(info)
{'ale.lives': 0}

工作原理...

使用 Gym,我们可以通过调用make()方法并以环境名称作为参数轻松创建一个环境实例。

正如您可能已经注意到的,代理执行的动作是使用sample()方法随机选择的。

请注意,通常情况下,我们会有一个更复杂的由强化学习算法引导的代理。在这里,我们只是演示了如何模拟一个环境以及代理如何无视结果而采取行动。

多次运行这个程序,看看我们能得到什么:

>>> env.action_space.sample()
0
>>> env.action_space.sample()
3
>>> env.action_space.sample()
0
>>> env.action_space.sample()
4
>>> env.action_space.sample()
2
>>> env.action_space.sample()
1
>>> env.action_space.sample()
4
>>> env.action_space.sample()
5
>>> env.action_space.sample()
1
>>> env.action_space.sample()
0

总共有六个可能的动作。我们还可以通过运行以下命令来查看:

>>> env.action_space
Discrete(6)

从 0 到 5 的动作分别代表无操作、开火、向上、向右、向左和向下,这些是游戏中飞船可以执行的所有移动。

step()方法将让代理执行指定为其参数的动作。render()方法将根据环境的最新观察更新显示窗口。

环境的观察值new_state由一个 210 x 160 x 3 的矩阵表示,如下所示:

>>> print(new_state.shape)
(210, 160, 3)

这意味着显示屏的每一帧都是一个大小为 210 x 160 的 RGB 图像。

这还不是全部...

您可能会想为什么我们需要安装 Atari 的依赖项。事实上,还有一些环境并没有随gym一起安装,比如 Box2d、经典控制、MuJoCo 和机器人学。

Box2d环境为例;在首次运行环境之前,我们需要安装Box2d依赖项。再次,以下是两种安装方法:

pip install gym[box2d]
pip install -e '.[box2d]'

之后,我们可以尝试使用LunarLander环境,如下所示:

>>> env = gym.make('LunarLander-v2')
>>> env.reset()
array([-5.0468446e-04,  1.4135642e+00, -5.1140346e-02,  1.1751971e-01,
 5.9164839e-04,  1.1584054e-02, 0.0000000e+00,  0.0000000e+00],
 dtype=float32)
>>> env.render()

一个游戏窗口将弹出:

另请参阅

如果您想模拟一个环境但不确定在make()方法中应该使用的名称,您可以在github.com/openai/gym/wiki/Table-of-environments的环境表中找到它。除了调用环境时使用的名称外,表还显示了观察矩阵的大小和可能动作的数量。玩转这些环境时尽情享乐吧。

模拟 CartPole 环境

在这个教程中,我们将模拟一个额外的环境,以便更加熟悉 Gym。CartPole 环境是强化学习研究中的经典环境之一。

CartPole 是传统的强化学习任务,其中一个杆子直立放在购物车顶部。代理人在每个时间步长内将购物车向左或向右移动 1 单位。目标是*衡杆子,防止其倒下。如果杆子与垂直方向超过 12 度,或者购物车离原点移动超过 2.4 单位,则认为杆子已倒下。当发生以下任何一种情况时,一个 episode 终止:

  • 杆子倒下了

  • 时间步数达到了 200

怎么做…

让我们按照以下步骤模拟 CartPole 环境:

  1. 要运行 CartPole 环境,让我们首先在github.com/openai/gym/wiki/Table-of-environments的环境表中搜索其名称。我们得到了 'CartPole-v0',并且还了解到观测空间由一个四维数组表示,有两种可能的动作(这是有道理的)。

  2. 我们导入 Gym 库,并创建一个 CartPole 环境的实例:

 >>> import gym >>> env = gym.make('CartPole-v0')
  1. 重置环境:
 >>> env.reset() array([-0.00153354,  0.01961605, -0.03912845, -0.01850426])

如您所见,这也返回了由四个浮点数组成的初始状态。

  1. 渲染环境:
 >>> env.render() True

您将看到一个小窗口弹出,如下所示:

  1. 现在,让我们制作一个 while 循环,并让代理尽可能执行多个随机动作:
 >>> is_done = False >>> while not is_done:
 ...     action = env.action_space.sample()
 ...     new_state, reward, is_done, info = env.step(action)
 ...     print(new_state)
 ...     env.render()
 ...
 [-0.00114122 -0.17492355 -0.03949854  0.26158095]
 True
 [-0.00463969 -0.36946006 -0.03426692  0.54154857]
 True
 ……
 ……
 [-0.11973207 -0.41075106  0.19355244 1.11780626]
 True
 [-0.12794709 -0.21862176  0.21590856 0.89154351]
 True

同时,您将看到购物车和杆子在移动。最后,您将看到它们停止。窗口看起来像这样:

由于随机选择左或右动作,每个 episode 只持续几个步骤。我们能记录整个过程以便之后回放吗?我们可以在 Gym 中仅用两行代码实现,如 Step 7 所示。如果您使用的是 Mac 或 Linux 系统,则需要先完成 Step 6;否则,您可以直接跳转到 Step 7

  1. 要记录视频,我们需要安装 ffmpeg 包。对于 Mac,可以通过以下命令安装:
brew install ffmpeg

对于 Linux,以下命令应该可以完成:

sudo apt-get install ffmpeg
  1. 创建 CartPole 实例后,添加以下两行:
>>> video_dir = './cartpole_video/' >>> env = gym.wrappers.Monitor(env, video_dir)

这将记录窗口中显示的内容,并存储在指定的目录中。

现在重新运行从 Step 3Step 5 的代码。在一个 episode 结束后,我们可以看到在 video_dir 文件夹中创建了一个 .mp4 文件。视频非常短暂,可能只有 1 秒左右。

工作原理…

在这个示例中,我们每一步都打印出状态数组。但是数组中的每个浮点数代表什么?我们可以在 Gym 的 GitHub wiki 页面上找到有关 CartPole 的更多信息:github.com/openai/gym/wiki/CartPole-v0。原来这四个浮点数分别表示以下内容:

  • 购物车位置:其范围从 -2.4 到 2.4,超出此范围的任何位置都将触发 episode 终止。

  • 购物车速度。

  • 杆角度:任何小于 -0.209(-12 度)或大于 0.209(12 度)的值将触发 episode 终止。

  • 杆末端的极点速度。

在动作方面,要么是 0,要么是 1,分别对应将车推向左侧和右侧。

在这个环境中,奖励是在每个时间步之前 +1。我们还可以通过打印每一步的奖励来验证这一点。而总奖励就是时间步数。

更多内容…

到目前为止,我们只运行了一个 episode。为了评估代理的表现,我们可以模拟许多 episode,然后对每个 episode 的总奖励取*均值。*均总奖励将告诉我们采取随机行动的代理的表现如何。

让我们设置 10,000 个 episodes:

 >>> n_episode = 10000

在每个 episode 中,我们通过累积每一步的奖励来计算总奖励:

 >>> total_rewards = [] >>> for episode in range(n_episode):
 ...     state = env.reset()
 ...     total_reward = 0
 ...     is_done = False
 ...     while not is_done:
 ...         action = env.action_space.sample()
 ...         state, reward, is_done, _ = env.step(action)
 ...         total_reward += reward
 ...     total_rewards.append(total_reward)

最后,我们计算*均总奖励:

 >>> print('Average total reward over {} episodes: {}'.format( n_episode, sum(total_rewards) / n_episode))
 Average total reward over 10000 episodes: 22.2473

*均而言,随机采取一个动作可以得到 22.25 分。

我们都知道,随机采取行动并不够复杂,我们将在接下来的示例中实施一个高级策略。但是在下一个示例中,让我们休息一下,回顾一下 PyTorch 的基础知识。

回顾 PyTorch 的基础知识

正如我们之前提到的,PyTorch 是本书中用来实现强化学习算法的数值计算库。

PyTorch 是由 Facebook 开发的时髦科学计算和机器学习(包括深度学习)库。张量是 PyTorch 的核心数据结构,类似于 NumPy 的ndarrays。在科学计算中,PyTorch 和 NumPy 是可以比较的。然而,在数组操作和遍历中,PyTorch 比 NumPy 更快。这主要是因为 PyTorch 中的数组元素访问速度更快。因此,越来越多的人认为 PyTorch 将取代 NumPy。

如何做到…

让我们快速回顾一下 PyTorch 的基本编程,以便更加熟悉它:

  1. 在之前的一个示例中,我们创建了一个未初始化的矩阵。那么随机初始化一个矩阵怎么样呢?请看以下命令:
 >>> import torch >>> x = torch.rand(3, 4)
 >>> print(x)
 tensor([[0.8052, 0.3370, 0.7676, 0.2442],
        [0.7073, 0.4468, 0.1277, 0.6842],
        [0.6688, 0.2107, 0.0527, 0.4391]])

在区间 (0, 1) 内生成随机浮点数。

  1. 我们可以指定返回张量的所需数据类型。例如,返回双精度类型(float64)的张量如下所示:
 >>> x = torch.rand(3, 4, dtype=torch.double) >>> print(x)
 tensor([[0.6848, 0.3155, 0.8413, 0.5387],
        [0.9517, 0.1657, 0.6056, 0.5794],
        [0.0351, 0.3801, 0.7837, 0.4883]], dtype=torch.float64)

默认情况下,返回的数据类型是float

  1. 接下来,让我们创建一个全零矩阵和一个全一矩阵:
 >>> x = torch.zeros(3, 4) >>> print(x)
    tensor([[0., 0., 0., 0.],
           [0., 0., 0., 0.],
           [0., 0., 0., 0.]])
    >>> x = torch.ones(3, 4)
    >>> print(x)
    tensor([[1., 1., 1., 1.],
           [1., 1., 1., 1.],
           [1., 1., 1., 1.]])
  1. 要获取张量的大小,使用以下代码:
 >>> print(x.size()) torch.Size([3, 4])

torch.Size 实际上是一个元组。

  1. 要重新塑造张量,我们可以使用view()方法:
 >>> x_reshaped = x.view(2, 6) >>> print(x_reshaped)
 tensor([[1., 1., 1., 1., 1., 1.],
         [1., 1., 1., 1., 1., 1.]])
  1. 我们可以直接从数据创建张量,包括单个值、列表和嵌套列表:
 >>> x1 = torch.tensor(3) >>> print(x1)
 tensor(3)
 >>> x2 = torch.tensor([14.2, 3, 4])
 >>> print(x2)
 tensor([14.2000,  3.0000, 4.0000])
 >>> x3 = torch.tensor([[3, 4, 6], [2, 1.0, 5]])
 >>> print(x3)
 tensor([[3., 4., 6.],
         [2., 1., 5.]])
  1. 要访问多个元素的张量中的元素,我们可以类似于 NumPy 使用索引:
 >>> print(x2[1]) tensor(3.)
 >>> print(x3[1, 0])
 tensor(2.)
 >>> print(x3[:, 1])
 tensor([4., 1.])
 >>> print(x3[:, 1:])
 tensor([[4., 6.],
         [1., 5.]])

与单元素张量一样,我们使用item()方法:

 >>> print(x1.item()) 3
  1. 张量和 NumPy 数组可以相互转换。使用numpy()方法将张量转换为 NumPy 数组:
 >>> x3.numpy() array([[3., 4., 6.],
        [2., 1., 5.]], dtype=float32)

使用from_numpy()将 NumPy 数组转换为张量:

>>> import numpy as np >>> x_np = np.ones(3)
>>> x_torch = torch.from_numpy(x_np)
>>> print(x_torch)
tensor([1., 1., 1.], dtype=torch.float64)

请注意,如果输入的 NumPy 数组是浮点数据类型,则输出张量将是双类型。偶尔可能需要类型转换。

看看以下示例,其中将双类型的张量转换为float

 >>> print(x_torch.float())
 tensor([1., 1., 1.])
  1. PyTorch 中的操作与 NumPy 类似。以加法为例,我们可以简单地执行以下操作:
>>> x4 = torch.tensor([[1, 0, 0], [0, 1.0, 0]]) >>> print(x3 + x4)
tensor([[4., 4., 6.],
         [2., 2., 5.]])

或者我们可以使用add()方法如下所示:

 >>> print(torch.add(x3, x4)) tensor([[4., 4., 6.],
         [2., 2., 5.]])
  1. PyTorch 支持原地操作,这些操作会改变张量对象。例如,让我们运行这个命令:
 >>> x3.add_(x4) tensor([[4., 4., 6.],
         [2., 2., 5.]])

你会看到x3被更改为原始的x3加上x4的结果:

 >>> print(x3) tensor([[4., 4., 6.],
         [2., 2., 5.]])

还有更多...

任何带有_的方法表示它是一个原地操作,它更新张量并返回结果值。

另请参阅

欲查看 PyTorch 中的所有张量操作,请访问官方文档pytorch.org/docs/stable/torch.html。这是在 PyTorch 编程问题上遇到困难时搜索信息的最佳位置。

实施和评估随机搜索策略

在使用 PyTorch 编程进行一些实践后,从这个示例开始,我们将致力于比纯粹的随机动作更复杂的策略来解决 CartPole 问题。我们从这个配方开始使用随机搜索策略。

一种简单但有效的方法是将观测映射到表示两个动作的两个数字的向量中。将选择具有较高值的动作。线性映射由一个大小为 4 x 2 的权重矩阵表示,因为在这种情况下,观测是 4 维的。在每个 episode 中,权重是随机生成的,并且用于计算该 episode 中每一步的动作。然后计算总奖励。这个过程重复多个 episode,并且最终能够提供最高总奖励的权重将成为学习策略。这种方法被称为随机搜索,因为在每个试验中权重都是随机选择的,希望通过大量的试验找到最佳权重。

如何实现...

让我们继续使用 PyTorch 实现一个随机搜索算法:

  1. 导入 Gym 和 PyTorch 包,并创建一个环境实例:
>>> import gym >>> import torch
>>> env = gym.make('CartPole-v0')
  1. 获取观测空间和动作空间的维度:
>>> n_state = env.observation_space.shape[0] >>> n_state
 4
>>> n_action = env.action_space.n
>>> n_action
 2

当我们为权重矩阵定义张量时,将使用这些内容,该矩阵的大小为 4 x 2。

  1. 定义一个函数,模拟给定输入权重的一个 episode,并返回总奖励:
 >>> def run_episode(env, weight): ...     state = env.reset()
 ...     total_reward = 0
 ...     is_done = False
 ...     while not is_done:
 ...         state = torch.from_numpy(state).float()
 ...         action = torch.argmax(torch.matmul(state, weight))
 ...         state, reward, is_done, _ = env.step(action.item())
 ...         total_reward += reward
 ...     return total_reward

在这里,我们将状态数组转换为浮点类型的张量,因为我们需要计算状态和权重张量的乘积torch.matmul(state, weight)以进行线性映射。使用torch.argmax()操作选择具有更高值的动作。不要忘记使用.item()获取结果动作张量的值,因为它是一个单元素张量。

  1. 指定 episode 的数量:
>>> n_episode = 1000
  1. 我们需要实时跟踪最佳总奖励,以及相应的权重。因此,我们指定它们的起始值:
>>> best_total_reward = 0 >>> best_weight = None

我们还将记录每个 episode 的总奖励:

>>> total_rewards = []
  1. 现在,我们可以运行 n_episode。对于每个 episode,我们执行以下操作:
  • 随机选择权重

  • 让代理根据线性映射采取行动

  • 一个 episode 结束并返回总奖励

  • 根据需要更新最佳总奖励和最佳权重

  • 同时,保留总奖励的记录

将其放入代码中如下:

 >>> for episode in range(n_episode): ...     weight = torch.rand(n_state, n_action)
 ...     total_reward = run_episode(env, weight)
 ...     print('Episode {}: {}'.format(episode+1, total_reward))
 ...     if total_reward > best_total_reward:
 ...         best_weight = weight
 ...         best_total_reward =  total_reward
 ...     total_rewards.append(total_reward)
 ...
 Episode 1: 10.0
 Episode 2: 73.0
 Episode 3: 86.0
 Episode 4: 10.0
 Episode 5: 11.0
 ……
 ……
 Episode 996: 200.0
 Episode 997: 11.0
 Episode 998: 200.0
 Episode 999: 200.0
 Episode 1000: 9.0

通过 1,000 次随机搜索,我们已经得到了最佳策略。最佳策略由 best_weight 参数化。

  1. 在我们在测试 episode 上测试最佳策略之前,我们可以计算通过随机线性映射获得的*均总奖励:
 >>> print('Average total reward over {} episode: {}'.format( n_episode, sum(total_rewards) / n_episode))
 Average total reward over 1000 episode: 47.197

这比我们从随机动作策略(22.25)获得的要多两倍。

  1. 现在,让我们看看学习到的策略在 100 个新 episode 上的表现:
 >>> n_episode_eval = 100 >>> total_rewards_eval = []
 >>> for episode in range(n_episode_eval):
 ...     total_reward = run_episode(env, best_weight)
 ...     print('Episode {}: {}'.format(episode+1, total_reward))
 ...     total_rewards_eval.append(total_reward)
 ...
 Episode 1: 200.0
 Episode 2: 200.0
 Episode 3: 200.0
 Episode 4: 200.0
 Episode 5: 200.0
 ……
 ……
 Episode 96: 200.0
 Episode 97: 188.0
 Episode 98: 200.0
 Episode 99: 200.0
 Episode 100: 200.0
 >>> print('Average total reward over {} episode: {}'.format(
           n_episode, sum(total_rewards_eval) / n_episode_eval))
 Average total reward over 1000 episode: 196.72

令人惊讶的是,在测试 episode 中,学习到的策略的*均奖励接*最大的 200 步。请注意,这个值可能会有很大的变化,从 160 到 200 不等。

工作原理如下...

随机搜索算法之所以如此有效,主要是因为我们的 CartPole 环境简单。它的观测状态仅由四个变量组成。你可能还记得,在阿塔利 Space Invaders 游戏中,观测超过 100,000(即 210 * 160 * 3)。CartPole 中动作状态的维度是 Space Invaders 的三分之一。总的来说,简单的算法对简单的问题效果很好。在我们的情况下,我们只需从随机池中搜索最佳的从观测到动作的线性映射。

我们还注意到的另一件有趣的事情是,在我们选择和部署最佳策略(最佳线性映射)之前,随机搜索也优于随机动作。这是因为随机线性映射确实考虑了观测值。随着从环境中获得的更多信息,随机搜索策略中做出的决策比完全随机的决策更为智能。

还有更多内容...

我们还可以绘制训练阶段每个 episode 的总奖励:

>>> import matplotlib.pyplot as plt >>> plt.plot(total_rewards)
>>> plt.xlabel('Episode')
>>> plt.ylabel('Reward')
>>> plt.show()

这将生成以下图表:

如果你尚未安装 matplotlib,则可以通过以下命令安装:

conda install matplotlib

我们可以看到每个 episode 的奖励相当随机,并且在逐个 episode 过程中没有改进的趋势。这基本上是我们预期的。

在奖励与 episode 的绘图中,我们可以看到有些 episode 的奖励达到了 200。一旦出现这种情况,我们可以结束训练阶段,因为没有改进的余地了。经过这一变化,我们现在的训练阶段如下:

 >>> n_episode = 1000 >>> best_total_reward = 0
 >>> best_weight = None
 >>> total_rewards = []
 >>> for episode in range(n_episode):
 ...     weight = torch.rand(n_state, n_action)
 ...     total_reward = run_episode(env, weight)
 ...     print('Episode {}: {}'.format(episode+1, total_reward))
 ...     if total_reward > best_total_reward:
 ...         best_weight = weight
 ...         best_total_reward = total_reward
 ...     total_rewards.append(total_reward)
 ...     if best_total_reward == 200:
 ...         break
 Episode 1: 9.0
 Episode 2: 8.0
 Episode 3: 10.0
 Episode 4: 10.0
 Episode 5: 10.0
 Episode 6: 9.0
 Episode 7: 17.0
 Episode 8: 10.0
 Episode 9: 43.0
 Episode 10: 10.0
 Episode 11: 10.0
 Episode 12: 106.0
 Episode 13: 8.0
 Episode 14: 32.0
 Episode 15: 98.0
 Episode 16: 10.0
 Episode 17: 200.0

在第 17 个回合找到了达到最大奖励的策略。再次提醒,由于每个回合的权重是随机生成的,这可能会有很大的变化。为了计算所需的训练回合的期望,我们可以重复前述的训练过程 1,000 次,并取训练回合的*均值:

 >>> n_training = 1000 >>> n_episode_training = []
 >>> for _ in range(n_training):
 ...     for episode in range(n_episode):
 ...         weight = torch.rand(n_state, n_action)
 ...         total_reward = run_episode(env, weight)
 ...         if total_reward == 200:
 ...             n_episode_training.append(episode+1)
 ...             break
 >>> print('Expectation of training episodes needed: ',
            sum(n_episode_training) / n_training)
 Expectation of training episodes needed:  13.442

*均来看,我们预计需要大约 13 个回合来找到最佳策略。

开发爬坡算法

正如我们在随机搜索策略中看到的,每个回合都是独立的。事实上,随机搜索中的所有回合可以并行运行,并最终选择达到最佳性能的权重。我们还通过奖励与回合的图表验证了这一点,在那里没有上升趋势。在本篇中,我们将开发一种不同的算法,即爬坡算法,以将一个回合中获得的知识转移到下一个回合中。

在爬坡算法中,我们同样从一个随机选择的权重开始。但是在这里,对于每个回合,我们会给权重添加一些噪声。如果总奖励有所改善,我们就用新的权重更新它;否则,我们保留旧的权重。在这种方法中,权重随着回合的进行逐渐改进,而不是在每个回合中跳动。

如何做...

让我们继续使用 PyTorch 实现爬坡算法:

  1. 如前所述,导入必要的包,创建环境实例,并获取观测空间和动作空间的维度:
>>> import gym >>> import torch
>>> env = gym.make('CartPole-v0')
>>> n_state = env.observation_space.shape[0]
>>> n_action = env.action_space.n
  1. 我们将重复使用在前一篇中定义的 run_episode 函数,因此在此不再赘述。同样,给定输入的权重,它模拟一个回合并返回总奖励。

  2. 现在,让我们先做 1,000 个回合:

>>> n_episode = 1000
  1. 我们需要实时跟踪最佳总奖励,以及相应的权重。因此,让我们指定它们的起始值:
>>> best_total_reward = 0 >>> best_weight = torch.rand(n_state, n_action)

我们还将记录每个回合的总奖励:

>>> total_rewards = []
  1. 正如我们之前提到的,我们将在每个回合为权重添加一些噪声。事实上,我们将为噪声应用一个比例,以防止噪声过多影响权重。在这里,我们将选择 0.01 作为噪声比例:
>>> noise_scale = 0.01
  1. 现在,我们可以运行 n_episode 函数。在我们随机选择了初始权重之后,每个回合,我们都会执行以下操作:
  • 向权重添加随机噪声

  • 让代理根据线性映射采取行动

  • 一个回合终止并返回总奖励

  • 如果当前奖励大于迄今为止获得的最佳奖励,则更新最佳奖励和权重

  • 否则,最佳奖励和权重保持不变

  • 同时,记下总奖励

将其转换为代码如下:

 >>> for episode in range(n_episode): ...     weight = best_weight +
                     noise_scale * torch.rand(n_state, n_action)
 ...     total_reward = run_episode(env, weight)
 ...     if total_reward >= best_total_reward:
 ...         best_total_reward = total_reward
 ...         best_weight = weight
 ...     total_rewards.append(total_reward)
 ...     print('Episode {}: {}'.format(episode + 1, total_reward))
 ...
 Episode 1: 56.0
 Episode 2: 52.0
 Episode 3: 85.0
 Episode 4: 106.0
 Episode 5: 41.0
 ……
 ……
 Episode 996: 39.0
 Episode 997: 51.0
 Episode 998: 49.0
 Episode 999: 54.0
 Episode 1000: 41.0

我们还计算了线性映射的爬坡版本所达到的*均总奖励:

 >>> print('Average total reward over {} episode: {}'.format( n_episode, sum(total_rewards) / n_episode))
 Average total reward over 1000 episode: 50.024
  1. 为了评估使用爬坡算法的训练,我们多次重复训练过程(运行从第四步第六步的代码多次)。我们观察到*均总奖励波动很大。以下是我们运行 10 次时得到的结果:
Average total reward over 1000 episode: 9.261   
Average total reward over 1000 episode: 88.565
Average total reward over 1000 episode: 51.796
Average total reward over 1000 episode: 9.41
Average total reward over 1000 episode: 109.758
Average total reward over 1000 episode: 55.787
Average total reward over 1000 episode: 189.251
Average total reward over 1000 episode: 177.624
Average total reward over 1000 episode: 9.146
Average total reward over 1000 episode: 102.311

什么会导致这样的差异?事实证明,如果初始权重不好,以小比例添加噪声将对改善性能影响甚微。这将导致收敛不良。另一方面,如果初始权重良好,以大比例添加噪声可能会使权重远离最优权重并危及性能。我们如何使爬坡模型的训练更稳定可靠?实际上,我们可以使噪声比例适应性地根据性能调整,就像梯度下降中的自适应学习率一样。更多详情请参见第八步

  1. 为了使噪声适应性,我们采取以下措施:
  • 指定一个起始噪声比例。

  • 如果一集的表现提高,减少噪声比例。在我们的情况下,我们取比例的一半,但将0.0001设为下限。

  • 如果一集的表现下降,增加噪声比例。在我们的情况下,我们将比例加倍,但将2设为上限。

将其编写成代码:

 >>> noise_scale = 0.01 >>> best_total_reward = 0
 >>> total_rewards = []
 >>> for episode in range(n_episode):
 ...     weight = best_weight +
                       noise_scale * torch.rand(n_state, n_action)
 ...     total_reward = run_episode(env, weight)
 ...     if total_reward >= best_total_reward:
 ...         best_total_reward = total_reward
 ...         best_weight = weight
 ...         noise_scale = max(noise_scale / 2, 1e-4)
 ...     else:
 ...         noise_scale = min(noise_scale * 2, 2)
 ...     print('Episode {}: {}'.format(episode + 1, total_reward))
 ...     total_rewards.append(total_reward)
 ...
 Episode 1: 9.0
 Episode 2: 9.0
 Episode 3: 9.0
 Episode 4: 10.0
 Episode 5: 10.0
 ……
 ……
 Episode 996: 200.0
 Episode 997: 200.0
 Episode 998: 200.0
 Episode 999: 200.0
 Episode 1000: 200.0

奖励随着集数的增加而增加。在前 100 集内达到最高值 200 并保持不变。*均总奖励看起来也很有前景:

 >>> print('Average total reward over {} episode: {}'.format( n_episode, sum(total_rewards) / n_episode))
 Average total reward over 1000 episode: 186.11

我们还绘制了每一集的总奖励如下:

>>> import matplotlib.pyplot as plt >>> plt.plot(total_rewards)
>>> plt.xlabel('Episode')
>>> plt.ylabel('Reward')
>>> plt.show()

在结果图中,我们可以看到一个明显的上升趋势,在达到最大值后趋于*稳:

随时运行新的训练过程几次。与使用固定噪声比例进行学习相比,结果非常稳定。

  1. 现在,让我们看看学习策略在 100 个新集数上的表现:
 >>> n_episode_eval = 100 >>> total_rewards_eval = []
 >>> for episode in range(n_episode_eval):
 ...     total_reward = run_episode(env, best_weight)
 ...     print('Episode {}: {}'.format(episode+1, total_reward))
 ...     total_rewards_eval.append(total_reward)
 ...
 Episode 1: 200.0
 Episode 2: 200.0
 Episode 3: 200.0
 Episode 4: 200.0
 Episode 5: 200.0
 ……
 ……
 Episode 96: 200.0
 Episode 97: 200.0
 Episode 98: 200.0
 Episode 99: 200.0
 Episode 100: 200.0 

让我们来看看*均表现:

>>> print('Average total reward over {} episode: {}'.format(n_episode, sum(total_rewards) / n_episode)) Average total reward over 1000 episode: 199.94

测试集合的*均奖励接*我们通过学习策略获得的最高值 200。你可以多次重新运行评估。结果非常一致。

运行原理如下...

我们通过简单地在每集中添加自适应噪声,使用爬坡算法能够实现比随机搜索更好的性能。我们可以将其视为一种没有目标变量的特殊梯度下降。额外的噪声就是梯度,尽管是以随机的方式。噪声比例是学习率,并且根据上一集的奖励进行自适应。在爬坡中,目标变量成为达到最高奖励。总之,爬坡算法中的智能体不是将每集孤立开来,而是利用从每集中学到的知识,并在下一集中执行更可靠的操作。正如其名称所示,奖励通过集数向上移动,权重逐渐朝向最优值。

还有更多内容...

我们可以观察到,在前 100 个回合内奖励可以达到最大值。当奖励达到 200 时,我们是否可以停止训练,就像我们在随机搜索策略中所做的那样?这可能不是一个好主意。记住,代理在爬坡时在持续改进。即使它找到了生成最大奖励的权重,它仍然可以在这个权重周围搜索最优点。在这里,我们将最优策略定义为能够解决 CartPole 问题的策略。根据以下维基页面,github.com/openai/gym/wiki/CartPole-v0,"解决"意味着连续 100 个回合的*均奖励不低于 195。

我们相应地完善了停止标准:

 >>> noise_scale = 0.01 >>> best_total_reward = 0
 >>> total_rewards = []
 >>> for episode in range(n_episode):
 ...     weight = best_weight + noise_scale * torch.rand(n_state, n_action)
 ...     total_reward = run_episode(env, weight)
 ...     if total_reward >= best_total_reward:
 ...         best_total_reward = total_reward
 ...         best_weight = weight
 ...         noise_scale = max(noise_scale / 2, 1e-4)
 ...     else:
 ...         noise_scale = min(noise_scale * 2, 2)
 ...     print('Episode {}: {}'.format(episode + 1, total_reward))
 ...     total_rewards.append(total_reward)
 ...     if episode >= 99 and sum(total_rewards[-100:]) >= 19500:
 ...         break
 ...
 Episode 1: 9.0
 Episode 2: 9.0
 Episode 3: 10.0
 Episode 4: 10.0
 Episode 5: 9.0
 ……
 ……
 Episode 133: 200.0
 Episode 134: 200.0
 Episode 135: 200.0
 Episode 136: 200.0
 Episode 137: 200.0

在第 137 回合,问题被认为已解决。

另见

如果您有兴趣了解更多关于爬坡算法的信息,以下资源是有用的:

开发策略梯度算法

第一章的最后一个配方是使用策略梯度算法解决 CartPole 环境的问题。对于这个简单的问题,这可能比我们需要的更复杂,随机搜索和爬山算法已经足够了。然而,这是一个很棒的学习算法,我们将在本书后面更复杂的环境中使用它。

在策略梯度算法中,模型权重在每个回合结束时朝着梯度的方向移动。我们将在下一节中解释梯度的计算。此外,在每个步骤中,它根据使用状态和权重计算的概率随机采样一个动作。与随机搜索和爬坡算法(通过执行获得更高分数的动作)相反,策略从确定性切换到随机

如何做...

现在,是时候用 PyTorch 实现策略梯度算法了:

  1. 如前所述,导入必要的包,创建环境实例,并获取观察和动作空间的维度:
>>> import gym >>> import torch
>>> env = gym.make('CartPole-v0')
>>> n_state = env.observation_space.shape[0]
>>> n_action = env.action_space.n
  1. 我们定义了run_episode函数,该函数模拟了给定输入权重的一个回合,并返回总奖励和计算的梯度。具体来说,它在每个步骤中执行以下任务:
  • 计算基于当前状态和输入权重的两个动作的概率 probs

  • 根据得出的概率样本一个动作 action

  • 计算softmax函数的导数 d_softmax,其中概率作为输入

  • 将得出的导数 d_softmax 除以概率 probs,得到与策略相关的对数项的导数 d_log

  • 应用链式法则计算权重grad的梯度

  • 记录结果梯度,grad

  • 执行动作,累积奖励,并更新状态

将所有这些放入代码中,我们得到以下内容:

 >>> def run_episode(env, weight): ...     state = env.reset()
 ...     grads = []
 ...     total_reward = 0
 ...     is_done = False
 ...     while not is_done:
 ...         state = torch.from_numpy(state).float()
 ...         z = torch.matmul(state, weight)
 ...         probs = torch.nn.Softmax()(z)
 ...         action = int(torch.bernoulli(probs[1]).item())
 ...         d_softmax = torch.diag(probs) -
                             probs.view(-1, 1) * probs
 ...         d_log = d_softmax[action] / probs[action]
 ...         grad = state.view(-1, 1) * d_log
 ...         grads.append(grad)
 ...         state, reward, is_done, _ = env.step(action)
 ...         total_reward += reward
 ...         if is_done:
 ...             break
 ...     return total_reward, grads

当一集结束后,它返回本集获得的总奖励和个别步骤计算的梯度。这两个输出将用于使用随机梯度上升法更新权重。

  1. 暂时让它运行 1,000 集:
>>> n_episode = 1000

这意味着我们将运行run_episoden_episode次。

  1. 初始化权重:
>>> weight = torch.rand(n_state, n_action)

我们还会记录每一集的总奖励:

>>> total_rewards = []
  1. 每集结束时,我们需要使用计算出的梯度更新权重。对于每一集的每一步,权重根据在剩余步骤中计算的学习率 * 梯度 * 总奖励*的策略梯度移动。在这里,我们选择0.001作为学习率:
>>> learning_rate = 0.001

现在,我们可以运行n_episode集:

 >>> for episode in range(n_episode): ...     total_reward, gradients = run_episode(env, weight)
 ...     print('Episode {}: {}'.format(episode + 1, total_reward))
 ...     for i, gradient in enumerate(gradients):
 ...         weight += learning_rate * gradient * (total_reward - i)
 ...     total_rewards.append(total_reward)
 ……
 ……
 Episode 101: 200.0
 Episode 102: 200.0
 Episode 103: 200.0
 Episode 104: 190.0
 Episode 105: 133.0
 ……
 ……
 Episode 996: 200.0
 Episode 997: 200.0
 Episode 998: 200.0
 Episode 999: 200.0
 Episode 1000: 200.0
  1. 现在,我们计算策略梯度算法达到的*均总奖励:
 >>> print('Average total reward over {} episode: {}'.format( n_episode, sum(total_rewards) / n_episode))
 Average total reward over 1000 episode: 179.728
  1. 我们还会绘制每集的总奖励如下:
 >>> import matplotlib.pyplot as plt >>> plt.plot(total_rewards)
 >>> plt.xlabel('Episode')
 >>> plt.ylabel('Reward')
 >>> plt.show()

在生成的图表中,我们可以看到在保持最大值之前有一个明显的上升趋势:

我们还可以看到,即使在收敛后,奖励仍然会波动。这是因为策略梯度算法是一种随机策略。

  1. 现在,让我们看看学习策略在 100 个新集上的表现:
 >>> n_episode_eval = 100 >>> total_rewards_eval = []
 >>> for episode in range(n_episode_eval):
 ...     total_reward, _ = run_episode(env, weight)
 ...     print('Episode {}: {}'.format(episode+1, total_reward))
 ...     total_rewards_eval.append(total_reward)
 ...
 Episode 1: 200.0
 Episode 2: 200.0
 Episode 3: 200.0
 Episode 4: 200.0
 Episode 5: 200.0
 ……
 ……
 Episode 96: 200.0
 Episode 97: 200.0
 Episode 98: 200.0
 Episode 99: 200.0
 Episode 100: 200.0

让我们看看*均表现:

>>> print('Average total reward over {} episode: {}'.format(n_episode, sum(total_rewards) / n_episode)) Average total reward over 1000 episode: 199.78

测试集的*均奖励接*于学习策略的最大值 200。您可以多次重新运行评估。结果非常一致。

工作原理...

策略梯度算法通过采取小步骤并根据这些步骤在一集结束时获得的奖励更新权重来训练代理程序。在整个一集结束后根据获得的奖励更新策略的技术被称为蒙特卡洛策略梯度。

根据当前状态和模型权重计算的概率分布选择动作。例如,如果左右动作的概率为[0.6, 0.4],这意味着左动作被选择的概率为 60%;这并不意味着左动作被选择,如在随机搜索和爬山算法中一样。

我们知道,在一集终止之前,每一步的奖励为 1。因此,我们用于在每一步计算策略梯度的未来奖励是剩余步数。在每集结束后,我们通过将梯度历史乘以未来奖励来使用随机梯度上升法更新权重。这样,一集越长,权重更新就越大。这最终会增加获得更大总奖励的机会。

正如我们在本节开头提到的,对于像 CartPole 这样简单的环境来说,策略梯度算法可能有点过头了,但它应该能够让我们准备好处理更复杂的问题。

还有更多……

如果我们检查奖励/每轮的图表,似乎在训练过程中当解决问题时也可以提前停止 - 连续 100 轮的*均奖励不少于 195。我们只需在训练会话中添加以下几行代码:

 >>> if episode >= 99 and sum(total_rewards[-100:]) >= 19500: ...     break

重新运行训练会话。您应该会得到类似以下的结果,几百轮后停止:

Episode 1: 10.0 Episode 2: 27.0
Episode 3: 28.0
Episode 4: 15.0
Episode 5: 12.0
……
……
Episode 549: 200.0
Episode 550: 200.0
Episode 551: 200.0
Episode 552: 200.0
Episode 553: 200.0

另见

查看有关策略梯度方法的更多信息,请访问 www.scholarpedia.org/article/Policy_gradient_methods

第二章:马尔可夫决策过程和动态规划

在本章中,我们将通过观察马尔可夫决策过程(MDPs)和动态规划来继续我们的实践强化学习旅程。本章将从创建马尔可夫链和 MDP 开始,这是大多数强化学习算法的核心。您还将通过实践策略评估更加熟悉贝尔曼方程。然后我们将继续并应用两种方法解决 MDP 问题:值迭代和策略迭代。我们将以 FrozenLake 环境作为示例。在本章的最后,我们将逐步展示如何使用动态规划解决有趣的硬币抛掷赌博问题。

本章将涵盖以下示例:

  • 创建马尔可夫链

  • 创建一个 MDP

  • 执行策略评估

  • 模拟 FrozenLake 环境

  • 使用值迭代算法解决 MDP

  • 使用策略迭代算法解决 MDP

  • 使用值迭代算法解决 MDP

技术要求

要成功执行本章中的示例,请确保系统中安装了以下程序:

  • Python 3.6, 3.7 或更高版本

  • Anaconda

  • PyTorch 1.0 或更高版本

  • OpenAI Gym

创建马尔可夫链

让我们从创建一个马尔可夫链开始,以便于开发 MDP。

马尔可夫链描述了遵守马尔可夫性质的事件序列。它由一组可能的状态 S = {s0, s1, ... , sm} 和转移矩阵 T(s, s') 定义,其中包含状态 s 转移到状态 s' 的概率。根据马尔可夫性质,过程的未来状态,在给定当前状态的情况下,与过去状态是条件独立的。换句话说,过程在 t+1 时刻的状态仅依赖于 t 时刻的状态。在这里,我们以学习和睡眠过程为例,基于两个状态 s0(学习)和 s1(睡眠),创建了一个马尔可夫链。假设我们有以下转移矩阵:

在接下来的部分中,我们将计算经过 k 步后的转移矩阵,以及在初始状态分布(如 [0.7, 0.3],表示有 70% 的概率从学习开始,30% 的概率从睡眠开始)下各个状态的概率。

如何做...

要为学习 - 睡眠过程创建一个马尔可夫链,并对其进行一些分析,请执行以下步骤:

  1. 导入库并定义转移矩阵:
>>> import torch
>>> T = torch.tensor([[0.4, 0.6],
...                   [0.8, 0.2]])
  1. 计算经过 k 步后的转移概率。这里,我们以 k = 2, 5, 10, 15, 和 20 为例:
>>> T_2 = torch.matrix_power(T, 2)
>>> T_5 = torch.matrix_power(T, 5)
>>> T_10 = torch.matrix_power(T, 10)
>>> T_15 = torch.matrix_power(T, 15)
>>> T_20 = torch.matrix_power(T, 20)
  1. 定义两个状态的初始分布:
>>> v = torch.tensor([[0.7, 0.3]])
  1. 步骤 2中,我们计算了经过 k = 1, 2, 5, 10, 15, 和 20 步后的转移概率,结果如下:
>>> v_1 = torch.mm(v, T)
>>> v_2 = torch.mm(v, T_2)
>>> v_5 = torch.mm(v, T_5)
>>> v_10 = torch.mm(v, T_10)
>>> v_15 = torch.mm(v, T_15)
>>> v_20 = torch.mm(v, T_20)

它是如何工作的...

步骤 2中,我们计算了经过 k 步后的转移概率,即转移矩阵的 k 次幂。结果如下:

>>> print("Transition probability after 2 steps:\n{}".format(T_2))
Transition probability after 2 steps:
tensor([[0.6400, 0.3600],
 [0.4800, 0.5200]])
>>> print("Transition probability after 5 steps:\n{}".format(T_5))
Transition probability after 5 steps:
tensor([[0.5670, 0.4330],
 [0.5773, 0.4227]])
>>> print(
"Transition probability after 10 steps:\n{}".format(T_10))
Transition probability after 10 steps:
tensor([[0.5715, 0.4285],
 [0.5714, 0.4286]])
>>> print(
"Transition probability after 15 steps:\n{}".format(T_15))
Transition probability after 15 steps:
tensor([[0.5714, 0.4286],
 [0.5714, 0.4286]])
>>> print(
"Transition probability after 20 steps:\n{}".format(T_20))
Transition probability after 20 steps:
tensor([[0.5714, 0.4286],
 [0.5714, 0.4286]])

我们可以看到,经过 10 到 15 步,过渡概率会收敛。这意味着无论过程处于什么状态,转移到 s0(57.14%)和 s1(42.86%)的概率都相同。

步骤 4中,我们计算了 k = 125101520步后的状态分布,这是初始状态分布和过渡概率的乘积。您可以在这里看到结果:

>>> print("Distribution of states after 1 step:\n{}".format(v_1))
Distribution of states after 1 step:
tensor([[0.5200, 0.4800]])
>>> print("Distribution of states after 2 steps:\n{}".format(v_2))
Distribution of states after 2 steps:
tensor([[0.5920, 0.4080]])
>>> print("Distribution of states after 5 steps:\n{}".format(v_5))
Distribution of states after 5 steps:
tensor([[0.5701, 0.4299]])
>>> print(
 "Distribution of states after 10 steps:\n{}".format(v_10))
Distribution of states after 10 steps:
tensor([[0.5714, 0.4286]])
>>> print(
 "Distribution of states after 15 steps:\n{}".format(v_15))
Distribution of states after 15 steps:
tensor([[0.5714, 0.4286]])
>>> print(
 "Distribution of states after 20 steps:\n{}".format(v_20))
Distribution of states after 20 steps:
tensor([[0.5714, 0.4286]])

我们可以看到,经过 10 步后,状态分布会收敛。长期内处于 s0(57.14%)和 s1(42.86%)的概率保持不变。

从[0.7, 0.3]开始,经过一次迭代后的状态分布变为[0.52, 0.48]。其详细计算过程如下图所示:

经过另一次迭代,状态分布如下[0.592, 0.408],如下图所示计算:

随着时间的推移,状态分布达到*衡。

还有更多...

事实上,无论初始状态如何,状态分布都将始终收敛到[0.5714, 0.4286]。您可以尝试其他初始分布,例如[0.2, 0.8]和[1, 0]。分布在经过 10 步后仍将保持为[0.5714, 0.4286]。

马尔可夫链不一定会收敛,特别是当包含瞬态或当前状态时。但如果它确实收敛,无论起始分布如何,它将达到相同的*衡。

另见

如果您想阅读更多关于马尔可夫链的内容,以下是两篇具有良好可视化效果的博客文章:

创建 MDP

基于马尔可夫链的发展,MDP 涉及代理和决策过程。让我们继续发展一个 MDP,并计算最优策略下的值函数。

除了一组可能的状态,S = {s0, s1, ... , sm},MDP 由一组动作,A = {a0, a1, ... , an};过渡模型,T(s, a, s');奖励函数,R(s);和折现因子𝝲定义。过渡矩阵,T(s, a, s'),包含从状态 s 采取动作 a 然后转移到 s'的概率。折现因子𝝲控制未来奖励和即时奖励之间的权衡。

为了使我们的 MDP 稍微复杂化,我们将学习和睡眠过程延伸到另一个状态,s2 play 游戏。假设我们有两个动作,a0 worka1 slack3 * 2 * 3 过渡矩阵 T(s, a, s') 如下所示:

这意味着,例如,当从状态 s0 study 中采取 a1 slack 行动时,有 60%的机会它将变成 s1 sleep(可能会累),有 30%的机会它将变成 s2 play games(可能想放松),还有 10%的机会继续学习(可能是真正的工作狂)。我们为三个状态定义奖励函数为[+1, 0, -1],以补偿辛勤工作。显然,在这种情况下,最优策略是在每个步骤选择 a0 工作(继续学习——不努力就没有收获,对吧?)。此外,我们选择 0.5 作为起始折扣因子。在下一节中,我们将计算状态值函数(也称为值函数,简称期望效用)在最优策略下的值。

如何做...

创建 MDP 可以通过以下步骤完成:

  1. 导入 PyTorch 并定义转移矩阵:
 >>> import torch
 >>> T = torch.tensor([[[0.8, 0.1, 0.1],
 ...                    [0.1, 0.6, 0.3]],
 ...                   [[0.7, 0.2, 0.1],
 ...                    [0.1, 0.8, 0.1]],
 ...                   [[0.6, 0.2, 0.2],
 ...                    [0.1, 0.4, 0.5]]]
 ...                  )
  1. 定义奖励函数和折扣因子:
 >>> R = torch.tensor([1., 0, -1.])
 >>> gamma = 0.5
  1. 在这种情况下,最优策略是在所有情况下选择动作a0
>>> action = 0
  1. 我们使用矩阵求逆方法计算了最优策略的值V
 >>> def cal_value_matrix_inversion(gamma, trans_matrix, rewards):
 ...     inv = torch.inverse(torch.eye(rewards.shape[0]) 
 - gamma * trans_matrix)
 ...     V = torch.mm(inv, rewards.reshape(-1, 1))
 ...     return V

我们将在下一节中展示如何推导下一个部分的值。

  1. 我们将所有变量输入函数中,包括与动作a0相关的转移概率:
 >>> trans_matrix = T[:, action]
 >>> V = cal_value_matrix_inversion(gamma, trans_matrix, R)
 >>> print("The value function under the optimal 
 policy is:\n{}".format(V))
 The value function under the optimal policy is:
 tensor([[ 1.6787],
 [ 0.6260],
 [-0.4820]])

它是如何工作的...

在这个过于简化的学习-睡眠-游戏过程中,最优策略,即获得最高总奖励的策略,是在所有步骤中选择动作 a0。然而,在大多数情况下,情况不会那么简单。此外,个别步骤中采取的行动不一定相同。它们通常依赖于状态。因此,在实际情况中,我们将不得不通过找到最优策略来解决一个 MDP 问题。

策略的值函数衡量了在遵循策略的情况下,对于一个 agent 而言处于每个状态的好处。值越大,状态越好。

第 4 步中,我们使用矩阵求逆法计算了最优策略的值V。根据贝尔曼方程,步骤t+1的值与步骤t的值之间的关系可以表达如下:

当值收敛时,也就是Vt+1 = Vt时,我们可以推导出值V,如下所示:

这里,I是具有主对角线上的 1 的单位矩阵。

使用矩阵求逆解决 MDP 的一个优点是你总是得到一个确切的答案。但其可扩展性有限。因为我们需要计算一个 m * m 矩阵的求逆(其中m是可能的状态数量),如果有大量状态,计算成本会变得很高昂。

还有更多...

我们决定尝试不同的折扣因子值。让我们从 0 开始,这意味着我们只关心即时奖励:

 >>> gamma = 0
 >>> V = cal_value_matrix_inversion(gamma, trans_matrix, R)
 >>> print("The value function under the optimal policy is:\n{}".format(V))
 The value function under the optimal policy is:
 tensor([[ 1.],
 [ 0.],
 [-1.]])

这与奖励函数一致,因为我们只看下一步的奖励。

随着折现因子向 1 靠拢,未来的奖励被考虑。让我们看看 𝝲=0.99:

 >>> gamma = 0.99
 >>> V = cal_value_matrix_inversion(gamma, trans_matrix, R)
 >>> print("The value function under the optimal policy is:\n{}".format(V))
 The value function under the optimal policy is:
 tensor([[65.8293],
 [64.7194],
 [63.4876]])

另请参阅

这个速查表,cs-cheatsheet.readthedocs.io/en/latest/subjects/ai/mdp.html,作为马尔可夫决策过程的快速参考。

执行策略评估

我们刚刚开发了一个马尔可夫决策过程,并使用矩阵求逆计算了最优策略的值函数。我们还提到了通过求逆大型 m * m 矩阵(例如 1,000、10,000 或 100,000)的限制。在这个方案中,我们将讨论一个更简单的方法,称为策略评估

策略评估是一个迭代算法。它从任意的策略值开始,然后根据贝尔曼期望方程迭代更新值,直到收敛。在每次迭代中,状态 s 下策略 π 的值更新如下:

这里,π(s, a) 表示在策略 π 下在状态 s 中采取动作 a 的概率。T(s, a, s') 是通过采取动作 a 从状态 s 转移到状态 s' 的转移概率,R(s, a) 是在状态 s 中采取动作 a 后获得的奖励。

有两种方法来终止迭代更新过程。一种是设置一个固定的迭代次数,比如 1,000 和 10,000,有时可能难以控制。另一种是指定一个阈值(通常是 0.0001、0.00001 或类似的值),仅在所有状态的值变化程度低于指定的阈值时终止过程。

在下一节中,我们将根据最优策略和随机策略对学习-睡眠-游戏过程执行策略评估。

如何操作...

让我们开发一个策略评估算法,并将其应用于我们的学习-睡眠-游戏过程如下:

  1. 导入 PyTorch 并定义过渡矩阵:
 >>> import torch
 >>> T = torch.tensor([[[0.8, 0.1, 0.1],
 ...                    [0.1, 0.6, 0.3]],
 ...                   [[0.7, 0.2, 0.1],
 ...                    [0.1, 0.8, 0.1]],
 ...                   [[0.6, 0.2, 0.2],
 ...                    [0.1, 0.4, 0.5]]]
 ...                  )
  1. 定义奖励函数和折现因子(现在使用 0.5):
 >>> R = torch.tensor([1., 0, -1.])
 >>> gamma = 0.5
  1. 定义用于确定何时停止评估过程的阈值:
 >>> threshold = 0.0001
  1. 定义最优策略,其中在所有情况下选择动作 a0:
 >>> policy_optimal = torch.tensor([[1.0, 0.0],
 ...                                [1.0, 0.0],
 ...                                [1.0, 0.0]])
  1. 开发一个策略评估函数,接受一个策略、过渡矩阵、奖励、折现因子和阈值,并计算 value 函数:
>>> def policy_evaluation(
 policy, trans_matrix, rewards, gamma, threshold):
...     """
...     Perform policy evaluation
...     @param policy: policy matrix containing actions and their 
 probability in each state
...     @param trans_matrix: transformation matrix
...     @param rewards: rewards for each state
...     @param gamma: discount factor
...     @param threshold: the evaluation will stop once values 
 for all states are less than the threshold
...     @return: values of the given policy for all possible states
...     """
...     n_state = policy.shape[0]
...     V = torch.zeros(n_state)
...     while True:
...         V_temp = torch.zeros(n_state)
...         for state, actions in enumerate(policy):
...             for action, action_prob in enumerate(actions):
...                 V_temp[state] += action_prob * (R[state] + 
 gamma * torch.dot(
 trans_matrix[state, action], V))
...         max_delta = torch.max(torch.abs(V - V_temp))
...         V = V_temp.clone()
...         if max_delta <= threshold:
...             break
...     return V
  1. 现在让我们插入最优策略和所有其他变量:
>>> V = policy_evaluation(policy_optimal, T, R, gamma, threshold)
>>> print(
 "The value function under the optimal policy is:\n{}".format(V)) The value function under the optimal policy is:
tensor([ 1.6786,  0.6260, -0.4821])

这与我们使用矩阵求逆得到的结果几乎相同。

  1. 我们现在尝试另一个策略,一个随机策略,其中动作以相同的概率选择:
>>> policy_random = torch.tensor([[0.5, 0.5],
...                               [0.5, 0.5],
...                               [0.5, 0.5]])
  1. 插入随机策略和所有其他变量:
>>> V = policy_evaluation(policy_random, T, R, gamma, threshold)
>>> print(
 "The value function under the random policy is:\n{}".format(V))
The value function under the random policy is:
tensor([ 1.2348,  0.2691, -0.9013])

工作原理...

我们刚刚看到了使用策略评估计算策略值的效果有多么有效。这是一种简单的收敛迭代方法,在动态规划家族中,或者更具体地说是*似动态规划。它从对值的随机猜测开始,然后根据贝尔曼期望方程迭代更新,直到它们收敛。

在第 5 步中,策略评估函数执行以下任务:

  • 将策略值初始化为全零。

  • 根据贝尔曼期望方程更新值。

  • 计算所有状态中值的最大变化。

  • 如果最大变化大于阈值,则继续更新值。否则,终止评估过程并返回最新的值。

由于策略评估使用迭代逼*,其结果可能与使用精确计算的矩阵求逆方法的结果不完全相同。事实上,我们并不真的需要价值函数那么精确。此外,它可以解决维度诅咒问题,这可能导致计算扩展到数以千计的状态。因此,我们通常更喜欢策略评估而不是其他方法。

还有一件事要记住,策略评估用于预测给定策略的预期回报有多大;它不用于控制问题。

还有更多内容...

为了更仔细地观察,我们还会绘制整个评估过程中的策略值。

policy_evaluation 函数中,我们首先需要记录每次迭代的值:

>>> def policy_evaluation_history(
 policy, trans_matrix, rewards, gamma, threshold):
...     n_state = policy.shape[0]
...     V = torch.zeros(n_state)
...     V_his = [V]
...     i = 0
...     while True:
...         V_temp = torch.zeros(n_state)
...         i += 1
...         for state, actions in enumerate(policy):
...             for action, action_prob in enumerate(actions):
...                 V_temp[state] += action_prob * (R[state] + gamma * 
 torch.dot(trans_matrix[state, action], V))
...         max_delta = torch.max(torch.abs(V - V_temp))
...         V = V_temp.clone()
...         V_his.append(V)
...         if max_delta <= threshold:
...             break
...     return V, V_his

现在我们将 policy_evaluation_history 函数应用于最优策略,折现因子为 0.5,以及其他变量:

>>> V, V_history = policy_evaluation_history(
 policy_optimal, T, R, gamma, threshold)

然后,我们使用以下代码绘制了值的历史结果:

>>> import matplotlib.pyplot as plt
>>> s0, = plt.plot([v[0] for v in V_history])
>>> s1, = plt.plot([v[1] for v in V_history])
>>> s2, = plt.plot([v[2] for v in V_history])
>>> plt.title('Optimal policy with gamma = {}'.format(str(gamma)))
>>> plt.xlabel('Iteration')
>>> plt.ylabel('Policy values')
>>> plt.legend([s0, s1, s2],
...            ["State s0",
...             "State s1",
...             "State s2"], loc="upper left")
>>> plt.show()

我们看到了以下结果:

在收敛期间,从第 10 到第 14 次迭代之间的稳定性是非常有趣的。

接下来,我们使用两个不同的折现因子,0.2 和 0.99,运行相同的代码。我们得到了折现因子为 0.2 时的以下绘图:

将折现因子为 0.5 的绘图与这个进行比较,我们可以看到因子越小,策略值收敛得越快。

同时,我们也得到了折现因子为 0.99 时的以下绘图:

通过将折现因子为 0.5 的绘图与折现因子为 0.99 的绘图进行比较,我们可以看到因子越大,策略值收敛所需的时间越长。折现因子是即时奖励与未来奖励之间的权衡。

模拟 FrozenLake 环境

到目前为止,我们处理过的 MDP 的最优策略都相当直观。然而,在大多数情况下,如 FrozenLake 环境,情况并不那么简单。在这个教程中,让我们玩一下 FrozenLake 环境,并准备好接下来的教程,我们将找到它的最优策略。

FrozenLake 是一个典型的 Gym 环境,具有离散状态空间。它是关于在网格世界中将代理程序从起始位置移动到目标位置,并同时避开陷阱。网格可以是四乘四 (gym.openai.com/envs/FrozenLake-v0/) 或者八乘八。

t (gym.openai.com/envs/FrozenLake8x8-v0/)。网格由以下四种类型的方块组成:

  • S:代表起始位置

  • G:代表目标位置,这会终止一个回合

  • F:代表冰面方块,可以行走的位置

  • H:代表一个地洞位置,这会终止一个回合

显然有四种动作:向左移动(0)、向下移动(1)、向右移动(2)和向上移动(3)。如果代理程序成功到达目标位置,奖励为+1,否则为 0。此外,观察空间由一个 16 维整数数组表示,有 4 种可能的动作(这是有道理的)。

这个环境的棘手之处在于冰面很滑,代理程序并不总是按其意图移动。例如,当它打算向下移动时,可能会向左或向右移动。

准备工作

要运行 FrozenLake 环境,让我们首先在这里的环境表中搜索它:github.com/openai/gym/wiki/Table-of-environments。搜索结果给出了FrozenLake-v0

怎么做……

让我们按以下步骤模拟四乘四的 FrozenLake 环境:

  1. 我们导入gym库,并创建 FrozenLake 环境的一个实例:
>>> import gym
>>> import torch
>>> env = gym.make("FrozenLake-v0")
>>> n_state = env.observation_space.n
>>> print(n_state)
16
>>> n_action = env.action_space.n
>>> print(n_action)
4
  1. 重置环境:
>>> env.reset()
0

代理程序从状态0开始。

  1. 渲染环境:
>>> env.render()
  1. 让我们做一个向下的动作,因为这是可行走的:
>>> new_state, reward, is_done, info = env.step(1)
>>> env.render()
  1. 打印出所有返回的信息,确认代理程序以 33.33%的概率落在状态4
>>> print(new_state)
4
>>> print(reward)
0.0
>>> print(is_done)
False
>>> print(info)
{'prob': 0.3333333333333333}

你得到了0作为奖励,因为它尚未到达目标,并且回合尚未结束。再次看到代理程序可能会陷入状态 1,或者因为表面太滑而停留在状态 0。

  1. 为了展示在冰面上行走有多困难,实现一个随机策略并计算 1,000 个回合的*均总奖励。首先,定义一个函数,该函数根据给定的策略模拟一个 FrozenLake 回合并返回总奖励(我们知道这要么是 0,要么是 1):
>>> def run_episode(env, policy):
...     state = env.reset()
...     total_reward = 0
...     is_done = False
...     while not is_done:
...         action = policy[state].item()
...         state, reward, is_done, info = env.step(action)
...         total_reward += reward
...         if is_done:
...             break
...     return total_reward
  1. 现在运行1000个回合,并且在每个回合中都会随机生成并使用一个策略:
>>> n_episode = 1000
>>> total_rewards = []
>>> for episode in range(n_episode):
...     random_policy = torch.randint(
 high=n_action, size=(n_state,))
...     total_reward = run_episode(env, random_policy)
...     total_rewards.append(total_reward)
...
>>> print('Average total reward under random policy: {}'.format(
 sum(total_rewards) / n_episode))
Average total reward under random policy: 0.014

这基本上意味着,如果我们随机执行动作,*均只有 1.4%的机会代理程序能够到达目标位置。

  1. 接下来,我们将使用随机搜索策略进行实验。在训练阶段,我们随机生成一堆策略,并记录第一个达到目标的策略:
>>> while True:
...     random_policy = torch.randint(
 high=n_action, size=(n_state,))
...     total_reward = run_episode(env, random_policy)
...     if total_reward == 1:
...         best_policy = random_policy
...         break
  1. 查看最佳策略:
>>> print(best_policy)
tensor([0, 3, 2, 2, 0, 2, 1, 1, 3, 1, 3, 0, 0, 1, 1, 1])
  1. 现在运行 1,000 个回合,使用我们刚挑选出的策略:
>>> total_rewards = []
>>> for episode in range(n_episode):
...     total_reward = run_episode(env, best_policy)
...     total_rewards.append(total_reward)
...
>>> print('Average total reward under random search 
     policy: {}'.format(sum(total_rewards) / n_episode))
Average total reward under random search policy: 0.208

使用随机搜索算法,*均情况下会有 20.8% 的概率达到目标。

请注意,由于我们选择的策略可能由于冰面滑动而达到目标,这可能会导致结果变化很大,可能不是最优策略。

工作原理……

在这个示例中,我们随机生成了一个由 16 个动作组成的策略,对应 16 个状态。请记住,在 FrozenLake 中,移动方向仅部分依赖于选择的动作,这增加了控制的不确定性。

在运行 Step 4 中的代码后,你将看到一个 4 * 4 的矩阵,代表冰湖和代理站立的瓷砖(状态 0):

在运行 Step 5 中的代码行后,你将看到如下结果网格,代理向下移动到状态 4:

如果满足以下两个条件之一,一个回合将终止:

  • 移动到 H 格(状态 5、7、11、12)。这将生成总奖励 0。

  • 移动到 G 格(状态 15)。这将产生总奖励 +1。

还有更多内容……

我们可以使用 P 属性查看 FrozenLake 环境的详细信息,包括转移矩阵和每个状态及动作的奖励。例如,对于状态 6,我们可以执行以下操作:

>>> print(env.env.P[6])
{0: [(0.3333333333333333, 2, 0.0, False), (0.3333333333333333, 5, 0.0, True), (0.3333333333333333, 10, 0.0, False)], 1: [(0.3333333333333333, 5, 0.0, True), (0.3333333333333333, 10, 0.0, False), (0.3333333333333333, 7, 0.0, True)], 2: [(0.3333333333333333, 10, 0.0, False), (0.3333333333333333, 7, 0.0, True), (0.3333333333333333, 2, 0.0, False)], 3: [(0.3333333333333333, 7, 0.0, True), (0.3333333333333333, 2, 0.0, False), (0.3333333333333333, 5, 0.0, True)]}

这会返回一个字典,其键为 0、1、2 和 3,分别代表四种可能的动作。值是一个列表,包含在执行动作后的移动。移动列表的格式如下:(转移概率,新状态,获得的奖励,是否结束)。例如,如果代理处于状态 6 并打算执行动作 1(向下),有 33.33% 的概率它会进入状态 5,获得奖励 0 并终止该回合;有 33.33% 的概率它会进入状态 10,获得奖励 0;有 33.33% 的概率它会进入状态 7,获得奖励 0 并终止该回合。

对于状态 11,我们可以执行以下操作:

>>> print(env.env.P[11])
{0: [(1.0, 11, 0, True)], 1: [(1.0, 11, 0, True)], 2: [(1.0, 11, 0, True)], 3: [(1.0, 11, 0, True)]}

由于踩到洞会终止一个回合,所以不会再有任何移动。

随意查看其他状态。

使用值迭代算法解决 MDP

如果找到其最优策略,则认为 MDP 已解决。在这个示例中,我们将使用 值迭代 算法找出 FrozenLake 环境的最优策略。

值迭代的思想与策略评估非常相似。它也是一种迭代算法。它从任意策略值开始,然后根据贝尔曼最优方程迭代更新值,直到收敛。因此,在每次迭代中,它不是采用跨所有动作的值的期望(*均值),而是选择实现最大策略值的动作:

这里,V*(s)表示最优值,即最优策略的值;T(s, a, s')是采取动作 a 从状态 s 转移到状态 s’的转移概率;而 R(s, a)是采取动作 a 时在状态 s 中收到的奖励。

计算出最优值后,我们可以相应地获得最优策略:

如何做…

让我们使用值迭代算法解决 FrozenLake 环境如下:

  1. 导入必要的库并创建 FrozenLake 环境的实例:
>>> import torch
>>> import gym
>>> env = gym.make('FrozenLake-v0')
  1. 将折扣因子设为0.99,收敛阈值设为0.0001
>>> gamma = 0.99
>>> threshold = 0.0001
  1. 现在定义一个函数,根据值迭代算法计算最优值:
>>> def value_iteration(env, gamma, threshold):
...     """
...     Solve a given environment with value iteration algorithm
...     @param env: OpenAI Gym environment
...     @param gamma: discount factor
...     @param threshold: the evaluation will stop once values for 
 all states are less than the threshold
...     @return: values of the optimal policy for the given 
 environment
...     """
...     n_state = env.observation_space.n
...     n_action = env.action_space.n
...     V = torch.zeros(n_state)
...     while True:
...         V_temp = torch.empty(n_state)
...         for state in range(n_state):
...             v_actions = torch.zeros(n_action)
...             for action in range(n_action):
...                 for trans_prob, new_state, reward, _ in 
 env.env.P[state][action]:
...                     v_actions[action] += trans_prob * (reward 
 + gamma * V[new_state])
...             V_temp[state] = torch.max(v_actions)
...         max_delta = torch.max(torch.abs(V - V_temp))
...         V = V_temp.clone()
...         if max_delta <= threshold:
...             break
...     return V
  1. 插入环境、折扣因子和收敛阈值,然后打印最优值:
>>> V_optimal = value_iteration(env, gamma, threshold)
>>> print('Optimal values:\n{}'.format(V_optimal))
Optimal values:
tensor([0.5404, 0.4966, 0.4681, 0.4541, 0.5569, 0.0000, 0.3572, 0.0000, 0.5905,
 0.6421, 0.6144, 0.0000, 0.0000, 0.7410, 0.8625, 0.0000])
  1. 现在我们有了最优值,我们开发提取最优策略的函数:
>>> def extract_optimal_policy(env, V_optimal, gamma):
...     """
...     Obtain the optimal policy based on the optimal values
...     @param env: OpenAI Gym environment
...     @param V_optimal: optimal values
...     @param gamma: discount factor
...     @return: optimal policy
...     """
...     n_state = env.observation_space.n
...     n_action = env.action_space.n
...     optimal_policy = torch.zeros(n_state)
...     for state in range(n_state):
...         v_actions = torch.zeros(n_action)
...         for action in range(n_action):
...             for trans_prob, new_state, reward, _ in 
                                   env.env.P[state][action]:
...                 v_actions[action] += trans_prob * (reward 
 + gamma * V_optimal[new_state])
...         optimal_policy[state] = torch.argmax(v_actions)
...     return optimal_policy
  1. 插入环境、折扣因子和最优值,然后打印最优策略:
>>> optimal_policy = extract_optimal_policy(env, V_optimal, gamma)
>>> print('Optimal policy:\n{}'.format(optimal_policy))
Optimal policy:
tensor([0., 3., 3., 3., 0., 3., 2., 3., 3., 1., 0., 3., 3., 2., 1., 3.])
  1. 我们想要评估最优策略的好坏程度。因此,让我们使用最优策略运行 1,000 次情节,并检查*均奖励。在这里,我们将重复使用我们在前面的配方中定义的run_episode函数:
>>> n_episode = 1000
>>> total_rewards = []
>>> for episode in range(n_episode):
...     total_reward = run_episode(env, optimal_policy)
...     total_rewards.append(total_reward)
>>> print('Average total reward under the optimal 
 policy: {}'.format(sum(total_rewards) / n_episode))
Average total reward under the optimal policy: 0.75

在最优策略下,代理将*均 75%的时间到达目标。这是我们能够做到的最好结果,因为冰很滑。

工作原理…

在值迭代算法中,我们通过迭代应用贝尔曼最优方程来获得最优值函数。

下面是贝尔曼最优方程的另一版本,适用于奖励部分依赖于新状态的环境:

这里,R(s, a, s')表示通过采取动作 a 从状态 s 移动到状态 s'而收到的奖励。由于这个版本更兼容,我们根据它开发了我们的value_iteration函数。正如您在Step 3中看到的,我们执行以下任务:

  • 将策略值初始化为全部为零。

  • 根据贝尔曼最优方程更新值。

  • 计算所有状态的值的最大变化。

  • 如果最大变化大于阈值,则继续更新值。否则,终止评估过程,并返回最新的值作为最优值。

还有更多…

我们在折扣因子为 0.99 时获得了 75%的成功率。折扣因子如何影响性能?让我们用不同的因子进行一些实验,包括00.20.40.60.80.991.

>>> gammas = [0, 0.2, 0.4, 0.6, 0.8, .99, 1.]

对于每个折扣因子,我们计算了 10,000 个周期的*均成功率:

>>> avg_reward_gamma = []
>>> for gamma in gammas:
...     V_optimal = value_iteration(env, gamma, threshold)
...     optimal_policy = extract_optimal_policy(env, V_optimal, gamma)
...     total_rewards = []
...     for episode in range(n_episode):
...         total_reward = run_episode(env, optimal_policy)
...         total_rewards.append(total_reward)
...     avg_reward_gamma.append(sum(total_rewards) / n_episode)

我们绘制了*均成功率与折扣因子的图表:

>>> import matplotlib.pyplot as plt
>>> plt.plot(gammas, avg_reward_gamma)
>>> plt.title('Success rate vs discount factor')
>>> plt.xlabel('Discount factor')
>>> plt.ylabel('Average success rate')
>>> plt.show()

我们得到以下的绘图:

结果显示,当折扣因子增加时,性能有所提升。这证实了一个小的折扣因子目前价值奖励,而一个大的折扣因子则更看重未来的更好奖励。

使用策略迭代算法解决 MDP

解决 MDP 的另一种方法是使用策略迭代算法,我们将在本配方中讨论它。

策略迭代算法可以分为两个部分:策略评估和策略改进。它从任意策略开始。每次迭代中,它首先根据贝尔曼期望方程计算给定最新策略的策略值;然后根据贝尔曼最优性方程从结果策略值中提取一个改进的策略。它反复评估策略并生成改进版本,直到策略不再改变为止。

让我们开发一个策略迭代算法,并使用它来解决 FrozenLake 环境。之后,我们将解释它的工作原理。

如何做…

让我们使用策略迭代算法解决 FrozenLake 环境:

  1. 我们导入必要的库并创建 FrozenLake 环境的实例:
>>> import torch
>>> import gym
>>> env = gym.make('FrozenLake-v0')
  1. 现在,暂将折扣因子设定为0.99,收敛阈值设定为0.0001
>>> gamma = 0.99
>>> threshold = 0.0001
  1. 现在我们定义policy_evaluation函数,它计算给定策略的值:
>>> def policy_evaluation(env, policy, gamma, threshold):
...     """
...     Perform policy evaluation
...     @param env: OpenAI Gym environment
...     @param policy: policy matrix containing actions and 
 their probability in each state
...     @param gamma: discount factor
...     @param threshold: the evaluation will stop once values 
 for all states are less than the threshold
...     @return: values of the given policy
...     """
...     n_state = policy.shape[0]
...     V = torch.zeros(n_state)
...     while True:
...         V_temp = torch.zeros(n_state)
...         for state in range(n_state):
...             action = policy[state].item()
...             for trans_prob, new_state, reward, _ in 
 env.env.P[state][action]:
...                 V_temp[state] += trans_prob * (reward 
 + gamma * V[new_state])
...         max_delta = torch.max(torch.abs(V - V_temp))
...         V = V_temp.clone()
...         if max_delta <= threshold:
...             break
...     return V

这与我们在执行策略评估配方中所做的类似,但输入是 Gym 环境。

  1. 接下来,我们开发策略迭代算法的第二个主要组成部分,即策略改进部分:
>>> def policy_improvement(env, V, gamma):
...     """
...     Obtain an improved policy based on the values
...     @param env: OpenAI Gym environment
...     @param V: policy values
...     @param gamma: discount factor
...     @return: the policy
...     """
...     n_state = env.observation_space.n
...     n_action = env.action_space.n
...     policy = torch.zeros(n_state)
...     for state in range(n_state):
...         v_actions = torch.zeros(n_action)
...         for action in range(n_action):
...             for trans_prob, new_state, reward, _ in 
 env.env.P[state][action]:
...                 v_actions[action] += trans_prob * (reward 
 + gamma * V[new_state])
...         policy[state] = torch.argmax(v_actions)
...     return policy

这根据贝尔曼最优性方程从给定的策略值中提取了一个改进的策略。

  1. 现在我们两个组件都准备好了,我们按以下方式开发策略迭代算法:
>>> def policy_iteration(env, gamma, threshold):
...     """
...     Solve a given environment with policy iteration algorithm
...     @param env: OpenAI Gym environment
...     @param gamma: discount factor
...     @param threshold: the evaluation will stop once values 
 for all states are less than the threshold
...     @return: optimal values and the optimal policy for the given 
 environment
...     """
...     n_state = env.observation_space.n
...     n_action = env.action_space.n
...     policy = torch.randint(high=n_action, size=(n_state,)).float()
...     while True:
...         V = policy_evaluation(env, policy, gamma, threshold)
...         policy_improved = policy_improvement(env, V, gamma)
...         if torch.equal(policy_improved, policy):
...             return V, policy_improved
...         policy = policy_improved
  1. 插入环境、折扣因子和收敛阈值:
>>> V_optimal, optimal_policy = 
 policy_iteration(env, gamma, threshold)
  1. 我们已经获得了最优值和最优策略。让我们看一看它们:
>>> print('Optimal values:\n{}'.format(V_optimal))
Optimal values:
tensor([0.5404, 0.4966, 0.4681, 0.4541, 0.5569, 0.0000, 0.3572, 0.0000, 0.5905,
 0.6421, 0.6144, 0.0000, 0.0000, 0.7410, 0.8625, 0.0000])
>>> print('Optimal policy:\n{}'.format(optimal_policy))
Optimal policy:
tensor([0., 3., 3., 3., 0., 3., 2., 3., 3., 1., 0., 3., 3., 2., 1., 3.])

这与使用值迭代算法得到的结果完全一样。

它是如何工作的…

策略迭代结合了每次迭代中的策略评估和策略改进。在策略评估中,根据贝尔曼期望方程计算给定策略(而非最优策略)的值,直到它们收敛:

这里,a = π(s),即在状态 s 下根据策略π采取的动作。

在策略改进中,根据贝尔曼最优性方程使用收敛的策略值 V(s)更新策略:

这重复策略评估和策略改进步骤,直到策略收敛。在收敛时,最新的策略和其值函数是最优策略和最优值函数。因此,在第 5 步,policy_iteration函数执行以下任务:

  • 初始化一个随机策略。

  • 使用策略评估算法计算策略的值。

  • 基于策略值获取改进的策略。

  • 如果新策略与旧策略不同,则更新策略并运行另一次迭代。否则,终止迭代过程并返回策略值和策略。

还有更多...

我们刚刚用策略迭代算法解决了 FrozenLake 环境。因此,您可能想知道何时最好使用策略迭代而不是值迭代,反之亦然。基本上有三种情况其中一种比另一种更占优势:

  • 如果有大量的动作,请使用策略迭代,因为它可以更快地收敛。

  • 如果动作数量较少,请使用值迭代。

  • 如果已经有一个可行的策略(通过直觉或领域知识获得),请使用策略迭代。

在这些情况之外,策略迭代和值迭代通常是可比较的。

在下一个案例中,我们将应用每种算法来解决硬币抛掷赌博问题。我们将看到哪种算法收敛得更快。

参见

请随意使用我们在这两个案例中学到的知识来解决一个更大的冰格,即 FrozenLake8x8-v0 环境 (gym.openai.com/envs/FrozenLake8x8-v0/)。

解决硬币抛掷赌博问题

对硬币抛掷赌博应该对每个人都很熟悉。在游戏的每一轮中,赌徒可以打赌硬币是否会正面朝上。如果结果是正面,赌徒将赢得他们下注的相同金额;否则,他们将失去这笔金额。游戏将继续,直到赌徒输掉(最终一无所有)或赢得(赢得超过 100 美元,假设)。假设硬币不公*,并且有 40%的概率正面朝上。为了最大化赢的机会,赌徒应该根据当前资本在每一轮下注多少?这绝对是一个有趣的问题要解决。

如果硬币正面朝上的概率超过 50%,就没什么好讨论的。赌徒可以每轮下注一美元,并且大多数情况下应该能赢得游戏。如果是公*硬币,赌徒每轮下注一美元时,大约一半的时间会赢。当正面朝上的概率低于 50% 时,保守的策略就行不通了。随机策略也不行。我们需要依靠本章学到的强化学习技术来做出明智的投注。

让我们开始将抛硬币赌博问题制定为马尔可夫决策过程(MDP)。它基本上是一个无折扣、周期性的有限 MDP,具有以下特性:

  • 状态是赌徒的美元资本。总共有 101 个状态:0、1、2、…、98、99 和 100+。

  • 如果达到状态 100+,则奖励为 1;否则,奖励为 0。

  • 行动是赌徒在一轮中可能下注的金额。对于状态 s,可能的行动包括 1、2、…,以及 min(s, 100 - s)。例如,当赌徒有 60 美元时,他们可以下注从 1 到 40 的任意金额。超过 40 的任何金额都没有意义,因为它增加了损失并且不增加赢得游戏的机会。

  • 在采取行动后,下一个状态取决于硬币正面朝上的概率。假设是 40%。因此,在采取行动 a 后,状态 s 的下一个状态将以 40% 的概率为 s+a,以 60% 的概率为 s-a

  • 过程在状态 0 和状态 100+ 处终止。

如何做…

我们首先使用值迭代算法解决抛硬币赌博问题,并执行以下步骤:

  1. 导入 PyTorch:
>>> import torch
  1. 指定折扣因子和收敛阈值:
>>> gamma = 1
>>> threshold = 1e-10

在这里,我们将折扣因子设为 1,因为这个 MDP 是一个无折扣的过程;我们设置了一个小阈值,因为我们预期策略值较小,所有奖励都是 0,除了最后一个状态。

  1. 定义以下环境变量。

总共有 101 个状态:

>>> capital_max = 100
>>> n_state = capital_max + 1

相应的奖励显示如下:

>>> rewards = torch.zeros(n_state)
>>> rewards[-1] = 1
>>> print(rewards)
tensor([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1.])

假设正面朝上的概率是 40%:

>>> head_prob = 0.4

将这些变量放入字典中:

>>> env = {'capital_max': capital_max,
...        'head_prob': head_prob,
...        'rewards': rewards,
...        'n_state': n_state}
  1. 现在我们开发一个函数,根据值迭代算法计算最优值:
>>> def value_iteration(env, gamma, threshold):
...     """
...     Solve the coin flipping gamble problem with 
 value iteration algorithm
...     @param env: the coin flipping gamble environment
...     @param gamma: discount factor
...     @param threshold: the evaluation will stop once values 
 for all states are less than the threshold
...     @return: values of the optimal policy for the given 
 environment
...     """
...     head_prob = env['head_prob']
...     n_state = env['n_state']
...     capital_max = env['capital_max']
...     V = torch.zeros(n_state)
...     while True:
...         V_temp = torch.zeros(n_state)
...         for state in range(1, capital_max):
...             v_actions = torch.zeros(
 min(state, capital_max - state) + 1)
...             for action in range(
 1, min(state, capital_max - state) + 1):
...                 v_actions[action] += head_prob * (
 rewards[state + action] +
 gamma * V[state + action])
...                 v_actions[action] += (1 - head_prob) * (
 rewards[state - action] +
 gamma * V[state - action])
...             V_temp[state] = torch.max(v_actions)
...         max_delta = torch.max(torch.abs(V - V_temp))
...         V = V_temp.clone()
...         if max_delta <= threshold:
...             break
...     return V

我们只需计算状态 1 到 99 的值,因为状态 0 和状态 100+ 的值为 0。而给定状态 s,可能的行动可以是从 1 到 min(s, 100 - s)。在计算贝尔曼最优方程时,我们应该牢记这一点。

  1. 接下来,我们开发一个函数,根据最优值提取最优策略:
>>> def extract_optimal_policy(env, V_optimal, gamma):
...     """
...     Obtain the optimal policy based on the optimal values
...     @param env: the coin flipping gamble environment
...     @param V_optimal: optimal values
...     @param gamma: discount factor
...     @return: optimal policy
...     """
...     head_prob = env['head_prob']
...     n_state = env['n_state']
...     capital_max = env['capital_max']
...     optimal_policy = torch.zeros(capital_max).int()
...     for state in range(1, capital_max):
...         v_actions = torch.zeros(n_state)
...         for action in range(1, 
 min(state, capital_max - state) + 1):
...             v_actions[action] += head_prob * (
 rewards[state + action] +
 gamma * V_optimal[state + action])
...             v_actions[action] += (1 - head_prob) * (
 rewards[state - action] +
 gamma * V_optimal[state - action])
...         optimal_policy[state] = torch.argmax(v_actions)
...     return optimal_policy
  1. 最后,我们可以将环境、折扣因子和收敛阈值输入,计算出最优值和最优策略。此外,我们还计时了使用值迭代解决赌博 MDP 所需的时间;我们将其与策略迭代完成所需的时间进行比较:
>>> import time
>>> start_time = time.time()
>>> V_optimal = value_iteration(env, gamma, threshold)
>>> optimal_policy = extract_optimal_policy(env, V_optimal, gamma)
>>> print("It takes {:.3f}s to solve with value 
 iteration".format(time.time() - start_time))
It takes 4.717s to solve with value iteration

我们在 4.717 秒内使用值迭代算法解决了赌博问题。

  1. 查看我们得到的最优策略值和最优策略:
>>> print('Optimal values:\n{}'.format(V_optimal))
>>> print('Optimal policy:\n{}'.format(optimal_policy))
  1. 我们可以绘制策略值与状态的图表如下:
>>> import matplotlib.pyplot as plt
>>> plt.plot(V_optimal[:100].numpy())
>>> plt.title('Optimal policy values')
>>> plt.xlabel('Capital')
>>> plt.ylabel('Policy value')
>>> plt.show()

现在我们已经通过值迭代解决了赌博问题,接下来是策略迭代?我们来看看。

  1. 我们首先开发policy_evaluation函数,该函数根据策略计算值:
>>> def policy_evaluation(env, policy, gamma, threshold):
...     """
...     Perform policy evaluation
...     @param env: the coin flipping gamble environment
...     @param policy: policy tensor containing actions taken 
 for individual state
...     @param gamma: discount factor
...     @param threshold: the evaluation will stop once values 
 for all states are less than the threshold
...     @return: values of the given policy
...     """
...     head_prob = env['head_prob']
...     n_state = env['n_state']
...     capital_max = env['capital_max']
...     V = torch.zeros(n_state)
...     while True:
...         V_temp = torch.zeros(n_state)
...         for state in range(1, capital_max):
...             action = policy[state].item()
...             V_temp[state] += head_prob * (
 rewards[state + action] +
 gamma * V[state + action])
...             V_temp[state] += (1 - head_prob) * (
 rewards[state - action] +
 gamma * V[state - action])
...         max_delta = torch.max(torch.abs(V - V_temp))
...         V = V_temp.clone()
...         if max_delta <= threshold:
...             break
...     return V
  1. 接下来,我们开发策略迭代算法的另一个主要组成部分,即策略改进部分:
>>> def policy_improvement(env, V, gamma):
...     """
...     Obtain an improved policy based on the values
...     @param env: the coin flipping gamble environment
...     @param V: policy values
...     @param gamma: discount factor
...     @return: the policy
...     """
...     head_prob = env['head_prob']
...     n_state = env['n_state']
...     capital_max = env['capital_max']
...     policy = torch.zeros(n_state).int()
...     for state in range(1, capital_max):
...         v_actions = torch.zeros(
 min(state, capital_max - state) + 1)
...         for action in range(
 1, min(state, capital_max - state) + 1):
...             v_actions[action] += head_prob * (
 rewards[state + action] + 
 gamma * V[state + action])
...             v_actions[action] += (1 - head_prob) * (
 rewards[state - action] +
 gamma * V[state - action])
...         policy[state] = torch.argmax(v_actions)
...     return policy
  1. 有了这两个组件,我们可以开发策略迭代算法的主要入口如下:
>>> def policy_iteration(env, gamma, threshold):
...     """
...     Solve the coin flipping gamble problem with policy 
 iteration algorithm
...     @param env: the coin flipping gamble environment
...     @param gamma: discount factor
...     @param threshold: the evaluation will stop once values
 for all states are less than the threshold
...     @return: optimal values and the optimal policy for the 
 given environment
...     """
...     n_state = env['n_state']
...     policy = torch.zeros(n_state).int()
...     while True:
...         V = policy_evaluation(env, policy, gamma, threshold)
...         policy_improved = policy_improvement(env, V, gamma)
...         if torch.equal(policy_improved, policy):
...             return V, policy_improved
...         policy = policy_improved
  1. 最后,我们将环境、折扣因子和收敛阈值插入以计算最优值和最优策略。我们记录解决 MDP 所花费的时间:
>>> start_time = time.time()
>>> V_optimal, optimal_policy 
 = policy_iteration(env, gamma, threshold)
>>> print("It takes {:.3f}s to solve with policy 
 iteration".format(time.time() - start_time))
It takes 2.002s to solve with policy iteration
  1. 查看刚刚获得的最优值和最优策略:
>>> print('Optimal values:\n{}'.format(V_optimal))
>>> print('Optimal policy:\n{}'.format(optimal_policy))

它是如何运作的……

在执行第 7 步中的代码行后,您将看到最优策略值:

Optimal values:
tensor([0.0000, 0.0021, 0.0052, 0.0092, 0.0129, 0.0174, 0.0231, 0.0278, 0.0323,
 0.0377, 0.0435, 0.0504, 0.0577, 0.0652, 0.0695, 0.0744, 0.0807, 0.0866,
 0.0942, 0.1031, 0.1087, 0.1160, 0.1259, 0.1336, 0.1441, 0.1600, 0.1631,
 0.1677, 0.1738, 0.1794, 0.1861, 0.1946, 0.2017, 0.2084, 0.2165, 0.2252,
 0.2355, 0.2465, 0.2579, 0.2643, 0.2716, 0.2810, 0.2899, 0.3013, 0.3147,
 0.3230, 0.3339, 0.3488, 0.3604, 0.3762, 0.4000, 0.4031, 0.4077, 0.4138,
 0.4194, 0.4261, 0.4346, 0.4417, 0.4484, 0.4565, 0.4652, 0.4755, 0.4865,
 0.4979, 0.5043, 0.5116, 0.5210, 0.5299, 0.5413, 0.5547, 0.5630, 0.5740,
 0.5888, 0.6004, 0.6162, 0.6400, 0.6446, 0.6516, 0.6608, 0.6690, 0.6791,
 0.6919, 0.7026, 0.7126, 0.7248, 0.7378, 0.7533, 0.7697, 0.7868, 0.7965,
 0.8075, 0.8215, 0.8349, 0.8520, 0.8721, 0.8845, 0.9009, 0.9232, 0.9406,
 0.9643, 0.0000])

您还将看到最优策略:

Optimal policy:
tensor([ 0,  1, 2, 3, 4,  5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 22, 29, 30, 31, 32, 33,  9, 35,
 36, 37, 38, 11, 40,  9, 42, 43, 44, 5, 4,  3, 2, 1, 50, 1, 2, 47,
 4, 5, 44,  7, 8, 9, 10, 11, 38, 12, 36, 35, 34, 17, 32, 19, 30,  4,
 3, 2, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11,
 10, 9, 8,  7, 6, 5, 4,  3, 2, 1], dtype=torch.int32)

第 8 步 生成了以下最优策略值的图表:

我们可以看到,随着资本(状态)的增加,估计的奖励(策略值)也在增加,这是有道理的。

第 9 步中我们所做的事情与Solving an MDP with a policy iteration algorithm配方中的所做的非常相似,但这次是针对抛硬币赌博环境。

第 10 步中,策略改进函数从给定的策略值中提取出改进的策略,基于贝尔曼最优方程。

正如您在第 12 步中所看到的,我们通过策略迭代在2.002秒内解决了赌博问题,比值迭代所花费的时间少了一半。

我们从第 13 步得到的结果包括以下最优值:

Optimal values:
tensor([0.0000, 0.0021, 0.0052, 0.0092, 0.0129, 0.0174, 0.0231, 0.0278, 0.0323,
 0.0377, 0.0435, 0.0504, 0.0577, 0.0652, 0.0695, 0.0744, 0.0807, 0.0866,
 0.0942, 0.1031, 0.1087, 0.1160, 0.1259, 0.1336, 0.1441, 0.1600, 0.1631,
 0.1677, 0.1738, 0.1794, 0.1861, 0.1946, 0.2017, 0.2084, 0.2165, 0.2252,
 0.2355, 0.2465, 0.2579, 0.2643, 0.2716, 0.2810, 0.2899, 0.3013, 0.3147,
 0.3230, 0.3339, 0.3488, 0.3604, 0.3762, 0.4000, 0.4031, 0.4077, 0.4138,
 0.4194, 0.4261, 0.4346, 0.4417, 0.4484, 0.4565, 0.4652, 0.4755, 0.4865,
 0.4979, 0.5043, 0.5116, 0.5210, 0.5299, 0.5413, 0.5547, 0.5630, 0.5740,
 0.5888, 0.6004, 0.6162, 0.6400, 0.6446, 0.6516, 0.6608, 0.6690, 0.6791,
 0.6919, 0.7026, 0.7126, 0.7248, 0.7378, 0.7533, 0.7697, 0.7868, 0.7965,
 0.8075, 0.8215, 0.8349, 0.8520, 0.8721, 0.8845, 0.9009, 0.9232, 0.9406,
 0.9643, 0.0000])

它们还包括最优策略:

Optimal policy:
tensor([ 0,  1, 2, 3, 4,  5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 22, 29, 30, 31, 32, 33,  9, 35,
 36, 37, 38, 11, 40,  9, 42, 43, 44, 5, 4,  3, 2, 1, 50, 1, 2, 47,
 4, 5, 44,  7, 8, 9, 10, 11, 38, 12, 36, 35, 34, 17, 32, 19, 30,  4,
 3, 2, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11,
 10, 9, 8,  7, 6, 5, 4,  3, 2, 1, 0], dtype=torch.int32)

来自值迭代和策略迭代的两种方法的结果是一致的。

我们通过值迭代和策略迭代解决了赌博问题。处理强化学习问题中最棘手的任务之一是将过程形式化为 MDP。在我们的例子中,通过下注一定的赌注(动作),将当前资本(状态)的策略转化为新资本(新状态)。最优策略最大化了赢得游戏的概率(+1 奖励),并在最优策略下评估了赢得游戏的概率。

另一个有趣的事情是注意我们的示例中如何确定贝尔曼方程中的转换概率和新状态。在状态 s 中采取动作 a(拥有资本 s 并下注 1 美元),将有两种可能的结果:

  • 如果硬币正面朝上,则移动到新状态 s+a。因此,转换概率等于正面朝上的概率。

  • 如果硬币反面朝上,则移动到新状态 s-a。因此,转换概率等于反面朝上的概率。

这与 FrozenLake 环境非常相似,代理人只有以一定概率着陆在预期的瓦片上。

我们还验证了在这种情况下,策略迭代比值迭代收敛更快。这是因为可能有多达 50 个可能的行动,这比 FrozenLake 中的 4 个行动更多。对于具有大量行动的马尔可夫决策过程,用策略迭代解决比值迭代更有效率。

还有更多...

你可能想知道最优策略是否真的有效。让我们像聪明的赌徒一样玩 10,000 个剧集的游戏。我们将比较最优策略与另外两种策略:保守策略(每轮下注一美元)和随机策略(下注随机金额):

  1. 我们首先通过定义三种上述的投注策略开始。

我们首先定义最优策略:

>>> def optimal_strategy(capital):
...     return optimal_policy[capital].item()

然后我们定义保守策略:

>>> def conservative_strategy(capital):
...     return 1

最后,我们定义随机策略:

>>> def random_strategy(capital):
...     return torch.randint(1, capital + 1, (1,)).item()
  1. 定义一个包装函数,用一种策略运行一个剧集,并返回游戏是否获胜:
>>> def run_episode(head_prob, capital, policy):
...     while capital > 0:
...         bet = policy(capital)
...         if torch.rand(1).item() < head_prob:
...             capital += bet
...             if capital >= 100:
...                 return 1
...         else:
...             capital -= bet
...     return 0
  1. 指定一个起始资本(假设是50美元)和一定数量的剧集(10000):
>>> capital = 50
>>> n_episode = 10000
  1. 运行 10,000 个剧集并跟踪获胜次数:
>>> n_win_random = 0
>>> n_win_conservative = 0
>>> n_win_optimal = 0
>>> for episode in range(n_episode):
...     n_win_random += run_episode(
 head_prob, capital, random_strategy)
...     n_win_conservative += run_episode(
 head_prob, capital, conservative_strategy)
...     n_win_optimal += run_episode(
 head_prob, capital, optimal_strategy)
  1. 打印出三种策略的获胜概率:
>>> print('Average winning probability under the random 
 policy: {}'.format(n_win_random/n_episode))
Average winning probability under the random policy: 0.2251
>>> print('Average winning probability under the conservative 
 policy: {}'.format(n_win_conservative/n_episode))
Average winning probability under the conservative policy: 0.0
>>> print('Average winning probability under the optimal 
 policy: {}'.format(n_win_optimal/n_episode))
Average winning probability under the optimal policy: 0.3947

我们的最优策略显然是赢家!

第三章:用于进行数值估计的蒙特卡洛方法

在前一章中,我们使用动态规划评估和解决了马尔可夫决策过程 (MDP)。像 DP 这样的模型基方法有一些缺点。它们需要完全了解环境,包括转移矩阵和奖励矩阵。它们的可扩展性也有限,特别是对于具有大量状态的环境。

在本章中,我们将继续我们的学习之旅,采用无模型方法,即蒙特卡洛 (MC) 方法,它不需要环境的先前知识,并且比 DP 具有更高的可扩展性。我们将从使用蒙特卡洛方法估计π的值开始。接下来,我们将讨论如何使用 MC 方法以首次访问和每次访问的方式来预测状态值和状态-动作值。我们将演示如何使用蒙特卡洛训练代理玩二十一点游戏。此外,我们还将实现基于策略和离策略的 MC 控制,以找到二十一点的最优策略。还将介绍具有ε-贪心策略和加权重要性采样的高级 MC 控制。

本章将涵盖以下配方:

  • 使用蒙特卡洛方法计算π

  • 执行蒙特卡洛策略评估

  • 使用蒙特卡洛预测玩二十一点

  • 执行基于策略的蒙特卡洛控制

  • 开发具有ε-贪心策略的蒙特卡洛控制

  • 执行离策略蒙特卡洛控制

  • 开发具有加权重要性采样的 MC 控制

使用蒙特卡洛方法计算π

让我们开始一个简单的项目:使用蒙特卡洛方法估算π的值,这是无模型强化学习算法的核心。

蒙特卡洛方法是使用随机性解决问题的任何方法。该算法重复适当的随机抽样,并观察满足特定属性的样本分数,以便进行数值估计。

让我们做一个有趣的练习,使用 MC 方法*似计算π的值。我们在一个边长为 2 的正方形内随机放置大量点(-1<x<1, -1<y<1),并计算落入单位半径圆内的点数。我们都知道正方形的面积为:

圆的面积为:

如果我们将圆的面积除以正方形的面积,我们有以下结果:

S/C可以用落入圆内的点的比例来衡量。因此,π的值可以估计为S/C的四倍。

如何做到...

我们使用 MC 方法来估算π的值如下:

  1. 导入必要的模块,包括 PyTorch,π的真实值的math,以及用于在正方形内绘制随机点的matplotlib
>>> import torch
>>> import math
>>> import matplotlib.pyplot as plt
  1. 我们随机生成 1,000 个点在正方形内,范围为-1<x<1 和-1<y<1:
>>> n_point = 1000
>>> points = torch.rand((n_point, 2)) * 2 - 1
  1. 初始化单位圆内的点数,并存储这些点的列表:
>>> n_point_circle = 0
>>> points_circle = []
  1. 对于每个随机点,计算到原点的距离。如果距离小于 1,则点落在圆内:
>>> for point in points:
...     r = torch.sqrt(point[0] ** 2 + point[1] ** 2)
...     if r <= 1:
...         points_circle.append(point)
...         n_point_circle += 1
  1. 统计圆内点的数量,并跟踪这些点:
>>> points_circle = torch.stack(points_circle)
  1. 绘制所有随机点,并对圆内的点使用不同的颜色:
>>> plt.plot(points[:, 0].numpy(), points[:, 1].numpy(), 'y.')
>>> plt.plot(points_circle[:, 0].numpy(), points_circle[:, 1].numpy(), 'c.')
  1. 绘制圆以获得更好的可视化效果:
>>> i = torch.linspace(0, 2 * math.pi)
>>> plt.plot(torch.cos(i).numpy(), torch.sin(i).numpy())
>>> plt.axes().set_aspect('equal')
>>> plt.show()
  1. 最后,计算π的值:
>>> pi_estimated = 4 * (n_point_circle / n_point)
>>> print('Estimated value of pi is:', pi_estimated)

工作原理是这样的...

第 5 步,您将看到以下图,其中的点是随机放置在圆内:

蒙特卡洛方法之所以如此强大,要归功于大数定律LLN)。根据大数定律,大量重复事件或动作的*均表现最终会收敛于期望值。在我们的情况下,大量随机点,4 * (n_point_circle / n_point) 最终会收敛于π的真实值。

最后,在第 8 步,我们打印π的估计值,得到以下结果:

Estimated value of pi is: 3.156 

使用蒙特卡洛方法*似计算π的值非常接*其真实值(3.14159...)。

还有更多内容...

我们可以通过比 1,000 次更多的迭代进一步改进我们的估计。在这里,我们将尝试 10,000 次迭代。在每次迭代中,我们在正方形内随机生成一个点,并检查它是否在圆内;根据落入圆内的点的比例,我们即时估算π的值。

然后我们将估计历史与π的真实值一起绘制。将它们放入以下函数中:

>>> def estimate_pi_mc(n_iteration):
...     n_point_circle = 0
...     pi_iteration = []
...     for i in range(1, n_iteration+1):
...         point = torch.rand(2) * 2 - 1
...         r = torch.sqrt(point[0] ** 2 + point[1] ** 2)
...         if r <= 1:
...             n_point_circle += 1
...         pi_iteration.append(4 * (n_point_circle / i))
...     plt.plot(pi_iteration)
...     plt.plot([math.pi] * n_iteration, '--')
...     plt.xlabel('Iteration')
...     plt.ylabel('Estimated pi')
...     plt.title('Estimation history')
...     plt.show()
...     print('Estimated value of pi is:', pi_iteration[-1]) The estimated value of pi is: 3.1364

然后我们使用 10,000 次迭代调用这个函数:

>>> estimate_pi_mc(10000)

参考以下图表查看估计历史的结果:

我们可以看到,随着更多的迭代次数,π的估计值越来越接*真实值。事件或行动总是存在一些变化。增加重复次数可以帮助*滑这种变化。

另请参阅

如果你对蒙特卡洛方法的更多应用感兴趣,这里有一些有趣的应用:

执行蒙特卡洛策略评估

在第二章,马尔可夫决策过程与动态规划中,我们应用 DP 进行策略评估,即策略的值(或状态值)函数。这确实效果很好,但也有一些限制。基本上,它需要完全了解环境,包括转移矩阵和奖励矩阵。然而,在大多数实际情况下,转移矩阵事先是未知的。需要已知 MDP 的强化学习算法被归类为基于模型的算法。另一方面,不需要先验知识的转移和奖励的算法被称为无模型算法。基于蒙特卡洛的强化学习是一种无模型方法。

在这个示例中,我们将使用蒙特卡洛方法评估值函数。我们再次使用 FrozenLake 环境作为示例,假设我们无法访问其转移和奖励矩阵。你会记得过程的返回,即长期内的总奖励,如下所示:

MC 策略评估使用经验均值返回而不是期望返回(如 DP 中)来估计值函数。有两种方法可以进行 MC 策略评估。一种是首次访问 MC 预测,它仅对状态 s 在一个 episode 中的第一次出现进行返回*均。另一种是每次访问 MC 预测,它对状态 s 在一个 episode 中的每次出现进行返回*均。显然,首次访问 MC 预测比每次访问版本计算要少得多,因此更频繁地使用。

如何做...

我们对 FrozenLake 的最优策略执行首次访问 MC 预测如下:

  1. 导入 PyTorch 和 Gym 库,并创建 FrozenLake 环境的实例:
>>> import torch
>>> import gym >>> env = gym.make("FrozenLake-v0")
  1. 要使用蒙特卡洛方法评估策略,我们首先需要定义一个函数,该函数模拟给定策略的 FrozenLake episode,并返回每个步骤的奖励和状态:
>>> def run_episode(env, policy):
 ...     state = env.reset()
 ...     rewards = []
 ...     states = [state]
 ...     is_done = False
 ...     while not is_done:
 ...         action = policy[state].item()
 ...         state, reward, is_done, info = env.step(action)
 ...         states.append(state)
 ...         rewards.append(reward)
 ...         if is_done:
 ...             break
 ...     states = torch.tensor(states)
 ...     rewards = torch.tensor(rewards)
 ...     return states, rewards

同样,在蒙特卡洛设置中,我们需要跟踪所有步骤的状态和奖励,因为我们无法访问完整的环境,包括转移概率和奖励矩阵。

  1. 现在,定义一个使用首次访问 MC 评估给定策略的函数:
>>> def mc_prediction_first_visit(env, policy, gamma, n_episode):
...     n_state = policy.shape[0]
...     V = torch.zeros(n_state)
...     N = torch.zeros(n_state)
...     for episode in range(n_episode):
...         states_t, rewards_t = run_episode(env, policy)
...         return_t = 0
...         first_visit = torch.zeros(n_state)
...         G = torch.zeros(n_state)
...         for state_t, reward_t in zip(reversed(states_t)[1:], 
                                            reversed(rewards_t)):
...             return_t = gamma * return_t + reward_t
...             G[state_t] = return_t
...             first_visit[state_t] = 1
...         for state in range(n_state):
...             if first_visit[state] > 0:
...                 V[state] += G[state]
...                 N[state] += 1
...     for state in range(n_state):
...         if N[state] > 0:
...             V[state] = V[state] / N[state]
...     return V
  1. 我们将折现率设定为 1 以便计算更加简便,并模拟了 10,000 个 episode:
>>> gamma = 1
>>> n_episode = 10000
  1. 我们使用前一章节中计算的最优策略,马尔可夫决策过程与动态规划,将其输入到首次访问 MC 函数中,同时还包括其他参数:
>>> optimal_policy = torch.tensor([0., 3., 3., 3., 0., 3., 2., 3., 3., 1., 0., 3., 3., 2., 1., 3.])
>>> value = mc_prediction_first_visit(env, optimal_policy, gamma, n_episode)
>>> print('The value function calculated by first-visit MC prediction:\n', value)
The value function calculated by first-visit MC prediction:
tensor([0.7463, 0.5004, 0.4938, 0.4602, 0.7463, 0.0000, 0.3914, 0.0000, 0.7463, 0.7469, 0.6797, 0.0000, 0.0000, 0.8038, 0.8911, 0.0000])

我们刚刚使用首次访问 MC 预测解决了最优策略的值函数。

它的工作原理...

第 3 步中,我们在 MC 预测中执行以下任务:

  • 我们运行n_episode个 episode

  • 对于每个 episode,我们计算每个状态的首次访问的返回

  • 对于每个状态,我们通过*均所有集的首次回报来获取值

正如您所看到的,在基于 MC 的预测中,并不需要了解环境的完整模型。事实上,在大多数真实情况下,过渡矩阵和奖励矩阵事先是未知的,或者极其难以获得。想象一下下棋或围棋中可能的状态数量以及可能的动作数量;几乎不可能计算出过渡矩阵和奖励矩阵。无模型强化学习是通过与环境交互从经验中学习的过程。

在我们的情况下,我们只考虑了可以观察到的内容,这包括每一步中的新状态和奖励,并使用 Monte Carlo 方法进行预测。请注意,我们模拟的集数越多,我们可以获得的预测越精确。如果您绘制每个集后更新的值,您将看到它如何随时间收敛,这与我们估计π值时看到的情况类似。

还有更多...

我们决定为冰湖的最优策略也执行每次访问的 MC 预测:

  1. 我们定义了使用每次访问 MC 评估给定策略的函数:
>>> def mc_prediction_every_visit(env, policy, gamma, n_episode):
...     n_state = policy.shape[0]
...     V = torch.zeros(n_state)
...     N = torch.zeros(n_state)
...     G = torch.zeros(n_state)
...     for episode in range(n_episode):
...         states_t, rewards_t = run_episode(env, policy)
...         return_t = 0
...         for state_t, reward_t in zip(reversed(states_t)[1:],  
                                            reversed(rewards_t)):
...             return_t = gamma * return_t + reward_t
...             G[state_t] += return_t
...             N[state_t] += 1
...     for state in range(n_state):
...         if N[state] > 0:
...             V[state] = G[state] / N[state]
...     return V

与首次访问 MC 类似,每次访问函数执行以下任务:

  • 运行n_episode

  • 对于每一集,它计算每次访问状态的回报

  • 对于每个状态,通过*均所有集的所有回报来获取值

  1. 通过在函数中输入策略和其他参数来计算值:
>>> value = mc_prediction_every_visit(env, optimal_policy, gamma, n_episode)
  1. 显示结果值:
>>> print('The value function calculated by every-visit MC prediction:\n', value)
The value function calculated by every-visit MC prediction:
tensor([0.6221, 0.4322, 0.3903, 0.3578, 0.6246, 0.0000, 0.3520, 0.0000, 0.6428, 0.6759, 0.6323, 0.0000, 0.0000, 0.7624, 0.8801, 0.0000])

使用 Monte Carlo 预测玩 21 点

在这个示例中,我们将玩 21 点(也称为 21),并评估我们认为可能有效的策略。您将通过 21 点的示例更加熟悉使用 Monte Carlo 预测,并准备在即将到来的示例中使用 Monte Carlo 控制搜索最优策略。

21 点是一种流行的纸牌游戏,目标是使牌的总和尽可能接* 21 点而不超过它。J、K 和 Q 牌的点数为 10,2 到 10 的牌的点数为 2 到 10。A 牌可以是 1 点或 11 点;选择后者称为可用A。玩家与庄家竞争。一开始,双方都会得到两张随机牌,但只有一张庄家的牌对玩家可见。玩家可以要求额外的牌(称为要牌)或停止接收更多的牌(称为停牌)。在玩家停牌之前,如果他们的牌的总和超过 21(称为爆牌),则玩家输。否则,如果庄家的牌总和超过 21,玩家赢。

如果两方都没有爆牌,得分最高的一方将获胜,或者可能是*局。Gym 中的 21 点环境如下所述:

  • Blackjack 有限 MDP 的一轮情节以每方两张牌开始,只有一张庄家的牌是可见的。

  • 一个情节以任一方获胜或双方*局结束。情节的最终奖励如下:如果玩家获胜,则为 +1;如果玩家输,则为 -1;如果*局,则为 0

  • 每轮中,玩家可以执行两个动作,要牌(1)和停牌(0),表示请求另一张牌和请求不再接收任何更多的牌。

我们首先尝试一个简单的策略,即只要总点数小于 18(或者您更喜欢的 19 或 20),就继续添加新的牌。

如何做到...

让我们从模拟 Blackjack 环境开始,并探索其状态和行动:

  1. 导入 PyTorch 和 Gym,并创建一个 Blackjack 实例:
>>> import torch
>>> import gym
>>> env = gym.make('Blackjack-v0')
  1. 重置环境:
>>> env.reset()
>>> env.reset()
(20, 5, False)

它返回三个状态变量:

  • 玩家的点数(本例中为 20

  • 庄家的点数(本例中为 5

  • 玩家是否拥有可重复使用的 Ace(本例中为 False

可重复使用的 Ace 意味着玩家拥有一个 Ace,可以将其计为 11 而不会爆牌。如果玩家没有 Ace,或者有 Ace 但使他们爆牌,状态参数将变为 False

查看以下情节:

>>> env.reset()
(18, 6, True)

18 点和 True 表示玩家有一个 Ace 和一个 7,Ace 被计为 11。

  1. 让我们执行一些动作,看看 Blackjack 环境的工作原理。首先,我们要牌(请求额外的一张牌),因为我们有可重复使用的 Ace,这提供了一些灵活性:
>>> env.step(1)
((20, 6, True), 0, False, {})

这返回三个状态变量 (20, 6, True),一个奖励(目前为0),以及该情节是否结束(目前为False)。

然后我们停止抽牌:

>>> env.step(0)
 ((20, 6, True), 1, True, {})

在这一情节中我们刚刚赢得了比赛,因此奖励为 1,现在情节结束了。再次提醒,一旦玩家选择 停牌,庄家将采取他们的行动。

  1. 有时我们会输掉;例如:
>>> env.reset()
(15, 10, False)
>>> env.step(1)
((25, 10, False), -1, True, {})

接下来,我们将预测一个简单策略的值,在该策略中,当分数达到 18 时停止添加新牌:

  1. 和往常一样,我们首先需要定义一个函数,模拟一个简单策略下的 Blackjack 情节:
>>> def run_episode(env, hold_score):
 ...     state = env.reset()
 ...     rewards = []
 ...     states = [state]
 ...     is_done = False
 ...     while not is_done:
 ...         action = 1 if state[0] < hold_score else 0
 ...         state, reward, is_done, info = env.step(action)
 ...         states.append(state)
 ...         rewards.append(reward)
 ...         if is_done:
 ...             break
 ...     return states, rewards
  1. 现在,我们定义一个评估简单的 Blackjack 策略的函数,使用首次访问 MC 方法:
>>> from collections import defaultdict
>>> def mc_prediction_first_visit(env, hold_score, gamma, 
                                                 n_episode):
 ...     V = defaultdict(float)
 ...     N = defaultdict(int)
 ...     for episode in range(n_episode):
 ...         states_t, rewards_t = run_episode(env, hold_score)
 ...         return_t = 0
 ...         G = {}
 ...         for state_t, reward_t in zip(states_t[1::-1], 
                                             rewards_t[::-1]):
 ...             return_t = gamma * return_t + reward_t
 ...             G[state_t] = return_t
 ...         for state, return_t in G.items():
 ...             if state[0] <= 21:
 ...                 V[state] += return_t
 ...                 N[state] += 1
 ...     for state in V:
 ...         V[state] = V[state] / N[state]
 ...     return V
  1. 我们将 hold_score 设置为 18,折扣率设置为 1,并模拟 500,000 个情节:
>>> hold_score = 18
>>> gamma = 1
>>> n_episode = 500000
  1. 现在,让我们通过插入所有变量来执行 MC 预测:
>>> value = mc_prediction_first_visit(env, hold_score, gamma, n_episode)

我们尝试打印结果值函数:

>>> print('The value function calculated by first-visit MC prediction:\n', value)

我们刚刚计算了所有可能状态的值:

>>> print('Number of states:', len(value))
Number of states: 280

总共有 280 个状态。

它的工作原理...

正如您所见,在 第 4 步,我们的点数超过了 21,所以我们输了。再次提醒,Blackjack 的状态实际上是一个三元组。第一个元素是玩家的分数;第二个元素是庄家牌堆中的明牌,其值可以是 1 到 10;第三个元素是是否拥有可重复使用的 Ace。

值得注意的是,在步骤 5中,在每一轮的一个回合中,代理根据当前分数是否停止,如果分数小于hold_score则停止,否则继续。同样,在蒙特卡罗设置中,我们跟踪所有步骤的状态和奖励。

执行步骤 8中的代码行,您将看到以下结果:

The value function calculated by first-visit MC prediction:
defaultdict(<class 'float'>, {(20, 6, False): 0.6923485653560042, (17, 5, False): -0.24390243902439024, (16, 5, False): -0.19118165784832453, (20, 10, False): 0.4326379146490474, (20, 7, False): 0.7686220540168588, (16, 6, False): -0.19249478804725503,
 ……
 ……
(5, 9, False): -0.20612244897959184, (12, 7, True): 0.058823529411764705, (6, 4, False): -0.26582278481012656, (4, 8, False): -0.14937759336099585, (4, 3, False): -0.1680327868852459, (4, 9, False): -0.20276497695852536, (4, 4, False): -0.3201754385964912, (12, 8, True): 0.11057692307692307})

我们刚刚体验了使用 MC 预测计算 21 点环境中 280 个状态的值函数的效果。在步骤 2中的 MC 预测函数中,我们执行了以下任务:

  • 我们在简单的 21 点策略下运行了n_episode轮次。

  • 对于每一轮次,我们计算了每个状态的首次访问的回报。

  • 对于每个状态,我们通过所有轮次的首次回报的*均值来获取价值。

请注意,我们忽略了玩家总分大于 21 分的状态,因为我们知道它们都会是-1。

21 点环境的模型,包括转移矩阵和奖励矩阵,在先验上是不知道的。此外,获取两个状态之间的转移概率是非常昂贵的。事实上,转移矩阵的大小将是 280 * 280 * 2,这将需要大量的计算。在基于 MC 的解决方案中,我们只需模拟足够的轮次,并且对于每一轮次,计算回报并相应地更新值函数即可。

下次你使用简单策略玩 21 点时(如果总分达到某一水*则停止),使用预测的值来决定每局游戏下注金额将会很有趣。

还有更多...

因为在这种情况下有很多状态,逐一读取它们的值是困难的。我们实际上可以通过制作三维表面图来可视化值函数。状态是三维的,第三个维度具有两个可能的选项(有可用有效王牌或无)。我们可以将我们的图分为两部分:一部分用于具有可用有效王牌的状态,另一部分用于没有可用有效王牌的状态。在每个图中,x轴是玩家的总和,y轴是庄家的明牌,z轴是值。

让我们按照这些步骤来创建可视化:

  1. 导入用于可视化的 matplotlib 中的所有必要模块:
>>> import matplotlib
>>> import matplotlib.pyplot as plt
>>> from mpl_toolkits.mplot3d import Axes3D
  1. 定义一个创建三维表面图的实用函数:
>>> def plot_surface(X, Y, Z, title):
...     fig = plt.figure(figsize=(20, 10))
...     ax = fig.add_subplot(111, projection='3d')
...     surf = ax.plot_surface(X, Y, Z, rstride=1, cstride=1,
...             cmap=matplotlib.cm.coolwarm, vmin=-1.0, vmax=1.0)
...     ax.set_xlabel('Player Sum')
...     ax.set_ylabel('Dealer Showing')
...     ax.set_zlabel('Value')
...     ax.set_title(title)
...     ax.view_init(ax.elev, -120)
...     fig.colorbar(surf)
...     plt.show()
  1. 接下来,我们定义一个函数,构建要在三个维度上绘制的数组,并调用plot_surface来可视化具有和不具有可用有效王牌的值:
>>> def plot_blackjack_value(V):
...     player_sum_range = range(12, 22)
...     dealer_show_range = range(1, 11)
...     X, Y = torch.meshgrid([torch.tensor(player_sum_range), 
                torch.tensor(dealer_show_range)])
...     values_to_plot = torch.zeros((len(player_sum_range),  
                                  len(dealer_show_range), 2))
...     for i, player in enumerate(player_sum_range):
...         for j, dealer in enumerate(dealer_show_range):
...             for k, ace in enumerate([False, True]):
...                 values_to_plot[i, j, k] = 
                               V[(player, dealer, ace)]
...     plot_surface(X, Y, values_to_plot[:,:,0].numpy(), 
                "Blackjack Value Function Without Usable Ace")
...     plot_surface(X, Y, values_to_plot[:,:,1].numpy(), 
                "Blackjack Value Function With Usable Ace")

我们只对玩家得分超过 11 分的状态感兴趣,并创建一个values_to_plot张量来存储这些数值。

  1. 最后,我们调用plot_blackjack_value函数:
>>> plot_blackjack_value(value)

无法使用有效王牌的状态的结果值图如下所示:

而对于有可用有效王牌的状态的值函数如下图所示:

随时调整hold_score的值并查看它如何影响值函数。

另请参阅

如果您对Blackjack环境还不熟悉,可以从源代码了解更多信息,位于github.com/openai/gym/blob/master/gym/envs/toy_text/blackjack.py

有时,阅读代码比阅读简单的英文描述更容易。

执行基于策略的 Monte Carlo 控制

在上一个示例中,我们预测了一种策略的值,其中代理如果得分达到 18 则持有。这是每个人都可以轻松提出的一种简单策略,尽管显然不是最优策略。在本示例中,我们将使用基于策略的 Monte Carlo 控制来寻找最优的 Blackjack 玩法策略。

Monte Carlo 预测用于评估给定策略的值,而Monte Carlo 控制MC 控制)用于在没有给定策略时寻找最优策略。基本上有两种类型的 MC 控制:基于策略和脱离策略。基于策略方法通过执行策略并评估和改进来学习最优策略,而脱离策略方法使用由另一个策略生成的数据来学习最优策略。基于策略 MC 控制的工作方式与动态规划中的策略迭代非常相似,有两个阶段,评估和改进:

  • 在评估阶段,它评估动作值函数(也称为行动值效用),而不是评估值函数(也称为状态值效用)。行动值更常被称为Q 函数,它是在给定策略下,通过在状态s中采取动作a来获得的状态-动作对(s, a)的效用。再次强调,评估可以以首次访问方式或每次访问方式进行。

  • 在改进阶段,通过为每个状态分配最优动作来更新策略:

最优策略将通过在大量迭代中交替两个阶段来获取。

如何做...

让我们通过以下步骤使用基于策略的 MC 控制来寻找最优的 Blackjack 策略:

  1. 导入必要的模块并创建一个 Blackjack 实例:
>>> import torch
>>> import gym
>>> env = gym.make('Blackjack-v0')
  1. 接下来,让我们开发一个函数,运行一个剧集并根据 Q 函数采取行动。这是改进阶段:
>>> def run_episode(env, Q, n_action):
 ...     """
 ...     Run a episode given a Q-function
 ...     @param env: OpenAI Gym environment
 ...     @param Q: Q-function
 ...     @param n_action: action space
 ...     @return: resulting states, actions and rewards for the entire episode
 ...     """
 ...     state = env.reset()
 ...     rewards = []
 ...     actions = []
 ...     states = []
 ...     is_done = False
 ...     action = torch.randint(0, n_action, [1]).item()
 ...     while not is_done:
 ...         actions.append(action)
 ...         states.append(state)
 ...         state, reward, is_done, info = env.step(action)
 ...         rewards.append(reward)
 ...         if is_done:
 ...             break
 ...         action = torch.argmax(Q[state]).item()
 ...     return states, actions, rewards
  1. 现在,我们开发了基于策略的 MC 控制算法:
 >>> from collections import defaultdict
 >>> def mc_control_on_policy(env, gamma, n_episode):
 ...     """
 ...     Obtain the optimal policy with on-policy MC control method
 ...     @param env: OpenAI Gym environment
 ...     @param gamma: discount factor
 ...     @param n_episode: number of episodes
 ...     @return: the optimal Q-function, and the optimal policy
 ...     """ ...     n_action = env.action_space.n
 ...     G_sum = defaultdict(float)
 ...     N = defaultdict(int)
 ...     Q = defaultdict(lambda: torch.empty(env.action_space.n))
 ...     for episode in range(n_episode):
 ...         states_t, actions_t, rewards_t = run_episode(env, Q, n_action) 
 ...         return_t = 0
 ...         G = {}
 ...         for state_t, action_t, reward_t in zip(states_t[::-1], 
                     actions_t[::-1], rewards_t[::-1]):
 ...             return_t = gamma * return_t + reward_t
 ...             G[(state_t, action_t)] = return_t
 ...         for state_action, return_t in G.items():
 ...             state, action = state_action
 ...             if state[0] <= 21:
 ...                 G_sum[state_action] += return_t
 ...                 N[state_action] += 1
 ...                 Q[state][action] = G_sum[state_action] 
                                         / N[state_action]
 ...      policy = {}
 ...      for state, actions in Q.items():
 ...          policy[state] = torch.argmax(actions).item()
 ...      return Q, policy
  1. 我们将折现率设为 1,并将使用 500,000 个剧集:
>>> gamma = 1
>>> n_episode = 500000
  1. 执行基于策略的 MC 控制,以获取最优的 Q 函数和策略:
>>> optimal_Q, optimal_policy = mc_control_on_policy(env, gamma, n_episode) >>> print(optimal_policy)
  1. 我们还可以计算最优策略的值函数,并打印出最优值如下:
>>> optimal_value = defaultdict(float)
>>> for state, action_values in optimal_Q.items():
 ...     optimal_value[state] = torch.max(action_values).item() >>> print(optimal_value)
  1. 使用plot_blackjack_value和我们在上一个示例使用 Monte Carlo 预测玩 Blackjack中开发的plot_surface函数来可视化值:
>>> plot_blackjack_value(optimal_value)

它是如何工作的...

在这个方案中,我们通过探索启动的 on-policy MC 控制解决了二十一点游戏。通过模拟每个剧集交替进行评估和改进,达到了我们的策略优化目标。

Step 2,我们运行一个剧集,并根据 Q 函数执行动作,具体任务如下:

  • 我们初始化一个剧集。

  • 我们以探索启动的方式随机选择一个动作。

  • 第一个动作后,我们根据当前 Q 函数采取动作,即 ![]。

  • 我们记录剧集中所有步骤的状态、动作和奖励,这将在评估阶段中使用。

需要注意的是,第一个动作是随机选择的,因为只有在这种情况下,MC 控制算法才会收敛到最优解。在 MC 算法中以随机动作开始一个剧集称为 探索启动

在探索启动设置中,为了确保策略收敛到最优解,一个剧集中的第一个动作是随机选择的。否则,一些状态将永远不会被访问,因此它们的状态-动作值永远不会被优化,最终策略将变得次优。

Step 2 是改进阶段,Step 3 是用于 MC 控制的阶段,在这个阶段我们执行以下任务:

  • 用任意小的值初始化 Q 函数。

  • 运行 n_episode 个剧集。

  • 对于每个剧集,执行策略改进,并获得状态、动作和奖励;并使用基于结果状态、动作和奖励的首次访问 MC 预测执行策略评估,更新 Q 函数。

  • 最后,最优 Q 函数完成并且通过在最优 Q 函数中为每个状态选择最佳动作获得最优策略。

在每次迭代中,我们通过采取相对于当前动作值函数 Q 的最优动作来使策略贪婪化(即 ![])。因此,我们将能够获得一个最优策略,即使我们从任意策略开始。

Step 5,您可以看到最优策略的结果如下:

{(16, 8, True): 1, (11, 2, False): 1, (15, 5, True): 1, (14, 9, False): 1, (11, 6, False): 1, (20, 3, False): 0, (9, 6, False): 0, (12, 9, False): 0, (21, 2, True): 0, (16, 10, False): 1, (17, 5, False): 0, (13, 10, False): 1, (12, 10, False): 1, (14, 10, False): 0, (10, 2, False): 1, (20, 4, False): 0, (11, 4, False): 1, (16, 9, False): 0, (10, 8,
 ……
 ……
1, (18, 6, True): 0, (12, 2, True): 1, (8, 3, False): 1, (13, 3, True): 0, (4, 7, False): 1, (18, 8, True): 0, (6, 5, False): 1, (17, 6, True): 0, (19, 9, True): 0, (4, 4, False): 0, (14, 5, True): 1, (12, 6, True): 0, (4, 9, False): 1, (13, 4, True): 1, (4, 8, False): 1, (14, 3, True): 1, (12, 4, True): 1, (4, 6, False): 0, (12, 5, True): 0, (4, 2, False): 1, (4, 3, False): 1, (5, 4, False): 1, (4, 1, False): 0}

Step 6,您可以看到最优策略的结果值如下:

{(21, 8, False): 0.9262458682060242, (11, 8, False): 0.16684606671333313, (16, 10, False): -0.4662476181983948, (16, 10, True): -0.3643564283847809, (14, 8, False): -0.2743947207927704, (13, 10, False): -0.3887477219104767, (12, 9, False): -0.22795115411281586
 ……
 ……
(4, 3, False): -0.18421052396297455, (4, 8, False): -0.16806723177433014, (13, 2, True): 0.05485232174396515, (5, 5, False): -0.09459459781646729, (5, 8, False): -0.3690987229347229, (20, 2, True): 0.6965699195861816, (17, 2, True): -0.09696969389915466, (12, 2, True): 0.0517241396009922}

Step 7,您将看到没有可用 Ace 的状态的结果值图如下:

并且对于有可用 Ace 的状态,其值函数如下所示:

还有更多...

您可能想知道最优策略是否比简单策略表现更好。现在让我们在最优策略和简单策略下模拟 100,000 个二十一点剧集。我们将比较两种策略的获胜和失败的机会:

  1. 首先,我们定义一个简单的策略,当分数达到 18 时采取 stick 动作:
>>> hold_score = 18
>>> hold_policy = {}
>>> player_sum_range = range(2, 22)
>>> for player in range(2, 22):
...     for dealer in range(1, 11):
...         action = 1 if player < hold_score else 0
...         hold_policy[(player, dealer, False)] = action
...         hold_policy[(player, dealer, True)] = action
  1. 接下来,我们定义一个包装函数,根据给定的策略运行一个剧集,并返回最终的奖励:
>>> def simulate_episode(env, policy):
...     state = env.reset()
...     is_done = False
...     while not is_done:
...         action = policy[state]
...         state, reward, is_done, info = env.step(action)
...         if is_done:
...             return reward
  1. 然后,我们指定回合数(100,000),并开始计算胜利和失败的次数:
>>> n_episode = 100000
>>> n_win_optimal = 0
>>> n_win_simple = 0
>>> n_lose_optimal = 0
>>> n_lose_simple = 0
  1. 然后,我们运行了 100,000 个回合并跟踪了胜利和失败的情况:
>>> for _ in range(n_episode):
...     reward = simulate_episode(env, optimal_policy)
...     if reward == 1:
...         n_win_optimal += 1
...     elif reward == -1:
...         n_lose_optimal += 1
...     reward = simulate_episode(env, hold_policy)
...     if reward == 1:
...         n_win_simple += 1
...     elif reward == -1:
...         n_lose_simple += 1
  1. 最后,我们打印出我们得到的结果:
>>> print('Winning probability under the simple policy: {}'.format(n_win_simple/n_episode))
Winning probability under the simple policy: 0.39923
>>> print('Winning probability under the optimal policy: {}'.format(n_win_optimal/n_episode))
Winning probability under the optimal policy: 0.41281

在最优策略下玩牌有 41.28%的赢的可能性,而在简单策略下玩牌有 39.92%的可能性。然后,我们有输的概率:

>>> print('Losing probability under the simple policy: {}'.format(n_lose_simple/n_episode))
Losing probability under the simple policy: 0.51024
>>> print('Losing probability under the optimal policy: {}'.format(n_lose_optimal/n_episode))
Losing probability under the optimal policy: 0.493

另一方面,在最优策略下玩牌有 49.3%的输的可能性,而在简单策略下玩牌有 51.02%的可能性。

我们的最优策略显然是赢家!

开发 MC 控制和ε-贪心策略

在前一步骤中,我们使用 MC 控制和贪心搜索搜索最优策略,选择具有最高状态-动作值的动作。然而,在早期回合中的最佳选择并不保证最优解。如果我们只关注临时的最佳选项并忽略整体问题,我们将陷入局部最优解而无法达到全局最优解。解决方法是ε-贪心策略

ε-贪心策略的 MC 控制中,我们不再始终利用最佳动作,而是在一定概率下随机选择动作。顾名思义,该算法有两个方面:

  • ε:给定参数ε,其值从01,每个动作的选择概率如下计算:

这里,|A| 是可能的动作数。

  • 贪心:偏爱具有最高状态-动作值的动作,并且它被选择的概率增加了1-ε

ε-贪心策略大部分时间都会利用最佳动作,同时也会时不时地探索不同的动作。

如何操作...

让我们使用ε-贪心策略解决 Blackjack 环境:

  1. 导入必要的模块并创建一个 Blackjack 实例:
>>> import torch
>>> import gym
>>> env = gym.make('Blackjack-v0')
  1. 接下来,让我们开发一个运行回合并执行ε-贪心的函数:
>>> def run_episode(env, Q, epsilon, n_action):
...     """
...     Run a episode and performs epsilon-greedy policy
...     @param env: OpenAI Gym environment
...     @param Q: Q-function
...     @param epsilon: the trade-off between exploration and exploitation
...     @param n_action: action space
...     @return: resulting states, actions and rewards for the entire episode
...     """
...     state = env.reset()
...     rewards = []
...     actions = []
...     states = []
...     is_done = False
...     while not is_done:
...         probs = torch.ones(n_action) * epsilon / n_action
...         best_action = torch.argmax(Q[state]).item()
...         probs[best_action] += 1.0 - epsilon
...         action = torch.multinomial(probs, 1).item()
...         actions.append(action)
...         states.append(state)
...         state, reward, is_done, info = env.step(action)
...         rewards.append(reward)
...         if is_done:
...             break
...     return states, actions, rewards
  1. 现在,开发 on-policy MC 控制和ε-贪心策略
>>> from collections import defaultdict
>>> def mc_control_epsilon_greedy(env, gamma, n_episode, epsilon):
...     """
...     Obtain the optimal policy with on-policy MC control with epsilon_greedy
...     @param env: OpenAI Gym environment
...     @param gamma: discount factor
...     @param n_episode: number of episodes
...     @param epsilon: the trade-off between exploration and exploitation
...     @return: the optimal Q-function, and the optimal policy
...     """
...     n_action = env.action_space.n
...     G_sum = defaultdict(float)
...     N = defaultdict(int)
...     Q = defaultdict(lambda: torch.empty(n_action))
...     for episode in range(n_episode):
...         states_t, actions_t, rewards_t = 
                     run_episode(env, Q, epsilon, n_action)
...         return_t = 0
...         G = {}
...         for state_t, action_t, reward_t in zip(states_t[::-1], 
                                 actions_t[::-1], rewards_t[::-1]):
...             return_t = gamma * return_t + reward_t
...             G[(state_t, action_t)] = return_t
...         for state_action, return_t in G.items():
...             state, action = state_action
...             if state[0] <= 21:
...                 G_sum[state_action] += return_t
...                 N[state_action] += 1
...                 Q[state][action] = 
                         G_sum[state_action] / N[state_action]
...     policy = {}
...     for state, actions in Q.items():
...         policy[state] = torch.argmax(actions).item()
...     return Q, policy
  1. 我们将折扣率指定为 1,ε指定为 0.1,并将使用 500,000 个回合:
>>> gamma = 1
>>> n_episode = 500000
>>> epsilon = 0.1
  1. 执行 MC 控制和ε-贪心策略以获取最优的 Q 函数和策略:
>>> optimal_Q, optimal_policy = mc_control_epsilon_greedy(env, gamma, n_episode, epsilon)

随意打印出最优值,并使用我们开发的plot_blackjack_valueplot_surface函数进行可视化。我们此处不再重复该过程。

  1. 最后,我们想知道ε-贪心方法是否真的效果更好。再次,我们模拟了 100,000 个 Blackjack 的回合,在ε-贪心生成的最优策略下计算赢和输的概率:
>>> n_episode = 100000
>>> n_win_optimal = 0
>>> n_lose_optimal = 0
>>> for _ in range(n_episode):
...     reward = simulate_episode(env, optimal_policy)
...     if reward == 1:
...         n_win_optimal += 1
...     elif reward == -1:
...         n_lose_optimal += 1

这里,我们重新使用了上一个示例中的simulate_episode函数。

工作原理...

在这个示例中,我们使用ε-贪心的 on-policy MC 控制解决了 Blackjack 游戏。

第 2 步中,我们运行一个回合,并执行ε-贪心,完成以下任务:

  • 我们初始化一个回合。

  • 我们计算选择各个动作的概率:基于当前 Q 函数的最佳动作的概率为![],否则的概率为![]。

  • 我们记录了每一集中所有步骤的状态、动作和奖励,这将在评估阶段中使用。

ε贪心方法通过以![]的概率利用最佳动作,并同时允许以![]的概率随机探索其他动作。超参数ε是利用与探索之间的权衡。如果其值为 0,则算法完全贪婪;如果值为 1,则每个动作均匀选择,因此算法只进行随机探索。

ε的值需要根据实验进行调整,没有一个适用于所有实验的通用值。话虽如此,一般来说,我们可以选择 0.1、0.2 或 0.3 作为起点。另一种方法是从稍大的值(如 0.5 或 0.7)开始,并逐渐减少(例如每集减少 0.999)。通过这种方式,策略将在开始时专注于探索不同的动作,并随着时间的推移,趋向于利用好的动作。

最后,在执行步骤 6、对来自 10 万集的结果进行*均并打印获胜概率后,我们现在有以下结果:

>>> print('Winning probability under the optimal policy: {}'.format(n_win_optimal/n_episode))
Winning probability under the optimal policy: 0.42436

通过ε贪心方法得到的最优策略具有 42.44%的获胜机率,比没有ε贪心的获胜机率(41.28%)要高。

然后,我们还打印出了失利概率:

>>> print('Losing probability under the optimal policy: {}'.format(n_lose_optimal/n_episode))
Losing probability under the optimal policy: 0.48048

如您所见,ε贪心方法具有较低的失利机率(48.05%与没有ε贪心的 49.3%相比)。

执行离策略蒙特卡洛控制

另一种基于 MC 的方法来解决 MDP 是离策略控制,我们将在这个章节中讨论。

离策略方法通过由另一个称为行为策略b 生成的数据来优化目标策略π。目标策略始终进行利用,而行为策略则用于探索目的。这意味着目标策略在当前 Q 函数的贪婪方面是贪婪的,而行为策略生成行为以便目标策略有数据可学习。行为策略可以是任何东西,只要所有状态的所有动作都能以非零概率选择,这保证了行为策略可以探索所有可能性。

由于我们在离策略方法中处理两种不同的策略,我们只能在两种策略中发生的剧集中使用共同步骤。这意味着我们从行为策略下执行的最新步骤开始,其行动与贪婪策略下执行的行动不同。为了了解另一个策略的目标策略,并使用一种称为重要性抽样的技术,这种技术通常用于估计在给定从不同分布生成的样本下的预期值。状态-动作对的加权重要性计算如下:

这里,π(ak | sk)是在目标策略下在状态sk中采取动作ak的概率;bak | sk)是在行为策略下的概率;权重wt是从步骤t到剧集结束时那两个概率的比率的乘积。权重wt应用于步骤t的回报。

如何做...

让我们使用以下步骤来搜索使用离策略蒙特卡洛控制的最优 21 点策略:

  1. 导入必要的模块并创建一个 21 点实例:
>>> import torch
>>> import gym
>>> env = gym.make('Blackjack-v0')
  1. 我们首先定义行为策略,它在我们的情况下随机选择一个动作:
>>> def gen_random_policy(n_action):
...     probs = torch.ones(n_action) / n_action
...     def policy_function(state):
...         return probs
...     return policy_function
>>> random_policy = gen_random_policy(env.action_space.n)

行为策略可以是任何东西,只要它以非零概率选择所有状态中的所有动作。

  1. 接下来,让我们开发一个函数,运行一个剧集,并在行为策略下执行动作:
>>> def run_episode(env, behavior_policy):
...     """
...     Run a episode given a behavior policy
...     @param env: OpenAI Gym environment
...     @param behavior_policy: behavior policy
...     @return: resulting states, actions and rewards for the entire episode
...     """
...     state = env.reset()
...     rewards = [] ...     actions = []
...     states = []
...     is_done = False
...     while not is_done:
...         probs = behavior_policy(state)
...         action = torch.multinomial(probs, 1).item()
...         actions.append(action)
...         states.append(state)
...         state, reward, is_done, info = env.step(action)
...         rewards.append(reward)
...         if is_done:
...             break
...     return states, actions, rewards

这记录了剧集中所有步骤的状态、动作和奖励,这将作为目标策略的学习数据使用。

  1. 现在,我们将开发离策略蒙特卡洛控制算法:
>>> from collections import defaultdict
>>> def mc_control_off_policy(env, gamma, n_episode, behavior_policy):
...     """
...     Obtain the optimal policy with off-policy MC control method
...     @param env: OpenAI Gym environment
...     @param gamma: discount factor
...     @param n_episode: number of episodes
...     @param behavior_policy: behavior policy
...     @return: the optimal Q-function, and the optimal policy
...     """
...     n_action = env.action_space.n
...     G_sum = defaultdict(float)
...     N = defaultdict(int)
...     Q = defaultdict(lambda: torch.empty(n_action))
...     for episode in range(n_episode):
...         W = {}
...         w = 1
...         states_t, actions_t, rewards_t = 
                     run_episode(env, behavior_policy)
...         return_t = 0 ...         G = {}
...         for state_t, action_t, reward_t in zip(states_t[::-1], 
                                 actions_t[::-1], rewards_t[::-1]):
...             return_t = gamma * return_t + reward_t
...             G[(state_t, action_t)] = return_t
...             if action_t != torch.argmax(Q[state_t]).item():
...                 break
...             w *= 1./ behavior_policy(state_t)[action_t]
...         for state_action, return_t in G.items():
...             state, action = state_action
...             if state[0] <= 21:
...                 G_sum[state_action] += 
                                 return_t * W[state_action]
...                 N[state_action] += 1
...                 Q[state][action] = 
                             G_sum[state_action] / N[state_action]
...     policy = {}
...     for state, actions in Q.items():
...         policy[state] = torch.argmax(actions).item()
...     return Q, policy
  1. 我们将折现率设为 1,并将使用 500,000 个剧集:
>>> gamma = 1
>>> n_episode = 500000
  1. 使用random_policy行为策略执行离策略蒙特卡洛控制以获取最优 Q 函数和策略:
>>> optimal_Q, optimal_policy = mc_control_off_policy(env, gamma, n_episode, random_policy)

它是如何工作的...

在这个示例中,我们使用离策略蒙特卡洛解决 21 点游戏。

步骤 4中,离策略蒙特卡洛控制算法执行以下任务:

  • 它用任意小的值初始化 Q 函数。

  • 它运行n_episode个剧集。

  • 对于每个剧集,它执行行为策略以生成状态、动作和奖励;它使用基于共同步骤的首次访问蒙特卡洛预测对目标策略进行策略评估;并根据加权回报更新 Q 函数。

  • 最后,最优 Q 函数完成,并且通过在最优 Q 函数中为每个状态选择最佳动作来获取最优策略。

它通过观察另一个代理并重复使用从另一个策略生成的经验来学习目标策略。目标策略以贪婪方式优化,而行为策略则继续探索不同的选项。它将行为策略的回报与目标策略中其概率的重要性比率*均起来。你可能会想知道为什么在重要性比率 wt 的计算中,π (ak | sk) 总是等于 1。回想一下,我们只考虑在行为策略和目标策略下采取的共同步骤,并且目标策略总是贪婪的。因此,π (a | s) = 1 总是成立。

还有更多内容…

我们可以实际上以增量方式实现蒙特卡洛方法。在一个 episode 中,我们可以即时计算 Q 函数,而不是为每个首次出现的状态-动作对存储回报和重要性比率。在非增量方式中,Q 函数在 n 个 episode 中的所有存储回报最终计算出来:

而在增量方法中,Q 函数在每个 episode 的每个步骤中更新如下:

增量等价版本更高效,因为它减少了内存消耗并且更具可扩展性。让我们继续实施它:

>>> def mc_control_off_policy_incremental(env, gamma, n_episode, behavior_policy):
...     n_action = env.action_space.n
...     N = defaultdict(int)
...     Q = defaultdict(lambda: torch.empty(n_action))
...     for episode in range(n_episode):
...         W = 1.
...         states_t, actions_t, rewards_t = 
                             run_episode(env, behavior_policy)
...         return_t = 0.
...         for state_t, action_t, reward_t in 
                     zip(states_t[::-1], actions_t[::-1], rewards_t[::-1]):
...             return_t = gamma * return_t + reward_t
...             N[(state_t, action_t)] += 1
...             Q[state_t][action_t] += (W / N[(state_t, action_t)]) * (return_t - Q[state_t][action_t])
...             if action_t != torch.argmax(Q[state_t]).item():
...                 break
...             W *= 1./ behavior_policy(state_t)[action_t]
...     policy = {}
...     for state, actions in Q.items():
...         policy[state] = torch.argmax(actions).item()
...     return Q, policy

我们可以调用此增量版本来获得最优策略:

>>> optimal_Q, optimal_policy = mc_control_off_policy_incremental(env, gamma, n_episode, random_policy)

另请参阅

欲了解重要性抽样的详细解释,请参考以下完美资源:

statweb.stanford.edu/~owen/mc/Ch-var-is.pdf

使用加权重要性抽样开发蒙特卡洛控制

在上一个示例中,我们简单地使用了行为策略的回报与目标策略中其概率的重要性比率的*均值。这种技术在形式上称为普通重要性抽样。众所周知,它具有很高的方差,因此我们通常更喜欢重要性抽样的加权版本,在本示例中我们将讨论这一点。

加权重要性抽样与普通重要性抽样的不同之处在于它在*均回报方面采用了加权*均值:

它通常与普通版本相比具有更低的方差。如果您对二十一点游戏尝试过普通重要性抽样,您会发现每次实验结果都不同。

如何做…

让我们通过以下步骤使用加权重要性抽样来解决二十一点游戏的离策略蒙特卡洛控制问题:

  1. 导入必要的模块并创建一个二十一点实例:
>>> import torch
>>> import gym
>>> env = gym.make('Blackjack-v0')
  1. 我们首先定义行为策略,该策略在我们的情况下随机选择一个动作:
>>> random_policy = gen_random_policy(env.action_space.n)
  1. 接下来,我们重复使用run_episode函数,该函数在行为策略下运行一个 episode 并采取行动。

  2. 现在,我们使用加权重要性抽样来开发离策略蒙特卡洛控制算法:

>>> from collections import defaultdict
>>> def mc_control_off_policy_weighted(env, gamma, n_episode, behavior_policy):
...     """
...     Obtain the optimal policy with off-policy MC control method with weighted importance sampling
...     @param env: OpenAI Gym environment
...     @param gamma: discount factor
...     @param n_episode: number of episodes
...     @param behavior_policy: behavior policy
...     @return: the optimal Q-function, and the optimal policy
...     """
...     n_action = env.action_space.n
...     N = defaultdict(float)
...     Q = defaultdict(lambda: torch.empty(n_action))
...     for episode in range(n_episode):
...         W = 1.
...         states_t, actions_t, rewards_t = 
                             run_episode(env, behavior_policy)
...         return_t = 0.
...         for state_t, action_t, reward_t in zip(states_t[::-1], 
                                 actions_t[::-1], rewards_t[::-1]):
...             return_t = gamma * return_t + reward_t
...             N[(state_t, action_t)] += W
...             Q[state_t][action_t] += (W / N[(state_t, action_t)]) 
                                 * (return_t - Q[state_t][action_t])
...             if action_t != torch.argmax(Q[state_t]).item():
...                 break
...             W *= 1./ behavior_policy(state_t)[action_t]
...     policy = {}
...     for state, actions in Q.items():
...         policy[state] = torch.argmax(actions).item()
...     return Q, policy

注意这是离策略蒙特卡洛控制的增量版本。

  1. 我们将折扣率设定为 1,并将使用 500,000 个情节:
>>> gamma = 1
>>> n_episode = 500000
  1. 使用random_policy行为策略执行离策略蒙特卡洛控制,以获取最优的 Q 函数和策略:
>>> optimal_Q, optimal_policy = mc_control_off_policy_weighted(env, gamma, n_episode, random_policy)

工作原理如下...

我们在这个示例中使用了带有加权重要性抽样的离策略蒙特卡洛控制来解决二十一点问题。这与普通重要性抽样非常相似,但不是通过比率来缩放回报和*均结果,而是使用加权*均值来缩放回报。实际上,加权重要性抽样的方差比普通重要性抽样低得多,因此被强烈推荐使用。

还有更多...

最后,为什么不模拟一些情节,看看在生成的最优策略下获胜和失败的机会如何?

我们重复使用了我们在执行策略蒙特卡洛控制食谱中开发的simulate_episode函数,并模拟了 100,000 个情节:

>>> n_episode = 100000
>>> n_win_optimal = 0
>>> n_lose_optimal = 0
>>> for _ in range(n_episode):
...     reward = simulate_episode(env, optimal_policy)
...     if reward == 1:
...         n_win_optimal += 1
...     elif reward == -1:
...         n_lose_optimal += 1

然后,我们打印出我们得到的结果:

>>> print('Winning probability under the optimal policy: {}'.format(n_win_optimal/n_episode))
Winning probability under the optimal policy: 0.43072
>>> print('Losing probability under the optimal policy: {}'.format(n_lose_optimal/n_episode))
Losing probability under the optimal policy: 0.47756

另请参阅

有关加权重要性抽样优于普通重要性抽样的证明,请随时查看以下内容:

  • Hesterberg, T. C., 进展中的重要性抽样, 统计系, 斯坦福大学, 1988

  • Casella, G., Robert, C. P., Post-processing accept-reject samples: recycling and rescaling. 计算与图形统计杂志, 7(2):139–157, 1988

  • Precup, D., Sutton, R. S., Singh, S., 离策略策略评估的资格痕迹。在第 17 届国际机器学习大会上的论文集, pp. 759766, 2000

第四章:时间差分和 Q-learning

在上一章中,我们通过蒙特卡洛方法解决了马尔可夫决策过程(MDP),这是一种无模型方法,不需要环境的先验知识。然而,在 MC 学习中,价值函数和 Q 函数通常在情节结束之前更新。这可能存在问题,因为有些过程非常长,甚至无法正常结束。在本章中,我们将采用时间差分TD)方法来解决这个问题。在 TD 方法中,我们在每个时间步更新动作值,显著提高了学习效率。

本章将从设置 Cliff Walking 和 Windy Gridworld 环境的游乐场开始,这些将作为本章 TD 控制方法的主要讨论点。通过我们的逐步指南,读者将获得 Q-learning 用于离策略控制和 SARSA 用于在策略控制的实际经验。我们还将处理一个有趣的项目——出租车问题,并展示如何分别使用 Q-learning 和 SARSA 算法来解决它。最后,我们将额外介绍双 Q-learning 算法。

我们将包括以下的步骤:

  • 设置 Cliff Walking 环境的游乐场

  • 开发 Q-learning 算法

  • 设置 Windy Gridworld 环境的游乐场

  • 开发 SARSA 算法

  • 用 Q-learning 解决出租车问题

  • 用 SARSA 解决出租车问题

  • 开发 Double Q-learning 算法

设置 Cliff Walking 环境的游乐场

在第一个步骤中,我们将开始熟悉 Cliff Walking 环境,我们将在后续步骤中使用 TD 方法来解决它。

Cliff Walking 是一个典型的 gym 环境,具有长时间的不确定结束的情节。这是一个 4 * 12 的网格问题。一个代理在每一步可以向上、向右、向下和向左移动。左下角的方块是代理的起点,右下角是获胜的点,如果到达则会结束一个情节。最后一行剩余的方块是悬崖,代理踩上其中任何一个后会被重置到起始位置,但情节仍继续。每一步代理走的时候会产生一个 -1 的奖励,除非踩到悬崖,那么会产生 -100 的奖励。

准备工作

要运行 Cliff Walking 环境,首先在github.com/openai/gym/wiki/Table-of-environments表格中搜索它的名称。我们得到 CliffWalking-v0,并且知道观察空间由整数表示,范围从 0(左上角方块)到 47(右下角目标方块),有四个可能的动作(上 = 0,右 = 1,下 = 2,左 = 3)。

怎么做...

让我们通过以下步骤模拟 Cliff Walking 环境:

  1. 我们导入 Gym 库,并创建一个 Cliff Walking 环境的实例:
>>> import gym
>>> env = gym.make("CliffWalking-v0")
>>> n_state = env.observation_space.n
>>> print(n_state)
48
>>> n_action = env.action_space.n
>>> print(n_action)
4
  1. 然后,我们重置环境:
>>> env.reset()
 0

代理从状态 36 开始,作为左下角的瓷砖。

  1. 然后,我们渲染环境:
>>> env.render()
  1. 现在,无论是否可行走,让我们进行一个向下的移动:
>>> new_state, reward, is_done, info = env.step(2)
>>> env.render()
 o  o  o  o  o  o  o  o  o  o  o  o
 o  o  o  o  o  o  o  o  o  o  o  o
 o  o  o  o  o  o  o  o  o  o  o  o
 x  C  C  C  C  C  C  C  C  C  C  T

代理保持不动。现在,打印出我们刚刚获得的内容:

>>> print(new_state)
36
>>> print(reward)
-1

再次,每个移动都会导致 -1 的奖励:

>>> print(is_done)
 False

该 episode 还没有完成,因为代理人还没有达到目标:

>>> print(info)
 {'prob': 1.0}

这意味着移动是确定性的。

现在,让我们执行一个向上的移动,因为它是可行走的:

>>> new_state, reward, is_done, info = env.step(0)
>>> env.render()
 o  o  o  o  o  o  o  o  o  o  o  o
 o  o  o  o  o  o  o  o  o  o  o  o
 x  o  o  o  o  o  o  o  o  o  o  o
 o  C  C  C  C  C  C  C  C  C  C  T

打印出我们刚刚获得的内容:

>>> print(new_state)
 24

代理人向上移动:

>>> print(reward)
 -1

这导致 -1 的奖励。

  1. 现在让我们尝试向右和向下移动:
>>> new_state, reward, is_done, info = env.step(1)
>>> new_state, reward, is_done, info = env.step(2)
>>> env.render()
 o  o  o  o  o  o  o  o  o  o  o  o
 o  o  o  o  o  o  o  o  o  o  o  o
 o  o  o  o  o  o  o  o  o  o  o  o
 x  C  C  C  C  C  C  C  C  C  C  T

代理人踩到了悬崖,因此被重置到起点并获得了 -100 的奖励:

>>> print(new_state)
36
>>> print(reward)
-100
>>> print(is_done)
False
  1. 最后,让我们尝试以最短路径达到目标:
 >>> new_state, reward, is_done, info = env.step(0)
 >>> for _ in range(11):
 ...     env.step(1)
 >>> new_state, reward, is_done, info = env.step(2)
 >>> env.render()
 o  o  o  o  o  o  o  o  o  o  o  o
 o  o  o  o  o  o  o  o  o  o  o  o
 o  o  o  o  o  o  o  o  o  o  o  o
 o  C  C  C  C  C  C  C  C  C  C  x
 >>> print(new_state)
 47
 >>> print(reward)
 -1
 >>> print(is_done)
 True

工作原理...

步骤 1中,我们导入 Gym 库并创建 Cliff Walking 环境的实例。然后,在步骤 2中重置环境。

步骤 3中,我们渲染环境,你会看到一个 4 * 12 的矩阵如下,表示一个网格,其中包括起始瓷砖(x)代表代理人所站的位置,目标瓷砖(T),10 个悬崖瓷砖(C),以及常规瓷砖(o):

步骤 456中,我们进行了各种移动,并看到了这些移动的各种结果和收到的奖励。

如你所想象的,Cliff Walking 的一个场景可能会非常长,甚至是无限的,因为一旦踩到悬崖就会重置游戏。尽早达到目标是更好的,因为每走一步都会导致奖励为 -1 或者 -100。在下一个实例中,我们将通过时间差分方法解决 Cliff Walking 问题。

开发 Q-learning 算法

时间差分(TD)学习也是一种无模型学习算法,就像 MC 学习一样。你会记得,在 MC 学习中,Q 函数在整个 episode 结束时更新(无论是首次访问还是每次访问模式)。TD 学习的主要优势在于它在 episode 中的每一步都更新 Q 函数。

在这个示例中,我们将介绍一种名为Q-learning的流行时间差分方法。Q-learning 是一种离策略学习算法。它根据以下方程更新 Q 函数:

这里,s' 是采取动作 a 后的结果状态 s;r 是相关的奖励;α 是学习率;γ 是折扣因子。此外,![] 意味着行为策略是贪婪的,选择状态 s' 中最高的 Q 值来生成学习数据。在 Q-learning 中,动作是根据 epsilon-greedy 策略执行的。

如何实现...

我们通过 Q-learning 解决 Cliff Walking 环境如下:

  1. 导入 PyTorch 和 Gym 库,并创建 Cliff Walking 环境的实例:
>>> import torch
>>> import gym >>> env = gym.make("CliffWalking-v0")
>>> from collections import defaultdict
  1. 让我们从定义 epsilon-greedy 策略开始:
>>> def gen_epsilon_greedy_policy(n_action, epsilon):
 ...     def policy_function(state, Q):
 ...         probs = torch.ones(n_action) * epsilon / n_action
 ...         best_action = torch.argmax(Q[state]).item()
 ...         probs[best_action] += 1.0 - epsilon
 ...         action = torch.multinomial(probs, 1).item()
 ...         return action
 ...     return policy_function
  1. 现在定义执行 Q-learning 的函数:
>>> def q_learning(env, gamma, n_episode, alpha):
 ...     """
 ...     Obtain the optimal policy with off-policy Q-learning method
 ...     @param env: OpenAI Gym environment
 ...     @param gamma: discount factor
 ...     @param n_episode: number of episodes
 ...     @return: the optimal Q-function, and the optimal policy
 ...     """
 ...     n_action = env.action_space.n
 ...     Q = defaultdict(lambda: torch.zeros(n_action))
 ...     for episode in range(n_episode):
 ...         state = env.reset()
 ...         is_done = False
 ...         while not is_done:
 ...             action = epsilon_greedy_policy(state, Q)
 ...             next_state, reward, is_done, info = 
                                         env.step(action)
 ...             td_delta = reward + 
                             gamma * torch.max(Q[next_state])
                             - Q[state][action]
 ...             Q[state][action] += alpha * td_delta
 ...             if is_done:
 ...                 break
 ...             state = next_state
 ...     policy = {}
 ...     for state, actions in Q.items():
 ...         policy[state] = torch.argmax(actions).item()
 ...     return Q, policy
  1. 我们将折扣率设为1,学习率设为0.4,ε设为0.1;然后模拟 500 个回合:
>>> gamma = 1
>>> n_episode = 500
>>> alpha = 0.4
>>> epsilon = 0.1
  1. 接下来,我们创建ε-贪心策略的一个实例:
>>> epsilon_greedy_policy = gen_epsilon_greedy_policy(env.action_space.n, epsilon)
  1. 最后,我们使用之前定义的输入参数执行 Q 学习,并打印出最优策略:
>>> optimal_Q, optimal_policy = q_learning(env, gamma, n_episode, alpha) >>> print('The optimal policy:\n', optimal_policy)
 The optimal policy:
 {36: 0, 24: 1, 25: 1, 13: 1, 12: 2, 0: 3, 1: 1, 14: 2, 2: 1, 26: 1, 15: 1, 27: 1, 28: 1, 16: 2, 4: 2, 3: 1, 29: 1, 17: 1, 5: 0, 30: 1, 18: 1, 6: 1, 19: 1, 7: 1, 31: 1, 32: 1, 20: 2, 8: 1, 33: 1, 21: 1, 9: 1, 34: 1, 22: 2, 10: 2, 23: 2, 11: 2, 35: 2, 47: 3}

工作原理...

步骤 2中,ε-贪心策略接受一个参数ε,其值从 0 到 1,|A|是可能动作的数量。每个动作的概率为ε/|A|,并且以 1-ε+ε/|A|的概率选择具有最高状态-动作值的动作。

步骤 3中,我们在以下任务中执行 Q 学习:

  • 我们用全零初始化 Q 表。

  • 在每个回合中,我们让代理根据ε-贪心策略选择动作。然后,我们针对每个步骤更新 Q 函数。

  • 我们运行n_episode个回合。

  • 我们基于最优的 Q 函数获得了最优策略。

步骤 6中,再次,up = 0,right = 1,down = 2,left = 3;因此,根据最优策略,代理从状态 36 开始,然后向上移动到状态 24,然后向右一直移动到状态 35,最后向下到达目标:

在 Q 学习中可以看到,它通过学习由另一个策略生成的经验来优化 Q 函数。这与离策略 MC 控制方法非常相似。不同之处在于,它实时更新 Q 函数,而不是在整个回合结束后。这被认为是有利的,特别是对于回合时间较长的环境,延迟学习直到回合结束是低效的。在 Q 学习(或任何其他 TD 方法)的每一个步骤中,我们都会获取更多关于环境的信息,并立即使用此信息来更新值。在我们的案例中,通过仅运行 500 个学习回合,我们获得了最优策略。

还有更多...

实际上,在大约 50 个回合后获得了最优策略。我们可以绘制每个回合的长度随时间变化的图表来验证这一点。还可以选择随时间获得的每个回合的总奖励。

  1. 我们定义两个列表分别存储每个回合的长度和总奖励:
>>> length_episode = [0] * n_episode
>>> total_reward_episode = [0] * n_episode
  1. 我们在学习过程中跟踪每个回合的长度和总奖励。以下是更新版本的q_learning
>>> def q_learning(env, gamma, n_episode, alpha):
 ...     n_action = env.action_space.n
 ...     Q = defaultdict(lambda: torch.zeros(n_action))
 ...     for episode in range(n_episode):
 ...         state = env.reset()
 ...         is_done = False
 ...         while not is_done:
 ...             action = epsilon_greedy_policy(state, Q)
 ...             next_state, reward, is_done, info = 
                                        env.step(action)
 ...             td_delta = reward + 
                        gamma * torch.max(Q[next_state])
                        - Q[state][action]
 ...             Q[state][action] += alpha * td_delta
 ...             length_episode[episode] += 1
 ...             total_reward_episode[episode] += reward
 ...             if is_done:
 ...                 break
 ...             state = next_state
 ...      policy = {}
 ...      for state, actions in Q.items():
 ...          policy[state] = torch.argmax(actions).item()
 ...      return Q, policy
  1. 现在,展示随时间变化的回合长度的图表:
>>> import matplotlib.pyplot as plt
>>> plt.plot(length_episode)
>>> plt.title('Episode length over time')
>>> plt.xlabel('Episode')
>>> plt.ylabel('Length')
>>> plt.show()

这将导致以下绘图:

  1. 展示随时间变化的回合奖励的图表:
>>> plt.plot(total_reward_episode)
>>> plt.title('Episode reward over time')
>>> plt.xlabel('Episode')
>>> plt.ylabel('Total reward')
>>> plt.show()

这将导致以下绘图:

再次,如果减小ε的值,您将看到较小的波动,这是ε-贪心策略中随机探索的效果。

设置多风格格子世界环境的游乐场

在上一个示例中,我们解决了一个相对简单的环境,在那里我们可以很容易地获取最优策略。在这个示例中,让我们模拟一个更复杂的网格环境,风格网格世界,在这个环境中,外部力会将代理从某些瓦片移开。这将为我们在下一个示例中使用 TD 方法搜索最优策略做准备。

风格网格世界是一个 7 * 10 的棋盘问题,显示如下:

代理在每一步可以向上、向右、向下和向左移动。第 30 块瓦片是代理的起始点,第 37 块瓦片是获胜点,如果达到则一个 episode 结束。每一步代理走动会产生-1 的奖励。

在这个环境中的复杂性在于,第 4 至 9 列有额外的风力。从这些列的瓦片移动时,代理会额外受到向上的推力。第 7 和第 8 列的风力为 1,第 4、5、6 和 9 列的风力为 2。例如,如果代理试图从状态 43 向右移动,它们将会落在状态 34;如果代理试图从状态 48 向左移动,它们将会落在状态 37;如果代理试图从状态 67 向上移动,它们将会落在状态 37,因为代理会受到额外的 2 单位向上的力;如果代理试图从状态 27 向下移动,它们将会落在状态 17,因为额外的 2 单位向上力抵消了 1 单位向下力。

目前,风格网格世界还没有包含在 Gym 环境中。我们将通过参考 Cliff Walking 环境来实现它:github.com/openai/gym/blob/master/gym/envs/toy_text/cliffwalking.py

如何做…

让我们开发风格网格世界环境:

  1. 从 Gym 中导入必要的模块,NumPy 和discrete类:
>>> import numpy as np
>>> import sys
>>> from gym.envs.toy_text import discrete
  1. 定义四个动作:
>>> UP = 0
>>> RIGHT = 1
>>> DOWN = 2
>>> LEFT = 3
  1. 让我们从在 WindyGridworldEnv 类中定义 __init__ 方法开始:
>>> class WindyGridworldEnv(discrete.DiscreteEnv):
 ...     def __init__(self):
 ...         self.shape = (7, 10)
 ...         nS = self.shape[0] * self.shape[1]
 ...         nA = 4
 ...         # Wind locations
 ...         winds = np.zeros(self.shape)
 ...         winds[:,[3,4,5,8]] = 1
 ...         winds[:,[6,7]] = 2
 ...         self.goal = (3, 7)
 ...         # Calculate transition probabilities and rewards
 ...         P = {}
 ...         for s in range(nS):
 ...             position = np.unravel_index(s, self.shape)
 ...             P[s] = {a: [] for a in range(nA)}
 ...             P[s][UP] = self._calculate_transition_prob(
                                       position, [-1, 0], winds)
 ...             P[s][RIGHT] = self._calculate_transition_prob(
                                       position, [0, 1], winds)
 ...             P[s][DOWN] = self._calculate_transition_prob(
                                       position, [1, 0], winds)
 ...             P[s][LEFT] = self._calculate_transition_prob(
                                       position, [0, -1], winds)
 ...         # Calculate initial state distribution
 ...         # We always start in state (3, 0)
 ...         isd = np.zeros(nS)
 ...         isd[np.ravel_multi_index((3,0), self.shape)] = 1.0
 ...         super(WindyGridworldEnv, self).__init__(nS, nA, P, isd)

这定义了观察空间、风区域和风力、转移和奖励矩阵,以及初始状态。

  1. 接下来,我们定义 _calculate_transition_prob 方法来确定动作的结果,包括概率(为 1),新状态,奖励(始终为-1),以及是否完成:
...     def _calculate_transition_prob(self, current, 
                                                delta, winds):
 ...         """
 ...         Determine the outcome for an action. Transition 
                                             Prob is always 1.0.
 ...         @param current: (row, col), current position 
                                                 on the grid
 ...         @param delta: Change in position for transition
 ...         @param winds: Wind effect
 ...         @return: (1.0, new_state, reward, is_done)
 ...         """
 ...         new_position = np.array(current) + np.array(delta) 
                     + np.array([-1, 0]) * winds[tuple(current)]
 ...         new_position = self._limit_coordinates( new_position).astype(int)
 ...         new_state = np.ravel_multi_index( tuple(new_position), self.shape)
 ...         is_done = tuple(new_position) == self.goal
 ...         return [(1.0, new_state, -1.0, is_done)]

这计算基于当前状态、移动和风效应的状态,并确保新位置在网格内。最后,它检查代理是否达到目标状态。

  1. 接下来,我们定义 _limit_coordinates 方法,用于防止代理掉出网格世界:
...     def _limit_coordinates(self, coord):
 ...         coord[0] = min(coord[0], self.shape[0] - 1)
 ...         coord[0] = max(coord[0], 0)
 ...         coord[1] = min(coord[1], self.shape[1] - 1)
 ...         coord[1] = max(coord[1], 0)
 ...         return coord
  1. 最后,我们添加 render 方法以显示代理和网格环境:
...     def render(self):
 ...         outfile = sys.stdout
 ...         for s in range(self.nS):
 ...             position = np.unravel_index(s, self.shape)
 ...             if self.s == s:
 ...                 output = " x "
 ...             elif position == self.goal:
 ...                 output = " T "
 ...             else:
 ...                 output = " o "
 ...             if position[1] == 0:
 ...                 output = output.lstrip()
 ...             if position[1] == self.shape[1] - 1:
 ...                 output = output.rstrip()
 ...                 output += "\n"
 ...             outfile.write(output)
 ...         outfile.write("\n")

X 表示代理当前的位置,T 是目标瓦片,其余瓦片表示为 o

现在,让我们按以下步骤模拟风格网格世界环境:

  1. 创建一个风格网格世界环境的实例:
>>> env = WindyGridworldEnv()
  1. 重置并渲染环境:
>>> env.reset()
 >>> env.render()
 o  o  o  o  o  o  o  o  o  o
 o  o  o  o  o  o  o  o  o  o
 o  o  o  o  o  o  o  o  o  o
 x  o  o  o  o  o  o  T  o  o
 o  o  o  o  o  o  o  o  o  o
 o  o  o  o  o  o  o  o  o  o
 o  o  o  o  o  o  o  o  o  o

代理器从状态 30 开始。

  1. 向右移动一步:
>>> print(env.step(1))
 >>> env.render()
 (31, -1.0, False, {'prob': 1.0})
 o  o  o  o  o  o  o  o  o  o
 o  o  o  o  o  o  o  o  o  o
 o  o  o  o  o  o  o  o  o  o
 o  x  o  o  o  o  o  T  o  o
 o  o  o  o  o  o  o  o  o  o
 o  o  o  o  o  o  o  o  o  o
 o  o  o  o  o  o  o  o  o  o

代理器降落在状态 31,奖励为 -1。

  1. 右移两步:
>>> print(env.step(1))
>>> print(env.step(1))
>>> env.render()
 (32, -1.0, False, {'prob': 1.0})
 (33, -1.0, False, {'prob': 1.0})
 o  o  o  o  o  o  o  o  o  o
 o  o  o  o  o  o  o  o  o  o
 o  o  o  o  o  o  o  o  o  o
 o  o  o  x  o  o  o  T  o  o
 o  o  o  o  o  o  o  o  o  o
 o  o  o  o  o  o  o  o  o  o
 o  o  o  o  o  o  o  o  o  o
  1. 现在,再向右移动一步:
>>> print(env.step(1))
 >>> env.render()
 (24, -1.0, False, {'prob': 1.0})
 o  o  o  o  o  o  o  o  o  o
 o  o  o  o  o  o  o  o  o  o
 o  o  o  o  x  o  o  o  o  o
 o  o  o  o  o  o  o  T  o  o
 o  o  o  o  o  o  o  o  o  o
 o  o  o  o  o  o  o  o  o  o
 o  o  o  o  o  o  o  o  o  o

风向上 1 单位,代理器降落在状态 24。

随意尝试环境,直到达到目标。

工作原理……

我们刚刚开发了一个类似于 Cliff Walking 的网格环境。Windy Gridworld 和 Cliff Walking 的区别在于额外的向上推力。每个动作在 Windy Gridworld 剧集中将导致奖励 -1。因此,尽快达到目标更为有效。在下一个步骤中,我们将使用另一种 TD 控制方法解决 Windy Gridworld 问题。

开发 SARSA 算法

你会记得 Q-learning 是一种离策略 TD 学习算法。在本配方中,我们将使用一种在线策略 TD 学习算法解决 MDP,称为 状态-行动-奖励-状态-行动SARSA)。

类似于 Q-learning,SARSA 关注状态-动作值。它根据以下方程更新 Q 函数:

在这里,s' 是在状态 s 中采取动作 a 后的结果状态;r 是相关的奖励;α 是学习率;γ 是折扣因子。你会记得,在 Q-learning 中,一种行为贪婪策略 ![] 用于更新 Q 值。在 SARSA 中,我们简单地通过遵循 epsilon-greedy 策略来选择下一个动作 a' 来更新 Q 值。然后动作 a' 在下一步中被执行。因此,SARSA 是一个在线策略算法。

如何实现……

我们执行 SARSA 解决 Windy Gridworld 环境,步骤如下:

  1. 导入 PyTorch 和 WindyGridworldEnvmodule(假设它在名为 windy_gridworld.py 的文件中),并创建 Windy Gridworld 环境的实例:
>>> import torch
>>> from windy_gridworld import WindyGridworldEnv >>> env = WindyGridworldEnv()
  1. 让我们从定义 epsilon-greedy 行为策略开始:
>>> def gen_epsilon_greedy_policy(n_action, epsilon):
 ...     def policy_function(state, Q):
 ...         probs = torch.ones(n_action) * epsilon / n_action
 ...         best_action = torch.argmax(Q[state]).item()
 ...         probs[best_action] += 1.0 - epsilon
 ...         action = torch.multinomial(probs, 1).item()
 ...         return action
 ...     return policy_function
  1. 我们指定了要运行的剧集数,并初始化了用于跟踪每一剧集的长度和总奖励的两个变量:
>>> n_episode = 500
>>> length_episode = [0] * n_episode
>>> total_reward_episode = [0] * n_episode
  1. 现在,我们定义执行 SARSA 的函数:
>>> from collections import defaultdict
>>> def sarsa(env, gamma, n_episode, alpha):
 ...     """
 ...     Obtain the optimal policy with on-policy SARSA algorithm
 ...     @param env: OpenAI Gym environment
 ...     @param gamma: discount factor
 ...     @param n_episode: number of episodes
 ...     @return: the optimal Q-function, and the optimal policy
 ...     """
 ...     n_action = env.action_space.n
 ...     Q = defaultdict(lambda: torch.zeros(n_action))
 ...     for episode in range(n_episode):
 ...         state = env.reset()
 ...         is_done = False
 ...         action = epsilon_greedy_policy(state, Q)
 ...         while not is_done:
 ...             next_state, reward, is_done, info 
                                            = env.step(action)
 ...             next_action = epsilon_greedy_policy(next_state, Q)
 ...             td_delta = reward + 
                          gamma * Q[next_state][next_action]
                          - Q[state][action]
 ...             Q[state][action] += alpha * td_delta
 ...             length_episode[episode] += 1
 ...             total_reward_episode[episode] += reward
 ...             if is_done:
 ...                 break
 ...             state = next_state
 ...             action = next_action
 ...     policy = {}
 ...     for state, actions in Q.items():
 ...         policy[state] = torch.argmax(actions).item()
 ...     return Q, policy
  1. 我们将折扣率指定为 1,学习率为 0.4,epsilon 为 0.1:
>>> gamma = 1
>>> alpha = 0.4
>>> epsilon = 0.1
  1. 接下来,我们创建 epsilon-greedy 策略的实例:
>>> epsilon_greedy_policy = gen_epsilon_greedy_policy(env.action_space.n, epsilon)
  1. 最后,我们使用之前步骤中定义的输入参数执行 SARSA,并打印出最优策略:
>>> optimal_Q, optimal_policy = sarsa(env, gamma, n_episode, alpha) >>> print('The optimal policy:\n', optimal_policy)
 The optimal policy:
 {30: 2, 31: 1, 32: 1, 40: 1, 50: 2, 60: 1, 61: 1, 51: 1, 41: 1, 42: 1, 20: 1, 21: 1, 62: 1, 63: 2, 52: 1, 53: 1, 43: 1, 22: 1, 11: 1, 10: 1, 0: 1, 33: 1, 23: 1, 12: 1, 13: 1, 2: 1, 1: 1, 3: 1, 24: 1, 4: 1, 5: 1, 6: 1, 14: 1, 7: 1, 8: 1, 9: 2, 19: 2, 18: 2, 29: 2, 28: 1, 17: 2, 39: 2, 38: 1, 27: 0, 49: 3, 48: 3, 37: 3, 34: 1, 59: 2, 58: 3, 47: 2, 26: 1, 44: 1, 15: 1, 69: 3, 68: 1, 57: 2, 36: 1, 25: 1, 54: 2, 16: 1, 35: 1, 45: 1}

工作原理……

步骤 4 中,SARSA 函数执行以下任务:

  • 它使用全零初始化 Q 表。

  • 在每一剧集中,它让代理器遵循 epsilon-greedy 策略来选择采取的行动。对于每一步,它根据方程 ![] 更新 Q 函数,其中 a' 是根据 epsilon-greedy 策略选择的。然后,在新状态 s' 中采取新的动作 a'

  • 我们运行 n_episode 个剧集。

  • 我们基于最优 Q 函数获取最优策略。

正如在 SARSA 方法中所见,它通过采取在相同策略下选择的动作来优化 Q 函数,即 epsilon 贪婪策略。这与 on-policy MC 控制方法非常相似。不同之处在于,它通过个别步骤中的小导数来更新 Q 函数,而不是在整个集结束后。在集合长度较长的环境中,此方法被认为是优势,因为将学习延迟到集的结束是低效的。在 SARSA 的每一个单步中,我们获得更多关于环境的信息,并利用这些信息立即更新值。在我们的案例中,仅通过运行 500 个学习集,我们获得了最优策略。

还有更多...

实际上,在大约 200 集后获得了最优策略。我们可以绘制每一集的长度和总奖励随时间变化的图表来验证这一点:

  1. 显示随时间变化的剧集长度图:
>>> import matplotlib.pyplot as plt
>>> plt.plot(length_episode)
>>> plt.title('Episode length over time')
>>> plt.xlabel('Episode')
>>> plt.ylabel('Length')
>>> plt.show()

这将导致以下图表:

您可以看到,剧集长度在 200 集后开始饱和。请注意,这些小波动是由 epsilon 贪婪策略中的随机探索造成的。

  1. 显示随时间变化的剧集奖励图:
>>> plt.plot(total_reward_episode)
>>> plt.title('Episode reward over time')
>>> plt.xlabel('Episode')
>>> plt.ylabel('Total reward')
>>> plt.show()

这将导致以下图表:

如果你减小 epsilon 的值,你将看到更小的波动,这是在 epsilon 贪婪策略中随机探索的影响。

在接下来的两个示例中,我们将使用我们刚学到的两种 TD 方法来解决一个更复杂的环境,该环境具有更多的可能状态和动作。让我们从 Q 学习开始。

使用 Q 学习解决出租车问题

出租车问题 (gym.openai.com/envs/Taxi-v2/) 是另一个流行的网格世界问题。在一个 5 * 5 的网格中,代理作为出租车司机,在一个位置接载乘客,然后将乘客送达目的地。看下面的例子:

彩色方块有以下含义:

  • 黄色:出租车的起始位置。起始位置在每一集中是随机的。

  • 蓝色:乘客的位置。在每一集中也是随机选择的。

  • 紫色:乘客的目的地。同样,在每一集中随机选择。

  • 绿色:带有乘客的出租车位置。

R、Y、B 和 G 这四个字母指示唯一允许接载和送达乘客的方块。其中一个是目的地,一个是乘客的位置。

出租车可以采取以下六个确定性动作:

  • 0:向南移动

  • 1:向北移动

  • 2:向东移动

  • 3:向西移动

  • 4:接载乘客

  • 5:送达乘客

两个方块之间有一根柱子 |,防止出租车从一个方块移动到另一个方块。

每一步的奖励通常是 -1,以下是例外情况:

  • +20:乘客被送达目的地。一个回合将结束。

  • -10:尝试非法接乘或下车(不在 R、Y、B 或 G 中)。

还有一件需要注意的事情是,观察空间远远大于 25(5*5),因为我们还应考虑乘客和目的地的位置,以及出租车是否为空或已满。因此,观察空间应为 25 * 5(乘客或已在出租车的 4 个可能位置) * 4(目的地)= 500 维度。

准备就绪

要运行出租车环境,让我们首先在环境表中搜索其名称,github.com/openai/gym/wiki/Table-of-environments。我们得到 Taxi-v2,并且知道观察空间由一个从 0 到 499 的整数表示,并且有四种可能的动作(向上 = 0,向右 = 1,向下 = 2,向左 = 3)。

如何做到…

让我们从以下步骤开始模拟出租车环境:

  1. 我们导入 Gym 库并创建出租车环境的实例:
>>> import gym
>>> env = gym.make('Taxi-v2')
>>> n_state = env.observation_space.n
>>> print(n_state)
 500
>>> n_action = env.action_space.n
>>> print(n_action)
 6
  1. 然后,我们重置环境:
>>> env.reset()
 262
  1. 然后,我们渲染环境:
>>> env.render()

您将看到一个类似的 5 * 5 矩阵如下:

乘客位于 R 位置,目的地位于 Y。由于初始状态是随机生成的,您将看到不同的结果。

  1. 现在让我们向西移动三个瓷砖,向北移动两个瓷砖去接乘客(您可以根据初始状态进行调整),然后执行接乘客。接着,我们再次渲染环境:
>>> print(env.step(3))
 (242, -1, False, {'prob': 1.0})
>>> print(env.step(3))
 (222, -1, False, {'prob': 1.0})
>>> print(env.step(3))
 (202, -1, False, {'prob': 1.0})
>>> print(env.step(1))
 (102, -1, False, {'prob': 1.0})
>>> print(env.step(1))
 (2, -1, False, {'prob': 1.0})
>>> print(env.step(4))
 (18, -1, False, {'prob': 1.0})
 Render the environment:
>>> env.render()
  1. 您将看到更新的最新矩阵(根据您的初始状态可能会得到不同的输出):

出租车变成了绿色。

  1. 现在,我们向南移动四个瓷砖去到达目的地(您可以根据初始状态进行调整),然后执行下车:
>>> print(env.step(0))
 (118, -1, False, {'prob': 1.0})
>>> print(env.step(0))
 (218, -1, False, {'prob': 1.0})
>>> print(env.step(0))
 (318, -1, False, {'prob': 1.0})
>>> print(env.step(0))
 (418, -1, False, {'prob': 1.0})
>>> print(env.step(5))
 (410, 20, True, {'prob': 1.0})

最后它获得 +20 的奖励,并且回合结束。

现在,我们渲染环境:

>>> env.render()

您将看到以下更新的矩阵:

现在我们将执行 Q 学习来解决出租车环境,如下所示:

  1. 导入 PyTorch 库:
>>> import torch
  1. 然后,开始定义 epsilon-greedy 策略。我们将重用“开发 Q 学习算法”食谱中定义的 gen_epsilon_greedy_policy 函数。

  2. 现在,我们指定回合的数量,并初始化用于跟踪每个回合长度和总奖励的两个变量:

>>> n_episode = 1000
>>> length_episode = [0] * n_episode
>>> total_reward_episode = [0] * n_episode
  1. 接下来,我们定义执行 Q 学习的函数。我们将重用“开发 Q 学习算法”食谱中定义的 q_learning 函数。

  2. 现在,我们指定其余的参数,包括折扣率、学习率和 epsilon,并创建一个 epsilon-greedy 策略的实例:

>>> gamma = 1
>>> alpha = 0.4
>>> epsilon = 0.1 >>> epsilon_greedy_policy = gen_epsilon_greedy_policy(env.action_space.n, epsilon)
  1. 最后,我们进行 Q 学习来获得出租车问题的最优策略:
>>> optimal_Q, optimal_policy = q_learning(env, gamma, n_episode, alpha)

工作原理…

在这个食谱中,我们通过离线 Q 学习解决了出租车问题。

步骤 6之后,您可以绘制每个周期的长度和总奖励,以验证模型是否收敛。时间序列的奖励图如下所示:

时间序列的奖励图如下所示:

您可以看到,优化在 400 个周期后开始饱和。

出租车环境是一个相对复杂的网格问题,有 500 个离散状态和 6 种可能的动作。Q-learning 通过学习贪婪策略生成的经验来优化每个步骤中的 Q 函数。我们在学习过程中获取环境信息,并使用这些信息按照ε-贪婪策略立即更新值。

使用 SARSA 解决出租车问题

在这个示例中,我们将使用 SARSA 算法解决出租车环境,并使用网格搜索算法微调超参数。

我们将从 SARSA 模型的默认超参数值开始。这些值是基于直觉和一些试验选择的。接下来,我们将提出最佳值的一组值。

如何做...

我们按照以下方式执行 SARSA 来解决出租车环境:

  1. 导入 PyTorch 和gym模块,并创建出租车环境的一个实例:
>>> import torch
>>> import gym >>> env = gym.make('Taxi-v2')
  1. 然后,开始定义 ε-贪婪行为策略。我们将重用开发 SARSA 算法配方中定义的gen_epsilon_greedy_policy函数。

  2. 然后,我们指定要追踪每个周期的长度和总奖励的两个变量的数量:

>>> n_episode = 1000 >>> length_episode = [0] * n_episode
>>> total_reward_episode = [0] * n_episode
  1. 现在,我们定义执行 SARSA 的函数。我们将重用开发 SARSA 算法配方中定义的sarsa函数。

  2. 我们将折现率设定为1,默认学习率设定为0.4,默认 ε 设定为0.1

>>> gamma = 1
>>> alpha = 0.4
>>> epsilon = 0.01
  1. 接下来,我们创建 ε-贪婪策略的一个实例:
>>> epsilon_greedy_policy = gen_epsilon_greedy_policy(env.action_space.n, epsilon)
  1. 最后,我们使用前面步骤中定义的输入参数执行 SARSA:
>>> optimal_Q, optimal_policy = sarsa(env, gamma, n_episode, alpha)

工作原理...

步骤 7之后,您可以绘制每个周期的长度和总奖励,以验证模型是否收敛。时间序列的奖励图如下所示:

时间序列的奖励图如下所示:

这个 SARSA 模型工作得很好,但不一定是最好的。稍后,我们将使用网格搜索来寻找 SARSA 模型下最佳的一组超参数。

出租车环境是一个相对复杂的网格问题,有 500 个离散状态和 6 种可能的动作。SARSA 算法通过学习和优化目标策略来优化每个步骤中的 Q 函数。我们在学习过程中获取环境信息,并使用这些信息按照ε-贪婪策略立即更新值。

还有更多...

网格搜索是一种程序化的方法,用于在强化学习中找到超参数的最佳值集合。每组超参数的性能由以下三个指标来衡量:

  • 前几个 episode 的*均总奖励:我们希望尽早获得最大的奖励。

  • 前几个 episode 的*均 episode 长度:我们希望出租车尽快到达目的地。

  • 每个时间步的前几个 episode 的*均奖励:我们希望尽快获得最大的奖励。

让我们继续实施它:

  1. 我们在这里使用了三个 alpha 候选值[0.4, 0.5 和 0.6]和三个 epsilon 候选值[0.1, 0.03 和 0.01],并且仅考虑了前 500 个 episode:
>>> alpha_options = [0.4, 0.5, 0.6]
>>> epsilon_options = [0.1, 0.03, 0.01]
>>> n_episode = 500
  1. 我们通过训练每组超参数的 SARSA 模型并评估相应的性能来进行网格搜索:
>>> for alpha in alpha_options:
 ...     for epsilon in epsilon_options:
 ...         length_episode = [0] * n_episode
 ...         total_reward_episode = [0] * n_episode
 ...         sarsa(env, gamma, n_episode, alpha)
 ...         reward_per_step = [reward/float(step) for 
                               reward, step in zip(
                            total_reward_episode, length_episode)]
 ...         print('alpha: {}, epsilon: {}'.format(alpha, epsilon))
 ...         print('Average reward over {} episodes: {}'.format( n_episode, sum(total_reward_episode) / n_episode))
 ...         print('Average length over {} episodes: {}'.format( n_episode, sum(length_episode) / n_episode))
 ...         print('Average reward per step over {} episodes: 
        {}\n'.format(n_episode, sum(reward_per_step) / n_episode))

运行上述代码会生成以下结果:

alpha: 0.4, epsilon: 0.1
 Average reward over 500 episodes: -75.442
 Average length over 500 episodes: 57.682
 Average reward per step over 500 episodes: -0.32510755063660324
 alpha: 0.4, epsilon: 0.03
 Average reward over 500 episodes: -73.378
 Average length over 500 episodes: 56.53
 Average reward per step over 500 episodes: -0.2761201410280632
 alpha: 0.4, epsilon: 0.01
 Average reward over 500 episodes: -78.722
 Average length over 500 episodes: 59.366
 Average reward per step over 500 episodes: -0.3561815084186654
 alpha: 0.5, epsilon: 0.1
 Average reward over 500 episodes: -72.026
 Average length over 500 episodes: 55.592
 Average reward per step over 500 episodes: -0.25355404831497264
 alpha: 0.5, epsilon: 0.03
 Average reward over 500 episodes: -67.562
 Average length over 500 episodes: 52.706
 Average reward per step over 500 episodes: -0.20602525679639022
 alpha: 0.5, epsilon: 0.01
 Average reward over 500 episodes: -75.252
 Average length over 500 episodes: 56.73
 Average reward per step over 500 episodes: -0.2588407558703358
 alpha: 0.6, epsilon: 0.1
 Average reward over 500 episodes: -62.568
 Average length over 500 episodes: 49.488
 Average reward per step over 500 episodes: -0.1700284221229244
 alpha: 0.6, epsilon: 0.03
 Average reward over 500 episodes: -68.56
 Average length over 500 episodes: 52.804
 Average reward per step over 500 episodes: -0.24794191768600077
 alpha: 0.6, epsilon: 0.01
 Average reward over 500 episodes: -63.468
 Average length over 500 episodes: 49.752
 Average reward per step over 500 episodes: -0.14350124172091722

我们可以看到,在这种情况下,最佳的超参数集合是 alpha: 0.6,epsilon: 0.01,它实现了每步最大的奖励和较大的*均奖励以及较短的*均 episode 长度。

开发双 Q-learning 算法

在这是一个额外的步骤,在本章中我们将开发双 Q-learning 算法。

Q-learning 是一种强大且流行的 TD 控制强化学习算法。然而,在某些情况下可能表现不佳,主要是因为贪婪组件maxa'Q(s', a')。它可能会高估动作值并导致性能不佳。双 Q-learning 通过利用两个 Q 函数来克服这一问题。我们将两个 Q 函数表示为Q1Q2。在每一步中,随机选择一个 Q 函数进行更新。如果选择Q1,则更新如下:

如果选择 Q2,则更新如下:

这意味着每个 Q 函数都从另一个 Q 函数更新,遵循贪婪搜索,这通过使用单个 Q 函数减少了动作值的高估。

如何做...

现在我们开发双 Q-learning 来解决出租车环境,如下所示:

  1. 导入所需的库并创建 Taxi 环境的实例:
>>> import torch >>> import gym
>>> env = gym.make('Taxi-v2')
  1. 然后,开始定义 epsilon-greedy 策略。我们将重用在开发 Q-learning 算法步骤中定义的gen_epsilon_greedy_policy函数。

  2. 然后,我们指定了 episode 的数量,并初始化了两个变量来跟踪每个 episode 的长度和总奖励:

>>> n_episode = 3000
>>> length_episode = [0] * n_episode
>>> total_reward_episode = [0] * n_episode

在这里,我们模拟了 3,000 个 episode,因为双 Q-learning 需要更多的 episode 才能收敛。

  1. 接下来,我们定义执行双 Q-learning 的函数:
>>> def double_q_learning(env, gamma, n_episode, alpha):
 ...     """
 ...     Obtain the optimal policy with off-policy double 
         Q-learning method
 ...     @param env: OpenAI Gym environment
 ...     @param gamma: discount factor
 ...     @param n_episode: number of episodes
 ...     @return: the optimal Q-function, and the optimal policy
 ...     """
 ...     n_action = env.action_space.n
 ...     n_state = env.observation_space.n
 ...     Q1 = torch.zeros(n_state, n_action)
 ...     Q2 = torch.zeros(n_state, n_action)
 ...     for episode in range(n_episode):
 ...         state = env.reset()
 ...         is_done = False
 ...         while not is_done:
 ...             action = epsilon_greedy_policy(state, Q1 + Q2)
 ...             next_state, reward, is_done, info 
                                     = env.step(action)
 ...             if (torch.rand(1).item() < 0.5):
 ...                 best_next_action = torch.argmax(Q1[next_state])
 ...                 td_delta = reward + 
                           gamma * Q2[next_state][best_next_action]
                           - Q1[state][action]
 ...                 Q1[state][action] += alpha * td_delta
 ...             else:
 ...                 best_next_action = torch.argmax(Q2[next_state])
 ...                 td_delta = reward + 
                           gamma * Q1[next_state][best_next_action]
                           - Q2[state][action]
 ...                 Q2[state][action] += alpha * td_delta
 ...             length_episode[episode] += 1
 ...             total_reward_episode[episode] += reward
 ...             if is_done:
 ...                 break
 ...             state = next_state
 ...     policy = {}
 ...     Q = Q1 + Q2
 ...     for state in range(n_state):
 ...         policy[state] = torch.argmax(Q[state]).item()
 ...     return Q, policy
  1. 然后,我们指定了剩余的参数,包括折扣率、学习率和 epsilon,并创建了 epsilon-greedy-policy 的实例:
>>> gamma = 1
>>> alpha = 0.4
>>> epsilon = 0.1 >>> epsilon_greedy_policy = gen_epsilon_greedy_policy(env.action_space.n, epsilon)
  1. 最后,我们执行双 Q-learning 以获得出租车问题的最优策略:
>>> optimal_Q, optimal_policy = double_q_learning(env, gamma, n_episode, alpha)

工作原理...

我们在本示例中使用双 Q 学习算法解决了出租车问题。

第 4 步中,我们执行双 Q 学习,完成以下任务:

  • 将两个 Q 表初始化为全零。

  • 在每个周期的每个步骤中,我们随机选择一个 Q 函数来更新。让代理根据 epsilon-greedy 策略选择动作并使用另一个 Q 函数更新所选的 Q 函数。

  • 运行n_episode个周期。

  • 基于最优 Q 函数获得最优策略,通过求和(或*均)两个 Q 函数来实现。

第 6 步之后,您可以绘制每个周期的长度和总奖励,以验证模型是否收敛。周期长度随时间的变化图如下所示:

奖励随时间变化的图表如下所示:

双 Q 学习克服了单 Q 学习在复杂环境中的潜在缺点。它随机地在两个 Q 函数之间切换并更新它们,这可以防止一个 Q 函数的动作值被高估。同时,它可能会低估 Q 函数,因为它不会在时间步长内更新相同的 Q 函数。因此,我们可以看到最优动作值需要更多的周期来收敛。

另请参阅

了解双 Q 学习背后的理论,请参阅 Hado van Hasselt 的原始论文,papers.nips.cc/paper/3964-double-q-learning,发表于神经信息处理系统进展 23(NIPS 2010),2613-2621,2010 年。

第五章:解决多臂老丨虎丨机问题

多臂老丨虎丨机算法可能是强化学习中最流行的算法之一。本章将从创建多臂老丨虎丨机开始,并尝试使用随机策略。我们将专注于如何使用ε-贪心、softmax 探索、上置信区间和汤普森抽样等四种策略解决多臂老丨虎丨机问题。我们将看到它们如何以独特的方式处理探索与利用的困境。我们还将解决一个价值十亿美元的问题,即在线广告,演示如何使用多臂老丨虎丨机算法解决它。最后,我们将使用上下文老丨虎丨机解决上下文广告问题,以在广告优化中做出更明智的决策。

本章将涵盖以下配方:

  • 创建多臂老丨虎丨机环境

  • 使用ε-贪心策略解决多臂老丨虎丨机问题

  • 使用 softmax 探索解决多臂老丨虎丨机问题

  • 使用上置信区间算法解决多臂老丨虎丨机问题

  • 使用多臂老丨虎丨机解决互联网广告问题

  • 使用汤普森抽样算法解决多臂老丨虎丨机问题

  • 使用上下文老丨虎丨机解决互联网广告问题

创建多臂老丨虎丨机环境

让我们开始一个简单的项目,使用蒙特卡洛方法来估计π的值,这是无模型强化学习算法的核心。

多臂老丨虎丨机问题是最简单的强化学习问题之一。它最好被描述为一个有多个杠杆(臂)的老丨虎丨机,每个杠杆有不同的支付和支付概率。我们的目标是发现具有最大回报的最佳杠杆,以便在之后继续选择它。让我们从一个简单的多臂老丨虎丨机问题开始,其中每个臂的支付和支付概率是固定的。在创建环境后,我们将使用随机策略算法来解决它。

如何做…

让我们按照以下步骤开发多臂老丨虎丨机环境:

>>> import torch
>>> class BanditEnv():
...     """
...     Multi-armed bandit environment
...     payout_list:
...         A list of probabilities of the likelihood that a 
 particular bandit will pay out
...     reward_list:
...         A list of rewards of the payout that bandit has
...     """
...     def __init__(self, payout_list, reward_list):
...         self.payout_list = payout_list
...         self.reward_list = reward_list
...
...     def step(self, action):
...         if torch.rand(1).item() < self.payout_list[action]:
...             return self.reward_list[action]
...         return 0

步骤方法执行一个动作,并在支付时返回奖励,否则返回 0。

现在,我们将以多臂老丨虎丨机为例,并使用随机策略解决它:

  1. 定义三臂老丨虎丨机的支付概率和奖励,并创建老丨虎丨机环境的实例:
>>> bandit_payout = [0.1, 0.15, 0.3]
>>> bandit_reward = [4, 3, 1]>>> bandit_env = BanditEnv(bandit_payout, bandit_reward)

例如,选择臂 0 获得奖励 4 的概率为 10%。

  1. 我们指定要运行的集数,并定义保存通过选择各个臂累积的总奖励、选择各个臂的次数以及各个臂随时间的*均奖励的列表:
>>> n_episode = 100000
>>> n_action = len(bandit_payout)
>>> action_count = [0 for _ in range(n_action)]
>>> action_total_reward = [0 for _ in range(n_action)]
>>> action_avg_reward = [[] for action in range(n_action)]
  1. 定义随机策略,随机选择一个臂:
>>> def random_policy():
...     action = torch.multinomial(torch.ones(n_action), 1).item()
...     return action
  1. 现在,我们运行 100,000 个集数。对于每个集数,我们还更新每个臂的统计数据:
>>> for episode in range(n_episode):
...     action = random_policy()
...     reward = bandit_env.step(action)
...     action_count[action] += 1
...     action_total_reward[action] += reward
...     for a in range(n_action):
...         if action_count[a]:
...             action_avg_reward[a].append(
                     action_total_reward[a] / action_count[a])
...         else:
...             action_avg_reward[a].append(0)
  1. 运行了 100,000 个集数后,我们绘制了随时间变化的*均奖励的结果:
>>> import matplotlib.pyplot as plt
 >>> for action in range(n_action):
 ...     plt.plot(action_avg_reward[action])
 >>> plt.legend([‘Arm {}’.format(action) for action in range(n_action)])
 >>> plt.title(‘Average reward over time’)
 >>> plt.xscale(‘log’)
 >>> plt.xlabel(‘Episode’)
 >>> plt.ylabel(‘Average reward’)
 >>> plt.show()

它是如何工作的…

在我们刚刚处理的示例中,有三台老丨虎丨机。每台机器都有不同的支付(奖励)和支付概率。在每个 episode 中,我们随机选择一台机器的一个臂来拉(执行一个动作),并以一定的概率获得支付。

执行Step 5中的代码行,你将看到以下图表:

臂 1 是*均奖励最高的最佳臂。此外,*均奖励在大约 10,000 个 episode 后开始饱和。

此解决方案看起来非常幼稚,因为我们仅对所有臂进行了探索。在接下来的配方中,我们将提出更智能的策略。

使用ε-贪婪策略解决多臂老丨虎丨机问题

不再仅仅通过随机策略进行探索,我们可以通过探索与利用的结合做得更好。这就是著名的ε-贪婪策略。

对于多臂老丨虎丨机的ε-贪婪策略,大部分时间利用最佳动作,同时不时探索不同的动作。给定参数ε,其取值范围为 0 到 1,执行探索和利用的概率分别为ε和 1 - ε:

  • ε:每个动作的概率如下计算:

这里,|A|是可能动作的数量。

  • 贪婪:优选具有最高状态-动作值的动作,并且其被选择的概率增加 1 - ε:

如何操作...

我们使用ε-贪婪策略解决多臂老丨虎丨机问题如下:

  1. 导入 PyTorch 和我们在之前的配方中开发的老丨虎丨机环境,创建多臂老丨虎丨机环境(假设BanditEnv类在名为multi_armed_bandit.py的文件中):
>>> import torch
 >>> from multi_armed_bandit import BanditEnv
  1. 定义三臂老丨虎丨机的支付概率和奖励,并创建一个老丨虎丨机环境的实例:
>>> bandit_payout = [0.1, 0.15, 0.3]
 >>> bandit_reward = [4, 3, 1] >>> bandit_env = BanditEnv(bandit_payout, bandit_reward)
  1. 指定要运行的 episode 数,并定义持有通过选择各个臂累积的总奖励、选择各个臂的次数以及每个臂随时间变化的*均奖励的列表:
>>> n_episode = 100000
 >>> n_action = len(bandit_payout)
 >>> action_count = [0 for _ in range(n_action)]
 >>> action_total_reward = [0 for _ in range(n_action)]
 >>> action_avg_reward = [[] for action in range(n_action)]
  1. 定义ε-贪婪策略函数,指定ε的值,并创建一个ε-贪婪策略实例:
>>> def gen_epsilon_greedy_policy(n_action, epsilon):
 ...     def policy_function(Q):
 ...         probs = torch.ones(n_action) * epsilon / n_action
 ...         best_action = torch.argmax(Q).item()
 ...         probs[best_action] += 1.0 - epsilon
 ...         action = torch.multinomial(probs, 1).item()
 ...         return action
 ...     return policy_function >>> epsilon = 0.2
 >>> epsilon_greedy_policy = gen_epsilon_greedy_policy(n_action, epsilon)
  1. 初始化Q函数,即各个臂获得的*均奖励:
>>> Q = torch.zeros(n_action)

我们将随时间更新Q函数。

  1. 现在,我们运行 100,000 个 episode。每个 episode,我们还会随时间更新每个臂的统计信息:
>>> for episode in range(n_episode):
 ...     action = epsilon_greedy_policy(Q)
 ...     reward = bandit_env.step(action)
 ...     action_count[action] += 1
 ...     action_total_reward[action] += reward
 ...     Q[action] = action_total_reward[action] / action_count[action]
 ...     for a in range(n_action):
 ...         if action_count[a]:
 ...             action_avg_reward[a].append(
                         action_total_reward[a] / action_count[a])
 ...         else:
 ...             action_avg_reward[a].append(0)
  1. 运行 100,000 个 episode 后,我们绘制了随时间变化的*均奖励结果:
>>> import matplotlib.pyplot as plt
 >>> for action in range(n_action):
 ...     plt.plot(action_avg_reward[action])
 >>> plt.legend([‘Arm {}’.format(action) for action in range(n_action)])
 >>> plt.title(‘Average reward over time’)
 >>> plt.xscale(‘log’)
 >>> plt.xlabel(‘Episode’)
 >>> plt.ylabel(‘Average reward’)
 >>> plt.show()

工作原理...

类似于其他 MDP 问题,ε-贪婪策略以 1 - ε的概率选择最佳臂,并以ε的概率进行随机探索。ε管理着探索与利用之间的权衡。

Step 7中,你将看到以下图表:

臂 1 是最佳臂,在最后具有最大的*均奖励。此外,它的*均奖励在大约 1,000 个剧集后开始饱和。

更多内容...

你可能想知道ε-贪婪策略是否确实优于随机策略。除了在ε-贪婪策略中最优臂的值较早收敛外,我们还可以证明,在训练过程中,通过ε-贪婪策略获得的*均奖励比随机策略更高。

我们可以简单地计算所有剧集的*均奖励:

>>> print(sum(action_total_reward) / n_episode)
 0.43718

在 100,000 个剧集中,使用ε-贪婪策略的*均支付率为 0.43718。对随机策略解决方案进行相同计算后,得到*均支付率为 0.37902。

使用 softmax 探索解决多臂赌博问题

在本示例中,我们将使用 softmax 探索算法解决多臂赌博问题。我们将看到它与ε-贪婪策略的不同之处。

正如我们在ε-贪婪中看到的,当进行探索时,我们以 ε/|A| 的概率随机选择非最佳臂之一。每个非最佳臂在 Q 函数中的价值不管其值都是等效的。此外,无论其值如何,最佳臂都以固定概率被选择。在 softmax 探索 中,根据 Q 函数值的 softmax 分布选择臂。概率计算如下:

这里,τ 参数是温度因子,用于指定探索的随机性。τ 值越高,探索就越接**等;τ 值越低,选择最佳臂的可能性就越大。

如何做到...

我们如下解决了使用 softmax 探索算法的多臂赌博问题:

  1. 导入 PyTorch 和我们在第一个示例中开发的赌博环境,创建多臂赌博环境(假设 BanditEnv 类在名为 multi_armed_bandit.py 的文件中):
>>> import torch
 >>> from multi_armed_bandit import BanditEnv
  1. 定义三臂赌博的支付概率和奖励,并创建赌博环境的实例:
>>> bandit_payout = [0.1, 0.15, 0.3]
 >>> bandit_reward = [4, 3, 1] >>> bandit_env = BanditEnv(bandit_payout, bandit_reward)
  1. 我们指定要运行的剧集数量,并定义保存通过选择各个臂累积的总奖励、选择各个臂的次数以及每个臂随时间的*均奖励的列表:
>>> n_episode = 100000
 >>> n_action = len(bandit_payout)
 >>> action_count = [0 for _ in range(n_action)]
 >>> action_total_reward = [0 for _ in range(n_action)]
 >>> action_avg_reward = [[] for action in range(n_action)]
  1. 定义 softmax 探索策略函数,指定 τ 的值,并创建 softmax 探索策略实例:
>>> def gen_softmax_exploration_policy(tau):
 ...     def policy_function(Q):
 ...         probs = torch.exp(Q / tau)
 ...         probs = probs / torch.sum(probs)
 ...         action = torch.multinomial(probs, 1).item()
 ...         return action
 ...     return policy_function >>> tau = 0.1
 >>> softmax_exploration_policy = gen_softmax_exploration_policy(tau)
  1. 初始化 Q 函数,即通过各个臂获得的*均奖励:
>>> Q = torch.zeros(n_action)

我们将随时间更新 Q 函数。

  1. 现在,我们运行 100,000 个剧集。对于每个剧集,我们还更新每个臂的统计信息:
>>> for episode in range(n_episode):
 ...     action = softmax_exploration_policy(Q)
 ...     reward = bandit_env.step(action)
 ...     action_count[action] += 1
 ...     action_total_reward[action] += reward
 ...     Q[action] = action_total_reward[action] / action_count[action]
 ...     for a in range(n_action):
 ...         if action_count[a]:
 ...             action_avg_reward[a].append(                         action_total_reward[a] / action_count[a])
 ...         else:
 ...             action_avg_reward[a].append(0)
  1. 运行了 100,000 个剧集后,我们绘制了随时间变化的*均奖励结果:
>>> import matplotlib.pyplot as plt
 >>> for action in range(n_action):
 ...     plt.plot(action_avg_reward[action])
 >>> plt.legend([‘Arm {}’.format(action) for action in range(n_action)])
 >>> plt.title(‘Average reward over time’)
 >>> plt.xscale(‘log’)
 >>> plt.xlabel(‘Episode’)
 >>> plt.ylabel(‘Average reward’)
 >>> plt.show()

工作原理...

使用 softmax 探索策略,利用基于 Q 值的 softmax 函数解决了开发与探索的困境。它不是使用最佳臂和非最佳臂的固定概率对,而是根据τ参数作为温度因子的 softmax 分布调整概率。τ值越高,焦点就会更多地转向探索。

步骤 7中,您将看到以下绘图:

臂 1 是最佳臂,在最后具有最大的*均奖励。此外,在这个例子中,它的*均奖励在大约 800 个 episode 后开始饱和。

使用上置信度边界算法解决多臂赌博问题

在前两个配方中,我们通过在 epsilon-贪婪策略中将概率分配为固定值或者根据 Q 函数值计算 softmax 探索算法中的概率,探索了多臂赌博问题中的随机动作。在任一算法中,随机执行动作的概率并不随时间调整。理想情况下,我们希望随着学习的进行减少探索。在本配方中,我们将使用称为上置信度边界的新算法来实现这一目标。

上置信度边界UCB)算法源于置信区间的概念。一般来说,置信区间是真值所在的一系列值。在 UCB 算法中,臂的置信区间是该臂获取的*均奖励所处的范围。该区间的形式为[下置信度边界,上置信度边界],我们只使用上置信度边界,即 UCB,来估计该臂的潜力。UCB 的计算公式如下:

这里,t 是 episode 的数量,N(a)是在 t 个 episode 中臂 a 被选择的次数。随着学习的进行,置信区间收缩并变得越来越精确。应该拉动的臂是具有最高 UCB 的臂。

如何做...

我们使用 UCB 算法解决多臂赌博问题的步骤如下:

  1. 导入 PyTorch 和第一个配方中开发的赌博环境,创建多臂赌博环境(假设BanditEnv类位于名为multi_armed_bandit.py的文件中):
>>> import torch
 >>> from multi_armed_bandit import BanditEnv
  1. 定义三臂赌博的赔率概率和奖励,并创建赌博环境的一个实例:
>>> bandit_payout = [0.1, 0.15, 0.3]
 >>> bandit_reward = [4, 3, 1] >>> bandit_env = BanditEnv(bandit_payout, bandit_reward)
  1. 我们指定要运行的 episode 数量,并定义保存通过选择不同臂积累的总奖励、选择各个臂的次数以及各个臂随时间的*均奖励的列表:
>>> n_episode = 100000
 >>> n_action = len(bandit_payout)
 >>> action_count = torch.tensor([0\. for _ in range(n_action)])
 >>> action_total_reward = [0 for _ in range(n_action)]
 >>> action_avg_reward = [[] for action in range(n_action)]
  1. 定义 UCB 策略函数,根据 UCB 公式计算最佳臂:
>>> def upper_confidence_bound(Q, action_count, t):
 ...     ucb = torch.sqrt((2 * torch.log(torch.tensor(float(t))))                                              / action_count) + Q
 ...     return torch.argmax(ucb)
  1. 初始化 Q 函数,它是使用各个臂获取的*均奖励:
>>> Q = torch.empty(n_action)

随着时间的推移,我们将更新 Q 函数。

  1. 现在,我们使用我们的 UCB 策略运行 100,000 个 episode。对于每个 episode,我们还更新每个臂的统计信息:
>>> for episode in range(n_episode):
 ...     action = upper_confidence_bound(Q, action_count, episode)
 ...     reward = bandit_env.step(action)
 ...     action_count[action] += 1
 ...     action_total_reward[action] += reward
 ...     Q[action] = action_total_reward[action] / action_count[action]
 ...     for a in range(n_action):
 ...         if action_count[a]:
 ...             action_avg_reward[a].append(                         action_total_reward[a] / action_count[a])
 ...         else:
 ...             action_avg_reward[a].append(0)
  1. 在运行了 10 万个剧集后,我们绘制了随时间变化的*均奖励结果:
>>> import matplotlib.pyplot as plt
 >>> for action in range(n_action):
 ...     plt.plot(action_avg_reward[action])
 >>> plt.legend([‘Arm {}’.format(action) for action in range(n_action)])
 >>> plt.title(‘Average reward over time’)
 >>> plt.xscale(‘log’)
 >>> plt.xlabel(‘Episode’)
 >>> plt.ylabel(‘Average reward’)
 >>> plt.show()

工作原理...

在这个示例中,我们使用了 UCB 算法解决了多臂赌博机问题。它根据剧集数调整开发-探索困境。对于数据点较少的动作,其置信区间相对较宽,因此选择此动作具有相对较高的不确定性。随着更多的动作剧集被选中,置信区间变窄并收缩到其实际值。在这种情况下,选择(或不选择)此动作是非常确定的。最后,在每个剧集中,UCB 算法拉动具有最高 UCB 的臂,并随着时间的推移获得越来越多的信心。

第 7 步中运行代码后,您将看到以下绘图:

第一个臂是最佳的臂,最终*均奖励最高。

还有更多内容...

您可能想知道 UCB 是否真的优于ε-greedy 策略。我们可以计算整个训练过程中的*均奖励,*均奖励最高的策略学习速度更快。

我们可以简单地*均所有剧集的奖励:

>>> print(sum(action_total_reward) / n_episode)
 0.44605

在 10 万个剧集中,使用 UCB 的*均支付率为 0.44605,高于ε-greedy 策略的 0.43718。

另请参阅

对于那些想要了解置信区间的人,请随时查看以下内容:www.stat.yale.edu/Courses/1997-98/101/confint.htm

解决互联网广告问题的多臂赌博机

想象一下,您是一位在网站上进行广告优化的广告商:

  • 广告背景有三种不同的颜色 – 红色,绿色和蓝色。哪种将实现最佳点击率(CTR)?

  • 广告有三种不同的文案 – 学习…免费…尝试…。哪一个将实现最佳 CTR?

对于每位访客,我们需要选择一个广告,以最大化随时间的点击率(CTR)。我们如何解决这个问题?

或许您在考虑 A/B 测试,其中您随机将流量分成几组,并将每个广告分配到不同的组中,然后在观察一段时间后选择具有最高 CTR 的组中的广告。然而,这基本上是完全的探索,我们通常不确定观察期应该多长,最终会失去大量潜在的点击。此外,在 A/B 测试中,假设广告的未知 CTR 不会随时间而变化。否则,这种 A/B 测试应定期重新运行。

多臂赌博机确实可以比 A/B 测试做得更好。每个臂是一个广告,臂的奖励要么是 1(点击),要么是 0(未点击)。

让我们尝试用 UCB 算法解决这个问题。

如何做到...

我们可以使用 UCB 算法解决多臂赌博机广告问题,具体如下:

  1. 导入 PyTorch 和我们在第一个示例中开发的老丨虎丨机环境,《创建多臂老丨虎丨机环境》(假设 BanditEnv 类位于名为 multi_armed_bandit.py 的文件中):
>>> import torch
>>> from multi_armed_bandit import BanditEnv
  1. 定义三臂老丨虎丨机(例如三个广告候选项)的支付概率和奖励,并创建老丨虎丨机环境的实例:
>>> bandit_payout = [0.01, 0.015, 0.03]
>>> bandit_reward = [1, 1, 1]>>> bandit_env = BanditEnv(bandit_payout, bandit_reward)

在这里,广告 0 的真实点击率为 1%,广告 1 为 1.5%,广告 2 为 3%。

  1. 我们指定要运行的周期数,并定义包含通过选择各个臂累积的总奖励、选择各个臂的次数以及每个臂随时间的*均奖励的列表:
>>> n_episode = 100000
>>> n_action = len(bandit_payout)
>>> action_count = torch.tensor([0\. for _ in range(n_action)])
>>> action_total_reward = [0 for _ in range(n_action)]
>>> action_avg_reward = [[] for action in range(n_action)]
  1. 定义 UCB 策略函数,根据 UCB 公式计算最佳臂:
>>> def upper_confidence_bound(Q, action_count, t):
...     ucb = torch.sqrt((2 * torch.log(
 torch.tensor(float(t)))) / action_count) + Q
...     return torch.argmax(ucb)
  1. 初始化 Q 函数,即各个臂获得的*均奖励:
>>> Q = torch.empty(n_action)

我们将随时间更新 Q 函数。

  1. 现在,我们使用 UCB 策略运行 100,000 个周期。对于每个周期,我们还更新每个臂的统计信息:
>>> for episode in range(n_episode):
...     action = upper_confidence_bound(Q, action_count, episode)
...     reward = bandit_env.step(action)
...     action_count[action] += 1
...     action_total_reward[action] += reward
...     Q[action] = action_total_reward[action] / action_count[action]
...     for a in range(n_action):
...         if action_count[a]:
...             action_avg_reward[a].append(
 action_total_reward[a] / action_count[a])
...         else:
...             action_avg_reward[a].append(0)
  1. 运行 100,000 个周期后,我们绘制随时间变化的*均奖励结果:
>>> import matplotlib.pyplot as plt
>>> for action in range(n_action):
...     plt.plot(action_avg_reward[action])
>>> plt.legend([‘Arm {}’.format(action) for action in range(n_action)])
>>> plt.title(‘Average reward over time’)
>>> plt.xscale(‘log’)
>>> plt.xlabel(‘Episode’)
>>> plt.ylabel(‘Average reward’)
>>> plt.show()

它的工作原理…

在这个示例中,我们以多臂老丨虎丨机的方式解决了广告优化问题。它克服了 A/B 测试方法所面临的挑战。我们使用 UCB 算法解决多臂(多广告)老丨虎丨机问题;每个臂的奖励要么是 1,要么是 0。UCB(或其他算法如 epsilon-greedy 和 softmax 探索)动态地在开发和探索之间切换。对于数据点较少的广告,置信区间相对较宽,因此选择此动作具有相对高的不确定性。随着广告被选择的次数增多,置信区间变窄,并收敛到其实际值。

您可以在 第 7 步 中看到生成的图表如下:

图片

模型收敛后,广告 2 是预测的点击率(*均奖励)最高的广告。

最终,我们发现广告 2 是最优选择,这是真实的。而且我们越早发现这一点越好,因为我们会损失更少的潜在点击。在这个例子中,大约在 100 个周期后,广告 2 表现优于其他广告。

使用汤普森抽样算法解决多臂老丨虎丨机问题

在这个示例中,我们将使用另一种算法——汤普森抽样,解决广告老丨虎丨机问题中的开发和探索困境。我们将看到它与前三种算法的显著区别。

汤普森抽样TS)也称为贝叶斯老丨虎丨机,因为它从以下角度应用贝叶斯思维:

  • 这是一个概率算法。

  • 它计算每个臂的先验分布并从每个分布中抽样一个值。

  • 然后选择值最高的臂并观察奖励。

  • 最后,根据观察到的奖励更新先验分布。这个过程称为贝叶斯更新

正如我们在广告优化案例中看到的,每个臂的奖励要么是 1 要么是 0。我们可以使用贝塔分布作为我们的先验分布,因为贝塔分布的值在 0 到 1 之间。贝塔分布由两个参数α和β参数化。α表示我们获得奖励为 1 的次数,β表示我们获得奖励为 0 的次数。

为了帮助你更好地理解贝塔分布,我们将首先看几个贝塔分布,然后再实施 TS 算法。

怎么做……

让我们通过以下步骤来探索贝塔分布:

  1. 导入 PyTorch 和 matplotlib 因为我们将可视化分布的形状:
>>> import torch
>>> import matplotlib.pyplot as plt
  1. 我们首先通过起始位置α=1 和β=1 来可视化贝塔分布的形状:
>>> beta1 = torch.distributions.beta.Beta(1, 1)
>>> samples1 = [beta1.sample() for _ in range(100000)]
>>> plt.hist(samples1, range=[0, 1], bins=10)
>>> plt.title(‘beta(1, 1)’)
>>> plt.show()

你将看到以下的绘图:

显然,当α=1 且β=1 时,它不提供有关真实值在 0 到 1 范围内位置的任何信息。因此,它成为均匀分布。

  1. 我们随后用α=5 和β=1 来可视化贝塔分布的形状:
>>> beta2 = torch.distributions.beta.Beta(5, 1)
>>> samples2 = [beta2.sample() for _ in range(100000)]
>>> plt.hist(samples2, range=[0, 1], bins=10)
>>> plt.title(‘beta(5, 1)’)
>>> plt.show()

你将看到以下的绘图:

当α=5 且β=1 时,这意味着在 4 次实验中有 4 次连续的奖励为 1。分布向 1 偏移。

  1. 现在,让我们实验α=1 和β=5:
>>> beta3 = torch.distributions.beta.Beta(1, 5)
>>> samples3= [beta3.sample() for _ in range(100000)]
>>> plt.hist(samples3, range=[0, 1], bins=10)
>>> plt.title(‘beta(1, 5)’)
>>> plt.show()

你将看到以下的绘图:

当α=1 且β=5 时,这意味着在 4 次实验中有 4 次连续的奖励为 0。分布向 0 偏移。

  1. 最后,让我们看看当α=5 且β=5 时的情况:
>>> beta4 = torch.distributions.beta.Beta(5, 5)
>>> samples4= [beta4.sample() for _ in range(100000)]
>>> plt.hist(samples4, range=[0, 1], bins=10)
>>> plt.title(‘beta(5, 5)’)
>>> plt.show()

你将看到以下的绘图:

当α=5 且β=5 时,在 8 轮中观察到相同数量的点击和未点击。分布向中间点0.5偏移。

现在是时候使用汤普森采样算法来解决多臂老丨虎丨机广告问题了:

  1. 导入我们在第一个示例中开发的老丨虎丨机环境,创建多臂老丨虎丨机环境(假设BanditEnv类在名为multi_armed_bandit.py的文件中):
>>> from multi_armed_bandit import BanditEnv
  1. 定义三臂老丨虎丨机(三个广告候选项)的支付概率和奖励,并创建一个老丨虎丨机环境的实例:
>>> bandit_payout = [0.01, 0.015, 0.03]
>>> bandit_reward = [1, 1, 1]>>> bandit_env = BanditEnv(bandit_payout, bandit_reward)
  1. 我们指定要运行的剧集数,并定义包含通过选择各个臂累积的总奖励、选择各个臂的次数以及每个臂的*均奖励随时间变化的列表:
>>> n_episode = 100000
>>> n_action = len(bandit_payout)
>>> action_count = torch.tensor([0\. for _ in range(n_action)])
>>> action_total_reward = [0 for _ in range(n_action)]
>>> action_avg_reward = [[] for action in range(n_action)]
  1. 定义 TS 函数,从每个臂的贝塔分布中抽样一个值,并选择具有最高值的臂:
>>> def thompson_sampling(alpha, beta):
...     prior_values = torch.distributions.beta.Beta(alpha, beta).sample()
...     return torch.argmax(prior_values)
  1. 为每个臂初始化α和β:
>>> alpha = torch.ones(n_action)
>>> beta = torch.ones(n_action)

注意,每个贝塔分布的起始值应为α=β=1。

  1. 现在,我们使用 TS 算法运行了 100,000 个剧集。对于每个剧集,我们还根据观察到的奖励更新每个臂的 α 和 β:
>>> for episode in range(n_episode):
 ...     action = thompson_sampling(alpha, beta)
 ...     reward = bandit_env.step(action)
 ...     action_count[action] += 1
 ...     action_total_reward[action] += reward
 ...     if reward > 0:
 ...         alpha[action] += 1
 ...     else:
 ...         beta[action] += 1
 ...     for a in range(n_action):
 ...         if action_count[a]:
 ...             action_avg_reward[a].append(                         action_total_reward[a] / action_count[a])
 ...         else:
 ...             action_avg_reward[a].append(0)
  1. 运行 100,000 个剧集后,我们绘制了随时间变化的*均奖励结果:
>>> import matplotlib.pyplot as plt
>>> for action in range(n_action):
...     plt.plot(action_avg_reward[action])
>>> plt.legend([‘Arm {}’.format(action) for action in range(n_action)])
>>> plt.title(‘Average reward over time’)
>>> plt.xscale(‘log’)
>>> plt.xlabel(‘Episode’)
>>> plt.ylabel(‘Average reward’)
>>> plt.show()

工作原理...

在本文中,我们使用 TS 算法解决了广告赌博机问题。TS 与另外三种方法的最大区别在于采用贝叶斯优化。它首先计算每个可能臂的先验分布,然后从每个分布中随机抽取一个值。然后选择具有最高值的臂,并使用观察到的结果更新先验分布。TS 策略既是随机的又是贪婪的。如果某个广告更有可能获得点击,则其贝塔分布向 1 移动,因此随机样本的值趋向于更接* 1。

运行步骤 7 中的代码行后,您将看到以下图表:

广告 2 是最佳广告,预测的点击率(*均奖励)最高。

另请参阅

对于希望了解贝塔分布的人,可以随时查看以下链接:

解决互联网广告问题的上下文赌博机

您可能会注意到,在广告优化问题中,我们只关心广告本身,而忽略可能影响广告是否被点击的其他信息,例如用户信息和网页信息。在本文中,我们将讨论如何考虑超出广告本身的更多信息,并使用上下文赌博机解决这个问题。

到目前为止,我们处理过的多臂赌博机问题不涉及状态的概念,这与 MDPs 非常不同。我们只有几个动作,并且会生成与所选动作相关联的奖励。上下文赌博机通过引入状态的概念扩展了多臂赌博机。状态提供了环境的描述,帮助代理人采取更加明智的行动。在广告示例中,状态可以是用户的性别(两个状态,男性和女性)、用户的年龄组(例如四个状态)或页面类别(例如体育、财务或新闻)。直观地说,特定人口统计学的用户更有可能在某些页面上点击广告。

理解上下文赌博机并不难。一个多臂赌博机是一台具有多个臂的单机,而上下文赌博机是一组这样的机器(赌博机)。上下文赌博机中的每台机器是一个具有多个臂的状态。学习的目标是找到每台机器(状态)的最佳臂(动作)。

我们将以两个状态的广告示例为例。

如何做...

我们使用 UCB 算法解决上下文老丨虎丨机广告问题如下:

  1. 导入 PyTorch 和我们在第一个示例中开发的老丨虎丨机环境,创建一个多臂老丨虎丨机环境(假设BanditEnv类在名为multi_armed_bandit.py的文件中):
>>> import torch
>>> from multi_armed_bandit import BanditEnv
  1. 定义两个三臂老丨虎丨机的支付概率和奖励:
>>> bandit_payout_machines = [
...     [0.01, 0.015, 0.03],
...     [0.025, 0.01, 0.015]
... ]
>>> bandit_reward_machines = [
...     [1, 1, 1],
...     [1, 1, 1]
... ]

在这里,广告 0 的真实 CTR 为 1%,广告 1 为 1.5%,广告 2 为 3%适用于第一个状态,以及第二个状态的[2.5%,1%,1.5%]。

我们的情况下有两台老丨虎丨机:

>>> n_machine = len(bandit_payout_machines)

根据相应的支付信息创建一个老丨虎丨机列表:

>>> bandit_env_machines = [BanditEnv(bandit_payout, bandit_reward)
...           for bandit_payout, bandit_reward in
...           zip(bandit_payout_machines, bandit_reward_machines)]
  1. 我们指定要运行的剧集数,并定义包含在每个状态下选择各个臂时累计的总奖励、每个状态下选择各个臂的次数以及每个状态下各个臂随时间的*均奖励的列表:
>>> n_episode = 100000
>>> n_action = len(bandit_payout_machines[0])
>>> action_count = torch.zeros(n_machine, n_action)
>>> action_total_reward = torch.zeros(n_machine, n_action)
>>> action_avg_reward = [[[] for action in range(n_action)] for _ in range(n_machine)]
  1. 定义 UCB 策略函数,根据 UCB 公式计算最佳臂:
>>> def upper_confidence_bound(Q, action_count, t):
...     ucb = torch.sqrt((2 * torch.log( 
                 torch.tensor(float(t)))) / action_count) + Q
...     return torch.argmax(ucb)
  1. 初始化 Q 函数,这是在各个状态下使用各个臂获得的*均奖励:
>>> Q_machines = torch.empty(n_machine, n_action)

我们将随时间更新 Q 函数。

  1. 现在,我们使用 UCB 策略运行 100,000 个剧集。对于每个剧集,我们还更新每个状态下每个臂的统计数据:
>>> for episode in range(n_episode):
...     state = torch.randint(0, n_machine, (1,)).item()
...     action = upper_confidence_bound(                 Q_machines[state], action_count[state], episode)
...     reward = bandit_env_machines[state].step(action)
...     action_count[state][action] += 1
...     action_total_reward[state][action] += reward
...     Q_machines[state][action] =                              action_total_reward[state][action]                              / action_count[state][action]
...     for a in range(n_action):
...         if action_count[state][a]:
...             action_avg_reward[state][a].append(                             action_total_reward[state][a]                              / action_count[state][a])
...         else:
...             action_avg_reward[state][a].append(0)
  1. 运行 100,000 个剧集后,我们绘制每个状态随时间变化的*均奖励结果:
>>> import matplotlib.pyplot as plt
>>> for state in range(n_machine):
...     for action in range(n_action):
...         plt.plot(action_avg_reward[state][action])
...     plt.legend([‘Arm {}’.format(action)                      for action in range(n_action)])
...     plt.xscale(‘log’)
...     plt.title(       ‘Average reward over time for state {}’.format(state))
...     plt.xlabel(‘Episode’)
...     plt.ylabel(‘Average reward’)
...     plt.show()

工作原理如下...

在这个示例中,我们使用 UCB 算法解决了上下文广告问题的上下文老丨虎丨机问题。

运行步骤 7中的代码行,您将看到以下绘图。

我们得到了第一个状态的结果:

我们得到了第二个状态的结果:

给定第一个状态,广告 2 是最佳广告,具有最高的预测点击率。给定第二个状态,广告 0 是最佳广告,具有最高的*均奖励。这两者都是真实的。

上下文老丨虎丨机是一组多臂老丨虎丨机。每个老丨虎丨机代表环境的唯一状态。状态提供了环境的描述,帮助代理者采取更明智的行动。在我们的广告示例中,男性用户可能比女性用户更有可能点击广告。我们简单地使用了两台老丨虎丨机来包含两种状态,并在每种状态下寻找最佳的拉杆臂。

请注意,尽管上下文老丨虎丨机涉及状态的概念,但它们仍然与 MDP 有所不同。首先,上下文老丨虎丨机中的状态不是由先前的动作或状态决定的,而只是环境的观察。其次,上下文老丨虎丨机中没有延迟或折现奖励,因为老丨虎丨机剧集是一步。然而,与多臂老丨虎丨机相比,上下文老丨虎丨机更接* MDP,因为动作是环境状态的条件。可以说上下文老丨虎丨机介于多臂老丨虎丨机和完整 MDP 强化学习之间是安全的。

第六章:通过函数逼*扩展学习

到目前为止,在 MC 和 TD 方法中,我们已经以查找表的形式表示了值函数。TD 方法能够在一个 episode 中实时更新 Q 函数,这被认为是 MC 方法的进步。然而,TD 方法对于具有许多状态和/或动作的问题仍然不够可扩展。使用 TD 方法学习太多个状态和动作对的值将会非常缓慢。

本章将重点讲述函数逼*,这可以克服 TD 方法中的扩展问题。我们将从设置 Mountain Car 环境开始。在开发线性函数估计器之后,我们将其融入 Q-learning 和 SARSA 算法中。然后,我们将利用经验重放改进 Q-learning 算法,并尝试使用神经网络作为函数估计器。最后,我们将讨论如何利用本章学到的内容解决 CartPole 问题。

本章将涵盖以下示例:

  • 设置 Mountain Car 环境的游乐场

  • 使用梯度下降逼*估算 Q 函数

  • 使用线性函数逼*开发 Q-learning

  • 使用线性函数逼*开发 SARSA

  • 使用经验重放进行批处理

  • 使用神经网络函数逼*开发 Q-learning

  • 使用函数逼*解决 CartPole 问题

设置 Mountain Car 环境的游乐场

TD 方法可以在一个 episode 中学习 Q 函数,但不具备可扩展性。例如,国际象棋游戏的状态数约为 1,040 个,围棋游戏为 1,070 个。此外,使用 TD 方法学习连续状态的值似乎是不可行的。因此,我们需要使用函数逼*(FA)来解决这类问题,它使用一组特征来逼*状态空间。

在第一个示例中,我们将开始熟悉 Mountain Car 环境,我们将在接下来的示例中使用 FA 方法来解决它。

Mountain Car (gym.openai.com/envs/MountainCar-v0/) 是一个具有连续状态的典型 Gym 环境。如下图所示,其目标是将车辆驶上山顶:

在一维轨道上,车辆位于-1.2(最左侧)到 0.6(最右侧)之间,目标(黄旗)位于 0.5 处。车辆的引擎不足以使其在单次通过中驱动到顶部,因此它必须来回驾驶以积累动量。因此,每一步有三个离散动作:

  • 向左推(0)

  • 无推力(1)

  • 向右推(2)

环境有两个状态:

  • 车的位置:这是一个从-1.2 到 0.6 的连续变量。

  • 车的速度:这是一个从-0.07 到 0.07 的连续变量。

每一步的奖励为-1,直到汽车达到目标位置(位置为 0.5)。

一集结束时,汽车到达目标位置(显然),或者经过 200 步之后。

准备工作

要运行山车环境,让我们首先在环境表中搜索其名称 – github.com/openai/gym/wiki/Table-of-environments。我们得到了MountainCar-v0,还知道观察空间由两个浮点数表示,有三种可能的动作(左=0,无推力=1,右=2)。

如何操作...

让我们按照以下步骤模拟山车环境:

  1. 我们导入 Gym 库并创建山车环境的一个实例:
>>> import gym
>>> env = gym.envs.make("MountainCar-v0")
>>> n_action = env.action_space.n
>>> print(n_action)
3
  1. 重置环境:
>>> env.reset()
array([-0.52354759,  0\. ])

汽车从状态[-0.52354759, 0.]开始,这意味着初始位置大约在-0.5,速度为 0。由于初始位置是从-0.6 到-0.4 随机生成的,你可能会看到不同的初始位置。

  1. 现在让我们采取一种简单的方法:我们只需不断向右推车,希望它能够到达山顶:
>>> is_done = False
 >>> while not is_done:
 ...     next_state, reward, is_done, info = env.step(2)
 ...     print(next_state, reward, is_done)
 ...     env.render()
 >>> env.render()
 [-0.49286453  0.00077561] -1.0 False
 [-0.4913191   0.00154543] -1.0 False
 [-0.48901538  0.00230371] -1.0 False
 [-0.48597058  0.0030448 ] -1.0 False
 ......
 ......
 [-0.29239555 -0.0046231 ] -1.0 False
 [-0.29761694 -0.00522139] -1.0 False
 [-0.30340632 -0.00578938] -1.0 True
  1. 关闭环境:
env.close()

工作原理...

Step 3中,状态(位置和速度)会相应地改变,每一步的奖励是-1。

你也会在视频中看到,汽车反复向右移动,然后回到左边,但最终未能到达山顶:

正如你所想象的那样,山车问题并不像你想象的那么简单。我们需要来回驾驶汽车以积累动量。而状态变量是连续的,这意味着表格查找/更新方法(如 TD 方法)不起作用。在下一个配方中,我们将使用 FA 方法解决山车问题。

使用梯度下降逼*估计 Q 函数

从这个配方开始,我们将开发 FA 算法来解决具有连续状态变量的环境。我们将从使用线性函数和梯度下降逼* Q 函数开始。

FA的主要思想是使用一组特征来估计 Q 值。这对于具有大状态空间的过程非常有用,其中 Q 表格变得非常庞大。有几种方法可以将特征映射到 Q 值上;例如,线性逼*是特征的线性组合和神经网络。通过线性逼*,动作的状态值函数可以用特征的加权和表示:

在这里,F1(s),F2(s),……,Fn(s)是给定输入状态 s 的一组特征;θ1,θ2,……,θn 是应用于相应特征的权重。或者我们可以将其表示为 V(s)=θF(s)。

正如我们在 TD 方法中所见,我们有以下公式来计算未来的状态:

在这里,r 是从状态 st 转换到 st+1 获得的相关奖励,α是学习率,γ是折扣因子。让我们将δ表示为 TD 误差项,现在我们有以下内容:

这与梯度下降的确切形式相同。因此,学习的目标是找到最优权重θ,以最佳方式逼*每个可能动作的状态值函数 V(s)。在这种情况下,我们尝试最小化的损失函数类似于回归问题中的损失函数,即实际值和估计值之间的均方误差。在每个 episode 的每一步之后,我们都有一个真实状态值的新估计,并且我们将权重θ朝向它们的最优值前进一步。

还要注意的一件事是特征集 F(s),给定输入状态 s。一个好的特征集能够捕捉不同输入的动态。通常,我们可以在各种参数下使用一组高斯函数生成一组特征,包括均值和标准差。

如何做到…

我们基于线性函数开发 Q 函数的逼*器如下:

  1. 导入所有必要的包:
>>> import torch
>>> from torch.autograd import Variable
>>> import math

变量包装了张量并支持反向传播。

  1. 然后,启动线性函数的Estimator类的__init__method
>>> class Estimator():
 ...     def __init__(self, n_feat, n_state, n_action, lr=0.05):
 ...         self.w, self.b = self.get_gaussian_wb(n_feat, n_state)
 ...         self.n_feat = n_feat
 ...         self.models = []
 ...         self.optimizers = []
 ...         self.criterion = torch.nn.MSELoss()
 ...         for _ in range(n_action):
 ...             model = torch.nn.Linear(n_feat, 1)
 ...             self.models.append(model)
 ...             optimizer = torch.optim.SGD(model.parameters(), lr)
 ...             self.optimizers.append(optimizer)

它接受三个参数:特征数量n_feat,状态数量和动作数量。它首先从高斯分布生成特征函数 F(s)的一组系数wb,稍后我们将定义。然后初始化n_action个线性模型,其中每个模型对应一个动作,并相应地初始化n_action个优化器。对于线性模型,我们在此处使用 PyTorch 的 Linear 模块。它接受n_feat个单元并生成一个输出,即一个动作的预测状态值。随机梯度下降优化器也与每个线性模型一起初始化。每个优化器的学习率为 0.05。损失函数是均方误差。

  1. 我们现在继续定义get_gaussian_wb方法,它生成特征函数 F(s)的一组系数wb
>>>     def get_gaussian_wb(self, n_feat, n_state, sigma=.2):
 ...         """
 ...         Generate the coefficients of the feature set from 
             Gaussian distribution
 ...         @param n_feat: number of features
 ...         @param n_state: number of states
 ...         @param sigma: kernel parameter
 ...         @return: coefficients of the features
 ...         """
 ...         torch.manual_seed(0)
 ...         w = torch.randn((n_state, n_feat)) * 1.0 / sigma
 ...         b = torch.rand(n_feat) * 2.0 * math.pi
 ...         return w, b

系数w是一个n_feat乘以n_state的矩阵,其值从由参数 sigma 定义的方差高斯分布生成;偏置b是从[0, 2π]均匀分布生成的n_feat值的列表。

注意,设置特定的随机种子(torch.manual_seed(0))非常重要,这样在不同运行中,状态始终可以映射到相同的特征。

  1. 接下来,我们开发将状态空间映射到特征空间的函数,基于wb
>>>     def get_feature(self, s):
 ...         """
 ...         Generate features based on the input state
 ...         @param s: input state
 ...         @return: features
 ...         """
 ...         features = (2.0 / self.n_feat) ** .5 * torch.cos(
                  torch.matmul(torch.tensor(s).float(), self.w) 
                  + self.b)
 ...         return features

状态 s 的特征生成如下:

使用余弦变换确保特征在[-1, 1]范围内,尽管输入状态的值可能不同。

  1. 由于我们已经定义了模型和特征生成,现在我们开发训练方法,用数据点更新线性模型:
>>>     def update(self, s, a, y):
 ...         """
 ...         Update the weights for the linear estimator with 
             the given training sample
 ...         @param s: state
 ...         @param a: action
 ...         @param y: target value
 ...         """
 ...         features = Variable(self.get_feature(s))
 ...         y_pred = self.modelsa
 ...         loss = self.criterion(y_pred, 
                     Variable(torch.Tensor([y])))
 ...         self.optimizers[a].zero_grad()
 ...         loss.backward()
 ...         self.optimizers[a].step()

给定一个训练数据点,它首先使用get_feature方法将状态转换为特征空间。然后将生成的特征馈送到给定动作a的当前线性模型中。预测结果连同目标值用于计算损失和梯度。然后通过反向传播更新权重θ。

  1. 下一个操作涉及使用当前模型预测每个动作在给定状态下的状态值:
>>>     def predict(self, s):
 ...         """
 ...         Compute the Q values of the state using 
                 the learning model
 ...         @param s: input state
 ...         @return: Q values of the state
 ...         """
 ...         features = self.get_feature(s)
 ...         with torch.no_grad():
 ...             return torch.tensor([model(features) 
                                     for model in self.models])

这就是关于Estimator类的全部内容。

  1. 现在,让我们玩弄一些虚拟数据。首先,创建一个Estimator对象,将一个二维状态映射到一个十维特征,并与一个可能的动作配合使用:
>>> estimator = Estimator(10, 2, 1)
  1. 现在,生成状态[0.5, 0.1]的特征。
>>> s1 = [0.5, 0.1]
>>> print(estimator.get_feature(s1))
tensor([ 0.3163, -0.4467, -0.0450, -0.1490,  0.2393, -0.4181, -0.4426, 0.3074,
         -0.4451,  0.1808])

正如您所看到的,生成的特征是一个 10 维向量。

  1. 对一系列状态和目标状态值(在本例中我们只有一个动作)进行估算器训练:
>>> s_list = [[1, 2], [2, 2], [3, 4], [2, 3], [2, 1]]
>>> target_list = [1, 1.5, 2, 2, 1.5]
>>> for s, target in zip(s_list, target_list):
...     feature = estimator.get_feature(s)
...     estimator.update(s, 0, target)
  1. 最后,我们使用训练好的线性模型来预测新状态的值:
>>> print(estimator.predict([0.5, 0.1]))
 tensor([0.6172])
>>> print(estimator.predict([2, 3]))
 tensor([0.8733])

对于状态[0.5, 0.1],预测的值与动作为 0.5847,而对于[2, 3],预测的值为 0.7969。

工作原理如下……

FA 方法通过比 TD 方法中的 Q 表计算更紧凑的模型来*似状态值。FA 首先将状态空间映射到特征空间,然后使用回归模型估算 Q 值。通过这种方式,学习过程变成了监督学习。类型回归模型包括线性模型和神经网络。在本文中,我们开发了一个基于线性回归的估算器。它根据从高斯分布中采样的系数生成特征。它通过梯度下降更新线性模型的权重,并根据状态预测 Q 值。

FA 显著减少了需要学习的状态数量,在 TD 方法中学习数百万个状态是不可行的。更重要的是,它能够推广到未见的状态,因为状态值是由给定输入状态的估计函数参数化的。

另请参阅

如果您对线性回归或梯度下降不熟悉,请查看以下资料:

开发具有线性函数逼*的 Q-learning

在前一篇文章中,我们基于线性回归开发了一个值估算器。我们将在 Q-learning 中使用这个估算器,作为我们 FA 旅程的一部分。

正如我们所看到的,Q-learning 是一种离线学习算法,它基于以下方程更新 Q 函数:

这里,s'是在状态s中采取动作a后得到的结果状态;r是相关的奖励;α是学习率;γ是折扣因子。此外,![] 表示行为策略是贪婪的,即在状态s'中选择最高的 Q 值以生成学习数据。在 Q-learning 中,根据ε-greedy 策略采取行动。同样地,Q-learning 与 FA 具有以下误差项:

我们的学习目标是将误差项最小化为零,这意味着估算的 V(st)应满足以下方程:

现在,目标是找到最优权重θ,例如 V(s)=θF(s),以最佳方式逼*每个可能动作的状态值函数 V(s)。在这种情况下,我们试图最小化的损失函数类似于回归问题中的损失函数,即实际值和估算值之间的均方误差。

如何做到…

让我们使用前一篇文章中开发的线性估算器linear_estimator.py中的Estimator,开发 Q-learning 与 FA:

  1. 导入必要的模块并创建一个 Mountain Car 环境:
>>> import gym
>>> import torch
>>> from linear_estimator import Estimator >>> env = gym.envs.make("MountainCar-v0")
  1. 然后,开始定义ε-greedy 策略:
>>> def gen_epsilon_greedy_policy(estimator, epsilon, n_action):
 ...     def policy_function(state):
 ...         probs = torch.ones(n_action) * epsilon / n_action
 ...         q_values = estimator.predict(state)
 ...         best_action = torch.argmax(q_values).item()
 ...         probs[best_action] += 1.0 - epsilon
 ...         action = torch.multinomial(probs, 1).item()
 ...         return action
 ...     return policy_function

这里的参数ε取值从 0 到 1,|A|表示可能的动作数,估算器用于预测状态-动作值。每个动作以ε/ |A|的概率被选中,而具有最高预测状态-动作值的动作则以 1- ε + ε/ |A|的概率被选中。

  1. 现在,定义执行使用线性估算器Estimator的 Q-learning 的函数:
>>> def q_learning(env, estimator, n_episode, gamma=1.0, 
                    epsilon=0.1, epsilon_decay=.99):
 ...     """
 ...     Q-Learning algorithm using Function Approximation
 ...     @param env: Gym environment
 ...     @param estimator: Estimator object
 ...     @param n_episode: number of episodes
 ...     @param gamma: the discount factor
 ...     @param epsilon: parameter for epsilon_greedy
 ...     @param epsilon_decay: epsilon decreasing factor
 ...     """
 ...     for episode in range(n_episode):
 ...         policy = gen_epsilon_greedy_policy(estimator, 
                   epsilon * epsilon_decay ** episode, n_action)
 ...         state = env.reset()
 ...         is_done = False
 ...         while not is_done:
 ...             action = policy(state)
 ...             next_state, reward, is_done, _ = env.step(action)
 ...             q_values_next = estimator.predict(next_state)
 ...             td_target = reward + 
                             gamma * torch.max(q_values_next)
 ...             estimator.update(state, action, td_target)
 ...             total_reward_episode[episode] += reward
 ...
 ...             if is_done:
 ...                 break
 ...             state = next_state

q_learning()函数执行以下任务:

  • 在每个 episode 中,创建一个ε-greedy 策略,其中ε因子衰减到 99%(例如,如果第一个 episode 中的ε为 0.1,则第二个 episode 中的ε将为 0.099)。

  • 运行一个 episode:在每一步中,根据ε-greedy 策略采取一个动作a;使用当前的估算器计算新状态的Q值;然后计算目标值,![],并用它来训练估算器。

  • 运行n_episode个 episode 并记录每个 episode 的总奖励。

  1. 我们指定特征数量为200,学习率为0.03,并相应地创建一个估算器:
>>> n_state = env.observation_space.shape[0]
>>> n_action = env.action_space.n
>>> n_feature = 200
>>> lr = 0.03 >>> estimator = Estimator(n_feature, n_state, n_action, lr)
  1. 我们使用 FA 进行 300 个 episode 的 Q-learning,并且记录每个 episode 的总奖励:
>>> n_episode = 300
>>> total_reward_episode = [0] * n_episode
>>> q_learning(env, estimator, n_episode, epsilon=0.1)
  1. 然后,我们显示随时间变化的 episode 长度的图表:
>>> import matplotlib.pyplot as plt
>>> plt.plot(total_reward_episode)
>>> plt.title('Episode reward over time')
>>> plt.xlabel('Episode')
>>> plt.ylabel('Total reward')
>>> plt.show()

工作原理是…

如您所见,在 Q 学习中使用函数逼*时,它尝试学习最佳权重以便最佳估计 Q 值。它与 TD Q 学习类似,因为它们都从另一个策略生成学习数据。对于具有大状态空间的环境,Q 学习使用一组回归模型和潜在特征来*似 Q 值,而 TD Q 学习则需要精确的表查找来更新 Q 值。Q 学习使用函数逼*在每一步之后更新回归模型,这也使它类似于 TD Q 学习方法。

在训练 Q 学习模型后,我们只需使用回归模型预测所有可能动作的状态-动作值,并在给定状态时选择值最大的动作。在步骤 6中,我们导入pyplot以绘制所有奖励,结果如下图所示:

您可以看到,在大多数情况下,经过前 25 次迭代后,汽车在约 130 到 160 步内到达山顶。

开发使用线性函数逼*的 SARSA

我们在前面的步骤中使用了离策略 Q 学习算法成功解决了 Mountain Car 问题。现在,我们将使用状态-动作-奖励-状态-动作SARSA)算法(当然是 FA 版本)来完成此任务。

一般来说,SARSA 算法根据以下方程更新 Q 函数:

这里,s'是在状态s中采取动作a后的结果状态;r是相关奖励;α是学习率;γ是折扣因子。我们通过遵循ε-greedy 策略来选择下一个动作a'来更新Q值。然后在下一步中执行动作a'。因此,带有函数逼*的 SARSA 具有以下误差项:

我们的学习目标是将误差项最小化为零,这意味着估计的 V(st)应满足以下方程:

现在,目标是找到最优权重θ,如 V(s)=θF(s),以最佳方式逼*每个可能动作的状态值函数 V(s)。在这种情况下,我们试图最小化的损失函数类似于回归问题中的损失函数,即实际值与估计值之间的均方误差。

如何做…

让我们使用在用梯度下降逼*估算 Q 函数食谱中开发的线性估计器linear_estimator.py中的Estimator,来开发使用线性估计的 SARSA。

  1. 导入必要的模块并创建一个 Mountain Car 环境:
>>> import gym
>>> import torch
>>> from linear_estimator import Estimator >>> env = gym.envs.make("MountainCar-v0")
  1. 我们将重用上一步骤中开发的ε-greedy 策略函数,使用线性函数逼*开发 Q 学习

  2. 现在,定义执行带有函数逼*的 SARSA 算法的函数:

>>> def sarsa(env, estimator, n_episode, gamma=1.0, 
                 epsilon=0.1, epsilon_decay=.99):
 ...     """
 ...     SARSA algorithm using Function Approximation
 ...     @param env: Gym environment
 ...     @param estimator: Estimator object
 ...     @param n_episode: number of episodes
 ...     @param gamma: the discount factor
 ...     @param epsilon: parameter for epsilon_greedy
 ...     @param epsilon_decay: epsilon decreasing factor
 ...     """
 ...     for episode in range(n_episode):
 ...         policy = gen_epsilon_greedy_policy(estimator, 
                             epsilon * epsilon_decay ** episode,
                             env.action_space.n)
 ...         state = env.reset()
 ...         action = policy(state)
 ...         is_done = False
 ...
 ...         while not is_done:
 ...             next_state, reward, done, _ = env.step(action)
 ...             q_values_next = estimator.predict(next_state)
 ...             next_action = policy(next_state)
 ...             td_target = reward + 
                             gamma * q_values_next[next_action]
 ...             estimator.update(state, action, td_target)
 ...             total_reward_episode[episode] += reward
 ...
 ...             if done:
 ...                 break
 ...             state = next_state
 ...             action = next_action

sarsa()函数执行以下任务:

  • 在每一集中,创建一个带有衰减至 99%的ε-greedy 策略。

  • 运行一个 episode:在每一步中,根据ε-greedy 策略选择一个动作a;在新状态中,根据ε-greedy 策略选择一个新动作;然后,使用当前估算器计算新状态的 Q 值;计算目标值![],并用它来更新估算器。

  • 运行n_episode个 episode 并记录每个 episode 的总奖励。

  1. 我们将特征数指定为 200,学习率为 0.03,并相应地创建一个估算器:
>>> n_state = env.observation_space.shape[0]
>>> n_action = env.action_space.n
>>> n_feature = 200
>>> lr = 0.03
>>> estimator = Estimator(n_feature, n_state, n_action, lr)
  1. 然后我们对 FA 执行 300 个 episode 的 SARSA,并且还跟踪每个 episode 的总奖励:
>>> n_episode = 300
>>> total_reward_episode = [0] * n_episode
>>> sarsa(env, estimator, n_episode, epsilon=0.1)
  1. 然后,我们显示随时间变化的 episode 长度的图表:
>>> import matplotlib.pyplot as plt
>>> plt.plot(total_reward_episode)
>>> plt.title('Episode reward over time')
>>> plt.xlabel('Episode')
>>> plt.ylabel('Total reward')
>>> plt.show()

工作原理是…

使用 FA 的 SARSA 尝试学习最佳权重以估算最佳的 Q 值。它通过选择在同一策略下选择的动作来优化估算,而不是像 Q-learning 中那样从另一种策略中学习经验。

类似地,训练完 SARSA 模型后,我们只需使用回归模型预测所有可能动作的状态-动作值,并在给定状态时选择具有最大值的动作。

第 6 步中,我们使用pyplot绘制奖励,将得到以下图表:

您可以看到,在大多数 episode 中,经过前 100 个 episode 后,汽车在大约 130 到 160 步内到达山顶。

使用经验重放来进行批处理整合

在前两个配方中,我们分别开发了两种 FA 学习算法:离线策略和在线策略。在本配方中,我们将通过引入经验重放来提高离线 Q-learning 的性能。

经验重放意味着我们在一个 episode 期间存储 agent 的经验,而不是运行 Q-learning。带有经验重放的学习阶段变成了两个阶段:获得经验和在 episode 完成后根据获得的经验更新模型。具体来说,经验(也称为缓冲区或内存)包括个别步骤中的过去状态、执行的动作、接收的奖励和下一个状态。

在学习阶段中,从经验中随机采样一定数量的数据点,并用于训练学习模型。经验重放可以通过提供一组低相关性样本来稳定训练,从而提高学习效率。

如何做…

让我们将经验重放应用于使用线性估算器Estimator的 FA Q-learning,该估算器来自我们在上一个配方中开发的使用梯度下降逼*估算 Q 函数

  1. 导入必要的模块并创建一个 Mountain Car 环境:
>>> import gym
 >>> import torch
 >>> from linear_estimator import Estimator
 >>> from collections import deque
 >>> import random >>> env = gym.envs.make("MountainCar-v0")
  1. 我们将在前一节开发带有线性函数*似的 Q-learning中开发的ε-greedy 策略函数进行重用。

  2. 然后,将特征数量指定为200,学习率指定为0.03,并相应地创建估算器:

>>> n_state = env.observation_space.shape[0]
 >>> n_action = env.action_space.n
 >>> n_feature = 200
 >>> lr = 0.03
 >>> estimator = Estimator(n_feature, n_state, n_action, lr)
  1. 接下来,定义保存经验的缓冲区:
>>> memory = deque(maxlen=400)

将新样本追加到队列中,并在队列中有超过 400 个样本时移除旧样本。

  1. 现在,定义执行带有经验重播 FA Q-learning 的函数:
>>> def q_learning(env, estimator, n_episode, replay_size, 
                 gamma=1.0, epsilon=0.1, epsilon_decay=.99):
 ...     """
 ...     Q-Learning algorithm using Function Approximation, 
             with experience replay
 ...     @param env: Gym environment
 ...     @param estimator: Estimator object
 ...     @param replay_size: number of samples we use to 
                             update the model each time
 ...     @param n_episode: number of episode
 ...     @param gamma: the discount factor
 ...     @param epsilon: parameter for epsilon_greedy
 ...     @param epsilon_decay: epsilon decreasing factor
 ...     """
 ...     for episode in range(n_episode):
 ...         policy = gen_epsilon_greedy_policy(estimator, 
                             epsilon * epsilon_decay ** episode,
                             n_action)
 ...         state = env.reset()
 ...         is_done = False
 ...         while not is_done:
 ...             action = policy(state)
 ...             next_state, reward, is_done, _ = env.step(action)
 ...             total_reward_episode[episode] += reward
 ...             if is_done:
 ...                 break
 ...
 ...             q_values_next = estimator.predict(next_state)
 ...             td_target = reward + 
                             gamma * torch.max(q_values_next)
 ...             memory.append((state, action, td_target))
 ...             state = next_state
 ...
 ...         replay_data = random.sample(memory, 
                              min(replay_size, len(memory)))
 ...         for state, action, td_target in replay_data:
 ...             estimator.update(state, action, td_target)

该函数执行以下任务:

  • 在每个周期中,创建一个 epsilon-greedy 策略,其中 epsilon 因子衰减到 99%(例如,如果第一个周期的 epsilon 为 0.1,则第二个周期为 0.099)。

  • 运行一个周期:在每个步骤中,根据 epsilon-greedy 策略选择一个动作a;使用当前估算器计算新状态的Q值;然后计算目标值,![],并将状态、动作和目标值元组存储在缓冲内存中。

  • 每个周期结束后,从缓冲内存中随机选择replay_size个样本,并使用它们来训练估算器。

  • 运行n_episode个周期,并记录每个周期的总奖励。

  1. 我们执行了 1,000 个周期的经验重播 Q-learning:
>>> n_episode = 1000

我们需要更多的周期,仅仅因为模型尚未充分训练,所以代理在早期周期中采取随机步骤。

我们将 190 设置为重播样本大小:

>>> replay_size = 190

我们还会跟踪每个周期的总奖励:

>>> total_reward_episode = [0] * n_episode
>>> q_learning(env, estimator, n_episode, replay_size, epsilon=0.1)
  1. 现在,我们展示随时间变化的情节长度的图表:
>>> import matplotlib.pyplot as plt
>>> plt.plot(total_reward_episode)
>>> plt.title('Episode reward over time')
>>> plt.xlabel('Episode')
>>> plt.ylabel('Total reward')
>>> plt.show()

这将导致以下图表:

您可以看到使用经验重播的 Q-learning 性能变得更加稳定。在第一个 500 个周期后,大多数周期的奖励保持在-160 至-120 的范围内。

运作方式...

在这个示例中,我们使用 FA Q-learning 解决了 Mountain Car 问题,同时使用了经验重播。它比纯 FA Q-learning 表现更好,因为我们使用经验重播收集了更少的校正训练数据。我们不会急于训练估算器,而是首先将在周期期间观察到的数据点存储在缓冲区中,然后我们从缓冲区中随机选择一个样本批次并训练估算器。这形成了一个输入数据集,其中样本之间更独立,从而使训练更加稳定和高效。

开发带有神经网络函数逼*的 Q-learning

正如前文所述,我们还可以使用神经网络作为逼*函数。在这个示例中,我们将使用神经网络对 Q-learning 进行逼*来解决 Mountain Car 环境问题。

FA 的目标是使用一组特征通过回归模型估计 Q 值。使用神经网络作为估算模型,通过在隐藏层中引入非线性激活增加回归模型的灵活性(多层神经网络)和非线性。Q-learning 模型的其余部分与线性逼*非常相似。我们还使用梯度下降来训练网络。学习的最终目标是找到网络的最优权重,以最佳逼*每个可能动作的状态值函数 V(s)。我们试图最小化的损失函数也是实际值与估计值之间的均方误差。

如何做到这一点…

让我们从实现基于神经网络的估计器开始。我们将重用我们在使用梯度下降逼*估算 Q 函数一节中开发的线性估计器的大部分部分。不同之处在于,我们将输入层和输出层连接到一个隐藏层,然后是一个激活函数,在这种情况下是一个 ReLU(修正线性单元)函数。因此,我们只需要按照以下方式修改__init__方法:

>>> class Estimator():
 ...     def __init__(self, n_feat, n_state, n_action, lr=0.05):
 ...         self.w, self.b = self.get_gaussian_wb(n_feat, n_state)
 ...         self.n_feat = n_feat
 ...         self.models = []
 ...         self.optimizers = []
 ...         self.criterion = torch.nn.MSELoss()
 ...         for _ in range(n_action):
 ...             model = torch.nn.Sequential(
 ...                              torch.nn.Linear(n_feat, n_hidden),
 ...                              torch.nn.ReLU(),
 ...                              torch.nn.Linear(n_hidden, 1)
 ...             )
 ...             self.models.append(model)
 ...             optimizer = torch.optim.Adam(model.parameters(), lr)
 ...             self.optimizers.append(optimizer)

正如你所见,隐藏层有n_hidden个节点,以及一个 ReLU 激活函数torch.nn.ReLU(),在隐藏层之后,接着是生成估算值的输出层。

神经网络Estimator的其他部分与线性Estimator相同。你可以将它们复制到nn_estimator.py文件中。

现在,我们继续使用经验回放的神经网络进行 Q-learning 如下:

  1. 导入必要的模块,包括我们刚刚开发的神经网络估计器Estimator,从nn_estimator.py中,并创建一个 Mountain Car 环境:
>>> import gym
>>> import torch
>>> from nn_estimator import Estimator
>>> from collections import deque
>>> import random >>> env = gym.envs.make("MountainCar-v0")
  1. 我们将重用在开发带有线性函数逼*的 Q-learning一节中开发的 epsilon-贪婪策略函数。

  2. 接着,我们将特征数设定为 200,学习率设定为 0.001,隐藏层大小设定为 50,并相应地创建一个估计器:

>>> n_state = env.observation_space.shape[0]
>>> n_action = env.action_space.n
>>> n_feature = 200
>>> n_hidden = 50
>>> lr = 0.001 
>>> estimator = Estimator(n_feature, n_state, n_action, n_hidden, lr)
  1. 接下来,定义保存经验的缓冲区:
>>> memory = deque(maxlen=300)

新样本将被附加到队列中,只要队列中有超过 300 个样本,旧样本就会被移除。

  1. 我们将重用我们在前一节使用经验回放进行批处理中开发的q_learning函数。它执行带有经验回放的 FA Q-learning。

  2. 我们进行经验回放的 Q-learning,共 1,000 个 episodes,并将 200 设置为回放样本大小。

>>> n_episode = 1000
>>> replay_size = 200

我们还会跟踪每个 episode 的总奖励:

>>> total_reward_episode = [0] * n_episode
>>> q_learning(env, estimator, n_episode, replay_size, epsilon=0.1)
  1. 然后,我们显示随时间变化的 episode 长度的图表:
>>> import matplotlib.pyplot as plt
>>> plt.plot(total_reward_episode)
>>> plt.title('Episode reward over time')
>>> plt.xlabel('Episode')
>>> plt.ylabel('Total reward')
>>> plt.show()

工作原理…

使用神经网络进行 FA 非常类似于线性函数逼*。它不再使用简单的线性函数,而是使用神经网络将特征映射到目标值。算法的其余部分基本相同,但由于神经网络的更复杂结构和非线性激活,具有更高的灵活性和更强的预测能力。

第 7 步中,我们绘制随时间变化的情节长度图表,结果如下:

图像

您可以看到,与使用线性函数相比,使用神经网络的 Q-learning 性能更好。在第一个 500 个情节之后,大多数情节的奖励保持在-140 到-85 的范围内。

参见

如果您想了解有关神经网络的知识,请查看以下材料:

使用函数逼*解决 CartPole 问题

这是本章的一个额外配方,在这里我们将使用 FA 解决 CartPole 问题。

正如我们在第一章中看到的,开始使用强化学习和 PyTorch,我们在模拟 CartPole 环境配方中模拟了 CartPole 环境,并分别使用随机搜索、爬山和策略梯度算法解决了环境,包括实施和评估随机搜索策略开发爬山算法开发策略梯度算法。现在,让我们尝试使用本章讨论的内容解决 CartPole 问题。

如何做到...

我们演示了基于神经网络的 FA 解决方案,没有经验重演如下:

  1. 导入必要的模块,包括神经网络Estimator,从我们在上一个配方中开发的nn_estimator.py中创建 CartPole 环境:
>>> import gym
>>> import torch
>>> from nn_estimator import Estimator >>> env = gym.envs.make("CartPole-v0")
  1. 我们将重用在上一个配方中开发的 epsilon-greedy 策略函数,使用线性函数逼*开发 Q-learning

  2. 我们然后指定特征数量为 400(请注意 CartPole 环境的状态空间是 4 维),学习率为 0.01,隐藏层大小为 100,并相应地创建神经网络估计器:

>>> n_state = env.observation_space.shape[0]
 >>> n_action = env.action_space.n
 >>> n_feature = 400
 >>> n_hidden = 100
 >>> lr = 0.01 
 >>> estimator = Estimator(n_feature, n_state, n_action, n_hidden, lr)
  1. 我们将重用在上一个配方中开发的q_learning函数,使用线性函数逼*开发 Q-learning。这执行 FA Q-learning。

  2. 我们进行了 1,000 个情节的 FA Q-learning,并跟踪每个情节的总奖励:

>>> n_episode = 1000
 >>> total_reward_episode = [0] * n_episode
 >>> q_learning(env, estimator, n_episode, epsilon=0.1)
  1. 最后,我们展示随时间变化的情节长度的图表:
>>> import matplotlib.pyplot as plt
 >>> plt.plot(total_reward_episode)
 >>> plt.title('Episode reward over time')
 >>> plt.xlabel('Episode')
 >>> plt.ylabel('Total reward')
 >>> plt.show()

它的工作原理...

我们使用神经网络中的 FA 算法解决了 CartPole 问题。请注意,环境具有四维观测空间,是 Mountain Car 的两倍,因此我们直观地增加了我们使用的特征数和隐藏层的大小。可以自由地尝试使用神经网络的 SARSA 或经验回放的 Q-learning,并查看它们是否表现更好。

第 6 步中,我们绘制了随时间变化的集数长度,结果如下图所示:

大多数情况下,从第 300 集后的总奖励值为最大值+200。

第七章:深度 Q 网络的实际应用

深度 Q 学习,或使用深度 Q 网络,被认为是最现代的强化学习技术。在本章中,我们将逐步开发各种深度 Q 网络模型,并将其应用于解决几个强化学习问题。我们将从基本的 Q 网络开始,并通过经验重播来增强它们。我们将通过使用额外的目标网络来提高鲁棒性,并演示如何微调深度 Q 网络。我们还将尝试决斗深度 Q 网络,并看看它们的价值函数如何与其他类型的深度 Q 网络不同。在最后两个实例中,我们将通过将卷积神经网络整合到深度 Q 网络中来解决复杂的 Atari 游戏问题。

本章将介绍以下内容:

  • 开发深度 Q 网络

  • 通过经验重播改进 DQN

  • 开发双重深度 Q 网络

  • 调整 CartPole 的双重 DQN 超参数

  • 开发决斗深度 Q 网络

  • 将深度 Q 网络应用于 Atari 游戏

  • 使用卷积神经网络玩 Atari 游戏

开发深度 Q 网络

您将回忆起函数逼*FA)是使用从原始状态生成的一组特征来逼*状态空间。深度 Q 网络DQN)与使用神经网络进行特征逼*非常相似,但它们直接使用神经网络将状态映射到动作值,而不是使用生成的特征作为媒介。

在深度 Q 学习中,神经网络被训练以输出每个动作给定输入状态 s 下的适当 Q(s,a) 值。根据 epsilon-greedy 策略选择代理的动作 a,基于输出 Q(s,a) 值。具有两个隐藏层的 DQN 结构如下图所示:

您将回忆起 Q 学习是一种离线学习算法,并且它根据以下方程更新 Q 函数:

在这里,s' 是在状态 s 中采取动作 a 后得到的结果状态;r 是相关的奖励;α 是学习率;γ 是折扣因子。同时,![] 表示行为策略是贪婪的,其中在状态 s' 中选择最高的 Q 值以生成学习数据。类似地,DQN 学习以最小化以下误差项:

现在,目标变成寻找最佳网络模型以最好地逼*每个可能动作的状态值函数 Q(s, a)。在这种情况下,我们试图最小化的损失函数类似于回归问题中的均方误差,即实际值和估计值之间的均方误差。

现在,我们将开发一个 DQN 模型来解决 Mountain Car(gym.openai.com/envs/MountainCar-v0/)问题。

怎么做……

我们使用 DQN 开发深度 Q 学习如下:

  1. 导入所有必要的包:
>>> import gym
>>> import torch
>>> from torch.autograd import Variable
>>> import random

变量包装了一个张量并支持反向传播。

  1. 让我们从 DQN 类的 __init__ 方法开始:
>>> class DQN():
 ...     def __init__(self, n_state, n_action, n_hidden=50, 
                     lr=0.05):
 ...         self.criterion = torch.nn.MSELoss()
 ...         self.model = torch.nn.Sequential(
 ...                         torch.nn.Linear(n_state, n_hidden),
 ...                         torch.nn.ReLU(),
 ...                         torch.nn.Linear(n_hidden, n_action)
 ...                 )
 ...         self.optimizer = torch.optim.Adam( self.model.parameters(), lr)
  1. 现在我们开发训练方法,用于更新神经网络与数据点:
>>>     def update(self, s, y):
 ...         """
 ...         Update the weights of the DQN given a training sample
 ...         @param s: state
 ...         @param y: target value
 ...         """
 ...         y_pred = self.model(torch.Tensor(s))
 ...         loss = self.criterion(y_pred, 
                         Variable(torch.Tensor(y)))
 ...         self.optimizer.zero_grad()
 ...         loss.backward()
 ...         self.optimizer.step()
  1. 接下来是给定状态预测每个动作的状态值:
>>>     def predict(self, s):
 ...     """
 ...     Compute the Q values of the state for all 
              actions using the learning model
 ...     @param s: input state
 ...     @return: Q values of the state for all actions
 ...     """
 ...     with torch.no_grad():
 ...          return self.model(torch.Tensor(s))

这就是 DQN 类的全部内容!现在我们可以继续开发学习算法了。

  1. 我们开始创建一个 Mountain Car 环境:
>>> env = gym.envs.make("MountainCar-v0")
  1. 然后,我们定义 epsilon-greedy 策略:
>>> def gen_epsilon_greedy_policy(estimator, epsilon, n_action):
 ...     def policy_function(state):
 ...         if random.random() < epsilon:
 ...             return random.randint(0, n_action - 1)
 ...         else:
 ...             q_values = estimator.predict(state)
 ...             return torch.argmax(q_values).item()
 ...     return policy_function
  1. 现在,使用 DQN 定义深度 Q 学习算法:
>>> def q_learning(env, estimator, n_episode, gamma=1.0,
                   epsilon=0.1, epsilon_decay=.99):
 ...     """
 ...     Deep Q-Learning using DQN
 ...     @param env: Gym environment
 ...     @param estimator: Estimator object
 ...     @param n_episode: number of episodes
 ...     @param gamma: the discount factor
 ...     @param epsilon: parameter for epsilon_greedy
 ...     @param epsilon_decay: epsilon decreasing factor
 ...     """
 ...     for episode in range(n_episode):
 ...         policy = gen_epsilon_greedy_policy( estimator, epsilon, n_action)
 ...         state = env.reset()
 ...         is_done = False
 ...         while not is_done:
 ...             action = policy(state)
 ...             next_state, reward, is_done, _ = env.step(action)
 ...             total_reward_episode[episode] += reward
 ...             modified_reward = next_state[0] + 0.5
 ...             if next_state[0] >= 0.5:
 ...                 modified_reward += 100
 ...             elif next_state[0] >= 0.25:
 ...                 modified_reward += 20
 ...             elif next_state[0] >= 0.1:
 ...                 modified_reward += 10
 ...             elif next_state[0] >= 0:
 ...                 modified_reward += 5
 ...
 ...             q_values = estimator.predict(state).tolist()
 ...
 ...             if is_done:
 ...                 q_values[action] = modified_reward
 ...                 estimator.update(state, q_values)
 ...                 break
 ...             q_values_next = estimator.predict(next_state)
 ...             q_values[action] = modified_reward + gamma * 
                             torch.max(q_values_next).item()
 ...             estimator.update(state, q_values)
 ...             state = next_state
 ...         print('Episode: {}, total reward: {}, epsilon: 
                     {}'.format(episode,
                     total_reward_episode[episode], epsilon))
 ...         epsilon = max(epsilon * epsilon_decay, 0.01)
  1. 然后,我们指定隐藏层的大小和学习率,并相应地创建一个 DQN 实例:
 >>> n_state = env.observation_space.shape[0]
 >>> n_action = env.action_space.n
 >>> n_hidden = 50
 >>> lr = 0.001
 >>> dqn = DQN(n_state, n_action, n_hidden, lr)
  1. 接下来,我们使用刚开发的 DQN 进行 1,000 个回合的深度 Q 学习,并且还跟踪每个回合的总(原始)奖励:
>>> n_episode = 1000
>>> total_reward_episode = [0] * n_episode
>>> q_learning(env, dqn, n_episode, gamma=.99, epsilon=.3)
 Episode: 0, total reward: -200.0, epsilon: 0.3
 Episode: 1, total reward: -200.0, epsilon: 0.297
 Episode: 2, total reward: -200.0, epsilon: 0.29402999999999996
 ……
 ……
 Episode: 993, total reward: -177.0, epsilon: 0.01
 Episode: 994, total reward: -200.0, epsilon: 0.01
 Episode: 995, total reward: -172.0, epsilon: 0.01
 Episode: 996, total reward: -200.0, epsilon: 0.01
 Episode: 997, total reward: -200.0, epsilon: 0.01
 Episode: 998, total reward: -173.0, epsilon: 0.01
 Episode: 999, total reward: -200.0, epsilon: 0.01
  1. 现在,让我们展示随时间变化的回合奖励图:
>>> import matplotlib.pyplot as plt
>>> plt.plot(total_reward_episode)
>>> plt.title('Episode reward over time')
>>> plt.xlabel('Episode')
>>> plt.ylabel('Total reward')
>>> plt.show()

工作原理是...

Step 2 中,DQN 类接受四个参数:输入状态数和输出动作数,隐藏节点数(我们这里只使用一个隐藏层作为示例),以及学习率。它初始化一个具有一个隐藏层的神经网络,并使用 ReLU 激活函数。它接收 n_state 个单位并生成一个 n_action 的输出,这些是各个动作的预测状态值。一个优化器 Adam 与每个线性模型一起初始化。损失函数是均方误差。

Step 3 用于更新网络:给定一个训练数据点,使用预测结果和目标值计算损失和梯度。然后通过反向传播更新神经网络模型。

Step 7 中,深度 Q 学习函数执行以下任务:

  • 在每个回合中,创建一个 epsilon-greedy 策略,其中 epsilon 的因子衰减到 99%(例如,如果第一个回合中的 epsilon 是 0.1,则第二个回合中将为 0.099)。我们还将 0.01 设置为较低的 epsilon 限制。

  • 运行一个回合:在状态 s 的每一步中,根据 epsilon-greedy 策略选择动作 a;然后,使用 DQN 的 predict 方法计算上一个状态的 Qq_value

  • 计算新状态 s'Qq_values_next;然后,通过更新旧的 Qq_values 来计算目标值,用于动作的

  • 使用数据点 (s, Q(s)) 来训练神经网络。注意 Q(s) 包含所有动作的值。

  • 运行 n_episode 个回合并记录每个回合的总奖励。

您可能注意到,我们在训练模型时使用了修改版奖励。它基于汽车的位置,因为我们希望它达到+0.5 的位置。而且,我们还为大于或等于+0.5、+0.25、+0.1 和 0 的位置提供分层激励。这种修改后的奖励设置区分了不同的汽车位置,并偏爱接*目标的位置;因此,与每步-1 的原始单调奖励相比,它大大加速了学习过程。

最后,在第 10 步,您将看到如下的结果图:

您可以看到,在最后的 200 个回合中,汽车在大约 170 到 180 步后达到山顶。

深度 Q 学习用神经网络而不是一组中间人工特征更直接地逼*状态值。给定一个步骤,其中旧状态通过采取行动转换为新状态,并接收奖励,训练 DQN 涉及以下阶段:

  • 使用神经网络模型估计旧状态的Q值。

  • 使用神经网络模型估计新状态的Q值。

  • 使用奖励和新的Q值更新动作的目标 Q 值,如![]

  • 注意,如果是终端状态,目标 Q 值将更新为 r。

  • 使用旧状态作为输入,并将目标Q值作为输出训练神经网络模型。

它通过梯度下降更新网络的权重,并能预测给定状态的 Q 值。

DQN 显著减少了需要学习的状态数量,在 TD 方法中学习数百万个状态是不可行的。此外,它直接将输入状态映射到 Q 值,并不需要生成人工特征的额外函数。

另见

如果您对 Adam 优化器作为高级梯度下降方法不熟悉,请查看以下资料:

使用经验重播改进 DQNs

使用神经网络逐个样本逼* Q 值的*似并不非常稳定。您将回忆起,在 FA 中,我们通过经验重播来提高稳定性。同样,在这个配方中,我们将应用经验重播到 DQNs 中。

使用经验回放,我们在训练会话的每个周期内存储代理的经验(一个经验由旧状态、新状态、动作和奖励组成)到内存队列中。每当我们积累到足够的经验时,从内存中随机抽取一批经验,并用于训练神经网络。学习经验回放分为两个阶段:积累经验和基于随机选择的过去经验更新模型。否则,模型将继续从最*的经验中学习,神经网络模型可能会陷入局部最小值。

我们将开发具有经验回放功能的 DQN 来解决山车问题。

如何实现...

我们将如下开发具有经验回放的 DQN:

  1. 导入必要的模块并创建一个山车环境:
 >>> import gym
 >>> import torch
 >>> from collections import deque
 >>> import random
 >>> from torch.autograd import Variable >>> env = gym.envs.make("MountainCar-v0")
  1. 要添加经验回放功能,我们将在DQN类中添加一个replay方法:
>>> def replay(self, memory, replay_size, gamma):
 ...     """
 ...     Experience replay
 ...     @param memory: a list of experience
 ...     @param replay_size: the number of samples we use to 
             update the model each time
 ...     @param gamma: the discount factor
 ...     """
 ...     if len(memory) >= replay_size:
 ...         replay_data = random.sample(memory, replay_size)
 ...         states = []
 ...         td_targets = []
 ...         for state, action, next_state, reward, 
                                     is_done in replay_data:
 ...             states.append(state)
 ...             q_values = self.predict(state).tolist()
 ...             if is_done:
 ...                 q_values[action] = reward
 ...             else:
 ...                 q_values_next = self.predict(next_state)
 ...                 q_values[action] = reward + gamma * 
                         torch.max(q_values_next).item()
 ...             td_targets.append(q_values)
 ...
 ...         self.update(states, td_targets)

DQN类的其余部分保持不变。

  1. 我们将重用我们在开发深度 Q 网络配方中开发的gen_epsilon_greedy_policy函数,这里不再重复。

  2. 然后,我们指定神经网络的形状,包括输入的大小、输出和隐藏层的大小,将学习率设置为 0.001,并相应地创建一个 DQN:

 >>> n_state = env.observation_space.shape[0]
 >>> n_action = env.action_space.n
 >>> n_hidden = 50
 >>> lr = 0.001
 >>> dqn = DQN(n_state, n_action, n_hidden, lr)
  1. 接下来,我们定义存储经验的缓冲区:
>>> memory = deque(maxlen=10000)

如果队列中的样本超过10000个,则将新样本附加到队列中,并删除旧样本。

  1. 现在,我们定义执行经验回放的深度 Q 学习函数:
>>> def q_learning(env, estimator, n_episode, replay_size, 
             gamma=1.0, epsilon=0.1, epsilon_decay=.99):
 ...     """
 ...     Deep Q-Learning using DQN, with experience replay
 ...     @param env: Gym environment
 ...     @param estimator: Estimator object
 ...     @param replay_size: the number of samples we use to 
                 update the model each time
 ...     @param n_episode: number of episodes
 ...     @param gamma: the discount factor
 ...     @param epsilon: parameter for epsilon_greedy
 ...     @param epsilon_decay: epsilon decreasing factor
 ...     """
 ...     for episode in range(n_episode):
 ...         policy = gen_epsilon_greedy_policy( estimator, epsilon, n_action)
 ...         state = env.reset()
 ...         is_done = False
 ...         while not is_done:
 ...             action = policy(state)
 ...             next_state, reward, is_done, _ = env.step(action)
 ...             total_reward_episode[episode] += reward
 ...             modified_reward = next_state[0] + 0.5
 ...             if next_state[0] >= 0.5:
 ...                 modified_reward += 100
 ...             elif next_state[0] >= 0.25:
 ...                 modified_reward += 20
 ...             elif next_state[0] >= 0.1:
 ...                 modified_reward += 10
 ...             elif next_state[0] >= 0:
 ...                 modified_reward += 5
 ...             memory.append((state, action, next_state, 
                               modified_reward, is_done))
 ...             if is_done:
 ...                 break
 ...             estimator.replay(memory, replay_size, gamma)
 ...             state = next_state
 ...         print('Episode: {}, total reward: {}, epsilon: 
             {}'.format(episode, total_reward_episode[episode],
              epsilon))
 ...         epsilon = max(epsilon * epsilon_decay, 0.01)
  1. 然后,我们为600个周期执行具有经验回放的深度 Q 学习:
>>> n_episode = 600

我们将20设置为每步的重放样本大小:

>>> replay_size = 20

我们还跟踪每个周期的总奖励:

 >>> total_reward_episode = [0] * n_episode
 >>> q_learning(env, dqn, n_episode, replay_size, gamma=.9, epsilon=.3)
  1. 现在,是时候显示随时间变化的周期奖励图了:
 >>> import matplotlib.pyplot as plt
 >>> plt.plot(total_reward_episode)
 >>> plt.title('Episode reward over time')
 >>> plt.xlabel('Episode')
 >>> plt.ylabel('Total reward')
 >>> plt.show()

工作原理...

第 2 步中,经验回放函数首先随机选择replay_size个经验样本。然后,将每个经验转换为由输入状态和输出目标值组成的训练样本。最后,使用选定的批量更新神经网络。

第 6 步中,执行具有经验回放的深度 Q 学习,包括以下任务:

  • 在每个周期中,创建一个带有衰减到 99%的ε贪婪策略。

  • 运行一个周期:在每一步中,根据ε-贪婪策略选择一个动作a;将这一经验(旧状态、动作、新状态、奖励)存储在内存中。

  • 在每一步中,进行经验回放来训练神经网络,前提是我们有足够的训练样本可以随机选择。

  • 运行n_episode个周期,并记录每个周期的总奖励。

执行第 8 步中的代码行将产生以下图表:

您可以看到,在最后 200 个周期的大多数周期中,车在大约 120 到 160 步内达到了山顶。

在深度 Q 学习中,经验重放意味着我们为每一步存储代理的经验,并随机抽取过去经验的一些样本来训练 DQN。 在这种情况下,学习分为两个阶段:积累经验和基于过去经验批次更新模型。 具体而言,经验(也称为缓冲区内存)包括过去的状态、采取的动作、获得的奖励和下一个状态。 经验重放可以通过提供一组低相关性的样本来稳定训练,从而增加学习效率。

开发双深度 Q 网络

在我们迄今开发的深度 Q 学习算法中,同一个神经网络用于计算预测值和目标值。 这可能导致很多发散,因为目标值不断变化,而预测必须追赶它。 在这个配方中,我们将开发一种新的算法,使用两个神经网络代替一个。

双重 DQN中,我们使用一个单独的网络来估计目标,而不是预测网络。 这个单独的网络与预测网络具有相同的结构。 其权重在每个T集后固定(T是我们可以调整的超参数),这意味着它们仅在每个T集后更新。 更新是通过简单地复制预测网络的权重来完成的。 这样,目标函数在一段时间内保持不变,从而导致更稳定的训练过程。

数学上,双重 DQN 被训练来最小化以下误差项:

在这里,s'是采取行动a后的结果状态,r是相关的奖励;α是学习率;γ是折扣因子。 另外,![]是目标网络的函数,而 Q 是预测网络的函数。

现在让我们使用双重 DQN 来解决 Mountain Car 问题。

如何做…

我们按以下方式开发使用双重 DQN 的深度 Q 学习:

  1. 导入必要的模块并创建一个 Mountain Car 环境:
 >>> import gym
 >>> import torch
 >>> from collections import deque
 >>> import random
 >>> import copy
 >>> from torch.autograd import Variable >>> env = gym.envs.make("MountainCar-v0")
  1. 在经验重放阶段,为了整合目标网络,我们首先在DQN类的__init__方法中对其进行初始化:
>>> class DQN():
 ...     def __init__(self, n_state, n_action, 
                     n_hidden=50, lr=0.05):
 ...         self.criterion = torch.nn.MSELoss()
 ...         self.model = torch.nn.Sequential(
 ...                         torch.nn.Linear(n_state, n_hidden),
 ...                         torch.nn.ReLU(),
 ...                         torch.nn.Linear(n_hidden, n_action)
 ...                 )
 ...         self.optimizer = torch.optim.Adam( self.model.parameters(), lr)
 ...         self.model_target = copy.deepcopy(self.model)

目标网络与预测网络具有相同的结构。

  1. 因此,我们添加了使用目标网络计算值的计算:
>>>     def target_predict(self, s):
 ...         """
 ...         Compute the Q values of the state for all actions 
             using the target network
 ...         @param s: input state
 ...         @return: targeted Q values of the state for all actions
 ...         """
 ...         with torch.no_grad():
 ...             return self.model_target(torch.Tensor(s))
  1. 我们还添加了同步目标网络权重的方法:
>>>     def copy_target(self):
 ...         self.model_target.load_state_dict(self.model.state_dict())
  1. 在经验重放中,我们使用目标网络来计算目标值,而不是预测网络:
>>>     def replay(self, memory, replay_size, gamma):
 ...         """
 ...         Experience replay with target network
 ...         @param memory: a list of experience
 ...         @param replay_size: the number of samples 
             we use to update the model each time
 ...         @param gamma: the discount factor
 ...         """
 ...         if len(memory) >= replay_size:
 ...             replay_data = random.sample(memory, replay_size)
 ...             states = []
 ...             td_targets = []
 ...             for state, action, next_state, reward, is_done 
                                                 in replay_data:
 ...                 states.append(state)
 ...                 q_values = self.predict(state).tolist()
 ...                 if is_done:
 ...                     q_values[action] = reward
 ...                 else:
 ...                     q_values_next = self.target_predict( next_state).detach()
 ...                     q_values[action] = reward + gamma * 
                             torch.max(q_values_next).item()
 ...
 ...                 td_targets.append(q_values)
 ...
 ...             self.update(states, td_targets)

DQN 类的其余部分保持不变。

  1. 我们将重用我们在《深度 Q 网络开发》配方中开发的gen_epsilon_greedy_policy函数,并且这里不会重复它。

  2. 然后我们指定神经网络的形状,包括输入的大小、输出和隐藏层的大小,将0.01作为学习率,并相应地创建一个 DQN:

 >>> n_state = env.observation_space.shape[0]
 >>> n_action = env.action_space.n
 >>> n_hidden = 50
 >>> lr = 0.01
 >>> dqn = DQN(n_state, n_action, n_hidden, lr)
  1. 接下来,我们定义保存经验的缓冲区:
>>> memory = deque(maxlen=10000)

只要队列中有超过10000个样本,新样本将被追加到队列中,并移除旧样本。

  1. 现在,让我们开发双 DQN 的深度 Q 学习:
>>> def q_learning(env, estimator, n_episode, replay_size, 
         target_update=10, gamma=1.0, epsilon=0.1,
         epsilon_decay=.99):
 ...     """
 ...     Deep Q-Learning using double DQN, with experience replay
 ...     @param env: Gym environment
 ...     @param estimator: DQN object
 ...     @param replay_size: number of samples we use 
             to update the model each time
 ...     @param target_update: number of episodes before 
             updating the target network
 ...     @param n_episode: number of episodes
 ...     @param gamma: the discount factor
 ...     @param epsilon: parameter for epsilon_greedy
 ...     @param epsilon_decay: epsilon decreasing factor
 ...     """
 ...     for episode in range(n_episode):
 ...         if episode % target_update == 0:
 ...             estimator.copy_target()
 ...         policy = gen_epsilon_greedy_policy( estimator, epsilon, n_action)
 ...         state = env.reset()
 ...         is_done = False
 ...         while not is_done:
 ...             action = policy(state)
 ...             next_state, reward, is_done, _ = env.step(action)
 ...             total_reward_episode[episode] += reward
 ...             modified_reward = next_state[0] + 0.5
 ...             if next_state[0] >= 0.5:
 ...                 modified_reward += 100
 ...             elif next_state[0] >= 0.25:
 ...                 modified_reward += 20
 ...             elif next_state[0] >= 0.1:
 ...                 modified_reward += 10
 ...             elif next_state[0] >= 0:
 ...                 modified_reward += 5
 ...             memory.append((state, action, next_state, 
                             modified_reward, is_done))
 ...             if is_done:
 ...                 break
 ...             estimator.replay(memory, replay_size, gamma)
 ...             state = next_state
 ...         print('Episode: {}, total reward: {}, epsilon: {}'.format(episode, total_reward_episode[episode], epsilon))
 ...         epsilon = max(epsilon * epsilon_decay, 0.01)
  1. 我们执行双 DQN 的深度 Q 学习,共进行1000个回合:
>>> n_episode = 1000

我们将每一步的回放样本大小设置为20

>>> replay_size = 20

我们每 10 个回合更新一次目标网络:

>>> target_update = 10

我们还会跟踪每个回合的总奖励:

 >>> total_reward_episode = [0] * n_episode
 >>> q_learning(env, dqn, n_episode, replay_size, target_update, gamma=.9, epsilon=1)
 Episode: 0, total reward: -200.0, epsilon: 1
 Episode: 1, total reward: -200.0, epsilon: 0.99
 Episode: 2, total reward: -200.0, epsilon: 0.9801
 ……
 ……
 Episode: 991, total reward: -151.0, epsilon: 0.01
 Episode: 992, total reward: -200.0, epsilon: 0.01
 Episode: 993, total reward: -158.0, epsilon: 0.01
 Episode: 994, total reward: -160.0, epsilon: 0.01
 Episode: 995, total reward: -200.0, epsilon: 0.01
 Episode: 996, total reward: -200.0, epsilon: 0.01
 Episode: 997, total reward: -200.0, epsilon: 0.01
 Episode: 998, total reward: -151.0, epsilon: 0.01
 Episode: 999, total reward: -200.0, epsilon: 0.01 
  1. 然后,我们显示随时间变化的回合奖励图:
 >>> import matplotlib.pyplot as plt
 >>> plt.plot(total_reward_episode)
 >>> plt.title('Episode reward over time')
 >>> plt.xlabel('Episode')
 >>> plt.ylabel('Total reward')
 >>> plt.show()

工作原理...

Step 5中,经验回放函数首先随机选择replay_size个样本经验。然后将每个经验转换为由输入状态和输出目标值组成的训练样本。最后,使用选定的批次更新预测网络。

Step 9是双 DQN 中最重要的步骤:它使用不同的网络计算目标值,然后定期更新这个网络。函数的其余部分类似于带经验回放的深度 Q 学习。

Step 11中的可视化函数将生成以下图表:

你可以看到,在大多数情况下,经过第一个400个回合后,小车在大约80160步内到达山顶。

在双 DQN 的深度 Q 学习中,我们分别创建两个用于预测和目标计算的网络。第一个网络用于预测和检索Q值,而第二个网络用于提供稳定的目标Q值。并且,经过一段时间(比如每 10 个回合或 1500 个训练步骤),我们同步预测网络和目标网络。在这种双网络设置中,目标值是暂时固定的,而不是被不断修改的,因此预测网络有更稳定的目标来学习。我们获得的结果表明,双 DQN 优于单 DQN。

为 CartPole 调优双 DQN 超参数

在这个示例中,让我们使用双 DQN 解决 CartPole 环境。我们将展示如何调优双 DQN 的超参数以达到最佳性能。

为了调优超参数,我们可以应用网格搜索技术来探索一组不同的值组合,并选择表现最佳的那一组。我们可以从粗范围的值开始,并逐渐缩小范围。并且不要忘记为所有后续的随机数生成器固定种子,以确保可重现性:

  • Gym 环境随机数生成器

  • ε-贪心随机数生成器

  • PyTorch 神经网络的初始权重

如何做到...

我们使用双 DQN 解决 CartPole 环境如下:

  1. 导入必要的模块并创建一个 CartPole 环境:
 >>> import gym
 >>> import torch
 >>> from collections import deque
 >>> import random
 >>> import copy
 >>> from torch.autograd import Variable >>> env = gym.envs.make("CartPole-v0")
  1. 我们将重用上一个开发双深度 Q 网络示例中开发的DQN类。

  2. 我们将重复使用在《深度 Q 网络开发》食谱中开发的gen_epsilon_greedy_policy函数,并且不在这里重复。

  3. 现在,我们将使用双重 DQN 开发深度 Q 学习:

>>> def q_learning(env, estimator, n_episode, replay_size, 
                 target_update=10, gamma=1.0, epsilon=0.1,
                 epsilon_decay=.99):
 ...     """
 ...     Deep Q-Learning using double DQN, with experience replay
 ...     @param env: Gym environment
 ...     @param estimator: DQN object
 ...     @param replay_size: number of samples we use to 
                 update the model each time
 ...     @param target_update: number of episodes before 
                 updating the target network
 ...     @param n_episode: number of episodes
 ...     @param gamma: the discount factor
 ...     @param epsilon: parameter for epsilon_greedy
 ...     @param epsilon_decay: epsilon decreasing factor
 ...     """
 ...     for episode in range(n_episode):
 ...         if episode % target_update == 0:
 ...             estimator.copy_target()
 ...         policy = gen_epsilon_greedy_policy( estimator, epsilon, n_action)
 ...         state = env.reset()
 ...         is_done = False
 ...         while not is_done:
 ...             action = policy(state)
 ...             next_state, reward, is_done, _ = env.step(action)
 ...             total_reward_episode[episode] += reward
 ...             memory.append((state, action, 
                         next_state, reward, is_done))
 ...             if is_done:
 ...                 break
 ...             estimator.replay(memory, replay_size, gamma)
 ...             state = next_state
 ...         epsilon = max(epsilon * epsilon_decay, 0.01)
  1. 然后,我们指定神经网络的形状,包括输入的大小、输出的大小、隐藏层的大小和周期数,以及用于评估性能的周期数:
 >>> n_state = env.observation_space.shape[0]
 >>> n_action = env.action_space.n
 >>> n_episode = 600
 >>> last_episode = 200
  1. 然后,我们为以下超参数定义了几个值,以便在网格搜索中探索:
 >>> n_hidden_options = [30, 40]
 >>> lr_options = [0.001, 0.003]
 >>> replay_size_options = [20, 25]
 >>> target_update_options = [30, 35]
  1. 最后,我们执行一个网格搜索,在每次迭代中,我们根据一组超参数创建一个 DQN,并允许其学习 600 个周期。然后,通过对最后 200 个周期的总奖励进行*均来评估其性能:
>>> for n_hidden in n_hidden_options:
 ...     for lr in lr_options:
 ...         for replay_size in replay_size_options:
 ...             for target_update in target_update_options:
 ...                 env.seed(1)
 ...                 random.seed(1)
 ...                 torch.manual_seed(1)
 ...                 dqn = DQN(n_state, n_action, n_hidden, lr)
 ...                 memory = deque(maxlen=10000)
 ...                 total_reward_episode = [0] * n_episode
 ...                 q_learning(env, dqn, n_episode, replay_size, 
                         target_update, gamma=.9, epsilon=1)
 ...                 print(n_hidden, lr, replay_size, target_update, 
             sum(total_reward_episode[-last_episode:])/last_episode)

它是如何工作的...

执行了第 7 步后,我们得到了以下网格搜索结果:

30 0.001 20 30 143.15
 30 0.001 20 35 156.165
 30 0.001 25 30 180.575
 30 0.001 25 35 192.765
 30 0.003 20 30 187.435
 30 0.003 20 35 122.42
 30 0.003 25 30 169.32
 30 0.003 25 35 172.65
 40 0.001 20 30 136.64
 40 0.001 20 35 160.08
 40 0.001 25 30 141.955
 40 0.001 25 35 122.915
 40 0.003 20 30 143.855
 40 0.003 20 35 178.52
 40 0.003 25 30 125.52
 40 0.003 25 35 178.85

我们可以看到,通过n_hidden=30lr=0.001replay_size=25target_update=35的组合,我们获得了最佳的*均奖励,192.77

随意进一步微调超参数,以获得更好的 DQN 模型。

在这个食谱中,我们使用双重 DQNs 解决了 CartPole 问题。我们使用网格搜索对超参数的值进行了微调。在我们的示例中,我们优化了隐藏层的大小、学习率、回放批量大小和目标网络更新频率。还有其他超参数我们也可以探索,如周期数、初始 epsilon 值和 epsilon 衰减值。为了确保实验的可重复性和可比性,我们保持了随机种子固定,使得 Gym 环境的随机性、epsilon-greedy 动作以及神经网络的权重初始化保持不变。每个 DQN 模型的性能是通过最后几个周期的*均总奖励来衡量的。

开发对抗深度 Q 网络

在这个食谱中,我们将开发另一种高级 DQN 类型,对抗 DQNsDDQNs)。特别是,我们将看到在 DDQNs 中如何将 Q 值的计算分为两部分。

在 DDQNs 中,Q 值由以下两个函数计算:

在这里,V(s)是状态值函数,计算处于状态s时的值;A(s, a)是状态相关的动作优势函数,估计采取动作a相比于在状态s下采取其他动作更好多少。通过解耦valueadvantage函数,我们能够适应我们的代理在学习过程中可能不一定同时查看值和优势的事实。换句话说,使用 DDQNs 的代理可以根据其偏好有效地优化任一或两个函数。

如何做...

我们使用 DDQNs 解决 Mountain Car 问题如下:

  1. 导入必要的模块并创建一个 Mountain Car 环境:
 >>> import gym
 >>> import torch
 >>> from collections import deque
 >>> import random
 >>> from torch.autograd import Variable
 >>> import torch.nn as nn >>> env = gym.envs.make("MountainCar-v0")
  1. 接下来,我们按以下方式定义 DDQN 模型:
>>> class DuelingModel(nn.Module):
 ...     def __init__(self, n_input, n_output, n_hidden):
 ...         super(DuelingModel, self).__init__()
 ...         self.adv1 = nn.Linear(n_input, n_hidden)
 ...         self.adv2 = nn.Linear(n_hidden, n_output)
 ...         self.val1 = nn.Linear(n_input, n_hidden)
 ...         self.val2 = nn.Linear(n_hidden, 1)
 ...
 ...     def forward(self, x):
 ...         adv = nn.functional.relu(self.adv1(x))
 ...         adv = self.adv2(adv)
 ...         val = nn.functional.relu(self.val1(x))
 ...         val = self.val2(val)
 ...         return val + adv - adv.mean()
  1. 因此,我们在DQN类中使用 DDQN 模型:
>>> class DQN():
 ...     def __init__(self, n_state, n_action, n_hidden=50, lr=0.05):
 ...         self.criterion = torch.nn.MSELoss()
 ...         self.model = DuelingModel(n_state, n_action, n_hidden)
 ...         self.optimizer = torch.optim.Adam(self.model.parameters(), lr)

DQN类的其余部分保持不变。

  1. 我们将重复使用我们在开发深度 Q-Networks配方中开发的gen_epsilon_greedy_policy函数,并且这里不会重复。

  2. 我们将重复使用我们在通过经验重播改进 DQNs配方中开发的q_learning函数,并且这里不会重复。

  3. 我们然后指定神经网络的形状,包括输入的大小,输出和隐藏层,将0.001设置为学习率,并相应创建一个 DQN 模型:

 >>> n_state = env.observation_space.shape[0]
 >>> n_action = env.action_space.n
 >>> n_hidden = 50
 >>> lr = 0.001
 >>> dqn = DQN(n_state, n_action, n_hidden, lr)
  1. 接下来,我们定义保存经验的缓冲区:
>>> memory = deque(maxlen=10000)

新样本将被添加到队列中,并且只要队列中有超过10000个样本,旧样本就会被删除。

  1. 然后,我们执行包含 DDQN 的 Deep Q-learning,进行了600个剧集:
>>> n_episode = 600

我们将每步设置为20作为回放样本大小:

>>> replay_size = 20

我们还会跟踪每一集的总奖励:

 >>> total_reward_episode = [0] * n_episode
 >>> q_learning(env, dqn, n_episode, replay_size, gamma=.9,
 epsilon=.3)
  1. 现在,我们可以显示随时间变化的剧集奖励的绘图:
 >>> import matplotlib.pyplot as plt
 >>> plt.plot(total_reward_episode)
 >>> plt.title('Episode reward over time')
 >>> plt.xlabel('Episode')
 >>> plt.ylabel('Total reward')
 >>> plt.show()

工作原理...

Step 2是 Dueling DQN 的核心部分。它由两部分组成,动作优势adv)和状态值val)。同样,我们使用一个隐藏层作为示例。

执行Step 9将导致以下绘图:

在 DDQNs 中,预测的 Q 值由两个元素组成:状态值和动作优势。第一个估计在某个状态下的表现有多好。第二个指示相比其他选择,采取特定动作有多好。这两个元素分别计算并结合到 DQN 的最后一层。您将记得,传统的 DQNs 只更新给定状态下某个动作的 Q 值。DDQNs 更新所有动作(而不仅仅是给定动作)可以利用的状态值,以及动作的优势。因此,认为 DDQNs 更加稳健。

将 Deep Q-Networks 应用于 Atari 游戏

到目前为止,我们处理的问题相当简单,有时应用 DQNs 可能有点过头了。在这个和下一个配方中,我们将使用 DQNs 来解决 Atari 游戏,这些游戏问题要复杂得多。

我们将在这个配方中以 Pong (gym.openai.com/envs/Pong-v0/)为例。它模拟了 Atari 2600 游戏 Pong,代理与另一玩家打乒乓球。这个环境的观察是屏幕的 RGB 图像(参考下面的截图):

这是一个形状为(210,160,3)的矩阵,意味着图像的大小为210 * 160,有三个 RGB 通道。

代理(右侧)在比赛中上下移动以击打球。如果错过了,另一名玩家(左侧)将获得 1 分;同样,如果另一名玩家错过了球,代理将获得 1 分。比赛的胜者是首先得到 21 分的人。代理可以在 Pong 环境中采取以下 6 种可能的动作:

  • 0: NOOP: 代理保持静止

  • 1: FIRE: 不是一个有意义的动作

  • 2: RIGHT: 代理向上移动

  • 3: LEFT: 代理向下移动

  • 4: RIGHTFIRE: 与 2 相同

  • 5: LEFTFIRE: 与 5 相同

每个动作都会在k帧的持续时间内重复执行(k可以是 2、3、4 或 16,取决于 Pong 环境的具体变体)。奖励可以是以下任意一种:

  • -1: 代理错过球。

  • 1: 对手错过球。

  • 0: 否则。

Pong 中的观察空间210 * 160 * 3比我们通常处理的要大得多。因此,我们将把图像缩小到84 * 84并转换为灰度,然后使用 DQNs 来解决它。

怎么做…

我们将从以下内容开始探索 Pong 环境:

  1. 导入必要的模块并创建一个 Pong 环境:
 >>> import gym
 >>> import torch
 >>> import random >>> env = gym.envs.make("PongDeterministic-v4")

在这个 Pong 环境的变体中,一个动作是确定性的,并且在 16 帧的持续时间内重复执行。

  1. 查看观察空间和动作空间:
 >>> state_shape = env.observation_space.shape
 >>> n_action = env.action_space.n
 >>> print(state_shape)
 (210, 160, 3)
 >>> print(n_action)
 6
 >>> print(env.unwrapped.get_action_meanings())
 ['NOOP', 'FIRE', 'RIGHT', 'LEFT', 'RIGHTFIRE', 'LEFTFIRE']
  1. 指定三个动作:
 >>> ACTIONS = [0, 2, 3]
 >>> n_action = 3

这些动作分别是不移动、向上移动和向下移动。

  1. 让我们采取随机动作并渲染屏幕:
 >>> env.reset()
 >>> is_done = False
 >>> while not is_done:
 ...     action = ACTIONS[random.randint(0, n_action - 1)]
 ...     obs, reward, is_done, _ = env.step(action)
 ...     print(reward, is_done)
 ...     env.render()
 0.0 False
 0.0 False
 0.0 False
 ……
 ……
 0.0 False
 0.0 False
 0.0 False
 -1.0 True

您将在屏幕上看到两名玩家在打乒乓球,即使代理正在输。

  1. 现在,我们开发一个屏幕处理函数来缩小图像并将其转换为灰度:
 >>> import torchvision.transforms as T
 >>> from PIL import Image
 >>> image_size = 84
 >>> transform = T.Compose([T.ToPILImage(),
 ...                        T.Grayscale(num_output_channels=1),
 ...                        T.Resize((image_size, image_size),
                                 interpolation=Image.CUBIC),
 ...                        T.ToTensor(),
 ...                        ])

现在,我们只需定义一个调整图像大小至84 * 84的调整器:

 >>> def get_state(obs):
 ...     state = obs.transpose((2, 0, 1))
 ...     state = torch.from_numpy(state)
 ...     state = transform(state)
 ...     return state

此函数将调整大小后的图像重塑为大小为(1, 84, 84):

 >>> state = get_state(obs)
 >>> print(state.shape)
 torch.Size([1, 84, 84])

现在,我们可以使用双 DQNs 开始解决环境,如下所示:

  1. 这次我们将使用一个较大的神经网络,有两个隐藏层,因为输入大小约为 21,000:
 >>> from collections import deque 
 >>> import copy
 >>> from torch.autograd import Variable
 >>> class DQN():
 ...     def __init__(self, n_state, n_action, n_hidden, lr=0.05):
 ...         self.criterion = torch.nn.MSELoss()
 ...         self.model = torch.nn.Sequential(
 ...                  torch.nn.Linear(n_state, n_hidden[0]),
 ...                  torch.nn.ReLU(),
 ...                  torch.nn.Linear(n_hidden[0], n_hidden[1]),
 ...                  torch.nn.ReLU(),
 ...                  torch.nn.Linear(n_hidden[1], n_action)
 ...                  )
 ...         self.model_target = copy.deepcopy(self.model)
 ...         self.optimizer = torch.optim.Adam( self.model.parameters(), lr)
  1. DQN类的其余部分与Developing double deep Q-networks食谱中的一样,只是对replay方法进行了小的更改:
>>> def replay(self, memory, replay_size, gamma):
 ...     """
 ...     Experience replay with target network
 ...     @param memory: a list of experience
 ...     @param replay_size: the number of samples we use 
                         to update the model each time
 ...     @param gamma: the discount factor
 ...     """
 ...     if len(memory) >= replay_size:
 ...         replay_data = random.sample(memory, replay_size)
 ...         states = []
 ...         td_targets = []
 ...         for state, action, next_state, reward, 
                                 is_done in replay_data:
 ...             states.append(state.tolist())
 ...             q_values = self.predict(state).tolist()
 ...             if is_done:
 ...                 q_values[action] = reward
 ...             else:
 ...                 q_values_next = self.target_predict( next_state).detach()
 ...                 q_values[action] = reward + gamma * 
                         torch.max(q_values_next).item()
 ...             td_targets.append(q_values)
 ...         self.update(states, td_targets)
  1. 我们将重复使用我们在Developing Deep Q-Networks食谱中开发的gen_epsilon_greedy_policy函数,并且这里不再重复。

  2. 现在,我们将开发带有双 DQN 的深度 Q 学习:

>>> def q_learning(env, estimator, n_episode, replay_size, 
             target_update=10, gamma=1.0, epsilon=0.1,
             epsilon_decay=.99):
 ...     """
 ...     Deep Q-Learning using double DQN, with experience replay
 ...     @param env: Gym environment
 ...     @param estimator: DQN object
 ...     @param replay_size: number of samples we use to 
                             update the model each time
 ...     @param target_update: number of episodes before 
                             updating the target network
 ...     @param n_episode: number of episodes
 ...     @param gamma: the discount factor
 ...     @param epsilon: parameter for epsilon_greedy
 ...     @param epsilon_decay: epsilon decreasing factor
 ...     """
 ...     for episode in range(n_episode):
 ...         if episode % target_update == 0:
 ...             estimator.copy_target()
 ...         policy = gen_epsilon_greedy_policy( estimator, epsilon, n_action)
 ...         obs = env.reset()
 ...         state = get_state(obs).view(image_size * image_size)[0]
 ...         is_done = False
 ...         while not is_done:
 ...             action = policy(state)
 ...             next_obs, reward, is_done, _ = 
                                 env.step(ACTIONS[action])
 ...             total_reward_episode[episode] += reward
 ...             next_state = get_state(obs).view( image_size * image_size)
 ...             memory.append((state, action, next_state, 
                                 reward, is_done))
 ...             if is_done:
 ...                 break
 ...             estimator.replay(memory, replay_size, gamma)
 ...             state = next_state
 ...         print('Episode: {}, total reward: {}, epsilon: 
             {}'.format(episode, total_reward_episode[episode],
             epsilon))
 ...         epsilon = max(epsilon * epsilon_decay, 0.01)

给定大小为[210, 160, 3]的观察结果,将其转换为更小尺寸的灰度矩阵[84, 84]并将其扁*化,以便我们可以将其馈送到我们的网络中。

  1. 现在,我们指定神经网络的形状,包括输入和隐藏层的大小:
 >>> n_state = image_size * image_size
 >>> n_hidden = [200, 50]

剩余的超参数如下:

 >>> n_episode = 1000
 >>> lr = 0.003
 >>> replay_size = 32
 >>> target_update = 10

现在,我们相应地创建一个 DQN:

>>> dqn = DQN(n_state, n_action, n_hidden, lr)
  1. 接下来,我们定义保存经验的缓冲区:
>>> memory = deque(maxlen=10000)
  1. 最后,我们执行深度 Q 学习,并跟踪每个 episode 的总奖励:
 >>> total_reward_episode = [0] * n_episode
 >>> q_learning(env, dqn, n_episode, replay_size, target_update, gamma=.9, epsilon=1)

它是如何工作的…

Pong 中的观察情况比本章中迄今为止我们处理过的环境复杂得多。它是一个 210 * 160 屏幕尺寸的三通道图像。因此,我们首先将其转换为灰度图像,将其缩小为 84 * 84,然后展*,以便馈送到全连接神经网络中。由于输入维度约为 6000,我们使用两个隐藏层来适应复杂性。

在 Atari 游戏中使用卷积神经网络

在上一篇文章中,我们将 Pong 环境中的每个观察图像视为灰度数组,并将其馈送到全连接神经网络中。将图像展*可能会导致信息丢失。为什么不直接使用图像作为输入呢?在这篇文章中,我们将卷积神经网络CNNs)集成到 DQN 模型中。

CNN 是处理图像输入的最佳神经网络架构之一。在 CNN 中,卷积层能够有效地从图像中提取特征,这些特征将传递给下游的全连接层。下图展示了一个具有两个卷积层的 CNN 示例:

正如你可以想象的,如果我们简单地将图像展*成一个向量,我们将丢失一些关于球和两名玩家位置的信息。这些信息对于模型学习至关重要。在 CNN 中的卷积操作中,多个滤波器生成的一组特征映射可以捕捉到这些信息。

再次,我们将图像从 210 * 160 缩小到 84 * 84,但这次保留三个 RGB 通道,而不是将它们展*成数组。

怎么做...

让我们使用基于 CNN 的 DQN 来解决 Pong 环境,如下所示:

  1. 导入必要的模块并创建 Pong 环境:
 >>> import gym
 >>> import torch
 >>> import random >>> from collections import deque
 >>> import copy
 >>> from torch.autograd import Variable
 >>> import torch.nn as nn
 >>> import torch.nn.functional as F
 >>> env = gym.envs.make("PongDeterministic-v4")
  1. 然后,我们指定三个动作:
 >>> ACTIONS = [0, 2, 3]
 >>> n_action = 3

这些动作是不动、向上移动和向下移动。

  1. 现在,我们开发一个图像处理函数来缩小图像:
 >>> import torchvision.transforms as T
 >>> from PIL import Image
 >>> image_size = 84
 >>> transform = T.Compose([T.ToPILImage(),
 ...                        T.Resize((image_size, image_size), 
                              interpolation=Image.CUBIC),
 ...                        T.ToTensor()])

现在,我们定义一个调整器,将图像缩小为 84 * 84,然后将图像重塑为 (3, 84, 84):

>>> def get_state(obs):
 ...     state = obs.transpose((2, 0, 1))
 ...     state = torch.from_numpy(state)
 ...     state = transform(state).unsqueeze(0)
 ...     return state
  1. 现在,我们开始通过开发 CNN 模型来解决 Pong 环境:
>>> class CNNModel(nn.Module):
 ...     def __init__(self, n_channel, n_action):
 ...         super(CNNModel, self).__init__()
 ...         self.conv1 = nn.Conv2d(in_channels=n_channel, 
                     out_channels=32, kernel_size=8, stride=4)
 ...         self.conv2 = nn.Conv2d(32, 64, 4, stride=2)
 ...         self.conv3 = nn.Conv2d(64, 64, 3, stride=1)
 ...         self.fc = torch.nn.Linear(7 * 7 * 64, 512)
 ...         self.out = torch.nn.Linear(512, n_action)
 ...
 ...     def forward(self, x):
 ...         x = F.relu(self.conv1(x))
 ...         x = F.relu(self.conv2(x))
 ...         x = F.relu(self.conv3(x))
 ...         x = x.view(x.size(0), -1)
 ...         x = F.relu(self.fc(x))
 ...         output = self.out(x)
 ...         return output
  1. 现在我们将使用刚刚在我们的DQN模型中定义的 CNN 模型:
>>> class DQN():
 ...     def __init__(self, n_channel, n_action, lr=0.05):
 ...         self.criterion = torch.nn.MSELoss()
 ...         self.model = CNNModel(n_channel, n_action)
 ...         self.model_target = copy.deepcopy(self.model)
 ...         self.optimizer = torch.optim.Adam( self.model.parameters(), lr)
  1. DQN类的其余部分与“开发双重深度 Q 网络”一章中的相同,只是replay方法略有改变:
>>> def replay(self, memory, replay_size, gamma):
 ...     """
 ...     Experience replay with target network
 ...     @param memory: a list of experience
 ...     @param replay_size: the number of samples we use 
                         to update the model each time
 ...     @param gamma: the discount factor
 ...     """
 ...     if len(memory) >= replay_size:
 ...         replay_data = random.sample(memory, replay_size)
 ...         states = []
 ...         td_targets = []
 ...         for state, action, next_state, reward,     
                                 is_done in replay_data:
 ...             states.append(state.tolist()[0])
 ...             q_values = self.predict(state).tolist()[0]
 ...             if is_done:
 ...                 q_values[action] = reward
 ...             else:
 ...                 q_values_next = self.target_predict( next_state).detach()
 ...                 q_values[action] = reward + gamma *         
                             torch.max(q_values_next).item()
 ...             td_targets.append(q_values)
 ...         self.update(states, td_targets)
  1. 我们将重复使用我们在“开发深度 Q 网络”一章中开发的gen_epsilon_greedy_policy函数,这里不再重复。

  2. 现在,我们使用双重 DQN 开发深度 Q-learning:

 >>> def q_learning(env, estimator, n_episode, replay_size, 
             target_update=10, gamma=1.0, epsilon=0.1,   
             epsilon_decay=.99):
 ...     """
 ...     Deep Q-Learning using double DQN, with experience replay
 ...     @param env: Gym environment
 ...     @param estimator: DQN object
 ...     @param replay_size: number of samples we use to 
                             update the model each time
 ...     @param target_update: number of episodes before 
                             updating the target network
 ...     @param n_episode: number of episodes
 ...     @param gamma: the discount factor
 ...     @param epsilon: parameter for epsilon_greedy
 ...     @param epsilon_decay: epsilon decreasing factor
 ...     """
 ...     for episode in range(n_episode):
 ...         if episode % target_update == 0:
 ...             estimator.copy_target()
 ...         policy = gen_epsilon_greedy_policy( estimator, epsilon, n_action)
 ...         obs = env.reset()
 ...         state = get_state(obs)
 ...         is_done = False
 ...         while not is_done:
 ...             action = policy(state)
 ...             next_obs, reward, is_done, _ = 
                             env.step(ACTIONS[action])
 ...             total_reward_episode[episode] += reward
 ...             next_state = get_state(obs)
 ...             memory.append((state, action, next_state, 
                                 reward, is_done))
 ...             if is_done:
 ...                 break
 ...             estimator.replay(memory, replay_size, gamma)
 ...             state = next_state
 ...         print('Episode: {}, total reward: {}, epsilon: {}' .format(episode, total_reward_episode[episode], epsilon))
 ...         epsilon = max(epsilon * epsilon_decay, 0.01)
  1. 然后,我们将剩余的超参数指定如下:
 >>> n_episode = 1000
 >>> lr = 0.00025
 >>> replay_size = 32
 >>> target_update = 10

根据需要创建一个 DQN:

 >>> dqn = DQN(3, n_action, lr)
  1. 接下来,我们定义保存经验的缓冲区:
>>> memory = deque(maxlen=100000)
  1. 最后,我们执行深度 Q-learning,并追踪每个周期的总奖励:
 >>> total_reward_episode = [0] * n_episode >>> q_learning(env, dqn, n_episode, replay_size, target_update, gamma=.9, epsilon=1)

它是如何工作的...

步骤 3 中的图像预处理函数首先将每个通道的图像缩小到 84 * 84,然后将其尺寸更改为 (3, 84, 84)。这是为了确保将具有正确尺寸的图像输入到网络中。

步骤 4 中,CNN 模型有三个卷积层和一个 ReLU 激活函数,每个卷积层后都跟随着。最后一个卷积层产生的特征映射然后被展*并输入到具有 512 个节点的全连接隐藏层,然后是输出层。

将 CNNs 结合到 DQNs 中首先由 DeepMind 提出,并发表在 Playing Atari with Deep Reinforcement Learningwww.cs.toronto.edu/~vmnih/docs/dqn.pdf)。该模型以图像像素作为输入,并输出估计的未来奖励值。它还适用于其他 Atari 游戏环境,其中观察是游戏屏幕的图像。卷积组件是一组有效的分层特征提取器,它们可以从复杂环境中的原始图像数据中学习特征表示,并通过全连接层学习成功的控制策略。

请记住,即使在 GPU 上,前面示例中的训练通常也需要几天时间,在 2.9 GHz 英特尔 i7 四核 CPU 上大约需要 90 小时。

另见

如果您对 CNN 不熟悉,请查看以下资料:

  • Hands-On Deep Learning Architectures with Python(Packt Publishing,作者:刘宇熙(Hayden)和萨兰什·梅塔),第四章,CNN 架构

  • R Deep Learning Projects(Packt Publishing,作者:刘宇熙(Hayden)和帕布罗·马尔多纳多),第一章,使用卷积神经网络识别手写数字,和第二章,智能车辆的交通标志识别

第八章:实现策略梯度和策略优化

在本章中,我们将专注于策略梯度方法,这是*年来最流行的强化学习技术之一。我们将从实现基础的 REINFORCE 算法开始,并继续改进算法基线。我们还将实现更强大的算法,演员-评论家及其变体,并将其应用于解决 CartPole 和 Cliff Walking 问题。我们还将体验一个具有连续动作空间的环境,并采用高斯分布来解决它。最后的有趣部分,我们将基于交叉熵方法训练一个代理来玩 CartPole 游戏。

本章将涵盖以下实例:

  • 实现 REINFORCE 算法

  • 开发带基线的 REINFORCE 算法

  • 实现演员-评论家算法

  • 使用演员-评论家算法解决 Cliff Walking

  • 设置连续的 Mountain Car 环境

  • 使用优势演员-评论家网络解决连续的 Mountain Car 环境

  • 通过交叉熵方法玩 CartPole

实现 REINFORCE 算法

最*的一篇文章指出,策略梯度方法变得越来越流行。它们的学习目标是优化动作的概率分布,以便在给定状态下,更有益的动作将具有更高的概率值。在本章的第一个实例中,我们将讨论 REINFORCE 算法,这是高级策略梯度方法的基础。

REINFORCE 算法也被称为 蒙特卡罗策略梯度,因为它基于蒙特卡罗方法优化策略。具体而言,它使用当前策略从一个回合中收集轨迹样本,并用它们来更新策略参数 θ 。策略梯度的学习目标函数如下:

其梯度可以如下推导:

这里,![] 是返回值,即累积折扣奖励直到时间 t,![]并且是随机策略,确定在给定状态下采取某些动作的概率。由于策略更新是在整个回合结束后和所有样本被收集后进行的,REINFORCE 算法是一种离策略算法。

在计算策略梯度后,我们使用反向传播来更新策略参数。通过更新后的策略,我们展开一个回合,收集一组样本,并使用它们来重复更新策略参数。

现在我们将开发 REINFORCE 算法来解决 CartPole (gym.openai.com/envs/CartPole-v0/) 环境。

如何做...

我们将开发带基线的 REINFORCE 算法来解决 CartPole 环境如下:

  1. 导入所有必要的包并创建一个 CartPole 实例:
>>> import gym
>>> import torch
>>> import torch.nn as nn >>> env = gym.make('CartPole-v0')
  1. 让我们从PolicyNetwork类的__init__方法开始,该方法使用神经网络逼*策略:
>>> class PolicyNetwork():
 ...     def __init__(self, n_state, n_action, n_hidden=50, lr=0.001):
 ...         self.model = nn.Sequential(
 ...                         nn.Linear(n_state, n_hidden),
 ...                         nn.ReLU(),
 ...                         nn.Linear(n_hidden, n_action),
 ...                         nn.Softmax(),
 ...                 )
 ...         self.optimizer = torch.optim.Adam( self.model.parameters(), lr)
  1. 接下来,添加predict方法,计算估计的策略:
>>>     def predict(self, s):
 ...         """
 ...         Compute the action probabilities of state s using 
                 the learning model
 ...         @param s: input state
 ...         @return: predicted policy
 ...         """
 ...         return self.model(torch.Tensor(s))
  1. 现在我们开发训练方法,使用一个情节中收集的样本更新神经网络:
>>>     def update(self, returns, log_probs):
 ...         """
 ...         Update the weights of the policy network given 
                 the training samples
 ...         @param returns: return (cumulative rewards) for 
                 each step in an episode
 ...         @param log_probs: log probability for each step
 ...         """
 ...         policy_gradient = []
 ...         for log_prob, Gt in zip(log_probs, returns):
 ...             policy_gradient.append(-log_prob * Gt)
 ...
 ...         loss = torch.stack(policy_gradient).sum()
 ...         self.optimizer.zero_grad()
 ...         loss.backward()
 ...         self.optimizer.step()
  1. PolicyNetwork类的最终方法是get_action,它基于预测的策略对给定状态采样一个动作:
>>>     def get_action(self, s):
 ...         """
 ...         Estimate the policy and sample an action, 
                 compute its log probability
 ...         @param s: input state
 ...         @return: the selected action and log probability
 ...         """
 ...         probs = self.predict(s)
 ...         action = torch.multinomial(probs, 1).item()
 ...         log_prob = torch.log(probs[action])
 ...         return action, log_prob

它还返回所选动作的对数概率,这将作为训练样本的一部分使用。

这就是PolicyNetwork类的全部内容!

  1. 现在,我们可以开始开发REINFORCE算法,使用一个策略网络模型:
>>> def reinforce(env, estimator, n_episode, gamma=1.0):
 ...     """
 ...     REINFORCE algorithm
 ...     @param env: Gym environment
 ...     @param estimator: policy network
 ...     @param n_episode: number of episodes
 ...     @param gamma: the discount factor
 ...     """
 ...     for episode in range(n_episode):
 ...         log_probs = []
 ...         rewards = []
 ...         state = env.reset()
 ...         while True:
 ...             action, log_prob = estimator.get_action(state)
 ...             next_state, reward, is_done, _ = env.step(action)
 ...             total_reward_episode[episode] += reward
 ...             log_probs.append(log_prob)
 ...             rewards.append(reward)
 ...
 ...             if is_done:
 ...                 returns = []
 ...                 Gt = 0
 ...                 pw = 0
 ...                 for reward in rewards[::-1]:
 ...                     Gt += gamma ** pw * reward
 ...                     pw += 1
 ...                     returns.append(Gt)
 ...                 returns = returns[::-1]
 ...                 returns = torch.tensor(returns)
 ...                 returns = (returns - returns.mean()) / (
 ...                     returns.std() + 1e-9)
 ...                 estimator.update(returns, log_probs)
 ...                 print('Episode: {}, total reward: {}'.format( episode, total_reward_episode[episode]))
 ...                 break
 ...
 ...             state = next_state
  1. 我们指定策略网络的大小(输入、隐藏和输出层)、学习率,然后相应地创建PolicyNetwork实例:
>>> n_state = env.observation_space.shape[0]
>>> n_action = env.action_space.n
>>> n_hidden = 128
>>> lr = 0.003
>>> policy_net = PolicyNetwork(n_state, n_action, n_hidden, lr)

我们将折扣因子设置为0.9

>>> gamma = 0.9
  1. 我们使用刚开发的策略网络执行 REINFORCE 算法的学习,共 500 个情节,并跟踪每个情节的总回报:
>>> n_episode = 500
>>> total_reward_episode = [0] * n_episode
>>> reinforce(env, policy_net, n_episode, gamma)
  1. 现在让我们显示随时间变化的回报情节图:
>>> import matplotlib.pyplot as plt
>>> plt.plot(total_reward_episode)
>>> plt.title('Episode reward over time')
>>> plt.xlabel('Episode') >>> plt.ylabel('Total reward')
>>> plt.show()

它的工作原理...

Step 2中,为了简单起见,我们使用一个隐藏层的神经网络。策略网络的输入是一个状态,接着是一个隐藏层,输出是可能采取的个别动作的概率。因此,我们使用 softmax 函数作为输出层的激活函数。

Step 4 用于更新网络参数:给定在一个情节中收集的所有数据,包括所有步骤的回报和对数概率,我们计算策略梯度,然后通过反向传播相应地更新策略参数。

Step 6 中,REINFORCE 算法执行以下任务:

  • 它运行一个情节:对于情节中的每一步,根据当前估计的策略采样一个动作;它在每一步存储回报和对数策略。

  • 一旦一个情节结束,它计算每一步的折扣累积回报;通过减去它们的*均值然后除以它们的标准差来对结果进行归一化。

  • 它使用回报和对数概率计算策略梯度,然后更新策略参数。我们还显示每个情节的总回报。

  • 它通过重复上述步骤运行n_episode个情节。

Step 8 将生成以下训练日志:

Episode: 0, total reward: 12.0
 Episode: 1, total reward: 18.0
 Episode: 2, total reward: 23.0
 Episode: 3, total reward: 23.0
 Episode: 4, total reward: 11.0
 ……
 ……
 Episode: 495, total reward: 200.0
 Episode: 496, total reward: 200.0
 Episode: 497, total reward: 200.0
 Episode: 498, total reward: 200.0
 Episode: 499, total reward: 200.0

您将在Step 9中观察以下情节:

您可以看到最* 200 个情节中的大部分奖励最高值为+200。

REINFORCE 算法是一系列策略梯度方法的家族,通过以下规则直接更新策略参数:

在这里,α是学习率,![],作为动作的概率映射,而![],作为累积折现奖励,是在一个 episode 中收集的经验。由于训练样本集仅在完成整个 episode 后构建,因此 REINFORCE 中的学习是以离线策略进行的。学习过程可以总结如下:

  1. 随机初始化策略参数θ

  2. 根据当前策略选择动作执行一个 episode。

  3. 在每个步骤中,存储所选动作的对数概率以及产生的奖励。

  4. 计算各步骤的回报。

  5. 使用对数概率和回报计算策略梯度,并通过反向传播更新策略参数θ。

  6. 重复步骤 25

同样地,由于 REINFORCE 算法依赖于由随机策略生成的完整轨迹,因此它构成了一种蒙特卡洛方法。

参见:

推导策略梯度方程相当棘手。它利用了对数导数技巧。如果你想知道,这里有一个详细的解释:

开发带基线的 REINFORCE 算法

在 REINFORCE 算法中,蒙特卡洛模拟在一个 episode 中播放整个轨迹,然后用于更新策略。然而,随机策略可能在不同的 episode 中在相同状态下采取不同的行动。这可能会导致训练时的混淆,因为一个采样经验希望增加选择某个动作的概率,而另一个采样经验可能希望减少它。为了减少这种高方差问题,在传统 REINFORCE 中,我们将开发一种变体算法,即带基线的 REINFORCE 算法。

在带基线的 REINFORCE 中,我们从回报 G 中减去基线状态值。因此,我们在梯度更新中使用了优势函数 A,描述如下:

这里,V(s)是估计给定状态的状态值函数。通常,我们可以使用线性函数或神经网络来逼*状态值。通过引入基线值,我们可以根据状态给出的*均动作校准奖励。

我们使用两个神经网络开发了带基线的 REINFORCE 算法,一个用于策略,另一个用于值估计,以解决 CartPole 环境。

如何实现...

我们使用 REINFORCE 算法解决 CartPole 环境的方法如下:

  1. 导入所有必要的包并创建一个 CartPole 实例:
>>> import gym
>>> import torch
>>> import torch.nn as nn >>> from torch.autograd import Variable
>>> env = gym.make('CartPole-v0')
  1. 关于策略网络部分,基本上与我们在实现 REINFORCE 算法配方中使用的PolicyNetwork类相同。请记住,在update方法中使用了优势值:
>>> def update(self, advantages, log_probs):
 ...     """
 ...     Update the weights of the policy network given 
             the training samples
 ...     @param advantages: advantage for each step in an episode
 ...     @param log_probs: log probability for each step
 ...     """
 ...     policy_gradient = []
 ...     for log_prob, Gt in zip(log_probs, advantages):
 ...         policy_gradient.append(-log_prob * Gt)
 ...
 ...     loss = torch.stack(policy_gradient).sum()
 ...     self.optimizer.zero_grad()
 ...     loss.backward()
 ...     self.optimizer.step()
  1. 对于价值网络部分,我们使用了一个带有一个隐藏层的回归神经网络:
>>> class ValueNetwork():
 ...     def __init__(self, n_state, n_hidden=50, lr=0.05):
 ...         self.criterion = torch.nn.MSELoss()
 ...         self.model = torch.nn.Sequential(
 ...                         torch.nn.Linear(n_state, n_hidden),
 ...                         torch.nn.ReLU(),
 ...                         torch.nn.Linear(n_hidden, 1)
 ...                 )
 ...         self.optimizer = torch.optim.Adam( self.model.parameters(), lr)

它的学习目标是*似状态值;因此,我们使用均方误差作为损失函数。

update方法通过反向传播训练值回归模型,使用一组输入状态和目标输出,当然是:

...     def update(self, s, y):
 ...         """
 ...         Update the weights of the DQN given a training sample
 ...         @param s: states
 ...         @param y: target values
 ...         """
 ...         y_pred = self.model(torch.Tensor(s))
 ...         loss = self.criterion(y_pred, Variable(torch.Tensor(y)))
 ...         self.optimizer.zero_grad()
 ...         loss.backward()
 ...         self.optimizer.step()

predict方法则是用来估计状态值:

...     def predict(self, s):
 ...         """
 ...         Compute the Q values of the state for all actions 
                 using the learning model
 ...         @param s: input state
 ...         @return: Q values of the state for all actions
 ...         """
 ...         with torch.no_grad():
 ...             return self.model(torch.Tensor(s))
  1. 现在,我们可以继续开发基准 REINFORCE 算法,其中包括一个策略和价值网络模型:
>>> def reinforce(env, estimator_policy, estimator_value, 
                     n_episode, gamma=1.0):
 ...     """
 ...     REINFORCE algorithm with baseline
 ...     @param env: Gym environment
 ...     @param estimator_policy: policy network
 ...     @param estimator_value: value network
 ...     @param n_episode: number of episodes
 ...     @param gamma: the discount factor
 ...     """
 ...     for episode in range(n_episode):
 ...         log_probs = []
 ...         states = []
 ...         rewards = []
 ...         state = env.reset()
 ...         while True:
 ...             states.append(state)
 ...             action, log_prob = 
                     estimator_policy.get_action(state)
 ...             next_state, reward, is_done, _ = env.step(action)
 ...             total_reward_episode[episode] += reward
 ...             log_probs.append(log_prob)
 ...             rewards.append(reward)
 ...
 ...             if is_done:
 ...                 Gt = 0
 ...                 pw = 0
 ...                 returns = []
 ...                 for t in range(len(states)-1, -1, -1):
 ...                     Gt += gamma ** pw * rewards[t]
 ...                     pw += 1
 ...                     returns.append(Gt)
 ...                 returns = returns[::-1]
 ...                 returns = torch.tensor(returns)
 ...                 baseline_values = 
                         estimator_value.predict(states)
 ...                 advantages = returns - baseline_values
 ...                 estimator_value.update(states, returns)
 ...                 estimator_policy.update(advantages, log_probs)
 ...                 print('Episode: {}, total reward: {}'.format( episode, total_reward_episode[episode]))
 ...                 break
 ...             state = next_state
  1. 我们指定策略网络的大小(输入、隐藏和输出层)、学习率,然后相应地创建一个PolicyNetwork实例:
>>> n_state = env.observation_space.shape[0]
>>> n_action = env.action_space.n
>>> n_hidden_p = 64
>>> lr_p = 0.003
>>> policy_net = PolicyNetwork(n_state, n_action, n_hidden_p, lr_p)

至于价值网络,我们也设置了其大小并创建了一个实例:

>>> n_hidden_v = 64
>>> lr_v = 0.003
>>> value_net = ValueNetwork(n_state, n_hidden_v, lr_v)

我们将折扣因子设置为0.9

>>> gamma = 0.9
  1. 我们使用基准的 REINFORCE 算法进行 2,000 个 episode 的学习,并且我们还会追踪每个 episode 的总奖励:
>>> n_episode = 2000
>>> total_reward_episode = [0] * n_episode
>>> reinforce(env, policy_net, value_net, n_episode, gamma)
  1. 现在,我们展示随时间变化的 episode 奖励的图表:
>>> import matplotlib.pyplot as plt
>>> plt.plot(total_reward_episode)
>>> plt.title('Episode reward over time')
>>> plt.xlabel('Episode')
>>> plt.ylabel('Total reward')
>>> plt.show()

工作原理...

REINFORCE 高度依赖蒙特卡洛方法生成用于训练策略网络的整个轨迹。然而,在相同的随机策略下,不同的 episode 可能会采取不同的动作。为了减少采样经验的方差,我们从返回中减去状态值。由此产生的优势度量了相对于*均动作的奖励,这将在梯度更新中使用。

步骤 4中,使用基准的 REINFORCE 算法执行以下任务:

  • 它运行一个 episode——处理状态、奖励,并且每一步记录策略的日志。

  • 一旦一个 episode 完成,它会计算每一步的折扣累积奖励;它通过价值网络估计基准值;它通过从返回中减去基准值计算优势值。

  • 它使用优势值和对数概率计算策略梯度,并更新策略和价值网络。我们还显示每个 episode 的总奖励。

  • 它通过重复上述步骤运行n_episode个 episode。

执行步骤 7中的代码将导致以下图表:

你可以看到,在大约 1,200 个 episode 后,性能非常稳定。

通过额外的价值基准,我们能够重新校准奖励并减少梯度估计的方差。

实施演员-评论家算法

在基准 REINFORCE 算法中,有两个独立的组成部分,策略模型和价值函数。实际上,我们可以结合这两个组件的学习,因为学习价值函数的目标是更新策略网络。这就是演员-评论家算法所做的事情,这也是我们将在本文中开发的内容。

演员-评论家算法的网络包括以下两个部分:

  • 演员:它接收输入状态并输出动作概率。本质上,通过使用评论家提供的信息来更新模型,它学习最优策略。

  • 评论家:这评估了在输入状态时表现良好的价值函数。价值指导演员如何调整。

这两个组件在网络中共享输入和隐藏层的参数,这样学习效率比分开学习更高。因此,损失函数是两部分的总和,具体是测量演员的动作的负对数似然和估计和计算回报之间的均方误差测量评论家。

演员-评论家算法的一个更受欢迎的版本是优势演员-评论家A2C)。正如其名称所示,评论部分计算优势值,而不是状态值,这类似于带基线的 REINFORCE。它评估了一个动作在一个状态下相对于其他动作的优越性,并且已知可以减少策略网络中的方差。

如何做...

我们开发演员-评论家算法以解决 CartPole 环境,具体如下:

  1. 导入所有必要的包并创建一个 CartPole 实例:
>>> import gym
>>> import torch
>>> import torch.nn as nn
>>> import torch.nn.functional as F >>> env = gym.make('CartPole-v0')
  1. 让我们从演员-评论家神经网络模型开始:
>>> class ActorCriticModel(nn.Module):
 ...     def __init__(self, n_input, n_output, n_hidden):
 ...         super(ActorCriticModel, self).__init__()
 ...         self.fc = nn.Linear(n_input, n_hidden)
 ...         self.action = nn.Linear(n_hidden, n_output)
 ...         self.value = nn.Linear(n_hidden, 1)
 ...
 ...     def forward(self, x):
 ...         x = torch.Tensor(x)
 ...         x = F.relu(self.fc(x))
 ...         action_probs = F.softmax(self.action(x), dim=-1)
 ...         state_values = self.value(x)
 ...         return action_probs, state_values
  1. 我们继续使用演员-评论家神经网络开发PolicyNetwork类的__init__方法:
>>> class PolicyNetwork():
 ...     def __init__(self, n_state, n_action, 
                     n_hidden=50, lr=0.001):
 ...         self.model = ActorCriticModel( n_state, n_action, n_hidden)
 ...         self.optimizer = torch.optim.Adam( self.model.parameters(), lr)
 ...         self.scheduler = torch.optim.lr_scheduler.StepLR( self.optimizer, step_size=10, gamma=0.9)

请注意,我们在此处使用了一个学习率减少器,根据学习进展动态调整学习率。

  1. 接下来,我们添加predict方法,它计算估计的动作概率和状态值:
>>>     def predict(self, s):
 ...         """
 ...         Compute the output using the Actor Critic model
 ...         @param s: input state
 ...         @return: action probabilities, state_value
 ...         """
 ...         return self.model(torch.Tensor(s))
  1. 现在,我们开发training方法,用于使用在一个 episode 中收集的样本更新神经网络:
>>>     def update(self, returns, log_probs, state_values):
 ...         """
 ...         Update the weights of the Actor Critic network 
                 given the training samples
 ...         @param returns: return (cumulative rewards) for 
                 each step in an episode
 ...         @param log_probs: log probability for each step
 ...         @param state_values: state-value for each step
 ...         """
 ...         loss = 0
 ...         for log_prob, value, Gt in zip( log_probs, state_values, returns):
 ...             advantage = Gt - value.item()
 ...             policy_loss = -log_prob * advantage
 ...             value_loss = F.smooth_l1_loss(value, Gt)
 ...             loss += policy_loss + value_loss
 ...         self.optimizer.zero_grad()
 ...         loss.backward()
 ...         self.optimizer.step()
  1. PolicyNetwork类的最终方法是get_action,根据预测的策略在给定状态下对动作进行抽样:
>>>     def get_action(self, s):
 ...         """
 ...         Estimate the policy and sample an action, 
                     compute its log probability
 ...         @param s: input state
 ...         @return: the selected action and log probability
 ...         """
 ...         action_probs, state_value = self.predict(s)
 ...         action = torch.multinomial(action_probs, 1).item()
 ...         log_prob = torch.log(action_probs[action])
 ...         return action, log_prob, state_value

它还返回所选动作的对数概率,以及估计的状态值。

这就是PolicyNetwork类的全部内容!

  1. 现在,我们可以继续开发主函数,训练演员-评论家模型:
>>> def actor_critic(env, estimator, n_episode, gamma=1.0):
 ...     """
 ...     Actor Critic algorithm
 ...     @param env: Gym environment
 ...     @param estimator: policy network
 ...     @param n_episode: number of episodes
 ...     @param gamma: the discount factor
 ...     """
 ...     for episode in range(n_episode):
 ...         log_probs = []
 ...         rewards = []
 ...         state_values = []
 ...         state = env.reset()
 ...         while True:
 ...             action, log_prob, state_value = 
                         estimator.get_action(state)
 ...             next_state, reward, is_done, _ = env.step(action)
 ...             total_reward_episode[episode] += reward
 ...             log_probs.append(log_prob)
 ...             state_values.append(state_value)
 ...             rewards.append(reward)
 ...
 ...             if is_done:
 ...                 returns = []
 ...                 Gt = 0
 ...                 pw = 0
 ...                 for reward in rewards[::-1]:
 ...                     Gt += gamma ** pw * reward
 ...                     pw += 1
 ...                     returns.append(Gt)
 ...                 returns = returns[::-1]
 ...                 returns = torch.tensor(returns)
 ...                 returns = (returns - returns.mean()) / 
                                     (returns.std() + 1e-9)
 ...                 estimator.update( returns, log_probs, state_values)
 ...                 print('Episode: {}, total reward: {}'.format( episode, total_reward_episode[episode]))
 ...                 if total_reward_episode[episode] >= 195:
 ...                     estimator.scheduler.step()
 ...                 break
 ...
 ...             state = next_state
  1. 我们指定策略网络的大小(输入、隐藏和输出层)、学习率,然后相应地创建一个PolicyNetwork实例:
>>> n_state = env.observation_space.shape[0]
>>> n_action = env.action_space.n
>>> n_hidden = 128
>>> lr = 0.03
>>> policy_net = PolicyNetwork(n_state, n_action, n_hidden, lr)

我们将折现因子设置为0.9

>>> gamma = 0.9
  1. 我们使用刚刚开发的策略网络进行演员-评论家算法的学习,进行了 1,000 个 episode,并跟踪每个 episode 的总奖励:
>>> n_episode = 1000
>>> total_reward_episode = [0] * n_episode
>>> actor_critic(env, policy_net, n_episode, gamma)
  1. 最后,我们显示随时间变化的 episode 奖励的图表:
>>> import matplotlib.pyplot as plt
>>> plt.plot(total_reward_episode)
>>> plt.title('Episode reward over time')
>>> plt.xlabel('Episode')
>>> plt.ylabel('Total reward')
>>> plt.show()

工作原理是...

正如您在步骤 2中所看到的,演员和评论家共享输入和隐藏层的参数;演员的输出包括采取各个动作的概率,评论家的输出是输入状态的估计值。

步骤 5中,我们计算优势值及其负对数似然。演员-评论家中的损失函数是优势的负对数似然与回报与估计状态值之间均方误差的组合。请注意,我们使用smooth_l1_loss,当绝对误差低于 1 时,它是一个*方项,否则是一个绝对误差。

Step 7中,演员-评论者模型的训练函数执行以下任务:

  • 它运行一个 episode:对于每个步骤,根据当前估计的策略采样一个动作;它在每个步骤存储奖励、对数策略和估计的状态值。

  • 一旦一个 episode 结束,它会计算每一步的折现累积奖励;然后通过减去它们的均值并除以它们的标准差来归一化返回结果。

  • 它使用回报、对数概率和状态值更新策略参数。我们还会显示每个 episode 的总奖励。

  • 如果一个 episode 的总奖励超过+195,我们会稍微降低学习率。

  • 通过重复上述步骤,它运行n_episode个 episode。

在执行Step 9的训练后,您将看到以下日志:

Episode: 0, total reward: 18.0
Episode: 1, total reward: 9.0
Episode: 2, total reward: 9.0
Episode: 3, total reward: 10.0
Episode: 4, total reward: 10.0
...
...
Episode: 995, total reward: 200.0
Episode: 996, total reward: 200.0
Episode: 997, total reward: 200.0
Episode: 998, total reward: 200.0
Episode: 999, total reward: 200.0

下面的图表展示了Step 10的结果:

您可以看到大约 400 个 episode 后的奖励保持在+200 的最大值。

在优势演员-评论者算法中,我们将学习分解为两个部分 - 演员和评论者。A2C 中的评论者评估在状态下动作的好坏,这指导演员如何反应。再次,优势值被计算为 A(s,a) = Q(s,a) - V(s),这意味着从 Q 值中减去状态值。演员根据评论者的指导估计动作的概率。优势的引入可以减少方差,因此 A2C 被认为比标准演员-评论者模型更稳定。正如我们在 CartPole 环境中看到的,经过数百个 episode 的训练后,A2C 的表现一直很稳定。它优于带基准的 REINFORCE 算法。

使用演员-评论者算法解决 Cliff Walking

在这个示例中,我们将使用 A2C 算法解决一个更复杂的 Cliff Walking 环境问题。

Cliff Walking 是一个典型的 Gym 环境,episode 很长且没有终止的保证。这是一个 4 * 12 的网格问题。代理在每一步可以向上、向右、向下和向左移动。左下角的瓦片是代理的起点,右下角是获胜点,如果到达则结束 episode。最后一行的其余瓦片是悬崖,代理在踩到它们后将被重置到起始位置,但 episode 继续。代理每走一步会产生-1 的奖励,但踩到悬崖时会产生-100 的奖励。

状态是一个从 0 到 47 的整数,表示代理的位置,如图所示:

这样的值并不包含数值意义。例如,处于状态 30 并不意味着它比处于状态 10 多 3 倍。因此,在将状态输入策略网络之前,我们将首先将其转换为一个 one-hot 编码向量。

如何做到……

我们使用 A2C 算法解决 Cliff Walking 如下:

  1. 导入所有必要的包,并创建一个 CartPole 实例:
>>> import gym
>>> import torch
>>> import torch.nn as nn
>>> import torch.nn.functional as F >>> env = gym.make('CliffWalking-v0')
  1. 由于状态变为 48 维,我们使用了一个更复杂的具有两个隐藏层的 actor-critic 神经网络:
>>> class ActorCriticModel(nn.Module):
 ...     def __init__(self, n_input, n_output, n_hidden):
 ...         super(ActorCriticModel, self).__init__()
 ...         self.fc1 = nn.Linear(n_input, n_hidden[0])
 ...         self.fc2 = nn.Linear(n_hidden[0], n_hidden[1])
 ...         self.action = nn.Linear(n_hidden[1], n_output)
 ...         self.value = nn.Linear(n_hidden[1], 1)
 ...
 ...     def forward(self, x):
 ...         x = torch.Tensor(x)
 ...         x = F.relu(self.fc1(x))
 ...         x = F.relu(self.fc2(x))
 ...         action_probs = F.softmax(self.action(x), dim=-1)
 ...         state_values = self.value(x)
 ...         return action_probs, state_values

再次强调,actor 和 critic 共享输入和隐藏层的参数。

  1. 我们继续使用刚刚在Step 2中开发的 actor-critic 神经网络来使用PolicyNetwork类。它与Implementing the actor-critic algorithm案例中的PolicyNetwork类相同。

  2. 接下来,我们开发主函数,训练一个 actor-critic 模型。它几乎与Implementing the actor-critic algorithm案例中的模型相同,只是额外将状态转换为 one-hot 编码向量:

>>> def actor_critic(env, estimator, n_episode, gamma=1.0):
 ...     """
 ...     Actor Critic algorithm
 ...     @param env: Gym environment
 ...     @param estimator: policy network
 ...     @param n_episode: number of episodes
 ...     @param gamma: the discount factor
 ...     """
 ...     for episode in range(n_episode):
 ...         log_probs = []
 ...         rewards = []
 ...         state_values = []
 ...         state = env.reset()
 ...         while True:
 ...             one_hot_state = [0] * 48
 ...             one_hot_state[state] = 1
 ...             action, log_prob, state_value = 
                     estimator.get_action(one_hot_state)
 ...             next_state, reward, is_done, _ = env.step(action)
 ...             total_reward_episode[episode] += reward
 ...             log_probs.append(log_prob)
 ...             state_values.append(state_value)
 ...             rewards.append(reward)
 ...
 ...             if is_done:
 ...                 returns = []
 ...                 Gt = 0
 ...                 pw = 0
 ...                 for reward in rewards[::-1]:
 ...                     Gt += gamma ** pw * reward
 ...                     pw += 1
 ...                     returns.append(Gt)
 ...                 returns = returns[::-1]
 ...                 returns = torch.tensor(returns)
 ...                 returns = (returns - returns.mean()) / 
                                 (returns.std() + 1e-9)
 ...                 estimator.update( returns, log_probs, state_values)
 ...                 print('Episode: {}, total reward: {}'.format( episode, total_reward_episode[episode]))
 ...                 if total_reward_episode[episode] >= -14:
 ...                     estimator.scheduler.step()
 ...                 break
 ...
 ...             state = next_state
  1. 我们指定策略网络的大小(输入、隐藏和输出层)、学习率,然后相应地创建一个PolicyNetwork实例:
>>> n_state = 48
>>> n_action = env.action_space.n
>>> n_hidden = [128, 32]
>>> lr = 0.03
>>> policy_net = PolicyNetwork(n_state, n_action, n_hidden, lr)

我们将折扣因子设为0.9

>>> gamma = 0.9
  1. 我们使用刚刚开发的策略网络进行 1000 个 episode 的 actor-critic 算法学习,并跟踪每个 episode 的总奖励:
>>> n_episode = 1000
>>> total_reward_episode = [0] * n_episode
>>> actor_critic(env, policy_net, n_episode, gamma)
  1. 现在,我们展示自第 100 个 episode 开始训练后的奖励变化曲线图:
>>> import matplotlib.pyplot as plt
>>> plt.plot(range(100, n_episode), total_reward_episode[100:])
>>> plt.title('Episode reward over time')
>>> plt.xlabel('Episode')
>>> plt.ylabel('Total reward')
>>> plt.show()

工作原理...

您可能会注意到在Step 4中,如果一个 episode 的总奖励超过-14,我们会略微降低学习率。-13 的奖励是我们能够通过路径 36-24-25-26-27-28-29-30-31-32-33-34-35-47 获得的最大值。

执行Step 6训练后,您将看到以下日志:

Episode: 0, total reward: -85355
 Episode: 1, total reward: -3103
 Episode: 2, total reward: -1002
 Episode: 3, total reward: -240
 Episode: 4, total reward: -118
 ...
 ...
 Episode: 995, total reward: -13
 Episode: 996, total reward: -13
 Episode: 997, total reward: -13
 Episode: 998, total reward: -13
 Episode: 999, total reward: -13

下图显示了Step 7的结果:

正如我们可以观察到的那样,在大约第 180 个 episode 之后,大多数 episode 的奖励达到了最优值-13。

在这个案例中,我们使用 A2C 算法解决了 Cliff Walking 问题。整数状态从 0 到 47 表示 4*12 棋盘中代理的位置,这些位置并没有数值意义,因此我们首先将其转换为 48 维的 one-hot 编码向量。为了处理 48 维输入,我们使用了一个稍微复杂的具有两个隐藏层的神经网络。在我们的实验中,A2C 已被证明是一个稳定的策略方法。

设置连续的 Mountain Car 环境

到目前为止,我们所处理的环境都具有离散的动作值,比如 0 或 1,代表上下左右。在这个案例中,我们将体验一个具有连续动作的 Mountain Car 环境。

连续的 Mountain Car(github.com/openai/gym/wiki/MountainCarContinuous-v0)是一个具有连续动作的 Mountain Car 环境,其值从-1 到 1。如下截图所示,其目标是将汽车驶到右侧山顶上:

在一维赛道上,汽车位于-1.2(最左侧)到 0.6(最右侧)之间,并且目标(黄旗)位于 0.5 处。汽车的引擎不足以在一次通过中将其推上山顶,因此必须来回驾驶以积累动量。因此,动作是一个浮点数,表示如果其值在-1 到 0 之间则将汽车向左推,如果在 0 到 1 之间则将汽车向右推。

环境有两个状态:

  • 汽车的位置:这是一个从-1.2 到 0.6 的连续变量

  • 汽车的速度:这是一个从-0.07 到 0.07 的连续变量

初始状态包括位置在-0.6 到-0.4 之间,速度为 0。

每一步的奖励与动作 a 相关,为-a²。并且达到目标还会有额外的+100 奖励。因此,它惩罚了每一步中所采取的力量,直到汽车到达目标位置。一个 episode 在汽车到达目标位置(显然是),或者经过 1000 步后结束。

如何操作...

让我们通过以下步骤来模拟连续的山车环境:

  1. 我们导入 Gym 库并创建一个连续山车环境的实例:
>>> import gym
>>> import torch
>>> env = gym.envs.make("MountainCarContinuous-v0")
  1. 看一下动作空间:
>>> print(env.action_space.low[0])
-1.0
>>> print(env.action_space.high[0])
1.0
  1. 然后我们重置环境:
>>> env.reset()
array([-0.56756635,  0\. ])

汽车的初始状态为[-0.56756635, 0. ],这意味着初始位置大约为-0.56,速度为 0. 由于初始位置是从-0.6 到-0.4 随机生成的,所以可能看到不同的初始位置。

  1. 现在让我们采取一个简单的方法:我们只是随机选择一个动作从-1 到 1:
>>> is_done = False
>>> while not is_done:
 ...     random_action = torch.rand(1) * 2 - 1
 ...     next_state, reward, is_done, info = env.step(random_action)
 ...     print(next_state, reward, is_done)
 ...     env.render()
 >>> env.render()
 [-0.5657432   0.00182313] -0.09924464356736849 False
 [-0.5622848   0.00345837] -0.07744002014160288 False
 [-0.55754507  0.00473979] -0.04372991690837722 False
 ......
 ......

状态(位置和速度)会相应地发生变化,每一步的奖励为-a²

您还会在视频中看到汽车反复向右移动和向左移动。

工作原理是这样的...

正如你所想象的那样,连续的山车问题是一个具有挑战性的环境,甚至比仅有三种不同可能动作的原始离散问题更加困难。我们需要来回驾驶汽车以积累正确的力量和方向。此外,动作空间是连续的,这意味着值查找/更新方法(如 TD 方法、DQN)将不起作用。在下一个示例中,我们将使用 A2C 算法的连续控制版本来解决连续的山车问题。

使用优势演员-评论者网络解决连续山车环境

在这个示例中,我们将使用优势演员-评论者算法来解决连续的山车问题,这当然是一个连续版本。你会看到它与离散版本有何不同。

正如我们在具有离散动作的环境中看到的那样,在连续控制中,由于我们无法对无数个连续动作进行采样,我们如何建模?实际上,我们可以借助高斯分布。我们可以假设动作值服从高斯分布:

在这里,均值,![],以及偏差,,是从策略网络中计算出来的。通过这种调整,我们可以通过当前均值和偏差构建的高斯分布采样动作。连续 A2C 中的损失函数类似于我们在离散控制中使用的损失函数,它是在高斯分布下动作概率的负对数似然和优势值之间的组合,以及实际回报值与预估状态值之间的回归误差。

注意,一个高斯分布用于模拟一个维度的动作,因此,如果动作空间是 k 维的,我们需要使用 k 个高斯分布。在连续 Mountain Car 环境中,动作空间是一维的。就连续控制而言,A2C 的主要困难在于如何构建策略网络,因为它计算了高斯分布的参数。

如何做...

我们使用连续 A2C 来解决连续 Mountain Car 问题,具体如下:

  1. 导入所有必要的包并创建一个连续 Mountain Car 实例:
>>> import gym
>>> import torch
>>> import torch.nn as nn
>>> import torch.nn.functional as F >>> env = gym.make('MountainCarContinuous-v0')
  1. 让我们从演员-评论神经网络模型开始:
>>> class ActorCriticModel(nn.Module):
 ...     def __init__(self, n_input, n_output, n_hidden):
 ...         super(ActorCriticModel, self).__init__()
 ...         self.fc = nn.Linear(n_input, n_hidden)
 ...         self.mu = nn.Linear(n_hidden, n_output)
 ...         self.sigma = nn.Linear(n_hidden, n_output)
 ...         self.value = nn.Linear(n_hidden, 1)
 ...         self.distribution = torch.distributions.Normal
 ...
 ...     def forward(self, x):
 ...         x = F.relu(self.fc(x))
 ...         mu = 2 * torch.tanh(self.mu(x))
 ...         sigma = F.softplus(self.sigma(x)) + 1e-5
 ...         dist = self.distribution( mu.view(1, ).data, sigma.view(1, ).data)
 ...         value = self.value(x)
 ...         return dist, value
  1. 我们继续使用刚刚开发的演员-评论神经网络的PolicyNetwork类中的__init__方法:
>>> class PolicyNetwork():
 ...     def __init__(self, n_state, n_action, 
                     n_hidden=50, lr=0.001):
 ...         self.model = ActorCriticModel( n_state, n_action, n_hidden)
 ...         self.optimizer = torch.optim.Adam( self.model.parameters(), lr)
  1. 接下来,我们添加predict方法,用于计算预估的动作概率和状态值:
>>>     def predict(self, s):
 ...         """
 ...         Compute the output using the continuous Actor Critic model
 ...         @param s: input state
 ...         @return: Gaussian distribution, state_value
 ...         """
 ...         self.model.training = False
 ...         return self.model(torch.Tensor(s))
  1. 我们现在开发训练方法,该方法使用一个 episode 中收集的样本来更新策略网络。我们将重用实施演员-评论算法食谱中开发的更新方法,这里不再重复。

  2. PolicyNetwork类的最终方法是get_action,它从给定状态的预估高斯分布中采样一个动作:

>>>     def get_action(self, s):
 ...         """
 ...         Estimate the policy and sample an action, 
                 compute its log probability
 ...         @param s: input state
 ...         @return: the selected action, log probability, 
                 predicted state-value
 ...         """
 ...         dist, state_value = self.predict(s)
 ...         action = dist.sample().numpy()
 ...         log_prob = dist.log_prob(action[0])
 ...         return action, log_prob, state_value

它还返回所选动作的对数概率和预估状态值。

这就是用于连续控制的PolicyNetwork类的全部内容!

现在,我们可以继续开发主函数,训练一个演员-评论模型:

>>> def actor_critic(env, estimator, n_episode, gamma=1.0):
 ...     """
 ...     continuous Actor Critic algorithm
 ...     @param env: Gym environment
 ...     @param estimator: policy network
 ...     @param n_episode: number of episodes
 ...     @param gamma: the discount factor
 ...     """
 ...     for episode in range(n_episode):
 ...         log_probs = []
 ...         rewards = []
 ...         state_values = []
 ...         state = env.reset()
 ...         while True:
 ...             state = scale_state(state)
 ...             action, log_prob, state_value = 
                         estimator.get_action(state)
 ...             action = action.clip(env.action_space.low[0],
 ...                                  env.action_space.high[0])
 ...             next_state, reward, is_done, _ = env.step(action)
 ...             total_reward_episode[episode] += reward
 ...             log_probs.append(log_prob)
 ...             state_values.append(state_value)
 ...             rewards.append(reward)
 ...             if is_done:
 ...                 returns = []
 ...                 Gt = 0
 ...                 pw = 0
 ...                 for reward in rewards[::-1]:
 ...                     Gt += gamma ** pw * reward
 ...                     pw += 1
 ...                     returns.append(Gt)
 ...                 returns = returns[::-1]
 ...                 returns = torch.tensor(returns)
 ...                 returns = (returns - returns.mean()) / 
                                 (returns.std() + 1e-9)
 ...                 estimator.update( returns, log_probs, state_values)
 ...                 print('Episode: {}, total reward: {}'.format( episode, total_reward_episode[episode]))
 ...                 break
 ...             state = next_state
  1. scale_state函数用于对输入进行标准化(规范化),以加快模型的收敛速度。我们首先随机生成 10,000 个观测数据,并用它们来训练一个缩放器:
>>> import sklearn.preprocessing
 >>> import numpy as np
 >>> state_space_samples = np.array(
 ...     [env.observation_space.sample() for x in range(10000)])
 >>> scaler = sklearn.preprocessing.StandardScaler()
 >>> scaler.fit(state_space_samples)

一旦缩放器被训练好,我们就在scale_state函数中使用它来转换新的输入数据:

>>> def scale_state(state):
 ...     scaled = scaler.transform([state])
 ...     return scaled[0]
  1. 我们指定策略网络的大小(输入、隐藏和输出层),学习率,然后相应地创建一个PolicyNetwork实例:
>>> n_state = env.observation_space.shape[0]
>>> n_action = 1
>>> n_hidden = 128
>>> lr = 0.0003
>>> policy_net = PolicyNetwork(n_state, n_action, n_hidden, lr)

我们将折现因子设为0.9

>>> gamma = 0.9
  1. 我们使用刚刚开发的策略网络进行 200 个剧集的 actor-critic 算法进行连续控制,并且我们还跟踪每个剧集的总奖励:
>>> n_episode = 200
>>> total_reward_episode = [0] * n_episode
>>> actor_critic(env, policy_net, n_episode, gamma)
  1. 现在,让我们展示随时间变化的剧集奖励图:
>>> import matplotlib.pyplot as plt
>>> plt.plot(total_reward_episode)
>>> plt.title('Episode reward over time')
>>> plt.xlabel('Episode')
>>> plt.ylabel('Total reward')
>>> plt.show()

如何工作...

在这个配方中,我们使用高斯 A2C 来解决连续的 Mountain Car 环境。

第 2 步中,我们示例中的网络有一个隐藏层。输出层有三个单独的组件。它们是高斯分布的均值和偏差,以及状态值。分布均值的输出通过 tanh 激活函数缩放到[-1, 1]范围(或此示例中的[-2, 2]),而分布偏差使用 softplus 作为激活函数以确保正偏差。网络返回当前的高斯分布(actor)和估计的状态值(critic)。

第 7 步的 actor-critic 模型训练函数与我们在实施 actor-critic 算法配方中开发的内容非常相似。您可能会注意到我们在采样动作时添加了一个值剪辑,以使其保持在[-1, 1]范围内。我们将在接下来的步骤中解释scale_state函数的作用。

在执行第 10 步的训练后,您将看到以下日志:

Episode: 0, total reward: 89.46417524456328
 Episode: 1, total reward: 89.54226159679301
 Episode: 2, total reward: 89.91828341346695
 Episode: 3, total reward: 90.04199470314816
 Episode: 4, total reward: 86.23157467747066
 ...
 ...
 Episode: 194, total reward: 92.71676277432059
 Episode: 195, total reward: 89.97484988523927
 Episode: 196, total reward: 89.26063135086025
 Episode: 197, total reward: 87.19460382302674
 Episode: 198, total reward: 79.86081433777699
 Episode: 199, total reward: 88.98075638481279

以下图表是第 11 步的结果:

根据github.com/openai/gym/wiki/MountainCarContinuous-v0中解决的要求,获得超过+90 的奖励被视为环境已解决。我们有多个剧集解决了环境问题。

在连续的 A2C 中,我们假设动作空间的每个维度都服从高斯分布。高斯分布的均值和偏差是策略网络输出层的一部分。输出层的其余部分用于估计状态值。从当前均值和偏差参数化的高斯分布中采样一个或多个动作。连续 A2C 的损失函数类似于其离散版本,即负对数似然与高斯分布下动作概率以及优势值之间的组合,以及实际回报值与估计状态值之间的回归误差。

还有更多内容...

到目前为止,我们一直是以随机的方式建模策略,从分布或计算的概率中采样动作。作为一个额外部分,我们将简要讨论确定性策略梯度DPG),在这里我们将策略建模为确定性决策。我们简单地将确定性策略视为随机策略的特例,直接将输入状态映射到动作而不是动作的概率。DPG 算法通常使用以下两组神经网络:

  • Actor-critic 网络:这与我们之前体验过的 A2C 非常相似,但是是以确定性方式进行。它预测状态值和需要执行的动作。

  • 目标 actor-critic 网络:这是 actor-critic 网络的定期副本,其目的是稳定学习。显然,你不希望目标一直在变化。该网络为训练提供了延迟的目标。

正如你所看到的,在 DPG 中并没有太多新东西,但它是 A2C 和延迟目标机制的良好结合。请随意自行实现该算法,并用它来解决连续的 Mountain Car 环境。

另请参阅

如果你对 softplus 激活函数不熟悉,或者想要了解更多关于 DPG 的内容,请查看以下材料:

通过交叉熵方法玩 CartPole

在这个最后的示例中,作为一个额外的(也很有趣的)部分,我们将开发一个简单而强大的算法来解决 CartPole 问题。它基于交叉熵,直接将输入状态映射到输出动作。事实上,它比本章中所有其他策略梯度算法更为直接。

我们已经应用了几种策略梯度算法来解决 CartPole 环境。它们使用复杂的神经网络架构和损失函数,这对于如 CartPole 这样的简单环境可能有些过度。为什么不直接预测给定状态下的动作呢?其背后的思想很简单:我们对过去最成功的经验进行建模,仅对正确的动作感兴趣。在这种情况下,目标函数是实际动作和预测动作之间的交叉熵。在 CartPole 中,有两种可能的动作:左和右。为了简单起见,我们可以将其转换为二元分类问题,并使用以下模型图表述:

如何实现它...

我们使用交叉熵来解决 CartPole 问题如下:

  1. 导入所有必要的包并创建一个 CartPole 实例:
>>> import gym
>>> import torch
>>> import torch.nn as nn
>>> from torch.autograd import Variable >>> env = gym.make('CartPole-v0')
  1. 让我们从动作估算器开始:
>>> class Estimator():
 ...     def __init__(self, n_state, lr=0.001):
 ...         self.model = nn.Sequential(
 ...                         nn.Linear(n_state, 1),
 ...                         nn.Sigmoid()
 ...                 )
 ...         self.criterion = torch.nn.BCELoss()
 ...         self.optimizer = torch.optim.Adam( self.model.parameters(), lr)
 ...
 ...     def predict(self, s):
 ...         return self.model(torch.Tensor(s))
 ...
 ...     def update(self, s, y):
 ...         """
 ...         Update the weights of the estimator given 
                 the training samples
 ...         """
 ...         y_pred = self.predict(s)
 ...         loss = self.criterion( y_pred, Variable(torch.Tensor(y)))
 ...         self.optimizer.zero_grad()
 ...         loss.backward()
 ...         self.optimizer.step()
  1. 现在我们为交叉熵算法开发主要的训练函数:
>>> def cross_entropy(env, estimator, n_episode, n_samples):
 ...     """
 ...     Cross-entropy algorithm for policy learning
 ...     @param env: Gym environment
 ...     @param estimator: binary estimator
 ...     @param n_episode: number of episodes
 ...     @param n_samples: number of training samples to use
 ...     """
 ...     experience = []
 ...     for episode in range(n_episode):
 ...         rewards = 0
 ...         actions = []
 ...         states = []
 ...         state = env.reset()
 ...         while True:
 ...             action = env.action_space.sample()
 ...             states.append(state)
 ...             actions.append(action)
 ...             next_state, reward, is_done, _ = env.step(action)
 ...             rewards += reward
 ...             if is_done:
 ...                 for state, action in zip(states, actions):
 ...                     experience.append((rewards, state, action))
 ...                 break
 ...             state = next_state
 ...
 ...     experience = sorted(experience, 
                         key=lambda x: x[0], reverse=True)
 ...     select_experience = experience[:n_samples]
 ...     train_states = [exp[1] for exp in select_experience]
 ...     train_actions = [exp[2] for exp in select_experience]
 ...
 ...     for _ in range(100):
 ...         estimator.update(train_states, train_actions)
  1. 然后我们指定动作估算器的输入大小和学习率:
>>> n_state = env.observation_space.shape[0]
>>> lr = 0.01

然后我们相应地创建一个 Estimator 实例:

>>> estimator = Estimator(n_state, lr)
  1. 我们将生成 5,000 个随机的情节,并精选出最佳的 10,000 个(状态,动作)对用于估算器的训练:
>>> n_episode = 5000
>>> n_samples = 10000
>>> cross_entropy(env, estimator, n_episode, n_samples)
  1. 模型训练完成后,让我们来测试一下。我们将用它来玩 100 个情节,并记录总奖励:
>>> n_episode = 100
>>> total_reward_episode = [0] * n_episode
>>> for episode in range(n_episode):
 ...     state = env.reset()
 ...     is_done = False
 ...     while not is_done:
 ...         action = 1 if estimator.predict(state).item() >= 0.5 else 0
 ...         next_state, reward, is_done, _ = env.step(action)
 ...         total_reward_episode[episode] += reward
 ...         state = next_state
  1. 然后我们将性能可视化如下:
>>> import matplotlib.pyplot as plt
>>> plt.plot(total_reward_episode)
>>> plt.title('Episode reward over time')
>>> plt.xlabel('Episode')
>>> plt.ylabel('Total reward')
>>> plt.show()

工作原理...

正如您在Step 2中所看到的,动作估计器有两层 - 输入层和输出层,接着是一个 sigmoid 激活函数,损失函数是二元交叉熵。

Step 3是为了训练交叉熵模型。具体而言,对于每个训练集,我们采取随机行动,累积奖励,并记录状态和行动。在体验了n_episode个集数后,我们提取最成功的集数(具有最高总奖励)并提取n_samples个(状态,行动)对作为训练样本。然后我们在刚构建的训练集上对估计器进行 100 次迭代的训练。

执行Step 7中的代码行将产生以下绘图:

正如您所见,所有测试集都有+200 的奖励!

交叉熵对于简单环境非常简单,但却很有用。它直接建模输入状态和输出行动之间的关系。一个控制问题被构建成一个分类问题,我们试图在所有备选行动中预测正确的行动。关键在于我们只从正确的经验中学习,这指导模型在给定状态时应该选择哪个最有益的行动。

第九章:毕业项目 – 使用 DQN 玩 Flappy Bird

在这最后一章中,我们将致力于一个毕业项目——使用强化学习玩 Flappy Bird。我们将应用我们在本书中学到的知识来构建一个智能机器人。我们还将专注于构建深度 Q 网络DQNs),微调模型参数并部署模型。让我们看看鸟能在空中停留多久。

最后一个章节将通过以下步骤逐步构建毕业项目:

  • 设置游戏环境

  • 构建一个深度 Q 网络来玩 Flappy Bird

  • 训练和调整网络

  • 部署模型并玩游戏

因此,每个食谱中的代码都将基于前面的食谱构建。

设置游戏环境

要使用 DQN 玩 Flappy Bird,我们首先需要设置环境。

我们将使用 Pygame 模拟 Flappy Bird 游戏。Pygame (www.pygame.org) 包含一组为创建视频游戏而开发的 Python 模块。它还包括在游戏中需要的图形和声音库。我们可以按照以下方式安装 Pygame 包:

pip install pygame

Flappy Bird 是由 Dong Nguyen 最初开发的一款著名移动游戏。你可以在 flappybird.io/ 使用键盘自己尝试。游戏的目标是尽可能长时间地保持存活。当鸟触碰到地面或管道时游戏结束。因此,鸟需要在正确的时机振翅通过随机的管道,避免落到地面上。可能的动作包括振翅和不振翅。在游戏环境中,每一步的奖励是 +0.1,并有以下两个例外情况:

  • 当发生碰撞时为 -1

  • 当鸟通过两个管道之间的间隙时为 +1。原始的 Flappy Bird 游戏根据通过的间隙数量进行评分。

准备工作

github.com/yanpanlau/Keras-FlappyBird/tree/master/assets/sprites 下载我们需要的游戏环境资产。为简单起见,我们将只使用 sprites 文件夹中的图像。具体来说,我们需要以下图像:

  • background-black.png: 屏幕的背景图像

  • base.png: 地板的图像

  • pipe-green.png: 鸟需要避开的管道的图像

  • redbird-downflap.png: 鸟向下振翅时的图像

  • redbird-midflap.png: 鸟静止时的图像

  • redbird-upflap.png: 鸟向上振翅时的图像

如果您感兴趣,还可以使用音频文件使游戏更有趣。

如何做…

我们将使用 Pygame 开发 Flappy Bird 游戏环境,步骤如下:

  1. 我们首先开发一个实用函数,加载图像并将其转换为正确的格式:
>>> from pygame.image import load
 >>> from pygame.surfarray import pixels_alpha
 >>> from pygame.transform import rotate
 >>> def load_images(sprites_path):
 ...     base_image = load(sprites_path + 
                             'base.png').convert_alpha()
 ...     background_image = load(sprites_path + 
                             'background-black.png').convert()
 ...     pipe_images = [rotate(load(sprites_path + 
                        'pipe-green.png').convert_alpha(), 180),
 ...                    load(sprites_path + 
                             'pipe-green.png').convert_alpha()]
 ...     bird_images = [load(sprites_path + 
                           'redbird-upflap.png').convert_alpha(),
 ...                    load(sprites_path + 
                         'redbird-midflap.png').convert_alpha(),
 ...                    load(sprites_path + 
                         'redbird-downflap.png').convert_alpha()]
 ...     bird_hitmask = [pixels_alpha(image).astype(bool) 
                             for image in bird_images]
 ...     pipe_hitmask = [pixels_alpha(image).astype(bool) 
                             for image in pipe_images]
 ...     return base_image, background_image, pipe_images, 
                 bird_images, bird_hitmask, pipe_hitmask
  1. 导入环境所需的所有包:
>>> from itertools import cycle
>>> from random import randint
>>> import pygame
  1. 初始化游戏和时钟,并将屏幕刷新频率设置为每秒 30 帧:
>>> pygame.init()
>>> fps_clock = pygame.time.Clock() >>> fps = 30
  1. 指定屏幕大小并相应地创建屏幕,然后为屏幕添加标题:
>>> screen_width = 288
 >>> screen_height = 512
 >>> screen = pygame.display.set_mode((screen_width, screen_height)) >>> pygame.display.set_caption('Flappy Bird')
  1. 然后,使用以下函数加载必要的图像(位于sprites文件夹中):
>>> base_image, background_image, pipe_images, bird_images, bird_hitmask, pipe_hitmask = load_images('sprites/')
  1. 获取游戏变量,包括鸟和管道的大小,并设置两个管道之间的垂直间隙为 100:
>>> bird_width = bird_images[0].get_width()
>>> bird_height = bird_images[0].get_height()
>>> pipe_width = pipe_images[0].get_width()
>>> pipe_height = pipe_images[0].get_height() >>> pipe_gap_size = 100
  1. 鸟的振动运动依次为向上、中间、向下、中间、向上等:
>>> bird_index_gen = cycle([0, 1, 2, 1])

这仅仅是为了使游戏更加有趣。

  1. 在定义完所有常量后,我们从游戏环境的FlappyBird类的__init__method开始:
>>> class FlappyBird(object):
 ...     def __init__(self):
 ...         self.pipe_vel_x = -4
 ...         self.min_velocity_y = -8
 ...         self.max_velocity_y = 10
 ...         self.downward_speed = 1
 ...         self.upward_speed = -9
 ...         self.cur_velocity_y = 0
 ...         self.iter = self.bird_index = self.score = 0
 ...         self.bird_x = int(screen_width / 5)
 ...         self.bird_y = int((screen_height - bird_height) / 2)
 ...         self.base_x = 0
 ...         self.base_y = screen_height * 0.79
 ...         self.base_shift = base_image.get_width() - 
                             background_image.get_width()
 ...         self.pipes = [self.gen_random_pipe(screen_width), 
                         self.gen_random_pipe(screen_width * 1.5)]
 ...         self.is_flapped = False
  1. 我们继续定义gen_random_pipe方法,该方法在给定的水*位置和随机垂直位置生成一对管道(一个上管道和一个下管道):
>>>     def gen_random_pipe(self, x):
 ...         gap_y = randint(2, 10) * 10 + int(self.base_y * 0.2)
 ...         return {"x_upper": x,
 ...                 "y_upper": gap_y - pipe_height,
 ...                 "x_lower": x,
 ...                 "y_lower": gap_y + pipe_gap_size}

上下两个管道的y位置分别为gap_y - pipe_heightgap_y + pipe_gap_size

  1. 我们接下来开发的方法是check_collision,如果鸟与基座或管道碰撞,则返回True
>>>     def check_collision(self):
 ...         if bird_height + self.bird_y >= self.base_y - 1:
 ...             return True
 ...         bird_rect = pygame.Rect(self.bird_x, self.bird_y, 
                                     bird_width, bird_height)
 ...         for pipe in self.pipes:
 ...             pipe_boxes = [pygame.Rect(pipe["x_upper"], 
                          pipe["y_upper"], pipe_width, pipe_height),
 ...                           pygame.Rect(pipe["x_lower"], 
                          pipe["y_lower"], pipe_width, pipe_height)]
 ...             # Check if the bird's bounding box overlaps to 
                     the bounding box of any pipe
 ...             if bird_rect.collidelist(pipe_boxes) == -1:
 ...                 return False
 ...             for i in range(2):
 ...                 cropped_bbox = bird_rect.clip(pipe_boxes[i])
 ...                 x1 = cropped_bbox.x - bird_rect.x
 ...                 y1 = cropped_bbox.y - bird_rect.y
 ...                 x2 = cropped_bbox.x - pipe_boxes[i].x
 ...                 y2 = cropped_bbox.y - pipe_boxes[i].y
 ...                 for x in range(cropped_bbox.width):
 ...                     for y in range(cropped_bbox.height):
 ...                         if bird_hitmask[self.bird_index][x1+x, 
                                    y1+y] and pipe_hitmask[i][
                                    x2+x, y2+y]:
 ...                             return True
 ...         return False
  1. 我们最后需要的最重要的方法是next_step,它执行一个动作并返回游戏的更新图像帧、收到的奖励以及本轮游戏是否结束:
>>>     def next_step(self, action):
 ...         pygame.event.pump()
 ...         reward = 0.1
 ...         if action == 1:
 ...             self.cur_velocity_y = self.upward_speed
 ...             self.is_flapped = True
 ...         # Update score
 ...         bird_center_x = self.bird_x + bird_width / 2
 ...         for pipe in self.pipes:
 ...             pipe_center_x = pipe["x_upper"] + 
                                     pipe_width / 2
 ...             if pipe_center_x < bird_center_x 
                                 < pipe_center_x + 5:
 ...                 self.score += 1
 ...                 reward = 1
 ...                 break
 ...         # Update index and iteration
 ...         if (self.iter + 1) % 3 == 0:
 ...             self.bird_index = next(bird_index_gen)
 ...         self.iter = (self.iter + 1) % fps
 ...         self.base_x = -((-self.base_x + 100) % 
                                 self.base_shift)
 ...         # Update bird's position
 ...         if self.cur_velocity_y < self.max_velocity_y 
                             and not self.is_flapped:
 ...             self.cur_velocity_y += self.downward_speed
 ...         self.is_flapped = False
 ...         self.bird_y += min(self.cur_velocity_y, 
                 self.bird_y - self.cur_velocity_y - bird_height)
 ...         if self.bird_y < 0:
 ...             self.bird_y = 0
 ...         # Update pipe position
 ...         for pipe in self.pipes:
 ...             pipe["x_upper"] += self.pipe_vel_x
 ...             pipe["x_lower"] += self.pipe_vel_x
 ...         # Add new pipe when first pipe is     
                 about to touch left of screen
 ...         if 0 < self.pipes[0]["x_lower"] < 5:
 ...             self.pipes.append(self.gen_random_pipe( screen_width + 10))
 ...         # remove first pipe if its out of the screen
 ...         if self.pipes[0]["x_lower"] < -pipe_width:
 ...             self.pipes.pop(0)
 ...         if self.check_collision():
 ...             is_done = True
 ...             reward = -1
 ...             self.__init__()
 ...         else:
 ...             is_done = False
 ...         # Draw sprites
 ...         screen.blit(background_image, (0, 0))
 ...         screen.blit(base_image, (self.base_x, self.base_y))
 ...         screen.blit(bird_images[self.bird_index], 
                             (self.bird_x, self.bird_y))
 ...         for pipe in self.pipes:
 ...             screen.blit(pipe_images[0], (pipe["x_upper"], pipe["y_upper"]))
 ...             screen.blit(pipe_images[1], 
                       (pipe["x_lower"], pipe["y_lower"]))
 ...         image = pygame.surfarray.array3d( pygame.display.get_surface())
 ...         pygame.display.update()
 ...         fps_clock.tick(fps)
 ...         return image, reward, is_done

至此,关于Flappy Bird环境的介绍就完成了。

它的工作原理...

第 8 步中,我们定义了管道的速度(每过 4 个单位向左移动一次)、鸟的最小和最大垂直速度(分别为-810)、其向上和向下加速度(分别为-91)、其默认垂直速度(0)、鸟图像的起始索引(0)、初始得分、鸟的初始水*和垂直位置、基座的位置,以及使用gen_random_pipe方法随机生成的管道的坐标。

第 11 步中,默认情况下,每个步骤的奖励为+0.1。如果动作是振翅,我们会增加鸟的垂直速度及其向上加速度。然后,我们检查鸟是否成功通过了一对管道。如果是,则游戏得分增加 1,步骤奖励变为+1。我们更新鸟的位置、其图像索引以及管道的位置。如果旧的一对管道即将离开屏幕左侧,将生成新的一对管道,并在旧的一对管道离开屏幕后删除它。如果发生碰撞,本轮游戏将结束,奖励为-1;游戏也将重置。最后,我们会在游戏屏幕上显示更新的帧。

构建一个 Deep Q-Network 来玩 Flappy Bird

现在Flappy Bird环境已经准备就绪,我们可以开始通过构建 DQN 模型来解决它。

正如我们所见,每次采取行动后都会返回一个屏幕图像。CNN 是处理图像输入的最佳神经网络架构之一。在 CNN 中,卷积层能够有效地从图像中提取特征,这些特征将传递到下游的全连接层。在我们的解决方案中,我们将使用具有三个卷积层和一个全连接隐藏层的 CNN。CNN 架构示例如下:

如何做到...

让我们开发一个基于 CNN 的 DQN 模型,步骤如下:

  1. 导入必要的模块:
>>> import torch
>>> import torch.nn as nn
>>> import torch.nn.functional as F
>>> import numpy as np
>>> import random
  1. 我们从 CNN 模型开始:
>>> class DQNModel(nn.Module):
 ...     def __init__(self, n_action=2):
 ...         super(DQNModel, self).__init__()
 ...         self.conv1 = nn.Conv2d(4, 32, 
                             kernel_size=8, stride=4)
 ...         self.conv2 = nn.Conv2d(32, 64, 4, stride=2)
 ...         self.conv3 = nn.Conv2d(64, 64, 3, stride=1)
 ...         self.fc = nn.Linear(7 * 7 * 64, 512)
 ...         self.out = nn.Linear(512, n_action)
 ...         self._create_weights()
 ...
 ...     def _create_weights(self):
 ...         for m in self.modules():
 ...             if isinstance(m, nn.Conv2d) or 
                                 isinstance(m, nn.Linear):
 ...                 nn.init.uniform(m.weight, -0.01, 0.01)
 ...                 nn.init.constant_(m.bias, 0)
 ...
 ...     def forward(self, x):
 ...         x = F.relu(self.conv1(x))
 ...         x = F.relu(self.conv2(x))
 ...         x = F.relu(self.conv3(x))
 ...         x = x.view(x.size(0), -1)
 ...         x = F.relu(self.fc(x))
 ...         output = self.out(x)
 ...         return output
  1. 现在使用我们刚刚构建的 CNN 模型开发一个带有经验回放的 DQN:
>>> class DQN():
 ...     def __init__(self, n_action, lr=1e-6):
 ...         self.criterion = torch.nn.MSELoss()
 ...         self.model = DQNModel(n_action)
 ...         self.optimizer = torch.optim.Adam( self.model.parameters(), lr)
  1. predict方法根据输入状态估计输出 Q 值:
>>>     def predict(self, s):
 ...         """
 ...         Compute the Q values of the state for all 
                 actions using the learning model
 ...         @param s: input state
 ...         @return: Q values of the state for all actions
 ...         """
 ...         return self.model(torch.Tensor(s))
  1. update方法根据训练样本更新神经网络的权重,并返回当前损失:
>>>     def update(self, y_predict, y_target):
 ...         """
 ...         Update the weights of the DQN given a training sample
 ...         @param y_predict:
 ...         @param y_target:
 ...         @return:
 ...         """
 ...         loss = self.criterion(y_predict, y_target)
 ...         self.optimizer.zero_grad()
 ...         loss.backward()
 ...         self.optimizer.step()
 ...         return loss
  1. DQN类的最后部分是replay方法,它在给定一系列过去经验时执行经验重播:
>>>     def replay(self, memory, replay_size, gamma):
 ...         """
 ...         Experience replay
 ...         @param memory: a list of experience
 ...         @param replay_size: the number of samples we 
                 use to update the model each time
 ...         @param gamma: the discount factor
 ...         @return: the loss
 ...         """
 ...         if len(memory) >= replay_size:
 ...             replay_data = random.sample(memory, replay_size)
 ...             state_batch, action_batch, next_state_batch, 
                     reward_batch, done_batch = zip(*replay_data)
 ...             state_batch = torch.cat( tuple(state for state in state_batch))
 ...             next_state_batch = torch.cat(    
                         tuple(state for state in next_state_batch))
 ...             q_values_batch = self.predict(state_batch)
 ...             q_values_next_batch = 
                         self.predict(next_state_batch)
 ...             reward_batch = torch.from_numpy(np.array( reward_batch, dtype=np.float32)[:, None])
 ...             action_batch = torch.from_numpy(
 ...                 np.array([[1, 0] if action == 0 else [0, 1] 
                     for action in action_batch], dtype=np.float32))
 ...             q_value = torch.sum( q_values_batch * action_batch, dim=1)
 ...             td_targets = torch.cat(
 ...             tuple(reward if terminal else reward + 
                         gamma * torch.max(prediction) for
                         reward, terminal, prediction
 ...                 in zip(reward_batch, done_batch, 
                         q_values_next_batch)))
 ...             loss = self.update(q_value, td_targets)
 ...             return loss

这就是 DQN 类的全部内容。在下一个示例中,我们将对 DQN 模型进行若干次迭代的训练。

工作原理...

步骤 2中,我们组装了基于 CNN 的 DQN 的骨干部分。它有三个具有不同配置的卷积层。每个卷积层后面跟着一个 ReLU 激活函数。然后将最后一个卷积层的特征图展*,并输入到一个具有 512 个节点的全连接隐藏层,然后是输出层。

注意,我们还设置了权重的初始随机值界限和零偏置,以便模型更容易收敛。

步骤 6是使用经验回放进行逐步训练。如果我们有足够的经验,我们会随机选择一个大小为replay_size的经验集合进行训练。然后,我们将每个经验转换为一个训练样本,该样本由给定输入状态的预测值和输出目标值组成。目标值计算如下:

  • 使用奖励和新的 Q 值更新动作的目标 Q 值,如下所示:![]

  • 如果是终端状态,则目标 Q 值更新为r

最后,我们使用选定的训练样本批次来更新神经网络。

训练和调整网络

在这个示例中,我们将训练 DQN 模型来玩 Flappy Bird。

在训练的每个步骤中,我们根据 epsilon-greedy 策略采取一个动作:在一定概率(epsilon)下,我们会随机采取一个动作,例如拍打或不拍打;否则,我们选择具有最高值的动作。我们还调整 epsilon 的值以便在 DQN 模型刚开始时更多地进行探索,在模型变得更加成熟时更多地进行利用。

正如我们所见,每一步观察的观察是屏幕的二维图像。我们需要将观察图像转换为状态。仅使用一步中的一个图像将无法提供足够的信息来指导代理程序如何反应。因此,我们使用四个相邻步骤的图像来形成一个状态。我们首先将图像重新形状为预期大小,然后将当前帧的图像与前三个帧的图像连接起来。

如何做...

我们按以下方式训练 DQN 模型:

  1. 导入必要的模块:
>>> import random
>>> import torch
>>> from collections import deque
  1. 我们从开发ε-greedy 策略开始:
>>> def gen_epsilon_greedy_policy(estimator, epsilon, n_action):
 ...     def policy_function(state):
 ...         if random.random() < epsilon:
 ...             return random.randint(0, n_action - 1)
 ...         else:
 ...             q_values = estimator.predict(state)
 ...             return torch.argmax(q_values).item()
 ...     return policy_function
  1. 我们指定预处理图像的大小、批处理大小、学习率、γ值、动作数量、初始和最终ε值、迭代次数以及内存的大小:
>>> image_size = 84
 >>> batch_size = 32
 >>> lr = 1e-6
 >>> gamma = 0.99
 >>> init_epsilon = 0.1
 >>> final_epsilon = 1e-4
 >>> n_iter = 2000000
 >>> memory_size = 50000
 >>> n_action = 2

我们还定期保存训练好的模型,因为这是一个非常漫长的过程:

>>> saved_path = 'trained_models'

不要忘记创建名为trained_models的文件夹。

  1. 我们为实验的可重现性指定随机种子:
>>> torch.manual_seed(123)
  1. 我们相应地创建一个 DQN 模型:
>>> estimator = DQN(n_action)

我们还创建一个内存队列:

>>> memory = deque(maxlen=memory_size)

只要队列中的样本超过 50,000 个,就会附加新样本并移除旧样本。

  1. 接下来,我们初始化一个 Flappy Bird 环境:
>>> env = FlappyBird()

然后我们获取初始图像:

>>> image, reward, is_done = env.next_step(0)
  1. 正如前面提到的,我们应该将原始图像调整为image_size * image_size
>>> import cv2
 >>> import numpy as np
 >>> def pre_processing(image, width, height):
 ...     image = cv2.cvtColor(cv2.resize(image, 
                     (width, height)), cv2.COLOR_BGR2GRAY)
 ...     _, image = cv2.threshold(image, 1, 255, cv2.THRESH_BINARY)
 ...     return image[None, :, :].astype(np.float32)

如果尚未安装cv2包,您可以使用以下命令安装:

pip install opencv-python

让我们相应地预处理图像:

>>> image = pre_processing(image[:screen_width, :int(env.base_y)], image_size, image_size)
  1. 现在,我们通过连接四个图像来构造一个状态。因为现在我们只有第一帧图像,所以我们简单地将其复制四次:
>>> image = torch.from_numpy(image) >>> state = torch.cat(tuple(image for _ in range(4)))[None, :, :, :]
  1. 然后我们对n_iter步骤的训练循环进行操作:
>>> for iter in range(n_iter):
 ...     epsilon = final_epsilon + (n_iter - iter) 
                 * (init_epsilon - final_epsilon) / n_iter
 ...     policy = gen_epsilon_greedy_policy( estimator, epsilon, n_action)
 ...     action = policy(state)
 ...     next_image, reward, is_done = env.next_step(action)
 ...     next_image = pre_processing(next_image[ :screen_width, :int(env.base_y)], image_size, image_size)
 ...     next_image = torch.from_numpy(next_image)
 ...     next_state = torch.cat(( state[0, 1:, :, :], next_image))[None, :, :, :]
 ...     memory.append([state, action, next_state, reward, is_done])
 ...     loss = estimator.replay(memory, batch_size, gamma)
 ...     state = next_state
 ...     print("Iteration: {}/{}, Action: {}, 
                 Loss: {}, Epsilon {}, Reward: {}".format(
 ...             iter + 1, n_iter, action, loss, epsilon, reward))
 ...     if iter+1 % 10000 == 0:
 ...         torch.save(estimator.model, "{}/{}".format( saved_path, iter+1))

在我们运行这部分代码后,我们将看到以下日志:

Iteration: 1/2000000, Action: 0, Loss: None, Epsilon 0.1, Reward: 0.1 Iteration: 2/2000000, Action: 0, Loss: None, Epsilon 0.09999995005000001, Reward: 0.1
 Iteration: 3/2000000, Action: 0, Loss: None, Epsilon 0.0999999001, Reward: 0.1
 Iteration: 4/2000000, Action: 0, Loss: None, Epsilon 0.09999985015, Reward: 0.1
 ...
 ...
 Iteration: 201/2000000, Action: 1, Loss: 0.040504034608602524, Epsilon 0.09999001000000002, Reward: 0.1
 Iteration: 202/2000000, Action: 1, Loss: 0.010011588223278522, Epsilon 0.09998996005, Reward: 0.1
 Iteration: 203/2000000, Action: 1, Loss: 0.07097195833921432, Epsilon 0.09998991010000001, Reward: 0.1
 Iteration: 204/2000000, Action: 1, Loss: 0.040418840944767, Epsilon 0.09998986015000001, Reward: 0.1
 Iteration: 205/2000000, Action: 1, Loss: 0.00999421812593937, Epsilon 0.09998981020000001, Reward: 0.1

训练会花费一些时间。当然,您可以通过 GPU 加速训练。

  1. 最后,我们保存最后训练的模型:
>>> torch.save(estimator.model, "{}/final".format(saved_path))

工作原理...

Step 9中,对于每一个训练步骤,我们执行以下任务:

  • 稍微减小ε,并相应地创建ε-greedy 策略。

  • 使用ε-greedy 策略计算采取的行动。

  • 对生成的图像进行预处理,并通过将其附加到之前三个步骤的图像中来构造新的状态。

  • 记录本步骤的经验,包括状态、行动、下一个状态、接收的奖励以及是否结束。

  • 使用经验重播更新模型。

  • 打印出训练状态并更新状态。

  • 定期保存训练好的模型,以避免从头开始重新训练。

部署模型并玩游戏

现在我们已经训练好了 DQN 模型,让我们将其应用于玩 Flappy Bird 游戏。

使用训练模型玩游戏很简单。我们只需在每一步中采取与最高值相关联的动作。我们将播放几个剧集来查看其表现。不要忘记预处理原始屏幕图像并构造状态。

如何做...

我们在新的剧集上测试 DQN 模型的表现如下:

  1. 我们首先加载最终模型:
>>> model = torch.load("{}/final".format(saved_path))
  1. 我们运行 100 集,并对每一集执行以下操作:
>>> n_episode = 100 >>> for episode in range(n_episode):
 ...     env = FlappyBird()
 ...     image, reward, is_done = env.next_step(0)
 ...     image = pre_processing(image[:screen_width, 
                :int(env.base_y)], image_size, image_size)
 ...     image = torch.from_numpy(image)
 ...     state = torch.cat(tuple(image for _ in range(4)))[ None, :, :, :]
 ...     while True:
 ...         prediction = model(state)[0]
 ...         action = torch.argmax(prediction).item()
 ...         next_image, reward, is_done = env.next_step(action)
 ...         if is_done:
 ...             break
 ...         next_image = pre_processing(next_image[:screen_width, :int(env.base_y)], image_size, image_size)
 ...         next_image = torch.from_numpy(next_image)
 ...         next_state = torch.cat((state[0, 1:, :, :], 
                           next_image))[None, :, :, :]
 ...         state = next_state

希望您能看到类似以下图像的内容,鸟类通过一系列管道:

工作原理是这样的...

第 2 步中,我们对每一集执行以下任务:

  • 初始化 Flappy Bird 环境。

  • 观察初始图像并生成其状态。

  • 使用模型计算给定状态的 Q 值,并选择具有最高 Q 值的动作。

  • 观察新图像以及集数是否结束。

  • 如果集数继续,计算下一个图像的状态并将其分配给当前状态。

  • 重复直到集数结束。

posted @ 2024-07-23 14:53  绝不原创的飞龙  阅读(3)  评论(0编辑  收藏  举报