行为树是一种描述计划或任务执行的数学模型,在计算机科学、机器人学、控制科学和电子游戏中均有应用。行为树用模块化的方式描述有限的任务集合之间的转换。其优势在于可以创建由简单任务组成的复杂任务,而不需要考虑简单任务是如何执行的。
行为树表现出与层级状态机(Hierarchical state machines)的许多相似性,但又有着关键的区别:行为树的主要构建模块是任务而不是状态。
因为行为树更容易被理解,在设计中相对不容易出错,使得其在游戏开发领域颇为流行。
行为树源于在计算机游戏开发中一个非常强大的对非玩家角色(NPC)的行为建模。它被诸如Halo、Bioshock和Spore等游戏高度使用。行为树在游戏人工智能教材中已经被认为是发展成熟的,在主流的游戏开发软件Unity和Unreal Engine中也是被广泛使用。
在机器人领域,行为树最先被作为一个多任务控制框架,使用在无人机、复杂机器人、机器人操作和多机器人系统中。
什么情形下使用行为树而不是有限状态机呢?
总的来说,行为树更适合用来设计人工智能(AI),而有限状态机更适用在可视化编程(Visual Programming)。当然,你也可以使用行为树在可视化编程上,以及使用有限状态机在人工智能上,但这不符合它们的设计初衷。有些人说,有限状态机的时代已经结束了。这里我们不打算延伸讨论,但行为树在人工智能领域确实比之有限状态机有着许多优势。
行为树相比有限状态机的一些优势包括:灵活性、功能强大和容易修改。
我们先来看第一项优势:灵活性。如果是使用有限状态机,你如何一下子运行两个不同的状态?唯一的方式就是创建两个分离的有限状态机。而使用行为树,你只需要添加一个并行任务就可以了,所有的子任务都会以并行的方式运行。
另一项行为树的优势是它的功能强大。这并不是说,有限状态机不强大,而是他们的强大表现在不同方面。行为树使得你设计的人工智能更容易对当前的状态进行反应。很容易创建一个行为树可以应对各种情形,而有限状态机则需要许多状态和状态转移来达到相似的人工智能。
最后一个优势是行为树非常容易进行修改。行为树开始变得流行的其中一个原因是他们很容易使用一个可视化编辑器来创建和修改。如果你希望改变有限状态机的状态执行方式,你需要同时改变状态之间的转移方式。在一个行为树中,你只要修改对应的子树即可,而不需要担心转移。并且,即使是整个改变人工智能体对于不同情形的反应方式,也只需要改变对应任务或者是给对应的任务加上一个父任务(parent task)。
我们来见识一下在行为树中常见的描述性语言(术语)。当前,在不同的文献和各种软件库中,对于行为树体系的表达不尽统一和标准。这里会尽可能按照《Behavior Trees in Robotics and AI》这本书的定义。
粗略的看,行为树是由各种节点组成的,这些节点可以用下列图形来表示:
行为树的执行是由称为ticks的更新步来引导的。当一个行为树被“tick”,通常是以一定的频率,它的子节点会依据树的结构递归地被执行。在一个节点执行完成, 它会返回一个状态给它的父节点,可以是成功、失败或运行中。
执行节点,也就是行为树的叶子,可以是动作或条件节点。它们唯一的区别是条件节点在一次执行中只可以返回成功或失败,而动作节点可以扩展执行多次,其间一直返回运行中直到达到一个终止状态。总体来说,条件节点代表简单的检查任务(比如门把手是不是开着?),而动作节点表示复杂任务(比如开门)。
控制节点,是内部节点,定义着行为树如何在给定子树的状态下进行遍历。需要注意的是,子树的控制节点可以是执行节点或控制节点本身。序列(Sequence)、回退(Fallback)和并行(Parallel)节点可以有很多子节点,它们区别于如何处理子节点。装饰器(Decorator)节点只需要一个节点,可以根据自定义的策略来修改其子节点的行为。
我们来通过实例来更好地理解上面介绍的术语和概念。比如有一个移动机器人需要在家庭环境搜寻一个特定的物体。假定机器人已知所有搜寻位置,也就是说,它已经有一个世界模型(world model)作为参考。
最简单地,如果有一个位置,称为A,那么行为树是一个简单的行为序列:去到目标位置,然后寻找物体。
我们选择把导航表示成一个动作节点,因为机器人移动到目标位置需要一些时间来完成,在这过程中返回“运行中”。另一方面,我们把看见表示成一个条件节点,假定机器人到达目的地时可以从一个图片中探测到物体。当然,这是为了说明各个执行节点做出了一些简化。
一个在书中非常常见的设计准则是你需要知道的,也就是明确的成功条件(explicit success conditions)。简单地说,你应该几乎总是在执行之前检查(check before you act)。举个例子,如果你已经到达一个特定位置,为什么不在开始导航动作之前检查一下你是否已经在那里了?
这里的移动机器人很可能会在环境中在多个位置操作,这样能看到尽可能多的位置,直到找到目标物体。这可以由**回退(Fallback)**节点来完成,以一种特别的顺序来重复上面的行为。
最后,如果不是寻找一个物体,而是想要考虑许多物体,比如说苹果和橘子。这个场景可以由如下的**并行(Parallel)**节点来实现。
这仅仅是一个简单的例子,为了能让你理解基本的过程。更多更复杂的例子可以在下面推荐的阅读中找到。