实验条件:方向传感器故障——机器人对自己的朝向感知永远是"北"。
最终结果:通关率 100%,平均步数从 370 降到 89,自动评测满分 50/50。

具身智能状态估计路径规划BFSPOMDP

目录

1. 先看结果

指标醉汉 baseline优化后提升
平均步数37089快 4.2 倍
金币收集率58%97%1.7 倍
通关成功率8/15 (53%)15/15 (100%)1.9 倍

逐图明细(每张图跑 3 次,策略确定性,3 次结果完全相同):

地图baseline 步数 / 金币 / 通关优化后 步数 / 金币 / 通关
地图 1 新手村370 / 67% / 1÷381 / 3÷3 / 3÷3
地图 2 小径319 / 75% / 2÷394 / 4÷4 / 3÷3
地图 3 花园408 / 73% / 2÷390 / 5÷5 / 3÷3
地图 4 迷宫入口333 / 50% / 2÷390 / 6÷6 / 3÷3
地图 5 挑战迷宫419 / 24% / 1÷392 / 6÷6 / 3÷3

① 状态估计 —— 我在哪、朝向哪本实验:用坐标位移反推朝向 | 进阶:扩展卡尔曼滤波、粒子滤波、SLAM② 世界建模 —— 环境长什么样本实验:字典存二值地形(空地/墙/金币) | 进阶:占据栅格 + log-odds 概率更新③ 路径规划 —— 怎么走过去本实验:BFS 最短路,每步重规划 | 进阶:A*、D* Lite、带转向代价的状态栅格④ 目标决策 —— 现在该干什么本实验:金币优先级 > 预算内探索 > 终点 | 进阶:信息增益/距离启发式、POMDP 策略

图 1 具身智能算法栈的四个层次,以及本实验与工业界做法的对应关系

2. 实验设定:坏掉的传感器

GridBot 是一个 10×10 网格世界的教学实验。机器人每步能拿到:

  • 四周感知 front / left / right / back,取值是"空地 / 墙壁 / 金币 / 终点 / 边界";
  • 自己的坐标 position,以及终点坐标 target(公开信息);
  • 记忆空间 memory,跨步持久的列表,可读可写;
  • 每步返回一个动作:前进 / 左转 / 右转 / 后转。

Baseline 是"醉汉策略"——随机走,撞墙就随机转身,平均 350 步通关、成功率 60%。

而我的实验条件更狠:方向传感器坏了,perception["direction"] 永远返回 "北"。 偏偏所有感知都是相对自身朝向的——朝向错了,"我看到的东西在地图哪一格"就算不出来, 建图、寻路、捡金币全部无从谈起。

一句话概括这个实验的难点:信息缺失 + 信息不可信,只能用"行动—反馈"把丢掉的信息重新算出来。

3. 我的解决方案总览:五个机制

先说全貌。整套方案就是五个机制叠起来,每个机制解决一个具体问题:

#机制解决什么问题关键代码为什么有效
1航位推算不知道自己的朝向坐标位移反推 + 转向叠加位移是绝对观测,无法被故障传感器污染
2开局校准第一步之前朝向未知先走一步,走不动就右转一次移动就能反推出朝向,成本 ≤ 4 步
3记忆地图不知道环境长什么样相对感知 → 绝对坐标地图只增不减,重复观测互相印证
4BFS 三级决策不知道该往哪走金币 > 探索 > 终点每步都是最短路,目标单调推进,不会死循环
5绕开终点格提前通关导致金币丢失BFS 的 avoid_target终点是吸收态,必须当障碍而非通路

下面逐条拆开讲,包括代码、为什么这么写、以及它成立的前提。

4. 机制一:航位推算 —— 用动作和坐标把朝向"算"回来

4.1 思路

方向传感器失灵,但坐标传感器是好的。而我自己发的每一条指令我自己都记得,于是: 前进之后坐标变化了多少,那个变化量的方向,就是我的真实朝向。

(0, -1) → 北    (1, 0) → 东    (0, 1) → 南    (-1, 0) → 西

四种位移唯一对应四个绝对朝向,没有歧义。这不是猜测,是引擎给出的坐标事实。

转向则更简单:转向时位置不变,但"左转/右转/后转"是我自己发出的指令,必然执行成功, 所以在已知朝向上叠加 ±90°/180° 即可;撞墙没动,朝向也不变。

动作 u_t:前进 / 左转 / 右转 / 后转自己发出的指令,必然被执行成功预测:θ̂_t = θ_t−1 ⊕ u_t转向可精确叠加 ±90° / 180°,所以预测无噪声观测:位移 Δx = 位置_t − 位置_t−1坐标传感器每步给出绝对位置,无累积漂移校正:θ_t 由 Δx 唯一确定(0,1)→南 (1,0)→东 (0,−1)→北 (−1,0)→西每步循环

图 2 朝向估计的预测—校正闭环(贝叶斯滤波的退化情形)

4.2 代码

# 航位推算:用"上一步发生了什么"更新真实朝向
last_pos = state[0]
500">if last_pos 500">is 500">not 500">None 500">and pos != last_pos:
    # 上一步"前进"且真的移动了 → 位移方向 = 真实朝向
    moved = (pos[0] - last_pos[0], pos[1] - last_pos[1])
    500">for d, delta 500">in DELTA.items():
        500">if delta == moved:
            state[0] = d
            state[0] = 500">True
            500">break
500">elif state[0] 500">in TURN_DELTA 500">and state[1]:
    # 上一步执行了转向 → 在已知朝向上叠加转角
    state[0] = (state[1] + TURN_DELTA[state[2]]) % 4
state[0] = pos

三条分支覆盖全部情形:动了 → 反推;转了 → 叠加;撞墙没动 → 不变。

4.3 为什么它不会累积漂移

观测类型误差行为
轮式里程计(真机器人)增量观测:这一小步走了多远、转了多少度微小误差逐步累加,位置误差 O(t)、朝向误差 O(√t) 发散
本实验的 position绝对观测:我在 (x, y),是真值误差每步被观测直接清零,不存在累积

所以我这里其实不是"估计",而是解方程:朝向是 4 个离散值之一, 观测(位移方向)能把候选一次性收敛到唯一答案。 唯一的代价是:没移动的那些步,朝向无法被观测校正——正好由"转向自己记 + 撞墙不变"补齐。

4.4 开局校准:解决"第一步之前朝向未知"

500">if 500">not state[0]:
    500">if perception[0] 500">not 500">in (1, 2):
        500">return 0      # 能走就走一步,一动就校准成功
    500">return 0          # 走不动就换个方向再试

代价上界:最坏情况原地转 3 次找到通路,第 4 步必然校准成功。 换来的是后面所有建图都建立在已证实的朝向上——否则错一步,整张地图的坐标就全歪了。

5. 机制二:记忆地图 —— 把局部感知贴到全局坐标

500">for rel 500">in range(4):
    abs_dir = (heading + rel) % 4          # 相对方向 → 绝对方向
    dx, dy = DELTA[abs_dir]
    cell_pos = (pos[0] + dx, pos[1] + dy)  # 贴到全局坐标
    cell = perception[REL_CELL[rel]]
    worldmap[cell_pos] = 0 500">if cell == 1 500">else cell

三个设计决定,每个都有原因:

  1. 地图存在 memory[0] 里(字典 {(x, y): 地形名})。 memory 是引擎给的跨步持久空间,memory[0] 放状态字典,后续每步取出复用。
  2. 地图外一律记成"墙壁"。后面的搜索天然不会跑出地图,不用在每个算法里重复做边界判断。
  3. 站上金币格要把它从地图里消掉。金币被捡走后若还当目标, 就会陷入"到那儿没东西 → 再去别处 → 又回来"的死循环。地图必须与真实世界同步。

6. 机制三:BFS 最短路 + 三级目标优先级

500">def bfs(goal_test, avoid_target=500">False):
    queue = deque([pos]); prev = {pos: 500">None}
    500">while queue:
        cur = queue.popleft()
        500">if cur != pos 500">and goal_test(cur):
            path = []
            500">while cur != pos:
                path.append(cur); cur = prev[cur]
            500">return path[::-1]
        500">for dx, dy 500">in DELTA.values():
            nxt = (cur[0] + dx, cur[1] + dy)
            500">if nxt 500">in prev 500">or 500">not in_grid(nxt):
                500">continue
            500">if avoid_target 500">and nxt == target:      # 别从终点身上踩过去
                500">continue
            500">if worldmap.get(nxt) == 0:          # 已知墙不撞
                500">continue
            prev[nxt] = cur; queue.append(nxt)      # 未知格子也允许走 = 探索能力
    500">return 500">None

优先级:① 有已知金币 → 去最近的那个(路上不许穿过终点); ② 没有已知金币且步数 < 75 → 去最近的"探索前沿"(已知通路旁还有未知格子的位置), 并在最短的若干条候选里优先挑第一步不用转身的(转身也算一步); ③ 预算用完或没有前沿可探 → 直奔终点。

  • 为什么绝不撞墙:BFS 只把"非墙壁"的格子入队,而相邻格子必然已被感知记录过(每步记录四周 4 格),所以路径第一步永远是已知可通行格。
  • 为什么不会死循环:每步都在缩短到当前目标的最短路距离,而目标集合(金币 → 前沿 → 终点)是单调消耗的。
  • 为什么要 avoid_target:踩上终点是吸收态,游戏立即结束。"不把终点当目标"和"不从终点经过"是两件事——后者我一开始漏了(见第 8 节坑 4)。

7. 机制四:探索预算 —— 一个真实的取舍问题

探索越久金币越多,但步数也越多;评测规则要求"步数 < 100"才拿满分 20 分。 所以这本质上是带约束的最优化问题:在步数 < 100 的约束下最大化金币收集率。

探索预算平均步数金币率总分
708389%50/50
758997%50/50
859797%50/50
95 以上≥10097%45/50

75 步是"金币已经吃满"里步数最少的一档,离 100 步红线还有 11 步余量。 再往上加,金币已经涨不动(原因见坑 6),步数却会突破 100 掉分。

8. 踩坑记录:六个问题与解决过程

整个实验我一共栽了六次,按"现象 → 排查 → 根因 → 修复"记下来。先看总览:

#问题根因修复
1完全不知道自己的朝向direction 永远返回"北",是废数据航位推算 + 开局校准
2机器人卡在角落来回抖动前沿判定把地图外的格子也算成未知加地图范围检查
3探索到一半就提前通关终点格也被当成了"探索目标"探索目标判定中排除终点
4修完坑 3,还是提前通关路径从终点格上穿过去(路过也算踩上)BFS 加 avoid_target 参数
5金币与步数此消彼长探索预算固定,两项评分互相拉扯扫参定在 75 步
6金币率怎么调都卡在 97%地图 5 声明 7 个金币,实际只有 6 个格确认是地图数据的天花板

坑 1:朝向完全不可知 —— 整个实验的出发点

现象:perception["direction"] 永远返回"北"。四个方向的感知都是相对自身朝向的, 朝向错了就意味着"我看到的东西在地图哪一格"完全算不出来。

排查:确认这是实验条件强制的传感器故障。盘一遍还有哪些信息可信: 四周格子(可信)、自己的坐标 position(可信)、终点坐标(公开)、记忆空间(可写)。

根因:传感器坏了,但"我做过什么动作"这件事我永远知道——因为动作就是我发出的。

修复:航位推算(位移反推 + 转向叠加)+ 开局校准(见第 4 节)。

坑 2:机器人卡在地图角落来回抖动

现象:步数暴涨,但地图格子数停在 37 不再增长,机器人在 (9,0) 和 (9,1) 之间反复横跳。

排查:打印每步的"可探索前沿"列表,发现右上角靠边的格子永远在列表里—— 它旁边的"未知格子"是地图外面的坐标。

根因:前沿判定把地图外的格子也算成了未知区域,靠边的格子永远满足条件,BFS 一直把它当最近目标。

修复:

500">def frontier_goal(c):
    cx, cy = c
    500">return any(
        0 <= cx + dx < GRID_SIZE 500">and 0 <= cy + dy < GRID_SIZE
        500">and (cx + dx, cy + dy) 500">not 500">in worldmap
        500">for dx, dy 500">in DELTA.values()
    )

坑 3:机器人"探索"到一半就提前通关了

现象:地图 3 只走 43 步就结束,5 个金币只捡到 2 个。疑点:预算才用 43/75,地图还有一大片没探。

排查:日志显示第 40 步机器人在 (9,6),地图已有 68 格、还有 10 个前沿没去, 但下一步就朝 (9,7) → (9,8) → (9,9) 走过去了——那是终点。

根因:终点格 (9,9) 在还没探索过它周围时,也满足"旁边还有未知格子", 被当成普通探索目标,而它恰好最近。踩终点不可逆。

修复:探索目标判定里排除终点,把"探索"和"去终点"彻底分开:

500">def frontier_goal(c):
    500">if c == target:      # 终点不算探索点
        500">return 500">False

修完地图 3 从 43 步 / 2 金币,变成 90 步 / 5 金币全捡。

坑 4:修完坑 3,还是提前通关

现象:地图 2 仍然只跑 38 步、4 个金币只捡 2 个。

排查:第 35 步机器人在 (9,7),还有 14 个探索点没去、预算才用一半, 但它连着三步 (9,7) → (9,8) → (9,9) 又踩上了终点。

根因:它的目标是探索点 (9,8)(合法),但最短路径正好从终点格上穿过去。 "不把终点当目标"和"不从终点身上路过"是两件事。

修复:

500">if avoid_target 500">and nxt == target:
    500">continue        # 别从终点身上踩过去

修完地图 2 从 2/4 金币变成 4/4 全捡。

坑 5:金币与步数此消彼长,怎么调都有短板

现象:一开始拍了个 80 步(金币 87%、步数 81);修完绕行问题后,同样 80 步,步数涨到 93,余量只剩 7 步。

根因:两项指标存在真实冲突——金币靠探索换来,探索必然消耗步数;且金币率有上限(坑 6),触顶后再加预算就是纯亏步数。

修复:扫参后定在 75 步(见第 7 节)。

坑 6:金币率怎么调都卡在 97%

现象:预算加到 120 步,地图 5 依然只有 6/7。

排查:日志显示地图 5 已无可探索前沿(整张图探完)。于是去数地图数据本身:

500">from maps 500">import ALL_MAPS
500">for m 500">in ALL_MAPS:
    actual = sum(1 500">for row 500">in m[0] 500">for c 500">in row 500">if c == 2)
    print(m[0], 1, m[2], 3, actual)
地图 1 — 新手村:   声明金币=3 实际=3  差异=0
地图 2 — 小径:     声明金币=4 实际=4  差异=0
地图 3 — 花园:     声明金币=5 实际=5  差异=0
地图 4 — 迷宫入口: 声明金币=6 实际=6  差异=0
地图 5 — 挑战迷宫: 声明金币=7 实际=6  差异=1   ← 问题在这

根因:MAP_5 的元数据 "coins": 7 与网格里真实金币格数量不一致。 地图 5 上限就是 6/7,全局天花板 = (3+4+5+6+6) / (3+4+5+6+7) = 96% (评测按每图比率取平均,显示 97%)。

修复:无需改代码,但要确认它不是策略缺陷—— 地图上真实存在的金币,我的机器人 100% 全部捡到了。 教训:分数上不去时,先分清"我的算法不行"还是"数据本身有问题"。

9. 深层算法:这套玩具背后站着什么

9.1 状态估计:我写了一个退化版的贝叶斯滤波

预测:bel⁻(θ_t) = ∫ p(θ_t | u_t, θ_{t-1}) · bel(θ_{t-1}) dθ_{t-1}
校正:bel(θ_t) ∝ p(x_t | θ_t) · bel⁻(θ_t)

我的场景是它的退化情形:状态只有 4 个离散值、运动确定性、观测无噪声, 后验概率直接塌缩成一个确定值——不是"估计",是"解方程"。真实机器人有三代方法:

① 扩展卡尔曼滤波(EKF)
状态:x = (x, y, θ)ᵀ,信念 N(μ, Σ)
预测:μ⁻ = f(μ, u)              Σ⁻ = F Σ Fᵀ + Q      F = ∂f/∂x |μ
校正:K  = Σ⁻ Hᵀ (H Σ⁻ Hᵀ + R)⁻¹                     H = ∂h/∂x |μ⁻
      μ  = μ⁻ + K (z − h(μ⁻))   Σ = (I − K H) Σ⁻

直观理解:Q 是"我对自己动作有多不信",R 是"我对传感器有多不信", 卡尔曼增益 K 就是在两个不信任度之间做加权平均。

② 粒子滤波(Particle Filter)
初始化:采样 N 个粒子 {x_i},权重 1/N
循环:
  1. 预测:每个粒子按运动模型采样   x_i ← f(x_i, u) + 噪声
  2. 加权:w_i ∝ p(z | x_i)        (谁0观测,谁就更重)
  3. 重采样:按权重有放回地重抽 N 个粒子(权重大的被复制,小的被淘汰)

杀手级场景是"全局定位":信念可能同时有几十个峰(几条长得一样的走廊),高斯分布表示不了,粒子群可以。

③ SLAM

当位姿和地图互相依赖(不知道自己在哪 → 地图画歪 → 定位更不准),必须联合估计位姿与地图。 EKF-SLAM 把地图特征点拼进状态向量(协方差随特征数平方增长),现代方案是图优化: 把位姿与观测建成图,用最小二乘整体求解。本实验若换成"坐标定位故障"条件,问题立刻退化到这个难度的下限版本。

9.2 建图:从二值字典到占据栅格

L(m_i) ← L(m_i) + log[ p(m_i | z_t) / (1 − p(m_i | z_t)) ] − L_0
其中 L = log[ p / (1 − p) ]
  • 数值稳定性:概率连乘会下溢到 0,log-odds 把乘法变加法;
  • 可累积:同一格被反复观测,每次加一个证据项,置信度自然增长;
  • 需要逆观测模型 p(m_i | z_t):传感器只说"这条激光被挡住了", 要从"射线打到了东西"反推"沿途格子大概是空的、终点那格大概有障碍"。

9.3 路径规划:BFS 只是这条链的起点

算法关键思想复杂度适用场景
BFS(我的实现)无权图逐层扩散O(V + E)格子等权、小地图
Dijkstra带权图,按已用代价出队O((V+E) log V)地形代价不同
A*f = g + h,启发式引导通常远快于 Dijkstrah 可采纳时才保证最优
D* Lite环境变化后增量修复路径比重算便宜一个量级边走边发现障碍
状态栅格 / Hybrid A*状态扩成 (x, y, θ)状态数 ×4 起步真车不能原地转,必须走圆弧

关于 A* 的 h:四邻接网格里取曼哈顿距离是可采纳的(实际最短步数不可能小于它), 所以 A* 既搜得快、又保证最优。BFS 其实就是"h ≡ 0 的 A*",Dijkstra 是"h ≡ 0 的带权版"。

一个诚实的瑕疵:我的 BFS 只数格子、不数转身,而转身在规则里也算一步。 我靠"等长候选里优先不转身"打了个补丁,正确做法是把状态扩成 (x, y, θ) 或给换方向的边加权重:

# 改造示意:把 (位置, 朝向) 作为搜索状态,边权 = 动作步数
500">for action 500">in (0, 1, 2, 3):
    cost = 1                                  # 每个动作都消耗 1 步
    nxt_state = apply(pos, heading, action)    # 前进会移动,转向只改朝向
    push(nxt_state, g + cost)                  # 4 倍状态空间的 Dijkstra/A*

9.4 金币收集顺序:一个 TSP 的在线版本

"按什么顺序把金币全捡了、总步数最短"就是旅行商问题(TSP)——NP 难。 我用的"每次奔最近的金币"是最邻近贪心:O(n²) 很快,但近似质量不保证。进阶做法:

  • 2-opt 局部搜索:反复尝试交换路径里两条边的连接方式,能变短就接受;
  • Christofides 算法:对度量 TSP 给出 1.5 倍近似保证(最小生成树 + 最小权完美匹配 + 欧拉回路短接)。
# 2-opt 伪代码
improved = 500">True
500">while improved:
    improved = 500">False
    500">for i 500">in range(len(route) - 1):
        500">for j 500">in range(i + 2, len(route)):
            500">if dist(route) > dist(reverse_segment(route, i + 1, j)):
                route = reverse_segment(route, i + 1, j)
                improved = 500">True

9.5 目标决策:POMDP 与"探索—利用"权衡

我真正的困难不是排序,而是金币位置未知——典型的探索与利用权衡。 形式化模型是 POMDP(部分可观测马尔可夫决策过程):

POMDP = ⟨ S, A, T, R, Ω, O ⟩
  S  状态(我在哪、地图长什么样、金币在哪)
  A  动作(前进/左转/右转/后转)
  T  转移概率 p(s' | s, a)
  R  奖励(捡到金币 +,撞墙 −,到达终点 +)
  Ω  观测(四周格子 + 坐标)
  O  观测概率 p(o | s', a)
策略 π(a | b) 作用在信念 b(s) = p(s | 历史) 上
信念更新:b0) ∝ O(o | s1 | s, a) b(s)

要点:状态看不见,所以策略不能依赖状态,只能依赖信念。 工程上常用简单启发式——给每个探索点打分:

score(frontier) = 信息增益(frontier) / 路径代价(frontier)

即"能新看到多少格子 ÷ 跑过去要多少步"。我那 75 步预算是这个打分函数的极简替代品: 先做高分动作,预算耗尽就收工。

9.6 一个被复用的工程思想:滚动时域

循环:
  1. 基于当前信息求解未来一段的最优计划
  2. 只执行计划的第一步
  3. 世界变化 / 拿到新观测 → 丢掉剩余计划,回到第 1 步

为什么不用一次性完整计划?因为信息不断更新(新发现的墙会让原计划作废),计划越长越容易失效。 短视 + 勤重算,在动态环境里比"一次算到底"更鲁棒。

9.7 如果继续升级这个实验,我会做三件事

升级具体改动预期收益
A* + 转向代价搜索状态从 (x, y) 扩成 (x, y, θ),动作边权统一为 1 步让"少转身"成为算法内生的解,而不是补丁
2-opt 金币顺序对已发现的金币做局部搜索排序,替换"每次奔最近"多金币地图上减少绕路
粒子滤波实验人为给 position 加噪声,对比航位推算漂移与粒子滤波恢复亲眼看懂"为什么真机器人要 EKF / SLAM"

10. 复现方式

cd gridbot
python run.py --evaluate        # 跑完整评测,输出分数报告
python run.py --map 5 --fast    # 可视化跑地图 5
python run.py --baseline        # 跑醉汉 baseline 做对比

代码要点集中在 my_bot.py 的 decide() 函数里: 航位推算 → 写记忆地图 → BFS 三级目标规划 → 动作换算。

11. 结语

智能不是看懂全局,而是在传感器残缺、信息不完备时, 靠"行动—反馈"不断修正自己的内部世界模型,再据此决策。

方向传感器坏了不可怕——可怕的是你只有那一个传感器。 只要你还能动、能动完还能看到一点反馈,你就能把丢掉的信息重新算回来。 这也是我从这个 10×10 的小网格里,第一次具体地摸到"具身智能"的形状。


本文记录的实验代码与数据均可复现;实验条件由学号分配的传感器故障类型决定。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐