為什麼單一規劃永遠不夠
想像你請一位朋友穿過繁忙的辦公室,幫你取一杯咖啡。在邁步之前,他會先在腦中描繪整條路線:出門、走過走廊、繞過印表機、進入茶水間。但他不會一步不差地照著這幅圖走。一路上,同事從門口閃出,椅子被往後推,地上出現了水漬——他會即時調整,卻始終不會忘記最終要去哪裡。這正是行動機器人必須完成的把戲,也是本章的核心。
到目前為止你遇到的一切——把世界變成組態空間、區分自由空間與障礙空間、做碰撞檢測,以及在 Dijkstra、A*、路標圖或 RRT 之間做選擇——都是在為一個能真正穿過雜亂多變房間的機器人做準備。難點在於:沒有任何單一規劃器能同時把這兩件事都做好。在大地圖上算一條有遠見的路線很慢,而對一把移動椅子的瞬間反應又根本來不及思考整棟樓。
於是真實機器人用最聰明的方式取巧:它們同時執行兩個規劃器,以兩種不同的節奏,再用一張不斷刷新的危險地圖把它們黏合起來。這套組合就是工程師所說的導航堆疊,而學會讀懂它,正是本章所有內容的回報。
導航堆疊:分層的流水線
導航堆疊是一條標準的、分層的流水線,它把諸如「去茶水間」這樣的目標轉化為車輪指令。可以把它想成一支接力隊:一名隊員拿著地圖,知道機器人在哪裡;下一名決定走哪條路線;再下一名不斷把這條路線塑造成安全、可達的運動;最後一名把原始的速度指令交給馬達。每名隊員都職責單一、可替換,正因如此,這種分層設計成了整個領域的預設做法。
這些層往往住在機器人作業系統(ROS)裡——這是一套開源的「管道」,讓各自獨立的程式彼此對話。ROS 為每一層提供了標準的訊息傳遞方式:這裡傳地圖,那裡傳路線,末端輸出速度指令——這樣團隊就能換上更好的局部規劃器,而不必重寫其餘部分。它與其說是一個單一程式,不如說是一個由協作部件組成的市集。
深入之前先記一個用詞。到目前為止,規劃器產出的是一條路徑——空間中純粹的形狀。而導航堆疊最終需要的是一條軌跡:同樣的形狀,但附上了時間與速度,好讓車輪不僅知道往哪走,還知道走多快。控制層正是路徑最終變成運動的地方。
兩個規劃器,兩種節奏
整個堆疊跳動的心臟,是全域規劃器與局部規劃器之間的分工。它們沿著「時間與細節」這條軸線劃分同一個問題:全域規劃器是戰略家,局部規劃器是戰術家。兩者誰也替代不了誰。
全域規劃器在整張已知地圖上,算出一條從起點到目標的完整路線。它執行一次圖搜尋——通常是在地圖的網格單元上跑 A* 或 Dijkstra——產出一條又長又有遠見的路徑。它可以慢,因為只在目標改變、或路線被嚴重堵死時才需要重算。關鍵是,它只知道地圖上已經畫出的障礙;那個剛走到機器人面前的人,對它來說是看不見的。
局部規劃器恰恰相反。每秒許多次,它只看機器人周圍的一小塊窗口,看見感測器剛剛回報的一切,不斷重新規劃一小段——既貼著全域路線,又躲開任何新出現的東西。因為它緊貼車輪,就必須尊重機器人實際的運動方式:像汽車那樣的底盤無法橫向平移,所以它的這一小段必須在動力學上可行。這正是完整約束與非完整約束之分在實踐中的體現——這個約束,遠在天邊的全域規劃器多半可以無視,而局部規劃器永遠不能。
成本地圖:一張活的危險圖
兩個規劃器真正在其上搜尋的是什麼?不是原始地圖,而是一張成本地圖——一個網格,其中每個單元都帶著一個數字,表示佔據它有多危險、多「貴」。空地板成本為零;牆壁不可通行(成本相當於無窮大);緊貼牆壁那一圈單元成本很高,但不是無窮大。規劃器於是在這片由數字構成的地形上尋找最便宜的路線,自然就偏好走廊空曠的中線,而不是擦著邊緣走。
有兩個想法讓成本地圖真正活了起來。第一是「膨脹」:每個障礙都被一圈逐漸升高的成本向外暈染開。這是成本地圖記住「機器人不是一個點、它有寬度」的方式。把機器人的尺寸作為餘量烤進地圖,規劃器就能繼續把機器人當作一個點來處理,卻永遠不會擦到拐角。它是在組態空間裡給障礙「長胖」那一招的實用表親。
第二是「衰退」:讀數會變舊。片刻前擋住門口的人已經走開,所以一處近來無人確認的感測器標記就該被清除,讓那個單元重新變便宜。一張好的成本地圖,會在感測器看見障礙的地方不斷添加新障礙,又在感測器現在看見空地的地方清掉過時的標記。通常會有兩張這樣的地圖——一張供全域規劃器用、大而靜態,另一張以機器人為中心、隨之滾動的小地圖供局部規劃器用——兩張都從同一條即時感測器資料流刷新。
GLOBAL LOOP (slow, e.g. on new goal): global_path = A_star(costmap_global, start, goal) LOCAL LOOP (fast, ~10-20x per second): costmap_local = update(sensor_scan) # add new, clear stale segment = best_feasible_arc_toward(global_path, costmap_local) cmd_velocity = to_trajectory(segment) # path + speed -> motion send(cmd_velocity)
它會在哪裡出問題,以及接下來是什麼
這套架構很結實,但它的失敗方式是有跡可循的;認得它們,就等於用好它的一半。
- 局部極小值。反應式局部規劃器可能把機器人引進一個死胡同——比如一圈擺成 U 形的椅子——在那裡,每個方向看起來都比原地不動更糟,於是它僵住了。這正是勢場方法臭名昭著的那個陷阱。常見的解法是讓全域規劃器重新選路,或觸發一個恢復行為,比如倒車並原地轉身。
- 成本地圖滯後。如果感測器資料慢了,或地圖更新晚了,機器人就會基於一幅已經過時的世界圖行動——為一個早已移開的「幽靈障礙」急煞,或者更糟,漏掉一個真實的障礙。調節單元膨脹與衰退的快慢,是在「神經過敏」與「魯莽」之間永無止境的權衡。
- 定位出錯。整個堆疊都假設機器人知道自己在地圖上的位置。一旦定位漂移,成本地圖就會對齊到錯誤的牆上,於是一個本來完美的規劃,反而把機器人開向一堵真實的牆。
前沿方向是軟化這些層之間的硬邊界。比起手工調好的規劃器加手工設計的成本地圖,基於學習的規劃器訓練單一模型,把原始感測器輸入直接映射為運動,從經驗或示範中同時吸收「找路」與「避讓」。它們許諾在人群中做出更順滑、更像人的導航——代價是更難驗證、更難信任。就目前而言,「全域加局部」的分層堆疊仍是可靠的主力,而習得的方法多半是嫁接在它之上,而非取而代之。