3. 任务规划基础:状态空间表示、动作与效果、规划域定义
各位好,欢迎来到任务规划的第一课。说实话,很多做机器人路径规划的朋友,一听到「任务规划」就觉得头大,觉得那是搞AI的人的事。我以前也这么想,直到有一次在产线上调试一个复合机器人——它明明能绕过障碍物,却不知道先抓A零件还是B零件,结果卡在那来回晃。那一刻我意识到:轨迹规划管的是「怎么走」,任务规划管的是「做什么」和「按什么顺序做」。缺了后者,机器人就是个有肌肉没脑子的大力士。
这一章我们就把任务规划的地基打牢。说白了,就是三件事:怎么描述世界状态、怎么定义动作、怎么把整个问题框起来。嗯,我们一个一个来。
3.1 状态空间表示:给世界拍一张快照
任务规划的第一步,是让机器人理解「世界现在长什么样」。你不能指望它像人一样看一眼就全明白,得给它一个结构化的描述。这个描述,就叫状态空间表示。
我个人习惯用一组命题(propositions)来描述状态。每个命题是一个布尔变量,真或假。举个例子:
状态 S = {
at(robot, kitchen) = True,
at(cup, shelf) = True,
holding(robot, nothing) = True,
clean(cup) = False
}
这四句话就拍了一张快照:机器人在厨房,杯子在架子上,机器人空手,杯子没洗。简单吧?但这里有个坑——状态空间的大小会爆炸。如果你有N个布尔命题,理论状态数就是2^N。我在一个仓储项目里见过有人定义了50个命题,结果状态空间比银河系的星星还多,规划器跑了一宿没出结果。
除了命题逻辑,还有一种常用的表示叫一阶逻辑。它允许你用谓词和对象来描述,表达能力更强。比如:
状态 S = {
At(Robot, Kitchen),
On(Cup, Shelf),
Holding(Robot, None),
Clean(Cup) = False
}
你看,这里把对象(Robot, Cup, Shelf)和关系(At, On, Holding)分开了。好处是:如果你有10个物体,不需要为每个物体单独定义「在桌上」的命题,一个On(x, y)谓词就能搞定。我建议你在对象数量超过5个时,果断用一阶逻辑,否则命题列表会写到让你怀疑人生。
3.2 动作与效果:机器人能做什么
有了状态描述,下一步就是告诉机器人它有哪些「本事」。每个动作由三部分组成:前提条件(preconditions)、效果(effects)、以及参数(parameters)。
拿「抓取」这个动作举例:
Action: Pick(robot, obj, from)
Parameters:
robot - 机器人
obj - 要抓的物体
from - 物体当前所在位置
Preconditions:
At(robot, from) = True
At(obj, from) = True
Holding(robot, None) = True
Effects:
Holding(robot, obj) = True
At(obj, from) = False
Holding(robot, None) = False
你想想看,这个定义其实就是在说:「如果机器人和物体在同一位置,而且机器人空着手,那么抓取之后,机器人手里就有了物体,物体不在原位置了,机器人也不再空手了。」——这不就是常识吗?但机器没有常识,你得一字一句写清楚。
动作效果通常用STRIPS风格来描述,也就是「添加列表」和「删除列表」。上面的例子用STRIPS写就是:
Action: Pick(robot, obj, from)
Pre: {At(robot, from), At(obj, from), Holding(robot, None)}
Add: {Holding(robot, obj)}
Del: {At(obj, from), Holding(robot, None)}
这种格式简洁明了,也是经典规划器(如FF、FastDownward)的标配。我个人觉得,初学者从STRIPS入手最不容易出错。
3.3 规划域定义:把问题框起来
有了状态和动作,最后一步就是定义规划域(Planning Domain)。说白了,就是告诉规划器:你在哪个世界里干活,你的目标是什么。
一个完整的规划域包含:
- 对象类型:比如机器人、位置、物体
- 谓词定义:比如At(?x ?y)、Holding(?r ?o)
- 动作定义:所有可用的动作模板
- 初始状态:世界一开始的样子
- 目标状态:世界应该变成的样子
下面是一个完整的PDDL(Planning Domain Definition Language)示例,这是规划领域的「普通话」:
(define (domain kitchen-world)
(:types robot location object)
(:predicates
(at ?r - robot ?loc - location)
(at ?obj - object ?loc - location)
(holding ?r - robot ?obj - object)
(clean ?obj - object)
)
(:action pick
:parameters (?r - robot ?obj - object ?from - location)
:precondition (and (at ?r ?from) (at ?obj ?from) (holding ?r nothing))
:effect (and (holding ?r ?obj) (not (at ?obj ?from)) (not (holding ?r nothing)))
)
(:action place
:parameters (?r - robot ?obj - object ?to - location)
:precondition (and (holding ?r ?obj) (at ?r ?to))
:effect (and (at ?obj ?to) (not (holding ?r ?obj)) (holding ?r nothing))
)
)
然后是一个具体的问题定义:
(define (problem wash-cup)
(:domain kitchen-world)
(:objects
robot1 - robot
cup1 - object
kitchen - location
sink - location
)
(:init
(at robot1 kitchen)
(at cup1 kitchen)
(holding robot1 nothing)
(not (clean cup1))
)
(:goal
(and (at cup1 sink) (clean cup1))
)
)
你看,这个定义说得很清楚:机器人叫robot1,杯子叫cup1,初始都在厨房,杯子没洗。目标是把杯子放到水槽里,并且洗干净。规划器拿到这个定义后,就会自动搜索出一系列动作序列,比如:Pick(robot1, cup1, kitchen) → Move(robot1, kitchen, sink) → Place(robot1, cup1, sink) → Wash(robot1, cup1)。
下面这张图总结了本章的知识体系,你可以对照着梳理一下思路:
最后说一句:任务规划的基础,说白了就是「用逻辑语言描述常识」。你定义的状态越精确、动作越完整、域越聚焦,规划器就越聪明。别小看这一步——我见过太多项目,算法调得天花乱坠,结果栽在状态表示上。地基没打好,楼盖得再高也得塌。