当前位置: 首页 > news >正文

行为树 Behavoir Tree入门教程|讲的最清晰的教程(大概)

在自主系统中,我们看到了一系列超越“简单”编程的抽象化,用于行为建模和执行。一些常见的抽象化形式包括teleo-reactive程序、Petri网、有限状态机(FSMs)和行为树(BTs)。FSMs和BTs是当今最常见的两种抽象化形式。通常,它们包括一组有限的实体,这些实体映射到系统内的特定行为或操作模式,比如“向前移动”,“关闭夹具”,“闪烁警告灯”,“前往充电站”。每个模型类都有一套规则,描述了主体应该何时执行这些行为,更重要的是主体应该如何在它们之间进行切换。

行为树(BTs)就是这样一种抽象概念,它有下面的特点:

  • 行为树是树状结构:它们从根节点开始,被设计为按特定顺序遍历,直到达到终端状态(成功或失败)为止。

  • 叶节点是可执行行为:每个叶节点都会执行一些操作,无论是简单的检查还是复杂的动作,并输出状态(成功、失败或运行中)。换句话说,叶节点是你将行为树连接到特定应用程序的底层代码的地方。

  • 内部节点控制树的遍历:树的内部(非叶)节点将接受其子节点的结果状态,并应用自己的规则来决定下一个应该扩展的节点。

行为树实际上起源于视频游戏行业,用于定义非玩家角色(NPC)的行为:无论是虚幻引擎还是Unity(这两者在这个领域都是主要力量)都有专门用于创作BTs的工具。这并不奇怪;BTs的一个重要优势是它们很容易组合和修改,甚至在运行时。然而,与其他抽象概念相比,这牺牲了设计反应性行为(例如模式切换)的便利性,这一点稍后将在本文中详细介绍。

行为树基本概念/术语

让我们深入了解行为树中的术语。 虽然文献和各种软件库中的语言不太统一,但基本上会遵循《机器人和人工智能行为树》中的定义。

下图是构成行为树的节点类型以及它们在图形上的表示方式:

行为树节点分类

行为树按照设定的频率(一般为100-200hz)以离散的更新步骤(称为ticks)从根节点依次向子结点执行。当行为树被tick时,通常在特定速率下,其子节点会根据树的构建方式递归tick。节点tick后,会向其父节点返回状态,可以是成功(Success)、失败(Failure)或运行中(Running)。

执行节点(Execution node)是行为树的叶子,可以是 Action 或 Condition 节点。 唯一的区别在于,Condition节点在单个 ticks 中只能返回 Success 或 Failure,而Action节点可以跨越多个 ticks 并可以返回 Running,直到它们达到最终状态。 通常,条件节点代表简单检查(例如“夹爪是否打开?”),而动作节点代表复杂动作(例如“打开门”)。

控制节点(Control node)是内部节点,定义如何在其子节点的状态之下遍历行为树。 重要的是,控制节点的子节点可以是执行节点或控制节点本身。 顺序(sequence),选择(Fallback)和并行(parallel)节点可以有任意数量的子节点,但在处理这些子节点时有所不同。 修饰(Decorator)节点必须有一个子节点,并使用一些自定义定义的策略修改其行为,它可以决定子节点是否、何时以及多少次被tick。

下图表示不同的控制节点的触发逻辑:

顺序节点按顺序执行子节点,直到一个子节点返回失败或所有子节点返回成功。

选择按顺序执行子节点,直到其中一个返回成功或所有子节点都返回失败。这些节点在设计主体的恢复行为时至关重要。

并行节点将以“并行”的方式执行所有子节点。这里用引号是因为这不是真正的并行;在每个时刻,每个子节点将按顺序独立地执行。并行节点在至少有M个子节点(介于1和N之间)成功时返回成功,当所有子节点都失败时返回失败。

装饰器节点使用自定义策略修改单个子节点。 装饰器具有自己的一套规则来改变“被装饰节点”的状态。 例如,“Invert”装饰器将把成功改为失败,反之亦然。 虽然装饰器可以增加行为树组合的灵活性,但您应尽可能坚持使用标准控制节点和常见装饰器,以便他人能够轻松理解您的设计。

例子:搜寻物体

理解前一节中的所有术语和图形的最佳方式是通过一个例子。 假设我们有一个移动机器人,必须在家居环境中搜索特定物体。 假设机器人事先知道所有搜索位置; 换句话说,它已经有一个可操作的世界模型。

让我们从简单的开始。如果只有一个位置(我们称其为A),那么行为树就是必要行动的简单序列:前往该位置,然后寻找物体。

我们选择将导航表示为一个动作节点,因为机器人移动可能需要一些时间(在过程中返回Running)。另一方面,我们将视觉表示为条件节点,假设机器人一旦到达目的地就可以从单个图像中检测到物体。

一个非常常见的设计原则在书中被定义为明确的成功条件。简单来说,你几乎每次都需要在行动之前进行检查。例如,如果你已经在特定位置,为什么不在开始导航动作之前检查一下是否已经到达那里呢?

明确的成功条件使用一个fallback节点,条件在动作之前。如果成功条件未满足,下一个动作将会执行。

我们的机器人可能在一个有多个地点的环境中运行,想法是在所有可能的地点查找,直到找到感兴趣的物体。这可以通过引入一个根级别的回退节点,并按照特定顺序重复上述行为来实现。

最后,假设我们不是在寻找单个对象,而是想考虑几个对象 —— 比如苹果和橘子。通过并行节点展示了组合条件的用例,如下所示。

  • 如果我们接受苹果或橘子(“或”条件),那么如果一个节点返回成功,我们就成功了。

  • 如果我们要求苹果和橘子两者都必须(“与”条件),那么只有两个节点都返回成功我们才成功。

  • 如果我们关心对象的顺序,例如你必须先找到一个苹果再找到一个橘子,那么这可以通过序列节点来实现。

当然,你也可以同时执行多个动作 — 例如,原地转动直到连续5次检测到一个人。虽然我的例子希望足够简单以便理解基础知识,但我强烈建议查阅文献以获取更复杂的示例,展示行为树的真正威力。

https://youtu.be/5wiJis5HS6U

机器人示例复盘: Decorators 和 blackboards

回看上面的行为树,会感觉有一些冗杂,它只是相同的行为被复制粘贴多次放在一个fallback节点下面。如果你有20个不同的地点,而每个地点的行为不仅仅涉及两个简单的执行节点呢?事情很快就会变得混乱起来。

在大多数为行为树设计的软件库中,你可以将这些执行节点定义为共享资源的参数化行为(例如,用于导航的相同ROS动作客户端,或者用于视觉的物体检测器)。同样,你可以编写代码自动构建复杂的树,并从一个现成的子树库中组合它们。因此,问题不是效率,而是可读性。

对于这个行为树,有一种替代实现方法,可以扩展到许多其他应用程序。下面是一些小tips:

  • 引入decorator:不要为每个位置重复相同的子树,而是使用一个单一的子树,并用重复行为直到成功的策略来装饰它。

  • 在每次迭代中更新目标位置:假设现在有一个要访问的目标位置列表,所以在每次子树的迭代中,你从队列中弹出一个元素。如果队列最终变空,那么我们的行为树失败了。

在大多数行为树中,我们经常需要一些共享数据的概念,比如我们正在讨论的位置队列。这就是blackboard的概念发挥作用的地方:你会在大多数行为树库中找到blackboard构造,它们实际上只是一个共同的存储区,个别行为可以读取或写入数据。

现在我们的示例行为树可以重构如下。我们引入一个“GetLoc”动作,从我们已知位置的队列中弹出一个位置,并将其写入黑板作为某个参数target_location。如果队列为空,这将返回失败;否则返回成功。然后,处理导航的下游节点可以使用这个target_location参数,每次子树重复时都会更改。

你可以利用blackboard来完成许多其他任务。这是我们示例的另一个延伸:假设在找到一个物体之后,机器人应该与它检测到的物体交流,如果有的话。所以,“FoundApple”和“FoundOrange”条件会写入黑板中的located_objects参数,接下来的“Speak”动作会相应地读取它。类似的解决方案也可以应用在机器人需要捡起检测到的物体,并根据物体的类型采取不同的处理策略的情况下。

行为树开发使用的软件库

有很多专门用于BT的库,但在机器人领域,推荐的两个是py_trees和BehaviorTree.CPP。

py_trees是由Daniel Stonier创建的Python库。

  • 因为它使用类似Python这样的解释型语言,接口非常灵活,基本上你想做什么都可以……这有它的优点和缺点。我个人认为,如果你计划在运行时自动修改行为树,这是一个不错的选择。

  • 它正在积极开发中,每次发布都会有新功能。然而,许多新的发展——不仅包括额外的装饰器和策略选项,还有可视化和日志工具——已经在ROS 2中全速推进。所以如果你仍在使用ROS 1,你会发现自己错过了很多新东西。查看PyTrees-ROS生态系统页面以了解更多详情。

  • 一些术语和设计范式与《机器人行为树》一书中的行为树略有不同。例如,这个库使用选择器节点而不是fallback节点,它们的行为略有不同。

BehaviorTree.CPP 是由 Davide Faconti 和 Michele Colledanchise 开发的 C++ 库(是的,其中一个书籍作者)。因此,这个库更忠实地遵循了书中的标记,这一点应该不足为奇。

  • 这个库正迅速获得 ROS 开发者生态系统中行为树库的关注,因为 C++ 同样是机器人技术生产质量开发的语言。实际上,官方的 ROS 2 导航栈在其 BT Navigator 功能中使用了这个库。

  • 它在 XML 工作流程上有很大依赖,这意味着推荐的编写 BT 的方式是通过 XML 文件。在你的代码中,你可以使用用户定义的类注册节点类型(这些类可以继承自丰富的现有类库),你的 BT 就会自动合成!

  • 它配备了一个名为 Groot 的强大工具,不仅是一个可视化工具,还是一个用于编辑行为树的图形界面。XML 设计原则基本上意味着你可以绘制一个 BT 并将其导出为一个 XML 文件,然后将其插入到你的代码中。

  • 如果你事先了解你的 BT 结构,这一切都能很好地运作,但如果你计划在运行时修改你的树,可能会有些欠缺。当然,你也可以使用编程方法来实现这一点,而不是使用 XML,但这种工作流程没有文档/推荐,并且目前与可视化工具不太兼容。

那么你该如何在这两个库之间做出选择呢?它们都很成熟,包含丰富的工具,并与ROS生态系统很好地集成。最终归根结底,取决于您是想使用 C++ 还是 Python 进行开发。在示例 GitHub 库中,我尝试了它们两个,因此可以自行决定!

Behavior trees vs. finite-state machines

在我在 MathWorks 的时候,我频繁使用 Stateflow 设计机器人行为的状态机中 。然而,机器人领域的人经常问我是否有类似的工具来建模行为树,而那时我从未听说过这个概念。快进到我在 CSAIL 的第一天,当时的同事(Daehyung Park)向我展示了他的一个代码库,我终于看到了我的第一个行为树。没过多久,我就开始在我的项目中使用它们作为规划和执行之间的一层,这也是我在 2020 年总结博客文章中描述的。

作为一个经常思考“行为树和有限状态机有什么不同”的人,我想重申它们各自的优势和劣势,最好的做法是学会何时问题更适合其中一个(或两者兼有)。

《机器人与人工智能中的行为树》一书更详细地展开了这些思考,但这里是我试图总结一些关键思想:

  • 从理论上讲,可以将任何东西表达为行为树、有限状态机、其他抽象之一,或者作为简单代码。然而,每种模型在帮助大规模设计时都有其优势和劣势。

  • 特别是对于行为树与有限状态机,模块化(modularity)和反应性(reactivity)之间存在一种权衡。一般来说,行为树更容易组合和修改,而有限状态机在设计反应性行为方面有其优势。

让我们通过另一个机器人示例深入探讨这些比较。假设我们有一个拾取任务,机器人必须移动到一个物体旁边,通过关闭夹持器抓取它,然后返回到其初始位置。下面是行为树和有限状态机的并排比较。对于这样一个简单的设计,两种实现都相对清晰且易于理解。

现在,如果我们想要修改这种行为会怎么样?比方说,我们首先要检查抓取前的位置是否有效,必要时进行纠正再闭合夹爪。使用BT,我们可以直接沿着预定的动作序列插入一个子树,而使用FSM则需要重新连接多个转换。这就是我们所说的BT对于模块化很棒的地方。

另一方面,还有一个响应性的问题。假设我们的机器人正在使用有限的电源,所以如果电池电量低,它必须先返回充电站再继续任务。你可以用行为树来实现类似的功能,但是一个完全的响应式行为(也就是说,电池状态导致机器人无论在哪里都去充电)用有限状态机更容易实现...尽管看起来有点凌乱。

说到“凌乱”,行为树狂热者倾向于用“意大利面状态机”来反驳为什么你永远不应该使用有限状态机。我认为这不是一个公平的比较。分层有限状态机(HFSM)的概念已经存在很长时间了,如果你遵循良好的设计实践,就可以避免这个问题,就像下面所示的那样。然而,管理HFSM中的转换仍然比在BT中添加或删除子树更困难。

为了使BT在这些应用中更具响应性(reactive),已经定义了特定的结构。例如,有一个 “Reactive Sequence” 的概念,即使它们返回成功,仍然可以继续对序列中的前面的孩子进行处理。在我们的例子中,这将允许我们在动作序列中任何时候电池电量低时用失败来终止子树,这可能是我们想要的。

向行为树添加电池检查和充电动作很容易,但请注意,这个检查并不是reactive的 - 它只会在序列开始时发生。要实现更多的反应性会让行为树的设计变得更复杂,但可以通过像"reactive sequence"这样的结构来实现。

有限状态机可以通过允许在任意两个状态之间定义转换来实现这种reactive。

层级有限状态机可以清晰整理出图表。在这种情况下,我们定义了一个名为“Nominal”的超级状态,从而定义了正常操作和充电之间的两种清晰工作模式。

因此,由于这种模块化/反应性的权衡,我认为有限状态机在管理更高层级的操作模式(如正常操作与充电)方面表现优秀,而行为树则擅长构建复杂的行为序列,非常擅长处理故障恢复。因此,如果这个设计由我负责,可能会是一个看起来像这样的混合模式:

结论

谢谢阅读这篇介绍性文章,期待您的评论、问题和建议。如果您想尝试代码示例,请查看示例GitHub存储库。

要了解更多关于行为树的知识,以下是我在过去一年多中依赖的一些好资源。

  • 《机器人和人工智能中的行为树: 介绍》- Michele Colledanchise和Petter Ögren 的教材。我非常推荐这本书!

  • py_trees 和 BehaviorTree.CPP 软件库。

  • Behavior Trees for AI: How They Work — blog post by Chris Simpson.

  • 比较分层有限状态机和行为树的幻灯片,由Kyong-Sok Chang和David Zhu 制作。

参考文章

https://roboticseabass.com/2021/05/08/introduction-to-behavior-trees/

https://robohub.org/anatomy-of-a-robotic-system/

相关文章:

  • 【介绍下R-tree,什么是R-tree?】
  • linux Ubuntu安装samba服务器与SSH远程登录
  • 基于构件开发模型-系统架构师(八)
  • 第一章 Docker入门
  • Mysql查询分析工具Explain的使用
  • Django里choices字段使用中文使用
  • 数据库索引推荐大PK,DBdoctor和资深DBA的终极较量
  • Hbase布隆过滤器
  • 手机丢失不惊慌,华为手机已升级至楼层级设备查找!
  • C++作业第四天
  • Handler通信机制
  • [论文笔记]Mixtral of Experts
  • 新版FMEA培训的应用误区是如何产生的?
  • XML解析库tinyxml2库使用详解
  • Windows系统安装Docker环境详细教程
  • 【腾讯Bugly干货分享】从0到1打造直播 App
  • 002-读书笔记-JavaScript高级程序设计 在HTML中使用JavaScript
  • 10个最佳ES6特性 ES7与ES8的特性
  • CEF与代理
  • Netty+SpringBoot+FastDFS+Html5实现聊天App(六)
  • Otto开发初探——微服务依赖管理新利器
  • React as a UI Runtime(五、列表)
  • Redis 中的布隆过滤器
  • SQLServer之创建数据库快照
  • webpack4 一点通
  • 对话 CTO〡听神策数据 CTO 曹犟描绘数据分析行业的无限可能
  • 如何使用 JavaScript 解析 URL
  • 视频flv转mp4最快的几种方法(就是不用格式工厂)
  • 通过git安装npm私有模块
  • 新手搭建网站的主要流程
  • 一个6年java程序员的工作感悟,写给还在迷茫的你
  • 最近的计划
  • Spark2.4.0源码分析之WorldCount 默认shuffling并行度为200(九) ...
  • 东超科技获得千万级Pre-A轮融资,投资方为中科创星 ...
  • 国内开源镜像站点
  • ​七周四次课(5月9日)iptables filter表案例、iptables nat表应用
  • (173)FPGA约束:单周期时序分析或默认时序分析
  • (2)(2.4) TerraRanger Tower/Tower EVO(360度)
  • (LeetCode 49)Anagrams
  • (PySpark)RDD实验实战——取一个数组的中间值
  • (附源码)springboot 基于HTML5的个人网页的网站设计与实现 毕业设计 031623
  • (力扣)1314.矩阵区域和
  • (六)激光线扫描-三维重建
  • (十一)手动添加用户和文件的特殊权限
  • (四) 虚拟摄像头vivi体验
  • (完整代码)R语言中利用SVM-RFE机器学习算法筛选关键因子
  • (幽默漫画)有个程序员老公,是怎样的体验?
  • (转)MVC3 类型“System.Web.Mvc.ModelClientValidationRule”同时存在
  • ******之网络***——物理***
  • *++p:p先自+,然后*p,最终为3 ++*p:先*p,即arr[0]=1,然后再++,最终为2 *p++:值为arr[0],即1,该语句执行完毕后,p指向arr[1]
  • .bat批处理出现中文乱码的情况
  • .net 7和core版 SignalR
  • .NET CF命令行调试器MDbg入门(三) 进程控制
  • .net core 6 使用注解自动注入实例,无需构造注入 autowrite4net
  • .NetCore 如何动态路由