原文:
annas-archive.org/md5/8a22ccc4f94e0a5a98e16b22a2b1f959译者:飞龙
序言
强化学习是机器学习中最令人兴奋且发展最快的领域之一。这得益于近年来开发出许多新颖的算法和发布的令人惊叹的结果。
本书将帮助你了解强化学习的核心概念,包括 Q 学习、策略梯度、蒙特卡洛过程以及多个深度强化学习算法。在阅读本书时,你将参与使用图像、文本和视频等多种模态的数据集进行项目工作。你将积累在多个领域的经验,包括游戏、图像处理和物理仿真。你还将探索如 TensorFlow 和 OpenAI Gym 等技术,以实现深度学习强化学习算法,这些算法还可以预测股价、生成自然语言,甚至构建其他神经网络。
本书结束时,你将通过八个强化学习项目获得实践经验,每个项目涉及不同的主题和/或算法。我们希望这些实用的练习能够帮助你更好地理解强化学习领域,并且学会如何将其算法应用到现实生活中的各种问题。
本书适用对象
Python 强化学习项目适合那些具有机器学习技术基础的数据显示分析师、数据科学家和机器学习专业人士,特别是那些希望探索机器学习新兴领域(如强化学习)的人。想要进行实际项目实现的个人也会发现本书非常有用。
本书内容概述
第一章,强化学习入门,介绍了人工智能、强化学习、深度学习、该领域的历史/应用及其他相关话题。它还将提供一个高层次的深度学习和 TensorFlow 概念概述,特别是与强化学习相关的部分。
第二章,平衡小车杆,将引导你实现第一个强化学习算法,使用 Python 和 TensorFlow 来解决小车平衡问题。
第三章,玩 ATARI 游戏,将引导你创建第一个深度强化学习算法来玩 ATARI 游戏。
第四章,模拟控制任务,简要介绍了用于连续控制问题的演员-评论家算法。你将学习如何模拟经典控制任务,如何实现基础的演员-评论家算法,并了解控制问题的最先进算法。
第五章,在 Minecraft 中构建虚拟世界,将前面章节中讲解的高级概念应用到 Minecraft 中,这是一个比 ATARI 游戏更复杂的游戏。
第六章,学习下围棋,让您构建一个能够下围棋的模型,这款流行的亚洲棋类游戏被认为是世界上最复杂的游戏之一。
第七章,创建聊天机器人,将教你如何在自然语言处理中应用深度强化学习。我们的奖励函数将是一个未来导向的函数,您将学习如何在创建此函数时考虑概率。
第八章,生成深度学习图像分类器,介绍了强化学习中最新和最激动人心的进展之一:使用强化学习生成深度学习模型。我们探索了由 Google Brain 发布的尖端研究,并实施了介绍的算法。
第九章,预测未来股票价格,讨论了构建能够预测股票价格的代理。
第十章,展望未来,通过讨论强化学习的真实世界应用以及介绍未来学术工作的潜在领域,结束了本书。
要充分利用本书
本书涵盖的示例可以在 Windows、Ubuntu 或 macOS 上运行。所有安装说明都已涵盖。需要基本的 Python 和机器学习知识。建议您拥有 GPU 硬件,但不是必需的。
下载示例代码文件
您可以从您的帐户在www.packt.com下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packt.com/support并注册,以便直接通过电子邮件获取文件。
您可以按照以下步骤下载代码文件:
-
在www.packt.com登录或注册。
-
选择“支持”选项卡。
-
点击“代码下载与勘误”。
-
在搜索框中输入书名,并按照屏幕上的说明操作。
下载文件后,请确保使用最新版本的以下工具解压缩文件夹:
-
Windows 的 WinRAR/7-Zip
-
Mac 上的 Zipeg/iZip/UnRarX
-
Linux 的 7-Zip/PeaZip
本书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Python-Reinforcement-Learning-Projects。如果代码有更新,将在现有的 GitHub 仓库中更新。
我们还从我们丰富的书籍和视频目录中提供了其他代码包,可在**github.com/PacktPublishing/**查看!
使用的约定
本书采用了多种文本约定。
CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 账户名。例如:“gym-minecraft包的接口与其他 Gym 环境相同。”
代码块设置如下:
import logging
import minecraft_py
logging.basicConfig(level=logging.DEBUG)
任何命令行输入或输出如下所示:
python3 -m pip install gym
python3 -m pip install pygame
粗体:表示新术语、重要单词或在屏幕上显示的单词。例如,菜单或对话框中的单词在文本中会这样显示。举个例子:“从管理面板中选择系统信息。”
警告或重要说明以这种形式出现。
提示和技巧通常以这种形式出现。
联系我们
我们总是欢迎读者的反馈。
一般反馈:如果您对本书的任何部分有疑问,请在邮件主题中提及书名,并通过customercare@packtpub.com与我们联系。
勘误表:虽然我们已经尽最大努力确保内容的准确性,但错误难免发生。如果您在本书中发现错误,我们将非常感激您能向我们报告。请访问www.packt.com/submit-errata,选择您的书籍,点击“勘误表提交表单”链接,并填写详细信息。
盗版:如果您在互联网上遇到任何形式的我们作品的非法复制品,我们将感激您提供相关的网址或网站名称。请通过copyright@packt.com联系,并附上该材料的链接。
如果您有兴趣成为作者:如果您在某个领域拥有专业知识,并且有意撰写或贡献书籍内容,请访问authors.packtpub.com。
评论
请留下评论。在您阅读并使用本书后,为什么不在您购买的站点上留下评论呢?潜在读者可以查看并参考您的公正意见来做出购买决策,我们 Packt 可以了解您对我们产品的看法,作者也可以看到您对他们书籍的反馈。谢谢!
获取更多关于 Packt 的信息,请访问packt.com。
第一章:强化学习快速入门
人工智能(AI)在未来将会是什么样子?随着 AI 算法和软件应用的日益突出,这个问题应当引起许多人的关注。AI 的研究人员和从业者面临着进一步的相关问题:我们如何实现我们设想的目标并解决已知的问题?还有哪些创新和算法尚未开发出来?机器学习中的几个子领域展示了极大的潜力,能够解答我们许多问题。本书将聚焦于其中的一个子领域——强化学习,也许它是机器学习中最令人兴奋的话题之一。
强化学习的动机来自于通过与环境互动来学习的目标。想象一下一个婴儿是如何在其环境中活动的。通过四处移动并与周围环境互动,婴儿学习物理现象、因果关系以及与其互动的物体的各种属性和特性。婴儿的学习通常受到达成某个目标的驱动,比如玩周围的物体或满足某种好奇心。在强化学习中,我们追求类似的目标;我们采用一种计算方法来学习环境。换句话说,我们的目标是设计出通过与环境互动来完成任务的算法。
这些算法的用途是什么?通过拥有一种通用的学习算法,我们可以为解决多个现实世界中的问题提供有效的解决方案。一个显著的例子是强化学习算法用于自动驾驶汽车。尽管尚未完全实现,但这样的应用场景将为社会带来巨大益处,因为强化学习算法已经在实证中证明了其在多个任务中超越人类水平的能力。一个标志性的时刻发生在 2016 年,当时 DeepMind 的 AlphaGo 程序以四比一击败了 18 次围棋世界冠军李世石。AlphaGo 本质上能够在几个月内学习并超越人类三千年来积累的围棋智慧。最近,强化学习算法已被证明在玩更复杂的实时多智能体游戏(如 Dota)中也非常有效。这些驱动游戏算法的同样算法也成功地控制机器人手臂拾取物体,并指引无人机通过迷宫。这些例子不仅展示了这些算法的能力,还暗示了它们未来可能完成的任务。
本书简介
本书为那些渴望学习强化学习的读者提供了实用的指南。我们将通过大量的算法实例及其应用,采取动手实践的方式学习强化学习。每一章都专注于一个特定的应用案例,并介绍用于解决该问题的强化学习算法。这些应用案例中有些依赖于最先进的算法;因此,通过本书,我们将学习并实现一些业内最优表现的算法和技术。
随着你阅读本书,项目的难度/复杂度会逐渐增加。下表描述了你将从每一章节中学到的内容:
| 章节名称 | 应用案例/问题 | 讨论和使用的概念/算法/技术 |
|---|---|---|
| 平衡推车杆 | 控制推车的水平移动以平衡竖直的杆 | OpenAI Gym 框架,Q 学习 |
| 玩 Atari 游戏 | 在人类水平的熟练度下玩各种 Atari 游戏 | 深度 Q 网络 |
| 模拟控制任务 | 控制一个在连续动作空间中的智能体,而非离散空间 | 确定性策略梯度(DPG)、信任域策略优化(TRPO)、多任务学习 |
| 在 Minecraft 中构建虚拟世界 | 在 Minecraft 的虚拟世界中操作一个角色 | 异步优势 Actor-Critic(A3C) |
| 学习围棋 | 围棋,世界上最古老且最复杂的棋盘游戏之一 | 蒙特卡罗树搜索、策略和价值网络 |
| 创建聊天机器人 | 在对话环境中生成自然语言 | 策略梯度方法、长短期记忆网络(LSTM) |
| 自动生成深度学习图像分类器 | 创建一个生成神经网络来解决特定任务的智能体 | 循环神经网络、策略梯度方法(REINFORCE) |
| 预测未来股票价格 | 预测股票价格并做出买卖决策 | Actor-Critic 方法、时间序列分析、经验回放 |
期望
本书最适合以下读者:
-
具备中级 Python 编程能力
-
具备基本的机器学习和深度学习理解,特别是以下主题:
-
神经网络
-
反向传播
-
卷积
-
提高泛化能力和减少过拟合的技术
-
-
喜欢动手实践的学习方式
由于本书旨在作为该领域的实用入门指南,我们尽量将理论内容控制在最小范围。然而,建议读者具备一些机器学习领域所依赖的基础数学和统计学概念的知识。这些包括以下内容:
-
微积分(单变量和多变量)
-
线性代数
-
概率论
-
图论
对这些主题有一定了解将极大帮助读者理解本书中将涉及的概念和算法。
硬件和软件要求
接下来的章节将要求你实现各种强化学习算法。因此,为了顺利学习,必须有一个合适的开发环境。特别是,你应该具备以下条件:
-
一台运行 macOS 或 Linux 操作系统的计算机(对于 Windows 用户,建议尝试设置一个安装了 Linux 镜像的虚拟机)
-
稳定的互联网连接
-
一块 GPU(最好有)
我们将 exclusively 使用 Python 编程语言来实现我们的强化学习和深度学习算法。此外,我们将使用 Python 3.6。我们将使用的库的列表可以在官方 GitHub 仓库中找到,网址是 (github.com/PacktPublishing/Python-Reinforcement-Learning-Projects)。你还可以在这个仓库中找到我们将在本书中讨论的每个算法的实现。
安装软件包
假设你已经有一个正常工作的 Python 安装,你可以通过我们的仓库中的 requirements.txt 文件安装所有所需的软件包。我们还建议你创建一个 virtualenv 来将开发环境与主操作系统隔离开来。以下步骤将帮助你构建环境并安装所需的包:
# Install virtualenv using pip
$ pip install virtualenv
# Create a virtualenv
$ virtualenv rl_projects
# Activate virtualenv
$ source rl_projects/bin/activate
# cd into the directory with our requirements.txt
(rl_projects) $ cd /path/to/requirements.txt
# pip install the required packages
(rl_projects) $ pip install -r requirements.txt
现在你已经准备好开始了!本章接下来的几节将介绍强化学习领域,并对深度学习进行回顾。
什么是强化学习?
我们的旅程始于了解强化学习的基本概念。那些熟悉机器学习的人可能已经知道几种学习范式,即监督学习和无监督学习。在监督学习中,机器学习模型有一个监督者,提供每个数据点的真实标签。模型通过最小化其预测与真实标签之间的差距来学习。因此,数据集需要对每个数据点进行标注,例如,每张狗狗和猫的图片都会有相应的标签。在无监督学习中,模型无法访问数据的真实标签,因此必须在没有标签的情况下学习数据的分布和模式。
在强化学习中,代理指的是学习完成特定任务的模型/算法。代理主要通过接收奖励信号来学习,奖励信号是衡量代理完成任务表现的标量指标。
假设我们有一个代理,负责控制机器人行走的动作;如果代理成功朝目标行走,则会获得正向奖励,而如果摔倒或未能取得进展,则会获得负向奖励。
此外,与监督学习不同,这些奖励信号不会立即返回给模型;相反,它们是作为代理执行一系列动作的结果返回的。动作就是代理在其环境中可以做的事情。环境指的是代理所处的世界,主要负责向代理返回奖励信号。代理的动作通常会根据代理从环境中感知到的信息来决定。代理感知到的内容被称为观察或状态。强化学习与其他学习范式的一个重要区别在于,代理的动作可以改变环境及其随后产生的响应。
例如,假设一个代理的任务是玩《太空入侵者》,这是一款流行的 Atari 2600 街机游戏。环境就是游戏本身,以及它运行的逻辑。在游戏过程中,代理向环境发出查询以获取观察结果。观察结果仅仅是一个(210, 160, 3)形状的数组,代表游戏屏幕,屏幕上显示代理的飞船、敌人、得分以及任何投射物。根据这个观察结果,代理做出一些动作,可能包括向左或向右移动、发射激光或什么也不做。环境接收代理的动作作为输入,并对状态进行必要的更新。
举例来说,如果激光击中敌方飞船,它会从游戏中消失。如果代理决定仅仅向左移动,游戏会相应更新代理的坐标。这个过程会一直重复,直到达到终止状态,即表示序列结束的状态。在《太空入侵者》游戏中,终止状态对应的是代理的飞船被摧毁,游戏随后返回记录的得分,该得分是根据代理成功摧毁的敌方飞船数量计算得出的。
请注意,一些环境没有终止状态,例如股票市场。这些环境会一直运行,直到它们结束。
让我们回顾一下到目前为止学习的术语:
| 术语 | 描述 | 示例 |
|---|---|---|
| 代理 | 负责学习完成任务的模型/算法。 | 自动驾驶汽车、行走机器人、视频游戏玩家 |
| 环境 | 代理所处的世界。它负责控制代理的感知内容,并提供关于代理执行特定任务表现的反馈。 | 汽车行驶的道路、视频游戏、股票市场 |
| 动作 | 代理在环境中做出的决策,通常取决于代理的感知。 | 驾驶汽车、买卖特定股票、从代理控制的太空船上发射激光 |
| 奖励信号 | 一个标量值,表示智能体在执行特定任务时的表现如何。 | 《太空入侵者》的得分、某只股票的投资回报、学习行走的机器人走过的距离 |
| 观察/状态 | 智能体可以感知的环境描述。 | 从仪表盘摄像头拍摄的视频、游戏屏幕、股市统计数据 |
| 终止状态 | 智能体无法再进行任何操作的状态。 | 迷宫的尽头,或者《太空入侵者》中的飞船被摧毁 |
更正式地说,在给定的时间步t,以下内容发生在智能体P和环境E之间:
- P queries E for some observation
- P decides to take action based on observation
- E receives and returns reward based on the action
- P receives
- E updates to based on and other factors
环境是如何计算https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/c1cbd545-2753-478c-866f-ed057127bf3d.png和https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/8f52597c-a3e6-43ed-baaa-1e849131b1fe.png的?环境通常有自己的算法,根据多个输入/因素计算这些值,包括智能体采取的行动。
有时候,环境由多个智能体组成,它们试图最大化自己的奖励。重力作用于从高处落下的球的方式,很好地表现了环境是如何运作的;就像我们的周围环境遵循物理定律一样,环境也有一些内部机制来计算奖励和下一个状态。这个内部机制通常对智能体是隐藏的,因此我们的任务是构建能够在这种不确定性中依然能做好各自任务的智能体。
在接下来的章节中,我们将更详细地讨论每个强化学习问题的主要主体——智能体(Agent)。
智能体
强化学习智能体的目标是在环境中学会出色地执行任务。从数学角度来看,这意味着最大化累计奖励R,可以通过以下公式表示:
我们只是计算每个时间步获得奖励的加权总和。https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/349f3223-b4de-429a-a18a-8ec1a2f14483.png被称为折扣因子,它是一个介于 0 和 1 之间的标量值。其概念是奖励来得越晚,它的价值就越低。这也反映了我们对奖励的看法;比如,我们宁愿现在收到$100,也不愿一年后再收到,这表明同样的奖励信号可以根据其接近现在的程度而具有不同的价值。
由于环境的机制对智能体来说并非完全可观察或已知,它必须通过执行行动并观察环境如何反应来获取信息。这与人类如何学习执行某些任务的方式非常相似。
假设我们正在学习下棋。虽然我们并未将所有可能的棋步记在脑中,或者完全知道对手会如何下棋,但我们能够随着时间的推移提高我们的技巧。特别是,我们能够在以下方面变得熟练:
-
学习如何应对对手做出的行动
-
评估我们在赢得游戏中的位置有多好
-
预测对手接下来会做什么,并利用该预测来决定行动
-
理解别人如何在类似情况下进行游戏
事实上,强化学习代理可以学习做类似的事情。特别是,一个代理可以由多个功能和模型组成,以辅助其决策。一个代理可以包含三个主要组件:策略、价值函数和模型。
策略
策略是一个算法或一组规则,用来描述一个代理如何做出决策。例如,投资者在股市中交易时使用的策略就是一种策略,其中投资者在股价下跌时购买股票,在股价上涨时卖出股票。
更正式地说,策略是一个函数,通常表示为https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/82dc222a-f23e-4460-ac00-c4ea31ac3cd5.png,它将一个状态https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/170b9e93-b340-45e9-988b-09c9e07e7fce.png映射到一个行动https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/db64ce78-6f6b-4bd8-913f-f1e4ec162373.png:
这意味着一个代理根据其当前状态来决定其行动。这个函数可以表示任何内容,只要它能够接收状态作为输入并输出一个行动,无论是表格、图表还是机器学习分类器。
例如,假设我们有一个代理需要导航一个迷宫。我们进一步假设代理知道迷宫的样子;以下是代理策略的表示方式:
图 1:一个迷宫,其中每个箭头表示代理下一个可能的行动方向
这个迷宫中的每个白色方块代表代理可能处于的状态。每个蓝色箭头指示代理在相应方块中会采取的行动。这本质上表示了代理在这个迷宫中的策略。此外,这也可以视为一个确定性策略,因为从状态到行动的映射是确定的。这与随机策略相对,后者会在给定某种状态时输出一个关于可能行动的概率分布:
这里,https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/fbac4100-413c-4698-b8dc-bee15f1dc709.png是所有可能行动的标准化概率向量,如以下示例所示:
图 2:一个将游戏状态(屏幕)映射到行动(概率)的策略
玩《Breakout》游戏的代理有一个策略,该策略以游戏屏幕为输入,并返回每个可能行动的概率。
价值函数
智能体的第二个组成部分称为值函数。如前所述,值函数有助于评估智能体在某一状态下的位置,无论是好是坏。在国际象棋中,玩家希望了解他们在当前棋盘状态下获胜的可能性。而在迷宫中,智能体则希望知道自己离目标有多近。值函数正是为此服务;它预测智能体在某一状态下将获得的预期未来奖励。换句话说,它衡量某一状态对智能体的吸引力。更正式地说,值函数将状态和策略作为输入,并返回一个标量值,表示预期的累计奖励:
以我们的迷宫示例为例,假设智能体每走一步就会收到-1 的惩罚奖励。智能体的目标是以尽可能少的步骤完成迷宫。每个状态的值可以如下表示:
图 3:一个迷宫,其中每个方格表示处于该状态时的值
每个方格基本上表示从当前位置到达迷宫终点所需的步数。如你所见,达到目标所需的最少步数为 15 步。
除了告诉我们某一状态的吸引力外,值函数还能如何帮助智能体更好地完成任务呢?正如我们将在接下来的章节中看到的,值函数在预测一系列动作是否能够成功之前起着至关重要的作用。这类似于国际象棋选手预想一系列未来动作如何有助于提高获胜的机会。为了做到这一点,智能体还需要了解环境是如何运作的。这时,智能体的第三个组成部分——模型——变得非常重要。
模型
在前面的章节中,我们讨论了环境对智能体的不可完全知晓。换句话说,智能体通常不知道环境内部算法的具体情况。因此,智能体需要与环境互动,以获取信息并学习如何最大化预期的累计奖励。然而,智能体可能会有一个环境的内部副本或模型。智能体可以使用该模型预测环境在给定状态下对某个动作的反应。例如,股市模型的任务是预测未来的价格。如果模型准确,智能体便可以利用其值函数评估未来状态的吸引力。更正式地说,模型可以表示为一个函数,https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/9cbc6ea0-aae1-46ea-b5f8-e0d8ac439aa0.png,它预测在当前状态和某个动作下,下一状态的概率:
在其他场景中,环境的模型可以用来列举可能的未来状态。这通常用于回合制游戏,如国际象棋和井字游戏,其中规则和可能动作的范围已明确规定。树状图常用于展示回合制游戏中可能的动作和状态序列:
图 4:一个使用其价值函数评估可能动作的模型
在前面的井字游戏示例中,https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/357a67fa-d45f-4755-90ce-03d347133091.png表示在给定状态下采取https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/e8560c16-362b-4411-b69f-50eddf987dba.png动作(表示为阴影圆圈)可能带来的结果,状态为https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/02d85b2c-a516-4199-8255-d805511504ea.png。此外,我们可以使用代理的价值函数计算每个状态的值。中间和底部的状态将产生较高的值,因为代理离胜利仅一步之遥,而顶部的状态将产生中等值,因为代理需要阻止对手获胜。
让我们回顾一下到目前为止涵盖的术语:
| 术语 | 描述 | 它输出什么? |
|---|---|---|
| 策略 | 算法或函数,用于输出代理做出的决策 | 输出单一决策的标量(确定性策略)或关于可能动作的概率向量(随机策略) |
| 价值函数 | 描述某个状态好坏的函数 | 表示期望累积奖励的标量值 |
| 模型 | 代理对环境的表示,预测环境如何对代理的行为作出反应 | 给定动作和当前状态下的下一个状态的概率,或根据环境规则列出可能的状态 |
在接下来的章节中,我们将利用这些概念来学习强化学习中最基础的框架之一:马尔可夫决策过程。
马尔可夫决策过程(MDP)
马尔可夫决策过程是一个用来表示强化学习问题环境的框架。它是一个有向图模型(意味着图中的一个节点指向另一个节点)。每个节点代表环境中的一个可能状态,每个从状态指向外部的边代表在给定状态下可以采取的动作。例如,考虑以下的 MDP:
图 5:一个示例马尔可夫决策过程
前面的 MDP 表示程序员典型一天的情景。每个圆圈代表程序员可能处于的某个状态,其中蓝色状态(醒来)是初始状态(即代理在 t=0 时的状态),而橙色状态(发布代码)表示终止状态。每个箭头表示程序员可以在状态之间进行的转换。每个状态都有与之相关的奖励,奖励越高,该状态越具吸引力。
我们也可以将奖励制成邻接矩阵:
| 状态\动作 | 醒来 | Netflix | 编写代码和调试 | 小睡 | 部署 | 睡觉 |
|---|---|---|---|---|---|---|
| 醒来 | N/A | -2 | -3 | N/A | N/A | N/A |
| Netflix | N/A | -2 | N/A | N/A | N/A | N/A |
| 编写代码和调试 | N/A | N/A | N/A | 1 | 10 | 3 |
| 小睡 | 0 | N/A | N/A | N/A | N/A | N/A |
| 部署 | N/A | N/A | N/A | N/A | N/A | 3 |
| 睡觉 | N/A | N/A | N/A | N/A | N/A | N/A |
左列表示可能的状态,顶行表示可能的动作。N/A 意味着在给定状态下无法执行该动作。该系统基本上表示程序员在一天中的决策过程。
当程序员醒来时,他们可以选择工作(编写和调试代码)或观看 Netflix。请注意,观看 Netflix 的奖励高于编写和调试代码的奖励。对于这个程序员来说,观看 Netflix 似乎是更有回报的活动,而编写和调试代码可能是一项苦差事(希望读者不会是这种情况!)。然而,这两种行为都会导致负奖励,尽管我们的目标是最大化累积奖励。如果程序员选择观看 Netflix,他们将陷入一个无休止的刷剧循环,这会不断降低奖励。相反,如果他们决定认真编写代码,更多有回报的状态将会向他们开放。我们来看看程序员可以采取的可能轨迹——即一系列动作:
-
醒来 | Netflix | Netflix | …
-
醒来 | 编写代码和调试 | 小睡 | 醒来 | 编写代码和调试 | 小睡 | …
-
醒来 | 编写代码和调试 | 睡觉
-
醒来 | 编写代码和调试 | 部署 | 睡觉
第一和第二条轨迹都代表了无限循环。让我们计算每条轨迹的累积奖励,假设我们设置了 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/2ce1a228-1215-4a7d-8ede-d76546086518.png:
很容易看出,尽管第一条和第二条轨迹没有达到终止状态,但它们永远不会返回正奖励。第四条轨迹带来了最高的奖励(成功部署代码是一个非常有回报的成就!)。
我们所计算的是四种策略的价值函数,程序员可以根据这些策略来规划他们的日常工作。回想一下,价值函数是指从给定状态出发并遵循某一策略的期望累计奖励。我们已经观察到四种可能的策略,并评估了每种策略如何导致不同的累计奖励;这个过程也叫做策略评估。此外,我们用于计算期望奖励的方程也被称为贝尔曼期望方程。贝尔曼方程是一组用于评估和改进策略与价值函数的方程,帮助强化学习智能体更好地学习。虽然本书并不深入介绍贝尔曼方程,但它们是构建强化学习理论理解的基础。我们鼓励读者深入研究这一主题。
虽然我们不会深入讲解贝尔曼方程,但我们强烈建议读者学习贝尔曼方程,以便建立强化学习的扎实理解。有关更多信息,请参见理查德·S·萨顿和安德鲁·巴托所著的《强化学习:导论》(本章末尾有参考文献)。
现在你已经了解了强化学习的一些关键术语和概念,或许你会想知道我们是如何教导强化学习智能体去最大化其奖励,换句话说,如何确定第四条轨迹是最优的。在本书中,你将通过解决许多任务和问题来实现这一目标,所有这些任务都将使用深度学习。虽然我们鼓励你熟悉深度学习的基础知识,但以下部分将作为该领域的轻度复习。
深度学习
深度学习已成为机器学习和计算机科学中最受欢迎且最具辨识度的领域之一。得益于可用数据和计算资源的增加,深度学习算法在无数任务中成功超越了以往的最先进成果。在多个领域,包括图像识别和围棋,深度学习甚至超越了人类的能力。
因此,许多强化学习算法开始利用深度学习来提高性能也就不足为奇了。本章开头提到的许多强化学习算法都依赖于深度学习。本书也将围绕深度学习算法展开,以解决强化学习问题。
以下部分将作为深度学习一些最基本概念的复习,包括神经网络、反向传播和卷积。然而,如果你不熟悉这些主题,我们强烈建议你寻求其他来源,获取更深入的介绍。
神经网络
神经网络是一种计算架构,由多层感知机组成。感知机由 Frank Rosenblatt 于 1950 年代首次构思,模拟生物神经元并计算输入向量的线性组合。它还使用非线性激活函数(如 sigmoid 函数)对线性组合进行转换并输出结果。假设一个感知机接收输入向量为https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/b39f4391-4708-416b-b9ec-d84b48dd6199.png。感知机的输出a如下:
其中https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/f5edf276-9cdb-4ffb-9a46-b75cf44e960a.png是感知机的权重,b是常数,称为偏置,而https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/4a6b153a-6786-4012-8b2a-af618dba2365.png是 sigmoid 激活函数,它输出一个介于 0 和 1 之间的值。
感知机被广泛用作决策的计算模型。假设任务是预测第二天晴天的可能性。每一个https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/0d974f85-0cd9-4f6a-9af3-185498a44dd6.png都代表一个变量,比如当天的温度、湿度或前一天的天气。然后,https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/5a2faf8a-f99b-4355-8570-70d24f7dfb7b.png将计算一个值,反映明天晴天的可能性。如果模型对于https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/d46a16f7-4915-42c1-8f20-1d63cdc4612b.png有一组好的值,它就能做出准确的决策。
在典型的神经网络中,有多层神经元,每一层的每个神经元都与前一层和后一层的所有神经元相连接。因此,这些层也被称为全连接层。给定层的权重,l,可以表示为矩阵W^l:
其中,每个w[ij]表示前一层的第i个神经元与当前层的第j个神经元之间的权重。Bl*表示偏置向量,每个神经元都有一个偏置。于是,给定层*l*的激活值*al可以定义如下:
其中*a⁰(x)*只是输入。具有多层神经元的神经网络被称为多层感知机(MLP)。一个 MLP 有三个组件:输入层、隐藏层和输出层。数据从输入层开始,通过隐藏层中的一系列线性和非线性函数进行转换,然后从输出层输出为决策或预测。因此,这种架构也被称为前馈网络。以下图示展示了一个完全连接的网络的样子:
图 6:多层感知机的示意图
反向传播
如前所述,神经网络的性能取决于 W 的值有多好(为简便起见,我们将权重和偏差统称为 W)。当整个网络的规模增长时,手动为每一层的每个神经元确定最优权重变得不可行。因此,我们依赖于反向传播算法,它迭代且自动地更新每个神经元的权重。
为了更新权重,我们首先需要地面真实值,或者说神经网络尝试输出的目标值。为了理解这个地面真实值是什么样子,我们设定了一个样本问题。MNIST 数据集是一个包含 28x28 手写数字图像的大型库。它总共有 70,000 张图像,并且是机器学习模型的一个常见基准。给定十个不同的数字类别(从 0 到 9),我们希望识别给定图像属于哪个数字类别。我们可以将每张图像的地面真实值表示为一个长度为 10 的向量,其中类别的索引(从 0 开始)被标记为 1,其余为 0。例如,图像 x 的类别标签为 5,则其地面真实值为 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/82513266-77d8-4584-9f41-3c233a88f953.png,其中 y 是我们要逼近的目标函数。
神经网络应该是什么样的?如果我们将图像中的每个像素看作输入,我们将会在输入层中拥有 28x28 个神经元(每张图像将被展平,变为一个 784 维的向量)。此外,由于有 10 个数字类别,我们在输出层中有 10 个神经元,每个神经元为给定类别产生一个 sigmoid 激活函数。隐藏层的神经元数量可以是任意的。
让 f 代表神经网络计算的转换序列,参数化为权重 W。f 本质上是目标函数 y 的近似,并将 784 维的输入向量映射到 10 维的输出预测。我们根据最大 sigmoid 输出的索引来对图像进行分类。
现在我们已经制定了地面真实值,我们可以衡量它与网络预测之间的距离。这个误差使得网络能够更新它的权重。我们将误差函数 E(W) 定义如下:
反向传播的目标是通过找到合适的 W 来最小化 E。这个最小化是一个优化问题,我们使用梯度下降法迭代地计算 E 相对于 W 的梯度,并从输出层开始将其传播通过网络。
不幸的是,反向传播的深入解释超出了本章节的范围。如果你对这个概念不熟悉,我们强烈建议你首先学习它。
卷积神经网络
通过反向传播,我们现在能够自动训练大型网络。这导致了越来越复杂的神经网络架构的出现。一个例子是卷积神经网络(CNN)。CNN 中主要有三种类型的层:卷积层、池化层和全连接层。全连接层与之前讨论的标准神经网络相同。在卷积层中,权重是卷积核的一部分。对二维图像像素数组的卷积定义如下:
其中,*f(u, v)是输入在坐标(u, v)*处的像素强度,*g(x-u, y-v)*是该位置卷积核的权重。
卷积层由一组卷积核组成;因此,卷积层的权重可以视为一个三维的盒子,而不是我们为全连接层定义的二维数组。应用于输入的单个卷积核的输出也是二维映射,我们称之为滤波器。由于有多个卷积核,卷积层的输出再次是一个三维的盒子,可以称之为体积。
最后,池化层通过对mm局部像素块进行操作,并输出一个标量,来减小输入的大小。最大池化层对mm像素块进行操作,并输出该像素块中的最大值。
给定一个形状为(32, 32, 3)的输入体积——对应于高度、宽度和深度(通道)——一个池化大小为 2x2 的最大池化层将输出形状为(16, 16, 3)的体积。CNN 的输入通常是图像,也可以看作是深度对应于 RGB 通道的体积。
以下是典型卷积神经网络的示意图:
图 7:一个示例卷积神经网络
神经网络的优势
CNN 相比标准神经网络的主要优势在于,前者能够学习输入的视觉和空间特征,而后者由于将输入数据展平为向量,丢失了这类信息。CNN 在计算机视觉领域取得了重大进展,最早体现在对MNIST数据的分类准确率提升,以及物体识别、语义分割等领域。CNN 在现实生活中有广泛的应用,从社交媒体中的人脸检测到自动驾驶汽车。最近的研究还将 CNN 应用于自然语言处理和文本分类任务,取得了最先进的成果。
现在我们已经介绍了机器学习的基础知识,接下来我们将进行第一次实现练习。
在 TensorFlow 中实现卷积神经网络
在本节中,我们将使用 TensorFlow 实现一个简单的卷积神经网络,以解决图像分类任务。由于本书其余部分将大量依赖于 TensorFlow 和 CNNs,我们强烈建议你熟悉如何使用该框架实现深度学习算法。
TensorFlow
TensorFlow 是由 Google 于 2015 年开发的,是世界上最流行的深度学习框架之一。它被广泛应用于研究和商业项目,并拥有丰富的 API 和功能,帮助研究人员和从业人员开发深度学习模型。TensorFlow 程序可以在 GPU 和 CPU 上运行,从而将 GPU 编程进行了抽象化,使开发变得更加便捷。
本书中,我们将专门使用 TensorFlow,因此在继续阅读本书的章节时,确保你熟悉基本知识。
访问 www.tensorflow.org/ 获取完整的文档和其他教程。
Fashion-MNIST 数据集
有深度学习经验的人大多听说过 MNIST 数据集。它是最广泛使用的图像数据集之一,作为图像分类和图像生成等任务的基准,并且被许多计算机视觉模型使用:
图 8:MNIST 数据集(本章末尾有参考文献)
然而,MNIST 数据集存在一些问题。首先,数据集过于简单,因为一个简单的卷积神经网络就能实现 99% 的测试准确率。尽管如此,该数据集在研究和基准测试中仍被过度使用。由在线时尚零售商 Zalando 提供的 F-MNIST 数据集,是对 MNIST 数据集的一个更复杂、更具挑战性的升级版本:
图 9:Fashion-MNIST 数据集(来源于 github.com/zalandoresearch/fashion-mnist,本章末尾有参考文献)
与数字不同,F-MNIST 数据集包含了十种不同类型的服装照片(从 T 恤到鞋子不等),这些照片被压缩为 28x28 的单色缩略图。因此,F-MNIST 作为 MNIST 的一个方便的替代品,越来越受到社区的欢迎。因此,我们也将在 F-MNIST 上训练我们的 CNN。前面的表格将每个标签索引映射到对应的类别:
| 索引 | 类别 |
|---|---|
| 0 | T 恤/上衣 |
| 1 | 长裤 |
| 2 | 套头衫 |
| 3 | 连衣裙 |
| 4 | 外套 |
| 5 | 凉鞋 |
| 6 | 衬衫 |
| 7 | 运动鞋 |
| 8 | 包 |
| 9 | 高帮靴 |
在接下来的子章节中,我们将设计一个卷积神经网络,它将学习如何对来自该数据集的数据进行分类。
构建网络
多个深度学习框架已经实现了加载F-MNIST数据集的 API,包括 TensorFlow。在我们的实现中,我们将使用 Keras,这是另一个与 TensorFlow 集成的流行深度学习框架。Keras 的数据集模块提供了一个非常方便的接口,将数据集加载为numpy数组。
最后,我们可以开始编写代码了!对于本次练习,我们只需要一个 Python 模块,我们将其命名为cnn.py。打开你喜欢的文本编辑器或 IDE,开始吧。
我们的第一步是声明我们将使用的模块:
import logging
import os
import sys
logger = logging.getLogger(__name__)
import tensorflow as tf
import numpy as np
from keras.datasets import fashion_mnist
from keras.utils import np_utils
以下描述了每个模块的用途以及我们将如何使用它:
| 模块 | 用途 |
|---|---|
logging | 用于在运行代码时打印统计信息 |
os, sys | 用于与操作系统交互,包括写文件 |
tensorflow | 主要的 TensorFlow 库 |
numpy | 一个优化过的向量计算和简单数据处理库 |
keras | 用于下载 F-MNIST 数据集 |
我们将实现一个名为SimpleCNN的类。__init__构造函数接受多个参数:
class SimpleCNN(object):
def __init__(self, learning_rate, num_epochs, beta, batch_size):
self.learning_rate = learning_rate
self.num_epochs = num_epochs
self.beta = beta
self.batch_size = batch_size
self.save_dir = "saves"
self.logs_dir = "logs"
os.makedirs(self.save_dir, exist_ok=True)
os.makedirs(self.logs_dir, exist_ok=True)
self.save_path = os.path.join(self.save_dir, "simple_cnn")
self.logs_path = os.path.join(self.logs_dir, "simple_cnn")
我们的SimpleCNN初始化时的参数在此进行描述:
| 参数 | 用途 |
|---|---|
learning_rate | 优化算法的学习率 |
num_epochs | 训练网络所需的 epoch 次数 |
beta | 一个浮动值(介于 0 和 1 之间),控制 L2 惩罚的强度 |
batch_size | 每次训练中处理的图像数量 |
此外,save_dir和save_path指代的是我们将存储网络参数的位置,logs_dir和logs_path则指代存储训练过程统计信息的位置(稍后我们会展示如何获取这些日志)。
构建网络的方法
在这一部分,我们将看到两种可以用于构建该功能的方法,它们分别是:
-
构建方法
-
拟合方法
构建方法
我们为SimpleCNN类定义的第一个方法是build方法,它负责构建 CNN 的架构。我们的build方法接受两个输入:输入张量和它应该预期的类别数:
def build(self, input_tensor, num_classes):
"""
Builds a convolutional neural network according to the input shape and the number of classes.
Architecture is fixed.
Args:
input_tensor: Tensor of the input
num_classes: (int) number of classes
Returns:
The output logits before softmax
"""
我们首先初始化tf.placeholder,命名为is_training。TensorFlow 的占位符类似于没有值的变量,只有在实际训练网络并调用相关操作时,我们才会给它赋值:
with tf.name_scope("input_placeholders"):
self.is_training = tf.placeholder_with_default(True, shape=(), name="is_training")
tf.name_scope(...)块允许我们为操作和张量命名。虽然这不是绝对必要的,但它有助于更好地组织代码,也有助于我们可视化网络。在这里,我们定义了一个名为is_training的tf.placeholder_with_default,其默认值为True。这个占位符将用于我们的 dropout 操作(因为 dropout 在训练和推理阶段有不同的模式)。
为操作和张量命名被认为是一种好习惯,它有助于你更好地组织代码。
下一步是定义我们 CNN 的卷积层。我们使用三种不同类型的层来创建多个卷积层:tf.layers.conv2d、tf.max_pooling2d和tf.layers.dropout:
with tf.name_scope("convolutional_layers"):
conv_1 = tf.layers.conv2d(
input_tensor,
filters=16,
kernel_size=(5, 5),
strides=(1, 1),
padding="SAME",
activation=tf.nn.relu,
kernel_regularizer=tf.contrib.layers.l2_regularizer(scale=self.beta),
name="conv_1")
conv_2 = tf.layers.conv2d(
conv_1,
filters=32,
kernel_size=(3, 3),
strides=(1, 1),
padding="SAME",
activation=tf.nn.relu,
kernel_regularizer=tf.contrib.layers.l2_regularizer(scale=self.beta),
name="conv_2")
pool_3 = tf.layers.max_pooling2d(
conv_2,
pool_size=(2, 2),
strides=1,
padding="SAME",
name="pool_3"
)
drop_4 = tf.layers.dropout(pool_3, training=self.is_training, name="drop_4")
conv_5 = tf.layers.conv2d(
drop_4,
filters=64,
kernel_size=(3, 3),
strides=(1, 1),
padding="SAME",
activation=tf.nn.relu,
kernel_regularizer=tf.contrib.layers.l2_regularizer(scale=self.beta),
name="conv_5")
conv_6 = tf.layers.conv2d(
conv_5,
filters=128,
kernel_size=(3, 3),
strides=(1, 1),
padding="SAME",
activation=tf.nn.relu,
kernel_regularizer=tf.contrib.layers.l2_regularizer(scale=self.beta),
name="conv_6")
pool_7 = tf.layers.max_pooling2d(
conv_6,
pool_size=(2, 2),
strides=1,
padding="SAME",
name="pool_7"
)
drop_8 = tf.layers.dropout(pool_7, training=self.is_training, name="drop_8")
下面是一些参数的解释:
| 参数 | 类型 | 描述 |
|---|---|---|
filters | int | 卷积层输出的过滤器数量。 |
kernel_size | int元组 | 卷积核的形状。 |
pool_size | int元组 | 最大池化窗口的形状。 |
strides | int | 每次卷积/最大池化操作时,滑动的像素数量。 |
padding | str | 是否添加填充(SAME)或者不添加(VALID)。如果添加填充,卷积的输出形状将与输入形状保持一致。 |
activation | func | 一个 TensorFlow 激活函数。 |
kernel_regularizer | op | 使用哪种正则化方法来约束卷积核。默认值是None。 |
training | op | 一个张量/占位符,用于告诉 dropout 操作当前是用于训练还是推理。 |
在上面的表格中,我们指定了卷积架构的层次顺序如下:
CONV | CONV | POOL | DROPOUT | CONV | CONV | POOL | DROPOUT
然而,我们鼓励您探索不同的配置和架构。例如,您可以添加批归一化层,以提高训练的稳定性。
最后,我们添加全连接层,将网络的输出生成最终结果:
with tf.name_scope("fully_connected_layers"):
flattened = tf.layers.flatten(drop_8, name="flatten")
fc_9 = tf.layers.dense(
flattened,
units=1024,
activation=tf.nn.relu,
kernel_regularizer=tf.contrib.layers.l2_regularizer(scale=self.beta),
name="fc_9"
)
drop_10 = tf.layers.dropout(fc_9, training=self.is_training, name="drop_10")
logits = tf.layers.dense(
drop_10,
units=num_classes,
kernel_regularizer=tf.contrib.layers.l2_regularizer(scale=self.beta),
name="logits"
)
return logits
tf.layers.flatten将卷积层的输出(即 3 维)转化为一个单一的向量(1 维),这样我们就可以将它们传入tf.layers.dense层。经过两个全连接层后,我们返回最终的输出,这个输出我们定义为logits。
请注意,在最终的tf.layers.dense层中,我们没有指定activation。当我们开始指定网络的训练操作时,您会明白为什么这么做。
接下来,我们实现几个辅助函数。_create_tf_dataset接受两个numpy.ndarray实例,并将它们转换为 TensorFlow 张量,可以直接传入网络。_log_loss_and_acc则简单地记录训练统计数据,如损失和准确率:
def _create_tf_dataset(self, x, y):
dataset = tf.data.Dataset.zip((
tf.data.Dataset.from_tensor_slices(x),
tf.data.Dataset.from_tensor_slices(y)
)).shuffle(50).repeat().batch(self.batch_size)
return dataset
def _log_loss_and_acc(self, epoch, loss, acc, suffix):
summary = tf.Summary(value=[
tf.Summary.Value(tag="loss_{}".format(suffix), simple_value=float(loss)),
tf.Summary.Value(tag="acc_{}".format(suffix), simple_value=float(acc))
])
self.summary_writer.add_summary(summary, epoch)
fit 方法
我们为SimpleCNN实现的最后一个方法是fit方法。这个函数触发我们的 CNN 训练。我们的fit方法有四个输入:
| 参数 | 描述 |
|---|---|
X_train | 训练数据 |
y_train | 训练标签 |
X_test | 测试数据 |
y_test | 测试标签 |
fit的第一步是初始化tf.Graph和tf.Session。这两个对象是任何 TensorFlow 程序的核心。tf.Graph表示定义所有 CNN 操作的图。你可以把它看作是一个沙箱,我们在其中定义所有层和函数。tf.Session是实际执行tf.Graph中定义操作的类:
def fit(self, X_train, y_train, X_valid, y_valid):
"""
Trains a CNN on given data
Args:
numpy.ndarrays representing data and labels respectively
"""
graph = tf.Graph()
with graph.as_default():
sess = tf.Session()
然后,我们使用 TensorFlow 的 Dataset API 和之前定义的_create_tf_dataset方法来创建数据集:
train_dataset = self._create_tf_dataset(X_train, y_train)
valid_dataset = self._create_tf_dataset(X_valid, y_valid)
# Creating a generic iterator
iterator = tf.data.Iterator.from_structure(train_dataset.output_types,
train_dataset.output_shapes)
next_tensor_batch = iterator.get_next()
# Separate training and validation set init ops
train_init_ops = iterator.make_initializer(train_dataset)
valid_init_ops = iterator.make_initializer(valid_dataset)
input_tensor, labels = next_tensor_batch
tf.data.Iterator构建一个迭代器对象,每次我们调用iterator.get_next()时,它都会输出一批图像。我们为训练数据和测试数据分别初始化数据集。iterator.get_next()的结果是输入图像和相应标签的元组。
前者是input_tensor,我们将其输入到build方法中。后者用于计算损失函数和反向传播:
num_classes = y_train.shape[1]
# Building the network
logits = self.build(input_tensor=input_tensor, num_classes=num_classes)
logger.info('Built network')
prediction = tf.nn.softmax(logits, name="predictions")
loss_ops = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(
labels=labels, logits=logits), name="loss")
logits(网络的非激活输出)输入到两个其他操作中:prediction,它只是对logits进行 softmax 处理,以获得类的归一化概率,以及loss_ops,它计算预测和标签之间的平均类别交叉熵。
然后我们定义了用于训练网络的反向传播算法和用于计算准确度的操作:
optimizer = tf.train.AdamOptimizer(learning_rate=self.learning_rate)
train_ops = optimizer.minimize(loss_ops)
correct = tf.equal(tf.argmax(prediction, 1), tf.argmax(labels, 1), name="correct")
accuracy_ops = tf.reduce_mean(tf.cast(correct, tf.float32), name="accuracy")
现在我们已经完成了网络和其优化算法的构建。我们使用tf.global_variables_initializer()来初始化网络的权重和操作。我们还初始化了tf.train.Saver和tf.summary.FileWriter对象。tf.train.Saver对象用于保存网络的权重和架构,而后者则跟踪各种训练统计数据:
initializer = tf.global_variables_initializer()
logger.info('Initializing all variables')
sess.run(initializer)
logger.info('Initialized all variables')
sess.run(train_init_ops)
logger.info('Initialized dataset iterator')
self.saver = tf.train.Saver()
self.summary_writer = tf.summary.FileWriter(self.logs_path)
最后,一旦我们设置好所需的一切,就可以实现实际的训练循环。每个 epoch,我们跟踪训练的交叉熵损失和网络的准确度。在每个 epoch 结束时,我们将更新后的权重保存到磁盘。我们还会每 10 个 epoch 计算一次验证损失和准确度。这是通过调用sess.run(...)来完成的,其中该函数的参数是sess对象应该执行的操作:
logger.info("Training CNN for {} epochs".format(self.num_epochs))
for epoch_idx in range(1, self.num_epochs+1):
loss, _, accuracy = sess.run([
loss_ops, train_ops, accuracy_ops
])
self._log_loss_and_acc(epoch_idx, loss, accuracy, "train")
if epoch_idx % 10 == 0:
sess.run(valid_init_ops)
valid_loss, valid_accuracy = sess.run([
loss_ops, accuracy_ops
], feed_dict={self.is_training: False})
logger.info("=====================> Epoch {}".format(epoch_idx))
logger.info("\tTraining accuracy: {:.3f}".format(accuracy))
logger.info("\tTraining loss: {:.6f}".format(loss))
logger.info("\tValidation accuracy: {:.3f}".format(valid_accuracy))
logger.info("\tValidation loss: {:.6f}".format(valid_loss))
self._log_loss_and_acc(epoch_idx, valid_loss, valid_accuracy, "valid")
# Creating a checkpoint at every epoch
self.saver.save(sess, self.save_path)
到这里,我们完成了fit函数。我们的最后一步是创建脚本,实例化数据集、神经网络,并运行训练,这将在cnn.py的底部编写:
我们首先配置日志记录器,并使用 Keras 的fashion_mnist模块加载数据集,该模块加载训练数据和测试数据:
if __name__ == "__main__":
logging.basicConfig(stream=sys.stdout,
level=logging.DEBUG,
format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
logger = logging.getLogger(__name__)
logger.info("Loading Fashion MNIST data")
(X_train, y_train), (X_test, y_test) = fashion_mnist.load_data()
然后我们对数据进行一些简单的预处理。Keras API 返回形状为(Number of images, 28, 28)的numpy数组。
然而,我们实际上需要的是(Number of images, 28, 28, 1),其中第三个轴是通道轴。这是必须的,因为我们的卷积层期望输入具有三个轴。此外,像素值本身的范围是[0, 255]。我们将它们除以 255,以获得[0, 1]的范围。这是一种常见的技术,有助于稳定训练。
此外,我们将标签(它只是标签索引的数组)转换为 one-hot 编码:
logger.info('Shape of training data:')
logger.info('Train: {}'.format(X_train.shape))
logger.info('Test: {}'.format(X_test.shape))
logger.info('Adding channel axis to the data')
X_train = X_train[:,:,:,np.newaxis]
X_test = X_test[:,:,:,np.newaxis]
logger.info("Simple transformation by dividing pixels by 255")
X_train = X_train / 255.
X_test = X_test / 255.
X_train = X_train.astype(np.float32)
X_test = X_test.astype(np.float32)
y_train = y_train.astype(np.float32)
y_test = y_test.astype(np.float32)
num_classes = len(np.unique(y_train))
logger.info("Turning ys into one-hot encodings")
y_train = np_utils.to_categorical(y_train, num_classes=num_classes)
y_test = np_utils.to_categorical(y_test, num_classes=num_classes)
然后,我们定义了SimpleCNN构造函数的输入。可以自由调整这些数字,看看它们如何影响模型的性能:
cnn_params = {
"learning_rate": 3e-4,
"num_epochs": 100,
"beta": 1e-3,
"batch_size": 32
}
最后,我们实例化SimpleCNN并调用其fit方法:
logger.info('Initializing CNN')
simple_cnn = SimpleCNN(**cnn_params)
logger.info('Training CNN')
simple_cnn.fit(X_train=X_train,
X_valid=X_test,
y_train=y_train,
y_valid=y_test)
要运行整个脚本,你只需运行模块:
$ python cnn.py
就是这样!你已经成功地在 TensorFlow 中实现了一个卷积神经网络,并在 F-MNIST 数据集上进行训练。要跟踪训练进度,你只需查看终端/编辑器中的输出。你应该看到类似以下的输出:
$ python cnn.py
Using TensorFlow backend.
2018-07-29 21:21:55,423 __main__ INFO Loading Fashion MNIST data
2018-07-29 21:21:55,686 __main__ INFO Shape of training data:
2018-07-29 21:21:55,687 __main__ INFO Train: (60000, 28, 28)
2018-07-29 21:21:55,687 __main__ INFO Test: (10000, 28, 28)
2018-07-29 21:21:55,687 __main__ INFO Adding channel axis to the data
2018-07-29 21:21:55,687 __main__ INFO Simple transformation by dividing pixels by 255
2018-07-29 21:21:55,914 __main__ INFO Turning ys into one-hot encodings
2018-07-29 21:21:55,914 __main__ INFO Initializing CNN
2018-07-29 21:21:55,914 __main__ INFO Training CNN
2018-07-29 21:21:58,365 __main__ INFO Built network
2018-07-29 21:21:58,562 __main__ INFO Initializing all variables
2018-07-29 21:21:59,284 __main__ INFO Initialized all variables
2018-07-29 21:21:59,639 __main__ INFO Initialized dataset iterator
2018-07-29 21:22:00,880 __main__ INFO Training CNN for 100 epochs
2018-07-29 21:24:23,781 __main__ INFO =====================> Epoch 10
2018-07-29 21:24:23,781 __main__ INFO Training accuracy: 0.406
2018-07-29 21:24:23,781 __main__ INFO Training loss: 1.972021
2018-07-29 21:24:23,781 __main__ INFO Validation accuracy: 0.500
2018-07-29 21:24:23,782 __main__ INFO Validation loss: 2.108872
2018-07-29 21:27:09,541 __main__ INFO =====================> Epoch 20
2018-07-29 21:27:09,541 __main__ INFO Training accuracy: 0.469
2018-07-29 21:27:09,541 __main__ INFO Training loss: 1.573592
2018-07-29 21:27:09,542 __main__ INFO Validation accuracy: 0.500
2018-07-29 21:27:09,542 __main__ INFO Validation loss: 1.482948
2018-07-29 21:29:57,750 __main__ INFO =====================> Epoch 30
2018-07-29 21:29:57,750 __main__ INFO Training accuracy: 0.531
2018-07-29 21:29:57,750 __main__ INFO Training loss: 1.119335
2018-07-29 21:29:57,750 __main__ INFO Validation accuracy: 0.625
2018-07-29 21:29:57,750 __main__ INFO Validation loss: 0.905031
2018-07-29 21:32:45,921 __main__ INFO =====================> Epoch 40
2018-07-29 21:32:45,922 __main__ INFO Training accuracy: 0.656
2018-07-29 21:32:45,922 __main__ INFO Training loss: 0.896715
2018-07-29 21:32:45,922 __main__ INFO Validation accuracy: 0.719
2018-07-29 21:32:45,922 __main__ INFO Validation loss: 0.847015
另一个需要查看的工具是 TensorBoard,这是由 TensorFlow 开发者开发的可视化工具,用于绘制模型的准确度和损失图。我们使用的 tf.summary.FileWriter 对象用于此目的。你可以通过以下命令运行 TensorBoard:
$ tensorboard --logdir=logs/
logs 是我们的 SimpleCNN 模型写入统计数据的地方。TensorBoard 是一个很棒的工具,用于可视化我们的 tf.Graph 结构,并观察准确度和损失等统计数据随时间的变化。默认情况下,TensorBoard 日志可以通过将浏览器指向 localhost:6006 进行访问:
图 10:TensorBoard 及其对我们 CNN 的可视化
恭喜!我们已经成功使用 TensorFlow 实现了一个卷积神经网络。然而,我们实现的 CNN 相当基础,且只实现了中等的准确度——挑战在于,读者可以调整架构以提高其性能。
总结
在本章中,我们迈出了强化学习世界的第一步。我们介绍了一些该领域的基本概念和术语,包括智能体、策略、价值函数和奖励。同时,我们也涵盖了深度学习的基本主题,并使用 TensorFlow 实现了一个简单的卷积神经网络。
强化学习的领域非常广阔且不断扩展;在一本书中无法涵盖所有内容。然而,我们希望能为你提供必要的实践技能和经验,以便在这个领域中导航。
接下来的章节将包含独立的项目——我们将使用强化学习和深度学习算法的结合来解决多个任务和问题。我们将构建智能体,让它们学习下围棋、探索 Minecraft 世界并玩 Atari 电子游戏。我们希望你已经准备好踏上这段激动人心的学习旅程!
参考文献
Sutton, Richard S., 和 Andrew G. Barto. 强化学习:导论. MIT 出版社,1998 年。
Y. LeCun, L. Bottou, Y. Bengio, 和 P. Haffner*. 基于梯度的学习应用于文档识别. 《IEEE 会议录》,86(11):2278-2324,1998 年 11 月。*
Xiao, Han, Kashif Rasul, 和 Roland Vollgraf. Fashion-mnist:一种用于基准测试机器学习算法的新型图像数据集. arXiv 预印本 arXiv:1708.07747 (2017)。
第二章:平衡 CartPole
在本章中,您将学习关于 CartPole 平衡问题的内容。CartPole 是一个倒立摆,杆子与重力平衡。传统上,这个问题通过控制理论和分析方程解决。然而,在本章中,我们将通过机器学习来解决这个问题。
本章将涵盖以下主题:
-
安装 OpenAI Gym
-
Gym 的不同环境
OpenAI Gym
OpenAI 是一个致力于人工智能研究的非盈利组织。访问 openai.com 了解更多关于 OpenAI 使命的信息。OpenAI 开发的技术对任何人都免费使用。
Gym
Gym 提供了一个基准工具包,用于评估基于人工智能的任务。该接口易于使用,目标是实现可重复的研究。访问gym.openai.com了解更多关于 Gym 的信息。一个智能体可以在gym中学习,并掌握诸如玩游戏或走路等活动。环境是一个问题库。
Gym 提供的标准问题集如下:
-
CartPole
-
摆锤
-
Space Invaders
-
Lunar Lander
-
蚂蚁
-
Mountain Car
-
Acrobot
-
赛车
-
双足行走者
任何算法都可以在 Gym 中通过训练这些活动来运行。所有问题都具有相同的接口。因此,任何通用强化学习算法都可以通过该接口使用。
安装
Gym 的主要接口通过 Python 使用。一旦你在一个带有 pip 安装器的环境中拥有 Python3,就可以通过以下方式安装 Gym:
sudo pip install gym
高级用户如果想要修改源代码,可以通过以下命令从源代码编译:
git clone https://github.com/openai/gym
cd gym
pip install -e .
可以通过源代码将一个新环境添加到 gym 中。有些环境需要更多的依赖项。对于 macOS,使用以下命令安装依赖项:
brew install cmake boost boost-python sdl2 swig wget
对于 Ubuntu,使用以下命令:
apt-get install -y python-numpy python-dev cmake zlib1g-dev libjpeg-dev xvfb libav-tools xorg-dev python-opengl libboost-all-dev libsdl2-dev swig
一旦依赖项准备好,按照以下方式安装完整的 gym:
pip install 'gym[all]'
这将安装大多数所需的环境。
运行一个环境
任何 Gym 环境都可以通过简单的接口初始化并运行。让我们从导入 gym 库开始,如下所示:
- 首先我们导入
gym库:
import gym
- 接下来,通过传递参数给
gym.make创建一个环境。以下代码以 CartPole 为例:
environment = gym.make('CartPole-v0')
- 接下来,重置环境:
environment.reset()
- 然后,开始迭代并渲染环境,如下所示:
for dummy in range(100):
environment.render()
environment.step(environment.action_space.sample())
此外,在每一步都改变动作空间,以观察 CartPole 的移动。运行前面的程序应该会生成一个可视化界面。场景应以以下的可视化效果开始:
上面的图像称为 CartPole。CartPole 由一个可以水平移动的车厢和一个相对于车厢中心可以旋转的杆组成。
杆子被固定在小车上。经过一段时间,您会发现杆子开始向一侧倾斜,如下图所示:
在经过几次迭代后,杆子会摆回,如下图所示。所有的动作都受到物理定律的约束。步骤是随机进行的:
其他环境可以通过类似的方式显示,只需替换 Gym 环境的参数,如MsPacman-v0或MountainCar-v0。对于其他环境,可能需要其他许可证。接下来,我们将介绍其余的环境。
Atari
要玩 Atari 游戏,可以调用任何环境。以下代码表示游戏《太空侵略者》:
environment = gym.make('SpaceInvaders-v0')
执行前面的命令后,您将看到以下屏幕:
可以在这个环境中玩 Atari 游戏。
算法任务
有一些算法任务可以通过强化学习来学习。可以调用一个副本环境,如下所示:
environment = gym.make('Copy-v0')
复制字符串的过程在下图中显示:
MuJoCo
MuJoCo 代表多关节动力学与接触。它是一个用于机器人和多体动力学的仿真环境:
environment = gym.make('Humanoid-v2')
以下是人形机器人的仿真可视化:
人形机器人仿真
在这个环境中可以模拟机器人和其他物体。
机器人技术
也可以创建一个机器人环境,如下所示:
environment = gym.make('HandManipulateBlock-v0')
以下是机器人手部的可视化:
OpenAI Gym 可以用于多个环境。
马尔可夫模型
问题被设定为强化学习问题,采用试错法。环境通过state_values state_values (?)来描述,state_values会受到动作的影响。动作由一个算法确定,基于当前的state_value,以实现一个特定的state_value,这被称为马尔可夫模型。在理想情况下,过去的state_values确实会影响未来的state_values,但在这里,我们假设当前的state_value已经包含了所有以前的state_values。state_values有两种类型;一种是可观察的,另一种是不可观察的。模型也必须考虑不可观察的state_values。这被称为隐马尔可夫模型。
CartPole
在每个小车和杆子的步骤中,可以观察到多个变量,如位置、速度、角度和角速度。小车的state_values可以向左右移动:
-
state_values:四维连续值。 -
Actions:两个离散值。 -
维度或空间,可以称之为
state_value空间和动作空间。我们首先导入所需的库,如下所示:
import gym
import numpy as np
import random
import math
- 接下来,创建用于玩 CartPole 的环境,如下所示:
environment = gym.make('CartPole-v0')
- 接下来,定义桶的数量和动作的数量,如下所示:
no_buckets = (1, 1, 6, 3)
no_actions = environment.action_space.n
- 接下来,定义
state_value_bounds,如下所示:
state_value_bounds = list(zip(environment.observation_space.low, environment.observation_space.high))
state_value_bounds[1] = [-0.5, 0.5]
state_value_bounds[3] = [-math.radians(50), math.radians(50)]
- 接下来,定义
action_index,如下所示:
action_index = len(no_buckets)
- 接下来,定义
q_value_table,如下所示:
q_value_table = np.zeros(no_buckets + (no_actions,))
- 接下来,定义最小探索率和最小学习率,如下所示:
min_explore_rate = 0.01
min_learning_rate = 0.1
- 接下来,定义最大回合数、最大时间步数、解决到达的连续次数、解决时间、折扣因子和连续次数作为常量:
max_episodes = 1000
max_time_steps = 250
streak_to_end = 120
solved_time = 199
discount = 0.99
no_streaks = 0
- 接下来,定义
select动作来决定行动,如下所示:
def select_action(state_value, explore_rate):
if random.random() < explore_rate:
action = environment.action_space.sample()
else:
action = np.argmax(q_value_table[state_value])
return action
- 接下来,选择探索状态,如下所示:
def select_explore_rate(x):
return max(min_explore_rate, min(1, 1.0 - math.log10((x+1)/25)))
- 接下来,选择学习率,如下所示:
def select_learning_rate(x):
return max(min_learning_rate, min(0.5, 1.0 - math.log10((x+1)/25)))
- 接下来,
bucketizestate_value,如下所示:
def bucketize_state_value(state_value):
bucket_indexes = []
for i in range(len(state_value)):
if state_value[i] <= state_value_bounds[i][0]:
bucket_index = 0
elif state_value[i] >= state_value_bounds[i][1]:
bucket_index = no_buckets[i] - 1
else:
bound_width = state_value_bounds[i][1] - state_value_bounds[i][0]
offset = (no_buckets[i]-1)*state_value_bounds[i][0]/bound_width
scaling = (no_buckets[i]-1)/bound_width
bucket_index = int(round(scaling*state_value[i] - offset))
bucket_indexes.append(bucket_index)
return tuple(bucket_indexes)
- 接下来,训练各个回合,如下所示:
for episode_no in range(max_episodes):
explore_rate = select_explore_rate(episode_no)
learning_rate = select_learning_rate(episode_no)
observation = environment.reset()
start_state_value = bucketize_state_value(observation)
previous_state_value = start_state_value
for time_step in range(max_time_steps):
environment.render()
selected_action = select_action(previous_state_value, explore_rate)
observation, reward_gain, completed, _ = environment.step(selected_action)
state_value = bucketize_state_value(observation)
best_q_value = np.amax(q_value_table[state_value])
q_value_table[previous_state_value + (selected_action,)] += learning_rate * (
reward_gain + discount * (best_q_value) - q_value_table[previous_state_value + (selected_action,)])
- 接下来,打印所有相关的训练过程指标,如下所示:
print('Episode number : %d' % episode_no)
print('Time step : %d' % time_step)
print('Selection action : %d' % selected_action)
print('Current state : %s' % str(state_value))
print('Reward obtained : %f' % reward_gain)
print('Best Q value : %f' % best_q_value)
print('Learning rate : %f' % learning_rate)
print('Explore rate : %f' % explore_rate)
print('Streak number : %d' % no_streaks)
if completed:
print('Episode %d finished after %f time steps' % (episode_no, time_step))
if time_step >= solved_time:
no_streaks += 1
else:
no_streaks = 0
break
previous_state_value = state_value
if no_streaks > streak_to_end:
break
- 经过一段时间的训练后,CartPole 将能够自我保持平衡,如下图所示:
你已经学会了一个程序,它可以使 CartPole 稳定。
总结
在这一章中,你了解了 OpenAI Gym,它被用于强化学习项目。你看到了几个提供开箱即用的训练平台示例。然后,我们提出了 CartPole 问题,并通过试错法使 CartPole 保持平衡。
在下一章,你将学习如何使用 Gym 和强化学习方法来玩 Atari 游戏。
第三章:玩 Atari 游戏
一个机器如何自己学习玩视频游戏并击败人类玩家?解决这个问题是通向游戏领域人工智能(AI)的第一步。创建 AI 玩家所需的关键技术是深度强化学习。2015 年,谷歌的 DeepMind(该团队因开发击败围棋冠军李世石的机器 AlphaGo 而闻名)提出了深度 Q 学习算法,用于构建一个能够学习玩 Atari 2600 游戏,并在多个游戏中超越人类专家的 AI 玩家。这项工作对 AI 研究产生了重大影响,展示了构建通用 AI 系统的可能性。
在本章中,我们将介绍如何使用 gym 来玩 Atari 2600 游戏,然后解释为什么深度 Q 学习算法有效,并且如何使用 TensorFlow 实现它。目标是能够理解深度强化学习算法以及如何应用它们来解决实际任务。本章将为理解后续章节奠定坚实的基础,后续章节将介绍更复杂的方法。
本章将覆盖的主题如下:
-
Atari 游戏介绍
-
深度 Q 学习
-
DQN 实现
Atari 游戏介绍
Atari, Inc. 是一家美国的视频游戏开发公司和家用计算机公司,由 Nolan Bushnell 和 Ted Dabney 于 1972 年创立。1976 年,Bushnell 开发了 Atari 视频计算机系统(或 Atari VCS,后来更名为 Atari 2600)。Atari VCS 是一款灵活的游戏主机,能够播放现有的 Atari 游戏,包括主机、两个摇杆、一对控制器和一张战斗游戏卡带。以下截图展示了 Atari 主机:
Atari 2600 拥有超过 500 款游戏,由 Atari、Sears 和一些第三方公司发布。一些著名的游戏包括《打砖块》(Breakout)、《吃豆人》(Pac-Man)、《陷阱》(Pitfall!)、《亚特兰蒂斯》(Atlantis)、《海底探险》(Seaquest)和《太空侵略者》(Space Invaders)。
由于 1983 年北美视频游戏崩溃的直接结果,Atari, Inc. 于 1984 年关闭,并将其资产拆分。Atari 的家用计算机和游戏主机部门于 1984 年 7 月被 Jack Tramiel 以 Atari Corporation 名义收购。
对于那些有兴趣玩 Atari 游戏的读者,这里有几个在线 Atari 2600 模拟器网站,您可以在这些网站上找到许多流行的 Atari 2600 游戏:
因为我们的目标是为这些游戏开发一个 AI 玩家,所以最好先玩这些游戏并了解它们的难点。最重要的是:放松并享受乐趣!
构建 Atari 模拟器
OpenAI gym 提供了一个具有 Python 接口的 Atari 2600 游戏环境。这些游戏由街机学习环境模拟,街机学习环境使用 Stella Atari 模拟器。有关更多细节,请阅读以下论文:
-
MG Bellemare, Y Naddaf, J Veness 和 M Bowling,街机学习环境:通用代理的评估平台,《人工智能研究杂志》(2012)
-
Stella:一个多平台的 Atari 2600 VCS 模拟器,
stella.sourceforge.net/
快速入门
如果你没有完整安装 OpenAI gym,可以通过以下方式安装 Atari 环境的依赖项:
pip install gym[atari]
这需要 cmake 工具。此命令将自动编译街机学习环境及其 Python 接口 atari-py。编译将在普通笔记本上花费几分钟时间,因此可以去喝杯咖啡。
安装完 Atari 环境后,可以尝试以下操作:
import gym
atari = gym.make('Breakout-v0')
atari.reset()
atari.render()
如果运行成功,将会弹出一个小窗口,显示 Breakout 游戏的屏幕,如下所示的截图所示:
Breakout 的 rom 名称中的 v0 后缀的含义将在稍后解释。我们将使用 Breakout 来测试我们的 AI 游戏玩家训练算法。在 Breakout 中,几层砖块位于屏幕的顶部。一颗球在屏幕上移动,撞击屏幕的顶部和侧壁。当球击中砖块时,砖块会被销毁,球会反弹,并根据砖块的颜色为玩家提供一定的分数。当球触及屏幕底部时,玩家失去一次回合。为了避免这种情况,玩家需要移动挡板将球弹回。
Atari VCS 使用摇杆作为控制 Atari 2600 游戏的输入设备。摇杆和挡板能够提供的总输入数为 18。 在 gym Atari 环境中,这些动作被标记为从 0 到 17 的整数。每个动作的含义如下:
| 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
| 无操作 | 开火 | 上 | 右 | 左 | 下 |
| 6 | 7 | 8 | 9 | 10 | 11 |
| 上+右 | 上+左 | 下+右 | 下+左 | 上+开火 | 右+开火 |
| 12 | 13 | 14 | 15 | 16 | 17 |
| 左+开火 | 下+开火 | 上+右+开火 | 上+左+开火 | 下+右+开火 | 下+左+开火 |
可以使用以下代码获取游戏中有效动作的含义:
actions = atari.env.get_action_meanings()
对于 Breakout,动作包括以下内容:
[0, 1, 3, 4] or ['NOOP', 'FIRE', 'RIGHT', 'LEFT']
要获取动作的数量,也可以使用以下代码:
num_actions = atari.env.action_space.n
在这里,atari.env中的成员变量action_space存储了有关游戏有效动作的所有信息。通常,我们只需要知道有效动作的总数。
我们现在知道如何访问 Atari 环境中的动作信息。但是,给定这些动作,如何控制游戏呢?要执行一个动作,可以调用 step 函数:
observation, reward, done, info = atari.step(a)
输入参数a是你想要执行的动作,它是有效动作列表中的索引。例如,如果想执行LEFT动作,输入应该是3而不是4,或者如果不执行任何动作,输入应该是0。step函数返回以下四个值之一:
-
Observation:一个环境特定的对象,表示你对环境的观察。对于 Atari 来说,它是执行动作后屏幕帧的图像。 -
Reward:由动作获得的奖励数量。 -
Done:是否到了重新初始化环境的时间。在 Atari 游戏中,如果你失去了最后一条生命,done会为真,否则为假。 -
Info:有助于调试的诊断信息。不能在学习算法中使用这些信息,所以通常我们可以忽略它。
Atari 模拟器的实现
我们现在准备使用 gym 构建一个简单的 Atari 模拟器。和其他电脑游戏一样,用于控制 Atari 游戏的键盘输入如下所示:
| w | a | s | d | space |
|---|---|---|---|---|
| 上 | 左 | 下 | 右 | 发射 |
为了检测键盘输入,我们使用pynput.keyboard包,它允许我们控制和监控键盘(pythonhosted.org/pynput/)。如果没有安装pynput包,请运行以下命令:
pip install pynput
pynput.keyboard提供了一个键盘监听器,用于捕获键盘事件。在创建键盘监听器之前,应该导入Listener类:
import gym
import queue, threading, time
from pynput.keyboard import Key, Listener
除了Listener类,程序中还需要其他包,如gym和threading。
以下代码展示了如何使用Listener来捕获键盘输入,即按下了W、A、S、D或space键时的情况:
def keyboard(queue):
def on_press(key):
if key == Key.esc:
queue.put(-1)
elif key == Key.space:
queue.put(ord(' '))
else:
key = str(key).replace("'", '')
if key in ['w', 'a', 's', 'd']:
queue.put(ord(key))
def on_release(key):
if key == Key.esc:
return False
with Listener(on_press=on_press, on_release=on_release) as listener:
listener.join()
实际上,键盘监听器是一个 Python 的threading.Thread对象,所有的回调都会从该线程中调用。在keyboard函数中,监听器注册了两个回调:on_press,当按下一个键时被调用,以及on_release,当一个键被释放时调用。该函数使用一个同步队列在不同线程之间共享数据。当W、A、S、D或space被按下时,其 ASCII 值会被发送到队列中,可以从另一个线程中访问。如果按下了esc键,一个终止信号*-*会被发送到队列中。然后,监听器线程会在esc键被释放时停止。
启动键盘监听器在 macOS X 上有一些限制;即以下条件之一应当为真:
-
进程必须以 root 权限运行
-
应用程序必须在辅助设备访问权限中列入白名单
详情请访问pythonhosted.org/pynput/keyboard.html。
使用 gym 的 Atari 模拟器
模拟器的另一部分是gym Atari 模拟器:
def start_game(queue):
atari = gym.make('Breakout-v0')
key_to_act = atari.env.get_keys_to_action()
key_to_act = {k[0]: a for k, a in key_to_act.items() if len(k) > 0}
observation = atari.reset()
import numpy
from PIL import Image
img = numpy.dot(observation, [0.2126, 0.7152, 0.0722])
img = cv2_resize_image(img)
img = Image.fromarray(img)
img.save('save/{}.jpg'.format(0))
while True:
atari.render()
action = 0 if queue.empty() else queue.get(block=False)
if action == -1:
break
action = key_to_act.get(action, 0)
observation, reward, done, _ = atari.step(action)
if action != 0:
print("Action {}, reward {}".format(action, reward))
if done:
print("Game finished")
break
time.sleep(0.05)
第一步是使用 gym.make 创建一个 Atari 环境。如果你有兴趣玩其他游戏,比如 Seaquest 或 Pitfall,只需将 Breakout-v0 改为 Seaquest-v0 或 Pitfall-v0。然后,调用 get_keys_to_action 获取 key to action 映射,该映射将 w、a、s、d 和 space 的 ASCII 值映射到内部动作。在 Atari 模拟器启动之前,必须调用 reset 函数来重置游戏参数和内存,并返回第一帧游戏画面。在主循环中,render 会在每一步渲染 Atari 游戏。输入动作从队列中拉取,且不会阻塞。如果动作是终止信号 -1,游戏将退出。否则,执行当前步骤的动作,通过运行 atari.step。
要启动模拟器,请运行以下代码:
if __name__ == "__main__":
queue = queue.Queue(maxsize=10)
game = threading.Thread(target=start_game, args=(queue,))
game.start()
keyboard(queue)
按下射击按钮开始游戏并享受它!这个模拟器提供了一个用于在 gym Atari 环境中测试 AI 算法的基本框架。稍后,我们将用我们的 AI 玩家替换 keyboard 功能。
数据准备
仔细的读者可能会注意到每个游戏名称后都有一个后缀 v0,并产生以下问题:v0 的意思是什么? 是否可以将其替换为 v1 或 v2? 实际上,这个后缀与从 Atari 环境提取的屏幕图像(观察)进行数据预处理的步骤有关。
每个游戏有三种模式,例如 Breakout、BreakoutDeterministic 和 BreakoutNoFrameskip,每种模式有两个版本,例如 Breakout-v0 和 Breakout-v4。三种模式的主要区别在于 Atari 环境中 frameskip 参数的值。这个参数表示一个动作重复的帧数(步骤数)。这就是所谓的 帧跳过 技术,它让我们能够在不显著增加运行时间的情况下玩更多游戏。
对于 Breakout,frameskip 是从 2 到 5 随机抽样的。以下截图显示了当提交 LEFT 动作时,step 函数返回的帧画面:
对于 BreakoutDeterministic,Space Invaders 游戏的 frameskip 被设置为 3,其他游戏的 frameskip 为 4。在相同的 LEFT 动作下,step 函数返回如下:
对于 BreakoutNoFrameskip,所有游戏的 frameskip 始终为 1,意味着没有帧跳过。类似地,LEFT 动作在每一步都会执行:
这些截图展示了尽管步进函数在相同的动作LEFT下被调用了四次,最终的球板位置却大不相同。由于 BreakoutDeterministic 的帧跳跃为 4,所以它的球板离左墙最近。而 BreakoutNoFrameskip 的帧跳跃为 1,因此它的球板离左墙最远。对于 Breakout,球板处于中间位置,因为在每一步中,帧跳跃是从[2, 5]中采样的。
从这个简单的实验中,我们可以看到帧跳跃参数的效果。它的值通常设置为 4,以便进行快速学习。回想一下,每个模式都有两个版本,v0 和 v4。它们的主要区别在于repeat_action_probability参数。这个参数表示尽管提交了另一个动作,仍然有概率采取无操作(NOOP)动作。对于 v0,它的值设置为 0.25,v4 的值为 0.0。由于我们希望得到一个确定性的 Atari 环境,本章选择了 v4 版本。
如果你玩过一些 Atari 游戏,你可能注意到游戏画面的顶部区域通常包含记分板,显示你当前的得分和剩余生命数。这些信息与游戏玩法无关,因此顶部区域可以被裁剪掉。另外,通过步进函数返回的帧图像是 RGB 图像。实际上,在 Atari 环境中,彩色图像并不提供比灰度图像更多的信息;换句话说,使用灰度屏幕也可以照常玩 Atari 游戏。因此,有必要通过裁剪帧图像并将其转换为灰度图像来保留有用的信息。
将 RGB 图像转换为灰度图像非常简单。灰度图像中每个像素的值表示光强度,可以通过以下公式计算:
这里,R、G 和 B 分别是 RGB 图像的红色、绿色和蓝色通道。给定一个形状为(height, width, channel)的 RGB 图像,可以使用以下 Python 代码将其转换为灰度图像:
def rgb_to_gray(self, im):
return numpy.dot(im, [0.2126, 0.7152, 0.0722])
以下图片给出了一个示例:
对于裁剪帧图像,我们使用opencv-python包,或者称为cv2,它是一个 Python 包装器,封装了原始的 C++ OpenCV 实现。欲了解更多信息,请访问opencv-python的官方网站:opencv-python-tutroals.readthedocs.io/en/latest/index.html。opencv-python包提供了基本的图像转换操作,如图像缩放、平移和旋转。在本章中,我们只需要使用图像缩放函数 resize,该函数接受输入图像、图像大小和插值方法作为输入参数,并返回缩放后的图像。
以下代码展示了图像裁剪操作,涉及两个步骤:
-
重塑输入图像,使得最终图像的宽度等于通过
resized_shape参数指定的调整后的宽度84。 -
使用
numpy切片裁剪重塑图像的顶部区域:
def cv2_resize_image(image, resized_shape=(84, 84),
method='crop', crop_offset=8):
height, width = image.shape
resized_height, resized_width = resized_shape
if method == 'crop':
h = int(round(float(height) * resized_width / width))
resized = cv2.resize(image,
(resized_width, h),
interpolation=cv2.INTER_LINEAR)
crop_y_cutoff = h - crop_offset - resized_height
cropped = resized[crop_y_cutoff:crop_y_cutoff+resized_height, :]
return numpy.asarray(cropped, dtype=numpy.uint8)
elif method == 'scale':
return numpy.asarray(cv2.resize(image,
(resized_width, resized_height),
interpolation=cv2.INTER_LINEAR),
dtype=numpy.uint8)
else:
raise ValueError('Unrecognized image resize method.')
例如,给定一张灰度输入图像,cv2_resize_image函数会返回一张裁剪后的图像,大小为 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/e6e3287e-f259-4902-a935-7c2ca2ce9e38.png,如下图所示:
到目前为止,我们已经完成了数据准备。数据现在已经可以用来训练我们的 AI 玩家了。
深度 Q 学习
现在是有趣的部分——我们 AI Atari 玩家的大脑设计。核心算法基于深度强化学习(Deep RL)。为了更好地理解它,需要一些基本的数学公式。深度强化学习是深度学习和传统强化学习的完美结合。如果不理解强化学习的基本概念,很难在实际应用中正确使用深度强化学习。例如,可能会有人在没有正确定义状态空间、奖励和转移的情况下尝试使用深度强化学习。
好了,别害怕这些公式的难度。我们只需要高中水平的数学知识,不会深入探讨为什么传统强化学习算法有效的数学证明。本章的目标是学习基本的 Q 学习算法,了解如何将其扩展为深度 Q 学习算法(DQN),并理解这些算法背后的直觉。此外,你还将学习 DQN 的优缺点,什么是探索与开发,为什么需要重放记忆,为什么需要目标网络,以及如何设计一个卷积神经网络来表示状态特征。
看起来很有趣,对吧?我们希望本章不仅帮助你理解如何应用深度强化学习来解决实际问题,也为深度强化学习研究打开了一扇门。对于已经熟悉卷积神经网络、马尔可夫决策过程和 Q 学习的读者,可以跳过第一部分,直接进入 DQN 的实现。
强化学习的基本元素
首先,让我们回顾一下在第一章中讨论的一些强化学习的基本元素:
-
状态:状态空间定义了环境的所有可能状态。在 Atari 游戏中,状态是玩家在某一时刻观察到的屏幕图像或几张连续的屏幕图像,表示当时的游戏状态。
-
奖励函数:奖励函数定义了强化学习问题的目标。它将环境的状态或状态-动作对映射到一个实数,表示该状态的可取性。在 Atari 游戏中,奖励是玩家在采取某个动作后获得的分数。
-
策略函数:策略函数定义了玩家在特定时间的行为,它将环境的状态映射到在这些状态下应该采取的动作。
-
价值函数:价值函数表示在长期内哪个状态或状态-动作对是好的。一个状态的价值是玩家从该状态开始,未来可以预期积累的奖励的总和(或折扣后的总和)。
演示基本的 Q 学习算法
为了演示基本的 Q 学习算法,我们来看一个简单的问题。假设我们的智能体(玩家)生活在一个网格世界中。一天,她被困在一个奇怪的迷宫中,如下图所示:
迷宫包含六个房间。我们的智能体出现在房间 1,但她对迷宫一无所知,也就是说,她不知道房间 6 有能够将她送回家的“心爱之物”,或者房间 4 有一个会击打她的闪电。因此,她必须小心地探索迷宫,尽快逃脱。那么,我们如何让我们可爱的智能体通过经验学习呢?
幸运的是,她的好朋友 Q 学习可以帮助她生存下来。这个问题可以表示为一个状态图,其中每个房间作为一个状态,智能体从一个房间到另一个房间的移动视为一个动作。状态图如下所示:
在这里,动作用箭头表示,箭头上标记的数字是该状态-动作对的奖励。例如,当我们的智能体从房间 5 移动到房间 6 时,由于达到了目标,她会获得 100 分。当她从房间 3 移动到房间 4 时,她会得到一个负奖励,因为闪电击中了她。这个状态图也可以用矩阵表示:
| 状态\动作 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| 1 | - | 0 | - | - | - | - |
| 2 | 0 | - | 0 | - | 0 | - |
| 3 | - | 0 | - | -50 | - | - |
| 4 | - | - | 0 | - | - | - |
| 5 | - | 0 | - | - | - | 100 |
| 6 | - | - | - | - | - | - |
矩阵中的虚线表示在该状态下该动作不可用。例如,我们的智能体不能直接从房间 1 移动到房间 6,因为两者之间没有连接的门。
让 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/c8412c07-9c66-46b2-a692-158c6b140f63.png 是一个状态,https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/61f535eb-890a-4d2c-af5f-905d0ad05cfc.png 是一个动作,https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/91112b13-bd5a-48aa-bcff-dd917bda7680.png 是奖励函数,https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/1baa24f0-4939-44da-afbe-c8572b2f76b7.png 是价值函数。回忆一下,https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/57c05ff8-0396-4939-96b9-98ad751f6b59.png 是状态-动作对 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/550d9049-162a-41a8-a884-8310644d63db.png 在长期内的期望回报,这意味着我们的智能体能够根据 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/39cbb45d-6168-4c09-b7de-c13c0ec7ddcc.png 来决定进入哪个房间。Q 学习算法非常简单,它通过以下更新规则来估计每个状态-动作对的 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/1f073dd4-441f-4c3a-b5a3-79025c0a1641.png:
在这里,https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/46756389-7363-4b76-a9f9-dc9cd63afc31.png 是当前状态,https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/7e3b8629-ecde-424d-bcab-aceae5e87687.png 是采取行动后进入的下一个状态,https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/21ea2c3c-7fd9-4214-92e9-02031ef4d5b9.png 是在 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/1f98e7be-c917-41e5-b561-0859edb01f46.png 时的动作,https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/4fd06b2f-e807-4fc6-99dd-ddd4eb34b03c.png 是在 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/8cdc93c0-28ac-41b7-878e-2dae3b464e3e.png 时可用的动作集, 是折扣因子,https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/0b053543-d0c4-485f-98eb-db30fd849896.png 是学习率。折扣因子 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/2f3f8ed1-0f61-4f7d-8112-f7763323f680.png 的值位于 [0,1] 范围内。折扣因子小于 1 意味着我们的智能体更偏好当前的奖励,而非过去的奖励。
一开始,我们的智能体对价值函数一无所知,因此 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/25114801-7fc6-40bd-b295-c57bc1ea4aa5.png 被初始化为所有状态-动作对的 0。她将从一个状态探索到另一个状态,直到达到目标。我们将每一次探索称为一个回合,它由从初始状态(例如,房间 1)到最终状态(例如,房间 6)组成。Q 学习算法如下所示:
Initialize to zero and set parameters ,;
Repeat for each episode:
Randomly select an initial state ;
While the goal state hasn't been reached:
Select action among all the possible actions in state (e.g., using greedy);
Take action and observe reward , next state ;
Update ;
Set the current state ;
End while
小心的读者可能会问一个问题:如何选择在状态https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/6472d012-4927-484b-abb1-fbae05294eea.png下的行动 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/4cb8f468-d816-409c-a988-c026494017a1.png,例如,行动 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/69c74860-fd8d-4d7e-a71b-13eb0e694079.png是从所有可能的行动中随机选择,还是根据当前估算的价值函数 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/62e69dcf-841b-492e-a49d-2c200bda5be8.png派生的策略来选择?什么是https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/996156af-478d-4a3e-b683-28051be43f3c.png贪心策略?这些问题涉及到两个重要的概念,即探索和利用。探索意味着尝试新事物以收集更多的环境信息,而利用则是根据你已有的信息做出最佳决策。例如,尝试一家新餐厅是探索,而去你最喜欢的餐厅则是利用。在我们的迷宫问题中,探索是我们的代理尝试进入一个她之前没有去过的新房间,而利用则是她根据从环境中收集到的信息选择她最喜欢的房间。
探索和利用在强化学习中都是必要的。如果没有探索,我们的代理就无法获得关于环境的新知识,因此她将一遍又一遍地做出错误决策。如果没有利用,她从探索中获得的信息就会变得毫无意义,因为她无法从中学习以做出更好的决策。因此,探索和利用之间的平衡或权衡是必不可少的。https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/a19c9030-f51d-47c0-b48f-d23865dad042.png贪心策略是实现这种权衡的最简单方式:
为了进一步理解 Q 学习的工作原理,我们通过几个步骤来手动演示。为清晰起见,我们设置学习率 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/93d46ebc-a126-41e8-9278-35e55cc2b8d3.png和折扣因子 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/9837b06c-85ec-4df6-b321-95c61facad22.png。以下代码展示了 Q 学习在 Python 中的实现:
import random, numpy
def Q_learning_demo():
alpha = 1.0
gamma = 0.8
epsilon = 0.2
num_episodes = 100
R = numpy.array([
[-1, 0, -1, -1, -1, -1],
[ 0, -1, 0, -1, 0, -1],
[-1, 0, -1, -50, -1, -1],
[-1, -1, 0, -1, -1, -1],
[-1, 0, -1, -1, -1, 100],
[-1, -1, -1, -1, -1, -1]
])
# Initialize Q
Q = numpy.zeros((6, 6))
# Run for each episode
for _ in range(num_episodes):
# Randomly choose an initial state
s = numpy.random.choice(5)
while s != 5:
# Get all the possible actions
actions = [a for a in range(6) if R[s][a] != -1]
# Epsilon-greedy
if numpy.random.binomial(1, epsilon) == 1:
a = random.choice(actions)
else:
a = actions[numpy.argmax(Q[s][actions])]
next_state = a
# Update Q(s,a)
Q[s][a] += alpha * (R[s][a] + gamma * numpy.max(Q[next_state]) - Q[s][a])
# Go to the next state
s = next_state
return Q
经过 100 轮训练后,价值函数 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/61445d4b-2ad1-4162-ad47-dc6144988430.png收敛到以下结果(对于那些对为什么该算法会收敛的读者,请参考*《强化学习:导论》*,作者为 Andrew Barto 和 Richard S. Sutton):
| 状态\行动 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| 1 | - | 64 | - | - | - | - |
| 2 | 51.2 | - | 51.2 | - | 80 | - |
| 3 | - | 64 | - | -9.04 | - | - |
| 4 | - | - | 51.2 | - | - | - |
| 5 | - | 64 | - | - | - | 100 |
| 6 | - | - | - | - | - | - |
因此,得到的状态图变为:
这表明从其他所有状态到目标状态的最佳路径如下:
基于这些知识,我们的智能体能够返回家中,无论她处于哪个房间。更重要的是,她变得更加聪明和快乐,实现了我们训练智能 AI 代理或玩家的目标。
这个最简单的 Q 学习算法只能处理离散的状态和动作。对于连续状态,它无法处理,因为由于存在无限的状态,收敛性不能得到保证。我们如何在像 Atari 游戏这样的无限状态空间中应用 Q 学习?答案是用神经网络代替表格来近似动作-价值函数!。这就是谷歌 DeepMind 论文《Playing Atari with deep reinforcement learning》背后的直觉。
为了将基本的 Q 学习算法扩展到深度 Q 学习算法,需要回答两个关键问题:
-
可以使用什么样的神经网络来从 Atari 环境中的观察数据(如屏幕图像)中提取高级特征?
-
如何在每个训练步骤中更新动作-价值函数,https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/38244a64-bc38-4734-8b1f-48b3af6f641f.png?
对于第一个问题,有几种方法可以近似动作-价值函数,https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/ba2ff8d1-d8b1-42d2-a019-2eef187c1257.png。一种方法是将状态和动作都作为神经网络的输入,网络输出它们的 Q 值标量估计,如下图所示:
这种方法的主要缺点是需要额外的前向传播来计算!,因为动作被作为输入之一传递到网络中,这会导致计算成本与所有可能动作的数量成线性关系。另一种方法是只将状态作为神经网络的输入,而每个可能的动作都有一个独立的输出:
这种方法的主要优点是能够通过网络的单次前向传播计算出给定状态下所有可能动作的 Q 值,而且通过选择相应的输出头可以轻松获取某个动作的 Q 值。
在深度 Q 网络中,应用了第二种架构。回顾一下,数据预处理步骤中的输出是一个 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/99b1f14f-8ad5-49e5-aca9-58720f322382.png 灰度帧图像。然而,当前的屏幕图像不足以进行 Atari 游戏,因为它不包含游戏状态的动态信息。以 Breakout 为例;如果我们只看到一帧,我们只能知道球和球拍的位置,但无法得知球的方向或速度。实际上,方向和速度对于决定如何移动球拍至关重要。如果没有它们,游戏就无法进行。因此,输入网络的不仅是单独的一帧图像,而是历史中的最后四帧图像被堆叠在一起,形成网络的输入。这四帧组成一个 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/47201bd5-f4f3-43b5-bded-eef0f697882e.png 图像。除了输入层,Q 网络包含三层卷积层和一层全连接层,如下所示:
第一层卷积层有 64 个 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/ed5987c9-3441-4a2d-b1b4-0057f5970083.png 卷积核,步长为 4,之后接一个整流线性单元(RELU)。第二层卷积层有 64 个 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/8594e75f-261f-4df0-9826-e7cf9ca7bdcb.png 卷积核,步长为 2,之后接 RELU。第三层卷积层有 64 个 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/6d194e05-96a0-4f1f-a044-322cf4760b64.png 卷积核,步长为 2,之后接 RELU。全连接的隐藏层有 512 个隐藏单元,再次接 RELU。输出层也是一个全连接层,每个动作对应一个输出。
熟悉卷积神经网络的读者可能会问,为什么第一层卷积层使用了一个 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/bdc4b421-2e99-4d82-a67c-543baa012ebf.png 卷积核,而不是广泛应用于计算机视觉中的 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/f7b07854-81ff-47a0-8555-a3c3dc499e66.png 卷积核或 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/030bd2b9-7e5f-455c-9a76-72f3cb6c1c26.png 卷积核。使用大卷积核的主要原因是,Atari 游戏通常包含非常小的物体,如球、子弹或弹丸。使用较大的卷积核的卷积层能够放大这些小物体,有助于学习状态的特征表示。对于第二层和第三层卷积层,较小的卷积核足以捕捉到有用的特征。
到目前为止,我们已经讨论了 Q 网络的架构。那么,我们如何在具有无限状态空间的 Atari 环境中训练这个 Q 网络呢?是否可以基于基本的 Q 学习算法来训练它?幸运的是,答案是肯定的。回顾一下,基本 Q 学习中 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/f85e009b-d049-4650-ba9e-1263cc12b425.png 的更新规则如下:
当学习率为 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/898498da-07ed-4c98-bfd1-6a7e3b3cbad2.png 时,该更新规则变为如下形式:
这就是所谓的贝尔曼方程。实际上,贝尔曼方程是许多强化学习算法的核心。使用贝尔曼方程作为迭代更新的算法称为值迭代算法。在本书中,我们不会详细讨论值迭代或策略迭代。如果你对它们感兴趣,可以参考 Andrew Barto 和 Richard S. Sutton 的《强化学习:导论》。
刚才显示的方程仅适用于确定性环境,其中给定当前状态 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/8fb40cc4-2a7a-4e08-8db1-8ee4b723133a.png和动作 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/f4ad75de-a4fe-42a7-95b4-78c0a40e42a6.png,下一个状态 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/ebe2a0c8-a332-4a3f-9fe2-8e72203d3dd6.png是固定的。在非确定性环境中,贝尔曼方程应该如下:
这里,右侧是关于下一个状态 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/3e11b272-3878-432a-ba38-a196a97e28e3.png的期望值(例如,https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/4cea44a6-e9d9-460d-b93b-b7d30189f66b.png的分布由 Atari 模拟器确定)。对于无限状态空间,通常使用函数逼近器(如 Q 网络)来估计动作价值函数 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/42b48d0e-14b8-46e4-b9c4-be0abead3f0d.png。然后,Q 网络可以通过最小化以下损失函数,在第i次迭代中进行训练,而不是迭代更新 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/06dc14b7-65d8-45cb-a20f-dbbe702cb163.png:
这里,Q(s,a;)表示由 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/6f25c491-93b4-47e4-b64e-ac207c62e209.png参数化的 Q 网络,https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/acba632a-dfff-4842-a8aa-32899fa0be04.png是第i次迭代的目标,https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/e4001b8d-2541-49a0-a2b9-1a4003a0d067.png是序列和动作的概率分布。在优化损失函数 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/2fd194bc-effd-41fc-87aa-0aef0063524b.png时,来自前一次迭代i-1的参数是固定的,损失函数是关于 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/c40e80b6-15d7-4440-85eb-c61a1d535023.png的。在实际应用中,无法精确计算 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/899b8f6b-bdc7-4c4a-bdbe-e6aa278babbd.png中的期望值。因此,我们不会直接优化 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/1ae281d7-5523-445f-afaf-69e80f91703b.png,而是最小化 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/fdb4b635-9045-4ce5-a410-ceb03c5644da.png的经验损失,它通过从概率分布 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/e4001b8d-2541-49a0-a2b9-1a4003a0d067.png和 Atari 模拟器中获得的样本 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/2cd96ade-1021-4337-af9b-8ab6e8f99a8d.png来替代期望值。与其他深度学习算法一样,经验损失函数可以通过随机梯度下降法进行优化。
这个算法不需要构建仿真器的估计,例如,它不需要知道 Atari 仿真器的内部游戏机制,因为它仅使用来自仿真器的样本来解决强化学习问题。这个特性称为无模型,即它可以将底层模型视为黑盒。这个算法的另一个特性是离策略。它学习贪婪策略https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/9a9692db-dc07-4a3d-be2a-87472934c71d.png,同时遵循平衡探索与开发的状态空间概率分布https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/127e4a41-2c25-4614-91bd-00e1c3d60cf4.png。如前所述,https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/84a647b9-5a68-495a-bf24-7993adaac0ad.png可以作为一种https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/0a4f00c0-58d2-4cee-a24e-6fc913e2b9c7.png贪婪策略进行选择。
深度 Q 学习算法的推导对于不熟悉强化学习或马尔科夫决策过程的读者来说可能有点困难。为了使其更容易理解,让我们来看一下下面的图示:
我们 AI 玩家的大脑是 Q 网络控制器。在每个时间步 t,她观察屏幕图像https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/d3b118a9-7148-4a00-bf27-891876015028.png(回想一下,st 是一个https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/c1550c24-4a86-4411-bca9-7e8c4ed1ef98.png堆叠了最后四帧的图像)。然后,她的大脑分析这个观察结果,并做出一个动作,https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/669306ba-08b7-442e-903a-164890128a34.png。Atari 仿真器接收到这个动作,并返回下一个屏幕图像https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/9a8cf78c-56e3-457a-b1af-3c0f1920b3b2.png,以及奖励https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/6395f46a-cf4f-488b-9ae2-4b9c536374e1.png。四元组https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/e3b66ad7-170d-4ee5-8713-db4be648c25a.png被存储在内存中,并作为样本用于通过随机梯度下降最小化经验损失函数来训练 Q 网络。
我们如何从存储在内存中的四元组中抽取样本?一种方法是,这些样本,https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/eb191497-6ef0-432c-89c5-e568b5e7056d.png,是通过我们的 AI 玩家与环境的互动得出的。例如,样本https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/a58a78de-c408-499e-b397-c6031a560ffb.png用于训练 Q 网络。这个方法的主要缺点是,一批中的样本具有强烈的相关性。强相关性破坏了构建经验损失函数时样本独立性的假设,导致训练过程不稳定,表现不佳:
深度 Q 学习算法应用了另一种方法,利用了一种叫做经验回放的技术。AI 玩家在每个时间步骤 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/4a7b4f5c-f375-4546-a0c1-dbed21263257.png 的经验被存储在回放记忆中,从中随机抽取一批样本以训练 Q 网络。从数学上讲,我们无法保证抽取样本之间的独立性。但在实际操作中,这种方法能够稳定训练过程并产生合理的结果:
到目前为止,我们已经讨论了深度 Q 学习算法中的所有组件。完整的算法如下所示:
Initialize replay memory to capacity ;
Initialize the Q-network with random weights ;
Repeat for each episode:
Set time step ;
Receive an initial screen image and do preprocessing ;
While the terminal state hasn't been reached:
Select an action at via greedy, i.e., select a random action with probability , otherwise select ;
Execute action at in the emulator and observe reward and image ;
Set and store transition into replay memory ;
Randomly sample a batch of transitions from ;
Set if is a terminal state or if is a non-terminal state;
Perform a gradient descent step on ;
End while
该算法在一些 Atari 游戏中表现良好,例如《打砖块》、《海底探险》、《乒乓》和《Qbert》,但仍然无法达到人类水平的控制。一个缺点是计算目标 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/5759a82c-7bf5-41ea-b065-ec74d08e7bb2.png 时使用了当前的动作值函数估计 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/fcf4a15e-0ffc-40f4-955a-0b733c40555e.png,这使得训练步骤变得不稳定,即一个增加 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/011ebf97-d241-4fd7-a1da-ba8490f2e1a9.png 的更新通常也会增加所有的 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/98fc7dbf-c711-494e-8187-55bd6369945c.png,因此也增加了目标 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/60bb25a5-2d55-444e-bfaf-8678c9ffeb6b.png,这可能导致策略的振荡或发散。
为了解决这个问题,谷歌 DeepMind 在他们的论文《通过深度强化学习实现人类水平的控制》中引入了目标网络,该论文发表于《自然》杂志。目标网络背后的理念相当简单:使用一个独立的网络来生成 Q 学习更新中的目标 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/5759a82c-7bf5-41ea-b065-ec74d08e7bb2.png。更准确地说,对于每个 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/6fd13b9a-c296-40cb-b309-cf0f3b6c762a.png Q 学习更新,网络 Q 被克隆以获得目标网络 Q,并用于生成接下来的 Q 更新中的目标 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/b49f9246-7588-483e-a7c6-6b5b6318f825.png。因此,深度 Q 学习算法变为如下:
Initialize replay memory to capacity ;
Initialize the Q-network with random weights ;
Initialize the target network with weights ;
Repeat for each episode:
Set time step ;
Receive an initial screen image and do preprocessing ;
While the terminal state hasn't been reached:
Select an action at via greedy, i.e., select a random action with probability , otherwise select ;
Execute action at in the emulator and observe reward and image ;
Set and store transition into replay memory ;
Randomly sample a batch of transitions from ;
Set if is a terminal state or if is a non-terminal state;
Perform a gradient descent step on ;
Set for every steps;
End while
使用目标网络,通过深度 Q 学习算法训练的 AI 玩家能够超越大多数先前强化学习算法的表现,并在 49 款 Atari 2600 游戏中实现了人类水平的表现,例如《星际枪手》、《亚特兰蒂斯》、《攻击》和《太空侵略者》。
深度 Q 学习算法在通用人工智能方面迈出了重要一步。尽管它在 Atari 2600 游戏中表现良好,但仍然存在许多未解决的问题:
-
收敛速度慢:它需要很长时间(在一块 GPU 上需要 7 天)才能达到人类水平的表现。
-
稀疏奖励失败:它在《蒙特祖玛的复仇》游戏中无法发挥作用,因为该游戏需要长期规划。
-
需要大量数据:这是大多数强化学习算法常见的问题。
为了解决这些问题,最近提出了深度 Q 学习算法的不同变种,例如双重 Q 学习、优先经验回放、引导式 DQN 和对抗网络架构。我们在本书中不讨论这些算法。对于想要深入了解 DQN 的读者,请参考相关论文。
DQN 的实现
本章将展示如何使用 Python 和 TensorFlow 实现深度 Q 学习算法的所有组件,例如 Q 网络、回放记忆、训练器和 Q 学习优化器。
我们将实现QNetwork类,这是我们在上一章中讨论的 Q 网络,其定义如下:
class QNetwork:
def __init__(self, input_shape=(84, 84, 4), n_outputs=4,
network_type='cnn', scope='q_network'):
self.width = input_shape[0]
self.height = input_shape[1]
self.channel = input_shape[2]
self.n_outputs = n_outputs
self.network_type = network_type
self.scope = scope
# Frame images
self.x = tf.placeholder(dtype=tf.float32,
shape=(None, self.channel,
self.width, self.height))
# Estimates of Q-value
self.y = tf.placeholder(dtype=tf.float32, shape=(None,))
# Selected actions
self.a = tf.placeholder(dtype=tf.int32, shape=(None,))
with tf.variable_scope(scope):
self.build()
self.build_loss()
构造函数需要四个参数,input_shape、n_outputs、network_type和scope。input_shape是输入图像的大小。经过数据预处理后,输入是一个https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/11c9512b-c3c6-4a42-830f-8d17d1475355.png图像,因此默认参数是https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/90bd34c3-690d-4b8b-b708-caecfce0be56.png。n_outputs是所有可能动作的数量,例如在 Breakout 游戏中,n_outputs为四。network_type表示我们要使用的 Q 网络类型。我们的实现包含三种不同的网络,其中两种是由 Google DeepMind 提出的卷积神经网络,另一个是用于测试的前馈神经网络。scope是 Q 网络对象的名称,可以设置为q_network或target_network。
在构造函数中,创建了三个输入张量。x变量表示输入状态(一批https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/033fb901-89a8-47df-abd0-13017738e6ce.png图像)。y和a变量分别表示动作价值函数的估计值和与输入状态对应的选定动作,用于训练 Q 网络。创建输入张量后,调用build和build_loss两个函数来构建 Q 网络。
使用 TensorFlow 构建 Q 网络非常简单,如下所示:
def build(self):
self.net = {}
self.net['input'] = tf.transpose(self.x, perm=(0, 2, 3, 1))
init_b = tf.constant_initializer(0.01)
if self.network_type == 'cnn':
self.net['conv1'] = conv2d(self.net['input'], 32,
kernel=(8, 8), stride=(4, 4),
init_b=init_b, name='conv1')
self.net['conv2'] = conv2d(self.net['input'], 64,
kernel=(4, 4), stride=(2, 2),
init_b=init_b, name='conv2')
self.net['conv3'] = conv2d(self.net['input'], 64,
kernel=(3, 3), stride=(1, 1),
init_b=init_b, name='conv3')
self.net['feature'] = dense(self.net['conv2'], 512,
init_b=init_b, name='fc1')
elif self.network_type == 'cnn_nips':
self.net['conv1'] = conv2d(self.net['input'], 16,
kernel=(8, 8), stride=(4, 4),
init_b=init_b, name='conv1')
self.net['conv2'] = conv2d(self.net['conv1'], 32,
kernel=(4, 4), stride=(2, 2),
init_b=init_b, name='conv2')
self.net['feature'] = dense(self.net['conv2'], 256,
init_b=init_b, name='fc1')
elif self.network_type == 'mlp':
self.net['fc1'] = dense(self.net['input'], 50,
init_b=init_b), name='fc1')
self.net['feature'] = dense(self.net['fc1'], 50,
init_b=init_b, name='fc2')
else:
raise NotImplementedError('Unknown network type')
self.net['values'] = dense(self.net['feature'],
self.n_outputs, activation=None,
init_b=init_b, name='values')
self.net['q_value'] = tf.reduce_max(self.net['values'],
axis=1, name='q_value')
self.net['q_action'] = tf.argmax(self.net['values'],
axis=1, name='q_action',
output_type=tf.int32)
self.vars = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES,
tf.get_variable_scope().name)
如前一章所讨论的,Atari 环境的 Q 网络包含三个卷积层和一个隐藏层,当network_type为cnn时可以构建该网络。cnn_nips类型是为 Atari 游戏简化的 Q 网络,只包含两个卷积层和一个隐藏层,且具有较少的滤波器和隐藏单元。mlp类型是一个具有两个隐藏层的前馈神经网络,用于调试。vars变量是 Q 网络中所有可训练变量的列表。
回顾一下,损失函数为https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/311d8928-3b5a-4ad1-b94c-f07867e654c2.png,可以按如下方式实现:
def build_loss(self):
indices = tf.transpose(tf.stack([tf.range(tf.shape(self.a)[0]),
self.a], axis=0))
value = tf.gather_nd(self.net['values'], indices)
self.loss = 0.5 * tf.reduce_mean(tf.square((value - self.y)))
self.gradient = tf.gradients(self.loss, self.vars)
tf.summary.scalar("loss", self.loss, collections=['q_network'])
self.summary_op = tf.summary.merge_all('q_network')
tf.gather_nd函数用于获取给定动作批次的动作值https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/a242dccc-2a1a-46cd-abbe-ab115852f00b.png。变量 loss 表示损失函数,gradient 是损失函数相对于可训练变量的梯度。summary_op用于 TensorBoard 可视化。
回放记忆的实现不涉及 TensorFlow:
class ReplayMemory:
def __init__(self, history_len=4, capacity=1000000,
batch_size=32, input_scale=255.0):
self.capacity = capacity
self.history_length = history_len
self.batch_size = batch_size
self.input_scale = input_scale
self.frames = deque([])
self.others = deque([])
ReplayMemory类接受四个输入参数,即history_len、capacity、batch_size和input_scale。history_len是堆叠在一起的帧数。通常,history_len在 Atari 游戏中设置为 4,形成一个https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/1c32790d-8486-44c4-9e05-18ec2582983e.png输入图像。capacity是回放记忆的容量,即可以存储的最大帧数。batch_size是训练时的一批样本大小。input_scale是输入图像的归一化因子,例如,对于 RGB 图像,它设置为 255。变量 frames 记录所有帧图像,变量 others 记录对应的动作、奖励和终止信号。
ReplayMemory提供了一个将记录(帧图像、动作、奖励、终止信号)添加到内存中的功能:
def add(self, frame, action, r, termination):
if len(self.frames) == self.capacity:
self.frames.popleft()
self.others.popleft()
self.frames.append(frame)
self.others.append((action, r, termination))
def add_nullops(self, init_frame):
for _ in range(self.history_length):
self.add(init_frame, 0, 0, 0)
它还提供了一个功能,通过连接历史中的最后四帧图像来构建一个https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/33f6663f-2d51-4cbb-87b4-fe3c25f809d6.png输入图像:
def phi(self, new_frame):
assert len(self.frames) > self.history_length
images = [new_frame] + [self.frames[-1-i] for i in range(self.history_length-1)]
return numpy.concatenate(images, axis=0)
以下函数从回放记忆中随机抽取一个过渡(状态、动作、奖励、下一个状态、终止信号):
def sample(self):
while True:
index = random.randint(a=self.history_length-1,
b=len(self.frames)-2)
infos = [self.others[index-i] for i in range(self.history_length)]
# Check if termination=1 before "index"
flag = False
for i in range(1, self.history_length):
if infos[i][2] == 1:
flag = True
break
if flag:
continue
state = self._phi(index)
new_state = self._phi(index+1)
action, r, termination = self.others[index]
state = numpy.asarray(state / self.input_scale,
dtype=numpy.float32)
new_state = numpy.asarray(new_state / self.input_scale,
dtype=numpy.float32)
return (state, action, r, new_state, termination)
请注意,只有对应状态中最后一帧的终止信号可以为 True。_phi(index)函数将四帧图像堆叠在一起:
def _phi(self, index):
images = [self.frames[index-i] for i in range(self.history_length)]
return numpy.concatenate(images, axis=0)
Optimizer类用于训练 Q 网络:
class Optimizer:
def __init__(self, config, feedback_size,
q_network, target_network, replay_memory):
self.feedback_size = feedback_size
self.q_network = q_network
self.target_network = target_network
self.replay_memory = replay_memory
self.summary_writer = None
self.gamma = config['gamma']
self.num_frames = config['num_frames']
optimizer = create_optimizer(config['optimizer'],
config['learning_rate'],
config['rho'],
config['rmsprop_epsilon'])
self.train_op = optimizer.apply_gradients(
zip(self.q_network.gradient,
self.q_network.vars))
它接受 Q 网络、目标网络、回放记忆和输入图像的大小作为输入参数。在构造函数中,它创建一个优化器(如 ADAM、RMSPROP 或 MOMENTUM 等流行优化器),然后构建一个用于训练的操作符。
要训练 Q 网络,需要构建一个与https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/96f94be4-2663-4447-bdcb-0ad52ff91e8c.png、https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/5a68c450-24b6-453a-b227-b89c5279dd64.png和https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/e23a5362-570b-4f5b-9f34-451d4bde8460.png对应的迷你批样本(状态、动作、目标)来计算损失函数https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/84bead4d-efad-4314-b78c-7cee54ccdfd2.png:
def sample_transitions(self, sess, batch_size):
w, h = self.feedback_size
states = numpy.zeros((batch_size, self.num_frames, w, h),
dtype=numpy.float32)
new_states = numpy.zeros((batch_size, self.num_frames, w, h),
dtype=numpy.float32)
targets = numpy.zeros(batch_size, dtype=numpy.float32)
actions = numpy.zeros(batch_size, dtype=numpy.int32)
terminations = numpy.zeros(batch_size, dtype=numpy.int32)
for i in range(batch_size):
state, action, r, new_state, t = self.replay_memory.sample()
states[i] = state
new_states[i] = new_state
actions[i] = action
targets[i] = r
terminations[i] = t
targets += self.gamma * (1 - terminations) * self.target_network.get_q_value(sess, new_states)
return states, actions, targets
请注意,目标https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/e23a5362-570b-4f5b-9f34-451d4bde8460.png是由目标网络计算的,而不是 Q 网络。给定一批状态、动作或目标,Q 网络可以通过以下方式轻松训练:
def train_one_step(self, sess, step, batch_size):
states, actions, targets = self.sample_transitions(sess, batch_size)
feed_dict = self.q_network.get_feed_dict(states, actions, targets)
if self.summary_writer and step % 1000 == 0:
summary_str, _, = sess.run([self.q_network.summary_op,
self.train_op],
feed_dict=feed_dict)
self.summary_writer.add_summary(summary_str, step)
self.summary_writer.flush()
else:
sess.run(self.train_op, feed_dict=feed_dict)
除了训练过程外,每 1000 步会将摘要写入日志文件。这个摘要用于监控训练过程,帮助调整参数和调试。
将这些模块结合在一起,我们可以实现用于主要深度 Q 学习算法的类 DQN:
class DQN:
def __init__(self, config, game, directory,
callback=None, summary_writer=None):
self.game = game
self.actions = game.get_available_actions()
self.feedback_size = game.get_feedback_size()
self.callback = callback
self.summary_writer = summary_writer
self.config = config
self.batch_size = config['batch_size']
self.n_episode = config['num_episode']
self.capacity = config['capacity']
self.epsilon_decay = config['epsilon_decay']
self.epsilon_min = config['epsilon_min']
self.num_frames = config['num_frames']
self.num_nullops = config['num_nullops']
self.time_between_two_copies = config['time_between_two_copies']
self.input_scale = config['input_scale']
self.update_interval = config['update_interval']
self.directory = directory
self._init_modules()
在这里,config 包含 DQN 的所有参数,例如训练的批量大小和学习率。game 是 Atari 环境的一个实例。在构造函数中,回放记忆、Q 网络、目标网络和优化器被初始化。要开始训练过程,可以调用以下函数:
def train(self, sess, saver=None):
num_of_trials = -1
for episode in range(self.n_episode):
self.game.reset()
frame = self.game.get_current_feedback()
for _ in range(self.num_nullops):
r, new_frame, termination = self.play(action=0)
self.replay_memory.add(frame, 0, r, termination)
frame = new_frame
for _ in range(self.config['T']):
num_of_trials += 1
epsilon_greedy = self.epsilon_min + \
max(self.epsilon_decay - num_of_trials, 0) / \
self.epsilon_decay * (1 - self.epsilon_min)
if num_of_trials % self.update_interval == 0:
self.optimizer.train_one_step(sess,
num_of_trials,
self.batch_size)
state = self.replay_memory.phi(frame)
action = self.choose_action(sess, state, epsilon_greedy)
r, new_frame, termination = self.play(action)
self.replay_memory.add(frame, action, r, termination)
frame = new_frame
if num_of_trials % self.time_between_two_copies == 0:
self.update_target_network(sess)
self.save(sess, saver)
if self.callback:
self.callback()
if termination:
score = self.game.get_total_reward()
summary_str = sess.run(self.summary_op,
feed_dict={self.t_score: score})
self.summary_writer.add_summary(summary_str,
num_of_trials)
self.summary_writer.flush()
break
这个函数很容易理解。在每一轮中,它调用 replay_memory.phi 来获取当前状态,并通过 choose_action 函数选择一个动作,该函数使用 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/ced55201-ffc9-4a33-a0a0-71ec35883db7.png贪婪策略。这个动作通过调用 play 函数提交到 Atari 模拟器,后者返回相应的奖励、下一帧图像和终止信号。然后,过渡(当前帧图像、动作、奖励、终止)被存储到回放记忆中。对于每一个 update_interval 步骤(默认 update_interval = 1),Q 网络会用从回放记忆中随机采样的一批过渡数据进行训练。对于每 time_between_two_copies 步骤,目标网络会复制 Q 网络,并将 Q 网络的权重保存到硬盘。
在训练步骤之后,可以调用以下函数来评估 AI 玩家表现:
def evaluate(self, sess):
for episode in range(self.n_episode):
self.game.reset()
frame = self.game.get_current_feedback()
for _ in range(self.num_nullops):
r, new_frame, termination = self.play(action=0)
self.replay_memory.add(frame, 0, r, termination)
frame = new_frame
for _ in range(self.config['T']):
state = self.replay_memory.phi(frame)
action = self.choose_action(sess, state, self.epsilon_min)
r, new_frame, termination = self.play(action)
self.replay_memory.add(frame, action, r, termination)
frame = new_frame
if self.callback:
self.callback()
if termination:
break
现在,我们准备好训练我们的第一个 Atari 游戏 AI 玩家了。如果你理解算法背后的直觉,实现并不难,不是吗?现在是时候运行程序,见证魔力了!
实验
深度 Q 学习算法的完整实现可以从 GitHub 下载(链接 xxx)。要训练我们的 Breakout AI 玩家,请在 src 文件夹下运行以下命令:
python train.py -g Breakout -d gpu
train.py 中有两个参数。一个是 -g 或 --game,表示要测试的游戏名称。另一个是 -d 或 --device,指定要使用的设备(CPU 或 GPU)来训练 Q 网络。
对于 Atari 游戏,即使使用高端 GPU,也需要 4-7 天才能让我们的 AI 玩家达到人类水平的表现。为了快速测试算法,实现了一个名为 demo 的特殊游戏作为轻量级基准。可以通过以下方式运行 demo:
python train.py -g demo -d cpu
演示游戏基于网站上的 GridWorld 游戏,网址为 cs.stanford.edu/people/karpathy/convnetjs/demo/rldemo.html:
在这个游戏中,2D 网格世界中的一个机器人有九只眼睛,分别指向不同的角度,每只眼睛沿着其方向感知三个值:距离墙壁的距离、距离绿豆的距离或距离红豆的距离。它通过使用五个不同的动作之一来导航,每个动作使它转向不同的角度。它吃到绿豆时会获得正奖励(+1),而吃到红豆时会获得负奖励(-1)。目标是在一次游戏中尽可能多地吃绿豆。
训练将持续几分钟。在训练过程中,你可以打开一个新的终端,输入以下命令来可视化 Q 网络的架构和训练过程:
tensorboard --logdir=log/demo/train
这里,logdir 指向存储示例日志文件的文件夹。一旦 TensorBoard 启动,你可以在浏览器中输入 localhost:6006 来查看 TensorBoard:
这两张图分别绘制了损失和得分与训练步数的关系。显然,经过 10 万步训练后,机器人表现变得稳定,例如得分约为 40。
你还可以通过 TensorBoard 可视化 Q 网络的权重。更多详情,请访问 TensorBoard 指南 www.tensorflow.org/programmers_guide/summaries_and_tensorboard。这个工具对于调试和优化代码非常有用,特别是像 DQN 这样复杂的算法。
概述
恭喜你!你刚刚学习了四个重要的内容。第一个是如何使用 gym 实现一个 Atari 游戏模拟器,并如何玩 Atari 游戏来放松和娱乐。第二个是你学会了如何在强化学习任务中处理数据,比如 Atari 游戏。对于实际的机器学习应用,你将花费大量时间去理解和优化数据,而数据对 AI 系统的表现有很大的影响。第三个是深度 Q 学习算法。你了解了它的直觉,比如为什么需要重放记忆,为什么需要目标网络,更新规则的来源等等。最后一个是你学会了如何使用 TensorFlow 实现 DQN,并且如何可视化训练过程。现在,你已经准备好深入探讨我们在接下来章节中要讨论的更高级的主题。
在下一章,你将学习如何模拟经典的控制任务,以及如何实现最先进的演员-评论家算法来进行控制。
第四章:控制任务的仿真
在上一章中,我们看到了深度 Q 学习(DQN)在训练 AI 代理玩 Atari 游戏中的显著成功。DQN 的一个局限性是,动作空间必须是离散的,也就是说,代理可以选择的动作数量是有限的,并且动作的总数不能太大。然而,许多实际任务需要连续动作,这使得 DQN 难以应用。在这种情况下,对 DQN 的一种简单补救方法是将连续动作空间离散化。但由于维度灾难,这种补救方法并不起作用,意味着 DQN 很快变得不可行,并且无法很好地泛化。
本章将讨论用于具有连续动作空间的控制任务的深度强化学习算法。首先将介绍几个经典控制任务,如 CartPole、Pendulum 和 Acrobot。你将学习如何使用 Gym 仿真这些任务,并理解每个任务的目标和奖励。接下来,将介绍一种基本的演员-评论家算法,称为确定性策略梯度(DPG)。你将学习什么是演员-评论家架构,为什么这类算法能够处理连续控制任务。除此之外,你还将学习如何通过 Gym 和 TensorFlow 实现 DPG。最后,将介绍一种更高级的算法,称为信任区域策略优化(TRPO)。你将理解为什么 TRPO 的表现比 DPG 更好,以及如何通过应用共轭梯度法来学习策略。
本章需要一些数学编程和凸/非凸优化的背景知识。别担心,我们会一步步地讨论这些算法,确保你能充分理解它们背后的机制。理解它们为何有效、何时无法工作以及它们的优缺点,比仅仅知道如何用 Gym 和 TensorFlow 实现它们要重要得多。完成本章后,你将明白深度强化学习的魔法表演是由数学和深度学习共同指导的。
本章将涵盖以下主题:
-
经典控制任务介绍
-
确定性策略梯度方法
-
复杂控制任务的信任区域策略优化
控制任务介绍
OpenAI Gym 提供了经典强化学习文献中的经典控制任务。这些任务包括 CartPole、MountainCar、Acrobot 和 Pendulum。欲了解更多信息,请访问 OpenAI Gym 网站:gym.openai.com/envs/#classic_control。此外,Gym 还提供了在流行物理模拟器 MuJoCo 中运行的更复杂的连续控制任务。MuJoCo 的主页地址是:www.mujoco.org/。MuJoCo 代表多关节动力学与接触,是一个用于机器人学、图形学和动画研究与开发的物理引擎。Gym 提供的任务包括 Ant、HalfCheetah、Hopper、Humanoid、InvertedPendulum、Reacher、Swimmer 和 Walker2d。这些名字很难理解,不是吗?有关这些任务的更多细节,请访问以下链接:gym.openai.com/envs/#mujoco.
快速入门
如果你没有完整安装 OpenAI Gym,可以按如下方式安装classic_control和mujoco环境的依赖:
pip install gym[classic_control]
pip install gym[mujoco]
MuJoCo 不是开源的,因此你需要按照mujoco-py中的说明(可在github.com/openai/mujoco-py#obtaining-the-binaries-and-license-key获取)进行设置。安装经典控制环境后,尝试以下命令:
import gym
atari = gym.make('Acrobot-v1')
atari.reset()
atari.render()
如果成功运行,一个小窗口将弹出,显示 Acrobot 任务的屏幕:
除了 Acrobot,你可以将Acrobot-v1任务名称替换为CartPole-v0、MountainCarContinuous-v0和Pendulum-v0,以查看其他控制任务。你可以运行以下代码来模拟这些任务,并尝试对它们的物理属性有一个高层次的理解:
import gym
import time
def start(task_name):
task = gym.make(task_name)
observation = task.reset()
while True:
task.render()
action = task.env.action_space.sample()
observation, reward, done, _ = task.step(action)
print("Action {}, reward {}, observation {}".format(action, reward, observation))
if done:
print("Game finished")
break
time.sleep(0.05)
task.close()
if __name__ == "__main__":
task_names = ['CartPole-v0', 'MountainCarContinuous-v0',
'Pendulum-v0', 'Acrobot-v1']
for task_name in task_names:
start(task_name)
Gym 使用相同的接口来处理所有任务,包括 Atari 游戏、经典控制任务和 MuJoCo 控制任务。在每一步中,动作是从动作空间中随机抽取的,方法是调用task.env.action_space.sample(),然后这个动作通过task.step(action)提交给模拟器,告诉模拟器执行该动作。step函数返回与该动作对应的观察值和奖励。
经典控制任务
我们现在将详细介绍每个控制任务,并回答以下问题:
-
控制输入是什么?对应的反馈是什么?
-
奖励函数是如何定义的?
-
动作空间是连续的还是离散的?
理解这些控制任务的细节对于设计合适的强化学习算法非常重要,因为它们的规格(例如动作空间的维度和奖励函数)会对性能产生很大影响。
CartPole 是控制和强化学习领域非常著名的控制任务。Gym 实现了由Barto, Sutton, 和 Anderson在其论文《Neuronlike Adaptive Elements That Can Solve Difficult Learning Control Problem》(1983 年)中描述的 CartPole 系统。在 CartPole 中,一根杆子通过一个没有驱动的关节连接到一辆小车上,小车在一条无摩擦轨道上移动,如下所示:
下面是 CartPole 的规格:
| 目标 | 目标是防止杆子倒下。 |
|---|---|
| 动作 | 动作空间是离散的,即通过对小车施加+1(右方向)和-1(左方向)的力量来控制系统。 |
| 观察 | 观察值是一个包含四个元素的向量,例如[ 0.0316304, -0.1893631, -0.0058115, 0.27025422],表示杆子和小车的位置。 |
| 奖励 | 每当杆子保持竖直时,都会获得+1 的奖励。 |
| 终止条件 | 当杆子的角度超过 15 度或小车移动超过 2.4 个单位时,回合结束。 |
因为本章讲解的是解决连续控制任务,接下来我们将设计一个包装器用于 CartPole,将其离散的动作空间转换为连续的动作空间。
MountainCar 首次由 Andrew Moore 在其博士论文《A. Moore, Efficient Memory-Based Learning for Robot Control》中描述,该论文于 1990 年发表,广泛应用于控制、马尔科夫决策过程(MDP)和强化学习算法的基准测试。在 MountainCar 中,一辆小车在一条一维轨道上移动,在两座山之间,试图到达黄色旗帜,如下所示:
以下表格提供了其规格:
| 目标 | 目标是到达右侧山顶。然而,小车的发动机不足以一次性爬上山顶。因此,成功的唯一方式是前后行驶以积累动能。 |
|---|---|
| 动作 | 动作空间是连续的。输入的动作是施加于小车的发动机力量。 |
| 观察 | 观察值是一个包含两个元素的向量,例如[-0.46786288, -0.00619457],表示小车的速度和位置。 |
| 奖励 | 奖励越大,表示用更少的能量达成目标。 |
| 终止条件 | 回合在小车到达目标旗帜或达到最大步数时结束。 |
Pendulum 摆动问题是控制文献中的经典问题,并作为测试控制算法的基准。在 Pendulum 中,一根杆子固定在一个支点上,如下所示:
下面是 Pendulum 的规格:
| 目标 | 目标是将杆子摆起并保持竖直,防止其倒下。 |
|---|---|
| 动作 | 动作空间是连续的。输入动作是施加在杆上的扭矩。 |
| 观察 | 观察是一个包含三个元素的向量,例如,[-0.19092327, 0.98160496, 3.36590881],表示杆的角度和角速度。 |
| 奖励 | 奖励由一个函数计算,该函数的输入包括角度、角速度和扭矩。 |
| 终止 | 当达到最大步数时,任务结束。 |
Acrobot 最早由 Sutton 在 1996 年的论文《强化学习中的泛化:使用稀疏粗编码的成功案例》中描述。Acrobot 系统包含两个关节和两个链接,其中两个链接之间的关节是驱动的:
以下是 Acrobot 的设置:
| 目标 | 目标是将下部链接的末端摆动到指定高度。 |
|---|---|
| 动作 | 动作空间是离散的,也就是说,系统是通过向链接施加 0、+1 和-1 的扭矩来控制的。 |
| 观察 | 观察是一个包含六个元素的向量,例如,[0.9926474, 0.12104186, 0.99736744, -0.07251337, 0.47965018, -0.31494488],表示两个链接的位置。 |
| 奖励 | 当下部链接位于给定高度时,每一步将获得+1 奖励,否则为-1 奖励。 |
| 终止 | 当下部链接的末端达到指定高度,或达到最大步数时,本轮任务结束。 |
请注意,在 Gym 中,CartPole 和 Acrobot 都有离散的动作空间,这意味着这两个任务可以通过应用深度 Q 学习算法来解决。由于本章讨论的是连续控制任务,我们需要将它们的动作空间转换为连续的。以下类为 Gym 经典控制任务提供了一个包装器:
class Task:
def __init__(self, name):
assert name in ['CartPole-v0', 'MountainCar-v0',
'Pendulum-v0', 'Acrobot-v1']
self.name = name
self.task = gym.make(name)
self.last_state = self.reset()
def reset(self):
state = self.task.reset()
self.total_reward = 0
return state
def play_action(self, action):
if self.name not in ['Pendulum-v0', 'MountainCarContinuous-v0']:
action = numpy.fmax(action, 0)
action = action / numpy.sum(action)
action = numpy.random.choice(range(len(action)), p=action)
else:
low = self.task.env.action_space.low
high = self.task.env.action_space.high
action = numpy.fmin(numpy.fmax(action, low), high)
state, reward, done, _ = self.task.step(action)
self.total_reward += reward
termination = 1 if done else 0
return reward, state, termination
def get_total_reward(self):
return self.total_reward
def get_action_dim(self):
if self.name not in ['Pendulum-v0', 'MountainCarContinuous-v0']:
return self.task.env.action_space.n
else:
return self.task.env.action_space.shape[0]
def get_state_dim(self):
return self.last_state.shape[0]
def get_activation_fn(self):
if self.name not in ['Pendulum-v0', 'MountainCarContinuous-v0']:
return tf.nn.softmax
else:
return None
对于 CartPole 和 Acrobot,输入动作应该是一个概率向量,表示选择每个动作的概率。在play_action函数中,动作会根据这个概率向量随机采样并提交给系统。get_total_reward函数返回一个回合中的总奖励。get_action_dim和get_state_dim函数分别返回动作空间和观察空间的维度。get_activation_fn函数用于演员网络的输出层,我们将在后续讨论中提到。
确定性策略梯度
正如前一章所讨论的,DQN 使用 Q 网络来估计 状态-动作值 函数,该函数对每个可用动作都有一个单独的输出。因此,由于动作空间是连续的,Q 网络无法应用。细心的读者可能还记得,Q 网络的另一种架构是将状态和动作作为输入,输出相应的 Q 值估计。这种架构不要求可用动作的数量是有限的,并且能够处理连续的输入动作:
如果我们使用这种网络来估计 状态-动作值 函数,那么必定还需要另一个网络来定义智能体的行为策略,即根据观察到的状态输出合适的动作。事实上,这正是演员-评论员强化学习算法的直观理解。演员-评论员架构包含两个部分:
-
演员:演员定义了智能体的行为策略。在控制任务中,它根据系统的当前状态输出控制信号。
-
评论员:评论员估计当前策略的 Q 值。它可以判断策略是否优秀。
因此,如果演员和评论员都能像在 DQN 中训练 Q 网络一样,利用系统反馈(状态、奖励、下一个状态、终止信号)进行训练,那么经典的控制任务就能得到解决。那么我们该如何训练它们呢?
策略梯度背后的理论
一种解决方案是 深度确定性策略梯度 (DDPG) 算法,它将演员-评论员方法与 DQN 成功的经验相结合。相关论文如下:
-
D. Silver, G. Lever, N. Heess, T. Degris, D. Wierstra 和 M. Riedmiller. 确定性策略梯度算法. 见于 ICML,2014。
-
T. P. Lillicrap, J. J. Hunt, A. Pritzel, N. Heess, T. Erez, Y. Tassa, D. Silver 和 D. Wierstra. 深度强化学习中的连续控制. 见于 ICLR,2016。
引入 DDPG 的原因首先是它与 DQN 非常相似,因此在完成前一章内容后,你可以更轻松地理解其背后的机制。回顾一下,DQN 能够以稳定且健壮的方式训练 Q 网络,原因如下:
-
Q 网络通过从回放记忆中随机抽取样本进行训练,以最小化样本之间的相关性。
-
使用目标网络来估计目标 Q 值,从而减少策略发生振荡或发散的概率。DDPG 采用了相同的策略,这意味着 DDPG 也是一种无模型且离线的算法。
我们在强化学习设置中使用与上一章相同的符号。在每个时间步 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/0213555b-9970-4ebb-a299-93cecc221371.png,智能体观察状态 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/37a22f30-aa50-4a09-bb47-2e17e53160d9.png,采取动作 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/3c05b5fd-7a4c-4908-bf33-124ab777c762.png,然后从函数 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/87145533-c582-40ee-8cda-be95c2ccc08c.png 生成的奖励 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/fc6a21d6-8165-4005-a792-6d83e1f304db.png 中获得相应的回报。与使用 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/f25863dd-e76e-4b76-9500-1e1d5e13c773.png 表示状态 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/dd850911-cd3e-4b3a-be3d-0c9f40a99565.png 下所有可用动作的集合不同,这里我们使用 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/77bd2c0f-79ee-4a83-b663-17c921f87f38.png 来表示智能体的策略,它将状态映射到动作的概率分布。许多强化学习方法,如 DQN,都使用贝尔曼方程作为基础:
这个公式与 DQN 中的公式唯一的区别是这里的策略 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/2735e862-bbe8-4a3b-a3cf-4e13d9f6316a.png 是随机的,因此对 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/9b74ca4d-4795-42e5-9963-782190843add.png 的期望是通过 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/63b79d7f-ff2c-4b13-b7d9-67134f921b91.png 来计算的。如果目标策略 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/df3852c6-b7d4-48f2-906c-85e20d45b6d5.png 是确定性的,可以通过函数 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/03d19098-5a51-4ecf-9349-97d235f0572c.png 来描述,那么就可以避免这个内部的期望:
期望仅依赖于环境。这意味着可以使用 状态-动作值 函数 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/0e72e393-18cf-49e8-987b-1665d38289b7.png 进行离策略学习,使用从其他策略生成的转换,就像我们在 DQN 中做的那样。函数 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/11aa1617-ea61-409b-a303-bf28c1aa4cb0.png 作为评估器,可以通过神经网络进行近似,该神经网络由 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/d3a5c035-972e-4dda-8af5-497e2bde1689.png 参数化,而策略 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/bf619c4b-02a7-4134-a83e-c7c5f7740445.png 作为演员,也可以通过另一神经网络来表示,且该神经网络由 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/0beff831-f282-44be-a4c1-fa3abdc824d9.png 参数化(在 DQN 中,https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/afb1eec9-f424-4f1b-a510-4e582f3dfa11.png 只是 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/b7c37d7d-ea25-475a-9b07-4e839a6024fa.png)。然后,可以通过最小化以下损失函数来训练评估器 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/e84ee47b-b0f0-4ab4-9c06-807b68db4548.png:
这里,https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/373f36de-a6b8-4fdd-9f4c-199efec86702.png。如同 DQN 中一样,https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/db4d322c-2054-423a-8834-466d98288b33.png 可以通过目标网络来估计,而用于近似 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/761e8806-ef76-48da-ac45-a8f533f2277e.png 的样本可以从重放记忆中随机抽取。
为了训练演员 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/65c68316-35e0-4352-b0bd-4512ce079d3a.png,我们通过最小化损失函数 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/51ce9801-6891-4d3b-aa28-5fcd61d91376.png来固定评论员 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/baeaa310-3fb3-4cfd-9786-5b9595570c8b.png,并尝试最大化 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/8a50c452-1c5f-41b1-8dc7-8f01a0a8686d.png相对于 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/f20dd0ca-a41a-4750-95d3-86434c885672.png的值,因为较大的 Q 值意味着更好的策略。这可以通过应用链式法则来计算相对于演员参数的期望回报:
以下图展示了 DDPG 的高层架构:
与 DQN 相比,更新目标网络的方式有所不同。不是在几次迭代后直接将 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/83eb027c-4020-42e0-8222-63ccf5903e5b.png的权重复制到目标网络,而是使用软更新:
这里, https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/4aa85430-4001-4131-b53c-e9d49369d0ee.png表示目标网络的权重。这个更新意味着目标值的变化受到约束,变化速度较慢,从而大大提高了学习的稳定性。这个简单的变化将学习值函数的相对不稳定问题,拉近到了监督学习的情况。
与 DQN 类似,DDPG 在训练过程中也需要平衡探索和开发。由于由策略生成的动作 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/9df779a4-c193-41f5-8bd6-380ca55498d2.png是连续的,因此无法应用 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/9a1c3ea8-ddcc-4e58-ad65-ec7cae3f99d7.png-贪婪策略。相反,我们可以通过向演员策略 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/f0bfd658-9b98-4c21-a175-efd17e3715b3.png中加入从分布 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/bf171446-b357-49f3-a646-c1cdb3b58a70.png中采样的噪声来构造一个探索策略 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/d75015e2-a728-46e8-9712-98640b2121d9.png:
https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/dde2dd51-dbd4-48c5-98d5-8cfab67139c5.png 其中 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/3d8a98da-c78f-43e5-b978-ca2258dddb19.png
https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/4d4c9de1-140a-446c-a4c9-77d2d42b8185.png 可以选择为 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/90caf88c-219a-40e7-8fe4-12dc270d38ce.png,其中 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/c8166a78-9468-4ac9-b5d1-b82e4e5e753c.png是标准高斯分布,且 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/a1072a0b-8629-45ef-a059-7c0de83c8a87.png在每个训练步骤中递减。另一种选择是应用奥恩斯坦-乌伦贝克过程生成探索噪声 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/d90e6e65-59e3-447f-9a31-42a692177ee9.png。
DPG 算法
以下伪代码展示了 DDPG 算法:
Initialize replay memory to capacity ;
Initialize the critic network and actor network with random weights and ;
Initialize the target networks and with weights and ;
Repeat for each episode:
Set time step ;
Initialize a random process for action exploration noise;
Receive an initial observation state ;
While the terminal state hasn't been reached:
Select an action according to the current policy and exploration noise;
Execute action in the simulator and observe reward and the next state ;
Store transition into replay memory ;
Randomly sample a batch of transitions from ;
Set if is a terminal state or if is a non-terminal state;
Update critic by minimizing the loss:
;
Update the actor policy using the sampled policy gradient:
;
Update the target networks:
,
;
End while
通过将用于近似演员和评论员的前馈神经网络替换为递归神经网络,DDPG 自然扩展出了一个变体。这个扩展被称为递归确定性策略梯度算法(RDPG),在论文《N. Heess, J. J. Hunt, T. P. Lillicrap 和 D. Silver. 基于记忆的控制与递归神经网络》中有讨论,发表于 2015 年。
循环评论家和演员通过时间反向传播(BPTT)进行训练。对于有兴趣的读者,可以从arxiv.org/abs/1512.04455下载相关论文。
DDPG 实现
本节将向你展示如何使用 TensorFlow 实现演员-评论家架构。代码结构几乎与上一章展示的 DQN 实现相同。
ActorNetwork是一个简单的多层感知机(MLP),它将观测状态作为输入:
class ActorNetwork:
def __init__(self, input_state, output_dim, hidden_layers, activation=tf.nn.relu):
self.x = input_state
self.output_dim = output_dim
self.hidden_layers = hidden_layers
self.activation = activation
with tf.variable_scope('actor_network'):
self.output = self._build()
self.vars = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES,
tf.get_variable_scope().name)
def _build(self):
layer = self.x
init_b = tf.constant_initializer(0.01)
for i, num_unit in enumerate(self.hidden_layers):
layer = dense(layer, num_unit, init_b=init_b, name='hidden_layer_{}'.format(i))
output = dense(layer, self.output_dim, activation=self.activation, init_b=init_b, name='output')
return output
构造函数需要四个参数:input_state、output_dim、hidden_layers和activation。input_state是观测状态的张量。output_dim是动作空间的维度。hidden_layers指定隐藏层的数量和每层的单元数。activation表示输出层的激活函数。
CriticNetwork也是一个多层感知机(MLP),足以应对经典控制任务:
class CriticNetwork:
def __init__(self, input_state, input_action, hidden_layers):
assert len(hidden_layers) >= 2
self.input_state = input_state
self.input_action = input_action
self.hidden_layers = hidden_layers
with tf.variable_scope('critic_network'):
self.output = self._build()
self.vars = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES,
tf.get_variable_scope().name)
def _build(self):
layer = self.input_state
init_b = tf.constant_initializer(0.01)
for i, num_unit in enumerate(self.hidden_layers):
if i != 1:
layer = dense(layer, num_unit, init_b=init_b, name='hidden_layer_{}'.format(i))
else:
layer = tf.concat([layer, self.input_action], axis=1, name='concat_action')
layer = dense(layer, num_unit, init_b=init_b, name='hidden_layer_{}'.format(i))
output = dense(layer, 1, activation=None, init_b=init_b, name='output')
return tf.reshape(output, shape=(-1,))
网络将状态和动作作为输入。它首先将状态映射为一个隐藏特征表示,然后将该表示与动作进行拼接,接着通过几个隐藏层。输出层生成与输入对应的 Q 值。
演员-评论家网络将演员网络和评论家网络结合在一起:
class ActorCriticNet:
def __init__(self, input_dim, action_dim,
critic_layers, actor_layers, actor_activation,
scope='ac_network'):
self.input_dim = input_dim
self.action_dim = action_dim
self.scope = scope
self.x = tf.placeholder(shape=(None, input_dim), dtype=tf.float32, name='x')
self.y = tf.placeholder(shape=(None,), dtype=tf.float32, name='y')
with tf.variable_scope(scope):
self.actor_network = ActorNetwork(self.x, action_dim,
hidden_layers=actor_layers,
activation=actor_activation)
self.critic_network = CriticNetwork(self.x,
self.actor_network.get_output_layer(),
hidden_layers=critic_layers)
self.vars = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES,
tf.get_variable_scope().name)
self._build()
def _build(self):
value = self.critic_network.get_output_layer()
actor_loss = -tf.reduce_mean(value)
self.actor_vars = self.actor_network.get_params()
self.actor_grad = tf.gradients(actor_loss, self.actor_vars)
tf.summary.scalar("actor_loss", actor_loss, collections=['actor'])
self.actor_summary = tf.summary.merge_all('actor')
critic_loss = 0.5 * tf.reduce_mean(tf.square((value - self.y)))
self.critic_vars = self.critic_network.get_params()
self.critic_grad = tf.gradients(critic_loss, self.critic_vars)
tf.summary.scalar("critic_loss", critic_loss, collections=['critic'])
self.critic_summary = tf.summary.merge_all('critic')
构造函数需要六个参数,分别为:input_dim和action_dim是状态空间和动作空间的维度,critic_layers和actor_layers指定评论家网络和演员网络的隐藏层,actor_activation表示演员网络输出层的激活函数,scope是用于scope TensorFlow 变量的作用域名称。
构造函数首先创建一个self.actor_network演员网络的实例,输入为self.x,其中self.x表示当前状态。然后,它使用以下输入创建评论家网络的实例:self.actor_network.get_output_layer()作为演员网络的输出,self.x作为当前状态。给定这两个网络,构造函数调用self._build()来构建我们之前讨论过的演员和评论家的损失函数。演员损失是-tf.reduce_mean(value),其中value是评论家网络计算的 Q 值。评论家损失是0.5 * tf.reduce_mean(tf.square((value - self.y))),其中self.y是由目标网络计算的预测目标值的张量。
ActorCriticNet类提供了在给定当前状态的情况下计算动作和 Q 值的功能,即get_action和get_value。它还提供了get_action_value,该函数根据当前状态和代理执行的动作计算状态-动作值函数:
class ActorCriticNet:
def get_action(self, sess, state):
return self.actor_network.get_action(sess, state)
def get_value(self, sess, state):
return self.critic_network.get_value(sess, state)
def get_action_value(self, sess, state, action):
return self.critic_network.get_action_value(sess, state, action)
def get_actor_feed_dict(self, state):
return {self.x: state}
def get_critic_feed_dict(self, state, action, target):
return {self.x: state, self.y: target,
self.critic_network.input_action: action}
def get_clone_op(self, network, tau=0.9):
update_ops = []
new_vars = {v.name.replace(network.scope, ''): v for v in network.vars}
for v in self.vars:
u = (1 - tau) * v + tau * new_vars[v.name.replace(self.scope, '')]
update_ops.append(tf.assign(v, u))
return update_ops
由于 DPG 与 DQN 的架构几乎相同,本章没有展示回放记忆和优化器的实现。欲了解更多细节,您可以参考前一章或访问我们的 GitHub 仓库(github.com/PacktPublishing/Python-Reinforcement-Learning-Projects)。通过将这些模块结合在一起,我们可以实现用于确定性策略梯度算法的DPG类:
class DPG:
def __init__(self, config, task, directory, callback=None, summary_writer=None):
self.task = task
self.directory = directory
self.callback = callback
self.summary_writer = summary_writer
self.config = config
self.batch_size = config['batch_size']
self.n_episode = config['num_episode']
self.capacity = config['capacity']
self.history_len = config['history_len']
self.epsilon_decay = config['epsilon_decay']
self.epsilon_min = config['epsilon_min']
self.time_between_two_copies = config['time_between_two_copies']
self.update_interval = config['update_interval']
self.tau = config['tau']
self.action_dim = task.get_action_dim()
self.state_dim = task.get_state_dim() * self.history_len
self.critic_layers = [50, 50]
self.actor_layers = [50, 50]
self.actor_activation = task.get_activation_fn()
self._init_modules()
这里,config包含 DPG 的所有参数,例如训练时的批量大小和学习率。task是某个经典控制任务的实例。在构造函数中,回放记忆、Q 网络、目标网络和优化器通过调用_init_modules函数进行初始化:
def _init_modules(self):
# Replay memory
self.replay_memory = ReplayMemory(history_len=self.history_len,
capacity=self.capacity)
# Actor critic network
self.ac_network = ActorCriticNet(input_dim=self.state_dim,
action_dim=self.action_dim,
critic_layers=self.critic_layers,
actor_layers=self.actor_layers,
actor_activation=self.actor_activation,
scope='ac_network')
# Target network
self.target_network = ActorCriticNet(input_dim=self.state_dim,
action_dim=self.action_dim,
critic_layers=self.critic_layers,
actor_layers=self.actor_layers,
actor_activation=self.actor_activation,
scope='target_network')
# Optimizer
self.optimizer = Optimizer(config=self.config,
ac_network=self.ac_network,
target_network=self.target_network,
replay_memory=self.replay_memory)
# Ops for updating target network
self.clone_op = self.target_network.get_clone_op(self.ac_network, tau=self.tau)
# For tensorboard
self.t_score = tf.placeholder(dtype=tf.float32, shape=[], name='new_score')
tf.summary.scalar("score", self.t_score, collections=['dpg'])
self.summary_op = tf.summary.merge_all('dpg')
def choose_action(self, sess, state, epsilon=0.1):
x = numpy.asarray(numpy.expand_dims(state, axis=0), dtype=numpy.float32)
action = self.ac_network.get_action(sess, x)[0]
return action + epsilon * numpy.random.randn(len(action))
def play(self, action):
r, new_state, termination = self.task.play_action(action)
return r, new_state, termination
def update_target_network(self, sess):
sess.run(self.clone_op)
choose_action函数根据当前演员-评论员网络的估计和观察到的状态选择一个动作。
请注意,添加了由epsilon控制的高斯噪声以进行探索。
play函数将一个动作提交给模拟器,并返回模拟器的反馈。update_target_network函数从当前的演员-评论员网络中更新目标网络。
为了开始训练过程,可以调用以下函数:
def train(self, sess, saver=None):
num_of_trials = -1
for episode in range(self.n_episode):
frame = self.task.reset()
for _ in range(self.history_len+1):
self.replay_memory.add(frame, 0, 0, 0)
for _ in range(self.config['T']):
num_of_trials += 1
epsilon = self.epsilon_min + \
max(self.epsilon_decay - num_of_trials, 0) / \
self.epsilon_decay * (1 - self.epsilon_min)
if num_of_trials % self.update_interval == 0:
self.optimizer.train_one_step(sess, num_of_trials, self.batch_size)
state = self.replay_memory.phi(frame)
action = self.choose_action(sess, state, epsilon)
r, new_frame, termination = self.play(action)
self.replay_memory.add(frame, action, r, termination)
frame = new_frame
if num_of_trials % self.time_between_two_copies == 0:
self.update_target_network(sess)
self.save(sess, saver)
if self.callback:
self.callback()
if termination:
score = self.task.get_total_reward()
summary_str = sess.run(self.summary_op, feed_dict={self.t_score: score})
self.summary_writer.add_summary(summary_str, num_of_trials)
self.summary_writer.flush()
break
在每一轮中,它调用replay_memory.phi来获取当前状态,并调用choose_action函数根据当前状态选择一个动作。然后,通过调用play函数将该动作提交给模拟器,该函数返回相应的奖励、下一个状态和终止信号。接着,(当前状态, 动作, 奖励, 终止)的转移将被存储到回放记忆中。每当update_interval步(默认为update_interval = 1)时,演员-评论员网络会通过从回放记忆中随机抽取的一批转移进行训练。每当经过time_between_two_copies步时,目标网络将更新,并将 Q 网络的权重保存到硬盘。
在训练步骤之后,可以调用以下函数来评估我们训练好的代理的表现:
def evaluate(self, sess):
for episode in range(self.n_episode):
frame = self.task.reset()
for _ in range(self.history_len+1):
self.replay_memory.add(frame, 0, 0, 0)
for _ in range(self.config['T']):
print("episode {}, total reward {}".format(episode,
self.task.get_total_reward()))
state = self.replay_memory.phi(frame)
action = self.choose_action(sess, state, self.epsilon_min)
r, new_frame, termination = self.play(action)
self.replay_memory.add(frame, action, r, termination)
frame = new_frame
if self.callback:
self.callback()
if termination:
break
实验
DPG 的完整实现可以从我们的 GitHub 仓库下载(github.com/PacktPublishing/Python-Reinforcement-Learning-Projects)。要训练一个用于 CartPole 的代理,请在src文件夹下运行以下命令:
python train.py -t CartPole-v0 -d cpu
train.py有两个参数。一个是-t或--task,表示您要测试的经典控制任务的名称。另一个是-d或--device,指定您要使用的设备(CPU 或 GPU)来训练演员-评论员网络。由于这些经典控制任务的状态空间维度相较于 Atari 环境较低,使用 CPU 训练代理已经足够快速,通常只需几分钟即可完成。
在训练过程中,你可以打开一个新的终端并输入以下命令,以可视化演员-评论员网络的架构和训练过程:
tensorboard --logdir=log/CartPole-v0/train
这里,logdir 指向存储 CartPole-v0 日志文件的文件夹。TensorBoard 运行后,打开浏览器并导航到 localhost:6006 来查看 TensorBoard:
Tensorboard 视图
前两个图显示了演员损失和评论员损失相对于训练步骤的变化。对于经典控制任务,演员损失通常会持续下降,而评论员损失则会有较大的波动。在 60,000 个训练步骤后,分数变得稳定,达到了 200,这是 CartPole 仿真器中能达到的最高分数。
使用类似的命令,你也可以为 Pendulum 任务训练一个智能体:
python train.py -t Pendulum-v0 -d cpu
然后,通过 Tensorboard 检查训练过程:
tensorboard --logdir=log/Pendulum-v0/train
以下截图显示了训练过程中分数的变化:
训练过程中分数的变化
一位细心的读者可能会注意到,Pendulum 的分数波动比 CartPole 的分数要大。造成这个问题的原因有两个:
-
在 Pendulum 中,杆子的起始位置是不确定的,即它可能在两次尝试之间有所不同
-
DPG 的训练过程可能并不总是稳定的,尤其是对于复杂任务,如 MuJoCo 控制任务
Gym 提供的 MuJoCo 控制任务,例如 Ant、HalfCheetah、Hopper、Humanoid、InvertedPendulum、Reacher、Swimmer 和 Walker2d,具有高维度的状态和动作空间,这使得 DPG 无法正常工作。如果你对在运行 DPG 时 Hopper-v0 任务会发生什么感到好奇,你可以尝试以下操作:
python train.py -t Hopper-v0 -d cpu
几分钟后,你会看到 DPG 无法教会 Hopper 如何行走。DPG 失败的主要原因是,本文讨论的简单的演员和评论员更新在高维输入下变得不稳定。
信任区域策略优化
信任区域策略优化(TRPO)算法旨在解决复杂的连续控制任务,相关论文如下:Schulman, S. Levine, P. Moritz, M. Jordan 和 P. Abbeel. 信任区域策略优化,发表于 ICML,2015。
要理解为什么 TRPO 有效,需要一些数学背景。主要思路是,最好确保由一个训练步骤优化后的新策略 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/31f8d3cd-e898-4f0e-8ba2-b373935baa9e.png,不仅在单调减少优化损失函数(从而改进策略)的同时,还不会偏离之前的策略 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/312b08f4-832f-453a-8d90-e4d0f2d9c0bb.png 太远,这意味着应该对 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/8fe518c7-f75b-44df-9f5b-e0299bf3ee1a.png 和 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/d8d38231-bc91-43c4-a75f-8f3d39b6d8ca.png 之间的差异施加约束,例如对某个约束函数 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/87c66a83-5938-4481-8b63-92a08ddd9b81.png 施加常数 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/5b9d38d9-0b1e-443b-b93f-0b124e2af135.png。
TRPO 背后的理论
让我们来看一下 TRPO 背后的机制。如果你觉得这一部分难以理解,可以跳过它,直接看如何运行 TRPO 来解决 MuJoCo 控制任务。考虑一个无限期折扣的马尔科夫决策过程,记作 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/4382afa1-271c-4016-bcf0-682471689d03.png,其中 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/fa7e1576-fa63-4912-9f28-766c74f39662.png 是状态的有限集合, https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/b8bf6678-7e34-4238-bfae-95ca5d706259.png 是行动的有限集合, https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/ee138461-338b-4461-9c8f-f852cea0201b.png 是转移概率分布, https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/4e72cb1f-a0b0-45c3-96df-eb65d2815c6a.png 是成本函数, https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/8bcde0ac-2124-40cb-ae6b-fbacc977fa36.png 是初始状态的分布, https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/61ab4a6b-a890-4de7-a592-f11acc0c5e6c.png 是折扣因子。设 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/779c9c94-3227-4173-bc9b-47a496df8645.png 为我们希望通过最小化以下预期折扣成本来学习的随机策略:
这里是 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/c6326a20-968a-4c4b-8edc-ef2a78164482.png, https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/4b3f52e1-26c7-469c-84ed-afc5dde95507.png 和 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/687aa323-eee8-4230-8c65-e74bae743a2e.png。在策略 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/f0c57c4c-00ae-41c7-ae5b-504777148477.png 下,状态-行动值函数 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/ec709246-272b-4d6e-a633-8650958a88d8.png,值函数 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/fd5bb2d1-02ca-4612-b937-a28495e2122b.png,以及优势函数 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/e2731530-62ec-47a3-ad8f-0e4f0fa96b4d.png 的定义如下:
https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/1ecdecae-c636-4f18-8a0f-d9f1852b3f55.pnghttps://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/dbe7950e-c726-4be1-ae1d-571772cd0ec3.pnghttps://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/695cfd42-af47-4f7a-a1df-2c19428aabb3.png
这里是 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/8ad044e8-e6bc-40d4-93c0-37581d8858a6.png 和 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/e487be73-6cde-4217-8024-e4f7cd401537.png。
我们的目标是在每个训练步骤中,通过减少预期折扣成本,改善策略 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/19d0debc-2372-4819-a9b2-ac5e04248f11.png。为了设计一个单调改善 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/68b1c497-e041-4391-8a5b-36ea39ab6685.png的算法,让我们考虑以下方程:
这里,表示的是 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/4878a2f8-03b8-4c93-8ca6-ebc08fc5382b.png,https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/311043e8-58ca-4b6b-89e5-f82e712a6f04.png 和 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/324cf7f3-59a2-460b-84a2-efef176891c5.png。这个方程对于任何策略 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/81d9e91f-d378-4c87-a354-414d1ad84942.png 都成立。对于有兴趣了解此方程证明的读者,请参阅 TRPO 论文的附录或 Kakade 和 Langford 撰写的论文《大致最优的近似强化学习》。为了简化这个方程,设 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/10619e12-c8bf-4cf2-84a7-1c4e1cb07ec1.png 为折扣访问频率:
通过将前面的方程重排,以对状态而不是时间步进行求和,它变为以下形式:
从这个方程我们可以看出,任何策略更新 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/fda3a1ca-854b-44d8-a0c6-197d8cc0cb8c.png,只要在每个状态 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/a6e419f2-c2d2-46fe-afef-48570caec744.png 上的期望优势非正,即 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/baf76ede-f391-4ed9-b6e9-605cb166bcdf.png,都能保证减少成本 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/0ffe18a6-a81d-4139-8539-c77c0badf09c.png。因此,对于像 Atari 环境这样的离散动作空间,DQN 中选择的确定性策略 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/88b0ef26-401f-4033-8576-82d6fca72fab.png 保证如果至少有一个状态-动作对的优势值为负且状态访问概率非零,就能改进策略。然而,在实际问题中,特别是当策略通过神经网络近似时,由于近似误差,某些状态的期望优势可能为正。除此之外,https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/5642fe43-339d-46fb-be5d-7b6bd14b1df0.png 对 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/0fc4904e-49e5-447a-a56a-efecaa1ff4a3.png 的依赖使得该方程难以优化,因此 TRPO 考虑通过将 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/d7fce747-17fc-4da3-97d6-20db3c3c9ed9.png 替换为 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/3f64995a-2da0-4ea4-9508-dd5d750197b6.png 来优化以下函数:
Kakade 和 Langford 显示,如果我们有一个参数化策略,https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/8fd694a5-34cc-4604-8330-309b3cae3d44.png,这是参数 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/1fe4211a-0762-4c8e-b964-f82205c58113.png 的可微函数,那么对于任何参数 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/aed1c2ad-56e3-4a6c-b9cc-667860302101.png:
https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/b26d3705-7acd-46e3-95d9-7960a539a17d.pnghttps://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/2c4d94aa-c595-4312-b77b-4f5b5aa2268d.png
这意味着,改进 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/7e29d171-632b-47f7-8fde-aa755abac2ad.png 也会通过对 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/a5a215c0-8b48-4d4c-af73-b0cb2855f812.png 的足够小的更新来改进 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/eb9f53b5-15ee-4804-99a3-d05ae7b70007.png。基于这个思想,Kakade 和 Langford 提出了一个叫做保守策略迭代的策略更新方案:
这里,https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/8e97e940-f4a8-4aca-a389-196e3672e47b.png 是当前策略,https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/0690ebf6-d1d9-4a5d-9975-88e070099654.png 是新策略,https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/656c223d-a53d-466f-9049-cfb3ea1aa43e.png 是通过求解 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/bb69abc4-4efa-431d-bde8-8cb81333e9d9.png 得到的。它们证明了以下更新的界限:
https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/2202bc30-b02d-4b4f-ac00-ccd006344629.png,其中 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/4e8d8a89-687b-4c09-a75b-fd05efbb42bd.png
请注意,这个界限仅适用于通过前述更新生成的混合策略。在 TRPO 中,作者将这个界限扩展到一般的随机策略,而不仅仅是混合策略。其主要思想是用 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/433ef82d-2ae0-4059-a0db-d0a9b5d60936.png 替换混合权重,使用 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/94e130cc-71b0-40ca-b6ad-e962705670f2.png 和 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/a5b4a9a7-64dd-420f-8422-255798c4721b.png 之间的距离度量。一个有趣的距离度量选项是总变差散度。以两个离散分布 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/3234f41f-86a5-4366-9189-5ca28c7c9043.png 和 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/6c78aec7-e723-4044-9371-d99781c5b6ce.png 为例,总变差散度定义如下:
对于策略 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/e1a2e0b2-a80f-4c94-a238-eb5c6a72884b.png 和 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/a5952e1b-fdf3-4d52-952d-08efb4aab567.png,令 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/962c116d-a5af-45c3-9868-2f3ff9c7c69c.png 为所有状态的最大总变差散度:
使用 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/f5ad7c02-2977-4428-92c9-e1d932054ab3.png 和 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/4e930847-34f4-4fd3-bbc6-65f4ec91fd6a.png,可以证明:
https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/eb185217-9538-492f-bffe-32d1307b6d08.png,其中 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/00448aeb-93d9-4f75-bb9c-32de71911cab.png。
实际上,总变差散度可以被 KL 散度上界,即 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/78c2353d-c2a3-4fe1-9e1b-407ac9f8101a.png,这意味着:
https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/2de74f64-0e3f-4130-9b12-48ec49724f87.png,其中 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/055f7bdb-5fcd-4f75-be54-a5033373ef64.png。
TRPO 算法
基于前述的策略改进界限,开发了以下算法:
Initialize policy ;
Repeat for each step :
Compute all advantage values ;
Solve the following optimization problem:
;
Until convergence
在每一步中,该算法最小化 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/af7c9656-8e07-4215-9d38-0f867bd07473.png 的上界,使得:
最后一条等式由此得出,https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/8acb0164-88a5-4075-a1ad-798769022863.png 对于任何策略 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/72c0ceb5-4b1f-4a7b-a34d-74cf20f5ca6e.png 都成立。这意味着该算法保证生成一系列单调改进的策略。
在实践中,由于难以计算 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/08cd6ec4-d66e-4134-b835-13a665021708.png 的确切值,并且很难通过惩罚项来控制每次更新的步长,TRPO 将惩罚项替换为 KL 散度受限于常数 https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/e604b012-bcac-4dca-b9bf-c5d4ac226e43.png 的约束:
但由于约束条件过多,这个问题仍然很难解决。因此,TRPO 使用了一种启发式近似方法,考虑了平均 KL 散度:
这导致了以下优化问题:
换句话说,通过展开https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/fff01813-299b-4fa6-910c-c6bdb438fbaf.png,我们需要解决以下问题:
现在,问题是:我们如何优化这个问题?一个直接的想法是通过模拟策略https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/e6fe0c4f-2dd5-4f93-a676-47a4d0599d9d.png进行若干步,然后使用这些轨迹来逼近该问题的目标函数。由于优势函数https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/d0f93482-d0f8-411f-b9e3-e0ae54566ede.png,我们将目标函数中的https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/bf05b41e-458f-4cbf-ac10-dff5f4e25f56.png替换为 Q 值https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/b9edf522-8800-4336-8ac7-1c14d0aa0bd8.png,这只是使目标函数变化一个常数。此外,注意以下几点:
因此,给定一个在策略https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/17240a16-8143-425d-a94b-3f3029041e9b.png下生成的轨迹https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/c40ea99f-c792-44cb-bef0-1944188cfac8.png,我们将按以下方式进行优化:
对于 MuJoCo 控制任务,策略https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/17f26470-538d-4521-be3f-f3e4ea1ed2b6.png和状态-动作值函数https://github.com/OpenDocCN/freelearn-dl-pt4-zh/raw/master/docs/py-rl-proj/img/21d3f97e-547f-4d2e-903b-9120ab4f820d.png都通过神经网络进行逼近。为了优化这个问题,KL 散度约束可以通过 Fisher 信息矩阵来近似。然后,可以通过共轭梯度算法来求解此问题。有关更多详细信息,您可以从 GitHub 下载 TRPO 的源代码,并查看optimizer.py,该文件实现了使用 TensorFlow 的共轭梯度算法。
MuJoCo 任务实验
Swimmer任务是测试 TRPO 的一个很好的例子。这个任务涉及一个在粘性流体中游泳的三连杆机器人,目标是通过驱动两个关节,使其尽可能快地游向前方(gym.openai.com/envs/Swimmer-v2/)。下面的截图显示了Swimmer在 MuJoCo 模拟器中的样子:
要训练一个Swimmer代理,请在src文件夹下运行以下命令:
CUDA_VISIBLE_DEVICES= python train.py -t Swimmer
train.py中有两个参数。一个是-t,或--task,表示您要测试的 MuJoCo 或经典控制任务的名称。由于这些控制任务的状态空间相比于 Atari 环境维度较低,单独使用 CPU 进行训练就足够了,只需将CUDA_VISIBLE_DEVICES设置为空,这将花费 30 分钟到两小时不等。
在训练过程中,您可以打开一个新的终端,并输入以下命令来可视化训练过程:
tensorboard --logdir=log/Swimmer
在这里,logdir指向存储Swimmer日志文件的文件夹。一旦 TensorBoard 正在运行,使用网页浏览器访问localhost:6006以查看 TensorBoard:
很明显,经过 200 个回合后,每个回合的总奖励变得稳定,即大约为 366。要检查训练后Swimmer如何移动,运行以下命令:
CUDA_VISIBLE_DEVICES= python test.py -t Swimmer
你将看到一个看起来很有趣的Swimmer对象在地面上行走。
总结
本章介绍了 Gym 提供的经典控制任务和 MuJoCo 控制任务。你已经了解了这些任务的目标和规格,以及如何为它们实现一个模拟器。本章最重要的部分是用于连续控制任务的确定性 DPG 和 TRPO。你了解了它们背后的理论,这也解释了它们为什么在这些任务中表现良好。你还学习了如何使用 TensorFlow 实现 DPG 和 TRPO,以及如何可视化训练过程。
在下一章,我们将学习如何将强化学习算法应用于更复杂的任务,例如,玩 Minecraft。我们将介绍异步演员-评论员(A3C)算法,它在复杂任务中比 DQN 更快,并且已广泛应用于许多深度强化学习算法作为框架。
2299

被折叠的 条评论
为什么被折叠?



