Python-深度强化学习第二版-全-

Python 深度强化学习第二版(全)

原文:annas-archive.org/md5/223c54d54a75689756f5c3d122685371

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

近年来,强化学习算法的质量和数量显著提升,本书第二版《Python 强化学习实战》已经被重塑为一本充满实例的指南,帮助读者学习最先进的强化学习RL)和深度强化学习算法,使用 TensorFlow 2 和 OpenAI Gym 工具包。

除了探讨强化学习(RL)的基础知识和基础概念,如贝尔曼方程、马尔可夫决策过程和动态规划外,本第二版深入探讨了基于价值、基于策略和演员-评论员(actor-critic)强化学习方法的全貌。书中详细介绍了如 DQN、TRPO、PPO、ACKTR、DDPG、TD3 和 SAC 等最先进的算法,揭示了背后的数学原理,并通过简单的代码示例演示了实现过程。

本书新增了几章,专门介绍了新的强化学习技术,包括分布式强化学习、模仿学习、逆向强化学习和元强化学习。你将学习如何利用 Stable Baselines(一种 OpenAI 基准库的改进版)来轻松实现流行的强化学习算法。本书最后将概述一些有前景的研究方法,如元学习和想象增强代理(imagination augmented agents)。

本书适合谁阅读

如果你是一个对神经网络经验较少或没有经验的机器学习开发者,并且有兴趣了解人工智能、从零开始学习强化学习,那么本书适合你。需要具备一些线性代数、微积分和 Python 的基本知识,若有 TensorFlow 的经验会更有帮助。

本书内容

第一章强化学习基础,帮助你建立强化学习概念的坚实基础。我们将学习强化学习的关键要素、马尔可夫决策过程,以及几个重要的基础概念,如动作空间、策略、回合、价值函数和 Q 函数。章节末我们还将了解一些强化学习的有趣应用,并探讨在强化学习中常用的关键术语和术语。

第二章Gym 工具包指南,提供了 OpenAI Gym 工具包的完整指南。通过实际实现,我们将详细了解 Gym 提供的几种有趣的环境。我们将从这一章开始动手实践强化学习,通过 Gym 实现一些基本的强化学习概念。

第三章贝尔曼方程与动态规划,将帮助我们通过大量数学推导详细理解贝尔曼方程。接下来,我们将学习两种有趣的经典强化学习算法——价值迭代和策略迭代方法,利用这些方法可以找到最优策略。我们还将看到如何通过实现价值迭代和策略迭代方法来解决 Frozen Lake 问题。

第四章蒙特卡罗方法,解释了无模型方法——蒙特卡罗方法。我们将了解什么是预测和控制任务,然后详细研究蒙特卡罗预测方法和蒙特卡罗控制方法。接下来,我们将使用 Gym 工具包实现蒙特卡罗方法来解决二十一点游戏。

第五章理解时间差分学习,讲解了一种非常流行且广泛使用的无模型方法——时间差分TD)学习。首先,我们将详细了解 TD 预测方法如何工作,然后我们将深入研究基于策略的 TD 控制方法——SARSA,以及基于行为的 TD 控制方法——Q 学习。我们还将实现 TD 控制方法,使用 Gym 来解决冰湖问题。

第六章案例研究——多臂老丨虎丨机问题,讲解了强化学习中的经典问题之一——多臂老丨虎丨机MAB)问题。我们将从理解 MAB 问题开始,然后学习几种探索策略,如 ε-贪婪、软最大探索、上置信界和汤普森采样方法,以详细解决 MAB 问题。

第七章深度学习基础,帮助我们建立深度学习的坚实基础。我们将从理解人工神经网络的工作原理开始。接着,我们将学习几种有趣的深度学习算法,如递归神经网络、LSTM 网络、卷积神经网络和生成对抗网络。

第八章TensorFlow 入门,讲解了一个非常流行的深度学习库——TensorFlow。我们将通过实现一个神经网络来识别手写数字,了解如何使用 TensorFlow。接下来,我们将学习如何使用 TensorFlow 执行几种数学操作。之后,我们将学习 TensorFlow 2.0,并了解它与以前版本的区别。

第九章深度 Q 网络及其变种,帮助我们启动深度强化学习之旅。我们将了解一种非常流行的深度强化学习算法——深度 Q 网络DQN)。我们将逐步理解 DQN 的工作原理,并进行详细的数学推导。我们还将实现一个 DQN 来玩 Atari 游戏。接下来,我们将探讨 DQN 的几个有趣变种,包括双重 DQN、对抗 DQN、带优先经验回放的 DQN 和 DRQN。

第十章策略梯度方法,介绍了策略梯度方法。我们将理解策略梯度方法如何工作,并进行详细推导。接下来,我们将学习几种方差减少方法,如带有回报目标的策略梯度和带有基准的策略梯度。我们还将理解如何使用策略梯度训练代理来完成平衡杆任务。

第十一章演员-评论家方法——A2C 和 A3C,介绍了几种有趣的演员-评论家方法,如优势演员-评论家和异步优势演员-评论家。我们将详细学习这些演员-评论家方法的工作原理,然后我们将实现它们来进行山地车爬坡任务,使用 OpenAI Gym。

第十二章学习 DDPG、TD3 和 SAC,涵盖了最先进的深度强化学习算法,如深度确定性策略梯度、双延迟 DDPG 和软演员方法,并进行了逐步推导。我们还将学习如何实现 DDPG 算法来执行倒立摆摆动任务,使用 Gym。

第十三章TRPO、PPO 和 ACKTR 方法,介绍了几种流行的策略梯度方法,如 TRPO 和 PPO。我们将一步步深入探讨 TRPO 和 PPO 背后的数学原理,并理解 TRPO 和 PPO 如何帮助代理找到最优策略。接下来,我们将学习如何实现 PPO 来执行倒立摆摆动任务。最后,我们将详细了解一种名为使用 Kronecker 分解信任区域的演员-评论家方法。

第十四章分布式强化学习,介绍了分布式强化学习算法。我们将从理解什么是分布式强化学习开始。然后,我们将探索一些有趣的分布式强化学习算法,如分类 DQN、分位回归 DQN 和分布式分布式 DDPG。

第十五章模仿学习与反向强化学习,解释了模仿学习和反向强化学习算法。首先,我们将详细了解如何进行监督模仿学习、DAgger 和从示范中学习深度 Q 学习。接下来,我们将学习最大熵反向强化学习。最后,在本章的结尾,我们将学习生成对抗模仿学习。

第十六章使用 Stable Baselines 的深度强化学习,帮助我们了解如何使用一个名为 Stable Baselines 的库来实现深度强化学习算法。我们将通过实现几种有趣的深度强化学习算法,如 DQN、A2C、DDPG、TRPO 和 PPO,详细学习什么是 Stable Baselines 以及如何使用它。

第十七章强化学习前沿,详细介绍了强化学习中的几个有趣领域,如元强化学习、层次强化学习和想象增强代理。

要充分利用本书

您需要以下软件来学习本书:

  • Anaconda

  • Python

  • 任何网络浏览器

下载示例代码文件

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

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

  1. 登录或注册www.packtpub.com

  2. 选择支持标签。

  3. 点击代码下载与勘误

  4. 搜索框中输入书名并按照屏幕上的指示操作。

文件下载完成后,请确保使用以下最新版本解压或提取文件夹:

  • WinRAR / 7-Zip for Windows

  • Zipeg / iZip / UnRarX for Mac

  • 7-Zip / PeaZip for Linux

本书的代码包也托管在 GitHub 上,地址为:github.com/PacktPublishing/Deep-Reinforcement-Learning-with-Python。我们还提供了来自我们丰富书籍和视频目录中的其他代码包,地址为:github.com/PacktPublishing/。快去看看吧!

下载彩色图像

我们还提供了一份包含本书中使用的截图/图表的彩色图像的 PDF 文件。你可以在此下载:static.packt-cdn.com/downloads/9781839210686_ColorImages.pdf

使用的约定

本书中使用了多种文本约定。

CodeInText:表示文本中的代码单词、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 用户名。例如:“epsilon_greedy计算最佳策略。”

一段代码如下设置:

def epsilon_greedy(epsilon):
    if np.random.uniform(0,1) < epsilon:
        return env.action_space.sample()
    else:
        return np.argmax(Q) 

当我们希望引起你对代码块中特定部分的注意时,相关的行或项目会被高亮显示:

def epsilon_greedy(epsilon):
**if** **np.random.uniform(****0****,****1****) < epsilon:**
        return env.action_space.sample()
    else:
        return np.argmax(Q) 

任何命令行输入或输出如下所示:

source activate universe 

粗体:表示新术语、重要单词,或者你在屏幕上看到的词汇,例如在菜单或对话框中,也会像这样出现在文本中。例如:“马尔可夫奖励过程MRP)是带有奖励函数的马尔可夫链的扩展。”

警告或重要提示如下所示。

提示和技巧如下所示。

联系我们

我们始终欢迎读者的反馈。

一般反馈:请发送电子邮件至feedback@packtpub.com,并在邮件主题中注明书名。如果你对本书的任何方面有疑问,请通过questions@packtpub.com与我们联系。

勘误:尽管我们已尽力确保内容的准确性,但错误仍然可能发生。如果你在本书中发现错误,我们将非常感激你能报告给我们。请访问www.packtpub.com/submit-errata,选择你的书籍,点击勘误提交表单链接,并输入相关信息。

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

如果你有兴趣成为作者:如果你在某个领域有专长并且有意撰写或贡献书籍内容,请访问authors.packtpub.com

评论

请留下评论。当您阅读并使用本书后,为什么不在您购买书籍的网站上留下评论呢?潜在的读者可以查看并参考您的公正意见来做出购买决定,我们 Packt 也可以了解您对我们产品的看法,而我们的作者可以看到您对他们书籍的反馈。谢谢!

如需了解更多关于 Packt 的信息,请访问 packtpub.com

第一章:强化学习的基本原理

强化学习RL)是机器学习ML)的一个领域。与其他机器学习范式(如监督学习和无监督学习)不同,强化学习通过与环境互动,以试验和错误的方式进行工作。

强化学习是人工智能领域最活跃的研究方向之一,被认为它将使我们更接近实现人工通用智能。强化学习在过去几年中迅速发展,应用范围广泛,从构建推荐系统到自动驾驶汽车都有涉及。这个发展迅速的主要原因是深度强化学习的出现,它是深度学习与强化学习的结合。随着新型强化学习算法和库的涌现,强化学习显然是机器学习领域最具前景的方向之一。

在本章中,我们将通过探讨强化学习中的几个重要且基本的概念,建立强化学习的坚实基础。

本章将涵盖以下内容:

  • 强化学习的关键元素

  • 强化学习的基本概念

  • 强化学习算法

  • 强化学习与其他机器学习范式的区别

  • 马尔可夫决策过程

  • 强化学习的基本概念

  • 强化学习的应用

  • 强化学习术语

我们将从定义 强化学习的关键元素 开始这一章节。这将有助于解释 强化学习的基本概念

强化学习的关键元素

让我们先来理解强化学习中的一些关键元素。

智能体

智能体是一个学习做出智能决策的软件程序。我们可以说智能体是强化学习环境中的学习者。例如,棋手可以视为一个智能体,因为他学习如何做出最佳的棋步(决策)以赢得比赛。同样,超级马里奥游戏中的马里奥也可以被视为智能体,因为马里奥在游戏中探索并学习如何做出最佳的行动。

环境

环境是智能体的世界。智能体存在于环境中。例如,在我们的棋局示例中,棋盘就称为环境,因为棋手(智能体)在棋盘(环境)内学习如何下棋。同样,在超级马里奥游戏中,马里奥的世界被称为环境。

状态与动作

状态是智能体能够处于的环境中的某个位置或时刻。我们了解到,智能体始终存在于环境中,并且环境中有许多位置,智能体可以停留在这些位置上,这些位置被称为状态。例如,在我们的棋局示例中,棋盘上的每个位置都称为状态。状态通常用 s 来表示。

智能体通过与环境互动,从一个状态移动到另一个状态,执行一个动作。在棋局环境中,动作是玩家(智能体)执行的棋步。动作通常用 a 来表示。

奖励

我们学到,代理者通过执行动作与环境交互,并从一个状态移动到另一个状态。根据动作,代理者会得到一个奖励。奖励只是一个数值,比如,执行一个好的动作得到+1,执行一个坏的动作得到-1。我们如何判断一个动作是好还是坏?

在我们的象棋游戏例子中,如果代理者走了一步吃掉对手的棋子,那么这被认为是一个好动作,代理者会得到积极奖励。同样地,如果代理者的一步棋导致对手吃掉代理者的棋子,那么这被认为是一个坏动作,代理者会得到负面奖励。奖励用r来表示。

强化学习的基本思想

让我们从一个类比开始。假设我们正在教一只狗(代理者)接住一个球。我们不是明确地教狗去接球,而是只是把球扔出去,每次狗接住球时我们给它一块饼干(奖励)。如果狗没有接住球,我们就不给它饼干。因此,狗将会找出是哪个动作让它得到了饼干并重复那个动作。这样,狗就会理解接住球让它得到了饼干,并会尝试重复接住球。通过这种方式,狗将学会接住球,并且会努力最大化它可以得到的饼干数量。

类似地,在强化学习设置中,我们不会教代理者要做什么或如何做;相反,我们会为代理者的每个动作给予奖励。当代理者执行一个好的动作时,我们会给予积极奖励,这样代理者就能理解它已经执行了一个好的动作,并会重复这个动作。如果代理者执行的动作是坏的,那么我们会给予负面奖励,这样代理者就能理解它执行了一个坏的动作,并且不会重复这个动作。

因此,强化学习可以被视为一个试错学习过程,代理者尝试不同的动作并学习到给予积极奖励的好动作。

在狗的类比中,狗代表着代理者,当狗接住球时给它一块饼干是积极奖励,不给饼干则是负面奖励。因此,狗(代理者)探索不同的动作,即接住球和不接住球,并且理解接住球是一个好的动作,因为这会给狗带来积极奖励(得到一块饼干)。

让我们通过一个更简单的例子进一步探讨强化学习的概念。假设我们想教一个机器人(代理者)在不撞到山的情况下行走,如图 1.1 所示:

图 1.1: 机器人行走

我们不会明确教机器人避免朝着山的方向走。相反,如果机器人撞到山并被卡住,我们会给机器人一个负奖励,例如-1。这样,机器人就能明白撞到山是错误的行为,它将不会再重复这一行为:

图 1.2:机器人撞到山脉

同样,当机器人朝着正确的方向走且没有撞到山时,我们会给机器人一个正奖励,例如+1。这样,机器人就会明白不撞到山是一个好的动作,它将会重复这一动作:

图 1.3:机器人避开山脉

因此,在强化学习的环境中,智能体探索不同的动作,并根据获得的奖励学习最佳动作。

现在我们对强化学习的基本原理有了一个大致的了解,接下来的章节中,我们将进一步深入,学习强化学习中涉及的重要概念。

强化学习算法

一个典型的强化学习算法的步骤如下:

  1. 首先,智能体通过执行一个动作与环境进行交互。

  2. 通过执行一个动作,智能体从一个状态转移到另一个状态。

  3. 然后,智能体会根据它所执行的动作获得一个奖励。

  4. 基于奖励,智能体将理解该行为是好是坏。

  5. 如果某个动作是好的,也就是说,智能体收到了正奖励,那么智能体会倾向于执行该动作;否则,智能体会尝试其他动作,寻找能获得正奖励的行动。

强化学习(RL)基本上是一个试错学习过程。现在,让我们回顾一下我们的国际象棋游戏示例。智能体(软件程序)就是下棋的玩家。因此,智能体通过执行一个动作(走棋)与环境(棋盘)进行交互。如果智能体因为某个动作获得正奖励,那么它会倾向于执行该动作;否则,它会寻找其他能够获得正奖励的动作。

最终,智能体的目标是最大化它所获得的奖励。如果智能体得到好的奖励,那就意味着它做出了好的动作。如果智能体做出了好的动作,那就意味着它能够赢得比赛。因此,智能体通过最大化奖励来学习如何赢得比赛。

网格世界中的强化学习智能体

让我们通过另一个简单的例子来进一步巩固对强化学习的理解。考虑以下的网格世界环境:

图 1.4:网格世界环境

环境中的位置AI被称为环境的状态。智能体的目标是从状态A出发,达到状态I,并且不经过阴影状态(BCGH)。因此,为了实现目标,每当智能体访问阴影状态时,我们会给予一个负奖励(例如-1),而当它访问非阴影状态时,我们会给予一个正奖励(例如+1)。环境中的动作包括向上向下向右向左。智能体可以执行这四个动作中的任何一个,以从状态A到达状态I

当智能体第一次与环境互动时(第一轮),它不太可能在每个状态中执行正确的动作,因此它会得到负奖励。也就是说,在第一轮中,智能体在每个状态中执行了一个随机动作,这可能导致智能体收到负奖励。但经过一系列迭代后,智能体通过获得的奖励学会在每个状态中执行正确的动作,从而帮助它实现目标。让我们详细探讨这个过程。

第一轮

正如我们所学,在第一轮中,智能体在每个状态中执行了一个随机动作。例如,看看下面的图。在第一轮中,智能体从状态A移动,达到了新状态B。但是由于B是一个阴影状态,智能体将收到负奖励,因此智能体会明白在状态A时,向右并不是一个好的动作。当它下次访问状态A时,它会尝试不同的动作,而不是向移动:

图 1.5:第一轮中智能体执行的动作

正如图 1.5所示,从状态B,智能体向移动,达到了新状态E。由于E是一个非阴影状态,智能体将获得正奖励,因此智能体明白了从状态B向下移动是一个好的动作。

从状态E,智能体向移动,达到了状态F。由于F是一个非阴影状态,智能体获得了正奖励,它会明白从状态E移动是一个好的动作。从状态F,智能体向移动,达到了目标状态I并获得了正奖励,因此智能体会明白从状态F向下移动是一个好的动作。

第二轮

在第二轮中,智能体从状态A出发,不再选择向右移动,而是尝试了一种不同的动作,因为智能体在上一轮学到,向右在状态A并不是一个好的动作。

因此,正如图 1.6所示,在这一轮中,智能体从状态A移动,达到了状态D。由于D是一个非阴影状态,智能体获得了正奖励,现在智能体明白了在状态A时,向下是一个好的动作:

图 1.6:第二轮中智能体执行的动作

如前图所示,从状态D,智能体向移动并到达状态G。但由于G是一个阴影状态,智能体将获得负奖励,因此智能体会明白,在状态D时向移动不是一个好的动作,当它下次访问状态D时,它会尝试不同的动作,而不是向移动。

从状态G,智能体向移动并到达状态H。由于H是一个阴影状态,智能体会收到负奖励,并理解在状态G时向移动不是一个好的动作。

从状态H,智能体向移动并到达目标状态I,并获得正奖励,因此智能体会明白,在状态H时向移动是一个好的动作。

迭代 3

在第三次迭代中,智能体从状态A移动,因为在第二次迭代中,我们的智能体学到在状态A时向移动是一个好的动作。因此,智能体从状态A移动并到达下一个状态D,如图 1.7所示:

图 1.7:智能体在迭代 3 中采取的动作

现在,从状态D,智能体尝试了一个不同的动作,而不是向移动,因为在第二次迭代中,我们的智能体已经学到在状态D时向移动不是一个好的动作。所以,在这一迭代中,智能体从状态D移动并到达状态E

从状态E,智能体向移动,因为智能体已经在第一次迭代中学到在状态E时向移动是一个好的动作,并到达状态F

现在,从状态F,智能体向移动,因为智能体在第一次迭代中学到在状态F时向移动是一个好的动作,并到达目标状态I

图 1.8 显示了第三次迭代的结果:

图 1.8:智能体成功到达目标状态,未经过阴影状态

如我们所见,我们的智能体已经成功学会了从状态A到达目标状态I,且没有经过阴影状态,这是基于奖励的学习结果。

通过这种方式,智能体将在每个状态中尝试不同的动作,并根据获得的奖励理解某个动作是好还是坏。智能体的目标是最大化奖励。因此,智能体总是会尝试执行能够获得正奖励的好动作,当智能体在每个状态中执行好动作时,最终会帮助它实现目标。

注意,这些迭代在强化学习术语中被称为“回合”。我们将在本章稍后部分详细学习回合。

RL 与其他机器学习范式的区别

我们可以将机器学习(ML)分为三种类型:

  • 监督学习

  • 无监督学习

  • 强化学习(RL)

在监督学习中,机器通过训练数据进行学习。训练数据由输入和输出的标签对组成。因此,我们使用训练数据训练模型(智能体),使得模型能够将其学习泛化到新的、未见过的数据上。这叫做监督学习,因为训练数据充当了监督者的角色,因为它有标签对输入和输出,并指导模型完成给定的任务。

现在,让我们通过一个例子来理解监督学习和强化学习之间的区别。考虑我们在本章前面讨论的狗的类比。在监督学习中,为了教狗抓球,我们将通过指定左转、右转、向前走七步、抓住球等方式明确地教它,形成训练数据。但是在强化学习中,我们只会扔一个球,每当狗抓住球时,我们就给它一块饼干(奖励)。因此,狗会通过尽量获取更多饼干(奖励)来学习抓球。

让我们再考虑一个例子。假设我们想用监督学习训练模型下棋。在这种情况下,我们将拥有训练数据,数据中包括玩家在每种状态下可以进行的所有棋步,并且有标签指示每步是否是好棋。然后,我们训练模型从这些训练数据中学习,而在强化学习(RL)中,我们的智能体不会获得任何形式的训练数据;相反,我们只会为智能体的每个行动给予奖励。接着,智能体通过与环境互动来学习,并根据获得的奖励选择其行动。

类似于监督学习,在无监督学习中,我们也根据训练数据训练模型(智能体)。但在无监督学习中,训练数据不包含任何标签;也就是说,它仅包含输入而没有输出。无监督学习的目标是确定输入中的隐藏模式。有一种常见的误解认为强化学习是一种无监督学习,但实际上并不是。在无监督学习中,模型学习隐藏的结构,而在强化学习中,模型通过最大化奖励来学习。

例如,考虑一个电影推荐系统。假设我们想给用户推荐一部新电影。在无监督学习中,模型(智能体)会根据用户(或与用户有相似档案的用户)之前观看过的电影,找出与之相似的电影,并推荐新电影给用户。

在强化学习中,智能体不断接收来自用户的反馈。这些反馈代表奖励(奖励可以是用户对其看过的电影评分、看电影的时间、看预告片的时间等等)。基于这些奖励,RL 智能体将理解用户的电影偏好,然后根据这些信息推荐新电影。

由于强化学习代理是通过奖励来学习的,它能够理解用户的电影偏好是否发生变化,并根据用户变化后的电影偏好动态地推荐新电影。

因此,我们可以说,在监督学习和无监督学习中,模型(代理)是基于给定的训练数据集进行学习的,而在强化学习中,代理是通过直接与环境互动来学习的。因此,强化学习本质上是代理与其环境之间的互动。

在深入学习强化学习的基本概念之前,我们将介绍一个常用的过程,帮助在强化学习环境中做出决策。

马尔可夫决策过程

马尔可夫决策过程MDP)提供了解决强化学习问题的数学框架。几乎所有的强化学习问题都可以被建模为一个 MDP。MDP 广泛应用于解决各种优化问题。在本节中,我们将了解 MDP 是什么以及它如何在强化学习中被使用。

要理解 MDP,首先我们需要了解马尔可夫性质和马尔可夫链。

马尔可夫性质和马尔可夫链

马尔可夫性质声明,未来只依赖于现在,而不依赖于过去。马尔可夫链,也叫做马尔可夫过程,由一系列严格遵循马尔可夫性质的状态组成;也就是说,马尔可夫链是一个仅依赖当前状态来预测下一个状态的概率模型,而不依赖于之前的状态。换句话说,未来是条件独立于过去的。

例如,如果我们想预测天气,并且我们知道当前状态是“多云”,我们可以预测下一个状态可能是“雨天”。我们仅通过考虑当前状态(多云),而不是考虑之前的状态(如晴天、风天等),得出下一个状态很可能是“雨天”。

然而,并不是所有过程都符合马尔可夫性质。例如,掷骰子(下一个状态)与骰子上显示的前一个数字(当前状态)无关。

从一个状态过渡到另一个状态叫做转移,其概率称为转移概率。我们用 来表示转移概率。它表示从状态 s 过渡到下一个状态 的概率。假设我们的马尔可夫链中有三个状态(多云、雨天和风天)。然后,我们可以通过一个叫做马尔可夫表的表格来表示从一个状态到另一个状态的转移概率,如表 1.1所示:

表 1.1:一个马尔可夫表的示例

表 1.1中,我们可以观察到:

  • 从状态“多云”出发,我们以 70%的概率过渡到“雨天”状态,并以 30%的概率过渡到“风天”状态。

  • 从状态“雨天”出发,我们以 80%的概率过渡到相同的“雨天”状态,并以 20%的概率过渡到“多云”状态。

  • 从状态“风天”出发,我们以 100%的概率过渡到“雨天”状态。

我们还可以将马尔可夫链的转移信息表示为状态图,如 图 1.9 所示:

图 1.9:马尔可夫链的状态图

我们还可以将转移概率表示为一个矩阵,称为转移矩阵,如 图 1.10 所示:

图 1.10:转移矩阵

因此,最后我们可以说,马尔可夫链或马尔可夫过程由一组状态及其转移概率组成。

马尔可夫奖励过程

马尔可夫奖励过程 (MRP) 是马尔可夫链的扩展,增加了奖励函数。也就是说,我们已经知道马尔可夫链包括状态和转移概率。MRP 包括状态、转移概率以及奖励函数。

奖励函数告诉我们在每个状态中获得的奖励。例如,基于我们之前的天气示例,奖励函数告诉我们在“多云”状态下获得的奖励,在“有风”状态下获得的奖励,依此类推。奖励函数通常表示为 R(s)。

因此,MRP 包括状态 s、一个转移概率 和一个奖励函数 R(s)。

马尔可夫决策过程

马尔可夫决策过程 (MDP) 是包含动作的 MRP 的扩展。也就是说,我们已经知道 MRP 包括状态、转移概率和奖励函数。MDP 包括状态、转移概率、奖励函数,以及动作。我们了解到,马尔可夫性质表明下一个状态仅依赖于当前状态,而不依赖于之前的状态。那么,马尔可夫性质是否适用于 RL 环境?是的!在 RL 环境中,智能体仅基于当前状态做出决策,而不依赖于过去的状态。因此,我们可以将 RL 环境建模为一个 MDP。

让我们通过一个例子来理解这个概念。在任何环境下,我们都可以使用 MDP 来建模环境。例如,考虑我们之前学习过的网格世界环境。图 1.11 显示了网格世界环境,智能体的目标是从状态 A 到达状态 I,而不经过阴影状态:

图 1.11:网格世界环境

智能体仅根据当前所在的状态做出决策(行动),而不依赖于过去的状态。因此,我们可以将环境建模为一个 MDP。我们知道,MDP 包括状态、动作、转移概率和奖励函数。现在,让我们了解这如何与 RL 环境相关:

状态 – 环境中存在的状态集合。因此,在网格世界环境中,我们有状态 AI

动作 - 代理在每个状态下可以执行的一组动作。代理执行某个动作,并从一个状态移动到另一个状态。因此,在网格世界环境中,动作集包括updownleftright

转移概率 - 转移概率用表示。它表示在执行动作a时,从状态s转移到下一个状态的概率。如果你观察,在 MRP 中,转移概率仅是,即从状态s转移到状态的概率,并不包括动作。但在 MDP 中,我们包含了动作,因此转移概率用表示。

例如,在我们的网格世界环境中,假设在执行right动作时,从A状态转移到B状态的转移概率为 100%。这可以表示为P(B|A, right) = 1.0。我们还可以在状态图中查看此情况,如图 1.12所示:

图 1.12:从 A 到 B 执行右移的转移概率

假设我们的代理处于C状态,并且在执行down动作时,从C状态转换到F状态的转移概率为 90%,则可以表示为P(F|C, down) = 0.9。我们还可以在状态图中查看此情况,如图 1.13所示:

图 1.13:从 C 到 F 执行下移的转移概率

奖励函数 - 奖励函数用表示。它表示我们的代理在执行动作a时,从状态s转换到状态时获得的奖励。

假设在执行right动作时,从A状态转换到B状态时,我们获得的奖励是-1,则可以表示为R(A, right, B) = -1。我们还可以在状态图中查看此情况,如图 1.14所示:

图 1.14:从 A 到 B 执行右移的奖励

假设我们的代理处于C状态,并且假设在执行down动作时,从C状态转换到F状态时,我们获得的奖励是+1,则可以表示为R(C, down, F) = +1。我们还可以在状态图中查看此情况,如图 1.15所示:

图 1.15:从 C 到 F 执行下移的奖励

因此,一个强化学习环境可以表示为一个具有状态、动作、转移概率和奖励函数的马尔可夫决策过程(MDP)。但是,等等!为什么要用 MDP 来表示强化学习环境呢?一旦我们将环境建模为 MDP,就可以轻松解决强化学习问题。例如,一旦我们用 MDP 建模了我们的网格世界环境,那么就可以轻松地找到如何从状态 A 到达目标状态 I,而不经过阴影状态。我们将在接下来的章节中学习更多内容。接下来,我们将探讨更多强化学习的基本概念。

强化学习的基本概念

在这一部分,我们将学习几个重要的强化学习基本概念。

数学基础

在继续之前,让我们快速回顾一下高中时学习的期望值,因为我们将在本书中多次涉及期望值。

期望值

假设我们有一个变量 X,它的取值为 1, 2, 3, 4, 5, 6。为了计算 X 的平均值,我们只需将 X 的所有值相加,然后除以 X 的取值个数。因此,X 的平均值是 (1+2+3+4+5+6)/6 = 3.5。

现在,假设 X 是一个随机变量。随机变量的取值基于随机实验,例如掷骰子、抛硬币等。随机变量会以一定的概率取不同的值。假设我们正在掷一颗公平的骰子,那么可能的结果(X)是 1、2、3、4、5 和 6,每个结果出现的概率都是 1/6,如 表 1.2 所示:

表 1.2:掷骰子的概率

我们如何计算随机变量 X 的平均值呢?由于每个值都有发生的概率,我们不能直接取平均值。所以,我们要计算加权平均值,也就是将 X 的各个值与它们对应的概率相乘后求和,这就是期望值。随机变量 X 的期望值可以定义为:

因此,随机变量 X 的期望值是 E(X) = 1(1/6) + 2(1/6) + 3(1/6) + 4(1/6) + 5(1/6) + 6(1/6) = 3.5。

期望值也称为期望值。因此,随机变量 X 的期望值为 3.5。所以,当我们提到期望或随机变量的期望值时,本质上是指加权平均值。

现在,我们将探讨一个随机变量函数的期望值。设 ,然后我们可以写成:

表 1.3:掷骰子的概率

随机变量函数的期望值可以通过以下方式计算:

因此,f(X) 的期望值为 E(f(X)) = 1(1/6) + 4(1/6) + 9(1/6) + 16(1/6) + 25(1/6) + 36(1/6) = 15.1。

动作空间

请考虑 图 1.16 中所示的网格世界环境:

图 1.16:网格世界环境

在上述网格世界环境中,智能体的目标是从状态A出发,达到状态I,并且不经过阴影状态。在每个状态中,智能体可以执行四种动作中的任何一种——向上向下向左向右——来实现目标。环境中所有可能动作的集合被称为动作空间。因此,对于这个网格世界环境,动作空间为[向上向下向左向右]。

我们可以将动作空间分为两种类型:

  • 离散动作空间

  • 连续动作空间

离散动作空间:当我们的动作空间由离散的动作组成时,它被称为离散动作空间。例如,在网格世界环境中,我们的动作空间由四个离散动作组成,即向上、向下、向左、向右,因此它被称为离散动作空间。

连续动作空间:当我们的动作空间由连续的动作组成时,它被称为连续动作空间。例如,假设我们正在训练一个智能体驾驶汽车,那么我们的动作空间将包含几个具有连续值的动作,比如我们需要驾驶汽车的速度、我们需要转动方向盘的角度等。当我们的动作空间由连续动作组成时,这种情况称为连续动作空间。

政策

策略定义了智能体在环境中的行为。策略告诉智能体在每个状态下应该执行什么动作。例如,在网格世界环境中,我们有状态AI,以及四个可能的动作。策略可能告诉智能体在状态A执行向下的动作,在状态D执行向右的动作,依此类推。

为了首次与环境交互,我们初始化一个随机策略,即随机策略告诉智能体在每个状态下执行一个随机动作。因此,在初始迭代中,智能体在每个状态下执行一个随机动作,并根据它获得的奖励来判断该动作是好是坏。经过一系列迭代后,智能体将学会在每个状态下执行能带来正奖励的好动作。因此,我们可以说,通过一系列迭代,智能体将学习到一个能够获得正奖励的好策略。

这个好策略被称为最优策略。最优策略是能让智能体获得好奖励并帮助智能体实现目标的策略。例如,在我们的网格世界环境中,最优策略告诉智能体在每个状态下执行某个动作,使得智能体能够从状态A到达状态I,并且不经过阴影状态。

最优策略如图 1.17所示。正如我们所观察到的,智能体根据最优策略在每个状态下选择动作,并从起始状态A到达终止状态I,而不会经过阴影状态:

图 1.17:网格世界环境中的最优策略

因此,最优策略告诉智能体在每个状态下执行正确的动作,以便智能体可以获得良好的奖励。

策略可以分为以下几种:

  • 一种确定性策略

  • 一种随机策略

确定性策略

我们刚才讨论的策略叫做确定性策略。确定性策略告诉智能体在某个状态下执行一个特定的动作。因此,确定性策略将状态映射到一个特定的动作,通常用 表示。给定某个时刻 t 的状态 s,确定性策略告诉智能体执行特定的动作 a。它可以表示为:

例如,考虑我们网格世界的例子。给定状态 A,确定性策略 告诉智能体执行动作 。这可以表示为:

因此,根据确定性策略,每当智能体访问状态 A 时,它会执行动作

随机策略

与确定性策略不同,随机策略并不直接将一个状态映射到某个特定的动作,而是将状态映射到动作空间上的概率分布。

也就是说,我们了解到,给定一个状态,确定性策略会告诉智能体在该状态下执行一个特定的动作,因此,每次智能体访问该状态时,它总是执行相同的特定动作。而在随机策略中,给定一个状态,随机策略会返回一个关于动作空间的概率分布。因此,智能体每次访问该状态时,不再总是执行相同的动作,而是基于随机策略返回的概率分布执行不同的动作。

让我们通过一个例子来理解这个问题;我们知道,我们的网格世界环境的动作空间包含四个动作,分别是 [上、下、左、右]。给定状态 A,随机策略返回关于动作空间的概率分布 [0.10, 0.70, 0.10, 0.10]。因此,每次智能体访问状态 A 时,它不会每次都选择相同的特定动作,而是有 10% 的概率选择 ,70% 的概率选择 ,10% 的概率选择 ,以及 10% 的概率选择

确定性策略和随机策略之间的区别如 图 1.18 所示。正如我们所观察到的,确定性策略将状态映射到一个特定的动作,而随机策略将状态映射到动作空间上的概率分布:

图 1.18:确定性策略和随机策略之间的区别

因此,随机策略将状态映射到动作空间上的一个概率分布,通常用 表示。假设在时刻 t 有一个状态 s 和一个动作 a,那么我们可以将随机策略表示为:

或者它也可以表示为

我们可以将随机策略分为两种类型:

  • 类别策略

  • 高斯策略

类别策略

当动作空间是离散时,随机策略被称为类别策略。也就是说,当动作空间是离散时,随机策略使用类别概率分布来选择动作。例如,在之前例子中的网格世界环境中,由于环境的动作空间是离散的,我们根据类别概率分布(离散分布)选择动作。正如图 1.19所示,给定状态A,我们根据动作空间上的类别概率分布选择一个动作:

图 1.19:从状态 A 出发的下一步动作概率(离散动作空间)

高斯策略

当我们的动作空间是连续时,随机策略被称为高斯策略。也就是说,当动作空间是连续时,随机策略使用高斯概率分布来选择动作。我们通过一个简单的例子来理解这一点。假设我们正在训练一个智能体驾驶汽车,并且假设在我们的动作空间中有一个连续的动作。让这个动作是汽车的速度,汽车速度的取值范围从 0 到 150 公里每小时。然后,随机策略使用高斯分布在动作空间上选择动作,如图 1.20所示:

图 1.20:高斯分布

在接下来的章节中,我们将更深入地了解高斯策略。

回合

智能体通过执行一些动作与环境进行交互,从初始状态出发,直到最终状态为止。这个从初始状态到最终状态的智能体与环境的交互过程被称为回合。例如,在一款赛车视频游戏中,智能体通过从初始状态(比赛起点)开始,直到最终状态(比赛终点)为止来进行游戏。这被视为一个回合。回合通常也被称为轨迹(智能体所经过的路径),并且表示为!

智能体可以玩任意数量的回合,并且每个回合彼此独立。那么,玩多个回合有什么意义呢?为了学习最优策略,也就是告诉智能体在每个状态下执行正确动作的策略,智能体需要玩多个回合。

例如,假设我们正在玩一款赛车游戏;第一次玩时,可能不会赢得比赛,所以我们会玩几次游戏,逐步了解游戏并发现一些有效的获胜策略。类似地,在第一轮中,智能体可能无法赢得比赛,它会玩多个回合,逐步了解游戏环境,并找出一些有效的获胜策略。

假设我们从时间步长 t = 0 的初始状态开始游戏,并在时间步长 T 时达到最终状态,那么 episode 信息包含了从初始状态到最终状态的智能体与环境交互,例如状态、动作和奖励,即 (s[0], a[0], r[0], s[1], a[1], r[1], …,s[T])。

图 1.21 显示了一个 episode/轨迹的示例:

图 1.21:一个 episode 的示例

让我们通过网格世界环境来加深对 episode 和最优策略的理解。我们了解到,在网格世界环境中,智能体的目标是从初始状态 A 到达最终状态 I,而不经过阴影状态。当智能体访问未阴影状态时,它会获得 +1 奖励;当它访问阴影状态时,它会获得 -1 奖励。

当我们说生成一个 episode 时,它意味着从初始状态到最终状态的过程。智能体使用随机策略生成第一个 episode,探索环境,并在多个 episode 后,学习到最优策略。

Episode 1

图 1.22所示,在第一个 episode 中,智能体使用随机策略,在每个状态中从初始状态到最终状态随机选择动作并观察奖励:

图 1.22:Episode 1

Episode 2

在第二个 episode 中,智能体尝试使用不同的策略来避免在上一个 episode 中获得的负奖励。例如,如我们在上一个 episode 中观察到的,智能体在状态 A 选择了动作 并获得了负奖励,因此在本 episode 中,智能体没有在状态 A 选择动作 ,而是尝试了不同的动作,比如 ,正如 图 1.23 所示:

图 1.23:Episode 2

Episode n

因此,在一系列 episode 后,智能体会学习到最优策略,也就是将智能体从状态 A 带到最终状态 I,并且不经过阴影状态的策略,正如 图 1.24 所示:

图 1.24:Episode n

集合任务与连续任务

一个强化学习任务可以被分类为:

  • 一个集合任务

  • 一个连续任务

集合任务:顾名思义,集合任务是具有终止/最终状态的任务。也就是说,集合任务由多个 episode 组成,因此它们有一个终止状态。例如,在一场赛车游戏中,我们从起点(初始状态)开始,达到终点(终止状态)。

连续任务:与集合任务不同,连续任务不包含任何 episode,因此没有终止状态。例如,个人助理机器人没有终止状态。

Horizon

Horizon 是智能体与环境交互的时间步长。我们可以将 Horizon 分为两类:

  • 有限 Horizon

  • 无限 Horizon

有限时域:如果智能体与环境的交互在某个特定时间步停止,则称之为有限时域。例如,在回合任务中,智能体从时间步 t = 0 的初始状态开始,并在时间步 T 达到最终状态。由于智能体与环境的交互在时间步 T 处停止,因此被视为有限时域。

无限时域:如果智能体与环境的交互永不停止,则称之为无限时域。例如,我们了解到,连续任务没有终止状态。这意味着在连续任务中,智能体与环境的交互将永不停止,因此被视为无限时域。

回报和折扣因子

回报可以定义为智能体在一个回合中获得的奖励之和。回报通常用 RG 来表示。假设智能体从初始状态开始,在时间步 t = 0 处,并在时间步 T 处达到最终状态,那么智能体获得的回报可以表示为:

让我们通过一个例子来理解这一点;考虑轨迹(回合)

图 1.25:轨迹/回合

轨迹的回报是奖励的总和,也就是

因此,我们可以说智能体的目标是最大化回报,也就是最大化在回合中获得的奖励总和(累计奖励)。我们如何最大化回报呢?如果我们在每个状态下都采取正确的行动,就能最大化回报。那么,我们如何在每个状态下执行正确的行动呢?我们可以通过使用最优策略在每个状态下执行正确的行动。这样,我们就能通过最优策略最大化回报。因此,最优策略就是通过在每个状态下采取正确的行动来获得最大回报(奖励总和)的策略。

好的,我们如何为连续任务定义回报呢?我们了解到,连续任务中没有终止状态,因此我们可以将回报定义为奖励的总和,直到无穷大:

但是我们如何最大化一个求和到无穷大的回报呢?我们引入一个新的术语,叫做折扣因子 ,并将回报重新写为:

好吧,那么这个折扣因子!是如何帮助我们的呢?它通过决定我们给未来奖励和即时奖励赋予多少重要性来帮助我们防止回报值达到无穷大。折扣因子的值范围从 0 到 1。当我们将折扣因子设置为小值(接近 0)时,意味着我们更重视即时奖励而非未来奖励。当我们将折扣因子设置为较大值(接近 1)时,意味着我们更重视未来奖励而非即时奖励。让我们通过一个具有不同折扣因子值的例子来理解这一点。

小折扣因子

让我们将折扣因子设置为一个小值,比如 0.2,也就是将!,然后我们可以写成:

从这个方程中,我们可以观察到每个时间步长的奖励都受到折扣因子的加权。随着时间步长的增加,折扣因子(权重)下降,因此未来时间步长的奖励的重要性也会减少。也就是说,从方程中我们可以观察到:

  • 在时间步长 0 时,奖励r[0]受到折扣因子 1 的加权。

  • 在时间步长 1 时,奖励r[1]受到一个大幅降低的折扣因子 0.2 的加权。

  • 在时间步长 2 时,奖励r[2]受到一个大幅降低的折扣因子 0.04 的加权。

如我们所观察到的,随后的时间步长中的折扣因子大幅降低,因此比起未来时间步长的奖励,当前奖励r[0]的重要性被赋予更多。因此,当我们将折扣因子设置为小值时,我们更重视即时奖励而非未来奖励。

大折扣因子

让我们将折扣因子设置为一个较大值,比如 0.9,也就是将,,然后我们可以写成:

从这个方程中,我们可以推断出,随着时间步长的增加,折扣因子(权重)会下降;然而,它并不像前一种情况那样大幅下降,因为这里我们一开始就设定了!。因此,在这种情况下,我们可以说我们更重视未来奖励。也就是说,从方程中我们可以观察到:

  • 在时间步长 0 时,奖励r[0]受到折扣因子 1 的加权。

  • 在时间步长 1 时,奖励r[1]受到一个稍微降低的折扣因子 0.9 的加权。

  • 在时间步长 2 时,奖励r[2]受到一个稍微降低的折扣因子 0.81 的加权。

如我们所观察到的,折扣因子对于随后的时间步长有所下降,但与之前的情况不同,折扣因子的下降幅度并不大。因此,当我们将折扣因子设置为较大值时,我们更重视未来奖励而非即时奖励。

当我们将折扣因子设置为 0 时,会发生什么?

当我们将折扣因子设置为 0 时,即 ,这意味着我们只考虑即时回报 r[0],而不考虑未来时间步获得的回报。因此,当我们将折扣因子设置为 0 时,代理将永远不会学习,因为它只会考虑即时回报 r[0],如这里所示:

正如我们所观察到的,当我们设置 时,我们的回报将仅为即时回报 r[0]。

当我们将折扣因子设置为 1 时会发生什么?

当我们将折扣因子设置为 1 时,即 ,这意味着我们考虑所有未来的回报。因此,当我们将折扣因子设置为 1 时,代理将永远学习,寻找所有未来的回报,这可能导致无限大,如下所示:

正如我们所观察到的,当我们设置 时,我们的回报将是到无限的回报之和。

因此,我们已经了解到,当我们将折扣因子设置为 0 时,代理将永远不会学习,只考虑即时回报,而当我们将折扣因子设置为 1 时,代理将永远学习,寻找导致无限大的未来回报。所以,折扣因子的最佳值介于 0.2 和 0.8 之间。

但问题是,为什么我们应该关心即时和未来的回报呢?我们根据任务的不同,给予即时回报和未来回报不同的权重。在某些任务中,未来的回报比即时回报更为重要,反之亦然。在象棋游戏中,目标是击败对方的国王。如果我们更重视即时回报,例如通过我们的兵击败对方的棋子,那么代理将学会执行这个子目标,而不是学习实际的目标。所以,在这种情况下,我们给予未来回报更大的重要性,而在某些情况下,我们更倾向于即时回报而不是未来回报。如果今天给你巧克力和 13 天后给你巧克力,你更愿意选择哪一种?

在接下来的两个部分中,我们将分析强化学习的两个基本函数。

值函数

值函数,也称为状态值函数,表示状态的值。状态的值是代理从该状态开始,遵循策略 所获得的回报。状态的值或值函数通常用 V(s) 表示,可以表示为:

其中 s[0] = s 表示起始状态是 s。状态的值称为状态值。

让我们通过一个例子来理解值函数。假设我们在网格世界环境中,按照某个策略 生成了轨迹 ,如 图 1.26 所示:

图 1.26:值函数示例

现在,我们如何计算我们轨迹中所有状态的值呢?我们了解到,状态的值是从该状态出发,遵循策略 获得的回报(奖励之和)。前述轨迹是通过策略 生成的,因此我们可以说,状态的值是从该状态开始的轨迹的回报(奖励之和):

  • 状态 A 的值是从状态 A 开始的轨迹的回报。因此,V(A) = 1+1+ -1+1 = 2。

  • 状态 D 的值是从状态 D 开始的轨迹的回报。因此,V(D) = 1-1+1 = 1。

  • 状态 E 的值是从状态 E 开始的轨迹的回报。因此,V(E) = -1+1 = 0。

  • 状态 H 的值是从状态 H 开始的轨迹的回报。因此,V(H) = 1。

那么,最终状态 I 的值是多少呢?我们知道,状态的值是从该状态开始的回报(奖励之和)。我们知道,在从一个状态过渡到另一个状态时会获得奖励。由于 I 是最终状态,我们从最终状态没有进行任何过渡,因此没有奖励,也没有最终状态 I 的值。

简而言之,状态的值是从该状态开始的轨迹的回报。

等等!这里有一个小变化:我们不再直接将回报作为状态的值,而是使用期望回报。因此,值函数或状态 s 的值可以定义为智能体按照策略 从状态 s 开始所获得的期望回报。它可以表示为:

现在的问题是,为什么是期望回报?为什么我们不能直接将状态的值计算为回报呢?因为我们的回报是一个随机变量,它会根据某些概率取不同的值。

让我们通过一个简单的例子来理解这个概念。假设我们有一个随机策略 。我们了解到,不像将状态直接映射到动作的确定性策略,随机策略将状态映射到动作空间上的概率分布。因此,随机策略根据概率分布选择动作。

假设我们在状态 A 中,随机策略返回的动作空间概率分布为 [0.0, 0.80, 0.00, 0.20]。这意味着,在状态 A 中,按照随机策略,我们 80%的时间执行动作 down,即 ,20%的时间执行动作 right,即

因此,在状态 A 中,我们的随机策略 80%的时间选择动作 down,20%的时间选择动作 right,并且假设我们的随机策略在状态 DE 中选择动作 right,在状态 BF 中选择动作 down 100%的时间。

首先,我们使用随机策略 生成一个情景 ,如图 1.27所示:

图 1.27:情景

为了更好地理解,我们只关注状态A的值。状态A的值是从状态A开始的轨迹的回报(奖励的总和)。因此,

假设我们使用相同的随机策略 生成另一个情景 ,如图 1.28所示:

图 1.28:情景

状态A的值是从状态A开始的轨迹的回报(奖励的总和)。因此,

如你所见,尽管我们使用相同的策略,但轨迹 中状态A的值是不同的。这是因为我们的策略是随机策略,它在状态A下 80%的时间执行动作down,20%的时间执行动作right。因此,当我们使用策略 生成轨迹时,轨迹 会在 80%的时间发生,轨迹 会在 20%的时间发生。因此,回报在 80%的时间为 4,在 20%的时间为 2。

因此,我们不会直接将状态的值作为回报,而是将期望回报作为参考,因为回报有一定的概率取不同的值。期望回报本质上是加权平均数,即回报与其概率的乘积之和。因此,我们可以写作:

状态A的值可以表示为:

因此,状态的值是从该状态开始的轨迹的期望回报。

请注意,值函数依赖于策略,也就是说,状态的值会根据我们选择的策略而变化。根据不同的策略,可以有许多不同的值函数。最优值函数V**(s*)与其他所有值函数相比,能够产生最大的值。它可以表示为:

例如,假设我们有两个策略 。假设使用策略 时状态s的值为 ,使用策略 时状态s的值为 。那么状态s的最优值将是 ,因为它是最大的。给出最大状态值的策略称为最优策略 。因此,在这种情况下, 是最优策略,因为它给出了最大的状态值。

我们可以通过一个叫做值表(value table)的表格来查看值函数。假设我们有两个状态 s[0] 和 s[1],那么值函数可以表示为:

表 1.4:值表

从值表中,我们可以看出处于状态 s[1] 比处于状态 s[0] 更好,因为 s[1] 的值更高。因此,我们可以说状态 s[1] 是最优状态。

Q 函数

Q 函数,也称为状态-动作值函数,表示一个状态-动作对的值。状态-动作对的值是智能体从状态 s 开始并执行动作 a,根据策略 所获得的回报。状态-动作对的值或 Q 函数通常用 Q(s,a) 表示,称为 Q 值或状态-动作值。其表达式为:

请注意,值函数和 Q 函数之间唯一的区别在于,在值函数中我们计算的是一个状态的值,而在 Q 函数中我们计算的是一个状态-动作对的值。让我们通过一个例子来理解 Q 函数。考虑使用策略 生成的轨迹 图 1.29

图 1.29:轨迹/回合示例

我们了解到 Q 函数计算的是一个状态-动作对的值。假设我们需要计算状态-动作对 A-down 的 Q 值。也就是说,计算在状态 A 下执行动作 down 的 Q 值。那么 Q 值就是从状态 A 开始并执行动作 down 的轨迹的回报:

假设我们需要计算状态-动作对 D-right 的 Q 值。也就是说,计算在状态 D 下执行动作 right 的 Q 值。Q 值将是从状态 D 开始并执行动作 right 的轨迹的回报:

类似地,我们可以计算所有状态-动作对的 Q 值。与我们学习的值函数类似,不是直接将回报作为状态-动作对的 Q 值,而是使用期望回报,因为回报是一个随机变量,并且它的值有一定的概率会发生变化。因此,我们可以将 Q 函数重新定义为:

这意味着 Q 值是智能体从状态 s 开始并执行动作 a,根据策略 所获得的期望回报。

与值函数类似,Q 函数依赖于策略,即 Q 值会根据我们选择的策略而变化。根据不同的策略,可能会有许多不同的 Q 函数。最优 Q 函数是所有 Q 函数中具有最大 Q 值的那个,可以表示为:

最优策略 是给予最大 Q 值的策略。

像价值函数一样,Q 函数也可以以表格的形式展示。它被称为 Q 表。假设我们有两个状态 s[0] 和 s[1],以及两个动作 0 和 1;那么 Q 函数可以表示如下:

表 1.5:Q 表

正如我们所观察到的,Q 表表示所有可能的状态-动作对的 Q 值。我们了解到,最优策略是能够为我们的智能体带来最大回报(奖励的总和)的策略。我们可以通过从 Q 表中选择每个状态下具有最大 Q 值的动作来提取最优策略。因此,我们的最优策略将在状态 s[0] 选择动作 1,在状态 s[1] 选择动作 0,因为它们的 Q 值较高,如 表 1.6 所示:

表 1.6:从 Q 表中提取的最优策略

因此,我们可以通过计算 Q 函数来提取最优策略。

基于模型的学习与无模型学习

现在,让我们来看看两种不同的学习类型:基于模型的学习和无模型学习。

基于模型的学习:在基于模型的学习中,智能体将拥有环境的完整描述。我们知道,转移概率告诉我们通过执行动作 a 从状态 s 转移到下一个状态 的概率。奖励函数告诉我们,在执行动作 a 从状态 s 转移到下一个状态 时所获得的奖励。当智能体知道其环境的模型动态时,即智能体知道其环境的转移概率时,这种学习被称为基于模型的学习。因此,在基于模型的学习中,智能体利用模型动态来找到最优策略。

无模型学习:无模型学习是指智能体不知道其环境的模型动态。也就是说,在无模型学习中,智能体尝试在没有模型动态的情况下找到最优策略。

接下来,我们将探索智能体所处的不同类型的环境。

不同类型的环境

在本章开始时,我们了解到环境是智能体的世界,智能体生活/停留在环境中。我们可以将环境分类为不同的类型。

确定性环境与随机环境

确定性环境:在确定性环境中,我们可以确定当智能体在状态 s 执行动作 a 时,它总是到达状态 。例如,假设我们有一个网格世界环境。假设智能体处于状态 A,当它从状态 A 移动时,它总是到达状态 D。因此,环境被称为确定性环境:

图 1.30:确定性环境

随机环境:在随机环境中,我们不能说通过在状态 s 执行动作 a 代理总是到达状态 ,因为在随机环境中会有一定的随机性。例如,假设我们的网格世界环境是一个随机环境。假设我们的代理处于状态 A,现在如果它从状态 A 向下移动,那么代理并不总是到达状态 D。相反,它 70%的概率到达状态 D,30%的概率到达状态 B。也就是说,如果代理在状态 A 下移,那么它以 70%的概率到达状态 D,以 30%的概率到达状态 B,正如图 1.31所示:

图 1.31:随机环境

离散环境与连续环境

离散环境:离散环境是指环境的动作空间是离散的。例如,在网格世界环境中,我们的动作空间是离散的,包含了动作[],因此我们的网格世界环境是离散的。

连续环境:连续环境是指环境的动作空间是连续的。例如,假设我们正在训练一个代理来驾驶一辆车,那么我们的动作空间将是连续的,其中包括多个连续的动作,如改变车速、旋转方向盘所需的角度等。在这种情况下,我们环境的动作空间是连续的。

逐步环境与非逐步环境

逐步环境:在逐步环境中,代理的当前动作不会影响未来的动作,因此逐步环境也被称为非顺序环境。

非逐步环境:在非逐步环境中,代理的当前动作会影响未来的动作,因此非逐步环境也被称为顺序环境。例如,国际象棋棋盘是一个顺序环境,因为代理的当前动作会影响未来的动作。

单代理与多代理环境

  • 单代理环境:当我们的环境只包含一个代理时,它被称为单代理环境。

  • 多代理环境:当我们的环境包含多个代理时,它被称为多代理环境。

我们已经覆盖了强化学习的许多概念。现在,我们将通过看看一些令人兴奋的强化学习应用来结束本章。

强化学习的应用

强化学习(RL)在过去几年中发展迅速,应用范围广泛,从玩游戏到自动驾驶汽车。导致这一发展的主要原因之一是深度强化学习DRL),它是 RL 和深度学习的结合。我们将在接下来的章节中学习各种最先进的深度 RL 算法,所以请期待!在这一节中,我们将探讨一些 RL 的现实应用:

  • 制造业:在制造业中,智能机器人通过强化学习被训练成能够将物品放置到正确的位置。使用智能机器人能够减少人工成本并提高生产力。

  • 动态定价:强化学习的一个热门应用是动态定价。动态定价意味着我们根据需求和供应来调整产品的价格。我们可以训练强化学习代理来实现产品的动态定价,目标是最大化收入。

  • 库存管理:强化学习广泛应用于库存管理,这是一个至关重要的业务活动。这些活动包括供应链管理、需求预测以及处理多个仓库操作(例如将产品放置在仓库中以高效管理空间)。

  • 推荐系统:强化学习广泛应用于构建推荐系统,在这些系统中,用户的行为不断变化。例如,在音乐推荐系统中,用户的行为或音乐偏好会随时间变化。因此,在这些情况下,使用强化学习代理非常有用,因为代理会通过与环境的交互不断学习。

  • 神经架构搜索:为了让神经网络以良好的准确性执行给定任务,网络的架构非常重要,必须正确设计。通过强化学习,我们可以自动化复杂神经架构搜索的过程,通过训练代理来寻找给定任务的最佳神经架构,目标是最大化准确性。

  • 自然语言处理(NLP):随着深度强化学习算法的普及,强化学习已广泛应用于多个自然语言处理任务,如抽象文本摘要、聊天机器人等。

  • 金融:强化学习广泛应用于金融投资组合管理,即不断将资金重新分配到不同的金融产品中。强化学习也被用于预测和交易商业交易市场。摩根大通成功地利用强化学习为大宗订单提供更好的交易执行结果。

强化学习术语

我们已经学习了强化学习的几个重要基本概念。在本节中,我们将回顾一些对于理解接下来章节非常有用的术语。

代理:代理是学习做出智能决策的软件程序,例如一个能够智能下棋的软件程序。

环境:环境是代理的世界。如果我们继续使用象棋的例子,棋盘就是代理进行象棋游戏的环境。

状态:状态是代理可能处于的环境中的一个位置或时刻。例如,棋盘上的所有位置都被称为状态。

动作:代理通过执行动作与环境交互,并从一个状态转移到另一个状态,例如棋子所做的移动就是动作。

奖励:奖励是代理根据其动作获得的数值。可以将奖励视为一个分数。例如,代理因执行一个好动作而获得 +1 分(奖励),因执行一个坏动作而获得 -1 分(奖励)。

动作空间:环境中所有可能动作的集合称为动作空间。当我们的动作空间由离散动作组成时,称为离散动作空间;当我们的动作空间由连续动作组成时,称为连续动作空间。

策略:代理根据策略做出决策。策略告诉代理在每个状态下应该执行什么动作。它可以被看作是代理的大脑。如果一个策略能精确地将一个状态映射到一个特定动作,则称之为确定性策略。与确定性策略不同,随机策略将状态映射到动作空间上的概率分布。最优策略是能带来最大奖励的策略。

回合:代理与环境从初始状态到终止状态的交互称为一个回合。回合通常被称为轨迹或展开。

回合任务和连续任务:如果一个 RL 任务有终止状态,则称为回合任务;如果任务没有终止状态,则称为连续任务。

时间跨度:时间跨度可以视为代理的生命周期,即代理与环境交互的时间步长。如果代理-环境交互在某一特定时间步长结束,则称为有限时间跨度;如果代理与环境的交互持续到永远,则称为无限时间跨度。

回报:回报是代理在一个回合中获得的所有奖励的总和。

折扣因子:折扣因子帮助我们控制是否要重视即时奖励还是未来奖励。折扣因子的值范围从 0 到 1。当折扣因子接近 0 时,意味着我们更重视即时奖励;而折扣因子接近 1 时,意味着我们更重视未来奖励而非即时奖励。

价值函数:价值函数或状态的价值是指代理在按照策略从状态 s 开始时所期望的回报。

Q 函数:Q 函数或状态-动作对的价值意味着代理在按照策略从状态 s 开始并执行动作 a 时所期望的回报。

基于模型和无模型学习:当代理尝试通过模型动力学学习最优策略时,这称为基于模型的学习;而当代理尝试在没有模型动力学的情况下学习最优策略时,这称为无模型学习。

确定性和随机性环境:当一个智能体在状态s下执行动作a,并且每次都到达状态 时,环境被称为确定性环境。当一个智能体在状态s下执行动作a,并且每次根据某些概率分布到达不同的状态时,环境被称为随机性环境。

总结

我们从理解强化学习的基本思想开始。本章中,我们了解到强化学习是一个基于试错的学习过程,学习依赖于奖励。接着,我们探讨了强化学习与其他机器学习范式(如监督学习和无监督学习)的不同。随后,我们了解了马尔可夫决策过程(MDP),并学习了如何将强化学习的环境建模为 MDP。接着,我们理解了强化学习中的几个重要基本概念,在本章结尾时,我们还探讨了强化学习的一些实际应用。

因此,在本章中,我们学习了强化学习的几个基本概念。在下一章,我们将通过使用一个流行的工具包 Gym,实施我们在本章中学习的所有基本概念,开始我们的实践强化学习之旅。

问题

让我们通过回答以下问题来评估我们新获得的知识:

  1. 强化学习(RL)与其他机器学习(ML)范式有何不同?

  2. 在强化学习的设定中,什么被称为环境?

  3. 确定性策略和随机性策略有什么区别?

  4. 什么是一个回合(episode)?

  5. 为什么我们需要折扣因子?

  6. 值函数与 Q 函数有什么区别?

  7. 确定性环境和随机性环境有何不同?

进一步阅读

进一步的信息,请参阅以下链接:

强化学习:由L. P. Kaelbling, M. L. Littman, A. W. Moore编写的调查报告,访问链接:arxiv.org/abs/cs/9605103

第二章:Gym 工具包指南

OpenAI 是一个 人工智能 (AI) 研究组织,旨在构建 人工通用智能 (AGI)。OpenAI 提供了一个著名的工具包,叫做 Gym,用于训练强化学习代理。

假设我们需要训练我们的代理驾驶一辆汽车。我们需要一个环境来训练代理。我们能在现实世界的环境中训练代理驾驶汽车吗?不能,因为我们已经知道强化学习(RL)是一个试错学习过程,所以在训练代理时,它会在学习过程中犯很多错误。例如,假设我们的代理撞到另一辆车,并获得了负奖励。它将学到撞击其他车辆不是一个好的动作,并会尝试避免再次执行这个动作。但我们不能通过让 RL 代理撞车来训练它在现实环境中驾驶对吧?这就是为什么我们使用模拟器,并在模拟环境中训练 RL 代理的原因。

有许多工具包提供了一个模拟环境,用于训练强化学习(RL)代理。一个流行的工具包是 Gym。Gym 提供了多种环境,用于训练 RL 代理,从经典控制任务到 Atari 游戏环境应有尽有。我们可以通过各种 RL 算法训练我们的 RL 代理,使其在这些模拟环境中学习。在本章中,首先,我们将安装 Gym,然后我们将探索各种 Gym 环境。我们还将通过在 Gym 环境中进行实验,实践我们在上一章学到的概念。

在本书中,我们将使用 Gym 工具包来构建和评估强化学习算法,因此在本章中,我们将熟悉 Gym 工具包。

在本章中,我们将学习以下主题:

  • 设置我们的机器

  • 安装 Anaconda 和 Gym

  • 理解 Gym 环境

  • 在 Gym 环境中生成一个回合

  • 探索更多 Gym 环境

  • 随机代理的倒立摆平衡

  • 一名代理玩网球游戏

设置我们的机器

在本节中,我们将学习如何安装一些运行全书代码所需的依赖项。首先,我们将学习如何安装 Anaconda,然后我们将探索如何安装 Gym。

安装 Anaconda

Anaconda 是一个开源的 Python 发行版,广泛用于科学计算和处理大量数据。它提供了一个优秀的包管理环境,支持 Windows、Mac 和 Linux 操作系统。Anaconda 自带 Python,并且包括许多用于科学计算的流行包,如 NumPy、SciPy 等。

要下载 Anaconda,请访问www.anaconda.com/download/,你将在该页面上看到适用于不同平台的 Anaconda 下载选项。如果你使用的是 Windows 或 macOS,你可以根据你的机器架构直接下载图形安装程序,并使用图形安装程序安装 Anaconda。

如果你使用的是 Linux,请按照以下步骤操作:

  1. 打开终端并输入以下命令以下载 Anaconda:

    wget https://repo.continuum.io/archive/Anaconda3-5.0.1-Linux-x86_64.sh 
    
  2. 下载完成后,我们可以使用以下命令安装 Anaconda:

    bash Anaconda3-5.0.1-Linux-x86_64.sh 
    

在成功安装 Anaconda 之后,我们需要创建一个虚拟环境。为什么需要虚拟环境呢?假设我们正在进行项目 A,该项目使用 NumPy 版本 1.14,而项目 B 使用 NumPy 版本 1.13。那么,要在项目 B 中工作,我们要么降级 NumPy,要么重新安装 NumPy。在每个项目中,我们使用的是不同版本的库,这些库在其他项目中不可用。为了避免每次新项目都需要降级、升级版本或重新安装库,我们使用虚拟环境。

虚拟环境是为特定项目创建的一个隔离环境,使得每个项目可以拥有自己独立的依赖项,并且不会影响其他项目。我们将使用以下命令创建一个名为universe的虚拟环境:

conda create --name universe python=3.6 anaconda 

请注意,我们使用的是 Python 版本 3.6。虚拟环境创建完成后,我们可以使用以下命令激活它:

source activate universe 

就这样!现在我们已经学会了如何安装 Anaconda 并创建虚拟环境,在接下来的章节中,我们将学习如何安装 Gym。

安装 Gym 工具包

在本节中,我们将学习如何安装 Gym 工具包。在继续之前,首先让我们激活虚拟环境universe

source activate universe 

现在,安装以下依赖项:

sudo apt-get update
sudo apt-get install golang libcupti-dev libjpeg-turbo8-dev make tmux htop chromium-browser git cmake zlib1g-dev libjpeg-dev xvfb libav-tools xorg-dev python-opengl libboost-all-dev libsdl2-dev swig
conda install pip six libgcc swig
conda install opencv 

我们可以通过pip直接安装 Gym。请注意,本书中将使用 Gym 版本 0.15.4。我们可以使用以下命令安装 Gym:

pip install gym==0.15.4 

我们也可以通过克隆 Gym 仓库来安装 Gym,如下所示:

cd ~
git clone https://github.com/openai/gym.git
cd gym
pip install -e '.[all]' 

常见错误修复

如果在安装 Gym 时遇到以下任何错误,以下命令将有所帮助:

  • 构建 pachi-py 轮文件失败构建 pachi-py atari-py 轮文件失败

    sudo apt-get update
    sudo apt-get install xvfb libav-tools xorg-dev libsdl2-dev swig cmake 
    
  • 构建 mujoco-py 轮文件失败

    git clone https://github.com/openai/mujoco-py.git
    cd mujoco-py
    sudo apt-get update
    sudo apt-get install libgl1-mesa-dev libgl1-mesa-glx libosmesa6-dev python3-pip python3-numpy python3-scipy
    pip3 install -r requirements.txt
    sudo python3 setup.py install 
    
  • 错误:命令'gcc'以退出状态 1 失败

    sudo apt-get update
    sudo apt-get install python-dev 
    sudo apt-get install libevent-dev 
    

现在我们已经成功安装了 Gym,在接下来的章节中,让我们开始我们的强化学习实践之旅。

创建我们的第一个 Gym 环境

我们已经了解到,Gym 提供了多种环境用于训练强化学习代理。为了清楚地理解 Gym 环境的设计,我们将从基本的 Gym 环境开始。之后,我们将理解其他复杂的 Gym 环境。

让我们介绍一个最简单的环境之一,叫做 Frozen Lake 环境。图 2.1显示了 Frozen Lake 环境。正如我们所观察到的,在 Frozen Lake 环境中,智能体的目标是从初始状态S开始,到达目标状态G

图 2.1:Frozen Lake 环境

在上述环境中,以下内容适用:

  • S表示起始状态

  • F表示冰冻状态

  • H表示洞穴状态

  • G表示目标状态

因此,智能体必须从状态S开始,达到目标状态G。但是有一个问题,如果智能体访问了状态H(即洞穴状态),那么智能体将掉进洞里并死亡,如图 2.2所示:

图 2.2:智能体掉进洞里

因此,我们需要确保智能体从S开始并达到G,而不掉进洞穴状态H,如图 2.3所示:

图 2.3:智能体达到目标状态

上述环境中的每个格子被称为一个状态,因此我们有 16 个状态(SG),并且我们有 4 个可能的动作,分别是。我们了解到,我们的目标是从S到达G,而不经过H。因此,我们为目标状态G分配+1 奖励,为所有其他状态分配 0 奖励。

因此,我们已经了解了 Frozen Lake 环境是如何工作的。现在,为了在 Frozen Lake 环境中训练我们的智能体,首先,我们需要通过 Python 从头开始编写代码来创建环境。但幸运的是,我们不必这么做!因为 Gym 提供了各种环境,我们可以直接导入 Gym 工具包并创建一个 Frozen Lake 环境。

现在,我们将学习如何使用 Gym 创建我们的 Frozen Lake 环境。在运行任何代码之前,请确保你已经激活了我们的虚拟环境universe。首先,让我们导入 Gym 库:

import gym 

接下来,我们可以使用make函数创建一个 Gym 环境。make函数需要环境 ID 作为参数。在 Gym 中,Frozen Lake 环境的 ID 是FrozenLake-v0。因此,我们可以按如下方式创建我们的 Frozen Lake 环境:

env = gym.make("FrozenLake-v0") 

创建环境后,我们可以使用render函数查看我们的环境长什么样:

env.render() 

上述代码呈现了以下环境:

图 2.4:Gym 的 Frozen Lake 环境

如我们所观察到的,Frozen Lake 环境由 16 个状态(SG)组成,正如我们所学到的那样。状态S被高亮显示,表示这是我们当前的状态,也就是说,智能体处于状态S。因此,每当我们创建一个环境时,智能体总是从初始状态开始,在我们的案例中,初始状态是状态S

就是这样!使用 Gym 创建环境就是这么简单。在下一部分,我们将通过结合上一章中学到的所有概念,进一步了解 Gym 环境。

探索环境

在上一章中,我们了解到强化学习环境可以被建模为 马尔可夫决策过程MDP),MDP 包括以下内容:

  • 状态:环境中存在的一组状态。

  • 动作:代理在每个状态下可以执行的动作集合。

  • 转移概率:转移概率用 表示。它表示执行某个动作 a 时,从状态 s 转移到状态 的概率。

  • 奖励函数:奖励函数用 表示。它表示代理从状态 s 转移到状态 时,执行动作 a 所获得的奖励。

现在,让我们理解如何从我们刚刚使用 Gym 创建的 Frozen Lake 环境中获取上述所有信息。

状态

状态空间由我们所有的状态组成。我们可以通过输入 env.observation_space 来获取环境中状态的数量,如下所示:

print(env.observation_space) 

上面的代码将打印:

Discrete(16) 

这意味着我们的状态空间中有 16 个离散状态,从状态 SG。注意,在 Gym 中,状态会被编码为数字,因此状态 S 会被编码为 0,状态 F 会被编码为 1,以此类推,如 图 2.5 所示:

图 2.5:十六个离散状态

动作

我们了解到,动作空间由环境中所有可能的动作组成。我们可以通过使用 env.action_space 来获取动作空间:

print(env.action_space) 

上面的代码将打印:

Discrete(4) 

它显示我们在动作空间中有 4 个离散动作,分别是 。注意,和状态类似,动作也会被编码成数字,如 表 2.1 所示:

表 2.1:四个离散动作

转移概率和奖励函数

现在,让我们来看一下如何获取转移概率和奖励函数。我们了解到,在随机环境中,我们不能说通过执行某个动作 a,代理就能总是精确到达下一个状态 ,因为会有一些与随机环境相关的随机性,执行动作 a 时,代理从状态 s 到达下一个状态 的概率是存在的。

假设我们处于状态 2(F)。现在,如果我们在状态 2 执行动作 1(下移),我们可以到达状态 6,如 图 2.6 所示:

图 2.6:代理从状态 2 执行下移动作

我们的 Frozen Lake 环境是一个随机环境。当我们的环境是随机时,执行动作 1(下移)在状态 2 时,我们不一定总是能到达状态 6;我们还会以一定的概率到达其他状态。所以,当我们在状态 2 执行动作 1(下移)时,我们以 0.33333 的概率到达状态 1,以 0.33333 的概率到达状态 6,以 0.33333 的概率到达状态 3,如 图 2.7 所示:

图 2.7:状态 2 中智能体的转移概率

如我们所见,在随机环境中,我们以一定的概率到达下一个状态。现在,让我们学习如何使用 Gym 环境获取这个转移概率。

我们可以通过输入 env.P[state][action] 来获取转移概率和奖励函数。因此,要获取通过执行动作 right 从状态 S 转移到其他状态的转移概率,我们可以输入 env.P[S][right]。但是我们不能直接输入状态 S 和动作 right,因为它们是以数字编码的。我们知道状态 S 编码为 0,动作 right 编码为 2,因此,为了获取执行动作 right 后状态 S 的转移概率,我们输入 env.P[0][2],如下面所示:

print(env.P[0][2]) 

上述代码将打印:

[(0.33333, 4, 0.0, False),
 (0.33333, 1, 0.0, False),
 (0.33333, 0, 0.0, False)] 

这意味着什么?我们的输出形式为 [(转移概率, 下一个状态, 奖励, 是否终止状态?)]。这意味着,如果我们在状态 0 (S) 执行动作 2 (right),则:

  • 我们以 0.33333 的概率到达状态 4 (F),并获得 0 奖励。

  • 我们以 0.33333 的概率到达状态 1 (F),并获得 0 奖励。

  • 我们以 0.33333 的概率到达相同的状态 0 (S),并获得 0 奖励。

图 2.8 显示了转移概率:

图 2.8:状态 0 中智能体的转移概率

因此,当我们输入 env.P[state][action] 时,我们将得到以下形式的结果:[(转移概率, 下一个状态, 奖励, 是否终止状态?)]。最后一个值是布尔值,告诉我们下一个状态是否是终止状态。由于 4、1 和 0 不是终止状态,因此给出的值为 false。

env.P[0][2] 的输出如 表 2.2 所示,以便更清楚地理解:

表 2.2:env.P[0][2] 的输出

让我们通过另一个例子来理解这个问题。假设我们处于状态 3 (F),如 图 2.9 所示:

图 2.9:状态 3 中的智能体

假设我们在状态 3 (F) 执行动作 1 (down)。那么,通过执行动作 1 (down),状态 3 (F) 的转移概率如下所示:

print(env.P[3][1]) 

上述代码将打印:

[(0.33333, 2, 0.0, False),
 (0.33333, 7, 0.0, True),
 (0.33333, 3, 0.0, False)] 

如我们所学,输出的形式为 [(转移概率, 下一个状态, 奖励, 是否终止状态?)]。这意味着如果我们在状态 3 (F) 执行动作 1 (down),那么:

  • 我们以 0.33333 的概率到达状态 2 (F),并获得 0 奖励。

  • 我们以 0.33333 的概率到达状态 7 (H),并获得 0 奖励。

  • 我们以 0.33333 的概率到达相同的状态 3 (F),并获得 0 奖励。

图 2.10 显示了转移概率:

图 2.10:状态 3 中智能体的转移概率

env.P[3][1] 的输出如 表 2.3 所示,以便更清楚地理解:

表 2.3:env.P[3][1] 的输出

正如我们所观察到的,在输出的第二行中,我们有 (0.33333, 7, 0.0, True),这里的最后一个值被标记为True。这意味着状态 7 是一个终止状态。也就是说,如果我们在状态 3 (F) 执行动作 1 (),则我们会以 0.33333 的概率到达状态 7 (H),并且由于 7 (H) 是一个洞,智能体如果到达状态 7 (H) 就会死亡。因此,7 (H) 是一个终止状态,所以它被标记为True

因此,我们已经学会了如何在 Gym 环境中获取状态空间、动作空间、转移概率和奖励函数。在接下来的部分,我们将学习如何生成一个回合。

在 Gym 环境中生成一个回合

我们了解到,从初始状态到终止状态的智能体-环境交互过程被称为一个回合。在这一部分,我们将学习如何在 Gym 环境中生成一个回合。

在我们开始之前,我们通过重置环境来初始化状态;重置操作将智能体带回初始状态。我们可以使用 reset() 函数来重置环境,具体如下所示:

state = env.reset() 

动作选择

为了让智能体与环境进行互动,它必须在环境中执行一些动作。所以,首先,让我们学习如何在 Gym 环境中执行一个动作。假设我们处于状态 3 (F),如图 2.11所示:

图 2.11:智能体处于冻结湖环境中的状态 3

假设我们需要执行动作 1 () 并移动到新的状态 7 (H)。我们该如何做呢?我们可以使用 step 函数来执行一个动作。只需将我们的动作作为参数输入 step 函数。于是,我们可以在状态 3 (F) 中使用 step 函数执行动作 1 (),如以下所示:

env.step(1) 

现在,让我们使用 render 函数渲染我们的环境:

env.render() 

图 2.12所示,智能体在状态 3 (F) 中执行动作 1 () 并到达下一个状态 7 (H):

图 2.12:智能体处于冻结湖环境中的状态 7

注意,每当我们使用 env.step() 执行动作时,它会输出一个包含 4 个值的元组。所以,当我们在状态 3 (F) 中使用 env.step(1) 执行动作 1 () 时,它会给出如下输出:

(7, 0.0, True, {'prob': 0.33333}) 

正如你可能已经猜到的,这意味着当我们在状态 3 (F) 执行动作 1 () 时:

  • 我们到达了下一个状态 7 (H)。

  • 智能体获得了奖励 0.0

  • 由于下一个状态 7 (H) 是终止状态,因此它被标记为 True

  • 我们以 0.33333 的概率到达下一个状态 7 (H)。

所以,我们可以将这些信息存储为:

(next_state, reward, done, info) = env.step(1) 

因此:

  • next_state 表示下一个状态。

  • reward 表示获得的奖励。

  • done表示我们的回合是否已经结束。也就是说,如果下一个状态是终止状态,那么我们的回合将结束,done将标记为True,否则它将标记为False

  • info—除了转移概率外,在某些情况下,我们还会获得其他信息,并将其保存为 info,这些信息用于调试目的。

我们还可以从我们的动作空间中采样动作,并执行一个随机动作以探索我们的环境。我们可以使用sample函数采样一个动作:

random_action = env.action_space.sample() 

在从我们的动作空间中采样了一个动作后,我们使用我们的步进函数执行采样的动作:

next_state, reward, done, info = env.step(random_action) 

现在我们已经学会了如何在环境中选择动作,让我们看看如何生成一个回合。

生成一个回合

现在让我们学习如何生成一个回合。回合是智能体与环境的交互,从初始状态开始,到终止状态结束。智能体通过在每个状态下执行一些动作与环境进行交互。如果智能体到达终止状态,回合结束。因此,在 Frozen Lake 环境中,如果智能体到达终止状态,即坑洞状态(H)或目标状态(G),回合就会结束。

让我们了解如何使用随机策略生成一个回合。我们了解到,随机策略在每个状态下选择一个随机动作。因此,我们将通过在每个状态下采取随机动作来生成一个回合。所以在回合的每个时间步长中,我们在每个状态下采取一个随机动作,如果智能体到达终止状态,回合就会结束。

首先,让我们设置时间步长的数量:

num_timesteps = 20 

对于每个时间步长:

for t in range(num_timesteps): 

通过从动作空间中采样,随机选择一个动作:

 random_action = env.action_space.sample() 

执行选择的操作:

 next_state, reward, done, info = env.step(random_action) 

如果下一个状态是终止状态,则退出。这意味着我们的回合结束:

 if done:
        break 
from the action space, and our episode will end if the agent reaches the terminal state:
import gym
env = gym.make("FrozenLake-v0")
state = env.reset()
print('Time Step 0 :')
env.render()
num_timesteps = 20
for t in range(num_timesteps):
  random_action = env.action_space.sample()
  new_state, reward, done, info = env.step(random_action)
  print ('Time Step {} :'.format(t+1))
  env.render()
  if done:
    break 

上述代码将打印出类似于图 2.13的内容。请注意,每次运行上述代码时,你可能会得到不同的结果,因为智能体在每个时间步长中都在执行一个随机动作。

如下输出所示,在每个时间步长上,智能体在每个状态下执行一个随机动作,并且一旦智能体到达终止状态,我们的回合就结束。如图 2.13所示,在第 4 个时间步长,智能体到达了终止状态H,因此回合结束:

图 2.13:智能体在每个时间步长所采取的动作

我们不仅可以生成一个回合,还可以通过在每个状态下执行一些随机动作来生成一系列回合:

import gym
env = gym.make("FrozenLake-v0")
**num_episodes =** **10**
num_timesteps = 20 
**for** **i** **in** **range(num_episodes):**

    state = env.reset()
    print('Time Step 0 :')
    env.render()
    for t in range(num_timesteps):
        random_action = env.action_space.sample()

        new_state, reward, done, info = env.step(random_action)
        print ('Time Step {} :'.format(t+1))
        env.render()
        if done:
            break 

因此,我们可以通过从动作空间中采样,在每个状态下选择一个随机动作来生成一个回合。但等等!这样做有什么用?我们为什么需要生成一个回合?

在上一章中,我们了解到,智能体可以通过生成多个回合来找到最优策略(即在每个状态下选择正确的动作)。但是在前面的例子中,我们仅仅是在所有回合中在每个状态下采取了随机动作。那么智能体怎么能找到最优策略呢?那么,在 Frozen Lake 环境中,智能体如何找到最优策略,告诉智能体如何从状态S到达状态G,而不经过坑洞状态H呢?

这就是我们需要强化学习算法的地方。强化学习的核心是寻找最优策略,也就是在每个状态下告诉我们该执行什么动作的策略。我们将在接下来的章节中,通过生成一系列的训练回合,学习如何通过各种强化学习算法来找到最优策略。在本章中,我们将重点了解 Gym 环境以及 Gym 的各种功能,因为我们将在本书的整个过程中使用 Gym 环境。

到目前为止,我们已经理解了如何使用基本的 Frozen Lake 环境来工作,但 Gym 还有许多其他功能,以及一些非常有趣的环境。在下一部分,我们将学习其他 Gym 环境,并探索 Gym 的功能。

更多 Gym 环境

在本节中,我们将探索几个有趣的 Gym 环境,并探索 Gym 的不同功能。

经典控制环境

Gym 提供了多个经典控制任务的环境,如小车-摆杆平衡、摆动倒立摆、山地车爬坡等。让我们了解如何为小车-摆杆平衡任务创建一个 Gym 环境。小车-摆杆环境如下所示:

图 2.14:小车-摆杆示例

小车-摆杆平衡是经典控制问题之一。如图 2.14所示,摆杆连接在小车上,我们的智能体目标是保持摆杆平衡在小车上,也就是说,智能体的目标是保持摆杆竖直地立在小车上,如图 2.15所示:

图 2.15:目标是保持摆杆竖直

因此,智能体试图左右推动小车,以保持摆杆竖直立在小车上。我们的智能体执行两种动作,即将小车推向左侧和将小车推向右侧,以保持摆杆竖直立在小车上。你也可以查看这个非常有趣的视频,youtu.be/qMlcsc43-lg,展示了强化学习智能体如何通过左右移动小车来平衡小车上的摆杆。

现在,让我们学习如何使用 Gym 创建小车-摆杆环境。Gym 中小车-摆杆环境的环境 ID 是 CartPole-v0,所以我们可以使用 make 函数来创建小车-摆杆环境,如下所示:

env = gym.make("CartPole-v0") 

创建环境后,我们可以使用 render 函数查看环境:

env.render() 

我们还可以使用 close 函数关闭渲染的环境:

env.close() 

状态空间

现在,让我们来看一下我们小车-杆环境的状态空间。等等!这里的状态是什么?在 Frozen Lake 环境中,我们有 16 个离散状态,从SG。但是在这里我们如何描述状态呢?我们能通过小车位置来描述状态吗?可以!请注意,小车位置是一个连续值。所以,在这种情况下,我们的状态空间将是连续值,不像 Frozen Lake 环境中我们的状态空间是离散值(SG)。

但是,仅凭小车位置我们无法完全描述环境的状态。因此,我们还包括了小车速度、杆角度和杆端的速度。因此,我们可以通过一个值数组来描述我们的状态空间,如下所示:

array([cart position, cart velocity, pole angle, pole velocity at the tip]) 

注意,所有这些值都是连续的,也就是说:

  1. 小车位置的值范围从-4.84.8

  2. 小车速度的值范围从-InfInf)。

  3. 杆角度的值范围从-0.418弧度到0.418弧度。

  4. 杆端的杆速度的值范围从-InfInf

因此,我们的状态空间包含一个由连续值组成的数组。让我们学习如何从 Gym 中获取这一点。为了获取状态空间,我们可以直接输入env.observation_space,如下所示:

print(env.observation_space) 

前面的代码将输出:

Box(4,) 

Box意味着我们的状态空间由连续值组成,而不是离散值。也就是说,在 Frozen Lake 环境中,我们的状态空间是Discrete(16),表示我们有 16 个离散状态(SG)。但现在我们的状态空间表示为Box(4,),这意味着我们的状态空间是连续的,并由 4 个值组成的数组表示。

例如,让我们重置我们的环境,看看我们的初始状态空间会是什么样子。我们可以使用reset函数来重置环境:

print(env.reset()) 

前面的代码将输出:

array([ 0.02002635, -0.0228838 ,  0.01248453,  0.04931007]) 

注意,这里状态空间是随机初始化的,因此每次运行前面的代码时,我们将获得不同的值。

前面代码的结果意味着我们的初始状态空间由 4 个值的数组组成,分别表示小车位置、小车速度、杆角度和杆端的杆速度。也就是说:

图 2.16:初始状态空间

好的,我们如何获取我们状态空间的最大值和最小值?我们可以使用env.observation_space.high获取状态空间的最大值,使用env.observation_space.low获取状态空间的最小值。

例如,让我们来看一下我们状态空间的最大值:

print(env.observation_space.high) 

前面的代码将输出:

[4.8000002e+00 3.4028235e+38 4.1887903e-01 3.4028235e+38] 

这意味着:

  1. 小车位置的最大值是4.8

  2. 我们了解到,小车速度的最大值是+Inf,我们知道无穷大并不是真正的数字,因此它用最大的正实数3.4028235e+38来表示。

  3. 杆角度的最大值是0.418弧度。

  4. 杆尖的最大速度值为+Inf,因此使用最大正实数值3.4028235e+38来表示。

类似地,我们可以得到我们的状态空间的最小值为:

print(env.observation_space.low) 

上述代码将输出:

[-4.8000002e+00 -3.4028235e+38 -4.1887903e-01 -3.4028235e+38] 

它说明:

  1. 推车位置的最小值是-4.8

  2. 我们了解到推车速度的最小值是-Inf,而且我们知道无穷大实际上不是一个数值,因此使用最大负实数值-3.4028235e+38来表示。

  3. 杆角的最小值是-0.418弧度。

  4. 杆尖的最小速度值为-Inf,因此使用最大负实数值-3.4028235e+38来表示。

动作空间

现在,让我们来看看动作空间。我们已经学到,在 Cart-Pole 环境中,我们执行两个动作,即将推车向左推和将推车向右推,因此动作空间是离散的,因为我们只有两个离散的动作。

为了获得动作空间,我们可以直接输入env.action_space,如下所示:

print(env.action_space) 

上述代码将输出:

Discrete(2) 

正如我们所观察到的,Discrete(2)表示我们的动作空间是离散的,并且我们在动作空间中有两个动作。请注意,动作会编码为数字,如表 2.4所示:

表 2.4:两个可能的动作

使用随机策略进行 Cart-Pole 平衡

让我们创建一个具有随机策略的智能体,也就是说,我们创建一个在环境中选择随机动作并尝试平衡杆子的智能体。每当杆子在推车上竖直时,智能体会获得+1 奖励。我们将生成超过 100 个回合,看看每个回合中获得的返回值(奖励总和)。让我们一步步学习。

首先,让我们创建我们的 Cart-Pole 环境:

import gym
env = gym.make('CartPole-v0') 

设置回合数和每回合的时间步数:

num_episodes = 100
num_timesteps = 50 

对于每个回合:

for i in range(num_episodes): 

将返回值设置为0

 Return = 0 

通过重置环境来初始化状态:

 state = env.reset() 

对于每一步:

 for t in range(num_timesteps): 

渲染环境:

 env.render() 

通过从环境中采样随机选择一个动作:

 random_action = env.action_space.sample() 

执行随机选择的动作:

 next_state, reward, done, info = env.step(random_action) 

更新返回值:

 Return = Return + reward 

如果下一个状态是终止状态,则结束回合:

 if done:
            break 

每经过 10 个回合,打印返回值(奖励总和):

 if i%10==0:
        print('Episode: {}, Return: {}'.format(i, Return)) 

关闭环境:

env.close() 

上述代码将输出每 10 个回合获得的奖励总和:

Episode: 0, Return: 14.0
Episode: 10, Return: 31.0
Episode: 20, Return: 16.0
Episode: 30, Return: 9.0
Episode: 40, Return: 18.0
Episode: 50, Return: 13.0
Episode: 60, Return: 25.0
Episode: 70, Return: 21.0
Episode: 80, Return: 17.0
Episode: 90, Return: 14.0 

因此,我们了解了一个有趣且经典的控制问题——Cart-Pole 平衡问题,以及如何使用 Gym 创建 Cart-Pole 平衡环境。Gym 提供了其他几个经典控制环境,如图 2.17所示:

图 2.17:经典控制环境

你也可以通过使用 Gym 创建上述任何环境进行实验。我们可以在这里查看 Gym 提供的所有经典控制环境:gym.openai.com/envs/#classic_control

Atari 游戏环境

你是 Atari 游戏的粉丝吗?如果是的话,那你一定会对这一部分感兴趣。Atari 2600 是一款由 Atari 公司推出的视频游戏主机。Atari 游戏主机提供了几款非常受欢迎的游戏,包括 Pong、Space Invaders、Ms. Pac-Man、Break Out、Centipede 等等。训练我们的强化学习代理玩 Atari 游戏是一个既有趣又具有挑战性的任务。通常,大多数强化学习算法都会在 Atari 游戏环境中进行测试,以评估算法的准确性。

在这一部分,我们将学习如何使用 Gym 创建 Atari 游戏环境。Gym 提供了大约 59 个 Atari 游戏环境,包括 Pong、Space Invaders、Air Raid、Asteroids、Centipede、Ms. Pac-Man 等等。Gym 提供的某些 Atari 游戏环境展示在 图 2.18 中,激起你的兴趣:

图 2.18:Atari 游戏环境

在 Gym 中,每个 Atari 游戏环境有 12 个不同的变体。我们以 Pong 游戏环境为例来理解这一点。Pong 游戏环境将有 12 个不同的变体,具体说明如下所述。

一般环境

  • Pong-v0 和 Pong-v4:我们可以通过环境 ID 创建一个 Pong 环境,ID 可以是 Pong-v0 或 Pong-v4。那么,环境的状态是什么呢?由于我们处理的是游戏环境,我们可以直接将游戏屏幕的图像作为状态。但我们不能直接处理原始图像,所以我们将使用游戏屏幕的像素值作为状态。我们将在接下来的章节中详细了解这一点。

  • Pong-ram-v0 和 Pong-ram-v4:这与 Pong-v0 和 Pong-v4 类似。但在这里,环境的状态是 Atari 机器的 RAM,即 128 字节,而不是游戏屏幕的像素值。

确定性环境

  • PongDeterministic-v0 和 PongDeterministic-v4:在这种类型中,顾名思义,每次初始化环境时,游戏的初始位置都会相同,环境的状态是游戏屏幕的像素值。

  • Pong-ramDeterministic-v0 和 Pong-ramDeterministic-v4:这与 PongDeterministic-v0 和 PongDeterministic-v4 类似,但这里的状态是 Atari 机器的 RAM。

不跳帧

  • PongNoFrameskip-v0 和 PongNoFrameskip-v4:在这种类型中,游戏帧不会被跳过;所有游戏画面对代理都是可见的,状态是游戏屏幕的像素值。

  • Pong-ramNoFrameskip-v0 和 Pong-ramNoFrameskip-v4:这与 PongNoFrameskip-v0 和 PongNoFrameskip-v4 类似,但这里的状态是 Atari 机器的 RAM。

因此,在 Atari 环境中,我们的环境状态将是游戏屏幕或 Atari 机器的 RAM。请注意,与 Pong 游戏类似,所有其他 Atari 游戏在 Gym 环境中的 ID 都以相同的方式命名。例如,假设我们想创建一个确定性的 Space Invaders 环境,那么我们只需使用 ID SpaceInvadersDeterministic-v0创建它。如果我们想创建一个没有帧跳过的 Space Invaders 环境,那么我们可以使用 ID SpaceInvadersNoFrameskip-v0来创建它。

我们可以在这里查看 Gym 提供的所有 Atari 游戏环境:gym.openai.com/envs/#atari

状态和动作空间

现在,让我们详细探讨 Atari 游戏环境的状态空间和动作空间。

状态空间

在这一部分,我们将理解 Gym 环境中 Atari 游戏的状态空间。我们通过 Pong 游戏来学习这一点。我们已经了解,在 Atari 环境中,环境的状态将是游戏屏幕的像素值或 Atari 机器的 RAM。首先,让我们理解状态空间,其中环境的状态是游戏屏幕的像素值。

让我们使用make函数创建一个 Pong 环境:

env = gym.make("Pong-v0") 

这里,游戏屏幕是我们环境的状态。因此,我们将把游戏屏幕的图像作为状态。然而,我们不能直接处理原始图像,因此我们将把图像(游戏屏幕)的像素值作为我们的状态。图像像素的维度将是3,包含图像的高度、宽度和通道数。

因此,我们的环境状态将是一个包含游戏屏幕像素值的数组:

 [Image height, image width, number of the channel] 

请注意,像素值的范围是从 0 到 255。为了获取状态空间,我们只需输入env.observation_space,如下所示:

print(env.observation_space) 

上面的代码将输出:

Box(210, 160, 3) 

这表示我们的状态空间是一个形状为[210,160,3]的三维数组。正如我们所学,210表示图像的高度,160表示图像的宽度,而3表示通道数。

例如,我们可以重置环境并查看初始状态空间的样子。我们可以使用重置函数来重置环境:

print(env.reset()) 

上面的代码将输出一个表示初始游戏屏幕像素值的数组。

现在,让我们创建一个 Pong 环境,其中环境的状态是 Atari 机器的 RAM,而不是游戏屏幕的像素值:

env = gym.make("Pong-ram-v0") 

让我们看看状态空间:

print(env.observation_space) 

上面的代码将输出:

Box(128,) 

这意味着我们的状态空间是一个包含 128 个值的一维数组。我们可以重置环境并查看初始状态空间的样子:

print(env.reset()) 

请注意,这适用于 Gym 环境中的所有 Atari 游戏。例如,如果我们创建一个 Space Invaders 环境,将环境的状态定义为游戏画面的像素值,那么我们的状态空间将是一个形状为Box(210, 160, 3)的三维数组。然而,如果我们将环境的状态定义为 Atari 机器的 RAM,那么我们的状态空间将是一个形状为Box(128,)的数组。

动作空间

现在让我们探索动作空间。一般来说,Atari 游戏环境的动作空间有 18 个动作,这些动作被编码为从 0 到 17,如表 2.5所示:

表 2.5:Atari 游戏环境动作

请注意,所有前述的 18 个动作并不适用于所有的 Atari 游戏环境,动作空间因游戏而异。例如,有些游戏只使用前六个动作作为其动作空间,有些游戏只使用前九个动作,而其他游戏则使用全部 18 个动作。让我们通过一个例子来理解这一点,使用 Pong 游戏:

env = gym.make("Pong-v0")
print(env.action_space) 

上述代码将打印:

Discrete(6) 

代码显示我们在 Pong 的动作空间中有6个动作,动作从05进行编码。因此,Pong 游戏中的可能动作是 noop(无动作)、fire、up、right、left 和 down。

现在让我们看看 Road Runner 游戏的动作空间。以防你以前没有遇到过这个游戏,游戏画面如下所示:

图 2.19:Road Runner 环境

让我们查看 Road Runner 游戏的动作空间:

env = gym.make("RoadRunner-v0")
print(env.action_space) 

上述代码将打印:

Discrete(18) 

这向我们展示了 Road Runner 游戏中的动作空间包含所有 18 个动作。

玩网球游戏的智能体

在本节中,让我们探索如何创建一个智能体来玩网球游戏。我们将创建一个随机策略的智能体,这意味着智能体将从动作空间中随机选择一个动作并执行。

首先,让我们创建我们的网球环境:

import gym
env = gym.make('Tennis-v0') 

让我们查看网球环境:

env.render() 

上述代码将显示以下内容:

图 2.20:网球游戏环境

设置回合数和每个回合的时间步数:

num_episodes = 100
num_timesteps = 50 

每个回合:

for i in range(num_episodes): 

将回报设置为0

 Return = 0 

通过重置环境初始化状态:

 state = env.reset() 

每个回合的每一步:

 for t in range(num_timesteps): 

渲染环境:

 env.render() 

通过从环境中采样随机选择一个动作:

 random_action = env.action_space.sample() 

执行随机选择的动作:

 next_state, reward, done, info = env.step(random_action) 

更新回报:

 Return = Return + reward 

如果下一个状态是终止状态,则结束该回合:

 if done:
            break 

每 10 个回合打印回报(奖励的总和):

 if i%10==0:
        print('Episode: {}, Return: {}'.format(i, Return)) 

关闭环境:

env.close() 

上述代码将输出每 10 个回合获得的回报(奖励总和):

Episode: 0, Return: -1.0
Episode: 10, Return: -1.0
Episode: 20, Return: 0.0
Episode: 30, Return: -1.0
Episode: 40, Return: -1.0
Episode: 50, Return: -1.0
Episode: 60, Return: 0.0
Episode: 70, Return: 0.0
Episode: 80, Return: -1.0
Episode: 90, Return: 0.0 

记录游戏

我们刚刚学习了如何创建一个代理,随机选择一个动作并玩网球游戏。那么,我们能否同时记录代理玩游戏的过程并将其保存为视频呢?当然可以!Gym 提供了一个包装类,我们可以用它将代理的游戏过程保存为视频。

为了录制游戏,我们的系统应该支持 FFmpeg。FFmpeg 是一个用于处理媒体文件的框架。因此,在继续之前,请确保你的系统支持 FFmpeg。

我们可以使用Monitor包装器来录制我们的游戏,如下代码所示。它需要三个参数:环境;我们想保存录制文件的目录;以及 force 选项。如果我们设置force = False,则意味着每次保存新录制时都需要创建一个新目录;如果我们设置force = True,目录中的旧录制文件将被清除并替换为新录制:

env = gym.wrappers.Monitor(env, 'recording', force=True) 

我们只需要在创建环境后添加前述的代码行。让我们通过一个简单的例子来看看录制是如何工作的。我们让代理随机玩一次网球游戏并记录代理的游戏过程作为视频:

import gym
env = gym.make('Tennis-v0')
#Record the game
env = gym.wrappers.Monitor(env, 'recording', force=True)
env.reset()
for _ in range(5000):
    env.render()
    action = env.action_space.sample() 
    next_state, reward, done, info = env.step(action)
    if done:
        break
env.close() 

一旦这一局结束,我们将看到一个名为recording的新目录,在这个目录中,我们可以找到 MP4 格式的视频文件,其中记录了我们代理的游戏过程,如图 2.21所示:

图 2.21:网球游戏玩法

其他环境

除了我们讨论过的经典控制和 Atari 游戏环境外,Gym 还提供了几种不同类别的环境。让我们了解更多关于它们的信息。

Box2D

Box2D 是一个二维模拟器,主要用于训练我们的代理执行连续控制任务,如行走。例如,Gym 提供了一个名为BipedalWalker-v2的 Box2D 环境,我们可以用它来训练代理行走。BipedalWalker-v2环境如图 2.22所示:

图 2.22:双足行走者环境

我们可以在这里查看 Gym 提供的其他 Box2D 环境:gym.openai.com/envs/#box2d

MuJoCo

MuJoCo代表带接触的多关节动力学,是用于训练代理执行连续控制任务的最流行模拟器之一。例如,MuJoCo 提供了一个有趣的环境,名为HumanoidStandup-v2,我们可以用它来训练代理站立。HumanoidStandup-v2环境如图 2.23所示:

图 2.23:类人站立环境

我们可以在这里查看 Gym 提供的其他 MuJoCo 环境:gym.openai.com/envs/#mujoco

机器人学

Gym 提供了几个用于执行目标任务的环境,适用于 fetch 和 shadow hand 机器人。例如,Gym 提供了一个名为 HandManipulateBlock-v0 的环境,我们可以用它来训练我们的智能体,通过机器人手操作一个盒子。HandManipulateBlock-v0 环境如 图 2.24 所示:

图 2.24:手部操作块环境

我们可以在这里查看 Gym 提供的多个机器人环境:gym.openai.com/envs/#robotics

玩具文本

玩具文本是最简单的基于文本的环境。我们已经在本章开头了解了其中一个环境,即 Frozen Lake 环境。我们可以在这里查看 Gym 提供的其他有趣的玩具文本环境:gym.openai.com/envs/#toy_text

算法

我们能否让我们的强化学习(RL)智能体解决一些有趣的问题,而不仅仅是玩游戏?当然可以!算法环境提供了多个有趣的问题,比如复制给定的序列、执行加法等。我们可以利用 RL 智能体来解决这些问题,学习如何进行计算。例如,Gym 提供了一个名为 ReversedAddition-v0 的环境,我们可以用它来训练我们的智能体加多位数。

我们可以在这里查看 Gym 提供的算法环境:gym.openai.com/envs/#algorithmic

环境简介

我们已经了解了几种类型的 Gym 环境。如果我们能在一个地方查看所有环境的信息,那该多好!没问题!Gym 的 Wiki 提供了所有环境的描述,包括环境 ID、状态空间、动作空间和奖励范围,以表格的形式呈现:github.com/openai/gym/wiki/Table-of-environments

我们还可以使用 registry.all() 方法查看 Gym 中所有可用的环境:

from gym import envs
print(envs.registry.all()) 

上述代码将打印出 Gym 中所有可用的环境。

因此,在本章中,我们了解了 Gym 工具包以及 Gym 提供的几个有趣的环境。在接下来的章节中,我们将学习如何在 Gym 环境中训练我们的 RL 智能体,以找到最优策略。

总结

本章开始时,我们通过安装 Anaconda 和 Gym 工具包来理解如何设置我们的机器。我们学习了如何使用 gym.make() 函数创建一个 Gym 环境。随后,我们还探索了如何使用 env.observation_space 获取环境的状态空间,以及使用 env.action_space 获取环境的动作空间。接着,我们学习了如何使用 env.P 获取环境的转移概率和奖励函数。之后,我们还学习了如何使用 Gym 环境生成一个回合。我们明白了在每个回合的步骤中,我们通过 env.step() 函数选择一个动作。

我们了解了 Gym 环境中的经典控制方法。我们学习了经典控制环境的连续状态空间,以及它们是如何存储在数组中的。我们还学习了如何使用一个随机智能体来平衡一个杆子。随后,我们了解了有趣的 Atari 游戏环境,学习了 Atari 游戏环境在 Gym 中的命名方式,然后我们探索了它们的状态空间和动作空间。我们还学习了如何使用包装类记录智能体的游戏过程,并在本章结束时,发现了 Gym 提供的其他环境。

在下一章中,我们将学习如何使用两种有趣的算法,称为值迭代和策略迭代,来找到最优策略。

问题

让我们通过回答以下问题来评估我们新获得的知识:

  1. Gym 工具包的用途是什么?

  2. 我们如何在 Gym 中创建一个环境?

  3. 我们如何获取 Gym 环境的动作空间?

  4. 我们如何可视化 Gym 环境?

  5. 列举一些 Gym 提供的经典控制环境。

  6. 我们如何使用 Gym 环境生成一个回合?

  7. Atari Gym 环境的状态空间是什么?

  8. 我们如何记录智能体的游戏过程?

进一步阅读

查看以下资源以获取更多信息:

第三章:贝尔曼方程和动态规划

在上一章中,我们学习到,在强化学习中,我们的目标是找到最优策略。最优策略是在每个状态下选择正确的动作,使得智能体可以获得最大回报并实现其目标。在本章中,我们将学习两种有趣的经典强化学习算法——值迭代和策略迭代方法,我们可以使用它们来找到最优策略。

在直接探讨值迭代和策略迭代方法之前,我们首先将学习贝尔曼方程。贝尔曼方程在强化学习中无处不在,广泛用于求解最优值函数和 Q 函数。我们将理解贝尔曼方程是什么,以及它如何找到最优值函数和 Q 函数。

在理解贝尔曼方程之后,我们将学习两种有趣的动态规划方法——值迭代和策略迭代,它们使用贝尔曼方程来寻找最优策略。在本章结束时,我们将学习如何通过使用值迭代和策略迭代方法来找到最优策略,解决冻结湖问题。

在本章中,我们将学习以下主题:

  • 贝尔曼方程

  • 贝尔曼最优性方程

  • 值函数和 Q 函数之间的关系

  • 动态规划 – 值迭代和策略迭代方法

  • 使用值迭代和策略迭代方法解决冻结湖问题

贝尔曼方程

贝尔曼方程以理查德·贝尔曼(Richard Bellman)命名,帮助我们解决马尔可夫决策过程MDP)。当我们说解决 MDP 时,我们指的是找到最优策略。

如本章引言中所述,贝尔曼方程在强化学习中无处不在,广泛用于递归地求解最优值函数和 Q 函数。计算最优值函数和 Q 函数非常重要,因为一旦我们得到了最优值函数或最优 Q 函数,我们就可以利用它们推导出最优策略。

在本节中,我们将学习贝尔曼方程究竟是什么,以及如何使用它来找到最优值函数和 Q 函数。

值函数的贝尔曼方程

贝尔曼方程指出,状态的值可以通过即时奖励与下一个状态的折扣值之和来获得。假设我们在状态 s 执行动作 a,并移动到下一个状态 并获得奖励 r,那么值函数的贝尔曼方程可以表示为:

在上述方程中,以下内容适用:

  • 表示在状态 s 执行动作 a 并移动到下一个状态时所获得的即时奖励

  • 是折扣因子

  • 表示下一个状态的值

让我们通过一个例子来理解贝尔曼方程。假设我们使用某个策略 生成一条轨迹

图 3.1:轨迹

假设我们需要计算状态 s[2] 的值。根据贝尔曼方程,状态 s[2] 的值表示为:

在上述方程中, 表示我们在状态 s[2] 中执行动作 a[2] 并移动到状态 s[3] 时获得的即时奖励。从轨迹中可以看出,即时奖励 r[2]。而项 是下一个状态的折扣值。

因此,根据贝尔曼方程,状态 s[2] 的值表示为:

因此,价值函数的贝尔曼方程可以表示为:

其中,上标 表示我们正在使用策略 。右侧项 通常被称为 贝尔曼备份

上述贝尔曼方程仅在我们拥有确定性环境时有效。假设我们的环境是随机的,那么在这种情况下,当我们在状态 s 中执行动作 a 时,并不能保证我们的下一个状态总是 ,它也可能是其他状态。例如,看看 图 3.2 中的轨迹。

如我们所见,当我们在状态 s[1] 中执行动作 a[1] 时,以 0.7 的概率到达状态 s[2],以 0.3 的概率到达状态 s[3]:

图 3.2:在状态 s[1] 中执行动作 a[1] 的转移概率

因此,当我们在状态 s[1] 中执行动作 a[1] 时,下一个状态为 s[2] 的概率为 70%,为 s[3] 的概率为 30%。我们了解到,贝尔曼方程是即时奖励和下一个状态的折扣值之和。但是,当由于环境中的随机性,无法保证下一个状态时,我们该如何定义贝尔曼方程呢?

在这种情况下,我们可以稍微修改贝尔曼方程,引入期望值(加权平均),也就是说,将贝尔曼备份与下一个状态的转移概率相乘,再求和:

在上述方程中,以下内容适用:

  • 表示通过在状态 s 中执行动作 a 到达 的转移概率

  • 表示贝尔曼备份

让我们通过考虑刚才使用的相同轨迹来更好地理解这个方程。正如我们所注意到的,当我们在状态 s[1] 中执行动作 a[1] 时,我们以 0.70 的概率到达 s[2],以 0.30 的概率到达 s[3]。因此,我们可以写成:

因此,包含环境中的随机性,利用期望(加权平均)的价值函数的贝尔曼方程表达为:

好的,但如果我们的策略是一个随机策略呢?我们了解到,使用随机策略时,我们是基于概率分布来选择动作;也就是说,在某个状态下,不是执行相同的动作,而是根据动作空间中的概率分布选择一个动作。让我们通过一个不同的轨迹来理解这一点,如图 3.3所示。正如我们所见,在状态s[1]下,以 0.8 的概率选择动作a[1],并到达状态s[2],以 0.2 的概率选择动作a[2],并到达状态s[3]:

图 3.3:使用随机策略的轨迹

因此,当我们使用随机策略时,下一状态将不再是固定的,它会是多个状态中的某一个,且具有一定的概率。那么,如何定义包括随机策略的贝尔曼方程呢?

  • 我们了解到,要将环境中的随机性包含进贝尔曼方程中,我们采取了期望(加权平均),也就是说,贝尔曼备份的总和乘以下一个状态的对应转移概率。

  • 类似地,要将策略的随机性包含在贝尔曼方程中,我们可以使用期望(加权平均),也就是说,将贝尔曼备份乘以相应的动作概率的总和。

因此,我们最终的价值函数贝尔曼方程可以写为:

上述方程也被称为贝尔曼期望方程,是价值函数的贝尔曼期望方程。我们也可以将上面的方程表达为期望形式。让我们回顾一下期望的定义:

在方程(1)中,分别表示随机环境和随机策略的概率。

因此,我们可以将价值函数的贝尔曼方程写为:

Q 函数的贝尔曼方程

现在,让我们学习如何计算状态-动作价值函数的贝尔曼方程,也就是 Q 函数。Q 函数的贝尔曼方程与价值函数的贝尔曼方程非常相似,除了一个小的区别。与价值函数的贝尔曼方程类似,Q 函数的贝尔曼方程指出,某个状态-动作对的 Q 值可以通过即时奖励与下一个状态-动作对的折扣 Q 值的总和得到:

在上述方程中,适用以下内容:

  • 表示在状态s下执行动作a并移动到下一个状态时获得的即时奖励!

  • 是折扣因子。

  • 是下一个状态-动作对的 Q 值

让我们通过一个例子来理解这个问题。假设我们使用某个策略 生成一个轨迹 ,如图3.4所示:

图 3.4:轨迹

假设我们需要计算状态-动作对(s[2], a[2])的 Q 值。那么,根据 Bellman 方程,我们可以写出:

在上述方程中,R(s[2], a[2], a[3])表示我们在执行动作a[2]时,从状态s[2]转移到状态s[3]所获得的即时奖励。从前面的轨迹中,我们可以看出,即时奖励R(s[2], a[2], s[3])是r[2]。而项 表示下一个状态-动作对的折扣 Q 值。因此:

因此,Q 函数的 Bellman 方程可以表示为:

其中,上标 表示我们正在使用策略 ,右侧项 Bellman 备份

类似于我们在值函数的 Bellman 方程中学到的内容,上述 Bellman 方程仅在我们拥有确定性环境时有效,因为在随机环境中我们的下一个状态不总是相同的,它将基于一个概率分布。假设我们有一个随机环境,那么当我们在状态s中执行动作a时,并不能保证我们的下一个状态总是 ;它也有可能是其他状态,且具有一定的概率。

所以,就像我们在上一节中所做的那样,我们可以使用期望(加权平均),即 Bellman 备份的和乘以下一个状态的相应转移概率,重新写出 Q 函数的 Bellman 方程:

类似地,当我们使用随机策略时,我们的下一个状态不一定总是相同的;它将是具有一定概率的不同状态。所以,为了包括策略的随机性,我们可以通过期望(加权平均)的方式重新写出 Bellman 方程,即 Bellman 备份的和乘以相应的动作概率,就像我们在值函数的 Bellman 方程中所做的那样。因此,Q 函数的 Bellman 方程可以表示为:

等等!上述方程有一个小变化。为什么在 Q 函数的情况下需要添加项?因为在值函数 V(s) 中,我们只给定一个状态 s,并根据策略选择一个动作 a。因此,我们添加了项来包括策略的随机性。但在 Q 函数 Q(s, a) 的情况下,我们既给定了状态 s,又给定了动作 a,因此我们不需要在方程中添加项,因为我们并不是根据策略来选择任何动作 a

然而,如果你查看上面的方程,我们需要根据策略选择动作,以计算下一个状态-动作对的 Q 值,因为不会给定。因此,我们可以将项放置在下一个状态-动作对的 Q 值之前。这样,我们的最终贝尔曼 Q 函数方程可以写为:

方程(3)也被称为 Q 函数的贝尔曼期望方程。我们还可以将方程(3)表示为期望形式:

现在我们已经理解了贝尔曼期望方程是什么,在下一节中,我们将学习贝尔曼最优性方程,并探讨它如何有助于找到最优的贝尔曼值和 Q 函数。

贝尔曼最优性方程

贝尔曼最优性方程给出了最优贝尔曼值和 Q 函数。首先,让我们看看最优贝尔曼值函数。我们已经学过,值函数的贝尔曼方程可以表示为:

在第一章中,我们学习了值函数依赖于策略,即状态的值根据我们选择的策略而变化。根据不同的策略,可能会有很多不同的值函数。最优值函数,,是相较于所有其他值函数,能够产生最大值的函数。同样,根据不同的策略,也可能会有许多不同的贝尔曼值函数。最优贝尔曼值函数是具有最大值的那个。

好的,如何计算具有最大值的最优贝尔曼值函数呢?

我们可以通过选择产生最大值的动作来计算最优贝尔曼值函数。但是我们不知道哪个动作能产生最大值,所以我们计算所有可能动作下的状态值,然后选择最大值作为该状态的值。

也就是说,我们不再使用某个策略 来选择行动,而是通过计算所有可能行动的状态值,然后选择最大值作为状态的值。由于我们没有使用任何策略,因此可以去掉对策略的期望 ,并对行动取最大值,将最优贝尔曼值函数表示为:

这与贝尔曼方程是一样的,只不过这里我们对所有可能的行动取最大值,而不是对策略取期望(加权平均),因为我们只关心最大值。让我们通过一个例子来理解这一点。假设我们在状态 s 中,并且该状态下有两个可能的行动。假设这两个行动分别是 0 和 1。那么 由以下公式给出:

从上述公式中可以观察到,我们使用所有可能的行动(0 和 1)来计算状态值,然后选择最大值作为状态的值。

现在,让我们来看一下最优贝尔曼 Q 函数。我们已经学到,Q 函数的贝尔曼方程表示为:

就像我们在学习最优贝尔曼值函数时一样,我们不再通过策略来选择在下一个状态 的行动 而是选择在该状态 下所有可能的行动,并计算最大 Q 值。可以表示为:

让我们通过一个例子来理解这一点。假设我们在状态 s 中,并采取了一个行动 a。我们在状态 s 中执行行动 a,到达下一个状态 。我们需要计算下一个状态的 Q 值 。在状态 中可能有多种行动。假设我们在状态 中有两个行动 0 和 1,那么我们可以将最优贝尔曼 Q 函数写为:

因此,总结一下,值函数和 Q 函数的贝尔曼最优性方程是:

我们还可以展开期望,并将前面的贝尔曼最优性方程改写为:

值函数与 Q 函数之间的关系

让我们稍微绕一下,回顾一下我们在第一章《强化学习基础》中学到的值函数和 Q 函数。我们学到,状态的值(值函数)表示从该状态开始,按照某个策略 进行的期望回报:

同样,状态-行动对的 Q 值(Q 函数)表示从该状态-行动对开始,按照某个策略 进行的期望回报:

我们已经学到,最优值函数给出了最大的状态值:

最优 Q 函数给出了最大的状态-动作值(Q 值):

我们能否推导出最优值函数与最优 Q 函数之间的关系?我们知道,当我们从一个状态s开始时,最优值函数具有最大的期望回报,而最优 Q 函数在我们从状态s执行某个动作a时,具有最大的期望回报。因此,我们可以说,最优值函数是所有可能动作中最优 Q 值的最大值,可以表示为以下形式(即,我们可以从 Q 推导出 V):

好的,现在让我们回到贝尔曼方程。在继续之前,我们先回顾一下贝尔曼方程:

  • 贝尔曼期望方程的值函数和 Q 函数

  • 贝尔曼 最优性 方程的值函数和 Q 函数

我们学到的是,最优贝尔曼 Q 函数可以表示为:

如果我们有一个最优值函数 ,那么我们可以使用它来推导出之前的最优贝尔曼 Q 函数(即,我们可以从V推导出Q):

上述方程是强化学习中最有用的恒等式之一,我们将在接下来的章节中看到它如何帮助我们找到最优策略。

因此,总结一下,我们学到的是可以从Q推导出V

并从V推导出Q

将方程(8)代入方程(7),我们可以写成:

如我们所观察到的,我们刚刚获得了最优贝尔曼值函数。现在我们理解了贝尔曼方程以及值函数与 Q 函数之间的关系,接下来我们可以继续学习如何利用这些方程来找到最优策略。

动态规划

动态规划DP)是一种解决复杂问题的技术。在动态规划中,我们不是将复杂问题作为一个整体来解决,而是将问题分解为简单的子问题,然后对于每个子问题,我们计算并存储解决方案。如果出现相同的子问题,我们就不再重新计算,而是使用已计算的结果。因此,动态规划有助于大幅减少计算时间。它在计算机科学、数学、生物信息学等广泛领域都有应用。

现在,我们将学习两种使用动态规划来寻找最优策略的重要方法。这两种方法是:

  • 值迭代

  • 策略迭代

请注意,动态规划是一种基于模型的方法,这意味着它只有在已知环境的模型动态(转移概率)的情况下,才能帮助我们找到最优策略。如果我们没有模型动态,就无法应用动态规划方法。

接下来的部分将通过手动计算进行说明,为了更好的理解,建议用纸笔跟着一起做。

值迭代

在值迭代方法中,我们试图找到最优策略。我们学习到,最优策略是指示代理在每个状态下执行正确动作的策略。为了找到最优策略,首先需要计算出最优值函数,一旦得到了最优值函数,我们可以利用它推导出最优策略。那么,如何计算最优值函数呢?我们可以使用最优贝尔曼方程来计算值函数。我们学习到,根据贝尔曼最优性方程,最优值函数可以计算为:

值函数与 Q 函数之间的关系部分,我们学习了在给定值函数的情况下,如何推导 Q 函数:

将(10)代入(9),我们可以写为:

因此,我们可以通过仅对最优 Q 函数取最大值来计算最优值函数。因此,为了计算某一状态的值,我们需要计算所有状态-动作对的 Q 值。然后,我们选择最大的 Q 值作为该状态的值。

让我们通过一个例子来理解这一点。假设我们有两个状态,s[0]和s[1],在这些状态下我们有两个可能的动作;假设这些动作是 0 和 1。首先,我们计算所有可能状态-动作对的 Q 值。表 3.1显示了所有可能状态-动作对的 Q 值:

表 3.1:所有可能状态-动作对的 Q 值

然后,在每个状态中,我们选择最大的 Q 值作为该状态的最优值。因此,状态s[0]的值为 3,状态s[1]的值为 4。状态的最优值(值函数)如表 3.2所示:

表 3.2:最优状态值

一旦我们获得了最优值函数,就可以使用它来提取最优策略。

现在,我们对值迭代方法如何找到最优值函数有了基本的理解。在下一部分,我们将详细了解值迭代方法是如何工作的,以及它如何从最优值函数中找到最优策略。

值迭代算法

值迭代的算法如下:

  1. 通过对 Q 函数取最大值来计算最优值函数,即!

  2. 从计算出的最优值函数中提取最优策略

让我们详细了解并学习上述两个步骤是如何工作的。为了更好的理解,我们手动执行值迭代。考虑图 3.5中显示的小型网格世界环境。假设我们处于状态A,我们的目标是到达状态C,并且不经过阴影状态B,假设我们有两个动作,0——左/右,和 1——上/下:

图 3.5:网格世界环境

你能想出这里的最优策略是什么吗?这里的最优策略是告诉我们在状态A执行动作 1,这样我们就可以到达C而不经过B。现在我们将看到如何使用值迭代找到这个最优策略。

表 3.3 显示了状态A的模型动态:

表 3.3:状态 A 的模型动态

步骤 1 – 计算最优值函数

我们可以通过计算 Q 函数的最大值来计算最优值函数。

也就是说,我们计算所有状态-动作对的 Q 值,然后选择最大 Q 值作为状态的值。

状态 s 和动作 a 的 Q 值可以计算为:

为了简化符号,我们可以将 表示为 ,将 表示为 ,并将前面的公式重写为:

因此,使用前面的公式,我们可以计算 Q 函数。如果你查看这个公式,要计算 Q 函数,我们需要过渡概率 、奖励函数 以及下一个状态的值 。模型动态为我们提供了过渡概率 和奖励函数 。但是下一个状态的值 呢?我们还不知道任何状态的值。所以,我们将用随机值或零初始化值函数(状态值),如表 3.4所示,并计算 Q 函数。

表 3.4:初始值表

迭代 1

让我们计算状态A的 Q 值。状态A有两个动作,分别是 0 和 1。因此,首先让我们计算状态A和动作 0 的 Q 值(注意,我们在整个章节中都使用折扣因子 ):

现在,让我们计算状态A和动作 1 的 Q 值:

在计算了状态A中两个动作的 Q 值之后,我们可以更新 Q 表,如表 3.5所示:

表 3.5:Q 表

我们了解到,状态的最优值就是 Q 函数的最大值。也就是说,。通过查看表 3.5,我们可以说,状态A的值,V(A),是 Q(A, 1),因为 Q(A, 1) 的值比 Q(A, 0) 高。因此,V(A) = 0.9。

我们可以在我们的值表中更新状态A的值,如表 3.6所示:

表 3.6:更新后的值表

类似地,为了计算状态B的值V(B),我们计算Q(B, 0)和Q(B, 1)的 Q 值,并选择最高的 Q 值作为状态B的值。 同样地,为了计算其他状态的值,我们计算所有状态-动作对的 Q 值,并选择最大的 Q 值作为状态的值。

在计算所有状态的值之后,我们的更新后的值表可能类似于表 3.7。 这是第一次迭代的结果:

表 3.7: 第 1 次迭代的值表

然而,作为第一次迭代的结果得到的值函数(值表)在不是最优的。 但为什么? 我们了解到最优值函数是最优 Q 函数的最大值。 也就是说,。 因此,为了找到最优值函数,我们需要最优 Q 函数。 但是 Q 函数可能在第一次迭代中并不是最优的,因为我们是基于随机初始化的状态值计算 Q 函数的。

正如下文所示,当我们开始计算 Q 函数时,我们使用了随机初始化的状态值。

因此,在下一次迭代中,在计算 Q 函数时,我们可以使用作为第一次迭代结果得到的更新的状态值。

换句话说,在第二次迭代中,为了计算值函数,我们计算所有状态-动作对的 Q 值,并选择最大的 Q 值作为状态的值。 为了计算 Q 值,我们需要知道状态值,在第一次迭代中,我们使用随机初始化的状态值。 但在第二次迭代中,我们使用从第一次迭代得到的更新的状态值(值表)如下所示:

第 2 次迭代

让我们计算状态A的 Q 值。 请记住,在计算 Q 值时,我们使用来自上一次迭代的更新的状态值。

首先,让我们计算状态A和动作 0 的 Q 值:

现在,让我们计算状态A和动作 1 的 Q 值:

如我们所见,由于状态 A 中动作 1 的 Q 值高于动作 0,因此状态 A 的值变为 1.44。 类似地,我们计算所有状态的值并更新值表。 表 3.8 显示了更新后的值表:

表 3.8: 第 2 次迭代的值表

第 3 次迭代

我们重复前一次迭代中看到的相同步骤,并通过选择最大的 Q 值来计算所有状态的值。 请记住,在计算 Q 值时,我们使用来自上一次迭代的更新的状态值(值表)。 因此,我们使用第 2 次迭代中得到的更新的状态值来计算 Q 值。

表 3.9 显示作为第三次迭代结果得到的更新的状态值:

表 3.9: 第 3 次迭代的值表

所以,我们重复这些步骤进行多次迭代,直到找到最优值函数。但是我们怎么知道是否找到了最优值函数呢?当值函数(值表)在多次迭代中没有变化,或者变化非常小的时候,我们可以说我们已经达到了收敛,也就是说,我们找到了最优值函数。

好的,我们如何判断值表是否发生变化,或者与上次迭代相比没有变化呢?我们可以计算当前迭代得到的值表与上次迭代得到的值表之间的差异。如果差异非常小——例如,差异小于一个非常小的阈值——那么我们可以说我们已经达到了收敛,因为值函数变化不大。

例如,假设表 3.10显示的是迭代 4得到的值表:

表 3.10:迭代 4 得到的值表

如我们所见,迭代 4 和迭代 3 得到的值表之间的差异非常小。所以,我们可以说我们已经达到了收敛,并且我们将迭代 4 得到的值表作为我们的最优值函数。请注意,上面的例子只是为了更好地理解;在实际操作中,我们不可能只通过四次迭代就达到收敛——通常需要多次迭代。

现在我们已经找到了最优值函数,在接下来的步骤中,我们将使用这个最优值函数来提取最优策略。

步骤 2 – 从步骤 1 中获得的最优值函数中提取最优策略

作为步骤 1的结果,我们得到了最优值函数:

表 3.11:最优值表(值函数)

现在,我们如何从获得的最优值函数中提取最优策略呢?

我们通常使用 Q 函数来计算策略。我们知道 Q 函数为每个状态-动作对提供 Q 值。一旦我们得到了所有状态-动作对的 Q 值,我们就可以通过在每个状态中选择具有最大 Q 值的动作来提取策略。例如,考虑表 3.12中的 Q 表。它显示了所有状态-动作对的 Q 值。现在,我们可以通过选择状态s[0]中的动作 1 和状态s[1]中的动作 0(因为它们具有最大的 Q 值)来从 Q 函数(Q 表)中提取策略。

表 3.12:Q 表

好的,现在我们使用步骤 1中获得的最优值函数来计算 Q 函数。一旦我们得到 Q 函数,我们就可以通过选择每个状态中具有最大 Q 值的动作来提取策略。由于我们是使用最优值函数来计算 Q 函数的,因此从 Q 函数中提取的策略将是最优策略。

我们了解到,Q 函数可以通过以下方式计算:

现在,在计算 Q 值时,我们使用从步骤 1 获得的最优价值函数。在计算 Q 函数后,我们可以通过选择具有最大 Q 值的动作来提取最优策略:

例如,我们可以使用最优价值函数计算状态 A 中所有动作的 Q 值。在状态 A 中,动作 0 的 Q 值计算如下:

在状态 A 中,动作 1 的 Q 值计算如下:

由于 Q(A, 1) 高于 Q(A, 0),因此我们的最优策略会在状态 A 中选择动作 1 作为最优动作。表 3.13 显示了使用最优价值函数计算所有状态-动作对的 Q 值后的 Q 表:

表 3.13:Q 表

从这个 Q 表中,我们选择每个状态中 Q 值最大的动作作为最优策略。因此,我们的最优策略会在状态 A 中选择动作 1,在状态 B 中选择动作 1,在状态 C 中选择动作 1。

因此,根据我们的最优策略,如果我们在状态 A 执行动作 1,我们可以到达状态 C,而不经过状态 B

在本节中,我们学习了如何使用价值迭代法计算最优策略。在下一节中,我们将学习如何使用 Gym 工具包在冰湖环境中实现价值迭代法来计算最优策略。

使用价值迭代法解决冰湖问题

在上一章中,我们了解了冰湖环境。冰湖环境如图 3.6所示:

图 3.6:冰湖环境

让我们回顾一下冰湖环境。在图 3.6所示的冰湖环境中,以下内容适用:

  • S 表示起始状态

  • F 表示冻结状态

  • H 表示洞穴状态

  • G 表示目标状态

我们了解到,在冰湖环境中,我们的目标是从起始状态 S 到达目标状态 G,同时避免经过洞穴状态 H。也就是说,在尝试从起始状态 S 到达目标状态 G 时,如果代理访问了洞穴状态 H,它就会掉进洞里并死亡,如图 3.7所示:

图 3.7:代理掉进洞里

因此,我们希望代理避免洞穴状态 H,以到达目标状态 G,如以下所示:

图 3.8:代理到达目标状态

我们如何实现这个目标呢?也就是说,如何从 S 状态到达 G 状态,而不经过 H 状态?我们了解到,最优策略会告诉代理在每个状态中执行正确的动作。因此,如果我们找到最优策略,就能从 S 到达 G,而不经过 H。好吧,我们如何找到最优策略?我们可以使用刚才学习的价值迭代法来找到最优策略。

请记住,所有的状态(SG)将在 Gym 工具包中从 0 到 16 编码,所有四个动作————将在 Gym 工具包中从 0 到 3 编码。

在这一部分中,我们将学习如何使用值迭代方法找到最优策略,使得代理可以从 S 状态到达 G 状态,而不会经过 H

首先,让我们导入必要的库:

import gym
import numpy as np 

现在,让我们使用 Gym 创建 Frozen Lake 环境:

env = gym.make('FrozenLake-v0') 

让我们使用 render 函数查看 Frozen Lake 环境:

env.render() 

上述代码将显示:

图 3.9:Gym 冻湖环境

如我们所见,代理处于状态 S,并且必须到达状态 G,而不能经过 H 状态。所以,让我们学习如何使用值迭代方法计算最优策略。

在值迭代方法中,我们执行两个步骤:

  1. 通过对 Q 函数取最大值来计算最优值函数,也就是

  2. 从计算得到的最优值函数中提取最优策略

首先,让我们学习如何计算最优值函数,然后我们将看到如何从计算得到的最优值函数中提取最优策略。

计算最优值函数

我们将定义一个名为 value_iteration 的函数,在这个函数中,我们通过对 Q 函数取最大值来迭代计算最优值函数,也就是 。为了更好地理解,我们将仔细查看该函数的每一行代码,最后再看完整的函数,这将更有助于理解。

定义 value_iteration 函数,它以环境作为参数:

def value_iteration(env): 

设置迭代次数:

 num_iterations = 1000 

设置用于检查值函数收敛的阈值:

 threshold = 1e-20 

我们还将折扣因子 设置为 1:

 gamma = 1.0 

现在,我们将通过将所有状态的值初始化为零来初始化值表:

 value_table = np.zeros(env.observation_space.n) 

对于每次迭代:

 for i in range(num_iterations): 

更新值表,也就是说,我们在每次迭代时使用上一迭代中的更新值表(状态值):

 updated_value_table = np.copy(value_table) 

现在,我们通过取 Q 值的最大值来计算值函数(状态值):

其中

因此,对于每个状态,我们计算该状态下所有动作的 Q 值,然后将状态的值更新为具有最大 Q 值的值:

 for s in range(env.observation_space.n): 

计算所有动作的 Q 值,

 Q_values = [sum([prob*(r + gamma * updated_value_table[s_])
                             for prob, s_, r, _ in env.P[s][a]]) 
                                   for a in range(env.action_space.n)] 

将状态的值更新为最大 Q 值,

 value_table[s] = max(Q_values) 

计算完值表,也就是所有状态的值后,我们检查当前迭代中获得的值表与前一次迭代中的值表之间的差异是否小于或等于阈值。如果差异小于阈值,我们就跳出循环并返回值表作为我们的最优值函数,如下代码所示:

 if (np.sum(np.fabs(updated_value_table - value_table)) <= threshold):
             break

    return value_table 
value_iteration function is shown to provide more clarity:
def value_iteration(env):

    num_iterations = 1000
    threshold = 1e-20
    gamma = 1.0    

    value_table = np.zeros(env.observation_space.n)

    for i in range(num_iterations):
        updated_value_table = np.copy(value_table) 

        for s in range(env.observation_space.n):

            Q_values = [sum([prob*(r + gamma * updated_value_table[s_])
                             for prob, s_, r, _ in env.P[s][a]])
                                   for a in range(env.action_space.n)]

            value_table[s] = max(Q_values)

        if (np.sum(np.fabs(updated_value_table - value_table)) <= threshold):
             break

    return value_table 

现在我们已经通过取 Q 值的最大值计算了最优值函数,让我们看看如何从最优值函数中提取最优策略。

从最优值函数中提取最优策略

在前一步中,我们计算了最优值函数。现在,让我们看看如何从计算出的最优值函数中提取最优策略。

首先,我们定义一个名为extract_policy的函数,接收value_table作为参数:

def extract_policy(value_table): 

将折扣因子 设置为 1:

 gamma = 1.0 

首先,我们将策略初始化为零,也就是将所有状态的动作都设置为零:

 policy = np.zeros(env.observation_space.n) 

现在,我们使用从前一步获得的最优值函数来计算 Q 函数。我们了解到 Q 函数可以这样计算:

在计算了 Q 函数后,我们可以通过选择具有最大 Q 值的动作来提取策略。由于我们使用最优值函数来计算 Q 函数,从 Q 函数提取出的策略将是最优策略。

如以下代码所示,对于每个状态,我们计算该状态下所有动作的 Q 值,然后通过选择具有最大 Q 值的动作来提取策略。

对于每个状态:

 for s in range(env.observation_space.n): 

计算该状态下所有动作的 Q 值,

 Q_values = [sum([prob*(r + gamma * value_table[s_])
                             for prob, s_, r, _ in env.P[s][a]])
                                   for a in range(env.action_space.n)] 

通过选择具有最大 Q 值的动作来提取策略,

 policy[s] = np.argmax(np.array(Q_values))

    return policy 
extract_policy function is shown here to give us more clarity: 
def extract_policy(value_table):
    gamma = 1.0

    policy = np.zeros(env.observation_space.n) 

    for s in range(env.observation_space.n):

        Q_values = [sum([prob*(r + gamma * value_table[s_])
                             for prob, s_, r, _ in env.P[s][a]]) 
                                   for a in range(env.action_space.n)]

        policy[s] = np.argmax(np.array(Q_values)) 

    return policy 

就是这样!现在,我们将看看如何在我们的 Frozen Lake 环境中提取最优策略。

将所有内容结合起来

我们了解到,在 Frozen Lake 环境中,我们的目标是找到最优策略,在每个状态下选择正确的动作,以便我们能够从状态A到达状态G,而不经过陷阱状态。

首先,我们通过传递我们的 Frozen Lake 环境作为参数,使用我们的value_iteration函数计算最优值函数:

optimal_value_function = value_iteration(env) 

接下来,我们使用我们的extract_policy函数从最优值函数中提取最优策略:

optimal_policy = extract_policy(optimal_value_function) 

我们可以打印出获得的最优策略:

print(optimal_policy) 

上述代码将打印出以下内容。正如我们所观察到的,我们的最优策略告诉我们在每个状态下执行正确的动作:

[0\. 3\. 3\. 3\. 0\. 0\. 0\. 0\. 3\. 1\. 0\. 0\. 0\. 2\. 1\. 0.] 

现在我们已经了解了什么是值迭代,以及如何执行值迭代方法来计算我们 Frozen Lake 环境中的最优策略,在下一部分,我们将学习另一个有趣的方法,叫做策略迭代。

策略迭代

在值迭代方法中,我们首先通过迭代地在 Q 函数(Q 值)上取最大值来计算最优值函数。一旦我们找到了最优值函数,就可以使用它来提取最优策略。而在策略迭代中,我们尝试通过迭代地使用策略来计算最优值函数,一旦找到了最优值函数,就可以用它来提取最优策略。

首先,让我们学习如何使用策略计算价值函数。假设我们有一个策略 ,我们如何使用这个策略 来计算价值函数呢?在这里,我们可以使用贝尔曼方程。我们知道,根据贝尔曼方程,我们可以使用策略 来计算价值函数,如下所示:

假设我们的策略是一个确定性策略,因此我们可以从前面的方程中去掉项 ,因为策略中没有随机性,并将我们的贝尔曼方程改写为:

为了简化符号,我们可以将 记作 ,将 记作 ,并将前面的方程改写为:

因此,通过使用上述方程,我们可以使用策略来计算价值函数。我们的目标是找到最优价值函数,因为一旦我们找到了最优价值函数,我们就可以用它来提取最优策略。

我们不会被提供任何策略作为输入。因此,我们将初始化一个随机策略,并使用这个随机策略来计算价值函数。然后,我们检查计算得到的价值函数是否是最优的。由于它是基于随机策略计算的,它将不是最优的。

所以,我们将从计算得到的价值函数中提取一个新的策略,然后我们将使用提取的新的策略来计算新的价值函数,再检查新的价值函数是否最优。如果它是最优的,我们就停止,否则我们将在一系列迭代中重复这些步骤。为了更好地理解,参见以下步骤:

迭代 1:让 为随机策略。我们使用这个随机策略来计算价值函数 。由于是基于随机策略计算的,我们的价值函数将不是最优的。因此,从 中,我们提取出一个新的策略

迭代 2:现在,我们使用从上一轮迭代中得到的新策略 来计算新的价值函数 ,然后检查 是否是最优的。如果它是最优的,我们就停止,否则从这个价值函数 中,我们提取出一个新的策略

迭代 3:现在,我们使用从上一轮迭代中得到的新策略 来计算新的价值函数 ,然后检查 是否是最优的。如果它是最优的,我们就停止,否则从这个价值函数 中,我们提取出一个新的策略

我们重复这个过程多次迭代,直到我们找到最优价值函数 ,如下所示:

上述步骤称为策略评估与改进。策略评估意味着在每一步中,我们通过检查使用该策略计算的价值函数是否最优来评估策略。策略改进意味着在每一步中,我们找到新的改进策略以计算最优价值函数。

一旦我们找到了最优的价值函数 ,那么这意味着我们也找到了最优策略。也就是说,如果 是最优的,那么用于计算 的策略将是最优策略。

为了更好地理解策略迭代是如何工作的,让我们通过以下伪代码步骤进行分析。在第一次迭代中,我们将初始化一个随机策略并用它来计算价值函数:

policy = random_policy
value_function = compute_value_function(policy) 

由于我们是使用随机策略计算的价值函数,因此计算出来的价值函数并非最优。因此,我们需要找到一个新的策略,用它来计算最优的价值函数。

因此,我们从使用随机策略计算得到的价值函数中提取出一个新的策略:

new_policy = extract_policy(value_function) 

现在,我们将使用这个新策略来计算新的价值函数:

policy = new_policy 
value_function = compute_value_function(policy) 

如果新的价值函数是最优的,我们就停止,否则我们将重复前面的步骤多次,直到找到最优价值函数。以下伪代码可以帮助我们更好地理解:

policy = random_policy
for i in range(num_iterations): 
    value_function = compute_value_function(policy)
    new_policy = extract_policy(value_function)
    if value_function = optimal:
        break
    else:
        policy = new_policy 

等等!我们怎么判断我们的价值函数是最优的呢?如果价值函数在迭代过程中不再变化,那么我们可以说我们的价值函数是最优的。那么,如何检查价值函数在迭代过程中没有发生变化呢?

我们知道,价值函数是使用策略计算的。如果策略在迭代过程中没有变化,那么我们的价值函数在迭代过程中也不会变化。因此,当策略在迭代过程中不再变化时,我们可以说我们找到了最优的价值函数。

因此,在一系列迭代中,当策略和新策略变得相同时,我们可以说我们已经得到了最优的价值函数。以下是为了清晰起见给出的最终伪代码:

policy = random_policy
for i in range(num_iterations): 
    value_function = compute_value_function(policy)
    new_policy = extract_policy(value_function)
**if** **policy == new_policy:**
        break
    else:
        policy = new_policy 

因此,当策略不再变化时,也就是当策略和新策略变得相同时,我们可以说我们已经得到了最优的价值函数,而用于计算最优价值函数的策略就是最优策略。

请记住,在价值迭代方法中,我们通过对 Q 函数(Q 值)进行最大化迭代来计算最优价值函数,一旦我们找到了最优价值函数,就从中提取出最优策略。但在策略迭代方法中,我们是通过策略的迭代来计算最优价值函数,一旦我们找到了最优价值函数,那么用于计算最优价值函数的策略将是最优策略。

现在,我们已经基本理解了策略迭代方法的工作原理,接下来我们将进入细节,学习如何手动计算策略迭代。

算法 – 策略迭代

策略迭代算法的步骤如下:

  1. 初始化一个随机策略

  2. 使用给定策略计算价值函数

  3. 使用步骤 2 获得的价值函数提取新的策略

  4. 如果提取的策略与步骤 2 中使用的策略相同,则停止;否则,将提取的新的策略发送到步骤 2,并重复步骤 2 至步骤 4

现在,让我们深入了解具体细节,学习前述步骤是如何工作的。为了清楚理解,让我们手动执行策略迭代。我们使用在值迭代方法中相同的网格世界环境。假设我们处于状态A,目标是到达状态C,而不经过阴影状态B,假设我们有两个动作,0 – /和 1 – /

图 3.10:网格世界环境

我们知道,在上述环境中,最优策略是告诉我们在状态A执行动作 1,以便我们可以到达C而不经过B。现在,我们将看到如何通过策略迭代找到这个最优策略。

表 3.14展示了状态A的模型动态:

表 3.14:状态 A 的模型动态

步骤 1 – 初始化一个随机策略

首先,我们将初始化一个随机策略。如下所示,我们的随机策略指示我们在状态A执行动作 1,在状态B执行动作 0,在状态C执行动作 1:

步骤 2 – 使用给定的策略计算价值函数

这一步与我们在值迭代中计算价值函数的方式完全相同,但有一个小小的区别。在值迭代中,我们通过在 Q 函数上取最大值来计算价值函数。但在策略迭代中,我们将使用策略来计算价值函数。

为了更好地理解这一点,让我们快速回顾一下在值迭代中是如何计算价值函数的。在值迭代中,我们通过在最优 Q 函数上取最大值来计算最优价值函数,具体如下所示:

其中

在策略迭代中,我们使用策略 来计算价值函数,这与值迭代不同,后者是通过在 Q 函数上取最大值来计算价值函数。使用策略 计算的价值函数可以通过以下方式得到:

如果你看看前面的公式,要计算值函数,我们需要转移概率 、奖励函数 和下一个状态的值 。转移概率 和奖励函数 的值可以从模型动态中获得。那么,下一个状态的值 呢?我们还不知道任何状态的值。所以,我们将值函数(状态值)初始化为随机值或零,如图 3.15 所示,并计算值函数:

表 3.15:初始值表

第一次迭代

让我们计算状态A的值(请注意,这里我们只计算策略给定的动作的值,不像在值迭代中,我们计算了所有动作的 Q 值并选择最大值)。

因此,策略给定的状态A的动作是 1,我们可以计算状态A的值,如下所示(请注意,在本节中,我们使用了折扣因子 ):

同样,我们使用策略给定的动作来计算所有状态的值。表 3.16 显示了第一次迭代结果中更新后的状态值:

表 3.16:第一次迭代的值表

然而,表 3.16 中第一次迭代得到的值函数(值表)将不准确。也就是说,给定政策的状态值(值函数)将不准确。

请注意,与值迭代方法不同,这里我们并没有检查我们的值函数是否最优;我们只是检查我们的值函数是否根据给定的策略准确计算。

值函数将不准确,因为当我们开始使用给定的策略计算值函数时,我们使用了随机初始化的状态值:

因此,在下一次迭代中,在计算值函数时,我们将使用第一次迭代结果中得到的更新后的状态值:

第二次迭代

现在,在第二次迭代中,我们使用策略 来计算值函数。记住,在计算值函数时,我们将使用第一次迭代中得到的更新后的状态值(值表)。

例如,让我们计算状态 A 的值:

类似地,我们使用策略给定的动作来计算所有状态的值。表 3.17 显示了第二次迭代结果中更新后的状态值:

表 3.17:第二次迭代的值表

第三次迭代

同样,在第 3 次迭代中,我们使用策略计算价值函数,并在计算价值函数时,使用第 2 次迭代获得的更新后的状态值(价值表)。

表 3.18显示了通过第三次迭代更新后的状态值:

表 3.18:第 3 次迭代的价值表

我们对这个过程进行多次迭代,直到价值表不再变化或变化非常小。例如,假设表 3.19显示了第 4 次迭代得到的价值表:

表 3.19:第 4 次迭代的价值表

如我们所见,从第 4 次迭代和第 3 次迭代获得的价值表之间的差异非常小。因此,我们可以说,价值表在多次迭代中变化不大,我们在此迭代中停止,并将其作为最终的价值函数。

第 3 步 – 使用上一阶段得到的价值函数提取新策略

通过第 2 步的结果,我们得到了价值函数,这是使用给定的随机策略计算的。然而,由于该价值函数是基于随机策略计算的,它并不是最优的。因此,我们将从上一阶段获得的价值函数中提取新策略。从上一阶段获得的价值函数(价值表)显示在表 3.20中:

表 3.20:上一阶段的价值表

好的,我们如何从价值函数中提取新策略呢?(提示:此步骤与如何在价值迭代方法的第 2 步中根据价值函数提取策略完全相同。)

为了提取新策略,我们使用从上一阶段获得的价值函数(价值表)计算 Q 函数。一旦计算出 Q 函数,我们就选择在每个状态中具有最大值的动作作为新策略。我们知道 Q 函数可以通过以下方式计算:

现在,在计算 Q 值时,我们使用从上一阶段获得的价值函数。

例如,让我们计算在状态A中所有动作的 Q 值,使用从上一阶段获得的价值函数。状态A中动作 0 的 Q 值计算为:

状态A中动作 1 的 Q 值计算为:

表 3.21显示了计算所有状态-动作对的 Q 值后的 Q 表:

表 3.21:Q 表

从这个Q表中,我们选择在每个状态中具有最大值的动作作为新策略。

第 4 步 – 检查新策略

现在我们将检查从第 3 步提取的新策略是否与我们在第 2 步中使用的策略相同。如果相同,我们就停止,否则我们将提取的新策略送回第 2 步并重复第 2 步第 4 步

因此,在本节中,我们学习了如何使用策略迭代法计算最优策略。在下一节中,我们将学习如何使用 Gym 工具包在 Frozen Lake 环境中实现策略迭代法以计算最优策略。

使用策略迭代法解决 Frozen Lake 问题

我们学到,在 Frozen Lake 环境中,我们的目标是从起始状态 S 到达目标状态 G,而不经过洞穴状态 H。现在,让我们学习如何在 Frozen Lake 环境中使用策略迭代法计算最优策略。

首先,让我们导入必要的库:

import gym
import numpy as np 

现在,让我们使用 Gym 创建 Frozen Lake 环境:

env = gym.make('FrozenLake-v0') 

我们学到,在策略迭代中,我们是通过迭代使用策略来计算价值函数。一旦我们找到了最优价值函数,那么用于计算最优价值函数的策略就是最优策略。

所以,首先,让我们学习如何使用策略计算价值函数。

使用策略计算价值函数

这一步与我们在价值迭代法中计算价值函数的方法完全相同,只是有一点小区别。这里,我们使用策略来计算价值函数,而在价值迭代法中,我们是通过取 Q 值的最大值来计算价值函数。现在,让我们学习如何定义一个函数,使用给定的策略来计算价值函数。

让我们定义一个名为 compute_value_function 的函数,它将策略作为参数:

def compute_value_function(policy): 

现在,让我们定义迭代次数:

 num_iterations = 1000 

定义阈值:

 threshold = 1e-20 

将折扣因子 的值设置为 1.0:

 gamma = 1.0 

现在,我们将通过将所有状态值初始化为零来初始化价值表:

 value_table = np.zeros(env.observation_space.n) 

对于每次迭代:

 for i in range(num_iterations): 

更新价值表;也就是说,我们学到了在每次迭代中,我们使用来自前一次迭代的更新后的价值表(状态值):

 updated_value_table = np.copy(value_table) 

现在,我们使用给定的策略计算价值函数。我们学到,价值函数可以根据某些策略 按如下方式计算:

因此,对于每个状态,我们根据策略选择动作,然后使用选择的动作更新状态的值,如下所示。

对于每个状态:

 for s in range(env.observation_space.n): 

根据策略在状态中选择动作:

 a = policy[s] 

使用选择的动作计算状态的值,

 value_table[s] = sum(
                [prob * (r + gamma * updated_value_table[s_])
                    for prob, s_, r, _ in env.P[s][a]]) 

在计算完价值表,也就是所有状态的值后,我们检查当前迭代中获得的价值表与前一次迭代之间的差值是否小于或等于阈值。如果小于或等于,我们就结束循环,并将价值表作为给定策略的准确价值函数返回:

 if (np.sum(np.fabs(updated_value_table - value_table)) <= threshold):
             break

    return value_table 

现在我们已经计算了策略的价值函数,让我们看看如何从价值函数中提取策略。

从价值函数中提取策略

这一步与在值迭代方法中从值函数中提取策略的方式完全相同。因此,类似于我们在值迭代方法中学到的那样,我们定义一个叫做extract_policy的函数,给定值函数来提取策略:

def extract_policy(value_table):

    gamma = 1.0
    policy = np.zeros(env.observation_space.n) 
    for s in range(env.observation_space.n):

        Q_values = [sum([prob*(r + gamma * value_table[s_])
                             for prob, s_, r, _ in env.P[s][a]]) 
                                   for a in range(env.action_space.n)] 

        policy[s] = np.argmax(np.array(Q_values)) 

    return policy 

综合起来

首先,让我们定义一个叫做policy_iteration的函数,该函数将环境作为参数:

def policy_iteration(env): 

设置迭代次数:

 num_iterations = 1000 

我们了解到,在策略迭代方法中,我们从初始化一个随机策略开始。所以,我们将初始化一个随机策略,在所有状态下选择动作 0:

 policy = np.zeros(env.observation_space.n) 

每次迭代:

 for i in range(num_iterations): 

使用策略计算值函数:

 value_function = compute_value_function(policy) 

从计算得到的值函数中提取新的策略:

 new_policy = extract_policy(value_function) 

如果policynew_policy相同,则退出循环:

 if (np.all(policy == new_policy)):
            break 

否则将当前的policy更新为new_policy

 policy = new_policy

    return policy 

现在,让我们学习如何在 Frozen Lake 环境中执行策略迭代并找到最优策略。所以,我们只需将 Frozen Lake 环境传递给我们的policy_iteration函数,如下所示,并获得最优策略:

optimal_policy = policy_iteration(env) 

我们可以打印出最优策略:

print(optimal_policy) 

上面的代码将输出以下内容:

array([0., 3., 3., 3., 0., 0., 0., 0., 3., 1., 0., 0., 0., 2., 1., 0.]) 

如我们所见,我们的最优策略告诉我们在每个状态下执行正确的动作。因此,我们学会了如何执行策略迭代方法来计算最优策略。

动态规划(DP)是否适用于所有环境?

在动态规划中,也就是在值迭代和策略迭代方法中,我们尝试找到最优策略。

值迭代:在值迭代方法中,我们通过对 Q 函数(Q 值)进行迭代计算最大值来得到最优值函数:

其中!。找到最优值函数后,我们从中提取最优策略。

策略迭代:在策略迭代方法中,我们通过迭代计算策略来得到最优值函数:

我们将从随机策略开始并计算值函数。一旦找到最优值函数,那么用来生成最优值函数的策略就是最优策略。

如果你看一下前面的两个方程,为了找到最优策略,我们计算值函数和 Q 函数。但为了计算值函数和 Q 函数,我们需要知道环境的转移概率 ,而当我们不知道环境的转移概率时,就无法计算值函数和 Q 函数来找到最优策略。

也就是说,动态规划是一种基于模型的方法,要应用此方法,我们需要知道环境的模型动态(转移概率),而当我们不知道模型动态时,就无法应用动态规划方法。

好的,当我们不知道环境的模型动态时,如何找到最优策略呢?在这种情况下,我们可以使用无模型方法。在下一章中,我们将学习一种有趣的无模型方法,叫做蒙特卡洛方法,看看它如何在不需要模型动态的情况下找到最优策略。

总结

我们通过理解价值函数和 Q 函数的贝尔曼方程开始本章内容。我们了解到,根据贝尔曼方程,一个状态的值是即时奖励与下一个状态的折扣值之和,而一个状态-动作对的值是即时奖励与下一个状态-动作对的折扣值之和。接着我们学习了最优贝尔曼值函数和 Q 函数,后者给出了最大值。

接下来,我们学习了价值函数与 Q 函数之间的关系。我们了解到,价值函数可以从 Q 函数中提取,公式为 ,然后我们学习了 Q 函数如何从价值函数中提取,公式为

后来我们学习了两种有趣的方法,分别是价值迭代和策略迭代,它们使用动态规划来找到最优策略。

在价值迭代方法中,首先我们通过对 Q 函数取最大值来迭代计算最优价值函数。找到最优价值函数后,我们使用它来提取最优策略。而在策略迭代方法中,我们尝试通过策略迭代计算最优价值函数。一旦找到最优价值函数,使用该价值函数生成的策略就会被提取为最优策略。

问题

让我们尝试回答以下问题,以评估我们对本章所学知识的掌握程度:

  1. 定义贝尔曼方程。

  2. 贝尔曼期望方程与贝尔曼最优方程有何区别?

  3. 我们如何从 Q 函数推导出价值函数?

  4. 我们如何从价值函数推导出 Q 函数?

  5. 价值迭代涉及哪些步骤?

  6. 策略迭代涉及哪些步骤?

  7. 策略迭代与价值迭代有什么不同?

第四章:蒙特卡罗方法

在上一章中,我们学习了如何使用两种有趣的动态规划方法:值迭代和策略迭代来计算最优策略。动态规划是一种基于模型的方法,需要环境的模型动态来计算价值和 Q 函数,从而找到最优策略。

但假设我们没有环境的模型动态。在这种情况下,我们如何计算价值和 Q 函数呢?这时我们就会使用无模型的方法。无模型方法不需要环境的模型动态来计算价值和 Q 函数,从而找到最优策略。一个常见的无模型方法就是蒙特卡罗MC)方法。

我们将从理解蒙特卡罗方法开始,然后介绍强化学习中两个重要的任务类型:预测任务和控制任务。随后,我们将学习蒙特卡罗方法如何在强化学习中应用,并了解它相比于上一章我们学习的动态规划方法有哪些优势。接下来,我们将了解蒙特卡罗预测方法及其不同类型,并学习如何使用蒙特卡罗预测方法训练智能体玩二十一点。

接下来,我们将学习蒙特卡罗控制方法以及不同类型的蒙特卡罗控制方法。之后,我们将学习如何使用蒙特卡罗控制方法训练智能体玩二十一点。

总结来说,在本章中,我们将学习以下内容:

  • 理解蒙特卡罗方法

  • 预测与控制任务

  • 蒙特卡罗预测方法

  • 使用蒙特卡罗预测方法玩二十一点

  • 蒙特卡罗控制方法

  • 使用蒙特卡罗控制方法玩二十一点

理解蒙特卡罗方法

在理解蒙特卡罗方法在强化学习中的应用之前,首先让我们了解蒙特卡罗方法是什么以及它是如何工作的。蒙特卡罗方法是一种统计技术,通过抽样来找到近似解。

例如,蒙特卡罗方法通过抽样来近似随机变量的期望值,当样本量增大时,近似效果会更好。假设我们有一个随机变量X,并且需要计算X的期望值;也就是E(X),那么我们可以通过将X的值乘以它们各自的概率并求和来计算,如下所示:

但我们能否用蒙特卡罗方法来近似计算期望值呢?可以!我们可以通过对X进行N次抽样,并计算X的平均值来估算X的期望值,具体如下:

N更大时,我们的近似值将更准确。因此,通过蒙特卡罗方法,我们可以通过采样来逼近解决方案,并且当样本量较大时,我们的近似值会更好。

在接下来的章节中,我们将学习蒙特卡罗方法在强化学习中是如何被使用的。

预测与控制任务

在强化学习中,我们执行两个重要的任务,它们是:

  • 预测任务

  • 控制任务

预测任务

在预测任务中,给定一个策略 作为输入,我们尝试使用给定策略预测值函数或 Q 函数。那么这样做有什么用呢?我们的目标是评估给定的策略。也就是说,我们需要判断给定策略是好是坏。我们如何判断呢?如果代理使用给定策略获得了良好的回报,那么我们可以说这个策略是好的。因此,为了评估给定的策略,我们需要了解代理使用该策略时能获得的回报。为了获得回报,我们通过给定策略预测值函数或 Q 函数。

也就是说,我们学到值函数或状态的值表示代理从该状态开始,遵循某个策略 所获得的期望回报。因此,通过使用给定策略 预测值函数,我们可以理解代理在每个状态下,如果使用给定策略 时所获得的期望回报。如果回报很好,那么我们可以说给定的策略是好的。

同样,我们已经学到,Q 函数或 Q 值表示代理从状态s和动作a开始,遵循策略 时获得的期望回报。因此,通过使用给定策略 预测 Q 函数,我们可以理解代理在每个状态-动作对中,如果使用给定策略时所获得的期望回报。如果回报很好,那么我们可以说给定的策略是好的。

因此,我们可以通过计算值函数和 Q 函数来评估给定策略

请注意,在预测任务中,我们不会对给定的输入策略做任何修改。我们保持给定的策略不变,并使用给定策略预测值函数或 Q 函数,得到期望的回报。基于期望回报,我们评估给定的策略。

控制任务

与预测任务不同,在控制任务中,我们不会获得任何策略作为输入。在控制任务中,我们的目标是找到最优策略。因此,我们将从初始化一个随机策略开始,并尝试迭代地找到最优策略。也就是说,我们尝试找到一个能提供最大回报的最优策略。

简而言之,在预测任务中,我们通过预测价值函数或 Q 函数来评估给定的输入策略,这有助于我们理解如果一个智能体使用给定的策略,它所获得的期望回报;而在控制任务中,我们的目标是找到最优策略,并且不会提供任何策略作为输入;因此,我们将从初始化一个随机策略开始,并且通过迭代的方式来寻找最优策略。

现在我们已经理解了预测和控制任务是什么,在下一节中,我们将学习如何使用蒙特卡罗方法执行预测和控制任务。

蒙特卡罗预测

在本节中,我们将学习如何使用蒙特卡罗方法来执行预测任务。我们已经学到,在预测任务中,我们会给定一个策略,然后通过预测价值函数或 Q 函数来评估该策略。首先,我们将学习如何使用蒙特卡罗方法预测给定策略的价值函数。接下来,我们将学习如何预测给定策略的 Q 函数。好了,让我们开始这一节。

为什么我们需要使用蒙特卡罗方法来预测给定策略的价值函数?为什么我们不能使用前一章学到的动态规划方法来预测价值函数呢?我们学到,要使用动态规划方法计算价值函数,我们需要知道模型的动态(转移概率),而当我们不知道模型动态时,就需要使用无模型方法。

蒙特卡罗方法是一种无模型方法,这意味着它不需要模型动态来计算价值函数。

首先,让我们回顾一下价值函数的定义。价值函数,或者说状态s的价值,可以定义为从状态s开始并遵循策略的智能体所获得的期望回报!。它可以表示为:

好的,我们如何使用蒙特卡罗方法来估计状态的价值(价值函数)呢?在本章开始时,我们了解到蒙特卡罗方法通过采样来近似随机变量的期望值,并且当样本量增大时,近似效果会更好。我们能否利用蒙特卡罗方法的这个概念来预测状态的价值呢?可以!

为了使用蒙特卡罗方法近似状态的价值,我们将按照给定策略!进行若干N次试验采样(轨迹),然后我们计算状态的价值,作为这些试验中状态的平均回报,它可以表示为:

从前面的方程中,我们可以理解,状态s的价值可以通过计算在若干N次试验中,状态s的平均回报来近似。当N增大时,我们的近似会更准确。

简而言之,在蒙特卡洛预测方法中,我们通过在N个回合中取一个状态的平均回报来近似该状态的值,而不是取期望回报。

好的,让我们通过一个例子更好地理解蒙特卡洛方法如何估计状态的值(价值函数)。我们以我们最喜欢的网格世界环境为例,该环境在第一章《强化学习基础》中已介绍,如图 4.1所示。我们的目标是从状态A到达状态I,而不经过阴影状态,代理在访问未阴影状态时获得+1 奖励,在访问阴影状态时获得-1 奖励:

图 4.1:网格世界环境

假设我们有一个随机策略 。假设在状态A中,我们的随机策略 80%的时间选择动作,20%的时间选择动作,并且在状态DE中选择动作,在状态BF中选择动作,且选择的概率为 100%。

首先,我们使用给定的随机策略 生成一个回合 ,如图 4.2所示:

图 4.2:回合

为了更好地理解,让我们只关注状态A。现在让我们计算状态A的回报。一个状态的回报是从该状态开始的轨迹上的奖励总和。因此,状态A的回报计算为 R1 = 1+1+1+1 = 4,其中R[1]中的下标 1 表示来自回合 1 的回报。

假设我们生成了另一集 ,使用与图 4.3所示相同的随机策略

图 4.3:回合

现在让我们计算状态A的回报。状态A的回报是 R2 = -1+1+1+1 = 2。

假设我们生成了另一集 ,使用与图 4.4所示相同的随机策略

图 4.4:回合

现在让我们计算状态A的回报。状态A的回报是 R3 = 1+1+1+1 = 4。

因此,我们生成了三个回合,并计算了状态A在所有三个回合中的回报。现在,我们如何计算状态A的值呢?我们了解到,在蒙特卡洛方法中,状态的值可以通过计算该状态在N个回合中的平均回报来近似(轨迹):

我们需要计算状态A的值,因此我们可以通过取状态AN个回合中的平均回报来计算它,如下所示:

我们生成了三个回合,因此:

因此,状态A的价值为 3.3。同样地,我们可以通过仅仅计算该状态在三个回合中的平均回报来计算所有其他状态的价值。

为了便于理解,在前面的例子中,我们只生成了三个回合。为了找到更好、更准确的状态价值估计,我们应该生成更多回合(而不仅仅是三个),并计算状态的平均回报作为该状态的价值。

因此,在蒙特卡洛预测方法中,使用给定的输入策略 来预测一个状态的价值(价值函数),我们通过给定的策略生成一些 N 个回合,然后计算该状态的价值作为这些 N 个回合中该状态的平均回报。

请注意,在计算状态的回报时,我们也可以包括折扣因子并计算折扣回报,但为了简化起见,我们这里不包括折扣因子。

现在,我们已经对蒙特卡洛预测方法如何预测给定策略的价值函数有了基本的了解,接下来让我们通过理解蒙特卡洛预测方法的算法来更详细地探讨这一方法。

MC 预测算法

蒙特卡洛预测算法如下所示:

  1. 让 total_return(s) 表示一个状态在多个回合中的回报总和,N(s) 表示计数器,即该状态在多个回合中被访问的次数。将 total_return(s) 和 N(s) 对所有状态初始化为零。策略 作为输入。

  2. 对于 M 次迭代:

    1. 使用策略 生成一个回合

    2. 将回合中获得的所有奖励存储在名为 rewards 的列表中

    3. 对于回合中的每一步 t

      1. 将状态 s[t] 的回报计算为 R(s[t]) = sum(rewards[t:])

      2. 更新状态 s[t] 的回报总和为 total_returns(s[t]) = total_return(s[t]) + R(s[t])

      3. 更新计数器为 N(s[t]) = N(s[t]) + 1

  3. 通过仅仅计算平均值来求一个状态的价值,即:

上述算法表明,状态的价值就是该状态在多个回合中的平均回报。

为了更好地理解前面的算法是如何工作的,我们通过一个简单的例子手动计算每个状态的价值。假设我们需要计算三个状态 s[0]、s[1] 和 s[2] 的价值。我们知道,在从一个状态过渡到另一个状态时,我们会获得奖励。因此,最终状态的奖励将为 0,因为我们不会从最终状态进行任何过渡。因此,最终状态 s[2] 的价值为零。现在,我们需要找到两个状态 s[0] 和 s[1] 的价值。

接下来的章节将通过手动计算进行说明,便于理解,请准备好笔和纸跟随练习。

步骤 1

表 4.1所示,将所有状态的 total_returns(s) 和 N(s) 初始化为零:

表 4.1:初始值

假设我们给定一个随机策略 ;在状态 s[0],我们的随机策略以 50% 的概率选择动作 0,50% 的概率选择动作 1,而在状态 s[1] 以 100% 的概率选择动作 1。

步骤 2:第一次迭代

使用给定的输入策略生成一个回合,如图 4.5所示:

图 4.5:使用给定策略生成回合

将在该回合中获得的所有奖励存储在名为 rewards 的列表中。因此,rewards = [1, 1]。

首先,我们计算状态 s[0] 的回报(即从 s[0] 开始的奖励总和):

更新表格中状态 s[0] 的总回报如下:

更新表格中状态 s[0] 被访问的次数如下:

现在,让我们计算状态 s[1] 的回报(即从 s[1] 开始的奖励总和):

更新表格中状态 s[1] 的总回报如下:

更新表格中状态 s[1] 被访问的次数如下:

经过第一次迭代后的更新表格如下:

表 4.2:第一次迭代后的更新表格

第二次迭代:

假设我们使用相同的给定策略生成另一个回合,正如图 4.6所示:

图 4.6:使用给定策略生成回合

将在该回合中获得的所有奖励存储在名为 rewards 的列表中。因此,rewards = [3, 1]。

首先,我们计算状态 s[0] 的回报(即从 s[0] 开始的奖励总和):

更新表格中状态 s[0] 的总回报如下:

更新表格中状态 s[0] 被访问的次数如下:

现在,让我们计算状态 s[1] 的回报(即从 s[1] 开始的奖励总和):

更新表格中状态 s[1] 的回报如下:

更新状态被访问的次数:

我们在第二次迭代后的更新表格如下:

表 4.3:第二次迭代后的更新表格

由于我们正在手动计算,为了简单起见,我们就止步于两次迭代;也就是说,我们只生成了两个回合。

步骤 3:

现在,我们可以计算状态的值如下:

因此:

因此,我们通过在多个回合中计算平均回报来估算状态的值。注意,在上述示例中,为了进行手动计算,我们只生成了两个回合,但为了更好地估算状态的值,我们应该生成多个回合,然后计算这些回合的平均回报(而不仅仅是 2 个回合)。

蒙特卡洛预测的类型

我们刚刚学到了蒙特卡洛预测算法的工作原理。我们可以将蒙特卡洛预测算法分为两种类型:

  • 首次访问蒙特卡洛方法

  • 每次访问蒙特卡洛方法

首次访问蒙特卡洛方法

我们已经学过,在蒙特卡洛预测方法中,我们通过在多个回合中取一个状态的平均回报来估计该状态的值。我们知道,在每个回合中,一个状态可能会被访问多次。在首次访问蒙特卡洛方法中,如果同一状态在同一个回合中再次被访问,我们不会再次计算该状态的回报。例如,考虑一个代理玩蛇梯游戏的情况。如果代理踩到蛇,那么很有可能它会回到一个之前访问过的状态。因此,当代理重新访问相同的状态时,我们不会第二次计算该状态的回报。

以下展示了首次访问蒙特卡洛算法;正如粗体部分所述,只有在状态 s[t] 在回合中第一次出现时,我们才会计算该状态的回报:

  1. 设 total_return(s) 为该状态在多个回合中的回报总和,N(s) 为计数器,即该状态在多个回合中被访问的次数。将所有状态的 total_return(s) 和 N(s) 初始化为零。输入给定策略

  2. 对于 M 次迭代:

    1. 使用策略生成一个回合

    2. 将回合中获得的所有奖励存储在名为 rewards 的列表中

    3. 对于回合中的每一步 t

      如果 状态 s[t] 在回合中首次出现:

      1. 计算状态 s[t] 的回报为 R(s[t]) = sum(rewards[t:])

      2. 将状态 s[t] 的总回报更新为 total_return(s[t]) = total_return(s[t]) + R(s[t])

      3. 更新计数器为 N(s[t]) = N(s[t]) + 1

  3. 通过取平均值来计算状态的值,即:

每次访问蒙特卡洛方法

如你所料,每次访问蒙特卡洛方法正好是首次访问蒙特卡洛方法的相反。在这里,我们每次访问到一个状态时,都会计算该状态的回报。每次访问蒙特卡洛算法与我们之前在本节开始时看到的算法是相同的,具体如下:

  1. 设 total_return(s) 为该状态在多个回合中的回报总和,N(s) 为计数器,即该状态在多个回合中被访问的次数。将所有状态的 total_return(s) 和 N(s) 初始化为零。输入给定策略

  2. 对于 M 次迭代:

    1. 使用策略生成一个回合

    2. 将回合中获得的所有奖励存储在名为 rewards 的列表中

    3. 对于回合中的每一步 t

      1. 计算状态 s[t] 的回报为 R(s[t]) = sum(rewards[t:])

      2. 更新状态 s[t] 的总回报:total_return(s[t]) = total_return(s[t]) + R(s[t])

      3. 更新计数器:N(s[t]) = N(s[t]) + 1

  3. 通过计算一个状态的平均值来获得该状态的值,即:

请记住,第一次访问 MC(First-Visit MC)和每次访问 MC(Every-Visit MC)方法之间的唯一区别在于,在第一次访问 MC 方法中,我们只计算某个状态在该回合第一次出现时的回报;而在每次访问 MC 方法中,我们会在每次访问该状态时计算回报。我们可以根据所解决的问题选择使用第一次访问 MC 或每次访问 MC 方法。

现在我们已经理解了蒙特卡洛预测方法如何预测给定策略的值函数,在下一节中,我们将学习如何实现蒙特卡洛预测方法。

实现蒙特卡洛预测方法

如果你喜欢玩纸牌游戏,那么这一节肯定会让你感兴趣。在这一节中,我们将学习如何使用蒙特卡洛预测方法来玩二十一点。在深入之前,先让我们了解一下二十一点游戏的规则和玩法。

了解二十一点游戏

二十一点,也被称为21 点,是最受欢迎的纸牌游戏之一。游戏由玩家和庄家组成。玩家的目标是使所有牌的总和为 21 或比庄家的牌总和更大,但不得超过 21。如果满足其中一个条件,玩家就赢得游戏;否则庄家赢得游戏。让我们更详细地了解这一点。

Jack(J)King(K)Queen(Q) 的牌值都将视为 10。Ace(A) 的值可以是 1 或 11,这取决于玩家的选择。也就是说,玩家可以决定在游戏中 Ace 的值是 1 还是 11。其他牌(210)的值即为其面值。例如,牌 2 的值为 2,牌 3 的值为 3,依此类推。

我们了解到,这个游戏由玩家和庄家组成。游戏中可以有多个玩家,但只有一个庄家。所有玩家与庄家竞争,而不是与其他玩家竞争。我们来考虑一个只有一个玩家和一个庄家的情况。让我们通过玩游戏并分析不同的情况来理解二十一点。假设我们是玩家,正在与庄家竞争。

案例 1:当玩家赢得游戏时

最初,玩家会被发两张牌。两张牌都面朝上,也就是说,玩家的两张牌对庄家都是可见的。类似地,庄家也会发两张牌,但庄家的一张牌面朝上,另一张面朝下。也就是说,庄家只展示他们的一张牌。

如我们在图 4.7中所见,玩家有两张卡牌(都面朝上),而庄家也有两张卡牌(其中一张面朝上):

图 4.7:玩家的总牌面为 20,庄家有一张牌面朝下的 2

现在,玩家执行两个动作中的一个,即要牌停牌。如果我们(玩家)选择执行要牌,我们将再拿一张牌。如果我们选择停牌,则表示我们不再需要任何牌,并告诉庄家亮出他们的所有牌。无论谁的牌面总和等于 21,或者比对方更大但不超过 21,就赢得游戏。

我们了解到,JKQ的牌值为 10。如图 4.7所示,我们手中有JK两张牌,它们的总和是 20(10+10)。因此,我们的牌面总和已经是一个大数,并且没有超过 21。所以我们选择停牌,这一动作告诉庄家亮出他们的牌。如图 4.8所示,庄家现在已展示出他们的所有牌,庄家的牌面总和为 12,而我们的(玩家的)牌面总和为 20,这个总和更大,并且也没有超过 21,因此我们赢得了游戏。

图 4.8:玩家获胜!

案例 2:玩家输掉游戏

图 4.9显示我们有两张牌,庄家也有两张牌,但庄家只有一张牌是我们可见的:

图 4.9:玩家的总牌面为 13,庄家有一张牌面朝下的 7

现在,我们必须决定是(执行动作)“要牌”还是“停牌”。图 4.9显示我们有两张牌,K3,它们的总和是 13(10+3)。让我们稍微乐观一些,希望庄家的牌面总和不会超过我们的。因此,我们选择停牌,这一动作告诉庄家亮出他们的牌。如图 4.10所示,庄家的牌面总和为 17,而我们的总和只有 13,所以我们输掉了游戏。也就是说,庄家的牌面总和比我们大,并且没有超过 21,因此庄家赢得了游戏,而我们输了:

图 4.10:庄家获胜!

案例 3:玩家爆掉

图 4.11显示我们有两张牌,庄家也有两张牌,但庄家只有一张牌是我们可见的:

图 4.11:玩家的总牌面为 8,庄家有一张牌面朝下的 10

现在,我们必须决定是(执行动作)“要牌”还是“停牌”。我们已经了解到,游戏的目标是让牌面总和为 21,或者比庄家的牌面总和大,但不超过 21。目前,我们的牌面总和是 3+5 = 8。因此,我们选择要牌,以便增加我们的牌面总和。在我们要牌之后,我们收到了如图 4.12所示的新牌:

图 4.12:玩家的总牌面为 18,庄家有一张牌面朝下的 10

如我们所见,我们得到了新的一张牌。现在,我们的牌的总点数是 3+5+10 = 18。再次,我们需要决定是否(执行操作)要牌或停牌。让我们有点贪心,选择(执行操作)要牌,以便我们可以稍微增加我们的点数。如图 4.13所示,我们要牌并得到了一张新牌,但现在我们的牌的总点数变为 3+5+10+10 = 28,超过了 21,这就是所谓的爆掉,我们输掉了游戏:

图 4.13:玩家爆掉了!

案例 4:可用的 A

我们知道A的点数可以是 1 或 11,玩家可以在游戏过程中决定A的点数。让我们学习一下这是如何运作的。如图 4.14所示,我们被发了两张牌,庄家也有两张牌,其中只有一张庄家的牌是面朝上的:

图 4.14:玩家的点数是 10,庄家的点数是 5,且有一张牌是面朝下的

如我们所见,我们的牌的总点数是 5+5 = 10。因此,我们选择要牌以便可以增加我们的点数。如图 4.15所示,在执行要牌操作后,我们获得了一张新牌,它是一张A。现在,我们可以决定将A的点数定为 1 或 11。如果我们将A的点数定为 1,那么我们的牌的总点数将是 5+5+1 = 11。如果我们将A的点数定为 11,那么我们的牌的总点数将是 5+5+11 = 21。在这种情况下,我们将A的点数定为 11,以便我们的点数变为 21。

因此,我们将A的点数定为 11 并赢得了游戏,在这种情况下,A被称为可用的A,因为它帮助我们赢得了游戏:

图 4.15:玩家将A视为 11 并赢得了游戏

案例 5:不可用的 A

图 4.16显示我们有两张牌,庄家也有两张牌,其中一张是面朝上的:

图 4.16:玩家的点数是 13,庄家的点数是 10,且有一张牌是面朝下的

如我们所见,我们的牌的总点数是 13(10+3)。我们(执行操作)要牌,以便我们可以稍微增加我们的点数:

图 4.17:玩家必须将A视为 1,否则将爆掉

图 4.17所示,我们要牌并获得了一张新牌,它是一张A。现在我们可以决定将A的点数定为 1 或 11。如果我们选择 11,那么我们的点数将变为 10+3+11 = 23。如我们所观察到的,当我们将A定为 11 时,我们的总点数超过了 21,因此我们输了游戏。因此,我们不选择将A定为 11,而是将A的点数定为 1,这样我们的总点数就变为 10+3+1 = 14。

同样,我们需要决定是执行(执行动作)hit 还是 stand。假设我们选择站立,希望庄家的牌面总和低于我们的牌面总和。正如 图 4.18 所示,执行站立动作后,庄家的两张牌都会显示,庄家的牌总和是 20,而我们的只有 14,所以我们输了游戏,在这种情况下,Ace 被称为不可用的 Ace,因为它并没有帮助我们赢得游戏。

图 4.18:玩家的牌是 14,庄家的牌是 20,庄家获胜

案例 6:当游戏是平局时

如果玩家和庄家的牌面总和相同,比如都是 20,则游戏称为平局。

现在我们已经了解了如何玩 blackjack,接下来让我们在 blackjack 游戏中实现蒙特卡洛预测方法。但在继续之前,首先让我们了解一下 Gym 中 blackjack 环境的设计。

Gym 库中的 blackjack 环境

导入 Gym 库:

import gym 

blackjack 环境的 ID 是 Blackjack-v0。因此,我们可以使用如下的 make 函数来创建 blackjack 游戏:

env = gym.make('Blackjack-v0') 

现在,让我们看看 blackjack 环境的状态;我们可以重置环境并查看初始状态:

print(env.reset()) 

请注意,每次运行前面的代码时,我们可能会得到不同的结果,因为初始状态是随机初始化的。前面的代码将打印如下内容:

(15, 9, True) 

如我们所见,状态表示为一个元组,那么这意味着什么呢?我们了解到,在 blackjack 游戏中,玩家会得到两张牌,并且还可以看到庄家的其中一张牌。因此,15表示我们手中牌的总值,9表示庄家一张牌的面值,True表示我们有可用的 Ace,如果没有可用的 Ace,则为False

因此,在 blackjack 环境中,状态表示为由三个值组成的元组:

  1. 我们手中牌的总和

  2. 庄家一张牌的面值

  3. 布尔值——如果我们有可用的 Ace,则为True,如果没有可用的 Ace,则为False

让我们看看 blackjack 环境的动作空间:

print(env.action_space) 

前面的代码将打印:

Discrete(2) 

如我们所见,这意味着我们的动作空间中有两个动作,分别是 0 和 1:

  • 动作stand用 0 来表示

  • 动作hit用 1 来表示

好的,那么奖励如何呢?奖励将按照如下方式分配:

  • 如果我们赢得游戏,奖励为+1.0

  • 如果我们输掉游戏,奖励为-1.0

  • 如果游戏是平局,奖励为0

现在我们已经理解了 Gym 中 blackjack 环境的设计,接下来让我们在 blackjack 游戏中实现 MC 预测方法。首先,我们将研究每次访问的 MC,然后学习如何实现首次访问的 MC 预测。

每次访问的 MC 预测与 blackjack 游戏

为了清晰理解这一部分,你应该回顾一下我们之前学习的每次访问蒙特卡罗方法。现在,让我们一步步理解如何用黑杰克游戏实现每次访问的蒙特卡罗预测:

导入必要的库:

import gym
import pandas as pd
from collections import defaultdict 

创建一个黑杰克环境:

env = gym.make('Blackjack-v0') 

定义策略

我们了解到,在预测方法中,我们将获得一个输入策略,并预测该输入策略的价值函数。因此,现在我们首先定义一个策略函数,作为输入策略。也就是说,我们定义了一个输入策略,其价值函数将在接下来的步骤中进行预测。

如下所示的代码,我们的策略函数以状态作为输入,如果state[0],即我们牌的总和,值大于 19,那么它将返回动作0(停牌),否则它将返回动作1(要牌):

def policy(state):
    return 0 if state[0] > 19 else 1 

我们定义了一个最优策略:当我们的牌的总和已经大于 19 时,执行动作 0(停牌)更有意义。也就是说,当总和大于 19 时,我们不必执行动作 1(要牌),以避免再拿到一张牌可能导致我们输掉游戏或爆掉。

例如,让我们通过重置环境生成一个初始状态,如下所示:

state = env.reset()
print(state) 

假设前面的代码打印了以下内容:

(20, 5, False) 

正如我们所注意到的,state[0] = 20;也就是说,我们牌的总和是 20,因此在这种情况下,我们的策略将返回动作 0(停牌),如下所示:

print(policy(state)) 

前面的代码将打印:

0 

现在我们已经定义了策略,在接下来的部分中,我们将预测该策略的价值函数(状态值)。

生成一集

接下来,我们使用给定的策略生成一集,因此我们定义了一个名为generate_episode的函数,该函数以策略作为输入,并使用给定的策略生成一集。

首先,让我们设定时间步数:

num_timesteps = 100 

为了更清楚地理解,让我们逐行查看这个函数:

def generate_episode(policy): 

让我们定义一个名为episode的列表,用于存储集:

 episode = [] 

通过重置环境来初始化状态:

 state = env.reset() 

然后,对于每一个时间步:

 for t in range(num_timesteps): 

根据给定的策略选择动作:

 action = policy(state) 

执行动作并存储下一个状态信息:

 next_state, reward, done, info = env.step(action) 

将状态、动作和奖励存入我们的集列表:

 episode.append((state, action, reward)) 

如果下一个状态是最终状态,则跳出循环,否则将下一个状态更新为当前状态:

 if done:
            break

        state = next_state
    return episode 

让我们看看generate_episode函数的输出是什么样子的。请注意,我们使用之前定义的策略生成了一集:

print(generate_episode(policy)) 

前面的代码会打印如下内容:

[((10, 2, False), 1, 0), ((20, 2, False), 0, 1.0)] 

如我们所见,我们的输出是[(state, action, reward)]的形式。如前所述,我们的集里有两个状态。在状态 (10, 2, False) 下,我们执行了动作 1(要牌),并且收到了 0 的奖励;在状态 (20, 2, False) 下,我们执行了动作 0(停牌),并且收到了 1.0 的奖励。

现在我们已经学会了如何使用给定的策略生成回合,接下来,我们将学习如何使用每次访问 MC 方法计算状态的值(值函数)。

计算值函数

我们了解到,为了预测值函数,我们通过给定的策略生成多个回合,并将状态的值计算为多个回合中的平均回报。让我们看看如何实现这一点。

首先,我们将total_returnN定义为字典,分别用于存储回合中每个状态的总回报和该状态被访问的次数:

total_return = defaultdict(float)
N = defaultdict(int) 

设置我们希望生成的迭代次数,即回合数:

num_iterations = 500000 

然后,对于每次迭代:

for i in range(num_iterations): 

使用给定的策略生成回合;即,使用我们之前定义的策略函数生成回合:

 episode = generate_episode(policy) 

存储从回合中获得的所有状态、动作和奖励:

 states, actions, rewards = zip(*episode) 

然后,对于回合中的每一步:

 for t, state in enumerate(states): 

计算状态的回报R,作为奖励的总和,R(s[t]) = sum(rewards[t:]):

 R = (sum(rewards[t:])) 

将状态的total_return更新为 total_return(s[t]) = total_return(s[t]) + R(s[t]):

 total_return[state] =  total_return[state] + R 

将状态在回合中被访问的次数更新为N(s[t]) = N(s[t]) + 1:

 N[state] =  N[state] + 1 

在计算了total_returnN后,我们可以将它们转换为 pandas 数据框,以便更好地理解。请注意,这只是为了清晰地理解算法;我们不一定非得转换为 pandas 数据框,我们也可以仅通过使用字典来高效实现。

total_returns字典转换为数据框:

total_return = pd.DataFrame(total_return.items(),columns=['state', 'total_return']) 

将计数器N字典转换为数据框:

N = pd.DataFrame(N.items(),columns=['state', 'N']) 

按状态合并两个数据框:

df = pd.merge(total_return, N, on="state") 

查看数据框的前几行:

df.head(10) 

上述代码将显示如下内容。正如我们所观察到的,我们有状态的总回报和访问次数:

图 4.19:状态的总回报和被访问的次数

接下来,我们可以计算状态的值作为平均回报:

因此,我们可以写出:

df['value'] = df['total_return']/df['N'] 

我们来看一下数据框的前几行:

df.head(10) 

上述代码将显示类似这样的内容:

图 4.20:值被计算为每个状态回报的平均值

正如我们可以观察到的,现在我们有了状态的值,这只是该状态在多个回合中的回报平均值。因此,我们成功地使用每次访问 MC 方法预测了给定策略的值函数。

好的,接下来我们检查一些状态的值,并了解根据给定策略我们估算的值函数有多准确。回想一下,当我们开始时,为了生成回合,我们使用了最优策略,当总和大于 19 时选择动作 0(停牌),当总和低于 19 时选择动作 1(要牌)\。

让我们评估状态 (21,9,False) 的值。正如我们所看到的,我们的牌面总和已经是 21,因此这是一个好状态,应该有较高的值。让我们看看我们估算的状态值:

df[df['state']==(21,9,False)]['value'].values 

上述代码将打印类似如下内容:

array([1.0]) 

如我们所观察到的,状态的值较高。

现在,让我们检查状态 (5,8,False) 的值。正如我们所注意到的,牌面总和仅为 5,即使庄家的单张牌值很高,达到了 8,在这种情况下,状态的值应该较低。让我们看看我们估算的状态值:

df[df['state']==(5,8,False)]['value'].values 

上述代码将打印类似如下内容:

array([-1.0]) 

如我们所见,状态的值较低。

因此,我们已经学会了如何使用每次访问蒙特卡洛预测方法预测给定策略的值函数。在下一节中,我们将学习如何使用首次访问蒙特卡洛方法计算状态的值。

使用黑杰克游戏的首次访问蒙特卡洛(MC)预测

使用首次访问蒙特卡洛方法预测值函数的方法与使用每次访问蒙特卡洛方法预测值函数的方式完全相同,唯一的区别是,这里我们只计算状态第一次在回合中出现时的返回值。首次访问蒙特卡洛的代码与我们在每次访问蒙特卡洛中看到的代码相同,只不过在这里,我们只计算状态第一次出现时的返回值,具体代码如下所示:

for i in range(num_iterations):

    episode = generate_episode(env,policy)
    states, actions, rewards = zip(*episode)
    for t, state in enumerate(states):
 **if** **state** **not****in** **states[****0****:t]:**
            R = (sum(rewards[t:]))
            total_return[state] = total_return[state] + R
            N[state] = N[state] + 1 

你可以从本书的 GitHub 仓库获取完整代码,并将获得与我们在每次访问蒙特卡洛部分看到的类似结果。

因此,我们已经学会了如何使用首次访问和每次访问蒙特卡洛方法预测给定策略的值函数。

增量均值更新

在首次访问蒙特卡洛和每次访问蒙特卡洛中,我们通过计算多个回合中状态的平均返回值(算术均值)来估算状态的值,具体如下所示:

我们不仅可以使用算术均值来近似状态的值,还可以使用增量均值,公式如下所示:

那么,为什么我们需要增量均值呢?考虑到我们的环境是非平稳的。在这种情况下,我们不需要将所有回合的状态返回值取平均。由于环境是非平稳的,我们可以忽略早期回合的返回值,仅使用最新回合的返回值来计算平均值。因此,我们可以使用增量均值来计算状态的值,具体如下所示:

其中 R[t] 是状态 s[t] 的返回值。

蒙特卡洛预测(Q 函数)

到目前为止,我们已经学会了如何使用蒙特卡洛方法预测给定策略的值函数。在本节中,我们将看到如何使用蒙特卡洛方法预测给定策略的 Q 函数。

使用蒙特卡罗(MC)方法预测给定策略的 Q 函数与我们在前一节中预测价值函数的方式完全相同,只是这里我们使用状态-动作对的回报,而在价值函数的情况下,我们使用状态的回报。也就是说,就像我们通过计算多个回合中状态的平均回报来逼近状态的价值(价值函数),我们也可以通过计算多个回合中状态-动作对的平均回报来逼近状态-动作对的价值(Q 函数)。

因此,我们使用给定的策略 生成多个回合,然后计算 total_return(s, a),即跨多个回合的状态-动作对回报之和,并且我们还计算 N(s, a),即状态-动作对跨多个回合被访问的次数。然后,我们计算 Q 函数或 Q 值,即状态-动作对的平均回报,如下所示:

例如,考虑一个小示例。假设我们有两个状态 s[0] 和 s[1],并且我们有两个可能的动作 0 和 1。现在,我们计算 total_return(s, a) 和 N(s, a)。假设我们计算后的表格如下所示,见 表 4.4

表 4.4:两个状态中两个动作的结果

一旦我们得到这个,就可以通过简单地取平均来计算 Q 值,即:

因此,我们可以计算所有状态-动作对的 Q 值,如下所示:

使用蒙特卡罗方法预测 Q 函数的算法如下所示。如我们所见,它与我们使用状态回报预测价值函数的方式完全相同,只是这里我们使用状态-动作对的回报来预测 Q 函数:

  1. 让 total_return(s, a) 为跨多个回合的状态-动作对回报之和,N(s, a) 为状态-动作对跨多个回合被访问的次数。将所有状态-动作对的 total_return(s, a) 和 N(s, a) 初始化为零。策略 作为输入给定。

  2. 对于 M 次迭代:

    1. 使用策略 生成一个回合

    2. 将回合中获得的所有奖励存储在名为 rewards 的列表中

    3. 对于回合中的每一步 t

      1. 计算状态-动作对的回报,R(s[t], a[t]) = sum(rewards[t:])

      2. 更新状态-动作对的总回报,total_return(s[t], a[t]) = total_return(s[t], a[t]) + R(s[t], a[t])

      3. 更新计数器,N(s[t], a[t]) = N(s[t], a[t]) + 1

  3. 通过简单地取平均来计算 Q 函数(Q 值),即:

回想一下,在 MC 预测值函数时,我们学到了两种类型的 MC——首次访问 MC 和每次访问 MC。在首次访问 MC 中,我们仅计算状态第一次在回合中被访问时的回报,而在每次访问 MC 中,我们计算状态每次在回合中被访问时的回报。

类似地,在 MC 预测 Q 函数时,我们有两种类型的 MC——首次访问 MC 和每次访问 MC。在首次访问 MC 中,我们仅计算状态-动作对第一次在回合中被访问时的回报,而在每次访问 MC 中,我们计算状态-动作对每次在回合中被访问时的回报。

如前一节所述,除了使用算术平均值外,我们还可以使用增量平均值。我们学到的是,状态的值可以通过增量平均值来计算,公式如下:

类似地,我们也可以使用增量平均值计算 Q 值,如下所示:

现在我们已经学会了如何使用蒙特卡洛方法执行预测任务,在接下来的部分,我们将学习如何使用蒙特卡洛方法执行控制任务。

蒙特卡洛控制

在控制任务中,我们的目标是找到最优策略。与预测任务不同,在这里,我们不会给定任何策略作为输入。因此,我们将从初始化一个随机策略开始,然后我们尝试通过迭代找到最优策略。也就是说,我们尝试找到一个能带来最大回报的最优策略。在本节中,我们将学习如何使用蒙特卡洛方法执行控制任务以找到最优策略。

好的,我们学到的是,在控制任务中我们的目标是找到最优策略。首先,我们如何计算一个策略?我们学到的是,策略可以从 Q 函数中提取。也就是说,如果我们有一个 Q 函数,那么我们可以通过在每个状态中选择具有最大 Q 值的动作来提取策略,正如下图所示:

因此,为了计算策略,我们需要计算 Q 函数。那么,我们如何计算 Q 函数呢?我们可以像在 MC 预测方法中那样计算 Q 函数。也就是说,在 MC 预测方法中,我们学到的是,当给定一个策略时,我们可以使用该策略生成多个回合,并计算 Q 函数(Q 值),作为状态-动作对在多个回合中的平均回报。

我们可以在这里执行相同的步骤来计算 Q 函数。但在控制方法中,我们并没有给定任何策略作为输入。因此,我们将初始化一个随机策略,然后使用这个随机策略计算 Q 函数。也就是说,就像我们在预测方法中学到的那样,我们使用随机策略生成多个回合。然后,我们计算 Q 函数(Q 值),作为状态-动作对在多个回合中的平均回报,正如下图所示:

假设在计算 Q 函数作为状态-动作对的平均回报后,我们的 Q 函数看起来像表 4.5

表 4.5:Q 表

从之前的 Q 函数中,我们可以通过选择每个状态下具有最大 Q 值的动作来提取新的策略。也就是说,。因此,我们的新策略在状态s[0]选择动作 0,在状态s[1]选择动作 1,因为它们具有最大的 Q 值。

然而,这个新策略不一定是最优策略,因为这个新策略是从 Q 函数中提取的,而 Q 函数是使用随机策略计算的。也就是说,我们初始化了一个随机策略,并使用该随机策略生成了多个回合,然后通过取多个回合中的状态-动作对的平均回报来计算 Q 函数。因此,我们使用随机策略计算 Q 函数,所以从 Q 函数中提取出的新策略将不会是最优策略。

但是,现在我们已经从 Q 函数中提取出了新的策略,我们可以使用这个新策略在下一次迭代中生成新的回合并计算新的 Q 函数。然后,从这个新的 Q 函数中,我们提取出新的策略。我们反复执行这些步骤,直到找到最优策略。以下步骤清晰地解释了这一过程:

第 1 次迭代—设!为随机策略。我们使用这个随机策略生成一个回合,然后通过取状态-动作对的平均回报来计算 Q 函数!。然后,从这个 Q 函数!中,我们提取出新的策略!。这个新策略!将不会是最优策略,因为它是从 Q 函数中提取的,而 Q 函数是使用随机策略计算的。

第 2 次迭代—因此,我们使用从上一次迭代中推导出的新策略!来生成回合并计算新的 Q 函数!,作为状态-动作对的平均回报。然后,从这个 Q 函数!中,我们提取出新的策略!。如果策略!是最优的,我们就停止,否则进入第 3 次迭代。

第 3 次迭代—现在,我们使用从上一次迭代中推导出的新策略!来生成回合并计算新的 Q 函数!。然后,从这个 Q 函数!中,我们提取出新的策略!。如果!是最优的,我们就停止,否则进入下一次迭代。

我们重复这个过程进行多次迭代,直到找到最优策略!,如图 4.21所示:

图 4.21:寻找最优策略的路径

这一步称为策略评估与改进,类似于我们在第三章《贝尔曼方程与动态规划》中讨论的策略迭代方法。策略评估意味着在每一步我们评估策略;策略改进意味着在每一步我们通过选择最大 Q 值来改进策略。请注意,这里我们采用贪婪方式选择策略,即通过选择最大 Q 值来选择策略 ,因此我们可以称我们的策略为贪婪策略。

现在我们对 MC 控制方法的基本工作原理有了基本了解,在下一节中,我们将深入探讨 MC 控制方法的算法,并对其进行更详细的学习。

MC 控制算法

以下步骤展示了蒙特卡洛控制算法。正如我们观察到的,与 MC 预测方法不同,这里我们不会给出任何策略。因此,我们首先通过初始化随机策略并使用该随机策略生成一个回合作为第一轮迭代。然后,我们将计算 Q 函数(Q 值),作为状态-动作对的平均回报。

一旦得到 Q 函数,我们通过选择每个状态中具有最大 Q 值的动作来提取新策略。在下一次迭代中,我们使用提取出的新策略生成一个回合,并计算新的 Q 函数(Q 值),作为状态-动作对的平均回报。我们重复这些步骤多次迭代,以找到最优策略。

还有一点,我们需要注意,就像我们在首次访问 MC 预测方法中学习到的那样,这里我们只计算状态-动作对在回合中首次访问时的回报。

为了更好地理解,我们可以将 MC 控制算法与 Q 函数的 MC 预测进行比较。我们可以观察到的一个区别是,在这里,我们在每次迭代中计算 Q 函数。但如果你注意到,在 Q 函数的 MC 预测中,我们是在所有迭代完成后计算 Q 函数。这里在每次迭代中计算 Q 函数的原因是,我们需要 Q 函数来提取新策略,以便在下一次迭代中使用提取的新策略生成回合:

  1. 让 total_return(s, a)表示一个状态-动作对在多个回合中的回报总和,N(s, a)表示该状态-动作对在多个回合中被访问的次数。将所有状态-动作对的 total_return(s, a)和N(s, a)初始化为零,并初始化一个随机策略

  2. 对于M次迭代:

    1. 使用策略生成一个回合

    2. 将回合中获得的所有奖励存储在名为 rewards 的列表中

    3. 对于每个回合中的步骤t

      如果(s[t], a[t])在该回合中是第一次出现:

      1. 计算状态-动作对的回报,R(s[t], a[t]) = sum(rewards[t:])

      2. 更新状态-行动对的总回报,公式为:total_return(s[t], a[t]) = total_return(s[t], a[t]) + R(s[t], a[t])

      3. 更新计数器,公式为:N(s[t], a[t]) = N(s[t], a[t]) + 1

      4. 通过简单取平均值来计算 Q 值,即:

    4. 使用 Q 函数计算新的更新策略

从前面的算法中,我们可以观察到,我们使用策略生成一个历程。然后,对于历程中的每一步,我们计算状态-行动对的回报,并计算 Q 函数Q(s[t], a[t])作为平均回报,然后从这个 Q 函数中提取出新的策略。我们重复这一步骤,迭代地找到最优策略。这样,我们就学会了如何使用蒙特卡洛方法执行控制任务。

我们可以将控制方法分为两类:

  • 在策略控制

  • 脱策略控制

在策略控制—在在策略控制方法中,智能体使用一种策略进行行为,并尝试改进相同的策略。也就是说,在在策略方法中,我们使用一种策略生成历程,并迭代地改进相同的策略,以找到最优策略。例如,刚刚学习过的 MC 控制方法可以称为在策略 MC 控制,因为我们使用策略生成历程,并且在每次迭代中尝试改进相同的策略,以计算最优策略。

脱策略控制—在脱策略控制方法中,智能体使用一种策略b进行行为,并尝试改进另一种策略。也就是说,在脱策略方法中,我们使用一种策略生成历程,并迭代地尝试改进另一种策略,以找到最优策略。

在接下来的章节中,我们将详细学习前面两种控制方法的具体工作原理。

在策略蒙特卡洛控制

有两种类型的在策略蒙特卡洛控制方法:

  • 蒙特卡洛探索启动

  • 使用 epsilon-greedy 策略的蒙特卡洛方法

蒙特卡洛探索启动

我们已经了解了蒙特卡洛控制方法的工作原理。有一点我们可能需要考虑的是探索。一个状态下可能有多个动作:有些动作是最优的,而有些则不是。为了理解一个动作是否最优,智能体必须通过执行该动作来进行探索。如果智能体从不探索一个特定的动作,它就永远不知道该动作是否是一个好动作。那么,如何解决这个问题呢?也就是说,我们如何确保足够的探索?这就是蒙特卡洛探索启动帮助我们的地方。

在 MC 探索起始状态方法中,我们为所有状态-动作对设置一个非零的概率,使其成为初始状态-动作对。因此,在生成情节之前,首先我们随机选择初始状态-动作对,然后按照策略 从该初始状态-动作对开始生成情节。接着,在每次迭代中,我们的策略将更新为贪心策略(选择最大 Q 值;更多细节请参见下一节关于 蒙特卡洛与 epsilon-greedy 策略)。

以下步骤展示了 MC 控制探索起始状态的算法。它本质上与我们之前在 MC 控制算法部分学到的相同,只是这里我们选择一个初始状态-动作对,并从该初始状态-动作对开始生成情节,如粗体部分所示:

  1. 令 total_return(s, a) 为多个情节中状态-动作对的回报之和,N(s, a) 为多个情节中状态-动作对的访问次数。将所有状态-动作对的 total_return(s, a) 和 N(s, a) 初始化为零,并初始化一个随机策略

  2. 对于 M 次迭代:

    1. 随机选择初始状态 s****0 和初始动作 a****0 ,使得所有状态-动作对的概率大于 0

    2. 使用策略 从选择的初始状态 s[0] 和初始动作 a[0] 开始生成一个情节

    3. 将情节中获得的所有奖励存储在名为 rewards 的列表中

    4. 对于情节中的每一步 t

      如果 (s[t], a[t]) 在该情节中第一次出现:

      1. 计算状态-动作对的回报,R(s[t], a[t]) = sum(rewards[t:])

      2. 更新状态-动作对的总回报:total_return(s[t], a[t]) = total_return(s[t], a[t]) + R(s[t], a[t])

      3. 更新计数器:N(s[t], a[t]) = N(s[t], a[t]) + 1

      4. 通过直接取平均值来计算 Q 值,即:

    5. 使用 Q 函数计算更新后的策略

探索起始状态方法的主要缺点之一是它不适用于每个环境。也就是说,我们不能随机选择任何状态-动作对作为初始状态-动作对,因为在某些环境中,可能只有一个状态-动作对可以作为初始状态-动作对。因此,我们不能随机选择状态-动作对作为初始状态-动作对。

例如,假设我们正在训练一个智能体玩赛车游戏;我们不能将情节从一个随机位置作为初始状态开始,也不能随机选择一个动作作为初始动作,因为我们有一个固定的单一起始状态和动作作为初始状态和动作。

因此,为了解决探索起始状态的问题,在下一节中,我们将学习蒙特卡洛控制方法,采用一种新的策略类型——epsilon-greedy 策略。

带有 epsilon-贪婪策略的蒙特卡洛方法

在继续之前,首先让我们理解什么是 epsilon-贪婪策略,因为它在强化学习中无处不在。

首先,让我们了解什么是贪婪策略。贪婪策略是选择当前时刻可用的最佳动作。例如,假设我们处于某个状态A,在该状态中有四个可能的动作。设这些动作为。但假设我们的代理只在状态A中探索了两个动作,,这两个动作在状态A中的 Q 值如表 4.6所示:

表 4.6:代理在状态 A 中仅探索了两个动作

我们了解到,贪婪策略选择当前时刻可用的最佳动作。因此,贪婪策略会检查 Q 表并选择在状态A中具有最大 Q 值的动作。正如我们所看到的,动作具有最大的 Q 值。所以我们的贪婪策略在状态A中选择动作

但贪婪策略有一个问题,那就是它从不探索其他可能的动作;相反,它总是选择当前时刻可用的最佳动作。在前面的例子中,贪婪策略总是选择动作。但在状态A中,可能有其他动作比代理尚未探索的动作更优。也就是说,在状态A中,我们仍然有两个未探索的动作————它们可能比动作更优。

所以,现在的问题是,代理是否应该探索状态中的所有其他动作,并将具有最大 Q 值的动作作为最佳动作,还是仅从已探索的动作中利用最优动作。这就是所谓的探索-利用困境

假设从我们工作地点到家有很多条路线,而到目前为止我们只探索了两条路线。因此,为了回家,我们可以从已探索的两条路线中选择最快的那一条。然而,还有许多其他我们尚未探索的路线,它们可能比我们目前的最优路线更好。问题是我们是否应该探索新路线(探索),还是应该始终使用当前的最优路线(利用)。

为了避免这种困境,我们引入了一种新的策略,称为 epsilon-贪婪策略。在这种策略中,所有动作都有一个非零的概率(epsilon)被尝试。以概率 epsilon,我们随机探索不同的动作;以概率 1-epsilon,我们选择具有最大 Q 值的动作。也就是说,以概率 epsilon,我们选择一个随机动作(探索),以概率 1-epsilon,我们选择最佳动作(利用)。

在 epsilon-greedy 策略中,如果我们将 epsilon 的值设置为 0,那么它就变成了一个贪婪策略(仅利用),而当我们将 epsilon 的值设置为 1 时,我们将总是进行探索。因此,epsilon 的值必须在 0 和 1 之间进行最佳选择。

假设我们设置 epsilon = 0.5;然后我们将从均匀分布中生成一个随机数,如果该随机数小于 epsilon(0.5),则选择一个随机动作(探索);但如果随机数大于或等于 epsilon,则选择最佳动作,也就是具有最大 Q 值的动作(利用)。

因此,通过这种方式,我们以 epsilon 的概率探索我们之前未见过的动作,并以 1-epsilon 的概率从已探索的动作中选择最佳动作。正如图 4.22所示,如果我们从均匀分布中生成的随机数小于 epsilon,则选择一个随机动作。如果随机数大于或等于 epsilon,则选择最佳动作:

图 4.22:epsilon-greedy 策略

以下代码片段展示了 epsilon-greedy 策略的 Python 实现:

def epsilon_greedy_policy(state, epsilon):
    if random.uniform(0,1) < epsilon:
         return env.action_space.sample()
    else:
         return max(list(range(env.action_space.n)), key = lambda x: q[(state,x)]) 

现在我们已经理解了什么是 epsilon-greedy 策略,以及它是如何用来解决探索与利用困境的,在下一节中,我们将研究如何在蒙特卡洛控制方法中使用 epsilon-greedy 策略。

使用 epsilon-greedy 策略的 MC 控制算法

使用 epsilon-greedy 策略的蒙特卡洛控制算法本质上与我们之前学习的 MC 控制算法相同,唯一不同的是,在这里我们基于 epsilon-greedy 策略选择动作,以避免探索与利用的困境。以下步骤展示了使用 epsilon-greedy 策略的蒙特卡洛算法:

  1. 令 total_return(s, a) 为某状态-动作对在多个回合中的回报总和,N(s, a) 为该状态-动作对在多个回合中被访问的次数。初始化所有状态-动作对的 total_return(s, a) 和 N(s, a) 为零,并初始化一个随机策略!

  2. 对于M次迭代:

    1. 使用策略生成一个回合!

    2. 将回合中获得的所有奖励存储在名为 rewards 的列表中

    3. 对回合中的每一步* t *:

      如果 (s[t], a[t]) 是回合中第一次出现的情况:

      1. 计算状态-动作对的回报,R(s[t], a[t]) = sum(rewards[t:])

      2. 更新状态-动作对的总回报为 total_return(s[t], a[t]) = total_return(s[t], a[t]) + R(s[t], a[t])

      3. 更新计数器为 N(s[t], a[t]) = N(s[t], a[t]) + 1

      4. 通过直接取平均值来计算 Q 值,即!

    4. 使用 Q 函数计算更新后的策略 。设 。策略 以概率 选择最佳动作 ,以概率 选择随机动作:

正如我们所观察到的,在每次迭代中,我们使用策略 生成回合,并且我们在每次迭代中都尝试改进相同的策略 来计算最优策略。

实现基于策略的 MC 控制:

现在,让我们学习如何使用 epsilon-greedy 策略实现 MC 控制方法来玩黑杰克游戏;也就是说,我们将看到如何使用 MC 控制方法在黑杰克游戏中找到最优策略。

首先,导入必要的库:

import gym
import pandas as pd
import random
from collections import defaultdict 

创建一个黑杰克环境:

env = gym.make('Blackjack-v0') 

初始化字典,用于存储 Q 值:

Q = defaultdict(float) 

初始化字典,用于存储状态-动作对的总回报:

total_return = defaultdict(float) 

初始化字典,用于存储状态-动作对的访问次数:

N = defaultdict(int) 

定义 epsilon-greedy 策略:

我们了解到,我们是基于 epsilon-greedy 策略选择动作的,因此我们定义了一个名为 epsilon_greedy_policy 的函数,它接受状态和 Q 值作为输入,并返回在给定状态下要执行的动作:

def epsilon_greedy_policy(state,Q): 

将 epsilon 值设置为 0.5:

 epsilon = 0.5 

从均匀分布中随机采样一个值;如果采样值小于 epsilon,则选择一个随机动作,否则选择具有最大 Q 值的最佳动作,如下所示:

 if random.uniform(0,1) < epsilon:
        return env.action_space.sample()
    else:
        return max(list(range(env.action_space.n)), key = lambda x: Q[(state,x)]) 

生成一个回合:

现在,让我们使用 epsilon-greedy 策略生成一个回合。我们定义一个名为 generate_episode 的函数,它接受 Q 值作为输入并返回回合。

首先,设置时间步数:

num_timesteps = 100 

现在,让我们定义函数:

def generate_episode(Q): 

初始化一个列表,用于存储回合:

 episode = [] 

使用 reset 函数初始化状态:

 state = env.reset() 

然后,对于每个时间步:

 for t in range(num_timesteps): 

根据 epsilon-greedy 策略选择动作:

 action = epsilon_greedy_policy(state,Q) 

执行所选动作并存储下一个状态信息:

 next_state, reward, done, info = env.step(action) 

在回合列表中存储状态、动作和奖励:

 episode.append((state, action, reward)) 

如果下一个状态是终止状态,则跳出循环,否则将下一个状态更新为当前状态:

 if done:
            break

        state = next_state
    return episode 

计算最优策略:

现在,让我们学习如何计算最优策略。首先,设置我们想要生成的迭代次数,即回合数:

num_iterations = 500000 

对于每次迭代:

for i in range(num_iterations): 

我们了解到,在策略控制方法中,我们不会提供任何策略作为输入。因此,在第一次迭代时,我们初始化一个随机策略,并通过计算 Q 值迭代地改进策略。由于我们从 Q 函数中提取策略,所以不需要显式定义策略。随着 Q 值的改进,策略也会隐式改进。也就是说,在第一次迭代时,我们通过从初始化的 Q 函数中提取策略(epsilon-greedy)来生成回合。经过一系列迭代后,我们会找到最优的 Q 函数,因此也找到了最优的策略。

所以,在这里我们传递初始化的 Q 函数来生成一个回合:

 episode = generate_episode(Q) 

获取回合中的所有状态-动作对:

 all_state_action_pairs = [(s, a) for (s,a,r) in episode] 

将回合中获得的所有奖励存储在奖励列表中:

 rewards = [r for (s,a,r) in episode] 

每个回合中的每一步:

 for t, (state, action,_) in enumerate(episode): 

如果状态-动作对在该回合中首次出现:

 if not (state, action) in all_state_action_pairs[0:t]: 

计算状态-动作对的回报 R,作为奖励的总和,R(s[t], a[t]) = sum(rewards[t:]):

 R = sum(rewards[t:]) 

更新状态-动作对的总回报为 total_return(s[t], a[t]) = total_return(s[t], a[t]) + R(s[t], a[t]):

 total_return[(state,action)] = total_return[(state,action)] + R 

更新状态-动作对被访问的次数为 N(s[t], a[t]) = N(s[t], a[t]) + 1:

 N[(state, action)] += 1 

通过简单地取平均值来计算 Q 值,即:

 Q[(state,action)] = total_return[(state, action)] / N[(state, action)] 

因此,在每次迭代中,Q 值都会改进,策略也会随之改进。

所有迭代结束后,我们可以查看 pandas 数据框中每个状态-动作对的 Q 值,以获得更清晰的了解。首先,让我们将 Q 值字典转换为 pandas 数据框:

df = pd.DataFrame(Q.items(),columns=['state_action pair','value']) 

让我们看一下数据框的前几行:

df.head(11) 

图 4.23:状态-动作对的 Q 值

正如我们所观察到的,我们得到了所有状态-动作对的 Q 值。现在我们可以通过选择每个状态中具有最大 Q 值的动作来提取策略。例如,假设我们在状态 (21,8, True) 中。现在,我们应该执行动作 0(stand)还是动作 1(hit)?在这里执行动作 0(stand)更有意义,因为我们卡牌的总值已经是 21,如果执行动作 1(hit),游戏就会爆掉。

请注意,由于随机性,你可能会得到与这里显示的不同的结果。

让我们查看该状态 (21,8, True) 中所有动作的 Q 值:

df[124:126] 

前面的代码将打印出以下内容:

图 4.24:状态 (21,8, True) 的 Q 值

正如我们所观察到的,与动作 1(hit)相比,动作 0(stand)的 Q 值最大。因此,我们在状态 (21,8, True) 下执行动作 0。同样地,通过这种方式,我们可以通过选择每个状态中具有最大 Q 值的动作来提取策略。

在下一节中,我们将学习一种使用两种不同策略的非策略控制方法。

非策略蒙特卡洛控制

离策略蒙特卡洛方法是另一种有趣的蒙特卡洛控制方法。在离策略方法中,我们使用两个策略,分别称为行为策略和目标策略。顾名思义,我们使用行为策略来进行操作(生成情节),而我们试图改进另一种策略,称为目标策略。

在在策略方法中,我们使用策略 生成情节,并且我们迭代地改进相同的策略 ,以找到最优策略。但在离策略方法中,我们使用一个名为行为策略b的策略生成情节,并且我们试图迭代地改进一个不同的策略,称为目标策略

也就是说,在在策略方法中,我们已经了解到,智能体使用策略 生成情节。然后,对于情节中的每个步骤,我们计算状态-动作对的回报,并将 Q 函数Q(s[t], a[t])作为平均回报来计算,然后从这个 Q 函数中提取一个新策略 。我们重复这一步骤,迭代地找到最优策略

但是在离策略方法中,智能体使用称为行为策略b的策略生成情节。然后,对于情节中的每个步骤,我们计算状态-动作对的回报,并将 Q 函数Q(s[t], a[t])作为平均回报来计算,然后从这个 Q 函数中提取一个新策略,称为目标策略 。我们重复这一步骤,迭代地找到最优的目标策略

行为策略通常设置为ε-greedy 策略,因此智能体会使用ε-greedy 策略探索环境并生成情节。与行为策略不同,目标策略设置为贪婪策略,因此目标策略在每个状态下始终选择最佳动作。

现在让我们具体了解离策略蒙特卡洛方法是如何工作的。首先,我们将 Q 函数初始化为随机值。然后,我们使用行为策略生成情节,行为策略为ε-greedy 策略。也就是说,从 Q 函数中,我们以 1-ε的概率选择最佳动作(即具有最大 Q 值的动作),以ε的概率选择随机动作。然后,对于情节中的每个步骤,我们计算状态-动作对的回报,并将 Q 函数Q(s[t], a[t])作为平均回报来计算。为了计算 Q 函数,我们可以使用增量均值,而不是使用算术均值。我们可以按照以下方式使用增量均值来计算 Q 函数:

在计算 Q 函数后,我们通过选择每个状态下具有最大 Q 值的动作来提取目标策略 ,如下所示:

算法如下所示:

  1. 将 Q 函数 Q(s, a) 初始化为随机值,将行为策略 b 设置为ε-贪婪策略,同时将目标策略 设置为贪婪策略。

  2. 对于 M 次回合:

    1. 使用行为策略 b 生成一个回合

    2. 初始化返回值 R 为 0

    3. 对于回合中的每一步 tt = T-1, T-2,…, 0:

      1. 计算返回值为 R = R+r[t][+1]

      2. 计算 Q 值为

      3. 计算目标策略

  3. 返回目标策略

正如我们从前面的算法中观察到的,首先我们将所有状态-动作对的 Q 值设置为随机值,然后使用行为策略生成一个回合。接着在每个回合的每一步,我们使用增量平均来计算更新后的 Q 函数(Q 值),然后从更新后的 Q 函数中提取目标策略。正如我们所注意到的,在每次迭代中,Q 函数在不断改进,且由于我们是从 Q 函数中提取目标策略,我们的目标策略在每次迭代中也会不断改进。

同时需要注意的是,由于这是一种非策略方法,回合是使用行为策略生成的,而我们则尝试改进目标策略。

但是等等!这里有一个小问题。由于我们是从 Q 函数中找到目标策略 ,而 Q 函数是基于由另一种策略——行为策略生成的回合计算出来的,因此我们的目标策略会不准确。原因是行为策略和目标策略的分布会有所不同。因此,为了纠正这一点,我们引入了一种新的技术,叫做重要性采样。这是一种通过给定来自另一个分布的样本来估计一个分布的值的技术。

假设我们想要计算一个函数 f(x) 的期望,其中 x 的值是从分布 p(x) 中采样的,也就是 ;那么我们可以写成:

使用重要性采样方法,我们通过一个不同的分布 q(x) 来估计期望;也就是说,我们不从 p(x) 中采样 x,而是使用一个不同的分布 q(x),如下面所示:

该比率 被称为重要性采样比率或重要性修正。

好的,重要性采样怎么帮助我们呢?我们了解到,使用重要性采样,我们可以通过使用重要性采样比率从另一个分布中采样来估计一个分布的值。在非策略控制中,我们可以使用来自行为策略的样本(回合)通过重要性采样比率来估计目标策略。

重要性采样有两种类型:

  • 普通重要性采样

  • 加权重要性采样

在普通重要性采样中,重要性采样比率将是目标策略与行为策略的比率 ,而在加权重要性采样中,重要性采样比率将是目标策略与行为策略的加权比率

现在我们来理解如何在脱策略蒙特卡罗方法中使用加权重要性采样。设W为权重,C(s[t], a[t])表示所有回合的权重累加和。我们了解到,计算 Q 函数(Q 值)时使用增量平均值,如下所示:

现在,我们略微修改了 Q 函数计算,采用加权重要性采样,如下所示:

脱策略蒙特卡罗方法的算法如下。首先,我们使用行为策略生成一个回合,然后将返回值R初始化为 0,权重W初始化为 1。接着,在回合的每一步,我们计算返回值并更新累积权重为 C(s[t], a[t]) = C(s[t], a[t]) + W。更新累积权重后,我们更新 Q 值为

从 Q 值中,我们提取目标策略为 。当行为策略和目标策略给定的动作 a[t] 不同,则退出循环并生成下一个回合;否则,我们将权重更新为

脱策略蒙特卡罗方法的完整算法在以下步骤中进行了说明:

  1. 将 Q 函数 Q(s, a) 初始化为随机值,将行为策略 b 设置为 epsilon-贪婪策略,目标策略 设置为贪婪策略,并将累积权重初始化为 C(s, a) = 0

  2. 对于M个回合:

    1. 使用行为策略 b 生成一个回合

    2. 将返回值R初始化为 0,权重W初始化为 1

    3. 对于回合中的每一步 tt = T-1, T-2,…, 0:

      1. 将返回值计算为 R = R + r[t][+1]

      2. 更新累积权重 C(s[t], a[t]) = C(s[t], a[t]) + W

      3. 更新 Q 值,如

      4. 计算目标策略

      5. 如果 ,则退出

      6. 更新权重为

  3. 返回目标策略

MC 方法适用于所有任务吗?

我们了解到,蒙特卡罗是一种无模型方法,因此它不需要环境的模型动态来计算值函数和 Q 函数,从而找到最优策略。蒙特卡罗方法通过仅计算状态的平均返回值和状态-动作对的平均返回值,分别来计算值函数和 Q 函数。

但是蒙特卡洛方法的一个问题是,它仅适用于回合任务。我们了解到,在蒙特卡洛方法中,我们通过取状态的平均回报来计算状态的值,而回报是回合的奖励总和。但是当没有回合时,也就是当我们的任务是连续任务(非回合任务)时,就无法应用蒙特卡洛方法。

好的,当我们有一个连续任务并且不知道环境的模型动态时,如何计算该状态的值呢?在这里,我们使用另一种有趣的无模型方法,称为时间差分学习。下一章,我们将准确了解时间差分学习是如何工作的。

总结

我们通过理解蒙特卡洛方法开始了本章的学习。我们了解到,在蒙特卡洛方法中,我们通过采样来近似随机变量的期望,当样本量更大时,近似结果会更好。接着,我们学习了预测和控制任务。在预测任务中,我们通过预测价值函数或 Q 函数来评估给定的策略,这有助于我们理解如果智能体使用给定的策略,预期会得到的回报。在控制任务中,我们的目标是找到最优策略,且不会给出任何策略作为输入,因此我们从初始化一个随机策略开始,并通过迭代方法寻找最优策略。

接下来,我们学习了如何使用蒙特卡洛方法执行预测任务。我们了解到,状态的值和状态-动作对的值可以通过分别取状态和状态-动作对在多个回合中的平均回报来计算。

我们还学习了首次访问 MC 和每次访问 MC 方法。在首次访问 MC 中,我们仅计算状态在回合中第一次访问时的回报,而在每次访问 MC 中,我们每次访问状态时都会计算回报。

然后,我们探讨了如何使用蒙特卡洛方法执行控制任务。我们学习了两种不同的控制方法——在策略控制和离策略控制。

在策略梯度方法中,我们使用一种策略生成回合,并且通过迭代改进同一策略来找到最优策略。我们首先学习了蒙特卡洛控制探索起始方法,在这种方法中,我们将所有状态-动作对的概率设置为非零概率,以确保进行探索。后来,我们学习了使用ε-贪婪策略的蒙特卡洛控制方法,在这种方法中,我们以概率ε选择一个随机动作(探索),以概率 1-ε选择最优动作(利用)。

在本章的最后,我们讨论了离策略蒙特卡洛控制方法,在这种方法中,我们使用两种不同的策略:行为策略(用于生成回合)和目标策略(用于找到最优策略)。

问题

让我们通过回答以下问题来评估我们对蒙特卡洛方法的理解:

  1. 什么是蒙特卡洛方法?

  2. 为什么蒙特卡洛方法比动态规划更受青睐?

  3. 预测任务与控制任务有什么区别?

  4. 蒙特卡洛预测方法是如何预测价值函数的?

  5. 首次访问蒙特卡洛(first-visit MC)与每次访问蒙特卡洛(every-visit MC)有何不同?

  6. 为什么我们使用增量均值更新(incremental mean updates)?

  7. 基于策略控制(on-policy control)与离策略控制(off-policy control)有什么区别?

  8. 什么是ε-贪婪策略(epsilon-greedy policy)?

第五章:理解时间差学习

时间差TD)学习是最受欢迎且广泛使用的无模型方法之一。原因在于,TD 学习结合了我们在前几章中介绍的动态规划DP)方法和蒙特卡洛MC)方法的优点。

本章开始时,我们将了解与 DP 和 MC 方法相比,TD 学习究竟有哪些好处。随后,我们将学习如何使用 TD 学习进行预测任务。接下来,我们将学习如何使用一种称为 SARSA 的基于策略 TD 控制方法和一种称为 Q 学习的离策略 TD 控制方法来执行 TD 控制任务。

我们还将学习如何在 Frozen Lake 环境中使用 SARSA 和 Q 学习方法找到最优策略。在本章结束时,我们将比较 DP、MC 和 TD 方法。

因此,在本章中,我们将学习以下主题:

  • TD 学习

  • TD 预测方法

  • TD 控制方法

  • 基于策略 TD 控制 – SARSA

  • 离策略 TD 控制 – Q 学习

  • 实现 SARSA 和 Q 学习以找到最优策略

  • Q 学习和 SARSA 的区别

  • 比较 DP、MC 和 TD 方法

TD 学习

TD 学习算法由理查德·S·萨顿(Richard S. Sutton)于 1988 年提出。在本章的介绍中,我们了解到,TD 方法之所以受到欢迎,是因为它结合了 DP 和 MC 方法的优点。那么,这些优点到底是什么呢?

首先,让我们快速回顾一下 DP 和 MC 方法的优缺点。

动态规划——DP 方法的优点是它使用贝尔曼方程来计算一个状态的价值。也就是说,我们已经了解到,根据贝尔曼方程,状态的价值可以通过即时奖励和下一个状态的折扣价值的总和来获得。这被称为自举(bootstrapping)。也就是说,计算一个状态的价值时,我们不必等到回合结束,相反,通过使用贝尔曼方程,我们可以仅根据下一个状态的价值来估算当前状态的价值,这就是所谓的自举。

记得我们如何在动态规划(DP)方法中估计价值函数吗(价值迭代和策略迭代)?我们将价值函数(一个状态的价值)估计为:

正如你可能记得的那样,我们学习到,为了找到一个状态的价值,我们不必等到回合结束。相反,我们进行自举,也就是说,我们通过估算下一个状态的价值来估算当前状态的价值 V(s)

然而,动态规划的缺点在于,只有在我们知道环境的模型动态时才能使用 DP 方法。也就是说,DP 是一种基于模型的方法,我们应该知道转移概率才能使用它。当我们不知道环境的模型动态时,就无法应用 DP 方法。

蒙特卡罗方法—MC 方法的优点在于它是一种无模型方法,这意味着它不需要已知环境的模型动态即可估算值函数和 Q 函数。

然而,MC 方法的缺点在于,为了估算状态值或 Q 值,我们需要等到回合结束,而如果回合较长,这将消耗大量时间。此外,我们无法将 MC 方法应用于连续任务(非回合任务)。

现在,让我们回到 TD 学习。TD 学习算法考虑了 DP 和 MC 方法的优点。因此,像在 DP 中一样,我们执行自举(bootstrapping),这样我们就不必等到回合结束才能计算状态值或 Q 值;像 MC 方法一样,它是一种无模型方法,因此在计算状态值或 Q 值时不需要环境的模型动态。现在我们已经了解了 TD 学习算法的基本理念,让我们深入细节,学习它是如何工作的。

类似于我们在第四章中学到的蒙特卡罗方法,我们可以将 TD 学习算法应用于预测任务和控制任务,因此我们可以将 TD 学习分为:

  • TD 预测

  • TD 控制

在上一章中,我们学习了预测方法和控制方法的含义。让我们在继续之前稍作回顾。

在预测方法中,给定一个策略作为输入,我们尝试使用该策略预测值函数或 Q 函数。如果我们使用给定的策略预测值函数,那么我们就能知道,如果代理按照该策略行动,它在每个状态中的表现如何。换句话说,我们可以知道代理在每个状态下,如果遵循给定策略,它能获得的预期回报。

在控制方法中,我们没有给定一个策略作为输入,控制方法的目标是找到最优策略。因此,我们初始化一个随机策略,然后我们尝试通过迭代找到最优策略。也就是说,我们尝试找到一个给我们最大回报的最优策略。

首先,让我们看看如何使用 TD 学习来执行预测任务,然后我们将学习如何使用 TD 学习来执行控制任务。

TD 预测

在 TD 预测方法中,给定一个策略作为输入,我们尝试使用该策略估算值函数。TD 学习像 DP 一样进行自举(bootstrapping),因此它不需要等到回合结束,而像 MC 方法一样,它不需要环境的模型动态来计算值函数或 Q 函数。现在,让我们看看 TD 学习的更新规则是如何设计的,考虑到前述的优点。

在 MC 方法中,我们通过取回报来估算一个状态的值:

然而,单一的回报值无法完美地近似一个状态的值。因此,我们生成N个回合,并将一个状态的值计算为该状态在N个回合中的平均回报值:

但是使用 MC 方法时,我们需要等到回合结束后才能计算状态的值,而当回合较长时,这需要花费大量时间。MC 方法的另一个问题是,我们无法将其应用于非回合任务(连续任务)。

因此,在 TD 学习中,我们利用自举(bootstrapping)并估计状态的值,如下所示:

上面的方程告诉我们,我们只需要考虑即时奖励 r 和下一个状态的折扣值 就能估计状态的值。如你从上述方程中所观察到的,与我们在 DP 方法(值迭代和策略迭代)中学到的相似,我们进行了自举,但在这里我们无需了解模型动态。

因此,使用 TD 学习,状态的值被近似为:

然而,单一的 值不能完美地近似状态的值。因此,我们可以取一个均值,而不是算术平均数,我们可以使用增量均值。

在 MC 方法中,我们学会了如何使用增量均值来估计状态的值,其公式如下:

类似地,在 TD 学习中,我们可以使用增量均值来估计状态的值,如下所示:

这个方程被称为 TD 学习更新规则。正如我们所观察到的,TD 学习与 MC 方法之间的唯一区别是:在 MC 方法中,我们使用完整回合计算得到的总回报 R 来计算状态值,而在 TD 学习方法中,我们使用自举估计值 ,因此我们无需等到回合结束才能计算状态的值。这样,我们就可以将 TD 学习应用于非回合任务。以下展示了 MC 方法和 TD 学习之间的区别:

图 5.1:MC 与 TD 学习的比较

因此,我们的 TD 学习更新规则是:

我们了解到, 是状态 V(s) 的一个估计值。因此,我们可以将 称为 TD 目标。这样,从 中减去 V(s) 就意味着我们在从目标值中减去预测值,这通常称为 TD 误差。好的,那么那个 呢?它基本上是学习率,也叫步长。即:

我们的 TD 学习更新规则基本上意味着:

状态的值 = 状态的值 + 学习率(奖励 + 折扣因子(下一个状态的值) - 状态的值)

现在我们已经了解了 TD 学习更新规则以及 TD 学习如何用于估计状态的值,在接下来的章节中,我们将深入探讨 TD 预测算法,并更清晰地理解 TD 学习方法。

TD 预测算法

我们了解到,在预测任务中,给定一个策略后,我们根据该策略估计值函数。因此,我们可以说,在每个状态下,智能体如果按照给定策略行动,期望获得的回报是多少。

我们了解到,TD 学习更新规则如下所示:

因此,通过这个方程,我们可以估计给定策略的值函数。

在直接查看算法之前,为了更好地理解,我们先手动计算并看看如何准确地使用 TD 学习更新规则估计状态的值。

接下来的章节通过手动计算进行解释,为了更好理解,建议拿起笔和纸跟着一起做。

让我们在“冻结湖泊”环境中探索 TD 预测。我们已经了解到,在“冻结湖泊”环境中,智能体的目标是从起始状态 S 到达目标状态 G,并避免经过洞穴状态 H。如果智能体到达状态 G,我们给予奖励 1,如果经过其他状态,我们则给予奖励 0。图 5.2 显示了“冻结湖泊”环境:

图 5.2:冻结湖泊环境

我们的动作空间有四个动作,分别是 ,而我们有从 SG 的 16 个状态。为了更方便理解,暂时不将状态和动作编码成数字,我们直接使用它们的名称。也就是说,假设状态 S(1,1),状态 F(1,2),依此类推,直到最后的状态 G,即 (4,4)

现在,让我们学习如何在“冻结湖泊”环境中执行 TD 预测。我们知道,在 TD 预测方法中,给定一个策略后,我们根据该策略预测状态值函数(状态的价值)。假设我们给定了以下策略。它基本上告诉我们在每个状态下应执行的动作:

表 5.1:一个策略

现在,我们将看到如何使用 TD 学习方法估计前述策略的值函数。在继续之前,我们首先将所有状态的值初始化为随机值,如下所示:

图 5.3:用随机值初始化状态

假设我们处于状态 (1,1),根据给定的策略,我们执行 向的动作并移动到下一个状态 (1,2),并获得奖励 r 为 0。我们将学习率 设置为 0.1,折扣因子 设置为 1。现在,如何更新状态的值呢?

回顾 TD 更新方程:

将状态 V(s) 中的 V(1,1) 和下一个状态 中的 V(1,2) 代入前面的方程中,我们可以得到:

将奖励 r = 0、学习率 和折扣因子 代入,我们可以写成:

我们可以从前面的价值表中获得状态值。也就是说,从前面的价值表中可以观察到状态(1,1)的值是 0.9,下一状态(1,2)的值是 0.6。将这些值代入前面的方程中,我们可以写成:

因此,状态(1,1)的值变为:

所以,我们将状态(1,1)的值更新为0.87,如图 5.4所示:

图 5.4: 状态 (1,1) 的值被更新

现在我们处于状态(1,2)。根据给定的策略,在状态(1,2)中我们选择动作,移动到下一个状态(1,3)并获得奖励 r 为 0。我们可以计算状态的值如下:

将状态 V(s) 的值替换为 V(1,2),将下一个状态 替换为 V(1,3),我们可以写成:

将奖励 r = 0、学习率 和折扣因子 代入,我们可以写成:

从前面的价值表中,我们可以观察到状态(1,2)的值是 0.6,下一状态(1,3)的值是 0.8,因此我们可以写成:

因此,状态(1,2)的值变为:

所以,我们将状态(1,2)的值更新为0.62,如图 5.5所示:

图 5.5: 状态 (1,2) 的值被更新

现在我们处于状态(1,3)。根据我们的策略,我们选择动作,移动到下一个状态(1,2)并获得奖励 r 为 0。我们可以计算状态的值如下:

将状态 V(s) 的值替换为 V(1,3),将下一个状态 替换为 V(1,2),我们得到:

将奖励 r = 0、学习率 和折扣因子 代入,我们可以写成:

请注意,我们在每一步都使用更新后的值,即状态(1,2)的值在上一阶段被更新为 0.62,如前面的价值表所示。因此,我们将 V(1,2) 替换为 0.62,将 V(1,3) 替换为 0.8:

因此,状态(1,3)的值变为:

所以,我们将状态(1,3)的值更新为0.782,如图 5.6所示:

图 5.6: 状态 (1,3) 的值被更新

因此,通过这种方式,我们使用给定的策略计算每个状态的值。然而,仅为一个回合计算状态的值是不准确的。因此,我们将这些步骤重复多次,计算状态值的准确估计(值函数)。

TD 预测算法如下所示:

  1. 初始化一个带有随机值的值函数 V(s)。给定一个策略

  2. 对于每个回合:

    1. 初始化状态 s

    2. 对于每个步骤:

      1. 根据给定策略 在状态 s 中执行动作 a,获取奖励 r,并移动到下一个状态

      2. 将状态的值更新为

      3. 更新 (这一步意味着我们将下一个状态 更新为当前状态 s

      4. 如果 s 不是终止状态,则重复 步骤 14

现在我们已经了解了 TD 预测方法如何预测给定策略的值函数,在下一节中,让我们学习如何实现 TD 预测方法,预测冻结湖环境中状态的值。

在冻结湖环境中预测状态的值

我们已经了解,在预测方法中,策略作为输入给定,我们使用给定的策略预测值函数。所以,让我们初始化一个随机策略,并使用该随机策略预测冻结湖环境中的值函数(状态值)。

首先,让我们导入必要的库:

import gym
import pandas as pd 

现在,我们使用 Gym 创建冻结湖环境:

env = gym.make('FrozenLake-v0') 

定义随机策略,通过从动作空间中采样返回随机动作:

def random_policy():
    return env.action_space.sample() 

让我们定义一个字典来存储状态的值,并将所有状态的值初始化为 0.0

V = {}
for s in range(env.observation_space.n):
    V[s]=0.0 

初始化折扣因子 和学习率

alpha = 0.85
gamma = 0.90 

设置回合数和每个回合中的时间步数:

num_episodes = 50000
num_timesteps = 1000 

计算状态的值

现在,让我们使用给定的随机策略计算值函数(状态值)。

对于每个回合:

for i in range(num_episodes): 

通过重置环境来初始化状态:

 s = env.reset() 

对于回合中的每个步骤:

 for t in range(num_timesteps): 

根据随机策略选择一个动作:

 a = random_policy() 

执行选择的动作并存储下一个状态信息:

 s_, r, done, _ = env.step(a) 

计算状态的值,如 所示:

 V[s] += alpha * (r + gamma * V[s_]-V[s]) 

更新下一个状态为当前状态

 s = s_ 

如果当前状态是终止状态,则跳出:

 if done:
            break 

在所有迭代完成后,我们将根据给定的随机策略得到所有状态的值。

评估状态的值

现在,让我们评估我们的值函数(状态值)。首先,让我们将值字典转换为 pandas 数据框以便更清晰地查看:

df = pd.DataFrame(list(V.items()), columns=['state', 'value']) 

在检查状态的值之前,让我们回顾一下在 Gym 中,所有 Frozen Lake 环境中的状态将被编码为数字。由于我们有 16 个状态,所有状态将被编码为数字 0 到 15,如 图 5.7 所示:

图 5.7:状态编码为数字

现在,让我们检查状态的值:

df 

前面的代码将打印:

图 5.8:值表

正如我们所观察到的,现在我们已经得到了所有状态的值。状态 14 的值很高,因为我们可以轻松地从状态 14 达到目标状态 15,而且正如我们所见,所有终端状态(洞状态和目标状态)的值都是零。

请注意,由于我们初始化了一个随机策略,每次运行前面的代码时可能会得到不同的结果。

现在我们已经理解了如何利用 TD 学习进行预测任务,在下一节中,我们将学习如何将 TD 学习应用于控制任务。

TD 控制

在控制方法中,我们的目标是找到最优策略,因此我们将从一个初始随机策略开始,并尝试迭代找到最优策略。在前一章中,我们学习到控制方法可以分为两类:

  • 在策略控制

  • 离策略控制

我们在前一章中学到了在策略和离策略控制的含义。在继续之前,让我们简要回顾一下。在 在策略控制 中,代理使用一个策略行为并尝试改进同一策略。也就是说,在在策略方法中,我们使用一个策略生成情节并迭代改进相同的策略以找到最优策略。在 离策略控制 方法中,代理使用一个策略行为并尝试改进一个不同的策略。也就是说,在离策略方法中,我们使用一个策略生成情节,并尝试迭代改进一个不同的策略以找到最优策略。

现在,我们将学习如何使用 TD 学习执行控制任务。首先,我们将学习如何执行在策略 TD 控制,然后我们将学习关于离策略 TD 控制。

在策略 TD 控制 - SARSA

在本节中,我们将研究流行的在策略 TD 控制算法,称为 SARSA,其代表了状态-动作-奖励-状态-动作。我们知道,在 TD 控制中,我们的目标是找到最优策略。首先,我们如何从策略中提取策略?我们可以从 Q 函数中提取策略。也就是说,一旦我们有了 Q 函数,我们就可以通过选择每个状态中具有最大 Q 值的动作来提取策略。

好的,我们如何在 TD 学习中计算 Q 函数?首先,让我们回顾一下如何计算值函数。在 TD 学习中,值函数计算为:

我们可以简单地将这个更新规则重写为 Q 函数的形式:

现在,我们使用前述的 TD 学习更新规则计算 Q 函数,然后从中提取策略。我们也可以将前述的更新规则称为 SARSA 更新规则。

但是等等!在预测方法中,我们给定了一个策略作为输入,因此我们在环境中根据该策略进行操作,并计算了价值函数。但在这里,我们没有输入策略。那么,我们如何在环境中行动呢?

首先,我们将 Q 函数初始化为随机值或零。然后,我们从这个随机初始化的 Q 函数中提取策略并在环境中进行操作。我们的初始策略肯定不是最优的,因为它是从随机初始化的 Q 函数中提取的,但每一轮后我们会更新 Q 函数(Q 值)。因此,在每一轮后,我们可以使用更新后的 Q 函数来提取新策略。这样,通过一系列回合后,我们将获得最优策略。

我们需要注意的一个重要点是,在 SARSA 方法中,我们并不是让我们的策略贪婪地执行,而是使用 epsilon-贪婪策略。也就是说,在贪婪策略中,我们总是选择具有最大 Q 值的操作。但是,使用 epsilon-贪婪策略时,我们以概率 epsilon 选择一个随机动作,并以概率 1-epsilon 选择最佳动作(即具有最大 Q 值的动作)。

在直接查看算法之前,为了更好地理解,我们首先手动计算并观察 Q 函数(Q 值)是如何通过 SARSA 更新规则估算的,以及我们如何找到最优策略。

让我们考虑相同的冰冻湖环境。在继续之前,我们将 Q 表(Q 函数)初始化为随机值。图 5.9 展示了冰冻湖环境以及包含随机值的 Q 表:

图 5.9:冰冻湖环境和带有随机值的 Q 表

假设我们处于状态 (4,2)。现在我们需要在这个状态下选择一个动作。我们如何选择动作呢?我们知道,在 SARSA 方法中,我们根据 epsilon-贪婪策略选择动作。以概率 epsilon,我们选择一个随机动作;以概率 1-epsilon,我们选择最佳动作(即具有最大 Q 值的动作)。假设我们使用概率 1-epsilon 选择最佳动作。因此,在状态 (4,2) 中,我们选择 ,因为它相对于其他动作具有最高的 Q 值,如下所示:

图 5.10:我们的智能体处于状态 (4,2)

好的,那么,我们在状态 (4,2) 执行 操作并移动到下一个状态 (4,3),正如 图 5.11 所示:

图 5.11:我们在状态 (4,2) 执行具有最大 Q 值的动作

因此,我们在状态 (4,2) 移动到下一个状态 (4,3) 并获得了奖励 r 为 0。我们将学习率 设置为 0.1,折扣因子 设置为 1。那么,我们该如何更新 Q 值呢?

让我们回顾一下 SARSA 更新规则:

将状态-动作对 Q(s,a) 替换为 Q((4,2), right) 和下一个状态 替换为 (4,3),代入前面的方程,我们可以写出:

将奖励 r = 0,学习率 ,折扣因子 代入方程,我们可以写出:

从之前的 Q 表中,我们可以观察到 Q((4,2), right) 的 Q 值为 0.8。因此,代入 Q((4,2), right) 为 0.8,我们可以将前面的方程重写为:

好的,那项 怎么处理呢?如前面的方程所示,我们有一项 ,它表示下一个状态-动作对的 Q 值。

因为我们已经移动到下一个状态 (4,3),所以我们需要在此状态下选择一个动作,以便计算下一个状态-动作对的 Q 值。因此,我们使用相同的 epsilon-greedy 策略来选择动作。也就是说,我们以概率 epsilon 随机选择一个动作,或者以概率 1-epsilon 选择具有最大 Q 值的最佳动作。

假设我们使用概率 epsilon 并选择随机动作。在状态 (4,3),我们随机选择 right 动作,如 图 5.12 所示。如你所见,尽管 right 动作的 Q 值不是最大,但我们仍然以概率 epsilon 随机选择了它:

图 5.12:我们在状态 (4,3) 执行了一个随机动作

因此,现在我们的更新规则变为:

从前面的 Q 表中,我们可以看到 Q((4,3), right) 的 Q 值为 0.9。因此,代入 Q((4,3), right) 的值 0.9,我们可以将上面的方程重写为:

因此,我们的 Q 值变为:

因此,通过这种方式,我们在每一步骤中更新 Q 函数,通过更新状态-动作对的 Q 值来实现。完成一个回合后,我们从更新后的 Q 函数中提取出新的策略,并使用该新策略在环境中执行动作。(记住我们的策略始终是 epsilon-greedy 策略)。我们重复这些步骤进行若干回合,以找到最优策略。下面给出的 SARSA 算法将帮助我们更好地理解这一点。

SARSA 算法如下所示:

  1. 初始化 Q 函数 Q(s, a),并赋予随机值

  2. 对于每个回合:

    1. 初始化状态 s

    2. Q(s, a) 中提取一个策略,并选择在状态 s 下执行的动作 a

    3. 对于每一步骤:

      1. 执行动作 a 并移动到下一个状态 ,然后观察奖励 r

      2. 在状态 下,使用 epsilon-greedy 策略选择动作

      3. 将 Q 值更新为

      4. 更新 (将下一个状态 -动作 对更新为当前状态 s-动作 a 对)

      5. 如果 s 不是终止状态,则重复 步骤 1步骤 5

既然我们已经了解了 SARSA 算法是如何工作的,那么在接下来的部分,我们将实现 SARSA 算法以找到最优策略。

使用 SARSA 计算最优策略

现在,让我们实现 SARSA 来在 Frozen Lake 环境中找到最优策略。

首先,让我们导入必要的库:

import gym
import random 

现在,我们使用 Gym 创建 Frozen Lake 环境:

env = gym.make('FrozenLake-v0') 

让我们定义一个字典来存储状态-动作对的 Q 值,并将所有状态-动作对的 Q 值初始化为 0.0

Q = {}
for s in range(env.observation_space.n):
    for a in range(env.action_space.n):
        Q[(s,a)] = 0.0 

现在,让我们定义 epsilon-greedy 策略。我们从均匀分布中生成一个随机数,如果随机数小于 epsilon,则选择随机动作,否则选择具有最大 Q 值的最佳动作:

def epsilon_greedy(state, epsilon):
    if random.uniform(0,1) < epsilon:
        return env.action_space.sample()
    else:
        return max(list(range(env.action_space.n)), key = lambda x: Q[(state,x)]) 

初始化折扣因子 ,学习率 和 epsilon 值:

alpha = 0.85
gamma = 0.90
epsilon = 0.8 

设置回合数和每回合的时间步数:

num_episodes = 50000
num_timesteps = 1000 

计算策略

对于每一回合:

for i in range(num_episodes): 

通过重置环境初始化状态:

 s = env.reset() 

使用 epsilon-greedy 策略选择动作:

 a = epsilon_greedy(s,epsilon) 

对于每个步骤,在这一回合中:

 for t in range(num_timesteps): 

执行所选动作并存储下一个状态信息:

 s_, r, done, _ = env.step(a) 

使用 epsilon-greedy 策略在下一个状态 中选择动作

 a_ = epsilon_greedy(s_,epsilon) 

计算状态-动作对的 Q 值,公式为

 Q[(s,a)] += alpha * (r + gamma * Q[(s_,a_)]-Q[(s,a)]) 

更新 (将下一个状态 -动作 对更新为当前状态 s-动作 a 对):

 s = s_
        a = a_ 

如果当前状态是终止状态,则跳出:

 if done:
            break 

请注意,在每次迭代中我们都会更新 Q 函数。经过所有迭代后,我们将得到最优的 Q 函数。一旦我们有了最优的 Q 函数,就可以通过选择每个状态下具有最大 Q 值的动作来提取最优策略。

脱策略 TD 控制 - Q 学习

在这一部分,我们将学习一种称为 Q 学习的脱策略 TD 控制算法。它是强化学习中非常流行的算法之一,我们将看到这一算法在其他章节中也会出现。Q 学习是一种脱策略算法,这意味着我们使用两种不同的策略,一种策略用于在环境中行为(选择动作),另一种策略用于寻找最优策略。

我们了解到,在 SARSA 方法中,我们使用 epsilon-greedy 策略在状态 s 中选择动作 a,然后转移到下一个状态 ,并使用此处显示的更新规则来更新 Q 值:

在前面的公式中,为了计算下一个状态-动作对的 Q 值,,我们需要选择一个动作。因此,我们使用相同的 epsilon-贪心策略选择动作,并更新下一个状态-动作对的 Q 值。

但是与 SARSA 不同,在 Q 学习中,我们使用两种不同的策略。一个是 epsilon-贪心策略,另一个是贪心策略。为了在环境中选择一个动作,我们使用 epsilon-贪心策略,但在更新下一个状态-动作对的 Q 值时,我们使用贪心策略。

也就是说,我们在状态s中使用 epsilon-贪心策略选择动作a,并移动到下一个状态 ,然后使用下面的更新规则更新 Q 值:

在前面的公式中,为了计算下一个状态-动作对的 Q 值,,我们需要选择一个动作。在这里,我们使用贪心策略选择动作,并更新下一个状态-动作对的 Q 值。我们知道,贪心策略总是选择具有最大 Q 值的动作。因此,我们可以将公式修改为:

从前面的公式中可以观察到,max 操作符意味着在状态 中,我们选择具有最大 Q 值的动作

因此,总结一下,在 Q 学习方法中,我们使用 epsilon-贪心策略在环境中选择动作,但在计算下一个状态-动作对的 Q 值时,我们使用贪心策略。因此,Q 学习的更新规则如下:

让我们通过手动计算 Q 值来更好地理解这一点,使用我们的 Q 学习更新规则。我们使用相同的冻结湖示例。我们将 Q 表初始化为随机值。图 5.13 显示了冻结湖环境,以及包含随机值的 Q 表:

图 5.13:带有随机初始化 Q 表的冻结湖环境

假设我们处于状态(3,2)。现在,我们需要选择该状态中的某个动作。我们如何选择动作呢?我们使用 epsilon-贪心策略选择一个动作。因此,以概率 epsilon,我们选择一个随机动作,而以概率 1-epsilon,我们选择具有最大 Q 值的最佳动作。

假设我们使用概率 1-epsilon 选择最佳动作。因此,在状态(3,2) 中,我们选择向下动作,因为它相对于该状态中的其他动作具有最高的 Q 值,正如图 5.14 所示:

图 5.14:我们在状态(3,2)中执行具有最大 Q 值的动作

好的,所以,我们在状态(3,2) 中执行向下动作,并如图 5.15 所示移动到下一个状态(4,2)

图 5.15:我们移动到状态(4,2)

因此,我们在状态 (3,2) 中向 down 方向移动,进入下一个状态 (4,2) 并获得奖励 r 为 0。我们将学习率 设为 0.1,折扣因子 设为 1。现在,我们如何更新 Q 值呢?

让我们回顾一下我们的 Q 学习更新规则:

将状态-动作对 Q(s,a) 替换为 Q((3,2), down),并将下一个状态 替换为 (4,2),我们可以将前面的方程写成:

将奖励 r = 0、学习率 和折扣因子 代入方程中,我们可以写出:

从之前的 Q 表中,我们可以观察到 Q((3,2), down) 的值为 0.8。因此,将 Q((3,2), down) 替换为 0.8,我们可以将前面的方程改写为:

如我们所见,在前面的方程中,我们有一个项 ,它表示我们在移动到新状态 (4,2) 后,下一状态-动作对的 Q 值。为了计算下一个状态的 Q 值,首先我们需要选择一个动作。在这里,我们使用贪心策略选择动作,即选择具有最大 Q 值的动作。

图 5.16 所示,right 动作在状态 (4,2) 中具有最大 Q 值。因此,我们选择 right 动作,并更新下一个状态-动作对的 Q 值:

图 5.16:我们在状态 (4,2) 中执行具有最大 Q 值的动作

因此,现在我们的更新规则变为:

从之前的 Q 表中,我们可以观察到 Q((4,2), right) 的值为 0.8。因此,将 Q((4,2), right) 的值替换为 0.8,我们可以将上述方程改写为:

因此,我们的 Q 值变为:

同样,我们为所有状态-动作对更新 Q 值。也就是说,我们在环境中选择动作时使用 epsilon-greedy 策略,而在更新下一个状态-动作对的 Q 值时使用贪心策略。这样,我们为每个状态-动作对更新 Q 值。

因此,通过这种方式,我们在每一步的训练过程中更新 Q 函数,更新状态-动作对的 Q 值。我们将在每一步从更新后的 Q 函数中提取新的策略,并使用这个新策略。(记住,我们在环境中选择动作时使用 epsilon-greedy 策略,但在更新下一个状态-动作对的 Q 值时使用贪心策略。)经过若干回合后,我们将得到最优的 Q 函数。以下给出的 Q 学习算法将帮助我们更好地理解这一点。

Q 学习算法如下所示:

  1. 初始化一个随机值的 Q 函数 Q(s, a)

  2. 对于每一回合:

    1. 初始化状态 s

    2. 对于回合中的每一步:

      1. Q(s, a) 中提取策略,并选择一个动作 a 在状态 s 中执行

      2. 执行动作 a,转移到下一个状态 ,并观察奖励 r

      3. 更新 Q 值为

      4. 更新 (将下一个状态 更新为当前状态 s

      5. 如果 s 不是终止状态,重复 步骤 1步骤 5

现在我们已经了解了 Q 学习算法是如何工作的,在下一节中,让我们实现 Q 学习以找到最优策略。

使用 Q 学习计算最优策略

现在,让我们在 Frozen Lake 环境中实现 Q 学习,找到最优策略。

首先,让我们导入必要的库:

import gym
import numpy as np
import random 

现在,我们使用 Gym 创建 Frozen Lake 环境:

env = gym.make('FrozenLake-v0') 

我们先定义一个字典来存储状态-动作对的 Q 值,并将所有状态-动作对的 Q 值初始化为0.0

Q = {}
for s in range(env.observation_space.n):
    for a in range(env.action_space.n):
        Q[(s,a)] = 0.0 

现在,让我们定义ε-贪婪策略。我们从均匀分布中生成一个随机数,如果该随机数小于ε,则选择一个随机动作,否则选择具有最大 Q 值的最佳动作:

def epsilon_greedy(state, epsilon):
    if random.uniform(0,1) < epsilon:
        return env.action_space.sample()
    else:
        return max(list(range(env.action_space.n)), key = lambda x: Q[(state,x)]) 

初始化折扣因子 、学习率 和ε值:

alpha = 0.85
gamma = 0.90
epsilon = 0.8 

设置回合数和每个回合中的时间步数:

num_episodes = 50000
num_timesteps = 1000 

计算策略。

对于每个回合:

for i in range(num_episodes): 

通过重置环境来初始化状态:

 s = env.reset() 

对于回合中的每一步:

 for t in range(num_timesteps): 

使用ε-贪婪策略选择动作:

 a = epsilon_greedy(s,epsilon) 

执行选择的动作并存储下一个状态的信息:

 s_, r, done, _ = env.step(a) 

现在,让我们计算状态-动作对的 Q 值为

首先,选择下一个状态 中具有最大 Q 值的动作

 a_ = np.argmax([Q[(s_, a)] for a in range(env.action_space.n)]) 

现在,我们可以计算状态-动作对的 Q 值为:

 Q[(s,a)] += alpha * (r + gamma * Q[(s_,a_)]-Q[(s,a)]) 

更新 (将下一个状态 更新为当前状态 s):

 s = s_ 

如果当前状态是终止状态,则中断:

 if done:
            break 

所有迭代完成后,我们将得到最优 Q 函数。然后,我们可以通过选择每个状态中具有最大 Q 值的动作来提取最优策略。

Q 学习和 SARSA 的区别

理解 Q 学习和 SARSA 之间的区别非常重要。所以,让我们回顾一下 Q 学习和 SARSA 的不同之处。

SARSA 是一个基于策略的算法,这意味着我们使用一个单一的ε-贪婪策略来选择环境中的动作,并计算下一个状态-动作对的 Q 值。SARSA 的更新规则如下所示:

Q 学习是一个基于策略外的算法,这意味着我们在环境中使用ε-贪婪策略来选择动作,但在计算下一个状态-动作对的 Q 值时,我们使用贪婪策略。Q 学习的更新规则如下所示:

比较 DP、MC 和 TD 方法

到目前为止,我们已经学习了几种有趣且重要的强化学习算法,例如 DP(值迭代和策略迭代)、MC 方法和 TD 学习方法,来寻找最优策略。这些被称为经典强化学习中的关键算法,理解这三种算法之间的区别非常重要。因此,在本节中,我们将回顾 DP、MC 和 TD 学习方法之间的区别。

动态规划DP),即值迭代和策略迭代方法,是一种基于模型的方法,意味着我们使用环境的模型动态来计算最优策略。当我们没有环境的模型动态时,无法应用 DP 方法。

我们还学习了蒙特卡罗MC)方法。MC 是一种无模型方法,意味着我们在不使用环境模型动态的情况下计算最优策略。但我们在使用 MC 方法时遇到的一个问题是,它仅适用于回合任务,而不适用于连续任务。

我们学习了一种有趣的无模型方法,称为时间差分TD)学习。TD 学习结合了通过自举进行的动态规划(DP)和通过无模型方法进行的蒙特卡罗(MC)方法的优势。

恭喜你学习了所有重要的强化学习算法。在下一章中,我们将探讨一个案例研究,称为多臂赌博机问题。

总结

我们通过理解 TD 学习是什么以及它如何结合 DP 和 MC 方法的优势来开始本章。我们了解到,像 DP 一样,TD 学习也进行自举;像 MC 方法一样,TD 学习是一种无模型方法。

后来,我们学习了如何使用 TD 学习执行预测任务,然后我们研究了 TD 预测方法的算法。

接下来,我们学习了如何使用 TD 学习进行控制任务。首先,我们学习了名为 SARSA 的在政策 TD 控制方法,然后我们学习了名为 Q 学习的离政策 TD 控制方法。我们还学习了如何使用 SARSA 和 Q 学习方法在 Frozen Lake 环境中找到最优策略。

我们还学习了 SARSA 和 Q 学习方法的区别。我们理解到,SARSA 是一种在政策算法,这意味着我们使用一个单一的 epsilon-greedy 策略在环境中选择一个动作,并计算下一个状态-动作对的 Q 值;而 Q 学习是一种离政策算法,这意味着我们使用 epsilon-greedy 策略在环境中选择一个动作,但在计算下一个状态-动作对的 Q 值时使用贪心策略。章节末尾,我们对 DP、MC 和 TD 方法进行了比较。

在下一章中,我们将探讨一个有趣的问题,称为多臂赌博机问题。

问题

让我们通过回答以下问题来评估我们新获得的知识:

  1. TD 学习与 MC 方法有何不同?

  2. 使用 TD 学习方法有什么优势?

  3. 什么是 TD 误差?

  4. TD 学习的更新规则是什么?

  5. TD 预测方法是如何工作的?

  6. 什么是 SARSA?

  7. Q 学习与 SARSA 有何不同?

进一步阅读

欲了解更多信息,请参考以下链接:

第六章:案例研究——MAB 问题

在前面的章节中,我们已经学习了强化学习的基本概念,并且了解了几种有趣的强化学习算法。我们学习了基于模型的方法——动态规划,以及无模型的方法——蒙特卡洛方法,随后我们学习了时间差分方法,它结合了动态规划和蒙特卡洛方法的优点。

在本章中,我们将学习强化学习中的经典问题之一——多臂土匪MAB)问题。我们首先理解 MAB 问题,然后学习几种探索策略,包括 epsilon-greedy、softmax 探索、上置信界和汤普森采样,来解决 MAB 问题。接着,我们将学习 MAB 在实际用例中的应用。

接下来,我们将了解如何通过将其构造为 MAB 问题来找到用户最常点击的最佳广告横幅。在本章结束时,我们将学习上下文土匪及其在不同用例中的应用。

本章我们将学习以下内容:

  • MAB 问题

  • Epsilon-greedy 方法

  • Softmax 探索

  • 上置信界算法

  • 汤普森采样算法

  • MAB 的应用

  • 使用 MAB 找到最佳广告横幅

  • 上下文土匪

MAB 问题

MAB 问题是强化学习中的经典问题之一。MAB 是一种老丨虎丨机,我们拉动臂(杠杆),根据某种概率分布获得支付(奖励)。单个老丨虎丨机叫做单臂土匪(one-armed bandit),当有多个老丨虎丨机时,称为 MAB 或k臂土匪,其中k表示老丨虎丨机的数量。

图 6.1展示了一个三臂土匪:

图 6.1:三臂土匪老丨虎丨机

老丨虎丨机是赌场中最受欢迎的游戏之一,我们拉动臂并获得奖励。如果获得 0 奖励,则输了游戏;如果获得+1 奖励,则赢得游戏。可以有多个老丨虎丨机,每个老丨虎丨机称为一个臂。例如,老丨虎丨机 1 称为臂 1,老丨虎丨机 2 称为臂 2,以此类推。因此,每当我们说臂n时,实际上是指老丨虎丨机n

每个臂都有自己的概率分布,表示赢得游戏和输掉游戏的概率。例如,假设我们有两个臂。如果拉动臂 1(老丨虎丨机 1)时,赢的概率为 0.7,拉动臂 2(老丨虎丨机 2)时,赢的概率为 0.5。

然后,如果我们拉动臂 1,70%的时间我们会赢得游戏并获得+1 奖励;如果拉动臂 2,50%的时间我们会赢得游戏并获得+1 奖励。

因此,我们可以说,拉臂 1 是值得的,因为它让我们在 70% 的情况下赢得了游戏。然而,这种臂的概率分布(老丨虎丨机)不会直接给我们。我们需要找出哪个臂最能帮助我们赢得游戏并提供良好的奖励。

好的,我们如何找到这个臂呢?

假设我们拉臂 1 一次并获得 +1 奖励,拉臂 2 一次并获得 0 奖励。由于臂 1 给出了 +1 奖励,我们不能仅仅通过拉一次臂 1 就得出它是最佳臂的结论。我们需要多次拉这两个臂,并计算每个臂的平均奖励,然后选择给予最大平均奖励的臂作为最佳臂。

设臂为 a,并定义拉臂 a 得到的平均奖励为:

Q(a) 表示臂 a 的平均奖励。

最优臂 a 是给我们带来最大平均奖励的臂,即:

好的,我们已经了解到,给出最大平均奖励的臂是最优臂。那么我们如何找到这个臂呢?

我们进行多轮游戏,每一轮只能拉一个臂。假设在第一轮我们拉臂 1 并观察奖励,在第二轮我们拉臂 2 并观察奖励。类似地,在每一轮中,我们继续拉臂 1 或臂 2 并观察奖励。在完成几轮游戏后,我们计算每个臂的平均奖励,然后选择具有最大平均奖励的臂作为最佳臂。

但这种方法并不是找到最佳臂的好办法。假设我们有 20 个臂;如果我们每一轮都拉一个不同的臂,那么在大多数回合中我们会输掉游戏并获得 0 奖励。除了找到最佳臂之外,我们的目标还应该是最小化识别最佳臂的成本,这通常被称为遗憾。

因此,我们需要在最小化遗憾的同时找到最佳臂。也就是说,我们需要找到最佳臂,但我们不希望最终选择那些让我们在大多数回合中输掉游戏的臂。

那么,我们是应该每一轮探索一个不同的臂,还是只选择在之前回合中获得良好奖励的臂呢?这就引出了探索-利用困境(exploration-exploitation dilemma),我们在第四章《蒙特卡罗方法》中学习过。为了应对这个问题,我们使用 epsilon-greedy 方法,以概率 1-epsilon 选择之前获得良好奖励的臂,并以概率 epsilon 随机选择一个臂。经过若干轮后,我们选择具有最大平均奖励的臂作为最佳臂。

类似于 epsilon-贪婪算法,还有几种不同的探索策略帮助我们克服探索与利用的困境。在接下来的章节中,我们将详细学习几种不同的探索策略,并了解它们如何帮助我们找到最优臂,但首先,让我们来看看如何创建一个 bandit。

在 Gym 中创建一个 bandit

在继续之前,让我们学习如何使用 Gym 工具包创建一个 bandit 环境。Gym 并没有预先包装好的 bandit 环境,因此我们需要创建一个 bandit 环境并将其与 Gym 集成。我们将使用 Jesse Cooper 提供的开源版本 bandit 环境,而不是从零开始创建 bandit 环境。

首先,让我们克隆 Gym bandits 仓库:

git clone https://github.com/JKCooper2/gym-bandits 

接下来,我们可以使用 pip 安装它:

cd gym-bandits
pip install -e . 

安装完成后,我们导入 gym_banditsgym 库:

import gym_bandits
import gym 

gym_bandits 提供了多个版本的 bandit 环境。我们可以在 github.com/JKCooper2/gym-bandits 上查看不同的 bandit 版本。

让我们创建一个简单的 2 臂 bandit,其环境 ID 为 BanditTwoArmedHighLowFixed-v0

env = gym.make("BanditTwoArmedHighLowFixed-v0") 

由于我们创建了一个双臂 bandit,所以我们的行动空间将是 2(因为有两个臂),如下所示:

print(env.action_space.n) 

上面的代码将输出:

2 

我们也可以通过以下方式检查臂的概率分布:

print(env.p_dist) 

上面的代码将输出:

[0.8, 0.2] 

它表示使用臂 1 时,我们有 80% 的概率赢得游戏,而使用臂 2 时,我们有 20% 的概率赢得游戏。我们的目标是找出拉臂 1 还是臂 2 更能让我们在大多数情况下赢得游戏。

现在我们已经学习了如何在 Gym 中创建 bandit 环境,接下来的章节中,我们将探索不同的探索策略来解决 MAB 问题,并将它们与 Gym 一起实现。

探索策略

在本章开始时,我们学习了 MAB 问题中的探索与利用困境。为了克服这个困境,我们使用不同的探索策略来找到最优臂。这里列出了不同的探索策略:

  • Epsilon-贪婪算法

  • Softmax 探索

  • 上置信界限

  • Thomson 采样

现在,我们将详细探讨所有这些探索策略,并实现它们来找到最优臂。

Epsilon-贪婪算法

我们在之前的章节中学习了 epsilon-贪婪算法。使用 epsilon-贪婪时,我们以概率 1-epsilon 选择最优臂,以概率 epsilon 随机选择一个臂。让我们通过一个简单的例子,详细学习如何通过 epsilon-贪婪方法找到最优臂。

假设我们有两个臂——臂 1 和臂 2。假设使用臂 1 时我们有 80% 的概率赢得游戏,而使用臂 2 时我们只有 20% 的概率赢得游戏。所以,我们可以说臂 1 是最好的臂,因为它让我们赢得游戏的概率是 80%。现在,让我们学习如何通过 epsilon-贪婪方法来找出这个最优臂。

首先,我们初始化 count(臂被拉动的次数)、sum_rewards(从拉动臂获得的奖励总和)和 Q(拉动臂获得的平均奖励),正如表 6.1所示:

表 6.1:将变量初始化为零

第一轮

假设,在游戏的第一轮中,我们以 epsilon 的概率选择一个随机的臂,假设我们随机拉动臂 1,并观察奖励。让拉动臂 1 所获得的奖励为 1\。因此,我们更新我们的表格,将臂 1 的 count 设置为 1,臂 1 的 sum_rewards 设置为 1,因此,第一轮后,臂 1 的平均奖励 Q 为 1,正如表 6.2所示:

表 6.2:第一轮后的结果

第二轮

假设,在第二轮中,我们以 1-epsilon 的概率选择最好的臂。最好的臂是那个具有最大平均奖励的臂。所以,我们查看我们的表格,看看哪个臂具有最大的平均奖励。由于臂 1 拥有最大平均奖励,我们拉动臂 1,观察奖励,并让拉动臂 1 所获得的奖励为 1\。

因此,我们更新我们的表格,将臂 1 的 count 设置为 2,臂 1 的 sum_rewards 设置为 2,因此,第二轮后,臂 1 的平均奖励 Q 为 1,正如表 6.3所示:

表 6.3:第二轮后的结果

第三轮

假设,在第三轮中,我们以 epsilon 的概率选择一个随机的臂。假设我们随机拉动臂 2,并观察奖励。让拉动臂 2 所获得的奖励为 0\。因此,我们更新我们的表格,将臂 2 的 count 设置为 1,臂 2 的 sum_rewards 设置为 0,因此,第三轮后,臂 2 的平均奖励 Q 为 0,正如表 6.4所示:

表 6.4:第三轮后的结果

第四轮

假设,在第四轮中,我们以 1-epsilon 的概率选择最好的臂。因此,我们拉动臂 1,因为它具有最大平均奖励。让拉动臂 1 所获得的奖励这次为 0。现在,我们更新我们的表格,将臂 1 的 count 设置为 3,臂 2 的 sum_rewards 设置为 2,因此,第四轮后,臂 1 的平均奖励 Q 将是 0.66,正如表 6.5所示:

表 6.5:第四轮后的结果

我们为多个轮次重复这个过程;也就是说,在多个游戏轮次中,我们以 1-epsilon 的概率拉动最好的臂,并以 epsilon 的概率拉动一个随机的臂。

表 6.6 显示了游戏进行 100 轮后的更新表格:

表 6.6:100 轮后的结果

表 6.6中,我们可以得出结论,臂 1 是最好的臂,因为它具有最大平均奖励。

实现 epsilon-greedy

现在,让我们学习实现 epsilon-greedy 方法来找到最好的臂。首先,让我们导入必要的库:

import gym
import gym_bandits
import numpy as np 

为了更好地理解,我们只创建一个具有两个臂的赌博机:

env = gym.make("BanditTwoArmedHighLowFixed-v0") 

让我们检查一下这个臂的概率分布:

print(env.p_dist) 

上述代码将输出:

[0.8, 0.2] 

我们可以观察到,使用臂 1 时,我们有 80% 的概率赢得游戏,使用臂 2 时,我们有 20% 的概率赢得游戏。这里,最佳臂是臂 1,因为使用臂 1 时,我们有 80% 的概率获胜。现在,让我们看看如何使用 epsilon-greedy 方法找到这个最佳臂。

首先,让我们初始化变量。

初始化 count 以存储每个臂被拉取的次数:

count = np.zeros(2) 

初始化sum_rewards以存储每个臂的奖励总和:

sum_rewards = np.zeros(2) 

初始化Q以存储每个臂的平均奖励:

Q = np.zeros(2) 

设置回合数(迭代次数):

num_rounds = 100 

现在,让我们定义 epsilon_greedy 函数。

首先,我们从均匀分布中生成一个随机数。如果随机数小于 epsilon,那么我们拉取一个随机臂;否则,我们拉取具有最大平均奖励的最佳臂,如下所示:

def epsilon_greedy(epsilon):

    if np.random.uniform(0,1) < epsilon:
        return env.action_space.sample()
    else:
        return np.argmax(Q) 

现在,让我们开始游戏,并尝试使用 epsilon-greedy 方法找到最佳臂。

对于每一回合:

for i in range(num_rounds): 

根据 epsilon-greedy 方法选择臂:

 arm = epsilon_greedy(epsilon=0.5) 

拉取臂并存储奖励和下一个状态信息:

 next_state, reward, done, info = env.step(arm) 

将臂的计数增加1

 count[arm] += 1 

更新臂的奖励总和:

 sum_rewards[arm]+=reward 

更新臂的平均奖励:

 Q[arm] = sum_rewards[arm]/count[arm] 

在所有回合结束后,我们查看每个臂获得的平均奖励:

print(Q) 

上述代码将打印出如下内容:

[0.83783784 0.34615385] 

现在,我们可以选择平均奖励最大化的臂作为最佳臂:

由于臂 1 的平均奖励高于臂 2,我们的最佳臂将是臂 1:

print('The optimal arm is arm {}'.format(np.argmax(Q)+1)) 

上述代码将打印出:

The optimal arm is arm 1 

因此,我们已经使用 epsilon-greedy 方法找到了最佳臂。

Softmax 探索

Softmax 探索,也称为 Boltzmann 探索,是另一种有用的探索策略,用于找到最佳臂。

在 epsilon-greedy 策略中,我们学习到,以 1-epsilon 的概率选择最佳臂,以 epsilon 的概率选择随机臂。正如你可能注意到的,在 epsilon-greedy 策略中,所有非最佳臂被平等地探索。也就是说,所有非最佳臂被选择的概率是均匀的。例如,假设我们有 4 个臂,其中臂 1 是最佳臂。那么我们就会均匀地探索非最佳臂——[臂 2,臂 3,臂 4]。

假设臂 3 永远不是一个好臂,它总是给出 0 的奖励。在这种情况下,我们可以花更多时间探索臂 2 和臂 4,而不是再探索臂 3。但 epsilon-greedy 方法的问题在于,它会平等地探索所有非最佳臂。因此,所有非最佳臂——[臂 2,臂 3,臂 4]——将被平等地探索。

为了避免这种情况,如果我们可以优先选择臂 2 和臂 4,而不是臂 3,那么我们就可以更频繁地探索臂 2 和臂 4,而不是臂 3。

好的,但我们如何能优先选择某些臂呢?我们可以通过基于平均奖励 Q 为所有臂分配一个概率来实现臂的优先选择。拥有最大平均奖励的臂将有更高的概率,而所有非最佳臂的选择概率将与它们的平均奖励成正比。

例如,正如表 6.7所示,臂 1 是最佳臂,因为它有较高的平均奖励Q。因此,我们给臂 1 分配较高的概率。臂 2、3 和 4 是非最佳臂,我们需要探索它们。正如我们所观察到的,臂 3 的平均奖励为 0。因此,我们不会均匀地选择所有的非最佳臂,而是将更多的优先级给予臂 2 和臂 4,而非臂 3。所以,臂 2 和 4 的概率将高于臂 3:

表 6.7:四臂老丨虎丨机的平均奖励

因此,在 softmax 探索中,我们是基于概率来选择臂的。每个臂的概率与其平均奖励成正比:

等等,概率应该加起来等于 1,对吧?平均奖励(Q 值)加起来并不等于 1。因此,我们使用 softmax 函数将它们转换为概率,如下所示:

所以,现在根据概率来选择臂。然而,在最初的几轮中,我们不知道每个臂的正确平均奖励,因此在初期基于平均奖励的概率选择臂会不准确。为了避免这种情况,我们引入了一个新的参数,叫做TT被称为温度参数。

我们可以用温度T重新写出之前的方程,如下所示:

好的,那么这个T怎么帮助我们呢?当T较高时,所有的臂被选择的概率相等;而当T较低时,具有最大平均奖励的臂将具有较高的选择概率。因此,我们在最初的几轮将T设置为较大的数值,在经过一系列轮次后,我们逐渐降低T的值。这意味着在最初的轮次中,我们平等地探索所有的臂,而在经过一系列轮次后,我们选择概率较高的最佳臂。

让我们通过一个简单的例子来理解这一点。假设我们有四个臂,从臂 1 到臂 4。假设我们拉动了臂 1 并获得了奖励 1,那么臂 1 的平均奖励将是 1,而所有其他臂的平均奖励将是 0,正如表 6.8所示:

表 6.8:每个臂的平均奖励

现在,如果我们使用方程(1)中给出的 softmax 函数将平均奖励转换为概率,那么我们的概率如下所示:

表 6.9:每个臂的概率

正如我们所观察到的,臂 1 的概率为 47%,而其他所有臂的概率为 17%。但是,我们不能仅仅通过拉动一次臂 1 就为臂 1 分配高概率。因此,我们将T设置为一个较大的数字,比如T = 30,并根据方程(2)计算概率。现在我们的概率变成了:

表 6.10:T=30 时每个臂的概率

如我们所见,现在所有臂被选中的概率相等。现在我们根据这个概率探索各个臂,经过若干回合后,T值将被降低,我们会将较高的概率分配给最优臂。假设经过大约 30 回合后,所有臂的平均奖励为:

表 6.11:经过 30 多轮后的每个臂的平均奖励

我们了解到,T的值在多轮中会逐渐减少。假设T的值减少,现在为 0.3(T=0.3);此时概率将变为:

表 6.12:当前 T 值设置为 0.3 时每个臂的概率

如我们所见,臂 1 相较于其他臂有较高的概率。因此,我们选择臂 1 作为最佳臂,并在接下来的回合中根据它们的概率探索非最佳臂——[臂 2, 臂 3, 臂 4]。

因此,在初始回合中,我们并不知道哪个臂是最佳臂。因此,我们不是根据平均奖励为臂分配较高的概率,而是为所有臂分配相等的概率,并且在初始回合中设置较高的T值,随着若干回合的进行,我们逐步减少T的值,并为具有较高平均奖励的臂分配较高的概率。

实现软最大探索

现在,让我们学习如何实现软最大探索方法来找到最佳臂。首先,我们导入必要的库:

import gym
import gym_bandits
import numpy as np 

让我们以在ε-贪婪部分看到的同一个二臂老丨虎丨机为例:

env = gym.make("BanditTwoArmedHighLowFixed-v0") 

现在,让我们初始化变量。

初始化count用于存储每个臂被拉动的次数:

count = np.zeros(2) 

初始化sum_rewards用于存储每个臂的奖励总和:

sum_rewards = np.zeros(2) 

初始化Q用于存储每个臂的平均奖励:

Q = np.zeros(2) 

设置回合数(迭代次数):

num_rounds = 100 

现在,我们定义具有温度T的软最大函数:

def softmax(T): 

根据前面的公式计算每个臂的概率:

 denom = sum([np.exp(i/T) for i in Q])
    probs = [np.exp(i/T)/denom for i in Q] 

根据计算的臂的概率分布选择臂:

 arm = np.random.choice(env.action_space.n, p=probs)

    return arm 

现在,让我们开始游戏,并尝试使用软最大探索方法找到最佳臂。

让我们从将温度T设置为一个较大的数值开始,比如50

T = 50 

对于每一回合:

for i in range(num_rounds): 

根据软最大探索方法选择臂:

 arm = softmax(T) 

拉动臂并存储奖励和下一状态信息:

 next_state, reward, done, info = env.step(arm) 

将该臂的计数增加 1:

 count[arm] += 1 

更新该臂的奖励总和:

 sum_rewards[arm]+=reward 

更新该臂的平均奖励:

 Q[arm] = sum_rewards[arm]/count[arm] 

降低温度T

 T = T*0.99 

所有回合结束后,我们检查 Q 值,也就是每个臂的平均奖励:

print(Q) 

上面的代码会输出类似于以下内容:

[0.77700348 0.1971831 ] 

如我们所见,臂 1 的平均奖励高于臂 2,因此我们选择臂 1 作为最优臂:

print('The optimal arm is arm {}'.format(np.argmax(Q)+1)) 

上面的代码会输出:

The optimal arm is arm 1 

因此,我们已经通过软最大探索方法找到了最优臂。

上置信区间

在这一部分,我们将探讨另一个有趣的算法,叫做上置信界限UCB),它用于解决探索与开发的困境。UCB 算法基于一个叫做“面对不确定性时的乐观主义”的原则。让我们通过一个简单的例子,了解 UCB 算法是如何工作的。

假设我们有两个臂——臂 1 和臂 2。假设我们通过随机拉臂 1 和臂 2 玩了 20 轮游戏,发现臂 1 的平均奖励是 0.6,臂 2 的平均奖励是 0.5。但是我们怎么确定这个平均奖励是准确的呢?也就是说,我们怎么确定这个平均奖励代表了真实的平均值(总体均值)呢?这时,我们就需要使用置信区间。

置信区间表示真实值所在的区间。因此,在我们的设置中,置信区间表示臂的真实平均奖励所在的区间。

例如,从图 6.2中,我们可以看到臂 1 的置信区间是 0.2 到 0.9,这表明臂 1 的平均奖励位于 0.2 到 0.9 之间。0.2 是下置信界限,0.9 是上置信界限。类似地,我们可以观察到臂 2 的置信区间是 0.5 到 0.7,这表明臂 2 的平均奖励位于 0.5 到 0.7 之间,其中 0.5 是下置信界限,0.7 是上置信界限。

图 6.2:臂 1 和臂 2 的置信区间

好的,从图 6.2中我们可以看到臂 1 和臂 2 的置信区间。那么,我们该如何做出决策呢?也就是说,我们如何决定是拉臂 1 还是拉臂 2?如果我们仔细观察,可以看到臂 1 的置信区间较大,而臂 2 的置信区间较小。

当置信区间较大时,我们对平均值的确定性较低。由于臂 1 的置信区间较大(0.2 到 0.9),我们无法确定拉动臂 1 时能获得什么奖励,因为平均奖励可能从 0.2 低至 0.9 高。所以,臂 1 存在很大的不确定性,我们不能确定臂 1 会给出高奖励还是低奖励。

当置信区间较小时,我们对平均值较为确定。由于臂 2 的置信区间较小(0.5 到 0.7),我们可以确定拉臂 2 时会得到一个不错的奖励,因为我们的平均奖励在 0.5 到 0.7 的范围内。

但是,臂 2 的置信区间为什么小,臂 1 的置信区间为什么大呢?在本节的开头,我们了解到我们通过随机拉动臂 1 和臂 2 玩了 20 回合游戏,并计算了臂 1 和臂 2 的平均奖励。假设臂 2 已经被拉动了 15 次,而臂 1 只被拉动了 5 次。由于臂 2 被拉动了很多次,臂 2 的置信区间很小,表示有确定的平均奖励。由于臂 1 被拉动的次数较少,臂的置信区间较大,表示有不确定的平均奖励。因此,这表明臂 2 比臂 1 探索得更多。

好的,回到我们的问题,我们应该拉动臂 1 还是臂 2?在 UCB 中,我们总是选择具有高上置信边界的臂,所以在我们的例子中,我们选择臂 1,因为它的上置信边界高达 0.9。但为什么我们必须选择具有最高上置信边界的臂呢?选择具有最高上界的臂帮助我们选择给出最大奖励的臂。

但这里有一个小问题。当置信区间较大时,我们无法确定平均奖励。例如,在我们的例子中,我们选择了臂 1,因为它有一个很高的上置信边界为 0.9;然而,由于臂 1 的置信区间较大,我们的平均奖励可能在 0.2 到 0.9 之间,因此我们甚至可能获得较低的奖励。但这没关系,我们仍然选择臂 1,因为它促进了探索。当臂被充分探索时,置信区间变得较小。

当我们通过选择具有高 UCB 的臂来进行几轮游戏时,我们两臂的置信区间将变得更窄,并表示更准确的平均值。例如,正如我们在图 6.3中看到的那样,在进行几轮游戏后,两臂的置信区间变小,并表示一个更准确的平均值:

图 6.3:几轮后臂 1 和臂 2 的置信区间

图 6.3中,我们可以看到两臂的置信区间较小,并且我们有一个更准确的平均值,由于在 UCB 中我们选择具有最高 UCB 的臂,因此我们选择臂 2 作为最佳臂。

因此,在 UCB 中,我们总是选择具有最高上置信边界的臂。在初始回合中,我们可能不会选择最佳臂,因为臂的置信区间在初始回合中较大。但在一系列回合中,置信区间变得较小,我们选择了最佳臂。

N(a)表示臂a被拉动的次数,t表示总回合数,则臂a的上置信边界可以计算为:

我们选择具有最高上置信边界的臂作为最佳臂:

UCB 算法如下所示:

  1. 选择上置信边界高的臂

  2. 拉动臂并获得奖励

  3. 更新臂的平均奖励和置信区间

  4. 步骤 1步骤 3 进行若干轮重复

实现 UCB

现在,让我们学习如何实现 UCB 算法来找到最佳臂。

首先,让我们导入必要的库:

import gym
import gym_bandits
import numpy as np 

让我们创建与上一节看到的相同的双臂赌博机:

env = gym.make("BanditTwoArmedHighLowFixed-v0") 

现在,让我们初始化这些变量。

初始化 count 用于存储某个臂被拉动的次数:

count = np.zeros(2) 

初始化 sum_rewards 用于存储每个臂的奖励总和:

sum_rewards = np.zeros(2) 

初始化 Q 用于存储每个臂的平均奖励:

Q = np.zeros(2) 

设置轮次数(迭代次数):

num_rounds = 100 

现在,我们定义 UCB 函数,它返回具有最高 UCB 的最佳臂:

def UCB(i): 

初始化 numpy 数组,用于存储所有臂的 UCB:

 ucb = np.zeros(2) 

在计算 UCB 之前,我们至少探索每个臂一次,因此在前两轮中,我们直接选择与轮次编号对应的臂:

 if i < 2:
        return i 

如果轮次大于 2,则我们按照公式(3)计算所有臂的 UCB,并返回具有最高 UCB 的臂:

 else:
        for arm in range(2):
            ucb[arm] = Q[arm] + np.sqrt((2*np.log(sum(count))) / count[arm])
        return (np.argmax(ucb)) 

现在,让我们玩这个游戏,试着通过 UCB 方法找到最佳臂。

每一轮:

for i in range(num_rounds): 

基于 UCB 方法选择臂:

 arm = UCB(i) 

拉动臂并存储奖励和下一个状态信息:

 next_state, reward, done, info = env.step(arm) 

将该臂的计数增加 1

 count[arm] += 1 

更新臂的奖励总和:

 sum_rewards[arm]+=reward 

更新臂的平均奖励:

 Q[arm] = sum_rewards[arm]/count[arm] 

在所有回合结束后,我们可以选择具有最大平均奖励的最佳臂:

print('The optimal arm is arm {}'.format(np.argmax(Q)+1)) 

上述代码将打印:

The optimal arm is arm 1 

因此,我们使用 UCB 方法找到了最佳臂。

汤普森采样

汤普森采样TS)是另一种有趣的探索策略,用于克服探索与利用之间的困境,并且它基于贝塔分布。因此,在深入研究汤普森采样之前,我们首先了解一下贝塔分布。贝塔分布是一种概率分布函数,表示为:

其中 是伽马函数。

分布的形状由两个参数控制!。当 的值相同时,我们将得到一个对称分布。

例如,正如 图 6.4 所示,由于 的值相等,我们得到了一个对称分布:

图 6.4:对称贝塔分布

的值大于 时,我们会得到一个接近 1 而非 0 的概率。例如,正如 图 6.5 所示,由于 的值相同,我们有一个接近 1 而非 0 的高概率:

图 6.5:贝塔分布,其中

的值大于 时,我们将有一个接近 0 而不是 1 的高概率。例如,如下图所示,由于 的值,我们有一个接近 0 而不是 1 的高概率:

图 6.6:伽玛分布,其中

现在我们对贝塔分布有了基本的了解,让我们探讨一下汤普森采样是如何工作的,以及它是如何利用贝塔分布的。理解每个臂部的真实分布非常重要,因为一旦我们知道臂部的真实分布,我们就能轻松了解该臂是否会给我们带来良好的奖励;也就是说,我们可以了解拉动该臂是否能帮助我们赢得比赛。例如,假设我们有两个臂——臂 1 和臂 2。图 6.7 显示了这两个臂的真实分布:

图 6.7:臂 1 和臂 2 的真实分布

图 6.7中我们可以看到,拉臂 1 比拉臂 2 更好,因为臂 1 的高概率接近 1,而臂 2 的高概率接近 0。因此,如果我们拉臂 1,我们将获得奖励 1 并赢得比赛,但如果我们拉臂 2,我们将获得奖励 0 并输掉比赛。因此,一旦我们知道臂部的真实分布,我们就能理解哪个臂部是最好的臂。

但是我们怎么才能了解臂 1 和臂 2 的真实分布呢?这就是我们使用汤普森采样方法的地方。汤普森采样是一种基于概率的方法,它是基于先验分布的。

首先,我们从臂 1 和臂 2 中各取 n 个样本并计算它们的分布。然而,在初始的几次迭代中,臂 1 和臂 2 的计算分布将不会与真实分布相同,因此我们称之为先验分布。正如图 6.8所示,我们有臂 1 和臂 2 的先验分布,并且它与真实分布有所不同:

图 6.8:臂 1 和臂 2 的先验分布

但是通过一系列迭代,我们会学到臂 1 和臂 2 的真实分布,正如图 6.9所示,经过一系列迭代后,臂的先验分布看起来与真实分布相同:

图 6.9:先验分布趋向真实分布

一旦我们学到所有臂的真实分布,那么我们就可以轻松选择最优臂部。好吧,但我们究竟是如何学习到真实分布的呢?让我们更详细地探索一下。

在这里,我们使用贝塔分布作为先验分布。假设我们有两个臂部,那么我们将有两个贝塔分布(先验分布),并且我们将初始化两者! 为相同的值,比如 3,正如图 6.10所示:

图 6.10:初始化的臂 1 和臂 2 的先验分布看起来相同

正如我们所看到的,由于我们将 alpha 和 beta 初始化为相同的值,臂 1 和臂 2 的 beta 分布看起来是一样的。

在第一轮中,我们只是从这两个分布中随机抽取一个值,并选择具有最大抽样值的臂。假设臂 1 的抽样值较高,所以在这种情况下,我们拉动臂 1。假设我们通过拉动臂 1 赢得了游戏,那么我们通过将臂 1 分布的 alpha 值增加 1 来更新其分布;也就是说,我们更新 alpha 值为 。正如图 6.11所示,臂 1 分布的 alpha 值已增加,并且我们可以看到,臂 1 的 beta 分布与臂 2 相比,在接近 1 的地方具有略高的概率:

图 6.11:第一轮后臂 1 和臂 2 的先验分布

在下一轮中,我们再次从这两个分布中随机抽取一个值,并选择具有最大抽样值的臂。假设在这一轮中,我们仍然从臂 1 获得了最大抽样值。那么我们再次拉动臂 1。假设我们通过拉动臂 1 赢得了游戏,那么我们通过将臂 1 的 alpha 值更新为 来更新臂 1 的分布。正如图 6.12所示,臂 1 分布的 alpha 值已增加,且臂 1 的 beta 分布在接近 1 的地方具有略高的概率:

图 6.12:第二轮后臂 1 和臂 2 的先验分布

类似地,在下一轮中,我们再次从这些分布中随机抽取一个值,并拉动具有最大值的臂。假设这次我们从臂 2 获得了最大值,因此我们拉动臂 2 并进行游戏。假设我们通过拉动臂 2 输掉了游戏。那么我们通过将臂 2 的 beta 值更新为 来更新臂 2 的分布。正如图 6.13所示,臂 2 分布的 beta 值已增加,并且臂 2 的 beta 分布在接近 0 的地方具有略高的概率:

图 6.13:第三轮后臂 1 和臂 2 的先验分布

同样,在下一轮中,我们从臂 1 和臂 2 的 beta 分布中随机抽取一个值。假设臂 2 的抽样值较高,因此我们拉动臂 2。假设我们再次通过拉动臂 2 输掉了游戏。那么我们通过将臂 2 的 beta 值更新为 来更新臂 2 的分布。正如图 6.14所示,臂 2 分布的 beta 值增加了 1,并且臂 2 的 beta 分布在接近 0 的地方具有略高的概率:

图 6.14:第四轮后臂 1 和臂 2 的先验分布

好的,你注意到我们在做什么吗?如果通过拉取某个臂我们赢得了游戏,我们实际上是在增加该臂分布的 alpha 值,否则我们增加 beta 值。如果我们在多轮中重复这样做,就能学到该臂的真实分布。假设经过若干轮后,我们的分布将像图 6.15一样。正如我们所见,两个臂的分布都接近真实分布:

图 6.15:经过若干轮后,臂 1 和臂 2 的先验分布

现在,如果我们从这些分布中抽取一个值,那么从臂 1 中抽取的值总是高的,我们总是拉臂 1 并赢得游戏。

汤普森抽样方法的步骤如下:

  1. 用相等的 alpha 和 beta 值初始化所有k臂的 beta 分布:

  2. 从所有k臂的 beta 分布中抽取一个值:

  3. 拉取抽取值高的臂

  4. 如果我们赢得游戏,则将分布的 alpha 值更新为

  5. 如果我们输掉游戏,则将分布的 beta 值更新为

  6. 重复步骤 2步骤 5多轮

实现汤普森抽样

现在,让我们学习如何实现汤普森抽样方法来找到最佳臂。

首先,让我们导入必要的库:

import gym
import gym_bandits
import numpy as np 

为了更好理解,让我们创建前一节中看到的相同的双臂老丨虎丨机:

env = gym.make("BanditTwoArmedHighLowFixed-v0") 

现在,让我们初始化变量。

初始化count用于存储每个臂被拉取的次数:

count = np.zeros(2) 

初始化sum_rewards用于存储每个臂的奖励总和:

sum_rewards = np.zeros(2) 

初始化Q用于存储每个臂的平均奖励:

Q = np.zeros(2) 

为两个臂初始化 alpha 值为1

alpha = np.ones(2) 

为两个臂初始化 beta 值为1

beta = np.ones(2) 

设置轮次数量(迭代次数):

num_rounds = 100 

现在,让我们定义thompson_sampling函数。

如下代码所示,我们从两个臂的 beta 分布中随机抽取值,并返回具有最大抽取值的臂:

def thompson_sampling(alpha,beta):

    samples = [np.random.beta(alpha[i]+1,beta[i]+1) for i in range(2)]
    return np.argmax(samples) 

现在,让我们开始游戏,并尝试使用汤普森抽样方法找到最佳臂。

对于每一轮:

for i in range(num_rounds): 

根据汤普森抽样方法选择臂:

 arm = thompson_sampling(alpha,beta) 

拉取臂并存储奖励和下一状态信息:

 next_state, reward, done, info = env.step(arm) 

将臂的计数增加 1:

 count[arm] += 1 

更新臂的奖励总和:

 sum_rewards[arm]+=reward 

更新臂的平均奖励:

 Q[arm] = sum_rewards[arm]/count[arm] 

如果我们赢得游戏,即奖励为 1,则将 alpha 值更新为 ,否则将 beta 值更新为

 if reward==1:
        alpha[arm] = alpha[arm] + 1
    else:
        beta[arm] = beta[arm] + 1 

所有轮次结束后,我们可以选择拥有最高平均奖励的臂作为最优臂:

print('The optimal arm is arm {}'.format(np.argmax(Q)+1)) 

上述代码将输出:

The optimal arm is arm 1 

因此,我们使用汤普森抽样方法找到了最优臂。

MAB 的应用

到目前为止,我们已经了解了 MAB 问题以及如何使用各种探索策略来解决它。但我们的目标不仅仅是使用这些算法来玩老丨虎丨机。我们可以将这些不同的探索策略应用到几个不同的使用场景中。

例如,强盗可以作为 AB 测试的替代方案。AB 测试是最常用的经典测试方法之一。假设我们有我们网站的两个版本的登陆页面。假设我们想知道哪个版本的登陆页面最受用户喜欢。在这种情况下,我们进行 AB 测试,以了解哪个版本的登陆页面最受用户喜欢。于是,我们将版本 1 的登陆页面展示给一组特定的用户,将版本 2 的登陆页面展示给另一组用户。然后我们衡量几个指标,如点击率、网站平均停留时间等,以了解哪个版本的登陆页面最受用户喜欢。一旦我们了解了哪个版本的登陆页面最受用户喜爱,我们将开始将该版本展示给所有用户。

因此,在 AB 测试中,我们为探索和利用安排了独立的时间段。也就是说,AB 测试有两个不同的专用时间段来进行探索和利用。但 AB 测试的问题在于,它会带来较高的后悔值。我们可以使用解决 MAB 问题时使用的各种探索策略来最小化后悔值。因此,我们可以通过在探索和利用中同时进行自适应操作,而不是完全分开地进行探索和利用,从而优化 AB 测试的效果。

强盗广泛应用于网站优化、最大化转化率、在线广告、活动策划等领域。

使用强盗找到最佳广告横幅

在本节中,让我们看看如何使用强盗找到最好的广告横幅。假设我们正在运行一个网站,并且我们有五个不同的广告横幅,用于展示同一个广告,并且假设我们想弄清楚哪个广告横幅最受用户喜欢。

我们可以将这个问题框架化为一个 MAB 问题。五个广告横幅代表了强盗的五个臂,如果用户点击广告,则奖励为+1;如果用户没有点击广告,则奖励为 0。因此,为了找出哪个广告横幅最受用户点击,即哪个广告横幅可以为我们带来最大奖励,我们可以使用各种探索策略。在本节中,我们只使用 epsilon-greedy 方法来找出最好的广告横幅。

首先,让我们导入必要的库:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
plt.style.use('ggplot') 

创建数据集

现在,让我们创建一个数据集。我们生成一个包含五列的列表示五个广告横幅的数据集,并生成 100,000 行,其中每行的值为 0 或 1,表示用户是否点击了广告横幅(1)或未点击(0):

df = pd.DataFrame()
for i in range(5):
    df['Banner_type_'+str(i)] = np.random.randint(0,2,100000) 

让我们来看一下数据集的前几行:

df.head() 

上述代码将打印以下内容。正如我们所见,我们有五个广告横幅(0 到 4),每行由 0 或 1 的值组成,表示广告横幅是否被点击(1)或未被点击(0)。

图 6.14:每个横幅的点击次数

初始化变量

现在,让我们初始化一些重要的变量。

设置迭代次数:

num_iterations = 100000 

定义横幅的数量:

num_banner = 5 

初始化 count 以存储横幅被点击的次数:

count = np.zeros(num_banner) 

初始化 sum_rewards 以存储每个横幅获得的奖励总和:

sum_rewards = np.zeros(num_banner) 

初始化 Q 以存储每个横幅的平均奖励:

Q = np.zeros(num_banner) 

定义一个列表来存储已选择的横幅:

banner_selected = [] 

定义 epsilon-greedy 方法

现在,让我们定义 epsilon-greedy 方法。我们从均匀分布中生成一个随机值。如果随机值小于 epsilon,则选择一个随机横幅;否则,选择具有最大平均奖励的最佳横幅:

def epsilon_greedy_policy(epsilon):

    if np.random.uniform(0,1) < epsilon:
        return np.random.choice(num_banner)
    else:
        return np.argmax(Q) 

运行赌博机测试

现在,我们运行 epsilon-greedy 策略来找出哪一个广告横幅是最好的。

对于每次迭代:

for i in range(num_iterations): 

使用 epsilon-greedy 策略选择横幅:

 banner = epsilon_greedy_policy(0.5) 

获取横幅的奖励:

 reward = df.values[i, banner] 

增加计数器:

 count[banner] += 1 

存储奖励总和:

 sum_rewards[banner]+=reward 

计算平均奖励:

 Q[banner] = sum_rewards[banner]/count[banner] 

将横幅存储到已选择的横幅列表中:

 banner_selected.append(banner) 

在所有轮次结束后,我们可以选择平均奖励最大的一面作为最佳横幅:

print( 'The best banner is banner {}'.format(np.argmax(Q))) 

上述代码将输出:

The best banner is banner 2 

我们还可以绘制并查看哪一面横幅被选择得最多:

ax = sns.countplot(banner_selected)
ax.set(xlabel='Banner', ylabel='Count')
plt.show() 

上述代码将绘制以下内容。正如我们所见,横幅 2 被选择的次数最多:

图 6.15:横幅 2 是最佳广告横幅

通过这种方式,我们学会了如何通过将问题框架化为 MAB 问题来找到最佳广告横幅。

上下文赌博机

我们刚刚学会了如何使用赌博机来为用户找到最好的广告横幅。但每个用户的横幅偏好是不同的。也就是说,用户 A 喜欢横幅 1,但用户 B 可能更喜欢横幅 3,依此类推。每个用户都有自己的偏好。因此,我们需要根据每个用户的需求个性化广告横幅。我们该如何做到这一点呢?这时,我们就需要使用上下文赌博机。

在 MAB 问题中,我们只执行动作并获得奖励。但在上下文赌博机中,我们根据环境的状态采取行动,状态包含了上下文信息。

例如,在广告横幅示例中,状态指定了用户行为,我们将根据状态(用户行为)采取行动(显示横幅),以便获得最大奖励(广告点击)。

上下文赌博机广泛应用于根据用户行为个性化内容。它们还用于解决推荐系统面临的冷启动问题。Netflix 使用上下文赌博机根据用户行为个性化电视节目的艺术作品。

总结

我们从理解 MAB 问题是什么以及如何通过几种探索策略来解决它开始。本章首先介绍了 epsilon-greedy 方法,在该方法中,我们以概率 epsilon 随机选择一个臂,并以概率 1-epsilon 选择最好的臂。接下来,我们学习了 softmax 探索方法,在该方法中,我们根据概率分布选择臂,每个臂的概率与其平均奖励成正比。

继而,我们学习了 UCB 算法,在该算法中,我们选择具有最高上置信界限的臂。然后,我们探索了 Thomspon 采样方法,其中我们基于贝塔分布学习臂的分布。

进一步学习后,我们了解了 MAB 如何作为 AB 测试的替代方法,以及如何通过将问题框定为 MAB 问题来找到最佳的广告横幅。在本章的结尾,我们还对上下文赌博机有了概述。

在下一章中,我们将学习几种对深度强化学习至关重要的有趣深度学习算法。

问题

通过回答以下问题,来评估我们在本章中获得的知识:

  1. 什么是 MAB 问题?

  2. epsilon-greedy 策略如何选择一个臂?

  3. 在 softmax 探索中,T 的意义是什么?

  4. 我们如何计算上置信界限?

  5. 当贝塔分布中的 alpha 值大于 beta 值时会发生什么?

  6. Thompson 采样中涉及的步骤是什么?

  7. 什么是上下文赌博机?

进一步阅读

更多信息,请查阅以下有趣的资源:

第七章:深度学习基础

在前几章中,我们已经学习了几个强化学习算法是如何工作的,以及它们如何找到最优策略。在接下来的章节中,我们将学习深度强化学习DRL),它是深度学习和强化学习的结合。为了理解 DRL,我们需要具备深度学习的坚实基础。因此,在本章中,我们将学习几种重要的深度学习算法。

深度学习是机器学习的一个子集,核心是神经网络。深度学习已经存在了十多年,但现在之所以如此流行,是因为计算能力的进步和海量数据的可获取性。借助这些大量数据,深度学习算法能够超越经典的机器学习算法。

我们将从理解生物神经元和人工神经元开始,然后学习人工神经网络ANN)及其实现方法。接下来,我们将学习一些有趣的深度学习算法,如循环神经网络RNN)、长短期记忆网络LSTM)、卷积神经网络CNN)和生成对抗网络GAN)。

本章我们将学习以下内容:

  • 生物神经元与人工神经元

  • ANNs

  • RNNs

  • LSTM RNNs

  • CNNs

  • GANs

让我们从理解生物神经元和人工神经元的工作原理开始本章内容。

生物神经元与人工神经元

在继续之前,我们首先要探索神经元是什么,以及我们大脑中的神经元是如何工作的,然后再学习人工神经元。

神经元可以被定义为人脑的基本计算单元。神经元是我们大脑和神经系统的基本单位。我们的大脑大约包含 1000 亿个神经元。每个神经元通过一种叫做突触的结构与其他神经元相连接,突触负责接收来自外部环境的输入信息(通过感觉器官),向我们的肌肉发送运动指令,并执行其他活动。

神经元还可以通过一种叫做树突的分支结构从其他神经元接收输入。这些输入会被加强或削弱,也就是说,它们根据重要性进行加权,然后在细胞体(胞体)中相加。从细胞体中,这些加权后的输入会被处理并通过轴突传递,最终送到其他神经元。

图 7.1 显示了一个基本的单一生物神经元:

图 7.1:生物神经元

现在,让我们看看人工神经元是如何工作的。假设我们有三个输入 x[1]、x[2] 和 x[3],用来预测输出 y。这些输入分别乘以权重 w[1]、w[2] 和 w[3],并按以下方式相加:

但是,为什么要将这些输入与权重相乘呢?因为在计算输出 y 时,并不是所有输入都同等重要。假设 x[2] 在计算输出时比其他两个输入更重要。那么,我们会为 w[2] 分配一个比其他两个权重更高的值。这样,在将权重与输入相乘后,x[2] 的值将大于其他两个输入。简单来说,权重是用来增强输入的。在将输入与权重相乘后,我们将它们加在一起,再加上一个称为偏置 b 的值:

如果你仔细观察前面的公式,它可能看起来很熟悉。z 看起来像是线性回归的公式,不是吗?它不就是一条直线的方程吗?我们知道,直线方程是这样的:

在这里,m 是权重(系数),x 是输入,b 是偏置(截距)。

是的。那么,神经元和线性回归有什么区别呢?在神经元中,我们通过应用一个叫做激活传输函数的函数 f(.),为结果 z 引入了非线性。因此,我们的输出变成了:

图 7.2 显示了一个单一的人工神经元:

图 7.2:人工神经元

所以,一个神经元接收输入 x,将其与权重 w 相乘,再加上偏置 b,形成 z,然后我们对 z 应用激活函数,得到输出 y

人工神经网络及其层次结构

虽然神经元非常酷,但我们不能仅依靠一个神经元来执行复杂任务。这就是我们大脑拥有数十亿个神经元,按层堆叠形成网络的原因。同样,人工神经元也被按层排列。每一层都将以某种方式连接,以便信息从一层传递到另一层。

一个典型的人工神经网络包括以下层:

  • 输入层

  • 隐藏层

  • 输出层

每一层都有一组神经元,且一层的神经元与其他层的所有神经元都有交互。然而,同一层的神经元之间不会相互作用。这是因为相邻层的神经元之间有连接或边缘,但同一层的神经元之间没有任何连接。我们用节点单元来表示人工神经网络中的神经元。

图 7.3 显示了一个典型的人工神经网络(ANN):

图 7.3:人工神经网络(ANN)

输入层

输入层 是我们将输入数据提供给网络的地方。输入层中神经元的数量就是我们提供给网络的输入数量。每个输入都会对预测输出产生一定影响。然而,输入层并不执行任何计算;它仅用于将外界的信息传递给网络。

隐藏层

输入层和输出层之间的任何层都称为隐藏层。它处理从输入层接收到的输入。隐藏层负责推导输入和输出之间的复杂关系。也就是说,隐藏层识别数据集中的模式。它主要负责学习数据表示和提取特征。

隐藏层的数量可以是任意的;然而,我们必须根据使用场景选择隐藏层的数量。对于非常简单的问题,我们只需要使用一个隐藏层,但在执行复杂任务(如图像识别)时,我们使用许多隐藏层,每一层负责提取重要特征。当我们有许多隐藏层时,网络被称为深度神经网络

输出层

在处理完输入后,隐藏层将其结果发送到输出层。顾名思义,输出层发出最终输出。输出层的神经元数量取决于我们希望网络解决的问题类型。

如果是二分类问题,则输出层中的神经元数量为 1,表示输入属于哪个类别。如果是多分类问题,比如有五个类别,并且我们希望得到每个类别的概率作为输出,则输出层中的神经元数量为五,每个神经元输出一个概率。如果是回归问题,则输出层只有一个神经元。

探索激活函数

激活函数,也称为传输函数,在神经网络中起着至关重要的作用。它用于引入神经网络中的非线性。正如我们之前所学,我们将激活函数应用于输入,输入会被权重乘以并加上偏置,即f(z),其中 z = (输入 * 权重) + 偏置f(.) 是激活函数。

如果不应用激活函数,那么神经元就仅仅类似于线性回归。激活函数的目的是引入非线性变换,以学习数据中复杂的潜在模式。

现在,让我们看一些常用的激活函数。

Sigmoid 函数

Sigmoid 函数是最常用的激活函数之一。它将值缩放到 0 和 1 之间。Sigmoid 函数可以定义如下:

它是一个 S 形曲线,如图 7.4所示:

图 7.4:Sigmoid 函数

它是可微的,这意味着我们可以在任意两个点之间找到曲线的斜率。它是单调的,这意味着它要么完全是非递增的,要么是非递减的。Sigmoid 函数也被称为逻辑斯蒂函数。我们知道概率值介于 0 和 1 之间,而由于 Sigmoid 函数将值压缩到 0 到 1 之间,它被用于预测输出的概率。

tanh 函数

双曲正切tanh)函数输出的值介于-1 到+1 之间,表示如下:

它也类似于 S 形曲线。与 sigmoid 函数中心在 0.5 不同,tanh 函数是以 0 为中心,如下图所示:

图 7.5:tanh 函数

修正线性单元函数

修正线性单元ReLU)函数是另一个最常用的激活函数之一。它输出一个从零到无穷大的值。它本质上是一个分段函数,可以表示如下:

也就是说,当x的值小于零时,f(x)返回零;当x的值大于或等于零时,f(x)返回x。它也可以表示如下:

图 7.6显示了 ReLU 函数:

图 7.6:ReLU 函数

如我们在前面的图示中看到的,当我们将任何负输入传递给 ReLU 函数时,它会将负输入转换为零。

softmax 函数

softmax 函数本质上是 sigmoid 函数的推广。它通常应用于网络的最后一层,并且在执行多类分类任务时使用。它给出每个类别的输出概率,因此,softmax 值的总和总是等于 1。

它可以表示如下:

图 7.7所示,softmax 函数将其输入转换为概率:

图 7.7:Softmax 函数

现在我们已经了解了不同的激活函数,在接下来的部分中,我们将学习人工神经网络(ANNs)中的前向传播。

人工神经网络中的前向传播

在本节中,我们将看到人工神经网络如何学习,其中神经元堆叠在不同的层中。网络中的层数等于隐藏层的数量加上输出层的数量。在计算网络的层数时,我们不考虑输入层。考虑一个包含一个输入层x、一个隐藏层h和一个输出层y的两层神经网络,如下图所示:

图 7.8:ANN 中的前向传播

假设我们有两个输入,x[1]和x[2],我们需要预测输出,。由于我们有两个输入,输入层中的神经元数量为两个。我们将隐藏层的神经元数量设为四个,输出层的神经元数量设为一个。现在,输入会与权重相乘,然后我们加上偏差,并将结果值传递到隐藏层,在那里应用激活函数。

在此之前,我们需要初始化权重矩阵。在实际中,我们不知道哪些输入比其他输入更重要,能够进行加权并计算输出。因此,我们随机初始化权重和偏置值。从输入层到隐藏层之间的权重和偏置值分别表示为 W[xh] 和 b[h]。那权重矩阵的维度呢?权重矩阵的维度必须是 当前层神经元的数量 x 下一层神经元的数量。为什么是这样呢?

因为这是一个基本的矩阵乘法规则。要乘以任意两个矩阵 AB,矩阵 A 的列数必须等于矩阵 B 的行数。所以,权重矩阵 W[xh] 的维度应该是 输入层神经元的数量 x 隐藏层神经元的数量,即 2 x 4:

上述方程表示, 。现在,这个值传递到隐藏层。在隐藏层,我们对 z[1] 应用激活函数。我们使用 sigmoid 激活函数 。然后,我们可以写成:

应用激活函数后,我们再次将结果 a[1] 与新的权重矩阵相乘,并加上在隐藏层和输出层之间流动的新偏置值。我们可以分别将这个权重矩阵和偏置表示为 W[hy] 和 b[y]。权重矩阵 W[hy] 的维度是 隐藏层神经元的数量 x 输出层神经元的数量。由于我们在隐藏层有四个神经元,输出层有一个神经元,W[hy] 的矩阵维度将是 4 x 1。因此,我们将 a[1] 与权重矩阵 W[hy] 相乘,并加上偏置 b[y],然后将结果 z[2] 传递给下一个层,即输出层:

现在,在输出层,我们对 z[2] 应用 sigmoid 函数,得到一个输出值:

从输入层到输出层的整个过程称为 前向传播。因此,为了预测输出值,输入从输入层传播到输出层。在传播过程中,它们会乘以每一层上的各自权重,并应用激活函数。完整的前向传播步骤如下所示:

前面的前向传播步骤可以通过以下 Python 代码实现:

def forward_prop(X):
    z1 = np.dot(X,Wxh) + bh
    a1 = sigmoid(z1)
    z2 = np.dot(a1,Why) + by
    y_hat = sigmoid(z2)

    return y_hat 

正向传播很酷,对吧?但我们怎么知道神经网络生成的输出是否正确呢?我们定义了一个新的函数,称为成本函数J),也叫损失函数L),它告诉我们神经网络的表现如何。成本函数有很多种不同的类型。我们将使用均方误差作为成本函数,可以定义为实际输出和预测输出之间平方差的平均值:

这里,n是训练样本的数量,y是实际输出,是预测输出。

好的,我们了解到成本函数是用来评估神经网络的,也就是说,它告诉我们神经网络在预测输出方面的好坏。但是问题是,我们的网络到底是怎么学习的呢?在正向传播中,网络只是试图预测输出。那么它是如何学习预测正确的输出呢?在下一部分,我们将探讨这个问题。

神经网络是如何学习的?

如果成本或损失非常高,那么说明我们的网络没有预测出正确的输出。因此,我们的目标是最小化成本函数,使得神经网络的预测更加准确。我们如何最小化成本函数呢?也就是说,我们如何最小化损失/成本呢?我们已经了解到,神经网络是通过正向传播来进行预测的。那么,如果我们能够在正向传播中改变一些值,我们就能预测出正确的输出并最小化损失。但我们可以改变正向传播中的哪些值呢?显然,我们不能改变输入和输出。现在,我们剩下的就是权重和偏置值。记住,我们刚开始是随机初始化了权重矩阵。由于这些权重是随机的,它们不可能是完美的。现在,我们将更新这些权重矩阵(W[xh] 和 W[hy]),使得我们的神经网络能够给出正确的输出。我们如何更新这些权重矩阵呢?这时出现了一种新技术,叫做梯度下降法

通过梯度下降法,神经网络可以学习到随机初始化的权重矩阵的最优值。有了这些最优的权重值,我们的网络就可以预测正确的输出并最小化损失。

现在,我们将探讨如何使用梯度下降法学习权重的最优值。梯度下降法是最常用的优化算法之一。它用于最小化成本函数,从而帮助我们最小化误差并获得可能的最低误差值。但梯度下降法是如何找到最优权重的呢?我们从一个类比开始。

想象一下,我们站在山顶,如下图所示,想要到达山的最低点。山上可能有许多看起来像是最低点的地方,但我们必须找到那个真正最低的点。

也就是说,我们不应该停留在一个点,认为它是最低点,尽管全局最低点存在:

图 7.9:梯度下降的类比

类似地,我们可以将成本函数表示如下。它是成本与权重的图像。我们的目标是最小化成本函数。也就是说,我们必须到达成本最小的最低点。下图中的实心黑点表示随机初始化的权重。如果我们将这个点向下移动,就可以到达成本最小的点:

图 7.10:梯度下降

那么我们如何将这个点(初始权重)向下移动呢?我们如何下降并到达最低点?梯度用于从一个点移动到另一个点。因此,我们可以通过计算成本函数相对于该点(初始权重)的梯度来移动这个点(初始权重),即

梯度是导数,实际上是切线的斜率,如下图所示。因此,通过计算梯度,我们可以下降(向下移动)并到达成本最小的最低点。梯度下降是一种一阶优化算法,这意味着我们在执行更新时只考虑一阶导数:

图 7.11:梯度下降

因此,使用梯度下降,我们将权重移动到成本最小的位置。但仍然,如何更新权重呢?

通过前向传播,我们到达了输出层。现在,我们将从输出层反向传播网络到输入层,并计算成本函数相对于输出层和输入层之间所有权重的梯度,以便我们最小化误差。计算完梯度后,我们将使用权重更新规则来更新旧的权重:

这意味着 权重 = 权重 - α x 梯度

那么 是什么?它被称为学习率。如以下图所示,如果学习率较小,那么我们会向下迈出较小的一步,梯度下降的速度可能会很慢。

如果学习率较大,那么我们会迈出较大的一步,梯度下降的速度会很快,但我们可能无法到达全局最小值,而是停留在局部最小值。因此,学习率应该选择得最优:

图 7.12:学习率的影响

这个从输出层反向传播网络到输入层并使用梯度下降更新网络权重以最小化损失的过程被称为反向传播。现在我们已经对反向传播有了基本了解,我们将通过一步步详细学习来加深理解。接下来我们将探讨一些有趣的数学内容,所以戴上你的微积分帽子,跟着步骤走。

所以,我们有两个权重,一个是 W[xh],即输入到隐藏层的权重,另一个是 W[hy],即从隐藏层到输出层的权重。我们需要找到这两个权重的最优值,以使我们得到最少的误差。因此,我们需要计算成本函数 J 关于这些权重的导数。由于我们正在进行反向传播,也就是从输出层到输入层,所以我们的第一个权重将是 W[hy]。因此,现在我们需要计算 JW[hy] 的导数。我们如何计算导数呢?首先,让我们回顾一下我们的成本函数 J

我们不能直接从前面的方程计算导数,因为没有 W[hy] 项。所以,我们不是直接计算导数,而是计算偏导数。让我们回顾一下我们的前向传播方程:

首先,我们将计算关于 的偏导数,然后从 中计算关于 z[2] 的偏导数。从 z[2] 开始,我们可以直接计算导数 W[hy]。这基本上就是链式法则。所以,JW[hy] 的导数变为如下:

现在,我们将计算前述方程中的每一项:

在这里, 是我们的 Sigmoid 激活函数的导数。我们知道 Sigmoid 函数是 ,所以 Sigmoid 函数的导数将是

接下来我们有:

因此,将前述所有项代入方程 (1),我们可以写成:

现在我们需要计算 J 关于下一个权重 W[xh] 的导数。

同样,由于我们在 J 中没有任何 W[xh] 项,我们不能直接计算 W[xh] 的导数。所以,我们需要使用链式法则。让我们再次回顾前向传播的步骤:

现在,根据链式法则,JW[xh] 的导数为:

我们已经看到如何计算前述方程中的前两项;现在,我们将看到如何计算其余的项:

因此,将前述所有项代入方程 (3),我们可以写成:

在我们计算了两个权重的梯度,W[hy] 和 W[xh] 之后,我们将根据权重更新规则更新初始权重:

就这样!这就是我们如何更新网络权重并最小化损失。现在,让我们看看如何在 Python 中实现反向传播算法。

在公式(2)(4)中,我们都有项,因此为了避免重复计算,我们将其命名为delta2

delta2 = np.multiply(-(y-yHat),sigmoidPrime(z2)) 

现在,我们计算关于W[hy]的梯度。参见公式(2)

dJ_dWhy = np.dot(a1.T,delta2) 

我们计算关于W[xh]的梯度。参见公式(4)

delta1 = np.dot(delta2,Why.T)*sigmoidPrime(z1)
dJ_dWxh = np.dot(X.T,delta1) 

我们将根据权重更新规则公式(5)(6)更新权重,具体如下:

Wxh = Wxh - alpha * dJ_dWhy
Why = Why - alpha * dJ_dWxh 

反向传播的完整代码如下所示:

def backword_prop(y_hat, z1, a1, z2):
    delta2 = np.multiply(-(y-y_hat),sigmoid_derivative(z2))
    dJ_dWhy = np.dot(a1.T, delta2)
    delta1 = np.dot(delta2,Why.T)*sigmoid_derivative(z1)
    dJ_dWxh = np.dot(X.T, delta1) 
    Wxh = Wxh - alpha * dJ_dWhy
    Why = Why - alpha * dJ_dWxh
    return Wxh,Why 

就这样。除此之外,还有不同变种的梯度下降方法,如随机梯度下降、小批量梯度下降、Adam、RMSprop 等。

在继续之前,让我们熟悉一些在神经网络中常用的术语:

  • 前向传播:前向传播意味着从输入层向输出层传播。

  • 反向传播:反向传播意味着从输出层回传到输入层。

  • 周期(Epoch):周期指定神经网络看到全部训练数据的次数。因此,我们可以说一个周期等于对所有训练样本进行一次前向传播和一次反向传播。

  • 批量大小:批量大小指定我们在一次前向传播和一次反向传播中使用的训练样本数。

  • 迭代次数:迭代次数指的是传递的次数,其中一次传递 = 一次前向传播 + 一次反向传播

假设我们有 12,000 个训练样本,并且我们的批量大小为 6,000。然后我们需要两次迭代来完成一个周期。也就是说,在第一次迭代中,我们传递前 6,000 个样本,并执行一次前向传播和一次反向传播;在第二次迭代中,我们传递接下来的 6,000 个样本,并执行一次前向传播和一次反向传播。经过两次迭代后,我们的神经网络将看到全部 12,000 个训练样本,这样就完成了一个周期。

综合起来

将我们到目前为止学习的所有概念结合起来,我们将看到如何从零开始构建一个神经网络。我们将理解神经网络如何学习执行 XOR 门操作。XOR 门仅当且仅当其输入中恰好有一个为 1 时返回 1,否则返回 0,如表 7.1所示:

表 7.1:XOR 运算

从零开始构建神经网络

为了执行 XOR 门操作,我们构建了一个简单的两层神经网络,如下图所示。如你所见,我们有一个包含两个节点的输入层、一个包含五个节点的隐藏层和一个包含一个节点的输出层:

图 7.13:人工神经网络(ANN)

我们将逐步理解神经网络如何学习 XOR 逻辑:

  1. 首先,导入所需的库:

    import numpy as np
    import matplotlib.pyplot as plt
    %matplotlib inline 
    
  2. 准备数据,如前面所示的 XOR 表:

    X = np.array([ [0, 1], [1, 0], [1, 1],[0, 0] ])
    y = np.array([ [1], [1], [0], [0]]) 
    
  3. 定义每一层的节点数:

    num_input = 2
    num_hidden = 5
    num_output = 1 
    
  4. 随机初始化权重和偏置。首先,我们初始化输入到隐藏层的权重:

    Wxh = np.random.randn(num_input,num_hidden)
    bh = np.zeros((1,num_hidden)) 
    
  5. 现在,我们初始化隐藏层到输出层的权重:

    Why = np.random.randn (num_hidden,num_output)
    by = np.zeros((1,num_output)) 
    
  6. 定义 Sigmoid 激活函数:

    def sigmoid(z):
        return 1 / (1+np.exp(-z)) 
    
  7. 定义 sigmoid 函数的导数:

    def sigmoid_derivative(z):
         return np.exp(-z)/((1+np.exp(-z))**2) 
    
  8. 定义前向传播:

    def forward_prop(x,Wxh,Why):
        z1 = np.dot(x,Wxh) + bh
        a1 = sigmoid(z1)
        z2 = np.dot(a1,Why) + by
        y_hat = sigmoid(z2)
    
        return z1,a1,z2,y_hat 
    
  9. 定义反向传播:

    def backword_prop(y_hat, z1, a1, z2):
        delta2 = np.multiply(-(y-y_hat),sigmoid_derivative(z2))
        dJ_dWhy = np.dot(a1.T, delta2)
        delta1 = np.dot(delta2,Why.T)*sigmoid_derivative(z1)
        dJ_dWxh = np.dot(x.T, delta1) 
        return dJ_dWxh, dJ_dWhy 
    
  10. 定义成本函数:

    def cost_function(y, y_hat):
        J = 0.5*sum((y-y_hat)**2)
    
        return J 
    
  11. 设置学习率和训练迭代次数:

    alpha = 0.01
    num_iterations = 5000 
    
  12. 现在,让我们通过以下代码开始训练网络:

    cost =[]
    for i in range(num_iterations):
        z1,a1,z2,y_hat = forward_prop(X,Wxh,Why)    
        dJ_dWxh, dJ_dWhy = backword_prop(y_hat, z1, a1, z2)
    
        #update weights
        Wxh = Wxh -alpha * dJ_dWxh
        Why = Why -alpha * dJ_dWhy
    
        #compute cost
        c = cost_function(y, y_hat)
    
        cost.append(c) 
    
  13. 绘制成本函数:

    plt.grid()
    plt.plot(range(num_iterations),cost)
    plt.title('Cost Function')
    plt.xlabel('Training Iterations')
    plt.ylabel('Cost') 
    

如你在下图中所观察到的,损失随着训练迭代次数的增加而减少:

图 7.14:成本函数

因此,我们对人工神经网络(ANNs)及其学习方式有了整体的理解。

递归神经网络

The sun rises in the ____.

如果我们被要求预测前面句子中的空白部分,我们可能会说是 east。为什么我们会预测单词 east 是正确的呢?因为我们读了整个句子,理解了上下文,并预测单词 east 是一个合适的词来完成这个句子。

如果我们使用前馈神经网络(在上一节中学习的网络)来预测空白,它将无法预测正确的单词。这是因为在前馈网络中,每个输入都是独立的,它们仅根据当前输入进行预测,而不会记住之前的输入。

因此,网络的输入将仅是空白前的单词,即单词the。仅凭这个单词作为输入,我们的网络无法预测正确的单词,因为它无法理解句子的上下文,这意味着它不知道前面的一系列单词,因此无法理解句子的上下文并预测出一个合适的下一个单词。

在这里我们使用递归神经网络RNNs)。它们不仅仅基于当前输入来预测输出,还会基于之前的隐藏状态进行预测。为什么它们必须基于当前输入和之前的隐藏状态来预测输出呢?为什么不能只使用当前输入和之前的输入呢?

这是因为之前的输入只会存储关于前一个单词的信息,而之前的隐藏状态将捕获网络到目前为止所看到的句子中所有单词的上下文信息。基本上,之前的隐藏状态就像记忆一样,它捕获了句子的上下文。有了这些上下文信息和当前输入,我们就可以预测相关的单词。

例如,让我们来看这个句子,The sun rises in the ____. 如下图所示,我们首先将单词the作为输入,然后将下一个单词sun作为输入;但同时,我们还会传递之前的隐藏状态,h[0]。因此,每次我们传递输入单词时,我们也会传递之前的隐藏状态作为输入。

在最后一步,我们传递单词 the,以及之前的隐藏状态 h[3],它捕获了网络迄今为止看到的单词序列的上下文信息。因此,h[3] 充当记忆并存储网络已看到的所有前面的单词的信息。通过 h[3] 和当前输入单词(the),我们可以预测下一个相关的单词:

图 7.15:RNN

简而言之,RNN 使用前一个隐藏状态作为记忆,这个记忆捕获并存储网络迄今为止看到的上下文信息(输入)。

RNN 被广泛应用于涉及顺序数据的应用场景,如时间序列、文本、音频、语音、视频、天气等。它们在各种 自然语言处理NLP)任务中得到了广泛应用,如语言翻译、情感分析、文本生成等。

前馈网络与 RNN 之间的区别

RNN 与前馈网络的比较见 图 7.16

图 7.16:前馈网络与 RNN 的区别

如前图所示,RNN 在隐藏层中包含一个循环连接,这意味着我们使用前一个隐藏状态以及输入来预测输出。

还是不明白?让我们来看一下 RNN 的展开版本。但等等,RNN 的展开版本是什么?

这意味着我们将网络展开以适应完整的序列。假设我们有一个包含 T 个单词的输入句子;那么,我们将有从 0 到 T-1 的层,每个单词对应一个层,如 图 7.17 所示:

图 7.17:展开的 RNN

图 7.17 所示,在时间步 t = 1 时,输出 y[1] 是基于当前输入 x[1] 和前一个隐藏状态 h[0] 预测的。类似地,在时间步 t = 2 时,y[2] 是基于当前输入 x[2] 和前一个隐藏状态 h[1] 预测的。这就是 RNN 的工作原理;它通过当前输入和前一个隐藏状态来预测输出。

RNN 中的前向传播

让我们看看 RNN 如何利用前向传播来预测输出;但在我们直接进入之前,先来了解一下符号的定义:

图 7.18:RNN 中的前向传播

上面的图示说明了以下内容:

  • U 表示输入到隐藏层的权重矩阵

  • W 表示隐藏到隐藏层的权重矩阵

  • V 表示隐藏到输出层的权重矩阵

在时间步 t 上的隐藏状态 h 可以通过以下公式计算:

即,时间步的隐藏状态,t = tanh([输入到隐藏层的权重 x 输入] + [隐藏到隐藏层的权重 x 前一个隐藏状态])。

时间步 t 上的输出可以通过以下公式计算:

即,时间步的输出,t = softmax(隐藏到输出层的权重 x 时间步 t 上的隐藏状态)

我们也可以像下图所示表示 RNN。正如你所看到的,隐藏层由一个 RNN 模块表示,这意味着我们的网络是一个 RNN,且前一个隐藏状态被用于预测输出:

图 7.19:RNN 中的前向传播

图 7.20 显示了在 RNN 的展开版本中前向传播的工作方式:

图 7.20:展开版本——RNN 中的前向传播

我们用随机值初始化初始隐藏状态 h[init]。正如你在前面的图中看到的,输出,,是基于当前输入,x[0],和前一个隐藏状态(即初始隐藏状态 h[init])预测的,使用以下公式:

类似地,看看输出是如何计算的,。它使用当前输入,x[1],以及前一个隐藏状态,h[0]:

因此,在前向传播中预测输出时,RNN 使用当前输入和前一个隐藏状态。

反向传播

我们刚刚了解了 RNN 中前向传播的工作原理以及它是如何预测输出的。现在,我们计算每个时间步 t 的损失 L,以确定 RNN 预测输出的效果如何。我们使用交叉熵损失作为损失函数。时间步 t 的损失 L 可以表示为:

这里,y[t] 是实际输出,而 是在时间步 t 预测的输出。

最终的损失是所有时间步损失的总和。假设我们有 T - 1 层,那么最终的损失可以表示为:

图 7.21 显示了最终的损失是通过所有时间步的损失总和获得的:

图 7.21:RNN 中的反向传播

我们计算了损失,现在我们的目标是最小化损失。我们怎么做才能最小化损失呢?我们可以通过找到 RNN 的最佳权重来最小化损失。正如我们所学,RNN 中有三种权重:从输入到隐藏的权重 U,从隐藏到隐藏的权重 W,以及从隐藏到输出的权重 V

我们需要为这三种权重找到最佳值,以最小化损失。我们可以使用我们喜欢的梯度下降算法来找到最佳权重。我们从计算损失函数关于所有权重的梯度开始,然后根据以下权重更新规则更新权重:

然而,RNN 有一个问题。梯度计算涉及计算相对于激活函数的梯度。当我们计算相对于 sigmoid 或 tanh 函数的梯度时,梯度会变得非常小。当我们在多个时间步上进一步反向传播并乘以梯度时,梯度会趋向变得越来越小。这就是所谓的消失梯度问题。

由于梯度随时间消失,我们无法学习关于长期依赖的信息,也就是说,RNN 无法在记忆中长时间保留信息。消失梯度问题不仅出现在 RNN 中,还出现在其他深度网络中,特别是在我们有多个隐藏层并使用 sigmoid/tanh 函数时。

解决消失梯度问题的一个方法是使用 ReLU 作为激活函数。然而,我们有一个 RNN 的变体叫做长短期记忆LSTM),它能够有效地解决消失梯度问题。我们将在接下来的部分看到它是如何工作的。

LSTM 来解救

在反向传播 RNN 时,我们学习了一个问题,叫做消失梯度。由于消失梯度问题,我们无法正确训练网络,这导致 RNN 无法在记忆中保持长序列。为了理解这个问题的含义,假设我们有一个简单的句子:

The sky is __.

RNN 可以根据它所看到的信息轻松预测空白处为蓝色,但它无法涵盖长期的依赖关系。这是什么意思?我们通过以下句子来更好地理解这个问题:

Archie 在中国生活了 13 年。他喜欢听好音乐。他是漫画迷。他能流利地说 __。

现在,如果我们被要求预测前一句话中的缺失词,我们会预测它是中文,但我们是如何预测的呢?我们简单地记住了前面的句子,并理解到 Archie 在中国生活了 13 年。这使我们得出结论,Archie 可能会说流利的中文。而 RNN 则无法将所有这些信息保存在记忆中,进而说出 Archie 能流利地讲中文。

由于消失梯度问题,它无法在记忆中长时间回忆/记住信息。也就是说,当输入序列较长时,RNN 的记忆(隐藏状态)无法保存所有信息。为了解决这个问题,我们使用了 LSTM 单元。

LSTM 是 RNN 的一种变体,能够解决消失梯度问题,并在需要时将信息保留在记忆中。基本上,RNN 单元在隐藏层中被 LSTM 单元替代,如图 7.22所示:

图 7.22:LSTM 网络

在下一部分中,我们将了解 LSTM 单元是如何工作的。

理解 LSTM 单元

LSTM 单元有什么特别之处?LSTM 单元是如何实现长期依赖的?它是如何知道哪些信息需要保留,哪些信息需要从记忆中丢弃的?

这一切都是通过名为的特殊结构实现的。如以下图所示,一个典型的 LSTM 单元由三个特殊的门组成,分别是输入门、输出门和遗忘门:

图 7.23:LSTM 门

这三个门负责决定从记忆中添加、输出和忘记哪些信息。有了这些门,LSTM 单元能够有效地仅在需要时保留信息在记忆中。图 7.24 展示了一个典型的 LSTM 单元:

图 7.24:LSTM 单元

如果你查看 LSTM 单元,顶部的水平线被称为单元状态。它是信息流动的地方。单元状态上的信息将通过 LSTM 门不断更新。现在,我们将了解这些门的功能:

遗忘门:遗忘门负责决定哪些信息不应该出现在单元状态中。请看以下语句:

哈里是个好歌手。他住在纽约。赞恩也是个好歌手。

一旦我们开始谈论赞恩,网络会理解主题已经从哈里转移到赞恩,哈里的信息不再需要。现在,遗忘门会从单元状态中删除/忘记哈里的信息。

输入门:输入门负责决定哪些信息应该被存储在记忆中。我们来看同一个例子:

哈里是个好歌手。他住在纽约。赞恩也是个好歌手。

因此,在遗忘门从单元状态中删除信息后,输入门决定哪些信息必须存在于记忆中。这里,由于哈里的信息被遗忘门从单元状态中移除,输入门决定更新单元状态,将赞恩的信息加入其中。

输出门:输出门负责决定在某一时刻 t 从单元状态中展示哪些信息。现在,考虑以下句子:

赞恩的首张专辑取得了巨大成功。祝贺 __。

这里,"congrats" 是一个形容词,用来描述名词。输出层将预测赞恩(名词),以填补空白。

因此,使用 LSTM,我们可以克服 RNN 中遇到的梯度消失问题。在下一节中,我们将学习另一种有趣的算法——卷积神经网络CNN)。

什么是 CNN?

CNN,也称为卷积神经网络,是计算机视觉任务中最广泛使用的深度学习算法之一。假设我们正在执行图像识别任务。请看以下图像。

我们希望我们的 CNN 能识别出它包含一匹马:

图 7.25:包含一匹马的图像

我们如何做到这一点呢?当我们将图像输入计算机时,它基本上会将图像转换为一个像素值矩阵。像素值的范围是 0 到 255,矩阵的维度是[图像宽度 x 图像高度 x 通道数]。灰度图像有一个通道,彩色图像有三个通道,分别是红色、绿色和蓝色RGB)。

假设我们有一张彩色输入图像,宽度为 11,高度为 11,即 11 x 11,那么我们的矩阵维度将是[11 x 11 x 3]。如11 x 11 x 3所示,11 x 11 表示图像的宽度和高度,3 表示通道数,因为我们有一张彩色图像。所以,我们将得到一个 3D 矩阵。

但是,3D 矩阵很难可视化,因此,为了便于理解,我们假设输入是灰度图像。由于灰度图像只有一个通道,所以我们将得到一个二维矩阵。

如下图所示,输入的灰度图像将被转换为一个像素值矩阵,像素值的范围是 0 到 255,像素值表示该点的像素强度:

图 7.26:输入图像被转换为像素值矩阵

输入矩阵中给出的值只是为了帮助我们理解而设置的任意值。

好的,现在我们有了一个像素值输入矩阵。接下来会发生什么呢?CNN 是如何理解图像包含一匹马的呢?CNN 由以下三个重要层组成:

  • 卷积层

  • 池化层

  • 全连接层

在这三层的帮助下,CNN 识别出图像中包含马的特征。接下来我们将详细探讨这些层。

卷积层

卷积层是 CNN 的第一个也是核心的层。它是 CNN 的构建块之一,用于从图像中提取重要特征。

我们有一张马的图片。你认为有哪些特征可以帮助我们理解这是马的图片呢?我们可以说是身体结构、面部、四肢、尾巴等等。但卷积神经网络(CNN)是如何理解这些特征的呢?这就是我们使用卷积操作的地方,卷积操作将从图像中提取出所有能够表征马的关键特征。所以,卷积操作帮助我们理解图像的内容。

好的,这个卷积操作到底是什么?它是如何执行的?它如何提取重要特征?我们来详细了解一下。

正如我们所知,每个输入图像都由一个像素值矩阵表示。除了输入矩阵,我们还需要另一个矩阵,称为滤波器矩阵

滤波器矩阵也称为,或者简单地称为滤波器,如图 7.27所示:

图 7.27:输入和滤波器矩阵

我们取滤波器矩阵,将它滑动到输入矩阵上一个像素,执行逐元素相乘,求和结果,最终得到一个数字。是不是有点让人困惑呢?让我们通过以下示意图更好地理解:

图 7.28:卷积操作

正如你在上面的示意图中看到的,我们将滤波器矩阵放在输入矩阵上方,执行逐元素相乘,求和结果,得到了一个数字。证明如下:

现在,我们将滤波器在输入矩阵上滑动一个像素,并执行相同的步骤,如图 7.29所示:

图 7.29:卷积操作

证明如下:

接下来,我们将滤波器矩阵滑动一个像素,并执行相同的操作,如图 7.30所示:

图 7.30:卷积操作

证明如下:

现在,再次,我们将滤波器矩阵滑动一个像素到输入矩阵上,并执行相同的操作,如图 7.31所示:

图 7.31:卷积操作

也就是说:

好的,我们在这里做什么?我们基本上是将滤波器矩阵滑动到整个输入矩阵上,每次滑动一个像素,执行逐元素相乘并将结果相加,最终产生一个新的矩阵,称为特征图激活图。这就是所谓的卷积操作

正如我们所学,卷积操作用于提取特征,新的矩阵,即特征图,表示提取的特征。如果我们绘制特征图,那么我们就可以看到卷积操作提取出的特征。

图 7.32展示了实际图像(输入图像)和卷积后的图像(特征图)。我们可以看到,我们的滤波器已经从实际图像中检测到了边缘作为特征:

图 7.32:将实际图像转换为卷积后的图像

使用各种滤波器从图像中提取不同的特征。例如,如果我们使用锐化滤波器,,它将会锐化我们的图像,如下图所示:

图 7.33:锐化后的图像

因此,我们已经了解到,利用滤波器,我们可以通过卷积操作从图像中提取重要特征。因此,我们不仅可以使用一个滤波器,还可以使用多个滤波器从图像中提取不同的特征,并生成多个特征图。这样,特征图的深度将是滤波器的数量。如果我们使用七个滤波器来提取图像中的不同特征,那么我们的特征图的深度将是七:

图 7.34:特征图

好的,我们已经学会了不同的滤波器从图像中提取不同的特征。但问题是,我们如何为滤波器矩阵设置正确的值,以便能够从图像中提取重要的特征呢?别担心!我们只需要随机初始化滤波器矩阵,而滤波器矩阵的最佳值——能够从图像中提取重要特征的值——会通过反向传播学习到。但是,我们只需要指定滤波器的大小以及我们想要使用的滤波器数量。

步长

我们刚刚学习了卷积操作是如何工作的。我们以一个像素为单位,用滤波器矩阵滑动输入矩阵并执行卷积操作。但我们不一定要每次只滑动一个像素,也可以选择按任意数量的像素滑动输入矩阵。

滤波器矩阵滑动输入矩阵的像素数量称为步长

如果我们将步长设置为 2,那么我们就会以两个像素为单位,用滤波器矩阵滑动输入矩阵。图 7.35展示了一个步长为 2 的卷积操作:

图 7.35:步长操作

那么我们如何选择步长呢?我们刚刚了解到,步长是指我们滑动滤波器矩阵时沿某一方向移动的像素数量。所以,当步长设置为较小的数字时,我们可以比设置较大步长时更详细地编码图像的特征。然而,较大的步长值所需的计算时间比较小步长值要少。

填充

在卷积操作中,我们是用滤波器矩阵在输入矩阵上滑动。但在某些情况下,滤波器无法完美地适配输入矩阵。这里是什么意思呢?举个例子,假设我们正在进行步长为 2 的卷积操作。在某些情况下,当我们将滤波器矩阵移动两个像素时,它会到达边界,而滤波器矩阵无法适配输入矩阵。也就是说,滤波器矩阵的一部分超出了输入矩阵,如下图所示:

图 7.36:填充操作

在这种情况下,我们执行填充操作。我们可以简单地用零填充输入矩阵,使得滤波器能够适配输入矩阵,如图 7.37所示。用零填充输入矩阵的操作称为相同填充零填充

图 7.37:相同填充

我们也可以选择不使用零填充,而是直接丢弃滤波器无法适配的输入矩阵区域。这称为有效填充

图 7.38:有效填充

池化层

好的,现在我们已经完成了卷积操作。通过卷积操作,我们得到了特征图。但是特征图的维度太大了。为了减少特征图的维度,我们进行池化操作。池化操作减少了特征图的维度,只保留必要的细节,从而减少计算量。

例如,为了从图像中识别马,我们需要提取并仅保留马的特征;我们可以简单地丢弃不需要的特征,例如图像的背景等。池化操作也叫做下采样子采样操作,它使得 CNN 具有平移不变性。因此,池化层通过只保留重要特征来减少空间维度。

有不同类型的池化操作,包括最大池化、平均池化和求和池化。

在最大池化中,我们在输入矩阵上滑动滤波器,并简单地从滤波窗口中取最大值,如图 7.39所示:

图 7.39:最大池化

在平均池化中,我们在滤波窗口内取输入矩阵的平均值,在求和池化中,我们将滤波窗口内输入矩阵的所有值求和。

全连接层

到目前为止,我们已经了解了卷积层和池化层的工作原理。一个 CNN 可以有多个卷积层和池化层。然而,这些层仅仅从输入图像中提取特征并生成特征图;也就是说,它们只是特征提取器。

给定任何图像,卷积层从图像中提取特征并生成特征图。现在,我们需要对这些提取的特征进行分类。因此,我们需要一个算法来分类这些提取的特征,并告诉我们这些特征是马的特征,还是其他的特征。为了进行分类,我们使用前馈神经网络。我们将特征图扁平化并将其转换为一个向量,然后将其作为输入传递给前馈网络。

前馈网络将这个扁平化的特征图作为输入,应用激活函数,例如 sigmoid,并返回输出,指出图像是否包含马;这被称为全连接层,下面的图示展示了这一过程:

图 7.40:全连接层

让我们看看这些如何融合在一起。

CNN 的架构

图 7.41展示了 CNN 的架构:

图 7.41:CNN 架构

正如你所注意到的,首先我们将输入图像传递给卷积层,在这里我们应用卷积操作,从图像中提取重要特征并创建特征图。然后,我们将特征图传递给池化层,在池化层中,特征图的维度将会被降低。

如前面的图所示,我们可以有多个卷积层和池化层,同时也需要注意,池化层不一定必须跟在每个卷积层之后;可以有多个卷积层,之后才接池化层。

所以,在卷积层和池化层之后,我们将结果特征图展平,并将其输入到全连接层,这基本上是一个前馈神经网络,依据特征图对给定的输入图像进行分类。

现在我们已经了解了卷积神经网络(CNN)的工作原理,在接下来的部分,我们将学习另一个有趣的算法——生成对抗网络。

生成对抗网络

生成对抗网络GAN)最早由 Ian J. Goodfellow、Jean Pouget-Abadie、Mehdi Mirza、Bing Xu、David Warde-Farley、Sherjil Ozair、Aaron Courville 和 Yoshua Bengio 在他们 2014 年的论文《生成对抗网络》中提出。

生成对抗网络(GAN)广泛用于生成新的数据点。它们可以应用于任何类型的数据集,但通常用于生成图像。生成对抗网络的一些应用包括生成逼真的人脸图像、将灰度图像转换为彩色图像、将文本描述转化为逼真的图像等。

近年来,生成对抗网络(GAN)已经发展得非常成熟,能够生成非常逼真的图像。下图展示了生成对抗网络在五年内生成图像的演变过程:

图 7.42:生成对抗网络的演变过程

对生成对抗网络(GAN)感到兴奋了吗?现在,我们将详细了解它们是如何工作的。在继续之前,让我们考虑一个简单的类比。假设你是警察,你的任务是找出伪钞,而伪钞制造者的角色是制造假钞并欺骗警察。

伪钞制造者不断尝试制造出与真钞几乎无法区分的假钞,但警察的任务是判断钞票是真是假。所以,伪钞制造者和警察实际上是在进行一场双人对抗游戏,其中一个试图战胜另一个。生成对抗网络的工作原理也类似。它们由两个重要的组件组成:

  • 生成器

  • 判别器

你可以将生成器看作是伪钞制造者,而判别器则类似于警察。也就是说,生成器的角色是制造假钞,而判别器的角色是识别这些钞票是真是假。

在不深入细节的情况下,我们首先对 GAN 有一个基本的了解。假设我们希望我们的 GAN 生成手写数字。我们该如何做到呢?首先,我们将获取一个包含手写数字的数据库,比如 MNIST 数据集。生成器学习我们数据集中的图像分布。因此,它学习训练集中手写数字的分布。一旦它学习到数据集中的图像分布,我们向生成器输入随机噪声,它将根据学到的分布,将随机噪声转换为类似于我们训练集中手写数字的新图像:

图 7.43: 生成器

判别器的目标是执行分类任务。给定一张图像,它将其分类为真实或虚假;也就是说,判断该图像是来自训练集,还是由生成器生成的图像:

图 7.44: 判别器

GAN 的生成器组件基本上是一个生成模型,而判别器组件基本上是一个判别模型。因此,生成器学习类别的分布,判别器学习类别的决策边界。

图 7.45所示,我们向生成器输入随机噪声,生成器将这些随机噪声转换为一张新的图像,这张图像与我们训练集中的图像相似,但不完全相同。由生成器生成的图像称为假图像,而训练集中的图像称为真实图像。我们将真实图像和假图像都输入判别器,判别器告诉我们它们是“真实”的概率。如果图像是假的,它返回 0;如果图像是真的,它返回 1:

图 7.45: GAN

现在我们对生成器和判别器有了基本了解,我们将详细研究每个组件。

解析生成器

GAN 的生成器组件是一个生成模型。我们所说的生成模型有两种类型——隐式显式密度模型。隐式密度模型不使用任何显式密度函数来学习概率分布,而显式密度模型顾名思义,使用显式密度函数。GAN 属于第一类。也就是说,它们是一个隐式密度模型。让我们详细研究并理解 GAN 是如何成为一个隐式密度模型的。

假设我们有一个生成器,G。它基本上是一个由 参数化的神经网络。生成器网络的作用是生成新的图像。它们是如何做到这一点的呢?生成器的输入应该是什么?

我们从一个正态分布或均匀分布中采样一个随机噪声,z,其概率分布为 P[z]。我们将这个随机噪声 z 作为输入传递给生成器,然后生成器将这个噪声转换为一张图像:

惊讶吧?生成器是如何将随机噪声转换为逼真的图像的呢?

假设我们有一个包含人脸图像的数据集,我们希望生成器生成一张新的面部图像。首先,生成器通过学习我们训练集中的图像概率分布来学习面部的所有特征。一旦生成器学会了正确的概率分布,它就能生成全新的面部图像。

那么,生成器是如何学习训练集的分布的呢?也就是说,生成器是如何学习训练集中人脸图像的分布的?

生成器其实不过是一个神经网络。那么,发生的事情是,神经网络隐式地学习了我们训练集中的图像分布;我们把这个分布叫做生成器分布,P[g]。在第一次迭代时,生成器生成的是一张非常嘈杂的图像。但经过多次迭代后,它学会了我们训练集的确切概率分布,并通过调整其参数学会生成正确的图像。

需要注意的是,我们并没有使用均匀分布P[z]来学习训练集的分布。它只是用来采样随机噪声,我们将这些随机噪声作为输入馈送给生成器。生成器网络隐式地学习了我们训练集的分布,我们把这个分布叫做生成器分布,P[g],这就是我们称生成器网络为隐式密度模型的原因。

现在我们理解了生成器,让我们来看看鉴别器。

拆解鉴别器

如名字所示,鉴别器是一个判别模型。假设我们有一个鉴别器,D。它也是一个神经网络,并且由参数化。

鉴别器的目标是区分两类图像。也就是说,给定一张图像x,它必须识别该图像是来自真实分布还是伪造分布(生成器分布)。也就是说,鉴别器必须识别输入的图像是来自训练集,还是来自生成器生成的伪造图像:

我们把训练集的分布叫做真实数据分布,用P[r]表示。我们知道生成器分布用P[g]表示。

所以,鉴别器D本质上是在尝试判断图像x是来自P[r]还是P[g]。

那么,它们是如何学习的呢?

到目前为止,我们只研究了生成器和鉴别器的作用,但它们究竟是如何学习的呢?生成器是如何学会生成新的真实图像的,鉴别器又是如何学会正确区分图像的呢?

我们知道生成器的目标是生成一张图像,使得它能够欺骗鉴别器,让鉴别器相信生成的图像来自真实分布。

在第一次迭代中,生成器生成了一张噪声图像。当我们将此图像输入鉴别器时,它可以轻松地检测到该图像来自生成器分布。生成器将此视为一种损失并尝试改进自身,因为它的目标是欺骗鉴别器。也就是说,如果生成器知道鉴别器容易将生成的图像检测为虚假图像,那么这意味着它没有生成类似于训练集中图像的图像。这暗示着它还没有学习到训练集的概率分布。

因此,生成器调整其参数,以学习训练集的正确概率分布。正如我们所知,生成器是一个神经网络,我们只需通过反向传播更新网络的参数。一旦它学会了真实图像的概率分布,就能够生成与训练集中图像相似的图像。

好的,鉴别器呢?它是如何学习的?如我们所知,鉴别器的作用是区分真实和虚假图像。

如果鉴别器错误地分类了生成的图像;也就是说,如果鉴别器将虚假图像分类为真实图像,那么就意味着鉴别器没有学会区分真实和虚假图像。因此,我们通过反向传播更新鉴别器网络的参数,使得鉴别器学会区分真实和虚假图像。

因此,基本上,生成器通过学习真实数据分布P[r]来试图欺骗鉴别器,而鉴别器则试图判断图像是来自真实分布还是虚假分布。那么问题来了,我们应该在何时停止训练网络,因为生成器和鉴别器在相互竞争?

基本上,GAN 的目标是生成与训练集中图像相似的图像。假设我们想生成一张人脸——我们学习训练集中图像的分布,然后生成新的面孔。因此,对于生成器,我们需要找到最佳的鉴别器。这是什么意思呢?

我们知道,生成器分布由P[g]表示,而真实数据分布由P[r]表示。如果生成器完美地学习了真实数据的分布,那么P[g]就等于P[r],正如图 7.46所示:

图 7.46:生成器和真实数据分布

P[g] = P[r]时,鉴别器无法区分输入图像是来自真实分布还是虚假分布,因此它会返回 0.5 作为概率,因为当两者分布相同时,鉴别器会变得混淆。

因此,对于生成器,最佳的鉴别器可以定义如下:

因此,当判别器对所有生成器生成的图像返回 0.5 的概率时,我们可以说生成器已经学习到了训练集图像的分布,并成功欺骗了判别器。

GAN 的架构

图 7.47 显示了 GAN 的架构:

图 7.47:GAN 的架构

如前面的图示所示,生成器 G 将随机噪声 z 作为输入,通过从均匀分布或正态分布中采样,隐式学习训练集的分布并生成假图像。

我们从真实数据分布 和假数据分布 中采样图像 x,并将其输入到判别器 D 中。我们将真实图像和假图像输入判别器,判别器执行二分类任务。也就是说,当图像是假的时,它返回 0,当图像是真的时,它返回 1。

揭开损失函数的神秘面纱

现在我们将研究 GAN 的损失函数。在继续之前,先回顾一下符号:

  • 作为生成器输入的噪声用 z 表示

  • 噪声 z 的采样来自均匀分布或正态分布,该分布用 P[z] 表示

  • 输入图像由 x 表示

  • 真实数据分布或训练集的分布用 P[r] 表示

  • 假数据分布或生成器的分布用 P[g] 表示

当我们写出 时,意味着图像 x 是从真实分布 P[r] 中采样的。同样, 表示图像 x 是从生成器分布 P[g] 中采样的,而 则意味着生成器输入 z 是从均匀分布 P[z] 中采样的。

我们已经了解到,生成器和判别器都是神经网络,且它们都会通过反向传播来更新其参数。现在我们需要找到最优的生成器参数 和判别器参数

判别器损失

现在我们将看看判别器的损失函数。我们知道判别器的目标是分类图像是真实的还是假的。我们用 D 来表示判别器。

判别器的损失函数如下所示:

这意味着什么呢?让我们逐个理解这些术语。

第一项

让我们来看第一项:

这里, 表示我们从真实数据分布 P[r] 中采样输入 x,因此 x 是一张真实图像。

D(x) 表示我们将输入图像 x 提供给判别器 D,判别器将返回输入图像 x 是真实图像的概率。由于 x 是从真实数据分布 P[r] 中采样的,我们知道 x 是一张真实图像。因此,我们需要最大化 D(x) 的概率:

但我们不是最大化原始概率,而是最大化对数概率,因此,我们可以写出以下内容:

所以,我们的最终方程变为以下形式:

表示从真实数据分布中采样的输入图像为真实图像的对数似然期望。

第二项

现在,让我们看一下第二项:

这里, 表示我们从均匀分布 P[z] 中采样一个随机噪声 zG(z) 表示生成器 G 以随机噪声 z 作为输入,并根据其隐式学习的分布 P[g] 返回一张假图像。

D(G(z)) 表示我们将生成器生成的假图像输入到判别器 D 中,判别器将返回该假输入图像是一个真实图像的概率。

如果我们从 1 中减去 D(G(z)),那么它将返回假输入图像是一个假图像的概率:

由于我们知道 z 不是一张真实的图像,判别器将最大化这个概率。也就是说,判别器最大化 z 被分类为假图像的概率,因此我们写出:

我们不是最大化原始概率,而是最大化对数概率:

表示生成器生成的输入图像为假图像的对数似然期望。

最后一项

所以,结合这两项,判别器的损失函数表示为:

这里, 分别是生成器和判别器网络的参数。因此,判别器的目标是找到合适的 ,以便它可以正确地分类图像。

生成器损失

生成器的损失函数如下:

我们知道生成器的目标是欺骗判别器,将假图像分类为真实图像。

判别器损失 部分,我们看到 表示将假输入图像分类为假图像的概率,而判别器最大化正确将假图像分类为假的概率。

但生成器希望最小化这个概率。由于生成器希望欺骗判别器,它最小化假输入图像被判别器分类为假图像的概率。因此,生成器的损失函数可以表示为:

总损失

我们刚刚了解了生成器和判别器的损失函数,通过结合这两者,我们将最终的损失函数写为:

因此,我们的目标函数本质上是一个 min-max 目标函数,也就是对判别器进行最大化,对生成器进行最小化,并且我们通过反向传播相应的网络来找到最优的生成器参数!,和判别器参数!

因此,我们进行梯度上升;也就是说,对判别器进行最大化:

另外,我们进行梯度下降;也就是说,对生成器进行最小化:

概要

我们通过理解生物神经元和人工神经元开始了本章的学习。然后我们学习了 ANN 及其层次结构。我们了解了不同类型的激活函数以及它们如何在网络中引入非线性。

后来,我们学习了神经网络中的前向传播和反向传播。接着,我们学习了如何实现一个 ANN。接下来,我们了解了 RNN 以及它们与前馈网络的不同之处。然后,我们学习了 RNN 的一个变种——LSTM。接着,我们学习了 CNN,了解了它们如何使用不同类型的层,并详细探讨了 CNN 的架构。

在本章结束时,我们了解了一个有趣的算法——GAN。我们理解了 GAN 的生成器和判别器组件,并且详细探讨了 GAN 的架构。接着,我们详细研究了 GAN 的损失函数。

在下一章,我们将学习一个最受欢迎的深度学习框架——TensorFlow。

问题

让我们通过回答以下问题来评估我们对深度学习算法的理解:

  1. 什么是激活函数?

  2. 定义 softmax 函数。

  3. 什么是一个 epoch?

  4. RNN 的一些应用是什么?

  5. 解释消失梯度问题。

  6. 有哪些不同类型的池化操作?

  7. 解释 GAN 的生成器和判别器组件。

进一步阅读

第八章:TensorFlow 入门

TensorFlow 是最受欢迎的深度学习库之一。在接下来的章节中,我们将使用 TensorFlow 构建深度强化学习模型。因此,在本章中,我们将熟悉 TensorFlow 及其功能。

我们将了解什么是计算图,以及 TensorFlow 如何使用它们。我们还将探讨 TensorBoard,这是 TensorFlow 提供的一个可视化工具,用于可视化模型。接下来,我们将理解如何使用 TensorFlow 构建神经网络来执行手写数字分类。

接下来,我们将学习 TensorFlow 2.0,它是 TensorFlow 的最新版本。我们将了解 TensorFlow 2.0 与其先前版本的不同之处,以及它如何使用 Keras 作为其高级 API。

在本章中,我们将学习以下内容:

  • TensorFlow

  • 计算图和会话

  • 变量、常量和占位符

  • TensorBoard

  • TensorFlow 中的手写数字分类

  • TensorFlow 中的数学运算

  • TensorFlow 2.0 和 Keras

什么是 TensorFlow?

TensorFlow 是 Google 的一个开源软件库,广泛用于数值计算。它是构建深度学习模型时使用最广泛的库之一,具有高度的可扩展性,并能在多个平台上运行,例如 Windows、Linux、macOS 和 Android。最初由 Google Brain 团队的研究人员和工程师开发。

TensorFlow 支持在各种设备上执行,包括 CPU、GPU、TPU(张量处理单元)、移动平台和嵌入式平台。由于其灵活的架构和易于部署,它已成为许多研究人员和科学家构建深度学习模型时的热门选择。

在 TensorFlow 中,每个计算都是通过数据流图表示的,也称为 计算图,其中节点代表操作(如加法或乘法),边代表张量。数据流图也可以在许多不同的平台上共享和执行。TensorFlow 提供了一种可视化工具,叫做 TensorBoard,用于可视化数据流图。

TensorFlow 2.0 是 TensorFlow 的最新版本。在接下来的章节中,我们将使用 TensorFlow 2.0 构建深度强化学习模型。然而,理解 TensorFlow 1.x 的工作原理也很重要。所以,首先,我们将学习如何使用 TensorFlow 1.x,然后再深入了解 TensorFlow 2.0。

你可以通过在终端输入以下命令,轻松通过 pip 安装 TensorFlow:

pip install tensorflow==1.13.1 

我们可以通过运行以下简单的 Hello TensorFlow! 程序来检查 TensorFlow 是否安装成功:

import tensorflow as tf
hello = tf.constant("Hello TensorFlow!")
sess = tf.Session()
print(sess.run(hello)) 

前面的程序应该打印出Hello TensorFlow!。如果你遇到任何错误,那么可能是你没有正确安装 TensorFlow。

理解计算图和会话

如我们所学,TensorFlow 中的每一个计算都由一个计算图表示。计算图由多个节点和边组成,其中节点是数学运算,如加法和乘法,边是张量。计算图在资源优化方面非常高效,并且促进了分布式计算。

计算图由多个 TensorFlow 操作组成,排列成一个节点图。

计算图帮助我们理解网络架构,尤其在构建复杂神经网络时。例如,假设我们考虑一个简单的层,h = Relu(WX + b)。其计算图将如下所示:

图 8.1:计算图

计算图中有两种依赖关系,分别称为直接依赖和间接依赖。假设我们有节点b,其输入依赖于节点a的输出;这种依赖关系称为直接依赖,如以下代码所示:

a = tf.multiply(8,5)
b = tf.multiply(a,1) 

当节点 b 的输入不依赖于节点 a 时,这称为间接依赖,如以下代码所示:

a = tf.multiply(8,5)
b = tf.multiply(4,3) 

因此,如果我们能够理解这些依赖关系,就能在可用资源中分配独立的计算,减少计算时间。每当我们导入 TensorFlow 时,默认图会自动创建,我们创建的所有节点都会与该默认图关联。我们还可以创建自己的图,而不是使用默认图,这在构建相互独立的多个模型时非常有用。可以使用tf.Graph()创建一个 TensorFlow 图,如下所示:

graph = tf.Graph()
with graph.as_default():
     z = tf.add(x, y, name='Add') 

如果我们想清除默认图(即清除图中先前定义的变量和操作),则可以使用tf.reset_default_graph()来实现。

会话

如前所述,创建了一个计算图,其节点上包含操作,边缘上连接张量。为了执行该图,我们使用一个 TensorFlow 会话。

可以使用tf.Session()来创建 TensorFlow 会话,如以下代码所示:

sess = tf.Session() 

创建会话后,我们可以使用sess.run()方法来执行我们的计算图。

TensorFlow 中的每一个计算都由一个计算图表示,因此我们需要运行计算图来进行所有计算。也就是说,要在 TensorFlow 中进行任何计算,我们需要创建一个 TensorFlow 会话。

执行以下代码以乘法运算两个数字:

a = tf.multiply(3,3)
print(a) 

与打印 9 不同,前面的代码将打印一个 TensorFlow 对象,Tensor("Mul:0", shape=(), dtype=int32)

如前所述,每当我们导入 TensorFlow 时,默认的计算图会自动创建,所有节点都会附加到该图上。因此,当我们打印 a 时,它只会返回 TensorFlow 对象,因为 a 的值尚未计算,计算图尚未执行。

为了执行计算图,我们需要初始化并运行 TensorFlow 会话,如下所示:

a = tf.multiply(3,3)
with tf.Session as sess:
    print(sess.run(a)) 

前面的代码打印出 9

现在我们已经了解了会话,接下来的部分将学习变量、常量和占位符。

变量、常量和占位符

变量、常量和占位符是 TensorFlow 的基础元素。然而,这三者之间经常会产生混淆。让我们逐一查看每个元素,并了解它们之间的区别。

变量

变量是用来存储值的容器。变量作为输入用于计算图中的多个其他操作。可以使用 tf.Variable() 函数来创建变量,如下列代码所示:

x = tf.Variable(13) 

让我们使用 tf.Variable() 创建一个名为 W 的变量,如下所示:

W = tf.Variable(tf.random_normal([500, 111], stddev=0.35), name="weights") 

正如前面的代码所示,我们通过从标准差为 0.35 的正态分布中随机抽取值来创建一个变量 W

tf.Variable() 中的 name 参数是什么?

它用于设置计算图中变量的名称。所以,在前面的代码中,Python 将变量保存为 W,但在 TensorFlow 图中,它将被保存为 weights

在定义变量后,我们需要初始化计算图中的所有变量。可以使用 tf.global_variables_initializer() 来完成这一步。

一旦创建了会话,我们运行初始化操作,初始化所有已定义的变量,只有在此之后才能运行其他操作,如下列代码所示:

x = tf.Variable(1212)
init = tf.global_variables_initializer()
with tf.Session() as sess:
  sess.run(init) 
  print(sess.run(x)) 

常量

常量与变量不同,常量的值不能改变。也就是说,常量是不可变的。一旦赋值,它们在整个程序中都无法更改。我们可以使用 tf.constant() 创建常量,如下列代码所示:

 x = tf.constant(13) 

占位符和馈送字典

我们可以把占位符看作变量,在其中只定义类型和维度,但不分配值。占位符的值将在运行时输入。我们使用占位符将数据传递给计算图。占位符在定义时没有值。

占位符可以通过 tf.placeholder() 来定义。它有一个可选参数 shape,表示数据的维度。如果 shape 设置为 None,则可以在运行时输入任何大小的数据。占位符可以这样定义:

 x = tf.placeholder("float", shape=None) 

简单来说,我们使用 tf.Variable 来存储数据,而使用 tf.placeholder 来输入外部数据。

为了更好地理解占位符,我们考虑一个简单的例子:

x = tf.placeholder("float", None)
y = x+3
with tf.Session() as sess:
    result = sess.run(y)
    print(result) 

如果我们运行上述代码,它将返回一个错误,因为我们正在尝试计算 y,其中 y = x + 3,而 x 是一个占位符,其值尚未分配。正如我们所学,占位符的值将在运行时分配。我们通过 feed_dict 参数来分配占位符的值。feed_dict 参数实际上是一个字典,其中键代表占位符的名称,值代表占位符的值。

如以下代码所示,我们设置了feed_dict = {x:5},这意味着x占位符的值为5

with tf.Session() as sess:
    result = sess.run(y, feed_dict={x: 5})
    print(result) 

上述代码返回8.0

就是这样。在下一节中,我们将学习 TensorBoard。

引入 TensorBoard

TensorBoard 是 TensorFlow 的可视化工具,可用于可视化计算图。它还可以用来绘制各种定量指标以及几个中间计算的结果。当我们训练一个非常深的神经网络时,如果我们必须调试网络,就会变得非常混乱。因此,如果我们能够在 TensorBoard 中可视化计算图,就可以轻松理解这些复杂的模型、调试它们并优化它们。TensorBoard 还支持共享。

图 8.2所示,TensorBoard 面板由多个标签组成——标量(SCALARS)图像(IMAGES)音频(AUDIO)图形(GRAPHS)分布(DISTRIBUTIONS)直方图(HISTOGRAMS)嵌入(EMBEDDINGS)

图 8.2:TensorBoard

这些标签是相当直观的。SCALARS 标签显示我们程序中使用的标量变量的有用信息。例如,它显示了一个名为 loss 的标量变量在多个迭代过程中如何变化。

GRAPHS 标签显示计算图。DISTRIBUTIONSHISTOGRAMS 标签显示一个变量的分布。例如,我们模型的权重分布和直方图可以在这两个标签下看到。EMBEDDINGS 标签用于可视化高维向量,如词嵌入。

让我们构建一个基本的计算图并在 TensorBoard 中可视化它。假设我们有四个常量,如下所示:

x = tf.constant(1,name='x')
y = tf.constant(1,name='y')
a = tf.constant(3,name='a')
b = tf.constant(3,name='b') 

让我们将 xy 以及 ab 相乘,并将结果分别存储为 prod1prod2,如以下代码所示:

prod1 = tf.multiply(x,y,name='prod1')
prod2 = tf.multiply(a,b,name='prod2') 

prod1prod2 相加,并将结果存储在 sum 中:

sum = tf.add(prod1,prod2,name='sum') 

现在,我们可以在 TensorBoard 中可视化所有这些操作。要在 TensorBoard 中可视化,我们首先需要保存事件文件。这可以通过使用tf.summary.FileWriter()来完成。它有两个重要的参数,logdirgraph

正如其名称所示,logdir 指定了我们要存储图形的目录,而 graph 指定了我们要存储的图形:

with tf.Session() as sess:
    writer = tf.summary.FileWriter(logdir='./graphs',graph=sess.graph)
    print(sess.run(sum)) 

在上述代码中,./graphs 是我们存储事件文件的目录,sess.graph 指定了我们 TensorFlow 会话中的当前图。因此,我们将 TensorFlow 会话的当前图存储在 graphs 目录中。

要启动 TensorBoard,请打开终端,定位到工作目录,并输入以下命令:

tensorboard --logdir=graphs --port=8000 

logdir参数表示事件文件存储的目录,port是端口号。运行上述命令后,打开浏览器并输入http://localhost:8000/

在 TensorBoard 面板下的GRAPHS标签中,你可以看到计算图:

图 8.3:计算图

如你所见,我们定义的所有操作在图中都有清晰的展示。

创建命名范围

范围作用用于减少复杂性,并通过将相关节点分组在一起来帮助我们更好地理解模型。拥有命名空间有助于我们将相似的操作分组在图中。当我们构建复杂架构时,这非常有用。可以使用tf.name_scope()来创建范围。在前面的例子中,我们执行了两个操作,Productsum。我们可以将它们简单地分组到两个不同的命名范围中,分别为Productsum

在上一节中,我们看到prod1prod2执行乘法并计算结果。我们将定义一个名为Product的命名范围,并将prod1prod2操作分组,如下所示:

with tf.name_scope("Product"):
    with tf.name_scope("prod1"):
        prod1 = tf.multiply(x,y,name='prod1')

    with tf.name_scope("prod2"):
        prod2 = tf.multiply(a,b,name='prod2') 

现在,定义sum的命名范围:

with tf.name_scope("sum"):
    sum = tf.add(prod1,prod2,name='sum') 

将文件存储在graphs目录中:

with tf.Session() as sess:
    writer = tf.summary.FileWriter('./graphs', sess.graph)
    print(sess.run(sum)) 

在 TensorBoard 中可视化图形:

tensorboard --logdir=graphs --port=8000 

如你所见,现在我们只有两个节点,sumProduct

图 8.4:计算图

一旦我们双击节点,就可以看到计算是如何进行的。如你所见,prod1prod2节点被分组在Product范围下,它们的结果被传送到sum节点,在那里进行相加。你可以看到prod1prod2节点是如何计算它们的值的:

图 8.5:详细的计算图

上述图仅是一个简单的示例。当我们处理一个有许多操作的复杂项目时,命名范围帮助我们将相似的操作分组在一起,并使我们更好地理解计算图。

现在我们已经了解了 TensorFlow,在接下来的章节中,让我们看看如何使用 TensorFlow 构建手写数字分类。

使用 TensorFlow 进行手写数字分类

将我们迄今为止学到的所有概念结合起来,我们将看到如何使用 TensorFlow 构建一个神经网络来识别手写数字。如果你最近在玩深度学习,那么你一定接触过 MNIST 数据集。它被称为深度学习的hello world。它包含了 55,000 个手写数字数据点(0 到 9)。

在本节中,我们将了解如何使用神经网络识别这些手写数字,并掌握 TensorFlow 和 TensorBoard 的使用。

导入所需的库

作为第一步,让我们导入所有所需的库:

import warnings
warnings.filterwarnings('ignore')
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
tf.logging.set_verbosity(tf.logging.ERROR)
import matplotlib.pyplot as plt
%matplotlib inline 

加载数据集

使用以下代码加载数据集:

mnist = input_data.read_data_sets("data/mnist", one_hot=True) 

在前面的代码中,data/mnist 表示我们存储 MNIST 数据集的位置,而 one_hot=True 表示我们正在对标签进行 one-hot 编码(0 到 9)。

我们将通过执行以下代码查看我们数据中的内容:

print("No of images in training set {}".format(mnist.train.images.shape))
print("No of labels in training set {}".format(mnist.train.labels.shape))
print("No of images in test set {}".format(mnist.test.images.shape))
print("No of labels in test set {}".format(mnist.test.labels.shape))
No of images in training set (55000, 784)
No of labels in training set (55000, 10)
No of images in test set (10000, 784)
No of labels in test set (10000, 10) 

我们的训练集中有 55000 张图像,每张图像的大小是 784,并且我们有 10 个标签,实际上是 0 到 9。同样,我们的测试集中有 10000 张图像。

现在,我们将绘制一个输入图像,以查看它的样子:

img1 = mnist.train.images[0].reshape(28,28)
plt.imshow(img1, cmap='Greys') 

因此,我们的输入图像如下所示:

图 8.6:训练集中数字 7 的图像

定义每一层神经元的数量

我们将构建一个四层神经网络,其中包含三层隐藏层和一层输出层。由于输入图像的大小是 784,我们将 num_input 设置为 784,并且因为我们有 10 个手写数字(0 到 9),我们将输出层设置为 10 个神经元。

我们定义每一层神经元的数量如下:

#number of neurons in input layer
num_input = 784
#num of neurons in hidden layer 1
num_hidden1 = 512
#num of neurons in hidden layer 2
num_hidden2 = 256
#num of neurons in hidden layer 3
num_hidden_3 = 128
#num of neurons in output layer
num_output = 10 

定义占位符

如我们所学,我们首先需要为 inputoutput 定义占位符。占位符的值将在运行时通过 feed_dict 提供:

with tf.name_scope('input'):
    X = tf.placeholder("float", [None, num_input])
with tf.name_scope('output'):
    Y = tf.placeholder("float", [None, num_output]) 

由于我们有一个四层网络,我们有四个权重和四个偏置。我们通过从截断正态分布中抽取值来初始化权重,标准差为 0.1。记住,权重矩阵的维度应该是前一层神经元的数量 x 当前层神经元的数量。例如,权重矩阵 w3 的维度应该是隐藏层 2 中神经元的数量 x 隐藏层 3 中神经元的数量

我们通常会将所有权重定义在一个字典中,如下所示:

with tf.name_scope('weights'):

 weights = {
 'w1': tf.Variable(tf.truncated_normal([num_input, num_hidden1], stddev=0.1),name='weight_1'),
 'w2': tf.Variable(tf.truncated_normal([num_hidden1, num_hidden2], stddev=0.1),name='weight_2'),
 'w3': tf.Variable(tf.truncated_normal([num_hidden2, num_hidden_3], stddev=0.1),name='weight_3'),
 'out': tf.Variable(tf.truncated_normal([num_hidden_3, num_output], stddev=0.1),name='weight_4'),
 } 

偏置的形状应该是当前层神经元的数量。例如,b2 偏置的维度是隐藏层 2 中神经元的数量。我们将偏置值设置为常量;在所有层中都设置为 0.1

with tf.name_scope('biases'):
    biases = {
        'b1': tf.Variable(tf.constant(0.1, shape=[num_hidden1]),name='bias_1'),
        'b2': tf.Variable(tf.constant(0.1, shape=[num_hidden2]),name='bias_2'),
        'b3': tf.Variable(tf.constant(0.1, shape=[num_hidden_3]),name='bias_3'),
        'out': tf.Variable(tf.constant(0.1, shape=[num_output]),name='bias_4')
    } 

前向传播

现在我们将定义前向传播操作。我们将在所有层中使用 ReLU 激活函数。在最后几层,我们将应用 sigmoid 激活函数,如下所示的代码所示:

with tf.name_scope('Model'):

    with tf.name_scope('layer1'):
        layer_1 = tf.nn.relu(tf.add(tf.matmul(X, weights['w1']), biases['b1']) )

    with tf.name_scope('layer2'):
        layer_2 = tf.nn.relu(tf.add(tf.matmul(layer_1, weights['w2']), biases['b2']))

    with tf.name_scope('layer3'):
        layer_3 = tf.nn.relu(tf.add(tf.matmul(layer_2, weights['w3']), biases['b3']))

    with tf.name_scope('output_layer'):
         y_hat = tf.nn.sigmoid(tf.matmul(layer_3, weights['out']) + biases['out']) 

计算损失和反向传播

接下来,我们将定义我们的损失函数。我们将使用 softmax 交叉熵作为我们的损失函数。TensorFlow 提供了 tf.nn.softmax_cross_entropy_with_logits() 函数来计算 softmax 交叉熵损失。它接受两个参数作为输入,logitslabels

  • logits 参数指定我们网络预测的 logits;例如,y_hat

  • labels 参数指定实际的标签;例如,真实标签,Y

我们使用 tf.reduce_mean() 计算 loss 函数的平均值:

with tf.name_scope('Loss'):
        loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=y_hat,labels=Y)) 

现在,我们需要使用反向传播来最小化损失。别担心!我们不需要手动计算所有权重的导数。相反,我们可以使用 TensorFlow 的 optimizer

learning_rate = 1e-4
optimizer = tf.train.AdamOptimizer(learning_rate).minimize(loss) 

计算准确率

我们按照以下方式计算模型的准确率:

  • y_hat参数表示我们模型中每个类别的预测概率。由于我们有10个类别,因此我们会有10个概率。如果在位置7的概率较高,那么意味着我们的网络以较高的概率将输入图像预测为数字7tf.argmax()函数返回最大值的索引。因此,tf.argmax(y_hat,1)给出概率较高的索引。如果在索引7的概率较高,它就返回7

  • Y参数表示实际标签,它们是独热编码的值。也就是说,Y参数除了实际图像的位置以外,其他位置都是 0,而在实际图像的位置上值为1。例如,如果输入图像是7,那么Y在所有索引位置的值都是 0,只有在索引7的位置上值为1。因此,tf.argmax(Y,1)返回7,因为在这个位置上我们有一个高值1

因此,tf.argmax(y_hat,1)给出预测的数字,而tf.argmax(Y,1)给出我们实际的数字。

tf.equal(x, y)函数将xy作为输入,并返回(x == y)元素级别的真值。因此,correct_pred = tf.equal(predicted_digit, actual_digit)在实际数字与预测数字相同的位置为True,而在两者不相同的位置为False。我们使用 TensorFlow 的类型转换操作tf.cast(correct_pred, tf.float32)correct_pred中的布尔值转换为浮动值。在将它们转换为浮动值后,我们使用tf.reduce_mean()求平均值。

因此,tf.reduce_mean(tf.cast(correct_pred, tf.float32))给出我们平均的正确预测:

with tf.name_scope('Accuracy'):

    predicted_digit = tf.argmax(y_hat, 1)
    actual_digit = tf.argmax(Y, 1)

    correct_pred = tf.equal(predicted_digit,actual_digit)
    accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32)) 

创建一个汇总

我们还可以在 TensorBoard 中可视化我们模型的损失和准确度在多次迭代过程中的变化。所以,我们使用tf.summary()来获取变量的汇总。由于损失和准确度是标量变量,我们使用tf.summary.scalar(),如下代码所示:

tf.summary.scalar("Accuracy", accuracy)
tf.summary.scalar("Loss", loss) 

接下来,我们将所有在图中使用的汇总合并,使用tf.summary.merge_all()。我们这样做是因为当我们有许多汇总时,运行和存储它们会变得低效,因此我们在会话中只运行一次,而不是多次运行:

merge_summary = tf.summary.merge_all() 

训练模型

现在,是时候训练我们的模型了。正如我们所学的,首先我们需要初始化所有变量:

init = tf.global_variables_initializer() 

定义批处理大小、迭代次数和学习率,如下所示:

learning_rate = 1e-4
num_iterations = 1000
batch_size = 128 

启动 TensorFlow 会话:

with tf.Session() as sess: 

初始化所有变量:

 sess.run(init) 

保存事件文件:

 summary_writer = tf.summary.FileWriter('./graphs', graph=tf.get_default_graph() 

训练模型若干次迭代:

 for i in range(num_iterations): 

根据批处理大小获取一批数据:

 batch_x, batch_y = mnist.train.next_batch(batch_size) 

训练网络:

 sess.run(optimizer, feed_dict={ X: batch_x, Y: batch_y}) 

打印每 100^(次)迭代的lossaccuracy

 if i % 100 == 0:
            batch_loss, batch_accuracy,summary = sess.run(
                [loss, accuracy, merge_summary],
                feed_dict={X: batch_x, Y: batch_y}
                )
            #store all the summaries    
            summary_writer.add_summary(summary, i)
            print('Iteration: {}, Loss: {}, Accuracy: {}'.format(i,batch_loss,batch_accuracy)) 

正如你从下面的输出中可以注意到的那样,损失在不断减少,准确度在不断增加:

Iteration: 0, Loss: 2.30789709091, Accuracy: 0.1171875
Iteration: 100, Loss: 1.76062202454, Accuracy: 0.859375
Iteration: 200, Loss: 1.60075569153, Accuracy: 0.9375
Iteration: 300, Loss: 1.60388696194, Accuracy: 0.890625
Iteration: 400, Loss: 1.59523034096, Accuracy: 0.921875
Iteration: 500, Loss: 1.58489584923, Accuracy: 0.859375
Iteration: 600, Loss: 1.51407408714, Accuracy: 0.953125
Iteration: 700, Loss: 1.53311181068, Accuracy: 0.9296875
Iteration: 800, Loss: 1.57677125931, Accuracy: 0.875
Iteration: 900, Loss: 1.52060437202, Accuracy: 0.9453125 

在 TensorBoard 中可视化图表

训练后,我们可以在 TensorBoard 中可视化我们的计算图,如图 8.7所示。如你所见,我们的Model接受inputweightsbiases作为输入,并返回输出。我们根据模型的输出计算LossAccuracy。我们通过计算gradients并更新weights来最小化损失:

图 8.7:计算图

如果我们双击并展开Model,我们可以看到该模型有三层隐藏层和一层输出层:

图 8.8:扩展模型节点

同样,我们可以双击并查看每个节点。例如,如果我们打开weights,我们可以看到四个权重是如何通过截断正态分布初始化的,以及它是如何通过 Adam 优化器更新的:

图 8.9:扩展权重节点

正如我们所学,计算图帮助我们理解每个节点发生了什么。

我们可以通过双击Accuracy节点查看准确度是如何计算的:

图 8.10:扩展准确度节点

记得我们还存储了lossaccuracy变量的摘要。我们可以在 TensorBoard 的SCALARS标签下找到它们。图 8.11展示了损失如何随着迭代逐步减少:

图 8.11:损失函数图

图 8.12展示了准确度随着迭代次数的增加而提升:

图 8.12 准确度图

就这样。在下一节中,我们将学习 TensorFlow 中另一个有趣的特性——急切执行模式。

引入急切执行模式

TensorFlow 中的急切执行模式更加符合 Python 编程风格,允许快速原型开发。与图模式不同,在图模式下,我们每次执行操作时都需要构建一个图,而急切执行模式遵循命令式编程范式,任何操作都可以立即执行,无需构建图,就像在 Python 中一样。因此,使用急切执行模式,我们可以告别会话和占位符。它还使得调试过程更加简便,因为会立即出现运行时错误,而不是像图模式那样需要先运行会话。例如,在图模式下,为了计算任何内容,我们必须运行会话。如下面的代码所示,要计算z的值,我们必须运行 TensorFlow 会话:

x = tf.constant(11)
y = tf.constant(11)
z = x*y
with tf.Session() as sess:
    print(sess.run(z)) 

使用急切执行模式时,我们不需要创建会话;我们可以像在 Python 中一样直接计算z。为了启用急切执行模式,只需调用tf.enable_eager_execution()函数:

x = tf.constant(11)
y = tf.constant(11)
z = x*y
print(z) 

它将返回以下内容:

<tf.Tensor: id=789, shape=(), dtype=int32, numpy=121> 

为了获取输出值,我们可以打印以下内容:

z.numpy()
121 

尽管急切执行模式实现了命令式编程范式,但在本书中,我们将以非急切模式来研究大多数示例,以便更好地理解从零开始的算法。在下一节中,我们将学习如何使用 TensorFlow 进行数学运算。

TensorFlow 中的数学运算

现在,我们将使用急切执行模式探索 TensorFlow 中的一些操作:

x = tf.constant([1., 2., 3.])
y = tf.constant([3., 2., 1.]) 

让我们从一些基本的算术运算开始。

使用 tf.add 将两个数字相加:

sum = tf.add(x,y)
sum.numpy()
array([4., 4., 4.], dtype=float32) 

tf.subtract 函数用于计算两个数字之间的差值:

difference = tf.subtract(x,y)
difference.numpy()
array([-2.,  0.,  2.], dtype=float32) 

tf.multiply 函数用于乘法运算两个数:

product = tf.multiply(x,y)
product.numpy()
array([3., 4., 3.], dtype=float32) 

使用 tf.divide 除以两个数字:

division = tf.divide(x,y)
division.numpy()
array([0.33333334, 1\.        , 3\.        ], dtype=float32) 

点积可以按以下方式计算:

dot_product = tf.reduce_sum(tf.multiply(x, y))
dot_product.numpy()
10.0 

接下来,我们来找出最小值和最大值的索引:

x = tf.constant([10, 0, 13, 9]) 

最小值的索引使用 tf.argmin() 计算:

tf.argmin(x).numpy()
1 

最大值的索引使用 tf.argmax() 计算:

tf.argmax(x).numpy()
2 

运行以下代码,找到 xy 之间的平方差:

x = tf.Variable([1,3,5,7,11])
y = tf.Variable([1])
tf.math.squared_difference(x,y).numpy()
[  0,   4,  16,  36, 100] 

让我们尝试类型转换;即,将一种数据类型转换为另一种。

打印 x 的类型:

print(x.dtype)
tf.int32 

我们可以使用 tf.castx 的类型从 tf.int32 转换为 tf.float32,如以下代码所示:

x = tf.cast(x, dtype=tf.float32) 

现在,检查 x 的类型。它将是 tf.float32,如下所示:

print(x.dtype)
tf.float32 

将两个矩阵连接起来:

x = [[3,6,9], [7,7,7]]
y = [[4,5,6], [5,5,5]] 

按行连接矩阵:

tf.concat([x, y], 0).numpy()
array([[3, 6, 9],
       [7, 7, 7],
       [4, 5, 6],
       [5, 5, 5]], dtype=int32) 

使用以下代码按列连接矩阵:

tf.concat([x, y], 1).numpy()
array([[3, 6, 9, 4, 5, 6],
       [7, 7, 7, 5, 5, 5]], dtype=int32) 

使用 stack 函数堆叠 x 矩阵:

tf.stack(x, axis=1).numpy()
array([[3, 7],
       [6, 7],
       [9, 7]], dtype=int32) 

现在,让我们看看如何执行 reduce_mean 操作:

x = tf.Variable([[1.0, 5.0], [2.0, 3.0]])
x.numpy()
array([[1., 5.],
       [2., 3.]] 

计算 x 的平均值;即,(1.0 + 5.0 + 2.0 + 3.0) / 4

tf.reduce_mean(input_tensor=x).numpy()
2.75 

计算行的平均值;即,(1.0+5.0)/2, (2.0+3.0)/2

tf.reduce_mean(input_tensor=x, axis=0).numpy()
array([1.5, 4\. ], dtype=float32) 

计算列的平均值;即,(1.0+5.0)/2.0, (2.0+3.0)/2.0

tf.reduce_mean(input_tensor=x, axis=1, keepdims=True).numpy()
array([[3\. ],
       [2.5]], dtype=float32) 

从概率分布中绘制随机值:

tf.random.normal(shape=(3,2), mean=10.0, stddev=2.0).numpy()
tf.random.uniform(shape = (3,2), minval=0, maxval=None, dtype=tf.float32,).numpy() 

计算 softmax 概率:

x = tf.constant([7., 2., 5.])
tf.nn.softmax(x).numpy()
array([0.8756006 , 0.00589975, 0.11849965], dtype=float32) 

现在,我们来看看如何计算梯度。

定义 square 函数:

def square(x):
  return tf.multiply(x, x) 

使用 tf.GradientTape 可以计算前面 square 函数的梯度,如下所示:

with tf.GradientTape(persistent=True) as tape:
     print(square(6.).numpy())
36.0 

TensorFlow 2.0 和 Keras

TensorFlow 2.0 具有一些非常酷的特性。默认启用了急切执行模式。它提供了简化的工作流程,并使用 Keras 作为构建深度学习模型的主要 API。它也向后兼容 TensorFlow 1.x 版本。

要安装 TensorFlow 2.0,请打开终端并输入以下命令:

pip install tensorflow==2.0.0-alpha0 

由于 TensorFlow 2.0 使用 Keras 作为高层 API,我们将在下一节中讨论 Keras 的工作原理。

Bonjour Keras

Keras 是另一个广泛使用的深度学习库。它由 Google 的 François Chollet 开发。以其快速原型设计著称,它使得模型构建变得简单。它是一个高级库,这意味着它不会自己执行任何低级操作,比如卷积。它使用一个后端引擎来完成这些操作,如 TensorFlow。Keras API 可在 tf.keras 中找到,TensorFlow 2.0 使用它作为主要 API。

在 Keras 中构建模型涉及四个重要步骤:

  1. 定义模型

  2. 编译模型

  3. 拟合模型

  4. 评估模型

定义模型

第一步是定义模型。Keras 提供了两种不同的 API 来定义模型:

  • 顺序 API

  • 函数式 API

定义顺序模型

在一个顺序模型中,我们将每一层堆叠在一起:

from keras.models import Sequential
from keras.layers import Dense 

首先,让我们将模型定义为Sequential()模型,如下所示:

model = Sequential() 

现在,定义第一个层,如下所示:

model.add(Dense(13, input_dim=7, activation='relu')) 

在前面的代码中,Dense表示一个全连接层,input_dim表示输入的维度,activation指定了我们使用的激活函数。我们可以堆叠任意数量的层,一个接一个。

定义下一个使用relu激活函数的层,如下所示:

model.add(Dense(7, activation='relu')) 

定义使用sigmoid激活函数的输出层:

model.add(Dense(1, activation='sigmoid')) 

顺序模型的最终代码块如下所示。正如你所看到的,Keras 代码比 TensorFlow 代码要简单得多:

model = Sequential()
model.add(Dense(13, input_dim=7, activation='relu'))
model.add(Dense(7, activation='relu'))
model.add(Dense(1, activation='sigmoid')) 

定义一个功能模型

功能模型比顺序模型提供了更多的灵活性。例如,在功能模型中,我们可以轻松地将任何一层连接到另一层,而在顺序模型中,每一层都是按顺序堆叠的。功能模型在创建复杂模型时非常有用,例如有向无环图、具有多个输入值、多个输出值和共享层的模型。现在,我们将看到如何在 Keras 中定义功能模型。

第一步是定义输入维度:

input = Input(shape=(2,)) 

现在,我们将使用Dense类定义第一个具有10个神经元并使用relu激活的全连接层,如下所示:

layer1 = Dense(10, activation='relu') 

我们定义了layer1,但是layer1的输入来自哪里?我们需要在末尾使用括号表示法指定layer1的输入,如下所示:

layer1 = Dense(10, activation='relu')(input) 

我们定义下一个层layer2,具有13个神经元和relu激活。layer2的输入来自layer1,因此在末尾的括号中添加,如以下代码所示:

layer2 = Dense(10, activation='relu')(layer1) 

现在,我们可以使用sigmoid激活函数定义输出层。输出层的输入来自layer2,因此这将在末尾的括号中添加:

output = Dense(1, activation='sigmoid')(layer2) 

定义完所有层后,我们使用Model类定义模型,在这里我们需要指定inputsoutputs,如以下代码所示:

model = Model(inputs=input, outputs=output) 

完整的功能模型代码如下所示:

input = Input(shape=(2,))
layer1 = Dense(10, activation='relu')(input)
layer2 = Dense(10, activation='relu')(layer1)
output = Dense(1, activation='sigmoid')(layer2)
model = Model(inputs=input, outputs=output) 

编译模型

现在我们已经定义了模型,下一步是编译它。在这个阶段,我们设置模型的学习方式。编译模型时,我们需要定义三个参数:

  • optimizer参数:这定义了我们想要使用的优化算法;例如,梯度下降法。

  • loss参数:这是我们试图最小化的目标函数;例如,均方误差或交叉熵损失。

  • metrics参数:这是我们用来评估模型性能的度量指标;例如,accuracy。我们也可以指定多个度量指标。

运行以下代码来编译模型:

model.compile(loss='binary_crossentropy', optimizer='sgd', metrics=['accuracy']) 

训练模型

我们已经定义并编译了模型。现在,我们将训练模型。训练模型可以通过 fit 函数完成。我们需要指定特征 x、标签 y、训练轮数 epochsbatch_size,如下所示:

model.fit(x=data, y=labels, epochs=100, batch_size=10) 

评估模型

在训练模型后,我们将在测试集上评估模型:

model.evaluate(x=data_test,y=labels_test) 

我们还可以在同一个训练集上评估模型,这将帮助我们理解训练的准确性:

model.evaluate(x=data,y=labels) 

就是这样。接下来我们将在下一节中学习如何使用 TensorFlow 进行 MNIST 数字分类任务。

使用 TensorFlow 2.0 进行 MNIST 数字分类

现在,我们将看到如何使用 TensorFlow 2.0 进行 MNIST 手写数字分类。与 TensorFlow 1.x 相比,它只需要几行代码。正如我们所学,TensorFlow 2.0 使用 Keras 作为其高级 API,我们只需要在 Keras 代码中添加 tf.keras

让我们从加载数据集开始:

mnist = tf.keras.datasets.mnist 

使用以下代码创建训练集和测试集:

(x_train,y_train), (x_test, y_test) = mnist.load_data() 

通过将 x 的值除以 x 的最大值,即 255.0,来归一化训练集和测试集:

x_train, x_test = tf.cast(x_train/255.0, tf.float32), tf.cast(x_test/255.0, tf.float32)
y_train, y_test = tf.cast(y_train,tf.int64),tf.cast(y_test,tf.int64) 

按如下定义顺序模型:

model = tf.keras.models.Sequential() 

现在,让我们给模型添加层。我们使用一个三层的网络,隐藏层使用 ReLU 函数,最后一层使用 softmax:

model.add(tf.keras.layers.Flatten())
model.add(tf.keras.layers.Dense(256, activation="relu"))
model.add(tf.keras.layers.Dense(128, activation="relu"))
model.add(tf.keras.layers.Dense(10, activation="softmax")) 

通过运行以下代码行来编译模型:

model.compile(optimizer='sgd', loss='sparse_categorical_crossentropy', metrics=['accuracy']) 

训练模型:

model.fit(x_train, y_train, batch_size=32, epochs=10) 

评估模型:

model.evaluate(x_test, y_test) 

就这样!使用 Keras API 编写代码就是这么简单。

总结

我们通过了解 TensorFlow 以及它如何使用计算图开始了本章的学习。我们了解到,TensorFlow 中的计算通过计算图表示,计算图由多个节点和边组成,其中节点是数学运算,例如加法和乘法,边则是张量。

接下来,我们了解了变量是用于存储值的容器,它们作为输入用于计算图中的多个其他操作。我们还了解到占位符类似于变量,它们仅定义类型和维度,但不赋值,值将在运行时提供。接下来,我们了解了 TensorBoard,这是 TensorFlow 的可视化工具,可以用来可视化计算图。我们还探索了急切执行,它更加符合 Python 风格,并支持快速原型开发。

我们理解到,与图模式不同,图模式下每次执行操作时都需要构建图,而急切执行(eager execution)遵循命令式编程范式,任何操作都可以立即执行,无需构建图,就像我们在 Python 中做的那样。

在下一章中,我们将通过理解一种流行的深度强化学习算法——深度 Q 网络DQN),开始我们的深度强化学习DRL)之旅。

问题

让我们通过回答以下问题来检验我们对 TensorFlow 的理解:

  1. 什么是 TensorFlow 会话?

  2. 定义一个占位符。

  3. 什么是 TensorBoard?

  4. 为什么急切执行模式有用?

  5. 使用 Keras 构建模型的所有步骤有哪些?

  6. Keras 的函数式模型与其顺序模型有何不同?

进一步阅读

你可以通过查看其官方文档来了解更多关于 TensorFlow 的信息,链接:www.tensorflow.org/tutorials

第九章:深度 Q 网络及其变体

在本章中,我们将从一种非常流行的深度强化学习DRL)算法——深度 Q 网络DQN)开始。理解 DQN 非常重要,因为许多最先进的 DRL 算法都是基于 DQN 的。DQN 算法首次由 Google 的 DeepMind 研究人员于 2013 年在论文《通过深度强化学习玩 Atari 游戏》中提出。他们描述了 DQN 架构,并解释了为什么它能以接近人类水平的准确度玩 Atari 游戏。本章首先将介绍深度 Q 网络到底是什么,以及它如何在强化学习中使用。接下来,我们将深入探讨 DQN 的算法。然后,我们将学习如何实现 DQN 来玩 Atari 游戏。

在了解了 DQN 后,我们将详细讲解几种 DQN 的变体,如双重 DQN、带优先经验重放的 DQN、对战 DQN 和深度递归 Q 网络。

本章将涵盖以下主题:

  • 什么是 DQN?

  • DQN 算法

  • 使用 DQN 玩 Atari 游戏

  • 双重 DQN

  • 带优先经验重放的 DQN

  • 对战 DQN

  • 深度递归 Q 网络

什么是 DQN?

强化学习的目标是找到最优策略,即给我们带来最大回报(本回合奖励总和)的策略。为了计算策略,首先我们需要计算 Q 函数。一旦我们得到了 Q 函数,就可以通过在每个状态中选择具有最大 Q 值的动作来提取策略。例如,假设我们有两个状态 AB,并且我们的动作空间由两个动作组成;假设这两个动作是 。因此,为了找出在状态 AB 中应该执行哪个动作,我们首先计算所有状态-动作对的 Q 值,如 表 9.1 所示:

表 9.1:状态-动作对的 Q 值

一旦我们获得了所有状态-动作对的 Q 值,我们将在每个状态中选择具有最大 Q 值的动作。因此,我们在状态 A 中选择动作 ,在状态 B 中选择动作 ,因为它们具有最大 Q 值。我们在每次迭代中改进 Q 函数,一旦获得了最优 Q 函数,就可以从中提取最优策略。

现在,让我们重新审视我们的网格世界环境,如 图 9.1 所示:

图 9.1:网格世界环境

我们了解到,在网格世界环境中,智能体的目标是从状态 A 到达状态 I,并且不能经过阴影状态,在每个状态中,智能体必须执行四个动作之一——

要计算策略,首先我们计算所有状态-动作对的 Q 值。这里,状态的数量是 9(AI),我们的动作空间中有 4 个动作,因此我们的 Q 表将包含 9 x 4 = 36 行,包含所有可能的状态-动作对的 Q 值。一旦我们获得 Q 值,我们就可以通过选择每个状态下具有最大 Q 值的动作来提取策略。但是,全面计算所有状态-动作对的 Q 值是否是一种好的方法呢?让我们更详细地探讨一下这个问题。

假设我们有一个环境,其中有 1,000 个状态,每个状态下有 50 个可能的动作。在这种情况下,我们的 Q 表将包含 1,000 x 50 = 50,000 行,包含所有可能的状态-动作对的 Q 值。在这种环境中,状态和动作数量非常大时,全面计算所有可能的状态-动作对的 Q 值将非常昂贵。

不采用这种方式计算 Q 值,我们能否使用任何函数逼近器来近似它们,例如神经网络?当然可以!我们可以用参数 来对 Q 函数进行参数化,并计算 Q 值,其中参数 只是我们神经网络的参数。因此,我们只需将环境的状态输入到神经网络中,它就会返回该状态下所有可能动作的 Q 值。一旦我们获得 Q 值,就可以选择 Q 值最大的动作作为最优动作。

例如,我们考虑我们的网格世界环境。如图 9.2所示,我们只需将状态D作为输入传递给网络,它就会返回状态D下所有动作的 Q 值,分别是向上向下向左向右。然后,我们选择具有最大 Q 值的动作。由于动作向右的 Q 值最大,我们在状态D下选择动作向右

图 9.2:深度 Q 网络

由于我们使用神经网络来近似 Q 值,因此神经网络被称为 Q 网络,如果我们使用深度神经网络来近似 Q 值,那么这个深度神经网络就被称为深度 Q 网络DQN)。

我们可以用 来表示我们的 Q 函数,其中下标中的参数 表示我们的 Q 函数由 参数化,且 只是我们神经网络的参数。

我们将网络参数 初始化为随机值,并近似 Q 函数(Q 值),但是由于我们将 初始化为随机值,近似的 Q 函数将不是最优的。因此,我们通过多次迭代训练网络,找到最优的参数 。一旦找到最优的 ,我们就得到了最优的 Q 函数。然后我们可以从最优的 Q 函数中提取最优策略。

好的,但我们怎么训练我们的网络呢?训练数据和损失函数是什么?是分类任务还是回归任务?现在我们基本理解了 DQN 的工作原理,接下来的部分,我们将深入细节并解答所有这些问题。

理解 DQN

在本节中,我们将了解 DQN 是如何工作的。我们知道,使用 DQN 来近似给定输入状态下所有动作的 Q 值。Q 值只是一个连续的数字,因此我们本质上是使用 DQN 执行一个回归任务。

好的,那么训练数据呢?我们使用一个叫做重放缓冲区的缓冲区来收集智能体的经验,并根据这些经验来训练我们的网络。让我们详细探讨重放缓冲区。

重放缓冲区

我们知道,智能体通过执行某个动作 a 从状态 s 转移到下一个状态 ,然后获得奖励 r。我们可以将这个转换信息 保存到一个叫做重放缓冲区或经验回放的缓冲区中。重放缓冲区通常用 表示。这个转换信息基本上就是智能体的经验。我们将在多个回合中将智能体的经验存储在重放缓冲区中。使用重放缓冲区存储智能体经验的关键思想是,我们可以通过从缓冲区中采样经验(转换)来训练我们的 DQN。一个重放缓冲区如下所示:

图 9.3:重放缓冲区

以下步骤帮助我们理解如何将转换信息保存在重放缓冲区中

  1. 初始化重放缓冲区

  2. 对于每一轮执行 步骤 3

  3. 对于回合中的每一步:

    1. 执行一次转换,即在状态 s 下执行一个动作 a,移动到下一个状态 ,并获得奖励 r

    2. 将转换信息 存储到重放缓冲区 中。

如前面步骤所解释的,我们在多轮中收集智能体的转换信息,并将其保存在重放缓冲区中。为了更清楚地理解这一点,假设我们有以下两个回合/轨迹:

回合 1

图 9.4:轨迹 1

回合 2

图 9.5:轨迹 2

现在,这些信息将存储在重放缓冲区中,正如 图 9.6 所示:

图 9.6:重放缓冲区

图 9.6所示,我们通过将过渡信息按顺序堆叠来存储它。我们通过从重放缓冲区中采样一个小批量的过渡来训练网络。等等!这里有一个小问题。由于我们将智能体的经验(过渡)一个接一个地按顺序堆叠,智能体的经验将高度相关。例如,如前图所示,过渡将与上下行的行相关。如果我们用这些相关的经验来训练网络,那么我们的神经网络将很容易出现过拟合。因此,为了应对这一问题,我们从重放缓冲区中随机采样一个小批量的过渡来训练网络。

请注意,重放缓冲区的大小是有限的,也就是说,重放缓冲区只会存储固定量的智能体经验。因此,当缓冲区满时,我们会用新经验替换旧经验。重放缓冲区通常实现为队列结构(先进先出)而不是列表。因此,如果缓冲区已满,当新经验进入时,我们会移除旧经验并将新经验添加到缓冲区中。

我们已经知道,我们通过从缓冲区随机采样一个小批量的经验来训练网络。那么,训练到底是如何进行的呢?我们的网络是如何利用这个小批量的样本来近似最优 Q 函数的呢?这正是我们在下一节中讨论的内容。

损失函数

我们已经了解到,在 DQN 中,我们的目标是预测 Q 值,这只是一个连续值。因此,在 DQN 中,我们基本上执行的是一个回归任务。我们通常使用均方误差MSE)作为回归任务的损失函数。MSE 可以定义为目标值与预测值之间的平均平方差,如下所示:

其中y是目标值,!是预测值,K是训练样本的数量。

现在,让我们学习如何在 DQN 中使用 MSE 并训练网络。我们可以通过最小化目标 Q 值和预测 Q 值之间的 MSE 来训练网络。首先,我们如何获得目标 Q 值呢?我们的目标 Q 值应该是最优 Q 值,这样我们就可以通过最小化最优 Q 值和预测 Q 值之间的误差来训练网络。但是我们如何计算最优 Q 值呢?这就是贝尔曼方程帮助我们的地方。在第三章贝尔曼方程与动态规划中,我们了解到最优 Q 值可以通过贝尔曼最优性方程来获得:

其中!表示在执行动作a时,我们在状态s下获得的即时奖励r,并移动到下一个状态!,因此我们可以将!表示为r

在上述方程中,我们可以去除期望值。我们将通过从重放缓冲区随机采样 K 个转移,并取平均值来近似期望值;稍后我们将学习更多内容。

因此,根据 Bellman 最优方程,最优 Q 值就是奖励与下一个状态-动作对的折扣最大 Q 值之和,即:

因此,我们可以将损失定义为目标值(最优 Q 值)与预测值(DQN 预测的 Q 值)之间的差异,并将损失函数 L 表示为:

将方程 (1) 代入上述方程,我们可以写出:

我们知道我们使用由 参数化的网络来计算预测的 Q 值。那么我们如何计算目标值呢?也就是说,我们已经学到,目标值是奖励与下一个状态-动作对的折扣最大 Q 值之和。我们如何计算下一个状态-动作对的 Q 值呢?

类似于预测的 Q 值,我们可以使用相同的 DQN 参数化来计算目标中下一个状态-动作对的 Q 值!。因此,我们可以将损失函数重写为:

如图所示,目标值和预测的 Q 值都由 参数化。

我们不再仅仅计算目标 Q 值和预测 Q 值之间的差异来计算损失,而是使用均方误差(MSE)作为我们的损失函数。我们已经学到,我们将智能体的经验存储在一个叫做重放缓冲区的缓冲区中。因此,我们从重放缓冲区随机采样一个 K 数量的转移小批量!,并通过最小化 MSE 来训练网络,如下所示:

图 9.7:DQN 的损失函数

因此,我们的损失函数可以表示为:

为了简化符号表示,我们可以将目标值表示为 y,并将上述方程重写为:

其中 。我们已经学到,目标值就是奖励与下一个状态-动作对的折扣最大 Q 值之和。但是如果下一个状态 是终止状态呢?如果下一个状态 是终止状态,则我们无法计算 Q 值,因为我们在终止状态中不进行任何动作,因此在这种情况下,目标值将只是奖励,如下所示:

因此,我们的损失函数表示为:

我们通过最小化损失函数来训练我们的网络。我们可以通过寻找最优参数 来最小化损失函数。因此,我们使用梯度下降法来寻找最优参数 。我们计算损失函数的梯度 ,并按如下方式更新我们的网络参数

目标网络

在上一节中,我们学到了通过最小化损失函数来训练网络,该损失函数是目标值与预测值之间的均方误差(MSE),如下所示:

然而,我们的损失函数存在一个小问题。我们已经了解到,目标值只是奖励和下一个状态-动作对的折扣最大 Q 值之和。我们通过相同的网络参数来计算下一个状态-动作对的 Q 值,这些参数由 表示,如下所示:

问题是,由于目标值和预测值都依赖于相同的参数 ,这将导致 MSE 的不稳定,并且网络学习效果较差。这还会导致训练过程中出现大量的发散。

让我们通过一个简单的例子来理解这一点。我们将使用任意数字来使其更容易理解。我们知道我们试图最小化目标值与预测值之间的差异。因此,在每次迭代中,我们计算损失的梯度并更新我们的网络参数 ,以使预测值与目标值相同。

假设在第一次迭代中,目标值为 13,预测值为 11。那么我们更新参数 ,使预测值与目标值 13 匹配。但是在下一次迭代中,目标值变为 15,而预测值变为 13,因为我们更新了网络参数 。因此,我们再次更新参数 ,使预测值与目标值 15 匹配。但是在下一次迭代中,目标值变为 17,而预测值变为 15,因为我们更新了网络参数

正如表 9.2所示,在每次迭代中,预测值都会尽量与目标值相同,而目标值则不断变化:

表 9.2:目标值和预测值

这是因为预测值和目标值都依赖于相同的参数 。如果我们更新 ,那么目标值和预测值都会发生变化。因此,预测值不断试图与目标值相同,但由于网络参数 的更新,目标值也在不断变化。

我们如何避免这种情况?我们能否冻结目标值一段时间,只计算预测值,使得我们的预测值与目标值匹配?可以!为此,我们引入了另一个神经网络,称为目标网络,用于计算下一个状态-动作对的 Q 值。目标网络的参数用 表示。因此,我们的主深度 Q 网络用于预测 Q 值,并通过梯度下降学习最佳参数 。目标网络会冻结一段时间,然后通过复制主深度 Q 网络参数 来更新目标网络参数 。冻结目标网络一段时间后,再通过主网络参数 更新目标网络参数 ,有助于稳定训练过程。

所以,现在我们的损失函数可以重写为:

因此,下一个状态-动作对的 Q 值由目标网络通过参数 计算,预测的 Q 值由我们的主网络通过参数 计算:

为了简化符号,我们可以用 y 来表示我们的目标值,并将前面的方程重写为:

其中

我们已经学习了与 DQN 相关的几个概念,包括经验回放、损失函数和目标网络。在下一节中,我们将把这些概念结合起来,看看 DQN 是如何工作的。

将所有内容放在一起

首先,我们使用随机值初始化主网络参数 。我们了解到目标网络参数只是主网络的副本。因此,我们通过复制主网络参数 来初始化目标网络参数 。我们还初始化了回放缓冲区

现在,在每一集的每一步中,我们将环境的状态输入到网络中,它输出该状态下所有可能动作的 Q 值。然后,我们选择具有最大 Q 值的动作:

如果我们只选择具有最高 Q 值的动作,那么我们将不会探索任何新的动作。因此,为了避免这种情况,我们使用 epsilon-greedy 策略选择动作。在 epsilon-greedy 策略下,我们以概率 epsilon 选择一个随机动作,以概率 1-epsilon 选择具有最大 Q 值的最佳动作。

请注意,由于我们用随机值初始化了网络参数 ,因此通过选择最大 Q 值来选择的动作并不是最优动作。但这没关系,我们只需执行选定的动作,进入下一个状态并获得奖励。如果动作是好的,我们将获得正奖励;如果动作不好,奖励则为负。我们将所有这些过渡信息 存储在重放缓冲区 中。

接下来,我们随机从重放缓冲区中采样一个K的过渡小批量并计算损失。我们已经了解到,损失函数是这样计算的:

其中 y[i] 是目标值,即

在初期迭代中,由于我们的网络参数 只是随机值,所以损失会非常高。为了最小化损失,我们计算损失的梯度并按以下方式更新网络参数

我们并不是在每一个时间步骤都更新目标网络参数 。我们将目标网络参数 冻结若干个时间步骤,然后将主网络参数 复制到目标网络参数

我们会继续重复前面的步骤,进行若干回合,以逼近最优 Q 值。一旦我们得到了最优 Q 值,就可以从中提取最优策略。为了让我们更详细地理解,DQN 算法将在下一节给出。

DQN 算法

DQN 算法的步骤如下:

  1. 使用随机值初始化主网络参数

  2. 通过复制主网络参数 初始化目标网络参数

  3. 初始化重放缓冲区

  4. 对于N个回合,执行第 5 步

  5. 对于回合中的每一步,即对于 t = 0, . . ., T-1:

    1. 观察状态 s 并使用ε-贪婪策略选择一个动作,即以概率 epsilon 选择随机动作 a,以概率 1-epsilon 选择动作

    2. 执行选定的动作并进入下一个状态 ,获得奖励 r

    3. 将过渡信息存储在重放缓冲区中

    4. 随机从重放缓冲区中采样一个K的过渡小批量

    5. 计算目标值,即

    6. 计算损失,

    7. 计算损失的梯度并使用梯度下降法更新主网络参数

    8. 冻结目标网络参数 若干时间步,然后通过复制主网络参数 来更新目标网络。

现在我们已经理解了 DQN 的工作原理,在接下来的部分,我们将学习如何实现它。

使用 DQN 玩 Atari 游戏

Atari 2600 是一款由游戏公司 Atari 推出的流行视频游戏主机。Atari 游戏主机提供了多个受欢迎的游戏,如 Pong、Space Invaders、Ms. Pac-Man、Breakout、Centipede 等等。在本节中,我们将学习如何构建一个 DQN 来玩 Atari 游戏。首先,让我们探讨一下用于玩 Atari 游戏的 DQN 架构。

DQN 的架构

在 Atari 环境中,游戏画面的图像就是环境的状态。因此,我们只需将游戏画面的图像作为输入传递给 DQN,它会返回该状态下所有动作的 Q 值。由于我们处理的是图像,因此,我们可以使用卷积神经网络(CNN)来近似 Q 值,因为它在处理图像时非常有效。

因此,现在我们的 DQN 是一个 CNN。我们将游戏画面(游戏状态)的图像作为输入传递给 CNN,CNN 输出该状态下所有动作的 Q 值。

图 9.8所示,给定游戏画面的图像,卷积层从图像中提取特征并生成特征图。接下来,我们将特征图展平,并将展平后的特征图作为输入传递给前馈网络。前馈网络将这个展平后的特征图作为输入,并返回该状态下所有动作的 Q 值:

图 9.8:DQN 的架构

注意,我们没有执行池化操作。池化操作通常在进行目标检测、图像分类等任务时使用,在这些任务中,我们不考虑图像中物体的位置,只需要知道目标物体是否存在于图像中。例如,如果我们想要识别图像中是否有一只狗,我们只看图像中是否有狗,而不检查狗的位置。因此,在这种情况下,池化操作用来识别图像中是否有狗,而不管狗的位置。

但在我们的设置中,应该避免执行池化操作,因为要理解当前的游戏状态,位置非常重要。例如,在 Pong 游戏中,我们不仅仅想知道游戏画面上是否有球。我们希望知道球的位置,以便做出更好的操作。因此,我们没有在 DQN 架构中包含池化操作。

现在我们已经了解了用于玩 Atari 游戏的 DQN 架构,接下来的部分我们将开始实现它。

动手实践 DQN

让我们实现一个 DQN 来玩 Ms Pacman 游戏。首先,导入必要的库:

import random
import gym
import numpy as np
from collections import deque
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Activation, Flatten, Conv2D, MaxPooling2D
from tensorflow.keras.optimizers import Adam 

现在,让我们使用 Gym 创建 Ms Pacman 游戏环境:

env = gym.make("MsPacman-v0") 

设置状态大小:

state_size = (88, 80, 1) 

获取动作数量:

action_size = env.action_space.n 

预处理游戏屏幕

我们了解到,我们将游戏状态(游戏屏幕的图像)作为输入传入 DQN,它是一个 CNN,输出该状态下所有动作的 Q 值。然而,直接输入原始的游戏屏幕图像效率不高,因为原始游戏屏幕的大小为 210 x 160 x 3,这将非常耗费计算资源。

为了避免这种情况,我们对游戏屏幕进行预处理,然后将预处理后的游戏屏幕输入到 DQN。首先,我们裁剪并调整游戏屏幕图像的大小,将图像转换为灰度图,进行归一化处理,然后将图像调整为 88 x 80 x 1。接着,我们将该预处理后的游戏屏幕图像作为输入,传入 CNN,它将返回 Q 值。

现在,让我们定义一个名为preprocess_state的函数,它接收游戏状态(游戏屏幕图像)作为输入,并返回预处理后的游戏状态:

color = np.array([210, 164, 74]).mean()
def preprocess_state(state): 

裁剪并调整图像大小:

 image = state[1:176:2, ::2] 

将图像转换为灰度图:

 image = image.mean(axis=2) 

提高图像对比度:

 image[image==color] = 0 

对图像进行归一化处理:

 image = (image - 128) / 128 - 1 

重新调整图像并返回:

 image = np.expand_dims(image.reshape(88, 80, 1), axis=0)
    return image 

定义 DQN 类

让我们定义一个名为 DQN 的类,在这里我们将实现 DQN 算法。为了更清晰地理解,我们逐行查看代码。你也可以从本书的 GitHub 仓库中获取完整代码:

class DQN: 

定义 init 方法

首先,让我们定义init方法

 def __init__(self, state_size, action_size): 

定义状态大小:

 self.state_size = state_size 

定义动作大小:

 self.action_size = action_size 

定义重放缓冲区:

 self.replay_buffer = deque(maxlen=5000) 

定义折扣因子:

 self.gamma = 0.9 

定义ε值:

 self.epsilon = 0.8 

定义我们希望更新目标网络的更新率:

 self.update_rate = 1000 

定义主网络:

 self.main_network = self.build_network() 

定义目标网络:

 self.target_network = self.build_network() 

将主网络的权重复制到目标网络:

 self.target_network.set_weights(self.main_network.get_weights()) 

构建 DQN

现在,让我们构建 DQN。我们已经了解到,玩 Atari 游戏时,使用 CNN 作为 DQN,将游戏屏幕的图像作为输入并返回 Q 值。我们定义了一个包含三层卷积层的 DQN。卷积层从图像中提取特征并输出特征图,然后我们将卷积层获得的特征图展平,并将展平后的特征图输入到前馈网络(即全连接层),该网络返回 Q 值:

 def build_network(self): 

定义第一个卷积层:

 model = Sequential()
        model.add(Conv2D(32, (8, 8), strides=4, padding='same', input_shape=self.state_size))
        model.add(Activation('relu')) 

定义第二个卷积层:

 model.add(Conv2D(64, (4, 4), strides=2, padding='same'))
        model.add(Activation('relu')) 

定义第三个卷积层:

 model.add(Conv2D(64, (3, 3), strides=1, padding='same'))
        model.add(Activation('relu')) 

展平通过第三个卷积层获得的特征图:

 model.add(Flatten()) 

将展平的特征图输入到全连接层:

 model.add(Dense(512, activation='relu'))
        model.add(Dense(self.action_size, activation='linear')) 

使用均方误差(MSE)编译模型:

 model.compile(loss='mse', optimizer=Adam()) 

返回模型:

 return model 

存储转换

我们已经了解到,通过从重放缓冲区随机采样一个小批量的转换数据来训练 DQN。因此,我们定义一个名为store_transition的函数,用于将转换信息存储在重放缓冲区:

 def store_transistion(self, state, action,
                          reward, next_state, done):
        self.replay_buffer.append((state, action,
                                   reward, next_state, done)) 

定义ε-贪婪策略

我们了解到,在 DQN 中,为了平衡探索与利用之间的权衡,我们使用 epsilon-greedy 策略来选择动作。所以,现在我们定义一个名为 epsilon_greedy 的函数,通过 epsilon-greedy 策略来选择动作:

 def epsilon_greedy(self, state):
        if random.uniform(0,1) < self.epsilon:
            return np.random.randint(self.action_size)
        Q_values = self.main_network.predict(state)
        return np.argmax(Q_values[0]) 

定义训练过程

现在,我们定义一个名为 train 的函数来进行网络训练:

 def train(self, batch_size): 

从回放缓冲区中采样一个小批量转移:

 minibatch = random.sample(self.replay_buffer, batch_size) 

使用目标网络计算目标值:

 for state, action, reward, next_state, done in minibatch:
            if not done:
                target_Q = (reward + self.gamma * np.amax(
                    self.target_network.predict(next_state)))
            else:
                target_Q = reward 

使用主网络计算预测值,并将预测值存储在 Q_values 中:

 Q_values = self.main_network.predict(state) 

更新目标值:

 Q_values[0][action] = target_Q 

训练主网络:

 self.main_network.fit(state, Q_values, epochs=1, 
                                  verbose=0) 

更新目标网络

现在,我们定义一个名为 update_target_network 的函数,通过从主网络复制来更新目标网络的权重:

 def update_target_network(self):
        self.target_network.set_weights(self.main_network.get_weights()) 

训练 DQN

现在,让我们开始训练网络。首先,设置我们希望训练网络的回合数:

num_episodes = 500 

定义时间步数:

num_timesteps = 20000 

定义批次大小:

batch_size = 8 

设置我们希望考虑的过去游戏画面数量:

num_screens = 4 

实例化 DQN 类:

dqn = DQN(state_size, action_size) 

将 done 设置为 False

done = False 

初始化 time_step

time_step = 0 

对于每个回合:

for i in range(num_episodes): 

Return 设置为 0

 Return = 0 

预处理游戏画面:

 state = preprocess_state(env.reset()) 

每个回合的每一步:

 for t in range(num_timesteps): 

渲染环境:

 env.render() 

更新时间步:

 time_step += 1 

更新目标网络:

 if time_step % dqn.update_rate == 0:
            dqn.update_target_network() 

选择动作:

 action = dqn.epsilon_greedy(state) 

执行动作:

 next_state, reward, done, _ = env.step(action) 

预处理下一个状态:

 next_state = preprocess_state(next_state) 

存储转移信息:

 dqn.store_transistion(state, action, reward, next_state, done) 

将当前状态更新为下一个状态:

 state = next_state 

更新返回值:

 Return += reward 

如果回合结束,则打印返回值:

 if done:
            print('Episode: ',i, ',' 'Return', Return)
            break 

如果回放缓冲区中的转移数量大于批次大小,则训练网络:

 if len(dqn.replay_buffer) > batch_size:
            dqn.train(batch_size) 

通过渲染环境,我们还可以观察到智能体如何通过一系列回合来学习玩游戏:

图 9.9:DQN 智能体学习玩游戏

现在,我们已经了解了 DQN 如何工作,以及如何构建一个 DQN 来玩 Atari 游戏,在下一节中,我们将学习一种 DQN 的有趣变体,称为双重 DQN。

双重 DQN

我们已经了解到,在 DQN 中,目标值是这样计算的:

DQN 的一个问题是它往往会高估目标中下一个状态-动作对的 Q 值:

这种高估是由于最大值操作符的存在。让我们通过一个例子来看看这种高估是如何发生的。假设我们处于状态 ,并且我们有三个动作 a[1]、a[2] 和 a[3]。假设 a[3] 是状态 下的最优动作。当我们估计状态 中所有动作的 Q 值时,估计的 Q 值会有一些噪声,并与实际值不同。比如,由于噪声,动作 a[2] 的 Q 值会比最优动作 a[3] 的 Q 值高。

我们知道目标值是这样计算的:

现在,如果我们选择最大值对应的动作作为最佳动作,那么我们最终会选择动作 a[2] 而不是最优动作 a[3],如图所示:

那么,我们如何消除这种过高的估计呢?我们可以通过仅修改目标值计算来消除这种过高估计,如下所示:

正如我们可以观察到的,现在我们在目标值计算中有两个 Q 函数。一个由主网络参数 参数化的 Q 函数用于选择动作,另一个由目标网络参数 参数化的 Q 函数用于计算 Q 值。

让我们通过将前面的公式分解为两个步骤来理解它:

  • 动作选择:首先,我们使用由主网络参数 参数化的主网络计算所有下一状态-动作对的 Q 值,然后选择具有最大 Q 值的动作

  • Q 值计算:一旦我们选择了动作 ,我们就使用由 参数化的目标网络计算该动作的 Q 值

让我们用一个例子来理解。假设状态 E,那么我们可以写成:

首先,我们使用由主网络参数 参数化的主网络计算状态 E 中所有动作的 Q 值,然后选择具有最大 Q 值的动作。假设具有最大 Q 值的动作是

现在,我们可以使用目标网络参数 和主网络选择的动作(即 )来计算 Q 值。因此,我们可以写成:

还不清楚吗?我们在 DQN 和双 DQN 中计算目标值的区别如下所示:

图 9.10:DQN 和双 DQN 之间的区别

因此,我们了解到,在双 DQN 中,我们使用两个 Q 函数计算目标值。一个由主网络参数 参数化的 Q 函数用于选择具有最大 Q 值的动作,另一个由目标网络参数 参数化的 Q 函数则使用主网络选择的动作来计算 Q 值:

除了目标值计算外,双 DQN 的工作方式与 DQN 完全相同。为了更清楚地说明,双 DQN 的算法将在下一节给出。

双 DQN 算法

双 DQN 的算法如下所示。正如我们所看到的,除了目标值计算(加粗步骤)外,其他步骤与 DQN 完全相同:

  1. 初始化主网络参数 为随机值

  2. 通过复制主网络参数 ,初始化目标网络参数

  3. 初始化重放缓冲区

  4. 对于N个回合,重复步骤 5

  5. 对于回合中的每一步,即对于t = 0, . . . , T-1:

    1. 观察状态s并使用 epsilon-贪婪策略选择一个动作,即以 epsilon 的概率选择一个随机动作a,以 1-epsilon 的概率选择该动作:

    2. 执行动作,进入下一个状态 ,并获得奖励r

    3. 将过渡信息存储在重放缓冲区中

    4. 从重放缓冲区随机抽取一个包含K个过渡的小批量

    5. 计算目标值,即

    6. 计算损失,

    7. 计算损失的梯度,并使用梯度下降法更新主网络参数

    8. 冻结目标网络参数 几次时间步,然后通过简单地复制主网络参数 来更新它

现在我们已经了解了双重 DQN 的工作原理,在下一节中,我们将学习一种名为优先经验重放的 DQN 的有趣变体。

使用优先经验重放的 DQN

我们了解到,在 DQN 中,我们从重放缓冲区随机抽取一个包含K个过渡的小批量并训练网络。那么,是否可以为重放缓冲区中的每个过渡分配一些优先级,并选择那些具有高优先级的过渡来进行学习呢?

是的,但是首先,为什么我们需要为过渡分配优先级?我们如何决定哪些过渡应该比其他过渡具有更高的优先级呢?让我们更详细地探讨这个问题。

TD 误差 是目标值与预测值之间的差异,如下所示:

具有较高 TD 误差的过渡意味着该过渡不正确,因此我们需要更多地学习该过渡以最小化误差。具有较低 TD 误差的过渡意味着该过渡已经很好。我们总是可以从错误中学到更多,而不仅仅是专注于我们已经擅长的内容,对吧?同样,我们可以从具有较高 TD 误差的过渡中学到更多,而不是从那些具有较低 TD 误差的过渡中。因此,我们可以给具有较高 TD 误差的过渡分配更高的优先级,而给具有较低 TD 误差的过渡分配较低的优先级。

我们知道,过渡信息由 组成,并且在这些信息的基础上,我们还添加了优先级p,并将带有优先级的过渡存储在我们的重放缓冲区中,格式为 。下图显示了包含过渡及其优先级的重放缓冲区:

图 9.11:优先级重放缓冲区

在下一节中,我们将学习如何使用基于 TD 误差的两种不同优先级方法对过渡进行优先排序。

优先级排序类型

我们可以使用以下两种方法对过渡进行优先级排序:

  • 成比例优先级排序

  • 基于排名的优先级排序

成比例优先级排序

我们了解到,可以使用 TD 误差来对过渡进行优先级排序,因此过渡i的优先级p将仅为其 TD 误差:

请注意,我们取 TD 误差的绝对值作为优先级,以保持优先级为正。那么,TD 误差为零的过渡如何处理呢?假设我们有一个过渡i,其 TD 误差为 0,那么过渡i的优先级将为 0:

但是,将过渡的优先级设置为零是不理想的,因为如果将某个过渡的优先级设置为零,那么该过渡将完全不参与训练。为了避免这个问题,我们将向 TD 误差中添加一个小值,称为 epsilon。因此,即使 TD 误差为零,由于 epsilon 的存在,我们仍然会有一个小的优先级。更准确地说,向 TD 误差中添加 epsilon 可以保证没有过渡的优先级为零。因此,我们的优先级可以修改为:

我们可以将优先级转换为概率,以便优先级范围从 0 到 1。我们可以通过如下方式将优先级转换为概率:

上述公式计算了过渡i的概率P

我们是否也可以控制优先级排序的程度呢?也就是说,除了仅采样优先级较高的过渡外,我们还能否采样随机的过渡?可以!为了做到这一点,我们引入了一个新的参数,称为 ,并将我们的公式改写为如下。当 的值较高时,比如为 1,我们就只采样优先级高的过渡;而当 的值较低时,比如为 0,我们就采样随机的过渡:

因此,我们已经学习了如何使用成比例优先级排序方法来分配过渡的优先级。在下一节中,我们将学习另一种优先级排序方法,称为基于排名的优先级排序。

基于排名的优先级排序

基于排名的优先级排序是最简单的优先级排序类型。在这种方法中,我们根据过渡的排名来分配优先级。那么,什么是过渡的排名呢?过渡i的排名可以定义为在回放缓冲区中,按 TD 误差从高到低排序的过渡位置。

因此,我们可以使用排名来定义过渡i的优先级,如下所示:

正如我们在上一节中所学,我们将优先级转换为概率:

类似于我们在上一节中学到的内容,我们可以添加一个参数α来控制优先级排序的程度,并将最终公式表示为:

修正偏差

我们已经学习了如何通过两种方法来优先考虑转移——比例优先级和基于排名的优先级。但这些方法的问题在于,我们会对具有高优先级的样本产生很大的偏差。也就是说,当我们更重视具有高 TD 误差的样本时,这实际上意味着我们只从具有高 TD 误差的样本子集进行学习。

好的,那这个问题是什么呢?它会导致过拟合问题,我们的智能体会对那些具有高 TD 误差的转移产生强烈的偏向。为了应对这个问题,我们使用了重要性权重w。重要性权重帮助我们减少多次发生的转移的权重。转移i的权重w可以表示为:

在前面的表达式中,N表示我们的回放缓冲区的长度,P(i)表示转移i的概率。那么,那个参数!是什么呢?它控制着重要性权重。我们从较小的值开始!,从 0.4 开始,并将其逐步调整到 1。

因此,在这一部分中,我们学习了如何通过优先经验回放在 DQN 中优先考虑转移。在下一部分中,我们将学习 DQN 的另一个有趣变种——对战 DQN。

对战 DQN

在继续之前,让我们学习一下强化学习中最重要的函数之一——优势函数。优势函数被定义为 Q 函数和值函数之间的差异,其表达式为:

好的,但是优势函数有什么用呢?它意味着什么?首先,我们回顾一下 Q 函数和值函数:

  • Q 函数:Q 函数给出了智能体从状态s开始,执行动作a并遵循策略后,所能获得的期望回报!

  • 值函数:值函数给出的是一个智能体从状态s开始并遵循策略后,所能获得的期望回报!

现在,如果我们直观地思考,Q 函数和值函数之间有什么区别?Q 函数给出了状态-动作对的价值,而值函数则给出了一个状态的价值,不考虑动作。现在,Q 函数和值函数之间的区别告诉我们,在状态s下,动作a相比于平均动作有多好。

因此,优势函数告诉我们,在状态s下,动作a相比于平均动作有多好。现在我们已经理解了什么是优势函数,接下来让我们看看为什么以及如何在 DQN 中使用它。

理解对战 DQN

我们已经了解到,在 DQN 中,我们将状态作为输入,网络计算该状态下所有动作的 Q 值。我们能否不以这种方式计算 Q 值,而是通过优势函数来计算 Q 值呢?我们已经了解到,优势函数是通过以下方式给出的:

我们可以将前面的方程式改写为:

从前面的方程式中我们可以看出,我们只需要将价值函数和优势函数加在一起,就可以计算 Q 值。等等!为什么我们要这么做?直接计算 Q 值不行吗?

假设我们处于某个状态s,并且在这个状态下有 20 个可能的动作可以执行。计算这 20 个动作在状态s下的 Q 值是没有用的,因为大多数动作不会对状态产生任何影响,而且大多数动作的 Q 值也非常相似。这是什么意思呢?让我们通过图 9.12 中展示的网格世界环境来理解这一点:

图 9.12:网格世界环境

正如我们所看到的,智能体处于状态A。在这种情况下,计算在状态A下动作向上的 Q 值有什么用呢?在状态A中,向移动不会产生任何效果,而且也不会让智能体到达任何地方。类似地,假设我们的动作空间非常大,比如说有 100 个动作。在这种情况下,大多数动作在给定的状态下不会产生任何效果。而且,当动作空间很大时,大多数动作的 Q 值会非常相似。

现在,让我们讨论状态的价值。请注意,并不是所有状态对智能体来说都是重要的。可能会有一个状态,无论我们执行什么动作,总是给出一个很差的奖励。在这种情况下,如果我们知道该状态总是会给出一个很差的奖励,那么计算该状态下所有可能动作的 Q 值是没有意义的。

因此,为了解决这个问题,我们可以将 Q 函数计算为价值函数和优势函数的和。也就是说,通过价值函数,我们可以理解一个状态是否有价值,而无需计算该状态下所有动作的值。通过优势函数,我们可以理解一个动作是否真的很好,或者它只是给我们带来了与其他所有动作相同的价值。

现在我们已经对对抗 DQN 有了基本的了解,让我们在下一节中深入探讨对抗 DQN 的架构。

对抗 DQN 的架构

我们已经了解到,在对抗 DQN 中,Q 值可以计算为:

我们如何设计神经网络以这种方式输出 Q 值呢?我们可以将网络的最终层分成两个流。第一个流计算值函数,第二个流计算优势函数。给定任何状态作为输入,值流输出一个状态的值,而优势流输出给定状态下所有可能动作的优势。例如,正如图 9.13所示,我们将游戏状态(游戏画面)作为输入传入网络。值流计算状态的值,而优势流计算该状态下所有动作的优势值:

图 9.13:对抗 DQN 的架构

我们了解到,我们通过将状态值和优势值相加来计算 Q 值,因此我们将值流和优势流通过另一个层——聚合层结合,并按照图 9.14所示计算 Q 值。这样,值流计算状态值,优势流计算优势值,聚合层将这两个流相加并计算 Q 值:

图 9.14:包含聚合层的对抗 DQN 架构

但这里有一个小问题。仅仅在聚合层中将状态值和优势值相加并计算 Q 值会导致可识别性问题。

所以,为了应对这个问题,我们让优势函数对于所选动作的优势为零。我们可以通过减去平均优势值来实现这一点,即所有动作在动作空间中的平均优势值,如下所示:

其中 表示动作空间的长度。

因此,我们可以写出最终的计算 Q 值的方程式,带有参数如下:

在上式中, 是卷积网络的参数, 是值流的参数, 是优势流的参数。计算出 Q 值后,我们可以选择动作:

因此,对抗 DQN 和 DQN 之间唯一的区别是,在对抗 DQN 中,我们不是直接计算 Q 值,而是通过将状态值和优势值结合来计算 Q 值。

在下一节中,我们将探索 DQN 的另一个变种,称为深度递归 Q 网络。

深度递归 Q 网络

深度递归 Q 网络DRQN)与 DQN 完全相同,只是增加了递归层。那么,DQN 中递归层的作用是什么呢?为了回答这个问题,我们首先需要了解一个叫做部分可观察马尔可夫决策过程POMDP)的问题。

当我们对环境的可用信息有限时,这个环境被称为 POMDP。在之前的章节中,我们看到的是一个完全可观察的 MDP,我们知道所有可能的动作和状态——虽然我们可能不知道转移和奖励的概率,但我们对环境有完全的了解。例如,在冰冻湖环境中,我们对所有状态和动作都有完整的知识。

但大多数现实世界的环境只能部分观察到;我们不能看到所有状态。例如,假设一个智能体在现实世界中学习走路。在这种情况下,智能体对环境(即真实世界)没有完全的了解;它无法获得超出视野的信息。

因此,在 POMDP 中,状态只提供部分信息,但将过去状态的信息保存在记忆中将帮助智能体更好地理解环境的性质,并找到最优策略。因此,在 POMDP 中,我们需要保留过去状态的信息,以便采取最优行动。

那么,我们能否利用循环神经网络来理解并保留过去状态的信息,只要它是需要的呢?是的,长短期记忆循环神经网络LSTM RNN)对于保留、遗忘和根据需要更新信息非常有用。因此,我们可以在 DQN 中使用 LSTM 层来保留过去状态的信息,只要它是需要的。保留过去状态的信息在 POMDP 问题中非常有帮助。

现在我们基本了解了为什么需要 DRQN 以及它如何解决 POMDP 问题,在下一节中,我们将深入研究 DRQN 的架构。

DRQN 的架构

图 9.15 展示了 DRQN 的架构。正如我们所见,它与 DQN 架构类似,唯一的区别是它有一个 LSTM 层:

图 9.15:DRQN 的架构

我们将游戏屏幕作为输入传递给卷积层。卷积层对图像进行卷积并生成特征图。生成的特征图随后传递给 LSTM 层。LSTM 层具有记忆功能,可以保存信息。因此,它会保留关于重要过去游戏状态的信息,并根据需要在时间步长上更新其记忆。然后,我们将 LSTM 层的隐藏状态输入到全连接层,后者输出 Q 值。

图 9.16 帮助我们理解 DRQN 是如何工作的。假设我们需要计算状态 s[t] 和动作 a[t] 的 Q 值。与 DQN 不同,我们不仅仅是直接计算 Q(s[t], a[t])。如我们所见,除了当前状态 s[t],我们还使用隐藏状态 h[t] 来计算 Q 值。之所以使用隐藏状态,是因为它保存了过去游戏状态的信息。

由于我们使用了 LSTM 单元,隐藏状态 h[t] 将包含有关过去游戏状态的记忆信息,只要它是需要的:

图 9.16:DRQN 架构

除了这一变化外,DRQN 的工作方式与 DQN 相同。等等,那回放缓冲区呢?在 DQN 中,我们了解到我们将转移信息存储在回放缓冲区中,并通过从回放缓冲区随机采样一小批次经验来训练网络。我们还了解到,转移信息会在回放缓冲区中按顺序排列,为了避免相关经验的影响,我们随机从回放缓冲区中采样一小批次经验来训练网络。

但在 DRQN 的情况下,我们需要序列信息,以便我们的网络能够保留过去游戏状态的信息。因此,我们需要序列信息,同时又不希望通过训练相关经验而使网络过拟合。我们该如何实现这一点?

为了实现这一点,在 DRQN 中,我们随机采样一小批次的 episode,而不是随机采样一小批次的转移。也就是说,我们知道在一个 episode 中,会有按顺序排列的转移信息,因此我们采样一个随机的 episode 小批次,在每个 episode 中,转移信息会按顺序排列。通过这种方式,我们既能实现随机化,又能保留按顺序排列的转移信息。这被称为 引导式序列更新

在随机采样 episode 小批次之后,我们可以像训练 DQN 网络一样,通过最小化 MSE 损失来训练 DRQN。如果想深入了解,可以参考 进一步阅读 部分中提供的 DRQN 论文。

总结

我们从学习什么是深度 Q 网络以及它们如何用于近似 Q 值开始。本章中,我们了解到,在 DQN 中,我们使用一个叫做回放缓冲区的缓存来存储智能体的经验。然后,我们从回放缓冲区随机采样一小批次经验,并通过最小化 MSE 来训练网络。接下来,我们更详细地了解了 DQN 的算法,随后学习了如何实现 DQN 来玩 Atari 游戏。

随后,我们了解到 DQN 因为使用了 max 操作符而高估了目标值。因此,我们使用了双重 DQN,在目标值计算中使用了两个 Q 函数。一个 Q 函数由主网络参数 参数化,用于选择动作,另一个 Q 函数由目标网络参数 参数化,用于 Q 值计算。

接下来,我们学习了带有优先经验回放的 DQN,其中转移根据 TD 错误进行优先级排序。我们探讨了两种不同的优先级排序方法,分别是按比例优先级排序和基于排名的优先级排序。

接下来,我们学习了 DQN 的另一个有趣变体——对抗 DQN。在对抗 DQN 中,我们不是直接计算 Q 值,而是使用两个流——值流和优势流——来计算它们。

在本章的最后,我们了解了 DRQN 以及它们如何解决部分可观测马尔可夫决策过程的问题。

在下一章中,我们将学习另一个流行的算法——策略梯度。

问题

让我们通过回答以下问题来评估我们对 DQN 及其变体的理解:

  1. 为什么我们需要 DQN?

  2. 什么是重放缓冲区?

  3. 为什么我们需要目标网络?

  4. 双重 DQN 与 DQN 有什么不同?

  5. 为什么我们必须优先考虑转换?

  6. 什么是优势函数?

  7. 为什么在 DRQN 中需要 LSTM 层?

进一步阅读

更多信息,请参考以下论文:

第十章:策略梯度方法

在前几章中,我们学习了如何使用基于值的强化学习算法来计算最优策略。也就是说,我们学到的是,使用基于值的方法时,我们迭代地计算最优 Q 函数,并从最优 Q 函数中提取最优策略。在本章中,我们将学习基于策略的方法,在这种方法中,我们可以在不计算最优 Q 函数的情况下直接计算最优策略。

我们将通过分析从 Q 函数计算策略的缺点来开始本章的学习,然后我们将学习基于策略的方法如何直接学习最优策略,而无需计算 Q 函数。接下来,我们将研究一种最流行的基于策略的方法——策略梯度。我们将首先概览策略梯度算法,然后详细学习它。

在接下来的内容中,我们还将学习如何一步步推导策略梯度,并更加详细地研究策略梯度方法的算法。章节最后,我们将学习策略梯度方法中的方差减少技术。

本章我们将学习以下内容:

  • 为什么选择基于策略的方法?

  • 策略梯度直觉

  • 推导策略梯度

  • 策略梯度算法

  • 使用奖励到达的策略梯度

  • 带基准的策略梯度

  • 带基准的策略梯度算法

为什么选择基于策略的方法?

强化学习的目标是找到最优策略,也就是提供最大回报的策略。到目前为止,我们已经学习了几种计算最优策略的不同算法,这些算法都是基于值的方法。等等,什么是基于值的方法?让我们回顾一下基于值的方法以及它们的问题,然后我们将学习基于策略的方法。回顾一下总是有益的,不是吗?

使用基于值的方法时,我们从最优 Q 函数(Q 值)中提取最优策略,这意味着我们计算所有状态-动作对的 Q 值,以找到策略。我们通过在每个状态中选择具有最大 Q 值的动作来提取策略。例如,假设我们有两个状态s[0]和s[1],我们的动作空间有两个动作,分别为 0 和 1。首先,我们计算所有状态-动作对的 Q 值,如下表所示。现在,我们通过在状态s[0]中选择动作 0,在状态s[1]中选择动作 1 来从 Q 函数(Q 值)中提取策略,因为它们具有最大 Q 值:

表 10.1:Q 表

后来我们了解到,当环境具有大量的状态和动作时,计算 Q 函数会很困难,因为计算所有可能状态-动作对的 Q 值非常昂贵。因此,我们转向了深度 Q 网络DQN)。在 DQN 中,我们使用神经网络来逼近 Q 函数(Q 值)。给定一个状态,网络将返回该状态下所有可能动作的 Q 值。例如,考虑网格世界环境。给定一个状态,我们的 DQN 会返回该状态下所有可能动作的 Q 值。然后我们选择具有最高 Q 值的动作。如在图 10.1中所示,给定状态E,DQN 返回所有可能动作(上、下、左、右)的 Q 值。然后我们在状态E中选择动作,因为它具有最大 Q 值:

图 10.1: DQN

因此,在基于值的方法中,我们会迭代地改善 Q 函数,一旦我们得到最优的 Q 函数,就可以通过选择每个状态下具有最大 Q 值的动作来提取最优策略。

基于值的方法的一个缺点是,它仅适用于离散环境(具有离散动作空间的环境),我们不能在连续环境(具有连续动作空间的环境)中应用基于值的方法。

我们了解到,离散的动作空间有一组离散的动作;例如,网格世界环境中有离散的动作(上、下、左、右),而连续的动作空间包含的是连续值的动作,例如控制汽车的速度。

到目前为止,我们只处理了一个离散的环境,其中有离散的动作空间,因此我们可以轻松计算所有可能状态-动作对的 Q 值。但当我们的动作空间是连续的,我们该如何计算所有可能状态-动作对的 Q 值呢?假设我们正在训练一个智能体驾驶汽车,并且我们在动作空间中有一个连续的动作。假设这个动作是汽车的速度,而汽车速度的范围是从 0 到 150 公里每小时。在这种情况下,我们该如何计算所有可能状态-动作对的 Q 值,其中动作是一个连续的值呢?

在这种情况下,我们可以将连续的动作离散化为速度(0 到 10)作为动作 1,速度(10 到 20)作为动作 2,依此类推。离散化之后,我们可以计算所有可能的状态-动作对的 Q 值。然而,离散化并不总是可取的。我们可能会丧失一些重要特征,并且可能最终会得到一个包含大量动作的动作空间。

大多数现实世界的问题都有连续的动作空间,比如自动驾驶汽车,或者机器人学习走路等。除了拥有连续的动作空间,它们还具有高维度。因此,DQN 及其他基于值的方法无法有效地处理连续的动作空间。

因此,我们使用基于策略的方法。在基于策略的方法中,我们不需要计算 Q 函数(Q 值)来找到最优策略;相反,我们可以直接计算它们。也就是说,我们不需要 Q 函数来提取策略。与基于值的方法相比,基于策略的方法有几个优点,并且它们能够处理离散和连续的动作空间。

我们了解到,DQN 通过使用 epsilon-greedy 策略来解决探索与利用的困境。使用 epsilon-greedy 策略时,我们以 1-epsilon 的概率选择最佳动作,或者以 epsilon 的概率选择随机动作。大多数基于策略的方法使用随机策略。我们知道,使用随机策略时,我们根据动作空间的概率分布选择动作,这使得智能体能够探索不同的动作,而不是每次都执行相同的动作。因此,基于策略的方法通过使用随机策略隐式地解决了探索与利用的权衡。然而,也有一些基于策略的方法使用确定性策略。我们将在接下来的章节中了解更多关于这些方法的内容。

好的,基于策略的方法究竟是如何工作的呢?它们是如何在不计算 Q 函数的情况下找到最优策略的?我们将在下一节中学习这一点。现在,我们已经对什么是策略梯度方法以及基于值的方法的缺点有了基本的理解,在接下来的章节中,我们将学习一种基本且有趣的基于策略的方法,称为策略梯度。

策略梯度直觉

策略梯度是深度强化学习中最流行的算法之一。正如我们所了解的,策略梯度是一种基于策略的方法,我们可以通过它在不计算 Q 函数的情况下找到最优策略。它通过使用某些参数 来直接参数化策略,从而找到最优策略。

策略梯度方法使用的是随机策略。我们已经了解到,使用随机策略时,我们根据动作空间的概率分布来选择一个动作。假设我们有一个随机策略 ,那么它给出了在状态 s 下采取动作 a 的概率。可以表示为 。在策略梯度方法中,我们使用参数化的策略,因此我们可以将我们的策略表示为 ,其中 表示我们的策略是参数化的。

等等!当我们说一个参数化的策略时,这是什么意思呢?它到底是什么?记住,在 DQN 中,我们学到我们将 Q 函数参数化来计算 Q 值?我们在这里也可以做类似的事,只不过不是参数化 Q 函数,而是直接参数化策略来计算最优策略。也就是说,我们可以使用任何函数逼近器来学习最优策略,且 是我们函数逼近器的参数。我们通常使用神经网络作为函数逼近器。因此,我们有一个由 参数化的策略 ,其中 是神经网络的参数。

假设我们有一个带有参数的神经网络 。首先,我们将环境的状态作为输入传递给网络,网络会输出在该状态下可以执行的所有动作的概率。也就是说,它输出的是动作空间上的概率分布。我们已经学过,使用策略梯度时,我们采用的是一个随机策略。所以,这个随机策略是基于神经网络给出的概率分布来选择一个动作的。通过这种方式,我们可以直接计算策略,而无需使用 Q 函数。

让我们通过一个例子来理解策略梯度方法是如何工作的。为了更好地理解,我们以我们最喜欢的网格世界环境为例。我们知道,在网格世界环境中,我们的动作空间有四个可能的动作:

给定任何状态作为输入,神经网络将输出一个动作空间上的概率分布。也就是说,如图 10.2所示,当我们将状态E作为输入传递给网络时,网络会返回所有动作的概率分布。现在,我们的随机策略将基于神经网络给出的概率分布选择一个动作。因此,它会在 10%的时间里选择,10%的时间里选择,10%的时间里选择,70%的时间里选择

图 10.2:一个策略网络

我们不应该把 DQN 和策略梯度方法弄混淆。使用 DQN 时,我们将状态作为输入传递给网络,网络会返回该状态下所有可能动作的 Q 值,然后我们选择具有最大 Q 值的动作。但在策略梯度方法中,我们将状态作为输入传递给网络,网络会返回一个动作空间上的概率分布,然后我们的随机策略使用神经网络返回的概率分布来选择一个动作。

好的,在策略梯度方法中,网络返回的是动作空间上各个动作的概率分布(动作概率),那么这些概率有多准确呢?网络是如何学习的呢?

与监督学习不同,在这里我们没有任何标签数据来训练我们的网络。因此,我们的网络不知道在给定状态下应该执行哪个正确的动作;也就是说,网络不知道哪个动作能获得最大的奖励。因此,在初始迭代中,我们的神经网络给出的动作概率将不准确,从而我们可能会得到不好的回报。

但是这没关系。我们只是根据网络给出的概率分布选择动作,存储回报,并在本回合结束前进入下一个状态。也就是说,我们玩一个回合,并存储状态、动作和回报。现在,这些就成了我们的训练数据。如果我们赢得了本回合,也就是如果我们得到了正回报或高回报(本回合所有奖励的总和),那么我们会增加本回合中所有动作的概率。如果我们得到了负回报或低回报,那么我们会降低本回合中所有动作的概率,直到本回合结束。

让我们通过一个例子来理解这一点。假设我们有从 s[1] 到 s[8] 的状态,我们的目标是到达 s[8] 状态。假设我们的动作空间仅包含两个动作:leftright。因此,当我们将任何状态输入到网络时,它会返回关于这两个动作的概率分布。

考虑以下轨迹(回合) ,其中我们基于网络返回的概率分布,通过随机策略在每个状态中选择一个动作:

图 10.3:轨迹

该轨迹的回报是 。由于我们得到了正回报,我们会增加每个状态中所有动作的概率,直到本回合结束。也就是说,我们会增加在 s[1] 中执行 left 动作的概率,在 s[2] 中执行 right 动作的概率,依此类推,直到本回合结束。

假设我们生成了另一条轨迹 ,在其中,我们根据网络返回的概率分布,通过随机策略在每个状态中选择一个动作,如 图 10.4 所示:

图 10.4:轨迹

该轨迹的回报是 。由于我们得到了负回报,我们会降低每个状态中所有动作的概率,直到本回合结束。也就是说,我们将降低在 s[1] 中执行 right 动作的概率,在 s[3] 中执行 right 动作的概率,依此类推,直到本回合结束。

好的,但我们究竟如何增加或减少这些概率呢?我们已经知道,如果轨迹的回报是正的,那么我们会增加本回合中所有动作的概率;否则我们会减少它。那么我们究竟该如何操作呢?这正是反向传播的帮助所在。我们知道,我们通过反向传播来训练神经网络。

因此,在反向传播过程中,网络计算梯度并更新网络参数 。梯度更新的方式是,高回报的动作会获得高概率,而低回报的动作会获得低概率。

简而言之,在策略梯度方法中,我们使用神经网络来找到最优策略。我们将网络参数 初始化为随机值。我们将状态作为输入传入网络,网络会返回动作概率。在初始迭代中,由于网络没有经过任何数据的训练,它会给出随机的动作概率。但我们根据网络给定的动作概率分布选择动作,并将状态、动作和奖励存储到剧集结束为止。现在,这些数据成为了我们的训练数据。如果我们赢得了这一轮,即得到了高回报,那么我们会给这一轮的所有动作分配高概率,否则,我们会给这一轮的所有动作分配低概率。

由于我们使用神经网络来找到最优策略,我们可以将这个神经网络称为策略网络。现在我们对策略梯度方法有了基本的理解,在下一节中,我们将学习神经网络如何精确找到最优策略;也就是说,我们将学习梯度计算是如何进行的,以及如何训练网络。

理解策略梯度

在上一节中,我们学到了,在策略梯度方法中,我们以一种方式更新梯度,使得高回报的动作会得到高概率,低回报的动作会得到低概率。在本节中,我们将学习如何精确做到这一点。

策略梯度方法的目标是找到神经网络的最优参数 ,使得网络能够返回正确的动作空间概率分布。因此,我们网络的目标是将高概率分配给那些最大化轨迹期望回报的动作。所以,我们可以将目标函数 J 写作:

在前面的方程中,以下内容适用:

  • 是轨迹。

  • 表示我们根据由 参数化的网络给定的策略 来采样轨迹。

  • 是轨迹 的回报。

因此,最大化我们的目标函数即最大化轨迹的回报。那么,如何最大化前面的目标函数呢?我们通常处理的是最小化问题,通过计算损失函数的梯度并使用梯度下降法更新参数,来最小化损失函数(即目标函数)。但在这里,我们的目标是最大化目标函数,因此我们计算目标函数的梯度并进行梯度上升。即:

其中 表示我们目标函数的梯度。因此,我们可以使用梯度上升法找到我们网络的最优参数

梯度 是通过 推导出来的。我们将在下一节学习如何推导这个梯度。在本节中,我们暂时只关注如何获得对策略梯度的良好基础理解。

我们了解到,我们通过以下方式更新我们的网络参数:

将梯度的值代入后,我们的参数更新方程变为:

在前面的方程中,以下内容适用:

  • 代表在时间 t 给定状态 s 时采取动作 a 的对数概率。

  • 代表轨迹的回报。

我们了解到,我们以这样的方式更新梯度:使得产生高回报的动作获得较高的概率,而产生低回报的动作获得较低的概率。现在让我们来看看到底是怎么做到的。

情况 1

假设我们使用策略 生成一个回合(轨迹),其中 是网络的参数。在生成回合后,我们计算该回合的回报。如果回合的回报是负的,比如 -1,即 ,那么我们会减少我们在每个状态下采取的所有动作的概率,直到回合结束。

我们了解到,我们的参数更新方程可以表示为:

在前面的方程中,将 乘以负回报 表示我们正在减少在状态 s[t] 下,采取动作 a[t] 的对数概率。因此,我们执行一个负向更新。即:

对于回合中的每一步,t = 0, . . ., T-1,我们按照以下方式更新参数

这意味着我们在每个状态下,直到回合结束,都会减少我们采取的所有动作的概率。

情况 2

假设我们使用策略 生成一个剧集(轨迹),其中 是网络的参数。生成剧集后,我们计算该剧集的回报。如果剧集的回报为正,比如 +1,也就是 ,那么我们会增加每个状态下采取的所有行动的概率,直到剧集结束。

我们了解到我们的参数更新方程如下:

在上述方程中,将 乘以正回报 意味着我们正在增加在状态 s[t] 中执行动作 a[t] 的对数概率。因此,我们执行一个正向更新。也就是:

对于剧集中的每一步,t = 0, . . ., T-1,我们更新参数 如下:

因此,如果我们得到一个正回报,我们会增加该剧集中所有执行动作的概率,否则我们会降低其概率。

我们了解到,对于剧集中的每一步,t = 0, . . ., T-1,我们更新参数 如下:

我们可以简化前述方程为:

因此,如果剧集(轨迹)给出了较高的回报,我们将增加剧集中所有动作的概率,否则我们会降低它们的概率。我们了解到 。那期望值呢?我们在更新方程中还没有考虑到这一点。当我们查看蒙特卡洛方法时,我们了解到可以通过平均值来近似期望值。因此,使用蒙特卡洛近似方法,我们将期望项改为对 N 条轨迹的求和。因此,我们的更新方程变为:

它表明,与其基于单一轨迹更新参数,我们收集一组 N 条轨迹来跟随策略 ,并根据平均值更新参数,即:

因此,首先,我们收集 N 条轨迹 来跟随策略 ,并计算梯度如下:

然后我们更新参数如下:

但是我们无法仅通过更新参数一次迭代来找到最优参数 。因此,我们需要重复之前的步骤进行多次迭代,以找到最优参数。

现在我们对策略梯度方法的工作原理有了基本的了解,在下一节中,我们将学习如何推导策略梯度 。之后,我们将一步一步地详细学习策略梯度算法。

推导策略梯度

在这一节中,我们将深入了解并学习如何计算梯度 ,以及它如何等于

让我们深入探讨一下有趣的数学,并看看如何简单地计算目标函数 J 关于模型参数 的导数。不要被接下来的方程吓到,其实这只是一个相当简单的推导。在继续之前,让我们复习一些数学基础知识,以便更好地理解我们的推导。

期望的定义:

X 是一个离散随机变量,其 概率质量函数 (pmf) 为 p(x)。设 f 是离散随机变量 X 的一个函数。那么,函数 f(X) 的期望可以定义为:

X 是一个连续随机变量,其 概率密度函数 (pdf) 为 p(x)。设 f 是连续随机变量 X 的一个函数。那么,函数 f(X) 的期望可以定义为:

一个对数导数技巧如下:

我们了解到,网络的目标是最大化轨迹的期望回报。因此,我们可以将目标函数 J 写成:

在上述方程中,以下条件适用:

  • 是轨迹。

  • 表示我们正在基于由 参数化的网络给定的策略 对轨迹进行采样。

  • 是轨迹的回报。

正如我们所看到的,我们的目标函数,方程 (4),处于期望形式。根据方程 (2) 中给出的期望定义,我们可以展开期望并将方程 (4) 重写为:

现在,我们计算目标函数 J 关于 的导数:

通过乘以并除以 ,我们可以写成:

通过重新排列上述方程,我们可以写成:

从方程 (3) 中,将 代入上面的方程,我们可以写成:

从方程 (2) 给出的期望定义,我们可以将上面的方程重新写成期望形式:

上面的方程给出了目标函数的梯度。但我们仍然没有解决方程。正如我们所看到的,在上述方程中,我们有项 。现在我们来看一下如何计算它。

轨迹的概率分布可以表示为:

其中 p(s[0]) 是初始状态分布。对两边取对数,我们可以写成:

我们知道,乘积的对数等于对数的和,也就是说,。将这个对数规则应用到上面的方程中,我们可以写成:

再次应用相同的规则,乘积的对数 = 对数之和,并将对数更改为的对数,如下所示:

现在,我们计算相对于的导数:

注意,我们正在计算相对于的导数,正如我们在前面的方程中看到的,右侧RHS)的第一项和最后一项不依赖于,因此在计算导数时它们会变为零。因此,我们的方程变为:

现在我们已经找到了的值,将其代入方程(5)中可以写出:

就是这样。但我们能否也去掉那个期望?是的!我们可以使用蒙特卡罗近似方法,将期望改为N个轨迹的总和。因此,我们的最终梯度变为:

方程(6)表明,代替基于单一轨迹更新参数,我们收集N个轨迹,并基于这N个轨迹的平均值来更新参数。

因此,在计算梯度之后,我们可以更新我们的参数为:

因此,在这一部分中,我们学会了如何推导策略梯度。在下一部分中,我们将深入了解更多细节,逐步学习策略梯度算法。

算法 – 策略梯度

我们目前讨论的策略梯度算法通常称为 REINFORCE 或蒙特卡罗策略梯度。REINFORCE 方法的算法如下所示:

  1. 使用随机值初始化网络参数

  2. 生成N个轨迹,遵循策略

  3. 计算轨迹的回报

  4. 计算梯度!

  5. 更新网络参数为

  6. 重复步骤 25进行多次迭代

正如我们从这个算法中看到的,参数在每次迭代中都在更新。由于我们使用了参数化策略,我们的策略在每次迭代中都在更新。

我们刚刚学习的策略梯度算法是一种在线方法,因为我们只使用单一策略。也就是说,我们使用一个策略生成轨迹,并且通过在每次迭代中更新网络参数来改进同一个策略。

我们了解到,使用策略梯度方法(REINFORCE 方法)时,我们使用一个策略网络,该网络返回动作空间的概率分布,然后我们根据网络返回的概率分布使用随机策略选择一个动作。但这仅适用于离散的动作空间,并且我们使用类别策略作为我们的随机策略。

如果我们的动作空间是连续的呢?也就是说,当动作空间是连续的时,我们如何选择动作?在这种情况下,我们的策略网络不能返回动作空间上的概率分布,因为动作空间是连续的。所以,在这种情况下,我们的策略网络将返回动作的均值和方差作为输出,然后我们使用这个均值和方差生成一个高斯分布,并通过从该高斯分布中采样来选择动作,采用高斯策略。我们将在接下来的章节中详细学习这个方法。因此,我们可以将策略梯度方法应用于离散和连续的动作空间。接下来,我们将学习两种减少策略梯度更新方差的方法。

方差减少方法

在上一节中,我们学到了最简单的策略梯度方法之一,叫做 REINFORCE 方法。我们在上一节学到的策略梯度方法面临的一个主要问题是,梯度 在每次更新中会有很高的方差。高方差主要是由于每次回合的回报差异较大。也就是说,我们学到策略梯度是一种在策略上的方法,这意味着我们在每次迭代中都使用相同的策略来生成回合。由于策略在每次迭代中都在改进,我们的回报在每个回合中差异很大,这在梯度更新中引入了很高的方差。当梯度具有高方差时,将需要很长时间才能达到收敛。

因此,现在我们将学习以下两种减少方差的重要方法:

  • 带有奖励到达(因果关系)的策略梯度

  • 带有基线的策略梯度

带有奖励到达的策略梯度

我们学到,策略梯度的计算方式是:

现在,我们对前面的公式做一个小的修改。我们知道,轨迹的回报是该轨迹奖励的总和,即:

我们不是使用轨迹的回报 ,而是使用一种叫做奖励到达 R[t] 的东西。奖励到达基本上是从状态 s[t] 开始的轨迹的回报。也就是说,我们不是在每个回合的每一步都将对数概率乘以整个轨迹的回报 ,而是将其乘以奖励到达 R[t]。奖励到达意味着从状态 s[t] 开始的轨迹的回报。但为什么我们要这么做呢?让我们通过一个例子更详细地理解这一点。

我们学到,我们生成 N 个轨迹并计算梯度如下:

为了更好地理解,我们只取一个轨迹,设置 N=1,所以我们可以写成:

比如,我们用策略 生成了以下轨迹:

图 10.5:轨迹

前述轨迹的回报是

现在,我们可以计算梯度为:

正如我们从之前的方程中观察到的那样,在每一步中,我们都在将动作的对数概率与整个轨迹的回报相乘 ,在前面的例子中它是 2。

假设我们想知道在状态 s[2] 下 right 动作的效果。如果我们理解到 right 是状态 s[2] 下一个好的动作,那么我们可以增加在状态 s[2] 下向右移动的概率,否则我们降低它。好,那么我们如何判断 right 动作在状态 s[2] 下是否好呢?正如我们在前一部分中所学到的(讨论 REINFORCE 方法时),如果轨迹的回报 很高,那么我们增加状态 s[2] 下 right 动作的概率,否则我们降低它。

但是现在我们不必这样做。相反,我们可以只从状态 s[2] 开始计算回报(轨迹的奖励总和),因为在采取 right 动作时,包含从轨迹开始到达状态 s[2] 之前获得的所有奖励并没有用处。正如 图 10.6 所示,包含所有这些奖励并不会帮助我们理解在状态 s[2] 下 right 动作的效果:

图 10.6:轨迹

因此,我们不再在所有步骤中使用整个轨迹的回报,而是使用奖励到达 R[t],它表示从状态 s[t] 开始的轨迹的回报。

因此,现在我们可以写出:

其中 R[0] 表示从状态 s[0] 开始的轨迹的回报,R[1] 表示从状态 s[1] 开始的轨迹的回报,依此类推。如果 R[0] 值较高,则我们增加状态 s[0] 下 up 动作的概率,否则我们降低它。如果 R[1] 值较高,则我们增加状态 s[1] 下 down 动作的概率,否则我们降低它。

因此,现在我们可以将奖励到达定义为:

前述方程表示奖励到达 R[t] 是从状态 s[t] 开始的轨迹的奖励总和。因此,现在我们可以将梯度用奖励到达替代轨迹的回报重新写为:

我们可以简单地将前述方程表示为:

奖励到达(reward-to-go)是

计算梯度后,我们可以按以下方式更新参数:

现在我们已经理解了带有奖励到达的策略梯度,接下来的部分将探讨该算法以更清楚地说明。

算法 – 奖励到达策略梯度

带有回报后续(reward-to-go)的策略梯度算法与 REINFORCE 方法类似,不同的是我们现在计算回报后续(从状态 s[t] 开始的轨迹的回报),而不是使用轨迹的完整回报,如下所示:

  1. 使用随机值初始化网络参数

  2. 根据策略 生成 N 条轨迹

  3. 计算回报(回报后续) R[t]

  4. 计算梯度:

  5. 更新网络参数为

  6. 重复 步骤 2步骤 5 多次迭代

从前面的算法中,我们可以观察到我们使用了回报后续(reward-to-go),而不是轨迹的回报。为了清楚理解回报后续策略梯度的工作原理,让我们在下一节中实现它。

使用策略梯度进行倒立摆平衡

现在,让我们学习如何实现具有回报后续(reward-to-go)的策略梯度算法来进行倒立摆平衡任务。

为了清楚地理解策略梯度方法如何工作,我们通过禁用 TensorFlow 2 的行为,使用非急切模式来实现 TensorFlow。

首先,让我们导入所需的库:

import tensorflow.compat.v1 as tf
tf.disable_v2_behavior()
import numpy as np
import gym 

使用 gym 创建倒立摆环境:

env = gym.make('CartPole-v0') 

获取状态的形状:

state_shape = env.observation_space.shape[0] 

获取动作的数量:

num_actions = env.action_space.n 

计算折扣和归一化奖励

我们可以使用折扣和归一化奖励,而不是直接使用奖励。

设置折扣因子,

gamma = 0.95 

定义一个名为 discount_and_normalize_rewards 的函数,用于计算折扣和归一化的奖励:

def discount_and_normalize_rewards(episode_rewards): 

初始化一个数组来存储折扣奖励:

 discounted_rewards = np.zeros_like(episode_rewards) 

计算折扣奖励:

 reward_to_go = 0.0
    for i in reversed(range(len(episode_rewards))):
        reward_to_go = reward_to_go * gamma + episode_rewards[i]
        discounted_rewards[i] = reward_to_go 

归一化并返回奖励:

 discounted_rewards -= np.mean(discounted_rewards)
    discounted_rewards /= np.std(discounted_rewards)

    return discounted_rewards 

构建策略网络

首先,让我们定义状态的占位符:

state_ph = tf.placeholder(tf.float32, [None, state_shape], name="state_ph") 

定义动作的占位符:

action_ph = tf.placeholder(tf.int32, [None, num_actions], name="action_ph") 

定义折扣奖励的占位符:

discounted_rewards_ph = tf.placeholder(tf.float32, [None,], name="discounted_rewards") 

定义第 1 层:

layer1 = tf.layers.dense(state_ph, units=32, activation=tf.nn.relu) 

定义第 2 层。请注意,第 2 层的单元数量设置为动作的数量:

layer2 = tf.layers.dense(layer1, units=num_actions) 

通过对第 2 层的结果应用 softmax 函数,获得动作空间上的概率分布作为网络的输出:

prob_dist = tf.nn.softmax(layer2) 

我们了解到梯度的计算方式为:

在计算出梯度后,我们使用梯度上升法来更新网络的参数:

然而,通常的约定是进行最小化而不是最大化。因此,我们可以通过仅添加一个负号将前面的最大化目标转化为最小化目标。我们可以使用 tf.nn.softmax_cross_entropy_with_logits_v2 来实现这一点。因此,我们可以定义负对数策略为:

neg_log_policy = tf.nn.softmax_cross_entropy_with_logits_v2(logits = layer2, labels = action_ph) 

现在,让我们定义损失函数:

loss = tf.reduce_mean(neg_log_policy * discounted_rewards_ph) 

定义用于最小化损失的训练操作,并使用 Adam 优化器:

train = tf.train.AdamOptimizer(0.01).minimize(loss) 

训练网络

现在,让我们训练网络进行多次迭代。为了简化起见,我们在每次迭代中只生成一个回合。

设置迭代次数:

num_iterations = 1000 

启动 TensorFlow 会话:

with tf.Session() as sess: 

初始化所有 TensorFlow 变量:

 sess.run(tf.global_variables_initializer()) 

对每次迭代:

 for i in range(num_iterations): 

初始化一个空列表,用于存储回合中获得的状态、动作和奖励:

 episode_states, episode_actions, episode_rewards = [],[],[] 

done设置为False

 done = False 

初始化Return

 Return = 0 

通过重置环境初始化状态:

 state = env.reset() 

当回合尚未结束时:

 while not done: 

改变状态形状:

 state = state.reshape([1,4]) 

将状态输入到策略网络中,网络返回动作空间的概率分布作为输出,这就是我们的随机策略

 pi = sess.run(prob_dist, feed_dict={state_ph: state}) 

现在,我们使用这个随机策略选择一个动作:

 a = np.random.choice(range(pi.shape[1]), p=pi.ravel()) 

执行所选动作:

 next_state, reward, done, info = env.step(a) 

渲染环境:

 env.render() 

更新回报:

 Return += reward 

对动作进行独热编码:

 action = np.zeros(num_actions)
            action[a] = 1 

将状态、动作和奖励存储到各自的列表中:

 episode_states.append(state)
            episode_actions.append(action)
            episode_rewards.append(reward) 

将状态更新为下一个状态:

 state=next_state 

计算折扣化和归一化后的奖励:

 discounted_rewards= discount_and_normalize_rewards(episode_rewards) 

定义输入字典:

 feed_dict = {state_ph: np.vstack(np.array(episode_states)),
                     action_ph: np.vstack(np.array(episode_actions)),
                     discounted_rewards_ph: discounted_rewards 
                    } 

训练网络:

 loss_, _ = sess.run([loss, train], feed_dict=feed_dict) 

每 10 次迭代打印一次回报:

 if i%10==0:
            print("Iteration:{}, Return: {}".format(i,Return)) 

现在我们已经学会了如何实现带有奖励的策略梯度算法,在下一节中,我们将学习另一种有趣的方差减少技术——带基准的策略梯度。

带基准的策略梯度

我们已经学会了如何通过使用神经网络找到最优策略,并且通过梯度上升更新网络的参数:

其中,梯度的值为:

现在,为了减少方差,我们引入一个新的函数,称为基准函数。将基准b从回报(未来奖励)R[t]中减去可以减少方差,因此我们可以将梯度重写为:

等等,什么是基准函数?减去基准函数后,R[t]如何减少方差?基准的目的是减少回报的方差。因此,如果基准b是一个可以为我们提供从智能体所在状态开始的预期回报的值,那么在每一步减去b就会减少回报的方差。

基准函数有几种选择。我们可以选择任何函数作为基准函数,但基准函数不应依赖于我们的网络参数。一个简单的基准可能是采样轨迹的平均回报:

因此,减去当前回报R[t]和平均回报有助于减少方差。如我们所见,我们的基准函数不依赖于网络参数。所以,我们可以使用任何函数作为基准函数,它不应影响我们的网络参数

基准的最常见功能之一是值函数。我们了解到,值函数或状态的值是从该状态开始,按照策略执行的智能体预期获得的回报。因此,减去状态的值(即预期回报)和当前回报R[t]可以减少方差。所以,我们可以将梯度重写为:

除了值函数,我们还可以使用不同的基准函数,例如 Q 函数、优势函数等。我们将在下一章中详细了解它们。

但现在的问题是,我们如何学习基准函数?假设我们使用值函数作为基准函数。我们如何学习最优的值函数?就像我们在近似策略一样,我们也可以使用另一个由!参数化的神经网络来近似值函数。

也就是说,我们使用另一个网络来近似值函数(一个状态的值),我们可以称这个网络为值网络。那么,我们如何训练这个值网络呢?

由于状态的值是一个连续值,我们可以通过最小化均方误差MSE)来训练网络。MSE 可以定义为实际回报R[t]与预测回报!之间的均方差。因此,值网络的目标函数可以定义为:

我们可以使用梯度下降最小化误差,并更新网络参数如下:

因此,在带基准的策略梯度方法中,我们通过使用基准函数来最小化梯度更新中的方差。基准函数可以是任何函数,并且不应依赖于网络参数!。我们使用值函数作为基准函数,然后为了近似值函数,我们使用另一个由!参数化的神经网络,并通过最小化均方误差(MSE)来找到最优的值函数。

简而言之,在带基准函数的策略梯度中,我们使用两个神经网络:

由!参数化的策略网络:通过执行梯度上升来寻找最优策略:

由!参数化的值网络:通过作为基准来修正梯度更新中的方差,并通过执行梯度下降来找到状态的最优值:

请注意,带基准函数的策略梯度通常被称为带基准的 REINFORCE方法。

现在我们已经看到了带基准的策略梯度方法是如何通过使用策略和值网络工作的,在接下来的部分,我们将研究该算法以获得更清晰的理解。

算法 – 带基准的 REINFORCE

带基准函数的策略梯度方法(REINFORCE 带基准)的算法如下所示:

  1. 初始化策略网络参数!和值网络参数!

  2. 生成N个轨迹!,按照策略!执行。

  3. 计算回报(未来奖励)R[t]

  4. 计算策略梯度:!

  5. 使用梯度上升法更新策略网络参数 ,如

  6. 计算值网络的均方误差(MSE):

  7. 计算梯度 并使用梯度下降更新值网络参数 ,如

  8. 重复 步骤 2步骤 7 若干次

总结

我们通过学习基于值的方法开始本章,在基于值的方法中,我们从最优 Q 函数(Q 值)中提取最优策略。然后我们了解到,当我们的动作空间是连续时,计算 Q 函数是困难的。我们可以对动作空间进行离散化;然而,离散化并不总是可取的,它会导致丧失若干重要特征,并生成一个包含大量动作的巨大动作空间。

因此,我们采用了基于策略的方法。在基于策略的方法中,我们在没有 Q 函数的情况下计算最优策略。我们了解了一种最受欢迎的基于策略的方法,称为策略梯度,其中我们通过使用某些参数来对策略进行参数化,从而直接找到最优策略!

我们还了解到,在策略梯度方法中,我们根据网络给出的动作概率分布来选择动作,如果我们赢得了这一回合,也就是获得了高回报,那么我们会为回合中的所有动作分配高概率,否则我们会为回合中的所有动作分配低概率。之后,我们学习了如何一步步推导策略梯度,并进一步详细研究了策略梯度方法的算法。

接下来,我们了解了方差减少方法,如奖励到达(reward-to-go)和带基准函数的策略梯度方法。在带基准函数的策略梯度方法中,我们使用两个网络,分别是策略网络和价值网络。策略网络的作用是找到最优策略,价值网络的作用是通过估计值函数来修正策略网络中的梯度更新。

在下一章中,我们将学习一组有趣的算法,称为演员-评论家方法(actor-critic methods)。

问题

让我们通过回答以下问题来评估我们对策略梯度方法的理解:

  1. 什么是基于值的方法?

  2. 为什么我们需要基于策略的方法?

  3. 策略梯度方法是如何工作的?

  4. 如何计算策略梯度方法中的梯度?

  5. 什么是奖励到达(reward-to-go)?

  6. 什么是带基准函数的策略梯度?

  7. 定义基准函数。

进一步阅读

关于策略梯度的更多信息,我们可以参考以下论文:

第十一章:演员-评论家方法——A2C 和 A3C

到目前为止,我们已经介绍了两种学习最优策略的方法。一种是基于价值的方法,另一种是基于策略的方法。在基于价值的方法中,我们使用 Q 函数来提取最优策略。在基于策略的方法中,我们无需使用 Q 函数就能计算出最优策略。

本章将介绍一种有趣的方法,称为演员-评论家方法,用于寻找最优策略。演员-评论家方法结合了基于价值和基于策略的方法。我们将从了解演员-评论家方法是什么以及它如何结合基于价值和基于策略的方法开始。我们将获得演员-评论家方法的基本理解,之后会详细学习它们。

接下来,我们还将了解演员-评论家方法与带基线的策略梯度方法的不同,并将详细学习演员-评论家方法的算法。接下来,我们将理解优势演员-评论家A2C)是什么,以及它如何利用优势函数。

本章结束时,我们将学习一种最常用的演员-评论家算法,称为异步优势演员-评论家A3C)。我们将理解 A3C 是什么,以及它的工作原理和架构。

本章将涵盖以下主题:

  • 演员-评论家方法概述

  • 理解演员-评论家方法

  • 演员-评论家方法算法

  • 优势演员-评论家(A2C)

  • 异步优势演员-评论家(A3C)

  • 异步优势演员-评论家(A3C)的架构

  • 使用 A3C 进行山地车攀爬

我们将通过首先获得演员-评论家方法的基本理解,来开始本章的内容。

演员-评论家方法概述

演员-评论家方法是深度强化学习中最受欢迎的算法之一。许多现代深度强化学习算法都是基于演员-评论家方法设计的。演员-评论家方法位于基于价值方法和基于策略方法的交集处。也就是说,它结合了基于价值和基于策略的方法。

在本节中,我们首先不深入细节,简单了解演员-评论家方法如何工作,接下来我们将深入探讨,并理解演员-评论家方法背后的数学原理。

如其名所示,演员-评论家方法由两种网络组成——演员网络和评论家网络。演员网络的作用是找到最优策略,而评论家网络的作用是评估演员网络生成的策略。因此,我们可以将评论家网络视为一个反馈网络,它评估并引导演员网络找到最优策略,正如图 11.1所示:

图 11.1:演员-评论家网络

好的,那么,演员和评论员网络到底是什么?它们如何协同工作并改进策略?演员网络基本上就是策略网络,它通过策略梯度方法找到最优策略。评论员网络基本上就是价值网络,它估计状态值。

因此,评论员网络利用其状态值评估由演员网络产生的动作,并将其反馈发送给演员。根据评论员的反馈,演员网络然后更新其参数。

因此,在演员-评论员方法中,我们使用两个网络——演员网络(策略网络),它计算策略,和评论员网络(价值网络),它通过计算价值函数(状态值)来评估演员网络产生的策略。这不就是我们在上一章刚学到的东西吗?

是的!如果你还记得,它类似于我们在上一章中学习的带基准的策略梯度方法(带基准的 REINFORCE)。与带基准的 REINFORCE 类似,在这里,我们也有一个演员(策略网络)和一个评论员(价值网络)。然而,演员-评论员并不等同于带基准的 REINFORCE。在带基准的 REINFORCE 方法中,我们了解到,我们使用价值网络作为基准,它有助于减少梯度更新中的方差。而在演员-评论员方法中,我们也使用评论员来减少演员的梯度更新中的方差,但它还帮助以在线方式迭代地改进策略。这两者之间的区别将在下一节中进一步阐明。

现在我们对演员-评论员方法有了基本的了解,在下一节中,我们将详细学习演员-评论员方法的工作原理。

理解演员-评论员方法

在带基准的 REINFORCE 方法中,我们学习到,我们有两个网络——策略网络和价值网络。策略网络找到最优策略,而价值网络作为基准,修正梯度更新中的方差。类似于带基准的 REINFORCE,演员-评论员方法也由策略网络(称为演员网络)和价值网络(称为评论员网络)组成。

带基准的 REINFORCE 方法和演员-评论员方法的根本区别在于,在带基准的 REINFORCE 方法中,我们在每一集的结束时更新网络的参数。但在演员-评论员方法中,我们在每一步都更新网络的参数。但为什么我们必须这样做?在每一步更新网络参数有什么用呢?让我们进一步探讨这个问题。

我们可以认为带基线的 REINFORCE 方法类似于我们在第四章“蒙特卡洛方法”中讨论的蒙特卡洛MC)方法,而演员-评论员方法类似于我们在第五章“理解时序差分学习”中讨论的 TD 学习方法。因此,首先,让我们回顾一下这两种方法。

在 MC 方法中,计算一个状态的值时,我们生成N条轨迹,并计算状态的值作为N条轨迹中该状态的平均回报。我们了解到,当轨迹过长时,MC 方法会花费大量时间计算状态值,并且不适用于非序列任务。因此,我们转向了 TD 学习方法。

在 TD 学习方法中,我们了解到,我们可以利用引导法,而不是等到每集结束才计算状态值,估计状态值为即时奖励与下一个状态折扣值之和。

现在,让我们看看 MC 和 TD 方法分别如何与带基线的 REINFORCE 方法和演员-评论员方法相关联。

首先,让我们回顾一下我们在带基线的 REINFORCE 方法中学到的内容。在带基线的 REINFORCE 方法中,我们使用策略生成N条轨迹,并计算梯度如下:

正如我们所观察到的,为了计算梯度,我们需要一个完整的轨迹。也就是说,正如以下方程所示,为了计算梯度,我们需要计算轨迹的回报。我们知道回报是轨迹中所有奖励的总和,因此为了计算回报(奖励-目标),首先,我们需要一个使用策略生成的完整轨迹。因此,我们使用策略生成多个轨迹,然后计算梯度:

计算梯度后,我们更新参数如下:

我们可以像在 TD 学习中学到的那样,利用引导法,而不是生成完整轨迹后再计算回报吗?是的!在演员-评论员方法中,我们通过仅使用即时奖励和下一个状态的折扣值来近似回报,公式为:

其中,r是即时奖励,是下一个状态的折扣值。

因此,我们可以通过将回报R替换为引导估计来重写策略梯度,如下所示:

现在,我们不必等到每一集的结束才能计算回报。相反,我们采用引导法,在每个步骤中计算梯度并更新网络参数。

我们在带基线的 REINFORCE 和演员-评论方法中计算梯度并更新策略网络参数的方式的区别如 图 11.2 所示。正如我们在带基线的 REINFORCE 中看到的那样,首先生成完整的回合(轨迹),然后更新网络参数。而在演员-评论方法中,我们在每个回合的每一步更新网络参数:

图 11.2:带基线的 REINFORCE 方法与演员-评论方法的区别

好的,那么评论网络(值网络)呢?我们如何更新评论网络的参数?与演员网络类似,我们在每个回合的每一步更新评论网络的参数。评论网络的损失是 TD 误差,即状态的目标值与网络预测的状态值之间的差异。状态的目标值可以通过奖励与下一个状态值的折现值之和来计算。因此,评论网络的损失表示为:

其中 是状态的目标值,! 是网络预测的状态值。

计算评论网络的损失后,我们计算梯度!,并在每个回合的每一步使用梯度下降更新评论网络的参数!

现在我们已经了解了演员(策略网络)和评论(值网络)在演员-评论方法中的工作原理;接下来让我们看看演员-评论方法的算法,以便更清楚地理解。

演员-评论算法

演员-评论算法的步骤如下:

  1. 初始化演员网络参数! 和评论网络参数!

  2. 对于 N 个回合,重复 步骤 3

  3. 对于每一步骤,即对于 t = 0, . . . , T-1:

    1. 使用策略选择动作,!

    2. 在状态 s[t] 中采取动作 a[t],观察奖励 r,并转移到下一个状态!

    3. 计算策略梯度:

    4. 使用梯度上升更新演员网络参数!

    5. 计算评论网络的损失:

    6. 计算梯度! 并使用梯度下降更新评论网络参数!

从上面的算法中我们可以观察到,演员网络(策略网络)的参数在每个步骤都会被更新。因此,在每个步骤中,我们都会根据更新后的策略选择一个动作,而评论员网络(价值网络)的参数也会在每个步骤中更新,因此评论员在每个步骤中都会提高对演员网络的评估。而在 REINFORCE 与基准方法中,我们只会在生成完整的轨迹后才更新网络的参数。

我们还应该注意到,REINFORCE 与基准方法和演员-评论员方法之间的一个重要区别是,在 REINFORCE 与基准方法中,我们使用轨迹的完整回报,而在演员-评论员方法中,我们使用引导回报。

我们刚刚学习的演员-评论员算法通常被称为优势演员-评论员A2C)。在下一节中,我们将深入探讨优势函数,并更详细地了解为什么我们的算法被称为优势演员-评论员。

优势演员-评论员(A2C)

在继续之前,首先让我们回顾一下优势函数。优势函数被定义为 Q 函数与价值函数之间的差异。我们可以将优势函数表示为:

优势函数告诉我们,在状态s下,动作a相对于平均动作的优劣。

在 A2C 中,我们使用优势函数计算策略梯度。因此,首先让我们看看如何计算优势函数。我们知道,优势函数是 Q 函数与价值函数之间的差异,即Q(s, a) – V(s),因此我们可以使用两个函数逼近器(神经网络),一个用于估计 Q 函数,另一个用于估计价值函数。然后,我们可以通过减去这两个网络的值来得到优势值。但这肯定不是一种最优方法,并且在计算上会很昂贵。

因此,我们可以将 Q 值逼近为:

那么我们如何像这样逼近 Q 值呢?你还记得在第三章中,贝尔曼方程和动态规划部分,在价值函数与 Q 函数的关系这一节中,我们学习过如何从价值函数推导出 Q 函数吗?利用这一公式,我们可以将 Q 函数逼近为即时奖励和下一个状态的折扣价值之和。

将前面提到的 Q 值代入优势函数中的方程(1),我们可以写出如下公式:

因此,现在我们已经有了优势函数。我们了解到,在 A2C 中,我们使用优势函数计算策略梯度。因此,我们可以写出如下公式:

展开优势函数,我们可以写出如下公式:

如我们所见,我们的策略梯度现在是使用优势函数来计算的:

现在,检查前面的公式与我们在上一部分中计算梯度的方法。我们可以观察到两者本质上是相同的。因此,A2C 方法与我们在前一部分学到的内容是一样的。

异步优势演员-评论员(A3C)

异步优势演员-评论员,以下简称 A3C,是一种流行的演员-评论员算法。异步优势演员-评论员方法背后的主要思想是,它使用多个代理并行学习,并汇总它们的整体经验。

在 A3C 中,我们将有两种类型的网络,一种是全局网络(全局代理),另一种是工作网络(工作代理)。我们将有多个工作代理,每个工作代理使用不同的探索策略,并在它们自己的环境副本中进行学习并收集经验。然后,从这些工作代理获得的经验将被汇总并发送到全局代理。全局代理汇总这些学习。

现在我们对 A3C 的工作原理有了一个基本的了解,让我们深入细节。

三个 A

在深入探讨之前,我们先了解一下 A3C 中的三个 A 代表什么。

异步:异步意味着 A3C 的工作方式。也就是说,A3C 并不是只有一个代理尝试学习最优策略,而是有多个代理与环境进行交互。由于我们有多个代理同时与环境进行交互,因此我们为每个代理提供环境副本,这样每个代理就可以与它们自己的环境副本进行交互。因此,这些多个代理被称为工作代理,我们有一个单独的代理称为全局代理。所有工作代理异步地向全局代理报告,全局代理汇总这些学习。

优势:我们已经在前一部分学过什么是优势函数。优势函数可以定义为 Q 函数与价值函数之间的差异。

演员-评论员:每个工作网络(工作代理)和全局网络(全局代理)基本上遵循演员-评论员架构。也就是说,每个代理由一个演员网络用于估计策略和一个评论员网络用于评估由演员网络生成的策略组成。

现在,让我们继续了解 A3C 的架构,并详细理解 A3C 是如何工作的。

A3C 的架构

现在,让我们理解 A3C 的架构。A3C 的架构如下图所示:

图 11.3:A3C 的架构

正如我们从前面的图中看到的,我们有多个工作代理,每个工作代理与它们自己的环境副本进行交互并收集经验。我们还可以看到,每个工作代理遵循演员-评论员架构。因此,工作代理计算演员网络损失(策略损失)和评论员网络损失(价值损失)。

在前一节中,我们学到了如何通过计算策略梯度来更新我们的演员网络:

因此,演员损失基本上是以下内容:

如我们所观察到的,演员损失是动作的对数概率与 TD(时间差分)误差的乘积。现在,我们在演员损失中加入一个新项,即策略的熵(随机性度量),并将演员损失重新定义为:

其中 表示策略的熵。加入策略的熵可以促进足够的探索,参数 用于控制熵的显著性。

评论家损失仅仅是均方 TD 误差。

在计算完演员和评论家网络的损失后,工作代理计算损失的梯度,然后将这些梯度发送给全局代理。也就是说,工作代理计算梯度,并将其梯度异步地累积到全局代理中。全局代理使用从工作代理异步接收到的梯度更新其参数。然后,全局代理周期性地将更新后的参数发送给工作代理,这样工作代理就会得到更新。

通过这种方式,每个工作代理计算损失、计算梯度,并异步地将这些梯度发送给全局代理。然后,全局代理通过从工作代理接收到的梯度更新其参数。接着,全局代理周期性地将更新后的参数发送给工作代理。

由于我们有许多工作代理与它们自己的环境副本进行交互,并将信息汇聚到全局网络,因此经验之间几乎没有相关性。

A3C 涉及的步骤如下:

  1. 工作代理与它们自己的环境副本进行交互。

  2. 每个工作代理遵循不同的策略并收集经验。

  3. 接下来,工作代理计算演员和评论家网络的损失。

  4. 计算完损失后,他们计算损失的梯度,并异步地将这些梯度发送给全局代理。

  5. 全局代理使用从工作代理接收到的梯度更新其参数。

  6. 现在,全局代理更新后的参数将周期性地发送给工作代理。

我们重复上述步骤进行若干次迭代,以找到最优策略。为了更清晰地了解 A3C 是如何工作的,在下一节中,我们将学习如何实现它。

使用 A3C 攀爬山地车

让我们为山地车爬坡任务实现 A3C 算法。在山地车爬坡环境中,一辆车被放置在两座山之间,代理的目标是驱车爬上右侧的山。但是问题是,代理不能一次性爬上山。所以,代理必须来回驾驶以积累动能,才能爬上右侧的山。如果代理在上山时消耗较少的能量,则会分配更高的奖励。图 11.4展示了山地车环境:

图 11.4:山地车环境

本节使用的代码改编自 Stefan Boschenriedter 提供的 A3C 开源实现(github.com/stefanbo92/A3C-Continuous)。

首先,让我们导入必要的库:

import warnings
warnings.filterwarnings('ignore')
import gym
import multiprocessing
import threading
import numpy as np
import os
import shutil
import matplotlib.pyplot as plt
import tensorflow.compat.v1 as tf
tf.disable_v2_behavior() 

创建山地车环境

让我们使用 Gym 创建一个山地车环境。请注意,我们的山地车环境是连续环境,意味着我们的动作空间是连续的:

env = gym.make('MountainCarContinuous-v0') 

获取环境的状态形状:

state_shape = env.observation_space.shape[0] 

获取环境的动作形状:

action_shape = env.action_space.shape[0] 

请注意,我们创建了连续的山地车环境,因此我们的动作空间由连续值组成。所以,我们获取动作空间的边界:

action_bound = [env.action_space.low, env.action_space.high] 

定义变量

现在,让我们定义一些重要的变量。

定义工作者的数量为 CPU 的数量:

num_workers = multiprocessing.cpu_count() 

定义回合数:

num_episodes = 2000 

定义时间步长的数量:

num_timesteps = 200 

定义全球网络(全球代理)作用域:

global_net_scope = 'Global_Net' 

定义我们希望更新全球网络的时间步长:

update_global = 10 

定义折扣因子,

gamma = 0.90 

定义 beta 值:

beta = 0.01 

定义我们希望存储日志的目录:

log_dir = 'logs' 

定义演员-评论家类

我们了解到,在 A3C 中,全球网络和工作代理都遵循演员-评论家架构。所以,让我们定义一个名为ActorCritic的类,在其中实现演员-评论家算法。为了清晰理解,让我们逐行查看代码。你也可以从本书的 GitHub 仓库中获取完整的代码。

class ActorCritic(object): 

定义初始化方法:

首先,让我们定义初始化方法:

 def __init__(self, scope, sess, globalAC=None): 

初始化 TensorFlow 会话:

 self.sess=sess 

将演员网络优化器定义为 RMS prop:

 self.actor_optimizer = tf.train.RMSPropOptimizer(0.0001, name='RMSPropA') 

将评论家网络优化器定义为 RMS prop:

 self.critic_optimizer = tf.train.RMSPropOptimizer(0.001, name='RMSPropC') 

如果作用域是全球网络(全球代理):

 if scope == global_net_scope:
            with tf.variable_scope(scope): 

定义状态的占位符:

 self.state = tf.placeholder(tf.float32, [None, state_shape], 'state') 

构建全球网络(全球代理),并获取演员和评论家参数:

 self.actor_params, self.critic_params = self.build_network(scope)[-2:] 

如果网络不是全球网络,则:

 else:
            with tf.variable_scope(scope): 

定义状态的占位符:

 self.state = tf.placeholder(tf.float32, [None, state_shape], 'state') 

我们了解到我们的环境是连续环境,因此我们的演员网络(策略网络)返回动作的均值和方差,然后我们根据这些均值和方差构建动作分布,并基于该分布选择动作。

定义占位符以获取动作分布:

 self.action_dist = tf.placeholder(tf.float32, [None, action_shape], 'action') 

定义目标值的占位符:

 self.target_value = tf.placeholder(tf.float32, [None, 1], 'Vtarget') 

构建工作网络(工作代理),并获取动作的均值和方差、状态的值以及演员和评论家网络的参数:

 mean, variance, self.value, self.actor_params, self.critic_params = self.build_network(scope) 

计算时间差误差(TD 误差),它是状态的目标值与预测值之间的差:

 td_error = tf.subtract(self.target_value, self.value, name='TD_error') 

现在,让我们定义评论家网络的损失:

 with tf.name_scope('critic_loss'):
                    self.critic_loss = tf.reduce_mean(tf.square(td_error)) 

基于动作的均值和方差创建正态分布:

 normal_dist = tf.distributions.Normal(mean, variance) 

现在,让我们定义演员网络的损失。我们了解到,演员网络的损失定义为:

 with tf.name_scope('actor_loss'): 

计算动作的对数概率:

 log_prob = normal_dist.log_prob(self.action_dist) 

定义策略的熵:

 entropy_pi = normal_dist.entropy() 

计算演员网络损失:

 self.loss = log_prob * td_error + (beta * entropy_pi)
                    self.actor_loss = tf.reduce_mean(-self.loss) 

基于正态分布选择动作:

 with tf.name_scope('select_action'):
                    self.action = tf.clip_by_value(tf.squeeze(normal_dist.sample(1), axis=0), action_bound[0], action_bound[1]) 

计算工作代理(本地代理)演员和评论家网络损失的梯度:

 with tf.name_scope('local_grad'):
                    self.actor_grads = tf.gradients(self.actor_loss, self.actor_params)
                    self.critic_grads = tf.gradients(self.critic_loss, self.critic_params) 

现在,让我们执行同步操作:

 with tf.name_scope('sync'): 

在计算完演员网络和评论家网络的损失的梯度后,工作代理将这些梯度发送(推送)到全局代理:

 with tf.name_scope('push'):
                    self.update_actor_params = self.actor_optimizer.apply_gradients(zip(self.actor_grads, globalAC.actor_params))
                    self.update_critic_params = self.critic_optimizer.apply_gradients(zip(self.critic_grads, globalAC.critic_params)) 

全局代理使用从工作代理(本地代理)接收到的梯度更新他们的参数。然后,工作代理从全局代理拉取更新后的参数:

 with tf.name_scope('pull'):
                    self.pull_actor_params = [l_p.assign(g_p) for l_p, g_p in zip(self.actor_params, globalAC.actor_params)]
                    self.pull_critic_params = [l_p.assign(g_p) for l_p, g_p in zip(self.critic_params, globalAC.critic_params)] 

构建网络

现在,让我们定义构建演员-评论家网络的函数:

 def build_network(self, scope): 

初始化权重:

 w_init = tf.random_normal_initializer(0., .1) 

定义演员网络,它返回动作的均值和方差:

 with tf.variable_scope('actor'):
            l_a = tf.layers.dense(self.state, 200, tf.nn.relu, kernel_initializer=w_init, name='la')
            mean = tf.layers.dense(l_a, action_shape, tf.nn.tanh,kernel_initializer=w_init, name='mean')
            variance = tf.layers.dense(l_a, action_shape, tf.nn.softplus, kernel_initializer=w_init, name='variance') 

定义评论家网络,它返回状态的值:

 with tf.variable_scope('critic'):
            l_c = tf.layers.dense(self.state, 100, tf.nn.relu, kernel_initializer=w_init, name='lc')
            value = tf.layers.dense(l_c, 1, kernel_initializer=w_init, name='value') 

获取演员和评论家网络的参数:

 actor_params = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope=scope + '/actor')
        critic_params = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope=scope + '/critic') 

返回由演员网络生成的动作的均值和方差,评论家网络计算的状态值,以及演员和评论家网络的参数:

 return mean, variance, value, actor_params, critic_params 

更新全局网络

让我们定义一个名为update_global的函数,用于通过工作网络计算的损失梯度更新全局网络的参数,即推送操作:

 def update_global(self, feed_dict):
        self.sess.run([self.update_actor_params, self.update_critic_params], feed_dict) 

更新工作网络

我们还定义了一个名为pull_from_global的函数,用于通过从全局网络拉取来更新工作网络的参数,即拉取操作:

 def pull_from_global(self):
        self.sess.run([self.pull_actor_params, self.pull_critic_params]) 

选择动作

定义一个名为select_action的函数来选择动作:

 def select_action(self, state):

        state = state[np.newaxis, :]

        return self.sess.run(self.action, {self.state: state})[0] 

定义工作类

让我们定义一个名为Worker的类,在这里我们将实现工作代理:

class Worker(object): 

定义初始化方法

首先,让我们定义init方法:

 def __init__(self, name, globalAC, sess): 

我们了解到,每个工作代理与他们自己的环境副本一起工作。所以,让我们创建一个山地车环境:

 self.env = gym.make('MountainCarContinuous-v0').unwrapped 

定义工作代理的名称:

 self.name = name 

创建我们的ActorCritic类的对象:

 self.AC = ActorCritic(name, sess, globalAC) 

初始化 TensorFlow 会话:

 self.sess=sess 

定义一个名为work的函数供工作代理学习:

 def work(self):
        global global_rewards, global_episodes 

初始化时间步长:

 total_step = 1 

初始化一个列表来存储状态、动作和奖励:

 batch_states, batch_actions, batch_rewards = [], [], [] 

当全局回合数小于回合总数且协调器处于激活状态时:

 while not coord.should_stop() and global_episodes < num_episodes: 

通过重置环境来初始化状态:

 state = self.env.reset() 

初始化返回值:

 Return = 0 

对于环境中的每一步:

 for t in range(num_timesteps): 

仅渲染工作代理 0 的环境:

 if self.name == 'W_0':
                    self.env.render() 

选择动作:

 action = self.AC.select_action(state) 

执行所选动作:

 next_state, reward, done, _ = self.env.step(action) 

如果已经到达回合的最后一步,则将 done 设置为 True,否则设置为 False

 done = True if t == num_timesteps - 1 else False 

更新回报:

 Return += reward 

将状态、动作和奖励存储到列表中:

 batch_states.append(state)
                batch_actions.append(action)
                batch_rewards.append((reward+8)/8) 

现在,让我们更新全局网络。如果 doneTrue,则将下一个状态的值设置为 0,否则计算下一个状态的值:

 if total_step % update_global == 0 or done:
                    if done:
                        v_s_ = 0
                    else:
                        v_s_ = self.sess.run(self.AC.value, {self.AC.state: next_state[np.newaxis, :]})[0, 0] 

计算目标值,公式为

 batch_target_value = []
                    for reward in batch_rewards[::-1]:
                        v_s_ = reward + gamma * v_s_
                        batch_target_value.append(v_s_) 

反转目标值:

 batch_target_value.reverse() 

堆叠状态、动作和目标值:

 batch_states, batch_actions, batch_target_value = np.vstack(batch_states), np.vstack(batch_actions), np.vstack(batch_target_value) 

定义馈送字典:

 feed_dict = {
                                 self.AC.state: batch_states,
                                 self.AC. action_dist: batch_actions,
                                 self.AC.target_value: batch_target_value,
                                 } 

更新全局网络:

 self.AC.update_global(feed_dict) 

清空列表:

 batch_states, batch_actions, batch_rewards = [], [], [] 

通过从全局网络拉取参数来更新工作网络:

 self.AC.pull_from_global() 

将状态更新到下一个状态,并增加总步数:

 state = next_state
                total_step += 1 

更新全局奖励:

 if done:
                    if len(global_rewards) < 5:
                        global_rewards.append(Return)
                    else:
                        global_rewards.append(Return)
                        global_rewards[-1] =(np.mean(global_rewards[-5:]))

                    global_episodes += 1
                    break 

训练网络

现在,让我们开始训练网络。初始化全局奖励列表,并初始化全局回合计数器:

global_rewards = []
global_episodes = 0 

启动 TensorFlow 会话:

sess = tf.Session() 

创建全局代理:

with tf.device("/cpu:0"):

    global_agent = ActorCritic(global_net_scope,sess) 

创建 n 个工作代理:

 worker_agents = []
    for i in range(num_workers):
        i_name = 'W_%i' % i
        worker_agents.append(Worker(i_name, global_agent,sess)) 

创建 TensorFlow 协调器:

coord = tf.train.Coordinator() 

初始化所有 TensorFlow 变量:

sess.run(tf.global_variables_initializer()) 

将 TensorFlow 计算图存储在日志目录中:

if os.path.exists(log_dir):
    shutil.rmtree(log_dir)
tf.summary.FileWriter(log_dir, sess.graph) 

现在,运行工作线程:

worker_threads = []
for worker in worker_agents:
    job = lambda: worker.work()
    t = threading.Thread(target=job)
    t.start()
    worker_threads.append(t)
coord.join(worker_threads) 

为了更好地理解 A3C 架构,让我们在下一节中查看 A3C 的计算图。

可视化计算图

正如我们所观察到的,我们有四个工作代理(工作网络)和一个全局代理(全局网络):

图 11.5:A3C 的计算图

让我们来看看工作代理的架构。正如我们所观察到的,工作代理遵循演员-评论家架构:

图 11.6:A3C 的计算图,展示了展开的 W_0 节点

现在,让我们检查同步节点。正如 图 11.7 所示,我们在同步节点中有两个操作,分别是 推送拉取

图 11.7:A3C 的计算图,展示了同步节点的推送和拉取操作

在计算了演员和评论家网络的损失的梯度之后,工作代理将这些梯度推送到全局代理:

图 11.8:A3C 的计算图——工作代理将其梯度推送到全局代理

全局代理通过从工作代理接收到的梯度来更新其参数。然后,工作代理从全局代理拉取更新后的参数:

图 11.9:A3C 的计算图——工作代理从全局代理拉取更新后的参数

现在我们已经了解了 A3C 的工作原理,在接下来的章节中,让我们回顾一下 A2C 方法。

再次审视 A2C

我们可以像 A3C 算法一样设计具有多个工作代理的 A2C 算法。然而,不同于 A3C,A2C 是一个同步算法,意味着在 A2C 中,我们可以有多个工作代理,每个工作代理与它们自己的环境副本进行交互,且所有工作代理执行同步更新,而 A3C 中工作代理执行的是异步更新。

也就是说,在 A2C 中,每个工作代理与环境互动,计算损失并计算梯度。然而,它不会独立地将这些梯度发送到全局网络。相反,它会等待所有其他工作代理完成工作,然后以同步的方式将权重更新到全局网络。进行同步的权重更新可以减少 A3C 引入的不一致性。

总结

我们通过理解演员-评论员方法是什么开始了本章的学习。我们了解到,在演员-评论员方法中,演员计算最优策略,而评论员通过估计价值函数来评估演员网络计算的策略。接下来,我们学习了演员-评论员方法与带基线的策略梯度方法的不同之处。

我们了解到,在带基线的策略梯度方法中,首先生成完整的回合(轨迹),然后更新网络的参数。而在演员-评论员方法中,我们在每一步回合中更新网络的参数。接下来,我们了解了优势演员-评论员算法是什么,以及它如何在梯度更新中使用优势函数。

在本章结束时,我们了解了另一种有趣的演员-评论员算法,称为异步优势演员-评论员方法。我们了解到,A3C 由多个工作代理和一个全局代理组成。所有工作代理异步地将它们的梯度发送到全局代理,然后全局代理使用从工作代理接收到的梯度更新它们的参数。更新参数后,全局代理会定期将更新后的参数发送给工作代理。

因此,在本章中,我们了解了两种有趣的演员-评论员算法——A2C 和 A3C。在下一章中,我们将了解几种最先进的演员-评论员算法,包括 DDPG、TD3 和 SAC。

问题

让我们通过回答以下问题来评估我们对演员-评论员方法的理解:

  1. 演员-评论员方法是什么?

  2. 演员网络和评论员网络的作用是什么?

  3. 演员-评论员方法与基线策略梯度方法有何不同?

  4. 演员网络的梯度更新方程是什么?

  5. A2C 是如何工作的?

  6. 异步在 A3C 中意味着什么?

  7. A2C 与 A3C 有何不同?

进一步阅读

若要了解更多,请参考以下论文:

第十二章:学习 DDPG、TD3 和 SAC

在前一章中,我们了解了有趣的演员-评论家方法,如 优势演员-评论家 (A2C) 和 异步优势演员-评论家 (A3C)。在本章中,我们将学习几种最先进的演员-评论家方法。我们将从理解一种流行的演员-评论家方法 深度确定性策略梯度 (DDPG) 开始。DDPG 仅在连续环境中使用,即具有连续动作空间的环境。我们将详细了解 DDPG 是什么以及它如何工作。我们还将逐步学习 DDPG 算法。

接下来,我们将了解 双延迟深度确定性策略梯度 (TD3)。TD3 是对 DDPG 算法的改进,包括解决 DDPG 中面临的几个问题的一些有趣特性。我们将详细了解 TD3 的关键特性,还将逐步学习 TD3 的算法。

最后,我们将学习另一种有趣的演员-评论家算法,称为 软演员-评论家 (SAC)。我们将学习 SAC 是什么以及它如何通过目标函数中的熵项工作。我们将详细了解 SAC 的演员和评论家组件,然后逐步学习 SAC 算法。

在本章中,我们将学习以下主题:

  • 深度确定性策略梯度 (DDPG)

  • DDPG 的组成部分

  • DDPG 算法

  • 双延迟深度确定性策略梯度 (TD3)

  • TD3 的关键特性

  • TD3 算法

  • 软演员-评论家(SAC)

  • SAC 的组成部分

  • SAC 算法

深度确定性策略梯度

DDPG 是一种离策略、无模型的算法,专为动作空间连续的环境设计。在前一章中,我们学习了演员-评论家方法的工作原理。DDPG 是一种演员-评论家方法,其中演员使用策略梯度估计策略,评论家使用 Q 函数评估演员产生的策略。

DDPG 使用策略网络作为演员和深度 Q 网络作为评论家。我们在前一章中学到的 DPPG 和演员-评论家算法之间的一个重要区别是,DDPG 尝试学习确定性策略而不是随机策略。

首先,我们将直观地了解 DDPG 的工作原理,然后详细学习算法。

DDPG 概述

DDPG 是一种演员-评论家方法,充分利用了基于策略的方法和基于值的方法。它使用确定性策略 而不是随机策略

我们了解到,确定性策略告诉代理在给定状态下执行一种特定的动作,这意味着确定性策略将状态映射到一个特定的动作:

而随机策略将状态映射到动作空间上的概率分布:

在确定性策略中,每当智能体访问某个状态时,它总是执行相同的特定动作。但是在随机策略中,智能体不是每次访问状态时都执行相同的动作,而是根据动作空间中的概率分布每次执行不同的动作。

现在,我们将概览 DDPG 算法中的演员和评论员网络。

演员

DDPG 中的演员本质上就是策略网络。演员的目标是学习状态与动作之间的映射关系。也就是说,演员的作用是学习能带来最大回报的最优策略。因此,演员使用策略梯度方法来学习最优策略。

评论员

评论员本质上就是价值网络。评论员的目标是评估演员网络所产生的动作。评论员网络如何评估演员网络产生的动作呢?假设我们有一个 Q 函数,我们能否使用 Q 函数来评估一个动作呢?当然可以!首先,让我们稍微绕一下路,回顾一下 Q 函数的使用。

我们知道,Q 函数给出了一个智能体从状态s开始并执行动作a时,按照特定策略所获得的预期回报。Q 函数产生的预期回报通常称为 Q 值。因此,给定一个状态和动作,我们可以得到一个 Q 值:

  • 如果 Q 值很高,我们可以说在该状态下执行的动作是一个好的动作。也就是说,如果 Q 值很高,意味着当我们在状态s中执行动作a时,预期回报很高,我们可以说动作a是一个好的动作。

  • 如果 Q 值很低,我们可以说在该状态下执行的动作不是一个好的动作。也就是说,如果 Q 值很低,意味着当我们在状态s中执行动作a时,预期回报很低,我们可以说动作a不是一个好的动作。

好的,那么评论网络如何基于 Q 函数(Q 值)来评估演员网络所产生的动作呢?假设演员网络在状态A下执行了一个down动作。那么,现在,评论网络计算在状态A下执行down动作的 Q 值。如果 Q 值很高,那么评论网络会给演员网络反馈,表示在状态A下,down动作是一个好的动作。如果 Q 值很低,那么评论网络会给演员网络反馈,表示在状态A下,down动作不是一个好的动作,因此演员网络会尝试在状态A下执行一个不同的动作。

因此,借助 Q 函数,评论网络可以评估演员网络执行的动作。但是,等等,评论网络怎么学习 Q 函数呢?因为只有当它知道 Q 函数时,才能评估演员执行的动作。那么,评论网络如何学习 Q 函数呢?这就是我们使用深度 Q 网络DQN)的地方。我们了解到,利用 DQN,可以使用神经网络来近似 Q 函数。因此,现在我们使用 DQN 作为评论网络来计算 Q 函数。

因此,简而言之,DDPG 是一种演员-评论方法,它结合了基于策略和基于价值的方法。DDPG 由演员(一个策略网络)和评论(一个深度 Q 网络)组成,演员通过策略梯度方法来学习最优策略,评论则评估演员产生的动作。

DDPG 组件

现在我们对 DDPG 算法的基本工作原理有了了解,接下来我们将深入探讨。通过分别查看演员和评论网络,我们将更好地理解它们的具体工作原理。

评论网络

我们了解到,评论网络基本上就是 DQN,它利用 DQN 来估计 Q 值。现在,让我们更详细地了解评论网络如何使用 DQN 来估计 Q 值,并回顾一下 DQN。

评论网络评估演员产生的动作。因此,评论的输入将是状态以及在该状态下由演员产生的动作,评论返回给定状态-动作对的 Q 值,如图 12.1所示:

图 12.1:评论网络

为了在评论中近似 Q 值,我们可以使用深度神经网络,如果我们用深度神经网络来近似 Q 值,那么这个网络就叫做 DQN。由于我们使用神经网络来近似评论中的 Q 值,所以我们可以用 来表示 Q 函数,其中 是网络的参数。

因此,在评论网络中,我们使用 DQN 来近似 Q 值,评论网络的参数由 表示,如图 12.2所示:

图 12.2:评论网络

如从图 12.2中可以观察到,给定状态 s 和由演员生成的动作 a,评论网络返回 Q 值。

现在,让我们看看如何获得演员产生的动作 a。我们了解到,演员基本上是策略网络,它通过策略梯度方法来学习最优策略。在 DDPG 中,我们学习的是确定性策略,而不是随机策略,因此我们可以用 来表示策略,而不是 。演员网络的参数由 表示。因此,我们可以将参数化策略表示为

给定一个状态 s 作为输入,演员网络返回在该状态下执行的动作 a

因此,评论家网络将状态 s 和演员网络在该状态下产生的动作 作为输入,返回 Q 值,如 图 12.3 所示:

图 12.3:评论家网络

好的,我们如何训练评论家网络(DQN)呢?我们通常通过最小化损失来训练网络,损失是目标值和预测值之间的差异。因此,我们可以通过最小化损失来训练评论家网络,损失是目标 Q 值与网络预测的 Q 值之间的差异。但我们如何获得目标 Q 值呢?目标 Q 值是最优 Q 值,我们可以使用贝尔曼方程来获得最优 Q 值。

我们了解到,最优 Q 函数(Q 值)可以通过使用贝尔曼最优性方程来获得。因此,最优 Q 函数可以通过以下贝尔曼最优性方程来获得:

我们知道, 表示我们在状态 s 中执行动作 a 并转移到下一个状态 时获得的即时奖励 r,因此我们可以将 简单表示为 r

在上面的方程中,我们可以去除期望。我们将通过从重放缓冲区中采样 K 数量的过渡状态并取平均值来逼近期望。稍后我们将更详细地了解这个过程。所以,我们可以将目标 Q 值表示为即时奖励和下一个状态-动作对的折扣最大 Q 值的和,如下所示:

因此,我们可以将评论家网络的损失函数表示为目标值(最优贝尔曼 Q 值)和预测值(评论家网络预测的 Q 值)之间的差异:

这里,动作 a 是由演员网络产生的动作,即

我们可以不将损失作为目标值和预测值之间的差异,而是将均方误差作为我们的损失函数。我们知道,在 DQN 中,我们使用重放缓冲区并存储过渡状态为 。因此,我们从重放缓冲区中随机抽取一个 K 数量的过渡状态小批量,并通过最小化目标值(最优贝尔曼 Q 值)和预测值(评论家网络预测的 Q 值)之间的均方损失来训练网络。因此,我们的损失函数表示为:

从前面的方程中,我们可以观察到目标 Q 函数和预测的 Q 函数都由相同的参数 参数化。这将导致均方误差的不稳定,网络的学习效果会很差。

因此,我们引入了另一个神经网络来学习目标值,通常称为目标评论家网络。目标评论家网络的参数用表示。我们的主要评论家网络用于预测 Q 值,并通过梯度下降学习正确的参数。目标评论家网络参数通过直接复制主要评论家网络的参数来更新。

因此,评论家网络的损失函数可以写作:

请记住,前面方程中的动作a[i]是由演员网络生成的动作,即!

由于最大项的存在,在目标值计算中有一个小问题,如下所示:

最大项意味着我们计算所有可能动作的 Q 值!,在状态!下,并选择具有最大 Q 值的动作!。但是,当动作空间是连续时,我们无法计算所有可能动作!在状态!下的 Q 值。因此,我们需要去掉损失函数中的最大项。我们该如何做呢?

正如我们在评论家中使用目标网络一样,我们也可以使用目标演员网络,目标演员网络的参数用表示。现在,我们不再选择具有最大 Q 值的动作!,而是可以使用目标演员网络生成一个动作!,即!

因此,如图 12.4所示,为了计算目标中的下一个状态-动作对的 Q 值,我们将状态!和由目标演员网络(参数化为!)生成的动作!输入到目标评论家网络中,它返回下一个状态-动作对的 Q 值:

图 12.4:目标评论家网络

因此,在我们的损失函数中,方程(1),我们可以去掉最大项,而是将写作,如下所示:

为了保持符号的一致性,我们用J表示损失函数:

为了减少杂乱,我们可以用y表示目标值,并写作:

其中y[i]是评论家的目标值,即!,而动作a[i]是由主要演员网络生成的动作,即!

为了最小化损失,我们计算目标函数的梯度,并通过梯度下降更新主要的评论家网络参数

好的,那目标评论网络的参数呢?我们如何更新它?我们可以通过直接复制主评论网络参数的参数来更新目标评论网络的参数,如下所示:

这通常被称为软替代,并且的值通常设置为 0.001。

因此,我们学到评论网络如何使用 DQN 来计算 Q 值,以评估演员网络产生的动作。在下一节中,我们将学习演员网络如何学习最优策略。

演员网络

我们已经学到演员网络是策略网络,它使用策略梯度来计算最优策略。我们还学到我们通过表示演员网络的参数,因此参数化的策略表示为

演员网络以状态 s 作为输入,并返回动作 a

这里我们可能需要注意的一个重要点是,我们使用的是确定性策略。由于我们使用的是确定性策略,我们需要处理探索-开发困境,因为我们知道确定性策略总是选择相同的动作,而不会探索新的动作,这与基于动作空间概率分布选择不同动作的随机策略不同。

好的,在使用确定性策略时,我们如何探索新的动作呢?请注意,DDPG 是为动作空间连续的环境设计的。因此,我们在连续动作空间中使用确定性策略。

与离散动作空间不同,在连续动作空间中,我们有连续的值。因此,为了探索新的动作,我们可以直接在演员网络产生的动作上添加一些噪声,因为动作是一个连续值。我们通过一种叫做奥恩斯坦-乌伦贝克随机过程的方式生成这个噪声。因此,我们修改后的动作可以表示为:

例如,假设演员网络产生的动作是 13。假设噪声是 0.1,那么我们的动作变成了 a = 13+0.1 = 13.1。

我们已经学到评论网络通过表示,它使用 Q 值来评估演员产生的动作。如果 Q 值很高,那么评论网络告诉演员它产生了一个好的动作;但当 Q 值很低时,评论网络告诉演员它产生了一个不好的动作。

但等等!我们了解到当动作空间是连续时,计算 Q 值是困难的。也就是说,当动作空间是连续的时,计算状态中所有可能动作的 Q 值并取最大 Q 值是困难的。这就是为什么我们转向策略梯度方法。但现在,我们正在计算具有连续动作空间的 Q 值。这会怎么样?

请注意,在 DDPG 中,我们并不计算所有可能的状态-动作对的 Q 值。我们只是简单地计算演员网络生成的状态 s 和动作 a 的 Q 值。

演员的目标是让评论者说出它产生的动作是一个好动作。也就是说,演员希望从评论者网络获得良好的反馈。评论者何时会给演员良好的反馈?当演员产生的动作具有最大的 Q 值时,评论者会给出良好的反馈。因此,演员试图以一种方式生成动作,使得它可以最大化评论者生成的 Q 值。

因此,演员的目标函数是生成一个动作,最大化评论者网络生成的 Q 值。因此,我们可以将演员的目标函数写为:

其中动作为 。最大化上述目标函数 意味着我们在最大化评论者网络生成的 Q 值。好的,我们如何最大化前述目标函数呢?我们可以通过执行梯度上升来最大化目标函数,并更新演员网络参数如下:

等等。我们不仅仅为单个状态 更新演员网络参数 ,而是从重播缓冲区 中采样 个状态并更新参数。因此,我们的目标函数现在变成:

其中动作为 。最大化前述目标函数意味着演员试图以一种方式生成动作,使得在所有采样状态下最大化 Q 值。我们可以通过执行梯度上升来最大化目标函数,并更新演员网络参数如下:

总结一下,演员的目标是以一种方式生成动作,使得它最大化评论者生成的 Q 值。因此,我们执行梯度上升,并更新演员网络参数。

好的,那么目标演员网络的参数如何更新?我们可以通过软替换仅复制主要演员网络参数 来更新目标演员网络参数,如下所示:

现在我们已经理解了演员和评论家网络是如何工作的,让我们整理一下到目前为止所学的内容,并通过将所有概念整合起来,来深入理解 DDPG 是如何工作的。

将所有内容整合在一起

为了避免在符号中迷失,首先,让我们回顾一下符号,以便更好地理解 DDPG。我们使用四个网络,两个演员网络和两个评论家网络:

  • 主评论家网络参数表示为

  • 目标评论家网络参数表示为

  • 主演员网络参数表示为

  • 目标演员网络参数表示为

请注意,DDPG 是一种演员-评论家方法,因此它的参数将在每个回合的每一步都更新,这与策略梯度方法不同,后者是先生成完整的回合,然后再更新参数。好了,让我们开始,理解 DDPG 是如何工作的。

首先,我们用随机值初始化主评论家网络参数 和主演员网络参数 。我们了解到,目标网络参数只是主网络参数的副本。因此,我们通过简单地复制主评论家网络参数 来初始化目标评论家网络参数 。类似地,我们通过复制主演员网络参数 来初始化目标演员网络参数 。我们还初始化了回放缓冲区

现在,在每个回合的每一步,首先,我们使用演员网络选择一个动作 a

然而,为了确保探索,我们不是直接使用动作 a,而是添加一些噪声 ,因此动作变为:

然后,我们执行动作 a,移动到下一个状态 ,并获得奖励 r。我们将这个过渡信息存储在回放缓冲区 中。

接下来,我们从回放缓冲区随机采样一个 K 过渡(sars')的小批量。这些 K 个过渡将用于更新评论家和演员网络。

首先,让我们计算评论家网络的损失。我们了解到,评论家网络的损失函数是:

其中 y[i] 是评论家的目标值,即 ,而动作 a[i] 是由演员网络生成的动作,即

在计算评论家网络的损失后,我们计算梯度 ,并使用梯度下降法更新评论家网络参数

现在,让我们更新演员网络。我们了解到,演员网络的目标函数是:

请注意,在上述方程中,我们仅使用从采样的 K 过渡 (s, a, r, s') 中的状态 (s[i])。动作 a 由演员网络选择,。现在,我们需要最大化前面的目标函数。最大化上述目标函数有助于演员以一种方式生成动作,从而最大化评论员生成的 Q 值。我们可以通过计算目标函数 的梯度并使用梯度上升更新演员网络参数 来最大化目标函数:

然后,在最后一步,我们通过软替换更新目标评论员网络参数 和目标演员网络参数

我们为多个回合重复这些步骤。因此,对于回合中的每一步,我们都会更新网络的参数。由于参数在每一步都会更新,我们的策略也会在每个回合的每一步得到改进。

为了更好地理解 DDPG 的工作原理,让我们在下一节中深入探讨 DDPG 算法。

算法 - DDPG

DDPG 算法如下所示:

  1. 初始化主评论员网络参数 和主演员网络参数

  2. 通过直接复制主评论员网络参数 初始化目标评论员网络参数

  3. 通过直接复制主演员网络参数 初始化目标演员网络参数

  4. 初始化重放缓冲区

  5. 对于 N 个回合,重复步骤 6 和 7。

  6. 初始化 Ornstein-Uhlenbeck 随机过程 ,用于动作空间的探索。

  7. 对于每个回合的每一步,即 t = 0,…,T – 1:

    1. 基于策略 和探索噪声选择动作 a,即

    2. 执行选定的动作 a,移动到下一个状态 ,获得奖励 r,并将这一过渡信息存储在重放缓冲区 中。

    3. 从重放缓冲区 随机抽取一个 K 过渡的小批量。

    4. 计算评论员的目标值,即

    5. 计算评论员网络的损失,

    6. 计算损失的梯度 ,并使用梯度下降更新评论员网络参数,

    7. 计算演员网络的梯度 ,并通过梯度上升更新演员网络参数,

    8. 更新目标评论员和目标演员网络参数,分别为

使用 DDPG 摆动一个摆锤

在本节中,让我们实现 DDPG 算法来训练智能体摆动一个摆锤。也就是说,我们将有一个从随机位置开始摆动的摆锤,智能体的目标是使摆锤摆动起来并保持直立。

首先,让我们导入所需的库:

import warnings
warnings.filterwarnings('ignore')
import tensorflow as tf
tf.compat.v1.disable_v2_behavior() 
import numpy as np
import gym 

创建 Gym 环境

让我们使用 Gym 创建一个摆锤环境:

env = gym.make("Pendulum-v0").unwrapped 

获取环境的状态形状:

state_shape = env.observation_space.shape[0] 

获取环境的动作形状:

action_shape = env.action_space.shape[0] 

请注意,摆锤是一个连续环境,因此我们的动作空间由连续值组成。因此,我们获取动作空间的边界:

action_bound = [env.action_space.low, env.action_space.high] 

定义变量

现在,让我们定义一些重要的变量。

设置折扣因子

gamma = 0.9 

设置的值,用于软替代:

tau = 0.001 

设置我们的重放缓冲区的大小:

replay_buffer = 10000 

设置批量大小:

batch_size = 32 

定义 DDPG 类

让我们定义一个名为DDPG的类,在其中实现 DDPG 算法。为了便于理解,让我们逐行查看代码。你也可以通过本书的 GitHub 仓库访问完整代码:

class DDPG(object): 

定义初始化方法

首先,让我们定义init方法:

 def __init__(self, state_shape, action_shape, high_action_value,): 

定义重放缓冲区,用于存储转移:

 self.replay_buffer = np.zeros((replay_buffer, state_shape * 2 + action_shape + 1), dtype=np.float32) 

num_transitions初始化为0,这意味着我们的重放缓冲区中的转移数为零:

 self.num_transitions = 0 

启动 TensorFlow 会话:

 self.sess = tf.Session() 

我们了解到,在 DDPG 中,为了确保探索,而不是直接选择动作a,我们通过使用奥恩斯坦-乌伦贝克过程添加了一些噪声。因此,我们首先初始化噪声:

 self.noise = 3.0 

然后,初始化状态形状、动作形状和高动作值:

 self.state_shape, self.action_shape, self.high_action_value = state_shape, action_shape, high_action_value 

定义状态的占位符:

 self.state = tf.placeholder(tf.float32, [None, state_shape], 'state') 

定义下一个状态的占位符:

 self.next_state = tf.placeholder(tf.float32, [None, state_shape], 'next_state') 

定义奖励的占位符:

 self.reward = tf.placeholder(tf.float32, [None, 1], 'reward') 

在演员变量作用域内:

 with tf.variable_scope('Actor'): 

定义主演员网络,该网络由进行参数化。演员网络以状态为输入,并返回在该状态下执行的动作:

 self.actor = self.build_actor_network(self.state, scope='main', trainable=True) 

定义目标演员网络,该网络由进行参数化。目标演员网络以下一个状态为输入,并返回在该状态下执行的动作:

 target_actor = self.build_actor_network(self.next_state, scope='target', trainable=False) 

在评论变量作用域内:

 with tf.variable_scope('Critic'): 

定义主评论网络,该网络由进行参数化。评论网络以状态和演员在该状态下产生的动作作为输入,并返回 Q 值:

 critic = self.build_critic_network(self.state, self.actor, scope='main', trainable=True) 

定义目标评论网络,该网络由进行参数化。目标评论网络以下一个状态和目标演员网络在该下一个状态下产生的动作作为输入,并返回 Q 值:

 target_critic = self.build_critic_network(self.next_state, target_actor, scope='target', trainable=False) 

获取主演员网络的参数

 self.main_actor_params = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope='Actor/main') 

获取目标演员网络的参数

 self.target_actor_params = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope='Actor/target') 

获取主评论网络的参数

 self.main_critic_params = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope='Critic/main') 

获取目标评论家网络的参数

 self.target_critic_params = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope='Critic/target') 

执行软替换,更新目标演员网络的参数为 ,并更新目标评论家网络的参数为

 self.soft_replacement = [
            [tf.assign(phi_, tau*phi + (1-tau)*phi_), tf.assign(theta_, tau*theta + (1-tau)*theta_)]
            for phi, phi_, theta, theta_ in zip(self.main_actor_params, self.target_actor_params, self.main_critic_params, self.target_critic_params)
            ] 

计算目标 Q 值。我们了解到目标 Q 值可以通过将奖励与下一个状态-动作对的折扣 Q 值相加来计算,

 y = self.reward + gamma * target_critic 

现在,让我们计算评论家网络的损失。评论家网络的损失是目标 Q 值与预测 Q 值之间的均方误差:

因此,我们可以定义均方误差为:

 MSE = tf.losses.mean_squared_error(labels=y, predictions=critic) 

通过使用 Adam 优化器最小化均方误差来训练评论家网络:

 self.train_critic = tf.train.AdamOptimizer(0.01).minimize(MSE, name="adam-ink", var_list = self.main_critic_params) 

我们了解到,演员的目标函数是生成一个动作,使评论家网络产生的 Q 值最大化,如下所示:

动作发生在 ,我们可以通过计算梯度并进行梯度上升来最大化这个目标。然而,通常的约定是执行最小化而不是最大化。因此,我们可以通过仅添加一个负号,将前面的最大化目标转换为最小化目标。因此,我们可以定义演员网络目标为:

现在,我们可以通过计算梯度并执行梯度下降来最小化演员网络目标。因此,我们可以写出:

 actor_loss = -tf.reduce_mean(critic) 

通过使用 Adam 优化器最小化损失来训练演员网络:

 self.train_actor = tf.train.AdamOptimizer(0.001).minimize(actor_loss, var_list=self.main_actor_params) 

初始化所有的 TensorFlow 变量:

 self.sess.run(tf.global_variables_initializer()) 

选择动作

让我们定义一个叫做select_action的函数,通过加入噪声来选择动作,以确保探索:

 def select_action(self, state): 

运行演员网络并获取动作:

 action = self.sess.run(self.actor, {self.state: state[np.newaxis, :]})[0] 

现在,我们生成一个均值为动作、标准差为噪声的正态分布,并从该正态分布中随机选择一个动作:

 action = np.random.normal(action, self.noise) 

我们需要确保我们的动作不会超出动作的边界。所以,我们将动作裁剪到动作边界内,然后返回这个动作:

 action = np.clip(action, action_bound[0],action_bound[1])

        return action 

定义训练函数

现在,让我们定义训练函数:

 def train(self): 

执行软替换:

 self.sess.run(self.soft_replacement) 

从回放缓冲区中随机选择给定批次大小的索引:

 indices = np.random.choice(replay_buffer, size=batch_size) 

从回放缓冲区中选择具有选定索引的状态转移批次:

 batch_transition = self.replay_buffer[indices, :] 

获取状态、动作、奖励和下一个状态的批次:

 batch_states = batch_transition[:, :self.state_shape]
        batch_actions = batch_transition[:, self.state_shape: self.state_shape + self.action_shape]
        batch_rewards = batch_transition[:, -self.state_shape - 1: -self.state_shape]
        batch_next_state = batch_transition[:, -self.state_shape:] 

训练演员网络:

 self.sess.run(self.train_actor, {self.state: batch_states}) 

训练评论家网络:

 self.sess.run(self.train_critic, {self.state: batch_states, self.actor: batch_actions, self.reward: batch_rewards, self.next_state: batch_next_state}) 

存储状态转移

现在,让我们将状态转移存储到回放缓冲区中:

 def store_transition(self, state, actor, reward, next_state): 

首先,将状态、动作、奖励和下一个状态堆叠起来:

 trans = np.hstack((state,actor,[reward],next_state)) 

获取索引:

 index = self.num_transitions % replay_buffer 

存储状态转移:

 self.replay_buffer[index, :] = trans 

更新状态转移的数量:

 self.num_transitions += 1 

如果状态转移的数量大于回放缓冲区,则训练网络:

 if self.num_transitions > replay_buffer:
            self.noise *= 0.99995
            self.train() 

构建演员网络

我们定义一个叫做build_actor_network的函数来构建演员网络。演员网络接收状态并返回在该状态下执行的动作:

 def build_actor_network(self, state, scope, trainable):
        with tf.variable_scope(scope):
            layer_1 = tf.layers.dense(state, 30, activation = tf.nn.tanh, name = 'layer_1', trainable = trainable)
            actor = tf.layers.dense(layer_1, self.action_shape, activation = tf.nn.tanh, name = 'actor', trainable = trainable) 
            return tf.multiply(actor, self.high_action_value, name = "scaled_a") 

构建评论员网络

我们定义了一个名为build_critic_network的函数来构建评论员网络。评论员网络接收状态和由演员在该状态下产生的动作,并返回 Q 值:

 def build_critic_network(self, state, actor, scope, trainable):
        with tf.variable_scope(scope):
            w1_s = tf.get_variable('w1_s', [self.state_shape, 30], trainable = trainable)
            w1_a = tf.get_variable('w1_a', [self.action_shape, 30], trainable = trainable)
            b1 = tf.get_variable('b1', [1, 30], trainable = trainable)
            net = tf.nn.tanh( tf.matmul(state, w1_s) + tf.matmul(actor, w1_a) + b1 )
            critic = tf.layers.dense(net, 1, trainable = trainable)
            return critic 

训练网络

现在,让我们开始训练网络。首先,我们创建一个 DDPG 类的对象:

ddpg = DDPG(state_shape, action_shape, action_bound[1]) 

设置回合数:

num_episodes = 300 

设置每个回合的时间步数:

num_timesteps = 500 

对于每个回合:

for i in range(num_episodes): 

通过重置环境初始化状态:

 state = env.reset() 

初始化返回值:

 Return = 0 

每一步:

 for t in range(num_timesteps): 

渲染环境:

 env.render() 

选择动作:

 action = ddpg.select_action(state) 

执行选定的动作:

 next_state, reward, done, info = env.step(action) 

将过渡存储在回放缓冲区中:

 ddpg.store_transition(state, action, reward, next_state) 

更新返回值:

 Return += reward 

如果状态是终止状态,则中断:

 if done:
            break 

将状态更新到下一个状态:

 state = next_state 

打印每 10 个回合的返回值:

 if i %10 ==0:
         print("Episode:{}, Return: {}".format(i,Return)) 

通过渲染环境,我们可以观察到代理是如何学习摆动摆钟的:

图 12.5:Gym 摆钟环境

现在我们已经了解了 DDPG 是如何工作的以及如何实现它,在下一节中,我们将了解另一个有趣的算法,称为双延迟 DDPG。

双延迟 DDPG

现在,我们将深入了解另一个有趣的演员-评论员算法,称为 TD3。TD3 是对我们刚才讨论的 DDPG 算法的改进(基本上是继任者)。

在上一节中,我们了解了 DDPG 如何使用确定性策略在连续动作空间上工作。DDPG 有几个优点,并且已经成功地应用于各种连续动作空间环境。

我们理解到 DDPG 是一种演员-评论员方法,其中演员是一个策略网络,负责寻找最优策略,而评论员通过使用 DQN 估计 Q 函数来评估演员产生的策略。

DDPG 的一个问题是评论员高估了目标 Q 值。这种高估导致了几个问题。我们了解到,策略是基于评论员给出的 Q 值来改进的,但当 Q 值存在近似误差时,会导致策略的不稳定,且策略可能会收敛到局部最优解。

因此,为了解决这个问题,TD3 提出了三项重要的功能,分别是:

  1. 裁剪双 Q 学习

  2. 延迟策略更新

  3. 目标策略平滑

首先,我们将直观地了解 TD3 是如何工作的,然后再详细查看算法。

TD3 的关键特点

TD3 本质上与 DDPG 相同,不同之处在于它提出了三项重要功能来缓解 DDPG 中的问题。在本节中,我们首先了解 TD3 的关键特点。TD3 的三大关键特点是:

  • 裁剪双 Q 学习:我们不再使用一个评论员网络,而是使用两个主要的评论员网络来计算 Q 值,同时使用两个目标评论员网络来计算目标值。

    我们使用两个目标 Q 值来计算两个目标评论网络,并在计算损失时使用这两个中的最小值。这有助于防止目标 Q 值的高估。我们将在下一节中更详细地学习这一点。

  • 延迟策略更新:在 DDPG 中,我们了解到我们在每个 episode 的每一步都更新演员(策略网络)和评论员(DQN)网络的参数。与 DDPG 不同,在这里我们延迟更新演员网络的参数。

    也就是说,评论员网络的参数在每个 episode 的每一步都更新,而演员网络(策略网络)的参数则延迟更新,仅在每两步之后更新一次。

  • 目标策略平滑:DDPG 方法即使对于相同的动作也会产生不同的目标值。因此,即使对于相同的动作,目标值的方差也会很高,因此我们通过给目标动作添加一些噪声来减少这种方差。

    现在我们已经对 TD3 的关键特性有了一个基本的了解,我们将深入探讨这三个关键特性是如何工作的,并了解它们是如何解决与 DDPG 相关的问题的。

剪切双重 Q 学习

记得在第九章深度 Q 网络及其变种中,我们在学习 DQN 时发现,它倾向于高估目标状态-动作对的 Q 值吗?如图所示:

为了减轻高估问题,我们使用了双重 Q 学习。通过双重 Q 学习,我们使用两个不同的网络,换句话说,两个不同的 Q 函数,一个用于选择动作,另一个用于计算 Q 值,如图所示:

因此,通过使用前面的公式计算目标值可以防止 DQN 中 Q 值的高估。

我们了解到在 DDPG 中,评论员网络就是 DQN,因此它也会遭遇目标中的 Q 值高估问题。那么我们能否在 DDPG 中应用双重 Q 学习来尝试解决高估偏差呢?当然可以!但问题是,在演员-评论员方法中,策略和目标网络的参数更新过程较慢,这将不会帮助我们消除高估偏差。

所以,我们将使用一种稍微不同的双重 Q 学习版本,称为剪切双重 Q 学习。在剪切双重 Q 学习中,我们使用两个目标评论网络来计算 Q 值。

我们使用两个目标评论网络并计算两个 Q 值,从这两个 Q 值中选择最小值来计算目标值。这有助于防止高估偏差。让我们更详细地理解这一点。

如果我们需要两个目标评论网络,那么我们也需要两个主评论网络。我们知道,目标网络参数只是主网络参数的延迟副本。因此,我们定义了两个主评论网络,使用参数 来计算两个 Q 值,即 ,分别。

我们还定义了两个目标评论网络,使用参数 来计算目标中下一个状态-动作对的两个 Q 值,即 ,分别。让我们一步一步清楚地理解这一点。

在 DDPG 中,我们了解到目标值的计算方式是:

以这种方式计算目标中下一个状态-动作对的 Q 值会产生高估偏差:

因此,为了避免这种情况,在 TD3 中,首先,我们使用第一个目标评论网络计算下一个状态-动作对的 Q 值,使用的参数为 ,即 ,然后我们使用第二个目标评论网络计算下一个状态-动作对的 Q 值,使用的参数为 ,即 。然后,我们使用这两个 Q 值的最小值来计算目标值,如下所示:

其中动作为

我们可以简单地表示前面的方程为:

其中动作为

以这种方式计算目标值可以防止下一个状态-动作对的 Q 值被高估。

好的,我们计算了目标值。我们如何计算损失并更新评论网络参数?我们了解到我们使用两个主评论网络,因此,首先,我们计算第一个主评论网络的损失,使用参数

计算损失后,我们计算梯度,并使用梯度下降法以 更新参数

接下来,我们计算第二个主评论网络的损失,使用的参数为

计算损失后,我们计算梯度,并使用梯度下降法以 更新参数

我们可以简单地表示前面的更新为:

在更新了两个主评论网络参数后,,我们可以通过软替换来更新两个目标评论网络的参数,,如图所示:

我们可以简单地表示前面的更新为:

延迟策略更新

延迟策略更新意味着我们更新演员网络(策略网络)的参数频率低于评论员网络的参数更新频率。但为什么我们要这样做呢?我们了解到,在 DDPG 中,演员和评论员网络的参数在每个回合的每一步都会更新。

当评论员网络的参数不好时,它估计的 Q 值就是错误的。如果评论员网络估计的 Q 值不正确,那么演员网络就无法正确地更新它的参数。也就是说,我们了解到,演员网络是根据评论员网络的反馈来学习的。这个反馈就是 Q 值。当评论员网络提供错误的反馈(错误的 Q 值)时,演员网络就无法学习到正确的动作,也无法正确更新它的参数。

因此,为了避免这种情况,我们暂时不更新演员网络的参数,而是只更新评论员网络,以使评论员估计正确的 Q 值。也就是说,我们在每个回合的每一步都更新评论员网络的参数,而延迟更新演员网络的参数,仅在回合中的某些特定步骤进行更新,因为我们不希望演员从错误的评论员反馈中学习。

简而言之,评论员网络的参数在每个回合的每一步都会更新,但演员网络的参数更新是延迟的。我们通常将更新延迟两步。

好的,在 DDPG 中,我们学到演员网络(策略网络)的目标是最大化 Q 值:

演员网络的前述目标在 TD3 中也是相同的。也就是说,类似于 DDPG,这里的演员目标是生成动作,使其最大化由评论员生成的 Q 值。但等等!与 DDPG 不同的是,这里我们有两个 Q 值,,因为我们使用了两个评论员网络,参数分别为。那么,我们的演员网络应该最大化哪个 Q 值呢?是还是?我们可以选择其中一个进行最大化。所以,我们可以选择

因此,在 TD3 中,演员网络的目标是最大化 Q 值,,如下所示:

请记住,在上面的方程中,动作a是由演员网络选择的,。为了最大化目标函数,我们计算目标函数的梯度,,并使用梯度上升法更新网络的参数:

现在,我们不再在每个时间步更新演员网络的参数,而是延迟更新,只在每隔一个步骤(即每两个步骤)更新一次参数。设 t 为该回合的时间步,d 为我们希望延迟更新的时间步数(通常 d 设置为 2);那么我们可以写出如下公式:

  1. 如果 t mod d = 0,则:

    1. 计算目标函数的梯度

    2. 使用梯度上升法更新演员网络参数

当我们查看最终算法时,这一点会更加清晰。

目标策略平滑

为了理解这一点,让我们先回顾一下在 TD3 中如何计算目标值。我们了解到,在 TD3 中,我们使用剪切双 Q 学习和两个目标评估网络来更新目标值:

其中,动作为

正如我们所注意到的,我们使用目标演员网络生成的动作 来计算目标值,。我们并不直接使用目标演员网络给出的动作,而是向该动作添加一些噪声 ,然后将动作修改为 ,如下所示:

在这里,−c 到 +c 表示噪声被剪切,从而使目标保持接近实际的动作。因此,我们现在的目标值计算变为:

在上面的公式中,动作为

那么我们为什么要这样做呢?为什么需要给动作添加噪声,并用它来计算目标值呢?相似的动作应该具有相似的目标值,对吧?然而,DDPG 方法即使对于相似的动作,也会产生高方差的目标值。这是因为确定性策略会对价值估计中的尖锐峰值产生过拟合。因此,我们可以通过添加一些噪声来平滑这些相似动作的峰值。如此一来,目标策略平滑基本上充当了一个正则化器,并减少了目标值的方差。

现在我们已经理解了 TD3 算法的关键特性,让我们通过将所有概念结合起来,进一步澄清到目前为止学到的内容以及 TD3 算法是如何工作的。

将这些内容整合起来

首先,让我们回顾一下符号,以更好地理解 TD3。我们使用六个网络——四个评估网络和两个演员网络:

  • 两个主要的评估网络参数用 表示。

  • 两个目标评估网络参数用 表示。

  • 主要演员网络参数用 表示。

  • 目标演员网络参数用 表示。

TD3 是一种演员-评论者方法,因此 TD3 的参数将在每一轮的每一步进行更新,这与策略梯度方法不同,后者需要生成完整的回合,然后再更新参数。现在,让我们开始并了解 TD3 是如何工作的。

首先,我们初始化两个主要评论者网络参数,,以及主要演员网络参数 ,并赋予随机值。我们知道目标网络参数只是主网络参数的副本。所以,我们通过复制 来初始化两个目标评论者网络参数 ,同样,我们通过复制主要演员网络参数 来初始化目标演员网络参数 。我们还初始化回放缓冲区

现在,在每一轮中,首先,我们使用演员网络选择一个动作a

但是,为了确保探索,我们不直接使用动作a,而是添加一些噪声 ,其中 。因此,我们的动作现在变为:

然后,我们执行动作a,移动到下一个状态 ,并获得奖励r。我们将这个转移信息存储在回放缓冲区中

接下来,我们从回放缓冲区随机抽取一个K个转移(sars')的小批量。这些K个转移将用于更新评论者和演员网络。

首先,我们来计算评论者网络的损失。我们已经知道评论者网络的损失函数是:

在前面的方程中,以下内容适用:

  • 动作a[i] 是由演员网络产生的动作,即

  • y[i] 是评论者的目标值,即 ,而动作 是目标演员网络产生的动作,即 ,其中

在计算完评论者网络的损失后,我们计算梯度 并使用梯度下降法更新评论者网络的参数:

现在,让我们更新演员网络。我们已经知道,演员网络的目标函数是:

请注意,在上述方程中,我们仅使用来自采样的K个转移(sars')的状态 (s[i])。动作a 是由演员网络选择的,。为了最大化目标函数,我们计算目标函数 的梯度,并使用梯度上升法更新网络的参数:

我们不是在每个回合的每个时间步都更新演员网络的参数,而是延迟更新。让 t 表示回合的时间步,d 表示我们希望延迟更新的时间步数(通常 d 设置为 2);那么我们可以写出如下公式:

  1. 如果 t mod d = 0,则:

    1. 计算目标函数的梯度

    2. 使用梯度上升法更新演员网络参数

最后,我们通过软替换更新目标评论员网络的参数 ,以及目标演员网络的参数

更新目标网络参数时有一个小的变化。就像我们延迟更新演员网络参数 d 步骤一样,我们每 d 步骤更新目标网络参数;因此,我们可以写出如下公式:

  1. 如果 t mod d = 0,则:

    1. 计算目标函数的梯度 ,并使用梯度上升法更新演员网络的参数

    2. 更新目标评论员网络参数和目标演员网络参数,分别为

我们重复上述步骤若干回合,并改进策略。为了更好地理解 TD3 如何工作,让我们在下一节深入研究 TD3 算法。

算法 – TD3

TD3 算法与 DDPG 算法完全相似,除了它包含了我们在前面部分学习的三个关键特性。所以,在直接查看 TD3 算法之前,您可以复习一下 TD3 的所有关键特性。

TD3 算法如下所示:

  1. 初始化两个主评论员网络参数 ,以及主演员网络的参数

  2. 通过复制主评论员网络参数 ,初始化两个目标评论员网络参数

  3. 通过复制主演员网络参数 ,初始化目标演员网络的参数

  4. 初始化回放缓冲区

  5. 对于 N 个回合,重复第 6 步。

  6. 对于回合中的每个步骤,即 t = 0,…,T – 1:

    1. 基于策略 和探索噪声 选择动作 a,即 ,其中,

    2. 执行选定的动作 a,移动到下一个状态 ,获得奖励 r,并将转换信息存储到回放缓冲区

    3. 从回放缓冲区 随机抽取一个 K 的小批量转换。

    4. 选择动作 来计算目标值,,其中

    5. 计算评论家的目标值,即

    6. 计算评论家网络的损失,

    7. 计算损失的梯度 ,并使用梯度下降法最小化损失,

    8. 如果 t mod d =0,则:

      1. 计算目标函数的梯度 ,并使用梯度上升法更新演员网络参数,

      2. 更新目标评论家网络参数和目标演员网络参数,分别为

现在我们已经了解了 TD3 的工作原理,在接下来的章节中,我们将学习另一个有趣的算法,叫做 SAC。

Soft Actor-Critic

现在,我们将研究另一个有趣的演员-评论家算法,叫做 SAC。这是一个离策略算法,它借用了 TD3 算法的几个特性。但与 TD3 不同的是,它使用了一个随机策略 。SAC 基于熵的概念。那么首先,让我们理解一下什么是熵。熵是衡量变量随机性的一个指标。它基本上告诉我们随机变量的不确定性或不可预测性,表示为

如果随机变量每次都给出相同的值,那么我们可以说它的熵是低的,因为没有随机性。但是如果随机变量给出不同的值,那么我们可以说它的熵是高的。

例如,考虑一个掷骰子的实验。每次掷骰子时,如果我们得到一个不同的数字,那么我们可以说熵是高的,因为每次我们得到的数字都不同,且有较大的不确定性,因为我们不知道下一次掷骰子会出现哪个数字。但如果每次掷骰子得到的数字都是相同的,例如 3,那么我们可以说熵是低的,因为这里没有随机性,我们每次掷骰子都得到相同的数字。

我们知道策略 指定了在给定状态下执行的动作。当策略的熵 高或低时会发生什么?如果策略的熵高,意味着我们的策略会执行不同的动作,而不是每次都执行相同的动作。但如果策略的熵低,那么这意味着我们的策略每次都执行相同的动作。正如你可能已经猜到的,增加策略的熵有助于探索,而减少策略的熵意味着减少探索。

我们知道,在强化学习中,我们的目标是最大化回报。因此,我们可以定义我们的目标函数,如下所示:

其中 是我们随机策略 的参数。

我们知道,轨迹的回报就是奖励的总和,即:

所以,我们可以通过展开回报来重写我们的目标函数:

最大化前述目标函数即是最大化回报。在 SAC 方法中,我们使用稍微修改过的带有熵项的目标函数,如下所示:

如我们所见,我们的目标函数现在有两个项,一个是奖励,另一个是策略的熵。因此,我们不仅仅最大化奖励,还最大化策略的熵。那么这么做的意义是什么呢?最大化策略的熵使我们能够探索新的行动。但我们不希望探索那些给我们带来不良奖励的行动。因此,最大化熵和奖励的结合意味着我们可以在保持最大奖励的同时,探索新的行动。前述目标函数通常被称为最大熵强化学习,或熵正则化强化学习。增加熵项也常常被称为熵奖励。

此外,目标函数中的项叫做温度,用来设置熵项的重要性,或者我们可以说它用于控制探索。当较高时,我们允许策略中的探索,但当它较低时,则不允许探索。

好的,现在我们对 SAC 有了一个基本了解,接下来我们将深入一些细节。

理解软演员-评论家方法

SAC,顾名思义,是一种类似于我们在前面章节中学习的 DDPG 和 TD3 的演员-评论家方法。与使用确定性策略的 DDPG 和 TD3 不同,SAC 使用的是随机策略。SAC 的工作方式与 TD3 非常相似。我们学到,在演员-评论家架构中,演员使用策略梯度来寻找最优策略,评论家则使用 Q 函数来评估演员产生的策略。

同样地,在 SAC 中,演员使用策略梯度来找到最优策略,评论家则评估演员产生的策略。然而,评论家不仅使用 Q 函数来评估演员的策略,还同时使用 Q 函数和价值函数。那么,为什么我们需要 Q 函数和价值函数来共同评估演员的策略呢?这一点将在接下来的章节中详细解释。

在 SAC 中,我们有三个网络,一个是演员网络(策略网络)用来寻找最优策略,另外两个是评论家网络——价值网络和 Q 网络,分别用来计算价值函数和 Q 函数,以评估演员产生的策略。

在继续之前,让我们看看带有熵项的价值函数和 Q 函数的修改版本。

带有熵项的 V 和 Q 函数

我们知道,价值函数(状态值)是从状态s开始,遵循策略!的轨迹的期望回报:

我们了解到,回报是轨迹奖励的总和,因此我们可以通过扩展回报来重写前述方程:

现在,我们可以通过添加熵项来重写价值函数,如下所示:

我们知道,Q 函数(状态-动作值)是从状态s和动作a开始,遵循策略!的轨迹的期望回报:

扩展轨迹的回报,我们可以写出以下公式:

现在,我们可以通过添加熵项来重写 Q 函数,如下所示:

带有熵项的前述 Q 函数的修改版贝尔曼方程给出如下:

在这里,价值函数可以通过 Q 函数与价值函数之间的关系计算得到,如下所示:

要了解我们是如何精确得出公式(2)和(3)的,可以查看论文《Soft Actor-Critic: Off-Policy Maximum Entropy Deep Reinforcement Learning with a Stochastic Actor》中关于软策略迭代的推导,作者为 Tuomas Haarnoja 等人:arxiv.org/pdf/1801.01290.pdf

SAC 的组成部分

现在我们对 SAC 有了基本了解,让我们更详细地探讨并分别了解 SAC 中每个组件是如何工作的。

评论网络

我们了解到,与之前看到的其他演员-评论家方法不同,SAC 中的评论家同时使用价值函数和 Q 函数来评估演员网络产生的策略。但为什么会这样呢?

在之前的算法中,我们使用评论网络来计算 Q 函数,以评估演员产生的动作。此外,评论网络中的目标 Q 值是通过贝尔曼方程计算的。我们在这里也可以这样做。然而,由于熵项的存在,这里我们对 Q 函数的贝尔曼方程进行了修改,正如我们在公式(2)中学到的那样:

从前述方程中,我们可以观察到,为了计算 Q 函数,我们首先需要计算价值函数。因此,我们需要计算 Q 函数和价值函数,以便评估演员产生的策略。我们可以使用单个网络来逼近 Q 函数和价值函数。然而,我们并没有使用一个网络,而是使用了两个不同的网络,一个 Q 网络用来估计 Q 函数,另一个价值网络用来估计价值函数。使用两个不同的网络来计算 Q 函数和价值函数有助于稳定训练。

如 SAC 论文中所述,"原则上不需要为状态值包含一个独立的函数逼近器(神经网络),因为它根据方程(2)与 Q 函数和策略相关。但在实际操作中,包含一个独立的函数逼近器(神经网络)来处理状态值可以稳定训练,并且便于与其他网络同时训练。"

首先,我们将学习价值网络是如何工作的,然后再学习 Q 网络。

价值网络

价值网络用V表示,价值网络的参数用表示,目标价值网络的参数用表示。

因此,表示我们通过神经网络(由表示)来逼近价值函数(状态值)。好的,如何训练价值网络呢?我们可以通过最小化目标状态值与我们网络预测的状态值之间的损失来训练网络。我们如何得到目标状态值?我们可以使用方程(3)给出的价值函数来计算目标状态值。

我们学到,根据方程(3),状态值是通过以下方式计算的:

在前面的方程中,我们可以去掉期望值。我们将通过从重放缓冲区中采样K个转移来逼近期望值。因此,我们可以使用前面的方程计算目标状态值y[v]:

如果我们看一下前面的方程,我们有一个 Q 函数。为了计算 Q 函数,我们使用一个 Q 网络,参数由表示,类似地,我们的策略由表示,因此我们可以将前面的方程用参数化的 Q 函数和策略重新写出,如下所示:

但是,如果我们使用前面的方程来计算目标值,Q 值将会高估。因此,为了避免这种高估,我们使用裁剪的双 Q 学习,就像我们在 TD3 中学到的一样。也就是说,我们通过两个 Q 网络来计算两个 Q 值,这两个 Q 网络的参数分别由表示,并取这两个值的最小值,如下所示:

如我们在前面的方程中所观察到的,对于裁剪的双 Q 学习,我们使用了两个主要的 Q 网络,这两个 Q 网络的参数分别由表示,而在 TD3 中,我们使用了两个目标 Q 网络,这两个目标 Q 网络的参数分别由表示。为什么会这样呢?

因为这里我们正在计算状态动作对 的 Q 值,所以我们可以使用由 参数化的两个主要 Q 网络,但在 TD3 中,我们计算下一个状态动作对的 Q 值 ,所以我们使用由 参数化的两个目标 Q 网络。因此,在这里,我们不需要目标 Q 网络。

我们可以简单地表达上述方程为:

现在,我们可以定义我们的值网络的目标函数 为目标状态值与我们的网络预测的状态值之间的均方差,如下所示:

其中 K 表示我们从重播缓冲区中抽样的转换数。

我们可以计算我们的目标函数的梯度,然后更新我们的主值网络参数 如下:

注意,我们使用 表示学习率,因为我们已经使用 表示温度。

我们可以使用软替换来更新目标值网络的参数

下一节我们将学习目标值网络的确切应用位置。

Q 网络

Q 网络由Q表示,并且其参数为 。因此, 意味着我们使用由 参数化的神经网络来近似 Q 函数。我们如何训练 Q 网络呢?我们可以通过最小化目标 Q 值与网络预测的 Q 值之间的损失来训练网络。我们如何获取目标 Q 值呢?这就是我们使用贝尔曼方程的地方。

我们了解到根据贝尔曼方程(2),可以计算 Q 值如下:

我们可以通过抽样 K 个转换从重播缓冲区中近似期望。因此,我们可以使用上述方程计算目标 Q 值 y[q]。

如果我们看一下上述方程,我们有一个下一个状态的值 。为了计算下一个状态的值 ,我们使用由 参数化的目标值网络,因此我们可以用参数化值函数重写上述方程,如下所示:

现在,我们可以定义我们 Q 网络的目标函数 为目标 Q 值与网络预测的 Q 值之间的均方差,如下所示:

其中 K 表示我们从重播缓冲区中抽样的转换数。

在上一节中,我们学习了如何使用由 参数化的两个 Q 网络来防止过估计偏差。因此,首先,我们计算由 参数化的第一个 Q 网络的损失:

然后,我们计算梯度并使用梯度下降法更新参数 ,如 所示。

接下来,我们计算由 参数化的第二个 Q 网络的损失:

然后,我们计算梯度并使用梯度下降法更新参数 ,如 所示。

我们可以简单地将上述更新表示为:

演员网络

演员网络(策略网络)由 参数化。让我们回顾一下我们在 TD3 中学到的演员网络的目标函数:

其中 a 是由演员产生的动作。

上述目标函数意味着,演员的目标是生成一种动作,使其最大化由评论员计算的 Q 值。

SAC 中的演员网络的目标函数与我们在 TD3 中学到的相同,不同之处在于,这里我们使用了一个随机策略 ,同时,我们还最大化了熵。因此,我们可以将 SAC 中演员网络的目标函数写成:

现在,我们如何计算上述目标函数的导数呢?因为与 TD3 不同,在这里,我们的动作是使用随机策略计算的。应用反向传播并计算使用随机策略计算的动作的目标函数梯度将会很困难。因此,我们使用了重参数化技巧。重参数化技巧保证了从我们的策略中采样是可微的。因此,我们可以将我们的动作重新写为如下所示:

在上述方程中,我们可以观察到,我们用神经网络 f 参数化策略,并且 是从球形高斯分布中采样的噪声。

因此,我们可以将目标函数重新写为如下所示:

请注意,在上述方程中,我们的动作是 。记得我们是如何使用由 参数化的两个 Q 函数来避免过估计偏差的吗?现在,在上述目标函数中,我们应该使用哪个 Q 函数?我们可以使用任一函数,因此我们使用由 参数化的 Q 函数,并将最终目标函数写为:

现在我们已经理解了 SAC 算法的工作原理,让我们回顾一下迄今为止所学的内容,并通过将所有概念结合起来,准确地了解 SAC 算法是如何工作的。

将所有内容结合起来

首先,让我们回顾一下符号,以便更好地理解 SAC。我们使用五个网络——四个评论员网络(两个值网络和两个 Q 网络)和一个演员网络:

  • 主要值网络参数用 表示。

  • 目标值网络参数用 表示。

  • 两个主要 Q 网络参数分别用 表示。

  • 演员网络(策略网络)参数用 表示。

  • 目标状态值用 y[v] 表示,目标 Q 值用 y[q] 表示。

SAC 是一种演员-评论员方法,因此 SAC 的参数会在每个回合的每一步进行更新。现在,让我们开始并了解 SAC 是如何工作的。

首先,我们初始化值网络的主要网络参数 ,两个 Q 网络参数 ,以及演员网络参数 。接下来,我们通过复制主要网络参数 来初始化目标值网络参数 ,然后初始化回放缓冲区

现在,对于回合中的每一步,首先,我们使用演员网络选择一个动作 a

然后,我们执行动作 a,移动到下一个状态 ,并获得奖励 r。我们将这个转移信息存储在回放缓冲区 中。

接下来,我们从回放缓冲区随机抽取一个 K 的小批量转移。这个 K 的转移(sars')用于更新我们的值、Q 和演员网络。

首先,让我们计算值网络的损失。我们已经知道值网络的损失函数是:

其中 是目标状态值,它表示为

计算损失后,我们计算梯度并使用梯度下降更新值网络的参数

现在,我们计算 Q 网络的损失。我们已经知道 Q 网络的损失函数是:

其中 是目标 Q 值,它表示为

计算损失后,我们计算梯度并使用梯度下降更新 Q 网络的参数:

接下来,我们更新演员网络。我们已经知道演员网络的目标是:

现在,我们计算梯度并使用梯度上升更新演员网络的参数

最后,我们通过软替换更新目标值网络参数,如下所示:

我们对前述步骤进行了多次迭代,并改进了策略。为了更好地理解 SAC 的工作原理,我们将在下一节深入探讨 SAC 算法。

算法 – SAC

SAC 算法如下所示:

  1. 初始化主值网络参数,Q 网络参数,以及演员网络参数

  2. 通过仅复制主值网络参数,初始化目标值网络

  3. 初始化回放缓冲区

  4. 对于N个回合,重复步骤 5

  5. 对于回合中的每个步骤,即对于t = 0,…, T – 1:

    1. 根据策略选择一个动作a,即

    2. 执行选择的动作a,移动到下一个状态,获取奖励r,并将过渡信息存储到回放缓冲区

    3. 从回放缓冲区随机抽取一个大小为K的迷你批次过渡

    4. 计算目标状态值

    5. 计算值网络的损失,并使用梯度下降法更新参数,

    6. 计算目标 Q 值

    7. 计算 Q 网络的损失,并使用梯度下降法更新参数,

    8. 计算演员目标函数的梯度,,并使用梯度上升法更新参数,

    9. 更新目标值网络参数为

恭喜你学习了多个重要的最先进演员-评论家算法,包括 DDPG、双延迟 DDPG 和 SAC。在下一章中,我们将研究几个最先进的策略梯度算法。

总结

我们通过理解 DDPG 算法开始了本章的内容。我们了解到 DDPG 是一个演员-评论家算法,其中演员使用策略梯度估计策略,评论家使用 Q 函数评估演员产生的策略。我们还学习了 DDPG 如何使用确定性策略以及它如何应用于具有连续动作空间的环境。

后来,我们详细研究了 DDPG 的演员和评论家组件,理解了它们是如何工作的,然后最终学习了 DDPG 算法。

接下来,我们了解了双延迟 DDPG,它是 DDPG 的继任者,并对 DDPG 算法进行了改进。我们详细学习了 TD3 的关键特性,包括剪切双 Q 学习、延迟策略更新和目标策略平滑,最后,我们深入了解了 TD3 算法。

在本章末尾,我们学习了 SAC 算法。我们了解到,与 DDPG 和 TD3 不同,SAC 方法使用的是随机策略。我们还理解了 SAC 如何在目标函数中与熵奖励一起工作,并且我们了解了最大熵强化学习的含义。

在下一章,我们将学习最先进的策略梯度算法,如信任区域策略优化(TRPO)、近端策略优化(PPO)和使用克罗内克因子信任区域的演员-评论家方法。

问题

让我们将演员-评论家方法的知识付诸实践。试着回答以下问题:

  1. 在 DDPG 中,演员和评论家网络的作用是什么?

  2. DDPG 中的评论家是如何工作的?

  3. TD3 的关键特性是什么?

  4. 为什么我们需要剪切双重 Q 学习?

  5. 什么是目标策略平滑?

  6. 什么是最大熵强化学习?

  7. 在 SAC 中,评论家网络的作用是什么?

进一步阅读

欲了解更多信息,请参阅以下文献:

  • 使用深度强化学习进行连续控制,作者 Timothy P. Lillicrap, 等人, arxiv.org/pdf/1509.02971.pdf

  • 演员-评论家方法中的函数逼近误差,作者 Scott Fujimoto, Herke van Hoof, David Meger, arxiv.org/pdf/1802.09477.pdf

  • 软演员-评论家:使用随机演员的离策略最大熵深度强化学习,作者 Tuomas Haarnoja, Aurick Zhou, Pieter Abbeel, Sergey Levine, arxiv.org/pdf/1801.01290.pdf

第十三章:TRPO、PPO 和 ACKTR 方法

在本章中,我们将学习两种有趣的最先进的策略梯度算法:信任区域策略优化和近端策略优化。这两种算法都是对我们在第十章中学习的策略梯度算法(带基线的 REINFORCE)的改进,策略梯度方法

我们通过了解信任区域策略优化TRPO)方法及其如何作为策略梯度方法的改进来开始本章内容。随后,我们将理解理解 TRPO 所需的几个重要数学概念。接下来,我们将学习如何设计和求解 TRPO 目标函数。在本节结束时,我们将了解 TRPO 算法是如何一步步工作的。

接下来,我们将了解近端策略优化PPO)。我们将详细了解 PPO 的工作原理以及它如何作为 TRPO 算法的改进。我们还将学习两种 PPO 算法,分别是 PPO-clipped 和 PPO-penalty。

在本章结束时,我们将学习一种有趣的演员-评论家方法,叫做使用克罗内克因子信任区域的演员-评论家ACKTR)方法,该方法利用克罗内克分解来近似二阶导数。我们将探讨 ACKTR 是如何工作的,以及它如何在其更新规则中使用信任区域。

在本章中,我们将学习以下主题:

  • 信任区域策略优化

  • 设计 TRPO 目标函数

  • 求解 TRPO 目标函数

  • 近端策略优化

  • PPO 算法

  • 使用克罗内克因子信任区域的演员-评论家

信任区域策略优化

TRPO 是深度强化学习中最常用的算法之一。TRPO 是一种策略梯度算法,它是对我们在第十章中学习的带基线的策略梯度方法的改进。我们了解到,策略梯度是一种在线方法,这意味着在每次迭代中,我们都会改进与生成轨迹所用的相同策略。在每次迭代中,我们更新网络的参数,并试图找到改进后的策略。更新网络参数 的更新规则如下:

其中 是梯度, 被称为步长或学习率。如果步长较大,则会有较大的策略更新,如果步长较小,则策略更新较小。我们如何找到最佳步长?在策略梯度方法中,我们保持步长较小,因此在每次迭代时,策略都会有小幅改进。

但是,如果我们在每次迭代中都迈出很大的一步会发生什么呢?假设我们有一个由 参数化的策略 。因此,在每次迭代中,更新 意味着我们在改进我们的策略。如果步长很大,那么每次迭代中的策略变化会很大,这意味着旧政策(上一迭代中使用的策略)和新政策(当前迭代中使用的策略)变化很大。由于我们使用的是参数化的策略,这意味着如果我们进行大规模的更新(大步长),则旧政策和新政策的参数变化会非常大,这会导致一个叫做模型崩溃的问题。

这就是为什么在策略梯度方法中,我们不是采取更大步骤来更新网络参数,而是采取小步伐并更新参数,以保持旧政策和新政策接近。但我们如何改进这个方法呢?

我们能否在保持旧政策和新政策接近的情况下,迈出更大的一步,从而不会影响我们的模型表现,并帮助我们快速学习?是的,这个问题通过 TRPO 得到了解决。

TRPO 尝试在施加约束的情况下进行大规模的策略更新,即旧政策和新政策不应变化太大。好的,这个约束是什么呢?但首先,我们如何衡量和理解旧政策和新政策的变化是否过大呢?这里我们使用了一种叫做Kullback-LeiblerKL)散度的度量。KL 散度在强化学习中无处不在。它告诉我们两个概率分布彼此之间的差异。因此,我们可以使用 KL 散度来理解旧政策和新政策是否变化过大。TRPO 增加了一个约束,要求旧政策和新政策之间的 KL 散度应小于或等于某个常数 。也就是说,当我们进行策略更新时,旧政策和新政策的变化应不超过某个常数。这个约束被称为信任区域约束。

因此,TRPO 尝试在施加约束的情况下进行大规模的策略更新,要求旧政策和新政策的参数应保持在信任区域内。请注意,在策略梯度方法中,我们使用的是参数化的策略。因此,保持旧政策和新政策的参数在信任区域内,意味着旧政策和新政策也在信任区域内。

TRPO 保证了策略的单调改进;也就是说,它保证在每次迭代中都会有策略改进。这是 TRPO 算法背后的基本思想。

为了理解 TRPO 是如何工作的,我们需要理解 TRPO 背后的数学。TRPO 包含了相当复杂的数学内容。但是别担心!只要我们理解了理解 TRPO 所需的基本数学概念,它就会变得简单。因此,在深入了解 TRPO 算法之前,我们首先会理解一些必备的数学概念。然后,我们将学习如何设计带有信任区域约束的 TRPO 目标函数,最后,我们将看看如何求解 TRPO 目标函数。

数学基础

在理解 TRPO 是如何工作的之前,我们首先会理解以下重要的数学概念:

  • 泰勒级数

  • 信任区域方法

  • 共轭梯度方法

  • 拉格朗日乘数

  • 重要抽样

泰勒级数

泰勒级数是一个无限项的级数,用于逼近一个函数。假设我们有一个以 x = a 为中心的函数 f(x);我们可以使用一个无限的多项式项和来进行逼近,如下所示:

前面的方程可以用 sigma 符号表示为:

所以,对于泰勒级数中的每一项,我们计算 n^(次) 导数,将它们除以 n!,并乘以 (xa)^n。

让我们通过一个例子来理解泰勒级数是如何逼近一个函数的。假设我们有一个指数函数 e^x,如图 13.1 所示:

图 13.1:指数函数

我们能否使用泰勒级数逼近指数函数 e^x?我们知道,泰勒级数给出的是:

这里,我们要逼近的函数 f(x) 是 e^x,也就是说:

假设我们的函数 f(x) = e^x 以 x = a 为中心,首先我们来计算该函数的前三阶导数。指数函数的导数就是函数本身,所以我们可以写成:

将前面的项代入方程(1),我们可以写成:

假设 a = 0;那么我们的方程变为:

我们知道 e⁰ = 1;因此,指数函数的泰勒级数表示为:

这意味着右侧项的和可以逼近指数函数 e^x。让我们通过一个图形来理解这一点。我们只取泰勒级数(方程 2)中的 0^(次) 导数项,也就是 e^x = 1,并绘制它们:

图 13.2:泰勒级数逼近至 0^(次) 导数

从前面的图中可以看出,仅取 0^(次) 导数,我们与实际的 e^x 函数相差甚远。也就是说,我们的逼近效果不好。那么,接下来我们取泰勒级数(方程 2)中的 1^(次) 导数项,即 e^x = 1 + x,并绘制它们:

图 13.3:泰勒级数近似到一阶导数

从前面的图中我们可以观察到,将泰勒级数展开到一阶导数能够使我们更接近实际的函数e^x。所以,让我们取泰勒级数展开到二阶导数的项(方程 2),即 ,并绘制它们。从以下图中我们可以观察到,我们的近似值变得更好,且更加接近实际的函数e^x:

图 13.4:泰勒级数近似到二阶导数

现在,让我们取泰勒级数到三阶导数的项,即 ,并绘制它们:

图 13.5:泰勒级数近似到三阶导数

通过查看前面的图表,我们可以理解,加入三阶导数项的泰勒级数后,我们的近似值明显提高。正如你可能猜到的,加入更多的泰勒级数项使得我们对e^x 的近似更加准确。因此,使用泰勒级数,我们可以近似任何函数。

泰勒多项式到一阶被称为线性近似。在线性近似中,我们只计算泰勒级数到一阶导数。因此,函数f(x)在点a附近的线性近似(一阶)可以表示为:

我们可以用 来表示一阶导数,所以我们可以将 替换为 ,并将前面的方程重写为:

泰勒多项式到二阶被称为二次近似。在二次近似中,我们只计算泰勒级数到二阶导数。因此,函数f(x)在点a附近的二次近似(二阶)可以表示为:

我们可以用 来表示一阶导数,用 来表示二阶导数;因此,我们可以将 替换为 ,将 替换为 ,并将前面的方程重写为:

Hessian 是二阶导数,所以我们可以用 H(a) 来表示 ,并将前面的方程重写为:

因此,总结来说,函数f(x)的线性近似表示为:

函数f(x)的二次近似表示为:

信赖域方法

假设我们有一个函数 f(x),并且我们需要找到该函数的最小值。假设找出 f(x) 的最小值是困难的。那么,我们可以做的是使用泰勒级数来近似给定函数 f(x),并尝试通过近似函数来寻找最小值。我们可以用 来表示近似函数。

假设我们使用二次近似法,我们了解到,在二次近似法中,我们只计算泰勒级数的二阶导数。因此,给定函数 f(x) 在区域 a 周围的二次近似(第二阶)可以表示为:

所以,我们可以直接使用近似函数 来计算最小值。但等等!如果我们的近似函数 在某一点(例如 a)上不准确,而 a 是最优解,那么我们就会错过找到最优值的机会。

因此,我们将引入一个新的约束,称为信任区域约束。信任区域意味着我们的实际函数 f(x) 和近似函数 之间接近的区域。因此,我们可以说,如果我们的近似函数 在信任区域内,我们的近似将是准确的。

例如,如图 13.6 所示,我们的近似函数 在信任区域内,因此我们的近似将是准确的,因为近似函数 更接近实际函数 f(x):

图 13.6:近似函数在信任区域内

但当 不在信任区域内时,我们的近似将不准确,因为近似函数 与实际函数 f(x) 相距较远:

图 13.7:近似函数不在信任区域内

因此,我们需要确保我们的近似函数保持在信任区域内,这样它就会接近实际函数。

共轭梯度法

共轭梯度法是一种迭代方法,用于求解线性方程组。它也用于求解优化问题。当系统形式为以下时,使用共轭梯度法:

其中,A 是正定的、方形的对称矩阵,x 是我们要找的向量,b 是已知向量。我们考虑以下二次函数:

A是正半定矩阵时;找到该函数的最小值等同于求解系统Ax = b。与梯度下降法一样,共轭梯度下降法也试图找到函数的最小值;然而,共轭梯度下降法的搜索方向将与梯度下降法不同,而且共轭梯度下降法在N次迭代中达到收敛。让我们通过等高线图来理解共轭梯度下降法与梯度下降法的区别。

首先,让我们看看梯度下降法的等高线图。正如我们在下图中看到的那样,为了找到一个函数的最小值,梯度下降法需要多次搜索方向,最终形成一个锯齿形的方向模式:

图 13.8:梯度下降法的等高线图

与梯度下降法不同,在共轭梯度下降法中,搜索方向与前一个搜索方向是正交的,如图 13.9所示:

图 13.9:共轭梯度下降法的等高线图

因此,使用共轭梯度下降法,我们可以求解形如Ax = b的系统。

拉格朗日乘子

假设我们有一个函数f(x) = x²:我们如何找到该函数的最小值?我们可以通过找到梯度为零的点来找到该函数的最小值。函数f(x) = x²的梯度为:

x = 0 时,函数的梯度为零;即,当x = 0 时,。因此,我们可以说,函数f(x) = x²的最小值出现在x = 0 处。我们刚才看到的问题称为无约束优化问题。

考虑一个有约束的情况——假设我们需要最小化函数f(x),同时满足约束g(x) = 1,如下所示:

那么,我们该如何解决这个问题呢?也就是说,我们如何在满足约束g(x)的情况下找到函数f(x)的最小值?当目标函数f(x)的梯度和约束g(x)的梯度指向相同方向时,我们可以找到最小值。也就是说,当f(x)的梯度和g(x)的梯度平行或反平行时,我们可以找到最小值:

尽管f(x)和g(x)的梯度方向相同,但它们的大小并不相同。因此,我们将把g(x)的梯度乘以一个叫做的变量,如下所示:

其中被称为拉格朗日乘子。因此,我们可以将前面的方程重新写为:

解前面的方程意味着我们需要找到函数f(x)的最小值,同时满足约束g(x)。因此,我们可以将目标函数重新写为:

前述函数的梯度为:

时,我们可以找到最小值。拉格朗日乘子被广泛用于求解约束优化问题。

让我们通过另一个例子来理解这一点。假设我们想要求解函数 的最小值,同时满足约束 ,如下所示:

我们可以将目标函数重写为带有拉格朗日乘子的约束形式:

通过求解 ,我们可以找到函数 的最小值,同时满足约束条件

重要性采样

让我们回顾一下我们在 第四章蒙特卡罗方法 中学到的 重要性采样方法。假设我们想计算函数 f(x) 的期望,其中 x 的值是从分布 p(x) 中采样的,也就是说,;我们可以写为:

我们能否近似函数 f(x) 的期望?我们已经学到,使用蒙特卡罗方法时,可以通过如下方式近似期望:

也就是说,使用蒙特卡罗方法时,我们从分布 p(x) 中采样 x 进行 N 次,并计算 f(x) 的平均值来近似期望。

我们不仅可以使用蒙特卡罗方法,还可以使用重要性采样来近似期望。在重要性采样方法中,我们使用不同的分布 q(x) 来估计期望;也就是说,代替从 p(x) 中采样 x,我们使用不同的分布 q(x):

该比率 被称为重要性采样比率或重要性修正。

现在我们已经理解了几个重要的数学先决条件,接下来我们将学习 TRPO 算法是如何工作的。

设计 TRPO 目标函数

在本章开始时,我们学到 TRPO 试图在施加约束的同时进行大幅度的策略更新,要求旧策略和新策略的参数保持在信任区域内。在本节中,我们将学习如何设计 TRPO 目标函数以及信任区域约束,以确保旧策略和新策略之间不会有太大差异。

本节内容可能比较复杂且是可选的。如果你对数学不感兴趣,可以直接跳转到 求解 TRPO 目标函数 这一节,在那里我们将一步步学习如何求解 TRPO 目标函数。

假设我们有一个策略 ;我们可以表示遵循该策略 的期望折扣回报 如下:

我们知道,在策略梯度方法中,每次迭代时,我们都在不断改进策略 。假设我们更新了旧策略 ,并得到了一个新策略 ;那么,我们可以将新策略 下的期望折扣回报 表示为相对于旧策略 的优势,如下所示:

正如我们从前面的方程中可以注意到的,遵循新政策的期望回报 ,也就是 ,仅仅是遵循旧政策的期望回报 ,也就是 ,以及旧政策的期望折扣优势 的总和。也就是说:

但是,为什么我们要使用旧政策的优势呢?因为我们是在衡量新政策 相对于旧政策 的平均表现有多好。

我们可以简化方程(2),并将时间步的求和替换为状态和动作的求和,如下所示:

其中 是新政策的折扣访问频率。我们已经学过,新政策的期望回报 是通过将旧政策的期望回报 和旧政策的优势 相加得到的。

在前面的方程(3)中,如果优势 始终为正,那么这意味着我们的政策在改进,并且我们有更好的 。也就是说,如果优势 始终为 ,那么我们将始终在我们的政策中看到改进。

然而,方程(3)很难优化,因此我们用局部近似值 来逼近

如你所注意到的,与方程(3)不同,在方程(4)中,我们使用 而不是 。也就是说,我们使用旧政策的折扣访问频率 ,而不是新政策的折扣访问频率 。但为什么我们要这么做呢?因为我们已经有了从旧政策采样的轨迹,所以比起新政策,获取 更容易。

替代函数是目标函数的近似函数;因此,我们可以称 为替代函数,因为它是我们目标函数 的局部近似。

因此, 是我们目标函数 的局部近似。我们需要确保我们的局部近似是准确的。还记得在信任区域方法部分,我们学习到如果函数的局部近似在信任区域内,它将是准确的吗?因此,如果我们的局部近似 在信任区域内,它将是准确的。因此,在更新 的值时,我们需要确保它仍然保持在信任区域内;也就是说,策略更新应保持在信任区域内。

所以,当我们将旧策略 更新为新策略 时,我们只需确保新的策略更新保持在信任区域内。为了做到这一点,我们必须衡量新策略与旧策略之间的距离,因此,我们使用 KL 散度来衡量这一点:

因此,在更新策略时,我们检查策略更新之间的 KL 散度,并确保我们的策略更新保持在信任区域内。为了满足这一 KL 约束,Kakade 和 Langford 提出了一个新的策略更新方案,称为保守策略迭代,并推导出了以下下界:

如我们所观察到的,在前面的方程中,我们将 KL 散度作为惩罚项,C 是惩罚系数。

现在,我们的代理目标函数 (4) 以及惩罚的 KL 项被写为:

最大化代理函数 可以改进我们的真实目标函数 ,并保证策略的单调改进。前面的目标函数被称为 KL 惩罚目标

策略的参数化

我们了解到,最大化代理目标函数可以最大化我们的真实目标函数。我们知道在策略梯度方法中,我们使用参数化策略;也就是说,我们使用像神经网络这样的函数逼近器,该网络通过某些参数 进行参数化,并学习最优策略。

我们用 对旧策略进行参数化,表示为 ,用 对新策略进行参数化,表示为 。因此,我们可以将我们的方程 (5) 用参数化策略重新写成如下形式:

如前面的方程所示,我们使用旧策略与新策略之间的最大 KL 散度,即 。使用最大 KL 项优化目标是困难的,因此我们可以取平均 KL 散度 ,并将代理目标函数重写为:

上述目标函数的问题在于,当我们将惩罚系数 C 的值替换为 时,会减少步长,这使得我们需要花费很多时间才能达到收敛。

因此,我们可以将代理目标函数重新定义为一个约束目标函数,如下所示:

上述方程意味着我们在最大化代理目标函数 的同时,保持约束条件,即旧策略 和新策略 之间的 KL 散度小于等于常数 ,并确保我们的旧策略和新策略不会发生太大变化。上述目标函数称为 KL 约束目标

基于样本的估计

在前面的部分中,我们学习了如何将目标函数框架设置为带有参数化策略的 KL 约束目标。在本节中,我们将学习如何简化我们的目标函数。

我们了解到我们的 KL 约束目标函数如下所示:

从方程(4)开始,在前面的方程中将 替换为 ,我们可以写成:

现在我们将看到如何通过使用采样来去除两个求和项,从而简化方程(9)。

第一个求和项 表示状态访问频率的求和;我们可以通过从状态访问中采样状态来替换它,记为 。然后,我们的方程变为:

接下来,我们将通过重要性采样估计器替换求和操作!。设 q 为采样分布,aq 中采样,即 。然后,我们可以将前面的方程重写为:

将采样分布 q 替换为 ,我们可以写成:

因此,我们的方程(9)变为:

在下一部分,我们将学习如何求解前面的目标函数,以找到最优策略。

求解 TRPO 目标函数

在前一节中,我们学习了 TRPO 目标函数的表示:

上述方程意味着我们尝试找到一个策略,使其在满足旧策略和新策略之间的 KL 散度小于等于 的约束条件下,能够提供最大的回报。这个 KL 约束确保我们的新策略不会离旧策略太远。

为了简化符号表示,我们用 表示我们的目标函数,用 表示 KL 约束,并将前面的方程重写为:

通过最大化我们的目标函数 ,我们可以找到最优策略。我们可以通过计算相对于 的梯度,并使用梯度上升法更新参数,从而最大化目标

其中 是搜索方向(梯度), 是回溯系数。

也就是说,为了更新参数 ,我们执行以下两个步骤:

  • 首先,我们使用泰勒级数近似计算搜索方向

  • 接下来,我们通过回溯线搜索方法,在计算出的搜索方向 中执行线搜索,找到 的值。

我们将在在搜索方向中执行线搜索部分学习回溯系数是什么,以及回溯线搜索方法是如何工作的。好的,但为什么我们需要执行这两个步骤呢?如果你看一下我们的目标函数(10),你会发现我们有一个约束优化问题。我们的约束是,在更新参数 时,我们需要确保参数更新在可信区域内;也就是说,旧参数和新参数之间的 KL 散度应该小于或等于

因此,执行这两个步骤并更新我们的参数有助于我们满足 KL 约束,并且还确保单调改进。让我们深入了解这两个步骤是如何工作的。

计算搜索方向

直接优化我们的目标函数(10)是困难的,因此首先我们使用泰勒级数来近似我们的函数。我们使用线性近似来近似替代目标函数 ,并使用二次近似来近似我们的约束条件

为了更好地理解接下来的步骤,回顾一下泰勒级数,可以参考数学基础部分。

我们目标函数在点 处的线性近似为:

我们用g表示梯度 ,因此前面的方程变为:

在求解前面的方程时, 的值变为零,因此我们可以写作:

我们约束条件在点 处的二次近似为:

其中 H 是二阶导数,即 。在前面的方程中,第一项 变为零,因为两个相同分布之间的 KL 散度为零,且一阶导数 处变为零。

因此,我们的最终方程变为:

将 (11) 和 (12) 代入方程 (10),我们可以写成:

注意,在前面的方程中, 代表旧策略的参数, 代表新策略的参数。

如我们所见,在方程 (13) 中,我们有一个约束优化问题。我们如何解决这个问题?我们可以使用拉格朗日乘子法来求解。

因此,使用拉格朗日乘子 ,我们可以将我们的目标函数 (13) 重写为:

为了简化符号,设 s 代表 ,因此我们可以将方程 (14) 重写为:

我们的目标是找到最优参数 。因此,我们需要计算前述函数的梯度,并使用梯度上升法更新参数,如下所示:

其中 是学习率,s 是梯度。现在我们来看一下如何确定学习率 和梯度 s

首先,我们计算 s。通过对方程 (15) 中给出的目标函数 L 关于梯度 s 的导数,我们可以写成:

因此,我们可以写成:

只是我们的拉格朗日乘子,它不会影响我们的梯度,所以我们可以写成:

因此,我们可以写成:

然而,直接计算 s 的值并不是最优的。这是因为在前述方程中,我们有 ,这意味着需要计算二阶导数的逆。计算二阶导数及其逆是一个昂贵的任务。所以,我们需要找到一种更好的方法来计算 s;我们该如何做呢?

从 (17) 中,我们了解到:

从上述方程中,我们可以观察到该方程的形式是 Ax = B。因此,使用共轭梯度下降法,我们可以近似计算 s 的值为:

因此,我们的更新方程变为:

其中 的值是使用共轭梯度下降法计算得到的。

现在我们已经计算出了梯度,我们需要确定学习率 。我们需要记住,更新应当在信任区域内,因此在计算 的值时,我们需要保持 KL 约束。

在方程 (18) 中,我们了解到我们的更新规则是:

通过调整项的顺序,我们可以写成:

从方程 (13) 中,我们可以将 KL 约束写为:

将 (19) 代入前述方程中,我们可以写成:

上述方程可以解为:

因此,我们可以将前面学习率 的值代入方程(18),并将我们的参数更新改写为:

其中,的值通过共轭梯度下降法计算得出。

因此,我们通过泰勒级数近似和拉格朗日乘子计算了搜索方向:

在下一节中,让我们学习如何执行线性搜索。

在搜索方向上执行线性搜索

为了确保我们的策略更新满足 KL 约束,我们使用回溯线性搜索方法。因此,我们的更新方程变为:

好的,这是什么意思?那个新的参数 在那里做什么?它被称为回溯系数, 的值在 0 到 1 之间。它帮助我们采取大步长更新我们的参数。也就是说,我们可以将 设置为一个较高的值,并进行大幅更新。然而,我们需要确保在满足约束条件 的同时,最大化我们的目标

所以,我们只需尝试从 0 到N的不同值,并将 计算为 。如果 对于某些 j 的值成立,则我们停止并将参数更新为

以下步骤阐明了回溯线性搜索方法是如何工作的:

  1. 对于迭代次数 j = 0, 1, 2, 3, . . . , N

    1. 计算

    2. 如果 ,则:

      1. 更新

      2. 终止

因此,TRPO 的最终参数更新规则如下:

在下一节中,我们将通过使用前面的更新规则,学习 TRPO 算法是如何工作的。

算法 – TRPO

TRPO 作为我们在第十章《策略梯度方法》中学习的策略梯度算法的改进。它确保我们可以采取较大的步伐更新参数,同时保持旧策略与新策略之间的差异尽可能小。TRPO 的更新规则如下:

现在,让我们看看 TRPO 的算法,看看 TRPO 是如何使用前面的更新规则并找到最优策略的。在继续之前,我们先回顾一下在策略梯度方法中我们是如何计算梯度的。在策略梯度方法中,我们计算梯度 g 如下:

其中,R[t]是回报。回报是从状态s和动作a开始的轨迹的奖励总和;它表示为:

奖励累计(reward-to-go)不正是我们之前学到的内容吗?没错!如果你回忆一下,我们学到 Q 函数是从状态 s 和动作 a 开始的轨迹的奖励和。因此,我们可以将奖励累计替换为 Q 函数,并将梯度写成:

在前面的方程中,我们看到 Q 函数与价值函数之间的差异。我们学到,优势函数是 Q 函数和价值函数之间的差异,因此我们可以将我们的梯度与优势函数重写为:

现在,让我们来看看 TRPO 的算法。记住,TRPO 是一种策略梯度方法,因此与演员-评论员方法不同,在这里我们首先生成 N 个轨迹,然后更新策略和价值网络的参数。

TRPO 中涉及的步骤如下:

  1. 初始化策略网络参数 和价值网络参数

  2. 生成 N 个轨迹 ,按照策略 执行

  3. 计算回报(奖励累计) R[t]

  4. 计算优势值 A[t]

  5. 计算策略梯度:

  6. 使用共轭梯度法计算

  7. 使用更新规则更新策略网络参数

  8. 计算价值网络的均方误差:

  9. 使用梯度下降法更新价值网络参数 ,如 所示

  10. 对步骤 2 到 9 进行若干次迭代

现在我们已经了解了 TRPO 的工作原理,在下一节中,我们将学习另一个有趣的算法,称为邻近策略优化。

邻近策略优化

在上一节中,我们了解了 TRPO 的工作原理。我们了解到 TRPO 通过施加一个约束条件,使得旧策略和新策略之间的 KL 散度小于等于 ,从而保持策略更新在可信区域内。TRPO 方法的问题在于其实现困难且计算开销大。因此,现在我们将学习一种最受欢迎且最先进的策略梯度算法,称为 邻近策略优化PPO)。

PPO 改进了 TRPO 算法,并且实现简单。与 TRPO 类似,PPO 确保策略更新处于可信区域。但与 TRPO 不同的是,PPO 在目标函数中不使用任何约束。接下来,我们将学习 PPO 的具体工作原理,以及 PPO 如何确保策略更新处于可信区域。

PPO 算法有两种不同的类型:

  • PPO-裁剪法 – 在 PPO-裁剪法中,为了确保策略更新在信任区域内(即新策略不会偏离旧策略太远),PPO 增加了一个新的函数,称为裁剪函数,确保新旧策略不会相差太远。

  • PPO-惩罚法 – 在 PPO-惩罚法中,我们通过将 KL 约束项转换为惩罚项来修改目标函数,并在训练过程中自适应地更新惩罚系数,确保策略更新在信任区域内。

现在我们将详细探讨前面提到的两种 PPO 算法。

PPO 与裁剪目标

首先,让我们回顾一下 TRPO 的目标函数。我们了解到,TRPO 的目标函数表示为:

这意味着我们试图在约束条件下最大化我们的策略,即旧策略和新策略应保持在信任区域内,即旧策略与新策略之间的 KL 散度应小于或等于

让我们只考虑目标函数,不考虑约束条件,将 PPO 目标函数写为:

在前面的方程中,项 表示概率比率,即新策略与旧策略的比率。我们用 来表示这个比率,并将 PPO 目标函数写为:

如果我们使用前面的目标函数来更新策略,那么策略更新将不在信任区域内。因此,为了确保我们的策略更新在信任区域内(即新策略不会偏离旧策略太远),我们通过添加一个新的裁剪函数来修改目标函数,并将目标函数重写为:

前面的函数意味着我们取两个项中的最小值:一个是 ,另一个是

我们知道,第一个项 基本上就是我们的目标,见方程(20),第二个项称为裁剪目标。因此,我们的最终目标函数就是未裁剪目标和裁剪目标中的最小值。那么,这有什么用呢?加入这个裁剪目标是如何帮助我们确保新策略不会偏离旧策略太远的呢?

让我们通过仔细研究来理解这一点:

我们知道,第一个项(未裁剪目标)正是由方程(20)给出。那么,我们来看看第二个项——裁剪目标。它表示为:

通过观察前面的项,我们可以说我们在将概率比率 限制在 范围内。但为什么我们需要对 进行裁剪呢?这可以通过考虑优势函数的两种情况来解释——当优势为正时和当优势为负时。

情况 1: 当优势为正时

当优势为正时,,这意味着对应的动作应该比所有其他动作的平均值更受偏好。因此,我们可以增加该动作的 值,以便它有更大的被选择的机会。然而,在增加 的值时,我们不应增加得太多,以免远离旧的策略。因此,为了防止这种情况,我们将 的值裁剪到

图 13.10 显示了当优势为正时我们如何增加 的值,以及如何将其裁剪到

图 13.10: 当优势为正时, 的值

情况 2: 当优势为负时

当优势为负时,,这意味着对应的动作不应该比所有其他动作的平均值更受偏好。因此,我们可以降低该动作的 值,以便它有更小的被选择的机会。然而,在降低 的值时,我们不应降低得太多,以免远离旧的策略。因此,为了防止这种情况,我们将 的值裁剪到

图 13.11 显示了当优势为负时我们如何降低 的值,以及如何将其裁剪到

图 13.11: 当优势为负时, 的值

的值通常设置为 0.1 或 0.2。因此,我们了解到,裁剪后的目标通过根据优势函数将我们的策略更新保持接近旧策略,裁剪点为 。因此,我们的最终目标函数取未裁剪目标和裁剪目标中的最小值,公式如下:

现在我们已经了解了带裁剪目标的 PPO 算法如何工作,接下来让我们研究下一节中的算法。

算法 – PPO 裁剪版

PPO 裁剪版算法的步骤如下:

  1. 初始化策略网络参数 和价值网络参数

  2. 收集 N 个符合策略 的轨迹

  3. 计算回报(奖励目标) R[t]

  4. 计算目标函数的梯度

  5. 使用梯度上升法更新策略网络参数 ,如

  6. 计算价值网络的均方误差:

  7. 计算价值网络的梯度

  8. 使用梯度下降更新值网络参数 ,如 所示:

  9. 重复步骤 2 到 8 多次迭代:

实现 PPO 剪切方法:

让我们为摆动摆任务实现 PPO 剪切方法。本节中使用的代码改编自 Morvan 的一个很好的 PPO 实现(github.com/MorvanZhou/Reinforcement-learning-with-tensorflow/tree/master/contents/12_Proximal_Policy_Optimization):

首先,让我们导入必要的库:

import warnings
warnings.filterwarnings('ignore')
import tensorflow.compat.v1 as tf
tf.disable_v2_behavior()
import numpy as np
import matplotlib.pyplot as plt
import gym 

创建 Gym 环境:

让我们使用 Gym 创建一个摆动环境:

env = gym.make('Pendulum-v0').unwrapped 

获取环境的状态形状:

state_shape = env.observation_space.shape[0] 

获取环境的动作形状:

action_shape = env.action_space.shape[0] 

请注意,摆动摆的环境是连续的,因此我们的动作空间由连续值组成。所以,我们获取动作空间的边界:

action_bound = [env.action_space.low, env.action_space.high] 

设置在剪切目标中使用的 epsilon 值:

epsilon = 0.2 

定义 PPO 类:

让我们定义一个名为 PPO 的类,在其中实现 PPO 算法。为了更清楚地理解,我们逐行查看代码:

class PPO(object): 

定义 init 方法:

首先,让我们定义 init 方法:

 def __init__(self): 

启动 TensorFlow 会话:

 self.sess = tf.Session() 

定义状态的占位符:

 self.state_ph = tf.placeholder(tf.float32, [None, state_shape], 'state') 

现在,让我们构建返回状态值的值网络:

 with tf.variable_scope('value'):
            layer1 = tf.layers.dense(self.state_ph, 100, tf.nn.relu)
            self.v = tf.layers.dense(layer1, 1) 

定义 Q 值的占位符:

 self.Q = tf.placeholder(tf.float32, [None, 1], 'discounted_r') 

定义优势值为 Q 值与状态值之间的差:

 self.advantage = self.Q - self.v 

计算值网络的损失:

 self.value_loss = tf.reduce_mean(tf.square(self.advantage)) 

通过使用 Adam 优化器最小化损失来训练值网络:

 self.train_value_nw = tf.train.AdamOptimizer(0.002).minimize(self.value_loss) 

现在,我们从策略网络获取新的策略及其参数:

 pi, pi_params = self.build_policy_network('pi', trainable=True) 

从策略网络获取旧策略及其参数:

 oldpi, oldpi_params = self.build_policy_network('oldpi', trainable=False) 

从新策略中采样一个动作:

 with tf.variable_scope('sample_action'):
            self.sample_op = tf.squeeze(pi.sample(1), axis=0) 

更新旧策略的参数:

 with tf.variable_scope('update_oldpi'):
            self.update_oldpi_op = [oldp.assign(p) for p, oldp in zip(pi_params, oldpi_params)] 

定义动作的占位符:

 self.action_ph = tf.placeholder(tf.float32, [None, action_shape], 'action') 

定义优势的占位符:

 self.advantage_ph = tf.placeholder(tf.float32, [None, 1], 'advantage') 

现在,让我们定义策略网络的代理目标函数:

 with tf.variable_scope('loss'):
            with tf.variable_scope('surrogate'): 

我们了解到策略网络的目标是:

首先,我们定义比率

 ratio = pi.prob(self.action_ph) / oldpi.prob(self.action_ph) 

通过将比率 和优势值 A[t] 相乘来定义目标:

 objective = ratio * self.advantage_ph 

使用剪切和未剪切的目标定义目标函数:

 L = tf.reduce_mean(tf.minimum(objective, tf.clip_by_value(ratio, 1.-epsilon, 1.+ epsilon)*self.advantage_ph)) 

现在,我们可以计算梯度并使用梯度上升法最大化目标函数。然而,实际上我们可以通过添加一个负号将上述最大化目标转换为最小化目标。所以,我们可以将策略网络的损失表示为:

 self.policy_loss = -L 

通过使用 Adam 优化器最小化损失来训练策略网络:

 with tf.variable_scope('train_policy'):
            self.train_policy_nw = tf.train.AdamOptimizer(0.001).minimize(self.policy_loss) 

初始化所有 TensorFlow 变量:

 self.sess.run(tf.global_variables_initializer()) 

定义训练函数:

现在,让我们定义 train 函数:

 def train(self, state, action, reward): 

更新旧策略:

 self.sess.run(self.update_oldpi_op) 

计算优势值:

 adv = self.sess.run(self.advantage, {self.state_ph: state, self.Q: reward}) 

训练策略网络:

 [self.sess.run(self.train_policy_nw, {self.state_ph: state, self.action_ph: action, self.advantage_ph: adv}) for _ in range(10)] 

训练值网络:

 [self.sess.run(self.train_value_nw, {self.state_ph: state, self.Q: reward}) for _ in range(10)] 

构建策略网络:

我们定义一个名为build_policy_network的函数,用于构建策略网络。请注意,这里我们的动作空间是连续的,因此我们的策略网络返回动作的均值和方差作为输出,然后我们使用这个均值和方差生成一个正态分布,并通过从这个正态分布中采样来选择一个动作:

 def build_policy_network(self, name, trainable):
        with tf.variable_scope(name): 

定义网络的层:

 layer = tf.layers.dense(self.state_ph, 100, tf.nn.relu, trainable=trainable) 

计算均值:

 mu = 2 * tf.layers.dense(layer, action_shape, tf.nn.tanh, trainable=trainable) 

计算标准差:

 sigma = tf.layers.dense(layer, action_shape, tf.nn.softplus, trainable=trainable) 

计算正态分布:

 norm_dist = tf.distributions.Normal(loc=mu, scale=sigma) 

获取策略网络的参数:

 params = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope=name)
        return norm_dist, params 

选择动作

定义一个名为select_action的函数,用于选择动作:

 def select_action(self, state):
        state = state[np.newaxis, :] 

从策略网络生成的正态分布中采样一个动作:

 action = self.sess.run(self.sample_op, {self.state_ph: state})[0] 

我们将动作裁剪到动作范围内,然后返回该动作:

 action = np.clip(action, action_bound[0], action_bound[1])
        return action 

计算状态值

我们定义一个名为get_state_value的函数,用于获取通过价值网络计算的状态值:

 def get_state_value(self, state):
        if state.ndim < 2: state = state[np.newaxis, :]
        return self.sess.run(self.v, {self.state_ph: state})[0, 0] 

训练网络

现在,让我们开始训练网络。首先,创建一个 PPO 类的对象:

ppo = PPO() 

设置回合数:

num_episodes = 1000 

设置每回合的时间步数:

num_timesteps = 200 

设置折扣因子,

gamma = 0.9 

设置批量大小:

batch_size = 32 

对每个回合:

for i in range(num_episodes): 

通过重置环境来初始化状态:

 state = env.reset() 

初始化用于保存回合中获得的状态、动作和奖励的列表:

 episode_states, episode_actions, episode_rewards = [], [], [] 

初始化回报:

 Return = 0 

每一步:

 for t in range(num_timesteps): 

渲染环境:

 env.render() 

选择动作:

 action = ppo.select_action(state) 

执行所选择的动作:

 next_state, reward, done, _ = env.step(action) 

将状态、动作和奖励存储在列表中:

 episode_states.append(state)
        episode_actions.append(action)
        episode_rewards.append((reward+8)/8) 

更新状态到下一个状态:

 state = next_state 

更新回报:

 Return += reward 

如果我们达到了批量大小或达到了回合的最后一步:

 if (t+1) % batch_size == 0 or t == num_timesteps-1: 

计算下一个状态的值:

 v_s_ = ppo.get_state_value(next_state) 

计算 Q 值为

 discounted_r = []
            for reward in episode_rewards[::-1]:
                v_s_ = reward + gamma * v_s_
                discounted_r.append(v_s_)
            discounted_r.reverse() 

堆叠回合状态、动作和奖励:

 es, ea, er = np.vstack(episode_states), np.vstack(episode_actions), np.array(discounted_r)[:, np.newaxis] 

训练网络:

 ppo.train(es, ea, er) 

清空列表:

 episode_states, episode_actions, episode_rewards = [], [], [] 

每 10 回合打印一次回报:

 if i %10 ==0:
         print("Episode:{}, Return: {}".format(i,Return)) 

现在我们已经了解了带剪切目标的 PPO 是如何工作的以及如何实现它,在接下来的部分,我们将学习另一种有趣的 PPO 算法类型,称为带惩罚目标的 PPO。

带惩罚目标的 PPO

在 PPO 惩罚方法中,我们将约束项转换为惩罚项。首先,让我们回顾一下 TRPO 的目标函数。我们知道 TRPO 的目标函数为:

在 PPO 惩罚方法中,我们可以通过将 KL 约束项转换为惩罚项,像这样重写前面的目标:

其中 被称为惩罚系数。

为目标 KL 散度;然后,我们自适应地设置 的值为:

  • 如果d大于或等于 ,那么我们设置

  • 如果d小于或等于 ,那么我们设置

我们可以通过查看接下来的部分中的 PPO 惩罚算法,来准确理解它是如何工作的。

算法 – PPO-penalty

PPO-penalty 算法的步骤如下:

  1. 初始化策略网络参数 和值网络参数 ,并初始化惩罚系数 和目标 KL 散度

  2. 对于迭代

    1. 按照策略 收集 N 条轨迹

    2. 计算回报(奖励) R[t]

    3. 计算

    4. 计算目标函数的梯度

    5. 使用梯度上升法更新策略网络参数 ,如

    6. 如果 d 大于或等于 ,则设置 ;如果 d 小于或等于 ,则设置

    7. 计算值网络的均方误差:

    8. 计算值网络的梯度

    9. 使用梯度下降法更新值网络参数 ,如

因此,我们了解了 PPO-clipped 和 PPO-penalized 目标的工作原理。一般而言,带有裁剪目标的 PPO 方法比带有惩罚目标的 PPO 方法使用得更多。

在下一节中,我们将学习另一个有趣的算法,称为 ACKTR。

使用 Kronecker 分解信任域的演员-评论家

ACKTR,如其名所示,是基于 Kronecker 分解和信任区域的演员-评论家算法。

我们知道,演员-评论家架构由演员网络和评论家网络组成,演员的角色是生成策略,而评论家的角色是评估演员网络生成的策略。我们已经了解到,在演员网络(策略网络)中,我们通过计算梯度并使用梯度上升更新演员网络的参数:

我们可以通过计算自然梯度来更新演员网络的参数,而不是使用前述更新规则:

其中 F 称为费舍尔信息矩阵。因此,自然梯度就是费舍尔矩阵的逆与标准梯度的乘积:

自然梯度的使用在于它能保证策略的单调改进。然而,使用上述更新规则更新演员网络(策略网络)参数是一项计算量大的任务,因为计算费舍尔信息矩阵并求其逆是一个计算量巨大的过程。因此,为了避免这项繁重的计算,我们可以通过克罗内克分解近似来逼近 的值。一旦我们使用克罗内克分解近似了 ,就可以按照公式(21)中给出的自然梯度更新规则更新我们的策略网络参数,在更新策略网络参数时,我们还要确保策略更新位于信任区域内,这样新策略就不会偏离旧策略太远。这就是 ACKTR 算法的核心思想。

现在,我们已经对 ACKTR 有了基本的了解,接下来让我们详细了解它是如何工作的。首先,我们将理解克罗内克分解是什么,然后我们将学习它如何在演员-评论家设置中使用,之后我们将学习如何在策略更新中结合信任区域。

在继续之前,让我们学习一些理解 ACKTR 所必需的数学概念。

数学基础

为了理解克罗内克分解如何工作,我们将学习以下重要概念:

  • 块矩阵

  • 块对角矩阵

  • 克罗内克积

  • vec 运算符

  • 克罗内克积的性质

块矩阵

块矩阵被定义为一个可以分解为称为块的子矩阵的矩阵,或者我们可以说,块矩阵是由一组子矩阵或块构成的。例如,假设我们考虑一个如图所示的块矩阵 A

矩阵 A 可以分解成四个子矩阵,如下所示:

现在,我们可以简单地将块矩阵 A 写成:

块对角矩阵

块对角矩阵是一个包含方阵对角线元素,非对角线元素设为 0 的块矩阵。块对角矩阵 A 可表示为:

其中对角线部分 是方阵。

一个块对角矩阵的例子如下所示:

如我们所见,对角线部分基本上是一个方阵,而非对角线元素被设为零:

因此,我们可以写成:

现在,我们可以简单地将我们的块对角矩阵 A 表示为:

克罗内克积

克罗内克积是两个矩阵之间执行的运算。克罗内克积不同于矩阵乘法。当我们执行两个矩阵之间的克罗内克积时,它会输出块矩阵。克罗内克积用表示。假设我们有一个阶数为的矩阵A和一个阶数为的矩阵B,那么矩阵AB的克罗内克积表示为:

这意味着我们将矩阵A中的每个元素与矩阵B相乘。让我们通过一个例子来理解这一点。假设我们有两个矩阵AB,如下所示:

然后,矩阵AB的克罗内克积表示为:

vec 运算符

vec 运算符通过将矩阵中的所有列堆叠在一起,创建一个列向量。例如,假设我们有如下所示的矩阵A

对* A *应用 vec 运算符会将矩阵中的所有列堆叠在一起,形成如下所示的矩阵:

克罗内克积的性质

克罗内克积有几个有用的性质,包括:

现在我们已经学习了几个重要概念,接下来让我们理解什么是克罗内克因式分解。

克罗内克因式近似曲率(K-FAC)

假设我们有一个由参数化的神经网络,并且我们使用梯度下降法训练神经网络。我们可以写出更新规则,包括自然梯度,如下所示:

其中F是费舍尔信息矩阵。问题在于计算F并求解其逆是一个昂贵的任务。为了避免这种情况,我们使用克罗内克因式分解近似来估算的值。

让我们学习如何使用克罗内克因式近似。假设我们的网络有层,网络的权重表示为。因此,表示层的权重。让表示网络的输出分布,我们将使用负对数似然作为损失函数J

然后,费舍尔信息矩阵可以写成:

K-FAC 近似费舍尔信息矩阵F,将其表示为块对角矩阵,每个块代表特定层的权重梯度。例如,块F[1]表示损失函数相对于第 1 层权重的梯度。块F[2]表示损失函数相对于第 2 层权重的梯度。块F[l]表示损失函数相对于第l层权重的梯度:

即,,其中:

正如我们所观察到的,每个块F[1]到F[L]包含了损失J相对于相应层权重的导数。那么,我们如何计算每个块呢?也就是说,如何计算前面这个对角矩阵中的值?

为了理解这一点,让我们先看一个块,假设是F[l],并学习它是如何计算的。假设我们有一层l,让a是输入激活向量,是该层的权重,s是输出前激活向量,它可以传递给下一层l + 1。

我们知道,在神经网络中,我们将激活向量与权重相乘并将其传递到下一层;因此,我们可以写成:

我们可以将对应于层l的块F[l]近似为:

上述方程F[l]表示损失对层l权重的梯度。

从(22)可得,损失函数J相对于层l的权重的偏导数可以写为:

将(24)代入(23),我们可以写为:

上述方程表明F[l]仅仅是克罗内克积的期望值。因此,我们可以将其重新写为期望值的克罗内克积;也就是说,F[l]可以近似为期望值的克罗内克积:

。我们可以写成:

这被称为克罗内克分解,AS被称为克罗内克因子。现在我们已经了解了如何计算块F[l],接下来我们来学习如何更新层l的权重

更新层l权重的更新规则为:

。我们可以写成:

让我们看看如何计算的值:

对两边应用 vec 运算符,我们可以写成:

从(25)我们可以代入F[l]的值并写成:

使用属性,我们可以写成:

如你所见,我们已经在不需要通过克罗内克因子进行昂贵的费舍尔信息矩阵逆运算的情况下计算了的值。现在,利用我们刚推导出的的值,我们可以更新层l的权重

简而言之,K-FAC 将 Fisher 信息矩阵近似为一个块对角矩阵,其中每个块包含导数。然后,每个块被近似为两个矩阵的 Kronecker 积,这就是所谓的 Kronecker 因式分解。

因此,我们已经学会了如何使用 Kronecker 因子近似自然梯度。在下一节中,我们将学习如何在演员-评论者方法中应用这一点。

K-FAC 在演员-评论者方法中

我们知道,在演员-评论者方法中,有演员网络和评论者网络。演员的角色是生成策略,评论者的角色是评估演员网络生成的策略。

首先,让我们看看演员网络。在演员网络中,我们的目标是找到最优策略。因此,我们试图找到最优参数 ,通过该参数可以获得最优策略。我们计算梯度并使用梯度上升法更新演员网络的参数:

我们可以通过计算自然梯度来更新演员网络的参数,而不是使用前述的更新规则,具体方法如下:

但是计算 是一项昂贵的任务。因此,我们可以使用 Kronecker 因式分解来近似 的值。我们可以将演员网络的 Fisher 信息矩阵 F 定义为:

正如我们在上一节中所学到的,我们可以将 Fisher 信息矩阵近似为一个块对角矩阵,其中每个块包含导数,然后我们可以将每个块近似为两个矩阵的 Kronecker 积。

。我们可以写为:

的值可以通过 Kronecker 因式分解计算为:

这正是我们在上一节中学到的内容。

现在,让我们来看一下评论者网络。我们知道,评论者通过估计 Q 函数来评估由演员网络生成的策略。因此,我们通过最小化目标值和预测值之间的均方误差来训练评论者。

我们使用梯度下降法最小化损失,并更新评论者网络的参数 ,具体方法如下:

其中 是标准的一阶梯度。

我们能否不使用一阶梯度,而是使用二阶梯度来更新评论员网络的参数 ,类似于我们对演员所做的操作?是的,在像最小二乘法(MSE)这样的设置中,我们可以使用一个叫做高斯-牛顿法的算法来找到二阶导数。你可以在这里了解更多关于高斯-牛顿法的信息:www.seas.ucla.edu/~vandenbe/236C/lectures/gn.pdf。让我们用 表示我们的误差。根据高斯-牛顿法,更新评论员网络参数 的更新规则为:

其中 G 被称为高斯-牛顿矩阵,其形式为 ,而 J 是雅可比矩阵。(雅可比矩阵是一个包含向量值函数的一阶偏导数的矩阵。)

如果你查看前面的方程式,计算 相当于计算我们在演员网络中看到的 。也就是说,计算高斯-牛顿矩阵的逆相当于计算费舍尔信息矩阵的逆。因此,我们可以使用克罗内克因子(K-FAC)来近似计算 的值,就像我们近似计算 的值一样。

我们可以不仅仅单独应用 K-FAC 于演员和评论员网络,还可以在共享模式下应用。正如 Yuhuai Wu、Elman Mansimov、Shun Liao、Roger Grosse 和 Jimmy Ba 在论文 Scalable trust-region method for deep reinforcement learning using Kronecker-factored approximation 中所述,"我们可以设计一个单一架构,其中演员和评论员共享底层表示,但它们具有不同的输出层。" 该论文可访问:arxiv.org/pdf/1708.05144.pdf

简而言之,在 ACKTR 方法中,我们通过计算二阶导数来更新演员和评论员网络的参数。由于计算二阶导数是一项昂贵的任务,我们使用了一种叫做克罗内克因子近似的方法来近似二阶导数。

在下一节中,我们将学习如何将信任区域融入我们的更新规则,使得新旧策略更新不会相差太远。

融入信任区域

我们已经学会了如何使用自然梯度来更新我们网络的参数,如下所示:

在上一节中,我们学习了如何使用 K-FAC 来近似矩阵。在更新策略时,我们需要确保策略更新处于信任区域内;也就是说,新策略不应与旧策略相差太远。为了确保这一点,我们可以选择步长作为,其中和信任区域半径是超参数,如 ACKTR 论文中所提到的(参见进一步阅读部分)。使用此步长更新网络参数可确保我们的策略更新处于信任区域内。

总结

我们从了解 TRPO 是什么以及它如何作为策略梯度算法的改进开始本章内容。我们了解到,当新旧策略差异过大时,会导致模型崩溃。

因此,在 TRPO 中,我们在更新策略时强加一个约束,要求新旧策略的参数保持在信任区域内。我们还了解到,TRPO 保证了单调的策略改进;也就是说,它确保每次迭代都会有策略改进。

后来,我们学习了 PPO 算法,它是 TRPO 算法的改进版。我们了解了两种 PPO 算法:PPO-clipped 和 PPO-penalty。在 PPO-clipped 方法中,为了确保策略更新处于信任区域,PPO 添加了一个名为裁剪函数的新功能,以确保新旧策略不会相差太远。在 PPO-penalty 方法中,我们通过将 KL 约束项转化为惩罚项来修改目标函数,并在训练过程中自适应地更新惩罚系数,确保策略更新位于信任区域内。

在本章结束时,我们学习了 ACKTR。在 ACKTR 方法中,我们通过计算二阶导数来更新演员网络和评论员网络的参数。由于计算二阶导数是一项昂贵的任务,我们使用一种叫做 Kronecker-分解近似法的方法来近似二阶导数,在更新策略网络参数时,我们还确保策略更新位于信任区域内,以确保新策略不会与旧策略相差太远。

在下一章中,我们将学习一些有趣的分布式强化学习算法。

问题

让我们评估一下我们对本章所学算法的理解。试着回答以下问题:

  1. 什么是信任区域?

  2. 为什么 TRPO 有用?

  3. 共轭梯度法与梯度下降法有何不同?

  4. TRPO 的更新规则是什么?

  5. PPO 与 TRPO 有何不同?

  6. 解释 PPO-clipped 方法。

  7. 什么是 Kronecker 分解?

进一步阅读

如需更多信息,请参考以下论文:

  • 信赖域策略优化约翰·舒尔曼谢尔盖·莱文菲利普·莫里茨迈克尔·I·乔丹皮特·阿贝尔 发表,arxiv.org/pdf/1502.05477.pdf

  • 邻近策略优化算法约翰·舒尔曼菲利普·沃尔斯基普拉夫拉·达里瓦尔阿列克·拉德福德奥列格·克利莫夫 发表,arxiv.org/pdf/1707.06347.pdf

  • 使用克罗内克因子近似的可扩展信赖域方法应用于深度强化学习余槐·吴埃尔曼·曼西莫夫邵·廖罗杰·格罗斯吉米·巴 发表,arxiv.org/pdf/1708.05144.pdf

第十四章:分布式强化学习

在本章中,我们将学习分布式强化学习。我们将通过理解分布式强化学习究竟是什么以及它为何有用来开始本章。接下来,我们将学习一种非常流行的分布式强化学习算法,叫做 分类 DQN。我们将了解分类 DQN 是什么,它与我们在 第九章深度 Q 网络及其变体 中学习的 DQN 有什么不同,然后我们将详细探讨分类 DQN 算法。

接下来,我们将学习另一个有趣的算法,叫做 分位数回归 DQNQR-DQN)。我们将了解 QR-DQN 是什么,它与分类 DQN 有什么不同,然后我们将详细探讨 QR-DQN 算法。

在本章结束时,我们将学习一种名为 分布式分布式深度确定性策略梯度D4PG)的策略梯度算法。我们将详细了解 D4PG 是什么,以及它与我们在 第十二章学习 DDPG、TD3 和 SAC 中学习的 DDPG 有什么不同。

在本章中,我们将涵盖以下主题:

  • 为什么使用分布式强化学习?

  • 分类 DQN

  • 分位数回归 DQN

  • 分布式分布式深度确定性策略梯度

让我们通过理解什么是分布式强化学习以及我们为什么需要它来开始本章。

为什么使用分布式强化学习?

假设我们处于状态 s,并且在这个状态下有两种可能的动作可以执行。设这两种动作分别是 。我们如何决定在这个状态下执行哪个动作呢?我们为该状态下的所有动作计算 Q 值,并选择 Q 值最大的动作。因此,我们计算 Q(s, 上) 和 Q(s, 下),并选择 Q 值最大的动作。

我们了解到,Q 值是一个智能体从状态 s 开始并执行一个动作 a 后,根据策略获得的预期回报!

但使用这种方法计算 Q 值时有一个小问题,因为 Q 值仅仅是回报的期望值,而期望值并未包含内在的随机性。让我们通过一个例子来准确理解这意味着什么。

假设我们要从工作地点开车回家,我们有两条路线 AB。现在,我们需要决定哪条路线更好,也就是说,哪条路线帮助我们在最短的时间内到达家里。为了找出哪条路线更好,我们可以计算 Q 值,并选择 Q 值最大的路线,也就是给我们最大期望回报的路线。

假设选择路线的 Q 值为Q(s, A) = 31,选择路线B的 Q 值为Q(s, B) = 28。由于 Q 值(路线A的期望回报)较高,我们可以选择路线A回家。但这里是不是遗漏了什么呢?我们能不能直接看回报的分布,而不是把 Q 值看作回报的期望,从而做出更好的决策?

是的!

但首先,让我们看看路线A和路线B的分布,并理解哪条路线更优。以下图表显示了路线A的分布。它告诉我们,有 70%的概率在 10 分钟内到家,而有 30%的概率需要 80 分钟才能到家。也就是说,如果选择路线A,我们通常能在 10 分钟内到家,但当遇到交通拥堵时,我们需要 80 分钟才能到家:

图 14.1:路线 A 的分布

图 14.2显示了路线B的分布。它告诉我们,有 80%的概率在 20 分钟内到家,20%的概率需要 60 分钟才能到家。

也就是说,如果我们选择路线B,通常在 20 分钟内到家,但当遇到交通拥堵时,我们需要 60 分钟才能到家:

图 14.2:路线 B 的分布

看过这两种分布后,我们会发现选择路线B比选择路线A更有意义。选择路线B时,即使在最糟糕的情况下,也就是遇到交通拥堵时,我们能在 60 分钟内到家。但选择路线A时,在交通繁忙时,我们需要 80 分钟才能到家。因此,选择路线B而不是A是明智的决定。

类似地,如果我们观察路线A和路线B的回报分布,我们就能获得更多信息。如果我们仅仅根据最大期望回报(即最大 Q 值)来采取行动,那么这些细节将会被忽略。因此,我们不是用期望回报来选择行动,而是使用回报的分布,然后根据分布选择最优的行动。

这是分布式强化学习的基本思想和动机。在下一节中,我们将学习一种最流行的分布式强化学习算法——分类 DQN,也叫做 C51 算法。

分类 DQN

在上一节中,我们了解了为什么基于回报分布来选择行动比仅仅基于 Q 值(即期望回报)来选择行动更有利。在这一节中,我们将学习如何使用一种名为分类 DQN 的算法来计算回报的分布。

回报的分布通常被称为值分布或回报分布。设Z为随机变量,Z(s, a)表示状态s和动作a的值分布。我们知道 Q 函数表示为Q(s, a),它给出一个状态-动作对的值。同样,现在我们有了Z(s, a),它给出状态-动作对的值分布(回报分布)。

好的,我们如何计算Z(s, a)?首先,让我们回顾一下如何计算Q(s, a)。

在 DQN 中,我们学习到使用神经网络来逼近 Q 函数,Q(s, a)。由于我们使用神经网络来逼近 Q 函数,我们可以通过来表示 Q 函数,其中是网络的参数。给定一个状态作为输入到网络,它输出所有可以在该状态下执行的动作的 Q 值,然后我们选择具有最大 Q 值的动作。

类似地,在分类 DQN 中,我们使用神经网络来逼近Z(s, a)的值。我们可以通过来表示这一点,其中是网络的参数。给定一个状态作为输入到网络,它输出所有可以在该状态下执行的动作的值分布(回报分布),然后我们根据这个值分布选择一个动作。

让我们通过一个例子来理解 DQN 和分类 DQN 之间的区别。假设我们处于状态s,并且假设我们的动作空间有两个动作ab。现在,如图 14.3所示,给定状态s作为输入,DQN 返回所有动作的 Q 值,然后我们选择具有最大 Q 值的动作,而在分类 DQN 中,给定状态s作为输入,它返回所有动作的值分布,然后我们根据这个值分布选择一个动作:

图 14.3:DQN 与分类 DQN

好的,我们如何训练网络?在 DQN 中,我们学习到通过最小化目标 Q 值与网络预测的 Q 值之间的损失来训练网络。我们知道目标 Q 值是通过贝尔曼最优方程获得的。因此,我们最小化目标值(最优贝尔曼 Q 值)与预测值(网络预测的 Q 值)之间的损失并训练网络。

类似地,在分类 DQN 中,我们通过最小化目标值分布与网络预测的值分布之间的损失来训练网络。好的,我们如何获得目标值分布?在 DQN 中,我们使用贝尔曼方程来获得目标 Q 值;类似地,在分类 DQN 中,我们可以使用分布式贝尔曼方程来获得目标值分布。那么,什么是分布式贝尔曼方程?首先,在学习分布式贝尔曼方程之前,让我们回顾一下贝尔曼方程。

我们了解到,Q 函数Q(s, a)的贝尔曼方程表示为:

类似地,值分布Z(s, a)的贝尔曼方程表示为:

这个方程被称为分布式贝尔曼方程。因此,在分类 DQN 中,我们通过最小化目标值分布(由分布式贝尔曼方程给出)和网络预测的值分布之间的损失来训练网络。

好的,我们应该使用什么样的损失函数呢?在 DQN 中,我们使用均方误差MSE)作为我们的损失函数。与 DQN 不同的是,在分类 DQN 中,我们不能使用 MSE 作为损失函数,因为在分类 DQN 中,我们预测的是概率分布,而不是 Q 值。由于我们处理的是分布,因此我们使用交叉熵损失作为我们的损失函数。因此,在分类 DQN 中,我们通过最小化目标值分布与网络预测的值分布之间的交叉熵损失来训练网络。

简而言之,分类 DQN 与 DQN 相似,唯一不同的是在分类 DQN 中,我们预测的是值分布,而在 DQN 中,我们预测的是 Q 值。因此,给定一个状态作为输入,分类 DQN 返回该状态下每个动作的值分布。我们通过最小化目标值分布(由分布式贝尔曼方程给出)和网络预测的值分布之间的交叉熵损失来训练网络。

现在我们已经理解了分类 DQN 是什么以及它与 DQN 的区别,在接下来的部分中,我们将学习分类 DQN 是如何准确预测值分布的。

预测值分布

图 14.4展示了一个简单的值分布:

图 14.4:值分布

水平轴的值称为支持或原子,垂直轴的值则是概率。我们用Z表示支持,用P表示概率。为了预测值的分布以及状态,我们的网络将分布的支持作为输入,并返回支持中每个值的概率。

那么,现在我们将看到如何计算分布的支持。为了计算支持,首先我们需要决定支持的值的数量N、支持的最小值和支持的最大值。给定支持的数量N,我们将其从分成N个相等的部分。

让我们通过一个例子来理解这一点。假设支撑值数量 N = 5,支撑值的最小值为 ,最大值为 。现在,如何找到支撑值呢?为了找到支撑值,首先,我们需要计算一个步长,记作 。值 可以通过以下公式计算:

现在,为了计算支撑值,我们从支撑值的最小值开始 ,并将 加到每个值上,直到我们达到支撑值数量 N。在我们的示例中,我们从 开始,它是 2,然后我们将 加到每个值上,直到我们达到支撑值数量 N。因此,支撑值变为:

因此,我们可以将支撑值表示为 。以下 Python 代码片段可以帮助我们更清楚地了解如何获得支撑值:

def get_support(N, V_min, V_max):
    dz = (V_max – V_min) / (N-1)
    return [V_min + i * dz for i in range(N)] 

好的,我们已经了解了如何计算分布的支撑值,现在神经网络是如何将这个支撑值作为输入并返回概率的呢?

为了预测价值分布,除了状态外,我们还需要将分布的支撑值作为输入,然后网络返回我们价值分布的概率作为输出。让我们通过一个例子来理解这一点。假设我们处于状态 s,并且在这个状态下有两个动作可供选择,分别是 。假设我们计算得到的支撑值是 z[1]、z[2] 和 z[3]。

正如 图 14.5 所示,在将状态 s 作为输入传入网络的同时,我们还将分布的支撑值 z[1]、z[2] 和 z[3] 输入。然后,网络返回给定支撑值对应的动作 和动作 的分布概率 pi:

图 14.5:分类 DQN

分类 DQN 论文的作者(详见 进一步阅读 部分)建议将支撑值数量 N 设置为 51,因此分类 DQN 也被称为 C51 算法。因此,我们已经了解了分类 DQN 如何预测价值分布。在下一部分,我们将学习如何基于这个预测的价值分布选择动作。

基于价值分布选择动作

我们已经学过分类 DQN 返回给定状态下每个动作的价值分布。但我们如何根据网络预测的价值分布来选择最佳动作呢?

我们通常基于 Q 值选择动作,也就是说,我们通常选择具有最大 Q 值的动作。但现在我们没有 Q 值,而是有一个价值分布。那我们如何基于价值分布选择动作呢?

首先,我们将从价值分布中提取 Q 值,然后选择具有最大 Q 值的动作。好的,如何提取 Q 值呢?我们可以通过计算价值分布的期望值来获得 Q 值。分布的期望值可以表示为支持度z[i]与它们相应的概率p[i]的乘积的总和。因此,价值分布Z的期望值为:

其中z[i]是支持度,p[i]是概率。

因此,价值分布的 Q 值可以计算为:

在计算 Q 值后,我们选择 Q 值最大的动作作为最佳动作:

让我们准确理解这一过程。假设我们处于状态s,并且在该状态下有两个动作。假设这两个动作分别为updown。首先,我们需要计算支持度。假设支持度的数量N = 3,支持度的最小值为 ,最大值为 。然后,我们计算出的支持度值为[2,3,4]。

现在,我们将支持度与状态s一起输入,然后类别 DQN 返回给定支持度下的动作up和动作down的价值分布概率pi,如图所示:

图 14.6:类别 DQN

现在,如何根据这两个价值分布选择最佳动作呢?首先,我们将从价值分布中提取 Q 值,然后选择具有最大 Q 值的动作。

我们了解到,Q 值可以通过将支持度与相应的概率相乘的和从价值分布中提取出来:

所以,我们可以计算在状态s中动作up的 Q 值,如下所示:

现在,我们可以计算在状态s中动作down的 Q 值,如下所示:

现在,我们选择具有最大 Q 值的动作。由于动作up具有较高的 Q 值,因此我们选择动作up作为最佳动作。

等等!那么,类别 DQN 究竟有什么特别之处?因为像 DQN 一样,我们最终也是根据 Q 值选择动作。一个我们必须注意的重要点是,在 DQN 中,我们是直接根据回报的期望来计算 Q 值的,但在类别 DQN 中,我们首先学习回报分布,然后基于回报分布的期望来计算 Q 值,这样能够捕捉到内在的随机性。

我们已经了解到,类别 DQN 输出给定状态下所有动作的价值分布,然后从中提取 Q 值并选择具有最大 Q 值的动作作为最佳动作。但问题是,我们的类别 DQN 到底是如何学习的?我们如何训练类别 DQN 以预测准确的价值分布?我们将在下一节讨论这个问题。

训练分类 DQN

我们通过最小化目标值分布和预测值分布之间的交叉熵损失来训练分类 DQN。我们如何计算目标分布呢?我们可以通过以下的分布贝尔曼方程来计算目标分布:

其中 表示即时奖励r,这是在执行动作a并从状态s移动到下一个状态 时获得的奖励,因此我们可以将 简单表示为r

记得在 DQN 中,我们使用由 参数化的目标网络计算目标值吗?同样地,这里我们使用由 参数化的目标分类 DQN 来计算目标分布。

计算目标分布后,我们通过最小化目标值分布与预测值分布之间的交叉熵损失来训练网络。这里有一个重要的点需要注意:只有在目标分布和预测分布的支持相等时,才能应用交叉熵损失;如果它们的支持不相等,我们就无法应用交叉熵损失。

例如,图 14.7 显示了目标分布和预测分布的支持相同,(1,2,3,4)。因此,在这种情况下,我们可以应用交叉熵损失:

图 14.7:目标分布与预测分布

图 14.8中,我们可以看到目标分布的支持(1,3,4,5)和预测分布的支持(1,2,3,4)是不同的,因此在这种情况下,我们无法应用交叉熵损失。

图 14.8:目标分布与预测分布

因此,当目标分布和预测分布的支持不同时,我们会执行一个特殊的步骤,称为投影步骤,借助这个步骤,我们可以使目标分布和预测分布的支持相等。一旦我们使目标和预测分布的支持相等,就可以应用交叉熵损失。

在接下来的部分中,我们将学习投影步骤是如何工作的,以及它如何使目标分布和预测分布的支持相等。

投影步骤

让我们通过一个例子来理解投影步骤是如何工作的。假设输入支持是z = [1, 2]。

设预测分布的概率为p = [0.5, 0.5]。图 14.9 显示了预测分布:

图 14.9:预测分布

设目标分布的概率为p = [0.3, 0.7]。设奖励r = 0.1,折扣因子为 。目标分布的支持值计算为 ,因此,我们可以写为:

因此,目标分布变为:

图 14.10:目标分布

从前面的图中我们可以看到,预测分布和目标分布的支持范围不同。预测分布的支持范围是[1, 2],而目标分布的支持范围是[1, 1.9],因此在这种情况下,我们无法直接应用交叉熵损失函数。

现在,使用投影步骤,我们可以将目标分布的支持范围转换为与预测分布相同的支持范围。一旦预测分布和目标分布的支持范围一致,我们就可以应用交叉熵损失函数。

好的,那么这个投影步骤到底是什么呢?我们该如何应用它,将目标分布的支持范围转换为与预测分布的支持范围一致呢?

让我们通过相同的例子来理解这个问题。如下面所示,我们的目标分布支持范围是[1, 1.9],而我们需要将其调整为预测分布支持范围[1, 2],我们该如何操作呢?

图 14.11:目标分布

所以,我们可以做的是将 0.7 的概率从支持 1.9 分配到支持 1 和 2:

图 14.12:目标分布

好的,但我们如何将概率从支持 1.9 分配到支持 1 和 2 呢?是不是应该均匀分配呢?当然不是。由于 2 比 1.9 更接近,我们将更多的概率分配给 2,较少的分配给 1。

图 14.13所示,从 0.7 开始,我们将 0.63 的概率分配给支持 2,0.07 的概率分配给支持 1。

图 14.13:目标分布

因此,现在我们的目标分布将变为:

图 14.14:目标分布

图 14.14中我们可以看到,目标分布的支持范围已从[1, 1.9]变化为[1, 2],现在它与预测分布的支持范围一致。这一步被称为投影步骤。

我们所学到的只是一个简单的例子,假设目标分布和预测分布的支持范围差异很大。在这种情况下,我们无法手动确定该将多少概率分配到各个支持范围,使其相等。因此,我们引入了一套步骤来进行投影,具体步骤如下。执行这些步骤后,我们的目标分布支持范围将通过将概率分配到支持范围上,从而与预测分布支持范围匹配。

首先,我们初始化一个数组m,其形状为支持范围的数量,值为零。m表示在投影步骤后目标分布的分配概率。

对于* j *,遍历支持范围的数量:

  1. 计算目标支持值:

  2. 计算b的值:

  3. 计算下限和上限:

  4. 在下限上分配概率:

  5. 在上限上分配概率:

理解这些投影步骤如何工作有点棘手!所以,让我们通过考虑之前使用的相同示例来理解这一过程。设 z = [1, 2],N = 2,,和

设预测分布的概率为 p = [0.5, 0.5]。图 14.15 显示了预测分布:

图 14.15:预测分布

设目标分布的概率为 p = [0.3, 0.7]。设奖励 r = 0.1 和折扣因子 ,我们知道 ,因此目标分布变为:

图 14.16:目标分布

图 14.16 中,我们可以推断目标分布的支持集与预测分布不同。现在,我们将学习如何使用前述步骤进行投影。

首先,我们初始化一个数组 m,其形状为支持集的大小,并将其值设置为零。即 m = [0, 0]。

迭代,j=0

  1. 计算目标支持值:

  2. 计算 b 的值:

  3. 计算下界和上界:

  4. 在下界上分配概率:

  5. 在上界上分配概率:

在第一次迭代后,m 的值变为 [0, 0]。

迭代,j=1

  1. 计算目标支持值:

  2. 计算 b 的值:

  3. 计算 b 的下界和上界:

  4. 在下界上分配概率:

  5. 在上界上分配概率:

在第二次迭代之后,m 的值变为 [0.07, 0.63]。迭代次数等于我们的支持集的长度。由于支持集的长度为 2,我们将在此停止,因此 m 的值成为我们为修改后的支持集分配的新概率,如 图 14.17 所示:

图 14.17:目标分布

以下代码片段将使我们更清楚投影步骤是如何工作的:

m = np.zeros(num_support)
for j in range(num_support):
    Tz = min(v_max,max(v_min,r+gamma * z[j]))
    bj = (Tz - v_min) / delta_z
    l,u = math.floor(bj),math.ceil(bj)
    pj = p[j] 
    m[int(l)] += pj * (u - bj)
    m[int(u)] += pj * (bj - l) 

现在我们已经了解了如何计算目标值分布以及如何通过投影步骤使目标值分布的支持集等于预测值分布的支持集,接下来我们将学习如何计算交叉熵损失。交叉熵损失的计算公式为:

其中 y 是实际值, 是预测值。因此,我们可以写出:

其中 m 是目标值分布的目标概率,p(s, a) 是预测值分布的预测概率。我们通过最小化交叉熵损失来训练网络。

因此,使用分类 DQN 时,我们根据回报的分布(价值分布)选择动作。在下一节中,我们将把所有这些概念整合在一起,看看分类 DQN 是如何工作的。

将所有内容汇总

首先,我们将主网络参数初始化为随机值,并将目标网络参数初始化为仅通过复制主网络参数来实现。我们还初始化了重放缓冲区

现在,对于每个步骤,在该回合中,我们将环境状态和支持值输入主分类 DQN,该网络由参数化。主网络将支持和环境状态作为输入,并返回每个支持的概率值。然后,可以计算价值分布的 Q 值,作为支持与其概率的乘积之和:

在计算状态中所有动作的 Q 值后,我们选择状态s中 Q 值最大的最佳动作:

然而,不是始终选择 Q 值最大的动作,我们使用 epsilon-greedy 策略选择动作。在 epsilon-greedy 策略中,我们以 epsilon 的概率选择一个随机动作,而以 1-epsilon 的概率选择 Q 值最大的最佳动作。我们执行选择的动作,移动到下一个状态,获得奖励,并将此转移信息存储在重放缓冲区中。

现在,我们从重放缓冲区中采样一个转移,并将下一个状态和支持值输入目标分类 DQN,该网络由参数化。目标网络将支持和下一个状态作为输入,并返回每个支持的概率值。

然后,Q 值可以作为支持与其概率的乘积之和来计算:

在计算所有下一个状态-动作对的 Q 值后,我们选择在状态中 Q 值最大的最佳动作:

现在,我们执行投影步骤。m表示投影步骤后目标分布的分布概率。

对于* j *,在支持数量的范围内:

  1. 计算目标支持值:

  2. 计算b的值:

  3. 计算下界和上界:

  4. 在下界上分配概率:

  5. 在上界上分配概率:

执行投影步骤后,计算交叉熵损失:

其中m是目标值分布的目标概率,p(s, a)是从预测值分布中得到的预测概率。我们通过最小化交叉熵损失来训练网络。

我们并不会在每个时间步更新目标网络参数!。我们会在几个时间步内冻结目标网络参数!,然后将主网络参数!复制到目标网络参数!。我们会在多个回合中不断重复上述步骤,以逼近最优值分布。为了让我们有更详细的理解,接下来会介绍类别化 DQN 算法。

算法 – 类别化 DQN

类别化 DQN 算法的步骤如下:

  1. 使用随机值初始化主网络参数!

  2. 通过复制主网络参数!来初始化目标网络参数!

  3. 初始化重放缓冲区!,支撑数量(原子数),以及!和!

  4. 对于N个回合,执行步骤 5

  5. 对于回合中的每一步,也就是,对于!

    1. 将状态s和支撑值输入到主类别化 DQN 中,由!参数化,得到每个支撑的概率值。然后计算 Q 值为!

    2. 计算完 Q 值后,使用 epsilon-greedy 策略选择动作,即以概率 epsilon 选择随机动作a,以概率 1-epsilon 选择动作!

    3. 执行选择的动作并转移到下一个状态!,并获得奖励r

    4. 将转移信息存储在重放缓冲区!

    5. 随机从重放缓冲区中采样一个转移!

    6. 将下一个状态!和支撑值输入到目标类别化 DQN 中,由!参数化,得到每个支撑的概率值。然后计算该值为!

    7. 计算完 Q 值后,我们选择状态!中的最佳动作,即选择具有最大 Q 值的动作!

    8. 用零值初始化数组m,其形状为支撑的数量

    9. 对于j,遍历支撑的数量:

      1. 计算目标支撑值:!

      2. 计算 b 的值:!

      3. 计算下界和上界:!

      4. 在下界上分配概率:!

      5. 在上界上分配概率:!

    10. 计算交叉熵损失:!

    11. 使用梯度下降最小化损失,并更新主网络的参数:

    12. 冻结目标网络参数 若干时间步长,然后通过简单地复制主网络参数 来更新它:

既然我们已经学习了类别 DQN 算法,为了理解类别 DQN 如何工作,我们将在下一节实现它。

使用类别 DQN 玩 Atari 游戏:

让我们实现类别 DQN 算法来玩 Atari 游戏。本节中使用的代码改编自开源的类别 DQN 实现,github.com/princewen/tensorflow_practice/tree/master/RL/Basic-DisRL-Demo,由 Prince Wen 提供。

首先,让我们导入必要的库:

import numpy as np
import random
from collections import deque
import math
import tensorflow.compat.v1 as tf
tf.disable_v2_behavior()
import gym
from tensorflow.python.framework import ops 

定义变量:

现在,让我们定义一些重要的变量:

初始化

v_min = 0
v_max = 1000 

初始化原子数(支持):

atoms = 51 

设置折扣因子,

gamma = 0.99 

设置批处理大小:

batch_size = 64 

设置我们希望更新目标网络的时间步长:

update_target_net = 50 

设置在 epsilon-贪婪策略中使用的 epsilon 值:

epsilon = 0.5 

定义重放缓冲区:

首先,让我们定义缓冲区长度:

buffer_length = 20000 

将重放缓冲区定义为 deque 结构:

replay_buffer = deque(maxlen=buffer_length) 

我们定义了一个名为 sample_transitions 的函数,它返回从重放缓冲区中随机采样的小批量转换:

def sample_transitions(batch_size):
    batch = np.random.permutation(len(replay_buffer))[:batch_size]
    trans = np.array(replay_buffer)[batch]
    return trans 

定义类别 DQN 类:

定义一个名为 Categorical_DQN 的类,我们将在其中实现类别 DQN 算法。我们不会一次性查看整个代码,而是只查看重要部分。本节中使用的完整代码可以在本书的 GitHub 仓库中找到。

为了更清楚地理解,我们逐行查看代码:

class Categorical_DQN(): 

定义初始化方法:

首先,让我们定义初始化方法:

 def __init__(self,env): 

启动 TensorFlow 会话:

 self.sess = tf.InteractiveSession() 

初始化

 self.v_max = v_max
        self.v_min = v_min 

初始化原子数:

 self.atoms = atoms 

初始化 epsilon 值:

 self.epsilon = epsilon 

获取环境的状态形状:

 self.state_shape = env.observation_space.shape 

获取环境的动作形状:

 self.action_shape = env.action_space.n 

初始化时间步长:

 self.time_step = 0 

初始化目标状态形状:

 target_state_shape = [1]
        target_state_shape.extend(self.state_shape) 

定义状态的占位符:

 self.state_ph = tf.placeholder(tf.float32,target_state_shape) 

定义动作的占位符:

 self.action_ph = tf.placeholder(tf.int32,[1,1]) 

定义 m 值的占位符(目标分布的分布式概率):

 self.m_ph = tf.placeholder(tf.float32,[self.atoms]) 

计算 的值,作为

 self.delta_z = (self.v_max - self.v_min) / (self.atoms - 1) 

计算支持值,作为

 self.z = [self.v_min + i * self.delta_z for i in range(self.atoms)] 

构建类别 DQN:

 self.build_categorical_DQN() 

初始化所有 TensorFlow 变量:

 self.sess.run(tf.global_variables_initializer()) 

构建类别 DQN:

定义一个名为 build_network 的函数,用于构建深度网络。由于我们处理的是 Atari 游戏,我们使用卷积神经网络:

 def build_network(self, state, action, name, units_1, units_2, weights, bias): 

定义第一个卷积层:

 with tf.variable_scope('conv1'):
            conv1 = conv(state, [5, 5, 3, 6], [6], [1, 2, 2, 1], weights, bias) 

定义第二卷积层:

 with tf.variable_scope('conv2'):
            conv2 = conv(conv1, [3, 3, 6, 12], [12], [1, 2, 2, 1], weights, bias) 

将第二卷积层得到的特征图展平:

 with tf.variable_scope('flatten'):
            flatten = tf.layers.flatten(conv2) 

定义第一个全连接层:

 with tf.variable_scope('dense1'):
            dense1 = dense(flatten, units_1, [units_1], weights, bias) 

定义第二个全连接层:

 with tf.variable_scope('dense2'):
            dense2 = dense(dense1, units_2, [units_2], weights, bias) 

将第二个全连接层与动作连接:

 with tf.variable_scope('concat'):
            concatenated = tf.concat([dense2, tf.cast(action, tf.float32)], 1) 

定义第三层,并对第三层的结果应用 softmax 函数,获得每个原子的概率:

 with tf.variable_scope('dense3'):
            dense3 = dense(concatenated, self.atoms, [self.atoms], weights, bias) 
        return tf.nn.softmax(dense3) 

现在,让我们定义一个名为 build_categorical_DQN 的函数,用于构建主类别和目标类别 DQN:

 def build_categorical_DQN(self): 

定义主类别 DQN 并获取概率:

 with tf.variable_scope('main_net'):
            name = ['main_net_params',tf.GraphKeys.GLOBAL_VARIABLES]
            weights = tf.random_uniform_initializer(-0.1,0.1)
            bias = tf.constant_initializer(0.1)
            self.main_p = self.build_network(self.state_ph,self.action_ph,name,24,24,weights,bias) 

定义目标类别 DQN 并获取概率:

 with tf.variable_scope('target_net'):
            name = ['target_net_params',tf.GraphKeys.GLOBAL_VARIABLES]
            weights = tf.random_uniform_initializer(-0.1,0.1)
            bias = tf.constant_initializer(0.1)
            self.target_p = self.build_network(self.state_ph,self.action_ph,name,24,24,weights,bias) 

使用从主类别 DQN 获得的概率计算主要 Q 值,如 所示:

 self.main_Q = tf.reduce_sum(self.main_p * self.z) 

类似地,使用从目标类别 DQN 获得的概率计算目标 Q 值,如 所示:

 self.target_Q = tf.reduce_sum(self.target_p * self.z) 

定义交叉熵损失,如 所示:

 self.cross_entropy_loss = -tf.reduce_sum(self.m_ph * tf.log(self.main_p)) 

定义优化器,并使用 Adam 优化器最小化交叉熵损失:

 self.optimizer = tf.train.AdamOptimizer(0.01).minimize(self.cross_entropy_loss) 

获取主网络参数:

 main_net_params = tf.get_collection("main_net_params") 

获取目标网络参数:

 target_net_params = tf.get_collection('target_net_params') 

定义 update_target_net 操作,通过复制主网络的参数来更新目标网络参数:

 self.update_target_net = [tf.assign(t, e) for t, e in zip(target_net_params, main_net_params)] 

定义训练函数:

让我们定义一个名为 train 的函数来训练网络:

 def train(self,s,r,action,s_,gamma): 

增加时间步数:

 self.time_step += 1 

获取目标 Q 值:

 list_q_ = [self.sess.run(self.target_Q,feed_dict={self.state_ph:[s_],self.action_ph:[[a]]}) for a in range(self.action_shape)] 

选择下一个状态的动作 ,该动作具有最大 Q 值:

 a_ = tf.argmax(list_q_).eval() 

初始化一个数组 m,其形状为支持的数量,并将其值设为零。m 表示在投影步骤后目标分布的分布概率:

 m = np.zeros(self.atoms) 

使用目标类别 DQN 获取每个原子的概率:

 p = self.sess.run(self.target_p,feed_dict = {self.state_ph:[s_],self.action_ph:[[a_]]})[0] 

执行投影步骤:

 for j in range(self.atoms):
            Tz = min(self.v_max,max(self.v_min,r+gamma * self.z[j]))
            bj = (Tz - self.v_min) / self.delta_z 
            l,u = math.floor(bj),math.ceil(bj) 
            pj = p[j]
            m[int(l)] += pj * (u - bj)
            m[int(u)] += pj * (bj - l) 

通过最小化损失来训练网络:

 self.sess.run(self.optimizer,feed_dict={self.state_ph:[s] , self.action_ph:[action], self.m_ph: m }) 

通过复制主网络参数来更新目标网络参数:

 if self.time_step % update_target_net == 0:
            self.sess.run(self.update_target_net) 

选择动作:

让我们定义一个名为 select_action 的函数,用于选择动作:

 def select_action(self,s): 

我们生成一个随机数,如果该数字小于 epsilon,则选择随机动作,否则选择具有最大 Q 值的动作:

 if random.random() <= self.epsilon:
            return random.randint(0, self.action_shape - 1)
        else: 
            return np.argmax([self.sess.run(self.main_Q,feed_dict={self.state_ph:[s],self.action_ph:[[a]]}) for a in range(self.action_shape)]) 

训练网络:

现在,让我们开始训练网络。首先,使用 gym 创建 Atari 游戏环境。让我们创建一个网球游戏环境:

env = gym.make("Tennis-v0") 

创建 Categorical_DQN 类的对象:

agent = Categorical_DQN(env) 

设置回合数:

num_episodes = 800 

对每一回合:

for i in range(num_episodes): 

done 设置为 False

 done = False 

初始化回报:

 Return = 0 

通过重置环境来初始化状态:

 state = env.reset() 

当回合尚未结束时:

 while not done: 

渲染环境:

 env.render() 

选择一个动作:

 action = agent.select_action(state) 

执行选定的动作:

 next_state, reward, done, info = env.step(action) 

更新回报:

 Return = Return + reward 

将过渡信息存储在回放缓冲区中:

 replay_buffer.append([state, reward, [action], next_state]) 

如果回放缓冲区的长度大于或等于缓冲区大小,则开始通过从回放缓冲区中采样过渡来训练网络:

 if len(replay_buffer) >= batch_size:
            trans = sample_transitions(batch_size)
            for item in trans:
                agent.train(item[0],item[1], item[2], item[3],gamma) 

将状态更新为下一个状态:

 state = next_state 

打印回合中获得的回报:

 print("Episode:{}, Return: {}".format(i,Return)) 

现在我们已经了解了类别 DQN 的工作原理及其实现方法,在接下来的章节中,我们将学习另一个有趣的算法。

分位回归 DQN:

在本节中,我们将研究另一种有趣的分布式强化学习算法,称为 QR-DQN。它是一种与类别 DQN 相似的分布式 DQN 算法;然而,它具有一些使其优于类别 DQN 的特性。

数学基础

在继续之前,让我们回顾一下在 QR-DQN 中使用的两个重要概念:

  • 分位数

  • 反向累积分布函数Inverse CDF

分位数

当我们将分布划分为均等的概率区域时,它们被称为分位数。例如,如图 14.18所示,我们将分布划分为两个均等的概率区域,并且我们有两个分位数,每个分位数的概率为 50%:

图 14.18:2-分位数图

反向 CDF(分位函数)

要理解 反向累积分布函数Inverse CDF),首先,让我们了解什么是 累积分布函数CDF)。

考虑一个随机变量 X,并且 P(X) 表示 X 的概率分布。那么,累积分布函数可以表示为:

这基本上意味着 F(x) 可以通过将所有小于或等于 x 的概率相加得到。

让我们看看下面的 CDF:

图 14.19:CDF

在前面的图表中, 表示累积概率,即 。假设 i = 1,那么

CDF 以 x 作为输入,返回累积概率 。因此,我们可以写为:

假设 x = 2,那么我们得到

现在,我们来看看反向 CDF。顾名思义,反向 CDF 是 CDF 的逆函数。也就是说,在 CDF 中,给定支持 x,我们可以得到累积概率 ,而在反向 CDF 中,给定累积概率 ,我们可以得到支持 x。反向 CDF 可以表示为:

以下图表显示了反向 CDF:

图 14.20:反向 CDF

图 14.20所示,给定累积概率 ,我们可以得到支持 x

假设 ,那么我们得到 x = 2。

我们已经学过分位数是均匀分布的概率。如图 14.20所示,我们有三个分位数 q[1] 到 q[3],它们的概率均等,分位数的值分别为 [0.3,0.6,1.0],这些正是我们的累积概率。因此,我们可以说,反向 CDF(分位函数)帮助我们在给定均等分布概率时,得到支持 x 的值。请注意,在反向 CDF 中,支持应该是递增的,因为它是基于累积概率的。

现在我们已经了解了分位数函数,我们将进一步了解如何使用名为 QR-DQN 的算法在分布式强化学习中利用分位数函数。

理解 QR-DQN

在类别 DQN(C51)中,我们了解到,为了预测价值分布,网络将分布的支持集作为输入并返回概率值。

为了计算支持集,我们还需要决定支持集的数量 N、支持集的最小值 和支持集的最大值

如果你还记得在 C51 中,我们的支持值是等间隔固定位置的 ,我们将这个等间隔的支持集作为输入,并得到非均匀概率 。正如 图 14.21 所示,在 C51 中,我们将等间隔的支持集 与状态(s)一起输入到网络,输出非均匀概率

图 14.21:类别 DQN

QR-DQN 可以看作是 C51 的反向操作。在 QR-DQN 中,为了估算价值分布,我们输入均匀概率 ,网络输出在不同位置(不等间隔位置)的支持集 。如以下图所示,我们将均匀概率 与状态(s)一起输入到网络,输出位于不同位置的支持集

图 14.22:QR-DQN

因此,从前两幅图中我们可以观察到,在类别 DQN 中,我们将等间距的固定支持集与状态一起输入到网络,网络返回非均匀概率;而在 QR-DQN 中,我们将均匀的概率与状态一起输入到网络,网络返回在不同位置(不等间隔位置)的支持集。

好的,但这有什么用呢?QR-DQN 究竟是如何工作的?我们来详细探讨一下。

我们理解到,QR-DQN 以均匀概率作为输入,并返回支持值,用于估算价值分布。我们能否利用分位数函数来估算价值分布呢?是的!我们了解到,分位数函数帮助我们根据等分概率获得支持值。因此,在 QR-DQN 中,我们通过估算分位数函数来估算价值分布。

分位数函数表示为:

其中 z 是支持集, 是等分的累积概率。因此,我们可以通过给定 来获得支持集 z

N 为分位数的数量,则概率可以表示为:

例如,如果 N = 4,则 p = [0.25, 0.25, 0.25, 0.25]。如果 N = 5,则 p = [0.20, 0.20, 0.20, 0.20, 0.20]。

一旦我们决定了量化数 N,累积概率 (量化值)可以通过以下方式获得:

例如,如果 N = 4,则 。如果 N = 5,则

我们只需将这个均分的累积概率 (量化值)作为输入馈送给 QR-DQN,它返回支持值。也就是说,我们已经知道 QR-DQN 将值分布估计为量化函数,因此我们只需输入 就能获得值分布的支持值 z

我们用一个简单的例子来理解这个问题。假设我们处于状态 s,并且在该状态下有两个可能的动作 。如图所示,除了将状态 s 作为输入馈送给网络外,我们还将量化值 作为输入,量化值就是均分的累积概率。然后,我们的网络返回 动作的分布支持和 动作的分布支持:

图 14.23:QR-DQN

如果你回想一下,在 C51 中,我们计算了给定状态和动作的概率 p(s, a),而在 QR-DQN 中,我们计算了给定状态和动作的支持 z(s, a)。

请注意,我们使用大写的 Z(s, a) 来表示值分布,小写的 z(s, a) 来表示分布的支持。

类似地,我们也可以使用量化函数来计算目标值分布。然后,我们通过最小化预测量化分布与目标量化分布之间的距离来训练我们的网络。

但根本问题是,我们为什么要这样做?它比 C51 更有利吗?量化回归 DQN 相比于类别 DQN 有几个优点。在量化回归 DQN 中:

  • 我们不需要选择支持的数量和支持的界限,分别是

  • 支持的界限没有限制,因此回报的范围可以在不同状态间变化。

  • 我们还可以摆脱在 C51 中进行的投影步骤,这一步是为了匹配目标分布和预测分布的支持。

QR-DQN 的另一个重要优点是它最小化了预测分布和目标分布之间的 p-Wasserstein 距离。但为什么这很重要呢?最小化目标和预测分布之间的 Wasserstein 距离,帮助我们比最小化交叉熵更好地达到收敛。

好的,p-Wasserstein 距离到底是什么?p-Wasserstein 距离,W[p],是逆 CDF 上的 L^p 度量。假设我们有两个分布 UV,则这两个分布之间的 p-Wasserstein 度量为:

其中, 分别表示分布 UV 的逆 CDF。因此,最小化两个逆 CDF 之间的距离意味着我们最小化 Wasserstein 距离。

我们了解到,在 QR-DQN 中,我们通过最小化预测分布和目标分布之间的距离来训练网络,而这两者都是分位数函数(逆 CDF)。因此,最小化预测分布和目标分布(逆 CDF)之间的距离意味着我们最小化 Wasserstein 距离。

QR-DQN 论文的作者(详见进一步阅读部分)还指出,与其计算分位数值 的支持值,他们建议使用分位数中点值 。分位数中点可以计算为:

即,可以使用分位数中点值来获得支持* z *,如 ,而不是使用分位数值来获得支持,如

那么,为什么使用分位数中点呢?分位数中点作为一个独特的最小化值,即,当我们使用分位数中点值 而不是分位数值 时,两个逆 CDF 之间的 Wasserstein 距离会更小。由于我们正在尝试最小化目标分布和预测分布之间的 Wasserstein 距离,我们可以使用分位数中点 ,使它们之间的距离更小。例如,如 图 14.24 所示,当我们使用分位数中点值 而不是分位数值 时,Wasserstein 距离会更小:

图 14.24:使用分位数中点值代替分位数值

来源 (arxiv.org/pdf/1710.10044.pdf)

简而言之,在 QR-DQN 中,我们将值分布计算为分位数函数。因此,我们只需将均等分配的累积概率输入网络,并获得分布的支持值,通过最小化目标分布和预测分布之间的 Wasserstein 距离来训练网络。

动作选择

在 QR-DQN 中,动作选择与 C51 相同。首先,我们从预测的值分布中提取 Q 值,然后选择具有最大 Q 值的动作。我们可以通过对值分布进行期望操作来提取 Q 值。分布的期望值为支持值与其对应概率的乘积之和。

在 C51 中,我们计算 Q 值的方法是:

其中,pi 是网络为状态 s 和动作 a 给出的概率,z[i] 是支持。

而在 QR-DQN 中,我们的网络输出的是支撑而不是概率。所以,QR-DQN 中的 Q 值可以计算为:

其中zi 是网络为状态s和动作a提供的支撑,p[i]是概率。

计算完 Q 值后,我们选择具有最大 Q 值的动作。例如,假设我们有一个状态s,并且在该状态下有两个动作,分别是updown。在状态s下,动作up的 Q 值计算为:

在状态s下,动作down的 Q 值计算为:

计算完 Q 值后,我们选择具有最大 Q 值的最优动作:

现在我们已经了解了如何在 QR-DQN 中选择动作,在接下来的部分,我们将探讨 QR-DQN 的损失函数。

损失函数

在 C51 中,我们使用交叉熵损失作为我们的损失函数,因为我们的网络预测的是值分布的概率。因此,我们使用交叉熵损失来最小化目标分布和预测分布之间的概率。但在 QR-DQN 中,我们预测的是分布的支撑,而不是概率。也就是说,在 QR-DQN 中,我们将概率作为输入,并预测支撑作为输出。那么,我们如何定义 QR-DQN 的损失函数呢?

我们可以使用分位回归损失来最小化目标支撑和预测支撑之间的距离。但首先,让我们理解如何计算目标支撑值。

在继续之前,让我们回顾一下如何在 DQN 中计算目标值。在 DQN 中,我们使用贝尔曼方程并计算目标值为:

在前面的方程中,我们通过对所有可能的下一个状态-动作对取最大 Q 值来选择动作!

类似地,在 QR-DQN 中,计算目标值时,我们可以使用分布式贝尔曼方程。分布式贝尔曼方程可以表示为:

所以,目标支撑z[j]可以计算为:

要计算状态!的支撑z[j],我们还需要选择某个动作!。我们如何选择一个动作?我们只需使用目标网络计算所有下一个状态-动作对的回报分布,并选择具有最大 Q 值的动作!

现在我们已经了解了如何计算目标支撑值,让我们来看一下如何计算分位回归损失。使用分位回归损失的优点在于它对过高估计和过低估计的误差添加了惩罚。让我们通过一个例子来理解这一点。

假设目标支撑值为[1, 5, 10, 15, 20],而预测支撑值为[100, 5, 10, 15, 20]。如我们所见,预测的支撑值在初始量化时有一个非常高的值,然后开始递减。在逆 CDF 部分,我们学到支撑值应该始终递增,因为它是基于累积概率的。但是,如果你看预测值,支撑值从 100 开始,然后递减。

让我们考虑另一种情况。假设目标支撑值为[1, 5, 10, 15, 20],而预测支撑值为[1, 5, 10, 15, 4]。如我们所见,预测的支撑值从初始量化值开始增加,然后在最后一个量化值时减少到 4。但这种情况不应该发生。因为我们使用的是逆 CDF,我们的支撑值应该始终是递增的。

因此,我们需要确保我们的支撑值应该是递增的,而不是递减的。所以,如果初始量化值被高估,而后续量化值被低估,我们可以对其进行惩罚。也就是说,我们将高估的值乘以,将低估的值乘以。好吧,我们如何判断一个值是高估还是低估的呢?

首先,我们计算目标值与预测值之间的差异。设u为目标支撑值与预测支撑值之间的差异。然后,如果u的值小于 0,我们将u乘以,否则我们将u乘以。这就是量化回归损失

但是量化回归损失的问题在于它在 0 处不会平滑,并且会使梯度保持常数。因此,我们不使用量化回归损失,而是使用一种称为量化 Huber 损失的新修改版损失函数。

为了更好地理解量化 Huber 损失是如何工作的,我们首先来看一下 Huber 损失。设实际值与预测值之间的差异为u。那么 Huber 损失!可以表示为:

设!,那么,当绝对值!小于或等于!时,Huber 损失给出的是二次损失!,否则它是线性损失!

以下 Python 代码片段有助于我们更好地理解 Huber 损失:

def huber_loss(target,predicted, kappa=1):
    #compute u as difference between target and predicted value
    u = target – predicted
    #absolute value of u
    abs_u = abs(u)
    #compute quadratic loss
    quad_loss = 0.5 * (abs_u ** 2) 
    #compute linear loss
    linear_loss = kappa * (abs_u - 0.5 * kappa)
    #true where the absolute value is less than or equal to kappa
    flag = abs_u <= kappa
    #Loss is the quadratic loss where the absolute value is less than kappa 
    #else it is linear loss
    loss = (flag) * quad_loss + (~flag) * linear_loss

    return loss 

现在我们已经理解了 Huber 损失!,让我们来看看量化 Huber 损失。在量化 Huber 损失中,当u(目标支撑值与预测支撑值之间的差异)的值小于 0 时,我们将 Huber 损失!乘以,当u的值大于或等于 0 时,我们将 Huber 损失!乘以

现在我们已经理解了 QR-DQN 的工作原理,在下一节中,我们将介绍另一种有趣的算法——D4PG。

分布式分布式 DDPG

D4PG,即分布式分布式深度确定性策略梯度,是最有趣的策略梯度算法之一。仅凭其名称,我们就可以猜测 D4PG 的工作原理。正如名字所示,D4PG 本质上是 深度确定性策略梯度DDPG)与分布式强化学习的结合,并且以分布式方式工作。感到困惑?让我们更深入地探讨,了解 D4PG 的详细工作原理。

要理解 D4PG 是如何工作的,强烈建议复习我们在第十二章中讲解的 DDPG 算法,学习 DDPG、TD3 和 SAC。我们了解到,DDPG 是一种演员-评论员方法,其中演员尝试学习策略,而评论员则尝试通过 Q 函数评估演员产生的策略。评论员使用深度 Q 网络来估计 Q 函数,演员则使用策略网络来计算策略。因此,演员执行一个动作,而评论员对演员执行的动作给出反馈,并且根据评论员的反馈,演员网络会更新。

D4PG 的工作原理与 DDPG 类似,但在评论员网络中,我们不使用 DQN 来估计 Q 函数,而是可以使用分布式 DQN 来估计值分布。也就是说,在之前的章节中,我们已经学习了几种分布式 DQN 算法,如 C51 和 QR-DQN。因此,在评论员网络中,我们可以使用任何分布式 DQN 算法,例如 C51,而不是常规的 DQN。

除此之外,D4PG 还对 DDPG 架构提出了若干改进。因此,我们将深入探讨并了解 D4PG 与 DDPG 的具体差异。在继续之前,让我们先明确一下符号:

  • 策略网络参数由 表示,目标策略网络参数由 表示。

  • 评论员网络参数由 表示,目标评论员网络参数由 表示。

  • 由于我们讨论的是确定性策略,假设它由 表示,并且我们的策略是通过策略网络参数化的,因此我们可以用 来表示该策略。

现在,我们将了解 D4PG 中评论员和演员网络的具体工作原理。

评论网络

在 DDPG 中,我们了解到我们使用评论员网络来估计 Q 函数。因此,给定一个状态和动作,评论员网络通过 来估计 Q 函数。为了训练评论员网络,我们最小化目标 Q 值(由贝尔曼最优方程给出)和网络预测的 Q 值之间的均方误差(MSE)。

在 DDPG 中,目标值是通过以下公式计算的:

一旦计算出目标值,我们计算目标值和预测值之间的均方误差(MSE)损失,公式为:

其中K表示从重放缓冲区随机采样的转移数量。计算损失后,我们计算梯度!,并使用梯度下降法更新评论员网络参数:

现在,让我们谈谈 D4PG 中的评论员。正如我们在 D4PG 中学到的那样,我们使用分布式 DQN 来估计 Q 值。因此,给定一个状态和动作,评论员网络估计价值分布,表示为!

为了训练评论员网络,我们最小化目标值分布(由分布式 Bellman 方程给出)与网络预测的值分布之间的距离。

D4PG 中的目标值分布计算公式为:

如你所见,方程(2)与(1)相似,唯一的不同是我们将Q替换为Z,这表明我们正在计算目标值分布。D4PG 对目标值计算(2)提出了一个小的改动。我们不再使用一步回报r,而是使用N 步回报,其表达式为:

其中N是转移的长度,我们从重放缓冲区中采样。

在计算目标值分布后,我们可以计算目标值分布与预测值分布之间的距离,计算公式为:

其中d表示用于衡量两个分布之间距离的任何距离度量。假设我们使用 C51,那么d表示交叉熵,K表示从重放缓冲区采样的转移数量。计算损失后,我们计算梯度并更新评论员网络参数。梯度的计算公式为:

D4PG 对我们的梯度更新提出了一个小的改动。在 D4PG 中,我们使用优先经验重放。假设我们的经验重放缓冲区大小为R。重放缓冲区中的每个转移都有一个非均匀的概率p[i]。这种非均匀概率帮助我们赋予某个转移比其他转移更高的权重。假设我们有一个样本i,那么它的概率可以表示为! 或者!。在更新评论员网络时,我们使用!对更新进行加权,从而赋予某些更新更大的重要性。

因此,我们的梯度计算公式为:

计算出梯度后,我们可以使用梯度下降法更新评论员网络参数,如!。现在我们已经理解了 D4PG 中评论员网络的工作原理,接下来让我们看看下一部分的演员网络。

演员网络

首先,让我们快速回顾一下 DDPG 中的演员网络是如何工作的。在 DDPG 中,我们了解到,演员网络将状态作为输入,返回动作:

请注意,我们在连续动作空间中使用确定性策略,并且为了探索新动作,我们只需向 actor 网络生成的动作中添加一些噪声 ,因为动作是连续值。

因此,我们修改后的动作可以表示为:

因此,actor 的目标函数是生成一个最大化评论员网络生成的 Q 值的动作:

其中

我们了解到,为了最大化目标,我们计算目标函数的梯度 ,并通过执行梯度上升来更新 actor 网络参数。

现在让我们谈谈 D4PG。在 D4PG 中,我们执行相同的步骤,唯一的区别是。请注意,在这里我们没有使用评论员中的 Q 函数。相反,我们计算价值分布,因此我们的目标函数变为:

其中,动作为 ,就像我们在 DDPG 中看到的那样,为了最大化目标,我们首先计算目标函数的梯度 。在计算梯度之后,我们通过执行梯度上升来更新 actor 网络参数:

我们了解到,D4PG 是一个分布式算法,这意味着我们不是使用一个 actor,而是使用L 个 actor,每个 actor 并行工作,独立于环境,收集经验,并将经验存储在重放缓冲区中。然后,我们定期更新网络参数给这些 actor。

因此,总结来说,D4PG 与 DDPG 相似,除了以下几点:

  1. 我们在评论员网络中使用分布式 DQN,而不是使用常规的 DQN 来估计 Q 值。

  2. 我们在目标中计算N步的回报,而不是计算一步回报。

  3. 我们使用优先经验重放,并在评论员网络中为梯度更新增加重要性。

  4. 我们使用L个独立的 actor,而不是一个 actor,每个 actor 并行工作,收集经验,并将经验存储在重放缓冲区中。

现在我们已经理解了 D4PG 是如何工作的,将我们学到的所有概念结合起来,接下来让我们看看 D4PG 的算法。

算法 – D4PG

表示我们希望更新目标评论员和 actor 网络参数的时间步骤。我们设置 ,表示我们每 2 步更新一次目标评论员网络和目标 actor 网络参数。类似地,令 表示我们希望将网络权重复制到L个 actor 的时间步骤。我们设置 ,表示我们每 2 步将网络权重复制到 actor。

D4PG 的算法如下所示:

  1. 初始化评论员网络参数 和演员网络参数

  2. 初始化目标评论员网络参数 和目标演员网络参数 ,方法是从 复制

  3. 初始化重放缓冲区

  4. 启动 L 个演员

  5. 对于 N 个回合,重复 步骤 6

  6. 对于每个回合中的每一步,即,!

    1. 从重放缓冲区随机抽取 K 个过渡的迷你批次

    2. 计算评论员的目标值分布,即,!

    3. 计算评论员网络的损失并计算梯度:

    4. 在计算梯度后,使用梯度下降更新评论员网络参数:

    5. 计算演员网络的梯度:

    6. 通过梯度上升更新演员网络参数:

    7. 如果 t 取模,则:

      使用软替换更新目标评论员和目标演员网络参数,分别为

    8. 如果 t 取模,则:

      将网络权重复制到演员中

然后我们在演员网络中执行以下步骤:

  1. 基于策略 和探索噪声选择动作 a,即,!

  2. 执行动作 a,移动到下一个状态 ,获得奖励 r,并将转换信息存储到重放缓冲区

  3. 重复 步骤 1步骤 2,直到学习者完成

因此,我们已经了解了 D4PG 的工作原理。

总结

我们通过理解分布式强化学习的工作原理开始了本章的学习。我们了解到,在分布式强化学习中,不是基于期望回报来选择动作,而是根据回报的分布来选择动作,这通常被称为价值分布或回报分布。

接下来,我们了解了分类 DQN 算法,也称为 C51,其中我们将状态和分布的支持作为输入,网络返回价值分布的概率。我们还学习了投影步骤如何匹配目标和预测值分布的支持,以便我们可以应用交叉熵损失。

接下来,我们了解了分位回归 DQN,在这种方法中,我们将状态和等分的累积概率 作为输入馈送到网络中,网络返回分布的支持值。

在本章结束时,我们了解了 D4PG 的工作原理,也了解了它与 DDPG 的不同之处。

问题

让我们通过回答以下问题来测试我们对分布式强化学习的知识:

  1. 什么是分布式强化学习?

  2. 什么是分类 DQN?

  3. 为什么分类 DQN 被称为 C51 算法?

  4. 什么是分位数函数?

  5. QR-DQN 与分类 DQN 有何不同?

  6. D4PG 与 DDPG 有何不同?

进一步阅读

更多信息,请参考以下论文:

第十五章:模仿学习与逆向强化学习

从示范学习通常被称为模仿学习。在模仿学习的设置中,我们拥有专家的示范,并训练我们的智能体模仿这些专家的示范。通过示范学习有很多好处,包括帮助智能体更快地学习。执行模仿学习有几种方法,其中两种是监督式模仿学习逆向强化学习IRL)。

首先,我们将了解如何使用监督学习来执行模仿学习,然后我们将学习一种叫做数据集聚合DAgger)的算法。接下来,我们将学习如何在 DQN 中使用示范数据,这里有一种叫做深度 Q 学习从示范DQfD)的算法。

接下来,我们将了解 IRL 以及它如何与强化学习不同。我们将学习一种非常流行的 IRL 算法——最大熵逆向强化学习。在本章的最后,我们将了解生成对抗模仿学习GAIL)是如何工作的。

在本章中,我们将学习以下主题:

  • 监督式模仿学习

  • DAgger

  • 深度 Q 学习与示范

  • 逆向强化学习

  • 最大熵逆向强化学习

  • 生成对抗模仿学习

让我们通过理解监督式模仿学习如何工作来开始本章。

监督式模仿学习

在模仿学习的设置中,我们的目标是模仿专家。假设,我们想要训练我们的智能体开车。我们可以通过专家的示范来训练智能体,而不是让它从头开始与环境互动。好,那么什么是专家示范呢?专家示范是一组包含状态-动作对的轨迹,其中每个动作都是由专家执行的。

我们可以训练智能体模仿专家在各种相应状态下执行的动作。因此,我们可以将专家的示范视为训练数据,用来训练我们的智能体。模仿学习的基本思想是模仿(学习)专家的行为。

执行模仿学习的一种最简单和最直接的方法是将模仿学习任务视为监督学习任务。首先,我们收集一组专家示范,然后训练一个分类器,在相应的状态下执行专家所执行的相同动作。我们可以将其视为一个大的多类分类问题,并训练我们的智能体在相应状态下执行专家的动作。

我们的目标是最小化损失 ,其中 是专家的动作, 表示我们智能体执行的动作。

因此,在监督式模仿学习中,我们执行以下步骤:

  1. 收集一组专家示范

  2. 初始化一个策略

  3. 通过最小化损失函数 来学习策略

然而,这种方法存在一些挑战和缺点。智能体的知识仅限于专家的示范(训练数据),因此如果智能体遇到一个在专家示范中不存在的新状态,它将不知道在该状态下该执行什么动作。

比如,我们使用监督模仿学习训练一个智能体驾驶汽车,并让智能体在现实世界中执行任务。如果训练数据中没有智能体遇到交通信号灯的状态,那么我们的智能体将对交通信号灯一无所知。

此外,智能体的准确性高度依赖于专家的知识。如果专家的示范不佳或不最优,那么智能体就无法学习到正确的动作或最优策略。

为了克服监督模仿学习中的挑战,我们引入了一种新的算法,叫做 DAgger。在下一节中,我们将学习 DAgger 如何工作以及它如何克服监督模仿学习的局限性。

DAgger

DAgger 是最常用的模仿学习算法之一。让我们通过一个例子来理解 DAgger 是如何工作的。我们重新回顾一下训练一个智能体驾驶汽车的例子。首先,我们初始化一个空数据集

在第一次迭代中,我们使用某个策略 来驾驶汽车。因此,我们利用该策略 生成一条轨迹 。我们知道,这条轨迹由一系列的状态和动作组成——即由我们的策略 访问的状态以及在这些状态下由我们的策略 执行的动作。现在,我们通过只取我们的策略 访问的状态来创建一个新数据集 ,并请专家为这些状态提供相应的动作。也就是说,我们取轨迹中的所有状态,并请专家为这些状态提供动作。

现在,我们将新数据集 与初始化的空数据集 结合,并更新 ,如:

接下来,我们在这个更新后的数据集上训练一个分类器 ,并学习一个新的策略

在第二次迭代中,我们使用新的策略 生成轨迹,创建一个新数据集 ,只取新的策略 访问的状态,并请专家为这些状态提供相应的动作。

现在,我们将数据集 结合,并更新 ,如:

接下来,我们在这个更新后的数据集上训练一个分类器 ,并学习一个新的策略

在第三次迭代中,我们使用新的策略 来生成轨迹,并通过只采集由新策略 访问的状态,创建一个新的数据集 ,然后我们要求专家提供这些状态的动作。

现在,我们将数据集 合并,并更新 为:

接下来,我们在这个更新后的数据集 上训练一个分类器,并学习一个新的策略 。通过这种方式,DAgger 通过一系列的迭代工作,直到找到最优策略。

现在我们对 Dagger 有了基本的理解;接下来,我们将深入探讨,了解 DAgger 如何找到最优策略。

了解 DAgger

假设我们有一个人类专家,并且用 表示专家策略。我们初始化一个空的数据集 ,并且初始化一个初学者策略

迭代 1

在第一次迭代中,我们创建一个新的策略 ,表示为:

上述方程意味着我们通过结合一定量的专家策略 和一定量的初学者策略 来创建新的策略 。我们取多少专家策略和初学者策略是由参数 决定的。 的值为:

p 的值在 0.1 到 0.9 之间选择。由于我们处于第一次迭代,代入 i = 1,我们可以写成:

因此,代入方程 (1) 中的 ,我们可以写成:

如我们所见,在第一次迭代中,策略 只是一个专家策略 。现在,我们使用这个策略 来生成轨迹。接下来,我们通过收集我们策略 访问的所有状态,创建一个新的数据集 ,并要求专家提供这些状态的动作。因此,我们的数据集将包括

现在,我们将数据集 与初始化的空数据集 合并,并更新 为:

现在,我们有了更新后的数据集 ,我们在这个新数据集上训练一个分类器并提取一个新的策略。设新策略为

迭代 2

在第二次迭代中,我们创建一个新的策略 ,表示为:

上述方程意味着我们通过结合一定量的专家策略 和我们在前一次迭代中获得的策略 来创建一个新的策略 。我们知道,beta 的值选取为: 。因此,我们得到

现在,我们使用这个策略 并生成轨迹。接下来,我们通过收集我们的策略 访问过的所有状态来创建一个新的数据集 ,并请求专家为这些状态提供动作。因此,我们的数据集将由 组成。

现在,我们将数据集 结合,并更新 如下:

现在,我们有了更新的数据集 ,我们在这个新数据集上训练分类器并提取新的策略。让我们将这个新策略命名为

我们重复这些步骤进行多次迭代,以获得最优策略。如我们在每次迭代中所观察到的,我们聚合数据集 并训练分类器以获得新的策略。请注意,值 正在指数衰减。这个现象是合理的,因为在多次迭代过程中,我们的策略会变得更好,因此可以减少专家策略的重要性。

现在我们已经了解了 DAgger 的工作原理,在接下来的章节中,我们将进一步研究 DAgger 算法以加深理解。

算法 – DAgger

DAgger 算法如下所示:

  1. 初始化一个空数据集

  2. 初始化策略

  3. 对于迭代 i = 1 到 N

    1. 创建策略

    2. 使用策略 生成轨迹。

    3. 通过收集策略 访问过的状态以及专家提供的这些状态的动作,创建一个数据集 。因此,

    4. 将数据集聚合为

    5. 在更新后的数据集上训练分类器 并提取新的策略

现在我们已经了解了 DAgger 算法,在接下来的章节中,我们将学习 DQfD。

来自演示的深度 Q 学习

我们了解到,在模仿学习中,我们尝试从专家演示中学习。我们能否在 DQN 中利用专家演示并获得更好的表现呢?答案是肯定的!在这一节中,我们将学习如何使用一种名为 DQfD 的算法,在 DQN 中利用专家演示。

在前面的章节中,我们学习了几种类型的 DQN。我们从基础的 vanilla DQN 开始,然后探索了各种对 DQN 的改进,如双重 DQN、对战 DQN、优先经验重放等。在所有这些方法中,智能体都试图通过与环境互动从零开始学习。智能体与环境互动,并将他们的互动经验存储在一个叫做重放缓冲区的地方,然后基于这些经验进行学习。

为了让智能体表现得更好,它必须从环境中获取大量经验,将其添加到重放缓冲区并进行自我训练。然而,这种方法会消耗大量的训练时间。在我们迄今为止学习的所有方法中,我们一直是在模拟器中训练智能体,因此智能体在模拟器环境中积累经验以提高表现。为了学习最优策略,智能体必须与环境进行大量的互动,其中一些互动可能会给智能体带来很差的奖励。在模拟器环境中,这种情况是可以容忍的。但是我们如何在真实环境中训练智能体呢?我们不能通过直接与真实环境互动并在真实环境中进行大量不良操作来训练智能体。

所以,在这些情况下,我们可以在一个对应于特定真实环境的模拟器中训练智能体。但是问题是,对于大多数用例,很难找到一个准确的模拟器来对应真实环境。不过,我们可以轻松地获得专家示范。

例如,假设我们要训练一个智能体来下棋。假设我们找不到一个准确的模拟器来训练智能体下棋。但是我们可以轻松地获得一个专家下棋的良好示范。

现在,我们能否利用这些专家示范来训练我们的智能体?可以!与其通过与环境互动从零开始学习,不如直接将专家示范添加到重放缓冲区,并基于这些专家示范预训练智能体,这样智能体可以更好、更快地学习。

这就是 DQfD 背后的基本理念。我们将专家示范填充到重放缓冲区中并预训练智能体。请注意,这些专家示范仅用于预训练智能体。一旦智能体完成预训练,它将与环境互动并获取更多经验,利用这些经验进行学习。因此,DQfD 包括两个阶段,分别是预训练和训练。

首先,我们基于专家示范对智能体进行预训练,然后通过与环境的交互来训练智能体。当智能体与环境进行交互时,它收集一些经验,并且智能体的经验(自生成数据)也被加入到回放缓冲区中。智能体利用专家示范和自生成的数据进行学习。我们使用优先经验回放缓冲区,给专家示范数据的优先级高于自生成数据。现在我们对 DQfD 有了基本的了解,让我们深入探讨并了解它是如何工作的。

DQfD 的各个阶段

DQfD 由两个阶段组成:

  • 预训练阶段

  • 训练阶段

预训练阶段

在预训练阶段,智能体不会与环境进行交互。我们直接将专家示范添加到回放缓冲区中,智能体通过从回放缓冲区中抽取专家示范进行学习。

智能体通过最小化损失J(Q)来学习专家示范,使用的是梯度下降法。然而,仅仅通过专家示范进行预训练并不足以使智能体表现得更好,因为专家示范无法包含所有可能的转移。然而,通过专家示范进行预训练为训练我们的智能体提供了一个良好的起点。一旦智能体通过示范完成了预训练,在训练阶段,智能体将能够从初始迭代开始就执行更好的动作,而不是执行随机动作,因此智能体能够更快地学习。

训练阶段

一旦智能体完成预训练,我们就进入训练阶段,在此阶段智能体与环境进行交互,并根据其经验进行学习。由于智能体已经在预训练阶段从专家示范中学到了一些有用的信息,它在环境中将不再执行随机动作。

在训练阶段,智能体与环境进行交互,并将其转移信息(经验)存储在回放缓冲区中。我们了解到,回放缓冲区将预先填充专家示范数据。所以,现在我们的回放缓冲区将由专家示范和智能体的经验(自生成数据)混合组成。我们从回放缓冲区中抽取一个小批量经验并训练智能体。注意,这里我们使用的是优先回放缓冲区,所以在抽样时,我们会优先选择专家示范数据而非智能体生成的数据。通过这种方式,我们通过从回放缓冲区抽样经验并使用梯度下降来最小化损失,从而训练智能体。

我们了解到,智能体与环境进行交互,并将经验存储在重放缓冲区中。如果重放缓冲区已满,则我们会用智能体生成的新转移信息覆盖缓冲区。然而,我们不会覆盖专家示范。因此,专家示范将始终保留在重放缓冲区中,以便智能体可以利用这些专家示范进行学习。

因此,我们已经学习了如何使用专家示范对智能体进行预训练和训练。在下一节中,我们将了解 DQfD 的损失函数。

DQfD 的损失函数

DQfD 的损失函数由四个损失的总和组成:

  1. 双重 DQN 损失

  2. N 步双重 DQN 损失

  3. 监督分类损失

  4. L2 损失

现在,我们将逐个查看这些损失。

双重 DQN 损失 代表 1 步双重 DQN 损失。

N 步双重 DQN 损失 代表 n 步双重 DQN 损失。

监督分类损失 代表监督分类损失。其表达式为:

其中:

  • a[E] 是专家采取的动作。

  • l(a[E], a) 被称为边际函数或边际损失。当所采取的动作等于专家动作 a = a[E] 时,它为 0;否则,它为正值。

L2 正则化损失 代表 L2 正则化损失。它可以防止智能体过度拟合示范数据。

因此,最终的损失函数将是前面四个损失的总和:

其中, 的值作为加权因子,帮助我们控制各个损失的相对重要性。

现在我们已经了解了 DQfD 的工作原理,接下来我们将在下一节中查看 DQfD 的算法。

算法 – DQfD

DQfD 的算法如下所示:

  1. 初始化主网络参数

  2. 通过复制主网络参数 来初始化目标网络参数

  3. 使用专家示范初始化重放缓冲区

  4. 设置 d:我们希望延迟更新目标网络参数的时间步数

  5. 预训练阶段:对于步骤

    1. 从重放缓冲区中采样一个小批量经验

    2. 计算损失 J(Q)

    3. 使用梯度下降更新网络参数

    4. 如果 t mod d = 0:

      通过复制主网络参数 来更新目标网络参数

  6. 训练阶段:对于步骤 t = 1, 2, ..., T

    1. 选择一个动作

    2. 执行所选动作,进入下一个状态,观察奖励,并将该转移信息存储在重放缓冲区中

    3. 从重放缓冲区中采样一个小批量经验,带有优先级

    4. 计算损失 J(Q)

    5. 使用梯度下降更新网络参数

    6. 如果 t mod d = 0:

      通过复制主网络参数 ,更新目标网络参数

就这样!在接下来的章节中,我们将学习一个非常有趣的概念——逆强化学习(IRL)。

逆强化学习

逆强化学习(IRL)是强化学习中最令人兴奋的领域之一。在强化学习中,我们的目标是学习最优策略。也就是说,我们的目标是找到能够提供最大回报(轨迹奖励总和)的最优策略。为了找到最优策略,首先,我们应该知道奖励函数是什么。奖励函数告诉我们,在状态 s 中执行一个动作 a 时,我们获得什么奖励。一旦我们拥有奖励函数,就可以训练我们的智能体学习能够提供最大奖励的最优策略。但问题是,对于复杂任务,设计奖励函数并不是一件容易的事。

考虑为一些任务设计奖励函数,比如智能体学习走路、自驾车等。在这些情况下,设计奖励函数并不简单,涉及为各种智能体行为分配奖励。例如,考虑为一个学习驾驶的智能体设计奖励函数。在这种情况下,我们需要为智能体的每个行为分配奖励。例如,如果智能体遵守交通信号、避开行人、没有撞到任何物体,我们可以为这些行为分配较高的奖励。但是以这种方式设计奖励函数并不是最优的,并且很可能会遗漏一些智能体的行为。

好的,现在问题是我们能否学习到奖励函数?当然可以!如果我们有专家演示,那么我们可以从专家演示中学习奖励函数。但是,我们具体该如何做呢?这就是 IRL 帮助我们的地方。顾名思义,IRL 是强化学习的逆过程。

在强化学习(RL)中,我们试图在给定奖励函数的情况下找到最优策略,但在逆强化学习(IRL)中,我们试图根据专家演示学习奖励函数。一旦我们通过 IRL 从专家演示中推导出奖励函数,就可以使用该奖励函数训练我们的智能体,通过任何强化学习算法学习最优策略。

IRL 包含几个有趣的算法。在接下来的章节中,我们将学习其中一个最流行的 IRL 算法——最大熵逆强化学习(maximum entropy IRL)。

最大熵逆强化学习

在这一节中,我们将学习如何使用一种名为最大熵逆强化学习(maximum entropy IRL)的 IRL 算法,从给定的专家演示集中提取奖励函数。在深入了解最大熵逆强化学习之前,让我们先学习一些理解最大熵逆强化学习运作所必需的重要术语。

关键术语

特征向量 – 我们可以通过特征向量 f 来表示状态。假设我们有一个状态 s,那么它的特征向量可以定义为 f[s]。

特征计数 – 假设我们有一条轨迹 ;那么该轨迹的特征计数定义为轨迹中所有状态的特征向量之和:

其中 表示轨迹 的特征计数。

奖励函数 – 奖励函数可以定义为特征的线性组合,即特征向量乘以权重的和

其中 表示权重,f[s] 表示特征向量。注意,这个 就是我们要学习的内容。当我们获得最优的 时,我们将拥有正确的奖励函数。我们将在下一节学习如何找到最优的

我们可以用 Sigma 表示法将前面的方程式表示为:

我们知道特征计数是轨迹中所有状态的特征向量之和,因此根据(2),我们可以将前面的方程式重写为:

因此,轨迹的奖励就是权重与轨迹的特征计数相乘。

回到最大熵逆向强化学习

现在,让我们了解最大熵逆向强化学习(IRL)是如何工作的。假设我们有专家演示 。我们的目标是从给定的专家演示中学习奖励函数。我们该如何做呢?

我们已经了解到奖励函数是 。找到最优参数 帮助我们学习正确的奖励函数。所以,我们将从专家演示中采样一条轨迹 并通过找到最优参数 来学习奖励函数。

从专家演示中采样轨迹的概率与奖励函数的指数成正比。也就是说,获得更多奖励的轨迹比获得更少奖励的轨迹更有可能从我们的演示中被采样:

概率应该在 0 到 1 之间,对吗?但是 的值不会在 0 到 1 之间。所以,为了归一化这个值,我们引入了 z,它作为归一化常数,并定义为 。我们可以用 z 重写前面的方程式:

现在,我们的目标是最大化 ,即最大化选择那些获得更多奖励的轨迹的对数概率。因此,我们可以定义我们的目标函数为:

其中 M 表示演示次数。

将 (3) 代入 (4),我们可以写成:

根据对数规则,,我们可以写成:

对数项和指数项相互抵消,所以前面的方程变为:

我们知道 ;将 z 的值代入后,我们可以重写前面的方程为:

我们知道 ;将 的值代入后,最终简化后的目标函数为:

为了找到最优参数 ,我们计算前述目标函数 的梯度,并将 的值更新为 。在下一部分,我们将学习如何计算梯度

计算梯度

我们了解到我们的目标函数为:

现在,我们计算关于 的目标函数梯度。经过计算,我们得到的梯度为:

特征计数的平均值即为特征期望 ,因此我们可以代入 并将前面的方程重写为:

我们可以通过将轨迹的所有状态结合起来,重写前面的方程:

因此,使用前面的方程,我们计算梯度并更新参数 。如果你看前面的方程,我们可以轻松计算出第一个项,它就是特征期望 ,但是第二项中的 怎么办? 被称为状态访问频率,它表示处于给定状态的概率。那么,我们如何计算 呢?

如果我们有一个策略 ,那么我们可以使用该策略计算状态访问频率。但我们还没有任何策略。因此,我们可以使用动态规划方法,比如值迭代,来计算策略。然而,为了通过值迭代方法计算策略,我们需要一个奖励函数。因此,我们只需输入我们的奖励函数 ,然后通过值迭代提取策略。接着,使用提取的策略计算状态访问频率。

使用策略 计算状态访问频率的步骤如下:

  1. 设在时间 t 访问状态 s 的概率为 。我们可以写出在第一次时间步 t = 1 访问初始状态 s[1] 的概率为:

  2. 然后对于时间步 t = 1 到 T

    计算

  3. 计算状态访问频率为

为了清楚理解最大熵 IRL 的工作原理,让我们在下一节中深入研究该算法。

算法 – 最大熵 IRL

最大熵 IRL 算法如下所示:

  1. 初始化参数 并收集演示

  2. 对于 N 次迭代:

    1. 计算奖励函数

    2. 使用上一阶段获得的奖励函数通过价值迭代计算策略

    3. 使用上一阶段获得的策略计算状态访问频率

    4. 计算相对于 的梯度,即

    5. 更新 的值为

因此,经过一系列迭代,我们将找到一个最优的参数 。一旦得到 ,我们就可以使用它来定义正确的奖励函数 。在下一节中,我们将学习 GAIL。

生成对抗模仿学习

生成对抗模仿学习 (GAIL) 是另一个非常流行的 IRL 算法。顾名思义,它基于 生成对抗网络 (GANs),这是我们在 第七章深度学习基础》中学习过的内容。要理解 GAIL 的工作原理,我们首先应该回顾 GAN 的工作原理。

在 GAN 中,我们有两个网络:一个是生成器,另一个是判别器。生成器的作用是通过学习输入数据集的分布生成新的数据点。判别器的作用是分类给定的数据点是由生成器(学习的分布)生成的,还是来自真实的数据分布。

最小化 GAN 的损失函数意味着最小化 Jensen Shannon (JS) 散度,JS 散度用于度量两个概率分布之间的差异。因此,当真实数据分布和假数据分布(学习的分布)之间的 JS 散度为零时,表示真实和假数据分布是相等的,即我们的生成器网络成功地学习到了真实分布。

现在,让我们学习如何在 IRL 设置中使用 GAN。首先,我们引入一个新的术语叫做 占据度量。它定义为我们的智能体在使用某个策略 探索环境时,遇到的状态和动作的分布。简单来说,它基本上是按照策略 跟随的状态-动作对的分布。策略 的占据度量用 表示。

在模仿学习设置中,我们有一个专家策略,记作 。同样,记智能体的策略为 。现在,我们的目标是让智能体学习专家策略。我们怎么做呢?如果我们使专家策略和智能体策略的占用度量相等,那么就意味着智能体已经成功学习了专家策略。也就是说,占用度量是遵循某个策略的状态-动作对的分布。如果我们能使智能体策略的状态-动作对分布等于专家策略的状态-动作对分布,那么就意味着我们的智能体学到了专家策略。接下来,我们将探讨如何使用 GAN 实现这一目标。

我们可以将专家策略的占用度量看作真实数据分布,将智能体策略的占用度量看作伪数据分布。因此,最小化专家策略 和智能体策略 的占用度量之间的 JS 散度,就意味着智能体会学习专家策略。

在 GAN 中,我们知道生成器的作用是通过学习给定数据集的分布来生成新的数据点。同样,在 GAIL 中,生成器的作用是通过学习专家策略的分布(占用度量)来生成新的策略。判别器的作用是分类给定的策略是专家策略还是智能体策略。

在 GAN 中,我们知道,对于生成器来说,最优的判别器是无法区分真实数据和伪数据分布的;同样,在 GAIL 中,最优的判别器是无法区分生成的状态-动作对是来自智能体策略还是来自专家策略。

为了更清楚地理解,我们通过将 GAIL 中的术语与 GAN 术语进行对比,来了解我们在 GAIL 中使用的术语:

  • 真实数据分布 – 专家策略的占用度量

  • 伪数据分布 – 智能体策略的占用度量

  • 真实数据 – 专家策略生成的状态-动作对

  • 伪数据 – 智能体策略生成的状态-动作对

简而言之,我们使用生成器生成状态-动作对,目的是让判别器无法分辨该状态-动作对是通过专家策略还是通过智能体策略生成的。生成器和判别器都是神经网络。我们通过 TRPO 训练生成器,生成一个与专家策略相似的策略。判别器是一个分类器,并使用 Adam 优化。因此,我们可以定义 GAIL 的目标函数如下:

其中 是生成器的参数, 是判别器的参数。

现在我们已经理解了 GAIL 是如何工作的,接下来让我们更详细地学习如何推导前面的方程。

GAIL 的公式化

在本节中,我们探讨了 GAIL 的数学原理,并看它是如何工作的。如果你对数学不感兴趣,可以跳过这一部分。我们知道,在强化学习中,我们的目标是找到一个给出最大奖励的最优策略。它可以表示为:

我们可以通过添加策略的熵来重新定义我们的目标函数,如下所示:

前面的方程告诉我们,我们可以在最大化奖励的同时,也最大化策略的熵。我们可以将目标函数定义为成本,而不是奖励。

也就是说,我们可以将我们的 RL 目标函数定义为成本,因为我们的目标是找到一个最小化成本的最优策略;这可以表示为:

其中c是成本。因此,给定成本函数,我们的目标是找到最小化成本的最优策略。

现在,让我们谈谈 IRL。我们了解到,在 IRL 中,我们的目标是从给定的专家示范集中找出奖励函数。我们也可以将 IRL 的目标定义为成本,而不是奖励。也就是说,我们可以将 IRL 的目标函数定义为成本,因为我们的目标是找到一个使专家示范最优的成本函数。该目标可以通过最大因果熵 IRL 表示为:

前面的方程意味着什么?在 IRL 设置中,我们的目标是根据专家示范(专家策略)学习成本函数。我们知道专家策略的表现优于其他策略,因此我们尝试学习成本函数c,该函数将低成本分配给专家策略,并将高成本分配给其他策略。因此,前面的目标函数意味着我们尝试找到一个成本函数,该函数将低成本分配给专家策略,并将高成本分配给其他策略。

为了减少过拟合,我们引入正则化项!,并将前面的方程改写为:

从方程(6)我们了解到,在强化学习设置中,给定一个成本,我们可以获得最优策略;而从(7)我们了解到,在逆向强化学习(IRL)设置中,给定专家策略(专家示范),我们可以获得成本。因此,从(6)和(7)可以观察到,IRL 的输出可以作为 RL 的输入。也就是说,IRL 的结果是成本函数,我们可以将该成本函数作为输入,在 RL 中学习最优策略。因此,我们可以写出!,这意味着 IRL 的结果作为输入传递给 RL。

我们可以将其表示为函数组合形式:

在方程(8)中,适用以下内容:

  • 是正则化器 的凸共轭。

  • 是智能体策略的占用度量。

  • 是专家策略的占用度量。

要了解公式 (8) 是如何推导出来的,可以参考本章末尾的进一步阅读部分中的 GAIL 论文。目标函数(公式 (8))意味着我们试图找到一个最优策略,其占用度量接近专家策略的占用度量。智能体策略和专家策略之间的占用度量通过 进行度量。对于正则化器 ,有几种选择。我们使用生成对抗正则化器 ,并将我们的方程写为:

因此,最小化 基本上意味着我们最小化智能体策略 和专家策略 之间的 JS 散度。因此,我们可以将方程 (9) 的右侧重写为:

其中 只是策略正则化器。我们知道,通过 GAN 最小化智能体策略 和专家策略 之间的 JS 散度,因此我们可以将前面的方程中的 替换为 GAN 目标函数,如下所示:

其中 是生成器的参数, 是判别器的参数。

因此,我们的 GAIL 最终目标函数变为:

目标方程意味着我们可以通过最小化专家策略和智能体策略的占用度量来找到最优策略,我们通过使用 GAN 来最小化它。

生成器的作用是通过学习专家策略的占用度量来生成一个策略,而判别器的作用是判断生成的策略是来自专家策略还是智能体策略。因此,我们使用 TRPO 训练生成器,而判别器基本上是一个神经网络,用来告诉我们生成的策略是专家策略还是智能体策略。

总结

我们通过了解模仿学习是什么以及监督模仿学习如何运作来开始本章内容。接下来,我们学习了 DAgger 算法,在该算法中,我们将通过一系列迭代获得的数据集进行聚合,并学习最优策略。

在看完 DAgger 后,我们学习了 DQfD,其中我们通过专家演示预填充回放缓冲区,并在训练阶段之前用专家演示预训练智能体。

接下来,我们学习了逆向强化学习(IRL)。我们理解到,在强化学习中,我们尝试根据奖励函数寻找最优策略,而在 IRL 中,我们则是根据专家示范来学习奖励函数。当我们通过 IRL 从专家示范中推导出奖励函数时,就可以使用该奖励函数来训练我们的智能体,借助任何强化学习算法来学习最优策略。然后,我们探索了如何使用最大熵 IRL 算法来学习奖励函数。

在本章的最后,我们了解了 GAIL,其中我们使用生成对抗网络(GAN)来学习最优策略。在下一章,我们将探索一个名为 Stable Baselines 的强化学习库。

问题

让我们评估一下我们对模仿学习和逆向强化学习(IRL)的理解。尝试回答以下问题:

  1. 监督式模仿学习是如何工作的?

  2. DAgger 与监督式模仿学习有何不同?

  3. 解释 DQfD 的不同阶段。

  4. 我们为什么需要逆向强化学习(IRL)?

  5. 什么是特征向量?

  6. GAIL 是如何工作的?

进一步阅读

更多信息,请参考以下论文:

第十六章:使用 Stable Baselines 进行深度强化学习

到目前为止,我们已经学习了各种深度强化学习RL)算法。如果我们有一个库,可以轻松实现深度 RL 算法,那该多好呢?是的!目前有多个库可以轻松构建深度 RL 算法。

一种流行的深度强化学习库是 OpenAI Baselines。OpenAI Baselines 提供了许多深度强化学习算法的高效实现,使得它们更容易使用。然而,OpenAI Baselines 并没有提供很好的文档。因此,我们将关注 OpenAI Baselines 的一个分支——Stable Baselines

Stable Baselines 是 OpenAI Baselines 的改进版。Stable Baselines 更易于使用,它还包括最先进的深度 RL 算法和一些有用的功能。我们可以使用 Stable Baselines 快速原型化 RL 模型。

让我们从安装 Stable Baselines 开始本章内容,然后我们将学习如何使用该库创建第一个代理。接下来,我们将学习向量化环境。然后,我们将学习如何使用 Stable Baselines 实现几种深度强化学习算法,并探索基准库的各种功能。

本章我们将学习以下内容:

  • 安装 Stable Baselines

  • 使用 Stable Baselines 创建我们的第一个代理

  • 使用向量化环境进行多进程处理

  • 使用 DQN 及其变种玩 Atari 游戏

  • 使用 A2C 进行月球着陆任务

  • 使用 DDPG 摆动起摆锤

  • 使用 TRPO 训练代理行走

  • 实现 GAIL

让我们从安装 Stable Baselines 开始本章内容。

安装 Stable Baselines

首先,让我们安装所需的依赖项:

sudo apt-get update && sudo apt-get install cmake libopenmpi-dev zlib1g-dev 

一些深度 RL 算法需要 MPI 才能运行,因此,让我们安装 MPI:

sudo pip install mpi4py 

现在,我们可以通过pip安装 Stable Baselines:

pip install stable-baselines[mpi] 

请注意,目前 Stable Baselines 仅支持 TensorFlow 1.x 版本。因此,请确保你在使用 TensorFlow 1.x 版本运行 Stable Baselines 实验。

现在我们已经安装了 Stable Baselines,让我们看看如何使用它创建第一个代理。

使用 Stable Baselines 创建我们的第一个代理

现在,让我们使用 Stable Baselines 构建我们的第一个深度 RL 算法。我们将使用深度 Q 网络DQN)创建一个简单的代理,用于山地汽车爬坡任务。我们知道,在山地汽车爬坡任务中,汽车被放置在两座山之间,代理的目标是驾车爬上右边的山。

首先,我们从stable_baselines导入gymDQN

import gym
from stable_baselines import DQN 

创建一个山地汽车环境:

env = gym.make('MountainCar-v0') 

现在,让我们实例化我们的代理。正如我们在下面的代码中看到的,我们传递了MlpPolicy,这意味着我们的网络是一个多层感知机:

agent = DQN('MlpPolicy', env, learning_rate=1e-3) 

现在,让我们通过指定训练的时间步数来训练代理:

agent.learn(total_timesteps=25000) 

就这样。构建一个 DQN 代理并训练它就这么简单。

评估训练后的代理

我们还可以通过使用evaluate_policy来评估训练后的代理,查看平均奖励:

from stable_baselines.common.evaluation import evaluate_policy 

在下面的代码中,agent是训练好的代理,agent.get_env()获取我们训练代理的环境,n_eval_episodes表示我们需要评估代理的集数:

mean_reward, n_steps = evaluate_policy(agent, agent.get_env(), n_eval_episodes=10) 

存储和加载训练好的代理

使用 Stable Baselines,我们还可以将训练好的代理保存到磁盘并从磁盘加载。

我们可以如下保存代理:

agent.save("DQN_mountain_car_agent") 

保存之后,我们可以如下加载代理:

agent = DQN.load("DQN_mountain_car_agent") 

查看训练好的代理

训练后,我们还可以看看我们的训练代理在环境中的表现。

初始化状态:

state = env.reset() 

对于 5000 步:

for t in range(5000): 

使用我们训练好的代理预测在给定状态下执行的动作:

 action, _ = agent.predict(state) 

执行预测的动作:

 next_state, reward, done, info = env.step(action) 

state更新为当前状态:

 state = next_state 

渲染环境:

 env.render() 

现在,我们可以看到训练好的代理在环境中的表现:

图 16.1:代理学习爬山

将所有内容结合起来

现在,让我们看一下结合我们迄今为止学到的所有内容的最终代码:

#import the libraries
import gym
from stable_baselines import DQN
from stable_baselines.common.evaluation import evaluate_policy
#create the gym environment
env = gym.make('MountainCar-v0')
#instantiate the agent
agent = DQN('MlpPolicy', env, learning_rate=1e-3)
#train the agent
agent.learn(total_timesteps=25000)
#evaluate the agent
mean_reward, n_steps = evaluate_policy(agent, agent.get_env(), n_eval_episodes=10)
#save the trained agent
agent.save("DQN_mountain_car_agent")
#view the trained agent
state = env.reset()
for t in range(5000):
    action, _ = agent.predict(state)
    next_state, reward, done, info = env.step(action)
    state = next_state
    env.render() 

既然我们已经对如何使用 Stable Baselines 有了基本的了解,接下来我们将详细探讨它。

向量化环境

Stable Baselines 的一个非常有趣且有用的功能是,我们可以在多个独立环境中训练我们的代理,既可以在单独的进程中(使用SubprocVecEnv),也可以在同一个进程中(使用DummyVecEnv)。

例如,假设我们正在一个平衡小车环境中训练我们的代理——我们可以不只在一个小车平衡环境中训练,而是在多个小车平衡环境中训练我们的代理。

我们通常每一步训练代理时只使用一个环境,但现在我们可以每一步在多个环境中训练代理。这有助于代理更快地学习。现在,我们的状态、动作、奖励和结束状态将以向量的形式呈现,因为我们正在多个环境中训练代理。因此,我们称之为向量化环境。

Stable Baselines 提供了两种类型的向量化环境:

  • SubprocVecEnv

  • DummyVecEnv

SubprocVecEnv

在子进程向量化环境中,我们在单独的进程中运行每个环境(利用多进程)。现在,让我们看看如何创建子进程向量化环境。

首先,让我们导入SubprocVecEnv

from stable_baselines.common.vec_env import SubprocVecEnv
from stable_baselines.common import set_global_seeds 

接下来,我们创建一个名为make_env的函数,用于初始化我们的环境:

def make_env(env_name, rank, seed=0):
    def _init():
        env = gym.make(env_name)
        env.seed(seed + rank)
        return env
    set_global_seeds(seed)
    return _init 

然后,我们可以如下创建子进程向量化环境:

env_name = 'Pendulum-v0'
num_process = 2
env = SubprocVecEnv([make_env(env_name, i) for i in range(num_process)]) 

DummyVecEnv

在虚拟向量化环境中,我们在当前的 Python 进程中按顺序运行每个环境。它不支持多进程。现在,让我们看看如何创建虚拟向量化环境。

首先,让我们导入DummyVecEnv

from stable_baselines.common.vec_env import DummyVecEnv 

接下来,我们可以如下创建虚拟向量化环境:

env_name = 'Pendulum-v0'
env = DummyVecEnv([lambda: gym.make(env_name)]) 

既然我们已经学会了如何在多个独立环境中使用向量化环境训练代理,接下来的部分,我们将学习如何将自定义环境集成到 Stable Baselines 中。

集成自定义环境

我们还可以使用 Stable Baselines 在自定义环境中训练智能体。在创建自定义环境时,我们需要确保我们的环境遵循 Gym 接口。也就是说,我们的环境应该包含如stepresetrender等方法。

假设我们自定义环境的名称是CustomEnv。首先,我们按如下方式实例化自定义环境:

env = CustomEnv() 

接下来,我们可以像往常一样在自定义环境中训练我们的智能体:

agent = DQN('MlpPolicy', env, learning_rate=1e-3)
agent.learn(total_timesteps=25000) 

就这样。在下一部分,让我们学习如何使用 DQN 及其变体来玩 Atari 游戏。

使用 DQN 及其变体玩 Atari 游戏

现在,让我们学习如何创建一个 DQN 来玩 Atari 游戏,使用 Stable Baselines。首先,让我们导入必要的模块:

from stable_baselines import DQN 

由于我们处理的是 Atari 游戏,我们可以使用卷积神经网络(CNN)而不是普通的神经网络。所以,我们使用CnnPolicy

from stable_baselines.deepq.policies import CnnPolicy 

我们了解到,在将游戏画面输入智能体之前,我们需要对其进行预处理。使用 Stable Baselines 时,我们不需要手动预处理;相反,我们可以使用make_atari模块,它会负责预处理游戏画面:

from stable_baselines.common.atari_wrappers import make_atari 

现在,让我们创建一个 Atari 游戏环境。我们先创建冰球游戏环境:

env = make_atari('IceHockeyNoFrameskip-v4') 

实例化智能体:

agent = DQN(CnnPolicy, env, verbose=1) 

训练智能体:

agent.learn(total_timesteps=25000) 

在训练完智能体之后,我们可以查看训练后的智能体在环境中的表现:

state = env.reset()
while True:
    action, _ = agent.predict(state)
    next_state, reward, done, info = env.step(action)
    state = next_state
    env.render() 

上述代码展示了我们的训练智能体如何玩冰球游戏:

图 16.2:智能体正在玩冰球游戏

实现 DQN 变体

我们刚刚学习了如何使用 Stable Baselines 实现 DQN。现在,让我们看看如何实现 DQN 的变体,如双重 DQN、带优先经验回放的 DQN 和对抗 DQN。在 Baselines 中实现 DQN 变体非常简单。

首先,我们定义我们的关键字参数如下:

kwargs = {"double_q": True, "prioritized_replay": True, "policy_kwargs": dict(dueling=True)} 

现在,在实例化智能体时,我们只需要传递关键字参数:

agent = DQN(CnnPolicy, env, verbose=1, **kwargs) 

然后,我们可以像往常一样训练智能体:

agent.learn(total_timesteps=25000) 

就这样!现在我们已经有了带优先经验回放的对抗双重 DQN。接下来的部分,我们将学习如何使用优势演员-评论家算法A2C)玩月球着陆游戏。

使用 A2C 进行月球着陆

让我们学习如何使用 Stable Baselines 实现 A2C 来处理月球着陆任务。在月球着陆环境中,我们的智能体驾驶太空飞行器,目标是准确地在着陆平台上着陆。如果我们的智能体(着陆器)偏离着陆平台,那么它将失去奖励,且如果智能体坠毁或停下,回合将会终止。环境的动作空间包括四个离散动作:什么也不做、启动左侧定向引擎、启动主引擎以及启动右侧定向引擎。现在,让我们看看如何使用 A2C 训练智能体,以便正确地在着陆平台上着陆。

首先,让我们导入必要的库:

import gym
from stable_baselines.common.policies import MlpPolicy
from stable_baselines.common.vec_env import DummyVecEnv
from stable_baselines.common.evaluation import evaluate_policy
from stable_baselines import A2C 

使用 Gym 创建月球着陆环境:

env = gym.make('LunarLander-v2') 

让我们使用虚拟向量化环境。我们知道,在虚拟向量化环境中,我们在同一个进程中运行每个环境:

env = DummyVecEnv([lambda: env]) 

创建智能体:

agent = A2C(MlpPolicy, env, ent_coef=0.1, verbose=0) 

训练智能体:

agent.learn(total_timesteps=25000) 

训练结束后,我们可以通过查看平均奖励来评估我们的智能体:

mean_reward, n_steps = evaluate_policy(agent, agent.get_env(), n_eval_episodes=10) 

我们还可以看看训练后的智能体在环境中的表现:

state = env.reset()
while True:
    action, _states = agent.predict(state)
    next_state, reward, done, info = env.step(action)
    state = next_state
    env.render() 

上述代码将展示我们训练后的智能体如何成功降落在着陆平台上:

图 16.3:智能体玩月球着陆者游戏

创建自定义网络

在上一节中,我们学习了如何使用 Stable Baselines 创建 A2C。我们可以定制网络架构吗?当然可以!通过 Stable Baselines,我们还可以使用自定义的网络架构。让我们来看一下如何做到这一点。

首先,让我们导入前馈策略(前馈网络):

from stable_baselines.common.policies import FeedForwardPolicy 
net_arch=[dict(pi=[128, 128, 128], vf=[128, 128, 128])], which specifies our network architecture. pi represents the architecture of the policy network and vf represents the architecture of value network: 
class CustomPolicy(FeedForwardPolicy):
    def __init__(self, *args, **kargs):
        super(CustomPolicy, self).__init__(*args, **kargs,
                                           net_arch=[dict(pi=[128, 128, 128], vf=[128, 128, 128])], feature_extraction="mlp") 

我们可以通过以下方式使用自定义策略来实例化智能体:

agent = A2C(CustomPolicy, 'LunarLander-v2', verbose=1) 

现在,我们可以像往常一样训练智能体:

agent.learn(total_timesteps=25000) 

就是这样。类似地,我们可以创建自己的自定义网络。在下一节中,让我们学习如何使用深度确定性策略梯度(DDPG)算法执行倒立摆摆动任务。

使用 DDPG 摆动倒立摆

让我们学习如何使用 Stable Baselines 实现倒立摆摆动任务的 DDPG。首先,让我们导入必要的库:

import gym
import numpy as np
from stable_baselines.ddpg.policies import MlpPolicy
from stable_baselines.common.noise import NormalActionNoise, OrnsteinUhlenbeckActionNoise, AdaptiveParamNoiseSpec
from stable_baselines import DDPG 

使用 Gym 创建倒立摆环境:

env = gym.make('Pendulum-v0') 

获取动作的数量:

n_actions = env.action_space.shape[-1] 

我们知道,在 DDPG 中,我们不是直接选择动作,而是使用 Ornstein-Uhlenbeck 过程添加一些噪声,以确保探索。因此,我们创建动作噪声如下:

action_noise = OrnsteinUhlenbeckActionNoise(mean=np.zeros(n_actions), sigma=float(0.5) * np.ones(n_actions)) 

实例化智能体:

agent = DDPG(MlpPolicy, env, verbose=1, param_noise=None, action_noise=action_noise) 

训练智能体:

agent.learn(total_timesteps=25000) 

在训练完智能体后,我们还可以通过渲染环境来查看训练后的智能体如何摆动倒立摆。我们也可以查看 DDPG 的计算图吗?是的!在下一节中,我们将学习如何做到这一点。

在 TensorBoard 中查看计算图

使用 Stable Baselines,我们可以更轻松地在 TensorBoard 中查看模型的计算图。为了做到这一点,我们只需要在实例化智能体时传递将存储日志文件的目录,如下所示:

agent = DDPG(MlpPolicy, env, verbose=1, param_noise=None, action_noise=action_noise, tensorboard_log="logs") 

然后,我们可以训练智能体:

agent.learn(total_timesteps=25000) 

训练结束后,打开终端并输入以下命令来运行 TensorBoard:

tensorboard --logdir logs 

如我们所见,我们现在可以查看 DDPG 模型(智能体)的计算图:

图 16.4:DDPG 的计算图

图 16.4中,我们可以理解如何生成 DDPG 的计算图,就像我们在第十二章中学习的那样,学习 DDPG、TD3 和 SAC

现在,让我们展开并深入查看模型节点,以便更清晰地理解:

图 16.5:DDPG 的计算图

如从图 16.5所示,我们的模型包括策略(演员)和 Q(评论员)网络。

现在我们已经学习了如何使用 Stable Baselines 实现 DDPG 来完成倒立摆摆动任务,接下来我们将学习如何使用 Stable Baselines 实现 TRPO。

使用 TRPO 训练一个走路的智能体

在本节中,我们将学习如何使用 信任区域策略优化 (TRPO) 训练智能体走路。我们将使用 MuJoCo 环境来训练智能体。MuJoCo 代表 带接触的多关节动力学,是用于训练智能体执行连续控制任务的最流行的模拟器之一。

请注意,MuJoCo 是一个专有物理引擎,因此我们需要获得许可证才能使用它。此外,MuJoCo 提供 30 天的免费试用期。安装 MuJoCo 需要一系列特定的步骤。接下来,我们将看到如何安装 MuJoCo 环境。

安装 MuJoCo 环境

首先,在您的主目录中创建一个新的隐藏文件夹,名为 .mujoco。接下来,访问 MuJoCo 网站 (www.roboti.us/),并根据您的操作系统下载 MuJoCo。如 图 16.6 所示,MuJoCo 支持 Windows、Linux 和 macOS:

图 16.6:不同的 MuJoCo 版本

如果您使用的是 Linux,则可以下载名为 mujoco200 linux 的压缩文件。下载压缩文件后,解压文件并将其重命名为 mujoco200。现在,将 mujoco200 文件夹复制并将该文件夹放置在主目录中的 .mujoco 文件夹内。

图 16.7 所示,现在在我们的主目录中,我们有一个 .mujoco 文件夹,且在 .mujoco 文件夹内有一个 mujoco200 文件夹:

图 16.7:安装 MuJoCo

现在,我们需要获取试用许可证。首先,访问 www.roboti.us/license.html 并注册试用许可证,如 图 16.8 所示:

图 16.8:注册试用许可证

注册时,我们还需要计算机 ID。如 图 16.8 所示,在 计算机 ID 字段的右侧,我们有不同平台的名称。现在,只需点击您的操作系统,您就会获得相应的可执行 getid 文件。例如,如果您使用的是 Linux,则会获得名为 getid_linux 的文件。

下载 getid_linux 文件后,在终端中运行以下命令:

chmod +x getid_linux 

然后,运行以下命令:

./getid_linux 

上述命令将显示您的计算机 ID。获取计算机 ID 后,填写表格并注册以获取许可。点击 提交 按钮后,您将收到 Roboti LLC Licensing 发送的电子邮件。

从电子邮件中下载名为 mjkey.txt 的文件。接下来,将 mjkey.txt 文件放入 .mujoco 文件夹中。如 图 16.9 所示,现在我们的 .mujoco 隐藏文件夹中包含 mjkey.txt 文件和一个名为 mujoco200 的文件夹:

图 16.9:安装 MuJoCo

接下来,打开终端并运行以下命令来编辑 bashrc 文件:

nano ~/.bashrc 

将以下行复制到 bashrc 文件中,并确保将用户名文本替换为您自己的用户名:

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/username/.mujoco/mujoco200/bin 

接下来,保存文件并退出 nano 编辑器。现在,在终端运行以下命令:

source ~/.bashrc 

做得好!我们快完成了。现在,克隆 MuJoCo 的 GitHub 仓库:

git clone https://github.com/openai/mujoco-py.git 

进入mujoco-py文件夹:

cd mujoco-py 

更新软件包:

sudo apt-get update 

安装依赖项:

sudo apt-get install libgl1-mesa-dev libgl1-mesa-glx libosmesa6-dev python3-pip python3-numpy python3-scipy 

最后,安装 MuJoCo:

pip3 install -r requirements.txt
sudo python3 setup.py install 

为了测试 MuJoCo 是否安装成功,让我们通过在环境中采取随机行动来运行一个 Humanoid 智能体。所以,创建一个名为mujoco_test.py的 Python 文件,内容如下:

import gym
env = gym.make('Humanoid-v2')
env.reset()
for t in range(1000):
  env.render()
  env.step(env.action_space.sample())
env.close() 

接下来,打开终端并运行 Python 文件:

python mujoco_test.py 

上述代码将渲染Humanoid环境,如图 16.10 所示

图 16.10:Humanoid 环境

现在我们已经成功安装了 MuJoCo,在下一节中,我们将开始实现 TRPO 训练我们的智能体走路。

实现 TRPO

导入必要的库:

import gym
from stable_baselines.common.policies import MlpPolicy
from stable_baselines.common.vec_env import DummyVecEnv, VecNormalize
from stable_baselines import TRPO
from stable_baselines.common.vec_env import VecVideoRecorder 

使用DummyVecEnv创建一个向量化的Humanoid环境:

env = DummyVecEnv([lambda: gym.make("Humanoid-v2")]) 

对状态(观测值)进行归一化:

env = VecNormalize(env, norm_obs=True, norm_reward=False,
                   clip_obs=10.) 

实例化智能体:

agent = TRPO(MlpPolicy, env) 

训练智能体:

agent.learn(total_timesteps=250000) 

在训练完智能体后,我们可以通过渲染环境来看我们的训练智能体是如何学会走路的:

state = env.reset()
while True:
    action, _ = agent.predict(state)
    next_state, reward, done, info = env.step(action)
    state = next_state
    env.render() 

将本节中使用的整个代码保存到一个名为trpo.py的 Python 文件中,然后打开终端并运行该文件:

python trpo.py 

我们可以在图 16.11中看到我们的训练智能体是如何学会走路的:

图 16.11:使用 TRPO 训练智能体走路

始终使用终端运行使用 MuJoCo 环境的程序。

就这样。在下一节中,我们将学习如何将我们训练过的智能体的动作录制成视频。

录制视频

在上一节中,我们使用 TRPO 训练了我们的智能体学会走路。我们也可以录制训练好的智能体的视频吗?可以!通过 Stable Baselines,我们可以轻松地使用VecVideoRecorder模块录制智能体的视频。

请注意,要录制视频,我们需要在机器上安装ffmpeg包。如果没有安装,可以使用以下命令集进行安装:

sudo add-apt-repository ppa:mc3man/trusty-media
sudo apt-get update
sudo apt-get dist-upgrade
sudo apt-get install ffmpeg 

现在,让我们导入VecVideoRecorder模块:

from stable_baselines.common.vec_env import VecVideoRecorder 

定义一个名为record_video的函数来录制视频:

def record_video(env_name, agent, video_length=500, prefix='', video_folder='videos/'): 

创建环境:

 env = DummyVecEnv([lambda: gym.make(env_name)]) 

实例化视频录制器:

 env = VecVideoRecorder(env, video_folder=video_folder,
        record_video_trigger=lambda step: step == 0, video_length=video_length, name_prefix=prefix) 

在环境中选择动作时,我们的训练智能体会将时间步数设置为视频长度:

 state = env.reset()
    for t in range(video_length):
        action, _ = agent.predict(state)
        next_state, reward, done, info = env.step(action)
        state = next_state
    env.close() 

就这样!现在,让我们调用我们的record_video函数。请注意,我们传递了环境名称、我们训练的智能体、视频时长和视频文件的名称:

record_video('Humanoid-v2', agent, video_length=500, prefix='Humanoid_walk_TRPO') 

现在,我们将在videos文件夹中生成一个名为Humanoid_walk_TRPO-step-0-to-step-500.mp4的新文件:

图 16.12:录制的视频

通过这种方式,我们可以录制训练智能体的动作。在下一节中,我们将学习如何使用 Stable Baselines 实现 PPO。

使用 PPO 训练猎豹机器人跑步

在本节中,我们将学习如何使用近端策略优化PPO)训练 2D 猎豹机器人跑步。首先,导入必要的库:

import gym
from stable_baselines.common.policies import MlpPolicy
from stable_baselines.common.vec_env import DummyVecEnv, VecNormalize
from stable_baselines import PPO2 

使用DummyVecEnv创建一个向量化环境:

env = DummyVecEnv([lambda: gym.make("HalfCheetah-v2")]) 

对状态进行归一化:

env = VecNormalize(env,norm_obs=True) 

实例化代理:

agent = PPO2(MlpPolicy, env) 

训练代理:

agent.learn(total_timesteps=250000) 

训练完成后,我们可以通过渲染环境看到我们训练的猎豹机器人是如何学会奔跑的:

state = env.reset()
while True:
    action, _ = agent.predict(state)
    next_state, reward, done, info = env.step(action)
    state = next_state
    env.render() 

将本节中使用的整个代码保存在名为ppo.py的 Python 文件中,然后打开终端并运行该文件:

python ppo.py 

我们可以看到我们训练的猎豹机器人是如何学会奔跑的,如图 16.13所示:

图 16.13:2D 猎豹机器人学习奔跑

制作一个训练代理的 GIF

在上一节中,我们学习了如何使用 PPO 训练猎豹机器人奔跑。我们能否也创建一个训练代理的 GIF 文件?当然可以!让我们看看如何做到这一点。

首先,导入必要的库:

import imageio
import numpy as np 

初始化用于存储图像的列表:

images = [] 

通过重置环境来初始化状态,其中agent是我们在前一节中训练的代理:

state = agent.env.reset() 

渲染环境并获取图像:

img = agent.env.render(mode='rgb_array') 

对环境中的每一步,保存图像:

for i in range(500):
    images.append(img)
    action, _ = agent.predict(state)
    next_state, reward, done ,info = agent.env.step(action)
    state = next_state
    img = agent.env.render(mode='rgb_array') 

按如下方式创建 GIF 文件:

imageio.mimsave('HalfCheetah.gif', [np.array(img) for i, img in enumerate(images) if i%2 == 0], fps=29) 

现在,我们将获得一个名为HalfCheetah.gif的新文件,如图 16.14所示:

图 16.14:训练代理的 GIF

这样,我们就可以获得训练代理的 GIF。在下一节中,我们将学习如何使用 Stable Baselines 实现 GAIL。

实现 GAIL

在本节中,我们将探讨如何使用 Stable Baselines 实现生成对抗模仿学习GAIL)。在第十五章模仿学习与逆向强化学习中,我们了解到,使用生成器以一种方式生成状态-动作对,使得判别器无法区分该状态-动作对是使用专家策略还是代理策略生成的。我们训练生成器使用 TRPO 生成类似专家策略的策略,而判别器是一个分类器,使用 Adam 进行优化。

要实现 GAIL,我们需要专家轨迹,这样我们的生成器才能学习模仿专家轨迹。那么,如何获得专家轨迹呢?首先,我们使用 TD3 算法生成专家轨迹,然后创建专家数据集。接着,使用这个专家数据集,我们训练 GAIL 代理。注意,除了使用 TD3,我们还可以使用任何其他算法生成专家轨迹。

首先,让我们导入必要的库:

import gym
from stable_baselines import GAIL, TD3
from stable_baselines.gail import ExpertDataset, generate_expert_traj 

实例化 TD3 代理:

agent = TD3('MlpPolicy', 'MountainCarContinuous-v0', verbose=1) 

生成专家轨迹:

generate_expert_traj(agent, 'expert_traj', n_timesteps=100, n_episodes=20) 

使用专家轨迹创建专家数据集:

dataset = ExpertDataset(expert_path='expert_traj.npz', traj_limitation=10, verbose=1) 

使用专家数据集(专家轨迹)实例化 GAIL 代理:

agent = GAIL('MlpPolicy', 'MountainCarContinuous-v0', dataset, verbose=1) 

训练 GAIL 代理:

agent.learn(total_timesteps=25000) 

训练完成后,我们还可以渲染环境,看看我们训练的代理在环境中的表现。就这样,使用 Stable Baselines 实现 GAIL 就是这么简单。

总结

本章开始时,我们理解了 Stable Baselines 是什么以及如何安装它。然后,我们学习了如何使用 DQN 在 Stable Baselines 中创建我们的第一个智能体。我们还学习了如何保存和加载智能体。接下来,我们学习了如何通过向量化创建多个独立的环境。我们还学习了两种类型的向量化环境,分别是 SubprocVecEnv 和 DummyVecEnv。

我们了解到,在 SubprocVecEnv 中,我们将每个环境运行在不同的进程中,而在 DummyVecEnv 中,我们将每个环境运行在同一个进程中。

接下来,我们学习了如何使用 Stable Baselines 实现 DQN 及其变体来玩 Atari 游戏。之后,我们学习了如何实现 A2C,以及如何创建自定义策略网络。接着,我们学习了如何实现 DDPG,并且如何在 TensorBoard 中查看计算图。

更进一步,我们学习了如何设置 MuJoCo 环境,以及如何使用 TRPO 训练智能体行走。我们还学习了如何录制一个训练好的智能体的视频。接下来,我们学习了如何实现 PPO,并且如何制作训练好的智能体的 GIF 动图。在本章的最后,我们学习了如何使用 Stable Baselines 实现生成对抗模仿学习。

问题

让我们来检验一下我们对 Stable Baselines 的理解。试着回答以下问题:

  1. 什么是 Stable Baselines?

  2. 如何存储和加载一个训练好的智能体?

  3. 什么是向量化环境?

  4. SubprocVecEnv 和 DummyVecEnv 环境有什么区别?

  5. 如何在 TensorBoard 中可视化计算图?

  6. 如何录制一个训练好的智能体的视频?

进一步阅读

要了解更多信息,请查看以下资源:

第十七章:强化学习前沿

恭喜!你已经来到了最后一章。我们走了很长一段路。我们从强化学习的基础开始,逐渐学习了最先进的深度强化学习算法。在本章中,我们将讨论一些激动人心且前景广阔的强化学习研究趋势。我们将从学习元学习是什么以及它与其他学习范式的区别开始。然后,我们将学习一个最常用的元学习算法——模型无关元学习MAML)。

我们将详细了解 MAML,然后看看如何将其应用于强化学习环境。接着,我们将学习层次化强化学习,并深入研究一种流行的层次化强化学习算法——MAXQ 值函数分解。

在本章结束时,我们将介绍一个有趣的算法——想象增强型智能体I2As),它同时利用基于模型和无模型学习。

本章将学习以下内容:

  • 元强化学习

  • 模型无关元学习

  • 层次化强化学习

  • MAXQ 值函数分解

  • 想象增强型智能体

让我们从理解元强化学习开始本章内容。

元强化学习

为了理解元强化学习是如何工作的,首先让我们了解元学习。

元学习是人工智能领域最有前景和最热门的研究方向之一。人们相信它是实现人工通用智能AGI)的垫脚石。什么是元学习?我们为什么需要元学习?为了回答这些问题,让我们回顾一下深度学习是如何工作的。

我们知道,在深度学习中,我们训练一个深度神经网络来执行某个任务。但深度神经网络的问题在于,我们需要一个大量的训练数据集来训练我们的网络,因为当数据点较少时,它无法学习。

假设我们训练了一个深度学习模型来执行任务A。现在假设有一个新的任务B,它与任务A密切相关。尽管任务B与任务A密切相关,我们不能直接用为任务A训练的模型来执行任务B。我们需要从头开始为任务B训练一个新模型。因此,对于每个任务,我们都需要从头开始训练一个新模型,尽管它们可能是相关的。但这真的算是人工智能吗?其实不算。那么,我们人类是如何学习的呢?我们将学习概括到多个概念中,从中学习。然而,当前的学习算法只能掌握一个任务。所以,这就是元学习发挥作用的地方。

元学习生成了一个多功能的 AI 模型,它可以学习执行各种任务,而无需从头开始训练。我们在各种相关任务上训练我们的元学习模型,并且使用很少的数据点,因此对于一个新的相关任务,它可以利用在之前任务中获得的学习成果。许多研究人员和科学家认为,元学习可以让我们更接近实现通用人工智能(AGI)。学习如何学习是元学习的核心焦点。我们将在下一部分通过了解一种流行的元学习算法——MAML,来深入理解元学习到底是如何工作的。

模型无关元学习

模型无关元学习MAML)是最受欢迎的元学习算法之一,它在元学习研究中取得了重大突破。MAML 的基本思想是找到更好的初始模型参数,这样通过一个好的初始参数,模型可以在新的任务上通过较少的梯度步骤快速学习。

那么,这意味着什么呢?假设我们正在执行一个使用神经网络的分类任务。我们如何训练网络?我们首先初始化随机权重,并通过最小化损失来训练网络。我们如何最小化损失?我们通过梯度下降最小化损失。好吧,如何使用梯度下降来最小化损失呢?我们使用梯度下降来找到能够给我们最小损失的最优权重。我们进行多次梯度步骤,以找到最优权重,从而达到收敛。

在 MAML 中,我们通过从相似任务的分布中学习来找到这些最优权重。因此,对于一个新任务,我们不必从随机初始化的权重开始;相反,我们可以从最优权重开始,这样只需较少的梯度步骤就能达到收敛,并且不需要更多的数据点来进行训练。

让我们用简单的术语来理解 MAML 是如何工作的。假设我们有三个相关的任务:T[1]、T[2] 和 T[3]。

首先,我们随机初始化我们的模型参数(权重),。我们在任务 T[1] 上训练我们的网络。然后,我们通过梯度下降来最小化损失 L。我们通过找到最优参数来最小化损失。令 为任务 T[1] 的最优参数。类似地,对于任务 T[2] 和 T[3],我们将从随机初始化的模型参数 开始,并通过梯度下降找到最优参数来最小化损失。令 分别为任务 T[2] 和 T[3] 的最优参数。

如下图所示,我们从随机初始化的参数 开始每个任务,并通过找到任务 T[1]、T[2] 和 T[3] 的最优参数 来最小化损失:

图 17.1: 被初始化在一个随机位置

然而,不同于在随机位置初始化 ,即使用随机值,如果我们在一个所有任务共同的位置初始化 ,那么我们就不需要进行太多的梯度步骤,训练所需的时间会更少。MAML 就是为了实现这一目标。MAML 尝试找到这个最优参数 ,它对多个相关任务都适用,这样我们就能以更少的数据点更快速地训练新任务,而不需要进行过多的梯度步骤。

图 17.2 所示,我们将 调整到一个所有不同最优 值共同的位置:

图 17.2: 被初始化在最优位置

因此,对于一个新的相关任务,假设是 T[4],我们不必从一个随机初始化的参数开始,。相反,我们可以从最优的 值(已偏移的 )开始,这样就能减少梯度下降的步数,加快收敛。

因此,在 MAML 中,我们试图找到这个对相关任务通用的最优 值,帮助我们通过更少的数据点进行学习,并减少训练时间。MAML 是与模型无关的,这意味着我们可以将 MAML 应用到任何可以通过梯度下降训练的模型上。那么,MAML 究竟是如何工作的呢?我们如何将模型参数调整到最优位置?现在我们对 MAML 有了基本的了解,接下来我们将解决这些问题。

理解 MAML

假设我们有一个由 参数化的模型 f,即 ,并且我们有一个任务分布 p(T)。首先,我们用一些随机值初始化我们的参数 。接下来,我们从任务分布中抽取一批任务 T[i]―即 T[i]* ~ p(T)。假设我们抽取了五个任务:T[1]、T[2]、T[3]、T[4]、T*[5]。

对于每个任务 T[i],我们抽取 k 个数据点并训练由 参数化的模型 f,即 。我们通过计算损失 来训练模型,并使用梯度下降最小化损失,找到最优参数 。使用梯度下降的参数更新规则如下:

在上述方程中,以下内容适用:

  • 是任务 T[i] 的最优参数

  • 是初始参数

  • 是学习率

  • 是任务 T[i] 的损失梯度,模型参数化为

所以,在使用梯度下降进行前述参数更新后,我们将获得所有五个任务的最优参数。也就是说,对于任务 T[1]、T[2]、T[3]、T[4]、T[5],我们将分别得到最优参数

现在,在下一次迭代之前,我们进行一次元更新或元优化。也就是说,在前一步中,我们通过在每个任务 T[i] 上训练,找到了最优参数 。现在我们拿到一组新的任务,对于这些新任务 T[i],我们不必从随机位置 开始;相反,我们可以从最优位置 开始训练模型。

也就是说,对于每个新任务 T[i],我们并不是使用随机初始化的参数 ,而是使用最优参数 。这意味着我们训练的是由 参数化的模型 f,也就是 ,而不是使用 。然后,我们计算损失 ,计算梯度,并更新参数 。这使得我们随机初始化的参数 移动到一个最优位置,避免了需要进行多次梯度更新。这一步称为元更新、元优化或元训练。可以表示为:

在方程 (2) 中,以下公式适用:

  • 是初始参数

  • 是学习率

  • 是每个新任务 T[i] 的损失梯度,模型的参数化为

如果仔细观察我们之前的元更新方程 (2),我们可以看到,我们通过仅仅对每个新任务 T[i] 的梯度取平均,来更新我们的模型参数 ,而这个模型 f 是由 参数化的。

图 17.3 有助于我们更好地理解 MAML 算法。正如我们所观察到的,MAML 算法有两个循环——内部循环,我们在这个循环中使用初始参数 ,也就是 ,通过在每个任务 T[i] 上训练来找出最优参数 外部循环,我们在这个循环中使用在前一步获得的最优参数 ,也就是 ,并在新的一组任务上训练模型,计算损失,计算损失的梯度,并更新随机初始化的模型参数

图 17.3:MAML 算法

请注意,在更新模型参数 时,我们不应该使用与找到最优参数时相同的一组任务 ,这些任务是在外循环中使用的。

简而言之,在 MAML 中,我们采样一批任务,对于批次中的每个任务 T[i],我们使用梯度下降最小化损失并得到最优参数 。然后,我们通过为每个新任务 T[i] 计算梯度,并以 为参数化模型,来更新我们随机初始化的模型参数

仍然不清楚 MAML 到底是如何工作的?不用担心!接下来,我们将更详细地查看步骤,理解 MAML 在监督学习中的运作方式。

监督学习中的 MAML

正如我们所学,MAML 是与模型无关的,因此我们可以将 MAML 应用于任何可以通过梯度下降训练的模型。在这一节中,让我们学习如何在监督学习环境中应用 MAML 算法。在继续之前,我们先定义我们的损失函数。

如果我们进行回归任务,那么可以使用均方误差作为我们的损失函数:

如果我们进行分类任务,那么可以使用交叉熵损失作为我们的损失函数:

现在让我们一步步地看看 MAML 是如何在监督学习中使用的。

假设我们有一个由参数 参数化的模型 f,并且我们有一个任务分布 p(T)。首先,我们随机初始化模型参数

接下来,我们从任务分布中采样一批任务 T[i],即 T[i] ~ p(T)。假设我们已经采样了三个任务,那么我们将得到 T[1]、T[2]、T[3]。

内循环: 对于每个任务 T[i],我们采样 k 个数据点,并准备训练和测试数据集:

等等!训练数据集和测试数据集是什么?我们在内循环中使用训练数据集来寻找最优参数 ,而在外循环中使用测试集来寻找最优参数 。测试数据集并不意味着我们在检查模型的性能,它基本上在外循环中作为训练集使用。我们也可以将测试集称为元训练集。

现在,我们在训练数据集 上训练模型 ,计算损失,使用梯度下降最小化损失,并得到最优参数 ,其值为

也就是说,对于每个任务 T[i],我们采样 k 个数据点,并准备 。接下来,我们在训练数据集 上最小化损失并得到最优参数 。由于我们采样了三个任务,所以我们将得到三个最优参数,

外部循环:现在,我们对测试集(元训练集)进行元优化;也就是说,我们尝试最小化测试集上的损失。在这里,我们使用之前步骤中计算出的最优参数对我们的模型f进行参数化。所以,我们计算模型的损失和损失的梯度,并使用我们的测试数据集(元训练数据集)更新我们随机初始化的参数,公式如下:

我们重复前面的步骤进行多次迭代,以找到最优参数。为了更清楚地理解 MAML 如何在监督学习中工作,接下来我们将深入研究该算法。

算法——MAML 在监督学习中的应用

MAML 在监督学习中的算法如下所示:

  1. 假设我们有一个由参数 参数化的模型f,并且我们有一个关于任务的分布p(T)。首先,我们随机初始化模型参数

  2. 从任务分布中采样一批任务T[i],即T[i] ~ p(T)

  3. 对于每个任务T[i]:

    1. 采样k个数据点,并准备我们的训练和测试数据集:

    2. 在训练数据集上训练模型,并计算损失。

    3. 使用梯度下降最小化损失,并获得最优参数,其公式为

  4. 现在,最小化测试集上的损失。使用之前步骤中计算出的最优参数对模型f进行参数化,计算损失。计算损失的梯度,并使用我们的测试(元训练)数据集更新我们随机初始化的参数,公式如下:

  5. 重复执行步骤 2步骤 4,进行多次迭代。

下图展示了 MAML 算法在监督学习中的工作原理:

图 17.4:MAML 概览

现在我们已经了解了如何在监督学习中使用 MAML,在接下来的部分中,我们将探讨如何在强化学习中使用 MAML。

MAML 在强化学习中的应用

现在,让我们了解如何在强化学习环境中应用 MAML 算法。我们知道强化学习的目标是找到最优策略,也就是能够提供最大回报的策略。我们已经了解了几种用于寻找最优策略的强化学习算法,并且也了解了几种深度强化学习算法,在这些算法中,我们使用了由参数化的神经网络。

我们可以将 MAML 应用于任何可以通过梯度下降进行训练的算法。例如,假设我们使用策略梯度方法。在策略梯度方法中,我们使用由参数化的神经网络来找到最优策略,并使用梯度下降来训练我们的网络。因此,我们可以将 MAML 算法应用于策略梯度方法。

让我们一步一步理解 MAML 如何在强化学习中工作。

假设我们有一个由参数参数化的模型(策略网络)f。该模型(策略网络)f试图通过学习最优参数来寻找最优策略。假设我们有一个任务分布p(T)。首先,我们随机初始化模型参数

接下来,我们从任务分布中抽样一批任务T[i],即T[i] ~ p(T)。假设我们抽样了三个任务,那么我们有T[1],T[2],T[3]。

内部循环:对于每个任务T[i],我们准备我们的训练数据集。好吧,我们如何在强化学习设置中创建训练数据集呢?

我们有一个模型(策略网络)。因此,我们使用我们的模型生成k条轨迹。我们知道这些轨迹由一系列状态-动作对组成。所以,我们有:

现在,我们计算损失并通过梯度下降最小化它,得到最优参数,即

也就是说,对于每个任务T[i],我们抽取k条轨迹并准备训练数据集。接下来,我们最小化训练数据集上的损失并得到最优参数。由于我们抽样了三个任务,因此我们将得到三个最优参数,

我们还需要测试数据集,它将在外部循环中使用。我们如何准备我们的测试数据集?现在,我们使用由最优参数参数化的模型f;也就是说,我们使用并生成k条轨迹。所以,我们有:

记住,是由创建的,测试(元训练)数据集是由创建的。

外部循环:现在,我们在测试(元训练)数据集上执行元优化;也就是说,我们尝试最小化测试数据集中的损失。在这里,我们通过在前一步计算的最优参数参数化我们的模型f。因此,我们计算模型的损失及其梯度,并使用测试(元训练)数据集来更新我们随机初始化的参数,公式如下:

我们重复前面的步骤若干次,以找到最优参数。为了清楚理解 MAML 在强化学习中的工作原理,接下来我们将研究算法的细节。

算法——强化学习中的 MAML

强化学习中的 MAML 算法如下所示:

  1. 假设我们有一个由参数参数化的模型f,并且我们有一个任务分布p(T)。首先,我们随机初始化模型参数

  2. 从任务分布中采样一批任务T[i],即T[i] *~ p(T)。

  3. 对于每个任务 T[i]:

    1. 使用采样k轨迹并准备训练数据集:

    2. 在训练数据集上训练模型,并计算损失。

    3. 使用梯度下降法最小化损失,并得到最优参数,即

    4. 使用采样k轨迹并准备测试数据集:

  4. 现在,我们在测试数据集上最小化损失。用之前步骤中计算出的最优参数对模型f进行参数化,并计算损失。计算损失的梯度,并使用我们的测试(元训练)数据集更新我们随机初始化的参数,计算公式为:

  5. 重复步骤 2步骤 4若干次。

就这样!元学习是一个不断发展的研究领域。现在我们对元学习有了一个基本的了解,你可以进一步探索元学习,并了解元学习在强化学习中的应用。在下一部分,我们将学习层次化强化学习。

层次化强化学习

强化学习的问题在于,当状态空间和动作空间的数量较大时,它无法很好地扩展,最终会导致所谓的“维度灾难”问题。层次化强化学习HRL)被提出用来解决维度灾难问题,在该方法中,我们将大型问题分解为层次结构中的小子问题。假设我们的智能体的目标是从学校回家。现在,我们的目标被分解为一组子目标,比如走出学校大门、叫出租车等等。

在 HRL 中使用了不同的方法,例如状态空间分解、状态抽象和时间抽象。在状态空间分解中,我们将状态空间分解为不同的子空间,并尝试在较小的子空间中解决问题。分解状态空间还可以加快探索速度,因为代理不需要探索整个状态空间。在状态抽象中,代理忽略当前状态空间中与完成当前子任务无关的变量。在时间抽象中,动作序列和动作集合被分组,将单步动作划分为多个步骤。

我们现在将探讨 HRL 中最常用的算法之一,叫做 MAXQ 值函数分解。

MAXQ 值函数分解

MAXQ 值函数分解是 HRL 中最常用的算法之一。在本节中,让我们基本了解 MAXQ 值函数分解是如何工作的。我们通过一个例子来理解 MAXQ 值函数分解的工作原理。我们以图 17.5所示的出租车环境为例:

图 17.5:出租车环境

假设我们的代理正在驾驶一辆出租车。如图 17.5所示,黄色的矩形代表由我们代理驾驶的出租车。字母(RGYB)表示不同的位置。因此,我们总共有四个位置,代理需要在一个位置接乘客并在另一个位置将其放下。代理成功放下乘客后将获得+20 分,每个时间步骤消耗 1 分。如果代理进行非法接送,将失去-10 分。

因此,我们的代理的目标是在短时间内将乘客接送到正确的位置,并且不添加非法乘客。

现在,我们将把代理的目标分解为以下四个子任务:

  • Navigate:在 Navigate 子任务中,我们的代理的目标是将出租车从当前位置开到一个目标位置。Navigate(t)子任务将使用四个原始动作:northsoutheastwest

  • Get:在 Get 子任务中,我们的代理的目标是将出租车从当前位置开到乘客所在的位置并接载乘客。

  • Put:在 Put 子任务中,我们的代理的目标是将出租车从当前位置开到乘客的目的地并将乘客放下。

  • Root:Root 是整个任务。

我们可以通过一个有向无环图来表示所有这些子任务,这个图被称为任务图,如图 17.6所示:

图 17.6:任务图

从前面的图中我们可以观察到,所有的子任务都是按层次排列的。每个节点代表一个子任务或原始动作,每条边连接的方式使得一个子任务可以调用它的子子任务。如所示,Navigate(t)子任务有四个原始动作:EastWestNorthSouthGet子任务有一个Pickup原始动作和一个Navigate(t)子任务。类似地,Put子任务有一个Putdown(放下)原始动作和一个Navigate(t)子任务。

在 MAXQ 值函数分解中,我们将值函数分解为每个子任务的一组值函数。为了高效设计和调试 MAXQ 分解,我们可以像图 17.7所示那样重新设计我们的任务图:

图 17.7:任务图

图 17.7中我们可以观察到,我们重新设计的图包含两种特殊类型的节点:最大节点和 Q 节点。最大节点定义了任务分解中的子任务,而 Q 节点定义了每个子任务可用的行动。

因此,在这一节中,我们对 MaxQ 值函数分解有了基本的了解。在下一节中,我们将学习 I2A。

想象增强代理

你是国际象棋的爱好者吗?如果我让你下棋,你会怎么下?在移动棋盘上的任何棋子之前,你可能会想象移动某个棋子的后果,并移动你认为能够帮助你赢得游戏的棋子。因此,基本上,在采取任何行动之前,我们会想象后果,如果后果是有利的,我们就会执行这个行动,否则我们会避免执行该行动。

类似地,想象增强代理I2As)通过想象得到了增强。在环境中采取任何行动之前,代理会想象采取该行动的后果,如果他们认为该行动会带来好的奖励,他们就会执行这个行动。I2A 利用了基于模型和无模型学习的优势。图 17.8展示了 I2A 的架构:

图 17.8:I2A 架构

图 17.8中我们可以观察到,I2A 架构既有基于模型的路径,也有无模型的路径。因此,代理采取的行动是基于模型路径和无模型路径的结果。在基于模型的路径中,我们有回滚编码器。

这些回滚编码器是代理执行想象任务的地方,让我们仔细看看回滚编码器。图 17.9展示了一个单一的回滚编码器:

图 17.9:单一的想象回滚

图 17.9中我们可以观察到,回滚编码器有两层:想象未来层和编码器层。想象未来层是进行想象的地方。想象未来层由想象核心组成。

当我们将状态s[t]输入到想象核心时,我们得到下一个状态 和奖励 ,然后当我们将这个下一个状态 输入到下一个想象核心时,我们得到下一个状态 和奖励 。如果我们重复这些操作 n 步,我们得到一个展开(rollout),它基本上是一个状态和奖励的对,然后我们使用编码器,如长短时记忆网络LSTM),来编码这个展开。结果,我们得到展开编码。这些展开编码实际上是描述未来想象路径的嵌入。我们将为不同的未来想象路径拥有多个展开编码器,并且我们使用一个聚合器来聚合这个展开编码器。

好的,但在想象核心中,想象究竟是如何发生的?想象核心中到底包含了什么?图 17.10 显示了一个单一的想象核心:

图 17.10:想象核心

正如我们从图 17.10中观察到的,想象核心由策略网络和环境模型组成。环境模型从代理迄今为止执行的所有动作中学习。它接收关于状态的信息 ,结合经验想象所有可能的未来,并选择能够获得高奖励的动作

图 17.11 显示了 I2A 的完整架构,所有组件都已展开:

图 17.11:完整的 I2A 架构

你玩过推箱子游戏吗?推箱子是一个经典的益智游戏,玩家需要将箱子推到目标位置。游戏规则非常简单:箱子只能被推,不能被拉。如果我们把箱子推错了方向,那么谜题就变得无法解决。

图 17.12:推箱子环境

I2A 架构在这些环境中提供了良好的结果,这些环境中代理必须在采取行动之前进行提前规划。论文的作者在推箱子游戏(Sokoban)上测试了 I2A 的性能,并取得了出色的结果。

深度强化学习领域正在发生各种令人兴奋的研究进展。现在你已经读完了这本书,你可以开始探索这些进展并尝试各种项目。学习并巩固!

总结

我们从理解元学习(meta learning)开始这一章。我们了解到,通过元学习,我们可以在各种相关任务上用少量数据点训练模型,这样对于一个新的相关任务,我们的模型就可以利用从之前任务中获得的学习成果。

接下来,我们学习了一种流行的元学习算法——MAML。在 MAML 中,我们从一批任务中采样,对于批次中的每个任务 T[i],我们通过梯度下降最小化损失并获得最优参数 。然后,通过计算每个新任务 T[i] 的梯度并用模型参数化!,我们更新随机初始化的模型参数!

接下来,我们学习了 HRL,在 HRL 中我们将大问题分解为层次化的小子问题。我们还研究了 HRL 中使用的不同方法,如状态空间分解、状态抽象和时间抽象。接下来,我们概述了 MAXQ 价值函数分解,我们将价值函数分解为每个子任务的价值函数集。

在本章结束时,我们学习了带有想象增强的 I2A。在采取任何行动之前,代理会想象采取该行动的后果,如果他们认为该行动会带来好的奖励,他们就会执行该行动。

深度强化学习每天都在发展,取得了有趣的进展。现在你已经了解了各种最先进的深度强化学习算法,可以开始构建有趣的项目,并为深度强化学习研究做出贡献。

问题

让我们测试一下你在本章中获得的知识;尝试回答以下问题:

  1. 为什么我们需要元学习?

  2. 什么是 MAML?

  3. 什么是元目标?

  4. 什么是元训练集?

  5. 定义 HRL。

  6. I2A 是如何工作的?

深入阅读

更多信息,我们可以参考以下论文:

附录 1 – 强化学习算法

让我们看看本书中介绍的所有强化学习算法。

强化学习算法

一般强化学习算法的步骤如下:

  1. 首先,代理通过执行一个动作与环境进行互动。

  2. 代理执行一个动作,并从一个状态转移到另一个状态。

  3. 然后代理将根据它执行的动作获得奖励。

  4. 根据奖励,代理会理解该动作是好还是坏。

  5. 如果动作是好的,即代理收到了正向奖励,那么代理会偏向于执行该动作,否则代理会尝试执行其他可能导致正向奖励的动作。所以,强化学习本质上是一个试错学习过程。

值迭代

值迭代的算法如下:

  1. 通过对 Q 函数取最大值来计算最优值函数,即!

  2. 从计算出的最优值函数中提取最优策略

策略迭代

策略迭代的算法如下:

  1. 初始化一个随机策略

  2. 使用给定策略计算值函数

  3. 使用从 第 2 步 获得的值函数提取新策略

  4. 如果提取的策略与 第 2 步 中使用的策略相同,则停止,否则将提取的新的策略发送到 第 2 步,并重复 第 2 步第 4 步

首次访问 MC 预测

首次访问 MC 预测的算法如下:

  1. 让 total_return(s) 为一个状态在多个回合中的回报总和,N(s) 为计数器,即状态在多个回合中被访问的次数。将 total_return(s) 和 N(s) 初始化为零,适用于所有状态。策略 作为输入给出。

  2. 对于 M 次迭代:

    1. 使用策略 生成一个回合

    2. 将回合中获得的所有奖励存储在一个名为 rewards 的列表中

    3. 对于回合中的每个步骤 t

    如果状态 s[t] 在回合中首次出现:

    1. 计算状态 s[t] 的回报 R(s[t]) = sum(rewards[t:])

    2. 更新状态 s[t] 的回报总和为 total_return(s[t]) = total_return(s[t]) + R(s[t])

    3. 更新计数器为 N(s[t]) = N(s[t]) + 1

  3. 通过取平均值来计算一个状态的值,即:

每次访问 MC 预测

每次访问 MC 预测的算法如下:

  1. 让 total_return(s) 为一个状态在多个回合中的回报总和,N(s) 为计数器,即状态在多个回合中被访问的次数。将 total_return(s) 和 N(s) 初始化为零,适用于所有状态。策略 作为输入给出。

  2. 对于 M 次迭代:

    1. 使用策略 生成一个回合

    2. 将回合中获得的所有奖励存储在名为 rewards 的列表中

    3. 对于每一步 t 在该回合中:

      1. 计算状态 s[t] 的回报为 R(s[t]) = sum(rewards[t:])

      2. 更新状态 s[t] 的总回报为 total_return(s[t]) = total_return(s[t]) + R(s[t])

      3. 更新计数器为 N(s[t]) = N(s[t]) + 1

  3. 通过简单地取平均值来计算状态的值,即:

MC 预测 – Q 函数

MC 预测 Q 函数的算法如下:

  1. 令 total_return(s, a) 为状态-动作对在多个回合中的回报总和,N(s, a) 为状态-动作对在多个回合中被访问的次数。初始化所有状态-动作对的 total_return(s, a) 和 N(s, a) 为零。给定策略 作为输入。

  2. 对于 M 次迭代:

    1. 使用策略 生成一个回合

    2. 将回合中获得的所有奖励存储在名为 rewards 的列表中

    3. 对于每一步 t 在该回合中:

      1. 计算状态-动作对的回报,R(s[t], a[t]) = sum(rewards[t:])

      2. 更新状态-动作对的总回报,total_return(s[t], a[t]) = total_return(s[t], a[t]) + R(s[t], a[t])

      3. 更新计数器为 N(s[t], a[t]) = N(s[t], a[t]) + 1

  3. 通过简单地取平均值来计算 Q 函数(Q 值),即:

MC 控制方法

MC 控制方法的算法如下:

  1. 令 total_return(s, a) 为状态-动作对在多个回合中的回报总和,N(s, a) 为状态-动作对在多个回合中被访问的次数。初始化所有状态-动作对的 total_return(s, a) 和 N(s, a) 为零,并初始化一个随机策略

  2. 对于 M 次迭代:

    1. 使用策略 生成一个回合

    2. 将回合中获得的所有奖励存储在名为 rewards 的列表中

    3. 对于每一步 t 在该回合中:

      如果 (s[t], a[t]) 是该回合中第一次出现:

      1. 计算状态-动作对的回报,R(s[t], a[t]) = sum(rewards[t:])

      2. 更新状态-动作对的总回报为 total_return(s[t], a[t]) = total_return(s[t], a[t]) + R(s[t], a[t])

      3. 更新计数器为 N(s[t], a[t]) = N(s[t], a[t]) + 1

      4. 通过简单地取平均值来计算 Q 值,即

    4. 使用 Q 函数从 计算新的更新策略:

在政策 MC 控制 – 探索开始

基于探索开始方法的在政策 MC 控制算法如下:

  1. 设 total_return(s, a) 为多次剧集中状态-动作对的回报总和,N(s, a) 为多次剧集中访问状态-动作对的次数。将所有状态-动作对的 total_return(s, a) 和 N(s, a) 初始化为零,并初始化随机策略

  2. 对于 M 次迭代:

    1. 随机选择初始状态 s[0] 和初始动作 a[0],使得所有状态-动作对的概率大于 0

    2. 使用策略 从选定的初始状态 s[0] 和动作 a[0] 生成一集

    3. 将剧集中获得的所有奖励存储在名为 rewards 的列表中

    4. 对于剧集中的每个步骤 t

      如果 (s[t], a[t]) 在这一集是首次出现:

      1. 计算状态-动作对的回报,R(s[t], a[t]) = sum(rewards[t:])

      2. 更新状态-动作对的总回报为 total_return(s[t], a[t]) = total_return(s[t], a[t]) + R(s[t], a[t])

      3. 更新计数器为 N(s[t], a[t]) = N(s[t], a[t]) + 1

      4. 通过简单地取平均值来计算 Q 值,即

      5. 使用 Q 函数从 计算更新后的策略:

在线蒙特卡洛控制 - Epsilon-Greedy

使用 epsilon-greedy 策略的在线蒙特卡洛控制算法如下:

  1. 设 total_return(s, a) 为多次剧集中状态-动作对的回报总和,N(s, a) 为多次剧集中访问状态-动作对的次数。将所有状态-动作对的 total_return(s, a) 和 N(s, a) 初始化为零,并初始化随机策略

  2. 对于 M 次迭代:

    1. 使用策略 生成一集

    2. 将剧集中获得的所有奖励存储在名为 rewards 的列表中

    3. 对于剧集中的每个步骤 t

      如果 (s[t], a[t]) 在这一集是首次出现:

      1. 计算状态-动作对的回报,R(s[t], a[t]) = sum(rewards[t:])。

      2. 更新状态-动作对的总回报为 total_return(s[t], a[t]) = total_return(s[t], a[t]) + R(s[t], a[t])。

      3. 更新计数器为 N(s[t], a[t]) = N(s[t], a[t]) + 1。

      4. 通过简单地取平均值来计算 Q 值,即

    4. 使用 Q 函数计算更新后的策略 。设 。策略 以概率 选择最佳动作 ,以概率 选择随机动作。

离策略蒙特卡洛控制

离策略蒙特卡洛控制方法的算法如下:

  1. 初始化 Q 函数 Q(s, a),赋值为随机值,设置行为策略 b 为 epsilon-greedy,设置目标策略 为贪婪策略,并初始化累积权重为 C(s, a) = 0

  2. 对于 M 次迭代:

    1. 使用行为策略 b 生成一个序列

    2. 将回报 R 初始化为 0,权重 W 初始化为 1

    3. 对于每个步骤 tt = T – 1, T – 2, . . . , 0:

      1. 计算回报为 R = R + r[t+1]

      2. 更新累积权重为 C(s[t], a[t]) = C(s[t], a[t]) +W

      3. 更新 Q 值为

      4. 计算目标策略

      5. 如果 则跳出循环

      6. 更新权重为

  3. 返回目标策略

TD 预测

TD 预测方法的算法如下:

  1. 使用随机值初始化值函数 V(s)。给定一个策略

  2. 对于每一集:

    1. 初始化状态 s

    2. 对于每个步骤:

      1. 根据给定策略 在状态 s 中执行动作 a,获得奖励 r,并移动到下一个状态

      2. 更新状态的值为

      3. 更新 (这一步表示我们将下一个状态 更改为当前状态 s

      4. 如果 s 不是终止状态,重复执行 步骤 14

基于策略的 TD 控制 – SARSA

基于策略的 TD 控制 – SARSA 的算法如下:

  1. 初始化 Q 函数 Q(s, a),赋值为随机值

  2. 对于每一集:

    1. 初始化状态 s

    2. Q(s, a) 中提取一个策略,并选择一个动作 a 在状态 s 中执行

    3. 对于每个步骤:

      1. 执行动作 a,移动到新状态 ,并观察奖励 r

      2. 在状态 下,使用 epsilon-greedy 策略选择动作

      3. 更新 Q 值为

      4. 更新 (更新下一个状态 -动作 对为当前状态 s-动作 a 对)

      5. 如果 s 不是终止状态,重复执行 步骤 15

离策略的 TD 控制 – Q 学习

离策略的 TD 控制 – Q 学习的算法如下:

  1. 初始化 Q 函数 Q(s, a),赋值为随机值

  2. 对于每一集:

    1. 初始化状态 s

    2. 对于每个步骤:

      1. Q(s, a) 中提取一个策略,并选择一个动作 a 在状态 s 中执行

      2. 执行动作 a,移动到新状态 ,并观察奖励 r

      3. 更新 Q 值为

      4. 更新 (更新下一个状态 为当前状态 s

      5. 如果 s 不是终止状态,重复执行 步骤 15

深度 Q 学习

深度 Q 学习算法如下所示:

  1. 使用随机值初始化主网络参数

  2. 通过复制主网络参数 初始化目标网络参数

  3. 初始化重放缓冲区

  4. 对于 N 次训练周期,执行 步骤 5

  5. 对于每个步骤,即对于 t = 0, . . ., T – 1:

    1. 观察状态 s 并使用 epsilon-贪婪策略选择动作,即以 epsilon 的概率选择随机动作 a,以 1-epsilon 的概率选择动作

    2. 执行动作并转移到下一个状态 ,并获得奖励 r

    3. 将转移信息存储在重放缓冲区

    4. 从重放缓冲区 随机抽取一个 K 转移的小批量

    5. 计算目标值,即

    6. 计算损失,

    7. 计算损失的梯度,并使用梯度下降法更新主网络参数

    8. 冻结目标网络参数 若干时间步,然后通过复制主网络参数 更新它

双重 DQN

双重 DQN 算法如下所示:

  1. 使用随机值初始化主网络参数

  2. 通过复制主网络参数 初始化目标网络参数

  3. 初始化重放缓冲区

  4. 对于 N 次训练周期,重复 步骤 5

  5. 对于每个步骤,即对于 t = 0, . . ., T – 1:

    1. 观察状态 s 并使用 epsilon-贪婪策略选择动作,即以 epsilon 的概率选择随机动作 a,以 1-epsilon 的概率选择动作

    2. 执行动作并转移到下一个状态 ,并获得奖励 r

    3. 将转移信息存储在重放缓冲区

    4. 从重放缓冲区 随机抽取一个 K 转移的小批量

    5. 计算目标值,即

    6. 计算损失,

    7. 计算损失的梯度,并使用梯度下降法更新主网络参数

    8. 冻结目标网络参数 若干时间步,然后通过复制主网络参数 更新它

REINFORCE 策略梯度

REINFORCE 策略梯度算法如下所示:

  1. 使用随机值初始化网络参数

  2. 根据策略 生成一些 N 条轨迹

  3. 计算轨迹的回报

  4. 计算梯度

  5. 更新网络参数,

  6. 重复 步骤 2步骤 5 多次迭代

带奖励到达的策略梯度

带奖励到达的策略梯度算法如下:

  1. 使用随机值初始化网络参数

  2. 根据策略 生成一些 N 条轨迹

  3. 计算回报(奖励到达) R[t]

  4. 计算梯度

  5. 更新网络参数:

  6. 重复 步骤 2步骤 5 多次迭代

REINFORCE 带基线

REINFORCE 带基线的算法如下:

  1. 初始化策略网络参数 和价值网络参数

  2. 根据策略 生成一些 N 条轨迹

  3. 计算回报(奖励到达) R[t]

  4. 计算策略梯度,

  5. 使用梯度上升更新策略网络参数

  6. 计算价值网络的均方误差,

  7. 使用梯度下降更新价值网络参数

  8. 重复 步骤 2步骤 7 多次迭代

优势演员评论员

优势演员评论员方法的算法如下:

  1. 初始化演员网络参数 和评论员网络参数

  2. 对于 N 条剧集,重复 步骤 3

  3. 对于每个时间步,即 t = 0, . . ., T – 1:

    1. 使用策略 选择一个动作

    2. 在状态 s[t] 中采取动作 a[t],观察奖励 r 并转到下一个状态

    3. 计算策略梯度:

    4. 使用梯度上升更新演员网络参数

    5. 计算评论员网络的损失:

    6. 计算梯度 并使用梯度下降更新评论员网络参数

异步优势演员评论员

优势演员评论员A3C)的步骤如下:

  1. 工作代理与其自己的环境副本交互

  2. 每个工作代理遵循不同的策略并收集经验

  3. 接下来,工作代理计算演员和评论员网络的损失

  4. 在计算损失后,他们计算损失的梯度,并异步地将这些梯度发送给全局代理

  5. 全局代理使用从工作代理接收到的梯度来更新其参数

  6. 现在,全局代理更新的参数将定期发送到工作代理

深度确定性策略梯度

深度确定性策略梯度 (DDPG) 算法如下所示:

  1. 初始化主评论员网络参数 和主演员网络参数

  2. 通过仅复制主评论员网络参数 来初始化目标评论员网络参数

  3. 通过仅复制主演员网络参数 来初始化目标演员网络参数

  4. 初始化重放缓冲区

  5. 对于 N 次实验,重复 第 6 步第 7 步

  6. 初始化 Ornstein-Uhlenbeck 随机过程 以进行动作空间的探索

  7. 对于每个步骤,即对于 t = 0, . . ., T – 1:

    1. 根据策略 和探索噪声选择动作 a,即

    2. 执行选定的动作 a,移动到下一个状态 并获得奖励 r,并将此转换信息存储在重放缓冲区

    3. 从重放缓冲区 随机抽取一个 K 的过渡小批量

    4. 计算评论员的目标值,即

    5. 计算评论员网络的损失

    6. 计算损失的梯度 并使用梯度下降更新评论员网络参数,

    7. 计算演员网络的梯度 并使用梯度上升更新演员网络参数,

    8. 更新目标评论员和目标演员网络参数,

双延迟 DDPG

双延迟 DDPG (TD3) 算法如下所示:

  1. 初始化两个主评论员网络参数,,以及主演员网络参数

  2. 通过分别复制主评论员网络参数 来初始化两个目标评论员网络参数

  3. 通过复制主演员网络参数 来初始化目标演员网络参数

  4. 初始化重放缓冲区

  5. 对于 N 次实验,重复 第 6 步

  6. 对于每个步骤,即对于 t = 0, . . ., T – 1:

    1. 根据策略 和探索噪声 选择动作 a,即 ,其中,

    2. 执行选择的动作 a,移动到下一个状态 ,获得奖励 r,并将转换信息存储在重放缓冲区

    3. 从重放缓冲区 随机抽取 K 个转换的小批量

    4. 选择动作 来计算目标值 ,其中

    5. 计算评论网络的目标值,即

    6. 计算评论网络的损失

    7. 计算损失的梯度 ,并使用梯度下降法最小化损失

    8. 如果 t mod d =0,则:

      1. 计算目标函数的梯度 ,并使用梯度上升法更新演员网络参数

      2. 更新目标评论网络参数和目标演员网络参数,分别为

Soft Actor-Critic

Soft Actor-Critic (SAC) 算法如下:

  1. 初始化主价值网络参数 、Q 网络参数 ,以及演员网络参数

  2. 通过复制主价值网络参数 来初始化目标价值网络

  3. 初始化重放缓冲区

  4. 对于 N 个回合,重复 步骤 5

  5. 对于每个回合中的步骤,即对于 t = 0, . . ., T – 1

    1. 基于策略 选择动作 a,即

    2. 执行选择的动作 a,移动到下一个状态 ,获得奖励 r,并将转换信息存储在重放缓冲区

    3. 从重放缓冲区随机抽取 K 个转换的小批量

    4. 计算目标状态值

    5. 计算价值网络的损失 ,并使用梯度下降法更新参数

    6. 计算目标 Q 值

    7. 计算 Q 网络的损失 ,并使用梯度下降法更新参数

    8. 计算演员目标函数的梯度 ,并使用梯度上升法更新参数

    9. 更新目标值网络参数

信任域策略优化

信任域策略优化 (TRPO) 算法如下:

  1. 初始化策略网络参数 和价值网络参数

  2. 生成 N 条轨迹 ,遵循策略

  3. 计算回报(奖励累积)R[t]

  4. 计算优势值 A[t]

  5. 计算策略梯度

  6. 使用共轭梯度法计算

  7. 使用更新规则 更新策略网络参数

  8. 计算值网络的均方误差,

  9. 使用梯度下降更新值网络参数

  10. 重复 步骤 2步骤 9 进行多次迭代

PPO-Clipped

PPO-Clip 方法的算法如下所示:

  1. 初始化策略网络参数 和值网络参数

  2. 收集一些 N 条轨迹 ,跟随策略

  3. 计算回报(奖励累积)R[t]

  4. 计算目标函数的梯度

  5. 使用梯度上升更新策略网络参数

  6. 计算值网络的均方误差,

  7. 计算值网络的梯度

  8. 使用梯度下降更新值网络参数

  9. 重复 步骤 2步骤 8 进行多次迭代

PPO-Penalty

PPO-惩罚方法的算法如下所示:

  1. 初始化策略网络参数 和值网络参数 ,并初始化惩罚系数 和目标 KL 散度

  2. 对于迭代次数

    1. 收集一些 N 条轨迹,跟随策略

    2. 计算回报(奖励累积)R[t]

    3. 计算

    4. 计算目标函数的梯度

    5. 使用梯度上升更新策略网络参数

    6. 如果 d 大于或等于 ,则设置 ;如果 d 小于或等于 ,则设置,

    7. 计算值网络的均方误差:

    8. 计算值网络的梯度

    9. 使用梯度下降更新值网络参数

类别 DQN

类别 DQN 的算法如下所示:

  1. 用随机值初始化主网络参数

  2. 通过复制主网络参数 来初始化目标网络参数

  3. 初始化重放缓冲区 ,原子数,以及

  4. 对于 N 次实验,执行 步骤 5

  5. 对于每一步骤,即对于 t = 0, . . ., T – 1:

    1. 将状态 s 和支持值输入主分类 DQN,参数化为 ,并获得每个支持值的概率值。然后计算 Q 值为

    2. 计算 Q 值后,使用 epsilon-greedy 策略选择一个动作,即以概率 epsilon 选择一个随机动作 a,并以概率 1-epsilon 选择一个动作,如

    3. 执行选定的动作并移动到下一个状态 ,并获得奖励 r

    4. 将过渡信息存储在重放缓冲区 中。

    5. 从重放缓冲区 随机抽取一个过渡。

    6. 将下一个状态 和支持值输入目标分类 DQN,参数化为 ,并获得每个支持的概率值。然后计算值为

    7. 计算 Q 值后,我们在状态 中选择具有最大 Q 值 的最佳动作。

    8. 用零值初始化数组 m,其形状为支持值的数量。

    9. 对于 j 在支持值的范围内:

      1. 计算目标支持值:

      2. 计算 b 的值:

      3. 计算下界和上界:

      4. 在下界上分布概率:

      5. 在上界上分布概率:

    10. 计算交叉熵损失

    11. 使用梯度下降最小化损失并更新主网络的参数

    12. 冻结目标网络参数 若干时间步,然后通过复制主网络参数 来更新它

分布式分布式 DDPG

分布式分布式深度确定性策略梯度 (D4PG) 算法如下所示:

  1. 初始化评论家网络参数 和演员网络参数

  2. 初始化目标评论家网络参数 和目标演员网络参数 ,通过从 复制初始化。

  3. 初始化重放缓冲区

  4. 启动 L 个演员

  5. 对于 N 次实验,重复 步骤 6

  6. 对于每一步骤,即对于 t = 0, . . ., T – 1:

    1. 从重放缓冲区 随机抽取一个 K 的小批量过渡

    2. 计算评论员的目标值分布,即

    3. 计算评论员网络的损失并计算梯度,如 所示

    4. 计算梯度后,使用梯度下降更新评论员网络参数:

    5. 计算演员网络的梯度

    6. 通过梯度上升更新演员网络参数:

    7. 如果 t mod 则:

      使用软替换更新目标评论员和目标演员网络的参数,如 所示

    8. 如果 t mod 则:

      将网络权重复制到演员中

我们在演员网络中执行以下步骤:

  1. 基于策略 和探索噪声选择动作 a,即

  2. 执行选择的动作 a,移动到下一个状态 并获得奖励 r,将过渡信息存储在重放缓冲区

  3. 重复执行 步骤 12,直到学习者完成

DAgger

DAgger 算法如下:

  1. 初始化空数据集

  2. 初始化一个策略

  3. 对于迭代 i = 1 到 N

    1. 创建一个策略

    2. 使用策略 生成一个轨迹。

    3. 通过收集策略 访问的状态和专家提供的这些状态的动作来创建数据集 。因此,

    4. 将数据集汇总为

    5. 在更新后的数据集 上训练分类器,并提取新策略

深度 Q 学习来自演示

来自演示的深度 Q 学习DQfD)的算法如下:

  1. 初始化主网络参数

  2. 通过复制主网络参数 初始化目标网络参数

  3. 使用专家演示初始化重放缓冲区

  4. 设置 d,即我们希望延迟更新目标网络参数的时间步数

  5. 预训练阶段:对于步骤 t = 1, 2, . . ., T

    1. 从重放缓冲区 中抽取一个小批量经验

    2. 计算损失 J(Q)

    3. 使用梯度下降更新网络参数

    4. 如果 t mod d = 0:

      通过复制主网络参数 更新目标网络参数

  6. 训练阶段:对于步骤 t = 1, 2, . . ., T

    1. 选择一个动作

    2. 执行选择的动作并进入下一个状态,观察奖励,并将此过渡信息存储在重放缓冲区中

    3. 从回放缓冲区 采样一个优先级的经验小批量

    4. 计算损失 J(Q)

    5. 使用梯度下降更新网络的参数

    6. 如果 t mod d = 0:

      通过复制主网络参数 来更新目标网络参数

最大熵逆强化学习

最大熵逆强化学习的算法如下所示:

  1. 初始化参数 并收集专家示范

  2. 对于 N 次迭代:

    1. 计算奖励函数

    2. 使用前一步骤获得的奖励函数通过值迭代计算策略

    3. 使用前一步骤获得的策略计算状态访问频率

    4. 计算相对于 的梯度,即

    5. 的值更新为

强化学习中的 MAML

强化学习中 MAML 的算法如下所示:

  1. 假设我们有一个由参数 参数化的模型 f,并且我们有一个关于任务的分布 p(T)。首先,我们随机初始化模型参数

  2. 从任务分布中采样一批任务 T[i],即 T[i] *~ p(T)。

  3. 对于每个任务 T[i]:

    1. 使用 采样 k 条轨迹,并准备训练数据集:

    2. 在训练数据集 上训练模型 ,并计算损失

    3. 使用梯度下降最小化损失并得到最优参数 ,其值为

    4. 使用 采样 k 条轨迹,并准备测试数据集:

  4. 现在,我们在测试数据集 上最小化损失。使用在前一步骤计算得到的最优参数 对模型 f 进行参数化,并计算损失 。计算损失的梯度,并使用我们的测试(元训练)数据集 更新我们随机初始化的参数

  5. 重复 步骤 24 进行若干次迭代。

附录 2 – 评估

以下是每章结尾提到的问题的答案。

第一章 – 强化学习基础

  1. 在监督学习和无监督学习中,模型(智能体)基于给定的训练数据集进行学习,而在强化学习RL)中,智能体通过直接与环境互动进行学习。因此,RL 本质上是智能体与环境之间的互动。

  2. 环境是智能体的世界。智能体存在于环境中。例如,在国际象棋游戏中,棋盘就是环境,因为棋手(智能体)在棋盘(环境)中学习如何下棋。同样,在超级马里奥兄弟游戏中,马里奥的世界就是环境。

  3. 确定性策略将状态映射到一个特定的动作,而随机策略则将状态映射到动作空间上的概率分布。

  4. 智能体通过执行动作与环境进行互动,从初始状态开始,直到到达最终状态。这个从初始状态到最终状态的智能体-环境交互过程称为一个回合。

  5. 折扣因子帮助我们通过决定给未来奖励和即时奖励的权重,防止回报达到无穷大。

  6. 价值函数(一个状态的价值)是从该状态开始的轨迹的期望回报,而 Q 函数(状态-动作对的 Q 值)是从该状态和动作开始的轨迹的期望回报。

  7. 在确定性环境中,我们可以确定当智能体在状态s下执行动作a时,它总是到达状态 。在随机环境中,我们无法确定通过在状态s下执行某个动作a,智能体总是到达状态 ,因为随机环境中会有一些随机性。

第二章 – 健身工具包指南

  1. Gym 工具包提供了各种环境,用于训练 RL 智能体,从经典控制任务到 Atari 游戏环境。我们可以使用各种 RL 算法训练我们的 RL 智能体,让它在这些模拟环境中学习。

  2. 我们可以使用make函数创建一个 Gym 环境。make函数需要环境 ID 作为参数。

  3. 我们知道,动作空间包含环境中所有可能的动作。我们可以通过使用env.action_space来获取动作空间。

  4. 我们可以使用render()函数来可视化 Gym 环境。

  5. Gym 提供的一些经典控制环境包括平衡杆环境、摆钟环境和山地车环境。

  6. 我们可以通过在每个状态中选择一个动作来生成一个回合,使用step()函数。

  7. Atari 环境的状态空间将是游戏屏幕的像素值或 Atari 机器的 RAM。

  8. 我们可以使用 Monitor 包装器记录智能体的游戏过程。它需要三个参数——环境、我们希望保存记录的目录以及强制选项。

第三章 – 贝尔曼方程与动态规划。

  1. 贝尔曼方程表示,一个状态的值可以通过当前奖励和下一个状态的折现值之和来获得。与值函数的贝尔曼方程类似,Q 函数的贝尔曼方程表示,状态-动作对的 Q 值可以通过当前奖励和下一个状态-动作对的折现 Q 值之和来获得。

  2. 贝尔曼期望方程给出了贝尔曼值函数和 Q 函数,而贝尔曼最优性方程给出了最优的贝尔曼值函数和 Q 函数。

  3. 值函数可以从 Q 函数中推导出来,如下图所示:

  4. Q 函数可以从值函数中推导出来,如下图所示:

  5. 在值迭代方法中,我们执行以下步骤:

    1. 通过对 Q 函数进行最大化计算最优值函数,即:

    2. 从计算出的最优值函数中提取最优策略。

  6. 在策略迭代方法中,我们执行以下步骤:

    1. 初始化随机策略。

    2. 使用给定策略计算值函数。

    3. 使用步骤 2中获得的值函数提取新策略。

    4. 如果提取的策略与步骤 2中使用的策略相同,则停止,否则将提取的新策略发送到步骤 2并重复步骤 2步骤 4

  7. 在值迭代方法中,首先,我们通过对 Q 函数进行迭代最大化来计算最优值函数。一旦找到最优值函数,我们将使用它来提取最优策略。在策略迭代方法中,我们将尝试通过策略迭代的方式计算最优值函数。一旦我们找到了最优值函数,创建该最优值函数的策略将被提取为最优策略。

第四章 – 蒙特卡罗方法。

  1. 在蒙特卡罗方法中,我们通过取N次实验中某个状态的回报平均值来近似该状态的值,而不是取预期回报。

  2. 要使用动态规划方法计算值函数,我们需要了解模型动态。当我们不知道模型动态时,我们使用无模型方法。蒙特卡罗方法是一种无模型方法,意味着它不需要模型动态(转移概率)来计算值函数。

  3. 在预测任务中,我们通过预测值函数或 Q 函数来评估给定的策略,这有助于我们理解智能体在使用给定策略时所能获得的预期回报。然而,在控制任务中,我们的目标是找到最优策略,并且不会给定任何策略作为输入,因此我们从初始化一个随机策略开始,并尝试通过迭代的方式找到最优策略。

  4. 在 MC 预测方法中,状态值和状态-动作对的值可以通过分别取多个回合中的状态的平均回报和状态-动作对的平均回报来计算。

  5. 在首次访问 MC 中,我们仅计算状态首次在回合中被访问时的回报。在每次访问 MC 中,我们每次访问状态时都会计算回报。

  6. 当环境是非平稳的时,我们不需要从所有回合中取状态的回报并计算平均值。由于环境是非平稳的,我们可以忽略早期回合的回报,仅使用最新回合的回报来计算平均值。这样,我们可以使用增量平均值计算状态的值。

  7. 在基于策略的方法中,我们使用一个策略生成回合,并且迭代地改进同一个策略来找到最优策略;而在基于策略外的蒙特卡罗控制方法中,我们使用两种不同的策略生成回合(行为策略)和寻找最优策略(目标策略)。

  8. epsilon-贪心策略是指我们以概率 epsilon 选择一个随机动作(探索),并以概率 1-epsilon 选择最佳动作(利用)。

第五章 – 理解时间差分学习

  1. 与蒙特卡罗方法不同,时间差分TD)学习方法利用自举技术,这样我们就不需要等到回合结束才能计算状态的值。

  2. TD 学习算法结合了动态规划和蒙特卡罗方法的优点。也就是说,像动态规划方法一样,我们执行自举操作,这样我们就不需要等到回合结束才能计算状态值或 Q 值;同时,像蒙特卡罗方法一样,它是一种无模型的方法,因此它不需要环境的模型动态来计算状态值或 Q 值。

  3. TD 误差可以定义为目标值与预测值之间的差异。

  4. TD 学习更新规则如图所示

  5. 在 TD 预测任务中,给定一个策略,我们使用该策略估计值函数。因此,我们可以说,代理在每个状态下如果按照给定策略行动,期望获得的回报是多少。

  6. SARSA是一种基于策略的 TD 控制算法,代表状态-动作-奖励-状态-动作。使用 SARSA 计算 Q 函数的更新规则如图所示

  7. SARSA 是基于策略的算法,这意味着我们使用单一的 epsilon-贪心策略来选择环境中的动作,并计算下一个状态-动作对的 Q 值,而 Q 学习是基于策略外的算法,意味着我们使用 epsilon-贪心策略选择环境中的动作,但计算下一个状态-动作对的 Q 值时,我们使用贪心策略。

第六章 – 案例研究 – 多臂强盗问题

  1. 多臂强盗MAB)问题是强化学习中的经典问题之一。多臂强盗就像一个老丨虎丨机,我们拉动臂(杠杆),并根据某种概率分布获得奖励(回报)。单臂老丨虎丨机称为单臂强盗,当有多个老丨虎丨机时,称为多臂强盗或k臂强盗,其中k表示老丨虎丨机的数量。

  2. 在 epsilon-贪心策略中,我们以 1-epsilon 的概率选择最佳臂,并以 epsilon 的概率选择随机臂。

  3. 在软最大探索中,臂会根据概率进行选择。然而,在最初的回合中,我们不知道每个臂的正确平均奖励,因此基于平均奖励的概率来选择臂在初期会不准确。为了避免这种情况,我们引入了一个新的参数叫做TT称为温度参数。

  4. 上置信区间计算为

  5. 的值大于时,我们将获得一个接近 1 的高概率,而不是接近 0 的低概率。

  6. Thomson 采样方法的步骤如下:

    1. 初始化贝塔分布时,将所有的k臂的 alpha 和 beta 设置为相等的值。

    2. 从所有k臂的贝塔分布中采样一个值

    3. 拉动采样值较高的臂

    4. 如果我们赢得了游戏,则将分布的 alpha 值更新为

    5. 如果我们输了游戏,则将分布的 beta 值更新为

    6. 重复步骤 25若干回合

  7. 使用上下文强盗时,我们会根据环境的状态采取行动,状态持有上下文。上下文强盗广泛应用于根据用户行为个性化内容推荐。它们也用于解决推荐系统中面临的冷启动问题。

第七章 – 深度学习基础

  1. 激活函数用于引入神经网络的非线性。

  2. Softmax 函数基本上是 sigmoid 函数的推广。它通常应用于网络的最后一层,并在进行多类分类任务时使用。它给出了每个类别的输出概率,因此,softmax 值的总和将始终等于 1。

  3. Epoch 指定神经网络看到整个训练数据的次数。因此,我们可以说一个 epoch 等于所有训练样本的一个前向传播和一个反向传播。

  4. RNN(循环神经网络)广泛应用于涉及序列数据的场景,如时间序列、文本、音频、语音、视频、天气等。它们已广泛用于各种自然语言处理NLP)任务,如语言翻译、情感分析、文本生成等。

  5. 在反向传播 RNN 时,我们在每个时间步骤上乘以权重和 tanh 函数的导数。当我们在每一步向后传播时,如果每次乘上较小的数字,我们的梯度会变得极小,导致计算机无法处理的数值;这就是所谓的消失梯度问题。

  6. 池化层通过仅保留重要特征来减少空间维度。不同类型的池化操作包括最大池化、平均池化和求和池化。

  7. 假设我们希望 GAN 生成手写数字。首先,我们将获取一个包含手写数字集合的数据集,比如 MNIST 数据集。生成器学习数据集中图像的分布。它学习训练集中手写数字的分布。我们将随机噪声输入生成器,它会将随机噪声转化为与训练集中的手写数字相似的新数字。判别器的目标是执行分类任务。给定一张图像,判别器将其分类为真实或伪造;即判断该图像是来自训练集还是由生成器生成的。

第八章 – TensorFlow 入门

  1. TensorFlow 会话用于执行计算图,其中包含节点上的操作和连接到边缘的张量。

  2. 变量是用来存储值的容器。变量将作为输入,传递给计算图中的其他操作。我们可以将占位符看作是变量,其中我们只定义类型和维度,但不会赋予值。占位符的值将在运行时提供。我们使用占位符将数据输入到计算图中。占位符在定义时没有值。

  3. TensorBoard 是 TensorFlow 的可视化工具,可以用来可视化计算图。它还可以用于绘制各种定量指标以及一些中间计算的结果。当我们训练一个非常深的神经网络时,调试模型时可能会感到困惑。由于我们可以在 TensorBoard 中可视化计算图,因此我们可以轻松理解、调试和优化这些复杂的模型。它还支持共享功能。

  4. TensorFlow 中的急切执行方式更符合 Python 编程风格,并且支持快速原型开发。与图模式不同,图模式下我们每次进行操作时都需要构建一个计算图,而急切执行遵循命令式编程范式,任何操作都可以立即执行,而无需创建图,就像我们在 Python 中一样。

  5. 在 Keras 中构建模型包含四个重要步骤:

    1. 定义模型

    2. 编译模型

    3. 拟合模型

    4. 评估模型

  6. 功能模型提供的灵活性比顺序模型更大。例如,在功能模型中,我们可以轻松地将任何一层连接到另一层,而在顺序模型中,每一层都堆叠在另一个之上。

第九章 – 深度 Q 网络及其变种

  1. 当环境由大量状态和动作组成时,采用穷举的方式计算所有可能的状态-动作对的 Q 值会非常昂贵。因此,我们使用深度 Q 网络来近似 Q 函数。

  2. 我们使用一个叫做回放缓冲区(replay buffer)的缓存来收集智能体的经验,并基于这些经验来训练我们的网络。回放缓冲区通常实现为队列结构(先进先出),而不是列表。因此,如果缓冲区已满并且有新的经验进入,我们会删除旧的经验,并将新的经验添加到缓冲区中。

  3. 当目标值和预测值依赖于相同的参数!时,会导致均方误差的不稳定,网络将学习得很差。它还会在训练过程中导致很多发散问题。因此,我们使用目标网络。

  4. 与 DQN 不同,在双 DQN 中,我们使用两个 Q 函数来计算目标值。一个 Q 函数由主网络的参数!进行参数化,选择具有最大 Q 值的动作,另一个 Q 函数由目标网络的参数!进行参数化,计算由主网络选择的动作的 Q 值。

  5. 一个具有较高 TD 误差的过渡意味着该过渡是不正确的,因此我们需要更多地学习这个过渡以减少误差。一个具有较低 TD 误差的过渡意味着该过渡已经很好。我们总是能从错误中学到更多,而不仅仅是关注我们已经做得好的部分,对吧?同样,我们可以从具有较高 TD 误差的过渡中学到更多,而不是从那些具有较低 TD 误差的过渡中学到的东西。因此,我们可以对具有较高 TD 误差的过渡赋予更高的优先级,而对那些误差较低的过渡赋予较低的优先级。

  6. 优势函数可以定义为 Q 函数和价值函数之间的差异。

  7. LSTM 层在 DQN 中用于保持对过去状态的信息,直到不再需要。保持过去状态的信息对我们解决部分可观察马尔可夫决策过程POMDPs)中的问题非常有帮助。

第十章 – 策略梯度方法

  1. 在基于价值的方法中,我们从最优 Q 函数(Q 值)中提取最优策略。

  2. 当我们的动作空间是连续时,使用基于价值的方法计算最优策略是困难的。因此,我们使用基于策略的方法。在基于策略的方法中,我们无需 Q 函数即可计算最优策略。

  3. 在策略梯度方法中,我们根据网络给出的动作概率分布来选择动作。如果我们赢得了这一轮,也就是说,如果我们获得了较高的回报,那么我们会给该轮的所有动作分配较高的概率,否则我们会给该轮的所有动作分配较低的概率。

  4. 策略梯度的计算公式是!

  5. 回报到达(Reward-to-go)基本上是从状态 s[t] 开始的轨迹的回报。它的计算公式为

  6. 带基线函数的策略梯度方法是一种使用基线函数来减少回报方差的策略梯度方法。

  7. 基线函数 b 给出的是智能体所在状态的期望回报,然后在每一步减去 b 可以减少回报的方差。

第十一章 – 演员-评论家方法 – A2C 和 A3C

  1. 演员-评论家方法是深度强化学习中最流行的算法之一。许多现代深度强化学习算法是基于演员-评论家方法设计的。演员-评论家方法位于基于价值的方法与基于策略的方法的交汇处。也就是说,它同时利用了基于价值和基于策略的方法。

  2. 在演员-评论家方法中,演员计算最优策略,评论家通过估计价值函数来评估演员网络计算的策略。

  3. 在带基线的策略梯度方法中,我们首先生成完整的回合(轨迹),然后更新网络的参数;而在演员-评论家方法中,我们在每一步的回合中更新网络参数。

  4. 在演员网络中,我们计算梯度为 .

  5. 优势演员-评论家A2C)方法中,我们使用优势函数来计算策略梯度,优势函数是 Q 函数与价值函数之间的差异,也就是 Q(s, a) – V(s)。

  6. “异步”一词暗示了 A3C 的工作方式。也就是说,A3C 不是通过单个智能体来学习最优策略,而是有多个智能体与环境互动。由于多个智能体同时与环境互动,我们为每个智能体提供环境的副本,使得每个智能体都能与自己专属的环境副本互动。因此,这些多个智能体被称为工作智能体,而我们还有一个单独的智能体叫做全局智能体。所有工作智能体都异步地向全局智能体报告,全局智能体则聚合学习结果。

  7. 在 A2C 中,我们可以有多个工作智能体,每个智能体与自己的环境副本进行互动,所有工作智能体都进行同步更新,不像 A3C 那样工作智能体进行异步更新。

第十二章 – 学习 DDPG、TD3 和 SAC

  1. DDPG 由演员和评论家组成。演员是一个策略网络,使用策略梯度方法来学习最优策略。评论家是一个 DQN,它评估演员产生的动作。

  2. 评论家基本上是一个 DQN。评论家的目标是评估演员网络产生的动作。评论家使用 DQN 计算的 Q 值来评估演员产生的动作。

  3. TD3 的关键特性包括裁剪双 Q 学习、延迟的策略更新和目标策略平滑。

  4. 我们不使用单一的评论者网络,而是使用两个主要评论者网络来计算 Q 值,同时使用两个目标评论者网络来计算目标值。我们使用两个目标评论者网络计算两个目标 Q 值,并在计算损失时取这两个值中的最小值。这有助于防止目标 Q 值的过高估计。

  5. DDPG 方法即使在相同的动作下也会产生不同的目标值,因此即使对于相同的动作,目标值的方差也会很高,因此我们通过向目标动作中加入一些噪声来减少这种方差。

  6. 在 SAC 方法中,我们使用带有熵项的目标函数的略微修改版本,表示为:,它通常被称为最大熵 RL熵正则化 RL。添加熵项也常常被称为熵奖励。

  7. 评论者网络的作用是评估由行为者生成的策略。在 SAC 中,评论者不仅使用 Q 函数来评估行为者的策略,还使用价值函数。

第十三章 – TRPO、PPO 和 ACKTR 方法

  1. 信任区域指的是实际函数f(x)和近似函数之间接近的区域。因此,我们可以说,如果我们的近似函数位于信任区域内,那么我们的近似将是准确的。

  2. TRPO 是一种策略梯度算法,是对带基准的策略梯度的改进。TRPO 试图进行一次大规模的策略更新,同时施加 KL 约束,确保旧策略和新策略之间的差异不会过大。TRPO 保证了单调的策略改进,确保每次迭代都会有策略的改进。

  3. 就像梯度下降一样,共轭梯度下降也试图找到函数的最小值;然而,共轭梯度下降的搜索方向与梯度下降不同,并且共轭梯度下降在N次迭代内达到收敛。

  4. TRPO 的更新规则如图所示:

  5. PPO 是对 TRPO 算法的改进,并且实现简单。与 TRPO 类似,PPO 确保策略更新位于信任区域内。但与 TRPO 不同,PPO 在目标函数中不使用任何约束。

  6. 在 PPO 裁剪方法中,为了确保策略更新位于信任区域,即新策略不远离旧策略,PPO 添加了一个新的函数,称为裁剪函数,确保新旧策略不会相差太远。

  7. K-FAC 将费舍尔信息矩阵近似为一个块对角矩阵,其中每个块包含导数。然后,每个块被近似为两个矩阵的克罗内克积,这被称为克罗内克因式分解。

第十四章 – 分布式强化学习

  1. 在分布式强化学习中,我们不是根据期望回报选择动作,而是根据回报的分布来选择动作,这通常被称为价值分布或回报分布。

  2. 在分类 DQN 中,我们将状态和分布的支持作为输入,网络返回价值分布的概率。

  3. 分类 DQN 的作者建议,选择支持的数量 N 为 51 会更高效,因此分类 DQN 也被称为 C51 算法。

  4. 逆累积分布函数(Inverse CDF)也被称为分位数函数。顾名思义,逆累积分布函数是累积分布函数的逆函数。也就是说,在 CDF 中,给定支持 x,我们得到累积概率 ,而在逆 CDF 中,给定累积概率 ,我们得到支持 x

  5. 在分类 DQN 中,我们将固定支持(在均匀间隔下)与状态一起输入到网络中,网络返回非均匀的概率。然而,在 QR-DQN 中,我们将固定的均匀概率与状态一起输入到网络中,网络返回在变量位置的支持(不均匀间隔的支持)。

  6. D4PG 类似于 DDPG,区别如下:

    1. 我们在评论网络中使用分布式 DQN,而不是使用常规的 DQN 来估计 Q 值。

    2. 我们计算 N 步回报作为目标,而不是计算一步回报。

    3. 我们使用优先经验回放,并在评论网络中对梯度更新赋予重要性。

    4. 我们不使用一个演员,而是使用 L 个独立的演员,每个演员并行行动,收集经验并将经验存储在回放缓冲区中。

第十五章 – 模仿学习与逆强化学习

  1. 执行模仿学习最简单和最直观的方法之一是将模仿学习任务视为监督学习任务。首先,我们收集一组专家示范,然后我们训练一个分类器,执行专家在特定状态下执行的相同动作。我们可以将其视为一个大的多类分类问题,并训练我们的智能体在相应状态下执行专家所执行的动作。

  2. 在 DAgger 中,我们通过一系列迭代来聚合数据集,并在聚合后的数据集上训练分类器。

  3. 在 DQfD 中,我们用专家示范填充回放缓冲区,并对智能体进行预训练。请注意,这些专家示范仅用于智能体的预训练。一旦智能体完成预训练,它将与环境交互,收集更多经验并利用这些经验进行学习。因此,DQfD 包括两个阶段:预训练和训练。

  4. IRL 用于当设计奖励函数很困难时。在 RL 中,我们尝试在给定奖励函数的情况下找到最优策略,但在 IRL 中,我们尝试根据专家演示学习奖励函数。一旦我们通过 IRL 从专家演示中推导出奖励函数,就可以使用这个奖励函数来训练我们的代理,使用任何强化学习算法来学习最优策略。

  5. 我们可以用特征向量f表示状态。假设我们有一个状态s,那么它的特征向量可以定义为f[s]。

  6. 在 GAIL 中,生成器的角色是通过学习专家策略的占用度量来生成一个策略,而判别器的角色是分类生成的策略是来自专家策略还是来自代理策略。因此,我们使用 TRPO 训练生成器。判别器基本上是一个神经网络,用于判断生成器生成的策略是专家策略还是代理策略。

第十六章 – 使用 Stable Baselines 进行深度强化学习

  1. Stable Baselines 是 OpenAI Baselines 的改进版。Stable Baselines 是一个更易于使用的高级库,提供了最新的深度强化学习算法,并且还包含了几个有用的功能。

  2. 我们可以通过agent.save()保存代理,并通过agent.load()加载已训练的代理。

  3. 我们通常在每个步骤中只在一个环境中训练我们的代理,但使用 Stable Baselines,我们可以在每个步骤中在多个环境中训练代理。这有助于我们的代理更快地学习。现在,我们的状态、动作、奖励和完成标志将以向量的形式呈现,因为我们在多个环境中训练代理。因此,我们称之为向量化环境。

  4. 在 SubprocVecEnv 中,我们在不同的进程中运行每个环境,而在 DummyVecEnv 中,我们在同一个进程中运行每个环境。

  5. 使用 Stable Baselines,查看我们模型的计算图在 TensorBoard 中变得更容易。为了做到这一点,我们只需在实例化代理时传递需要存储日志文件的目录。

  6. 使用 Stable Baselines,我们可以轻松地通过VecVideoRecorder模块录制代理的视频。

第十七章 – 强化学习前沿

  1. 元学习生成了一种多功能的 AI 模型,它可以学习执行各种任务,而无需从头开始训练。我们在多个相关任务上用少量数据点训练我们的元学习模型,因此对于一个新的但相关的任务,它可以利用从前一个任务中获得的学习成果,我们不必从头开始训练它。

  2. 模型无关元学习MAML)是最常用的元学习算法之一,它在元学习研究中取得了重大突破。MAML 的基本思路是找到更好的初始模型参数,这样通过好的初始参数,模型可以在新任务上快速学习,且只需较少的梯度更新步骤。

  3. 在 MAML 的外循环中,我们更新模型参数,如 ,它被称为元目标。

  4. 元训练集基本上充当外循环中的训练集,用于更新外循环中的模型参数。

  5. 在层次化强化学习中,我们将一个大问题分解为层次结构中的小子问题。层次化强化学习中使用的不同方法包括状态空间分解、状态抽象和时间抽象。

  6. 使用增强想象力的智能体,在采取任何行动之前,智能体会想象采取该行动的后果,如果他们认为该行动会带来好的奖励,他们就会执行这个行动。

posted @ 2025-07-08 21:22  绝不原创的飞龙  阅读(2)  评论(0)    收藏  举报