原文:zh.annas-archive.org/md5/05e803b985c0aa9c73a9374fca010716

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

机器人的定义是一种能够执行类似人类任务的机器。为了执行这些任务,机器人必须能够看到、理解和与环境交互。人工智能是识别物体和导航的最快方式。这本书将赋予你使用诸如卷积神经网络CNNs)、计算机视觉、物体识别、遗传算法和强化学习等人工智能技术高效操作机器人的基本技能。

那么,谁在写这本书?正如你可以从我的传记中看到的那样,我已经做了40多年,从12岁的孩子做科学展览项目开始,然后作为空军列兵、NASA的初级工程师,等等。我于1992年开始从事人工智能,并专注于机器决策。今天,我设计的是重量达数万千克的完整飞行自主车辆。我很高兴能把这些经验写下来与你分享。

我为什么写这本书?我感觉在现有的文献中,对于正在机器人与自主领域崭露头角,需要从业余爱好者向工业和商业机器人初期阶段过渡的人,存在一个空白。在这样做的时候,我希望尽可能地消除我感知到的你和你想要实现的下一个级别机器人探索者之间的障碍。我省略了方程式、奇怪的术语和神秘感,用如何从你的机器人中获得你想要的东西的简单解释来代替。重要的是要记住,我的机器人阿尔伯特只是这个过程中的一个工具。这本书的目标不是设计一个特定的机器人,而是教授一套我认为你需要掌握的技能。真正的问题是接下来你将走向何方。利用这本书作为跳板,继续探索、实验并在机器人领域继续你的教育。从这里,你可以阅读那些启发和/或指导过我的人的作品:罗宾·墨菲博士、塞巴斯蒂安·特伦、罗德尼·布鲁克斯博士、鲍勃·祖布林、罗伯特·L·福沃德博士、艾萨克·阿西莫夫、亚瑟·C·克拉克,以及许多人。

这本书面向的对象

这本书是为那些已经开始了学习机器人知识之旅,并希望通过应用人工智能技术将能力提升到更高级阶段的机器人工程师和爱好者而编写的。对于寻找解决特定问题或进行困难机器人设计实用指南的学生和研究人员来说,它将是一个有用的参考。阅读这本书时,具备基本的Python编程技能、熟悉电子和布线以及能够使用基于Linux的命令行界面CLI)的能力将会很有帮助。

这本书涵盖的内容

第1章机器人学和人工智能的基础,解释了本书将涵盖的内容、标准机器人部件、控制概念、实时计算以及机器人如何做出决策的观察、定位、决策、行动OODA)概念。

第2章设置您的机器人,向您介绍电机、控制系统、如何使用Subsumption架构将机器人问题分解成部分,以及机器人操作系统2ROS 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版本。

在任何地方、任何设备上阅读。从您最喜欢的技术书籍中直接搜索、复制和粘贴代码到您的应用程序中。

优惠远不止这些,您还可以获得独家折扣、新闻通讯以及每天收件箱中的优质免费内容。

按照以下简单步骤获取福利:

  1. 扫描下面的二维码或访问以下链接

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_QR_Free_PDF.jpg

https://packt.link/free-ebook/9781805129592

  1. 提交您的购买证明

  2. 就这些!我们将直接将您的免费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机器人的设计者。

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_01_1.jpg

图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仓库中。

如下两张不同角度拍摄的照片所示,我的机器人配备了新的全向轮、一个六自由度的机械臂和一台电脑大脑:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_01_2.jpg

图1.2 – 阿尔伯特机器人有轮子和机械臂

我会称它为阿尔伯特,因为它需要某种名称,而且我喜欢它对维多利亚女王丈夫阿尔伯特亲王的引用,他因对他们的九个孩子照顾得非常好而闻名。他的九个孩子都长大成人,这在维多利亚时代是罕见的,他还有42个孙子孙女。他以他的中间名为人所知;他的真实名字是弗朗西斯。

本书中的任务主要集中在室内空间捡起玩具,因此我们的机器人有一个坚固的底盘,配备四个电机和全向轮,以便在地毯上行驶。我们的转向方法是坦克式,或差速驱动,通过向轮电机发送不同的命令来进行转向。如果我们想直行,我们将所有四个电机设置为相同的向前速度。如果我们想倒退,我们将两个电机以相同的量反转。转向是通过将一侧向前移动而另一侧向后移动(这使得机器人原地转向)或通过给一侧比另一侧更多的向前驱动来实现。我们可以用这种方式进行任何类型的转向。全向轮还允许我们做一些其他的技巧——我们可以将车轮转向彼此并直接向侧面移动,甚至可以在指向地面上同一位置的同时旋转。我们主要会像卡车或汽车一样驾驶,但偶尔会使用Y轴运动来对齐。说到轴,我会用x轴表示机器人将直线前进,y轴指的是从一侧到另一侧的水平移动,而z轴是上下移动,这是我们机器人手臂所需要的。

为了捡起玩具,我们需要某种机械臂,所以我包括了一个六轴机器人臂,它模仿了肩部-肘部-腕部-手部的组合,非常灵巧,而且由于它是由标准数字伺服电机制成的,所以连接和编程都非常简单。

Albert机器人的主要控制器是英伟达Nano单板计算机(SBC),它通过USB Wi-Fi闪存盘与操作员通信。Nvidia与Arduino Mega 2560微控制器和电机控制器通信,我们将使用它通过脉冲宽度调制(PWM)脉冲来控制电机。以下图显示了机器人的内部组件:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_01_3.jpg

图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循环。前者为我们提供了一个基础,使我们能够轻松且一致地控制机器人,而后者为机器人的自主性提供了基础。

如何控制你的机器人

机器人工作的基本概念,尤其是那些用于驱动的机器人,是简单的。存在一个主控制循环,它反复执行相同的事情——从传感器和电机控制器读取数据,寻找操作员(或机器人的自主功能)的指令,根据这些指令对机器人的状态进行任何更改,然后向电机或执行器发送指令以使机器人移动。

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_01_4.jpg

图1.4 – 机器人控制循环

上述图表说明了我们如何在机器人的软件和硬件中实现OODA循环。机器人可以自主行动,或者通过无线网络接受连接的控制站的指令。

我们需要始终以一致的方式执行这个控制循环。我们需要设置一个基本帧率或基本更新频率,以设定控制循环的时间。这使得机器人的所有系统一起工作。如果没有某种形式的时间管理器,机器人的每个控制周期完成所需的时间都不同,任何路径规划、位置估计或手臂运动都会变得非常复杂。ROS本身是非同步的,因此不提供时间管理器;如果需要,我们必须自己创建一个。

使用控制循环

为了控制我们的机器人,我们必须建立某种控制或反馈回路。假设我们告诉机器人向前移动12英寸(30厘米)。机器人必须向电机发送命令以开始前进,然后有一种机制来测量12英寸的行程。我们可以使用多种方法,但让我们只使用一个时钟。机器人每秒移动3英寸(7.5厘米)。我们需要控制回路开始移动,然后在每个更新周期,或通过回路的每次时间,检查时间并查看是否已经过去了四秒钟。如果已经过去了,那么它就向电机发送一个停止命令。计时器是控制,四秒钟是设定点,电机是受控的系统。这个过程还生成一个误差信号,告诉我们应用什么控制(在这种情况下,停止)。让我们看看一个简单的控制回路:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_01_5.jpg

图1.5 – 样本控制回路 – 维持水壶的温度

根据前面的图示,我们希望水壶中的温度保持恒定。阀门控制由火焰产生的热量,从而加热水壶温度传感器检测水是否过冷、过热或恰到好处。控制器使用这些信息来控制阀门以产生更多热量。这种类型的方案被称为闭环****控制系统

你也可以将这个过程视为一个过程。我们开始这个过程,然后获取反馈来显示我们的进度,以便我们知道何时停止或修改过程。我们可能在进行速度控制,需要机器人以特定的速度移动,或者进行指向控制,机器人指向或转向特定的方向。

让我们看看另一个例子。我们有一个带有自充电对接站的机器人,顶部有一组发光二极管LEDs)作为光学目标。我们希望机器人直接驶入对接站。我们使用摄像头来观察对接站上的目标LED灯。摄像头生成一个误差信号,用于引导机器人向LED灯移动。LED灯之间的距离也给我们提供了到对接站的大致距离。这个过程在下图中展示:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_01_6.jpg

图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。我们可以修改PID常数来调整(调谐)我们的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运行(每帧一次):

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_01_7.jpg

图1.7 – 基于帧的任务调度

我们对调度表的第一遍(图的最上方)显示所有四个任务在帧1、13和25上落在同一个帧上。如果我们像图的下半部分所示的那样在第二帧延迟任务B的开始,我们可以改善控制程序负载的平衡。

这类似于音乐中的度量方式,其中度量是一定的时间,不同的音符有不同的间隔——一个全音符每度量只能出现一次,一个二分音符可以出现两次,一直到最后是64分音符。就像作曲家确保每个度量有正确数量的拍子一样,我们也可以确保我们的控制循环在每个帧中执行平衡的度量过程。

让我们先写一个小程序来控制我们的定时循环,并让你玩这些原则。

这很令人兴奋——我们第一次一起编写代码。这个程序只是演示了我们将在主机器人控制程序中使用的定时控制循环,并且在这里让你可以玩一些参数并查看结果。这是我认为可能的最简单的软时间控制循环版本,所以请随意改进和装饰它。我已经为你制作了一个流程图,以帮助你更好地理解:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_01_8.jpg

图 1.8 – 软实时控制器流程图

让我们更仔细地看看前面图中使用的术语:

  • 帧时间:我们分配给执行循环一次迭代的时间

  • 开始时间:循环/帧开始时

  • 进行大量数学运算:你正在管理的程序

  • 停止时间:帧完成时

  • 剩余时间:已过时间与期望帧时间的差

  • 已过时间:实际运行一次循环所需的时间

  • 帧睡眠时间:我们使用剩余时间来告诉计算机睡眠,以便帧正好花费我们想要的时间。

现在,我们将开始编码。这是一段相当直接的Python代码——我们不会在后面变得复杂:

  1. 我们首先导入我们的库。我们首先从time模块开始并不奇怪。我们还将使用numpy(Python数值分析)中的mean函数和matplotlib来在最后绘制我们的图表。我们还将进行一些数学计算来模拟我们的处理并创建对帧率的负载:

    import time
    from numpy import mean
    import matplotlib.pyplot as plt
    import math
    #
    
  2. 现在我们有一些参数来控制我们的测试。这是你可以尝试不同定时的地方。我们的基本控制是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
    
  3. 测试的持续时间由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
    
  1. 我们将在程序结束时收集一些数据来绘制一个抖动图。我们使用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
    
  2. 在这一步,我们将设置一些变量来测量我们的时间。正如我们提到的,目标是有一系列计算帧,每个帧的长度都相同。每个帧有两个部分: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):
    
  3. 这个部分是我们的有效载荷——执行工作的代码部分。这可能是臂角计算、状态估计或命令解释器。我们将插入一些三角函数和一些数学运算,让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
        #
    
  4. 现在我们读取时钟以找到工作时间。我们现在可以计算出在下一个帧之前需要睡眠进程多长时间。重要的是工作时间 + 睡眠时间 = 帧时间。我将称这个为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)
  1. 因此,这是我们实际的睡眠命令。sleep命令并不总是提供精确的时间间隔,因此我们将检查错误:

        # put this process to sleep
        time.sleep(sleepTime)
    
  2. 这是时间校正部分。我们计算出我们的帧时间总共有多长(工作和睡眠时间)并从我们希望帧时间达到的值(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秒内完成。你可以尝试不同的周期时间和帧率。

  3. 现在我们已经完成了程序,我们可以制作一个小报告和图表。我们打印出帧时间和总运行时间,计算平均帧时间(总时间/计数器),并显示我们遇到的平均误差,这可以通过平均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
    

下图显示了我们的程序计时图:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_01_10.jpg

图1.9 – 我们程序的计时图

图像中的尖峰是由操作系统中断引起的抖动。你可以看到程序在相当狭窄的范围内控制帧时间。如果我们不提供控制,随着程序的执行,帧时间会越来越大。图表显示帧时间保持在狭窄的范围内,并不断回到正确的值。

现在我们已经锻炼了我们的编程肌肉,我们可以将这种知识应用到我们的机器人主控制循环中,实现软实时控制。这个控制循环有两个主要功能:

  • 响应来自控制站的命令

  • Arduino Mega中与机器人电机和传感器的接口

我们将在第7章中详细讨论。

摘要

在本章中,我们介绍了人工智能的主题,这一主题将在整本书中得到强调。我们确定了人工智能机器人和普通机器人之间的主要区别,即人工智能机器人可能是非确定性的。这意味着它可能对相同的刺激有不同的反应,这是由于学习造成的。我们介绍了本书将使用的主题,即在一个游戏室里捡起玩具并将它们放入玩具箱。接下来,我们讨论了人工智能机器人两个关键工具:OODA循环,它为我们机器人如何做出决策提供了一个模型,以及软实时控制循环,它管理和控制我们程序执行的速率。我们在计时循环演示中应用了这些技术,并开始开发我们的主要机器人控制程序。

在下一章中,我们将教机器人识别玩具——我们希望机器人捡起并放回的对象。我们将使用带有视频摄像头的计算机视觉来寻找和识别地板上留下的玩具。

问题

  1. 缩写PID代表什么?这被认为是人工智能软件方法吗?

  2. 图灵测试是什么?您觉得这是评估AI的有效方法吗?

  3. 您认为为什么机器人会与负向障碍物(如楼梯和坑洼)有问题?

  4. 在OODA循环中,Orient步骤做什么?

  5. 从Python及其优势的讨论中,计算以下内容。您的程序需要测试50次更改。假设每次更改都需要重新编译步骤和一个运行步骤来测试,C Make编译需要450秒,Python run命令需要3秒。您在等待编译器时空闲了多少时间?

  6. RTOS代表什么?

  7. 您的机器人有以下预定任务:遥测频率为10 Hz,GPS频率为5 Hz,惯性测量频率为50 Hz,电机控制频率为20 Hz。您将如何安排基础任务的频率,以及您将使用什么间隔来安排较慢的任务(例如,10 Hz的基础频率,每三帧一次电机控制,每两帧一次遥测等)?

  8. 假设一个帧率调度器最快的任务频率为20 fps,您会如何安排需要以7 fps运行的任务?对于以3.5 fps运行的任务呢?

  9. 什么是阻塞调用函数?为什么在像机器人这样的实时系统中使用阻塞调用是不好的?

进一步阅读

您可以参考以下资源以获取更多详细信息:

  • 《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

第二章:设置您的机器人

本章从我对机器人是什么以及机器人由什么组成的思考开始,这是一个相当标准的部件和组件列表。本章旨在让您能够复制练习并使用书中找到的源代码。我将描述我是如何设置我的开发环境的,我使用了哪些工具来创建我的代码,以及如何安装机器人操作系统版本2ROS 2)。我使用的示例机器人Albert的组装可以在本书的GitHub仓库中找到。还有许多其他类型和配置的机器人可以通过对本书中的代码进行一些修改来与之配合工作。我将尝试提供所有可能的快捷方式,包括我机器人SD卡的全图,在Git仓库中。

在本章中,我们将涵盖以下主题:

  • 理解机器人的解剖结构

  • 介绍吸收架构

  • ROS简介

  • 软件设置:Linux,ROS 2,Jetson Nano,和Arduino

技术要求

要完成本章中的实践练习,您将需要本书开头前言中指定的要求。本章的代码可以在https://github.com/PacktPublishing/Artificial-Intelligence-for-Robotics-2e/找到。

理解机器人的解剖结构

机器人是一种能够自行执行复杂动作和行为的技术设备。大多数机器人由计算机或数字可编程设备控制。机器人的一些关键特性如下:

  • 自动化:机器人可以在没有直接人类输入的情况下自动运行,基于它们的编程。这使得它们可以持续地执行重复性或危险的任务。

  • 传感器:机器人使用摄像头、光学、激光雷达和压力传感器等传感器来收集有关其环境的信息,以便它们可以导航和交互。这些感官信息被处理以确定机器人应采取哪些行动。

  • 编程:机器人的“大脑”由一个运行代码和算法的机载计算机或设备组成,这些代码和算法定义了它的行为方式。机器人由人类编程以执行所需的行为。

  • 移动:大多数机器人能够通过轮子、腿、螺旋桨或其他运动系统在一定程度上移动。这使得它们能够在环境中移动以执行任务。

  • 交互:高级机器人可以通过语音、视觉显示、灯光、声音、物理手势等方式与人类进行交流。这允许有用的人类-机器人交互和工作。

  • 自主性:虽然机器人由人类编程,但它们在实现目标的方式上具有一定的自我治理和独立性。在没有人类监督的情况下采取行动和做出决策的能力是它们的自主性。

总结来说,机器人集成了自动化、感知、移动、编程和自主性,以可靠地执行可能复杂、重复、不安全或不适于人类的工作。它们形状和大小各异,从工业机器人臂到社交伴侣机器人,再到自动驾驶汽车。

有一个相当标准的组件和部件集合,构成了绝大多数机器人的主体。即使是外表差异很大的自动驾驶汽车、制造汽车的焊接机器人,以及Roomba吸尘器,它们也有很多相同的组件或部件。有些可能会有更多,有些可能会有更少,但大多数移动机器人将具有以下几类部件:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_02_1.jpg

图2.1 – 典型移动机器人的框图

让我们更详细地看看这些组件:

  • 计算机:运行控制机器人的程序的单元。这可以是一台传统计算机、一个微控制器、一个单板计算机SBC)如我们所拥有的,或者某种其他类型的处理器,它发送和接收命令。机器人臂和一些类型的工业机器人将使用可编程逻辑控制器PLC),这是一种特殊的控制器,它将逻辑(ANDORNOT)应用于各种输入以产生输出。为了使计算机能够向机器人发送命令并接收遥测数据,我们需要某种类型的传感器接口,例如USB端口、串行端口、通用输入/输出GPIO)端口,或者如以太网或Wi-Fi之类的网络接口。

  • 控制站人机界面HRI):机器人被设计来执行任务,这要求操作员必须有一些方式来发送和接收来自机器人的数据,并监督机器人是否表现正确。我们将使用笔记本电脑或台式计算机来完成这个功能,并且我们将通过无线网络与机器人进行通信。我们的控制站向机器人发送命令,并从机器人那里接收遥测数据,这些数据以数据视频音频的形式存在。

  • 无线电数据链路:移动机器人,如我们在本书中设计的机器人,能够移动和探索它们的环境。虽然通过绳索或电线向机器人发送命令是可能的,但首选的方式是使用无线电链路。无线网络如Wi-Fi和蜂窝数据服务的普遍可用性使得创建数据链路变得容易得多。我有很多机器人项目,其中网络链路不可用或不切实际,需要设计定制无线电解决方案。在机器人中使用的其他类型的无线电包括蓝牙、Zigbee以及各种网状网络系统,如Flutter。

  • 电机执行器:我们定义的机器人包括自主推进的能力;也就是说,机器人能够移动。为了移动,机器人需要电机或一组电机。我们的机器人,阿尔伯特,有十个电机,四个用于驱动,六个用于控制机器人手臂和手。电机将电能转化为运动。有各种不同的类型,选择正确的电机是一项挑战。你必须匹配扭矩(电机能拉多硬),电机轴的转速(每分钟的转数),以及电压。以下是选择机器人驱动系统电机时需要考虑的一些关键因素:

    • 扭矩:考虑机器人运动和负载处理所需的扭矩。更大的扭矩允许更快的加速和承载更重负载的能力。如果扭矩不足,机器人会“陷入困境”或使电机停转。电机在停转时(它被供电但不动)会拉取最多的电流。所有这些无法使用的能量都会转化为热量,最终会熔化电线或引起火灾。

    • 速度:确定机器人需要运行的速度。更高的速度需要具有更高RPM(每分钟转数)的电机。我们只希望我们的机器人以适度的速度行驶。玩具无法逃脱。

    • 负载周期:选择一个可以在不过热的情况下连续运行机器人所需负载周期的电机。间歇性负载周期允许使用更小、更轻的电机。我们将驾驶或移动很多——大约50%的时间,但不会太快。

    • 尺寸和重量:大型、重型电机提供大量功率,但可能会限制机器人设计。考虑整个驱动系统的尺寸和重量。记住电机本身也需要移动。

    • 控制:无刷直流电机需要电子速度控制器。步进电机允许开环位置控制。伺服电机,如机器人手臂中的电机,具有集成编码器,并通过串行接口控制。我使用的驱动电机是刷式电机,通过改变电压来控制,我们通过脉冲宽度调制PWM)来控制电压。

    • 电压:高电压允许小型电机输出更多功率。选择与其它电子设备兼容的电压。我的电池是7.2伏,与选定的电机相匹配。

    • 噪音:家庭/办公室机器人可能需要安静的电机。无刷、减速电机很安静但价格昂贵。齿轮传动系统也很嘈杂。

    • 成本:更强大的电机成本更高。在性能需求和预算限制之间取得平衡。阿尔伯特的刷式电机非常便宜。

    一些机器人电机还配备了变速箱以降低电机速度,基本上是以速度换取扭矩。阿尔伯特的电动电机具有减速变速箱,允许电机以比车轮更快的速度运行。

    为机器人提供运动的方式有很多。我们把这些使机器人移动的“东西”称为执行器。执行器的限制仅在于你的想象力,包括气动(由压缩空气驱动的装置)、液压(由不可压缩流体驱动的装置)、线性执行器(将旋转运动转换为线性运动的装置)、旋转关节回转关节(如肘关节一样的角关节)以及甚至一些异类执行器,如形状记忆合金压电晶体,当施加电力时它们会改变形状。

  • 伺服电机:我们机器人中的一些电机属于一种特殊的电机类别,称为伺服电机。伺服电机具有反馈机制和控制回路,用于维持位置或速度。反馈由某种传感器提供。我们使用的伺服电机由一个小型电动机驱动一个由一系列齿轮组成的变速箱,这些齿轮降低了速度并相应地增加了电机的扭矩。我们使用的传感器是一个电位计(可变电阻),可以测量输出齿轮轴的角度。当我们向伺服电机发送命令时,它会告诉电机设置到特定的角度。角度由传感器测量,电机位置与传感器之间的任何差异都会产生一个错误信号,该信号将电机移动到正确的方向。你可以听到电机发出很多噪音,因为电机需要通过七个减速齿轮转动多次才能使机械臂移动。变速箱使我们能够在不消耗太多电流的情况下获得大量的扭矩。

    图2.2展示了如何使用脉冲位置调制PPM)来控制伺服电机。要控制伺服电机,你必须生成一个特定宽度的脉冲:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_02_2.jpg

图2.2 – 伺服电机控制通过PPM信号

短脉冲将伺服电机移动到其范围的起始位置。中等脉冲(1,500微秒)是伺服电机位置的中间。晚脉冲会导致伺服电机移动到其范围的末端。我在这个版本的机器人中使用的机械臂有一个与机械臂硬件一起提供的伺服控制器。我们将通过串行命令控制这个控制器,具体在第五章中介绍。

  • 电机控制器或电子速度控制器:电机本身并不很有用——你需要将控制计算机的命令转换为电机运动的能力。由于电机需要的电压和电流比控制计算机(我们的Jetson Nano)能提供的要多,我们需要一个设备将小的数字信号转换为大的模拟电压和电流。这个设备被称为电机控制器。这个控制器我必须单独购买,并且由两部分组成——一个Arduino Uno和一个连接到其上的电机控制器屏蔽板:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_02_3.jpg

图2.3 – 我为Albert使用的电机控制器屏蔽板

如图中所示,四个电机线连接到字母连接处。

  • 由于我们有一个坦克式驱动机器人(我们通过以不同速度运行电机来转向,也称为差速驱动),我们还需要电机能够向前或向后运行。电机控制器接收一个特殊的输入信号,称为脉冲宽度调制PWM)。PWM是一个重复信号,其中电压开启和关闭。电机的油门(电机转速)与PWM信号保持开启状态的时间成正比。

    电机控制器有几种类型的连接,由于提供的高电压和大电流,必须仔细接线。这可以通过以下步骤完成:

    • 有两个控制线输入——一个用于速度(PWM信号)和另一个是方向信号。我们通过改变方向信号将电机置于倒车状态——1表示前进,0表示后退。

    • 接下来我们需要的是地面——确保发送PWM信号(在我们的例子中,是Arduino Mega)和控制电机有它们的接地线连接是非常重要的。

    • 接下来,电机控制器需要电机电压电流,这些我们可以直接从我们的电池中获得。

    • 最后,我们将每个电机的两根线连接到控制器上。有趣的是,我们并不关心哪根线连接到电机的哪一侧,因为我们可以同时向前和向后运行。如果电机转向错误,只需交换两根线。这是唯一一次在科幻电影之外说“只需反转极性”的时候。

    我们将在在线附录中介绍示例机器人——Albert——的具体接线。

  • 传感器:为了让机器人,一个可以移动并对环境做出反应的机器,能够看到其周围的环境,它需要传感器。传感器从机器人的外部或内部获取信息并将其转换为数字形式。如果我们使用数字摄像头传感器,它将光转换为数字像素(图像元素),记录为数组。一个声纳传感器通过发送能量脉冲(声波)并监听回声前的延迟时间来测量到物体的距离,例如墙壁。测量延迟时间给我们提供了到物体的距离,因为声速相对恒定。在我们的Albert项目中,机器人有几种类型的传感器:

    • 我们的主要传感器是一个广角视频摄像头,我们将用它来避开障碍物和检测物体。

    • 我们还将使用一个麦克风来监听声音并执行语音识别。

    • 我们在本列表中之前提到过伺服电机——每个伺服电机都包含一个角度传感器,它可以检测旋转量并允许我们控制机械臂和手。

    • 我们有我们的紧急停止按钮,它连接到Arduino,是一种触觉(触摸)传感器。当按钮被按下时,机器人可以将其解释为停止命令。

    • 我选择的机器人手臂有一个方便的电压监控器,我们将用它来跟踪剩余的电池寿命(充电)。

在下一节中,我们将讨论机器人软件架构,它将作为我们创建的自主行为的框架。

介绍吞没式架构

在这一点上,我想花点时间讨论一下吞没式架构背后的理念,并指出我们将如何在我们的机器人项目设计中使用这个概念的一些具体细节。你们中的许多人可能在学校或学习中已经熟悉了这个概念,所以你们可以看看我的图,然后继续前进。对于其他人,让我们谈谈这个受生物学启发的机器人概念。

吞没式架构最初由麻省理工学院教授罗德尼·布鲁克斯博士描述,他后来帮助创立了iRobot公司并发明了Baxter机器人。罗德尼试图开发昆虫大脑的类似物,以便了解如何编程智能机器人。在此之前的机器人(1986年)基本上是单线程机器,一次只能做一件事。它们读取传感器,做出决定,然后行动——在任何时候只有一个目标。像苍蝇或蚂蚁这样的生物拥有非常简单的头脑,但仍然能够在现实世界中发挥作用。布鲁克斯推理认为,存在多个同时进行的闭环反馈过程层。

吞没式的基本概念已经存在了一段时间,自从首次引入以来,它已经被适应、重用、改进和简化。我在这里展示的是我对如何将吞没式概念应用于我们试图达成的机器人环境中的解释。

首先要理解的是,我们希望我们的机器人能够根据一系列目标行动。机器人并不是简单地对每个刺激做出完全独立的反应,而是执行某种以目标为导向的行为。目标可能是捡起玩具或导航房间,避开障碍物。我们正在创建的范例是让用户为机器人设定目标,机器人决定如何实现这些目标,即使目标仅仅是向前移动一米。

问题始于机器人需要在同一时间记住多个目标。机器人不仅仅是四处驾驶,还要在避开障碍物的同时寻找可以捡起的玩具。我们如何在这不同的目标之间进行仲裁,以确定哪个目标具有优先级?答案可以在下面的图中找到:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_02_4.jpg

图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 设计:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_02_5.jpg

图 2.5 – RAMSEE,由作者设计的安保机器人

我很快意识到,虽然使用 ROS 的初始学习曲线很陡峭,但回报是能够创建和实施模块化、易于移植的服务,这些服务可以独立开发。我无需将所有内容组合成一个程序,甚至在一个 CPU 中。我可以利用我的多核计算机来运行独立的过程,甚至拥有多个计算机,并将事物自由地从一台移动到另一台。RAMSEE 有一个拥有八个核心的计算机和另一个拥有四个核心的计算机。

重要提示

ROS 可以描述为 模块化开放式系统软件MOSA)。它提供了一个标准接口,允许程序通过 发布-订阅 模式相互通信。这意味着一个程序发布数据,使其可供其他程序使用。需要这些数据的程序会订阅这些数据,并在有新数据可用时收到消息。这使得我们可以独立开发程序,并在程序之间创建标准化的接口。这确实使创建机器人变得更加容易,并且更加灵活。

另一个主要优势,并且值得所有麻烦,是 ROS 拥有一个非常大的库,其中包括传感器、电机、驱动器和执行器的现成接口,以及所有可想象到的机器人导航和控制工具。例如,我们将使用 OAK-D 3D 深度相机,该相机在 https://github.com/luxonis/depthai-ros 提供了 ROS 2 驱动器。

RViz2 工具提供了您所有传感器数据的可视化,以及展示定位和导航过程。我非常欣赏 ROS 中包含的日志和调试工具。您可以将数据记录到 ROSBag 中——任何跨发布/订阅接口的数据——稍后回放以测试您的代码,而无需连接机器人,这非常实用。

下面的插图显示了 RViz2 的输出,展示了我的机器人绘制的地图:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_02_6.jpg

图 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仓库中遵循的基本步骤如下:

  1. 第一步是准备一张带有操作系统映像的SD卡。我使用了Imager,但还有其他几个程序可以完成这项工作。您需要一个至少32GB空间的SD卡 – 请记住,在这个过程中您将擦除SD卡。这意味着您需要一张大于32GB的卡来开始 – 我使用了一张64GB的SD卡,因为32GB的SD卡没有按网站上的说明工作。

  2. 按照SD卡的指示操作 – Jetson Nano Ubuntu网站(https://github.com/jetsonhacks/installROS2)建议我们使用容量为64GB的Class 10存储卡。将SD卡插入读卡器,并启动您的磁盘映像程序。务必(并且再三)确认您选择了正确的驱动器字母 – 您将在该驱动器中擦除磁盘。选择您下载的磁盘映像。点击写入按钮,让格式化程序在SD卡上创建磁盘映像:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_02_07.jpg

图2.7 – Imager程序用于在SD卡上写入磁盘映像

  1. 您可以按照常规设置来设置您的语言和键盘,以及设置网络。我喜欢为机器人使用静态IP地址,因为我们将会大量使用它。

  2. 总是设置一个新的用户ID并更改默认密码是一个好主意。

现在,让我们看看如何安装ROS 2。

安装ROS 2

我们需要在Jetson Nano上安装ROS 2。我在我的机器上使用了Foxy版本。您可以按照此链接中的说明操作:https://github.com/Razany98/ROS-2-installation-on-Jetson-Nano

您将需要设置源并让您的计算机指向ROS 2仓库。为此,请按照以下步骤操作:

  1. 使用以下代码设置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
    
  2. 设置要使用的源仓库:

    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
    
  3. 安装ROS包:

    sudo apt update
    sudo apt upgrade
    sudo apt install ros-foxy-desktop
    sudo apt install ros-foxy-ros-base
    
  4. 设置环境:

    source /opt/ros/foxy/setup.bash
    
  5. 完成后,您可以通过输入以下内容来检查您的安装是否正确完成:

    ros2 topic list
    ros2 node list
    

在我们继续之前,让我们看看ROS是如何工作的。

理解ROS的工作原理

你可以将ROS视为一种连接不同程序的中间件。它提供了程序之间的进程间通信IPC),这样我们就不必将所有函数放在一个大块代码中——我们可以将机器人的能力分散开来,独立开发和测试。

ROS机器人控制系统中的每个独立部分都称为节点。节点是一个单一用途的编程模块。我们将有收集摄像头图像、执行物体识别或控制机器人手臂的节点。使用ROS,我们可以隔离这些功能,并独立开发和测试它们。

不同的节点(程序)通过/image_raw相互通信。这种标准消息类型包括有关图像格式的数据,以及图像本身。我们还使用sensor_msgs/CameraInfo格式在/camera_info主题上发布摄像头数据,该格式在以下图像中描述:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_02_8.jpg

图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的软件。

问题

  1. 列出三种类型的机器人传感器。

  2. PWM这个缩写代表什么?

  3. 模拟到数字转换是什么?输入和输出是什么?

  4. 谁发明了子吸收架构?

  5. 将我的三层子吸收架构图与艾萨克·阿西莫夫提出的机器人三大定律进行比较。是否存在相关性?为什么有,或者为什么没有?

    提示:思考这些法律如何改变机器人的行为。从子吸收的角度来看,哪一条是最底层的法律?哪一条是最顶层的?

  6. 你认为我应该给我的机器人项目——阿尔伯特——起一个名字吗?你给你的机器人起名字吗?你的洗衣机呢?为什么不给它起名字?

  7. 环境变量ROS_ROOT的重要性是什么?

进一步阅读

第三章:构想实用机器人设计过程

本章代表了前几章关于一般理论、介绍和设置的桥梁,以及接下来的章节,我们将应用使用人工智能技术(AI)的解决问题的方法。第一步是清楚地表述我们的问题,从机器人的使用角度出发,这与作为机器人设计师/建造者的我们的观点不同。然后,我们需要决定如何应对我们和机器人将尝试的每个基于硬件和软件的挑战。到本章结束时,你将能够理解如何系统地设计机器人的过程。

本章将涵盖以下主题:

  • 基于系统工程的机器人方法

  • 理解我们的用例范围

  • 如何借助用例来表述问题

  • 如何通过故事板来解决问题

  • 理解我们的用例范围

  • 确定我们的硬件需求

  • 软件需求分解

  • 编写规范

基于系统工程的机器人方法

当你着手创建一个基于人工智能软件的复杂机器人时,你不能没有某种关于机器人如何组装以及所有部件如何相互通信的游戏计划就盲目地开始编写代码和拼凑东西。我们将讨论基于系统工程原则的机器人设计系统方法。我们将学习用例,并使用故事板作为理解我们正在构建的内容以及需要哪些部分(硬件和软件)的技术。

理解我们的任务 – 清理游戏室

我们已经就本书的示例机器人 Albert 的主要任务谈了一些内容,这个机器人是用来在我孙子辈来访后清理我家的游戏室的。我们需要为我们的问题提供一个更正式的定义,然后将其转化为机器人要执行的列表任务,以及我们可能如何完成这些任务的行动计划。

我们为什么要这样做呢?让我们考虑一下史蒂夫·马拉博利的这句话:

“如果你不知道你要去哪里,你怎么知道你到了那里?”

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_03_1.jpg

图 3.1 – 了解你的机器人做什么很重要

互联网和各种机器人网站上充斥着成百上千的机器人,它们有一个共同的致命缺陷:机器人和它的软件是先设计出来的,然后才去寻找适合它的工作。在机器人行业中,这被称为有备无患,瞄准再射击的问题。机器人的任务、客户、目的、用途和工作是首要的。另一种说法是:要创造一个有效的工具,第一步是决定你用它做什么。

我本可以将这本书写成一套理论和练习,这些在课堂环境中会非常有效,这将让你接触到许多你不知道如何应用的新工具。然而,这一章的目的在于为你提供工具和方法,以尽可能少的误导、痛苦、苦难、眼泪和拔掉的头发,从有一个好想法到拥有一个优秀的机器人提供一个路径。

重要提示

在烧伤方面,你需要自己小心处理;请在使用烙铁时格外小心。

我们将使用的流程是直接的:

  1. 第一步是从用户的角度审视机器人,然后描述它的功能。我们将把这些描述称为用例——机器人将被如何使用的例子。

  2. 接下来,我们将每个用例分解成故事板(逐步插图),这些可以是文字图片或实际图片。从故事板中,我们可以提取任务——我们机器人要完成的待办事项清单。

  3. 这个流程部分的最后一步是将待办事项清单分为我们可以用软件完成的事情和我们需要硬件来完成的事情。这将为我们设计机器人和其基于AI的软件提供详细的信息。记住,机器人的一个用途是作为这本书的好例子。

让我们从查看用例开始。

用例

让我们从陈述问题开始我们的任务。

我们机器人的任务 – 第一部分

大约每个月一两次,我那五个可爱、聪明且好动的孙子孙女会来拜访我和我的妻子。像大多数祖父母一样,我们在楼上的游戏室里放了一个装满玩具的盒子,让他们在来访时玩耍。他们一到——至少是年长的孙子孙女们——就会把玩具盒里的每一个玩具都拿出来开始玩。这导致了以下照片中所示的场景——玩具在游戏室里随机且均匀地分布:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_03_2.jpg

图3.2 – 孙辈来访后的游戏室

实际上,你找不到比这更好的随机分布了。他们在这方面真的很擅长。由于作为祖父母,我们希望最大化孙子孙女在我们家玩耍的时间,并希望他们把祖父和祖母的房子与玩耍联系起来,所以我们不让他们回家时收拾玩具。你可以看到这会走向何方。

顺便说一句,如果你是父母,让我提前向你道歉;这确实是我们祖父母这边的一个邪恶计划,当你有了自己的孙子孙女时,你就会理解——你也会这样做。

我们在哪里……?是的,一个满是随机且均匀分布的外来物品——玩具——散落在原本可用的游戏室里,需要被清理。通常,我只需要重重地叹口气,自己把这些东西都收拾起来,但我是机器人设计师,所以我想要做的是制造一个能够完成以下任务的机器人:

  1. 拾起玩具——而不是房间的家具、灯光、书籍、扬声器或其他非玩具物品。

  2. 将它们放入玩具箱中。

  3. 继续这样做,直到找不到更多的玩具,然后停止。

这是这个过程的视觉表示:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_03_3.jpg

图 3.3 – 用例:捡起玩具

现在我们可以提出一些相关的问题。我在学校上过新闻学课程,我被教导了“5W1H”的有用性——什么何时何地为什么如何。这些对于检查用例同样有用。在这个部分,我有一个坚定的规则:不要涉及实现细节。不要担心你将如何做到这一点。只需关注定义结果。所以,我们现在暂时不考虑“如何”(H),而专注于“W”。让我们试一试:

  • :机器人。这很简单。我们希望机器人做某事,就像机器人做这件事而不是我做一样。我们希望机器人做什么?

  • 做什么:这个问题可以用两种方式回答:

    • 捡起玩具并将它们放入玩具箱中:这个答案告诉我们什么?它说我们将要抓住并抬起一些东西——玩具。什么是玩具?我们也可以将其重新表述为否定,这引出了第二个答案。

    • 将不在房间中的物品捡起并放入玩具箱中:玩具在孙子辈把它们全部拿出来之前不在房间里。所以,我们要么将物品分类为玩具,要么分类为之前不在房间中的物品。“不在房间中”意味着机器人以某种方式知道房间中应该有什么,可能是在孩子们到来之前进行一次调查。然而,“玩具”意味着机器人至少可以将物体分类为玩具非玩具。让我们先坚持这一点。我们可能会有一些不在玩具箱中的物品,它们不是玩具但放错了地方,因此不属于玩具箱。您已经可以看到这些问题正在塑造这个过程中后续的内容。

  • 何时:在孙子辈来访并离开后,继续捡起玩具,直到没有剩余。

    这为我们提供了两个“何时”的条件:开始和结束。在这种情况下,开始定义为孙子辈来访并离开。现在,我在用例中声明我将告诉机器人何时满足这些条件是完全合理的,因为这不会给我带来不便。我会在这里,我知道房间需要打扫。此外,我需要将机器人取出并放入房间。当机器人不工作时,它放在书架上。所以,让我们将我们的“何时”语句改为以下内容:

    当我(用户)告诉你时,直到找不到更多的玩具为止。

    现在,我们本可以决定机器人需要自己解决这个问题,在孙子辈离开后自动开启,但这样做的投资回报率是多少?那将是一大堆工作,但收益却不多。对我来说,作为用户,痛点在于捡玩具,而不是决定何时去做。这要简单得多。

    注意,我的when语句有一个开始和一个结束。任何看过《幻想曲》中米老鼠的《魔法师的学徒》片段的人都会明白,当你有一个机器人时,告诉它何时停止是很重要的。另一个重要的概念是定义结束条件。我没有说当所有玩具都被捡起时停止,因为这会意味着机器人需要知道所有的玩具,无论是通过视觉还是数量。作为任务定义来说,说当你看不到更多玩具时停止更容易,这样就能达到我们的目的,而不需要给我们的机器人增加额外的要求。

    当机器人设计师对问题了解得更多时,重新审视用例是很正常的——有时你可能正在努力解决一个与解决用户任务无关的问题。你可以想象一些团队中的机器人工程师被分配了一个任务,即捡起所有玩具,这意味着所有文化、所有地区发明的所有玩具!然后,你得到了一个需要500,000美元数据库软件许可和服务器农场来存放它的请求。我们只想捡起游戏室里找到的玩具。

  • 位置:楼上的游戏室。现在有一些棘手的部分。要清洁的区域是房子的一个特定区域,但它并不是真正由墙壁所界定。而且它在上楼的地方——游戏室里有一个通往楼下的楼梯,我们不希望我们的机器人滚下去。你怎么会知道这些?除非你问这类问题!机器人运作的环境和它所做的事情一样重要。在这种情况下,让我们回头去询问用户。我会在这里插入一个楼层平面图来定义我所说的游戏室。从积极的一面来看,我们在这个任务中不需要爬楼梯或下楼。但我们确实需要留意楼梯作为一个潜在的危险:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_03_4.jpg

图3.4 - 我家的楼层平面图,楼上

  • 原因:那么,为什么机器人要捡起玩具呢?我差点就写下“因为有人得做这件事。”然而,答案是我不想让孙子辈的孩子们捡玩具,这样他们就有更多的时间玩耍,我也不想这么做。所以,我们为这个任务制作了一个机器人。机器人世界中的一个格言是,适合机器人的任务通常是脏乱枯燥危险的。这个任务无疑属于枯燥类别。

我们的这个机器人有多种用途——它有多种功能要执行。

我们机器人的任务——第二部分

机器人需要与我孙子孙女互动。为什么这很重要?正如我在第 1 章中告诉你的,孙子孙女们被介绍了一些我的其他机器人,最大的孙子威廉总是试图与机器人交谈。我有三个在自闭症谱系上的孙子孙女,所以这不是一个无足轻重的愿望——我阅读了相关的研究,例如 Robots for Autism (https://www.robokind.com/),该研究指出,在这种情况下机器人可能会有所帮助。虽然我不是在尝试进行治疗,但我希望我的机器人能够以口头方式与我的孙子孙女互动。我还有一个具体的要求——机器人必须能够讲敲门笑话并回应它们,因为这是威廉的最爱。我希望这个机器人能够进行口头互动。

因此,这里是这个用例的图示:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_03_5.jpg

图 3.5 – 用例:与人互动

让我们用这个用例进行同样的练习。我们提出相关的问题:什么何时何地,和为什么?让我们来分解这些:

  • 人物:机器人、用户(爷爷)和孙子孙女们。

    在这种情况下,用户交互是任务的一部分。我们与谁互动?我需要能够命令机器人开始互动。然后,我们希望机器人既能与孩子交谈也能听孩子说话。

  • 功能:接收命令并以口头方式与孩子互动(进行对话),这必须包括敲门笑话。我们保留两种功能:接收来自——让我们称我为机器人控制器的命令,使这个更通用。另一种功能是与孩子进行对话,包括讲敲门笑话。我们将在我们的分解中进一步定义对话。你可以参考第 6 章关于使用机器人作为数字助理的内容。我们将使用一个名为Mycroft的开源数字助理作为机器人的语音界面。我们将在Mycroft的基本功能上添加我们自己的技能,这实际上非常灵活。机器人可以获取天气信息,设置定时器,播放音乐,在谷歌上查找信息(例如,四分之一杯有多少汤匙),甚至告诉你国际空间站现在在哪里。但它不能讲敲门笑话——直到现在,因为我们正在为机器人添加这个功能。幸运的是,敲门笑话有一个非常结构化的形式,基于双关语和这样的问答格式:

    机器人:敲门。

    孩子:谁在那里?

    机器人:让我们进来。

    孩子:让我们进来谁?

    机器人:让我们进来,这里太冷了!

    我会把相反形式的绘图——回应敲门笑话——留给你。

  • 时间:根据机器人控制器的请求,然后当孩子对机器人说话时。

    我认为这相当直观:当机器人收到执行命令时,它会进行交互。然后它等待有人与之交谈。我们可以从这个信息中推断出,当我们正在捡玩具时,我们并不期望机器人说话——这两个活动是互斥的。我们只在孩子们离开后捡玩具,因此有一个可以与之交谈的对象。

  • 位置:在游戏室里,距离机器人大约六英尺。

    我们必须设定一些关于我们能听到多远的限制——我们的麦克风的灵敏度是有极限的。我建议六英尺作为最大距离。我们可能稍后会重新考虑这个距离。当你遇到这样的需求时,你可以问客户“为什么是六英尺?”他们可能会说,“嗯,这听起来像是一个合理的距离。”然后你可以问,“如果它是五英尺,这会是这个功能的失败吗?”用户可能会回答,“不,但会不那么舒适。”你可以继续询问距离,直到你对所需的距离(多远才算不失败)有一个感觉,在这个例子中可能是三英尺(这样孩子就不需要弯腰到机器人那里才能被听到),以及期望的距离,即用户希望功能能工作的距离。当我们开始测试时,这些是重要的区分点。这个需求的通过-失败界限在哪里?

  • 原因:因为我的孙子辈想要和机器人交谈,并希望它做出回应(即,用户明确要求了这个功能)。

现在,让我们更深入地探讨我们的机器人任务。

我们的机器人要做什么?

现在我们将使用故事板过程对机器人需要执行的操作进行详细分析。这个过程是这样的:我们根据所有关于“W”问题的答案,尽可能详细地将我们的两个任务分解。然后我们描绘出每个步骤。这些图片可以是绘画或文字描述(一段话),描述该步骤中发生的事情。我喜欢通过描述机器人作为一个状态机来开始分解过程,对于我们问题的前一部分,这可能是一个理解机器人每一步内部发生情况的好方法。

你可能熟悉状态机图,但以防万一,状态机图描述了机器人的行为作为一系列离散的状态或条件集,这些状态或条件定义了机器人可以执行哪些操作:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_03_6.jpg

图3.6 – 机器人状态机图

我们的第一个状态仅仅是关闭——机器人没有开启电源。

每个状态都是一个事件(或事件),它会导致状态发生变化。这些被称为转换。要从“关闭”状态转换到下一个状态,必须发生某些事件——例如,人类操作员打开电源。我们将称这个转换事件为“施加电源”。现在我们处于什么状态?需要一些时间来启动计算机并加载程序(“初始化”)。一旦一切启动并初始化,机器人将准备好接受命令。让我们称这个状态为“待机”。机器人只是坐着等待指令。现在我们想要开始清洁房间。我向机器人发送一个“开始清洁”命令,这将状态转换为——什么?接下来需要发生什么?我们可以定义一个名为“清洁”的状态,但那将包括很多复杂的功能,而且我们不会从中学到很多东西。我们需要机器人使用其摄像头寻找玩具。如果它找不到玩具,它需要向前移动一小段距离——避开障碍物——然后再次寻找。在实践中,我们应该能够在驾驶的同时寻找玩具而无需不断停车。我们需要使“寻找玩具”功能在看到玩具时中断驾驶。

如果机器人找到了玩具,那么它需要调整自己的位置,使得玩具在机器人手臂的触及范围内。在状态机图中,我们已经添加了名为“开始清洁”的转换,它将状态从“待机”转换为“寻找玩具”。现在我们可以添加两个额外的转换:一个叫做“玩具=无”,另一个叫做“玩具=有”。“玩具=无”分支会进入一个名为“前进”的状态,在那里机器人向前移动——同时避开障碍物——然后返回到“寻找玩具”状态并再次尝试找到玩具。我们需要某种方式来告诉软件多久查找一次玩具。我们可以使用一个简单的计时器——经过多少秒。或者我们可以使用基于轮子运动的某种距离函数。

那么,现在我们已经找到了玩具,我们该怎么办?我们需要开车到玩具那里,使其进入我们机器人手臂的范围内。我们尝试用机器人的手臂和手握住玩具。我们可能不会在第一次尝试就成功,在这种情况下,我们想要再次尝试。标记为“抓握失败”的循环转换表示如果你第一次尝试不成功,就回去再试一次。我以前在哪里听过这样的话?你可以看到同样的情况在“拿起玩具”中。为什么有两个部分?在我们能够举起玩具之前,我们需要首先抓住它。所以我认为需要两个状态,因为我们可能无法成功抓住——玩具从手中掉落,这独立于拿起玩具,玩具太重或太笨重而无法举起。

好的,我们找到了一个玩具并把它拿了起来。接下来是什么?我们需要把它放进玩具箱。下一个状态是驶向玩具箱。在这个阶段不用担心如何做;我们只需要做这件事。稍后,我们可以进一步将这个状态分解成更详细版本。我们驾驶直到到达事件找到玩具箱。这意味着我们看到了玩具箱。然后我们进入放置放下位置状态,这个状态将机器人移动到可以放下玩具的位置。最终状态放下玩具是显而易见的。我们放下了玩具,机器人的爪子中没有东西了,而且你知道吗?我们通过回到寻找玩具状态重新开始。如果机器人决定放下不成功(玩具仍然在爪子中),那么我们将它再次尝试那个步骤,通过重新定位手在玩具箱上方并尝试通过打开手放下玩具。我们如何知道爪子是否为空?我们尝试关闭握持并观察手伺服机构的位置。如果爪子可以关闭(达到最小状态),那么它是空的。如果玩具掉出玩具箱(机器人完全错过了箱子),那么它又变成了地板上的玩具,并且会以正常方式处理——机器人会找到它,拿起它,并再次尝试。

这一切都很不错,我们的这个小机器人会永远四处寻找玩具,对吧?我们忽略了两个重要的转换。我们需要一个没有更多玩具的事件,并且我们需要一种方法回到关闭状态。回到关闭状态很简单——用户关闭电源。我使用了一个标记为任何状态的简写方法,因为我们可以在机器人做任何事情的时候随时按下关闭按钮,而且机器人对此无能为力。也许从每个状态画一条线回到关闭状态会更合适,但这会使图表变得杂乱,而这种记法仍然能够传达意思。新的状态机图看起来是这样的:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_03_7.jpg

图3.7 – 新的状态机图

让我们花一分钟时间来谈谈没有更多玩具的概念。我们如何定义这个?这可能需要一些实验,但到目前为止,我们会说如果我们尝试了10分钟还没有找到玩具,那么我们就满意了,没有更多玩具可以找到。稍后,我们可以根据需要调整这个时间。可能5分钟对我们这个大小的房间来说已经足够了。请注意,没有更多玩具事件只能从寻找玩具状态产生,这应该是合理的。

我们提到机器人需要避开障碍物。但我们没有“避开障碍物”的状态。为什么是这样?那是因为几个状态包括驾驶,每个状态都包括避开障碍物。为避开障碍物设立一个状态是不合适的,因为它不是特定于一个状态的。我们需要的是一个描述机器人驾驶的独立状态机。正如我在上一章的“介绍子吸收架构”部分提到的,我们可以同时有多个目标处于操作状态。

拾取玩具的任务是任务,这是机器人的总体目标。“避开障碍物”是驱动引擎的目标,是我们机器人的中级管理者。

我们已经讨论了我们的用例并绘制了状态机图,现在让我们继续下一步,即创建我们的故事板。

使用故事板

在本节中,我们将进一步分解我们的用例,以便了解我们的机器人在执行两次任务过程中必须代表我们完成的各项任务。我创建了一些故事板——简短的草图——来阐述每个要点。

故事板的概念借鉴自电影行业,在那里使用类似漫画的叙述方式,将剧本中的文字转换成一系列图片或卡通,以传达剧本中未包含的额外信息,例如构图、背景、动作、道具、场景和摄像机移动。故事板的实践可以追溯到无声电影时代,至今仍在使用。

我们可以在机器人设计中使用故事板的原因相同:为了传达用例文字中未包含的额外信息。故事板应该是简单、快捷的,并且只需传达足够的信息,帮助你理解正在发生的事情。

让我们开始吧。我们不会为“电源应用”、“初始化”或“待机”创建故事板,因为对于这些简单概念来说,故事板实际上并不需要。我们将跳到状态图中的“开始清洁”事件。

故事板 – 收起玩具

当我们的故事开始时,机器人正在做什么?它已经被打开,处于待机状态,等待被告知要做什么。它是如何接收命令的?一种很好的、免提的方式是接收一个语音命令来“开始清洁”,或者一些意思相同的话。

在“开始清洁”之后的下一步是“寻找玩具”。这个故事板帧是当机器人被命令开始清洁时“机器人所看到的”。它看到了房间,其中可见三种类型的物体——也就是说,玩具、不是玩具的东西(扶手椅和壁炉),以及房间本身,包括墙壁和地板:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_03_8.jpg

图3.8 – 等待语音命令开始清洁

我们可以选择任何类型的传感器来检测我们的玩具并指导我们的机器人。我们可以有一个激光雷达、热传感器或声纳扫描仪。让我们假设这个任务的最佳传感器工具是一个普通的USB摄像头。我们可以控制照明,玩具并不比周围环境更暖或更冷,我们需要足够的信息来按类型识别对象。所以,视频就是了。我们将在稍后确定我们确切需要什么类型的摄像头,所以把它加到我们的待办事项列表中。

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_03_9.jpg

图3.9 – 寻找玩具

我们下一个故事板是寻找玩具。我们需要运行某种算法或技术来按类型分类对象。该算法的结果是找到对象——将它们从地板的背景中分离出来——然后对每个对象进行分类,判断它是玩具还是非玩具。我们并不关心有更多的细分——我们忽略所有非玩具对象,并捡起所有玩具对象。注意,我们在玩具对象周围画圆圈,这也是另一种说法,即我们必须在相机帧中定位它们。

那么,这张简单的图片告诉我们我们之前不知道什么?它告诉我们以下内容:

  • 我们需要通过对象来分割相机图像。

  • 我们需要在相机帧中定位对象。

  • 我们需要将对象分类为玩具非玩具

  • 我们需要能够存储和记住这些信息。

我们一次只能捡起和移动一个玩具——我们只有一只手,而且没有人说在用例中我们需要一次捡起多个。所以,我们只关心一个玩具——让我们随意说我们捡起离机器人最近的那个:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_03_10.jpg

图3.10 – 选择最近的玩具

我们也可以说,这是最容易拿到手的玩具,这可能是一个与选择最近的玩具略有不同的过程。我们将这个玩具设定为下一次行动的目标,那么这个目标是什么呢?如果你说是开车去玩具,你就说对了。然而,我们不仅要开车去玩具,还要将机器人的身体放置到一个位置,以便使用机器人手臂抓取玩具。顺便说一句,这意味着机器人手臂必须能够触及地面或非常接近地面,因为我们有一些小玩具。

我们的机器人必须规划一条从当前位置到可以尝试捡起玩具的位置的路线。我们在玩具中心手臂长度处设定一个目标目标:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_03_11.jpg

图3.11 – 规划到目标的路

机器人需要确保路上没有障碍物。有两种方法可以做到这一点。如图所示,我们可以通过增加机器人的宽度(再加上一点)来清除机器人正在行驶的道路,看看是否有障碍物在那个区域,或者我们可以在障碍物周围添加一个边界,看看我们的路径是否进入那些边界。无论如何,我们需要一个没有障碍物的路径:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_03_12.jpg

图 3.12 – 在路线上寻找障碍物

机器人自己决定合适的对准方式,为拾取玩具做准备:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_03_13.jpg

图 3.13 – 定位机器人手

现在机器人已经完成了它的行驶,机器人可以将机器人手移动到拾取玩具的位置。我们需要将机器人手放在玩具的重心上方,然后旋转手以匹配玩具的狭窄部分,这样我们就可以拾取它。我们这个项目的目标之一不是规定机器人如何做,而是让它自己学习。因此,我们可以说对于这个故事板面板,机器人使用其培训和机器学习来使用适当的手势来准备抓取物体。我们可以推断这包括将手对齐:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_03_14.jpg

图 3.14 – 拾取玩具

很可能 故事板 6 是难点 (图 3*.13*),而在 故事板 7 中,机器人完成了对物体的抓取并拾起了它 (图 3*.14*)。机器人必须能够确定拾取是否成功,如果不成功,则再次尝试。那是在我们之前做的状态机图中。我们现在已经拾起了玩具。接下来是什么?找到玩具箱!

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_03_15.jpg

图 3.15 – 找到玩具箱

现在我们需要机器人找到玩具箱。同样,目前我们并不关心它是如何做到的。我们仍然担心的是 什么 而不是 如何。不知何故,机器人四处张望并找到了玩具箱,在这个例子中,它很大,靠墙,并且有独特的颜色。无论如何,机器人必须自己找到玩具箱。图片中的标签表明机器人可以区分玩具箱,并且它将感知到的所有其他物体视为障碍物。我们可以看到我们不需要同时激活 玩具/非玩具 功能,只需要 玩具箱/非玩具箱 决策过程。这确实减少了一些所需的处理,并将使机器学习更容易。

现在我们已经找到了玩具箱,我们展示了稍微复杂一些的任务,即绕过障碍物到达那里。在这个例子中,我们展示了机器人底座的紫色轮廓,与障碍物周围的红色轮廓相比,我将其标记为 禁止进入区域。这为我们提供了更多关于如何避开障碍物的指导:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_03_16.jpg

图 3.16 – 计划到达玩具箱的路径

我们希望将机器人的中心保持在 禁止进入区域 之外。我们需要足够接近玩具箱,以便将玩具放入其中:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_03_17.jpg

图 3.17 – 将玩具与箱子对齐

故事板10中,我们将玩具高高举起,超过玩具盒顶部,并在我们放手时将玩具定位在玩具盒内。请注意,我们必须在距离玩具盒最后几英寸之前将玩具举起。我们将机器人手放在玩具盒开口的顶部,尽可能向前,并在玩具盒的中间位置。

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_03_18.jpg

图3.18 – 将玩具放入盒子

在玩具传奇的最终步骤中,我们要打开机器人手,让玩具有可能落入玩具盒中。我预测我们可能需要花费一些试错时间来做到这一点。我们可能需要将张开的手左右倾斜,以使玩具掉落。如果玩具掉出盒子外,那么它就没有被收好,我们必须从头开始,再次尝试收好它。我们不需要为这个状态创建新的状态,因为它会回到地板上的玩具状态,而我们已经有了一个对应的状态。

我希望你在故事板过程中已经看到了这如何为可视化机器人的任务提供洞察力。我会说最重要的好处是它迫使你思考机器人正在做什么,并将每个步骤分解成越来越小的部分。如果你觉得需要这样做,不要犹豫,将单个画板分解成自己的故事板。

项目目标

由于这是一个AI/机器学习项目,我们必须在项目目标中增加不仅仅是收好玩具,还要使用机器学习、自适应系统、神经网络和其他工具来提供一种解决这类问题的新方法。你可能想,“为什么要费这个劲?你用标准的编程方法做得更好。” 我会从经验告诉你,这种方法很难解决问题,你可以自己进行研究,看看大小公司都尝试过解决这类问题但失败了——或者至少没有成功。这个问题用任何方法都很难解决,而使用基于AI的方法成功的可能性比标准编程技术要大得多。现在,我并不是说我们在这本书的任务中会取得超乎想象的巨大成功,但我们的目标是在这个过程中学到很多东西!

因此,我们在定义项目的过程中暂停一下,明确表示我们故意选择使用人工智能和机器学习作为解决其他方法难以证明的问题的方法。

由于我们将教机器人各种任务,如果我们能够远程操作机器人,像遥控车一样驾驶它,以便收集数据并拍照,这些数据我们将用于后续的对象识别,这将更有效。我们不需要这个用于操作,我们需要这个用于训练。我们将把这个必需的操作添加到我们的待办事项列表中。

在我们的下一步中,我们将从所有努力中提取出我们的机器人必须完成的硬件软件任务。但在我们这样做之前,让我们暂停一下,讨论在定义用例范围时常见的错误。

理解我们用例的范围

愿望(由desirerequirements组合而成的词)是那些“想要有”但不是严格必要的功能。例如,如果我们决定添加闪烁的灯光到机器人上因为它看起来很酷,那将是一个愿望。你可能想要它,但这并不有助于机器人的任务或它需要执行的任务。

另一个例子是,如果我们添加了机器人必须在黑暗中运行的条件。在当前情况下,这没有理由,我们也没有在用例中提到机器人将在黑暗中运行——只是在室内房间中。这将是范围蔓延的一个例子,即在没有充分理由的情况下扩展操作条件。重要的是要非常努力地将需求和用例保持在最低限度,甚至要丢弃不必要或冗余的用例。我可能添加了一个按颜色排序玩具的要求,但排序并不能帮助捡起玩具,而且,我只有一个玩具箱。我可能添加这个任务是为了教育我的读者,但这也不助于那个目标,所以颜色排序不包括在内。

现在,让我们继续确定我们的硬件需求。

确定我们的硬件需求

根据我们的故事板,我提取或推导出以下硬件任务:

  • 驱动机器人底座

  • 承载机器人手臂

  • 拉起玩具

  • 将玩具放入玩具箱(手臂长度)

  • 传感器:

    • 机械臂位置

    • 手臂状态(开/关)

    • 机器人视觉(摄像头)用于避障

  • 为所有系统提供电源:

    • 为Nvidia Nano提供5V电源

    • 为Arduino提供5V电源

    • 机械臂电源 – 7.2V

    • 电机电源 – 7.2V

  • 板载计算机:

    • 一台可以接收远程命令的计算机(Wi-Fi Nano):

      • 运行ROS 2

      • 运行Python 3

    • 一台可以与相机接口的计算机

    • 一台可以控制电机的计算机(Arduino)

    • 一个可以驱动机器人手臂伺服电机的接口(伺服控制器)

现在,让我们来看看软件需求。

拆解我们的软件需求

这份软件任务列表是通过审查状态机图、用例和故事板编制的。我已经突出显示了需要AI并将在后续章节中详细介绍的步骤:

  1. 开机自检Power on self-test (POST)):

    1. 启动机器人程序。

    2. 检查Nano能否与Arduino通信并返回。

    3. 尝试与控制站建立通信。

    4. 根据适当的情况报告POST成功或失败并记录在日志中。

  2. 通过Wi-Fi接收遥控命令:

    • 驱动电机底座(右/左/前进/后退)

    • 手臂上下/左右/旋转移动

    • 录制视频或记录图片作为图像文件

  3. 通过Wi-Fi发送遥测数据。

  4. 监控进度。

  5. 发送视频。

  6. 安全导航:

    • 学习避免障碍

    • 学习不摔倒楼梯

  7. 找到玩具:

    • 检测物体

    • 学习分类物体(玩具/非玩具)

    • 确定哪个玩具最近

  8. 拿起玩具:

    1. 移动到手臂可以触及玩具的位置

    2. 制定掌握策略

    3. 尝试抓取

    4. 确定抓取是否成功

    5. 如果没有,尝试用不同的策略再次尝试

    6. 根据成功率重新加权抓取技术得分

  9. 把玩具放进玩具箱:

    • 学习识别玩具箱

    • 找到玩具箱

    • 使用导航驾驶到已知的玩具箱位置

    • 移动到垃圾投放点:

      • 避免障碍

      • 将玩具举过玩具箱盖

    • 放下玩具

    • 检查玩具是否成功掉入玩具箱

    • 如果不行,重新定位再试一次

    • 如果玩具没有掉进玩具箱,我们将其视为地板上的玩具

  10. 确定没有更多的玩具。

  11. 等待指令。

  12. 远程操作:

    • 移动底盘前后左右

    • 移动手臂上下左右

    • 手臂进出/扭转/张开/合上

    • 录制视频/拍照

  13. 模拟个性:

    • 说话

    • 听/识别单词

    • 理解一些命令

    • 讲讲敲门笑话

    • 理解敲门笑话

  14. 语音命令:

    • 清理房间

    • 把这个收起来

    • 过来

    • 停止

    • 等待

    • 简历

    • 回家

    • 向左/向右转

    • 前进/后退

    • 手举/手放

    • 左手/右手

    • 张开手/合上手

在这个列表中,我在哪里看到了远程操作?我们不记得在用例和故事板中讨论过这一点。我们需要教会机器人导航和找到玩具,为此,我们需要移动机器人并拍照。一个简单的方法是通过远程操作(遥控)来驾驶机器人。

编写规格说明

我们接下来的任务是编写我们各种组件的规格说明。这里我将通过一个例子来讲解,这是我们玩具抓取机器人项目必须完成的部分:我们需要选择一个相机。任何旧的相机都不适用——我们需要一个满足我们需求的相机。但那些需求是什么?我们需要编写一个相机规格说明,这样当我们查看要购买的相机时,我们可以判断哪一台能够胜任这项工作。

我们已经创建了我们的故事板和用例,因此我们有了确定我们的相机需要做什么所需的信息。我们可以某种程度上逆向工程这个过程:让我们讨论一下是什么让一个相机与另一个相机不同。首先当然是接口:这个相机安装在机器人上,因此它必须与机器人的计算机接口,该计算机有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英尺,这足够但不是很好。

我带你走过了这个过程,以展示如何编写规范,以及在你决定为你的项目获取哪些传感器时,你应该问自己哪些类型的问题。

摘要

本章概述了在开发机器人项目时,如何开发待办事项列表的建议流程。这个过程被称为系统工程。我们的第一步是创建用例或描述机器人从用户角度应该如何表现。然后,我们通过创建分镜脚本,逐步通过用例来创建更多细节。我们的例子是机器人找到并识别玩具,然后拿起它们并将它们放入玩具箱。我们提取了我们的硬件和软件需求,创建了一个待办事项列表,列出了机器人将能够做什么。最后,我们为我们的关键传感器之一——摄像头编写了规范。

在下一章中,我们将深入探讨我们的第一个机器人任务——使用计算机视觉和神经网络教机器人识别玩具。

问题

  1. 描述一下电影或卡通的分镜脚本与软件程序的分镜脚本之间的区别。

  2. 什么是五个“W”问题?你能想到任何其他与检查用例相关的问题吗?

  3. 完成这个句子:用例显示了机器人做什么,但没有显示______。

  4. 以图3**.16**中的分镜9为例,其中机器人正在驶向玩具箱,并在你自己的分镜脚本中将它分解成更多有序的步骤。考虑在第9帧第10帧之间必须发生的一切。

  5. 完成敲敲门笑话的回复表格,其中机器人回答用户讲述的笑话。你认为最后一步是什么?

  6. 查看远程操作操作。你会添加更多吗,或者这个列表看起来已经很好了?

  7. 为一个使用距离测量来防止机器人驶下楼梯的传感器编写规范。

  8. 一个摄像头在320 x 200像素和30度视野下,能看到多远处的6英寸宽的填充动物,仍然假设我们需要35像素来进行识别?

进一步阅读

关于本章主题的更多信息,您可以参考以下资源:

  • 《SysML实用指南:系统建模语言》,由Sanford Friedenthal、Alan Moore和Rick Steiner著,由Morgan Kaufman出版;这是基于模型的系统工程(MBSE)的标准入门书籍。

  • 《敏捷开发者手册》,由Paul Flewelling著,由Packt出版。

第二部分:将感知、学习和交互添加到机器人技术

为了观察、理解和与环境互动,机器人需要具备感知能力。人工智能是识别物体和导航的一种方法。本部分将赋予你使用人工智能技术高效操作机器人的基本技能。本书的例子是创建一个能够拾取玩具的机器人,因此我们首先从使用神经网络识别玩具开始。然后,我们与机器人手臂合作,使用强化学习遗传算法等工具拾取玩具。下一章将介绍创建一个能够倾听和理解你的命令,甚至讲笑话的机器人数字助手。

本部分包含以下章节:

  • 第4章, 使用神经网络和监督学习识别物体

  • 第5章, 使用强化学习和遗传算法拾取和放置玩具

  • 第6章, 教机器人学会倾听

第四章:使用神经网络和监督学习识别对象

这是我们将开始将机器人技术人工智能AI)结合起来以完成我们在前几章中仔细规划的一些任务的章节。本章的主题是对象识别 – 我们将教会机器人识别玩具,以便它可以决定要捡起什么,留下什么。我们将使用卷积神经网络CNNs)作为机器学习工具,在图像中分离对象、识别它们并在相机帧中定位它们,以便机器人可以找到它们。更具体地说,我们将使用图像来识别对象。我们将拍照,然后查看计算机是否在那些照片中识别特定类型的对象。我们不会识别对象本身,而是识别对象的图像或图片。我们还将围绕对象放置边界框,将它们与其他对象和背景像素分开。

在本章中,我们将涵盖以下主题:

  • 图像处理简要概述

  • 理解我们的对象识别任务

  • 图像处理

  • 使用YOLOv8 – 一个对象识别模型

技术要求

如果您的机器人还不能行走,您将能够完成本章的所有任务而无需机器人。然而,如果摄像头在机器人上的位置正确,我们将获得更好的结果。如果您没有机器人,您仍然可以使用笔记本电脑和USB摄像头完成所有这些任务。

总体来说,以下是您完成本章任务所需的硬件和软件:

本章的源代码可在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(值)并丢弃HS值。正如你可以想象的那样,这要简单得多。这一点很重要,因为在本章中我们将进行大量的图像处理。但在开始之前,在接下来的部分,我们将讨论本章将要执行图像识别任务。

理解我们的目标识别任务

让计算机或机器人识别玩具的图像并不像拍两张照片然后说“如果图片 A 等于图片 B,那么是玩具”那么简单。我们需要做大量的工作才能识别出各种随机旋转、散布在不同位置且距离不同的物体。我们可以编写一个程序来识别简单的形状——例如六边形或简单的颜色块——但无法像填充狗玩具那样复杂。编写一个对图像进行某种分析并计算每个可能排列的像素、颜色、分布和范围的程序将极其困难,而且结果将非常脆弱——它会在光线或颜色发生最轻微的变化时失败。

从经验出发,我最近遇到了一个大型机器人,它使用传统的计算机视觉系统来寻找其电池充电站。那个机器人将一个旧、褪色的饮料机误认为是其充电器——让我们说,我不得不去买更多的保险丝。

我们将采取的做法是教机器人识别一组与我们从不同角度拍摄的玩具对应的图像。我们将通过使用一种特殊的人工神经网络ANN)来实现这一点,该网络对图像执行卷积操作。它被归类为人工智能技术,因为我们不是通过编写代码来编程软件识别物体,而是训练一个神经网络,使其能够通过如何接近网络训练时学习到的标记像素组来正确地分割(从图像的其余部分分离)和标记(分类)图像中的像素组。而不是代码决定机器人的行为,而是我们训练网络时所使用的数据来完成这项工作。由于我们将通过提供分割和标记的图像来训练神经网络,因此这被称为监督学习。这涉及到告诉网络我们希望它学习的内容,并根据其表现的好坏来强化(奖励)网络。我们将在第 8 章中讨论无监督学习。这个过程涉及到我们不对软件确切地说明要学习的内容,这意味着它必须自己确定这一点。

为了澄清,在本节中,我们将告诉人工神经网络(ANN)我们希望它学习的内容,在本例中是识别我们称之为“玩具”的一类物体,并在这些物体周围绘制边界框。这个边界框将告诉机器人的其他部分玩具是可见的,以及它在图像中的位置。

我将强调我们将使用的独特组件来完成识别图像中玩具的任务。你还记得第 3 章中的故事板告诉我们做什么吗?

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_04_1.jpg

图 4.1 – 识别玩具用例

我们的图像识别器必须确定哪些是玩具,然后在图像中定位它们。这在前面的草图中有说明;标记为玩具的物体周围有圆圈。图像识别器不仅要识别它们是什么,还要识别它们在哪里。

图像处理

那么,现在我们有了图像,我们能用它做什么呢?你可能玩过Adobe Photoshop或其他图像处理程序,如GIMP,你知道可以在图像上执行数百种操作、过滤器、更改和技巧。例如,可以通过调整亮度使图像变得更亮或更暗。我们可以增加图像白色部分和黑色部分之间的对比度。我们可以通过应用高斯模糊过滤器使图像变得模糊。我们还可以通过使用如非锐化掩模之类的过滤器使图像(在一定程度上)变得更清晰。你还可以使用边缘检测过滤器,如Canny过滤器,来隔离图像的边缘,其中颜色或值发生变化。我们将使用所有这些技术来帮助计算机识别图像:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_04_2.jpg

图4.2 – 应用到图像上的各种卷积

通过执行这些操作,我们希望计算机软件对图像的大小、拍摄照片的角度或角度不变性以及可用的照明,即照明不变性不敏感。在计算机视觉系统中,这些都是非常理想的——我们不希望一个AI系统只能从与原始图像相同的角度和距离识别我们的玩具。记住,我们将训练我们的视觉系统根据我们事先拍摄的标记训练图像来识别玩具,机器人将必须根据从训练集中学习到的内容来识别物体。在这里,我们将使用那些主要不基于大小、角度、距离或照明的图像特征。这些特征可能是什么?

如果我们从一个常见的家庭用品,比如一把椅子,从几个角度检查它,那么椅子的哪些部分不会改变?简单的答案是边缘和角落。椅子始终有相同数量的角落,并且我们可以从大多数角度看到一致数量的它们。它也有一致的边缘数量。

承认,这确实是对方法的一种简化的描述。我们将训练我们的神经网络(ANN)在一系列可能或可能不独特于该对象的所有图像特征上,并让它决定哪些有用,哪些无用。我们将通过使用一种通用的图像处理方法,称为卷积来实现这一点。

卷积

有时,你会遇到一些数学构造,将复杂任务转化为只是一些加法、减法、乘法和除法。几何中的向量就是这样工作的,在图像处理中,我们有卷积核。结果是,大多数常见的图像处理技术——边缘检测、角点检测、模糊、锐化、增强等等——都可以通过一个简单的数组结构实现。

很容易理解,在图像中,像素的邻居对像素本身的重要性与像素本身一样重要。如果你要去尝试找到盒子的所有边缘像素,你会寻找一种颜色在一侧,另一种颜色在另一侧的像素。我们需要一个函数,通过比较像素的一侧与另一侧的像素来找到边缘。

卷积核是一个矩阵函数,它将权重应用于像素邻居——或者我们正在分析的像素周围的像素。该函数通常写成这样,作为一个3x3的矩阵:

-1 0 1
-2 0 2
-1 0 1

表4.1 – 一个示例卷积核

Sobel边缘检测Y方向上表示。这检测上下方向的边缘。每个块代表一个像素。正在处理的像素位于中心。像素两侧的邻居是其他块——顶部、底部、左侧和右侧。为了计算卷积,将相应的权重应用于每个像素的值,通过乘以该像素的值(强度),然后将所有结果相加。如果这幅图像是彩色的——RGB——那么我们将分别对每种颜色进行卷积计算,然后将结果合并。以下是将卷积应用于图像的示例:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_04_3.jpg

图4.3 – Sobel边缘检测卷积的结果

结果图像与原始图像大小相同。请注意,我们只得到边缘作为结果——如果中心像素两侧的颜色相同,它们会相互抵消,我们得到零,或黑色。如果它们不同,我们得到255,或白色,作为答案。如果我们需要一个更复杂的结果,我们也可以使用5x5卷积,它考虑了每侧的两个最近像素,而不仅仅是其中一个。

好消息是,你不必选择要应用于输入图像的卷积操作——我们将构建一个软件前端,它会设置所有的卷积。这个前端只是程序的一部分,在开始训练之前设置网络。我们将使用的神经网络包将确定哪些卷积提供了最多的数据并支持我们想要的训练输出。

“但是等等,”我听到你说,“如果像素位于图像的边缘,而我们没有一边的相邻像素怎么办?”在这种情况下,我们必须向图像添加填充——这是一个额外的像素边界,允许我们考虑边缘像素。

在下一节中,我们将深入了解神经网络的内部结构。

人工神经元

什么是神经元?我们如何将它们组合成网络?如果你能记住你在生物学中学到的知识,一个生物或自然神经元有输入,或树突,将它们连接到其他神经元或传感器输入。所有输入都汇聚到一个中央体,然后通过轴突,或连接,通过其他树突离开,到达其他神经元。神经元之间的连接称为突触,这是一个信号必须跳过的微小间隙。神经元接收输入,处理它们,并在达到某个阈值水平后激活或发送输出。人工神经元是一种软件构造,它近似于你大脑中神经元的运作方式,是自然神经元的非常简化的版本。它有几个输入,一组权重,一个偏差,一个激活函数,然后作为网络的结果输出到其他神经元,如图所示:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_04_4.jpg

图4.4 – 人工神经元的示意图

让我们详细描述每个组件:

  • 输入:这是一个从其他神经元或作为网络输入接收的数字或值。在我们的图像处理示例中,这些是像素。这个数字可以是浮点数或整数——但它必须只是一个数字。

  • 权重:这是我们为了训练神经元而改变的可调整值。增加权重意味着输入对我们的答案更重要,同样地,减少权重意味着输入的使用较少。为了确定神经元的值,我们必须组合所有输入的值。随着神经网络的训练,每个输入的权重都会进行调整,这有利于某些输入而牺牲其他输入。我们将输入乘以权重,然后将所有结果相加。

  • 偏差:这是一个加到权重总和上的数字。偏差防止神经元陷入零,并改善训练。这通常是一个很小的数字。想象一下这样一个场景,一个神经元的所有输入都是零;在这种情况下,权重将没有任何效果。添加一个小偏差允许神经元仍然有输出,网络可以使用这一点来影响学习。没有偏差,输入为零的神经元无法进行训练(改变权重没有效果)并且是卡住的。

  • 激活函数:这决定了神经元输出的值,基于其输入的加权和。最常见类型的是ReLU(修正线性单元) – 如果神经元的值小于零,输出为零;否则,输出是输入值 – 以及S型函数,这是一个对数函数。激活函数在网络中传播信息,并为神经元的输出引入非线性,这使得神经网络能够逼近非线性函数:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_04_5.jpg

图4.5 – 常见激活函数

  • 输出:序列神经网络中的每一层都连接到下一层。有些层是完全连接的 – 第一层的每个神经元都与第二层的每个神经元连接。其他层是稀疏连接的。在神经网络训练中有一个常见的流程称为dropout,其中我们随机移除连接。这迫使网络为它学习的每一点信息有多条路径,这加强了网络,并使其能够处理更多样化的输入。

  • 输出最大池化:我们使用一种特殊的网络层(与全连接或稀疏层相比),称为最大池化,其中对应于图像中区域的神经元组 – 比如一个2x2像素块 – 被映射到下一层的单个神经元。最大池化神经元只从四个输入神经元中取最大的值。这具有下采样图像(使其变小)的效果。这允许网络将小特征(如Hot Wheels汽车的轮子)与较大特征(如引擎盖或挡风玻璃)关联起来,以识别玩具车:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_04_6.jpg

图4.6 – 最大池化操作

现在您已经了解了神经网络由什么组成,让我们来探讨如何训练和测试一个神经网络。

训练CNN

我想要向您展示本章剩余部分代码中我们将要执行的操作的全过程。请记住,我们正在构建一个卷积神经网络(CNN),它检查视频帧中的像素,并输出图像中是否有一个或多个类似玩具的像素区域,以及它们的位置。以下图表显示了我们将逐步进行神经网络训练的过程:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_04_7.jpg

图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目标模型仅检测到一个颠倒的火柴盒车,并将其错误地标记为滑板:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_04_8.jpg

图4.8 – YOLOv8输出,未针对我们的玩具进行特定训练

您必须承认,从这个角度看,这个小玩具车确实有点像滑板,但这不是我们想要的结果。我们需要检测图像中的所有玩具,而不仅仅是其中一个。我们该怎么办?

答案是我们可以向网络添加新的训练,获得YOLOv8的所有优势,并且我们的自定义对象也能被检测到。为此,我们可以使用一个称为迁移学习的过程。

下面是我们将如何训练我们的玩具检测器的概述,之后我们将更详细地讨论这些步骤:

  1. 首先,我们将准备一个包含玩具房间图像的训练集。这意味着我们必须从机器人视角拍摄大量玩具的照片,使用机器人将使用的相同相机。我们希望从玩具的所有不同角度和侧面拍照。我按顺时针方向绕着房间走,然后逆时针走,每隔几英寸拍一张照片。在这个步骤中,我拍了48张照片。

  2. 接下来,我们必须使用像RoboFlow(https://roboflow.com)这样的数据标注程序来标注图像(您可以参考相关文档以获取详细说明)。该程序允许我们在想要识别的对象(玩具)周围绘制方框,并用标签进行标记——我们将使用名称toy。我们正在将包含玩具的图像部分分离出来,并告诉神经网络这种类型对象的名称:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_04_09.jpg

图4.9 – 使用RoboFlow进行标注,一个免费的数据标注工具

  1. 然后,我们必须将训练集分成三部分:一部分用于训练网络,一部分用于验证训练,另一部分用于测试网络。我们将创建包含87%图像的训练集,8%的验证集和5%的测试集。我们将训练数据和测试数据放在不同的文件夹中。RoboFlow在生成标签页下有相应的流程,其中有一个标记为训练/测试分割的部分。

  2. 现在,我们必须将每张图像的训练值通过将不同图像的部分组合成马赛克来乘以。我们将从四张随机不同的图片中取部分并组合它们。这将使我们的训练集增加三倍,这个过程称为数据增强。这是RoboFlow内置的功能。我开始时有36张图片;经过增强后,我有99张:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_04_10.jpg

图 4.10 – 瓦片数据增强从我们有限的图片数量中创建更多训练数据

为什么我们使用这种瓦片方法?我们仍然希望有有效的边界框。瓦片过程会调整任何与边缘相交的局部边界框的大小。

  1. 接下来,我们将构建两个程序:训练程序,它在我们的台式计算机上运行并训练网络;以及工作程序,它使用训练好的网络来寻找玩具。训练过程可能不会在我们的机器人机载小计算机上运行,或者可能需要很长时间才能运行,所以我们将使用台式计算机来完成这项工作。

  2. 现在,我们需要训练网络。为了实现这一点,我们必须做以下事情:

    1. 首先,我们必须将所有图像缩小以减少处理时间到合理的水平。

    2. 然后,我们必须使用均匀随机权重初始化网络。

    3. 接下来,我们必须对标记的图像进行编码,并将其输入到网络中。神经网络只使用图像数据来预测图片中包含的对象类别及其边界框。由于我们预先用正确的答案标记了图像并使用了正确的边界框,我们可以判断答案是否正确。如果答案是正确的,我们可以通过增加(训练值)来加强导致这个答案的输入权重。如果答案是错误的,我们可以减少权重。在神经网络中,期望结果和实际结果之间的误差称为损失。对每张图像重复这个过程。

    4. 现在,我们必须通过运行测试图像集来测试网络——这些图像是训练集中没有的相同玩具的图片。我们必须分析在这个集合上得到的输出类型(有多少是错误的,有多少是正确的)。如果这个答案超过90%,我们就停止。否则,我们返回并再次运行所有训练图像。

    5. 一旦我们对结果满意——我们通常需要50到100次迭代才能达到这个目标——我们就必须停止并存储训练网络中最终得到的权重。这是我们训练好的CNN

  3. 我们接下来的任务是找到玩具。为此,我们必须通过加载它并使用从实时机器人视频图像中获取的图像来部署训练好的网络。我们将从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是那些有华丽名字的简单概念之一。随机只是意味着随机。我们想要做的是调整神经元的权重,以给出比第一次更好的答案——这就是我们通过调整权重来训练的内容。我们想要改变权重的一小部分——但朝哪个方向?我们想要改变权重的方向,以改善答案——它使预测更接近我们想要的样子。

为了更好地理解这一点,让我们做一个简单的思想实验。我们有一个神经元,我们知道它正在产生错误的答案并且需要调整。我们将增加一点权重并看看答案如何变化。它变得稍微糟糕了——数字离正确答案更远了。所以,我们必须减去一小部分——正如你可能想到的,答案变得更好。我们稍微减少了错误量。如果我们绘制神经元产生的错误图,我们会看到我们正在朝着零误差移动,或者图正在下降到某个最小值。另一种说法是,线的斜率是负的——趋向于零。斜率的大小可以称为梯度——就像你将山丘的斜率或陡峭程度称为梯度一样。我们可以计算偏导数(换句话说,就是误差曲线在此点的斜率),这告诉我们线的斜率。

我们调整整个网络上的权重以最小化真实值和预测值之间损失的方法被称为Y1Y2Y3。我们有三个权重——W1W2W3。我们将有偏差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。如果我们将训练率值加到每个权重上,会发生什么?看看下面的图:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_04_11.jpg

图4.11 – 反向传播如何调整权重

输出值现在上升到 0.5192。我们的错误下降到 0.4808。我们正在正确的道路上!我们错误斜率的梯度是 (0.4808-0.49) / 1 = -0.97。这里的 1 是因为我们到目前为止只有一个训练样本。那么,随机部分从何而来?我们的识别网络可能有 5000 万个神经元。我们不可能对每个神经元都进行所有这些数学运算。因此,我们必须对输入进行随机采样,而不是全部采样,以确定我们的训练是正面的还是负面的。

用数学术语来说,方程的斜率由该方程的导数提供。因此,在实践中,反向传播计算训练周期之间错误的偏导数,以确定错误的斜率,并据此确定我们是否正确地训练了网络。随着斜率的减小,我们降低训练速率到一个更小的数字,以便越来越接近正确答案:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_04_12.jpg

图 4.12 – 梯度下降过程

现在,我们来解决下一个问题:我们如何将权重调整传播到神经网络层?我们可以在输出神经元处确定错误——即标签值减去网络的输出。我们如何将这个信息应用到前一层?每个神经元对错误的贡献与其权重成正比。我们必须将错误除以每个输入的权重,这个值现在就是链中下一个神经元的应用错误。然后,我们可以重新计算它们的权重,依此类推。这就是为什么神经网络需要如此多的计算能力:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_04_13.jpg

图 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

我们预测的结果显示在下面的图中:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_04_14.jpg

图 4.14 – 玩具检测器在工作

通过这种方式,我们已成功使用神经网络创建了一个玩具检测器。检测器的输出,我们将在 第 5 章 中使用它来指导机器人和机械臂驶向玩具并抓取它,看起来是这样的:

"predictions": [
 {
 "x": 287.5,
 "y": 722.5,
 "width": 207,
 "height": 131,
 "confidence": 0.602,
 "class": "toy"
 },

对于每个检测,神经网络将提供一些信息。我们得到边界框中心的 xy 位置,然后是那个框的高度和宽度。然后,我们得到一个置信度数字,表示网络对这个决策是检测的确定性。最后,我们得到物体的类别(是什么类型的物体),当然是一个玩具。

当我们运行神经网络的训练过程时,如果你查看 runs/detect/train 中的 training 文件夹,你会看到一系列图表。这些图表告诉我们什么?

我们首先需要查看的是 F1_curve。这是精确度和召回率的乘积。精确度是所有正例中正确分类的对象的比例。召回率是正确识别的正检测的比例。因此,精确度定义为以下:

精确度 = TP / (TP + FP)

精确度是真实正例数除以真实正例数和假正例数(被识别为检测但实际不是的项)。

召回率的定义略有不同:

召回率 = TP / (TP + FN)

在这里,召回率是真实正例数除以真实正例数加上假负例数。一个假负例是一个漏检或实际上存在但未被检测到的物体。

要创建 F1 曲线,我们必须将精确度和召回率相乘,并将其与 置信度 对应。图表显示了产生最佳结果(在精确度和召回率之间权衡)的检测置信度水平:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_04_15.jpg

图 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自适应下降算法调整我们的结果。

在下一章中,我们将从强化学习和遗传算法的角度探讨机器人臂的机器学习。

问题

  1. 在本章中,我们经历了很多。你可以使用提供的框架来研究神经网络的特性。尝试几种激活函数,或者不同的卷积设置,看看训练过程中有什么变化。

  2. 绘制一个人工神经元的图并标注各部分。查找一个自然的人类生物神经元,并将它们进行比较。

  3. 真实神经元和人工神经元有哪些相同的特征?有哪些不同的?

  4. 学习率对梯度下降有什么影响?如果学习率太大?太小?

  5. 神经网络的第一层与输入有什么关系?

  6. 神经网络的最外层与输出有什么关系?

  7. 查找三种损失函数并描述它们的工作原理。包括均方损失和两种交叉熵损失。

  8. 如果你的网络在训练后达到了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,然后有东西阻止电机达到我们为其设定的目标。这些信息对于训练机器人手臂将非常有价值。

我们可以使用正向运动学,这意味着将手臂的所有角度和杠杆相加,以推断出手的位置(我将在本章后面提供相应的代码)。我们可以将这个手的位置作为我们的期望状态——我们的奖励标准。我们将根据手部与期望位置和方向之间的接近程度给机器人评分,或给予奖励。我们希望机器人能够找出达到该位置所需的方法。我们需要为机器人提供一种测试不同理论或行动的方法,这些行动会导致手臂移动。

我们将首先与机器人手部进行工作,或者用时髦的机器人术语,称为末端执行器

以下图表显示了我们是如何尝试通过旋转手腕来调整我们的机器人手臂以拿起玩具的:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_05_1.jpg

图5.1 – 拿起玩具的故事板

对于抓取,我们有三个动作可以操作。我们将机械臂定位以拾取玩具,通过旋转手腕伺服电机调整手的角位,并关闭手以抓取物体。如果手完全关闭,那么我们错过了玩具,手是空的。如果玩具阻止夹爪关闭,因为我们已经拾取了它,那么我们就成功了,已经抓到了玩具。我们将使用这个过程来教会机器人使用不同的手部位置根据玩具的形状来拾取玩具。

软件设计

设计机械臂控制软件的第一步是建立一个坐标系(我们如何测量运动),然后我们通过创建状态(机械臂位置)和动作(改变位置的运动)来设置我们的解决方案空间。以下图显示了机械臂的坐标系:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_05_2.jpg

图5.2 – 机械臂坐标系

让我们定义机器人的坐标系——我们用来测量运动的参考——如图所示的前图。X方向朝向机器人的前方,因此前后移动沿着X轴。水平移动(左或右)沿着Y轴。垂直移动(上下)在Z方向。我们将零点——我们坐标的原点——放置在机械臂中心的下方,Z=0在地板上。因此,如果我说机器人手部在X轴上正向移动,那么它是在远离机器人的前方移动。如果手(手臂的末端)在Y轴上移动,那么它是在向左或向右移动。

现在,我们必须有一组名称,我们将用它来称呼机械臂中的伺服电机。我们将进行一些拟人化命名,并给机械臂的各个部分赋予解剖学名称。电机在控制系统中编号,我机器人臂上的伺服电机标记如下:

  • 电机1 控制夹爪的开启和关闭。我们也可以将夹爪称为手。

  • 电机2 是手腕旋转电机,它旋转手部。

  • 电机3 是手腕俯仰(上下)方向。

  • 我们将电机4称为肘部。肘部在中间弯曲手臂,正如你所期望的那样。

  • 电机5 是肩部俯仰伺服电机,当机械臂指向正前方时,它使机械臂上下移动,绕Y轴旋转。

  • 电机6 位于机械臂的底部,因此我们将其称为肩部偏航(右或左)伺服电机。它绕Z轴旋转整个机械臂。我决定不移动这个轴,因为由于全向轮,整个机器人底座可以旋转。我们将只移动机械臂的上下位置以简化问题。我们在第8章中开发的导航系统将使机械臂指向正确的方向。

我们将首先定义一个机器人臂的接口,其余的机器人控制系统可以使用:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_05_3.jpg

图5.3 – 机械臂电机命名

在这里,俯仰指的是上下运动,而偏航指的是左右运动。

我们将使用在机器人世界中常见的两个术语来描述我们如何根据我们拥有的数据计算手臂的位置:

  • 正向运动学FK)是从机器人手臂的基座开始,逐步计算出抓取器的位置和方向,依次计算每个关节的位置和方向的过程。我们取关节的位置和角度,并加上该关节与下一个关节之间的手臂长度。这个过程通过计算产生一个X-Y-Z位置和机器人手指末端的俯仰-滚转-偏航方向,称为正向运动学,因为我们是从基座向前计算到手臂的。

  • 逆向运动学IK)采取不同的方法。我们知道手的位置和方向,或者我们希望它在哪里。然后我们沿着手臂向后计算,以确定产生该手位置的关节角度。逆向运动学有点复杂,因为可能有多个解决方案(关节位置的组合)可以产生给定的手结果。用你自己的手臂试一试。抓住门把手。现在在保持手在门把手上的同时移动你的手臂。你的关节有多种组合可以使你的手保持在相同的位置和方向。在这本书中,我们不会使用逆向运动学,但我希望你对这个术语熟悉,它经常在机器人手臂中用来驱动机器人末端执行器(夹具或手)的位置。

若想对这些概念有更深入的解释,你可以参考https://control.com/technical-articles/robot-manipulation-control-with-inverse-and-forward-kinematics/

接下来,让我们讨论如何使手臂运动起来。

设置解决方案

我们将把将电机设置到不同位置的行为称为动作,并将机器人手臂和手的位置称为状态。对一个状态应用动作会导致手臂进入一个新的状态。

我们将让机器人将状态(手的初始位置)和动作(在该状态下使用的电机命令)与产生正或负结果的概率相关联——我们将训练机器人找出哪些动作组合可以最大化奖励。奖励是什么?它只是一个任意值,我们用它来定义机器人完成的学习是积极的——我们想要的——还是消极的——我们不想要的。如果动作导致了积极的学习,那么我们就增加奖励,如果没有,那么我们就减少奖励。机器人将使用一个算法来尝试最大化奖励,并逐步学习一个任务。

让我们通过探索机器学习所扮演的角色来更好地理解这个过程。

机器人手臂的机器学习

由于增量学习也是神经网络的一部分,我们将使用之前在神经网络中使用的一些相同工具,将奖励传播到导致手移动到某个位置的连续动作链中的每一步。在强化学习中,这被称为折现奖励——将奖励的部分分配给多步过程中的每一步。同样,状态和动作的组合称为策略——因为我们正在告诉机器人,“当你处于这个位置,并想要到达那个位置时,执行这个动作。”让我们通过更仔细地观察我们使用机器人手臂进行学习的流程来更好地理解这个概念:

  1. 我们设定了机器人手的最终位置,即机器人手在X和Z坐标上相对于手臂旋转中心的毫米位置。

  2. 机器人将尝试一系列动作,试图接近那个目标。我们不会给机器人提供到达那个目标所需的电机位置——机器人必须学习。初始动作将是完全随机生成的。我们将限制增量动作(类似于上一章中的学习率)的大小,以避免手臂剧烈挥动。

  3. 在每次增量动作中,我们将根据手臂是否更接近目标位置来评分该动作。

  4. 机器人将通过将初始状态和动作(移动)与奖励评分关联来记住这些动作。

  5. 之后,我们将训练一个神经网络,根据起始状态和动作输入生成积极结果的概率。这将使手臂能够学习哪些动作序列能够产生积极的结果。然后,我们将能够根据起始位置预测哪种动作会导致手臂正确移动。

  6. 你也可以推测,我们必须为快速完成任务添加奖励——我们希望结果高效,因此我们将为完成任务的用时最短添加奖励——或者说,我们可以为达到目标所需的每一步减去奖励,这样步骤最少的流程将获得最多的奖励。

  7. 我们使用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’))选择在那种状态下从可用动作集中产生最大奖励的动作。在方程中,sa代表当前状态和动作,而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 创建此接口。请按照以下步骤操作:

  1. 首先,在 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.
    
  2. 我们需要安装 xArm 的驱动程序,以便我们可以在 Python 中使用它们:

    pip install xarm
    
  3. 现在我们转到我们的新源目录:

    xarm_mgr.py, which is short for xarm manager.
    
  4. 打开编辑器,让我们开始编码。首先,我们需要一些导入:

    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. 接下来,我们将创建一些预定义的臂命名位置作为快捷方式。这是一种简单的方法,将臂放置在我们需要的位置。我定义了五个我们可以调用的臂预设位置:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_05_4.jpg

图 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]
  1. 现在我们可以开始定义我们的机器人手臂控制类了。我们将从类定义和初始化函数开始:

    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]。我们发布我们的输出并订阅我们的输入。

  2. 我们的输入,或者说订阅,为手臂提供了外部接口。我思考了如何使用手臂,并提出了我想要的接口。在我们的情况下,我们有一个非常简单的手臂,只需要几个命令。首先,我们有一个名为RobotCmd的字符串命令,它允许我们创建控制机器人模式或状态的命令。这将用于许多机器人的命令,而不仅仅是手臂。我创建了一些手臂模式命令,我们将在接下来的几段中介绍。RobotCmd的有用之处在于我们可以向这个输入发送任何字符串,并在接收端处理它。这是一个非常灵活且有用的接口。请注意,对于每个订阅者,我们都会创建一个函数调用到回调例程。当数据在接口上发布时,我们的程序(xarm_mgr.py)会自动调用回调例程:

    self.cmdSubscribe = self.create_subscription(String, 'RobotCmd', self.cmdCallback,10)
    
  3. 接口的下一段允许我们在偏航方向上移动手臂的底部,并独立操作手和手腕。在本章中,我们开始仅训练夹爪,因此有一个独立的接口来旋转、打开和关闭夹爪是有帮助的。操作手部不会改变夹爪的坐标位置,因此这可以分开。同样,我们通过偏航方向(向右和向左)移动手部,以对齐要抓取的玩具。我们将从这个功能开始锁定,稍后添加偏航功能。这是由我们在上一章中设计的计算机视觉系统控制的,因此需要一个独立的接口。我们有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)
    
  4. 最后的命令接口使我们能够将手臂移动到我们指定的任何位置。通常,我们使用一组数字来命令手臂移动,如下所示:[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)
    
  5. 现在我们已经定义了所有的发布和订阅接口,我们可以打开连接到机器人手臂的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命令数组中伺服电机的快速作弊指南:

[握紧/松开,手腕旋转,手腕俯仰,肘部俯仰,肩部俯仰,肩部偏航]

  1. 我们在源代码中的下一个函数是设置遥测定时器。我们希望定期发布机械臂的位置,以便机器人其他部分可以使用。我们将创建一个定时器回调,它以我们指定的速率定期执行。让我们从每秒一次开始。这是一个信息值,我们不会用它来控制——伺服控制器负责这一点。这是我们需要编写的代码:

            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 例程。

  2. 我们接下来的代码是硬件接口的一部分。由于我们正在初始化机械臂控制器,我们需要打开与机械臂的硬件连接,这是一个使用人机界面设备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),这样我们就可以在程序后续部分避免抛出不必要的错误,并且可以测试机械臂是否已连接。

  3. 下一个代码块是我们的定时器回调,它发布有关机械臂的遥测信息。记住,我们定义了两个输出消息,机械臂位置和机械臂角度。我们可以在这里服务它们:

        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) 来获取,这将返回一个角度而不是伺服单元。

  4. 现在我们可以处理来自其他程序的命令。接下来,我们将为机器人创建一个控制面板,可以发送命令并设置机器人的模式:

        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 命令,该变量包含所有六个电机的伺服位置。

  5. 接下来,我们编写机器人接收并执行手腕伺服电机的代码,该命令旋转夹爪。这个命令发送到电机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主题上发送字符串而不是整数。我们还检查命令中数据的范围是否有效,在这种情况下是01000个伺服单位。如果我们得到越界错误,我们将使用minmax函数将命令限制在允许的范围内。

  6. 末端执行器命令和基础命令(控制整个手臂的左右旋转)的工作方式完全相同:

        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秒)延迟是有效的。如果你省略这个值,只有一个伺服电机会移动,手臂将不会处理其余的命令。伺服电机以菊花链的方式使用串行接口,这意味着它们相互传递消息。每个伺服电机都连接到另一个伺服电机,这比所有伺服电机单独连接要好得多。

  7. 我们可以用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学习强化学习技术训练机器人手臂末端执行器拿起形状奇特的物体涉及几个步骤。以下是该过程的逐步解释:

  1. 定义状态空间和动作空间:

    • 定义状态空间:这包括有关环境和机器人手臂的所有相关信息,例如物体的位置和方向、末端执行器的位置和方向以及任何其他相关传感器数据。

    • 定义动作空间:这些是机器人手臂可以采取的可能动作,例如旋转末端执行器、在不同方向上移动它或调整其夹爪。

  2. 设置Q表:创建一个表示状态-动作对的Q表,并用随机值初始化它。Q表将包含每一状态一行,每一动作一列。当我们测试手臂移动到的每个位置时,我们将使用Q学习方程(在机器人手臂的机器学习部分介绍)计算出的奖励存储在这个表中,以便我们稍后可以参考。我们将通过状态和动作搜索Q表,以查看哪个状态-动作对会产生最大的奖励。

  3. 定义奖励函数:定义一个奖励函数,根据机器手臂的动作为其提供反馈。奖励函数应鼓励手臂成功拿起物体,并阻止不良行为。

  4. 启动训练循环:启动训练循环,它由多个剧集组成。每个剧集代表训练过程的迭代:

    • 重置环境和设置初始状态。

    • 根据当前状态使用探索-利用策略(如ε-贪婪)选择动作,其中以一定的概率(ε)探索随机动作或选择具有最高Q值的动作。

    • 执行选定的动作,并观察新的状态和奖励。

    • 使用Q学习更新方程更新Q表中的Q值,该方程结合了奖励、下一个状态的最大Q值以及学习率(alpha)和折扣因子(gamma)参数。

    • 将当前状态更新为新状态。

    • 重复之前的步骤,直到剧集结束,无论是成功拿起物体还是达到最大步数。

  5. 探索与利用:随着时间的推移调整探索率(用epsilon表示),逐渐减少探索并优先利用学到的知识。这允许机器人臂最初探索不同的动作,并逐渐专注于利用学到的信息以提高性能。

  6. 重复训练:继续多个回合的训练循环,直到Q值收敛或性能达到满意水平。

  7. 执行测试:在训练后,使用学到的Q值在测试环境中做出决策。将训练好的策略应用于机器人臂末端执行器,使其能够根据学到的知识捡起形状奇特的物体。

注意

实现机器人臂末端执行器的Q-learning训练需要软件和硬件组件的组合,例如仿真环境、机器人臂控制器和感官输入接口。具体实现方式可能因机器人臂平台和使用的工具和库而异。

编写代码

现在,我们将通过构建代码来实现我们刚才描述的七个步骤,该代码将使用我们在上一节中制作的机器人臂接口来训练手臂:

  1. 首先,我们包含所需的导入 – 我们将需要实现训练代码的功能:

    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引用。

  2. 接下来,让我们定义一些我们稍后会使用的函数:

    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表示)放在第一位。

  3. 在这一步,我们将声明一个将教会机器人抓取物体的类。我们将把这个类命名为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参数设置每个学习任务重复次数的值。

  4. 下一个代码块完成了学习函数的设置:

     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值,这是我们存储训练结果的地方。

  5. 以下函数集帮助我们控制手臂:

     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允许我们创建新的动作对以进行学习。

  6. 现在我们准备进行手臂训练。这是一个结合了人和机器人活动的过程,真的非常有趣。我们将尝试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值或预期奖励的角度。

  7. 程序的这一部分是机器人手臂将要经历的物理运动:

         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指令。如果我们成功,机器人手臂现在将持有玩具。如果不成功,夹爪将是空的。

  8. 现在我们可以检查夹爪中是否有物体:

         #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变量。

  9. 现在我们进行机器学习部分。我们使用在机器人臂的机器学习部分中引入的特别修改后的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分数。每次成功都会增加奖励,而任何失败都会导致减少。

  10. 为了完成我们的学习函数,我们将更新的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键以继续。

  11. 现在我们来看看程序的其余部分,这部分相当直接。我们必须做一些维护工作,处理一些调用,并构建我们的主要训练循环:

     def cmdCallBack(self,msg):
       robotCmd = msg.data
       if robotCmd == "GoLearnHand":
         self.mode = "start"
       if robotCmd == "StopLearnHand":
         self.mode = "idle"
    

    这个cmdCallBack接收来自RobotCmd主题的命令。在这个程序中,我们只处理两个命令:GoLearnHand,它启动学习过程,以及StopLearnHand,它允许你停止训练。

  12. 这个部分是我们的臂接口到机器人臂的接口,并设置了我们需要用来控制臂的发布/订阅接口:

    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主题上添加了设置机器人臂位置的能力,但你可能不需要这个功能。

    对于每个订阅,我们需要一个回调函数。我们有一个armPosCallbackarmAngleCallback,当臂发布其位置时将被调用,我将此设置为每秒2赫兹,即每秒两次。如果你觉得有必要,可以在xarm_mgr程序中增加这个速率。

  13. 现在我们进入主程序。对于许多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块,当找不到这个训练文件时发送错误消息。这将在你第一次运行程序时发生,但我们将很快为下一次运行创建一个新的文件。

    我们还实例化了臂训练器和臂接口的类变量,这创建了我们的训练程序的主要部分。

  14. 这是我们的训练循环的核心。我们设置了训练的方面和试验重复次数:

    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次。欢迎来到机器学习!

  15. 你可能会猜到我们最后要做的事情是保存我们的训练数据,如下所示:

    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. 我们进行一次试验运行,从位置1(中性携带)到位置2(拾取)。在将手放入正确位置之前,机器人将手臂移动100次。为什么是100次?我们需要足够大的样本空间,以便算法能够探索不同的解决方案。当值为50时,解决方案没有满意地收敛,而值为200时,结果与100次相同。

  2. 我们根据目标完成百分比对每次移动进行评分,表明这次移动对目标的贡献程度。

  3. 我们将10个最佳移动放入数据库中。

  4. 我们再次进行测试,并做同样的事情——现在我们有10个更多的“最佳移动”和20个动作在数据库中。

  5. 我们从第一组中选取五个最佳动作,与第二组中选取的五个最佳动作进行交叉——再加上随机选择的五个动作和五个完全随机的动作。交叉两个解决方案指的是从第一组中取一段,从第二组中取一段的过程。在遗传学的术语中,这就像从两个父母中各取一半的DNA来制造一个新的孩子

  6. 我们运行这一系列动作,然后选择10个最佳的单个动作并继续进行。

通过选择的过程,我们应该很快就能得到一个执行任务的序列。它可能不是最优的,但它是有效的。我们正在管理我们的基因库(我们问题的试验解决方案列表),通过连续近似来创建一个问题的解决方案。我们希望保持一个良好的可能性混合,这些可能性可以用不同的方式组合起来,以解决将我们的手臂移动到目标位置的问题。

我们实际上可以使用几种杂交我们动作序列的方法。我描述的是一种简单的杂交——第一父母遗传物质的一半和第二父母物质的一半(如果你能原谅这个生物学的比喻)。我们也可以使用四分之一——四分之一第一,四分之一第二,四分之一第一,四分之一第二——来进行两次杂交。我们也可以随机从其中一个或另一个中抓取片段。我们现在将坚持一半/一半的策略,但你完全可以根据自己的意愿进行实验。本质上,在这些所有选项中,我们都在采取一个解决方案,将其一分为二,然后随机将其与另一个试验中一半的解决方案结合。

你可能要提出一个反对意见:如果动作少于10步怎么办?简单——当我们到达目标时,我们停止,并丢弃剩余的步骤。

注意

我们不是在寻找完美或最优的任务执行,只是足够好以完成任务的东西。对于许多实时机器人,我们没有时间上的奢侈来创建一个完美的解决方案,所以任何能完成任务的解决方案都是足够的。

为什么我们要添加五个额外的随机样本动作和五个完全随机的动作?这也模仿了自然选择——变异的力量。我们的遗传代码(我们体内的DNA)并不完美,有时劣质材料会被传递下去。我们也可能从基因的坏副本、宇宙射线和病毒中经历随机突变。我们引入一些随机因素来调整我们算法的调谐——自然选择的元素——以防我们收敛到一个局部最小值或错过一些简单的路径,因为在我们之前的动作中还没有发生。

但为什么我们要费这么大的劲呢?遗传算法过程可以为软件做一件非常困难的事情——它可以通过尝试直到找到有效和无效的方法,从基本动作中创新或进化出新的解决方案。我们提供了一个额外的机器学习过程来添加到我们的工具箱中,但这是一个可以创建我们程序员没有预先设想出的解决方案的过程。

现在,让我们深入GA过程。为了提高透明度,我们将从头开始构建自己的GA过程。

注意

在这个版本中,我们将构建自己的工具,但也有一些预构建的工具集可以帮助你创建GA,例如pip install deap

构建GA过程

我们松散地采用“适者生存”的概念来决定哪些计划是最适合的,并得以生存和繁衍。我给你一个沙盒,让你在其中扮演遗传工程师的角色,你将能够访问所有部件,没有任何东西隐藏在幕后。你会发现,对于我们的问题,代码并不那么复杂:

  1. 我们将首先创建computefitness函数,即评分我们的遗传材料。适应性是我们评估算法的标准。我们可以随心所欲地改变适应性,以调整我们的输出以满足我们的需求。在这种情况下,我们正在为机器人手臂从起始位置到目标位置构建空间路径。我们根据路径上的任何点接近目标的方式评估我们的路径。就像我们之前的程序一样,机器人的运动由三个电机的顺时针、逆时针或不动这27种组合构成。我们将运动分成小步骤,每个步骤大约是三个电机单元(1.8度)的运动。我们将这些步骤连在一起形成一个路径。适应性函数沿着路径前进,并在每个步骤计算手的位置。

  2. predictReward函数对机器人手由于该步骤而移动的位置进行试验计算。假设我们顺时针移动电机1三个步骤,保持电机2不动,并逆时针移动电机3三个步骤。这导致手稍微向上和向外移动。我们通过每个步骤接近目标的方式单独评分。我们的评分是100分;100分正好在目标处,我们每100分之1的距离从目标处减去一分,最多减去340毫米。为什么是340?这是手臂的总长度。我们评分的方式可能与你想象的略有不同。总奖励的加总没有区别,因为我们想要的是最接近目标点的点。因此,我们选择具有最高奖励的单个步骤并保存该值。我们丢弃该步骤之后的任何步骤,因为它们只会让我们离目标更远。因此,我们自动修剪路径,使其在目标处结束。

  3. 我使用术语“等位基因”来表示整个路径中的一个单独步骤,我将其称为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
    
  4. 我们如何创建初始路径?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
    
  5. 我们使用 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 = []
    
  6. 我们首先用随机部分构建初始种群。它们的原始适应度将非常低:大约 13% 或更低。我们维持一个包含 300 个个体路径的池,我们称之为染色体:

    for i in range(population): pop.append(make_new_individual())
      trainingData=[] epochs = 100
    
  7. 在这里,我们设置循环以遍历 100 代的自然选择过程。我们首先计算每个个体的适应度,并将该分数添加到一个适应度列表中,该列表的索引指向染色体:

    for jj in range(epochs):
      # evaluate the population
      fitnessList = computeFitness(pop,GOAL,learningRate, INITIAL_POS)
    
  8. 我们按逆序排序适应度以获得最佳个体。最大的数字应该排在第一位:

    fitnessList.sort(reverse=True)
    
  9. 我们保留种群中排名前 50% 的个体,并丢弃排名后 50% 的个体。下半部分由于不适应而被排除在基因池之外:

    fitLen = 150
    fitnessList = fitnessList[0:fitLen] # survival of the fittest...
    
  10. 我们从整个列表中挑选出表现最好的个体,并将其放入 名人堂HOF)。这将是我们的最终输出。同时,我们使用 HOF 或 HOF 适应度HOFF)值作为这一代适应度的衡量标准:

     hoff = pop[fitnessList[0][1]]
     print("HOF = ",fitnessList[0])
    
  11. 我们将 HOFF 值存储在 trainingData 列表中,以便在程序结束时绘制结果图:

    trainingData.append(fitnessList[0][0])
    newPop = []
    for ddex in fitnessList: newPop.append(pop[ddex[1]])
      print ("Survivors: ",len(newPop))
    
  12. 在这个阶段,我们已经删除了种群中排名后 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:]
    
  13. 我们的下一步是 变异。在真实自然选择中,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)
    
  14. 现在我们已经完成了所有的处理,我们将这条新的后代路径添加到我们的种群中,并为下一代评估做好准备。我们记录一些数据并返回到起点:

    # 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()
    

那么,我们的疯狂遗传实验做得怎么样?以下输出图表自说自话:

https://github.com/OpenDocCN/freelearn-dl-zh/raw/master/docs/ai-rbt/img/B19846_05_5.jpg

图5.5 – GA解决方案的学习曲线

尽管GA看起来像是一种有点像巫术的编程,但作为训练我们机器人臂的特定案例的机器学习工具,它工作得相当好。我们的解决方案在90代左右达到了99.76%的目标(大约2毫米),这对于人工智能学习过程来说相当快。你可以看到学习的平滑性,这表明这种方法可以用来解决我们机器人臂的路径规划问题。我必须承认,我对这个过程相当怀疑,但它似乎在这个特定的问题领域工作得相当好。

编程实际上并不太难,你可以花些时间通过调整GA的参数来改进这个过程。如果我们有一个更小的种群会怎样?如果我们改变了适应度标准会怎样?进去,捣鼓一下,看看你能学到什么。

替代的机器人臂机器学习方法

通过机器学习进行机器人臂控制的领域实际上才刚刚开始。有几个研究方向我想在您寻找进一步研究时引起您的注意。理解机器人运动的一种方法就是考虑利用探索之间的平衡。利用就是尽可能快地将机器人带到目标位置。探索则是利用机器人周围的空间尝试新事物。路径规划程序可能已经陷入了局部最小值(可以想象成死胡同),可能存在更好的、更优的解决方案,而这些方案尚未被考虑。

教导机器人的方法不止一种。我们在训练中一直使用一种自我探索的形式。如果我们能够向机器人展示该做什么,并让它通过示例学习会怎样?我们可以让机器人观察人类执行同样的任务,并让它尝试模仿结果。让我们在接下来的章节中讨论一些替代方法。

谷歌的SAC-X

谷歌正在尝试一种稍微不同的方法来解决机器人臂问题。在他们的计划辅助控制SAC-X)程序中,他们认为给机器人臂的个别动作分配奖励点可能相当困难。他们将复杂任务分解成更小的辅助任务,并为支持这些任务的辅助任务分配奖励点,让机器人逐步建立起面对复杂挑战的能力。如果我们用机器人臂堆叠方块,我们可能会将拾取方块作为一个任务,手持方块移动作为另一个任务,等等。谷歌将这种如果只在主要任务上使用强化,即堆叠方块在另一个方块上作为稀疏奖励问题。你可以想象,在教机器人堆叠方块的过程中,会有数千次失败的尝试,直到一个成功的移动导致奖励的产生。

亚马逊机器人挑战赛

亚马逊有数百万个箱子、零件、碎片和其他东西堆放在货架上。该公司需要将这些东西从货架上取下来放入小箱子中,以便在你下单时尽可能快地将它们运送到你那里。在过去几年中,亚马逊赞助了亚马逊机器人挑战赛,邀请来自大学的团队使用机械臂从货架上取下物品,然后,正如你所猜到的,将它们放入箱子中。

当你考虑到亚马逊几乎销售所有可以想象得到的东西时,这是一个真正的挑战。2017年,来自澳大利亚昆士兰州的一支团队凭借一个低成本机械臂和一个非常好的手部追踪系统赢得了挑战。

摘要

本章的任务是使用机器学习教机器人如何使用它的机械臂。我们使用了两种技术,并做了一些变体。我们使用了多种强化学习技术,或称为Q学习,通过根据机器人机械臂的状态选择单个动作来开发运动路径。每个动作都被单独评分作为奖励,作为整体路径的一部分作为价值。这个过程将学习结果存储在一个Q矩阵中,可以用来生成路径。我们通过索引,或编码,从可能的电机组合的27元素数组中提取动作作为从0到26的数字,同样将机器人状态索引到状态查找表中。这导致学习过程的速度提高了40倍。我们的Q学习方法在处理机器人机械臂可能处于的大量状态时遇到了困难。

我们的第二种技术是遗传算法(GA)。我们创建了个体的随机路径来形成一个种群。我们创建了一个适应度函数来评估每条路径与我们的目标,并保留每一代的顶尖表现者。然后,我们从两个随机选择的个体中交叉遗传物质来创建一个新的子路径。GA还通过在路径步骤中随机改变一小部分来模拟突变。GA的结果显示,对于我们的机器人机械臂的状态空间复杂性没有问题,并在几代之后生成了一个有效的路径。

我们为什么要费这么大的劲?当其他经验方法要么难以实现,要么不可靠,或者不产生在合理时间内解决问题的解决方案时,我们使用机器学习技术。我们还可以使用这些技术解决可能对暴力或仅数学解决方案难以处理的大量更复杂的问题。

在下一章中,我们将为机器人添加一个带有自然语言处理功能的语音界面,这样你就可以与机器人交谈,它会倾听——并回应。

问题

  1. 在Q学习中,Q代表什么?

    提示:你需要自己进行研究。

  2. 我们能做些什么来限制Q学习算法需要搜索的状态数量?

  3. 改变学习率对学习过程有什么影响?

  4. 在 Q-learning 方程中,哪个函数或参数用于惩罚较长的路径?增加或减少这个函数会有什么影响?

  5. 在遗传算法中,你将如何对较长的路径进行惩罚,以便更偏好较短的路径(步骤数量较少)?

  6. 查找 SARSA 变体的 Q-learning。你将如何将 SARSA 技术应用到程序 2 中。

  7. 改变遗传算法中的学习率会有什么影响?学习率的上限和下限是多少?

  8. 在遗传算法中,减少种群数量会有什么影响?

进一步阅读

Logo

更多推荐