自动机有状态没动作,马尔可夫模型有概率没目标。 把它们合起来,再加一个「现在该干什么」,就是一个状态驱动智能体。 这一节跑一个最小的例子:扫地机器人。两个版本 —— 只盯眼前的反应式,和带三个状态的状态驱动。 差别不在代码行数,在「记不记得自己正在干什么」。
前两节把「状态」和「转移」拆开讲了,这一节把它们装回一个具体的东西里。 这也是后面所有智能体讨论的起点: 第 8 章的强化学习,学的正是「什么状态该做什么动作」这张表 —— 只不过表不是人写的, 是试出来的。
一、三个零件:感知、状态、动作
智能体的最小结构只有三句话:传感器读环境,根据读到的更新状态,按状态选一个动作。 它和自动机差在哪?自动机的输入是外面喂进来的符号,智能体的输入是自己感知到的; 自动机只有转移,智能体必须产出一个动作去改变环境。
| 模型 | 输入 | 内部 | 输出 |
|---|---|---|---|
| 有限自动机 | 外部给的符号 | 状态 | 接受/拒绝 |
| 马尔可夫链 | 无 | 状态上的概率 | 下一状态的概率 |
| 状态驱动智能体 | 自己感知到的 | 状态 | 动作 |
| 中间那一列是同一个东西。状态这个词看着抽象,说白了就一句: | |||
| 把过去发生的一切,压成现在够用的一句话。扫地机器人的状态是「清扫/回充/充电」, | |||
| 这三个词把它过去几小时干的事全压缩进去了。 | |||
| 压缩必然丢信息,这是设计的一部分。状态选得太细,状态数爆炸;选得太粗,该记得的没记住。 | |||
| 这一节后面两条都在讲这个取舍。 |
二、只盯眼前会怎样
最省事的写法是反应式: 每一步只看当前感知,直接选动作,不留状态。布鲁克斯 1986 年那套「包容架构」就是这一派, 他的口号是「世界就是自己最好的模型」—— 不用建模,用传感器顶着。 优点很实在:简单、快、坏一个模块不影响别的、不需要预先知道环境长什么样。昆虫机器人、 扫地机的底层避障、四足机器人的平衡控制,都是这一套。 缺点也很实在:没有目标,会卡在局部最优。Demo 第一段那个机器人, 规则只有一条「朝最近的脏格走一格」,它擦掉 59 个格子就停了 —— 不是累了,是没电了。 注意这不是规则写错了。规则对每一格都判断正确, 可它推不出「该回去充电」这件事 —— 因为充电桩不是一个脏格,在它的感知里根本不存在。
三、三个状态,一个圈
加三个状态,问题就没了:
| 状态 | 这时候干什么 | 什么条件下离开 |
|---|---|---|
| 清扫 | 朝最近的脏格走 | 电量降到 20 以下 |
| 回充 | 朝充电桩走 | 人已经在桩上 |
| 充电 | 原地不动,每步回 5 格 | 电回到 100 |
| 三个状态连成一个圈,每个状态有自己的目标和退出条件。Demo 第三段的结果:同样 3000 步, | ||
| 反应式擦掉 59 格,状态驱动擦掉 120 格 —— 满盘 120 格,全擦完了,还剩 60 的电。 | ||
| 最有说服力的是「用掉的步数」这一行:100 对 3000。反应式只用了 100 步就停在原地, | ||
| 剩下 2900 步什么也没干。它不是在低效率地工作,是彻底停摆了。 | ||
| 状态替它记住的东西,比任何变量都硬。「我现在在回充」这件事要跨过几十步、 | ||
| 跨过一路上的脏格才算完,这几十步里它每走一步都在用这个状态做决定。换成变量也能实现, | ||
| 但那就等于承认「需要一个状态」了。 |
四、门槛是代价,不是成绩
Demo 第四段把充电门槛从 10 扫到 80:
| 门槛 | 擦掉 | 回桩次数 | 走过的格子 |
|---|---|---|---|
| 10 | 120 格 | 25 次 | 262 个 |
| 20 | 120 格 | 28 次 | 259 个 |
| 30 | 120 格 | 32 次 | 270 个 |
| 50 | 120 格 | 44 次 | 240 个 |
| 80 | 64 格 | 60 次 | 167 个 |
| 门槛 10 到 50 这几档都把 120 格擦完了,区别只是回去的趟数从 25 涨到 44。 | |||
| 来回的路程也算在步数里,所以这个涨幅是实打实的浪费。 | |||
| 提到 80 就崩了:只擦了 64 格。因为一半以上的步数都在路上。 | |||
| 这条曲线有个很典型的形状:左边一段平的,右边突然掉下去。 | |||
| 工程上所有状态机的参数都是这个形状:超时时间、重试次数、电池阈值、心跳间隔。 | |||
| 它们不是「调大更保险」,是「过了某个点就翻脸」。所以这类参数得算,不能凭感觉拍。 | |||
| 这一派做法的好处和坏处都很清楚。好处是行为可以解释 —— | |||
| 出了事问一句「它当时在哪个状态」就有答案,这是安全相关的系统非要它不可的原因。 | |||
| 坏处是状态和门槛都得人来定,环境一变就得重定。 |
五、状态不是白来的
想让机器人更聪明,就得让它记更多东西。每多记一样,状态数就往上乘:
| 要它记住的事 | 乘上 | 合计 |
|---|---|---|
| 只管擦地 | 1 | 1 |
| 加电量管理 | × 3 | 3 |
| 再加「这格刚擦过」 | × 2 | 6 |
| 再加「上次往哪走」 | × 4 | 24 |
| 再加「房间分四块」 | × 4 | 96 |
| 再加「卡住过几次」 | × 3 | 288 |
| 乘到 288。到这一步,手写转移表已经不现实了,而且大部分组合永远不会出现:电快没了、 | ||
| 又卡住过两次、又在第三块房间、又刚从左边来 —— 这种情况谁见过。 | ||
| 两条出路。一条是拆:把大状态机拆成几个小模块各管一摊,只在对得上的时候对暗号, | ||
| 这就是上一节说的层级状态机。 | ||
| 另一条是不写表:让机器自己学「什么状态该做什么动作」。那就是强化学习,第 8 章的内容。 | ||
| 这一章三节说到底是一句话:智能体身上最要紧的东西不是它算得多快,是它记得住什么。 | ||
| 自动机记得住「现在在哪一档」,马尔可夫模型记得住「上一步」,状态驱动智能体记得住「我正在干什么」。 | ||
| 记的东西不一样,能做的事就不一样。 |
六、动手 Demo:会擦地、会自己回家充电的机器人
房间 20 乘 20,120 个脏格,角落里一个充电桩。机器人只能上下左右走一格,走到脏格上就擦掉,
每步掉 1 格电,在桩上每步回 5 格。
两个版本共用同一套移动函数 step_to 和找脏格的 nearest,
唯一的区别是有没有那个 st 变量。反应式版本里压根没有这个变量,它每步现算;
状态驱动版本把它带在身上。
def run_states(seed, steps=3000, low=20):
dirt = make_world(seed) # 120 个脏格
pos, bat = DOCK, FULL # 从桩上满电出发
cleaned = charges = 0
st = u'清扫'
for _ in range(steps):
if st == u'清扫':
if bat <= low:
st = u'回充' # 唯一的「记得住」的地方
else:
if pos in dirt:
dirt.discard(pos)
cleaned += 1
pos = step_to(pos, nearest(pos, dirt))
elif st == u'回充':
if pos == DOCK:
st = u'充电'
charges += 1
else:
pos = step_to(pos, DOCK)
else:
bat = min(FULL, bat + 5)
if bat >= FULL:
st = u'清扫'
bat -= 1
return cleaned, charges, bat
实跑输出:
--------------------------------------------------------------------
会记得自己在干什么的机器人
--------------------------------------------------------------------
==============================================================
[1] 先把任务摆清楚
20x20 的房间,120 个格子是脏的,角落里有一个充电桩。
机器人每次只能上下左右走一格,走到脏格上就把那一格擦干净。
电量满格 100,每走一步掉 1 格;在桩上充电每步回 5 格。
一个只盯眼前的机器人会这么干:
每一步:找出离我最近的脏格,朝它走一格。
这条规则很简单,也没写错。可它有个致命的地方:
它不知道「我正在回充电桩」这件事,因为桩不是一个脏格。
电量掉到 0,它就停在原地——不是坏了,是它压根没有「该充电了」这个念头。
[2] 给它加三个状态
状态 这时候干什么 什么条件下离开
----------------------------------------------------------------
清扫 朝最近的脏格走,走到就擦 电量降到 20 以下
回充 朝充电桩走,不理脏格 人已经在桩上
充电 原地不动,每步回 5 格电 电回到 100
三个状态,连成一个圈。这里的关键不是多了两行代码,
而是机器人身上多了一个变量:它现在处在哪个状态。
这个变量替它记住了「我刚才决定要回去充电」。
[3] 两种机器人各跑 3000 步
棋盘种子 反应式擦掉 状态驱动擦掉
--------------------------------------------
1 59 格 120 格
2 57 格 119 格
3 60 格 120 格
种子里 1 这一局摊开看:
反应式 状态驱动
------------------------------------------
擦掉 59 格 120 格
走到的格子 94 个 259 个
用掉的步数 100 3000
最后剩电 0 60
回桩次数 0 28
反应式那一边,100 步电就见底了,之后一直停在原地。
它不是不够努力,是被自己的规则卡住了:
规则里没有「该回去」这一档,它就只能一直朝脏格走。
状态驱动那一边把 120 个脏格全擦了,电还剩 60。
差别不在它算得快、也不在规则更聪明,
而在于它身上多了一个「我现在在回充」的状态。
这个状态替它把「回去充电」这件事一直记着,
直到它真的踩到充电桩上才换掉。
[4] 回桩次数是代价,不是成绩
把充电的门槛从 10 一路提到 80,看擦掉多少、回去几趟:
电量门槛 擦掉 回桩次数 走过的格子
--------------------------------------------------
10 120 格 25 次 262 个
20 120 格 28 次 259 个
30 120 格 32 次 270 个
50 120 格 44 次 240 个
80 64 格 60 次 167 个
门槛 10 到 50 这几档,120 个脏格都擦完了,区别只是回去的趟数;
提到 80 就崩了:3000 步里大半都在路上,只擦了 64 格。
来回的路程也算在步数里,门槛不是越大越好,得算。
这就是「状态驱动」这一派的做法:把行为切成几段,
每段有自己的目标和退出条件,用状态把段和段接起来。
好处是行为可以解释 —— 出了问题问一句「它当时在哪个状态」。
坏处是状态和门槛都得人来定,定得不好就白跑。
[5] 状态不是白来的
现在这个机器人只记一件事:我在哪个状态。
想让它更聪明,就得让它记更多东西,而每多记一样,状态数就往上乘:
要它记住的事 状态数乘上 合计
--------------------------------------------------
只管擦地 1 1
加电量管理 x 3 3
再加「这格刚擦过」 x 2 6
再加「上次往哪走」 x 4 24
再加「房间分四块」 x 4 96
再加「卡住过几次」 x 3 288
乘到 288 个状态。到这一步,手写转移表已经不现实了,
而且大部分组合永远不会出现:电快没了、又卡住过两次、
又在第三块房间、又刚从左边来 —— 这种情况谁见过。
所以真实的机器人不会这么写。它们要么把大状态拆成几个小模块各管一摊,
要么干脆不写状态表,让机器自己学「什么情况该干什么」——
那就是后面几章要讲的强化学习了。
这一章三节连起来其实是一句话:
智能体身上最要紧的东西不是它算得多快,是它记得住什么。
自动机记得住「现在在哪一档」,马尔可夫模型记得住「上一步」,
状态驱动智能体记得住「我正在干什么」。
记的东西不一样,能做的事就不一样。
第一段先把任务摆清楚,注意「桩不是一个脏格」这句话。这是全部问题的根: 反应式机器人只认脏格,所以它永远看不见桩。 第二段那张三行表就是整个智能体。每行都有「干什么」和「什么时候走」, 少了后一半,状态机就会卡死在某个状态里 —— 这是写状态机最常见的错。 第三段那张对比表里,最扎眼的是「用掉的步数 100 对 3000」。擦掉 59 对 120 只是结果, 步数才是原因:反应式 100 步就停摆了,剩下 2900 步全是在原地耗着。 第四段扫门槛,形状是「先平后崩」。10 到 50 都一样擦完,只是回桩次数从 25 涨到 44; 到 80 直接掉到 64 格。这条曲线是这类参数的通例,别指望「调大更保险」。 第五段是把账算给你看。1 → 3 → 6 → 24 → 96 → 288,每一行只是「再记一样东西」, 可它是乘上去的,不是加上去的。这就是为什么真实机器人不硬写状态表。 最后那段把三节串起来了,值得回头再读一遍。自动机、马尔可夫、状态驱动智能体, 用的是同一个「状态」,差别只在状态里装了什么,以及装的东西从哪来。
30 秒实验
- 把
low改成 5,重跑第三段。 看它还擦不擦得完 —— 门槛压得太低,从房间另一头往回赶的时候电量可能不够, 来回反而更费步数。 - 把房间放大到 40 乘 40、脏格改成 400,还是 3000 步。 这次两个版本都擦不完了,看状态驱动那一边擦掉的比例,再想想 3000 步大概够跑多大一片。
- 在「清扫」状态里加一条规则:如果上一步是往左走的,这一步优先往右。 比较「走到的格子」这个数字变不变 —— 这条规则是防止它在两个脏格之间来回抖的。
常见误区
- 以为状态越多越智能。状态是乘起来的,加一个维度可能翻几倍。 「再加一个状态就能解决」是智能体设计里最常见的错误,代价通常是指数级的。
- 以为反应式已经过时了。扫地机的避障、四足机器人的平衡、昆虫大小的机器人,底层全是反应式。 它的优点是「不需要知道环境长什么样」,这在真实世界里非常值钱。
- 以为门槛调高更保险。Demo 里门槛 80 直接让擦掉的格子从 120 掉到 64。 状态机里的每个阈值都是「多跑的路」和「少做的事」之间的交易,得算。
- 以为状态驱动智能体就是「加一堆 if」。区别还是上一节那条: 状态是一份可数的、能画成图的东西。能画成图,才能发现「这个状态没有出口」这种错。
自测三问(点开对答案)
1. 反应式机器人擦掉 59 格就停了,到底是哪里出了问题? —— 规则没错,**是规则里缺了一档**。 「该回去充电」不是一个脏格,所以按「朝最近的脏格走」这条规则,它永远推不出这个结论。 **这就是为什么需要状态:状态能把「我刚才决定了什么」带过好几十步。** 2. 为什么「用掉的步数」这一行比「擦掉」那一行更能说明问题? —— 因为擦掉多少是结果,步数是被卡住的直接证据。 **反应式只用了 100 步就停住,剩下 2900 步什么也没干 —— 它不是在低效率地工作,** **是彻底停摆了。**换成「擦掉」,你还得去猜它是慢还是坏;看步数,猜都不用猜。 3. 状态乘到 288 之后,为什么工程上不硬写? —— 两个原因。**一是大部分组合永远不会出现**,写了也用不上; **二是人写不出 288 行不出错的转移表**,而且写完了没人能验证。 真做法是拆成几个小状态机各管一摊(层级状态机),或者让机器自己学(强化学习)。这一章讲的是「人把状态和规则写好,让机器按着跑」。 下一章开始换问题:规则不写了,改成给一堆数据点,让机器自己去找一条最合适的线。
- ☐ (本章完,下一节 4.1 线性问题和非线性问题)