Python-强化学习实用指南-全-
Python 强化学习实用指南(全)
零、前言
强化学习是机器学习的一种自我发展的类型,它使我们更接近实现真正的人工智能。 这份易于遵循的指南使用 Python 编写的丰富示例从头开始解释了所有内容。
这本书是给谁的
本书适用于对人工智能感兴趣并希望从头开始学习强化学习的机器学习开发人员和深度学习爱好者。 阅读本书,并通过在工作中或项目中实现实际示例,成为强化学习专家。 掌握线性代数,微积分和 Python 编程语言的知识将帮助您理解本书的流程。
本书涵盖的内容
第 1 章,“强化学习简介”帮助我们了解强化学习是什么以及它如何工作。 我们将学习强化学习的各种特征,例如智能体,环境,策略和模型,并且还将看到用于强化学习的不同类型的环境,平台和库。 在本章的后面,我们将看到强化学习的一些应用。
第 2 章和 “OpenAI 和 TensorFlow 入门”可帮助我们为各种强化学习任务设置机器。 我们将学习如何通过安装 Anaconda,Docker,OpenAI Gym,Universe 和 TensorFlow 来设置机器。 然后,我们将学习如何在 OpenAI Gym 中模拟智能体,并且将了解如何构建视频游戏机器人。 我们还将学习 TensorFlow 的基础知识,并了解如何使用 TensorBoard 进行可视化。
第 3 章,“马尔可夫决策过程和动态规划”首先说明什么是马尔可夫链和马尔可夫过程,然后我们将了解如何将强化学习问题建模为马尔可夫决策过程。 我们还将学习一些基本概念,例如值函数,Q 函数和贝尔曼方程。 然后,我们将了解什么是动态规划以及如何使用值和策略迭代来解决冻湖问题。
第 4 章,“使用蒙特卡洛方法进行的游戏”解释了蒙特卡洛方法和不同类型的蒙特卡洛预测方法,例如首次访问 MC 和每次访问 MC。 我们还将学习如何使用蒙特卡洛方法玩二十一点。 然后,我们将探讨不同的策略上和策略外的蒙特卡洛控制方法。
第 5 章,“时间差异学习”涵盖了时差(TD)学习,TD 预测以及 TD 脱离策略和基于策略的控制方法,例如 Q 学习和 SARSA。 我们还将学习如何使用 Q 学习和 SARSA 解决出租车问题。
第 6 章,“多臂老丨虎丨机问题”处理强化学习的经典问题之一,即多臂老丨虎丨机(MAB)或 k 臂老丨虎丨机问题。 我们将学习如何使用各种探索策略(例如 epsilon-greedy,softmax 探索,UCB 和 Thompson 采样)解决此问题。 在本章的后面,我们将看到如何使用 MAB 向用户显示正确的广告横幅。
第 7 章,“深度学习基础知识”涵盖了深度学习的各种基本概念。 首先,我们将学习神经网络是什么,然后我们将看到不同类型的神经网络,例如 RNN,LSTM 和 CNN。 我们将通过构建一些应用来学习,这些应用执行诸如生成歌曲歌词和对时尚产品进行分类等任务。
第 8 章,“使用深度 Q 网络玩 Atari 游戏”涵盖了最广泛使用的深度强化学习算法之一,称为深度 Q 网络(DQN)。 我们将通过探索 DQN 的各个组成部分来学习 DQN,然后我们将了解如何构建一个智能体来使用 DQN 玩 Atari 游戏。 然后,我们将研究 DQN 架构的一些升级,例如双 DQN 和决斗 DQN。
第 9 章,“使用深度循环 Q 网络玩末日游戏”,介绍了深度循环 Q 网络(DRQN)及其与 DQN 的区别。 我们将看到如何构建使用 DRQN 播放 Doom 的智能体。 在本章的后面,我们将学习深度注意力循环 Q 网络,该网络将注意力机制添加到 DRQN 架构中。
第 10 章,“异步优势演员评论家网络”解释了异步优势演员评论家(A3C)网络的工作方式。 我们将详细探讨 A3C 架构,然后将学习如何构建使用 A3C 上山的智能体。
第 11 章,“策略梯度和优化”涵盖了策略梯度如何帮助我们找到合适的策略而无需 Q 函数。 我们还将探索深度确定性策略梯度方法。 在本章的后面,我们将看到最新的策略优化方法,例如信任区域策略优化和近端策略优化。
第 12 章,“Capstone 项目–使用 DQN 进行赛车”提供了逐步的方法,以使用决斗 DQN 构建智能体以赢得赛车游戏。
第 13 章,“最近的进展和后续步骤”提供了有关强化学习的各种进展的信息,例如想象力增强智能体,从人的偏好中学习,从示威活动中进行深度学习以及事后 ,然后我们将研究不同类型的强化学习方法,例如分层强化学习和逆强化学习。
充分利用这本书
本书需要以下软件:
- Anaconda
- Python
- 任何网络浏览器
- Docker
下载示例代码文件
您可以从 www.packtpub.com 的帐户中下载本书的示例代码文件。 如果您在其他地方购买了此书,则可以访问 www.packtpub.com/support 并注册以将文件直接通过电子邮件发送给您。
您可以按照以下步骤下载代码文件:
- 登录或登录 www.packtpub.com 。
- 选择支持选项卡。
- 单击代码下载和勘误。
- 在搜索框中输入书籍的名称,然后按照屏幕上的说明进行操作。
下载文件后,请确保使用以下最新版本解压缩或解压缩文件夹:
- Windows 的 WinRAR/7-Zip
- Mac 版 Zipeg/iZip/UnRarX
- 适用于 Linux 的 7-Zip/PeaZip
本书的代码包也托管在 GitHub 上。 如果代码有更新,它将在现有的 GitHub 存储库中进行更新。
我们还从这里提供了丰富的书籍和视频目录中的其他代码包。 去看一下!
下载彩色图像
我们还提供了 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。 您可以在此处下载。
使用约定
本书中使用了许多文本约定。
CodeInText:指示文本,数据库表名称,文件夹名称,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄中的代码字。 这是一个示例:“将下载的WebStorm-10*.dmg磁盘镜像文件安装为系统中的另一个磁盘。”
代码块设置如下:
policy_iteration():
Initialize random policy
for i in no_of_iterations:
Q_value = value_function(random_policy)
new_policy = Maximum state action pair from Q value
任何命令行输入或输出的编写方式如下:
bash Anaconda3-5.0.1-Linux-x86_64.sh
粗体:表示新术语,重要单词或您在屏幕上看到的单词。 例如,菜单或对话框中的单词会出现在这样的文本中。
警告或重要提示如下所示。
提示和技巧如下所示。
一、强化学习导论
强化学习(RL)是机器学习的一个分支,其中学习是通过与环境交互来进行的。 这是面向目标的学习,不教导学习器采取什么行动; 相反,学习器从其行动的结果中学习。 随着各种算法的迅速发展,它是人工智能(AI)中最活跃的研究领域之一。
在本章中,您将了解以下内容:
- RL 的基本概念
- RL 算法
- 智能体环境接口
- RL 环境的类型
- RL 平台
- RL 的应用
什么是 RL?
考虑到您正在教狗接球,但是您不能明确地教狗接球; 取而代之的是,您将只扔一个球,每次狗抓到球时,您都将给它一个曲奇。 如果无法接住球,则不会提供曲奇。 狗将找出使它收到 Cookie 的动作,然后重复这些动作。
同样,在 RL 环境中,您不会教智能体做什么或如何做,而是会针对智能体执行的每个操作给予奖励。 奖励可以是正面的或负面的。 然后,智能体将开始执行使其获得正面奖励的操作。 因此,这是一个反复试验的过程。 在先前的类比中,狗代表智能体。 接球时给狗送饼干是一种积极的奖励,而不送饼干则是一种消极的奖励。
奖励可能会延迟。 您可能无法在每一步都获得奖励。 只有在任务完成后才能给予奖励。 在某些情况下,您会在每一步中获得奖励,以查明您是否犯了任何错误。
想象一下,您想教一个机器人走路而不会撞上山而被卡住,但是您不会明确地教它不要朝着山的方向走:

相反,如果机器人撞到并卡在山上,您将获得 10 分,这样机器人就会知道撞上山会产生负奖励,并且不会再次朝该方向前进:

如果机器人在正确的方向上行走而不被卡住,您将获得 20 分。 因此,机器人将了解哪条道路是正确的,并会通过朝正确的方向努力来最大化回报:

RL 智能体可以探索可能提供良好奖励的不同动作,也可以利用(使用)导致良好奖励的先前动作。 如果 RL 智能体探索不同的行动,则该智能体很可能会收到较差的报酬,因为所有行动都不会成为最佳行动。 如果 RL 智能体仅利用已知的最佳动作,那么也有可能会错过最佳动作,这可能会提供更好的回报。 探索与利用之间总是要权衡取舍。 我们不能同时进行探索和利用。 在接下来的章节中,我们将详细讨论探索与利用难题。
RL 算法
典型的 RL 算法涉及的步骤如下:
- 首先,智能体通过执行操作与环境进行交互
- 智能体执行动作并从一种状态转移到另一种状态
- 然后,智能体将根据其执行的动作获得奖励
- 根据奖励,智能体将了解该操作是好是坏
- 如果该动作是好的,即如果该智能体收到了积极的报酬,则该智能体将更喜欢执行该动作,否则该智能体将尝试执行导致积极报酬的其他动作。 所以这基本上是一个反复试验的学习过程
RL 与其他 ML 范式有何不同
在监督学习中,机器(智能体)从训练数据中学习,该训练数据具有一组标记的输入和输出。 目的是模型外推并概括其学习,以便可以很好地应用于看不见的数据。 有一个外部主管,他具有完整的环境知识基础并监督智能体以完成一项任务。
考虑一下我们刚才讨论的狗类比; 在监督学习中,要教狗接球,我们将通过指定向左转,向右走,前进五个步骤,接球等等来明确地教它。 但是在 RL 中,我们只是扔一个球,每当狗抓到球时,我们都会给它一个曲奇(奖励)。 因此,狗将学会接球,这意味着它收到了饼干。
在无监督学习中,我们为模型提供只有一组输入的训练数据。 模型学习确定输入中的隐藏模式。 有一个普遍的误解,认为 RL 是一种无监督的学习,但事实并非如此。 在无监督学习中,模型学习隐藏的结构,而在 RL 中,模型通过最大化奖励来学习。 假设我们想向用户推荐新电影。 无监督学习会分析用户观看过的相似电影并建议电影,而 RL 会不断从用户那里收到反馈,了解他的电影偏好,并在此基础上建立知识库并建议新电影。
还有另一种称为半监督学习的学习,它基本上是有监督学习和无监督学习的结合。 它涉及对标记和未标记数据的函数估计,而 RL 本质上是智能体与其环境之间的相互作用。 因此,RL 与所有其他机器学习范例完全不同。
RL 的元素
RL 的元素在以下各节中显示。
智能体
智能体是做出明智决策的软件程序,它们基本上是 RL 的学习器。 智能体通过与环境互动来采取行动,他们会根据自己的行动获得奖励,例如,在视频游戏中导航的超级马里奥。
策略函数
策略定义环境中智能体的行为。 智能体决定执行哪种操作的方式取决于策略。 假设您想在家中到达办公室; 到达办公室的路线会有所不同,有些路线是捷径,有些路线很长。 这些路线被称为策略,因为它们代表了我们选择采取行动以达到目标的方式。 策略通常用符号π表示。 策略可以采用查找表或复杂的搜索过程的形式。
值函数
值函数表示智能体处于特定状态的程度如何。 它取决于策略,通常用v(s)表示。 它等于智能体从初始状态开始收到的总预期奖励。 可以有多个值函数。 最优值函数是与其他值函数相比在所有状态下具有最高值的函数。 同样,最优策略是具有最优值函数的策略。
模型
模型是智能体程序对环境的表示。 学习可以分为两种类型:基于模型的学习和无模型的学习。 在基于模型的学习中,智能体利用先前学习的信息来完成任务,而在无模型的学习中,智能体仅依靠反复试验的经验来执行正确的操作。 假设您想在家中更快地到达办公室。 在基于模型的学习中,您只需使用以前学习的经验(地图)即可更快地到达办公室,而在无模型的学习中,您将不会使用以前的经验,而是尝试所有不同的路线并选择更快的路线。
智能体环境接口
智能体是指一次执行Aₜ,t移动的动作的软件智能体。 从一种状态Sₜ,到另一种状态S[t + 1]。 基于行为,智能体从环境中获得数值奖励R。 最终,RL 就是寻找可以增加数值奖励的最佳行动:

让我们通过迷宫游戏来理解 RL 的概念:

迷宫的目的是到达目的地而不会卡在障碍物上。 这是工作流程:
- 智能体是穿越迷宫的人,这是我们的软件程序/ RL 算法
- 环境是迷宫
- 状态是智能体当前位于迷宫中的位置
- 智能体通过从一种状态转移到另一种状态来执行动作
- 当智能体的动作没有遇到任何障碍时,它会获得正向奖励;当智能体的行为没有遇到任何障碍时,它会得到负向奖励,因此它无法到达目的地
- 目的是探查迷宫并到达目的地
RL 环境的类型
智能体与之交互的所有事物都称为环境。 环境是外部世界。 它包含智能体之外的所有内容。 有不同类型的环境,将在下一节中介绍。
确定性环境
当我们基于当前状态知道结果时,就可以说环境是确定性的。 例如,在国际象棋游戏中,我们知道移动任何玩家的确切结果。
随机环境
当我们无法根据当前状态确定结果时,就说环境是随机的。 不确定性将更大。 例如,我们永远不知道掷骰子时会显示多少数字。
完全可观察的环境
当智能体可以始终确定系统状态时,称为完全可观察。 例如,在国际象棋游戏中,系统状态,即所有玩家在国际象棋棋盘上的位置始终可用,因此玩家可以做出最佳决策。
部分可观察的环境
当智能体无法始终确定系统状态时,称为部分可观察。 例如,在扑克游戏中,我们不知道对手拥有的牌。
离散环境
当只有有限状态的动作可用于从一种状态转移到另一种状态时,它称为离散环境。 例如,在国际象棋游戏中,我们只有一组有限的动作。
连续环境
当动作的无限状态可以从一种状态转移到另一种状态时,称为连续环境。 例如,我们有多条路线可用于从源头到目的地的旅行。
情景和非情景环境
情景环境也称为非顺序环境。 在情景环境中,主体的当前动作不会影响将来的动作,而在非周期性环境中,主体的当前动作会影响未来的动作,也称为顺序环境。 也就是说,主体在剧集环境中执行独立的任务,而在非周期性环境中,所有主体的动作都是相关的。
单智能体和多智能体环境
顾名思义,单智能体环境只有一个智能体,多智能体环境有多个智能体。 在执行复杂任务时,广泛使用多智能体环境。 在完全不同的环境中将存在不同的智能体。 不同环境中的智能体将彼此通信。 由于多主体环境具有更大的不确定性,因此它几乎是随机的。
RL 平台
RL 平台用于在环境中模拟,构建,渲染和试验我们的 RL 算法。 有很多可用的 RL 平台,如以下各节所述。
OpenAI Gym 和 Universe
OpenAI Gym 是用于构建,评估和比较 RL 算法的工具包。 它与在 TensorFlow,Theano,Keras 等任何框架中编写的算法兼容。 它很容易理解。 它不对智能体的结构做任何假设,并提供了所有 RL 任务的接口。
OpenAI Universe 是 OpenAI Gym 的扩展。 它提供了在各种简单到实时复杂环境中训练和评估智能体的能力。 它可以无限制地访问许多游戏环境。 使用 Universe,可以通过在虚拟网络计算远程桌面后自动启动程序来将任何程序转换为 Gym 环境,而无需访问程序内部,源代码或 API,因为 Universe 可以工作。
DeepMind Lab
DeepMind Lab 是另一个基于 AI 智能体的惊人研究平台。 它提供了一个丰富的模拟环境,可以作为运行几种 RL 算法的实验室。 它是高度可定制和可扩展的。 视觉效果非常丰富,科幻风格且逼真。
RLGlue
RL-Glue 提供了一个接口,用于将智能体,环境和程序连接在一起,即使它们是用不同的编程语言编写的。 它具有与他人共享您的智能体和环境以在您的工作之上进行构建的能力。 由于这种兼容性,可重用性大大提高了。
Malmo 计划
Malmo 计划是微软在 Minecraft 之上构建的另一个 AI 实验平台。 它为自定义环境提供了良好的灵活性。 它与复杂的环境集成在一起。 它还允许超频,这使程序员能够比标准 Minecraft 更快地播放场景。 但是,与 Open AI Universe 不同,Malmo 当前仅提供 Minecraft 游戏环境。
ViZDoom
顾名思义,ViZDoom 是一个基于 Doom 的 AI 平台。 它为多智能体提供支持,并提供竞争环境来测试智能体。 但是,ViZDoom 仅支持 Doom 游戏环境。 它提供了屏幕外渲染以及单人和多人游戏支持。
RL 的应用
凭借更大的进步和研究,RL 已在从玩计算机游戏到汽车自动化的多个领域迅速发展了日常应用。 以下各节列出了一些 RL 应用。
教育
许多在线教育平台都在使用 RL 为每个学生提供个性化的内容。 有些学生可能会从视频内容中学习得更好,一些学生可能会通过做项目来学习,而有些人可能会从笔记中学习。 RL 用于根据每个学生的学习风格来调整个性化的教育内容,并且可以根据用户的行为进行动态更改。
医药保健
RL 在医学和卫生保健方面有无数的应用; 其中一些包括个性化医疗,基于医学图像的诊断,获得临床决策中的治疗策略,医学图像分割等。
制造业
在制造中,智能机器人用于将物体放置在正确的位置。 如果它失败或无法成功将对象放置在正确的位置,它将记住该对象并训练自己以更高的精度执行此操作。 使用智能智能体将减少人工成本并提高表现。
库存管理
RL 广泛用于库存管理,这是一项至关重要的业务活动。 其中一些活动包括供应链管理,需求预测和处理多个仓库操作(例如将产品放置在仓库中以有效管理空间)。 DeepMind 的 Google 研究人员开发了 RL 算法,可有效降低其数据中心的能耗。
金融
RL 被广泛用于金融投资组合管理,这是将资金不断重新分配给不同金融产品的过程,也用于商业交易市场的预测和交易。 JP Morgan 已成功使用 RL 为大订单提供更好的交易执行结果。
自然语言处理与计算机视觉
凭借深度学习和 RL 的统一能力,深度强化学习(DRL)在自然语言处理(NLP)和计算机视觉(CV)领域中得到了极大的发展。。 DRL 已用于文本摘要,信息提取,机器翻译和图像识别,比当前系统具有更高的准确率。
总结
在本章中,我们学习了 RL 的基础知识以及一些关键概念。 我们了解了 RL 的不同元素和 RL 环境的不同类型。 我们还介绍了各种可用的 RL 平台以及 RL 在各个领域中的应用。
在下一章第 2 章,“OpenAI 和 TensorFlow 入门”中,我们将学习 OpenAI 和 TensorFlow 的基础知识以及如何安装 OpenAI 和 TensorFlow,然后模拟环境并教给智能体如何在环境中学习。
问题
问题列表如下:
- 什么是强化学习?
- RL 与其他 ML 范式有何不同?
- 什么是智能体,智能体如何学习?
- 策略函数和值函数有什么区别?
- 基于模型的学习与基于模型的学习有什么区别?
- RL 中有哪些不同类型的环境?
- OpenAI Universe 与其他 RL 平台有何不同?
- RL 有哪些应用?
进一步阅读
二、OpenAI 和 TensorFlow 入门
OpenAI 是由 Elon Musk 和 Sam Altman 创立的非营利性,开源人工智能(AI)研究公司,旨在构建通用 AI。 他们是由顶级行业领导者和顶尖公司赞助的。 OpenAI 具有 Gym 和 Universe 两种样式,我们可以使用它们模拟现实环境,构建强化学习(RL)算法,并在这些环境中测试我们的智能体。 TensorFlow 是 Google 的开源机器学习库,已广泛用于数值计算。 在接下来的章节中,我们将使用 OpenAI 和 TensorFlow 来构建和评估强大的 RL 算法。
在本章中,您将了解以下内容:
- 通过安装 Anaconda,Docker,OpenAI Gym 和 Universe 和 TensorFlow 设置机器
- 使用 OpenAI Gym 和 Universe 模拟环境
- 训练机器人走路
- 构建一个视频游戏机器人
- TensorFlow 的基本原理
- 使用 TensorBoard
配置机器
安装 OpenAI 并不是一件容易的事。 要设置和运行系统,必须正确遵循一组步骤。 现在,让我们看看如何设置我们的机器并安装 OpenAI Gym 和 Universe。
安装 Anaconda
本书中的所有示例均使用 Anaconda 版本的 Python。 Anaconda 是 Python 的开源发行版。 它被广泛用于科学计算和处理大量数据。 它提供了一个出色的包管理环境。 它提供对 Windows,macOS 和 Linux 的支持。 Anaconda 随附安装了 Python 以及用于科学计算的流行包,例如 NumPy,SciPy 等。
要下载 Anaconda,请访问这里,您将在此处看到用于下载用于不同平台的 Anaconda 的选项。
如果使用 Windows 或 Mac,则可以根据计算机架构直接下载图形安装程序,然后使用图形安装程序进行安装。
如果您使用的是 Linux,请按照以下步骤操作:
- 打开终端并输入以下内容以下载 Anaconda:
wget https://repo.continuum.io/archive/Anaconda3-5.0.1-Linux-x86_64.sh
- 完成后,我们可以通过以下命令安装 Anaconda:
bash Anaconda3-5.0.1-Linux-x86_64.sh
成功安装 Anaconda 后,我们需要创建一个新的 Anaconda 环境,该环境基本上是一个虚拟环境。 虚拟环境有什么需求? 假设您正在使用 Aum 版本 1.14 的项目 A 和使用 NumPy 版本 1.13 的项目 B。 因此,要进行项目 B,您可以降级 NumPy 或重新安装 Anaconda。 在每个项目中,我们使用具有不同版本的不同库,这些库不适用于其他项目。 我们使用虚拟环境来代替降级或升级版本或为新项目每次重新安装 Anaconda。 这为当前项目创建了一个隔离的环境,因此每个项目可以具有自己的依赖,并且不会影响其他项目。 我们将使用以下命令创建这样的环境,并将我们的环境命名为universe:
conda create --name universe python=3.6 anaconda
我们可以使用以下命令激活环境:
source activate universe
安装 Docker
安装 Anaconda 之后,我们需要安装 Docker。 Docker 使将应用部署到生产变得容易。 假设您在具有 TensorFlow 和其他一些库的localhost中构建了一个应用,并且要将应用部署到服务器中。 您将需要在服务器上安装所有这些依赖项。 但是使用 Docker,我们可以将应用及其依赖项打包在一起,这称为容器,并且我们可以在服务器上运行应用而无需在打包的 Docker 容器中使用任何外部依赖项。 OpenAI 不支持 Windows,因此要在 Windows 中安装 OpenAI,我们需要使用 Docker。 而且,大多数 OpenAI Universe 环境都需要 Docker 来模拟环境。 现在让我们看看如何安装 Docker。
要下载 Docker,请转至这里,您将在其中看到一个名为“Get Docker”的选项。 如果选择该选项,则将看到不同操作系统的选项。 如果使用 Windows 或 Mac,则可以下载 Docker 并直接使用图形安装程序进行安装。
如果您使用的是 Linux,请按照以下步骤操作:
打开您的终端并输入以下内容:
sudo apt-get install \
apt-transport-https \
ca-certificates \
curl \
software-properties-common
然后输入:
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
然后输入:
sudo add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) \
stable"
最后,输入:
sudo apt-get update
sudo apt-get install docker-ce
我们需要成为 Docker 用户组的成员才能开始使用 Docker。 您可以通过以下命令加入 Docker 用户组:
sudo adduser $(whoami) docker
newgrp docker
groups
我们可以通过运行内置的hello-world程序来测试 Docker 的安装:
sudo service docker start
sudo docker run hello-world
为了避免每次使用sudo来使用 Docker,我们可以使用以下命令:
sudo groupadd docker
sudo usermod -aG docker $USER
sudo reboot
安装 OpenAI Gym 和 Universe
现在让我们看看如何安装 OpenAI Gym 和 Universe。 在此之前,我们需要安装几个依赖项。 首先,让我们使用以下命令激活刚刚创建的conda环境:
source activate universe
然后,我们将安装以下依赖项:
sudo apt-get update
sudo apt-get install golang libcupti-dev libjpeg-turbo8-dev make tmux htop chromium-browser git cmake zlib1g-dev libjpeg-dev xvfb libav-tools xorg-dev python-opengl libboost-all-dev libsdl2-dev swig
conda install pip six libgcc swig
conda install opencv
在本书中,我们将使用gym版本0.7.0,因此您可以使用pip直接安装gym,如下所示:
pip install gym==0.7.0
或者,您可以克隆gym存储库并通过以下命令安装最新版本:
cd ~
git clone https://github.com/openai/gym.git
cd gym
pip install -e '.[all]'
前面的命令将获取gym存储库并以包的形式安装gym,如以下屏幕截图所示:

常见错误修复
在安装 Gym 时,您很可能会遇到以下任何错误。 如果出现这些错误,只需运行以下命令并尝试重新安装:
Failed building wheel for pachi-py或Failed building wheel for pachi-py atari-py:
sudo apt-get update
sudo apt-get install xvfb libav-tools xorg-dev libsdl2-dev swig cmake
Failed building wheel for mujoco-py:
git clone https://github.com/openai/mujoco-py.git
cd mujoco-py
sudo apt-get update
sudo apt-get install libgl1-mesa-dev libgl1-mesa-glx libosmesa6-dev python3-pip python3-numpy python3-scipy
pip3 install -r requirements.txt
sudo python3 setup.py install
Error: command 'gcc' failed with exit status 1:
sudo apt-get update
sudo apt-get install python-dev
sudo apt-get install libevent-dev
同样,我们可以通过获取universe存储库并将universe作为包安装来安装 OpenAI Universe:
cd ~
git clone https://github.com/openai/universe.git
cd universe
pip install -e .
以下屏幕快照显示了安装:

如前所述,Open AI Universe 需要 Docker,因为大多数 Universe 环境都在 Docker 容器中运行。
因此,让我们构建一个 Docker 镜像并将其命名为universe:
docker build -t universe .
构建 Docker 镜像后,我们运行以下命令,该命令从 Docker 镜像启动容器:
docker run --privileged --rm -it -p 12345:12345 -p 5900:5900 -e DOCKER_NET_HOST=172.17.0.1 universe /bin/bash
OpenAI Gym
借助 OpenAI Gym,我们可以模拟各种环境,并开发,评估和比较 RL 算法。 现在让我们了解如何使用 Gym。
基本模拟
让我们看看如何模拟基本的购物车杆环境:
- 首先,让我们导入库:
import gym
- 下一步是使用
make函数创建一个仿真实例:
env = gym.make('CartPole-v0')
- 然后,我们应该使用
reset方法初始化环境:
env.reset()
- 然后,我们可以循环执行一些时间步骤,并在每个步骤渲染环境:
for _ in range(1000):
env.render()
env.step(env.action_space.sample())
完整的代码如下:
import gym
env = gym.make('CartPole-v0')
env.reset()
for _ in range(1000):
env.render()
env.step(env.action_space.sample())
如果运行前面的程序,则可以看到输出,该输出显示了购物车杆环境:

OpenAI Gym 提供了许多模拟环境,用于训练,评估和构建我们的智能体。 我们可以通过检查其网站或输入以下内容来检查可用的环境,这些将列出可用的环境:
from gym import envs
print(envs.registry.all())
由于 Gym 提供了不同的有趣环境,因此让我们模拟一个赛车环境,如下所示:
import gym
env = gym.make('CarRacing-v0')
env.reset()
for _ in range(1000):
env.render()
env.step(env.action_space.sample())
您将获得如下输出:

训练机器人走路
现在,让我们学习如何使用 Gym 训练机器人走路以及一些基础知识。
该策略是当机器人向前移动时将获得X点作为奖励,如果机器人无法移动,则会减少Y点。 因此,机器人将在最大化奖励的情况下学习行走。
首先,我们将导入库,然后通过make函数创建一个仿真实例。 Open AI Gym 提供了一个称为BipedalWalker-v2的环境,用于在简单的地形中训练机器人智能体:
import gym
env = gym.make('BipedalWalker-v2')
然后,对于每个剧集(初始状态和最终状态之间的智能体程序-环境交互),我们将使用reset方法初始化环境:
for episode in range(100):
observation = env.reset()
然后,我们将循环并渲染环境:
for i in range(10000):
env.render()
我们从环境的行动空间中抽样随机行动。 每个环境都有一个动作空间,其中包含所有可能的有效动作:
action = env.action_space.sample()
对于每个操作步骤,我们将记录observation,reward,done和info:
observation, reward, done, info = env.step(action)
observation是代表观察环境的对象。 例如,机器人在地形中的状态。
reward是上一动作获得的奖励。 例如,机器人成功前进所获得的奖励。
done是布尔值; 如果为真,则表明该剧集已经完成(也就是说,机器人学会了行走或完全失败)。 剧集完成后,我们可以使用env.reset()初始化下一个剧集的环境。
info是可用于调试的信息。
当done为true时,我们打印该剧集采取的时间步长并中断当前剧集:
if done:
print("{} timesteps taken for the Episode".format(i+1))
break
完整的代码如下:
import gym
env = gym.make('BipedalWalker-v2')
for i_episode in range(100):
observation = env.reset()
for t in range(10000):
env.render()
print(observation)
action = env.action_space.sample()
observation, reward, done, info = env.step(action)
if done:
print("{} timesteps taken for the episode".format(t+1))
break
输出显示在以下屏幕截图中:

OpenAI Universe
OpenAI Universe 提供了广泛的现实游戏环境。 它是 OpenAI Gym 的扩展。 它提供了在各种简单到实时复杂环境中训练和评估智能体的能力。 它可以无限制地访问许多游戏环境。
构建一个视频游戏机器人
让我们学习如何构建一个可以玩赛车游戏的视频游戏机器人。 我们的目标是赛车必须前进,而不会卡在任何障碍物上或撞到其他赛车上。
首先,我们导入必要的库:
import gym
import universe # register universe environment
import random
然后,我们使用make函数模拟赛车环境:
env = gym.make('flashgames.NeonRace-v0')
env.configure(remotes=1) #automatically creates a local docker container
让我们创建用于移动汽车的变量:
# Move left
left = [('KeyEvent', 'ArrowUp', True), ('KeyEvent', 'ArrowLeft', True),
('KeyEvent', 'ArrowRight', False)]
#Move right
right = [('KeyEvent', 'ArrowUp', True), ('KeyEvent', 'ArrowLeft', False),
('KeyEvent', 'ArrowRight', True)]
# Move forward
forward = [('KeyEvent', 'ArrowUp', True), ('KeyEvent', 'ArrowRight', False),
('KeyEvent', 'ArrowLeft', False), ('KeyEvent', 'n', True)]
我们将初始化一些其他变量:
# We use turn variable for deciding whether to turn or not
turn = 0
# We store all the rewards in rewards list
rewards = []
#we will use buffer as some threshold
buffer_size = 100
#we will initially set action as forward, which just move the car forward #without any turn
action = forward
现在,让我们的游戏智能体在无限循环中玩,该循环基于与环境的交互持续执行动作:
while True:
turn -= 1
# Let us say initially we take no turn and move forward.
# We will check value of turn, if it is less than 0
# then there is no necessity for turning and we just move forward.
if turn <= 0:
action = forward
turn = 0
然后,我们使用env.step()一次性执行一项操作(暂时向前移动):
action_n = [action for ob in observation_n]
observation_n, reward_n, done_n, info = env.step(action_n)
对于每个时间步,我们将结果记录在observation_n,reward_n,done_n和info变量中:
observation _n:汽车状态reward_n:如果汽车成功前进但没有卡在障碍物上,则通过上一个动作获得奖励done_n:这是一个布尔值; 如果游戏结束,它将设置为trueinfo_n:用于调试目的
显然,智能体(汽车)无法在整个游戏中前进; 它需要转弯,避免障碍物,并且还会撞到其他车辆。 但是它必须确定是否应该转弯,如果需要转弯,则应朝哪个方向转弯。
首先,我们将计算到目前为止获得的奖励的平均值; 如果是0,则很明显我们在前进时被卡在某处,我们需要转弯。 然后,我们需要转向哪个方向? 您是否还记得我们在第 1 章,“强化学习简介”中研究的策略函数。
关于同一概念,我们这里有两个策略:一个是左转,另一个是右转。 我们将在这里采取随机策略,并计算出奖励并加以改善。
我们将生成一个随机数,如果它小于0.5,那么我们将获得一个右数,否则我们将得到一个左数。 稍后,我们将更新奖励,并根据奖励,了解哪个方向最佳:
if len(rewards) >= buffer_size:
mean = sum(rewards)/len(rewards)
if mean == 0:
turn = 20
if random.random() < 0.5:
action = right
else:
action = left
rewards = []
然后,对于每个剧集(例如游戏结束),我们使用env.render()重新初始化环境(从头开始游戏):
env.render()
完整的代码如下:
import gym
import universe # register universe environment
import random
env = gym.make('flashgames.NeonRace-v0')
env.configure(remotes=1) # automatically creates a local docker container
observation_n = env.reset()
##Declare actions
#Move left
left = [('KeyEvent', 'ArrowUp', True), ('KeyEvent', 'ArrowLeft', True),
('KeyEvent', 'ArrowRight', False)]
#move right
right = [('KeyEvent', 'ArrowUp', True), ('KeyEvent', 'ArrowLeft', False),
('KeyEvent', 'ArrowRight', True)]
# Move forward
forward = [('KeyEvent', 'ArrowUp', True), ('KeyEvent', 'ArrowRight', False),
('KeyEvent', 'ArrowLeft', False), ('KeyEvent', 'n', True)]
#Determine whether to turn or not
turn = 0
#store rewards in a list
rewards = []
#use buffer as a threshold
buffer_size = 100
#initial action as forward
action = forward
while True:
turn -= 1
if turn <= 0:
action = forward
turn = 0
action_n = [action for ob in observation_n]
observation_n, reward_n, done_n, info = env.step(action_n)
rewards += [reward_n[0]]
if len(rewards) >= buffer_size:
mean = sum(rewards)/len(rewards)
if mean == 0:
turn = 20
if random.random() < 0.5:
action = right
else:
action = left
rewards = []
env.render()
如果运行该程序,则可以看到汽车如何学习运动而不会被卡住或撞到其他车辆:

TensorFlow
TensorFlow 是 Google 的开源软件库,已广泛用于数值计算。 它被广泛用于构建深度学习模型,并且是机器学习的一个子集。 它使用可以在许多不同平台上共享和执行的数据流图。 张量只是多维数组,因此,当我们说 TensorFlow 时,它实际上是计算图中的多维数组(张量)的流。
安装 Anaconda 后,安装 TensorFlow 变得非常简单。 无论使用什么平台,都可以通过键入以下命令轻松安装 TensorFlow:
source activate universe
conda install -c conda-forge tensorflow
在安装 TensorFlow 之前,请不要忘记激活universe环境。
我们可以通过简单地运行以下Hello World程序来检查 TensorFlow 安装是否成功:
import tensorflow as tf
hello = tf.constant("Hello World")
sess = tf.Session()
print(sess.run(hello))
变量,常量和占位符
变量,常量和占位符是 TensorFlow 的基本元素。 但是,这三者之间总是存在混淆。 让我们逐一查看每个元素,并了解它们之间的区别。
变量
变量是用于存储值的容器。 变量将用作计算图中其他几个操作的输入。 我们可以使用tf.Variable()函数创建 TensorFlow 变量。 在下面的示例中,我们定义一个具有随机正态分布值的变量,并将其命名为weights:
weights = tf.Variable(tf.random_normal([3, 2], stddev=0.1), name="weights")
但是,在定义变量后,我们需要使用tf.global_variables_initializer()方法显式创建初始化操作,该方法将为变量分配资源。
常量
常量与变量不同,不能更改其值。 常量是不可变的。 一旦为它们分配了值,就不能在整个过程中进行更改。 我们可以使用tf.constant()函数创建常量:
x = tf.constant(13)
占位符
可以将占位符视为变量,在其中仅定义类型和大小而不会分配值。 占位符定义为无值。 占位符的值将在运行时输入。 占位符有一个可选参数shape,它指定数据的维数。 如果shape设置为None,那么我们可以在运行时提供任意大小的数据。 可以使用tf.placeholder()函数定义占位符:
x = tf.placeholder("float", shape=None)
简单来说,我们使用tf.Variable存储数据,并使用tf.placeholder馈送外部数据。
计算图
TensorFlow 中的所有内容都将表示为由节点和边组成的计算图,其中节点是数学运算(例如加法,乘法等),而边是张量。 拥有计算图非常有效地优化了资源,并且还促进了分布式计算。
假设我们有节点B,其输入取决于节点A的输出; 这种依赖关系称为直接依赖关系。
例如:
A = tf.multiply(8,5)
B = tf.multiply(A,1)
当节点B的输入不依赖于节点A时,称为间接依赖。
例如:
A = tf.multiply(8,5)
B = tf.multiply(4,3)
因此,如果我们能够理解这些依赖性,则可以在可用资源中分配独立的计算并减少计算时间。
每当我们导入 TensorFlow 时,都会自动创建一个默认图,并且我们创建的所有节点都将与该默认图关联。
会话
计算图只会被定义; 为了执行计算图,我们使用 TensorFlow 会话:
sess = tf.Session()
我们可以使用tf.Session()方法为我们的计算图创建会话,该方法将分配用于存储变量当前值的内存。 创建会话后,我们可以使用sess.run()方法执行图。
为了在 TensorFlow 中运行任何内容,我们需要为实例启动 TensorFlow 会话; 请参考代码:
import tensorflow as tf
a = tf.multiply(2,3)
print(a)
它将打印一个 TensorFlow 对象而不是6。 就像已经说过的,每当我们导入 TensorFlow 时,都会自动创建一个默认计算图,并且我们创建的所有节点a都将附加到该图上。 为了执行图,我们需要初始化一个 TensorFlow 会话,如下所示:
#Import tensorflow
import tensorflow as tf
#Initialize variables
a = tf.multiply(2,3)
#create tensorflow session for executing the session
with tf.Session() as sess:
#run the session
print(sess.run(a))
前面的代码将显示6。
TensorBoard
TensorBoard 是 TensorFlow 的可视化工具,可用于可视化计算图。 它也可以用来绘制各种定量指标和一些中间计算的结果。 使用 TensorBoard,我们可以轻松地可视化复杂的模型,这对于调试和共享非常有用。
现在,让我们构建一个基本的计算图,并在 TensorBoard 中对其进行可视化。
首先,让我们导入库:
import tensorflow as tf
接下来,我们初始化变量:
a = tf.constant(5)
b = tf.constant(4)
c = tf.multiply(a,b)
d = tf.constant(2)
e = tf.constant(3)
f = tf.multiply(d,e)
g = tf.add(c,f)
现在,我们将创建一个 TensorFlow 会话。 我们将使用tf.summary.FileWriter()将图的结果写入名为event的文件中:
with tf.Session() as sess:
writer = tf.summary.FileWriter("output", sess.graph)
print(sess.run(g))
writer.close()
为了运行 TensorBoard,请转到您的终端,找到工作目录,然后键入tensorboard --logdir=output --port=6003。
您可以看到如下所示的输出:

添加范围
范围划分用于降低复杂性,并通过将相关节点分组在一起来帮助我们更好地理解模型。 例如,在前面的示例中,我们可以将图分为两个不同的组,称为计算和结果。 如果看前面的示例,可以看到节点a至e执行计算,而节点g计算结果。 因此,我们可以使用范围将它们分开分组,以便于理解。 可以使用tf.name_scope()函数创建作用域。
让我们通过Computation使用tf.name_scope()函数:
with tf.name_scope("Computation"):
a = tf.constant(5)
b = tf.constant(4)
c = tf.multiply(a,b)
d = tf.constant(2)
e = tf.constant(3)
f = tf.multiply(d,e)
让我们通过Result使用tf.name_scope()函数:
with tf.name_scope("Result"):
g = tf.add(c,f)
查看Computation范围; 我们可以将其进一步分解为更多的部分。 我们可以创建一个范围为Part 1并具有节点a至c的范围,并创建一个范围为Part 2并具有节点d至e的范围,因为第 1 部分和第 2 部分彼此独立 :
with tf.name_scope("Computation"):
with tf.name_scope("Part1"):
a = tf.constant(5)
b = tf.constant(4)
c = tf.multiply(a,b)
with tf.name_scope("Part2"):
d = tf.constant(2)
e = tf.constant(3)
f = tf.multiply(d,e)
通过在 TensorBoard 中可视化它们可以更好地了解作用域。 完整的代码如下:
import tensorflow as tf
with tf.name_scope("Computation"):
with tf.name_scope("Part1"):
a = tf.constant(5)
b = tf.constant(4)
c = tf.multiply(a,b)
with tf.name_scope("Part2"):
d = tf.constant(2)
e = tf.constant(3)
f = tf.multiply(d,e)
with tf.name_scope("Result"):
g = tf.add(c,f)
with tf.Session() as sess:
writer = tf.summary.FileWriter("output", sess.graph)
print(sess.run(g))
writer.close()
如果查看下图,您可以轻松地了解范围如何通过将相似节点分组在一起来帮助我们降低理解的复杂性。 范围界定在处理复杂项目时被广泛使用,以更好地了解节点的功能和依赖项:

总结
在本章中,我们学习了如何通过安装 Anaconda,Docker,OpenAI Gym,Universe 和 TensorFlow 来设置机器。 我们还学习了如何使用 OpenAI 创建模拟,以及如何训练智能体在 OpenAI 环境中学习。 然后,我们了解了 TensorFlow 的基础知识,然后在 TensorBoard 中可视化了图。
在下一章第 3 章,“马尔可夫决策过程和动态规划”中,我们将学习马尔可夫决策过程和动态规划以及如何使用值和策略迭代来解决冻湖问题。
问题
问题列表如下:
- 为什么以及如何在 Anaconda 中创建新环境?
- 使用 Docker 有什么需要?
- 我们如何在 OpenAI Gym 中模拟环境?
- 我们如何检查 OpenAI Gym 中的所有可用环境?
- OpenAI Gym 和 Universe 是否相同? 如果没有,原因是什么?
- TensorFlow 变量和占位符有何区别?
- 什么是计算图?
- 为什么我们需要 TensorFlow 中的会话?
- TensorBoard 的目的是什么?我们如何启动它?
进一步阅读
您可以进一步参考以下论文:
三、马尔可夫决策过程与动态规划
马尔可夫决策过程(MDP)提供了解决强化学习(RL)问题的数学框架。 几乎所有的 RL 问题都可以建模为 MDP。 MDP 被广泛用于解决各种优化问题。 在本章中,我们将了解什么是 MDP 以及如何使用它来解决 RL 问题。 我们还将学习动态规划,它是一种有效解决复杂问题的技术。
在本章中,您将学习以下主题:
- 马尔可夫链与马尔可夫过程
- 马尔可夫决策过程
- 奖励与回报
- 贝尔曼方程
- 使用动态规划求解贝尔曼方程
- 利用值和策略迭代来解决冻湖问题
马尔可夫链与马尔可夫过程
在进入 MDP 之前,让我们了解马尔可夫链和马尔可夫过程,它们构成了 MDP 的基础。
马尔可夫性质指出,未来仅取决于现在而不是过去。 马尔可夫链是一个概率模型,仅依靠当前状态来预测下一个状态,而不是先前的状态,也就是说,未来有条件地独立于过去。 马尔可夫链严格遵循马尔可夫属性。
例如,如果我们知道当前状态是多云,则可以预测下一个状态可能是雨天。 我们得出的结论是,只有考虑当前状态(多云)而不考虑过去的状态(可能是晴天,大风等),下一个状态才会下雨。 但是,马尔可夫属性并不适用于所有进程。 例如,掷骰子(下一个状态)与前一个数字无关,无论骰子上显示的是什么(当前状态)。
从一种状态移动到另一种状态称为转移,其概率称为转移概率。 我们可以用表格的形式来表示转移概率,如下所示,它被称为马尔可夫表。 在给定当前状态的情况下,它显示了移至下一个状态的概率为:
| 当前状态 | 下一个状态 | 转移概率 |
|---|---|---|
| 多云 | 下雨 | 0.6 |
| 下雨 | 下雨 | 0.2 |
| 晴天 | 多云 | 0.1 |
| 下雨 | 晴天 | 0.1 |
我们还可以以状态图的形式表示马尔可夫链,该状态图显示转移概率:

前面的状态图显示了从一种状态转移到另一种状态的可能性。 还是不了解马尔可夫链? 好吧,让我们谈谈。
我:“你在做什么?”
您:“我正在阅读有关马尔可夫链的信息。”
我:“看完书后你打算做什么?”
你:“我要睡觉了。”
我:“您确定要睡觉吗?”
您:“可能。如果我不困,我会看电视的。”
我:“很酷;这也是一条马尔可夫链。”
你:“嗯?”
我们可以将对话公式化为马尔可夫链,并绘制如下状态图:

马尔可夫链位于核心概念中,即未来仅取决于现在而不是过去。 如果遵循马尔可夫属性,则随机过程称为马尔可夫过程。
马尔可夫决策过程
MDP 是马尔可夫链的延伸。 它提供了用于建模决策情况的数学框架。 几乎所有的强化学习问题都可以建模为 MDP。
MDP 由五个重要元素表示:
- 智能体实际上可以处于的一组状态
(S)。 - 智能体可以执行的一组动作
(A),用于从一种状态转移到另一种状态。 - 转移概率(
P[ss']^a),是通过执行某些操作a从一种状态s转移到另一种状态s'的概率。 - 奖励概率(
R[ss']^a),是智能体通过执行某些动作a从一种状态s转移到另一种状态s'所获得的奖励的概率。 - 折扣因子(
γ),用于控制立即和将来奖励的重要性。 我们将在接下来的部分中详细讨论。
奖励与回报
如我们所知,在 RL 环境中,智能体通过执行操作与环境交互,并从一种状态转移到另一种状态。 根据其执行的动作,它会获得奖励。 奖励不过是一个数字值,例如,一个好动作为 +1,而一个坏动作为 -1。 我们如何确定一个动作是好是坏? 在迷宫游戏中,好动作是指智能体进行移动以使其不会撞到迷宫壁的地方,而不好的动作是指智能体进行移动并撞到迷宫壁的地方。
智能体试图最大化从环境而不是即时奖励中获得的奖励(累积奖励)总量。 智能体从环境中获得的总奖励金额称为回报。 因此,我们可以将智能体收到的奖励(回报)总额计算如下:

r[t + 1]是智能体在执行操作a₀从一种状态转换到另一种状态时在时间步骤t₀时收到的奖励。 r[t + 2]是智能体在执行从一个状态到另一状态的动作时,在
步骤t₁时收到的奖励。 类似地,rₜ是智能体在执行从一个状态到另一状态的动作时,在最后时间步骤T所收到的奖励。
间歇性和连续性任务
情景任务是具有最终状态(结束)的任务。 在 RL 中,剧集被视为从初始状态到最终状态的智能体与环境的相互作用。
例如,在赛车视频游戏中,您启动游戏(初始状态)并玩游戏直到游戏结束(最终状态)。 这称为剧集。 游戏结束后,您可以通过重新启动游戏来开始下一个剧集,并且无论您在上一个游戏中所处的位置如何,都将从初始状态开始。 因此,每个剧集彼此独立。
在连续任务中,没有最终状态。 连续的任务永远不会结束。 例如,个人协助机器人没有最终状态。
折扣因子
我们已经看到,智能体的目标是使回报最大化。 对于一项临时任务,我们可以将返回定义为R[t] = r[t + 1] + r[t + 2] + ... + r[T],其中T是剧集的最终状态,我们尝试最大化回报Rₜ。
由于连续任务没有任何最终状态,因此我们可以将连续任务的收益定义为R[t] = r[t + 1] + r[t + 2] + ...,总和为无穷大。 但是,如果永不止息,我们如何才能最大化回报呢?
这就是为什么我们引入折扣因子的概念。 我们可以使用折扣因子γ重新定义收益,如下所示:
---(1)
---(2)
折扣系数决定了我们对未来奖励和即时奖励的重视程度。 折扣因子的值在0至1之内。 折扣因子0意味着即时奖励更为重要,而折扣因子1意味着未来奖励比即时奖励更为重要。
折扣系数0永远不会只考虑立即获得的奖励。 同样,1的折扣因子将永远学习,以寻找未来的奖励,这可能导致无限。 因此,折扣因子的最佳值在 0.2 到 0.8 之间。
根据使用情况,我们重视即时奖励和将来的奖励。 在某些情况下,未来的奖励比立即的奖励更可取,反之亦然。 在国际象棋游戏中,目标是击败对手的国王。 如果我们重视即时奖励,而即时奖励是通过典当击败任何对手玩家等行动获得的,那么坐席将学会执行此子目标,而不是学习达到实际目标。 因此,在这种情况下,我们重视未来的奖励,而在某些情况下,我们更喜欢即时奖励,而不是未来的奖励。 (说,如果我今天或 13 个月后给您巧克力,您会喜欢巧克力吗?)
策略函数
我们已经在第 1 章,“强化学习简介”中了解了策略函数,该函数将状态映射到操作。 用π表示。
策略函数可以表示为π(s): s -> a,指示从状态到动作的映射。 因此,基本上,策略函数会说明在每种状态下要执行的操作。 我们的最终目标在于找到最佳策略,该策略指定在每个状态下执行的正确操作,从而最大化回报。
状态值函数
状态值函数也简称为值函数。 它通过策略π指定智能体处于特定状态的状态如何。 值函数通常由V(s)表示。 它表示遵循策略的状态的值。
我们可以定义一个状态值函数,如下所示:

这根据策略π指定从状态s开始的预期收益。 我们可以用(2)的值函数替换Rₜ的值,如下所示:

请注意,状态值函数取决于策略,并且取决于我们选择的策略而有所不同。
我们可以在表中查看值函数。 假设我们有两个状态,并且这两个状态都遵循策略π。 根据这两个状态的值,我们可以判断出执行策略后,我们的智能体处于该状态有多好。 值越大,状态越好:
| 状态 | 值 |
|---|---|
| 状态 1 | 0.3 |
| 状态 2 | 0.9 |
根据上表,我们可以知道处于状态 2 很好,因为它具有很高的值。 在接下来的部分中,我们将看到如何直观地估计这些值。
状态作用值函数(Q 函数)
状态作用值函数也称为Q函数。 它指定智能体在具有策略π的状态下执行特定操作的效果如何。Q函数由Q(s)表示。 它表示在遵循策略π的状态下执行操作的值。
我们可以定义Q函数,如下所示:

这根据策略π指定从状态s开始的预期回报,其动作为a。 我们可以从(2)的Q函数中替换Rₜ的值,如下所示:

值函数和Q函数之间的区别在于,值函数指定状态的优劣,而Q函数指定状态的行为的优劣。
与状态值函数一样,Q函数可以在表中查看。 也称为Q表。 让我们说我们有两个状态和两个动作。 我们的Q表如下所示:
| 状态 | 动作 | 值 |
|---|---|---|
| 状态 1 | 动作 1 | 0.03 |
| 状态 1 | 动作 2 | 0.02 |
| 状态 2 | 动作 1 | 0.5 |
| 状态 2 | 动作 2 | 0.9 |
因此,Q表显示了所有可能的状态动作对的值。 因此,通过查看此表,我们可以得出结论,在状态 1 中执行动作 1 和在状态 2 中执行动作 2 是更好的选择,因为它具有很高的值。
每当我们说值函数V(S)或Q函数Q(S, a)时,它实际上表示值表,而Q表,如前所示。
贝尔曼方程和最优性
以美国数学家理查德·贝尔曼(Richard Bellman)命名的贝尔曼方程式可帮助我们求解 MDP。 它在 RL 中无处不在。 当我们说解决 MDP 时,实际上意味着找到最佳的策略和值函数。 根据不同的策略,可以有许多不同的值函数。 与所有其他值函数相比,最优值函数V*(s)是产生最大值的函数:

类似地,最优策略是导致最优值函数的策略。
由于最优值函数V*(s)是比所有其他值函数(即最大收益)更高的值的函数,因此它将是Q函数的最大值。 因此,可以通过将Q函数的最大值取如下来轻松地计算最佳值函数:
---(3)
值函数的贝尔曼方程可以表示为(在下一主题中,我们将看到如何推导该方程):

它指示状态值及其后继状态与所有可能性的平均值之间的递归关系。
类似地,用于Q函数的贝尔曼方程可以表示为:
---(4)
将公式(4)代入(3),我们得到:

前面的方程式称为贝尔曼最优方程式。 在接下来的部分中,我们将看到如何通过求解该方程式找到最佳策略。
推导值和 Q 函数的贝尔曼方程
现在,让我们看看如何导出值和Q函数的贝尔曼方程。
如果您对数学不感兴趣,可以跳过本节。 但是,数学将非常有趣。
首先,我们将P[ss']^a定义为在执行动作a时从状态s转换为s'的转移概率:

我们将R[ss']^a定义为在执行动作a时从状态s移至s'所获得的奖励概率:

来自(2)---(5)
我们知道值函数可以表示为:

来自(1)
我们可以通过获取第一笔报酬来重写值函数:
---(6)
如果我们处于状态s,并通过策略π执行动作a,则值函数中的期望值指定了期望收益。
因此,我们可以通过总结所有可能的动作和奖励来明确地重写我们的期望,如下所示:

在 RHS 中,我们将等式(5)中的R[ss']^a替换为:

同样,在 LHS 中,我们将从等式(2)中替换r[t + 1]的值,如下所示:

因此,我们的最终期望方程变为:
---(7)
现在,我们将期望值(7)替换为值函数(6),如下所示:

代替

我们可以用等式(6)代替V[s']^π,我们的最终值函数如下所示:

以非常相似的方式,我们可以为Q函数导出一个贝尔曼方程; 最终方程如下:

现在,对于值函数和Q函数都有一个贝尔曼方程,我们将看到如何找到最佳策略。
求解贝尔曼方程
我们可以通过求解贝尔曼最优性方程来找到最优策略。 为了解决贝尔曼最优性方程,我们使用一种称为动态规划的特殊技术。
动态规划
动态规划(DP)是一种解决复杂问题的技术。 在 DP 中,不是一次解决一个复杂的问题,而是将问题分解为简单的子问题,然后针对每个子问题,我们计算并存储解决方案。 如果出现相同的子问题,我们将不会重新计算,而是使用已经计算的解决方案。 因此,DP 有助于极大地减少计算时间。 它的应用广泛,包括计算机科学,数学,生物信息学等。
我们使用两种强大的算法来求解贝尔曼方程:
- 值迭代
- 策略迭代
值迭代
在值迭代中,我们从随机值函数开始。 显然,随机值函数可能不是最佳函数,因此我们以迭代方式寻找新的改进值函数,直到找到最佳值函数为止。 一旦找到最优值函数,就可以轻松地从中得出最优策略:

值迭代涉及的步骤如下:
- 首先,我们初始化随机值函数,即每个状态的随机值。
- 然后,我们为
Q(s, a)的所有状态动作对计算Q函数。 - 然后,我们使用
Q(s, a)中的最大值更新值函数。 - 我们重复这些步骤,直到值函数的变化很小。
让我们通过手动逐步执行值迭代来直观地理解它。
考虑此处显示的网格。 让我们说我们处于状态A,我们的目标是在不访问状态B的情况下达到状态C,我们有两个动作,0 是左/右和 1 是上/下:

您能想到这里的最佳策略吗? 此处的最佳策略是告诉我们在A状态下执行操作 1 的策略,这样我们就可以访问C而无需访问B。 我们如何找到这个最佳策略? 现在让我们看看:
初始化随机值函数,即所有状态的随机值。 让我们将0分配给所有状态:

让我们计算所有状态动作对的Q值。
Q 值告诉我们每个状态下一个动作的值。 首先,让我们计算状态A的Q值。 调用Q函数的方程式。 为了进行计算,我们需要转移和奖励概率。 让我们考虑状态A的转移和奖励概率,如下所示:

状态A的 Q 函数可以计算如下:
Q(s, a) = 转移概率 * (奖励概率 + gamma * next_state 的值)
在此,gamma是折扣因子; 我们将其视为1。
状态A和操作0的 Q 值:

Q(A, 0) = (0.1 * (0 + 0)) + (0.4 * (-1.0 + 0)) + (0.3 * (1.0 + 0))
Q(A, 0) = -0.1
现在我们将计算状态A和操作1的Q值:

Q(A, 1) = (0.3 * (0 + 0)) + (0.1 * (-2.0 + 0)) + (0.5 * (1.0 + 0))
Q(A, 1) = 0.3
现在,我们将在Q表中对此进行更新,如下所示:

从Q(s, a)更新值函数为最大值。
如果您查看前面的Q函数,则Q(A, 1)的值大于Q(A, 0)的值,因此我们将将状态A的值更新为Q(A, 1):

同样,我们为所有状态动作对计算Q值,并通过获取具有最高状态动作值的Q值来更新每个状态的值函数。 我们的更新值函数如下所示。 这是第一次迭代的结果:

我们重复此步骤几次迭代。 也就是说,我们将步骤2重复到步骤3(在每次迭代中,在计算Q值时,我们使用更新后的值函数,而不是相同的随机初始化函数) 值函数)。
这是第二次迭代的结果:

这是第三次迭代的结果:

但是我们什么时候停止呢? 当每次迭代之间的值变化较小时,我们将停止; 如果查看第二和第三次迭代,则值函数的变化不大。 在这种情况下,我们停止迭代并将其视为最佳值函数。
好了,既然我们已经找到了最优值函数,那么我们如何得出最优策略呢?
这很简单。 我们用最终的最优值函数计算Q函数。 假设我们计算出的Q函数如下所示:

从此Q函数中,我们选择每种状态下具有最大值的动作。 在状态A下,我们为操作 1 设置了最大值,这是我们的最佳策略。 因此,如果我们在状态A中执行动作 1,则无需访问B就可以达到C。
策略迭代
与值迭代不同,在策略迭代中,我们从随机策略开始,然后找到该策略的值函数。 如果值函数不是最优的,那么我们会找到新的改进策略。 我们重复此过程,直到找到最佳策略。
策略迭代有两个步骤:
- 策略评估:评估随机估计策略的值函数。
- 策略改进:在评估值函数时,如果它不是最优的,我们会发现新的改进策略:

策略迭代涉及的步骤如下:
- 首先,我们初始化一些随机策略
- 然后我们找到该随机策略的值函数,并进行评估以检查其是否最优,这称为策略评估
- 如果不是最佳选择,我们会找到新的改进策略,称为策略改进
- 我们重复这些步骤,直到找到最佳策略
让我们通过逐步手动执行策略迭代来直观地理解。
考虑我们在值迭代部分中看到的相同网格示例。 我们的目标是找到最佳策略:
- 初始化随机策略函数。
让我们通过为每个状态指定随机动作来初始化随机策略函数:
A -> 0
B -> 1
C -> 0
- 查找随机初始化策略的值函数。
现在,我们必须使用随机初始化的策略来查找值函数。 我们说一下计算后的值函数如下:

现在,根据随机初始化的策略有了新的值函数,让我们使用新的值函数计算新的策略。 我们如何做到这一点? 这与我们在值迭代中所做的非常相似。 我们为新值函数计算Q值,然后针对具有最大值的每个状态采取措施作为新策略。
我们说新策略的结果是:
A -> 0
B -> 1
C -> 1
我们检查旧策略,即随机初始化的策略和新策略。 如果它们相同,则我们已经达到收敛,即找到了最佳策略。 如果没有,我们将旧策略(随机策略)更新为新策略,并从步骤2开始重复。
听起来令人困惑? 看一下伪代码:
policy_iteration():
Initialize random policy
for i in no_of_iterations:
Q_value = value_function(random_policy)
new_policy = Maximum state action pair from Q value
if random_policy == new policy:
break
random_policy = new_policy
return policy
解决冻湖问题
如果您到目前为止还不了解我们所学的知识,请放心,我们将研究所有概念以及冻湖问题。
想象一下,有一个从家到办公室的冰冻湖泊。 您必须在结冰的湖面上行走才能到达办公室。 但是糟糕! 冰冻的湖面上有洞,因此您在冰冻的湖面上行走时必须小心,以免被困在洞中:

看上图:
S是起始位置(原点)F是您可以漫步的冰冻湖H是孔,您必须非常小心G是目标(办公室)
好的,现在让我们代替您使用我们的智能体来找到到达办公室的正确方法。 该智能体的目标是找到从S到G的最佳路径,而不会陷入H的陷阱。 智能体如何做到这一点? 如果智能体正确地在冰冻的湖面上行走,我们给 +1 分作为奖励,如果智能体正确落入洞中,则给 0 分,以便智能体确定正确的行动。 智能体现在将尝试找到最佳策略。 最优策略意味着采取正确的道路,这将最大化智能体的报酬。 如果智能体正在使报酬最大化,则显然智能体正在学习跳过漏洞并到达目的地。
我们可以将问题建模为我们先前研究的 MDP。 MDP 包含以下内容:
- 状态:状态集。 在这里,我们有 16 个状态(网格中的每个小方框)。
- 动作:所有可能动作的集合(左,右,上,下;这是我们的智能体在冰冻湖面环境中可以采取的所有四种可能动作)。
- 转移概率:通过执行动作
a从一种状态(F)转换为另一种状态(H)的概率。 - 奖励概率:这是执行动作
a从一种状态(F)迁移到另一种状态(H)时获得奖励的概率。
现在我们的目标是解决 MDP。 解决 MDP 意味着寻找最佳策略。 现在,我们介绍三个特殊函数:
- 策略函数:指定在每种状态下要执行的操作
- 值函数:指定状态的良好程度
- Q 函数:指定动作在特定状态下的状态
当我们说有多好时,这到底意味着什么? 这意味着最大化奖励是多么的好。
然后,我们使用称为贝尔曼最优性方程的特殊方程式表示值函数和Q函数。 如果我们解决这个方程,我们可以找到最佳策略。 在这里,求解方程式意味着找到正确的值函数和策略。 如果我们找到正确的值函数和策略,那将是我们获得最大回报的最佳途径。
我们将使用一种称为动态规划的特殊技术来求解贝尔曼最优性方程。 要应用 DP,必须预先知道模型动态,这基本上意味着必须预先知道模型环境的转换概率和奖励概率。 由于我们知道模型动态,因此可以在此处使用 DP。 我们使用两种特殊的 DP 算法来找到最佳策略:
- 值迭代
- 策略迭代
值迭代
简单来说,在值迭代中,我们首先将一些随机值初始化为值函数。 我们初始化的随机值很有可能不会达到最佳状态。 因此,我们遍历每个状态并找到新的值函数; 我们停止迭代,直到找到最佳值函数。 一旦找到最优值函数,就可以轻松地从中提取最优策略。
现在我们将看到如何使用值迭代来解决冻湖问题。
首先,我们导入必要的库:
import gym
import numpy as np
然后,我们使用 OpenAI 的 Gym 创建冰冻的湖泊环境:
env = gym.make('FrozenLake-v0')
我们将首先探索环境。
由于我们有一个4 * 4的网格,因此环境中的状态数为 16:
print(env.observation_space.n)
环境中的操作数为四个,分别为上,下,左和右:
print(env.observation_space.n)
现在我们定义一个value_iteration()函数,该函数返回最佳值函数(值表)。 我们将首先逐步了解该函数,然后再查看整个函数。
首先,我们为所有状态和迭代次数初始化随机值表0:
value_table = np.zeros(env.observation_space.n)
no_of_iterations = 100000
然后,在每次迭代开始时,我们将value_table复制到updated_value_table:
for i in range(no_of_iterations):
updated_value_table = np.copy(value_table)
现在,我们计算 Q 表,并选择具有最高值的最大状态动作对作为值表。
我们将使用之前解决的示例来理解代码。 我们在上一个示例中计算了状态A和操作1的Q值:
Q(A, 1) = (0.3 * (0 + 0)) + (0.1 * (-1.0 + 0)) + (0.5 + (1.0 + 0))
Q(A, 1) = 0.5
我们没有为每个状态创建Q表,而是创建了一个名为Q_value的列表,然后为该状态中的每个动作创建了一个名为next_states_rewards的列表,该列表存储了Q_value 下一个转移状态。 然后,我们对next_state_rewards求和并将其附加到我们的Q_value中。
请看前面的示例,其中状态为A,操作为1。 (0.3 * (0 + 0))是转移状态A的下一个状态奖励,(0.1 * (-1.0 + 0))是转移状态B的下一状态奖励。 (0.5 + (1.0 + 0))是转移状态C的下一个状态奖励。 我们将所有这些加总为next_state_reward,并将其附加到Q_value中,该值为 0.5。
当我们为状态的所有动作计算next_state_rewards并将其附加到Q值时,我们选取最大的Q值并将其更新为我们的状态值:
for state in range(env.observation_space.n):
Q_value = []
for action in range(env.action_space.n):
next_states_rewards = []
for next_sr in env.P[state][action]:
trans_prob, next_state, reward_prob, _ = next_sr
next_states_rewards.append((trans_prob * (reward_prob + gamma * updated_value_table[next_state])))
Q_value.append(np.sum(next_states_rewards))
#Pick up the maximum Q value and update it as value of a state
value_table[state] = max(Q_value)
然后,我们将检查是否已经达到收敛,即,我们的值表和更新后的值表之间的差异非常小。 我们怎么知道它很小? 我们定义了一个名为threshold的变量,然后我们看一下差异是否小于我们的threshold; 如果小于,则中断循环,并将值函数返回为最佳值函数:
threshold = 1e-20
if (np.sum(np.fabs(updated_value_table - value_table)) <= threshold):
print ('Value-iteration converged at iteration# %d.' %(i+1))
break
查看value_iteration()的完整函数可以更好地理解:
def value_iteration(env, gamma = 1.0):
value_table = np.zeros(env.observation_space.n)
no_of_iterations = 100000
threshold = 1e-20
for i in range(no_of_iterations):
updated_value_table = np.copy(value_table)
for state in range(env.observation_space.n):
Q_value = []
for action in range(env.action_space.n):
next_states_rewards = []
for next_sr in env.P[state][action]:
trans_prob, next_state, reward_prob, _ = next_sr
next_states_rewards.append((trans_prob * (reward_prob + gamma * updated_value_table[next_state])))
Q_value.append(np.sum(next_states_rewards))
value_table[state] = max(Q_value)
if (np.sum(np.fabs(updated_value_table - value_table)) <= threshold):
print ('Value-iteration converged at iteration# %d.' %(i+1))
break
return value_table, Q_value
因此,我们可以使用value_iteration导出optimal_value_function:
optimal_value_function = value_iteration(env=env,gamma=1.0)
找到optimal_value_function后,如何从optimal_value_function中提取最佳策略? 我们使用最佳值操作来计算Q值,并选择每个状态中具有最高Q值的操作作为最佳策略。 我们通过称为extract_policy()的函数来完成此操作; 我们现在将逐步介绍这一点。
首先,我们定义随机策略; 对于所有状态,我们将其定义为0:
policy = np.zeros(env.observation_space.n)
然后,对于每个状态,我们都构建一个Q_table,并且针对该状态中的每个动作,我们计算Q值并将其添加到我们的Q_table中:
for state in range(env.observation_space.n):
Q_table = np.zeros(env.action_space.n)
for action in range(env.action_space.n):
for next_sr in env.P[state][action]:
trans_prob, next_state, reward_prob, _ = next_sr
Q_table[action] += (trans_prob * (reward_prob + gamma * value_table[next_state]))
然后,我们为state选择策略,将其作为具有Q最高值的操作:
policy[state] = np.argmax(Q_table)
看一下完整的函数:
def extract_policy(value_table, gamma = 1.0):
policy = np.zeros(env.observation_space.n)
for state in range(env.observation_space.n):
Q_table = np.zeros(env.action_space.n)
for action in range(env.action_space.n):
for next_sr in env.P[state][action]:
trans_prob, next_state, reward_prob, _ = next_sr
Q_table[action] += (trans_prob * (reward_prob + gamma * value_table[next_state]))
policy[state] = np.argmax(Q_table)
return policy
因此,我们可以得出optimal_policy如下:
optimal_policy = extract_policy(optimal_value_function, gamma=1.0)
我们将获得如下输出,即optimal_policy,即在每种状态下要执行的操作:
array([0., 3., 3., 3., 0., 0., 0., 0., 3., 1., 0., 0., 0., 2., 1., 0.])
完整的程序如下:
import gym
import numpy as np
env = gym.make('FrozenLake-v0')
def value_iteration(env, gamma = 1.0):
value_table = np.zeros(env.observation_space.n)
no_of_iterations = 100000
threshold = 1e-20
for i in range(no_of_iterations):
updated_value_table = np.copy(value_table)
for state in range(env.observation_space.n):
Q_value = []
for action in range(env.action_space.n):
next_states_rewards = []
for next_sr in env.P[state][action]:
trans_prob, next_state, reward_prob, _ = next_sr
next_states_rewards.append((trans_prob * (reward_prob + gamma * updated_value_table[next_state])))
Q_value.append(np.sum(next_states_rewards))
value_table[state] = max(Q_value)
if (np.sum(np.fabs(updated_value_table - value_table)) <= threshold):
print ('Value-iteration converged at iteration# %d.' %(i+1))
break
return value_table
def extract_policy(value_table, gamma = 1.0):
policy = np.zeros(env.observation_space.n)
for state in range(env.observation_space.n):
Q_table = np.zeros(env.action_space.n)
for action in range(env.action_space.n):
for next_sr in env.P[state][action]:
trans_prob, next_state, reward_prob, _ = next_sr
Q_table[action] += (trans_prob * (reward_prob + gamma * value_table[next_state]))
policy[state] = np.argmax(Q_table)
return policy
optimal_value_function = value_iteration(env=env,gamma=1.0)
optimal_policy = extract_policy(optimal_value_function, gamma=1.0)
print(optimal_policy)
策略迭代
在策略迭代中,首先我们初始化一个随机策略。 然后,我们将评估初始化的随机策略:它们是否好? 但是,我们如何评估策略呢? 我们将通过计算它们的值函数来评估我们随机初始化的策略。 如果它们不好,那么我们会找到新的策略。 我们重复此过程,直到找到好的策略。
现在让我们看看如何使用策略迭代来解决冻湖问题。
在查看策略迭代之前,我们将了解在给定策略的情况下如何计算值函数。
我们用状态数将value_table初始化为零:
value_table = np.zeros(env.nS)
然后,对于每个状态,我们从策略中获取操作,然后根据action和state计算值函数,如下所示:
updated_value_table = np.copy(value_table)
for state in range(env.nS):
action = policy[state]
value_table[state] = sum([trans_prob * (reward_prob + gamma * updated_value_table[next_state])
for trans_prob, next_state, reward_prob, _ in env.P[state][action]])
当value_table和updated_value_table之差小于我们的threshold时,我们将打破这一点:
threshold = 1e-10
if (np.sum((np.fabs(updated_value_table - value_table))) <= threshold):
break
查看以下完整函数:
def compute_value_function(policy, gamma=1.0):
value_table = np.zeros(env.nS)
threshold = 1e-10
while True:
updated_value_table = np.copy(value_table)
for state in range(env.nS):
action = policy[state]
value_table[state] = sum([trans_prob * (reward_prob + gamma * updated_value_table[next_state])
for trans_prob, next_state, reward_prob, _ in env.P[state][action]])
if (np.sum((np.fabs(updated_value_table - value_table))) <= threshold):
break
return value_table
现在,我们将逐步了解如何执行策略迭代。
首先,我们将random_policy初始化为零个 NumPy 数组,其形状为状态数:
random_policy = np.zeros(env.observation_space.n)
然后,对于每次迭代,我们根据随机策略计算new_value_function:
new_value_function = compute_value_function(random_policy, gamma)
我们将使用计算出的new_value_function提取策略。 extract_policy函数与我们在值迭代中使用的函数相同:
new_policy = extract_policy(new_value_function, gamma)
然后,我们检查是否已经达到收敛,即是否通过比较random_policy和新策略找到了最佳策略。 如果它们相同,我们将中断迭代。 否则,我们用new_policy更新random_policy:
if (np.all(random_policy == new_policy)):
print ('Policy-Iteration converged at step %d.' %(i+1))
break
random_policy = new_policy
查看完整的policy_iteration函数:
def policy_iteration(env,gamma = 1.0):
random_policy = np.zeros(env.observation_space.n)
no_of_iterations = 200000
gamma = 1.0
for i in range(no_of_iterations):
new_value_function = compute_value_function(random_policy, gamma)
new_policy = extract_policy(new_value_function, gamma)
if (np.all(random_policy == new_policy)):
print ('Policy-Iteration converged at step %d.' %(i+1))
break
random_policy = new_policy
return new_policy
因此,我们可以使用policy_iteration获得optimal_policy:
optimal_policy = policy_iteration(env, gamma = 1.0)
我们将获得一些输出,即optimal_policy,这是在每种状态下要执行的操作:
array([0., 3., 3., 3., 0., 0., 0., 0., 3., 1., 0., 0., 0., 2., 1., 0.])
完整的程序如下:
import gym
import numpy as np
env = gym.make('FrozenLake-v0')
def compute_value_function(policy, gamma=1.0):
value_table = np.zeros(env.nS)
threshold = 1e-10
while True:
updated_value_table = np.copy(value_table)
for state in range(env.nS):
action = policy[state]
value_table[state] = sum([trans_prob * (reward_prob + gamma * updated_value_table[next_state])
for trans_prob, next_state, reward_prob, _ in env.P[state][action]])
if (np.sum((np.fabs(updated_value_table - value_table))) <= threshold):
break
return value_table
def extract_policy(value_table, gamma = 1.0):
policy = np.zeros(env.observation_space.n)
for state in range(env.observation_space.n):
Q_table = np.zeros(env.action_space.n)
for action in range(env.action_space.n):
for next_sr in env.P[state][action]:
trans_prob, next_state, reward_prob, _ = next_sr
Q_table[action] += (trans_prob * (reward_prob + gamma * value_table[next_state]))
policy[state] = np.argmax(Q_table)
return policy
def policy_iteration(env,gamma = 1.0):
random_policy = np.zeros(env.observation_space.n)
no_of_iterations = 200000
gamma = 1.0
for i in range(no_of_iterations):
new_value_function = compute_value_function(random_policy, gamma)
new_policy = extract_policy(new_value_function, gamma)
if (np.all(random_policy == new_policy)):
print ('Policy-Iteration converged at step %d.' %(i+1))
break
random_policy = new_policy
return new_policy
print (policy_iteration(env))
因此,我们可以得出最佳策略,该策略使用值和策略迭代来解决冻结湖问题,从而指定在每种状态下要执行的操作。
总结
在本章中,我们了解了什么是马尔可夫链和马尔可夫过程,以及如何使用 MDP 表示 RL 问题。 我们还研究了贝尔曼方程,并解决了贝尔曼方程,从而使用 DP 推导了最优策略。 在下一章第 4 章,“使用蒙特卡洛方法进行游戏”中,我们将研究蒙特卡洛树搜索以及如何使用它进行智能游戏的构建。
问题
问题列表如下:
- 马尔可夫属性是什么?
- 为什么我们需要马尔可夫决策过程?
- 我们何时更喜欢即时奖励?
- 折扣因子有什么用?
- 为什么要使用贝尔曼函数?
- 您将如何导出 Q 函数的贝尔曼方程?
- 值函数和 Q 函数有何关系?
- 值迭代和策略迭代有什么区别?
进一步阅读
四、用于游戏的蒙特卡洛方法
蒙特卡洛算法是从物理,机械到计算机科学的各个领域中最受欢迎和最常用的算法之一。 当未知环境模型时,在强化学习(RL)中使用蒙特卡洛算法。 在上一章第 3 章,“马尔可夫决策过程和动态规划”中,我们着眼于使用动态规划(DP)查找我们了解模型动态的最佳策略,即转移和奖励概率。 但是,当我们不知道模型动态时,如何确定最佳策略? 在这种情况下,我们使用蒙特卡洛算法; 当我们不了解环境时,它对于找到最佳策略非常有用。
在本章中,您将了解以下内容:
- 蒙特卡洛方法
- 蒙特卡洛预测
- 使用蒙特卡洛玩二十一点
- 蒙特卡洛控制模型
- 蒙特卡洛探索
- 策略性蒙特卡洛控制
- 脱离策略的蒙特卡洛控制
蒙特卡洛方法
蒙特卡洛方法通过随机采样找到近似解,也就是说,它通过运行多个踪迹来近似结果的概率。 通过抽样找到近似答案是一种统计技术。 让我们通过一个示例更好地直观地了解蒙特卡洛。
有趣的事实:蒙特卡洛以斯坦尼斯瓦夫·乌兰的叔叔的名字命名,他经常从亲戚那里借钱在蒙特卡洛赌场赌博。
使用蒙特卡洛估计pi的值
想象一下,将一个圆的象限放置在正方形内,如下所示,然后我们在正方形内生成一些随机点。 您会看到一些点落在圆内,而另一些点在圆外:

我们可以这样写:

我们知道一个圆的面积是πr^2,一个正方形的面积是a^2:

让我们考虑一个圆的半径是一半,而正方形的边是1,因此我们可以替换为:

现在我们得到以下内容:

估计π的步骤非常简单:
- 首先,我们在正方形内生成一些随机点。
- 然后,我们可以使用公式
x^2 + y^2 <= size计算落入圆内的点数。 - 然后,我们通过将圆内的点数除以平方内的点数的四倍来计算π的值。
- 如果我们增加样本数(随机点数),则可以更好地近似
让我们逐步了解如何在 Python 中执行此操作。 首先,我们导入必要的库:
import numpy as np
import math
import random
import matplotlib.pyplot as plt
%matplotlib inline
现在,我们初始化圆和正方形内的正方形大小和点数。 我们还初始化了样本大小,该样本大小表示要生成的随机点数。 我们定义arc,它基本上是圆象限:
square_size = 1
points_inside_circle = 0
points_inside_square = 0
sample_size = 1000
arc = np.linspace(0, np.pi/2, 100)
然后,我们定义一个名为generate_points()的函数,该函数在正方形内部生成随机点:
def generate_points(size):
x = random.random()*size
y = random.random()*size
return (x, y)
我们定义了一个名为is_in_circle()的函数,该函数将检查生成的点是否在圆内:
def is_in_circle(point, size):
return math.sqrt(point[0]**2 + point[1]**2) <= size
然后定义一个用于计算π值的函数:
def compute_pi(points_inside_circle, points_inside_square):
return 4 * (points_inside_circle / points_inside_square)
然后对于样本数,我们在正方形内生成一些随机点,并增加points_inside_square变量,然后我们将检查所生成的点是否位于圆内。 如果是,那么我们增加points_inside_circle变量:
plt.axes().set_aspect('equal')
plt.plot(1*np.cos(arc), 1*np.sin(arc))
for i in range(sample_size):
point = generate_points(square_size)
plt.plot(point[0], point[1], 'c.')
points_inside_square += 1
if is_in_circle(point, square_size):
points_inside_circle += 1
现在,我们使用compute_pi()函数计算π的值,该函数将打印出大约π的值:
print("Approximate value of pi is {}" .format(calculate_pi(points_inside_circle, points_inside_square)))
如果运行该程序,将得到如下所示的输出:
Approximate value of pi is 3.144

完整的程序如下所示:
import numpy as np
import math
import random
import matplotlib.pyplot as plt
%matplotlib inline
square_size = 1
points_inside_circle = 0
points_inside_square = 0
sample_size = 1000
arc = np.linspace(0, np.pi/2, 100)
def generate_points(size):
x = random.random()*size
y = random.random()*size
return (x, y)
def is_in_circle(point, size):
return math.sqrt(point[0]**2 + point[1]**2) <= size
def compute_pi(points_inside_circle, points_inside_square):
return 4 * (points_inside_circle / points_inside_square)
plt.axes().set_aspect('equal')
plt.plot(1*np.cos(arc), 1*np.sin(arc))
for i in range(sample_size):
point = generate_points(square_size)
plt.plot(point[0], point[1], 'c.')
points_inside_square += 1
if is_in_circle(point, square_size):
points_inside_circle += 1
print("Approximate value of pi is {}" .format(calculate_pi(points_inside_circle, points_inside_square)))
因此,蒙特卡洛方法通过使用随机采样来近似pi的值。 我们使用正方形内部生成的随机点(样本)估计pi的值。 采样量越大,我们的近似值越好。 现在,我们将看到如何在 RL 中使用蒙特卡洛方法。
蒙特卡洛预测
在 DP 中,我们通过使用值迭代和策略迭代来解决马尔可夫决策过程(MDP)。 这两种技术都需要转换和奖励概率才能找到最佳策略。 但是,当我们不知道转移和奖励概率时,如何解决 MDP? 在这种情况下,我们使用蒙特卡洛方法。 蒙特卡洛方法仅需要状态,动作和奖励的样本序列。 蒙特卡罗方法仅适用于剧集任务。 由于蒙特卡洛不需要任何模型,因此称为无模型学习算法。
蒙特卡洛方法的基本思想非常简单。 您还记得我们在上一章第 3 章,“马尔可夫决策过程和动态规划”中如何定义最佳值函数以及如何得出最佳策略吗?
值函数基本上是状态S与策略π的预期收益。 在这里,我们使用均值回报代替预期回报。
因此,在蒙特卡洛预测中,我们通过取均值回报而不是期望回报来近似值函数。
使用蒙特卡洛预测,我们可以估计任何给定策略的值函数。 蒙特卡洛预测中涉及的步骤非常简单,如下所示:
- 首先,我们将随机值初始化为我们的值函数
- 然后我们初始化一个称为
return的空列表来存储我们的回报 - 然后针对剧集中的每个状态,我们计算收益
- 接下来,我们将回报附加到回报清单中
- 最后,我们将收益平均值作为我们的值函数
以下流程图使其更简单:

蒙特卡洛预测算法有两种类型:
- 首次访问蒙特卡洛
- 每次访问蒙特卡洛
首次访问蒙特卡洛
如我们所见,在蒙特卡洛方法中,我们通过取平均收益来近似值函数。 但是在首次访问 MC 方法中,我们仅在剧集中首次访问状态时才对返回值进行平均。 例如,假设一个智能体正在玩蛇和梯子游戏,那么如果该智能体被蛇咬伤,它很有可能会返回到该状态。 当智能体重新访问状态时,我们不考虑平均回报。 我们仅在智能体首次访问该状态时才考虑平均回报。
每次访问蒙特卡洛
在蒙特卡洛的每次访问中,我们平均将剧集中每次访问状态的收益均值化。 考虑相同的蛇和梯子游戏示例:如果智能体在蛇咬之后返回到相同的状态,尽管智能体正在重新访问状态,但我们可以将其视为平均收益。 在这种情况下,我们平均每次智能体访问该状态时的回报。
让我们使用蒙特卡洛玩二十一点
现在,让我们通过二十一点游戏更好地了解蒙特卡洛。 二十一点,也称为 21,是在赌场玩的一种流行的纸牌游戏。 游戏的目标是使您的所有牌的总和接近 21 并且不超过 21。牌 J,K 和 Q 的值为 10。王牌的值为 1 或 11;王牌的值为 1 或 11。 这取决于玩家的选择。 其余卡(1 至 10)的值与它们显示的数字相同。
游戏规则非常简单:
-
可以与一个或多个玩家和一个发牌人一起玩游戏。
-
每个玩家仅与庄家竞争,而不与其他玩家竞争。
-
最初,给玩家一张两张牌。 这两个卡都面朝上,即对其他人可见。
-
庄家也得到了两张牌。 一张卡面朝上,另一张面朝下。 也就是说,发牌人只显示他的一张牌。
-
如果在收到两张牌后,一张牌的总和为 21(例如,一位牌手已收到
10 + 11 = 21的杰克和王牌),则称其为自然或黑杰克,玩家获胜。 -
如果发牌人在收到两张卡后立即的总卡数也为 21,则称为两可(Draw),因为它们两张都有 21 张。
-
在每个回合中,玩家决定是否需要另一张纸牌来总计接近 21 张纸牌。
-
如果玩家需要纸牌,则称其为拿牌(Hit)。
-
如果玩家不需要纸牌,则称为停牌(Stand)。
-
如果玩家的纸牌总数超过 21,则称为胀死(Bust); 那么发牌者将赢得比赛。
让我们通过玩来更好地了解二十一点。 我让你成为玩家,我是智能体:

在上图中,我们有一个参与者和一个庄家。 他们两个都有两张卡。 玩家的两张牌都朝上(可见),而发牌者的一张牌朝上(可见),另一张牌朝下(不可见)。 在第一轮中,您得到了两张卡,例如一个千斤顶和一个数字 7,即(10 + 7 = 17),而我作为发牌人只会向您显示一张数字 2。 面朝下。 现在,您必须决定要击中(需要另一张牌)还是站起来(不需要另一张牌)。 如果您选择击中并接收数字 3,您将获得10 + 7 + 3 = 20(接近 21),您将获胜:

但是,如果您收到一张卡,说出数字 7,则10 + 7 + 7 = 24,超过了 21。然后被称为破产,您输了游戏。 如果您决定站立使用初始卡,则只有10 + 7 = 17。然后我们将检查智能体的卡总和。 如果它大于 17 且不超过 21,则智能体赢,否则您赢:

这里的奖励是:
- 如果玩家赢得比赛,则为 +1
- 如果玩家输了则为 -1
- 如果游戏是平局,则为 0
可能的操作是:
- 拿牌:如果玩家需要纸牌
- 停牌:如果玩家不需要纸牌
玩家必须决定一张王牌的值。 如果玩家的纸牌总数为 10,并且在击中后获得一张王牌,则可以将其视为 11,而10 + 11 = 21。但是,如果玩家的纸牌总数为 15,并且在击中后,则获得一张王牌。 ,如果他将其视为 11 且15 + 11 = 26,则表示破产。 如果玩家拥有一张王牌,我们可以将其称为可用王牌; 玩家可以将其视为 11,而不会破产。 如果玩家通过将王牌视为 11 来破产,则称为不可用王牌。
现在,我们将看到如何使用首次访问的蒙特卡洛算法来实现二十一点。
首先,我们将导入必要的库:
import gym
from matplotlib import pyplot
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from collections import defaultdict
from functools import partial
%matplotlib inline
plt.style.use('ggplot')
现在,我们将使用 OpenAI 的 Gym 创建二十一点环境:
env = gym.make('Blackjack-v0')
然后我们定义策略函数,该函数采用当前状态并检查分数是否大于或等于2o; 如果是,则返回0,否则返回1。 也就是说,如果得分大于或等于20,我们将保持(0)或达到(1):
def sample_policy(observation):
score, dealer_score, usable_ace = observation
return 0 if score >= 20 else 1
现在,我们将了解如何生成剧集。 剧集是游戏的一个回合。 我们将逐步看到它,然后看完整函数。
我们将状态,动作和奖励定义为列表,并使用env.reset初始化环境并存储观察变量:
states, actions, rewards = [], [], []
observation = env.reset()
然后,执行以下操作,直到到达最终状态,即直到剧集结束为止:
- 将观察值追加到状态列表:
states.append(observation)
- 现在,我们使用
sample_policy函数创建一个动作,并将这些动作附加到action列表中:
action = sample_policy(observation)
actions.append(action)
- 然后,对于环境中的每个步骤,我们都存储
state,reward和done(指定是否达到最终状态),并将奖励添加到reward列表中:
observation, reward, done, info = env.step(action)
rewards.append(reward)
- 如果我们到达最终状态,那么我们将中断:
if done:
break
- 完整的
generate_episode函数如下:
def generate_episode(policy, env):
states, actions, rewards = [], [], []
observation = env.reset()
while True:
states.append(observation)
action = policy(observation)
actions.append(action)
observation, reward, done, info = env.step(action)
rewards.append(reward)
if done:
break
return states, actions, rewards
这就是我们生成剧集的方式。 我们如何玩游戏? 为此,我们需要知道每个状态的值。 现在,我们将看到如何使用首次访问蒙特卡洛方法获取每个状态的值。
首先,我们将空值表初始化为用于存储每个状态的值的字典:
value_table = defaultdict(float)
然后,对于一定数量的剧集,我们执行以下操作:
- 首先,我们生成剧集并存储状态和奖励; 我们将回报初始化为
0,这是奖励的总和:
states, _, rewards = generate_episode(policy, env)
returns = 0
- 然后,对于每个步骤,我们将奖励存储到变量
R中,并声明为S,然后将收益计算为奖励总和:
for t in range(len(states) - 1, -1, -1):
R = rewards[t]
S = states[t]
returns += R
- 现在,我们进行首次访问蒙特卡洛; 我们会在访问时间内检查是否正在访问该剧集。 如果是的话,我们只取收益的平均值并将状态值指定为收益的平均值:
if S not in states[:t]:
N[S] += 1
value_table[S] += (returns - V[S]) / N[S]
- 查看完整函数来更好地理解:
def first_visit_mc_prediction(policy, env, n_episodes):
value_table = defaultdict(float)
N = defaultdict(int)
for _ in range(n_episodes):
states, _, rewards = generate_episode(policy, env)
returns = 0
for t in range(len(states) - 1, -1, -1):
R = rewards[t]
S = states[t]
returns += R
if S not in states[:t]:
N[S] += 1
value_table[S] += (returns - V[S]) / N[S]
return value_table
- 我们可以得到每个状态的值:
value = first_visit_mc_prediction(sample_policy, env, n_episodes=500000)
- 让我们看看一些状态的值:
print(value)
defaultdict(float,
{(4, 1, False): -1.024292170184644,
(4, 2, False): -1.8670191351012455,
(4, 3, False): 2.211363314854649,
(4, 4, False): 16.903201033000823,
(4, 5, False): -5.786238030898542,
(4, 6, False): -16.218211752577602,
我们还可以绘制状态值以查看其收敛方式,如下所示:

完整的代码如下:
import numpy
import gym
from matplotlib import pyplot
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from collections import defaultdict
from functools import partial
%matplotlib inline
plt.style.use('ggplot')
## Blackjack Environment
env = gym.make('Blackjack-v0')
env.action_space, env.observation_space
def sample_policy(observation):
score, dealer_score, usable_ace = observation
return 0 if score >= 20 else 1
def generate_episode(policy, env):
states, actions, rewards = [], [], []
observation = env.reset()
while True:
states.append(observation)
action = sample_policy(observation)
actions.append(action)
observation, reward, done, info = env.step(action)
rewards.append(reward)
if done:
break
return states, actions, rewards
def first_visit_mc_prediction(policy, env, n_episodes):
value_table = defaultdict(float)
N = defaultdict(int)
for _ in range(n_episodes):
states, _, rewards = generate_episode(policy, env)
returns = 0
for t in range(len(states) - 1, -1, -1):
R = rewards[t]
S = states[t]
returns += R
if S not in states[:t]:
N[S] += 1
value_table[S] += (returns - value_table[S]) / N[S]
return value_table
def plot_blackjack(V, ax1, ax2):
player_sum = numpy.arange(12, 21 + 1)
dealer_show = numpy.arange(1, 10 + 1)
usable_ace = numpy.array([False, True])
state_values = numpy.zeros((len(player_sum),
len(dealer_show),
len(usable_ace)))
for i, player in enumerate(player_sum):
for j, dealer in enumerate(dealer_show):
for k, ace in enumerate(usable_ace):
state_values[i, j, k] = V[player, dealer, ace]
X, Y = numpy.meshgrid(player_sum, dealer_show)
ax1.plot_wireframe(X, Y, state_values[:, :, 0])
ax2.plot_wireframe(X, Y, state_values[:, :, 1])
for ax in ax1, ax2:
ax.set_zlim(-1, 1)
ax.set_ylabel('player sum')
ax.set_xlabel('dealer showing')
ax.set_zlabel('state-value')
fig, axes = pyplot.subplots(nrows=2, figsize=(5, 8), subplot_kw={'projection': '3d'})
axes[0].set_title('value function without usable ace')
axes[1].set_title('value function with usable ace')
plot_blackjack(value, axes[0], axes[1])
蒙特卡洛控制
在蒙特卡洛预测中,我们已经看到了如何估计值函数。 在蒙特卡洛控制中,我们将看到如何优化值函数,即如何使值函数比估计值更准确。 在控制方法中,我们遵循一种称为通用策略迭代的新型迭代,其中策略评估和策略改进彼此交互。 它基本上是在策略评估和改进之间循环的,也就是说,相对于值函数而言,策略总是会得到改进,而根据策略,值函数总是会得到改善。 它继续这样做。 当没有变化时,可以说策略和值函数已经达到收敛,即发现了最优值函数和最优策略:

现在,我们将看到如下不同的蒙特卡洛控制算法。
蒙特卡洛探索
与 DP 方法不同,这里我们不估计状态值。 相反,我们专注于动作值。 当我们知道环境模型时,仅状态值就足够了。 由于我们不了解模型动态,因此这不是单独确定状态值的好方法。
估计动作值比估计状态值更直观,因为状态值根据我们选择的策略而变化。 例如,在二十一点游戏中,假设我们处于某些纸牌为 20 的状态。该状态的值是什么? 这完全取决于策略。 如果我们选择策略作为命中目标,那将不是一个好的状态,而且此状态的值非常低。 但是,如果我们选择我们的策略作为立场,那肯定是一个好的国家。因此,状态的值取决于我们选择的策略。 因此,估计操作的值而不是状态的值更为重要。
我们如何估计作用值? 还记得我们在第 3 章,“马尔可夫决策过程和动态规划”中学习的Q函数吗? 表示为Q(s, a)的Q函数用于确定特定状态下的动作有多好。 它基本上指定了状态-动作对。
但是,这里出现了探索的问题。如果我们还没有处于状态状态值,我们如何知道状态状态值? 如果我们不采取所有可能的措施探索所有状态,我们可能会错过丰厚的回报。
假设在二十一点游戏中,我们处于纸牌总数为 20 的状态。如果我们仅尝试拿牌动作,我们将获得负数奖励,我们将得到负的奖励。但是,如果我们尝试停牌动作,我们将获得积极的回报,这实际上是最好的状态。因此,每次进入此特定状态 ,我们站立而不是受到打击。 为了让我们知道哪个是最佳操作,我们必须探索每种状态下的所有可能操作以找到最佳值。 我们应该怎么做?
让我介绍一个名为探索起始的蒙特卡洛,这意味着对于每个剧集,我们都将随机状态作为初始状态开始并执行操作。 因此,如果我们有大量的剧集,我们可能会以所有可能的动作覆盖所有状态。 它也称为 MC-ES 算法。
MC-ES 算法非常简单,如下所示:
-
我们首先使用一些随机值初始化
Q函数和策略,然后我们初始化返回到空列表的过程 -
然后,我们以随机初始化的策略开始
-
然后,我们计算该剧集中发生的所有唯一状态操作对的收益,并将收益附加到我们的收益清单中
-
我们只为唯一的状态-动作对计算返回值,因为同一状态-动作对多次出现在剧集中,并且没有多余的信息点
-
然后,我们对返回列表中的返回值取平均值,然后将该值分配给我们的
Q函数 -
最后,我们将为一个状态选择一个最佳策略,为该状态选择具有最大
Q(s, a)的操作 -
我们将永久重复整个过程,或者重复进行多次,以便涵盖所有不同的状态和动作对
这是一个流程图:

策略性蒙特卡洛控制
在开始进行蒙特卡洛探索时,我们将探索所有状态-动作对,并选择能给我们带来最大值的对。 但是考虑一下我们拥有大量状态和行动的情况。 在这种情况下,如果我们使用 MC-ES 算法,则将花费大量时间来探索状态和动作的所有组合并选择最佳状态和动作。 我们如何克服这个问题? 有两种不同的控制算法。 上策略和下策略。 在基于策略的蒙特卡洛控制中,我们使用ε贪婪策略。 让我们了解什么是贪婪算法。
贪婪的算法会选择当时可用的最佳选择,尽管当您考虑整个问题时,该选择可能不是最佳选择。 考虑您要从数字列表中找到最小的数字。 您可以将列表分为三个子列表,而不是直接从列表中找到最小的数字。 然后,您将在每个子列表中找到最小的数字(局部最优值)。 考虑整个列表时,在一个子列表中找到的最小数目可能不是最小数目(全局最优)。 但是,如果您表现得很贪婪,那么您将仅在当前子列表中看到最小的数字(此刻),然后将其视为最小的数字。
贪婪策略表示在所探索的行动中的最佳行动。 最佳动作是具有最高值的动作。
假设我们已经探索了状态 1 中的一些动作,如 Q 表所示:
| 状态 | 动作 | 值 |
|---|---|---|
| 状态 1 | 动作 0 | 0.5 |
| 状态 1 | 动作 1 | 0.1 |
| 状态 1 | 动作 2 | 0.8 |
如果我们表现出贪婪的态度,那么我们将从所有探索的动作中挑选出具有最大值的动作。 在前面的情况中,我们具有较高值的动作 2,因此我们选择该动作。 但是状态 1 中可能还有其他我们尚未探讨的动作,可能值最高。 因此,我们必须在所有探索的动作中寻找最佳动作或利用最佳动作。 这就是所谓的探索利用困境。 假设您听过爱德·希兰(Ed Sheeran),并且非常喜欢他,所以您因为喜欢音乐而一直只听(探索)爱德·希兰(Ed Sheeran)。 但是,如果您尝试听其他艺术家的音乐,您可能会喜欢比 Ed Sheeran(探索)更好的人。 关于您是否只需要听 Ed Sheeran(利用)还是尝试听不同的艺术家以查看您是否喜欢他们(利用)的这种困惑称为探索利用难题。
因此,为了避免这种困境,我们引入了一种称为ε贪婪策略的新策略。 在这里,所有动作均以非零概率(ε)进行尝试。 对于概率ε,我们随机地探索不同的动作,而对于概率 1,我们选择具有最大值的动作,即,我们不进行任何探索。 因此,我们不仅会以概率ε始终利用最佳行动,而且还会随机探索不同的行动。 如果epsilon的值设置为零,那么我们将不做任何探索。 这只是贪婪的策略,如果将epsilon的值设置为 1,则它将始终仅进行探索。epsilon的值会随着时间的流逝而衰减,因为我们不想永远探索。 因此,随着时间的流逝,我们的策略会采取良好的行动:

假设我们将epsilon的值设置为0.3。 在下面的代码中,我们从均匀分布中生成一个随机值,如果该值小于epsilon值,即 0.3,则选择一个随机动作(以这种方式,我们搜索一个不同的动作)。 如果来自均匀分布的随机值大于 0.3,则我们选择具有最佳值的操作。 因此,通过这种方式,我们以概率epsilon探索了以前从未见过的动作,并从概率为 1 epsilon的探索动作中选择了最佳动作:
def epsilon_greedy_policy(state, epsilon):
if random.uniform(0,1) < epsilon:
return env.action_space.sample()
else:
return max(list(range(env.action_space.n)), key = lambda x: q[(state,x)])
让我们想象一下,我们已经使用ε贪婪策略探索了状态 1 中的其他动作(尽管不是所有动作对),并且我们的 Q 表如下所示:
| 状态 | 动作 | 值 |
|---|---|---|
| 状态 1 | 动作 0 | 0.5 |
| 状态 1 | 动作 1 | 0.1 |
| 状态 1 | 动作 2 | 0.8 |
| 状态 1 | 动作 4 | 0.93 |
在状态 1 中,动作 4 具有比我们之前发现的动作 2 高的值。 因此,在ε贪婪策略下,我们以概率epsilon寻找不同的动作,并以概率为 1 epsilon寻找最佳动作。
策略性蒙特卡洛方法涉及的步骤非常简单:
-
首先,我们初始化随机策略和随机 Q 函数。
-
然后,我们初始化一个称为
return的列表,用于存储回报。 -
我们使用随机策略
π生成剧集。 -
我们将剧集中发生的每个状态操作对的返回存储到返回列表中。
-
然后,我们对返回列表中的返回值取平均值,然后将该值分配给
Q函数。 -
现在,由
ε决定由状态s选择动作a的可能性。 -
如果概率为
1-ε,我们将选择最大Q值的动作。 -
如果概率为
ε,我们将探索不同的动作。
脱离策略的蒙特卡洛控制
非策略性蒙特卡洛是另一种有趣的蒙特卡洛控制方法。 在这种方法中,我们有两种策略:一种是行为策略,另一种是目标策略。 在非策略方法中,智能体遵循一项策略,但与此同时,它会尝试学习和改进另一种策略。 智能体遵循的策略称为行为策略,智能体尝试评估和改进的策略称为目标策略。 行为与目标策略完全无关。 行为策略探索所有可能的状态和动作,这就是为什么将行为策略称为软策略,而将目标策略称为贪心策略(它选择具有最大值的策略)的原因。
我们的目标是为目标策略π估计Q函数,但我们的智能体使用完全不同的策略(称为行为策略μ)进行操作。 我们现在能做什么? 我们可以通过使用μ中发生的常见事件来估计π的值。 我们如何估计这两个策略之间的共同点? 我们使用一种称为重要性抽样的新技术。 这是一种在给定来自另一个分布的样本的情况下从一个分布估计值的技术。
重要抽样有两种类型:
- 普通重要性抽样
- 加权重要性抽样
在普通重要性抽样中,我们基本上采用行为策略和目标策略获得的收益之比,而在加权重要性抽样中,我们采用加权平均值,而C是权重的累加和。
让我们一步一步看一下:
-
首先,我们将
Q(s, a)初始化为随机值,并将C(s, a)初始化为0,权重w为1。 -
然后我们选择目标策略,这是一个贪婪策略。 这意味着它将从
Q表中选取具有最大值的策略。 -
我们选择行为策略。 行为策略不是贪婪的,它可以选择任何状态-行为对。
-
然后,我们开始我们的剧集,并根据我们的行为策略在
s状态下执行动作a,并存储奖励。 我们重复此操作直到剧集结束。 -
现在,对于剧集中的每个状态,我们执行以下操作:
-
我们将计算回报
G。 我们知道回报是折扣奖励的总和:G = 折扣因子 * G + 奖励。 -
然后我们将
C(s, a)更新为C(s, a) + C(s, a) + w。 -
我们更新
Q(s, a):![]()
-
我们更新
w的值:![]()
-
总结
在本章中,我们了解了蒙特卡洛方法的工作原理,以及当我们不了解环境模型时如何使用它来解决 MDP。 我们研究了两种不同的方法:一种是用于估计值函数的蒙特卡洛预测,另一种是用于优化值函数的蒙特卡洛控制。
我们在蒙特卡洛预测中研究了两种不同的方法:首次访问蒙特卡洛预测,其中只有在剧集中首次访问该状态时,我们才对收益进行平均;以及每次访问蒙特卡洛方法,其中每次在剧集中访问状态时,我们都将收益平均。
在蒙特卡洛控制方面,我们研究了不同的算法。 我们首先遇到了 MC-ES 控件,该控件用于覆盖所有状态-动作对。 我们研究了策略上的 MC 控制(它使用ε贪婪策略)和策略外的 MC 控制(一次使用两个策略)。
在下一章第 5 章,“时间差异学习”中,我们将介绍一种不同的无模型学习算法。
问题
问题列表如下:
- 什么是蒙特卡洛方法?
- 使用蒙特卡洛方法估计黄金分割率的值。
- 蒙特卡洛预测的用途是什么?
- 首次访问 MC 和每次访问 MC 有什么区别?
- 为什么我们要估计状态作用值?
- 策略上的 MC 控制和策略外的 MC 控制有什么区别?
- 编写一些 Python 代码,以使用策略性 MC 控件玩二十一点游戏。
进一步阅读
请参考以下链接:
五、时间差异学习
在上一章第四章,“使用蒙特卡洛方法的游戏”中,我们了解了有趣的蒙特卡洛方法,该方法用于解决马尔可夫决策过程(MDP),而不像动态规划那样预先未知环境的模型动态。 我们研究了蒙特卡洛预测方法,该方法用于预测值函数,而控制方法用于进一步优化值函数。 但是蒙特卡洛方法存在一些陷阱。 它仅适用于情景任务。 如果剧集很长,那么我们必须等待很长时间才能计算值函数。 因此,我们将使用另一种有趣的算法,称为时差(TD)学习,这是一种无模型的学习算法:不需要事先知道模型动态,它也可以用于非临时性任务。
在本章中,您将学习:
- TD 学习
- Q 学习
- SARSA
- 使用 Q 学习和 SARSA 调度出租车
- Q 学习和 SARSA 之间的区别
TD 学习
TD 学习算法由 Sutton 于 1988 年提出。该算法兼顾了蒙特卡洛方法和动态规划(DP)的优点。 像蒙特卡洛方法一样,它不需要模型动态,而像 DP 一样,它不需要等到剧集结束就可以估计值函数。 取而代之的是,它基于先前学习的估计值来估计当前估计值,这也称为自举。 如果您在蒙特卡洛方法中看到没有引导程序,那么我们仅在剧集结束时进行估计,但在 TD 方法中我们可以进行引导。
TD 预测
就像我们在蒙特卡洛预测中所做的一样,在 TD 预测中,我们尝试预测状态值。 在蒙特卡洛预测中,我们仅通过取均值收益来估计值函数。 但是在 TD 学习中,我们通过当前状态更新先前状态的值。 我们应该怎么做? TD 学习使用一种称为 TD 更新规则的东西来更新状态值,如下所示:

先前状态的值 = 先前状态的值 + 学习率(奖励 + 折扣因子(当前状态的值)- 先前状态的值)
这个方程实际上是什么意思?
如果您直观地想到此方程,则实际上是实际奖励(r + γV(s'))和预期奖励(V(s))之间的差乘以学习率α。 学习率代表什么? 学习率(也称为步长)对于收敛很有用。
你注意到了吗? 由于我们将实际值和预测值之差作为r + γV(s') - V(s),因此实际上是一个误差。 我们可以称其为 TD 误差。 在多次迭代中,我们将尝试最小化此误差。
让我们通过前面几章中的冰湖示例来了解 TD 预测。 接下来显示的是冰冻的湖泊环境。 首先,对于所有状态,我们将值函数初始化为0,就像在V(S)中将其初始化为0一样,如以下状态值图所示 :

假设我们处于s = (1, 1)的初始状态,我们将采取正确的操作并移至下一个状态s' = (1, 2),并获得 -0.3 的奖励(r)。 我们如何使用此信息来更新状态的值?
回忆一下 TD 更新公式:

让我们将学习率(α)视为0.1,将折扣率(γ)视为 0.5; 我们知道状态(1, 1)的值(如V(S)中的值)为 0,而下一个状态(1, 2)的值与V(S)一样,也是0。 我们获得的奖励(r)为 -0.3。 我们将其替换为 TD 规则,如下所示:
V(s) = 0 + 0.1 * (-0.3 + 0.5 (0) - 0)
V(s) = -0.03
因此,我们在值表中将状态(1, 1)的值更新为-0.03,如下图所示:

现在我们以(1, 2)的状态处于s的状态,我们将采取正确的操作并移至下一个状态s' = (1, 3)并获得奖励r = -0.3。 我们现在如何更新状态(1, 2)的值?
像我们之前所做的那样,我们将 TD 更新方程中的值替换为:
V(s) = 0 + 0.1 * (-0.3 + 0.5(0) - 0)
V(s) = -0.03
因此,我们将状态(1, 2)的值设置为-0.03,并在值表中对其进行更新,如下所示:

现在我们处于s = (1, 3)状态; 假设我们要采取行动了。 我们再次回到该状态s' = (1, 2),我们将获得奖励r = -0.3。 此处,状态(1, 3)的值为0,下一个状态(1, 2)的值为值表中的-0.03。
现在我们可以更新状态(1, 3)的值,如下所示:
V(s) = 0 + 0.1 * (-0.3 + 0.5 * (-0.03) - 0))
V(s) = 0.1 * -0.315
V(s) = -0.0315
因此,我们在值表中将状态(1, 3)的值更新为-0.0315,如下所示:

以类似的方式,我们使用 TD 更新规则更新所有状态的值。 TD 预测算法涉及的步骤如下:
- 首先,我们将
V(S)初始化为0或一些任意值 - 然后我们开始该剧集,并在剧集中的每个步骤中,在状态
S中执行动作A,并获得奖励R,然后移至下一个状态s' - 现在,我们使用 TD 更新规则更新先前状态的值
- 重复步骤
3和4,直到达到最终状态
TD 控制
在 TD 预测中,我们估计了值函数。 在 TD 控制中,我们优化了值函数。 对于 TD 控制,我们使用两种控制算法:
- 无策略学习算法:Q 学习
- 策略学习算法:SARSA
Q 学习
现在,我们将研究称为 Q 学习的非常流行的非策略性 TD 控制算法。 Q 学习是一种非常简单且广泛使用的 TD 算法。 在控制算法中,我们不在乎状态值。 在这里,在 Q 学习中,我们关心的是状态-动作值对-在状态S下执行动作A的效果。
我们将根据以下公式更新Q值:

前面的公式与 TD 预测更新规则相似,只是有一点点差异。 我们将逐步详细介绍这一点。 Q 学习涉及的步骤如下:
- 首先,我们将
Q函数初始化为一些任意值 - 我们使用
ε贪婪策略(ε > 0)从某个状态采取了一项行动,并将其移至新的状态 - 我们通过遵循更新规则来更新先前状态的
Q值:

- 重复步骤
2和3,直到达到最终状态
现在,我们将使用不同的步骤来理解算法。
考虑相同的冻湖示例。 假设我们处于状态(3, 2),并且有两个动作(左和右)。 现在让我们参考该图,并将其与ε贪婪策略进行比较:

在“Q 学习”中,我们使用ε贪婪策略选择一个动作。 我们要么探索概率为ε的新动作,要么选择概率为1ε的最佳动作。 假设我们选择一个概率ε,并探索一个新的动作向下,然后选择该动作:

现在,我们已经对状态(3, 2)执行了向下操作,并使用ε贪婪策略达到了新状态(4, 2),我们如何使用我们的更新规则来更新先前状态(3, 2)的值? 这很简单。 查看Q表,如下所示:

让我们将alpha视为0.1,并将折扣因子视为1:

Q( (3,2) down) = Q( (3,2), down ) + 0.1 ( 0.3 + 1 max [Q( (4,2) action) ]- Q( (3,2), down)
我们可以说具有向下作用的状态(3, 2)的值,例如Q((3, 2), down)的值为Q表中的0.8。
状态(4, 2)的最大值Q((4, 2), op)是什么? 我们仅研究了三个动作(向上,向下和向右),因此我们将仅基于这些动作来获取最大值。 (此处,我们将不执行epsilon贪婪策略;我们仅选择具有最大值的操作。)
因此,基于先前的Q表,我们可以将值替换为:
Q( (3,2), down) = 0.8 + 0.1 ( 0.3 + 1 max [0.3, 0.5, 0.8] - 0.8 )
= 0.8 + 0.1 ( 0.3 + 1 (0.8) - 0.8)
= 0.83
因此,我们将Q((3, 2), down)的值更新为0.83。
请记住,在选择要采取的操作时,我们将执行ε贪婪策略:我们要么探索具有概率epsilon的新操作,要么采取具有最大值的概率 1 epsilon。 在更新 Q 值时,我们不执行ε贪婪策略,我们仅选择具有最大值的操作。
现在我们处于状态(4, 2),我们必须执行一个动作。 我们应该执行什么动作? 我们决定基于ε贪婪策略,要么探索具有概率epsilon的新操作,要么选择具有概率 1-epsilon 的最佳操作。 假设我们选择概率1-ε,然后选择最佳操作。 因此,在(4, 2)中,向右的操作具有最大值。 因此,我们将选择向右操作:

现在我们处于状态(4, 3),因为我们对状态(4, 2)采取了向右动作。 我们如何更新先前状态的值? 像这样:
Q( (4,2), right) = Q( (4,2), right ) + 0.1 ( 0.3 + 1 max [Q( (4,3) action) ]- Q( (4,2), right)
如果您查看下面的Q表,对于状态(4, 3),我们仅探讨了两个操作(向上和向下),因此我们仅根据这些操作得出最大值。 (这里,我们将不执行ε贪婪策略;我们只选择具有最大值的操作):
Q ( (4,2), right) = Q((4,2),right) + 0.1 (0.3 + 1 max [ (Q (4,3), up) , ( Q(4,3),down) ] - Q ((4,2), right )
Q ( (4,2), right) = 0.8 + 0.1 (0.3 + 1 max [ 0.1,0.3] - 0.8)
= 0.8 + 0.1 (0.3 + 1(0.3) - 0.8)
= 0.78
查看下面的Q表:

现在我们将状态Q((4,2), right)的值更新为0.78。
因此,这就是我们在 Q 学习中获得状态作用值的方式。 为了决定采取什么行动,我们使用ε贪婪策略,并在更新Q值时,我们只选择最大的行动; 这是流程图:

使用 Q 学习解决出租车问题
为了演示该问题,我们假设智能体是驱动程序。 有四个地点,智能体必须在一个地点接客并在另一地点下车。 智能体将获得 +20 积分作为成功下车的奖励,而每走一步便获得 -1 积分。 该智能体还将因非法取送丢掉 -10 分。 因此,我们智能体的目标是学会在短时间内在正确的位置上落客而不增加非法乘客。
这里显示的环境中,字母(R,G,Y,B)代表不同的位置,并且一个小矩形是驾驶出租车的智能体:

让我们看一下编码部分:
import gym
import random
现在,我们使用gym创建环境:
env = gym.make("Taxi-v1")
这种出租车的环境如何? 像这样:
env.render()
好的,首先让我们初始化学习率alpha,epsilon值和gamma:
alpha = 0.4
gamma = 0.999
epsilon = 0.017
然后我们初始化一个 Q 表; 它有一个字典,将状态-操作值对存储为(状态,操作):
q = {}
for s in range(env.observation_space.n):
for a in range(env.action_space.n):
q[(s,a)] = 0.0
我们将通过 Q 学习更新规则定义用于更新 Q 表的函数; 如果您看下面的函数,您将看到我们采取的状态/动作对具有最大值的动作并将其存储在qa变量中。 然后,我们通过更新规则更新先前状态的Q值,如下所示:

def update_q_table(prev_state, action, reward, nextstate, alpha, gamma):
qa = max([q[(nextstate, a)] for a in range(env.action_space.n)])
q[(prev_state,action)] += alpha * (reward + gamma * qa -q[(prev_state,action)])
然后,我们定义一个函数以执行ε贪婪策略,并在其中传递状态和epsilon值。 我们生成一些均匀分布的随机数,如果该数小于epsilon,则在状态中探索不同的动作,否则我们将利用具有最大 q 值的动作:
def epsilon_greedy_policy(state, epsilon):
if random.uniform(0,1) < epsilon:
return env.action_space.sample()
else:
return max(list(range(env.action_space.n)), key = lambda x: q[(state,x)])
我们将结合所有这些函数,了解如何进行 Q 学习:
# For each episode
for i in range(8000):
r = 0
#first we initialize the environment
prev_state = env.reset()
while True:
#In each state we select action by epsilon greedy policy
action = epsilon_greedy_policy(prev_state, epsilon)
#then we take the selected action and move to the next state
nextstate, reward, done, _ = env.step(action)
#and we update the q value using the update_q_table() function
#which updates q table according to our update rule.
update_q_table(prev_state, action, reward, nextstate, alpha, gamma)
#then we update the previous state as next stat
prev_state = nextstate
#and store the rewards in r
r += reward
#If done i.e if we reached the terminal state of the episode
#if break the loop and start the next episode
if done:
break
print("total reward: ", r)
env.close()
完整的代码在这里给出:
import random
import gym
env = gym.make('Taxi-v1')
alpha = 0.4
gamma = 0.999
epsilon = 0.017
q = {}
for s in range(env.observation_space.n):
for a in range(env.action_space.n):
q[(s,a)] = 0
def update_q_table(prev_state, action, reward, nextstate, alpha, gamma):
qa = max([q[(nextstate, a)] for a in range(env.action_space.n)])
q[(prev_state,action)] += alpha * (reward + gamma * qa - q[(prev_state,action)])
def epsilon_greedy_policy(state, epsilon):
if random.uniform(0,1) < epsilon:
return env.action_space.sample()
else:
return max(list(range(env.action_space.n)), key = lambda x: q[(state,x)])
for i in range(8000):
r = 0
prev_state = env.reset()
while True:
env.render()
# In each state, we select the action by ε-greedy policy
action = epsilon_greedy_policy(prev_state, epsilon)
# then we perform the action and move to the next state, and
# receive the reward
nextstate, reward, done, _ = env.step(action)
# Next we update the Q value using our update_q_table function
# which updates the Q value by Q learning update rule
update_q_table(prev_state, action, reward, nextstate, alpha, gamma)
# Finally we update the previous state as next state
prev_state = nextstate
# Store all the rewards obtained
r += reward
#we will break the loop, if we are at the terminal
#state of the episode
if done:
break
print("total reward: ", r)
env.close()
SARSA
状态-动作-奖励-状态-动作(SARSA)是一种策略上的 TD 控制算法。 就像我们在 Q 学习中所做的一样,这里我们也关注状态-动作值,而不是状态-值对。 在 SARSA 中,我们根据以下更新规则更新 Q 值:

在前面的等式中,您可能会注意到,没有最大的Q(s', a'),就像在 Q 学习中一样。 这里只是Q(s', a')。 我们可以通过执行一些步骤来详细了解这一点。 SARSA 涉及的步骤如下:
- 首先,我们将
Q值初始化为一些任意值 - 我们通过
ε贪婪策略(ε > 0)选择一个动作,然后从一种状态转移到另一种状态 - 我们遵循更新规则
Q(s, a) = Q(s, a) + α(r + γQ(s', a') - Q(s, a))来更新Q值的先前状态,其中a'是由ε贪婪策略(ε > 0)选择的操作
现在,我们将逐步了解算法。 让我们考虑相同的冰冻湖的例子。 假设我们处于(4, 2)状态。 我们根据ε贪婪策略决定采取的措施。 假设我们使用概率为 1 epsilon并选择最佳操作,即向右:

现在,我们在状态(4, 2)下执行了向右动作之后,就处于(4, 3)状态。 我们如何更新先前状态(4, 2)的值? 让我们将alpha设为0.1,将奖励设为0.3和折扣系数1:

Q( (4,2), right) = Q( (4,2),right) + 0.1 ( 0.3 + 1 Q( (4,3), action)) - Q((4,2) , right)
我们如何选择Q((4, 3), action)的值? 在这里,与 Q 学习不同,我们不只是获取max Q((4, 3), action)。 在 SARSA 中,我们使用ε贪婪策略。
查看下面的 Q 表。 在状态(4, 3)中,我们探索了两个动作。 与 Q 学习不同,我们不会直接选择最大动作:

我们在这里也遵循ε贪婪策略。 我们要么以概率epsilon进行探索,要么以概率 1 epsilon进行利用。 假设我们选择概率ε并探索新的动作。 我们探索一个新动作向右,然后选择该动作:

Q ( (4,2), right) = Q((4,2),right) + 0.1 (0.3 + 1 (Q (4,3), right) - Q ((4,2), right )
Q ( (4,2), right) = 0.8 + 0.1 (0.3 + 1(0.9) - 0.8)
= 0.8 + 0.1 (0.3 + 1(0.9) - 0.8)
= 0.84
因此,这就是我们在 SARSA 中获取状态操作值的方式。 我们使用ε贪婪策略采取措施,并且在更新 Q 值的同时,我们使用ε贪婪策略采取措施。
下图说明了 SARSA 算法:

使用 SARSA 解决出租车问题
现在,我们将使用 SARSA 解决相同的出租车问题:
import gym
import random
env = gym.make('Taxi-v1')
另外,我们将初始化学习率gamma和epsilon。 Q 表有一个字典:
alpha = 0.85
gamma = 0.90
epsilon = 0.8
Q = {}
for s in range(env.observation_space.n):
for a in range(env.action_space.n):
Q[(s,a)] = 0.0
和往常一样,我们为探索定义了epsilon_greedy策略:
def epsilon_greedy(state, epsilon):
if random.uniform(0,1) < epsilon:
return env.action_space.sample()
else:
return max(list(range(env.action_space.n)), key = lambda x: Q[(state,x)])
现在,出现了实际的 SARSA 算法:
for i in range(4000):
#We store cumulative reward of each episodes in r
r = 0
#Then for every iterations, we initialize the state,
state = env.reset()
#then we pick up the action using epsilon greedy policy
action = epsilon_greedy(state,epsilon)
while True:
#Then we perform the action in the state and move the next state
nextstate, reward, done, _ = env.step(action)
#Then we pick up the next action using epsilon greedy policy
nextaction = epsilon_greedy(nextstate,epsilon)
#we calculate Q value of the previous state using our update rule
Q[(state,action)] += alpha * (reward + gamma * Q[(nextstate,nextaction)]-Q[(state,action)])
#finally we update our state and action with next action
# and next state
action = nextaction
state = nextstate
r += reward
#we will break the loop, if we are at the terminal
#state of the episode
if done:
break
env.close()
您可以运行该程序,然后查看 SARSA 如何找到最佳路径。
完整的程序在这里给出:
#Like we did in Q learning, we import necessary libraries and initialize environment
import gym
import random
env = gym.make('Taxi-v1')
alpha = 0.85
gamma = 0.90
epsilon = 0.8
#Then we initialize Q table as dictionary for storing the state-action values
Q = {}
for s in range(env.observation_space.n):
for a in range(env.action_space.n):
Q[(s,a)] = 0.0
#Now, we define a function called epsilon_greedy for performing action
#according epsilon greedy policy
def epsilon_greedy(state, epsilon):
if random.uniform(0,1) < epsilon:
return env.action_space.sample()
else:
return max(list(range(env.action_space.n)), key = lambda x: Q[(state,x)])
for i in range(4000):
#We store cumulative reward of each episodes in
r = 0
#Then for every iterations, we initialize the state,
state = env.reset()
#then we pick up the action using epsilon greedy policy
action = epsilon_greedy(state,epsilon)
while True:
#Then we perform the action in the state and move the next state
nextstate, reward, done, _ = env.step(action)
#Then we pick up the next action using epsilon greedy policy
nextaction = epsilon_greedy(nextstate,epsilon)
#we calculate Q value of the previous state using our update rule
Q[(state,action)] += alpha * (reward + gamma * Q[(nextstate,nextaction)]-Q[(state,action)])
#finally we update our state and action with next action
#and next state
action = nextaction
state = nextstate
r += reward
#we will break the loop, if we are at the terminal
#state of the episode
if done:
break
env.close()
Q 学习和 SARSA 之间的区别
Q 学习和 SARSA 对许多人来说总是很困惑。 让我们分解一下两者之间的差异。 在此处查看流程图:

您看得出来差别吗? 在 Q 学习中,我们使用ε贪婪策略采取行动,并且在更新 Q 值的同时,我们仅采取最大行动。 在 SARSA 中,我们使用ε贪婪策略采取措施,并且在更新 Q 值的同时,我们使用ε贪婪策略采取措施。
总结
在本章中,我们学习了一种克服了蒙特卡洛方法局限性的不同的无模型学习算法。 我们看到了预测和控制方法。 在 TD 预测中,我们根据下一个状态更新了状态的状态值。 在控制方法方面,我们看到了两种不同的算法:Q 学习和 SARSA。
问题
问题列表如下:
- TD 学习与蒙特卡洛方法有何不同?
- TD 误差到底是什么?
- TD 预测和控制之间有什么区别?
- 如何使用 Q 学习构建智能体?
- Q 学习和 SARSA 有什么区别?
进一步阅读
六、多臂老丨虎丨机问题
在前面的章节中,我们学习了强化学习(RL)的基本概念和几种 RL 算法,以及如何将 RL 问题建模为马尔可夫决策过程(MDP)。 我们还看到了用于解决 MDP 的不同的基于模型和无模型的算法。 在本章中,我们将看到 RL 中的经典问题之一,称为多臂老丨虎丨机(MAB)问题。 我们将看到什么是 MAB 问题,以及如何使用不同的算法解决该问题,然后了解如何使用 MAB 识别将接收大部分点击的正确广告横幅。 我们还将学习广泛用于构建推荐系统的上下文老丨虎丨机。
在本章中,您将了解以下内容:
- MAB 问题
ε贪婪算法- softmax 探索算法
- 置信区间上界算法
- 汤普森采样算法
- MAB 的应用
- 使用 MAB 识别正确的广告横幅
- 情境老丨虎丨机
MAB 问题
MAB 问题是 RL 中的经典问题之一。 MAB 实际上是一台老丨虎丨机,是一种在赌场玩的赌博游戏,您可以拉动手臂(杠杆)并根据随机生成的概率分布获得支出(奖励)。 一台老丨虎丨机称为单臂老丨虎丨机,当有多台老丨虎丨机时,称为多臂老丨虎丨机或 k 臂老丨虎丨机。
MAB 如下所示:

由于每台老丨虎丨机都通过自己的概率分布为我们提供奖励,因此我们的目标是找出哪台老丨虎丨机将在一段时间内为我们提供最大的累积奖励。 因此,智能体在每个时间步t上执行动作aₜ,即从投币游戏机中拉出一条手臂并获得奖励Rₜ和,我们智能体的目标是使累积奖励最大化。
我们将手臂Q(a)的值定义为通过拉动手臂获得的平均奖励:

因此,最优臂就是为我们提供最大累积奖励的臂,即:

我们智能体的目标是找到最佳手臂,并最大程度地减少后悔,这可以定义为了解k哪个手臂是最佳手臂的代价。 现在,我们如何找到最好的手臂? 我们应该探索所有武器还是选择已经给予我们最大累积奖励的武器? 这就是探索与利用的困境。 现在,我们将看到如何使用以下各种探索策略来解决这个难题:
- ε 贪婪策略
- Softmax 探索
- 置信区间上界算法
- 汤姆森采样技术
在继续之前,让我们在 OpenAI Gym 中安装bandit环境; 您可以通过在终端中键入以下命令来安装bandit环境:
git clone https://github.c[om/JKCoop](https://www.google.com/url?q=https://github.com/JKCooper2/gym-bandits.git&sa=D&ust=1529836954889000&usg=AFQjCNFpMNcU8k-62v6Bb0UZSngaldPxeg)[er2/gym-ba](https://www.google.com/url?q=https://github.com/JKCooper2/gym-bandits.git&sa=D&ust=1529836954889000&usg=AFQjCNFpMNcU8k-62v6Bb0UZSngaldPxeg)[ndits.g](https://www.google.com/url?q=https://github.com/JKCooper2/gym-bandits.git&sa=D&ust=1529836954889000&usg=AFQjCNFpMNcU8k-62v6Bb0UZSngaldPxeg)[it](https://www.google.com/url?q=https://github.com/JKCooper2/gym-bandits.git&sa=D&ust=1529836954889000&usg=AFQjCNFpMNcU8k-62v6Bb0UZSngaldPxeg)
cd gym-bandits
pip install -e .
安装后,让我们导入gym和gym_bandits:
import gym_bandits
import gym
现在我们将初始化环境; 我们使用带有十个臂的 MAB:
env = gym.make("BanditTenArmedGaussian-v0")
我们的行动空间将是 10,因为我们有 10 条手臂:
env.action_space
输出如下:
10
贪婪策略
我们已经学到了很多关于ε贪婪策略的知识。 在ε贪婪策略中,或者我们选择概率为 1 epsilon的最佳手臂,或者我们随机选择概率为epsilon的手臂:

现在,我们将看到如何使用ε贪婪策略选择最佳手臂:
- 首先,让我们初始化所有变量:
# number of rounds (iterations)
num_rounds = 20000
# Count of number of times an arm was pulled
count = np.zeros(10)
# Sum of rewards of each arm
sum_rewards = np.zeros(10)
# Q value which is the average reward
Q = np.zeros(10)
- 现在我们定义
epsilon_greedy函数:
def epsilon_greedy(epsilon):
rand = np.random.random()
if rand < epsilon:
action = env.action_space.sample()
else:
action = np.argmax(Q)
return action
- 开始拉动手臂:
for i in range(num_rounds):
# Select the arm using epsilon greedy
arm = epsilon_greedy(0.5)
# Get the reward
observation, reward, done, info = env.step(arm)
# update the count of that arm
count[arm] += 1
# Sum the rewards obtained from the arm
sum_rewards[arm]+=reward
# calculate Q value which is the average rewards of the arm
Q[arm] = sum_rewards[arm]/count[arm]
print( 'The optimal arm is {}'.format(np.argmax(Q)))
以下是输出:
The optimal arm is 3
softmax 探索算法
Softmax 探索(也称为玻尔兹曼探索)是用于找到最佳老丨虎丨机的另一种策略。 在ε贪婪策略中,我们等效地考虑所有非最佳分支,但是在 softmax 探索中,我们根据来自玻尔兹曼分布的概率选择一个分支。 选择手臂的概率由下式给出:

称为温度因子,它指定我们可以探索多少个随机臂。 当τ高时,将平等地探索所有手臂,但是当τ低时,将选择高回报手臂。 请看以下步骤:
- 首先,初始化变量:
# number of rounds (iterations)
num_rounds = 20000
# Count of number of times an arm was pulled
count = np.zeros(10)
# Sum of rewards of each arm
sum_rewards = np.zeros(10)
# Q value which is the average reward
Q = np.zeros(10)
- 现在我们定义
softmax函数:
def softmax(tau):
total = sum([math.exp(val/tau) for val in Q])
probs = [math.exp(val/tau)/total for val in Q]
threshold = random.random()
cumulative_prob = 0.0
for i in range(len(probs)):
cumulative_prob += probs[i]
if (cumulative_prob > threshold):
return i
return np.argmax(probs)
- 开始拉动手臂:
for i in range(num_rounds):
# Select the arm using softmax
arm = softmax(0.5)
# Get the reward
observation, reward, done, info = env.step(arm)
# update the count of that arm
count[arm] += 1
# Sum the rewards obtained from the arm
sum_rewards[arm]+=reward
# calculate Q value which is the average rewards of the arm
Q[arm] = sum_rewards[arm]/count[arm]
print( 'The optimal arm is {}'.format(np.argmax(Q)))
以下是输出:
The optimal arm is 3
置信区间上限算法
通过ε贪婪和 softmax 探索,我们以概率探索了随机动作。 随机动作对于探索各种武器很有用,但也可能导致我们尝试无法给我们带来丰厚回报的动作。 我们也不想错过实际上是好的武器,但在最初的回合中却给出了差的奖励。 因此,我们使用一种称为上置信界上限(UCB)的新算法。 它基于面对不确定性时称为乐观的原则。
UCB 算法可帮助我们根据置信区间选择最佳分支。 好的,置信区间是多少? 让我们说我们有两条手臂。 我们拉开这两个手臂,发现第一手臂给了我们 0.3 奖励,第二手臂给了我们 0.8 奖励。 但是,随着一轮拉动手臂,我们不应该得出这样的结论:第二臂将给我们最好的回报。 我们必须尝试几次拉动手臂,取每个手臂获得的奖励平均值,然后选择平均值最高的手臂。 但是,我们如何才能找到每个臂的正确平均值? 这是置信区间进入图片的位置。 置信区间指定武器平均奖励值所在的区间。 如果手臂 1 的置信区间为[0.2, 0.9],则表示手臂 1 的平均值在此区间 0.2 至 0.9 内。 0.2 称为下置信界,而 0.9 称为 UCB。 UCB 选择具有较高 UCB 的机器进行探索。
假设我们有三台老丨虎丨机,并且每台老丨虎丨机都玩了十次。 下图显示了这三个老丨虎丨机的置信区间:

我们可以看到老丨虎丨机 3 具有较高的 UCB。 但是我们不应该得出这样的结论:老丨虎丨机 3 只需拉 10 次就能给我们带来丰厚的回报。 一旦我们几次拉起手臂,我们的置信区间就会准确。 因此,随着时间的流逝,置信区间会变窄并缩小为实际值,如下图所示。 因此,现在,我们可以选择具有较高 UCB 的老丨虎丨机 2 :

UCB 背后的想法非常简单:
- 选择具有较高平均奖励和较高置信度上限的动作(手臂)
- 拉手臂并获得奖励
- 更新手臂的奖励和置信范围
但是,我们如何计算 UCB?
我们可以使用公式√(2log(t) / N(a))计算 UCB,其中N(a)是拉动手臂的次数,t是回合的总数。
因此,在 UCB 中,我们选择具有以下公式的手臂:

首先,初始化变量:
# number of rounds (iterations)
num_rounds = 20000
# Count of number of times an arm was pulled
count = np.zeros(10)
# Sum of rewards of each arm
sum_rewards = np.zeros(10)
# Q value which is the average reward
Q = np.zeros(10)
现在,让我们定义我们的UCB函数:
def UCB(iters):
ucb = np.zeros(10)
#explore all the arms
if iters < 10:
return i
else:
for arm in range(10):
# calculate upper bound
upper_bound = math.sqrt((2*math.log(sum(count))) / count[arm])
# add upper bound to the Q value
ucb[arm] = Q[arm] + upper_bound
# return the arm which has maximum value
return (np.argmax(ucb))
让我们开始武装起来:
for i in range(num_rounds):
# Select the arm using UCB
arm = UCB(i)
# Get the reward
observation, reward, done, info = env.step(arm)
# update the count of that arm
count[arm] += 1
# Sum the rewards obtained from the arm
sum_rewards[arm]+=reward
# calculate Q value which is the average rewards of the arm
Q[arm] = sum_rewards[arm]/count[arm]
print( 'The optimal arm is {}'.format(np.argmax(Q)))
输出如下:
The optimal arm is 1
汤普森采样算法
汤普森采样(TS)是另一种广泛使用的算法,可克服探索利用难题。 它是一种概率算法,基于先验分布。 TS 背后的策略非常简单:首先,我们先计算每个k武器的平均回报,也就是说,我们从K个手臂的每个中提取n个样本,并计算k个分布。 这些初始分布将与真实分布不同,因此我们将其称为先验分布:

由于我们有伯努利奖赏,因此我们使用 beta 分布来计算先验。 beta 分布[alpha, beta]的值在[0, 1]区间内。 Alpha 代表我们获得正面奖励的次数,而 Beta 代表我们获得负面奖励的次数。
现在,我们将看到 TS 如何帮助我们选择最佳手臂。 TS 中涉及的步骤如下:
- 从每个
k分布中采样一个值,并将该值用作先验平均值。 - 选择具有最高先验均值的手臂并观察奖励。
- 使用观察到的奖励来修改先前的分布。
因此,经过几轮之后,先前的分布将开始类似于真实的分布:

我们将通过在 Python 中实现 TS 来更好地理解 TS。 首先,让我们初始化变量:
# number of rounds (iterations)
num_rounds = 20000
# Count of number of times an arm was pulled
count = np.zeros(10)
# Sum of rewards of each arm
sum_rewards = np.zeros(10)
# Q value which is the average reward
Q = np.zeros(10)
# initialize alpha and beta values
alpha = np.ones(10)
beta = np.ones(10)
定义我们的thompson_sampling函数:
def thompson_sampling(alpha,beta):
samples = [np.random.beta(alpha[i]+1,beta[i]+1) for i in range(10)]
return np.argmax(samples)
使用 TS 开始与老丨虎丨机一起玩:
for i in range(num_rounds):
# Select the arm using thompson sampling
arm = thompson_sampling(alpha,beta)
# Get the reward
observation, reward, done, info = env.step(arm)
# update the count of that arm
count[arm] += 1
# Sum the rewards obtained from the arm
sum_rewards[arm]+=reward
# calculate Q value which is the average rewards of the arm
Q[arm] = sum_rewards[arm]/count[arm]
# If it is a positive reward increment alpha
if reward >0:
alpha[arm] += 1
# If it is a negative reward increment beta
else:
beta[arm] += 1
print( 'The optimal arm is {}'.format(np.argmax(Q)))
输出如下:
The optimal arm is 3
MAB 的应用
到目前为止,我们已经研究了 MAB 问题以及如何使用各种探索策略来解决它。 但是老丨虎丨机不仅仅用来玩老丨虎丨机,还可以用来玩老丨虎丨机。 他们有很多应用。
老丨虎丨机被用来代替 AB 测试。 AB 测试是常用的经典测试方法之一。 假设您有网站登录页面的两个版本。 您怎么知道大多数用户喜欢哪个版本? 您进行 AB 测试以了解用户最喜欢哪个版本。
在 AB 测试中,我们分配一个单独的探索时间和一个单独的开采时间。 也就是说,它有两个不同的专用时间,仅用于探索和利用。 但是这种方法的问题在于,这将引起很多遗憾。 因此,我们可以使用解决 MAB 的各种探索策略来最大程度地减少后悔。 与其与老丨虎丨机分别进行完整的探索和利用,不如以适应性方式同时进行探索和利用。
老丨虎丨机广泛用于网站优化,最大化转化率,在线广告,广告序列等。 考虑您正在运行一个短期活动。 如果在这里进行 AB 测试,那么几乎所有的时间都将花费在探索和利用上,因此在这种情况下,使用老丨虎丨机将非常有用。
使用 MAB 识别正确的广告横幅
假设您正在运营一个网站,并且同一广告有五个不同的横幅,并且您想知道哪个横幅吸引了用户。 我们将此问题陈述建模为老丨虎丨机问题。 假设这五个横幅是老丨虎丨机的五个武器,如果用户点击广告,我们将奖励 1 分;如果用户未点击广告,我们将奖励 0 分。
在正常的 AB 测试中,我们将对这五个横幅广告进行完整的探索,然后再确定哪个横幅广告是最好的。 但这将耗费我们大量的精力和时间。 取而代之的是,我们将使用良好的探索策略来确定哪个横幅广告将为我们带来最多的回报(最多的点击次数)。
首先,让我们导入必要的库:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
让我们模拟一个形状为5 x 10,000的数据集,其中列为Banner_type广告,行为0或1,即单击了广告(1)或没有被用户点击(0):
df = pd.DataFrame()
df['Banner_type_0'] = np.random.randint(0,2,100000)
df['Banner_type_1'] = np.random.randint(0,2,100000)
df['Banner_type_2'] = np.random.randint(0,2,100000)
df['Banner_type_3'] = np.random.randint(0,2,100000)
df['Banner_type_4'] = np.random.randint(0,2,100000)
让我们查看几行数据:
df.head()

num_banner = 5
no_of_iterations = 100000
banner_selected = []
count = np.zeros(num_banner)
Q = np.zeros(num_banner)
sum_rewards = np.zeros(num_banner)
定义ε贪婪策略:
def epsilon_greedy(epsilon):
random_value = np.random.random()
choose_random = random_value < epsilon
if choose_random:
action = np.random.choice(num_banner)
else:
action = np.argmax(Q)
return action
for i in range(no_of_iterations):
banner = epsilon_greedy(0.5)
reward = df.values[i, banner]
count[banner] += 1
sum_rewards[banner]+=reward
Q[banner] = sum_rewards[banner]/count[banner]
banner_selected.append(banner)
我们可以绘制结果并查看哪个横幅广告为我们带来了最大的点击次数:
sns.distplot(banner_selected)

情境老丨虎丨机
我们刚刚看到了老丨虎丨机如何用于向用户推荐正确的广告横幅。 但是横幅首选项因用户而异。 用户 A 喜欢横幅广告类型 1,但是用户 B 喜欢横幅广告类型 3。因此,我们必须根据用户行为来个性化广告横幅广告。 我们该怎么做? 我们引入了一种新的老丨虎丨机类型,称为情境老丨虎丨机。
在正常的 MAB 问题中,我们执行操作并获得奖励。 但是对于情境老丨虎丨机,我们不仅要单独采取行动,还要采取环境状态。 国家拥有环境。 在这里,状态指定了用户的行为,因此我们将根据状态(用户行为)采取行动(展示广告),从而获得最大的回报(广告点击)。 因此,上下文老丨虎丨机被广泛用于根据用户的偏好行为来个性化内容。 它们用于解决推荐系统中面临的冷启动问题。 Netflix 使用上下文盗贼根据用户行为来个性化电视节目的插图。
总结
在本章中,我们了解了 MAB 问题以及如何将其应用于不同的应用。 我们了解了解决探索-利用困境的几种方法。 首先,我们查看了ε贪婪策略,在其中我们以概率epsilon进行了探索,并以概率 1 epsilon进行了探索。 我们查看了 UCB 算法,在该算法中我们选择了具有最大上限值的最佳操作,其次是 TS 算法,在此我们通过 beta 分布获得了最佳操作。
在接下来的章节中,我们将学习深度学习以及如何使用深度学习解决 RL 问题。
问题
问题列表如下:
- 什么是 MAB 问题?
- 什么是探索利用困境?
epsilon在ε贪婪策略中有何意义?- 我们如何解决探索与利用的困境?
- 什么是 UCB 算法?
- 汤普森采样与 UCB 算法有何不同?
进一步阅读
您还可以参考以下链接:
七、深度学习基础
到目前为止,我们已经了解了强化学习(RL)的工作原理。 在接下来的章节中,我们将学习深度强化学习(DRL),它是深度学习和 RL 的结合。 DRL 在 RL 社区引起了很多关注,并且对解决许多 RL 任务产生了严重影响。 要了解 DRL,我们需要在深度学习方面有坚实的基础。 深度学习实际上是机器学习的一个子集,并且全都与神经网络有关。 深度学习已经存在了十年,但是之所以现在如此流行,是因为计算的进步和海量数据的可用性。 拥有如此庞大的数据量,深度学习算法将胜过所有经典的机器学习算法。 因此,在本章中,我们将学习几种深度学习算法,例如循环神经网络(RNN),长短期记忆(LSTM)和卷积神经网络(CNN)算法及其应用。
在本章中,您将了解以下内容:
- 人工神经元
- 人工神经网络(ANN)
- 建立神经网络对手写数字进行分类
- RNN
- LSTM
- 使用 LSTM 生成歌曲歌词
- CNN
- 使用 CNN 对时尚产品分类
人工神经元
在了解 ANN 之前,首先让我们了解什么是神经元以及大脑中神经元的实际工作方式。 神经元可以定义为人脑的基本计算单元。 我们的大脑包含大约 1000 亿个神经元。 每个神经元通过突触连接。 神经元通过称为树突的分支状结构从外部环境,感觉器官或其他神经元接收输入,如下图所示。 这些输入被增强或减弱,也就是说,根据它们的重要性对其进行加权,然后将它们加在一起在体(细胞体)中。 然后,这些合计的输入从细胞体中被处理并穿过轴突,然后被发送到其他神经元。 下图显示了基本的单个生物神经元:

现在,人工神经元如何工作? 假设我们有三个输入, x1,x2和x3,预测输出y。 这些输入乘以权重,即w₁,w₂和w₃,即x1 * w[1] + w2 * w[2] + x3 * w[3]。 但是,为什么我们将这些输入乘以权重? 因为在计算输出y时,所有输入都不是同等重要的。 假设x2在计算输出方面比其他两个输入更为重要。 然后,为w₂分配较高的值,而不是为其他两个权重分配较高的值。 因此,将权重乘以输入后, x2的值将比其他两个输入值高。 在将输入与权重相乘后,我们将它们求和,然后添加一个称为偏差b的值。 因此,z = (x1 * w1 + x2 * w2 + x3 * w3) + b,即:

z看起来不像线性回归方程吗? 不仅仅是直线方程吗?z = mx + b。
其中m是权重(系数),x是输入,b是偏差(截距)。 嗯,是。 那么神经元和线性回归有什么区别? 在神经元中,我们通过应用称为激活或传递函数的函数f()向结果z引入非线性。 因此,我们的输出为y = f(z)。 下图显示了一个人工神经元:

在神经元中,我们将输入x乘以权重w,并在应用激活函数f(z)之前加上偏差b。得出此结果,并预测输出y。
人工神经网络
神经元很酷,对吗? 但是单个神经元不能执行复杂的任务,这就是为什么我们的大脑拥有数十亿个神经元,这些神经元分层组织,形成一个网络。 类似地,人工神经元分层排列。 每层都将以信息从一层传递到另一层的方式连接。 典型的 ANN 由以下几层组成:
- 输入层
- 隐藏层
- 输出层
每层都有一个神经元集合,一层中的神经元与另一层中的所有神经元相互作用。 但是,同一层中的神经元不会互相影响。 下图显示了典型的 ANN:

输入层
输入层是我们向网络提供输入的地方。 输入层中神经元的数量就是我们馈送到网络的输入数量。 每个输入都会对预测输出产生一定的影响,并将其乘以权重,同时将添加偏差并将其传递给下一层。
隐藏层
输入层和输出层之间的任何层都称为隐藏层。 它处理从输入层接收的输入。 隐藏层负责推导输入和输出之间的复杂关系。 即,隐藏层标识数据集中的模式。 可以有任意数量的隐藏层,但是我们必须根据我们的问题选择许多隐藏层。 对于一个非常简单的问题,我们只能使用一个隐藏层,但是在执行诸如图像识别之类的复杂任务时,我们使用许多隐藏层,其中每个层都负责提取图像的重要特征,以便我们可以轻松识别图像。 当我们使用许多隐藏层时,该网络称为深度神经网络。
输出层
处理输入后,隐藏层将其结果发送到输出层。 顾名思义,输出层发出输出。 输出层中神经元的数量与我们希望网络解决的问题类型有关。 如果是二分类,则输出层中神经元的数量会告诉我们输入所属的类。 如果是五类的多类分类,并且如果我们要获取每个类作为输出的概率,则输出层中的神经元数为五,每个神经元发出该概率。 如果是回归问题,则输出层中只有一个神经元。
激活函数
激活函数用于在神经网络中引入非线性。 我们将激活函数应用于权重乘以并加到偏差上的输入,即f(z),其中z = 输入 * 权重 + 偏差。 激活函数有以下几种:
- Sigmoid 函数:Sigmoid 函数是最常用的激活函数之一。 它在
0和1之间缩放值。 可以将 Sigmoid 函数定义为f(z) = 1 / (1 + e^z)。 当我们将此函数应用于z时,值将在 0 到1的范围内缩放。 这也称为逻辑函数。 它是 Sigmoid,如下图所示:

- 双曲正切函数:与 Sigmoid 函数不同,双曲正切函数在
-1和+1之间缩放值。 双曲正切函数可以定义为f(z) = (e^(2z) - 1) / (e^(2z) + 1)。 当我们将此函数应用于z时,这些值将在-1至+1的范围内缩放。 它也是 S 形,但居中为零,如下图所示:

- ReLU 函数:ReLU 也称为整流线性单元。 它是使用最广泛的激活函数之一。 ReLU 函数可以定义为
f(z) = max(0, z),即当z小于 0 且f(z)为 0。 当z大于或等于0时,等于z:

- Softmax 函数:softmax 函数实际上是 Sigmoid 函数的推广。 它通常应用于网络的最后一层,同时执行多类分类任务。 它给出了每个类别作为输出的概率,因此 softmax 值的总和将始终等于
1。 可以定义为σ(z[i]) = e^z[i] / Σ[j] e^z[j]。
深入研究 ANN
我们知道,在人工神经元中,我们将输入乘以权重,对其施加偏倚,然后应用激活函数来产生输出。 现在,我们将看到在神经元分层放置的神经网络环境中如何发生这种情况。 网络中的层数等于隐藏层数加输出层数。 我们不考虑输入层。 考虑一个具有一个输入层,一个隐藏层和一个输出层的两层神经网络,如下图所示:

假设我们有两个输入, x1和x2,我们必须预测输出y]。 由于我们有两个输入,因此输入层中的神经元数将为两个。 现在,这些输入将乘以权重,然后添加偏差并将结果值传播到将应用激活函数的隐藏层。 因此,首先我们需要初始化权重矩阵。 在现实世界中,我们不知道哪个输入真正重要,因此需要权重较高才能计算输出。 因此,我们将随机初始化权重和偏差值。 我们可以将输入层和隐藏层之间的权重和偏差分别表示为w[xh]和 b[h]。 权重矩阵的大小如何? 权重矩阵的大小必须为[当前层中的神经元数量 * 下一层中的神经元数量]。 这是为什么? 因为这是基本的矩阵乘法规则。 要乘以任意两个矩阵AB,矩阵A中的列数必须等于矩阵B中的行数。因此,权重矩阵w[xh]的大小应为[输入层中的神经元数量 * 隐藏层中的神经元数量],即2 x 4:

即,z[1] = 输入 * 权重 + 偏差。 现在,这将传递到隐藏层。 在隐藏层中,我们将激活函数应用于z₁。 让我们考虑以下 Sigmoid 激活函数:

应用激活函数后,我们再次将结果a₁乘以新的权重矩阵,并添加在隐藏层和输出层之间流动的新偏差值。 我们可以将该权重矩阵和偏差分别表示为w[hy]和b[y]。 该权重矩阵w[hy]的大小将为[隐藏层中的神经元数量 * 输出层中的神经元数量]。 由于我们在隐藏层有四个神经元,在输出层有一个神经元,因此w[hy]矩阵大小为4 x 1。 因此,我们将a₁乘以权重矩阵w[hy],然后加上偏差b[y]并将结果传递到下一层,即输出层:

现在,在输出层中,我们将 Sigmoid 函数应用于z₂,这将产生输出值:

从输入层到输出层的整个过程称为正向传播,如下所示:
def forwardProp():
z1 = np.dot(x,wxh) + bh
a1 = sigmoid(z1)
z2 = np.dot(a1,why) + by
yHat = sigmoid(z2)
前向传播很酷,不是吗? 但是我们如何知道神经网络生成的输出是否正确? 我们必须定义一个新函数,称为成本函数J,也称为损失函数,它告诉我们神经网络的表现如何。 有许多不同的成本函数。 我们将均方误差用作成本函数,可以将其定义为实际值(y)与预测值(y_hat)之间的均方差:

我们的目标是最小化成本函数,以便我们的神经网络预测会更好。 如何使成本函数最小化? 我们可以通过更改正向传播中的某些值来最小化成本函数。 我们可以改变哪些值? 显然,我们不能更改输入和输出。 现在,我们剩下了权重和偏差值。 我们只是随机初始化权重矩阵和偏差,所以它不是完美的。 现在,我们将调整神经网络的权重矩阵(w[xh]和w[hy]) 结果。 我们如何调整这些权重矩阵? 这是一种称为梯度下降的新技术。
梯度下降
由于正向传播,我们处于输出层。 因此,现在,我们将网络从输出层反向传播到输入层,并通过计算成本函数相对于权重的梯度来最小化误差,从而更新权重。 听起来令人困惑,对吧? 让我们从一个类比开始。 假设您位于山顶上,如下图所示,并且您想到达山顶的最低点。 您将必须在山上向下走一步,这会导致您到达最低点(即,您从山上下降到最低点)。 可能有许多地区看起来像山上的最低点,但我们必须到达最低点,实际上这是最低点。 也就是说,当存在全局最低点时,您不应被认为是最低点:

同样,我们可以表示成本函数,如下所示。 这是成本与权重的关系图。 我们的目标是使成本函数最小化。 也就是说,我们必须达到成本最低的最低点。 该点显示了我们的初始权重(即我们在山上的位置)。 如果将这一点向下移动,则可以到达误差最小的位置,即成本函数上的最低点(山上的最低点):

我们如何向下移动该点(初始权重)? 我们如何下降并到达最低点? 我们可以通过计算成本函数相对于该点的梯度来移动该点(初始权重)。 梯度是导数,实际上是切线的斜率,如下图所示。 因此,通过计算梯度,我们下降(向下移动)并到达最低点:

计算梯度后,我们通过权重更新规则更新旧权重:

什么是α? 它被称为学习率。 如果学习率很小,那么我们会向下走一小步,并且梯度下降会很慢。 如果学习率很高,那么我们将迈出一大步,梯度下降将很快,但是我们可能无法达到全局最小值,而陷入局部最小值。 因此,学习率应选择最佳,如下所示:

现在,让我们从数学上看一下。 现在,我们将讨论很多有趣的数学运算,因此戴上微积分帽并按照以下步骤操作。 因此,我们有两个权重,一个是w[xh],对输入权重是隐藏的,另一个是w[hy], 隐藏以输出权重。 我们需要根据我们的权重更新规则来更新这些权重。 为此,首先,我们需要计算成本函数相对于权重的导数。
由于我们正在反向传播,也就是说,从输出层到输入层,我们的第一权重将是w[hy]。 因此,现在我们需要计算J相对于w[hy]的导数。 我们如何计算导数? 回想一下我们的成本函数J = 1 / 2(y - y_hat)。 我们无法直接计算导数,因为J中没有w[hy]项。
回顾如下给出的前向传播方程:


首先,我们将计算相对于y_hat的偏导数,然后从y_hat计算相对于z₂的偏导数。 从z₂,我们可以直接计算我们的导数w[hy]。 这实际上是连锁规则。
因此,我们的等式变为:
----(1)
我们将计算以下每一项:


其中σ'是我们的 Sigmoid 激活函数的导数。 我们知道 Sigmoid 函数是σ = 1 / (1 + e^(-z)),所以 Sigmoid 函数的导数将是σ' = e^(-z) / (1 + e^(-z))^2。

我们将所有这些替换为第一个方程式(1)。
现在,我们需要针对下一个权重w[xh]来计算J的导数。 同样,我们无法直接根据J计算w[xh]的导数,因为我们在J中的没有任何w[xh]项。 因此,我们需要使用链式规则; 再次回顾我们的前向传播步骤:*




现在,权重w[xh]的梯度计算将变为:
----(2)
我们将计算以下每一项:





一旦我们计算了两个权重的梯度,就将根据权重更新规则更新以前的权重。
现在,让我们做一些编码。 看等式(1)和(2)。 我们在两个方程式中都有∂J/∂y_hat和∂J/∂z[2],因此我们不必一遍又一遍地进行计算。 我们将其定义为delta3:
delta3 = np.multiply(-(y-yHat),sigmoidPrime(z2))
现在,我们计算w[hy]的梯度为:
dJ_dWhy = np.dot(a1.T,delta3)
我们将w[xh]的梯度计算为:
delta2 = np.dot(delta3,Why.T)*sigmoidPrime(z1)
dJ_dWxh = np.dot(X.T,delta2)
我们将根据权重更新规则将权重更新为:
Wxh += -alpha * dJ_dWhy
Why += -alpha * dJ_dWxh
此反向传播的完整代码如下:
def backProp():
delta3 = np.multiply(-(y-yHat),sigmoidPrime(z2))
dJdW2 = np.dot(a1.T, delta3)
delta2 = np.dot(delta3,Why.T)*sigmoidPrime(z1)
dJdW1 = np.dot(X.T, delta2)
Wxh += -alpha * dJdW1
Why += -alpha * dJdW2
在继续之前,让我们熟悉一下神经网络中一些常用的术语:
- 正向传播:正向传播意味着从输入层到输出层的正向传播。
- 反向传播:反向传播表示从输出层向输入层的反向传播。
- 周期:周期指定了神经网络看到我们整个训练数据的次数。 因此,对于所有训练样本,我们可以说一个周期等于一个向前通过和一个向后通过。
- 批量大小:批量大小指定了我们在一次向前和向后一次通过中使用的训练样本数。
- 迭代次数:迭代次数表示通过次数,其中
一遍 = 一次正向 + 一次反向。
假设我们有 12,000 个训练样本,而我们的批量大小为 6,000。 我们将需要两个迭代来完成一个周期。 也就是说,在第一次迭代中,我们传递了前 6,000 个样本,并执行了前向遍历和后向遍历; 在第二次迭代中,我们传递接下来的 6,000 个样本,并执行正向传播和反向传递。 经过两次迭代,我们的神经网络将看到整个 12,000 个训练样本,这使它成为一个周期。
TensorFlow 中的神经网络
现在,我们将看到如何使用 TensorFlow 构建基本的神经网络,该网络可以预测手写数字。 我们将使用流行的 MNIST 数据集,该数据集具有标记的手写图像的集合以进行训练。
首先,我们必须导入 TensorFlow 并从tensorflow.examples.tutorial.mnist加载数据集:
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("/tmp/data/", one_hot=True)
现在,我们将看到数据中包含的内容:
print("No of images in training set {}".format(mnist.train.images.shape))
print("No of labels in training set {}".format(mnist.train.labels.shape))
print("No of images in test set {}".format(mnist.test.images.shape))
print("No of labels in test set {}".format(mnist.test.labels.shape))
它将打印以下内容:
No of images in training set (55000, 784)
No of labels in training set (55000, 10)
No of images in test set (10000, 784)
No of labels in test set (10000, 10)
training set中有55000个图像,每个图像的大小均为784。 我们也有10标签,它们实际上是0至9。 类似地,我们在test set中有10000图像。
现在,我们绘制输入图像以查看其外观:
img1 = mnist.train.images[41].reshape(28,28)
plt.imshow(img1, cmap='Greys')

让我们开始建立我们的网络。 我们将构建一个包含一个输入层,一个隐藏层和一个预测手写数字的输出层的两层神经网络。
首先,我们为输入和输出定义占位符。 由于我们的输入数据形状为784,因此我们可以将输入占位符定义为:
x = tf.placeholder(tf.float32, [None, 784])
None表示什么? None指定传递的样本数(批大小),这将在运行时动态确定。
由于我们将10类作为输出,因此可以将placeholder输出定义为:
y = tf.placeholder(tf.float32, [None, 10]
接下来,我们初始化超参数:
learning_rate = 0.1
epochs = 10
batch_size = 100
然后,我们将隐藏层的输入之间的权重和偏差分别定义为w_xh和b_h。 我们用值初始化权重矩阵,从正态分布中随机抽取标准差为0.03的值:
w_xh = tf.Variable(tf.random_normal([784, 300], stddev=0.03), name='w_xh')
b_h = tf.Variable(tf.random_normal([300]), name='b_h')
接下来,我们将隐藏层与输出层之间的权重和偏差分别定义为w_hy和b_y:
w_hy = tf.Variable(tf.random_normal([300, 10], stddev=0.03), name='w_hy')
b_y = tf.Variable(tf.random_normal([10]), name='b_y')
现在开始进行正向传播。 回想一下我们在前向传播中执行的步骤:
z1 = tf.add(tf.matmul(x, w_xh), b_h)
a1 = tf.nn.relu(z1)
z2 = tf.add(tf.matmul(a1, w_hy), b_y)
yhat = tf.nn.softmax(z2)
我们将成本函数定义为交叉熵损失。 交叉熵损失也称为对数损失,可以定义如下:

其中yᵢ是实际值,y_hat[i]是预测值:
cross_entropy = tf.reduce_mean(-tf.reduce_sum(y * tf.log(yhat), reduction_indices=[1]))
我们的目标是使成本函数最小化。 我们可以通过向后传播网络并执行梯度下降来最小化成本函数。 使用 TensorFlow,我们不必手动计算梯度; 我们可以使用 TensorFlow 的内置梯度下降优化器功能,如下所示:
optimiser = tf.train.GradientDescentOptimizer(learning_rate=learning_rate).minimize(cross_entropy)
为了评估我们的模型,我们将计算精度如下:
correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(yhat, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
我们知道 TensorFlow 是通过构建计算图来运行的,到目前为止,我们所写的任何内容实际上只有在启动 TensorFlow 会话后才能运行。 所以,让我们这样做。
首先,初始化 TensorFlow 变量:
init_op = tf.global_variables_initializer()
现在,开始 TensorFlow 会话并开始训练模型:
with tf.Session() as sess:
sess.run(init_op)
total_batch = int(len(mnist.train.labels) / batch_size)
for epoch in range(epochs):
avg_cost = 0
for i in range(total_batch):
batch_x, batch_y = mnist.train.next_batch(batch_size=batch_size)
_, c = sess.run([optimiser, cross_entropy],
feed_dict={x: batch_x, y: batch_y})
avg_cost += c / total_batch
print("Epoch:", (epoch + 1), "cost =""{:.3f}".format(avg_cost))
print(sess.run(accuracy, feed_dict={x: mnist.test.images, y: mnist.test.labels}))
RNN
鸟儿在 ____ 中飞翔。 如果我要求您预测空白,则可能会预测“天空”。 您如何预测“天空”一词会很好地填补这一空白? 因为您阅读了整个句子并根据理解句子的上下文,预测“天空”是正确的词。 如果我们要求正常的神经网络为该空格预测正确的单词,它将无法预测正确的单词。 这是因为正常神经网络的输出仅基于当前输入。 因此,神经网络的输入将只是前一个单词the。 也就是说,在正常的神经网络中,每个输入都彼此独立。 因此,在我们必须记住输入序列以预测下一个序列的情况下,它将不能很好地执行。
我们如何使我们的网络记住整个句子以正确预测下一个单词? 这是 RNN 发挥作用的地方。 RNN 不仅基于当前输入,而且还基于先前的隐藏状态来预测输出。 您可能想知道为什么 RNN 必须基于当前输入和先前的隐藏状态来预测输出,以及为什么它不能仅使用当前输入和先前的输入而不是当前输入和先前的隐藏状态来预测输出 。 这是因为前一个输入将存储有关前一个单词的信息,而前一个隐藏状态将捕获有关整个句子的信息,也就是说,前一个隐藏状态将存储上下文。 因此,基于当前输入和先前的隐藏状态而不是仅基于当前输入和先前的输入来预测输出非常有用。
RNN 是一种特殊类型的神经网络,广泛应用于顺序数据。 换句话说,它适用于排序重要的数据。 简而言之,RNN 有一个存储先前信息的存储器。 它广泛应用于各种自然语言处理(NLP)任务,例如机器翻译,情感分析等。 它也适用于时间序列数据,例如股票市场数据。 仍然不清楚 RNN 到底是什么? 查看下图,该图显示了正常神经网络和 RNN 的比较:

您是否注意到 RNN 与我们在上一主题中看到的普通神经网络有何不同? 是。 区别在于隐藏状态中存在一个循环,这意味着如何使用以前的隐藏状态来计算输出。
还是令人困惑? 查看以下 RNN 的展开版本:

如您所见,根据当前输入x1,当前隐藏状态*预测输出y1, h[1]以及先前的隐藏状态h₀。 类似地,看看如何计算输出y₂。 它采用当前输入x₂和当前隐藏状态 h₂以及先前的隐藏状态h₁。 这就是 RNN 的工作方式; 它需要当前输入和先前的隐藏状态来预测输出。 我们可以将这些隐藏状态称为内存,因为它们保存了到目前为止已经看到的信息。
现在,我们将看到一些数学运算:

在上图中:
U代表输入到隐藏状态的权重矩阵W代表隐藏状态到隐藏状态的权重矩阵V表示隐藏状态到输出的权重矩阵
因此,在前向传递中,我们计算以下内容:

即,时间 t 的隐藏状态 = tanh([输入到隐藏权重的矩阵 * 输入] + [隐藏权重到隐藏权重的矩阵 *时间 t-1 的先前隐藏状态]):

即,时间的 t 输出 = Sigmoid(隐藏权重到输出的矩阵 * 时间 t 的隐藏状态)。
我们还可以将损失函数定义为交叉熵损失,如下所示:


在前面的示例中,yₜ是时间t时的实际单词,y_hat[t]是时间t时的预测单词。 由于我们将整个序列作为训练样本,因此总损失将是每个时间步的损失总和。
时间上的反向传播
现在,我们如何训练 RNN? 就像我们训练了正常的神经网络一样,我们可以使用反向传播来训练 RNN。 但是在 RNN 中,由于所有时间步长都具有依赖性,因此每个输出的梯度将不仅取决于当前时间步长,还取决于先前时间步长。 我们将此沿时间的反向传播(BPTT)。 它与反向传播基本相同,不同之处在于它应用了 RNN。 要查看它如何在 RNN 中发生,让我们考虑一下 RNN 的展开版本,如下图所示:

在上图中,L₁,L₂和L₃是每个时间步的损失。 现在,我们需要在每个时间步长计算相对于我们的权重矩阵U,V和W的损失梯度。 就像我们之前通过对每个时间步长求和来计算总损失一样,我们用每个时间步长的梯度之和来更新权重矩阵:

但是,此方法存在问题。 梯度计算涉及计算关于激活函数的梯度。 当我们计算相对于 Sigmoid/tanh 函数的梯度时,该梯度将变得非常小。 当我们在许多时间步长上进一步传播网络并乘以梯度时,梯度将趋于变得越来越小。 这称为消失梯度问题。 那么,由于这个问题会发生什么呢? 由于梯度会随着时间消失,因此我们无法了解有关长期依赖关系的信息,也就是说,RNN 无法将信息在内存中保留更长的时间。
逐渐消失的梯度不仅会出现在 RNN 中,还会发生在其他深层网络中,在这些深层网络中,当我们使用 Sigmoid/tanh 函数时会出现许多隐藏层。 还有一个称为爆炸梯度的问题,其中梯度值变得大于 1,当我们将这些梯度相乘时,它将导致很大的数字。
一种解决方案是将 ReLU 用作激活函数。 但是,我们有一个称为 LSTM 的 RNN 变体,它可以有效解决消失的梯度问题。 我们将在接下来的部分中看到它的工作原理。
长短期记忆 RNN
RNN 非常酷,对吧? 但是我们在训练 RNN 时遇到了一个问题,即消失梯度问题。 让我们来探讨一下。 天空是 __ 的。 RNN 可以根据所看到的信息轻松地将最后一个单词预测为“蓝色”。 但是 RNN 无法涵盖长期依赖关系。 这意味着什么? 假设 Archie 在中国生活了 20 年。 他喜欢听好音乐。 他是一个非常大的漫画迷。 他的 __ 很流利。 现在,您将预测空白为中文。 您是如何预测的? 因为您了解 Archie 在中国生活了 20 年,所以您认为他可能会说流利的中文。 但是 RNN 不能在记忆中保留所有这些信息以说 Archie 能够说流利的中文。 由于消失的梯度问题,它无法长时间在内存中重新收集/记住信息。 我们该如何解决?
LSTM 来了!!!!
LSTM 是 RNN 的一种变体,可以解决梯度消失的问题。 LSTM 会在需要时将信息保留在内存中。 因此,基本上,RNN 单元将替换为 LSTM。 LSTM 如何实现这一目标?
下图显示了一个典型的 LSTM 单元:

LSTM 单元称为内存,它们负责存储信息。 但是信息必须在存储器中保留多长时间? 我们什么时候可以删除旧信息并用新信息更新单元格? 所有这些决定将由以下三个特殊部门做出:
- 遗忘门
- 输入门
- 输出门
如果查看 LSTM 单元,则顶部水平线Cₜ被称为单元状态。 这是信息流向的地方。 LSTM 门将不断更新有关单元状态的信息。 现在,我们将看到这些门的功能:
-
遗忘门:遗忘门负责确定哪些信息不应处于单元状态。 看下面的陈述:
“Harry 是一位好歌手。 他住在纽约。 Zayn 也是一位出色的歌手。”一旦我们开始谈论 Zayn,网络就会了解到主题已从 Harry 更改为 Zayn,并且不再需要有关 Harry 的信息。 现在,“遗忘门”将从单元状态中删除/忘记关于哈利的信息。
-
输入门:输入门负责确定应在存储器中存储哪些信息。 让我们考虑相同的示例:
“Harry 是一位出色的歌手。 他住在纽约。 Zayn 也是一位出色的歌手。”
因此,在“遗忘门”从单元状态中删除信息之后,输入门将确定存储器中必须包含哪些信息。 在此,由于通过遗忘门从单元状态中删除了关于哈利的信息,因此输入门决定用关于 Zayn 的信息来更新单元状态。
-
输出门:该输出门负责确定一次从单元状态
t应该显示什么信息。 现在,考虑以下句子:“Zayn 的首张专辑取得了巨大的成功。 恭喜 ____。”
在这里,“恭喜”是用于形容名词的形容词。 输出层将预测
Zayn(名词),以填补空白。
使用 LSTM RNN 生成歌曲歌词
现在,我们将看到如何使用 LSTM 网络生成 Zayn Malik 的歌曲歌词。 可以从此处下载数据集,其中包含 Zayn 的歌词集。
首先,我们将导入必要的库:
import tensorflow as tf
import numpy as np
现在,我们将读取包含歌词的文件:
with open("Zayn_Lyrics.txt","r") as f:
data=f.read()
data=data.replace('\n','')
data = data.lower()
让我们看看数据中包含的内容:
data[:50]
"now i'm on the edge can't find my way it's inside "
然后,我们将所有字符存储在all_chars变量中:
all_chars=list(set(data))
我们将唯一字符数存储在unique_chars中:
unique_chars = len(all_chars)
我们还将字符总数存储在total_chars中:
total_chars =len(data)
现在,我们将在每个字符与其索引之间创建一个映射。 char_to_ix将具有字符到索引的映射,而ix_to_char将具有字符到索引的映射:
char_to_ix = { ch:i for i,ch in enumerate(all_chars) }
ix_to_char = { i:ch for i,ch in enumerate(all_chars) }
也就是说,例如:
char_to_ix['e']
9
ix_to_char[9]
e
接下来,我们定义一个generate_batch函数,该函数将生成输入值和目标值。 目标值只是i乘以输入值的偏移。
例如:如果input = [12,13,24]的偏移值为1,则目标将为[13,24]:
def generate_batch(seq_length,i):
inputs = [char_to_ix[ch] for ch in data[i:i+seq_length]]
targets = [char_to_ix[ch] for ch in data[i+1:i+seq_length+1]]
inputs=np.array(inputs).reshape(seq_length,1)
targets=np.array(targets).reshape(seq_length,1)
return inputs,targets
我们将定义序列长度,学习率和节点数,即神经元数:
seq_length = 25
learning_rate = 0.1
num_nodes = 300
让我们构建我们的 LSTM RNN。 TensorFlow 为我们提供了用于构建 LSTM 单元的BasicLSTMCell()函数,我们需要指定 LSTM 单元中的单元数以及希望使用的激活函数的类型。
因此,我们将创建一个 LSTM 单元,然后使用tf.nn.dynamic_rnn()函数使用该单元构建 RNN,它将返回输出和状态值:
def build_rnn(x):
cell= tf.contrib.rnn.BasicLSTMCell(num_units=num_nodes, activation=tf.nn.relu)
outputs, states = tf.nn.dynamic_rnn(cell, x, dtype=tf.float32)
return outputs,states
现在,我们将为输入X和目标Y创建一个占位符:
X=tf.placeholder(tf.float32,[None,1])
Y=tf.placeholder(tf.float32,[None,1])
将X和Y转换为int:
X=tf.cast(X,tf.int32)
Y=tf.cast(Y,tf.int32)
我们还将为X和Y创建onehot表示形式,如下所示:
X_onehot=tf.one_hot(X,unique_chars)
Y_onehot=tf.one_hot(Y,unique_chars)
通过调用build_rnn函数从 RNN 获取输出和状态:
outputs,states=build_rnn(X_onehot)
转置输出:
outputs=tf.transpose(outputs,perm=[1,0,2])
初始化权重和偏差:
W=tf.Variable(tf.random_normal((num_nodes,unique_chars),stddev=0.001))
B=tf.Variable(tf.zeros((1,unique_chars)))
我们将通过将输出乘以权重并加上偏差来计算输出:
Ys=tf.matmul(outputs[0],W)+B
接下来,执行 softmax 激活并获得概率:
prediction = tf.nn.softmax(Ys)
我们将计算cross_entropy损失为:
cross_entropy=tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=Y_onehot,logits=Ys))
我们的目标是使损失最小化,因此我们将反向传播网络并执行梯度下降:
optimiser = tf.train.GradientDescentOptimizer(learning_rate=learning_rate).minimize(cross_entropy)
现在,我们将定义名为predict的辅助函数,该函数根据我们的 RNN 模型得出下一个预测字符的索引:
def predict(seed,i):
x=np.zeros((1,1))
x[0][0]= seed
indices=[]
for t in range(i):
p=sess.run(prediction,{X:x})
index = np.random.choice(range(unique_chars), p=p.ravel())
x[0][0]=index
indices.append(index)
return indices
我们设置batch_size,批数和epochs的数量以及shift值以生成批量:
batch_size=100
total_batch=int(total_chars//batch_size)
epochs=1000
shift=0
最后,我们将开始 TensorFlow 会话并构建模型:
init=tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init)
for epoch in range(epoch):
print("Epoch {}:".format(epoch))
if shift + batch_size+1 >= len(data):
shift =0
## get the input and target for each batch by generate_batch
#function which shifts the input by shift value
## and form target
for i in range(total_batch):
inputs,targets=generate_batch(batch_size,shift)
shift += batch_size
# calculate loss
if(i%100==0):
loss=sess.run(cross_entropy,feed_dict={X:inputs, Y:targets})
# We get index of next predicted character by
# the predict function
index =predict(inputs[0],200)
# pass the index to our ix_to_char dictionary and
#get the char
txt = ''.join(ix_to_char[ix] for ix in index)
print('Iteration %i: '%(i))
print ('\n %s \n' % (txt, ))
sess.run(optimiser,feed_dict={X:inputs,Y:targets})
我们可以看到输出在初始周期是一些随机字符,但是随着训练步骤的增加,我们会得到更好的结果:
Epoch 0:
Iteration 0:
wsadrpud,kpswkypeqawnlfyweudkgt,khdi nmgof' u vnvlmbis . snsblp,podwjqehb,e;g-'fyqjsyeg,byjgyotsrdf;;u,h.a;ik'sfc;dvtauofd.,q.;npsw'wjy-quw'quspfqw-
.
.
.
Epoch 113:
Iteration 0:
i wanna see you, yes, and she said yes!
卷积神经网络
CNN,也称为卷积网络(ConvNet),是一种特殊的神经网络,广泛用于计算机视觉。 CNN 的应用范围包括从自动驾驶汽车中的视觉功能到 Facebook 图片中朋友的自动标记。 CNN 利用空间信息来识别图像。 但是它们如何真正起作用? 神经网络如何识别这些图像? 让我们逐步进行此步骤。
CNN 通常包含三个主要层:
- 卷积层
- 池化层
- 全连接层
卷积层
当我们输入图像作为输入时,它实际上将转换为像素值矩阵。 这些像素值的范围为 0 到 255,此矩阵的大小为[图像高度 * 图像宽度 * 通道数]。 如果输入图像的大小为64 x 64,则像素矩阵大小将为64 x 64 x 3,其中 3 表示通道号。 灰度图像具有 1 个通道,彩色图像具有 3 个通道(RGB)。 看下面的照片。 当将此图像作为输入输入时,它将转换为像素值矩阵,我们稍后将看到。 为了更好地理解,我们将考虑灰度图像,因为灰度图像具有 1 个通道,因此我们将获得 2D 矩阵。
输入图像如下:

现在,让我们在下图中看到矩阵值:

因此,这就是图像由矩阵表示的方式。 接下来发生什么? 网络如何从该像素的值识别图像? 现在,我们介绍一个称为卷积的操作。 它用于从图像中提取重要特征,以便我们可以了解图像的全部含义。 假设我们有一只狗的形象; 您认为这张图片的特征是什么,这将有助于我们了解这是狗的图片? 我们可以说身体结构,脸,腿,尾巴等等。 卷积运算将帮助网络学习狗的特征。 现在,我们将看到如何精确执行卷积运算以从图像中提取特征。
众所周知,每个图像都由一个矩阵表示。 假设我们有一个狗图像的像素矩阵,并将其称为输入矩阵。 我们还将考虑称为过滤器的另一个n x n矩阵,如下图所示:

现在,该过滤器将在我们的输入矩阵上滑动一个像素,并执行逐元素乘法,从而生成一个数字。 困惑? 看下图:

也就是说,(13 * 0) + (8 * 1) + (18 * 0) + (5 * 1) + (3 * 1) + (1 * 1) + (1 * 0) + (9 * 0) + (0 * 1) = 17。
同样,我们将过滤器矩阵在输入矩阵上移动一个像素,然后执行逐元素乘法:

即(8 * 0) + (18 * 1) + (63 * 0) + (3 * 1) + (1 * 1) + (2 * 1) + (9 * 0) + (0 * 0) + (7 * 1) = 31。
过滤器矩阵将在整个输入矩阵上滑动,执行逐元素乘法,并生成一个称为特征映射或激活图的新矩阵。 该操作称为卷积,如下图所示:

以下输出显示了实际的卷积图像:

您可以看到我们的过滤器已检测到实际图像中的边缘并产生了卷积图像。 类似地,使用不同的过滤器从图像中提取不同的特征。
例如,如果我们使用过滤器矩阵,比如说锐化过滤器:

那么我们的卷积图像将如下所示:

因此,过滤器负责通过执行卷积运算从实际图像中提取特征。 将有多个过滤器用于提取生成特征映射的图像的不同特征。 特征映射的深度是我们使用的过滤器的数量。 如果我们使用 5 个过滤器提取特征并生成 5 个特征映射,则特征映射的深度为5,如下所示:

当我们有许多过滤器时,我们的网络将通过提取许多特征来更好地理解图像。 在构建 CNN 时,我们不必为此过滤矩阵指定值。 在训练过程中将学习此过滤器的最佳值。 但是,我们必须指定过滤器的数量和要使用的过滤器的大小。
我们可以使用过滤器在输入矩阵上滑动一个像素,然后执行卷积运算。 我们不仅可以滑动一个像素。 我们还可以在输入矩阵上滑动任意数量的像素。 我们在输入矩阵中滑过输入矩阵的像素数称为步幅。
但是,当滑动窗口(过滤器矩阵)到达图像边界时会发生什么? 在这种情况下,我们用零填充输入矩阵,以便可以在图像边缘应用过滤器。 图像上带有零的填充称为相同填充,或宽卷积或零填充,如下所示:

除了用零填充以外,我们还可以简单地丢弃该区域。 这称为有效填充或窄卷积,如下所示:

执行卷积运算后,我们应用 ReLU 激活函数引入非线性。
池化层
在卷积层之后,我们有了池化层。 池化层用于减少特征映射的维数,并且仅保留必要的细节,因此可以减少计算量。 例如,要确定图像中是否有一只狗,我们不想了解狗在图像中的哪个位置,我们只需要狗的特征。 因此,池化层通过仅保留重要特征来减小空间大小。 有多种类型的池化操作。 最大池化是最常用的池化操作之一,我们仅从窗口内的特征映射中获取最大值。
带有2 x 2过滤器且步幅为 2 的最大池如下所示:

在平均池中,我们只取窗口内特征映射中元素的平均值,而在汇总池中,我们取窗口中特征映射中元素的总和。
合并操作不会更改特征映射的深度,只会影响高度和宽度。
全连接层
我们可以有多个卷积层,然后是池化层。 但是,这些层只会从输入图像中提取特征并生成激活图。 我们如何仅凭激活图对图像中是否有一条狗进行分类? 我们必须引入一个称为全连接层的新层。 当激活图(现在基本上是图像的特征)应用激活函数时,它将接收输入,并产生输出。 全连接层实际上是正常的神经网络,其中我们具有输入层,隐藏层和输出层。 在这里,我们使用卷积和池化层代替输入层,它们一起产生激活图作为输入。
CNN 架构
现在,让我们看看如何在 CNN 架构中组织所有这些层,如下所示:

首先,将图像传递到卷积层,在卷积层中我们应用卷积运算以提取特征,然后将特征映射传递到池化层,在其中减小大小。 我们可以根据用例添加任意数量的卷积和池化层。 此后,我们可以添加一个神经网络,该神经网络的末尾有一个隐藏层,称为全连接层,该层对图像进行分类。
使用 CNN 对时尚产品分类
现在,我们将看到如何使用 CNN 对时尚产品进行分类。
首先,我们将照常导入所需的库:
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
现在,我们将读取数据。 该数据集位于tensorflow.examples中,因此我们可以按以下方式直接提取数据:
from tensorflow.examples.tutorials.mnist import input_data
fashion_mnist = input_data.read_data_sets('data/fashion/', one_hot=True)
我们将检查数据中包含的内容:
print("No of images in training set {}".format(fashion_mnist.train.images.shape))
print("No of labels in training set {}".format(fashion_mnist.train.labels.shape))
print("No of images in test set {}".format(fashion_mnist.test.images.shape))
print("No of labels in test set {}".format(fashion_mnist.test.labels.shape))
No of images in training set (55000, 784)
No of labels in training set (55000, 10)
No of images in test set (10000, 784)
No of labels in test set (10000, 10)
因此,我们在training set中具有55000数据点,在test set中具有10000数据点。 我们还具有10标签,这意味着我们具有10类别。
我们有10个产品类别,并将为所有这些产品加上标签:
labels = {
0: 'T-shirt/top',
1: 'Trouser',
2: 'Pullover',
3: 'Dress',
4: 'Coat',
5: 'Sandal',
6: 'Shirt',
7: 'Sneaker',
8: 'Bag',
9: 'Ankle boot'
}
现在,我们来看一些图像:
img1 = fashion_mnist.train.images[41].reshape(28,28)
# Get corresponding integer label from one-hot encoded data
label1 = np.where(fashion_mnist.train.labels[41] == 1)[0][0]
# Plot sample
print("y = {} ({})".format(label1, labels[label1]))
plt.imshow(img1, cmap='Greys')
输出和视觉效果如下:
y = 6 (Shirt)

那是一件很不错的衬衫,不是吗? 我们将再看一张图片:
img1 = fashion_mnist.train.images[19].reshape(28,28)
# Get corresponding integer label from one-hot encoded data
label1 = np.where(fashion_mnist.train.labels[19] == 1)[0][0]
# Plot sample
print("y = {} ({})".format(label1, labels[label1]))
plt.imshow(img1, cmap='Greys')
输出和视觉效果如下:
y = 8 (Bag)

这是一个很好的提包!
因此,现在,我们必须建立一个卷积神经网络,该网络实际上将所有这些图像分类为各自的类别。 我们为输入图像和输出标签定义占位符。 由于我们的输入图像的大小为784,因此我们为输入x定义了一个占位符,如下所示:
x = tf.placeholder(tf.float32, [None, 784])
我们需要将输入调整为[p,q,r,s]格式,其中q和r是输入图像的实际大小,即28 x 28,s是通道号。 由于我们只有灰度图像,因此s的值为1。 p表示训练样本的数量,即批量大小。 由于我们不知道批量大小,因此可以将其设置为-1,并且在训练过程中会动态更改它:
x_shaped = tf.reshape(x, [-1, 28, 28, 1])
由于我们有10个不同的标签,因此我们为输出定义了占位符,如下所示:
y = tf.placeholder(tf.float32, [None, 10])
现在,我们需要定义一个称为conv2d的函数,该函数实际上执行卷积运算,即输入矩阵(x)与过滤器(w)的元素级相乘,步长为1。 和SAME填充。
我们设置strides = [1, 1, 1, 1]。 步幅的第一个和最后一个值设置为1,这意味着我们不想在训练样本和不同通道之间移动。 步幅的第二个和第三个值也设置为1,这意味着我们将过滤器在高度和宽度方向上移动1像素:
def conv2d(x, w):
return tf.nn.conv2d(x, w, strides=[1, 1, 1, 1], padding='SAME')
我们定义了一个称为maxpool2d的函数来执行池化操作。 我们以2和SAME填充跨度执行最大池化。 ksize表示我们的合并窗口形状:
def maxpool2d(x):
return tf.nn.max_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
接下来,我们定义权重和偏差。 我们将构建一个具有两个卷积层,然后是一个全连接层和一个输出层的卷积网络,因此我们将定义所有这些层的权重。 权重实际上是卷积层中的过滤器。
因此,权重矩阵将初始化为[filter_shape[0],filter_shape[1], number_of_input_channel, filter_size]。
我们将使用5 x 5过滤器,并将过滤器大小设置为32。 由于我们使用灰度图像,因此我们的输入通道号将为1。 因此,我们的权重矩阵将为[5,5,1,32]:
w_c1 = tf.Variable(tf.random_normal([5,5,1,32]))
当第二个卷积层将来自具有32作为其通道输出的第一卷积层的输入时,到下一层的输入通道数变为32:
w_c2 = tf.Variable(tf.random_normal([5,5,32,64]))
接下来,我们初始化偏差:
b_c1 = tf.Variable(tf.random_normal([32]))
b_c2 = tf.Variable(tf.random_normal([64]))
现在,我们在第一个卷积层上执行操作,即对输入x进行卷积操作,并激活 ReLU,然后进行最大池化:
conv1 = tf.nn.relu(conv2d(x, w_c1) + b_c1)
conv1 = maxpool2d(conv1)
现在,第一个卷积层的结果将传递到下一个卷积层,在此我们对具有 ReLU 激活的第一个卷积层的结果执行卷积运算,然后进行最大池化:
conv2 = tf.nn.relu(conv2d(conv1, w_c2) + b_c2)
conv2 = maxpool2d(conv2)
经过两个带卷积和池化操作的卷积层后,我们的输入图像将从28 * 28 * 1降采样为7 * 7 * 1。 我们需要先平整该输出,然后再将其馈送到全连接层。 然后,第二个卷积层的结果将被馈送到全连接层中,我们将其与权重相乘,添加偏差并应用 ReLU 激活:
x_flattened = tf.reshape(conv2, [-1, 7`7`64])
w_fc = tf.Variable(tf.random_normal([7`7`64,1024]))
b_fc = tf.Variable(tf.random_normal([1024]))
fc = tf.nn.relu(tf.matmul(x_flattened,w_fc)+ b_fc)
现在,我们需要为输出层定义权重和偏差,即[number of neurons in the current layer, number of neurons layer in the next layer]:
w_out = tf.Variable(tf.random_normal([1024, 10]))
b_out = tf.Variable(tf.random_normal([10]))
我们可以通过将全连接层的结果与权重矩阵相乘并加上偏差来获得输出。 我们将使用softmax激活函数来获得输出的概率:
output = tf.matmul(fc, w_out)+ b_out
yhat = tf.nn.softmax(output)
我们可以将损失函数定义为交叉熵损失。 我们将使用一种称为 Adam 优化器的新型优化器,而不是使用梯度下降优化器,来使损失函数最小化。 :
cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=output, labels=y))optimiser = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(cross_entropy)
接下来,我们将如下计算accuracy:
correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(yhat, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
并定义超参数:
epochs = 10
batch_size = 100
现在,我们将开始 TensorFlow 会话并构建模型:
init_op = tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init_op)
total_batch = int(len(fashion_mnist.train.labels) / batch_size)
# For each epoch
for epoch in range(epochs):
avg_cost = 0
for i in range(total_batch):
batch_x, batch_y = fashion_mnist.train.next_batch(batch_size=batch_size)
_, c = sess.run([optimiser, cross_entropy],
feed_dict={x: batch_x, y: batch_y})
avg_cost += c / total_batch
print("Epoch:", (epoch + 1), "cost =""{:.3f}".format(avg_cost))
print(sess.run(accuracy, feed_dict={x: mnist.test.images, y: mnist.test.labels}))
总结
在本章中,我们学习了神经网络的实际工作原理,然后使用 TensorFlow 构建了一个神经网络来对手写数字进行分类。 我们还看到了不同类型的神经网络,例如 RNN,可以记住内存中的信息。 然后,我们看到了 LSTM 网络,该网络用于通过保持多个门来将信息保留在内存中(只要需要)来克服消失的梯度问题。 我们还看到了另一个有趣的神经网络,用于识别图像,称为 CNN。 我们看到了 CNN 如何使用不同的层来理解图像。 之后,我们学习了如何使用 TensorFlow 构建 CNN 以识别时尚产品。
在下一章第 8 章,“使用深度 Q 网络玩 Atari 游戏”中,我们将看到神经网络实际上将如何帮助我们的 RL 智能体更有效地学习。
问题
问题列表如下:
- 线性回归和神经网络有什么区别?
- 激活函数有什么用?
- 为什么我们需要计算梯度下降中的梯度?
- RNN 的优势是什么?
- 什么是消失和爆炸的梯度问题?
- LSTM 中的门是什么?
- 池化层的用途是什么?
进一步阅读
深度学习是一个巨大的话题。 要探索有关深度学习和其他相关算法的更多信息,请查看以下非常有用的链接:
八、深度 Q 网络和 Atari 游戏
深度 Q 网络(DQN)是非常流行且广泛使用的深度强化学习(DRL)算法之一。 实际上,它在发布之后,在强化学习(RL)社区周围引起了很多轰动。 该算法由 Google 的 DeepMind 的研究人员提出,在玩任何 Atari 游戏时,只要将游戏屏幕作为输入,就可以达到人类水平的结果。
在本章中,我们将探讨 DQN 的工作原理,并学习如何通过仅将游戏屏幕作为输入来构建可玩任何 Atari 游戏的 DQN。 我们将研究 DQN 架构的一些改进,例如双重 DQN 和决斗网络架构。
在本章中,您将学习:
- 深度 Q 网络(DQN)
- DQN 的架构
- 建立智能体来玩 Atari 游戏
- 双 DQN
- 优先经验回放
什么是深度 Q 网络?
在继续之前,首先让我们回顾一下 Q 函数。 什么是 Q 函数? Q 函数(也称为状态动作值函数)指定a在s状态下的状态。 因此,我们将每个状态下所有可能动作的值存储在一个称为 Q 表的表中,并选择一个状态下具有最大值的动作作为最佳动作。 还记得我们是如何学习这个 Q 函数的吗? 我们使用了 Q 学习,这是一种非策略性的时差学习算法,用于估计 Q 函数。 我们在第 5 章“时间差异学习”中对此进行了研究。
到目前为止,我们已经看到了状态数量有限且动作有限的环境,并且我们对所有可能的状态动作对进行了详尽搜索,以找到最佳 Q 值。 想想一个环境,我们有很多状态,并且在每个状态下,我们都有很多动作可以尝试。 仔细检查每个状态下的所有操作将非常耗时。 更好的方法是使用某些参数θ将Q函数近似为Q(s, a; θ) ≈ Q*(s, a)。 我们可以使用权重为θ的神经网络来估计每种状态下所有可能动作的Q值。 当我们使用神经网络来近似Q函数时,我们可以称其为 Q 网络。 好的,但是我们如何训练网络,我们的目标函数是什么? 回顾我们的 Q 学习更新规则:
。
r + r maxQ(s', a)是目标值,Q(s, a)是预测值; 我们试图通过学习正确的策略来最大程度地降低这一值。
同样,在 DQN 中,我们可以将损失函数定义为目标值和预测值之间的平方差,并且我们还将尝试通过更新权重θ来最大程度地减少损失:

其中:

我们更新权重,并通过梯度下降使损失最小化。 简而言之,在 DQN 中,我们使用神经网络作为函数近似器来近似Q函数,并且通过梯度下降使误差最小化。
DQN 的架构
现在,我们对 DQN 有了基本的了解,我们将详细介绍 DQN 的工作原理以及用于玩 Atari 游戏的 DQN 的架构。 我们将研究每个组件,然后将整个算法视为一个整体。
卷积网络
DQN 的第一层是卷积网络,网络的输入将是游戏屏幕的原始帧。 因此,我们采用原始框架并将其传递给卷积层以了解游戏状态。 但是原始帧将具有210 x 160像素和 128 个调色板,并且如果我们直接输入原始像素,显然将需要大量的计算和内存。 因此,我们将像素下采样为84 x 84,并将 RGB 值转换为灰度值,然后将经过预处理的游戏屏幕作为卷积层的输入。 卷积层通过识别图像中不同对象之间的空间关系来理解游戏屏幕。 我们使用两个卷积层,然后使用具有 ReLU 作为激活函数的全连接层。 在这里,我们不使用池化层。
当执行诸如对象检测或分类之类的任务时,池层非常有用,其中我们不考虑对象在图像中的位置,而只想知道所需对象是否在图像中。 例如,如果我们要对图像中是否有狗进行分类,我们只查看图像中是否有狗,而不检查狗在哪里。 在那种情况下,使用池化层对图像进行分类,而与狗的位置无关。 但是对于我们来说,要了解游戏屏幕,位置很重要,因为它描述了游戏状态。 例如,在乒乓游戏中,我们不仅要分类游戏屏幕上是否有球。 我们想知道球的位置,以便我们下一步行动。 这就是为什么我们在架构中不使用池化层的原因。
好的,我们如何计算 Q 值? 如果我们传递一个游戏画面和一个动作作为 DQN 的输入,它将给我们 Q 值。 但这将需要一个完整的前向通过,因为一个状态中将有许多动作。 而且,游戏中将有许多状态,每个动作都有一个前移,这在计算上将是昂贵的。 因此,我们只需将游戏屏幕单独作为输入,并通过将输出层中的单元数设置为游戏状态下的动作数,即可获得该状态下所有可能动作的 Q 值。
下图显示了 DQN 的架构,我们在其中馈送了一个游戏屏幕,它提供了该游戏状态下所有动作的 Q 值:

为了预测游戏状态的 Q 值,我们不仅仅使用当前的游戏屏幕; 我们还考虑了过去的四个游戏屏幕。 这是为什么? 考虑“吃豆人”游戏,其中“吃豆人”的目标是移动并吞噬所有点。 仅查看当前的游戏屏幕,我们就无法知道吃豆人的前进方向。 但是,如果我们有过去的游戏画面,我们可以了解吃豆人的发展方向。 我们使用过去的四个游戏屏幕以及当前的游戏屏幕作为输入。
经验回放
我们知道,在 RL 环境中,我们通过执行某些操作a,从一个状态s转移到下一状态s',并获得奖励r。 我们将此转移信息作为<s, a, r, s'>保存在称为回放缓冲区或经验回放的缓冲区中。 这些转移称为智能体的经验。
经验回放的关键思想是,我们使用从回放缓冲区采样的转移来训练深度 Q 网络,而不是使用最后的转移进行训练。 智能体的经历一次相关,因此从回放缓冲区中随机选择一批训练样本将减少智能体的经历之间的相关性,并有助于智能体更好地从广泛的经验中学习。
而且,神经网络将过拟合相关经验,因此通过从答复缓冲区中选择随机的经验批量,我们将减少过拟合。 我们可以使用统一采样来采样经验。 我们可以将经验回放视为队列而不是列表。 回放缓冲区将仅存储固定数量的最新经验,因此,当出现新信息时,我们将删除旧信息:

目标网络
在损失函数中,我们计算目标值和预测值之间的平方差:

我们使用相同的 Q 函数来计算目标值和预测值。 在前面的公式中,您可以看到相同的权重θ用于目标Q和预测的Q。 由于同一网络正在计算预测值和目标值,因此两者之间可能会有很大差异。
为避免此问题,我们使用称为目标网络的单独网络来计算目标值。 因此,我们的损失函数变为:

您可能会注意到目标Q的参数是θ'而不是θ。 我们的实际 Q 网络用于预测Q值,它通过使用梯度下降来学习θ的正确权重。 将目标网络冻结几个时间步,然后通过从实际 Q 网络复制权重来更新目标网络权重。 冻结目标网络一段时间,然后使用实际的 Q 网络权重更新其权重,以稳定训练。
裁剪奖励
我们如何分配奖励? 奖励分配因每个游戏而异。 在某些游戏中,我们可以分配奖励,例如 +1 表示获胜,-1 表示损失,而 0 则不计任何收益,但是在某些其他游戏中,我们必须分配诸如 +100 表示执行某项操作和 +50 表示进行另一项操作的奖励。 为避免出现此问题,我们将所有奖励分别裁剪为 -1 和 +1。
了解算法
现在,我们将了解 DQN 的整体工作方式。 DQN 涉及的步骤如下:
-
首先,我们预处理游戏屏幕(状态
s)并将其馈送到 DQN,DQN 将返回该状态下所有可能动作的Q值。 -
现在,我们使用
ε贪婪策略选择一个动作:对于概率epsilon,我们选择一个随机动作a;对于概率为 1epsilon,我们选择一个具有最大Q的动作值,例如a = argmax(Q(s, a; θ))。 -
在选择动作
a之后,我们在s状态下执行此动作,然后移至新的s'状态并获得奖励。 下一个状态s'是下一个游戏屏幕的预处理图像。 -
我们将此转移存储在
<s,a,r,s'>的回放缓冲区中。 -
接下来,我们从回放缓冲区中抽取一些随机的转移批量并计算损失。
-
我们知道损失就像目标
Q与预测的Q之间的平方差一样。![]()
-
我们针对实际网络参数
θ执行梯度下降,以最大程度地减少这种损失。 -
在每个
k个步骤之后,我们将实际网络权重θ复制到目标网络权重θ'。 -
对于
M个剧集,我们重复这些步骤。
建立智能体来玩 Atari 游戏
现在,我们将看到如何建立一个智能体来玩任何 Atari 游戏。 您可以在此处获得 Jupyter 笔记本的完整代码及其说明。
首先,我们导入所有必需的库:
import numpy as np
import gym
import tensorflow as tf
from tensorflow.contrib.layers import flatten, conv2d, fully_connected
from collections import deque, Counter
import random
from datetime import datetime
在此示例中,我们使用“吃豆人”游戏环境:
env = gym.make("MsPacman-v0")
n_outputs = env.action_space.n
此处显示了吃豆人环境:

现在,我们定义了preprocess_observation函数,用于预处理输入游戏屏幕。 我们减小图像大小并将图像转换为灰度:
color = np.array([210, 164, 74]).mean()
def preprocess_observation(obs):
# Crop and resize the image
img = obs[1:176:2, ::2]
# Convert the image to greyscale
img = img.mean(axis=2)
# Improve image contrast
img[img==color] = 0
# Next we normalize the image from -1 to +1
img = (img - 128) / 128 - 1
return img.reshape(88,80,1)
好的,现在我们定义一个q_network函数来构建我们的 Q 网络。 Q 网络的输入将是游戏状态X。
我们构建一个 Q 网络,该网络包含三个具有相同填充的卷积层,然后是一个全连接层:
tf.reset_default_graph()
def q_network(X, name_scope):
# Initialize layers
initializer = tf.contrib.layers.variance_scaling_initializer()
with tf.variable_scope(name_scope) as scope:
# initialize the convolutional layers
layer_1 = conv2d(X, num_outputs=32, kernel_size=(8,8), stride=4, padding='SAME', weights_initializer=initializer)
tf.summary.histogram('layer_1',layer_1)
layer_2 = conv2d(layer_1, num_outputs=64, kernel_size=(4,4), stride=2, padding='SAME', weights_initializer=initializer)
tf.summary.histogram('layer_2',layer_2)
layer_3 = conv2d(layer_2, num_outputs=64, kernel_size=(3,3), stride=1, padding='SAME', weights_initializer=initializer)
tf.summary.histogram('layer_3',layer_3)
# Flatten the result of layer_3 before feeding to the
# fully connected layer
flat = flatten(layer_3)
fc = fully_connected(flat, num_outputs=128, weights_initializer=initializer)
tf.summary.histogram('fc',fc)
output = fully_connected(fc, num_outputs=n_outputs, activation_fn=None, weights_initializer=initializer)
tf.summary.histogram('output',output)
# Vars will store the parameters of the network such as weights
vars = {v.name[len(scope.name):]: v for v in tf.get_collection(key=tf.GraphKeys.TRAINABLE_VARIABLES, scope=scope.name)}
return vars, output
接下来,我们定义一个epsilon_greedy函数以执行ε贪婪策略。 在ε贪婪策略中,我们选择概率为 1epsilon的最佳操作,或者选择概率为epsilon的随机操作。
我们使用衰减的epsilon贪婪策略,其中epsilon的值会随着时间的流逝而衰减,因为我们不想永远探索。 因此,随着时间的流逝,我们的策略只会利用良好的行动:
epsilon = 0.5
eps_min = 0.05
eps_max = 1.0
eps_decay_steps = 500000
def epsilon_greedy(action, step):
p = np.random.random(1).squeeze()
epsilon = max(eps_min, eps_max - (eps_max-eps_min) * step/eps_decay_steps)
if np.random.rand() < epsilon:
return np.random.randint(n_outputs)
else:
return action
现在,我们初始化长度为 20000 的经验回放缓冲区,其中包含该经验。
我们将智能体的所有经验(状态,动作,奖励)存储在经验回放缓冲区中,并抽样此小批经验来训练网络:
def sample_memories(batch_size):
perm_batch = np.random.permutation(len(exp_buffer))[:batch_size]
mem = np.array(exp_buffer)[perm_batch]
return mem[:,0], mem[:,1], mem[:,2], mem[:,3], mem[:,4]
接下来,我们定义所有超参数:
num_episodes = 800
batch_size = 48
input_shape = (None, 88, 80, 1)
learning_rate = 0.001
X_shape = (None, 88, 80, 1)
discount_factor = 0.97
global_step = 0
copy_steps = 100
steps_train = 4
start_steps = 2000
logdir = 'logs'
现在,我们为输入定义placeholder,例如游戏状态:
X = tf.placeholder(tf.float32, shape=X_shape)
我们定义一个布尔值in_training_mode来切换训练:
in_training_mode = tf.placeholder(tf.bool)
我们构建我们的 Q 网络,该网络接受输入X并为状态中的所有动作生成 Q 值:
mainQ, mainQ_outputs = q_network(X, 'mainQ')
同样,我们建立目标 Q 网络:
targetQ, targetQ_outputs = q_network(X, 'targetQ')
为我们的动作值定义placeholder:
X_action = tf.placeholder(tf.int32, shape=(None,))
Q_action = tf.reduce_sum(targetQ_outputs * tf.one_hot(X_action, n_outputs), axis=-1, keep_dims=True)
将主要 Q 网络参数复制到目标 Q 网络:
copy_op = [tf.assign(main_name, targetQ[var_name]) for var_name, main_name in mainQ.items()]
copy_target_to_main = tf.group(*copy_op)
为我们的输出定义一个placeholder,例如动作:
y = tf.placeholder(tf.float32, shape=(None,1))
现在我们计算损失,它是实际值和预测值之间的差:
loss = tf.reduce_mean(tf.square(y - Q_action))
我们使用AdamOptimizer来最大程度地减少损失:
optimizer = tf.train.AdamOptimizer(learning_rate)
training_op = optimizer.minimize(loss)
在 TensorBoard 中设置日志文件以进行可视化:
loss_summary = tf.summary.scalar('LOSS', loss)
merge_summary = tf.summary.merge_all()
file_writer = tf.summary.FileWriter(logdir, tf.get_default_graph())
接下来,我们启动 TensorFlow 会话并运行模型:
init = tf.global_variables_initializer()
with tf.Session() as sess:
init.run()
# for each episode
for i in range(num_episodes):
done = False
obs = env.reset()
epoch = 0
episodic_reward = 0
actions_counter = Counter()
episodic_loss = []
# while the state is not the terminal state
while not done:
#env.render()
# get the preprocessed game screen
obs = preprocess_observation(obs)
# feed the game screen and get the Q values for each action
actions = mainQ_outputs.eval(feed_dict={X:[obs], in_training_mode:False})
# get the action
action = np.argmax(actions, axis=-1)
actions_counter[str(action)] += 1
# select the action using epsilon greedy policy
action = epsilon_greedy(action, global_step)
# now perform the action and move to the next state,
# next_obs, receive reward
next_obs, reward, done, _ = env.step(action)
# Store this transition as an experience in the replay buffer
exp_buffer.append([obs, action, preprocess_observation(next_obs), reward, done])
# After certain steps, we train our Q network with samples from the experience replay buffer
if global_step % steps_train == 0 and global_step > start_steps:
# sample experience
o_obs, o_act, o_next_obs, o_rew, o_done = sample_memories(batch_size)
# states
o_obs = [x for x in o_obs]
# next states
o_next_obs = [x for x in o_next_obs]
# next actions
next_act = mainQ_outputs.eval(feed_dict={X:o_next_obs, in_training_mode:False})
# reward
y_batch = o_rew + discount_factor * np.max(next_act, axis=-1) * (1-o_done)
# merge all summaries and write to the file
mrg_summary = merge_summary.eval(feed_dict={X:o_obs, y:np.expand_dims(y_batch, axis=-1), X_action:o_act, in_training_mode:False})
file_writer.add_summary(mrg_summary, global_step)
# now we train the network and calculate loss
train_loss, _ = sess.run([loss, training_op], feed_dict={X:o_obs, y:np.expand_dims(y_batch, axis=-1), X_action:o_act, in_training_mode:True})
episodic_loss.append(train_loss)
# after some interval we copy our main Q network weights to target Q network
if (global_step+1) % copy_steps == 0 and global_step > start_steps:
copy_target_to_main.run()
obs = next_obs
epoch += 1
global_step += 1
episodic_reward += reward
print('Epoch', epoch, 'Reward', episodic_reward,)
您可以看到如下输出:

我们可以在 TensorBoard 中看到 DQN 的计算图,如下所示:

我们可以在主网络和目标网络中可视化权重的分布:

我们还可以看到损失:

双 DQN
深度 Q 学习非常酷,对吗? 它已经普及了玩任何 Atari 游戏的学习。 但是 DQN 的问题在于,它倾向于高估Q值。 这是因为Q学习方程式中的最大值运算符。 最大运算符在选择和评估动作时使用相同的值。 那是什么意思?假设我们处于s状态,并且我们有五个动作a₁至a₅。 假设a₃是最好的动作。 当我们估计状态为s的所有这些动作的Q值时,估计的Q值会有些杂音并且与实际值有所不同。 由于这种噪声,动作a₂会比最佳动作a₃获得更高的值。 现在,如果我们选择最佳动作作为具有最大值的动作,则最终将选择次优动作a₂而不是最佳动作a₃。
我们可以通过具有两个单独的Q函数来解决此问题,每个函数都是独立学习的。 一个Q函数用于选择一个动作,另一个Q函数用于评估一个动作。 我们可以通过调整 DQN 的目标函数来实现。 调用 DQN 的目标函数:

我们可以如下修改目标函数:

在前面的公式中,我们有两个Q函数,每个函数具有不同的权重。 因此,权重为θ'的Q函数用于选择操作,权重为θ-的其他Q函数用于评估操作。 我们还可以切换这两个Q函数的角色。
优先经验回放
在 DQN 架构中,我们使用经验回放来消除训练样本之间的相关性。 但是,从记忆回放中均匀采样转移不是最佳方法。 相反,我们可以确定转换的优先级并根据优先级进行采样。 优先安排转移有助于网络快速有效地学习。 我们如何确定转移的优先级? 我们优先考虑具有较高 TD 误差的转换。 我们知道,TD 误差指定了估计的 Q 值和实际 Q 值之间的差。 因此,具有较高 TD 误差的转移是我们必须关注和学习的转移,因为这些转移与我们的估计背道而驰。 凭直觉,让我们说您尝试解决一系列问题,但是您无法解决其中两个问题。 然后,您仅将这两个问题放在首位,以专注于问题所在并尝试解决该问题:

我们使用两种类型的优先级:比例优先级和基于等级的优先级。
在比例优先级中,我们将优先级定义为:

pᵢ是转换的优先级i,pᵢ是转换的 TD 误差i,而ε只是一些正常数,可确保每次转换具有非零优先级。 当δ为零时,添加ε将使转换具有优先级而不是零优先级。 但是,转换的优先级要比δ不为零的转换低。 指数α表示使用的优先级数量。 当α为零时,则仅是均匀情况。
现在,我们可以使用以下公式将此优先级转换为概率:

在基于等级的优先级划分中,我们将优先级定义为:

rank(i)指定转移i在回放缓冲区中的位置,在该位置中,转移从高 TD 误差到低 TD 误差被分类。 计算优先级后,我们可以使用相同的公式将优先级转换为概率:

决斗网络架构
我们知道Q函数指定智能体在状态s下执行动作a有多好,而值函数则指定使智能体处于s状态有多好。 现在,我们引入一个称为优势函数的新函数,该函数可以定义为值函数和优势函数之间的差。 优势函数指定与其他动作相比,智能体执行一个动作a有多好。
因此,值函数指定状态的优劣,而优势函数指定动作的优劣。 如果我们将值函数和优势函数结合起来会发生什么? 这将告诉我们智能体在状态s实际上是我们的Q函数下执行动作a有多好。 因此,我们可以像Q(s, a) = V(s) + A(a)中那样将Q函数定义为值函数和优势函数的和。
现在,我们将看到决斗网络架构是如何工作的。 下图显示了决斗 DQN 的架构:

决斗 DQN 的架构与 DQN 基本上相同,只是在末端的全连接层分为两个流。 一个流计算值函数,而另一个流计算优势函数。 最后,我们使用聚合层组合这两个流,并获得 Q 函数。
为什么我们必须将我们的 Q 函数计算分成两个流? 在许多状态,计算所有动作的值估计并不重要,尤其是当我们在一个状态中有较大的动作空间时; 那么大多数动作将不会对状态产生任何影响。 同样,可能会有许多具有冗余效果的动作。 在这些情况下,与现有 DQN 架构相比,决斗 DQN 可以更精确地估计 Q 值:
- 当我们在状态中有大量动作时,并且估计每个动作的值并不是很重要时,第一个流与值函数流中一样有用。
- 与优势函数流一样,第二个流在网络必须决定优先选择哪个操作的情况下很有用
聚合器层合并这两个流的值,并产生Q函数。 因此,决斗网络比标准 DQN 架构更有效,更健壮。
总结
在本章中,我们学习了一种非常流行的深度强化学习算法,称为 DQN。 我们看到了如何使用深度神经网络来近似 Q 函数。 我们还学习了如何建立智能体来玩 Atari 游戏。 后来,我们研究了 DQN 的一些改进,例如双 DQN,它用于避免高估 Q 值。 然后,我们研究了优先级经验回放,优先级经验和决斗的网络架构,该架构将 Q 函数计算分为两个流,分别称为值流和优势流。
在下一章第 9 章,“实用深度循环 Q 网络玩末日之战”中,我们将介绍一种称为 DRQN 的非常酷的 DQN 变体,它利用 RNN 近似于一个 Q 函数。
问题
问题列表如下:
- 什么是 DQN?
- 经验回放有什么需要?
- 为什么我们要保留一个单独的目标网络?
- 为什么 DQN 高估了?
- 双重 DQN 如何避免高估 Q 值?
- 优先经验回放中的优先经验是怎么样的?
- 决斗架构有什么需求?
进一步阅读
九、用深度循环 Q 网络玩《毁灭战士》
在上一章中,我们介绍了如何使用深度 Q 网络(DQN)构建智能体以玩 Atari 游戏。 我们利用神经网络来近似 Q 函数,使用了卷积神经网络(CNN)来理解输入游戏画面,并利用过去的四个游戏画面来更好地理解当前的游戏状态。 在本章中,我们将学习如何利用循环神经网络(RNN)来提高 DQN 的表现。 我们还将研究马尔可夫决策过程(MDP)的部分可观察之处,以及如何使用深度循环 Q 网络(DRQN)。 接下来,我们将学习如何构建一个智能体来使用 DRQN 玩《毁灭战士》游戏。 最后,我们将看到 DRQN 的一种变体,称为深度注意力循环 Q 网络(DARQN),它增强了 DRQN 架构的注意力机制。
在本章中,您将学习以下主题:
- DRQN
- 部分可观察的 MDP
- DRQN 的架构
- 如何建立智能体以使用 DRQN 玩《毁灭战士》游戏
- DARQN
DRQN
那么,当我们在 Atari 游戏中以人为水平执行 DQN 时,为什么我们需要 DRQN? 为了回答这个问题,让我们理解部分可观察的马尔可夫决策过程(POMDP)的问题。 当我们可获得的关于环境的信息有限时,该环境称为部分可观察的 MDP。 到目前为止,在前面的章节中,我们已经看到了一个完全可观察的 MDP,在其中我们了解所有可能的动作和状态-尽管该智能体可能不知道转移和奖励的可能性,但它对环境有完整的了解,例如,冰冻的湖泊环境,我们清楚地知道了环境的所有状态和行为; 我们轻松地将该环境建模为一个完全可观察的 MDP。 但是大多数现实世界环境只能部分观察到。 我们看不到所有状态。 考虑智能体学习如何在现实环境中行走; 显然,智能体将不会完全了解环境,它将无法获得任何信息。 在 POMDP 中,状态仅提供部分信息,但是将有关过去状态的信息保留在内存中可能会帮助智能体更好地了解环境的性质并改善策略。 因此,在 POMDP 中,我们需要保留有关先前状态的信息,以便采取最佳措施。
为了回顾我们在前几章中学到的知识,请考虑以下所示的乒乓游戏。 仅通过查看当前的游戏屏幕,我们就可以知道球的位置,但是我们还需要知道球的运动方向和球的速度,以便采取最佳行动。 但是,仅查看当前的游戏屏幕并不能告诉我们球的方向和速度:

为了克服这个问题,我们将不仅仅考虑当前的游戏屏幕,而将使用过去的四个游戏屏幕来了解球的方向和速度。 这就是我们在 DQN 中看到的。 我们将过去的四个游戏屏幕以及当前的游戏屏幕作为输入输入到卷积层,并接收该状态下所有可能动作的 Q 值。 但是,您认为仅使用过去的四个屏幕将有助于我们了解不同的环境吗? 在某些环境下,我们甚至可能需要过去的 100 个游戏屏幕来更好地了解当前游戏状态。 但是,堆叠过去的n游戏画面会减慢我们的训练过程,而且还会增加我们的经验回放缓冲区的大小。
因此,只要需要,我们就可以利用 RNN 的优势来理解和保留有关先前状态的信息。 在第 7 章“深度学习基础知识”中,我们了解了如何通过保留,忘记和更新所需的信息,将长短期记忆循环神经网络(LSTM RNN)用于生成文本以及了解文本的上下文。 我们将通过扩展 LSTM 层来修改 DQN 架构,以了解先前的信息。 在 DQN 架构中,我们用 LSTM RNN 替换了第一卷积后全连接层。 这样,我们也可以解决部分可观察性的问题,因为现在我们的智能体可以记住过去的状态并可以改进策略。
DRQN 的架构
接下来显示 DRQN 的架构。 它类似于 DQN,但是我们用 LSTM RNN 替换了第一卷积后全连接层,如下所示:

因此,我们将游戏屏幕作为卷积层的输入。 卷积层对图像进行卷积并生成特征映射。 然后将生成的特征映射传递到 LSTM 层。 LSTM 层具有用于保存信息的内存。 LSTM 层保留有关重要的先前游戏状态的信息,并根据需要随时间步长更新其内存。 穿过全连接层后,它将输出Q值。 因此,与 DQN 不同,我们不直接估计Q(s[t], a[t])。 相反,我们估计Q(h[t], a[t]),其中hₜ是网络在上一个时间步长返回的输入。 即,h[t] = LSTM(h[t-1], o[t])。 当我们使用 RNN 时,我们通过时间的反向传播来训练我们的网络。
等待。 经验回放缓冲区如何? 在 DQN 中,为避免相关的经验,我们使用了经验回放,该经验存储了游戏的转移,并使用了随机的一组经验来训练网络。 对于 DRQN,我们将整个剧集存储在经验缓冲区中,并从随机的剧集批量中随机采样n个步骤。 因此,通过这种方式,我们既可以适应随机化,又可以适应另一种实际的经验。
训练智能体玩《毁灭战士》
毁灭战士是一款非常受欢迎的第一人称射击游戏。 游戏的目标是杀死怪物。 末日是部分可观察的 MDP 的另一个示例,因为智能体(玩家)的视角限制为 90 度。 该智能体对其余环境一无所知。 现在,我们将看到如何使用 DRQN 来训练我们的经纪人玩《毁灭战士》。
代替 OpenAI Gym,我们将使用 ViZDoom 包来模拟 Doom 环境以训练我们的智能体。 要了解有关 ViZDoom 包的更多信息,请访问其官方网站。 我们可以使用以下命令简单地安装 ViZDoom:
pip install vizdoom
ViZDoom 提供了许多 Doom 方案,可以在包文件夹vizdoom/scenarios中找到这些方案。
基本《毁灭战士》游戏
在开始之前,让我们通过看一个基本示例来熟悉vizdoom环境:
- 让我们加载必要的库:
from vizdoom import *
import random
import time
- 为
DoomGame创建一个实例:
game = DoomGame()
- 众所周知,ViZDoom 提供了很多 Doom 方案,让我们加载基本方案:
game.load_config("basic.cfg")
init()方法使用场景初始化游戏:
game.init()
- 现在,让我们定义一个带有热编码的
actions的代码:
shoot = [0, 0, 1]
left = [1, 0, 0]
right = [0, 1, 0]
actions = [shoot, left, right]
- 现在,让我们开始玩游戏:
no_of_episodes = 10
for i in range(no_of_episodes):
# for each episode start the game
game.new_episode()
# loop until the episode is over
while not game.is_episode_finished():
# get the game state
state = game.get_state()
img = state.screen_buffer
# get the game variables
misc = state.game_variables
# perform some action randomly and receive reward
reward = game.make_action(random.choice(actions))
print(reward)
# we will set some time before starting the next episode
time.sleep(2)
运行程序后,可以看到如下输出:

DRQN 的《毁灭战士》
现在,让我们看看如何利用 DRQN 算法来训练我们的智能体玩《毁灭战士》。 我们为成功杀死怪物分配正面奖励,为失去生命,自杀和失去弹药(子弹)分配负面奖励。 您可以在这里获得 Jupyter 笔记本的完整代码及其解释。 本节中使用的代码的权利归于 Luthanicus。
首先,让我们导入所有必需的库:
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
from vizdoom import *
import timeit
import math
import os
import sys
现在,让我们定义get_input_shape函数,以在卷积层卷积后计算输入图像的最终形状:
def get_input_shape(Image,Filter,Stride):
layer1 = math.ceil(((Image - Filter + 1) / Stride))
o1 = math.ceil((layer1 / Stride))
layer2 = math.ceil(((o1 - Filter + 1) / Stride))
o2 = math.ceil((layer2 / Stride))
layer3 = math.ceil(((o2 - Filter + 1) / Stride))
o3 = math.ceil((layer3 / Stride))
return int(o3)
现在,我们将定义DRQN类,该类实现了 DRQN 算法。 检查每行代码之前的注释以了解它:
class DRQN():
def __init__(self, input_shape, num_actions, initial_learning_rate):
# first, we initialize all the hyperparameters
self.tfcast_type = tf.float32
# shape of our input, which would be (length, width, channels)
self.input_shape = input_shape
# number of actions in the environment
self.num_actions = num_actions
# learning rate for the neural network
self.learning_rate = initial_learning_rate
# now we will define the hyperparameters of the convolutional neural network
# filter size
self.filter_size = 5
# number of filters
self.num_filters = [16, 32, 64]
# stride size
self.stride = 2
# pool size
self.poolsize = 2
# shape of our convolutional layer
self.convolution_shape = get_input_shape(input_shape[0], self.filter_size, self.stride) * get_input_shape(input_shape[1], self.filter_size, self.stride) * self.num_filters[2]
# now, we define the hyperparameters of our recurrent neural network and the final feed forward layer
# number of neurons
self.cell_size = 100
# number of hidden layers
self.hidden_layer = 50
# drop out probability
self.dropout_probability = [0.3, 0.2]
# hyperparameters for optimization
self.loss_decay_rate = 0.96
self.loss_decay_steps = 180
# initialize all the variables for the CNN
# we initialize the placeholder for input whose shape would be (length, width, channel)
self.input = tf.placeholder(shape = (self.input_shape[0], self.input_shape[1], self.input_shape[2]), dtype = self.tfcast_type)
# we will also initialize the shape of the target vector whose shape is equal to the number of actions
self.target_vector = tf.placeholder(shape = (self.num_actions, 1), dtype = self.tfcast_type)
# initialize feature maps for our corresponding 3 filters
self.features1 = tf.Variable(initial_value = np.random.rand(self.filter_size, self.filter_size, input_shape[2], self.num_filters[0]),
dtype = self.tfcast_type)
self.features2 = tf.Variable(initial_value = np.random.rand(self.filter_size, self.filter_size, self.num_filters[0], self.num_filters[1]),
dtype = self.tfcast_type)
self.features3 = tf.Variable(initial_value = np.random.rand(self.filter_size, self.filter_size, self.num_filters[1], self.num_filters[2]),
dtype = self.tfcast_type)
# initialize variables for RNN
# recall how RNN works from chapter 7
self.h = tf.Variable(initial_value = np.zeros((1, self.cell_size)), dtype = self.tfcast_type)
# hidden to hidden weight matrix
self.rW = tf.Variable(initial_value = np.random.uniform(
low = -np.sqrt(6\. / (self.convolution_shape + self.cell_size)),
high = np.sqrt(6\. / (self.convolution_shape + self.cell_size)),
size = (self.convolution_shape, self.cell_size)),
dtype = self.tfcast_type)
# input to hidden weight matrix
self.rU = tf.Variable(initial_value = np.random.uniform(
low = -np.sqrt(6\. / (2 * self.cell_size)),
high = np.sqrt(6\. / (2 * self.cell_size)),
size = (self.cell_size, self.cell_size)),
dtype = self.tfcast_type)
# hidden to output weight matrix
self.rV = tf.Variable(initial_value = np.random.uniform(
low = -np.sqrt(6\. / (2 * self.cell_size)),
high = np.sqrt(6\. / (2 * self.cell_size)),
size = (self.cell_size, self.cell_size)),
dtype = self.tfcast_type)
# bias
self.rb = tf.Variable(initial_value = np.zeros(self.cell_size), dtype = self.tfcast_type)
self.rc = tf.Variable(initial_value = np.zeros(self.cell_size), dtype = self.tfcast_type)
# initialize weights and bias of feed forward network
# weights
self.fW = tf.Variable(initial_value = np.random.uniform(
low = -np.sqrt(6\. / (self.cell_size + self.num_actions)),
high = np.sqrt(6\. / (self.cell_size + self.num_actions)),
size = (self.cell_size, self.num_actions)),
dtype = self.tfcast_type)
# bias
self.fb = tf.Variable(initial_value = np.zeros(self.num_actions), dtype = self.tfcast_type)
# learning rate
self.step_count = tf.Variable(initial_value = 0, dtype = self.tfcast_type)
self.learning_rate = tf.train.exponential_decay(self.learning_rate,
self.step_count,
self.loss_decay_steps,
self.loss_decay_steps,
staircase = False)
# now let us build the network
# first convolutional layer
self.conv1 = tf.nn.conv2d(input = tf.reshape(self.input, shape = (1, self.input_shape[0], self.input_shape[1], self.input_shape[2])), filter = self.features1, strides = [1, self.stride, self.stride, 1], padding = "VALID")
self.relu1 = tf.nn.relu(self.conv1)
self.pool1 = tf.nn.max_pool(self.relu1, ksize = [1, self.poolsize, self.poolsize, 1], strides = [1, self.stride, self.stride, 1], padding = "SAME")
# second convolutional layer
self.conv2 = tf.nn.conv2d(input = self.pool1, filter = self.features2, strides = [1, self.stride, self.stride, 1], padding = "VALID")
self.relu2 = tf.nn.relu(self.conv2)
self.pool2 = tf.nn.max_pool(self.relu2, ksize = [1, self.poolsize, self.poolsize, 1], strides = [1, self.stride, self.stride, 1], padding = "SAME")
# third convolutional layer
self.conv3 = tf.nn.conv2d(input = self.pool2, filter = self.features3, strides = [1, self.stride, self.stride, 1], padding = "VALID")
self.relu3 = tf.nn.relu(self.conv3)
self.pool3 = tf.nn.max_pool(self.relu3, ksize = [1, self.poolsize, self.poolsize, 1], strides = [1, self.stride, self.stride, 1], padding = "SAME")
# add dropout and reshape the input
self.drop1 = tf.nn.dropout(self.pool3, self.dropout_probability[0])
self.reshaped_input = tf.reshape(self.drop1, shape = [1, -1])
# now we build the recurrent neural network, which takes the input from the last layer of the convolutional network
self.h = tf.tanh(tf.matmul(self.reshaped_input, self.rW) + tf.matmul(self.h, self.rU) + self.rb)
self.o = tf.nn.softmax(tf.matmul(self.h, self.rV) + self.rc)
# add drop out to RNN
self.drop2 = tf.nn.dropout(self.o, self.dropout_probability[1])
# we feed the result of RNN to the feed forward layer
self.output = tf.reshape(tf.matmul(self.drop2, self.fW) + self.fb, shape = [-1, 1])
self.prediction = tf.argmax(self.output)
# compute loss
self.loss = tf.reduce_mean(tf.square(self.target_vector - self.output))
# we use Adam optimizer for minimizing the error
self.optimizer = tf.train.AdamOptimizer(self.learning_rate)
# compute gradients of the loss and update the gradients
self.gradients = self.optimizer.compute_gradients(self.loss)
self.update = self.optimizer.apply_gradients(self.gradients)
self.parameters = (self.features1, self.features2, self.features3,
self.rW, self.rU, self.rV, self.rb, self.rc,
self.fW, self.fb)
现在,我们定义ExperienceReplay类以实现经验回放缓冲区。 我们将智能体的所有经验(即状态,动作和奖励)存储在经验回放缓冲区中,并且我们抽取了这一小批量经验来训练网络:
class ExperienceReplay():
def __init__(self, buffer_size):
# buffer for holding the transition
self.buffer = []
# size of the buffer
self.buffer_size = buffer_size
# we remove the old transition if the buffer size has reached it's limit. Think off the buffer as a queue, when the new
# one comes, the old one goes off
def appendToBuffer(self, memory_tuplet):
if len(self.buffer) > self.buffer_size:
for i in range(len(self.buffer) - self.buffer_size):
self.buffer.remove(self.buffer[0])
self.buffer.append(memory_tuplet)
# define a function called sample for sampling some random n number of transitions
def sample(self, n):
memories = []
for i in range(n):
memory_index = np.random.randint(0, len(self.buffer))
memories.append(self.buffer[memory_index])
return memories
现在,我们定义train函数来训练我们的网络:
def train(num_episodes, episode_length, learning_rate, scenario = "deathmatch.cfg", map_path = 'map02', render = False):
# discount parameter for Q-value computation
discount_factor = .99
# frequency for updating the experience in the buffer
update_frequency = 5
store_frequency = 50
# for printing the output
print_frequency = 1000
# initialize variables for storing total rewards and total loss
total_reward = 0
total_loss = 0
old_q_value = 0
# initialize lists for storing the episodic rewards and losses
rewards = []
losses = []
# okay, now let us get to the action!
# first, we initialize our doomgame environment
game = DoomGame()
# specify the path where our scenario file is located
game.set_doom_scenario_path(scenario)
# specify the path of map file
game.set_doom_map(map_path)
# then we set screen resolution and screen format
game.set_screen_resolution(ScreenResolution.RES_256X160)
game.set_screen_format(ScreenFormat.RGB24)
# we can add particles and effects we needed by simply setting them to true or false
game.set_render_hud(False)
game.set_render_minimal_hud(False)
game.set_render_crosshair(False)
game.set_render_weapon(True)
game.set_render_decals(False)
game.set_render_particles(False)
game.set_render_effects_sprites(False)
game.set_render_messages(False)
game.set_render_corpses(False)
game.set_render_screen_flashes(True)
# now we will specify buttons that should be available to the agent
game.add_available_button(Button.MOVE_LEFT)
game.add_available_button(Button.MOVE_RIGHT)
game.add_available_button(Button.TURN_LEFT)
game.add_available_button(Button.TURN_RIGHT)
game.add_available_button(Button.MOVE_FORWARD)
game.add_available_button(Button.MOVE_BACKWARD)
game.add_available_button(Button.ATTACK)
# okay, now we will add one more button called delta. The preceding button will only
# work like keyboard keys and will have only boolean values.
# so we use delta button, which emulates a mouse device which will have positive and negative values
# and it will be useful in environment for exploring
game.add_available_button(Button.TURN_LEFT_RIGHT_DELTA, 90)
game.add_available_button(Button.LOOK_UP_DOWN_DELTA, 90)
# initialize an array for actions
actions = np.zeros((game.get_available_buttons_size(), game.get_available_buttons_size()))
count = 0
for i in actions:
i[count] = 1
count += 1
actions = actions.astype(int).tolist()
# then we add the game variables, ammo, health, and killcount
game.add_available_game_variable(GameVariable.AMMO0)
game.add_available_game_variable(GameVariable.HEALTH)
game.add_available_game_variable(GameVariable.KILLCOUNT)
# we set episode_timeout to terminate the episode after some time step
# we also set episode_start_time which is useful for skipping initial events
game.set_episode_timeout(6 * episode_length)
game.set_episode_start_time(10)
game.set_window_visible(render)
# we can also enable sound by setting set_sound_enable to true
game.set_sound_enabled(False)
# we set living reward to 0, which rewards the agent for each move it does even though the move is not useful
game.set_living_reward(0)
# doom has different modes such as player, spectator, asynchronous player, and asynchronous spectator
# in spectator mode humans will play and agent will learn from it.
# in player mode, the agent actually plays the game, so we use player mode.
game.set_mode(Mode.PLAYER)
# okay, So now we, initialize the game environment
game.init()
# now, let us create instance to our DRQN class and create our both actor and target DRQN networks
actionDRQN = DRQN((160, 256, 3), game.get_available_buttons_size() - 2, learning_rate)
targetDRQN = DRQN((160, 256, 3), game.get_available_buttons_size() - 2, learning_rate)
# we will also create an instance to the ExperienceReplay class with the buffer size of 1000
experiences = ExperienceReplay(1000)
# for storing the models
saver = tf.train.Saver({v.name: v for v in actionDRQN.parameters}, max_to_keep = 1)
# now let us start the training process
# we initialize variables for sampling and storing transitions from the experience buffer
sample = 5
store = 50
# start the tensorflow session
with tf.Session() as sess:
# initialize all tensorflow variables
sess.run(tf.global_variables_initializer())
for episode in range(num_episodes):
# start the new episode
game.new_episode()
# play the episode till it reaches the episode length
for frame in range(episode_length):
# get the game state
state = game.get_state()
s = state.screen_buffer
# select the action
a = actionDRQN.prediction.eval(feed_dict = {actionDRQN.input: s})[0]
action = actions[a]
# perform the action and store the reward
reward = game.make_action(action)
# update total reward
total_reward += reward
# if the episode is over then break
if game.is_episode_finished():
break
# store the transition to our experience buffer
if (frame % store) == 0:
experiences.appendToBuffer((s, action, reward))
# sample experience from the experience buffer
if (frame % sample) == 0:
memory = experiences.sample(1)
mem_frame = memory[0][0]
mem_reward = memory[0][2]
# now, train the network
Q1 = actionDRQN.output.eval(feed_dict = {actionDRQN.input: mem_frame})
Q2 = targetDRQN.output.eval(feed_dict = {targetDRQN.input: mem_frame})
# set learning rate
learning_rate = actionDRQN.learning_rate.eval()
# calculate Q value
Qtarget = old_q_value + learning_rate * (mem_reward + discount_factor * Q2 - old_q_value)
# update old Q value
old_q_value = Qtarget
# compute Loss
loss = actionDRQN.loss.eval(feed_dict = {actionDRQN.target_vector: Qtarget, actionDRQN.input: mem_frame})
# update total loss
total_loss += loss
# update both networks
actionDRQN.update.run(feed_dict = {actionDRQN.target_vector: Qtarget, actionDRQN.input: mem_frame})
targetDRQN.update.run(feed_dict = {targetDRQN.target_vector: Qtarget, targetDRQN.input: mem_frame})
rewards.append((episode, total_reward))
losses.append((episode, total_loss))
print("Episode %d - Reward = %.3f, Loss = %.3f." % (episode, total_reward, total_loss))
total_reward = 0
total_loss = 0
让我们训练10000剧集,其中每个剧集的长度为300:
train(num_episodes = 10000, episode_length = 300, learning_rate = 0.01, render = True)
运行该程序时,可以看到如下所示的输出,还可以看到我们的智能体如何通过剧集学习:

DARQN
我们通过添加可捕获时间依赖性的循环层来改进 DQN 架构,我们将其称为 DRQN。 您认为我们可以进一步改善我们的 DRQN 架构吗? 是。 通过在卷积层之上添加关注层,我们可以进一步改善我们的 DRQN 架构。 那么,关注层的功能是什么? 注意意味着单词的字面意思。 注意机制广泛用于图像字幕,对象检测等。 考虑神经网络为图像添加字幕的任务; 为了理解图像中的内容,网络必须注意图像中的特定对象以生成字幕。
类似地,当我们在 DRQN 中添加关注层时,我们可以选择并关注图像的小区域,最终这会减少网络中的参数数量,并减少训练和测试时间。 与 DRQN 不同,DARQN 中的 LSTM 层不仅存储了先前的状态信息以采取下一个最佳操作,而且还保留了先前的状态信息。 它还存储用于确定下一个图像焦点的信息。
DARQN 的架构
DARQN 的架构如下所示:

它由三层组成; 卷积层,注意力层和 LSTM 循环层。 游戏屏幕作为图像被馈送到卷积网络。 卷积网络处理图像并生成特征映射。 然后,特征贴图会进入关注层。 注意层将它们转换为向量,并产生它们的线性组合,称为上下文向量。 然后将上下文向量以及先前的隐藏状态传递到 LSTM 层。 LSTM 层提供两个输出; 一方面,它提供 Q 值来决定在某种状态下要执行的动作;另一方面,它可以帮助注意力网络确定在下一个时间步中要关注的图像区域,从而可以生成更好的上下文向量。 。
注意有两种类型:
- 软注意力:我们知道,卷积层产生的特征映射将作为输入提供给关注层,然后生成上下文向量。 轻描淡写地,这些上下文向量只是卷积层产生的所有输出(特征映射)的加权平均值。 根据功能的相对重要性选择权重。
- 硬注意力:硬注意力,根据某些位置选择策略
π,我们仅关注图像在特定时间步长t上的特定位置。 该策略由神经网络表示,其权重是策略参数,网络的输出是位置选择概率。 但是,硬注意力不比软注意力好多少。
总结
在本章中,我们学习了如何使用 DRQN 记住有关先前状态的信息,以及它如何克服部分可观察的 MDP 问题。 我们已经看到了如何训练我们的智能体使用 DRQN 算法玩《毁灭战士》游戏。 我们还了解了 DARQN 作为 DRQN 的改进,它在卷积层的顶部增加了一个关注层。 在此之后,我们看到了两种类型的注意力机制: 即软硬关注。
在下一章第 10 章,“异步优势演员评论家网络”中,我们将学习另一种有趣的深度强化学习算法,称为异步优势演员评论家网络。
问题
问题列表如下:
- DQN 和 DRQN 有什么区别?
- DQN 的缺点是什么?
- 我们如何在 DQN 中设置经验回放?
- DRQN 和 DARQN 有什么区别?
- 为什么我们需要 DARQN?
- 注意机制有哪些不同类型?
- 我们为什么要在《毁灭战士》中设定生存奖励?
进一步阅读
考虑以下内容,以进一步了解您的知识:
十、异步优势演员评论家网络
在前面的章节中,我们已经看到了深度 Q 网络(DQN)多么酷,以及它如何成功地推广了学习玩具有人类水平表现的 Atari 系列游戏的方法 。 但是我们面临的问题是它需要大量的计算能力和训练时间。 因此,Google 的 DeepMind 引入了一种称为异步优势演员评论家(A3C)的新算法,该算法在其他深度强化学习算法中占主导地位,因为它需要较少的计算能力和训练时间。 A3C 的主要思想是,它使用多个智能体并行学习并汇总其整体经验。 在本章中,我们将了解 A3C 网络如何工作。 接下来,我们将学习如何使用 A3C 构建智能体以推动一座山。
在本章中,您将学习以下内容:
- 异步优势演员评论家算法
- A3C
- A3C 的架构
- A3C 如何运作
- 驾驶 A3C 上山
- TensorBoard 中的可视化
异步优势演员评论家
A3C 网络风起云涌,并接管了 DQN。 除了前面提到的优点之外,与其他算法相比,它还具有良好的准确率。 它在连续和离散动作空间中都可以很好地工作。 它使用多个智能体,每个智能体在实际环境的副本中与不同的探索策略并行学习。 然后,将从这些智能体获得的经验汇总到全局智能体。 全局智能体也称为主网络或全局网络,其他智能体也称为工作器。 现在,我们将详细了解 A3C 的工作原理以及与 DQN 算法的区别。
A3C
潜水之前,A3C 是什么意思? 这三个 As 代表什么?
在 A3C 中,第一个 A,异步表示其工作方式。 在这里,我们有多个与环境交互的智能体,而不是像 DQN 那样有单个智能体尝试学习最佳策略。 由于我们有多个智能体同时与环境交互,因此我们将环境的副本提供给每个智能体,以便每个智能体可以与自己的环境副本进行交互。 因此,所有这些多个智能体都称为辅助智能体,我们有一个单独的智能体,称为全局网络,所有智能体都向其报告。 全局网络聚合了学习内容。
第二个 A 是优势; 在讨论 DQN 的决斗网络架构时,我们已经看到了优势函数。 优势函数可以定义为 Q 函数和值函数之间的差。 我们知道 Q 函数指定状态下动作的状态,而值函数指定状态下状态的状态。 现在,凭直觉思考; 两者之间的区别意味着什么? 它告诉我们,与其他所有动作相比,智能体在状态s下执行动作a有多好。
第三个 A 是演员评论家; 该架构具有两种类型的网络,即演员和评论家。 演员的角色是学习策略,评论家的角色是评估演员学习的策略有多好。
A3C 的架构
现在,让我们看一下 A3C 的架构。 看下图:

仅通过查看上图就可以了解 A3C 的工作原理。 正如我们所讨论的,我们可以看到有多个工作程序智能体,每个工作智能体都与自己的环境副本进行交互。 然后,工作器将学习策略并计算策略损失的梯度,并将该梯度更新到全局网络。 每个智能体都会同时更新此全局网络。 A3C 的优点之一是,与 DQN 不同,我们在这里不使用经验回放内存。 实际上,这是 A3C 网络的最大优势之一。 由于我们有多个与环境交互并将信息聚合到全局网络的智能体,因此经验之间的相关性很低甚至没有。 经验回放需要占用所有经验的大量内存。 由于 A3C 不需要它,因此我们的存储空间和计算时间将减少。
A3C 如何运作
首先,辅助智能体重置全局网络,然后它们开始与环境进行交互。 每个工作器遵循不同的探索策略以学习最佳策略。 然后,他们计算值和策略损失,然后计算损失的梯度并将梯度更新到全局网络。 随着工作器智能体开始重置全局网络并重复相同的过程,该循环继续进行。 在查看值和策略损失函数之前,我们将了解优势函数的计算方式。 众所周知,优点是Q函数和值函数之间的区别:

由于我们实际上并未直接在 A3C 中计算Q值,因此我们将折扣收益用作Q值的估计值。 折扣收益R可以写为:

我们将折价收益R替换为Q函数,如下所示:

现在,我们可以将值损失写为折扣收益与状态值之间的平方差:

保单损失可以定义如下:

好的,新项目H(π)是什么? 它是熵项。 它用于确保充分探索策略。 熵告诉我们行动概率的扩散。 当熵值高时,每个动作的概率都将相同,因此智能体将不确定要执行哪个动作,而当熵值降低时,一个动作将比其他动作具有更高的概率,并且智能体可以选取这个可能性很高的动作。 因此,将熵添加到损失函数中会鼓励智能体进一步探索并避免陷入局部最优状态。
驾驶 A3C 上山
让我们通过山车示例了解 A3C。 我们的智能体是汽车,它被放置在两座山之间。 我们智能体的目标是向右上方爬。 但是,汽车不能一口气上山。 它必须来回驱动以建立动力。 如果我们的经纪人在开车上花费更少的精力,将获得高额奖励。 本节使用的代码归功于 Stefan Boschenriedter。 环境如下所示:

好的,让我们开始编码吧! 完整的代码可在 Jupyter 笔记本中获得,并在此处进行解释。
首先,让我们导入必要的库:
import gym
import multiprocessing
import threading
import numpy as np
import os
import shutil
import matplotlib.pyplot as plt
import tensorflow as tf
现在,我们将初始化所有参数:
# number of worker agents
no_of_workers = multiprocessing.cpu_count()
# maximum number of steps per episode
no_of_ep_steps = 200
# total number of episodes
no_of_episodes = 2000
global_net_scope = 'Global_Net'
# sets how often the global network should be updated
update_global = 10
# discount factor
gamma = 0.90
# entropy factor
entropy_beta = 0.01
# learning rate for actor
lr_a = 0.0001
# learning rate for critic
lr_c = 0.001
# boolean for rendering the environment
render=False
# directory for storing logs
log_dir = 'logs'
初始化我们的MountainCar环境:
env = gym.make('MountainCarContinuous-v0')
env.reset()
获取states和actions的数量,以及action_bound的数量:
no_of_states = env.observation_space.shape[0]
no_of_actions = env.action_space.shape[0]
action_bound = [env.action_space.low, env.action_space.high]
我们将在ActorCritic类中定义演员评论家网络。 像往常一样,我们首先了解一个类中每个函数的代码,并在最后看到整个最终代码。 注释被添加到每行代码中,以更好地理解。 最后,我们将研究干净无注释的整个代码:
class ActorCritic(object):
def __init__(self, scope, sess, globalAC=None):
# first we initialize the session and RMS prop optimizer for both
# our actor and critic networks
self.sess=sess
self.actor_optimizer = tf.train.RMSPropOptimizer(lr_a, name='RMSPropA')
self.critic_optimizer = tf.train.RMSPropOptimizer(lr_c, name='RMSPropC')
# now, if our network is global then,
if scope == global_net_scope:
with tf.variable_scope(scope):
# initialize states and build actor and critic network
self.s = tf.placeholder(tf.float32, [None, no_of_states], 'S')
# get the parameters of actor and critic networks
self.a_params, self.c_params = self._build_net(scope)[-2:]
# if our network is local then,
else:
with tf.variable_scope(scope):
# initialize state, action, and also target value
# as v_target
self.s = tf.placeholder(tf.float32, [None, no_of_states], 'S')
self.a_his = tf.placeholder(tf.float32, [None, no_of_actions], 'A')
self.v_target = tf.placeholder(tf.float32, [None, 1], 'Vtarget')
# since we are in continuous actions space,
# we will calculate
# mean and variance for choosing action
mean, var, self.v, self.a_params, self.c_params = self._build_net(scope)
# then we calculate td error as the difference
# between v_target - v
td = tf.subtract(self.v_target, self.v, name='TD_error')
# minimize the TD error
with tf.name_scope('critic_loss'):
self.critic_loss = tf.reduce_mean(tf.square(td))
# update the mean and var value by multiplying mean
# with the action bound and adding var with 1e-4
with tf.name_scope('wrap_action'):
mean, var = mean * action_bound[1], var + 1e-4
# we can generate distribution using this updated
# mean and var
normal_dist = tf.contrib.distributions.Normal(mean, var)
# now we shall calculate the actor loss.
# Recall the loss function.
with tf.name_scope('actor_loss'):
# calculate first term of loss which is log(pi(s))
log_prob = normal_dist.log_prob(self.a_his)
exp_v = log_prob * td
# calculate entropy from our action distribution
# for ensuring exploration
entropy = normal_dist.entropy()
# we can define our final loss as
self.exp_v = exp_v + entropy_beta * entropy
# then, we try to minimize the loss
self.actor_loss = tf.reduce_mean(-self.exp_v)
# now, we choose an action by drawing from the
# distribution and clipping it between action bounds,
with tf.name_scope('choose_action'):
self.A = tf.clip_by_value(tf.squeeze(normal_dist.sample(1), axis=0), action_bound[0], action_bound[1])
# calculate gradients for both of our actor
# and critic networks,
with tf.name_scope('local_grad'):
self.a_grads = tf.gradients(self.actor_loss, self.a_params)
self.c_grads = tf.gradients(self.critic_loss, self.c_params)
# now, we update our global network weights,
with tf.name_scope('sync'):
# pull the global network weights to the local networks
with tf.name_scope('pull'):
self.pull_a_params_op = [l_p.assign(g_p) for l_p, g_p in zip(self.a_params, globalAC.a_params)]
self.pull_c_params_op = [l_p.assign(g_p) for l_p, g_p in zip(self.c_params, globalAC.c_params)]
# push the local gradients to the global network
with tf.name_scope('push'):
self.update_a_op = self.actor_optimizer.apply_gradients(zip(self.a_grads, globalAC.a_params))
self.update_c_op = self.critic_optimizer.apply_gradients(zip(self.c_grads, globalAC.c_params))
# next, we define a function called _build_net for building
# our actor and critic network
def _build_net(self, scope):
# initialize weights
w_init = tf.random_normal_initializer(0., .1)
with tf.variable_scope('actor'):
l_a = tf.layers.dense(self.s, 200, tf.nn.relu6, kernel_initializer=w_init, name='la')
mean = tf.layers.dense(l_a, no_of_actions, tf.nn.tanh,kernel_initializer=w_init, name='mean')
var = tf.layers.dense(l_a, no_of_actions, tf.nn.softplus, kernel_initializer=w_init, name='var')
with tf.variable_scope('critic'):
l_c = tf.layers.dense(self.s, 100, tf.nn.relu6, kernel_initializer=w_init, name='lc')
v = tf.layers.dense(l_c, 1, kernel_initializer=w_init, name='v')
a_params = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope=scope + '/actor')
c_params = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope=scope + '/critic')
return mean, var, v, a_params, c_params
# update the local gradients to the global network
def update_global(self, feed_dict):
self.sess.run([self.update_a_op, self.update_c_op], feed_dict)
# get the global parameters to the local networks
def pull_global(self):
self.sess.run([self.pull_a_params_op, self.pull_c_params_op])
# select action
def choose_action(self, s):
s = s[np.newaxis, :]
return self.sess.run(self.A, {self.s: s})[0]
现在,我们将初始化Worker类:
class Worker(object):
def __init__(self, name, globalAC, sess):
# initialize environment for each worker
self.env = gym.make('MountainCarContinuous-v0').unwrapped
self.name = name
# create an ActorCritic agent for each worker
self.AC = ActorCritic(name, sess, globalAC)
self.sess=sess
def work(self):
global global_rewards, global_episodes
total_step = 1
# store state, action, reward
buffer_s, buffer_a, buffer_r = [], [], []
# loop if the coordinator is active and the global
# episode is less than the maximum episode
while not coord.should_stop() and global_episodes < no_of_episodes:
# initialize the environment by resetting
s = self.env.reset()
# store the episodic reward
ep_r = 0
for ep_t in range(no_of_ep_steps):
# Render the environment for only worker 1
if self.name == 'W_0' and render:
self.env.render()
# choose the action based on the policy
a = self.AC.choose_action(s)
# perform the action (a), receive reward (r),
# and move to the next state (s_)
s_, r, done, info = self.env.step(a)
# set done as true if we reached maximum step per episode
done = True if ep_t == no_of_ep_steps - 1 else False
ep_r += r
# store the state, action, and rewards in the buffer
buffer_s.append(s)
buffer_a.append(a)
# normalize the reward
buffer_r.append((r+8)/8)
# we update the global network after a particular time step
if total_step % update_global == 0 or done:
if done:
v_s_ = 0
else:
v_s_ = self.sess.run(self.AC.v, {self.AC.s: s_[np.newaxis, :]})[0, 0]
# buffer for target v
buffer_v_target = []
for r in buffer_r[::-1]:
v_s_ = r + gamma * v_s_
buffer_v_target.append(v_s_)
buffer_v_target.reverse()
buffer_s, buffer_a, buffer_v_target = np.vstack(buffer_s), np.vstack(buffer_a), np.vstack(buffer_v_target)
feed_dict = {
self.AC.s: buffer_s,
self.AC.a_his: buffer_a,
self.AC.v_target: buffer_v_target,
}
# update global network
self.AC.update_global(feed_dict)
buffer_s, buffer_a, buffer_r = [], [], []
# get global parameters to local ActorCritic
self.AC.pull_global()
s = s_
total_step += 1
if done:
if len(global_rewards) < 5:
global_rewards.append(ep_r)
else:
global_rewards.append(ep_r)
global_rewards[-1] =(np.mean(global_rewards[-5:]))
global_episodes += 1
break
现在,让我们开始 TensorFlow 会话并运行我们的模型:
# create a list for string global rewards and episodes
global_rewards = []
global_episodes = 0
# start tensorflow session
sess = tf.Session()
with tf.device("/cpu:0"):
# create an instance to our ActorCritic Class
global_ac = ActorCritic(global_net_scope,sess)
workers = []
# loop for each worker
for i in range(no_of_workers):
i_name = 'W_%i' % i
workers.append(Worker(i_name, global_ac,sess))
coord = tf.train.Coordinator()
sess.run(tf.global_variables_initializer())
# log everything so that we can visualize the graph in tensorboard
if os.path.exists(log_dir):
shutil.rmtree(log_dir)
tf.summary.FileWriter(log_dir, sess.graph)
worker_threads = []
#start workers
for worker in workers:
job = lambda: worker.work()
t = threading.Thread(target=job)
t.start()
worker_threads.append(t)
coord.join(worker_threads)
输出如下所示。 如果您运行该程序,则可以看到我们的智能体如何在几个剧集中学会爬山:

TensorBoard 中的可视化
让我们在 TensorBoard 中可视化我们的网络。 要启动 TensorBoard,请打开您的终端并输入以下内容:
tensorboard --logdir=logs --port=6007 --host=127.0.0.1
这是我们的 A3C 网络。 我们拥有一个全局网络和四名员工:

让我们扩展我们的全局网络; 您可以看到我们有一位演员和一位评论家:

好的,工作器的实际情况是什么? 让我们扩展我们的工作器网络。 您可以看到工作节点的表现如何:

同步节点呢? 那是在做什么?同步节点将本地梯度从本地网络推送到全局网络,并将梯度从全局网络推送到本地网络:

总结
在本章中,我们学习了 A3C 网络的工作方式。 在 A3C 中,“异步”表示多个智能体通过与环境的多个副本进行交互而独立工作,“优势”表示“优势”函数,即 Q 函数和值函数之间的差异,“演员评论家”是指演员评论家网络,其中演员网络负责生成策略,评论家网络评估由演员网络生成的策略。 我们已经了解了 A3C 的工作原理,并了解了如何使用该算法解决山地车问题。
在下一章第 11 章,“策略梯度和优化”中,我们将看到无需 Q 函数即可直接优化策略的策略梯度方法。
问题
问题列表如下:
- 什么是 A3C?
- 这三个 A 代表什么?
- 列举 A3N 优于 DQN 的一项优势
- 全局和工作节点之间有什么区别?
- 为什么我们计算损失函数的熵?
- 解释 A3C 的工作原理。
进一步阅读
您还可以参考以下论文:
十一、策略梯度和优化
在最后三章中,我们学习了各种深度强化学习算法,例如深度 Q 网络(DQN),深度循环 Q 网络(DRQN)和异步优势演员评论家(A3C)网络。 在所有算法中,我们的目标是找到正确的策略,以便我们能够最大化回报。 我们使用 Q 函数来找到最佳策略,因为 Q 函数告诉我们哪个动作是在某种状态下执行的最佳动作。 您认为我们不使用 Q 函数就能直接找到最优策略吗? 是。 我们可以。 在策略梯度方法中,我们无需使用 Q 函数就可以找到最优策略。
在本章中,我们将详细了解策略梯度。 我们还将研究不同类型的策略梯度方法,例如深度确定性策略梯度,然后是最新的策略优化方法,例如信任区域策略优化和近端策略优化。
在本章中,您将学习以下内容:
- 策略梯度
- 使用策略梯度的 Lunar Lander
- 深度确定性策略梯度
- 使用深度确定性策略梯度(DDPG)摆动
- 信任区域策略优化
- 近端策略优化
策略梯度
策略梯度是强化学习(RL)的惊人算法之一,在该算法中,我们直接优化由某些参数θ设置的策略。 到目前为止,我们已经使用 Q 函数来找到最佳策略。 现在,我们将了解如何找到没有 Q 函数的最优策略。 首先,让我们将策略函数定义为π(a | s),即在状态为s的情况下采取a动作的概率。 我们通过参数θ将策略参数化为π(a | s; θ),这使我们能够确定状态下的最佳操作。
策略梯度方法具有多个优点,它可以处理连续动作空间,在该连续动作空间中,我们具有无限数量的动作和状态。 假设我们正在制造自动驾驶汽车。 驾驶汽车时应避免撞到其他车辆。 当汽车撞到车辆时,我们得到负奖励,而当汽车没有撞到其他车辆时,我们得到正奖励。 我们以仅获得积极奖励的方式更新模型参数,以使我们的汽车不会撞到任何其他车辆。 这是策略梯度的基本思想:我们以最大化报酬的方式更新模型参数。 让我们详细看一下。
我们使用神经网络来找到最佳策略,我们将此网络称为策略网络。 策略网络的输入将是状态,而输出将是该状态中每个操作的概率。 一旦有了这个概率,就可以从该分布中采样一个动作,并在状态下执行该动作。 但是我们采样的动作可能不是在该状态下执行的正确动作。 很好-我们执行动作并存储奖励。 同样,我们通过从分布中采样一个动作来在每种状态下执行动作,并存储奖励。 现在,这成为我们的训练数据。 我们执行梯度下降并以这样的方式更新梯度:在状态下产生高奖励的动作将具有较高的概率,而在状态下产生低奖励的动作将具有较低的概率。 什么是损失函数? 在这里,我们使用 softmax 交叉熵损失,然后将损失乘以奖励值。
使用策略梯度的 Lunar Lander
假设我们的经纪人正在驾驶航天器,而我们的经纪人的目标是正确着陆在着陆垫上。 如果我们的智能体(着陆器)从着陆点着陆,则它会失去奖励,并且如果智能体崩溃或休息,剧集将终止。 在环境中可用的四个离散动作是“不执行任何操作”,“点火向左的引擎”,“点火主引擎”和“点火向右的引擎”。
现在,我们将看到如何训练我们的智能体以策略梯度正确降落在降落区。 本节中使用的代码属于 Gabriel:

首先,我们导入必要的库:
import tensorflow as tf
import numpy as np
from tensorflow.python.framework import ops
import gym
import numpy as np
import time
然后,我们定义PolicyGradient类,该类实现了策略梯度算法。 让我们分解类并分别查看每个函数。 您可以将整个程序看作 Jupyter 笔记本:
class PolicyGradient:
# first we define the __init__ method where we initialize all variables
def __init__(self, n_x,n_y,learning_rate=0.01, reward_decay=0.95):
# number of states in the environment
self.n_x = n_x
# number of actions in the environment
self.n_y = n_y
# learning rate of the network
self.lr = learning_rate
# discount factor
self.gamma = reward_decay
# initialize the lists for storing observations,
# actions and rewards
self.episode_observations, self.episode_actions, self.episode_rewards = [], [], []
# we define a function called build_network for
# building the neural network
self.build_network()
# stores the cost i.e loss
self.cost_history = []
# initialize tensorflow session
self.sess = tf.Session()
self.sess.run(tf.global_variables_initializer())
接下来,我们定义一个store_transition函数,该函数存储转换,即state,action和reward。 我们可以使用以下信息来训练网络:
def store_transition(self, s, a, r):
self.episode_observations.append(s)
self.episode_rewards.append(r)
# store actions as list of arrays
action = np.zeros(self.n_y)
action[a] = 1
self.episode_actions.append(action)
给定state,我们定义choose_action函数来选择action:
def choose_action(self, observation):
# reshape observation to (num_features, 1)
observation = observation[:, np.newaxis]
# run forward propagation to get softmax probabilities
prob_weights = self.sess.run(self.outputs_softmax, feed_dict = {self.X: observation})
# select action using a biased sample this will return
# the index of the action we have sampled
action = np.random.choice(range(len(prob_weights.ravel())), p=prob_weights.ravel())
return action
我们定义用于构建神经网络的build_network函数:
def build_network(self):
# placeholders for input x, and output y
self.X = tf.placeholder(tf.float32, shape=(self.n_x, None), name="X")
self.Y = tf.placeholder(tf.float32, shape=(self.n_y, None), name="Y")
# placeholder for reward
self.discounted_episode_rewards_norm = tf.placeholder(tf.float32, [None, ], name="actions_value")
# we build 3 layer neural network with 2 hidden layers and
# 1 output layer
# number of neurons in the hidden layer
units_layer_1 = 10
units_layer_2 = 10
# number of neurons in the output layer
units_output_layer = self.n_y
# now let us initialize weights and bias value using
# tensorflow's tf.contrib.layers.xavier_initializer
W1 = tf.get_variable("W1", [units_layer_1, self.n_x], initializer = tf.contrib.layers.xavier_initializer(seed=1))
b1 = tf.get_variable("b1", [units_layer_1, 1], initializer = tf.contrib.layers.xavier_initializer(seed=1))
W2 = tf.get_variable("W2", [units_layer_2, units_layer_1], initializer = tf.contrib.layers.xavier_initializer(seed=1))
b2 = tf.get_variable("b2", [units_layer_2, 1], initializer = tf.contrib.layers.xavier_initializer(seed=1))
W3 = tf.get_variable("W3", [self.n_y, units_layer_2], initializer = tf.contrib.layers.xavier_initializer(seed=1))
b3 = tf.get_variable("b3", [self.n_y, 1], initializer = tf.contrib.layers.xavier_initializer(seed=1))
# and then, we perform forward propagation
Z1 = tf.add(tf.matmul(W1,self.X), b1)
A1 = tf.nn.relu(Z1)
Z2 = tf.add(tf.matmul(W2, A1), b2)
A2 = tf.nn.relu(Z2)
Z3 = tf.add(tf.matmul(W3, A2), b3)
A3 = tf.nn.softmax(Z3)
# as we require, probabilities, we apply softmax activation
# function in the output layer,
logits = tf.transpose(Z3)
labels = tf.transpose(self.Y)
self.outputs_softmax = tf.nn.softmax(logits, name='A3')
# next we define our loss function as cross entropy loss
neg_log_prob = tf.nn.softmax_cross_entropy_with_logits(logits=logits, labels=labels)
# reward guided loss
loss = tf.reduce_mean(neg_log_prob * self.discounted_episode_rewards_norm)
# we use adam optimizer for minimizing the loss
self.train_op = tf.train.AdamOptimizer(self.lr).minimize(loss)
接下来,我们定义discount_and_norm_rewards函数,该函数将导致折扣和标准化奖励:
def discount_and_norm_rewards(self):
discounted_episode_rewards = np.zeros_like(self.episode_rewards)
cumulative = 0
for t in reversed(range(len(self.episode_rewards))):
cumulative = cumulative * self.gamma + self.episode_rewards[t]
discounted_episode_rewards[t] = cumulative
discounted_episode_rewards -= np.mean(discounted_episode_rewards)
discounted_episode_rewards /= np.std(discounted_episode_rewards)
return discounted_episode_rewards
现在我们实际执行学习:
def learn(self):
# discount and normalize episodic reward
discounted_episode_rewards_norm = self.discount_and_norm_rewards()
# train the network
self.sess.run(self.train_op, feed_dict={
self.X: np.vstack(self.episode_observations).T,
self.Y: np.vstack(np.array(self.episode_actions)).T,
self.discounted_episode_rewards_norm: discounted_episode_rewards_norm,
})
# reset the episodic data
self.episode_observations, self.episode_actions, self.episode_rewards = [], [], []
return discounted_episode_rewards_norm
您可以看到如下输出:

深度确定性策略梯度
在第 8 章,“深度 Q 网络和 Atari 游戏”中,我们研究了 DQN 的工作原理,并应用了 DQN 玩 Atari 游戏。 但是,在那些离散的环境中,我们只有一组有限的动作。 想象一个连续的环境空间,例如训练机器人走路; 在那些环境中,应用 Q 学习是不可行的,因为要找到一个贪婪的策略将需要在每一步进行很多优化。 即使我们使连续的环境离散,我们也可能会失去重要的函数并最终获得大量的动作空间。 当我们拥有巨大的行动空间时,很难实现融合。
因此,我们使用称为演员评论家的新架构,该架构具有两个网络:演员和评论家。 演员评论家架构结合了策略梯度和状态操作值函数。 演员网络的作用是通过调整参数θ来确定状态中的最佳动作,而评论家的作用是评估演员产生的动作。 评论家通过计算时间差异误差来评估演员的行动。 也就是说,我们在演员网络上执行策略梯度以选择操作, 评论家网络使用 TD 误差评估由演员网络产生的操作。 下图显示了演员评论家架构:

与 DQN 相似,这里我们使用经验缓冲区,通过采样少量的经验来训练演员和评论家网络。 我们还使用单独的目标演员和评论家网络来计算损失。
例如,在乒乓游戏中,我们将具有不同比例的不同特征,例如位置,速度等。 因此,我们以所有特征都处于相同比例的方式来缩放特征。 我们使用一种称为批归一化的方法来缩放特征。 它将所有特征归一化以具有单位均值和方差。 我们如何探索新的行动? 在连续环境中,将有n个动作。 为了探索新动作,我们在演员网络产生的动作中添加了一些噪声N。 我们使用称为 Ornstein-Uhlenbeck 随机过程的过程来生成此噪声。
现在,我们将详细介绍 DDPG 算法。
假设我们有两个网络:演员网络和评论家网络。 我们用输入为状态的μ(s; θ^μ)表示演员网络
,并以θ^μ作为演员网络权重的结果进行操作。 我们将评论家网络表示为Q(s, a; θ^Q),它将输入作为状态和动作并返回Q值,其中θ^Q是评论家网络权重。
同样,我们将演员网络和评论家网络的目标网络分别定义为μ(s; θ^μ')和Q(s, a; θ^Q'),其中θ^μ'和θ^Q'是目标演员和评论家网络的权重。
我们使用策略梯度更新演员网络权重,并使用根据 TD 误差计算得出的梯度更新评论家网络权重。
首先,我们通过将探索噪声N添加到演员网络产生的动作(例如μ(s; θ^μ) + N)来选择动作。 我们在s状态下执行此操作,获得r奖励,然后移至新状态s'。 我们将此转移信息存储在经验回放缓冲区中。
经过一些迭代后,我们从回放缓冲区采样转移并训练网络,然后计算目标Q值:

我们将 TD 误差计算为:

其中M是来自回放缓冲区的用于训练的样本数。 我们使用根据此损失L计算出的梯度来更新评论家网络的权重。
同样,我们使用策略梯度更新策略网络权重。 然后,我们在目标网络中更新 Actor 和评论家网络的权重。 我们会缓慢更新目标网络的权重,从而提高稳定性。 它称为软替换:

摆动摆锤
我们有一个从随机位置开始的摆锤,我们的探员的目标是向上摆动摆锤使其保持直立。 我们将在这里看到如何使用 DDPG。 wshuail 提供了本节中使用的代码。
首先,让我们导入必要的库:
import tensorflow as tf
import numpy as np
import gym
接下来,我们按如下方式定义超参数:
# number of steps in each episode
epsiode_steps = 500
# learning rate for actor
lr_a = 0.001
# learning rate for critic
lr_c = 0.002
# discount factor
gamma = 0.9
# soft replacement
alpha = 0.01
# replay buffer size
memory = 10000
# batch size for training
batch_size = 32
render = False
我们将在DDPG类中实现 DDPG 算法。 我们分解类以查看每个函数。 首先,我们初始化所有内容:
class DDPG(object):
def __init__(self, no_of_actions, no_of_states, a_bound,):
# initialize the memory with shape as no of actions, no of states and our defined memory size
self.memory = np.zeros((memory, no_of_states * 2 + no_of_actions + 1), dtype=np.float32)
# initialize pointer to point to our experience buffer
self.pointer = 0
# initialize tensorflow session
self.sess = tf.Session()
# initialize the variance for OU process for exploring policies
self.noise_variance = 3.0
self.no_of_actions, self.no_of_states, self.a_bound = no_of_actions, no_of_states, a_bound,
# placeholder for current state, next state and rewards
self.state = tf.placeholder(tf.float32, [None, no_of_states], 's')
self.next_state = tf.placeholder(tf.float32, [None, no_of_states], 's_')
self.reward = tf.placeholder(tf.float32, [None, 1], 'r')
# build the actor network which has separate eval(primary)
# and target network
with tf.variable_scope('Actor'):
self.a = self.build_actor_network(self.state, scope='eval', trainable=True)
a_ = self.build_actor_network(self.next_state, scope='target', trainable=False)
# build the critic network which has separate eval(primary)
# and target network
with tf.variable_scope('Critic'):
q = self.build_crtic_network(self.state, self.a, scope='eval', trainable=True)
q_ = self.build_crtic_network(self.next_state, a_, scope='target', trainable=False)
# initialize the network parameters
self.ae_params = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope='Actor/eval')
self.at_params = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope='Actor/target')
self.ce_params = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope='Critic/eval')
self.ct_params = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope='Critic/target')
# update target value
self.soft_replace = [[tf.assign(at, (1-alpha)*at+alpha*ae), tf.assign(ct, (1-alpha)*ct+alpha*ce)]
for at, ae, ct, ce in zip(self.at_params, self.ae_params, self.ct_params, self.ce_params)]
# compute target Q value, we know that Q(s,a) = reward + gamma *
Q'(s',a')
q_target = self.reward + gamma * q_
# compute TD error i.e actual - predicted values
td_error = tf.losses.mean_squared_error(labels=(self.reward + gamma * q_), predictions=q)
# train the critic network with adam optimizer
self.ctrain = tf.train.AdamOptimizer(lr_c).minimize(td_error, name="adam-ink", var_list = self.ce_params)
# compute the loss in actor network
a_loss = - tf.reduce_mean(q)
# train the actor network with adam optimizer for
# minimizing the loss
self.atrain = tf.train.AdamOptimizer(lr_a).minimize(a_loss, var_list=self.ae_params)
# initialize summary writer to visualize our network in tensorboard
tf.summary.FileWriter("logs", self.sess.graph)
# initialize all variables
self.sess.run(tf.global_variables_initializer())
我们如何在 DDPG 中选择一个动作? 我们通过向动作空间添加噪音来选择动作。 我们使用 Ornstein-Uhlenbeck 随机过程生成噪声:
def choose_action(self, s):
a = self.sess.run(self.a, {self.state: s[np.newaxis, :]})[0]
a = np.clip(np.random.normal(a, self.noise_variance), -2, 2)
return a
然后,我们定义learn函数,在该函数中进行实际训练。 在这里,我们从经验缓冲区中选择一批states,actions,rewards和下一个状态。 我们以此来训练演员和评论家网络:
def learn(self):
# soft target replacement
self.sess.run(self.soft_replace)
indices = np.random.choice(memory, size=batch_size)
batch_transition = self.memory[indices, :]
batch_states = batch_transition[:, :self.no_of_states]
batch_actions = batch_transition[:, self.no_of_states: self.no_of_states + self.no_of_actions]
batch_rewards = batch_transition[:, -self.no_of_states - 1: -self.no_of_states]
batch_next_state = batch_transition[:, -self.no_of_states:]
self.sess.run(self.atrain, {self.state: batch_states})
self.sess.run(self.ctrain, {self.state: batch_states, self.a: batch_actions, self.reward: batch_rewards, self.next_state: batch_next_state})
我们定义了一个store_transition函数,该函数将所有信息存储在缓冲区中并执行学习:
def store_transition(self, s, a, r, s_):
trans = np.hstack((s,a,[r],s_))
index = self.pointer % memory
self.memory[index, :] = trans
self.pointer += 1
if self.pointer > memory:
self.noise_variance *= 0.99995
self.learn()
我们定义了build_actor_network函数来构建演员网络:
def build_actor_network(self, s, scope, trainable):
# Actor DPG
with tf.variable_scope(scope):
l1 = tf.layers.dense(s, 30, activation = tf.nn.tanh, name = 'l1', trainable = trainable)
a = tf.layers.dense(l1, self.no_of_actions, activation = tf.nn.tanh, name = 'a', trainable = trainable)
return tf.multiply(a, self.a_bound, name = "scaled_a")
我们定义build_ crtic_network函数:
def build_crtic_network(self, s, a, scope, trainable):
# Critic Q-leaning
with tf.variable_scope(scope):
n_l1 = 30
w1_s = tf.get_variable('w1_s', [self.no_of_states, n_l1], trainable = trainable)
w1_a = tf.get_variable('w1_a', [self.no_of_actions, n_l1], trainable = trainable)
b1 = tf.get_variable('b1', [1, n_l1], trainable = trainable)
net = tf.nn.tanh( tf.matmul(s, w1_s) + tf.matmul(a, w1_a) + b1 )
q = tf.layers.dense(net, 1, trainable = trainable)
return q
现在,我们使用make函数初始化gym环境:
env = gym.make("Pendulum-v0")
env = env.unwrapped
env.seed(1)
我们得到状态数:
no_of_states = env.observation_space.shape[0]
我们得到的动作数:
no_of_actions = env.action_space.shape[0]
此外,该动作的上限:
a_bound = env.action_space.high
现在,我们为DDPG类创建一个对象:
ddpg = DDPG(no_of_actions, no_of_states, a_bound)
我们初始化列表以存储总奖励:
total_reward = []
设置剧集数:
no_of_episodes = 300
现在,让我们开始训练:
# for each episodes
for i in range(no_of_episodes):
# initialize the environment
s = env.reset()
# episodic reward
ep_reward = 0
for j in range(epsiode_steps):
env.render()
# select action by adding noise through OU process
a = ddpg.choose_action(s)
# perform the action and move to the next state s
s_, r, done, info = env.step(a)
# store the the transition to our experience buffer
# sample some minibatch of experience and train the network
ddpg.store_transition(s, a, r, s_)
# update current state as next state
s = s_
# add episodic rewards
ep_reward += r
if j == epsiode_steps-1:
# store the total rewards
total_reward.append(ep_reward)
# print rewards obtained per each episode
print('Episode:', i, ' Reward: %i' % int(ep_reward))
break
您将看到如下输出:

我们可以在 TensorBoard 中看到计算图:

信任区域策略优化
在了解信任区域策略优化(TRPO)之前,我们需要了解受约束的策略优化。 我们知道,在 RL 智能体中,通过反复试验来学习,以使报酬最大化。 为了找到最佳策略,我们的智能体将探索所有不同的行动,并选择能获得良好回报的行动。 在探索不同的动作时,我们的智能体很有可能也会探索不良的动作。 但是最大的挑战是,当我们允许智能体在现实世界中学习以及奖励函数设计不当时。 例如,考虑一个学习走路而没有遇到任何障碍的智能体。 如果智能体被任何障碍物击中,它将获得负奖励;而如果没有被任何障碍物击中,则将获得正奖励。 为了找出最佳策略,智能体会探索不同的操作。 智能体还采取行动,例如撞到障碍物以检查它是否给出了良好的回报。 但这对我们的经纪人来说并不安全; 当智能体在现实环境中学习时,这尤其不安全。 因此,我们介绍了基于约束的学习。 我们设置一个阈值,如果碰到障碍物的可能性小于该阈值,则我们认为我们的智能体是安全的,否则我们认为我们的智能体是不安全的。 添加了约束以确保我们的智能体位于安全区域内。
在 TRPO 中,我们迭代地改进了该策略,并施加了一个约束,以使旧策略和新策略之间的 Kullback-Leibler(KL)差异要小于某个常数。
。 该约束称为信任区域约束。
那么什么是 KL 散度? KL 散度告诉我们两个概率分布如何彼此不同。 由于我们的策略是针对行动的概率分布,因此 KL 差异告诉我们新策略与旧策略有多远。 为什么我们必须使旧策略和新策略之间的距离保持小于恒定值δ? 因为我们不希望我们的新策略与旧策略脱节。 因此,我们施加了约束以使新策略接近旧策略。 同样,为什么我们必须保持旧策略呢? 当新策略与旧策略相距甚远时,它将影响智能体的学习表现,并导致完全不同的学习行为。 简而言之,在 TRPO 中,我们朝着改善策略的方向迈出了一步,即使报酬最大化,但我们还应确保满足信任区域约束。 它使用共轭梯度下降优化网络参数θ,同时满足约束条件。 该算法保证了单调策略的改进,并且在各种连续环境中也取得了出色的效果。
现在,我们将了解 TRPO 的数学原理。 如果您对数学不感兴趣,可以跳过本节。
准备一些很棒的数学。
让我们指定预期的总折扣奖励η(π),如下所示:

现在让我们将新策略视为π'; 就相对于旧策略π的优势而言,可以将其定义为策略π'的预期回报,如下所示:

好吧,为什么我们要利用旧策略的优势? 因为我们正在衡量新策略π'相对于旧策略π的平均效果有多好。 我们可以用状态之和而不是时间步来重写前面的方程,如下所示:

是折扣的访问频率,即:

如果您看到前面的方程η(π'),则ρ[π'](s)与π'
之间存在复杂的依存关系,因此很难对方程进行优化。 因此,我们将局部近似L[π](π')引入η(π'),如下所示:

使用访问频率ρ[π]而不是ρ[π'],也就是说,由于策略的变化,我们忽略了状态访问频率的变化。 简而言之,我们假设新旧策略的状态访问频率均相同。 当我们计算L[π]的梯度时,相对于某些参数θ而言,这也会提高η,我们不确定要采取多少步骤。
Kakade 和 Langford 提出了一种新的策略更新方法,称为保守策略迭代,如下所示:
----(1)
是新策略。 π_old是旧策略。
,即π',是最大化L[π_old]的策略。
Kakade 和 Langford 从(1)得出以下方程式:
----(2)
C是惩罚系数,等于4εγ / (1 - α)^2,D_max[KL]表示旧策略与新策略之间的 KL 散度。
如果我们仔细观察前面的方程式(2),我们会注意到,只要右侧最大化,我们的预期长期回报η就会单调增加。
让我们将此右侧项定义为M[i](π),如下所示:
----(3)
将公式(3)代入(2),我们得到:
----(4)
由于我们知道两个相同策略之间的 KL 差异为0,我们可以这样写:
----(5)
结合方程式(4)和(5),我们可以写:

在前面的等式中,我们可以理解,最大化Mᵢ可以保证我们期望收益的最大化。 因此,现在我们的目标是最大化Mᵢ,从而最大化我们的预期回报。 由于我们使用参数化策略,因此在上一个公式中将π替换为θ,然后使用θ_old表示我们要改进的策略,如下所示:

但是,在前面的公式中具有惩罚系数C将导致步长非常小,从而减慢了更新速度。 因此,我们对 KL 散度的旧策略和新策略施加了约束,即信任区域约束,这将有助于我们找到最佳步长:

现在,问题是在状态空间的每个点上都施加了 KL 散度,当我们拥有高维状态空间时,解决它确实是不可行的。 因此,我们使用启发式近似,其平均 KL 散度为:

因此,现在,我们可以将平均 KL 散度约束重写为先前的目标函数,如下所示:

扩展L的值,我们得到以下信息:

在前面的公式中,我们将状态总和Σ[s] ρ θ_old替换为期望E[s ~ ρ θ_old],并且将重要性总和估计值替换为行动总和,如下所示:

然后,我们将优势目标值A[θ_old]替换为 Q 值Q[θ_old]。
因此,我们最终的目标函数将变为:

优化前面提到的具有约束的目标函数称为约束优化。 我们的约束是使旧策略和新策略之间的平均 KL 差异小于δ。我们使用共轭梯度下降来优化先前的函数。
近端策略优化
现在,我们将看另一种策略优化算法,称为近端策略优化(PPO)。 它是对 TRPO 的改进,由于其表现,已成为解决许多复杂 RL 问题的默认 RL 算法。 它是 OpenAI 的研究人员为克服 TRPO 的缺点而提出的。 回忆一下 TRPO 的替代目标函数。 这是一个约束优化问题,我们在其中施加了一个约束-新旧策略之间的平均 KL 差异应小于δ。 但是 TRPO 的问题在于,它需要大量计算能力才能计算共轭梯度以执行约束优化。
因此,PPO 通过将约束更改为惩罚项来修改 TRPO 的目标函数,因此我们不想执行共轭梯度。 现在,让我们看看 PPO 的工作原理。 我们将r[t](θ)定义为新旧策略之间的概率比。 因此,我们可以将目标函数编写为:

L_CPI表示保守策略迭代。 但是最大化L会导致无限制地进行大量策略更新。 因此,我们通过添加惩罚项来重新定义我们的目标函数,该惩罚项会惩罚较大的策略更新。 现在目标函数变为:

我们刚刚在实际方程式中添加了一个新项:

这是什么意思? 实际上,它会在间隔[1 - ε, 1 + ε]之间裁剪r[t](θ)的值,也就是说,如果r[t](θ)的值导致目标函数增加,则在间隔之间大量裁剪的值会降低其效果。
基于两种情况,我们将概率比限制在1 - ε或ε:
- 案例 1:
A_hat[t] > 0
当优势为正时,这意味着相对于所有其他操作的平均值,应优先选择相应的操作。 我们将为该操作增加r[t](θ)的值,因此将有更大的机会被选择。 当我们执行r[t](θ)的限幅值时,不会超过1 + ε:

- 案例 2:
A_hat[t]
当优势的值为负时,这意味着该动作没有意义,因此不应采用。 因此,在这种情况下,我们将减小该操作的r[t](θ)值,以使其被选择的机会较小。 类似地,当我们执行裁剪时,r[t](θ)的值将不会减小到小于1 - ε:

当我们使用神经网络架构时,我们必须定义损失函数,其中包括目标函数的值函数误差。 就像在 A3C 中一样,我们还将添加熵损失以确保足够的探索。 因此,我们最终的目标函数变为:

c₁和c₂是系数,L[t]^(VP)是实际值和目标值函数之间的平方误差损失,即:

S是熵增。
总结
我们从策略梯度方法开始,该方法无需 Q 函数即可直接优化策略。 我们通过解决 Lunar Lander 游戏了解了策略梯度,并研究了 DDPG,它具有策略梯度和 Q 函数的优点。
然后,我们研究了诸如 TRPO 之类的策略优化算法,该算法通过对新旧策略之间的 KL 差异实现不大于δ的限制来确保单调策略的改进。
我们还研究了近端策略优化,该优化通过惩罚大型策略更新将约束变为惩罚。 在下一章第 12 章, “Capstone 项目 – 使用 DQN 进行赛车”,我们将了解如何构建能够赢得赛车游戏的智能体。
问题
问题列表如下:
- 什么是策略梯度?
- 策略梯度为何有效?
- DDPG 中的演员评论家网络有什么用途?
- 约束优化问题是什么?
- 什么是信任区域?
- PPO 如何克服 TRPO 的缺点?
进一步阅读
您可以进一步参考以下论文:
十二、Capstone 项目 – 将 DQN 用于赛车
在最后几章中,我们通过使用神经网络近似 q 函数来了解 Deep Q 学习的工作原理。 在此之后,我们看到了深度 Q 网络(DQN)的各种改进,例如双重 Q 学习,决斗网络架构和深度循环 Q 网络。 我们已经了解了 DQN 如何利用回放缓冲区来存储智能体的经验,并使用缓冲区中的小批样本来训练网络。 我们还实现了用于玩 Atari 游戏的 DQN 和一个用于玩 Doom 游戏的深度循环 Q 网络(DRQN)。 在本章中,让我们进入决斗 DQN 的详细实现,它与常规 DQN 基本相同,除了最终的全连接层将分解为两个流,即值流和优势流,而这两个流将合并在一起以计算 Q 函数。 我们将看到如何训练决斗的 DQN 来赢得赛车比赛的智能体。
在本章中,您将学习如何实现以下内容:
- 环境包装器函数
- 决斗网络
- 回放缓冲区
- 训练网络
- 赛车
环境包装器函数
本章使用的代码归功于 Giacomo Spigler 的 GitHub 存储库。 在本章中,每一行都对代码进行了说明。 有关完整的结构化代码,请查看上面的 GitHub 存储库。
首先,我们导入所有必需的库:
import numpy as np
import tensorflow as tf
import gym
from gym.spaces import Box
from scipy.misc import imresize
import random
import cv2
import time
import logging
import os
import sys
我们定义EnvWrapper类并定义一些环境包装器函数:
class EnvWrapper:
我们定义__init__方法并初始化变量:
def __init__(self, env_name, debug=False):
初始化gym环境:
self.env = gym.make(env_name)
获取action_space:
self.action_space = self.env.action_space
获取observation_space:
self.observation_space = Box(low=0, high=255, shape=(84, 84, 4))
初始化frame_num以存储帧数:
self.frame_num = 0
初始化monitor以记录游戏画面:
self.monitor = self.env.monitor
初始化frames:
self.frames = np.zeros((84, 84, 4), dtype=np.uint8)
初始化一个名为debug的布尔值,将其设置为true时将显示最后几帧:
self.debug = debug
if self.debug:
cv2.startWindowThread()
cv2.namedWindow("Game")
接下来,我们定义一个名为step的函数,该函数将当前状态作为输入并返回经过预处理的下一状态的帧:
def step(self, a):
ob, reward, done, xx = self.env.step(a)
return self.process_frame(ob), reward, done, xx
我们定义了一个称为reset的函数来重置环境; 重置后,它将返回预处理的游戏屏幕:
def reset(self):
self.frame_num = 0
return self.process_frame(self.env.reset())
接下来,我们定义另一个用于渲染环境的函数:
def render(self):
return self.env.render()
现在,我们定义用于预处理帧的process_frame函数:
def process_frame(self, frame):
# convert the image to gray
state_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# change the size
state_resized = cv2.resize(state_gray,(84,110))
#resize
gray_final = state_resized[16:100,:]
if self.frame_num == 0:
self.frames[:, :, 0] = gray_final
self.frames[:, :, 1] = gray_final
self.frames[:, :, 2] = gray_final
self.frames[:, :, 3] = gray_final
else:
self.frames[:, :, 3] = self.frames[:, :, 2]
self.frames[:, :, 2] = self.frames[:, :, 1]
self.frames[:, :, 1] = self.frames[:, :, 0]
self.frames[:, :, 0] = gray_final
# Next we increment the frame_num counter
self.frame_num += 1
if self.debug:
cv2.imshow('Game', gray_final)
return self.frames.copy()
经过预处理后,我们的游戏屏幕如下图所示:

决斗网络
现在,我们构建决斗 DQN; 我们先构建三个卷积层,然后是两个全连接层,最后一个全连接层将被分为两个单独的层,用于值流和优势流。 我们将使用将值流和优势流结合在一起的聚合层来计算 q 值。 这些层的大小如下:
- 第 1 层:32 个
8x8过滤器,步幅为 4 + RELU - 第 2 层:64 个
4x4过滤器,步幅为 2 + RELU - 第 3 层:64 个
3x3过滤器,步幅为 1 + RELU - 第 4a 层:512 个单元的全连接层 + RELU
- 第 4b 层:512 个单元的全连接层 + RELU
- 第 5a 层:1 个 FC + RELU(状态值)
- 第 5b 层:动作 FC + RELU(优势值)
- 第 6 层:总计
V(s) + A(s, a)
class QNetworkDueling(QNetwork):
我们定义__init__方法来初始化所有层:
def __init__(self, input_size, output_size, name):
self.name = name
self.input_size = input_size
self.output_size = output_size
with tf.variable_scope(self.name):
# Three convolutional Layers
self.W_conv1 = self.weight_variable([8, 8, 4, 32])
self.B_conv1 = self.bias_variable([32])
self.stride1 = 4
self.W_conv2 = self.weight_variable([4, 4, 32, 64])
self.B_conv2 = self.bias_variable([64])
self.stride2 = 2
self.W_conv3 = self.weight_variable([3, 3, 64, 64])
self.B_conv3 = self.bias_variable([64])
self.stride3 = 1
# Two fully connected layer
self.W_fc4a = self.weight_variable([7`7`64, 512])
self.B_fc4a = self.bias_variable([512])
self.W_fc4b = self.weight_variable([7`7`64, 512])
self.B_fc4b = self.bias_variable([512])
# Value stream
self.W_fc5a = self.weight_variable([512, 1])
self.B_fc5a = self.bias_variable([1])
# Advantage stream
self.W_fc5b = self.weight_variable([512, self.output_size])
self.B_fc5b = self.bias_variable([self.output_size])
我们定义__call__方法并执行卷积运算:
def __call__(self, input_tensor):
if type(input_tensor) == list:
input_tensor = tf.concat(1, input_tensor)
with tf.variable_scope(self.name):
# Perform convolutional on three layers
self.h_conv1 = tf.nn.relu( tf.nn.conv2d(input_tensor, self.W_conv1, strides=[1, self.stride1, self.stride1, 1], padding='VALID') + self.B_conv1 )
self.h_conv2 = tf.nn.relu( tf.nn.conv2d(self.h_conv1, self.W_conv2, strides=[1, self.stride2, self.stride2, 1], padding='VALID') + self.B_conv2 )
self.h_conv3 = tf.nn.relu( tf.nn.conv2d(self.h_conv2, self.W_conv3, strides=[1, self.stride3, self.stride3, 1], padding='VALID') + self.B_conv3 )
# Flatten the convolutional output
self.h_conv3_flat = tf.reshape(self.h_conv3, [-1, 7`7`64])
# Fully connected layer
self.h_fc4a = tf.nn.relu(tf.matmul(self.h_conv3_flat, self.W_fc4a) + self.B_fc4a)
self.h_fc4b = tf.nn.relu(tf.matmul(self.h_conv3_flat, self.W_fc4b) + self.B_fc4b)
# Compute value stream and advantage stream
self.h_fc5a_value = tf.identity(tf.matmul(self.h_fc4a, self.W_fc5a) + self.B_fc5a)
self.h_fc5b_advantage = tf.identity(tf.matmul(self.h_fc4b, self.W_fc5b) + self.B_fc5b)
# Club both the value and advantage stream
self.h_fc6 = self.h_fc5a_value + ( self.h_fc5b_advantage - tf.reduce_mean(self.h_fc5b_advantage, reduction_indices=[1,], keep_dims=True) )
return self.h_fc6
回放记忆
现在,我们构建经验回放缓冲区,该缓冲区用于存储所有智能体的经验。 我们从回放缓冲区中抽取了少量经验来训练网络:
class ReplayMemoryFast:
首先,我们定义__init__方法并启动缓冲区大小:
def __init__(self, memory_size, minibatch_size):
# max number of samples to store
self.memory_size = memory_size
# minibatch size
self.minibatch_size = minibatch_size
self.experience = [None]*self.memory_size
self.current_index = 0
self.size = 0
接下来,我们定义store函数来存储经验:
def store(self, observation, action, reward, newobservation, is_terminal):
将经验存储为元组(当前状态action,reward,下一个状态是最终状态):
self.experience[self.current_index] = (observation, action, reward, newobservation, is_terminal)
self.current_index += 1
self.size = min(self.size+1, self.memory_size)
如果索引大于内存,那么我们通过减去内存大小来刷新索引:
if self.current_index >= self.memory_size:
self.current_index -= self.memory_size
接下来,我们定义一个sample函数,用于对小批量经验进行采样:
def sample(self):
if self.size < self.minibatch_size:
return []
# First we randomly sample some indices
samples_index = np.floor(np.random.random((self.minibatch_size,))*self.size)
# select the experience from the sampled indexed
samples = [self.experience[int(i)] for i in samples_index]
return samples
训练网络
现在,我们将看到如何训练网络。
首先,我们定义DQN类并在__init__方法中初始化所有变量:
class DQN(object):
def __init__(self, state_size,
action_size,
session,
summary_writer = None,
exploration_period = 1000,
minibatch_size = 32,
discount_factor = 0.99,
experience_replay_buffer = 10000,
target_qnet_update_frequency = 10000,
initial_exploration_epsilon = 1.0,
final_exploration_epsilon = 0.05,
reward_clipping = -1,
):
初始化所有变量:
self.state_size = state_size
self.action_size = action_size
self.session = session
self.exploration_period = float(exploration_period)
self.minibatch_size = minibatch_size
self.discount_factor = tf.constant(discount_factor)
self.experience_replay_buffer = experience_replay_buffer
self.summary_writer = summary_writer
self.reward_clipping = reward_clipping
self.target_qnet_update_frequency = target_qnet_update_frequency
self.initial_exploration_epsilon = initial_exploration_epsilon
self.final_exploration_epsilon = final_exploration_epsilon
self.num_training_steps = 0
通过为我们的QNetworkDueling类创建实例来初始化主要决斗 DQN:
self.qnet = QNetworkDueling(self.state_size, self.action_size, "qnet")
同样,初始化目标决斗 DQN:
self.target_qnet = QNetworkDueling(self.state_size, self.action_size, "target_qnet")
接下来,将优化器初始化为RMSPropOptimizer:
self.qnet_optimizer = tf.train.RMSPropOptimizer(learning_rate=0.00025, decay=0.99, epsilon=0.01)
现在,通过为我们的ReplayMemoryFast类创建实例来初始化experience_replay_buffer:
self.experience_replay = ReplayMemoryFast(self.experience_replay_buffer, self.minibatch_size)
# Setup the computational graph
self.create_graph()
接下来,我们定义copy_to_target_network函数,用于将权重从主网络复制到目标网络:
def copy_to_target_network(source_network, target_network):
target_network_update = []
for v_source, v_target in zip(source_network.variables(), target_network.variables()):
# update target network
update_op = v_target.assign(v_source)
target_network_update.append(update_op)
return tf.group(*target_network_update)
现在,我们定义create_graph函数并构建我们的计算图:
def create_graph(self):
我们计算q_values并选择具有最大q值的动作:
with tf.name_scope("pick_action"):
# placeholder for state
self.state = tf.placeholder(tf.float32, (None,)+self.state_size , name="state")
# placeholder for q values
self.q_values = tf.identity(self.qnet(self.state) , name="q_values")
# placeholder for predicted actions
self.predicted_actions = tf.argmax(self.q_values, dimension=1 , name="predicted_actions")
# plot histogram to track max q values
tf.histogram_summary("Q values", tf.reduce_mean(tf.reduce_max(self.q_values, 1))) # save max q-values to track learning
接下来,我们计算目标未来奖励:
with tf.name_scope("estimating_future_rewards"):
self.next_state = tf.placeholder(tf.float32, (None,)+self.state_size , name="next_state")
self.next_state_mask = tf.placeholder(tf.float32, (None,) , name="next_state_mask")
self.rewards = tf.placeholder(tf.float32, (None,) , name="rewards")
self.next_q_values_targetqnet = tf.stop_gradient(self.target_qnet(self.next_state), name="next_q_values_targetqnet")
self.next_q_values_qnet = tf.stop_gradient(self.qnet(self.next_state), name="next_q_values_qnet")
self.next_selected_actions = tf.argmax(self.next_q_values_qnet, dimension=1)
self.next_selected_actions_onehot = tf.one_hot(indices=self.next_selected_actions, depth=self.action_size)
self.next_max_q_values = tf.stop_gradient( tf.reduce_sum( tf.mul( self.next_q_values_targetqnet, self.next_selected_actions_onehot ) , reduction_indices=[1,] ) * self.next_state_mask )
self.target_q_values = self.rewards + self.discount_factor*self.next_max_q_values
接下来,我们使用 RMSProp 优化器执行优化:
with tf.name_scope("optimization_step"):
self.action_mask = tf.placeholder(tf.float32, (None, self.action_size) , name="action_mask")
self.y = tf.reduce_sum( self.q_values * self.action_mask , reduction_indices=[1,])
## ERROR CLIPPING
self.error = tf.abs(self.y - self.target_q_values)
quadratic_part = tf.clip_by_value(self.error, 0.0, 1.0)
linear_part = self.error - quadratic_part
self.loss = tf.reduce_mean( 0.5*tf.square(quadratic_part) + linear_part )
# optimize the gradients
qnet_gradients = self.qnet_optimizer.compute_gradients(self.loss, self.qnet.variables())
for i, (grad, var) in enumerate(qnet_gradients):
if grad is not None:
qnet_gradients[i] = (tf.clip_by_norm(grad, 10), var)
self.qnet_optimize = self.qnet_optimizer.apply_gradients(qnet_gradients)
将主要网络权重复制到目标网络:
with tf.name_scope("target_network_update"):
self.hard_copy_to_target = DQN.copy_to_target_network(self.qnet, self.target_qnet)
我们定义了store函数,用于将所有经验存储在experience_replay_buffer中:
def store(self, state, action, reward, next_state, is_terminal):
# rewards clipping
if self.reward_clipping > 0.0:
reward = np.clip(reward, -self.reward_clipping, self.reward_clipping)
self.experience_replay.store(state, action, reward, next_state, is_terminal)
我们定义了一个action函数,用于使用衰减的ε贪婪策略选择动作:
def action(self, state, training = False):
if self.num_training_steps > self.exploration_period:
epsilon = self.final_exploration_epsilon
else:
epsilon = self.initial_exploration_epsilon - float(self.num_training_steps) * (self.initial_exploration_epsilon - self.final_exploration_epsilon) / self.exploration_period
if not training:
epsilon = 0.05
if random.random() <= epsilon:
action = random.randint(0, self.action_size-1)
else:
action = self.session.run(self.predicted_actions, {self.state:[state] } )[0]
return action
现在,我们定义一个train函数来训练我们的网络:
def train(self):
将主要网络权重复制到目标网络:
if self.num_training_steps == 0:
print "Training starts..."
self.qnet.copy_to(self.target_qnet)
记忆回放中的示例经验:
minibatch = self.experience_replay.sample()
从minibatch获取状态,动作,奖励和下一个状态:
batch_states = np.asarray( [d[0] for d in minibatch] )
actions = [d[1] for d in minibatch]
batch_actions = np.zeros( (self.minibatch_size, self.action_size) )
for i in xrange(self.minibatch_size):
batch_actions[i, actions[i]] = 1
batch_rewards = np.asarray( [d[2] for d in minibatch] )
batch_newstates = np.asarray( [d[3] for d in minibatch] )
batch_newstates_mask = np.asarray( [not d[4] for d in minibatch] )
执行训练操作:
scores, _, = self.session.run([self.q_values, self.qnet_optimize],
{ self.state: batch_states,
self.next_state: batch_newstates,
self.next_state_mask: batch_newstates_mask,
self.rewards: batch_rewards,
self.action_mask: batch_actions} )
更新目标网络权重:
if self.num_training_steps % self.target_qnet_update_frequency == 0:
self.session.run( self.hard_copy_to_target )
print 'mean maxQ in minibatch: ',np.mean(np.max(scores,1))
str_ = self.session.run(self.summarize, { self.state: batch_states,
self.next_state: batch_newstates,
self.next_state_mask: batch_newstates_mask,
self.rewards: batch_rewards,
self.action_mask: batch_actions})
self.summary_writer.add_summary(str_, self.num_training_steps)
self.num_training_steps += 1
赛车
到目前为止,我们已经看到了如何构建决斗 DQN。 现在,我们将看到在玩赛车游戏时如何利用我们的决斗 DQN。
首先,让我们导入必要的库:
import gym
import time
import logging
import os
import sys
import tensorflow as tf
初始化所有必需的变量:
ENV_NAME = 'Seaquest-v0'
TOTAL_FRAMES = 20000000
MAX_TRAINING_STEPS = 20 * 60 * 60 / 3
TESTING_GAMES = 30
MAX_TESTING_STEPS = 5 * 60 * 60 / 3
TRAIN_AFTER_FRAMES = 50000
epoch_size = 50000
MAX_NOOP_START = 30
LOG_DIR = 'logs'
outdir = 'results'
logger = tf.train.SummaryWriter(LOG_DIR)
# Intialize tensorflow session
session = tf.InteractiveSession()
构建智能体:
agent = DQN(state_size=env.observation_space.shape,
action_size=env.action_space.n,
session=session,
summary_writer = logger,
exploration_period = 1000000,
minibatch_size = 32,
discount_factor = 0.99,
experience_replay_buffer = 1000000,
target_qnet_update_frequency = 20000,
initial_exploration_epsilon = 1.0,
final_exploration_epsilon = 0.1,
reward_clipping = 1.0,
)
session.run(tf.initialize_all_variables())
logger.add_graph(session.graph)
saver = tf.train.Saver(tf.all_variables())
存储录音:
env.monitor.start(outdir+'/'+ENV_NAME,force = True, video_callable=multiples_video_schedule)
num_frames = 0
num_games = 0
current_game_frames = 0
init_no_ops = np.random.randint(MAX_NOOP_START+1)
last_time = time.time()
last_frame_count = 0.0
state = env.reset()
现在,让我们开始训练:
while num_frames <= TOTAL_FRAMES+1:
if test_mode:
env.render()
num_frames += 1
current_game_frames += 1
给定当前状态,选择操作:
action = agent.action(state, training = True)
在环境上执行操作,接收reward,然后移至next_state:
next_state,reward,done,_ = env.step(action)
将此转移信息存储在experience_replay_buffer中:
if current_game_frames >= init_no_ops:
agent.store(state,action,reward,next_state,done)
state = next_state
训练智能体:
if num_frames>=TRAIN_AFTER_FRAMES:
agent.train()
if done or current_game_frames > MAX_TRAINING_STEPS:
state = env.reset()
current_game_frames = 0
num_games += 1
init_no_ops = np.random.randint(MAX_NOOP_START+1)
在每个周期之后保存网络参数:
if num_frames % epoch_size == 0 and num_frames > TRAIN_AFTER_FRAMES:
saver.save(session, outdir+"/"+ENV_NAME+"/model_"+str(num_frames/1000)+"k.ckpt")
print "epoch: frames=",num_frames," games=",num_games
我们每两个周期测试一次表现:
if num_frames % (2*epoch_size) == 0 and num_frames > TRAIN_AFTER_FRAMES:
total_reward = 0
avg_steps = 0
for i in xrange(TESTING_GAMES):
state = env.reset()
init_no_ops = np.random.randint(MAX_NOOP_START+1)
frm = 0
while frm < MAX_TESTING_STEPS:
frm += 1
env.render()
action = agent.action(state, training = False)
if current_game_frames < init_no_ops:
action = 0
state,reward,done,_ = env.step(action)
total_reward += reward
if done:
break
avg_steps += frm
avg_reward = float(total_reward)/TESTING_GAMES
str_ = session.run( tf.scalar_summary('test reward ('+str(epoch_size/1000)+'k)', avg_reward) )
logger.add_summary(str_, num_frames)
state = env.reset()
env.monitor.close()
我们可以看到智能体如何学习赢得赛车游戏,如以下屏幕截图所示:

总结
在本章中,我们学习了如何详细实现决斗 DQN。 我们从用于游戏画面预处理的基本环境包装器函数开始,然后定义了QNetworkDueling类。 在这里,我们实现了决斗 Q 网络,该网络将 DQN 的最终全连接层分为值流和优势流,然后将这两个流组合以计算q值。 之后,我们看到了如何创建回放缓冲区,该缓冲区用于存储经验并为网络训练提供经验的小批量样本,最后,我们使用 OpenAI 的 Gym 初始化了赛车环境并训练了我们的智能体。 在下一章第 13 章,“最新进展和后续步骤”中,我们将看到 RL 的一些最新进展。
问题
问题列表如下:
- DQN 和决斗 DQN 有什么区别?
- 编写用于回放缓冲区的 Python 代码。
- 什么是目标网络?
- 编写 Python 代码以获取优先级的经验回放缓冲区。
- 创建一个 Python 函数来衰减
ε贪婪策略。 - 决斗 DQN 与双 DQN 有何不同?
- 创建用于将主要网络权重更新为目标网络的 Python 函数。
进一步阅读
以下链接将帮助您扩展知识:
十三、最新进展和后续步骤
恭喜你! 您已进入最后一章。 我们已经走了很长一段路! 我们从 RL 的基础知识开始,例如 MDP,蒙特卡洛方法和 TD 学习,然后转向高级深度强化学习算法,例如 DQN,DRQN 和 A3C。 我们还了解了有趣的最新策略梯度方法,例如 DDPG,PPO 和 TRPO,并建立了赛车智能体作为我们的最终项目。 但是 RL 每天还有越来越多的进步供我们探索。 在本章中,我们将学习 RL 的一些进步,然后介绍分层和逆 RL。
在本章中,您将学习以下内容:
- 想象力增强智能体(I2A)
- 从人类偏好学习
- 来自演示的深度 Q 学习
- 事后经验回放
- 分层强化学习
- 逆强化学习
想象力增强智能体
您是国际象棋迷吗? 如果我要你下棋,你会怎么玩? 在棋盘上移动任何棋子之前,您可能会想象移动任何棋子并移动您认为会帮助您获胜的棋子的后果。 因此,基本上,在采取任何措施之前,您先想像一下后果,如果有利,则继续进行该措施,否则就不要执行该措施。
同样,想象力增强的智能体也会增加想象力。 在环境中采取任何行动之前,他们会想象采取行动的后果,如果他们认为该行动会带来良好的回报,他们将执行该行动。 他们还想象采取不同行动的后果。 用想象力增强智能体是迈向通用人工智能的下一步。
现在,我们将简短地了解想象力增强智能体的工作原理。 I2A 充分利用了基于模型的学习和基于模型的学习。
I2A 的架构如下:

智能体采取的行动是基于模型的路径和没有模型的路径的结果。 在基于模型的路径中,我们有一些称为“滚动编码器”的产品。 这些推出编码器是智能体执行想象任务的地方。 让我们仔细看一下滚动编码器。 推出编码器如下所示:

推出编码器分为两层:想象未来和编码器。 想象未来是想象力发生的地方。 看上图。 想象未来由想象核心组成。 当将状态oₜ馈送到想象力核心时,我们得到了新状态o_hat[t + 1]和奖励r_hat[t + 1],当我们将这个新状态o_hat[t + 1]馈给了下一个想象力核心时,我们得到了下一个新状态o_hat[t + 2]和奖励r_hat[t + 2]。 当我们在某些n步骤中重复这些步骤时,我们会得到一个基本上是状态和奖励对的推出,然后我们使用诸如 LSTM 的编码器对该推广进行编码。 结果,我们得到了卷展编码。 这些推出编码实际上是描述未来想象路径的嵌入。 我们将针对未来不同的想象路径使用多个推出编码器,并使用聚合器来聚合此推出编码器。
等待。 想象力如何在想象力核心中发生? 想象力核心中实际上是什么? 下图显示了单个想象核心:

想象核心由策略网络和环境模型组成。 环境模型实际上是发生一切的地方。 环境模型从智能体到目前为止执行的所有动作中学习。 它获取有关状态o_hat[t]的信息,并根据经验来想象所有可能的期货,并选择给予较高奖励的操作a_hat[t]。
扩展了所有组件的 I2A 架构如下所示:

您以前玩过推箱子吗? 推箱子是一款经典的益智游戏,玩家必须将盒子推到目标位置。 游戏的规则非常简单:盒子只能推入而不能拉出。 如果我们向错误的方向推箱子,那么难题将变得无法解决:

如果我们被要求玩推箱子,那么在做出任何举动之前,我们会想一想并计划好,因为糟糕的举动会导致游戏结束。 I2A 架构将在此类环境中提供良好的结果,在这种环境中,智能体必须提前计划才能采取任何措施。 本文的作者在推箱子上测试了 I2A 表现,并取得了显著成果。
从人类偏好学习
向人类学习是 RL 的重大突破。 该算法由 OpenAI 和 DeepMind 的研究人员提出。 该算法背后的思想是使智能体根据人的反馈进行学习。 最初,智能体会随机行动,然后将执行动作的智能体的两个视频片段提供给人类。 人们可以检查视频剪辑,并告诉智能体哪个视频剪辑更好,也就是说,智能体在哪个视频中更好地执行任务,并将其实现目标。 给出反馈后,智能体将尝试执行人类喜欢的操作并相应地设置奖励。 设计奖励函数是 RL 中的主要挑战之一,因此,与智能体进行人为互动可以直接帮助我们克服挑战,也可以最大程度地减少编写复杂目标函数的过程。
训练过程如下图所示:

让我们看一下以下步骤:
- 首先,我们的智能体通过随机策略与环境交互。
- 智能体与环境交互的行为将在两到三秒钟的视频剪辑对中捕获并提供给人类。
- 人员将检查视频剪辑,并了解智能体在哪个视频剪辑中表现更好。 他们会将结果发送给奖励预测器。
- 现在,智能体将从预测的奖励中接收这些信号,并根据人类的反馈设置其目标和奖励函数。
轨迹是一系列观察和动作。 我们可以将轨迹段表示为σ,因此σ = (o0, a0), (o1, a1), ..., (o[k-1], a[k-1]),其中o是观察值,a是动作。 智能体从环境接收观察并执行某些操作。 假设我们将交互序列存储在两个轨迹段中,即σ[1]和σ[2]。 现在,这两个轨迹已显示给人类。 如果人类更喜欢σ[2]而不是σ[1],那么智能体的目标是产生人类所喜欢的轨迹,并相应地设置奖励函数。 这些轨迹段以(σ[1], σ[2], μ)的形式存储在数据库中; 如果人类更喜欢σ[2]而不是σ[1],则μ设置为更喜欢σ[2]。 如果没有一条轨迹是可取的,则两条轨迹都将从数据库中删除。 如果两者均首选,则将μ设置为统一。
您可以在这个页面上查看视频,以了解算法的工作原理。
来自演示的深度 Q 学习
我们已经了解了很多有关 DQN 的知识。 我们从原始 DQN 开始,然后看到了各种改进,例如双重 DQN,决斗的网络架构和优先级的经验回放。 我们还学会了构建 DQN 来玩 Atari 游戏。 我们将智能体与环境的交互存储在经验缓冲区中,并使智能体从这些经验中学习。 但是问题是,我们花了很多训练时间来提高表现。 对于在模拟环境中学习,这很好,但是当我们让智能体在现实环境中学习时,会引起很多问题。 为了克服这个问题,谷歌公司 DeepMind 的研究人员在演示(DQfd)的基础上,对 DQN 进行了改进,称为深度 Q 学习。
如果我们已经有了一些演示数据,则可以将这些演示直接添加到经验回放缓冲区中。 例如,考虑一个学习玩 Atari 游戏的智能体。 如果我们已经有了一些演示数据来告诉我们的智能体,哪个状态更好,哪个动作可以提供良好的回报,那么智能体可以直接使用此数据进行学习。 即使是少量的演示,也可以提高智能体的表现并最大程度地减少训练时间。 由于演示的数据将直接添加到优先经验回放缓冲区中,因此智能体可以从演示数据中使用的数据量以及智能体可以从其自己的交互中用于学习的数据量将由优先经验回放来控制缓冲区,因为经验将被优先考虑。
DQfd 中的损失函数将是各种损失的总和。 为了防止我们的智能体过度适合演示数据,我们在网络权重上计算 L2 正则化损失。 我们像往常一样计算 TD 损失,也计算监督损失,以了解我们的智能体如何从演示数据中学习。 本文的作者在 DQfd 和各种环境下进行了实验,并且 DQfd 的表现比双重决斗 DQN 优先排序的表现更好,更快。
您可以观看此视频,以了解 DQfd 如何学会玩“Private Eye”游戏。
事后经验回放
我们已经了解了如何在 DQN 中使用经验回放来避免相关经验。 此外,我们了解到,优先经验回放是对原始经验回放的一种改进,因为它优先考虑 TD 误差的每个经验。 现在,我们将研究由 OpenAI 研究人员提出的一种用于处理稀疏奖励的名为事后经验回放(HER)的新技术。 您还记得如何学习骑自行车吗? 第一次尝试时,您将无法正确平衡自行车。 您可能多次无法正确平衡。 但是,所有这些失败并不意味着您没有学到任何东西。 失败会教你如何不平衡自行车。 即使您没有学会骑自行车(目标),您也学习了一个不同的目标,即,您学会了如何不平衡自行车。 这是我们人类学习的方式,对吗? 我们从失败中学习,这就是事后回顾经验的想法。
让我们考虑本文中给出的相同示例。 如图所示,查看 FetchSlide 环境。 在这种环境下的目标是移动机械臂并在桌子上滑动冰球以击中目标,这是一个红色的小圆圈(图表来自这里):

在最初的几次尝试中,智能体不一定能实现目标。 因此,智能体只收到 -1 作为奖励,这告诉智能体他做错了,没有达到目标:

但这并不意味着智能体没有学到任何东西。 智能体已经实现了一个不同的目标,即它学会了接近实际目标。 因此,我们认为它有一个不同的目标,而不是认为它是失败的。 如果我们在多次迭代中重复此过程,智能体将学会实现我们的实际目标。 HER 可以应用于任何非策略算法。 将 HER 的表现与没有 HER 的 DDPG 进行比较,反之亦然,可以看出带有 HER 的 DDPG 的收敛速度比没有 HER 的 DDPG 快。 您可以在以下视频中看到 HER 的表现。
分层强化学习
RL 的问题在于它无法在大量状态空间和动作下很好地扩展,最终导致维度的诅咒。 分层强化学习(HRL)被提出来解决维数的诅咒,其中我们将大问题解压缩为层次结构中的小子问题。 假设智能体的目标是从学校到达家中。 在这里,问题被分解为一组子目标,例如从学校大门出来,预定出租车等。
HRL 中使用了不同的方法,例如状态空间分解,状态抽象和时间抽象。 在状态空间分解中,我们将状态空间分解为不同的子空间,并尝试在较小的子空间中解决问题。 分解状态空间还可以加快探查速度,因为智能体程序不希望探究整个状态空间。 在状态抽象中,智能体会忽略与在当前状态空间中实现当前子任务无关的变量。 在时间抽象中,将动作序列和动作集分组,这将单个步骤分为多个步骤。
现在,我们可以研究 HRL 中最常用的算法之一,称为 MAXQ 值函数分解。
MAXQ 值函数分解
MAXQ 值函数分解是 HRL 中最常用的算法之一。 让我们看看 MAXQ 的工作原理。 在 MAXQ 值函数分解中,我们将值函数分解为每个子任务的一组值函数。 让我们以本文中给出的相同示例为例。 还记得我们使用 Q 学习和 SARSA 解决的出租车问题吗?
总共有四个地点,智能体必须在一个地点接客并在另一地点下车。 智能体将获得 +20 积分作为成功下车的奖励,而每走一步便获得 -1 积分。 该智能体还将因非法取送丢掉 -10 分。 因此,我们智能体的目标是学会在短时间内在正确的位置上落客而不增加非法乘客。
接下来显示的是环境,其中的字母(R,G,Y,B)代表不同的位置, 黄色矩形是由我们的智能体驾驶的出租车:

现在,我们将目标分为以下四个子任务:
- 导航:这里的目标是将出租车从当前位置驾驶到目标位置之一。 导航子任务应使用北,南,东和西四个原始动作。
- 获取:这里的目标是将出租车从当前位置驾驶到乘客的位置并接客。
- 放置:这里的目标是将出租车从当前位置驾驶到乘客的目的地位置并下车。
- 根:根是整个任务。
我们可以在称为任务图的有向无环图中表示所有这些子任务,如下所示:

您可以在上图中看到所有子任务都是按层次排列的。 每个节点代表子任务或原始动作,并且每个边都连接一个子任务可以调用其子子任务的方式。
导航子任务具有四个原始动作:东部,西部,北部和南部。获取子任务具有拾取基本操作和导航子任务; 同样,放置子任务,具有放置(放置)原始动作并导航子任务。
在 MAXQ 分解中,MDP
将分为一组任务,例如(M[0], M[1], ,,, M[n])。
M₀是根任务,M[1], M[2], M[n]是子任务。
子任务Mᵢ使用状态Sᵢ,操作Aᵢ,概率转换函数P[i]^π(s', N | s, a)和预期奖励函数R_bar(s, a) = V^π(a, s)定义半 MDP,其中V^π(a, s)是子任务M[a]在s状态下的投影值函数。
如果动作a是原始动作,那么我们可以将V^π(a, s)定义为在s状态下执行动作a的预期立即回报:

现在,我们可以按贝尔曼方程形式重写前面的值函数,如下所示:
----(1)
我们将状态动作值函数Q表示如下:
----(2)
现在,我们再定义一个称为完成函数的函数,它是完成子任务Mᵢ的预期折扣累积奖励:
----(3)
对于方程式(2)和(3),我们可以将 Q 函数写为:

最后,我们可以将值函数重新定义为:

前面的等式会将根任务的值函数分解为各个子任务任务的值函数。
为了高效地设计和调试 MAXQ 分解,我们可以按如下方式重绘任务图:

我们重新设计的图包含两种特殊类型的节点:最大节点和 Q 节点。 最大节点定义任务分解中的子任务,而 Q 节点定义可用于每个子任务的动作。
逆强化学习
那么,我们在 RL 中做了什么? 我们试图找到具有奖励函数的最优策略。 逆强化学习只是强化学习的逆,也就是说,给出了最优策略,我们需要找到奖励函数。 但是为什么反强化学习会有所帮助? 设计奖励函数不是一项简单的任务,而较差的奖励函数会导致智能体的不良行为。 我们并不总是知道适当的奖励函数,但我们知道正确的策略,即在每个状态采取正确的行动。 因此,这种最佳策略由人类专家提供给智能体,智能体尝试学习奖励函数。 例如,考虑一个学习在真实环境中行走的智能体; 很难为将要执行的所有动作设计奖励函数。 取而代之的是,我们可以将人类专家的演示(最佳策略)提供给智能体,智能体将尝试学习奖励函数。
RL 周围有各种改进和进步。 现在,您已经阅读完本书,可以开始探索强化学习的各种进步,并开始尝试各种项目。 学习和加强!
总结
在本章中,我们了解了 RL 的一些最新进展。 我们看到了 I2A 架构如何将想象力核心用于前瞻性计划,然后如何根据人的喜好来训练智能体。 我们还了解了 DQfd,它可以通过从演示中学习来提高 DQN 的表现并减少其训练时间。 然后,我们研究了事后的经验回放,从中我们了解了智能体如何从失败中学习。
接下来,我们学习了层次 RL,其中目标被解压缩为子目标层次。 我们了解了反向 RL,其中智能体尝试根据给定策略来学习奖励函数。 RL 每天都在以有趣的进步发展。 既然您已经了解了各种强化学习算法,则可以构建智能体以执行各种任务并为 RL 研究做出贡献。
问题
问题列表如下:
- 智能体的想象力是什么?
- 想象力的核心是什么?
- 智能体如何从人的偏好中学习?
- DQfd 与 DQN 有何不同?
- 什么是事后经验回放?
- 分层强化学习有什么需求?
- 逆强化学习与强化学习有何不同?
进一步阅读
您可以进一步参考以下论文:
十四、答案
第 1 章
- 强化学习(RL)是机器学习的一个分支,其中学习是通过与环境交互来进行的。
- 与其他 ML 范例不同,RL 通过训练和误差方法工作。
- 智能体是做出明智决策的软件程序,它们基本上是 RL 的学习器。
- 策略函数指定在每个状态下要执行的操作,而值函数指定每个状态的值。
- 在基于模型的智能体中,使用以前的经验,而在无模型的学习中,则不会有任何以前的经验。
- 确定性的,随机的,完全可观察的,部分可观察的,离散的连续的,事件的和非事件的。
- OpenAI Universe 为训练 RL 智能体提供了丰富的环境。
- 请参阅 RL 的“应用”部分。
第 2 章
-
conda create --name universe python=3.6 anaconda -
使用 Docker,我们可以将应用及其依赖关系打包,称为容器,并且我们可以在服务器上运行应用,而无需将任何外部依赖关系与打包的 Docker 容器一起使用。
-
gym.make(env_name) -
from gym import envs
print(envs.registry.all()) -
OpenAI Universe 是 OpenAI Gym 的扩展,还提供各种丰富的环境。
-
占位符用于提供外部数据,而变量用于保持值。
-
TensorFlow 中的所有内容都将表示为由节点和边组成的计算图,其中节点是数学运算(例如加法,乘法等),而边是张量。
-
计算图只会被定义; 为了执行计算图,我们使用 TensorFlow 会话。
第 3 章
- 马尔可夫性质指出,未来仅取决于现在而不是过去。
- MDP 是马尔可夫链的延伸。 它提供了用于建模决策情况的数学框架。 几乎所有的 RL 问题都可以建模为 MDP。
- 请参阅“折扣系数”部分。
- 折扣系数决定了我们对未来奖励和即时奖励的重视程度。
- 我们使用贝尔曼函数求解 MDP。
- 有关值和 Q 函数的信息,请参见“推导贝尔曼方程”部分。
- 值函数指定状态的优劣,而 Q 函数指定状态下的行为的优劣。
- 请参阅“值迭代”和“策略迭代”部分。
第 4 章
-
当环境模型未知时,在 RL 中使用蒙特卡洛算法。
-
请参阅“使用蒙特卡洛估计
pi的值”部分。 -
在蒙特卡洛预测中,我们通过取均值回报而不是期望回报来近似值函数。
-
在蒙特卡洛的每次访问中,我们平均将剧集中每次访问状态的收益均值化。 但是在首次访问 MC 方法中,我们仅在剧集中首次访问状态时才对返回值进行平均。
-
请参阅“蒙特卡洛控制”部分。
-
请参阅“策略上的蒙特卡洛控制”和“策略外的蒙特卡洛控制”部分
-
请参阅“让我们使用蒙特卡洛玩二十一点”部分。
第 5 章
- 蒙特卡罗方法仅适用于剧集任务,而 TD 学习可应用于剧集任务和非剧集任务
- 实际值与预测值之差称为 TD 误差
- 请参阅“TD 预测”和“TD 控制”部分
- 请参阅“使用 Q 学习解决滑行问题”部分
- 在 Q 学习中,我们使用
ε贪婪策略采取行动,并且在更新 Q 值的同时,我们仅采取最大行动。 在 SARSA 中,我们使用ε贪婪策略采取措施,并且在更新 Q 值的同时,我们使用ε贪婪策略采取措施。
第 6 章
- MAB 实际上是一台老丨虎丨机,是一种在赌场玩的赌博游戏,您可以拉动手臂(杠杆)并根据随机生成的概率分布获得支出(奖励)。 一台老丨虎丨机称为单臂老丨虎丨机,当有多台老丨虎丨机时,称为多臂老丨虎丨机或 k 臂老丨虎丨机。
- 当业务代表不确定是使用以前的经验来探索新动作还是利用最佳动作时,就会出现探索-利用困境。
- ε用于确定智能体是否应使用
1-ε进行探索或利用我们选择最佳作用的作用,而使用ε则探索新作用。 - 我们可以使用各种算法(例如
ε贪婪策略,softmax 探索,UCB,Thompson 采样)解决探索-利用难题。 - UCB 算法可帮助我们根据置信区间选择最佳分支。
- 在 Thomson 抽样中,我们使用先验分布进行估计,而在 UCB 中,我们使用置信区间进行估计。
第 7 章
- 在神经元中,我们通过应用称为激活或传递函数的函数
f()将非线性引入结果z。 请参阅“人工神经元”部分。 - 激活函数用于引入非线性。
- 我们计算成本函数相对于权重的梯度以最小化误差。
- RNN 不仅基于当前输入,而且还基于先前的隐藏状态来预测输出。
- 在网络反向传播时,如果梯度值变得越来越小,则称为消失梯度问题,如果梯度值变得更大,则它正在爆炸梯度问题。
- 门是 LSTM 中的特殊结构,用于决定保留,丢弃和更新哪些信息。
- 池化层用于减少特征映射的维数,并且仅保留必要的细节,因此可以减少计算量。
第 8 章
- 深度 Q 网络(DQN)是用于近似 Q 函数的神经网络。
- 经验回放用于删除智能体经验之间的相关性。
- 当我们使用同一网络来预测目标值和预测值时,会有很多差异,因此我们使用单独的目标网络。
- 由于最大运算符,DQN 高估了 Q 值。
- 通过具有两个单独的 Q 函数,每个学习都独立地加倍 DQN,从而避免了高估 Q 值的情况。
- 经验是优先经验回放中基于 TD 误差的优先级。
- DQN 通过将 Q 函数计算分解为值函数和优势函数来精确估计 Q 值。
第 9 章
- DRQN 利用循环神经网络(RNN),其中 DQN 利用原始神经网络。
- 当可以部分观察 MDP 时,不使用 DQN。
- 请参阅“DRQN 的 Doom”部分。
- 与 DRQN 不同,DARQN 利用注意力机制。
- DARQN 用于理解和专注于游戏屏幕的特定区域,这一点更为重要。
- 软硬注意力。
- 即使该举动无用,我们也将智能体的每次举动设置为活奖赏 0。
第 10 章
- A3C 是“异步优势演员评论家网络”,它使用多个智能体进行并行学习。
- 三个 A 是异步,优势,演员评论家。
- 与 DQN 相比,A3C 需要更少的计算能力和训练时间。
- 所有智能体(员工)都在环境副本中工作,然后全局网络汇总他们的经验。
- 熵用于确保足够的探索。
- 请参阅“A3C 的工作方式”部分。
第十一章
-
策略梯度是 RL 中令人惊奇的算法之一,在该算法中,我们直接优化由某些参数设置参数的策略。
-
策略梯度是有效的,因为我们无需计算 Q 函数即可找到最佳策略。
-
演员网络的作用是通过调整参数来确定状态中的最佳动作,而评论家的作用是评估演员产生的动作。
-
请参阅“信任区域策略优化”部分
-
我们迭代地改进了该策略,并施加了一个约束,即旧策略和新策略之间的 Kullback-Leibler(KL)差异要小于某个常数。 该约束称为信任区域约束。
-
PPO 通过将约束更改为惩罚项来修改 TRPO 的目标函数,因此我们不想执行共轭梯度。
第十二章
- DQN 直接计算 Q 值,而决斗 DQN 将 Q 值计算分解为值函数和优势函数。
- 请参阅“记忆回放”部分。
- 当我们使用同一网络来预测目标值和预测值时,会有很多差异,因此我们使用单独的目标网络。
- 请参阅“记忆回放”部分。
- 请参阅“决斗网络”部分。
- 决斗 DQN 将 Q 值计算分解为值函数和优势函数,而双 DQN 使用两个 Q 函数来避免高估。
- 请参阅“决斗网络”部分。
第十三章
-
智能体中的想象力指定了采取任何行动之前的可视化和计划。
-
想象力核心由执行想象力的策略网络和环境模型组成。
-
智能体反复从人类那里获得反馈,并根据人类的喜好改变目标。
-
DQfd 使用一些演示数据进行训练,因为 DQN 并未预先使用任何演示数据。
-
请参阅事后经验回放(HER)部分。
-
提出了分层强化学习(HRL),以解决维数诅咒,其中我们将大问题解压缩为层次结构中的小子问题
-
我们试图在 RL 中找到给定奖励函数的最优策略,而在逆向强化学习中,给出最优策略并找到奖励函数




浙公网安备 33010602011771号