人工智能速成课-全-

人工智能速成课(全)

原文:annas-archive.org/md5/8fe4dfb11c5c9418c47bd0627c2859ab

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

你好,数据科学家和 AI 爱好者。多年来,我创建了许多关于人工智能(AI)的在线课程,它们取得了非常大的成功,并为 AI 社区做出了良好的贡献。然而,缺少了一些至关重要的内容。在某个时刻,AI 课程太多了,我的大部分学生开始向我求助,询问该如何学习这些课程。于是,我决定不再给出学习课程的顺序,而是创建一本全方位的 AI 指南,将我课程中所有最好的讲解和实际案例以完美的结构整合在书中。

你看,我的目标是让 AI 普及化,让每个人都意识到 AI 是一种可以带来积极变化的技术,是可以触手可及的。我正尽力将知识传播到全球,帮助人们为 21 世纪的未来工作和机会做好准备。我认为,与其通过数十门难以驾驭的在线课程来学习 AI,人们通过一本随时可以带走的全方位书籍来学习 AI,效率会更高。也就是说,这本书对于那些喜欢并选择在线课程的人来说,也是一个极好的附加资源。

我对这本书的简单期望是,更多的人能够通过这本书以正确的方式学习 AI,从而为他们提供一种高效的替代在线课程的学习方式。我已经成功地将我的培训内容的精华融入到一本书中,今天我非常高兴能够发布它。我真心希望它能帮助更多的人找到梦想工作,打造一份精彩的职业生涯,并为解决 21 世纪的严峻挑战带来美好的解决方案。

本书的目标读者

任何对机器学习、深度学习或 AI 感兴趣的人。

那些不太擅长编程,但对 AI 感兴趣,并希望轻松地将其应用到现实问题中的人。

希望开始从事数据科学或 AI 职业的大学或学院学生。

希望在 AI 领域提升技能的数据分析师。

对自己目前的工作不满意并希望迈出数据科学职业第一步的任何人。

希望通过使用强大的 AI 工具为企业增值的商业主。

渴望学习如何利用 AI 优化业务、最大化盈利并提高效率的企业家。

希望了解可以为员工提供的项目的 AI 从业者。

有抱负的数据科学家,寻找可以添加到自己作品集的商业案例。

对利用机器学习和 AI 解决商业问题感兴趣的技术爱好者。

希望帮助企业转型为 AI 驱动型公司的顾问。

至少具备高中数学知识,想要开始学习 AI 的学生。

本书内容

第一章欢迎来到机器人世界,带你进入人工智能的世界。

第二章发现你的 AI 工具包,揭示了一个易于使用的工具包,包含所有 AI 模型的 Python 文件,借助惊人的 Google Colaboratory 平台,随时可以运行。

第三章Python 基础——学习如何用 Python 编程,提供了正确的 Python 基础知识,并教你如何用 Python 编程。

第四章AI 基础技术,向你介绍了强化学习及其五大基本原则。

第五章你的第一个 AI 模型——小心“强盗”问题!,讲解了多臂老丨虎丨机问题的理论,并介绍了如何通过汤普森采样 AI 模型以最佳方式解决该问题。

第六章销售与广告中的 AI——像 AI 街的狼一样卖货,应用第五章中的汤普森采样 AI 模型,解决与销售和广告相关的实际商业问题。

第七章欢迎进入 Q 学习,介绍了 Q 学习 AI 模型的理论。

第八章物流中的 AI——仓库中的机器人,应用第七章中的 Q 学习 AI 模型,解决与物流优化相关的实际商业问题。

第九章通过人工大脑走向专业——深度 Q 学习,介绍了深度学习的基础和深度 Q 学习 AI 模型的理论。

第十章自动驾驶汽车中的 AI——构建自动驾驶汽车,应用第九章中的深度 Q 学习 AI 模型,构建虚拟的自动驾驶汽车。

第十一章商业中的 AI——通过深度 Q 学习最小化成本,应用第九章中的深度 Q 学习 AI 模型,解决与成本优化相关的实际商业问题。

第十二章深度卷积 Q 学习,介绍了卷积神经网络的基础和深度卷积 Q 学习 AI 模型的理论。

第十三章游戏中的 AI——成为贪吃蛇的高手,应用第十二章中的深度卷积 Q 学习 AI 模型来击败著名的贪吃蛇视频游戏。

第十四章总结与结论,通过回顾如何创建 AI 框架,并在书末提供一些作者关于你在 AI 世界未来的最终话语,总结了本书内容。

为了最大限度地利用本书的内容

  • 在我们开始之前,你不需要了解太多内容;本书包含了理解 AI 模型所需的所有先决条件的复习内容。如果你需要,还可以通过本书中的完整章节学习 Python 基础,帮助你掌握 Python 编程。

  • 不需要进行任何额外的安装,因为本书中的所有实践指导都是从零开始提供的。你只需要准备好你的电脑并打开它。

  • 我建议你在阅读本书时同时打开 Google,以便访问书中提供的链接资源,并更详细地了解本书中 AI 模型背后的数学概念。

下载示例代码文件

您可以从您的账户中下载本书的示例代码文件,访问 www.packtpub.com。如果您在其他地方购买了这本书,可以访问 www.packtpub.com/support 注册,以便我们直接将文件发送到您的邮箱。

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

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

  2. 选择 SUPPORT 标签。

  3. 点击 Code Downloads & Errata

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

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

  • Windows 下使用 WinRAR / 7-Zip

  • Mac 下使用 Zipeg / iZip / UnRarX

  • Linux 下使用 7-Zip / PeaZip

这本书的代码包也托管在 GitHub 上,地址为 github.com/PacktPublishing/AI-Crash-Course。我们还有来自丰富书籍和视频目录的其他代码包,您可以在 github.com/PacktPublishing/ 访问。赶快去看看吧!

下载彩色图片

我们还提供了一份 PDF 文件,里面有本书中使用的带颜色的截图/图示。您可以在这里下载:static.packt-cdn.com/downloads/9781838645359_ColorImages.pdf

使用的约定

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

CodeInText:表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账户名。例如:“要获得这些数字,您可以将 nPosRewardnNegReward 两个列表加在一起。”

一块代码的设置如下:

# Creating the dataset
X = np.zeros((N, d))
for i in range(N):
    for j in range(d):
        if np.random.rand() < conversionRates[j]:
            X[i][j] = 1 

当我们希望您特别注意代码块中的某一行时,我们已经将行号作为注释添加,以便我们可以精确地引用它们:

 self.last_state = new_state   #80
        self.last_action = new_action   #81
        self.last_reward = new_reward   #82
        return new_action   #83 

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

conda install -c conda-forge keras 

粗体:表示新术语、重要词汇,或者您在屏幕上看到的词汇,例如在菜单或对话框中,也会以这种方式出现在文本中。例如:“从 Administration 面板中选择 System info。”

警告或重要提示以这种形式出现。

提示和技巧以这种形式出现。

联系我们

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

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

勘误:虽然我们已尽最大努力确保内容的准确性,但错误难免。如果您在本书中发现错误,我们将非常感激您向我们报告。请访问 www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并填写相关信息。

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

如果您有兴趣成为作者:如果您在某个领域有专长并且有兴趣撰写或参与撰写书籍,请访问authors.packtpub.com

评论

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

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

第一章:欢迎来到机器人世界

“我们正生活在一个最激动人心的时代!”这句话出自伟大的科技企业家 Peter Diamandis,对于那些从事人工智能AI)生态系统的人来说,这句话更是如此。AI 工作被认为是 21 世纪最性感的职业并非没有原因:除了薪资非常优厚,AI 也是一个令人兴奋的研究主题。

AI 在全球的重要性日益增加,今天我们几乎可以在所有行业中找到它的应用。这不是一种短暂的趋势;AI 将长期存在。正如顶级 AI 领袖和影响力人物 Andrew Ng 所说,AI 是新的电力。就像 19 世纪的工业革命改变了人们的生活和工作一样,AI 将在 21 世纪做出同样的改变。因此,你对它的理解和使用能力越强,机会就会越多。

给你提供一些重要数据,根据普华永道PwC)进行的一项研究,到 2030 年,AI 可能为全球经济贡献多达 15.7 万亿美元,这个数字超过了中国和印度目前的总产值。所以,你选择学习这个领域绝对是一个明智的决定。欢迎来到人工智能的不可思议的世界!

在这一章中,你将开始你的 AI 旅程,首先对你将在接下来的章节中学习的所有内容进行一个顶层概览。然后,我将通过介绍人工智能的各种顶级行业应用,帮助你了解学习 AI 能带你走到哪里。

开始你的 AI 之旅

作为一名年轻的 AI 科学家,我清楚地记得自己在 AI 领域的初学时光。这一点非常重要,因为本书就是一本 AI 速成课程。在学习本书的章节时,你不需要任何该领域的先验知识。

在本书中,我将解释 AI 的坚实基础,同时确保详细回答我当初刚进入这个领域时所有的问题。这意味着每个概念都将一步步解释,你的学习过程将遵循一个顺畅的路径,并且有相应的逻辑支持。

拥有正确的信息是成功进入 AI 领域的基础,但仅仅拥有信息是不够的。你还需要能量、热情和兴奋感。更好的是,你需要对这个领域充满激情,理想情况下,应该有所痴迷。作为一名有经验的在线课程导师,我希望能够传递我的知识,最重要的是,我的热情。

在这本书中,你将和我一起踏上旅程,走过充满激动人心的 AI 应用的世界,包括章节中的许多实际案例研究。这些应用将按照难度逐渐递增,从 AI 中最简单的模型,到更高级的水平。

对于每个 AI 应用,我将主要关注理解它们所需的直觉,然后,对于那些对应用背后的数学和纯理论感兴趣的人,我将提供相关内容。之所以选择侧重于直觉而非数学,不仅是因为我希望这本书对每个人都易于理解,更因为如今要在 AI 领域表现出色,拥有正确的直觉至关重要。当你在解决 AI 问题时,必须弄清楚哪个模型最适合你的问题环境,只有当你对每个 AI 模型的工作原理有了正确的直觉,你才能做到这一点。

四种不同的 AI 模型

这些 AI 模型之所以被选入本书,是因为它们在各种行业应用中都得到了广泛使用,能够解决许多不同的现实问题。在我们深入研究这些模型之前,我将在这里简单介绍它们的名称。本书中你将全面学习的四个 AI 模型如下:

  1. 汤普森采样

  2. Q 学习

  3. 深度 Q 学习

  4. 深度卷积 Q 学习

对于这四个模型中的每一个,我们都将遵循相同的三步方法:

  1. 直观理解其工作原理。

  2. 获取理论背后的所有数学知识。

  3. 从头开始用 Python 实现模型。

我多次在学生中使用这种结构,我可以告诉你,这种方法效果最佳。其思想很简单:因为你从直觉出发,就不会被数学淹没,而是能够更容易地理解它。你也会感觉更舒适地编写一些你既有直觉理解又具备深入理论知识的模型代码。

实际中的模型

在本书的整个过程中,你将找到可供学习或自己实现的实际例子。以下是你将在本课程各章节中找到的 AI 实现列表,这些内容将从第三章开始,在你完成第二章中的 AI 旅程工具准备后。

基础知识

第三章Python 基础——学习如何用 Python 编程,包含了本书所需的 Python 编程基础。你可以在此提醒自己,或者从头开始学习如何用 Python 编程。

第四章AI 基础技术,包含了一个伪代码示例,用于说明人工智能的五大核心原理。

汤普森采样

第五章你的第一个 AI 模型——警惕强盗!,包含了用于说明汤普森采样 AI 模型背后理论的入门代码。

第六章面向销售与广告的 AI——像 AI 街头的狼一样卖东西,包含了汤普森采样模型的实际应用案例,应用于在线广告。

Q 学习

第七章欢迎来到 Q 学习,包含了伪代码,用以说明 Q 学习 AI 模型的理论。

第八章物流中的 AI——仓库中的机器人,包含了 Q 学习模型的实际应用案例,应用于过程自动化和优化。

深度 Q 学习

第九章与人工大脑同行 – 深度 Q 学习,包含了介绍性代码,用以展示人工神经网络背后的理论。

第十章自动驾驶车辆 – 构建自驾车,包含了现实世界中应用深度 Q 学习模型的案例,应用于自动驾驶汽车。

第十一章AI 与商业,包含了另一个现实世界中的深度 Q 学习模型应用,应用于能源和商业领域。

深度卷积 Q 学习

第十二章深度卷积 Q 学习,包含了介绍性代码,用以展示卷积神经网络CNN)的实现。

第十三章AI 与视频游戏 – 成为贪吃蛇游戏的高手,包含了深度卷积 Q 学习模型在游戏中的现实世界应用。

正如你所看到的,每次你接触到一个新模型时,你首先学习直觉,然后是数学,最后是模型的实现。那么,为什么学习如何实现这些模型值得你的投入呢?

学习 AI 能带你去哪里?

我想通过向你展示你做出了正确的选择来激励你学习 AI。为此,我将带你参观 AI 在 21 世纪能够且将会拥有的所有令人难以置信的应用。我有一个关于 AI 如何改变世界的愿景,而这个愿景围绕着 10 个领域展开。

能源

2016 年,谷歌利用 AI 减少了其数据中心的能耗超过 30%。如果谷歌能为数据中心做到这一点,那么同样的方法也可以应用于整个城市。通过构建一个利用物联网IoT)技术的智能 AI 平台,可以在大规模上优化能源的消费和分配。

医疗保健

AI 在医疗领域具有巨大的潜力。它已经能够诊断疾病、开处方并设计新的药物配方。将这些技能结合成一个智能医疗平台,将使人们受益于真正个性化的医疗服务。这对社会来说将是非常惊人的。实现这一目标的挑战不仅体现在技术上,还在于获取匿名患者数据,而这些数据目前受到法规的保护。

交通和物流

自动驾驶车辆正在成为现实。虽然仍有许多工作要做,但技术在不断进步。通过建设智能数字基础设施,AI 将帮助减少事故数量并大幅减少交通拥堵。此外,自动驾驶货运卡车和无人机将加速物流过程,从而促进经济增长;主要通过其最大的引擎之一——电子商务行业。

教育

今天,我们生活在大规模开放在线课程的时代。任何人都可以在线学习任何内容。这是件好事,因为全世界的人都能获得教育;但这显然还不够。一个显著的改进是教育的个性化;每个人的学习方式和节奏都不同。比如,外向型的人可能更喜欢课堂学习,而内向型的人则更喜欢在家学习。有些人是视觉型学习者,而另一些人则是听觉型学习者。考虑到这些以及其他因素,AI 是一种强大的技术,能够提供个性化的培训,优化每个人的学习曲线。

安全

计算机视觉技术取得了巨大的进步。AI 现在可以高精度地检测人脸。不仅如此,监控摄像头的数量也在显著增加。所有这些都可以集成到一个全球安全平台中,以减少犯罪,增加公共安全,并使人们不敢违法。此外,AI 和机器学习已经成为强大的技术,广泛应用于欺诈检测与预防。

就业

AI 可以构建强大的推荐系统。我们已经看到了数字招聘平台,AI 将最佳候选人与职位匹配。这不仅对经济产生了积极的影响,也对人们的幸福感产生了影响,因为工作占据了一个人生活的超过一半。

智能家居与机器人

智能家居、物联网和连接设备正在大规模发展。机器人将帮助人们在家中生活,让人类能够专注于更重要的活动,比如工作或与家人共度美好时光。它们还将帮助老年人独立地在家中生活,甚至使他们能够更长时间地保持活跃,继续工作。

娱乐与幸福

当今技术的一个缺点是,尽管人们在虚拟世界中保持紧密联系,但他们却感到越来越孤独。孤独是我们必须在这个世纪里对抗的问题,因为它对人们的身心健康非常不利。AI 在这场斗争中扮演着重要角色,因为它同样是一种强大的推荐系统,不仅可以根据用户的兴趣推荐相关的电影和歌曲,还可以通过基于过去经验和共同兴趣的活动推荐,帮助人们建立联系。

通过全球智能娱乐平台,AI 技术可以帮助志同道合的人们进行线下社交,而不是仅限于虚拟交流。

另一个对抗孤独的想法是伴侣机器人,它们将在未来十年中越来越多地进入家庭。AI 的一个研究与开发领域是情感创造。这一领域的 AI 将使机器人能够展示情感和同理心,从而更成功地与人类互动。

环境

通过计算机视觉,机器可以优化垃圾分类,并更高效地重新分配垃圾处理周期。将纯 AI 模型与物联网结合,可以优化个人的电力和水资源消耗。某些平台上已经有程序,允许人们实时跟踪他们的消耗,从而收集数据。结合 AI 的整合,可以减少这一消耗,或优化分配周期以便有益再利用。结合交通减少和自动驾驶汽车的发展,这将大大减少污染,创造更健康的环境。

经济、商业和金融

AI 正在席卷商业世界。之前,我提到过 PwC 进行的研究,显示 AI 到 2030 年可能为全球经济贡献高达 15 万亿美元(www.pwc.com/gx/en/issues/data-and-analytics/publications/artificial-intelligence-study.html)。但 AI 是如何创造如此巨额收入的呢?AI 可以通过三种不同的方式为企业带来显著的附加值:流程自动化、利润优化和创新。在我对 AI 驱动经济的设想中,我看到大多数公司至少会采用一项 AI 技术,或者设立 AI 部门。在金融行业,我们已经看到一些工作被机器人取代。例如,在开发了表现良好的高频交易机器人之后,金融交易员的数量大幅减少。

如你所见,机器人世界为你提供了许多伟大的发展方向。AI 已经处于一个充满活力的阶段,并且随着前进,正在获得强大的势头。我的专业目标是将 AI 普及化,并激励人们通过 AI 在这个世界上产生积极影响——谁知道呢,也许你的目标就是与 AI 合作造福人类。我相信,这 10 个应用中至少有一个与你产生共鸣;如果是这样,努力成为 AI 大师,你将有机会做出改变。

如果你准备好进入 AI 领域,或者仅仅想增加你的知识,让我们开始吧!

总结

在本章中,你开始了你的 AI 之旅,见识到了未来为你打开的广阔机遇之地。也许你已经能够想到哪个行业应用最能引起你的共鸣,这样你就可以更加热爱你所做的 AI 工作,并理解自己为什么要这么做。在下一章,你将揭开本书中所使用的 AI 工具包。

第二章:发现你的 AI 工具包

在上一章,你开始了 AI 之旅。在继续之前,你需要准备好 AI 工具包。本书不仅仅是理论;它还包含了一个易于使用的工具包,里面包含了所有 AI 模型的 Python 文件,得益于强大的 Google Colaboratory 平台,这些文件已经准备好可以直接运行,你将在本章中进一步了解该平台。

为了丰富你的 AI 工具包,我准备了一个 GitHub 页面,里面包含了所有的 AI 实现代码供你下载,并且提供了 Python 笔记本的 Google Colab 链接,所有代码都可以通过简单的插拔式操作来执行。

GitHub 页面

你可以从以下 GitHub 页面下载到本书的所有代码:

github.com/PacktPublishing/AI-Crash-Course

要下载代码,你只需点击 Clone or download 按钮,然后点击 Download Zip

图 1:GitHub 仓库

然后,一旦你下载了这些代码,随便使用你喜欢的 Python 集成开发环境IDE)打开它们,无论是 Jupyter Notebook、Spyder、一个简单的文本编辑器,甚至是你的终端。

如果你从未使用过 Python 编程,并且不知道如何用 Python 编辑器打开这些文件,也不用担心;我为你准备了最佳且最简单的解决方案:Colaboratory(或 Google Colab)。

Colaboratory

Colaboratory 是一个免费的开源 Python 开发环境,无需任何配置,完全在云端运行。它包含了所有 AI 实现所需的预装包,这样你只需通过简单的插拔式操作即可运行这些代码。这里的“插入”是指将代码复制并粘贴到新的 Colab 文件中(接下来我会解释如何打开一个),而“播放”则是指点击播放按钮(下面会有示例)。

这是 Colaboratory 主页的链接:

colab.research.google.com/notebooks/welcome.ipynb

你应该会看到像这样的页面:

图 2:Colaboratory – 主页

点击左上角的 文件,然后点击 新建 Python 3 笔记本

图 3:Colaboratory – 打开一个笔记本

然后你会看到这个界面。将你的 Python 代码粘贴到单元格中(红色箭头指示)。这就是“插入”部分:

图 4:Colaboratory – "插入"部分

我推荐为本书中的每个模型使用独立的 Colaboratory 笔记本。

现在让我们来看看“播放”部分。在 Chapter 06 文件夹中打开 Thompson Sampling 模型,代码实现位于 thompson_sampling.py 文件中:

图 5:GitHub – 打开 Thompson Sampling

从 Python 文件中复制完整的代码;现在不需要担心理解代码(或结果)。一切将在第六章中逐步讲解,销售与广告的 AI——像 AI 街头的狼一样销售

图 6:GitHub – 复制 Thompson Sampling

接下来,将其粘贴到 Colaboratory 中(在图 4中由箭头标出的单元格内)。然后我们会得到如下结果:

图 7:粘贴 Thompson Sampling

现在我们准备好进行“播放”部分了!只需点击下面的“播放”按钮:

图 8:“播放”部分

然后代码会执行。现在不需要关注结果,因为一切将在第六章中讲解,销售与广告的 AI——像 AI 街头的狼一样销售

一切就绪!你现在拥有了一个 AI 工具包,它将帮助你跟随书中的每个示例进行学习。

在你真正开始 AI 之旅之前,必须确保你具备正确的基础编码知识。在成为 AI 大师之前,这一点至关重要。如果你对 Python 经验较少或没有经验,请确保在第三章Python 基础——学习如何用 Python 编程中学习 Python,作为进入机器人世界的最后准备阶段。

总结

在本章中,你已经准备好了行李,装上了我们的 AI 工具包,里面不仅包括了本书中的多种 AI 模型,还包括了非常易于使用的 Google Colaboratory 环境。你已经看到如何轻松地将我们的模型从 GitHub 插件到 Colaboratory。现在,你只需要具备编程技能,就可以开始真正的旅程了。在下一章,你将有机会学习——或者复习——你的 Python 基础知识。

第三章:Python 基础 – 学习如何编写 Python 代码

本章适合那些对 Python 编程语言几乎没有或完全没有经验的人。如果你已经知道如何使用for/while循环、方法和类,你可以跳过本章,之后也不会遇到问题。

然而,如果你以前没有使用过 Python,或者只是稍微用过一点,我强烈建议你按照本指南进行学习。你将学习到我在上一段中提到的 Python 元素,完全理解本书中的代码,并能够独立编写 Python 代码。我还会在本章中提供一些额外的练习,叫做“作业”,我强烈建议你完成它们。

在你开始之前,打开你的 Python 编辑器。我推荐使用 Google Colab 笔记本,它在上一章的 AI 工具包中已介绍给你。所有代码和作业解答都可以在本书 GitHub 页面的Chapter 3中找到,对应的部分文件夹里。里面有两个 Python 文件:一个(与章节同名)是本书中使用的代码,而homework.py文件是练习的解决方案。每个作业的指示将在每节的末尾提供。

本章将涵盖以下主题:

  • 显示文本

  • 变量和运算

  • 列表和数组

  • if语句和条件

  • forwhile循环

  • 函数

  • 类和对象

尤其是如果你是从零开始,按顺序覆盖每一部分,并记得尝试做作业。让我们开始吧!

显示文本

我们将从介绍任何编程语言的最常见方法开始;你将学会如何在 Python 控制台中显示一些文本。控制台是每个 Python 编辑器的一部分,它显示我们想要的信息或任何发生的错误(让我们希望没有错误!)。

在控制台中显示内容的最简单方法是使用print()方法,就像这样:

# Displaying text
print('Hello world!') 

print上面的文本,从#开始,是注释。执行代码时,注释会被忽略,只对你可见。

在 Google Colab 中运行这段简短的代码后,你将看到如下输出:

Hello world! 

总之,只需将你想显示的内容放入print方法的括号中——用引号括起来的文本,像这个例子一样,或者变量。

如果你对变量是什么感到好奇,那太好了——你将在这个练习之后学习它们。

练习

只使用一个print()方法,尝试显示两行或更多行内容。

提示:试试使用\n符号。

解决方案可以在 GitHub 页面上的Chapter 03/Displaying Text/homework.py文件中找到。

变量和运算

变量只是分配在计算机内存中某个地方的值。它们类似于数学中的变量。它们可以是任何东西:文本,整数或浮点数(小数点后带有精度的数字,例如 2.33)。

要创建一个新变量,你只需要写这个:

x = 2 

在这种情况下,我们命名了一个变量x并将其值设置为2

就像数学中一样,你可以对这些变量执行一些操作。最常见的操作是加法,减法,乘法和除法。在 Python 中,写法如下:

x = x + 5   #x += 5
x = x - 3   #x -= 3
x = x * 2.5 #x *= 2.5
x = x / 3   #x /= 3 

如果你第一次看到它,它可能没有太多意义 - 我们怎么能写出x = x + 5呢?

在 Python 中,以及大多数编程语言中,“=”符号并不意味着两个术语相等。它意味着我们将新的x值与旧的x值相关联,加上 5。理解这一点非常重要,这不是一个等式,而是创建一个与之前同名的新变量。

你也可以将这些操作写在右侧的注释中显示。通常你会看到它们以这种方式编写,因为这样更节省空间。

你也可以对其他变量执行这些操作,例如:

y = 3
x += y
print(x) 

在这里,我们创建了一个新变量y并将其设置为3。然后,我们将它添加到我们现有的x中。当你运行这段代码时,也会显示x的值。

那么,经过所有这些操作后,x的结果是什么?如果你运行这段代码,你会得到这个结果:

6.333333333333334 

如果你手工计算这些操作,你会发现x确实等于6.33

练习

尝试找到一种方法来将一个数的幂提高到另一个数。

提示:尝试使用 Python 的pow()内置函数。

解决方案可以在 GitHub 页面上的Chapter 03/Variables/homework.py文件中找到。

列表和数组

列表和数组可以用表格表示。想象一下一维(1D)向量或矩阵,你刚刚想象到了一个列表/数组。

列表和数组可以包含数据。数据可以是任何东西 - 变量,其他列表或数组(这些称为多维列表/数组),或者某些类的对象(我们稍后会学习它们)。

例如,这是一个包含整数的一维列表/数组:

这是一个二维(2D)列表/数组的示例,也包含整数:

要创建一个二维列表,你必须创建一个列表的列表。创建列表非常简单,就像这样:

L1 = list()
L2 = []
L3 = [3,4,1,6,7,5]
L4 = [[2, 9, -5], [-1, 0, 4], [3, 1, 2]] 

在这里,我们创建了四个列表:L1L2L3L4。前两个列表是空的 - 它们没有任何元素。后两个列表中有一些预定义的值。L3是一个一维列表,与第一张图片中的相同。L4是一个二维列表,与第二张图片中的相同。正如你所看到的,L4实际上由三个较小的 1D 列表组成。

每当我提到数组时,我通常指的是"NumPy"数组。NumPy 是一个 Python 库(库是一个包含预先编写的程序的集合,允许你在不编写代码的情况下执行很多操作),广泛用于列表/数组操作。你可以将 NumPy 数组视为一种特殊类型的列表,具有许多附加的功能。

要创建一个 NumPy 数组,你需要指定大小并使用初始化方法。下面是一个例子:

import numpy as np
nparray = np.zeros((5,5)) 

在第一行,我们导入了 NumPy 库(正如你所看到的,要导入库,必须写import),然后通过使用as,我们给 NumPy 取了一个缩写np,以便于使用。接着,我们创建了一个新数组,命名为nparray,这是一个 5 x 5 的二维数组,全部元素为零。初始化方法是.后面的部分;在这个例子中,我们通过zeros函数将数组初始化为零。

为了访问列表或数组中的值,你需要提供该值的索引。例如,如果你想更改L3列表中的第一个元素,你必须找到它的索引。在 Python 中,索引从0开始,所以你需要写L3[0]。实际上,你可以写print(L3[0])并执行,它会显示你期望的数字3

访问多维列表/数组中的单个值时,你需要输入与维度数目相同的索引。例如,要从L4列表中获取0,你需要写L4[1][1]L4[1]会返回整个第二行,它是一个列表。

练习

尝试找出L4列表中所有数字的平均值。这里有多种解法。

提示:最简单的解决方案是使用 NumPy 库。你可以在这里查看它的一些函数:docs.scipy.org/doc/numpy/reference/

解决方案已提供在 GitHub 页面的Chapter 03/Lists and Arrays/homework.py文件中。

if 语句和条件

现在,我想向你介绍编程中一个非常有用的工具——if条件语句!

它们广泛用于检查一个语句是否为真。如果给定的语句为真,则会执行一些代码中的指令。

我将通过一些简单的代码向你展示这个主题,这段代码可以判断一个数字是正数、负数还是零。代码非常简短,所以我会一次性展示全部内容:

a = 5
if a > 0:
    print('a is greater than 0')
elif a == 0:
    print('a is equal to 0')
else:
    print('a is lower than 0') 

在第一行,我们引入了一个新变量a,并将其值设为5。这就是我们要检查值的变量。

在下一行中,我们检查这个变量是否大于0。我们通过使用if条件语句来做到这一点。如果a大于0,则执行缩进块中的指令;在这个例子中,只有一条指令,就是显示消息a is greater than 0

然后,如果第一个条件失败,即a小于或等于0,我们将进入下一个条件,后者通过elif引入(elifelse if的缩写)。此语句将检查a是否等于零。如果是,我们执行缩进的指令,显示一条消息:a 等于 0

最终条件通过else引入。else条件中的指令会在其他条件都失败时执行。在这种情况下,两个条件都失败意味着a < 0,因此我们将显示a 小于 0

很容易预测我们的代码将返回什么。它将是第一条指令,print('a is greater than 0')。事实上,一旦运行这段代码,你会得到如下输出:

a is greater than 0 

简而言之,if用于引入语句检查和第一个条件,elif用于检查我们想要的多个进一步条件,而else是当所有其他语句都失败时的真语句。

还需要注意的是,一旦一个条件为真,其他条件将不再检查。所以,在这种情况下,一旦我们进入第一个条件并且发现它为真,我们就不再检查其他语句。如果你想检查其他条件,你需要将elifelse语句替换为新的if语句。新的if会检查新的条件;因此,if中的条件总是会被检查。

练习

构建一个条件,检查一个数字是否能被 3 整除。

提示:你可以使用一种称为模运算的数学表达式,它在使用时返回两个数字相除后的余数。在 Python 中,模运算用%表示。例如:

5 % 3 = 2

71 % 5 = 1

解决方案可以在 GitHub 页面上的Chapter 03/If Statements/homework.py文件中找到。

forwhile 循环

你可以把循环看作是不断重复相同的指令,直到满足某个条件打破循环。例如,之前的代码不是一个循环;因为它只执行了一次,所以我们只检查了一次a

Python 中有两种循环类型:

  • for 循环

  • while 循环

for 循环有一个特定的迭代次数。你可以将一次迭代看作是for循环中指定指令的单次执行。迭代次数告诉程序循环内的指令应该执行多少次。

那么,如何创建一个for循环呢?很简单,就像这样:

for i in range(1, 20):
    print(i) 

我们通过编写for来初始化这个循环,以指定循环类型。然后,我们创建一个变量i,,它将与range(1,20)中的整数值关联。这意味着当我们第一次进入这个循环时,i将等于1,第二次时将等于2,以此类推,一直到19。为什么是19?这是因为在 Python 中,区间的上界是排除的,因此在最后一次迭代时,i将等于19。至于我们的指令,在这种情况下,它只是通过使用print()方法在控制台显示当前的i。还需要理解的是,主代码在for循环完成之前不会继续执行。

这是我们执行代码后得到的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 

你可以看到我们的代码显示了所有大于 0 且小于 20 的整数。

你也可以使用for循环按以下方式遍历列表中的元素:

L3 = [3,4,1,6,7,5]
for element in L3:
    print(element) 

这里我们回到我们的L3一维列表。该代码会遍历L3列表中的每个元素并显示它。如果你运行它,你将看到从35的所有元素。

另一方面,while循环需要一个停止条件。它会继续执行,直到给定的条件满足为止。以这个while循环为例:

stop = False
i = 0
while stop == False:  # alternatively it can be "while not stop:"
    i += 1
    print(i)
    if i >= 19:
        stop = True 

在这里,我们创建了一个名为stop的新变量。这种类型的变量叫做布尔变量,因为它只能赋值为TrueFalse。然后,我们创建了一个名为i的变量,来计算我们的while循环执行了多少次。接下来,我们创建了一个while循环,只有当变量stopFalse时它才会继续执行;只有当stop被更改为True时,循环才会停止。

在循环中,我们将i增加 1,显示它,并检查它是否大于或等于19。如果大于或等于19,我们将stop设置为True;一旦我们将stop设置为True,循环将中断!

执行这段代码后,你将看到与for循环示例完全相同的输出,即:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 

同时,非常重要的一点是你可以将forwhile循环嵌套使用。例如,为了显示我们之前创建的 2D 列表L4中的所有元素,你需要创建一个for循环来遍历每一行,然后在这个循环内再创建一个for循环来遍历每一行中的每个值。像这样:

L4 = [[2, 9, -5], [-1, 0, 4], [3, 1, 2]]
for row in L4:
    for element in row:
        print(element) 

执行这段代码会得到以下输出:

2
9
-5
-1
0
4
3
1
2 

这与L4列表相匹配。

总结一下,forwhile循环使我们能够轻松地执行重复任务。for循环总是作用于一个预定义的范围;你可以准确知道它们什么时候停止。while循环作用于一个未定义的范围;仅凭它们的stop条件,你可能无法判断会执行多少次迭代。while循环会一直执行,直到满足特定条件为止。

练习

编写能够计算正整数变量阶乘的forwhile循环。

提示:阶乘是一个数学函数,返回所有小于或等于该函数参数的正整数的乘积。这个公式是:

f(n) = n * (n – 1) * (n – 2) ... 1

其中:

  • f(n) – 阶乘函数

  • n – 目标整数,我们要计算该整数的阶乘

该函数在数学中由!表示,例如:

5! = 5 * 4 * 3 * 2 * 1 = 120

4! = 4 * 3 * 2 * 1 = 24

解决方案可以在 GitHub 页面的Chapter 03/For and While Loops/homework.py文件中找到。

函数

函数在你想要提高代码可读性时非常有用。你可以把它们看作是主代码流程外的代码块。函数会在被主代码调用时执行。

你可以这样写一个函数:

def division(a, b):
    result = a / b
    return result
d = division(3, 5)
print(d) 

前三行是新创建的一个名为division的函数,后两行是主代码的一部分。

你可以通过编写def来创建一个函数,然后写下函数的名称。名称后面加上括号,并在其中写下函数的参数;这些是你可以在函数内使用的变量,是主代码与函数之间的连接部分。在这个例子中,我们的函数接受两个参数:ab

然后,当我们调用我们的函数时,我们做的就是计算a除以b并将这个除法结果称为result。接着,在函数的最后一行,我们说return,这样当我们在代码中调用这个函数时,它就会返回一个值。在这种情况下,返回的值是result

接下来,我们回到主代码中并调用我们的函数。我们通过写division来做到这一点,然后在括号内输入我们想要除的两个数字。记住,division函数返回的是这次除法的result;因此,我们创建了一个变量d来保存这个返回值。在最后一行,我们简单地显示d来检查这段代码是否真的有效。如果你运行它,你将看到输出:

0.6 

正如你手动验证的那样,3 除以 5 的确是 0.6;你也可以用其他数字进行测试。

在实际的代码中,函数可能会更长,有时甚至会调用其他函数。你会在本书的其他章节中看到它们的广泛应用。它们还提高了代码的可读性,正如你稍后会看到的那样;如果没有函数,我提供的代码将无法理解。

练习

构建一个函数来计算二维平面上两点之间的距离:一个点的坐标为x1y1,另一个点的坐标为x2y2

提示:你可以使用以下公式:

解决方案可以在 GitHub 页面的Chapter 03/Functions/homework.py文件中找到。

类和对象

类和函数一样,都是位于主代码之外的另一部分代码,只有在主代码中调用时才会执行。对象是相应类的实例,存在于我们代码的主流程中。为了更好地理解它,可以将类视为某物的计划,例如一辆车的计划。它包含了某些组件的外观以及它们如何相互协作。Python 中的类是某物的一种通用计划。

你可以将对象视为基于计划构建的现实世界中的构造物。例如,一辆真实、可工作的自动驾驶汽车就是一个对象的例子。你创建了一个车的计划(即类),然后你根据这个计划建造一辆车(即对象)。当然,当你有了某物的计划时,你可以根据这个计划创建任意多个副本;例如,你可以运行一个生产线来生产汽车。

为了让你对类有更深入的了解,我们将创建一个简单的bot。我们从编写一个类开始,像这样:

class Bot():

    def __init__(self, posx, posy):
        self.posx = posx
        self.posy = posy

    def move(self, speedx, speedy):
        self.posx += speedx
        self.posy += speedy 

我们写class来指定我们正在创建一个新的类,我们将其命名为Bot。然后,一个非常重要的步骤是编写__init__()方法,这是创建类时的必要步骤。每当在代码的主流程中创建该类的对象时,这个函数会自动调用。

类中的所有函数都需要接受self作为一个参数。那么,self是什么?这个参数指定了这个函数及其变量(变量名以self为前缀)是这个类的一部分。只有在我们拥有该类的对象时,才能调用self变量。我们的bot__init__()方法同样接受两个参数,posxposy,这将是我们bot的初始位置。

我们还创建了一个方法,它将通过增加或减少posxposy来移动我们的bot。方法是一个嵌入类中的函数。你可以将其视为一种说明,描述当我们拥有一个计划时,某个事物应该如何工作。例如,回到汽车的例子,方法可以定义我们的发动机或变速箱的工作方式。

现在,你可以创建该类的一个对象了。记住,这将是一个基于计划(class)构建的现实世界中的对象。之前,类是预定义的,并未与代码一起工作。创建对象后,类便成为了你主代码的一部分。我们可以通过如下方式实现:

bot = Bot(3, 4) 

这将创建一个Bot类的新对象;我们将这个对象命名为bot。我们还需要指定Bot类的__init__()方法所接受的两个参数,分别是posxposy。这不是可选的;在创建对象时,你必须始终指定__init__()方法中给出的所有参数。

现在,在主代码中,你可以移动bot并显示它的新位置,像这样:

bot.move(2, -1)
print(bot.posx, bot.posy) 

在第一行中,我们使用了 Bot 类中的 move 方法。如你所见,在其定义中,move 需要两个参数。这两个参数分别指定了我们将增加 posxposy 的量。然后我们只需显示新的 posxposy。这就是 self 起作用的地方;如果在我们的 Bot 类中,posxposy 前面没有加上 self,我们就无法通过该方法访问它们。运行此代码会得到如下结果:

5 3 

如你所见,结果显示我们的机器人在 x 轴上向前移动了两个单位,在 y 轴上向后移动了一个单位。记住,posx 最初设置为 3,并且通过 Bot 类的 move 方法增加了 2posy 最初设置为 4,并通过相同的 move 方法减少了 1

编写一个 Bot 类的一个巨大优势是,现在我们可以创建任意数量的机器人,而无需增加任何代码。简而言之,对象是类的副本,我们可以创建任意数量的对象。

总之,你可以把类看作是一个包含预定义指令并封装在方法中的集合,而对象可以看作是类的一个实例,它在我们的代码中是可访问的,并与代码一同运行。

练习

你的最终挑战是构建一个非常简单的汽车类。作为参数,汽车对象应该接收汽车能达到的最高速度(单位为 m/s),以及汽车的加速度(单位为 m/s²)。我还挑战你构建一个方法,计算汽车从当前速度加速到最高速度所需的时间,已知加速度(使用当前速度作为该方法的参数)。

提示:要计算所需时间,你可以使用以下公式:

其中:

  • – 达到最高速度所需的时间

  • – 最高速度

  • – 当前速度

  • – 加速度

解决方案在 GitHub 页面上的 Chapter 03/Classes/homework.py 文件中提供。

总结

在本章中,我们介绍了学习 Python 基础知识所需的内容,帮助你跟上本书中的代码,从向控制台发送简单的文本显示,到编写你的第一个 Python 类。现在,你已经具备了继续你人工智能之旅所需的所有技能;在第四章人工智能基础技术中,我们将开始研究人工智能的基础技术。

第四章:人工智能基础技术

在本章中,你将正式开始学习人工智能理论。你将从强化学习这一人工智能的主要分支开始,并学习支撑每一个强化学习模型的五个原则。这些原则将为你提供理论基础,帮助你理解本书中所有即将出现的人工智能模型。

什么是强化学习?

当人们今天提到人工智能时,有些人会想到机器学习,而另一些人则会想到强化学习。我属于后者。我一直认为机器学习是具有学习某些关联性的统计模型,它们通过这些关联性进行预测,而无需明确的编程。

虽然这在某种程度上是一种人工智能的表现,但机器学习并不包含像我们人类那样进行行动和与环境互动的过程。事实上,作为智能的存在,我们不断进行以下的行为:

  1. 我们观察一些输入,不论是通过眼睛看到的,耳朵听到的,还是我们记得的。

  2. 这些输入随后会在我们的大脑中处理。

  3. 最终,我们做出决策并采取行动。

与环境互动的这一过程是我们试图在人工智能领域重现的内容。为此,人工智能的一个分支——强化学习,致力于此。它是最接近我们思考方式的人工智能形式;如果我们把人工智能看作是试图模仿(或超越)人类智能的科学,那么它就是人工智能中最先进的形式。

强化学习在人工智能的商业应用中也取得了最令人印象深刻的成果。例如,阿里巴巴利用强化学习将其在线广告的投资回报率提高了 240%,而无需增加广告预算(参见arxiv.org/pdf/1802.09756.pdf,第 9 页,表格 1 最后一行(DCMAB))。我们将在本书中处理相同的行业应用!

强化学习的五个原则

让我们开始建立你对强化学习如何工作的第一批直觉。这些是强化学习的基本原则,它们将为你在人工智能领域打下坚实的基础。

以下是五个原则:

  1. 原则 #1:输入与输出系统

  2. 原则 #2:奖励

  3. 原则 #3:人工智能环境

  4. 原则 #4:马尔可夫决策过程

  5. 原则 #5:训练与推理

在接下来的章节中,你将依次阅读每个内容。

原则 #1 – 输入与输出系统

第一步是理解,今天所有的人工智能模型都基于输入和输出的共同原则。每一种形式的人工智能,包括机器学习模型、聊天机器人、推荐系统、机器人,当然也包括强化学习模型,都会接收某些东西作为输入,并返回另一种东西作为输出。

图 1:输入和输出系统

在强化学习中,这些输入和输出有一个特定的名称:输入叫做状态,或输入状态。输出是 AI 执行的动作。中间的部分,只有一个函数,它以状态为输入,返回动作作为输出。这个函数叫做策略。记住这个名字,“策略”,因为你会在 AI 文献中经常看到它。

以自动驾驶汽车为例。试着想象一下,在这种情况下输入和输出是什么。

输入是嵌入式计算机视觉系统所看到的内容,输出则是汽车的下一步动作:加速、减速、左转、右转或刹车。请注意,任何时候(t),输出可能是多个动作同时执行。例如,自驾车可以在加速的同时左转。同样,每个时刻(t)的输入也可以由多个元素组成:主要是计算机视觉系统观察到的图像,但也包括一些汽车参数,如当前速度、油箱剩余的油量等。

这就是人工智能的第一个重要原则:它是一个智能系统(一个策略),它接受一些元素作为输入,在中间进行处理,然后返回一些动作作为输出。记住,输入也叫做状态

下一个重要的原则是奖励。

原则#2 – 奖励

每个 AI 的表现都通过奖励系统来衡量。这一点毫不复杂;奖励只是一个度量标准,用来告诉 AI 它随时间的表现如何。

最简单的例子是二元奖励:0 或 1。想象一个 AI 需要猜测一个结果。如果猜对了,奖励是 1;如果猜错了,奖励是 0。这可能就是定义 AI 奖励系统的方式;实际上,它可以简单到如此!

然而,奖励不一定是二元的。它可以是连续的。考虑一下著名的Breakout游戏:

图 2:Breakout 游戏

想象一个 AI 在玩这个游戏。试着思考在这种情况下奖励是什么。它可能仅仅是得分;更准确地说,得分就是在一局游戏中随时间积累的奖励,而奖励可以定义为得分的导数

这是我们可以为这个游戏定义奖励系统的众多方式之一。不同的 AI 将有不同的奖励结构;我们将在本书中为五个不同的实际应用构建五个奖励系统。

牢记这一点:AI 的最终目标总是最大化随时间积累的奖励。

这些就是人工智能当前存在的前两个基本而根本的原则;输入输出系统和奖励。接下来要考虑的是 AI 环境。

原则#3 – AI 环境

第三个原则是我们所说的“AI 环境”。它是一个非常简单的框架,你需要在每个时间点(t)定义三件事:

  • 输入(状态)

  • 输出(动作)

  • 奖励(性能指标)

对于今天构建的每一个基于强化学习的 AI,我们始终定义一个由上述元素组成的环境。然而,理解一个 AI 环境中不仅仅有这三种元素是很重要的。

例如,如果你正在构建一个 AI 来打败一款赛车游戏,那么环境还会包含该游戏的地图和游戏玩法。或者,在自动驾驶汽车的例子中,环境还会包含 AI 行驶的所有道路以及围绕这些道路的物体。但你在构建任何 AI 时,总是能找到的共同点就是状态、动作和奖励这三个元素。下一个原则,马尔可夫决策过程,讲述了它们在实践中是如何运作的。

原则 #4 – 马尔可夫决策过程

马尔可夫决策过程,或 MDP,简单来说,是一种模型,描述 AI 如何随着时间与环境互动。该过程从t = 0 开始,然后在每个后续的迭代中,也就是在t = 1,t = 2,… t = n单位时间时(单位可以是任何东西,例如 1 秒),AI 遵循相同的过渡格式:

  1. AI 观察当前状态,

  2. AI 执行动作,

  3. AI 收到奖励,

  4. AI 进入下一个状态,

强化学习中 AI 的目标始终是一样的:最大化随着时间积累的奖励总和,也就是在每次过渡时收到的所有的总和。

以下图示将帮助你更好地理解并记住 MDP,它是强化学习模型的基础:

图 3:马尔可夫决策过程

现在,四个基本支柱已经在塑造你对 AI 的直觉。再加上最后一个重要的支柱,就完成了你对 AI 理解的基础。最后一个原则是训练与推理;在训练阶段,AI 进行学习,而在推理阶段,AI 进行预测。

原则 #5 – 训练与推理

你需要理解的最后一个原则是训练和推理之间的区别。构建 AI 时,有一个阶段是训练模式,另一个阶段是推理模式。我将从训练模式开始解释这意味着什么。

训练模式

现在你已经从前三个原则中理解到,构建 AI 的第一步是建立一个环境,在这个环境中,输入状态、输出动作和奖励系统需要明确定义。从第四个原则,你也能理解到,在这个环境中我们将构建一个 AI 来与之交互,尽力最大化随着时间积累的总奖励。

简单来说,会有一个初步的(且漫长的)时间段,在这段时间里,AI 将接受训练以完成目标。这个时间段被称为训练期;我们也可以说 AI 处于训练模式。在此期间,AI 会不断尝试完成某个目标,直到成功为止。在每次尝试后,AI 模型的参数都会被调整,以便下次表现得更好。

比如说,假设你正在构建一辆自动驾驶汽车,你希望它从点开到点。我们还假设存在一些障碍物,你希望自动驾驶汽车能够避开它们。训练过程如下:

  1. 你选择一个 AI 模型,可能是汤普森采样(第五章第六章),Q 学习(第七章第八章),深度 Q 学习(第九章第十章第十一章)甚至深度卷积 Q 学习(第十二章第十三章)。

  2. 你初始化模型的参数。

  3. 你的 AI 尝试从(通过观察状态并执行其行动)。在第一次尝试中,AI 越接近,你给它的奖励就越高。如果它没能到达或者碰到障碍,你就给 AI 一个非常差的奖励。如果它成功到达且没有撞到任何障碍,你就给它一个极好的奖励。这就像你训练一只狗坐下:如果狗坐下了,你给它奖励或说“好狗”(正面奖励)。如果狗不听话,你就给予它必要的惩罚(负面奖励)。这个过程就是训练,在强化学习中是一样的。

  4. 在尝试结束时(也称为一个回合),你会调整模型的参数,以便下次做得更好。这些参数的修改是智能化的,可以通过迭代的方程(Q 学习)来完成,或者使用机器学习和深度学习技术,如随机梯度下降或反向传播。所有这些技术将在本书中讲解。

  5. 你需要不断重复步骤 3 和 4,直到达到理想的性能;也就是说,直到你拥有一辆完全安全的自动驾驶汽车!

所以,这就是训练。那么,推理呢?

推理模式

推理模式仅在你的 AI 完全训练好并准备好表现时开始。在推理模式下,AI 只需通过执行行动来与环境互动,以完成在训练模式下曾经训练过的目标。在推理模式中,每个回合结束后,AI 的参数不会被修改。

比如,假设你拥有一家为企业提供定制 AI 解决方案的 AI 公司,且其中一个客户要求你开发一个 AI 来优化智能电网中的流量。首先,你会进入研发阶段,在这个阶段,你将训练 AI 来优化这些流量(训练模式),一旦达到了一个良好的性能水平,你就会将 AI 交付给客户并进入生产阶段。你的 AI 只通过观察电网的当前状态并执行它被训练过的动作来调节智能电网中的流量。这就是推理模式。

有时候,环境可能会发生变化,这时你必须在训练模式和推理模式之间快速切换,以便让你的 AI 适应环境中的新变化。一个更好的解决方案是每天训练你的 AI 模型,并使用最新训练的模型进入推理模式。

这就是每个人工智能(AI)共有的最后一个基本原则。恭喜你——现在你已经对人工智能有了坚实的基础理解!既然你掌握了这些,你已经准备好在下一章中处理你的第一个 AI 模型:一个简单但非常强大的模型,至今在商业和营销中仍被广泛使用,用来解决一个有着迷人名字的多臂赌博机问题。

总结

在本章中,你从强化学习的角度学习了人工智能的五个基本原则。首先,AI 是一个系统,它将观察(值、图像或任何数据)作为输入,并返回一个要执行的动作作为输出(原则#1)。然后,有一个奖励系统,帮助它衡量其性能。AI 会通过试错法学习,根据它随着时间获得的奖励(原则#2)。输入(状态)、输出(动作)和奖励系统定义了 AI 环境(原则#3)。AI 通过马尔可夫决策过程与这个环境互动(原则#4)。最后,在训练模式下,AI 通过迭代更新其参数,学习如何最大化总奖励;而在推理模式下,AI 只是执行其动作,贯穿整个过程而不更新任何参数——也就是说,不进行学习(原则#5)。

在下一章中,你将学习汤普森采样(Thompson Sampling),一个简单的强化学习模型,并使用它来解决多臂赌博机问题。

第五章:你的第一个 AI 模型 —— 小心赌博机!

在本章中,你将掌握你的第一个 AI 模型!你将创建一个模型来解决非常著名的多臂赌博机问题。这是 AI 中的经典问题,也广泛应用于许多现实世界的商业问题中。

多臂赌博机问题

想象你在拉斯维加斯,你最喜欢的赌场里。你所在的房间里有五台老丨虎丨机。每台老丨虎丨机的游戏规则相同:你下注一定金额,比如 1 美元,拉动把手,机器要么拿走你的钱,要么给你两倍的钱。还记得我们在上一章讨论的奖励吗?假设如果机器拿走你的钱,你的奖励是 -1;如果机器还给你两倍的钱,你的奖励是 +1。

如你所见,你已经开始定义一个 AI 环境了,提醒一下,这是解决 AI 问题时至关重要的步骤。到目前为止,AI 还没有出现,但它很快就会到来。你总是从定义环境开始。

你已经定义了奖励;稍后你将定义状态(输入)和动作(输出)。现在,仍在定义环境的过程中,假设你以某种方式知道其中一台老丨虎丨机比其他老丨虎丨机在你拉动其把手时,给你 +1 奖励的概率更高。你知道这个信息并不重要,但它必须是问题假设的一部分。请放心,这个假设在上述现实世界的商业问题中总是能自然得到验证,这些问题可以应用多臂赌博机问题。

你的目标,和任何 AI 环境一样,是在游戏过程中获得最高的累积奖励。假设你将总共投注 1,000 美元,也就是说你将投注 1 美元,1,000 次,每次都拉动其中任何一台老丨虎丨机的把手。问题是:

你应该采取什么策略,以便在玩 1,000 次后,能够获得最多的钱带回家?

你策略的第一步必须是在最少的次数内,找出这五台老丨虎丨机中哪个有最高的概率给你 +1 奖励。换句话说,你必须快速找出成功率最高的老丨虎丨机。然后,一旦你找到了,你只需继续在那台最成功的老丨虎丨机上进行游戏。

找到最成功的老丨虎丨机并不难;一种简单的策略是分别在这五台老丨虎丨机上各玩 100 次,然后在最后查看哪一台给你带来了更多的钱。从统计学的角度来看,这样做能够让你有很大机会找到那个最慷慨的老丨虎丨机。

挑战就在于“快速”。最难的部分是 在最少的试验次数内 找到最好的老丨虎丨机。这就是你第一个 AI 模型派上用场的地方。

汤普森采样模型

你将立即构建这个模型。现在,你将实现这个方法的一个简单版本,之后会展示其背后的理论。我们开始吧!

如我们之前定义的,我们的问题是要从多个老丨虎丨机中找到中奖概率最高的那个。一个不太理想的解决方案是对每台老丨虎丨机玩 100 轮,看看哪台的中奖率最高。一个更好的解决方案是使用一种叫做汤普森采样(Thompson Sampling)的方法。

我不会深入讲解其背后的理论,稍后我们会讨论。现在,只需要知道,汤普森采样(Thompson Sampling)使用一个分布函数(分布的相关内容将在本章中进一步解释),这个函数叫做 Beta,它接受两个参数。为了简化,我们可以假设,第一个参数越大,我们的老丨虎丨机就越好;第二个参数越大,我们的老丨虎丨机就越差。

因此,我们可以将这个函数定义为:

其中:

  • x – 从我们的 Beta 分布中随机选择的一个值

  • – 我们的 Beta 函数

  • a – 第一个参数

  • b – 第二个参数

如果你现在还不完全理解这些内容,别担心;你之后会读到相关内容。

编写模型

让我们开始编写解决方案的代码。所有这些代码也可以在本书的 GitHub 页面的Chapter 05文件夹中找到。我们从第一段代码开始:

# Importing the libraries
import numpy as np 

你只需要一个库,叫做 NumPy。这个库非常有用,尤其是在处理多维数组和列表时。将它简写为np,这是行业标准,这样使用起来会更方便。

现在我们必须理解一个非常重要的概念。你正在创建一个模拟,其目的是模拟现实生活中的情况。在现实中,每台老丨虎丨机都有一定的中奖概率,并且有些机器的中奖概率高于其他机器。因此,在模拟这个环境时,你也需要做同样的事情。然而,需要记住的是,我们的 AI 并不知道这些预定义的中奖概率。它不能直接读取这些概率,并基于这些概率判断哪个机器是最好的。

对于这个例子,我们将这个中奖机会的列表称为conversionRates

# Setting conversion rates and the number of samples
conversionRates = [0.15, 0.04, 0.13, 0.11, 0.05]
N = 10000
d = len(conversionRates) 

这里,你有五台老丨虎丨机。它们每台有不同的中奖概率;例如,老丨虎丨机编号 1 的中奖概率是 15%。接着你创建一个样本数量N。记住,你是在做模拟,所以你需要一个预定义的数据集,告诉你在游戏时是否获胜。你还引入了一个变量d,表示你的中奖率列表的长度,也就是老丨虎丨机的数量。使用这样的简短变量名非常有用,因为如果不这样做,代码会更长,且不易阅读。

你知道接下来该怎么做了吗?你正在运行一个模拟,因此需要为每个样本的每个老丨虎丨机预定义一个胜负结果。我强烈建议你自己先尝试做这一步。你需要有一个集合,告诉你在某个时刻i,通过玩某个老丨虎丨机你是否获胜。答案在下一个代码片段中。

# Creating the dataset
X = np.zeros((N, d))
for i in range(N):
    for j in range(d):
        if np.random.rand() < conversionRates[j]:
            X[i][j] = 1 

在第一行,你创建了一个充满零的二维数组,大小为N * d。这意味着你创建了一个N(在这个例子中为10000)行和d(在这个例子中为5)列的数组。然后,在一个for循环中,你遍历了这个二维数组X中的每一行。在一个嵌套的for循环中,你遍历了该行中的每一列。在前面代码片段的第 5 行,对于每个老丨虎丨机(每一列),我们检查从范围(0,1)中得到的随机浮动数是否小于相应老丨虎丨机的转换率。

这就像是在玩老丨虎丨机;由于从这个范围中得到任何浮动数的机会是均等的,得到一个小于x的数字的机会(其中x也在区间(0,1)内)等于x。例如,当d = 0.15 时,在 100 次中有 15 次会得到一个小于 0.15 的浮动数,因此老丨虎丨机 1 的高奖励概率是 15%。换句话说,如果随机浮动数较小,那么意味着你在某个时刻玩这个特定的机器时会赢。

为了确保你理解,如果你的数据集X中的N个样本之一看起来像这样:[0, 1, 0, 0, 1],那么在这个时刻,通过玩老丨虎丨机编号 2 或 5 你会赢。

接下来,你需要创建两个数组,用来记录你玩每个老丨虎丨机时获胜和失败的次数,像这样:

# Making arrays to count our losses and wins
nPosReward = np.zeros(d)
nNegReward = np.zeros(d) 

将它们命名为nPosReward(获胜次数)和nNegReward(失败次数)。

现在,你已经创建了一个模拟集合和这两个计数器,你可以开始编写一些 Thompson Sampling 代码了。请记住,理论和另一个示例稍后会讲解。

接下来,初始化一个for循环,遍历数据集中的每个样本并选择最佳老丨虎丨机。最初,只需要创建两个变量,一个叫selected,它会告诉你选择了哪个老丨虎丨机,另一个叫maxRandom,你将用它来获得所有老丨虎丨机中最高的 Beta 分布猜测:

# Taking our best slot machine through beta distribution and updating its losses and wins
for i in range(N):
    selected = 0
    maxRandom = 0 

现在你可以理解 Thompson Sampling 的核心了。你将从我们的 Beta 分布中随机猜测,并找出所有老丨虎丨机中的最高值。

你可以使用 NumPy 中的方法np.random.beta(a,b)来返回这个随机猜测。知道这一点后,试着自己找出最高的猜测和最好的机器吧!即使失败也完全没关系——我们还没讲到理论——我会给你一个答案的。祝你好运!

希望你已经试过了。不管是否成功,这里是我的答案:

 for j in range(d):
        randomBeta = np.random.beta(nPosReward[j] + 1, nNegReward[j] + 1)
        if randomBeta > maxRandom:
            maxRandom = randomBeta
            selected = j 

你没有漏掉任何内容—这就是完成该任务所需的所有代码。你创建了一个for循环来遍历每个老丨虎丨机并找出最好的一个。对于每个老丨虎丨机索引j(记住你依然在大的for循环中,索引为i),你从我们的 Beta 分布中进行一次随机抽样,叫做randomBeta,并检查它是否大于maxRandom

如果是的话,那么你将重新分配maxRandom的值为randomBeta,并将selected设置为这个新的最大猜测老丨虎丨机j的索引。还值得提一下的是,Beta 函数的ab参数在此处的含义;它们是我们在特定老丨虎丨机上获得的胜利和失败次数。记住,第一个参数越大越好,我们的随机猜测也会越高;第二个参数越大越糟,我们的随机猜测也会越低。

现在你已经选择了最好的老丨虎丨机,接下来你认为应该做什么?

你需要根据是否获胜来更新nPosRewardnNegReward。我们可以通过以下代码来实现:

 if X[i][selected] == 1:
        nPosReward[selected] += 1
    else:
        nNegReward[selected] += 1 

在这里,你可以看到你之前创建的X数组的使用。你通过检查X数组中相应位置是否为1来判断是否赢得了这一轮。如果赢了,你就通过在nPosReward中相应的索引加1来更新该机器的奖励。如果输了,你则在nNegReward中同样的位置加1。你可以清楚地看到,如果你赢了,下一次你从 Beta 分布中抽到的该机器的随机猜测值会更高;如果你输了,随机猜测值则会更低。

这段代码已经能正常工作,虽然值得添加几行代码来展示你的代码认为哪个老丨虎丨机是最好的:

# Showing which slot machine is considered the best
nSelected = nPosReward + nNegReward 
for i in range(d):
    print('Machine number ' + str(i + 1) + ' was selected ' + str(nSelected[i]) + ' times')
print('Conclusion: Best machine is machine number ' + str(np.argmax(nSelected) + 1)) 

在这里,你只需展示你的算法选择了多少次每个老丨虎丨机。为了得到这些数字,你可以将nPosRewardnNegReward的列表相加。在最后一行,你展示了选择次数最多的老丨虎丨机,从而确定其为被认为最好的老丨虎丨机。

现在,你可以直接运行代码并查看结果:

Machine number 1 was selected 7927.0 times
Machine number 2 was selected 82.0 times
Machine number 3 was selected 1622.0 times
Machine number 4 was selected 306.0 times
Machine number 5 was selected 63.0 times
Conclusion: Best machine is machine number 1 

如我们所见,你的算法快速找到了 1 号机器是最好的。它在大约 2000 轮(2000 个样本)后找到了答案。

理解模型

Thompson Sampling 是目前为止解决这种问题的最佳模型;在本章的结尾,你将看到与另一种方法的对比。它的魔力是这样运作的。首先,我们做的事情显然是依次玩每个老丨虎丨机的转盘。我们开始吧:

第 1 轮:我们玩的是老丨虎丨机 1 号的转盘。假设我们获得了奖励 0。

第 2 轮:我们玩的是老丨虎丨机 2 号的转盘。假设我们获得了奖励 1。

第 3 轮:我们玩的是老丨虎丨机 3 号的转盘。假设我们获得了奖励 0。

第 4 轮:我们玩的是老丨虎丨机 4 号的转盘。假设我们获得了奖励 0。

第 5 轮:我们玩老丨虎丨机 5 的臂。假设我们获得了奖励 1。

那么,你认为我们为什么要这么做?我们这么做只是为了从每个老丨虎丨机收集一些初步信息。这些信息将在未来的轮次中使用。

现在,事情开始变得有趣了。第 6 轮我们该怎么做?我们要玩哪个臂?

好吧,我们需要回顾一下前五轮发生了什么。对于每个老丨虎丨机,我们引入两个新变量,一个是统计老丨虎丨机返回 0 奖励的次数,另一个是统计老丨虎丨机返回 1 奖励的次数。

我们将这些变量表示为 ,其中 是老丨虎丨机编号 i 在第 n 轮之前返回奖励 0 的次数,而 是老丨虎丨机编号 i 在第 n 轮之前返回奖励 1 的次数。这两个变量在我们的代码中分别表示为 nNegRewardnPosReward。所以,根据我们在第 5 轮时所获得的数据,以下是这些变量的值示例:

表示老丨虎丨机 1 在 1 轮中返回了 1 次失败。

表示老丨虎丨机 1 在 1 轮中返回了 0 次胜利。

表示老丨虎丨机 2 在 1 轮中返回了 0 次失败。

表示老丨虎丨机 2 在 1 轮中返回了 1 次胜利。

表示老丨虎丨机 5 在 4 轮中返回了 0 次失败。

表示老丨虎丨机 5 在 4 轮中返回了 0 次胜利。

表示老丨虎丨机 5 在 5 轮中返回了 0 次失败。

表示老丨虎丨机 5 在 5 轮中返回了 1 次胜利。

好了,这部分比较简单。好消息是,我们已经创建了所有 AI 所需的变量。坏消息是,现在开始进入难部分——数学。如果你认为数学是好消息,我喜欢你的精神;但如果你不喜欢数学,也别担心,我不会让你失望的。

什么是分布?

我们 AI 旅程的下一步是引入数学中的分布。为此,我将用我自己的话给你一个简单的定义,而不是那些你在数学书籍中找到的非常正式的定义。我想确保每个人都能理解。定义是这样的:一个变量的分布是一个函数,它会为变量可能取的每个值,提供这个变量等于该值的概率。

让我们通过一个例子真正理解它:

图 1:正态分布

在前面的图中,你可以看到一个分布的例子。现在,记住在我给你的定义中,我提到了两个量度:“变量可能取的值的范围”,以及“该变量等于该值的概率”。在任何分布中,x 轴上是变量可能取的值的范围,y 轴上是该变量等于每个值的概率。

如果还不清楚,不用担心。为了扩展我们的例子,假设在前面的图中,这个变量是某个特定国家的人们的年薪。

x 轴上,我们会有年薪范围,从最低工资到最高工资,假设从 15,000 美元到 150,000 美元。而在 y 轴上,我们会有一个人拥有该薪资的概率。

现在应该更有意义了。对于低薪水,曲线较低,这意味着一个人年薪大约 15,000 美元的概率较低。

然后,直到 x 轴的中心,标记为 ,即薪水的平均值,人的薪水的概率会增加。假设 等于 45,000 美元。我们直观地理解,一个特定国家的个人年薪 45,000 美元的概率最高,简单来说,因为大多数人的年薪大约是 45,000 美元。正因为如此,图中的分布在这个薪水上是最高的。

当我们走高于年薪 45,000 美元的区域时,赚取这种薪水的人会越来越少,因此赚取此类薪水的概率会减少,直到超过年薪 150,000 美元,几乎没有人能赚到这么多,从而导致接近零的概率。

好的,这就是直观解释的分布。现在,你需要知道有许多类型的分布:高斯分布(看起来像前面的图形)、正态分布(均值为 0,方差为 1 的高斯分布)、Beta 分布,等等。

这是下一步:Beta 分布。Beta 分布是我们为解决赌博机问题而构建的人工智能的核心。以下是 Beta 分布的样子:

图 2:三种 Beta 分布

让我们做一些练习,以确保你理解分布的运作方式。假设这三种分布对应着三个不同的国家,且再次假设它们是这些国家的薪水分布。哪个国家的薪水最高?是紫色的那个,绿色的,还是黄色的?答案当然是黄色的!正是在这个国家,我们对最高薪水的概率是正的(记住,薪水在 x 轴上,概率在 y 轴上)。

这只是一个简单的测试,确保你跟上了我的思路。现在,你不需要记住 Beta 分布的确切公式,但你必须知道它有两个参数,并且了解这些参数如何影响分布。不要忘记,当我们在实际解决问题时已经提到过这一点,现在它将得到更加详细的解释。

如果我们再次将这两个参数记作 ab,我们可以用以下方式表示 Beta 分布:

你可能会问刚才发生了什么——为什么会出现 ?别担心,我们会揭开所有的谜团。在上面的公式中, 是概率, 是仅关于 的函数, 是工资,而 是任何 Beta 分布中都有的两个参数。再说一次,你不需要知道函数 的确切定义,但只需记住其曲线形状,如前面的图所示。

然而,现在你需要真正理解的,是两个参数 的作用。接下来是你必须知道并在脑海中形象化的两点:

  1. 给定两个具有相同参数 的 Beta 分布,具有更大参数 的分布会向右偏移更多。

  2. 给定两个具有相同参数 的 Beta 分布,具有更大参数 的分布会向左偏移更多。

就是这样!这足以让你直观地理解我们的 AI 将如何解决强盗问题。换句话说,参数 越大,Beta 分布就越向右偏移,而参数 越大,Beta 分布就越向左偏移。

让我们来练习一下!如果我给你以下三个 Beta 分布:

你能告诉我下面图中的三个 Beta 分布大概是什么样子吗?

图 3:三个 Beta 分布

根据以上两点, 是紫色的, 是黄色的,而 是绿色的。如果你猜对了,恭喜你!

现在你已经准备好解决我们的强盗问题了。但让我先问你一个问题,这个问题可能会让你比这本书更快地理解其中的奥秘:

如果,X 轴不是国家的工资,而是赌场中机器的成功率,并且每一个 Beta 分布代表一个特定的老丨虎丨机,你会选择哪一个来下注 1,000 美元?

你会选择黄色的那个!

当然!这个分布具有最高转化率的正概率,因为它是最偏右的那个。

这在本章之前的代码部分已经讨论过;我在那里告诉你,参数越大的老丨虎丨机越好。事实上,Beta 分布会更多地偏向右侧,意味着这个老丨虎丨机更有可能给我们带来胜利。而第二个参数越大,老丨虎丨机就越差,现在,Beta 分布会偏向左侧,意味着这个机器给我们带来胜利的机会更小。

现在有另一个问题,在解决我们的赌博机问题之前。记住你有五个老丨虎丨机可以玩,试着回答这个问题:如果这五个老丨虎丨机与以下五个成功率的 Beta 分布相关联:

你会选择哪一个来下注 1,000 美元?

答案是 !

当然,还是那个!因为它的参数 最大,且参数 最小,所以它最偏向右侧,因此具有最高转化率的正概率。

如果你还在跟着我走,那么你绝对准备好理解 AI 的魔力了。如果没有,请再阅读这一部分。在下一部分,我将最终揭示第 5 轮之后会发生什么。

解决 MABP 问题

接下来,在每轮游戏之前,我们将为每个老丨虎丨机分配一个特定的 Beta 分布。在每轮 n 中,老丨虎丨机编号 i (i=1,2,3,4,5) 将与以下 Beta 分布关联:

在这里,你应该回想以下内容:

  • 是老丨虎丨机编号 在第 轮之前返回 1 奖励的次数。

  • 是老丨虎丨机编号 在第 轮之前返回 0 奖励的次数。

记住,在 Beta 分布 中,参数 越高,分布就越向右移动。参数 越高,分布就越向左移动。因此,由于在每一轮 和每个老丨虎丨机中,参数 是到第 轮为止返回 1 的次数(加 1),而参数 是到第 轮为止返回 0 的次数(加 1),这意味着以下几点:老丨虎丨机返回 1(成功)的次数越多,它的分布就会越向右移动;老丨虎丨机返回 0(失败)的次数越多,它的分布就会越向左移动。

恭喜你,如果你自己弄明白了 应该是什么。我们已经在上面的实用教程中使用过它们;我们有两个数组,nPosRewardnNegReward,它们分别对应

一旦你理解了这一点,尝试在我给你答案之前弄清楚策略。

好的,你即将看到魔法。我们将要做的是,在每一轮玩老丨虎丨机之前,从对应五台老丨虎丨机的五个分布中随机抽取一个值。如果你不清楚这是什么意思,我来解释一下。让我再次给你展示三条 Beta 分布的图像:

图 4:三条 Beta 分布

我说的随机抽取是什么意思?首先,记住在我们的老丨虎丨机问题中,x 轴上是从 0 到 1 的成功率。例如,x = 0.25 意味着机器有 25%的时间返回 1 的奖励(成功)。然后,在 y 轴上,我们仍然有这些成功率的概率。

让我们聚焦于一个分布,例如紫色分布。从该分布中随机抽取意味着什么呢?很简单,就是我们在 x 轴上随机选择一个分布值为正的位置,这样,在概率最高的 x 值就会有更大的机会被选中。例如,假设紫色曲线的最高点对应 x = 0.2 和 y = 0.35。

然后,从那个紫色分布中随机抽取意味着我们有 35%的概率选到成功率为 20%的值。为了推广这个概念,假设 是与紫色分布相关的函数,那么从该紫色分布中随机抽取意味着对于 x 轴上的每个成功率 x,我们有 的概率选择 x。这就是“从分布中随机抽取”的含义,这也叫做“从分布中采样”。

现在你理解了这一点,接下来让我们回顾一下进度。我们之前提到,在每一轮游戏之前,我们会从每台老丨虎丨机对应的五个 Beta 分布中进行随机抽取。因此,我们得到五个值,分别对应五台老丨虎丨机的 x 轴。接下来,问题就来了,这是决定你是否理解策略直觉的关键问题。

根据你自己的理解,你会选择玩哪个老丨虎丨机,基于这五个值的观察? 我真心希望你能花点时间来回答这个问题,因为现在我们正处于策略的核心部分(你也可以看看我们之前写的代码)。答案可以在下一个段落中找到。

我真心希望你能自己试着搞明白:你接下来要玩的老丨虎丨机是我们从五次随机抽样中获得的最高值对应的那个。为什么?因为最高的随机抽样值对应着最高的成功率,而对于这个最高成功率,关联的 Beta 分布在该成功率附近具有正的概率。

由于我们希望最大化我们所玩的机器的成功率(因为我们想赚钱),我们必须选择那个 Beta 分布在最高成功率附近具有正概率的老丨虎丨机。在下图中,那就是黄色分布。

图 5:三种 Beta 分布

现在,我们必须退一步。我曾多次身处你现在的境地,尤其是在学习一些新知识时,那些技术性的内容有时让人感到不知所措。在这种情况下,最好的做法就是退一步,而我们现在正是这样做的——回顾一下策略及其直觉。

三步法的汤普森抽样策略

在我们玩完前五轮中的每台老丨虎丨机之后,AI 在每一轮的操作如下所示

  1. 对于每个老丨虎丨机 i (i=1,2,3,4,5),我们从其 Beta 分布中进行一次随机抽取

    其中:

    是老丨虎丨机编号 在第 轮次之前返回 1 奖励的次数。

    是老丨虎丨机编号 在第 轮次之前返回 0 奖励的次数。

  2. 我们拉动了老丨虎丨机的手柄 ,该老丨虎丨机的抽样值最高

  3. 我们不忘更新

    如果所玩的老丨虎丨机 返回了 1 奖励:

    如果所玩的老丨虎丨机 返回了 0 奖励:

然后,我们在每一回合重复这三步,直到我们花完了 1,000 美元。这种策略被称为汤普森抽样法,它是 AI 的一个特定分支——强化学习中的一个基础但强大的模型。

塑造你汤普森抽样直觉的最后一笔

你对这个方法为什么有效以及它是如何有效的直觉应该是这样的(尽量记住它或者在图示上可视化):

每个老丨虎丨机都有自己独特的 Beta 分布。在每一回合中,转换率最高的老丨虎丨机的 Beta 分布会逐渐向右移动,而转换率较低的策略的 Beta 分布会逐渐向左移动(步骤 1 和步骤 3)。因此,由于步骤 2 的原因,转换率最高的老丨虎丨机会越来越频繁地被选择。

看!恭喜你——你刚刚学会了一个强大的 AI 模型,这是你旅程中的一个重要步骤。为了展示汤普森抽样法的实际应用,并验证它确实有效,我不会强迫你去赌场试一试;我们将在第六章面向销售和广告的 AI —— 像 AI 街的狼一样卖东西中,将它应用于另一个现实中的模型。

最后,让我用一个问题结束这部分理论教程。记得在书的前面我告诉过你,我们今天构建的任何 AI 都会接受一个状态作为输入,返回一个要执行的动作作为输出,执行该动作后会得到一个奖励(正向或负向)。对于这个特定的强盗问题,输入状态是什么?执行的动作是什么?获得的奖励是什么? 在阅读下一个段落之前,先思考一下。

下面是答案:

  • 输入状态是我们当前所处的具体回合,包含两个参数的信息!

  • 输出的动作是从选中的老丨虎丨机中拉动的臂。

  • 奖励是 1 或 0,如果老丨虎丨机返还是我们投入的两倍金额,则奖励为 1;如果我们输掉了一美元,则奖励为 0。

如果你答对了这一题,恭喜你,同时也祝贺你成功挑战了第一个 AI 模型——汤普森抽样法。别忘了,在第六章面向销售和广告的 AI —— 像 AI 街的狼一样卖东西,我们将这一方法应用于实践,解决了一个真实的商业问题。

汤普森抽样法与标准模型的对比

当我第一次学习汤普森抽样法时,我心里有一个主要问题:它真有这么好吗?事实上,如果你分别运行标准模型(“标准模型”指的是每个老丨虎丨机都玩一定次数)和汤普森抽样法,你可能看不出太大差别;你可能得出结论,它们的效果差不多。

为了验证 Thompson Sampling 是否真的没有优势,我编写了代码,在许多不同的场景中测试这两种解决方案。测试的变更包括:样本数量(200 或 1,000 或 5,000)、老丨虎丨机数量(从 3 到 20)、以及转化率范围(可设置的转化率范围:0-0.1;0-0.3;0-0.5)。

每个场景都被测试了 100 次,以计算每个模型的准确率。

结果和使用的代码分别保存在本书 GitHub 页面 Chapter 05 中的 resultsModified.xlsxcomparison.py 文件中。在这里,你可以看到一些从该 Excel 文件中提取的图表,展示了两种模型的表现:

图 6:准确率与老丨虎丨机数量的关系(200 个样本)

图 6 中的第一张图展示了两种模型在不同老丨虎丨机数量下的准确率。样本数量设置为 200,转化率范围设置为 0-0.1,这意味着这些转化率之间的差异较小。这是本次对比中的最严苛设置。总体而言,Thompson Sampling 的表现优于标准模型(提高了 22%)。

图 7:准确率与老丨虎丨机数量的关系(5,000 个样本)

图 7 中的第二张图展示了在最简单条件下的表现。样本数量设置为 5,000,转化率范围设置为 0-0.5,这意味着差异非常明显。Thompson Sampling 的整体准确率下降小于标准解法的准确率下降。Thompson Sampling 这次表现显著更好(提高了 41%)。

考虑所有场景后,Thompson Sampling 的平均准确率为 57%,标准模型的准确率为 43%。考虑到测试的是非常严苛的场景(例如,仅有 200 个样本,转化率范围为 0-0.1,以及 20 台老丨虎丨机),这一差异非常显著。

总结

Thompson Sampling 是一种强大的采样技术,使你能够快速找出一组未知转化率中最高的那个。它总是应用于同一个框架,即多臂老丨虎丨机问题,经典意义上该问题由几台老丨虎丨机组成,每台老丨虎丨机具有不同的正面结果转化率。我们已经初步了解了这种 AI 如何比标准方法更好、更快地解决这个问题。

在下一章中,我们将进行一次完整的实践活动,展示如何利用多臂老丨虎丨机框架轻松地建模一个商业问题——在线广告,并且如何通过 Thompson Sampling 带来显著的附加值。

第六章:销售和广告中的人工智能 – 像《AI 街头的狼》一样销售

现在是时候将你新学到的技能付诸实践,开始编码,提升你的人工智能技能了!你已经学会了所有关于汤普森采样的知识,现在是时候实施这个人工智能模型来解决一个现实问题,最大化电子商务企业的销售额。

在这个实践练习中,你将真正采取行动,亲自构建人工智能来解决问题。保持积极性非常重要,因为这是你将有机会通过实践来学习的地方,而实践是最有效的学习方式;实践造就完美。换句话说,我希望你成为这次人工智能冒险的英雄。是你,而不是我。准备好了吗?

待解决问题

想象一个拥有数百万客户的电子商务企业。这些客户是偶尔在网站上购买产品的人,购买的产品会送到他们家里。企业的经营情况良好,但执行董事会决定采取行动计划来最大化收入。

这个计划包括为客户提供订阅高级计划的选项,这将为他们带来一些福利,如折扣、特价等。这个高级计划的年费为 200 美元,而这个电子商务企业的目标当然是让尽可能多的客户订阅这个高级计划。让我们做一些快速的数学计算,激励我们构建一个人工智能来最大化这个企业的收入。

假设这个电子商务企业有 1 亿客户。现在考虑两种策略来将客户转化为高级计划:一种差的,转化率为 1%,另一种好的,转化率为 11%。如果企业部署了差的策略,一年后它将从高级计划订阅中获得总计:100,000,000 × 0.01 × 200 = 2 亿美元的额外收入。

另一方面,如果企业部署了好的策略,一年后将从高级计划订阅中获得总计:100,000,000 × 0.11 × 200 = 22 亿美元的额外收入。通过找出最佳策略来部署,企业通过增加 20 亿美元的收入最大化了其收益。

在这个乌托邦式的例子中,我们只有两种策略,而且我们知道它们的转化率。在我们的案例研究中,我们将面临九种不同的策略。我们的人工智能完全不知道哪种是最好的,也没有任何关于它们转化率的先验信息。

然而,我们会假设这九种策略每一种都有固定的转化率。这些策略是市场团队经过精心设计和巧妙策划的,每种策略的目标都是将尽可能多的客户转化为高级计划订阅者。然而,这九种策略是不同的。它们有不同的形式、不同的套餐、不同的广告和不同的特惠,目的是说服客户订阅高级计划。当然,市场团队并不知道哪种策略会是最佳的。让我们总结一下这九种策略的特点差异:

图 1:九种策略——哪一种卖得最好?

市场团队希望尽快找出哪种策略的转化率最高,并且以最小的投入实现这一目标。他们知道,找到并实施最佳策略可以显著提高业务收入。市场专家们也选择不直接向他们的 1 亿客户发送电子邮件,因为那样做既昂贵又可能会导致过多客户被垃圾邮件骚扰。相反,他们将通过在线学习微妙地寻找最佳策略。那么,什么是在线学习呢?它指的是每当客户浏览电子商务网站时,部署不同的策略。

当客户浏览网站时,他们会突然看到一个弹出广告,建议他们订阅高级计划。对于每个浏览网站的客户,只有九种策略中的一种会被展示。然后,用户会选择是否采取行动并订阅高级计划。如果客户订阅了,则策略成功;否则,策略失败。我们进行的客户测试越多,收集的反馈就越多,我们对最佳策略的认识也就越清晰。

当然,我们不会手动、通过视觉或简单的数学来解决这个问题。相反,我们希望实现最智能的算法,它能在最短的时间内找出最佳策略。这有两个原因:首先,因为每次部署策略都会产生成本(例如,来自弹出广告的成本);其次,因为公司希望尽量减少广告对客户的骚扰。

在模拟中构建环境

本节内容非常特殊,因为有一些关键点需要理解,这些并不是一开始就显而易见的。之所以有这个警告,是因为我在教授这门课程时的经验;我的许多学生曾经很难理解,为什么我们在这个问题中必须做一个模拟。

当我开始时也是这样的!如果你已经理解了为什么我们必须做模拟,那太好了——这意味着你已经把在线学习内化了。如果没有,请跟着我来,让我仔细为你解释。

为了理解,我们从现实生活中会发生什么开始:你只需将九种策略中的“号召性广告”弹窗展示给正在浏览网站的客户,每次只展示给一个客户。你需要一位一位客户地展示,因为每个客户你都需要收集他们的反馈:客户是否选择订阅高级计划。如果客户选择,奖励为 1;如果不选择,奖励为 0。过程如下:

第 1 轮:我们将策略 1广告 1展示给客户客户 1,并检查客户是否选择订阅。如果选择,则获得 1 的奖励;如果不选择,则获得 0 的奖励。收集到奖励后,我们继续进行下一个客户(下一轮)。

第 2 轮:我们将策略 2广告 2展示给新客户客户 2,并检查客户是否选择订阅。如果选择,则获得 1 的奖励;如果不选择,则获得 0 的奖励。收集到奖励后,我们继续进行下一个客户(下一轮)。

第 9 轮:我们将策略 9广告 9展示给新客户客户 9,并检查客户是否选择订阅。如果选择,则获得 1 的奖励;如果不选择,则获得 0 的奖励。收集到奖励后,我们继续进行下一个客户(下一轮)。

第 10 轮:我们终于开始激活汤普森采样!我们使用汤普森采样 AI 来告诉我们哪个广告具有最强的魔力,能够将最多客户转化为订阅高级计划。我们想要那笔额外收入!AI(由汤普森采样提供支持)选择 9 个广告中的一个展示给新客户客户 10,然后检查客户是否选择订阅。如果选择,则获得 1 的奖励;如果不选择,则获得 0 的奖励。收集到奖励后,我们继续进行下一个客户(下一轮)。

第 11 轮:AI(由汤普森采样提供支持)选择 9 个广告中的一个展示给新客户,假设是客户 11,然后检查客户是否选择订阅。如果选择,则获得 1 的奖励;如果不选择,则获得 0 的奖励。收集到奖励后,我们继续进行下一个客户(下一轮)。

好的,我停下来了!你明白了。这个过程会一直持续下去,持续几百轮,或者至少直到 AI 找出最好的广告——那个转化率最高的广告。

这就是现实生活中的情况。在每一轮中,我们不需要其他任何东西;如果你查看汤普森采样算法,你会发现每一轮它只需要知道每个广告在前几轮中获得 1 奖励的次数,以及获得 0 奖励的次数。总之,这是一个非常重要的结论:汤普森采样完全不需要知道广告的转化率就能找出最好的广告。

然而,为了模拟这个应用,我们需要为每个广告分配一个转化率。原因很简单:如果我们不这么做,就无法验证汤普森抽样是否确实找到了最佳广告。这只是为了验证 AI 是否正常工作!

我们将为这九种策略分配不同的转化率。这个模拟的目的只是为了检查 AI 是否能够找到转化率最高的广告。让我将这一点重新表述为两个关键点:

  1. 汤普森抽样在任何时候都不需要知道转化率,以便找出最高的那个。

  2. 我们之所以提前知道这些转化率,是因为我们正在进行模拟,目的是检查汤普森抽样是否能够找出转化率最高的广告。

现在我们已经解决了这个问题,接下来让我们设定这些转化率。我们假设这九种策略的转化率如下:

图 2:九种策略的转化率

现在,我们在幕后提前知道哪种策略的转化率最高:策略 7。然而,汤普森抽样并不知道这一点。如果你注意到,你会看到汤普森抽样在执行其算法时,从未使用过转化率。它只知道前几轮中成功(订阅)和失败(未订阅)的次数。你可以在代码中最清楚地看到这一点。

最后,请务必记住,在现实情况下,我们是无法知道这些转化率的具体数值的。我们在这里只知道它们是为了模拟的目的,这样我们最终才能检查出我们的 AI 是否找到了最佳策略——在我们的模拟中,这就是策略 7

接下来的问题是:我们到底如何运行这个模拟呢?

运行模拟

首先,让我们回顾一下环境的不同组成部分(状态、动作和奖励):

  1. 状态就是特定的客户,我们将在其身上部署策略并向其展示该策略的广告。

  2. 动作是选择部署给客户的策略。

  3. 如果客户订阅了高级计划,则奖励为 1,反之为 0。

那么,假设这个电子商务公司想要通过 10,000 个客户来进行实验,找出最佳策略。为什么选择 10,000?因为从统计学角度来看,这已经是一个足够大的样本量,能够代表整个客户群。那么,我们如何基于之前设定的广告转化率来模拟这 10,000 个客户的反应呢?我们没有其他选择,只能拿出像 Excel 或 Google Sheets 这样的电子表格来模拟这 10,000 个客户对每一条广告的反应。我们将按照以下方式进行,这其实是一个很巧妙的技巧。

我们将创建一个包含 10,000 行和 9 列的矩阵。每一行对应一个特定的客户,每一列对应一个特定的策略。为了更清楚地说明,假设:

第一行对应于客户 1

第二行对应于客户 2

第 10000 行对应于客户 10000

第一列对应于策略 1

第二列对应于策略 2

第九列对应于策略 9

在这个矩阵的单元格中,我们会根据这 10,000 个客户对 9 个策略的反应来放置奖励 1 或 0。反应是正面(订阅)还是负面(不订阅)。这就是“相当不错的技巧”的用武之地。为了模拟这 10,000 个客户对 9 个广告的反应,同时考虑到这些广告的转化率,我们做了以下处理:

对于每个客户(行)和每个策略(列),我们从 0 到 1 之间随机抽取一个数字。如果这个随机数小于策略的转化率,则奖励为 1;如果随机数大于策略的转化率,则奖励为 0。为什么这么做有效?因为这样,我们就能保证每个策略对于每个客户都能保持p%的概率获得奖励 1,其中p是应用于该客户的策略的转化率。

例如,我们来看一下策略 4,它的转化率是 0.16。对于每个客户,我们从 0 到 1 之间随机抽取一个数字。这个随机数有 16%的概率位于 0 到 0.16 之间,剩下的 84%概率位于 0.16 到 1 之间。因此,当我们的随机数位于 0 到 0.16 之间时,我们得到 1,而当它位于 0.16 到 1 之间时,我们得到 0。这意味着我们有 16%的概率得到 1,84%的概率得到 0。

这完全模拟了当策略 4应用于某个客户时,该客户有 16%的概率订阅高级计划;这正好对应于获得奖励 1。

希望你喜欢这个技巧。它相当经典,但在 AI 中经常使用,了解它对你来说很重要。我们将这个技巧应用于每一对(客户,策略),即这 10,000 x 9 个组合,得到如下矩阵(此图只展示了前 10 行):

图 3:奖励的模拟矩阵

让我们详细分析前三行:

  1. 第一位客户(索引为0的行)在接触到任何策略时都不会订阅高级计划。

  2. 第二位客户(索引为1的行)只有在接触到策略 5策略 7时才会订阅高级计划。

  3. 第三位客户(索引为2的行)在任何策略下都不会订阅高级计划。

我们已经可以在这个预览中看到我们的小技巧奏效了;具有最低转化率的广告(策略 1、6 和 9)在前 11 位顾客中只有 0 的奖励,而具有最高转化率的广告(策略 4 和 7)已经有一些 1 的奖励。请注意,这里 Python 表中的索引从 0 开始;在 Python 中总是这样的,不幸的是我们无法改变这一点。不过,别担心,你会习惯的!

如果你是代码爱好者,生成这个模拟的代码将在本章稍后展示。

我们的下一步是退后一步并回顾一下。

回顾

我们准备好模拟汤普森抽样在连续 10,000 名顾客身上的行动,这些顾客逐一被接触到的九种策略之一,感谢之前的矩阵,它将精确模拟顾客决定是否订阅高级计划的决策。

如果对应于特定顾客和特定选择的策略的单元格为 1,那么这就模拟了顾客订阅高级计划。如果单元格为 0,则模拟拒绝。汤普森抽样将收集每个顾客是否订阅高级计划的反馈,逐个顾客进行。然后,借助其强大的算法,它将迅速找出具有最高转化率的策略。

这个策略是部署在数百万客户身上的最佳策略,最大化公司从这个新收入流的收入。

AI 解决方案和直觉复习

在你享受看到你的 AI 在行动之前,让我们回顾一下,并将整个汤普森抽样 AI 模型适应这个新问题。

顺便说一句,如果你不喜欢这个电子商务业务应用程序,完全可以想象自己回到赌场,周围有九台老丨虎丨机,其转化率与我们的策略所给出的转化率相同。这正是相同的情景;这九种策略可能很可能就像九台老丨虎丨机,给出相同的转化率,无论是 1(赚钱)还是 0(失去钱)。你的目标是尽快找出哪台老丨虎丨机有最大的中奖机会!完全由你决定。不妨选择去拉斯维加斯或 AI 街区,但是在本章中,我将坚持我们的电子商务。

首先,让我们提醒自己,每次向新顾客展示广告都被视为新的一轮,n,并选择我们的九种策略之一来尝试转化(订阅高级计划)。目标是在尽可能少的轮数内找出最佳策略(与具有最高转化率的广告相关)。以下是汤普森抽样的工作原理:

AI 解决方案

对于每一轮 超过 10,000 轮,重复以下三个步骤:

步骤 1:对于每个策略 ,从以下分布中随机抽取一个值:

其中:

  1. 是策略 在回合 之前获得 1 奖励的次数。

  2. 是策略 在回合 之前获得 0 奖励的次数。

步骤 2:选择具有最高 的策略

步骤 3:根据以下条件更新

  1. 如果选择的策略 获得了 1 奖励:

  2. 如果选择的策略 获得了 0 奖励:

现在我们已经看过了数学步骤,让我们回顾一下这些步骤背后的直觉。

直觉

每个策略都有自己独特的 Beta 分布。随着回合的进行,具有最高转化率的策略的 Beta 分布将逐渐向右移动,而具有较低转化率的策略的 Beta 分布将逐渐向左移动(步骤 13)。因此,在 步骤 2 中,具有最高转化率的策略将被越来越多地选择。以下是展示三种策略的三种 Beta 分布的图表,帮助你可视化这一过程:

图 4:三种 Beta 分布

你已经退后一步并进行了复习;我认为你现在已经准备好进行实现了!在接下来的章节中,你将把所有理论付诸实践——换句话说,付诸于代码。

实现

你将在本章中逐步开发代码,但请记住,我已经提供了此应用程序的汤普森采样完整实现;你可以在本书的 GitHub 页面(github.com/PacktPublishing/AI-Crash-Course)上找到。如果你想尝试并运行代码,可以在 Colaboratory、Anaconda 中的 Spyder 或者你喜欢的 IDE 上进行。

汤普森采样与随机选择

在实现汤普森采样的同时,你还将实现随机选择算法,该算法将在每一回合中随机选择一个策略。这将作为你评估汤普森采样模型性能的基准。当然,汤普森采样和随机选择算法将在同一个模拟环境中竞争,也就是说,在相同的环境矩阵上进行竞争。

性能度量

最后,在整个模拟完成后,你可以通过计算相对回报来评估汤普森采样的性能,定义如下公式:

你还将有机会绘制所选广告的直方图,以便检查具有最高转化率的策略(策略 7)是否是被选中最多的。

让我们开始编写代码

首先,导入以下三个必需的库:

  1. numpy,你将用它来构建环境矩阵。

  2. matplotlib.pyplot,你将用它来绘制直方图。

  3. random,你将用它来生成模拟所需的随机数。

以下是从 GitHub 提取的代码:

# AI for Sales & Advertizing - Sell like the Wolf of AI Street
# Importing the libraries
import numpy as np
import matplotlib.pyplot as plt
import random 

然后设置客户数量和策略的参数:

  1. N = 10,000 个客户。

  2. d = 9 种策略。

代码

# Setting the parameters
N = 10000
d = 9 

然后,通过构建一个环境矩阵来创建模拟,该矩阵有 10,000 行代表客户,9 列代表策略。每一回合,对每个策略,你都从 0 到 1 之间随机抽取一个数字。如果这个随机数字低于该策略的转换率,奖励为 1;否则奖励为 0。环境矩阵在代码中命名为X

代码

# Building the environment inside a simulation
conversion_rates = [0.05,0.13,0.09,0.16,0.11,0.04,0.20,0.08,0.01]
X = np.array(np.zeros([N,d]))
for i in range(N):
    for j in range(d):
        if np.random.rand() <= conversion_rates[j]:
            X[i,j] = 1 

现在环境已经准备好,你可以开始实现 AI 了。首先一步是引入并初始化实现过程中需要的变量:

  1. strategies_selected_rs:一个列表,存储 Random Selection 算法在各回合中选择的策略。初始化为空列表。

  2. strategies_selected_ts:一个列表,存储 Thompson Sampling AI 模型在各回合中选择的策略。初始化为空列表。

  3. total_rewards_rs:Random Selection 算法在各回合中累计的总奖励。初始化为 0。

  4. total_rewards_ts:Thompson Sampling AI 模型在各回合中累计的总奖励。初始化为 0。

  5. number_of_rewards_1:一个包含 9 个元素的列表,每个元素记录每个策略收到 1 奖励的次数。初始化为包含 9 个零的列表。

  6. number_of_rewards_0:一个包含 9 个元素的列表,每个元素记录每个策略收到 0 奖励的次数。初始化为包含 9 个零的列表。

代码

# Implementing Random Selection and Thompson Sampling
strategies_selected_rs = []
strategies_selected_ts = []
total_reward_rs = 0
total_reward_ts = 0
numbers_of_rewards_1 = [0] * d
numbers_of_rewards_0 = [0] * d 

接下来,你需要开始for循环,遍历环境矩阵的 10,000 行(即客户)。在每一回合,你将得到两个独立的策略选择:一个来自 Random Selection 算法,另一个来自 Thompson Sampling。

我们从 Random Selection 算法开始,它每回合随机选择一个策略。

代码

for n in range(0, N):
    # Random Selection
    strategy_rs = random.randrange(d)
    strategies_selected_rs.append(strategy_rs)
    reward_rs = X[n, strategy_rs]
    total_reward_rs = total_reward_rs + reward_rs 

接下来,你需要按照之前提供的步骤 1步骤 2步骤 3实现 Thompson Sampling。我建议在编写下一部分代码之前,再次回顾这些步骤,并尝试自己先编写代码,而不是直接查看我的解决方案。这是你进步的最佳方式;实践出真知。你已经拥有了编写代码所需的所有元素,甚至在第五章《你的第一个 AI 模型——小心强盗!》中有类似的代码。祝你好运!以下是解决方案。

你应该一步一步实现 Thompson Sampling,从第一步开始。让我们回顾一下它:

步骤 1:对于每个策略 ,从以下分布中随机抽取一个值:

其中:

  1. 是策略 在第 轮次之前获得奖励 1 的次数。

  2. 是策略 在第 轮次之前获得奖励 0 的次数。

让我们看看 步骤 1 是如何实现的。

编写第二个 for 循环,遍历这 9 个策略,因为你需要从每个策略的 Beta 分布中进行一次随机抽取。

从 Beta 分布中抽取的随机数是通过从 random 库中引入的 betavariate() 函数生成的,该函数在一开始时就已被导入。

代码

 # Thompson Sampling
    strategy_ts = 0
    max_random = 0
    for i in range(0, d):
        random_beta = random.betavariate(numbers_of_rewards_1[i] + 1, numbers_of_rewards_0[i] + 1) 

现在实现 步骤 2,即:

步骤 2:选择具有最高 的策略

要实现 步骤 2,你需要待在第二个 for 循环中,它遍历 9 个策略,并使用一个简单的技巧,借助 if 条件来找出最高的

诀窍如下:在迭代策略时,如果你发现一个随机抽取(random_beta)大于迄今为止获得的最大随机抽取值(max_random),那么该最大值就更新为这个更高的随机抽取值。

代码

 # Thompson Sampling
    strategy_ts = 0
    max_random = 0
    for i in range(0, d):
        random_beta = random.betavariate(numbers_of_rewards_1[i] + 1, numbers_of_rewards_0[i] + 1)
        if random_beta > max_random:
            max_random = random_beta
            strategy_ts = i
    reward_ts = X[n, strategy_ts] 

最后,让我们实现 步骤 3,这是最简单的一步:

步骤 3:根据以下条件更新

  1. 如果选择的策略 收到奖励 1:

  2. 如果选择的策略 收到奖励 0:

简单地使用完全相同的两个 if 条件,将它们转化为代码实现。

代码

 # Thompson Sampling
    strategy_ts = 0
    max_random = 0
    for i in range(0, d):
        random_beta = random.betavariate(numbers_of_rewards_1[i] + 1, numbers_of_rewards_0[i] + 1)
        if random_beta > max_random:
            max_random = random_beta
            strategy_ts = i
    reward_ts = X[n, strategy_ts]
    if reward_ts == 1:
        numbers_of_rewards_1[strategy_ts] = numbers_of_rewards_1[strategy_ts] + 1
    else:
        numbers_of_rewards_0[strategy_ts] = numbers_of_rewards_0[strategy_ts] + 1 

接下来,别忘了将步骤 2 中选择的策略添加到我们的策略列表(strategies_selected_ts)中,并计算汤普森采样在各轮中累计的总奖励(total_reward_ts)。

代码

 # Thompson Sampling
    strategy_ts = 0
    max_random = 0
    for i in range(0, d):
        random_beta = random.betavariate(numbers_of_rewards_1[i] + 1, numbers_of_rewards_0[i] + 1)
        if random_beta > max_random:
            max_random = random_beta
            strategy_ts = i
    reward_ts = X[n, strategy_ts]
    if reward_ts == 1:
        numbers_of_rewards_1[strategy_ts] = numbers_of_rewards_1[strategy_ts] + 1
    else:
        numbers_of_rewards_0[strategy_ts] = numbers_of_rewards_0[strategy_ts] + 1
    strategies_selected_ts.append(strategy_ts)
    total_reward_ts = total_reward_ts + reward_ts 

然后计算最终得分,这是相对于我们基准的汤普森采样的相对回报,基准是随机选择:

代码

# Computing the Relative Return
relative_return = (total_reward_ts - total_reward_rs) / total_reward_rs * 100
print("Relative Return: {:.0f} %".format(relative_return)) 

最终结果

执行此代码后,我得到了 91% 的最终相对回报。换句话说,汤普森采样几乎将我的随机选择基准的性能提高了两倍。还不错吧!

最后,绘制所选策略的直方图,以检查 策略 7(索引 6)是否是被选中的最多的策略,因为它具有最高的转换率。为此,使用 matplotlib 库中的 hist() 函数。

代码

# Plotting the Histogram of Selections
plt.hist(strategies_selected_ts)
plt.title('Histogram of Selections')
plt.xlabel('Strategy')
plt.ylabel('Number of times the strategy was selected')
plt.show() 

这是最激动人心的时刻——代码完成了(顺便说一句,恭喜你),你可以享受结果了。拥有最终的相对回报很好,但用干净的可视化图表展示结果会更好。执行最终代码后,你就能得到这一点:

图 5:选择的直方图

你可以看到,索引 6 的策略,策略 7,是被选中的最多的。汤普森采样法很快就能将其识别为最佳策略。事实上,如果你重新运行相同的代码,但只使用 1,000 个客户,你会发现汤普森采样法仍然能够识别 策略 7 为最佳策略。

汤普森采样法为这个电子商务企业做得非常出色。它不仅能够在少量轮次内识别出最佳策略——这意味着较少的客户,从而节省广告和运营成本——而且它还能够清晰地找出转换率最高的策略。

如果这个电子商务企业有 5000 万个客户,并且高级计划的价格是每年 200 美元,那么部署这项最佳策略,且转换率为 20%,将产生额外的收入:50,000,000 × 0.2 × 200 美元 = 20 亿美元!

换句话说,汤普森采样法清晰而迅速地让这个电子商务企业在销售和广告方面大获成功,甚至可以说它真的是 AI 街头的“狼”。

现在,休息一下吧,你值得拥有。放松一下,一旦你充电完毕,准备好迎接新的 AI 探险,我也会在这里,准备好开始下一章。很快再见!

总结

在这个第一个实践教程中,你实现了汤普森采样法来解决多臂老丨虎丨机问题,应用于广告活动中。汤普森采样法能够迅速找到最佳的商业策略,这是随机选择法无法做到的。总的来说,你实现了 91% 的相对回报,这在做出一些假设后,将额外带来 20 亿美元的收入。你仅用一个文件、不到 60 行代码就实现了这一切。相当惊人,对吧?

第七章:欢迎来到 Q 学习

各位女士们,先生们,事情即将变得比以往更有趣。我们即将处理的下一个模型是今天许多人工智能的核心;机器人、自动驾驶车辆,甚至视频游戏的 AI 玩家都在使用 Q 学习作为核心模型。它们中的一些甚至将 Q 学习与深度学习结合,创造出了一个更高级的 Q 学习版本,叫做深度 Q 学习,我们将在第九章《成为人工智能专家——深度 Q 学习》中讲解。

所有的 AI 基础仍然适用于 Q 学习,具体如下:

  1. Q 学习是一个强化学习模型。

  2. Q 学习基于输入(状态)和输出(动作)的原理。

  3. Q 学习在一个预定义的环境中工作,包括状态(输入)、动作(输出)和奖励。

  4. Q 学习是通过马尔可夫决策过程建模的。

  5. Q 学习使用一种训练模式,在这个模式中,学习到的参数称为 Q 值,还有一种推理模式。

现在我们可以再添加两个基本原理,这次是特定于 Q 学习的:

  1. 状态是有限的(没有无限多的可能输入)。

  2. 动作是有限的(只能执行有限的动作)。

就这样!没有更多需要记住的基础了;现在我们可以真正深入探讨 Q 学习,你会发现它其实并不难,而且非常直观。

为了解释 Q 学习,我们将使用一个例子,这样你就不会迷失在纯粹的理论中,也能直观地理解发生了什么。顺便说一句:欢迎来到迷宫。

迷宫

你将学习 Q 学习在迷宫中的工作原理。我们现在就来画出我们的迷宫;它在这里:

图 1:迷宫

我知道,这个迷宫是你见过的最简单的迷宫。这对于简化问题很重要,这样你可以主要专注于人工智能如何发挥它的魔力。想象一下,如果你因为迷宫而迷失在这一章,而不是因为 AI 公式的话,那可就糟糕了!关键是你有一个清晰的迷宫,你可以想象人工智能是如何从起点走到终点的。

说到开始和结束,想象一下一个小机器人在这个迷宫中,从E(入口)点开始。它的目标是找到通向G(目标)点的最快路径。我们人类能在瞬间搞定,但那只是因为我们的迷宫太简单了。你将要构建的是一个能够从起点走到终点的人工智能,不管迷宫有多复杂。我们开始吧!

开始

这是一个问题:你认为第一步应该是什么?

我将给你三个可能的答案:

  1. 我们开始写一些数学方程。

  2. 我们构建环境。

  3. 我们尝试通过汤普森采样(上一章的 AI 模型)使其工作。

正确答案是……

2. 我们构建环境。

这很简单,但我想通过提问来强调这一点,以确保你记住构建 AI 时这一点必须始终是第一步。在明确理解问题后,构建 AI 解决方案的第一步始终是设置环境。

这引出了一个进一步的问题:

构建该环境时,具体需要采取哪些步骤?

尝试记住答案——你已经学过了——然后继续阅读以进行回顾。

  1. 首先,你将定义状态(你 AI 的输入)。

  2. 其次,你将定义可以执行的动作(你 AI 的输出)。

  3. 第三,你将定义奖励。记住,奖励是 AI 在某个状态下执行动作后获得的结果。

现在我们已经掌握了基础,你可以开始处理定义环境的第一步。

构建环境

为了构建环境,我们需要定义状态、动作和奖励。

状态

让我们从状态开始。你认为这个问题的状态是什么?记住,状态是你 AI 的输入。它们应该包含足够的信息,让 AI 能够采取一种行动,从而带领它实现最终目标(到达 E 点)。

在这个模型中,我们没有太多选择。在特定时间或特定迭代时,状态将仅仅是 AI 当时的位置。换句话说,它将是 AL 之间的字母,表示 AI 在特定时刻所在的位置。

正如你可能猜到的,构建环境之后的下一步是编写 AI 核心的数学方程式,为了帮助你做到这一点,将状态编码为独特的整数要比将其保持为字母更加容易。这正是我们将要做的,使用以下映射:

图 2:位置与状态映射

请注意,我们遵守 Q 学习的第一个基本原则,即:状态的数量是有限的

让我们继续讨论动作。

动作

动作将简单地是 AI 从一个位置移动到下一个位置的下一步。例如,假设 AI 当前在 J 位置,AI 可以执行的可能动作是前往 IFK。同样,由于你将使用数学方程式,你可以将这些动作用与状态相同的索引进行编码。

根据之前的示例,假设 AI 在特定时间位于 J 位置,AI 可以执行的可能动作是 5810,根据我们之前的映射:索引 5 对应 F,索引 8 对应 I,索引 10 对应 K

因此,可能的动作只是可以到达的不同位置的索引:

可能的动作 = {0,1,2,3,4,5,6,7,8,9,10,11}

请注意,我们再次遵循 Q 学习的第二个基本原则,即:动作的数量是有限的

显然,在一个特定的位置时,有些动作是 AI 无法执行的。以之前的例子为例,如果 AI 处于位置J,它能执行动作5810,但无法执行其他动作。你可以通过为无法执行的动作指定 0 奖励,为能够执行的动作指定 1 奖励来确保这一点。这就涉及到了奖励。

奖励

你离构建环境已经不远了——最后,你需要定义一个奖励系统。更具体地说,你需要定义一个奖励函数 R,它接受状态 s 和动作 a 作为输入,并返回一个数值奖励 r,即 AI 在状态 s 下执行动作 a 时将获得的奖励:

R: (s, a)

那么,如何为我们的案例研究构建这样的函数呢?这里很简单。由于有离散且有限的状态数量(从 0 到 11 的索引),以及离散且有限的动作数量(同样是从 0 到 11 的索引),构建奖励函数 R 的最佳方法就是直接构建一个矩阵。

你的奖励函数将是一个恰好有 12 行 12 列的矩阵,其中行对应状态,列对应动作。这样,在你的函数 R 中: (s, a) s 将是矩阵的行索引,a 将是矩阵的列索引,而 r 将是矩阵中索引为(s, a)的单元格的值。

要构建这个奖励矩阵,首先你需要为 12 个位置中的每一个指定一个奖励。对于机器人不能执行的动作,给予 0 奖励;对于机器人能执行的动作,给予 1 奖励。通过对每个位置执行这一步,你将得到一个奖励矩阵。我们从第一个位置:位置A开始,一步一步地构建它。

当机器人处于位置A时,它只能前往位置B。因此,由于位置A的索引为 0(矩阵的第一行),而位置B的索引为 1(矩阵的第二列),所以奖励矩阵的第一行将在第二列标记 1,其他列则为 0,如下所示:

图 3:奖励矩阵 - 第一步

让我们继续讨论位置B。当机器人处于位置B时,它只能前往三个不同的位置:ACF。由于B的索引为 1(第二行),而ACF分别有索引 0、2 和 5(第一列、第三列和第六列),因此奖励矩阵的第二行将在第一列、第三列和第六列上标记 1,其他列则为 0:

图 4:奖励矩阵 - 第二步

C(索引为 2)仅与BG(索引为 1 和 6)相连,因此奖励矩阵的第三行是:

图 5:奖励矩阵 – 步骤 3

对所有其他地点做相同的操作,最终你将得到最终的奖励矩阵:

图 6:奖励矩阵 - 步骤 4

这就是如何初始化奖励矩阵的方式。

但是等一下——你其实还没有完成。还有一件最后的事情需要做。这一步至关重要,需要理解。事实上,让我问你一个问题,最终问题,这将检查你的直觉是否已经开始形成:

你如何让 AI 知道它必须去那个优先级最高的地点 G?

这很简单——你只需通过奖励来玩弄它。你必须记住,在强化学习中,一切都从奖励开始。如果你为地点G分配一个高奖励,例如 1000,那么 AI 将自动试图去获取这个高奖励,仅仅因为它比其他地点的奖励要大。

简而言之,这是强化学习中需要理解和记住的一个基本点,AI 始终在寻找最高的奖励。这就是为什么到达地点G的诀窍就是给予它比其他地点更高的奖励。

目前,手动在对应地点G的单元格中放入一个高奖励(1000),因为它是我们希望 AI 前往的目标地点。由于地点G的索引是 6,我们就在第 6 行第 6 列的单元格中放置 1000 的奖励。相应地,我们的奖励矩阵变为:

图 7:奖励矩阵 - 步骤 5

你已经定义了奖励!你通过构建这个奖励矩阵实现了这一点。重要的是要理解,这通常是我们在进行 Q 学习时定义奖励系统的方式。

第九章与人工大脑一同成长——深度 Q 学习中,你将看到我们将以非常不同的方式进行,并且构建环境的过程会更加简单。事实上,深度 Q 学习是 Q 学习的高级版本,它在今天的 AI 中被广泛使用,远远超过了简单的 Q 学习模型。但是,你必须首先深入理解 Q 学习,才能为深度 Q 学习做好准备。

既然你已经定义了状态、动作和奖励,你就完成了环境的构建。这意味着你已经准备好处理下一步,构建将在你刚刚定义的这个环境中发挥作用的 AI。

构建 AI

现在,您已经构建了一个明确定义了目标并有相关奖励系统的环境,是时候构建 AI 了。我希望您已经准备好迎接一点数学挑战。

我将把第二步分解成几个子步骤,引导你完成最终的 Q 学习模型。为此,我们将按以下顺序讲解 Q 学习核心的三个重要概念:

  1. Q 值

  2. 时间差

  3. 贝尔曼方程

让我们开始了解 Q 值。

Q 值

在你开始深入 Q 学习的细节之前,我需要解释 Q 值的概念。它是这样运作的:

对于每一对状态和动作(sa),我们将关联一个数值Qsa):

我们将说Qsa)是“在状态s下执行动作a的 Q 值。”

现在我知道你脑海中可能会问这样的问题:这个 Q 值是什么意思?它代表什么?我到底该怎么计算它?这些都是我第一次学习 Q 学习时心里想的问题。

为了回答这些问题,我需要引入时间差。

时间差(Temporal difference)

这就是数学真正发挥作用的地方。假设我们处于一个特定的状态!,在特定的时间t。我们随机执行一个动作,任何一个动作都可以。这样我们就进入了下一个状态!,并获得了奖励!

时间差在时间t时刻,记作!,是以下两者的差:

  1. ,即执行动作!在状态!下获得的奖励!,加上在未来状态!下执行的最佳动作的 Q 值,按一个因子!折扣,这个因子被称为折扣因子。

  2. 和!,即在状态!下执行动作!的 Q 值。

这导致了:

你可能会觉得太棒了,理解了所有术语,但你可能也在想:“那到底是什么意思?”别担心——这正是我在学习这个过程时的想法。

我将一边解释,一边提升你的 AI 直觉。首先要理解的是,时间差(temporal difference)表示 AI 学习的好坏。它是如何运作的,关于训练过程(在这个过程中 Q 值被学习)如下:

  1. 在训练的开始,Q 值被设置为零。由于 AI 的目标是获取好的奖励(这里是 1 或 1000),它在寻找高的时间差(参见 TD 公式)。因此,在前几次迭代中,如果!很高,AI 会得到一个“愉快的惊讶”,因为这意味着 AI 能够找到一个好的奖励。另一方面,如果!很小,AI 就会感到“沮丧”。

  2. 当 AI 获得了很大的奖励时,导致这个巨大奖励的(状态,动作)的特定 Q 值会增加,这样 AI 就可以记住如何达到那个高奖励(你将在下一节中看到确切的增加方式)。例如,假设是在状态 中执行的动作 导致了那个高奖励 。这意味着 Q 值 会自动增加(请记住,你将在下一节中看到具体如何增加)。这些增加的 Q 值是重要信息,因为它们向 AI 指示了哪些过渡通向了好的奖励。

  3. AI 的下一步不仅是寻找很大的奖励,同时也要同时寻找高 Q 值。为什么?因为高 Q 值才是导致大奖励的那些。事实上,高 Q 值会导致更高的 Q 值,它们自己又会导致更高的 Q 值,最终导致最高奖励(1000)。这就是时差公式中 的作用。当你将这一切付诸实践时,一切都会变得清晰明了。AI 寻找高 Q 值,一旦找到,又会增加(状态,动作)的 Q 值,因为它们指示了通向目标的正确路径。

  4. 在某些时候,AI 将知道所有导致好的奖励和高 Q 值的过渡。由于这些过渡的 Q 值随时间已经增加,最终时差会减小。事实上,我们越接近最终目标,时差就变得越小。

总之,时差就像一个临时的内在奖励,AI 会在训练开始时试图找到这些大值。最终,随着训练接近尾声——即接近最终目标时,AI 将最小化这个奖励。

这正是你必须记住的时差的直觉,因为它确实会帮助你理解 Q 学习的魔力。说到那种魔力,我们即将揭示这个难题的最后一部分。

现在你明白了,AI 将迭代一些 Q 值向高时差的更新,这些时差最终会减小。但它是如何做到的呢?这个问题有一个具体的答案——贝尔曼方程,强化学习中最著名的方程。

贝尔曼方程

为了执行更好、更有效的动作,引导 AI 达到目标,当你发现时间差较大时,必须增加动作的 Q 值。剩下的唯一问题是:AI 如何更新这些 Q 值?强化学习的先驱理查德·贝尔曼为此提供了答案。在每次迭代中,你通过以下方程更新从 t-1(上一次迭代)到 t(当前迭代)的 Q 值,这就是著名的 Bellman 方程:

其中 是学习率,决定了 Q 值学习的速度。它的值通常介于 0 和 1 之间,比如 0.75。 的值越低,Q 值的更新就越小,Q-learning 所需的时间就越长。它的值越高,Q 值的更新就越大,Q-learning 的速度就越快。正如你在这个方程中清楚地看到的那样,当时间差 较大时,Q 值 会增加。

强化学习直觉

现在你已经掌握了所有 Q-learning 的元素——顺便说一句,祝贺你——让我们将这些元素连接起来,强化你对 AI 的直觉。

Q 值衡量的是与一对动作和状态相关的“好惊讶”或“挫败感”的积累

在时间差较大的“好惊讶”情况下,AI 会得到强化;而在时间差较小的“挫败感”情况下,AI 会变得较弱。

我们希望学习出能给 AI 带来最大“好惊讶”的 Q 值,而这正是 Bellman 方程通过在每次迭代中更新 Q 值所做的事情。

你已经学到了不少新信息,尽管你已经完成了一个将所有知识点连接起来的直觉部分,但这还不足以真正掌握 Q-learning。下一步是后退一步,最好的方法是从头到尾过一遍完整的 Q-learning 过程,这样它在你脑中就会变得清晰透彻。

完整的 Q-learning 过程

让我们总结一下整个 Q-learning 过程的不同步骤。为了明确,整个过程的唯一目的是在一定数量的迭代中更新 Q 值,直到 Q 值不再更新(我们称这一点为收敛)。

迭代次数取决于问题的复杂性。对于我们的问题,1,000 次迭代就足够了,但对于更复杂的问题,你可能需要考虑更高的迭代次数,比如 10,000 次。简而言之,Q-learning 过程就是我们训练 AI 的部分,之所以叫 Q-learning,是因为这是一个学习 Q 值的过程。接下来,我会解释推理部分(纯预测)发生了什么,这部分总是在训练后进行。完整的 Q-learning 过程从训练模式开始。

训练模式

初始化(第一次迭代)

对于所有状态 s 和动作 a 的组合,Q 值被初始化为 0。

下一次迭代

在每次迭代 t ≥ 1 时,你会重复以下步骤若干次(由你作为开发者选择):

  1. 你从可用的状态中选择一个随机状态

  2. 从该状态出发,你执行一个随机动作 ,该动作可以导致下一个可能的状态,即使得

  3. 你到达下一个状态 ,并获得奖励

  4. 你计算时间差异 :

  5. 你通过应用贝尔曼方程更新 Q 值:

在这个过程结束时,你将获得不再更新的 Q 值。这意味着只有一个结果:你准备好通过进入推理模式来破解迷宫了。

推理模式

训练完成,现在开始推理。提醒一下,推理部分是当你有了一个完全训练好的模型,准备用来进行预测时。在我们的迷宫中,你将要做出的预测是执行哪些动作,以便将你从起点(位置 E)带到终点(位置 G)。所以,问题是:

你如何使用学习到的 Q 值来执行动作?

好消息;对于 Q-learning 这是非常简单的。当处于某一状态 时,你只需执行在该状态下具有最高 Q 值的动作

就是这样——通过在每个位置(每个状态)执行此操作,你将通过最短路径到达最终目的地。我们将在实际活动或下一章中实现这个过程并查看结果。

总结

本章我们研究了 Q-learning 模型,该模型仅应用于具有有限数量输入状态和有限数量可执行动作的环境。

在执行 Q-learning 时,AI 通过迭代过程学习 Q 值,从而使得 (状态,动作) 对的 Q 值越高,AI 越接近最高奖励。

在每次迭代中,Q 值通过贝尔曼方程进行更新,该方程仅仅是将时间差异与学习率因子折扣相加。我们将在下一章中进行完整的实际 Q-learning 活动,将其应用于一个真实的商业问题。

第八章:物流中的人工智能——仓库中的机器人

现在是我们人工智能之旅的下一步了。书的开头我曾告诉你,人工智能在运输和物流方面具有巨大的价值,特别是自动驾驶配送车辆,它们能加速物流流程。这些技术通过电子商务产业为经济带来了巨大的推动。

在这一章中,我们将为这种应用构建一个人工智能。我们将使用的模型当然是 Q 学习(我们将深度 Q 学习留给自动驾驶汽车)。Q 学习是一个简单但强大的人工智能模型,可以优化仓库中的移动流,这是你将在这里解决的现实问题。为了便于这一旅程,你将使用一个你已经熟悉的环境:我们在上一章看到的迷宫。

不同之处在于,这次迷宫实际上是某个企业的仓库。它可以是任何一种企业:电子商务企业、零售企业,或者任何一个销售产品给顾客并拥有仓库来存储大量产品的企业。

再看看这个迷宫,现在它变成了一个仓库:

图 1:仓库

在这个仓库里,产品存放在 12 个不同的位置,用 AL 的字母标记:

图 2:仓库中的位置

当客户下订单时,机器人会在仓库内移动,收集待配送的产品。那就是你的人工智能!它长这样:

图 3:仓库机器人

这 12 个位置都连接到计算机系统,该系统实时对这些 12 个位置的产品收集优先级进行排序。举个例子,假设在某个特定时间 t,它返回如下排序:

图 4:最高优先级的位置

位置 G 排在第一位,这意味着它是最高优先级的,因为它包含一个必须立即收集并配送的产品。我们的机器人必须根据当前位置通过最短的路线到达位置 G。我们的目标实际上是构建一个人工智能,它能返回这条最短路线,无论机器人在哪里。

但是我们可以做得更好。这里,位置 KL 排在前三位。因此,为我们的机器人实现一个选项,允许它通过一些中介位置再到达最终的最高优先级位置,将是非常有意义的。

系统如何计算位置的优先级超出了本案例研究的范围。原因是,计算这些优先级的方式有很多种,从简单的规则或算法,到确定性计算,再到机器学习。但其中大多数方式都不是我们今天所说的 AI。我们在这次练习中真正想要关注的是核心 AI,包括强化学习和 Q 学习。为了本例的目的,我们可以简单地说,位置G是最高优先级的,因为公司的一个最忠实的铂金级客户下了一个紧急订单,产品存放在位置G,因此必须尽快交付。

总结来说,我们的使命是构建一个 AI,它始终会选择从任意起点出发,走最短的路线到达优先级最高的位置,并且可以选择经过一个位于前三个优先级中的中介位置。

构建环境

在构建 AI 时,我们首先需要做的是定义环境。定义环境总是需要以下三个元素:

  • 定义状态

  • 定义动作

  • 定义奖励

这三个元素在前一章关于 Q 学习的内容中已经定义过了,但我们来快速回顾一下它们是什么。

状态

在特定时刻t,状态是机器人在该时刻t所在的位置。不过,请记住,你需要对位置名称进行编码,这样我们的 AI 才能进行计算。

为了避免让你失望,尽管有关于 AI 的疯狂炒作,我们还是要保持现实,理解 Q 学习不过是一堆数学公式;就像其他任何 AI 模型一样。我们让编码的整数从 0 开始,仅仅是因为 Python 中的索引从 0 开始:

图 5:位置到状态的映射

动作

动作是机器人可以前往的下一个可能目的地。你可以用与状态相同的索引对这些目的地进行编码。因此,AI 可以执行的动作总列表如下:

actions = [0,1,2,3,4,5,6,7,8,9,10,11] 

奖励

请记住,在特定位置时,机器人无法执行某些动作。例如,如果机器人处于位置J,它可以执行动作 5、8 和 10,但无法执行其他动作。你可以通过为无法执行的动作赋予奖励 0,为可以执行的动作赋予奖励 1 来指定这一点。

这将引导你构建以下奖励矩阵:

图 6:奖励矩阵

AI 解决方案回顾

在实施模型之前,回顾一下模型总是有益的!让我们回顾一下 Q 学习过程的步骤;这一次,我们将其调整到你的新问题上。让我们欢迎 Q 学习重新登场:

初始化(第一次迭代)

对于所有状态s和动作a的组合,Q 值初始化为 0:

下一步迭代

在每次迭代中,t ≥ 1,AI 将重复以下步骤:

  1. 它从可能的状态中选择一个随机状态!

  2. 它执行一个随机动作 ,这个动作可能导致下一个可能的状态,即:

  3. 它到达下一个状态 ,并获得奖励

  4. 它计算时间差异!

  5. 它通过应用贝尔曼方程来更新 Q 值:

我们将在 1,000 次迭代中重复这些步骤。为什么是 1,000?选择 1,000 是因为我在这个特定环境中的实验结果。我选择了一个足够大的数字,使得 Q 值能够在训练过程中收敛。100 次迭代不够大,但 1,000 次足够了。通常,你可以选择一个非常大的数字,例如 5,000,这样你就会看到收敛(即 Q 值不再更新)。不过,这取决于问题的复杂性。如果你正在处理一个更复杂的环境,例如仓库中有数百个位置,你可能需要更多的训练迭代次数。

这就是整个过程。现在,你将从零开始在 Python 中实现它!

你准备好了吗?我们开始吧。

实现

好的,让我们来挑战这个。不过首先,试着自己先搞定它,别等我来。虽然这是我们一起走的旅程,但如果你能提前走几步,我也不介意。你在 AI 上变得独立的速度越快,你就越能发挥它的奇迹。试着按照之前提到的 Q-learning 过程来实现,完全按照原样来。即使你没有实现所有内容也没关系,重要的是你要尝试。

这已经是足够的指导了;不管你多么成功,还是让我们一起看看解决方案吧。

首先,开始导入你将在这个实现中使用的库。这次只需要一个库:numpy 库,它提供了一种方便的方式来处理数组和数学运算。给它起个快捷名称 np

# AI for Logistics - Robots in a warehouse
# Importing the libraries
import numpy as np 

然后,设置模型的参数。这些包括折扣因子 γ 和学习率!,它们是 Q-learning 模型的唯一参数。分别赋值为 0.750.9,这两个值是我随便选的,但通常是一个不错的选择。如果你不知道该使用什么,这些值是一个不错的起点。不过,你也可以使用类似的值,结果会相同。

# Setting the parameters gamma and alpha for the Q-Learning
gamma = 0.75
alpha = 0.9 

前两个代码部分只是引导部分,在你真正开始构建 AI 模型之前。下一步是开始我们实现的第一部分。

现在试着记住你需要做的事情,这是构建 AI 的第一个通用步骤。

你需要构建环境!

我只是想再次强调一下,这真的很重要。环境将是你代码的第一部分:

第一部分 – 构建环境

让我们来看看这个实现的整体结构,这样你可以稍微退后一步。你的代码将分为三部分:

  • 第一部分 – 构建环境

  • 第二部分 – 使用 Q 学习构建 AI 解决方案(训练)

  • 第三部分 – 进入生产(推理)

让我们从第一部分开始。首先,定义状态、动作和奖励。首先定义状态,使用一个 Python 字典将位置名称(从 A 到 L 的字母)映射到状态(从 0 到 11 的索引)。将这个字典命名为location_to_state

# PART 1 - BUILDING THE ENVIRONMENT
# Defining the states
location_to_state = {'A': 0,
                     'B': 1,
                     'C': 2,
                     'D': 3,
                     'E': 4,
                     'F': 5,
                     'G': 6,
                     'H': 7,
                     'I': 8,
                     'J': 9,
                     'K': 10,
                     'L': 11} 

然后,用一个简单的从 0 到 11 的索引列表定义动作。记住,每个动作索引对应于该动作引导到的下一个位置:

# Defining the actions
actions = [0,1,2,3,4,5,6,7,8,9,10,11] 

最后,定义奖励,通过创建一个奖励矩阵,其中行对应当前状态 ,列对应导致下一个状态的动作 ,单元格包含奖励 。如果一个单元格包含 1,意味着 AI 可以从当前状态 执行动作 ,到达下一个状态 。如果一个单元格 包含 0,意味着 AI 无法从当前状态 执行动作 到达任何下一个状态

现在,你可能记得这个非常重要的问题,它的答案是强化学习的核心。

你将如何让 AI 知道它必须去那个优先级最高的位置 G

一切都与奖励相关。

我必须再次强调,记住这一点。如果你给 G 位置分配一个高奖励,那么 AI 通过 Q 学习过程将学会以最有效的方式获取这个高奖励,因为它比到达其他位置的奖励要大。

记住这个非常重要的规则:当 AI 使用 Q 学习(或者你很快会学到的深度 Q 学习)时,它总是会学习通过最快的路径达到最高奖励,同时避免因负奖励而惩罚 AI。这就是为什么达到 G 位置的诀窍仅仅是给它赋予比其他位置更高的奖励。

首先手动放入一个高奖励,任何大于 1 的数字都可以,只要它大于 1,放在对应 G 位置的单元格中;G 位置是机器人必须去的优先级最高的位置,以便收集产品。

由于 G 位置的编码索引状态为 6,因而在第 6 行第 6 列的单元格中放入一个 1000 的奖励。稍后,我们将通过实现一种自动去往优先级最高位置的方法来改进你的解决方案,无需手动更新奖励矩阵,并且保持初始化为 0 和 1,这正是它应有的样子。目前,这是你的奖励矩阵,包括手动更新部分。

# Defining the rewards
R = np.array([[0,1,0,0,0,0,0,0,0,0,0,0],
              [1,0,1,0,0,1,0,0,0,0,0,0],
              [0,1,0,0,0,0,1,0,0,0,0,0],
              [0,0,0,0,0,0,0,1,0,0,0,0],
              [0,0,0,0,0,0,0,0,1,0,0,0],
              [0,1,0,0,0,0,0,0,0,1,0,0],
              [0,0,1,0,0,0,1000,1,0,0,0,0],
              [0,0,0,1,0,0,1,0,0,0,0,1],
              [0,0,0,0,1,0,0,0,0,1,0,0],
              [0,0,0,0,0,1,0,0,1,0,1,0],
              [0,0,0,0,0,0,0,0,0,1,0,1],
              [0,0,0,0,0,0,0,1,0,0,1,0]]) 

这完成了第一部分。现在,让我们开始第二部分的实现。

第二部分 – 使用 Q 学习构建 AI 解决方案

为了构建你的 AI 解决方案,按照之前提供的 Q 学习算法逐步进行。如果你在自己实现 Q 学习时遇到了困难,现在是复仇的时刻了。接下来的一切都完全是之前的 Q 学习过程翻译成代码。

现在你已经在脑海中理解了,试着再次独立编写代码吧。你能做到的!

恭喜你尝试了,不管结果如何。接下来,让我们检查一下你是否做对了。

首先,通过创建一个全是 0 的 Q 值矩阵来初始化所有的 Q 值,其中行对应当前状态 ,列对应到达下一个状态的动作 ,单元格包含 Q 值

# PART 2 - BUILDING THE AI SOLUTION WITH Q-LEARNING

# Initializing the Q-values
Q = np.array(np.zeros([12,12])) 

然后,用一个循环执行 Q 学习过程,进行 1,000 次迭代,重复 Q 学习过程中的每一个步骤 1,000 次。

# Implementing the Q-Learning process
for i in range(1000):
    current_state = np.random.randint(0,12)
    playable_actions = []
    for j in range(12):
        if R[current_state, j] > 0:
            playable_actions.append(j)
    next_state = np.random.choice(playable_actions)
    TD = R[current_state, next_state] + gamma * Q[next_state, np.argmax(Q[next_state,])] - Q[current_state, next_state]
    Q[current_state, next_state] = Q[current_state, next_state] + alpha * TD 

现在你已经到达了旅程中的第一个真正令人兴奋的步骤。你实际上已经准备好启动 Q 学习过程并获得最终的 Q 值。执行你到目前为止实现的全部代码,并通过以下简单的打印语句来可视化 Q 值:

print("Q-values:")
print(Q.astype(int)) 

这是我得到的结果:

Q-values:
[[   0 1661    0    0    0    0    0    0    0    0    0    0]
 [1246    0 2213    0    0 1246    0    0    0    0    0    0]
 [   0 1661    0    0    0    0 2970    0    0    0    0    0]
 [   0    0    0    0    0    0    0 2225    0    0    0    0]
 [   0    0    0    0    0    0    0    0  703    0    0    0]
 [   0 1661    0    0    0    0    0    0    0  931    0    0]
 [   0    0 2213    0    0    0 3968 2225    0    0    0    0]
 [   0    0    0 1661    0    0 2968    0    0    0    0 1670]
 [   0    0    0    0  528    0    0    0    0  936    0    0]
 [   0    0    0    0    0 1246    0    0  703    0 1246    0]
 [   0    0    0    0    0    0    0    0    0  936    0 1661]
 [   0    0    0    0    0    0    0 2225    0    0 1246    0]] 

如果你在 Anaconda 的 Spyder 中工作,那么为了更清晰地看到,你甚至可以直接在变量浏览器中查看 Q 值矩阵,只需双击 Q。然后,要将 Q 值显示为整数,可以点击格式并输入浮动格式%.0f。这样你就会得到如下结果,这样更清晰,因为你可以看到 Q 矩阵中行和列的索引:

图 7:Q 值矩阵

现在你已经有了 Q 值矩阵,你可以进入生产阶段了——可以继续进行实现的第三部分。

第三部分 – 进入生产阶段

换句话说,你现在进入了推理模式!在这一部分,你将计算从任何起始位置到任何终极目标位置的最佳路径。这里的思路是实现一个route函数,它接受起始位置和终止位置作为输入,并返回一个包含最短路径的 Python 列表。起始位置对应我们自主仓库机器人在某一时刻的位置,终止位置对应机器人必须去的优先位置。

由于你需要输入的是位置的名称(用字母表示),而不是位置的状态(用索引表示),你需要一个字典,将位置的状态(用索引表示)映射到位置的名称(用字母表示)。这是第三部分的第一个任务,使用一个技巧来反转你之前的字典location_to_state,因为你只需要从这个字典中获取精确的反向映射:

# PART 3 - GOING INTO PRODUCTION

# Making a mapping from the states to the locations
state_to_location = {state: location for location, state in location_to_state.items()} 

现在,请集中注意力——如果这些点在你的脑海中还没有完全连接,现在是它们连接的时候了。我将向你展示机器人是如何计算出最短路径的准确步骤。

你的机器人将从位置 E 移动到位置 G。以下是它是如何做到的解释——我将列出过程的不同步骤。请在我讲解时关注 Q 值矩阵:

  1. AI 从起始位置 E 开始。

  2. AI 获得位置 E 的状态,根据你的 location_to_state 映射,这是

  3. 在我们的 Q 值矩阵的索引行 中,AI 选择具有最大 Q 值(703)的列。

  4. 该列的索引为 8,因此 AI 执行索引 8 的操作,这将引导它进入下一个状态

  5. AI 获取状态 8 的位置,根据我们的 state_to_location 映射,这是位置 I。由于下一个位置是位置 II 被添加到 AI 的包含最佳路径的列表中。

  6. 然后,从新位置 I 开始,AI 重复之前的五个步骤,直到到达最终目的地位置 G

就是这样!这正是你需要实现的内容。你需要将其通用化到任何起始和结束位置,最好的方法是通过一个接受两个输入的函数来实现:

  1. starting_location: AI 启动的起始位置

  2. ending_location: 它必须到达的最高优先级位置

并返回最佳路径。因为我们在谈论路径,你可以调用该函数 route()

route() 函数中,理解的一个重要点是,由于你不知道 AI 在起始位置和结束位置之间需要经过多少个位置,你必须创建一个 while 循环,重复之前描述的 5 步骤,并在到达最高优先级的结束位置时停止。

# Making the final function that will return the optimal route
def route(starting_location, ending_location):
    route = [starting_location]
    next_location = starting_location
    while (next_location != ending_location):
        starting_state = location_to_state[starting_location]
        next_state = np.argmax(Q[starting_state,])
        next_location = state_to_location[next_state]
        route.append(next_location)
        starting_location = next_location
    return route 

恭喜!你的 AI 现在准备好了。它不仅实现了训练过程,还有推理模式下运行的代码。到目前为止,唯一不太完美的是你仍然需要手动更新奖励矩阵;但别担心,我们稍后会解决这个问题。在我们解决这个问题之前,先检查一下你是否已经在这里获得了阶段性的胜利,然后我们可以继续进行改进。

# Printing the final route
print('Route:')
route('E', 'G') 

以下是输出:

Route:
Out[1]: ['E', 'I', 'J', 'F', 'B', 'C', 'G']
Out[2]: ['E', 'I', 'J', 'K', 'L', 'H', 'G'] 

完美——在测试时,我运行了两次代码从 E 到 G,这就是为什么你会看到前两个输出的原因。返回了两条可能的最佳路径:一条经过 F,另一条经过 K。

这是一个良好的开端。你有了第一版运作良好的 AI 模型。现在,让我们改进你的 AI,将它提升到更高的水平。

你可以通过两种方式改进人工智能。首先,通过自动化奖励分配到最高优先级的位置,这样你就不必手动进行操作。其次,添加一个功能,让人工智能在前往最高优先级位置之前可以选择经过一个中间位置——该中间位置应当位于前三个优先级位置之内。

在我们的最高优先级位置排名中,第二高优先级的位置是位置 K。因此,为了优化仓库流动,你的自动仓库机器人必须经过位置 K,收集产品,然后再前往最高优先级位置 G。实现这一目标的一种方法是,在route()函数的过程中选择经过一个中间位置。这正是你将作为第二个改进来实现的内容。

首先,让我们实现第一个改进,即自动化奖励分配。

改进 1——自动化奖励分配

这样做的方法分为三个步骤。

第 1 步:返回到最初的奖励矩阵,像之前那样只包含 1 和 0。代码的第一部分变成如下,并将被包含在最终代码中:

# PART 1 - BUILDING THE ENVIRONMENT

# Defining the states
location_to_state = {'A': 0,
                     'B': 1,
                     'C': 2,
                     'D': 3,
                     'E': 4,
                     'F': 5,
                     'G': 6,
                     'H': 7,
                     'I': 8,
                     'J': 9,
                     'K': 10,
                     'L': 11}

# Defining the actions
actions = [0,1,2,3,4,5,6,7,8,9,10,11]

# Defining the rewards
R = np.array([[0,1,0,0,0,0,0,0,0,0,0,0],
              [1,0,1,0,0,1,0,0,0,0,0,0],
              [0,1,0,0,0,0,1,0,0,0,0,0],
              [0,0,0,0,0,0,0,1,0,0,0,0],
              [0,0,0,0,0,0,0,0,1,0,0,0],
              [0,1,0,0,0,0,0,0,0,1,0,0],
              [0,0,1,0,0,0,1,1,0,0,0,0],
              [0,0,0,1,0,0,1,0,0,0,0,1],
              [0,0,0,0,1,0,0,0,0,1,0,0],
              [0,0,0,0,0,1,0,0,1,0,1,0],
              [0,0,0,0,0,0,0,0,0,1,0,1],
              [0,0,0,0,0,0,0,1,0,0,1,0]]) 

第 2 步:在代码的第二部分,复制一份你的奖励矩阵(称之为R_new),在其中route()函数可以自动更新结束位置单元格的奖励。

为什么需要复制?因为你必须保持原始奖励矩阵初始化为 1 和 0,以便将来在你需要前往新的优先级位置时进行修改。那么,route()函数如何自动更新结束位置单元格的奖励呢?这个很简单:由于结束位置是route()函数的输入之一,因此通过使用你的location_to_state字典,你可以非常容易地找到该单元格并将其奖励更新为1000。下面是如何实现的:

# Making a function that returns the shortest route from a starting to ending location
def route(starting_location, ending_location):
    R_new = np.copy(R)
    ending_state = location_to_state[ending_location]
    R_new[ending_state, ending_state] = 1000 

第 3 步:你必须将整个 Q 学习算法(包括初始化步骤)包含在route()函数中,在我们更新奖励矩阵复制(R_new)中的奖励后执行。在你之前的实现中,Q 学习过程是在原始奖励矩阵上进行的。现在,原始版本需要保持原样,即仅初始化为 1 和 0。因此,你必须将 Q 学习过程包含在route()函数中,并使其在奖励矩阵的复制R_new上执行,而不是原始奖励矩阵R。下面是如何实现的:

# Making a function that returns the shortest route from a starting to ending location
def route(starting_location, ending_location):
    R_new = np.copy(R)
    ending_state = location_to_state[ending_location]
    R_new[ending_state, ending_state] = 1000
    Q = np.array(np.zeros([12,12]))
    for i in range(1000):
        current_state = np.random.randint(0,12)
        playable_actions = []
        for j in range(12):
            if R_new[current_state, j] > 0:
                playable_actions.append(j)
        next_state = np.random.choice(playable_actions)
        TD = R_new[current_state, next_state] + gamma * Q[next_state, np.argmax(Q[next_state,])] - Q[current_state, next_state]
        Q[current_state, next_state] = Q[current_state, next_state] + alpha * TD
    route = [starting_location]
    next_location = starting_location
    while (next_location != ending_location):
        starting_state = location_to_state[starting_location]
        next_state = np.argmax(Q[starting_state,])
        next_location = state_to_location[next_state]
        route.append(next_location)
        starting_location = next_location
    return route 

完美;第二部分已经准备好!以下是第二部分的最终完整代码:

# PART 2 - BUILDING THE AI SOLUTION WITH Q-LEARNING

# Making a mapping from the states to the locations
state_to_location = {state: location for location, state in location_to_state.items()}

# Making a function that returns the shortest route from a starting to ending location
def route(starting_location, ending_location):
    R_new = np.copy(R)
    ending_state = location_to_state[ending_location]
    R_new[ending_state, ending_state] = 1000
    Q = np.array(np.zeros([12,12]))
    for i in range(1000):
        current_state = np.random.randint(0,12)
        playable_actions = []
        for j in range(12):
            if R_new[current_state, j] > 0:
                playable_actions.append(j)
        next_state = np.random.choice(playable_actions)
        TD = R_new[current_state, next_state] + gamma * Q[next_state, np.argmax(Q[next_state,])] - Q[current_state, next_state]
        Q[current_state, next_state] = Q[current_state, next_state] + alpha * TD
    route = [starting_location]
    next_location = starting_location
    while (next_location != ending_location):
        starting_state = location_to_state[starting_location]
        next_state = np.argmax(Q[starting_state,])
        next_location = state_to_location[next_state]
        route.append(next_location)
        starting_location = next_location
    return route 

如果你多次执行这个新代码,起点和终点分别为EG,你将得到与之前相同的两个可能的最优路径。你还可以尝试route()函数,尝试不同的起点和终点。试试看吧!

改进 2——添加中间目标

现在,让我们来解决第二个改进问题。添加一个通过中间位置K的选项问题有三种可能的解决方案。当你看到它们时,你会明白我之前说过的“强化学习的一切都是通过奖励来工作的”是什么意思。

只有一个解决方案适用于每一个起始点,但我想给你三个解决方案,以帮助你加深直觉。为了帮助你理解,这里是我们的仓库布局的提醒:

图 8:仓库中的位置

解决方案 1:给从位置J到位置K的动作一个高奖励。这个高奖励必须大于 1,且小于 1,000。它必须大于 1,这样 Q 学习过程才会偏向于从JK的动作,而不是从JF的动作,因为后者的奖励为 1。它还必须小于 1,000,这样最高的奖励才能保持在最高优先级的位置,确保 AI 最终到达那里。例如,在你的奖励矩阵中,你可以给第 9 行第 10 列的单元格一个高奖励500,因为该单元格对应的是从位置J(状态索引 9)到位置K(状态索引 10)的动作。这样,你的 AI 机器人在从位置E到位置G时,就会始终经过位置K。在这种情况下,奖励矩阵的样子应该是:

# Defining the rewards
R = np.array([[0,1,0,0,0,0,0,0,0,0,0,0],
              [1,0,1,0,0,1,0,0,0,0,0,0],
              [0,1,0,0,0,0,1,0,0,0,0,0],
              [0,0,0,0,0,0,0,1,0,0,0,0],
              [0,0,0,0,0,0,0,0,1,0,0,0],
              [0,1,0,0,0,0,0,0,0,1,0,0],
              [0,0,1,0,0,0,1,1,0,0,0,0],
              [0,0,0,1,0,0,1,0,0,0,0,1],
              [0,0,0,0,1,0,0,0,0,1,0,0],
              [0,0,0,0,0,1,0,0,1,0,500,0],
              [0,0,0,0,0,0,0,0,0,1,0,1],
              [0,0,0,0,0,0,0,1,0,0,1,0]]) 

这个解决方案并不适用于所有情况,实际上它只适用于起始点EIJ。这是因为500的权重只能影响 AI 是否应该从JK的决策;它不会改变 AI 在最初选择是否去J的可能性。

解决方案 2:给从位置J到位置F的动作一个差奖励。这个差奖励必须小于 0。通过惩罚这个动作,Q 学习过程就不会偏向于从JF的动作。例如,在你的奖励矩阵中,你可以给第 9 行第 5 列的单元格一个差奖励-500,因为该单元格对应的是从位置J(状态索引 9)到位置F(状态索引 5)的动作。这样,你的自动化仓库机器人在前往位置G时,就永远不会从位置J到位置F。在这种情况下,奖励矩阵的样子应该是:

# Defining the rewards
R = np.array([[0,1,0,0,0,0,0,0,0,0,0,0],
              [1,0,1,0,0,1,0,0,0,0,0,0],
              [0,1,0,0,0,0,1,0,0,0,0,0],
              [0,0,0,0,0,0,0,1,0,0,0,0],
              [0,0,0,0,0,0,0,0,1,0,0,0],
              [0,1,0,0,0,0,0,0,0,1,0,0],
              [0,0,1,0,0,0,1,1,0,0,0,0],
              [0,0,0,1,0,0,1,0,0,0,0,1],
              [0,0,0,0,1,0,0,0,0,1,0,0],
              [0,0,0,0,0,-500,0,0,1,0,1,0],
              [0,0,0,0,0,0,0,0,0,1,0,1],
              [0,0,0,0,0,0,0,1,0,0,1,0]]) 

这个解决方案并不适用于所有情况,实际上它只适用于起始点EIJ。和解决方案 1 一样,这是因为-500的权重只能影响 AI 是否应该从JF的决策;它不会改变 AI 最初选择是否去J的可能性。

方案 3:新增一个 best_route() 函数,接受三个输入:起始地点、中介地点和结束地点,该函数将调用你之前的 route() 函数两次;第一次从起始地点到中介地点,第二次从中介地点到结束地点。

前两个方案可以手动轻松实现,但自动实现起来则比较棘手。你可以轻松地自动获得中介地点的索引,但很难得到导致 AI 到达该中介地点的路径,因为它依赖于起点和终点。如果你尝试实现第一个或第二个方案,你就会明白我的意思。此外,方案 1 和方案 2 并不能作为全局解决方案。

只有方案 3 能保证 AI 在前往最终地点之前会经过一个中介地点。

因此,我们将实现方案 3,该方案只需额外增加两行代码,我已在 第三部分 – 进入生产阶段 中包含了该代码:

# PART 3 - GOING INTO PRODUCTION

# Making the final function that returns the optimal route
def best_route(starting_location, intermediary_location, ending_location):
    return route(starting_location, intermediary_location) + route(intermediary_location, ending_location)[1:]

# Printing the final route
print('Route:')
best_route('E', 'K', 'G') 

很简单,对吧?有时候,最佳的解决方案往往是最简单的。这里的情况就是如此。如你所见,第三部分包括了运行最终测试的代码。如果 AI 能在从地点 E 到地点 G 的最短路径上经过 K 地点,那么这个测试就会成功。你可以多次执行这段全新的代码,你总会得到相同的预期输出:

Route:
['E', 'I', 'J', 'K', 'L', 'H', 'G'] 

恭喜你!你已经开发出了一个完全功能的 AI,采用 Q 学习算法,能够解决物流优化问题。通过这个 AI 机器人,我们现在可以从任何地点出发,前往任何新的最高优先级地点,同时优化路径以便在第二优先级的中介位置收集产品。做得不错!如果你对物流有些厌倦,可以随时想象自己回到迷宫中,并使用 best_route() 函数,随意设置起点和终点,看看你创建的 AI 有多么灵活。玩得开心!当然,你也可以在 GitHub 页面上获取完整的代码。

总结

在本章中,你实现了一个 Q 学习解决方案,用于解决业务问题。你需要找到通往仓库某个位置的最佳路线。不仅如此,你还实现了额外的代码,允许你的 AI 按照需要进行多个中介停靠点。基于获得的奖励,AI 能够找到经过这些停靠点的最佳路线。这就是用于仓库机器人的 Q 学习方法。接下来,让我们进入深度 Q 学习!

第九章:使用人工智能大脑——深度 Q 学习

下一个 AI 模型非常棒,因为它是第一个真正受到人类智能启发的 AI 模型。我希望你已经准备好在 AI 之旅的下一步中大展拳脚;这本书不仅是 AI 的速成课程,也是深度学习的入门介绍。

今天,一些顶尖的 AI 模型已经融合了深度学习。它们形成了一个新的 AI 分支,叫做深度强化学习。本章将介绍的模型属于这个分支,称为深度 Q 学习。你已经知道 Q 学习的原理,但可能对深度学习和人工神经网络ANNs)一无所知;我们将从这些内容开始。当然,如果你是深度学习的专家,你可以跳过本章的前几节,但要考虑到,回顾一下基本知识也无妨。

在我们开始讲解理论之前,你将从写出实际可运行的 Python 代码开始。你首先会创建一些 AI,接下来我会帮助你理解它。现在,我们将构建一个人工神经网络来预测房价。

预测房价

我们想要做的是根据一些变量预测某个房子的价格。为了实现这个目标,你需要遵循以下四个步骤:

  1. 获取一些关于房屋销售的历史数据;在这个例子中,你将使用一份包含大约 20,000 套房子的西雅图数据集。

  2. 在你的代码中导入这些数据,同时对变量应用一些缩放(我会在过程中解释缩放)。

  3. 使用任何库构建人工神经网络——你将使用 Keras,因为它简单且可靠。

  4. 训练你的人工神经网络(ANN),并获得结果。

现在你已经了解了未来代码的结构,你可以开始编写代码了。由于你将使用的所有库都可以在 Google Colab 中找到,因此你可以轻松地使用它来完成这项任务。

上传数据集

从创建一个新的 Google Colab 笔记本开始。一旦创建了新笔记本,在你开始编码之前,你需要上传数据集。你可以在 GitHub 的第九章文件夹中找到这个数据集,名为kc_house_data.csv

图 1:GitHub – 第九章

一旦完成,你可以通过以下方式将其上传到 Colab:

  1. 点击这里的小箭头:

    图 2:Google Colab – 上传文件(1/3)

  2. 在弹出的窗口中,进入文件。你应该会看到类似这样的内容:

    图 3:Google Colab – 上传文件(2/3)

  3. 点击上传,然后选择你保存kc_house_data数据集的文件位置。

  4. 完成后,你应该会得到一个包含我们数据集的新文件夹,像这样:

    图 4:Google Colab – 上传文件(3/3)

太棒了!现在你可以开始编码了。

导入库

每次开始编码时,你应该先导入必要的库。因此,我们的代码从这些行开始:

# Importing the libraries   #3
import pandas as pd   #4
import numpy as np   #5
import keras   #6
from sklearn.model_selection import train_test_split   #7
from sklearn.preprocessing import MinMaxScaler   #8
from keras.layers import Dense, Dropout   #9
from keras.models import Sequential   #10
from keras.optimizers import Adam   #11 

在第 4 和第 5 行,在注释之后,你导入了 pandasnumpy 库。Pandas 将帮助你读取数据集,而 NumPy 在处理数组或列表时非常有用;你将用它来删除数据集中一些不必要的列。

在接下来的两行代码中,你从 Scikit-Learn 库中导入了两个有用的工具。第一个工具帮助你将数据集拆分成训练集和测试集(你应该始终有这两者;AI 模型在训练集上进行训练,然后在测试集上进行测试),第二个工具是一个缩放器,稍后你在缩放值时会用到它。

第 9、10 和 11 行负责导入 keras 库,你将在其中构建神经网络。每个工具稍后会在代码中使用。

现在你已经导入了库,可以开始读取数据集。只需使用之前导入的 Pandas 库,通过这一行代码来完成:

# Importing the dataset   #13
dataset = pd.read_csv('kc_house_data.csv')   #14 

由于你在导入 Pandas 库时使用了 pd 作为缩写,因此可以利用它来简化代码。在调用 Pandas 库时使用 pd,然后你可以使用它的函数之一 read_csv,顾名思义,它用于读取 csv 文件。接着在括号中输入文件名,你的文件名是 kc_house_data.csv,不需要其他参数。

现在我给你一个小练习!看一下数据集,尝试判断哪些变量对我们的价格预测有影响。相信我,并不是所有变量都有用。我强烈建议你先尝试自己做,尽管我们将在下一节讨论这些变量。

排除的变量

你能分辨出哪些变量是必要的,哪些不是吗?如果没有关系,我们现在就来解释它们及其相关性。

以下表格解释了我们数据集中每一列的含义:

变量 描述
Id 每个家庭的唯一 ID
Date 房屋售出的日期
Price 房屋售出时的价格
Bedrooms 卧室数量
Bathrooms 卫生间的数量;0.5 代表有马桶但没有淋浴的房间
Sqft_living 公寓内部生活空间的平方英尺数
Sqft_lot 土地空间的平方英尺数
Floors 楼层数
Waterfront 如果公寓没有面朝海滨,则为 0;如果有则为 1
View 视野质量的值,范围为 0-4,取决于房产视野的好坏
条件 定义属性条件的 1-5 的值
Grade 从 1 到 13 的值,表示建筑的设计和构造
Sqft_above 地面以上的室内空间的平方英尺数
Sqft_basement 地下室的平方英尺数
Yr_built 房屋建造的年份
Yr_renovated 房屋翻修的年份(如果没有翻修则为 0)
Zipcode 房屋所在区域的邮政编码
Lat 纬度
Long 经度
Sqft_living15 最近 15 个邻居住宅内部生活空间的平方英尺
Sqft_lot15 最近 15 个邻居土地面积的平方英尺

结果是,在这 21 个变量中,只有 18 个是有效的。因为像 Id、Date 和 Zipcode 这样的唯一类别型值对你的预测没有任何影响。Price 是你的预测目标,因此你也应该将其从变量中去除。完成这些之后,你将剩下 17 个独立变量。

现在我们已经解释了所有变量,并决定了哪些是相关的,哪些是不相关的,你可以回到你的代码了。你将排除这些不必要的变量,并将数据集拆分为特征和目标(在我们的例子中,目标是价格)。

# Getting separately the features and the targets   #16
X = dataset.iloc[:, 3:].values   #17
X = X[:, np.r_[0:13,14:18]]   #18
y = dataset.iloc[:, 2].values   #19 

在第 17 行,你从数据集中选择所有行和从第四列开始的所有列(因为你排除了 Id、Date 和 Price),并将这个新集称为X。你使用.iloc来切片数据集,然后使用.values将其转换为 NumPy 对象。这些将是你的特征。

接下来你需要排除 Zipcode,遗憾的是它正好位于特征集的中间。因此,你必须使用 NumPy 函数(np.r_)来分割X,排除你选择的列(在这个案例中是第 14 列。13 是这列的索引,因为 Python 的索引是从零开始的;值得一提的是,Python 中的上界是排除的,所以我们写0:13),然后再将它们合并成一个新的数组。在下一行,你获取你的预测目标并将其称为y。这对应于数据集中的第三列,即 Price。

数据准备

现在你已经分离了重要的特征和目标,你可以将Xy拆分成训练集和测试集。我们用以下这行代码来做到这一点:

# Splitting the dataset into a training set and a test set   #21
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = 0)   #22 

这是进行任何机器学习时非常重要的一步。你总是需要有一个训练集来训练你的模型,以及一个测试集来测试它。你通过之前导入的train_test_split函数来执行这个操作。执行后,你将得到X_train,它的大小与y_train相等,每个都正好是我们之前Xy集的 80%。X_testy_test则由剩下的 20%的Xy组成。

现在你已经有了训练集和测试集,你认为接下来的步骤是什么?嗯,你需要对数据进行缩放。

数据缩放

现在你可能会想,为什么你要进行这样的操作?你已经有了数据,为什么不直接构建并训练神经网络呢?

这里有个问题;如果我们保持数据不变,你会发现你的人工神经网络(ANN)无法学习。原因在于,不同的变量会根据它们的值对你的预测产生不同程度的影响。

看看这个图示,说明我想表达的意思,这个图基于一个有 3 个卧室和 1350 平方英尺生活空间的房产。

图 5:3 卧室和 1350 平方英尺的居住面积示例

你可以清楚地看到,卧室数量不会像Sqft_living那样显著影响预测。即使是我们人类,也看不出在这张图上零卧室和三卧室之间有什么区别。

解决这个问题的众多方法之一是将所有变量缩放到 0 和 1 之间。我们通过计算以下公式来实现这一点:

其中:

  • x – 在我们这个例子中是列中的每个值

  • x[min] – 列中所有值的最小值

  • x[max] – 列中所有值的最大值

  • x[scaled] – 执行缩放后的x

在进行缩放之后,我们之前的图表现在看起来是这样的:

图 6:缩放后相同的图表

现在我们可以毫无疑问地说,卧室数量将对房价产生类似的影响,和Sqft_living一样。我们可以清楚地看到零卧室和三卧室之间的差异。

那么,如何在代码中实现这一点呢?既然你知道公式了,我建议你自己尝试一下。如果失败了也不用担心,我会在下一段向你展示一种非常简单的实现方法。

如果你能够自己完成数据缩放,那么恭喜你!如果没有,继续阅读接下来的部分来查看答案。你可能已经注意到你导入了 Scikit-learn 库中的MinMaxScaler类。你可以使用这个类来缩放变量,代码如下:

# Scaling the features   #24
xscaler = MinMaxScaler(feature_range = (0,1))   #25
X_train = xscaler.fit_transform(X_train)   #26
X_test = xscaler.transform(X_test)   #27
#28
# Scaling the target   #29
yscaler = MinMaxScaler(feature_range = (0,1))   #30
y_train = yscaler.fit_transform(y_train.reshape(-1,1))   #31
y_test = yscaler.transform(y_test.reshape(-1,1))   #32 

这段代码创建了两个缩放器,一个用来缩放特征,另一个用来缩放目标。将它们分别称为xscaleryscalerfeature_range参数指定你希望数据缩放到的范围(在你的例子中是从 0 到 1)。

然后你使用fit_transform方法,它会对X_trainy_train进行缩放,并根据这两个数据集调整缩放器(该方法的fit部分会设置x[min]和x[max])。之后,你使用transform方法对X_testy_test进行缩放,而不调整yscalerxscaler

在缩放y变量时,必须使用.reshape(-1,1)将其重塑,以便创建一个假的第二维(这样代码就可以将这个一维数组当作具有一列的二维数组来处理)。我们需要这个假的第二维来避免格式错误。

如果你仍然不明白为什么我们必须使用缩放,请再次阅读这一部分内容。等到我们学习完理论部分后,你会更加明白。

最后,你可以开始构建神经网络了!请记住,所有的理论内容会在后面的章节中讲解,所以如果你遇到理解上的困难,别怕,我会帮助你理清思路。

构建神经网络

要构建神经网络,你可以使用一个非常可靠且易于使用的库——Keras。让我们直接开始编写代码:

# Building the Artificial Neural Network   #34
model = Sequential()   #35
model.add(Dense(units = 64, kernel_initializer = 'uniform', activation = 'relu', input_dim = 17))   #36
model.add(Dense(units = 16, kernel_initializer = 'uniform', activation = 'relu'))   #37
model.add(Dense(units = 1, kernel_initializer = 'uniform', activation = 'relu'))   #38
model.compile(optimizer = Adam(lr = 0.001), loss = 'mse', metrics = ['mean_absolute_error'])   #39 

在代码块的第 35 行,你通过使用 Keras 库中的Sequential类来实例化你的模型。

接下来,你添加一行代码,为你的神经网络添加一个包含 64 个神经元的新层。kernel_initializer是一个定义层中初始权重创建方式的参数,activation是该层的激活函数,input_dim是输入大小;在你的情况下,这些是定义房价的 17 个特征。

接下来,你添加两个新层,一个有 16 个神经元,另一个有 1 个神经元,作为神经网络的输出。

compile method, which describes how you want to train your net. Inside this compile method, optimizer is the tool that performs backpropagation, lr is the learning rate—the speed at which the weights in the ANN are updated. loss is how you want to calculate the error of the output (I have decided to go for the mean squared error mse), and metrics is just a value that will help you visualize performance—you can use mean absolute error.

如果你现在不知道我在讲什么,什么是激活函数、损失函数和优化器,你不用担心。你很快就会理解它们,当我们在后面的章节中讲解理论时。

训练神经网络

现在你已经建立了模型,终于可以开始训练它了!

# Training the Artificial Neural Network   #41
model.fit(X_train, y_train, batch_size = 32, epochs = 100, validation_data = (X_test, y_test))   #42 

这个简单的一行代码负责学习。

作为该fit方法的前两个参数,你输入了X_trainy_train,它们是你的模型将用于训练的集合。然后是一个名为batch_size的参数;它定义了在数据集中的多少条记录后,权重会被更新(损失会在batch_size输入后进行累加和反向传播)。接下来是epochs,这个值定义了你的模型将对整个X_trainy_train集合学习多少次。最后一个参数是validation_data,在这里,你可以看到,你放入了X_testy_test。这意味着在每次 epoch 后,你的模型都会在这个数据集上进行测试,但不会从中学习。

显示结果

你快完成了;只剩下一个非强制性的步骤了。你计算测试集上的绝对误差,并查看它的真实未缩放预测值(实际价格,而非(0,1)区间内的值)。

# Making predictions on the test set while reversing the scaling   #44
y_test = yscaler.inverse_transform(y_test)   #45
prediction = yscaler.inverse_transform(model.predict(X_test))   #46
#47
# Computing the error rate   #48
error = abs(prediction - y_test)/y_test   #49
print(np.mean(error))   #50 

在第 45 行,你将y_test重新缩放回去。然后,你对测试集的特征进行预测,并将其也重新缩放回来,因为预测值也已经被缩小。

在最后两行,你使用公式计算绝对误差:

由于预测值和y_test都是 NumPy 数组,你可以直接使用/符号进行除法运算。在最后一行,你使用 NumPy 函数计算平均误差。

太棒了!现在你已经完成所有工作,终于可以运行这段代码并查看结果了。

图 7:结果

如最后一行所示,结果已经显示出来。我的情况下,平均误差是 13.5%。这个结果非常好!

现在我们可以进入深度学习背后的理论,了解神经网络是如何工作的。

深度学习理论

这是我们的攻坚计划,准备转行成为深度学习高手:

  1. 神经元

  2. 激活函数

  3. 神经网络是如何工作的?

  4. 神经网络是如何学习的?

  5. 正向传播和反向传播

  6. 梯度下降,包括批量法、随机法和小批量法

希望你对这一部分感到兴奋——深度学习是一个很棒且强大的学习领域。

神经元

神经元是人工神经网络的基本构建模块,它们基于大脑中的神经元细胞。

生物神经元

以下图片是现实生活中的神经元,它们被涂抹到玻片上,稍微上了些颜色,并通过显微镜观察:

图 8:神经元

正如你所看到的,它们有一个中央主体,周围有许多不同的分支。问题是:我们如何将它在机器中重现呢?我们真的希望在机器中重现它,因为深度学习的整个目的就是模仿人脑的工作方式,希望通过这样做创造出一些惊人的东西:一个强大的学习机器基础设施。

为什么我们希望这样做?因为人脑恰好是地球上最强大的学习工具之一。我们希望,如果我们能够重现它,那么我们将拥有与之同样令人惊叹的东西。

我们目前面临的挑战,创建人工神经网络的第一步,是重现一个神经元。那么我们该如何做到这一点呢?首先,让我们仔细看看神经元到底是什么。

1899 年,神经学家圣地亚哥·拉蒙·卡哈尔将神经元染色后放在实际的脑组织上,并用显微镜观察它们。观察时,他画下了他所看到的内容,这和我们之前看到的玻片图非常相似。如今,技术已经取得了很大进步,使我们能够更近距离、更详细地观察神经元。这意味着我们可以将它们的外观以示意图的形式绘制出来:

图 9:神经元的结构

这个神经元在与其邻近的神经元之间交换信号。树突是信号的接收器,轴突是信号的传递器。

神经元的树突与其他神经元的轴突连接。当神经元发射信号时,信号沿着轴突传递,并传递给下一个神经元的树突。这就是它们的连接方式,也是神经元工作的方式。现在我们可以从神经科学转向技术领域了。

人工神经元

以下是神经元在人工神经网络中的表现方式:

图 10:带有单个神经元的人工神经网络

就像人类神经元一样,它接收一些输入信号,并且有一个输出信号。连接输入信号到神经元,以及神经元到输出信号的蓝色箭头,就像人类神经元中的突触。

在这个人工神经元中,输入和输出信号究竟是什么呢?输入信号是缩放后的独立变量,构成了环境的状态。例如,在本书稍后会编写的服务器冷却实际示例中(第十一章面向商业的 AI – 使用深度 Q 学习最小化成本),这些输入信号包括服务器的温度、用户数量和数据传输率。输出信号是输出值,在深度 Q 学习模型中,这些值始终是 Q 值。了解了这些之后,我们可以对机器的神经元做一个通用的表示:

图 11:神经元 – 输出值

为了完成对神经元的描述,我们需要添加这个表示中缺失的最后一个元素,这也是最重要的元素:权重。

每个突触(蓝色箭头)都会被分配一个权重。权重越大,通过突触的信号就越强。理解这一点的关键是,这些权重就是机器随时间更新的部分,以改进其预测。让我们将它们添加到之前的图表中,确保你能清楚地看到它们:

图 12:神经元 – 权重

这就是神经元。接下来需要理解的是激活函数;它是神经元在给定一组输入时决定产生何种输出的方式。

激活函数

激活函数是,它在神经元内部工作,输入是输入值的加权和,返回的输出值如下面的图所示:

图 13:激活函数

如下所示:

你下一个问题可能是: 究竟是什么函数?

它们可能有很多种,但在这里我们将描述三种最常用的,包括你在实际活动中会使用的那种:

  1. 阈值激活函数

  2. Sigmoid 激活函数

  3. 整流激活函数

让我们通过逐一查看它们来进一步推动你的专业知识。

阈值激活函数

阈值激活函数仅由以下公式定义:

并且可以用以下曲线表示:

图 14:阈值激活函数

这意味着通过神经元的信号是不连续的,并且只有在以下情况下才会激活:

现在让我们看看下一个激活函数:Sigmoid 激活函数。Sigmoid 激活函数是人工神经网络中最有效且最广泛使用的激活函数,尤其是在通向输出层的最后一个隐藏层中。

Sigmoid 激活函数

Sigmoid 激活函数由以下公式定义:

并且可以用以下曲线表示:

图 15:Sigmoid 激活函数

这意味着通过神经元传递的信号是连续的,并且会一直被激活。而且值越高:

信号越强。

现在让我们来看一下另一个广泛使用的激活函数:修正线性激活函数(rectifier activation function)。你会在大多数深度神经网络中找到它,但主要是在早期的隐藏层,而不像 sigmoid 函数,它主要用于最后一个隐藏层,通向输出层。

修正线性激活函数

修正线性激活函数的定义非常简单,如下所示:

因此,它由以下曲线表示:

图 16:修正线性激活函数

这意味着通过神经元传递的信号是连续的,只有当值满足特定条件时才会被激活:

输入加权和越高,信号越强。

这引出了一个问题:你应该选择哪个激活函数,或者说,如何知道选择哪个激活函数?

好消息是,答案很简单。其实它取决于作为因变量返回的是什么。如果是二元结果,即 0 或 1,那么一个好的选择是阈值激活函数。如果你想要返回的是因变量为 1 的概率,那么 sigmoid 激活函数是一个非常合适的选择,因为它的 sigmoid 曲线非常适合建模概率。

回顾一下,下面是这个图中突出显示的小蓝图:

图 17:激活函数蓝图

记住,修正线性激活函数应该在具有多个隐藏层的深度神经网络中的隐藏层中使用,而 sigmoid 激活函数应该用于最后一个隐藏层,通向输出层。

让我们在下面的图中突出显示这一点,以便你能更好地视觉化并记住它:

图 18:不同层中的激活函数

我们进展得很快!你已经掌握了相当多的深度学习知识。但这还没有结束——让我们继续下一部分,解释神经网络到底是如何工作的。

神经网络是如何工作的?

为了说明这一点,我们回到预测房地产价格的问题。我们有一些独立变量,用来预测房屋和公寓的价格。为了简化并能够将所有内容表示在图表中,假设我们唯一的独立变量(我们的预测变量)是以下内容:

  1. 面积(平方英尺)

  2. 卧室数量

  3. 到市中心的距离(英里)

  4. 年龄

我们的因变量是我们预测的公寓价格。以下是深度学习中“魔法”是如何运作的。

每个独立的、经过缩放的变量都会被分配一个权重,使得权重越高,独立变量对因变量的影响就越大;也就是说,它将是一个更强的因变量预测器。

一旦新的输入进入神经网络,信号就会从每个输入开始前向传播,最终到达隐藏层的神经元。

在每个隐藏层神经元内部,都会应用激活函数,权重越小,激活函数就越会阻止来自该输入的信号,而权重越大,激活函数则越容易让该信号通过。

最后,所有从隐藏神经元传来的信号(经过激活函数的部分阻断)都会前向传播到输出层,返回最终结果:价格预测。

这是神经网络如何工作的可视化:

图 19:神经网络的工作原理 – 房地产价格预测示例

这只是故事的一半。现在我们知道神经网络如何工作,我们需要了解它是如何学习的。

神经网络是如何学习的?

神经网络通过在多次迭代中更新所有输入和隐藏神经元的权重(当有多个隐藏层时),始终朝着同一个目标前进:减少预测值和实际值之间的损失误差。

为了让神经网络学习,我们需要实际的值,这些值也称为目标。在我们前面的房地产定价示例中,实际值就是从我们的数据集中提取的房屋和公寓的真实价格。这些真实价格依赖于之前列出的独立变量(面积、卧室数量、距离城市的距离和房龄),而神经网络通过以下过程学习更好地预测这些价格:

  1. 神经网络前向传播来自输入的信号;独立变量

  2. 然后它在输出层获得预测价格

  3. 接着计算预测价格 (预测值)和实际价格 y(目标值)之间的损失误差 C

  4. 然后这个损失误差会在神经网络中反向传播,从我们表示的右侧传播到左侧。

  5. 然后,神经网络在每个神经元上运行一种叫做梯度下降的技术(我们将在下一节中讨论),以更新权重,朝着减少损失的方向前进,也就是更新为减少损失误差 C 的新权重。

  6. 然后这个过程会反复执行多次,每次都用新的输入和新的目标,直到我们得到期望的性能(早停)或最后一次迭代(实现中的迭代次数)。

让我们在下一节的两幅单独的图表中展示这个整个过程的两个主要阶段,即前向传播和反向传播的两个主要阶段。

前向传播和反向传播

第一阶段:前向传播

下面是信号如何在人工神经网络中进行前向传播的方式,从输入到输出:

图 20:前向传播

一旦信号通过整个网络传播,就会计算损失误差 C,以便进行反向传播。

第二阶段:反向传播

在前向传播后,接着进行反向传播,期间损失误差 C 从输出传播回神经网络的输入。

图 21:反向传播

在反向传播期间,权重会更新以减少预测(输出值)与目标(实际值)之间的损失误差 C。它们是如何更新的?这就是梯度下降发挥作用的地方。

梯度下降

梯度下降是一种优化技术,帮助我们找到代价函数的最小值,就像前面我们所得到的损失误差 C 一样:

让我们用最直观的方式来可视化它,就像碗中的这个球(上面稍微加点数学):

图 22:梯度下降(1/4)

想象这是一个碗的横截面,我们往里面放入一个小红球,让它找到碗底的位置。经过一段时间,它会停下来,找到碗底的甜点位置。

您可以将梯度下降理解为同样的方式。它从碗中的某个位置(参数的初始值)开始,并尝试找到碗底,或者换句话说,代价函数的最小值。

让我们通过上述图像中显示的示例来进行说明。参数的初始值设置了我们球的位置。基于此,我们得到一些预测结果,然后将其与目标值进行比较。这两组之间的差异就是当前参数集的损失。

然后计算成本函数的一阶导数,关于参数的导数。这就是“梯度”一词的来源。在这里,这一阶导数给出了球所在曲线的切线斜率。如果斜率是负的,就像前面的图像上那样,我们向右边迈下一步。如果斜率是正的,我们向左边迈下一步。

因此,“下降”这个名字来源于我们总是朝着下坡的方向迈下一步,就像下图中所示:

图 23:梯度下降(2/4)

在下一个位置,我们的球停在了一个正斜坡上,所以我们必须向左迈出下一步:

图 24:梯度下降(3/4)

最终,通过重复相同的步骤,球最终会停在碗底:

图 25:梯度下降(4/4)

就是这样!这就是梯度下降在一维(一个参数)中的运作方式。现在你可能会问:“好极了,但它如何扩展?”我们看到了一维优化的例子,但二维或三维呢?

这是一个很好的问题。梯度下降保证了这种方法在需要的维度上可以扩展,前提是成本函数是凸的。事实上,如果成本函数是凸的,梯度下降将找到成本函数的绝对最小值。以下是一个二维的例子:

图 26:梯度下降——凸的成本函数保证收敛

然而,如果成本函数不是凸的,梯度下降将只会找到局部最小值。这里是一个三维的例子:

图 27:非凸函数(左)的非收敛例子(右)

现在我们理解了梯度下降的基本原理,我们可以研究它最先进且最有效的版本:

  1. 批量梯度下降

  2. 随机梯度下降

  3. 小批量梯度下降

“梯度下降”,“批量梯度下降”,“小批量梯度下降”,“随机梯度下降”,有这么多术语,对于像你这样的初学者可能会感到非常困惑。别担心——我会帮助你的。

所有这些版本的梯度下降之间的主要区别在于我们如何将数据输入模型,以及我们多频繁地更新我们的参数(权重)来移动我们的小红球。我们先从解释批量梯度下降开始。

批量梯度下降

批量梯度下降是当我们有一批输入(与单个输入相对)输入神经网络时,前向传播它们,最终获得一批预测,这些预测会与一批目标进行比较。然后,计算预测和目标之间的全局损失误差,作为每个预测与其关联目标之间损失误差的总和。

这个全局损失被反向传播到神经网络中,在那里进行梯度下降或随机梯度下降,以根据每个权重对该全局损失误差的责任大小来更新所有权重。

这里是一个批量梯度下降的例子。要解决的问题是根据学习时间(Study Hrs)和睡眠时间(Sleep Hrs),预测学生在考试中的得分(从 0 到 100 %):

图 28:批量梯度下降

在前面的图示中,值得注意的一点是,这些并不是多个神经网络,而是由不同的权重更新表示的单个神经网络。正如我们在这个批量梯度下降的例子中看到的那样,我们将所有数据一次性输入模型。

这产生了权重的集体更新,并加速了网络的优化。然而,这也有不好的方面。再次出现了可能陷入局部最小值的情况,正如我们在以下图形中看到的那样:

图 29:陷入局部最小值

我们之前已经解释了这一现象的原因:是因为前面图示中的成本函数不是凸函数,这种优化方法(简单的梯度下降)要求成本函数是凸的。如果不是这样,我们就可能会陷入局部最小值,无法找到全局最小值和最佳参数。另一方面,这里是一个凸成本函数的例子,和我们之前看到的那个相同:

图 30:凸函数的例子

简单来说,如果一个函数只有一个全局最小值,它就是凸函数。凸函数的图形呈碗状。然而,在大多数问题中,包括商业问题,成本函数通常不是凸函数(如以下 3D 图示例所示),因此,简单的梯度下降法无法表现良好。这就是随机梯度下降发挥作用的地方。

图 31:非凸函数(左)未收敛的例子(右)

随机梯度下降

随机梯度下降SGD)来拯救局面。它提供了更好的整体结果,防止算法陷入局部最小值。然而,正如其名字所示,它是随机的,换句话说,它是有随机性的。

由于这种特性,无论你运行多少次算法,过程都会略有不同,无论初始化如何。

SGD 不是一次性在整个数据集上运行,而是逐个输入。过程是这样的:

  1. 输入一个单一的观察值。

  2. 将该输入前向传播以得到单一预测值。

  3. 计算预测值(输出)与目标值(实际值)之间的损失误差。

  4. 将损失误差反向传播到神经网络中。

  5. 使用梯度下降更新权重。

  6. 对整个数据集重复步骤 1 至 5。

让我们展示前面提到的例子中前 3 次迭代的情况,这里预测的是考试分数:

第一行观察输入

图 32:随机梯度下降 - 第一行观察输入

第二行观察输入

图 33:随机梯度下降 - 第二行观察输入

第三行观察输入

图 34:随机梯度下降 - 第三行观察输入

前面三个图示都是 SGD 更新过程中某个权重的示例。如我们所见,每次我们只将数据集中的一行观测输入神经网络,然后根据该行数据更新权重,并继续输入下一行数据。

初看之下,SGD 似乎更慢,因为我们将每一行数据分别输入。但实际上,它要快得多,因为我们不需要将整个数据集加载到内存中,也不需要等待整个数据集通过模型来更新权重。

为了总结这一部分,让我们用下面的图表回顾批量梯度下降和 SGD 之间的区别:

图 35:批量梯度下降与随机梯度下降

现在我们可以考虑一种折衷的方法:小批量梯度下降。

小批量梯度下降

小批量梯度下降结合了两者的优点,将批量梯度下降与随机梯度下降(SGD)结合起来。实现方法是通过将小批量数据馈送到人工神经网络中,而不是一行一行地馈送单一输入观测值,或者一次性将整个数据集馈送进网络。

这种方法比经典的 SGD 更快,同时仍能防止你陷入局部最小值。小批量梯度下降也很有帮助,特别是当你没有足够的计算资源将整个数据集加载到内存中,或者没有足够的处理能力来充分利用 SGD 时。

这就是关于神经网络的全部内容!现在你已经准备好将神经网络的知识与 Q-learning 的知识结合起来了。

深度 Q-learning

你已经了解了深度学习的基础,并且已经熟悉了 Q-learning;由于深度 Q-learning 是将 Q-learning 与深度学习结合起来的,你现在准备好直观地掌握深度 Q-learning,并充分理解它。

在我们开始之前,试着猜一猜这将如何运作。我希望你花点时间思考一下,如何将 Q-learning 整合到人工神经网络(ANN)中。

首先,你可能已经猜到了神经网络的输入和输出是什么。人工神经网络的输入当然是环境的输入状态,它可能是一个 1 维的向量,编码了环境中的变化,或者是一张图像(就像自动驾驶汽车所看到的那样)。输出则是每个动作的 Q 值集,即它将是一个 1 维的向量,包含多个 Q 值,每个 Q 值对应一个可执行的动作。然后,就像之前一样,AI 会选择具有最大 Q 值的动作并执行。

简单来说,这意味着我们不再通过贝尔曼方程(简单的 Q-learning)进行迭代更新来预测 Q 值,而是通过人工神经网络来预测 Q 值,神经网络的输入是状态信息,输出则是不同动作的 Q 值。

这就引出了一个问题:我们知道要预测什么很好,但在训练 AI 时,这些预测的目标(实际值)是什么呢?提醒一下,目标是实际值,或者说你希望预测的理想值:你的预测越接近目标,就越正确。因此,我们计算预测与目标之间的损失误差 C,以便通过反向传播与随机或小批量梯度下降来减少它。

当我们做简单的房价预测时,目标是显而易见的。它们只是数据集中可以获得的价格。但当你在训练自动驾驶汽车时,Q 值的目标是什么呢?这就不那么明显了,尽管它是 Q 值和奖励的显式函数。

答案是深度 Q 学习中的一个基本公式。输入状态 的目标是:

其中 是最后获得的奖励, 是折扣因子,如前所述。

你认出目标的公式了吗?如果你记得 Q 学习,你应该不难回答这个问题。

当然在时间差分中!记住,时间差分定义为:

所以现在很明显了。目标就是时间差分左边的第一个元素:

所以我们得到:

请注意,在开始时,Q 值为零,因此目标仅仅是奖励。

在我们能说自己真正理解深度 Q 学习之前,还有一个关键点:Softmax 方法。

Softmax 方法

这是我们准备好将一切组合成深度 Q 学习之前的最后一块拼图。Softmax 方法是我们在预测 Q 值后选择执行动作的方式。在 Q 学习中,这很简单;执行的动作是 Q 值最高的那个。那是 argmax 方法。在深度 Q 学习中,情况不同了。问题通常更加复杂,因此,为了找到最优解,我们必须经历一个叫做 探索 的过程。

探索包括以下内容:我们不会执行具有最大 Q 值的动作(称为利用),而是将每个动作的概率与其 Q 值成正比,即 Q 值越高,概率越高。这样就创建了一个可执行动作的分布。最终,执行的动作将从这个分布中随机抽取。让我用一个例子来说明。

假设我们正在构建一辆自动驾驶汽车(实际上我们会在 第十章,自动驾驶车辆 AI - 构建自动驾驶汽车 中实现)。假设可以执行的动作很简单:前进、左转或右转。

然后,在某个特定时刻,假设我们的 AI 预测了以下 Q 值:

向前移动 向左转 向右转
24 38 11

我们可以创建所需概率分布的方法是将每个 Q 值除以三个 Q 值的总和,每次得到一个特定动作的概率。让我们进行这些求和:

向左转的概率

完美——这些概率加起来为 1,并且与 Q 值成比例。这为我们提供了动作的分布。为了执行一个动作,Softmax 方法从这个分布中随机抽取一个动作,即:

  • 向前移动的动作有 33%的概率被选择。

  • 向左转的动作有 52%的概率被选择。

  • 向右转的动作有 15%的概率被选择。

你能感受到 Softmax 和 argmax 之间的区别吗?你理解为什么它被称为探索而不是开发吗?在使用 argmax 时,动作向左转将是唯一确定执行的动作。这就是开发。但在使用 Softmax 时,尽管向左转是被选择的概率最高的动作,但仍然有可能选择其他动作。

现在,当然,问题是:为什么我们要这么做?原因很简单:我们想要探索其他动作,以防它们导致的状态转移带来比纯开发更高的奖励。这通常发生在复杂问题中,而深度 Q 学习正是用来解决这类问题的。深度 Q 学习通过其先进的模型找到了解决方案,同时也通过探索动作实现。这是 AI 中的一种技术,叫做策略探索(Policy Exploration)。

正如之前所说,下一步是退一步。我们将回顾一下深度 Q 学习是如何工作的。

深度 Q 学习回顾

深度 Q 学习是将 Q 学习与人工神经网络(ANN)结合的过程。

输入是编码向量,每个向量定义了环境的一个状态。这些输入进入一个 ANN,其中输出包含每个动作的预测 Q 值。

更准确地说,如果有n个可能的动作可以由 AI 执行,那么人工神经网络的输出将是一个包含n个元素的 1D 向量,每个元素对应于当前状态下可以执行的每个动作的 Q 值。然后,所执行的动作是通过 Softmax 方法选择的。

因此,在每个状态下

  1. 预测是 Q 值 ,其中 是通过 Softmax 方法执行的。

  2. 目标是

  3. 预测与目标之间的损失误差是时间差分的平方:

这个损失误差会通过反向传播传入神经网络,并且根据它们对误差的贡献,通过随机梯度下降或小批量梯度下降更新权重。

经验回放

你可能注意到,到目前为止,我们只考虑了从一个状态 到下一个状态 的过渡。这样做的问题在于, 大部分时间是高度相关的;因此,神经网络的学习进展不大。

如果我们每次不仅仅考虑最后一个过渡,而是考虑最近的 m 个过渡,其中 m 是一个较大的数值,这样可能会有所改进。这一组最近的 m 个过渡被称为经验回放记忆,或简称为内存。从这个内存中,我们会随机抽取一些过渡状态,组成小批量数据。然后,我们用这些批量数据训练神经网络,并通过小批量梯度下降来更新权重。

整个深度 Q 学习算法

让我们总结一下整个深度 Q 学习过程的不同步骤。

初始化:

  1. 将经验回放的内存初始化为空列表 M

  2. 选择一个最大内存大小。

在每个时间点 t,我们重复以下过程,直到一个周期结束:

  1. 预测当前状态的 Q 值

  2. 执行 Softmax 方法选择的动作:

  3. 获取奖励

  4. 达到下一个状态

  5. 将过渡状态 添加到内存 M 中。

  6. 从一个随机批次中取出 的过渡状态。对于随机批次中的所有过渡状态

    • 获取预测值:

    • 获取目标值:

    • 计算整个批次的预测值与目标之间的损失 :

    • 将这个损失误差通过反向传播回到神经网络,并通过随机梯度下降,更新权重以反映它们对损失误差的贡献。

你刚刚解锁了完整的深度 Q 学习过程!这意味着你现在能够在多个领域构建强大的现实世界 AI 应用。以下是一些深度 Q 学习能够创造显著附加价值的应用场景:

  1. 能源:正是深度 Q 学习模型帮助 DeepMind AI 将 Google 数据中心的冷却费用减少了 40%。此外,深度 Q 学习还能优化智能电网的运行;换句话说,它能让智能电网变得更智能。

  2. 交通:深度 Q 学习可以优化交通信号灯控制,以减少交通拥堵。

  3. 自动驾驶汽车:深度 Q 学习可以用于构建自动驾驶汽车,我们将在本书的下一章进行说明。

  4. 机器人技术:今天,许多先进的机器人都采用了深度 Q 学习。

  5. 以及更多:化学、推荐系统、广告等等——甚至是视频游戏,正如你在第十三章中所发现的,AI 与游戏——成为贪吃蛇游戏的高手,在这一章中,你将使用深度卷积 Q 学习训练 AI 玩贪吃蛇游戏。

总结

你在这一章学到了很多内容;我们首先讨论了人工神经网络(ANNs)。ANNs 由多个层次的神经元构成,每一层的神经元与上一层的所有神经元连接,每一层都有自己的激活函数——该函数决定了每个输出信号应该被屏蔽多少。

神经网络进行预测的步骤称为前向传播,而进行学习的步骤称为反向传播。反向传播有三种主要类型:批量梯度下降、随机梯度下降和最佳的迷你批量梯度下降,它结合了前两种方法的优点。

我们在这一章最后讨论的内容是深度 Q 学习。这种方法利用神经网络预测采取某些行动的 Q 值。我们还提到了经验回放记忆,它为我们的 AI 存储了大量经验。

在下一章,你将通过编写代码实践这一切,打造你自己的自动驾驶汽车。

第十章:自主车辆的人工智能——构建一辆自动驾驶汽车

我真是非常激动,期待你开始这个新章节。这可能是本书中最具挑战性,同时也是最有趣的冒险。你将从零开始,使用强大的深度 Q 学习模型,构建一辆 2D 地图上的自动驾驶汽车。我觉得这真是令人兴奋!

快点想想,第一步是什么?

如果你回答了“构建环境”,你完全正确。我希望这个答案已经让你感到如此熟悉,以至于我还没问完你就回答出来了。让我们从构建一个汽车可以自己学习如何驾驶的环境开始。

构建环境

这一次,我们需要定义的内容远不止状态、动作和奖励。构建一辆自动驾驶汽车是一个非常复杂的问题。现在,我不会要求你去车库,把自己变成一位混合型 AI 机械师;你只需在一个 2D 地图上构建一个虚拟的自动驾驶汽车,让它四处移动。

你将在 Kivy 网络应用中构建这个 2D 地图。Kivy 是一个免费的开源 Python 框架,用于开发类似游戏的应用,或者任何种类的移动应用。你可以在这里查看网站:kivy.org/#home

本项目的整个环境都是用 Kivy 构建的,从头到尾。地图和虚拟汽车的开发与 AI 无关,因此我们不会逐行讲解实现它的代码。

然而,我将描述一下地图的功能。对于那些好奇地图是如何构建的朋友,我已经在 GitHub 上提供了一个完全注释过的 Python 文件,名为 map_commented.py,它从头开始构建这个环境,并提供了详细的解释。

在查看所有功能之前,让我们先看一下这个地图,上面有一辆小虚拟车:

图 1:地图

你首先会注意到的是一个黑色的屏幕,那就是 Kivy 用户界面。你在这个界面内构建你的游戏或应用。正如你可能猜到的,它实际上是整个环境的容器。

你会看到里面有些奇怪的东西,一个白色的矩形,前面有三个彩色的圆点。嗯,那就是汽车!抱歉,我不是个更好的艺术家,但保持简单是很重要的。那个白色的小矩形就是汽车的形状,而那三个小圆点是汽车的传感器。为什么我们需要传感器呢?因为在这个地图上,我们将有选项构建被沙地隔开的道路,而汽车必须避免通过这些沙地。

要在地图上放一些沙子,只需持续按住鼠标左键并绘制你想要的任何内容。它不一定只是道路;你还可以添加一些障碍物。无论如何,汽车必须避免通过沙地。

如果你记得一切都来自于奖励,我相信你已经知道如何实现这一点;那就是当自动驾驶汽车驶上沙地时,通过给予它一个不好的奖励来进行惩罚。我们稍后会处理这个问题。与此同时,让我们看一下我绘制的带有沙子的道路示意图:

图 2:带有绘制道路的地图

传感器用来检测沙子,以便汽车能够避免。蓝色传感器覆盖汽车左侧的区域,红色传感器覆盖汽车前方的区域,黄色传感器覆盖汽车右侧的区域。

最后,在屏幕的左下角有三个按钮可以点击,它们是:

clear: 移除地图上绘制的所有沙子

save: 保存 AI 的权重(参数)

load: 加载最后保存的权重

现在我们已经看过了这张小地图,接下来让我们定义一下我们的目标。

定义目标

我们理解我们的目标是构建一辆自动驾驶汽车。很好。那么我们如何在 AI 和强化学习的框架下对这个目标进行形式化定义呢?希望你的直觉已经让你想到了我们将设定的奖励。我同意——如果我们的汽车成功实现自动驾驶,我们会给予它一个高奖励。但是,我们怎么判断它已经成功实现了自动驾驶呢?

我们有很多方法可以评估这个问题。例如,我们可以在地图上画一些障碍物,并训练我们的自动驾驶汽车在不碰到障碍物的情况下绕过它们。这是一个简单的挑战,但我们可以尝试一些更有趣的挑战。还记得我之前画的那条路吗?那我们来训练汽车从地图的左上角到右下角,沿着我们在这两个点之间建立的任何道路行驶。这才是一个真正的挑战,这就是我们要做的。假设这张地图是一个城市,左上角是机场,右下角是市中心:

图 3:两个目的地——机场和市中心

现在我们可以清楚地制定一个目标;训练自动驾驶汽车在机场和市中心之间往返。当它到达机场时,就必须前往市中心;而当它到达市中心时,又必须返回机场。更重要的是,它应该能够沿着任何连接这两个地点的道路完成这些往返。它还应该能够应对道路上任何需要避免的障碍。这里是另一条更具挑战性的道路示例:

图 4:更具挑战性的道路

如果你认为这条路看起来太简单,这里有一个更具挑战性的例子;这次不仅是更难的道路,而且还有许多障碍:

图 5:一条更具挑战性的道路

最后一个例子,我想分享这张由我的一位学生设计的地图,它可能出现在电影盗梦空间中:

图 6:有史以来最具挑战性的道路

如果你仔细看,它仍然是从机场到市中心来回的道路,只不过现在变得更具挑战性了。我们创建的 AI 将能够应对这些地图中的任何一种。

我希望你能和我一样感到兴奋!保持这种能量,因为我们还有很多工作要做。

设置参数

在你定义输入状态、输出动作和奖励之前,必须先设置所有关于地图和汽车的参数,这些参数将成为你环境的一部分。输入、输出和奖励都是这些参数的函数。让我们列出它们,使用和代码中相同的名称,这样你就能轻松理解文件 map.py

  1. angle:地图的 x-轴与汽车轴之间的角度

  2. rotation:汽车做的最后一次旋转(我们稍后会看到,执行一个动作时,汽车会进行旋转)

  3. pos = (self.car.x, self.car.y):汽车的位置(self.car.x 是汽车的 x-坐标,self.car.y 是汽车的 y-坐标)

  4. velocity = (velocity_x, velocity_y):汽车的速度向量

  5. sensor1 = (sensor1_x, sensor1_y):第一个传感器的位置

  6. sensor2 = (sensor2_x, sensor2_y):第二个传感器的位置

  7. sensor3 = (sensor3_x, sensor3_y):第三个传感器的位置

  8. signal1:传感器 1 接收到的信号

  9. signal2:传感器 2 接收到的信号

  10. signal3:传感器 3 接收到的信号

现在我们放慢一下进度;我们得定义这些信号是如何计算的。这些信号是传感器周围沙子密度的度量。你打算如何计算这种密度呢?你首先引入一个新变量,叫做 sand,它初始化为一个数组,数组的单元格数量与我们的图形界面的像素数量相同。简单来说,sand 数组就是黑色地图本身,像素则是数组的单元格。然后,如果某个位置有沙子,sand 数组中的相应单元格会被赋值为 1,如果没有沙子,则赋值为 0。

例如,这里 sand 数组的前几行只有 1,其余部分全是 0:

图 7:只有前几行有沙子的地图

我知道边界有点晃动——就像我说的,我不是伟大的艺术家——这只是意味着 sand 数组的那些行会在有沙子的地方显示 1,而没有沙子的地方显示 0。

现在你有了这个 sand 数组,计算每个传感器周围的沙子密度变得非常简单。你将传感器周围放置一个 20x20 单元格的正方形(传感器从 sand 数组中读取这些单元格),然后计算这些单元格中 1 的数量,最后将这个数字除以该正方形中的总单元格数,即 20 x 20 = 400 个单元格。

由于sand数组只包含 1(表示有沙子)和 0(表示没有沙子),我们可以通过简单地对这个 20x20 平方中的sand数组单元格求和,轻松地计算出 1 的数量。这就给出了每个传感器周围的沙子密度,而这正是在map.py文件中的第 81、82 和 83 行计算的内容:

 self.signal1 = int(np.sum(sand[int(self.sensor1_x)-10:int(self.sensor1_x)+10, int(self.sensor1_y)-10:int(self.sensor1_y)+10]))/400\.   #81
        self.signal2 = int(np.sum(sand[int(self.sensor2_x)-10:int(self.sensor2_x)+10, int(self.sensor2_y)-10:int(self.sensor2_y)+10]))/400\.   #82
        self.signal3 = int(np.sum(sand[int(self.sensor3_x)-10:int(self.sensor3_x)+10, int(self.sensor3_y)-10:int(self.sensor3_y)+10]))/400\.   #83 

现在我们已经讲解了信号是如何计算的,让我们继续讨论其余的参数。下面我标出的一些最后的参数很重要,因为它们是我们需要的最后几块拼图,以揭示最终的输入状态向量。它们是:

  1. goal_x:目标的x坐标(可以是机场或市中心)

  2. goal_y:目标的y坐标(可以是机场或市中心)

  3. xx = (goal_x - self.car.x):目标和汽车之间的x坐标差

  4. yy = (goal_y - self.car.y):目标和汽车之间的y坐标差

  5. orientation:测量汽车相对于目标方向的角度

让我们再慢下来一下。我们需要知道如何计算方向;它是汽车的轴(来自我们参数列表中的velocity向量)与连接目标和汽车中心的轴之间的角度。目标的坐标是(goal_xgoal_y),汽车中心的坐标是(self.car.xself.car.y)。例如,如果汽车正朝目标完全前进,那么方向 = 0°。如果你对如何在 Python 中计算这两个轴之间的角度感兴趣,这里是获取orientation的代码(map.py文件中的第 126、127 和 128 行):

 xx = goal_x - self.car.x   #126
        yy = goal_y - self.car.y   #127
        orientation = Vector(*self.car.velocity).angle((xx,yy))/180\.   #128 

好消息——我们终于准备好定义环境的主要支柱了。我说的当然是输入状态、动作和奖励。

在我定义它们之前,试着猜猜它们是什么。再检查一下所有前面的参数,并记住目标:在两个地点之间来回旅行,即机场和市中心,同时避免道路上的任何障碍。解决方案在下一节。

输入状态

你认为输入状态是什么?你可能回答了“汽车的位置”。在这种情况下,输入状态将是一个包含两个元素的向量,汽车的坐标:self.car.xself.car.y

这是一个不错的开始。从你在第九章《成为人工智能专家 - 深度 Q 学习》中学到的深度 Q 学习的直觉和基础技术来看,你知道在做深度 Q 学习时,输入状态不一定像 Q 学习那样是单一的元素。实际上,在深度 Q 学习中,输入状态可以是多个元素的向量,从而允许你为 AI 提供多个信息来源,帮助它预测智能的动作来执行。

输入状态甚至可以比一个简单的向量更大:它可以是一个图像!在这种情况下,AI 模型被称为深度卷积 Q 学习。它和深度 Q 学习相同,只不过在神经网络的入口处加入了卷积神经网络,让你的 AI(机器)能够处理图像。我们将在第十二章深度卷积 Q 学习中介绍这项技术。

我们可以做得比仅仅提供汽车位置坐标更好。位置坐标告诉我们自动驾驶汽车的位置,但有一个更好的参数,它更简单,并且与目标更直接相关。我说的是orientation变量。方向是一个单一的输入,它直接告诉我们是否朝着正确的方向前进,朝着目标。如果我们有了这个方向,我们就不再需要汽车的位置坐标来导航到目标;我们只需要改变方向一定角度,就可以让汽车更朝着目标的方向行驶。AI 执行的操作将是改变这个方向的动作。我们将在下一节讨论这些操作。

我们有了输入状态的第一个元素:方向。

但这还不够。记住,我们还有另一个目标,或者说是约束。我们的车需要保持在道路上,并避开道路上的任何障碍物。

在输入状态中,我们需要一些信息来告诉 AI 它是否即将驶出道路或撞到障碍物。试着自己推理一下——我们有办法获取这些信息吗?

解决方案就是传感器。记住,我们的车有三个传感器,给我们提供关于周围沙子多少的信号。蓝色传感器告诉我们车左边是否有沙子,红色传感器告诉我们车前方是否有沙子,黄色传感器告诉我们车右边是否有沙子。这些传感器的信号已经被编码成三个变量:signal1signal2signal3。这些信号将告诉 AI 它是否即将撞到障碍物或驶出道路,因为道路是由沙子界定的。

这就是你所需的输入状态的其余信息。通过这四个元素,signal1signal2signal3orientation,你拥有了足够的信息,能够从一个位置驾驶到另一个位置,同时保持在道路上,并避免撞到任何障碍物。

总结一下,下面是每次的输入状态:

输入状态 = (orientation, signal1, signal2, signal3)

而这正是map.py文件中第 129 行的编码内容:

 state = [orientation, self.car.signal1, self.car.signal2, self.car.signal3]   #129 

state是给输入状态指定的变量名。

不必太担心signalself.signalself.car.signal之间的代码语法差异;它们是一样的。我们使用这些不同的变量是因为 AI 是用类(如面向对象编程OOP))编写的,这样我们就可以在同一张地图上创建多个自动驾驶汽车。

如果你想在地图上拥有多辆自动驾驶汽车,例如,如果你想让它们进行比赛,那么你可以通过self.car.signal更好地区分这些车。例如,如果你有两辆车,你可以将这两个对象命名为car1car2,然后通过使用self.car1.signal1self.car2.signal1来区分这两辆车的第一个传感器信号。在本章中,我们只有一辆车,因此无论是使用signal1car.signal1还是self.car.signal1,结果都是一样的。

我们已经讨论过输入状态;现在让我们来处理动作。

输出动作

我已经简要提到或暗示了这些动作会是什么。根据我们的输入状态,猜测这些动作是很容易的。自然地,因为你正在构建一辆自动驾驶汽车,你可能会认为动作应该是:前进、左转或右转。你完全正确!这正是这些动作会是的内容。

这不仅直观,而且与我们选择的输入状态极其吻合。输入状态包含了orientation变量,它告诉我们汽车是否朝着正确的方向驶向目标。简单来说,如果orientation输入告诉我们我们的车指向正确的方向,我们就执行前进的动作。如果orientation输入告诉我们目标在车的右侧,我们就执行右转的动作。最后,如果orientation告诉我们目标在车的左侧,我们就执行左转的动作。

同时,如果任何信号检测到车周围有沙子,汽车会向左或向右转弯以避开它。前进、左转和右转这三种可能的动作与我们所设定的目标、约束和输入状态是逻辑一致的,我们可以将它们定义为以下三个旋转:

旋转 = [转动 0°(即前进)、向左转 20°、向右转 20°]

选择 20°是相当任意的。你完全可以选择 10°、30°或 40°。我建议避免选择超过 40°,因为那样你的车子会有不稳定、抖动的动作,看起来就不像是一辆平稳行驶的车了。

然而,人工神经网络输出的动作不会是 0°、20°和-20°,它们将是 0、1 和 2。

动作 = [0, 1, 2]

在处理人工神经网络的输出时,使用像这样的简单类别总是更好的。由于 0、1 和 2 将是 AI 返回的动作,那么你认为我们是如何得到旋转角度的呢?

你将使用一个简单的映射,代码中叫做action2rotation,它将动作 0、1、2 映射到相应的旋转角度 0°、20°、-20°。这正是map.py文件中第 34 行和第 131 行的代码:

action2rotation = [0,20,-20]   #34

        rotation = action2rotation[action]   #131 

现在,让我们继续讨论奖励。这个部分会很有趣,因为在这里你决定如何奖励或惩罚你的汽车。先尝试自己思考一下,然后再看看下一节中的解决方案。

奖励

为了定义奖励系统,我们必须回答以下问题:

  • 在哪些情况下我们会给 AI 一个好奖励?每种情况下的奖励有多好?

  • 在哪些情况下我们会给 AI 一个坏奖励?每种情况下的奖励有多坏?

为了回答这些问题,我们只需要记住目标和约束是什么:

  • 目标是进行机场和市区之间的往返。

  • 约束是保持在道路上,并避免任何障碍物。换句话说,约束是远离沙地。

因此,基于这个目标和约束,我们之前问题的答案是:

  1. 当 AI 向目标靠近时,我们会给予它一个好奖励。

  2. 当 AI 离目标越来越远时,我们会给予它一个坏奖励。

  3. 如果 AI 即将驶入沙地,我们会给予它一个坏奖励。

就是这样!这样应该能奏效,因为这些好坏奖励直接影响目标和约束。

为了回答每个问题的第二部分,即对于每种情况,奖励应该有多好或多坏,我们将采取强硬手段;这通常更有效。强硬手段包括在汽车犯错时给予比表现好时更多的惩罚。换句话说,坏奖励将比好奖励更强烈。

这种方法在强化学习中效果很好,但这并不意味着你应该用同样的方法训练你的狗或孩子。当你面对一个生物系统时,反过来(高好奖励和小坏奖励)是一种更有效的训练或教育方式。仅供参考。

说到这一点,以下是每种情况下我们会给予的奖励:

  1. 如果 AI 驶入沙地,它会得到-1 的坏奖励。真讨厌!

  2. 如果 AI 远离目标,它会得到-0.2 的坏奖励。

  3. 如果 AI 向目标靠近,它会得到一个 0.1 的好奖励。

我们将最差奖励(-1)赋给汽车驶入沙地的情况,这是合理的。驶入沙地是我们绝对希望避免的。地图上的沙地代表现实生活中的障碍物;在现实生活中,你会训练自动驾驶汽车避开任何障碍物,以避免事故发生。为了实现这一点,当 AI 在训练过程中撞到障碍物时,我们会给予它极其严重的坏奖励。

那么,如何将其转化为代码呢?很简单;你只需要检查sand数组,看汽车是否刚刚驶入包含 1 的格子。如果是,那就意味着汽车驶入了沙地,因此必须获得-1 的坏奖励。这正是map.py文件中第 138、139 和 140 行的代码(包括更新汽车速度向量,不仅通过将汽车速度减慢到 1 来更新速度,还通过一定角度更新汽车方向self.car.angle)。

 if sand[int(self.car.x),int(self.car.y)] > 0:   #138
            self.car.velocity = Vector(1, 0).rotate(self.car.angle)   #139
            reward = -1   #140 

对于其他的奖励分配,你只需要在前面的if条件后面加一个else,这将说明在汽车没有驶入沙地的情况下会发生什么。

在这种情况下,你开始一个新的 ifelse 条件,表示如果汽车远离目标,你给予一个坏的奖励 -0.2,如果汽车更接近目标,则给予一个好的奖励 0.1。衡量汽车是否远离或接近目标的方法是通过比较两个距离,这两个距离分别存储在两个变量中:last_distance,表示在时刻 t-1 时汽车与目标之间的距离,以及 distance,表示在时刻 t 时汽车与目标之间的当前距离。如果将这些组合在一起,你会得到以下代码,完成前面的代码行:

 if sand[int(self.car.x),int(self.car.y)] > 0:   #138
            self.car.velocity = Vector(1, 0).rotate(self.car.angle)   #139
            reward = -1   #140
        else:   #141
            self.car.velocity = Vector(6, 0).rotate(self.car.angle)   #142
            reward = -0.2   #143
            if distance < last_distance:   #144
                reward = 0.1   #145 

为了防止汽车尝试驶出地图,map.py 文件的第 147 行到 158 行会惩罚 AI,如果自动驾驶汽车距离地图的任意 4 条边界 10 像素以内,它会被赋予一个坏奖励 -1。最后,map.py 文件的第 160 行到 162 行会在汽车距离当前目标 100 像素以内时,更新目标,将其从机场切换到市区,反之亦然。

AI 解决方案回顾

让我们通过回顾深度 Q 学习过程的步骤,来刷新一下记忆,同时将其适应到我们的自动驾驶汽车应用中。

初始化:

  1. 经验重放的记忆被初始化为空列表,在代码中称为 memory

  2. 记忆的最大容量被设置,在代码中称为 capacity

在每个时刻 t,AI 重复以下过程,直到本轮结束:

  1. AI 预测当前状态 S[t] 的 Q 值。因此,由于可以执行三种动作(0 <-> 0°,1 <-> 20°,或 2 <-> -20°),它得到了三个预测的 Q 值。

  2. AI 执行一个通过 Softmax 方法选定的动作(参见 第五章你的第一个 AI 模型 – 当心土匪!):

  3. AI 收到一个奖励 ,奖励值可能是-1、-0.2 或 +0.1。

  4. AI 到达下一个状态 ,该状态由三个传感器的下一个信号以及汽车的方向组成。

  5. AI 将转换 添加到记忆中。

  6. AI 获取一个随机批次 的转换。对于随机批次中的所有转换

    • AI 获取预测值:

    • AI 获取目标值:

    • AI 计算整个批次中预测值与目标值之间的损失 :

    • 最后,AI 将这个损失误差反向传播到神经网络中,并通过随机梯度下降法根据每个权重对损失误差的贡献来更新权重。

实现

现在是时候开始实现了!你首先需要一套专业的工具包,因为你不可能仅仅用简单的 Python 库来构建一个人工大脑。你需要的是一个高级框架,它能够快速计算神经网络的训练过程。

今天,构建和训练 AI 的最佳框架是TensorFlow(由 Google 开发)和PyTorch(由 Facebook 开发)。你应该如何在这两者之间做出选择?它们都非常适合使用,且功能强大。它们都有动态计算图,可以快速计算训练模型时反向传播和小批量梯度下降所需的复杂函数的梯度。实际上,选择哪一个框架并不重要;两者都非常适合我们的自动驾驶汽车。就我个人而言,我在 PyTorch 方面稍有更多经验,因此我将选择 PyTorch,本章中的示例也将继续使用 PyTorch 进行演示。

回过头来看,我们的自动驾驶汽车实现由三个 Python 文件组成:

  1. car.kv,包含 Kivy 对象(汽车的矩形形状和三个传感器)

  2. map.py,用于构建环境(地图、汽车、输入状态、输出动作、奖励)

  3. deep_q_learning.py,用于通过深度 Q 学习来构建和训练 AI

我们已经介绍了map.py的主要元素,现在我们将开始处理deep_q_learning.py,在这里你不仅要构建一个人工神经网络,还要实现深度 Q 学习训练过程。让我们开始吧!

第一步 – 导入库

和往常一样,你需要通过导入必要的库和模块来开始构建 AI。这些包括:

  1. os:操作系统库,用于加载保存的 AI 模型。

  2. random:用于从记忆中抽取一些随机转移进行经验回放。

  3. torch:PyTorch 的主要库,将用于通过张量构建我们的神经网络,而不是像numpy数组那样使用简单的矩阵。矩阵是二维数组,而张量可以是n维数组,其单元格中不仅包含一个数字。以下是一个图示,帮助你清楚地理解矩阵和张量之间的区别:

  4. torch.nn:PyTorch 库中的nn模块,用于构建我们 AI 的人工神经网络中的全连接层。

  5. torch.nn.functionalnn模块中的functional子模块,用于调用激活函数(修正线性单元和 Softmax),以及用于反向传播的损失函数。

  6. torch.optim:PyTorch 库中的optim模块,用于调用 Adam 优化器,它计算损失函数相对于权重的梯度,并在减小损失的方向上更新这些权重。

  7. torch.autograd:PyTorch 库中的autograd模块,用于调用Variable类,该类将每个张量及其梯度关联到同一个变量中。

这构成了你的第一段代码:

# AI for Autonomous Vehicles - Build a Self-Driving Car   #1
#2
# Importing the libraries   #3
#4
import os   #5
import random   #6
import torch   #7
import torch.nn as nn   #8
import torch.nn.functional as F   #9
import torch.optim as optim   #10
from torch.autograd import Variable   #11 

第 2 步 – 创建神经网络的架构

这一段代码是你真正开始构建 AI 大脑的地方。你将要构建输入层、全连接层和输出层,并选择一些激活函数,用于在大脑内部进行前向传播信号。

首先,你将把这个大脑构建在一个类里面,我们将这个类称为Network

什么是类?在我们解释为什么使用类之前,先来解释一下什么是类。类是 Python 中的一种高级结构,它包含了我们想要构建的对象的指令。以神经网络(即对象)为例,这些指令包括你想要多少层、每一层里面有多少个神经元、你选择了哪种激活函数,等等。这些参数定义了你的人工大脑,并且都聚集在我们称之为__init__()的方法中,这也是我们在构建类时总是从这里开始的。但这还不是全部——类还可以包含工具,称为方法,这些方法是执行某些操作或返回某些东西的函数。你的Network类将包含一个方法,用于在神经网络中进行前向传播并返回预测的 Q 值。我们将这个方法命名为forward

那么,为什么使用类呢?这是因为构建类可以让你创建任意多个对象(也叫实例),并且只需改变类的参数就能轻松切换。举个例子,你的Network类包含两个参数:input_size(输入的数量)和nb_actions(动作的数量)。如果你将来想要构建一个拥有更多输入(除了信号和方向)或更多输出(例如添加一个能够刹车的动作)的 AI,得益于类的高级结构,你可以快速实现。这非常实用,如果你还不熟悉类的概念,你将需要尽快掌握它们。几乎所有的 AI 实现都使用类。

这只是一个简短的技术说明,确保我在讲解过程中没有让任何人迷失。现在,让我们开始构建这个类。由于代码中有许多重要的元素需要解释,并且你可能对 PyTorch 不太熟悉,所以我会先展示代码,然后逐行解释来自deep_q_learning.py文件的内容:

# Creating the architecture of the Neural Network   #13
#14
class Network(nn.Module):   #15
    #16
    def __init__(self, input_size, nb_action):   #17
        super(Network, self).__init__()   #18
        self.input_size = input_size   #19
        self.nb_action = nb_action   #20
        self.fc1 = nn.Linear(input_size, 30)   #21
        self.fc2 = nn.Linear(30, nb_action)   #22
    #23
    def forward(self, state):   #24
        x = F.relu(self.fc1(state))   #25
        q_values = self.fc2(x)   #26
        return q_values   #27 

第 15 行:你引入了Network类。在这个类的括号内,你可以看到nn.Module。这意味着你调用了Module类,它是从nn模块中提取的一个现有类,用来获取Module类的所有属性和工具,并在你的Network类中使用它们。这个在新类中调用另一个现有类的技巧叫做继承

第 17 行:你从__init__()方法开始,它定义了人工神经网络的所有参数(输入数量、输出数量等)。你可以看到三个参数:selfinput_sizenb_actionself指代对象,即类创建后将要生成的未来实例。每当你看到self出现在变量前面,并且通过点(.)与变量分隔时,意味着该变量属于该对象。这应该能解开关于self的一切谜团!

然后,input_size是输入状态向量中的输入数量(因此是 4),nb_action是输出动作的数量(因此是 3)。重要的是要理解,__init__()方法中的参数(除了self)是你在创建未来对象时会输入的参数,也就是未来你的 AI 人工大脑。

第 18 行:你使用super()函数来激活继承(如第 15 行所述),该函数位于__init__()方法内。

第 19 行:这里你引入了第一个对象变量self.input_size,并将其设置为与参数input_size相等(稍后将输入为4,因为输入状态有 4 个元素)。

第 20 行:你引入了第二个对象变量self.nb_action,并将其设置为与参数nb_action相等(稍后将输入为3,因为可以执行三个动作)。

第 21 行:你引入了第三个对象变量self.fc1,它是输入层(由输入状态组成)与隐藏层之间的第一个全连接。这个第一个全连接作为nn.Linear类的对象创建,它接受两个参数:第一个是左侧层(输入层)中的元素数量,因此应该使用input_size作为参数,第二个是右侧层(隐藏层)中的隐藏神经元数量。在这里,你选择了 30 个神经元,因此第二个参数是30。选择 30 只是一个任意的决定,自动驾驶汽车也可以在其他数量下正常工作。

第 22 行:你引入了第四个对象变量self.fc2,它是隐藏层(由 30 个隐藏神经元组成)与输出层之间的第二个全连接。它本来也可以是与新隐藏层的全连接,但你的问题不复杂到需要多个隐藏层,因此你在人工大脑中只会有一个隐藏层。和之前一样,这个第二个全连接作为nn.Linear类的对象创建,它接受两个参数:第一个是左侧层(隐藏层)中的元素数量,因此是30,第二个是右侧层(输出层)中的隐藏神经元数量,因此是3

第 24 行:你开始构建类的第一个也是唯一的方法——forward 方法,该方法将信号从输入层传播到输出层,然后返回预测的 Q 值。这个 forward 方法接受两个参数:self,因为你将在 forward 方法中使用对象变量,以及 state,输入状态向量,由四个元素(方位加上三个信号)组成。

第 25 行:你通过一个整流激活函数(也叫 ReLU整流线性单元))将信号从输入层前向传播到隐藏层并激活信号。这个过程分为两步。首先,通过调用第一个全连接层 self.fc1,并将输入状态向量 state 作为输入,完成从输入层到隐藏层的前向传播:self.fc1(state)

这将返回隐藏层。然后我们调用 relu 函数,将该隐藏层作为输入,以以下方式打破信号的线性关系:

图 8:整流激活函数

ReLU 层的目的是通过在全连接层上创建非线性操作来打破线性关系。你希望实现这一点,因为你在解决的是一个非线性问题。最后,F.relu(self.fc1(state)) 返回 x,即带有非线性信号的隐藏层。

第 26 行:你将信号从隐藏层前向传播到包含 Q 值的输出层。和上一行一样,这也是通过调用第二个全连接层 self.fc2,并将隐藏层 x 作为输入来完成的:self.fc2(x)。这将返回 Q 值,命名为 q_values。这里不需要激活函数,因为你稍后将在另一个类中使用 Softmax 选择要执行的动作。

第 27 行:最后,forward 方法返回 Q 值。

让我们来看一看你刚刚创建的内容!

图 9:我们 AI 的神经网络(大脑)

self.fc1输入层隐藏层 之间所有的蓝色连接线。

self.fc2隐藏层输出层 之间所有的蓝色连接线。

这应该能帮助你更好地可视化完整的连接。干得不错!

第 3 步 – 实现经验回放

是时候进入下一步了!你现在将构建另一个类,来构建用于经验回放的内存对象(如 第五章 所示,你的第一个 AI 模型 - 小心强盗!)。首先,我们来看一下代码,然后我将逐行解释 deep_q_learning.py 文件中的所有内容。

# Implementing Experience Replay   #29
#30
class ReplayMemory(object):   #31
    #32
    def __init__(self, capacity):   #33
        self.capacity = capacity   #34
        self.memory = []   #35
    #36
    def push(self, event):   #37
        self.memory.append(event)   #38
        if len(self.memory) > self.capacity:   #39
            del self.memory[0]   #40
    #41
    def sample(self, batch_size):   #42
        samples = zip(*random.sample(self.memory, batch_size))   #43
        return map(lambda x: Variable(torch.cat(x, 0)), samples)   #44 

第 31 行:你引入了 ReplayMemory 类。这次你不需要从任何其他类继承,因此只需在类的括号中输入 object 即可。

第 33 行:和往常一样,你从 __init__() 方法开始,该方法只接受两个参数:self,对象本身,以及 capacity,内存的最大容量。

第 34 行:你引入了第一个对象变量self.capacity,并将其设置为参数capacity,该参数将在稍后创建类的对象时传入。

第 35 行:你引入了第二个对象变量self.memory,并将其初始化为空列表。

第 37 行:你开始构建类的第一个工具——push 方法,该方法以一个过渡作为输入并将其添加到记忆中。然而,如果添加该过渡会超出记忆的容量,push 方法还会删除记忆中的第一个元素。你看到的event参数是要添加的过渡。

第 38 行:使用append函数,你将过渡添加到记忆中。

第 39 行:你开始了一个if条件语句,用来检查记忆的长度(即它的过渡数量)是否大于容量。

第 40 行:如果真是这样,你会删除记忆中的第一个元素。

第 42 行:你开始构建类的第二个工具——sample方法,该方法从经验回放记忆中抽取一些随机过渡。它以batch_size为输入,表示用于训练神经网络的过渡批次大小。

记住它是如何工作的:你不是将单一输入状态正向传播到神经网络并在每次由输入状态导致的过渡后更新权重,而是正向传播小批量的输入状态,并在反向传播相同的整个批次的过渡后通过小批量梯度下降更新权重。这与随机梯度下降(每次输入时更新权重)和批量梯度下降(每批输入时更新权重)不同,正如在第九章与人工大脑同行 – 深度 Q 学习中解释的那样:

图 10:批量梯度下降与随机梯度下降

第 43 行:你从记忆中随机抽取一些过渡,并将它们放入一个大小为batch_size的批次中。例如,如果batch_size = 100,你将从记忆中抽取 100 个随机过渡。抽取过程使用随机库中的sample()函数完成。然后,zip(*list)被用来将状态、动作和奖励重新分组为相同大小的独立批次(batch_size),以便将抽取的过渡格式化为 PyTorch 所期望的格式(即接下来在第 44 行的Variable格式)。

现在可能是个不错的时机,退一步来看一下第 43 行的内容:

图 11:最后状态、动作、奖励和下一状态的批次

第 44 行:使用map()函数,将每个样本包装成一个torch Variable对象(因为Variable()实际上是一个类),这样样本中的每个张量都会与梯度关联。简单来说,torch Variable可以看作是一个包含张量和梯度的高级结构。

这就是 PyTorch 的魅力所在。这些 torch Variables 都位于一个动态计算图中,这使得我们能够快速计算复杂函数的梯度。这样的快速计算是反向传播过程中通过小批量梯度下降更新权重所必需的。在 Variable 类内部,我们看到 torch.cat(x,0)。这只是一个拼接技巧,沿着垂直轴将样本格式化为 Variable 类所期望的格式。

需要记住的最重要的事情是:在使用 PyTorch 训练神经网络时,我们始终使用torch Variables,而不仅仅是张量。你可以在 PyTorch 文档中找到更多相关细节。

第 4 步 – 实现深度 Q 学习

你成功了!你终于要开始编写整个深度 Q 学习的代码了。再次强调,你将把它全部封装成一个类,这次叫做 Dqn,也就是深度 Q 网络。这是你在终点线之前的最后一次冲刺。让我们加油,冲刺到底!

这一次,类比较长,所以我将逐行展示并解释来自 deep_q_learning.py 文件的代码,按方法来逐一说明。这是第一行,__init__() 方法:

# Implementing Deep Q-Learning   #46
#47
class Dqn(object):   #48
    #49
    def __init__(self, input_size, nb_action, gamma):   #50
        self.gamma = gamma   #51
        self.model = Network(input_size, nb_action)   #52
        self.memory = ReplayMemory(capacity = 100000)   #53
        self.optimizer = optim.Adam(params = self.model.parameters())   #54
        self.last_state = torch.Tensor(input_size).unsqueeze(0)   #55
        self.last_action = 0   #56
        self.last_reward = 0   #57 

第 48 行:你引入了 Dqn 类。你不需要从其他类继承,因此只需在类的括号中输入 object

第 50 行:和往常一样,你从 __init__() 方法开始,这次它有四个参数:

  1. self:该对象

  2. input_size:输入状态向量中的输入数目(即,4)

  3. nb_action:动作数目(即,3)

  4. gamma:时序差分公式中的折扣因子

第 51 行:你引入了第一个对象变量 self.gamma,并将其设置为 gamma 参数的值(该值将在稍后创建 Dqn 类对象时输入)。

第 52 行:你引入了第二个对象变量 self.model,它是你之前构建的 Network 类的一个对象。这个对象就是你的神经网络;换句话说,就是我们 AI 的“大脑”。在创建这个对象时,你需要输入 Network 类中的 __init__() 方法的两个参数,分别是 input_sizenb_action。稍后,你会在创建 Dqn 类的对象时输入它们的实际值(分别是 43)。

第 53 行:你引入了第三个对象变量 self.memory,它是你之前构建的 ReplayMemory 类的一个对象。这个对象是经验回放内存。由于 ReplayMemory 类的 __init__ 方法只需要一个参数 capacity,所以你在这里输入了 100,000。换句话说,你创建了一个大小为 100,000 的内存,这意味着 AI 将记住最近的 100,000 次过渡,而不仅仅是最后一个。

第 54 行:你引入了第四个对象变量self.optimizer,它是Adam类的一个对象,Adam类是torch.optim模块中现有的类。这个对象是优化器,通过小批量梯度下降在反向传播过程中更新权重。在参数中,保持大部分默认值(你可以在 PyTorch 文档中查看),并且只输入模型参数(params参数),这些参数通过self.model.parameters访问,这是nn.Module类的一个属性,而Network类继承了这个类。

第 55 行:你引入了第五个对象变量self.last_state,它将成为每次(最后状态,动作,奖励,下一个状态)转变中的最后一个状态。这个最后状态被初始化为Tensor类的一个对象,该类来自torch库,初始化时只需要输入input_size参数。然后,使用.unsqueeze(0)在索引 0 处创建一个额外的维度,这个维度将对应于批次。这样我们可以像下面这样做,将每个最后状态与相应的批次匹配:

图 12:为批次添加一个维度

第 56 行:你引入了第六个对象变量self.last_action,其初始值为0,表示每次迭代时执行的最后一个动作。

第 57 行:我们引入了最后一个对象变量self.last_reward,其初始值为0,表示在上一次执行动作self.last_action后获得的最后奖励,该奖励发生在最后一个状态self.last_state中。

现在,你已经准备好__init__方法了。让我们继续进入下一个代码部分,并讨论下一个方法:select_action方法,它使用 Softmax 选择每次迭代时要执行的动作。

 def select_action(self, state):   #59
        probs = F.softmax(self.model(Variable(state))*100)   #60
        action = probs.multinomial(len(probs))   #61
        return action.data[0,0]   #62 

第 59 行:你开始定义select_action方法,它接收一个输入状态向量(方向、信号 1、信号 2、信号 3)作为输入,并返回作为输出的选择动作。

第 60 行:你通过torch.nn.functional模块中的 Softmax 函数获得三个动作的概率。这个 Softmax 函数以 Q 值作为输入,这些 Q 值正是通过self.model(Variable(state))返回的。记住,self.modelNetwork类的一个对象,而该类具有forward方法,forward方法接收一个输入状态张量,该张量被封装在torchVariable中,并返回三个动作的 Q 值作为输出。

极客笔记:通常我们会指定以这种方式调用forward方法——self.model.forward(Variable(state))——但由于forwardNetwork类的唯一方法,直接调用self.model就足够了。

softmax内将 Q 值乘以一个数(这里是100)是一个值得记住的技巧:它可以调节探索与开发之间的平衡。这个数字越小,你的探索就越多,因此优化动作的时间也会更长。在这里,问题并不复杂,所以选择一个较大的数字(100),以便做出更自信的动作,且路径更加平滑。若你移除*100,你会明显看到不同。简单来说,带上*100,你会看到一辆车很有自信;没有*100,你会看到一辆车在焦躁不安。

第 61 行:你从第 60 行的softmax函数创建的动作分布中随机抽取,通过调用multinomial()函数从概率probs中获取。

第 62 行:你返回要执行的选定操作,你可以通过action.data[0,0]来访问该操作。返回的action具有高级的张量结构,操作索引(0, 1 或 2)位于操作张量的data属性中,索引[0,0]的第一个单元格。

让我们继续进入下一个代码部分,learn方法。这个方法非常有趣,因为它是深度 Q 学习的核心所在。正是在这个方法中,我们计算时序差分(temporal difference),进而计算损失,并使用优化器更新权重,以减少损失。这就是为什么这个方法叫做learn,因为正是在这里,AI 学会了执行越来越好的动作,进而提高累计的奖励。我们继续:

 def learn(self, batch_states, batch_actions, batch_rewards, batch_next_states):   #64
        batch_outputs = self.model(batch_states).gather(1, batch_actions.unsqueeze(1)).squeeze(1)   #65
        batch_next_outputs = self.model(batch_next_states).detach().max(1)[0]   #66
        batch_targets = batch_rewards + self.gamma * batch_next_outputs   #67
        td_loss = F.smooth_l1_loss(batch_outputs, batch_targets)   #68
        self.optimizer.zero_grad()   #69
        td_loss.backward()   #70
        self.optimizer.step()   #71 

第 64 行:你首先定义了learn()方法,该方法接受四个元素组成的转移批次作为输入(输入状态、动作、奖励、下一个状态):

  1. batch_states:输入状态的批次。

  2. batch_actions:一批执行的动作。

  3. batch_rewards:收到的一批奖励。

  4. batch_next_states:到达的下一个状态批次。

在我解释第 65、66 和 67 行之前,让我们先回顾一下你需要做的事情。如你所知,learn方法的目标是通过每次训练迭代来更新权重,以减少反向传播的损失。首先,让我们提醒一下损失的公式:

在损失公式中,我们清晰地识别出了输出(预测的 Q 值)和目标:

因此,为了计算损失,你将按如下方式处理接下来的四行代码:

第 65 行:你收集输出批次,

第 66 行:你计算目标的部分,称其为batch_next_outputs

第 67 行:你获取目标的批次。

第 68 行:因为你已经有了输出和目标,现在可以计算损失了。

现在让我们详细讨论一下。

第 65 行:你收集了输出的批次 ,即输入状态和在批次中执行的动作的预测 Q 值。获取它们需要几个步骤。首先,你调用self.model(batch_states),如第 60 行所见,它返回每个输入状态在batch_states中的 Q 值,以及三个动作 0、1 和 2 的 Q 值。为了帮助你更好地可视化,它返回类似这样的结果:

图 13:self.model(batch_states)返回的内容

你只想要来自输出批次中选择动作的预测 Q 值,这些 Q 值位于动作批次batch_actions中。这正是.gather(1, batch_actions.unsqueeze(1)).squeeze(1)技巧所做的:对于批次中的每个输入状态,它会选择与批次中的所选动作相对应的 Q 值。为了帮助更好地可视化,假设动作批次如下:

图 14:动作批次

然后,你将得到以下由红色 Q 值组成的输出批次:

图 15:输出批次

希望这很清楚;我会尽力避免让你迷失。

第 66 行:现在你得到了目标的 部分。称之为batch_next_outputs;你可以通过两个步骤获得它。首先,调用self.model(batch_next_states)来获取批次下一个状态的每个 Q 值的预测值,以及三个动作的 Q 值。然后,对于批次中的每个下一个状态,使用.detach().max(1)[0]取三个 Q 值中的最大值。这就给你目标的 值部分。

第 67 行:由于你已经有了奖励的批次 (它是参数的一部分),并且由于你刚刚在第 66 行得到了目标的 值部分,那么你就准备好获取目标的批次:

这正是你在第 67 行所做的,通过将batch_rewardsbatch_next_outputs乘以self.gamma(这是Dqn类中的一个对象变量)相加。现在,你已经有了输出批次和目标批次,因此你可以准备好计算损失了。

第 68 行:让我们回顾一下损失的公式:

因此,为了计算损失,你只需要计算目标和输出批次之间平方差的总和。这正是smooth_l1_loss函数所做的。它来自torch.nn.functional模块,接受输出批次和目标批次作为输入,并根据前面的公式返回损失。在代码中,将此损失称为td_loss,即时序差分损失

出色的进展!现在你有了损失,表示预测与目标之间的误差,你准备通过反向传播这个损失到神经网络,并通过小批量梯度下降来更新权重,从而减少这个损失。接下来的步骤是使用你的优化器,它将执行权重更新。

第 69 行:你首先初始化梯度,通过调用self.optimizer对象的zero_grad()方法(zero_gradAdam类的方法),它基本上会将所有权重的梯度设置为零。

第 70 行:你通过调用td_lossbackward()函数将损失误差td_loss反向传播到神经网络中。

第 71 行:你通过调用self.optimizer对象的step()方法来执行权重更新(stepAdam类的方法)。

恭喜你!你已经在Dqn类中构建了一个工具,可以训练你的汽车开得更好。你已经完成了最难的部分。现在你剩下的工作就是将这些工作整合到一个最终的方法中,叫做update,它将在达到新状态后更新权重。

如果你在想,“那不就是我已经在learn方法中做的事吗?”,嗯,你是对的;但你需要创建一个额外的函数,在适当的时机更新权重。更新权重的适当时机是在我们的 AI 到达新状态后。简而言之,接下来你要实现的update方法将把learn方法和动态环境联系起来。

这就是终点!你准备好了吗?下面是代码:

 def update(self, new_state, new_reward):   #73
        new_state = torch.Tensor(new_state).float().unsqueeze(0)   #74
        self.memory.push((self.last_state, torch.LongTensor([int(self.last_action)]), torch.Tensor([self.last_reward]), new_state))   #75
        new_action = self.select_action(new_state)   #76
        if len(self.memory.memory) > 100:   #77
            batch_states, batch_actions, batch_rewards, batch_next_states = self.memory.sample(100)   #78
            self.learn(batch_states, batch_actions, batch_rewards, batch_next_states)   #79
        self.last_state = new_state   #80
        self.last_action = new_action   #81
        self.last_reward = new_reward   #82
        return new_action   #83 

第 73 行:你引入了update()方法,该方法的输入是刚执行一个动作后所到达的新状态以及新获得的奖励。此处输入的新状态将是你在map.py文件第 129 行看到的state变量,而新奖励将是你在map.py文件第 138 到 145 行看到的reward变量。这个update方法执行一些操作,包括权重更新,最后返回需要执行的新动作。

第 74 行:你首先将新状态转换为 torch 张量,并通过 unsqueeze 操作为其创建一个额外的维度(放在索引 0 的位置),该维度对应于批次。为了方便以后的操作,你还确保新状态的所有元素(方向和三个信号)都被转换为浮动数值,通过添加.float()

第 75 行:使用你内存对象的push()方法,向内存中添加一个新的过渡。这个新的过渡由以下部分组成:

  1. self.last_state:到达新状态之前的最后一个状态

  2. self.last_action:导致到达新状态的最后一个动作

  3. self.last_reward:执行最后一个动作后获得的最后一个奖励

  4. new_state:刚刚到达的新状态

这个新过渡的所有元素都被转换为 torch 张量。

第 76 行:使用Dqn类中的select_action()方法,从刚到达的新状态执行一个新动作。

第 77 行:检查内存大小是否大于 100。在self.memory.memory中,第一个memory是第 53 行创建的对象,第二个memory是第 35 行引入的变量对象。

第 78 行:如果是这种情况,从内存中采样 100 个过渡,使用self.memory对象中的sample()方法。这将返回四个大小为 100 的批次:

  1. batch_states:当前状态的批次(在过渡时刻的状态)。

  2. batch_actions:在当前状态下执行的动作批次。

  3. batch_rewards:在batch_states的当前状态下执行batch_actions动作后获得的奖励批次。

  4. batch_next_states:执行batch_actions动作后,在batch_states的当前状态下到达的下一个状态批次。

第 79 行:仍在if条件中,使用learn()方法更新权重,该方法从同一个Dqn类中调用,并以四个先前的批次作为输入。

第 80 行:更新最后到达的状态self.last_state,并将其设为new_state

第 81 行:更新最后执行的动作self.last_action,并将其设为new_action

第 82 行:更新最后获得的奖励self.last_reward,并将其设为new_reward

第 83 行:返回执行的新动作。

这就是update()方法的全部!希望你能看到我们是如何将各个部分连接起来的。现在,为了更好地连接这些点,让我们看看你在map.py文件中如何以及在哪里调用update方法。

首先,在调用update()方法之前,你必须创建一个Dqn类的对象,这里称之为brain。这正是你在map.py文件第 33 行做的事情。

brain = Dqn(4,3,0.9)   #33 

这里输入的参数是我们在Dqn类的__init__()方法中看到的三个参数:

  • 4是输入状态中的元素数量(input_size)。

  • 3是可能的动作数量(nb_action)。

  • 0.9是折扣因子(gamma)。

然后,从这个brain对象中,你在map.py文件第 130 行调用update()方法,紧接着到达一个新的状态,该状态在代码中被称为state

 state = [orientation, self.car.signal1, self.car.signal2, self.car.signal3]   #129
        action = brain.update(state, reward)   #130 

回到你的Dqn类,你需要两个额外的方法:

  1. save()方法,保存 AI 网络权重的方法,保存的是它们最后一次更新后的权重。每当你在运行地图时点击保存按钮时,都会调用这个方法。然后,你的 AI 权重将被保存并放入一个名为last_brain.pth的文件中,该文件将自动存储在包含你的 Python 文件的文件夹中。这就让你能够拥有一个预训练的 AI。

  2. load()方法,加载保存的last_brain.pth文件中的权重。当你在运行地图时点击加载按钮时,它会调用这个方法。它使你能够以一个预训练的自动驾驶汽车开始地图,而无需等待训练。

这最后两种方法与 AI 无关,因此我们不会花时间解释每一行代码。不过,如果你以后想用它们来构建其他 AI 模型,能认识这两种工具还是挺有用的。

它们是这样实现的:

 def save(self):   #85
        torch.save({'state_dict': self.model.state_dict(),   #86
                    'optimizer' : self.optimizer.state_dict(),   #87
                   }, 'last_brain.pth')   #88
    #89
    def load(self):   #90
        if os.path.isfile('last_brain.pth'):   #91
            print("=> loading checkpoint... ")   #92
            checkpoint = torch.load('last_brain.pth')   #93
            self.model.load_state_dict(checkpoint['state_dict'])   #94
            self.optimizer.load_state_dict(checkpoint['optimizer'])   #95
            print("done !")   #96
        else:   #97
            print("no checkpoint found...")   #98 

恭喜!

没错!你已经完成了我们自驾车中的 AI 代码实现,总共 100 行代码。这是一个相当了不起的成就,尤其是在第一次编写深度 Q 学习时。你真的可以为自己走到这一步感到自豪。

在经历了这一切的辛勤工作后,你完全值得享受一些乐趣,而我认为最有趣的就是看到你辛勤工作的成果。换句话说,你即将看到你的自驾车开始运行!我记得第一次运行这个程序时,我是多么兴奋。你也会有这种感觉,真的很酷!

演示

我有一些好消息和一些坏消息。

我先说坏消息:我们不能通过简单的即插即用方式在 Google Colab 上运行 map.py 文件。原因是 Kivy 在 Colab 上的安装非常棘手。所以,我们将采用经典的方式运行 Python 文件:通过终端。

好消息是,一旦通过终端安装了 Kivy 和 PyTorch,你将拥有一个精彩的演示!

让我们安装运行自驾车所需的一切。以下是我们需要按顺序安装的内容:

  1. Anaconda:一个免费的开源 Python 发行版,通过 conda 命令提供了一种简便的方式来安装包。我们将用它来安装 PyTorch 和 Kivy。

  2. Python 3.6 的虚拟环境:Anaconda 默认安装 Python 3.7 或更高版本;然而,3.7 版本与 Kivy 不兼容。我们将创建一个虚拟环境,在其中安装 Python 3.6,这是与 Kivy 以及我们的实现兼容的版本。如果这听起来有点吓人,不用担心,我会提供所有需要的细节,帮助你完成设置。

  3. PyTorch:然后,在虚拟环境中,我们将安装 PyTorch,这个用于构建深度 Q 网络的 AI 框架。我们将安装一个与我们的实现兼容的 PyTorch 特定版本,以确保每个人都能顺利运行,不会出现问题。PyTorch 升级有时会更改模块的名称,这可能会导致旧的实现与最新版本的 PyTorch 不兼容。在这里,我们确保安装了与我们的实现兼容的正确版本。

  4. Kivy:最后,仍然在虚拟环境中,我们将安装 Kivy,这个开源 Python 框架,我们将在其上运行我们的地图。

我们先从 Anaconda 开始。

安装 Anaconda

在 Google 或你喜欢的浏览器中,访问 www.anaconda.com。在 Anaconda 网站上,点击屏幕右上角的 Download 按钮。向下滚动,你会看到可供下载的 Python 版本:

图 16:安装 Anaconda – 第 2 步

在顶部,确保你的系统(Windows、macOS 或 Linux)已正确选择。如果是,点击 Python 3.7 版本框中的 Download 按钮。这将下载带有 Python 3.7 的 Anaconda。

然后双击下载的文件,并不断点击 ContinueAgree 进行安装,直到安装完成。如果提示你选择安装对象,选择 仅为我安装

使用 Python 3.6 创建虚拟环境

现在 Anaconda 已安装,你可以创建一个名为 selfdrivingcar 的虚拟环境,并安装 Python 3.6。为此,你需要打开终端并输入一些命令。以下是三种系统的打开方法:

  1. 对于 Linux 用户,只需按 Ctrl + Alt + T

  2. 对于 Mac 用户,按 Cmd + Space,然后在聚焦搜索中输入 Terminal

  3. 对于 Windows 用户,点击屏幕左下角的 Windows 按钮,在程序列表中找到 anaconda,点击打开 Anaconda 提示符。一个黑色窗口会打开;这就是你用来安装包的终端。

在终端中输入以下命令:

conda create -n selfdrivingcar python=3.6 

就是这样:

这个命令创建了一个名为 selfdrivingcar 的虚拟环境,并安装了 Python 3.6 和其他包。

按下 Enter 后,几秒钟内你会看到这个:

y 继续。这将下载并解压包。几秒钟后,你会看到这个,这标志着安装完成:

接下来,我们将激活 selfdrivingcar 虚拟环境,意味着我们将进入该环境,以便在 selfdrivingcar 虚拟环境中安装 PyTorch 和 Kivy。

如你所见,之前为了激活环境,我们将输入以下命令:

conda activate selfdrivingcar 

输入该命令后,你将进入虚拟环境:

现在我们可以看到在我的电脑名称 hadelins-macbook-pro 前面出现 (selfdrivingcar),这意味着我们已经进入了 selfdrivingcar 虚拟环境。

我们已经准备好进行下一步,即在这个虚拟环境中安装 PyTorch 和 Kivy。不要关闭你的终端,否则当你重新打开时,你将回到主环境。

安装 PyTorch

现在,我们将通过输入以下命令在虚拟环境中安装 PyTorch:

conda install pytorch==0.3.1 -c pytorch 

就是这样:

几秒钟后,我们得到了这个:

再次按下 y,然后按 Enter 键。

几秒钟后,PyTorch 已安装:

安装 Kivy

现在我们继续安装 Kivy。在同一个虚拟环境中,我们将通过输入以下命令来安装 Kivy:

conda install -c conda-forge/label/cf201901 kivy 

再次,我们得到了这个:

再次输入 y,然后再等几秒钟,Kivy 就安装好了。

现在,我有个好消息要告诉你:你已经准备好运行自动驾驶汽车了!为了实现这一点,我们需要在终端中运行我们的代码,仍然是在我们的虚拟环境中。

如果你已经关闭了终端,那么重新打开终端后,输入conda activate selfdrivingcar命令,以便重新进入虚拟环境。

好的,我们来运行代码吧!如果你还没有这样做,先通过点击 GitHub 页面上的Clone or download按钮下载整个仓库:

(github.com/PacktPublishing/AI-Crash-Course)

图 17:GitHub 仓库

然后解压缩文件,并将解压后的文件夹移动到桌面上,像这样:

接下来,进入Chapter 10并选择并复制其中的所有文件:

然后,由于我们现在只对这些文件感兴趣,并且为了简化终端中的命令行,将这些文件粘贴到主AI-Crash-Course-master文件夹内,并删除其他不需要的文件,最终你会得到如下结果:

现在,我们将通过终端访问这个文件夹。由于我们已经将仓库文件夹放在桌面上,所以很容易找到它。再次回到终端,输入ls(l 代表 lion)查看当前所在的文件夹:

我可以看到我在我的主根文件夹中,这个文件夹包含了Desktop文件夹。通常你也应该能看到这个。所以现在我们进入Desktop文件夹,输入以下命令:

cd Desktop 

再次输入ls命令,确认你确实看到了AI-Crash-Course-master文件夹:

接下来,通过输入以下命令进入AI-Crash-Course-master文件夹:

cd AI-Crash-Course-master 

完美!现在我们已经到了正确的位置!再次输入ls命令,你可以看到仓库中的所有文件,包括map.py文件,这是我们要运行的文件,用来查看我们的自动驾驶汽车实际运行!

如果你遇到任何困难,无法达到这一点,可能是因为你的主根目录中没有包含Desktop文件夹。如果是这种情况,只需将AI-Crash-Course-master仓库文件夹放入你在终端中输入ls命令时看到的文件夹中,然后重新执行相同的步骤。

你需要做的就是找到并进入AI-Crash-Course-master文件夹,使用cd命令。就这样!不要忘记确保你的AI-Crash-Course-master文件夹只包含自动驾驶汽车相关的文件:

现在,你只差一条命令就能运行你的自动驾驶汽车了。我希望你已经迫不及待地想看到自己努力的成果了;我完全能理解你的心情,毕竟不久前我也是这个样子!

那么,废话少说,我们现在就输入最终命令。这是:

python map.py 

一进入后,带有汽车的地图就会像这样弹出:

图 18:地图

在最初的一分钟左右,你的自动驾驶汽车会通过执行一些无意义的动作来探索自己的行为;你可能会看到它在旋转。每进行 100 次动作,AI 神经网络中的权重会更新,汽车会改进其动作以获得更高的奖励。突然,也许在再过 30 秒左右,你应该能看到汽车在机场和市区之间进行往返,这里我再次标出了:

图 19:目的地

现在来点乐趣吧!在地图上画些障碍物,看看汽车是否能避开它们。

我这一侧刚画了这个,经过几分钟的训练后,我能清晰地看到汽车避开了障碍物:

图 20:有障碍物的道路

你还能玩得更开心!例如,可以像这样画一条路:

图 21:演示的道路

几分钟的训练后,汽车能够沿着那条路自驾行驶,并且在机场和市区之间进行多次往返。

快问你个问题:你是如何编程让汽车在目的地之间行驶的?

你是通过在汽车接近目标时给予 AI 一个小的正奖励来实现的。这个程序代码写在map.py文件的第 144 和 145 行:

 if distance < last_distance:   #144
                reward = 0.1   #145 

恭喜你完成了这一章关于这个不那么基础的自动驾驶应用!希望你玩得开心,也为能够掌握深度强化学习中的这样一个先进模型而感到自豪。

总结

在这一章中,我们学习了如何构建一个深度 Q 学习模型来驾驶自动驾驶汽车。它的输入是来自三个传感器的信息以及当前的方向。输出是决定直行、左转或右转的 Q 值。至于奖励,我们对撞到沙地的情况给予严重惩罚,对走错方向的情况轻微惩罚,对走正确方向的情况略微奖励。我们使用 PyTorch 实现了这个 AI,并使用 Kivy 进行图形展示。为了运行这一切,我们使用了 Anaconda 环境。

现在,休息一下吧,你应该值得拥有!我们将在下一章继续我们的 AI 挑战,这一次我们将解决一个实际的商业问题,涉及到数百万的成本。

第十一章:AI 在商业中的应用 – 通过深度 Q 学习最小化成本

你能用深度 Q 学习模型来构建一辆自动驾驶汽车真是太棒了。真的,再次祝贺你。但我也希望你能用深度 Q 学习来解决现实世界的商业问题。通过这个下一个应用,你将完全准备好通过利用 AI 为你的工作或商业增加价值。尽管我们再次使用了一个具体应用,但这一章将为你提供一个通用的 AI 框架,一个包含你在用深度 Q 学习解决现实世界问题时必须遵循的一般步骤的蓝图。本章对你和你的职业生涯都非常重要;我不希望你在掌握这里学到的技能之前就把这本书合上。让我们一起冲破这个下一个应用吧!

要解决的问题

当我说我们要解决一个现实世界的商业问题时,我并没有夸大其词;我们将通过深度 Q 学习来解决的问题与以下问题非常相似,且该问题已经在现实世界中通过深度 Q 学习得以解决。

在 2016 年,DeepMind AI 通过使用其 DQN AI 模型(深度 Q 学习)将 Google 数据中心的冷却费用减少了 40%,从而大大减少了 Google 的年度开支。请查看此链接:

deepmind.com/blog/deepmind-ai-reduces-google-data-centre-cooling-bill-40

在这个案例研究中,我们将做类似的事情。我们将搭建自己的服务器环境,并且构建一个 AI 来控制服务器的冷却和加热,使其保持在最佳的温度范围内,同时尽可能节省能源,从而最小化成本。

就像 DeepMind AI 所做的那样,我们的目标将是至少实现 40%的能源节省!你准备好了吗?让我们开始吧!

一如既往,我对你的第一个问题是:我们的第一步是什么?

我相信到此时,我不需要再拼写出答案了。让我们直接开始搭建环境吧!

搭建环境

在定义状态、动作和奖励之前,我们需要搭建服务器并解释其操作方式。我们将分几步完成:

  1. 首先,我们将列出所有控制服务器的环境参数和变量。

  2. 之后,我们将设定问题的基本假设,你的 AI 将依赖这些假设来提供解决方案。

  3. 然后,我们将指定如何模拟整个过程。

  4. 最后,我们将解释服务器的整体工作原理,以及 AI 如何发挥作用。

服务器环境的参数和变量

这是服务器环境中所有固定值参数的列表:

  1. 每个月的平均大气温度。

  2. 服务器的最佳温度范围,我们将其设置为

  3. 最低温度,低于该温度服务器将无法正常运行,我们将其设置为

  4. 服务器无法正常运行的最高温度,我们将其设置为!

  5. 服务器的最小用户数量,我们将其设置为 10。

  6. 服务器的最大用户数量,我们将其设置为 100。

  7. 服务器每分钟用户数量的最大变化值,我们将其设置为 5;因此每分钟,服务器的用户数量最多只能变化增加 5 或减少 5。

  8. 服务器的最小数据传输速率,我们将其设置为 20。

  9. 服务器的最大数据传输速率,我们将其设置为 300。

  10. 数据传输速率每分钟的最大变化值,我们将其设置为 10;因此每分钟,数据传输速率只能在任一方向上变化最多 10。

接下来,我们将列出服务器环境中所有的变量,这些变量的值会随时间波动:

  1. 给定时刻服务器的温度。

  2. 给定时刻连接到服务器的用户数量。

  3. 给定时刻的数据传输速率。

  4. AI 在给定时刻向服务器所消耗的能量(用于冷却或加热)。

  5. 服务器的集成冷却系统所消耗的能量,以自动将服务器温度恢复到最佳范围内,每当服务器温度超出这个最佳范围时。此举旨在跟踪非 AI系统所使用的能量,以便与我们的 AI 系统进行比较。

所有这些参数和变量都将成为环境的一部分,并将影响我们 AI 的行为。

接下来,我们将解释环境的两个核心假设。需要理解的是,这些假设与 AI 无关,而是用来简化环境,使我们能够专注于创建一个功能性 AI 解决方案。

服务器环境的假设

我们将依赖以下两个关键假设:

假设 1 – 我们可以近似服务器温度

服务器的温度可以通过多元线性回归来近似,即通过大气温度、用户数量和数据传输速率的线性函数来近似,如下所示:

服务器温度 = + 大气温度 + 用户数量 + 数据传输速率

其中!、!、! 和!

这个假设的存在意义以及为什么 ,和 是直观易懂的原因。可以理解的是,当大气温度升高时,服务器的温度也会升高。连接到服务器的用户越多,服务器需要花费更多的能量来处理它们,因此服务器的温度会更高。最后,服务器内部传输的数据越多,服务器需要花费更多的能量来处理这些数据,因此服务器的温度也会更高。

为了简化起见,我们可以假设这些相关性是线性的。然而,您完全可以假设它们是二次或对数的,并相应地修改代码来反映这些方程式。这只是我对虚拟服务器环境的模拟;您可以根据需要进行调整!

假设在执行此多元线性回归后,我们得到了以下系数值:,以及。因此:

服务器温度 = 大气温度 + 用户数量 + 数据传输速率

现在,如果我们在现实生活中面对这个问题,我们可以获取服务器温度的数据集,并直接计算这些值。在这里,我们只是假设一些易于编码和理解的值,因为本章的目标不是完美地模拟一个真实服务器,而是通过人工智能的步骤解决一个现实世界的问题。

假设 2 – 我们可以近似计算能源成本

任何冷却系统(无论是我们的 AI 还是我们与之对比的服务器集成冷却系统)在 1 个时间单位内(在我们这里是 1 分钟)将服务器温度从 变化到 所消耗的能量,可以通过回归近似为一个服务器绝对温度变化的线性函数,如下所示:

其中:

  1. 是系统在时间 分钟之间消耗的能量。

  2. 是由系统引起的服务器温度变化,在时间 分钟之间。

  3. 是时间 分钟时服务器的温度。

  4. 是在时间 分钟时服务器的温度。

让我们用来解释为什么这个假设在直观上是合理的。这仅仅是因为,AI 或传统的集成冷却系统越是加热或冷却服务器,它所花费的能量越多,才能实现那个热量转移。

例如,假设服务器突然出现过热问题,温度已经达到了C;那么在一个时间单位(1 分钟)内,任何系统都需要更多的能量才能将服务器的温度从过高温度恢复到其最佳温度C,而不是将其恢复到C。

为了简化起见,在这个例子中我们假设这些相关性是线性的,而不是从真实数据集中计算出实际值。如果你在想为什么我们取绝对值,那是因为当 AI 冷却服务器时,,因此!。由于能量消耗总是正数,我们必须取的绝对值。

牢记我们想要的简化假设,我们假设回归结果为,因此我们根据假设 2得出以下最终方程:

因此:

,也就是说,如果服务器被加热,

,也就是说,如果服务器被冷却。

现在我们已经覆盖了假设,接下来让我们解释如何模拟服务器的运行,用户登录与退出,以及数据的进出。

仿真

用户数量和数据传输速率将会随机波动,以模拟实际服务器中不可预测的用户活动和数据需求。这导致了温度的随机性。AI 需要学习它应该向服务器传输多少冷却或加热能量,以避免服务器性能恶化,同时通过优化热量转移来尽可能减少能量消耗。

现在我们已经有了完整的图景,接下来我将解释在这个环境下服务器和 AI 的整体功能。

整体功能

在数据中心内,我们处理的是一个特定的服务器,这个服务器由之前列出的参数和变量控制。每分钟,都会有一些新用户登录到服务器,也会有一些当前用户退出服务器,因此更新服务器中活动用户的数量。同时,每分钟会有一些新的数据传输到服务器,也会有一些现有数据从服务器传出,从而更新服务器内部的数据传输速率。

因此,基于之前给出的假设 1,服务器的温度每分钟更新一次。现在请集中注意力,因为在这里你将理解到 AI 在服务器中所扮演的重要角色。

有两种可能的系统可以调节服务器的温度:AI 或服务器的集成冷却系统。服务器的集成冷却系统是一个没有智能的系统,会自动将服务器的温度带回其最佳温度范围。

每分钟,服务器的温度会被更新。如果服务器使用集成冷却系统,该系统会观察发生了什么;这个更新可能会使温度保持在最佳温度范围内(),或者将其推移到这个范围之外。如果它超出了最佳范围,例如达到C,服务器的集成冷却系统会自动将温度带回最佳范围的最近边界,在此案例中是C。为了我们的模拟假设,不管温度变化有多大,我们假设集成冷却系统可以在不到一分钟的时间内将其恢复到最佳范围。这显然是一个不现实的假设,但本章的目的是让你构建一个能解决问题的功能性 AI,而不是完美地模拟真实服务器的热力学。一旦我们一起完成这个示例,我强烈建议你修改代码并尝试让它更现实;目前,为了简化问题,我们将相信这个神奇有效的集成冷却系统。

如果服务器改为使用 AI,那么服务器的集成冷却系统将被禁用,由 AI 本身来更新服务器的温度,以最佳方式进行调节。AI 在进行一些先前预测后改变温度,而不是像没有智能的集成冷却系统那样以完全确定的方式改变温度。在更新用户数量和数据传输速率之前,AI 会预测是否应该降温、什么都不做,或者加热服务器,并进行相应的操作。然后温度变化发生,AI 会重新迭代。

由于这两个系统彼此独立,我们可以单独评估它们以比较性能;训练或运行 AI 时,我们可以跟踪在相同情况下集成冷却系统所消耗的能量。

这就引出了能量问题。记住,AI 的一个主要目标是降低运行服务器的能量成本。因此,我们的 AI 必须尽量使用比没有智能的冷却系统在服务器上使用的能量更少。由于根据前面的假设 2,服务器上花费的能量(无论是哪个系统)与单位时间内的温度变化成正比:

因此:

,也就是说,如果服务器加热,

,也就是说,如果服务器降温,

那么这意味着 AI 在每次迭代 (每分钟)节省的能量等于由非智能服务器的集成冷却系统与 AI 之间在服务器中造成的温度绝对变化的差异,从

AI 在 之间节省的能量

其中:

  1. 是服务器集成冷却系统在迭代 期间在服务器中造成的温度变化,即从 分钟。

  2. 是 AI 在迭代 期间在服务器中造成的温度变化,即从 分钟。

AI 的目标是每分钟尽可能节省能源,从而在 1 年的模拟中节省最大总能量,并最终帮助企业在冷却/加热电费上节省最大成本。这就是我们在 21 世纪做生意的方式;有了 AI!

现在我们已经完全理解了服务器环境的工作方式,以及它是如何被模拟的,接下来是定义 AI 环境时绝对必须做的事情。你已经知道接下来的步骤:

  1. 定义状态。

  2. 定义动作。

  3. 定义奖励。

定义状态

记住,当你进行深度 Q 学习时,输入状态总是一个 1D 向量。(除非你在进行深度卷积 Q 学习,在这种情况下,输入状态是一个 2D 图像,但那是另一个话题!等到第十二章深度卷积 Q 学习再说)。那么,在这个服务器环境中,输入状态向量是什么?它将包含哪些信息,才能充分描述环境的每一个状态?这些是你在建模 AI 问题并构建环境时必须问自己的问题。试着先自己回答这些问题,找出在这种情况下的输入状态向量,接下来你可以看看我们在下一段中使用了什么。提示:再看一下前面定义的变量。

输入状态 在时间 由以下三个元素组成:

  1. 在时间 服务器的温度

  2. 在时间 服务器中的用户数量

  3. 在时间 服务器中的数据传输速率

因此,输入状态将是这三个元素的输入向量。我们未来的 AI 将以这个向量作为输入,并会在每个时刻返回一个动作 。说到动作,它们将是什么?我们一起来看看。

定义动作

为了确定需要执行哪些动作,我们需要记住目标,即最优调节服务器温度。动作简单来说就是 AI 能够在服务器内部引起的温度变化,用以加热或冷却服务器。在深度 Q 学习中,动作必须是离散的;它们不能从一个范围中选取,我们需要一个明确数量的可能动作。因此,我们将考虑五个可能的温度变化,从C 到C,这样我们就得到了 AI 可以用来调节服务器温度的五个可能动作:

图 1:定义动作

太好了。最后,让我们看看如何对 AI 进行奖励和惩罚。

定义奖励

你可能已经从之前的总体功能部分猜到奖励是什么。在迭代时,奖励是 AI 节省的能量,具体是相对于服务器集成冷却系统本应消耗的能量;也就是说,AI 关闭时非智能冷却系统本应消耗的能量与 AI 为服务器所消耗的能量之间的差异:

由于根据假设 2,所消耗的能量等于服务器中温度变化所引起的能量变化(无论是由任何系统引起,包括 AI 或非智能冷却系统):

因此:

,如果服务器加热,

,如果服务器被冷却,

然后我们在时刻收到奖励,奖励是服务器中温度变化的差异,比较非智能冷却系统(即没有 AI 的情况下)与 AI 的效果:

AI 在之间节省的能量

其中:

  1. 是服务器集成冷却系统在迭代期间对服务器造成的温度变化,即从分钟。

  2. 是 AI 在迭代期间对服务器造成的温度变化,即从分钟。

重要说明:需要理解的是,系统(我们的 AI 和服务器的集成冷却系统)将被单独评估,以计算奖励。由于每个时刻,两个不同系统的行为会导致不同的温度,我们必须分别记录这两个温度,分别为。换句话说,我们正在同时进行两个独立的模拟,跟踪用户和数据的波动;一个是 AI 的,另一个是服务器集成冷却系统的。

为了完成这一部分,我们将做一个小的模拟,模拟 2 次迭代(即 2 分钟),作为示例来使一切更加清晰。

最终模拟示例

假设现在是下午,服务器的温度是℃,无论有无 AI,情况都一样。在这个确切的时刻,AI 会预测一个动作:0、1、2、3 或 4。由于此时服务器的温度已经超出了最佳温度范围,因此 AI 很可能会预测动作 0、1 或 2。假设它预测的是 1,这意味着将服务器温度降至℃。因此,在下午之间,AI 将服务器的温度从调整到

因此,根据假设 2,AI 对服务器消耗的能量为:

现在,计算奖励时只缺少一个信息:如果在下午 4:00 到 4:01 之间 AI 被停用,服务器的集成冷却系统将消耗多少能量。请记住,这个非智能冷却系统会自动将服务器的温度调整到最佳温度范围的最接近边界。由于在下午时,温度为℃,因此当时最佳温度范围的最接近边界为℃。因此,服务器的集成冷却系统将温度从调整到,如果没有 AI,服务器温度的变化为:

根据假设 2,如果没有 AI,非智能冷却系统所消耗的能量为:

总结来说,AI 在下午进行这一动作后获得的奖励为:

我相信你已经注意到,当前我们的 AI 系统并未涉及服务器的最佳温度范围;正如我之前提到的,所有内容都来自于奖励,而 AI 在最佳温度范围内并不会获得奖励,也不会因超出范围而受到惩罚。一旦我们完全构建了 AI,我建议你可以尝试修改代码,添加一些奖励或惩罚机制,让 AI 尽可能保持在最佳温度范围内;但现在,为了简化操作并让我们的 AI 正常运行,我们将奖励完全与节省的能量挂钩。

然后,在 pm 到 pm 之间,会发生一些新变化:一些新用户登录到服务器,一些现有用户登出,新的数据传输进服务器,已有的数据传输出服务器。根据假设 1,这些因素使得服务器的温度发生变化。假设总的来说,它们将服务器的温度提高了C:

现在,请记住我们分别评估两个系统:我们的 AI 和服务器的集成冷却系统。因此,我们必须分别计算这两个系统在 pm 时得到的温度,假设这两个系统互不干扰。我们先从 AI 开始。

当 AI 启用时,在 pm 时我们得到的温度是:

如果 AI 未启用,那么在 pm 时我们得到的温度是:

现在我们有了两个独立的温度,当 AI 启用时,温度为= 31.5°C,当 AI 未启用时,温度为= 29°C。

让我们模拟一下 pm 到 pm 之间会发生什么。再次强调,我们的 AI 将做出预测,假设服务器正在升温,那么它预测执行动作 0,也就是让服务器降温,降至。因此,AI 在 pm 到 pm 之间消耗的能量是:

现在关于服务器的集成冷却系统(即当没有 AI 时),由于在 pm 时我们有,因此最佳温度范围的最近边界仍然是,所以服务器的非智能冷却系统在 pm 到 pm 之间将消耗的能量是:

因此,在! 下午和! 下午之间获得的奖励,仅仅完全基于节省的能量,计算结果为:

最后,在! 下午和! 下午之间获得的总奖励是:

这是整个过程发生两分钟的一个示例。在我们的实现中,我们将对训练进行 1000 个周期,每个周期为 5 个月;然后,一旦我们的 AI 训练完成,我们将在 1 年的完整模拟中运行相同的过程进行测试。

现在我们已经详细定义并构建了环境,是时候让我们的 AI 采取行动了!这就是深度 Q 学习发挥作用的地方。我们的模型将比之前的模型更先进,因为我将引入一些新的技巧,称为dropout(丢弃法)和early stopping(提前停止),这些都是非常棒的技巧,可以成为你工具包中的一部分;它们通常会提升深度 Q 学习的训练性能。

别忘了,你还会获得一个 AI 蓝图,它将允许你将我们在这里所做的应用到任何其他业务问题,使用深度 Q 学习来解决。

准备好了吗?让我们开始吧。

AI 解决方案

让我们首先回顾一下整个深度 Q 学习模型,并将其适应到这个案例研究中,这样你就不需要滚动或者翻回前面的章节了。重复永远没有坏处,它能帮助我们将知识牢牢地记在脑海中。这里是你再次看到的深度 Q 学习算法:

初始化:

  1. 经验回放的记忆初始化为空列表,在代码中称为memory(位于 GitHub 仓库的Chapter 11文件夹中的dqn.py Python 文件)。

  2. 我们为记忆选择一个最大大小,在代码中称为max_memory(位于 GitHub 仓库的Chapter 11文件夹中的dqn.py Python 文件)。

在每个时间点t(每分钟),我们重复以下过程,直到周期结束:

  1. 我们预测当前状态的 Q 值!。由于可以执行五种动作(0 == 降温 3°C,1 == 降温 1.5°C,2 == 无热传递,3 == 加热 1.5°C,4 == 加热 3°C),我们会得到五个预测 Q 值。

  2. 我们执行通过 argmax 方法选择的动作,这个方法简单地选择具有五个预测 Q 值中最高值的动作:

  3. 我们获得了奖励!,它是差值!

  4. 我们达到了下一个状态!,它由以下三个元素组成:

    • 服务器在时间点!时的温度

    • 服务器中用户的数量在时间点!

    • 服务器在时间的数据显示传输速率

  5. 我们将过渡过程附加到内存中。

  6. 我们随机选择一批过渡!。对于随机批次中的所有过渡!

    • 我们获得预测值:

    • 我们获得目标:

    • 我们计算预测值和目标值之间的损失,涵盖整个批次!:

然后我们最终通过反向传播将这个损失误差传递回神经网络,并通过随机梯度下降,根据它们对损失误差的贡献程度更新权重。

希望复习一下让你耳目一新!让我们继续谈谈这个系统的大脑。

大脑

所谓大脑,当然是指我们的 AI 的人工神经网络。

我们的大脑将是一个全连接神经网络,包含两层隐藏层,第一层有 64 个神经元,第二层有 32 个神经元。提醒一下,这个神经网络的输入是环境的状态,输出是每个可能动作的 Q 值。

这种神经网络设计,分别有 64 和 32 个神经元的两层隐藏层,通常被认为是经典架构。它适用于解决许多问题,并且在这里会很好地工作。

这个人工大脑将使用均方误差MSE)损失和Adam优化器进行训练。选择 MSE 损失是因为我们想要衡量并减少预测值和目标值之间的平方差,而Adam优化器是实践中默认使用的经典优化器。

这是这个人工大脑的样子:

图 2:我们 AI 的人工大脑

这个人工大脑看起来很复杂,但得益于强大的 Keras 库,我们可以非常轻松地构建它。在上一章中,我们使用了 PyTorch,因为它是我更熟悉的神经网络库;但我希望你能尽可能多地使用 AI 工具,所以在这一章中,我们将使用 Keras。以下是完整实现的预览,其中包含构建这个大脑的部分(来自brain_nodropout.py文件):

# BUILDING THE BRAIN
class Brain(object):

    # BUILDING A FULLY CONNECTED NEURAL NETWORK DIRECTLY INSIDE THE INIT METHOD

    def __init__(self, learning_rate = 0.001, number_actions = 5):
        self.learning_rate = learning_rate

        # BUILDING THE INPUT LAYER COMPOSED OF THE INPUT STATE
        states = Input(shape = (3,))

        # BUILDING THE FULLY CONNECTED HIDDEN LAYERS
        x = Dense(units = 64, activation = 'sigmoid')(states)
        y = Dense(units = 32, activation = 'sigmoid')(x)

        # BUILDING THE OUTPUT LAYER, FULLY CONNECTED TO THE LAST HIDDEN LAYER
        q_values = Dense(units = number_actions, activation = 'softmax')(y)

        # ASSEMBLING THE FULL ARCHITECTURE INSIDE A MODEL OBJECT
        self.model = Model(inputs = states, outputs = q_values)

        # COMPILING THE MODEL WITH A MEAN-SQUARED ERROR LOSS AND A CHOSEN OPTIMIZER
        self.model.compile(loss = 'mse', optimizer = Adam(lr = learning_rate)) 

如你所见,这仅仅需要几行代码,我会在后续章节中逐行解释这段代码。现在让我们继续实现部分。

实现

该实现将分为五个部分,每个部分都有其独立的 Python 文件。你可以在 GitHub 仓库的Chapter 11文件夹中找到完整的实现。这五个部分构成了通用的 AI 框架,或者称为 AI 蓝图,应该在每次建立环境来解决深度强化学习的业务问题时遵循。

从步骤 1 到步骤 5,它们是这样的:

  • 步骤 1:构建环境(environment.py

  • 步骤 2:构建大脑(brain_nodropout.pybrain_dropout.py

  • 步骤 3:实现深度强化学习算法,在我们的案例中是深度 Q 学习模型(dqn.py

  • 步骤 4:训练 AI(training_noearlystopping.pytraining_earlystopping.py

  • 步骤 5:测试 AI(testing.py

按顺序,这些是通用 AI 框架的主要步骤。

我们将遵循这个 AI 蓝图,在接下来的五个部分中为我们的特定案例实现 AI,每个部分对应以下五个主要步骤。在每个步骤中,我们将通过将通用 AI 框架的所有子步骤的代码部分标题写成大写字母,来区分那些仍然属于通用 AI 框架的子步骤与那些特定于我们项目的子步骤,将后者的标题写成小写字母。

这意味着,每当你看到一个新的代码部分,其中标题是大写字母时,它就是通用 AI 框架的下一个子步骤,在为你自己业务问题构建 AI 时,你也应该遵循这一点。

下一步,构建环境,是这个项目中最大的 Python 实现文件。确保你已经休息好了,电量充足,一旦准备好,让我们一起解决这个问题!

步骤 1 – 构建环境

在第一步中,我们将把环境构建成一个类。为什么选择类?因为我们希望环境成为一个对象,可以轻松地通过选择一些参数的值来创建。

例如,我们可以为一台服务器创建一个环境对象,这台服务器在特定时间有一定数量的连接用户和一定的数据传输速率;同时为另一台服务器创建一个环境对象,这台服务器有不同数量的连接用户和不同的数据传输速率。得益于该类的先进结构,我们可以轻松地将我们在不同服务器上创建的环境对象进行即插即用,这些服务器有各自的参数,通过多个不同的 AI 来调节它们的温度,从而最小化整个数据中心的能耗,就像 Google DeepMind 使用其 DQN(深度 Q 学习)算法为 Google 的数据中心做的那样。

该类遵循以下子步骤,这些步骤属于步骤 1 – 构建环境中的通用 AI 框架:

  • 步骤 1-1:介绍并初始化环境的所有参数和变量。

  • 步骤 1-2:创建一个方法,在 AI 执行一个动作后更新环境。

  • 步骤 1-3:创建一个重置环境的方法。

  • 步骤 1-4:创建一个方法,随时向我们提供当前状态、最后获得的奖励以及游戏是否结束。

你将在本节中找到整个Environment类的实现。记住最重要的事情:所有标题为大写字母的代码部分都是通用 AI 框架/蓝图的步骤,而所有标题为小写字母的代码部分则是我们案例研究的具体实现。

环境实现代码共有 144 行。我不会逐行解释代码,原因有两个:

  1. 这会让本章内容显得非常繁重。

  2. 代码非常简单,并且已经进行了注释以提高清晰度,基本上是创建了我们在本章迄今为止定义的所有内容。

我相信你理解起来不会有问题。除此之外,代码段的标题和所选的变量名称已经足够清晰,能够让你从表面上就理解代码的结构和流程。我将简要介绍一下代码,开始吧!

首先,我们开始构建Environment类,并定义它的第一个方法,即__init__方法,该方法会引入并初始化我们之前提到的所有参数和变量:

# BUILDING THE ENVIRONMENT IN A CLASS
class Environment(object):

    # INTRODUCING AND INITIALIZING ALL THE PARAMETERS AND VARIABLES OF THE ENVIRONMENT

    def __init__(self, optimal_temperature = (18.0, 24.0), initial_month = 0, initial_number_users = 10, initial_rate_data = 60):
        self.monthly_atmospheric_temperatures = [1.0, 5.0, 7.0, 10.0, 11.0, 20.0, 23.0, 24.0, 22.0, 10.0, 5.0, 1.0]
        self.initial_month = initial_month
        self.atmospheric_temperature = self.monthly_atmospheric_temperatures[initial_month]
        self.optimal_temperature = optimal_temperature
        self.min_temperature = -20
        self.max_temperature = 80
        self.min_number_users = 10
        self.max_number_users = 100
        self.max_update_users = 5
        self.min_rate_data = 20
        self.max_rate_data = 300
        self.max_update_data = 10
        self.initial_number_users = initial_number_users
        self.current_number_users = initial_number_users
        self.initial_rate_data = initial_rate_data
        self.current_rate_data = initial_rate_data
        self.intrinsic_temperature = self.atmospheric_temperature + 1.25 * self.current_number_users + 1.25 * self.current_rate_data
        self.temperature_ai = self.intrinsic_temperature
        self.temperature_noai = (self.optimal_temperature[0] + self.optimal_temperature[1]) / 2.0
        self.total_energy_ai = 0.0
        self.total_energy_noai = 0.0
        self.reward = 0.0
        self.game_over = 0
        self.train = 1 

你会注意到self.monthly_atmospheric_temperatures变量,它是一个列表,包含了 12 个月份的平均大气温度:1 月为 1°C,2 月为 5°C,3 月为 7°C,依此类推。

self.atmospheric_temperature变量表示当前模拟月份的平均大气温度,它被初始化为初始月份的气温,我们稍后会将其设置为 1 月。

self.game_over变量告诉 AI 是否需要重置服务器温度,防止其超过允许的范围[-20°C, 80°C]。如果超过范围,self.game_over将被设置为 1,否则保持为 0。

最后,self.train变量告诉我们当前是处于训练模式还是推理模式。如果是训练模式,self.train = 1;如果是推理模式,self.train = 0。剩下的代码就是将本章开头所定义的所有内容实现成代码。

我们继续!

现在,我们来定义第二个方法update_env,它在 AI 执行某个操作后更新环境。这个方法有三个输入参数:

  1. direction:描述 AI 对服务器施加的热量传输方向的变量,像这样:如果direction == 1,则表示 AI 在加热服务器;如果direction == -1,则表示 AI 在冷却服务器。我们需要在调用update_env方法之前获取这个方向的值,因为该方法是在操作执行后调用的。

  2. energy_ai:AI 在执行动作时,所消耗的能量,用来加热或冷却服务器。根据假设 2,它等于 AI 在服务器中引起的温度变化。

  3. month:表示在执行动作时我们所在的月份。

程序在此方法内执行的第一步是计算奖励。实际上,在执行动作后,我们可以立即推断出奖励,因为它是服务器集成系统在没有 AI 的情况下消耗的能量与 AI 实际消耗的能量之间的差异:

 # MAKING A METHOD THAT UPDATES THE ENVIRONMENT RIGHT AFTER THE AI PLAYS AN ACTION

    def update_env(self, direction, energy_ai, month):

        # GETTING THE REWARD

        # Computing the energy spent by the server's cooling system when there is no AI
        energy_noai = 0
        if (self.temperature_noai < self.optimal_temperature[0]):
            energy_noai = self.optimal_temperature[0] - self.temperature_noai
            self.temperature_noai = self.optimal_temperature[0]
        elif (self.temperature_noai > self.optimal_temperature[1]):
            energy_noai = self.temperature_noai - self.optimal_temperature[1]
            self.temperature_noai = self.optimal_temperature[1]
        # Computing the Reward
        self.reward = energy_noai - energy_ai
        # Scaling the Reward
        self.reward = 1e-3 * self.reward 

你可能已经注意到,我们选择在最后对奖励进行缩放。简而言之,缩放是将数值(这里是奖励)缩小到一个较小的范围。例如,归一化是一种缩放技术,其中所有值都缩小到 0 和 1 之间的范围。另一种广泛使用的缩放技术是标准化,这将在稍后解释。

缩放是深度强化学习研究论文中通常推荐的一种常见做法,因为它可以稳定训练并提高 AI 性能。

在获得奖励后,我们进入下一个状态。记住,每个状态由以下元素组成:

  1. 服务器在时间点的温度

  2. 服务器在时间点的用户数量

  3. 服务器在时间点的数据传输速率

所以,当我们进入下一个状态时,我们逐个更新这些元素,遵循下一段代码中的注释部分所强调的子步骤:

 # GETTING THE NEXT STATE

        # Updating the atmospheric temperature
        self.atmospheric_temperature = self.monthly_atmospheric_temperatures[month]
        # Updating the number of users
        self.current_number_users += np.random.randint(-self.max_update_users, self.max_update_users)
        if (self.current_number_users > self.max_number_users):
            self.current_number_users = self.max_number_users
        elif (self.current_number_users < self.min_number_users):
            self.current_number_users = self.min_number_users
        # Updating the rate of data
        self.current_rate_data += np.random.randint(-self.max_update_data, self.max_update_data)
        if (self.current_rate_data > self.max_rate_data):
            self.current_rate_data = self.max_rate_data
        elif (self.current_rate_data < self.min_rate_data):
            self.current_rate_data = self.min_rate_data
        # Computing the Delta of Intrinsic Temperature
        past_intrinsic_temperature = self.intrinsic_temperature
        self.intrinsic_temperature = self.atmospheric_temperature + 1.25 * self.current_number_users + 1.25 * self.current_rate_data
        delta_intrinsic_temperature = self.intrinsic_temperature - past_intrinsic_temperature
        # Computing the Delta of Temperature caused by the AI
        if (direction == -1):
            delta_temperature_ai = -energy_ai
        elif (direction == 1):
            delta_temperature_ai = energy_ai
        # Updating the new Server's Temperature when there is the AI
        self.temperature_ai += delta_intrinsic_temperature + delta_temperature_ai
        # Updating the new Server's Temperature when there is no AI
        self.temperature_noai += delta_intrinsic_temperature 

然后,如果需要,我们更新 self.game_over 变量,也就是说,如果服务器的温度超出允许的范围 [-20°C, 80°C],则会发生这种情况。如果服务器温度低于最低温度 -20°C,或者高于最高温度 80°C,就会出现这种情况。此外,我们还做了两件事:将服务器温度恢复到最佳温度范围内(最接近的边界),并且由于此操作会消耗一些能量,我们更新了 AI 消耗的总能量(self.total_energy_ai)。这正是下一段代码所实现的内容:

 # GETTING GAME OVER

        if (self.temperature_ai < self.min_temperature):
            if (self.train == 1):
                self.game_over = 1
            else:
                self.total_energy_ai += self.optimal_temperature[0] - self.temperature_ai
                self.temperature_ai = self.optimal_temperature[0]
        elif (self.temperature_ai > self.max_temperature):
            if (self.train == 1):
                self.game_over = 1
            else:
                self.total_energy_ai += self.temperature_ai - self.optimal_temperature[1]
                self.temperature_ai = self.optimal_temperature[1] 

现在,我知道对于服务器来说,温度从 80 度迅速降回 24 度,或者从-20 度升到 18 度似乎不太现实,但这是我们之前定义的高效集成冷却系统完全能够做到的动作。可以将其理解为在温度灾难情况下,AI 会短暂切换到集成系统。再次强调,这个领域将从你在 AI 正常运行后继续调试中受益巨大;之后,你可以随意调整这些数值,以便实现更真实的服务器模型。

然后,我们更新来自两个独立仿真模型的两个分数,它们是:

  1. self.total_energy_ai:AI 消耗的总能量

  2. self.total_energy_noai:当没有 AI 时,服务器集成冷却系统消耗的总能量。

 # UPDATING THE SCORES

        # Updating the Total Energy spent by the AI
        self.total_energy_ai += energy_ai
        # Updating the Total Energy spent by the server's cooling system when there is no AI
        self.total_energy_noai += energy_noai 

然后,为了提高性能,我们通过对下一个状态的三个元素(服务器温度、用户数量和数据传输率)进行缩放来进行标准化处理。具体做法是通过简单的标准化缩放技术,即先减去变量的最小值,再除以变量的最大变化量:

 # SCALING THE NEXT STATE

        scaled_temperature_ai = (self.temperature_ai - self.min_temperature) / (self.max_temperature - self.min_temperature)
        scaled_number_users = (self.current_number_users - self.min_number_users) / (self.max_number_users - self.min_number_users)
        scaled_rate_data = (self.current_rate_data - self.min_rate_data) / (self.max_rate_data - self.min_rate_data)
        next_state = np.matrix([scaled_temperature_ai, scaled_number_users, scaled_rate_data]) 

最后,我们通过返回下一个状态、收到的奖励以及游戏是否结束,来结束这个update_env方法:

 # RETURNING THE NEXT STATE, THE REWARD, AND GAME OVER

        return next_state, self.reward, self.game_over 

很好!我们完成了这个冗长但重要的方法,它会在每个时间步(每分钟)更新环境。现在只剩下两个最终且非常简单的方法:一个是重置环境,另一个是随时提供三项信息:当前状态、上次收到的奖励以及游戏是否结束。

这是reset方法,它会在新的训练回合开始时重置环境,将环境的所有变量恢复到最初初始化的值:

 # MAKING A METHOD THAT RESETS THE ENVIRONMENT

    def reset(self, new_month):
        self.atmospheric_temperature = self.monthly_atmospheric_temperatures[new_month]
        self.initial_month = new_month
        self.current_number_users = self.initial_number_users
        self.current_rate_data = self.initial_rate_data
        self.intrinsic_temperature = self.atmospheric_temperature + 1.25 * self.current_number_users + 1.25 * self.current_rate_data
        self.temperature_ai = self.intrinsic_temperature
        self.temperature_noai = (self.optimal_temperature[0] + self.optimal_temperature[1]) / 2.0
        self.total_energy_ai = 0.0
        self.total_energy_noai = 0.0
        self.reward = 0.0
        self.game_over = 0
        self.train = 1 

最后,这是observe方法,它让我们随时知道当前状态、上次收到的奖励以及游戏是否结束:

 # MAKING A METHOD THAT GIVES US AT ANY TIME THE CURRENT STATE, THE LAST REWARD AND WHETHER THE GAME IS OVER

    def observe(self):
        scaled_temperature_ai = (self.temperature_ai - self.min_temperature) / (self.max_temperature - self.min_temperature)
        scaled_number_users = (self.current_number_users - self.min_number_users) / (self.max_number_users - self.min_number_users)
        scaled_rate_data = (self.current_rate_data - self.min_rate_data) / (self.max_rate_data - self.min_rate_data)
        current_state = np.matrix([scaled_temperature_ai, scaled_number_users, scaled_rate_data])
        return current_state, self.reward, self.game_over 

太棒了!我们完成了实现的第一步——构建环境。现在让我们继续下一步,开始构建大脑。

第 2 步——构建大脑

在这一步,我们将构建我们 AI 的人工大脑,它就是一个完全连接的神经网络。再看一遍:

图 3:我们 AI 的人工大脑

我们将把这个人工大脑构建在一个类里,原因和之前一样,就是为了让我们能够为数据中心内的不同服务器创建多个人工大脑。也许有些服务器需要不同的人工大脑,并且具有不同的超参数。这就是为什么,通过这个类/对象的高级 Python 结构,我们可以轻松地从一个大脑切换到另一个大脑,以调节需要不同神经网络参数的新服务器的温度。这就是面向对象编程OOP)的魅力所在。

我们正在使用强大的 Keras 库来构建这个人工大脑。在这个库中,我们使用Dense()类来创建两个完全连接的隐藏层,第一个隐藏层有 64 个神经元,第二个隐藏层有 32 个神经元。记住,这是一个经典的神经网络架构,通常作为默认结构使用,也是许多研究论文中看到的常见做法。最后,我们再次使用Dense()类返回 Q 值,这些是人工神经网络的输出。

以后,当我们编写训练和测试文件时,我们将使用 argmax 方法选择具有最大 Q 值的动作。然后,通过创建一个Model()类对象来组装大脑的所有组件,包括输入和输出(这非常有用,因为我们可以保存和加载具有特定权重的模型)。最后,我们将其与均方误差损失和 Adam 优化器一起编译。稍后我会详细解释这一切。

以下是通用 AI 框架的新步骤:

  • 步骤 2-1:构建输入层,由输入状态组成。

  • 步骤 2-2:构建一定数量的隐藏层,每个隐藏层包含一定数量的神经元,并与输入层以及彼此之间全连接。

  • 步骤 2-3:构建输出层,完全连接到最后一个隐藏层。

  • 步骤 2-4:将完整架构组装到一个模型对象中。

  • 步骤 2-5:使用均方误差损失函数和所选优化器编译模型。

该实现以两种不同文件的形式提供给你:

  1. brain_nodropout.py:一个实现文件,使用没有 dropout 正则化技术的人工神经网络(我很快会解释它是什么)。

  2. brain_dropout.py:一个实现文件,使用 dropout 正则化技术构建人工神经网络。

首先,给你提供没有 dropout 的实现,然后再提供带有 dropout 的实现并解释它。

没有 dropout

这是没有任何 dropout 正则化技术的人工神经网络完整实现:

# AI for Business - Minimize cost with Deep Q-Learning   #1
# Building the Brain without Dropout   #2
#3
# Importing the libraries   #4
from keras.layers import Input, Dense   #5
from keras.models import Model   #6
from keras.optimizers import Adam   #7
   #8
# BUILDING THE BRAIN   #9
   #10
class Brain(object):   #11
    #12
    # BUILDING A FULLY CONNECTED NEURAL NETWORK DIRECTLY INSIDE THE INIT METHOD   #13
    #14
    def __init__(self, learning_rate = 0.001, number_actions = 5):   #15
        self.learning_rate = learning_rate   #16
        #17
        # BUILDING THE INPUT LAYER COMPOSED OF THE INPUT STATE   #18
        states = Input(shape = (3,))   #19
        #20
        # BUILDING THE FULLY CONNECTED HIDDEN LAYERS   #21
        x = Dense(units = 64, activation = 'sigmoid')(states)   #22
        y = Dense(units = 32, activation = 'sigmoid')(x)   #23
        #24
        # BUILDING THE OUTPUT LAYER, FULLY CONNECTED TO THE LAST HIDDEN LAYER   #25
        q_values = Dense(units = number_actions, activation = 'softmax')(y)   #26
        #27
        # ASSEMBLING THE FULL ARCHITECTURE INSIDE A MODEL OBJECT   #28
        self.model = Model(inputs = states, outputs = q_values)   #29
        #30
        # COMPILING THE MODEL WITH A MEAN-SQUARED ERROR LOSS AND A CHOSEN OPTIMIZER   #31
        self.model.compile(loss = 'mse', optimizer = Adam(lr = learning_rate))   #32 

现在,让我们详细查看代码。

第 5 行:我们从keras库的layers模块中导入InputDense类。Input类允许我们构建输入层,而Dense类允许我们构建全连接层。

第 6 行:我们从keras库的models模块中导入Model类。它允许我们通过组装不同的层来构建整个神经网络模型。

第 7 行:我们从keras库的optimizers模块中导入Adam类。它允许我们使用 Adam 优化器,通过随机梯度下降更新神经网络的权重,在每次训练迭代中反向传播损失误差。

第 11 行:我们引入了Brain类,它不仅包含人工神经网络的整个架构,还包含模型与损失(均方误差)以及 Adam 优化器的连接。

第 15 行:我们引入了__init__方法,这将是该类的唯一方法。我们在其中定义了神经网络的整个架构,通过创建连续的变量,这些变量共同组装成神经网络。此方法接受两个参数作为输入:

  1. 学习率(learning_rate),这是一个衡量你希望神经网络学习速度的指标(学习率越高,神经网络学习越快;但代价是质量下降)。默认值为0.001

  2. number_actions(动作数量),当然是指我们的 AI 能执行的动作数。现在你可能在想:为什么我们需要将这个作为参数呢?这只是为了防止你想要构建一个可以执行更多或更少动作的 AI。在这种情况下,你只需要更改参数的值,其他就不需要改动了。相当实用,不是吗?

第 16 行:我们为学习率创建一个对象变量,self.learning_rate,并初始化为__init__方法中提供的learning_rate参数的值(因此当我们将来创建Brain类的对象时,该参数就会被使用)。

第 19 行:我们创建输入状态层,命名为states,作为Input类的一个对象。我们向这个Input类传入一个参数,shape = (3,),这只是说明输入层是一个由三个元素组成的 1D 向量(服务器温度、用户数量和数据传输速率)。

第 22 行:我们创建第一个完全连接的隐藏层,命名为x,作为Dense类的一个对象,它接受两个参数作为输入:

  1. units:我们希望在第一个隐藏层中拥有的隐藏神经元数量。在这里,我们选择了 64 个隐藏神经元。

  2. activation:前向传播输入到第一个隐藏层时使用的激活函数。这里我们默认选择了 sigmoid 激活函数,其形式如下:

图 4:sigmoid 激活函数

ReLU 激活函数在这里也会非常有效;我鼓励你进行实验!还要注意,输入层到第一个隐藏层的连接是通过在Dense类之后调用states变量来实现的。

第 23 行:我们创建第二个完全连接的隐藏层,命名为y,作为Dense类的一个对象,它接受相同的两个参数作为输入:

  1. units:我们希望在第二个隐藏层中拥有的隐藏神经元数量。这次我们选择了 32 个隐藏神经元。

  2. activation:前向传播输入到第一个隐藏层时使用的激活函数。这里,我们再次选择了 sigmoid 激活函数。

再次注意,第一隐藏层到第二隐藏层的连接是通过在Dense类之后调用x变量来实现的。

第 26 行:我们创建输出层,命名为q_values,并完全连接到第二个隐藏层,作为Dense类的一个对象。这次我们输入number_actions个单元,因为输出层包含要执行的动作,并且使用softmax激活函数,正如在第五章你的第一个 AI 模型——警惕“土匪”!中,关于深度 Q 学习理论的内容所看到的那样。

第 29 行:使用 Model 类,我们将神经网络的各层串联起来,只需将 states 作为输入,q_values 作为输出。

第 32 行:使用 Model 类中的 compile 方法,我们将模型连接到均方误差损失函数和 Adam 优化器。后者接受 learning_rate 参数作为输入。

使用 dropout

对你来说,添加一个更强大的技术到你的工具箱里会非常有价值:dropout

Dropout 是一种正则化技术,用于防止过拟合,过拟合是指人工智能模型在训练集上表现良好,但在测试集上表现较差的情况。Dropout 通过在每次前向传播和反向传播步骤中禁用随机选择的一部分神经元来实现。这意味着并不是所有的神经元都以相同的方式学习,从而防止了神经网络对训练数据的过拟合。

使用 keras 添加 dropout 非常简单。你只需要在 Dense 类后面调用 Dropout 类,并输入你希望禁用的神经元比例,如下所示:

# AI for Business - Minimize cost with Deep Q-Learning
# Building the Brain with Dropout
# Importing the libraries
from keras.layers import Input, Dense, Dropout
from keras.models import Model
from keras.optimizers import Adam
# BUILDING THE BRAIN
class Brain(object):

    # BUILDING A FULLY CONNECTED NEURAL NETWORK DIRECTLY INSIDE THE INIT METHOD

    def __init__(self, learning_rate = 0.001, number_actions = 5):
        self.learning_rate = learning_rate

        # BUILDING THE INPUT LAYER COMPOSED OF THE INPUT STATE
        states = Input(shape = (3,))

        # BUILDING THE FIRST FULLY CONNECTED HIDDEN LAYER WITH DROPOUT ACTIVATED
        x = Dense(units = 64, activation = 'sigmoid')(states)
        x = Dropout(rate = 0.1)(x)

        # BUILDING THE SECOND FULLY CONNECTED HIDDEN LAYER WITH DROPOUT ACTIVATED
        y = Dense(units = 32, activation = 'sigmoid')(x)
        y = Dropout(rate = 0.1)(y)

        # BUILDING THE OUTPUT LAYER, FULLY CONNECTED TO THE LAST HIDDEN LAYER
        q_values = Dense(units = number_actions, activation = 'softmax')(y)

        # ASSEMBLING THE FULL ARCHITECTURE INSIDE A MODEL OBJECT
        self.model = Model(inputs = states, outputs = q_values)

        # COMPILING THE MODEL WITH A MEAN-SQUARED ERROR LOSS AND A CHOSEN OPTIMIZER
        self.model.compile(loss = 'mse', optimizer = Adam(lr = learning_rate)) 

在这里,我们对第一层和第二层全连接层应用 dropout,每层禁用 10% 的神经元。接下来,让我们进入我们的一般 AI 框架的下一步:第 3 步 – 实现深度强化学习算法。

第 3 步 – 实现深度强化学习算法

在这个新实现中(见 dqn.py 文件),我们只需要遵循之前提供的深度 Q-learning 算法。因此,这个实现遵循以下子步骤,它们是一般 AI 框架的一部分:

  • 第 3-1 步:引入并初始化深度 Q-learning 模型的所有参数和变量。

  • 第 3-2 步:制作一个构建经验回放记忆的方法。

  • 第 3-3 步:制作一个构建并返回两批次 10 个输入和 10 个目标的方法。

首先,浏览一下整个代码,然后我会逐行解释:

# AI for Business - Minimize cost with Deep Q-Learning   #1
# Implementing Deep Q-Learning with Experience Replay   #2
#3
# Importing the libraries   #4
import numpy as np   #5
#6
# IMPLEMENTING DEEP Q-LEARNING WITH EXPERIENCE REPLAY   #7
#8
class DQN(object):   #9
    #10
    # INTRODUCING AND INITIALIZING ALL THE PARAMETERS AND VARIABLES OF THE DQN   #11
    def __init__(self, max_memory = 100, discount = 0.9):   #12
        self.memory = list()   #13
        self.max_memory = max_memory   #14
        self.discount = discount   #15
#16
    # MAKING A METHOD THAT BUILDS THE MEMORY IN EXPERIENCE REPLAY   #17
    def remember(self, transition, game_over):   #18
        self.memory.append([transition, game_over])   #19
        if len(self.memory) > self.max_memory:   #20
            del self.memory[0]   #21
#22
    # MAKING A METHOD THAT BUILDS TWO BATCHES OF INPUTS AND TARGETS BY EXTRACTING TRANSITIONS FROM THE MEMORY   #23
    def get_batch(self, model, batch_size = 10):   #24
        len_memory = len(self.memory)   #25
        num_inputs = self.memory[0][0][0].shape[1]   #26
        num_outputs = model.output_shape[-1]   #27
        inputs = np.zeros((min(len_memory, batch_size), num_inputs))   #28
        targets = np.zeros((min(len_memory, batch_size), num_outputs))   #29
        for i, idx in enumerate(np.random.randint(0, len_memory, size = min(len_memory, batch_size))):   #30
            current_state, action, reward, next_state = self.memory[idx][0]   #31
            game_over = self.memory[idx][1]   #32
            inputs[i] = current_state   #33
            targets[i] = model.predict(current_state)[0]   #34
            Q_sa = np.max(model.predict(next_state)[0])   #35
            if game_over:   #36
                targets[i, action] = reward   #37
            else:   #38
                targets[i, action] = reward + self.discount * Q_sa   #39
        return inputs, targets   #40 

第 5 行:我们导入 numpy 库,因为我们将使用 numpy 数组。

第 9 行:我们引入 DQN 类(DQN 代表 深度 Q 网络),它包含深度 Q-learning 算法的主要部分,包括经验回放。

第 12 行:我们引入 __init__ 方法,它创建了 DQN 模型的以下三个对象变量:经验回放记忆、容量(记忆的最大大小)和目标公式中的折扣因子。它的参数是 max_memory(容量)和 discount(折扣因子),如果我们想构建具有不同容量的其他经验回放记忆,或者如果我们想更改折扣因子在目标计算中的值,可以传入这些参数。默认值分别是 1000.9,这两个值是任意选择的,结果表明效果相当好;这些是很好的实验参数,可以试试看当它们设置为不同值时会有什么区别。

第 13 行:我们创建经验回放记忆对象变量self.memory,并将其初始化为空列表。

第 14 行:我们创建记忆容量的对象变量self.max_memory,并将其初始化为max_memory参数的值。

第 15 行:我们创建折扣因子对象变量self.discount,并将其初始化为discount参数的值。

第 18 行:我们引入了remember方法,该方法的输入为需要添加到记忆中的过渡,以及game_over,它表示该过渡是否导致服务器温度超出允许的温度范围。

第 19 行:通过从memory列表调用append函数,我们将带有game_over布尔值的过渡添加到记忆中(位于最后位置)。

第 20 行:如果在添加此过渡后,记忆的大小超过了记忆容量(self.max_memory)。

第 21 行:我们删除记忆中的第一个元素。

第 24 行:我们引入了get_batch方法,该方法的输入为我们在前一个 Python 文件中构建的模型(model)和批处理大小(batch_size),并通过从记忆中提取10个过渡(如果批处理大小是 10),构建两个输入和目标的批次。

第 25 行:我们获取记忆中当前的元素数量,并将其存入一个新变量len_memory

第 26 行:我们获取输入状态向量中的元素数量(为 3),但不是直接输入 3,而是通过访问记忆中输入状态向量元素的shape属性来获取这个数字,这个元素通过取[0][0][0]索引来获得。记忆中的每个元素结构如下:

[[current_state, action, reward, next_state], game_over]

因此,在[0][0][0]中,第一个[0]对应记忆的第一个元素(即第一个过渡),第二个[0]对应元组[current_stateactionrewardnext_state],所以第三个[0]对应该元组中的current_state元素。因此,self.memory[0][0][0]对应第一个当前状态,通过添加.shape[1],我们可以得到该输入状态向量中的元素数量。你可能会问,为什么我们没有直接输入 3;这是因为我们想将这段代码推广到任何你可能希望在环境中使用的输入状态向量维度。例如,你可能希望考虑一个包含更多关于服务器信息的输入状态,比如湿度。得益于这行代码,你无需更改关于状态元素数量的任何内容。

第 27 行:我们获取模型输出的元素数量,也就是动作的数量。就像前一行一样,我们并不是直接输入 5,而是通过访问model对象中的shape属性来实现这个目标,modelModel类的实例。-1意味着我们获取shape属性的最后一个索引,在这个位置包含了动作的数量。

第 28 行:我们引入并初始化输入批次作为一个numpy数组,batch_size = 10 行和 3 列,分别对应输入状态元素,初始值全为零。如果内存中还没有 10 个过渡,行数将是内存的长度。

如果内存中已经有至少 10 个过渡,这行代码的输出将是:

图 5:输入批次(1/2)

第 29 行:我们引入并初始化目标批次作为一个numpy数组,batch_size = 10 行和 5 列,分别对应五种可能的动作,初始值全为零。和之前一样,如果内存中还没有 10 个过渡,行数就会是内存的长度。如果内存中已经有至少 10 个过渡,这行代码的输出将是:

图 6:目标批次(1/3)

第 30 行:我们在同一个for循环内进行双重迭代。第一个迭代变量i从 0 遍历到批次大小(或者如果len_memory < batch_size,则迭代到len_memory为止):

i = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9

这样,i将迭代批次中的每个元素。第二个迭代变量idx随机选择内存中的 10 个索引,用于从内存中提取 10 个随机过渡。在for循环内,我们通过迭代每个元素来填充输入和目标的两个批次,并为它们赋予正确的值。

第 31 行:我们获取从内存中抽样的索引idx对应的过渡,包含当前状态、动作、奖励和下一个状态。我们之所以加上[0],是因为内存中的元素结构如下:

[[current_state, action, reward, next_state], game_over]

我们将在下一行代码中单独获取game_over值。

第 32 行:我们获取对应于内存中相同索引idxgame_over值。如你所见,这次我们在末尾加上[1]来获取内存元素中的第二个元素:

[[current_state, action, reward, next_state], game_over]

第 33 行:我们用所有当前状态填充输入批次,最终在for循环结束时得到:

图 7:输入批次(2/2)

第 34 行:现在我们开始用正确的值填充目标批次。首先,我们用模型预测的所有 Q 值 填充它,这些 Q 值对应不同的状态-动作对:(当前状态,动作 0)、(当前状态,动作 1)、(当前状态,动作 2)、(当前状态,动作 3)和(当前状态,动作 4)。因此,我们首先得到了这个(在 for 循环的末尾):

图 8:目标批次 (2/3)

请记住,对于执行的操作,目标的公式必须是这样的:

我们在接下来的代码行中所做的事情是将这个公式填入每个在 10 个选定过渡中执行的动作对应的列中。换句话说,我们得到了这个:

图 9:目标批次 (3/3)

在这个例子中,动作 1 在第一次过渡中执行(目标 1),动作 3 在第二次过渡中执行(目标 2),动作 0 在第三次过渡中执行(目标 3),依此类推。让我们在接下来的代码行中填充这个内容。

第 35 行:我们首先开始获取目标公式中的 部分:

第 36 行:我们检查 game_over 是否为 1,表示服务器已经超出了允许的温度范围。因为如果是这样,实际上就没有下一个状态(因为我们基本上通过将服务器温度恢复到最佳范围来重置环境,所以我们从一个新状态开始);因此,我们不应该考虑

第 37 行:在这种情况下,我们只保留目标中的 部分。

第 38 行:然而,如果游戏没有结束(game_over = 0)...

第 39 行:我们保留目标的完整公式,但当然只保留执行的动作部分,也就是说,这里是

因此,我们得到了如下的目标批次,正如你之前所看到的:

图 10:目标批次 (3/3)

第 40 行:最后,我们 return 最终的 inputstargets 批次。

那真是太棒了——你已经成功创造了一个人工大脑。现在,既然你已经完成了它,我们可以开始训练了。

步骤 4:训练 AI

既然我们的 AI 拥有了完全功能的大脑,接下来就是训练它的时候了。这正是我们在第四个 Python 实现中所做的事情。你实际上可以选择两个文件来进行此操作:

  1. training_noearlystopping.py,它在 5 个月的周期内训练你的 AI,共 1000 轮。

  2. training_earlystopping.py,它同样训练你的 AI 1000 轮,但如果性能在迭代过程中不再提高,它会提前停止训练。这种技术叫做 提前停止

这两个实现都比较长,但非常简单。我们从设置所有参数开始,然后通过创建Environment()类的对象来构建环境,接着通过创建Brain()类的对象来构建 AI 的大脑,然后通过创建DQN()类的对象来构建深度 Q 学习模型,最后将所有这些对象连接起来,进行 1000 个周期的 5 个月训练。

在训练循环中,你会注意到我们在执行动作时也进行了一些探索,偶尔会执行一些随机动作。在我们的案例中,30%的时间我们会这样做,因为我们使用了探索参数 ,然后当我们从 0 到 1 之间抽取一个随机值,并且这个值低于 时,我们就强制 AI 执行一个随机动作。我们进行一些探索的原因是它能够改进深度强化学习过程,正如我们在第九章《走向专业:深度 Q 学习》中所讨论的那样,而我们在这个项目中不使用 Softmax 的原因仅仅是为了让你了解如何实现一种不同的探索方法。

后面你会在training_noearlystopping.py文件中看到另一个小的改进,我们使用了一种早停技术,如果性能没有改善,训练会提前停止。

让我们重点介绍这些新的步骤,它们仍然属于我们的通用 AI 框架/蓝图:

  • 步骤 4-1:通过创建Environment类的对象来构建环境。

  • 步骤 4-2:通过创建Brain类的对象来构建人工大脑。

  • 步骤 4-3:通过创建DQN类的对象来构建DQN模型。

  • 步骤 4-4:选择训练模式。

  • 步骤 4-5:通过一个for循环开始训练,进行 100 个周期的 5 个月训练。

  • 步骤 4-6:在每个 epoch 中,我们重复整个深度 Q 学习过程,同时 30%的时间也进行一些探索。

没有早停

准备好实现这个了吗?或许先喝杯好咖啡或茶吧,因为这个过程会有点长(88 行代码,但都是简单的!)。我们会先从不使用早停开始,然后在最后我会解释如何添加早停技术。接下来的文件是training_noearlystopping.py。由于这个过程比较长,这次我们按部分来进行,从第一部分开始:

# AI for Business - Minimize cost with Deep Q-Learning   #1
# Training the AI without Early Stopping   #2
#3
# Importing the libraries and the other python files   #4
import os   #5
import numpy as np   #6
import random as rn   #7
import environment   #8
import brain_nodropout   #9
import dqn   #10 

第 5 行:我们导入os库,它将用于设置随机种子,以确保结果的可重复性,这样即使你多次运行训练,每次都会得到相同的结果。当然,你也可以在自己修改代码时选择去除它!

第 6 行:我们导入numpy库,因为我们将处理numpy数组。

第 7 行:我们导入random库,它将用于进行一些探索。

第 8 行:我们导入environment.py文件,它在步骤 1 中实现,包含了整个定义的环境。

第 9 行:我们导入了brain_nodropout.py文件,这是我们在第 2 步实现的无 dropout 人工大脑。它包含了我们 AI 的整个神经网络。

第 10 行:我们导入了第 3 步实现的dqn.py文件,它包含了深度 Q 学习算法的主要部分,包括经验回放。

接下来进入下一部分:

# Setting seeds for reproducibility   #12
os.environ['PYTHONHASHSEED'] = '0'   #13
np.random.seed(42)   #14
rn.seed(12345)   #15
#16
# SETTING THE PARAMETERS   #17
epsilon = .3   #18
number_actions = 5   #19
direction_boundary = (number_actions - 1) / 2   #20
number_epochs = 100   #21
max_memory = 3000   #22
batch_size = 512   #23
temperature_step = 1.5   #24
#25
# BUILDING THE ENVIRONMENT BY SIMPLY CREATING AN OBJECT OF THE ENVIRONMENT CLASS   #26
env = environment.Environment(optimal_temperature = (18.0, 24.0), initial_month = 0, initial_number_users = 20, initial_rate_data = 30)   #27
#28
# BUILDING THE BRAIN BY SIMPLY CREATING AN OBJECT OF THE BRAIN CLASS   #29
brain = brain_nodropout.Brain(learning_rate = 0.00001, number_actions = number_actions)   #30
#31
# BUILDING THE DQN MODEL BY SIMPLY CREATING AN OBJECT OF THE DQN CLASS   #32
dqn = dqn.DQN(max_memory = max_memory, discount = 0.9)   #33
#34
# CHOOSING THE MODE   #35
train = True   #36 

第 13、14、15 行:我们设置了种子以确保可重复性,这样可以在多轮训练后获得相同的结果。这实际上只是为了让你能重现你的实验结果——如果你不需要这样做,部分人可能偏好使用种子,其他人则不使用。如果你不想使用种子,可以直接移除它们。

第 18 行:我们引入了探索因子 ,并将其设置为0.3,这意味着有 30%的探索(执行随机动作)与 70%的利用(执行 AI 的动作)。

第 19 行:我们将动作数量设置为5

第 20 行:我们设置了方向边界,意思是设置一个动作索引,低于该索引时我们会让服务器冷却,高于该索引时我们会让服务器加热。由于动作 0 和 1 使服务器冷却,动作 3 和 4 使服务器加热,因此该方向边界为(5-1)/2 = 2,对应于不向服务器传递热量的动作(动作 2)。

第 21 行:我们将训练的 epoch 数设置为100

第 22 行:我们将内存容量(即最大大小)设置为3000

第 23 行:我们将批量大小设置为512

第 24 行:我们引入了温度步长,意味着 AI 通过执行动作 0、1、3 或 4 对服务器产生的绝对温度变化。而这个值当然是1.5°C。

第 27 行:我们创建了environment对象,作为Environment类的实例,调用自environment文件。在这个Environment类中,我们输入了init方法的所有参数:

optimal_temperature = (18.0, 24.0),
initial_month = 0,
initial_number_users = 20,
initial_rate_data = 30 

第 30 行:我们创建了brain对象,作为Brain类的实例,调用自brain_nodropout文件。在这个Brain类中,我们输入了init方法的所有参数:

learning_rate = 0.00001,
number_actions = number_actions 

第 33 行:我们创建了dqn对象,作为DQN类的实例,调用自dqn文件。在这个DQN类中,我们输入了init方法的所有参数:

max_memory = max_memory,
discount = 0.9 

第 36 行:我们将训练模式设置为True,因为接下来的代码部分将包含执行所有训练的大for循环。

到目前为止都还好吗?如果你感到有些不知所措或迷失,不妨稍作休息,或者回过头阅读前面的段落。

现在让我们开始进行大规模的训练循环;这是这个文件的最后一部分代码:

# TRAINING THE AI   #38
env.train = train   #39
model = brain.model   #40
if (env.train):   #41
    # STARTING THE LOOP OVER ALL THE EPOCHS (1 Epoch = 5 Months)   #42
    for epoch in range(1, number_epochs):   #43
        # INITIALIAZING ALL THE VARIABLES OF BOTH THE ENVIRONMENT AND THE TRAINING LOOP   #44
        total_reward = 0   #45
        loss = 0\.   #46
        new_month = np.random.randint(0, 12)   #47
        env.reset(new_month = new_month)   #48
        game_over = False   #49
        current_state, _, _ = env.observe()   #50
        timestep = 0   #51
        # STARTING THE LOOP OVER ALL THE TIMESTEPS (1 Timestep = 1 Minute) IN ONE EPOCH   #52
        while ((not game_over) and timestep <= 5 * 30 * 24 * 60):   #53
            # PLAYING THE NEXT ACTION BY EXPLORATION   #54
            if np.random.rand() <= epsilon:   #55
                action = np.random.randint(0, number_actions)   #56
                if (action - direction_boundary < 0):   #57
                    direction = -1   #58
                else:   #59
                    direction = 1   #60
                energy_ai = abs(action - direction_boundary) * temperature_step   #61
            # PLAYING THE NEXT ACTION BY INFERENCE   #62
            else:   #63
                q_values = model.predict(current_state)   #64
                action = np.argmax(q_values[0])   #65
                if (action - direction_boundary < 0):   #66
                    direction = -1   #67
                else:   #68
                    direction = 1   #69
                energy_ai = abs(action - direction_boundary) * temperature_step   #70
            # UPDATING THE ENVIRONMENT AND REACHING THE NEXT STATE   #71
            next_state, reward, game_over = env.update_env(direction, energy_ai, ( new_month + int(timestep/(30*24*60)) ) % 12)   #72
            total_reward += reward   #73
            # STORING THIS NEW TRANSITION INTO THE MEMORY   #74
            dqn.remember([current_state, action, reward, next_state], game_over)   #75
            # GATHERING IN TWO SEPARATE BATCHES THE INPUTS AND THE TARGETS   #76
            inputs, targets = dqn.get_batch(model, batch_size = batch_size)   #77
            # COMPUTING THE LOSS OVER THE TWO WHOLE BATCHES OF INPUTS AND TARGETS   #78
            loss += model.train_on_batch(inputs, targets)   #79
            timestep += 1   #80
            current_state = next_state   #81
        # PRINTING THE TRAINING RESULTS FOR EACH EPOCH   #82
        print("\n")   #83
        print("Epoch: {:03d}/{:03d}".format(epoch, number_epochs))   #84
        print("Total Energy spent with an AI: {:.0f}".format(env.total_energy_ai))   #85
        print("Total Energy spent with no AI: {:.0f}".format(env.total_energy_noai))   #86
        # SAVING THE MODEL   #87
        model.save("model.h5")   #88 

第 39 行:我们将env.train对象变量(这是我们environment对象的一个变量)设置为之前输入的train变量的值,当然这个值为True,表示我们确实处于训练模式。

第 40 行:我们从brain对象中获取模型。这个模型包含神经网络的完整架构,以及它的优化器。它还具有额外的实用工具,例如saveload方法,分别允许我们在训练后保存权重,或随时加载它们。

第 41 行:如果我们处于训练模式...

第 43 行:我们开始主训练for循环,迭代训练周期从 1 到 100。

第 45 行:我们将总奖励(训练过程中累积的总奖励)设置为0

第 46 行:我们将损失设置为00因为损失将是一个float类型)。

第 47 行:我们将训练的起始月份,称为new_month,设置为 0 到 11 之间的一个随机整数。例如,如果随机整数是 2,我们就从 3 月开始训练。

第 48 行:通过调用我们在步骤 1 中构建的Environment类的env对象中的reset方法,我们将从new_month重新设置环境。

第 49 行:我们将game_over变量设置为False,因为我们正处于允许的服务器温度范围内。

第 50 行:通过调用我们在步骤 1 中构建的Environment类的env对象中的observe方法,我们只获得当前的状态,这就是我们的起始状态。

第 51 行:我们将第一个timestep设置为0。这是训练的第一分钟。

第 53 行:我们开始while循环,该循环将迭代整个周期内的所有时间步(分钟),即 5 个月。因此,我们将迭代5 * 30 * 24 * 60分钟;即 216,000 个时间步。

然而,如果在这些时间步内,我们超出了允许的服务器温度范围(也就是说,如果game_over = 1),那么我们会停止这一轮并开始一个新的周期。

第 55 行到 61 行确保 AI 在 30%的时间内执行随机动作。这是探索。其窍门在于从 0 到 1 之间采样一个随机数字,如果这个数字在 0 到 0.3 之间,AI 就会执行一个随机动作。这意味着 AI 将在 30%的时间内执行随机动作,因为这个采样数字有 30%的机会落在 0 到 0.3 之间。

第 55 行:如果从 0 到 1 之间采样的数字小于...

第 56 行:...我们执行一个从 0 到 4 的随机动作索引。

第 57 行:既然我们刚刚执行了一个动作,我们就计算方向和耗费的能量;记住它们是Environment类的update_env方法所需的参数,我们稍后会调用该方法来更新环境。AI 通过检查动作是否低于或高于 2 的方向边界来区分两种情况。如果动作低于 2 的方向边界,意味着 AI 正在冷却服务器...

第 58 行:...则加热方向为-1(冷却)。

第 59 行和 60 行:否则,加热方向为+1(加热)。

第 61 行:我们计算 AI 在服务器上消耗的能量,根据假设 2,计算方法是:

|动作 - 方向边界| * 温度步长 = |动作 - 2| * 1.5 焦耳

例如,如果动作是 4,那么 AI 将服务器加热 3°C,因此根据假设 2,消耗的能量是 3 焦耳。我们检查到确实有|4-2|*1.5 = 3。

第 63 行:现在我们通过推理来执行动作,即直接根据我们 AI 的预测进行。推理从else语句开始,这对应于第 55 行的if语句。这个else语句对应的情况是抽样的数字介于 0.3 和 1 之间,这种情况发生的概率是 70%。

第 64 行:通过调用我们model对象的predict方法(predictModel类的一个预构建方法),我们从 AI 模型中获得五个预测的 Q 值。

第 65 行:使用numpy中的argmax函数,我们从第 64 行预测的五个 Q 值中选择 Q 值最大的动作。

第 66 至 70 行:我们做的与第 57 至 61 行完全相同,只不过这次是使用推理执行的动作。

第 72 行:现在我们已经准备好更新环境。我们调用第 1 步中Environment类中制作的update_env方法,输入加热方向、AI 消耗的能量以及我们当前在while循环中特定时刻的月份。我们得到的返回值是下一个状态、获得的奖励,以及游戏是否结束(即是否超出了服务器温度的最佳范围)。

第 73 行:我们将最后收到的奖励加到总奖励中。

第 75 行:通过调用我们在第 3 步构建的DQN类中的dqn对象的remember方法,我们将新的过渡[[当前状态动作奖励下一个状态],游戏结束]存储到内存中。

第 77 行:通过调用我们在第 3 步构建的DQN类中的dqn对象的get_batch方法,我们创建了两个独立的inputstargets批次,每个批次包含 512 个元素(因为batch_size = 512)。

第 79 行:通过调用我们model对象的train_on_batch方法(train_on_batchModel类的一个预构建方法),我们计算预测值与目标值之间的损失误差,覆盖整个批次。提醒一下,这个损失误差是均方误差损失。然后在同一行中,我们将这个损失误差加到当前周期的总损失中,以便我们可以查看在训练过程中这个总损失如何随周期变化。

第 80 行:我们增加了timestep

第 81 行:我们更新当前状态,它变成了新的达到的状态。

第 83 行:我们打印一个新行,以便将训练结果分开,这样我们就能更容易地查看它们。

第 84 行:我们打印出当前已达到的周期(即我们在主训练for循环的这一特定时刻所处的周期)。

第 85 行:我们打印出 AI 在该特定轮次(即当前for循环所处的那一轮)所消耗的总能量。

第 86 行:我们打印出服务器集成冷却系统在该特定轮次(即同一轮训练中)所消耗的总能量。

第 88 行:我们在训练结束时保存模型的权重,以便将来加载这些权重,随时使用我们预训练的模型来调节服务器的温度。

这就是没有使用早停技术训练我们的 AI 的过程;现在让我们来看一下需要做什么更改才能实现它。

早停

现在打开training_earlystopping.py文件。与之前的文件进行对比,代码第 1 到 40 行是一样的。然后,在最后的代码部分“TRAINING THE AI”中,我们执行相同的过程,并增加了早停技术。提醒一下,早停技术的作用是在性能不再提升时停止训练,这可以通过两种方式来评估:

  1. 如果某一轮的总奖励在多轮训练中不再显著增加。

  2. 如果某一轮的总损失在多轮训练中不再显著下降。

让我们看看如何做到这一点。

首先,我们在主要的训练for循环之前引入四个新变量:

# TRAINING THE AI   #38
env.train = train   #39
model = brain.model   #40
early_stopping = True   #41
patience = 10   #42
best_total_reward = -np.inf   #43
patience_count = 0   #44
if (env.train):   #45
    # STARTING THE LOOP OVER ALL THE EPOCHS (1 Epoch = 5 Months)   #46
    for epoch in range(1, number_epochs):   #47 

第 41 行:我们引入了一个新变量early_stopping,如果我们决定激活早停技术,即当性能不再提升时停止训练,它的值为True

第 42 行:我们引入了一个新变量patience,它表示在没有性能提升的情况下,我们等待多少轮才停止训练。在这里,我们选择10轮的耐心值,这意味着如果某一轮的最佳总奖励在接下来的 10 轮内没有提高,我们将停止训练。

第 43 行:我们引入了一个新变量best_total_reward,它记录了一轮训练中获得的最佳总奖励。如果在 10 轮训练内没有超过这个最佳总奖励,则训练停止。它的初始值为-np.inf,代表负无穷。这只是一个技巧,表示最初没有什么比这个最佳总奖励还要低的。然后,一旦我们得到第一轮训练的总奖励,best_total_reward就变为该总奖励。

第 44 行:我们引入了一个新变量patience_count,它是一个从0开始的计数器,每当某一轮的总奖励没有超过最佳总奖励时,patience_count加 1。如果patience_count达到 10(即耐心值),则停止训练。如果某一轮的总奖励超过最佳总奖励,patience_count会被重置为 0。

然后,主要的训练for循环与之前相同,但在保存模型之前,我们添加了以下内容:

 # EARLY STOPPING   #91
        if (early_stopping):   #32
            if (total_reward <= best_total_reward):   #93
                patience_count += 1   #94
            elif (total_reward > best_total_reward):   #95
                best_total_reward = total_reward   #96
                patience_count = 0   #97
            if (patience_count >= patience):   #98
                print("Early Stopping")   #99
                break   #100
        # SAVING THE MODEL   #101
        model.save("model.h5")   #102 

第 92 行:如果early_stopping变量为True,表示早停技术被激活…

第 93 行:如果当前周期的总奖励(我们仍然处于主训练for循环中,该循环会迭代周期)低于到目前为止获得的最佳周期总奖励…

第 94 行:...我们将patience_count变量增加1

第 95 行:然而,如果当前周期的总奖励高于到目前为止获得的最佳周期总奖励…

第 96 行:...我们更新最佳总奖励,将其设置为当前周期的新的总奖励。

第 97 行:...我们将patience_count变量重置为0

第 98 行:然后,在一个新的if条件中,我们检查patience_count变量是否超过 10 的耐心阈值…

第 99 行:...我们打印Early Stopping

第 100 行:...然后我们通过break语句停止主训练for循环。

这就是全部内容。简单直观,对吧?现在你知道如何实现早停法了。

在执行代码后(稍后我会解释如何运行它),我们会看到我们的 AI 在训练期间表现良好,大部分时间消耗的能量比服务器的集成冷却系统还少。但这只是训练阶段;现在我们需要看看 AI 在新的 1 年模拟中的表现如何。这就是我们下一个也是最后一个 Python 文件的作用所在。

步骤 5 – 测试 AI

现在我们需要在全新的环境中测试我们的 AI 表现。为此,我们在推理模式下运行一个 1 年的模拟,这意味着在任何时刻都不会进行训练。我们的 AI 只会对整个 1 年的模拟进行预测。然后,借助我们的环境对象,最终我们将能够看到 AI 在整个 1 年中消耗的总能量,以及服务器集成冷却系统在同一年中本应消耗的总能量。最后,我们通过计算两者的相对差异(以%表示)来比较这两种总能量消耗,准确地显示出 AI 节省的总能量。系好安全带,最终结果马上揭晓!

在 AI 蓝图方面,对于测试实现,我们几乎和训练实现过程相同,唯一不同的是这次我们不需要创建brain对象或DQN模型对象;当然,我们也不会在一些训练周期上运行深度 Q 学习过程。然而,我们确实需要创建一个新的environment对象,并且,我们将不会创建一个brain,而是将我们的人工大脑加载到其从之前训练中获得的预训练权重。让我们来看看 AI 框架/蓝图最后部分的最终子步骤:

  • 步骤 5-1:通过创建Environment类的对象来构建一个新的环境。

  • 步骤 5-2:加载人工大脑,并从之前的训练中加载其预训练权重。

  • 步骤 5-3:选择推理模式。

  • 步骤 5-4:开始为期 1 年的模拟。

  • 步骤 5-5:在每次迭代中(每分钟),我们的 AI 仅执行根据其预测得出的动作,不进行任何探索或深度 Q 学习训练。

实现非常简单易懂。它实际上与训练文件相同,只是:

  1. 不再从Brain类创建brain对象,而是加载训练得到的预训练权重。

  2. 不再进行 100 个 epochs 的 5 个月周期训练,而是对一个 12 个月的周期运行推理循环。在这个推理循环中,你会看到与训练for循环的推理部分完全相同的代码。你能搞定的!

请查看以下代码中的完整测试实现:

# AI for Business - Minimize cost with Deep Q-Learning
# Testing the AI

# Installing Keras
# conda install -c conda-forge keras

# Importing the libraries and the other python files
import os
import numpy as np
import random as rn
from keras.models import load_model
import environment

# Setting seeds for reproducibility
os.environ['PYTHONHASHSEED'] = '0'
np.random.seed(42)
rn.seed(12345)

# SETTING THE PARAMETERS
number_actions = 5
direction_boundary = (number_actions - 1) / 2
temperature_step = 1.5

# BUILDING THE ENVIRONMENT BY SIMPLY CREATING AN OBJECT OF THE ENVIRONMENT CLASS
env = environment.Environment(optimal_temperature = (18.0, 24.0), initial_month = 0, initial_number_users = 20, initial_rate_data = 30)

# LOADING A PRE-TRAINED BRAIN
model = load_model("model.h5")

# CHOOSING THE MODE
train = False

# RUNNING A 1 YEAR SIMULATION IN INFERENCE MODE
env.train = train
current_state, _, _ = env.observe()
for timestep in range(0, 12 * 30 * 24 * 60):
    q_values = model.predict(current_state)
    action = np.argmax(q_values[0])
    if (action - direction_boundary < 0):
        direction = -1
    else:
        direction = 1
    energy_ai = abs(action - direction_boundary) * temperature_step
    next_state, reward, game_over = env.update_env(direction, energy_ai, int(timestep / (30 * 24 * 60)))
    current_state = next_state

# PRINTING THE TRAINING RESULTS FOR EACH EPOCH
print("\n")
print("Total Energy spent with an AI: {:.0f}".format(env.total_energy_ai))
print("Total Energy spent with no AI: {:.0f}".format(env.total_energy_noai))
print("ENERGY SAVED: {:.0f} %".format((env.total_energy_noai - env.total_energy_ai) / env.total_energy_noai * 100)) 

一切与之前基本相同,只是我们删除了与训练相关的部分。

演示

鉴于我们有不同的文件,请确保理解有四种可能的运行程序的方式:

  1. 不启用 dropout 并且不启用 early stopping

  2. 不启用 dropout 并启用 early stopping

  3. 启用 dropout 并且不启用 early stopping

  4. 启用 dropout 并启用 early stopping

然后,对于这四种组合,运行的方式是相同的:我们首先执行训练文件,然后执行测试文件。在本演示部分,我们将执行第 4 个选项,包含 dropout 和 early stopping。

那么,我们该如何运行呢?我们有两个选项:使用或不使用 Google Colab。

我将解释如何在 Google Colab 上操作,并且我还会给你一个 Google Colab 文件,你只需点击播放按钮即可。对于那些希望在自己的 Python IDE 或终端上执行此操作的用户,我将解释如何进行。这很简单;你只需要从 GitHub 下载主仓库,然后在 Python IDE 中设置正确的工作目录文件夹,即Chapter 11文件夹,然后按此顺序运行以下两个文件:

  1. training_earlystopping.py,你应该确保在第 9 行导入brain_dropout。这将执行训练,你需要等待直到完成(大约需要 10 分钟)。

  2. testing.py,它将在一整年的数据上测试模型。

现在,回到 Google Colab。首先,打开一个新的 Colaboratory 文件,并将其命名为Deep Q-Learning for Business。然后将来自 GitHub 的Chapter 11文件夹中的所有文件添加到该 Colaboratory 文件中,如下所示:

图 11:Google Colab - 第 1 步

不幸的是,手动添加不同的文件并不容易。你只能通过使用os库来实现这一点,但我们不会在这里处理它。相反,请将五个 Python 实现代码粘贴到我们 Colaboratory 文件的五个不同单元格中,顺序如下:

  1. 第一个单元格包含完整的environment.py实现。

  2. 第二个单元格包含完整的brain_dropout.py实现。

  3. 第三个单元格包含完整的dqn.py实现。

  4. 一个包含整个 training_earlystopping.py 实现的第四个单元格。

  5. 还有一个包含整个 testing.py 实现的最后一个单元格。

添加一些炫酷标题后的样子如下:

图 12:Google Colab – 第 2 步

图 13:Google Colab – 第 3 步

图 14:Google Colab – 第 4 步

图 15:Google Colab – 第 5 步

图 16:Google Colab – 第 6 步

现在,在按照顺序执行每个单元格(从第一个到第五个)之前,我们需要移除 Python 文件中的 import 命令。原因是现在这些实现都在单元格中,它们就像一个完整的 Python 实现,我们不需要在每个单元格中都导入相互依赖的文件。首先,移除训练文件中的以下三行:

图 17:Google Colab – 第 7 步

完成之后,结果是这样的:

图 18:Google Colab – 第 8 步

然后,由于我们移除了这些导入,我们也需要在创建对象时移除 environmentbraindqn 这三个文件名:

首先是环境

图 19:Google Colab – 第 9 步

然后是大脑

图 20:Google Colab – 第 10 步

最后是 dqn

图 21:Google Colab – 第 11 步

现在训练文件已经准备好。在测试文件中,我们只需移除两件事,分别是第 12 行的 environment 导入:

图 22:Google Colab – 第 12 步

以及第 25 行的 environment.

图 23:Google Colab – 第 13 步

就这样;现在一切准备就绪!你可以开始按顺序从上到下逐个点击每个单元格的播放按钮了。

首先,执行第一个单元格。执行后不会显示任何输出。这是正常的!

然后执行第二个单元格:

Using TensorFlow backend. 

执行后,你会看到输出 Using TensorFlow backend.

然后执行第三个单元格,执行后不会显示任何输出。

现在开始变得有点令人兴奋了!你即将执行训练,并实时跟踪训练表现。通过执行第四个单元格来实现。执行后,训练将启动,你应该看到以下结果:

图 24:输出结果

别担心那些警告,一切都按照预期运行。由于启用了早期停止,训练会在 100 个 epoch 之前很早就结束,通常在第 15 个 epoch 时:

图 25:第 15 个 epoch 的输出结果

注意,预训练的权重保存在文件中,文件名为 model.h5

图 26:model.h5 文件

训练结果看起来很有前景。大多数时候,AI 所消耗的能量比替代服务器的集成冷却系统要少。通过完整测试,检查一下在模拟一整年的情况下,情况是否仍然如此。

执行最后一个单元格,运行完成后(大约需要 3 分钟),你将在打印结果中看到 AI 节省的总能量消耗是……

Total Energy spent with an AI: 261985
Total Energy spent with no AI: 1978293
ENERGY SAVED: 87% 

AI 节省的总能源 = 87%

这节省了大量的能源!谷歌 DeepMind 在 2016 年也取得了类似令人印象深刻的成果。如果你搜索“DeepMind 减少谷歌冷却费用”,你会看到他们取得的成果是 40%。不错吧!当然,我们也要保持批判性:他们的服务器/数据中心环境比我们的服务器环境要复杂得多,参数也更多,所以即使他们拥有世界上最优秀的 AI 团队之一,他们的冷却费用也只能减少不到 50%。

我们的环境非常简单,如果你深入研究(我建议你这样做),你可能会发现用户和数据的变化,以及温度的变化,遵循的是均匀分布。因此,服务器的温度通常保持在最优温度范围内。AI 对此理解得很好,因此大多数时候它选择不采取任何行动,也不会改变温度,从而消耗的能量非常少。

我强烈建议你尝试调整你的服务器冷却模型;可以根据自己的喜好使其变得更加复杂,并尝试不同的奖励,看看是否能引发不同的行为。

即使我们的环境很简单,你也可以为自己的成就感到骄傲。重要的是你成功地为一个真实世界的商业问题构建了一个深度 Q 学习模型。环境本身并不重要;最重要的是你知道如何将一个深度强化学习模型与环境连接,并且如何在其中训练该模型。

现在,在你取得了自动驾驶汽车和这个商业应用的成功之后,你知道该如何做到了!

我们所构建的系统对于我们的商业客户来说非常出色,因为我们的 AI 将大幅降低他们的成本。记住,得益于我们的面向对象结构(使用类和对象),我们可以非常容易地将这个实现中为一个服务器创建的对象,直接移植到其他服务器上,这样最终我们就能降低整个数据中心的总能耗!这正是谷歌凭借其 DeepMind AI 的DQN模型节省数十亿美元能源成本的方式。

衷心祝贺你成功实现了这个新应用。你刚刚在 AI 技能上取得了巨大进步。

最后,这是承诺的带有完整实现的 Colaboratory 文件链接。你无需安装任何东西,Keras 和 NumPy 已经预安装好(这就是 Google Colab 的魅力!):

colab.research.google.com/drive/1KGAoT7S60OC3UGHNnrr_FuN5Hcil0cHk

在我们完成这一章并进入深度卷积 Q 学习的世界之前,让我给你提供一份关于建立深度强化学习模型时整体 AI 蓝图的有用回顾。

回顾 – 一般 AI 框架/蓝图

让我们回顾一下整个 AI 蓝图,这样你就可以将其打印出来并挂在墙上。

步骤 1:建立环境

  • 步骤 1-1:引入并初始化环境的所有参数和变量。

  • 步骤 1-2:创建一个方法,在 AI 执行动作后立即更新环境。

  • 步骤 1-3:创建一个方法,重置环境。

  • 步骤 1-4:创建一个方法,使我们可以随时获取当前状态、最后获得的奖励以及游戏是否结束。

步骤 2:建立大脑

  • 步骤 2-1:构建由输入状态组成的输入层。

  • 步骤 2-2:构建隐藏层,选择这些层的数量以及每层中的神经元数量,并将其与输入层以及彼此之间完全连接。

  • 步骤 2-3:建立与最后一个隐藏层完全连接的输出层。

  • 步骤 2-4:将完整的架构组装到一个模型对象中。

  • 步骤 2-5:使用均方误差损失函数和选择的优化器(推荐使用 Adam)来编译模型。

步骤 3:实现深度强化学习算法

  • 步骤 3-1:引入并初始化 DQN 模型的所有参数和变量。

  • 步骤 3-2:创建一个方法,用于在经验回放中构建记忆。

  • 步骤 3-3:创建一个方法,构建并返回两个批次,每个批次包含 10 个输入和 10 个目标。

步骤 4:训练 AI

  • 步骤 4-1:通过创建在步骤 1 中构建的 Environment 类的对象来建立环境。

  • 步骤 4-2:通过创建在步骤 2 中构建的 Brain 类的对象来建立人工大脑。

  • 步骤 4-3:通过创建在步骤 3 中构建的 DQN 类的对象来建立 DQN 模型。

  • 步骤 4-4:选择训练模式。

  • 步骤 4-5:通过一个 for 循环来开始训练,遍历选择的 epoch 数量。

  • 步骤 4-6:在每一个 epoch 中,我们重复整个深度 Q 学习过程,同时 30% 的时间进行一些探索。

步骤 5:测试 AI

  • 步骤 5-1:通过创建在步骤 1 中构建的 Environment 类的对象来建立一个新的环境。

  • 步骤 5-2:通过加载来自先前训练的人工大脑及其预训练的权重来加载人工大脑。

  • 步骤 5-3:选择推理模式。

  • 步骤 5-4:开始模拟。

  • 步骤 5-5:在每次迭代(每分钟)中,我们的 AI 只执行其预测结果产生的动作,不进行任何探索或深度 Q 学习训练。

总结

在本章中,你重新应用了深度 Q 学习来解决一个新的商业问题。你需要找到最优策略来冷却和加热服务器。在开始定义 AI 策略之前,你需要对环境做出一些假设,例如温度是如何计算的。作为你的人工神经网络(ANN)的输入,你有关于服务器在任何给定时刻的信息,比如温度和数据传输情况。作为输出,你的 AI 预测是否应该将服务器冷却或加热一定的量。奖励是相较于其他传统冷却系统所节省的能量。你的 AI 能够节省 87%的能量。

第十二章:深度卷积 Q 学习

现在你已经了解了人工神经网络ANNs)是如何工作的,你可以开始学习一个非常有用的工具,它主要用于处理图像——卷积神经网络CNNs)。简单来说,CNN 使得你的 AI 可以像有眼睛一样实时“看到”图像。

我们将通过以下步骤来处理它们:

  1. CNN 的应用是什么?

  2. CNN 是如何工作的?

  3. 卷积

  4. 最大池化

  5. 扁平化

  6. 完全连接

一旦你理解了这些步骤,你就能理解 CNN,以及它们如何在深度卷积 Q 学习中发挥作用。

CNN 的应用是什么?

CNN 主要用于图像或视频,有时也用于处理文本以解决自然语言处理NLP)问题。它们通常用于物体识别,例如预测一张图片或视频中是猫还是狗。它们还经常与深度 Q 学习一起使用(我们稍后将讨论),当环境返回自身的二维状态时,例如当我们尝试构建一个可以读取周围摄像头输出的自动驾驶汽车时。

记得在第九章,“使用人工大脑走向专业——深度 Q 学习”中,我们预测了房价。作为输入,我们有定义房子的所有值(面积、年龄、卧室数量等),作为输出,我们有房子的价格。在 CNN 的情况下,事情非常相似。例如,如果我们想用 CNN 解决同样的问题,我们将以房子的图像作为输入,房价作为输出。

这个图应该能说明我的意思:

图 1:输入图像 – CNN – 输出标签

如你所见,输入的是一张图像,图像流经 CNN 并作为输出产生结果。在这个图中,输出是与图像对应的类别。什么是类别?例如,如果我们想预测输入的图像是笑脸还是悲伤的面孔,那么一个类别就是笑脸,另一个类别就是悲伤面孔。我们的输出应该能够正确判断输入图像对应的类别。

说到快乐和悲伤的面孔,这里有一个更详细的图示:

图 2:两个不同的类别预测(快乐或悲伤)

在前面的例子中,我们通过 CNN 处理了两张图像。第一张是笑脸,另一张是悲伤的面孔。正如我之前提到的,我们的网络预测图像是快乐面孔还是悲伤面孔。

我能想象你现在在想什么:这一切是怎么运作的?我们所说的这个黑盒子——CNN,里面到底是什么?我将在接下来的章节中回答这些问题。

CNN 是如何工作的?

在我们深入探讨 CNN 的结构之前,我们需要理解几个要点。我将通过一个问题引导你了解第一个要点:一个彩色 RGB 图像有多少个维度?

答案可能会让你吃惊:是 3!

为什么?因为每个 RGB 图像实际上由三张 2D 图像表示,每张图像对应 RGB 结构中的一个颜色。因此,红色对应一张图像,绿色对应一张图像,蓝色对应一张图像。灰度图像只有 2D,因为它们只由一个灰度值表示,没有颜色。下面的图示应该能让这一点更加清晰:

图 3:RGB 图像与黑白图像的对比

正如你所看到的,一张彩色图像由 3D 数组表示。每种颜色在图像中有自己的层,这层被称为通道。而灰度(黑白)图像只有一个通道,因此它是一个 2D 数组。

正如你所知,图像是由像素组成的。每个像素都由一个介于 0 到 255 之间的数值表示,其中 0 表示关闭的像素,255 表示完全亮起的像素。理解这一点很重要:当我们说一个像素的值是(255,255,0)时,这意味着这个像素在红色和绿色通道上完全亮起,而在蓝色通道上是关闭的。

从现在起,为了更好地理解一切,我们将处理非常简单的图像。实际上,我们的图像将是灰度图像(1 个通道,2D),像素将是完全亮起的或关闭的。为了让图像更容易读取,我们将关闭的像素(黑色)赋值为 1,完全亮起的像素(白色)赋值为 0。

回到悲伤和快乐面孔的例子,这就是我们用 2D 数组表示的快乐面孔的样子:

图 4:像素表示

正如你所看到的,我们有一个数组,其中0表示白色像素,1表示黑色像素。右边的图片是我们用数组表示的笑脸。

现在我们已经理解了基础知识,并且简化了问题,我们准备好迎接卷积神经网络(CNN)的挑战了。为了完全理解它们,我们需要将学习内容分为组成 CNN 的四个步骤:

  1. 卷积

  2. 最大池化

  3. 扁平化

  4. 全连接

现在,我们将逐一了解这四个步骤。

步骤 1 – 卷积

这是每个卷积神经网络(CNN)中的第一个关键步骤。在卷积操作中,我们将一种叫做特征检测器的东西应用于输入的图像。为什么我们要这么做呢?因为所有图像都包含一些特定的特征,这些特征定义了图像中的内容。例如,要识别哪个面部表情是悲伤的,哪个是快乐的,我们需要理解嘴巴的形状,这就是图像中的一个特征。从图示中理解这个更为清晰:

图 5:步骤 1 – 卷积(1/5)

在上面的图中,我们应用了一个特征检测器,也就是一个滤波器,作用于我们输入的笑脸图像。如你所见,滤波器是一个包含一些值的二维数组。当我们将这个特征检测器应用到图像上时,它覆盖了(在这个例子中是 3 x 3 网格)。我们检查图像的这一部分有多少像素与滤波器的像素匹配。然后,我们把这个数字放入一个新的二维数组,称为特征图。换句话说,图像的某一部分与特征检测器匹配得越多,我们就将越高的数字放入特征图中。

接下来,我们滑动特征检测器遍历整张图像。在下一次迭代中,会发生这样的情况:

图 6:步骤 1 – 卷积(2/5)

如你所见,我们将滤波器向右滑动了一格。这次,滤波器和图像中的这一部分有一个像素匹配。这就是为什么我们在特征图中放置1的原因。

你觉得当我们碰到这张图像的边界时会发生什么?你会怎么做?我将通过这两个图示给你展示:

图 7:步骤 1 – 卷积(3/5)

图 8:步骤 1 – 卷积(4/5)

这里,我们正好遇到了这种情况:在第一张图像中,我们的滤波器碰到了边界。结果我们的特征检测器会直接跳跃到下一行。

如果我们只有一个滤波器,卷积的所有魔法都无法奏效。实际上,我们使用多个滤波器,产生多个不同的特征图。这组特征图被称为卷积层,或者卷积神经层。下面是一个总结图:

图 9:步骤 1 – 卷积(5/5)

这里,我们可以看到一张输入图像,已经应用了多个滤波器。所有这些滤波器一起创建了一个卷积层,由多个特征图组成。这是构建卷积神经网络(CNN)的第一步。

现在我们理解了卷积操作,我们可以继续进行另一个重要的步骤——最大池化。

步骤 2 – 最大池化

在卷积神经网络(CNN)中,这一步骤负责降低每个特征图的大小。在处理神经网络时,我们不希望输入数据过多,否则我们的网络由于复杂性过高,无法正常学习。因此,需要引入一种叫做最大池化的尺寸缩减方法。它让我们在不丢失重要特征的情况下减少大小,并且使特征对位移(平移和旋转)具有部分不变性。

从技术上讲,最大池化算法也基于一个数组滑动整个特征图。在这种情况下,我们不是在寻找任何特征,而是寻找特征图中特定区域的最大值。

让我通过这个图形给你展示我是什么意思:

图 10:步骤 2 – 最大池化(1/5)

在这个例子中,我们正在使用之前卷积步骤得到的特征图,然后通过最大池化进行处理。正如你所看到的,我们有一个大小为 2 x 2 的窗口,寻找它覆盖部分特征图中的最大值。在这个例子中,最大值是 1。

你能预测下一次迭代会发生什么吗?

正如你可能已经猜到的,这个窗口将滑动到右边,尽管方式与之前稍有不同。它的移动方式如下:

图 11:步骤 2 – 最大池化(2/5)

这个窗口跳跃到右边,我希望你记得,这与卷积步骤不同,在卷积步骤中,特征检测器一次滑动一个单元格。在这个例子中,最大值也是 1,因此我们在池化特征图中写下1

这次当我们碰到特征图的边界时,会发生什么呢?事情再次看起来与之前有所不同。发生的情况是:

图 12:步骤 2 – 最大池化(3/5)

窗口穿越边界,寻找特征图中仍然在最大池化窗口内的部分的最大值。再次地,最大值是 1。

那现在会发生什么呢?毕竟,右边已经没有空间可走了。而且底部左侧只有一行可以进行最大池化。算法会这样做:

图 13:步骤 2 – 最大池化(4/5)

如我们所见,它再次穿越边界,寻找窗口内部的最大值。在这个例子中,最大值是 0。这个过程会一直重复,直到窗口碰到特征图的右下角。为了回顾一下我们目前的卷积神经网络长什么样,可以看看以下的图示:

图 14:步骤 2 – 最大池化(5/5)

我们输入了一个笑脸,然后通过卷积获得了许多特征图,这些图被称为卷积层。现在我们已经将所有特征图通过最大池化处理,并获得了许多池化特征图,这些图合起来被称为池化层

现在我们可以继续进行下一步,这将让我们将池化层输入神经网络。这个步骤叫做扁平化

步骤 3 – 扁平化

这是一个非常简短的步骤。正如名字所示,我们将所有池化后的特征图从二维数组转换为一维数组。正如我之前提到的,这样做能让我们轻松地将图像输入神经网络。那么,我们到底是如何实现这一点的呢?以下图示应该能帮助你理解:

图 15:步骤 3 – 扁平化(1/3)

现在我们回到之前得到的池化特征图。为了扁平化它,我们从左上角开始获取像素值,一直到右下角。像这样的操作会返回一个一维数组,包含与我们最初的二维数组相同的值。

但记住,我们并不是只有一个池化特征图,而是有一整层池化特征图。你认为我们应该怎么处理这些呢?

答案很简单:我们将整个层压缩成一个单一的 1D 扁平数组,一个接一个地放入池化特征图。为什么必须是 1D?因为人工神经网络(ANNs)只接受 1D 数组作为输入。传统神经网络的所有层都是 1D 的,这意味着输入也必须是 1D。因此,我们将所有池化特征图扁平化,像这样:

图 16:步骤 3 – 扁平化 (2/3)

我们已经将整个层转换为一个单一的扁平化 1D 数组。我们将很快把这个数组用作传统神经网络的输入。

首先,让我们回顾一下当前模型的结构:

图 17:步骤 3 – 扁平化 (3/3)

所以,我们有一个卷积层、池化层,以及一个新添加的扁平化 1D 层。现在我们可以回到经典的人工神经网络(ANN),即全连接神经网络,并将这个最后的层视为该网络的输入。这引领我们进入最后一步,即全连接

步骤 4 – 全连接

创建 CNN 的最后一步是将其连接到经典的全连接神经网络。记住,我们已经有了一个 1D 数组,简明地告诉我们图像的外观,那么为什么不直接将它作为输入传递给全连接神经网络呢?毕竟,是后者能够进行预测。

这正是我们接下来要做的,就像这样:

图 18:步骤 4 – 全连接

扁平化后,我们将这些返回的值直接输入到全连接神经网络中,然后得到预测值——输出值。

你可能会想,现在反向传播阶段是如何工作的。在 CNN 中,反向传播不仅更新全连接神经网络中的权重,还更新卷积步骤中使用的滤波器。最大池化和扁平化步骤保持不变,因为那里没有需要更新的内容。

总之,CNN 会寻找一些特定的特征。这也是它们主要在处理图像时使用的原因,因为在图像处理中,特征的搜索至关重要。例如,当尝试识别一个悲伤和一个开心的面孔时,CNN 需要理解哪个嘴巴的形状表示悲伤面孔,哪个表示开心面孔。为了得到输出,CNN 必须执行以下步骤:

  1. 卷积 – 将滤波器应用于输入图像。这个操作将会找到我们 CNN 所需的特征,并将它们保存在特征图中。

  2. 最大池化 – 通过在给定区域内取最大值,来降低特征图的大小,并将这些值保存在一个新的数组中,称为池化特征图。

  3. 扁平化 – 将整个池化层(所有池化特征图)转换为 1D 向量。这将允许我们将这个向量输入到神经网络中。

  4. 全连接 – 创建一个神经网络,它将扁平化的池化层作为输入,并返回我们想要预测的值。这个最后的步骤使我们能够进行预测。

深度卷积 Q 学习

在深度 Q 学习(第九章人工大脑进阶——深度 Q 学习)的章节中,我们的输入是定义环境状态的编码值的向量。当处理图像或视频时,编码向量并不是描述状态(输入帧)的最佳输入方式,因为编码向量不能保留图像的空间结构。空间结构非常重要,因为它为我们提供更多信息,帮助预测下一个状态,而预测下一个状态对于 AI 学习正确的下一步至关重要。

因此,我们需要保留空间结构。为了做到这一点,我们的输入必须是 3D 图像(对于像素数组来说是 2D,再加上一个额外的维度来表示颜色,正如本章开头所示)。例如,如果我们训练一个 AI 来玩电子游戏,那么输入就是游戏屏幕本身的图像,完全就是人类玩游戏时所看到的内容。

按照这个类比,AI 就像是拥有了人类的眼睛;它在玩游戏时观察屏幕上的输入图像。这些输入图像进入一个 CNN(人类的眼睛),它检测每一幅图像的状态。然后,它们通过池化层进行前向传播,在池化层中应用最大池化。接着,池化层会被展平为 1D 向量,作为我们深度 Q 学习网络的输入(与第九章人工大脑进阶——深度 Q 学习中的网络完全相同)。最终,执行相同的深度 Q 学习过程。

以下图示展示了深度卷积 Q 学习在著名游戏《毁灭战士》中的应用:

图 19:深度卷积 Q 学习用于《毁灭战士》

总结来说,深度卷积 Q 学习与深度 Q 学习相同,唯一的不同是输入现在是图像,并且在完全连接的深度 Q 学习网络的开始部分加入了 CNN,用于检测这些图像的状态。

总结

你已经了解了另一种类型的神经网络——卷积神经网络。

我们已经确定,这个网络主要用于图像,并在这些图像中搜索特定的特征。它使用了三步是 ANNs 没有的额外步骤:卷积,用于搜索特征;最大池化,用于缩小图像大小;以及展平,将 2D 图像展平为 1D 向量,以便将其输入到神经网络中。

在下一章,你将构建一个深度卷积 Q 学习模型,来解决一个经典的游戏问题:贪吃蛇。

第十三章

游戏 AI – 成为 Snake 大师

这是最后一章实用章节;恭喜你完成了前面的章节!我希望你真的很享受这些内容。现在,让我们暂时放下商业问题和自动驾驶汽车。通过玩一个名为 Snake 的流行游戏来玩得开心,同时制作一个能够自学的 AI 来玩这个游戏!

这正是我们在这一章要做的。我们将实现的模型称为深度卷积 Q 学习,使用 卷积神经网络CNN)。

我们的 AI 并不完美,也不会填满整个地图,但经过一些训练后,它将开始以与人类相当的水平进行游戏。

让我们通过先看看这个游戏是什么样子以及目标是什么,来解决这个问题。

待解决的问题

首先,让我们来看一下游戏本身:

图 1:蛇游戏

看起来有点眼熟吗?

我相当确信它会做到;每个人至少玩过一次 Snake。

这个游戏非常简单;它由一条蛇和一个苹果组成。我们控制蛇,目标是吃尽可能多的苹果。

听起来简单吗?嗯,其实有一个小陷阱。每当我们的蛇吃掉一个苹果时,蛇会长大一格。这意味着游戏在开始时异常简单,但它会逐渐变得更加困难,直到成为一个策略性游戏。

此外,当控制我们的蛇时,我们不能撞到自己,也不能撞到棋盘的边界。这会导致我们输掉游戏,这也是非常容易预见的。

现在我们已经理解了问题,我们可以进入创建 AI 时的第一步——构建环境!

构建环境

这一次,与本书中的其他一些实用部分不同,我们不需要指定任何变量或做出任何假设。我们可以直接进入每个深度 Q 学习项目中的三个关键步骤:

  1. 定义状态

  2. 定义动作

  3. 定义奖励

让我们开始吧!

定义状态

在之前的每个例子中,我们的状态都是一个 1D 向量,表示定义环境的某些值。例如,对于我们的自动驾驶汽车,我们有来自汽车周围三个传感器和汽车位置的信息。所有这些信息都被放入一个 1D 数组中。

但是,如果我们想让它看起来更真实一点呢?如果我们希望 AI 能像我们一样通过相同的来源来查看并收集信息呢?好吧,这就是我们在本章要做的。我们的 AI 将看到与我们玩 Snake 时完全相同的棋盘!

游戏的状态应该是一个 2D 数组,代表游戏的棋盘,完全与我们看到的情况相同。

这个解决方案有一个问题。看一下以下图片,看看你是否能回答这个问题:现在我们的蛇正朝哪个方向移动?

图 2:蛇游戏

如果你说“我不知道”,那你完全正确。

基于单一的画面,我们无法判断我们的蛇在朝哪个方向移动。因此,我们需要堆叠多个图像,然后将它们一起输入到卷积神经网络中。这样,我们将得到三维状态,而不是二维状态。

所以,简单回顾一下:

图 3:AI 视觉

我们将有一个三维数组,包含一个接一个堆叠的游戏画面,其中最上面的画面是我们游戏中获得的最新画面。现在,我们可以清楚地看到我们的 AI 在朝哪个方向移动;在这个例子中,它是向上走,朝着苹果走去。

现在我们已经定义了状态,我们可以迈出下一步:定义动作!

定义动作

当我们在手机或网站上玩贪吃蛇时,有四个可供我们选择的动作:

  1. 向上走

  2. 向下走

  3. 向右走

  4. 向左走

然而,如果我们所采取的动作需要蛇做出 180° 的掉头,那么游戏将阻止这个动作,蛇将继续朝着当前的方向前进。

在前面的示例中,如果我们选择动作 2——向下走——我们的蛇仍然会继续向上走,因为向下走是不可能的,因为蛇无法直接做出 180° 的掉头。

值得注意的是,所有这些动作都是相对于棋盘的,而不是相对于蛇的;它们不会受到蛇当前运动方向的影响。向上、向下、向右或向左的动作,始终是相对于棋盘的,不是相对于蛇当前的运动方向。

好吧,现在你可能属于以下两种情况之一,关于我们在 AI 中建模的动作:

  1. 我们可以将这四个相同的动作用于我们的 AI。

  2. 我们不能使用这些相同的动作,因为阻止某些动作会让我们的 AI 感到困惑。相反,我们应该发明一种方法,告诉蛇向左走、向右走,或者继续前进。

事实上,我们确实可以将这些相同的动作用于我们的 AI!

为什么这不会让我们的智能体感到困惑呢?那是因为,只要我们的 AI 智能体根据它选择的动作获得奖励,而不是根据蛇最终执行的动作获得奖励,那么深度 Q 学习就能有效工作,我们的 AI 将会理解,在上面的例子中,选择 向上走向下走 会导致相同的结果。

例如,假设 AI 控制的蛇当前正向左移动。它选择了动作 3,向右走;但是因为这将导致蛇做出 180° 的掉头,所以蛇仍然继续向左走。假设该动作意味着蛇撞到了墙壁,并因此死亡。为了避免这对我们的智能体造成困扰,我们所需要做的就是告诉它,向右走的动作导致它撞墙,尽管蛇依然向左移动。

可以把它看作是教 AI 用手机上的实际按钮来玩游戏。如果你不断地按右键,试图让蛇在向左移动时转回去,游戏会一直忽略你不断要求它执行的不可能的操作,继续向左走,最终崩溃。这就是 AI 需要学习的全部内容。

这是因为,请记住,在深度 Q 学习中,我们只更新 AI 执行的动作的 Q 值。如果蛇正在向左走,而 AI 决定向右走并且蛇死了,它需要明白“向右走”这个动作导致了负奖励,而不是蛇向左移动的事实;即使选择“向左走”也会导致相同的结果。

我希望你明白,AI 可以使用我们在玩游戏时使用的相同动作。我们可以继续到下一个最终步骤——定义奖励!

定义奖励

最后一步很简单;我们只需要三个奖励:

  1. 吃苹果的奖励

  2. 死亡奖励

  3. 生存惩罚

前两个应该容易理解。毕竟,我们希望鼓励我们的代理尽可能多地吃苹果,因此我们会设置它的奖励为正值。准确来说:吃苹果 = +2

同时,我们希望避免让我们的蛇死亡。这就是为什么我们将奖励设置为负值。准确来说:死亡 = -1

然后是最终的奖励:生存惩罚。

那是什么,为什么它是必要的?我们必须让我们的代理相信,尽快收集苹果而不死掉是一个好主意。如果我们只定义已经有的这两个奖励,我们的代理将只是在整个地图上游荡,希望在某个时刻找到苹果。它不会明白它需要尽可能快地收集苹果。

这就是为什么我们引入生存惩罚。除非某个动作导致死亡或收集到苹果,否则它会稍微惩罚我们的 AI 每一个动作。这将向我们的代理展示,它需要尽快收集苹果,因为只有收集苹果的动作才能获得正奖励。那么,这个奖励应该有多大呢?嗯,我们不想惩罚它太多。准确来说:生存惩罚 = -0.03

如果你想调整这些奖励,这个奖励的绝对值应该始终相对较小,和其他奖励(死亡(-1)和收集苹果(+2))相比。

AI 解决方案

和往常一样,深度 Q 学习的 AI 解决方案由两部分组成:

  1. 大脑 – 将学习并采取行动的神经网络

  2. 经验重放记忆 – 存储我们经验的记忆;神经网络将从这些记忆中学习

现在让我们来处理这些吧!

大脑

这部分 AI 解决方案将负责教导、存储和评估我们的神经网络。为了构建它,我们将使用 CNN!

为什么选择 CNN?在解释其背后的理论时,我提到它们通常在“我们的环境作为状态返回图像”的情况下使用,而这正是我们在这里处理的内容。我们已经确认,游戏状态将是一个堆叠的 3D 数组,包含了最后几帧游戏画面。

在上一章中,我们讨论了 CNN 如何将一个 2D 图像作为输入,而不是一个堆叠的 3D 图像数组;但是你还记得这个图形吗?

图 4:RGB 图像

在这里,我告诉过你,RGB 图像是通过 3D 数组表示的,这些数组包含了图像的每个 2D 通道。听起来像是很熟悉吗?我们可以用完全相同的方法来解决我们的这个问题。就像 RGB 结构中的每个颜色一样,我们只需将每一帧游戏画面作为一个新的通道输入,这将为我们提供一个 3D 数组,然后我们可以将它输入到 CNN 中。

实际上,CNN 通常只支持 3D 数组作为输入。为了输入 2D 数组,你需要创建一个虚拟的单通道,将 2D 数组转换为 3D 数组。

关于 CNN 架构,我们将有两个卷积层,中间有一个池化层。一个卷积层将有 32 个 3x3 的滤波器,另一个卷积层将有 64 个 2x2 的滤波器。池化层将把尺寸缩小 2 倍,因为池化窗口的大小是 2x2。为什么选这样的架构?这是一个经典架构,在许多研究论文中都能找到,我随便选择了一个常用的架构,结果证明它的效果非常好。

我们的神经网络将有一个包含 256 个神经元的隐藏层和一个包含 4 个神经元的输出层;每个神经元对应我们可能的一个结果动作。

我们还需要为 CNN 设置两个最后的参数——学习率和输入形状。

学习率,在前面的例子中有使用,是一个指定我们在神经网络中更新权重幅度的参数。学习率太小,模型就学不起来;学习率太大,模型也无法学习,因为更新幅度太大,无法进行任何有效的优化。通过实验,我发现这个例子中合适的学习率是 0.0001。

我们已经达成一致,输入应该是一个包含游戏中最后几帧的 3D 数组。更准确地说,我们并不会从屏幕上读取像素,而是会读取表示游戏屏幕在某一时刻的直接 2D 数组。

正如你可能已经注意到的,我们的游戏是基于一个网格构建的。在我们使用的示例中,网格的大小是 10x10。然后,环境中有一个大小相同(10x10)的数组,数学上告诉我们棋盘的状态。例如,如果蛇的部分在一个格子里,那么我们就在相应的 2D 数组格子中放置值 0.5,我们将从中读取它。一个苹果在这个数组中的值为 1。

现在我们知道如何查看一帧,我们需要决定在描述当前游戏状态时将使用多少前一帧。2 帧应该足够,因为我们可以从中辨别蛇的方向,但为了确保,我们将使用 4 帧。

你能告诉我我们的 CNN 输入到底是什么形状吗?

它将是 10x10x4,这样我们就得到了一个 3D 数组!

经验回放内存

正如在深度 Q 学习的理论章节中定义的那样,我们需要一个存储在训练过程中收集的经验的记忆。

我们将存储以下数据:

  • 当前状态 – AI 执行动作时所在的游戏状态(即我们输入到 CNN 中的内容)

  • 动作 – 执行的动作

  • 奖励 – 通过在当前状态下执行该动作所获得的奖励

  • 下一状态 – 执行动作后发生了什么(状态是什么样的)

  • 游戏结束 – 关于我们是否输了的信息

此外,我们始终需要为每个经验回放内存指定两个参数:

  • 内存大小 – 我们的内存的最大大小

  • Gamma – 折扣因子,存在于贝尔曼方程中

我们将内存大小设置为 60,000,gamma 参数设置为 0.9。

这里还有一件事需要指定。

我告诉过你,我们的 AI 将从这个记忆中学习,确实如此;但 AI 并不会从整个记忆中学习。而是从其中提取的小批量进行学习。指定这个批量大小的参数叫做批量大小,在这个例子中,我们将其设置为 32。也就是说,我们的 AI 将从每次迭代中,从经验回放内存中提取一个大小为 32 的批量进行学习。

既然你已经理解了所有需要编码的内容,你可以开始动手了!

实现

你将在五个文件中实现整个 AI 代码和贪吃蛇游戏:

  1. environment.py文件 – 包含环境(贪吃蛇游戏)的文件

  2. brain.py文件 – 我们构建 CNN 的文件

  3. DQN.py – 构建经验回放内存的文件

  4. train.py – 我们将训练 AI 玩贪吃蛇的文件

  5. test.py – 我们将测试 AI 表现如何的文件

你可以在 GitHub 页面上找到所有这些内容,并且还可以找到一个预训练的模型。要访问该页面,请在主页面选择Chapter 13文件夹。

我们将按相同的顺序逐个查看每个文件。让我们开始构建环境吧!

第一步 – 构建环境

通过导入所需的库,开始这一步骤。像这样:

# Importing the libraries   #4
import numpy as np   #5
import pygame as pg   #6 

你只需要使用两个库:NumPy 和 PyGame。前者在处理列表或数组时非常有用,后者将用于构建整个游戏——绘制蛇和苹果,并更新屏幕。

现在,让我们创建Environment类,这个类将包含你需要的所有信息、变量和方法。为什么是类?因为这样后续会更方便。你可以从这个类的对象中调用特定的方法或变量。

你必须总是有的第一个方法是__init__方法,每当这个类的一个新对象在主代码中被创建时,都会调用它。要创建这个类以及__init__方法,你需要编写:

# Initializing the Environment class   #8
class Environment():   #9
    #10
    def __init__(self, waitTime):   #11
        #12
        # Defining the parameters   #13
        self.width = 880            # width of the game window   #14
        self.height = 880           # height of the game window   #15
        self.nRows = 10             # number of rows in our board   #16
        self.nColumns = 10          # number of columns in our board   #17
        self.initSnakeLen = 2       # initial length of the snake   #18
        self.defReward = -0.03      # reward for taking an action - The Living Penalty   #19
        self.negReward = -1\.        # reward for dying   #20
        self.posReward = 2\.         # reward for collecting an apple   #21
        self.waitTime = waitTime    # slowdown after taking an action   #22
        #23
        if self.initSnakeLen > self.nRows / 2:   #24
            self.initSnakeLen = int(self.nRows / 2)   #25
        #26
        self.screen = pg.display.set_mode((self.width, self.height))   #27
        #28
        self.snakePos = list()   #29
        #30
        # Creating the array that contains mathematical representation of the game's board   #31
        self.screenMap = np.zeros((self.nRows, self.nColumns))   #32
        #33
        for i in range(self.initSnakeLen):   #34
            self.snakePos.append((int(self.nRows / 2) + i, int(self.nColumns / 2)))   #35
            self.screenMap[int(self.nRows / 2) + i][int(self.nColumns / 2)] = 0.5   #36
            #37
        self.applePos = self.placeApple()   #38
        #39
        self.drawScreen()   #40
        #41
        self.collected = False   #42
        self.lastMove = 0   #43 

你创建了一个新的类,即Environment()类,并且它有一个__init__方法。这个方法只需要一个参数,即waitTime。然后,在定义方法后,创建一个常量列表,每个常量都在内联注释中解释。之后,进行一些初始化。你确保蛇的长度在第 24 和第 25 行不超过屏幕的一半,并且在第 27 行设置屏幕。需要注意的一点是,在第 32 行你创建了screenMap数组,它更数学化地表示了游戏板。单元格中的 0.5 表示这个单元格被蛇占据,而单元格中的 1 表示这个单元格被苹果占据。

在第 34 到第 36 行,你将蛇放置在屏幕中央,面朝上,然后在剩余的行中使用placeapple()方法(我们即将定义)放置一个苹果,绘制屏幕,设置苹果未被收集,并且设置没有上一次的移动。

这是第一个方法的完成。现在你可以继续下一个方法:

 # Building a method that gets new, random position of an apple
    def placeApple(self):
        posx = np.random.randint(0, self.nColumns)
        posy = np.random.randint(0, self.nRows)
        while self.screenMap[posy][posx] == 0.5:
            posx = np.random.randint(0, self.nColumns)
            posy = np.random.randint(0, self.nRows)

        self.screenMap[posy][posx] = 1

        return (posy, posx) 

这个简短的方法将苹果放置在screenMap数组中的一个新的随机位置。当我们的蛇收集到苹果时,需要使用这个方法来放置新的苹果。它还会返回新苹果的随机位置。

然后,你需要一个绘制所有内容的函数,让你可以看到:

 # Making a function that draws everything for us to see
    def drawScreen(self):

        self.screen.fill((0, 0, 0))

        cellWidth = self.width / self.nColumns
        cellHeight = self.height / self.nRows

        for i in range(self.nRows):
            for j in range(self.nColumns):
                if self.screenMap[i][j] == 0.5:
                    pg.draw.rect(self.screen, (255, 255, 255), (j*cellWidth + 1, i*cellHeight + 1, cellWidth - 2, cellHeight - 2))
                elif self.screenMap[i][j] == 1:
                    pg.draw.rect(self.screen, (255, 0, 0), (j*cellWidth + 1, i*cellHeight + 1, cellWidth - 2, cellHeight - 2))

        pg.display.flip() 

如你所见,这个方法的名称是drawScreen,它不接受任何参数。在这里,你只是清空整个屏幕,然后用白色的瓦片填充蛇所在的位置,用红色瓦片填充苹果所在的位置。最后,你用pg.display.flip()更新屏幕。

现在,你需要一个只更新蛇的位置,而不更新整个环境的函数:

 # A method that updates the snake's position
    def moveSnake(self, nextPos, col):

        self.snakePos.insert(0, nextPos)

        if not col:
            self.snakePos.pop(len(self.snakePos) - 1)

        self.screenMap = np.zeros((self.nRows, self.nColumns))

        for i in range(len(self.snakePos)):
            self.screenMap[self.snakePos[i][0]][self.snakePos[i][1]] = 0.5

        if col:
            self.applePos = self.placeApple()
            self.collected = True

        self.screenMap[self.applePos[0]][self.applePos[1]] = 1 

你可以看到,这个新方法接受两个参数:nextPoscol。前者告诉你在执行某个动作后蛇头的位置。后者则告诉你蛇是否通过这个动作收集了苹果。记住,如果蛇收集了苹果,那么蛇的长度会增加 1。如果深入这段代码,你可以看到这一点,但由于这对 AI 不太相关,我们在这里不做详细说明。你还可以看到,如果蛇收集了苹果,会在新位置生成一个新的苹果。

现在,让我们进入这段代码最重要的部分。你定义了一个更新整个环境的函数。它会移动你的蛇,计算奖励,检查你是否输了,并返回一个新的游戏帧。它的起始方式如下:

 # The main method that updates the environment
    def step(self, action):
        # action = 0 -> up
        # action = 1 -> down
        # action = 2 -> right
        # action = 3 -> left

        # Resetting these parameters and setting the reward to the living penalty
        gameOver = False
        reward = self.defReward
        self.collected = False

        for event in pg.event.get():
            if event.type == pg.QUIT:
                return

        snakeX = self.snakePos[0][1]
        snakeY = self.snakePos[0][0]

        # Checking if an action is playable and if not then it is changed to the playable one
        if action == 1 and self.lastMove == 0:
            action = 0
        if action == 0 and self.lastMove == 1:
            action = 1
        if action == 3 and self.lastMove == 2:
            action = 2
        if action == 2 and self.lastMove == 3:
            action = 3 

如你所见,这个方法被称为step,它接受一个参数:告诉你蛇想朝哪个方向移动的动作。方法定义下方的注释中,你可以看到每个动作代表的方向。

然后你重置一些变量。你将gameOver设置为False,因为这个布尔变量会告诉你在执行这个动作后是否输了。你将reward设置为defReward,这是生存惩罚;如果我们收集了苹果或稍后死掉,这个值可能会改变。

然后有一个for循环。它的作用是确保 PyGame 窗口不会冻结;这是 PyGame 库的要求。它必须存在。

snakeXsnakeY告诉你蛇头的位置。稍后,算法会使用这个信息来确定蛇头移动后的状态。

在最后几行,你可以看到阻止不可能动作的算法。简单回顾一下,不可能的动作是指蛇在原地做 180°转弯的动作。lastMove告诉你蛇现在的移动方向,并与action进行比较。如果这些导致了矛盾,action就会被设置为lastMove

仍然在这个方法中,你更新蛇的位置,检查是否结束游戏,并计算奖励,像这样:

 # Checking what happens when we take this action
        if action == 0:
            if snakeY > 0:
                if self.screenMap[snakeY - 1][snakeX] == 0.5:
                    gameOver = True
                    reward = self.negReward
                elif self.screenMap[snakeY - 1][snakeX] == 1:
                    reward = self.posReward
                    self.moveSnake((snakeY - 1, snakeX), True)
                elif self.screenMap[snakeY - 1][snakeX] == 0:
                    self.moveSnake((snakeY - 1, snakeX), False)
            else:
                gameOver = True
                reward = self.negReward 

在这里,你检查蛇头向上移动会发生什么。如果蛇头已经在最上面的一行(第0行),那么显然你已经输了,因为蛇撞到墙壁了。所以,reward被设置为negRewardgameOver被设置为True。否则,你检查蛇头前方的情况。

如果前方的格子已经包含了蛇身的一部分,那么你就输了。你在第一个if语句中检查这个情况,然后将gameOver设置为True,并将reward设置为negReward

如果前方的格子是一个苹果,那么你将reward设置为posReward。你还会通过调用你刚刚创建的那个方法来更新蛇的位置。

如果前方的格子是空的,那么你不会对reward做任何更新。你会再次调用相同的方法,但这次将col参数设置为False,因为蛇并没有吃到苹果。对于每个其他动作,你会执行相同的过程。我不会逐行讲解,但你可以看一下代码:

 elif action == 1:
            if snakeY < self.nRows - 1:
                if self.screenMap[snakeY + 1][snakeX] == 0.5:
                    gameOver = True
                    reward = self.negReward
                elif self.screenMap[snakeY + 1][snakeX] == 1:
                    reward = self.posReward
                    self.moveSnake((snakeY + 1, snakeX), True)
                elif self.screenMap[snakeY + 1][snakeX] == 0:
                    self.moveSnake((snakeY + 1, snakeX), False)
            else:
                gameOver = True
                reward = self.negReward

        elif action == 2:
            if snakeX < self.nColumns - 1:
                if self.screenMap[snakeY][snakeX + 1] == 0.5:
                    gameOver = True
                    reward = self.negReward
                elif self.screenMap[snakeY][snakeX + 1] == 1:
                    reward = self.posReward
                    self.moveSnake((snakeY, snakeX + 1), True)
                elif self.screenMap[snakeY][snakeX + 1] == 0:
                    self.moveSnake((snakeY, snakeX + 1), False)
            else:
                gameOver = True
                reward = self.negReward 

        elif action == 3:
            if snakeX > 0:
                if self.screenMap[snakeY][snakeX - 1] == 0.5:
                    gameOver = True
                    reward = self.negReward
                elif self.screenMap[snakeY][snakeX - 1] == 1:
                    reward = self.posReward
                    self.moveSnake((snakeY, snakeX - 1), True)
                elif self.screenMap[snakeY][snakeX - 1] == 0:
                    self.moveSnake((snakeY, snakeX - 1), False)
            else:
                gameOver = True
                reward = self.negReward 

以和向上移动时相同的方式处理每个动作。检查蛇是否没有撞到墙壁,检查蛇前方的情况并更新蛇的位置、rewardgameOver

这个方法还有两个步骤,我们直接跳到第一个:

 # Drawing the screen, updating last move and waiting the wait time specified
        self.drawScreen()

        self.lastMove = action

        pg.time.wait(self.waitTime) 

你通过在屏幕上绘制蛇和苹果来更新我们的屏幕,然后将lastMove设置为action,因为蛇已经移动,现在正朝着action的方向移动。

这个方法的最后一步是返回当前游戏的样子、获得的奖励以及你是否输了,像这样:

 # Returning the new frame of the game, the reward obtained and whether the game has ended or not
        return self.screenMap, reward, gameOver 

screenMap提供了执行动作后游戏界面所需的信息,reward告诉你通过执行这个动作获得的奖励,gameOver告诉你是否输了。

这就是这个方法的全部内容!为了拥有一个完整的Environment类,你只需要创建一个重置环境的函数,就像这样一个reset方法:

 # Making a function that resets the environment
    def reset(self):
        self.screenMap  = np.zeros((self.nRows, self.nColumns))
        self.snakePos = list()

        for i in range(self.initSnakeLen):
            self.snakePos.append((int(self.nRows / 2) + i, int(self.nColumns / 2)))
            self.screenMap[int(self.nRows / 2) + i][int(self.nColumns / 2)] = 0.5

        self.screenMap[self.applePos[0]][self.applePos[1]] = 1

        self.lastMove = 0 

它只是简单地重置了游戏板(screenMap),以及蛇的位置,恢复到默认的中间位置。它还将苹果的位置设置为和上一轮相同。

恭喜!你刚刚完成了环境的构建。现在我们将进入第二步,构建大脑。

第 2 步 – 构建大脑

这里你将用卷积神经网络(CNN)来构建我们的“大脑”。你还将为其训练设置一些参数,并定义一个加载预训练模型进行测试的方法。

开始吧!

一如既往地,你首先导入你将使用的库,像这样:

# Importing the libraries
import keras
from keras.models import Sequential, load_model
from keras.layers import Dense, Dropout, Conv2D, MaxPooling2D, Flatten
from keras.optimizers import Adam 

正如你可能已经注意到的,所有这些类都是 Keras 库的一部分,而这正是你将在本章中使用的库。实际上,Keras 是你在这个文件中唯一需要使用的库。现在让我们一起回顾一下这些类和方法:

  1. Sequential – 一个可以初始化神经网络的类,并定义网络的一般结构。

  2. load_model – 一个从文件中加载模型的函数。

  3. Dense – 一个类,用来创建人工神经网络(ANN)中的全连接层。

  4. Dropout – 一个向我们的网络中添加 Dropout 的类。你已经在第八章 《物流中的 AI – 仓库中的机器人》中见过它的使用。

  5. Conv2D – 一个用于构建卷积层的类。

  6. MaxPooling2D – 一个用于构建最大池化层的类。

  7. Flatten – 一个执行扁平化操作的类,以便为经典的人工神经网络(ANN)提供输入。

  8. Adam – 一个优化器,用来优化你的神经网络。在训练 CNN 时使用它。

现在你已经导入了库,接下来可以创建一个叫做Brain的类,在这个类中使用所有这些类和方法。首先定义一个类和__init__方法,像这样:

# Creating the Brain class
class Brain():

    def __init__(self, iS = (100,100,3), lr = 0.0005):

        self.learningRate = lr
        self.inputShape = iS
        self.numOutputs = 4
        self.model = Sequential() 

你可以看到,__init__方法接收两个参数:iS(输入形状)和lr(学习率)。然后你定义了一些与这个类相关的变量:learningRateinputShapenumOutputs。将numOutputs设置为4,因为这是我们的 AI 可以采取的动作数量。最后,在最后一行创建一个空模型。为此,使用我们之前导入的Sequential类。

这样做会让你将所需的所有层添加到模型中。这正是你通过这些代码行完成的:

 # Adding layers to the model   #20
        self.model.add(Conv2D(32, (3,3), activation = 'relu', input_shape = self.inputShape))   #21
        #22
        self.model.add(MaxPooling2D((2,2)))   #23
        #24
        self.model.add(Conv2D(64, (2,2), activation = 'relu'))   #25
        #26
        self.model.add(Flatten())   #27
        #28
        self.model.add(Dense(units = 256, activation = 'relu'))   #29
        #30
        self.model.add(Dense(units = self.numOutputs))   #31 

让我们将这段代码分解成几行:

第 21 行:你向模型中添加了一个新的卷积层。它有 32 个 3x3 的过滤器,使用 ReLU 激活函数。你还需要在这里指定输入形状。记住,输入形状是这个函数的参数之一,并且保存在inputShape变量中。

第 23 行:你添加了一个最大池化层。窗口的大小是 2x2,这将使我们的特征图的大小缩小2倍。

第 25 行:你添加了第二个卷积层。这次它有 64 个 2x2 的过滤器,使用相同的 ReLU 激活函数。为什么这次使用 ReLU 呢?我在实验中尝试了其他一些激活函数,结果发现对于这个 AI,ReLU 效果最好。

第 27 行:在应用了卷积之后,你获得了新的特征图,并将其展平为 1D 向量。这正是这一行所做的——它将 2D 图像展平为 1D 向量,之后你将能够将其作为神经网络的输入。

第 29 行:现在,你进入了全连接步骤——你正在构建传统的人工神经网络(ANN)。这一行代码为我们的模型添加了一个新的隐藏层,包含256个神经元,并使用 ReLU 激活函数。

第 31 行:你创建了神经网络中的最后一层——输出层。它有多大呢?它必须拥有和你能采取的动作数量一样多的神经元。你在之前的numOutputs变量中设置了这个值,它的值为4。这里没有指定激活函数,这意味着激活函数默认为线性。事实证明,在这种情况下,使用线性输出比 Softmax 输出效果更好;它使训练更加高效。

你还需要compile你的模型。这将告诉你的代码如何计算误差,以及在训练模型时使用哪个优化器。你可以用这一行代码来实现:

 # Compiling the model
        self.model.compile(loss = 'mean_squared_error', optimizer = Adam(lr = self.learningRate)) 

在这里,你使用了一个属于Sequential类的方法(这就是你可以用模型调用它的原因)来完成这个操作。这个方法叫做compile,在本例中需要两个参数。loss是一个函数,告诉 AI 如何计算神经网络的误差;你将使用mean_squared_error。第二个参数是优化器。你已经导入了Adam优化器,并在这里使用它。这个优化器的学习率是该类__init__方法的参数之一,值由learningRate变量表示。

在这节课中,只剩下一步了——编写一个从文件中加载模型的函数。你可以使用以下代码来实现:

 # Making a function that will load a model from a file
    def loadModel(self, filepath):
        self.model = load_model(filepath)
        return self.model 

你可以看到,你创建了一个新的函数loadModel,它接收一个参数——filepath。这个参数是预训练模型的文件路径。定义完函数后,你就可以从这个文件路径加载模型。为此,你使用了之前导入的load_model方法。这个函数接收相同的参数——filepath。最后一行,你返回加载的模型。

恭喜你!你刚刚完成了大脑的构建。

让我们继续前进,构建经验重放记忆。

第三步 – 构建经验重放记忆

你现在将构建这个内存,稍后,你将从这个内存的小批量中训练你的模型。内存将包含有关游戏状态(采取行动前)、所采取的行动、获得的奖励以及执行行动后游戏状态的信息。

我有个好消息要告诉你——你还记得这段代码吗?

# AI for Games - Beat the Snake game
# Implementing Deep Q-Learning with Experience Replay

# Importing the libraries
import numpy as np

# IMPLEMENTING DEEP Q-LEARNING WITH EXPERIENCE REPLAY

class Dqn(object):

    # INTRODUCING AND INITIALIZING ALL THE PARAMETERS AND VARIABLES OF THE DQN
    def __init__(self, max_memory = 100, discount = 0.9):
        self.memory = list()
        self.max_memory = max_memory
        self.discount = discount

    # MAKING A METHOD THAT BUILDS THE MEMORY IN EXPERIENCE REPLAY
    def remember(self, transition, game_over):
        self.memory.append([transition, game_over])
        if len(self.memory) > self.max_memory:
            del self.memory[0]

    # MAKING A METHOD THAT BUILDS TWO BATCHES OF INPUTS AND TARGETS BY EXTRACTING TRANSITIONS FROM THE MEMORY
    def get_batch(self, model, batch_size = 10):
        len_memory = len(self.memory)
        num_inputs = self.memory[0][0][0].shape[1]
        num_outputs = model.output_shape[-1]
        inputs = np.zeros((min(len_memory, batch_size), num_inputs))
        targets = np.zeros((min(len_memory, batch_size), num_outputs))
        for i, idx in enumerate(np.random.randint(0, len_memory, size = min(len_memory, batch_size))):
            current_state, action, reward, next_state = self.memory[idx][0]
            game_over = self.memory[idx][1]
            inputs[i] = current_state
            targets[i] = model.predict(current_state)[0]
            Q_sa = np.max(model.predict(next_state)[0])
            if game_over:
                targets[i, action] = reward
            else:
                targets[i, action] = reward + self.discount * Q_sa
        return inputs, targets 

你将几乎使用相同的代码,只是做了两个小小的改变。

首先,去掉这一行:

 num_inputs = self.memory[0][0][0].shape[1] 

然后,修改这一行:

 inputs = np.zeros((min(len_memory, batch_size), num_inputs)) 

到这里:

 inputs = np.zeros((min(len_memory, batch_size), self.memory[0][0][0].shape[1],self.memory[0][0][0].shape[2],self.memory[0][0][0].shape[3])) 

为什么你需要这么做呢?好吧,你去掉了第一行,因为你不再拥有一个 1D 的输入向量。现在你有一个 3D 数组。

然后,如果你仔细观察,你会发现其实你并没有改变inputs。之前,你有一个二维数组,其中一个维度是批量大小,另一个维度是输入的数量。现在,事情非常相似;第一个维度仍然是批量大小,最后三个维度则对应输入的大小!

由于我们的输入现在是一个 3D 数组,所以你写了.shape[1].shape[2].shape[3]。这些形状到底是什么意思?

.shape[1]是游戏中的行数(在你的例子中是 10)。.shape[2]是游戏中的列数(在你的例子中是 10)。.shape[3]是最后几帧叠加在一起的数量(在你的例子中是 4)。

如你所见,你其实并没有真正改变什么。你只是让代码适用于我们的 3D 输入。

我还将这个dqn.py文件重命名为DQN.py,并将类DQN重命名为Dqn

就这样!这可能比你们大多数人预期的要简单得多。

你终于可以开始训练你的模型了。我们将在下一节中进行训练 AI。

第 4 步 – 训练 AI

这是迄今为止最重要的一步。在这里,我们终于教会了 AI 玩蛇游戏!

和往常一样,首先导入你需要的库:

# Importing the libraries
from environment import Environment
from brain import Brain
from DQN import Dqn
import numpy as np
import matplotlib.pyplot as plt 

在前三行中,你导入了之前创建的工具,包括BrainEnvironment和经验回放内存。

然后,在接下来的两行中,你导入了你将使用的库。这些包括 NumPy 和 Matplotlib。你已经会识别前者;后者将用于展示你模型的表现。具体来说,它将帮助你展示一个图表,每 100 局游戏,会显示你收集到的苹果的平均数量。

这就是这一步的全部内容。现在,定义一些代码的超参数:

# Defining the parameters
memSize = 60000
batchSize = 32
learningRate = 0.0001
gamma = 0.9
nLastStates = 4

epsilon = 1.
epsilonDecayRate = 0.0002
minEpsilon = 0.05

filepathToSave = 'model2.h5' 

我会在这个列表中解释它们:

  1. memSize – 你的经验回放内存的最大大小。

  2. batchSize – 每次迭代中,从经验回放内存中获取的输入和目标的批量大小,用于模型训练。

  3. learningRate – 你在Brain中使用的Adam优化器的学习率。

  4. gamma – 你的经验回放内存的折扣因子。

  5. nLastStates – 你保存作为当前游戏状态的最后几帧。记住,你会将一个大小为nRows x nColumns x nLastStates的 3D 数组输入到你的 CNN 中,作为Brain的一部分。

  6. epsilon – 初始 epsilon,即采取随机动作的机会。

  7. epsilonDecayRate – 每场/轮游戏后你减少epsilon的比率。

  8. minEpsilon – 最低可能的 epsilon 值,之后不能再调低。

  9. filepathToSave – 你希望保存模型的位置。

就这样 - 你已经定义了超参数。稍后在编写其余代码时将使用它们。现在,你需要创建一个环境、一个大脑和一个经验重播内存:

# Creating the Environment, the Brain and the Experience Replay Memory
env = Environment(0)
brain = Brain((env.nRows, env.nColumns, nLastStates), learningRate)
model = brain.model
dqn = Dqn(memSize, gamma) 

你可以看到,在第一行中,你创建了Environment类的一个对象。你需要在这里指定一个变量,即你的环境减慢(移动之间的等待时间)。你在训练期间不想有任何减速,所以在这里输入0

接下来你创建了Brain类的一个对象。它接受两个参数 - 输入形状和学习率。正如我多次提到的,输入形状将是一个大小为nRows x nColumns x nLastStates的 3D 数组,所以你在这里输入这些内容。第二个参数是学习率,由于你已经为此创建了一个变量,所以你简单地输入这个变量的名称 - learningRate。在这一行之后,你取得了这个Brain类的模型,并在你的代码中创建了这个模型的一个实例。保持简单,称其为model

在最后一行中,你创建了Dqn类的一个对象。它接受两个参数 - 内存的最大大小和内存的折扣因子。你为此指定了两个变量,memSizegamma,所以在这里使用它们。

现在,你需要编写一个函数,重置你的 AI 的状态。你需要它是因为状态相当复杂,在主代码中重置它们会造成很大的混乱。这是它的样子:

# Making a function that will initialize game states   #30
def resetStates():   #31
    currentState = np.zeros((1, env.nRows, env.nColumns, nLastStates))   #32
    #33
    for i in range(nLastStates):   #34
        currentState[:,:,:,i] = env.screenMap   #35
    #36
    return currentState, currentState   #37 

让我们把它分成单独的行:

第 31 行:你定义了一个名为resetStates的新函数。它不接受任何参数。

第 32 行:你创建了一个名为currentState的新数组。它充满了零,但你可能会问为什么是 4D 的;我们不是说输入应该是 3D 吗?你完全正确,确实应该是。第一个维度称为批处理大小,简单地表示你一次向神经网络输入多少个输入。你每次只输入一个数组,因此第一个大小是1。接下来的三个大小对应输入的大小。

第 34-35 行:在一个for循环中,将执行nLastStates次,你将每个 3D 状态的层的板子设置为来自环境的当前游戏板的初始外观。每个状态中的每一帧最初看起来都一样,就像开始游戏时游戏板的样子一样。

第 37 行:这个函数将返回两个currentStates。为什么?因为你需要两个游戏状态数组。一个表示在你执行动作之前的板子状态,另一个表示在你执行动作之后的板子状态。

现在你可以开始编写整个训练的代码了。首先,创建几个有用的变量,如下所示:

# Starting the main loop
epoch = 0
scores = list()
maxNCollected = 0
nCollected = 0.
totNCollected = 0 

epoch会告诉你现在处于哪个 epoch/游戏。scores是一个列表,你在每 100 局游戏/epoch 后保存每局游戏的平均分。maxNCollected告诉你训练过程中获得的最高分,而nCollected是每局游戏/epoch 的分数。最后一个变量totNCollected告诉你在 100 个 epoch/游戏中收集了多少苹果。

现在,你开始一个重要的、无限的while循环,如下所示:

while True:
    # Resetting the environment and game states
    env.reset()
    currentState, nextState = resetStates()
    epoch += 1
    gameOver = False 

在这里,你遍历每一局游戏,每个 epoch。这就是为什么你在第一行重置环境,在接下来的行中创建新的currentStatenextState,将epoch加一,并将gameOver设置为False,因为显然你还没有输掉游戏。

请注意,这个循环不会结束;因此,训练永远不会停止。我们这样做是因为我们没有设定何时停止训练的目标,因为我们还没有定义什么是我们 AI 的满意结果。我们可以计算平均结果或类似的指标,但那样训练可能会花费太长时间。我更倾向于让训练继续进行,你可以在任何时候停止训练。一个合适的停止时机是当 AI 每局游戏收集到平均六个苹果时,或者如果你想要更好的表现,你甚至可以设定每局游戏收集 12 个苹果。

你已经开始了第一个循环,它将遍历每个 epoch。现在你需要创建第二个循环,在其中 AI 执行动作、更新环境并训练你的 CNN。从以下几行代码开始:

 # Starting the second loop in which we play the game and teach our AI
    while not gameOver: 

        # Choosing an action to play
        if np.random.rand() < epsilon:
            action = np.random.randint(0, 4)
        else:
            qvalues = model.predict(currentState)[0]
            action = np.argmax(qvalues) 

正如我之前提到的,这是你的 AI 做出决策、移动并更新环境的循环。你首先初始化一个while循环,只要你没有输掉游戏,也就是gameOver被设置为False,这个循环就会执行。

接下来,你可以看到if条件。这是你的 AI 做出决策的地方。如果从范围(0,1)中随机选出的值小于 epsilon,那么将执行一个随机动作。否则,你会根据当前游戏状态预测 Q 值,并从这些 Q 值中选择最大的 Q 值对应的索引。这将是你的 AI 执行的动作。

然后,你需要更新环境:

 # Updating the environment
        state, reward, gameOver = env.step(action) 

你使用的是Environment类对象的step方法。它接受一个参数,即你执行的动作。它还会返回执行这个动作后从游戏中获得的新帧,以及获得的奖励和游戏结束信息。你很快就会使用到这些变量。

请记住,这个方法返回的是你游戏中的单一 2D 帧。这意味着你必须将这个新帧添加到nextState中,并删除最后一个帧。你可以用以下几行代码实现:

 # Adding new game frame to the next state and deleting the oldest frame from next state
        state = np.reshape(state, (1, env.nRows, env.nColumns, 1))
        nextState = np.append(nextState, state, axis = 3)
        nextState = np.delete(nextState, 0, axis = 3) 

正如你所看到的,首先你重新塑形了state,因为它是二维的,而currentStatenextState是四维的。然后,你将这个新的、重新塑形后的帧添加到nextState的第 3 维。为什么是第 3 维呢?因为第 3 维对应于数组的第四个维度,它保存了二维帧。在最后一行,你简单地删除了nextState中的第一帧(索引为 0),保留了最旧的帧在最低的索引位置。

现在,你可以将这个转换remember到经验回放记忆中,并从这个记忆中随机抽取一批进行训练。你可以用这些代码行来实现:

 # Remembering the transition and training our AI
        dqn.remember([currentState, action, reward, nextState], gameOver)
        inputs, targets = dqn.get_batch(model, batchSize)
        model.train_on_batch(inputs, targets) 

在第一行,你将这个转换添加到记忆中。它包含了执行动作前的游戏状态(currentState)、所采取的动作(action)、获得的奖励(reward)以及执行该动作后的游戏状态(nextState)。你还记住了gameOver状态。在接下来的两行中,你从记忆中随机选择一批输入和目标,并用它们来训练你的模型。

完成这一步后,你可以检查你的蛇是否收集到苹果,并更新currentState。你可以用以下代码行来实现:

 # Checking whether we have collected an apple and updating the current state
        if env.collected:
            nCollected += 1

        currentState = nextState 

在前两行中,你检查蛇是否收集到了苹果,如果收集到了,你就增加nCollected。然后你通过将nextState的值赋给currentState来更新currentState

现在,你可以退出这个循环了。你还需要做几件事:

 # Checking if a record of apples eaten in a around was beaten and if yes then saving the model
    if nCollected > maxNCollected and nCollected > 2:
        maxNCollected = nCollected
        model.save(filepathToSave)

    totNCollected += nCollected
    nCollected = 0 

你检查自己是否打破了单局游戏吃苹果的记录(这个数字必须大于 2),如果是,你就更新记录,并将当前模型保存到之前指定的文件路径中。你还增加了totNCollected并将nCollected重置为 0,准备迎接下一局游戏。

然后,在 100 局游戏后,你展示平均分数,如下所示:

 # Showing the results each 100 games
    if epoch % 100 == 0 and epoch != 0:
        scores.append(totNCollected / 100)
        totNCollected = 0
        plt.plot(scores)
        plt.xlabel('Epoch / 100')
        plt.ylabel('Average Score')
        plt.savefig('stats.png')
        plt.close() 

你有一个名为scores的列表,用来存储 100 局游戏后的平均分数。你将一个新的值添加到列表中,然后重置这个值。接着,你用之前导入的 Matplotlib 库在图表中展示scores。这个图表每进行 100 局游戏/迭代后会保存在stats.png中。

然后,你降低 epsilon 值,像这样:

 # Lowering the epsilon
    if epsilon > minEpsilon:
        epsilon -= epsilonDecayRate 

通过if条件,你确保 epsilon 不会低于最小阈值。

在最后一行,你显示关于每一局游戏的额外信息,如下所示:

 # Showing the results each game
    print('Epoch: ' + str(epoch) + ' Current Best: ' + str(maxNCollected) + ' Epsilon: {:.5f}'.format(epsilon)) 

你展示当前的轮次(游戏),当前单局游戏收集苹果的记录,以及当前的 epsilon 值。

就这样!恭喜你!你刚刚构建了一个训练你模型的函数。记住,这个训练会无限进行,直到你决定它完成为止。当你满意后,你就可以进行测试了。为了测试,你需要一个简单的文件来验证你的模型。让我们来做吧!

第 5 步 – 测试 AI

这将是一个非常简短的部分,所以不用担心。你很快就会运行这段代码!

和往常一样,你首先导入所需的库:

# Importing the libraries
from environment import Environment
from brain import Brain
import numpy as np 

这次你不会使用 DQN 记忆也不会使用 Matplotlib 库,因此不需要导入它们。

你还需要指定一些超参数,像这样:

# Defining the parameters
nLastStates = 4
filepathToOpen = 'model.h5'
slowdown = 75 

你稍后在代码中会用到nLastStates。你还创建了一个文件路径来测试你的模型。最后,还有一个变量,用来指定每次移动后的等待时间,以便你能清晰地看到 AI 的表现。

再次,你创建了一些有用的对象,比如EnvironmentBrain

# Creating the Environment and the Brain
env = Environment(slowdown)
brain = Brain((env.nRows, env.nColumns, nLastStates))
model = brain.loadModel(filepathToOpen) 

Environment的括号里,你输入slowdown,因为这是这个类所需要的参数。你还创建了一个Brain类的对象,但这次你没有指定学习率,因为你不会训练模型。在最后一行,你使用Brain类的loadModel方法加载一个预训练模型。这个方法需要一个参数,也就是加载模型的文件路径。

再次,你需要一个函数来重置状态。你可以使用之前的那个,所以直接复制并粘贴这些行:

# Making a function that will reset game states
def resetStates():
    currentState = np.zeros((1, env.nRows, env.nColumns, nLastStates))

    for i in range(nLastStates):
        currentState[:,:,:,i] = env.screenMap

    return currentState, currentState 

现在,你可以像以前一样进入主while循环。不过这次,你不需要定义任何变量,因为你不需要它们:

# Starting the main loop
while True:
    # Resetting the game and the game states
    env.reset()
    currentState, nextState = resetStates()
    gameOver = False 

如你所见,你已经开始了这个无限的while循环。再次提醒,你每次都需要重启环境、状态和游戏结束判定。

现在,你可以进入游戏的while循环,执行动作、更新环境等等:

 # Playing the game
    while not gameOver: 

        # Choosing an action to play
        qvalues = model.predict(currentState)[0]
        action = np.argmax(qvalues) 

这次,你不需要任何if语句。毕竟,你是在测试 AI,所以这里不能有任何随机行为。

再次,你需要更新环境:

 # Updating the environment
        state, _, gameOver = env.step(action) 

你并不关心奖励,所以只需用"_"代替reward。环境在执行动作后仍然返回帧,并且提供关于游戏是否结束的信息。

由于这个原因,你需要以与之前相同的方式重塑state并更新nextState

 # Adding new game frame to next state and deleting the oldest one from next state
        state = np.reshape(state, (1, env.nRows, env.nColumns, 1))
        nextState = np.append(nextState, state, axis = 3)
        nextState = np.delete(nextState, 0, axis = 3) 

在最后一行,你需要像在另一个文件中一样更新currentState

 # Updating current state
        currentState = nextState 

这一部分的编码到此为止!然而,这并不是本章的结束。你仍然需要运行代码。

演示

不幸的是,由于 Google Colab 不支持 PyGame,你需要使用 Anaconda。

感谢你,你应该在第十章自动驾驶汽车的 AI – 构建一辆自驾车后完成安装,所以安装所需的包和库会更加容易。

安装

首先,在 Anaconda 中创建一个新的虚拟环境。这次,我将通过 PC 上的 Anaconda Prompt 演示安装过程,这样你们可以在任何系统上看到如何操作。

Windows 用户,请在 PC 上打开 Anaconda Prompt,Mac/Linux 用户,请在 Mac/Linux 上打开终端。然后输入:

conda create -n snake python=3.6 

就像这样:

然后,按下键盘上的Enter键。你应该会得到如下内容:

在键盘上输入y并再次按下Enter键。安装完成后,在你的 Anaconda 提示符中输入以下命令:

conda activate snake 

然后再次按下Enter键。现在在左侧,你应该看到snake而不是base。这意味着你已经进入了新创建的 Anaconda 环境。

现在你需要安装所需的库。第一个是 Keras:

conda install -c conda-forge keras 

写完之后,按下Enter键。当你看到这个:

再次输入y并按下Enter键。一旦安装完成,你需要安装 PyGame 和 Matplotlib。

第一个可以通过输入pip install pygame来安装,而第二个可以通过输入pip install matplotlib来安装。安装过程与安装 Keras 时的步骤相同。

好了,现在你可以运行你的代码了!

如果你不小心关闭了你的 Anaconda 提示符/终端,重新打开它并输入以下命令来激活我们刚刚创建的snake环境:

conda activate snake 

然后按下Enter键。我在做这一步之后收到了很多警告,你也可能会看到类似的警告,但不用担心:

现在,你需要将此终端导航到包含你想运行的文件的文件夹,在本例中是train.py。我建议你将第十三章的所有代码放在一个名为Snake的文件夹中,并放在桌面上。然后,你就能跟着我给出的确切指示来操作了。要导航到该文件夹,你需要使用cd命令。

首先,通过运行cd Desktop来导航到桌面,像这样:

然后进入你创建的Snake文件夹。就像之前的命令一样,运行cd Snake,像这样:

你已经快到了。要训练一个新的模型,你需要输入:

python train.py 

然后按下Enter键。这就是你应该看到的内容:

你有一个左侧显示游戏窗口,一个右侧显示终端,告诉你每场游戏的情况(每个 epoch)。

恭喜!你刚刚完成了本章的代码并为 Snake 游戏创建了一个 AI。尽管如此,请耐心等待!训练可能需要几个小时。

那么,你可以期待什么样的结果呢?

结果

首先,确保在 Anaconda 提示符/终端中逐 epoch 地跟踪结果。一个 epoch 就是玩一次游戏。经过成千上万的游戏(epoch)后,你会看到分数和蛇的体积增加。

在经历了数千个 epoch 的训练后,尽管蛇并没有填满整个地图,但你的 AI 在与人类相当的水平上进行游戏。这是 25,000 个 epoch 后的截图。

图 5:结果示例 1

图 6:结果示例 2

你还会得到一个在文件夹中创建的图表(stats.png),显示了每个 epoch 的平均得分。这是我在训练我们的 AI 25,000 次后得到的图表:

图 7:超过 25,000 次训练的平均得分

你可以看到我们的 AI 达到了每局 10-11 的平均得分。考虑到在训练之前它对游戏一无所知,这个成绩还不错。

如果你使用本章附带的预训练模型 model.h5 运行 test.py 文件,你也能看到相同的结果。为了做到这一点,你只需要在 Anaconda Prompt/Terminal 中输入(仍然在桌面上包含 第十三章 所有代码的同一个 Snake 文件夹内,并且仍然在 snake 虚拟环境中):

python test.py 

如果你想在训练后测试你的模型,只需要在 test.py 文件中将 model.h5 替换为 model2.h5。这是因为在训练过程中,你的 AI 神经网络的权重会保存到名为 model2.h5 的文件中。然后在你的 Anaconda Prompt/Terminal 中重新输入 python test.py,就可以欣赏到你自己的结果了。

总结

在本书的最后一章实践部分,我们为贪吃蛇构建了一个深度卷积 Q 学习模型。在我们开始构建之前,必须先定义我们的 AI 能看到什么。我们确定需要堆叠多个帧,以便 AI 能看到其动作的连续性。这是我们卷积神经网络的输入。输出是对应四个可能动作的 Q 值:向上、向下、向左和向右。我们奖励 AI 吃到苹果,惩罚它失败,稍微惩罚它执行任何动作(生存惩罚)。经过 25,000 局游戏后,我们可以看到我们的 AI 每局能吃到 10-11 个苹果。

希望你喜欢这个内容!

第十四章:回顾与结论

在本章最后,我将为你提供一个关于通用 AI 框架的回顾,供你参考,并在你将 AI 学习提升到新的水平时提供一些建议。你已经走了很长一段路,未来你还可以将 AI 学习推向更远的地方!

回顾 – 通用 AI 框架/蓝图

让我们回顾一下,并提供整个 AI 蓝图,供你在需要时参考。你甚至可以把它打印出来,贴在墙上!

步骤 1 – 构建环境

  1. 步骤 1-1:引入并初始化环境的所有参数和变量。

  2. 步骤 1-2:创建一个方法,在 AI 执行动作后更新环境。

  3. 步骤 1-3:创建一个重置环境的方法。

  4. 步骤 1-4:创建一个方法,随时提供当前状态、最后获得的奖励以及游戏是否结束。

步骤 2 – 构建大脑

  1. 步骤 2-1:构建由输入状态组成的输入层。

  2. 步骤 2-2:构建隐藏层,选择这些层及每层内部神经元的数量,完全连接到输入层和彼此之间。

  3. 步骤 2-3:构建输出层,完全连接到最后一个隐藏层。

  4. 步骤 2-4:在模型对象内部组装完整的架构。

  5. 步骤 2-5:使用均方误差损失函数和选择的优化器编译模型。

步骤 3 – 实现深度强化学习算法

  1. 步骤 3-1:引入并初始化深度 Q 学习神经网络(DQN)模型的所有参数和变量。

  2. 步骤 3-2:创建一个方法,构建经验重放中的记忆。

  3. 步骤 3-3:创建一个方法,构建并返回两个批次的输入和目标,每个批次有 batch_size 个元素。

步骤 4 – 训练 AI

  1. 步骤 4-1:通过创建步骤 1 中的 Environment 类对象来构建环境。

  2. 步骤 4-2:通过创建步骤 2 中的 Brain 类对象来构建人工大脑。

  3. 步骤 4-3:通过在步骤 3 中创建 DQN 类的对象,构建 DQN 模型。

  4. 步骤 4-4:选择训练模式。

  5. 步骤 4-5:通过在选择的训练周期数上使用 for 循环,开始训练。

  6. 步骤 4-6:在每个训练周期中,我们会重复整个深度 Q 学习过程,同时 30% 的时间进行一些探索。

步骤 5 – 测试 AI

  1. 步骤 5-1:通过创建步骤 1 中的 Environment 类对象来构建一个新的环境。

  2. 步骤 5-2:加载人工大脑,使用之前训练的权重。

  3. 步骤 5-3:选择推理模式。

  4. 步骤 5-4:开始模拟。

  5. 步骤 5-5:在每次迭代中(每分钟),我们的 AI 只执行其预测结果的动作,不进行任何探索或深度 Q 学习训练。

探索 AI 未来的方向

你已经走了很长一段路!让我们再回顾一下你已经获得的知识和掌握的技能:

  • 你对强化学习有扎实的直觉。

  • 你可以用它来解决现实世界中的问题。

  • 你可以以一种与众不同的方式进行编程,把你带到 AI 的前沿。

  • 你可以编写能够学习并随着时间改进的系统。

  • 你拥有坚实的基础,可以让你在 AI 领域走得更远。

说到更进一步,问题是:怎么做?你将如何应用你所学到的知识?接下来你打算做什么?首先,你的下一步是:

练习,练习,再练习

有很多方式可以锻炼你的 AI 技能。你可以参加像 Kaggle 这样的平台上的 AI 竞赛,那里有许多可以用深度强化学习解决的问题。你还可以像我们为自动驾驶汽车创建的 AI 一样,开发一些新的 AI。例如,你可以用 Kivy 开发一个玩“乒乓”游戏的 AI。有一个很棒的 AI 平台,叫做OpenAI Gym,你可以在这里练习构建多种应用领域的 AI,包括:

  • 一个能玩 Atari 游戏(比如《打砖块》、《吃豆人》、《太空侵略者》等)的 AI。

  • 一个能玩赛车游戏的 AI。

  • 一个能玩游戏《毁灭战士》的 AI。

  • 训练虚拟机器人学习如何行走和奔跑。

我强烈建议你查看 OpenAI Gym 网站,那里有许多你可以练习和工作的精彩应用。

更进一步,你将采取什么步骤在这个世界上产生影响?你还记得我们在介绍中提到并解释的 AI 的 10 个应用领域吗?只需选择你最喜欢的一个!选择一个最能与你产生共鸣的领域。让我们来提醒你一下,它们分别是:

  1. 能源

  2. 医疗

  3. 交通与物流

  4. 教育

  5. 安全

  6. 就业

  7. 智能家居与机器人

  8. 娱乐与幸福

  9. 环境

  10. 经济与商业

如果你对这些领域中的某一领域感兴趣或充满热情,或者更好的是,具备一些领域知识,你可以将这些与新学到的 AI 技能结合起来,解决这些行业中的一些问题。通过与一些科技公司合作或在其中工作,或者建立自己的公司,你可以增加你的影响力。在每个领域中,AI 的需求都会非常庞大,这将永远为你打开许多大门。

说到开辟新机遇,这让我想到另一个我推荐的下一步。

建立人脉

练习是必要的,但仅靠练习并不足以在这个世界上通过 AI 产生影响。你还必须建立人际网络。无论是在科技公司工作,还是在其他行业中有 AI 团队的公司,或是为自己的公司工作,你都应该始终建立网络。这将开启新的大门,播下新的机会种子,增加你成功的机会。

如今建立人脉变得容易了。你可以参加许多人工智能活动和会议,离你最近的活动也绝不会太远。如果你无法参加,你可以轻松地自己组织一些人工智能见面会或下班后的讨论会,与其他有热情的人讨论人工智能。你还可以通过社交媒体创建群组,交换想法、头脑风暴人工智能问题,甚至通过这些方式建立新的人脉,从而形成协同效应。同样,你建立的人脉越多,你获得的好处也越多:连接、产生的创意、协同效应、机会、打开的门和人工智能之旅。

我想通过给你一些职业生涯方面的最佳建议来结束这本书。我要给你的最终建议是:

永不停歇地学习。

人工智能是一个快速发展的领域,你必须紧跟最新的最先进特性。本书没有覆盖人工智能的最新突破,但它为你提供了正确的基础和直觉,让你能够自信地应对人工智能领域的最新发展。

正确的基础知识是必要的,但仅凭这些你无法在长期内保持进步。你必须做的是永不停歇地学习。好消息是,今天继续学习变得轻而易举!有许多优秀的 MOOC 课程覆盖了人工智能领域最新的先进模型,还有文章、研究论文、博客,更不用说 YouTube 上的视频,在那里你可以找到从零开始讲解人工智能理论到最先进模型的完整内容。你有许多选择来为你的大脑输入最新的人工智能知识。只要确保不要选择评分最低的内容,你就没问题。

让我们回顾一下。在读完这本书后,你的下一步是什么?

  1. 练习,练习,再练习。

  2. 将你的人工智能技能与最能引起你共鸣的应用领域结合起来。

  3. 建立人脉。

  4. 永不停歇地学习。

  5. 当然,继续努力工作!

是的,努力工作始终是必不可少的。记住这一点:成功只是冰山一角,而冰山下隐藏着大量的艰苦工作。但别担心;只要你对你的工作和所追随的目标充满热情,工作就永远不会觉得太困难。事实上,它会感觉轻松自如。这就是为什么我的第二个建议如此重要:如果你能选择一个与你的目标高度契合的领域,那么你就找到了用激情产生影响的道路。如果你的激情纯粹在人工智能领域,那就更好了!你可以利用它解决多个应用领域中的问题和挑战,这为你提供了一个多元化职业的惊人机会。

在此,我想祝愿你有一个精彩且非常成功的职业生涯。为你写这本书是我的荣幸;我的目标是让人工智能大众化,并提高每个人对人工智能作为一项可触及技术的认识,它能为这个世界带来更好的改变。非常感谢你,享受人工智能吧!

posted @ 2025-07-13 15:43  绝不原创的飞龙  阅读(4)  评论(0)    收藏  举报