Part IV · 第 14 章 运动规划 · 14.1

图搜索与 A*

把地图切成格子、把构型空间离散成状态,运动规划就变成了在图上找一条最便宜的路。Dijkstra 按“离起点多远”一圈圈向外扩展;A* 再加一个“离终点大概还有多远”的估计,把搜索拉向目标,只要估计从不高估,结果仍是最优的。本节一步步运行 BFS、Dijkstra 与 A*,看启发式的好坏、加权 A* 的速度与代价、栅格连通性对路径质量的影响,再看汽车用的 Hybrid A* 与状态格,以及边走边发现障碍时如何用 D* Lite 增量重规划。

约 40 分钟 前置:6.12 动态规划 · 7.2 构型空间 · 9.1 移动机器人与非完整约束 6 个交互

    1为什么机器人需要它

    仓库里的搬运机器人要从货架 A 开到打包台,扫地机器人要绕过沙发回到充电座,自动驾驶汽车要从车道开进一个车位,机械臂要从一个姿势摆到另一个姿势而不碰到桌子。它们共同的问题是运动规划:在构型空间的自由部分 \(\mathcal C_{\text{free}}\) 里,找一条从起点到终点的路径,最好还是最短、最省时或最安全的那条。

    最直接的办法是把连续的空间离散化:把地图切成格子(每个空闲格是一个节点,相邻格之间连一条边,边上写着走过去的代价),问题就变成了在一张图上找最短路。6.12 节已经用值迭代在格子世界里算出过“每个格子到终点的最优代价” \(V\),那是对所有起点一次性算完。可是机器人通常只关心一个起点:此刻它在哪。这时没必要把整张表算出来,只要从起点出发,按某种聪明的顺序往外探,探到终点就停。这种顺序的选择,就是 BFS、Dijkstra 与 A* 的区别。

    A* 是 1968 年为 SRI 的移动机器人 Shakey 发明的,五十多年后仍然是 ROS 导航栈、游戏寻路和自动驾驶泊车规划的核心。本节不仅讲它为什么对、为什么快,也讲在真实机器人上用它时要面对的几件事:启发式怎么选、能不能为了速度牺牲一点最优性、栅格路径为什么歪歪扭扭、汽车不能原地转向怎么办、地图边走边变时怎么不必从头再来。

    2直觉:永远先处理“最有希望”的那个节点

    看下面这张小路网:10 个路口,路的长度就是两点间的直线距离(虚线那条是土路,同样长度要付 1.5 倍代价)。从 S 到 G 怎么走最短?

    Dijkstra 算法维护两个集合。开放集(open list,也叫“前沿”)放的是“已经找到一条路能到、但还不确定是不是最短”的节点,每个节点记着目前已知的从起点出发的最小代价 \(\hl{g}{g(n)}\);关闭集(closed list)放的是“最短代价已经确定”的节点。每一步:从开放集里取出 \(g\) 最小的节点 \(u\),把它移进关闭集;然后看它的每个邻居 \(v\),如果“经过 \(u\) 到 \(v\)”比 \(v\) 现有的记录更便宜,就更新 \(g(v)\) 并记下 \(v\) 的父节点是 \(u\)(这一步叫松弛)。终点被取出时,沿父节点倒推回去就是最短路。

    为什么取出的那一刻 \(g(u)\) 就是最短的?因为边的代价都不为负,开放集里别的节点的 \(g\) 都不比它小,任何经过它们再绕到 \(u\) 的路只会更长。所以 Dijkstra 是按“离起点的距离”从近到远、一圈圈地把节点定下来:这正是 6.12 节值迭代的“水波”,只是从起点出发、并且每个节点只处理一次。

    A* 只改了一处:取节点时不看 \(g(n)\),而看 \(f(n)=g(n)+\hl{h}{h(n)}\),其中 \(h(n)\) 是对“从 \(n\) 到终点还要多少”的估计,这里取到 G 的直线距离。远离目标的节点 \(h\) 大,就被往后排。下面一步步跑一遍,对比两者取出了多少个节点。

    FIG 1路网上的 Dijkstra 与 A*:一步一步
    每条路旁的数字是它的代价。青色填充的节点在关闭集里(最短代价已确定),紫色圆环的节点在开放集里,琥珀色圆环是刚被取出的节点,琥珀色细线是这一步被松弛的边。节点上方的蓝色数字是当前的 \(g\),蓝色粗线是父节点指针组成的“搜索树”。A* 模式下,紫色虚线从每个开放节点指向 G,长度就是 \(h\)。终点被取出后,琥珀色粗线是最短路。右侧表格是开放集,按排序键从小到大排列,第一行(琥珀色)就是下一个要取出的节点;键相同时先取 \(g\) 大的。可以拖动任何一个路口:路的长度跟着变。
    试一试
    • 先用 Dijkstra、再用 A*,各自按“单步”或“运行”一直走到 G 被取出(不要拖动路口):Dijkstra 取出了全部 10 个节点,A* 只取出 4 个,最短路一样。
    • 拖动路口,让最短路不再是 S→C→E→G(比如把 E 往下拖,或把 F 拖到离 C 只有一格多的地方)。
    核心洞见

    三种算法是同一个循环:“从开放集里取一个节点,展开它的邻居”。唯一的区别是取哪一个:BFS 取最早进来的(先进先出),Dijkstra 取 \(g\) 最小的,A* 取 \(g+h\) 最小的。A* 的 \(f(n)\) 是“经过 \(n\) 的整条路大概要多少”的估计,所以它优先展开“看起来在最短路上”的节点。只要 \(h\) 从不高估真实剩余代价,它取出终点时找到的就仍然是最短路。

    3动手:在栅格地图上让五种搜索赛跑

    现在换成机器人最常用的图:占据栅格。每个空闲格是一个节点;“4 连通”只能上下左右走(每步代价 1),“8 连通”还能走斜线(代价 \(\sqrt2\)),但斜走时不能切过墙格的角(斜向两侧的两个直邻格都必须空闲)。走进一个格子要付该格的价格:普通格 1,绿色泥地 4(和 6.12 节一样)。墙不能走。

    栅格上 \(h\) 有几种常见选择,都按“最便宜的格子价格 1”来估计剩余距离,设 \(\Delta x,\Delta y\) 是到终点相差的格数:曼哈顿距离 \(\Delta x+\Delta y\)(4 连通空地图上恰好等于真实代价);八方向距离(octile)\(\max(\Delta x,\Delta y)+(\sqrt2-1)\min(\Delta x,\Delta y)\)(8 连通空地图上恰好等于真实代价);欧氏距离 \(\sqrt{\Delta x^2+\Delta y^2}\)(两种连通都不会高估,但比前两者更“松”)。另有两个变体:加权 A* 用 \(g+w\,h\),\(w>1\) 时更“急于”冲向终点;贪心最佳优先只看 \(h\),完全不管已经走了多远。

    FIG 2栅格搜索实验台
    青色格子是已展开(关闭)的节点,紫色方框是开放集(前沿),琥珀色描边是当前正在展开的格子,琥珀色粗线是找到的路径。实心填充的格子是墙,绿色格子是泥地(价格 4)。S(蓝)和 G(红)可以拖动;在地图上点击或拖动,用图标题栏里的工具涂墙、泥地或擦除。“运行”按设定速度逐个展开节点,“完成”直接显示结果。读数里的“最优代价 C*”由 Dijkstra 在同一张图上算出,用来判断当前路径是不是最短。默认地图上,贪心最佳优先只展开 26 个格子,但它直穿泥地,路径代价 35.24,比最优的 23.73 贵 49%。
    • 保持 A*(八方向距离、8 连通),按“运行”:前沿朝着 G 长过去;读数里它展开的格子不到 Dijkstra 的一半(118 对 307),路径代价相同。
    • 选 BFS:它找到的是步数最少的路,可它直穿泥地,代价比 C* 高(34.07 对 23.73)。BFS 只在每步代价都相同时才是最短路。
    • 选加权 A*,取 \(w\ge2\):展开的格子不到普通 A* 的一半,路径可能变贵,但读数里代价永远不超过 \(w\cdot C^*\)。
    看到了什么

    Dijkstra 的前沿是一圈圈“等代价线”(碰到泥地会凹进去),它不知道终点在哪个方向。A* 的前沿被拉成朝向终点的细长形状:离终点越远的格子 \(f\) 越大,排到后面一直没轮到。BFS 的前沿是“等步数线”,泥地对它不存在。贪心最佳优先几乎一条线冲过去,碰到障碍才拐弯,所以快但路差。加权 A* 介于 A* 和贪心之间:\(w\) 越大越像贪心。

    4另外两个视角:启发式有多好,栅格路径有多直

    启发式与真实剩余代价

    每个格子都有一个真实的“到终点的最小代价” \(\hl{hstar}{h^*(n)}\):它就是 6.12 节的价值函数 \(V(n)\),可以从终点反向跑一遍 Dijkstra 算出来。启发式 \(h\) 是它的廉价替身,好坏可以用比值 \(h/h^*\) 衡量:处处等于 1 时 A* 只展开最短路上的格子;处处等于 0(即 \(h\equiv0\))时 A* 退化成 Dijkstra。只要 \(h\le h^*\)(称为可容许,admissible),A* 就保证最优;一旦某处 \(h>h^*\),就可能错过最短路。

    加权 A* 故意把 \(h\) 乘上 \(\hl{w}{w}>1\),让它在很多格子上高估。好消息是代价有上界:只要原来的 \(h\) 满足一致性(第 5 节),找到的路径代价不超过 \(w\) 倍最优。下图左边按 \(w\,h/h^*\) 给每个格子着色,右边扫一遍 \(w\) 从 1 到 5,画出展开数和代价比。

    FIG 3启发式的质量与加权 A* 的代价上界
    左:8 连通地图,S、G 可拖动。每个格子按 \(w\,h/h^*\) 着色:越青表示越接近真实剩余代价(越“有信息”),颜色淡表示严重低估;红色格子 \(w\,h>h^*\),即高估。蓝色小点标出这一次 A* 展开过的格子,琥珀色线是它的路径。把鼠标放在第 4、5 节公式里的 \(h^*\) 上,左图改为显示 \(h^*\) 本身(越蓝越小)。右:当前启发式下,\(w\) 从 1 扫到 5。上半是展开的格子数(蓝),下半是路径代价与最优代价之比(琥珀),紫色虚线是上界 \(w\),竖线是当前的 \(w\)。
    • 在 U 形陷阱地图上用八方向距离,把 \(w\) 往上加,直到路径不再最优(代价比 > 1):此时的代价比远小于 \(w\),上界很松。
    • 选“房间”地图和曼哈顿距离(\(w=1\)):很多格子变红,因为 8 连通时斜走一步只要 \(\sqrt2\),曼哈顿却算 2。把 S 往右拖两三格,找一个 A* 返回非最优路径的起点(代价比 > 1)。

    几点观察。(1) 在 U 形陷阱里,\(h=0\)、欧氏、八方向距离分别展开 295、132、114 个格子:\(h\) 越接近 \(h^*\),展开越少。但 U 形开口里所有格子的 \(h\) 都远小于 \(h^*\)(直线距离穿墙而过),A* 也只好把陷阱“填满”才绕出来,这是所有只看几何距离的启发式的通病。(2) 加权 A* 的代价比往往远低于上界 \(w\):上界是最坏情况的保证,不是典型值。(3) 曼哈顿距离在 8 连通栅格上不可容许,但大多数起点下它仍然碰巧给出最短路;“不可容许”说的是失去保证,不是每次都错。

    连通性与路径质量:栅格路径为什么不直

    栅格上的“最短路”只是在允许的移动方向里最短。4 连通只能走 0°、90°……,一条 45° 方向的直线要走成阶梯,长度是 \(\hl{c4}{\Delta x+\Delta y}\),比直线长 \(\sqrt2-1\approx41\%\)。8 连通加上了斜线,长度变成 \(\hl{c8}{\max+(\sqrt2-1)\min}\),最坏出现在 22.5° 方向,长 8.2%。16 连通再加上“马步” \((2,1)\) 一类的移动,最坏只长 2.7%。更麻烦的是形状:同样代价的阶梯路径有很多条,哪条被选中取决于平局怎么打破,结果往往是不自然的折线,贴着墙角拐弯。

    任意角度(any-angle)规划不再把路径限制在栅格边上。Theta*(Nash 等,2007)在展开节点 \(u\)、更新它的邻居 \(v\) 时,检查 \(v\) 和 \(u\) 的父节点之间有没有直线视线,有就让 \(v\) 直接连到那个父节点上(跳过 \(u\)),于是路径的拐点只出现在障碍的角上。它不保证是真正的欧氏最短路,但通常非常接近。

    FIG 44 / 8 / 16 连通与任意角度路径
    拖动 S 和 G。红线是 4 连通最短路,绿线是 8 连通,蓝线是 16 连通,琥珀色是 Theta* 的任意角度路径,细虚线是 S 到 G 的直线。读数给出每条路径的长度和它比参考长度长多少:空地图上参考是直线距离(这时直线就是真正的最短路),有柱子时参考是 Theta* 的长度。把鼠标放在上文的 \(\Delta x+\Delta y\) 或八方向距离公式上,对应的路径会加粗。
    • 在空地图上拖动 G,找一个方向让 8 连通路径比直线长 7% 以上(接近 22.5°);同时看 4 连通和 16 连通:前者长二三十个百分点,后者不到 3%。
    • 选“柱子”地图:找一个 S、G 位置,让 Theta* 比 8 连通路径短 5% 以上。
    分辨率的两难

    格子越细,路径越接近连续最短路,也越不会把一条窄通道“看成”堵死;但格子数随维数指数增长(6.12 节的维数灾难)。一张 100 m × 100 m、5 cm 分辨率的二维地图有 400 万个格子,A* 仍然应付得了;一条 6 自由度机械臂的关节空间若每维取 100 个格点,就有 \(10^{12}\) 个格子,根本存不下。所以图搜索主宰的是低维问题(平面导航、车的 \((x,y,\theta)\)),高维的机械臂规划交给 14.2 节的采样式方法。

    5再看数学

    提示:把鼠标悬停在公式中带底色的符号上,FIG 1–FIG 6 中对应的元素会被高亮。

    问题与贝尔曼方程

    图 \(\mathcal G=(\mathcal V,\mathcal E)\),每条边有非负代价 \(c(u,v)\ge0\)。一条路径的代价是它各边代价之和,\(C^*\) 是从起点 \(s\) 到终点 \(t\) 所有路径中的最小代价。记 \(g^*(n)\) 为从 \(s\) 到 \(n\) 的最小代价,\(\hl{hstar}{h^*(n)}\) 为从 \(n\) 到 \(t\) 的最小代价,它们各满足一个贝尔曼方程:

    $$ \begin{gathered} g^*(s)=0,\quad g^*(v)=\min_{u}\big[g^*(u)+c(u,v)\big];\\ h^*(t)=0,\quad h^*(u)=\min_{v}\big[c(u,v)+h^*(v)\big];\\ C^*=g^*(t)=h^*(s)=\min_n\big[g^*(n)+h^*(n)\big]. \end{gathered} $$

    第二个方程正是 6.12 节的 \(V\)(\(\gamma=1\) 的最短路问题)。最后一个等式说:经过最短路上任意一点 \(n\) 的 \(g^*+h^*\) 都等于 \(C^*\),不在任何最短路上的点则严格大于 \(C^*\)。A* 的全部思想,就是用一个可计算的 \(h\) 去近似这里的 \(h^*\)。

    算法:一个循环,三种排序

    把起点放进开放集 \(\hl{open}{\mathcal O}\),\(g(s)=0\),其余 \(g=\infty\)。重复:从 \(\mathcal O\) 取出排序键最小的 \(u\),放进关闭集 \(\hl{closed}{\mathcal K}\);若 \(u=t\) 则沿父指针返回路径;否则对每个邻居 \(v\notin\mathcal K\),若 \(g(u)+c(u,v)<g(v)\),令 \(g(v)\leftarrow g(u)+c(u,v)\)、父节点 \(\pi(v)\leftarrow u\),并把 \(v\) 以新的键放进 \(\mathcal O\)。排序键:

    $$ \hl{g}{g(n)},\qquad f(n)=\hl{g}{g(n)}+\hl{h}{h(n)},\qquad g(n)+\hl{w}{w}\,h(n),\qquad h(n). $$

    从左到右依次是 Dijkstra、A*、加权 A* 和贪心最佳优先的键;BFS 不用键,按进入开放集的先后(先进先出)。

    开放集用二叉堆实现,复杂度为 \(O\big((|\mathcal V|+|\mathcal E|)\log|\mathcal V|\big)\);栅格上每个节点至多 8 条边。BFS 用普通队列,\(O(|\mathcal V|+|\mathcal E|)\),但只在所有边代价相同时最优(FIG 2 的泥地)。实现上通常不做“降低堆中键值”,而是把新键再压一次堆,取出时跳过过期的条目(惰性删除)。

    可容许与一致:A* 为什么最优

    $$ \begin{gathered} \text{(1)}\quad 0\le \hl{h}{h(n)}\le \hl{hstar}{h^*(n)}\ \ \forall n;\\ \text{(2)}\quad h(t)=0,\ \ h(u)\le c(u,v)+h(v)\ \ \forall (u,v)\in\mathcal E. \end{gathered} $$

    (1) 是可容许,(2) 是一致。一致(也叫单调)是边上的三角不等式:估计值走一步最多下降这一步的代价。沿着从 \(n\) 到 \(t\) 的最短路把它加起来,就得到 \(h(n)\le h^*(n)\):一致蕴含可容许。直线距离、八方向距离(8 连通)、曼哈顿距离(4 连通)都是一致的。

    理解一致性最干净的方式是约化代价(reduced cost)。定义

    $$ \begin{gathered} \tilde c(u,v)=c(u,v)-h(u)+h(v)\ \ge 0 \iff \text{(2)};\\ \tilde c(P_{s\to n})=c(P_{s\to n})-h(s)+h(n). \end{gathered} $$

    任何一条从 \(s\) 出发的路径 \(P_{s\to n}\),其约化代价只是原代价减去常数 \(h(s)\) 再加上终点的 \(h(n)\),所以到 \(t\) 的路径谁短谁长完全不变(\(h(t)=0\)),而 Dijkstra 在约化代价上的键 \(\tilde g(n)=g(n)+h(n)-h(s)=f(n)-h(s)\),与 A* 的 \(f\) 只差一个常数。结论:一致启发式下的 A* 就是边代价为 \(\tilde c\ge0\) 的 Dijkstra。Dijkstra 的一切性质随之而来:节点被取出时 \(g\) 已是最优,关闭的节点不必重开,取出的 \(f\) 值单调不减,取出 \(t\) 时路径最优。只可容许、不一致的 \(h\) 也能保证最优,但需要允许重开关闭集里的节点。

    A* 展开哪些节点。由于 \(f\) 单调不减,所有 \(g^*(n)+h(n)<C^*\) 的节点在 \(t\) 之前必然被展开,\(f>C^*\) 的节点永远不会。所以 \(h\) 越大(越接近 \(h^*\)),这个集合越小:若 \(h_1\ge h_2\) 且都一致,用 \(h_1\) 展开的节点(除平局外)是用 \(h_2\) 的子集。两个极端:\(h\equiv0\) 时是 Dijkstra;\(h=h^*\) 时 \(f\) 在最短路上处处等于 \(C^*\),配合“平局取 \(g\) 大”的规则,A* 只展开最短路上的节点。这也是“学习一个启发式”有意义的原因:价值函数本身就是最好的启发式。

    加权 A* 的上界。令 \(f_w=g+w\,h\)。取出 \(t\) 的那一刻,最短路上总有某个节点 \(n\) 在开放集里且 \(g(n)=g^*(n)\),于是

    $$ \begin{aligned} g(t)=f_w(t)\le f_w(n)&=g^*(n)+\hl{w}{w}\,h(n)\\ &\le w\big(g^*(n)+h^*(n)\big)=w\,C^*\qquad (w\ge1,\ h\le h^*). \end{aligned} $$

    这一推导在允许重开节点时直接成立;Likhachev 等(2003)证明了 \(h\) 一致时即使不重开也成立(FIG 2、FIG 3 都不重开)。所以 \(w\) 是一个旋钮:用至多 \(w\) 倍的代价,换取通常少得多的展开。ARA*(随时修复 A*)从大 \(w\) 开始,先快速给出一条路,再逐步降低 \(w\) 并复用之前的搜索,时间允许就一直改进。

    车的状态空间:运动基元、状态格与 Hybrid A*

    9.1 节的运动学自行车模型:后轴中心 \((x,y)\)、航向 \(\theta\),

    $$ \begin{gathered} \dot x=v\cos\theta,\quad \dot y=v\sin\theta,\quad \dot\theta=v\,\kappa,\\ \hl{prim}{|\kappa|\le 1/R_{\min}},\quad v\in\{+,-\}. \end{gathered} $$

    在 \((x,y)\) 栅格上,8 连通的“边”要求原地转向,车做不到。办法是把图的节点换成 \((x,y,\theta)\),边换成运动基元:在曲率约束下实际能开出来的短轨迹(直行、左转弧、右转弧,前进或倒车)。

    • 状态格(state lattice,Pivtoraiko 与 Kelly):航向只取有限几个值(FIG 5 用 8 个,每 45° 一个),每个基元都精心设计成从一个格点出发、恰好落在另一个格点上(FIG 5 的转弯基元是半径 \(R=2+2\sqrt2\approx4.83\) m 的 45° 圆弧加 0.83 m 直线,正好从 \((0,0,0°)\) 到 \((4,2,45°)\))。它仍是一张普通的图,A* 的最优性在这张图上成立;代价是表达能力受基元集合限制。
    • Hybrid A*(Dolgov 等):节点里存连续的位姿,基元直接从这个连续位姿积分出去,只用 \((x,y,\theta)\) 的栅格单元来去重:同一个单元里只保留代价最小的那个连续状态。这样每条边都严格可行,但同一单元里被丢掉的状态可能正是通往终点所需要的,所以它既不完备也不最优,只是“分辨率够细时很好用”。真实实现在接近终点时还会尝试一条 Reeds–Shepp 曲线直接解析地连到目标位姿(解析扩展),FIG 5 为简洁省略了这一步,改为允许到达目标附近 0.8 m、航向差 0.25 rad(约 14°)以内。

    启发式取两个估计的较大者(Dolgov 等称两者都可容许):考虑运动学、忽略障碍(无障碍时的 Reeds–Shepp 最短长度,可以离线制表),以及考虑障碍、忽略运动学(在二维栅格上从终点跑一次 Dijkstra)。前者在开阔处管用(车需要掉头时直线距离严重低估,9.1 节已经指出),后者把搜索从死胡同里拉出来。FIG 5 只实现了后者与直线距离的较大者。注意二维栅格距离可能比连续空间的真实距离略长(FIG 4:最多 8%),严格说并不可容许;FIG 5 因此把它减去 1.5 m 的余量再用,实践中这点误差通常被接受。

    D* Lite:搜索倒过来做,变化只修补一小块

    机器人边走边用传感器发现新障碍,每发现一次,图上就有几条边的代价变成 \(\infty\)。从头跑一遍 A* 当然可以,但大部分旧结果其实仍然有效。D* Lite(Koenig 与 Likhachev,2002)的做法:从终点向机器人方向搜索,于是 \(g(s)\) 是“从 \(s\) 到终点”的代价,机器人移动时终点不变,这些值不会作废;再给每个节点加一个一步前瞻值

    $$ \begin{gathered} \hl{rhs}{\mathrm{rhs}(s)}=\min_{s'}\big[c(s,s')+g(s')\big],\\ k(s)=\Big[\min\big(g,\mathrm{rhs}\big)+h(s_{\text{start}},s)+k_m;\ \ \min\big(g,\mathrm{rhs}\big)\Big]. \end{gathered} $$

    \(g=\mathrm{rhs}\) 的节点叫局部一致;边代价一变,只有它两端的节点变得不一致,被放进队列。队列按两段键 \(k\) 处理不一致的节点,直到机器人所在节点一致且队列中再没有键更小的节点:效果上等价于“在新地图上重新算出了需要的那部分 \(g\)”,但只碰受影响的区域。\(h(s_{\text{start}},\cdot)\) 让修补朝机器人聚焦(像 A*),机器人每走一段,\(k_m\) 加上 \(h(s_{\text{last}},s_{\text{start}})\),使队列里旧的键仍然是下界,不必重排整个队列。D* Lite 的路径与在当前已知地图上重新跑 A* 的代价完全相同(我们在 FIG 6 两张地图、半径 2 到 6 的每一次重规划上都核对过),省的只是计算。

    6机器人中的它:车不能原地转向,地图边走边变

    下图是一辆 4.0 m × 1.8 m 的车,最小转弯半径 4.83 m,状态是后轴中心的 \((x,y,\theta)\)。车身用沿中心线的 3 个圆盘近似做碰撞检测(这是常见做法:圆盘到障碍的距离很便宜)。Hybrid A* 每次展开生成 5 种曲率 \(\times\) 前进/倒车的 1.6 m 弧,用 1 m × 1 m × 5°(72 个航向)的单元去重;代价是路程,倒车的路程乘 2,每次换挡加 3 m,打方向的变化也有小惩罚。

    FIG 5Hybrid A* 与状态格:给车找一条开得出来的路
    场地 30 m × 18 m,实心矩形是障碍。蓝色车框是起点,绿色虚线车框是目标:拖动车框里后轴中心的圆点移动它,拖动车头前方的小圆点改变航向(状态格模式下会吸附到整数格点和 45° 的倍数)。青色细线是搜索树里前进的基元,紫色细线是倒车的基元。找到的路径中,琥珀色段是前进,红色段是倒车,路径上每隔一段画一个车身轮廓;随后一辆琥珀色的车沿路径开一遍。“显示栅格 A*”叠加一条把车当作点、在 1 m 栅格上 8 连通搜出来的路径(蓝色虚线):它在每个拐点要求原地转向,车开不出来。把鼠标放在第 5 节的曲率约束上,起点处会画出一次展开的全部基元。
    • 选“死胡同”:街宽 9 m,掉头圆的直径就要约 9.7 m。“前进 + 倒车”时 Hybrid A* 找到一个多点掉头;改成“只前进”:找不到路。
    • 选“长墙”,分别用两种启发式搜索:考虑障碍的启发式展开的节点不到直线距离的一半。

    几个细节。(1) 换成“状态格”:停车场仍能停进去,但要倒车调整两次,代价约为 Hybrid A* 的两倍;死胡同里要换向 5 次,代价 66.6,是 Hybrid A* 的 33.9 的近两倍;侧方停车干脆无从下手:目标位姿吸附到最近的整数格点 \((10,1)\) 后,车身压到了场地边界,读数显示“起点或目标与障碍相撞”。原因是 8 个航向、整数格点、4 m 长的转弯基元太粗了,换成更密的航向和更短的基元才行。基元集合的设计就是状态格规划器的全部功夫。(2) Hybrid A* 的路径还要后处理:实际系统会再做平滑(Dolgov 等用了带障碍项的共轭梯度优化,与 6.13、14.3 节的轨迹优化同类),再交给 9.1 节的路径跟踪控制器。(3) 打开“显示栅格 A*”比一比:点机器人的路径更短,却是车开不出的折线。

    边走边发现:重规划

    下图的机器人一开始不知道墙在哪,只假设未知的地方都能走(乐观假设)。它每走一格,用半径为 \(r\) 的传感器看一圈,发现新墙就重规划。同一时刻用同样的已知地图做两件事:D* Lite 增量修补,以及从机器人当前位置从头跑一次 A*,比较两者展开的节点数。

    FIG 6未知地图里的重规划:D* Lite 对比每次从头 A*
    虚线方框是机器人还不知道的墙,实心格子是已经看到的墙。琥珀色圆点是机器人,青色圆是传感器范围,琥珀色线是它当前计划的路径,紫色线是走过的轨迹,G 是终点。最近一次重规划中,青色填充的格子是 D* Lite 处理过的节点,蓝色小点是从头 A* 展开的节点。点击地图可以添加或移除墙(机器人走近才会发现)。把鼠标放在上文的 \(\mathrm{rhs}\) 上,地图会用红色标出此刻 \(g\ne\mathrm{rhs}\)、还在队列里的节点。
    • 选“迷宫”,传感器半径保持 2 到 5(默认 3),按“运行”让机器人走到终点:D* Lite 的累计展开不到从头 A* 的一半。

    在迷宫里(传感器半径 3),D* Lite 的累计展开只有从头 A* 的约三分之一(943 对 2767);半径越大,一次看到的墙越多,优势越小(半径 6 时约一半)。原因是:迷宫里直线距离启发式很差,从头 A* 每次都要重新填满大片走廊,而墙总是在机器人身边被发现,D* Lite 只需修补靠近机器人的一小块。在“房间”地图上差别就小得多,甚至可能反过来(半径 3 时 326 对 300):开阔地上 A* 本来就只展开一条窄带,D* Lite 每次修补还要付出维护 \(\mathrm{rhs}\) 和队列的开销。增量方法省多少,取决于变化发生在哪里、启发式有多好。还要注意第一次规划:迷宫里(半径 3)D* Lite 展开 350 个节点,A* 只展开 33 个。在还几乎全是未知(当作空闲)的地图上,许多格子的 \(f\) 相同;D* Lite 的第二段键取 \(g\) 小的先处理,会把这些平局的格子都处理一遍,而这里的 A* 平局时取 \(g\) 大的。D* Lite 的优势要在之后多次重规划里才体现出来。

    真实系统

    ROS 导航:ROS 1 的 navfn 与 global_planner 在代价地图(costmap,障碍按机器人半径“膨胀”,即 7.2 节的 C-障碍物)上跑 Dijkstra 或 A*;ROS 2 的 Nav2 提供 NavFn、Theta* 规划器,以及 Smac Planner 系列(二维 A*、Hybrid-A*、状态格三种)。自动驾驶:斯坦福的 Junior 在 2007 年 DARPA 城市挑战赛中用 Hybrid A* 在停车场和受阻路段规划(Dolgov 等);冠军 CMU 的 Boss 在停车场里用状态格加随时 D*(Likhachev 与 Ferguson)。今天的开源自动驾驶栈 Autoware(Autoware Universe)的泊车用 freespace planner 也基于 Hybrid A*。行星车:Stentz 的 D*(1994)为在部分未知地形中行驶的移动机器人而设计;Ferguson 与 Stentz 的插值版本 Field D* 于 2006 年随软件更新上传到 NASA 的火星探测漫游者(MER,勇气号与机遇号),在自主导航中与局部规划器 GESTALT 配合做全局规划(Carsten 等,2007)。游戏:A* 是游戏寻路的事实标准,在均匀栅格上常配合跳点搜索(JPS,Harabor 与 Grastien,2011)利用对称性跳过大量等价节点。

    接下来:图搜索在二维、三维里无可替代,但到了机械臂的 6、7 维关节空间就撑不住了。14.2 节改为在 \(\mathcal C\) 里随机撒点、用碰撞检测连线(PRM、RRT),14.3 节把图搜索或采样得到的折线优化成平滑、可执行的轨迹,15.3 节讲规划器所用的代价地图从哪里来。

    7常见误区

    误区 1:启发式与代价不是同一种“单位”

    可容许性是相对于你的代价函数说的。代价是“米”时直线距离可容许;代价若是“秒”,启发式就要除以最高速度;代价若是代价地图里的每格价格(例如 ROS 的 global_planner 每格收一个“中性代价”,默认 50,再加上代价地图值乘以 cost_factor,默认 3),最便宜的格子价格是多少,启发式就要乘多少。把曼哈顿距离用在 8 连通栅格上(FIG 3)、或者把格数当米用,都会悄悄高估,A* 依然“能跑”,只是不再保证最短。反过来,启发式比必要的小很多(比如价格最低是 50 却按 1 估计),A* 会退化得很接近 Dijkstra,慢但仍正确。

    误区 2:在入队时就把节点标记为“已访问”

    这是 BFS 的写法,用在 Dijkstra/A* 上就错了:第一次碰到某个节点时找到的路未必最便宜(FIG 1 里很多节点的 \(g\) 会被后来更新),只有取出时才确定。同类错误还有:用了二叉堆却不处理过期条目,同一节点被展开两次;启发式不一致却又不允许重开关闭的节点,结果悄悄失去最优性。还有一个效率细节:大片开阔区域里 \(f\) 相同的节点非常多,平局时优先 \(g\) 大的节点能少展开很多。

    误区 3:把点机器人的栅格路径直接交给真实机器人

    栅格上的最短路贴着障碍的角走,而真实机器人有半径:必须先把障碍按机器人半径膨胀(7.2 节的 C-障碍物),并且通常再加一层随距离衰减的代价,让路径离墙远一点。格子太粗,窄门会被“膨胀”成堵死;8 连通不禁止“斜穿墙角”,路径会从两个对角相接的障碍之间挤过去。拿到的路径还要平滑,对车来说还要满足最小转弯半径(FIG 5 的栅格 A* 对比,以及 9.1 节)。

    误区 4:以为 Hybrid A* 是完备、最优的

    Hybrid A* 每个单元只保留一个连续状态,被丢掉的状态可能恰好是唯一可行的那个;它“找不到路”不等于真的没有路,代价也不保证最小(FIG 5 里换个启发式,路径代价都可能变)。航向分辨率太粗、基元太长时尤其明显。需要保证时,用状态格(对它这张图 A* 是最优的),或者把单元做细、把找不到路当作“需要更细分辨率”的信号而不是最终结论。

    误区 5:把“未知”当成“空闲”或“占据”而不自知

    FIG 6 用的是乐观假设:没看过的地方都当作能走。这让机器人敢于探索,但会一次次撞进死胡同再退出来;把未知当作占据则可能根本规划不出路。很多系统给未知格一个介于两者之间的代价,并在每个控制周期重规划。另外,增量算法不是免费的午餐:变化离终点近、或者一次变化很多时,D* Lite 可能比从头 A* 还慢,并且它的内存(每个节点的 \(g\)、\(\mathrm{rhs}\) 和队列)要一直保留。

    8小练习

    1. 8 连通栅格,直走一步代价 1,斜走一步代价 \(\sqrt2\),所有格子价格为 1。下面哪个启发式不可容许?
    对角相邻的格子:真实代价 \(\sqrt2\approx1.41\),曼哈顿距离却是 2,高估了。八方向距离在空地图上恰好等于真实代价,有障碍时只会更小;欧氏距离更小;\(h\equiv0\) 永远可容许(那就是 Dijkstra)。
    2. 如果启发式恰好是真实剩余代价 \(h=h^*\),并且平局时优先 \(g\) 大的节点,A* 会展开哪些节点?
    最短路上的节点 \(f=g^*+h^*=C^*\),不在任何最短路上的节点 \(f>C^*\),后者永远不会在终点之前被取出。最短路上 \(f\) 都相等,“平局取 \(g\) 大”让搜索一路向前,不在最短路的分叉上停留。这说明完美的启发式就是价值函数(6.12 节的 \(V\)),而 A* 的工作量取决于 \(h\) 离它有多近。
    3. 用一致启发式、\(w=1.5\) 的加权 A* 找到一条代价 30 的路径。关于最优代价 \(C^*\),能确定什么?
    上界“代价 \(\le w\,C^*\)”给出 \(C^*\ge30/1.5=20\);而 30 是一条真实路径的代价,最优代价不会比它大,所以 \(C^*\le30\)。FIG 3 里实际的代价比通常远低于 \(w\),所以真实的 \(C^*\) 往往接近 30。
    4. Hybrid A* 为什么在节点里保存连续的 \((x,y,\theta)\),而不是把它对齐到单元中心?
    如果把位姿对齐到单元中心,基元的终点就不再是车真正能到达的位姿,拼起来的路径会有跳变。保存连续位姿让路径处处可行;但同一单元里只留一个状态,可能丢掉通往终点的那个,所以它既不完备也不最优。状态格走的是另一条路:把基元设计成正好落在格点上,于是可以对齐,最优性在格图上成立(选项 B 说的其实是状态格)。
    5. D* Lite 为什么从终点向机器人搜索,而不是像 A* 那样从机器人出发?
    如果从机器人出发,\(g\) 是“从机器人到这里”的代价,机器人一走全部作废。反向搜索让 \(g\) 以终点为基准,机器人的移动只改变“搜索要服务的起点”,靠 \(k_m\) 修正键值即可;新发现的障碍只让相邻节点的 \(\mathrm{rhs}\) 变化,修补局限在受影响的区域(FIG 6 的青色格子)。

    9延伸阅读

    • E. W. Dijkstra, A note on two problems in connexion with graphs, Numerische Mathematik, 1959 — 最短路算法的原始论文,只有三页
    • P. E. Hart, N. J. Nilsson & B. Raphael, A Formal Basis for the Heuristic Determination of Minimum Cost Paths, IEEE Transactions on Systems Science and Cybernetics, 1968 — A* 及其最优性证明,为 Shakey 机器人而作
    • I. Pohl, Heuristic search viewed as path finding in a graph, Artificial Intelligence, 1970 — 加权启发式搜索
    • M. Likhachev, G. Gordon & S. Thrun, ARA*: Anytime A* with Provable Bounds on Sub-Optimality, NIPS 2003 — 不重开时的 \(w\) 倍上界与随时改进
    • A. Stentz, Optimal and Efficient Path Planning for Partially-Known Environments, ICRA 1994 — D*
    • S. Koenig & M. Likhachev, D* Lite, AAAI 2002 — 本节 FIG 6 实现的算法
    • A. Nash, K. Daniel, S. Koenig & A. Felner, Theta*: Any-Angle Path Planning on Grids, AAAI 2007 — 任意角度路径
    • D. Dolgov, S. Thrun, M. Montemerlo & J. Diebel, Path Planning for Autonomous Vehicles in Unknown Semi-structured Environments, International Journal of Robotics Research, 2010 — Hybrid A*、两种启发式、解析扩展与路径平滑
    • M. Pivtoraiko, R. A. Knepper & A. Kelly, Differentially Constrained Mobile Robot Motion Planning in State Lattices, Journal of Field Robotics, 2009 — 状态格
    • S. M. LaValle, Planning Algorithms, Cambridge University Press, 2006, Ch. 2 — 离散规划与图搜索的统一视角(网上免费)