R-强化学习实用指南-全-
R 强化学习实用指南(全)
原文:
annas-archive.org/md5/25a973e468806ffc3c064b967b5af8dd译者:飞龙
前言
强化学习是机器学习中一个令人兴奋的领域。它在技术中的应用广泛,涵盖了从自动驾驶汽车到游戏的方方面面。强化学习创造了能够学习并适应环境变化的算法。
本书提供了强化学习实现的动手实践方法。它将探讨有趣的实践案例,比如使用表格 Q 学习来控制机器人。此外,还将介绍相关方法论,帮助你迅速上手。
我们将介绍强化学习的基本概念。我们将涵盖智能体-环境接口、马尔可夫决策过程和策略梯度方法。接着,我们将使用 R 语言中的相关库来开发基于马尔可夫链的模型。我们将探讨多臂老丨虎丨机问题。通过应用动态规划和蒙特卡洛方法,我们将找到最优策略并进行预测。最后,我们将使用时序差分学习来解决车辆路径规划问题。随后,我们将把目前学到的概念应用到实际问题中。我们将从游戏世界开始,接着转向金融问题,如欺诈检测。从医疗领域来看,我们将展示如何利用 TD 学习来进行癌症检测。
最后,我们将探索深度强化学习。这涉及使用基于神经网络的算法来提升强化学习的潜力。我们将通过使用 Keras 模型来实现强化学习。最后,我们将探讨强化学习面临的下一个挑战。
通过阅读本书,你将能够在 R 中实现各种深度学习算法,以应对多种使用场景。
本书适合谁阅读
本书适合任何想从零开始学习强化学习的人。它将拓展你在不同领域中对机器学习的理解。书中涵盖了强化学习的重要概念及其相关问题,同时使用 R 3.x 版本进行演示。
本书内容概览
第一章,使用 R 语言概述强化学习,通过介绍强化学习并使用MDPtoolbox包来帮助你快速入门强化学习。
第二章,强化学习的基础构建块,帮助你通过 R 语言掌握强化学习的核心概念,并构建模型。读完这一章后,你将准备好开始在实际项目中应用强化学习。
第三章,马尔可夫决策过程应用实践,详细解释了马尔可夫决策过程。本章进一步说明了该过程如何在实际应用中得以实现。
第四章,多臂老丨虎丨机模型,解释了多臂老丨虎丨机模型及其各种应用场景。
第五章,用于最优策略的动态规划,涵盖了动态规划(DP)的各种特性。它解释了 DP 的自顶向下方法,以及可以应用的几个优化技术。最后,本章展示了如何构建一个 DP 应用程序。
第六章,用于预测的蒙特卡罗方法,教你如何使用蒙特卡罗方法预测股市价格。
第七章,时序差分学习,讲解如何使用时序差分(TD)学习算法解决车辆路径规划问题。
第八章,游戏应用中的强化学习,展示了如何使用强化学习算法解决博弈论中的问题。
第九章,金融工程中的 MAB,利用强化学习(RL)解决金融工程问题,学习优化和异常识别技术。
第十章,健康护理中的 TD 学习,借助强化学习解决健康护理问题。
第十一章,探索深度强化学习方法,教你人工神经网络的基础知识。在这里,你将学习如何使用 R 实现深度递归 Q 网络。
第十二章,使用 Keras 的深度 Q 学习,探讨了使用 TensorFlow 作为后端引擎的 Keras 模型,以及如何使用 Keras 设置多层感知器(MLP)模型。然后,你将学习如何使用深度强化学习来平衡一个倒立摆系统。
第十三章,接下来是什么?,探索强化学习概念的简要总结,并探索强化学习在现实生活中的主要应用。在此过程中,我们将发现强化学习的下一步以及在构建和实施机器学习模型方面的挑战。
为了最大限度地利用本书
本书要求具有基本的 R 知识。
下载示例代码文件
你可以从你的账户在 www.packt.com 下载本书的示例代码文件。如果你在其他地方购买了这本书,你可以访问 www.packtpub.com/support 并注册以直接将文件通过电子邮件发送给你。
你可以按照以下步骤下载代码文件:
-
在 www.packt.com 登录或注册。
-
选择“支持”标签。
-
点击代码下载。
-
在搜索框中输入书名并按照屏幕上的指示操作。
下载完成后,请确保使用最新版本的解压缩工具解压或提取文件夹:
-
WinRAR/7-Zip for Windows
-
Zipeg/iZip/UnRarX for Mac
-
Linux 版的 7-Zip/PeaZip
本书的代码包也托管在 GitHub 上,网址是 github.com/PacktPublishing/Hands-On-Reinforcement-Learning-with-R。如果代码有更新,现有的 GitHub 仓库将会更新。
我们还提供来自我们丰富的书籍和视频目录的其他代码包,点击github.com/PacktPublishing/查看!
下载彩色图像
我们还提供一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。你可以在这里下载:www.packtpub.com/sites/default/files/downloads/9781789616712_ColorImages.pdf。
使用的约定
本书中使用了许多文本约定。
CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号。例如:“keras 库基于用于管理输入和输出的层。”
代码块的格式如下:
Xtrain <- MnistData$train$x
Ytrain <- MnistData$train$y
Xtest <- MnistData$test$x
Ytest <- MnistData$test$y
当我们希望引起你对某一代码块特定部分的注意时,相关行或项目会以粗体显示:
> dim(Xtrain)
[1] 60000 28 28
> dim(Xtest)
[1] 10000 28 28
粗体:表示新术语、重要词汇或屏幕上出现的词汇。例如,菜单或对话框中的词汇会在文本中像这样出现。这里是一个例子:“修正线性单元(ReLU)是自 2015 年以来最广泛使用的激活函数。”
警告或重要提示会像这样出现。
小贴士和技巧会像这样显示。
动作中的代码
访问以下链接查看代码运行的视频:
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果你对本书的任何部分有疑问,请在邮件主题中提到书名,并通过 customercare@packtpub.com 向我们发送邮件。
勘误:虽然我们已经尽一切努力确保内容的准确性,但错误是难以避免的。如果你在本书中发现错误,我们非常感激你能向我们报告。请访问 www.packtpub.com/support/errata,选择你的书籍,点击勘误提交表单链接并输入详细信息。
盗版:如果你在互联网上发现我们作品的非法复制版本,无论形式如何,我们会感激你提供位置地址或网站名称。请通过 copyright@packt.com 联系我们,并附上该材料的链接。
如果你有兴趣成为作者:如果你在某个领域拥有专业知识,并且有兴趣参与编写或贡献一本书,请访问 authors.packtpub.com。
评论
请留下评论。在您阅读并使用了本书之后,为什么不在您购买本书的网站上留下评论呢?潜在读者可以看到并利用您公正的意见来做出购买决策,我们 Packt 也可以了解您对我们产品的看法,而我们的作者则能看到您对他们书籍的反馈。谢谢!
欲了解更多关于 Packt 的信息,请访问packt.com。
第一部分 - 使用 R 语言入门强化学习
本节介绍了强化学习的基本概念。此节帮助你理解强化学习的基本概念和要素。
本节包含以下章节:
-
第一章,使用 R 语言的强化学习概述
-
第二章,强化学习的构建块
第一章:使用 R 进行强化学习概述
强化学习 (RL) 是机器学习中一个非常激动人心的领域,广泛应用于从自动驾驶汽车到游戏的各种应用中。学习是一种过程,表现为由个体经验引发的持久适应性行为变化。学习的能力——即建立事件之间的因果关系、根据这些经验修改行为并记忆这些关系——是通过我们神经系统的功能组织得以实现的。动物研究表明,大脑具有一个或多个神经机制,通过这些机制,刺激和动作可以相互关联。基于这些考虑,提出了一个新的范式,它将认知学习的概念转移到机器学习中。在这个范式中,环境、智能体和奖励的概念成为寻找决策策略的关键,这些策略帮助我们做出正确的选择。
在强化学习中,算法是通过学习和适应环境变化而创建的。与外界的互动通过环境基于算法选择生成的外部反馈信号(奖励信号)来实现。正确的选择会带来奖励,而错误的选择则会导致惩罚。
在本章中,你将接触到 R 环境,并学习如何利用它通过强化学习解决问题。我们将首次了解 R 中可用的解决马尔可夫决策问题的包,并看到许多来自现实世界的应用示例。
到本章结束时,我们将了解强化学习的基本概念和该技术的不同方法。我们还将开始学习如何在 R 环境中使用可用的包来处理这项技术,并了解一些不同的实际应用。
本章将涵盖以下主题:
-
强化学习简介
-
理解强化学习算法
-
选择 R 进行强化学习
-
使用 MDPtoolbox 包
-
强化学习应用
强化学习简介
强化学习是个体在其所生活的环境中所采用的主要行为方式。通过老师给予的口头奖励,学习正确行为的孩子;通过反复练习学会投篮的篮球运动员;通过分析可能的对方反制措施来规划自己行动的战略家:这些都是适应性行为的例子。在这些情境中,一切都取决于代理所处的环境条件。强化学习是指解决上述问题的过程以及适用于最大化某个奖励函数的一组计算方法。它是研究这些问题及其可能解决方法的学科。在强化学习中,系统并不会提供问题的正确答案,而是对一个响应进行批评。
之前我们提到过,强化学习(RL)代表了一种新的机器学习范式,因此,在继续之前,区分人工智能中不同的方法是有用的。机器学习指的是能够在没有外部帮助的情况下从经验中学习,这正是我们人类在大多数情况下所做的。为什么机器不能做到这一点呢?
机器学习在解决日常生活中的多个问题中的成功,归功于其算法的质量。这些算法经过时间的不断改进和更新。可以通过根据用于学习的信号性质或系统采用的反馈类型将其分组为大的类别,从而实现多样化。
这些类别如下:
-
监督学习:该算法基于一系列示例的观察,每个数据输入都已事先标注。通过这种初步分析,它生成一个将输入值与期望输出关联的函数。它用于构建预测模型。
-
无监督学习:该算法家族试图从非标记的通用输入中提取知识。这些算法用于构建描述性模型。一个典型的应用例子是搜索引擎。
-
RL:这种算法可以根据其执行环境中发生的变化进行学习。事实上,由于每个动作都会对相关环境产生影响,算法便受到相同反馈环境的驱动。这些算法中的一些应用于语音或文本识别。
我们刚才提出的细分并不禁止在这些不同领域之间使用混合方法,事实上,这些方法常常取得良好的结果。
强化学习(RL)是指进行可以学习并适应自然变化的计算。这种编程策略依赖于根据计算决策接受外部升级的思想。正确的决策会得到奖励,而错误的决策会导致惩罚。系统的目标是实现最优结果,显然。在这一节中,我们了解了 RL 的基础。
让我们尝试更好地描述我们所介绍的不同范式,从监督算法开始。
监督学习
在监督学习中,我们尝试构建一个模型,从带标签的训练数据开始,利用该模型预测不可用的数据或未来的数据。因此,监督意味着在我们的样本集(数据集)中,期望的输出信号已经被标记为已知。在这种学习方式中,基于离散类的标签,我们将进行基于分类技术的任务。监督学习中使用的另一种技术是回归,其中输出信号是连续的数值。
在下图中,属于两个集合的样本被标记为不同的符号,从而使得容易识别分隔这两个集合的线:

监督学习算法,从足够数量的示例开始,使我们能够创建一个派生函数,该函数能够近似目标函数。如果算法返回了足够的近似度,提供输入数据给派生函数,我们应该能够得到类似于目标函数所提供的输出响应。这些算法基于这样一个概念:相似的输入对应相似的输出。
在现实世界中,这并不总是成立;然而,在某些情况下,这种近似是可以接受的。这些算法的成功在很大程度上依赖于输入数据。如果只有少量的训练输入,算法可能没有足够的经验来提供正确的输出。相反,输入值过多可能会使算法变得非常慢,因为由许多输入生成的导数函数可能非常复杂。此外,错误的数据会使整个系统不可靠,并导致智能体做出错误的决策,这告诉我们监督算法对噪声非常敏感。监督算法分为两大类:
-
分类:如果输出值是类别性的——例如,属于某个类或不属于某个类——这就是一个分类问题。
-
回归:如果输出是某个范围内的连续实数值,那么这就是一个回归问题。
在现实生活中,我们并不总是有带标签的数据可以用于学习。当这种情况发生时,我们需要通过不同的方式来解决问题,正如我们将在下一节中看到的那样。
无监督学习
与监督学习不同,无监督学习中我们有没有标签的数据或非结构化数据。通过这些技术,我们可以观察数据结构并提取有意义的信息。然而,在这些技术中,不能依赖于已知与结果相关的变量或奖励函数。在下面的图表中,我们看到了聚类的例子,这是一种探索性技术,允许我们将数据聚合到之前不知道属于的组(称为聚类)中:

这些算法的成功取决于它们能够从数据库中提取信息的重要性。原则上,这些算法基于比较数据并搜索相似性或差异。输入数据必须仅包含描述每个示例所需的功能集。如果输入数据由数值元素组成,无监督算法将实现出色的结果,但对非数值数据的精度要低得多。显然,在存在包含明确可识别的顺序或分组的数据时,它们可以正常工作。从数据中学习是监督和无监督算法基于的方法。在这两种情况下,都没有与环境的实时交互,这限制了这些技术用于解决众多问题的能力。在接下来的部分中,我们将看到如何处理这些问题。
强化学习
机器学习的第三种范式是强化学习。这种学习的目标是通过与环境的互动来提高系统(代理)的性能。为了提高系统功能,引入了强化信号,即奖励信号。这种强化不是由标签或正确值来确定,而是衡量系统所采取的行动质量的度量。因此,它不能用于监督学习。
在下面的图表中,我们可以看到代理-环境交互方案的示意图:

在现实世界中,我们并不总是有明确的指示来确定正确的输出;我们通常只有定性信息(强化信号)。现有的数据通常不提供任何有关如何更新代理行为的信息,因此没有指示如何更新权重的策略。无法定义成本函数或梯度。在这些情况下,我们可以定义一个系统,旨在创建能够从自身经验中学习的智能代理。
与环境的互动是这项技术的主要概念。让我们深入理解这一切是如何发生的。
理解强化学习算法
正如我们在前面章节中所看到的,强化学习(RL)是一种编程技术,旨在开发能够学习和适应环境变化的算法。这种编程技术基于一个假设,即代理能够从外部接收刺激并根据这些刺激改变自己的行为。因此,正确的选择会带来奖励,而错误的选择则会导致系统的惩罚。系统的目标是获得尽可能高的奖励,从而得到最佳的结果。
这一结果可以通过两种方法来实现:
-
第一种方法涉及评估算法的选择,然后根据结果对算法进行奖励或惩罚。这些技术还可以适应环境中的重大变化。一个例子是随着使用而提高性能的图像识别程序。在这种情况下,我们可以说学习是持续进行的。
-
在第二种方法中,首先进行一个阶段,在这个阶段中算法进行预先训练,当系统被认为是可靠的时,它变得固定且不可修改。这源于一个观察结果,即不断评估算法的行为可能是一个无法自动化或非常昂贵的过程。
这些仅仅是实现选择,因此可能存在某些算法包含了新分析的方法。
到目前为止,我们已经介绍了强化学习的基本概念。现在我们可以分析这些概念如何转化为算法。在本节中,我们将列出这些方法,提供一个概述,并深入探讨我们将在接下来的章节中处理的实际案例。
动态规划
动态规划(DP)代表一类算法,用于在环境的完美模型(即马尔可夫决策过程(MDP))下计算最优策略。动态规划的基本思想,以及强化学习(RL)的一般理念,是利用状态值和动作寻找好的策略。
蒙特卡洛方法
蒙特卡洛方法用于估计价值函数和发现优秀策略,不需要环境模型的存在。它们能够仅通过智能体的经验或从智能体与环境交互中获得的状态序列、动作和奖励的样本来进行学习。经验可以通过智能体在学习过程中获得,或者通过预先填充的数据集进行模拟。学习过程中获得经验的可能性(在线学习)非常有趣,因为它使得智能体即使在没有先验环境动态知识的情况下,也能够获得优秀的行为。即使是通过一个已经填充的经验数据集进行学习也很有趣,因为当它与在线学习结合时,它使得通过他人经验引发的自动策略改进成为可能。
时序差分学习
时序差分(TD)学习算法基于减少智能体在不同时间点的估计差异。TD 算法试图预测一个依赖于给定信号未来值的量。其名称来源于在预测连续时间步时所使用的差异,以引导学习过程。任何时刻的预测都会更新,以使其更接近下一个时间步预测的同一量。在强化学习(RL)中,这些预测用于预测未来期望的总奖励量。
在接下来的章节中,我们将介绍三种处理时序差分学习的算法家族,每种家族采用不同的方法。
SARSA
SARSA 算法实现了一种基于策略的时序差分方法,其中动作-价值函数的更新是基于从状态 s 到状态 t 通过动作 a 的转移结果,并基于选择的策略进行更新,
(s, a)。
Q 学习
Q 学习是最常用的强化学习算法之一。这是因为它能够在不需要环境模型的情况下比较可用动作的预期效用。得益于这一技术,可以在已完成的 MDP 中找到每个给定状态的最优动作。
深度 Q 学习
深度 Q 学习这个术语标识了一种强化学习方法,用于函数近似。因此,它代表了基本 Q 学习方法的进化,因为状态-动作表被神经网络所取代,目的是近似最优价值函数。
与之前的方法相比,它们用于构建网络,以请求输入和动作并提供预期回报,深度 Q 学习通过结构上的革命,仅请求环境的状态,并提供与环境中可执行的动作数量相同的状态-动作值。
因此,RL 代表了一种先进技术,能够解决不同的现实生活问题。现在,随着这项技术基础知识的详细介绍,是时候探索我们将在本书其余部分中使用的编程平台了。
选择 R 用于 RL
R 表示一种解释型脚本语言,其中“解释型”一词意味着应用程序将在不需要事先编译的情况下执行。R 采用面向对象的编程范式,通过这种方式,它将能够创建现代且灵活的应用程序;在 R 环境中,一切都是可以根据特定需求重用的对象。R 还是一个最初为统计计算和生成高质量图形而开发的环境。它由一个语言和一个运行时环境组成,拥有图形界面、调试器、以及访问一些系统功能的能力,并提供执行存储在脚本文件中的程序的能力。
它对统计学的偏向并非源自语言的性质,而是源自大量统计函数的可用性以及最初发明并随着时间发展这一语言的研究人员的兴趣。
R 的核心是一个解释型编程语言,允许使用常见的结构来控制信息流,并使用函数进行模块化编程。在 R 环境中,用户可见的大多数函数是用 R 本身编写的。R 还是一个开源程序,其受欢迎程度反映了公司内部使用的软件类型的变化。在这方面,我们应该记住,开源软件不仅在使用上没有任何限制,更重要的是,在开发过程中也没有限制。R 的强项在于其在数据分析和表示方面的灵活性。该语言专注于这一领域,并拥有无数的功能,以便为统计学家或数据科学家提供便利。
我们可以总结 R 的特点如下:
-
数据管理和操作的简便性
-
提供一套计算向量、矩阵和其他复杂操作的工具
-
访问大量集成的统计分析工具
-
产生大量特别灵活的图形潜力
-
使用面向对象的编程语言的可能性,该语言允许使用条件和循环结构,以及用户创建的函数
是什么使 R 如此有用,并有助于解释它为何迅速获得用户的接受?原因在于,统计学家、工程师和科学家们随着时间的推移,使用该软件改进代码或为特定任务编写变种,已经开发出了大量的脚本,并将其以包的形式汇集在一起。用 R 编写的包可以为分析数据库中信息提供更详细的高级算法、带有纹理的彩色图形以及数据挖掘技术。现在,让我们看看如何在我们的计算机上安装 R 环境,以便能够复制书中的示例。
安装 R
首先,让我们看看可以在哪里获取软件以便在我们的计算机上安装 R,开始用它进行编程。我们需要安装的包可以在语言的官方网站上找到,www.r-project.org/。
综合 R 档案网络(CRAN)是一个分布在全球各地的服务器网络(实时更新),用于存储与 R 相关的源代码和文档的相同版本。CRAN 可以通过 R 网站直接访问,在该网站上,还可以找到关于 R 的信息、一些技术手册、R 杂志,以及开发用于 R 并存储在 CRAN 仓库中的包的详细信息。
在以下截图中,我们可以看到 R 项目的官方网站:

自然,在下载软件版本之前,我们需要了解可用的计算机类型以及它们上面安装的操作系统;然而,值得注意的是,R 实际上可以在所有流通中的操作系统上使用。R 环境中的编程得益于众多包的可用性。我们来看一下这些包是什么。
R 包
这些包利用 R 的功能将复杂的算法分解成执行特定任务的简单单元,从而便于数据共享。在编程中,我们经常使用重复的代码片段,可能是因为需要对来自不同来源的数据执行相同的操作,或者更简单地说,两个不同的程序执行相似的过程。在这种情况下,每次为执行类似操作而重新编写相同的代码单元显然是低效的。
在这方面,R 语言与所有高级编程语言一样,允许实现子程序,这些子程序可以表示在单独文件中编写的程序部分,或者在同一个文件中作为独立单元,并且可以由主程序调用。从 R 脚本语言的第一个版本开始,就可以利用包(packages),这是一种现代且极其有效的信息交换方式,用于在不同程序单元之间交换信息,并实现增强编程环境的附加功能。R 环境由一系列函数组成,这些函数被聚合到包中——也就是说,这些包是一般专门用于实现某些目标和任务的函数组。因此,一个包就是一组相关的函数、帮助文件和数据文件,它们被整合到一个单独的文件中。R 中的包就像 Perl 模块,它们与 C 或 C++ 中的库以及 Java 类一起使用。
我们机器上安装的 R 发行版默认已经安装了一系列包。这些包与许多其他包一起,在需要时可以激活。此外,还有大量高度专业化的包(贡献包),它们提供执行各种计算和分析类型的有用功能。这些包必须先进行安装。一般而言,安装过程是自动进行的:R 会通过互联网连接到一个存储库(包存档),允许你选择所需的包,下载所需的程序并进行安装。安装后的程序必须通过在工作区加载它来激活。R 提供了良好的工具,可以在 GUI 内安装包,但没有提供同样有效的方式来查找特定的包。幸运的是,借助简单的网页浏览器,在网上找到包非常容易。例如,我们可以在 CRAN 网站上搜索我们的包,网址为 cran.r-project.org/web/packages/。
当前,CRAN 包存储库中提供 14,345 个可用包。以下截图显示了 CRAN 存储库的网页:

此时,确定实现基于 RL 的算法所需的包就足够了——让我们从 MDPtoolbox 包开始。
使用 MDPtoolbox 包
在强化学习(RL)中,通常假设环境可以通过马尔可夫决策过程(MDP)来描述。这个话题将在第三章中进一步讨论,马尔可夫决策过程的应用。现在,我们将讨论 MDPtoolbox 包,它是一个专门为解决基于马尔可夫决策过程的问题而创建的 R 语言包。该包提供了与解决离散时间马尔可夫决策过程相关的函数——如有限时域、值迭代、策略迭代和线性规划算法(以及一些变体)——并且还提供了与强化学习(RL)相关的一些函数。
以下表格提供了该包的一些信息:
| 包名 | MDPtoolbox |
|---|---|
| 日期 | 2017-03-02 |
| 版本 | 4.0.3 |
| 标题 | 马尔可夫决策过程工具包 |
| 作者 | Iadine Chades, Guillaume Chapron, Marie-Josee Cros, Frederick Garcia, Regis Sabbadin |
以下列表展示了该包中最有用的函数,并附上了来自官方文档的简短描述:
-
mdp_Q_learning: 使用 Q 学习算法(RL)解决折扣马尔可夫决策过程(MDP) -
mdp_computePR: 计算任意形式的转移和奖励函数的奖励矩阵 -
mdp_eval_policy_iterative: 使用迭代方法评估策略 -
mdp_LP: 使用线性规划算法解决折扣马尔可夫决策过程(MDP) -
mdp_policy_iteration: 使用策略迭代算法解决折扣马尔可夫决策过程(MDP) -
mdp_relative_value_iteration: 使用相对值迭代算法解决具有平均奖励的马尔可夫决策过程(MDP) -
mdp_value_iteration: 使用值迭代算法解决折扣马尔可夫决策过程(MDP)
MDPtoolbox 通过设定优化准则来解决马尔可夫决策过程,找出最优策略。在这个准则的基础上,识别出能提供最高累计奖励的策略。该包使用了四种最常用的优化准则。强化学习(RL)在现实生活中的各种应用中尤其有用:我们来看看其中一些应用。
强化学习(RL)应用
从手机到无人驾驶汽车,消费社会已经开始关注强化学习(RL)的力量。事实上,近年来,强化学习作为一项基础技术在多个领域崭露头角:从语音、文本和面部识别到多语言翻译,从交通控制系统到互联网流量控制系统。然而,近年来,这项技术在现实世界中的最新应用例子包括医学诊断、互联网安全以及构建预测模型来做出重要商业决策。
事实上,强化学习(RL)教会机器人和机器像人类一样自然地做事:与环境互动并从经验中学习。新的低成本硬件推动了深度和多层神经网络的使用,这些网络模拟人脑的神经网络。因此,生产技术获得了识别图像和趋势、做出预测以及做出智能决策的全新能力。从最初训练期间发展出的基本逻辑开始,基于强化学习的算法可以通过监控环境状态的代理提供的反馈,持续优化性能。在接下来的部分,我们将讨论一些示例。
软件故障预测
现代软件系统的复杂性不断增长,而这种复杂性的增加也导致了软件故障的增加,后者在系统故障中发挥着重要作用。我们的目标是尽可能地提高系统在某段时间内不发生故障的概率,这段时间被称为任务时间。处理软件故障通常是一项复杂的工作,其中一个主要问题是故障的可重现性,即识别故障激活模式的能力。测试活动在应对这种类型的故障时被证明是不足的。由于几乎不可能识别所有可能的故障,关键系统通常采用容错机制。
容错机制由一系列程序组成,使得系统即使在出现故障的情况下也能继续运行。验证容错机制的主要技术之一是软件故障注入,即在系统组件中引入软件故障(错误),以分析它对其他组件以及整个系统的影响。这一技术非常重要,因为软件故障是系统故障的一个重要原因。现代的解决方案采用基于强化学习(RL)的方法,在复杂的软件系统中进行故障注入。这种系统架构使我们能够以简单而有效的方式分析算法,易于集成到复杂系统中,并且具有良好的可扩展性。此外,它还允许我们对算法进行探索性分析,以评估该方法在重大案例中的适用性,并为未来的集成做好准备。
自适应交通流量控制
道路交通理想情况下应像流体动力学研究方法那样进行研究。同样,交通流有自然的惯性,会随着时间的推移稳定下来,就像流体因其固有特性而在积聚时形成不稳定的失衡情况一样。然而,任何瓶颈的出现都会阻碍流动,交通拥堵总是迫在眉睫。在信号控制交叉口的自适应调整中,目标是调整孤立的交叉口,以优化其容量并最小化车辆延误。自适应控制方法有许多种,通常会考虑可用的检测能力。
最广泛的自治方法是基于不同交叉口入口处的流量和密度测量,这些入口距离交叉口的距离不同。其他较新的方法则利用摄像头提供的数据,如尾巴长度和转弯动作。因此,有用的数据基本上是基本变量——流量和密度(或使用率)、排队长度以及转弯动作。在这些系统中,基于机器学习的技术已多次被用于解决这一问题。强化学习(RL)尤其适合,因为它能够通过传感器实时检测到的测量与环境进行交互,这些传感器通常放置在信号控制交叉口附近。
展示广告
展示广告使用网页上的商业空间来推广产品或服务。这种广告方式不同于按点击付费模式,因为它还利用了图形元素。公司购买一个或多个属于广告网络的网页空间,并在这些已购买的空间中展示自己的广告。此外,搜索引擎不会随机地在广告商购买的空间中展示广告,而是仅展示与用户搜索相关的广告,并与用户浏览的页面历史记录相符。为了自动且最佳地投放广告,广告商必须开发一种学习算法,以便在实时展示广告时提供智能报价。
到目前为止,使用的大多数算法采用了基于静态优化的方式来处理每次展示的价值。优化过程要么独立进行,要么为每个广告量段设定报价;然而,在广告活动的过程中,广告报价会在预算到期之前多次重复。出价决策可以视为一个强化学习问题,其中状态空间由拍卖信息和实时的活动参数表示,而动作则是需要设置的出价。
机器人自治
自我治理意味着在独特和非结构化的条件下工作,且不需要持续的人类参与。在这种情况下,许多情况是无法通过以往经验得知的,这要求机器人(或任何自我治理系统)能够识别当前环境的显著特征,并根据需要进行行动,决定采取哪些措施。
避免在长时间内进行人类干预的需求意味着,机器人自我管理和生存的完全能力(例如,通过避开障碍保持身体自由或避免完全耗尽能量供应)必须是首要任务。通常,这些机器在工作单元内执行任务,工作单元能促进这些操作,并且禁止接触任何外部元素,包括人类。这类机器人可以通过评估与工作场所相关的多个角度来创建,且大多数情况下,会使用大型的编程控制算法来赋予机器人通过利用从环境中收集的数据来工作的能力。
计算机视觉识别
计算机视觉是一组旨在通过二维图像创建现实世界(3D)近似模型的过程。机器视觉的主要目的是再现人类视觉。视觉不仅仅是理解为获取某一地区的二维照片,更重要的是对该地区内容的解释。在这种情况下,信息被理解为意味着自动决策的事物。
机器学习已经成为解决与计算机视觉相关的各种任务的标准,例如特征检测、图像分割、物体识别和跟踪。在现代控制系统中,机器人配备了视觉传感器,可以利用这些传感器通过解决相应的计算机视觉任务来学习周围环境的状态。这些系统用于决策关于可能的未来行动。强化学习(RL)用于解决计算机视觉问题——如物体检测、视觉检测和动作识别——以及机器人导航。
游戏
游戏是人工智能(AI)的一个重要研究领域,因为它们提供了现实问题的便捷模型。事实上,游戏虽然呈现出与现实世界问题相当的复杂性,但却有明确且可形式化的规则。此外,每个游戏都有能够评估机器所产生结果质量的专家。因此,首先在一个明确的环境中进行工作和实验(如游戏世界),然后将获得的结果推广和适应到更具变化性的环境中(如现实世界),是非常方便的。
在现有的各种游戏类型中,最成功的科学研究对象是完美信息游戏,即具有两个玩家的确定性游戏。这个类别包括国际象棋、跳棋和围棋。因此,研究人员的注意力主要集中在国际象棋游戏上。在过去的几年中,得益于多位科学家的努力和奉献,已经创造出了一个能够达到世界冠军水平的人工智能棋手。
该项目中最重要的成功是在深蓝击败国际象棋世界冠军加里·卡斯帕罗夫时取得的。另一个例子是人工智能学习如何下围棋——这是一种类似于国际象棋的中国起源的游戏。2016 年初,韩国棋手李世石与谷歌的人工智能 AlphaGo 之间进行了一场历史性的围棋对局。
金融市场预测
最近,金融市场的分析经历了重要的发展,无论是在基础研究方面还是在市场直接应用方面。尤其是数据的计算机化使得关于价格趋势和交易量的详细信息变得轻松可得,从而创造了新的研究领域。同时,电子交易系统的引入使得大型金融机构有兴趣通过算法自动化交易过程。
然而,这些模型不能盲目应用。虽然有一些通用的规则,但研究者的任务是确定最适合描述所提出问题的特征参数。在这种背景下,强化学习(RL)自然地适应了这个环境,因为它具有与环境互动的能力,可以实时验证市场对已做出的预测的反应。根据收到的反馈,它可以通过将数据引导到正确的方向来修正预测。
摘要
在本章中,我们探索了机器学习的奇妙世界,并根据用于学习的信号性质以及系统采用的反馈类型,分析了三种可用的范式。我们游览了最流行的强化学习(RL)算法,以选择最适合我们需求的算法,并理解什么最适合我们的需求。
接着,我们介绍了 R 脚本语言及其使其特别适合处理强化学习问题的特点。然后,我们探索了MDPtoolbox包——该包提供了与离散时间马尔可夫决策过程求解相关的函数:有限时域、值迭代、策略迭代和线性规划算法。最后,我们分析了一系列强化学习应用,特别是那些正在现实世界中迅速传播并取得惊人结果的现代应用。
在下一章中,我们将学习代理–环境接口。我们将学习如何使用马尔可夫决策过程。我们还将了解规范的梯度方法,并且将研究最广泛使用的强化学习包——R。
第二章:强化学习的构建模块
基于强化学习的算法的主要目标是学习并适应环境的变化。为了实现这一目标,我们利用环境根据算法做出的选择生成的外部反馈信号(奖励信号)。在这种情况下,算法推荐的正确选择将会获得奖励,而错误的选择将会受到惩罚。所有这些操作都是为了实现最佳的结果。在本章中,你将会了解构建模型的智能体与环境接口的概念。在本章结束时,你将准备好深入研究马尔可夫决策过程。我们还将探索策略的基本概念,并通过应用策略梯度来提升结果。
在本章中,我们将涵盖以下主题:
-
智能体与环境的接口
-
理解马尔可夫决策过程
-
解释策略
-
探索策略梯度方法
智能体与环境接口
在强化学习中,智能体学习并适应环境的变化。这种编程技术的基础源于根据算法选择接收外部刺激的概念。正确的选择会带来奖励,而错误的选择会带来惩罚。系统的目标是实现最佳的结果。
这些机制源自机器学习的基本概念(从经验中学习),旨在模拟人类推理。事实上,在我们的脑海中,我们激活了大脑机制,促使我们追逐并重复那些带来满足感和幸福感的体验。每当我们经历愉悦的时刻(如食物、音乐、艺术等),我们的脑部会分泌一些物质,这些物质通过强化同样的刺激来加深这种体验。伴随着这种神经化学强化机制,我们的记忆会记住这一经历,以便将来能够重新体验这些感觉。进化为我们提供了这种机制,使我们能够重复对我们有益的经历。
这就是为什么我们记得生活中重要的经历,特别是那些带来巨大奖励的经历,它们成为我们记忆的一部分,并影响我们未来的探索。根据所使用的学习信号的性质或系统返回的反馈类型,学习经验可以通过数值算法以不同方式进行模拟。
在监督学习中,系统有一个教师,告诉它正确的输出是什么。实际上,并不总是有一个导师引导我们做出选择。通常,我们只能获得定性的反馈信息,了解环境如何回应我们的行为。这些信息被称为强化信号。系统不会提供关于如何更新智能体行为(即权重)的任何信息。你不能定义代价函数或梯度。系统的目标是创建能够从经验中学习的智能代理。在下图中,我们可以看到展示强化学习如何与环境交互的流程图:

科学文献对于强化学习是否作为一种范式的分类持不确定态度。事实上,在其初期阶段,它被视为监督学习的一个特例,直到完全被推广为机器学习算法的第三大范式。它应用于监督学习效率低下的不同场景:与环境交互的问题就是一个明显的例子。
我们需要遵循以下步骤来正确应用强化学习算法:
-
准备智能体。
-
观察环境。
-
选择最佳策略。
-
执行动作。
-
计算相应的奖励(或惩罚)。
-
开发更新的策略(如果需要)。
-
反复执行步骤 2 - 5,直到智能体学会最佳策略。
强化学习基于一个心理学理论,该理论源自一系列对动物进行的实验。特别是,美国心理学家爱德华·桑代克(Edward Thorndike)指出,如果一只猫在表现出被认为正确的行为后立即获得奖励,那么猫重复这一行为的概率会增加。然而,面对不想要的行为时,施加惩罚则会降低猫重复该行为的概率。
基于这一理论,强化学习试图最大化执行某个行为或一组行为时获得的奖励,以便实现某个特定目标。
强化学习可以看作是实现目标的交互问题的特例。必须实现目标的实体被称为智能体。智能体必须与之交互的实体被称为环境,它对应着智能体外部的一切。
到目前为止,我们一直关注术语“智能体”,但它代表什么呢?智能体(软件)是一个代表其他程序执行服务的软件实体,通常是自动和隐形的。这些软件也被称为智能代理。
以下是智能体最重要的特征:
-
它可以选择对环境执行的动作,动作可以是连续的或离散的。
-
执行的动作取决于情况。情况在系统状态中得到总结。
-
代理持续监控环境(输入),并不断改变状态。
-
选择动作并非 trivial(微不足道),需要一定程度的“智能”。
-
代理具有智能记忆。
代理有目标导向的行为,但它在不确定环境中如何行动是事先无法知道的。代理通过与环境的互动来学习。规划可以在代理通过其测量了解环境时逐步开发。这一策略与试错理论接近。
试错理论是解决问题的一个关键方法。试验会不断重复,直到代理成功或停止尝试。
代理与环境的互动是持续的,因为代理选择要采取的行动,并且环境会改变状态,呈现出代理将面临的新情况。
在强化学习的情况下,环境为代理提供奖励。奖励的来源必须是环境,以避免形成个人强化机制,这样会妨碍学习。
奖励的值与行动在达成目标方面的影响成正比,因此在正确的行动下奖励是正向或高的,在错误的行动下奖励是负向或低的。
以下是一些现实生活中的例子,其中代理和环境通过互动解决了特定问题:
-
一个棋手,每一步都能提供关于对手可能采取反击的提示。
-
一只小长颈鹿,几小时内可以学会站起来并以每小时 50 公里的速度奔跑。
-
一台真正的自主机器人学会在房间里移动,从而能够走出房间。
-
炼油厂的参数(如油压、流量等)实时设置,以便我们获得最大产量或最高质量。
所有这些例子有以下共同特点:
-
与环境的互动
-
代理的目标
-
环境的不确定性或部分知识
从中可以得出以下观察:
-
代理从自身经验中学习
-
行为会改变状态(情况)以及未来可以做出多少变化(延迟奖励)。
-
一项行为的效果无法完全预测。
-
代理具有对其行为的全局评估。
-
代理必须利用这些信息来改善其选择。这些选择随着经验的积累而改进。
-
问题可以有有限或无限的时间范围。
本质上,代理通过其传感器从环境中接收感知。根据其感受,代理决定在环境中采取什么行动。根据其行动的即时结果,代理可能会得到奖励。
如果你想使用自动学习方法,你需要给出环境的正式描述。知道环境的具体构成并不重要——有趣的是对环境的属性做出一般性假设。在强化学习中,通常假设环境可以用马尔可夫决策过程来描述。让我们来学习一下。
理解马尔可夫决策过程
为了避免负载问题和计算困难,代理-环境交互被视为马尔可夫决策过程。马尔可夫决策过程是一个离散时间随机控制过程。
随机过程是用于研究遵循随机或概率法则的现象演化的数学模型。在所有自然现象中,已知由于其本质以及观察误差,存在随机或偶然成分。这个成分表明,在每一时刻 t,观察现象的结果是一个随机数或随机变量 st:无法确定地预测结果是什么;我们只能说它将取多个可能值中的一个,每个值都有一个给定的概率。
当选择某一时刻 t 的观察时,若一个随机过程被认为是马尔可夫过程,则从 t 开始,过程的演变仅依赖于 t,而与之前的时刻无关。因此,马尔可夫过程指的是在给定观察时刻的情况下,时刻决定了过程的未来演变,而这一演变不依赖于过去。
在马尔可夫过程中,每个时间步,过程处于某一状态 s ∈ S,代理可以选择状态 s 中可用的任何动作 a ∈ A。在下一时间步,过程会通过随机移动到新状态 s',并给予代理一个相应的奖励 r(s,s')。
在以下图示中,我们可以看到马尔可夫决策过程中代理与环境的交互:

前面图示中的代理-环境交互可以总结如下:
-
代理与环境在离散的时间间隔上进行交互,t = 0, 1, 2… n。
-
在每个间隔时,代理接收环境状态的表示 st。
-
每个元素 st S,其中 S 是可能状态的集合。
-
一旦状态被识别,代理必须在 A(st) 中采取一个动作,其中 A(st) 是状态 st 中可能的动作集合。
-
采取行动的选择依赖于要实现的目标,并通过策略 π(折扣累积奖励)进行映射,该策略将每个状态 s 中的动作与 A(s) 关联。术语 πt(s,a) 表示在状态 s 中执行动作 a 的概率。
-
在接下来的时间间隔 t + 1 中,作为行动的结果,代理会收到一个数值奖励,rt + 1 R,对应于之前采取的行动。
-
现在,行动的结果代表新的状态 st。此时,代理必须对状态进行编码,并根据将要采取的行动做出选择。
-
这个迭代会不断重复,以便代理达到目标。
状态 st + 1 的定义取决于前一个状态和所采取的行动(MDP),如下所示:

在公式中,δ 代表状态函数。
总结来说,我们可以陈述以下内容:
-
在马尔可夫决策过程中,代理可以感知它所处的状态 s S,并且有一组可用的行动。
-
在每个离散时间间隔 t 中,代理会检测当前状态 st 并决定执行某个动作 A。
-
环境通过提供奖励(强化)rt = r (st, at) 并进入状态 st + 1 = δ (st, at) 来做出回应。
-
r 和 δ 函数是环境的一部分;它们仅依赖于当前的状态和行动(而非之前的状态和行动),且不一定为代理所知。
-
强化学习的目标是学习一个策略,对于系统所处的每个状态 s,为代理指定一个行动,以便它能够最大化在整个行动序列中获得的总强化。
让我们更详细地讨论一下我们使用的一些术语:
-
奖励函数定义了强化学习问题中的目标。它将环境检测到的状态映射为一个数值,从而定义了奖励。正如我们之前提到的,唯一的目标是最大化它在长期内获得的总奖励。奖励函数决定了哪些行动对代理是正面的,哪些是负面的。奖励函数需要是正确的,并且可以作为改变策略的基础。如果策略建议的行动返回较低的奖励,那么在下一步中,可以修改策略以在相同情况下建议其他行动。
-
策略定义了学习代理在给定时间的行为。它将检测到的环境状态和在这些状态下应采取的行动进行映射。这对应于心理学中所谓的刺激反应规则或关联。策略是强化学习代理的核心部分,因为它足以决定行为。
-
值函数表示某个状态对于代理的价值。它等于从状态 s 开始,代理预期获得的总奖励。值函数依赖于代理为所要执行的动作选择的策略。
-
动作值函数返回一个值,即在某一状态 s 下,按照策略执行动作 a 后的预期回报(总体奖励)。
在下一节中,我们将学习如何最大化在学习过程中获得的总强化。
折扣累积奖励
在马尔可夫决策过程部分,我们提到过,强化学习的目标是学习一个策略,对于每个状态s,在该状态下,指定一个动作给代理,以便它能够最大化在整个动作序列中获得的总强化。那么,我们如何在整个动作序列中最大化获得的总强化呢?
从策略中获得的总强化计算如下:

这里,r[T]表示驱动环境进入终止状态 s[T]的动作的奖励。
解决这个问题的一个可能方法是将提供最高奖励的动作与每个状态关联;也就是说,我们必须确定一个最优策略,以便最大化前述量。
对于在有限步数内无法到达目标或终止状态的问题(持续任务),Rt 会趋向于无限大。
在这些情况下,我们希望最大化的奖励和会在无穷大处发散,因此这种方法不可行。由于这个原因,有必要开发一种替代的强化技术。
最适合强化学习范式的技巧是折扣累积奖励,它试图最大化以下量:

这里,γ被称为折扣因子,代表未来奖励的重要性。该参数可以取值 0 ≤ γ ≤ 1,具有以下含义:
-
如果γ < 1,序列rt将收敛到一个有限值。
-
如果γ = 0,代理将不关心未来的奖励,而是尝试最大化当前状态的奖励。
-
如果γ = 1,代理将尽力增加未来奖励,即使这会以当前的奖励为代价。
折扣因子可以在学习过程中进行修改,以突出特定的动作或状态。一个最优策略可以导致执行单一动作时获得的强化较低(甚至为负),前提是这会导致更大的强化。当你希望收集有用信息时,探索环境是一种正确的方式。然而,在某些情况下,这也是一个计算开销较大的过程,所以我们来看一下如何处理这个问题。
探索与利用
理想情况下,代理必须将每个动作与相应的奖励r相关联,以选择最有奖励的行为来实现目标。对于复杂问题,这种方法是不实际的,因为状态数量特别高,且可能的关联以指数级增长。
这个问题被称为探索-开发困境。理想情况下,代理必须探索每个状态下所有可能的行为,并找到当其被开发时奖励最多的行为。
因此,决策涉及一个根本性的选择:
-
开发:根据当前信息做出最佳决策。
-
探索:这会收集更多信息。
在这个过程中,最好的长期策略可能需要在短期内做出相当大的牺牲。因此,有必要收集足够的信息来做出最佳决策。
探索-开发困境在我们尝试学习新事物时总会有所启示。通常,我们必须决定是选择我们已经知道的东西(开发),从而让我们的文化包袱保持不变,还是选择新的东西,转而通过这种方式来学习(探索)。第二种选择有可能使我们做出错误的决策。这是我们经常面临的一个经历;例如,想一想我们在餐厅选择菜单时所做的决策:
-
我们可以选择已经了解的东西,过去曾给我们带来已知的满足感和奖励(开发),比如比萨饼(谁不知道玛格丽塔比萨的美味呢?)。
-
我们可以尝试一些以前从未品尝过的新东西,看看会有什么收获(探索),比如千层面(唉,并不是每个人都知道千层面碗中的神奇味道)。
我们做出的选择将取决于许多边界条件:菜肴的价格、我们的饥饿程度、我们对菜肴的了解等等。重要的是,研究如何做出这种选择的最佳方法表明,最佳学习有时需要我们做出错误的选择。这意味着,有时你必须选择避免你认为最有回报的行动,而选择一个你认为回报较少的行动。其逻辑是,这些行动对于获得长期利益是必要的:有时候,你需要弄脏你的双手才能学到更多。
以下是一些在实际案例中采用此技术的更多示例:
-
选择商店:
-
开发:去你最喜欢的商店
-
探索:尝试新商店
-
-
选择路线:
-
开发:选择你已知的最佳路线
-
探索:尝试一条新路线
-
在实践中,对于非常复杂的问题,收敛到一个非常好的策略可能太慢。解决这个问题的一个好方法是找到探索和开发之间的平衡:
-
一个仅限于探索的代理将在每个状态下始终采取随意的行动,显然收敛到一个最优策略是不可能的。
-
如果一个代理仅仅进行少量探索,它将总是使用它通常会使用的行为,这可能不是最优的。
在每一步,代理都必须在重复迄今为止的做法和尝试新的方法之间做出选择,后者可能会获得更好的结果。
在选择要执行的动作时,策略至关重要。在下一节中,我们将通过分析在寻找最佳策略时可以使用的不同方法来进一步探讨这一点。
解释策略
正如我们在马尔可夫决策过程部分提到的,策略定义了学习代理在某一时刻的行为。它将环境中检测到的状态与在这些状态下采取的行动对应起来。策略是强化学习代理的核心部分,因为仅凭策略就足以决定行为。策略在代理需要做出选择时至关重要。事实上,一旦获得观察结果,接下来的决策就是基于策略来做出的。在这种情况下,我们不需要状态的值或特定动作的值——我们只需要考虑能够实现总奖励的策略。
策略可以是确定性的,即对于给定状态采取相同的动作;也可以是概率性的,即基于某些分布计算在状态之间选择的动作。
为了更好地理解策略的含义,我们来看一个例子。假设我们需要实现一个算法,让送货车辆从商店开到顾客家里。我们可以定义以下元素:
-
道路地图是环境。
-
车辆的当前位置是一个状态。
-
策略就是代理为完成任务所采取的行动。
现在,我们提供一些我们的车辆可以采用的策略示例,以完成任务,比如送货:
-
策略 1:不受控制的车辆将随机移动,直到它们偶然到达正确的地方(顾客家)。在这里,可能会消耗大量燃料,且送货过程会持续很长时间。
-
策略 2:其他车辆可以学会只在主干道上行驶,从而行驶更长的距离。
-
策略 3:受控车辆将通过选择一条能将它们带到目的地的路线来规划路线,从而行驶较少的道路。
显然,一些策略优于其他策略,且有许多方法可以评估它们,即状态的函数值和值-行动函数。目标是学习最佳策略。策略可以通过两种方式来接近:策略迭代和策略搜索。这两种技术的主要区别在于价值函数的使用。在接下来的部分中,我们将详细分析这两种方法。
策略迭代
策略迭代是一种动态规划算法,使用价值函数来建模每对动作-状态的期望回报。强化学习中的许多技术都基于这一技术,包括 Q 学习、TD 学习、SARSA、QV 学习等。这些技术通过立即奖励和下一个状态的(折扣后)价值来更新价值函数,这一过程称为引导法。因此,它们隐含了将Q(s, a)存储在表格中或使用近似函数技术的做法。
策略迭代通常应用于离散的马尔可夫决策过程,其中状态空间S和动作空间A都是离散且有限的集合。
从初始的 P[0]策略开始,策略迭代在以下两个阶段之间交替进行:
-
策略评估:给定当前策略 P,估计动作价值函数 Q[P]。
-
策略改进:基于 Q[P]计算更好的策略 P',然后将 P'设为新的策略并返回到上一步。
当可以计算每个动作-状态对的动作价值函数时,采用贪婪策略改进的策略迭代会通过返回最优策略而收敛。实质上,反复执行这两个过程将一般过程收敛到最优解。
不幸的是,Q[P]价值函数并非总能被精确计算;通常只能通过样本进行估计。在这种情况下,必须在策略中引入一定程度的随机性,以确保对状态空间的充分探索。这些算法将价值函数存储在有限的表格中(表格方法)。这些算法的局限性在于,它们无法应用于连续马尔可夫决策过程的情况。此外,在某些离散情况下,如果状态-动作空间的基数过高,这些方法也可能无法使用。让我们看看另一种解决这一问题的方法。
策略搜索
在策略搜索方法中,存储的是一个参数化的策略,但不使用或估计价值函数。策略搜索方法可以依赖于使用基于轨迹的采样的展开策略。其他策略搜索方法则使用如进化算法等优化技术来搜索最优策略参数。策略梯度方法就是策略搜索的一个例子。让我们详细看看这些方法。
探索策略梯度方法
策略梯度是一类基于参数化策略的强化学习算法。其思想是计算每个参数相对于预期回报梯度(奖励),从而在有利的方向上调整参数,以提高性能。该方法没有传统强化学习中的问题,如缺乏值函数保证、状态不确定性带来的问题以及连续空间中状态和动作的复杂性。在策略搜索方法中,不使用或估计值函数。值函数可以用来学习策略参数;然而,动作选择时并不一定需要它。策略梯度方法通过直接寻找最优策略,绕过了与基于值函数的技术相关的所有问题。
应用策略梯度方法的优点如下:
-
连续状态和动作可以视作离散情况处理,学习性能通常会提高。
-
文献中有各种不同的算法,它们具有强大的理论基础。
-
即使没有使用特定的状态估计器,状态不确定性也不会降低学习过程的效果。
应用策略梯度方法的缺点如下:
-
学习率可以决定收敛速度的数量级。
-
它们需要非常快速地更新数据,以避免在梯度估计器中引入错误。这意味着样本数据的使用效率不高。
实际中,策略梯度返回的是我们需要修改算法参数的方向,以改善策略并最大化总的累积奖励。这个梯度等于所选动作的对数概率的梯度。换句话说,我们希望增加那些能带来良好总奖励的动作的概率,并减少那些带来负面结果的动作的概率——我们保留有效的部分,剔除无效的部分。
正如我们在马尔可夫决策过程部分提到的,选择要执行的动作取决于要实现的目标,并通过表示为π的策略(折扣累积奖励)映射, π将每个状态 s 下的动作与 a ∈ A(s)关联。术语πt(s,a)表示在状态s下执行动作a的概率。
该过程的目的是通过使用θ参数对策略进行参数化,这样我们就可以确定在状态s下的最佳动作a。这个策略函数将定义为πθ(s,a)。
为了找到最优策略,我们将使用一个神经网络,该网络将输入状态并输出该状态下每个动作的概率。这个概率将用于从该分布中采样一个动作,并在该状态下执行该动作。没有任何信息告诉我们,所采样的动作是执行该状态下的正确动作。然后,我们执行该动作并记录奖励。这个过程将对每个状态重复进行。获取的数据将作为我们的训练数据。在此时,为了更新梯度,我们将使用一个基于梯度下降的算法。
通过这样做,在某一状态下返回高奖励的动作将具有较高的概率,而低奖励的动作将具有较低的概率。
与所有梯度下降方法一样,参数向量按照性能度量的梯度方向进行更新。在此情况下,性能的度量由以下公式给出:

在上述公式中,E 是期望回报,r 是奖励。我们的目标是学习一个能够最大化累积未来奖励的策略。期望回报的梯度被称为策略梯度,我们将利用它来更新 θ 参数,如下所示:

这里,我们有以下内容:
-
θ 是参数
-
∇ 是期望回报的梯度
-
α 是学习率
每次学习迭代时都会执行参数更新。梯度下降算法保证至少会收敛到一个局部最优解。
一般的策略梯度算法可以总结如下:
set initial policy parametrization
repeat until converge
generate N trajectories following a policy πθ
compute policy gradient estimate
update the parameter θ
在接下来的部分,我们将分析一些基于策略梯度的方法,即蒙特卡洛策略梯度和演员-评论员方法。
蒙特卡洛策略梯度
蒙特卡洛策略梯度,亦称为REINFORCE,是一类用于与随机单元连接的网络的关联强化学习算法。研究表明,在具有即时强化的任务中,且在一些限制条件下,甚至在一些具有延迟强化的任务中,这些算法能够在期望强化的梯度方向上进行参数调整。由于其特性,这些算法能够与其他梯度下降方法,尤其是反向传播方法,轻松结合。主要的缺点是它们无法区分局部(全局)最大值(最小值),且没有通用的收敛理论。
在这类算法中,智能体通过当前策略生成一个情节轨迹,并利用该轨迹来更新策略参数。该算法提供了一种脱离策略的更新,因为必须完成完整的轨迹才能构建样本空间。
以下代码是 REINFORCE 算法的伪代码:
Initialise θ randomly
For each episode
For t=1:T-1
calculate return
update θ
如我们所见,不需要显式的探索。在这种情况下,通过神经网络计算概率后,探索会自动进行。首先,使用随机权重初始化网络,并返回均匀概率分布。该分布等价于随机智能体的行为。
为了减少梯度估计的方差同时保持偏差,可以通过从回报中减去基准值来修改 REINFORCE 算法。
现在,让我们看看基于策略梯度的另一种方法。
Actor-critic 方法
Actor-critic 方法结合了策略搜索和价值函数估计,以便执行低方差的梯度更新。这些方法将记忆结构分开,使得策略与价值函数相互独立。策略模块被称为演员,因为它选择动作,而估计的价值函数模块被称为评论员,因为它对由策略执行的动作进行评估。从这一点来看,可以明确地看出学习是一种基于策略的方法,在这种方法中,评论员学习并评估所跟随的策略的执行结果。
通常,评论员是状态评估函数。每次选择动作后,评论员评估新状态,以确定事情是否比预期更好或更糟。
智能体执行两项任务,并通过两种不同的网络扮演两个角色:
-
演员 网络是根据评论员建议的方向,通过更新策略参数来确定在某一状态下需要执行的动作。
-
评论员 网络是评估该动作后果的网络,修改下一时间步的函数值,并更新价值函数参数。
环境通过输入传感器进行感知和测量。系统在状态的评估/估计过程中处理这些输入。然后,状态估计和任何奖励会传递给智能体。
以下是 Actor-critic 算法的伪代码:
Initialize parameters randomly
For t=1:T
Sample reward and next state
Sample the next action
Update the policy parameters
Compute the correction for action-value at time t
Use the correction to update the parameters of action-value function
Update action and state
如我们所见,评论员网络和演员网络在每一步都进行更新。
总结
在本章中,我们学习了如何基于马尔可夫过程构建模型的基础知识。马尔可夫决策过程是一个由五个元素组成的随机过程:时刻、状态、动作、转移概率和奖励。过程的路径由一个控制的智能体来决定。在路径的某个特定点和时刻,t,智能体介入并做出决定,这个决定将影响过程未来的演变。这些时刻称为决策的时刻,而所做的决策则具有动作的含义。
接着,我们发现了定义学习智能体在特定时刻行为的策略。它映射了环境的检测状态以及在这些状态下应该采取的行动。策略是强化学习智能体的核心部分,因为单凭它就足以决定行为。策略可以通过两种方式来处理:策略迭代和策略搜索。这两种技术的主要区别在于它们对价值函数的使用。
最后,我们学习了如何基于策略梯度方法构建模型,也就是基于使用参数化策略的一类强化学习算法。其核心思想是计算相对于每个参数的期望回报梯度(奖励),以便在某个方向上调整参数,从而提高性能。
在下一章,我们将看到马尔可夫决策过程的实际应用。我们将深入理解马尔可夫决策过程的概念,并理解智能体与环境的交互过程。接下来,我们将学习如何使用贝尔曼方程作为最优价值函数的一致性条件,从而确定最优策略。最后,我们将发现并实现马尔可夫链,并学习如何使用它们来模拟随机游走。
第二部分 - 强化学习算法与技术
在本节中,我们将深入探讨最广泛使用的强化学习技术,详细分析每种算法的基本概念,发展相应的数学工具,并评估实际限制和可用的替代方法。
本节包含以下章节:
-
第三章,马尔可夫决策过程实战
-
第四章,多臂老丨虎丨机模型
-
第五章,最优策略的动态规划
-
第六章,蒙特卡罗方法在预测中的应用
-
第七章,时间差分学习
第三章:马尔科夫决策过程实践
随机过程涉及根据概率法则随时间(或更广泛地,随空间)演变的系统。此类系统或模型描述了具有随机性的现实世界复杂现象。这些现象比我们想象的更常见。当我们感兴趣的量无法被绝对确定地预测时,我们便遇到这些现象。然而,当这些现象表现出多种可能的结果,并且能够以某种方式被解释或描述时,我们便可以引入该现象的概率模型。
马尔科夫链是一种随机过程,其中系统的演变仅依赖于其当前状态,而不依赖于其过去状态。马尔科夫链由一组状态和状态之间转移发生的概率所表征。可以想象一个点在一条线上沿着离散时间间隔随机向前或向后移动,每个时间间隔覆盖一定的距离。
在本章中,我们将深入理解马尔科夫过程的概念。我们将详细分析随机马尔科夫过程。我们将介绍马尔科夫链,并学习如何利用这些算法进行天气预测。最后,我们将学习如何评估解决马尔科夫奖励问题的最优策略。
本章将涵盖以下主题:
-
马尔科夫过程概述
-
马尔科夫链简介
-
马尔科夫链应用 —— 天气预测
-
马尔科夫奖励模型
技术要求
查看以下视频,查看代码的实际应用:
马尔科夫过程概述
正如我们在第二章《强化学习的构建模块》中提到的,当选择某一时刻 t 进行观察时,随机过程被称为马尔科夫过程。从 t 开始的过程演变仅依赖于 t,而不依赖于之前的任何时刻。因此,当给定观察时刻后,只有该时刻决定了过程的未来演变,而该演变不依赖于过去时刻时,过程就是马尔科夫过程。在接下来的部分,我们将探索随机过程的概念,并看到它如何与概率论相关联。
理解随机过程
为了提供马尔科夫过程的正式定义,有必要明确什么是具有时间顺序的随机变量集合。这样一个随机变量集合最好通过一个随机过程来表示。
随机过程理论研究的是那些根据概率法则随时间(但也可以更一般地随空间)演变的系统。这类系统或模型描述了现实世界中那些可能是随机的复杂现象。这样的现象比我们想象的更为频繁,我们在面对那些我们无法确定预测的量时,常常会遇到这种情况。我们定义了一个在离散时间和离散状态下的随机过程,它由以下随机变量构成:

在前面的序列中,每个 X[n] 都是一个离散随机变量,取值来自一个集合 S = s[1], s[2],…, s[n], 这个集合被称为状态空间。为了不失一般性,假设 S 是整数集 Z 的子集。随着索引 n 的变化,每个 X[n] 的值将代表系统随时间变化的状态。我们将要分析的过程从任何由 X[n] 表示的状态开始,并将转移到下一个状态 X[n + 1]。每一次转移称为一步。
随着时间的推移,过程可以从一个状态跳跃到另一个状态。如果第 n 步处于状态 i,而在下一步 n + 1 处于状态 j ≠ i,我们可以说发生了转移。
计算概率
给定一个随机过程(X[n]),我们感兴趣的是计算与之相关的概率。现在,让我们探讨概率的基本概念。如果你已经了解这些概念,可以跳过本节内容。不管怎样,本节内容将帮助你探索概率论的基础知识。
给定事件发生的概率(先验概率)是事件本身的有利情况数(s)与所有可能情况总数(n)的比率,前提是所有考虑的情况具有相等的概率:

让我们看一个简单的例子:
- 在掷骰子的过程中,掷出 3 的概率是多少?可能的情况数是 6,{1, 2, 3, 4, 5, 6},而符合条件的情况数是 1,即 {3}。所以,P(3) = 1/6 = 0.166 = 16.6%。
事件的概率 P(E) 总是介于 0 和 1 之间,可以如下表示:

极值的定义如下:
-
概率为 0 的事件称为不可能事件。假设我们有六个红球在一个袋子里,那么抽到黑球的概率是多少?可能的情况数是 6,符合条件的情况数是 0,因为袋子里没有黑球。因此,P(E) = 0/6 = 0。
-
概率为 1 的事件称为必然事件。假设我们有六个红球在一个袋子里,那么抽到红球的概率是多少?可能的情况数是 6,符合条件的情况数是 6,因为袋子里只有红球。因此,P(E) = 6/6 = 1。
到目前为止,我们已经讨论了事件发生的可能性,那么如果有多个可能的事件会发生呢?如果事件 A 的发生概率不依赖于事件 B 是否发生,且反之亦然,那么两个随机事件 A 和 B 是独立的。
举个例子,假设我们有两副 52 张的法国扑克牌。当从每副牌中抽出一张卡片时,以下两个事件是独立的:
-
E1: 从第一副牌中抽出的卡片是一个王牌。
-
E2: 从第二副牌中抽出的卡片是一张梅花牌。
每个事件都可以以相同的概率发生,且不受另一个事件发生的影响。
相反,如果事件 A 的发生概率依赖于事件 B 是否发生,那么事件 A 就依赖于事件 B。假设我们有一副 52 张的牌;如果连续抽出两张卡片且不将第一张卡片放回牌堆,以下两个事件是相关的:
-
E1: 第一张抽出的卡片是一个王牌。
-
E2: 第二张抽出的卡片是一个王牌。
精确地说,E2 的概率取决于 E1 是否发生,具体如下:
-
E1 的概率是 4/52。
-
如果第一张卡片是王牌,那么 E2 的概率是 3/51。
-
如果第一张卡片不是王牌,那么 E2 的概率是 4/51。
理解联合概率
现在,让我们来处理联合概率的情况,既包括独立事件,也包括相关事件。给定两个事件 A 和 B,如果这两个事件是独立的(即一个事件的发生不会影响另一个事件的概率),那么事件的联合概率等于 A 和 B 的概率的乘积:

让我们来看一个例子。我们有两副 52 张的扑克牌。当从每副牌中抽出一张卡片时,让我们考虑这两个独立的事件:
-
A: 从第一副牌中抽出的卡片是一个王牌。
-
B: 从第二副牌中抽出的卡片是一张梅花牌。
两个事件都发生的概率是多少?
-
P(A) = 4/52
-
P(B) = 13/52
-
P(A ∩ B) = 4/52 * 13/52 = 52 / (52 * 52) = 1/52
如果两个事件是相关的(即一个事件的发生会影响另一个事件的概率),那么可以应用相同的规则,只要 P(B|A)是事件 B 在事件 A 发生的条件下的概率。这个条件引入了条件概率,我们将深入探讨:

一个袋子里有两个白球和三个红球。从袋子里连续抽出两颗球,且不将第一颗抽出的球放回袋中。
让我们计算抽出的两颗球都是白球的概率:
-
第一颗球是白色的概率是 2/5。
-
如果第一颗球是白色的,那么第二颗球是白色的概率是 1/4。
两颗球都是白球的概率如下:
- P(两颗白球) = 2/5 * 1/4 = 2/20 = 1/10
理解条件概率
现在,是时候向你介绍条件概率的概念了。事件 B 发生的概率,在事件 A 发生的条件下计算,被称为条件概率,用符号 P(B | A)表示。它是通过以下公式计算的:

现在我们能够理解不同种类的概率了,让我们将它们应用到随机过程上。我们从最简单的概率类型开始,它写作以下形式:

这表示在第 n 步时观察系统处于状态 i 的概率。除了这些简单的概率之外,我们应该对计算涉及多个步骤的更复杂概率感兴趣。
例如,计算在第 n + 1 步处于状态 j 的概率,知道在第 n 步时它处于状态 i,可能是很有意思的(正如我们所看到的,这就是我们之前定义的条件概率):

这被称为从 i 到 j 在第 n 步的转移概率。通过条件概率定义,它可以重写如下:

因此,对于这个计算,知道先验概率和联合概率就足够了。要计算更复杂的表达式,需要知道通过以下公式给出的通用联合概率:

所有这些都源于整数集合 Z 中所有 i[0],...,i[n]的变化。从某种意义上讲,这些概率消耗了所有可能的信息:当所有联合(离散)密度已知时,随机过程在统计上是确定的,也就是说,已知所有多个离散变量(X[1], ..., X[n])的密度对所有 i[0],...,i[n] Z 的变化。计算这些联合概率通常是一个非常困难的问题。
在下一节中,我们将深入探讨马尔可夫过程背后的概念。
马尔可夫链简介
马尔可夫链是一个随机现象的数学模型,它随时间演变,以这样的方式:过去通过现在仅影响未来。时间可以是离散的(整数变量)、连续的(实数变量),或完全有序的整数。在本节中,我们将只考虑离散链。马尔可夫链由安德烈·安德烈耶维奇·马尔可夫(1856–1922)在 1906 年引入,这也是该名称的由来。
马尔可夫链是一种随机模型,表示可能的案例序列,其中每个案例发生的概率仅依赖于相对于前一个案例的状态。因此,马尔可夫链具有无记忆性特性。
让我们考虑一个由随机变量序列 X = X[0], ..., X[n]描述的随机过程,它可以在一个集合中取值,即 j[0], j[1], …, jn。假设我们分析的过程具有马尔科夫性质,即过程未来的演化只依赖于当前状态的值,而不依赖于过去的历史。在公式中,使用条件概率,我们将得到以下表达式:

如果所有的参数都是良好定义的条件概率,那么这个关系必须适用于所有参数。一个具有马尔科夫性质的离散时间随机过程 X 被称为马尔科夫链。如果转移概率满足以下条件,则该马尔科夫链是齐次的:

这与 n 无关,仅与 i 和 j 有关。当这种情况发生时,我们得到以下结果:

我们可以通过知道概率 p[ij] 和以下初始分布来计算所有的联合概率:

这个概率被称为过程在零时刻的分布。p[ij] 概率被称为转移概率,而 p[ij] 是从状态 i 到状态 j 在一个时间步长内发生转移的概率。
理解转移矩阵
研究齐次马尔科夫链在使用矩阵表示时变得特别简单且有效。特别是,前述命题所表达的公式变得更加易于阅读。由于这一点,马尔科夫链的结构可以通过以下转移矩阵完全表示:

转移概率矩阵的性质直接来源于构成它们的元素的特性。事实上,通过观察矩阵的元素是概率,它们必须在 0 和 1 之间。因此,这是一种正矩阵,其中每一行元素的和为 1。事实上,第 i 行的元素是链在时刻 t 处于状态 Si 时,下一步转移到 S1 或 S2,...或 Sn 的概率。这些转移是互斥的,并且涵盖了所有可能性。这样的矩阵(每行和为 1 的正矩阵)被称为随机矩阵。因此,我们需要将每个正行向量定义为随机矩阵,如下所示:

在这个向量中,元素的和取值为 1,如下式所示:

现在,我们将看到,在一维随机游走的情况下,这个矩阵采用了特定的形式。如以下图所示,在一维随机游走中,我们研究的是一个类点粒子的运动,它被限制只能沿着直线在两个允许的方向(右和左)上移动。在每次运动中,它以固定的概率 p 随机向右移动一步,或以概率 q 向左移动一步,使得 p+q=1。每一步的长度相等,并且与其他步独立:

假设随机变量 Z[n],其中 n = 1,2, ..,是独立的并且具有相同的分布。因此,粒子在时刻 n 的位置由以下公式给出:

这里,X[0] = 0。状态空间为 S = (0, ±1, ±2,…)。X[n]过程是一个马尔可夫链,因为,为了确定粒子下一个时刻处于某个位置的概率,我们只需要知道它在当前时刻的位置,即使我们知道它在当前时刻之前的所有时刻的位置。这一概念可以通过以下方程表达:

这里,Zn 变量是独立的。转移矩阵是一个行数有限且列数相等的矩阵,其中主对角线上是 0,主对角线上的上方是 p,下方是 q,其他地方都是 0,公式如下所示:

在这里,我们可以看到,这一概化极大地简化了当前的问题。
理解转移图
将马尔可夫链与转移矩阵的描述相对直观的替代方法是将马尔可夫链与一个有向图(转移图)关联。在这里,发生以下情况:
-
顶点通过 S1、S2、…、Sn 状态标记(或者简而言之,通过状态的索引 1、2、…、n 标记)。
-
如果且仅如果从 Si 到 Sj 的转移概率为正,则存在一个有向边将顶点 Si 连接到顶点 Sj(这个概率也作为边本身的标签)。
很明显,转移矩阵和转移图提供了关于相同马尔可夫链的相同信息。为了理解这种二重性,我们需要看一个简单的例子——考虑一个具有三个可能状态的马尔可夫链,即 1、2 和 3,及其以下的转移矩阵:

新引入的马尔可夫链的转移图可以在以下图中看到。我们可以识别出三个可能的状态:1、2 和 3。两状态边界包含转移概率 p[ij]。当两个状态之间没有边界时,表示转移的概率为零:

在上述图示中,我们可以看到从一个状态出来的箭头总和恰好为 1,就像转移矩阵中每一行的值必须加起来恰好为 1 —— 这代表了概率分布。通过比较转移矩阵和转移图,我们可以理解两者之间的对偶性。像往常一样,图示要比文字更具解释性。
在接下来的部分,我们将通过解决一个与马尔可夫链相关的预测问题来实践我们学到的内容。
马尔可夫链应用 —— 天气预测
为了应用我们迄今为止学到的内容,我们将查看一个基于马尔可夫链的天气预测模型。为了简化这个模型,我们假设只有三种状态——雨天、阴天和晴天。我们还假设我们已经进行了一些计算,并发现明天的天气某种程度上依赖于今天的天气,依据以下转移矩阵:

每一行必须包含非负数,且它们的和必须等于 1。回想一下,这个矩阵包含了条件概率,形式为 P (A | B),即在给定 B 的情况下 A 发生的概率。因此,这个矩阵包含了以下条件概率:

这里,我们有以下几个属性:
-
Ra:雨天
-
Cl:阴天
-
Su:晴天
两天之间的天气状况不一定相关,因此该过程是马尔可夫过程。
此时,以下问题浮现在脑海中:
-
如果今天是晴天,我们如何计算未来几天变成雨天的概率?
-
在若干天之后,晴天和雨天的比例将会是多少?
这两个问题,以及可能浮现的其他问题,都可以通过使我们能够使用马尔可夫链的工具来解答。
以下是允许我们在特定初始条件下交替出现晴天、阴天和雨天的 R 代码:
library(markovchain)
set.seed(1)
States <- c("Rainy","Cloudy","Sunny")
TransMat <- matrix(c(0.30,0.50,0.20,0.25,0.4,0.35,0.1,0.2,0.70),
nrow = 3, byrow = TRUE,dimnames = list(States,States))
MarkovChainModel <- new("markovchain",transitionMatrix=TransMat, states=States,
byrow = TRUE, name="MarkovChainModel")
MarkovChainModel
states(MarkovChainModel)
dim(MarkovChainModel)
str(MarkovChainModel)
MarkovChainModel@transitionMatrix
library(diagram)
plot(MarkovChainModel,package="diagram")
transitionProbability(MarkovChainModel, "Sunny", "Rainy")
StartState<-c(0,0,1)
After3Days <- StartState * (MarkovChainModel ^ 3)
print (round(After3Days, 3))
After1Week <- StartState * (MarkovChainModel ^ 7)
print (round(After1Week, 3))
steadyStates(MarkovChainModel)
YearWeatherState <- rmarkovchain(n = 365, object = MarkovChainModel, t0 = "Sunny")
YearWeatherState[1:40]
让我们逐行分析这段代码:
- 第一行加载了库:
library(markovchain)
请记住,如果需要安装 R 初始分发版中没有的库,必须使用install.packages()函数。这个函数只需使用一次,而不是每次运行代码时都使用。
- 例如,要安装
markovchain包,我们应该写如下代码:
install.packages("markovchain")
这个函数从 CRAN 类的仓库或本地文件下载并安装软件包。相反,必须在每次在新的 R 会话中执行脚本时使用加载命令。
导入 markovchain 包
markovchain包包含我们可以用来创建和管理离散时间马尔可夫链的函数和 S4 方法。除此之外,还提供了可用于执行统计(拟合和绘制随机变量)和概率(分析其结构属性)分析的函数。从官方文档中提取的markovchain包的简要描述如下表所示:
| 版本 | 0.6.9.14 |
|---|---|
| 日期 | 2019-01-20 |
| 维护者 | Giorgio Alfredo Spedicato |
| 许可证 | GPL-2 |
| 作者 | Giorgio Alfredo Spedicato, Tae Seung Kang, Sai Bhargav Yalamanchi, Mildenberger Thoralf, Deepak Yadav, Ignacio Cordón, Vandit Jain, Toni Giorgino |
我们将在本书的多个章节中使用此包,以演示它所提供的功能的实用性。让我们开始吧:
- 让我们继续分析代码:
set.seed(1)
set.seed()命令设置 R 随机数生成器的种子。每当我们希望使示例可重现时,这都是必要的。当使用set.seed()时,算法中使用的随机数将始终相同,因此随后的算法重现将提供相同的结果。每个种子值将对应于为给定随机数生成器生成的值序列。
- 在接下来的行中,我们定义天气条件的状态:
States <- c("Rainy","Cloudy","Sunny")
如图所示,仅提供了三种状态:Rainy、Cloudy和Sunny。此时,我们需要定义天气条件的可能转移。
- 接下来,我们根据本节开始时的内容来定义转移矩阵:
TransMat <- matrix(c(0.30,0.50,0.20,0.25,0.4,0.35,0.1,0.2,0.70),nrow = 3, byrow = TRUE,dimnames = list(States,States))
记住,这个矩阵包含的是条件概率类型,表达式为P(A | B),即给定B时A发生的概率。正如我们之前提到的,这个矩阵的行加起来等于 1。
- 现在,我们可以创建
markovchain对象:
MarkovChainModel <- new("markovchain",transitionMatrix=TransMat, states=States, byrow = TRUE, name="MarkovChainModel")
markovchain类旨在处理同质马尔可夫链过程。以下槽将被传递:
-
transitionMatrix:包含转移矩阵概率的方阵。 -
states:状态的名称。它必须与转移矩阵的列名和行名相同。这是一个字符向量,列出了定义了转移概率的状态。 -
byrow:二进制标志。一个逻辑元素,指示转移概率是按行显示还是按列显示。 -
name:可选的字符元素,用于为离散时间马尔可夫链命名。
为了提供我们刚刚创建的模型的总结,请使用以下命令:
MarkovChainModel
返回以下结果:
MarkovChainModel
A 3 - dimensional discrete Markov Chain defined by the following states:
Rainy, Cloudy, Sunny
The transition matrix (by rows) is defined as follows:
Rainy Cloudy Sunny
Rainy 0.30 0.5 0.20
Cloudy 0.25 0.4 0.35
Sunny 0.10 0.2 0.70
如您所见,物体的维度、状态和转移矩阵已被打印出来。为了单独获取这些信息,我们可以使用一些与markovchain对象相关的方法:
- 例如,要获取
markovchain对象的状态,我们可以使用states方法,如下所示:
states(MarkovChainModel)
返回以下结果:
[1] "Rainy" "Cloudy" "Sunny"
- 要获取
markovchain对象的维度,我们可以使用dim方法,如下所示:
dim(MarkovChainModel)
返回以下结果:
[1] 3
- 要查看我们创建的对象中包含哪些元素,我们可以使用
str()函数,它显示了 R 对象内部结构的紧凑视图:
str(MarkovChainModel)
打印以下结果:
Formal class 'markovchain' [package "markovchain"] with 4 slots
..@ states : chr [1:3] "Rainy" "Cloudy" "Sunny"
..@ byrow : logi TRUE
..@ transitionMatrix: num [1:3, 1:3] 0.3 0.25 0.1 0.5 0.4 0.2 0.2 0.35 0.7
.. ..- attr(*, "dimnames")=List of 2
.. .. ..$ : chr [1:3] "Rainy" "Cloudy" "Sunny"
.. .. ..$ : chr [1:3] "Rainy" "Cloudy" "Sunny"
..@ name : chr "MarkovChainModel"
如我们所见,列出了四个槽:states、byrow、transitionMatrix和name。要检索每个槽中包含的元素,我们可以使用对象的名称(MarkovChainModel),后跟槽的名称,中间用@符号分隔。
- 例如,要打印转移矩阵,我们将写如下代码:
MarkovChainModel@transitionMatrix
返回以下结果:
Rainy Cloudy Sunny
Rainy 0.30 0.5 0.20
Cloudy 0.25 0.4 0.35
Sunny 0.10 0.2 0.70
正如我们在转移图部分所提到的,通过转移矩阵描述马尔可夫链的一个非常直观的替代方法是将马尔可夫链与有向图(转移图)关联。
导入diagram包
要绘制转移图,我们可以使用diagram包。这个包包含了几个用于可视化简单图(网络)和绘制流程图的函数。
我们可以在下面的表格中看到从官方文档提取的diagram包的简短描述:
| 版本 | 1.6.4 |
|---|---|
| 日期 | 2017-08-16 |
| 维护者 | Karline Soetaert |
| 许可证 | GPL-2 |
| 作者 | Karline Soetaert |
现在,让我们学习如何使用包中提供的函数来创建图:
- 让我们从导入库开始:
library(diagram)
- 现在,我们可以创建名为
diagram的markovchain对象:
plot(MarkovChainModel,package="diagram")
打印以下图:

在前面的图中,我们可以看到,所有从状态中流出的箭头的总和始终等于 1,就像转移矩阵中每一行的值必须加起来等于 1 一样。这代表了概率分布。
获取转移概率
我们还可以从新开发的模型中提取转移概率,它表示从一个状态到另一个状态的概率。请记住,如果从一个状态到另一个状态的转移概率不依赖于时间索引,则称一个马尔可夫链是时间齐次的。要获取这些信息,我们将使用transitionProbability()函数,它可以帮助我们获取从初始状态到后续状态的转移概率。让我们开始吧:
- 使用以下命令获取此信息:
transitionProbability(MarkovChainModel, "Sunny", "Rainy")
返回以下结果:
[1] 0.1
我们可以通过分析转移矩阵和转移图来验证这个结果。在转移矩阵中,从Sunny状态到Rainy状态的转移由元素p31给出,其值为 0.1。同样,在转移图中,从Sunny状态到Rainy状态的分支的值也为 0.1。
在正确设置好我们的基于马尔科夫链的模型后,接下来就是用它来做预测了。但首先,我们需要设定初始状态。假设我们从晴天(Sunny)状态开始。
- 基于包含我们模型三种状态的向量,这个条件用向量 (0,0,1) 来表示。我们可以这样设置这个值:
StartState<-c(0,0,1)
例如,为了计算 3 天后的天气状态,我们可以利用马尔科夫链的一个特性。如果 X[n] 是一个齐次马尔科夫链,具有转移概率 p[ij] 和初始分布 p^([0]),那么以下公式成立:

这个公式在向量形式下变成了如下:

在前面的公式中,p^([n]) 和 p^([0]) 是行向量,而 p^([0]) x P^([n]) 表示行向量与矩阵之间的乘积(行与列的乘积)。
- 让我们用我们的模型来写出这个乘积:
Pred3Days <- StartState * (MarkovChainModel ^ 3)
print (round(Pred3Days, 3))
返回以下结果:
Rainy Cloudy Sunny
[1,] 0.17 0.299 0.53
- 这样,我们得到了一个三天的预测。为了得到一周的预测,我们将写下如下代码:
Pred1Week <- StartState * (MarkovChainModel ^ 7)
print (round(Pred1Week, 3))
返回以下结果:
Rainy Cloudy Sunny
[1,] 0.184 0.319 0.497
我们从已开发的模型中还可以得到一个静态分布。一个马尔科夫链的静态分布是一个向量π,满足π⋅P = π(换句话说,π是由矩阵 P 保持不变的)。π是一个行向量,其元素是概率,总和为 1。这个概率分布在马尔科夫链随着时间演变时保持不变。
markovchain 包含一个特定函数,叫做 steadyStates() 函数,用来获取马尔科夫链的静态分布。我们来调用它:
steadyStates(MarkovChainModel)
返回以下结果:
Rainy Cloudy Sunny
[1,] 0.1848739 0.3193277 0.4957983
最后,让我们学习如何从一个特定状态开始,逐日生成一整年的天气状态预测。为此,我们可以使用 rmarkovchain() 函数,它返回来自齐次或非齐次马尔科夫链的状态序列。让我们这样做:
YearWeatherState <- rmarkovchain(n = 365, object = MarkovChainModel, t0 = "Sunny")
传入以下参数:
-
n: 样本大小 -
object: 一个markovchain或markovchainList对象 -
t0: 初始状态
此时,我们可以提取明年每一天的预测结果。让我们打印接下来的 40 天的预测结果:
YearWeatherState[1:40]
返回以下结果:
[1] "Sunny" "Sunny" "Sunny" "Rainy" "Cloudy" "Rainy" "Sunny" "Sunny" "Sunny" "Sunny" "Sunny"
[12] "Sunny" "Sunny" "Sunny" "Cloudy" "Sunny" "Cloudy" "Rainy" "Cloudy" "Rainy" "Sunny" "Sunny"
[23] "Sunny" "Sunny" "Sunny" "Sunny" "Sunny" "Sunny" "Cloudy" "Cloudy" "Sunny" "Sunny" "Sunny"
[34] "Sunny" "Cloudy" "Sunny" "Cloudy" "Cloudy" "Sunny" "Sunny"
预测序列是明确定义的,从初始状态开始。
在下一部分,我们将开发使用奖励来扩展马尔科夫链特性的模型。
马尔科夫奖励模型
到目前为止,我们处理了马尔科夫过程、没有记忆的随机过程、一系列满足马尔科夫性质的随机状态等等。这个过程是通过状态空间 S 和转移函数 P 来定义的,后者决定了其动态。在这些模型中,没有与特定状态相关联的值能够帮助我们达成目标。
如果我们为每个状态添加一个奖励率,我们就得到了一个马尔可夫奖励模型,它表示一个扩展了马尔可夫链或连续时间马尔可夫链特性的随机过程。随时间累积的奖励(R)被记录在一个额外的变量中。这些概念在第二章,强化学习的基本构建块中介绍。现在,让我们尝试将这些概念应用于森林管理的实际案例。
微型森林管理问题
为了理解这些新修订的概念,我们将使用MDPToolbox包中的一个示例(mdp_example_forest)。该示例处理森林经营管理问题,并有两个主要目标:
-
第一个目标是保持一片古老的森林供野生动物栖息。
-
第二个目标是通过出售砍伐的木材赚取收入。
为了实现这些目标,可以选择两种行动:等待或砍伐。每 20 年周期决定一次行动,并在该周期开始时应用。
根据三个树木年龄组定义了三种状态:
-
状态 1:年龄组 0-20 岁
-
状态 2:年龄组 21-40 岁
-
状态 3:年龄组 40 年以上
状态 3 对应于最老的年龄组。在一个周期 t 结束时,如果状态是 s 并选择了等待动作,则下一个周期的状态将由以下两个值中的较小者给出(s + 1, 3),如果没有发生火灾。这是因为,如果没有发生火灾,树木会衰老,但永远不能超过状态 3。如果发生火灾,应用动作后可能会烧毁森林,使得整个群体回到较年轻的年龄组(状态 1)。
假设 p = 0.1 是某一时期内发生野火的概率。问题在于如何从长远来看管理这个过程,以最大化奖励。这个问题可以视为一个马尔可夫决策过程(MDP)。
首先,我们定义过渡矩阵 P(s,s',a)。记住,它告诉我们从一个状态到另一个状态的概率。由于可用的行动是(等待,砍伐),我们将定义两个过渡矩阵。如果我们用 p 表示火灾的概率,那么我们将得到与选择行动 1(等待)相关的以下过渡矩阵:

这是因为,如果我们处于状态 1,那么我们有概率 p 保持在该状态(如果发生火灾),而剩余的 1-p 概率转移到下一个状态(如果没有发生火灾)。虽然转移到状态 3 的概率为 0,不能直接从状态 1 转移到状态 3。另一方面,如果我们处于状态 2,我们有概率 p 转移到状态 1(如果发生火灾),而剩余的 1-p 概率转移到下一个状态,即状态 3(如果没有发生火灾)。
在这里,留在状态 2 的概率为 0。最后,如果我们处于状态 3,我们将有概率 p 进入状态 1(如果发生火灾),其余的 1-p 概率将保持在状态 3(如果没有发生火灾),因为这是时间上最后可能发生的情况。进入状态 2 的概率为 0。
现在,让我们定义在选择行动 2(砍伐)情况下的转换矩阵:

在这种情况下,当选择砍伐木材时,它的含义更加直观。这里,转换在三种情况下都以单位概率导致进入状态 1。
现在,我们可以定义奖励 R(s, a)的两个向量:

如果选择的行动是等待森林生长,那么前两个状态的奖励将是 0,而状态 3 的奖励将是最大值。在这种情况下,我们选择了 4 作为奖励,表示系统默认提供的值。如果选择的行动是砍伐木材,则我们将使用以下公式:

在这里,如果选择的行动是砍伐木材,那么状态 1 的奖励为 0,状态 2 的奖励为 1,状态 3 的奖励为 2。
我们的目标是计算一个策略,使我们能够根据刚才开发的设置获得最大回报:
- 让我们看看实现这个的代码:
library(MDPtoolbox)
data = mdp_example_forest()
print(data$P[,,1])
print(data$P[,,2])
print(data$R[,1])
print(data$R[,2])
mdp_check(data$P, data$R)
solver=mdp_policy_iteration(P=data$P, R=data$R, discount = 0.95)
print(solver$V)
print(solver$policy)
print(solver$iter)
print(solver$time)
- 让我们逐行分析代码,以理解每个命令的含义。首先,我们导入库:
library(MDPtoolbox)
MDPtoolbox包提供了与离散时间马尔可夫决策过程求解相关的函数,也就是有限时间、值迭代、策略迭代、线性规划算法的一些变种,以及一些与强化学习相关的函数。我们可以从官方文档中提取出以下表格,简要描述MDPtoolbox包:
| 版本 | 4.0.3 |
|---|---|
| 日期 | 2017-03-02 |
| 维护者 | Guillaume Chapron |
| 许可证 | BSD_3_clause + 文件 LICENSE |
| 作者 | Iadine Chades, Guillaume Chapron, Marie-Josee Cros, Frederick Garcia, Regis Sabbadin |
正如我们预期的那样,首先需要做的是定义转换函数的矩阵 P 和奖励函数的矩阵 R。首先,我们将使用包中提供的示例数据:
- 为此,只需调用示例,如下所示:
data = mdp_example_forest()
- 我们创建的对象(在这种情况下是一个列表)包含了转换矩阵 P 和奖励向量。让我们看看它的内容:
str(data)
以下是返回的结果:
List of 2
$ P: num [1:3, 1:3, 1:2] 0.1 0.1 0.1 0.9 0 0 0 0.9 0.9 1 ...
$ R: num [1:3, 1:2] 0 0 4 0 1 2
..- attr(*, "dimnames")=List of 2
.. ..$ : NULL
.. ..$ : chr [1:2] "R1" "R2"
- 为了提取转换矩阵,我们可以写出以下内容:
print(data$P[,,1])
print(data$P[,,2])
这样做后,我们可以看到两个转换矩阵:
> print(data$P[,,1])
[,1] [,2] [,3]
[1,] 0.1 0.9 0.0
[2,] 0.1 0.0 0.9
[3,] 0.1 0.0 0.9
> print(data$P[,,2])
[,1] [,2] [,3]
[1,] 1 0 0
[2,] 1 0 0
[3,] 1 0 0
- 默认情况下,
p = 0.1。这里,p 表示发生火灾的概率。使用这个值,我们可以确认转换矩阵的形状。接下来,我们来看奖励向量:
print(data$R[,1])
print(data$R[,2])
以下是返回的结果:
> print(data$R[,1])
[1] 0 0 4
> print(data$R[,2])
[1] 0 1 2
- 在开发模型之前,需要验证 P 和 R 是否满足问题属于 MDP 类型所必需的标准。为此,我们将使用
mdp_check()函数:
mdp_check(data$P, data$R)
该函数检查由转移概率数组 (P) 和奖励数组 (R) 定义的 MDP 是否有效。如果 P 和 R 正确,函数将返回空的错误信息。如果它们不正确,函数将返回描述问题的错误信息。返回的结果如下:
> mdp_check(data$P, data$R)
[1] ""
在这里,我们可以看到问题已经设定好了。现在,我们可以开始搜索最佳的森林管理策略。
策略迭代算法
正如我们在第二章中提到的,强化学习的基础构建模块,策略迭代是一个动态规划算法,它使用价值函数来模拟每对动作-状态的预期回报。我们将把这个方法应用到当前的问题中。
此时,我们将尝试使用 mdp_policy_iteration() 函数来解决当前的问题:
solver=mdp_policy_iteration(P=data$P, R=data$R, discount = 0.95)
该函数通过策略迭代算法解决了折扣 MDP 问题。正如我们在第二章中提到的,强化学习的基础构建模块,策略迭代是一个动态规划算法,它使用价值函数来模拟每对动作-状态的预期回报。这些技术通过使用即时奖励和下一个状态的(折扣)价值来更新价值函数,这一过程称为自举(bootstrapping)。因此,它们意味着将 Q (s, a) 存储在表格中或使用近似函数技术。
从初始的 P0 策略开始,策略的迭代在以下两个阶段之间交替进行:
-
策略评估:给定当前的策略 P,估计动作-价值函数 QP。 -
策略改进:如果我们基于 QP 计算出一个更好的策略 P',则将 P' 设置为新的策略,并返回上一步。
当 QP 值函数可以准确计算每个动作-状态对时,采用贪婪策略改进的策略迭代会导致收敛,并返回最优策略。实际上,反复执行这两个过程使得整个过程向最优解收敛。
传递以下参数:
-
P:转移概率数组。P 可以是一个三维数组 [S,S,A] 或一个列表 [[A]],每个元素包含一个稀疏矩阵 [S,S]。
-
R:奖励数组。R 可以是一个三维数组 [S,S,A] 或一个列表 [[A]],每个元素包含一个稀疏矩阵 [S,S] 或一个可能是稀疏的二维矩阵 [S,A]。
-
折扣:折扣因子。折扣因子是一个实数,属于 ]0; 1[ 区间。
策略迭代算法通过评估当前策略来逐步改进策略。当两个连续的策略相同时,或已执行指定次数(max_iter)的迭代时,迭代停止。
返回以下结果:
-
V: 最优值函数。V 是一个长度为 S 的向量。
-
policy:最优策略。策略是一个长度为 S 的向量。每个元素是一个整数,表示最大化值函数的行动。
-
iter:迭代次数。
-
cpu_time:运行程序所用的 CPU 时间。
现在模型已准备就绪,我们只需通过检查获得的策略来评估结果:
- 让我们学习如何从模型中提取这些结果:
print(solver$V)
我们首先可视化的元素是最优值函数。
记住,值函数表示一个状态对代理来说有多好。它等于代理从状态 s 预期的总奖励。值函数取决于代理为执行的动作选择的策略。
返回以下结果:
[1] 58.482 61.902 65.902
- 让我们看看模型返回的策略:
print(solver$policy)
回忆一下,策略定义了在给定时间内学习代理的行为。它将环境中检测到的状态映射到在这些状态下要采取的行动。这相当于心理学中所说的刺激-反应规则或联结。策略是强化学习代理的核心部分,因为它单独足以决定行为。
返回以下结果:
[1] 1 1 1
在这里,最优策略是在三个状态下都不砍伐森林。这是因为火灾发生的概率较低,导致等待成为最佳行动。这样,森林有时间生长,我们可以实现两个目标:保护老森林以供野生动物栖息,并通过销售砍伐的木材赚钱。
- 现在,我们可以看看模型收敛所需的迭代次数:
print(solver$iter)
打印出以下结果:
[1] 2
- 如我们所见,只需两次迭代就能得到结果。最后,我们想看看处理程序所需的 CPU 时间:
print(solver$time)
打印出以下结果:
Time difference of 0.4140239 secs
这个第一个例子让我们理解了从一个已恰当表述的问题中得出最优策略是多么简单。现在,让我们看看当我们修改系统的初始条件时会发生什么。
新的状态转移矩阵
到目前为止,我们已经看到,当火灾发生的概率较低时,最优策略建议我们等待而不是砍伐森林。那么如果火灾发生的概率更高会怎样呢?在这里,我们只需通过更改概率值 p 来修改问题设置。让我们开始吧:
- 以下代码允许我们做到这一点:
library(MDPtoolbox)
data = mdp_example_forest(3,4,2,0.8)
print(data$P[,,1])
print(data$P[,,2])
print(data$R[,1])
print(data$R[,2])
mdp_check(data$P, data$R)
solver=mdp_policy_iteration(P=data$P, R=data$R, discount = 0.95)
print(solver$V)
print(solver$policy)
print(solver$iter)
print(solver$time)
- 让我们逐行分析代码,重点关注对初始代码所做的更改:
data = mdp_example_forest(3,4,2,0.8)
这是修改后的代码。我们没有使用问题提供的值;相反,我们设置了新的值。请记住,mdp_example_forest()函数的语法如下:
mdp_example_forest(S, r1, r2, p)
传递以下参数:
-
S(可选):状态的数量。S 是大于 0 的整数。默认情况下,S 设置为 3。
-
r1(可选):当森林处于最老状态且执行等待操作时的奖励。r1 是一个大于 0 的实数。默认情况下,r1 设置为 4。
-
r2(可选):当森林处于最老状态且执行砍伐操作时的奖励。r2 是一个大于 0 的实数。默认情况下,r2 设置为 2。
-
p(可选):火灾发生的概率。p 是一个实数,范围在 ]0, 1[ 之间。默认情况下,p 设置为 0.1。
在这里,我们确认了三个状态,合法地修改了奖励,并增加了火灾发生的概率,将其从初始值 0.1 提高到新的值 0.8。
- 让我们看看通过进行此更改我们能得到什么结果:
print(data$P[,,1])
print(data$P[,,2])
在这里,我们可以看到两个转移矩阵:
> print(data$P[,,1])
[,1] [,2] [,3]
[1,] 0.8 0.2 0.0
[2,] 0.8 0.0 0.2
[3,] 0.8 0.0 0.2
> print(data$P[,,2])
[,1] [,2] [,3]
[1,] 1 0 0
[2,] 1 0 0
[3,] 1 0 0
- 让我们继续查看奖励数组:
print(data$R[,1])
print(data$R[,2])
以下结果被返回:
> print(data$R[,1])
[1] 0 0 3
> print(data$R[,2])
[1] 0 1 2
- 在开发模型之前,有必要验证 P 和 R 是否满足问题的标准,以确保它们属于 MDP 类型。为此,我们将使用
mdp_check()函数:
mdp_check(data$P, data$R)
以下结果被返回:
> mdp_check(data$P, data$R)
[1] ""
- 现在问题已经设置完毕,我们可以尝试使用
mdp_policy_iteration函数来解决问题:
solver=mdp_policy_iteration(P=data$P, R=data$R, discount = 0.95)
让我们从模型中提取结果:
print(solver$V)
我们已经可视化的第一个元素是最优价值函数。
回忆一下,价值函数表示一个状态对代理的价值。它等于代理从状态 s 中预期的总奖励。价值函数依赖于代理为执行动作选择的策略。
以下结果被返回:
[1] 3.193277 4.033613 6.699865
- 通过将其与我们在初始模型中获得的结果进行比较,我们可以看到总奖励明显减少了。让我们看看模型返回的策略:
print(solver$policy)
以下结果被返回:
[1] 1 2 1
在这里,我们可以看到最优策略发生了变化。在这种情况下,在状态 1 和状态 3 中,建议选择等待操作。然而,在状态 2 中,建议进行砍伐,以避免失去至今获得的木材。
- 现在,让我们看看模型需要经历多少次迭代才能收敛:
print(solver$iter)
以下结果被打印出来:
[1] 1
- 最后,我们可以看到处理程序所需的 CPU 时间:
print(solver$time)
以下结果被打印出来:
Time difference of 0.009001017 secs
这个例子展示了如何修改问题的参数。在这里,我们可以看到,通过增加火灾发生的概率,模型开发出的最优策略发生了变化。
总结
在这一章中,我们讨论了随机过程及其应用。随机过程理论研究的是根据概率法则随时间演变的系统。由于这一点,我们关注的是与之相关的概率计算。因此,我们学习了概率的基本概念。我们定义了先验概率、联合概率和条件概率,并通过示例展示了如何计算它们。
接着,我们被介绍了马尔科夫链。马尔科夫链是一种随机现象的数学模型,随着时间的推移以某种方式演变,使得过去通过现在影响未来。换句话说,它表示了一系列可能事件的随机描述。每个事件的概率取决于前一个事件所达到的状态。在这里,我们学习了如何定义和读取转移矩阵以及转移图。我们使用马尔科夫链预测了连续 365 天的天气状况。最后,我们看到了如何使用MDPtoolbox包来计算管理一个小森林的最优策略。
在下一章,我们将探讨多臂赌博机模型的基本概念。我们将发现可以使用的不同技术以及行动值实现的含义。我们将学习如何通过上下文方法来解决问题,并学习如何实现异步演员-评论家代理。
第四章:多臂老丨虎丨机模型
多臂老丨虎丨机问题是一个经典的强化学习问题,体现了探索与利用的困境。当我们只能依赖有限的资源来做出选择时,采用一种方法来确定哪些竞争选择可以最大化预期利润变得至关重要。多臂老丨虎丨机这个名字源于一个赌徒在一排老丨虎丨机中挣扎,他必须决定是继续使用当前的机器,还是尝试不同的机器。
本章将概述多臂老丨虎丨机模型的基本概念,探索可用于解决此问题的不同技术,并深入了解行动-价值实现的含义。接着,我们将学习如何使用情境方法来解决这个问题,如何实现异步演员-评论家代理,以及如何在 R 中实现多臂老丨虎丨机问题。
本章将涵盖以下主题:
-
多臂老丨虎丨机模型
-
多臂老丨虎丨机应用
-
行动-价值实现
-
理解问题解决技术
-
实现情境方法
-
理解异步演员-评论家代理
-
使用 MAB 模型进行在线广告
多臂老丨虎丨机模型
学习理论中的一个常见问题是,在不知道每个选项能提供何种收益的情况下,如何在一组选项中识别最佳选项,同时最小化选择的成本。
多臂老丨虎丨机(MAB)问题得名于决策理论中遇到的一个著名问题。一个赌徒必须从他面前的多台老丨虎丨机中选择一台进行游戏。游戏后,他将获得关于某些机器奖励的某种程度的知识,但他对其他机器一无所知,因此他将被迫在部分已知的机器和完全未知的机器之间做出选择。
这个问题非常适合模拟已知机会的利用与未知机会的探索之间的妥协,也可以在高度无知和不确定的情况下测试策略。
从技术术语上讲,每台老丨虎丨机都被建模为一个概率分布,具有一个平均值和一个标准差。这两个参数可以随时间变化,可能是相互依赖的,也可能是独立变化的,例如,模拟随时间的演变或竞争场景的动态,或者响应一个或多个玩家的选择,以模拟竞争或对情境的影响。
概率分布显然对玩家来说是未知的,但随着每台老丨虎丨机奖励的获取,玩家可以逐渐学习这些分布。
MAB 问题是由 H. Robbins 提出的,用来模拟在环境未知的情况下的决策制定。这些问题在以下论文中有所讨论:实验设计的某些方面,Robbins, H. (1952),《美国数学学会公报》,55,527–535。
在每个时间步长中,只有一个杠杆被操作,并且观察到一个随机奖励,其中每次操作杠杆 k 会生成独立同分布的样本,这些样本来自某个未知的分布。目标是最大化在定义的时间区间内获得的所有奖励之和。
每个指定应当选择哪个杠杆的算法,基于已获得的奖励历史,代表了我们的策略。通常用来衡量后者表现的度量是遗憾,它是衡量我们所选策略与最佳策略(已知收益的分布平均值)相比,预期损失的程度。
由于这些分布对于我们的策略来说是未知的,因此需要通过多次操作来学习它们,但同时,我们也希望通过选择已经被评定为好的机器来最大化奖励。这两个冲突的目标——探索未知和利用已知——是广泛的机器学习问题中存在的一个基本权衡。
数学模型
K 臂赌博机问题由随机变量 X[i, n] 定义,其中 1 ≤ i ≤ K 和 n ≥ 1(n 是游戏次数),其中 i 是识别老丨虎丨机杠杆的索引。通过操作杠杆 i,可以获得奖励 X[i, 1], X[i, 2], · · ·,这些奖励是独立且同分布的,遵循一个未知的概率分布,并具有未知的期望 µi(期望值)。此外,不同杠杆之间的奖励保持独立,即,对于每一个 1 ≤ i < j ≤ K 和所有的 s, t ≥ 1,X[i, s] 和 X[j, t] 是独立的(且通常不是同分布的)。这个问题在形式上等价于一个单状态的马尔可夫决策过程。
分配策略是一种算法,它根据之前投注的序列和获得的奖励来选择下一个要操作的杠杆。正如我们之前提到的,遗憾的概念被用来衡量模型的表现,衡量的是跟随所选策略获得的收益与最佳策略之间的累计损失。
设 T[i] (n) 为策略在前 n 次游戏中操作杠杆 i 的次数。这里,策略在 n 次游戏后的遗憾定义如下公式:

在这里,我们有以下内容:
-
µ = max[i=1;:::k] µ[i]*
-
Ε(Tk) 是关于策略将在多少次中选择机器 k 的期望值。
目标是最小化这种遗憾,或者等价地,最大化 n 次操作后获得的奖励总和。事实上,我们可以将遗憾解释为最大可能收益与实际收益之间的差异(最大可能收益是通过定义中每次拉动最佳杆获得的µ*奖励)。
在下一部分,你将看到可以通过这项技术解决的实际应用示例。
多臂老丨虎丨机应用
到目前为止,我们已经从数学角度看到了如何解决 MAB 问题。那么,哪些实际应用可以用这种方式建模呢?我们将在以下子章节中探讨一些示例。
在线广告
在在线广告中,MAB 解决方案使用机器学习算法动态地将广告分配给表现良好的网站页面,同时避免显示表现较差的广告。从理论上讲,MAB 应当能更快地得出结果,因为不需要等待单一获胜的变体。
新闻分配系统
MAB 的另一个应用涉及新闻网站:当一个新用户访问网站时,他们必须从一系列文章中选择一个新闻项目,每次用户点击文章时,网站都会收到奖励。由于网站的目标是最大化收入,它希望展示最有可能被点击的内容。显然,这个选择依赖于用户的特征。问题在于,我们不知道一篇文章被点击的概率,这是我们希望学习的参数。我们可以清楚地看到,前述情境中展示的探索与开发的困境可以被建模为 MAB 问题。
健康护理
在这一领域有多种治疗方法可供选择。管理者需要在最小化患者损失的同时决定使用哪种治疗方法。这些治疗方法是实验性的,意味着治疗的效果需要通过对患者的实施来学习。上述问题可以被建模为一个 MAB 问题,其中治疗方法如同“臂”一样,治疗效率也需要被学习。管理者可以通过探索不同的治疗方法来了解它们的成功率(效率),或者选择利用至今为止成功率最高的治疗方法。
人员招聘
在新员工选拔中,应用这种方法论代表了一种强大的工具,雇主或服务需求方可以利用它按时且经济地完成任务。雇主的目标是最大化完成的任务数量。可用的员工在这种情况下充当“武器”,因为他们的特质对雇主来说是未知的。因此,这个问题可以被看作是一个 MAB 问题,雇主可以通过探索员工的特质来了解他们,或者选择利用迄今为止已识别的最佳员工。
财务投资组合的选择
最优投资组合的选择是一个典型的决策问题,因此它的解决方案包括以下要素:识别一组备选方案,使用选择标准对不同的可能性进行排序,以及问题的解决。为了优化金融投资组合,我们首先要衡量可用产品的收益和风险。风险与回报变量可以看作是同一枚硬币的两面,因为一定程度的风险将对应于一定的回报。
收益可以定义为与所使用的资本相关的投资结果的总和,而风险的概念可以通过与特定金融工具相关的回报波动度来表示。这个问题可以被建模为一个多臂老丨虎丨机(MAB)问题,其中金融产品作为“臂”,产品表现作为结果。
在下一节中,我们将学习如何估计动作价值函数,以实现多臂老丨虎丨机算法。
动作价值实现
强化学习问题的一般解决方案是通过学习过程估计一个价值函数。这个函数必须能够通过奖励的总和来评估一个特定策略的便利性或其他方面。首先,我们将定义状态价值函数。
状态价值函数
价值函数表示一个状态对智能体来说有多好。它等于从状态 s 开始,智能体所期望的总奖励。价值函数依赖于智能体选择要执行的动作的策略。
策略π将概率π(s, a)与状态-动作对(s, a)关联,从而返回在状态 s 下执行动作 a 的概率。基于此,我们可以定义一个价值函数 Vπ(s),它是根据策略π从状态 s 开始的总奖励 R[t]的期望值:

这里,序列 rt 是在状态 s 下按照策略π生成的。换句话说,训练模式的示例必须指导学习过程朝向评估最优策略π*,如下所示:

假设δ(s, a)是由状态-动作对(s, a)生成的新状态的函数,我们可以执行前瞻搜索,从状态 s 开始选择最佳动作,因为我们可以表达以下内容:

这里,r(s, a)表示在状态 s 下执行动作 a 所获得的奖励。只有在已知这些函数的情况下,这个解才是可接受的,如下方方程所示:


然而,这种情况并不总是被遵守。
现在我们理解了状态价值函数的作用,我们可以继续定义动作价值函数。
动作价值函数
当这种情况没有发生时,需要定义一个类似于 Vπ ∗的新函数:

如果智能体能够估计函数 Q,那么即使不知道函数δ,也可以选择最优动作 s:

函数 Q 通常被称为动作值函数。根据策略π,动作值函数返回在特定状态 s 下使用动作 a 的期望奖励。
状态值函数和动作值函数之间的主要区别在于,Q 值允许你——至少在第一阶段——采取与策略预期不同的动作。这是因为 Q 是根据总奖励来推理的,因此在特定状态下,它也可能返回比其他动作获得的奖励更低的奖励。状态值函数包含到达某一状态的价值,而动作值函数包含在某一状态下选择一个动作的价值。如何选择最佳动作呢?让我们一探究竟。
选择一个动作
假设我们选择 n 个动作:a = a[1]… .a[n]。每个动作都有其自身的动作值函数值。在 t-t[h]时刻(回合)其估计值为 Q[t] (a[k])。回想一下,动作的真实值是当选择该动作时获得的平均奖励。估计这个值的自然方法是计算实际选择该动作时获得的奖励的平均值。换句话说,如果在第 t 回合选择了动作 a,并且在 t 之前选择了 k 次,获得了奖励 r[1], r[2], ..., r[ka],那么它的估计值为:

这里,我们有如下内容:
-
对于 k[a] = 0,我们将 Q[t] (a[k])设置为默认值,Q[0] (a[k]) = 0(没有可用的估计)。
-
对于 k[a] → ∞,Q[t] (a[k]) → Q ^* (a[k])(根据大数法则)。
在所有这些情况下,动作值函数是作为平均值(样本平均法)计算的。
现在我们已经探讨了这项技术的基本概念,接下来我们将继续探索可能的解决方案。
理解问题解决技巧
到目前为止,我们已经将问题形式化,并且我们已经看到有哪些工具可以帮助我们选择那些能带来最小遗憾的动作。现在,我们可以制定选择方法。我们将在以下小节中探讨以下问题解决技巧:
-
贪心方法
-
上置信界
贪心方法
这个问题,比看起来更复杂,可以通过使用一个非常简单的策略来解决,尽管它并不非常有效:
-
最初,每个拉杆都会被试验。
-
选择返回平均最高奖励的拉杆。
在这个算法中,我们可以观察到以下内容:
-
我们允许智能体拥有记忆
-
我们存储与不同动作相关的值
-
我们选择给出最大奖励的动作
最佳动作称为贪婪动作,基于这一动作的算法称为贪婪方法。执行以下步骤:
- 在时间步 t,按照如下方式估计每个动作的值:

- 选择具有最大值的动作:

在贪婪方法中,没有通过该方法探索其他替代解决方案。为什么我们必须选择一个看起来不是最好的动作?这是因为我们需要探索不同的解决方案,因为奖励并不是确定性的。这意味着我们可能通过其他动作获得更多的奖励,因为重要的不是瞬时的奖励,而是通过这些动作累计的奖励。在贪婪解法中,算法完全基于利用。为了提升模型的表现,必须引入探索。接下来我们看看怎么做。
ε-贪婪方法
在这里,需要保持一种倾向于探索不同动作的心态。我们再次面对一个基于探索与利用困境的问题,正如我们在第二章《强化学习的构建模块》中详细讨论过的那样,探索与利用困境。
理想情况下,智能体必须探索每个状态下所有可能的动作,找出最能通过利用该动作来达成目标的动作。因此,决策过程涉及一个基本的选择:
-
利用:根据当前信息做出最佳决策
-
探索:收集更多信息
在这个过程中,最佳的长期策略可能会在短期内带来相当大的牺牲。因此,需要收集足够的信息来做出最佳决策。
我假设以概率ε,选择一个不同的动作。这个动作是以均匀概率从 n 个可用动作中选择的。执行以下步骤:
- 在时间步 t,按照如下方式估计每个动作的值:

- 以概率 1-𝜖,选择最大值的动作,如下所示:

- 以概率𝜖,从所有动作中随机选择一个,且每个动作的概率相同。
最常用的参数值是𝜖 = 0.1,但这可以根据具体情况有所不同。在这种方法中,我们引入了探索的元素,以改善性能。然而,如果两个动作的 Q 值差异非常小,这个算法也会选择一个概率比其他动作更高的动作。
ε-贪婪方法与渐进递减
引入𝜀进行探索使我们有机会尝试那些至今未知的选项。然而,策略中的随机成分意味着那些已经采取并且得到较差结果的行动仍然可能被重新探索。通过逐渐减少随机探索成分,可以避免这种低效的探索。
换句话说,通过随着时间的推移减少参数𝜖,我们可以减少探索,因为我们已经对具有强大潜力的最优值的行动产生了信心。该策略在开始时提供了高度的探索行为,而在最后则表现出高度的利用行为。让我们一起来学习如何同时进行利用与探索。
上置信界
在游戏开始时,我们并不知道哪个臂是最好的。因此,我们无法对任何一个臂进行描述。因此,UCB 算法假设所有臂的观察平均值都是相同的。于是,将为每个臂创建一个置信区间,并随机选择一个臂。
在这种情况下,每个臂要么给出奖励,要么不给。如果选中的臂返回错误,臂的观察平均值以及置信区间都会下降。如果选中的臂返回奖励,观察到的平均值和置信区间都会增加。通过利用最佳臂,我们降低了置信区间。随着更多回合的进行,其他臂表现良好的可能性也在增加。
为了实施这一策略,请按照以下步骤操作:
-
每一轮中,都会计算两个变量:
-
Ri:杠杆 i 在进行 n 次操作后获得的奖励总和
-
T[i](n):策略在前 n 次操作中选择杠杆 i 的次数
-
-
我们使用以下公式计算在进行 n 次操作后杠杆 i 的平均奖励:

- 我们使用以下公式计算在进行 n 次操作后的置信区间:

- 我们按照以下方式选择返回最大 UCB 的杠杆 i:

这两种算法都记录它们对任何可用臂的了解程度,只关注它们从各个臂获得的奖励。与此相反,我们迄今为止分析的算法,对于那些初始经验没有返回显著奖励的选项,进行了不足的探索,即使它们没有足够的数据来确定这些臂。
在接下来的章节中,我们将介绍“状态”的概念,它代表了智能体可以用来执行有针对性行动的环境描述。
实现情境方法
到目前为止,在解决多臂老丨虎丨机(MAB)问题时,我们已经生成了一个动作,但我们并没有利用环境(上下文)的任何状态信息。代理可用的动作范围包括拉动一个或多个老丨虎丨机的臂。通过这种方式,可以获得+1 或-1 的奖励。
如果代理选择的臂不断返回正奖励,那么问题就被认为已解决。在这种情况下,我们可以设计一个完全忽视环境状态的代理,因为实际上,始终只有一个不可变的状态。
在上下文老丨虎丨机中,引入了“状态”概念,它表示环境的描述,代理可以利用该描述执行有针对性的动作。该模型通过将决策与环境状态相联系,扩展了原始模型。
下图展示了这两个模型的示意图:

原始问题在本质上发生了变化,因为在问题的新形式化中,不再只有一个老丨虎丨机(我们至今考虑的情况),而是有多个老丨虎丨机。环境的状态告诉我们正在处理哪个老丨虎丨机,代理的目标是学习针对任何可用老丨虎丨机的最佳动作。由于每个老丨虎丨机对每个臂的奖励概率不同,我们的代理需要学习根据环境状态来调整其行动。除非他们这样做,否则无法随着时间推移获得最大的奖励。
使用上下文老丨虎丨机模型,你不仅基于先前的观察优化决策,还可以根据每种情况个性化决策。
从前面的模型中,我们可以观察到以下几点:
-
算法观察一个上下文。
-
算法通过从一系列备选动作中选择一个动作来做出决策。
-
我们可以观察到该决策的结果,它返回一个奖励。
-
目标是最大化平均奖励。
将该算法应用于现实世界的一个例子是选择要在网站上展示的广告,以优化点击率的问题。上下文是关于用户的信息:用户来自哪里、使用的设备信息、之前访问过的网站页面、地理位置等等。一个动作对应于选择展示哪条广告。一个结果是用户是否点击了广告横幅。奖励是二元的:如果没有点击,则奖励为 0;如果点击,则奖励为 1。现在,让我们学习如何交替评估策略与改进策略。
理解异步演员-评论员代理
Actor-Critic 方法实现了一个广义的策略迭代,交替进行策略评估和策略改进步骤。Actor 改进的两个紧密相关的过程旨在改善当前策略,而 Critic 评估则是通过评估当前策略来实现的。如果 Critic 所定义的过程具有引导作用,那么方差就会减少。通过这样做,相比于策略梯度方法,算法的学习会变得更加稳定。
这些方法的特点是分离内存结构,使得策略与价值函数相互独立。策略模块被称为 Actor,因为它选择行动,而估计的价值函数则被称为 Critic,从这个角度看,Critic 批评的是策略执行的动作。从这一点我们可以理解,学习是基于策略类型的——事实上,Critic 学习并批评当前策略的工作。
我们已经介绍了 Actor-Critic 模型,现在我们将解释“异步”这一术语。这非常简单,并且具有有效的直觉,例子如下:
-
该模型有多个智能体同时探索环境(每个智能体都有整个环境的副本)。
-
该模型给出不同的初始策略,使得各个智能体彼此独立。
-
在该模型中,全局状态会随着每个智能体的贡献而更新,整个过程会重新开始。
2016 年,Google DeepMind 小组提出了一种名为异步优势 Actor-Critic(A3C)的算法。该算法比大多数现有算法更快、更简洁。
在此算法中,处理了多个不同初始化的智能体实例,它们处于各自的环境中。每个开始行动并学习的智能体都会积累自己的经验。然后,这些经验将用于更新所有智能体共享的全局神经网络。该网络会影响所有智能体的行动,每个智能体的新经验会加速整体网络的更新。由于有多个实例的智能体,训练将变得更加快速和高效。
在接下来的章节中,我们将通过解决一个实际案例,应用迄今为止学到的概念。
使用 MAB 模型的在线广告
在线广告属于新媒体范畴,并利用了互联网能够接触到大量人群的优势。广告对公司至关重要,因为公司可以以比传统方式更低的成本,轻松接触到广泛的受众。互联网广告的主要优势之一是结果的可追踪性,或者说它对公众的影响。这得益于广告服务器,在横幅广告的情况下,它们可以衡量展示次数、实际用户数量和点击次数。
在在线广告中,我们可以区分以下两种宏类型:
-
上下文
-
行为广告
在上下文广告中,谷歌是典型的案例,您可以根据页面上的词语或网站的类型或主题放置广告。在行为广告中,我们通过收集关于每个用户在网络和应用程序上的行为(访问的页面、进行的搜索)来选择目标,以识别他们的兴趣和需求,然后根据这些信息投放相关广告。
在这两种情况下,明显的是,引用上下文需要与环境交互。也显然,在展示广告之前,我们无法先知用户的行为。这些问题可以通过基于 MAB 的模型来解决,如下所示:
-
上下文由访问者和网页的特征表示。
-
手臂通过可用的广告类型来表示。
-
一个动作等同于将要展示的广告类型。
-
奖励由访问者的行为返回。通过点击展示的广告,您会收到奖励 1,而不点击广告则会收到奖励 0。
为了使这个讨论尽可能易于理解,我们将限制我们希望评估的广告数量为三个,并旨在找出在一定数量的展示后,哪个策略能提供最大的总点击率。所以,让我们通过采用上下文方法来学习如何解决这个问题。
实现上下文包
在上下文方法部分,我们提到,在上下文赌博机中,引入了状态的概念,表示代理可以用来执行有针对性行动的环境描述。该模型通过将决策与环境的状态联系起来,扩展了原始模型。在本节中,我们将看到一些使用上下文方法解决武装赌博机问题的应用示例。为此,我们将使用contextual包。
该包简化了无上下文和上下文 MAB 策略或算法的模拟和评估,便于现有和新算法及策略的实现、评估和传播。
diagram包的简要描述已从官方文档中提取,并显示在下表中:
| 版本 | 0.9.8.2 |
|---|---|
| 日期 | 2019-07-08 |
| 维护者 | Robin van Emden |
| 许可证 | GPL-3 |
| 作者 | Robin van Emden, Maurits Kaptein |
contextual包由作者在以下论文中提出:van Emden, R. 和 Kaptein, M., 2018. Contextual: Evaluating Contextual Multi-Armed Bandit Problems in R. arXiv 预印本 arXiv:1811.01926。
在此包中,MAB 问题是在以下假设下处理的:
-
赌博机是一组手臂,每个手臂由某些奖励函数定义,这些奖励函数将维度上下文向量映射到每个时间步长的奖励,直到预定的时间范围。
-
政策的功能是最大化累积奖励。这个功能是通过选择当前可用的赌博机臂之一来实现的。
-
在学习过程中,策略观察环境的当前状态,状态由上下文特征向量表示。之后,策略使用臂选择策略选择一个可用的动作。作为结果,它会获得一个奖励。这个过程允许策略更新策略臂的选择。此过程随后会重复T次。
为了表示算法中所述的内容,我们可以说,对于每一轮,策略执行以下操作:
-
观察当前上下文特征向量
-
选择一个动作
-
获得奖励
-
更新臂选择策略参数
策略的目标是优化其累积奖励。
然后,我们将应用上下文包中包含的函数到实际案例中。
在线广告无上下文策略
我们将进行的第一次模拟将不考虑上下文。我们将简单地评估在一定次数展示后,每个广告收到的点击量。首先,我们需要设置初始设置。正如预期的那样,我们只考虑三个公告,它们在 MAB 模型中对应于三条臂,每条臂都有不同的点击概率。让我们开始吧:
- 我们将使用以下代码进行分析:
library(contextual)
horizon <- 500
simulations <- 1000
ProbClick <- c(0.1, 0.3, 0.7)
bandit <- BasicBernoulliBandit$new(weights = ProbClick)
policy <- EpsilonFirstPolicy$new(time_steps = 100)
agent <- Agent$new(policy, bandit)
simulator <- Simulator$new(agent, horizon, simulations, do_parallel = FALSE)
history <- simulator$run()
plot(history, type = "average", regret = FALSE, lwd = 2, legend_position = "bottomright")
plot(history, type = "cumulative", regret = FALSE, rate = TRUE, lwd = 2)
plot(history, type = "arms")
- 我们将逐行分析这段代码。第一行加载了库:
library(contextual)
该库包含许多函数,允许模拟和评估无上下文和有上下文的 MAB 策略或算法。
- 现在,让我们固定一些必要的参数,以将问题设定为 MAB:
horizon <- 500
- 视野是必须玩的轮次数。让我们设置模拟的次数:
simulations <- 1000
模拟次数表示重复模拟的次数。
- 继续并设置用户点击广告的概率:
ProbClick <- c(0.1, 0.3, 0.7)
已经定义了一个向量,包含每个广告被点击的概率。在 MAB 术语中,它们表示与三条臂相关联的概率。此时,初始参数已经固定,因此我们可以继续定义对象。
第一个将是赌博机:
bandit <- BasicBernoulliBandit$new(weights = ProbClick)
在这里,我们使用了BasicBernoulliBandit()函数。这个函数模拟 k 个伯努利臂,其中每个臂以均匀概率 p 发放奖励 1,否则发放奖励 0。在一个赌博机场景中,这可以用来模拟一个命中或未命中的事件,比如用户是否点击了一个标题、广告或推荐的产品。只需要一个参数(权重):它是一个数值向量,表示每个赌博机臂的奖励概率。new()方法生成并实例化一个新的BasicBernoulliBandit实例。
- 接下来定义策略:
policy <- EpsilonFirstPolicy$new(time_steps = 100)
这里,我们使用了 EpsilonFirstPolicy() 函数来实现一个简单的策略,其中纯探索阶段后跟纯利用阶段。在前 N 个时间步内进行探索。在此期间,每个时间步 t,EpsilonFirstPolicy 会随机选择一个 arm。接下来的步骤中进行利用,我们选择最佳的 arm,直到 N 为止,或者在剩余的 N 次试验或时间步长 T 内进行利用。这里,我们使用了 new() 方法来生成一个新的 EpsilonFirstPolicy 对象。接下来继续:
agent <- Agent$new(policy, bandit)
agent 类负责运行一个 bandit/policy 配对。以下是可用的参数:
-
policy:一个策略实例。 -
bandit:一个 bandit 实例。 -
name character:设置 agent 的名称。如果为 NULL(默认值),则 agent 根据其策略实例的名称生成一个名称。 -
sparse numeric:通过为当前的 bandit 和 policy 配对设置稀疏度水平,人工降低数据大小。
在我们的例子中,只有两个参数被传递:policy 和 bandit。接下来我们来运行模拟:
simulator <- Simulator$new(agent, horizon, simulations, do_parallel = FALSE)
这是任何模拟的入口点。Simulator 类封装了一个或多个 agent,针对每个 agent 创建一个克隆进行重复模拟,运行这些 agent,并将所有 agent 交互的日志保存在历史对象中。以下参数将被传递:
-
agent:一个 agent 实例或一个 agent 实例列表 -
horizon:运行每个 agent 的拉取次数或时间步数,其中 t = 1, . . . , T(整数值) -
simulations:每个 agent 在 t = 1, . . . , T 的模拟重复次数,每次重复时使用新的随机种子(整数值) -
do_parallel:是否并行运行模拟器进程(逻辑值)
关于 R6 类模拟器覆盖的所有主题的详细列表,请参阅官方文档:CRAN.R-project.org/package=contextual。
这里,我们使用了 new() 方法来生成一个新的模拟器对象。现在是时候运行模拟了:
history <- simulator$run()
run() 方法只是运行一个模拟器实例。在此时,我们已经将所有模拟历史记录保存在历史变量中。现在,我们可以利用这些数据绘制图表。首先,我们将分析平均奖励:
plot(history, type = "average", regret = FALSE, lwd = 2, legend_position = "bottomright")
plot() 函数根据历史数据生成图表。以下是可用的图表类型:
-
cumulative:绘制累计遗憾或奖励随时间变化的曲线。如果传入 regret=TRUE,则返回累计遗憾;如果传入 regret=FALSE,则返回累计奖励。 -
average:绘制平均遗憾或奖励的曲线。如果传入 regret=TRUE,则返回累计遗憾;如果传入 regret=FALSE,则返回累计奖励。 -
arms:绘制每个时间步中每个 arm 被选择的模拟百分比。如果运行了多个 agent,则仅绘制第一个 agent。
接下来的图表将被打印:

请注意,在第一次探索阶段中,所有的臂都被平等测试后,我们进入了利用阶段,在该阶段中,返回最高奖励的臂会被优先选择。两个阶段之间的过渡可以通过平均奖励的净增加看到。接下来,让我们查看累积图:
plot(history, type = "cumulative", regret = FALSE, rate = TRUE, lwd = 2)
以下图表被返回:

这张图中探索阶段和利用阶段的过渡更为明显。在前 100 步中,只有探索阶段被使用,之后累积遗憾开始呈现对数型增长。最后,我们将绘制臂的类型:
plot(history, type = "arms")
这种类型的图表返回了每个时间步骤中每个臂被选择的模拟次数百分比。以下图表被打印出来:

从前面的图表中,我们可以看到最常选择的臂是 3 号臂。事实上,即使在这种情况下,在首次探索阶段中,三个臂的选择比例相近,但进入利用阶段后,选择的臂就完全集中在了 3 号臂上。
基于 ε-greedy 策略的在线广告
现在,我们将通过采用 ε-greedy 策略来解决同样的问题。正如我们之前提到的,使用这种方法,我们引入了探索元素,从而提高了性能。以下代码展示了使用 ε-greedy 方法时的分析过程:
library(contextual)
horizon <- 500
simulations <- 1000
ProbClick <- c(0.1, 0.3, 0.7)
bandit <- BasicBernoulliBandit$new(weights = ProbClick)
policy <- EpsilonGreedyPolicy$new(epsilon = 0.1)
agent <- Agent$new(policy,bandit)
simulator <- Simulator$new(agent, horizon, simulations, do_parallel = FALSE)
history <- simulator$run()
plot(history, type = "average", regret = FALSE, lwd = 2, legend_position = "bottomright")
plot(history, type = "cumulative", regret = FALSE, rate = TRUE, lwd = 2, legend_position = "bottomright")
plot(history, type = "arms", legend_position = "topright")
我们将逐行分析这段代码。第一行加载了库:
library(contextual)
现在,让我们设置一些必要的参数,以将问题设为 MAB:
horizon <- 500
horizon 是必须进行的回合数。让我们设置模拟次数:
simulations <- 1000
接下来,我们设置用户点击广告的概率:
ProbClick <- c(0.1, 0.3, 0.7)
我们使用了与前一个示例相同的概率向量。接下来,我们定义对象:
bandit <- BasicBernoulliBandit$new(weights = ProbClick)
为了定义赌博机,我们使用了 BasicBernoulliBandit() 函数。这个函数模拟了 k 个伯努利臂,其中每个臂以均匀概率 p 发出奖励 1,否则发出奖励 0。接下来,我们来定义策略:
policy <- EpsilonGreedyPolicy$new(epsilon = 0.1)
这里,我们使用了 EpsilonGreedyPolicy() 函数。该函数在探索阶段以概率 epsilon 随机选择一个臂;否则,它会在利用阶段贪婪地选择回报最高的臂。只有 epsilon 参数被传入,表示随机选择臂的概率。new() 方法用于生成一个新的 EpsilonGreedyPolicy 对象。最后,我们将创建一个代理对象,如下所示:
agent <- Agent$new(policy,bandit)
代理类负责运行一个 Bandit/Policy 配对。现在,我们可以运行模拟:
simulator <- Simulator$new(agent, horizon, simulations, do_parallel = FALSE)
以下结果被打印出来:
Simulation horizon: 500
Number of simulations: 1000
Number of batches: 1
Starting main loop.
data.table 1.12.0 Latest news: r-datatable.com
Finished main loop.
Completed simulation in 0:01:25.191
Computing statistics.
现在,我们可以运行模拟:
history <- simulator$run()
我们进行的模拟历史记录保存在 history 变量中。我们可以使用这些数据来绘制图表。首先,绘制了遗憾平均值图:
plot(history, type = "average", regret = TRUE, lwd = 2, legend_position = "topright")
以下图表被返回:

然后,我们绘制累计遗憾图:
plot(history, type = "cumulative", regret = TRUE, rate = TRUE, lwd = 2,legend_position = "topright")
返回以下图表:

最后,绘制了臂选择的百分比图:
plot(history, type = "arms")
返回以下图表:

在前面三个图中,突出了一个特征(Arm choice %)。遗憾随时间的减少是逐渐的,并不像之前的仿真(基于无上下文策略的例子)那样出现不连续。这是因为算法同时进行了探索和利用阶段。一方面,它在 ε 的时间里随机均匀地探索某个广告,而另一方面,它在 1-ε 的时间里利用点击率最高的广告。
在线广告基于上下文的策略
如果将访问者分为男性和女性,或者年轻人和成年人两类,会发生什么情况?我们可以在迄今为止分析的模型中引入对上下文的引用。回想一下,在上下文 Bandit 模型中,引入了状态的概念,状态代表了代理可以用来执行有针对性行动的环境描述。该模型通过将决策与环境状态关联,扩展了原始模型。以下代码使用上下文方法进行分析:
library(contextual)
horizon <- 500
simulations <- 1000
ProbClickContx <- matrix(c(0.1, 0.3, 0.7, 0.8, 0.4, 0.1),
nrow = 2, ncol = 3, byrow = TRUE)
bandit <- ContextualBinaryBandit$new(weights = ProbClickContx)
policy <- EpsilonGreedyPolicy$new(epsilon = 0.1)
agent <- Agent$new(policy,bandit)
simulator <- Simulator$new(agent, horizon, simulations, do_parallel = FALSE)
history <- simulator$run()
plot(history, type = "arms", legend_position = "bottomright")
我们将逐行分析这段代码。第一行加载了库:
library(contextual)
现在,让我们设置一些必要的参数,将问题设置为 MAB:
horizon <- 500
横向轴表示必须进行的轮次数。我们来设置仿真次数:
simulations <- 1000
继续设置用户点击广告的概率。在这种情况下,引入了对二元上下文的引用。定义了两种可能的概率分布,它们分别对应两种用户画像:
ProbClickContx <- matrix(c(0.1, 0.3, 0.7, 0.8, 0.4, 0.1),
nrow = 2, ncol = 3, byrow = TRUE)
第一个画像的概率向量为 (0.1, 0.3, 0.7),而第二个画像的概率向量为 (0.8, 0.4, 0.1)。接下来,让我们继续定义对象:
bandit <- ContextualBinaryBandit$new(weights = ProbClickContx)
为了定义 Bandit,我们使用了 ContextualBinaryBandit() 函数。该函数模拟了一个上下文 Bernoulli MAB 问题,其中至少有一个上下文特征在任何时刻是活动的。在这种情况下,weights 参数包含一个 d x k 的数值矩阵,其中包含了每个 k 个臂的 d 个上下文特征的奖励概率。接下来,让我们继续定义策略:
policy <- EpsilonGreedyPolicy$new(epsilon = 0.1)
在这里,我们使用了 EpsilonGreedyPolicy() 函数。最后,我们将创建一个代理对象,如下所示:
agent <- Agent$new(policy,bandit)
代理类负责运行一个 Bandit/Policy 配对。现在,我们可以运行仿真:
simulator <- Simulator$new(agent, horizon, simulations, do_parallel = FALSE)
输出以下结果:
Simulation horizon: 500
Number of simulations: 1000
Number of batches: 1
Starting main loop.
Finished main loop.
Completed simulation in 0:01:43.277
Computing statistics.
现在,我们可以存储仿真结果:
history <- simulator$run()
仿真的历史记录保存在 history 变量中。现在,我们可以使用这些数据绘制图表。在此情况下,仅绘制了臂选择的百分比:
plot(history, type = "arms", legend_position = "bottomright")
返回以下图表:

从这个分析中可以看出,最常选择的臂是第 1 号臂,然后是第 3 号臂,最后是第 2 号臂。
解决技术比较
解决 MAB 问题有几种策略。在问题解决技术部分,我们分析了一些策略。上下文包提出了一些策略。以下是可用策略的列表:
-
上下文时期贪心策略 -
上下文ε-贪心策略 -
上下文 LogitBTS 策略 -
`上下文 TSProbit 策略` -
ε-贪心策略 -
ε-贪心策略 -
Exp3 策略 -
固定策略 -
GittinsBrezziLai 策略 -
梯度策略 -
Lif 策略 -
LinUCB 不相交优化策略 -
LinUCB 不相交策略 -
LinUCB 通用策略 -
LinUCB 混合优化策略 -
LinUCB 混合策略 -
Oracle 策略 -
随机策略 -
Softmax 策略 -
汤普森采样策略 -
UCB1 策略 -
UCB2 策略
有关这些策略的详细描述,请参阅该包的官方文档,文档可通过以下网址访问:CRAN.R-project.org/package=contextual。
其中一些策略已经在我们本章查看的示例中采用过。为了分析这些策略的特点,我们可以基于已经获得的结果进行比较。以下是完整的代码:
library(contextual)
horizon <- 500
simulations <- 1000
ProbClick <- c(0.8, 0.3, 0.1)
bandit <- BasicBernoulliBandit$new(weights = ProbClick)
agents <- list(Agent$new(OraclePolicy$new(), bandit),
Agent$new(UCB1Policy$new(), bandit),
Agent$new(ThompsonSamplingPolicy$new(1.0, 1.0), bandit),
Agent$new(EpsilonGreedyPolicy$new(0.1), bandit),
Agent$new(SoftmaxPolicy$new(tau = 0.1),bandit),
Agent$new(Exp3Policy$new(0.1), bandit))
simulation <- Simulator$new(agents, horizon, simulations)
history <- simulation$run()
plot(history, type = "cumulative", regret = FALSE)
我们将逐行分析这段代码。第一行加载了库:
library(contextual)
现在,让我们设置一些必要的参数,将问题设置为 MAB:
horizon <- 500
simulations <- 1000
现在,让我们继续设置用户点击广告的概率:
ProbClick <- c(0.8, 0.3, 0.1)
现在我们继续定义赌博机对象:
bandit <- BasicBernoulliBandit$new(weights = ProbClick)
这里,我们使用了BasicBernoulliBandit()函数。现在,我们将创建一个代理对象的列表,如下所示:
agents <- list(Agent$new(OraclePolicy$new(), bandit),
Agent$new(UCB1Policy$new(), bandit),
Agent$new(ThompsonSamplingPolicy$new(1.0, 1.0), bandit),
Agent$new(EpsilonGreedyPolicy$new(0.1), bandit),
Agent$new(SoftmaxPolicy$new(tau = 0.1),bandit),
Agent$new(Exp3Policy$new(0.1), bandit))
定义了六个具有不同策略的代理,如下所示:
-
Oracle 策略:此策略始终知道奖励概率,并将始终选择最优臂。它通常作为基准,用于与其他策略进行比较。 -
UCB1 策略:此策略构建了一个乐观的估计,以上置信区间的形式创建每个动作的预期回报估计,并选择具有最高估计值的动作。如果估计错误,乐观估计会迅速下降,直到另一个动作的估计值更高。 -
汤普森采样策略:此策略的过程利用了武器的平均奖励记忆。为此,使用带有 alpha 和 beta 参数的 beta-二项式模型,从上一步中为每个臂采样值,并选择具有最高值的臂。当拉动一个臂并观察到一个伯努利奖励时,它根据奖励修改先验。此过程会为下一次拉臂重复。 -
EpsilonGreedyPolicy:该策略遵循的程序是以 epsilon 概率随机选择一个臂来探索环境。否则,它会贪婪地选择估计奖励最高的臂。通过这种方式,代理可以利用前一步获得的信息。 -
SoftmaxPolicy:该策略基于 Boltmann 分布中的概率选择一个臂。它使用了一个温度参数 tau,指定我们可以探索多少个臂。当 tau 较高时,所有臂的探索机会相同;而当 tau 较低时,系统会选择那些提供更高奖励的臂。 -
Exp3Policy:该策略遵循的程序使用概率分布,它是均匀分布的混合物。它还使用一种分布,将每个动作的概率质量分配在该动作的累积奖励的指数值上。
现在,我们可以运行仿真并存储其历史记录:
simulation <- Simulator$new(agents, horizon, simulations)
history <- simulation$run()
最后,我们将绘制我们模拟的代理的累积悔恨值:
plot(history, type = "cumulative")
返回以下图表:

正如我们预期的那样,OraclePolicy 在绝对悔恨值上表现最低,这意味着它成功地更好地利用了提供更好结果的臂。
总结
在本章中,我们学习了多臂强盗模型的基本概念。该模型基于探索与开发的困境。在资源有限的情况下,基于我们做出选择的标准,了解哪些竞争性替代方案可以最大化预期利润至关重要。该名称源于一个例子,描述了一位玩家在面对一排老丨虎丨机时的困境,他必须决定是否继续使用当前机器还是尝试另一台机器。我们描述了该问题的数学模型。然后,我们了解了行动价值实现的含义,并探讨了它与价值函数的区别。状态值函数包含到达某个状态的值,而行动值函数包含在某个状态下选择一个行动的值。
分析了几种问题求解技术,并讨论了上下文方法。我们还查看了 MAB 问题的一些实际应用列表。最后,使用几个 MAB 模型处理了在线广告问题。
在下一章中,我们将学习优化技术的基本概念。我们将学习如何将一个问题分解为子问题,并如何实现各种优化技术。然后,我们将理解递归与记忆化的区别,并发现如何使用动态规划方法做出最合适的选择。
第五章:动态规划与最优策略
动态规划(DP)代表了一组可以用来计算最优策略的算法,前提是有一个完美的环境模型,以马尔可夫决策过程(MDP)的形式给出。DP 方法根据之前步骤中的估计值来更新状态值的估计。在 DP 中,优化问题被分解为更简单的子问题,每个子问题的解被存储,以便每个子问题只被解决一次。在本章中,我们将学习如何通过 R 代码实现 DP 来选择最优的投资组合。
本章涵盖以下主题:
-
理解 DP
-
学习自顶向下的 DP 方法
-
分析递归与备忘录化的区别
-
学习优化技术
-
在强化学习应用中实现 DP
-
解决背包问题
-
机器人导航系统的优化
技术要求
查看以下视频,了解代码如何运行:
理解 DP
DP(动态规划)是理查德·贝尔曼(Richard Bellman)在 1950 年代开发的数学方法论。它用于解决需要按顺序处理一系列相互依赖的决策问题。该方法论背后的基本原则是贝尔曼的最优性——无论初始状态和初始决策如何,后续的决策必须相对于前一个决策所导致的状态提供最优的策略。这是最优策略的本质特征。
设想一个寻找连接两个位置的最佳路径的例子。最优性原则指出,路径中每一个子路径,无论是从中间位置到最终位置,都必须是最优的。基于这一原则,DP 通过一次作出一个决策来解决问题。在每一步,都会确定未来的最佳策略,而不考虑过去的选择(它是一个马尔可夫过程),前提是这些选择也是最优的。
因此,DP 在原问题可以分解为一系列较小的子问题,或当支付的成本或获得的利润可以表示为与每个单独决策相关的基本成本之和时,DP 是有效的。更一般地,成本必须通过某些运算符表示为基本成本的组合,这些基本成本各自依赖于单个决策。
以下图示展示了网络中两个节点之间的最佳路径(红色路径),在所有可用路径中选择的“最佳”路径是“最短”的:

有许多路径可以到达相同的目的地:只有一条是最短的。
现在让我们来分析这项技术的基本概念。我们将通过比较两种非常流行的技术开始。
学习自顶向下的 DP 方法
为了理解动态规划背后的机制,我们可以将其与另一种非常常见的解决问题的机制——分治法进行比较。通过分治法,一个问题被分解成两个或更多的子问题,原问题的解是从子问题的解开始构建的。这种方法叫做自顶向下的技术,按以下步骤进行:
-
将问题实例分解成两个或更多的子实例。
-
对每个子实例递归地解决问题。
-
重新组合子问题的解,以获得全局解。
这种机制广泛应用于解决多个问题。最常见的应用是两种最常用的排序算法——快速排序和归并排序。
例如,在快速排序算法中,待排序列表的元素被分为两个块,一块是小于主元的元素,另一块是大于主元的元素,然后算法会递归地对这两个块进行调用。归并排序中,算法找到中间位置的索引,并将列表分成两个各包含 n/2 元素的块。然后,算法会递归地对这两个块进行调用。
有些情况下,分治法无法应用,因为我们不知道如何获得子问题——问题本身并不包含足够的信息来让我们决定如何将其分解为多个部分。
在这种情况下,动态规划发挥作用:我们继续计算所有可能的子问题的解,并从子解开始,逐步得到新的子解,直到解决原问题。与分治法不同,待解决的子问题不一定是互不相交的,这意味着一个子问题可以是多个其他子问题的共同部分。为了避免对子问题进行重复计算,子问题通过自底向上的策略解决——从最小的子问题开始解决,逐步向大的子问题推进,并将这些子问题的解存储在适当的表格中,以便它们可以在需要时(如有必要)为解决其他子问题提供帮助。
在递归算法中,我们常常遇到一些从计算角度来看不必要的繁重过程。让我们看看如何解决这个问题。
分析递归和记忆化之间的区别
在这里所说的基础上,我们可以推断出动态规划(DP)用于那些具有递归定义的问题,但将该定义直接转化为算法会由于不同递归调用对相同数据子集的重复计算,导致程序的时间复杂度呈指数增长。一个例子是斐波那契数的计算,我们将在后续详细分析。
我们已经看到,动态规划(DP)是一种更高效地解决递归问题的技术。这是为什么呢?在递归过程中,我们很多时候会重复地解决子问题。在动态规划中,情况并非如此——我们会记住这些子问题的解,从而避免再次求解。这种做法叫做记忆化。
如果一个变量在某一给定步骤的值依赖于之前计算的结果,并且这些计算会被重复多次,那么存储中间结果就变得非常有用,从而避免重复计算那些计算成本高昂的部分。
为了更好地理解递归和记忆化之间的区别,我们来分析一个简单的例子:计算一个数字的阶乘。一个自然数 n 的阶乘,记作n!,是小于或等于该数字的所有正整数的乘积。n 的阶乘计算公式如下:

一个数字的阶乘也可以通过递归来定义:

如果一个函数调用自身,则称该函数为递归函数。递归函数只能直接解决问题的某些特定情况,这些特定情况称为基准情况(例如之前公式中的那些情况):如果传入的数据属于某个基准情况,它就返回一个结果。在每次调用时,数据都会减少,直到某个时刻,我们到达了基准情况。当函数调用自身时,它会暂停当前执行,去执行新的调用。内部调用结束后,执行会恢复。递归调用的序列在最内层的调用遇到基准情况时终止。现在让我们看看如何优化这种技术。
学习优化技术
优化问题是一个可以通过成本函数(也叫做目标函数)来衡量其解的问题。要寻找的值通常是该函数的最小值或最大值。优化问题可以被简化为一系列的决策问题。
要解决优化问题,必须使用迭代算法。即一种计算程序,它在给定当前解的近似值时,通过适当的操作序列来确定一个新的近似值。从一个初始近似值开始,问题的可能解就以一种连续的方式被确定出来。
最优解的搜索算法可以分为以下三类:
-
列举技术:列举技术通过遍历函数定义域内的所有点来寻找最优解。通过将问题简化为更简单的子问题,可以减少问题的复杂性。动态规划(DP)就是其中的一种技术。
-
数值技巧:这些技巧通过利用一组必要且充分的条件来优化问题。它们可以分为直接方法和间接方法。间接方法通过求解一组非线性方程并迭代地寻找解,直到代价函数的梯度为零,来寻找最小值。直接方法则通过梯度指导搜索解的过程。
-
概率技巧:概率技巧基于枚举技术,但它们使用额外的信息来进行研究,可以看作是进化过程。这一类包括模拟退火算法,它使用热力学进化过程,以及基因算法类,它们利用生物进化技术。
在下一部分,我们将讨论基于动态规划(DP)的优化技术。
计算费波那契数列
列奥纳多·皮萨诺,也叫费波那契,是一位著名的意大利数学家(皮萨,1175 - 1240)。他的名字与费波那契数列相关,该数列源自斯瓦比亚皇帝腓特烈二世提出的一个问题。1223 年,在比萨的数学家比赛中,他提出了如下问题:在不考虑死亡的情况下,每对兔子每月生育一对兔子,且最年轻的兔子在生命的第二个月就能繁殖,一年内能得到多少对兔子?
费波那契对测试给出了如此快速的回答,以至于有人认为比赛是作弊的:

看看这里给出的数列:
-
前两个元素是 1, 1。
-
每个元素是由前两个元素之和给出的。
以 F(n) 表示第 n 个月的对数,费波那契数列变为以下形式:
-
F(1) = 1
-
F(2) = 1
-
F(n) = F (n-1) + F (n-2) 在第 n 个月,其中 n> 2
基于这个定义,我们通常假设 F(0) = 0,以便递归关系 F(n) = F(n-1) + F(n-2) 在 n = 2 时也成立。
费波那契数列促使我们研究了数学和自然科学的许多领域。然而,尽管发现了这一重要数列,费波那契并没有掌握它的许多方面。四个世纪后,开普勒观察到,两个连续项之间的关系趋向于黄金比例。
那么,来看看一个简单的 R 函数,它通过递归过程计算费波那契数:
FibRec <- function(n) {
if (n<=2)
return(1)
return (FibRec(n-1)+FibRec(n-2))
}
StartTime <- Sys.time()
paste("20th Fibonacci number is: ",FibRec(20))
EndTime <- Sys.time()
paste("Computational time using Recursion is: ",EndTime - StartTime)
在函数内部,有一个 if 结构包含两个选项:如果 n> 2,函数会调用自身;当 n<=2 时返回 1。对 FibRec(n-1) 的调用要求函数解决比最初问题更简单的问题(数值较小),但问题始终是相同的。函数会一直调用自身,直到达到它可以立即解决的基本情况。为了比较两种解决技术,使用 Sys.time() 函数来计算计算成本。结果如下所示:
"20th Fibonacci number is: 6765"
Computational time using Recursion is: 0.0400021076202393
由于使用的递归算法的性质,该程序需要进行 n + 1 次阶乘函数调用才能得到结果,并且每次调用都伴随着与函数返回计算值所需时间相关的成本。
通过记忆化,可以按如下方式改进该程序:
-
创建一个变量来存储临时结果(
RecTable)。 -
在进行计算之前,先检查该计算是否已经完成。如果完成了,则使用存储的结果。
-
如果这是第一次计算,存储结果以备将来使用。
以下代码展示了程序的记忆化版本:
RecTable <- c(1, 1, rep(NA, 100))
FibMem <- function(x) {
if(!is.na(RecTable[x])) return(RecTable[x])
ans <- FibMem(x-2) + FibMem(x-1)
RecTable[x] <<- ans
ans
}
StartTime <- Sys.time()
paste("20th Fibonacci number is: ",FibMem(20))
EndTime <- Sys.time()
paste("Computational time using Memoization is: ",EndTime - StartTime)
在这种情况下,我们将斐波那契数存储在一个表中,之后可以在下一次计算时检索。这样,避免了每次都进行整个计算。记忆化提高了函数的时间效率。每次该函数被调用时,都会突出改进,从而加速了算法。以下是结果:
20th Fibonacci number is: 6765
Computational time using Memoization is: 0.0310020446777344
通过比较两种计算成本,可以发现带有记忆化的版本更快。现在让我们来看一下如何在强化学习的背景下利用 DP 提供的潜力。
在强化学习应用中实现 DP
DP 代表一组算法,可用于在环境的完美模型(以 MDP 形式)下计算最优策略。DP 的基本思想,以及强化学习的一般思想,是利用状态值和动作来寻找良好的策略。
DP 方法通过迭代两个过程来解决马尔可夫决策过程,分别是 策略评估 和 策略改进:
-
策略评估算法通过应用迭代方法来求解贝尔曼方程。由于只有当 k → ∞ 时收敛才有保障,我们必须满足于通过设置停止条件得到良好的近似。
-
策略改进算法基于当前值来改进策略。
策略迭代算法的一个缺点是每一步都需要评估一个策略。这涉及到一个迭代过程,我们事先并不知道它的收敛时间,这将取决于起始策略是如何选择的。
克服这一缺点的一种方法是,在特定步骤中切断策略的评估。这一操作不会改变收敛到最优值的保证。在策略评估被逐步阻断(也叫做遍历)的特殊情况下,定义了值迭代算法。在值迭代算法中,每次政策改进步骤之间都会执行一次值计算的迭代。
因此,DP 算法本质上是基于策略评估和策略改进,这两个过程并行进行。反复执行这两个过程使得整个过程趋向于最优解。在策略迭代算法中,这两个阶段交替进行,且每个阶段结束后才开始下一个阶段。
DP 方法通过环境中所有可能的状态集进行操作,在每次迭代时对每个状态执行完整的备份操作。每次备份操作都根据所有可能的后继状态的值更新一个状态的值。这些状态会根据它们发生的概率进行加权,这个概率由策略选择和环境的动态性共同决定。完整备份与贝尔曼方程密切相关,它们不过是将贝尔曼方程转化为赋值指令的过程。
当一次完整的备份迭代没有对状态值产生任何变化时,收敛就达成,因此最终的状态值完全满足贝尔曼方程。DP 方法仅在存在完美的交替器模型时适用,该模型必须等同于马尔可夫决策过程(MDP)。
正是由于这个原因,DP 算法在强化学习中的应用有限,既因为它假设环境有完美模型,又因为计算量高且昂贵。但仍然有必要提及它们,因为它们代表了强化学习的理论基础。事实上,所有强化学习方法都试图达到 DP 方法的同样目标,只是计算成本较低,并且不假设环境有完美的模型。
DP 方法通过与状态数 𝑛 和动作数 𝑚 相比的多项式操作次数,收敛到最优解,而与之相对的是基于直接搜索的方法需要 𝑚*𝑛 的指数操作次数。
DP 方法基于先前步骤中做出的估计,更新状态值的估算。这代表了一种特殊的属性,称为自举(bootstrapping)。多种强化学习方法执行自举,即便是那些不要求环境的完美模型(如 DP 方法所需的),也同样执行自举。我们来看一个使用 DP 的实际案例。
求解背包问题
在这一节中,我们将分析一个经过超过一个世纪研究的经典问题,自 1897 年以来—背包问题。首位处理背包问题的是数学家托比亚斯·丹齐格,他将其命名为源自普通的装载最有用物品而不过载背包的问题。
这种类型的问题可以与现实生活中不同情况联系起来。为了更好地描述这个问题,我们将提出一个非常独特的情景:一个小偷进入房子并想偷走贵重物品。他把它们放在他的背包里,但受到重量的限制。每个物体都有自己的价值和重量,所以他必须选择价值高但重量不大的物品。不能超过背包的重量限制,但同时又要优化价值。
现在,我们将从数学的角度解决这个问题。假设我们有一个由整数标记为 1 到n的 n 个对象组成的集合 X:{1, 2, ..., n}。这些对象满足以下条件:
-
第 i 个物品具有重量 p[i]和价值 v[i]。
-
每个物体只有一个实例。
我们有一个容器,最多可以携带重量为 P 的物品。我们想确定对象的子集 Y ⊆ X:
-
Y 中物品的总重量≤ P。
-
Y 中物品的总价值是可能的最大值。
这两个数学形式中的条件如下:
- 我们希望确定一个对象的子集 Y ⊆ X,以便:

- 为了最大化以下总价值:

如所放置的那样,这是一个优化问题。一般来说,优化问题有两个部分:
-
一组必须遵守的约束(可能为空)。
-
必须最大化或最小化的目标函数。
我们已经采用的数学形式定义问题,明确澄清了我们刚才提到的两部分。许多实际问题可以相对简单地被表述为可以使用计算器解决的优化问题。将新问题简化为已知问题允许使用现有的解决方案。
与大多数问题一样,即使对于优化问题,解决问题的不同方法也允许我们达到解决方案。它们在时间和内存要求的复杂性以及所需的编程工作方面自然有所不同。
有两个问题的版本:
-
0-1 背包问题:每个物品要么全部接受,要么全部拒绝。
-
分数背包问题:我们可以取物品的分数部分。
这两个问题之间的实质差异在于物品是否可以分割。在 0-1 背包问题中,我们不能分割物品。相反,在分数背包中,我们可以分割物体以最大化背包的总价值。
我们介绍的背包问题可以很容易地应用于金融投资组合优化问题。实际上,只需将物体的重量与考虑中的金融产品的风险权重相关联,将物体的价值与金融产品的预期价值相关联即可。基于这些假设,可以选择使预期价值最大化,并将风险保持在特定值以下的金融产品。
在接下来的部分,我们将通过三种不同的方法解决背包问题:
-
暴力算法
-
贪婪算法
-
DP
我们将深入探讨每个解决方案,突出它们的优缺点。
暴力算法
暴力算法列出了可能表示解决方案的所有可能值,并检查每个值是否满足问题所施加的条件。此算法易于实现,如果存在解决方案,则总是返回解决方案,但其成本与可能解的数量成正比。因此,通常在问题规模有限或存在可以减少可能解集的假设时使用暴力搜索。该方法也用于在实现简单性比速度更重要时。
要解决背包问题,暴力算法是最直接的解决方案:检查填充背包的所有可能方法,这些方法有2n种,并打印出一个最优解(可能不止一个)。对于n > 15,这种方法变得非常慢。这种算法通常直接基于问题的定义和相关概念的理解。
这里是这个简单算法的要点:
-
枚举每种可能的组合。
-
选择最佳解决方案(检查所有组合,返回最大值且总重量小于或等于 P 的组合)。
-
优化性得到保证。
-
对于较大的n,时间成本极高。运行时间将为O(2n)。
在下面的代码块中是解决 0-1 背包问题的示例代码:
W = 10
WeightArray = c(5,2,4,6)
ValueArray = c(18,9,12,25)
DataKnap<-data.frame(WeightArray,ValueArray)
BestValue = 0
ItemsSelected = c()
TempWeights<-c()
TempValues<-c()
for(i in 1:4){
CombWeights<-as.data.frame(combn(DataKnap[,1], i))
CombValues<-as.data.frame(combn(DataKnap[,2], i))
SumWeights<-colSums(CombWeights)
SumValue<-colSums(CombValues)
TempWeights<-which(SumWeights<=W)
if(length(TempWeights) != 0){
TempValues<-SumValue[TempWeights]
BestValue<-max(TempValues)
Index<-which((TempValues)==BestValue)
MaxIndex<-TempWeights[Index]
MaxVW<-CombWeights[, MaxIndex]
j=1
while (j<=i){
ItemsSelected[j]<-which(DataKnap[,1]==MaxVW[j])
j=j+1
}
}
}
list(value=round(BestValue),elements=ItemsSelected)
我们将逐行分析这段代码。前几行设置了数据:
W = 10
WeightArray = c(5,2,4,6)
ValueArray = c(18,9,12,25)
DataKnap<-data.frame(WeightArray,ValueArray)
让我们逐个元素看一下:
-
W是最大重量容量。 -
WeightArray是重量数组。 -
ValueArray是值数组。 -
DataKnap是包含重量和值的数据框。
现在我们将初始化算法中使用的变量:
BestValue = 0
ItemsSelected = c()
TempWeights<-c()
TempValues<-c()
如前所述,暴力算法系统地列出了可能表示解决方案的所有可能值,并检查每个值是否满足问题所施加的条件。由于系统传递了四个对象,将设置一个四步循环如下:
for(i in 1:4){
对于for循环的每一步,我们将计算所有取出的物体的组合,i次不重复。为此,使用了combn()函数:
CombWeights<-as.data.frame(combn(DataKnap[,1], i))
CombValues<-as.data.frame(combn(DataKnap[,2], i))
这个函数生成所有DataKnap列中元素的组合,取i个元素。接下来,我们将对返回的数组进行求和:
SumWeights<-colSums(CombWeights)
SumValue<-colSums(CombValues)
现在需要选择只返回重量总和<=W的组合:
TempWeights<-which(SumWeights<=W)
如果该操作返回至少一个组合,我们将计算最佳解:
if(length(TempWeights) != 0){
TempValues<-SumValue[TempWeights]
BestValue<-max(TempValues)
Index<-which((TempValues)==BestValue)
MaxIndex<-TempWeights[Index]
MaxVW<-CombWeights[, MaxIndex]
j=1
while (j<=i){
ItemsSelected[j]<-which(DataKnap[,1]==MaxVW[j])
j=j+1
}
}
}
最后,最佳组合的列表将被打印出来:
list(value=round(BestValue),elements=ItemsSelected)
以下是 0-1 背包问题解返回的结果:
$value
[1] 37
$elements
[1] 3 4
结果表明,最佳解返回值为 37,所选物体位于第 3 和第 4 位置。正如预期的那样,我们刚才处理的背包问题的最优解是最直接的,但从计算的角度来看也是最昂贵的。在接下来的部分中,我们将尝试获得其他解,力求在计算上节省开销。
贪心算法
在引入贪心算法来寻找背包问题的最优解之前,回顾一下任何贪心技术的主要特点是很有必要的。任何贪心技术都是迭代进行的。从一个空的解开始,在每次迭代中,元素 A 会被添加到正在构建的部分解中。在所有可以添加的候选元素中,元素 A 是最有前途的,即如果选择它,它将导致目标函数的最大改进。显然,并不是所有问题都能通过这种策略解决;只有那些可以证明当前做出最佳选择能导致全局最优解的问题,才能使用这种方法。
让我们首先看一个简单的算法,执行以下操作:
-
丢弃所有重量超过最大容量的物体(预处理)。
-
根据给定的标准对物体进行排序。
-
一次选择一个物体,直到满足重量限制。
-
返回解的值和所选物体的集合。
在以下代码块中,我们可以看到执行该算法的代码:
K = 10
w = c(5,2,4,6)
v = c(18,9,12,25)
DataKnap<-data.frame(w,v)
DataKnap$rows_idx <- row(DataKnap)
DataKnap <- DataKnap[DataKnap$w < K,]
DataKnap$VWRatio <- DataKnap$v/DataKnap$w
DescOrder <- order(DataKnap$VWRatio, decreasing = TRUE)
DataKnap <- DataKnap[DescOrder,]
KnapSol <- list(value = 0)
SumWeights <- 0
i <- 1
while (i<=nrow(DataKnap) & SumWeights + DataKnap$w[i]<=K){
SumWeights <- SumWeights + DataKnap$w[i]
KnapSol$value <- KnapSol$value + DataKnap$v[i]
KnapSol$elements[i] <- DataKnap$row[i]
i <- i + 1
}
print(KnapSol)
我们将逐行分析这段代码。初始的几行设置了初始数据:
K = 10
w = c(5,2,4,6)
v = c(18,9,12,25)
DataKnap<-data.frame(w,v)
为了找到最佳解,我们首先对物体按价值密度进行降序排列,计算方法如下:

这种技术在以下代码中实现:
DataKnap$rows_idx <- row(DataKnap)
DataKnap <- DataKnap[DataKnap$w < K,]
DataKnap$VWRatio <- DataKnap$v/DataKnap$w
DescOrder <- order(DataKnap$VWRatio, decreasing = TRUE)
DataKnap <- DataKnap[DescOrder,]
以下几行用于初始化变量:
KnapSol <- list(value = 0)
SumWeights <- 0
i <- 1
现在,将使用while循环来迭代该过程:
while (i<=nrow(DataKnap) & SumWeights + DataKnap$w[i]<=K){
SumWeights <- SumWeights + DataKnap$w[i]
KnapSol$value <- KnapSol$value + DataKnap$v[i]
KnapSol$elements[i] <- DataKnap$row[i]
i <- i + 1
}
循环会一直重复,直到两个条件都为真。一旦其中一个条件为假,循环就会停止。第一次检查是在数据矩阵的行数上进行的,最多会有与行数相等的迭代次数。第二次检查是在设定的最大容量上进行的。一旦超过这个容量,循环会停止。
最后,结果会被打印出来:
print(KnapSol)
结果显示如下:
$value
[1] 34
$elements
[1] 2 4
从对前面数据的分析中,我们可以注意到,我们没有像暴力算法那样获得最优解,但这个过程非常快速。
使用动态规划实现解法
在前面的章节中,我们已经看到了如何通过不同的方式解决背包问题。特别是,我们学会了用一种叫做暴力求解的算法来处理这个问题。在这种情况下,我们通过极大的计算代价获得了最优解。相反,后面看到的贪心算法从计算的角度给我们提供了一个更轻量的算法,但它无法获得最优解。通过动态规划(DP),可以提供一个同时满足最优解和快速算法这两个需求的解法。
在动态规划中,我们将一个优化问题分解为更简单的子问题,并存储每个子问题的解,以便每个子问题仅解决一次。该方法的核心思想是,首先计算子问题的解并将其存储在表格中,以便稍后可以重复使用这些解(重复使用)。
在下面的代码块中,使用动态规划实现了一个背包问题的解决方案:
v <- c(18,9,12,25)
w <- c(5,2,4,6)
W <- 10
Tabweights<-c(0,w)
TabValues<-c(0,v)
n<-length(w)
TabMatrix<-matrix(NA,nrow =n+1,ncol = W+1)
TabMatrix[,]<-0
for (j in 2:W+1){
for (i in 2:n+1){
if (Tabweights[i] > j) {
TabMatrix[i,j] = TabMatrix[i-1,j]
}
else
{
TabMatrix[i,j]<-max(TabMatrix[i-1,j], TabValues[i] + TabMatrix[i-1,j-Tabweights[i]])
}
}
}
cat("The best value is",TabMatrix[i,j])
i = n+1
w = W+1
ItemSelected = c()
while(i>1 & w>0)
{
if(TabMatrix[i,w]!=TabMatrix[i-1,w])
{
ItemSelected<-c(ItemSelected,(i)-1)
w = w - Tabweights[i]
i = i - 1
}
else
{
i = i - 1
}
}
cat("The items selected are",ItemSelected)
我们将逐行分析这段代码。这个算法从定义将在过程中使用的数据开始:
v <- c(18,9,12,25)
w <- c(5,2,4,6)
W <- 10
然后定义其他变量:
Tabweights<-c(0,w)
TabValues<-c(0,v)
n<-length(w)
TabMatrix<-matrix(NA,nrow =n+1,ncol = W+1)
TabMatrix[,]<-0
我们来看一下这段代码的元素:
-
Tabweights是一个包含重量和 0 作为第一个元素的向量。 -
TabValues是一个包含值和 0 作为第一个元素的向量。 -
n是物品的数量。 -
TabMatrix是一个表格矩阵。
我们首先定义并初始化一个将包含值的表格。该表格是从上到下按列构建的,如下图所示:

然后我们设置一个对所有物品和所有重量值的迭代循环:
for (j in 2:W+1){
for (i in 2:n+1){
if (Tabweights[i] > j) {
TabMatrix[i,j] = TabMatrix[i-1,j]
}
else
{
TabMatrix[i,j]<-max(TabMatrix[i-1,j], TabValues[i] + TabMatrix[i-1,j-Tabweights[i]])
}
}
}
首先,我们用 0 填充第一行 i=1。这意味着当没有物品时,重量为 0,因此我们将第一列 w = 1 填充为 0。这意味着当重量为 0 时,所考虑的物品为 0。实际上,我们将第一行初始化为 0,这对应于对于不同的可运输重量,我们没有任何物品的情况(T[1, w] = 0)。将第一列初始化为 0,这对应于对于多个可能的物品,我有一个零容量的背包的情况(T[i, 1] = 0)。
填充表格的规则由以下算法提供:
if (Tabweights[i] > j) {
TabMatrix[i,j] = TabMatrix[i-1,j]
}
else
{
TabMatrix[i,j]<-max(TabMatrix[i-1,j], TabValues[i] + TabMatrix[i-1,j-Tabweights[i]])
如果第 i^(th) 个元素的重量大于列的重量,则第 i^(th) 个元素将等于前一个元素,海拔将通过以下公式计算:

一旦到达表格最后一行的最后一个单元格,我们可以记住得到的结果,这代表背包中可以携带物品的最大值:
cat("The best value is",TabMatrix[i,j])
返回以下结果:
The best value is 37
到目前为止的过程没有指出哪个子集提供最优解。我们必须通过分析表格的最后一列(w = P)来提取此信息;我们将从最后一个值开始,逐步向上运行。
i = n+1
w = W+1
ItemSelected = c()
while(i>1 & w>0)
{
if(TabMatrix[i,w]!=TabMatrix[i-1,w])
{
ItemSelected<-c(ItemSelected,(i)-1)
w = w - Tabweights[i]
i = i - 1
}
else
{
i = i - 1
}
}
如果当前元素与前一个元素相同,我们就跳到下一个;否则,当前物体将被包含在背包中:
if(TabMatrix[i,w]!=TabMatrix[i-1,w])
{
ItemSelected<-c(ItemSelected,(i)-1)
如果元素被插入到背包中,通过从选定物体的重量中减去当前w值来获得列值:
w = w - Tabweights[i]
最后,所选的物品将被打印出来:
cat("The best value is",TabMatrix[i,j])
结果如下所示:
The items selected are 4 3
动态规划(DP)算法使我们能够获得最优解,从而节省计算成本。
在下一节中,我们将分析一个实际案例;我们将优化机器人的导航系统。
机器人导航系统的优化
机器人是一种根据提供的指令执行特定动作的机器,这些指令可以是基于直接人工监督,或基于使用人工智能过程的通用指导来独立执行的。机器人应该能够替代或协助人类完成诸如制造、建筑、在不适合人类的条件下处理重型和危险物料,或者仅仅是解放一个人免于承担某些责任的工作。
机器人应通过感知与行动之间的反馈来装备引导连接,而不是通过直接的人工控制。动作可以通过电磁马达或执行器的形式来进行,这些执行器可以移动四肢、开关夹爪或移动机器人。逐步控制和反馈由一个外部或内部机器人计算机或微控制器运行的程序提供。根据这个定义,机器人概念几乎可以包括所有自动化设备。
训练一个代理在环境中移动
为了理解如何解决与机器人自主导航相关的问题,我们将从一个广泛的问题开始——gridworld问题。在这些问题中,环境被定义为一个简单的二维矩形网格,尺寸为(N, M),代理从一个网格格子出发,试图移动到另一个位于其他位置的网格格子。这个环境非常适合应用强化学习算法,帮助代理在网格上发现到达目标网格格子的最优路径和策略,以最少的移动次数达到目标。
以下图示展示了一个 5 x 5 的网格:

代理在探索状态的过程中不断演化。该环境没有终止状态。代理可以执行{右移、左移、上移、下移}的动作。如果动作使代理移出网格,代理将保持在当前状态,但会应用一个负奖励。对于所有其他状态(和动作),奖励为 R = -2,除了将代理移至终点的动作。在这种情况下,四个动作的奖励为 R = +20,并将代理带到最终状态。
为了更好地理解上下文,我们将只处理一个 2 x 2 网格的问题,该网格中有一堵墙,禁止从第 1 格到第 4 格的通过,具体如以下图所示:

我们的目标是制定出最优策略,从 C1(起点)出发,最终到达 C4(终点)。以下代码是解决网格世界问题的一个示例:
library(MDPtoolbox)
UpAct=matrix(c(0.3, 0.7, 0, 0,
0, 0.9, 0.1, 0,
0, 0.1, 0.9, 0,
0, 0, 0.7, 0.3),
nrow=4,ncol=4,byrow=TRUE)
DownAct=matrix(c( 1, 0, 0, 0,
0.7, 0.2, 0.1, 0,
0, 0.1, 0.2, 0.7,
0, 0, 0, 1),
nrow=4,ncol=4,byrow=TRUE)
LeftAct=matrix(c( 0.9, 0.1, 0, 0,
0.1, 0.9, 0, 0,
0, 0.7, 0.2, 0.1,
0, 0, 0.1, 0.9),
nrow=4,ncol=4,byrow=TRUE)
RightAct=matrix(c( 0.9, 0.1, 0, 0,
0.1, 0.2, 0.7, 0,
0, 0, 0.9, 0.1,
0, 0, 0.1, 0.9),
nrow=4,ncol=4,byrow=TRUE)
AllActions=list(up=UpAct, down=DownAct, left=LeftAct, right=RightAct)
AllRewards=matrix(c( -2, -2, -2, -2,
-2, -2, -2, -2,
-2, -2, -2, -2,
20, 20, 20, 20),
nrow=4,ncol=4,byrow=TRUE)
mdp_check(AllActions, AllRewards)
GridModel=mdp_policy_iteration(P=AllActions, R=AllRewards, discount = 0.1)
GridModel$policy
names(AllActions)[GridModel$policy]
GridModel$V
GridModel$iter
GridModel$time
我们将逐行分析这段代码。首先,我们加载了库:
library(MDPtoolbox)
接下来,我们设置所有可能的动作:
UpAct=matrix(c(0.3, 0.7, 0, 0,
0, 0.9, 0.1, 0,
0, 0.1, 0.9, 0,
0, 0, 0.7, 0.3),
nrow=4,ncol=4,byrow=TRUE)
DownAct=matrix(c( 1, 0, 0, 0,
0.7, 0.2, 0.1, 0,
0, 0.1, 0.2, 0.7,
0, 0, 0, 1),
nrow=4,ncol=4,byrow=TRUE)
LeftAct=matrix(c( 0.9, 0.1, 0, 0,
0.1, 0.9, 0, 0,
0, 0.7, 0.2, 0.1,
0, 0, 0.1, 0.9),
nrow=4,ncol=4,byrow=TRUE)
RightAct=matrix(c( 0.9, 0.1, 0, 0,
0.1, 0.2, 0.7, 0,
0, 0, 0.9, 0.1,
0, 0, 0.1, 0.9),
nrow=4,ncol=4,byrow=TRUE)
每个动作矩阵都是 4 x 4 类型。事实上,它包含了从每个状态(由行表示)出发,在可能转移到其他状态(由列表示)时的所有概率。例如,MoveUp 矩阵的第一行包含了从 C1 状态出发,进行向上的动作时能够转移到其他状态的所有概率。显然,在这个状态下,执行此动作时,我可以转移到状态 C2。事实上,相关的概率是 0.7。相同矩阵的第二行包含了从 C2 状态出发,执行向上的动作时能够转移到其他状态的概率,在这种情况下,最大概率是它保持在此状态。
在下一步,我们将把所有定义的动作合并成一个列表:
AllActions=list(up=UpAct, down=DownAct, left=LeftAct, right=RightAct)
根据假设,我们来定义问题允许的奖励和惩罚:
AllRewards=matrix(c( -2, -2, -2, -2,
-2, -2, -2, -2,
-2, -2, -2, -2,
20, 20, 20, 20),
nrow=4,ncol=4,byrow=TRUE)
在继续之前,我们必须检查我们定义的问题格式。我们将使用mdp_check()函数:
mdp_check(AllActions, AllRewards)
这个函数检查由转移概率数组(AllActions)和奖励矩阵(AllRewards)定义的 MDP 是否有效。如果AllActions和AllRewards是正确的,函数将返回一个空的错误消息。反之,函数将返回一个描述问题的错误消息。让我们搜索从 C1 到 C4 的最优策略:
GridModel=mdp_policy_iteration(P=AllActions, R=AllRewards, discount = 0.1)
使用了mdp_policy_iteration()函数。这个函数应用策略迭代算法来解决折扣 MDP。该算法的基本思路是通过评估当前策略,迭代地改善策略。当两次连续的策略相同或已达到指定的迭代次数时,迭代过程停止。传入了三个参数:
-
AllActions:转移概率数组。这个数组可以是一个三维数组,也可以是一个列表,每个元素包含一个稀疏矩阵。 -
AllRewards:奖励数组。这个数组可以是一个三维数组或一个列表,每个元素包含一个稀疏矩阵或一个可能是稀疏的二维矩阵。 -
discount:折扣因子。折扣因子是一个介于[0; 1]之间的实数。
到此为止,我们可以恢复策略:
GridModel$policy
返回以下结果:
1 4 2 2
为了更好地理解策略,我们可以提取由策略定义的动作名称:
names(AllActions)[GridModel$policy]
返回以下结果:
"up" "right" "down" "down"
现在我们可以提取每个步骤的最优值。这些值在每次运行时可能不同:
GridModel$V
返回以下结果:
-2.213209 -2.097323 -0.474916 22.222222
我们讨论了迭代。实际上,我们可以看到算法在多少次迭代后达到收敛:
GridModel$iter
返回以下结果:
3
最后,我们打印了执行时间:
GridModel$time
返回以下结果:
Time difference of 0.377022 secs
面临的问题看似微不足道,但它让我们理解了必须如何处理。除非定义了最大行动和奖励的矩阵,否则必须以相同的方式处理更大的网格。
总结
在本章中,我们讨论了优化技术的基本概念。首先,我们学习了动态规划(DP)背后的基本元素。在动态规划中,我们将一个优化问题细分为更简单的子问题:我们计算所有可能子问题的解,从这些子解中获取新的子解,然后解决原始问题。
然后我们学习了递归与备忘录化的区别。随后,我们学习了背包问题的基础。这个问题通过三种不同的方法进行了解决:暴力法、贪心算法和动态规划。对于每种方法,提供了一个解法算法并进行了结果比较。
最后,我们讨论了导航路径的优化问题。为了处理机器人自主导航,我们学习了如何解决在网格世界中寻找路径的问题。通过这种方式,我们看到了如何解决找到最佳策略以确定路径的问题。
在下一章,我们将学习预测技术的基本概念。
第六章:蒙特卡洛方法在预测中的应用
蒙特卡洛方法用于估计价值函数并发现优秀策略时,并不需要环境的模型。你可以仅通过智能体的经验,或通过从智能体与环境交互中获得的状态序列、动作和奖励样本来进行学习。经验可以通过智能体的学习过程获得,或者通过先前填充的数据集进行模拟。在本章中,我们将学习如何使用蒙特卡洛方法来预测最优策略。
到本章结束时,你应该熟悉预测技术的基本概念,并学会如何应用蒙特卡洛方法来预测环境行为。我们还将学习无模型方法来处理强化学习问题,如何估计动作值,以及如何通过学习最优策略的价值来构成一个脱离策略的算法,而无需考虑智能体的动作。
本章将覆盖以下主题:
-
预测概述
-
理解蒙特卡洛方法
-
接近无模型算法
-
动作值估计
-
使用蒙特卡洛方法预测黑杰克策略
技术要求
请观看以下视频,查看代码的实际应用:
预测概述
试图预测未来有着悠久的历史,贯穿了整个人类历史,适应了不同文明的典型方式和宗教背景。预测未来事件的需求似乎不仅仅是为了纯粹的推测和认知目的,还具有操作性目的。其目标是选择最合适的行为来应对将要出现的问题,并尽力充分利用未来的情况。
预测和预言常常被作为同义词使用,但区分这两个术语的含义通常是个好主意。预测允许你将未来事件的发生概率与之关联,或者指定置信区间来估计将来可观察和可测量的大小。另一方面,预言涉及识别某一可测量量在未来将呈现的具体值。因此,使用推断统计的经典工具,可以轻松地将相应的预测与所制定的预言关联起来,从而得出相关的置信区间。
在下图中,我们可以看到两个例子,用来理解预测与预言之间的区别:

预测中遇到的困难源于未来的不确定性,而在预期的时刻,未来尚未确定。预测中不确定性元素的强大相关性意味着概率计算工具在制定良好预测时至关重要。
在制定预测时,需要观察一些方面,如下所示:
-
预测的性质可以是定性的或定量的,但通常在复杂现象中,两者的方面都会出现。
-
预测对象可以是以连续性表现出来的现象的未来值,或者是现象发生的时间,或者是未来将发生事件的方式和特征。
-
预测的时间范围通常被划分为短期、中期和长期。这个区分并不清晰和精确。通常,我们谈论短期预测时,结构条件保持不变。这是因为要预测的事件将在很大程度上由在预测时已经实施的行为和行动决定。而如果决定要预测事件的基本条件仍然基本不确定时,我们则称之为长期预测。
-
最后,关于维度,预测可以仅涉及一个现象(单变量预测),或者同时涉及更多相关现象(多变量预测)。在这种情况下,它可以基于因果关系,通过这些关系,现象的行为可能会在一定的时间滞后后,决定其他现象的趋势(因果预测)。
风险和不确定性在预测制定中至关重要。事实上,最好标明与预测相关的不确定性程度。无论如何,数据必须更新,以便预测尽可能准确。
与统计预测概念相关的三个术语是:对象、目的和方法。让我们尝试理解统计预测的含义。统计预测适用于概念上定义的现象,以便进行客观测量。因此,现象和测量方法必须明确和定义,并且在整个调查过程中保持不变。预测的目的是研究现象的未来表现,这些表现由过去所发生的结构性稳定性决定。最后,预测方法代表了使用与随机过程相关的概率计算发展的数学模型,一方面,另一方面是统计推理的范式和在不确定条件下的决策理论原则。现在,让我们通过分析实际例子来理解如何使用不同的方法。
预测方法
预测方法的差异主要基于所使用决策的特征和目标。时间范围的长度、广泛历史数据库的可用性与均质性,以及预测所涉及产品的特征(如生命周期阶段)是影响方法选择的一些因素。
从本质上讲,预测方法分为两大类——定性和定量。在下图中,我们可以看到这两类方法的示例:

在接下来的章节中,我们将讨论这两种方法。我们将分析具体案例,理解这种分类的依据。
定性方法
定性预测方法用于根据过去的数据预测未来的数据。当过去的数值数据可用,并且合理假设数据中的某些模式应当延续时,便采用这些方法。这些方法通常应用于短期或中期决策。因此,定性方法主要依赖于判断,因此依赖于消费者和专家的意见与判断。当定量信息有限或不存在时,但有足够的定性信息时,便采用这些方法。
以下项目列出了一些定性方法的应用示例:
-
销售部门评估:每位销售代表估计其所在区域下一个时期的未来需求。该方法的假设是,最接近客户的人比其他任何人更了解客户的未来需求。然后,这些信息会被汇总,得出每个地理区域或产品系列的全球预测。
-
市场调查:公司通常会寻求专门从事市场调查的公司来进行此类预测。信息通常直接来自客户,或者更常见的是来自他们的代表性样本。然而,这种调查主要用于寻找新想法、了解客户是否喜欢或不喜欢现有产品、查找某个产品的最受欢迎品牌等等。
定量方法
当定量信息充分可用时,我们谈论定量方法。它们也用于根据过去的数据预测未来的数据。这些方法通常应用于短期或中期决策。
定量预测方法的示例包括以下内容:
-
时间序列:待预测的现象被视为一个黑箱,因为它并不试图识别可能影响它的现象。该方法的目标是识别现象的过去演变,并通过外推过去的数据来做出预测。换句话说,待预测的现象是相对于时间进行建模的,而不是相对于某个解释性变量(考虑销售趋势、国内生产总值(GDP)趋势等)。
-
解释性方法:假设待预测的变量可以与一个或多个独立变量或解释性变量相关联。例如,一个家庭的消费品需求取决于其收入和商品的年龄。
这样的预测技术采用回归方法,因此,分析的主要阶段是指定并估计一个模型,该模型将待预测的变量(响应变量)与解释变量(例如,广告和/或价格促销对销售的影响)关联起来。
这些方法可以用于以下假设:
-
关于现象过去演变的足够信息是可用的。
-
这些信息是可以量化的。
-
可以假设过去演变的特征在未来仍然存在,以便做出预测。
最终,定量方法在有足够的定量信息可用时被使用。
在接下来的部分,我们将介绍蒙特卡罗方法,特别是我们将重点介绍这些方法在解决强化学习问题中的应用。我们还将探讨使用这些方法进行预测和控制意味着什么。
理解蒙特卡罗方法
蒙特卡罗方法用于估计价值函数并发现优秀策略,不需要环境模型的存在。这些方法可以仅通过代理的经验来进行学习,或通过从代理与环境交互中获得的状态序列、动作和奖励样本来进行学习。经验可以通过代理在学习过程中的获取,也可以通过先前填充的数据集来模拟。在线学习中获得经验的可能性非常有趣,因为它使得即使在没有事先了解环境动态的情况下,也能获得优秀的行为。即使是通过已经填充的经验数据集进行学习,也可以很有趣,因为如果与在线学习相结合,它使得由他人经验所引发的自动策略改进成为可能。
为了解决强化学习问题,蒙特卡洛方法通过基于过去回合中获得的奖励总和(平均值)来估计价值函数。这假设经验被分为回合,并且每个回合由有限数量的过渡组成。这是因为在蒙特卡洛方法中,策略更新和价值函数估计发生在回合完成之后。事实上,蒙特卡洛方法是通过迭代估计策略和价值函数的。然而,在这种情况下,每次迭代周期相当于完成一个回合。因此,策略更新和价值函数估计是按回合进行的,正如我们刚才所说的。
蒙特卡洛方法通过使用示例回报来获取最佳策略。因此,一个能够生成这些示例过渡的环境模型就足够了。与动态规划不同,蒙特卡洛方法不需要知道所有可能过渡的概率。在许多情况下,实际上很容易生成满足期望概率分布的样本,而显式表达所有概率分布是不可行的。这些算法模拟一个称为“回合”的示例序列,并基于观察值、更新值和策略估计进行运算。在经过足够多的回合迭代后,所得结果显示出令人满意的准确性。
与基于动态规划的算法相比,蒙特卡洛算法不需要完整的系统模型。然而,它们提供了在每次仿真结束时才更新价值和策略的可能性,而不像动态规划算法那样在每一步都更新估计。
现在是时候理解使用蒙特卡洛方法进行预测和控制的意义了。
蒙特卡洛预测方法
蒙特卡洛预测用于估计价值函数。在这种情况下,给定策略下,从任何给定状态开始的预期总奖励将被预测。该过程遵循以下流程:
-
给出策略
-
计算价值函数
你会回忆到,策略定义了代理在当前状态下的行为方式,从而以特定方式表示在特定状态下采取某一动作的概率。
预测任务要求提供策略,目的是衡量其表现,即预测在给定状态下由策略提供的总奖励,假设策略是预先设定的。
蒙特卡洛控制方法
蒙特卡洛控制用于优化价值函数,以使价值函数比估计更准确。在控制中,策略并非固定,目标是找到最优策略。在这种情况下,我们的目标是找到能够最大化每个给定状态下总奖励的策略。
控制算法也适用于预测,以不同的方式预测动作的值,并调整策略以在每个阶段选择最佳动作。因此,这些算法的输出提供了一个近似最优策略和遵循该策略的未来预期奖励。
在接下来的部分中,我们将了解如何区分模型无关和模型基础的算法。
接近模型无关算法
在上一节中,理解蒙特卡罗方法,我们说过蒙特卡罗方法不需要环境模型来估计值函数或发现优秀的策略。这意味着蒙特卡罗是模型无关的:不需要马尔可夫决策过程(MDP)转移或奖励的知识。因此,我们之前不需要对环境进行建模,但在与环境的交互中会收集必要的信息(在线学习)。蒙特卡罗方法直接从经验中学习,其中一段经验是一系列元组(状态、动作、奖励和下一个状态)。
在下面的截图中,我们可以看到模型基础和模型无关方法的比较:

模型无关方法可以应用于许多不需要任何环境模型的强化学习问题。许多模型无关方法尝试学习值函数并从中推断出最优策略,或者直接在策略参数空间中搜索最优策略。这些方法也可以分类为在策略方法或离策略方法。在策略方法使用当前策略生成动作,并更新策略本身,而离策略方法则使用不同的探索策略生成动作,并相对于更新的策略。两种方法——模型无关和模型基础的差异是什么?在接下来的部分中,我们将尝试突出这些差异,以便选择解决问题的正确方法。
模型无关与模型基础
在这一部分中,我们将尝试通过强化学习来澄清这两种方法的差异,以解决问题。在这些问题中,代理人不知道系统的所有元素,这使他无法计划解决方案。特别是,代理人不知道环境将如何响应他的行动而发生变化。这是因为转移函数T是未知的。此外,他甚至不知道他的行动将获得什么即时奖励。这是因为他还没有注意到奖励函数。代理人将不得不通过尝试行动、观察回答,并在某种程度上找到一个好的策略,以获得可能的最佳最终奖励。
接下来出现一个问题:如果代理既不知道转移函数,也不知道奖励函数,那么他如何推导出好的策略呢?为此,可以采取两种方法:基于模型的方法和无模型的方法。
在第一种方法(基于模型的方法)中,代理从其观察到的环境功能中学习一个模型,然后利用该模型推导出解决方案。例如,如果代理处于状态 s1,并执行一个动作 a1,他可以观察到环境转移将其带到状态 s2,从而获得奖励 r2。此信息可以用于更新转移矩阵 T(s2 | s1, a1)和 R(s1, a1)的评估。可以使用监督学习范式执行此更新。一旦代理充分建模了环境,他就可以使用该模型来找到一个策略。采用这种方法的算法被称为基于模型的方法。
第二种方法不涉及学习环境模型来找到一个好的策略。最经典的例子之一是 Q 学习,我们将在第七章中详细分析,时间差学习。该算法直接估计每个状态下每个动作的最优值,从中可以通过选择当前状态下值最高的动作来推导出策略。由于这些方法不学习环境模型,它们被称为无模型方法。
最终,如果在学习之后,代理能够在采取任何行动之前预测下一个状态和奖励,那么我们的算法就是基于模型的。否则,它就是一个无模型算法。
现在,让我们看看蒙特卡洛方法中的动作值是如何更新的。
动作值的估计
一般来说,蒙特卡洛方法依赖于重复的随机采样来获得数值结果。为此,它们使用随机性来解决确定性问题。在我们的例子中,我们将使用状态和动作-状态对的随机采样,查看奖励,然后以迭代的方式回顾策略。随着我们探索每个可能的动作-状态对,过程的迭代将收敛到最优策略。
例如,我们可以使用以下程序:
-
我们将正确的动作赋予奖励+1,错误的动作赋予奖励-1,平局赋予奖励 0。
-
我们建立一个表格,其中每个键对应于一个特定的状态-动作对,每个值是该对的值。这代表了在该状态下执行该动作所获得的平均奖励。
为了解决强化学习问题,蒙特卡罗方法通过估计基于过去回合中平均获得的总奖励的值函数。这个假设是经验被分为若干回合,并且每个回合包含有限数量的转换。因为在蒙特卡罗方法中,新的值估计和策略修改发生在每个回合结束后。蒙特卡罗方法通过迭代方式估计策略和值函数。然而,在这种情况下,每个迭代周期相当于完成一次回合——新的策略和值函数估计是在每个回合后逐步进行的,如下图所示:

工作流包括对经验回合的采样以及在每个回合结束时更新估计值。由于每个回合中有许多随机决策,这些方法的方差很高,尽管它们是无偏的。
你可能会回忆起两个过程,分别叫做策略评估和策略改进:
-
策略评估算法通过应用迭代方法解决贝尔曼方程。由于我们只能在 k → ∞时保证收敛性,因此我们必须通过设置停止条件来获得良好的近似值。
-
策略改进算法基于当前的值来改进策略。
正如我们所说,新的策略和值函数估计是在每个回合后逐步进行的;因此,策略仅在回合结束时更新。
以下代码块显示了蒙特卡罗策略评估的伪代码:
Initialize
arbitrary policy π
arbitrary state-value function
Repeat
generate episode using π
for each state s in episode
the received reward R is added to the set of
reinforcers obtained so far
estimate the value function on the basis on the average
of the total sum of rewards obtained
通常,蒙特卡罗一词用于描述涉及随机组件的估计方法。在这里,蒙特卡罗指的是基于总奖励平均值的强化学习方法。与动态规划方法通过计算每个状态的值不同,蒙特卡罗方法计算每个状态-动作对的值,因为在没有模型的情况下,只有状态值不足以决定在某个状态下执行哪个动作最优。
在详细分析了蒙特卡罗方法如何基于强化学习解决问题之后,接下来我们将查看一个实际案例。为此,我们将使用一个非常流行的游戏——二十一点。我们将看到如何使用蒙特卡罗方法预测最佳游戏策略。
使用蒙特卡罗方法进行二十一点策略预测
二十一点是一种在庄家和玩家之间进行的纸牌游戏。玩家如果得分超过庄家且不超过 21 点,则获胜;而得分超过 21 点的玩家爆牌并输掉游戏。二十一点通常使用由两副法式扑克牌组成的牌组(104 张牌)。在游戏中,A 牌可以算作 11 点或 1 点,图牌算作 10 点,其它牌按照面值计算。种子牌不具备影响或价值。点数的计算通过简单的算术运算来完成。
一旦玩家下注,庄家从左到右依次为每位玩家发放一张未盖面的牌,在每个位置上发一张,最后一张发给自己。然后庄家进行第二轮发牌,仍然不发给自己。一旦发牌结束,庄家按顺序读取每位玩家的得分,并邀请他们展示自己的牌:他们可以选择要牌(hit)或停牌(stick),完全由他们决定。如果某个玩家的点数超过 21 点,他就会输,庄家将赢得该玩家的赌注。一旦玩家们确定了自己的得分,庄家根据一个简单的规则进行游戏;也就是说,如果庄家的点数低于 17 点,他必须继续要牌,一旦得分达到或超过 17 点,他就必须停牌。如果庄家的点数超过 21 点,庄家爆牌,并且必须支付桌面上所有剩余的赌注。一旦所有得分都确定,庄家会将自己的得分与其他玩家进行比较,支付比自己高的组合,收取低于自己的赌注,并对平局的赌注不做处理。赢钱的赌注按面值支付。
二十一点游戏作为一个 MDP
二十一点可以视为一个 MDP(马尔可夫决策过程),因为玩家的状态可以通过他们手中的牌的点数来定义,而与玩家自己手中的牌无关。庄家的状态完全由他面前显示的单张牌的点数决定,因此整个游戏的最终状态由玩家和庄家的状态共同决定。最后,游戏的下一状态完全由当前状态和玩家的行动以随机方式定义。
我们回顾一下,问题可以被定义为 MDP,如果下一状态仅仅是当前状态和执行的动作的随机函数。此外,MDP 适用于那些决策空间有限且离散,结果不确定,且终止状态和相对奖赏明确的情况。MDP 的解决方案为我们提供了基于一个旨在最大化每个可能状态的奖励的过程,来执行的最优行动。
在接下来的章节中,我们将逐行解释代码:
- 我们将开始定义可用的操作:
HIT <- 1
STICK <- 2
让我们来看一下要牌和停牌的含义:
-
HIT:从庄家那里再拿一张牌。 -
STICK:不再拿牌。
- 现在,我们需要模拟从牌堆中发牌的过程,这个操作由庄家执行,庄家会给每个玩家发两张牌:
BJCard <- function()
return(sample(10,1))
sample()函数从传递的元素中提取指定大小的样本,可以选择有放回(1)或无放回(0)抽样。
- 现在,让我们开始随机生成一个初始状态:
StateInput <- function () {
return ( c(sample(10, 1), sample(10, 1), 0))
}
因此,初始状态以及一般的所有可能状态,由具有这里指定的三个元素的向量表示:
-
庄家牌(sample(10, 1)):一个介于 1 到 10 之间的值
-
玩家手牌值(sample(10, 1)):玩家卡牌值的总和
-
终止状态(0):一个二进制值(0-1),告诉我们手牌是否结束
状态和奖励更新
在接下来的代码块中,我们将分析并更新状态以及从环境中返回的奖励。
- 现在,我们将创建一个函数来执行过程的单步操作(
StepFunc),根据传递的状态(s)和动作(a),返回新的状态和奖励:
StepFunc <- function (s, a) {
if(s[3]==1)
return(list(s, 0))
NewState <- s
BJReward <- 0
第一个检查项是验证我们是否处于终止状态(手牌结束),如果是,则退出循环,返回当前状态和奖励为零。
- 让我们检查传递的动作:
if(a==1) {
NewState[2] <- s[2] + BJCard()
if (NewState[2]>21 || NewState[2]<1) {
NewState[3] <- 1
BJReward <- -1
}
}
如果要执行的动作是HIT,则会发现一张新卡,并更新状态和奖励。
- 让我们看看如果过去的动作是
STICK会发生什么:
else {
NewState[3] <- 1
DealerWork <- FALSE
DealerSum <- s[1]
while(!DealerWork) {
DealerSum <- DealerSum + BJCard()
if (DealerSum>21) {
DealerWork <- TRUE
BJReward <- 1
} else if (DealerSum >= 17) {
DealerWork <- TRUE
if(DealerSum==s[2])
BJReward <- 0
else
BJReward <- 2*as.integer(DealerSum<s[2])-1
}
}
}
return(list(NewState, BJReward))
}
然后,手牌传给庄家,庄家执行他的游戏。你开始通过将玩家的终止状态更新为 1 来开始。然后,庄家状态(DealerWork <- FALSE)及其当前分数被更新(DealerSum <- s [1])。从这一点开始,使用while循环运行庄家的游戏。首先,发一张新卡。这时,使用第一个IF值检查庄家是否爆掉(DealerSum > 21)。如果爆掉了,游戏以玩家胜利结束。庄家游戏的状态更新(DealerWork <- TRUE),并且总奖励BJReward <- 1。如果不是,若DealerSum >= 17,则庄家停止游戏并检查玩家状态。如果分数相等(DealerSum == s [2]),则游戏以平局结束(BJReward <- 0);否则,如果庄家的分数大于玩家的分数,玩家的BJReward = -1,玩家失败。如果庄家的分数小于玩家的分数,则玩家的BJReward = 1,玩家胜利。最后,如前所述,函数返回更新后的状态和步骤的最终奖励。
策略预测
现在是预测成功游戏最佳策略的时候了:
- 在定义了更新状态和奖励的函数后,现在是定义策略的时候了:
ActionsEpsValGreedy <- function(s, QFunc, EpsVal) {
if(runif(1)<EpsVal)
return(sample(1:2, 1))
else
return(which.max(QFunc[s[1],s[2],]))
}
为了定义策略,创建了一个ActionsEpsValGreedy()函数。该函数接受以下输入:
-
s: 状态 -
QFunc: 动作值函数 -
EpsVAl: epsilon 的数值
这返回要遵循的动作。
正如我们在第四章中所说的,多臂老丨虎丨机模型,在ε-贪婪方法中,我们假设以ε的概率选择不同的行动。这个行动是在 n 个可能的行动中均匀选择的。通过这种方式,我们引入了一种探索的元素,从而提高了性能。然而,如果两个行动之间的 Q 值差异非常小,那么该算法也会选择 Q 值更高的那个行动。
- 现在,让我们加载
foreach库:
library("foreach")
该包处理foreach循环结构。foreach命令允许你在不使用显式计数器的情况下遍历集合中的项。我们建议使用该包的返回值,而不是它的副作用。以这种方式使用时,它类似于标准的lapply函数,但不需要评估函数。因此,使用foreach可以方便地并行执行循环。
- 最后,我们可以定义
MontecarloFunc函数,它将指导我们解决问题。此函数接受以下变量作为输入:
NumEpisode:要播放的回合数
MontecarloFunc()函数返回以下值:
-
QFunc:更新的行动值函数 -
N:更新后的状态-行动访问次数
- 让我们详细分析一下:
MontecarloFunc <- function(NumEpisode){
QFunc <- array(0, dim=c(10, 21, 2))
N <- array(0, c(10,21,2))
N0=100
一旦传入输入,以下变量将被初始化:
-
QFunc:作为数组的行动值函数,包含以下变量:所有可能的卡牌值(dim=10)、所有可能的和值(dim=21)以及所有可能的行动(dim=2) -
N:状态-行动访问次数 -
N0:N 的偏移量
- 然后,我们将定义一个策略:
policy <- function(s) {
ActionsEpsValGreedy(s, QFunc, N0/(sum(N[s[1], s[2],])+N0))
}
- 我们现在将使用一个循环执行所有必要的回合,以计算最佳策略:
foreach(i=1:NumEpisode) %do% {
s <- StateInput()
SumReturns <- 0
N.episode <- array(0, c(10,21,2))
- 现在,让我们为每个回合玩一场游戏:
while(s[3]==0) {
a <- policy(s)
N.episode[s[1], s[2], a] <- N.episode[s[1], s[2], a] + 1
StateReward <- StepFunc(s, a)
s <- StateReward[[1]]
SumReturns <- SumReturns + StateReward[[2]]
}
在游戏的终止状态为 0(游戏进行中)之前,它执行以下操作:
-
根据定义的策略选择一个行动来执行
-
增加访问计数器
-
通过调用
StepFunc()函数执行一个步骤 -
更新你的奖励
- 做完这些后,我们继续更新 Q 和 N:
IndexValue <- which(N.episode!=0)
N[IndexValue] <- (N[IndexValue]+N.episode[IndexValue])
QFunc[IndexValue] <- QFunc[IndexValue] + (SumReturns-QFunc[IndexValue]) / N[IndexValue]
}
上述代码块包括了整个过程的关键元素——Q 函数的更新模式。在这种情况下,采用了增量方法。
- 事实上,函数q是使用以下函数更新的:

这里:
-
G:这是奖励的总和。
-
Q:这是行动值函数。
-
N:这是状态-行动访问次数。
- 最后,返回以下结果:
return(list(QFunc=QFunc, N=N))
}
- 定义了所有必要的函数后,到了运行模拟的时候:
MCModel <- MontecarloFunc(NumEpisode=100000)
我们只需传递获得良好预期的最佳策略所需的回合数。
- 此时,为了分析结果,我们可以绘制图形。然而,需要适当格式化行动值函数:
StateValueFunc <- apply(MCModel$QFunc, MARGIN=c(1,2), FUN=max)
为此,我们使用了apply()函数,该函数返回一个向量或数组,或者是通过将函数应用于数组或矩阵的边界所得到的值列表。
- 现在,我们可以绘制一张图表:
persp(StateValueFunc, x=1:10, y=1:21, theta=50, phi=35, d=1.9, expand=0.3, border=NULL, ticktype="detailed",
shade=0.6, xlab="Dealer exposed card", ylab="Player sum", zlab="Value", nticks=10)
使用了persp()函数。此函数绘制了一个在 x-y 平面上表面的透视图。
绘制了以下图表:

通过这种方式,我们对价值函数有了良好的估计。
总结
在本章中,我们探讨了蒙特卡罗方法的基本概念。蒙特卡罗方法通过将问题的解决方案表示为假设总体的一个参数,并通过随机数序列获取的总体样本来估算这个参数。之后,我们强调了这种技术为我们提供的不同方法之间的差异。蒙特卡罗预测用于估算价值函数,而蒙特卡罗控制用于优化价值函数,使价值函数比估算值更准确。
然后我们继续分析基于无模型方法的算法与基于有模型方法的算法之间的区别。此外,我们还逐步分析了进行蒙特卡罗策略评估的过程。最后,作为学习到的概念的实际案例,进行了涉及蒙特卡罗方法的二十一点策略预测。
在下一章中,我们将学习不同类型的时序差分(TD)学习算法。你将了解如何使用 TD 算法预测系统的未来行为,并学习 Q 学习算法的基本概念。你还将学习如何使用当前最佳策略估计通过 Q 学习算法生成系统行为。
第七章:时序差分学习
时序差分 (TD) 学习算法基于减少代理在不同时间做出的估计之间的差异。它是 蒙特卡洛 (MC) 方法和 动态规划 (DP) 思想的结合。该算法可以直接从原始数据中学习,而无需环境动态模型(就像 MC)。更新估计部分依赖于其他已学得的估计,而无需等待结果(自举,就像 DP)。在本章中,我们将学习如何使用 TD 学习算法来解决车辆路径规划问题。
本章将涵盖以下主题:
-
理解 TD 方法
-
介绍图论及其在 R 中的实现
-
将 TD 方法应用于车辆路径规划问题
本章结束时,你将学习到不同类型的 TD 学习算法,并了解如何使用它们来预测系统的未来行为。我们将学习 Q 学习算法的基本概念,并使用它们通过当前最优策略估计生成系统行为。最后,我们将区分 SARSA 和 Q 学习方法。
查看以下视频,看看代码的实际操作:
理解 TD 方法
TD 方法基于减少代理在不同时间做出的估计之间的差异。Q 学习是一个 TD 算法,我们将在接下来的部分学习,它基于相邻时刻状态之间的差异。TD 方法更加通用,可能会考虑更远的时刻和状态。
TD 方法结合了 MC 方法和动态规划(DP)的思想,正如你可能记得的那样,可以总结如下:
-
MC 方法使我们能够根据获得结果的平均值来解决强化学习问题。
-
DP 代表一组算法,这些算法可以在给定环境的完美模型(MDP)下,用于计算最优策略。
一方面,TD 方法继承了从与系统交互中积累的经验中直接学习的思想,这与蒙特卡洛(MC)方法类似,而不需要系统本身的动态信息。另一方面,它们继承了动态规划(DP)方法的思想,即基于其他状态的估计来更新某一状态下的函数估计(自举)。TD 方法适合在没有动态环境模型的情况下进行学习。如果时间步长足够小,或者随着时间的推移减少,你需要通过一个固定策略来收敛。
这些方法与其他技术的不同之处在于,它们试图最小化连续时间预测的误差。为了实现这一目标,这些方法将价值函数的更新重写为贝尔曼方程的形式,从而通过自举法提高预测精度。在这里,每次更新步骤都会减少预测的方差。为了实现更新的反向传播并节省内存,采用了资格向量。示例轨迹的使用效率更高,从而获得了良好的学习速率。
基于时间差异的方法使我们能够通过根据向下一个状态过渡的结果来更新价值函数,从而管理控制问题(即寻找最优策略)。在每一步中,函数Q(行动-价值函数)基于它为下一个状态-动作对所假定的值以及通过以下方程获得的奖励来更新:

通过采用一步前瞻,很明显,也可以使用两步公式,如下所示:

术语“前瞻”指的是一种试图预测在评估某个值时选择一个分支变量的效果的过程。该过程有以下目的:选择一个变量稍后进行评估,并评估分配给它的值的顺序。
更一般地说,通过n步前瞻,我们得到以下公式:

基于时间差异的不同类型算法的一个特征是选择行动的方法。有“在策略”方法,其中更新基于由所选策略确定的行动的结果;还有“离策略”方法,在这种方法中,可以通过假设的行动评估不同的策略,这些假设的行动实际上并未执行。与“在策略”方法不同,后者可以将探索问题与控制问题分离,且学习策略在学习阶段并不一定被应用。
在接下来的章节中,我们将通过两种方法学习如何实现时间差异方法:SARSA 和 Q 学习。
SARSA
正如我们在第一章《使用 R 进行强化学习概述》中所预期的,SARSA 算法实现了一种在策略的时间差异方法,其中,行动-价值函数(Q)的更新是基于从状态s = s (t)过渡到状态s' = s (t + 1)的结果,并且该过渡是基于所选策略π (s, a)采取的行动a (t)。
一些策略总是选择提供最大奖励的行动,而非确定性策略(如ε-贪心、ε-软策略或软最大策略)则确保在学习阶段有一定的探索成分。
在 SARSA 中,必须估算动作价值函数 𝑞 (𝑠, 𝑎),因为在没有环境模型的情况下,状态 𝑣 (𝑠)(价值函数)的总值不足以让策略根据给定的状态判断执行哪个动作是最好的。然而,在这种情况下,值是通过遵循贝尔曼方程,并考虑状态-动作对代替状态,逐步估算的。
由于其在策略中的特性,SARSA 根据π策略的行为估算动作价值函数,同时根据从动作价值函数中更新的估算值,修改策略的贪婪行为。SARSA 的收敛性,和所有 TD 方法一样,依赖于策略的性质。
以下代码块展示了 SARSA 算法的伪代码:
Initialize
arbitrary action-value function
Repeat (for each episode)
Initialize s
choose a from s using policy from action-value function
Repeat (for each step in episode)
take action a
observe r, s'
choose a' from s' using policy from action-value function
update action-value function
update s,a
动作价值函数的update规则使用所有五个元素(s[t],a[t],r[t + 1],s[t + 1],以及 a[t + 1]),因此被称为状态-动作-奖励-状态-动作 (SARSA)。
Q-learning
Q-learning 是最常用的强化学习算法之一。其原因在于它能够比较可用动作的期望效用,而不需要环境模型。得益于这项技术,可以在完成的 MDP 中为每个给定的状态找到最优动作。
强化学习问题的一个通用解决方案是在学习过程中估算评估函数。这个函数必须能够通过奖励的总和评估特定策略的便利性或其他方面。事实上,Q-learning 试图最大化 Q 函数(动作价值函数)的值,Q 函数表示我们在状态 s 下执行动作 a 时的最大折扣未来奖励。
Q-learning 和 SARSA 一样,逐步估算函数值 𝑞 (𝑠, 𝑎),在环境的每个步骤中更新状态-动作对的值,遵循更新 TD 方法估算值的通用公式逻辑。与 SARSA 不同,Q-learning 具有离策略特性。也就是说,虽然策略是根据 𝑞 (𝑠, 𝑎) 估算的值进行改进的,但价值函数更新估算值时遵循严格的贪婪次级策略:给定一个状态,选择的动作总是那个能够最大化值 max𝑞 (𝑠, 𝑎) 的动作。然而,π策略在估算值方面起着重要作用,因为要访问和更新的状态-动作对是通过它来决定的。
以下代码块展示了 Q-learning 算法的伪代码:
Initialize
arbitrary action-value function
Repeat (for each episode)
Initialize s
choose a from s using policy from action-value function
Repeat (for each step in episode)
take action a
observe r, s'
update action-value function
update s
Q-learning 使用一个表格来存储每个状态-动作对。在每个步骤中,智能体观察当前环境的状态,并使用π策略选择并执行动作。通过执行该动作,智能体获得奖励 𝑅[𝑡+1],以及新的状态 𝑆[𝑡+1]。此时,智能体可以计算 𝑄 (s[𝑡], a[𝑡]),并更新估算值。
在接下来的部分中,将给出图论的基础,并说明如何在 R 中处理这项技术。
引入图论并在 R 中实现
图是广泛应用于优化问题的数据结构。图由顶点和边的结构表示。顶点可以是从中出发的不同选择(即边)。通常,图用于清晰地表示网络:顶点代表独立的计算机、路口或公交车站,边则是电气连接或道路。边可以以任何可能的方式连接顶点。
图论是数学的一个分支,它允许你描述对象集合及其关系;由莱昂哈德·欧拉在 1700 年发明。
图通常用G = (V, E)的紧凑形式表示,其中V表示顶点集合,E表示构成图的边集合。顶点的数量是|V|,边的数量是|E|。图的顶点数,或其子部分的顶点数,显然是定义其维度的基本量;边的数量和分布描述了它们的连接性。
有不同类型的边:我们讨论的是无向边,其边没有方向,而与有向边相比。有向边称为弧,相关的图称为有向图。例如,无向边用于表示具有同步链路的数据传输计算机网络(如下图所示),而有向图则可以表示道路网络,允许表示双向和单向道路。
下图表示一个简单的图:

如果我们能够从任何给定的顶点到达图中的所有其他顶点,我们就说这个图是连通的。如果每条边都关联一个权重,并且通常由权重函数(w)定义,则图是加权图。权重可以视为两个节点之间的成本或距离。成本可能取决于流量通过边的规律。在这个意义上,权重函数w可以是线性的,也可以不是,并且取决于通过边的流量(非拥塞网络)或周围边的流量(拥塞网络)。
顶点的特征是其度数,度数等于以该顶点为终点的边的数量。根据度数,顶点如下所示:
-
度数为 0 的顶点称为孤立顶点。
-
度数为 1 的顶点称为叶子顶点。
下图展示了一个按度数标记的图:

在有向图中,我们可以区分出度(即出发边的数量)和入度(即进入边的数量)。基于这一假设,入度为零的顶点称为源顶点,出度为零的顶点称为汇顶点。
最后,简约顶点是其邻居形成团体的顶点:每两个邻居都是相邻的。通用顶点是与图中所有其他顶点相邻的顶点。
表示图的方法有多种,例如以下几种:
-
图形表示(如前图所示)
-
邻接矩阵
-
顶点 V 和弧 E 的列表
表示图的第一种方法通过实际示例进行了清晰的介绍(见前面的图示)。在图形表示中,圆圈表示顶点,线条表示两个顶点之间的连接,如果它们相连。若该连接具有方向性,则通过添加箭头来表示。在接下来的部分,我们将分析表示图的其他两种方法。
邻接矩阵
到目前为止,我们已经通过顶点和边来表示图。当顶点数量较少时,这种表示方法是最好的,因为它使我们能够直观地分析图的结构。当顶点数量变大时,图形表示变得混乱。在这种情况下,通过邻接矩阵表示图会更好。邻接矩阵或连接矩阵是图表示中常用的数据结构,广泛应用于图操作算法的设计以及图的计算机表示中。如果它是稀疏矩阵,使用邻接表优于使用矩阵。
给定任何图,其邻接矩阵由一个方形二进制矩阵组成,矩阵的行和列是图中顶点的名称。在矩阵的 (i, j) 位置上,如果图中存在一条从顶点 i 到顶点 j 的边,则该位置为 1;否则为 0。在无向图的表示中,矩阵相对于主对角线是对称的。例如,查看以下图示所表示的图:

前面的图可以通过以下邻接矩阵来表示:

如预期所示,矩阵相对于主对角线是对称的,表示图是无向的。如果矩阵中不是 1 而是其他数字,那么这些数字表示分配给每个连接(边)的权重。在这种情况下,矩阵被称为马尔可夫矩阵,因为它适用于马尔可夫过程。例如,如果图的顶点集表示地图上的一系列点,那么边的权重可以解释为它们连接的点之间的距离。
这个矩阵的一个基本特点是,它可以计算从节点i到节点j的路径数,这些路径必须经过n个顶点。为了得到这些信息,只需将矩阵的n次方计算出来,并查看在i, j位置上的数字即可。另一种表示图的方式是使用邻接列表。我们来看一下。
邻接列表
邻接列表是图在内存中的一种表示方式。这可能是最简单的实现方式,尽管通常来说,它在占用空间方面不是最有效的。
让我们分析一个简单的图;每个顶点旁边列出的是其相邻顶点的列表。表示方法的基本思想是,每个顶点Vi都与一个包含所有与其相连的顶点Vj的列表相关联,即存在一条从Vi到Vj的边。
假设你记住了所有类型为(Vi, L)的顶点对,其中L是顶点Vi的邻接列表,那么我们就能得到图的唯一描述。或者,如果你决定对邻接列表进行排序,那么就不需要显式地存储顶点了。
让我们举个例子——我们将使用上一节中采用的相同图形,图示如下:

基于前面提到的内容,我们将构建邻接列表。上图中的图可以表示如下:
| 1 | 相邻于 | 2,3 |
|---|---|---|
| 2 | 相邻于 | 1,3 |
| 3 | 相邻于 | 1,2,4 |
| 4 | 相邻于 | 3 |
邻接列表由对组成。每个图中的顶点都有一对。对的第一个元素是正在分析的顶点,第二个元素是由所有与它相邻的顶点组成的集合,这些顶点通过一条边与之相连。
假设我们有一个包含n个顶点和m条边(有向)的图,且假设邻接列表已按顺序记忆(为了避免显式记忆索引),那么每条边会出现在一个且仅一个邻接列表中,并且它会以指向的顶点编号的形式出现。因此,需要记住总共m个小于等于n的数字,总的成本为mlog2n。
对于无向图来说,没有明显的方法来优化这种表示法;每条弧必须在连接的两个顶点的邻接列表中都进行记忆,从而降低了效率。如果图是有向图,我们仍然需要一种有效的方法来知道指向某个顶点的弧。在这种情况下,将每个顶点关联两个列表是比较方便的:一个是进入弧的列表,另一个是出去弧的列表。
在时间效率方面,邻接列表的表示方式在访问和插入操作中表现得相当不错,主要操作在O(n)时间内完成。到目前为止,我们已经分析了图形表示的技术。接下来,让我们学习如何在 R 环境中使用这些技术。
在 R 中处理图形
在 R 中,节点集(V)和弧集(E)是不同类型的数据结构。对于V,一旦我们为每个节点分配唯一标识符,就可以无歧义地访问每个节点。因此,它就像是在说,托管节点属性的数据结构是一维的,因此是一个向量。
相反,弧集(节点之间的链接)E的数据结构不能是向量,它不表示单一对象的特征,而是表示对象对之间的关系(在这种情况下是节点对之间的关系)。因此,如果例如在V(节点集)中有 10 个节点,那么E的维度将是 10 × 10,即所有可能节点对之间的关系。最终,E有两个维度,因此它不是向量,而是矩阵。
在矩阵E中,我们有多行等于V中节点的数量,多列等于V中节点的数量。这表示在邻接矩阵部分中详细分析的邻接矩阵。
要在 R 中处理图形,我们可以使用igraph包——该包包含用于简单图形和网络分析的函数。它能够很好地处理大型图形,并提供生成随机图和规则图、图形可视化、中心性方法等功能。
以下表格提供了有关该包的一些信息:
| 包 | igraph |
|---|---|
| 日期 | 2019-22-04 |
| 版本 | 1.2.4.1 |
| 标题 | 网络分析与可视化 |
| 维护者 | Gábor Csárdi |
为了开始使用可用工具,我们将分析一个简单的示例。假设我们有一个由四个节点和四条边组成的图。首先要做的是定义这四个节点之间的链接;为此,我们将使用graph函数(记得在安装后加载igraph库):
library(igraph)
Graph1 <- graph(edges=c(1,2, 2,3, 3, 1, 3,4), n=4, directed=F)
graph函数是graph.constructors方法的一部分,提供了创建图形的各种方法:空图、有给定边的图、从邻接矩阵构建的图、星形图、格状图、环形图和树形图。我们使用的方法是通过使用数值向量来定义边来定义图形,向量中的第一个元素到第二个元素为第一条边,第三个元素到第四个元素为第二条边,以此类推。
实际上,我们可以看到传入了四对值:第一对定义了节点 1 和节点 2 之间的连接,第二对定义了节点 2 和节点 3 之间的连接,第三对定义了节点 3 和节点 1 之间的连接,最后,第四对定义了节点 4 和节点 2 之间的连接。为了更好地理解图中节点之间的连接,我们将绘制该图:
plot(Graph1)
绘制了以下图形:

我们创建的图是一个具有特征的对象,可以按如下方式进行分析:
Graph1
返回的结果如下:
IGRAPH 143ffd1 U--- 4 4 --
+ edges from 143ffd1:
[1] 1--2 2--3 1--3 3--4
节点之间的边已指示。然后,我们计算节点 1 和节点 4 之间的最短路径:
get.shortest.paths(Graph1, 1, 4)
get.shortest.paths() 计算从源顶点到目标顶点的单一最短路径。该函数对于无权图使用广度优先搜索,对于有权图使用 Dijkstra 算法。在我们的例子中,由于添加了权重属性,因此使用了 Dijkstra 算法。
返回以下结果:
$vpath
$vpath[[1]]
+ 3/4 vertices, from 1b5d9f3:
[1] 1 3 4
现在,计算此路径上两点之间的距离:
distances(Graph1, 1, 4)
返回以下结果:
[1,] 2
目前我们所表示的图在识别两地之间最短路径方面的用途有限,这正是我们的目标。为了计算最佳路径,需要引入边权重的概念。在我们的例子中,我们可以将此属性视为两个节点之间路径长度的度量;通过这种方式,我们可以通过路径来评估两个节点之间的距离。为此,我们将使用以下方式来定义属性权重:
WeightsGraph1<- c(1,1,4,1)
E(Graph1)$weight <- WeightsGraph1
首先,我们通过一个向量定义了权重,确认了在图创建时定义的边的顺序。然后,我们将权重属性添加到先前创建的图中。现在,每条边都有了自己的长度。如果没有定义权重会怎样呢?简单地说,它们都会被设为 1;在这种情况下,最短路径将是节点数最少的路径。
现在,让我们重新计算节点 1 和节点 4 之间的最短路径:
get.shortest.paths(Graph1, 1, 4)
返回以下结果:
$vpath
$vpath[[1]]
+ 4/4 vertices, from 1b5d9f3:
[1] 1 2 3 4
我们可以看到,现在的路径涉及多个节点。我们将验证这两个节点之间的距离:
distances(Graph1, 1, 4)
返回以下结果:
[1,] 3
通过这种方式,我们验证了所指示的路径是最短路径,因为最长的连接已被避免。在下一节中,我们将看到如何使用 Dijkstra 算法找到最佳路径。
Dijkstra 算法
Dijkstra 算法用于解决从源节点 s 到所有节点的最短路径问题。该算法为节点维护一个标签 d(i),表示节点 i 最短路径长度的上限。
在每一步中,算法将 V 中的节点分成两组:一组是永久标记的节点,另一组是仍然是临时标记的节点。永久标记节点的距离表示从源节点到这些节点的最短路径距离,而临时标记的节点则包含一个值,该值可以大于或等于最短路径长度。
该算法的基本思想是从源节点开始,尝试永久标记后继节点。开始时,算法将源节点的距离值设为零,并将其他节点的距离初始化为一个任意高的值(按照惯例,我们将距离的初始值设为 d[i] = + ∞, ∀i ∈ V)。在每次迭代中,节点标签 i 是从源节点出发的路径中最小距离的值,该路径除了 i 之外只有永久标记的节点。算法选择那些临时标记的节点中标签值最低的节点,将其永久标记,并更新所有与之相邻节点的标签。当所有节点都被永久标记时,算法终止。
通过执行该算法,针对每个目标节点 v(属于 V),我们可以获得一个最短路径 p(从 s 到 v),并计算以下内容:
-
d [v]:节点 v 到源节点 s 的距离 p
-
π [v]: 节点 v 的前驱节点为 p
对于每个节点 v(属于 V)的初始化,我们将使用以下过程:
-
d [v] = ∞ 如果 v ≠ s,否则 d [s] = 0
-
π [v] = Ø
在执行过程中,我们使用泛化边 (u, v)(属于 E)的松弛技术来改善 d 的估算值。
边 (u, v) 的松弛操作,旨在评估是否可以通过将 u 作为 v 的前驱节点来改善当前的距离值 d [v],如果可以改善,则更新 d [v] 和 π [v]。该过程如下:
-
如果 d[v]> d[u] + w (u, v) 则
-
d[v] = d[u] + w (u, v)
-
π [v] = u
该算法基本上执行两个操作:节点选择操作和更新距离的操作。第一个操作在每一步选择标签值最低的节点;另一个操作验证条件 d[v]> d[u] + w(u, v),如果满足条件,则更新标签值,令 d[v] = d[u] + w (u, v)。
在接下来的部分中,我们将实现一种 TD 方法来解决一个实际应用问题。
将 TD 方法应用于车辆路径问题
给定一个加权图和一个指定的顶点 V,通常需要找出从一个节点到图中每个其他顶点的路径。识别连接两个或多个节点的路径是许多离散优化问题的子问题,并且在现实世界中有广泛的应用。
例如,考虑一个问题:在一张道路地图上标识两地之间的路线,其中顶点表示地点,边表示连接它们的道路。在这种情况下,每个代价都与道路的公里长度或覆盖该段道路的平均时间相关。如果我们想要识别的是最小总代价的路径,而非任意路径,那么所得到的问题被称为图中的最短路径问题。换句话说,图中两个顶点之间的最短路径是连接这两个顶点并最小化穿越每条边的代价之和的路径。
所以,让我们通过一个实际的例子来考虑——假设一位游客开车从罗马前往威尼斯。假设他手中有一张意大利地图,上面标出了各个城市之间的直连路径及其长度,游客如何找到最短的路径?
该系统可以通过一个图来示意,其中每个城市对应一个顶点,道路对应顶点之间的连接弧。你需要确定图中源顶点和目标顶点之间的最短路径。
该问题的解决方案是为从罗马到威尼斯的所有可能路线编号。对于每条路线,计算总长度,然后选择最短的一条。这个解决方案不是最有效的,因为需要分析的路径有数百万条。
实际上,我们将意大利地图建模为一个加权有向图 G = (V, E),其中每个顶点表示一个城市,每条边 (u, v) 表示从 u 到 v 的直接路径,而每个权重 w(u, v) 对应于边 (u, v),表示 u 和 v 之间的距离。因此,要解决的问题是找到从表示罗马的顶点到表示威尼斯的顶点的最短路径。
给定一个加权有向图 G = (V, E),路径 p = (v0, v1, ..., vk) 的权重由其组成的边的权重之和给出,如下公式所示:

从节点 u 到节点 v 的最短路径是一个路径 p = (u, v1, v2, ..., v),使得 w(p) 最小,如下所示:

从 u 到 v 的最短路径的代价用 δ(u, v) 表示。如果从 u 到 v 没有路径,则 δ(u, v) = ∞。
给定一个连通的加权图 G = (V, E) 和一个源节点 s,有多种算法可以找到从 s 到 V 中其他节点的最短路径。在上一节中,我们分析了 Dijkstra 算法,现在是时候使用基于强化学习的算法来解决这个问题了。
正如本章开始时预期的那样,车辆路径问题(VRP)是一个典型的配送和运输问题,旨在优化使用一组有限容量的车辆来接送货物或人员,并将其运送到地理上分布的站点。以最佳方式管理这些操作可以显著降低成本。在用 Python 代码解决问题之前,让我们分析一下这一主题的基本特征,以便理解可能的解决方案。
基于迄今为止所述,很明显,这类问题可以被视为路径优化过程,可以通过图论有效地解决。
假设我们有以下图,其中边上的数字表示顶点之间的距离:

很容易看出,从 1 到 6 的最短路径是 1 – 2 – 5 – 4 - 6。
在理解 TD 方法部分中,我们已经看到,选择动作的方法根据 TD 的不同,算法的类型也会有所不同。在基于策略的方法(SARSA)中,更新是根据由选定策略决定的动作结果进行的,而在离策略方法(Q-learning)中,策略是通过假设的动作来评估的,而这些动作并未真正执行。我们将通过这两种方法来解决刚才提到的问题,突显解决方案的优点和缺点。那么,让我们看看如何使用 Q-learning 来处理车辆路径问题。
Q-learning 方法
正如我们在Q-learning部分所说,Q-learning 试图最大化 Q 函数(动作-价值函数)的值,该函数表示当我们在状态s中执行动作a时,能够获得的最大折扣未来奖励。
以下代码块是一个 R 代码的实现,通过 Q 学习技术让我们研究这一路径:
N <- 1000
gamma <- 0.9
alpha <- 1
FinalState <- 6
RMatrix <- matrix(c(-1,50,1,-1,-1,-1,
-1,-1,-1,1,50,-1,
-1,-1,-1,1,-1,-1,
-1,-1,-1,-1,-1,100,
-1,-1,-1,50,-1,-1,
-1,-1,-1,-1,-1,100),nrow=6,byrow = TRUE)
print(RMatrix)
QMatrix <- matrix(rep(0,length(RMatrix)), nrow=nrow(RMatrix))
for (i in 1:N) {
CurrentState <- sample(1:nrow(RMatrix), 1)
repeat {
AllNS <- which(RMatrix[CurrentState,] > -1)
if (length(AllNS)==1)
NextState <- AllNS
else
NextState <- sample(AllNS,1)
QMatrix[CurrentState,NextState] <- QMatrix[CurrentState,NextState] + alpha*(RMatrix[CurrentState,NextState] + gamma*max(QMatrix[NextState, which(RMatrix[NextState,] > -1)]) - QMatrix[CurrentState,NextState])
if (NextState == FinalState) break
CurrentState <- NextState
}
}
print(QMatrix)
我们将逐行分析代码,从以下参数的设置开始:
N <- 1000
gamma <- 0.9
alpha <- 1
FinalState <- 6
这里,我们有以下内容:
-
N: 迭代的回合数 -
gamma: 折扣因子 -
alpha: 学习率 -
FinalState: 目标节点
让我们继续设置奖励矩阵:
RMatrix <- matrix(c(-1,50,1,-1,-1,-1,
-1,-1,-1,1,50,-1,
-1,-1,-1,1,-1,-1,
-1,-1,-1,-1,-1,100,
-1,-1,-1,50,-1,-1,
-1,-1,-1,-1,-1,100),nrow=6,byrow = TRUE)
我们看到矩阵的呈现方式如下:
print(RMatrix)
以下矩阵被打印出来:
[,1] [,2] [,3] [,4] [,5] [,6]
[1,] -1 50 1 -1 -1 -1
[2,] -1 -1 -1 1 50 -1
[3,] -1 -1 -1 1 -1 -1
[4,] -1 -1 -1 -1 -1 100
[5,] -1 -1 -1 50 -1 -1
[6,] -1 -1 -1 -1 -1 100
让我们尝试理解如何设置这个矩阵。一切都非常简单:我们在最方便的边上(那些权重较低的边,即较短的路径)关联了高奖励。然后,我们将最高的奖励(100)赋给了通向目标的边。最后,我们将负奖励分配给不存在的连接。下图显示了我们如何设置奖励:

我们只是将边的权重替换成与长度值相对应的奖励。较长的边返回较低的奖励,而较短的边返回较高的奖励。最终,当目标到达时,将获得最大的奖励。
正如我们在Q-learning部分所说,我们的目标是估计一个评估函数,该函数根据奖励的总和来评估策略的便利性。Q-learning 算法试图最大化 Q 函数(行动值函数)的值,Q 函数表示我们在状态s中执行动作a时的最大折现未来奖励。
让我们再次分析我们需要使用 R 实现的程序:
Initialize
arbitrary action-value function
Repeat (for each episode)
Initialize s
choose a from s using policy from action-value function
Repeat (for each step in episode)
take action a
observe r, s'
update action-value function
update s
在每一步,代理观察环境的当前状态,并使用π策略选择并执行动作。通过执行该动作,代理获得奖励𝑅𝑡 + 1和新状态𝑆𝑡 + 1。此时,代理可以通过更新估计来计算𝑄 (s𝑡, a𝑡)。
因此,Q 函数代表了程序的核心元素;它是一个与奖励矩阵维度相同的矩阵。首先,我们将其初始化为全零矩阵:
QMatrix <- matrix(rep(0,length(RMatrix)), nrow=nrow(RMatrix))
此时,我们必须设置一个循环,对每个回合重复操作:
for (i in 1:N) {
循环的初始部分用于设置初始状态和初始策略;在我们的案例中,我们将随机选择一个初始状态:
CurrentState <- sample(1:nrow(RMatrix), 1)
设置初始状态后,我们必须插入一个循环,直到达到最终状态,即我们的目标:
repeat {
现在我们必须根据当前状态中可用的可能动作选择下一个状态。为了移动到下一个节点,我们可以采取哪些行动?如果只有一个可能的动作可用,我们将选择那个动作。否则,我们将随机选择一个动作,然后再分析其他动作:
AllNS <- which(RMatrix[CurrentState,] > -1)
if (length(AllNS)==1)
NextState <- AllNS
else
NextState <- sample(AllNS,1)
根据获得的结果,我们可以更新行动值函数(QMatrix):
QMatrix[CurrentState,NextState] <- QMatrix[CurrentState,NextState] + alpha*(RMatrix[CurrentState,NextState] + gamma*max(QMatrix[NextState, which(RMatrix[NextState,] > -1)]) - QMatrix[CurrentState,NextState])
用于更新 Q 函数的公式如下:

现在,我们将检查已达成的状态:如果我们已经达到了目标,则使用 break 命令退出循环;否则,我们将把下一个状态设为当前状态(NextState):
if (NextState == FinalState) break
CurrentState <- NextState
}
}
一旦程序完成,我们将打印 Q 矩阵:
print(QMatrix)
返回以下结果:
[,1] [,2] [,3] [,4] [,5] [,6]
[1,] 0 864.5 811.9 0 0 0
[2,] 0 0.0 0.0 901 905 0
[3,] 0 0.0 0.0 901 0 0
[4,] 0 0.0 0.0 0 0 1000
[5,] 0 0.0 0.0 950 0 0
[6,] 0 0.0 0.0 0 0 1000
让我们尝试理解这个矩阵告诉了我们什么。首先,我们可以说这个矩阵允许我们计算从任何状态开始的最短路径,因此,不一定是从节点 1 开始。在我们的案例中,我们将从节点 1 开始,以确认视觉上获得的结果。回想一下,矩阵的每一行代表一个状态,每一列中的值告诉我们转移到列索引标记的状态时的奖励。
在接下来的流程路径中,我们有如下内容:
-
从第一行开始,我们看到最大值位于第二列,因此,最佳路径将我们从状态 1 带到状态 2。
-
然后,我们进入由第二行标识的状态 2;在这里,我们看到最大奖励值位于第五列,因此,最佳路径将我们从状态 2 带到状态 5。
-
然后我们继续讨论由第五行标识的状态 5。在这里,我们看到奖励的最大值与第四列相对应,因此,最佳路径将我们从状态 5 带到状态 4。
-
最后,我们转到由第四行标识的状态 4。在这里,我们看到奖励的最大值与第六列相对应,因此,最佳路径将我们从状态 4 带到状态 6。
我们已经到达目标,并且通过这样做,我们从节点 1 到节点 6 绘制了更短的路径,路径如下:
1 – 2 - 5 – 4 – 6
这条路径与本节开始时视觉上得到的路径一致。提取QMatrix矩阵的最短路径的过程可以如下轻松地自动化:
RowMaxPos<-apply(QMatrix, 1, which.max)
ShPath <- list(1)
i=1
while (i!=6) {
IndRow<- RowMaxPos[i]
ShPath<-append(ShPath,IndRow)
i= RowMaxPos[i]
}
print(ShPath)
返回以下结果:
[[1]]
[1] 1
[[2]]
[1] 2
[[3]]
[1] 5
[[4]]
[1] 4
[[5]]
[1] 6
如我们所见,返回的结果是相同的。现在,让我们看看如果我们尝试用不同的方法解决同样的问题会发生什么。
SARSA 方法
正如我们在 SARSA 中所预见的,从当前状态St出发,采取一个动作At并且代理获得奖励 R。这样,代理就被转移到下一个状态St + 1,并在St + 1中采取动作At + 1。实际上,SARSA 是元组(S, A, R, St + 1, At + 1)的缩写。
以下是 SARSA 方法的完整代码:
N <- 1000
gamma <- 0.9
alpha <- 1
FinalState <- 6
RMatrix <- matrix(c(-1,50,1,-1,-1,-1,
-1,-1,-1,1,50,-1,
-1,-1,-1,1,-1,-1,
-1,-1,-1,-1,-1,100,
-1,-1,-1,50,-1,-1,
-1,-1,-1,-1,-1,100),nrow=6,byrow = TRUE)
print(RMatrix)
QMatrix <- matrix(rep(0,length(RMatrix)), nrow=nrow(RMatrix))
for (i in 1:N) {
CurrentState <- sample(1:nrow(RMatrix), 1)
repeat {
AllNS <- which(RMatrix[CurrentState,] > -1)
if (length(AllNS)==1)
NextState <- AllNS
else
NextState <- sample(AllNS,1)
AllNA <- which(RMatrix[NextState,] > -1)
if (length(AllNA)==1)
NextAction <- AllNA
else
NextAction <- sample(AllNA,1)
QMatrix[CurrentState,NextState] <- QMatrix[CurrentState,NextState] + alpha*(RMatrix[CurrentState,NextState] + gamma*QMatrix[NextState,NextAction] - QMatrix[CurrentState,NextState])
if (NextState == FinalState) break
CurrentState <- NextState
}
}
print(QMatrix)
RowMaxPos<-apply(QMatrix, 1, which.max)
ShPath <- list(1)
i=1
while (i!=6) {
IndRow<- RowMaxPos[i]
ShPath<-append(ShPath,IndRow)
i= RowMaxPos[i]
}
print(ShPath)
如你所见,大部分代码与前一个案例(Q-learning)相似,因为这两种方法之间有许多相似之处。我们只会分析两者之间的差异。在代码的第一部分,设置了初始参数并定义了奖励矩阵:
N <- 1000
gamma <- 0.9
alpha <- 1
FinalState <- 6
RMatrix <- matrix(c(-1,50,1,-1,-1,-1,
-1,-1,-1,1,50,-1,
-1,-1,-1,1,-1,-1,
-1,-1,-1,-1,-1,100,
-1,-1,-1,50,-1,-1,
-1,-1,-1,-1,-1,100),nrow=6,byrow = TRUE)
print(RMatrix)
现在,让我们继续初始化 Q 矩阵并设置将允许我们更新动作价值函数的循环:
QMatrix <- matrix(rep(0,length(RMatrix)), nrow=nrow(RMatrix))
for (i in 1:N) {
CurrentState <- sample(1:nrow(RMatrix), 1)
repeat {
AllNS <- which(RMatrix[CurrentState,] > -1)
if (length(AllNS)==1)
NextState <- AllNS
else
NextState <- sample(AllNS,1)
到目前为止,与前一个示例中分析的公式相比,没有任何变化。但现在有了一些重要的变化。在SARSA部分,我们看到了算法的伪代码;为了方便起见,我们在此重复一下:
Initialize
arbitrary action-value function
Repeat (for each episode)
Initialize s
choose a from s using policy from action-value function
Repeat (for each step in episode)
take action a
observe r, s'
choose a' from s' using policy from action-value function
update action-value function
update s,a
与前一部分(Q-learning 方法)中提出的方法相比,我们可以看到这两种方法的实质性区别在于更新动作价值函数所使用的公式以及计算下一状态要采取的动作。我们将在下一个状态中执行的动作计算公式如下:
AllNA <- which(RMatrix[NextState,] > -1)
if (length(AllNA)==1)
NextAction <- AllNA
else
NextAction <- sample(AllNA,1)
该方法用于评估下一个状态。我们将使用的公式如下:

这个公式在 R 代码中变为:
QMatrix[CurrentState,NextState] <- QMatrix[CurrentState,NextState] + alpha*(RMatrix[CurrentState,NextState] + gamma*QMatrix[NextState,NextAction] - QMatrix[CurrentState,NextState])
其余的代码与前一部分类似:
if (NextState == FinalState) break
CurrentState <- NextState
}
}
print(QMatrix)
RowMaxPos<-apply(QMatrix, 1, which.max)
ShPath <- list(1)
i=1
while (i!=6) {
IndRow<- RowMaxPos[i]
ShPath<-append(ShPath,IndRow)
i= RowMaxPos[i]
}
print(ShPath)
然后我们可以分析结果:
[,1] [,2] [,3] [,4] [,5] [,6]
[1,] 0 50 1 0 0 0.0000
[2,] 0 0 0 1 50 0.0000
[3,] 0 0 0 1 0 0.0000
[4,] 0 0 0 0 0 999.9999
[5,] 0 0 0 50 0 0.0000
[6,] 0 0 0 0 0 999.9999
最短路径如下:
[[1]]
[1] 1
[[2]]
[1] 2
[[3]]
[1] 5
[[4]]
[1] 4
[[5]]
[1] 6
结果与前一个示例中得到的结果相同。让我们来理解这两种方法有何不同。
区分 SARSA 和 Q-learning
从算法的角度来看,我们在前面章节分析的两种方法之间的实质性差异在于我们用来更新动作价值函数的两个方程。我们来对比一下它们,以便更好地理解:


Q 学习计算Q (s, a)和动作的最大值之间的差异,而 SARSA 计算Q (s, a)与下一步动作的值之间的差异。在这样做时,您可以突出以下几点:
-
SARSA 使用智能体在环境中生成经验时使用的策略(如 epsilon-贪心),以选择额外的动作A t + 1。然后,它使用Q (S t + 1, A t + 1)来折扣 gamma 因子,并将其作为预期的未来回报,计算更新目标。
-
Q 学习不使用此策略来选择额外的动作A t + 1。相反,它在更新规则中估计期望的未来回报,将其表示为max Q (S t + 1, A),并对所有动作进行计算。
这两种方法收敛到不同的解:
-
SARSA 通过遵循与生成经验时相同的策略收敛到最优解。这将包含一些随机性,以确保收敛。
-
Q 学习通过遵循贪心策略生成经验并进行训练,最终收敛到一个最优解。
当我们需要确保智能体在学习过程中的表现时,建议使用 SARSA。这是因为在学习过程中,我们必须确保错误的数量较少,而这些错误对于我们使用的设备来说是昂贵的。因此,我们关心其在学习过程中的表现。
像 Q 学习这样的算法在我们不关心学习过程中智能体的表现,仅仅希望智能体学会一个最优的贪心策略(我们将在过程结束时采用)时是值得推荐的。
总结
在这一章中,介绍了 TD 学习算法。这些算法基于减少智能体在不同时间做出的估计之间的差异。SARSA 算法实现了一个在策略的 TD 方法,而 Q 学习具有脱离策略的特点。
然后,介绍了图论的基础——包括邻接矩阵和邻接列表的内容。我们已经看到如何使用igraph包在 R 中表示图。通过这样做,我们解决了最短路径问题,并且分析了 R 中的 Dijkstra 算法。
最后,使用 Q 学习和 SARSA 算法解决了车辆路径规划问题。详细分析了这两种方法解决该问题的差异。
在下一章,我们将学习博弈论的基本概念。我们将学习如何安装和配置 OpenAI Gym 库,并理解它是如何工作的。我们将了解 Q-learning 和 SARSA 算法之间的区别,并理解如何进行学习阶段和测试阶段。最后,我们将学习如何使用 R 开发 OpenAI Gym 应用。
第三部分 - 现实世界的应用
在本节结束时,你将熟练掌握强化学习的现实世界应用。
本节包含以下章节:
-
第八章,游戏应用中的强化学习
-
第九章,金融工程中的 MAB
-
第十章,健康护理中的 TD 学习
第八章:博弈应用中的强化学习
游戏一直是人类文化中的一种现象,人们通过它展现智慧、互动和竞争。但游戏也是逻辑学、人工智能(AI)、计算机科学、语言学、生物学以及最近越来越多地出现在社会科学和心理学中的一个重要理论范式。游戏,尤其是战略游戏,为强化学习算法提供了理想的测试环境,因为它们可以作为实际问题的模型。
在本章中,我们将学习如何使用强化学习算法解决博弈论中的问题。到本章结束时,我们将掌握博弈论的基本概念。我们还将学习如何安装和配置 OpenAI Gym 库,了解 OpenAI Gym 库的工作原理,并学习如何使用 Q 学习解决游戏问题。除此之外,我们还将了解如何进行学习和测试阶段,并学习如何使用 R 开发 OpenAI Gym 应用。
在本章中,我们将涵盖以下主题:
-
理解博弈论的基础
-
探索博弈论的应用
-
玩井字棋游戏
-
介绍 OpenAI Gym 库
-
使用 FrozenLake 环境的机器人控制系统
技术要求
查看以下视频,了解代码在实际应用中的表现:
理解博弈论的基础
博弈论是一门数学科学,研究和分析主体在冲突或战略互动情况下的个体决策,这些决策涉及与其他竞争主体的互动,目的是最大化每个主体的利润。在这种情况下,一个主体的决策可能会影响另一个(或多个)主体的结果,反之亦然,依据反馈机制,通过模型寻求竞争性和/或合作性解决方案。
博弈论的理论起源可以追溯到 1654 年,那时布莱兹·帕斯卡(Blaise Pascal)与皮埃尔·德·费马(Pierre de Fermat)就赌博概率的计算进行书信往来。博弈论一词最早由埃米尔·博雷尔(Emil Borel)在 1920 年代使用。博雷尔发展了博弈理论,提出了一个包含两名玩家的零和博弈,并试图寻找一种解决方案,即冯·诺依曼的零和博弈解决概念。普遍认为,约翰·冯·诺依曼(John von Neumann)和奥斯卡·摩根斯坦(Oskar Morgenstern)于 1944 年发布的《博弈论与经济行为》一书标志着现代博弈论的诞生,尽管其他作者(如恩斯特·泽梅洛(Ernst Zermelo)和阿尔芒·博雷尔(Armand Borel))也曾讨论过博弈论。
这两位学者的思想可以非正式地描述为试图在涉及资源的赢得或分配的情境下,用数学描述人类行为。后来的著名学者,特别是在非合作博弈方面有深入研究的,是数学家约翰·福布斯·纳什(John Forbes Nash jr.),他的事迹被荣·霍华德的电影《美丽心灵》所呈现。
曾有八位诺贝尔经济学奖获得者处理过博弈论问题。约翰·梅纳德·史密斯(John Maynard Smith),一位长期卓越的生物学家、遗传学家以及萨塞克斯大学教授,也因其在这一领域的贡献获得了克拉福德奖。
在接下来的章节中,我们将介绍博弈论的基本概念,然后分析研究人员所面临的主要博弈类型。
博弈论的基本概念
博弈论的主要目标是胜利。每个人必须了解游戏规则并意识到每一步的后果。个人打算做出的所有行动构成了一种策略。根据所有玩家采取的策略,每个玩家根据适当的衡量单位获得报酬。报酬可以是正面、负面或无。若每个玩家的支付与其他玩家的损失相对应,则该游戏称为常数和游戏。两名玩家之间的零和游戏代表了一个奖励从一方支付给另一方的情况。若策略对所有玩家都是满意的,则称为均衡策略;否则,就需要计算并最大化玩家的数学期望或预期值,即可能奖励的加权平均值,每个奖励根据该事件的概率进行加权。
在一个游戏中,有一个或多个竞争者试图赢得比赛,也就是最大化他们的收益。收益由一个规则定义,量化地规定了竞争者根据他们的行为获得的奖励。这种函数叫做支付函数。每个玩家可以采取有限或无限的行动或决策,从而确定一种策略。每种策略都有一个结果,表现为采纳该策略的玩家所获得的后果,可能是奖励(正面/负面)。游戏的结果完全由他们策略的顺序以及其他玩家所采取的策略决定。
如何为每个玩家表征游戏的结果呢?如果你以奖励来衡量策略的后果,那么每个策略都可以匹配一个数值:负值表示支付给对手,如罚款;而正值则表示收益,即获得奖励。与玩家所采取的策略以及其他所有玩家在某一时刻所采取的策略相关的增益或损失,通常通过支付函数所指示的货币值来表示。
玩家所做的决策自然会相互碰撞,或者与其他玩家所做的决策相一致,从而衍生出各种类型的博弈。
一个有用的工具来表示两个玩家、两家公司或两个个人之间的互动是一个双重决策矩阵或表格。这个决策表显示了由两个玩家进行的博弈的策略和获胜情况。因此,决策矩阵是一个表示工具,通过它我们可以列出玩家之间互动的所有可能结果,并为每种情形分配奖励值,该奖励值在每种情况下竞争给予每个玩家。另一种表示形式涉及到每个决策的采取顺序,或者行动的执行方式。游戏的这一特征可以通过树形图来描述,树形图表示从初始状态到最终状态之间,竞争者的每一个可能组合,最终在这些状态中分配奖励。
描述一个战略情境需要四个基本要素:
-
玩家: 游戏中的决策者(谁参与?)
-
行动: 玩家可以选择的可能行动或动作(他们能做什么?)
-
策略: 玩家们的行动计划(他们打算做什么?)
-
收益: 玩家获得的可能收益(他们得到了什么?)
因此,策略是一个完整且有条件的计划,或决策决策,明确规定了玩家在任何可能需要做出决策的情况下必须采取的行动。作为一个完整的有条件计划,策略通常定义了玩家在游戏中可能无法实现的情境下应该选择哪种行动。在接下来的部分中,游戏将被分类,并且每个主题会有简短的描述。
游戏类型
游戏可以根据不同的范式进行分类,以下是其中几种:
-
合作
-
对称性
-
总和
-
排序
在接下来的部分中,我们将简要描述这些主题。
合作博弈
当玩家的利益不直接对立,而是具有共同利益时,便呈现出合作博弈。玩家们追求一个共同的目标,至少在游戏进行期间如此;其中一些玩家可能会倾向于联合起来以提高他们的回报。保证由绑定协议提供。那么,如何用数学来表示共同利益呢?个人利益在联盟或联合中的结合体的概念通过定义本质性博弈来表达;而一个普通联盟的价值则通过一个称为特征函数的函数来衡量。
相反,在非合作博弈中,也称为竞争性博弈,玩家不能达成具有约束力的协议(即使是通过规定)。约翰·纳什提出的纳什均衡适用于这一类博弈,它可能是整个理论中最著名的概念,因为它有广泛的应用领域。在非合作博弈中采用的理性行为标准是个体化的,称为最大策略。
对称博弈
在对称博弈中,采用某一策略所获得的利润仅取决于其他玩家所采用的策略,而与采用该策略的玩家无关。如果玩家的身份可以改变而不改变支付结果,则该博弈是对称的。
相比之下,在不对称博弈中,两名玩家没有相同的策略系列。然而,也有可能一个博弈对于两名玩家来说具有相同的策略,但它依然是不对称的。
零和博弈
零和博弈是常数和博弈的一个特例,其中常数为零。零和博弈建模了所有对立的情境,其中两名玩家的对抗是完全的:一名玩家的胜利恰好与另一名玩家的失败相对应。换句话说,两个竞争者根据所使用策略所获得的总利润始终为零。例如,在象棋中,这意味着唯一的三种可能结果是:胜利、失败和平局(奖励:+1,-1 和 0)。
顺序博弈
在顺序博弈中,后续的玩家保留了一些关于前一玩家行为的知识。这并不意味着他们知道前一玩家的所有行动。例如,一名玩家可能知道前一名玩家没有进行某个动作,但不知道第一名玩家做了哪些其他可选动作。
现在我们已经学会了根据一些范式来分类游戏。为什么分析博弈如此重要?这是因为许多现实生活中的问题可以通过博弈论得出的解决方案来应对。在下一节中,我们将看到一些例子。
探索博弈论的应用
博弈论一直吸引着许多研究者,因为它在实际领域和人类各个工作领域中的应用非常有价值,例如以下几个方面:
-
哲学:哲学一直在分析博弈论,因为它提供了一种方法来澄清一些哲学家的逻辑难题,例如康德、卢梭、霍布斯以及其他社会政治理论家的难题。
-
经济学:商业世界中的许多投机行为可以通过博弈论的方法来建模。一个著名的例子是寡头定价与囚徒困境之间的相似性。
-
生物学:尽管大自然常被认为是残酷的,但许多不同物种之间存在合作。这种共存的原因可以通过博弈论来建模。
-
AI:人类可以根据其所接收的环境刺激做出决策。而机器则只能在编程时根据多种条件的决策列表进行规划。这个限制可以通过人工智能来克服,它赋予机器从创造者那里获取新决策的能力,而不是仅依赖预先计划的规则。为此,程序必须根据观察到的刺激和经验生成新的回报矩阵。
在接下来的章节中,我们将分析一个广泛流行的游戏,并学习如何使用强化学习进行处理。
玩井字游戏
井字游戏是用强化学习解决的第一个游戏的完美例子:实际上,与其他策略游戏相比,它有一些简单的规则。此外,它相对容易编程,而且因为游戏最多只进行九轮,评估函数的训练非常快速。井字游戏是一个完美信息的双人游戏,每个玩家被分配一个符号来进行游戏。通常使用的符号是叉和圈。游戏由使用叉号的玩家开始。
游戏网格具有 3x3 的结构,初始时呈现九个空单元格,如下所示:

玩家轮流选择一个空格,并绘制自己的符号。成功将三个符号放置在水平、垂直或对角线上的玩家获胜。如果游戏网格填满且没有任何玩家成功完成三符号的直线,游戏以平局结束。因此,如果正确玩法,井字游戏最终会以平局告终,使得游戏毫无意义。
在接下来的章节中,我们将介绍使用 Q-learning 算法玩游戏的tictactoe包。
tictactoe 包
为了处理井字游戏,我们将使用 CRAN 网站上可用的tictactoe包。该包实现了一个控制台上的井字游戏,可以与人类或 AI 玩家对战。各种等级的 AI 玩家通过 Q-learning 算法进行训练。
以下表格提供了有关该包的一些信息:
| 包 | tictactoe |
|---|---|
| 日期 | 2017-05-26 |
| 版本 | 0.2.2 |
| 标题 | 井字游戏 |
| 作者 | 森本光太 |
在该包的帮助下,我们将与计算机进行第一次对局,以突出游戏的特性,然后我们将训练一个人工代理,按照最佳策略进行游戏,以获得最多的胜利。
玩井字游戏
首先,我们将看到如何设置游戏环境,并开始第一次游戏:
- 首先,我们需要导入库:
library(tictactoe)
- 然后,我们可以在 R 控制台上使用
ttt()函数启动井字游戏,如下所示:
ttt(ttt_human(name = "GIUSEPPE"), ttt_random())
请注意以下事项:
-
ttt_human()创建一个人类井字游戏玩家;如果我们愿意,还可以通过 name 属性设置名字(例如name = "GIUSEPPE")。 -
ttt_random()设置一个随机玩家,该玩家仅随机地将对方的符号(圆圈)放置在一个空位上。
控制台返回以下网格:
A B C
------
1| . . .
2| . . .
3| . . .
Player 1 (GIUSEPPE) to play
choose move (e.g. A1) >
正如预期的那样,游戏基于一个简单的 3x3 网格;为了方便识别单元格,列用字母 A、B、C 命名,而行用数字 1、2、3 表示。这意味着左上角的第一个单元格将被标识为 A1。控制台打印的最后一行邀请玩家 1(GIUSEPPE)进行下一步操作。
- 我们首先在单元格 A1 中放置 X,随后返回以下结果:
action = A1
A B C
------
1| X . .
2| . . .
3| . . .
Player 2 (random AI) to play
action = B1
A B C
------
1| X O .
2| . . .
3| . . .
Player 1 (GIUSEPPE) to play
choose move (e.g. A1) >
如我们在前面的代码块中看到的,X 已正确放置在左上角的单元格中,然后玩家 2(计算机)随机地将符号(O)放置在一个空位上。最后一行再次邀请玩家 1 进行下一步操作。我们可以这样进行,直到游戏结束。
有三种结果可供选择——0、1 和 2,分别表示平局、玩家 1 获胜和玩家 2 获胜。例如:
action = B2
game over
A B C
------
1| X . X
2| O X O
3| X . O
won by Player 1 (GIUSEPPE)!
在这种情况下,我赢了,但仅仅是因为计算机随机地放置了它的符号,并没有遵循任何策略。
使用 Q-learning 训练代理
我们可以训练代理遵循一种策略。我们来看看怎么做:
- 为了开始,我们可以模拟游戏,检查两个人工智能代理互相对弈时得到的结果:
player1<- ttt_ai()
player2<- ttt_ai()
SimulatedGame <- ttt_simulate(player1, player2, N = 100)
在上述代码中,使用了以下函数:
-
ttt_ai() -
ttt_simulate
ttt_ai()函数创建了一个人工智能的井字棋玩家。我们没有使用任何参数,实际上可以使用以下参数:
-
name:玩家名称。 -
level:AI 强度必须是从 0(最弱)到 5(最强)之间的整数。
level参数定义了我们创建的代理的有效性;从 0 到 5,代理的游戏管理技能逐渐提高,使得对手的游戏变得更加困难。在之前的指令中,我们已经使用ttt_random()函数创建了一个人工智能代理,它是ttt_ai()函数的别名,其中代理的级别默认设置为 0。
ttt_ai()函数返回一个对象,该对象与getmove()函数关联;此函数接受一个ttt_game类型的对象,并使用策略函数返回一个最优的动作。
一个ttt_ai对象包含值函数和策略函数。值函数将一个游戏状态与从第一个玩家视角的评估相关联。策略函数将一个游戏状态与通过评估值函数获得的一组最优动作相关联。这些函数通过基于 Q-learning 的算法进行训练,我们在第七章中详细讨论了时序差分学习。
使用的第二个函数是ttt_simulate(),它模拟了两个人工智能代理之间的井字棋游戏。传入的参数如下:
-
player1,player2:用于模拟的人工玩家。 -
N:模拟游戏的次数。
除这些外,还可以使用以下附加参数:
-
verbose:如果为真,则显示进度报告。 -
showboard:如果为真,则显示游戏过渡。 -
pauseif:当出现指定结果时暂停模拟。这对探索性目的很有用。
该函数返回一个整数向量,表示所执行模拟的结果。实际上,每次模拟都会返回一个值,介于 0、1 和 2 之间,分别代表平局、玩家 1 胜利或玩家 2 胜利,正如我们已经提到的那样。
- 我们可以使用
str()函数验证我们所说的内容,该函数返回一个 R 对象内部结构的紧凑视图:
str(SimulatedGame)
返回以下结果:
int [1:100] 1 2 2 0 1 1 2 1 2 2 ...
- 我们可以做更多的事;例如,我们可以使用
prop.table()函数验证整个模拟中的三种游戏结果的发生情况,方法如下:
prop.table(table(SimulatedGame))
- 此函数接受一个表格作为参数,并计算其包含的数据的比例。返回以下结果:
SimulatedGame
0 1 2
0.12 0.51 0.37
通过这种方式,我们可以看到,在进行的模拟中,玩家 1 获胜的次数更多(51%),而玩家 2 获胜的次数较少(37%),平局的次数明显较低(12%)。
- 现在,我们重复实验,但这次我们将通过基于 Q 学习算法的训练来提高两个玩家之一的表现。如同之前的做法,我们首先创建两个代理:
player3<- ttt_ai()
player4<- ttt_ai()
- 完成此操作后,我们将重点放在玩家 4 上,尝试通过训练阶段提高他的表现,在这个阶段他将学习遵循最佳策略:
TrainPlayer4 <- ttt_qlearn(player4, N = 500, verbose = FALSE)
ttt_qlearn() 函数通过 Q-learning 训练井字棋代理。以下参数可用:
-
player:待训练的人工玩家。 -
N:回合数。 -
epsilon:随机探索动作的比例。 -
alpha:学习率。 -
gamma:折扣因子。 -
simulate:如果为真,则在训练期间进行模拟。 -
sim_every:在进行此多次训练后进行模拟。 -
N_sim:模拟游戏的次数。 -
verbose:如果为真,则显示进度报告。
在基于 Q 学习的训练过程中,代理与自己对弈以更新值函数和策略。所使用的算法是带有 epsilon 贪心策略的 Q 学习。
对于每个状态 s,玩家根据以下公式更新值函数:

这发生在第一个玩家的回合。当第二个玩家的回合到来时,使用的公式将始终相同,只要你将最大值替换为最小值。以类似的方式,我们继续更新策略;也就是说,我们寻找一组行动,使我们能够到达下一个状态,从而最大化值函数。
控制过程的参数如下:
-
学习率决定了新信息被获取的频率,并将替代旧信息。折扣因子为 0 时,会阻止智能体学习;然而,折扣因子为 1 时,智能体只对最近的信息感兴趣。
-
折扣因子决定未来奖励的重要性。折扣因子为 0 时,智能体只会使用当前奖励,而折扣因子接近 1 时,智能体也会关注他在长期未来将获得的奖励。
算法中使用的策略使得玩家采用 e-greedy 方法选择下一个动作。这意味着智能体将在概率 1-e 下遵循其策略,而在概率 e 下选择随机动作。
游戏结束时,玩家将最终状态设置如下:
-
如果玩家 1 胜利,结果为 100
-
如果玩家 2 获胜,则结果为 -100
-
如果是平局,结果为 0
学习过程重复 N 次,N 是用户设定的值:
- 在训练好玩家 4 后,我们可以模拟游戏:
SimulatedGameQLearn <- ttt_simulate(player3, player4, N = 100)
- 我们再次验证结果的出现次数:
prop.table(table(SimulatedGameQLearn))
返回以下结果:
SimulatedGameQLearn
0 1 2
0.31 0.21 0.48
在这种情况下,训练过的智能体(玩家 4)赢得了最多的游戏(48%),其次是平局(31%),最后是玩家 3 赢得的游戏(21%)。因此,玩家的训练通过创建一个能够识别出让他获得更多胜利的策略的智能体,逆转了游戏的结果。
在接下来的部分中,将介绍 OpenAI Gym 库;这个库包含许多环境,可以让我们使用强化学习来训练智能体。
介绍 OpenAI Gym 库
OpenAI Gym 是一个帮助我们实现基于强化学习算法的库。它专注于强化学习中的阶段性设置。换句话说,智能体的经历被分为一系列的阶段。智能体的初始状态由一个分布随机采样,并且交互直到环境达到终止状态。这一过程对每个阶段重复,目的是最大化每个阶段的总奖励期望,并在尽可能少的阶段内实现高性能。
Gym 是一个用于开发和比较强化学习算法的工具包。它支持训练智能体完成从走路到玩 Pong 或弹球等游戏的各种任务。该库可以通过以下链接访问:gym.openai.com/。
OpenAI Gym 是一个更加宏大的项目的一部分,该项目被称为 OpenAI 项目。OpenAI 是一家由埃隆·马斯克和山姆·奥特曼创办的人工智能(AI)研究公司。它是一个非营利项目,旨在促进和发展友好的 AI,以便造福整个人类。该组织的目标是通过将专利和研究公开,来与其他机构和研究人员自由合作。创始人之所以启动这一项目,是因为他们对 AI 的不加限制使用可能带来的生存风险感到担忧。
OpenAI Gym 是一个程序库,允许你开发人工智能、衡量智能能力,并增强学习能力。简而言之,它是一个以算法形式呈现的健身房,用来训练当前的数字大脑,并将其投射到未来。除此之外,还有另一个目标。OpenAI 希望通过资助那些能够促进人类进步的项目来激励 AI 领域的研究,即便这些项目在经济上没有直接回报。而通过 Gym,它旨在标准化 AI 的衡量方法,以便研究人员能够在平等的条件下竞争,并了解他们的同事取得了哪些进展,最重要的是,它专注于那些对所有人都真正有益的成果。
可用的工具非常多。从玩像 Pong 这样的老式视频游戏,到在围棋中对战,再到控制机器人,我们只需将算法输入到这个数字化空间,便可看到它如何工作。第二步是将获得的基准数据与其他人进行比较,看看我们在同行中处于什么位置,也许我们还可以与他们合作,共享互利成果。
以下列表展示了库中可用的一些环境;这些环境按类别分组,便于搜索:
-
算法:执行计算任务,如加法多位数和反转序列。你可能会认为这些任务对计算机来说很简单,但挑战在于如何仅通过示例来学习这些算法。这些任务的一个优点是,可以通过变化序列长度轻松调节难度。
-
Atari:玩经典的 Atari 游戏。我们已将街机学习环境(对强化学习研究产生了巨大影响)集成到一个易于安装的形式中。
-
Box2D:在 Box2D 模拟器中进行连续控制任务。
-
经典控制:完成小规模任务,主要来自强化学习文献。它们的目的是帮助你入门。
-
MuJoCo:连续控制任务,在快速物理模拟器中运行。此任务使用 MuJoCo 物理引擎,该引擎专为快速且精确的机器人模拟设计。
-
机器人技术:为 Fetch 和 ShadowHand 机器人提供基于目标的模拟任务。
-
Toy text:简单的文本环境,帮助你入门。
特别是,经典控制类别提供了非常有用的环境,能够重现重要物理实验的场景。
OpenAI Gym 对我们代理的结构没有假设,并且与任何数值计算库兼容。Gym 库是一组测试问题——环境——我们可以用来处理强化学习算法。这些环境具有共享的接口,允许你编写通用的算法。首先,让我们看看如何安装这个库。
OpenAI Gym 安装
如前所述,OpenAI Gym 是一个在 Python 环境中编写的库。为了能够将其集成到 R 环境中,必须在计算机上安装 Python 版本。首先,我们需要安装 OpenAI 的gym-http-api API;这些是允许访问越来越多环境的 API。
API,即应用程序编程接口,是创建和集成应用软件的一组定义和协议。它们允许你的产品或服务与其他产品或服务进行通信,而无需知道它们是如何实现的,从而简化了应用开发,并节省了时间和成本。在创建新工具和产品或管理现有工具时,API 提供了灵活性,保证了创新机会,并简化了设计、管理和使用。
- 要下载并安装 OpenAI
gym-http-apiAPI,你可以运行以下 shell 命令。执行这些命令时,必须使用命令窗口:
git clone https://github.com/openai/gym-http-api
cd gym-http-api
pip install -r requirements.txt
上面代码块中的第一行代码使用 Git 软件从 github 仓库下载蜜蜂并进行安装。Git 软件允许你在管理项目的同时保持对源代码及其历史的控制,并且允许更多开发者在项目中协作;它本质上是一个版本控制系统。Git 是开源社区的事实标准,由 Linus Torvald 在 2005 年为 Linux 内核开发而创建,且在创建两个月后由 Google 开发者 Junio Hamano 维护;Git 不断发展,并且完全免费且开源。第二行代码将命令行移动到我们复制仓库的文件夹中。最后,第三行使用 pip 软件安装 API。
上述代码旨在由单个用户在本地运行。包括一个 Python 客户端,用于演示如何与服务器进行交互。
- 要从命令行启动服务器,进入我们安装 API 的文件夹,然后运行以下命令:
python gym_http_server.py
返回以下结果:
Server starting at: http://127.0.0.1:5000
- 现在服务器已准备好与我们的脚本交互。我们可以安装
gym包:
install.packages("gym")
以下表格提供了一些关于gym包的信息:
| 包 | gym |
|---|---|
| 日期 | 2016-10-25 |
| 版本 | 0.1.0 |
| 标题 | 提供对 OpenAI Gym API 的访问 |
| 作者 | Paul Hendricks |
为了验证包的功能,我们可以执行文档中提供的示例脚本。
- 让我们开始导入这个库:
library(gym)
- 首先,我们将与客户端建立连接,以便与服务器进行交互:
RemoteBase <- "http://127.0.0.1:5000"
Client <- create_GymClient(RemoteBase)
print(Client)
create_GymClient()函数实例化一个GymClient实例,以便与 OpenAI Gym 服务器集成。以下结果已打印:
<GymClient: http://127.0.0.1:5000>
- 连接已建立,现在我们可以创建环境:
EnvId <- "CartPole-v0"
InstanceId <- env_create(Client, EnvId)
print(InstanceId)
以下结果已打印:
[1] "376e0df2"
- 现在,我们可以列出当前工作会话中创建的所有环境,并列出可用的蜜蜂:
AllEnvsAvailable <- env_list_all(Client)
print(AllEnvsAvailable)
以下结果已打印:
$`376e0df2`
[1] "CartPole-v0"
- 通过这种方式,我们可以确认模拟环境已正确创建,现在我们可以请求与其交互所需的信息:
ActionSpaceInfo <- env_action_space_info(Client, InstanceId)
print(ActionSpaceInfo)
env_action_space_info()函数评估一个动作是否属于环境的动作空间。以下结果已打印:
$n
[1] 2
$name
[1] "Discrete"
- 在此环境中,只有两个动作可用。现在让我们创建代理:
agent <- random_discrete_agent(ActionSpaceInfo[["n"]])
random_discrete_agent()函数仅创建一个示例随机离散代理。
- 现在,我们将设置一个文件夹来保存结果,并打开一个窗口以监控环境变化:
OutDir <- "/TempFolder/results"
env_monitor_start(Client, InstanceId, OutDir, force = TRUE, resume = FALSE)
- 让我们初始化一些在模拟中需要的变量:
EpisodeCount <- 100
MaxSteps <- 200
Reward <- 0
done <- FALSE
- 现在,我们将实现两个循环来使用随机动作与环境进行交互:
for (i in 1: EpisodeCount) {
Object <- env_reset(Client, InstanceId)
for (i in 1: MaxSteps) {
action <- env_action_space_sample(Client, InstanceId)
results <- env_step(Client, InstanceId, action, render = TRUE)
if (results[["done"]]) break
}
}
返回以下截图:

在截图中,你可以看到小车-杠杆(cart-pole),它将根据算法提供的指示进行移动。
- 最后,我们将使用以下命令关闭窗口:
env_monitor_close(Client, InstanceId)
这个例子帮助我们开始与 OpenAI Gym 中可用的环境进行交互。如果你没有理解某些部分,不用担心,接下来的部分我们可以深入学习它们。
OpenAI Gym 方法
OpenAI Gym 提供了env类,该类封装了环境及其可能的内部动态。该类具有不同的方法和属性,用于实现创建新环境。最重要的方法分别是 reset、step 和 render,我们来简要了解一下它们:
-
reset 方法的任务是重置环境,将其初始化为初始状态。在 reset 方法中,必须包含构成环境的元素的定义,在本例中是机械臂、待抓取物体及其支撑物的定义。
-
step 方法的任务是将环境向前推进一步。它需要输入要执行的动作,并返回代理的新观察结果。在该方法中,必须定义运动动态的管理、状态和奖励的计算以及完成回合的控制。
-
第三种也是最后一种方法是我们必须定义要渲染到哪个内部,因为每个步骤的元素都必须被表示出来。该方法涉及不同类型的渲染,例如人类、
rgb_array或ansi。使用人类类型时,渲染在屏幕或命令行界面上进行,且该方法不返回任何内容;使用rgb_array类型时,调用该方法会返回一个表示屏幕 RGB 像素的 n 维数组;选择第三种类型时,返回方法会返回一个包含文本表示的字符串。为了渲染,OpenAI Gym 提供了一个 viewer 类,通过它可以将环境的元素绘制为一组多边形和圆形。
关于环境的属性,env类提供了动作空间、观察空间和奖励范围的定义。动作空间属性表示动作空间,即智能体在环境中可以执行的可能动作的集合。通过观察空间属性,定义组成状态的参数的数量,并且为每个参数定义可以假设的值范围。奖励范围属性包含在环境中可以获得的最小和最大奖励,默认设置为(-∞,+∞)。
使用框架所提出的env类作为新环境的基础,采用工具包提供的通用接口。通过这种方式,创建的环境可以被集成到工具包库中,并且它们的动态可以通过 OpenAI Gym 社区用户已实现的算法进行学习。
在接下来的部分中,我们将使用 OpenAI Gym 环境实现一个机器人控制系统。
使用 FrozenLake 环境的机器人控制系统
从技术角度讲,机器人可以被看作是一种特殊类型的自动控制,即一种物理上位于环境中的自动化装置,它能够通过传感器感知环境的某些特征,并且可以执行动作以改变环境。这些动作是通过所谓的执行器来完成的。
传感器所做的测量与执行器所给出的命令之间的所有内容可以被定义为控制程序或机器人的控制器。这是机器人智能编码的组件,在某种意义上,它构成了必须引导其行动以获得期望行为的大脑。控制器可以通过多种方式实现:通常它是运行在一个或多个微控制器上的软件,这些微控制器物理集成在系统中(车载),但它也可以通过电子电路(模拟或数字)直接连接到机器人的硬件中。让我们从观察环境开始。
FrozenLake 环境
FrozenLake 环境(gym.openai.com/envs/FrozenLake-v0/)是一个 4 x 4 的网格,包含四个可能的区域:安全(S)、冰面(F)、洞(H)和目标(G)。智能体在网格世界中控制一个角色的移动,并在网格中移动,直到到达目标或掉入洞中。网格中的一些瓷砖是可行走的,而其他瓷砖则会导致智能体掉进水里。如果掉进洞里,智能体必须从头开始,并且奖励为 0。智能体沿着一条不确定的路径移动,该路径仅部分依赖于所选择的方向。智能体在找到通向设定目标的可能路径时会获得奖励。智能体有四个可用的移动方向:上、下、左和右。该过程将持续,直到智能体从每次失败中学习并最终到达目标。
该表面使用如下网格进行描述:
-
SFFF (S: 起点,安全)
-
FHFH (F: 冰面,安全)
-
FFFH (H: 洞,掉进深渊)
-
HFFG (G: 目标,飞盘所在位置)
在下图中,我们可以看到 FrozenLake 网格(4 x 4):

当你到达目标或掉进洞里时,剧集结束。如果你到达目标,你将获得奖励 1,否则奖励为 0。
Q 学习解决方案
正如我们在第七章中所说的,时序差分学习,Q 学习是最常用的强化学习算法之一。这是因为它能够在不需要环境模型的情况下比较可用动作的期望效用。得益于这一技术,可以为每个给定状态找到一个最优动作,这适用于完备的 马尔可夫决策过程(MDP)。
在以下代码中,我们将使用 Q 学习方法,从起点单元格到目标单元格在 4 x 4 网格环境中找到正确的路径:
- 一如既往,我们将逐行分析代码。让我们从导入库开始:
library(gym)
- 首先,我们将建立与客户端的连接,以便与服务器进行交互:
remote_base <- "http://127.0.0.1:5000"
client <- create_GymClient(remote_base)
print(client)
create_GymClient() 函数实例化一个 GymClient 实例,以便与 OpenAI Gym 服务器进行集成。以下结果被打印出来:
<GymClient: http://127.0.0.1:5000>
- 连接已经建立;现在我们可以创建环境:
env_id <- "FrozenLake-v0"
instance_id <- env_create(client, env_id)
print(instance_id)
以下结果被打印出来:
[1] "af775b0a"
- 现在我们可以列出当前工作会话中通过蜜蜂创建的所有环境:
AllEnvsAvailable <- env_list_all(Client)
print(AllEnvsAvailable)
以下结果被打印出来:
$af775b0a
[1] "FrozenLake-v0"
通过这种方式,我们确认模拟环境已被正确创建。
- 现在,我们将从环境中获取一些信息:
ActionSpaceInfo <- env_action_space_info(client, instance_id)
print(ActionSpaceInfo)
env_action_space_info() 函数评估某个动作是否为环境动作空间的成员。以下结果被打印出来:
$n
[1] 4
$name
[1] "Discrete"
在这个环境中,有以下四个可用的动作:
-
0: 向左移动
-
1: 向下移动
-
2: 向右移动
-
3: 向上移动
- 现在,让我们来看一下一个在此环境中移动的智能体可以采取的状态:
ObservationSpaceInfo <- env_observation_space_info(client, instance_id)
print(ObservationSpaceInfo)
env_observation_space_info() 函数获取环境观察空间的信息(名称和维度/边界)。以下结果被打印出来:
$n
[1] 16
$name
[1] "Discrete"
从以下图示中,我们可以看到这个环境中有十六个状态(0 – 15),覆盖一个 4x4 的网格,从左到右、从上到下依次编号,如下所示:

- 然后我们将提取这些值以便重新使用:
ActionSize = action_space_info$n
StateSize = observation_space_info$n
- 现在,初始化参数,从
QTable开始:
Qtable <- matrix(data = 0, nrow = StateSize, ncol = ActionSize)
QTable 的行数等于观察空间的大小 (observation_space_info$n),而列数等于动作空间的大小 (action_space_info$n)。正如我们所说,FrozenLake 环境为 4x4 网格中的每个单元提供一个状态,并且提供四个动作,从而返回一个 16 x 4 的表格。
这个表格通过 matrix() 函数初始化为全零,初始化方式如下:
print(Qtable)
接下来,打印出以下表格:
[,1] [,2] [,3] [,4]
[1,] 0 0 0 0
[2,] 0 0 0 0
[3,] 0 0 0 0
[4,] 0 0 0 0
[5,] 0 0 0 0
[6,] 0 0 0 0
[7,] 0 0 0 0
[8,] 0 0 0 0
[9,] 0 0 0 0
[10,] 0 0 0 0
[11,] 0 0 0 0
[12,] 0 0 0 0
[13,] 0 0 0 0
[14,] 0 0 0 0
[15,] 0 0 0 0
[16,] 0 0 0 0
现在,我们定义一些参数:
alpha = 0.80
gamma = 0.95
基本上,alpha 是学习率,gamma 是折扣因子。学习率处理已获取信息的更新,它确定何时该用新的获取信息替代旧的。将学习率设置为 0 表示智能体不会学习任何东西,只会利用以前的知识。将学习率设置为 1 时,智能体只考虑最新的信息,并忽略以前的知识。这就是探索与利用的困境。理想情况下,智能体必须探索每个状态下的所有可能动作,找到那个实际上最能通过利用它来达成目标的动作。折扣因子决定了未来奖励的重要性。因子为 0 时,表示只考虑当前奖励,而接近 1 时,智能体将更加努力地追求长期的高奖励。
- 现在,我们设置 episode 的数量:
NumEpisodes = 200
NumEpisodes 参数的含义如下。智能体通过经验进行学习,没有导师来指导它;这种方式表示没有监督的学习。智能体将持续探索,直到达到目标,从一个状态移动到下一个状态。每次探索称为一次episode。每个 episode 包括智能体从初始状态到目标状态的移动。每当智能体到达目标状态时,我们就开始下一个 episode。
- 现在,我们将创建一个列表来保存总奖励:
RewardsList = list()
然后初始化另外两个参数;我们将需要它们来恢复结果:
IndxList = 1
NumGoal=0
- 在这一点上,设置好参数后,就可以开始 Q-learning 循环了:
for (i in 1:NumEpisodes) {
cat("######Episode ", i, " ######", "\n")
- 所以,我们通过
env_reset()方法初始化系统:
state = env_reset(client, instance_id)
然后,初始化一个奖励计数器和周期计数器:
SumReward = 0
j = 0
从这一点出发,Q-learning 表格算法被实现:
while (j < 99){
- 每当进入新的一步时,我们就增加周期计数器:
j=j+1
- 现在,我们需要选择一个动作:
action = which.max(Qtable[state+1,] + runif(4,0, 1)*(1./(i+1)))
action = action - 1
通过贪心方法从Qtable中选择动作。由于环境是未知的,因此必须以某种方式探索,代理将利用随机性的力量来进行探索。使用了两个函数,which.max()和runif()。which.max()函数返回沿某一轴的最大值的索引。runif()函数返回一个标准正态分布的样本(或多个样本)。在刚分析的代码块的第二行中,我们将动作索引减少了一单位。这是必要的,因为如前所述,环境中的可用动作范围是 0 到 3,而在 r 中表格的索引从 1 开始(与 Python 不同,后者从 0 开始)。因此,为了使结果与 r 环境兼容,必须进行这一修正。
- 现在,我们将使用
env_step()方法来返回根据我们传给它的动作作出的新状态。显然,我们传给方法的动作是我们刚刚决定的:
Results<- env_step(client, instance_id, action, render = TRUE)
env_step()方法返回一个包含以下内容的列表:
-
action: 在环境中采取的动作 -
observation: 代理对当前环境的观察 -
reward: 上一个动作后返回的奖励数量 -
done: 回合是否已结束 -
info: 包含辅助诊断信息的列表
- 目前,我们只对以下代码感兴趣:
NewState<-Results$observation
ActualReward<-Results$reward
- 然后,我们可以用新知识更新
Qtable字段:
Qtable[state+1, action+1] = (1 - alpha) * Qtable[state+1, action+1] + alpha * (ActualReward + gamma * max(Qtable[NewState+1,]))
用于 Q 函数更新的公式如下:

同时,在这种情况下,我们必须为状态和动作引入修正(状态 + 1,动作 + 1),因为 FrozenLake 环境期望状态值为 0-15,动作值为 0-3,但我们知道在 r 中行和列索引是从 1 开始的。
- 现在,我们将用刚刚获得的奖励和环境状态更新奖励总和:
SumReward = SumReward + ActualReward
state = NewState
- 最后,我们插入一个检查,查看是否已经到达回合结束的条件。关于这一点,我们记得当到达目标或掉入陷阱时,回合被视为结束:
if (Results[["done"]]) {
print("#####STEP######")
print(j)
cat("State achieved", NewState+1, "\n")
cat("Reward", ActualReward, "\n")
if (state==15) {
NumGoal=NumGoal+1
}
break
}
}
用于回合结束检查的if循环包含一系列指令,帮助我们找到解决方案。实际上,我插入了一个控制,用来区分是否到达目标或掉入陷阱,然后是一系列打印输出,让我们能够验证算法的成功。
- 在最终离开贯穿各个回合的循环之前,我们将尝试更新奖励总和和奖励列表的索引:
RewardsList[IndxList]<-SumReward
IndxList = IndxList +1
}
- 最后,我们打印结果,首先是分数:
cat("Numbers of goal achieved ", NumGoal, "\n")
print ("Score: ")
print(do.call(sum,RewardsList)/NumEpisodes)
然后,我们打印Qtable的值:
print ("Final Q-Table Values")
print (Qtable)
返回以下表格:
[,1] [,2] [,3] [,4]
[1,] 8.269208e-02 2.915901e-03 0.0026752394 2.927068e-03
[2,] 3.346197e-05 4.689209e-05 0.0001546271 1.524209e-01
[3,] 6.866991e-04 9.183121e-04 0.0032547141 2.434100e-01
[4,] 1.191637e-03 5.130816e-04 0.0005775460 7.667945e-02
[5,] 8.717573e-02 2.206559e-04 0.0002355794 3.511920e-04
[6,] 0.000000e+00 0.000000e+00 0.0000000000 0.000000e+00
[7,] 6.317772e-05 4.168548e-05 0.1003525858 6.572051e-05
[8,] 0.000000e+00 0.000000e+00 0.0000000000 0.000000e+00
[9,] 2.501462e-03 9.431424e-04 0.0022643051 7.393004e-02
[10,] 4.658178e-05 5.656864e-01 0.0000000000 1.469386e-04
[11,] 1.261091e-02 4.726838e-04 0.0005489759 2.321079e-03
[12,] 0.000000e+00 0.000000e+00 0.0000000000 0.000000e+00
[13,] 0.000000e+00 0.000000e+00 0.0000000000 0.000000e+00
[14,] 8.009153e-04 0.000000e+00 0.8782760471 2.432799e-04
[15,] 0.000000e+00 9.929596e-01 0.0000000000 0.000000e+00
[16,] 0.000000e+00 0.000000e+00 0.0000000000 0.000000e+00
包含价值函数的表格已经准备好,我们可以使用它来提取从起始单元到目标单元的路径,而不掉入陷阱。
- 首先,我们将搜索限制在五条可能的路径上:
for (episode in 1:5) {
print("****************EPISODE**********************")
print(episode)
- 为了做到这一点,我们使用了一个循环,并包括了每个回合的打印输出以进行检查。现在,正如我们在训练阶段所做的那样,开始吧:使用
env_reset()函数重置环境:
state = env_reset(client, instance_id)
在这方面,我们回顾一下,env_reset()函数将环境恢复到初始状态,即状态 0,对应于 Qtable 的第一行。正如我们之前所强调的,为了使 OpenAI Gym 环境与该环境兼容,需要通过添加一个单位进行修正。这样,状态 0 就会对应于 Qtable 的第一行。
- 接下来的
for循环将允许我们在网格中移动,以按照Qtable提供的方向到达目标:
for (step in 1:100) {
- 现在,让我们初始化一个奖励计数器:
TotRew = 0
- 现在,我们将提取在给定实际状态下具有最大预期未来回报的动作,即与当前步骤相关的动作。Qtable 返回此值作为当前状态对应行中最大值的索引:
action = which.max(Qtable[state+1,])
action = action - 1
我们再次将动作索引减去一个单位。
- 现在,我们将使用
env_step()方法返回响应提取的动作后的新状态:
Results<- env_step(client, instance_id, action, render = TRUE)
对于env_step()函数返回的值,我们目前只关心状态和奖励:
Newstate<-Results$observation
reward<-Results$reward
- 接下来,我们将更新奖励计数器:
TotRew <- TotRew + reward
- 现在,我们将检查是否已到达回合的结束:
if (Results[["done"]]) {
cat("Number of steps", step, "\n")
print(TotRew)
cat("STATO", Newstate, "\n")
break
}
state = Newstate
}
}
通过这种方式,我们得到了五条最佳路线,可以在不掉入陷阱的情况下到达目标。
总结
在这一章节中,我们学习了博弈论的基本概念。我们了解了游戏的基本特征,以及所采用的解决方案如何帮助我们解决现实问题。我们分析了一系列实际应用,在这些应用中使用了这些理论来获得解决方案。接着,我们详细分析了 OpenAI Gym 库,以及如何与可用的环境进行交互以模拟现实问题。
接着,我们使用强化学习解决了井字游戏问题。我们使用tictactoe包来设置环境,并通过 Q 学习训练代理玩游戏。最后,我们查看了 FrozenLake 环境。这个环境是一个 4 × 4 的网格,包含四个可能的区域:安全区(S)、冰面(F)、陷阱(H)和目标(G)。代理控制一个角色在网格世界中的移动,直到它到达目标或掉进陷阱。这个环境特别适合模拟与机器人在充满障碍物的环境中的移动问题。定义环境之后,我们创建了一个能够在环境中移动并使用基于 Q 学习的算法找到目标的代理。
在接下来的章节中,我们将学习金融问题的基本概念;如何预测股市价格,以及如何优化股票投资组合。然后,我们将了解如何实现欺诈检测技术。
第九章:金融工程中的多臂老丨虎丨机(MAB)
今天,机器学习(ML)在金融生态系统中的许多领域已扮演着至关重要的角色:从贷款批准、活动管理到风险评估。人工智能(AI)系统在股票投资组合管理和交易算法的创建中具有重要作用。同样,AI 在欺诈检测中也变得越来越重要,用于寻找和识别欺诈行为。能够学习并根据新威胁调整行为的算法正在克服传统依赖严格检查清单的过程的局限性。本章将讨论一些金融工程问题,采用强化学习(RL)来学习优化和异常识别技术。
到本章结束时,我们将了解金融问题的基本概念,以及如何使用马尔可夫链来建模信用风险。我们还将理解如何构建一个定价优化系统,以找到推出新产品的最佳价格。最后,我们将学习如何优化股票投资组合。
本章将涵盖以下主题:
-
金融问题要点
-
将信用风险建模为马尔可夫链
-
定价优化系统
-
优化股票投资组合
-
欺诈检测技术
技术要求
查看以下视频,了解代码的实际应用:
金融问题要点
技术创新与金融领域已共同发展多年,并随着机器学习的引入而加速推进。大量的数据、访问精确历史文档的可能性以及金融领域的量化特性,使其成为集成自动学习的最合适行业之一。通过这种方式,行业从业者能够摆脱一系列低创意价值的必要活动。
机器学习使机器能够自主决策:与执行活动的指令序列不同,它提供了如何学习独立完成这些活动的指令。这种自我学习的能力,结合大数据分析和特殊算法,已在各种金融操作中扮演着基础性角色,从风险评估到贷款批准。
通过机器学习,金融公司可以创新其工作活动,提高效率、产出,并最终提升盈利能力。了解这些公司所进行的技术演进揭示了诸多机会,同时也凸显了需要识别变化、欢迎并管理变化的必要性。
在复杂的金融活动体系中,很难假设有任何一个领域是机器学习无法介入的,去加速、简化和精简程序,也正因如此,正是在这个背景下,关于人工员工在未来将扮演何种角色的深刻反思才得以展开。但不必担心:从持续增长的角度来看,金融机构将仅仅引导其员工从事高附加值的活动,在这些活动中,人工代理将作为重要的顾问,尽管无法扮演决策者的角色。
金融一直以来都充满数字,但在最近几十年,它的复杂性不断增长。首先是数字革命,随后是通信革命,促使各种数据的可用性呈指数级增长。社交网络上的互动、搜索引擎上的查询、银行交易和电子商务活动,都增加了可以自动分析的数字信息的庞大目录。
由于需要了解和稳定日益复杂的信用体系,电子商务网站内容推荐系统产生了对数据管理服务的新需求。对于金融而言,几乎是完美市场中的竞争压力促使持续改进。我们正目睹着代理商之间不断的张力:一些代理商试图打破平衡,追求新的利润形式,而另一些代理商则推向平衡点,力图实现小规模的、系统性的利润。
在金融领域,有许多领域可以利用人工代理来简化活动,如下图所示:

在接下来的章节中,我们将分析使用基于机器学习算法的最广泛应用活动。
金融交易
金融交易是一种高收益的投机性投资活动,涉及到金融市场。金融市场被定义为金融资产交易的场所。金融市场的交易基于一组因素:技术分析、微观和宏观经济基本面、新闻和市场情绪。当我们谈到高频交易时,仓位的保持时间甚至可能只有几分之一秒,这时由高级算法管理的作用是至关重要的。机器学习的引入使得技术得到了强有力的细化,可以通过各种指标进行战略性处理,用来识别套利机会、评估绩效、学习关键问题,并将改进的模型应用于即将到来的交易机会。
信用评分
信用评估过程是商业银行业务的支柱,并且随着新的方法论的出现以及对越来越大数据集的访问,已经得到了多年的精细化发展。在这一活动中,通常被称为信用评分,机器学习能够发挥关键作用,许多机构已经在这一领域有所作为,这也并非巧合。在这里,机器学习的作用体现在扩展现有流程:除了所有经过验证的参数,它们仍然是信用评分的基础,机器学习还可以通过分析非常异质的数据,如社交网络上的行为,来完善评估过程。
金融聊天机器人
在银行业中,机器学习的早期应用之一是虚拟助手。说到成本降低,将大量客户服务工作委托给由机器学习支持的机器人是极为高效的。此外,应用这一规则,常见的例行请求(如反索赔、转账和取款)是最常见的,也是最容易交给软件处理的。美国银行在 2016 年推出其 Erica 机器人,旨在简化个人财务管理,服务数千万客户,这并非巧合。
机器人顾问
一个虚拟助手的例子是机器人顾问,和简单的常规操作自动化相比,它属于金融咨询领域。通过机器人顾问,用户可以根据自身目标获得个性化的资产管理建议,可能还可以买卖金融产品。同样,机器人顾问还可以跟踪投资趋势,向客户提供定期报告,并提出任何必要的纠正措施。
在下一部分,我们将使用马尔可夫链来模拟公司信用风险。
将信用风险建模为马尔可夫链
金融公司面临着不同类型的风险,通常分为三大类:市场风险、信用风险和操作风险。在近年来的国际金融环境中,信用评分在被定义为信用风险分析的过程中发挥了至关重要的作用。信用评分模型的目标是能够正确地分类哪些申请人能获得信贷渠道的访问权限,从而减少与申请人可能违约相关的风险。继美国因次贷危机爆发后,金融和信贷机构在过去十年中遭遇了重大损失,随后进入了漫长的衰退期。所有这些不可避免地将整个信用风险分析过程置于放大镜下,特别强调了信用评分技术。
信用评分在近几十年来经历了显著的增长。在这些发展之前,贷款通常是根据主观评估来授予的,评估依据是申请人和信用机构的信用分析师之间建立的人际关系。在大多数情况下,信用分析师通常是代表着提供资金的银行。然而,日益增长的融资需求和众多可用的金融产品迫使我们不得不调整信用管理系统。我们越来越多地尝试应用自动化和客观的技术,使得对潜在客户的分析可以更快速、更高效。这些技术的输出结果通常是采用一个评分,能够可靠地分类客户的信用状况。
对信用风险的正确评估对于银行的所有活动能够成功管理至关重要。金融中介机构需要提高风险评估的准确性,这促使了新模型和新技术的发展、更新和发现,用于计算违约概率和预期回收率。对债务人信用状况演变的精确分析,以及对影响信用风险的参数的正确估计,也是解决与贷款和债券定价相关问题的必要条件。在各种模型和成熟的方法中,现代方法利用了新的数据存储、分析和统计理论训练的技术能力,以便更准确地识别信用风险的组成部分。
在下一节中,我们将介绍使用 R 中已存在的一些软件包进行信用评分建模。
CreditMetrics软件包
为了处理信用评分技术,我们将使用在 CRAN 上可用的CreditMetrics软件包。该软件包包含一组用于计算CreditMetrics风险模型的函数。
下表提供了关于此软件包的一些信息:
| 软件包 | CreditMetrics |
|---|---|
| 日期 | 2015-02-19 |
| 版本 | 0.0-2 |
| 标题 | 用于计算CreditMetrics风险模型的函数 |
| 作者 | 安德烈亚斯·维特曼 |
CreditMetrics 模型首次由 J.P. 摩根提出,作为计算与信贷风险敞口相关的风险价值(VAR)的工具。该模型是最广泛使用的信用风险管理系统之一。它旨在定义贷款组合的构成,使其与中介所期望的风险-回报组合一致,从而实现更高效的资本配置。特别地,信用风险值会随着与投资组合中应收账款相关的质量变化而变化,导致单一信用(迁移)在识别的三个主要类别之间的变动(财务困境、信用评级的升级或改善,以及与单一贷款相关的质量下降或恶化)。信用风险与市场风险(riskmetrics)有显著不同,因为对于后者,可以假设相对于其平均值对称的正态概率分布,而与暴露于信用风险的投资组合相关的风险则是正向非对称的。在下一节中,我们将讨论一个使用CreditMetrics包进行信用风险建模的实际案例。
CreditMetrics 风险模型
信用风险是指与对方的偿付能力或信用状况相关的意外事件,可能会改变信用头寸的价值。
在评估信用风险时,我们可以区分违约模式方法,它只考虑破产的发生,以及多状态方法,在这种方法中,即使对方信用状况的恶化也代表了信用风险的来源。
第一种方法设想了信用头寸的两种可能状态:违约或非违约。违约事件的发生仅由相关的二元变量的违约概率决定。多状态方法考虑了迁移风险,即由于对方信用状况的恶化,头寸价值发生变化的风险。违约状态代表可能的一个状态,而在该状态下的迁移与破产事件一致。
多状态方法的基本工具是转移矩阵,可以基于历史观察来估计,并由评级机构提供。
我们将通过定义迁移矩阵开始分析,正如我们预见的那样,该矩阵由评级机构提供:
- 首先,我们将导入
CreditMetrics库:
library(CreditMetrics)
- 现在,让我们复现这个实验:
set.seed(1)
这个命令设置了 R 随机数生成器的种子。每次想要得到可重现的随机结果时,都必须使用这个函数。使用此命令时生成的随机数将始终相同,因此每次执行代码时,得到的结果也将始终相同。每个种子值将对应于给定随机数生成器生成的数值序列。
现在,让我们定义迁移矩阵,它表示马尔可夫过程的转移矩阵。评级机构,如 Standard% Poor,基于公司不同行业超过 20 年的观察数据提供转移矩阵。
有七个评级类别,从最高的 AAA 类到最低的 CCC 类。最后一个状态是默认状态,其特征是吸收状态,在这种状态下,退出的概率为零。
- 然后,我们定义评级类别:
RatingClasses <- c("AAA", "AA", "A", "BBB", "BB", "B", "CCC", "D")
- 做完这些后,我们定义了转移矩阵:
TransitionMatrix <- matrix(c(90.710, 8.340, 0.710, 0.075, 0.095, 0.025, 0.022, 0.023,
0.710, 90.550, 7.810, 0.720, 0.060, 0.120, 0.020, 0.010,
0.092, 2.220, 91.250, 5.420, 0.720, 0.230, 0.011, 0.057,
0.020, 0.420, 5.890, 85.880, 5.290, 1.190, 1.140, 0.170,
0.036, 0.124, 0.670, 7.730, 80.590, 8.790, 1.010, 1.050,
0.011, 0.119, 0.230, 0.440, 6.510, 83.440, 4.060, 5.190,
0.220, 0.000, 0.230, 1.330, 2.360, 11.210, 64.830, 19.820,
0, 0, 0, 0, 0, 0, 0, 100
)/100, 8, 8, dimnames = list(RatingClasses, RatingClasses), byrow = TRUE)
打印以下矩阵:
AAA AA A BBB BB B CCC D
AAA 0.90710 0.08340 0.0071 0.00075 0.00095 0.00025 0.00022 0.00023
AA 0.00710 0.90550 0.0781 0.00720 0.00060 0.00120 0.00020 0.00010
A 0.00092 0.02220 0.9125 0.05420 0.00720 0.00230 0.00011 0.00057
BBB 0.00020 0.00420 0.0589 0.85880 0.05290 0.01190 0.01140 0.00170
BB 0.00036 0.00124 0.0067 0.07730 0.80590 0.08790 0.01010 0.01050
B 0.00011 0.00119 0.0023 0.00440 0.06510 0.83440 0.04060 0.05190
CCC 0.00220 0.00000 0.0023 0.01330 0.02360 0.11210 0.64830 0.19820
D 0.00000 0.00000 0.0000 0.00000 0.00000 0.00000 0.00000 1.00000
在这个表中,第一列的评级是起始或当前评级。第一行的评级是风险视野处的评级。此外,矩阵的每一行都总和为 100%。现在清楚了转移矩阵的含义。这是一个包含转移概率的方阵。例如,AAA 评级的信用有 8.33% 的概率在一年内降级为 AA 评级。通过这种方式,我们可以恢复出在任何一年内最有可能发生的信用评级,作为当前评级。作为一个规则,转移矩阵应该是在我们估计风险的风险视野内计算的同样时间间隔。在我们的情况下,我们对提供的值感到满意:
- 首先,我们可以创建
markovchain对象,如下所示:
MCModel <- new("markovchain", transitionMatrix = TransitionMatrix, states=RatingClasses, byrow = TRUE, name="MarkovChainModel")
markovchain 类已被设计用来处理同质马尔可夫链过程。
传递了以下插槽:
-
-
transitionMatrix: 这是包含转移概率的方阵转移矩阵。 -
states: 这是状态的名称。它必须与转移矩阵的colnames和rownames相同,这是一个字符向量,列出了定义了转移概率的状态。 -
byrow: 这是一个二进制标志,一个逻辑元素,指示转移概率是按行还是按列显示。 -
name: 这是一个可选的字符元素,用于命名离散时间马尔可夫链。
-
- 要显示我们刚刚创建的模型的摘要,使用以下命令:
MCModel
返回以下结果:
MCModel
A 8 - dimensional discrete Markov Chain defined by the following states:
AAA, AA, A, BBB, BB, B, CCC, D
The transition matrix (by rows) is defined as follows:
AAA AA A BBB BB B CCC D
AAA 0.90710 0.08340 0.0071 0.00075 0.00095 0.00025 0.00022 0.00023
AA 0.00710 0.90550 0.0781 0.00720 0.00060 0.00120 0.00020 0.00010
A 0.00092 0.02220 0.9125 0.05420 0.00720 0.00230 0.00011 0.00057
BBB 0.00020 0.00420 0.0589 0.85880 0.05290 0.01190 0.01140 0.00170
BB 0.00036 0.00124 0.0067 0.07730 0.80590 0.08790 0.01010 0.01050
B 0.00011 0.00119 0.0023 0.00440 0.06510 0.83440 0.04060 0.05190
CCC 0.00220 0.00000 0.0023 0.01330 0.02360 0.11210 0.64830 0.19820
D 0.00000 0.00000 0.0000 0.00000 0.00000 0.00000 0.00000 1.00000
正如我们所见,列出了四个插槽——states、byrow、transitionMatrix 和 name。要检索每个插槽中包含的元素,使用对象的名称(MarkovChainModel),后跟插槽的名称,用@符号分隔。
- 例如,要打印状态,我们将写以下内容:
MCModel@states
返回以下结果:
[1] "AAA" "AA" "A" "BBB" "BB" "B" "CCC" "D"
我们可以从新开发的模型中提取的其他信息是过渡概率,表示从一种状态过渡到另一种状态的概率。请记住,如果马尔可夫链在时间上是齐次的,那么从一个状态到另一个状态的过渡概率是独立于时间指数的。为了获取此信息,我们将使用 transitionProbability() 函数,它允许我们获取从初始状态到后续状态的过渡概率。
- 让我们学习如何获取这些信息:
transitionProbability(MCModel,"AAA","AA")
返回以下结果:
[1] 0.0834
我们可以通过分析过渡矩阵来确认这一结果。在该矩阵中,从 AAA 状态到 AA 状态的过渡由元素 p[12]给出,其值正好等于 0.083。
现在,让我们计算迁移矩阵的信用违约利差。信用违约利差指一系列用于确定投资者支付多少来补偿所承担的证券固有信用风险的措施。
要计算信用违约利差,我们将使用 cm.cs() 函数,它需要迁移矩阵和违约给予损失作为输入。违约给予损失表示信用机构由于交易对手破产而遭受的损失,仅在信用恢复操作结束时才能确定。因此,它从不是预先可预测的;它只有在信用恢复操作结束时才发生。
- 在我们的情况下,我们将此值作为初始变量设置如下:
LGD <- 0.40
- 上述代码表明,在破产情况下,银行的损失将为 40%。现在,我们可以应用
cm.cs()函数,如下所示:
CreditRiskSpread<-cm.cs(TransitionMatrix, LGD)
- 此函数返回迁移矩阵中每个评级在时间t = 1的信用违约利差,如下所示:
AAA AA A BBB BB
9.200423e-05 4.000080e-05 2.280260e-04 6.802313e-04 4.208845e-03
B CCC
2.097852e-02 8.259931e-02
- 让我们计算一年内的信用价值。为此,我们可以使用
cm.ref()函数。需要以下参数:
-
-
迁移矩阵,其中最后一行给出违约类别
-
违约给予损失
-
公司评级
-
违约风险
-
无风险利率
-
前两个参数已经定义并使用,即迁移矩阵和违约给予损失。对于剩余的三个参数,需要初始化它们。
- 让我们从我们想要评估其信用价值的公司的评级开始。我们将设置三个值,如下:
Rating <- c("B", "BB", "CCC")
违约暴露(EAD)是银行可能由于违约而遭受的损失金额。由于违约发生在未来的未知日期,这种损失取决于银行在违约时对借款人暴露的金额。
- 我们将设置三个值,如下:
EAD <- c(4000, 10000, 500000)
- 然后,我们需要设置无风险利率;该参数表示投资者在给定时间段内从完全无风险投资中预期的利率。我们将此值设置如下:
Rindex <- 0.02
现在我们已经有了 cm.ref() 函数所需的所有参数,我们只需应用它:
RefValue<-cm.ref(TransitionMatrix, LGD, EAD, Rindex, Rating)
返回两个值:
-
-
constVal:一年内的信用价值 -
constPV:一年内所有信用值的投资组合
-
- 要提取这些值,我们可以使用以下命令:
RefValue$constVal
RefValue$constPV
以下结果被打印:
> RefValue$constVal
B BB CCC
3839.399 9760.818 451244.261
> RefValue$constPV
[1] 464844.5
- 最后,我们可以搜索模型的吸收状态。记住,吸收状态是一种一旦进入就无法退出的状态。为了评估吸收状态,我们可以使用
absorbingStates()函数,方法如下:
absorbingStates(MCModel)
返回以下结果:
[1] "D"
很容易理解,默认状态是一种吸收状态,一旦进入,就无法再退出。在下一节中,我们将使用多臂赌博机分析来解决价格优化问题。
定价优化系统
定价不仅仅是将价格与市场上竞争对手的价格对齐。全球贸易的一个负面方面是,总有某些地方有人以远低于市场价格的价格出售相同的产品。自动将价格与竞争对手对齐并不是一个可持续的策略。
有两种定价策略。第一种考虑的是交易员的观点,提供了一种直观的定价方法,根据该方法,选择正确的价格必须依赖于经理的直觉。由于这种策略非常主观,且结果在很大程度上取决于经理的能力,因此很难评估它。如果经理离开公司,关于该策略的大部分知识将会丧失。第二种策略基于更理论化的观点,提供了一种计量经济学定价方法,在该方法中,价格的选择必须基于先进的数学参数。
一旦我们选择了适用于金融产品的价格范围,采用一种优化程序来评估市场对我们想要推出的新产品的反应是一个良好的做法。在最简单的情况下,优化问题包括通过系统地选择允许集合中的输入值并计算函数值来最大化或最小化一个实数函数。理论和优化技术的推广到其他公式构成了应用数学的一个广阔领域。更一般地说,优化包括在每个领域中寻找一些目标函数的最佳可用值,涵盖了各种不同类型的目标函数和领域。
有许多优化方法可用;以下是一些最重要的方法:
-
线性规划
-
最小二乘法
-
单纯形算法
-
拉格朗日乘数法
-
随机优化
在这个例子中,我们将尝试通过多臂赌博机分析和 Bandit R 包来解决价格优化问题。欲了解更多关于多臂赌博机算法的信息,请参见第四章,多臂赌博机模型。我们将从介绍 Bandit 包开始。
Bandit 包
该包包含一组用于分析 A/B 拆分测试数据和一般网页指标的函数。以下表格提供了有关该包的一些信息:
| 包 | Bandit |
|---|---|
| 日期 | 2015-02-19 |
| 版本 | 0.5.0 |
| 标题 | 用于简单 A/B 拆分测试和多臂强盗分析的函数 |
| 作者 | 托马斯·洛茨和马库斯·勒歇尔 |
在接下来的章节中,我们将形式化这个问题,并通过多臂强盗分析来解决它。
新金融服务的定价策略
一家在市场上非常活跃的金融公司正在考虑推出一项新的金融服务。在测试了市场上竞争对手已提供的所有服务并对其性能进行了比较后,该公司决定为其自己推出的服务设定一系列价格。
为了检查哪种确定的价格最能满足消费者的需求,他们决定通过收集服务订阅来测试四种不同的价格。每个价格将会在一段时间内归属于该服务,在此期间,将记录已经联系过的用户和已获得的订阅。
监控活动将持续三个月,每个月都会进行分析。最后,将采用表现最佳的价格。请按照以下步骤操作:
- 首先,我们需要导入
Bandit库:
library(Bandit)
- 现在,让我们重现实验:
set.seed(1)
- 现在,我们报告在第一个月内收集的数据,涉及已联系的用户、已购买的服务以及提供的价格:
UsersContacted1 <- c(10000, 9580, 10011, 10007)
Purchases1 <- c(571, 579, 563, 312)
Prices <- c(299, 306, 312, 335)
- 如我们所见,这里有三个向量,其中包含每个类别的数据。现在,我们可以开始模拟不同价格下每个手臂的后验分布:
PostDistr1Month = sim_post(Purchases1,UsersContacted1, ndraws = 10000)
在这里,我们使用了sim_post()函数,该函数模拟了每个手臂作为最优二项分布强盗的贝叶斯概率的后验分布。以下是传递的参数:
-
-
Purchases1:成功次数的向量 -
UsersContacted1:试验次数的向量 -
ndraws:来自后验的随机抽样次数
-
通过这种方式,我们获得了 10,000 次对后验分布的评估,用于贝叶斯概率的估计。以下是前 10 次评估:
[,1] [,2] [,3] [,4]
[1,] 0.05836046 0.06371775 0.05441352 0.03088567
[2,] 0.05829739 0.06186407 0.05624850 0.02976481
[3,] 0.05621800 0.05805901 0.05795378 0.03280708
[4,] 0.05841852 0.06459235 0.05968413 0.03087549
[5,] 0.05342895 0.06145472 0.05901734 0.03027870
[6,] 0.05382709 0.06170789 0.05352246 0.03130340
[7,] 0.05566469 0.05698650 0.05440684 0.03335165
[8,] 0.05723522 0.06025616 0.05833294 0.03082094
[9,] 0.05617222 0.06314493 0.05721080 0.03309235
[10,] 0.05308393 0.05988580 0.05163597 0.03038333
- 因此,我们可以评估赢家:
ProbWinner1 <- prob_winner(PostDistr1Month)
使用了prob_winner()函数,该函数计算每个手臂在给定前一步模拟后的结果时成为赢家的概率。该函数只接受一个参数——sim_post()函数提供的后验模拟结果。
- 我们现在为我们模拟的赢家分配名称:
names(ProbWinner2) <- Prices
- 现在,让我们可视化模拟结果:
ProbWinner1
返回以下结果:
299 306 312 335
0.1397 0.7846 0.0757 0.0000
如我们所见,模拟结果告诉我们,似乎在用户中最受欢迎的价格是$306,该价格收集了 78.5%的概率。
正如我们预期的那样,我们通过扩展分析到第二个月,继续我们的监控。
- 首先,我们报告第二个月收集的数据,这些数据涉及联系的用户和进行的服务购买:
UsersContacted2 <- c(12350, 12001, 11950, 12500)
Purchases2 <- c(621, 625, 601, 520)
正如我们所见,这些是分别包含每个类别四个数据的两个向量。
- 此时,我们可以进入不同价格下每个臂的后验分布模拟:
PostDistr2Month = sim_post(Purchases2,UsersContacted2, ndraws = 10000)
- 因此,我们可以评估获胜者:
ProbWinner2 <- prob_winner(PostDistr2Month)
- 我们现在给模拟的获胜者分配名称:
names(ProbWinner2) <- Prices
- 现在,让我们可视化模拟结果:
ProbWinner2
返回以下结果:
299 306 312 335
0.196679 0.602588 0.200732 0.000001
正如我们所见,模拟结果告诉我们,似乎在用户中最受欢迎的价格是 $306,获得了 60.3%的概率。
- 最后,让我们进入价格监控的最后一个月:
UsersContacted3 <- c(14864, 14990, 14762, 10073)
Purchases3 <- c(803, 825, 791, 141)
- 此时,我们可以进入不同价格下每个臂的后验分布模拟:
PostDistr3Month = sim_post(Purchases3,UsersContacted3, ndraws = 10000)
- 因此,我们可以评估获胜者:
ProbWinner3 <- prob_winner(PostDistr3Month)
- 我们现在给模拟的获胜者分配名称:
names(ProbWinner3) <- Prices
- 现在,让我们可视化模拟结果:
ProbWinner3
返回以下结果:
299 306 312 335
0.272597 0.529096 0.198307 0.000000
正如我们所见,模拟结果告诉我们,似乎在用户中最受欢迎的价格是 $306,获得了 52.9%的概率。
让我们尝试看看得到的结果是否具有统计学意义。我们什么时候可以说我们的实验是显著的?实际上,没有任何结果是显著的。我们决定实验结果需要有多有说服力,才能得出它真的有效。表示研究显著性的一种常见方式是 P 值。P 值是一个数字,表示结果是由于偶然因素而非研究现象本身的概率。通常,在设计实验时,你会决定想要达到的特定 P 值,例如 1%,然后相应地决定样本大小;在其他情况下,样本大小无法控制,研究的显著性则是事后计算的。
为了进行显著性检验,我们将使用 significance_analysis() 函数,如下所示:
significance_analysis(Purchases3,UsersContacted3)
该函数执行总体比例比较,并返回以下值:
-
successes: 成功次数的向量 -
totals: 试验次数的向量 -
estimated_proportion: 成功次数/试验次数 -
lower: 0.95 置信区间,表示此方案比下一个低效方案的估计优势 -
upper: 0.95 置信区间,表示此方案比下一个低效方案的估计优势 -
significance: 此方案比下一个低效方案的 P 值 -
order: 按成功比例从高到低排序 -
best: 如果它属于“表现最好的组”——即那些与最佳组没有显著差异的组,则为 1 -
p_best: 贝叶斯后验概率,表示此方案是最优二项带状选手
监控第三个月的数据返回了显著性检验的以下结果:

结果确认我们选择的第四个作为价格的值产生了不显著的结果,因此我们可以在后续的分析中将其排除。到目前为止进行的监控为我们提供了足够的数据。接下来,我们将计算另一个臂可能对当前最佳臂产生的改进金额的分布。
首先,我们将按如下方式计算剩余值:
ValueRemaining <- value_remaining(Purchases3,UsersContacted3)
我们使用了value_remaining()函数,该函数返回另一个臂可能对当前最佳臂产生的改进金额的分布。最后,我们将计算潜在的剩余值:
PotentialValue <- quantile(ValueRemaining, 0.95)
PotentialValue
在之前的代码中,我们使用了quantile()函数。该函数返回指定函数(ValueRemaining)对应的分位数值(0.95)。
返回以下结果:
95%
0.07145922
这个值告诉我们,相较于看似获胜的价格($306),任何其他值都可能以高达 7.14%的概率击败它,这代表了剩余值的 95%分位数。
在接下来的部分,我们将处理一个优化问题:我们将尝试评估金融投资组合的最佳配置。
优化股票投资组合
选择一个最佳投资组合是一个典型的决策问题,因此其解决方案包括以下几个元素:识别一组备选方案,使用选择标准筛选不同的可能性,以及问题的解决。为了优化金融投资组合,我们首先衡量可用产品的收益和风险。风险-收益变量可以视为同一枚硬币的两面,因为一定水平的风险会对应一个给定的收益。收益可以定义为与所投入资本相关的投资产生的结果之和,而风险的概念可以转化为与特定金融工具相关的收益变动程度。前述问题可以建模为一个 MAB 问题,金融产品如同臂,产品性能作为结果。在分析代码之前,我们将定义一些与金融投资相关的概念。
交易型基金投资组合优化
交易型基金(ETF)这一术语指的是一种特定类型的投资基金,具有两个主要特点:它像股票一样在证券交易所交易,且其唯一的投资目标是通过完全被动的管理复制其所跟踪的指数(基准)。
ETF 总结了基金和股票的特点,允许投资者利用这两种工具的优势:
-
资金的多样化和风险降低
-
股票实时交易的灵活性和信息透明度
鉴于其特点,ETF 可以用于多种方式:中长期投资,甚至是日内交易和卖空,借此对基准指数采取看空立场。由于 ETF 可以轻松实现投资组合的多样化、精准复制基准指数以及管理成本低,这使得它特别适合通过定期小额付款(即使是个体投资者)来构建累积投资计划。让我们来看看 ETF 数据集的结构。
ETF 数据集
因此,ETF 是一组证券,它追踪一个基础指数。最著名的例子是 SPDR S&P 500(SPY)ETF,它复制了 S&P 500 指数。在我们即将处理的示例中,我们将使用包含以下六只 ETF 的历史数据集:
-
SPY: SPDR S&P 500 ETF
-
XLF: 金融精选行业 SPDR 基金
-
IWM: iShares Russell 2000 ETF; IEF
-
IEF: iShares 7-10 年期美国国债 ETF
-
GLD: SPDR 黄金股票
-
VWO: Vanguard FTSE 新兴市场 ETF
这些数据对应的时间范围是从 2009 年 1 月 1 日到 2019 年 1 月 1 日,数据从雅虎财经网站下载。
首先,我们将导入数据集:
dataset = read.csv('ETFs.csv')
数据集包含以下内容:
-
150 次观察: 月度历史数据
-
6 个变量:
EFTreturns
以下信息是通过str()函数返回的,如下所示:
str(dataset)
打印出以下列表:
> str(dataset)
'data.frame': 150 obs. of 6 variables:
$ SPY: num 0.01054 -0.01962 0.00759 0.0443 0.03392 ...
$ IEF: num -0.00725 0.01668 -0.00467 0.00217 -0.01765 ...
$ XLF: num 0.00325 -0.03047 -0.0089 0.03873 0.02405 ...
$ IWM: num 0.00852 -0.00655 0.00786 0.01624 0.04372 ...
$ VWO: num -0.0105 -0.0232 0.0426 0.038 0.0666 ...
$ GLD: num 0.0197 0.0255 -0.0111 0.0205 -0.0231 ...
在下图中,我们可以看到数据集的前 20 行:

可以使用summary()函数获得更多信息,该函数生成每个变量的摘要:
summary(dataset)
返回以下结果:

通过这种方式,我们获得了一系列统计描述符,展示了各指数的值分布。为了概览数据,我们可以绘制一个箱线图。箱线图通过简单的离散度和位置指标描述数据集的分布。箱线图可以是水平的或垂直的,并包含一个由两个分段组成的矩形分区。矩形(箱子)由以下特征限定:第一四分位数(25^(th) 百分位数)和第三四分位数(75^(th) 百分位数)。箱子内部的线表示中位数(50^(th) 百分位数)。通常,boxplot()函数用于绘制箱线图,如下所示:
boxplot(dataset)
下图展示了数据集中包含的 ETF 的箱线图:

boxplot() 函数也可以用来识别异常值。异常值是远离其他可用观察值的极端值。这些值通常会扭曲数据分析的结果。因此,异常值必须在数据清理阶段提前识别,或者在随后的数据分析阶段处理中。通过分析前面的图表,我们可以看到,XLF 变量在极值处表现出不同的异常值。
上置信界限方法
正如我们在第四章 多臂老丨虎丨机模型 中提到的,游戏允许我们同时进行开发和探索。在游戏开始时,我们不知道哪个臂是最好的,因此我们无法对任何臂进行特征化。因此,UCB 算法假设所有臂的观察平均值相同。所以,每个臂都会创建一个置信区间,且臂会被随机选择。
在这个上下文中,每个臂会给出奖励或不奖励。如果所选的臂返回一个错误,则该臂观察到的平均值会下降,置信区间也会减小。如果所选的臂返回奖励,则观察到的平均值会增加,置信区间也会增加。通过利用最好的臂,我们实际上是在减少置信区间。增加更多的回合时,其他臂表现良好的概率也会增加。
我们的目标是选择那个在最多观察次数中显示最大回报的股票。为此,我们将把包含月度回报的数据矩阵转换成一个矩阵,其中每行会有一些零,除了最大值之外,最大值位置会呈现为 1。为此,我们将初始化一个与起始数据集大小相同的矩阵,并将其填充为零。然后,我们会在起始数据集每行的最大值位置填入 1:
DataSel<- matrix(0, nrow = 150, ncol = 6)
rowmax = apply(dataset, 1, max)
for (i in 1:150){
for (j in 1:6){
if (dataset[i,j] == rowmax[i])
DataSel[i,j] <- 1
}
}
从这一刻起,我们将使用这个数据集。首先,我们将设置观察次数和臂的数量:
NumObs = 150
NumArms = 6
做完这些之后,我们将尝试初始化一系列对我们的计算有用的变量:
EFTSelected = integer()
NumSelections = integer(NumArms)
RewSum = integer(NumArms)
TotRew = 0
变量名称非常直观,但我们还是来验证一下它们的含义:
-
EFTSelected:在迭代周期中选定的 EFT -
NumSelections:所做选择的次数 -
RewSum:每个臂的奖励总和 -
TotRew:获得的总奖励
在这一点上,我们必须使用两个迭代周期来遍历整个矩阵。第一个周期将允许我们遍历所有行,而第二个周期将处理列。之前,我们将这些定义为 NumObs 行数和 NumArms 列数:
for (n in 1:NumObs){
对于每一行,我们初始化两个额外的变量:
EFT = 0
MaxUpBound = 0
现在,我们可以查看各列:
for (i in 1:NumArms){
记住,为了在每一轮进行上置信区间(UCB)计算,需要计算两个变量:杠杆i在n次操作后获得的奖励总和,以及策略在前n次操作中使用杠杆i的次数。我们按如下方式计算杠杆i在n次操作后获得的平均奖励:
if(NumSelections[i]> 0 ){
AverageReward = RewSum[i]/NumSelections[i]
我们使用以下命令计算在n次操作后获得的置信区间:
DeltaI = sqrt(3/2 * log(n)/NumSelections[i])
然后,我们选择返回最大 UCB 的杠杆i,如下所示:
UpBound = AverageReward + DeltaI
完成此操作后,我们进行了一些检查:
} else{
UpBound = 1e400
}
if(UpBound > MaxUpBound){
MaxUpBound = UpBound
EFT = i
}
}
最后,我们更新了这些值:
EFTSelected = append(EFTSelected, EFT)
NumSelections[EFT] = NumSelections[EFT] + 1
reward = DataSel[n, EFT]
print(reward)
RewSum[EFT] = RewSum[EFT] + reward
TotRew = TotRew + reward
}
此时,我们的分析已经完成,可以查看结果。为了可视化结果,我们需要绘制一个直方图。直方图是一种显示给定项目在特定范围内出现频率的图形。直方图类似于条形图,但通常使用其面积来图示某个元素出现的频率。直方图用于表示一组连续数据,如时间、测量值或温度。直方图的主要问题在于,比较两组数据的困难,以及无法精确读取数据所假设的值。
为了绘制直方图,我们将使用hist()函数,它计算给定数据值的直方图,如下所示:
hist(EFTSelected,
col = 'blue',
main = 'Histogram of EFTs selections',
xlab = 'EFTs',
ylab = 'Number of times each EFT was selected')
打印出以下直方图:

从前面的图表分析来看,股票(GLD)编号 6 是算法选择最多的。
最后,让我们看看如何处理欺诈识别的问题。
欺诈检测技术
在银行领域,防止欺诈至关重要。在这里,机器学习在整合算法中的应用也是一个明显的成功因素。在此,我们将回到人工智能的预测能力——一个可疑的动作不仅在数字的层面和地理位置上表现出异常,还可能偏离人工智能明确识别的客户行为模型。通过这种方式,面对明显的异常,人工智能可以在欺诈完全暴露之前就设定阻止措施或限制。
欺诈检测问题通常通过使用监督算法来解决。在这些算法中,一系列标签引导机器识别系统异常。强化学习是一种非常强大的算法,它允许智能体从环境中接收状态,然后根据这些状态执行行动。根据这些行动,智能体会相应地获得奖励。正如我们在第二章《强化学习的构建模块》中看到的,三个基本要素如下:
-
状态
-
行动
-
奖励
如果你想使用它来解决分类问题,就需要仔细定义我们刚刚提到的三个参数。
假设我们想要识别信用卡的欺诈使用,这是最常见的计算机犯罪之一。可用数据为我们提供了一些变量,这些变量展示了用户的行为,然后是标记操作的二进制类(1.0)。在这种情况下,问题的正确表述包括以下参数:
-
status: 这些是总结用户行为的所有变量。 -
action: 这是包含操作标签的二进制值(0 = 有效操作,1 = 欺诈操作)。 -
reward: 这为系统提供反馈。奖励是通过对操作的评估获得的。如果操作=0,或者操作有效,则会获得奖励。这将使基于强化学习的系统学会何时操作有效,并将其与欺诈行为区分开来。
为了改进奖励函数,可以在欺诈行为的情况下考虑负面奖励。
总结
在这一章中,我们学习了如何利用强化学习技术解决金融问题。在介绍应用领域之后,我们研究了一些实际案例。首先,我们学习了如何通过建模信用风险(如马尔科夫链)进行信用评分分析。然后,我们学习了如何为即将上市的新产品选择最佳价格。一旦我们选择了一系列价格来应用于金融产品,采用一种优化程序评估市场对新产品的反应是一种良好的做法。为此,我们通过一个基于多臂赌博机方法的算法对系统进行了建模。最后,我们学习了如何基于近年来获得的回报优化股票市场的选择。
在下一章中,我们将学习如何在医疗保健中应用强化学习。首先,我们将学习如何从观察数据中选择最优策略,并了解如何使用 TD 学习检测乳腺癌。最后,我们将学习如何使用 R 预测甲状腺疾病。
第十章:医疗领域中的 TD 学习
人工智能与医学——这两个领域虽然有着截然不同的起源,但它们已经开始建立起紧密的合作关系,基本目标是提高全球人口的健康水平和预期寿命。这种关系将在未来几十年内进一步加强,尤其是在 21 世纪医学面临的挑战极为严峻的背景下。在本章中,我们将通过强化学习来解决其中的一些问题。我们将学习如何在医疗领域应用强化学习。接着,我们将学习如何将医疗保险建模为马尔可夫决策过程。最后,我们将了解如何利用 TD 学习规划手术室卫生安排。
在本章中,我们将讨论以下主题:
-
在医疗领域引入强化学习
-
医疗保险计划建模
-
在健康保险中使用转移模型
-
手术室卫生安排
技术要求
请查看以下视频,观看代码示范:
在医疗领域引入强化学习
近年来,医疗行业对人工智能的关注巨大——事实上,我们已经观察到谷歌的 DeepMind 项目与英国国家医疗服务体系(NHS)的合作,以及 IBM 通过人工智能在基因组学和药物发现领域的持续投资。
很多人会问,机器学习在医疗领域带来的创新能够为患者的生活带来哪些具体的改善。在 5 到 10 年的时间里,人们普遍认为医疗行业将会发生彻底的革命。高层管理人员、医生、护士等是否已经为这一新挑战做好准备?
许多专家预测,在 2025 到 2030 年之间,健康领域将被基于人工智能和机器学习的新技术所席卷。这个时间框架实际上与许多其他预测一致,表明数字智能的使用将影响文化和人类活动的许多其他领域。
此刻,在西方世界,许多公司积极参与开发专门面向医疗领域的机器学习系统。除了这些知名公司外,我们还有一定数量的大学创业公司和衍生企业,几乎每天都在将注意力转向医学领域。
在接下来的章节中,我们将探讨机器学习在医疗领域的一些可能应用。
疾病诊断
诊断疾病是一个困难的过程,且需要大量经验。它旨在根据症状或现象识别疾病或心理病理,前者是病人身上的主观表现,后者也是医生或心理学家可以察觉的现象。症状和体征的集合,其中一些是特定的或病理特征性的,而其他则是更或更少具有通用性的,构成了疾病的临床表现。诊断方法的集合称为诊断学。诊断学在使用特殊设备或仪器时尤为重要,如影像学或临床诊断,尤其是在通过医生直接检查病人时。
信息技术在诊断方面提供了极大的支持。世界各地的各种中心利用全球最强大的超级计算机的空间和计算能力,获取快速而准确的诊断,这些诊断从搜索引擎和互联网数据库中庞大的知识库中提取和处理信息,包括匿名的电子病历。
近年来,基于机器的算法做出了巨大贡献。通过这些算法,病理的识别和分类受到的错误影响较小。通过这种方式,疾病的早期诊断可以使治疗更加有效。
流行病事件的预测
流行病一词指的是通过直接或间接传染迅速传播的疾病,直到影响到一个更或更广泛的地区的大量人群,并且在持续一段时间后消失。这一定义通常适用于传染性疾病,尽管目前有将这一术语转移到非传染性疾病领域的趋势,特别是当某些人群中这些疾病的发生频率发生明显且意外的增加时,这与因果因素的数量和质量变化相吻合。
疾病的传播依赖于由领土影响和人口流动所表征的现象。将所有这些信息关联起来是一个繁重的任务。为此,我们需要人工智能的帮助,人工智能可以通过考虑疾病发生地来预测流行病如何扩散。这项分析的结果可以帮助当局制定疫情风险控制计划。
新药实验
机器学习在新药的识别方面具有非常重要的作用,从药物成分的研究到预测其效果。
总部位于英国的皇家学会表示,机器学习是通过生物方法生产药物的最佳解决方案。通过这种方式,制药公司在药物生产过程中扮演着重要的助手角色,尤其是在时效性和降低生产成本方面。
在精准医学中,MIT 临床机器学习小组使用算法来识别生产药物和疗法的最佳方法,主要针对糖尿病。
另一个例子是微软的汉诺威项目,该项目在多个案例中使用了机器学习,特别是在治疗癌症的疗法技术中,尤其是在为急性髓性白血病识别个性化治疗方案方面。
DeepMind 健康
DeepMind 可以在短短几分钟内处理数百万条医疗信息,大大加快了临床性质的医疗过程,如档案归档和诊断。DeepMind 的研究人员还在开发模拟预测行动后果能力的模型——实际上,他们正在试图理解什么是智能和想象力,并将其转化为算法。谷歌的生命科学分支 Verily 也在进行一个名为基线研究的项目,以收集基因数据。该项目的目的是采用一些谷歌的算法来分析是什么使人保持健康。为了这个项目,研究人员还使用了疾病监测技术,例如智能隐形眼镜来测量血糖水平。
现在,应用强化学习算法到与医疗保健相关的实际案例的时机已经到来。
建模健康保险计划
长期护理(LTC)一词指的是一系列必要的干预措施,旨在确保为主要是老年人提供足够的帮助,这些人因事故、疾病或仅仅因衰老而处于非自理状态。这些干预措施由公共或私人机构提供,可能包括整体健康服务和/或社会福利服务的内容,无论是在家中还是在专门为应对这些风险而设立的机构中。
这些形式的保险,无论是私人还是公共保险,其重要性本质上是由于人口老龄化进程,这一进程已经影响了所有工业化国家。这个过程正在各个福利国家的各个领域造成强烈的财务和保障问题。例如,包括养老金系统、健康服务的需求,更特别的是,当老年人在完全或部分失去自理能力时,他们所需的长期社会和健康服务的需求。
健康保险基础
健康保险指的是一类广泛的保险覆盖,允许保险公司在被保险人的健康状况因疾病或伤害而受到影响,导致高额医疗费用并无法产生收入,从而造成经济损失时进行干预。
这些保险形式的目标是保护被保险人免受与其健康状况和相对工作能力相关的风险,这些风险由于疾病的发生或事故的发生,可能会在较长或较短的时间内暂时或永久中断。
一些保险公司为替代公共卫生系统提供的健康解决方案提供财务支持,例如医疗费用报销保险;其他公司则提供收入或资本形式的收益,至少部分地补偿因工作能力丧失而导致的收入损失。
为了使保险合同货币化,需要专家介入,研究寿险公司及一般社会保障机构的技术组织,通过建立基础并从统计、财务和数学的角度检验结果。这个角色被称为精算师。
第一个生命保险精算模型实际上是在 17 世纪末和 18 世纪上半叶之间的时期出现的。几个世纪以来,已提出了不同的模型。在下一节中,我们将分析一个基于多重递减的模型。
引入多重递减模型
多重递减理论始于 18 世纪下半叶。这一理论在数学表述上提出了一个与其他任何类型保险活动都相关的问题。它首先在连续时间模型领域发展起来,直到 18 世纪末,才在离散时间模型的背景下发展。后者主要是在 19 世纪末期开始被考虑,用于精算计算在公共养老金系统中的首次应用。
保险费的计算基于大数法则,该法则指出,在事件E的n次独立试验中,成功的比例会随着* n趋近于无穷大而趋向事件E*发生的概率。这意味着,当参考样本足够大且疾病的概率彼此独立时,感染疾病的个体比例接近感染该疾病的概率,从而这部分人口将会患病。正因为有了这个原理,保险公司能够计算预期的赔付金额并提高同质性。被保险人的数量也增加了估算的准确性。因此,保险公司并不知晓谁会得病,而仅知道会生病的个体的百分比。
保费的计算必须得到双方的同意,即被保险人和保险人。在最简单的情况下,假设利率在保单的活动期内是恒定的,且个人的利益取决于死亡事件的发生。在基于共同集体的模型中,服务以相同的方式支付给每个群体。在这种情况下,必须定义何时支付利益。有两种可能的解决方案:支付群体中第一次死亡时的利益,或者支付群体中最后一次死亡时的利益。在这两种情况下,唯一的随机变量是由未来的预期寿命表示的。
寿命表用于计算预期寿命。寿命表是一种表格,显示每个年龄段的人在下一次生日之前死亡的概率。因此,在寿命表中,只有单一的退出模式。这个表格被定义为单一递减表。当我们预期一组个体有不同的递减原因时,我们可以通过基于多种递减原因的模型族来对系统进行建模。
在多重递减模型中,存在同时发生的不同递减原因。一个人的生命结束是由于这些递减功能之一。在接下来的部分中,我们将通过将问题建模为马尔可夫决策过程来计算将支付给被保险人的利益。
在健康保险中使用转移模型
递减模型可以看作是多状态模型的一种特殊情况。实际上,就像多状态模型中发生的那样,我们可以考虑一个年龄为x的个体,在时间t时处于n + 1个潜在状态之一。在多重递减模型中,从状态 0(活动)开始,所有其他转移状态都被定义为吸收状态,一旦到达这些状态,就不能再退出。
在我们的例子中,提供了三种状态:活动、无法工作和死亡。起始点显然是由活动状态表示,而其他两种(无法工作和死亡)是吸收状态,如下图所示:

我们之前已经提到,除了活动状态,所有的转移状态都是吸收状态。这意味着无法再返回活动状态。从无法工作状态转移是可能的——不幸的是,这并不愉快,因为它涉及死亡。现在,让我们学习如何设置递减表。
设置递减表
为了处理这个问题,必须有一个递减表。递减表包含个体从一个状态转移到另一个状态的概率,这听起来像是一个转移矩阵。我们在第三章中详细分析了转移矩阵,马尔可夫决策过程的实际应用。
首先,我们将学习如何构建减量表。你可能还记得,减量矩阵与寿命预期表的不同之处在于,除了包含死亡概率作为受试者年龄的函数外,它还包含其他预测状态下的转移概率:
- 我们将创建包含这些信息的向量,并从年龄开始:
age<-seq(from = 30, to = 50, by=1)
在这里,我们使用了seq()函数,它会生成规则的序列。使用了三个参数,具体如下:
-
-
from:序列的起始值。 -
to:序列的结束值。 -
by:序列的增量。默认值为 1。
-
在我们的例子中,我们本可以省略by参数,但我们插入了它以提高代码的可读性。我们只考虑了一部分工作生涯,即从 30 岁开始到达 50 岁的这一段。当然,我们本可以将这个区间扩展到整个工作生涯,但对于我们的目的来说,这样就足够了。现在,我们可以创建包含从一个状态到另一个状态转移概率的向量。我们从活跃状态开始,接着有三个转移状态:从活跃到无法,从活跃到死亡,以及从活跃到活跃。
- 我们将再次使用
seq()函数,具体如下:
ProbA2U<-seq(0.0006,0.0060,length.out = 21)
ProbA2D<-seq(0.0005,0.0020,length.out = 21)
ProbA2A<-1-(ProbA2U+ProbA2D)
我们再次使用了seq()函数,但这次使用了一个新的参数:length.out。此参数用于设置序列的期望长度。
让我们分析一下新创建的向量。第一个向量是从活跃状态到无法状态的转移概率,并且随着年龄的增长值不断增加。第二个向量是从活跃状态到死亡状态的转移概率,同样随着年龄的增长而增加。最后,第三个向量是从活跃状态到活跃状态的转移概率,得自于 1 减去前两个值的和。这是因为三个转移概率的总和必须等于 1。
- 现在,让我们进入无法状态。同样地,我们有三个转移:无法到活跃,无法到死亡,以及无法到无法。我们来创建这三个向量:
ProbU2A<-seq(0,0,length.out = 21)
ProbU2D<-seq(0.1219,0.1879,length.out = 21)
ProbU2U<-1-(ProbU2A+ProbU2D)
如我们所见,第一个向量仅包含零。这是因为,正如我们预料的那样,无法状态不允许转移到活跃状态;从这个意义上来说,它是一个吸收状态。第二个向量表示从无法状态到死亡状态的转移概率,且随着年龄的增长而增加。最后,第三个向量表示从无法状态到无法状态的转移概率,它是通过 1 减去前两个值的和得到的。这是因为三个转移概率的总和必须等于 1。
- 最后,让我们进入死亡状态。同样地,我们有三个转移:死亡到活跃,死亡到无法,以及死亡到死亡。我们来创建这三个向量:
ProbD2A<-seq(0,0,length.out = 21)
ProbD2U<-seq(0,0,length.out = 21)
ProbD2D<-1-(ProbD2A+ProbD2U)
与其他两个状态相比,死态无疑是最具吸引力的。为了验证这一点,前两个向量包含零,而第三个向量包含一。这是因为状态转移概率的和必须等于 1 这一规则仍然有效。
- 此时,我们只需要使用这些向量来创建递减表,如下所示:
DecrementsTable<- data.frame(age,ProbA2A,ProbA2U,ProbA2D,ProbU2A,ProbU2D,ProbU2U,ProbD2A,ProbD2U,ProbD2A)
- 让我们看一下我们刚刚创建的表格;我们可以使用
str()函数,它显示了 R 对象内部结构的紧凑表示:
str(DecrementsTable)
结果显示在以下代码块中:
'data.frame': 21 obs. of 10 variables:
$ age : num 30 31 32 33 34 35 36 37 38 39 ...
$ ProbA2A: num 0.999 0.999 0.998 0.998 0.998 ...
$ ProbA2U: num 0.0006 0.00087 0.00114 0.00141 0.00168 0.00195 0.00222 0.00249 0.00276 0.00303 ...
$ ProbA2D: num 0.0005 0.000575 0.00065 0.000725 0.0008 ...
$ ProbU2A: num 0 0 0 0 0 0 0 0 0 0 ...
$ ProbU2D: num 0.122 0.125 0.128 0.132 0.135 ...
$ ProbU2U: num 0.878 0.875 0.871 0.868 0.865 ...
$ ProbD2A: num 0 0 0 0 0 0 0 0 0 0 ...
$ ProbD2U: num 0 0 0 0 0 0 0 0 0 0 ...
$ ProbD2D: num 1 1 1 1 1 1 1 1 1 1 ...
数据框包含 21 个观察值和 10 个变量。每个观察值表示从 30 岁到 50 岁的年龄。变量包括年龄和三个状态(活跃、无法工作和死亡)之间的九个转移概率。正如我们所看到的,表中包含的变量由其名称标识。要在数据框中引用变量,可以在数据框名称和变量名称之间插入美元符号($)。此外,我们还可以读取其他信息:每个变量的类型。在本例中,我们有 10 个数值型变量。
- 可以使用
summary()函数获取更多信息,该函数生成变量分布的摘要:
summary(TransMatrix)
以下结果被返回:

summary()函数调用特定的方法,这些方法依赖于第一个参数的类别。对于每个特征,返回以下描述符——最小值、第一四分位数、中位数、均值、第三四分位数和最大值。快速查看这些值可以帮助我们理解变量所采用值的统计分布。
- 为了理解这个表格是如何显示的,我们可以打印递减表的前几行,如下所示:
head(DecrementsTable)
以下是打印的结果:

如我们所见,每个观察值对应一个工人的年龄,对于每个年龄,都会报告从一个状态到另一个状态的转移概率。现在,我们可以在下一部分设置模型的基础。
马尔可夫决策过程模型
我们的目标是将这个问题视为一个马尔可夫决策过程。为此,我们必须定义状态。此前我们提到,工人可以处于三个状态——活跃、无法工作和死亡。让我们按照以下步骤进行:
- 我们将创建一个包含这些信息的变量:
WorkesStates<-c("Active","Unable","Dead")
马尔可夫过程由一个转移矩阵表示,该矩阵定义了从一个状态到另一个状态的转移概率。我们已经说过,我们的系统由三个状态定义,因此转移矩阵的大小将是 3x3。我们需要的信息包含在递减表中。
- 然后,我们将提取在特定年龄时工人的转移矩阵:
TransMatrix35<-matrix(as.numeric(DecrementsTable[DecrementsTable$age==35,2:10]),nrow = 3,ncol = 3, byrow = TRUE, dimnames = list(WorkesStates, WorkesStates))
为此,我们仅提取了与年龄变量值为 35 对应的观察值相关的递减表的最后九列。我们使用matrix()函数将提取的数据安排成一个 3x3 的矩阵(nrow = 3, ncol = 3),并固定了逐行的值(byrow = TRUE)。让我们看看结果:
Active Unable Dead
Active 0.997175 0.00195 0.000875
Unable 0 0.8616 0.1384
Dead 0 0 1
通过分析前面的矩阵,我们可以看到它具有一个特殊的形状;实际上,主对角线下方的所有元素都等于零。此类矩阵称为上三角矩阵,并且具有以下特性:
-
-
秩等于主对角线上非零元素的数量。
-
行列式等于主对角线上元素的乘积。
-
特征值由主对角线上的元素表示。
-
- 此时,我们可以定义模型:
MCModel35<-new("markovchain", transitionMatrix = TransMatrix35, states = WorkesStates , name="MCModel35")
markovchain 类是专门设计用来处理同质马尔科夫链过程的。
传递以下参数:
-
-
transitionMatrix:这是一个方形转移矩阵,包含转移概率的元素。 -
states:这是状态的名称,必须与转移矩阵的colnames和rownames相同。这是一个字符向量,列出了定义转移概率的状态。 -
name:这是一个可选的字符元素,用于命名离散时间马尔科夫链。
-
- 为了显示我们刚刚创建的模型的总结,让我们使用以下命令:
MCModel35
返回以下结果:
MCModel35
A 3 - dimensional discrete Markov Chain defined by the following states:
Active, Unable, Dead
The transition matrix (by rows) is defined as follows:
Active Unable Dead
Active 0.997175 0.00195 0.000875
Unable 0.000000 0.86160 0.138400
Dead 0.000000 0.00000 1.000000
- 如我们在第三章《马尔科夫决策过程实战》中所见,要获取
markovchain对象的状态,我们可以使用states方法,如下所示:
states(MCModel35)
返回以下结果:
[1] "Active" "Unable" "Dead"
- 要获取
markovchain对象的维度,可以使用dim()方法,如下所示:
dim(MCModel35)
返回以下结果:
[1] 3
- 要查看对象中包含哪些元素,可以使用
str()函数,它展示了对象的内部结构的紧凑视图:
str(MCModel35)
打印以下结果:
Formal class 'markovchain' [package "markovchain"] with 4 slots
..@ states : chr [1:3] "Active" "Unable" "Dead"
..@ byrow : logi TRUE
..@ transitionMatrix: num [1:3, 1:3] 0.99718 0 0 0.00195 0.8616 ...
.. ..- attr(*, "dimnames")=List of 2
.. .. ..$ : chr [1:3] "Active" "Unable" "Dead"
.. .. ..$ : chr [1:3] "Active" "Unable" "Dead"
..@ name : chr "MCModel35"
- 要检索每个元素,只需使用对象的名称(
MCModel35),后跟插槽的名称,中间用@符号分隔。例如,要打印转移矩阵,我们可以写如下代码:
MCModel35@transitionMatrix
返回以下结果:
Active Unable Dead
Active 0.997175 0.00195 0.000875
Unable 0.000000 0.86160 0.138400
Dead 0.000000 0.00000 1.000000
- 我们可以这样评估吸收状态:
absorbingStates(MCModel35)
返回以下状态:
[1] "Dead"
- 正如我们所知,从这个状态,我们无法回到之前的状态。此时,我们可以预测工人的状态。这是一个模拟,接下来让我们看看会发生什么:
set.seed(5)
WorkerStatePred35<- rmarkovchain(n = 1000, object = MCModel35, t0 ="Active")
set.seed()命令定义了随机数生成器的种子。这样,每次执行代码时,所有使用的随机数都会是相同的,从而确保例子的可重复性。
- 我们将从结果中提取统计数据:
table(WorkerStatePred35)
返回以下结果:
WorkerStatePred35
Active
1000
让我们看看 50 岁工人的情况:
- 首先,我们将提取工人 50 岁时的转移矩阵:
TransMatrix50<-matrix(as.numeric(DecrementsTable[DecrementsTable$age==50,2:10]),nrow = 3,ncol = 3, byrow = TRUE, dimnames = list(WorkesStates, WorkesStates))
- 然后,我们将设置模型:
MCModel50<-new("markovchain", transitionMatrix = TransMatrix50, states = WorkesStates, name="MCModel50")
- 最后,我们将模拟工作生活:
WorkerStatePred50<- rmarkovchain(n = 1000, object = MCModel50, t0 ="Active")
- 现在,我们可以提取结果:
table(WorkerStatePred50)
以下结果被打印出来:
WorkerStatePred50
Active Dead Unable
33 956 11
现在,我们可以预测工作生活,模拟一个工人在不同时间段的状态:
-
我们需要做的第一件事是为每个可用的工作年龄创建一个
markovchain对象。我们将参考 30 到 50 岁之间的区间,因为我们有该数据。 -
为了加快这个过程,我们将创建一个迭代循环,并将每个
markovchain对象插入一个列表中:
MCModelsList=list()
j=1
for(i in 30:50){
TransMatrix<-matrix(as.numeric(DecrementsTable[DecrementsTable$age==i,2:10]),nrow = 3,ncol = 3, byrow = TRUE, dimnames = list(WorkesStates, WorkesStates))
MCModelsList[[j]]<-new("markovchain", transitionMatrix = TransMatrix, states = WorkesStates)
j=j+1
}
-
首先,我们初始化了一个列表和一个计数器,它将帮助我们更新列表。
-
然后,我们从减少表中提取值,以获得每个年龄的转移矩阵。
-
最后,我们为该年龄段创建了
markovchain对象。在这一点上,我们可以创建一个markovchainList对象,如下所示:
MCList30to50<-new("markovchainList", markovchains = MCModelsList,name="MCList30to50")
markovchainlist对象是一个markovchain对象的列表。这些对象可以用来建模非齐次离散时间马尔科夫链,当转移概率随时间变化时。
- 现在,我们可以模拟工人可能处于的状态之间的转移:
StatesSequence<-rmarkovchain(n=10000, object=MCList30to50,t0="Active")
rmarkovchain()函数生成一个从齐次或非齐次马尔科夫链生成的状态序列。在我们的例子中,我们已经生成了 10,000 次模拟的序列。
- 让我们看看新创建的对象包含了什么:
str(StatesSequence)
以下结果被打印出来:
'data.frame': 210000 obs. of 2 variables:
$ iteration: num 1 1 1 1 1 1 1 1 1 1 ...
$ values : Factor w/ 3 levels "Active","Dead",..: 1 1 1 1 1 1 1 1 1 1 ...
这是一个包含 210,000 个观察值的二维数据框。我们对第二个值变量感兴趣,该变量包含模拟的状态。
- 接下来,我们将提取一些简单的统计数据,帮助我们获取所有模拟中每个状态出现的次数:
StatesOccurences<-table(StatesSequence$value)
以下结果被返回:
Active Dead Unable
202189 4889 2922
- 利用现有数据,计算工人处于
Unable状态的预期时间非常容易:
ExpectedUnableOccurence<-StatesOccurences[3]/nrow(StatesSequence)
这里,我们将无法出现的次数除以StatesSequence对象中包含的行数。StatesSequence对象中的行数等于模拟次数(10,000)与工作年限(21 年,从 30 岁到 50 岁)的乘积。
以下结果被返回:
Unable
0.01391429
从我们进行的模拟开始,保险公司可以预测预期的保费。
在下一部分中,我们将学习如何优化医院手术室的使用。
手术室卫生规划
每年,大约 8,000 例死亡与手术室感染相关。在美国,感染会增加住院时长,从而增加费用,并导致患者提起法律诉讼。医院是一个建筑,其中适当的清洁条件有助于患者及工作人员的生活质量,并减少微生物传播的概率。
环境卫生涉及到实际和卫生程序的复杂性,以及通过清洁和去污活动使植物或特定环境保持健康的操作。当使用消毒剂时,这被称为卫生处理。必须在消毒之前进行充分的清洁周期,并且在任何情况下,都必须与消毒操作结合进行。每个环境都有一个最佳标准,这是环境本身预期用途的结果。因此,手术室的卫生与病房的卫生完全不同,后者又与公共区域的卫生不同。医院可以分为三类感染风险区:低、中、高风险区。低风险区包括走廊、办公室和候诊室等公共区域。中风险区包括病房、诊所和实验室。高风险区包括手术室、重症监护室、复苏室和恢复室。
手术室感染的预防是一个重要且当前的问题,因为它代表着手术过程中常见的严重并发症,会对患者的健康造成严重后果,并增加住院和院外的费用。对于患者而言,医院感染意味着额外的疾病;对于医生或护士来说,这些感染可能会使治疗效果失效,质疑他们的专业性,并使他们对所治疗患者的死亡率增加负有责任。基于这些原因,实施旨在控制感染的预防性干预措施必须成为一个共同且共享的目标。
我们将通过定义可用的数据和希望实现的目标来框定问题。
定义背景
在这个例子中,我们将处理手术室的卫生和灭菌活动规划问题。这是一项常规操作,涉及到资源使用和手术室空闲时间的成本。在手术室卫生维护活动的规划中,必须追求两个主要目标:第一个目标是避免手术室内患者发生感染,第二个目标是节省卫生和灭菌操作的成本。
这两个目标在任何情况下都是相关的,因为手术室可能发生的感染会启动一个干预协议,该协议需要额外的操作,并会导致活动停止 9 小时。另一方面,标准操作会导致活动停止 3 小时。卫生消毒操作每 30 天进行一次。鉴于最近的感染案例,手术室管理者希望了解在截止日期之前进行额外操作的规划是否能够改善情况。有两个可用的操作:不执行额外操作(NoSS = 无卫生消毒)或执行额外操作(SS = 卫生消毒)。
两次操作之间的间隔(30 天)被划分为三个子间隔,分别对应手术室所处的三个状态:
-
状态 1:上次干预后的 0-10 天间隔
-
状态 2:上次干预后的 11-20 天间隔
-
状态 3:上次干预后的 21-30 天间隔
在卫生消毒操作之后,房间进入状态 1。如果在前 10 天内没有进行其他操作,房间将进入状态 2,最后,在接下来的 10 天后,房间进入状态 3,届时预期在所有情况下都需要进行干预。如果在两次操作之间,患者感染了,手术室的持续操作将被中断,并进行紧急操作,将系统恢复到状态 1。问题是如何在长期视角下,除了已经预见的操作之外,管理卫生消毒操作,以最大化奖励。
过渡概率和奖励
这个问题可以看作一个马尔科夫决策过程。首先,我们必须定义过渡矩阵 P (s, s', a)。记住,它告诉我们从一个状态到另一个状态的概率是多少。由于有两个可用的操作(NoSS 和 SS),我们将定义两个过渡矩阵。我们用 ps 表示感染的概率,感染的概率取决于手术室的状态。上次干预的时间越长,感染的概率越高。在这方面,我们将定义三个感染概率:p1 = 0.2,p2 = 0.3 和 p3 = 0.4。请记住,由于有两个可能的操作,我们将定义两个过渡矩阵。与操作 1(NoSS)选择相关的过渡矩阵如下:

如果我们处于状态 1,那么我们将有概率p1保持在该状态,如果发生感染。剩余的概率1-p1则意味着如果没有感染,我们将转移到下一个状态。而进入状态 3 的概率为 0,不能直接从状态 1 转到状态 3。回想一下,在过渡矩阵中,处于同一行中的所有概率之和必须等于 1。如果我们处于状态 2,我们将有概率p2转移到状态 1,如果发生感染。剩余的概率1-p2则意味着如果没有感染,我们将转移到下一个状态,如状态 3。在这种情况下,保持在状态 2 的概率为 0。最后,如果我们处于状态 3,我们将有概率p3转移到状态 1,如果发生感染。
剩余的概率1-p3预计如果没有发生感染,仍然保持在状态 3。在这个状态之后,仍然会进行消毒-灭菌操作,而进入状态 2 的概率为 0。
通过替换与过渡矩阵中的三个状态相关联的三个概率值,我们得到以下矩阵:

现在,让我们定义与选择动作 2(SS)相关的过渡矩阵:

在这种情况下,过渡矩阵的形式更为直接:它是预见进行消毒-灭菌操作的行动,正如我们预期的那样,无论如何,房间都处于状态 1。这是我们所有三个状态中死去的人,这意味着我们转到状态 1 的概率是 1。
有了过渡矩阵,我们就完成了。现在,我们必须定义奖励矩阵。它是一个 3x2 的矩阵:三个状态和两个动作。动作 1(NoSS)计划不进行额外操作;在这种情况下,随着我们接近常规操作计划的结束期(30 天),奖励会递减。然后,奖励矩阵的第一列将呈现以下形式:

其含义显而易见:如果选择的行动是不给予消毒-灭菌操作,那么在第一种状态下我们将获得 3 的奖励,在第二种状态下我们将获得 2 的奖励,而在第三种状态下将获得最小奖励。如果选择的行动是进行消毒-灭菌操作,那么我们将得到以下结果:

如我们所见,情况与之前的情况相反:如果选择的行动是进行消毒-灭菌操作,那么在第一种状态下我们将获得 1 的奖励,在第二种状态下获得 2 的奖励,在第三种状态下获得最大奖励。
在这些条件下,奖励矩阵变为以下形式:

在定义了处理该问题所需的必要工具后,让我们继续构建模型。
模型设置
正如我们已经提到的,我们的目标是计算一个策略,使我们能够根据刚刚开发的设置获得最大奖励。这意味着通过最小化手术室卫生消毒操作的成本,减少感染风险。让我们开始吧:
- 让我们看一下允许我们执行此操作的代码:
library(MDPtoolbox)
P <- array(0, c(3,3,2))
P[,,1] <- matrix(c(0.2, 0.8, 0, 0.3,0,0.7,0.4,0,0.6), 3, 3, byrow=TRUE)
P[,,2] <- matrix(c(1, 0, 0, 1, 0, 0,1, 0, 0), 3, 3, byrow=TRUE)
R <- matrix(c(3, 1, 2, 2,1,3), 3, 2, byrow=TRUE)
mdp_check(P, R)
QLearnModel=mdp_Q_learning(P=P, R=R, discount = 0.95)
print(QLearnModel$Q)
print(QLearnModel$V)
print(QLearnModel$policy)
print(QLearnModel$mean_discrepancy)
- 我们将逐行分析代码,以理解每条命令的含义。首先,导入库:
library(MDPtoolbox)
马尔可夫决策过程(MDP)工具箱包含许多功能,帮助我们解决离散时间马尔可夫决策过程的问题。我们在第三章中介绍了该包,马尔可夫决策过程的实际应用。
首先要做的是定义转移矩阵P和奖励矩阵R。这两个矩阵已经在转移概率和奖励部分进行了充分介绍。
- 转移矩阵是一个 3x3 矩阵。首先,我们将创建一个新矩阵并初始化为零:
P <- array(0, c(3,3,2))
- 我们需要定义与动作 1(NoSS)相关的转移矩阵:
P[,,1] <- matrix(c(0.2, 0.8, 0, 0.3,0,0.7,0.4,0,0.6), 3, 3, byrow=TRUE)
定义如下矩阵:
> P[,,1]
[,1] [,2] [,3]
[1,] 0.2 0.8 0.0
[2,] 0.3 0.0 0.7
[3,] 0.4 0.0 0.6
- 让我们定义与动作 2(SS)相关的转移矩阵:
P[,,2] <- matrix(c(1, 0, 0, 1, 0, 0,1, 0, 0), 3, 3, byrow=TRUE)
定义如下矩阵:
> P[,,2]
[,1] [,2] [,3]
[1,] 1 0 0
[2,] 1 0 0
[3,] 1 0 0
- 接下来我们定义奖励矩阵,如下所示:
R <- matrix(c(3, 1, 2, 2,1,3), 3, 2, byrow=TRUE)
引入以下矩阵:
> R
[,1] [,2]
[1,] 3 1
[2,] 2 2
[3,] 1 3
在继续开发模型之前,需要验证P和R是否满足将问题归类为 MDP 类型所必需的标准。
- 为此,我们将使用
mdp_check()函数:
mdp_check(P, R)
该函数对我们定义的 MDP 过程进行检查,通过设置转移概率矩阵(P)和奖励矩阵(R)。如果P和R设置正确,则函数返回一个空消息。如果P和R没有正确设置,函数将返回一条描述问题的错误消息。返回的结果如下:
> mdp_check(P, R)
[1] ""
- 所以,问题已经设置好了。现在,我们可以使用
mdp_Q_learning()函数尝试解决问题,如下所示:
QLearnModel=mdp_Q_learning(P=P, R=R, discount = 0.95)
mdp_Q_learning()函数使用 Q-learning 算法解决折扣 MDP。正如我们在第七章中提到的,时序差分学习,Q-learning 是最常用的强化学习算法之一。得益于这一技术,我们能够为每个给定状态在一个完成的 MDP 中找到最佳动作。
强化学习问题的一般解法是通过学习过程估计一个评估函数。该函数必须能够通过奖励的总和来评估某一特定策略的优劣。实际上,Q-learning 试图最大化 Q 函数(动作值函数)的值,该函数表示当我们在状态 s 下执行动作 a 时的最大折扣未来奖励。
Q-learning 通过逐步更新环境中状态-动作对的值,按照估算 TD 方法值的通用公式的更新逻辑,递增地估计函数值 𝑞(𝑠, 𝑎)。Q-learning 具有离策略特性,即尽管策略是根据 𝑞(𝑠, 𝑎) 估算的值来改进的,但值函数通过遵循严格贪婪的二级策略来更新估算值:给定一个状态,选择的动作始终是最大化值 max𝑞(𝑠, 𝑎) 的动作。然而,π 策略在估算值时扮演着重要角色,因为通过它可以确定要访问和更新的状态-动作对。
传入以下参数:
-
P:转移概率数组
-
R:奖励数组
-
折扣:折扣因子——折扣是一个实数,属于 ]0; 1[
mdp_Q_learning() 函数计算 Q 矩阵、平均偏差,并在分配足够的迭代次数后给出最优值函数和最优策略。返回以下结果:
-
Q:动作值函数
-
V:值函数
-
policy:策略
-
mean_discrepancy:偏差表示经过 100 次迭代后的偏差
现在,我们来看一下结果:
- 让我们从动作值函数开始分析我们得到的结果:
print(QLearnModel$Q)
打印出以下表格:
> print(QLearnModel$Q)
[,1] [,2]
[1,] 47.14339 40.10113
[2,] 46.59996 37.67135
[3,] 29.04791 47.32842
Q 函数代表了该过程的核心要素;它是与奖励矩阵相同维度的矩阵,即一个 SxA 矩阵。值-动作函数返回我们在给定状态下采取某个特定动作并遵循最优策略时期望达到的效用。
- 让我们打印出值函数:
print(QLearnModel$V)
打印出以下向量:
> print(QLearnModel$V)
[1] 47.14339 46.59996 47.32842
值函数表示一个状态对于智能体的好坏。它等于智能体从状态 s 中期望获得的总奖励。值函数取决于智能体选择的策略,该策略决定了要执行的动作。V 是一个 S 长度的向量。
- 然后,我们将提取策略:
print(QLearnModel$policy)
返回以下结果:
> print(QLearnModel$policy)
[1] 1 1 2
策略是一个 S 长度的向量。每个元素是一个整数,对应于最大化价值函数的动作。策略定义了在给定时刻学习代理的行为。它将环境的检测状态与在这些状态下采取的行动相对应。这与心理学中所说的刺激反应规则或关联集合相对应。策略是强化学习代理的基本部分,因为仅凭它就足以决定行为。
因此,方法建议的策略是在前两个状态下不进行卫生消毒操作,而是在处于状态 3 时执行。原因是,在前两个状态下感染的概率较低,而在状态 3 中则变得重要。
- 最后,我们打印了 100 次迭代中的不一致均值:
print(QLearnModel$mean_discrepancy)
以下表格已打印:

mean_discrepancy 值是 V 的不一致均值向量,经过 100 次迭代。这里,默认的 N 值下,向量的长度为 100。
总结
在本章中,我们学习了如何在医疗领域使用强化学习。我们首先分析了基于机器学习算法在医疗中的应用概况。我们看到这些算法如何用于疾病诊断、流行病事件预测以及新药物测试。
然后,我们处理了两个实际案例。首先,我们看到如何通过将医疗保险问题建模为马尔科夫过程来解决问题。通过这种方式,可以预测工人受伤的概率,并量化需要支付的保费。在第二个例子中,我们确定了在手术室消毒操作计划中应该采用的最佳策略。这个问题通过 Q-learning 技术得以解决。
在下一章中,我们将探索深度强化学习的世界,以及如何利用神经网络使最佳策略研究操作更加高效。
第四部分 - 深度强化学习
本节展示了如何使用强化学习算法实现深度学习架构。
本节包含以下章节:
-
第十一章,探索深度强化学习方法
-
第十二章,使用 Keras 进行深度 Q 学习
-
第十三章,接下来是什么?
第十一章:探索深度强化学习方法
神经网络(NNs)在获取高度结构化数据的良好特征方面非常有效。然后,我们可以用神经网络来表示我们的 Q 函数,该神经网络以状态和动作作为输入,并输出(给出)相应的 Q 值。深度强化学习(DRL)方法使用深度神经网络来逼近以下任何强化学习组件:价值函数、策略和模型。
在本章中,我们将逐步学习 DRL。首先,我们将学习人工神经网络的基本概念,并通过实际示例看到如何应用它们。随后,我们将看到如何将这些概念应用于强化学习,以提高算法的性能。
到本章结束时,我们将学习人工神经网络的基本原理,如何将前馈神经网络方法应用于你的数据,以及神经网络算法如何工作。我们将理解深度神经网络用来逼近强化学习组件的基本概念,并将学习如何使用 R 实现深度 Q 网络。最后,我们将学习如何使用 R 实现深度递归 Q 网络。
在本章中,我们将讨论以下主题:
-
介绍神经网络基本概念
-
管理前馈神经网络
-
用于回归的神经网络
-
接近 DRL
-
深度递归 Q 网络
技术要求
查看以下视频,了解代码的实际应用:
介绍神经网络基本概念
人工神经网络(ANNs)是数学模型,其目的是尝试模拟一些典型的人类大脑活动,例如模式识别、语言理解、图像感知等。一个 ANN 的架构由一组节点组成,这些节点指代人类大脑中的神经元,节点之间通过加权连接相互连接,模拟神经元之间的突触。网络的输出通过连接权重迭代更新,直到收敛。实验领域收集的数据在输入层提供,网络结果在输出层提供。输入节点代表预测输出神经元所需的独立或预测变量。
神经网络提供了一套非常强大的工具,可以解决分类、回归和非线性控制领域的问题。除了具有较高的处理速度外,神经网络还能够从一组示例中学习解决方案。在许多应用中,这使得我们能够绕过开发物理过程模型的需求,而这一模型往往难以找到,甚至不可能找到。
ANNs 尝试模拟生物神经元的行为。我们来看看具体如何实现的。
生物神经网络
神经网络的灵感来源于对生物神经系统信息处理机制的研究,特别是人类大脑;事实上,神经网络的研究正是为了探究这些机制。人工神经网络由许多神经元或简单的处理单元组成。人工神经元模拟生物神经元的特性——人类神经系统中的每个细胞都能够接收、处理并传输电信号。
它由四个基本部分组成,分别是:
-
细胞体
-
突触
-
轴突
-
树突
树突通过突触接收来自其他神经元的电信号,并将其传输到细胞体。在这里,这些信号被加在一起,如果总的兴奋度超过了某个阈值,细胞就会通过轴突将信号传递给其他细胞。
以下图示展示了一个生物神经元的结构:

当信号到达突触时,它会引起一种叫做神经递质的化学物质的释放,这些化学物质进入其他神经元的细胞体。根据突触的类型(可以是兴奋性或抑制性),这些物质分别增加或减少下一个神经元被激活的概率。在每个突触处,都会有一个权重值与之相关,这个权重决定了兴奋或抑制效应的类型和大小。因此,每个神经元都会对来自其他神经元的输入进行加权求和,如果这个求和结果超过了某个阈值,神经元就会被激活。
每个神经元执行的操作持续时间为毫秒级,因此它代表了一个相对较慢的处理系统。然而,整个网络拥有大量的神经元和突触,这些神经元和突触能够并行并同时操作,从而使得实际的处理能力非常强大。此外,生物神经网络对不准确甚至错误的信息具有很高的容忍度;它具有学习和归纳的能力,这使得它在识别和分类操作中表现得非常高效。
神经元的功能调控着大脑的活动,大脑是一个自然优化的解决复杂问题的机器。它由简单的元素构成,经过时间的演化,朝着提升其能力的方向发展:大脑没有中央控制,各个区域共同协作完成任务或解决问题。如果大脑的某一部分停止工作,它仍然能够继续执行任务,尽管可能没有原来的表现那么好。大脑具有容错能力;随着神经元的破坏,其性能会缓慢下降。
人工神经网络
类似于生物神经元,人工神经元接收来自不同神经元的输入刺激。每个输入会乘以一个相应的权重,并与其他输入相加,进而通过另一个函数决定神经元的激活水平。
神经网络的架构特点在于输入神经元与输出神经元的区分、突触(或神经元)层数以及反馈连接的存在,如下图所示:

当输入向量(刺激)作用于神经网络的输入神经元时,信号沿着连接以并行方式在内部节点之间传播,直到输出并产生神经网络的响应。在最简单的模型中,每个节点仅处理局部信息,不知道整体处理目标,并且没有记忆。网络的响应和行为本质上取决于其架构和人工突触的值。
在某些情况下,单一的突触层不足以学习输入和输出模式之间的期望关联:在这种情况下,需要使用多层网络,具有内部神经元和多个突触层。这些网络被称为深度神经网络。这样的网络响应是通过逐层计算神经元的激活值,逐渐从内部节点向输出节点推进得到的。
人工神经网络的目标仅仅是通过确定性计算计算所有神经元的输出。基本上,ANN 是一组数学函数逼近。以下元素是 ANN 架构中至关重要的:
-
层
-
权重
-
偏置
-
激活函数
在接下来的部分中,我们将深入探讨这些概念。
层类型
我们已经在人工神经网络部分介绍了人工神经网络的架构,并且能够分析一个突出不同类型神经元的方案。在该方案中,可以识别出一个层次结构。事实上,我们可以轻松地识别出输入层、一个中间层(称为隐藏层)和输出层,如下图所示:

在之前的图中,可以识别出最简单的架构,包括输入层、一个隐藏层和输出层。
每一层都有自己的任务,通过它所包含的神经元的作用来执行。输入层的作用是将初始数据输入系统,以便后续层进一步处理。从输入层开始,人工神经网络的工作流程启动。
在输入层,人工神经元扮演着不同的角色,因为它们不会接收来自前一层的信息。在一般情况下,它们接收一系列输入,并将信息首次引入系统。然后,这一层将数据传递给下一层,在那里神经元接收加权输入。
人工神经网络中的隐藏层位于输入层和输出层之间。隐藏层的神经元接收一组加权输入,并根据从激活函数接收到的指示产生输出。它代表了整个网络的核心部分,因为正是在这里,输入数据转化为输出响应的“魔法”发生。
隐藏层可以以多种方式操作。在某些情况下,输入是随机加权的;在其他情况下,它们通过迭代过程进行调整。通常,隐藏层的神经元类似于大脑中的生物神经元:它接收其概率输入信号,对其进行处理,并将其转换为对应于生物神经元轴突的输出。
最后,输出层为模型生成特定的输出。虽然它们的生成方式与神经网络中的其他人工神经元非常相似,但输出层中神经元的类型和数量取决于系统必须提供的响应类型。例如,如果我们正在设计一个用于对象分类的神经网络,则输出层将由一个节点组成,该节点将为我们提供此值。事实上,该节点的输出只需提供一个正面或负面的指示,表示目标在输入数据中是否存在。
权重和偏置
在人工神经网络中,输入转换为输出依赖于连接权重的贡献。在线性回归中,斜率与输入相乘以提供输出。对于神经网络中的权重,也可以做出相同的论点。实际上,权重是表示每个神经元对最终结果贡献的数值参数。例如,如果输入是 x[1]、x[2] 和 x[3],则应用于这些输入的突触权重分别表示为 w[1]、w[2] 和 w[3]。
在这种假设下,我们可以通过以下公式表示神经元返回的输出:

在前面的公式中,i 是输入的数量。
在前面的公式中,矩阵乘法定义了加权和。为了这个加权和,必须加上偏置,这可以与线性方程中的加截距进行比较。因此,偏置是一个额外的参数,用于调整每个神经元的输出。
神经元的处理过程因此表示如下:

输出由激活函数进行调整。某一层神经元的输出将代表下一层的输入,如下图所示:

这个方案的含义是,我们给输入信号(x[i])赋予一个权重(w[i]),它是一个实数,模拟了自然突触。当* w[i]的值大于零时,通道被称为兴奋性;如果值小于零,则通道为抑制性。w[i]*的绝对值表示连接的强度。
激活函数
激活函数在处理系统输出中起着至关重要的作用。激活函数表示一种数学函数,它将输入转换为输出,并根据神经网络定义处理过程。如果没有激活函数的贡献,神经网络将简化为一个简单的线性函数。在线性函数中,从输入到输出的转换通过直接的比例关系实现,如下所示:

简单来说,线性函数是一阶多项式,即一条直线。在现实世界中,大多数问题都是非线性且复杂的。为了解决非线性问题,必须使用激活函数。非线性函数是高阶多项式函数,如下所示:

它是一个包含复杂度因子的非线性函数。激活函数为神经网络添加了非线性特性,并将其表征为通用函数的逼近器。
神经网络可使用许多激活函数。以下是最常用的几种:
-
Sigmoid:该函数由 S 形的 Sigmoid 曲线表示。它是最常用的激活函数。它的作用是将输入转换为 0 和 1 之间的值。这样,模型呈现出一种逻辑性质。
-
单位阶跃:该函数将输入转换为 0(如果参数为负数),或 1(如果参数为正数)。这样,输出具有二进制性质。这些激活函数用于二进制方案。
-
双曲正切:它是一个非线性函数,定义在值的范围内(-1, 1)。这些函数很有趣,因为它们允许神经元具有连续的输出,从而可以进行概率解释。
-
整流线性单元(ReLU):它是一个具有线性特性的函数,对于存在域的部分,如果输入是正数,则直接输出输入值;否则,输出为零。其输出范围介于 0 和无限大之间。ReLU 在计算机视觉和语音识别中通过深度神经网络得到应用。
管理前馈神经网络
当数据流从输入层传递到隐含层,再到输出层时,我们称之为前馈传播。在这种情况下,转移函数应用于每一层隐藏层。因此,激活函数的值会传播到下一层。下一层可以是另一层隐藏层,或者是输出层。
前馈一词用于表示每个节点仅接收来自低层的连接的网络。这些网络会对每个输入模式发出响应,但无法捕捉输入信息可能的时间结构或展示内生的时间动态。
现在让我们进入神经网络的一个关键话题:神经网络训练。
神经网络训练
为了选择使神经元开启或关闭的输入值,需要训练网络。这是实现模型中的关键步骤,旨在训练神经网络以从一组对应已知输出的输入中进行泛化。网络的性能在很大程度上取决于呈现给它们的信息:这些信息必须代表网络需要学习的内容。训练是构建神经网络的基础,所使用的示例(训练集)必须仔细选择。
我们将采取逐步的方式来理解具有单一隐藏层的神经网络训练。假设输入层有一个神经元,输出将解决一个二分类问题(预测 0 或 1)。以下是训练网络的所有步骤列表:
-
将输入加载为矩阵。
-
使用随机值初始化权重和偏置。这一步仅在开始时进行,之后只需更新它们。
-
对每个周期重复步骤 4 到 9,直到收敛。
-
将输入传递到网络中。
-
从输入层通过隐藏层(s)到输出层,估计输出。
-
估计输出的误差。
-
采用输出误差来计算前面层的误差信号。
-
采用误差信号计算权重变化。
-
使用权重变化来更新权重。
步骤 4 和 5 是前向传播,步骤 6 到 9 是反向传播。
通常,教会网络通过调整神经元权重 (w[i]) 来进行泛化的最常用方法是遵循增量规则,该规则通过比较网络输出与期望值:将两个值相减,差值用于更新所有输入的权重,其中这些输入的值不为零。
该过程会迭代,直到收敛为止。下图显示了净重调整过程的图示:

在实践中,算法将输入与输出进行比较:计算加权输入值与输出或期望值之间的差异,并利用该差异(误差)重新计算所有输入权重。该过程会反复进行,直到输入和输出之间的误差接近于零。
在接下来的部分中,我们将应用神经网络来解决回归问题。
回归神经网络
回归分析是数据科学的起点;事实上,它们是数值模拟中最为理解透彻的模型。回归模型易于解释,因为它们基于坚实的数学基础——可以想象为矩阵代数。线性回归允许我们推导出一个数学公式来代表相应的模型。因此,这些技术极易理解。
回归分析是一种统计过程,旨在识别一组自变量(解释变量)与因变量(响应变量)之间的关系。通过这项技术,可以确定当解释变量变化时,响应变量的值如何变化。
在接下来的段落中,提出了一个回归预测建模问题的示例,旨在理解如何通过神经网络来解决。将使用波士顿数据集作为数据来源;预测测试数据中业主自住住宅的中位数价格。该数据集描述了波士顿郊区房屋的 12 个数值特征,目的是建模这些郊区房屋的价格(以千美元为单位)。因此,这是一个回归预测建模问题,因为输出是一个连续变量。
记住,回归与分类都与预测有关:在分类中,我们试图通过将输出分组为类别(分类变量)来预测输出,而在回归中,我们试图以连续的方式(连续变量)预测输出值。
波士顿数据集的输入属性包括犯罪率、非零售商业区土地比例和化学物质浓度等特征。
获取数据时,我们利用 UCI 机器学习库中的大量数据集,数据链接如下:archive.ics.uci.edu/ml。
实例数量和变量数量如下所示:
-
实例数量:506
-
变量数量:13 个连续变量(包括类属性
medv)和 1 个二值属性
所有变量如下所示:
-
crimper:每个城镇的人均犯罪率 -
zn:规划为大于 25,000 平方英尺地块的住宅用地比例 -
indus:每个城镇的非零售商业区土地比例 -
chas:查尔斯河虚拟变量(= 1 如果地块与河流相邻;否则为 0) -
nox:氮氧化物浓度(每千万分之一) -
rm:每个住宅的平均房间数 -
age:1940 年之前建造的房主自有住房比例 -
dis:到波士顿五个就业中心的加权距离 -
rad:径向公路可达性指数 -
tax:每$10,000 的房产税率 -
ptratio:每个城镇的师生比例 -
lstat:人口中低社会经济地位的比例 -
medv:房主自有住宅的中位数价值(以千美元为单位)
在之前的列表中,medv是响应变量,其他 13 个变量是预测变量。我们的目标是开发一个回归模型,用以模拟medv值的变化。该模型应该能够识别前 13 列与响应变量medv之间的关系(如果存在的话)。
这个数据集已经通过 R 库(MASS)提供,因此我们无需担心获取数据。
首先,我们需要获取数据。正如我们所说的那样,我们可以使用 MASS 库:
- 让我们加载所需的库:
library(MASS)
要安装一个新的库,需要使用install.packages()函数。这个功能安装所需的包。需要传递一个包含名称的向量和目标库,之后该命令将从仓库下载并安装这些包。
- 现在让我们关注如何使实验具有可重复性:
set.seed(5)
set.seed()命令使得示例可重复,即每次模拟生成的所有随机数始终相同。
- 现在我们可以加载数据集了:
InputData = Boston
- 我们将只使用对分析有用的变量:
InputData = subset(InputData, select = -c(12) )
数据框还包括原始数据集中变量的名称。
让我们先看看数据。
探索性分析
现在我们进行探索性分析,看看数据是如何分布的,并提取初步知识。让我们首先使用str()函数检查数据集。这个函数返回一个紧凑的 R 对象内部结构总结。
理想情况下,每个基本结构只显示一行:
str(InputData)
返回的结果如下:
'data.frame': 506 obs. of 13 variables:
$ crim : num 0.00632 0.02731 0.02729 0.03237 0.06905 ...
$ zn : num 18 0 0 0 0 0 12.5 12.5 12.5 12.5 ...
$ indus : num 2.31 7.07 7.07 2.18 2.18 2.18 7.87 7.87 7.8 ...
$ chas : int 0 0 0 0 0 0 0 0 0 0 ...
$ nox : num 0.538 0.469 0.469 0.458 0.458 0.458 0.524 ...
$ rm : num 6.58 6.42 7.18 7 7.15 ...
$ age : num 65.2 78.9 61.1 45.8 54.2 58.7 66.6 96.1 100...
$ dis : num 4.09 4.97 4.97 6.06 6.06 ...
$ rad : int 1 2 2 3 3 3 5 5 5 5 ...
$ tax : num 296 242 242 222 222 222 311 311 311 311 ...
$ ptratio: num 15.3 17.8 17.8 18.7 18.7 18.7 15.2 15.2 15\. ...
$ lstat : num 4.98 9.14 4.03 2.94 5.33 ...
$ medv : num 24 21.6 34.7 33.4 36.2 28.7 22.9 27.1 16.5...
所以,我们确认数据集包含 506 个观测值和 13 个变量:11 个数值型和 2 个整数型。现在,为了获得数据集的简要总结,我们可以使用summary()函数,如下所示:
summary(InputData)
summary()函数返回一系列数据统计信息。
结果显示如下:

结果分析表明,变量的范围不同。当预测变量的极端值差异很大时,具有极端值的特征对响应变量的权重可能会占主导地位,这会影响预测的准确性。因此,我们可能需要对不同特征下的值进行缩放,以便它们落在一个共同的范围内。通过这一统计过程,可以比较属于不同分布的相同变量,也可以比较以不同单位表示的变量。
请记住,在训练回归算法之前,重新缩放数据是一个好习惯。使用重新缩放技术可以消除数据单位,这使得我们可以轻松比较来自不同位置的数据。
为了重新缩放数据,我们将使用最小-最大方法,将所有数据缩放到[0, 1]范围内。实现这一点的公式如下:

首先,我们需要计算数据库中每一列的最小值和最大值。我们将使用apply()函数将函数应用于矩阵的值:
MaxData <- apply(InputData, 2, max)
三个参数已经传递:第一个指定要应用函数的数据集(InputData)。第二个参数指定函数(2)将应用的索引。由于是矩阵,1 指定行,2 指定列。第三个参数指定要应用的函数,在我们的案例中是max()函数。
现在,我们将计算每一列的最小值:
MinData <- apply(InputData, 2, min)
现在,我们需要应用scale()函数来归一化数据。scale()函数对数值矩阵的列进行中心化和/或缩放,如下所示:
DataScaled <- scale(InputData,center = MinData, scale = MaxData - MinData)
为了确认数据是否已归一化,我们再次应用summary()函数:
summary(DataScaled)
打印出以下结果:

让我们开始我们的探索性分析。我们可以通过绘制变量的箱型图来进行,如下所示:
boxplot(BHDataScaled)
打印出以下图表:

前面的图表清楚地显示了一些变量具有异常值。例如,变量 crim 显示了最多的离群值。离群值在数值上与其余数据明显不同。含有异常值的变量所获得的统计数据可能会返回错误的信息。
训练网络
在训练网络之前,我们必须先拆分数据。我们将从数据拆分开始,将数据按照指定的比例划分为训练集和验证集。在数据集非常大的情况下,这种技术尤其有用。在这种情况下,数据集被划分为两个部分:训练集和测试集。训练集用于训练模型,而测试集则为我们提供了显著的性能估计。当使用缓慢的算法并且需要快速估算性能时,这种方法非常有利。
以下示例将数据集划分为 70%的数据用于训练神经网络模型,其余 30%的数据用于评估模型性能:
IndexData = sample(1:nrow(InputData),round(0.70*nrow(InputData)))
TrainData <- as.data.frame(DataScaled[IndexData,])
TestData <- as.data.frame(DataScaled[-IndexData,])
第一行的代码将 70:30 的数据进行拆分,即使用 70%的数据来训练网络,剩下的 30%用于测试网络。在第二行和第三行中,名为DataScaled的数据框被拆分为两个新数据框,分别称为TrainData和TestData。
现在,我们需要设置构建神经网络时使用的公式:
n = names(InputData)
f = as.formula(paste("medv ~",
paste(n[!n %in% "medv"],
collapse = " + ")))
在前面的代码中,我们首先通过names()函数获取所有变量名。接下来,我们创建用于构建网络的公式。
neuralnet()函数使用紧凑的符号形式的公式。~操作符定义了模型。例如,公式 y ~模型的意思是,输出 y 由符号方式指定的预测变量建模。该模型由一系列通过+操作符分隔的项组成,每项是通过:操作符分隔的变量名。
然后,我们将使用neuralnet库来构建和训练网络。让我们加载该库:
library("neuralnet")
neuralnet库用于使用反向传播、弹性反向传播(RPROP)以及带或不带权重回溯的训练神经网络,或者使用修改后的全局收敛版本(GRPROP)。以下表格提供了该包的一些信息:
| 包 | neuralnet |
|---|---|
| 日期 | 2019-02-07 |
| 版本 | 1.44.2 |
| 标题 | 神经网络的训练 |
| 作者 | Stefan Fritsch, Frauke Guenther, Marvin N. Wright, Marc Suling, Sebastian M. Mueller |
以下列出了该包中最有用的函数:
-
neuralnet:神经网络的训练。 -
compute:计算给定协变量向量的神经网络结果。 -
prediction:总结神经网络的输出、数据以及glm对象的拟合值(如果可用)。 -
plot.nn:神经网络的绘图方法。
现在,我们可以构建和训练网络。首先,我们必须选择神经元的数量,为此,我们需要了解以下内容:
-
选择较少神经元的层会导致较高的误差;这是因为预测因子可能过于复杂。
-
相反,过多的神经元会使训练数据过载,无法进行泛化。每个隐藏层中的神经元数量应介于输入层和输出层之间,例如取平均值。
-
每个隐藏层中的神经元数量不应超过输入神经元数量的两倍。
我们选择在隐藏层设置十个神经元。别担心——最佳选择通过经验得出:
NetDataModel = neuralnet(f,data=TrainData,hidden=10,linear.output=T)
hidden参数指定每个隐藏层的神经元数量。linear.output参数执行回归(如果linear.output=TRUE)或分类(如果linear.output=FALSE)。
为了生成模型结果的汇总,我们使用summary()函数:
summary(NetDataModel)
返回以下结果:
> summary(NetDataModel)
Length Class Mode ca
ll 5 -none- call
response 354 -none- numeric
covariate 4248 -none- numeric
model.list 2 -none- list
err.fct 1 -none- function
act.fct 1 -none- function
linear.output 1 -none- logical
data 13 data.frame list
net.result 1 -none- list
weights 1 -none- list
startweights 1 -none- list
generalized.weights 1 -none- list
result.matrix 144 -none- numeric
每个神经网络模型组件显示三个特征:
-
长度: 此功能指定该类型包含的元素数量。
-
类: 此功能返回有关组件类别的具体指示。
-
模式: 此功能描述了组件的类型(数字、列表、函数、逻辑等)。
plot()函数绘制一张图,显示神经网络的架构,包括各层、节点和每个连接的权重:
plot(NetDataModel)
神经网络图如下所示:

在前面的图示中,黑线表示各层之间的连接;此外,每个连接上的权重值也被打印出来。蓝线显示了每一步中添加的偏置。
神经网络模型评估
现在,我们可以使用网络进行预测。为此,我们已经将 30%的数据保留在TestData数据框中。现在是时候使用它了:
PredNetTest <- compute(NetDataModel,TestData[,1:12])
我们如何判断网络的预测是否准确呢?我们可以使用均方误差(MSE)作为衡量我们预测与真实数据之间差距的标准。
在算法的第一部分,我们对数据进行了归一化。为了比较数据,我们需要回退并返回到原始数据。一旦数据集的值被恢复,我们就可以通过以下方程计算 MSE:

以下代码执行了 MSE 计算:
PredNetTestStart <- PredNetTest$net.result*(max(InputData$medv)-
min(InputData$medv))+min(InputData$medv)
TestStart <- as.data.frame((TestData$medv)*(max(InputData$medv)-
min(InputData$medv))+min(InputData$medv))
MSENetData <- sum((TestStart -
PredNetTestStart)²)/nrow(TestStart)
现在我们有了结果,但我们该与什么进行比较呢?为了与另一个模型进行比较,我们可以构建一个线性回归模型。然后,我们通过应用lm()函数来建立线性回归模型。这个函数用于处理线性回归模型,如下所示:
RegressionModel <- lm(medv~., data=InputData)
为了生成模型结果的总结,我们可以再次使用summary()函数,如下所示:
summary(RegressionModel)
以下是返回的结果:

现在,我们将计算基于多重回归的模型的 MSE:
TestDataComp <- InputData[-IndexData,]
PredictLm <- predict(RegressionModel,TestDataComp)
MSERegrData <- sum((PredictLm - TestDataComp$medv)²)/nrow(TestDataComp)
最后,我们可以比较两个模型的结果:
cat("MSE for Neural Network Model =",MSENetData,"\n")
cat("MSE for Regression Model =",MSERegrData,"\n")
以下是打印出的结果:
> cat("MSE for Neural Network Model =",MSENetData,"\n")
MSE for Neural Network Model = 19.41977332
> cat("MSE for Regression Model =",MSERegrData,"\n")
MSE for Regression Model = 34.83062039
从两个模型(神经网络模型与线性回归模型)的比较中可以看出,神经网络获胜(19.4 对 34.8)。
在接下来的部分,我们将看到如何开发一个 DRL 模型。
接近 DRL
在第七章,时间差分学习中,我们展示了一个实际的例子,使用 Q-learning 来解决车辆路径规划问题。在那个案例中,值函数的估计是通过一个表格完成的,其中每个格子代表一个状态或一个状态-动作对。使用表格表示值函数能够创建简单的算法。在马尔可夫环境下,这个表格允许我们准确地估计值函数,因为它为环境的每一个可能配置分配了在策略迭代过程中预期的表现。然而,使用表格也带来了局限性。这些方法仅适用于状态和动作数目较少的环境。问题不仅限于存储表格所需的大量内存,更主要的是准确估计每个状态-动作对所需的大量数据和时间。换句话说,主要问题在于泛化能力。
为了解决这个问题,我们可以采用一种基于强化学习方法与函数逼近方法结合的方案。下图展示了一个深度 Q 学习方案:

“深度 Q 学习”这一术语指的是一种强化学习方法,它采用神经网络作为价值函数的逼近。因此,它代表了基本 Q 学习方法的进化,因为动作-状态表被神经网络所取代,用以逼近最优价值函数。
这是与前几章看到的方法相比,一种创新的 approach。到目前为止,算法的输入提供了状态和动作,以便提供期望的回报。深度 Q 学习彻底革新了结构,因为它只需要环境的状态作为输入,并提供所有的状态-动作值,因为在环境中可以执行的动作是多样的。
Q 学习是一种在强化学习中广泛使用的算法。最初,当它与神经网络一起使用时,被认为是不稳定的算法,因此其使用被限制在涉及有限维度状态空间的任务和问题中。Q 学习算法和技术可以与深度神经网络(DNN)一起使用。这些算法已展示出卓越的性能。
深度 Q 学习或深度 Q 网络(DQN)是一种用于函数逼近的强化学习方法。它代表了 Q 学习方法的进化,其中动作-状态表被神经网络取代。在这个算法中,学习不再是更新表格,而是调整构成网络的神经元的权重。这个更新过程是通过反向传播技术进行的,正如我们在本章的神经网络训练部分中所学到的。
因此,价值函数的学习是基于使用以下函数来修改权重:

在前面的方程中,两个项具有以下含义:
-
L[t] 是损失函数。
-
是最优的预期回报。 -
是网络的估计值。
损失函数计算的误差将通过反向步骤(反向传播)在网络中进行传播,遵循梯度下降的逻辑。实际上,梯度指示了函数增长的最大方向;朝相反方向移动,我们将误差降至最小。策略行为通过 e-贪婪方法给出,以确保足够的探索。DQN 的关键方面是经验回放的使用。通过这种技术,代理的经验在每个时间步* t * 时被捕获并保存在一个名为回放记忆的数据集中。
训练通过小批量技术进行,即从回放记忆中随机提取一部分经验样本。通过这种方式,过去的经验被用来更新网络。此外,回放记忆随机选择的子集可以打断连续经验之间的强相关性,从而减少更新之间的方差。
以下是伪代码中的算法:
Initialize Replay Memory D
Initialize Q (s, a) with random weights
repeat Observe initial state s1
for t = 1 to T do
Select an action using Q (greedy)
Perform the action at
Look at the reward rt and the new state st+1
Save the observation (st, at, rt, s+ 1) in the Replay Memory D
Take a sample (sj, aj, rj, sj+1) from D
Calculate the target T for each observation
if sj + 1 is Terminal state then
T = rj
else
T = rj + γ maxa Q (sj+1, aj)
end if
trains the Q network by minimizing (T - Q (sj, aj))2
end for
until
该算法可以使用 R 和可用于神经网络及强化学习的库来实现。
现在我们来看一个高级的深度强化学习(DRL)示例。
深度递归 Q 网络
在上一节《接近深度强化学习》中,我们已经提到深度 Q 学习使用神经网络作为价值函数的近似。然而,这种方法有着有限的内存,并且依赖于在每个决策点感知环境状态的可能性。为了克服这个问题,我们可以通过用递归 LSTM 替换第一级全连接神经网络,向深度 Q 网络(DQN)中添加递归。通过这种方式,得到了深度递归 Q 网络(DRQN)模型。
让我们从递归神经网络开始。
递归神经网络
递归神经网络(RNN)是一种神经网络模型,其中存在双向信息流。换句话说,在前馈网络中,信号的传播仅以单向连续的方式从输入到输出进行,而递归网络则不同。在递归网络中,这种传播不仅可以发生在一个神经层向后传播到前一个神经层,还可以在同一层中的神经元之间,甚至在神经元与其自身之间发生。
递归网络将在特定的时刻做出决定,这些决定会影响它随后的决策。递归网络有两个输入来源:当前的状态和最近的过去。这些信息被结合起来以确定如何对新数据作出反应。递归网络与前馈网络的不同之处在于,它们加入了与过去决策相关的反馈。这一功能使得递归网络具有记忆,能够执行前馈网络无法完成的任务。
内存的访问是通过内容进行的,而不是通过地址或位置。一种方法是,内存内容是 RNN 节点上激活的模式。其理念是,从一个部分或噪声表示的激活模式开始网络,作为请求的内存内容的表示,然后网络将稳定到所需的内容。
RNN 是一类神经网络,其中至少存在一个神经元之间的反馈连接,形成一个有向循环。一个典型的具有输出层和隐藏层连接的 RNN 如下图所示:

在前面图示的递归网络中,输入层和输出层都用来定义隐藏层的权重。最终,我们可以将 RNN 看作是 ANN 的一种变体:这些变体可以通过不同数量的隐藏层和不同的数据流趋势来特征化。RNN 的特点是数据流的不同趋势,事实上,神经元之间的连接形成了一个循环。递归神经网络可以利用内部记忆进行处理,因为它们在隐藏层之间有连接,这些连接会随时间传播,学习序列数据。
总结
在本章中,我们探索了 DRL 的世界。首先,我们学习了神经网络的基本概念。我们理解了层、节点、偏置和传递函数的概念。简而言之,我们了解了全连接神经网络的架构如何构建。随后,我们应用所学的技能,构建了一个神经网络来解决回归问题。接着,我们学习了 DRL 的含义以及神经网络如何用来逼近价值函数。最后,我们分析了另一种形式的 DRL,其中神经网络被递归网络所替代。这些被称为 DRQN 的网络已被证明特别高效。
在下一章,我们将探索使用 TensorFlow 作为后端引擎的 Keras 模型。我们将学习如何使用 Keras 设置一个多层感知器模型。接着,我们将学习如何使用 DRL 平衡一个小车摆系统。
第十二章:使用 Keras 进行深度 Q 学习
Keras 是一个高级神经网络库,使用 Python 编写,并能够与不同的支持库配合使用。它的开发旨在允许快速实验。Keras 通过完全模块化、极简主义和可扩展性,提供了简单且快速的原型设计功能。它支持卷积网络和递归网络,以及两者的组合。此外,它支持任意连接方案,并能在 CPU 和 GPU 上平稳运行。在本章中,我们将学习如何使用 Keras 处理强化学习。我们将学习如何使用 Keras 开发一个能够识别手写数字的模型。随后,我们将使用深度 Q 学习来平衡小车摆杆系统。
在本章中,我们将涵盖以下主题:
-
Keras 介绍
-
图像处理中的多层感知机
-
深度 Q 学习方法
在本章结束时,我们将探索使用 TensorFlow 作为后端引擎的 Keras 模型,并学习如何使用 Keras 设置多层感知机(MLP)模型。接着,我们将学习如何使用深度强化学习来平衡小车摆杆系统。
技术要求
查看以下视频,观看代码运行实例:
Keras 介绍
Keras 是一个 Python 库,提供了一种简单、清晰的方式来创建各种深度学习模型。Keras 的代码在 MIT 许可下发布。Keras 的结构基于简洁和简约原则,提供了一种没有多余功能的编程模型,最大限度地提高了可读性。它允许神经网络以非常模块化的方式表达,将模型视为一系列组件或单一图形。这是一个很好的近似,因为深度学习模型的组件是可以任意组合的离散元素。新的组件可以在为工程师设计的框架内轻松集成和修改,以快速测试和探索新想法。最后,使用 Python 编程语言提供的构造允许在小规模和大规模上进行清晰编程。
在下方截图中,我们可以看到 Keras 的官方网站首页(keras.io/):

Keras 的易用性是其强大优势之一。在设计阶段,开发者一直将用户需求作为重点,创造出了通过简单一致的 API 来减少用户工作量的产品。这样,解决常见用例所需的操作数量被大大减少。此外,结果返回清晰,便于迅速识别潜在错误。
在 Keras 中,一个模型由一系列自主且完全可配置的模块组成,这些模块可以与最低限度的约束条件相关联。Keras 中的一切都是模块——神经网络层、代价函数、优化器、初始化方案、激活函数和正则化方案。这些独立的模块可以组合在一起,创建新的、更复杂的模型。
Keras 中可用的所有模块都很容易添加,就像在编程语言中添加新的类和函数一样。此外,这些模块已经可用,并且附带了大量解释其实际应用的示例。但 Keras 并不限于内置模块的可用性。用户可以轻松创建新的模块,使得 Keras 成为一个易于扩展的环境。
keras 库基于用于管理输入和输出的层技术。在 Keras 中,应用可以通过以下四个简单步骤实现:
-
准备输入和输出数据。
-
创建第一个层以管理输入数据。
-
设置中间层以进行分析。
-
创建输出层以管理目标。
Keras 作为一个特定的高层 API 用于神经网络。它可以充当用户界面,并且可以扩展它所运行的其他深度学习框架后端的功能。正因为这个特点,Keras 成为了框架之间迁移的封装器。不仅可以交换深度学习神经网络的算法和模型,还可以交换网络和预训练权重。
封装库由一层薄代码组成,负责将库的现有接口转换为兼容的接口。
另一方面,由于 Keras 是自主的,它可以在不与其运行的后端框架交互的情况下使用。Keras 有自己的图数据结构用于定义计算图;它并不依赖于底层后端框架的数据结构。这样,你就不必学习如何编程后端框架。
Keras 易于学习和使用。使用 Keras 就像玩 LEGO® 积木一样;你只需要将一系列兼容的模块按顺序排列即可。它的设计目的是让人们能够通过一个高度模块化和可扩展的框架,快速进行模型的实验阶段。Keras 主要集中在定义神经网络的层。你不需要处理张量,但可以用更少的代码轻松编写。
后端负责复杂的数学运算,在下一节中,我们将看到 TensorFlow 后端的工作原理。
TensorFlow 中的 Keras 后端
Keras 是一个模型级库,提供用于深度学习模型开发的高级模块。Keras 开发者专注于创建高级模型,而忽略了低级操作,如张量乘积和卷积。这些操作已被交给专门的、经过优化的张量操作库来处理,因此它们充当了 Keras 的后端引擎。多个后端引擎可以完美地连接到 Keras。实际上,Keras 提供了三个可用的后端实现——TensorFlow、Theano 和微软 认知工具包(CNTK)。
TensorFlow 是一个基于图模型(数据流图)的开源软件库,用于数值计算。图被定义为在张量上操作的数学运算的抽象管道,并被称为多维数组。每个图由节点和弧线组成,其中节点是数据上的操作,弧线表示通过各种操作传递的张量。
你可以在以下链接找到该库的最新版本和所有提供的文档:www.tensorflow.org。
TensorFlow 是机器学习和神经网络领域最常用的库。它有许多 API,包括最低级别的 TensorFlow Core,允许完全控制编程。这些 API 通常用于机器学习领域,因为它们使得可以详细检查实现中的所有模型元素。最高级别的 API 是基于 TensorFlow Core 构建的。在某些情况下,它们可以使某些操作,如重复性和预定义任务,更加快速和简单,但通常无法深入细节,且在神经网络的实现中,往往需要更精确地控制操作。然而,它们在标准机器学习模型的开发中仍然非常有用。
那么,让我们来看一下如何利用 keras 库在 R 环境中提供的潜力。
在 R 中使用 Keras
正如我们在 Keras 简介 部分中预见到的那样,Keras 是用 Python 编写的,因此它是操作的自然开发环境。尽管如此,和许多库一样,已经构建了一个接口,使我们能够利用 Keras 在 R 环境中进行操作。这是因为 keras 库技术的极大简易性,它使得基于机器学习的算法实现变得简单而直观。
要在 R 中使用 Keras,我们可以使用以下网址提供的接口:keras.rstudio.com/index.html。
在下面的截图中,我们可以看到 R 接口到 Keras 的官方网站首页:

将能够检索安装界面所需的所有信息,并开始使用它。在接下来的部分中,我们将看到如何使用 Keras 识别手写数字,确认使用keras库的简易操作性。
用于图像处理的多层感知器
正如我们在第十一章中看到的探索深度强化学习方法,MLP 是一个前馈人工神经网络。最简单的变体是单层变体,它由一个输出节点层组成,而输入则通过一系列权重直接提供给单元。MLP 是一种网络类型,其前馈连接至少包括三个相互连接的层——输入层、隐藏层和输出层。
对于除输入层外的每个节点,都使用非线性激活函数。事实上,如果 MLP 网络具有线性激活函数,将每个神经元的加权输入映射到输出,即使有多个层,也被认为是一个两级输入/输出模型。在训练阶段,通过处理模型中包含的数据来修改连接的权重。更新基于输出中存在的错误量与预期结果的比较。
误差函数是属于权重空间的函数,用于衡量网络在解决问题时的可靠性。学习算法的任务是最小化此函数,因此要找到权重空间中使函数达到全局最小点的点,或者在某些情况下,局部最小点可能就足够了。
为了验证 Keras 在图像处理中的潜力,我们将处理手写数字识别的实际案例。为此,我们将使用开发者社区广泛使用的数据集——MNIST 数据集。
MNIST 数据集
美国国家标准与技术研究院改进版(MNIST)数据集是一个大型手写数字数据库。它包含 70,000 个数据示例。它是 NIST 更大数据集的子集。这些数字的分辨率为 28x28 像素,并存储在一个 70,000 行和 785 列的矩阵中;784 列形成每个 28x28 矩阵的像素值,其中一个值是实际数字。这些数字已经进行了尺寸标准化和中心化处理,存储在固定大小图像中。
MNIST 数据集中的数字图像最初由 Chris Burges 和 Corinna Cortes 选择并进行了边界框归一化和居中处理的实验。Yann LeCun 的版本使用了较大窗口中心质量的中心化方法。数据可在 Yann LeCun 的网站上获取:yann.lecun.com/exdb/mnist/。
下图展示了 MNIST 数据集中 0-8 的图像样本:

该数据集已经包含在 keras 库中,包含 60,000 张 28x28 的灰度图像(用于训练)以及 10,000 张图像的测试集,数字范围为 10。
首先,我们通过适当地准备数据来进行预处理,以便在接下来的 Keras 模型中使用。
数据预处理
在这一部分,我们将分析 MNIST 数据集的特征,并学习如何将数据准备为 Keras 可兼容的格式:
- 让我们开始导入
keras库:
library(keras)
- 要导入
mnist数据集,我们可以使用以下命令:
MnistData <- dataset_mnist()
- 让我们看看数据集里包含了什么:
str(MnistData)
以下是返回的结果:
List of 2
$ train:List of 2
..$ x: int [1:60000, 1:28, 1:28] 0 0 0 0 0 0 0 0 0 0 ...
..$ y: int [1:60000(1d)] 5 0 4 1 9 2 1 3 1 4 ...
$ test :List of 2
..$ x: int [1:10000, 1:28, 1:28] 0 0 0 0 0 0 0 0 0 0 ...
..$ y: int [1:10000(1d)] 7 2 1 0 4 1 4 9 5 9 ...
现在我们可以看到,数据集包含了 60,000 个训练样本和 10,000 个测试样本。每个观察值代表一个 28x28 像素的 (x) 图像,并为每个观察值提供了相应的标签 (y)。
- 现在,我们将提取四个列表,并将它们放入四个变量中,这些变量代表我们要处理的模型的输入和输出数据:
Xtrain <- MnistData$train$x
Ytrain <- MnistData$train$y
Xtest <- MnistData$test$x
Ytest <- MnistData$test$y
在MNIST 数据集部分,我们之前提到过该数据集包含 10 个数字。
- 让我们验证一下;我们还将分析数据集中这些数字的分布情况:
table(Ytrain)
table(Ytest)
以下是返回的结果:
Ytrain
0 1 2 3 4 5 6 7 8 9
5923 6742 5958 6131 5842 5421 5918 6265 5851 5949
Ytest
0 1 2 3 4 5 6 7 8 9
980 1135 1032 1010 982 892 958 1028 974 1009
- 实际上,我们有 10 个数字——从 0 到 9。此外,我们可以验证每个数字的频率是相似的。为此,我们可以绘制一个直方图:
hist(Ytrain)
hist(Ytest)
以下图显示了两个分布的直方图并排展示(YTrain 在左侧,YTest 在右侧):

通过分析之前的图,我们可以看到这两个数据集中 10 个数字的出现频率是均匀分布的。正如我们所说,每个样本图像由一个 28x28 的矩阵组成。
- 为了确认这一点,我们将提取这两个输入向量的维度:
dim(Xtrain)
dim(Xtest)
以下是返回的形状:
> dim(Xtrain)
[1] 60000 28 28
> dim(Xtest)
[1] 10000 28 28
因此,每个观察值包含与 28x28 像素的灰度值相关的数据。
- 为了降低维度,我们将 28x28 的图像展平为大小为
784的向量:
Xtrain <- array_reshape(Xtrain, c(nrow(Xtrain), 784))
Xtest <- array_reshape(Xtest, c(nrow(Xtest), 784))
array_reshape() 函数重新塑形一个多维数组,默认情况下使用行主序(C 风格)重新塑形语义。此函数为数组赋予新形状,而不改变其数据。新形状应与原始形状兼容。新形状的第一维是观察值的数量,第二维表示起始数据的最后两个维度的乘积(28 x 28 = 784)。
- 为了更好地理解这一转化,我们打印出转化后数据集的形状:
dim(Xtrain)
dim(Xtest)
以下是打印的结果:
> dim(Xtrain)
[1] 60000 784
> dim(Xtest)
[1] 10000 784
现在,我们必须将所有值归一化到 0 和 1 之间。MNIST 图像以像素格式存储,其中每个像素(共 28x28)作为一个 8 位整数存储,数值范围从 0 到 255。通常,0 被认为是黑色,255 被认为是白色。
- 其中的值构成了不同的灰度。现在,为了将所有值标准化到 0 和 1 之间,只需将每个值除以 255。因此,包含值 255 的像素将变为 1,而包含值 0 的像素保持不变;介于两者之间的是所有其他值:
Xtrain <- Xtrain / 255
Xtest <- Xtest / 255
在准备好输入数据之后,需要重新排列输出数据。我们之前说过,输出数据表示图像的标签。我们已经看到每个图像都标注了一个从 0 到 9 的数字。为了在 Keras 模型中使用这些值,必须对它们进行修改。
- 定量化每个类别的定性预测变量的方法涉及创建一个二进制变量,0-1(称为虚拟变量),该变量表示每个统计单元中属性的存在与否。我们来看看如何操作:
Ytrain <- to_categorical(Ytrain, 10)
Ytest <- to_categorical(Ytest, 10)
to_categorical() 函数接受一个类标签的向量或 1 列矩阵,并将其转换为一个有 p 列的矩阵,每一列代表一个类别。这是神经网络拟合和预测中最常用的格式。虚拟变量可以取两个值:
-
0:如果属性不存在
-
1:如果属性存在
现在,每个观测将是一个包含 10 个值的行,除了标识包含 1 的数字的列外,其余列均为零。
在准备好数据之后,是时候使用这些数据训练 Keras 模型了。
Keras MLP 模型
在适当预处理数据之后,我们可以定义 Keras 模型的架构。Keras 是基于面向对象的编程方法构建的。因此,创建模型非常简单:选择基本架构,然后添加必要的层以构建所需的模型。如前所述,顺序模型允许你逐层创建模型,作为层的线性堆叠。然而,顺序模型不能创建共享层或具有多个输入或输出的模型。顺序模型是通过将一系列层实例传递给构造函数来创建的:
- 首先,我们从
keras_model_sequential类实例化一个对象:
KerasMLPModel <- keras_model_sequential()
所有关于网络的信息,如权重、层和操作,都将存储在这个对象中。
- 在实例化对象之后,我们将继续添加层:
KerasMLPModel %>%
layer_dense(units = 256, activation = 'relu', input_shape = c(784)) %>%
layer_dense() 函数将一个密集连接的神经网络层添加到输出中。在一个密集连接的层中,每个输入都通过一个权重与每个输出相连,通常后面跟着一个非线性激活函数。第一个参数包含输出空间的维度(units = 256)。第二个参数包含激活函数(activation = 'relu')。修正线性单元(ReLU)是自 2015 年以来最常用的激活函数。它是一个简单的条件,并且相对于其他函数具有优势。
- 最后,第三个参数包含输入形状(
input_shape = c(784))。请记住,传递给顺序模型的第一层应该有一个已定义的输入形状。接下来我们添加第二层:
layer_dropout(rate = 0.45) %>%
dropout 层对输入进行 dropout。dropout 在训练期间通过随机将一定比例的输入单元设为 0 来帮助防止过拟合。只传入一个参数,即比例,它是一个介于 0 和 1 之间的浮动数值,表示丢弃单元的比例。
- 然后,添加第二个全连接层:
layer_dense(units = 128, activation = 'relu') %>%
在这种情况下,输入不存在,输出节点的数量逐渐调整,并且激活函数始终为relu。
- 让我们再加一个 dropout 层:
layer_dropout(rate = 0.3) %>%
- 让我们结束时加上最后一个全连接层:
layer_dense(units = 10, activation = 'softmax')
在前面的代码块中,输出单元为 10,因为系统必须分类 10 个数字。softmax 函数是一种更通用的逻辑激活函数,通常用于多类别分类。
现在,我们将分析我们所定义的 Keras 模型的整体架构。在 Keras 中,若要总结模型,可以使用summary()函数。该总结以文本格式返回,并包括以下信息:
-
模型中的层及其顺序
-
每一层的输出形状
-
每一层的参数数量(权重)
-
模型中的总参数数量(权重)
要打印模型的总结,我们只需键入以下命令:
summary(KerasMLPModel)
以下截图展示了定义的 Keras 模型架构:

在上一张截图中,我们可以清晰地看到每一层的输出形状和权重数量。
- 在训练模型之前,您需要配置学习过程,这可以通过
compile()方法来完成,如下所示:
KerasMLPModel %>% compile(
loss = 'categorical_crossentropy',
optimizer = optimizer_rmsprop(),
metrics = c('accuracy')
)
compile()方法配置 Keras 模型以进行训练。传入三个参数,如下所示:
-
loss:传入categorical_crossentropy损失函数。使用categorical_crossentropy时,您的目标应该是分类格式。我们有 10 个类别;每个样本的目标必须是一个 10 维的向量,除非对应类别的索引位置为 1,其余位置都为 0。 -
optimizer:传入optimizer_rmsprop。该优化器通过平方梯度的指数衰减平均值来调整学习率。 -
metrics:传入准确率度量。度量是用来评估模型在训练和测试过程中的表现的函数。
现在,我们可以进入训练阶段。首先,您需要设置一些参数:
BatchSize <- 128
NumEpochs <- 50
BatchSize是每次梯度更新的样本数量。NumEpochs是训练模型的轮次。一个轮次是对提供的整个输入和输出数据的一次迭代。
- 要训练模型,可以使用
fit()方法,如下所示:
ModelHistory <- KerasMLPModel %>% fit(
Xtrain, Ytrain,
batch_size = BatchSize,
epochs = NumEpochs,
verbose = 1,
validation_split = 0.3
)
传入以下参数:
-
Xtrain:这是输入训练数据的数组。 -
Ytrain:这是目标(标签)数据的数组。 -
epochs:这是训练模型的轮次。一个轮次是对提供的整个x和y数据的一次迭代。 -
batch_size:这是每次梯度更新的样本数量。 -
verbose:这是一个整数值,值可以是 0、1 或 2。详细程度模式为:0 = 静默,1 = 进度条,2 = 每个周期一行。 -
validation_split:这是一个介于 0 和 1 之间的浮动值,表示将用于验证的数据占训练数据的比例。
当使用fit()函数时,训练周期结束时会显示损失和准确度,如下截图所示:

- 为了了解损失函数和准确度在训练周期中的变化情况,可以通过如下方式绘制损失和准确度图表,展示训练和验证阶段的表现:
plot(ModelHistory)
绘制出以下图表:

我们可以看到模型在两个子集上的演化,随着损失和准确度的变化。
- 为了评估我们刚刚调整的模型的表现,我们使用
evaluate()函数,如下所示:
ScoreValues <- KerasMLPModel %>% evaluate(
Xtrain, Ytrain,
verbose = 0
)
- 该函数返回模型在测试模式下的损失值和度量值。计算是分批进行的。让我们打印出损失和准确度:
cat('Loss :', ScoreValues[[1]], '\n')
cat('Accuracy:', ScoreValues[[2]], '\n')
打印出以下结果:
Loss: 0.04588709
Accuracy: 0.99245
获得的准确度证明了深度神经网络能够对手写数字进行分类。
在接下来的部分,我们将看到如何使用深度 Q 学习来平衡一个推车。
接近深度 Q 学习
在第十一章,探索深度强化学习方法中,我们看到深度 Q 学习采用神经网络作为值函数的近似。这些方法是基本 Q 学习方法的演变,因为动作-状态表被神经网络所替代,以逼近最优值函数。深度 Q 学习只需要环境的状态作为输入,并提供所有状态-动作值,因为在环境中可以执行的动作是已知的。因此,在这个算法中,学习的过程并不是更新表格,而是调整构成网络的神经元的权重。这一更新过程通过反向传播技术完成。
首先,让我们看看如何安装我们将用作深度 Q 学习初步方法的库。
R 中的深度 Q 学习
为了在 R 中实现 DQN,我们将使用rlR库。要使用此库,必须在我们的计算机上安装 Keras,并将 TensorFlow 作为后端。此外,为了运行我们将要提出的示例,还需要安装 OpenAI Gym 库:
-
首先,我们提供安装
rlXR库的方法。该库可以在 GitHub 上找到,网址为:github.com/smilesun/rlR。 -
要安装一个在 GitHub 上提供的 R 包,我们可以使用
install_github()函数,该函数来自devtools包。 -
然后,我们首先需要安装
devtools包:
install.packages("devtools")
devtools包包含了多个用于开发 R 包的函数。通过使用这个库,许多常见任务变得更加简单。
- 在此时,我们可以使用
devtools中包含的install_github()函数,方法如下:
devtools::install_github("smilesun/rlR")
此命令将首先从 GitHub 网站下载包并安装。函数参数包含一个文本字符串,调用了作者和包名。
- 现在,我们可以加载库:
library(rlR)
作为该方法应用的示例,我们将使用一个已经在第八章中介绍的 OpenAI 库环境,游戏应用中的强化学习。我指的是CartPole-v0环境,这是一个经典的强化学习问题。
系统由一个杆子(像倒立摆一样)和通过关节连接的小车组成,如下图所示:

系统通过向小车施加+1 或-1 的力来控制。小车上施加的力量可以控制,目标是将杆子向上摆动并保持稳定。必须在小车不掉到地面的情况下完成这一过程。平衡过程包括以下动作:智能体将杆子向右或向左移动。每当杆子保持平衡时,返回一个奖励值 1。如果杆子偏离垂直位置超过 15 度,过程结束。为了平衡杆子并解决问题,需要施加与杆子倾斜方向相反的推力:
- 要使用 OpenAI Gym 库加载
CartPole环境,只需输入以下代码:
CPEnv = makeGymEnv("CartPole-v0")
- 我们检查有哪些牌号是可用的:
CPEnv
返回以下信息:
action cnt: 2
state original dim: 4
discrete action
有两个动作(action cnt: 2);这与前述内容一致。系统通过向小车施加+1 或-1 的力来控制。这是两个可用的动作。第二个信息,state original dim: 4,告诉我们系统的状态由四个信息组成,具体如下:
-
小车位置
-
小车速度
-
杆子角度
-
杆尖的速度
最后,第三个信息,discrete action,告诉我们动作空间是由离散选择定义的。这使得 DQN 成为处理此类问题的最佳解决方案。
我们初始化的环境包含多个方法。例如,我们可以使用step()方法,它执行一个动作并返回环境的状态:
CPEnv$step(1)
返回以下结果:
$state
[1] -0.02800090 -0.17157109 -0.01648416 0.27999059
$reward
[1] 1
$done
[1] FALSE
$info
named list()
返回的值具有以下含义:
-
$state:这是一个环境特定的对象,表示你对环境的观察。 -
$reward:这是前一个动作所获得的奖励值。奖励的规模因环境而异,但目标始终是增加你的总奖励。 -
$done:这表示是否需要重置环境。大多数(但不是所有)任务被划分为明确的阶段,当done为True时,表示该阶段已经结束。 -
$info:这是用于调试的诊断信息,有时也对学习很有帮助。
到此为止,我们可以基于深度 Q 学习进一步构建模型:
- 让我们初始化智能体:
CPAgent = initAgent("AgentDQN", CPEnv)
- 让我们分析一下我们实例化的对象包含了什么:
str(CPAgent)
我们获得了大量的信息,这里我们仅突出显示了其中的一部分:
Classes 'AgentDQN', 'AgentArmed', 'Agent', 'R6' <AgentDQN>
act_cnt: 2
afterEpisode: function ()
afterStep: function ()
env: EnvGym, Environment, R6 epochs: 1
gamma: 0.99 lr_decay: 0.999000499833375
mem: ReplayMemUniform, ReplayMem, R6
policy: PolicyEpsilonGreedy, Policy, R6
replay: function (batchsize)
sess: tensorflow.python.client.session.Session, tensorflow.python.client.session.BaseSession, tensorflow.python.client.session.SessionInterface, python.builtin.object
state_dim: 4
stopLearn: function ()
- 通过详细分析这些信息,我们可以得到此智能体可用的功能。在实例化智能体后,是时候开始训练它了:
CPAgent$learn(500L)
- 下一步是训练智能体进行 500 轮。每轮结束时,以下信息将被打印出来:
Episode: 472 finished with steps:200, rewards:200.000000 global step 44519
Last 100 episodes average reward 131.702970
Epsilon0.010000
rand steps:2
replaymem size GB:0.0388956144452095
learning rate: 0.000589783361647278
- 最后,我们可以打印出一张图表,展示奖励在各轮之间的变化:
CPAgent$plotPerf(F)
以下图表将被打印出来:

我们可以看到,随着智能体的学习,从系统中获得的奖励逐渐增加,这意味着智能体正在执行最佳策略,以实现期望的结果。
总结
在这一章中,我们学习了如何使用 Keras 解决深度强化学习问题。首先,我们探讨了keras库并分析了 TensorFlow 后端。接着,我们使用 Keras 通过多层神经网络识别手写数字。通过这种方式,我们理解了 Keras 模型的结构,并通过一个实际的例子来学习。最后,我们使用rlR库,在 OpenAI Gym 库的 CartPole 环境中应用深度强化学习。
在下一章中,我们将总结本书到目前为止的内容,并讨论从此时起的下一步。我们将探讨在构建和实现机器学习模型过程中面临的实际挑战,以及其他资源和技术,帮助我们学习如何提升机器学习能力。
第十三章:接下来是什么?
在本章中,我们将总结本书至今为止的内容,并说明接下来的步骤。你将学习如何将你所掌握的技能应用于其他项目,包括在构建和部署强化学习(RL)模型以及数据科学家常用的其他技术方面的现实挑战。通过本章的学习,你将更好地理解在构建和部署 RL 模型时面临的实际挑战,并获得进一步学习 RL 技能的资源和技术。
本章结束时,我们将快速总结 RL 概念及其在现实生活中的主要应用。因此,我们将探索 RL 的下一步发展以及构建和实现机器学习模型时面临的现实世界挑战。
在本章中,我们将涵盖以下主题:
-
强化学习总结
-
探索强化学习项目
-
RL 的下一步
强化学习总结
基于机器学习(ML)的算法可以根据它们使用的训练范式进行分类。在监督学习中,有一个教师告诉系统正确的输出是什么。但这并不总是可能的。通常,我们只有定性的、二元的对错或成功/失败的信息。这些信息被称为强化信号。然而,问题在于系统没有提供如何更新代理行为的信息,也没有成本函数或梯度。在强化学习(RL)的情况下,我们希望创造能够从经验中学习的智能体。
最初,强化学习(RL)被视为监督学习,但随后它被认为是机器学习算法的第三种范式。它应用于监督学习效率低的不同场景,例如,当我们面临与环境交互的问题时。
以下流程展示了正确应用 RL 算法的步骤:
-
准备代理。
-
观察环境。
-
选择最优策略。
-
执行动作。
-
计算相应的奖励(或惩罚)。
-
开发更新策略(如有必要)。
-
重复步骤 2-5,直到代理学习到最优策略。
RL 算法通过反复试验和奖励惩罚的反馈循环工作。当我们将数据集输入算法时,它将环境视为一种游戏,每次执行一个动作时,它会被告知是否赢得了游戏。通过这种方式,它建立了对胜负的认知。
由于不良行为,会施加惩罚,从而降低错误重复的概率。在展示正确行为的情况下,会施加奖励,从而确认一个正确的策略。在确定了要达成的目标后,算法会尽力最大化因执行某一行动所获得的奖励。必须达成目标的对象被称为代理。代理必须与之互动的对象被称为环境,它对应于所有外部于代理的事物。
代理是一个自动执行操作并且不显现的软件对象。代理具有目标导向的行为,但它在一个不确定的环境中执行操作,这个环境最初并不完全已知,或者只有部分已知。代理通过与环境的互动来学习。它所做的决策可以通过代理本身进行的测量,逐步发展,在学习环境的过程中形成。
这种互动的特点是反复且多样的尝试,直到成功或代理停止尝试为止。代理-环境互动是连续的;代理选择要执行的行动,随后环境的状态发生变化,呈现一个新的情况需要应对。在强化学习中,环境向代理提供奖励。奖励的来源必须是环境,以避免在代理内形成个人强化机制,这样会妨碍学习的进行。
奖励的价值与行动对实现目标的影响成正比;因此,在正确的行动下,奖励是正的或较高的,而在错误的行动下,奖励则是负的或较低的。
在下一节中,我们将探讨使用强化学习的最著名项目。我们将看到世界上最大的公司已经投入大量资源,以挖掘这些算法所提供的潜力。
探索强化学习项目
强化学习(RL)是一种编程范式,用于处理能够学习和适应环境变化的算法。在这种编程技术的基础上,存在与环境的交互,代理根据算法的选择从外界接收刺激。正确的选择将提供奖励,而错误的选择将带来惩罚。通过最大化系统获得的奖励,能够实现最优结果。例如,计算机通过执行某个任务学习在游戏中击败对手,目标是最大化奖励。这意味着系统从之前的错误中学习,基于之前探索中获得的结果来提高性能。强化学习在日常生活中的应用已经非常广泛,其中一些已经进入日常使用。例如,强化学习是自动驾驶汽车发展的基础——事实上,它们通过传感器收集的数据学习识别周围的环境。因此,它们根据需要克服的障碍来调整自己的行为。另一个例子是用于分析网页用户的程序。它们受益于自动学习,也就是从用户浏览网站时的行为和偏好中学习。其他应用包括电商平台,如亚马逊,或者娱乐和内容访问,如 Netflix 或 Spotify。在接下来的部分中,我们将探讨一些基于强化学习技术应用的实际例子。
Facebook ReAgent
推荐系统应用于不同的领域,但其目标是独特的:帮助人们根据不同的方面做出选择。根据个人的不同,这些方面可以是例如他们的时间轴、他们已经购买的物品、他们已经给予的正面评价,或者类似人的偏好。
推荐系统是一种有用的工具,通过分析使用者的历史记录来生成物品推荐。目前,已经有多个这些工具的应用,它们仍然是一个重要的研究领域,涵盖了从亚马逊的消费品推荐,到 MovieLens 的电影推荐,再到新闻或研究文章的推荐。通常,推荐系统执行三个操作:从输入数据中获取人们的偏好,计算推荐结果,并将其呈现给用户。
因此,它是一个引导用户做决策的系统。这样的系统可以被视为一个马尔可夫过程,这意味着它可以通过强化学习来建模。例如,我们可以使用上下文赌博机(contextual bandits),它在根据上下文信息选择动作时非常有效。在新闻文章推荐中,这涉及到根据用户和文章的上下文信息来为用户选择文章,比如用户的历史活动、描述性信息和内容类别。
Facebook ReAgent 是一个基于强化学习的 платформ,利用这一范式来优化数十亿人使用的产品和服务 (github.com/facebookresearch/ReAgent)。Horizon 是第一个基于开源强化学习的生产平台。Facebook 的研究人员开发了这个平台,旨在将他们在研究中取得的卓越成果,尤其是决策算法的应用,扩展到生产领域。
Unity ML-Agents
机器学习代理工具包是一个开源插件,免费提供于 GitHub,旨在为开发者提供在 Unity 引擎中使用临时环境训练虚拟实体的可能性 (github.com/Unity-Technologies/ml-agents)。
对于代理训练,可以通过一个易于使用的 Python API 使用强化学习、模仿学习、神经进化以及其他机器学习方法。此外,基于 TensorFlow 的算法实现也可用于轻松训练适用于 2D、3D 和 VR/AR 游戏的智能代理。ML-Agents 工具包可以高效地开发应用程序——实际上,进展可以在 Unity 丰富的环境中得到评估。
Google DeepMind
DeepMind 是一家英国的人工智能公司,成立于 2010 年,最初名为 DeepMind Technologies,后来在 2014 年被 Google 收购。DeepMind 是 Google 已经传奇的人工智能部门,创造了 AlphaGo,这个 AI 打败了世界上最强的围棋选手。
因此,DeepMind 所做的选择并不是基于外部指示或写在程序源代码行中的指令。Google 的超级计算机可以从多个可能性中选择一个选项,这个选项基于所获得的经验和来自外部环境的信息。所有这些都得益于 差分神经计算机 (DNC),一种模仿人类记忆功能的计算机系统。
使用 IBM Watson 管理大量数据
Watson 是 IBM 集团的一个项目,旨在创建一个能够处理大量非结构化数据的人工智能系统,将这些资源转化为结构化信息,进而做出决策。Watson 最初作为一个平台诞生,用户可以通过它将自己的创意付诸实践。数据的复杂性和异质性,尤其是许多非结构化数据,是需要克服的最大障碍。尽管经典分析方法有所改进,从分散的数据中构建有用信息仍然只是部分实现。数据以无缝方式生成,每天产生 25 亿 GB 的数据。因此,必须将数据视为一种新的自然资源,从中可以获取竞争优势。
到目前为止,我们已经分析了众多算法,并学习了如何在不同领域应用强化学习。那么,全球研究人员面临的未来挑战是什么?
强化学习的下一步
现代技术使我们习惯于看到机器代替人类执行任务。在生产过程的自动化中,快速且精准的计算以及执行指令时的最小误差使得机器具有竞争力,甚至可能比人类更具竞争力。简而言之,越来越多的算法正在被开发出来,帮助软件从经验中学习。这就是机器学习。
基于这些算法,不再需要预定义的规则来与机器一起学习,但它们通过模型和指令做出决策,通过这些我们可以学习解决当前问题的正确规则。正如我们在上一节中看到的,这些技术已经在现实生活中得到了广泛应用。例子包括反垃圾邮件斗争、信用卡欺诈识别、语音识别与手写识别、经济与金融预测、自动分类图像等等。
这项技术未来将为我们提供什么?让我们来看一看使用强化学习算法时未来的挑战是什么。
理解逆向强化学习
在强化学习(RL)算法中,智能体在执行某个动作后会立即收到强化信号。有几个领域中很难估计强化函数。逆强化学习(IRL)允许你通过一组示范重建一个定义智能体行为的强化函数。在学习逆强化的过程中,奖励函数来自于观察到的行为。通常,在强化学习中,我们使用奖励来学习系统的行为;而在逆强化学习中,这个过程是反向的;实际上,智能体观察系统的行为,进而理解目标正在尝试实现什么。为了做到这一点,必须拥有环境的状态,以便学习每个强化函数的最优策略。此外,还需要定义一组特征,进行组合,以识别单一的强化函数。
在逆强化学习(IRL)中,问题从以下内容开始:
-
对智能体行为随时间的测量
-
对该智能体的感官输入进行测量
-
物理环境的模型
基于这些数据,我们可以确定智能体正在优化的奖励函数。
就策略而言,解决逆强化学习问题意味着将专家的行为编码为参考策略,然后识别一个奖励函数,以使该策略成为最优策略。这种方法的目标是识别奖励函数,它可以比仅仅构建一个策略使智能体模仿专家的方式提供更好的结果。这是因为奖励函数不仅描述了智能体的行为,还更深入地编码了动机。
这种表示比基于策略的表示更加简洁,并且允许智能体的行为被泛化,甚至扩展到专家尚未探索的状态空间区域。通过了解奖励函数,还可以通过重新生成一个新的最优策略,从奖励函数本身开始来应对模型的变化,而不必完全重新学习。最后,有一些情境,其中奖励函数本身就是研究的目标。
另一个与策略算法相关的挑战是深度确定性策略梯度。我们来看看如何解决这个问题。
深度确定性策略梯度
深度确定性策略梯度(DDPG)是策略梯度算法家族的一部分,它在探索阶段使用随机行为策略。DDPG 算法评估的是一个确定性的目标策略,这个策略更容易学习。在这些策略算法中,迭代的特点如下:
-
策略评估
-
遵循策略梯度以最大化性能
DDPG 代表了一种基于策略外算法,它使用确定性目标策略。在这些条件下,允许使用确定性政策的梯度定理。DDPG 也是一种演员-评论员算法的例子。从这个意义上说,它主要使用两个人工神经网络:一个用于演员,一个用于评论员。两个网络都计算当前状态的行动预测,并每次生成一个时差 (TD) 错误信号。演员网络的输入使用当前状态,输出是由连续动作空间选择的动作。评论员的输出只是当前状态和由演员提供的动作的估计 Q 值。确定性策略梯度定理提供了演员网络权重更新规则。评论员网络则通过从 TD 错误信号获得的梯度进行更新。
到目前为止,我们已经看到智能体如何与环境互动,但当它与人类互动时会发生什么呢?
来自人类偏好的强化学习
在许多情况下,无法定义一个明确的奖励函数。许多现实问题的特点是目标复杂且定义不清或难以指定。例如,假设你想用强化学习训练一个机器人选择最佳路径以到达目标。奖励函数并不容易定义,它必须依赖于来自机器人传感器的数据。如果我们能够成功地将我们的真实目标传达给智能体,我们在解决这些问题时将会取得巨大的成果。
如果我们有期望任务的示例,我们可以使用逆向强化学习提取奖励函数。之后,我们可以通过模仿学习来克隆已经验证的行为。不幸的是,这些方法并不直接适用于许多现实问题。解决这个问题的一个可能途径是来自人类反馈,定义活动的过程。在这种情况下,我们进入了强化学习的范式,尽管将人类反馈直接作为奖励函数使用对那些需要数百或数千小时经验的强化学习系统来说是不可行的。
在基于人类偏好的深度强化学习中,智能体从人类反馈中学习奖励函数,从而优化这个奖励函数。这为没有明确奖励函数的序列决策问题提供了解决方案。
该算法具有以下特点:
-
它使我们能够解决那些我们只能识别出期望行为,但不一定能够证明的任务。
-
它允许智能体接受非专家用户的指导。
-
它是一个大规模问题。
-
它节省了用户反馈。
该算法将奖励函数调整为符合人类偏好的同时,形成一个策略来优化当前期望的奖励函数。有些情况下,环境的探索非常复杂,因此我们必须采用不同的方法。让我们看看可以做些什么。
后视经验回放
人类的一个特点是从错误中学习并适应,以避免重复犯同样的错误。这正是强化学习(RL)算法的秘密。当遇到分散奖励时,实施这些算法时会遇到问题。我们来分析以下场景:一个智能体必须控制一个机器人臂打开一个盒子并将物体放入其中。这个任务的奖励很容易定义;相反,学习问题却很难实现。智能体必须探索一系列正确的动作,以识别返回分散奖励的环境配置:在这种情况下,物体在盒子内的位置。找到这个奖励信号是一个复杂的探索问题,几乎无法通过随机探索解决。
为了解决这类问题,可以采用一种名为后视经验回放(Hindsight Experience Replay)的新技术。该技术通过省略其他方法中使用的复杂奖励技术,采用基于稀缺和二元奖励的高效学习。它可以与任何非策略强化学习算法结合使用。
这种方法适用于处理机器人臂操作物体,以及推、滑、拾取和放置任务。只使用二元奖励,指示活动是否已完成。最近的研究表明,后视经验回放在这些困难环境中使训练成为可能。其他研究表明,已在物理仿真中训练的策略可以在物理机器人上实施,并成功完成任务。
总结
在本章中,我们总结了基于强化学习算法的基本要素。然后,我们探索了强化学习技术的一些实际应用示例。最后,我们讨论了使用强化学习算法的未来挑战。我们探讨了逆向强化学习(IRL)、深度确定性策略梯度(DDPG)、基于人类偏好的强化学习和后视经验回放。
这是本书的最后一章,到此为止,我们已经探索了基于强化学习算法的不同场景。首先,我必须祝贺你已经完成了本书的学习;你已完成了一项艰难的挑战。我还建议你将所学技能立即应用于实际问题。


是最优的预期回报。
是网络的估计值。
浙公网安备 33010602011771号