机器人的人工智能-全-
机器人的人工智能(全)
原文:
zh.annas-archive.org/md5/05e803b985c0aa9c73a9374fca010716
译者:飞龙
前言
机器人的定义是一种能够执行类似人类任务的机器。为了执行这些任务,机器人必须能够看到、理解和与环境交互。人工智能是识别物体和导航的最快方式。这本书将赋予你使用诸如卷积神经网络(CNNs)、计算机视觉、物体识别、遗传算法和强化学习等人工智能技术高效操作机器人的基本技能。
那么,谁在写这本书?正如你可以从我的传记中看到的那样,我已经做了40多年,从12岁的孩子做科学展览项目开始,然后作为空军列兵、NASA的初级工程师,等等。我于1992年开始从事人工智能,并专注于机器决策。今天,我设计的是重量达数万千克的完整飞行自主车辆。我很高兴能把这些经验写下来与你分享。
我为什么写这本书?我感觉在现有的文献中,对于正在机器人与自主领域崭露头角,需要从业余爱好者向工业和商业机器人初期阶段过渡的人,存在一个空白。在这样做的时候,我希望尽可能地消除我感知到的你和你想要实现的下一个级别机器人探索者之间的障碍。我省略了方程式、奇怪的术语和神秘感,用如何从你的机器人中获得你想要的东西的简单解释来代替。重要的是要记住,我的机器人阿尔伯特只是这个过程中的一个工具。这本书的目标不是设计一个特定的机器人,而是教授一套我认为你需要掌握的技能。真正的问题是接下来你将走向何方。利用这本书作为跳板,继续探索、实验并在机器人领域继续你的教育。从这里,你可以阅读那些启发和/或指导过我的人的作品:罗宾·墨菲博士、塞巴斯蒂安·特伦、罗德尼·布鲁克斯博士、鲍勃·祖布林、罗伯特·L·福沃德博士、艾萨克·阿西莫夫、亚瑟·C·克拉克,以及许多人。
这本书面向的对象
这本书是为那些已经开始了学习机器人知识之旅,并希望通过应用人工智能技术将能力提升到更高级阶段的机器人工程师和爱好者而编写的。对于寻找解决特定问题或进行困难机器人设计实用指南的学生和研究人员来说,它将是一个有用的参考。阅读这本书时,具备基本的Python编程技能、熟悉电子和布线以及能够使用基于Linux的命令行界面(CLI)的能力将会很有帮助。
这本书涵盖的内容
第1章,机器人学和人工智能的基础,解释了本书将涵盖的内容、标准机器人部件、控制概念、实时计算以及机器人如何做出决策的观察、定位、决策、行动(OODA)概念。
第2章,设置您的机器人,向您介绍电机、控制系统、如何使用Subsumption架构将机器人问题分解成部分,以及机器人操作系统2(ROS 2)。
第3章,概念化实用机器人设计流程,描述了机器人设计、用例和故事板的系统工程技术。
第4章,使用神经网络和监督学习识别物体,解释了您如何使用CNN来训练物体识别并从背景中分割物体。
第5章,使用强化学习和遗传算法拾取和放置玩具,涵盖了Q学习和遗传算法,这些算法用于教会机械臂高效移动。
第6章,教机器人听话,展示了您如何为机器人添加数字助手并为其创建一些自定义控制,包括讲敲门笑话。
第7章,教机器人导航和避免楼梯,概述了如何使用另一个CNN教会机器人在家中导航并避开障碍物。
第8章,放置物品,描述了如何完成机器人的任务以及如何找到玩具箱。
第9章,赋予机器人人工个性,解释了在机器人中模拟个性以增加交互的概念和理论。
第10章,结论和反思,基于作者40年的机器人设计职业生涯,讨论了机器人学作为一项职业。
要充分利用本书
您应该熟练掌握Python 3版本的编程。我们使用ROS 2作为机器人的控制架构。如果您需要更详细的说明,Packt出版社有几本优秀的书籍解释了如何使用ROS 2。在使用本书时,需要具备Python编程技能、熟悉电子、布线和单板计算机、使用基于Linux的CLI的能力以及了解AI/ML概念。如果您想跟随机器人的构建过程,则需要基本的动手工具(螺丝刀、扳手、艾伦键和烙铁)。所有其他安装说明将在本书的适当章节中提供。
下载示例代码文件
您可以从GitHub下载本书的示例代码文件https://github.com/PacktPublishing/Artificial-Intelligence-for-Robotics-2e。如果代码有更新,它将在GitHub仓库中更新。
我们还有其他来自我们丰富的图书和视频目录的代码包可供在https://github.com/PacktPublishing/下载。查看它们!
使用的约定
本书中使用了多种文本约定。
文本中的代码
: 表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟URL、用户输入和Twitter昵称。以下是一个示例:“我们创建了一个名为downloadDataset.py
的简短Python程序。”
代码块设置如下:
from roboflow import Roboflow
rf = Roboflow(api_key="*****************")
project = rf.workspace("toys").project("toydetector")
dataset = project.version(1).download("yolov8")
任何命令行输入或输出都按以下方式编写:
cd ~/ros2_ws/src
ros2 pkg create –build-type ament-cmake ros_xarm
colcon build
粗体: 表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“在Roboflow上使用生成选项卡,然后点击添加增强步骤以选择将影响我们图像的操作类型。”
小贴士或重要注意事项
看起来像这样。
联系我们
我们始终欢迎读者的反馈。
一般反馈: 如果您对本书的任何方面有疑问,请通过客户关怀@packtpub.com给我们发邮件,并在邮件主题中提及书名。
勘误: 尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告,我们将不胜感激。请访问www.packtpub.com/support/errata并填写表格。
盗版: 如果您在互联网上以任何形式发现我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过版权@packt.com与我们联系,并提供材料的链接。
如果您有兴趣成为作者: 如果您在某个主题上具有专业知识,并且您有兴趣撰写或为本书做出贡献,请访问authors.packtpub.com。
分享您的想法
读完《人工智能机器人》后,我们非常想听听您的想法!请点击此处直接进入本书的亚马逊评论页面并分享您的反馈。
您的审阅对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。
。
下载本书的免费PDF副本
感谢您购买本书!
您喜欢在路上阅读,但无法携带您的印刷书籍到处走吗?
您的电子书购买是否与您选择的设备不兼容?
别担心,现在每购买一本Packt书籍,您都可以免费获得该书的DRM免费PDF版本。
在任何地方、任何设备上阅读。从您最喜欢的技术书籍中直接搜索、复制和粘贴代码到您的应用程序中。
优惠远不止这些,您还可以获得独家折扣、新闻通讯以及每天收件箱中的优质免费内容。
按照以下简单步骤获取福利:
- 扫描下面的二维码或访问以下链接
https://packt.link/free-ebook/9781805129592
-
提交您的购买证明
-
就这些!我们将直接将您的免费PDF和其他福利发送到您的电子邮件。
第一部分:机器人学和人工智能的构建模块
本书的第一部分从机器人学和人工智能(AI)的基础开始,涵盖AI是什么以及它是如何被使用的。然后我们开始定义我们的机器人系统并讨论控制。在第二章中,我们研究机器人的解剖结构,机器人的各个部分,并讨论自主原则和Subsumption架构概念。您将了解机器人操作系统(ROS)和我们的单板超级计算机。最后,我们展示了使用系统工程原理和故事板进行机器人设计的系统化过程。
本部分包含以下章节:
第一章:机器人和人工智能的基础
在这本书中,我邀请你和我一起踏上旅程,发现如何将人工智能(AI)添加到移动机器人中。我所说的AI机器人和更常规机器人之间的基本区别是机器人和其软件根据传感器提供的数据做出决策、学习和适应其环境的能力。更具体地说,我们正在告别预先编码的机器人设计的世界。我们不是预先编程所有机器人的行为,而是机器人(更准确地说,是机器人软件)将从我们提供的示例或与外部世界的交互中学习。机器人软件不会像我们用来训练人工智能系统的数据那样控制其行为。
人工智能机器人将利用其学习过程对环境以及如何实现目标进行预测,然后利用这些预测来创建行为。在我们的旅途中,我们将尝试几种人工智能的形式,包括监督学习和无监督学习、强化学习、神经网络和遗传算法。我们将创建一个能够交谈并理解命令(以及讲笑话)的数字机器人助手,并为我们的机器人创建一个人工个性(AP)。我们将学习如何教会我们的机器人无地图导航、通过试错法抓取物体,以及三维视觉。
在本章中,我们将涵盖以下关键主题:
-
机器人和人工智能的基本原理
-
什么是人工智能和自主性(以及它不是什么)?
-
人工智能的近期发展有什么新内容吗?
-
什么是机器人?
-
介绍我们的示例问题
-
何时需要为你的机器人使用人工智能?
-
介绍机器人和我们的开发环境
技术要求
完成本章任务的技术要求在本书的前言中有描述。
本书的所有代码都可在GitHub仓库中找到,网址为https://github.com/PacktPublishing/Artificial-Intelligence-for-Robotics-2e/。
机器人和人工智能的基本原理
将人工智能应用于机器人开发需要你,即机器人设计师或开发者,具备不同的技能。你可能之前制作过机器人。你可能有一个四旋翼无人机或3D打印机(实际上,它也是一个机器人)。熟悉的比例-积分-微分(PID)控制器、传感器循环和状态机的世界被人工神经网络(ANNs)、专家系统、遗传算法和搜索路径规划器所增强。我们希望机器人不仅仅是对其环境做出反射性反应,而是有目标和意图——并且能够学习和适应环境,并且是被教导或训练而不是被编程的。通过这种方式我们可以解决的问题可能在其他情况下是困难的、难以处理的或不可能的。
在这本书中,我们要介绍一个问题——在游戏室里捡起玩具——我们将用它作为全书的例子,当我们学习一系列将人工智能应用于我们机器人的技术时。重要的是要理解,在这本书中,过程远比目的地更重要。在书的结尾,你应该获得一些具有广泛适用性的重要技能,而不仅仅是学会如何捡起玩具。
我们要做的第一件事是提供一些工具和背景,以匹配书中开发例子所使用的基础设施。这是为了提供一个公平的竞争环境,并且不假设你具备任何实际知识。为了执行我们将要构建的一些高级神经网络,我们将使用Jetson中的GPU。
在本章的剩余部分,我们将讨论一些关于机器人和人工智能的基础知识,然后继续开发我们将用于本书其余部分所有例子的两个重要工具。我们将介绍软实时控制的概念,然后提供一个框架或模型,称为观察-定位-决策-行动(OODA)循环,以为我们机器人创建自主性。
人工智能和自主性(以及它不是什么)是什么?
人工智能的定义是什么?一般来说,它意味着一种表现出某些智能特征的机器——思考、推理、规划、学习和适应。它也可以指一种可以模拟思考或推理的软件程序。让我们尝试一些例子:一个通过简单规则(如果障碍物在右边,就向左走)避开障碍物的机器人不是人工智能。一个通过示例学习在视频中识别猫的程序是人工智能。一个由操纵杆操作的机器人手臂不使用人工智能,但一个能够适应不同物体以便捡起它们的机器人手臂是人工智能的应用。
你必须了解人工智能机器人的两个定义特征。首先,人工智能机器人主要是训练来完成任务的,通过提供示例,而不是一步一步地进行编程。例如,我们将通过用玩具的外观示例训练神经网络来教机器人的软件识别玩具——我们希望它捡起的东西。我们将提供一组包含玩具的图片的训练集。我们将特别标注图像中哪些部分是玩具,机器人将从中学习。然后我们将测试机器人,看看它是否学到了我们希望它学到的,这有点像老师测试学生。第二个特征是涌现行为,其中机器人表现出没有明确编程进它的演变行为。我们为机器人提供了一种本质上非线性且自组织的控制软件。机器人可能会突然对某个事件或情况表现出一些奇怪或异常的反应,这可能会显得奇怪、古怪,甚至带有情感。我曾与一辆自动驾驶汽车合作,我们确信它有细腻的情感,移动得非常优雅,因此给它起了昵称“费迪南德”,这个名字来自一部卡通片中敏感、爱花的公牛,这在九吨重的卡车上显得很奇怪,因为卡车似乎喜欢植物。这些行为只是各种软件组件和控制算法的交互作用的结果,并不代表任何更多的事情。
你在人工智能领域会听到的一个概念是图灵测试。图灵测试是由艾伦·图灵在1950年提出的,在一篇题为《计算机与智能》的论文中。他假设一个人类审问者会询问一个隐藏的、看不见的人工智能系统,以及另一个人类。如果提出问题的人类无法分辨出哪个人是计算机,哪个人是人类,那么那个人工智能计算机就通过了测试。这个测试假设人工智能将能够完全具备倾听对话、理解内容并给出与人类相同类型答案的能力。当前的人工智能聊天机器人可以轻松通过图灵测试,你可能在本周已经与人工智能在电话中互动了几次,而自己却没有意识到。
来自人工智能协会(AAAI)的一个小组提出,对于人工智能来说,可能一个更合适的测试是组装平板家具——使用提供的说明书。然而,到目前为止,还没有任何机器人通过这个测试。
本书的目标不是通过图灵测试,而是采用一些新颖的方法,利用机器学习、规划、目标寻求、模式识别、分组和聚类等技术来解决问题。许多这些问题用其他方法解决起来都非常困难。能够通过图灵测试的人工智能软件将是一个通用人工智能的例子,或者是一个完整、工作的人工智能大脑,就像你一样,通用人工智能不需要专门训练来解决任何特定问题。到目前为止,通用人工智能尚未被创造出来,但我们所拥有的只是窄人工智能或模拟在非常狭窄的应用中思考的软件,例如识别物体,或者挑选购买的好股票。
虽然我们在本书中不是构建通用人工智能,这意味着我们不会担心我们的创造物会发展出自己的思维或失去控制。这来自科幻小说和糟糕电影的领域,而不是今天计算机的现实。我坚信,任何宣扬人工智能弊端或预测机器人将统治世界的人可能都没有看到人工智能研究在解决一般问题或创造类似实际智能的东西方面的悲观状态。
人工智能最近的发展有什么新意吗?
“过去的事必将重演,做过的事必将再做,太阳之下并无新事”——《传道书》1:9,《詹姆斯国王圣经》
人工智能的现代实践并非新鲜事物。其中大部分技术都是在20世纪60年代和70年代开发的,但由于当时的计算设备不足以处理软件的复杂性或所需的计算量,这些技术逐渐失去了人们的青睐。它们只等待计算机变得更强大,以及另一个非常重大的事件——互联网的发明。在之前的几十年里,如果你需要10,000张猫的数字化图片来编译一个数据库以训练神经网络,这项任务几乎是不可能的——你可以拍摄很多猫的照片,或者从书中扫描图像。今天,通过谷歌搜索猫的图片,0.44秒内就能返回1亿2600万个结果。找到猫的图片,或者任何其他东西,只需搜索一下,你就有了一个用于训练神经网络的训练集——除非你需要训练一个非常特定的对象集合,而这些对象恰好不在互联网上,正如我们将在本书中看到的,在这种情况下,我们又将使用另一种现代工具,而不是60年代就能找到的工具,那就是数码相机。非常快速的计算机、廉价的、丰富的存储以及几乎无限的数据访问的结合,催生了人工智能的复兴。
另一项现代发展发生在计算机光谱的另一端。虽然现在任何人都可以在家中的桌子上拥有我们过去称之为超级计算机的东西,但智能手机的发展推动了一系列创新,这些创新正在技术领域感受到。你可能会对智能手机的加速度计和陀螺仪感到惊奇,这些是由称为微机电系统(MEMS)的微小硅芯片制成的。它还配备了一个高分辨率但非常小的数码相机和一个多核计算机处理器,运行时所需的电量很少。它还包含(可能)三个无线电——一个Wi-Fi无线网络、一部移动电话和一个蓝牙发射接收器。尽管这些部件在使你的iPhone变得有趣使用方面做得很好,但它们也进入了为机器人提供的部件中。这对我们来说很有趣,因为过去只有研究实验室和大学才能使用的东西,现在可以出售给个人用户。如果你恰好有一个大学或研究实验室,或者为拥有数百万美元开发预算的技术公司工作,你也会从这本书中学到一些东西,并找到希望激发你的机器人创作或为新产品带来令人兴奋功能的有用工具和想法。
现在你已经熟悉了机器人AI的概念,让我们看看机器人实际上是什么。
机器人是什么?
词语机器人是从捷克作家卡雷尔·恰佩克的戏剧《R.U.R》中进入现代语言的,这部戏剧于1920年出版。“Roboti”是捷克语,意为“强制劳动”。在这部戏剧中,一个工业家学会了如何制造人造人——不是机械的、金属的人,而是由肉体和血液构成,并且是从工厂中完全成长起来的。将名称R.U.R*翻译成“罗素通用机器人”(Rossum’s Universal Robots)将词语机器人介绍给了世界。
为了这本书的目的,机器人是一种能够感知和对其环境做出反应的机器,并且具有某些人类或动物般的职能。我们通常认为机器人是一种自动的、自我指导的移动机器,能够与环境互动。也就是说,机器人具有物理形态并表现出某种形式的自主性,即根据对外部环境的观察做出自己决策的能力。
接下来,让我们讨论这本书中我们将试图解决的问题。
我们的示例问题——清理这个房间!
在这本书的过程中,我们将使用一个我认为大多数人都能轻松相关联的问题集,同时仍然代表了对经验丰富的机器人学家的真正挑战。我们将使用人工智能和机器人技术来在我孙子辈访问后清理我家的玩具。你刚才听到的那个声音是观众中专业机器人工程师和研究人员的惊呼声——这是一个难题。为什么这是一个难题,为什么它适合这本书?
让我们讨论这个问题,并对其进行一些分解。稍后,在第二章中,我们将进行完整任务分析,学习如何编写用例,并创建故事板来开发我们的方法,但我们可以从这里开始,做一些一般性的观察。
机器人设计师首先从环境开始考虑——机器人将在哪里工作?我们将环境分为两类:结构化和非结构化。一个结构化环境,比如FIRST机器人竞赛的赛场(这是美国高中生建造的机器人竞赛,所有赛场在比赛前都是已知的),装配线或实验室工作台,都有一个有组织的空间。你可能听说过这样的话:“物有所归,物归其位”——这就是结构化环境。另一种思考方式是,我们事先知道一切的位置或去向。我们知道物体的颜色、它们在空间中的位置以及它们的形状。这种类型的信息被称为先验知识——我们事先知道的事情。在机器人领域,对环境的先验知识有时是绝对必要的。装配线机器人期望零件以精确的位置和方向到达,以便抓取并放置到正确的位置。换句话说,我们已经安排好世界以适应机器人。
在我的房子这个世界上,这根本不是一种选择。如果我能让我的孙子孙女每次都把玩具放在完全相同的地方,那么我们就不需要机器人来完成这个任务。我们有一套相对固定的物体——他们只有这么多玩具可以玩。我们偶尔会添加一些东西或丢失玩具,或者有些东西从楼梯上掉下来,但玩具是固定物体集合的一部分。它们不是以任何特定的方式定位或定向的——它们只是孩子们玩完回家后留下的地方。我们还有一套固定的家具,但有些部分会移动——脚凳或椅子可以移动。这是一个非结构化环境,在这个环境中,机器人和软件需要适应,而不是玩具或家具。
问题是要让机器人绕着房间行驶并拿起玩具。以下是这个任务的一些目标:
-
我们希望用户通过与机器人交谈来与机器人互动。我们希望机器人能够理解我们希望它做什么,也就是说,我们给出的命令的意图是什么。
-
一旦被命令开始,机器人将必须识别一个物体是玩具还是不是玩具。我们只想拿起玩具。
-
机器人必须避免危险,最重要的是一楼下去的楼梯。机器人特别容易遇到负面障碍(悬崖、台阶、悬崖、楼梯等),这正是我们这里的情况。
-
一旦机器人找到玩具,它必须确定如何用其机器人手臂拿起玩具。它可以直接抓住物体,还是必须用勺子挖起,或者推它?我们预计机器人会尝试不同的方法来拿起玩具,并且可能需要多次尝试和错误。
-
一旦玩具被机器人手臂拿起,机器人需要将玩具携带到玩具箱。机器人必须识别房间中的玩具箱,记住它的位置以便于重复行程,然后定位自己将玩具放入箱子。再次强调,可能需要多次尝试。
-
在玩具被放下后,机器人将返回到巡逻房间寻找更多的玩具。希望最终能够找回所有的玩具。它可能需要询问我们,人类,房间是否可以接受,或者是否需要继续清洁。
我们将从这个问题中学到什么?我们将利用这个背景来检验各种人工智能技术和工具。本书的目的是教会你如何使用机器人开发人工智能解决方案。这里的关键信息是过程和方法,而不是问题,也不是我为本书开发的机器人。我们将展示如何制作一个能够学习和适应其环境的移动机器。我预计你们会根据自己的兴趣和需求挑选和阅读章节,并且按照自己的顺序,因此每一章都将是一个独立的课程。
前三章是基础材料,通过建立问题和提供坚实的框架来支持本书的其余部分。
机器人学基础
本书中的所有章节或主题并不都被认为是经典的人工智能方法,但它们确实代表了处理机器学习和决策问题的不同方式。我们将一起探讨以下主题:
-
控制理论和时间管理:我们将通过理解控制理论和时间管理来为机器人控制建立一个坚实的基础。我们将使用一种软实时控制方案,我称之为基于帧的控制循环。这项技术有一个复杂的名字——速率单调调度——但我认为你会发现这个概念直观且易于理解。
-
OODA循环:在最基本层面上,人工智能是机器人做出行动决策的一种方式。我们将介绍一个来自美国空军的决策模型,称为OODA循环。它描述了机器人(或人)是如何做出决策的。我们的机器人将有两个这样的循环,一个是内部循环或内省循环,另一个是向外看的环境传感器循环。较低的内部循环比较慢的外部循环优先级更高,就像你身体自主的部分(如心跳、呼吸和进食)比你的任务功能(如去上班、付账单和修剪草坪)优先级更高一样。这使得我们的系统成为一种吸收架构,这是一种由麻省理工学院的罗德尼·布鲁克斯(Rodney Brooks)命名的生物启发式控制范式,他是iRobot和Rethink Robotics的创始人之一,也是Baxter机器人的设计者。
图1.1 – 我版本的OODA循环
注意
OODA循环是由约翰·博伊德上校(Col. John Boyd)发明的,他也被称作F-16之父。博伊德上校的思想至今仍被广泛引用,他的OODA循环被用来描述机器人人工智能、军事规划和营销策略,具有同等效用。OODA提供了一个模型,说明了与环境互动的思考机器可能的工作方式。
我们机器人工作的方式不是简单地按步骤执行命令或指令,而是通过设定目标然后努力实现这些目标。机器人可以自由地设定自己的路径或决定如何到达目标。我们会告诉机器人拿起那个玩具,然后机器人会决定是哪个玩具,如何进入范围,以及如何拿起玩具。如果我们,作为人类机器人拥有者,试图将机器人当作遥控手来对待,我们就必须给机器人提供许多单独的指令,例如向前移动、向右移动、伸出手臂、张开手,每个动作都要单独给出,而且不向机器人说明我们为什么要做出这些动作。在以目标为导向的结构中,机器人会知道哪些物体是玩具,哪些不是,并且它会知道如何找到玩具箱以及如何把玩具放入箱中。这就是自主机器人和遥控无线电操作设备之间的区别。
在设计我们机器人和其软件的具体细节之前,我们必须将其能力与环境以及它必须解决的问题相匹配。本书将介绍一些设计机器人和管理软件开发的工具。我们将使用系统工程领域的两个工具来完成这项任务——用例和故事板。我会尽可能使这个过程简化。更高级的系统工程类型被NASA、航空航天公司和汽车公司用于设计火箭、汽车和飞机——这让你尝到了那些类型结构化过程的味道。
本书使用的技术
以下各节将逐步详细说明将人工智能技术应用于机器人问题的示例:
-
我们从物体识别开始。我们需要我们的机器人能够识别物体,并将它们分类为玩具(需要拾起)或非玩具(需要留下)。我们将使用经过训练的人工神经网络(ANN)从不同角度和光照条件下识别来自视频摄像头的物体。我们将使用迁移学习的过程来扩展现有的物体识别系统,YOLOv8,以便快速且可靠地识别我们的玩具。
-
下一个任务,一旦识别出玩具,就是将其拾起。为机器人手臂编写一个通用的拾起任何东西程序是一个困难的任务,涉及大量的高等数学(使用互联网查找逆运动学来了解我的意思)。如果我们让机器人自己解决这个问题会怎样呢?我们使用遗传算法,允许机器人发明自己的行为,并学会自己使用手臂。然后我们将使用深度强化学习(DRL)让机器人自己学习如何使用末端执行器(机器人的手)抓取各种物体。
-
我们的机器人需要理解其所有者(我们)的命令和指示。我们使用自然语言处理(NLP)不仅是为了识别语音,而且是为了理解我们的意图,以便让机器人创建符合我们期望的目标。我们使用一种我称之为填空法的巧妙技术,允许机器人从命令的上下文中进行推理。这个过程对于许多机器人规划任务都很有用。
-
机器人的下一个问题是导航房间,同时避开楼梯和其他危险。我们将结合一种独特的、无地图的导航技术与由特殊立体相机提供的3D视觉,以看到并避开障碍物。
-
机器人需要能够找到玩具箱来存放物品,以及拥有一个用于未来移动规划的一般框架。我们将使用决策树进行路径规划,并讨论剪枝或快速拒绝不良计划。如果你想象一下计算机国际象棋程序算法必须做什么,提前几步考虑,并在选择策略之前对好走和坏走的步骤进行评分,这将给你一个关于这种技术力量的概念。这种类型的决策树有许多用途,可以处理许多策略维度。我们将将其用作找到放置玩具的路径的两种方法之一。
-
我们的最后任务需要使用一套在机器人技术中不常用,或者至少不是以我们即将使用的方式使用的工具。
我有五个可爱、有才华、令人愉快的孙子孙女,他们喜欢来拜访。在整个书中,你将听到很多关于他们的故事。最大的孙子今年10岁,患有自闭症,我的孙女,第三个孩子,8岁,以及最小的男孩,6岁,也是我写这篇文章的时候。我向我的大孙子威廉介绍了这个机器人——他立刻想和它交谈。他问,“你叫什么名字?”和“你做什么?”当机器人没有回应时,他感到失望。所以对于孙子孙女们,我们将为机器人开发一个执行简短对话的引擎——我们将创建一个与孩子互动的机器人个性。威廉对这个机器人还有一个要求——他希望它能讲并回应“敲门”笑话,所以我们将使用这个作为特殊对话的原型。
虽然在机器人或AI领域,开发具有真实情感的机器人远远超出了当前的技术水平,但我们可以通过有限状态机和一些蒙特卡洛建模来模拟拥有个性。我们还将为机器人提供一个人类交互的模型,这样机器人就会考虑到孩子的情绪。我喜欢将这种类型的软件称为AP,以区别于我们的AI。AI构建思考模型,而AP为我们的机器人构建情感模型。
既然你已经了解了我们将在本书中解决的问题,让我们简要讨论一下你何时以及为什么可能需要为你的机器人使用AI。
你什么时候需要为你的机器人使用AI?
我们通常将AI描述为一种模拟或模拟过程的技术,它模仿我们的大脑如何做出决策。让我们讨论AI如何在机器人中应用,以提供可能难以通过传统编程技术实现的能力。其中之一是识别图像或图片中的对象。如果你将相机连接到计算机,计算机接收到的不是图像,而是一系列代表像素(图像元素)的数字。如果我们试图确定某个特定对象,比如玩具,是否位于图像中,那么这可能相当棘手。你可以找到形状,比如圆形或正方形,但熊玩具呢?此外,如果熊玩具是倒置的,或者平躺在表面上呢?这是AI程序可以解决的问题,而其他任何方法都无法解决。
我们创建机器人行为传统的方法是确定我们想要的函数,并编写代码来实现它。当我们有一个简单的函数,比如绕过障碍物时,这种方法效果很好,我们只需稍作调整就能得到结果。
人工智能和机器学习在机器人领域的例子包括:
-
NLP:使用AI/ML让机器人理解和回应自然的人类语言和命令。这使得与机器人的交互更加直观。
-
计算机视觉:使用AI让机器人看到并识别物体或人脸,读取文本等。这有助于机器人在现实世界环境中运行。
-
运动规划:AI可以帮助机器人规划最优路径和动作,以避开障碍物和人群。这使得机器人的动作更加高效和类似人类。
-
强化学习:机器人可以通过使用AI强化学习算法通过试错来学习如何完成任务,并提高完成任务的能力。这意味着需要的显式编程更少。
主要的指导原则是在你想要机器人在一个复杂、动态的真实世界环境中稳健地执行任务时使用AI/ML。AI赋予它更多的感知和决策能力。
现在我们来看一下这个机器人需要的一个功能——识别一个物体是玩具(需要被拿起)还是不是。通过编程创建这样一个标准功能相当困难。常规的计算机视觉过程将图像分离成形状、颜色或区域。我们的问题是玩具没有可预测的形状(圆形、方形或三角形),它们没有一致的颜色,而且大小也不一样。我们更愿意教机器人什么是玩具,什么不是。这就是我们如何对待人的。我们只需要一个过程来教机器人如何使用相机来识别特定的物体。幸运的是,这是AI领域已经深入研究的一个领域,已经有技术可以完成这项任务,我们将在第4章中使用这些技术。我们将使用卷积神经网络(CNN)从相机图像中识别玩具。这是一种监督学习,我们使用示例向软件展示我们想要识别的对象类型,然后创建一个定制的函数,根据图像中代表它的像素来预测对象的类别(或类型)。我们将应用的一个AI原则是逐步学习,使用梯度下降。这意味着我们不会试图一次性让计算机学习一项技能,而是逐步训练它,通过观察错误(或损失)并做出小的调整,温和地训练一个函数输出我们想要的结果。我们使用梯度下降的原则——观察错误变化的斜率——来确定调整训练的方向。
你可能会想,到这个时候,“如果那适用于学习分类图片,那么也许它可以用来分类其他事物”,你会是对的。我们将使用类似的方法——使用略有不同的神经网络——来教机器人通过识别声音来回应它的名字。
所以,总的来说,我们什么时候需要在机器人中使用AI呢?当我们需要模拟某种难以或无法通过程序步骤(即编程)创建的决策过程时。很容易看出,神经网络是动物思维过程的模拟,因为它们是神经元交互的(大大)简化模型。其他AI技术可能更难以理解。
一个可能的主题是,人工智能始终使用示例编程作为技术,用通用框架替换代码,用数据替换变量。我们不再使用过程编程,而是通过展示软件我们想要的结果,让软件想出如何达到那个结果。因此,对于使用图片进行物体识别,我们提供物体的图片以及图片所代表的物体类型的答案。我们反复这样做,并通过修改代码中的参数来训练软件。
我们可以用人工智能创造的另一种行为类型与行为有关。有很多任务可以被视为游戏。我们可以轻松想象它是如何工作的。假设你希望你的孩子们捡起他们房间里的玩具。你可以命令他们这样做——这可能有效也可能无效。或者,你可以通过为每个捡起的玩具奖励积分,并根据得分多少给予奖励(比如给一美元)来将其变成一个游戏。我们通过这样做增加了什么?我们增加了一个指标,或测量工具,让孩子们知道他们做得怎么样——一个积分系统。更重要的是,我们为特定的行为增加了奖励。这可以是一个我们可以用来修改或创建机器人行为的流程。这正式称为强化学习。虽然我们不能给机器人一个情感上的奖励(因为机器人没有欲望或需求),但我们可以编程让机器人寻求最大化奖励函数。然后我们可以使用调整参数以改变奖励的相同流程,看看这是否会提高得分,然后要么保留这个变化(当学习导致更多奖励时,我们的强化),要么如果得分下降就放弃它。这种类型的流程对机器人运动和机器人手臂的控制非常有效。
我必须告诉你,这本书中提出的任务——在非结构化环境中捡起玩具——没有人工智能技术几乎是不可能完成的。可以通过修改环境来完成,比如在玩具上放置RFID标签,但除此之外不行。那么,这本书的目的就是——展示某些任务,这些任务在没有人工智能和机器人技术的情况下难以或无法解决,如何通过人工智能和机器人的结合来完成。
接下来,让我们讨论本书中我们将使用的机器人和开发环境。
介绍机器人和我们的开发环境
这是一本关于机器人和人工智能的书,所以我们真的需要一台机器人来用于所有的实际示例。正如我们将在第二章中详细讨论的那样,我选择了普通读者可以接触到的机器人硬件和软件。具体品牌和类型并不重要,自从五年前第一版出版以来,我已经对阿尔伯特进行了相当大的升级。为了保持内容的时效性,我们将所有硬件细节都放在了这本书的GitHub仓库中。
如下两张不同角度拍摄的照片所示,我的机器人配备了新的全向轮、一个六自由度的机械臂和一台电脑大脑:
图1.2 – 阿尔伯特机器人有轮子和机械臂
我会称它为阿尔伯特,因为它需要某种名称,而且我喜欢它对维多利亚女王丈夫阿尔伯特亲王的引用,他因对他们的九个孩子照顾得非常好而闻名。他的九个孩子都长大成人,这在维多利亚时代是罕见的,他还有42个孙子孙女。他以他的中间名为人所知;他的真实名字是弗朗西斯。
本书中的任务主要集中在室内空间捡起玩具,因此我们的机器人有一个坚固的底盘,配备四个电机和全向轮,以便在地毯上行驶。我们的转向方法是坦克式,或差速驱动,通过向轮电机发送不同的命令来进行转向。如果我们想直行,我们将所有四个电机设置为相同的向前速度。如果我们想倒退,我们将两个电机以相同的量反转。转向是通过将一侧向前移动而另一侧向后移动(这使得机器人原地转向)或通过给一侧比另一侧更多的向前驱动来实现。我们可以用这种方式进行任何类型的转向。全向轮还允许我们做一些其他的技巧——我们可以将车轮转向彼此并直接向侧面移动,甚至可以在指向地面上同一位置的同时旋转。我们主要会像卡车或汽车一样驾驶,但偶尔会使用Y轴运动来对齐。说到轴,我会用x轴表示机器人将直线前进,y轴指的是从一侧到另一侧的水平移动,而z轴是上下移动,这是我们机器人手臂所需要的。
为了捡起玩具,我们需要某种机械臂,所以我包括了一个六轴机器人臂,它模仿了肩部-肘部-腕部-手部的组合,非常灵巧,而且由于它是由标准数字伺服电机制成的,所以连接和编程都非常简单。
Albert机器人的主要控制器是英伟达Nano单板计算机(SBC),它通过USB Wi-Fi闪存盘与操作员通信。Nvidia与Arduino Mega 2560微控制器和电机控制器通信,我们将使用它通过脉冲宽度调制(PWM)脉冲来控制电机。以下图显示了机器人的内部组件:
图1.3 – 机器人框图
我们将主要关注英伟达Nano单板计算机,它是我们机器人的大脑。我们将一次性设置其余组件,并在整本书中不会更改它们。
英伟达Nano作为我们控制站(运行Windows的PC)和机器人本身通过Wi-Fi网络之间的主要接口。几乎任何基于Linux的低功耗单板计算机(SBC)都可以执行这项任务,例如BeagleBone Black、Odroid XU4或英特尔爱迪生。Nano的一个优点是它可以使用其图形处理单元(GPU)来加速神经网络的处理。
连接到SBC的是带有电机控制器的Arduino。Nano通过一个被指定为串行端口的USB端口进行通信。我们还需要一个5V稳压器,将从11.1V可充电锂离子电池组提供适当的电源到机器人。我的电源包是一个可充电的3S1P(三节串联,一节并联)2700Ah电池(通常用于四旋翼无人机),并附带适当的充电器。与任何锂离子电池一样,遵循电池组附带的所有说明,并在发生火灾时在金属箱或容器中充电。
软件组件(ROS、Python和Linux)
我将再次指导您查看Git仓库,以查看运行机器人的所有软件,但我会在这里介绍基础知识以提醒您。正如我们所说,机器人的基础操作系统是运行在Nvidia Nano SBC上的Linux。我们使用ROS 2将所有各种软件组件连接在一起,并且它还出色地处理了所有那些棘手的网络任务,例如设置套接字和建立连接。它还附带了一个功能强大的库,我们可以直接利用,例如操纵杆接口。ROS 2不是一个像Linux或Windows那样控制整个计算机的真正操作系统,而是一个通信、接口标准和实用程序的骨干,这使得组装机器人变得更加简单。我喜欢为这种类型的系统起名为模块化开放式系统架构(MOSA)。ROS 2使用发布/订阅技术将数据从一个地方移动到另一个地方,这真正地将产生数据(如传感器和摄像头)的程序与使用数据(如控制和显示)的程序解耦。我们将制作很多自己的东西,并且只使用少数ROS函数。Packt有几本关于学习ROS的出色书籍;我最喜欢的是《有效的ROS机器人编程》。
在本书中,我们将使用一种编程语言,除了少数几个小例外,那就是Python。Python是这种用途的绝佳语言,原因有两个:它在与ROS结合使用时在机器人社区中得到了广泛的应用,同时也在机器学习和人工智能社区中得到了广泛的认可。这种双重优势使得使用Python变得无法抗拒。Python是一种解释型语言,它对我们来说有三个惊人的优势:
-
可移植性:Python在Windows、Mac和Linux之间非常便携。通常,如果你使用操作系统中的函数,如打开文件,只需进行一行或两行的更改即可。Python可以访问大量的C/C++库,这也增加了它的实用性。
-
无需编译:作为解释型语言,Python不需要编译步骤。我们在这本书中开发的一些程序相当复杂,如果我们用C或C++编写,每次我们做出更改时都需要10或20分钟的构建时间。你可以用那么多时间做很多事情,你可以用这些时间让你的程序运行,而不是等待make过程完成。
-
隔离:这是一个很少被提及的好处,但鉴于我有很多与机器人相关的操作系统崩溃的经验,我可以告诉你,Python解释器与核心操作系统隔离的事实意味着你的Python ROS程序崩溃计算机是非常罕见的。计算机崩溃意味着需要重新启动计算机,也可能丢失所有用于诊断崩溃所需的数据。我有一个从Python迁移到C++的专业机器人项目,结果操作系统崩溃开始发生,这大大降低了我们机器人的可靠性。如果一个Python程序崩溃,另一个程序可以监控它并重新启动它。如果操作系统崩溃,没有额外的硬件帮助你按下重置按钮,你几乎无能为力。
在我们深入到基础控制系统的编码之前,让我们谈谈我们将用于创建一个健壮、模块化和灵活的机器人控制系统的理论。
机器人控制系统和决策框架
如我在这章前面提到的,我们在接下来的几节中将要使用两组工具:软实时控制和OODA循环。前者为我们提供了一个基础,使我们能够轻松且一致地控制机器人,而后者为机器人的自主性提供了基础。
如何控制你的机器人
机器人工作的基本概念,尤其是那些用于驱动的机器人,是简单的。存在一个主控制循环,它反复执行相同的事情——从传感器和电机控制器读取数据,寻找操作员(或机器人的自主功能)的指令,根据这些指令对机器人的状态进行任何更改,然后向电机或执行器发送指令以使机器人移动。
图1.4 – 机器人控制循环
上述图表说明了我们如何在机器人的软件和硬件中实现OODA循环。机器人可以自主行动,或者通过无线网络接受连接的控制站的指令。
我们需要始终以一致的方式执行这个控制循环。我们需要设置一个基本帧率或基本更新频率,以设定控制循环的时间。这使得机器人的所有系统一起工作。如果没有某种形式的时间管理器,机器人的每个控制周期完成所需的时间都不同,任何路径规划、位置估计或手臂运动都会变得非常复杂。ROS本身是非同步的,因此不提供时间管理器;如果需要,我们必须自己创建一个。
使用控制循环
为了控制我们的机器人,我们必须建立某种控制或反馈回路。假设我们告诉机器人向前移动12英寸(30厘米)。机器人必须向电机发送命令以开始前进,然后有一种机制来测量12英寸的行程。我们可以使用多种方法,但让我们只使用一个时钟。机器人每秒移动3英寸(7.5厘米)。我们需要控制回路开始移动,然后在每个更新周期,或通过回路的每次时间,检查时间并查看是否已经过去了四秒钟。如果已经过去了,那么它就向电机发送一个停止命令。计时器是控制,四秒钟是设定点,电机是受控的系统。这个过程还生成一个误差信号,告诉我们应用什么控制(在这种情况下,停止)。让我们看看一个简单的控制回路:
图1.5 – 样本控制回路 – 维持水壶的温度
根据前面的图示,我们希望水壶中的温度保持恒定。阀门控制由火焰产生的热量,从而加热水壶。温度传感器检测水是否过冷、过热或恰到好处。控制器使用这些信息来控制阀门以产生更多热量。这种类型的方案被称为闭环****控制系统。
你也可以将这个过程视为一个过程。我们开始这个过程,然后获取反馈来显示我们的进度,以便我们知道何时停止或修改过程。我们可能在进行速度控制,需要机器人以特定的速度移动,或者进行指向控制,机器人指向或转向特定的方向。
让我们看看另一个例子。我们有一个带有自充电对接站的机器人,顶部有一组发光二极管(LEDs)作为光学目标。我们希望机器人直接驶入对接站。我们使用摄像头来观察对接站上的目标LED灯。摄像头生成一个误差信号,用于引导机器人向LED灯移动。LED灯之间的距离也给我们提供了到对接站的大致距离。这个过程在下图中展示:
图1.6 – 自停靠充电站的目标跟踪
让我们更详细地了解一下:
-
假设图中的LED灯在中心左侧的50%处关闭,并且机器人到目标物的距离是3英尺(1米)。我们将这个信息通过控制回路发送到电机——稍微向左转并向前行驶一点。
-
我们再次检查,LED灯更接近中心(40%),到目标物的距离是2.9英尺或90厘米。我们的误差信号略小,距离也略小。我们将不得不开发一个缩放因子来确定多少像素等于多少转速,这以全功率的百分比来衡量。由于我们使用的是固定的摄像头和镜头,这将是一个常数。
-
现在我们在这个更新周期中给电机发送一个更慢的转动和移动。我们最终正好在中心,当我们接触到对接站时,速度变为零。
对于那些正在说“但是如果你使用PID控制器……”的人,是的,你是对的——你也知道我刚刚描述了一个P或比例控制方案。我们可以添加更多的功能来帮助防止机器人由于自身的重量和惯性而超出或低于目标,以及抑制由这些超出引起的振荡。
PID控制器是一种使用三种类型输入来管理闭环控制系统的控制系统。比例控制使用检测到的误差的倍数来驱动控制。
例如,在我们的水壶中,我们测量温度的误差。如果期望的温度是100°C,而我们用温度计测量到90°C,那么温度误差就是10°C。我们需要通过打开阀门按比例增加热量。如果误差是0,那么值的改变也是0。假设我们尝试通过将阀门值改变10%来应对10°C的误差。因此,我们将10°C乘以0.01来设置我们的阀门位置为+0.1。这个0.01值是我们的P项或比例常数。
在我们的下一个示例中,我们看到我们的锅温现在是93°C,我们的误差是7°C。我们将阀门位置更改为+0.07,略低于之前。我们可能会发现,由于水的滞后性,使用这种方法,我们可能会超过期望的温度——因为水加热需要一段时间,这会在响应中造成延迟。最终,我们可能会过度加热水并超过期望的温度。防止这种情况的一种方法是在PID控制器的D项,即导数项。你还记得导数描述的是函数线的斜率——在这种情况下,我们测量的温度曲线。我们温度图的Y轴是时间,所以我们有温度变化/时间变化。为了在我们的控制器中添加一个D项,我们还添加了上一次样本误差和这次样本误差之间的差异(-10 – (-7) = -3)。我们通过将这个值乘以一个常数D来添加到我们的控制中。积分项只是误差乘以一个常数的累积总和,我们可以称之为I。我们可以修改P、I和D常数来调整(调谐)我们的PID控制器,以提供适当的响应,以控制回路——没有超调、欠调或漂移。更多解释请参阅https://jjrobots.com/pid/。这些示例的目的是指出机器控制的概念——我们必须进行测量,将它们与我们的期望结果进行比较,计算误差信号,然后多次每秒进行任何控制修正,并且持续这样做是实时控制的概念。
控制回路类型
为了以一致的时间间隔执行我们的控制回路(或者使用正确的术语,确定性地),我们有两种控制程序执行的方法:软实时和硬实时。硬实时控制系统需要计算机硬件的帮助——这就是标题中“硬”的部分的由来。硬实时通常需要一个实时操作系统(RTOS)或对处理器中所有计算机周期的完全控制。我们面临的问题是,运行操作系统的计算机始终被其他进程、线程链、上下文切换和执行任务所中断。你在桌面计算机或甚至智能手机上的经验是,启动相同的过程,如启动文字处理程序,每次启动时似乎总是需要不同的时间。
在实时系统中,我们需要提前确切知道一个进程将花费多长时间,甚至到微秒级别,这种行为是无法容忍的。你可以很容易地想象,如果我们为飞机创建了一个自动驾驶仪,它不是管理飞机的方向和高度,而是不断地被磁盘驱动器访问或网络调用中断,这些调用会破坏控制循环,导致平稳的飞行或跑道上的着陆,会出现什么问题。
实时操作系统(RTOS)系统允许程序员和开发者完全控制进程何时以及如何执行,以及哪些例程可以中断以及中断多长时间。RTOS系统中的控制循环在每次循环中总是消耗相同数量的计算机周期(因此是时间),这使得当输出至关重要时,它们既可靠又可信赖。重要的是要知道,在硬实时系统中,硬件强制执行时间约束,并确保计算机资源在需要时可用。
我们实际上可以在Arduino微控制器中实现硬实时,因为它没有操作系统,一次只能执行一个任务或运行一个程序。我们的机器人也将拥有一个更强大的处理器,即运行Linux的Nvidia Nano。这台计算机拥有一些真正的实力,可以同时执行多项任务以支持操作系统,运行网络接口,将图形发送到输出HDMI端口,提供用户界面,甚至支持多个用户。
软实时是一种更为宽松的方法,更适合我们的游戏室清洁机器人,而不是一个安全关键的硬实时系统——此外,RTOS可能很昂贵(有开源版本)并且需要特殊培训。我们将要做的是将我们的控制循环视为一个反馈系统。我们将在每个循环的末尾留出一些额外的空间——比如说大约10%——以便操作系统完成其工作,这应该会给我们留下一个执行在恒定时间间隔上的一致控制循环。就像我们刚才讨论的控制循环示例一样,我们将进行测量,确定误差,并对每个循环应用校正。
我们不仅担心我们的更新速率。我们还必须担心抖动,即由于操作系统被中断并执行其他操作而引起的定时循环中的随机变化。中断会导致我们的定时循环变长,导致周期时间的随机跳跃。我们必须设计我们的控制循环来处理软实时中一定量的抖动,但这些事件相对较少。
运行控制循环
实际上运行控制循环的过程相当简单。我们首先初始化计时器,它需要是高分辨率时钟。我们用Python编写控制循环,所以我们将使用time.time()
函数,该函数专门设计用来测量我们内部程序的时间性能(设置帧率,执行循环,测量时间,生成错误,睡眠以纠正错误,循环)。每次我们调用time.time()
,我们都会得到一个浮点数,这是从Unix时钟以来的秒数,并且具有Nvidia Nano上的微秒级分辨率。
这个过程的理念是将我们的处理分成一组固定的时间间隔,我们称之为帧。我们做的所有事情都将适合在整数的帧数内。我们的基本运行速度将以每秒30 帧(fps)的速度处理。这就是我们将更新机器人的位置估计、读取传感器和向电机发送命令的速度。我们还有运行速度低于30 fps的其他函数,因此我们可以将它们均匀地分配到帧之间。一些函数每帧运行一次(30 fps)并且每帧被调用和执行。
假设我们有一个只能每秒更新10次的声纳传感器。我们每隔三帧调用一次读取声纳函数。我们将所有函数分配为基本30 fps帧率的倍数,因此如果我们每帧调用函数,我们将有30 fps、15 fps、10 fps、7.5 fps、6 fps、5 fps、4.28 fps、2 fps和1 fps。我们甚至可以做到小于1 fps – 每隔60帧调用一次的函数每2秒执行一次。
困难之处在于我们需要确保每个过程都适合在一个帧时间内完成 – 这相当于1/30秒或0.033秒或33毫秒。如果过程需要更长的时间,我们必须将其分成几个部分,或者在一个单独的线程或程序中运行,这样我们可以在一个帧中开始过程,并在另一个帧中得到结果。尝试平衡帧也很重要,以便不是所有的处理都落在同一个帧上。以下图显示了基于30 fps基本速率的任务调度系统。在这里,我们有四个任务需要处理:任务A以15 fps运行,任务B以6 fps运行(每五帧一次),任务C以10 fps运行(每三帧一次),任务D以30 fps运行(每帧一次):
图1.7 – 基于帧的任务调度
我们对调度表的第一遍(图的最上方)显示所有四个任务在帧1、13和25上落在同一个帧上。如果我们像图的下半部分所示的那样在第二帧延迟任务B的开始,我们可以改善控制程序负载的平衡。
这类似于音乐中的度量方式,其中度量是一定的时间,不同的音符有不同的间隔——一个全音符每度量只能出现一次,一个二分音符可以出现两次,一直到最后是64分音符。就像作曲家确保每个度量有正确数量的拍子一样,我们也可以确保我们的控制循环在每个帧中执行平衡的度量过程。
让我们先写一个小程序来控制我们的定时循环,并让你玩这些原则。
这很令人兴奋——我们第一次一起编写代码。这个程序只是演示了我们将在主机器人控制程序中使用的定时控制循环,并且在这里让你可以玩一些参数并查看结果。这是我认为可能的最简单的软时间控制循环版本,所以请随意改进和装饰它。我已经为你制作了一个流程图,以帮助你更好地理解:
图 1.8 – 软实时控制器流程图
让我们更仔细地看看前面图中使用的术语:
-
帧时间:我们分配给执行循环一次迭代的时间
-
开始时间:循环/帧开始时
-
进行大量数学运算:你正在管理的程序
-
停止时间:帧完成时
-
剩余时间:已过时间与期望帧时间的差
-
已过时间:实际运行一次循环所需的时间
-
帧睡眠时间:我们使用剩余时间来告诉计算机睡眠,以便帧正好花费我们想要的时间。
现在,我们将开始编码。这是一段相当直接的Python代码——我们不会在后面变得复杂:
-
我们首先导入我们的库。我们首先从
time
模块开始并不奇怪。我们还将使用numpy
(Python数值分析)中的mean
函数和matplotlib
来在最后绘制我们的图表。我们还将进行一些数学计算来模拟我们的处理并创建对帧率的负载:import time from numpy import mean import matplotlib.pyplot as plt import math #
-
现在我们有一些参数来控制我们的测试。这是你可以尝试不同定时的地方。我们的基本控制是
FRAMERATE
——我们想要尝试每秒更新多少次?让我们从30
开始,就像我们在之前讨论的例子中做的那样:# set our frame rate - how many cycles per second to run our loop? FRAMERATE = 30 # how long does each frame take in seconds? FRAME = 1.0/FRAMERATE # initialize myTimer # This is one of our timer variables where we will store the clock time from the operating system. myTimer = 0.0
-
测试的持续时间由
counter
变量设置。测试将花费的时间是FRAME
时间乘以counter
中的循环次数。在我们的例子中,2,000帧除以30 fps等于66.6秒,或者略超过一分钟来运行测试:# how many cycles to test? counter*FRAME = runtime in seconds counter = 2000
我们将以两种方式控制我们的定时循环:
- 我们将首先测量执行此帧计算所需的时间。我们有一个带有一些我们将调用的三角函数的示例程序,以向计算机添加负载。例如,机器人控制函数,如计算机器人臂所需的角,需要大量的三角运算。这可以从程序头部的
import math
中获取。
- 我们将首先测量执行此帧计算所需的时间。我们有一个带有一些我们将调用的三角函数的示例程序,以向计算机添加负载。例如,机器人控制函数,如计算机器人臂所需的角,需要大量的三角运算。这可以从程序头部的
注意
我们将测量控制函数运行的时间,这将占用我们帧的一部分。然后我们计算我们帧剩余的部分,并告诉计算机在这段时间内睡眠此进程。使用sleep
函数释放计算机去处理操作系统中的其他事务,这是一种比运行某种紧密循环来浪费我们帧剩余时间更好的标记时间的方法。
-
我们控制循环的第二种方式是通过测量整个帧的时间——计算时间加上休息时间——并查看我们是否超出了或低于帧时间。我们使用
TIME_CORRECTION
为此功能调整睡眠时间,以考虑睡眠函数的变异性以及从操作系统返回的任何延迟:# factor for our timing loop computations TIME_CORRECTION= 0.0
-
我们将在程序结束时收集一些数据来绘制一个抖动图。我们使用
dataStore
结构来完成这项工作。让我们在屏幕上放一个标题来告诉您程序已经开始,因为完成它需要一段时间:# place to store data dataStore = [] # Operator information ready to go # We create a heading to show that the program is starting its test print "START COUNTING: FRAME TIME", FRAME, "RUN TIME:",FRAME*counter
-
在这一步,我们将设置一些变量来测量我们的时间。正如我们提到的,目标是有一系列计算帧,每个帧的长度都相同。每个帧有两个部分:
myTime
是帧的顶部时间,即帧开始时的时间。newTime
是工作周期计时器的结束。我们使用masterTime
来计算程序运行的总时间:# initialize the precision clock myTime = newTime = time.time() # save the starting time for later masterTime=myTime # begin our timing loop for ii in range(counter):
-
这个部分是我们的有效载荷——执行工作的代码部分。这可能是臂角计算、状态估计或命令解释器。我们将插入一些三角函数和一些数学运算,让CPU为我们做一些工作。通常,这个工作部分是我们帧的大部分,所以让我们重复这些数学术语1,000次:
# we start our frame - this represents doing some detailed math calculations # this is just to burn up some CPU cycles for jj in range(1000): x = 100 y = 23 + ii z = math.cos(x) z1 = math.sin(y) # # read the clock after all compute is done # this is our working frame time #
-
现在我们读取时钟以找到工作时间。我们现在可以计算出在下一个帧之前需要睡眠进程多长时间。重要的是工作时间 + 睡眠时间 = 帧时间。我将称这个为
timeError
:newTime = time.time() # how much time has elapsed so far in this frame # time = UNIX clock in seconds # so we have to subract our starting time to get the elapsed time myTimer = newTime-myTime # what is the time left to go in the frame? timeError = FRAME-myTimer
我们在这里向前传递一些来自前一帧的信息。
TIME_CORRECTION
是我们对前一帧时间中任何时间错误的调整。我们在开始循环之前将其初始化为零,以避免在这里出现未定义变量错误。我们还进行了一些范围检查,因为我们可能会因为操作系统而得到一些大的抖动,这可能导致如果我们尝试睡眠负时间,我们的睡眠计时器会崩溃:
注意
我们使用Python的max
函数作为快速将睡眠时间限制为零或更大的方法。它返回两个参数中较大的一个。另一种方法是类似这样的代码:if a < 0 : a = 0
。
# OK time to sleep
# the TIME CORRECTION helps account for all of this clock
reading
# this also corrects for sleep timer errors
# we are using a porpotional control to get the system to
converge
# if you leave the divisor out, then the system oscillates
out of control
sleepTime = timeError + (TIME_CORRECTION/2.0)
# quick way to eliminate any negative numbers
# which are possible due to jitter
# and will cause the program to crash
sleepTime=max(sleepTime,0.0)
-
因此,这是我们实际的睡眠命令。
sleep
命令并不总是提供精确的时间间隔,因此我们将检查错误:# put this process to sleep time.sleep(sleepTime)
-
这是时间校正部分。我们计算出我们的帧时间总共有多长(工作和睡眠时间)并从我们希望帧时间达到的值(
FrameTime
)中减去。然后我们将时间校正设置为该值。我还会将测量的帧时间保存到数据存储中,这样我们就可以使用matplotlib
来绘制我们之后的图表。这种技术是Python更有用的特性之一:#print timeError,TIME_CORRECTION # set our timer up for the next frame time2=time.time() measuredFrameTime = time2-myTime ##print measuredFrameTime, TIME_CORRECTION=FRAME-(measuredFrameTime) dataStore.append(measuredFrameTime*1000) #TIME_CORRECTION=max(-FRAME,TIME_CORRECTION) #print TIME_CORRECTION myTime = time.time()
这完成了程序的循环部分。这个例子做了每秒30帧的2000个周期,并在66.6秒内完成。你可以尝试不同的周期时间和帧率。
-
现在我们已经完成了程序,我们可以制作一个小报告和图表。我们打印出帧时间和总运行时间,计算平均帧时间(总时间/计数器),并显示我们遇到的平均误差,这可以通过平均
dataStore
中的数据来获得:# Timing loop test is over - print the results # # get the total time for the program endTime = time.time() - masterTime # compute the average frame time by dividing total time by our number of frames avgTime = endTime / counter #print report print "FINISHED COUNTING" print "REQUESTED FRAME TIME:",FRAME,"AVG FRAME TIME:",avgTime print "REQUESTED TOTAL TIME:",FRAME*counter,"ACTUAL TOTAL TIME:", endTime print "AVERAGE ERROR",FRAME-avgTime, "TOTAL_ERROR:",(FRAME*counter) - endTime print "AVERAGE SLEEP TIME: ",mean(dataStore),"AVERAGE RUN TIME",(FRAME*1000)-mean(dataStore) # loop is over, plot result # this lets us see the "jitter" in the result plt.plot(dataStore) plt.show()
我们程序的结果在下面的代码块中显示。请注意,平均误差仅为0.00018秒,或者说在33毫秒的帧中只有0.18毫秒:
START COUNTING: FRAME TIME 0.0333333333333 RUN TIME: 66.6666666667 FINISHED COUNTING REQUESTED FRAME TIME: 0.0333333333333 AVG FRAME TIME: 0.0331549999714 REQUESTED TOTAL TIME: 66.6666666667 ACTUAL TOTAL TIME: 66.3099999428 AVERAGE ERROR 0.000178333361944 TOTAL_ERROR: 0.356666723887 AVERAGE SLEEP TIME: 33.1549999714 AVERAGE RUN TIME 0.178333361944
下图显示了我们的程序计时图:
图1.9 – 我们程序的计时图
图像中的尖峰是由操作系统中断引起的抖动。你可以看到程序在相当狭窄的范围内控制帧时间。如果我们不提供控制,随着程序的执行,帧时间会越来越大。图表显示帧时间保持在狭窄的范围内,并不断回到正确的值。
现在我们已经锻炼了我们的编程肌肉,我们可以将这种知识应用到我们的机器人主控制循环中,实现软实时控制。这个控制循环有两个主要功能:
-
响应来自控制站的命令
-
Arduino Mega中与机器人电机和传感器的接口
我们将在第7章中详细讨论。
摘要
在本章中,我们介绍了人工智能的主题,这一主题将在整本书中得到强调。我们确定了人工智能机器人和普通机器人之间的主要区别,即人工智能机器人可能是非确定性的。这意味着它可能对相同的刺激有不同的反应,这是由于学习造成的。我们介绍了本书将使用的主题,即在一个游戏室里捡起玩具并将它们放入玩具箱。接下来,我们讨论了人工智能机器人两个关键工具:OODA循环,它为我们机器人如何做出决策提供了一个模型,以及软实时控制循环,它管理和控制我们程序执行的速率。我们在计时循环演示中应用了这些技术,并开始开发我们的主要机器人控制程序。
在下一章中,我们将教机器人识别玩具——我们希望机器人捡起并放回的对象。我们将使用带有视频摄像头的计算机视觉来寻找和识别地板上留下的玩具。
问题
-
缩写PID代表什么?这被认为是人工智能软件方法吗?
-
图灵测试是什么?您觉得这是评估AI的有效方法吗?
-
您认为为什么机器人会与负向障碍物(如楼梯和坑洼)有问题?
-
在OODA循环中,Orient步骤做什么?
-
从Python及其优势的讨论中,计算以下内容。您的程序需要测试50次更改。假设每次更改都需要重新编译步骤和一个运行步骤来测试,C Make编译需要450秒,Python
run
命令需要3秒。您在等待编译器时空闲了多少时间? -
RTOS代表什么?
-
您的机器人有以下预定任务:遥测频率为10 Hz,GPS频率为5 Hz,惯性测量频率为50 Hz,电机控制频率为20 Hz。您将如何安排基础任务的频率,以及您将使用什么间隔来安排较慢的任务(例如,10 Hz的基础频率,每三帧一次电机控制,每两帧一次遥测等)?
-
假设一个帧率调度器最快的任务频率为20 fps,您会如何安排需要以7 fps运行的任务?对于以3.5 fps运行的任务呢?
-
什么是阻塞调用函数?为什么在像机器人这样的实时系统中使用阻塞调用是不好的?
进一步阅读
您可以参考以下资源以获取更多详细信息:
-
《ROS高效机器人编程 - 第三版》,作者Anil Mahtani,Luis Sanchez,Enreque Fernandez Perdomo,Packt Publishing,2016
-
《人工智能机器人导论 - 第二版》,作者Robin R. Murphy,Bradford Books,2019
-
《实时调度:从硬实时到软实时系统》,Palopoli Lipari撰写的一份白皮书,2015 (https://arxiv.org/pdf/1512.01978.pdf)
-
《改变战争艺术的飞行员:博伊德》,作者Robert Coram,Little, Brown and Company,2002
第二章:设置您的机器人
本章从我对机器人是什么以及机器人由什么组成的思考开始,这是一个相当标准的部件和组件列表。本章旨在让您能够复制练习并使用书中找到的源代码。我将描述我是如何设置我的开发环境的,我使用了哪些工具来创建我的代码,以及如何安装机器人操作系统版本2(ROS 2)。我使用的示例机器人Albert的组装可以在本书的GitHub仓库中找到。还有许多其他类型和配置的机器人可以通过对本书中的代码进行一些修改来与之配合工作。我将尝试提供所有可能的快捷方式,包括我机器人SD卡的全图,在Git仓库中。
在本章中,我们将涵盖以下主题:
-
理解机器人的解剖结构
-
介绍吸收架构
-
ROS简介
-
软件设置:Linux,ROS 2,Jetson Nano,和Arduino
技术要求
要完成本章中的实践练习,您将需要本书开头前言中指定的要求。本章的代码可以在https://github.com/PacktPublishing/Artificial-Intelligence-for-Robotics-2e/找到。
理解机器人的解剖结构
机器人是一种能够自行执行复杂动作和行为的技术设备。大多数机器人由计算机或数字可编程设备控制。机器人的一些关键特性如下:
-
自动化:机器人可以在没有直接人类输入的情况下自动运行,基于它们的编程。这使得它们可以持续地执行重复性或危险的任务。
-
传感器:机器人使用摄像头、光学、激光雷达和压力传感器等传感器来收集有关其环境的信息,以便它们可以导航和交互。这些感官信息被处理以确定机器人应采取哪些行动。
-
编程:机器人的“大脑”由一个运行代码和算法的机载计算机或设备组成,这些代码和算法定义了它的行为方式。机器人由人类编程以执行所需的行为。
-
移动:大多数机器人能够通过轮子、腿、螺旋桨或其他运动系统在一定程度上移动。这使得它们能够在环境中移动以执行任务。
-
交互:高级机器人可以通过语音、视觉显示、灯光、声音、物理手势等方式与人类进行交流。这允许有用的人类-机器人交互和工作。
-
自主性:虽然机器人由人类编程,但它们在实现目标的方式上具有一定的自我治理和独立性。在没有人类监督的情况下采取行动和做出决策的能力是它们的自主性。
总结来说,机器人集成了自动化、感知、移动、编程和自主性,以可靠地执行可能复杂、重复、不安全或不适于人类的工作。它们形状和大小各异,从工业机器人臂到社交伴侣机器人,再到自动驾驶汽车。
有一个相当标准的组件和部件集合,构成了绝大多数机器人的主体。即使是外表差异很大的自动驾驶汽车、制造汽车的焊接机器人,以及Roomba吸尘器,它们也有很多相同的组件或部件。有些可能会有更多,有些可能会有更少,但大多数移动机器人将具有以下几类部件:
图2.1 – 典型移动机器人的框图
让我们更详细地看看这些组件:
-
计算机:运行控制机器人的程序的单元。这可以是一台传统计算机、一个微控制器、一个单板计算机(SBC)如我们所拥有的,或者某种其他类型的处理器,它发送和接收命令。机器人臂和一些类型的工业机器人将使用可编程逻辑控制器(PLC),这是一种特殊的控制器,它将逻辑(AND、OR、NOT)应用于各种输入以产生输出。为了使计算机能够向机器人发送命令并接收遥测数据,我们需要某种类型的传感器接口,例如USB端口、串行端口、通用输入/输出(GPIO)端口,或者如以太网或Wi-Fi之类的网络接口。
-
控制站或人机界面(HRI):机器人被设计来执行任务,这要求操作员必须有一些方式来发送和接收来自机器人的数据,并监督机器人是否表现正确。我们将使用笔记本电脑或台式计算机来完成这个功能,并且我们将通过无线网络与机器人进行通信。我们的控制站向机器人发送命令,并从机器人那里接收遥测数据,这些数据以数据、视频或音频的形式存在。
-
无线电或数据链路:移动机器人,如我们在本书中设计的机器人,能够移动和探索它们的环境。虽然通过绳索或电线向机器人发送命令是可能的,但首选的方式是使用无线电链路。无线网络如Wi-Fi和蜂窝数据服务的普遍可用性使得创建数据链路变得容易得多。我有很多机器人项目,其中网络链路不可用或不切实际,需要设计定制无线电解决方案。在机器人中使用的其他类型的无线电包括蓝牙、Zigbee以及各种网状网络系统,如Flutter。
-
电机或执行器:我们定义的机器人包括自主推进的能力;也就是说,机器人能够移动。为了移动,机器人需要电机或一组电机。我们的机器人,阿尔伯特,有十个电机,四个用于驱动,六个用于控制机器人手臂和手。电机将电能转化为运动。有各种不同的类型,选择正确的电机是一项挑战。你必须匹配扭矩(电机能拉多硬),电机轴的转速(每分钟的转数),以及电压。以下是选择机器人驱动系统电机时需要考虑的一些关键因素:
-
扭矩:考虑机器人运动和负载处理所需的扭矩。更大的扭矩允许更快的加速和承载更重负载的能力。如果扭矩不足,机器人会“陷入困境”或使电机停转。电机在停转时(它被供电但不动)会拉取最多的电流。所有这些无法使用的能量都会转化为热量,最终会熔化电线或引起火灾。
-
速度:确定机器人需要运行的速度。更高的速度需要具有更高RPM(每分钟转数)的电机。我们只希望我们的机器人以适度的速度行驶。玩具无法逃脱。
-
负载周期:选择一个可以在不过热的情况下连续运行机器人所需负载周期的电机。间歇性负载周期允许使用更小、更轻的电机。我们将驾驶或移动很多——大约50%的时间,但不会太快。
-
尺寸和重量:大型、重型电机提供大量功率,但可能会限制机器人设计。考虑整个驱动系统的尺寸和重量。记住电机本身也需要移动。
-
控制:无刷直流电机需要电子速度控制器。步进电机允许开环位置控制。伺服电机,如机器人手臂中的电机,具有集成编码器,并通过串行接口控制。我使用的驱动电机是刷式电机,通过改变电压来控制,我们通过脉冲宽度调制(PWM)来控制电压。
-
电压:高电压允许小型电机输出更多功率。选择与其它电子设备兼容的电压。我的电池是7.2伏,与选定的电机相匹配。
-
噪音:家庭/办公室机器人可能需要安静的电机。无刷、减速电机很安静但价格昂贵。齿轮传动系统也很嘈杂。
-
成本:更强大的电机成本更高。在性能需求和预算限制之间取得平衡。阿尔伯特的刷式电机非常便宜。
一些机器人电机还配备了变速箱以降低电机速度,基本上是以速度换取扭矩。阿尔伯特的电动电机具有减速变速箱,允许电机以比车轮更快的速度运行。
为机器人提供运动的方式有很多。我们把这些使机器人移动的“东西”称为执行器。执行器的限制仅在于你的想象力,包括气动(由压缩空气驱动的装置)、液压(由不可压缩流体驱动的装置)、线性执行器(将旋转运动转换为线性运动的装置)、旋转关节或回转关节(如肘关节一样的角关节)以及甚至一些异类执行器,如形状记忆合金或压电晶体,当施加电力时它们会改变形状。
-
-
伺服电机:我们机器人中的一些电机属于一种特殊的电机类别,称为伺服电机。伺服电机具有反馈机制和控制回路,用于维持位置或速度。反馈由某种传感器提供。我们使用的伺服电机由一个小型电动机驱动一个由一系列齿轮组成的变速箱,这些齿轮降低了速度并相应地增加了电机的扭矩。我们使用的传感器是一个电位计(可变电阻),可以测量输出齿轮轴的角度。当我们向伺服电机发送命令时,它会告诉电机设置到特定的角度。角度由传感器测量,电机位置与传感器之间的任何差异都会产生一个错误信号,该信号将电机移动到正确的方向。你可以听到电机发出很多噪音,因为电机需要通过七个减速齿轮转动多次才能使机械臂移动。变速箱使我们能够在不消耗太多电流的情况下获得大量的扭矩。
图2.2展示了如何使用脉冲位置调制(PPM)来控制伺服电机。要控制伺服电机,你必须生成一个特定宽度的脉冲:
图2.2 – 伺服电机控制通过PPM信号
短脉冲将伺服电机移动到其范围的起始位置。中等脉冲(1,500微秒)是伺服电机位置的中间。晚脉冲会导致伺服电机移动到其范围的末端。我在这个版本的机器人中使用的机械臂有一个与机械臂硬件一起提供的伺服控制器。我们将通过串行命令控制这个控制器,具体在第五章中介绍。
- 电机控制器或电子速度控制器:电机本身并不很有用——你需要将控制计算机的命令转换为电机运动的能力。由于电机需要的电压和电流比控制计算机(我们的Jetson Nano)能提供的要多,我们需要一个设备将小的数字信号转换为大的模拟电压和电流。这个设备被称为电机控制器。这个控制器我必须单独购买,并且由两部分组成——一个Arduino Uno和一个连接到其上的电机控制器屏蔽板:
图2.3 – 我为Albert使用的电机控制器屏蔽板
如图中所示,四个电机线连接到字母连接处。
-
由于我们有一个坦克式驱动机器人(我们通过以不同速度运行电机来转向,也称为差速驱动),我们还需要电机能够向前或向后运行。电机控制器接收一个特殊的输入信号,称为脉冲宽度调制(PWM)。PWM是一个重复信号,其中电压开启和关闭。电机的油门(电机转速)与PWM信号保持开启状态的时间成正比。
电机控制器有几种类型的连接,由于提供的高电压和大电流,必须仔细接线。这可以通过以下步骤完成:
-
有两个控制线输入——一个用于速度(PWM信号)和另一个是方向信号。我们通过改变方向信号将电机置于倒车状态——1表示前进,0表示后退。
-
接下来我们需要的是地面——确保发送PWM信号(在我们的例子中,是Arduino Mega)和控制电机有它们的接地线连接是非常重要的。
-
接下来,电机控制器需要电机电压和电流,这些我们可以直接从我们的电池中获得。
-
最后,我们将每个电机的两根线连接到控制器上。有趣的是,我们并不关心哪根线连接到电机的哪一侧,因为我们可以同时向前和向后运行。如果电机转向错误,只需交换两根线。这是唯一一次在科幻电影之外说“只需反转极性”的时候。
我们将在在线附录中介绍示例机器人——Albert——的具体接线。
-
-
传感器:为了让机器人,一个可以移动并对环境做出反应的机器,能够看到其周围的环境,它需要传感器。传感器从机器人的外部或内部获取信息并将其转换为数字形式。如果我们使用数字摄像头传感器,它将光转换为数字像素(图像元素),记录为数组。一个声纳传感器通过发送能量脉冲(声波)并监听回声前的延迟时间来测量到物体的距离,例如墙壁。测量延迟时间给我们提供了到物体的距离,因为声速相对恒定。在我们的Albert项目中,机器人有几种类型的传感器:
-
我们的主要传感器是一个广角视频摄像头,我们将用它来避开障碍物和检测物体。
-
我们还将使用一个麦克风来监听声音并执行语音识别。
-
我们在本列表中之前提到过伺服电机——每个伺服电机都包含一个角度传感器,它可以检测旋转量并允许我们控制机械臂和手。
-
我们有我们的紧急停止按钮,它连接到Arduino,是一种触觉(触摸)传感器。当按钮被按下时,机器人可以将其解释为停止命令。
-
我选择的机器人手臂有一个方便的电压监控器,我们将用它来跟踪剩余的电池寿命(充电)。
-
在下一节中,我们将讨论机器人软件架构,它将作为我们创建的自主行为的框架。
介绍吞没式架构
在这一点上,我想花点时间讨论一下吞没式架构背后的理念,并指出我们将如何在我们的机器人项目设计中使用这个概念的一些具体细节。你们中的许多人可能在学校或学习中已经熟悉了这个概念,所以你们可以看看我的图,然后继续前进。对于其他人,让我们谈谈这个受生物学启发的机器人概念。
吞没式架构最初由麻省理工学院教授罗德尼·布鲁克斯博士描述,他后来帮助创立了iRobot公司并发明了Baxter机器人。罗德尼试图开发昆虫大脑的类似物,以便了解如何编程智能机器人。在此之前的机器人(1986年)基本上是单线程机器,一次只能做一件事。它们读取传感器,做出决定,然后行动——在任何时候只有一个目标。像苍蝇或蚂蚁这样的生物拥有非常简单的头脑,但仍然能够在现实世界中发挥作用。布鲁克斯推理认为,存在多个同时进行的闭环反馈过程层。
吞没式的基本概念已经存在了一段时间,自从首次引入以来,它已经被适应、重用、改进和简化。我在这里展示的是我对如何将吞没式概念应用于我们试图达成的机器人环境中的解释。
首先要理解的是,我们希望我们的机器人能够根据一系列目标行动。机器人并不是简单地对每个刺激做出完全独立的反应,而是执行某种以目标为导向的行为。目标可能是捡起玩具或导航房间,避开障碍物。我们正在创建的范例是让用户为机器人设定目标,机器人决定如何实现这些目标,即使目标仅仅是向前移动一米。
问题始于机器人需要在同一时间记住多个目标。机器人不仅仅是四处驾驶,还要在避开障碍物的同时寻找可以捡起的玩具。我们如何在这不同的目标之间进行仲裁,以确定哪个目标具有优先级?答案可以在下面的图中找到:
图2.4 – 吞没式架构示例
我们将把机器人的决策系统分为三个层次,每个层次都有不同的责任级别和不同的时间尺度。
在最低层,我们可以称之为机器人的自主神经系统——它包含机器人内部的健康保持和监控功能。这些过程运行得非常快——大约每秒20次,或者说20赫兹(Hz),并且只处理机器人内部的事情。这包括读取内部传感器、检查电池电量以及读取和响应心跳消息。我把这个层次标记为照顾好自己。
重要注意事项
什么是心跳消息?每秒一次,我会让控制站向机器人发送一个特殊的心跳消息,这个消息的时间标签精确到毫秒,即主机的时钟时间。这个消息传到控制计算机,并重复将心跳消息发送回主机。我们可以通过比较时间标签来看到我们消息的延迟——我们的命令延迟。我们希望看到心跳的往返时间小于25毫秒。如果机载计算机不工作或被锁定,那么时间标签就不会返回,我们就知道机器人出现了问题。
下一层处理单个任务,例如驾驶或寻找玩具。这些任务是短期性的,处理传感器可以看到的事情。决策的时间周期在秒的范围内,因此这些任务的更新率可能是1或2赫兹,但比内部检查慢。我把这个层次称为完成任务——你可能称之为驾驶车辆或操作有效载荷。
最后一层和最高层是专门用于完成任务的部分,它处理机器人的整体目的。这一层有寻找玩具、捡起它们然后放回原处的整体状态机,这是这个机器人的任务。这一层还处理与人类交互和响应命令。顶层处理需要几分钟甚至几小时才能完成的任务。
吞没架构的规则——甚至它的名字从何而来——与这些层级中进程的优先级和交互有关。规则如下(这是我的版本):
-
每一层只能与相邻的层通信。顶层只与中间层通信,底层也只与中间层通信。中间层可以与顶层和底层通信。
-
低层次的层级具有最高优先级。低层有中断或覆盖高层命令的能力。
想想这个问题。我给你举了一个在房间里驾驶我们机器人的例子。最低层检测障碍物。中间层将机器人驱动到特定方向,顶层则指导任务。从上到下,最高层被命令去清理房间,中间层被命令去四处驾驶,底层则接收到左电机和右电机前进60%油门的命令。现在,底层检测到一个障碍物。它中断了四处驾驶功能,并覆盖了来自顶层的命令,使机器人避开障碍物。一旦障碍物被清除,最低层将控制权交还给中间层以确定驾驶方向。
另一个例子是,如果最低层失去了心跳信号,这表明软件或硬件出现了问题。最低层会停止电机,覆盖来自上层的一切命令。无论他们想要什么;机器人出现了故障,需要停止。这种最低层具有最高优先级的优先级反转是我们称之为吸收架构的原因,因为高层吸收——整合——低层的功能以执行其任务。
这种组织方式的主要好处是它使程序清晰,明确哪些事件、故障或命令比其他事件、故障或命令具有优先级,并防止机器人陷入犹豫不决的循环。
每种机器人的架构中可能具有不同数量的层级。你甚至可以有一个监督层来控制多个其他机器人,并为机器人团队设定目标。我迄今为止最多使用过五个层级,这被应用在我的一个自动驾驶汽车项目中。
现在,让我们看看这本书中你需要了解的最重要概念之一——ROS。
ROS 简介简述
好的,在我们完成以下部分描述的所有工作以使用 ROS 2——机器人操作系统的第二个版本——之前,让我们回答你的问题。ROS是什么,它的优点是什么?
首先要知道的是,ROS不是一个真正的操作系统,如Linux或Windows。相反,它是一个中间件层,作为连接不同程序以协同工作控制机器人的手段。它最初是为运行 Willow Garage 的 PR2 机器人而设计的,这个机器人确实很复杂。ROS 由一个非常大的开源社区支持,并且不断更新。
我曾经是 ROS 的怀疑者,坦白说,阅读文档并没有帮助我第一次对它最起码觉得它很繁琐,难以使用。然而,在一位商业伙伴的坚持下,我们开始使用 ROS 为一个名为 RAMSEE 的非常复杂的自主保安机器人,由 Gamma 2 Robotics 设计:
图 2.5 – RAMSEE,由作者设计的安保机器人
我很快意识到,虽然使用 ROS 的初始学习曲线很陡峭,但回报是能够创建和实施模块化、易于移植的服务,这些服务可以独立开发。我无需将所有内容组合成一个程序,甚至在一个 CPU 中。我可以利用我的多核计算机来运行独立的过程,甚至拥有多个计算机,并将事物自由地从一台移动到另一台。RAMSEE 有一个拥有八个核心的计算机和另一个拥有四个核心的计算机。
重要提示
ROS 可以描述为 模块化开放式系统软件(MOSA)。它提供了一个标准接口,允许程序通过 发布-订阅 模式相互通信。这意味着一个程序发布数据,使其可供其他程序使用。需要这些数据的程序会订阅这些数据,并在有新数据可用时收到消息。这使得我们可以独立开发程序,并在程序之间创建标准化的接口。这确实使创建机器人变得更加容易,并且更加灵活。
另一个主要优势,并且值得所有麻烦,是 ROS 拥有一个非常大的库,其中包括传感器、电机、驱动器和执行器的现成接口,以及所有可想象到的机器人导航和控制工具。例如,我们将使用 OAK-D 3D 深度相机,该相机在 https://github.com/luxonis/depthai-ros 提供了 ROS 2 驱动器。
RViz2 工具提供了您所有传感器数据的可视化,以及展示定位和导航过程。我非常欣赏 ROS 中包含的日志和调试工具。您可以将数据记录到 ROSBag 中——任何跨发布/订阅接口的数据——稍后回放以测试您的代码,而无需连接机器人,这非常实用。
下面的插图显示了 RViz2 的输出,展示了我的机器人绘制的地图:
图 2.6 – ROS RViz 允许您看到机器人所看到的内容,在这种情况下,是一个仓库的地图
由于这是本书的第二版,我们将使用 ROS 2,这是 ROS 的新版本和改进版本。关于旧 ROS 最令人沮丧的事情之一是使用 ROSCORE,这是一个交通警察,通过网络连接机器人的所有部分。现在这已经不存在了,各种组件可以通过一种不同类型的服务找到彼此,称为 分布式数据服务(DDS)。我们还需要使用 Python 3 而不是 Python 2 来编写我们的代码,因为 Python 2 已经停止使用,不再受支持。
硬件和软件设置
为了匹配本书中的示例,并访问代码示例中使用的相同工具,您需要设置三个环境:
-
笔记本电脑或台式计算机:这将运行我们的控制面板,并用于训练神经网络。我使用了一台Windows 10计算机,它通过Oracle VirtualBox支持运行Ubuntu 20.04的虚拟机。如果您想单独运行运行Ubuntu或其他Linux操作系统的计算机(没有Windows),也可以这样做。我们将在这台计算机上加载ROS 2。我还会在这台计算机上使用PlayStation游戏控制器进行遥操作(遥控)机器人,当我们教机器人如何导航时。我还安装了ROS 2 for Windows,这可能避免了运行虚拟机。两种方法都可行,因为我们将使用的控制Python程序可以在两种模式下运行。
-
Nvidia Jetson Nano 8GB:这也运行Ubuntu Linux 20.04(您也可以运行其他Linux版本,但您将不得不自己在这之间进行调整)。Nano还运行ROS 2。我们将在接下来的小节中介绍我们需要的附加库。
-
Arduino Mega 256:我们需要能够为Arduino编写代码。我正在使用来自Arduino网站的常规Arduino IDE。它可以在Windows或Linux上运行。我们将使用Arduino来控制机器人底座的电机并使其移动。它还为我们提供了很多扩展,例如添加紧急停止按钮。
准备笔记本电脑
您需要为Windows安装ROS 2,以便机器人控制软件能够工作。为此,您可以遵循https://docs.ros.org/en/foxy/Installation/Windows-Install-Binary.html提供的说明。
我还使用了虚拟网络计算(VNC)从笔记本电脑与我的Nano进行通信,这节省了很多时间和与电缆和键盘的麻烦。否则,您需要将Nano连接到显示器、键盘和鼠标,才能在机器人上工作您的代码。我使用了RealVNC,可以在https://www.realvnc.com/en/找到。您也可以使用UltraVNC,这是一款免费软件。
安装Python
Linux Ubuntu系统将自带一个Python默认版本。我将假设您熟悉Python,因为我们将全书都会用到它。如果您需要Python的帮助,Packt有关于这个主题的几本很好的书。
一旦您登录到虚拟机,请通过打开一个终端窗口并在命令提示符中输入python
来检查您拥有的Python版本。您应该看到Python版本,如下所示:
>python
Python 3.8.16 (default, Jan 17 2023, 22:25:28) [MSC v.1916 64 bit (AMD64)]
你可以看到,在这种情况下我使用的是版本 3.8.16。
我们将需要几个附加库,这些库可以添加到Python中并扩展其功能。首先需要检查的是您是否已安装pip
。这是通过在以下命令提示符中输入以下内容来完成的:
pip
如果您得到输出未找到命令'pip'
,那么您需要安装Pip。输入以下内容:
sudo apt-get install python-pip python-dev build-essential
sudo pip install --upgrade pip
现在我们可以安装我们需要的其他包。作为开始,我们需要Python数学包numpy
、科学Python库scipy
和数学绘图库matplotlib
。让我们来安装它们:
sudo apt-get install python-numpy python-scipy python-matplotlib python-sympy
我将在适当章节中介绍我们将要使用的其他Python库(OpenCV、scikit-learn、Keras等),因为我们需要在适当章节中使用它们。
设置Nvidia Jetson Nano
对于这个设置,我们将使用一个映像在Jetson Nano上运行Ubuntu 20.04,这对于ROS 2是必需的。这个版本的来源之一是https://github.com/Qengineering/Jetson-Nano-Ubuntu-20-image。
您可以在Git仓库中遵循的基本步骤如下:
-
第一步是准备一张带有操作系统映像的SD卡。我使用了Imager,但还有其他几个程序可以完成这项工作。您需要一个至少32GB空间的SD卡 – 请记住,在这个过程中您将擦除SD卡。这意味着您需要一张大于32GB的卡来开始 – 我使用了一张64GB的SD卡,因为32GB的SD卡没有按网站上的说明工作。
-
按照SD卡的指示操作 – Jetson Nano Ubuntu网站(https://github.com/jetsonhacks/installROS2)建议我们使用容量为64GB的Class 10存储卡。将SD卡插入读卡器,并启动您的磁盘映像程序。务必(并且再三)确认您选择了正确的驱动器字母 – 您将在该驱动器中擦除磁盘。选择您下载的磁盘映像。点击写入按钮,让格式化程序在SD卡上创建磁盘映像:
图2.7 – Imager程序用于在SD卡上写入磁盘映像
-
您可以按照常规设置来设置您的语言和键盘,以及设置网络。我喜欢为机器人使用静态IP地址,因为我们将会大量使用它。
-
总是设置一个新的用户ID并更改默认密码是一个好主意。
现在,让我们看看如何安装ROS 2。
安装ROS 2
我们需要在Jetson Nano上安装ROS 2。我在我的机器上使用了Foxy版本。您可以按照此链接中的说明操作:https://github.com/Razany98/ROS-2-installation-on-Jetson-Nano。
您将需要设置源并让您的计算机指向ROS 2仓库。为此,请按照以下步骤操作:
-
使用以下代码设置
locale
:locale sudo apt update && sudo apt install locales sudo locale-gen en_US en_US.UTF-8 sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 export LANG=en_US.UTF-8 locale
-
设置要使用的源仓库:
apt-cache policy | grep universe or sudo apt install software-properties-common sudo add-apt-repository universe sudo apt update && sudo apt install curl gnupg2 lsb-release sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(source /etc/os-release && echo $UBUNTU_CODENAME) main" | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null
-
安装ROS包:
sudo apt update sudo apt upgrade sudo apt install ros-foxy-desktop sudo apt install ros-foxy-ros-base
-
设置环境:
source /opt/ros/foxy/setup.bash
-
完成后,您可以通过输入以下内容来检查您的安装是否正确完成:
ros2 topic list ros2 node list
在我们继续之前,让我们看看ROS是如何工作的。
理解ROS的工作原理
你可以将ROS视为一种连接不同程序的中间件。它提供了程序之间的进程间通信(IPC),这样我们就不必将所有函数放在一个大块代码中——我们可以将机器人的能力分散开来,独立开发和测试。
ROS机器人控制系统中的每个独立部分都称为节点。节点是一个单一用途的编程模块。我们将有收集摄像头图像、执行物体识别或控制机器人手臂的节点。使用ROS,我们可以隔离这些功能,并独立开发和测试它们。
不同的节点(程序)通过/image_raw
相互通信。这种标准消息类型包括有关图像格式的数据,以及图像本身。我们还使用sensor_msgs/CameraInfo
格式在/camera_info
主题上发布摄像头数据,该格式在以下图像中描述:
图2.8 – ROS 2节点、主题和消息类型
/camera_info
主题包含有关图像或帧的大量有价值信息,包括收集数据的时间戳和帧号。它还提供了校准信息,帮助我们理解捕获图像的几何形状,我们可以使用这些信息将像素映射到机器人周围的3D空间。
通常,对于组件之间需要传达的内容,都存在一个现有的或ROS标准消息格式。我喜欢使用通用的std_msgs/String
消息格式,在名为RobotCmd
的主题上发送一般命令,例如模式更改,从控制应用程序发送到机器人。
ROS 2允许我们设置arm_base_lock
,将其定义为布尔值,并使用以下命令:
ros2 param set /robot_arm arm_base_lock true
这将开启旋转锁定。然后我们可以使用以下方式来检查这个设置:
ros2 param get /robot_arm arm_base_lock
我们得到以下回复:
Boolean value is true
由于我们的机器人将由多个节点(程序)组成,这些节点都需要一起启动,ROS 2提供了启动文件的概念,使我们能够通过一个命令启动所有程序。在ROS 1中,启动文件是内置的YAML格式。YAML代表另一种标记语言。在ROS 2中,我们可以使用YAML、Python或可扩展标记语言(XML)来定义启动文件。我习惯于创建YAML格式的文件,所以我们将继续使用它。在我们的启动文件中,我们可以启动节点、更改参数,如果需要启动多个节点的副本(例如,如果我们有三个摄像头),我们还可以创建命名空间。
虚拟网络计算
我在我的Jetson Nano上添加了一个工具,那就是虚拟网络计算(VNC)。如果你不熟悉它,这个实用程序允许你像使用键盘、鼠标和显示器连接到它一样查看并使用Nano桌面。由于Nano物理安装在自行移动的机器人内部,因此连接键盘、鼠标和显示器通常不方便(或不可能)。VNC有许多不同的版本,这是一个在许多Unix和非Unix操作系统之间使用的标准协议。我使用的是名为Vino的版本。你需要两个部分:服务器和客户端。服务器在Nano上运行,基本上会复制屏幕上出现的所有像素并将它们发送到以太网端口。客户端捕获所有这些数据,并在另一台计算机上显示给你。让我们按照这个网页上的步骤安装VNC服务器:https://developer.nvidia.com/embedded/learn/tutorials/vnc-setup。
在你的Windows PC或Linux虚拟机上加载查看器,或者像我一样,在你的Apple iPad上加载VNC。你会发现能够直接登录到机器人并使用桌面工具非常有帮助。
重要提示
为了在没有连接显示器的情况下在Nano上运行VNC,你必须将Nano设置为自动登录。你可以编辑/etc/gdm3/custom.conf
文件来启用自动登录:
# 启用
自动登录
AutomaticLoginEnable=true
AutomaticLogin=[你的用户名]
设置colcon工作空间
我们需要在你的开发机器——笔记本电脑或台式机上,以及Jetson Nano上设置一个colcon
工作空间。遵循https://docs.ros.org/en/foxy/Tutorials/Beginner-Client-Libraries/Colcon-Tutorial.html上的说明。
如果你已经是ROS的用户,那么你就知道什么是工作空间,以及它是如何用来创建可以作为一个单元使用和部署的包的。我们将把所有的程序都放在一个我们将称之为albert
的包中。
摘要
本章涵盖了几个重要主题。它从一些机器人学的基础知识开始,为需要更多背景知识的读者提供介绍。我们讨论了常见的机器人部件,例如传感器、计算机和电机/执行器。我们更深入地讨论了子吸收架构,并展示了它是如何帮助机器人在响应不同事件和命令之间进行仲裁的。下一节涵盖了运行机器人的软件设置,包括离线开发环境和Jetson Nano计算机环境。我们设置了ROS并安装了Python工具。
最后的部分涵盖了ROS 2,并解释了它是什么以及它为我们做了什么。ROS 2是一个中间件层,它允许我们构建模块化组件和多个单次使用的程序,而不是将所有内容都打包到一个可执行文件中。ROS还提供了日志记录、可视化和调试工具,这些工具有助于我们设计复杂机器人的任务。ROS 2也是一个非常好的额外功能库,我们可以添加包括传感器驱动程序、导航功能和控制功能。
在下一章中,我们将讨论如何从概念到实际的工作计划,使用系统工程实践,如用例和故事板,来开发基于复杂机器人AI的软件。
问题
-
列出三种类型的机器人传感器。
-
PWM这个缩写代表什么?
-
模拟到数字转换是什么?输入和输出是什么?
-
谁发明了子吸收架构?
-
将我的三层子吸收架构图与艾萨克·阿西莫夫提出的机器人三大定律进行比较。是否存在相关性?为什么有,或者为什么没有?
提示:思考这些法律如何改变机器人的行为。从子吸收的角度来看,哪一条是最底层的法律?哪一条是最顶层的?
-
你认为我应该给我的机器人项目——阿尔伯特——起一个名字吗?你给你的机器人起名字吗?你的洗衣机呢?为什么不给它起名字?
-
环境变量
ROS_ROOT
的重要性是什么?
进一步阅读
-
在Jetson Nano上安装ROS 2的脚本:https://github.com/jetsonhacks/installROS2
-
如果你在ROS 2安装过程中遇到问题,可以找到有用的故障排除信息:https://docs.ros.org/en/rolling/How-To-Guides/Installation-Troubleshooting.html
-
霍华德·罗德尼·布鲁克斯博士关于子吸收架构的论文:https://people.csail.mit.edu/brooks/papers/AIM-864.pdf
第三章:构想实用机器人设计过程
本章代表了前几章关于一般理论、介绍和设置的桥梁,以及接下来的章节,我们将应用使用人工智能技术(AI)的解决问题的方法。第一步是清楚地表述我们的问题,从机器人的使用角度出发,这与作为机器人设计师/建造者的我们的观点不同。然后,我们需要决定如何应对我们和机器人将尝试的每个基于硬件和软件的挑战。到本章结束时,你将能够理解如何系统地设计机器人的过程。
本章将涵盖以下主题:
-
基于系统工程的机器人方法
-
理解我们的用例范围
-
如何借助用例来表述问题
-
如何通过故事板来解决问题
-
理解我们的用例范围
-
确定我们的硬件需求
-
软件需求分解
-
编写规范
基于系统工程的机器人方法
当你着手创建一个基于人工智能软件的复杂机器人时,你不能没有某种关于机器人如何组装以及所有部件如何相互通信的游戏计划就盲目地开始编写代码和拼凑东西。我们将讨论基于系统工程原则的机器人设计系统方法。我们将学习用例,并使用故事板作为理解我们正在构建的内容以及需要哪些部分(硬件和软件)的技术。
理解我们的任务 – 清理游戏室
我们已经就本书的示例机器人 Albert 的主要任务谈了一些内容,这个机器人是用来在我孙子辈来访后清理我家的游戏室的。我们需要为我们的问题提供一个更正式的定义,然后将其转化为机器人要执行的列表任务,以及我们可能如何完成这些任务的行动计划。
我们为什么要这样做呢?让我们考虑一下史蒂夫·马拉博利的这句话:
“如果你不知道你要去哪里,你怎么知道你到了那里?”
图 3.1 – 了解你的机器人做什么很重要
互联网和各种机器人网站上充斥着成百上千的机器人,它们有一个共同的致命缺陷:机器人和它的软件是先设计出来的,然后才去寻找适合它的工作。在机器人行业中,这被称为有备无患,瞄准再射击的问题。机器人的任务、客户、目的、用途和工作是首要的。另一种说法是:要创造一个有效的工具,第一步是决定你用它做什么。
我本可以将这本书写成一套理论和练习,这些在课堂环境中会非常有效,这将让你接触到许多你不知道如何应用的新工具。然而,这一章的目的在于为你提供工具和方法,以尽可能少的误导、痛苦、苦难、眼泪和拔掉的头发,从有一个好想法到拥有一个优秀的机器人提供一个路径。
重要提示
在烧伤方面,你需要自己小心处理;请在使用烙铁时格外小心。
我们将使用的流程是直接的:
-
第一步是从用户的角度审视机器人,然后描述它的功能。我们将把这些描述称为用例——机器人将被如何使用的例子。
-
接下来,我们将每个用例分解成故事板(逐步插图),这些可以是文字图片或实际图片。从故事板中,我们可以提取任务——我们机器人要完成的待办事项清单。
-
这个流程部分的最后一步是将待办事项清单分为我们可以用软件完成的事情和我们需要硬件来完成的事情。这将为我们设计机器人和其基于AI的软件提供详细的信息。记住,机器人的一个用途是作为这本书的好例子。
让我们从查看用例开始。
用例
让我们从陈述问题开始我们的任务。
我们机器人的任务 – 第一部分
大约每个月一两次,我那五个可爱、聪明且好动的孙子孙女会来拜访我和我的妻子。像大多数祖父母一样,我们在楼上的游戏室里放了一个装满玩具的盒子,让他们在来访时玩耍。他们一到——至少是年长的孙子孙女们——就会把玩具盒里的每一个玩具都拿出来开始玩。这导致了以下照片中所示的场景——玩具在游戏室里随机且均匀地分布:
图3.2 – 孙辈来访后的游戏室
实际上,你找不到比这更好的随机分布了。他们在这方面真的很擅长。由于作为祖父母,我们希望最大化孙子孙女在我们家玩耍的时间,并希望他们把祖父和祖母的房子与玩耍联系起来,所以我们不让他们回家时收拾玩具。你可以看到这会走向何方。
顺便说一句,如果你是父母,让我提前向你道歉;这确实是我们祖父母这边的一个邪恶计划,当你有了自己的孙子孙女时,你就会理解——你也会这样做。
我们在哪里……?是的,一个满是随机且均匀分布的外来物品——玩具——散落在原本可用的游戏室里,需要被清理。通常,我只需要重重地叹口气,自己把这些东西都收拾起来,但我是机器人设计师,所以我想要做的是制造一个能够完成以下任务的机器人:
-
拾起玩具——而不是房间的家具、灯光、书籍、扬声器或其他非玩具物品。
-
将它们放入玩具箱中。
-
继续这样做,直到找不到更多的玩具,然后停止。
这是这个过程的视觉表示:
图 3.3 – 用例:捡起玩具
现在我们可以提出一些相关的问题。我在学校上过新闻学课程,我被教导了“5W1H”的有用性——谁、什么、何时、何地、为什么和如何。这些对于检查用例同样有用。在这个部分,我有一个坚定的规则:不要涉及实现细节。不要担心你将如何做到这一点。只需关注定义结果。所以,我们现在暂时不考虑“如何”(H),而专注于“W”。让我们试一试:
-
谁:机器人。这很简单。我们希望机器人做某事,就像机器人做这件事而不是我做一样。我们希望机器人做什么?
-
做什么:这个问题可以用两种方式回答:
-
捡起玩具并将它们放入玩具箱中:这个答案告诉我们什么?它说我们将要抓住并抬起一些东西——玩具。什么是玩具?我们也可以将其重新表述为否定,这引出了第二个答案。
-
将不在房间中的物品捡起并放入玩具箱中:玩具在孙子辈把它们全部拿出来之前不在房间里。所以,我们要么将物品分类为玩具,要么分类为之前不在房间中的物品。“不在房间中”意味着机器人以某种方式知道房间中应该有什么,可能是在孩子们到来之前进行一次调查。然而,“玩具”意味着机器人至少可以将物体分类为玩具和非玩具。让我们先坚持这一点。我们可能会有一些不在玩具箱中的物品,它们不是玩具但放错了地方,因此不属于玩具箱。您已经可以看到这些问题正在塑造这个过程中后续的内容。
-
-
何时:在孙子辈来访并离开后,继续捡起玩具,直到没有剩余。
这为我们提供了两个“何时”的条件:开始和结束。在这种情况下,开始定义为孙子辈来访并离开。现在,我在用例中声明我将告诉机器人何时满足这些条件是完全合理的,因为这不会给我带来不便。我会在这里,我知道房间需要打扫。此外,我需要将机器人取出并放入房间。当机器人不工作时,它放在书架上。所以,让我们将我们的“何时”语句改为以下内容:
当我(用户)告诉你时,直到找不到更多的玩具为止。
现在,我们本可以决定机器人需要自己解决这个问题,在孙子辈离开后自动开启,但这样做的投资回报率是多少?那将是一大堆工作,但收益却不多。对我来说,作为用户,痛点在于捡玩具,而不是决定何时去做。这要简单得多。
注意,我的when语句有一个开始和一个结束。任何看过《幻想曲》中米老鼠的《魔法师的学徒》片段的人都会明白,当你有一个机器人时,告诉它何时停止是很重要的。另一个重要的概念是定义结束条件。我没有说当所有玩具都被捡起时停止,因为这会意味着机器人需要知道所有的玩具,无论是通过视觉还是数量。作为任务定义来说,说当你看不到更多玩具时停止更容易,这样就能达到我们的目的,而不需要给我们的机器人增加额外的要求。
当机器人设计师对问题了解得更多时,重新审视用例是很正常的——有时你可能正在努力解决一个与解决用户任务无关的问题。你可以想象一些团队中的机器人工程师被分配了一个任务,即捡起所有玩具,这意味着所有文化、所有地区发明的所有玩具!然后,你得到了一个需要500,000美元数据库软件许可和服务器农场来存放它的请求。我们只想捡起游戏室里找到的玩具。
-
位置:楼上的游戏室。现在有一些棘手的部分。要清洁的区域是房子的一个特定区域,但它并不是真正由墙壁所界定。而且它在上楼的地方——游戏室里有一个通往楼下的楼梯,我们不希望我们的机器人滚下去。你怎么会知道这些?除非你问这类问题!机器人运作的环境和它所做的事情一样重要。在这种情况下,让我们回头去询问用户。我会在这里插入一个楼层平面图来定义我所说的游戏室。从积极的一面来看,我们在这个任务中不需要爬楼梯或下楼。但我们确实需要留意楼梯作为一个潜在的危险:
图3.4 - 我家的楼层平面图,楼上
- 原因:那么,为什么机器人要捡起玩具呢?我差点就写下“因为有人得做这件事。”然而,答案是我不想让孙子辈的孩子们捡玩具,这样他们就有更多的时间玩耍,我也不想这么做。所以,我们为这个任务制作了一个机器人。机器人世界中的一个格言是,适合机器人的任务通常是脏乱、枯燥或危险的。这个任务无疑属于枯燥类别。
我们的这个机器人有多种用途——它有多种功能要执行。
我们机器人的任务——第二部分
机器人需要与我孙子孙女互动。为什么这很重要?正如我在第 1 章中告诉你的,孙子孙女们被介绍了一些我的其他机器人,最大的孙子威廉总是试图与机器人交谈。我有三个在自闭症谱系上的孙子孙女,所以这不是一个无足轻重的愿望——我阅读了相关的研究,例如 Robots for Autism (https://www.robokind.com/),该研究指出,在这种情况下机器人可能会有所帮助。虽然我不是在尝试进行治疗,但我希望我的机器人能够以口头方式与我的孙子孙女互动。我还有一个具体的要求——机器人必须能够讲敲门笑话并回应它们,因为这是威廉的最爱。我希望这个机器人能够进行口头互动。
因此,这里是这个用例的图示:
图 3.5 – 用例:与人互动
让我们用这个用例进行同样的练习。我们提出相关的问题:谁,什么,何时,何地,和为什么?让我们来分解这些:
-
人物:机器人、用户(爷爷)和孙子孙女们。
在这种情况下,用户交互是任务的一部分。我们与谁互动?我需要能够命令机器人开始互动。然后,我们希望机器人既能与孩子交谈也能听孩子说话。
-
功能:接收命令并以口头方式与孩子互动(进行对话),这必须包括敲门笑话。我们保留两种功能:接收来自——让我们称我为机器人控制器的命令,使这个更通用。另一种功能是与孩子进行对话,包括讲敲门笑话。我们将在我们的分解中进一步定义对话。你可以参考第 6 章关于使用机器人作为数字助理的内容。我们将使用一个名为Mycroft的开源数字助理作为机器人的语音界面。我们将在Mycroft的基本功能上添加我们自己的技能,这实际上非常灵活。机器人可以获取天气信息,设置定时器,播放音乐,在谷歌上查找信息(例如,四分之一杯有多少汤匙),甚至告诉你国际空间站现在在哪里。但它不能讲敲门笑话——直到现在,因为我们正在为机器人添加这个功能。幸运的是,敲门笑话有一个非常结构化的形式,基于双关语和这样的问答格式:
机器人:敲门。
孩子:谁在那里?
机器人:让我们进来。
孩子:让我们进来谁?
机器人:让我们进来,这里太冷了!
我会把相反形式的绘图——回应敲门笑话——留给你。
-
时间:根据机器人控制器的请求,然后当孩子对机器人说话时。
我认为这相当直观:当机器人收到执行命令时,它会进行交互。然后它等待有人与之交谈。我们可以从这个信息中推断出,当我们正在捡玩具时,我们并不期望机器人说话——这两个活动是互斥的。我们只在孩子们离开后捡玩具,因此有一个可以与之交谈的对象。
-
位置:在游戏室里,距离机器人大约六英尺。
我们必须设定一些关于我们能听到多远的限制——我们的麦克风的灵敏度是有极限的。我建议六英尺作为最大距离。我们可能稍后会重新考虑这个距离。当你遇到这样的需求时,你可以问客户“为什么是六英尺?”他们可能会说,“嗯,这听起来像是一个合理的距离。”然后你可以问,“如果它是五英尺,这会是这个功能的失败吗?”用户可能会回答,“不,但会不那么舒适。”你可以继续询问距离,直到你对所需的距离(多远才算不失败)有一个感觉,在这个例子中可能是三英尺(这样孩子就不需要弯腰到机器人那里才能被听到),以及期望的距离,即用户希望功能能工作的距离。当我们开始测试时,这些是重要的区分点。这个需求的通过-失败界限在哪里?
-
原因:因为我的孙子辈想要和机器人交谈,并希望它做出回应(即,用户明确要求了这个功能)。
现在,让我们更深入地探讨我们的机器人任务。
我们的机器人要做什么?
现在我们将使用故事板过程对机器人需要执行的操作进行详细分析。这个过程是这样的:我们根据所有关于“W”问题的答案,尽可能详细地将我们的两个任务分解。然后我们描绘出每个步骤。这些图片可以是绘画或文字描述(一段话),描述该步骤中发生的事情。我喜欢通过描述机器人作为一个状态机来开始分解过程,对于我们问题的前一部分,这可能是一个理解机器人每一步内部发生情况的好方法。
你可能熟悉状态机图,但以防万一,状态机图描述了机器人的行为作为一系列离散的状态或条件集,这些状态或条件定义了机器人可以执行哪些操作:
图3.6 – 机器人状态机图
我们的第一个状态仅仅是关闭——机器人没有开启电源。
每个状态都是一个事件(或事件),它会导致状态发生变化。这些被称为转换。要从“关闭”状态转换到下一个状态,必须发生某些事件——例如,人类操作员打开电源。我们将称这个转换事件为“施加电源”。现在我们处于什么状态?需要一些时间来启动计算机并加载程序(“初始化”)。一旦一切启动并初始化,机器人将准备好接受命令。让我们称这个状态为“待机”。机器人只是坐着等待指令。现在我们想要开始清洁房间。我向机器人发送一个“开始清洁”命令,这将状态转换为——什么?接下来需要发生什么?我们可以定义一个名为“清洁”的状态,但那将包括很多复杂的功能,而且我们不会从中学到很多东西。我们需要机器人使用其摄像头寻找玩具。如果它找不到玩具,它需要向前移动一小段距离——避开障碍物——然后再次寻找。在实践中,我们应该能够在驾驶的同时寻找玩具而无需不断停车。我们需要使“寻找玩具”功能在看到玩具时中断驾驶。
如果机器人找到了玩具,那么它需要调整自己的位置,使得玩具在机器人手臂的触及范围内。在状态机图中,我们已经添加了名为“开始清洁”的转换,它将状态从“待机”转换为“寻找玩具”。现在我们可以添加两个额外的转换:一个叫做“玩具=无”,另一个叫做“玩具=有”。“玩具=无”分支会进入一个名为“前进”的状态,在那里机器人向前移动——同时避开障碍物——然后返回到“寻找玩具”状态并再次尝试找到玩具。我们需要某种方式来告诉软件多久查找一次玩具。我们可以使用一个简单的计时器——经过多少秒。或者我们可以使用基于轮子运动的某种距离函数。
那么,现在我们已经找到了玩具,我们该怎么办?我们需要开车到玩具那里,使其进入我们机器人手臂的范围内。我们尝试用机器人的手臂和手握住玩具。我们可能不会在第一次尝试就成功,在这种情况下,我们想要再次尝试。标记为“抓握失败”的循环转换表示如果你第一次尝试不成功,就回去再试一次。我以前在哪里听过这样的话?你可以看到同样的情况在“拿起玩具”中。为什么有两个部分?在我们能够举起玩具之前,我们需要首先抓住它。所以我认为需要两个状态,因为我们可能无法成功抓住——玩具从手中掉落,这独立于拿起玩具,玩具太重或太笨重而无法举起。
好的,我们找到了一个玩具并把它拿了起来。接下来是什么?我们需要把它放进玩具箱。下一个状态是驶向玩具箱。在这个阶段不用担心如何做;我们只需要做这件事。稍后,我们可以进一步将这个状态分解成更详细版本。我们驾驶直到到达事件找到玩具箱。这意味着我们看到了玩具箱。然后我们进入放置放下位置状态,这个状态将机器人移动到可以放下玩具的位置。最终状态放下玩具是显而易见的。我们放下了玩具,机器人的爪子中没有东西了,而且你知道吗?我们通过回到寻找玩具状态重新开始。如果机器人决定放下不成功(玩具仍然在爪子中),那么我们将它再次尝试那个步骤,通过重新定位手在玩具箱上方并尝试通过打开手放下玩具。我们如何知道爪子是否为空?我们尝试关闭握持并观察手伺服机构的位置。如果爪子可以关闭(达到最小状态),那么它是空的。如果玩具掉出玩具箱(机器人完全错过了箱子),那么它又变成了地板上的玩具,并且会以正常方式处理——机器人会找到它,拿起它,并再次尝试。
这一切都很不错,我们的这个小机器人会永远四处寻找玩具,对吧?我们忽略了两个重要的转换。我们需要一个没有更多玩具的事件,并且我们需要一种方法回到关闭状态。回到关闭状态很简单——用户关闭电源。我使用了一个标记为任何状态的简写方法,因为我们可以在机器人做任何事情的时候随时按下关闭按钮,而且机器人对此无能为力。也许从每个状态画一条线回到关闭状态会更合适,但这会使图表变得杂乱,而这种记法仍然能够传达意思。新的状态机图看起来是这样的:
图3.7 – 新的状态机图
让我们花一分钟时间来谈谈没有更多玩具的概念。我们如何定义这个?这可能需要一些实验,但到目前为止,我们会说如果我们尝试了10分钟还没有找到玩具,那么我们就满意了,没有更多玩具可以找到。稍后,我们可以根据需要调整这个时间。可能5分钟对我们这个大小的房间来说已经足够了。请注意,没有更多玩具事件只能从寻找玩具状态产生,这应该是合理的。
我们提到机器人需要避开障碍物。但我们没有“避开障碍物”的状态。为什么是这样?那是因为几个状态包括驾驶,每个状态都包括避开障碍物。为避开障碍物设立一个状态是不合适的,因为它不是特定于一个状态的。我们需要的是一个描述机器人驾驶的独立状态机。正如我在上一章的“介绍子吸收架构”部分提到的,我们可以同时有多个目标处于操作状态。
拾取玩具的任务是任务,这是机器人的总体目标。“避开障碍物”是驱动引擎的目标,是我们机器人的中级管理者。
我们已经讨论了我们的用例并绘制了状态机图,现在让我们继续下一步,即创建我们的故事板。
使用故事板
在本节中,我们将进一步分解我们的用例,以便了解我们的机器人在执行两次任务过程中必须代表我们完成的各项任务。我创建了一些故事板——简短的草图——来阐述每个要点。
故事板的概念借鉴自电影行业,在那里使用类似漫画的叙述方式,将剧本中的文字转换成一系列图片或卡通,以传达剧本中未包含的额外信息,例如构图、背景、动作、道具、场景和摄像机移动。故事板的实践可以追溯到无声电影时代,至今仍在使用。
我们可以在机器人设计中使用故事板的原因相同:为了传达用例文字中未包含的额外信息。故事板应该是简单、快捷的,并且只需传达足够的信息,帮助你理解正在发生的事情。
让我们开始吧。我们不会为“电源应用”、“初始化”或“待机”创建故事板,因为对于这些简单概念来说,故事板实际上并不需要。我们将跳到状态图中的“开始清洁”事件。
故事板 – 收起玩具
当我们的故事开始时,机器人正在做什么?它已经被打开,处于待机状态,等待被告知要做什么。它是如何接收命令的?一种很好的、免提的方式是接收一个语音命令来“开始清洁”,或者一些意思相同的话。
在“开始清洁”之后的下一步是“寻找玩具”。这个故事板帧是当机器人被命令开始清洁时“机器人所看到的”。它看到了房间,其中可见三种类型的物体——也就是说,玩具、不是玩具的东西(扶手椅和壁炉),以及房间本身,包括墙壁和地板:
图3.8 – 等待语音命令开始清洁
我们可以选择任何类型的传感器来检测我们的玩具并指导我们的机器人。我们可以有一个激光雷达、热传感器或声纳扫描仪。让我们假设这个任务的最佳传感器工具是一个普通的USB摄像头。我们可以控制照明,玩具并不比周围环境更暖或更冷,我们需要足够的信息来按类型识别对象。所以,视频就是了。我们将在稍后确定我们确切需要什么类型的摄像头,所以把它加到我们的待办事项列表中。
图3.9 – 寻找玩具
我们下一个故事板是寻找玩具。我们需要运行某种算法或技术来按类型分类对象。该算法的结果是找到对象——将它们从地板的背景中分离出来——然后对每个对象进行分类,判断它是玩具还是非玩具。我们并不关心有更多的细分——我们忽略所有非玩具对象,并捡起所有玩具对象。注意,我们在玩具对象周围画圆圈,这也是另一种说法,即我们必须在相机帧中定位它们。
那么,这张简单的图片告诉我们我们之前不知道什么?它告诉我们以下内容:
-
我们需要通过对象来分割相机图像。
-
我们需要在相机帧中定位对象。
-
我们需要将对象分类为玩具或非玩具。
-
我们需要能够存储和记住这些信息。
我们一次只能捡起和移动一个玩具——我们只有一只手,而且没有人说在用例中我们需要一次捡起多个。所以,我们只关心一个玩具——让我们随意说我们捡起离机器人最近的那个:
图3.10 – 选择最近的玩具
我们也可以说,这是最容易拿到手的玩具,这可能是一个与选择最近的玩具略有不同的过程。我们将这个玩具设定为下一次行动的目标,那么这个目标是什么呢?如果你说是开车去玩具,你就说对了。然而,我们不仅要开车去玩具,还要将机器人的身体放置到一个位置,以便使用机器人手臂抓取玩具。顺便说一句,这意味着机器人手臂必须能够触及地面或非常接近地面,因为我们有一些小玩具。
我们的机器人必须规划一条从当前位置到可以尝试捡起玩具的位置的路线。我们在玩具中心手臂长度处设定一个目标目标:
图3.11 – 规划到目标的路
机器人需要确保路上没有障碍物。有两种方法可以做到这一点。如图所示,我们可以通过增加机器人的宽度(再加上一点)来清除机器人正在行驶的道路,看看是否有障碍物在那个区域,或者我们可以在障碍物周围添加一个边界,看看我们的路径是否进入那些边界。无论如何,我们需要一个没有障碍物的路径:
图 3.12 – 在路线上寻找障碍物
机器人自己决定合适的对准方式,为拾取玩具做准备:
图 3.13 – 定位机器人手
现在机器人已经完成了它的行驶,机器人可以将机器人手移动到拾取玩具的位置。我们需要将机器人手放在玩具的重心上方,然后旋转手以匹配玩具的狭窄部分,这样我们就可以拾取它。我们这个项目的目标之一不是规定机器人如何做,而是让它自己学习。因此,我们可以说对于这个故事板面板,机器人使用其培训和机器学习来使用适当的手势来准备抓取物体。我们可以推断这包括将手对齐:
图 3.14 – 拾取玩具
很可能 故事板 6 是难点 (图 3**.13),而在 故事板 7 中,机器人完成了对物体的抓取并拾起了它 (图 3**.14)。机器人必须能够确定拾取是否成功,如果不成功,则再次尝试。那是在我们之前做的状态机图中。我们现在已经拾起了玩具。接下来是什么?找到玩具箱!
图 3.15 – 找到玩具箱
现在我们需要机器人找到玩具箱。同样,目前我们并不关心它是如何做到的。我们仍然担心的是 什么 而不是 如何。不知何故,机器人四处张望并找到了玩具箱,在这个例子中,它很大,靠墙,并且有独特的颜色。无论如何,机器人必须自己找到玩具箱。图片中的标签表明机器人可以区分玩具箱,并且它将感知到的所有其他物体视为障碍物。我们可以看到我们不需要同时激活 玩具/非玩具 功能,只需要 玩具箱/非玩具箱 决策过程。这确实减少了一些所需的处理,并将使机器学习更容易。
现在我们已经找到了玩具箱,我们展示了稍微复杂一些的任务,即绕过障碍物到达那里。在这个例子中,我们展示了机器人底座的紫色轮廓,与障碍物周围的红色轮廓相比,我将其标记为 禁止进入区域。这为我们提供了更多关于如何避开障碍物的指导:
图 3.16 – 计划到达玩具箱的路径
我们希望将机器人的中心保持在 禁止进入区域 之外。我们需要足够接近玩具箱,以便将玩具放入其中:
图 3.17 – 将玩具与箱子对齐
在故事板10中,我们将玩具高高举起,超过玩具盒顶部,并在我们放手时将玩具定位在玩具盒内。请注意,我们必须在距离玩具盒最后几英寸之前将玩具举起。我们将机器人手放在玩具盒开口的顶部,尽可能向前,并在玩具盒的中间位置。
图3.18 – 将玩具放入盒子
在玩具传奇的最终步骤中,我们要打开机器人手,让玩具有可能落入玩具盒中。我预测我们可能需要花费一些试错时间来做到这一点。我们可能需要将张开的手左右倾斜,以使玩具掉落。如果玩具掉出盒子外,那么它就没有被收好,我们必须从头开始,再次尝试收好它。我们不需要为这个状态创建新的状态,因为它会回到地板上的玩具状态,而我们已经有了一个对应的状态。
我希望你在故事板过程中已经看到了这如何为可视化机器人的任务提供洞察力。我会说最重要的好处是它迫使你思考机器人正在做什么,并将每个步骤分解成越来越小的部分。如果你觉得需要这样做,不要犹豫,将单个画板分解成自己的故事板。
项目目标
由于这是一个AI/机器学习项目,我们必须在项目目标中增加不仅仅是收好玩具,还要使用机器学习、自适应系统、神经网络和其他工具来提供一种解决这类问题的新方法。你可能想,“为什么要费这个劲?你用标准的编程方法做得更好。” 我会从经验告诉你,这种方法很难解决问题,你可以自己进行研究,看看大小公司都尝试过解决这类问题但失败了——或者至少没有成功。这个问题用任何方法都很难解决,而使用基于AI的方法成功的可能性比标准编程技术要大得多。现在,我并不是说我们在这本书的任务中会取得超乎想象的巨大成功,但我们的目标是在这个过程中学到很多东西!
因此,我们在定义项目的过程中暂停一下,明确表示我们故意选择使用人工智能和机器学习作为解决其他方法难以证明的问题的方法。
由于我们将教机器人各种任务,如果我们能够远程操作机器人,像遥控车一样驾驶它,以便收集数据并拍照,这些数据我们将用于后续的对象识别,这将更有效。我们不需要这个用于操作,我们需要这个用于训练。我们将把这个必需的操作添加到我们的待办事项列表中。
在我们的下一步中,我们将从所有努力中提取出我们的机器人必须完成的硬件和软件任务。但在我们这样做之前,让我们暂停一下,讨论在定义用例范围时常见的错误。
理解我们用例的范围
愿望(由desire和requirements组合而成的词)是那些“想要有”但不是严格必要的功能。例如,如果我们决定添加闪烁的灯光到机器人上因为它看起来很酷,那将是一个愿望。你可能想要它,但这并不有助于机器人的任务或它需要执行的任务。
另一个例子是,如果我们添加了机器人必须在黑暗中运行的条件。在当前情况下,这没有理由,我们也没有在用例中提到机器人将在黑暗中运行——只是在室内房间中。这将是范围蔓延的一个例子,即在没有充分理由的情况下扩展操作条件。重要的是要非常努力地将需求和用例保持在最低限度,甚至要丢弃不必要或冗余的用例。我可能添加了一个按颜色排序玩具的要求,但排序并不能帮助捡起玩具,而且,我只有一个玩具箱。我可能添加这个任务是为了教育我的读者,但这也不助于那个目标,所以颜色排序不包括在内。
现在,让我们继续确定我们的硬件需求。
确定我们的硬件需求
根据我们的故事板,我提取或推导出以下硬件任务:
-
驱动机器人底座
-
承载机器人手臂
-
拉起玩具
-
将玩具放入玩具箱(手臂长度)
-
传感器:
-
机械臂位置
-
手臂状态(开/关)
-
机器人视觉(摄像头)用于避障
-
-
为所有系统提供电源:
-
为Nvidia Nano提供5V电源
-
为Arduino提供5V电源
-
机械臂电源 – 7.2V
-
电机电源 – 7.2V
-
-
板载计算机:
-
一台可以接收远程命令的计算机(Wi-Fi Nano):
-
运行ROS 2
-
运行Python 3
-
-
一台可以与相机接口的计算机
-
一台可以控制电机的计算机(Arduino)
-
一个可以驱动机器人手臂伺服电机的接口(伺服控制器)
-
现在,让我们来看看软件需求。
拆解我们的软件需求
这份软件任务列表是通过审查状态机图、用例和故事板编制的。我已经突出显示了需要AI并将在后续章节中详细介绍的步骤:
-
开机自检(Power on self-test (POST)):
-
启动机器人程序。
-
检查Nano能否与Arduino通信并返回。
-
尝试与控制站建立通信。
-
根据适当的情况报告POST成功或失败并记录在日志中。
-
-
通过Wi-Fi接收遥控命令:
-
驱动电机底座(右/左/前进/后退)
-
手臂上下/左右/旋转移动
-
录制视频或记录图片作为图像文件
-
-
通过Wi-Fi发送遥测数据。
-
监控进度。
-
发送视频。
-
安全导航:
-
学习避免障碍
-
学习不摔倒楼梯
-
-
找到玩具:
-
检测物体
-
学习分类物体(玩具/非玩具)
-
确定哪个玩具最近
-
-
拿起玩具:
-
移动到手臂可以触及玩具的位置
-
制定掌握策略
-
尝试抓取
-
确定抓取是否成功
-
如果没有,尝试用不同的策略再次尝试
-
根据成功率重新加权抓取技术得分
-
-
把玩具放进玩具箱:
-
学习识别玩具箱
-
找到玩具箱
-
使用导航驾驶到已知的玩具箱位置
-
移动到垃圾投放点:
-
避免障碍
-
将玩具举过玩具箱盖
-
-
放下玩具
-
检查玩具是否成功掉入玩具箱
-
如果不行,重新定位再试一次
-
如果玩具没有掉进玩具箱,我们将其视为地板上的玩具
-
-
确定没有更多的玩具。
-
等待指令。
-
远程操作:
-
移动底盘前后左右
-
移动手臂上下左右
-
手臂进出/扭转/张开/合上
-
录制视频/拍照
-
-
模拟个性:
-
说话
-
听/识别单词
-
理解一些命令
-
讲讲敲门笑话
-
理解敲门笑话
-
-
语音命令:
-
清理房间
-
把这个收起来
-
过来
-
停止
-
等待
-
简历
-
回家
-
向左/向右转
-
前进/后退
-
手举/手放
-
左手/右手
-
张开手/合上手
-
在这个列表中,我在哪里看到了远程操作?我们不记得在用例和故事板中讨论过这一点。我们需要教会机器人导航和找到玩具,为此,我们需要移动机器人并拍照。一个简单的方法是通过远程操作(遥控)来驾驶机器人。
编写规格说明
我们接下来的任务是编写我们各种组件的规格说明。这里我将通过一个例子来讲解,这是我们玩具抓取机器人项目必须完成的部分:我们需要选择一个相机。任何旧的相机都不适用——我们需要一个满足我们需求的相机。但那些需求是什么?我们需要编写一个相机规格说明,这样当我们查看要购买的相机时,我们可以判断哪一台能够胜任这项工作。
我们已经创建了我们的故事板和用例,因此我们有了确定我们的相机需要做什么所需的信息。我们可以某种程度上逆向工程这个过程:让我们讨论一下是什么让一个相机与另一个相机不同。首先当然是接口:这个相机安装在机器人上,因此它必须与机器人的计算机接口,该计算机有USB、以太网和专门的相机总线。我们还需要关注相机的哪些其他方面?我们当然关心成本。我们不希望(或需要)为我们的低成本机器人使用价值1000美元的相机。相机有分辨率:每张图片中的像素数。这可以从320 x 240变化到4,000 x 2,000(4K)。相机还有视野,这是相机可以看到的角度数。这可以从2.5度(非常窄)变化到180度(非常宽)。还有一些相机可以在黑暗中看到或者有各种类型的光学红外灵敏度。最后,还有尺寸和重量;我们需要一个适合我们机器人的小型相机。
这决定了我们需要决定的以下参数:
-
视野:[180 - > 2.5]
-
分辨率:[320 x 280 -> 4,000 x 2,000]
-
成本:(从低到高)——越便宜越好
-
在黑暗中看:是/否
-
尺寸和重量:越小越轻越好;必须适合安装在机器人上
-
接口:USB、以太网或相机总线;电源 >11V
列出这些参数的原因是,我们现在可以集中精力选择那些我们可以选择的功能,这样我们就不会浪费时间查看我们不关心的其他参数。让我们看看我们是否可以取消一些参数:
-
如果我们使用USB作为接口,电源由连接器提供,我们不需要额外的电缆或路由器。这也是成本最低的方法,因此我们选择USB作为接口。
-
在我们的用例中,我们没有对在黑暗中看的要求,因此我们不需要特殊的红外相机。
-
下一个问题是要确定视野。我们需要看到机器人手臂在拿起玩具时可以移动的整个区域。我们还需要足够的视野来避免障碍物时驾驶。我们可以从机器人上获取一些测量数据,但我们可以很快地看到我们主要需要看到靠近机器人的地方,而且我们看不到两侧轨道之外的地方。这确定了所需的视野接近90度。比这更大的视野是可以接受的,比这更小的则不行。
-
我们最终的问题是确定进行物体识别所需的分辨率。为此,我们需要一个额外的数据点——我们需要多少像素才能将一个物体识别为玩具?这就是我们将用这个摄像头做的——识别玩具和不是玩具的东西。我们还必须选择一个可以识别玩具的距离。我们没有从用例中得出一个明确的要求,所以我们必须做出一个有根据的猜测。我们知道我们的房间长17英尺,里面有家具。让我们猜测我们需要8英尺的距离。我们怎么知道这是正确的呢?我们做一个思想实验。如果我们能在8英尺远的地方识别一个玩具,我们能完成我们的任务吗?我们可以看到房间一半远处的玩具。这给机器人提供了足够的空间去驾驶到玩具那里,而且它不会花太多时间寻找玩具。作为一个检查,如果机器人必须距离4英尺才能识别一个玩具,那会不可用吗?答案可能是不会——机器人会正常工作。3英尺呢?现在我们到了机器人必须直接开到玩具那里才能确定它是什么的程度,这可能会导致检查玩具的逻辑更加复杂。所以,我们说3英尺不够,4英尺可以接受,而8英尺会很好。
摄像头需要多少分辨率才能在8英尺远的地方用90度镜头识别一个玩具?我可以告诉你,ImageNet数据库需要宽度至少为35像素的样本才能识别一个物体,所以我们可以将其作为一个基准。我们假设在这个阶段,我们需要至少35像素宽的图像。让我们从一个拥有1,024 x 768像素的摄像头开始,它的宽度是1,024像素。我们将它除以90度,得到每个度有11.3像素(1,024/90)。我们的最小玩具在8英尺处有多大?我们的最小玩具是一个Hot Wheels玩具,大约3英寸长。在8英尺处,这是1.79度或20.23像素(1.79度 x 11.3像素/度)。这还不够。解出3英寸的距离方程,我们得到一个具有1,024 x 768像素的摄像头的最大距离为4.77英尺。这勉强可以接受。如果我们有一个具有1,900 x 1200像素的HD传感器呢?那么,在8英尺处,我得到75像素——足够给我们提供最佳距离。如果我们使用宽度为1,200像素的传感器,我们的识别距离为5.46英尺,这足够但不是很好。
我带你走过了这个过程,以展示如何编写规范,以及在你决定为你的项目获取哪些传感器时,你应该问自己哪些类型的问题。
摘要
本章概述了在开发机器人项目时,如何开发待办事项列表的建议流程。这个过程被称为系统工程。我们的第一步是创建用例或描述机器人从用户角度应该如何表现。然后,我们通过创建分镜脚本,逐步通过用例来创建更多细节。我们的例子是机器人找到并识别玩具,然后拿起它们并将它们放入玩具箱。我们提取了我们的硬件和软件需求,创建了一个待办事项列表,列出了机器人将能够做什么。最后,我们为我们的关键传感器之一——摄像头编写了规范。
在下一章中,我们将深入探讨我们的第一个机器人任务——使用计算机视觉和神经网络教机器人识别玩具。
问题
-
描述一下电影或卡通的分镜脚本与软件程序的分镜脚本之间的区别。
-
什么是五个“W”问题?你能想到任何其他与检查用例相关的问题吗?
-
完成这个句子:用例显示了机器人做什么,但没有显示______。
-
以图3.16中的分镜9为例,其中机器人正在驶向玩具箱,并在你自己的分镜脚本中将它分解成更多有序的步骤。考虑在第9帧和第10帧之间必须发生的一切。
-
完成敲敲门笑话的回复表格,其中机器人回答用户讲述的笑话。你认为最后一步是什么?
-
查看远程操作操作。你会添加更多吗,或者这个列表看起来已经很好了?
-
为一个使用距离测量来防止机器人驶下楼梯的传感器编写规范。
-
一个摄像头在320 x 200像素和30度视野下,能看到多远处的6英寸宽的填充动物,仍然假设我们需要35像素来进行识别?
进一步阅读
关于本章主题的更多信息,您可以参考以下资源:
-
《SysML实用指南:系统建模语言》,由Sanford Friedenthal、Alan Moore和Rick Steiner著,由Morgan Kaufman出版;这是基于模型的系统工程(MBSE)的标准入门书籍。
-
《敏捷开发者手册》,由Paul Flewelling著,由Packt出版。
第二部分:将感知、学习和交互添加到机器人技术
为了观察、理解和与环境互动,机器人需要具备感知能力。人工智能是识别物体和导航的一种方法。本部分将赋予你使用人工智能技术高效操作机器人的基本技能。本书的例子是创建一个能够拾取玩具的机器人,因此我们首先从使用神经网络识别玩具开始。然后,我们与机器人手臂合作,使用强化学习和遗传算法等工具拾取玩具。下一章将介绍创建一个能够倾听和理解你的命令,甚至讲笑话的机器人数字助手。
本部分包含以下章节:
第四章:使用神经网络和监督学习识别对象
这是我们将开始将机器人技术和人工智能(AI)结合起来以完成我们在前几章中仔细规划的一些任务的章节。本章的主题是对象识别 – 我们将教会机器人识别玩具,以便它可以决定要捡起什么,留下什么。我们将使用卷积神经网络(CNNs)作为机器学习工具,在图像中分离对象、识别它们并在相机帧中定位它们,以便机器人可以找到它们。更具体地说,我们将使用图像来识别对象。我们将拍照,然后查看计算机是否在那些照片中识别特定类型的对象。我们不会识别对象本身,而是识别对象的图像或图片。我们还将围绕对象放置边界框,将它们与其他对象和背景像素分开。
在本章中,我们将涵盖以下主题:
-
图像处理简要概述
-
理解我们的对象识别任务
-
图像处理
-
使用YOLOv8 – 一个对象识别模型
技术要求
如果您的机器人还不能行走,您将能够完成本章的所有任务而无需机器人。然而,如果摄像头在机器人上的位置正确,我们将获得更好的结果。如果您没有机器人,您仍然可以使用笔记本电脑和USB摄像头完成所有这些任务。
总体来说,以下是您完成本章任务所需的硬件和软件:
-
硬件:
-
笔记本电脑
-
Nvidia Jetson Nano
-
USB摄像头
-
-
软件:
-
Python 3
-
OpenCV2
-
TensorFlow
-
可在https://github.com/ultralytics/ultralytics找到的YOLOv8
-
本章的源代码可在https://github.com/PacktPublishing/Artificial-Intelligence-for-Robotics-2e找到。
在下一节中,我们将讨论什么是图像处理。
图像处理简要概述
大多数人对计算机图像、格式、像素深度甚至卷积都非常熟悉。我们将在以下章节中讨论这些概念;如果您已经了解这些,可以跳过。如果这是新领域,请仔细阅读,因为我们将要做的一切都基于这些信息。
图像在计算机中以像素或图像元素组成的二维数组形式存储。每个像素是一个小点。数千或数百万个小点构成了每一幅图像。每个像素是一个或一系列数字,描述了其颜色。如果图像仅是灰度或黑白图像,那么每个像素由一个数字表示,该数字对应于小点的明暗程度。到目前为止,这很简单。
如果图像是彩色图片,那么每个点有三个数字组合起来形成其颜色。通常,这些数字是红、绿、蓝(RGB)颜色的强度。组合(0,0,0)代表黑色(或所有颜色的缺失),而(255,255,255)是白色(所有颜色的总和)。这个过程称为加色模型。如果你用水彩而不是计算机像素工作,你会知道将你水彩盒中的所有颜色混合在一起会得到黑色——这是一个减色模型。红、绿、蓝是原色,可以用来制作所有其他颜色。由于RGB像素由三种颜色表示,所以实际图像是一个三维数组,而不是二维数组,因为每个像素有三个数字,形成一个(高度,宽度,3)的数组。因此,800 x 600像素的图片将表示为一个(800,600,3)维度的数组,或1,440,000个数字。这有很多数字。我们将非常努力地减少在任何给定时间处理的像素数量。
虽然RGB是一组可以描述像素的三个数字,但还有其他描述颜色公式的方法,这些方法有各种用途。我们不必使用RGB——例如,我们还可以使用青色、黄色和品红色(CMY),它们是RGB的互补色,如图4**.2所示。我们还可以使用色调、饱和度和值(HSV)模型来分解颜色,该模型通过色调(颜色的阴影)、饱和度(颜色的强度)和值(颜色的亮度)来分类颜色。HSV是某些计算非常有用的颜色空间,例如将彩色图像转换为灰度(黑白)。要将RGB转换为灰度像素,你必须做一些数学运算——你不能只是拉出一个通道并保留它。RGB到灰度的公式,如国家电视系统委员会(NTSC)定义的,如下所示:
0.299红 + 0.587*绿 + 0.114蓝
这是因为不同波长的光在我们眼睛中的表现不同,我们眼睛对绿色更为敏感。如果你在HSV颜色模型中有颜色,那么创建灰度图像需要考虑V(值)并丢弃H和S值。正如你可以想象的那样,这要简单得多。这一点很重要,因为在本章中我们将进行大量的图像处理。但在开始之前,在接下来的部分,我们将讨论本章将要执行图像识别任务。
理解我们的目标识别任务
让计算机或机器人识别玩具的图像并不像拍两张照片然后说“如果图片 A 等于图片 B,那么是玩具”那么简单。我们需要做大量的工作才能识别出各种随机旋转、散布在不同位置且距离不同的物体。我们可以编写一个程序来识别简单的形状——例如六边形或简单的颜色块——但无法像填充狗玩具那样复杂。编写一个对图像进行某种分析并计算每个可能排列的像素、颜色、分布和范围的程序将极其困难,而且结果将非常脆弱——它会在光线或颜色发生最轻微的变化时失败。
从经验出发,我最近遇到了一个大型机器人,它使用传统的计算机视觉系统来寻找其电池充电站。那个机器人将一个旧、褪色的饮料机误认为是其充电器——让我们说,我不得不去买更多的保险丝。
我们将采取的做法是教机器人识别一组与我们从不同角度拍摄的玩具对应的图像。我们将通过使用一种特殊的人工神经网络(ANN)来实现这一点,该网络对图像执行卷积操作。它被归类为人工智能技术,因为我们不是通过编写代码来编程软件识别物体,而是训练一个神经网络,使其能够通过如何接近网络训练时学习到的标记像素组来正确地分割(从图像的其余部分分离)和标记(分类)图像中的像素组。而不是代码决定机器人的行为,而是我们训练网络时所使用的数据来完成这项工作。由于我们将通过提供分割和标记的图像来训练神经网络,因此这被称为监督学习。这涉及到告诉网络我们希望它学习的内容,并根据其表现的好坏来强化(奖励)网络。我们将在第 8 章中讨论无监督学习。这个过程涉及到我们不对软件确切地说明要学习的内容,这意味着它必须自己确定这一点。
为了澄清,在本节中,我们将告诉人工神经网络(ANN)我们希望它学习的内容,在本例中是识别我们称之为“玩具”的一类物体,并在这些物体周围绘制边界框。这个边界框将告诉机器人的其他部分玩具是可见的,以及它在图像中的位置。
我将强调我们将使用的独特组件来完成识别图像中玩具的任务。你还记得第 3 章中的故事板告诉我们做什么吗?
图 4.1 – 识别玩具用例
我们的图像识别器必须确定哪些是玩具,然后在图像中定位它们。这在前面的草图中有说明;标记为玩具的物体周围有圆圈。图像识别器不仅要识别它们是什么,还要识别它们在哪里。
图像处理
那么,现在我们有了图像,我们能用它做什么呢?你可能玩过Adobe Photoshop或其他图像处理程序,如GIMP,你知道可以在图像上执行数百种操作、过滤器、更改和技巧。例如,可以通过调整亮度使图像变得更亮或更暗。我们可以增加图像白色部分和黑色部分之间的对比度。我们可以通过应用高斯模糊过滤器使图像变得模糊。我们还可以通过使用如非锐化掩模之类的过滤器使图像(在一定程度上)变得更清晰。你还可以使用边缘检测过滤器,如Canny过滤器,来隔离图像的边缘,其中颜色或值发生变化。我们将使用所有这些技术来帮助计算机识别图像:
图4.2 – 应用到图像上的各种卷积
通过执行这些操作,我们希望计算机软件对图像的大小、拍摄照片的角度或角度不变性以及可用的照明,即照明不变性不敏感。在计算机视觉系统中,这些都是非常理想的——我们不希望一个AI系统只能从与原始图像相同的角度和距离识别我们的玩具。记住,我们将训练我们的视觉系统根据我们事先拍摄的标记训练图像来识别玩具,机器人将必须根据从训练集中学习到的内容来识别物体。在这里,我们将使用那些主要不基于大小、角度、距离或照明的图像特征。这些特征可能是什么?
如果我们从一个常见的家庭用品,比如一把椅子,从几个角度检查它,那么椅子的哪些部分不会改变?简单的答案是边缘和角落。椅子始终有相同数量的角落,并且我们可以从大多数角度看到一致数量的它们。它也有一致的边缘数量。
承认,这确实是对方法的一种简化的描述。我们将训练我们的神经网络(ANN)在一系列可能或可能不独特于该对象的所有图像特征上,并让它决定哪些有用,哪些无用。我们将通过使用一种通用的图像处理方法,称为卷积来实现这一点。
卷积
有时,你会遇到一些数学构造,将复杂任务转化为只是一些加法、减法、乘法和除法。几何中的向量就是这样工作的,在图像处理中,我们有卷积核。结果是,大多数常见的图像处理技术——边缘检测、角点检测、模糊、锐化、增强等等——都可以通过一个简单的数组结构实现。
很容易理解,在图像中,像素的邻居对像素本身的重要性与像素本身一样重要。如果你要去尝试找到盒子的所有边缘像素,你会寻找一种颜色在一侧,另一种颜色在另一侧的像素。我们需要一个函数,通过比较像素的一侧与另一侧的像素来找到边缘。
卷积核是一个矩阵函数,它将权重应用于像素邻居——或者我们正在分析的像素周围的像素。该函数通常写成这样,作为一个3x3的矩阵:
-1 | 0 | 1 |
---|---|---|
-2 | 0 | 2 |
-1 | 0 | 1 |
表4.1 – 一个示例卷积核
Sobel边缘检测在Y方向上表示。这检测上下方向的边缘。每个块代表一个像素。正在处理的像素位于中心。像素两侧的邻居是其他块——顶部、底部、左侧和右侧。为了计算卷积,将相应的权重应用于每个像素的值,通过乘以该像素的值(强度),然后将所有结果相加。如果这幅图像是彩色的——RGB——那么我们将分别对每种颜色进行卷积计算,然后将结果合并。以下是将卷积应用于图像的示例:
图4.3 – Sobel边缘检测卷积的结果
结果图像与原始图像大小相同。请注意,我们只得到边缘作为结果——如果中心像素两侧的颜色相同,它们会相互抵消,我们得到零,或黑色。如果它们不同,我们得到255,或白色,作为答案。如果我们需要一个更复杂的结果,我们也可以使用5x5卷积,它考虑了每侧的两个最近像素,而不仅仅是其中一个。
好消息是,你不必选择要应用于输入图像的卷积操作——我们将构建一个软件前端,它会设置所有的卷积。这个前端只是程序的一部分,在开始训练之前设置网络。我们将使用的神经网络包将确定哪些卷积提供了最多的数据并支持我们想要的训练输出。
“但是等等,”我听到你说,“如果像素位于图像的边缘,而我们没有一边的相邻像素怎么办?”在这种情况下,我们必须向图像添加填充——这是一个额外的像素边界,允许我们考虑边缘像素。
在下一节中,我们将深入了解神经网络的内部结构。
人工神经元
什么是神经元?我们如何将它们组合成网络?如果你能记住你在生物学中学到的知识,一个生物或自然神经元有输入,或树突,将它们连接到其他神经元或传感器输入。所有输入都汇聚到一个中央体,然后通过轴突,或连接,通过其他树突离开,到达其他神经元。神经元之间的连接称为突触,这是一个信号必须跳过的微小间隙。神经元接收输入,处理它们,并在达到某个阈值水平后激活或发送输出。人工神经元是一种软件构造,它近似于你大脑中神经元的运作方式,是自然神经元的非常简化的版本。它有几个输入,一组权重,一个偏差,一个激活函数,然后作为网络的结果输出到其他神经元,如图所示:
图4.4 – 人工神经元的示意图
让我们详细描述每个组件:
-
输入:这是一个从其他神经元或作为网络输入接收的数字或值。在我们的图像处理示例中,这些是像素。这个数字可以是浮点数或整数——但它必须只是一个数字。
-
权重:这是我们为了训练神经元而改变的可调整值。增加权重意味着输入对我们的答案更重要,同样地,减少权重意味着输入的使用较少。为了确定神经元的值,我们必须组合所有输入的值。随着神经网络的训练,每个输入的权重都会进行调整,这有利于某些输入而牺牲其他输入。我们将输入乘以权重,然后将所有结果相加。
-
偏差:这是一个加到权重总和上的数字。偏差防止神经元陷入零,并改善训练。这通常是一个很小的数字。想象一下这样一个场景,一个神经元的所有输入都是零;在这种情况下,权重将没有任何效果。添加一个小偏差允许神经元仍然有输出,网络可以使用这一点来影响学习。没有偏差,输入为零的神经元无法进行训练(改变权重没有效果)并且是卡住的。
-
激活函数:这决定了神经元输出的值,基于其输入的加权和。最常见类型的是ReLU(修正线性单元) – 如果神经元的值小于零,输出为零;否则,输出是输入值 – 以及S型函数,这是一个对数函数。激活函数在网络中传播信息,并为神经元的输出引入非线性,这使得神经网络能够逼近非线性函数:
图4.5 – 常见激活函数
-
输出:序列神经网络中的每一层都连接到下一层。有些层是完全连接的 – 第一层的每个神经元都与第二层的每个神经元连接。其他层是稀疏连接的。在神经网络训练中有一个常见的流程称为dropout,其中我们随机移除连接。这迫使网络为它学习的每一点信息有多条路径,这加强了网络,并使其能够处理更多样化的输入。
-
输出最大池化:我们使用一种特殊的网络层(与全连接或稀疏层相比),称为最大池化,其中对应于图像中区域的神经元组 – 比如一个2x2像素块 – 被映射到下一层的单个神经元。最大池化神经元只从四个输入神经元中取最大的值。这具有下采样图像(使其变小)的效果。这允许网络将小特征(如Hot Wheels汽车的轮子)与较大特征(如引擎盖或挡风玻璃)关联起来,以识别玩具车:
图4.6 – 最大池化操作
现在您已经了解了神经网络由什么组成,让我们来探讨如何训练和测试一个神经网络。
训练CNN
我想要向您展示本章剩余部分代码中我们将要执行的操作的全过程。请记住,我们正在构建一个卷积神经网络(CNN),它检查视频帧中的像素,并输出图像中是否有一个或多个类似玩具的像素区域,以及它们的位置。以下图表显示了我们将逐步进行神经网络训练的过程:
图4.7 – CNN过程
对于这个任务,我决定使用一个现成的神经网络而不是从头开始构建。有很多好的CNN目标检测器可用,而且说实话,很难改进现有的模型结构。我为这本书选择的是称为YOLOv8的模型,其中YOLO代表You Only Look Once。让我们了解我们如何使用这个模型来完成我们的任务。
使用YOLOv8 – 一个目标识别模型
在我们深入YOLOv8模型细节之前,让我们谈谈为什么我选择了它。首先,对于任何我们可能使用的CNN,学习过程基本上是相同的。YOLO是一个强大的开源物体检测模型,背后有许多开发。它被认为是行业最佳,它已经能够完成我们所需要的任务——通过在图像周围绘制边界框来检测物体并显示它们的位置。因此,它告诉我们物体是什么,以及它们在哪里。正如您将看到的,它非常容易使用,并且可以扩展以检测除了它最初训练的类别之外的其他类别的物体。有许多YOLO用户可以提供大量支持,并为我们学习机器人AI物体识别提供了一个很好的基础。
如我在本章开头提到的,我们需要完成两个任务才能达到用机器人捡起玩具的目标。首先,我们必须确定机器人是否可以用它的摄像头检测到玩具(确定摄像头图像中是否有玩具),然后确定它在图像中的位置,这样我们就可以开过去并捡起它。在本章中,我们将学习如何检测玩具,而在第7章中,我们将讨论我们如何确定距离并导航到玩具。
YOLOv8一次完成两项任务,因此得名。其他类型的物体识别模型,例如我在本书第一版中创建的模型,在图像中识别和定位物体需要两个步骤。首先,它发现图像中存在物体,然后在单独的步骤中确定物体在图像中的位置。这个单独的步骤会使用滑动窗口方法,取图像的一部分,并使用神经网络中的检测部分来表示“是”或“否”,如果该部分包含它所识别的物体。然后,它会将考虑的窗口在图像上滑动并再次测试。这个过程会重复,直到我们有一系列包含检测到的物体的图像部分。然后,一个称为minmax的过程会选择包含物体所有可见部分的最小框(min)。
YOLOv8通过结合两个神经网络采取不同的方法——一个检测它被训练来识别的物体,另一个训练根据物体的中心绘制边界框。YOLOv8的直接输出包括物体的检测和边界框。YOLOv8还可以通过像素分割图像,不仅识别包含物体的框,还包括属于该物体的所有像素。我们将使用边界框来帮助我们驾驶机器人到达玩具。
YOLOv8在一系列物体类别(大约80个)上进行了预训练,但我们仍然可以检查它是否已经能够检测我们想要检测的玩具。让我们测试YOLOv8检测我们玩具的能力。我们可以在PC上使用以下简单命令安装YOLOv8:
pip install ultralytics
现在,为了测试我们在游戏室玩具照片上的检测,我们将使用YOLOv8检测模型中最小(就模型大小而言)的模型——yolov8n.pt
)。这是Ultralytics与YOLOv8一起提供的预训练神经网络:
yolo task=detect mode=predict model=yolov8n.pt source="test.png"
如以下图所示,现成的YOLOv8目标模型仅检测到一个颠倒的火柴盒车,并将其错误地标记为滑板:
图4.8 – YOLOv8输出,未针对我们的玩具进行特定训练
您必须承认,从这个角度看,这个小玩具车确实有点像滑板,但这不是我们想要的结果。我们需要检测图像中的所有玩具,而不仅仅是其中一个。我们该怎么办?
答案是我们可以向网络添加新的训练,获得YOLOv8的所有优势,并且我们的自定义对象也能被检测到。为此,我们可以使用一个称为迁移学习的过程。
下面是我们将如何训练我们的玩具检测器的概述,之后我们将更详细地讨论这些步骤:
-
首先,我们将准备一个包含玩具房间图像的训练集。这意味着我们必须从机器人视角拍摄大量玩具的照片,使用机器人将使用的相同相机。我们希望从玩具的所有不同角度和侧面拍照。我按顺时针方向绕着房间走,然后逆时针走,每隔几英寸拍一张照片。在这个步骤中,我拍了48张照片。
-
接下来,我们必须使用像RoboFlow(https://roboflow.com)这样的数据标注程序来标注图像(您可以参考相关文档以获取详细说明)。该程序允许我们在想要识别的对象(玩具)周围绘制方框,并用标签进行标记——我们将使用名称
toy
。我们正在将包含玩具的图像部分分离出来,并告诉神经网络这种类型对象的名称:
图4.9 – 使用RoboFlow进行标注,一个免费的数据标注工具
-
然后,我们必须将训练集分成三部分:一部分用于训练网络,一部分用于验证训练,另一部分用于测试网络。我们将创建包含87%图像的训练集,8%的验证集和5%的测试集。我们将训练数据和测试数据放在不同的文件夹中。RoboFlow在生成标签页下有相应的流程,其中有一个标记为训练/测试分割的部分。
-
现在,我们必须将每张图像的训练值通过将不同图像的部分组合成马赛克来乘以。我们将从四张随机不同的图片中取部分并组合它们。这将使我们的训练集增加三倍,这个过程称为数据增强。这是RoboFlow内置的功能。我开始时有36张图片;经过增强后,我有99张:
图 4.10 – 瓦片数据增强从我们有限的图片数量中创建更多训练数据
为什么我们使用这种瓦片方法?我们仍然希望有有效的边界框。瓦片过程会调整任何与边缘相交的局部边界框的大小。
-
接下来,我们将构建两个程序:训练程序,它在我们的台式计算机上运行并训练网络;以及工作程序,它使用训练好的网络来寻找玩具。训练过程可能不会在我们的机器人机载小计算机上运行,或者可能需要很长时间才能运行,所以我们将使用台式计算机来完成这项工作。
-
现在,我们需要训练网络。为了实现这一点,我们必须做以下事情:
-
首先,我们必须将所有图像缩小以减少处理时间到合理的水平。
-
然后,我们必须使用均匀随机权重初始化网络。
-
接下来,我们必须对标记的图像进行编码,并将其输入到网络中。神经网络只使用图像数据来预测图片中包含的对象类别及其边界框。由于我们预先用正确的答案标记了图像并使用了正确的边界框,我们可以判断答案是否正确。如果答案是正确的,我们可以通过增加(训练值)来加强导致这个答案的输入权重。如果答案是错误的,我们可以减少权重。在神经网络中,期望结果和实际结果之间的误差称为损失。对每张图像重复这个过程。
-
现在,我们必须通过运行测试图像集来测试网络——这些图像是训练集中没有的相同玩具的图片。我们必须分析在这个集合上得到的输出类型(有多少是错误的,有多少是正确的)。如果这个答案超过90%,我们就停止。否则,我们返回并再次运行所有训练图像。
-
一旦我们对结果满意——我们通常需要50到100次迭代才能达到这个目标——我们就必须停止并存储训练网络中最终得到的权重。这是我们训练好的CNN。
-
-
我们接下来的任务是找到玩具。为此,我们必须通过加载它并使用从实时机器人视频图像中获取的图像来部署训练好的网络。我们将从0%到100%得到包含玩具的图像的概率。我们将以部分扫描输入视频图像,并找出哪些部分包含玩具。如果我们对这个网络不满意,我们可以将其重新加载到训练程序中,并对其进行更多训练。
现在,让我们一步一步详细地介绍这个过程。在我们开始编写代码之前,我们还有一些理论需要讲解。
理解如何训练我们的玩具检测器
我们的首要任务是准备一个训练集。我们将把相机放在机器人上,使用遥操作界面(或者只是用手推动它)来驾驶机器人,每隔大约一英尺就拍一张静态照片。我们只需要包含玩具的图片,因为我们将会标注玩具。我们需要大约200张图片——越多越好。我们还需要一套白天有自然光和夜晚(如果你的房间在白天和夜晚之间改变照明)的图片。这给我们带来了几个优势:我们使用相同的房间和相同的相机在相同的照明条件下寻找玩具。
现在,我们需要对图像进行标注。我们将图像加载到RoboFlow中,创建一个名为toydetector
的数据集。使用上传标签,拖放图像或选择包含图像的文件夹。
对于我们来说,这个过程相当直接。我们依次查看每张图片,并在任何玩具对象周围画一个框。我们按Enter键或输入toy
。这需要一些时间。
当我们在图像中标注了大约160个玩具后,我们可以使用RoboFlow中的生成按钮来创建我们的数据集。我们必须设置预处理任务,将我们的图像调整到640x640像素。这使我们在机器人上的有限计算机容量得到最佳利用。然后,我们必须增强数据集以创建我们有限集合的额外图像,如前所述。我们将使用马赛克方法来增强数据集,同时保留边界框。为此,我们必须使用RoboFlow中的生成标签,然后点击添加增强步骤来选择将影响我们图像的操作类型。然后,我们必须添加马赛克增强来从我们的训练集中创建更多图像。现在,我们可以点击生成按钮来创建我们的数据集。
我们从48张我拍摄的图片(在步骤1中)开始;经过增强后,我们有114张。我们将设置测试/训练分割,使其包含99张训练图像,9张验证图像和6张测试图像(87%训练,8%验证和5%测试)。这使我们在有限的数据集上得到最佳利用。
要从RoboFlow下载我们的数据集,我们必须在计算机上安装RoboFlow的界面。它是一个Python包:
pip install roboflow
然后,我们必须创建一个名为downloadDataset.py
的简短Python程序。当你构建你的数据集时,RoboFlow将提供一个唯一的api_key
值;这将是授权访问你账户的密码。它如下所示,我在这里放置了星号:
from roboflow import Roboflow
rf = Roboflow(api_key="*****************")
project = rf.workspace("toys").project("toydetector")
dataset = project.version(1).download("yolov8")
在下一节中,我们将使用以下命令重新训练网络:
yolo task=detect mode=train model=yolov8n.pt data=datasets/data.yaml epochs=100 imgsz=640
一旦我们完成这些,程序将产生大量的输出,如下所示:
(p310) E:\BOOK\YOLO>yolo task=detect mode = val model=runs\detect\train3\weights\best.pt data=ToyDetector-1\data.yaml
Ultralytics YOLOv8.0.78 Python-3.10.10 torch-2.0.0 CUDA:0 (NVIDIA GeForce RTX 2070, 8192MiB)
Model summary (fused): 168 layers, 3005843 parameters, 0 gradients, 8.1 GFLOPs
..................
AMP: checks passed
optimizer: SGD(lr=0.01) with parameter groups 57 weight(decay=0.0), 64 weight(decay=0.0005), 63 bias
train: Scanning E:\BOOK\YOLO\datasets\ToyDetector-1\train\labels.cache… 99 images, 0 backgrounds, 0 corrupt: 100%|███
val: Scanning E:\BOOK\YOLO\datasets\ToyDetector-1\valid\labels.cache… 9 images, 0 backgrounds, 0 corrupt: 100%|██████
Plotting labels to runs\detect\train5\labels.jpg…
Image sizes 640 train, 640 val
Using 8 dataloader workers
Logging results to runs\detect\train5
Starting training for 100 epochs…
训练我们模型的一个关键部分是训练优化器。我们将使用随机梯度下降(SGD)来完成这项工作。SGD是那些有华丽名字的简单概念之一。随机只是意味着随机。我们想要做的是调整神经元的权重,以给出比第一次更好的答案——这就是我们通过调整权重来训练的内容。我们想要改变权重的一小部分——但朝哪个方向?我们想要改变权重的方向,以改善答案——它使预测更接近我们想要的样子。
为了更好地理解这一点,让我们做一个简单的思想实验。我们有一个神经元,我们知道它正在产生错误的答案并且需要调整。我们将增加一点权重并看看答案如何变化。它变得稍微糟糕了——数字离正确答案更远了。所以,我们必须减去一小部分——正如你可能想到的,答案变得更好。我们稍微减少了错误量。如果我们绘制神经元产生的错误图,我们会看到我们正在朝着零误差移动,或者图正在下降到某个最小值。另一种说法是,线的斜率是负的——趋向于零。斜率的大小可以称为梯度——就像你将山丘的斜率或陡峭程度称为梯度一样。我们可以计算偏导数(换句话说,就是误差曲线在此点的斜率),这告诉我们线的斜率。
我们调整整个网络上的权重以最小化真实值和预测值之间损失的方法被称为Y1
、Y2
和Y3
。我们有三个权重——W1
、W2
和W3
。我们将有偏差B
和我们的激活函数D
,它是ReLU整流器。我们的输入值是0.2、0.7和0.02。权重是0.3、0.2和0.5。我们的偏差是0.3,期望的输出是1.0。我们计算输入和权重的总和,得到0.21的值。加上偏差后,我们得到0.51。ReLU函数通过任何大于零的值,所以这个神经元的激活输出是0.51。我们的期望值是1.0,这来自真实(标签)数据。所以,我们的错误是0.49。如果我们将训练率值加到每个权重上,会发生什么?看看下面的图:
图4.11 – 反向传播如何调整权重
输出值现在上升到 0.5192。我们的错误下降到 0.4808。我们正在正确的道路上!我们错误斜率的梯度是 (0.4808-0.49) / 1 = -0.97。这里的 1 是因为我们到目前为止只有一个训练样本。那么,随机部分从何而来?我们的识别网络可能有 5000 万个神经元。我们不可能对每个神经元都进行所有这些数学运算。因此,我们必须对输入进行随机采样,而不是全部采样,以确定我们的训练是正面的还是负面的。
用数学术语来说,方程的斜率由该方程的导数提供。因此,在实践中,反向传播计算训练周期之间错误的偏导数,以确定错误的斜率,并据此确定我们是否正确地训练了网络。随着斜率的减小,我们降低训练速率到一个更小的数字,以便越来越接近正确答案:
图 4.12 – 梯度下降过程
现在,我们来解决下一个问题:我们如何将权重调整传播到神经网络层?我们可以在输出神经元处确定错误——即标签值减去网络的输出。我们如何将这个信息应用到前一层?每个神经元对错误的贡献与其权重成正比。我们必须将错误除以每个输入的权重,这个值现在就是链中下一个神经元的应用错误。然后,我们可以重新计算它们的权重,依此类推。这就是为什么神经网络需要如此多的计算能力:
图 4.13 – 反向传播错误
我们将错误反向传播回网络,从末端开始,一直传播到开始处。然后,我们从头开始进行下一轮。
在这一点上,我们可以测试我们的玩具检测器。让我们看看我们如何做到这一点。
构建玩具检测器
我们可以使用以下命令来测试我们的结果:
yolo task=detect mode=predict model=last.pt source=toy1.jpg imgsz=640
程序产生了以下输出。我们可以在 ./runs/detect/predict
目录中找到带有标记检测的图像,目录中附加的数字取决于我们运行检测的次数:
Speed: 4.0ms preprocess, 44.7ms inference, 82.6ms postprocess per image at shape (1, 3, 640, 640)
Results saved to runs\detect\predict4
我们预测的结果显示在下面的图中:
图 4.14 – 玩具检测器在工作
通过这种方式,我们已成功使用神经网络创建了一个玩具检测器。检测器的输出,我们将在 第 5 章 中使用它来指导机器人和机械臂驶向玩具并抓取它,看起来是这样的:
"predictions": [
{
"x": 287.5,
"y": 722.5,
"width": 207,
"height": 131,
"confidence": 0.602,
"class": "toy"
},
对于每个检测,神经网络将提供一些信息。我们得到边界框中心的 x
和 y
位置,然后是那个框的高度和宽度。然后,我们得到一个置信度数字,表示网络对这个决策是检测的确定性。最后,我们得到物体的类别(是什么类型的物体),当然是一个玩具。
当我们运行神经网络的训练过程时,如果你查看 runs/detect/train
中的 training
文件夹,你会看到一系列图表。这些图表告诉我们什么?
我们首先需要查看的是 F1_curve
。这是精确度和召回率的乘积。精确度是所有正例中正确分类的对象的比例。召回率是正确识别的正检测的比例。因此,精确度定义为以下:
精确度 = TP / (TP + FP)
精确度是真实正例数除以真实正例数和假正例数(被识别为检测但实际不是的项)。
召回率的定义略有不同:
召回率 = TP / (TP + FN)
在这里,召回率是真实正例数除以真实正例数加上假负例数。一个假负例是一个漏检或实际上存在但未被检测到的物体。
要创建 F1 曲线,我们必须将精确度和召回率相乘,并将其与 置信度 对应。图表显示了产生最佳结果(在精确度和召回率之间权衡)的检测置信度水平:
图 4.15 – F1 置信曲线
在这种情况下,置信度为 0.21 时,检测率为 0.87。这意味着我们得到了最佳的真实检测与误检测的比率。然而,这个最佳比率 – 87% – 发生在 0.21 的置信度 – 这是一个相当低的数字。在这个低置信度水平上的检测很难区分,可能是由于测量中的噪声引起的。可能更希望我们的峰值出现在更高的置信度水平。我尝试了几个方法来解决这个问题。我运行了 200 个 epoch 而不是 100,并将峰值 F1 置信度水平移动到 51%,但检测水平略有下降到 85%。然后,我将梯度下降技术从 SDM 更改为 Adam,这是一种自适应梯度下降技术,当接近我们的目标时,它会降低学习率。这可以通过以下代码实现:
yolo task=detect mode=train model=yolov8n.pt data=datasets/data.yaml epochs=100 optimizer='adamW' imgsz=640
这产生了 88% 的真实检测率在 49% 置信度下的更令人满意的结果,我认为这将更好地为我们的小玩具检测器工作。在回顾我的检测时,有几个误报(家具和其他被检测为玩具的物体),所以我认为这个版本将是我们的玩具检测器神经网络。尽管我使用了一个相当小的数据集,但拥有更多不同角度的图片来工作也不会有害。在结束这一章之前,让我们简要总结一下到目前为止我们已经学到的内容。
摘要
在本章中,我们一头扎进了人工神经网络的世界。人工神经网络可以被视为一种逐步的非线性逼近函数,它逐渐调整自己以适应曲线,使所需的输入与所需的输出相匹配。学习过程包括几个步骤,包括准备数据、标记数据、创建网络、初始化权重、创建正向传递以提供输出,以及计算损失(也称为误差)。我们创建了一种特殊类型的人工神经网络,即卷积神经网络(CNN),来检查图像。网络使用带有玩具的图像进行训练,我们在图像上添加了边界框来告诉网络图像的哪一部分是玩具。我们训练网络,使其在包含玩具的图像分类中达到超过87%的准确率。最后,我们测试了网络以验证其输出,并使用Adam自适应下降算法调整我们的结果。
在下一章中,我们将从强化学习和遗传算法的角度探讨机器人臂的机器学习。
问题
-
在本章中,我们经历了很多。你可以使用提供的框架来研究神经网络的特性。尝试几种激活函数,或者不同的卷积设置,看看训练过程中有什么变化。
-
绘制一个人工神经元的图并标注各部分。查找一个自然的人类生物神经元,并将它们进行比较。
-
真实神经元和人工神经元有哪些相同的特征?有哪些不同的?
-
学习率对梯度下降有什么影响?如果学习率太大?太小?
-
神经网络的第一层与输入有什么关系?
-
神经网络的最外层与输出有什么关系?
-
查找三种损失函数并描述它们的工作原理。包括均方损失和两种交叉熵损失。
-
如果你的网络在训练后达到了40%的分类准确率并陷入停滞,或者无法进一步学习,你会做些什么改变?
进一步阅读
关于本章涵盖的主题的更多信息,请参考以下资源:
-
《Python深度学习食谱》,作者:Indra den Bakker,Packt出版社,2017年
-
《用Python实现人工智能》,作者:Prateek Joshi,Packt出版社,2017年
-
《Python深度学习》,作者:瓦伦蒂诺·佐卡,吉安马里奥·斯帕卡尼亚,丹尼尔·斯莱特,以及彼得·罗兰茨,Packt出版社,2017年
-
《PyImageSearch博客》,作者:阿德里安·罗斯布鲁克,可在pyimagesearch.com找到,2018年
第五章:使用强化学习和遗传算法捡起和放回玩具
本章是机器人变得具有挑战性和有趣的地方。我们现在想要让机器人的操作臂开始捡起物体。不仅如此,我们希望机器人能够学会如何捡起物体,以及如何移动它的手臂而不会撞到自己。
你会如何教一个孩子在他们房间里捡起玩具?你会为完成任务提供奖励,比如“如果你捡起你的玩具,你将得到一份奖励?”或者你会提供惩罚的威胁,比如“如果你不捡起你的玩具,你就不能在你的平板电脑上玩游戏.”这个概念,为良好的行为提供正面反馈,为不良行为提供负面反馈,被称为强化学习。这是我们本章将要训练机器人的方法之一。
如果你需要机器人臂来执行代码,你需要一个机器人臂。我使用的是从Amazon.com购买的LewanSoul Robot xArm。这个臂使用数字伺服电机,这使得编程变得容易得多,并为我们提供了位置反馈,这样我们就知道手臂在什么位置。我购买的臂可以在出版时在http://tinyurl.com/xarmRobotBook找到。
在本章中,我们将涵盖以下主题:
-
设计软件
-
设置解决方案
-
介绍用于抓取物体的Q学习
-
介绍用于路径规划的遗传算法(GAs)
-
替代机器人臂机器学习方法
技术要求
本章的练习不需要任何我们在前几章中没有见过的新的软件或工具。我们将首先使用Python和ROS 2。你需要一个Python的IDE(IDLE或Visual Studio Code)来编辑源代码。
如果这听起来像是一款游戏,你在达到目标时获得正分,错过目标时失去分数,那么你就对了。我们有一些想要实现的胜利概念,我们创建了一种某种类型的点系统来强化——也就是说,奖励——当机器人做我们希望它做的事情时。
注意
如果你不想购买机械臂(或者不能购买),你可以使用ROS 2和Gazebo(一个仿真引擎)的机械臂仿真来运行此代码。你可以在此处找到说明:https://community.arm.com/arm-research/b/articles/posts/do-you-want-to-build-a-robot。
你可以在本书的GitHub仓库中找到本章的代码,网址为:https://github.com/PacktPublishing/Artificial-Intelligence-for-Robotics-2e。
任务分析
本章的任务相当直接。我们将使用机器人手臂来拿起我们在上一章中确定的小玩具。这可以分为以下任务:
-
首先,我们构建了一个控制机器人手臂的界面。我们使用ROS 2将机器人的各个部分连接起来,因此这个界面是系统其余部分向手臂发送命令和接收数据的方式。然后我们开始教手臂执行其功能,即拿起玩具。能力的第一级是拿起或抓取玩具。每个玩具都略有不同,相同的策略并不总是有效。此外,玩具可能处于不同的方向,因此我们必须适应玩具呈现给机器人末端执行器(即其手的别称)的方式。因此,我们不想编写大量的可能或可能不起作用的定制代码,而是想创建一个结构,使机器人能够自我学习。
-
我们面临下一个问题是要让手臂移动。问题不仅仅是手臂有位置,它还需要从一个起点到一个终点的路径。手臂不是一个单一的部分——它由六个不同的电机组成(如图5.3所示),每个电机都执行不同的功能。其中两个电机——握持和手腕——根本不会移动手臂;它们只影响手部。因此,我们的手臂路径由四个电机控制。另一个大问题是,如果我们不小心,手臂可能会与机器人的身体碰撞,所以我们的手臂路径规划必须避免碰撞。
我们将使用一种完全不同的技术来学习手臂路径。GA是一种机器学习技术,它使用进化的模拟来从简单的动作中进化出复杂的行为。
现在让我们先谈谈我们必须要处理的事情。我们有一个600
,(在允许电机移动的短暂时间间隔后)我们看到伺服位置是421
,然后有东西阻止电机达到我们为其设定的目标。这些信息对于训练机器人手臂将非常有价值。
我们可以使用正向运动学,这意味着将手臂的所有角度和杠杆相加,以推断出手的位置(我将在本章后面提供相应的代码)。我们可以将这个手的位置作为我们的期望状态——我们的奖励标准。我们将根据手部与期望位置和方向之间的接近程度给机器人评分,或给予奖励。我们希望机器人能够找出达到该位置所需的方法。我们需要为机器人提供一种测试不同理论或行动的方法,这些行动会导致手臂移动。
我们将首先与机器人手部进行工作,或者用时髦的机器人术语,称为末端执行器。
以下图表显示了我们是如何尝试通过旋转手腕来调整我们的机器人手臂以拿起玩具的:
图5.1 – 拿起玩具的故事板
对于抓取,我们有三个动作可以操作。我们将机械臂定位以拾取玩具,通过旋转手腕伺服电机调整手的角位,并关闭手以抓取物体。如果手完全关闭,那么我们错过了玩具,手是空的。如果玩具阻止夹爪关闭,因为我们已经拾取了它,那么我们就成功了,已经抓到了玩具。我们将使用这个过程来教会机器人使用不同的手部位置根据玩具的形状来拾取玩具。
软件设计
设计机械臂控制软件的第一步是建立一个坐标系(我们如何测量运动),然后我们通过创建状态(机械臂位置)和动作(改变位置的运动)来设置我们的解决方案空间。以下图显示了机械臂的坐标系:
图5.2 – 机械臂坐标系
让我们定义机器人的坐标系——我们用来测量运动的参考——如图所示的前图。X方向朝向机器人的前方,因此前后移动沿着X轴。水平移动(左或右)沿着Y轴。垂直移动(上下)在Z方向。我们将零点——我们坐标的原点——放置在机械臂中心的下方,Z=0在地板上。因此,如果我说机器人手部在X轴上正向移动,那么它是在远离机器人的前方移动。如果手(手臂的末端)在Y轴上移动,那么它是在向左或向右移动。
现在,我们必须有一组名称,我们将用它来称呼机械臂中的伺服电机。我们将进行一些拟人化命名,并给机械臂的各个部分赋予解剖学名称。电机在控制系统中编号,我机器人臂上的伺服电机标记如下:
-
电机1 控制夹爪的开启和关闭。我们也可以将夹爪称为手。
-
电机2 是手腕旋转电机,它旋转手部。
-
电机3 是手腕俯仰(上下)方向。
-
我们将电机4称为肘部。肘部在中间弯曲手臂,正如你所期望的那样。
-
电机5 是肩部俯仰伺服电机,当机械臂指向正前方时,它使机械臂上下移动,绕Y轴旋转。
-
电机6 位于机械臂的底部,因此我们将其称为肩部偏航(右或左)伺服电机。它绕Z轴旋转整个机械臂。我决定不移动这个轴,因为由于全向轮,整个机器人底座可以旋转。我们将只移动机械臂的上下位置以简化问题。我们在第8章中开发的导航系统将使机械臂指向正确的方向。
我们将首先定义一个机器人臂的接口,其余的机器人控制系统可以使用:
图5.3 – 机械臂电机命名
在这里,俯仰指的是上下运动,而偏航指的是左右运动。
我们将使用在机器人世界中常见的两个术语来描述我们如何根据我们拥有的数据计算手臂的位置:
-
正向运动学(FK)是从机器人手臂的基座开始,逐步计算出抓取器的位置和方向,依次计算每个关节的位置和方向的过程。我们取关节的位置和角度,并加上该关节与下一个关节之间的手臂长度。这个过程通过计算产生一个X-Y-Z位置和机器人手指末端的俯仰-滚转-偏航方向,称为正向运动学,因为我们是从基座向前计算到手臂的。
-
逆向运动学(IK)采取不同的方法。我们知道手的位置和方向,或者我们希望它在哪里。然后我们沿着手臂向后计算,以确定产生该手位置的关节角度。逆向运动学有点复杂,因为可能有多个解决方案(关节位置的组合)可以产生给定的手结果。用你自己的手臂试一试。抓住门把手。现在在保持手在门把手上的同时移动你的手臂。你的关节有多种组合可以使你的手保持在相同的位置和方向。在这本书中,我们不会使用逆向运动学,但我希望你对这个术语熟悉,它经常在机器人手臂中用来驱动机器人末端执行器(夹具或手)的位置。
若想对这些概念有更深入的解释,你可以参考https://control.com/technical-articles/robot-manipulation-control-with-inverse-and-forward-kinematics/。
接下来,让我们讨论如何使手臂运动起来。
设置解决方案
我们将把将电机设置到不同位置的行为称为动作,并将机器人手臂和手的位置称为状态。对一个状态应用动作会导致手臂进入一个新的状态。
我们将让机器人将状态(手的初始位置)和动作(在该状态下使用的电机命令)与产生正或负结果的概率相关联——我们将训练机器人找出哪些动作组合可以最大化奖励。奖励是什么?它只是一个任意值,我们用它来定义机器人完成的学习是积极的——我们想要的——还是消极的——我们不想要的。如果动作导致了积极的学习,那么我们就增加奖励,如果没有,那么我们就减少奖励。机器人将使用一个算法来尝试最大化奖励,并逐步学习一个任务。
让我们通过探索机器学习所扮演的角色来更好地理解这个过程。
机器人手臂的机器学习
由于增量学习也是神经网络的一部分,我们将使用之前在神经网络中使用的一些相同工具,将奖励传播到导致手移动到某个位置的连续动作链中的每一步。在强化学习中,这被称为折现奖励——将奖励的部分分配给多步过程中的每一步。同样,状态和动作的组合称为策略——因为我们正在告诉机器人,“当你处于这个位置,并想要到达那个位置时,执行这个动作。”让我们通过更仔细地观察我们使用机器人手臂进行学习的流程来更好地理解这个概念:
-
我们设定了机器人手的最终位置,即机器人手在X和Z坐标上相对于手臂旋转中心的毫米位置。
-
机器人将尝试一系列动作,试图接近那个目标。我们不会给机器人提供到达那个目标所需的电机位置——机器人必须学习。初始动作将是完全随机生成的。我们将限制增量动作(类似于上一章中的学习率)的大小,以避免手臂剧烈挥动。
-
在每次增量动作中,我们将根据手臂是否更接近目标位置来评分该动作。
-
机器人将通过将初始状态和动作(移动)与奖励评分关联来记住这些动作。
-
之后,我们将训练一个神经网络,根据起始状态和动作输入生成积极结果的概率。这将使手臂能够学习哪些动作序列能够产生积极的结果。然后,我们将能够根据起始位置预测哪种动作会导致手臂正确移动。
-
你也可以推测,我们必须为快速完成任务添加奖励——我们希望结果高效,因此我们将为完成任务的用时最短添加奖励——或者说,我们可以为达到目标所需的每一步减去奖励,这样步骤最少的流程将获得最多的奖励。
-
我们使用Q函数来计算奖励,如下所示:
*Q = Q(s,a) + (reward(s,a) + g ** max(Q(s’,a’))
其中Q代表机器人从特定动作获得的奖励(或期望获得的奖励)。Q(s,a)是在给定起始状态下,我们期望的该动作的最终奖励。reward(s,a)是该动作的奖励(我们现在采取的小增量步骤)。g是一个折扣函数,奖励更快到达目标,即以更少的步骤(步骤越多,g折扣(移除奖励)越多),max(Q(s’,a’))选择在那种状态下从可用动作集中产生最大奖励的动作。在方程中,s和a代表当前状态和动作,而s’和a’分别代表后续状态和动作。这是我针对决策问题的Bellman方程版本,进行了一些适应。我添加了对更长解决方案(更多步骤,因此执行时间更长)的折扣,以奖励更快的手臂移动(更少的步骤),并且省略了学习率(alpha),因为我们对每个状态都采取整步(我们没有中间状态来学习)。
接下来,让我们了解如何教机器人手臂学习运动。
我们如何选择动作?
机器人手臂可以执行哪些动作?如*图5**.3所示,我们有六个电机,每个电机有三个选项:
-
我们可以什么都不做——也就是说,根本不移动
-
我们可以逆时针移动,这将使我们的电机角度变小
-
我们可以顺时针移动,这使得我们的电机角度变大
注意
大多数伺服电机将正位置变化视为顺时针旋转。因此,如果我们命令旋转从200度变为250度,电机将顺时针旋转50度。
我们对机器人手臂每个动作的动作空间是移动每个电机顺时针、逆时针或根本不移动。这给我们提供了6个电机的729种组合(3^6种可能动作)。这相当多。我们将要构建的软件界面通过数字来引用机器人手臂的电机,1代表手,6代表肩部旋转电机。
让我们减少这个数字,只考虑三个电机的运动——[-1, 0, 1]
。我们将在动作矩阵中使用仅+/-1或0的值来以小增量移动电机。手部的x-y坐标可以通过每个关节角度的总和乘以手臂长度来计算。
这里有一个Python函数,用于计算机器人手的位姿,假设每个手臂段长10厘米。你可以替换你机器人手臂段的长度。这个函数将代表手位姿的电机角度从度数转换为厘米的x-y坐标:
def forward_kinematics(theta1, theta2, theta3, segment_length):
# Convert degrees to radians
theta1_rad = math.radians(theta1)
theta2_rad = math.radians(theta2)
theta3_rad = math.radians(theta3)
# Calculate positions of each joint
x1 = segment_length * math.cos(theta1_rad)
y1 = segment_length * math.sin(theta1_rad)
x2 = x1 + segment_length * math.cos(theta1_rad + theta2_rad)
y2 = y1 + segment_length * math.sin(theta1_rad + theta2_rad)
x3 = x2 + segment_length * math.cos(theta1_rad + theta2_rad + theta3_rad)
y3 = y2 + segment_length * math.sin(theta1_rad + theta2_rad + theta3_rad)
return x3, y3
手臂的动作(可能的移动)构成了我们机器人手臂的动作空间,即所有可能动作的集合。在本章中,我们将探讨各种选择执行哪个动作以及何时执行的方法,以便完成我们的任务,并使用机器学习来实现这一点。
另一种看待这个过程的方式是我们正在生成一个决策树。你可能熟悉这个概念。当我们将这个概念应用到机器人臂时,我们有一个独特应用,因为我们的臂是一系列连接在一起的关节,移动一个关节会使其他所有关节在臂上向外移动。当我们移动电机 5 时,电机 4 和 3 在空间中的位置会发生变化,它们与地面和我们的目标的角度和距离也会改变。每个可能的电机移动都会为我们的决策树添加 27 个新分支,并可以生成 27 个新的臂位置。我们唯一要做的就是选择保留哪一个。
本章的其余部分将讨论我们如何选择动作。现在是时候开始编写一些代码了。首要任务是创建一个机器人臂的接口,以便机器人其余部分可以使用。
创建机器人臂的接口
如前所述,我们使用 ROS 2 作为我们的接口服务,它创建了一个模块化开放式系统架构(MOSA)。这使得我们的组件变成了即插即用的设备,可以添加、删除或修改,就像智能手机上的应用程序一样。实现这一点的秘诀是创建一个有用的通用接口,我们现在将这样做。
注意
我正在为这本书创建自己的 ROS 2 接口。我们不会使用任何其他 ROS 包与这个臂一起使用——只使用我们创建的,所以我希望接口尽可能简单,以便完成这项工作。
我们将使用 Python 创建此接口。请按照以下步骤操作:
-
首先,在 ROS 2 中为机器人臂创建一个包。包是 ROS 2 中功能的一个可移植组织单元。由于我们为机器人臂有多个程序和多个功能,我们可以将它们捆绑在一起:
cd ~/ros2_ws/src ros2 pkg create –build-type ament-cmake ros_xarm src directory where we will store all of the parts we need.
-
我们需要安装 xArm 的驱动程序,以便我们可以在 Python 中使用它们:
pip install xarm
-
现在我们转到我们的新源目录:
xarm_mgr.py, which is short for xarm manager.
-
打开编辑器,让我们开始编码。首先,我们需要一些导入:
import rclpy import xarm import time from rlcpy.node import Node from std_msgs.msg import String, Int32MultiArray, Int32
rclpy
是 ROS 2 的 Python 接口。xarm
是机器人臂的接口,而time
当然是一个我们将用来设置计时器的时间模块。最后,我们使用一些标准的 ROS 消息格式来进行通信。 -
接下来,我们将创建一些预定义的臂命名位置作为快捷方式。这是一种简单的方法,将臂放置在我们需要的位置。我定义了五个我们可以调用的臂预设位置:
图 5.4 – 机器人臂位置
让我们详细描述这些位置:
-
高携带是我们携带玩具等物体时希望手臂所处的位置。手臂在机器人上方,手部抬高。这有助于防止玩具从手中掉落。
-
中性携带是当机器人驾驶时,手臂不在摄像机前的标准位置。
-
拾取是抓取和抓取闭合(在图中没有单独显示)的组合。前者是手臂位置,将手放在地面上以便拾取物体。手臂尽可能向前伸展并接触地面。后者只是关闭末端执行器以抓取玩具。
-
放下是将玩具放入玩具箱(相当高)的手臂位置。
-
对齐(未显示)是一种实用模式,用于检查手臂的对齐情况。所有伺服电机都设置为中间位置,手臂应该以直线指向天花板。如果不这样做,您需要使用随附的实用程序调整手臂。
让我们看看我们如何设置ROS接口。这些数字是伺服电机的位置(角度),单位从0
(完全逆时针)到1000
(完全顺时针)。9999
代码表示在该位置不改变伺服电机,这样我们可以创建不改变手臂部分位置(如夹爪)的命令:
HighCarry=[9999,500,195,858,618,9999]
MidCarry=[9999, 500, 500, 807, 443, 9999]
Grasp = [100,500,151,553,117,9999]
GraspClose=[700,9999,9999,9999,9999,9999]
Align=[500,500,500,500,500,500]
-
现在我们可以开始定义我们的机器人手臂控制类了。我们将从类定义和初始化函数开始:
class xarmControl(Node): def __init__(self): super().__init__('xarm_manager') # node name self.publisher = self.create_publisher(Int32MultiArray, 'xarm_pos', 10) self.armAngPub = self.create_publisher(Int32MultiArray, 'xarm_angle', 10)
在这里设置我们的机器人手臂的ROS接口有很多事情要做:
-
首先,我们调用对象类结构(
super
)来使用名称xarm_manager
初始化我们的ROS 2节点。 -
然后,我们创建了一个用于手臂位置信息的发布者,方便地称为
xarm_pos
。在这里,POS代表位置。它以伺服单位发布手臂位置,这些单位从0
(完全逆时针)到1000
(完全顺时针)。我们还以xarm_angle
发布手臂角度(以度为单位),以防我们需要该信息。伺服器行程的中心是0度(伺服单位中的500
)。逆时针位置是负角度,而顺时针位置是正角度。我仅使用了整数度数(没有小数点),因为我们不需要那么高的精度来控制手臂。我们的高抬位置在伺服单位中是[666,501,195,867,617,500]
,在伺服角度中是[41,0,-76,91,29,0]
。我们发布我们的输出并订阅我们的输入。
-
-
我们的输入,或者说订阅,为手臂提供了外部接口。我思考了如何使用手臂,并提出了我想要的接口。在我们的情况下,我们有一个非常简单的手臂,只需要几个命令。首先,我们有一个名为
RobotCmd
的字符串命令,它允许我们创建控制机器人模式或状态的命令。这将用于许多机器人的命令,而不仅仅是手臂。我创建了一些手臂模式命令,我们将在接下来的几段中介绍。RobotCmd
的有用之处在于我们可以向这个输入发送任何字符串,并在接收端处理它。这是一个非常灵活且有用的接口。请注意,对于每个订阅者,我们都会创建一个函数调用到回调例程。当数据在接口上发布时,我们的程序(xarm_mgr.py
)会自动调用回调例程:self.cmdSubscribe = self.create_subscription(String, 'RobotCmd', self.cmdCallback,10)
-
接口的下一段允许我们在偏航方向上移动手臂的底部,并独立操作手和手腕。在本章中,我们开始仅训练夹爪,因此有一个独立的接口来旋转、打开和关闭夹爪是有帮助的。操作手部不会改变夹爪的坐标位置,因此这可以分开。同样,我们通过偏航方向(向右和向左)移动手部,以对齐要抓取的玩具。我们将从这个功能开始锁定,稍后添加偏航功能。这是由我们在上一章中设计的计算机视觉系统控制的,因此需要一个独立的接口。我们有
xarmWrist
命令来旋转手腕,xarmEffector
来打开和关闭夹爪手指,以及xarmBase
来将手臂的底部向右或向左移动:self.wristSubscribe = self.create_subscription(Int32, 'xarmWrist', self.wristCallback,10) self.effSubscribe = self.create_subscription(Int32, 'xarmEffector', self.effCallback,10) self.baseSubscribe = self.create_subscription(Int32, 'xarmBase', self.baseCallback,10)
-
最后的命令接口使我们能够将手臂移动到我们指定的任何位置。通常,我们使用一组数字来命令手臂移动,如下所示:
[100,500,151,553,117,500]
。我在这个命令中增加了一个秘密功能。由于我们可能希望在不需要改变偏航角度(来自视觉系统)或手部位置(可能或可能不握有玩具)的情况下移动手臂,我们可以发送移动手臂但不影响某些伺服电机的命令,例如手部。我使用了值9999
作为不移动此伺服电机的值。因此,如果手臂位置命令读取为[9999, 9999, 500, 807, 443, 9999]
,则偏航位置(电机6)和手部位置(电机0和1)不会改变:self.baseSubscribe = self.create_subscription( Int32MultiArray, 'newArmPos', self.moveArmCallback,10)
-
现在我们已经定义了所有的发布和订阅接口,我们可以打开连接到机器人手臂的USB接口,看看它是否在响应。如果没有响应,我们将抛出一个错误信息:
timer_period = 1.0 # seconds self.timer = self.create_timer(timer_period, self.timer_callback) self.i = 0 # counter try: self.arm = xarm.Controller('USB') print("ARM OPEN") except: self.get_logger().error("xarm_manager init NO ARM DETECTED") self.arm = None print("ERROR init: NO ARM DETECTED") return
注意
这里是xarmPos
命令数组中伺服电机的快速作弊指南:
[握紧/松开,手腕旋转,手腕俯仰,肘部俯仰,肩部俯仰,肩部偏航]
-
我们在源代码中的下一个函数是设置遥测定时器。我们希望定期发布机械臂的位置,以便机器人其他部分可以使用。我们将创建一个定时器回调,它以我们指定的速率定期执行。让我们从每秒一次开始。这是一个信息值,我们不会用它来控制——伺服控制器负责这一点。这是我们需要编写的代码:
timer_period = 1.0 # seconds self.timer = self.create_timer(timer_period, self.timer_callback) self.i = 0 # counter
timer_period
是中断之间的间隔。self.timer
类变量是一个指向定时器函数的函数指针,我们将它指向另一个函数,self.timer_callback
,我们将在下一个代码块中定义它。每秒钟,中断将会触发并调用timer_callback
例程。 -
我们接下来的代码是硬件接口的一部分。由于我们正在初始化机械臂控制器,我们需要打开与机械臂的硬件连接,这是一个使用人机界面设备(HID)协议的USB端口:
try: self.arm = xarm.Controller('USB') print("ARM OPEN") except: self.get_logger().error("xarm_manager init NO ARM DETECTED") self.arm = None print("ERROR init: NO ARM DETECTED") return
我们首先创建一个
try
块,以便我们可以处理任何异常。机器人臂可能未开启电源,或者可能未连接,因此我们必须准备好处理这种情况。我们创建一个臂对象(self.arm
),它将成为我们与硬件的接口。如果臂成功打开,则返回。如果不成功,我们运行except
例程:-
首先,我们在 ROS 错误日志中记录我们没有找到机械臂。ROS 日志记录函数非常灵活,提供了一个方便的地方来存储您在调试过程中需要的信息。
-
然后我们将机械臂设置为空对象(
None
),这样我们就可以在程序后续部分避免抛出不必要的错误,并且可以测试机械臂是否已连接。
-
-
下一个代码块是我们的定时器回调,它发布有关机械臂的遥测信息。记住,我们定义了两个输出消息,机械臂位置和机械臂角度。我们可以在这里服务它们:
def timer_callback(self): msg = Int32MultiArray() # call arm and get positions armPos=[] for i in range(1,7): armPos.append(self.arm.getPosition(i)) msg.data = armPos self.publisher.publish(msg) # get arm positions in degrees armPos=[] for i in range(1,7): armPos.append(int(self.arm.getPosition(i, True))) msg.data = armPos #print(armPos) self.armAngPub.publish(msg)
我们使用
Int32MultiArray
数据类型,这样我们就可以将机械臂位置数据发布为一个整数数组。我们通过调用self.arm.getPosition(servoNumber)
从机械臂收集数据。我们将输出追加到我们的数组中,完成后,调用 ROS 发布例程(self.<topic name>.publish(msg))
。对于机械臂角度,我们可以通过调用arm.getPosition(servoNumber, True)
来获取,这将返回一个角度而不是伺服单元。 -
现在我们可以处理来自其他程序的命令。接下来,我们将为机器人创建一个控制面板,可以发送命令并设置机器人的模式:
def cmdCallback(self, msg): self.get_logger().info("xarm rec cmd %s" % msg.data) robotCmd = msg.data if robotCmd=="ARM HIGH_CARRY": self.setArm(HighCarry) if robotCmd=="ARM MID_CARRY": self.setArm(MidCarry) if robotCmd=="ARM GRASP_POS": self.setArm(Grasp) if robotCmd=="ARM GRASP_CLOSE": self.setArm(GraspClose) if robotCmd=="ARM ALIGN": self.setArm(Align)
这个部分相当直接。我们接收一个包含命令的字符串消息,并解析消息以查看它是否是程序可以识别的内容。如果是,我们处理消息并执行适当的命令。如果我们收到
ARM MID_CARRY
命令,这是一个将机械臂定位到中间位置的命令,那么我们使用MidCarry
全局变量发送一个setArm
命令,该变量包含所有六个电机的伺服位置。 -
接下来,我们编写机器人接收并执行手腕伺服电机的代码,该命令旋转夹爪。这个命令发送到电机2:
def wristCallback(self, msg): try: newArmPos = int(msg.data) except ValueError: self.get_logger().info("Invalid xarm wrist cmd %s" % msg.data) print("invalid wrist cmd ", msg.data) return # set limits newArmPos = float(min(90.0,newArmPos)) newArmPos = float(max(-90.0,newArmPos)) self.arm.setPosition(2,newArmPos, True)
当
xarmWrist
主题发布时,执行这个函数调用。这个命令只是移动手腕旋转,我们会用它来调整手的手指以对准我们正在抓取的物体。我为无效值添加了一些异常处理,并在输入范围内进行了限制检查,我认为这是外部输入的标准做法。我们不希望手臂对无效输入执行奇怪的操作,例如如果有人能够在xarmWrist
主题上发送字符串而不是整数。我们还检查命令中数据的范围是否有效,在这种情况下是0
到1000
个伺服单位。如果我们得到越界错误,我们将使用min
和max
函数将命令限制在允许的范围内。 -
末端执行器命令和基础命令(控制整个手臂的左右旋转)的工作方式完全相同:
def effCallback(self, msg): # set just the end effector position try: newArmPos = int(msg.data) except ValueError: self.get_logger().info("Invalid xarm effector cmd %s" % msg.data) return # set limits newArmPos = min(1000,newArmPos) newArmPos = max(0,newArmPos) self.arm.setPosition(1,newArmPos) def baseCallback(self, msg): # set just the base azimuth position try: newArmPos = int(msg.data) except ValueError: self.get_logger().info("Invalid xarm base cmd %s" % msg.data) return # set limits newArmPos = min(1000,newArmPos) newArmPos = max(0,newArmPos) self.arm.setPosition(6,newArmPos)
setArm
命令让我们可以发送一个命令来同时设置每个伺服电机的位置。我们发送一个包含六个整数的数组,这个程序将这个数组传递给伺服电机控制器。如前所述,我设置了一个特殊值,
9999
,这个值告诉这段代码不要移动那个电机。这使得我们可以向手臂发送命令,移动一些伺服电机,或者只移动其中一个。这使得我们可以独立地移动手臂末端的上下轴和左右轴,这非常重要。另一件重要的事情是,尽管这段Python代码几乎瞬间执行,但伺服电机移动需要一定的时间。我们必须在伺服命令之间加入一些延迟,以便伺服控制器可以处理它们并将它们发送到正确的电机。我发现命令之间的
0.1
(1/10秒)延迟是有效的。如果你省略这个值,只有一个伺服电机会移动,手臂将不会处理其余的命令。伺服电机以菊花链的方式使用串行接口,这意味着它们相互传递消息。每个伺服电机都连接到另一个伺服电机,这比所有伺服电机单独连接要好得多。 -
我们可以用
MAIN
来完成我们的手臂控制代码——程序的执行部分:#######################MAIN#################################### rclpy.init() print("Arm Control Active") xarmCtr = xarmControl() # spin ROS 2 rclpy.spin(xarmCtr) # destroy node explicitly xarmCtr.destroy_node() rclpy.shutdown()
在这里,我们初始化
rclpy
(ROS 2 Python接口)以将我们的程序连接到ROS基础设施。然后我们创建我们创建的xarm
控制类的实例。我们将它称为xarmCtr
。然后我们只需告诉ROS 2执行。我们甚至不需要循环。程序将执行发布和订阅调用,我们的计时器发送遥测数据,这些都包含在我们的xarmControl
对象中。当我们退出spin
时,我们就完成了程序,所以我们将关闭ROS节点,然后程序结束。
现在我们准备开始训练我们的机器人手臂!为此,我们将使用三种不同的方法来训练我们的手臂拿起物体。在第一阶段,我们将仅训练机器人手——末端执行器——来抓取物体。我们将使用Q学习,一种强化学习类型,来完成这项任务。我们将让机器人尝试拿起物品,如果机器人成功,我们将给予奖励或得分,如果机器人失败,我们将减分。软件将尝试最大化奖励以获得最高分数,就像玩游戏一样。我们将生成不同的策略或动作计划来实现这一点。
介绍用于抓取物体的Q学习。
使用Q学习强化学习技术训练机器人手臂末端执行器拿起形状奇特的物体涉及几个步骤。以下是该过程的逐步解释:
-
定义状态空间和动作空间:
-
定义状态空间:这包括有关环境和机器人手臂的所有相关信息,例如物体的位置和方向、末端执行器的位置和方向以及任何其他相关传感器数据。
-
定义动作空间:这些是机器人手臂可以采取的可能动作,例如旋转末端执行器、在不同方向上移动它或调整其夹爪。
-
-
设置Q表:创建一个表示状态-动作对的Q表,并用随机值初始化它。Q表将包含每一状态一行,每一动作一列。当我们测试手臂移动到的每个位置时,我们将使用Q学习方程(在机器人手臂的机器学习部分介绍)计算出的奖励存储在这个表中,以便我们稍后可以参考。我们将通过状态和动作搜索Q表,以查看哪个状态-动作对会产生最大的奖励。
-
定义奖励函数:定义一个奖励函数,根据机器手臂的动作为其提供反馈。奖励函数应鼓励手臂成功拿起物体,并阻止不良行为。
-
启动训练循环:启动训练循环,它由多个剧集组成。每个剧集代表训练过程的迭代:
-
重置环境和设置初始状态。
-
根据当前状态使用探索-利用策略(如ε-贪婪)选择动作,其中以一定的概率(ε)探索随机动作或选择具有最高Q值的动作。
-
执行选定的动作,并观察新的状态和奖励。
-
使用Q学习更新方程更新Q表中的Q值,该方程结合了奖励、下一个状态的最大Q值以及学习率(alpha)和折扣因子(gamma)参数。
-
将当前状态更新为新状态。
-
重复之前的步骤,直到剧集结束,无论是成功拿起物体还是达到最大步数。
-
-
探索与利用:随着时间的推移调整探索率(用epsilon表示),逐渐减少探索并优先利用学到的知识。这允许机器人臂最初探索不同的动作,并逐渐专注于利用学到的信息以提高性能。
-
重复训练:继续多个回合的训练循环,直到Q值收敛或性能达到满意水平。
-
执行测试:在训练后,使用学到的Q值在测试环境中做出决策。将训练好的策略应用于机器人臂末端执行器,使其能够根据学到的知识捡起形状奇特的物体。
注意
实现机器人臂末端执行器的Q-learning训练需要软件和硬件组件的组合,例如仿真环境、机器人臂控制器和感官输入接口。具体实现方式可能因机器人臂平台和使用的工具和库而异。
编写代码
现在,我们将通过构建代码来实现我们刚才描述的七个步骤,该代码将使用我们在上一节中制作的机器人臂接口来训练手臂:
-
首先,我们包含所需的导入 – 我们将需要实现训练代码的功能:
import rclpy import time import random from rclpy.node import Node from std_msgs.msg import String, Int32MultiArray, Int32 from sensor_msgs.msg import Image from vision_msgs.msg import Detection2D from vision_msgs.msg import ObjectHypothesisWithPose from vision_msgs.msg import Detection2DArray import math import pickle
rclpy
是ROS 2的Python接口。我们使用Detection2D
与上一章(YOLOV8)中的视觉系统通信。当我们到达那里时,我会解释pickle
引用。 -
接下来,让我们定义一些我们稍后会使用的函数:
global learningRate = 0.1 # learning rate def round4(x): return (math.round(x*4)/4) # function to restrict a variable to a range. if x < minx, x=min x,etc. def rangeMinMax(x,minx,maxx): xx = max(minx,x) xx = min(maxx,xx) return xx def sortByQ(listByAspect): return(listByAspect[2])
学习率在强化学习中就像在其他机器学习算法中一样,用于调整系统根据输入做出改变的速度。我们将从
0.1
开始。如果这个值太大,我们的训练会有大的跳跃,这可能导致不稳定的输出。如果太小,我们可能需要进行很多次重复。actionSpace
是我们正在教授的可能的手部动作列表。这些值是手腕的角度(以度为单位)。请注意,就抓取而言,-90
和+90
是相同的。round4
函数用于将边界框的宽高比四舍五入。如您所记得,当我们检测到玩具时,对象识别系统会在其周围画一个框。我们使用这个边界框作为线索,了解玩具相对于机器人的方向。我们希望训练有限的宽高角,因此我们将它四舍五入到最近的0.25
。SortbyQ
函数是我们将用于对训练进行排序的自定义排序键,将最高奖励(用字母Q
表示)放在第一位。 -
在这一步,我们将声明一个将教会机器人抓取物体的类。我们将把这个类命名为
LearningHand
,并将其作为ROS 2中的一个节点:class LearningHand(Node): def __init__(self): super().__init__('armQLearn') # node name # we need to both publish and subscribe to the RobotCmd topic self.armPosSub = self.create_subscription(Int32MultiArray, "xarm_pos", self.armPosCallback, 10) self.cmdSubscribe = self.create_subscription(String, 'RobotCmd', self.cmdCallback,10) self.cmdPub = self.create_publisher(String, 'RobotCmd', 10) self.wristPub = self.create_publisher(Int32,'xarmWrist', 10) # declare parameter for number of repetitions self.declare_parameter('ArmLearningRepeats', rclpy.Parameter.Type.INTEGER) # get the current value from configuration self.repeats = self.get_parameter('ArmLearningRepeats').get_parameter_value().int_value
在这里,我们通过将
init
函数传递给父类(使用super
)来初始化对象。我们给节点命名为armQLearn
,这样机器人其他部分就能找到它。我们的ROS接口订阅了几个主题。我们需要与机械臂通信,因此我们订阅了
xarm_pos
(手臂位置)。我们需要订阅(就像与机器人通信的每个程序一样)RobotCmd
,这是我们主模式命令通道。我们还需要能够在RobotCmd
上发送命令,因此我们在该主题上创建了一个发布者。最后,我们使用ROS参数设置每个学习任务重复次数的值。 -
下一个代码块完成了学习函数的设置:
self.mode = "idle" self.armInterface = ArmInterface() # define the state space self.stateActionPairs = [] # state space is the target aspect and the hand angle # aspect is length / width length along x axis(front back) width on y axis) aspects = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75] handAngles = [90, -45, 0, 45] # note +90 and -90 are the same angle for jj in range(0,len(aspects)): for ii in range(0,4): self.stateActionPairs.append([aspects[jj], handAngles[ii],0.0])
我们将学习系统模式设置为
idle
,这意味着“等待用户开始学习。”我们通过实例化我们导入的ArmInterface
类对象来创建手臂接口。接下来,我们需要设置我们的学习矩阵,它存储可能的方面(我们可以看到的事物)和可能的行为(我们可以做的事情)。这里我们设置为0的最后一个元素是Q
值,这是我们存储训练结果的地方。 -
以下函数集帮助我们控制手臂:
def sndCmd(self,msgStr): msg = String() msg.data = msgStr self.cmpPub.publish(msg) def setHandAngle(self,ang): msg = Int32() msg.data = ang self.wristPub.publish(msg) def armPosCallback(self,msg): self.currentArmPos = msg.data def setActionPairs(self,pairs): self.stateActionPairs = pairs
sndCmd
(发送命令)在RobotCmd
主题上发布并设置手臂模式。SetHandAngle
,正如你所期望的,设置手腕伺服电机的角度。armPosCallback
接收手臂的当前位置,这是由手臂控制程序发布的。setActionPairs
允许我们创建新的动作对以进行学习。 -
现在我们准备进行手臂训练。这是一个结合了人和机器人活动的过程,真的非常有趣。我们将尝试20次相同的方面:
def training(self, aspect): # get the aspect from the vision system #aspect = 1.0 # start here stateActionPairs.sort(key=sortByQ) # sort by Q value if len(stateActionPairs)<1: #error - no aspects found! # self.get_logger().error("qLearningHand No Aspect for Training") return else: mySetup = stateActionPairs[0] # using the highest q value handAngle = mySetup[1] myOldQ = mySetup[2]
这在机械臂上启动了训练程序。我们首先基于方面进行训练。我们首先查看我们的
stateActionPairs
以按此方面的最高Q
值进行排序。我们使用我们的自定义SortbyQ
函数对stateActionPairs
列表进行排序。我们将手角度设置为具有最高Q
值或预期奖励的角度。 -
程序的这一部分是机器人手臂将要经历的物理运动:
sndCmd("ARM MID_CARRY") timer.pause(1.0) sndCmd("ARM GRASP") time.sleep(1.0) setHandAngle(handAngle) time.sleep(0.3) # close the gripper sndCmd("ARM GRASP_CLOSE") time.sleep(0.5) # now raise the arm sndCmd("ARM MID_CARRY") time.sleep(1.0)
我们首先告诉手臂移动到Mid Carry位置——中间位置。然后我们等待1秒钟,直到手臂完成其动作,然后我们将手臂移动到抓取位置。下一步将手腕移动到
Q
函数得到的角。然后我们使用ARM GRASP_CLOSE
命令关闭夹爪。现在我们抬起手臂,看看夹爪是否能够举起玩具,使用ARM MID_CARRY
指令。如果我们成功,机器人手臂现在将持有玩具。如果不成功,夹爪将是空的。 -
现在我们可以检查夹爪中是否有物体:
#check to see if grip is OK handPos = self.currentArmPos[0] gripSuccess = False if handPos > 650: ## fail gripSuccess = -1 # reward value of not gripping else: # success! gripSuccess = +1 # reward value of gripping
如果机器人手的握持正确,玩具将阻止夹爪关闭。我们检查手的位置(手臂每秒发送两次)以查看位置。对于我的特定手臂,对应于650伺服单位或更大的位置是完全关闭的。你的手臂可能不同,所以检查手臂报告的完全关闭和空夹爪的位置。我们根据适当的情况设置
gripSuccess
变量。 -
现在我们进行机器学习部分。我们使用在机器人臂的机器学习部分中引入的特别修改后的Bellman方程来调整这个状态-动作对的Q值:
# the Bellman Equation ### Q(s, a) = Q(s, a) + α * [R + γ * max(Q(s', a')) - Q(s, a)] newQ = myOldQ + (learningRate*(gripSuccess)) mySetup[2]=newQ
由于我们不是使用未来的奖励值(我们从这个关闭夹具和抬起手臂的动作中获得完整的奖励),我们不需要预期的未来奖励,只需要当前的奖励。我们将
gripSuccess
值(+1
或-1
)乘以学习率,并将其添加到旧的Q分数中,以获得新的Q分数。每次成功都会增加奖励,而任何失败都会导致减少。 -
为了完成我们的学习函数,我们将更新的Q值放回与测试的角度和手腕角度相匹配的学习表中:
foundStateActionPair = False # re insert back into q learning array for i in range (0,len(stateActionPairs): thisStateAction = stateActionPairs[i] if thisStateAction[0] == mySetup[0] and thisStateAction[1] == mySetup[0]: foundStateActionPair=True stateActionPairs[2]=mySetup[2] # store the new q value in the table if not foundStateActionPair: # we don't have this in the table - let's add it stateActionPairs.append(mySetup) input("Reset and Press Enter") # wait for enter key to continue
如果这个状态-动作对不在表中(它应该在那里),我们就添加它。我这样做只是为了防止在给出奇怪的臂角度时程序出错。最后,我们暂停程序并等待用户按下Enter键以继续。
-
现在我们来看看程序的其余部分,这部分相当直接。我们必须做一些维护工作,处理一些调用,并构建我们的主要训练循环:
def cmdCallBack(self,msg): robotCmd = msg.data if robotCmd == "GoLearnHand": self.mode = "start" if robotCmd == "StopLearnHand": self.mode = "idle"
这个
cmdCallBack
接收来自RobotCmd
主题的命令。在这个程序中,我们只处理两个命令:GoLearnHand
,它启动学习过程,以及StopLearnHand
,它允许你停止训练。 -
这个部分是我们的臂接口到机器人臂的接口,并设置了我们需要用来控制臂的发布/订阅接口:
class ArmInterface(): init(self): self.armPosSub = self.create_subscription(Int32MultiArray, 'xarm_pos',self.armPosCallback, 10) self.armAngSub = self.create_subscription(Int32MultiArray, 'xarm_angle',self.armAngCallback, 10) self.armPosPub = self.create_publisher(Int32MultiArray, 'xarm') def armPosCallback(self,msg): self.armPos = msg.data def armAngCallback(self, msg): self.armAngle = msg.data # decoder ring: [grip, wrist angle, wrist pitch, elbow pitch, sholder pitch, sholder yaw] def setArmPos(self,armPosArray): msg = Int32MultiArray msg.data = armPosArray self.armPosPub.publish(msg)
我们订阅了
xarm_pos
(伺服单元中的臂位置)和xarm_angle
(以度为单位的手臂位置)。我在xarm
主题上添加了设置机器人臂位置的能力,但你可能不需要这个功能。对于每个订阅,我们需要一个回调函数。我们有一个
armPosCallback
和armAngleCallback
,当臂发布其位置时将被调用,我将此设置为每秒2赫兹,即每秒两次。如果你觉得有必要,可以在xarm_mgr
程序中增加这个速率。 -
现在我们进入主程序。对于许多ROS程序,这个主要部分相当简短。我们需要在这里添加一个额外的例程。为了在训练后保存训练函数,我想出了这个解决方案——将状态-动作对pickle并放入一个文件中:
### MAIN #### # persistent training file to opeate the arm ArmTrainingFileName = "armTrainingFile.txt" armIf = ArmInterface() armTrainer = LearningHand() #open and read the file after the appending: try: f = open(ArmTrainingFileName, "r") savedActionPairs = pickle.load(f) armTrainer.setActionPairs(savedActionPairs) f.close() except: print("No Training file found") self.get_logger().error("qLearningHand No Training File Found armTrainingFile.txt")
当我们运行这个程序时,我们需要加载这个文件并将我们的动作对表设置为这些保存的值。我设置了一个
try
/except
块,当找不到这个训练文件时发送错误消息。这将在你第一次运行程序时发生,但我们将很快为下一次运行创建一个新的文件。我们还实例化了臂训练器和臂接口的类变量,这创建了我们的训练程序的主要部分。
-
这是我们的训练循环的核心。我们设置了训练的方面和试验重复次数:
aspectTest = [1.0, 0.5, 1.5,2] trainingKnt = 20 for jj in aspectTest: for ii in range(0,trainingKnt): print("Starting Training on Aspect ", jj) armTrainer.training(jj)
从玩具与机器人前方平行开始。进行20次拾取尝试,然后移动玩具45度向右进行下一部分。然后进行另外20次尝试。然后,将玩具移动到与机器人成90度角。运行20次试验。最后,将玩具设置为-45度(向左)进行最终设置,运行20次。欢迎来到机器学习!
-
你可能会猜到我们最后要做的事情是保存我们的训练数据,如下所示:
f = open("ArmTrainingFileName", "w") # open file in write mode pickle.dump(armTrainer.stateActionPairs,f) print("Arm Training File Written") f.close()
这完成了我们的训练程序。重复进行这种训练,直到你对所有类型的玩具都进行了训练,你应该会得到一个能够以各种角度持续拾取玩具的机器人手臂。首先,选择你希望机器人拾取的玩具。将玩具的角度设置为机器人0度——比如说,这是玩具最长部分与机器人前方平行。然后我们向RobotCmd
发送GoLearnHand
以将机器人手臂置于学习模式。
我们尝试过几种不同的配置进行Q学习,但在训练我们的机器人方面取得了一些有限的成果。Q学习的主要问题是,我们有一个非常大的可能状态数,或者说位置数,机器人手臂可以处于这些位置。这意味着通过重复试验获得任何单个位置的大量知识是非常困难的。接下来,我们将介绍一种使用遗传算法生成我们的运动动作的不同方法。
介绍遗传算法(GAs)
移动机器人手臂需要同时协调三个电机以创建平滑的运动。我们需要一种机制来为机器人创建不同的电机运动组合以进行测试。我们本可以使用随机数,但这将是不高效的,可能需要数千次试验才能达到我们想要的训练水平。
如果我们有一种方法来尝试不同的电机运动组合,并将它们相互对抗以选择最佳组合,这将是一种类似于达尔文的“适者生存”的机器人手臂运动脚本——例如遗传算法过程。让我们探讨如何将这个概念应用到我们的用例中。
理解遗传算法(GA)过程的工作原理
这里是我们遗传算法过程中涉及的步骤:
-
我们进行一次试验运行,从位置1(中性携带)到位置2(拾取)。在将手放入正确位置之前,机器人将手臂移动100次。为什么是100次?我们需要足够大的样本空间,以便算法能够探索不同的解决方案。当值为50时,解决方案没有满意地收敛,而值为200时,结果与100次相同。
-
我们根据目标完成百分比对每次移动进行评分,表明这次移动对目标的贡献程度。
-
我们将10个最佳移动放入数据库中。
-
我们再次进行测试,并做同样的事情——现在我们有10个更多的“最佳移动”和20个动作在数据库中。
-
我们从第一组中选取五个最佳动作,与第二组中选取的五个最佳动作进行交叉——再加上随机选择的五个动作和五个完全随机的动作。交叉两个解决方案指的是从第一组中取一段,从第二组中取一段的过程。在遗传学的术语中,这就像从两个父母中各取一半的DNA来制造一个新的孩子。
-
我们运行这一系列动作,然后选择10个最佳的单个动作并继续进行。
通过选择的过程,我们应该很快就能得到一个执行任务的序列。它可能不是最优的,但它是有效的。我们正在管理我们的基因库(我们问题的试验解决方案列表),通过连续近似来创建一个问题的解决方案。我们希望保持一个良好的可能性混合,这些可能性可以用不同的方式组合起来,以解决将我们的手臂移动到目标位置的问题。
我们实际上可以使用几种杂交我们动作序列的方法。我描述的是一种简单的杂交——第一父母遗传物质的一半和第二父母物质的一半(如果你能原谅这个生物学的比喻)。我们也可以使用四分之一——四分之一第一,四分之一第二,四分之一第一,四分之一第二——来进行两次杂交。我们也可以随机从其中一个或另一个中抓取片段。我们现在将坚持一半/一半的策略,但你完全可以根据自己的意愿进行实验。本质上,在这些所有选项中,我们都在采取一个解决方案,将其一分为二,然后随机将其与另一个试验中一半的解决方案结合。
你可能要提出一个反对意见:如果动作少于10步怎么办?简单——当我们到达目标时,我们停止,并丢弃剩余的步骤。
注意
我们不是在寻找完美或最优的任务执行,只是足够好以完成任务的东西。对于许多实时机器人,我们没有时间上的奢侈来创建一个完美的解决方案,所以任何能完成任务的解决方案都是足够的。
为什么我们要添加五个额外的随机样本动作和五个完全随机的动作?这也模仿了自然选择——变异的力量。我们的遗传代码(我们体内的DNA)并不完美,有时劣质材料会被传递下去。我们也可能从基因的坏副本、宇宙射线和病毒中经历随机突变。我们引入一些随机因素来调整我们算法的调谐——自然选择的元素——以防我们收敛到一个局部最小值或错过一些简单的路径,因为在我们之前的动作中还没有发生。
但为什么我们要费这么大的劲呢?遗传算法过程可以为软件做一件非常困难的事情——它可以通过尝试直到找到有效和无效的方法,从基本动作中创新或进化出新的解决方案。我们提供了一个额外的机器学习过程来添加到我们的工具箱中,但这是一个可以创建我们程序员没有预先设想出的解决方案的过程。
现在,让我们深入GA过程。为了提高透明度,我们将从头开始构建自己的GA过程。
注意
在这个版本中,我们将构建自己的工具,但也有一些预构建的工具集可以帮助你创建GA,例如pip install deap
。
构建GA过程
我们松散地采用“适者生存”的概念来决定哪些计划是最适合的,并得以生存和繁衍。我给你一个沙盒,让你在其中扮演遗传工程师的角色,你将能够访问所有部件,没有任何东西隐藏在幕后。你会发现,对于我们的问题,代码并不那么复杂:
-
我们将首先创建
computefitness
函数,即评分我们的遗传材料。适应性是我们评估算法的标准。我们可以随心所欲地改变适应性,以调整我们的输出以满足我们的需求。在这种情况下,我们正在为机器人手臂从起始位置到目标位置构建空间路径。我们根据路径上的任何点接近目标的方式评估我们的路径。就像我们之前的程序一样,机器人的运动由三个电机的顺时针、逆时针或不动这27种组合构成。我们将运动分成小步骤,每个步骤大约是三个电机单元(1.8度)的运动。我们将这些步骤连在一起形成一个路径。适应性函数沿着路径前进,并在每个步骤计算手的位置。 -
predictReward
函数对机器人手由于该步骤而移动的位置进行试验计算。假设我们顺时针移动电机1三个步骤,保持电机2不动,并逆时针移动电机3三个步骤。这导致手稍微向上和向外移动。我们通过每个步骤接近目标的方式单独评分。我们的评分是100分;100分正好在目标处,我们每100分之1的距离从目标处减去一分,最多减去340毫米。为什么是340?这是手臂的总长度。我们评分的方式可能与你想象的略有不同。总奖励的加总没有区别,因为我们想要的是最接近目标点的点。因此,我们选择具有最高奖励的单个步骤并保存该值。我们丢弃该步骤之后的任何步骤,因为它们只会让我们离目标更远。因此,我们自动修剪路径,使其在目标处结束。 -
我使用术语“等位基因”来表示整个路径中的一个单独步骤,我将其称为
chrom
,是染色体(chromosome)的简称:def computeFitness(population, goal, learningRate, initialPos): fitness = [] gamma = 0.6 state=initialPos index = 0 for chrom in population: value=0 for allele in chrom: action = ACTIONMAT[allele] indivFit, state = predictReward(state,goal,action,learningRate) value += indivFit if indivFit > 95: # we are at the goal – snip the DNA here break fitness.append([value,index]) index += 1 return fitness
-
我们如何创建初始路径?
make_new_individual
函数使用随机数构建我们的初始染色体种群,或路径。每个染色体包含由0到26的数字组成的路径,这些数字代表所有有效的电机命令组合。我们将路径长度设置为10到60之间的随机数:def make_new_individual(): # individual length of steps lenInd = random.randint(10,60) chrom = [] # chromosome description for ii in range(lenInd): chrom.append(randint(26)) return chrom
-
我们使用
roulette
函数选择我们种群的一部分继续进行。每一代,我们从得分最高的 50% 的个体中选择他们的 DNA 来创建下一代。我们希望路径或染色体的奖励值在选择过程中起到作用;奖励分数越高,成为后代的机会就越大。这是我们选择过程的一部分:# select an individual in proportion to its value def roulette(items): total_weight = sum(item[0] for item in items) weight_to_target = random.uniform(0, total_weight) for item in items: weight_to_target -= item[0] if weight_to_target <= 0: return item # main Program INITIAL_POS = [127,127,127] GOAL=[-107.39209423, -35.18324771] robotArm=RobotArm() robotArm.setGoal(GOAL) population = 300 learningRate = 3 crossover_chance = .50 mutate_chance = .001 pop = []
-
我们首先用随机部分构建初始种群。它们的原始适应度将非常低:大约 13% 或更低。我们维持一个包含 300 个个体路径的池,我们称之为染色体:
for i in range(population): pop.append(make_new_individual()) trainingData=[] epochs = 100
-
在这里,我们设置循环以遍历 100 代的自然选择过程。我们首先计算每个个体的适应度,并将该分数添加到一个适应度列表中,该列表的索引指向染色体:
for jj in range(epochs): # evaluate the population fitnessList = computeFitness(pop,GOAL,learningRate, INITIAL_POS)
-
我们按逆序排序适应度以获得最佳个体。最大的数字应该排在第一位:
fitnessList.sort(reverse=True)
-
我们保留种群中排名前 50% 的个体,并丢弃排名后 50% 的个体。下半部分由于不适应而被排除在基因池之外:
fitLen = 150 fitnessList = fitnessList[0:fitLen] # survival of the fittest...
-
我们从整个列表中挑选出表现最好的个体,并将其放入 名人堂(HOF)。这将是我们的最终输出。同时,我们使用 HOF 或 HOF 适应度(HOFF)值作为这一代适应度的衡量标准:
hoff = pop[fitnessList[0][1]] print("HOF = ",fitnessList[0])
-
我们将 HOFF 值存储在
trainingData
列表中,以便在程序结束时绘制结果图:trainingData.append(fitnessList[0][0]) newPop = [] for ddex in fitnessList: newPop.append(pop[ddex[1]]) print ("Survivors: ",len(newPop))
-
在这个阶段,我们已经删除了种群中排名后 50% 的个体,移除了表现最差的个体。现在我们需要用这一代最佳表现者的后代来替换他们。我们将使用交叉作为配对技术。有几种遗传配对可以产生成功的后代。交叉很受欢迎,是一个好的起点,同时也很容易编码。我们所做的一切只是在基因组中挑选一个位置,从一位父母那里取前半部分,从另一位父母那里取后半部分。我们从剩余的种群中随机选择父母进行配对,按其适应度成比例加权。这被称为 轮盘赌选择。更好的个体被赋予更高的权重,更有可能被选中进行繁殖。我们为这一代创造了 140 个新的个体:
# crossover # pick to individuals at random # on the basis of fitness numCross = population-len(newPop)-10 print ("New Pop Crossovers",numCross) # # # add 5 new random individuals for kk in range(10): newPop.append(make_new_individual()) for kk in range(int(numCross)): p1 = roulette(fitnessList)[1] p2 = roulette(fitnessList)[1] chrom1 = pop[p1] chrom2 = pop[p2] lenChrom = min(len(chrom1),len(chrom2)) xover = randint(lenChrom) # xover is the point where the chromosomes cross over newChrom = chrom1[0:xover]+chrom2[xover:]
-
我们的下一步是 变异。在真实自然选择中,DNA 有很小的机会会被宇宙射线、序列的错误复制或其他因素所损坏或改变。一些变异是有益的,而一些则不是。我们通过让新后代路径中的一个基因有很小的机会(大约 1/100)随机改变成其他值来创建我们这个过程的版本:
# now we do mutation bitDex = 0 for kk in range(len(newChrom)-1): mutDraw = random.random() if mutDraw < mutate_chance: # a mutation has occured! bit = randint(26) newChrom[kk]=bit print ("mutation") newPop.append(newChrom)
-
现在我们已经完成了所有的处理,我们将这条新的后代路径添加到我们的种群中,并为下一代评估做好准备。我们记录一些数据并返回到起点:
# welcome the new baby from parent 1 (p1) and parent 2 (p2) print("Generation: ",jj,"New population = ",len(newPop)) pop=newPop mp.plot(trainingData) mp.show()
那么,我们的疯狂遗传实验做得怎么样?以下输出图表自说自话:
图5.5 – GA解决方案的学习曲线
尽管GA看起来像是一种有点像巫术的编程,但作为训练我们机器人臂的特定案例的机器学习工具,它工作得相当好。我们的解决方案在90代左右达到了99.76%的目标(大约2毫米),这对于人工智能学习过程来说相当快。你可以看到学习的平滑性,这表明这种方法可以用来解决我们机器人臂的路径规划问题。我必须承认,我对这个过程相当怀疑,但它似乎在这个特定的问题领域工作得相当好。
编程实际上并不太难,你可以花些时间通过调整GA的参数来改进这个过程。如果我们有一个更小的种群会怎样?如果我们改变了适应度标准会怎样?进去,捣鼓一下,看看你能学到什么。
替代的机器人臂机器学习方法
通过机器学习进行机器人臂控制的领域实际上才刚刚开始。有几个研究方向我想在您寻找进一步研究时引起您的注意。理解机器人运动的一种方法就是考虑利用和探索之间的平衡。利用就是尽可能快地将机器人带到目标位置。探索则是利用机器人周围的空间尝试新事物。路径规划程序可能已经陷入了局部最小值(可以想象成死胡同),可能存在更好的、更优的解决方案,而这些方案尚未被考虑。
教导机器人的方法不止一种。我们在训练中一直使用一种自我探索的形式。如果我们能够向机器人展示该做什么,并让它通过示例学习会怎样?我们可以让机器人观察人类执行同样的任务,并让它尝试模仿结果。让我们在接下来的章节中讨论一些替代方法。
谷歌的SAC-X
谷歌正在尝试一种稍微不同的方法来解决机器人臂问题。在他们的计划辅助控制(SAC-X)程序中,他们认为给机器人臂的个别动作分配奖励点可能相当困难。他们将复杂任务分解成更小的辅助任务,并为支持这些任务的辅助任务分配奖励点,让机器人逐步建立起面对复杂挑战的能力。如果我们用机器人臂堆叠方块,我们可能会将拾取方块作为一个任务,手持方块移动作为另一个任务,等等。谷歌将这种如果只在主要任务上使用强化,即堆叠方块在另一个方块上作为稀疏奖励问题。你可以想象,在教机器人堆叠方块的过程中,会有数千次失败的尝试,直到一个成功的移动导致奖励的产生。
亚马逊机器人挑战赛
亚马逊有数百万个箱子、零件、碎片和其他东西堆放在货架上。该公司需要将这些东西从货架上取下来放入小箱子中,以便在你下单时尽可能快地将它们运送到你那里。在过去几年中,亚马逊赞助了亚马逊机器人挑战赛,邀请来自大学的团队使用机械臂从货架上取下物品,然后,正如你所猜到的,将它们放入箱子中。
当你考虑到亚马逊几乎销售所有可以想象得到的东西时,这是一个真正的挑战。2017年,来自澳大利亚昆士兰州的一支团队凭借一个低成本机械臂和一个非常好的手部追踪系统赢得了挑战。
摘要
本章的任务是使用机器学习教机器人如何使用它的机械臂。我们使用了两种技术,并做了一些变体。我们使用了多种强化学习技术,或称为Q学习,通过根据机器人机械臂的状态选择单个动作来开发运动路径。每个动作都被单独评分作为奖励,作为整体路径的一部分作为价值。这个过程将学习结果存储在一个Q矩阵中,可以用来生成路径。我们通过索引,或编码,从可能的电机组合的27元素数组中提取动作作为从0到26的数字,同样将机器人状态索引到状态查找表中。这导致学习过程的速度提高了40倍。我们的Q学习方法在处理机器人机械臂可能处于的大量状态时遇到了困难。
我们的第二种技术是遗传算法(GA)。我们创建了个体的随机路径来形成一个种群。我们创建了一个适应度函数来评估每条路径与我们的目标,并保留每一代的顶尖表现者。然后,我们从两个随机选择的个体中交叉遗传物质来创建一个新的子路径。GA还通过在路径步骤中随机改变一小部分来模拟突变。GA的结果显示,对于我们的机器人机械臂的状态空间复杂性没有问题,并在几代之后生成了一个有效的路径。
我们为什么要费这么大的劲?当其他经验方法要么难以实现,要么不可靠,或者不产生在合理时间内解决问题的解决方案时,我们使用机器学习技术。我们还可以使用这些技术解决可能对暴力或仅数学解决方案难以处理的大量更复杂的问题。
在下一章中,我们将为机器人添加一个带有自然语言处理功能的语音界面,这样你就可以与机器人交谈,它会倾听——并回应。
问题
-
在Q学习中,Q代表什么?
提示:你需要自己进行研究。
-
我们能做些什么来限制Q学习算法需要搜索的状态数量?
-
改变学习率对学习过程有什么影响?
-
在 Q-learning 方程中,哪个函数或参数用于惩罚较长的路径?增加或减少这个函数会有什么影响?
-
在遗传算法中,你将如何对较长的路径进行惩罚,以便更偏好较短的路径(步骤数量较少)?
-
查找 SARSA 变体的 Q-learning。你将如何将 SARSA 技术应用到程序 2 中。
-
改变遗传算法中的学习率会有什么影响?学习率的上限和下限是多少?
-
在遗传算法中,减少种群数量会有什么影响?
进一步阅读
-
《Python 深度学习》,作者 Zocca, Spacagna, Slater 和 Roelants,Packt 出版
-
《使用 Python 的人工智能》,作者 Prateek Joshi,Packt 出版
-
《AI 爱好者:遗传算法 - 简要概述》,来自 http://www.ai-junkie.com/ga/intro/gat2.html
-
《基本强化学习教程 2:SARSA》:https://github.com/vmayoral/basic_reinforcement_learning/tree/master/tutorial2
-
Google DeepMind 博客:通过玩耍学习(机器人臂(SAC-X)):https://deepmind.com/blog/learning-playing/
第六章:教会机器人倾听
教会机器人倾听口头指令本身就是一个完整的学科。机器人仅仅识别个别单词或一些预先设定的短语是不够的。我们希望机器人能够以正常多样的措辞对口头命令做出反应。我们可能会说,“拿起玩具”,或者“请把所有玩具都拿起来”,或者“清理这个混乱”,这些都可以是有效的指令,指示机器人开始搜索房间中的玩具并取走。我们将在本章中使用各种技术和流程。我们将基于一个开源的语音助手 Mycroft 进行构建,这是一个基于人工智能的语音识别和 自然语言处理(NLP)引擎,我们可以对其进行编程和扩展。我们将为 Mycroft 添加一些额外的功能——我们将使用我称之为“填空”方法的命令处理技术来提取用户语音指令的意图,这样机器人就能做你想让它做的事情,即使那不是你说的原话。我们将通过教会机器人讲述和回应特定形式的人类交流——敲门笑话来完成这一章。
本章将涵盖以下主题:
-
使用 NLP 探索机器人语音识别——包括 语音转文本(STT)和 文本转语音(TTS)
-
编程我们的机器人
技术要求
本章使用以下工具:
-
Mycroft 开源语音助手 (http://mycroft.ai) - 我必须从 GitHub 仓库 (https://github.com/MycroftAI) 中从头开始构建,因此请预期您也需要这样做以保持与我们在机器人上运行的 机器人操作系统(ROS)兼容。
-
Python 3.2。
-
您需要一个 GitHub 账户,请访问 https://github.com/.
-
我在这个项目中使用了微型 USB 扬声器和麦克风,它们与 Jetson 配合得非常好。您可以在 https://www.amazon.com/gp/product/B08R95XJW8 找到它们。
本章使用的代码可以在本书的 GitHub 仓库中找到,请访问 https://github.com/PacktPublishing/Artificial-Intelligence-for-Robotics-2e。
使用 NLP 探索机器人语音识别
这将是一个相当复杂的章节,但所有概念都相对容易理解。我们将最终构建一个强大的框架,用于构建语音识别和命令。你不仅会得到一个基于语音的机器人命令系统,还会得到一个功能齐全的数字助手,可以讲笑话。让我们首先快速介绍 NLP。
简要介绍 NLP 概念
自然语言处理(NLP)不仅仅是将声音波转换为书面文字(语音到文本,或STT),还包括理解这些文字的含义。我们不想只有一些僵化的、预先编程的语音命令,而希望机器人能够对人类语音做出响应。
我们将使用两种不同的STT处理形式:
-
频谱分析:这种类型有助于检测你何时说出机器人的名字。这种技术通过采样构成单词的频率和振幅来识别单词或短语。这个过程的优势在于不需要太多的计算机资源,并且擅长识别单个单词或短语——我们的“唤醒词”,这将使计算机切换到第二种类型的语音识别。这就是为什么其他语音操作助手需要你使用特定的词(例如,Siri或Alexa)来启用它们开始监听的原因。
-
音素识别:这种技术通过识别构成单词的发音部分——音素——来转换语音识别技术(STT)。这种试图将所有声音解释为单词的技术要困难得多,所以我们使用唤醒词来触发这种转换。我们将在本章后面更详细地介绍这一点。
接下来,让我们探讨实现语音识别的主要目标。
设定我们的目标
我们在第二章中为我们的机器人设定了几个目标,包括能够对机器人发出语音命令,因为我们可能在没有基站的情况下使用机器人。我还希望机器人能够与我的孙子孙女互动,特别是能够讲并回应敲门笑话,这是我的孙子威廉最喜欢的活动。对于我们的机器人,我们不想使用预先录制或记忆中的语音命令,而是希望它能够进行一些自然语言处理,以创建一种机器人对 spoken word 的理解形式。例如,如果我们想要一个捡起玩具的命令,我们人类可以用几种方式表达:抓起一个玩具,握住一个玩具,拿起那个玩具车,甚至拿那个。我们希望机器人能够理解或至少对所有的这些表述做出相同的反应,驱动机器人手臂前往最近的玩具并将其捡起。STT系统今天相当普遍,但我们希望机器人的语音模式有一些自然的变体,以帮助创造机器人比实际更聪明的错觉。我们可以将这个过程分解为几个步骤,我们将独立处理这些步骤:
-
接收音频(声音)输入。我们需要机器人能够听到或具有将声音转换为数字形式的能力。
-
这些声音需要被转换成机器人可以处理的文本。
-
对这些文本单词进行加工处理,以理解说话者的意图。我们需要识别单个单词,并将这些单词组合成句子,从这些句子中推断说话者的意图,以理解机器人应该做什么。
-
使用那个意图作为命令来执行某些任务。
-
以语音的形式(文本到语音,或TTS)向操作员提供口头回应,以确认机器人已听到并理解了指令。
-
创建一个定制的语音界面,既能讲述也能回应“敲门”笑话。
我们将在下一节介绍STT的过程,这是机器人如何接收您的语音输入。
理解STT过程
在本章的剩余部分,我们将实现机器人的基于AI的语音识别和响应系统,并创建我们自己的定制语音界面。我们将使用Mycroft,这是一个开源的语音激活数字助手,擅长理解语音,并且易于扩展以实现新的功能和定制界面。
我们将详细讨论语音交互中涉及的每个步骤。在这个过程中涉及两种STT形式,这大大简化了机器人的任务:唤醒词识别和STT识别。让我们首先探索唤醒词识别。
监听唤醒词
在第一种方法中,机器人持续监听唯一的语音——唤醒词。这是一个特定的声音,仅意味着一件事——准备处理下一个声音成为指令。为什么这是必要的呢?由于机器人只有一个非常小的处理器——Jetson Nano,它实际上并没有足够的板载计算能力来运行一个健壮的语音识别引擎。但它可以运行一个简单的声音识别器,可以监听唤醒词。您可能从其他语音命令系统中熟悉这一概念,例如Alexa或Siri,它们也使用特殊的唤醒词或按钮来让界面注意(参见https://www.howtogeek.com/427686/how-alexa-listens-for-wake-words/)。
一旦接收到唤醒词,Jetson Nano将切换到录音模式,并记录我们接下来所说的话。然后,它将信息传输到在线系统,即Google Cloud Speech to Text系统(与运行Google Assistant的系统相同)。
机器人是如何识别唤醒词的?我们将使用的语音系统,开源系统Mycroft,使用两种方法之一:
-
第一个是一个名为Sphynx的音素识别系统。什么是音素?你可以理解,单词是由单个声音组成的,我们大致将这些声音分配给字母表中的字母。例如,单词pet或pick中的p声音就是一个音素的例子。单词“Albert”有几个音素——A声音(啊),L声音,B,ER组合在一起,最后是T。我们与声音关联的字母——例如,cherry中的ch和Albert中的er——被称为图形符号,因为它们图形地表示了这些声音。我们可以这样说,语音到文本(STT)问题是将这些音素映射到图形符号的问题,但我们知道这太简单了——英语中有各种各样的借词和短语,它们的发音和拼写相差甚远。
Mycroft语音识别过程的前端使用音素来识别唤醒词。你会发现它非常敏感。我没有任何问题让语音处理器从八英尺远的地方接收唤醒词。当我们设置时,我们将默认的Mycroft唤醒词从“嘿,Mycroft”更改为“嘿,阿尔伯特”。
-
Mycroft还可以使用一个经过训练的神经网络,该网络被训练成通过它们的频谱功率图一次性识别整个单词。什么是频谱图?你的声音不是单一频率的声音能量——它是由我们的嘴巴和声带产生的不同频率的复杂集合。如果我们用纯频率说话,我们会像笛子一样——主要是单一频率的纯音。我们可以使用一个称为快速傅里叶变换的过程将一段语音转换成一个图表,该图表显示了每个频率的能量(音量)。这被称为频谱图或频谱功率图。低频在左侧,高频在右侧。大多数人类语音的能量集中在300 Hz到4,000 Hz之间的频率上。每个单词在这些频率上的声音能量分布都是独特的,可以通过这种方式被神经网络识别:
图6.1 – (顶部)模拟音频波形和短语“嘿,阿尔伯特”的频谱图(底部)
上述图表展示了短语“嘿,阿尔伯特”在时域中的音频波形(顶部图表)和频域中的频谱图。
语音识别方法和神经网络方法都使用频谱图来识别声音作为单词,但语音识别过程将单词分解成单个声音,而神经网络则一次性监听并识别整个单词。这为什么会造成很大的差异呢?语音识别系统可以开发出来识别英语中的任何单词,而无需重新编程或重新训练,而神经网络必须对每个单词单独进行训练,并且希望由很多不同口音的不同说话者进行训练。我们将为阿尔伯特使用神经网络方法。
注意
你可以从第4章中回忆起,我们需要标记的数据来训练一个神经网络。你记得我们有一些分类的图片,并在每个分类上进行训练。为声音训练人工神经网络(ANNs)也是一样的:我们需要声音和相关的单词。你能想到一个地方,那里有很多不同声音的样本,同时也有精确的书面脚本相匹配吗?你有没有听过录音带上的书?
转换语音识别
在接收到唤醒词之后,我们的下一步是记录机器人听到的下一个声音。然后,Mycroft系统将音频数据通过互联网传输到谷歌在线语音识别引擎(https://cloud.google.com/speech-to-text/)。这是一种快速解决问题的方法,我们的Jetson Nano处理能力或存储空间不足,无法拥有强大的语音识别能力。
谷歌云中发生了什么?语音识别引擎将语音分解成音素(声音),并使用神经网络为这些声音分配最可能的图形(字母)。输出会比我们想要的更音素化。例如,根据Carnegie Mellon大学发音词典,句子“请捡起玩具?”发音为P IH K . AH P . DH AH . T OY Z . P L IY Z
。为什么会这样?发生了什么?这些都是构成那个句子的音素。句号表示单词之间的空格。现在系统必须将其转换为我们期望的单词。语音识别系统使用单词规则和词典来得出最可能的常规单词转换。这包括专家系统(单词规则)和基于音素的预测输出单词的训练神经网络。
我们可以称这个步骤为语言模型。我们的语音识别输出句子“一加仑有多少盎司?”并将其在不到两秒内发送回机器人。
现在我们有了文本中的命令,一个英语句子,机器人如何识别你的意图呢?
明确意图
我们正在做的NLP有一个目标或目标。我们通过语音界面向我们的机器人下达命令。英语中的命令通常遵循句子模式,类似于“你 - 做这个。”句子中的“你”主语通常是隐含或理解的,并被省略。我们剩下的是像“清理这个房间”或“捡起那些玩具”这样的陈述。这些命令的意图是让机器人启动一个程序,使机器人捡起玩具并将它们放好。机器人和它的处理器必须从所说的词语中推断或推导出用户的意图。我们希望任何合理的句子都有这样的意义:“你,机器人,开始你的捡起玩具过程。”
想想我们可以有多少种方式对机器人说那个命令。以下是一些例子:
-
让我们清理这个房间
-
把玩具放好
-
拾起玩具
-
拾起所有玩具
-
清理这个房间
-
把那些放好
-
把这个放好
-
是时候清理了
这些短语有什么共同之处?它们都暗示执行动作的主体是机器人。没有像“你”、“机器人”或“阿尔伯特”这样的词来指示命令的目标对象。“玩具”这个词出现很多,同样“捡起”、“清理”和“放好”也经常出现。我们可能只需要关注这些关键词来理解这个命令。如果我们去掉所有常见的连词和代词,这个列表看起来会是什么样子?
-
清理房间
-
放置玩具
-
拾起玩具
-
拾起玩具
-
清理房间
-
放好
-
放好
-
时间清洁
本章的一个重要概念是理解我们不是试图理解所有语音,而是只理解机器人可以执行的命令子集。解决这个语音识别问题的通用解决方案可能是有能力从机器人收到的命令中预测,用户的意图指向一个命令的可能性比指向其他任何命令的可能性更大。你可以看到,在“清理”这个词的情况下,我们的其他命令(“开车绕圈”、“移动手臂”或“停止”)与“清理”根本不相关。因此,包含“清理”的句子很可能与“捡起玩具”命令相关。这个决定意图的过程将在本章后面用于通过Mycroft向机器人发送命令。
现在我们将直接进入编程Albert机器人,使其通过Mycroft监听和理解命令。
编程我们的机器人
如本章前面所讨论的,Mycroft是一种数字助理版本,类似于苹果的Siri或亚马逊的Alexa,因为它可以以基本上正常的方式监听语音命令并将这些命令与计算机接口。我们使用它是因为它有一个在Jetson Nano 3上运行的界面。在本节中,我们将设置我们的硬件和我们的软件(即Mycroft)。
设置硬件
我们将在 Nvidia Jetson Nano(或您使用的任何微处理器)上安装 Mycroft。Jetson Nano 没有附带的一件少数几件事情是音频功能。它没有扬声器或麦克风。我发现,添加这种功能的一个快速有效的方法是使用现有的硬件套件,该套件提供了一个非常高质量的扬声器和一个优秀的立体声麦克风套件,且适合机器人使用。请注意,这几乎适用于任何 Linux 单板 计算机(SBC)。
该套件是一个微型 USB 音频板,可插入 Jetson Nano。它既有扬声器也有麦克风。
注意
我为 Jetson Nano 使用了一个 USB 音频板(品牌不重要,因为任何一种都行),它对我来说工作得非常好,并且适合我们机器人上非常小的空间。安装过程非常简单。插入音频板。您需要前往屏幕右上角的设置来选择 USB 音频版本。将列出几个其他选项。
使用新的扬声器和麦克风开启您的 Jetson Nano 3。我使用 YouTube 进行了快速测试,以确保音频工作正常,您可以直接在设置用户界面中进行测试。现在我们可以深入软件部分。
设置 Mycroft 软件
安装 Mycroft 有几种方法,因为我们必须将 Mycroft 安装在我们已经安装的其他软件之上:
-
由于 Mycroft 必须与 ROS 以及我们安装的所有 AI 库(如 TensorFlow、Theano 和 Keras)兼容,因此我们最好使用
git clone
方法下载源代码,并在 Jetson Nano 上构建 Mycroft:git clone https://github.com/MycroftAI/mycroft-core.git cd Mycroft-core bash dev_setup.sh
Mycroft 将创建它运行所需的虚拟环境。它还将 Mycroft 软件包与其他 Jetson Nano 上的软件包隔离开。
注意
请不要以 root 用户(或超级用户)的身份安装 Mycroft。这会导致配置文件出现权限问题。
-
为了让 Mycroft 系统能够以这种方式运行,我还必须再进行一个步骤。当我第一次尝试让 Mycroft 运行时,系统总是失败。当我尝试启动调试器时,它会退出或卡住。为了解决这个问题,我必须按照以下步骤重新编译整个系统:
sudo rm -R -/.virtualenvs/Mycroft cd ~/mycroft-core mycroft-core directory.
-
您可以从调试模式开始:
Time skill is totally self-contained inside the Jetson Nano. The robot should give you a voice response that is replicated on the debug console.
-
接下来,你可以向 Mycroft 询问更高级的技能,例如在互联网上查找信息。问:“嘿,Mycroft,一加仑有多少盎司?”Mycroft 将使用互联网查找答案并回复。
-
接下来,你可以在 Mycroft 网站上更改唤醒词,使其更合适——我们并没有给这个机器人起名为 Mycroft。我们一直称这个机器人为阿尔伯特,但你也可以选择给机器人起任何你想要的名字。你可能发现一个非常短的名字,比如鲍勃,可能太短,不适合作为唤醒词,所以选择一个至少有两个音节的词。为此,导航到 Mycroft 网页(http://home.mycroft.ai)并登录你的账户,这是我们之前在步骤 4中创建的。点击右上角的你的名字,从菜单中选择设置。你可以在这一页上选择几个设置,例如你想要的语音类型、度量单位以及时间和日期格式。选择高级设置,这将带你去一个可以更改唤醒词的页面。
-
我们将第一个字段,即唤醒词字段,更改为自定义。我们将下一行更改为输入我们的自定义唤醒词——“嘿,阿尔伯特。”
-
我们还需要查找这个唤醒词的音素。从卡内基梅隆大学的CMU 发音词典(http://www.speech.cs.cmu.edu/cgi-bin/cmudict)查找。输入我们的短语,你将得到音素短语。复制并粘贴这个短语,然后回到 Mycroft 页面,将音素短语粘贴到音素字段中。你已经完成了——不要更改其他任何设置。
-
在你离开页面之前,请点击页面顶部的保存按钮。
你可以在 Jetson Nano 上测试你的新唤醒词。再次以调试模式启动 Mycroft 并等待其启动。说出你的新唤醒短语并享受其响应。我有一组标准的测试短语来展示 Mycroft 作为我们机器人声音的能力。尝试以下短语:
-
嘿,阿尔伯特。现在几点了?
-
嘿,阿尔伯特。明天的天气怎么样?嘿,阿尔伯特。一加仑有多少盎司?
-
嘿,阿尔伯特。英国国王是谁?
你应该得到对这些问题的适当答案。
Mycroft 有许多其他我们可以利用的技能,例如设置定时器、设置闹钟、在 Pandora 上听音乐或播放新闻。我们接下来要做的是通过创建我们自己的、针对我们房间清洁机器人的特定技能来扩展这些技能。
添加技能
我们将创建的第一个技能是捡起玩具的命令。我们将把这个命令连接到 ROS 以控制机器人。稍后,我们将添加一个讲敲门笑话的技能。
设计我们的对话
我们的第一步是设计我们的对话,确定我们将如何与机器人交谈。首先,列出你可能告诉机器人去捡起游戏室玩具的各种方式。以下是我的列表,这是我使用 ChatGPT(版本 3.5)生成的:
-
嘿,机器人,你能帮我开始捡起所有的玩具吗?
-
是时候整理一下了。你能帮我收集所有的玩具吗?
-
需要你的帮助,机器人。你能帮我捡起所有的玩具并放进玩具箱吗?
-
我们一起来打扫。首先,请收集所有的玩具。
-
我需要一间干净的房间。您能先捡起散落在各处的玩具吗?
-
亲爱的机器人,您能否麻烦一下,把所有的玩具收拾起来,整理一下房间?
-
让我们把这个房间打扫得干干净净。请先收集所有玩具。
-
是时候打扫了。
-
你能捡起所有的玩具吗?
-
拾起所有玩具。
-
您可以将所有玩具收集起来,放在玩具箱里。
-
是时候展示您的清洁技能了。请先从捡起所有玩具开始。
-
请您通过捡起所有玩具来整理房间。
您会注意到有几个关键词是针对清洁房间命令的特定关键词。当然,我们有“清洁”这个词。我们有短语“捡起”和“拿开”。我们还有“玩具”或“玩具”,最后还有“杂乱”。这些关键词将引导自然语言处理器,并允许在确切使用的词语上有所变化。
接下来,我们写下我们希望机器人回应的内容。我们不希望每次都是相同的预设回复;机器人回应的自然变化会更好。以下是我由ChatGPT3创建的回应列表,其中展示了各种机器人的态度:
-
命令已接收——捡起玩具。
-
当然,我会立即开始打扫房间。
-
明白了,我会整理房间,并确保一切物品都归位。
-
清洁模式已激活!我会确保您的房间干净整洁、井井有条。
-
如您所愿,我将为您打扫房间。
-
没问题,我会使用我的清洁能力高效地整理房间并清除任何杂乱。
-
已完成!我将打扫、除尘和拖地,让您有一个一尘不染的房间。
-
清洁中!我会勤奋地打扫房间,并确保它整洁、可展示。
-
我明白了!我会使用我的清洁工具和技术,让您的房间看起来一尘不染。
-
已确认,我将整理房间并确保它整洁有序,以便您使用。
-
清洁任务已接受!我会勤奋工作,让您的房间变得干净、舒适,就像您喜欢的那样。
您可以使用您想要的任何数量的这些短语。Mycroft系统会从这个列表中随机抽取一个短语。这为我们提供了创意空间,并给人一种机器人比实际更智能的错觉。这种响应系统使我们能够快速开发我们的对话。
在下一节中,我们将探讨如何将语音命令连接到ROS接口。
创建技能
在本节中,我们将构建Mycroft框架,使其能够将我们的口语短语与发送给机器人的命令关联起来。这需要添加我们希望机器人拥有的每个技能。
正在整理玩具
首先,让我们为Albert添加最基本的功能——通过捡起玩具来打扫房间。为此,请按照以下步骤操作:
-
使用
mycroft-msk create
命令,这有助于我们将技能以正确的格式组合在一起:cleanroomrobot-skill.
-
然后,它将询问类名和存储库名,这两个我都使用了
Cleanroomrobot
。 -
为你的技能输入一行描述:
捡起房间里的所有玩具
。 -
输入一个长描述,例如
命令机器人检测玩具,移动去抓取一个玩具,拿起它,并将其放入
玩具箱
。 -
输入一些示例短语以触发你的技能:
-
嘿机器人,你能开始捡起所有
玩具吗?
-
是时候整理了。你能帮我收集所有玩具吗?
-
你能捡起所有
玩具吗?
-
-
输入以下参数:
-
<你的
名字在这里>
-
Productivity
-
IoT
-
-
输入标签可以更容易地搜索你的技能(尽管这是可选的):
robot
,cleanup
,pick up
和toys
。 -
我们最终会在
/opt/Mycroft/skills/cleanroomrobot-skill
目录结构中拥有以下结构:Cleanroomrobot-skill Git __pycache__ Locale En-us cleanroomrobot.dialog cleanroomrobot.intent __init__.py LISCENSE.md Manifest.yml README.md init.py file in the skill_pickup_toys directory that we copied from the template.
-
我们将导入 Mycroft 所需的库(
IntentBuilder
,MycroftSkill
,getLogger
和intent_handler
)。我们还导入rclpy
,ROS 的 Python 接口,以及 ROS 标准消息String
,我们通过在syscommand
主题上发布来使用它向机器人发送命令:from mycroft import MycroftSkill, intent_handler, intent_file_handler import rclpy from rclpy.node import Node from std_msgs.msg import String, Int32MultiArray, Int32 from adapt.intent import IntentBuilder from mycroft.util.log import getLogger
注意
MycroftSkill
是一个在它的短语被 Mycroft 意图引擎识别时被调用的函数。因此,它没有主体或主函数,只有一个 create_skill
函数的定义,该函数实例化一个 MycroftSkill
对象。init
函数执行设置技能的各种对话框、意图处理程序和词汇的大部分工作。这种安排在我们的有限环境中工作得非常好,即给出机器人命令或讲笑话。
-
下一个行是 Mycroft 的日志记录器,这样我们就可以保存我们的响应。我们将输出到 stdout 的任何内容,例如打印语句,最终都会记录在日志中,或者在调试模式下显示在屏幕上:
Cleanroomrobot to match what we defined previously:
class Cleanroomrobot(MycroftSkill):
def init(self):
MycroftSkill.init(self)
def setRobotInterface(self,interfce):
self.interface = interfce
def initialize(self):
pass # 目前只返回
-
我们在 ROS 中设置了
syscommand
主题的发布者。这是我们通过 ROS 发布/订阅系统向机器人控制程序发送命令的方式。我们只发布命令,我们需要的唯一消息格式是String
:pub = rospy.Publisher('/syscommand', String, queue_size=1000) # define our service for publishing commands to the robot control system # all our robot commands go out on the topic syscommand def pubMessage(str): pub.publish(str)
-
我们的 Mycroft 技能被创建为
MycroftSkill
对象的子对象。我们将我们的技能对象类重命名为CleanRoomSkill
:class CleanRoomSkill(MycroftSkill): def init (self): super(CleanRoomSkill, self). init (name="PickupToys")
根据模板,Mycroft 需要一个
init
方法和initialize
方法。这些命令在 Mycroft 的意图构建器部分设置意图,并在我们任何短语被说出时注册我们的处理器。 -
接下来,我们使用
require("CleanRoomKeyword")
引用我们在 创建技能 部分构建的对话框,所以请确保所有的拼写都是正确的:def initialize(self): clean_room_intent = IntentBuilder("cleanroomrobot").require("cleanroomrobot").build() self.register_intent(clean_room_intent, self.handle_cleanroomrobot)
-
下一节创建了一个处理程序,当系统识别到我们的一种短语时,并希望执行此命令的操作。这就是我们通过之前定义的
pubMessage
函数使用ROS启动向机器人的控制程序发布命令的地方:@intent_file_handler('cleanroomrobot.intent') ##@intent_handler('cleanroomrobot.intent') def handle_cleanroomrobot(self, message): self.speak_dialog('cleanroomrobot') self.interface.cmdPublisher("CleanRoom")
-
我们还需要一个
stop
函数,其中我们可以命令机器人停止清洁,如果需要,以防止任何形式的米老鼠-魔术师-学徒事故:def stop(self): self.interface.cmdPublisher("STOPCleanRoom") pass
注意
在电影《幻想曲》中,米老鼠扮演了童话中魔术师的学徒的角色。在故事中,学徒学会了使扫帚活动,他教扫帚从井里取水。问题是学徒从未学会如何停止魔法,很快房间里就充满了水。
-
现在我们需要一个代码块来在程序中创建技能,以便我们将ROS接口与机器人关联到技能中。我们将添加一个
create_skill
函数,以便Mycroft创建技能并有一个函数指针来启用技能:def create_skill(): newSkill = Cleanroomrobot() newSkill.setRobotInterface(rosInterface()) return newSkill
-
接下来,我们有ROS接口。我们只需要向机器人发送一个命令,在
RobotCmd
主题上发布模式命令:class rosInterface(Node): def __init__(self): super().__init__('mycroftROS') # node name self.cmdSubscribe = self.create_subscription(String, 'RobotCmd', self.cmdCallback,10) self.cmdPublisher = self.create_publisher(String, 'RobotCmd', 10) def cmdCallback(self,msg): robotCmd = msg.data
我们定义我们的ROS接口并创建一个名为
mycroftROS
的控制节点,作为我们的接口。然后我们创建一个订阅者和发布者到RobotCmd
主题,这样我们就可以从ROS 2接口发送和接收命令。 -
程序的其余部分只是日常维护。我们需要启动我们的ROS节点,启动Mycroft记录器,并实例化ROS接口对象和ROS和Mycroft的
cleanSkill
对象。然后我们将cleanSkill
对象指向ROS接口,以便它们可以通信。最后,我们使用.spin
函数启动ROS 2接口。当程序停止时,我们退出.spin
并关闭我们的程序:## main ### rclpy.init() LOGGER = getLogger(__name__) interface = rosInterface() cleanSkill = Cleanroomrobot() cleanSkill.setRobotInterface(interface) rclpy.spin(interface) rosInterface.destroy_node() rclpy.shutdown()
-
为了使我们的技能正常工作,我们需要将我们的目录复制到
/opt/mycroft/skills
。从那里,我们可以在调试模式下测试它。记住,你必须源ROS 2目录(source /opt/ros/foxy/local_setup.sh
和source ~/ros2_ws/install/local_setup.sh
),否则程序将无法找到所有包含文件或ROS节点。
我们下一个技能是根据我的孙子威廉的要求来的,他非常喜欢敲敲门笑话。威廉七岁,所以他正是适合这个年龄。让我们看看我们如何实现它。
讲笑话
在本节中,我们将处理机器人讲敲敲门笑话的情况。如您所知,敲敲门笑话是基于双关语的笑话,通常采用相同的形式:
第一个人:敲,敲
第二个人:谁在那里?
第一个人:木
第二个人:木谁?
第一个人:你愿意知道吗!
如您所见,对话非常简单。其中一些部分是标准的,例如前两行——“敲,敲”和“谁在那里?”我们可以在以下形式中创建一个通用的敲敲门笑话:
-
敲,敲。
-
谁在那里?
-
<``word 1>
-
<word
1>
谁? -
<``punchline phrase>
在定义我们的笑话时,您可以看到我们只有两个变量元素——步骤3中的单词和步骤5中的笑点短语。我们的单词在步骤4中被重复。
我们首先创建一个包含单行笑话的笑话数据库,我们将将其放入一个文本文件中。由于我们只有两个元素,我们可以用斜杠(/
)来分隔它们。以下是一个示例:
tarzan / tarzan stripes forever
orange / orange you glad I can tell jokes?
我在当前章节的仓库文件部分提供了一个大约10个笑话的数据库。请随意添加您喜欢的所有笑话,或者发送给我,我会添加它们。
现在,让我们看看讲笑话所涉及的步骤:
-
我们将像任何技能一样,从唤醒词“嘿,阿尔伯特”开始。
-
然后,我们需要一个短语来表示我们想要听一个笑话,所以我们将使用“告诉我一个敲门笑话”的变体,例如“我想听一个敲门笑话”。
-
这将触发我们的技能程序查找一个笑话。我们将创建几个意图或响应能力来响应用户(或孩子)与机器人交谈。我们将从“谁在那里?”对话意图开始。这将让机器人知道继续到笑话的下一部分,即说出我们的单词。
-
然后我们禁用了“谁在那里?”对话,并启用了一个用于监听
<word>
和短语“who”的对话。 -
然后,我们可以通过背诵笑点短语来传达笑话的最后一部分,这样我们就完成了。
我们如何实现这个功能?您可以按照以下步骤进行:
-
我们首先创建我们的词汇文件,我们需要三个。这些是用户将对机器人说的话。我们有第一个“告诉我一个敲门笑话”短语——所以让我们创建一个名为
knockknock.voc
的文件(您可以使用任何文本编辑器来创建文件)并将以下内容放入其中:Tell me a knock-knock joke Can I have a knock-knock joke Give me a knock-knock joke Play me a knock-knock joke
请注意,Mycroft STT系统将短语“敲门”解释为带有连字符的
knock-knock
,所以将这一点放入我们的脚本中非常重要。 -
现在我们第二个词汇是“谁在那里”,因此我们可以创建这个作为第二个
.voc
文件,whosthere.voc
,它包含一行Whos there
。 -
我们的最后一行有点棘手。我们真正关心的是触发笑点的关键词“谁”,所以我们只需查找那个词。创建一个名为
who.voc
的文件,并将一个单词who
放入其中。记住,这些文件都应该放在我们的skill
目录下的dialog/en-us
文件夹中。 -
现在我们来处理我们的回应。我们有一个预设的回应,即用短语“敲门”来回应“告诉我一个敲门笑话”。我们不需要任何复杂的对话系统,我们只需要让机器人说出“敲门”短语。为此,我们首先导入在这个程序中需要调用的库,即
MycroftSkill
类和intent_file_handler
函数:from mycroft import MycroftSkill, intent_file_handler
-
我们将我们的技能定义为
MycroftSkill
对象的子对象——这是一个标准的面向对象设计。我们正在继承MycroftSkill
父对象的所有功能和数据,并添加我们自己的功能。我们创建了一个初始化函数,然后调用init
父函数来执行父类的代码。我们正在增强init
父函数的功能。如果没有这个调用,我们将用我们自己的替换init
函数,可能需要复制大量工作:class Knockknock(MycroftSkill): def __init__(self): MycroftSkill.__init__(self)
-
下一步是创建我们的
knockknock.intent
文件,并将该文件放置在voc
目录中(之前是dialog/voc-en
):@intent_file_handler('knockknock.intent') def handle_knockknock(self, message): name,punchline = self.pick_joke()
在这里,我们从笑话数据库中获取两个部分:
-
在“谁在那里”之后要说的名字
-
结束笑话的点睛之笔
-
-
我们使用
MycroftSkill
中的get_response
函数让机器人发表声明并等待回复,该回复将被转换成文本字符串并存储在response
变量中:response=self.get_response(announcement="knock, knock") # response will always be "who's there" response=self.get_response(announcement=name)
-
现在我们到了机器人回应名字的部分。例如,用户问“谁在那里?”机器人回答“哈罗德。”我们期待用户接下来会说“哈罗德(或任何名字)谁?”我们将检查我们的回应,看看是否包含单词“谁”。如果没有,我们可以提示用户跟随笑话。我们只会这样做一次,以避免陷入循环。如果他们不参与,机器人将继续:
# response will be "name who" # if end of respose is not the word who, we can re-prompt if "who" not in response: prompt = "You are supposed to say "+name+" who" response=self.get_response(announcement=prompt)
-
我们已经讲完了笑话,现在轮到说出点睛之笔,比如“哈罗德,你想拥抱吗?”(你想要拥抱吗?)。任务完成,我们退出程序;既包括喜剧程序也包括程序程序:
self.speak(punchline)
-
我们需要一个函数来读取我们之前定义的笑话数据库。如前所述,数据库中每行有一个敲门笑话,名字和点睛之笔之间用正斜杠(
/
)分隔。我们读取所有笑话,将它们放入一个列表中,然后使用(等着瞧)random.choice
函数随机选择一个。我们分别返回名字和点睛之笔。我们应该只为每个笑话实例调用这个程序一次:def pick_joke(): jokeFile="knockknock.jokes" jfile = open(jokeFile,"r") jokes = [] for jokeline in jfile: jokes.append(jokeline) joke = choice(jokes) jokeParts = joke.split("/") name = jokeParts[0] punchline = jokeParts[1] return name, punchline
-
我们通过定义
Knockknock
类的实例并返回该对象给调用程序 Mycroft 来结束程序:def create_skill(): return Knockknock()
接下来,我们将讨论敲敲门笑话概念的另一端,即接收笑话——孩子想要告诉机器人一个笑话。如果你认识任何七岁的孩子,那么你知道这也是一个要求——孩子也会想要告诉机器人一个笑话。
接收笑话
接收对话也很简单。用户会说“敲门,敲门”,这是机器人进入接收“敲门笑话”模式的提示。然后机器人只有一个回应——“谁在那里。”如果我们想保持科幻中常见的概念,即机器人不使用缩写,我们也可以添加“谁在那里?”
注意
来自《星际迷航:下一代》的机器人Data多次声明他无法使用缩写,尽管编剧偶尔会犯错误。
为了创建我们新的Mycroft技能的架构,我们将使用pip3 install msk
。MSK提供了一个基于对话的方法来构建技能,这将创建一个框架,包括所有对话文件和词汇的子目录。这节省了很多时间,所以让我们试试:
-
以下是为创建“接收敲门”笑话代码的命令:
$ msk create Enter a short unique skill name (ie. "siren alarm" or "pizza orderer"): receiveKnock Class name: ReceiveKnockSkill Repo name: receive-knock-skill Looks good? (Y/n) y Enter some example phrases to trigger your skill: knock knock - Enter what your skill should say to respond: who's there Enter a one line description for your skill (ie. Orders fresh pizzas from the store): This skill receives a knock knock joke from the user Enter a long description: This is the other half of the Knock Knock joke continuum - we are giving the robot the ability to receive knock knock jokes. The user says knock knock, the robot responds whos there and so on > Enter author: Francis Govers Would you like to create a GitHub repo for it? (Y/n) Y === GitHub Credentials === Username: ********** Password:********* Counting objects: 12, done. Delta compression using up to 4 threads. Compressing objects: 100% (5/5), done. Writing objects: 100% (12/12), 1.35 KiB | 0 bytes/s, done. Total 12 (delta 0), reused 0 (delta 0) To https://github.com/FGovers/receive-knock-skill * [new branch] master -> master /opt/Mycroft/skills/receive-knock-skill. The program is still the init.py file.
-
我们从导入开始,这些是
MycroftSkill
和intent_file_handler
。我们还需要time
库来进行一些暂停:from mycroft import MycroftSkill, intent_file_handler import time
-
这里是我们为
ReceiveKnock
类定义的类,它是我们导入的MycroftSkill
对象的子类。我们在init
函数中通过传递一个init
命令回传给父类(MycroftSkill
)并让它执行初始化。然后我们在其基础上添加我们的自定义功能:class ReceiveKnock(MycroftSkill): def __init__(self): MycroftSkill.__init__(self)
-
下一节是我们的意图处理器,用于接收“敲门”笑话。我们使用
@decorator
来扩展意图处理器,在这种情况下,从名为knock.receive.intent
的文件中读取意图的参数。意图处理器只有我们两个关键词,不朽的短语:“敲门,敲门”。我们很幸运,所有的笑话都以完全相同的方式开始,所以我们只需要这两个词。在意图引擎看到短语“敲门,敲门”并激活
handle_knock_receive
函数后,控制权传递到我们的处理器。我们的下一步是什么?我们用一个简单的回答“谁在那里?”来回应。你会记得我们说过机器人不使用缩写。我们使用不同的函数来做这件事。我们不希望使用另一个意图处理器,但幸运的是,Mycroft提供了一个自由形式的接口,称为get_response
。你需要查找这个多功能函数的文档,但它使我们的笑话程序变得更加简单。get_response
函数既让我们说出我们的回答,然后接收用户接下来说的任何话,并将其存储为字符串在response
变量中:@intent_file_handler('knock.receive.intent') def handle_knock_receive(self, message): response =self.get_response('who.is.there')
现在我们有了我们的回应,我们可以用机器人的声音重复它,并额外加上一个词“谁?”。所以,如果孩子说,“霍华德”,机器人回应“霍华德谁?”
-
我们再次使用
get_response
让机器人说话,并记录孩子或成人接下来所说的话。我们不需要它,但我们想让机器人的语音系统听到接下来所说的话。我们丢弃了回应,但将我们自己的评论插入到我们的对话veryfunny.dialog
中,这是一个位于dialog
目录的文件。我创建了这个文件来保存机器人对我们笑话的回应。我尝试了一些孙子辈可能会觉得好笑的回应——我想我可以把“机器人笑话作家”加到我的简历上,因为我似乎在我的职业生涯中做了很多这样的事情。之后,我添加了一个睡眠计时器,以便在返回控制之前让一切平静下来。我们包括所有MycroftSkills
所需的stop
函数,并让我们的create_skill
函数创建一个ReceiveCall
对象并返回它:response2= response + " who?" response3 =self.get_response(announcement=response2) self.speak_dialog('veryfunny') time.sleep(3) def stop(self): pass def create_skill(): return ReceiveKnock()
你可以想多创意就有多创意,但这里是我的建议:
-
那真是太有趣了!
-
哈哈哈哈。
-
真是好笑。
-
我喜欢这个。谢谢!
-
哈哈!哈哈。
-
那真可爱!
-
我没有咕噜咕噜的声音!
-
这里是我们的接收敲门笑话技能的目录结构和文件:
receive-knock-skill directory:
init .py README.md
settingsmeta.json
./dialog/en-us:
knock.receive.dialog veryfunny.dialog
./vocab/en-us:
knock.receive.intent
将技能的本地版本放入/opt/mycroft/skills/receive-knock-skill
目录。现在尽情测试吧——你能对机器人讲多少个敲门笑话?
摘要
本章介绍了用于机器人的自然语言处理技术,并专注于开发一个能够完成三项任务的机器人自然语言界面:启动捡玩具过程,讲讲敲门笑话,以及听敲门笑话。
介绍的概念包括通过音素识别单词,将音素转换为音素和音素转换为单词,从句子中解析意图,以及通过语音界面执行计算机程序。我们介绍了开源AI引擎Mycroft,这是一个基于AI的语音助手程序,在Jetson Nano上运行。我们还编写了一个笑话数据库,用一些非常简单的对话来娱乐小孩子。
在下一章中,我们将学习使用地标、神经网络、避障和机器学习进行机器人导航。
问题
-
做一些互联网研究,了解为什么AI引擎被命名为Mycroft。你找到了多少不同的故事,哪一个是你喜欢的?
-
在讨论意图时,你会如何设计一个神经网络来从自然语言句子中预测命令意图?
-
将“接收敲门笑话”重写为记住机器人所讲的笑话,将它们添加到“讲敲门笑话”程序使用的笑话数据库中。这是机器学习吗?
-
将“讲笑话”程序修改为播放波形文件中的声音,例如音乐剪辑,以及进行语音合成。
-
本章使用的句子结构都是基于英语语法的。其他语言,如法语和日语,有不同的结构。这会如何改变句子的解析?我们编写的程序能否理解Yoda?
-
你认为 Mycroft 的意图引擎实际上是在理解意图,还是只是在提取关键词?
-
描述必要的语音命令,以指导机器人驶向一个物体并拿起它,而机器人无法识别该物体。你需要多少个命令?
-
来自 问题7,努力减少命令的数量。你可以消除或合并多少个?
-
也来自 问题7,涉及多少个独特的关键词?多少个非独特关键词?
进一步阅读
-
《Python 自然语言处理》 by Jalaj Thanaki,Packt Publishing
-
《Python 人工智能》 by Prateek Joshi,Packt Publishing
-
Mycroft 开发技能教程位于 https://mycroft.gitbook.io/mycroft-docs/developing_a_skill/introduction-developing-skills
-
使用 Mycroft 的附加文档位于 https://media.readthedocs.org/pdf/mycroft-core/stable/mycroft-core.pdf
第3部分:高级概念 – 导航、操作、情感等
在本书的最后部分,我们探讨更高级的主题,包括基于 AI 的导航和避障。我们了解无监督学习的决策树和分类算法,然后开始一个关于创建机器人性格模拟的激动人心的章节。虽然我们无法给机器人赋予真实的情感,但我们可以使用状态机和蒙特卡洛技术来创建情感模拟。最后,我们以作者的角度讨论 AI 哲学,并展望未来,为那些想将机器人学和自主性作为职业的人提供建议。
本部分包含以下章节:
第七章:教机器人导航和避免楼梯
让我们快速回顾一下我们创建能够捡起玩具的机器人的目标。我们已经创建了一个玩具检测器并训练了机器人手臂。接下来我们的待办事项是什么?我们需要将机器人驱动到玩具的位置以便捡起它。这听起来很重要。
本章涵盖了我们的玩具抓取机器人助手的任务中的导航和路径规划。你必须承认这是机器人学中最困难的问题之一。任务分为两部分——确定你的位置(定位),然后确定你想去哪里(路径规划)。到目前为止,大多数机器人都会使用某种形式的同时定位与地图构建(SLAM)算法,该算法首先绘制房间地图,然后确定机器人在其中的位置。但这是否真的必要呢?首先,SLAM通常需要某种3D传感器,我们没有,而且需要大量的处理,我们不想这样做。我们还可以补充说,它不使用机器学习,而这本书是关于人工****智能(AI)的。
是否可以在不制作地图或使用测距传感器的情况下完成任务?你能想到任何其他清洁房间但不进行制图的机器人吗?当然可以。你可能甚至有一个Roomba®(我有三个),而且大多数型号根本不做任何制图——它们通过伪随机的统计清洁程序进行导航。
本章的任务是创建一个可靠的导航系统,用于我们的机器人,该系统能够适应我们的任务,即清洁单个房间或玩具层,并且使用我们已有的传感器。
本章将涵盖以下主题:
-
理解SLAM方法
-
探索替代导航技术
-
介绍用于避开障碍物的Floor Finder算法
-
实现神经网络
技术要求
我们需要机器人操作系统版本2(ROS 2)来完成本章。本书使用Foxy Fitzroy版本:http://wiki.ros.org/foxy/Installation。本章假设你已经完成了第6章,其中我们给了机器人声音和接收语音命令的能力。我们将使用Mycroft界面和语音文本到语音系统,称为Mimic:https://github.com/MycroftAI/mimic3。你可以在本书的GitHub仓库中找到本章的代码:https://github.com/PacktPublishing/Artificial-Intelligence-for-Robotics-2e。
我们还将使用Python的Keras库(https://keras.io),这是一个强大的机器学习应用库,允许我们构建自定义神经网络。你可以使用以下命令安装它:
pip install keras
你还需要PyTorch,可以使用以下命令安装:
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
任务分析
就像我们为每一章做的那样,让我们回顾一下我们想要达成的目标。我们将驾驶机器人绕着房子转,寻找玩具。一旦我们有了玩具,我们将把它带到玩具箱,通过把它扔进玩具箱来把它放好。然后,机器人将去寻找更多的玩具。在这个过程中,我们需要避开障碍物和危险,包括一组会导致机器人损坏的向下楼梯。
备注
我在测试的第一部分用婴儿门封住了楼梯,在第二部分则在楼梯上放上了枕头。在机器人还在学习的时候,没有必要让它从楼梯上弹跳下来。
我们将假设这个任务列表中的任何一项都不需要机器人知道自己的位置。这是真的吗?我们需要找到玩具箱——这很重要。我们能否在不了解位置的情况下找到玩具箱?答案是,当然,机器人可以使用它的摄像头搜索玩具箱,直到找到它。我们之前在第4章中开发了一种使用神经网络识别玩具箱的技术。
现在,如果机器人要完成更大的任务,比如清理一个100万平方英尺的仓库,那么我们就需要一张地图。但我们的任务是清理一个16 x 16英尺的房间。考虑到我们不能走得太远,而且我们无论如何都必须开车到玩具箱,寻找玩具箱所花费的时间并不那么重要。因此,我们将设定一个挑战,在不制作地图的情况下完成我们的任务。
备注
我曾经监督过麻省理工学院创建的一个机器人系统的评估。他们有一个不使用地图的导航系统,我非常怀疑。为了自卫,机器人实际上在测试中迷路了。现在,我正在制作一个无地图的导航器,他们可以提出批评。
我们还需要让机器人完成以下任务:
-
在房间里导航,避开障碍物(玩具和家具)和危险(楼梯)。
-
在房间里找到玩具(使用我们之前创建的玩具探测器)。
-
驾驶到机器人手臂可以触及玩具的位置。
-
用机器人手臂拿起玩具。
-
把玩具拿到玩具箱。
-
把玩具放进玩具箱。
-
去找另一个玩具。
-
如果没有更多的玩具,那么停止。
我们在其他章节中已经涵盖了找到玩具和拿起玩具的内容。在本章中,我们将讨论开车到玩具那里去拿它。
我非常喜欢电影《公主新娘》。它有剑斗、悬崖、两次智慧较量,还有不同寻常的大老鼠(ROUS)。它还包含了一个我们可以效仿的计划课程。当我们的英雄,巨人菲兹克,伊尼戈·蒙托亚,和韦斯利计划突袭城堡以营救公主时,韦斯利首先问的是“我们的责任是什么?”和“我们的资产是什么?”让我们为我们的用例做同样的事情:
-
我们的责任:我们有一个小型机器人,其传感器和计算能力非常有限。我们有一个房间里满是错位的玩具,还有一组机器人可能会掉下去的致命楼梯。
-
我们的资产:我们有一个带有全向轮的机器人,可以四处行驶,一个声音,一个摄像头和一个机器人臂。机器人通过Wi-Fi与控制计算机连接。我们有这本书。我们有一个颜色独特的玩具箱。还有许多标准尺寸的玩具(TOUS)。
无论我们是设计机器人还是入侵城堡,适当的下一步是做一些头脑风暴。你将如何解决这个问题?
我们可以使用SLAM制作地图,然后定位机器人在地图上的位置,并使用它来导航。虽然我们最终不会遵循这种方法,但让我们快速看看它是如何工作的。
理解SLAM方法学
SLAM是室内机器人导航的常见方法。在我们深入具体细节之前,让我们看看两个关键问题:
-
室内机器人驾驶中我们遇到的第一问题是我们没有地图。
-
我们遇到的第二个问题是,我们没有定位自己的参考框架——室内GPS不起作用。
这就是两个问题——我们需要一个地图,然后我们需要一种方法来定位自己在地图上。虽然SLAM以字母"S"代表“同时”,但事实上,大多数机器人在开始时制作地图,将其存储起来,然后稍后驾驶。当然,在制作地图的同时,机器人必须制作地图并定位自己在地图上——通常在中心位置。
SLAM是如何工作的?通常与SLAM相关的传感器是旋转的LIDAR。你可以把LIDAR想象成激光雷达——它使用激光测量物体距离,并旋转以收集机器人周围的所有数据。
我们可以将SLAM方法总结如下:
-
机器人通过在圆形内旋转激光测距仪来测量房间的尺寸。
-
返回的数据是一系列距离测量值,其中角度测量是列表中位置的函数。如果我们有一个圆周上360个测量的列表,那么列表中的第一个数字是0度,下一个是1度,以此类推。
-
我们可以通过寻找角落、边缘、跳跃和不连续性来从LIDAR数据中提取特征。
-
我们查看每个特征从后续测量中的角度和距离,并创建一个函数,给出机器人移动的最佳估计。
-
我们使用这些信息将LIDAR数据从传感器中心坐标系转换到某种房间坐标系,通常是通过假设机器人的起始位置是坐标0,0。我们的变换,或者说数学变换,将是机器人身体框架的平移(移动)和旋转的组合。
-
估计这种变换的一种方法就是使用粒子。我们在机器人可能移动的每一个可能点上创建机器人的运动空间样本,并在所有点上随机放置点。我们计算每个样本的变换,然后测试哪个样本最适合收集到的数据。这被称为粒子滤波器,是我用于大多数我的SLAM项目的技术。
更多细节,您可以参考https://www.cs.cmu.edu/~16831-f14/notes/F12/16831_lecture04_dfouhey.pdf。
在长而特征不明显的走廊中,SLAM可能难以工作或根本无法工作,因为它没有任何信息可以工作——一个LIDAR扫描看起来就像下一个。为了帮助解决这个问题,许多SLAM系统需要向机器人添加其他传感器,这些传感器测量轮距或使用光流来测量运动,以提供额外的位置估计数据。以下是用ROS和RViz显示的由ROS制作的SLAM地图的示意图。机器人使用500个粒子来估计机器人位置的最佳变化,以使LIDAR数据与地图中其余数据对齐。这是我早期的一个机器人项目:
图7.1 – 由SLAM导航过程生成的地图
在SLAM过程中,我们必须做的是以下内容:
-
首先,进行一次扫描,测量机器人到房间中所有物体的距离。
-
然后,我们将机器人移动一段距离——例如,向前移动三英寸。
-
然后,我们进行另一次扫描并再次测量距离。
-
我们现在需要想出一个转换,将第二次扫描中的数据转换成与第一次扫描中的数据对齐。为此,两个扫描中必须有可以相关联的信息——角落、门道、边缘和家具。
你可以花大约100美元购买一个非常小的机器人LIDAR(例如,SLAMtec的RPLidar),并使用它来制作地图。有一个名为Hector Mapping的优秀ROS包,它使得使用这种LIDAR变得简单。你会发现SLAM不是一个可靠的过程,并且需要多次尝试才能生成一个可用的地图。一旦地图创建完成,如果房间中的任何东西发生变化,例如重新布置家具,你必须保持地图更新。
SLAM过程实际上非常有趣,不是因为单个扫描中发生了什么,而是因为扫描是如何拼接在一起的。有一个名为手持式地图在RoboCup 2011救援竞技场的优秀视频,由德国达姆施塔特大学的Hector SLAM作者制作,展示了地图制作过程。视频可在以下链接中找到:https://www.youtube.com/watch?v=F8pdObV_df4list=PL0E462904E5D35E29。
我想快速提醒你关于SLAM,这样我们就可以讨论为什么我们不会使用它。SLAM是一个重要的话题,并且广泛应用于导航,但它绝不是解决我们问题的唯一方法。SLAM在我们目的中的弱点包括以下内容:
-
需要某种类型的扫描传感器,如LIDAR、超声波或红外线,这些可能很昂贵,机械复杂,并产生大量数据。我们希望保持我们的机器人便宜、可靠和简单。
-
如果机器人有轮式里程计,SLAM通常工作得更好,但我们的Albert机器人没有。全向轮在转弯时会滑动或打滑,以改变方向——我们没有阿克曼转向,比如车轮指向的汽车。当车轮打滑时,它是在没有转向的情况下在表面上移动,这使任何类型的轮式里程计无效,因为轮式里程计假设车轮始终在接触表面上转动。
-
SLAM不处理正在变化的平面图。Albert机器人必须处理房间内分布的玩具,这会干扰LIDAR并改变SLAM用于估计位置的平面图。当机器人捡起玩具并放回原处时,它也在改变平面图。
-
SLAM在计算上很昂贵。它需要使用传感器来开发地图,然后将实时传感器数据与地图进行比较,以定位机器人,这是一个复杂的过程。
-
如果数据模糊不清,或者没有足够的特点供机器人估计变化,SLAM就会遇到问题。我在没有特征的走廊以及高度对称的房间里也遇到过问题。
所以,为什么我要用这么多空间来谈论SLAM,尽管我并不打算教你们如何使用它?因为你们需要知道它是什么以及它是如何工作的,因为你们可能有一个需要制作地图的任务。关于SLAM有很多好的教程,但关于我接下来要教你们的,即使用AI在没有地图的情况下安全导航的内容,却非常少。
探索替代导航技术
在本节中,我们将探讨一些潜在的替代导航方法,这些方法现在我们可以用于我们的机器人,因为我们已经排除了SLAM方法:
-
我们可以随意驾驶,寻找玩具。当我们找到玩具时,机器人会捡起它,然后随意驾驶寻找玩具箱。当它看到玩具箱时,它会开到那里并放下玩具。但我们仍然需要一个避免撞到障碍物的方法。我们可以遵循一个叫做运动结构(SfM)的过程,从我们的单摄像头中获取深度信息,并使用它来制作地图。运动结构需要大量的纹理和边缘,而房屋可能没有。它还在地图中留下许多必须填充的空洞。运动结构使用视频图像中的视差来估计摄像头视场中物体的距离。这个领域已经有很多有趣的工作,我使用它创造了一些有希望的结果。视频图像必须有很多细节,这样过程才能将一个视频图像中的点与下一个视频图像中的点匹配起来。如果你对此感兴趣,可以参考以下关于SfM各种方法的调查文章:https://www.ijcit.com/archives/volume6/issue6/IJCIT060609.pdf。
-
你可能听说过一种叫做地面寻找的技术,它在其他机器人和自动驾驶汽车中得到了应用。我从Stephen Gentner在软件包RoboRealm中编写的复杂算法中学到了很多关于地面寻找的知识,这是一个用于原型设计机器人视觉系统的优秀工具。你可以在http://www.roborealm.com找到它。
这一层面寻找技术就是我们将在本章中使用的。让我们在下一节详细讨论这个问题。
介绍地面寻找技巧
我在本章中将要介绍的是一种不同于RoboRealm或其他地面寻找算法的地面寻找技巧版本,但它能实现相同的结果。让我们为了便于理解,把这个简单概念分解开来。
我们知道机器人正前方的地面是没有障碍物的。我们以机器人前方区域的视频图像像素为例,寻找更远处的相同纹理。我们正在将图像中已知为地面的部分纹理与更远处的像素进行匹配。如果纹理匹配,我们将该区域标记为绿色,以显示该区域可行驶且无障碍。我们将在本章中使用这个技巧的某些部分。顺便问一下,你注意到我说的是纹理而不是颜色了吗?我们不是匹配地面的颜色,因为地面不是单一的颜色。我在楼上的游戏室里有一块棕色地毯,它的颜色变化仍然相当大。仅仅使用颜色匹配,这种方法是行不通的。我们必须匹配纹理,这可以用颜色、强度(亮度)、色调和粗糙度(表面颜色平滑度的度量)来描述。
让我们在游戏室的地板图像上尝试一些快速实验。在实际操作中涉及几个步骤:
-
我们从相机获取的图像开始。为了加速处理并最大限度地有效利用带宽,我们将相机的原生分辨率——全分辨率为1900 x 1200——降低到仅为640 x 480。由于我们的机器人体积小,我们使用了一台小型计算机——Nvidia Jetson Nano。
-
我们将这个方法应用到我们的图像处理程序中,使用OpenCV,这是一个开源的计算机视觉库,它也被集成到了ROS中。
-
我们的第一步是使用高斯模糊函数对图像进行模糊处理。高斯模糊使用抛物线函数来减少图像中的高频信息量——通过减少相邻像素之间的差异来使图像变得模糊。为了获得足够的模糊效果,我不得不将模糊函数应用三次,使用一个5 x 5的卷积核。卷积核是一个矩阵函数——在这种情况下,一个5 x 5的数字矩阵。我们使用这个函数根据其邻居(周围的像素)来修改一个像素。这种平滑使颜色更加均匀,减少噪声,并使下一步更容易。为了模糊图像,我们从周围的像素中取一点——每边两个——并将其加到中心像素上。我们曾在第4章中讨论过卷积核。
-
我们指定机器人前方的一个区域为能够清晰看到地板的区域。我使用了一个三角形区域,但正方形区域也同样适用。我选择了三角形内发现的每种颜色,并抓取了所有具有15个单位该颜色值的像素。15个单位是什么意思?每种颜色都用一个从0到255的RGB值进行编码。我们的地毯颜色,棕色,在红、绿、蓝单位中大约是162、127和22。我们选择所有与该颜色相差15个单位的颜色,对于红色来说,是从147到177。这选择了与我们的地板颜色相似的区域。我们的墙壁是一种非常相似的棕色或米色,但幸运的是,有一个白色的踢脚板,我们可以将其隔离,这样机器人就不会试图爬上墙壁。
颜色并不是匹配我们地板上像素的唯一方式。我们还可以寻找具有相似色调(无论颜色是亮是暗,都是颜色的阴影)的像素,具有相同饱和度(颜色的暗淡或明亮程度)的像素,以及具有相同值或亮度(与在单色图像或灰度图像中匹配颜色相同的结果)的颜色。我编制了一张图表来说明这个原理:
图7.2 – 通过各种属性(如颜色、色调或饱和度)的相似性选择图像中的像素
前面的图示展示了各种选择属性(颜色、色调、饱和度和亮度)作为工具来为我们的机器人进行地面定位的能力。在这个测试中,色调属性似乎提供了最佳结果。我在另一张图片上测试了它,以确保它正在正常工作。它似乎无法将踢脚线区分开来,而踢脚线不属于安全驾驶区域。
-
我们选择所有与我们的地面颜色匹配的像素并将它们涂成绿色——或者更准确地说,我们在图像的副本中创建一个掩码区域,其中包含我们想要以某种方式指定的所有像素。例如,我们可以使用数字10。我们制作一个与我们的图像大小相同的空白缓冲区,并将该缓冲区中的所有像素都设置为10,这在另一张图像中将是地面。
对掩码数据进行膨胀操作可以帮助在这方面。可能存在一些小孔或噪声,其中一两个像素没有完全匹配我们的地毯颜色——比如说有人掉落了一块饼干。膨胀函数通过选择一个小区域(例如3 x 3)并将掩码像素设置为10,只有当所有周围的像素也是10时,来降低掩码的细节水平。这减少了掩码的边界像素,并移除了任何可能是一两个像素大的小斑点和点。你可以从图7.3中看到,我非常成功地使用一个非常坚固的掩码隔离了地面区域。鉴于我们现在知道地面在哪里,我们将我们的掩码中的其他像素涂成红色,或者某个表示那里不安全行驶的数字。让我们使用255:
图7.3 – 我版本的地面定位算法
注意,在这种情况下,它非常成功地识别了安全驾驶的区域。投射的路径是必要的,以防止机器人试图沿着墙壁行驶。如果你能识别出角落里的机器人,你将获得额外的分数。
-
我们的下一步可能需要你思考。我们需要识别安全驾驶的区域。在使用此过程时,有两种情况可能会给我们带来问题:
-
地面中间可能有一个单独的物体,比如一个玩具,它的两侧都有绿色像素。
-
我们还可能有一个凹形区域,机器人可以进入但不能出来。
在图7.3中,你可以看到算法将墙壁像素涂成了绿色,因为它们与地面的颜色相匹配。在踢脚线处有一个强烈的红色禁止区域。为了检测这两种情况,我们从机器人的位置向上投射线条,并识别我们遇到的第一个红色像素。这设定了机器人可以行驶的边界。如果你从图像底部向上追踪直到遇到红色像素,并在第一个红色像素处停止,你也会得到类似的结果。让我们再次尝试地面定位过程,但向图像中添加一些玩具,以确保我们得到想要的结果:
-
图7.4 – 向图像中添加玩具以确定我们是否将玩具检测为障碍物
这似乎效果很好。我们能够找到一条良好的道路来驾驶。记住,我们在驾驶过程中不断使用地面探测器更新障碍物视图,并更新我们的路径。这个过程有一个缺点。如果一个玩具与地毯的颜色和纹理相匹配,那么我们可能很难找到它。你可以在物体上添加一条遮蔽胶带,以便相机能够看到。
-
在这个过程中,我们可以使用的一个技巧是利用固定的相机几何形状来进行距离和尺寸估计。我们有一个“固定”的相机——它固定在机器人上,距离地面有一定高度,因此可以从像素的 y 值测量沿地面的距离。我们需要通过使用卷尺和一个盒子来仔细校准相机,将像素值与从机器人基座到障碍物所绘的相同路径线的距离相匹配。这些距离将是非线性的,并且仅在像素继续变化的距离内有效。由于相机垂直于地面,我们得到一定量的透视效果,这种效果在距离相机大约20英尺处减小到0。我的校准结果如下表:
英寸测量 顶部距离 底部距离 0 1080 0 12 715 365 24 627 453 36 598.3 481.7 48 581.5 498.5 60 571.8 508.2 72 565 515
表7.5 – 像素与英寸比较的测量表,用于确定比例
以下图像显示了在机器人相机视场中测量距离的技术。物体位于卷尺上距离机器人基座四英尺的位置。Albert在具有高清功能的网络摄像头中使用180度鱼眼镜头。我们需要在第9章中做导航时使用宽视场:
图7.5 – 确定导航相机图像中像素的比例
需要注意的一点是,机器人无法进入的狭窄通道。我们可以根据距离和像素来估计宽度。一种常见的机器人技术是在所有障碍物周围放置一个等于机器人宽度一半的边界。如果有障碍物在两侧,那么两个边界将相遇,机器人将知道它无法进入。
在下一节中,我们将创建一个卷积神经网络(CNN)来处理我们的图像,并将它们转换为机器人命令——从本质上讲,通过在视频图像中看到地标或特征来教我们的机器人如何驾驶。
实现神经网络
那么,神经网络到底做什么呢?我们使用神经网络来预测输入与输出之间的某些关联。当我们使用CNN时,我们可以将一张图片与某些期望的输出关联起来。我们在上一章中做的是将类别名称(玩具)与某些图像关联起来。但如果我们尝试将其他东西与图像关联起来呢?
怎么样?我们使用神经网络来对来自相机的图像进行分类。我们手动驾驶机器人,使用摇杆,每秒大约拍照四次。我们记录下机器人在每张图片中的动作——前进、向右转、向左转或后退。我们使用这些信息来预测给定图像的机器人运动命令。我们制作了一个CNN,以相机图像作为输入,四个输出——前进、向左或向右的命令。这有一个优点,可以自动避免固定障碍和危险。当我们到达楼梯时(记得我在游戏室里有通往下方的楼梯,那会损坏机器人),机器人会知道要转身,因为这是我们训练时所做的——我们不会故意在训练中让机器人沿着楼梯下去(对吧?)。我们通过示例教机器人如何导航房间。
你现在可能正在对着这本书大喊大叫(你应该这么做)说,“那玩具怎么办?”除非,当然,你正在跟随我的思维过程,并且自己在想,“哦,这就是我们为什么花了那么多时间谈论地面定位器!”神经网络方法将帮助我们绕过房间,避免危险和家具,但不会帮助机器人避免训练集中没有的玩具。我们无法将它们放入这个训练集中,因为玩具永远不会出现在同一个地方两次。我们将使用地面定位器来帮助避免玩具。我们如何结合两者?神经网络为机器人提供更长的目标,地面定位器则修改这个目标以避免局部、短程物体。在我们的程序中,我们首先评估神经网络,然后使用地面定位器选择一条清晰的路线。
在这个主题上,我们还将为训练我们的机器人施展另一个技巧。由于我们的地面可能会发生变化,并且可能覆盖着玩具,我们将这部分从训练图像中排除。在将图像发送到神经网络之前,我们将图像切成两半,只使用上半部分。由于我们的相机是固定并且与地面平齐的,这给了我们只有房间上半部分可用于导航。我们的图像是180度的广角,因此我们有大量的信息可以处理。这应该能让我们在任何条件下都能导航:
图7.6 – 训练机器人驾驶的数据集仅包括图像的顶部
我们第二个问题是定位玩具箱。为此,我们需要创建一个新的图像训练集,它将代表一种替代的驾驶模式。我们在各种随机的位置启动机器人,然后简单地驾驶到玩具箱。我们使用与之前导航相同的流程 – 我们正在创建一个训练集,告诉机器人如何到达玩具箱。关键是获取每个可能起始位置的良好样本。我们确实有一些休息时间 – 如果地图上的一个点(房间中的位置)已经在一条路径上,我们就不需要再次覆盖它。换句话说,所有包含在其他路径中的点都不需要重复。我们仍然希望从这两组图像中至少有1,000张图像进行训练 – 一组是探索房间,另一组是驾驶到玩具箱。
我创建了一个简单的程序,它只允许操作员用操纵杆驾驶机器人。它每秒自动拍摄一张快照。每一帧都通过查看cmd_vel
主题的值来标记,这是我们控制机器人底盘运动的方式。如果角速度Z项(angular.z
)为正,我们正在向右转。如果它是负的,我们正在向左转,如果这个项为零(你猜对了),我们正在直行。我创建了一个独立程序,它与相机一起工作,并在接收到ROS syscommand
主题上的TAKE PIC LEFT
、RIGHT
、CENTER
或BACK
命令时存储快照。这些程序将在本书的GitHub仓库中提供 – 我不会在这里包括它们。我们将每个类别的图片放在它自己的子目录中。
你可以将神经网络想象成这样工作:
-
我们将图像展示给神经网络。
-
它从该图像中提取特征,然后选择训练数据库中最像提供的图像特征的图像。
-
训练数据库中的每张图片都与一个驾驶命令(左、中、右)相关联。因此,如果图像最接近机器人向左转的图像,那么网络将返回向左转。
现在让我们更详细地看看这些流程。
处理图像
现在,在我们将数据呈现给神经网络进行训练之前,我们需要进行几个步骤。机器人上的相机分辨率远远超出了网络所需的,我们希望使用神经网络中尽可能少的数据:
图7.7 – CNN的图像处理
让我们分解这个流程,使其更清晰:
-
前面的图中的第一幅图像代表我们的原始图像。
-
我们的第一步是将图像下采样到640 x 480。我们将图像切成两半,只保留上半部分,这样就消除了地板的考虑。我们将矩形图像调整大小为244 x 244,这是我们的神经网络处理的一个合适大小。
-
我们将图像转换为灰度,这样我们只有一个通道需要处理,使用这个公式(由国家电视标准委员会(NTSC)提出):
灰度 = 0.299 * R + 0.587 * G + 0.114 * B
-
我们下一步是对图像进行直方图均衡化,以获取所有可能值的整个范围。摄像头的原始输出既没有纯白色(
255
),也没有纯黑色(0
)。最低值可能是53
,最高值是180
,范围是127
。我们通过减去低值(53
)并乘以缩放因子(127
/255
)来缩放灰度值。这扩大了图像的范围到全尺度,并消除了可能存在的许多光照和照明变化。我们试图向神经网络呈现一致的数据。 -
下一步是对数据进行高斯模糊处理。我们希望减少图像中的一些高频数据,以平滑一些边缘。这是一个可选步骤,可能对您的环境不是必需的。我在机器人的视野中有很多细节,我觉得模糊会给我们更好的结果。它还填补了上一步中直方图均衡化过程留下的部分灰度值空缺。
-
我们必须对数据进行归一化,将范围从
0-255
缩小到0-1
。这是为了满足人工神经网络的输入要求。为了执行此操作,我们只需将每个像素除以255
。我们还必须将数据从OpenCV图像格式转换为NumPy数组。所有这些都是CNN预处理的一部分。 -
我们的网络是一个九层的CNN。我使用这种常见的架构,因为它是对LeNet的变体,LeNet被广泛用于此类任务(http://vision.stanford.edu/cs598_spring07/papers/Lecun98.pdf)。然而,在我们的最终步骤中,我们不会使用由二元分类器确定的二元输出,而是使用具有四个输出的Softmax分类器 – 前进、左转或右转。如果我们想的话,我们实际上可以创建更多的类别,并且有容易的右转和困难的右转,而不仅仅是单一级别的转向。我没有使用机器人新全向轮的完整功能,以保持这个问题简单。请记住,输出类别的数量必须与我们的训练集标签完全匹配。
在我们的卷积神经网络(CNN)中,前六个层是成对的卷积神经网络,层间有最大池化层。这使得网络能够处理图像中逐渐增大的细节。最后的两个层是完全连接的,并使用修正线性单元(ReLU)激活。请记住,ReLU只取来自其他层的正值。这是我们的最终层,它是一个具有四个输出的Softmax分类器:
图7.8 – 我们神经网络的组织结构
就像任何其他神经网络训练任务一样,过程中的下一步是将输入数据分成训练集和验证集。让我们学习如何训练神经网络。
训练用于导航的神经网络
我们将使用80%的数据进行训练,20%进行验证。我们真的不能使用通过随机旋转重复图像来美化数据的过程,就像我们在玩具识别程序中所做的那样,因为我们不仅是在识别图像,而且是在使用它们进行方向导航。改变旋转会搞乱我们的方向。
现在,让我们把我们的训练程序组合起来。这个程序部分受到了Adrian Rosebrock的pyImageSearch博客以及Sullivan和Lawson在海军研究实验室发表的论文Deep Obstacle Avoidance的启发。你可以按照以下步骤进行:
- 我们需要通过驾驶机器人并记录我们的驾驶动作来收集我们的训练数据。这把我们的数据分成了三个集合——左转、右转和直行。我们的训练图像有三个子文件夹来匹配我们的标签。我们读取数据,将其与标签关联,并预处理数据以供神经网络使用。
注意
我在我的台式计算机上运行训练,而不是在Jetson Nano上。稍后我们将使用完全训练好的网络在Jetson Nano上部署。
-
这里列出了我们这个程序需要的导入项——相当多:
# import the necessary packages from keras.preprocessing.image import ImageDataGenerator from keras.optimizers import Adam from sklearn.model_selection import train_test_split from keras.preprocessing.image import img_to_array from keras.utils import to_categorical import matplotlib.pyplot as plt import numpy as np import cv2 import os from keras.models import Sequential from keras.layers.convolutional import Conv2D from keras.layers.convolutional import MaxPooling2D from keras.layers.core import Activation from keras.layers.core import Flatten from keras.layers.core import Dense from keras import backend as K
-
这是CNN的设置:
-
我们有三个卷积层,每个卷积层后面跟着一个
maxpooling
层。记住,每个maxpooling
层都会将网络考虑的图像分辨率减半,也就是数据量的1/4,因为我们把宽度和高度都减半了。卷积层使用ReLU激活函数,因为我们不希望有任何负像素值。 -
在卷积层之后,我们有两个各有500个神经元的全连接层。
-
最后一层是我们的三个神经元输出层,包含一个Softmax分类器,它将输出每个分类(左、右和中心)的百分比。输出将看起来像
(0.8, 0.15, 0.05)
,三个数字加起来等于1。
这是一个通用的卷积网络类,可以用于其他事物,因为它是一个通用的多类图像分类CNN:
class ConvNet(): @staticmethod def create(width, height, depth, classes): # initialize the network network = Sequential() inputShape = (height, width, depth) # first set of CONV => RELU => POOL layers network.add(Conv2D(50, (10, 10), padding="same", input_shape=inputShape)) network.add(Activation("relu")) network.add(MaxPooling2D(pool_size=(2, 2), strides=(2, 2))) # second set of CONV => RELU => POOL layers network.add(Conv2D(50, (5, 5), padding="same")) network.add(Activation("relu")) network.add(MaxPooling2D(pool_size=(2, 2), strides=(2, 2))) # third set of CONV => RELU => POOL layers network.add(Conv2D(50, (5, 5), padding="same")) network.add(Activation("relu")) network.add(MaxPooling2D(pool_size=(2, 2), strides=(2, 2))) # Fully connected ReLU layers network.add(Flatten()) network.add(Dense(500)) network.add(Activation("relu")) network.add(Dense(500)) network.add(Activation("relu")) # softmax classifier network.add(Dense(classes)) network.add(Activation("softmax")) # return the constructed network architecture return network
-
-
现在,我们设置我们的学习计划。我们将运行25次训练,学习率为0.001。我们设置每个批次的图像数量为32张,如果我们内存不足,我们可以减少这个数量:
EPOCHS = 25 LEARN_RATE = 1e-3 BATCH = 32 # batch size - modify if you run out of memory
-
下一个部分加载所有我们的图像。我们在这里设置了图像所在的路径。我们将三种类型的训练图像放在名为
left
、right
和center
的文件夹中:print ("Loading Images") images=[] labels=[] #location of your images imgPath = "c:\users\fxgovers\documents\book\chapter7\train\" imageDirs=["left","right","center"] for imgDir in imageDirs: fullPath = imgPath + imgDir # find all the images in this directory allFileNames = os.listdir(fullPath) ifiles=[] label = imgDirs.index(imgDir) # use the integer version of the label # 0= left, 1 = right, 2 = center for fname in allFileNames: if ".jpg" in fname: ifiles.append(fname)
-
现在,你可以参考我关于我们将要经历的图像预处理过程的图(图7**.7)。我们将把图像切成两半,只处理图像的上半部分。然后,我们将图像缩小到244 x 244以适应神经网络,神经网络需要正方形图像。由于我们不需要考虑颜色,只需形状,我们将图像转换为灰度(黑白)。这将进一步减少我们的数据。我们将对图像进行均衡化,这会将灰度颜色的范围重新缩放到0到255,从而平衡光照并设置对比度:
# process all of the images for ifname in ifiles: # load the image, pre-process it, and store it in the data list image = cv2.imread(ifname) # let's get the image to a known size regardless of what was collected image = cv2.resize(image, (800, 600)) halfImage = 800*300 # half the pixels # cut the image in half -we take the top half image = image[0:halfimage] #size the image to what we want to put into the neural network image=cv2.resize(image,(224,224)) # convert to grayscale image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) #equalize the image to use the full range from 0 to 255 # this gets rid of a lot of illumination variation image = cv2.equalizeHist(image)
-
接下来,我们有高斯模糊。这是一个可选项——如果你的房间没有很多细节,你可能想移除它。我的游戏室有很多家具,所以我认为减少噪声会提高性能:
# gaussian blur the image to remove high frequency noise # we use a 5x kernel image = cv2.GaussianBlur(img,(5,5),0)
-
我们将图像转换为浮点数的NumPy数组,范围从0到1,而不是从0到255的整数集。这个神经网络工具包只允许NumPy数组作为输入。我们还把与标签相关的数字(左=
0
,右=1
,中心=2
)放入匹配的labels
NumPy数组中:# convert to a numpy array image = img_to_array(image) # normalize the data to be from 0 to 1 image2 = np.array(image, dtype="float") / 255.0 images=images.append(image) labels.append(label) labels = np.array(labels) # convert to array
-
我们将数据分成两部分——一个用于训练神经网络的训练集和一个用于验证训练集的测试集。我们将使用80%的图像样本进行训练,20%进行测试:
# split data into testing data and training data 80/20 (trainData, testData, trainLabel, testLabel) = train_test_split(data, labels, test_size=0.20, random_state=42)
-
我们必须将标签转换为张量,这仅仅是一种特定的数据格式:
# convert the labels from integers to vectors trainLabel = to_categorical(trainLabel, num_classes=3) testLabel = to_categorical(testLabel, num_classes=3)
-
现在,我们通过实例化
ConvNet
对象来构建我们的实际神经网络,这个对象实际上在Keras中构建我们的CNN。我们设置了优化器,它是自适应动量估计(ADAM),一种自适应梯度下降法。ADAM像带有摩擦的重球一样作用于误差梯度——它有一定的动量,但不会快速加速:# initialize the artificial neural network print("compiling CNN...") cnn = ConvNet.build(width=224, height=224, depth=1, classes=3) opt = Adam(lr=LEARN_RATE, decay=LEARN_RATE / EPOCHS) model.compile(loss="categorical_crossentropy", optimizer=opt, metrics=["accuracy"])
-
我们在这一步训练网络。这需要相当长的时间才能完成——从15分钟到一两个小时——具体取决于你有多少图像。我们希望训练结果至少达到80%。如果没有,可以增加一些周期来观察学习曲线何时趋于平稳。如果这仍然不起作用,你需要更多的训练图像。我目标是每个集合有1,000张图像,这大约需要50分钟的机器人驾驶时间:
# train the network print("Training network. This will take a while") trainedNetwork = model.fit_generator(aug.flow(trainImage, trainLabel, batch_size=BATCH), validation_data=(testImage, testLable), steps_per_epoch=len(trainImage) // BATCH, epochs=EPOCHS, verbose=1) # save the model to disk print("Writing network to disk") cnn.save("nav_model")
-
现在我们已经完成了,所以我们将创建的模型保存到磁盘上,以便我们可以将其传输到机器人的电脑,即Nvidia Jetson Nano。
-
现在,制作你的第二个驾驶训练集,从随机位置到玩具箱。选择随机位置,并使用摇杆将机器人驾驶到玩具箱。继续这样做,直到你有大约1,000张图像。将这些图像通过训练程序运行,并通过更改程序的最后一行将此模型标记为
toybox_model
:cnn.save("toybox_model")
这太棒了——我们已经构建并训练了我们的神经网络。现在,我们需要将其用于驾驶机器人,我们将在下一节中这样做。
CNN机器人控制实现
我们需要结合一个发送ROS命令的程序和我们的神经网络分类过程。我通过ROS的syscommand
主题添加了一些命令,我使用这个主题向我的机器人发送非周期性命令。syscommand
只是发布一个字符串,所以你可以用它来做几乎所有的事情。你可以按照以下步骤进行:
-
我们从ROS、OpenCV2和Keras开始导入,因为我们将会结合这三个库中的函数:
import roslib import sys import rospy import cv2 from std_msgs.msg import String from sensor_msgs.msg import Image from geometry_msgs.msg import Twist from cv_bridge import CvBridge, CvBridgeError from keras.preprocessing.image import img_to_array from keras.models import load_model import numpy as np
-
这一部分是ROS接口。我喜欢以这种方式封装ROS接口,将所有的发布和订阅放在一个地方。我们需要设置几个主题——我们需要能够在
syscommand
主题上发送和接收命令。我们将向机器人的电机在cmd_vel
主题上发布命令。我们从image_topic
主题接收来自摄像头的图像。我们使用回调来处理在机器人上其他地方发布的主题事件。这些可以在任何时间调用。当我们向主题发布时,我们拥有更多的控制权,这通过pubTwist
和pubCmd
方法来处理。我添加了标志来接收命令和图像,这样我们就不小心重复处理相同的图像或命令两次:class ROSIF(): def init (self): self.bridge = CvBridge() self.image_sub = rospy.Subscriber("image_topic",Image,self.callback) self.cmd_sub = rospy.Subscriber( "syscommand",String,self.cmdCallback) self.cmd_pub = rospy.Publisher( "syscommand",String,queue_size=10) self.twist_pub = rospy.Publisher("cmd_vel",Twist,queue_size=10) self.newImage = False self.cmdReceived="" def callback(self): try: self.image = self.bridge.imgmsg_to_cv2(data, "bgr8") self.newImage = True except CvBridgeError as e: print(e) def cmdCallback(self,data): # receieve a message on syscommand self.cmdReceived = data.data def getCmd(self): cmd = self.cmdReceived self.cmdReceived = "" # clear the command so we dont do it twice return cmd
-
下一个函数是程序其余部分获取摄像头系统最新图像的手段,该图像在ROS上发布在
image_topic
。我们获取最新图像并将newImage
变量设置为False
,这样我们就知道下次是否正在尝试连续两次处理相同的图像。每次我们获取到新图像时,我们将newImage
设置为True
,每次我们使用图像时,我们将newImage
设置为False
:def getImage(self): if self.newImage=True: self.newImage = False # reset the flag so we don't process twice return self.image self.newImage = False # we send back a list with zero elements img = [] return img
-
这一部分向机器人发送速度命令,以匹配CNN预测的我们应执行的操作。CNN的输出是三个值之一:左转、右转或直行。这些作为神经网络中的三个枚举值之一——
0
、1
或2
输出。我们将它们转换回左转、右转和中心值,然后使用这些信息向机器人发送运动命令。机器人使用Twist
消息发送电机命令。Twist
数据消息旨在适应非常复杂的机器人、四旋翼飞行器和全向轮驱动系统,这些系统可以朝任何方向移动,因此它有很多额外的值。我们发送一个Twist.linear.x
命令来设置机器人的前后速度,以及一个Twist.angular.z
值来设置底座的旋转或转向。在我们的情况下,正的angular.z
旋转值向右转,负值向左转。我们的最后一条语句将数据值作为Twist
消息发布到cmd_vel
主题:# publishing commands back to the robot def pubCmd(self,cmdstr): self.cmd_pub.publish(String(cmdstr)): def pubTwist(self,cmd): if cmd == 0: # turn left turn = -2 speed = 1 if cmd==1: turn = 2 speed = 1 if cmd ==3: turn=0 speed = 1 # all stop if cmd==4: turn = 0 speed = 0 cmdTwist = Twist() cmdTwist.linear.x = speed cmdTwist.angular.z = turn self.twist_pub.publish(cmdTwist)
-
我们创建一个函数,通过一个命令来完成所有的图像处理。这正是我们为训练程序预处理图像的方式——正如你可能想象的那样。你可能觉得我先将图像放大,然后再缩小,这有点奇怪。这样做的原因是为了在图像的垂直部分有细节。如果我将它缩小到 240 x 240,然后将其切成两半,我会在之后拉伸像素以再次使其成为正方形。我喜欢在缩小时有额外的像素。这种技术的最大优点是,它不关心输入图像的分辨率——我们最终会得到正确尺寸和裁剪的图像。
其他步骤包括将图像转换为灰度,对对比度范围进行均衡,这会扩展我们的颜色值以填充可用空间,并执行高斯模糊以减少噪声。我们通过将我们的整数 0-255 灰度值转换为 0 到 1 的浮点值来对图像进行归一化,以便神经网络使用:
def processImage(img): # need to process the image image = cv2.resize(image, (640, 480)) halfImage = 640*240 # half the pixels # cut the image in half -we take the top half image = image[0:halfimage] #size the image to what we want to put into the neural network image=cv2.resize(image,(224,224)) # convert to grayscale image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) #equalize the image to use the ful image = cv2.equalizeHist(image) # gaussian blur the image to remove high freqency noise # we use a 5x kernel image = cv2.GaussianBlur(img,(5,5),0) # convert to a numpy array image = img_to_array(image) # normalize the data to be from 0 to 1 image2 = np.array(image, dtype="float") / 255.0 return image2
-
现在我们已经设置好了,我们进入主程序。我们必须初始化我们的 ROS 节点,这样我们才能与 ROS 发布/订阅系统通信。我们创建一个变量,mode,我们用它来控制要进入哪个处理分支。我们创建一个接口,允许操作员打开和关闭导航功能,并在正常导航和我们的玩具箱寻找模式之间进行选择。
在本节中,我们将加载我们之前训练的两个神经网络模型:
# MAIN PROGRAM ic = image_converter() rosif = ROSIF() rospy.init_node('ROS_cnn_nav') mode = "OFF" # load the model for regular navigation navModel = load_model("nav_model") toyboxModel = load_model("toybox_model")
-
本节开始处理循环,该循环在程序运行时执行。运行
rospy.spin()
告诉 ROS 系统处理任何可能等待我们的消息。我们的最后一步是暂停程序 0.02 秒,以便让 Raspberry Pi 处理其他数据并运行其他程序:while not rospy.is_shutdown(): rospy.spin() time.sleep(0.02)
因此,我们的导航章节到此结束。我们介绍了使用神经网络教机器人通过天花板上的地标来驾驶,以及如何避开障碍物和进行房间导航——而且无需地图。
摘要
本章介绍了一些在非结构化环境中进行机器人导航的概念,也就是说,在现实世界中,机器人的设计者无法控制空间的内容。我们首先介绍了 SLAM,以及基于地图导航的一些优缺点。我们讨论了 Roomba 如何通过随机交互和统计模型进行导航。我们为玩具收集机器人项目 Albert 选择的算法结合了两种主要依赖视觉传感器的算法。
第一项是 Floor Finder,这是一种我在 DARPA 大挑战获胜者使用时学到的技术。Floor Finder 算法使用近视(靠近机器人)来教远视(远离机器人)地板的纹理。然后我们可以将房间分成可以驾驶的安全区域和不可驾驶的区域。这解决了我们的避障问题。我们的导航技术使用训练好的神经网络通过将房间从地平线向上的图像(房间的上半部分)与行驶方向关联起来,来识别房间内的路径。这也教会了机器人远离楼梯。我们因为玩具在那里,所以从图像中排除了房间的下半部分,并使用相同的过程训练另一个神经网络来找到玩具箱。
这个过程与我们看到的 第 4 章 中的过程相同,但训练图像都被标记了从该点到玩具箱的路径。这种组合使我们能够通过视觉教会机器人找到路径,而且不需要地图,就像你做的那样。
在下一章中,我们将介绍对象分类,并回顾一些其他路径规划方法。
问题
-
关于 SLAM,最常用的传感器是什么,用于创建 SLAM 制作地图所需的数据?
-
为什么当有轮式里程计数据可用时,SLAM 工作得更好?
-
在 Floor Finder 算法中,高斯模糊函数是如何改善结果的?
-
Floor Finder 的最后一步是从机器人位置向上追踪到第一个红色像素。这个步骤还可以通过什么其他方式完成(参考 图 7**.3)?
-
为什么我们在进行神经网络处理之前要将图像水平切半?
-
使用神经网络方法相比 SLAM 技术有什么优势?
-
如果我们只用随机驾驶函数而不是神经网络,我们需要为机器人添加什么新的程序或函数才能达到相同的结果?
-
我们在章节中提出的方法是如何避免楼梯的?你认为这足够吗?你会建议其他完成这项任务的方法吗?
进一步阅读
-
《深度避障》,作者 Sullivan 和 Lawson,由海军研究实验室,Rosebrock,Adrian 出版。
-
《使用 Python 的人工智能食谱》,作者 Ben Auffarth,Packt Publishing,2020年
-
《使用 Python 的人工智能 - 第二版》,作者 Prateek Joshi,Packt Publishing,2020年
-
《Python 图像处理食谱》,作者 Sandipan Dey,Packt Publishing,2020年
第八章:放置物品
假设你必须去奶奶家,根据传说,奶奶家在山丘之上,穿过树林,距离两个州。如果你住在欧洲,那将是两个国家之外。为了规划你的旅行,你可以从以下两种方式之一开始。忽略谷歌已经从今天的年轻人那里夺走了大部分的地图阅读和导航技能的事实,你会拿出地图并做以下之一:
-
从你家出发,尝试找到最接近奶奶家直线的道路
-
从奶奶家出发,尝试找到通往你家的道路
从任何方向出发,你会发现你寻求的道路或路径会分叉、交叉、改变、蜿蜒,甚至可能走到死胡同。此外,并非所有道路都同等重要——有些道路更宽,限速更高,而有些道路则更窄,有更多的停车标志。最终,你将通过一系列决策来选择路线,以实现最低的成本。这种成本可能是以时间来衡量的——到达那里需要多长时间。它可能是以距离来衡量的——需要覆盖多少英里。或者它可能是以金钱来衡量的——存在一条收费道路,需要额外收费。
在本章中,我们将讨论几种解决涉及选择一系列决策的问题的方法,其中有一些度量标准——例如成本——可以帮助我们选择哪种组合可能是最好的。这里有很多信息在机器人学中得到了广泛应用,我们将超越我们的玩具抓取机器人,看看机器人的路径规划和一般决策。这些是任何机器人实践者必备的技能,因此它们被包含在这里。本章涵盖了人工智能(AI)决策过程的基础,其中问题可以用分类问题(确定这种情况是否属于一个或多个类似情况的一组)或回归问题(拟合或近似一个可以是曲线或路径的函数)来描述。最后,我们将应用两种方法来解决我们的机器人问题——专家系统和随机森林。
本章将涵盖以下主题:
-
决策树和随机森林
-
路径规划、网格搜索和A*(A星)算法
-
使用D*(D星)技术进行动态规划
-
专家系统和知识库
初看,我们将在本节中涵盖的概念——即路径规划、决策树、随机森林、网格搜索和GPS路线查找——除了都是人工智能中使用的计算机算法的一部分之外,并没有太多共同之处。从我的观点来看,它们基本上是相同的概念,并以相同的方式处理问题。
技术要求
我们在本章中使用的工具,你应该已经在之前的章节中安装过——scikit-learn (http://scikit-learn.org/stable/developers/advanced_installation.html)。
或者,如果你有Python的pip
安装程序,你可以使用以下命令安装它:
pip install –U scikit-learn
你可以在https://github.com/PacktPublishing/Artificial-Intelligence-for-Robotics-2e找到本章的代码。
任务分析
我们在本章的任务可能是如果你从第3章开始就一直在关注进度,你可能会期待的任务。在第3章中,我们讨论了我们的故事板。我们需要在轮子上在房间里导航,找到通往目的地的路径,无论是捡起玩具还是开车去玩具箱。
为了实现这一点,我们将使用决策树、分类(一种无监督学习)、鱼骨图,这对于故障排除很有用,最后是路径规划。
介绍决策树
决策树的概念相当简单。你正在人行道上行走,来到一个拐角。在这里,你可以向右转,左转,或者直行。那是你的决策。做出决策后 – 向左转 – 你现在面临的不同决策比如果向右转要多。每个决策都会创建通往其他决策的路径。
当我们沿着人行道行走时,我们心中有一个目标。我们并不是漫无目的地闲逛;我们试图达到某个目标。一个或多个决策组合将帮助我们达到目标。假设目标是去杂货店买面包。可能有四到五条人行道可以通往商店,但每条路径可能长度不同或路径不同。如果一条路径要上坡,那可能比走平路更难。另一条路径可能需要你在红绿灯处等待,这会浪费时间。我们为这些属性中的每一个分配一个值,通常我们希望选择成本最低或奖励最高的路径,具体取决于问题。
在下面的决策树中,我们可以分解机器人捡起玩具的动作。我们首先查看玩具的宽高比(我们在第4章中检测到的边界框的长度与宽度)。我们根据玩具最窄的部分调整机器人手臂的腕部。然后,我们尝试用那个腕部位置捡起玩具。如果我们成功,我们就把玩具从地上拿起来,拿到玩具箱里。如果我们失败,我们尝试另一个位置。尝试了所有位置后,我们继续到下一个玩具,并试图稍后从这个玩具的角度回来,希望是从不同的角度。你可以看到这样分解我们的动作是有用的,结果证明决策树对很多事情都很有用,正如我们将在本章中看到的:
图8.1 – 如何捡起玩具的简单决策树
决策树类型问题的一般问题在于指数级增长。让我们以棋局为例,这是人工智能的一个热门问题集。我们有一个开局的20种选择(8个兵和2个骑士,每种都有2种可能的移动)。这20种移动中的每一种都有20种可能的后续移动,以此类推。所以第一步有20种选择,第二步有400种选择。第三步有197,281种选择!当我们试图提前规划时,很快就会有一个非常大的决策树。我们可以这样说,这些可能的决策是分支,做出决策后我们所处的状态是叶子,整个概念结构是一个决策树。
备注
与决策树一起工作的秘诀是无情地剪枝,这样你考虑的决策尽可能少。
处理决策树有两种方法(实际上,有三种——在我解释之前,你能猜到第三种吗):
-
第一种方法是从小到大开始,向外工作到你的目标。你可能会走到死胡同,这意味着回溯或可能重新开始。我们将称之为正向链(链,因为我们正在树中从叶子到叶子制作一个链接路径)。
-
另一种方法是先从目标开始,向上工作到树的起点。这是反向链。反向链的酷之处在于,要穿越的分支要少得多。你可以猜到反向链的一个主要问题是,你必须提前知道所有叶子,才能使用它们。在许多问题中,例如网格搜索或路径规划器,这是可能的。在棋类游戏中,由于树的大小呈指数级增长,这是不可行的。
-
第三种技术?没有人说我们不能两者兼得——我们可以结合正向和反向链,在中间某个地方相遇。
决策树形状的选择、链技术以及构建是基于以下考虑的:
-
可用的数据是什么?
-
已知或未知的信息是什么?路径是如何评分或评级的?
使用决策树进行路径规划也有不同的解决方案。如果你拥有无限资源,最大的计算机,提前获得完美的知识,并且愿意等待,那么你可以生成最优路径或解决方案。
我从多年开发基于人工智能的实用机器人和无人车辆中学到的一个教训是,任何满足所有标准或目标的解决方案都是可接受和可用的解决方案,你不必等待并继续计算完美或最优的解决方案。通常情况下,一个“足够好”的解决方案可以在最优解决方案的1/10甚至1/100的时间内找到,因为最优解决方案需要穷举搜索,可能需要考虑所有可能的路径和组合。
那么,我们如何着手使我们的决策树运行得更快,或更有效率?我们做任何好的园丁都会做的事情——开始修剪我们的树。
我们所说的剪枝是什么意思?
有时候在计算机行业中,我们必须使用隐喻来帮助解释事物的工作原理。你可能还记得苹果公司,后来是Windows,采用的桌面隐喻来帮助解释图形操作系统。有时候,我们只是把这些隐喻用滥了,比如垃圾桶用来删除文件,或者Clippy,那个纸夹助手。
当我讨论剪枝你的决策树时,你可能觉得我已经进入了隐喻的深渊。接下来是什么,肥料和树桩?实际上,剪枝是决策树类型系统中一个关键的概念。你树上的每个分支都可能引导成百上千个子分支。如果你能尽早决定一个分支没有用,你可以将其剪掉,你就不必处理该分支或该分支中的任何分支或叶子。你越早发现一条路径无法带你达到目标,你就越快可以减少创建解决方案所需的时间和精力,这对于实时系统来说非常重要,比如机器人、自动驾驶汽车或自主飞机;这可能是可用和毫无价值之间的区别。
让我们快速通过一个例子来了解一下我们如何使用剪枝方法。决策树过程的一个很好的用途是故障检测、隔离和恢复(FDIR)。这是机器人的典型功能。让我们为我们的Tinman机器人无法移动的情况制作一个FDIR的决策树。我们可以采取哪些自动化步骤来检测故障、隔离问题,然后恢复?我们可以使用的一种技术是根本原因分析,我们通过系统地列出并消除(剪枝)导致因素,然后看症状是否匹配来试图找出我们的问题。进行根本原因分析的一种方法是通过一种特殊的决策树形式,称为鱼骨图,或石川图。这张图是以其发明者,东京大学的教授石川馨的名字命名的。在他的1968年论文《质量控制指南》中,鱼骨图因其形状而得名,它有一个中央脊柱和两侧突出的肋骨。我知道,当我们有一个像鱼一样的决策树时,这些隐喻变得越来越深。
现在,我们开始遇到问题。记住,在机器人中,问题是一个症状,而不是原因。我们的问题是机器人无法移动。什么可以导致这个问题?让我们列一个清单:
-
驱动系统
-
软件
-
通信系统
-
电池和电线
-
传感器
-
操作员错误
现在,对于每一个分支,我们将它们细分为更小的分支。哪些部分的驱动系统可能导致机器人无法移动?轮子可能卡住了。电机可能没有得到电源。齿轮可能卡住了。电机驱动器可能过热了。以下是我的鱼骨图,用以说明机器人无法移动的问题:
图8.2 – 鱼骨图或石川图常用于故障排除
对于这些因素中的每一个,你可以考虑如果这个问题是原因,那么它的症状会是什么。如果电机中的齿轮卡住了,那么电机无法转动,车轮也无法转动。如果我们能够检查掉这些因素中的任何一个,我们就可以从我们的图或决策树中剪枝或消除齿轮。我们检查了齿轮,用手转动车轮和电机,所以齿轮不是原因。我们剪掉了那个分支。如果我们有一个自动化的测试方法,我们可以自动剪枝分支,我们将在本章后面的例子中做到这一点。
那电池呢?电池可能需要充电(电池耗尽),电池可能被断开,或者电源线可能松动。我们检查电池电压 – 那是好的,所以从树上剪掉那个叶子。我们检查线路 – 没有松动。电池分支被剪掉了。
我们就这样继续下去,直到我们得到一个要么匹配所有症状,要么是最后一个剩下的东西。假设最后一个分支是通信。现在怎么办?我们问,“通信中的哪些事情会导致我们无法移动?”我们的第一个答案是电机命令消息没有通过网络到达我们的机器人。我们检查日志,确实没有电机消息(在我们的例子中是cmd_vel
)。这就是我们的问题,但是什么导致了这个问题?网络可能坏了(检查过 – 没有,网络是好的),或者IP地址可能错了(没有,那没关系)。我们查看是否对控制软件进行了任何最近的变化,确实有。我们恢复到之前的版本,看到机器人移动了。这就是我们的问题,我们使用了决策树来找到它。
因此,在这种情况下,我们几乎完全通过剪枝树上的分支和叶子来解决问题,直到只剩下一条路径,或者我们达到了目标。
我们如何在软件中剪枝分支?我们可以寻找死胡同。死胡同是叶子 – 树的结束部分,没有未来的分支。当我们到达死胡同时,我们不仅可以剪掉那个叶子,还可以剪掉那些唯一导致那个分支的路径部分。这将是一种反向链剪枝方法,因为我们从终点开始,向后工作。
我们还可以看到树中未使用或从未被引用或调用的部分。我们可以以这种方式删除整个部分。这是正向链,因为我们正在正向遍历树,从前面到后面。
到目前为止,我们,故事中的人类,一直是手动制作这些决策树的。我们甚至还没有讨论如何编写一个程序,让机器人能够使用树来做出决策。如果计算机能够代替我们完成所有艰难的树制作、分支决定和节点标记的工作,那岂不是更好?这正是我们将在下一节中讨论的内容。
创建自我分类的决策树
让我们考虑一下对玩具进行分类的问题。我们可能想要发明一个更高效的机器人,它以某种方式对玩具进行分类,而不是仅仅将它们扔进一个盒子里。在一个理想的世界里,从20个玩具的群体中,我们会有一些特征将群体均匀地分成两半——10个和10个。让我们假设它是长度——一半的玩具长度小于六英寸,另一半的玩具长度大于六英寸。那么,如果另一个特征将这10个组中的每一个都再分成一半——分成四个组,每组五个,那就更理想了。
让我们假设它是颜色——我们有五种红色玩具,五种蓝色玩具,五种绿色玩具和五种黄色玩具。你可能已经注意到我们在做生物学家在课堂上对新的物种进行分类时所做的事情——我们正在创建一个分类法。现在,我们选择另一个属性,将玩具分成更小的组——这可能是玩具的类型或轮子的大小。我想你应该明白了这个道理。让我们来看一个例子。
现在,如果我们能够将所有玩具和所有属性列在一个表中,并让计算机找出有多少组以及它们是什么类型,那就太好了。我们可以创建一个像这样的表:
类型 | 长度 | 宽度 | 重量 | 颜色 | 轮子数量 | 噪音 | 软硬 | 材料 | 眼睛 | 玩具名称 |
---|---|---|---|---|---|---|---|---|---|---|
汽车 | 3 | 1 | 35 | 红色 | 4 | 0 | 硬 | 金属 | 0 | 热轮车 |
汽车 | 3 | 1 | 35 | 橙色 | 4 | 0 | 硬 | 金属 | 0 | 热轮车 |
汽车 | 3 | 1 | 35 | 蓝色 | 4 | 0 | 硬 | 金属 | 0 | 热轮车 |
汽车 | 3 | 1 | 35 | 蓝色 | 4 | 0 | 硬 | 金属 | 0 | 热轮车 |
汽车 | 3 | 1 | 35 | 白色 | 4 | 0 | 硬 | 金属 | 0 | 热轮车 |
填充玩具 | 5 | 5 | 50 | 白色 | 0 | 0 | 非常软 | 毛绒 | 2 | 毛绒玩具 |
填充玩具 | 7 | 5 | 55 | 棕色 | 0 | 0 | 非常软 | 毛绒 | 3 | 毛绒玩具 |
动作 | 2 | 4 | 80 | 灰色 | 0 | 0 | 硬 | 金属 | 0 | 柔软的 |
制作 | 2 | 2 | 125 | 木材 | 0 | 0 | 硬 | 木材 | 0 | 2x2积木 |
制作 | 2 | 2 | 75 | 木材 | 0 | 0 | 硬 | 木材 | 0 | 木材积木三角形 |
制作 | 4 | 2 | 250 | 木材 | 0 | 0 | 硬 | 木材 | 0 | 4x2积木 |
餐具 | 3 | 3 | 79 | 蓝色 | 0 | 0 | 硬 | 陶瓷 | 0 | 茶壶 |
飞机 | 7 | 5 | 65 | 白色 | 4 | 1 | 硬 | 塑料 | 0 | 太空穿梭机 |
飞机 | 13 | 7 | 500 | 绿色 | 8 | 1 | 硬 | 塑料 | 0 | 雷鸟2号 |
汽车 | 5 | 1 | 333 | 黄色 | 6 | 1 | 硬 | 金属 | 0 | 校车 |
音乐 | 12 | 4 | 130 | 木材 | 0 | 2 | 硬 | 木材 | 0 | 玩具吉他 |
音乐 | 5 | 2 | 100 | 黄色 | 0 | 1 | 硬 | 塑料 | 0 | 演唱麦克风 |
音乐 | 4 | 4 | 189 | 白色 | 0 | 2 | 硬 | 木材 | 0 | 玩具鼓 |
表8.1 – 用于分类的一组玩具的属性表
我们现在有一个问题需要解决。我们将使用 scikit-learn
Python 包中提供的决策树分类器 DecisionTreeClassifier
。这个程序不能使用字符串作为输入数据。我们必须将所有字符串数据转换成某种数值。幸运的是,scikit-learn
库为我们提供了一个专门为此目的的函数。它提供了几个编码函数,可以将字符串转换为数字。我们将使用的函数称为 LabelEncoder
。这个函数接受一个字符串数组并将其转换为一系列整数。
我们可以取我们的第一列,其中包含玩具的类型。我的命名法是 toy = toy car,stuffed = stuffed animal,aircraft = toy aircraft,music = toy musical instrument。我们还有 action 代表 action toy,build 代表 building toy(即积木、LEGO™ 等)。我们得把这些转换成某种数字。
LabelEncoder
将将我们的数据表中填充有字符串的列转换为数值。数据中的 type
列如下代码所示:
['car' 'car' 'car' 'car' 'car' 'stuffed' 'stuffed' 'action' 'build' 'build' 'build' 'dish' 'aircraft' 'aircraft' 'car' 'music' 'music' 'music']
它将其转换为标签编码的玩具类型:
[3 3 3 3 3 6 6 0 2 2 2 4 1 1 3 5 5 5]
您可以看到,所有提到 car
的地方现在都变成了数字 3
。您还可以看到 6
代表 stuffed
,0
代表 action
,等等。为什么数字这么奇怪?编码器首先按字母顺序对字符串进行排序。
我们将从这里直接开始创建一个分类程序:
-
下面是我们的决策树分类器程序:
# decision tree classifier # author: Francis X Govers III # # example from book "Artificial Intelligence for Robotics" #
-
我们首先导入我们将要使用的库。有一个额外的库称为
graphviz
,它对于绘制决策树图像很有用。您可以使用以下命令安装它:pandas package, which provides a lot of data table-handling tools:
from sklearn import tree
import numpy as np
import pandas as pd
import sklearn.preprocessing as preproc
import graphviz
-
我们的第一步是读取我们的数据。我在 Microsoft Excel 中创建了我的表格,并将其导出为 逗号分隔值(CSV)格式。这允许我们直接读取数据文件并带有列标题。我打印出数据文件的大小和形状以供参考。我的文件版本有 18 行和 11 列。最后一列只是我对每个玩具实际名称的备注。我们不会使用最后一列进行任何操作。我们正在构建一个分类器,该分类器将根据类型对玩具进行分类:
toyData = pd.read_csv("toy_classifier_tree.csv") print ("Data length ",len(toyData)) print ("Data Shape ",toyData.shape)
-
现在,我们可以开始构建我们的决策树分类器。我们首先创建
DecisionTreeClassifier
对象的一个实例。有两种不同的 决策树分类(DTC)算法可供选择:-
基尼系数:基尼系数是在1912年由意大利统计学家科拉多·基尼在他的论文《Variabilita e Mutabilita》中提出的。这个系数或指数衡量一组数字中的不平等程度。零值表示组内的所有成员都是相同的。
-
熵方法:当我们谈论AI时,熵指的是一组数据中的不确定性量。这个概念来自信息理论,其中它衡量随机变量的不确定性量。这个概念是在20世纪40年代由克劳德·香农提出的。为了创建决策树,算法试图通过在某个点上分割组来减少熵(减少不确定性),使得每个子节点比其父节点更同质。
-
在这里,我们将使用基尼系数。如果我们有一组所有玩具车大小相同且都是红色的玩具车,那么该组的基尼系数将是0。如果组内的成员都不同,那么基尼系数将更接近1。基尼系数由以下方程给出:
G(S) = 1− ∑ i=1 n p i 2
我们有4辆玩具车,共18个玩具,所以玩具车在组中的概率是4/18或0.222。决策树将继续细分类别,直到组的基尼系数为0:
dTree = tree.DecisionTreeClassifier(criterion ="gini")
-
我们需要将数据表中的值分离出来。第一列的数据,在Python中称为列
0
,是我们的分类标签。我们需要单独提取这些标签,因为它们用于将玩具分类。从我们之前使用神经网络的工作中,这些将是我们输出或我们在其他机器学习过程中使用的标签数据。我们将训练我们的分类器,根据表中的属性(大小、重量、颜色等)来预测玩具的类别。我们使用切片来从pandas表中提取数据。我们的pandas数据表称为toyData
。如果我们想要表中的条目,我们需要请求toyData.values
,这将返回一个二维数组:dataValues=toyData.values[:,1:10] classValues = toyData.values[:,0]
如果你不太熟悉Python中的切片表示法,语句toyData.values[:,1:10]
返回表中从1到10的列——它省略了列0。实际上,我们的表中确实有11列,但由于Python从0开始编号,我们最终需要1到10。你可能猜到另一种表示法只是抓取第一列的数据。
-
这就是我们之前提到的标签编码器——它将把我们的数据中的字符串转换为数字。例如,颜色如红色、绿色和蓝色将被转换为数字如0、1和2。要编码的第一个项目是我们用来标记数据的类别值列表。我们使用
LabelEncoder.fit()
函数来得出将字符串转换为数字的公式,然后使用LabelEncoder.transform()
函数来应用它。请注意,fit()
不会产生输出。 -
最后,我们需要使字符串文本和编码数字列表相匹配。
LabelEncoder
会按字母顺序对字符串进行排序,并从 A 开始编号,忽略任何重复项。如果我们输入car, car, car, block, stuffed, airplane
,我们将得到2,2,2,1,3,0
作为编码,并且我们需要知道airplane
=0
,block
=1
,car
=2
,和stuffed
=3
。我们需要生成一个airplane, block, car, stuffed
。我们通过在我们的字符串格式类别名列表上使用两个函数来复制LabelEncoder
函数:-
我们使用
set()
函数来消除重复项 -
我们使用
sorted()
函数以正确的顺序排序
-
现在,我们的类别名表和由 LabelEncoder
生成的枚举相匹配。我们稍后会用到这个:
lencoder = preproc.LabelEncoder() lencoder.fit(classValues)
classes = lencoder.transform(classValues)
classValues = list(sorted(set(classValues)))
-
为了方便起见,我创建了一个函数来自动找出我们的数据中哪些列由字符串组成,并将这些列转换为数字。我们首先构建一个空列表来存储我们的数据。我们将遍历我们的数据列,查看第一个数据值是否为字符串。如果是,我们将使用我们创建的标签编码器对象(
lencoder
)将整个列转换为数字。标签编码过程有两个部分。我们调用lencoder.fit()
来查看我们的列中有多少唯一的字符串,并为每个创建一个数字。然后,我们使用lencoder.transpose
将这些数字插入到列表中:newData = [] for ii in range(len(dataValues[0])) line = dataValues[:,ii] if type(line[0])==str: lencoder.fit(line) line = lencoder.transform(line)
-
现在,我们将所有数据放回
newData
列表中,但有一个问题——我们已经将所有列转换成了行!我们使用numpy
的transpose
函数来纠正这个问题。但是等等!我们不再有数组了,因为我们将其转换成了列表,以便将其拆分并重新组合(你无法用numpy
数组这样做——相信我,我试过了):newData.append(line) newDataArray = np.asarray(newData) newDataArray = np.transpose(newDataArray)
-
现在,所有的预处理都完成了,所以我们最终可以调用真正的
DecisionTreeClassifier
。它需要两个参数:-
我们数据值数组
-
我们想要决策树将我们的组划分成类别的类别类型数组
-
DecisionTreeClassifier
将确定表格中哪些具体数据对预测我们的玩具属于哪个类别是有用的:
dTree = dTree.fit(newDataArray,classes)
就这样——一行。但是等等——我们想看到结果。如果我们只是尝试打印决策树,我们会得到以下内容:
DecisionTreeClassifier(class_weight=None, criterion='gini', max_depth=None, max_features=None, max_leaf_nodes=None,
min_impurity_split=1e-07, min_samples_leaf=1, min_samples_split=2, min_weight_fraction_leaf=0.0, presort=False, random_state=None, splitter='best')
这并没有告诉我们任何东西;这是对 DecisionTreeClassifier
对象的描述(它确实显示了我们可以设置的参数,这就是为什么我把它放在这里)。
-
因此,我们使用一个名为
graphviz
的包,它非常擅长打印决策树。我们甚至可以将我们的列名和类别名传递给图形。最后两行将图形输出为.pdf
文件并存储在硬盘上:c_data=tree.export_graphviz(dTree,out_file=None,feature_names=toyData.colum ns, class_names=classValues, filled = True, rounded=True,special_characters=True) graph = graphviz.Source(c_data) graph.render("toy_graph_gini")
这里是结果。我要提醒你,这可能会上瘾:
图 8.3 – 使用 Gini 指数方法的决策树输出
我们可以通过查看我们的输入表并检查数字是否对齐来快速检查我们的解决方案。我们应该看到以下内容:
-
五辆玩具车
-
三个积木
-
一个盘子
-
一个动作玩具
-
两个填充动物
-
三种乐器
-
两个玩具飞机
确实如此。
另一个需要关注的数字是基尼指数。如图8.3所示,顶级框显示整个组的指数整体值为0.8166
,接近1,表明高度异质性。随着我们向下推进树,基尼指数越来越小,直到在每个识别的组中达到0
,这表明那些组中的项目具有所有相同的属性。
这个图表告诉我们什么?首先,我们可以通过仅一个属性来区分玩具车——宽度。只有玩具车的宽度小于1.5英寸(38毫米)。我们不需要看颜色、重量或宽度以外的任何东西来将所有玩具车与其他所有东西分开。我们看到我们有5辆玩具车在我们的18个玩具中,所以我们还有13个需要分类。我们的下一个划分是长度。我们有7个玩具长度小于4.5英寸(11厘米),5个更长。在五个玩具中,有两个有眼睛,三个没有。有眼睛的玩具是两个填充动物。如果你跟随树,通向玩具乐器的分支是宽度>1.5英寸、长度>4.5英寸且没有眼睛,它们在长度和宽度上确实比其他玩具大,并且没有眼睛。
在分类方面,其他任何信息都不重要。这意味着像颜色这样的属性是玩具所属类别的糟糕预测因子——这是有道理的。我们其他有用的标准是轮子数量、重量和长度。这些数据足以将所有玩具分类到不同的组中。你可以看到每个叶节点的基尼指数确实是0
。我在图表中添加了一些额外的标签,以使说明更清晰,因为程序在图表中使用的是类别编号而不是类别名称。
因此,那次练习是令人满意的——我们能够从我们的玩具数据中创建一个自动决策树,将我们的玩具分类。我们甚至可以使用这些数据来分类一个新的玩具,并预测它可能属于哪个类别。如果我们发现那个新玩具在分类上有所违反,那么我们就需要重新运行分类过程并创建一个新的决策表。
创建决策树并将数据细分为类别的另一种类型的过程被称为熵模型,或信息增益。让我们接下来讨论这个话题。
理解熵
熵是衡量提供的数据样本中无序程度的度量。我们也可以称这个过程为信息增益,因为我们正在衡量每个标准对了解它属于哪个类别的知识贡献了多少。
熵的公式是一个以2为底的对数函数的负值,它仍然主要关注一个类别属于一个群体的概率,这仅仅是属于每个类别的个体数除以样本总数:
熵 = -plog2(p) – p_ilog2(p_i)
为了在我们的程序中将熵作为我们的组标准,我们只需要更改一行:
dTree = tree.DecisionTreeClassifier(criterion ="entropy")
结果在以下图表中显示:
图8.4 – 使用熵(信息增益)的决策树输出
你可以注意到,熵从我们整个组的2.55开始,并减少到叶节点(分支的末端)的0。我们可以检查我们有七个分类,但你可以看到熵方法选择了与基尼方法不同的标准。例如,基尼分类器从长度
开始,而熵分类器从材料
开始。熵方法还选择了噪音
(玩具是否发出噪音)并正确地选择了只有玩具乐器和玩具飞机发出噪音,这些玩具都有电子音箱发出飞机声音。
然而,有一件事引起了一些关注。有两个显示材料
的块,将玩具的值分为小于2.5的材料。材料
是一个离散值。我们可以生成一个材料列表,并通过sorted(set(list))
过程运行它,以获取排序后的唯一值:
['陶瓷', '毛皮', '金属', '塑料', '木材']
因此,材料
值为2.5或更小的是陶瓷或毛皮。毛皮和陶瓷除了在字母表中的位置外没有共同之处。这是一种相当令人不安的关系,这是由于我们将数据编码为一系列数字的顺序集所导致的。这暗示了实际上并不存在的关联和分组。我们如何纠正这个问题?
实际上,有一个处理此类问题的过程。这种技术在AI程序中广泛使用,并且是处理分类的必备工具,无论是决策树部分还是神经网络。这个工具有一个奇怪的名称,叫做独热编码。
实现独热编码
独热编码的概念相当简单。我们不是用枚举来替换一个类别,而是为每个可能的值在我们的数据中添加一列,并根据该值将其设置为1
或0
。这个名字来源于事实,即在这个集合中只有一个列是热或被选中的。
我们可以将这个原则应用到我们的例子中。我们可以将数据库中每种材料类型的单列材料
替换为五个列:陶瓷
、毛皮
、金属
、塑料
和木材
:
材料 | 陶瓷 | 毛皮 | 金属 | 塑料 | 木材 |
---|---|---|---|---|---|
金属 | 0 | 0 | 1 | 0 | 0 |
金属 | 0 | 0 | 1 | 0 | 0 |
金属 | 0 | 0 | 1 | 0 | 0 |
金属 | 0 | 0 | 1 | 0 | 0 |
金属 | 0 | 0 | 1 | 0 | 0 |
毛皮 | 0 | 1 | 0 | 0 | 0 |
毛皮 | 0 | 1 | 0 | 0 | 0 |
金属 | 0 | 0 | 1 | 0 | 0 |
木头 | 0 | 0 | 0 | 0 | 1 |
木头 | 0 | 0 | 0 | 0 | 1 |
木头 | 0 | 0 | 0 | 0 | 1 |
陶瓷 | 1 | 0 | 0 | 0 | 0 |
塑料 | 0 | 0 | 0 | 1 | 0 |
塑料 | 0 | 0 | 0 | 1 | 0 |
金属 | 0 | 0 | 1 | 0 | 0 |
木头 | 0 | 0 | 0 | 0 | 1 |
塑料 | 0 | 0 | 0 | 1 | 0 |
木头 | 0 | 0 | 0 | 0 | 1 |
表 8.2 – 材料类别的单热编码数据结构
这确实给我们的程序带来了一些结构上的复杂性。我们必须为每种类型插入列,这用 14 个新列替换了 3 个列。
我找到了两个函数可以将文本类别转换为单热编码的多列:
-
OneHotEncoder
是scikit-learn
的一部分,它使用方式类似于LabelEncoder
—— 事实上,你必须同时使用这两个函数。你必须使用LabelEncoder
将字符串数据转换为数值形式,然后应用OneHotEncoder
将其转换为所需的单比特值形式。 -
简单的方法是使用 pandas 的一个名为
get_dummies()
的函数。这个名字显然是因为我们正在创建虚拟值来用数字替换字符串。它确实执行了相同的功能。涉及的步骤比使用OneHotEncoder
流程简单得多,所以我们将使用示例中的这个方法。
让我们看看我们需要遵循的步骤来实现这一点:
-
顶部标题部分与之前相同 —— 我们有相同的导入:
# decision tree classifier # with One Hot Encoding and Gini criteria # # Author: Francis X Govers III # # Example from book "Artificial Intelligence for Robotics" # from sklearn import tree import numpy as np import pandas as pd import sklearn.preprocessing as preproc import graphviz
-
我们将像之前一样开始读取表格。我在我的末端添加了一个额外的列,称为
Toy Name
,这样我就可以跟踪哪个玩具是哪个。我们不需要这个列来进行决策树,所以我们可以使用 pandas 的del
函数通过指定要删除的列名来移除它:toyData = pd.read_csv("toy_classifier_tree.csv") del toyData["Toy Name"] # we don't need this for now
-
现在,我们将创建一个列表,列出我们将从 pandas
dataTable
中删除并替换的列。这些是Color
、Soft
和Material
列。我使用术语 Soft 来标识那些柔软且容易压扁的玩具(与硬塑料或金属相比),因为这是我们可能需要用于我们的机器人手的一个单独标准。我们生成虚拟值,并用 18 个新列替换这 3 个列。pandas 会自动将列名命名为旧列名和值的组合。例如,单个Color
列被替换为Color_white
、Color_blue
、Color_green
等等:textCols = ['Color','Soft','Material'] toyData = pd.get_dummies(toyData,columns=textCols)
-
我在这里添加了一个
print
语句,只是为了检查所有内容是否正确组装。这是可选的。我对 pandas 在数据表方面的能力印象深刻 —— 它有很多功能可以执行数据库类型的功能和数据分析:print toyData
-
现在,我们已经准备好生成我们的决策树。我们实例化对象并命名为
dTree
,将分类标准设置为 Gini。然后我们从toyData
数据框中提取数据值,并将第一个(0号)列中的类别值放入classValues
变量中,使用数组切片运算符:dTree = tree.DecisionTreeClassifier(criterion ="gini") dataValues=toyData.values[:,1:] classValues = toyData.values[:,0]
-
我们仍然需要使用
LabelEncoder
将类别名称转换为枚举类型,就像我们在前两个例子中所做的那样。我们不需要进行独热编码。每个类别代表我们的分类示例的终端状态——决策树上的叶子。如果我们进行神经网络分类器,这些将是我们的输出神经元。一个很大的不同之处在于,当使用决策树时,计算机会告诉你它用来分类和分离项目的标准。而在神经网络中,它会进行分类,但你无法知道使用了什么标准:lencoder = preproc.LabelEncoder() lencoder.fit(classValues) classes = lencoder.transform(classValues)
-
正如我们所说的,为了在最终输出中使用类别值名称,我们必须消除任何重复的名称并按字母顺序排序。这对嵌套函数做到了这一点:
classValues = list(sorted(set(classValues)))
-
这就是我们的程序结论。实际上创建决策树只需要一行代码,因为我们已经设置了所有数据。我们使用之前相同的步骤,然后使用
graphviz
创建图形并将其保存为PDF。这并不难——现在我们已经有了所有这些设置经验:print "" dTree = dTree.fit(dataValues,classes) c_data=tree.export_graphviz(dTree,out_file=None,feature_names=toyData.columns, class_names=classValues, filled = True, rounded=True,special_characters=True) graph = graphviz.Source(c_data) graph.render("toy_decision_tree_graph_oneHot_gini")
结果是以下图中所示的流程图。这种使用独热编码的输出比 图8**.4 更容易阅读,因为我们可以看到每个类别的数字。你会注意到每个叶子(终端节点)只有一个类别和一个计数(两个填充动物和三个乐器):
图8.5 – 使用独热编码的决策树输出更容易阅读
由于我们已经能够描述和制作各种决策树,如果我们使用一大堆决策树,会发生什么呢?一个森林!让我们探索这可能会是什么样子。
随机森林
我真的很想添加关于 随机森林分类器 的这一部分,但不仅仅是因为这个名字听起来很酷。虽然我可能被指责过度夸张了隐喻,但这次,这个名字可能启发了这种决策树过程的名称。我们已经学会了如何制作决策树,我们也了解到它们有一些弱点。如果数据确实属于不同的和区分开的组,那就最好不过了。它们对数据中的噪声不太容忍。如果你想要扩展它们,它们会变得非常难以控制——你可以想象一下,如果我们要处理200个类别而不是我们之前处理的6或7个类别,这个图会变得有多大。
如果你想要利用决策树的简单性和实用性,但想要处理更多的数据、更多的不确定性和更多的类别,你可以使用随机森林,正如其名称所表明的,它只是一大批随机生成的决策树。让我们一步一步地通过这个过程:
-
我们收集了我们的信息数据库,但我们的数据库中不是18行,而是有10,000条记录或1,000,000条记录。我们将这些数据细分为随机集合——我们生成100个数据集合,每个集合都是随机地从我们所有的数据中选择的——并将它们随机排序。我们还抽取出一组数据作为测试集,就像我们为神经网络所做的那样。
-
现在,对于每一组随机数据,我们使用我们已经学过的相同过程来制作决策树。
-
现在,我们有了这100个分类引擎的集合,每个引擎都是从不同的、随机生成的数据子集中生成的。我们现在通过从测试集中取数据并运行我们森林中的所有100棵树来测试我们的随机森林。每一棵树都会为我们测试记录中的数据分类提供一个估计。如果我们仍在分类玩具,那么其中一棵树可能会估计我们正在描述一辆玩具车。另一棵可能认为它是一件乐器。我们接受每个估计并将其视为一票。然后,多数决定——多数树选择的类别就是赢家。这就是全部。
设置和程序与之前所做的完全相同,但你不能从随机森林中绘制决策树,或者仅仅创建一个树作为最终目标,因为那不是随机森林的作用——如果你只需要决策树,你知道如何做。你可以做的是像神经网络一样使用随机森林,作为分类引擎(这些数据属于哪个类别?)或近似非线性曲线的回归引擎。
在这一点上,你可以和我一起得出结论,决策树在许多事情上都非常有用。但你是否知道你可以用它们来导航?下一节将介绍机器人的路径规划——使用不同类型的决策树。
介绍机器人路径规划
在本节中,我们将应用决策树技术来执行机器人导航。有些人喜欢将这些称为基于图的解决方案,但任何类型的导航问题最终都会变成一个决策树。考虑一下当你开车时,你是否可以将你的导航问题分解成一系列决策——向右转,向左转,还是直行?
我们将把到目前为止所学的内容应用到与分类相关的问题上,那就是网格搜索和路径查找。我们将学习著名的、广泛使用的A(发音为A-star)算法。这将从网格导航方法开始,如拓扑路径查找,例如GPS路线查找,最后是专家系统。你会看到这些都是我们已经学习过的决策树主题的版本和变体。
一些问题和数据集,尤其是在机器人领域,适合使用基于网格的解决方案,作为简化导航问题的方法。如果我们试图为机器人规划绕房子或穿过田野的路径,将地面划分为某种棋盘格网格,并使用它来绘制机器人可以驾驶到的坐标,这是非常有意义的。我们可以使用纬度和经度,或者我们可以选择某个参考点作为零点——比如我们的起始位置——并相对于机器人测量一些矩形网格。网格在棋盘游戏中也起到相同的作用,限制考虑的潜在未来移动的位置数量,并限制和划定了我们在空间中的可能路径。
虽然本节讨论的是网格路径查找,无论是否涉及地图,都有一些机器人导航范例不使用地图,甚至有些不使用网格,或者使用间距不均匀的网格。我设计过具有多层地图的机器人导航系统,其中一些层是可变的——可更改的——而另一些则不是。这是一个充满想象力和实验的肥沃土壤,如果你对这个主题感兴趣,我建议进行进一步的研究。现在,让我们先描述我们将要使用的坐标系。
理解坐标系
让我们回到手头的主题。我们有一个大致呈矩形的机器人和房间,在这个矩形内部也有一些大致呈矩形的障碍物,如家具、椅子、书架、壁炉等。考虑我们用网格来表示这个空间,并创建一个与物理房间相对应的虚拟房间的数字数组,这是一个简单的概念。我们将网格间距设置为1厘米——每个网格方格是1厘米 x 1厘米,给我们一个580 x 490个方格的网格,或284,200个方格。我们在机器人的记忆中用二维数组中的无符号整数来表示每个方格。
现在,我们需要一些其他数据。我们有一个起点和一个目标点,指定为网格坐标。我们将把网格的0,0
放在房间最近和最左边的角落,这样我们的所有方向和角度都将为正。在我为你绘制的房间地图图8.6中,那个角落将始终是地图的左下角。在标准的右手定则表示法中,左转是正角度,右转是负角度。x方向是水平方向,y方向是垂直方向。对于机器人来说,x轴是右侧,y轴是运动方向。
你可能会觉得我提供这些细节很奇怪,但设置正确的坐标系是进行网格搜索和路径规划的第一步。我们在室内使用笛卡尔坐标系。在户外,我们会使用不同的规则,使用纬度和经度。在那里,我们可能希望使用北东下(北是正的,南是负的,东是正的,西是负的,z轴向下,x轴与机器人的行驶方向对齐):
图8.6 – 地球导航和室内导航的坐标系
我们将在稍后更详细地查看这个房间地图。
因此,我们有一个网格和一个我们同意的坐标系,或者至少我们同意我们都理解。我们还有一个起点和一个终点。我们的目标是确定机器人从起点到终点的最佳路径。在中间,我们必须规划一条绕过可能挡道的任何障碍物的路径。
接下来,我们必须谈谈知识。
基于我们的知识开发地图
基本上有两种类型的网格搜索和路径查找程序:
-
先验知识,你知道地图上的一切
-
事后知识,你不知道障碍物在哪里
我们将从更容易的位置开始,在那里我们可以利用对房间布局的完美知识来进行路径规划——我们有一张地图。
我们实际上有三个目标,我们试图在路径规划中同时实现:
-
达到我们的目标
-
避开障碍物
-
选择最短路径
我们可以谈谈我们可能如何进行。我们可以从起点开始,用铅笔画一条从起点到目标的想象中的线。如果没有障碍物挡道,我们就完成了。但是等等——我们的铅笔在纸上只是一条细线。我们的机器人则相对较胖——它在行驶过程中有一个显著的宽度。我们如何判断机器人是否正在通过一条它无法进入的狭窄通道?我们需要修改我们的地图!
我们有自己的网格,或者是一张代表网格的纸张。我们可以在网格上绘制所有障碍物的轮廓,并按比例绘制。我们有两张椅子、两张桌子、一个壁炉、两个脚凳和四个书架。我们用最深的黑色将所有障碍物都涂上颜色。现在,我们拿一支浅色的铅笔——比如说蓝色——围绕所有机器人宽度一半的家具画一个轮廓。我们的机器人宽度是32厘米,所以一半是16厘米,这是一个很好的偶数。我们的网格是每平方厘米1厘米,所以我们围绕所有东西画一个16平方单位的边界。它看起来像这样:
图8.7 – 为障碍物添加安全边界有助于防止碰撞
因此,现在我们的地图有两种颜色——障碍物和禁止边界。我们将保持机器人的中心不在禁止区域内,这样我们就不会撞到任何东西。这应该是有意义的。至于判断通道和门,如果禁止区域在两侧接触(也就是说,中间没有剩下的白色方格),那么机器人太大,无法通过。你可以在插图右上角的脚凳周围看到这一点。
现在我们来看这条线。我们需要一种方法来编写一个计算机算法,该算法确定机器人可以穿过的白色方格,从而从起点到达终点。
由于我们有了目标在笛卡尔坐标系中的位置,并且我们有我们的起点,我们可以用一条直线从起点到终点的距离来表示。如果起点是x1, y1
,终点是x2, y2
,那么距离是两点之间差值的平方和的平方根:
距离 = sqrt((x2-x1)^2 + (y2-y1)^2)
开发路径规划算法的一种方法是用波前法。我们知道起点在哪里。我们向八个相邻的方格方向延伸。如果其中任何一个遇到障碍物或禁止区域,我们就将其排除在可能的路径之外。我们记录我们如何到达每个方格,在我的插图(图8.8)中,这由箭头表示。我们使用我们如何到达方格的信息,因为我们还不知道我们接下来要去哪里。现在,我们拿所有新的方格并再次做同样的事情——选择一个方格,看看它的八个邻居中哪个是合法的移动,然后在其中放一个箭头(或指向前一个方格位置的指针)以记录我们是如何到达那里的。我们继续这样做,直到我们到达目标。我们记录我们检查方格的顺序,并跟随箭头向后到达我们的起点。
如果有多个方格都有通往当前方格的路径,那么我们选择最近的那个,也就是说,最短的路径。我们跟随这些前驱者一直回到起点,这就是我们的路径:
图8.8 – 波前法路径规划涉及很少的数学计算。每个图都是过程的一个步骤,从左上角开始,横向移动,然后向下
你会注意到在这个例子中,我允许机器人进行对角转向以从一个方格移动到另一个方格。我也可以指定只允许直角转向,但这不是很高效,而且对机器人的驱动系统来说很困难。只允许直角转向在某种程度上简化了处理,因为您只需要考虑一个方格周围的四个邻居,而不是八个。
另一种开发看起来有希望路径规划算法的方法是贪婪最佳优先方法。与我们在波前方法中记录并检查所有网格点不同,我们只需保留我们刚刚测试的八个方格中的最佳路径方格。我们用来决定保留哪个方格的度量标准是离我们的直线路径最近的那个。另一种说法是,它是离目标最近的那个方格。我们当然会移除被障碍物阻挡的方格。最终结果是,我们考虑的方格比波前法路径规划少得多:
图8.9 – 被恰当地命名为“贪婪最佳优先”的算法速度快,但可能会陷入困境
贪婪技术适用于所有情况吗?实际上并不适用。
为什么不呢?这似乎是一个简单的算法,我们只考虑合法的移动。问题是它无法处理局部最小值。什么是局部最小值?它是在地图上机器人必须后退才能找到好路径的地方。最容易可视化的最小值类型是U形区域,机器人可以进入但不能退出。贪婪最佳优先算法也不是试图找到最短路径,而只是找到一个有效路径:
图8.10 – 当不存在直线路径时,可能会出现“局部最小值”,机器人将不得不后退或改变方向
如果我们要找到最短路径,我们需要做一些额外的数学计算。
一种更系统和数学的方法来处理网格搜索问题中绕过障碍物找到最短路径是A*算法,最初是为Shakey机器人开发的。
介绍A*算法
实话实说,如果不提及A算法,你真的无法写出一本关于机器人的书。A算法的起源可以追溯到1968年斯坦福大学的Shakey机器人。这是最早的地图导航机器人之一。Nils Nilsson和他的团队试图找到一种方法来让Shakey在斯坦福的大厅中导航,并开始尝试不同的算法。第一个算法被称为A1,第二个A2,以此类推。经过多次迭代,团队决定技术组合的效果最好。在计算机科学中,A*表示字母A后面跟着任何其他字符,因此A-star就这样命名了。
A*过程的概念与我们之前使用其他路径规划器所做的是非常相似的。像波前规划器一样,我们首先考虑起点的邻居。我们将根据两个因素计算每个方块的一个估计值:从起点到该方块的距离和到目标的直线距离。我们将使用这些因素来找到累积成本最低的路径。我们通过将路径中每个网格方块的价值相加来计算这个成本。公式如下:
F(n) = g(n) + h(n)
在这里,F(n)指的是这个方块对路径成本的贡献,g(n)代表从该方块到起点沿所选路径的距离(即路径成本的总和),而h(n)是从该方块到目标的直线距离,这是一个启发式或对剩余距离的估计。由于我们不知道之后我们还要绕过哪些障碍,我们使用这个猜测作为比较路径的衡量标准。
这个值代表如果这个方块是最终路径的一部分,它的成本或贡献是多少。我们将选择成本最低的方块作为路径的一部分。与波前规划器一样,我们跟踪前驱方块或在这个方块之前走过的方块,以重建我们的路径:
图8.11 – A*计算使用从起点(G)到目标(H)的距离
上述图示说明了A算法。每个方块都是基于从起点沿路径的距离(G)和到目标的剩余距离的估计(H*)来评估的。黄色方块代表迄今为止选定的路径。
让我们通过实例说明A*算法是如何工作的:
-
我们保留一个集合,包含我们在地图上计算值的所有网格方块。我们将这个称为
exploredMap
。我们的地图网格方块对象看起来是这样的:# globals mapLength = 1280 mapWidth = 1200 mapSize = mapLength*mapWidth map = []
-
现在,我们将地图填充为零以初始化一切。我们将在代码中稍后定义
mapGridSquare
函数——它创建我们的数据结构:for ii in range(0, mapWidth): for jj in range(0,mapLength): mapSq = mapGridSquare()#defined later mapSq.position = [ii,jj] mapSq.sType =EMPTY
-
下一个部分将在地图上创建所有障碍。我们将放置要填充或使其不可通行的网格方块的位置:
# create obstacles obstacles = [[1,1],[1,2],[1,3],[45,18],[32,15] …..[1000,233]] # iterate through obstacles and mark on the map for pos in obstacles: map[pos]. sType = OBSTACLE pathGrid = []
-
现在,我们声明我们的起始和结束位置:
START = [322, 128] GOAL = [938,523] exploredMap = [] A_Star_navigation(start, goal, exploredMap, map)
-
在本节中,我们正在创建我们的数据结构来跟踪我们做出的所有计算。
G
值是从起点计算出的距离,H
值是到目标的估计距离。F
只是这两个值的总和。我们还创建了一个函数来计算这些值:def mapGridSquare(): def __init__(self): self.F_value = 0.0 #total of G and H self.G_value = 0.0 # distance to start self.H_value = 0.0 # distance to goal self.position=[0,0] # grid location x and y self. predecessor =None # pointer to previous square self.sType = PATH def compute(self, goal, start): self.G_value = distance(goal.position,self.position) self.H_value = distance(start.position,self.position self.F_value = self.G_value + self.H_value return self.F_value
-
一旦完成地图计算,我们需要一个函数来追踪从目标到起点的路径。这个函数被称为
reconstructPath
:def reconstructPath(current): totalPath=[current] done=False while not done: a_square = current.predecessor if a_square == None: # at start position? done = True totalPath.append(a_square) current = a_square return totalPath
-
我们创建了一个
findMin
函数来定位我们探索过的网格块中具有最低F
分数的那个:def findMin(map): minmap = [] for square in map: if minmap == []: minmap = square continue if square.F_value < minmap.F_value: minmap = square return minmap
-
然后,我们创建
navigation
函数本身:def A_Star_navigation(start, goal, exploredMap, map): while len(exploredMap>0): current = findMin(exploredMap) if current.position == goal.position: # we are done – we are at the goal return reconstructPath(current) neighbors = getNeighbors(current)
-
neighbors
函数返回当前方块所有未标记为障碍物的相邻方块:for a_square in neighbors: if a_square.predecessor == None:
-
我们只计算每个网格方块一次:
old_score = a_square.F_value score = a_square.compute(GOAL, START)
-
现在,我们寻找具有最低
G
值的方块——即离起点最近的那个:if a_square.G_value < current.G_value: a_square.predecessor = current current = a_square current.compute(GOAL, START) exploredMap.append(current)
因此,在本节中,我们介绍了在已知所有障碍物位置的情况下,在地图上找到最短路径的 A* 方法。但如果我们不知道呢?我们可以使用的另一种方法是 D* 算法。
介绍 D(D-星或动态 A)算法
在本章的前面部分,我谈到了 先验 知识。尽管 A* 算法非常有用,但它要求在地图上预先知道所有障碍物。如果我们计划进入一个未知空间,我们将边走边创建地图,我们该怎么办?如果我们有一个带有传感器(如声纳或激光雷达)的机器人,那么机器人将在移动过程中检测和识别障碍物。因此,它必须根据不断增多的信息不断重新规划其路线。
A* 过程只运行一次,在机器人开始移动之前为机器人规划路线。D*,一个动态重新规划过程,会随着新信息的出现不断更新机器人的路径。
D* 算法通过向每个网格方块添加一些附加信息来允许重新规划。你可能会记得,在 A* 中,我们有 G
值(路径上到起点的距离)和 H
值(到目标的直线距离)。D-star 在方块上添加了一个可以具有几个可能值的标签:
-
方块的标签可能是
NEW
,表示一个以前从未被探索过的新方块。 -
它可能是
OPEN
,表示已评估并作为路径一部分考虑的标签。 -
CLOSED
是指已被从考虑中删除的方块。 -
下两个标签是
RAISED
和LOWERED
。如果传感器的读数或附加信息导致该方块的代价增加,则设置RAISED
标志,而LOWERED
则相反。对于LOWERED
方块,我们需要将新的路径代价传播到现在代价较低的方块相邻的方块,以便它们可以重新评估。这可能会导致相邻方块上的标签发生变化。RAISED
方块的代价增加,因此可能从路径中删除,而LOWERED
方块的代价减少,可能被添加到路径中。
注意
请记住,当路径回溯到起点时,成本值的变化会在 D* 的路径评估中像波浪一样传播。
D* 和 A* 之间的另一个主要区别在于,D* 从目标开始,逆向工作到起点。这使得 D* 能够知道到达目标的确切成本——它使用从当前位置到目标的实际路径距离,而不是像 A* 那样使用启发式或距离估计。
这是个提醒你的时候,我们刚刚覆盖的所有这些网格搜索技术仍然是决策树的变体。我们是从叶子到叶子——我们称之为网格方块,但它们仍然是决策树的叶子。我们设定了一些标准来选择走哪条路径,这形成了分支路径。我们在每种情况下都朝着某个目标或终点工作。我提这一点是因为,在下一节中,我们将结合决策树和从 A* 和 D* 算法中学到的路径规划类型,以使用 GPS 在街道上找到一条路径。
GPS 路径查找
我想有机会(既然我们已经走到这一步)简单谈谈拓扑路径规划器。这是前几节中使用的基于网格技术的替代方法。有些问题和导航类型不适合基于网格的方法,或者可能需要天文数字般的大量详细数据,而这些数据可能在小机器人中不可用或不实用。
例如,我想谈谈你的汽车中的 GPS 如何在街道上找到一条路线到达目的地。你一定想知道那个小盒子在其微小的“大脑”中如何拥有足够的信息来提供从一地到另一地的逐个转弯指示。如果你停下来思考,你可能想象 GPS 正在使用你在 LCD 屏幕上查看的同一张地图来确定你需要去哪里。你也会认为发生了一种基于网格的搜索,就像我们之前详细讨论的 A* 算法一样。但你会错的。
GPS 用于规划路线的数据看起来根本不像地图。相反,它是一个 拓扑网络,显示了街道是如何相互连接的。在格式上,它看起来更像是一个矢量数据库(具有方向和大小或距离),而不是由像素组成的 X, Y 网格栅格地图。数据库格式在 GPS 内部存储中也占用更少的空间。街道被 节点 或道路交叉或改变的地方分隔。每个节点显示哪些街道是相连的。节点通过 链接 相连,允许您从节点到节点遍历数据。链接代表道路,并具有长度,以及关于道路质量的成本数据。成本数据用于计算路线的吸引力。限速高速公路的成本会很低,而有很多停车标志的小巷或土路成本会很高,因为该链接既不太受欢迎,速度也较慢。
大多数 GPS 路径规划器使用的算法被称为 迪杰斯特拉算法(Dijkstra’s algorithm),以荷兰的埃德加·W·迪杰斯特拉(Edsger W. Dijkstra)的名字命名。他在 1956 年想要找到从鹿特丹到格罗宁根的最短路径。他的基于图的解决方案经受住了时间的考验,并且非常普遍地用于 GPS 路由。对我们这个机器人来说,这并没有任何帮助,所以你可以自己研究一下。
我们使用与在网格图上执行 A* 过程相同的程序来处理 GPS 路网数据库。我们评估每个节点,并从起始节点向外扩展,选择最接近目的地方向的路径:
图 8.12 – 基于道路的网络可以表示为一系列节点(圆圈)和链接(线条)
许多 GPS 系统也会同时尝试从终点——目标或目的地——反向链,并试图在中间某处相遇。为了使我们的 GPS 系统小巧、轻便和可靠,投入了惊人的工作量。当然,它们依赖于数据库中的最新信息。
摘要
嗯,这已经是一个非常繁忙的章节了。我们讨论了决策树在多种应用中的用途。基本决策树有叶子(节点)和链接,或分支,每个都代表一个决策或路径上的变化。我们学习了鱼骨图和根本原因分析,这是一种特殊的决策树。我们展示了使用scikit-learn
的方法,让计算机为我们构建一个分类决策树并创建一个可用的图。我们讨论了随机森林的概念,它只是使用决策树组进行预测或回归的演变形式。然后,我们探讨了图搜索算法和路径规划器,花了一些时间讨论A(或A-star)算法,该算法广泛用于制作路线和路径。当我们没有预先创建地图时,D(或动态A-star)过程可以使用动态重新规划来不断调整机器人的路径以到达目标。最后,我们介绍了拓扑图路径规划,并讨论了GPS系统是如何为你找到去咖啡店的路线的。
在我们接下来的章节中,我们将讨论通过使用蒙特卡洛模型模拟情绪来给你的机器人赋予人工个性。
问题
-
有哪三种方法可以遍历决策树?
-
在鱼骨图示例中,如何对决策树的分支进行剪枝?
-
Gini评估器在创建分类中扮演什么角色?
-
在使用基尼指数的玩具分类器示例中,玩具的哪些属性没有被决策树使用?为什么没有使用?
-
我们尝试的某个分类技术中,哪种颜色被用作玩具的标准?
-
请举一个在餐厅菜单项中进行标签编码和独热编码的例子。
-
在A*算法中,讨论
G()
和H()
的不同计算方式。 -
在A算法中,为什么
H()
被认为是启发式,而G()
不是?此外,在D算法中,没有使用启发式。为什么? -
在D*算法中,为什么有
RAISED
和LOWERED
标签,而不是仅仅一个CHANGED
标志?
进一步阅读
-
A算法简介:https://www.redblobgames.com/pathfinding/a-star/introduction.html
-
人工智能机器人导论,罗宾·R·墨菲著,麻省理工学院出版社,2000年
-
决策树算法是如何工作的:https://dataaspirant.com/2017/01/30/how-decision-tree-algorithm-works/
-
游戏编程 启发式方法:http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html
-
DLite算法博客(快速重新规划项目)*,斯文·科宁:http://idm-lab.org/project-a.html
-
移动机器人的基于图的路径规划,大卫·伍德南的论文,乔治亚理工学院电子与计算机工程系,2006年12月
-
《实时重规划中的聚焦D算法》 由安东尼·斯坦茨撰写:https://robotics.caltech.edu/~jwb/courses/ME132/handouts/Dstar_ijcai95.pdf
第九章:给机器人赋予人工智能
当一个人想到具有人工智能的机器人时,许多人认为的人工智能是一个具有情感、感觉、心态,以及某种人类思维或感觉模型或概念的机器人。我们可以称这种形式的人工智能为人工智能。虽然给机器人赋予情感肯定超出了本书(或当前技术)的范围,但我们可以通过使用标准计算机建模技术,如蒙特卡洛分析和有限状态机,为机器人创建一个个性模拟。
在本章中,我们将涵盖以下主题:
-
什么是人工智能?
-
对(已过时)图灵测试、聊天机器人和生成式 AI(GenAI)的简要介绍
-
模拟的艺术与科学
-
情绪状态机
-
玩情绪游戏
-
创建人类行为模型
-
开发机器人情感引擎
技术要求
在本章中,我们不会介绍任何新的编程库。我们将基于之前构建的语音系统。你所需要的只是想象力和一些写作技巧。
你可以在https://github.com/PacktPublishing/Artificial-Intelligence-for-Robotics-2e找到本章的代码。
什么是人工智能?
好莱坞和电影产业已经制作了一些非常难忘的机器人。你可以想到R2D2和C3PO,科幻电影中的劳伦斯·哈迪。你最喜欢这两者中的哪一个?可能是他们的个性?想想看。尽管R2D2大部分是一个垃圾桶形状,有一个圆顶头和没有脸,但他有一个明确的个性。你会形容他为活泼或固执。机器人和机器人网站(http://www.robots-and-androids.com/R2D2.html)这样描述他:
那个机器人被描绘成非常勇敢,忠诚坚定,有点固执。他从不放弃任务,即使看起来似乎所有的几率都对他不利。他的个性经常与[C3PO]形成对比,后者挑剔而胆小。
对于一个从不说话,只用哔哔声和哨声交流的机器人来说,这相当令人印象深刻。
有哪些其他电影机器人给你留下了印象?当然,来自同名的电影中可爱的垃圾压缩机器人WALL-E是首选。WALL-E也有一个小词汇量,只由他的名字组成,就像宝可梦一样。WALL-E表现出很多情感,甚至发展了爱好,收集和修复旧垃圾。你可能也记得M-O,那个小而痴迷于清洁的机器人,对WALL-E带来的所有脏东西感到沮丧。
因此,作为机器人创造者和设计师,我们可能会给我们的机器人赋予某种人格。这有利于人类更好地与机器人建立联系。它还给人一种错觉,即机器人比实际更聪明,能够做到更多。这确实使得机器人更具吸引力和趣味性。
我们也可以从R2D2和WALL-E给出的例子中推断出,在沟通方面,少即是多——我们需要的不仅仅是文字,还有肢体语言和声音。
在本节中,我们将为我们的机器人开发一个虚拟人格。虽然我们无法给机器人赋予实际的情绪或感觉,无论你如何定义它,但我们可以创建一个能够提供令人信服错觉的人格模拟。我认为这是一个有意义的练习,因为当前机器人技术的艺术状态要求与人类互动的机器人具有一定的个性和一贯的态度。
图9.1中看到的卡通展示了人们和机器人可能如何互动。机器人已经厌倦了捡玩具,并希望表达它的感受:
图9.1 – 一个人和一个机器人的互动
目前在这个领域正在进行大量工作,例如苹果的Siri和亚马逊的Alexa这样的数字个人助理。请注意,这些机器人或AI有独特的名字和声音,但我感觉它们在个性和能力上非常相似。当你向Siri或Alexa提出一个个人问题时,比如他们的年龄(Siri比Alexa年长得多)时,会有一些区别。
在本章中,我们将从模拟科学中借用工具——特别是状态机和蒙特卡洛分析——并使用它们来为机器人Albert构建一个人格模型。我们已经有了一个相当强大的工具,那就是我们用来讲笑话的Mycroft语音系统。我们将扩展Mycroft的功能和特性,以及开发机器人及其对我们人类感受的机器人观点的仿生情感。
我想强调的是,我们正在模拟情绪,而不是创造一个有情感的机器人。我们的模拟与真实情绪的相似之处,就像飞行模拟器与航天飞机的相似之处——两者都提供相同的信息,但航天飞机在90分钟内绕地球飞行,而飞行模拟器永远不会移动。
让我们先从著名的图灵测试谈起,来讨论本节我们试图达成的目标——我们能否创造一个与人类无法区分的机器人?
对(已过时)的图灵测试、聊天机器人和生成式AI的简要介绍
艾伦·图灵在其1950年在《Mind – 心理学与哲学季刊》上发表的论文《计算机机械与智能》中提出了他著名的测试,他称之为《模仿游戏》。在原文中,图灵想象了一个游戏,玩家必须通过在电传打字机上打字提问来猜测一个隐藏的人的性别——男性或女性。然后,他提出,一个真正智能的机器将是一个你无法区分电传打字机另一端隐藏的人格是真人还是计算机软件程序的地方。
注意
电影《模仿游戏》由本尼迪克特·康伯巴奇饰演艾伦·图灵,并展示了他在二战期间作为布莱切利公园数学家之一破解德国密码的角色。标题指的是图灵以其名字命名的著名测试的原名。
这些天,你可能一天之内会多次与计算机软件交谈,却没意识到你并没有在与人说话。机器人电话和聊天机器人可能会给你打电话,或者在推特上与你进行虚假的政治对话。图灵测试已被机器赢得(https://www.nature.com/articles/d41586-023-02361-7),但我们是否在计算机中发展了智能?根本不是——我们只是变得非常擅长模拟对话。最近,机器人专家们建议用更严格、更困难的评估来代替图灵测试,以评估计算机的认知技能和自我理解:https://techxplore.com/news/2023-11-redefining-quest-artificial-intelligence-turing.html。
我们将使用艾伦·图灵在其论文中提到的另一个工具——状态机。我们将在“情感状态机”部分使用状态机来定义和模拟我们机器人的情感。
现在让我们回到聊天机器人的概念。一个工作定义可能是一个旨在与人进行对话或通过语音或文本与人类互动的软件程序。虽然大多数聊天机器人都会明确表示自己是计算机生成的,但还有很多聊天机器人不会——包括那些试图影响选举或公众舆论的推特聊天机器人。许多聊天机器人被用来接听技术支持电话、进行营销电话,以及在游戏中作为非玩家角色(NPCs)与用户进行对话。
根据Stefan Kojouharov在2016年发表的由chatbotslife.com出版的文章《利用NLP和机器学习为您的聊天机器人提供终极指南》,聊天机器人有两种类型:
-
基于检索:这些聊天机器人依赖于存储的短语和单词,并使用软件决策来选择最合适的回复。可能会有一些关键词识别和名词主语插入,但主要动作是选择最合适的短语。
-
基于生成:这些聊天机器人根据词性(POS)和机器人对你意图的推测来编造新句子。它们可以被看作是机器翻译引擎,将输入(你的文本或语音)翻译成输出(机器人的回复)。正如你可能想象的那样,生成式聊天机器人要难实现得多,这就是为什么我们将使用基于检索的方法。来自ChatGPT等项目的新进展重新定义了聊天机器人的可能性。生成预训练转换器(GPT)这个术语指的是这个神经网络(NN)的三个特点:
-
生成式:该模型能够创建新的文本短语,而不仅仅是重复或分类文本。
-
预训练:该模型在庞大的数据集上进行预训练——超过万亿个示例——以理解语言和词语之间的关系。
-
Transformer:NN架构使用转换器一次处理整个句子,学习句子中的词义和关系(句子中单词的位置)。
-
在最简单的形式中,GenAI NN根据数十亿个示例的训练预测句子中下一个最可能出现的单词。
我们将利用GenAI来开发我们的一些文本输出,但将这些应用到经典聊天机器人上,从而实现两者的最佳结合——既不需要我们自己生成文本,又有一个我们可以控制输入和输出的系统,并且可以信任系统来控制机器人。
我们还需要注意两个其他细节。聊天机器人可以被设计成处理简短对话或长对话。绝大多数聊天机器人——包括Siri、Alexa和Mycroft等数字助手——都是为非常简短的对话设计的。Siri,天气怎么样? 有20%的降雨概率。 最高温度为88度。 就这样——整个对话只有三句话和两个互动。如果你再问一个问题,它就会开始一个新的对话,几乎没有参考之前的对话。
一个更具挑战性的任务是进行更长时间的对话,包括多次互动甚至选择多个话题。这要求计算机跟踪上下文或已经讨论的信息以及可能再次被引用的信息。
我们将尝试教会我们的机器人能够以7岁孩子的水平进行中等长度的对话。我将把中等长度定义为两次到六次互动之间。
现在,在我们继续讨论机器人之前,让我们快速讨论一些统计分布,因为我发现使用蒙特卡洛分析——以及创建定制的随机数分布——在机器人技术中非常有用。
模拟的艺术与科学
什么是模拟?模拟器是物理世界的计算机模型。你可能熟悉飞行模拟器,它们可以在不离开地面的情况下提供飞行感觉和交互。还有很多其他类型的模拟和模拟器。我们可以有一个医疗模拟器,模仿疾病或对治疗做出反应。它可能是一个金融模拟,根据趋势模拟股市上的利润。还有结构模拟,模拟桥梁和建筑物的负荷,以查看材料是否足够。
创建模拟最常见的方式是通过构建被测试物品的物理模型。对于一个飞行模拟器来说,这意味着要输入飞机或直升机的四个力的公式——升力、重力、推力和阻力。每个因素都有影响其性能的参数——例如,升力是空气速度、飞机重量、机翼大小和攻击角(即机翼与风之间的角度)的函数。改变这些中的任何一个,升力都会改变。如果升力超过由于重力(即飞机的重量)产生的力,那么飞机就会飞起来。模拟设置了一个时间步长间隔,就像我们为机器人设置的控制回路一样,并计算每个时间步长对飞机的力。然后我们可以应用控制并观察我们的模型表现如何。这样的模型被用来在构建原型或测试飞机之前预测性能。
另一种类型的模拟被称为蒙特卡洛模型。蒙特卡洛方法使用概率论用随机数的变体来代替复杂的物理模型,以近似相同的结果。如果你想要创建一个抛硬币的计算机模型,你不会花很多时间去确定镍币的物理属性或根据力建模空中抛掷的次数。你只需从1到100中随机选择一个数字,如果抽到的数字小于50,则结果为正面,如果大于50,则结果为反面。本质上,这就是蒙特卡洛方法。有很多物理过程可以使用这种技术近似和研究,其中结果可以用概率来描述。
我们可以将蒙特卡洛分析应用于模拟人们在机场通过安检的情况。如果你有一份航空公司的典型航班时刻表和每班航班的平均乘客数量,你就会知道机场的日客流量。难点在于模拟人们何时到达航班。让我们假设我们委托进行了一项研究,并大致确定50%的人提前1小时到达,25%的人提前2小时到达,其余的人均匀分布在2.5小时到30分钟之间,每200名乘客中就有1名乘客因迟到而错过航班。我们可以用两个标准分布(钟形曲线)和两个均匀分布(矩形)来近似乘客到达函数:
图9.2 – 一个复杂的概率分布函数可以通过简单分布的并集来近似
这些信息使我们能够创建乘客到达的样本大小,从而获得安检队伍长度的估计。我们会添加一些表示通过安检所需时间的值分布,然后我们就有了一个模型。我们有一份航班列表,并且对于每个航班,我们根据研究结果为每个乘客生成一个到达时间的分布,通过为每个乘客分配随机数并将他们根据到达时间分组来实现。
让我们为从达拉斯飞往华盛顿DC的08:00航班到达的212名乘客分配1到100的随机数。现在,我们根据这个数字分配到达时间——如果数字在1到25之间,乘客比航班提前2小时到达(06:00)。如果数字在26到75之间(接下来的50%),那么他们提前1小时到达(07:00)。其余的乘客,编号在76到100之间,被分配在提前2.5小时到30分钟之间的随机时间。并且我们从我们的212名乘客中挑选出1名不幸的乘客,他因迟到而完全错过航班。由于有些人打算提前2小时到达,但可能会稍微延误或提前,我们可以抖动或为每个到达时间添加一个±10分钟的微小随机分布因子。现在,我们有了一个人到达航班的统计正确分布。现在,重复这个过程,为今天从这个机场起飞的其他1,849个航班生成乘客到达时间的分布。你可以从这个模型中看到,我们仍然可以在随机数的选取中投入大量的科学。
那么,我们如何制作一个看起来像这样的概率分布函数(PDF)(见图9.2顶部图)呢?答案是,我们将几个分布结合起来。在图9.2的底部图中,你可以看到两个标准分布(绿色的峰值)和一个均匀分布(下方的绿色矩形),它们结合起来构成了我们的整体正确函数。
如你所猜测的,使这种技术真正奏效的真正秘密是选择正确的随机数分布。你可能认为,“嘿,随机数就是随机数,对吧?”但事实根本不是这样。你听说过标准分布或钟形曲线。大多数值都集中在中心,随着你远离中心,越来越少。许多自然过程都遵循这种钟形曲线,包括考试分数、人们的身高,或者一串葡萄有多少颗。当你进行蒙特卡洛分析时,我们通常使用某种形式的正态或标准分布。
你能想到其他类型的随机数吗?第二常见的随机数类型是均匀分布。每个数字被选中的概率完全相同,这导致了一个平坦的分布曲线。这种均匀分布的另一个名称是白噪声。均匀分布确实出现在分析中,如果我们想向图像添加噪声,例如,我们将使用均匀分布。但在建模情绪和人时,正态或标准分布是规则。
你可能会发现,在使用蒙特卡洛建模时,标准分布或均匀分布根本不起作用。那么,你可以创建一个自定义分布,就像我在机场示例中所做的那样,在那里我们使用了重采样将均匀分布转换为适合我们乘客到达模型的自定义分布。
图 9**.3 展示了各种类型的随机数生成器产生的分布形状,以及它们常用的名称——钟形曲线和平坦线。在我看来,对数正态图看起来像滑雪道——你认为它看起来像什么?
图 9.3 – 各种类型的随机数生成器和它们产生的分布
我们接下来的任务是生成我们机器人的个性模拟,我们将使用蒙特卡洛建模加上状态机来创建我们机器人情绪的模型。让我们首先讨论一下什么是情绪状态机。
情绪状态机
什么是状态机?我们在第 2 章的系统工程部分介绍了状态机——状态机是理解或建模自动化或计算机程序的技术。状态是一组存在于当前的条件。我喜欢把状态想象成一组受限于限制的函数。机器(我们的机器人)根据导致状态变化的事件从一种状态转换到另一种状态。
让我们通过一个快速复习的例子来操作。让我们拿一片面包。当我们拿到它时,它已经烤好并切片了,所以它的初始状态就是一片面包。如果我们用红外辐射(即热量)照射面包,那么表面就会变成焦糖色,我们称之为烤面包。面包的状态发生了变化,连同它的味道和质地,从烤面包变成了烤面包。导致这种转变的事件是在烤面包机中加热面包的行为。这很简单,我相信你以前一定遇到过状态机。
现在,让我们思考一下我们的机器人情绪模型。我们可以从列出我们希望机器人拥有的情绪开始:
-
快乐
-
欢迎的
-
友好
-
好奇
-
积极的
-
精力充沛
然后,我们可以列出这些情绪的反面:
-
悲伤
-
遥远
-
不友好
-
挫败的
-
疲倦
这些是我希望在机器人中模拟的情绪列表。我研究了机器人可能有的不同交互以及机器人的人类版本会如何反应:
图9.4 – 机器人情绪状态机模型
在前面的图中,我们通过在极坐标图上绘制各种情绪成分或状态来模拟机器人的整体情绪状态,如下所示:
-
每个情绪元素都有一个矢量或方向。图表的左侧是快乐、友好、开放的感觉,而右侧是悲伤、封闭、遥远的感觉。
-
从中心到距离的长度表示情绪的强度。
-
黄色圆圈是机器人在每个轴上的当前值。
-
紫色线是这些情绪的矢量总和,这为我们提供了机器人的整体情绪。弧线显示了情绪的最小和最大强度。
我们取由这四个组成部分组成的区域,并找到该区域的中心,这就是机器人的整体状态。在这个例子中,主导情绪是友好的。由于机器人的物理状态决定了它是否感到疲倦,因此这些数据是单独计算的——这可以让机器人感到疲倦但友好,或者清新但悲伤。
机器人开始时处于一种快乐、遥远、好奇和新鲜的状态。也就是说,它感到快乐,它没有处于互动状态,因此没有可以友好对待的人,它会对周围环境感到好奇。随着环境的变化,机器人的状态也会变化。例如,在阿尔伯特当前的电池上,我们大约有2小时的运行时间。它一开始处于新鲜状态,随着接近2小时的时间标记,它会越来越累。我们打算使用一个简单的计时器来创建这种状态,但你也可以使用机械臂上的电压传感器来提供有关机器人电源新鲜度的信息。每种情绪都沿着一个轴存在,并且都穿过一个中心或中点。
我们将根据机器人获得多少对话点来驱动“喜忧参半”的轴。我们将在“玩情感游戏”部分详细说明这一点。我们还将描述一个模拟机器人与你分享感受的同理心功能。远距离欢迎这一方面由人类如何与机器人互动来控制。如果你对机器人友好,那么它会表示欢迎。如果你是新手,机器人会开始谨慎地提问或互动。如果你不回答问题,那么机器人会变得更加疏远。同样,朋友-陌生人这一方面基于机器人对其对话者了解多少。最后,好奇-沮丧轴基于机器人获取信息或完成任务需要付出多少努力。如果它在任务上不断失败或没有得到奖励,它会在其表达和词汇中变得沮丧。
没有脸的机器人如何表达情感?记住,我们开始谈论好莱坞机器人,其中许多机器人没有脸甚至没有头,但个性鲜明。我们将使用机器人手臂的身体语言和词汇变化来表达我们模拟的情感。我们还将让机器人公开表达它的感受和原因。
我们在这个机器人开发部分的目标是提供一个人类交互框架,邀请人们与机器人交谈并感到受欢迎或被需要。我是通过观察我的孙子孙女来模拟这种交互的。我希望他们喜欢并觉得机器人有趣。我们想要做的是开发机器人传达意图或提供机器人有需求和欲望的模拟的能力。我们将通过创建机器人将与人类一起玩的游戏来实现这一点——让我们称这个游戏为情感游戏。
玩情感游戏
那么,这个游戏究竟是什么呢?我们希望机器人做的事情是向另一个人提问,并使用对话从他们那里获取个人信息,这样机器人就可以使用这些信息进行回复。在这个游戏中,机器人将通过让人类进行社交互动来尝试获得积分。机器人将通过从人类那里获取信息来获得积分,以便它能更好地了解他们。对我们来说,技巧是需要机器人保存这些信息并记住它们。我们将保存所有这些信息,并使用它来修改机器人实际运行的代码,从而将机器学习(ML)融入到对话中。机器人将使用我们在上一章中训练机器人手臂时使用的相同类型的点数奖励系统。一旦机器人通过学习一个事实获得积分,它将不再为那个事实获得奖励,并继续学习其他事实。一旦它学习了所有的事实,它将结束对话。在实践中,我认为大多数用户都会希望对话相对较短,因此我们将目标设定为每次对话之间有两次到六次互动。
让我们快速总结一下游戏将如何进行:
-
用户将通过说出机器人的唤醒词来始终启动对话,目前这个唤醒词是嘿,阿尔伯特。
-
机器人将使用Mycroft语音引擎以哔哔声进行回复。
-
用户将通过使用唤醒词来启动对话,这个唤醒词是某种版本的你好,机器人。
-
然后,机器人将通过提问来尝试通过获得积分来获得奖励。
-
情绪引擎将通过修改机器人的情绪来响应人类的答案。
-
我们将根据重要性对问题进行排名——比如说,机器人通过学习你的名字获得10分,学习你的年龄获得9分,以此类推。
-
一旦学习到某个事实,就不会再获得积分,因此机器人不会重复提问。
我们希望机器人了解的关于每个人的事实如下:
-
你的名字
-
你的年龄
-
你今天感觉怎么样?
-
你最喜欢的食物是什么?
-
你最喜欢的书是什么?
-
你上学吗?
-
如果是的,你最喜欢的科目是什么?
-
你的生日是什么时候?
-
你最喜欢的歌曲是什么?
-
你喜欢粉色/唱歌/跳舞/恐龙/赛车/建造东西/机器人/飞机/宇宙飞船吗?
-
你刷牙了吗?
-
你喜欢打趣笑话吗?
作为机器人学习游戏的一部分,我们将通过调整我们提供给机器人的八种情绪(或四种情绪类型)的水平,来调整机器人在学习和互动过程中的情绪。我们将特别关注八种情绪的平衡点——它们是在图表的快乐/友好/好奇一边,还是更多地在悲伤/沮丧/疏远一边?
由于我们试图获取机器人对话者的一些个人信息,他们也可能想更多地了解这个机器人。因此,为了回应,我们的机器人也将拥有一个背景故事或传记,它将用来回答关于它自己的问题。我们将为机器人提供一个简短的叙述:
-
他的名字是Albert。
-
他的全名是Albert Robot the Second。
-
他8个月大。
-
他是由爷爷制造的。
-
他出生于2023年1月28日。
-
他喜欢绿色。
-
他最喜欢的食物是电能。
-
他最喜欢的作者是艾萨克·阿西莫夫。
-
他不上学但喜欢学习。
-
他的工作、爱好和激情是捡拾玩具。
-
如果你问他感觉如何,他会告诉你哪种情绪状态是最高的,以及他的电池有多新鲜或有多累。我们希望他偶尔在不被询问的情况下,将他的感受插入到对话中。
注意
我一直在整本书中用他和他来称呼这个机器人。这只是我的人性化投射,并暗示了一些机器人并不具备的特征。Albert身份的主要原因是他的声音——我为他选择了男性声音的合成器,主要是因为我想让它与周围所有女性GPS和个人助理计算机的声音区分开来。请随意创建任何你喜欢的声音——有很多女性声音可供选择,你可以在你的机器人中创建任何你喜欢的角色,并给予他们任何形式的称呼。有趣的是,我们甚至对非类人形机器人也倾向于这样做。
根据irobot公司首席执行官Colin Angle的说法,超过80%的Roomba吸尘器主人已经给自己的机器人起名,包括他(https://slate.com/technology/2014/03/roomba-vacuum-cleaners-have-names-irobot-ceo-on-peoples-ties-to-robots.html)。无论人们多么依恋他们的烤面包机或立式搅拌机,你都不会看到他们给这些设备起名字。随着我的孙女逐渐长大,Albert机器人可能也会进行性别转换手术,或者得到一个妹妹。
我们还将使用情绪状态来设定机器人的肢体语言,这主要是指他如何携带他的机器人手臂。如果他很高兴,手臂就会伸展,手指向上方。如果他难过,手臂就会靠近他的身体,手指向下方。我们将存储所有这些信息,以便机器人对个人问题给出一致的回答。
创建人类行为模型
为了让机器人支持进行对话,我们还必须有一个模型来了解它正在与之交谈的人的感受。你可能有过一个朋友或亲戚一直在谈论自己,而完全忽视了你的感受或对他们的谈话的反应。我们不希望这种类型的机器人个性。因此,机器人必须有一些内部表示,以了解你认为你是什么感受。我们不会使用视觉来完成这个功能,所以机器人知道你怎么样,唯一的方式就是通过提问和评估语言的使用。
我们将给机器人一个类似于我们的状态机但只有两个轴上的四个情感的人模型:快乐/悲伤和友好/疏远。机器人将假设在对话开始时每个人都在中间某个位置。机器人可以使用语言中的线索来了解你可能会有的感受,因此我们将为情感分配颜色或色调,以帮助驱动机器人的模型。以下图表可以说明这一点:
图9.5 – 简化的情感连续体
人的不同情绪会引发机器人不同的反应。我们特别希望机器人能够注意到人类因机器人不理解或以人们不希望的方式响应而变得沮丧的线索。这使我们实际上选择和设计我们机器人的一个艺术个性。
将人工个性集成到我们的机器人中
我们需要设定一些指南和参数,以确定机器人将具有哪种个性。我们可以先列出一些机器人可能具有的个性类型以及它们可能扮演的角色。这一点尤为重要,因为这款机器人将主要与儿童互动。让我们尝试几种,看看哪种适合:
-
教师/教授:机器人试图传达信息,并试图以某种方式教导或改变人的行为。机器人以权威者的身份出现,并提供了关于自己的事实和信息,例如:“我是一个机器人。你知道什么是机器人吗?”
-
超级友好:机器人喜欢与人玩耍,对与人交谈感到兴奋,积极与人交谈,并提出很多问题。机器人表现出很多热情和鼓励。“我喜欢我的工作。你喜欢你的工作吗?太棒了!我非常喜欢和人交谈!你想听个笑话吗?”
-
亲切友好:机器人试图交朋友并有所帮助。“嗨!很高兴见到你!你叫什么名字?”
-
友好但中立:机器人不急于分享信息,但确实想和你交谈。“你好,我是阿尔伯特。你今天怎么样?”
-
活泼:机器人支持互动,友好,并希望获取更多信息。如果需要,它会重复问题。“嗨!很高兴见到你。你叫什么名字?你多大了?”
-
庄重而严肃:这个机器人显得有些古板和权威。想象一下与一位英国管家交谈的场景。机器人使用正式的英语。(这显然是许多数字助手的默认非个性化。)“你好。我能帮您什么忙?我可以问一下您的名字吗?”
-
非常中立或机械:机器人不表达任何观点,也不提供关于自己的信息。它不表达任何情绪或插话。“你好。我是一个机器人。请下达命令。”
-
烦躁和易怒:机器人对于不得不捡玩具并不高兴,并且不介意清楚地表达出来。这种个性旨在寻求幽默,而不是愤怒。“所以,你又回来了。我想这意味着又有玩具要捡了。”
从这个列表中,我们可以看到我们可以为模拟选择的各种个性范围。我将选择亲切友好类型的个性,因为我认为这会与我的孙子孙女们相处得最好。当然,你也可以选择任何你喜欢的,或者添加一些新的。现在,让我们看看我们如何继续进行。
构建个性
在快速回顾中,我们目前为具有人工个性的机器人准备了哪些部分?
-
我们有模拟工具来模拟情绪。
-
我们有一个包含六种情绪的状态机,我们可以将这些情绪混合在一起。
-
我们有关于机器人个人信息的背景故事或传记。
-
我们已经选择了一个要模仿的个性。
-
我们有一个游戏引擎的概念,其中机器人通过参与闲聊或寒暄交流来收集信息以得分。
-
机器人将根据对话改变情绪。情绪可以通过询问机器人它感觉如何或通过机器人手臂的身体语言来表达。
现在,我们需要某种框架来将这些部分组合在一起,以便大致进行对话,我们可以调整和操作。为此,我们将重新拜访一位老朋友。让我们再次谈谈ELIZA。
ELIZA可能是最初的对话引擎软件程序。它是在1964年由麻省理工学院的Joseph Weizenbaum教授发明的,他是二战期间逃离德国的犹太难民,被认为是人工智能的先驱之一。ELIZA的名字来源于乔治·萧伯纳的书《皮格马利翁》中的主要角色伊莉莎·多利特。在希腊神话中,皮格马利翁是一位雕塑家,爱上了一尊雕像。他向阿芙罗狄蒂祈祷,阿芙罗狄蒂赋予了他的愿望,让雕像复活。这非常适合我们所做的事情——通过这一奇怪的事件链、标题和希腊神话,让我们的机器人进入模拟的生命。
ELIZA是一个罗杰斯治疗师的模拟。什么是罗杰斯治疗师?根据《心理学今日》(https://www.psychologytoday.com/us/blog/what-doesnt-kill-us/201210/unconditional-positive-regard),卡尔·罗杰斯(1902-1987)是>提示符
的创始人。顺便说一句,我喜欢猫——这只是一个例子。请不要告诉我家的猫这个例子:
ELIZA: How do you do. Please tell me your problem.
> I hate cats.
You say you hate cats?
> Yes.
You seem to be quite positive.
> I hate cats.
Can you elaborate on that?
> I don't like cats because they smell bad.
Don't you really like cats because they smell bad?
> No. Don't like cats.
Are you saying no just to be negative?
> No.
You are being a bit negative.
> I am
Is it because you are that you came to me?
> No
Why not?
> I am afraid of cats
How long have you been afraid of cats?
> About a year
I'm not sure I understand you fully.
> 1 year
Please go on.
> I have been afraid of cats since the cat jumped on my head.
Your head ?
> yes
因此,你可以看到ELIZA会重复用户说的话,反映出用户在谈论的内容。当我说我“害怕猫”时,程序回复说,“你害怕猫有多久了?”
ELIZA在识别和回应句子时经过几个步骤。程序的大部分内容不是硬编码的,它通过一系列可以编辑和替换的脚本工作,从而基本上重新编程了程序的性格——这就是我们为什么现在在谈论ELIZA的原因:
-
第一步是将句子分成由空格分隔的单词。程序还将所有大写字母替换为小写字母。
-
程序用标准词替换几个版本。例如,如果省略了撇号,程序会将cant替换为can’t。这被称为预处理。
-
程序寻找它所知道的关键词。一个简单的关键词是sorry。任何包含sorry的句子都会得到如“请不要道歉”之类的回复。关键词根据它们在脚本文件中出现的顺序进行收集和排序。
-
程序寻找与该关键字相关的分解模式。这会将句子收集到常见的组中。例如,一个模式是:
* i was *
,这可以读作任何词组
–I was - 任何
词组
。 -
ELIZA选择了一个
* 我曾经 *
模式,一个回复是“也许我已经知道你曾经(2)。”括号中的数字(2)告诉程序用句子中我曾经
后面的词组来替换。如果你输入,“然后我被留在了公交车站”,这个模式下的回复可能是,“也许我已经知道你被留在了公交车站。”你也可能会得到一个更实际的回复,“哦,真的。”重要的是要知道,ELIZA对短语的内容一无所知——它只是在根据模式操纵单词来创建句子。 -
ELIZA执行后处理替换单词。例如,它将单词I替换为you。如果你输入“我去睡觉了”,程序会回复,“你说你去睡觉了?”,这是在所有其他规则都用尽后的最终回复规则。
控制ELIZA个性的数据被称为脚本。这为程序提供了形成回复的所有规则。罗杰斯治疗师的脚本被称为DOCTOR脚本。它包含一些问候语,一些程序退出时的结束语,一个预替换规则列表,一个后处理替换词列表,一个同义词列表,以及一个具有分解和重组规则的关键字列表。
这里是关于I
am
关键词的一个示例规则:
decomp: * i am *
reasmb: Is it because you are (2) that you came to me ?
reasmb: How long have you been (2) ?
reasmb: Do you believe it is normal to be (2) ?
reasmb: Do you enjoy being (2) ?
程序从提供的四个短语中随机选择一个。如果我说是“我害怕猫”,那么这个规则就会被触发,并生成这四个短语中的一个。它可能会说,“你喜欢害怕猫吗?”或者“你害怕猫有多久了?”
几乎所有由ELIZA创建的对话都来自脚本文件,这使得ELIZA成为一种基于规则的专家系统,同时也为创建新的对话引擎应用提供了一个开放框架。
正如所有计算机程序一样,ELIZA生成对话的概念被扩展并增强为一个通用(即,不仅限于治疗师)的软件程序,称为ALICE,这是对Artificial Linguist Internet Computer Entity的逆命名。ELIZA使用的简单脚本数据文件变成了人工智能标记语言(AIML)。ALICE被扩展得比ELIZA更有用,覆盖了更多的主题和能力。然而,ALICE保留了相同的前置和后置替换、关键词和响应模式的概念。有一个Mycroft插件引擎,允许Mycroft使用AIML文件来设计对话(https://github.com/forslund/fallback-aiml)。这个工具是构建你的机器人个性的优秀替代构建工具。
在这里,我们提供了一个关于AIML外观的示例(http://www.digitalanywhere.com/projects/dany/index.html)。你会看到包含语句类别的块,然后是那个类别的外观模式。下一个标签<template>
描述了当触发这个类别时机器人将做出的响应。例如,第一个类别是关于演奏乐器的。如果你对机器人说“我会弹吉他”,那么响应将是,“吉他是一种游戏还是一种乐器?”我们可以用这种方式创建我们的简单句子模式:
<category>
<pattern>I PLAY *</pattern>
<template>Is <set_it><person/></set_it> a game or a musical instrument?</template>
</category>
<category>
<pattern>I PLAY THE *</pattern>
<template>I wish I played an instrument. My parents could not afford the lessons.
<think><set_it><set_does><settopic>play the
<person/></settopic></set_does></set_it></think></template>
</category>
<category>
<pattern>I PRIDE MYSELF *</pattern>
<template>It seems you have good self esteem.</template>
</category>
<category>
<pattern>I QUIT MY JOB *</pattern>
<template>Where did you work and why did you quit?</template>
</category>
<category>
<pattern>I READ *</pattern>
<template>I don't think I've read that. Can you give me a reference?
<think><set_it><set_does><settopic>read
<person/></settopic></set_does></set_it></think></template>
</category>
<category>
<pattern>I READ * BOOKS</pattern>
<template>Name some books in that genre.
<think><set_it><set_does><settopic>read <person/> books</settopic></set_does></set_it></think>
<think><set_personality>critical</set_personality></think></template>
</category>
ALICE是开源软件(OSS),在GNU公共许可证下发布。
现在,让我们看看我们如何为我们的机器人对话添加一些上下文。
添加上下文
ELIZA最严重的缺点是完全缺乏记忆。ELIZA只记得之前的陈述。你不能谈论你的父亲然后说,“他是个好人。”ELIZA将不知道你在谈论谁。ELIZA没有超出上一句话的上下文概念。
上下文是什么?在对话过程中,我们经常将名词缩短为代词。我们可能会说,“我喜欢我的狗”,然后在下一句话中说,“它表现得很好。”谁是指的她?我们知道它是指狗,但计算机如何知道?我们将向我们的程序添加一些从上下文中推理的能力。
我们将创建一个名为上下文记忆的存储对象。在那个对象中,我们将能够跟踪我们对话的几个部分,包括我们目前正在与之交谈的人、我们最后讨论的主题、如果我们还未能得到答案的问题,以及任何先前问题的答案,以防我们再次需要它们。计算机将假设除了I之外的其他代词将指代最后一个主题,无论那是什么。如果我谈论的是一只狗,然后说,“她很可爱”,那么机器人就会假设我指的是狗。
之前,我们讨论了通过玩游戏从一个人那里获取信息。机器人将收集并记住这些信息,即使在关闭后,这样下次那个人与机器人交谈时,它也能记住上次学到的信息——就像你与新朋友做的那样。如果你想继续扩展这个AI聊天机器人概念,你可以使用这些信息来创建额外的对话。例如,如果人类表示他们喜欢棒球,机器人可以询问他们最喜欢的球队,然后在互联网上查找下一场棒球比赛的时间表。
这就是我们将用于构建机器人个性的部分列表的结束。我们现在可以深入使用我们的个性构建工具包。我将使用Wade Brainerd的ELIZA Python开源程序的主体来构建Albert机器人的个性。
为了节省时间和空间,我只会在这里展示我添加到基础程序中的部分。完整的代码将在GitHub仓库中。如果你想了解这本书之外的内容,可以在https://github.com/wadetb/eliza获取原始程序。
正在建设中
让我们回顾一下我们需要组合在一起以制作我们的机器人个性的所有部分:
-
模拟
-
蒙特卡洛(基于随机或随机基础的)建模
-
我们的机器人情感状态机
-
人类情感状态机的感知
-
我们的机器人传记(关于机器人的内部事实列表)
-
一个名为ELIZA的对话引擎框架
-
上下文或记住和返回事实以及“填补空白”的能力
在本节和接下来的几节中,我将展示我添加到Albert中以便于他的人工个性。其中很大一部分将包含提供Albert说话规则和模式的脚本文件。还将有他的情感引擎、人类情感模型和游戏引擎的代码函数。让我们开始吧:
-
我需要为ELIZA使用的脚本语言添加一些新功能。首先,我添加了我们的机器人(它取代了ELIZA,这位治疗师)的上下文。首先,我们有当我们通过说“嘿,Albert”(或你决定叫它什么),Mycroft的唤醒词,然后就是“你好。”来启动Albert的交互式对话模式时的开场白。
-
机器人用
initial
短语回答,如冒号前的标签所示。我们还有我们的结束语。实际上,你可以放任意多的短语,计算机将随机选择一个。这些规则被放入我命名为AlbertPersonality.txt
的文件中,这个文件最初是ELIZA附带的原始doctor.txt
脚本文件的副本:initial: Hello. My name is Albert the Robot. initial: Hello. I am Albert the Robot, but you can call me Albert. initial: Hello. Nice to meet you. Call me Albert. final: Goodbye. Thank you for talking to me. final: Goodbye. It was nice to talk to you. final: Goodbye. I need to get back to my tasks. quit: bye quit: goodbye
-
我添加了一些单词替换,以防用户用名字而不是you来称呼机器人。这只是为了将you替换为用户可能用来称呼机器人的任何词。我还为机器人的各种版本设置了同义词,所以你可以称呼它为robot或bot,以及Albert、Bert或甚至Bertie。
前面带有
pre:
的规则在所有其他处理之前被替换。从第一条规则开始,如果句子中出现单词robot,例如“Robot, how old are you?”,程序将移除robot并将其替换为you,以使解析保持一致。我们还将所有大写字母转换为小写,因此在规则中没有大写字母。synon:
(同义词)规则将列表中的任何单词替换为第一个给出的单词:pre: robot you pre: albert you … synon: you robot albert bert bertie bot synon: belief feel think believe wish
-
接下来我们需要创建机器人需要提问以获取信息的问题。程序将自动从我们定义的任何关键词中提取这些数据。以下是关于提问规则的定义:
questions: reasmb: What is your name? <assert name> reasmb: What can I call you? <assert name> reasmb: How old are you? <assert old> reasmb: How are you feeling today <assert feeling>
-
我们为脚本文件中的问题创建了一个新的标志。每一行代表一个问题,但我们可以用不同的方式或形式来提问。程序将随机选择一个版本,并根据我们为问题设置的相对优先级来决定提问哪个问题。我添加了带有
<>
符号的assert
关键字作为另一个新标志,以提示上下文记忆,即我们已经创建了一个提问上下文,接下来的语句可能是答案:datum: name decomp * my name is * decomp I am * decomp call me * decomp <name> * # we are in the name context reasmb: Hello (1). Nice to meet you reasmb: Hi (1). reasmb: Your name is (1), right? reasmb: Thank you for telling me your name, (1) store: <name> (1) decomp * my name is * reasmb: Hello (2). Nice to meet you.[welcome][happy] store:<name> (2)
-
我创建了一个新的数据结构,我称之为
datum
,它是data的单数形式。这代表我们希望机器人询问的一些信息。我们给datum一个标题——在这个例子中是name
,因为我们希望机器人询问它正在与谁交谈的名字。decomp
(分解)标签是用户可能说出他们名字的句子模式。*
代表任何短语。所以,如果人类说“Hello. My name is Fred Rodgers”,那么机器人将从此以后称呼他们为Fred Rodgers。如果人类出于某种原因说“Call me Ishmael”,那么机器人将使用那个名字。我们必须使用reasmb
(重新组装)规则来重新组装响应短语。(1)
指的是第一个*
短语。如果用户说“I am John”,那么当我们使用重新组装规则时,(1)
将被替换为John
。机器人将随机选择提供的短语之一,例如:“Your name is John, right?”我添加了另一个新标签,以便机器人可以使用上下文记忆区域执行两个功能。当我们提问时,我们可以声明一个上下文主题,我们将在后面的章节中这样做。例如,当我们问用户,“你叫什么名字?”时,我们希望机器人知道下一个答案将是在该问题的上下文中。机器人说“你是谁?”而用户立即回答“朱莉娅”,句子中不再有其他词语,这是完全合理的。程序如何知道如何处理 朱莉娅 呢?答案是上下文标志,用
<>
括起来表示。我们读取decomp <name> *
分解规则,就像你处于请求名字的上下文中一样,然后得到一个没有关键词的回复,所以接受你得到的一切作为答案。情感标签用括号表示,例如
[快乐]
或[悲伤]
。这会根据它是从人类接收到的语句还是机器人说出的句子,来移动机器人的情感或机器人对人类情感的感知。一个语句可以与多个情感标签相关联。这里是监听
age
问题答案的规则:datum: age decomp <age> * I am * years old decomp <age> * I am % # integer reasmb: You are (2) years old? reasmb: (2) years old! decomp <age> * reasmb: You are (1) years old? reasmb: (1) years old! store: <age> (1)
在最后一行,
store:
是告诉计算机这是问题的答案并将其存储在字典中提供的标题下的命令。 -
接下来,让我们用一个带有一些与之相关的情感交互的例子来展示,这样我们就可以看到我们将如何使用情感引擎来控制机器人说什么。当用户对机器人说“你感觉怎么样?”时,将执行这组规则:
key: feeling decomp: how are you feeling decomp: how are you decomp: hows it hanging decomp: how are you today reasmb: <happy> I'm doing well. How are you? <assert feeling> reasmb: <sad> I am feeling sad. How are you? <assert feeling> reasmb: <curious> I am curious about my surroundings reasmb: <friend> I am feeling friendly today reasmb: <welcome> I am in a welcoming mood today, my friend reasmb: <frust> I am a bit frustrated, to tell you the truth reasmb: <frust> I am feeling a bit frustrated reasmb: <strange> I am having relationship problems reasmb: <distant> None of my friends have come to visit reasmb: <tired> My batteries are low. Maybe I need a rest.
-
我们将把机器人的情感放入上下文记忆中,以便脚本处理程序可以访问它。对于对话目的,我们将情感视为我们正在讨论的上下文的一部分,我认为这是处理情感的一种合理方法。每个情感在上下文记忆字典中都有一个标签或名称。如果机器人的主要情感是快乐,那么机器人将在上下文记忆中设置
happy
上下文。然后,规则库将使用上下文标签来确定回答“你感觉怎么样?”时使用哪个短语。我们也可以提出后续问题。看看<happy>
的规则。机器人回答,“我过得很好。你呢?”然后设置feeling
上下文,让引擎知道我们提出了关于感觉的问题。最后,最后一行与tired
情感相关。如果机器人感到疲倦,那么我们将跳转到另一个单独的部分,让机器人谈论感到疲倦。我们将其作为一个单独的程序,因为我们需要从几个地方调用它,这说明了基于规则的方法在语音中的实用性。我不想想象需要多少行 C 或 C++ 源代码来为每一行对话创建所有这些规则。我们继续使用这些指南修改脚本,直到我们完成了所有问题,并为所有答案建立了模式。
现在,我们将切换回Python代码,以完成这个示例的其余部分。我们的下一节将描述我们将如何模拟情感。
开发机器人情感引擎
现在我们将构建机器人的情感模型。这是人工智能个性的核心,因为它计算、更新并记住机器人的情感状态。机器人从一种通常的中性状态开始,并根据八种情感特质进行更新:快乐/悲伤,欢迎/疏远,友好/陌生人,好奇/沮丧,以及新鲜/疲惫。随着机器人在经历中的事件发生,它会得到导致其情感状态变化的线索。例如,如果用户对机器人说的话说“那太愚蠢了”,那么机器人就会在其情感的悲伤
轴上增加。
我们使用极坐标来计算整体情感状态,就像我们在本章前面绘制的图中看到的那样(图9**.4)。当前的情感状态是通过计算其他情感的质心来确定的。如果情感平衡得更多或更少,那么在极坐标图上绘制的情感质心会接近中心。如果机器人主要快乐和友好,那么质量就会更多地移动到图表的那一侧。我们选择距离质心最近的单一情感状态。这旨在成为为机器人创建复杂情感性格的基础。新鲜/疲惫这一属性是独特的,因为该情感的价值基于机器人的运行时间。
机器人情感的主要表达将是机器人手臂的位置——更快乐的机器人会举起并向前伸展他们的手臂——以及对话中的词汇选择。
下面的代码块为情感引擎创建所需格式的数据条目,用于我们的对话引擎。我们正在创建一个基于规则的格式的Python接口,以便我们可以将其连接到机器人的其余部分:
class RobotEmotionEngine():
def __in _(self):
self.emostate = [90,0]
self.emoText = "neutral 50"
self.emotions = {"happy" : 50, "sad": 50,"welcome" : 50, "distant":50,"friend" : 50,"strange" :50, "curious" : 50,"frustrated":50, "fresh" : 50, "tired",50}
self.bio = {"name":"Albert Albert", "lastname": "Albert", "age": "6 months","maker": "granddad", "color":"green","food","electricity","author":"Isaac Asimov, of course","school": "I do not go to school but I love to learn","hobby":"picking up toys", "job":"picking up toys"}
# list of happy emotions and sad emotions self.emotBalance={"happy": "sad", "welcome":"distant","friend": "strange", "curious": "frustrated","fresh": "tired"} self.emotionAxis{"happy":112, "welcome": 22,"friend":67,"curious":157,
"sad":292,"distant":202,"strange":247,"frustrated",337}
self.update()
def change(self,emot, val):
self.emotions[emot]=val
balance = 100 - val
otherEmotion = self.emotBalance[emot]
self.emotions[otherEmotion]=balance
接下来是 update
函数;这个函数检查我们的情感状态是否发生了变化,如果是,我们就改变我们的当前情感:
def update(self):
rmin = 100
rmax = 0
thetamin =360
thetamax=0
for emote in self.emotions:
theta = self.emotionAxis[emote]
thetamax = min(theta,thetamax)
thetamin = max(theta,thetamin)
r = self.emotions[emote]
rmin = max(rmin, r)
rmax = max(rmax,r)
stateR = (rmax-rmin)/ 2
stateTheta = (thetamax-thetamin) / 2
for emo in self.emotionAxis:
thisAngle = self.emotionAxis[emo]
if stateTheta > thisAngle
myEmotion = emo
break
self.emostate = [stateTheta, stateR]
if stateR < 55 and stateR > 45:
myEmotion = "neutral"
self.emoText = myEmotion + " "+ str(stateR)
print "Current Emotional State" = myEmotion, stateR, stateTheta
return
机器人还需要一个模型来模拟它正在与之交谈的人类,以便它可以根据人类的互动方式做出不同的响应。在下一节中,我们将创建之前使用的情感模型的一个较小版本。
创建人类情感模型
我们为机器人在制定响应时使用的人类交互模型建模四种情感:快乐/悲伤和欢迎/疏远。我们可以在脚本文件的模式中放入 [happy]
、[sad]
、[welcome]
或 [distant]
情感标签来标记响应的情感。例如,如果我们没有得到问题的答案,我们可以用 [distant]
标记那个响应,以表明我们的主题不合作:
图9.6 – 简化的人类情感模型,展示快乐和悲伤情感之间的轴:机器人视角
我们的人类情感模型使用Python字典数据结构来存储我们的模型。让我们更仔细地看看这个:
-
我们有两个轴:快乐/悲伤轴和欢迎/疏远轴。我们根据响应上下移动快乐/悲伤指数。如果我们认为一个响应表达了快乐的思绪(“你喜欢学校吗?” “是的”),程序就会将情感指数向上移动到快乐方向。我们使用这些交点来设置当前的情感指数。如果人类接近中心,我们将其标记为中性,我们的起点:
class HumanEmotionEngine(): def _init_ (self): self.emostate = [90,0] self.emoText = "neutral 50" self.emotions = {"happy" : 50, "sad": 50,"welcome" : 50, "distant":50} # list of happy emotions and sad emotions self.emotBalance={"happy": "sad", "welcome":"distant"} self.emotionAxis = {'distant': 315, 'welcome': 135, 'sad': 225,'happy': 45} self.update()
-
让我们看看
change
函数。如果happy
增加,sad
减少,所以当情感变化时,我们会自动平衡这一点:def change(self,emot, val): self.emotions[emot]=val balance = 100 – val otherEmotion = self.emotBalance[emot] self.emotions[otherEmotion]=balance
-
update
函数获取人类模型的当前情感平衡:def update(self): stateR = self.emotion["happy"] stateS = self.emotion["welcome"] self.emostate = [stateR, stateS]
-
如果情感状态接近中间,我们称之为
neutral
:if stateR < 55 and stateR > 45 and stateS < 55 and stateS > 45: myEmotion = "neutral" happySad = stateR-50 welcomDist = stateS-50 if abs(happySad) > abs(welcomDist): myEmotion = "sad" if happySad > 0: myEmotion = "happy" else: myEmotion = "distant" if welcomDist> 0: myEmotion = "welcome" self.emoText = myEmotion + " "+ str(stateR) print "Current Human Emotional State" = myEmotion, stateR, stateTheta return
下一个部分将讨论我们将存储从人类收集的信息的地方。
创建人类信息存储
这个数据结构存储了我们收集的人类用户信息,并让机器人知道还有哪些问题尚未回答。我们使用Python字典来存储数据。让我们制作一个类似数据库表格的自由形式版本。随着我们的进行,我会向字典中添加值,以便更容易扩展。我放了一个名为stuff
的杂项问题列表,以给机器人的信息收集增添一些趣味,使问题不会显得过于单调。机器人会问你喜不喜欢粉色、唱歌、跳舞、机器人、飞机等等。
我们通过复制字典并替换数据字段为20到0的相对点值来按优先级顺序排列列表。name
排在第一位,得20分,age
排在第二位,得18分,以此类推。随着每个问题的回答,我们将这个点值设为零。例如,如果我们得到“你最喜欢的食物是什么?”的答案为“苹果”,我们将self.info["food"] = apple
,并将self.points["food"] = 0
。我还为孙子辈喜欢的事物添加了一些额外的问题,点值设置得非常低,以便使对话更加多样化。我会问关于公主、飞机、恐龙、唱歌和建造东西的问题:
class HumanInformation():
def __init__(self):
self.info = {"name":"none"}
self.info["age"]=0
self.info["school"]="none"
self.info["feeling"]="none"
self.info["food"]="none"
self.info["book"]="none"
self.info["subject"]="none"
self.info["song"]="none"
self.info["teeth"]="none"
self.info["jokes"]="none"
# stuff is random information that we use to get more information and have the human answer questions
# these are aimed at 3-7 year olds
self.info["stuff"]="none"
self.stuff = ["the color pink", "singing", "dancing", "dinosaurs", "race cars", "building things",
"robots", "airplanes", "space ships", "unicorns", "princesses"] self.points = self.info
# setup points scoring scheme
points = 20
for item in self.points:
self.points[item]=points
points -= 2
现在,在下一节中,我们将为我们的机器人创建一个记忆,以便机器人可以记住它已经学到的内容。我们需要一个地方来存放我们收到的答案。
环境记忆
这段代码构建了机器人的环境记忆。你可以将其视为机器人可以记住的信息池。我们设置机器人的情感和人类情感模型都为中性。我创建了一些数据结构,以便我们可以通过将self.currentHuman
指向我们之前创建的HumanInformation
数据对象来引用多个人类用户。
这是我们让机器人记住它处于何种情绪状态的地方,以及软件感知的人类情绪状态,这样机器人就不会突然忘记它已经做出的决定。在下面的代码片段中,我们定义了self.emotion
,这是机器人的内部状态,以及humanEmotion
,这是我们正在与之互动的人。然后,我们使用这些结构创建一个写入磁盘的文件,这样即使关闭机器人,它的个性也能持续存在。我们使用inContext
函数从上下文池中检索数据。如果没有可用数据,我们返回一个整数为0
:
class ContextMemory():
def __init__(self):
self.currentContext = "None"
self.currentHuman = None # pointer to the data file for the human we are currentl talking to
self.humanFile = []
self.emotion = "happy"
self.humanEmotion = "happy"
self.contextDict={}
self.contextDict['currentHuman'] = self.currentHuman
self.contextDict['robotEmotion'] = self.emotion
self.contextDict['humanEmotion'] = self.humanEmotion
def inContext(self, datum):
if datum in self.contextDict:
return self.contextDict[datum]
else:
return 0
def setHuman(self,human):
self.currentHuman = human
self.humanFile.append(human) # add this person to the database of people we know
def addHuman(self,human):
self.humanFile.append(human)
在这些部分中,我们为机器人创建了一个存储它当前感受、机器人处于何种情绪状态以及它对所交谈的人类了解的信息的地方。我们还有一个结构,以便在我们遇到新人时将新人添加到我们的数据库中。
摘要
这是非常繁忙的一章。我们为我们的机器人创建了一个情感引擎,并创建了一个对话界面,以便它能与人类互动。我们介绍了模拟概念,因为我们正在为我们的机器人创建情感模拟。我们无法创造真实的、由生物学动机驱动的情感,所以我们通过玩游戏来模拟情感。在这个游戏中,我们试图让人类提供关于他们自己的信息,这就是我们人类所说的“闲聊”。我们为情感分配了分数,并为机器人的内部“感受”以及机器人通过互动感知你的情绪的方式创建了一个情感状态机。
在我们的最后一章中,我们将讨论你的机器人教育之旅,提供一些关于机器人职业的指导,并讨论未来,这是一个总是充满危险的话题。
问题
-
你最喜欢的电影机器人是什么?你会如何描述它的个性?
-
电影制作者使用了哪些技巧来表达那个机器人的个性(肢体语言、声音等等)?
-
有哪两种类型的聊天机器人?列出每种的优缺点。
-
在自定义分布建模的图示(机场示例)中,底部图像显示了两种标准分布和两种均匀分布。为什么曲线没有延伸到图表的顶部?
-
设计你自己的机器人情感。选择六种对比鲜明的情感,以表达你机器人个性的全部范围。你为什么选择这些情感?
进一步阅读
-
罗莎琳德·皮卡德的《情感计算》: https://direct.mit.edu/books/book/4296/Affective-Computing。这本书是情感计算领域的奠基性文本,这对于在人工智能中模拟情感直接相关。
-
《社交机器人的情感建模》由安娜·帕瓦、伊奥兰达·莱特和蒂亚戈·里贝罗著:https://people.ict.usc.edu/~gratch/CSCI534/Readings/ACII-Handbook-Robots.pdf. 本书包含专注于在机器人中实现情感反应的研究论文和研究成果。
-
《设计社交机器人》由辛西娅·布雷泽尔著:https://direct.mit.edu/books/book/2309/Designing-Sociable-Robots. 这是了解机器人中情感和社会动态整合的关键资源。
第十章:结论和反思
在这本书的整个旅程中,我学到了很多,我希望你也一样。我有机会重新审视我对机器人的热爱,并花了很多时间研究人工智能和机器人设计的最新进展,试图以易于理解的形式向你解释这些概念。
在本章中,我们将讨论完成我们的机器人。我将为你提供一些关于人工智能和机器人领域职业的建议。我们还将简要谈谈人工智能的未来,至少就我所见而言,并以关于风险的话题结束。
本章将涵盖以下主要内容:
-
学习何时停止
-
机器人领域的职业
-
探索人工智能的当前状态
-
理解人工智能中的风险
学习何时停止
在过去的九个章节中,我们一直在设计一个特定的机器人来完成特定的任务。我们设计了名为阿尔伯特的机器人,使其能够在非结构化环境中捡起玩具,即家庭环境。为此,它需要能够通过摄像头识别玩具,用机器人的手臂和手捡起玩具,在屋内导航,并将玩具放入玩具箱中。我们还增加了交互功能,教会机器人听从并反应命令。最后,它获得了一个人工性格和模拟的情感。
那么,下一个问题是,我们是否完成了机器人的设计、构建和测试?有时,设计和构建机器人的最困难的部分是确定何时才算完成。我经常看到,有些小的地方可能需要改进,或者添加一些细节,或者增强一些功能。哦,机器人需要一盏聚光灯。如果它能记住玩具箱里有什么玩具那就好了。如果它有两个手臂会怎样?等等。你可以永远这样继续下去,修补和添加,永远不会真正完成任何事情。
解决这个问题的方法是设定具体的目标并与之衡量。例如,我们希望机器人能捡起玩具。多少玩具?地上所有的玩具。因此,我们可以进行多次试验,如果在每个测试结束时所有玩具都被捡起,那么任务就算完成了。但如果不是所有玩具都被捡起呢?什么程度算足够好?我们可以说,如果我们能一次性用手握住所有剩余的玩具,那么这是可以接受的。什么水平是不可以接受的?好吧,一个玩具都不捡起是不可以接受的。一半呢?我们会认为那可以吗?我会说可能不会。你可以继续这种自我对话,直到你清晰地界定终点在哪里。然后,一旦你越过了这条线,你就完成了。
有时候,当我们尝试新事物或创新时,我们必须调整期望。预计你需要花一些时间来消除错误。我有过这样的经历,创造了一些东西,然后机器人做了些新的、出乎意料的事情,结果比最初的好得多。例如,我创造了一个具有相当复杂的跟随我功能机器人。它使用人体识别来识别人类,然后,当被命令时,会跟在一个人后面,即使是在人群中——只要它总能保持在它视野中跟踪的人。机器人被编程为与人的距离保持六英尺。这意味着如果你朝它走去,而不是离开,它会后退。然后你可以通过朝它走去简单地控制机器人向后移动——我的跟随我功能变成了向前走功能。
有时候,一项创新可能根本不起作用,在某个时刻,它就必须被放弃。通常,当你不得不一次又一次地添加拐杖或解决方案时,你就能知道这一点。每一次解决方案都会使软件变得更加复杂和脆弱。我有一个用声纳避障系统的问题——我使用的声纳传感器太不可靠了,对表面的反应非常敏感——例如,它无法从抛光的木门上看到(获取任何回声)。经过几周的测试后,我们放弃了那个传感器,转而使用另一个名为红外接近探测器(IRPD)的光学传感器,它工作得更加可靠。
在下一节中,我们将探讨一些如果你对机器人或人工智能感兴趣可以走的职业道路。
机器人领域的职业
我经常被问及机器人设计师需要具备哪些技能或学位,或者他们应该上哪些课程。我在机器人竞赛、学生活动、会议和招聘之旅中遇到了很多年轻人。我给很多人的建议已经包含在这本书里了——特别是现在人工智能、神经网络、图形处理单元(GPU)、专家系统、聊天机器人、导航和图像处理都变得非常重要。你需要理解机器人是一个涵盖众多行业和技能集的宽泛术语。
谁使用机器人?应用范围每天都在不断扩大。以下是一些例子:
-
在医疗领域,有机器人辅助手术、机器人假肢、外骨骼帮助截瘫者行走,以及植入式设备帮助人们听和看。
-
在交通领域,我们有自动驾驶汽车、自动驾驶飞机和直升机(这就是我从事的工作)。
-
我们在旧金山的街道上使用机器人递送包裹,一些公司在非洲、瑞士、德克萨斯州和其他地方测试通过无人机递送包裹。
-
我们在几个国家测试着自动驾驶汽车。在美国的DARPA 大挑战机器人汽车比赛中首次亮相的安全功能,以及为自动驾驶汽车开发的功能——车道保持、自适应巡航控制、驾驶员辅助和自动泊车——现在甚至是最基础的汽车上也已成为常见功能。目前有超过80家公司正在开发某种形式的电动垂直起降(VTOL)载人车辆,每一款都将其作为控制系统的一部分使用先进的自动化和自主技术。在澳大利亚的远西部地区,力拓矿业在皮尔巴拉开发了未来矿山,在那里,80辆自动驾驶卡车从1500公里外的珀斯远程操作。
机器人技术的未来正在被书写,而你,正在阅读这本书,可能将在决定其走向中扮演一部分角色。
那么,设计并制造像我刚才描述的机器人需要哪些技能呢?事实是,一家现代机器人公司几乎会雇佣所有可想象到的技能。让我们更仔细地看看其中的一些技能:
-
即使是最简单的机器人也需要机械设计师来开发零部件、齿轮和杠杆,使机器人移动并帮助包装电子产品。
-
电气工程师与电池和电机工作。
-
射频(RF)工程师和技术人员使用无线电和数据链路,这些用于将移动机器人连接到其控制站(如无人机****车辆(UAV))。
-
认知专家设计人工智能程序,开发机器人情感,运用机器学习技术,以及设计用户界面。
-
作家和艺术家制作语音程序,编写对话,设计用户界面,编写手册和文档,并为机器人的内外部增添创意。
-
经理和监督员跟踪预算和进度。
-
供应链专家与供应商、零部件、按图生产车间、电子产品仓库和销售人员合作,以确保零部件到位,组装生产线。
-
工业机器人由特殊类型的程序员管理,他们使用可编程逻辑阵列(PLAs)和梯形逻辑来控制涂漆和组装组件的机器人手臂。这种编程模拟了机器人最初使用继电器和开关设计的原理。
-
簿记员和会计师确保账单以及员工得到支付。
-
销售人员、市场营销和客户关系团队负责产品的销售并保持客户满意。
所有这些技能在专业机器人工程师身上都必须以某种形式存在,尤其是如果你认为自己将经营自己的公司。我曾参与过各种规模的机器人项目,从一个人到数千人。每个项目都有其优势和劣势,但作为一个机器人设计师,你可以确信你将是风暴的中心,让魔法发生,解决问题,将想法转化为实物。对我来说,没有比看到我的创造物在驾驶或飞行中,完成其工作,并知道所有这些辛勤的工作、研究、编码、机械、不眠之夜、手指和脚趾的挫伤,以及跳过的餐食都值得这个结果更令人满足的时刻了。
现在,让我们谈谈围绕机器人和人工智能的新闻中的一些炒作。
探索人工智能的当前状态
目前,在人工智能和机器人技术的交汇世界中,炒作很多。而且其中很多都是夸张的。
一个常见的误解是机器人正在夺走人们的就业机会。实际上,机器人和自动化让工人能够从事更多富有成效的任务。这一真相可以从就业统计数据中看出——尽管工厂自动化取得了巨大进步,但美国的失业率处于50年来的最低水平(https://www.wsj.com/articles/january-jobs-report-unemployment-rate-economy-growth-2023-11675374490)。然而,根据《哈佛商业评论》,机器人技术的提高生产率创造了比它消除的更多的工作机会(https://hbr.org/2021/03/why-robots-wont-steal-your-job)。由于自动化和生产率的提高,就业的整体水平是增加的,而不是下降。
我确实认识到,现代工人,甚至像我这样的从事技术工作的人,必须随时准备并愿意——无论年龄多大——重新培训自己,学习并适应新的工作方式、新的经济和新的机遇。每当有新的市场和新技术出现时,我就不得不至少重新培训自己六次。有时,会出现一个第二波,其中一些技术被发明出来,但由于其提供的效益过高或适当的硬件尚未发明,这些技术随后消失了。神经网络就属于这一类,虚拟现实也是如此,它在1999年是一个大事件,而现在随着为手机开发的小型高分辨率屏幕的出现而重新出现。
展望人工智能和机器人技术
我对所谓的共享经济的长期影响非常感兴趣,这种经济模式中,像Uber、Lyft和Airbnb这样的公司通过在巨大规模上连接供应商和消费者来创造价值,而不拥有提供任何服务的资本或资源。所有这一切都得益于无处不在的互联网,它以快速的速度持续增长和演变。互联网的可用性使得公众或单个学生能够访问超级计算机级别的能力,在线运行ChatGPT等AI程序,这些程序太大,无法装入家用电脑、智能手机或平板电脑。我经常在提到一些可能只有24个月历史的想法时使用“但那在互联网年份中相当于十年”这个术语,以表明互联网技术的快速更迭。这一趋势将持续下去。将很有趣地看到20年后是否还有人拥有汽车,或者只有汽车服务的订阅。
另一个变得非常有趣的趋势是许多行业中进入门槛的降低。过去,你需要一个庞大的机器车间和巨型机器来制造精密的机械部件——直到3D打印机出现,将这种能力带到了你的桌面。生成式AI是一种能够合成写作和绘画,并能直接创作音乐、编写程序和软件、提供建议,以及仅通过文本提示帮助用户编写脚本、绘制图片和制作动画的AI。你想制作电影吗?你可以在iPhone上做到。你想开设录音室吗?专业结果所需的部件(需要大量努力)的价格不到200美元,你还可以使用AI生成歌词、和弦进行、琶音,甚至歌曲创意。
符合降低进入门槛这一类别的项目之一是无人机或小型无人机。当我开始制作无人机时,一个不错的全球定位系统(GPS)和惯性测量单元(IMU)——使不稳定的四旋翼无人机能够控制的东西——需要花费数万美元到数十万美元。无人机技术的真正突破并非来自航空领域,而是来自我们的手机。手机的发展使得公司能够投资数十亿美元来制造下一代手机、智能手机、手持电脑安抚器,或者你想要称之为什么的东西。非常小的无线电、非常小的GPS和非常非常小的加速度计的融合,使得一个无人驾驶飞行物的世界——四旋翼无人机、滑翔机、飞艇、飞机和混合垂直起降(VTOL)飞行器——得以出现。这,加上来自(你猜对了)手机和笔记本电脑的高密度电池,使得人们发现,如果你给它足够的动力,几乎任何东西都能飞起来,包括你自己。
飞行四旋翼无人机取得惊人成功的秘诀在于,那些测量运动变化的微型加速度计(加速度计)和测量方向变化的微型陀螺仪(陀螺仪)变得既便宜又容易获得。没有这些传感器,以及控制它们的机器人算法,四旋翼无人机将是不稳定的,无法控制。四旋翼无人机成功的另一个原因是,它仅使用油门设置——即电机的速度——来控制其所有飞行方面的稳定性。这与使直升机工作的非常复杂的集体控制和循环俯仰控制形成对比。你可以看到无线电遥控直升机和四旋翼无人机的区别,前者非常昂贵,只有少数人能够驾驶,而后者相当便宜,任何人都可以驾驶,只要有计算机和一些传感器。你可以在集体/循环无线电遥控直升机上添加无人机自动驾驶仪,最终得到一个非常可控的无人机直升机。四旋翼无人机和更复杂的飞行器使用人工智能进行稳定、自适应飞行控制、物体识别和避障。
人工智能和机器人技术的这些进步也导致了一些人的反感和机器人恐惧症。
人工智能恐惧症合理吗?
你可能在网上看到过来自各种非常可信来源的令人震惊的标题,说了一些令人难以置信的事情。
大约十年前,著名科学家斯蒂芬·霍金表示,“完全人工智能的发展可能会给人类带来末日...它将自行起飞并以越来越快的速度自我重新设计...受限于缓慢的生物进化的人类,无法与之竞争,将被取代” (https://www.bbc.com/news/technology-30290540)。这句话至今仍被人工智能的批评者频繁引用。
最近,埃隆·马斯克表示,人工智能可能导致文明毁灭,尽管他已经在人工智能的增长上投入了大量资金 (https://edition.cnn.com/2023/04/17/tech/elon-musk-ai-warning-tucker-carlson/index.html)。
微软前董事长比尔·盖茨持更中庸的观点,他认为人工智能既带来了希望,也带来了担忧。在一封公开信中,他详细阐述了人工智能的潜力,但也讨论了发展这项技术的风险。他写道,“世界需要建立交通规则,以确保人工智能的任何负面影响都远远小于其 好处” (https://www.forbes.com/sites/qai/2023/03/24/bill-gatess-open-letter-suggests-ais-potential-is-both-exciting-and-terrifying/)。
首先,让我明确一点,我认为自己并不与这些先生们处于同一水平。但我是这样的人,每天都在人工智能、无人车辆和机器人技术的最前沿工作,并且参加并发表了许多人工智能会议。这种对人工智能的恐惧可以在任何浴室里找到,挂在墙上——我们正在用我们的镜子将我们的动机和欲望反映到我们的创造物上。
我会把目前人工智能的先进状态比作一个成功将V-8引擎安装在滑板上的青少年——拥有巨大的力量,却无法使用。虽然计算机、硬件和网络以惊人的速度发展和扩展,但我们今天构建软件的方式与20年前相同,许多人工智能的进步都是在40或50年前发明的。例如,神经网络是在1940年代发明的(https://link.springer.com/article/10.1007/BF02478259)。遗传算法是在1950年代由艾伦·图灵提出的。现在,由于我们拥有大量的数据(感谢互联网)和大量的带宽,我们能够在应用学习理论方面取得一些进步,但我们并没有创造出任何令人震惊的新概念或能力,显示出任何人工智能突破的迹象。最近在生成式人工智能方面的工作——如ChatGPT和Dall-E图像生成——创造了一种可以描述为计算机版的想象力(从噪声中逐步创建数据),这确实是一个进步,但仍然,像ChatGPT这样的生成式人工智能所做的,与神经网络之前所做的一样:预测句子中的下一个单词。
什么是生成式人工智能?在这种情况下,我们可以向人工智能本身寻求定义。以下文本是由名为ChatGPT4的人工智能程序根据提示“你能提供生成式人工智能的定义吗?”所撰写的:
“生成式人工智能是指一类可以从其训练数据中学习到的模式和信息生成新内容的人工智能技术,例如文本、图像、音乐或代码。这类人工智能使用机器学习算法,特别是生成模型,如生成对抗网络(GANs)或转换器,来理解和复制人类生成内容的复杂性。”
“‘生成’这一方面意味着人工智能不仅仅是分析或处理现有信息,而是实际上在创造一些新东西,以前从未见过。例如,在文本的情况下,生成式人工智能可以撰写文章、诗歌或代码;对于图像,它可以创作艺术品或不存在事物的逼真照片;在音乐中,它可以创作各种风格的新作品。”
但这难道就是所说的通用人工智能(具有人类智能的软件)吗?我不这么认为。尽管硬件和软件在进步,但我看不到任何通往那种类型问题的道路,正如尊敬的盖茨先生、马斯克先生或霍金博士所提出的。为什么没有呢?我们将在下一节中关注这一点。
比较人类和人工智能的需求
我看不到重大问题的原因是因为机器人没有需求。人类有生存和抱负的需求。我们被脆弱的肉体细胞包裹着,威廉·巴勒斯称之为软机器。我们必须提供空气、食物、水、住所和衣物来保护我们脆弱的外壳,并与其他软机器(人)互动以繁殖和创造更多自己。你可以像理查德·道金斯在他的书《自私的基因》中所做的那样争论,所有这些都是我们DNA延续的一种进化方式,而我们只是我们生物编程的产物。人类与需求是不可分割的——如果我们不这么做,我们几分钟就会死去。是我们的需求推动我们前进,走出树木,学会耕种,建造城市,并创造文明。
相反,机器人作为其存在条件并不需要需求。它们只是我们在电子设备中设定的指令集——头脑中有文字的哥莱姆,这些文字使它们移动(如特里·普拉切特在《粘土之足》一书中所描述的)。如果我们不给他们提供食物——什么也不会发生。如果我们不使用它们——什么也不会发生。如果我们忘记它们一周,然后稍后检查,它们仍然是相同的。
首先,让我们讨论一下人类的需求。马斯洛在1943年提出了需求层次理论,并且自那以后一直被引用。马斯洛说,我们不仅有需求,而且它们形成了一个层次——越重要的需求在底部,越抽象的需求在顶部。只有当所有低于这个级别的需求都得到满足时,我们才会担心这个级别的需求,如下面的图所示:
图10.1 – 人类需求层次结构
让我们详细看看这个需求金字塔:
-
在底部是生理需求——空气、食物、水和衣物。
-
接下来是安全需求——我们需要感到从捕食者或其他想要伤害我们的人类那里得到保护。
-
在安全需求之上是社交需求——想要成为群体的一部分或社会的一部分。人类想要属于一个家庭、一个社区、一个国家。这种驱动力非常强烈,正如我们发明战争来保护我们的社会所证明的那样。
-
接下来是自我实现需求——被认可、特殊、与众不同的需求,从我们努力想要成为其中一员的人群中脱颖而出。记住,只有当所有其他需求都得到满足后,我们才能表达这种需求,所以你只有在成为群体的一部分后才会担心认可。
-
我们最终的需求被称为自我实现,正如马斯洛所描述的——我们可能会称之为自我提升,或提高自己的驱动力。这就是我们得到运动员、艺术家、音乐家和写书的人的地方。
注
我和我妻子之间有一个持续的笑话,那就是我们在大学里读过的每一本教科书都包含对马斯洛及其需求层次理论的引用。这相当不寻常,因为我学习的是数学和工程,而我妻子的学位是人力资源管理。我很欣赏这本书为那个引用马斯洛的书籍列表增添的讽刺意味。
现在,让我们来看看机器智能,并想象一下一组机器人需求可能是什么样子。我发现这是一个有趣的思想实验——我们制造了一个能够学习任何婴儿人类(或婴儿老鼠,或婴儿蟋蟀)能学会的东西的婴儿机器人。它会有什么需求?让我们看看马斯洛需求层次的一个修改版:
图10.2 – 机器人的需求层次
我们可以这样分解:
-
与机器人的需求相比,饥饿是嵌入在人类生物学中的。然而,在AI系统的案例中,我们,作为创造者,需要将其构建进去。这相当于我们在人工个性中所做的那样,即电能或电池寿命。
-
下一个需求层次将是AI被创造出的目标和任务。
-
更高层次的需求可能是好奇心和探索的需求——我们的AI系统会有获取更多数据或获取更多资源的驱动力。一旦机器人有了数据,这就会给它提供获取更多数据的基础,如此类推。
-
我们将赋予我们的AI的下一个需求层次是友谊或沟通的需求,无论是与其他机器人还是与人。
-
最后,我们可以给我们的机器人赋予学习的需求,或者说是获取和学习新技能和技术——作为机器人成长。
你可能已经注意到,我们没有在这本书中涵盖这些主题,也没有涵盖其他任何主题。我们没有谈论赋予机器人需求,只是如何模拟情感和机器人完全不懂的对话规则。它不会因为告诉一个7岁的孩子一个糟糕的笑话而感到快乐,因为它不知道这些概念中的任何一种。它只是因为我们的指令而将电子发送到一条路径或另一条路径。这里的唯一智能是我们,机器人设计师的反思和想象力。其他一切都是幻觉——魔术师的把戏。
我经常遇到这类问题,并觉得如果这能帮助你抵御AI批评者的攻击,我可以给你一些我的答案。底线是,我根本不担心AI会接管世界。我的意思并不是说永远不会有通用AI,只是我看不出在可预见的未来会有一个出现。
在有了这个背景之后,让我们讨论如何理解和管理AI中的风险。
理解AI中的风险
我在会议和出版物中经常讨论的一个主题是AI在信任和控制方面的风险。我这里不是在谈论AI失控,而是在讨论如何使AI可靠。非常有趣的是,我们一直在考虑的这种AI——特别是人工神经网络(ANNs)——做了一些其他计算机软件很少做的事情。给定相同的输入和条件,AI系统的输出并不总是相同的。给定相同的输入,AI系统有时会得出不同的答案。这种行为的形式名称是非确定性。
这还有第二个推论。给定相同的输入,AI过程有时会花费不同的时间来完成其任务。这对计算机来说根本不是正常的行为。
虽然如此,我们并不是用AI来解答像2+2这样的数学问题,而是如何去做诸如诊断癌症肿瘤或为自动驾驶汽车识别人行横道上的行人这类事情。我们如何处理可能错误的计算机输出呢?你可以亲自验证这一点——看看我们在进行神经网络训练时覆盖的例子。我们是否曾经在一次训练运行中实现了100%的成功,即所有答案都正确?没有,一次都没有。这是因为人工神经网络(ANNs)是通用逼近函数,它们将输入映射到输出——这些输入可能相当复杂。它们通过处理概率和平均值来实现这一点,这些都是在时间中发展起来的。你可以把一个人工神经元想象成一个概率引擎,它会说:“在过去的50次中,有45次我得到了这组输入,输出应该是正确的。这次它也有可能是正确的。”然后它就设定为正确。在我们的网络中,我们可能有数百万个这样的小人工神经元,每个都在做类似的计算。最终结果是做出一个关于答案的非常明智的猜测。
对于我们神经网络的大多数应用来说,这种行为是可以接受的。我们在对图片进行分类,如果有一两张分类错误是可以接受的。我们进行一次关于鸭嘴兽的谷歌搜索,结果在100张图片中有一张是鸭嘴兽品牌的网球鞋。这对于谷歌搜索来说是完全可以接受的,但如果我们在做更严肃的事情,比如在自动驾驶汽车中识别行人呢?如果我们错误地识别了100个行人中的一个,而没有避开他们,这是否可以接受?当然不行。这就是为什么,目前,我们不允许AI系统在如此关键的功能中使用。但人们希望以这种方式使用AI——实际上,很多人都在这样做。如果能有一个AI系统识别飞行中的鹅,并告诉你的飞机如何避开它们,那将是极好的。如果能有一个AI系统识别出患者在医院中被误诊,并需要立即关注,那也会很棒。但我们不能做到这一点,除非我们找到了处理AI非确定性和不可靠性的方法。
目前,我们一直在汽车中处理非确定性元素,它们被称为驾驶员。人们普遍认为,绝大多数车祸是由人为因素造成的,这就是为什么我们需要具有更好百分比的自动驾驶汽车。我们如何处理人类驾驶员?让我们看看驾驶员所需的必要标准:
-
我们要求他们达到一定的年龄,这意味着他们已经积累了经验。
-
他们必须通过测试,证明他们在完成任务方面的能力。
-
他们必须通过知识测试来证明他们遵守规则和法规。
-
他们必须定期通过更新执照来重新认证。
-
我们还要求使用安全带和气囊来部分减轻人类驾驶员犯错误的风险,通过减少由此产生的伤害
我们可以将这些类型的标准应用于AI。我们可以要求一定数量的训练案例。我们可以测试并证明一定的能力水平。我们可以提前预测错误或错误的水平,并采取措施减轻这种风险。也许我们可以有两个AI系统——一个用于检测障碍,另一个被训练来识别第一个AI已经犯了一个错误。如果我们有90%的第一个AI是正确的可能性,另一个90%的第二个AI是正确的可能性,那么我们就有90% + (10%的90%) = 99%的避免事故的可能性。
我认为在关键安全应用中使用AI的关键是能够提前预测风险,并提前设计以减轻风险的原因或影响。
摘要
在这本书的最后一章中,我们总结了我们在机器人和AI方面的旅程。我们讨论了机器人作为职业,并讨论了AI机器人作为职业。我提出了一些关于AI未来的问题,既有现实的也有想象的。无人机和自动驾驶汽车是现实的;机器人取代人类工作或接管世界是想象的,至少在我看来是这样。我谈到了机器人和AI没有需求,因此缺乏进化所需的动机、压力甚至能力。最后,我们讨论了AI中的风险以及如何识别它。我希望这些信息能为你在机器人和AI方面的兴趣提供一些指导,并从该领域的从业者那里提供一些内部信息。
现在我们几乎到了这本书的结尾,我想感谢您与我一起走过这段旅程。我希望您在旅途中有所收获,即使只是知道更多的问题去问。我鼓励您深入其中,构建自己的机器人,了解AI,并成为那些将机器人作为爱好或职业贡献的人的社区的一部分。
我必须承认,我非常感激所有机器人学和人工智能开源社区,因为它们使所有这些材料、知识和专业知识变得可用,并且继续使人工智能成为开源作为人类知识进步模式的成功例证。ROS(完全由志愿者运营)就是一个很好的例子,因为它使构建机器人变得容易得多。
问题
-
考虑到我们以轻松的语气开始本章,最终讨论了机器人恐惧和关于存在的哲学问题,你认为AI是一个威胁吗?为什么或为什么不?
-
列出五个必要的职业,以将我们的Albert机器人转变为产品公司。
-
如果我们想象一家公司打算将Albert机器人投入生产,它需要心理学家吗?是为了机器人,还是为了人类?
-
在手机或智能手机中发现的哪些组件也出现在四旋翼飞行器中?
-
为什么AI系统,特别是ANNs,在结果和时间上自然是非确定性的?
-
一个可预测地犯错的AI系统可能有哪些实际应用?
-
如果一个AI系统为你挑选股票,并且43%的时间预测到赢家股票,然后你有一个第二个AI,它在确定第一个AI没有挑选到好股票时80%准确,那么AI组合在多少时间内会挑选到盈利的股票?
进一步阅读
-
《行为组织》,由 Donald Hebb,Wiley 出版。
-
《计算机机制与智能》,由 Alan M. Turing 在《Mind》杂志上发表。第LIX卷(238)。
-
《粘土之足》,作者 Terry Pratchett,由 HarperCollins 出版,伦敦 2009年。这本书讨论了虚构的哥莱姆概念,它们是由写在纸上的指令编程的粘土生物,并将其放入它们的头脑中,这是机器人的一种有趣的类比。
-
《人类动机理论》,由 A.H. Maslow 在《Psychological Review》杂志上发表,第50卷(4)。
-
美国交通部。全国机动车事故原因调查。https://crashstats.nhtsa.dot.gov/Api/Public/ViewPublication/811059。
-
《攻击错误推理:无谬误论证的实用指南》,作者 T. Edward Damer,Cengage Learning。
-
《使用ChatGPT和OpenAI模型的现代生成式人工智能》,作者 Valentina Alito,Packt Publishing。
答案
第1章
-
PID这个缩写代表什么?这被认为是AI软件方法吗?
PID代表比例、积分、微分,是一种不需要模型(模拟)即可运行的闭环控制器。PID不是AI方法,因为在决策过程中没有涉及学习或适应。PID是非常有用的控制技术,并且广泛用于控制电机和恒温器。
-
图灵测试是什么?你感觉这是评估人工智能系统的有效方法吗?
图灵测试,最初由艾伦·图灵命名为模仿游戏,是一个想象中的测试或思想实验,其中一个人通过电传打字机(或文本消息,对年轻人来说)与某人或某物进行交流。如果一个人无法判断他们正在与之交流的是人类还是机器人,那么人工智能就会通过图灵测试。现代基于人工智能的聊天机器人和生成式人工智能引擎(如ChatGPT)已经通过了图灵测试,并且正在创建新的智能测试(https://www.nature.com/articles/d41586-023-02361-7)。
-
你认为机器人通常为什么会有负向障碍物(如楼梯和坑洼)的问题?
使用机器人的传感器很难看到负向障碍物(洞、悬崖、下行的楼梯等),这些传感器更容易处理正向(上升)障碍物。通常,由于底部被遮挡(不可见),摄像头和激光雷达只能看到负向障碍物的一部分。有时,通过看到它们的影子——传感器无法看到的部分——更容易对负向障碍物进行推理。以下图示显示了机器人如何感知一个洞:
图11.1 – 机器人如何看待洞或负向障碍物
以下图示显示了机器人如何感知楼梯:
图11.2 – 机器人如何看待楼梯
-
在OODA循环中,定位步骤做什么?
在定位步骤中,所有数据都被放入同一个参考框架中,这通常是机器人的中心视角点。这允许机器人确定哪些数据与决策相关。这是决策过程中最重要的步骤。
-
从Python优势的讨论中,计算以下内容:你有一个需要测试50次更改的程序。假设每次更改都需要一次运行和重新编译步骤来测试。C语言的Make编译和构建需要450秒,而Python的
run
命令需要3秒。你坐等C编译器的时间有多长?使用Python作为解释性语言可以在非常复杂的构建上节省大量时间,其中C/C++编译器和链接可能需要20分钟或更长时间。所讨论的C程序测试周期需要6.25小时才能完成,而Python测试程序只需要2.5分钟。
-
RTOS代表什么?
RTOS代表实时操作系统。这是一个在操作系统内部强制执行时间限制和处理分区的操作系统。RTOS是一个确定性的操作系统,它总是以相同的时间执行一个任务。
-
你的机器人有以下预定任务:遥测频率为10 Hz,GPS频率为5 Hz,惯性测量频率为50 Hz,电机控制频率为20 Hz。你会在什么频率下安排基本任务,以及你会使用什么间隔来安排较慢的任务(例如,10 Hz基本频率,每三帧一次电机控制,每两帧一次遥测等)?
你需要一个所有这些速率(10、5、50、20)都能均匀除尽的数字。适合的最小数字是100 Hz。如果学生假设20 Hz会在一帧中更新两次,在下一帧中更新三次,我会接受50 Hz,这有点作弊,但这是实时系统的一种常见适应方式。
-
假设一个帧率调度器最快的任务为每秒20帧,你将如何安排一个需要以每秒7帧运行的任务?对于每秒3.5帧运行的又如何?
如前所述,为了得到一个恒定的帧率,每个帧中不一定需要有相同数量的样本,只要每个样本都是基本帧率的倍数即可。在这种情况下,20 x 7 = 140,所以7 Hz可以在20 Hz的基本频率下运行,并且每140帧或7秒重复一次模式。7的一半是3.5,可以在相同的基频下运行,模式每70帧重复一次,或3.5秒。
每次更新之间会有5.7帧的距离,这会被四舍五入到6。
-
什么是阻塞调用函数?为什么在像机器人这样的实时系统中使用阻塞调用是坏事?
一个阻塞调用会暂停你的程序执行,直到发生中断或事件,例如接收数据报或UDP数据包。这些调用很糟糕,因为你失去了对程序时序的控制,无法维持软实时执行。对于串行端口和网络接口,请使用轮询调用代替。轮询调用会在接口上查找数据,然后在没有数据可用时继续。
第二章
-
列举三种机器人传感器的类型。
传感器是任何将外部世界的数据传递给机器人的东西。文中提到的传感器包括以下几种:
-
声纳传感器
-
摄像头
-
麦克风
-
按钮
-
模拟-数字电压传感器
-
通过热敏电阻测量温度
-
-
PWM的缩写代表什么?
PWM代表脉冲宽度调制,这是一种数字到模拟的控制方案,其中发送的脉冲根据所需的控制量变长。换句话说,脉冲占空比(开启/关闭的时间)被转换为电压来驱动电机。这通常用于控制直流电机。
-
什么是模拟-数字转换?输入和输出是什么?
正如其名所示,模拟-数字(A2D)转换将模拟值(通常是电压)转换为数字值或数字,这是计算机的数字部分可以理解的。一个典型的应用是测量电池电压以确定充电状态。
-
谁发明了子吸收架构?
如罗德尼·布鲁克斯在《 Cambrian Intelligence: The Early History of the New AI》一书中所述,子吸收架构最初由麻省理工学院教授罗德尼·布鲁克斯博士描述,他后来帮助创立了 iRobot 公司并发明了 Baxter 机器人。罗德尼试图开发昆虫大脑的类似物,以了解如何编程智能机器人。
-
将我的三层子吸收架构图与艾萨克·阿西莫夫提出的机器人三大法则进行比较。是否存在相关性?为什么或为什么不?
不,实际上并不是。艾萨克·阿西莫夫的机器人三大法则是虚构的,而子吸收架构(SA)是一个真实存在的架构,用于制作现实世界中的机器人。
阿西莫夫的三大法则:
-
机器人不会伤害人类,或者通过不作为,让人类受到伤害
-
机器人会服从人类的命令,除非这违反了第一条法则
-
机器人会保护自己免受伤害,除非这违反了前两条法则
让我们来看看 SA 中的三层:
-
SA 的底层是机器人内部的部分,负责处理内部系统——我喜欢将其比作自主神经系统。这保护了机器人。
-
第二层是短期管理者——它告诉机器人去哪里,包括遵守用户的命令。
-
顶层包含战略思考和规划过程。坦白说,相关性较弱。
还有,读者们,记住最后的,或者说零法则:机器人不得伤害人类或让人类受到伤害。这是一项后来的补充。
-
-
你认为我应该给我的机器人项目 Albert 取一个名字吗?你会单独给机器人命名还是按型号命名?
Roombas 作为机器人吸尘器,表现出人们通常与生物实体相关联的特征,如移动和自主导航空间的能力。这种行为可能会引发人类将拟人化或赋予非人类实体类似人类品质的倾向。命名是这种拟人化的自然延伸。
-
ROS_ROOT
环境变量的重要性是什么?最重要的变量是
ROS_ROOT
和ROS_PACKAGE_PATH
。这些变量用于定义 ROS 包和资源的文件系统路径。它们对于 ROS 系统正确定位和使用各种包和资源至关重要。
第 3 章
-
描述一下电影或卡通的分镜脚本与软件程序分镜脚本之间的区别。
电影的分镜脚本不仅用于推进剧情,还用于展示将使用什么视角——换句话说,它用于规划摄像机角度、方向和动作。由于两个分镜脚本的目的都是为了“讲述”发生的事情,它们是相同的。计算机软件分镜脚本的视角应该是用户。
-
五个 W 问题是什么?你能想到任何其他在检查用例时相关的疑问吗?
谁,什么,何时,何地,为什么(为什么是最重要的)。更相关的问题可能包括:做得怎么样?多长时间一次?多少或多少?
-
完成这个句子:用例展示了机器人做什么,但不是 ________。
“它是如何做到的。”用例是从用户的角度出发的,永远不会包括实现细节。
-
在 步骤 9 中取故事板,其中机器人正在驶向玩具箱,并在你自己的故事板中将它分解成更多有序的步骤。考虑在 帧 9 和 帧 10 之间必须发生的一切。
机器人必须执行以下操作:
-
确定通往玩具箱的路线。
-
规划一条路径。
-
在路上避开障碍物。
-
与玩具箱的前端对齐。
-
驶向玩具箱。
-
将机器人手臂移动以清除顶部。
-
-
完成回复“敲门”笑话的表格,其中机器人回答用户告诉的笑话。你认为最后一步是什么?
这是为了补充讲笑话的人 – 机器人应该说“这非常有趣”或“我很抱歉,我无法呻吟”。是的,这是我的观点,而不是官方笑话创作者的想法。你怎么看?
-
看看远程操作操作。你会添加更多吗?或者这看起来像是一个很好的列表?
机器人需要将视频发送回操作员,以便操作员可以看到他们要去哪里。
-
为使用距离测量来防止机器人驶下楼梯的传感器编写规范。
机器人应具有一种传感器,能够在至少六英寸的距离上检测到地面上的负障碍物(例如,向下走的楼梯,阳台)。
-
在哪个距离上,一个320x200像素、30度 视野(FOV)的摄像头可以垂直和水平地看到6英寸的填充动物玩具,假设我们需要35像素进行识别?
要解决这个问题,我们首先需要确定每像素有多少度,然后使用这个值来计算目标的角度尺寸,该目标高度为35像素:
30 度 / 320 像素宽 = 0.0937 度/像素
要找到每像素的度数,我们可以进行以下计算:
35 像素 度/像素 =* 3.28 度
这给我们一个等腰三角形,但我们需要一个直角三角形来进行数学计算。将底边分成两半以形成一个直角三角形;因此,这个三角形的底边现在是 3 英寸。
我们还将角度分成两半:
3.28/2 = 1.64 度
然后,为了计算垂直高度,我们将底边的长度除以 tan 的值:
3 / tan(1.64) = 104.78 英寸
这相当于 8.73 英尺。
这可以通过以下图表说明:
图 11.3 – 计算识别所需的像素
因此,所需距离是 8.73 英尺。
第 4 章
-
我们在本章中学到了很多。你可以使用提供的框架来研究神经网络的特性。调整学习率、批量大小、训练轮数和损失函数。
这是一个学生的练习。你应该看到随着这些参数的变化,会出现不同的曲线。有些可能根本不会产生答案(看起来像随机结果——曲线保持在同一水平,因为没有发生学习)。有些会学得更快或更慢。
-
绘制一个人工神经元的图并标注各部分。查找一个自然的人类生物神经元并比较。
参见章节中的图4**.3。人工神经元有几个输入,一组权重,每个输入一个,一个偏差,一个激活和一个输出集。
-
真实神经元和人工神经元有哪些相同之处?
两者都有多个输入和多个输出,接受输入,执行一些处理,然后输出。两者都使用某种激活函数(生物上的等效物是突触)来确定何时触发或产生输出。两者都是网络的一部分:组成一个神经网络需要很多神经元,同样,动物大脑也是如此。
-
真实神经元和人工神经元有哪些不同之处?
自然神经元是一种模拟设备,它可以处理许多级别的输入,没有像计算机神经元那样的简单开/关二进制表示。神经元使用化学路径,这些路径和连接在使用过程中变得更加容易,这就是神经元的 学习功能。这通过人工神经元中的权重来模拟。自然神经元有一个轴突,或连接体,它延伸到输出,这些输出可以距离神经输入相当远。神经元以随机方式连接到其他神经元,而人工神经元则以规则模式连接。一些神经网络使用dropout层,随机断开神经元,为输出提供一些随机性,这有助于网络估计非线性解。
-
神经网络的第几层与输入有什么关系?
第一层包含网络的输入数量。例如,如果你有五个输入,那么第一层必须包含五个神经元。
-
神经网络的最后一层与输出有什么关系?
ANN的最后一层是输出层,必须与潜在输出的数量相同。
-
查找三种损失函数并描述它们的工作原理。包括均方损失和两种交叉熵损失。
ANN中的损失函数是误差函数,它比较神经元的预期输出与实际输出。让我们详细看看它们:
-
均方损失(MSL):这是最常用的损失函数。它由输出与期望输出之间距离的平方和给出。MSL会放大离期望解越远的错误。
-
交叉熵(XE):这也被称为对数损失,主要用于CNN的分类。随着预测值接近1(无错误),XE缓慢减少。随着值的发散,XE迅速增加。以下有两种类型的交叉熵:
-
二进制(开/关,用于是/否问题)
-
Sigmoid交叉熵,可以处理多个类别
-
-
-
如果你的网络训练到40%并且“卡住”或无法进一步学习,你会改变什么?
你可能过度拟合了,样本大小太小,或者你的网络不够宽或不够深。
第五章
-
在Q学习中,Q代表什么(你需要在互联网上查找这个答案)?
Q学习的起源是伦敦国王学院克里斯托弗·约翰·康尼什·赫拉比·沃特金斯博士的博士论文,1989年5月(https://www.researchgate.net/publication/33784417_Learning_From_Delayed_Rewards)。显然,Q只是代表数量。
-
我们可以做什么来限制Q学习算法必须搜索的状态数量?
只选择与当前状态相关且是后续状态的Q状态。如果某个状态从当前位置或状态无法达到,则不考虑它。
-
改变学习率对学习过程有什么影响?
如果学习率太小,训练可能需要非常长的时间。如果学习率太大,系统不会学习路径,而是过度估计,可能错过最小或最佳解决方案。如果学习率太大,解决方案可能不会收敛或可能突然下降。
-
在Q学习方程中,哪个函数或参数用于惩罚较长的路径?增加或减少这个函数会有什么影响?
折扣因子通过随着路径长度的增加而减少奖励来工作。它通常是一个略小于1.0的值,例如,0.93。提高折扣因子可能导致系统拒绝有效的较长路径,找不到解决方案。如果折扣太小,则路径可能非常长。
-
在遗传算法中,你将如何惩罚较长的路径,以便更偏好较短的路径(步骤数较少)?
你会调整适应度函数,将路径长度作为适应度计算的因子。
-
改变遗传算法中的学习率会有什么影响?学习率的上限和下限是多少?
通常,增加学习率可以缩短每一代的学习时间,直到达到一个路径跳出有效范围的极限。对于我们的示例程序,返回有效解决方案的最低学习率是5,最高值是15。
-
在遗传算法中,降低种群数量会有什么影响?
这使得模拟运行得更快,但需要更多代才能找到解决方案。
第6章
-
做一些互联网研究,了解为什么开源语音助手被命名为Mycroft。你找到了多少故事,哪一个你喜欢?
我至少找到了三个。我最喜欢的是Mycroft是福尔摩斯的哥哥,有些人说更聪明。福尔摩斯在英国的电视上由本尼迪克特·康伯巴奇扮演,他在电影《模仿游戏》中扮演了艾伦·图灵,这是图灵测试的原名,是一种AI对话测试,这正是Mycroft所做的事情。
-
在意图的讨论中,你会如何设计一个神经网络来从自然语言句子中预测命令意图?
一种方法可能是收集一系列命令,标注命令的意图,将命令作为神经网络的输入,并将意图作为训练的输出标签。
-
将“接收敲门笑话”程序重写为记住机器人被告诉的笑话,通过将其添加到“讲敲门笑话”程序使用的笑话数据库中。这是机器学习吗?
将程序添加到仅向“敲门笑话”程序数据库写入是非常简单的。你可以在GitHub仓库中找到这个版本的程序。这是机器学习吗?我会说绝对是!这台机器有了它以前没有的能力。它不需要重新编程来获得新的能力,所以它是通过学习获得的。
-
修改
KnockKnock
(讲敲门笑话的程序)以播放WAV
文件中的声音,例如音乐片段,以及进行文本到语音转换。将这些行添加到
KnockKnock
程序中:"play_wav_cmdline": "paplay %1 --stream-name=mycroft-voice",
你可以用这个来播放音频。你还可以给笑话文件添加一个标签,表示一个
WAV
文件<groan.wav>
。然后,如果你看到这个标签,就调用上面的play_wav_cmdline
函数。 -
本章使用的句子结构都是基于英语语法的。其他语言,如法语和日语,有不同的结构。这会如何改变句子的解析?我们编写的程序能否理解Yoda?
在其他语言中,宾语或主语的出现顺序不同,就像在Yoda的说话模式中一样。“我倒着说,”Yoda会说。这确实要求我们更改或添加新的句子模式到我们的
.voc
文件中。你可以遵循Mycroft的说明,在https://mycroft-ai.gitbook.io/docs/using-mycroft-ai/customizations/languages中将引擎更改为理解法语。
-
你认为Mycroft的意图引擎实际上是在理解意图,还是只是在提取关键词?
我并不将Mycroft归类为建筑AI聊天机器人,而更是一种参考类型,它在数据库中查找答案,这使得它更像是一个专家系统,而不是一个AI程序。它在语音转文本部分确实使用了AI神经网络。
-
描述必要的语音命令,以指导机器人前往一个物体并拾取它,而机器人无法识别该物体。你需要多少个命令?
我们需要两个命令:
-
你能看到 任何物体吗?
-
前往最近的物体
-
-
从问题7中,努力减少命令的数量。你可以消除或合并多少个?
以下是将机器人驱动到最近物体的语音命令:
-
嘿,阿尔伯特
-
前往最近的物体
-
-
从问题7中,涉及了多少个独特的关键词?有多少个非独特的关键词?
四个关键词是看到、物体、前往和最近。
所有的词都是唯一的,除了物体,所以有三个独特的单词。它们在Mycroft单词数据库中未作其他定义。
第7章
-
关于SLAM,哪种传感器最常用于创建SLAM制作地图所需的数据?
光探测与测距(LiDAR)传感器是最常用的SLAM传感器,远远超过其他传感器。LiDAR提供的3D数据非常适合SLAM的映射功能。
-
为什么SLAM在有轮式里程计数据的情况下工作得更好?
轮式里程计减少了SLAM算法在移动后寻找机器人可能位置所需搜索的空间。因此,它增加了地图中的信息并减少了不确定性。它是如何做到这一点的?通过提供额外的关于机器人位置(移动了多远)的测量,然后我们可以将搜索范围缩小到与我们的传感器读数匹配的位置。
-
在Floor Finder算法中,高斯模糊函数是如何改善结果的?
高斯模糊函数减少噪声并去除图像中的散乱的单个像素,从而得到更平滑的结果。
-
Floor Finder的最后一步是从机器人的位置向上追踪到第一个红色像素。这个步骤还可以用其他什么方式完成(参考图7.3)?
而不是使用径向红色线条,程序可以直接从屏幕底部向上绘制一系列垂直线条。
-
为什么我们在进行神经网络处理之前将图像水平切半?
我们只想使用房间上半部分来训练网络,因为下半部分有玩具,并且可能会发生变化。房间上半部分不会因为玩具的增加而改变。
-
使用神经网络方法相较于SLAM技术有哪些优势?
我们不需要地图就能成功导航房间。我们只是通过驾驶机器人并在固定间隔拍照来提供训练集的标记。这种方法对房间中的变化(如家具位置略有不同)也更具弹性。请参阅https://hackaday.com/2021/10/25/fast-indoor-robot-watches-ceiling-lights-instead-of-the-road/,了解其他人实现这一想法的示例,用于室内赛车。
-
如果我们只使用随机驾驶功能(在随机时间进行随机转弯)而不是神经网络,我们还需要在机器人上添加什么新程序或功能才能达到相同的结果?
我们需要一个导航功能来确定我们在房间中的位置——这很可能意味着一个SLAM算法。我们还需要某种东西来检测楼梯。
-
我们是如何在章节中提出的方法中避免楼梯的?你认为这足够吗?你会建议其他方法来完成这项任务吗?
我们训练机器人通过观察房间的上半部分来导航。我们只在安全区域驾驶机器人,并使用这些信息让机器人根据它在房间中的位置预测其下一个行驶命令。由于我们没有在这个过程中让机器人沿着楼梯行驶,机器人将永远不会收到向楼梯行驶的命令。如果楼梯附近有一个玩具,机器人仍然会去捡起它,但在返回导航模式后会避开楼梯行驶。然而,我们必须在让机器人自由行动之前小心地获得良好的训练结果。我使用婴儿门来阻挡楼梯进行早期测试。作为额外的安全措施,我们可以添加一个向下看的传感器来检测楼梯。我会使用红外接近探测器(IRPD)来完成这个目的。
第8章
-
遍历决策树的三种方式是什么?
从开始到结束(起点到目标);从目标到开始;以及同时从两端出发在中途相遇。
-
在鱼骨图示例中,你是如何剪掉决策树的分支的?
通过消除分支上项目的影响。例如,使用我们的“机器人不移动”故障,如果分支说“Arduino无电源”并且你检查Arduino是否有电源并且它确实有,你可以剪掉那个分支。如果分支是“电机卡住”,电机卡住的影响是机器人会绕圈行驶。由于机器人没有绕圈行驶——它根本就没有行驶——你可以剪掉那个分支。
-
Gini系数在创建分类中扮演什么角色?
它决定了样本或池中的 不纯度。当基尼系数 = 0 时,该类别的所有成员具有相同的属性,并且无法进一步细分。这最小化了误分类。基尼系数由 1 减去一个项目属于该类别的概率的平方和给出。
-
在使用基尼系数的玩具分类器示例中,哪些玩具属性没有被决策树使用?为什么?
颜色、噪声、柔软和材料对于通过标签划分类别没有用,因为标签和项目之间没有相关性。颜色对于通过类型划分玩具没有用是有意义的。
-
我们尝试的其中一个分类技术将哪种颜色用于玩具作为标准?
使用基尼指数和独热编码来分离填充玩具的决策树使用了白色。
-
请给出餐厅菜单项的标签编码和独热编码的例子。
让我们有三类菜单项:开胃菜、主菜和甜点。标签编码将开胃菜替换为 0,主菜替换为 1,甜点替换为 2。独热编码将开胃菜表示为 1 0 0,主菜表示为 0 1 0,甜点表示为 0 0 1。
-
在 A* 算法中,讨论
G()
和H()
的不同计算方式。G()
函数是从当前位置到起点的路径距离。H()
是从当前位置直接到目标(欧几里得距离)。请注意,G()
沿路径计算,而H()
是到目标点的直线距离,因为我们还没有计算到目标点的路径。 -
在 A* 算法中,为什么
H()
被认为是启发式,而G()
不是?在 D* 算法中,不使用启发式。为什么?H()
– 到目标点的直接线距离 – 是一个估计值,忽略了任何障碍物,不能直接使用,只是比较一个位置到另一个位置的一种方式。D* 和 A* 之间的一个主要区别是 D* 从目标点开始,反向工作到起点。这允许 D* 知道到目标的确切成本 – 它使用从当前位置到目标点的实际路径距离,而不是像 A* 那样使用启发式方法或距离估计。 -
在 D* 算法中,为什么有
RAISED
和LOWERED
标签而不是仅仅一个CHANGED
标志?RAISED
的方块或点被排除在考虑之外。LOWERED
的方块可能被添加回队列以考虑作为路径。记住,由于新的传感器读数而降低分数会在路径规划器中产生涟漪效应。
第 9 章
-
你最喜欢的电影机器人是什么?你会如何描述它的个性?
这当然是一个主观问题。我是一个大R2D2粉丝。R2既活泼、坚定、固执,又是一个忠诚的伴侣和助手。R2能帮你摆脱困境,修理你的星际战斗机,提供对抗敌对火力的掩护,还能破解帝国的电脑。他就像一个轮子上的瑞士军刀。
-
电影制作者使用了哪些技巧来表达R2D2的性格(肢体语言、声音等)?
R2D2的性格得益于他情感的哔哔声和尖叫声(由本·伯特提供)以及他底盘内的身体动作(肯尼·贝克提供)。他们被固定在为第一部《星球大战》电影设计的并不非常灵活的底盘上,这个底盘只有一个可以移动的头。R2的大部分个性都体现在他的声音上,包括他著名的尖叫声。
-
有哪两种聊天机器人?列出每种的一些优点和缺点。
两种类型的聊天机器人如下:
-
基于检索的聊天机器人:基于检索的聊天机器人会在脚本列表中查找响应,并从人类预先编写的一系列短语中选择。这些聊天机器人的优点是编程简单,可以更好地控制输出,并且程序更小、更快。缺点是响应有限,使用关键词使它们的词汇量很小。
-
生成式聊天机器人:生成式聊天机器人使用语法规则和句子模型来创建具有适当意义的新句子,它们更加灵活,可以处理更广泛的主题,但它们编程起来更困难,且复杂且慢(相对而言)。鉴于ChatGPT和其他生成式AI模型的成功,生成式聊天机器人现在已经占据了主导地位。
-
-
在图9.2中,建模自定义分布(机场示例)的插图,下面的图片显示了两个标准分布和两个均匀分布。为什么曲线没有延伸到图表的顶部?
这两个分布将相加——标准分布位于顶部的均匀分布之上,两者结合后达到图表的顶部。
-
设计你自己的机器人情感:挑选六种对比鲜明的情感,以表达你机器人性格的整个范围。你为什么选择这些情感?
这又是一个主观问题。我的答案在文本中。我选择了代表我的机器人能力和它将遇到的情况的情感。我保持了一个友好的机器人类型,所以唯一的负面情感是悲伤——例如,没有愤怒。
-
如果你设计一个机器人,让它拥有一个讨厌的小男孩的性格(如果你足够老,想想巴特·辛普森、卡特曼或丹尼斯·麻烦),它会有哪些特点?
一个小男孩可能会顽皮,注意力不集中,经常改变话题,不断试图重复同一个话题,并重复相同问题的变体。我们如何表示顽皮呢?或许是通过忽略指令,生成随机事件或干扰,让机器人将注意力从手头的任务上转移开。
-
为什么机器人拥有背景故事或传记很重要?
为了提供对个人问题的连贯答案,例如“你多大了?”
-
对于以下问题,从我的列表中挑选一个角色来模拟(来自《整合人工智能个性》)。
-
为机器人写六行对话,询问人类他们上次度假去了哪里。
-
那么,你上次度假去了哪里?
-
夏天即将来临。你去年去哪里度假了?你喜欢旅行吗?你去过哪里?
-
我从未去过度假。你上次去哪里了?
-
我听说过这个叫做度假的概念。你喜欢去哪里?你去过海滩吗?
-
-
写出六种方法,让机器人表达它感到疲倦并需要充电,同时听起来不像机器人。
-
我累了——你看到我的充电器了吗?
-
哇,时间不早了。我已经做这个很久了。
-
哎,我的电池快没电了。应该是下班时间了。我开始感到有点疲惫。
-
哎,看看时间!我的电池需要充电了。我这里开始饿了。我现在可以去充电吗?
-
第10章
-
既然我们以敲门笑话开始这一章,最后却讨论了机器人恐惧症,并探讨了关于存在的哲学问题,你感觉人工智能是一种威胁吗?为什么或为什么不呢?
我不认为机器人或人工智能在任何方面都是一种威胁,因为机器人成为威胁的必要和充分条件不存在,也就是说,机器人必须想要统治世界,并且必须需要统治。目前,机器人和人工智能没有这样的欲望或需求。
-
列出五个必要的职业,将我们的Albert机器人转变为产品。
我们需要项目经理、包装设计师、广告和营销专家、销售人员、工程师、技术人员、艺术家、包装设计师、机械师、电工、会计师、律师、心理学家以及支持人员等。你可以从这些选项中选出任何五个。
-
为什么在我们的想象中制造机器人的公司需要心理学家?
心理学家研究正常和异常的心理状态以及认知过程,这正是我们试图在人工个性中模拟的内容。我们希望机器人不会在人们身上引发不良反应。我曾经有一个眼睛闪烁着红色的机器人,这让小孩子产生了恐慌症。心理学家会帮助避免这样的错误。
-
在手机或智能手机中发现的哪些组件也出现在四旋翼飞行器中?
GPS接收器、收音机、Wi-Fi、蓝牙、加速度计、陀螺仪,以及如今的应用程序或应用。
-
为什么人工智能系统,特别是人工神经网络,在结果和时间上都是自然非确定性的?
它们是通用的近似系统,在概率和平均值上工作,而不是在离散数字和逻辑上。人工神经网络可以花费不同的时间,因为特定的数据可能在不同时间采取不同的路径,通过不同数量的神经元,因此处理所需的时间不同。确实,如果你向神经网络提供完全相同的输入,它每次都会给出完全相同的答案。然而,在机器人生活的现实世界中,两个输入在所有方面都完全相同的情况可能非常罕见。
-
一个会犯可预测错误的AI系统的实际应用可能是什么?
你可以使用基于神经网络的系统来模拟一个糟糕的人类操作员进行驾驶模拟,以帮助其他驾驶员(和自动驾驶汽车)学习如何避免糟糕的驾驶员。理想状态是难以预测的驾驶员,所以只需训练神经网络到大约60%。现在,网络将有40%的时间给出错误的答案,即统计上可预测。我实际上在一个海军项目的这个项目上做了这件事,海军想要一个模拟不完美的人以可预测的水平误用系统的模拟,这样他们就可以创建一个能够处理这些错误的响应控制系统。
-
如果一个AI系统为你挑选股票,43%的时间预测股票是赢家,而你有一个第二个AI系统,它在确定第一个AI没有选择好股票时80%准确,那么AI组合在多少时间内会选择盈利的股票?
我们有100只由我们的AI程序挑选的股票。在这个集合中,不确定的数量是赢家和输家。有43%的概率正确预测股票是赢家,有57%的概率它是有预测性的输家。我们没有其他方法来判断股票是赢家还是输家,除非投资我们的钱,这正是我们试图避免的——投资坏股票。43%的赢率并不好。
第二个AI有80%的概率告诉你第一个AI选择了不好的股票。在100次中有80次,你会知道股票不是赢家。你剩下80%的概率正确地识别出57只坏股票中的一只,这消除了45只股票。这让你剩下55只股票,其中43只是赢家(平均而言),这提高了你的胜率到78%。
贝叶斯定理显示了两个独立概率的组合(在发生c的情况下x发生的概率):
px = x的概率,pc = c的概率)
p(x|c) = (px pc) / (**pxpc)+(1-px)(1-pc)
使用这个定理,我重新计算了联合概率为75.1%,所以我会接受任何一个答案。
附录
机器人操作系统(ROS)是一个旨在使复杂机器人软件的开发成为可能的框架,由一家名为Willow Garage的公司开发,专门用于控制PR2机器人。PR2是一个人形机器人,有两个7自由度(7DOF)的臂和一套完整的传感器。控制这个非常复杂的机器人需要众多传感器、电机和通信的交互。ROS框架允许独立开发机器人组件。虽然它不是一个传统意义上的操作系统,但它是一个模块化开源架构(MOSA)。
ROS的主要工具是一个强大的发布-订阅服务,它使得进程间的通信——即进程间通信(IPC)——变得简单和灵活。它还标准化了机器人传感器、电机和控制之间的许多接口。
在本书中,我们将使用ROS 2。ROS 2是ROS的新版本。在本附录中,我们将讨论如何安装ROS 2,使用它进行通信,并简要介绍ROS提供的一些工具。我们将深入介绍ROS 2,并描述我们玩具收集机器人Albert设计中所涉及的硬件。我们还将介绍在创建Albert作为本书示例时,书中提到的部分硬件。本书第二版的机器人Albert V2,是我大约第30个机器人设计。
附录中涵盖的主题包括以下内容:
-
介绍MOSA
-
简要概述ROS 2
-
机器人软件需求
-
介绍机器人的硬件
-
机器人安全提示
介绍MOSA
ROS是一个MOSA的例子。为什么这很重要?想象一下,如果你的家里的每个电器都有自己的插头,不同的电压,不同的电线,那会给你生活带来很大的困难。但所有的电器插头形状相同,输出相同的电压。它们是标准化的接口,允许你将许多不同类型的电器插入其中。MOSA对软件来说就像这样,标准化接口,允许即插即用的兼容性。
以下是其优势:
-
MOSA系统架构允许模块化——即能够以部分或模块的形式创建软件,这些模块可以独立开发、调试和运行。在ROS之前,我创建了一个主要的可执行文件,它在我的机器人上运行所有功能。这个问题首先在于,我无法利用我的单板计算机(SBC)的多核特性,这是机器人的大脑。我所有的代码都在一个线程、一个程序中,将函数分割出来独立运行是困难的。
-
然后是交互。如果我改变了电机驱动器的时序,它就会搞乱摄像头的传感器时序。如果我改变了路径规划器,那么转向就需要调整。这种交互在 单元架构 中很典型。然而,在 MOSA 中,机器人的每个部分,例如机器人手臂控制器,都是独立的,并运行在自己的程序中。它们可以独立开发和调试,交互仅限于我们在那些程序中创建的接口。这使我们摆脱了许多我们本会遇到的问题。
-
我们可以利用的另一个特性是 ROS 提供的非常庞大的标准、已创建的接口和程序库。我们不需要创建控制接口;ROS 已经有一个(
Twist
命令)。我们也不需要为摄像头图像数据创建数据类型;ROS 提供了多种选择。ROS 还提供了我们可以使用的 USB 摄像头驱动器和查看器,而无需编写任何代码。
现在让我们简单谈谈 ROS 2 的工作原理。
ROS 2 的简要概述
如前所述,ROS 2 是 ROS 的最新版本,ROS 是一个广泛使用的用于开发机器人应用的框架。我已经使用 ROS 一段时间了,并欣赏它如何使各种组件、传感器和功能集成到我的机器人中变得更加简单。我一度抵制转向 ROS,但现在我已经投入时间学习它能做什么,我无法想象在没有它的帮助下开发机器人。
在本节中,我们将讨论一些对我们理解 ROS 2 基础概念至关重要的概念,如何安装 ROS 2,以及我们可以与之一起使用的某些基本命令。
注意
请使用 ROS 2 的最新版本。
理解基本概念
ROS 2 的工作方式与其他编程范式略有不同。ROS 基于发布/订阅机制,允许不同的程序或进程在事先不知道接收者是谁的情况下相互传递信息。让我们看看这个过程是如何工作的:
-
在 ROS 中进行通信的每个程序或代码都称为 节点。每个节点都有自己的名称,该名称唯一地标识了它在系统中的身份。
-
节点在 主题 上发布数据,这些主题代表消息的接口。
-
消息是具有数据类型(字符串、浮点数、固定点、数组等)的接口。
例如,一个操纵杆接口节点通过 Twist
消息发布到命令速度 (cmd_vel
) 主题。在消息的接收端,电机控制接口订阅 cmd_vel
主题以接收包含速度和转向信息的 Twist 消息。
注意
为什么叫它“Twist”消息?使用“twist”这个术语的原因是概念性的。想象一下机器人的运动是直线(线性)运动和旋转(扭曲或转动)的组合。通过结合线性和角速度,Twist 消息有效地描述了机器人如何在空间中“扭曲”,这包括平移(从一个地方移动到另一个地方)和旋转(改变方向)。
ROS 还有一些其他有用的特性:
-
存在一个系统级的
DEBUG
、INFO
、WARN
、ERROR
或FATAL
。日志消息会自动显示在本地输出(通常是命令行)上。它们被集中收集,你可以解析日志消息来调试问题。定期,你确实需要清除旧日志文件,因为它们确实会堆积起来。日志被放入~/.log
目录中。 -
我个人最喜欢的一个特性是 参数。这些是独立于源代码创建的外部数据值,可以用来开启或关闭功能,或者设置关键设置,例如图像大小、分辨率、范围和其他功能。拥有外部参数的能力在很大程度上使得 ROS 可移植并且接口可重用。参数可以在启动文件中指定,这是一种同时启动多个程序(节点)的方法。Albert 有五个主要子系统(控制、电机驱动、手臂控制、视觉和语音),所有这些子系统都必须一起启动。
比较 ROS 2 和 ROS
ROS 2 在原始 ROS 的基础上引入了一些非常显著的改进。其中一些如下:
-
最显著的改进是 roscore 交通警察 应用程序的缺失。这个中央程序在任何 ROS 实现中充当主节点,并指导其他节点通过套接字进行通信。它指导网络上的所有流量。因此,每个节点都必须与 roscore 通信以了解其运行位置。如果 roscore 死亡或被关闭,那么整个 ROS 应用程序集都将停止。相反,ROS 2 使用 数据分发服务(DDS)作为其 中间件层。DDS 是一个由 对象管理组(OMG)创建并由 DDS 基金会管理的用于高性能、可扩展和实时数据交换的标准。它为节点提供了一个去中心化的发现机制,以便它们可以找到彼此,而无需一个像 roscore 这样的中央主节点。当一个节点启动时,它会向其他节点宣布其存在,并发现现有的节点和主题。这个过程由底层的 DDS 实现管理。
-
ROS 2 具有更多针对 实时处理 的功能,并且没有 ROS 那样的奇怪解决方案,例如将消息向后推迟到未来。这些功能包括使用 DDS(旨在用于实时系统,能够与 实时操作系统(RTOSs)协同工作,并使用抢占来控制进程)。
-
一个重大改进是ROS 2将首次原生运行在Windows上。你不需要虚拟机就可以在Windows上运行ROS - 你可以直接在Windows中构建自己的控制面板和界面。
-
ROS 2还改进了发现(在网络中查找节点的过程)并有一些针对网络安全的增强。
因此,总的来说,ROS 2值得花时间升级,我感觉它比旧的ROS更简单,更容易在Windows和Linux上设置,也更容易管理。
让我们来看看软件需求。
机器人的软件需求
在本节中,我们将讨论机器人的软件需求以及如何在机器人的CPU上安装它们。
安装ROS 2
我在Albert上安装的ROS 2版本是Foxy。请随意使用ROS 2的最新版本。Jetson通常落后于Ubuntu升级,并且落后几个版本。我们使用Jetson Nano是因为它具有运行神经网络软件所需的图形处理单元(GPU)。我的Nano版本正在运行Ubuntu 20.04,但你也应该能够使用Ubuntu版本18.04使其工作。
我使用了可以在https://github.com/jetsonhacks/installROS2找到的标准ROS 2安装脚本。这是一个包含在常规ROS 2安装页面https://docs.ros.org/en/foxy/Installation/Alternatives/Ubuntu-Development-Setup.html上找到的所有步骤的脚本。
注意,我们正在进行一个从源安装的设置,因为许多程序需要重新编译才能在Jetson Nano的ARM架构上运行。对我来说,这个过程大约需要4个小时,所以需要一些耐心。
安装其他包
你还需要安装以下Python包:科学Python(SciPy)、数值Python(NumPy)、scikit-learn、开放计算机视觉(OpenCV)和PyTorch。让我们看看安装这些包所需的命令:
-
SciPy:
python -m pip install scipy
-
NumPy:
pip install numpy
-
scikit-learn:
pip3 install -U scikit-learn
-
OpenCV:
sudo apt-get install python3-opencv
关于OpenCV的更多详细信息,你可以参考https://docs.opencv.org/3.4/d6/d00/tutorial_py_root.html
-
PyTorch:
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
现在我们来看看如何开始使用ROS 2。
基本ROS 2命令
以下命令与在ROS 2中启动、控制和监控节点相关:
-
要在ROS 2中启动执行一个包,请使用以下命令:
turtlesim_node from the turtlesim package in the background.
-
为了检查这个节点是否正在运行,我们输入以下命令:
ros2 node list
这将列出当前正在运行的节点列表:
/turtlesim
-
我们可以通过输入以下命令来获取更多关于节点的信息:
ros2 node info /turtlesim
这将产生以下结果:
/my_turtle Subscribers: /parameter_events: rcl_interfaces/msg/ParameterEvent /turtle1/cmd_vel: geometry_msgs/msg/Twist Publishers: /parameter_events: rcl_interfaces/msg/ParameterEvent /rosout: rcl_interfaces/msg/Log /turtle1/color_sensor: turtlesim/msg/Color /turtle1/pose: turtlesim/msg/Pose
在这里,我们可以注意以下关键点:
-
turtlesim
发布parameter_events
(当更改参数值时发生),它有一个到/rosout
的日志接口,并发布海龟机器人的颜色及其位置(姿态) -
对于输入,它订阅
cmd_vel
,使用Twist
命令移动海龟,并订阅parameter_events
,允许程序接收参数更改
查看主题的命令如下:
ros2 topic list
这显示了活动的主题。对于我们的 turtlesim
示例,我们得到以下输出:
/parameter_events
/rosout
/turtle1/cmd_vel
/turtle1/color_sensor
/turtle1/pose
如果你在该命令末尾添加 -t
,你还可以获得主题消息类型:
/parameter_events [rcl_interfaces/msg/ParameterEvent]
/rosout [rcl_interfaces/msg/Log]
/turtle1/cmd_vel [geometry_msgs/msg/Twist]
/turtle1/color_sensor [turtlesim/msg/Color]
/turtle1/pose [turtlesim/msg/Pose]
你可以在 https://ros2-industrial-workshop.readthedocs.io/en/latest/_source/navigation/ROS2-Turtlebot.html 找到更多细节和完整教程。此外,我建议你参考ROS 2网站提供的教程中的主题列表:https://docs.ros.org/en/foxy/Tutorials.html。这将为你提供一个更全面的ROS 2介绍。本书中运行程序所需的其他内容将在文本中提供。
现在,让我们看看构成阿尔伯特机器人的硬件。
介绍机器人的硬件
我设计了阿尔伯特机器人来执行一项手动任务 – 拿起玩具。因此,我选择了一套电机、一个扬声器和机械臂作为效能部件,以及一个摄像头和麦克风作为传感器。以下是阿尔伯特的外观图示:
图12.1 – 阿尔伯特机器人
在接下来的章节中,我们将探讨我是如何组装阿尔伯特的。
效能部件 – 基座、电机和轮子
机器人基座是一个两层铝合金框架,我在 https://www.amazon.com/gp/product/B093WDD9N5 购买了这个基座。这个基座使用 Mecanum 轮子,它们具有独特的功能,不仅可以向前和向后移动底盘,还可以向侧面和任何角度移动。对于游戏玩家来说,这种侧向移动有时被称为 侧滑。你会注意到轮子上安装了45度角的小滚轮。这些将各种输入转换为多个方向。将所有四个电机向前移动,不出所料,平台就会向前移动。将右轮向前移动,左轮向后移动,就会原地向右转动。
当我们将左轮相互远离,右轮相互靠近时(左前轮向前,右前轮向后,左后轮向后,右后轮向前),车辆就会向侧面 – 侧滑 – 向右移动。反转这些方向,你就可以向左侧滑。这些是我们完成书中练习所需完成的活动。
我对基座进行了一些修改:我切掉了连接到四个电机的白色小连接器,以便我可以将它们连接到电机控制器。
电池
我使用了一块4200mAh的镍氢电池(NiMH)——这里没有锂电池,所以火灾风险更小。输出电压为7.2V。这应该能为我们的机器人提供数小时的运行时间,并且它可以安装在底盘上。你可以使用任何具有相同规格的无人机电池。更大的电池无法安装在底盘上,因为底盘相当小。这块电池完全符合我们的需求。以下是你可以为你的机器人购买的电池示例:https://www.amazon.com/gp/product/B08KXYY53G。
DC/DC电源供应
我最喜欢的配件之一是这款DROK DC/DC可调电源。这块板子为主计算机和Arduino提供5V电源。你可以用一个小螺丝刀调整电压到5V。我真的很喜欢在机器人上显示这个,以表明一切正常。这是我使用的链接:https://www.amazon.com/Converter-DROK-Adjustable-Stabilizer-Protective/dp/B01FQH4M82?th=1。
CPU - 设备的大脑
正如我们在整本书中提到的,阿尔伯特的主要计算机是Nvidia Jetson Nano。这是一款坚固的小型单板计算机,带有大散热片。这款CPU专门设计用于运行AI代码,拥有128个图形处理单元(GPUs)和四个运行在1.43 GHz的Arm A57 中央处理单元(CPUs),并配备了外围端口和外部I/O功能。这是一块非常适合我们运行具有感知和决策能力的机器人的板子。Jetson安装在开发板上,它提供了对其所有功能的接口。Jetson家族中的任何其他成员(TX2、Xavier和AGX)也可以使用,但我将书中代码的大小调整到了Nano上。这是购买Jetson Nano计算机的链接:https://developer.nvidia.com/embedded/jetson-nano-developer-kit。
我已经添加了一张Wi-Fi网卡,它安装在CPU下方。这是我使用的这张:https://www.amazon.com/gp/product/B07SGDRG34。
执行器 - 机器人手臂
我无法过多地称赞这本书中使用的数字伺服手臂(https://www.hiwonder.com.cn/store/learn/42.html)相较于我在第一版书中使用的先前模拟伺服手臂的升级有多大。这款手臂的一些优点如下:
-
这个机械臂使用数字伺服电机,这简化了机械臂的布线,因为我们只需将一个伺服电机插入到下一个串联的伺服电机中。这些伺服电机不是由模拟信号控制,而是具有数字串行接口,它不仅提供了精细的控制,而且能够确定电机实际的位置,而不仅仅是它们被命令到的位置。这为什么很重要?如果机械臂碰到东西而无法继续移动,它将停止。这不是一个惊喜,但当你要求电机位置时,它会告诉你它停止的位置。这意味着你可以使用机械臂本身作为传感器!为了确定地板的位置,你命令机械臂向下移动,当它碰到地板时停止,然后读取电机位置以查看地板相对于机械臂的实际位置。
-
这个机械臂还允许你手动移动机械臂——用你的手——然后读取机械臂的位置,这在设计姿态时非常有用。
-
另一个有用的功能是自编程模式。你可以使用相关按钮将机械臂置于程序模式,然后移动机械臂并按下另一个按钮(标记为运行),程序就会进入机械臂而无需电脑。然后你可以再次按下程序来存储这些动作,然后按下运行,它将回放你所做的一切。这对于测试动作非常有用。我使用它来原型化抓取玩具的位置。
如果你想要控制一个模拟的机械臂而不是真实的机械臂,可以在https://community.arm.com/arm-research/b/articles/posts/do-you-want-to-build-a-robot找到教程。
机械臂控制器
在连接机械臂的注意事项中非常重要——机械臂消耗的电力远超过USB所能支持的。不要在没有连接到电源端口电源的情况下运行机械臂。机械臂可以直接从电池获取电力运行,而不使用DC/DC转换器。我在机器人上添加了一个主电源开关,这样我可以安全地打开和关闭它。如果没有电源开关,唯一关闭机器人的方法就是物理上拔掉电池,这很麻烦。
Arduino 微控制器和电机控制器
Arduino UNO集成在电机控制器中,该控制器插在Arduino的通用输入/输出(GPIO)端口上。这是创建与驱动机器人底座的四个电机计算机接口的最简单方法。电机控制器必须从电池获取电力(同样不经过DC/DC转换器)。Arduino使用脉冲宽度调制(PWM)来控制驱动底座的四个刷式直流电机。电机控制器来自Adafruit,可在以下网站找到:https://www.adafruit.com/product/1438。
传感器 - USB 摄像头
Albert 的主要传感器是具有 USB 接口的宽 视场(FOV)相机。任何数量的相机都可以满足这一需求,包括许多网络摄像头。我的相机具有 170 度的视场和 1,024x768 像素的分辨率。你可以使用分辨率更高的相机,但请确保 USB ROS 相机驱动程序将图像下采样到 1,024x768,这样我们就可以知道其余的软件可以处理带宽。我使用了一个具有 RGB(三种颜色)输出的彩色相机。
传感器和执行器 – 音频接口
我购买了一张 USB 音频卡(https://www.amazon.com/gp/product/B08R95XJW8)来支持机器人的语音输入和输出。这个音频接口有一个麦克风和一些小型扬声器。这个单元使我们能够与语音接口进行语音识别和文本到语音输出的接口。这个单元结构坚固,而且音乐播放效果也很好。
这里是 Albert 组装的一个实用的接线图:
图 12.2 – Albert 机器人的模块化接线图
现在我们已经组装好了 Albert 机器人,我们可以用它来演示书中的例子。Albert 是一个非常通用的平台,功能强大,你很快就会看到。
机器人安全提示
让我们快速看一下与机器人操作相关的安全提示:
-
我们使用的是相当高电流的电池和驱动系统。请务必小心接线,并注意短路,即正负电线接触的地方。在电池和电源之间放置一个大约 10 安培的保险丝以防止意外短路是个不错的主意。
-
当机器人运行时,要小心。它可能会突然改变方向或卡住。我有一个政策,当机器人的电机驱动器激活时,我不会坐下、把手放在口袋里或使用手机。你需要保持注意力集中。
-
注意机器人手臂中的夹紧点——你很容易将手指夹在其中(正如我所学到的)。不要在电源开启时将手指放入关节中。
-
在设置机器人、启动所有软件和打开硬件时,请有一个清单。这将防止你担心遗漏了某个步骤。
-
通常,你应该遵循以下步骤。打开机器人的电源。等待计算机启动。使用 虚拟网络计算(VNC)从你的笔记本电脑或台式机连接到机载计算机。启动机载软件,然后开始发送命令。
-
在充电锂离子电池时要小心;它们可能会起火。最好在金属箱内充电。我之所以在 Albert 原型中使用 NiCad 电池,就是这个原因——没有火灾隐患。锂离子电池更轻、更强,但它们也有自己的缺点,比如起火、成为危险化学物质以及冻结时永久失去电荷。