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

82 阅读1小时+

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/err…,选择您的书籍,点击“勘误提交表格”链接,并填写详细信息。

盗版:如果您在互联网上发现我们作品的任何形式的非法复制,请向我们提供位置地址或网站名称,我们将不胜感激。请通过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/distributio…),用于数据科学和机器学习。我们将使用 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

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