4、第一个仿真器:搭建一个简单的 Grid World 环境
好,咱们今天来动手搭第一个仿真环境。
说实话,很多初学者一上来就啃复杂的机器人仿真、自动驾驶仿真,结果被各种依赖库和配置搞得头大。我个人习惯是:从最简模型开始。你想想看,连一个 4x4 的格子世界都跑不通,谈何复杂的连续控制?
4.1 什么是 Grid World?
Grid World,说白了就是一个棋盘。智能体(Agent)在格子上走来走去,每个格子代表一个状态,智能体可以执行上下左右等动作,走到某个格子会得到对应的奖励。
我在项目中遇到过类似的需求:当时要给一个仓储机器人做路径规划验证,但真实场地还没建好。我就用 Grid World 先模拟了一遍,把货架位置设为障碍物,充电桩设为终点。嗯,效果还不错,至少提前发现了两个逻辑漏洞。
核心四要素:
- 状态空间:所有格子的集合,比如 4x4=16 个状态
- 动作空间:智能体可以执行的动作,比如上、下、左、右
- 转移函数:执行某个动作后,从当前状态到下一个状态的映射
- 奖励函数:进入某个状态或执行某个动作后获得的即时奖励
4.2 定义状态与动作
我们先定一个小目标:一个 4x4 的 Grid World。
状态怎么表示?最简单的方式就是给每个格子编号。从左上角 (0,0) 开始,到右下角 (3,3) 结束。我习惯用一维索引,比如第 0 行第 0 列是状态 0,第 0 行第 1 列是状态 1,以此类推。
动作呢?四个方向:上(0)、下(1)、左(2)、右(3)。
小技巧: 动作用整数编码,后续做 Q-learning 或 DQN 时可以直接当索引用,省去类型转换的麻烦。
4.3 转移函数的设计
转移函数是仿真器的核心。它决定了:你在状态 s 执行动作 a,会到哪个状态 s'?
这里有个坑,我曾经踩过:边界处理。比如智能体在左上角 (0,0) 想往上走,怎么办?
我的做法是:撞墙不动。也就是说,如果执行动作后超出边界,就停留在原地。这样既简单又符合直觉。
另外,我还加了一个障碍物设定。比如把 (1,1) 和 (2,2) 设为障碍物,智能体走进去就弹回原地,并且给一个负奖励。
注意: 转移函数必须是确定性的吗?不一定。你可以加入随机性,比如有 10% 的概率滑到旁边的格子。这在模拟真实机器人打滑时很有用。
4.4 奖励函数的设计
奖励函数决定了智能体的学习方向。设计得好,训练事半功倍;设计得不好,智能体可能学会一些奇怪的行为。
我一般这样设计:
- 到达终点:+10 分
- 走进障碍物:-5 分
- 正常移动:-0.1 分(鼓励智能体尽快到达终点)
为什么要给正常移动一个小的负奖励?你想想看,如果不给惩罚,智能体可能会在终点附近来回转悠,反正也没损失。加一个时间惩罚,它就会想办法走最短路径。
4.5 代码实现
下面是我写的一个简单版本。代码不长,但麻雀虽小五脏俱全。
import numpy as np
class GridWorld:
def __init__(self, size=4, obstacles=None, goal=None):
self.size = size
self.obstacles = obstacles if obstacles else [(1,1), (2,2)]
self.goal = goal if goal else (3,3)
self.state = (0, 0) # 起点
# 动作映射:0上 1下 2左 3右
self.actions = {
0: (-1, 0), # 上
1: (1, 0), # 下
2: (0, -1), # 左
3: (0, 1) # 右
}
def reset(self):
self.state = (0, 0)
return self.state
def step(self, action):
row, col = self.state
dr, dc = self.actions[action]
new_row, new_col = row + dr, col + dc
# 边界检查:撞墙不动
if new_row < 0 or new_row >= self.size or new_col < 0 or new_col >= self.size:
new_row, new_col = row, col
# 障碍物检查:撞障碍物弹回
if (new_row, new_col) in self.obstacles:
new_row, new_col = row, col
reward = -5
done = False
elif (new_row, new_col) == self.goal:
reward = 10
done = True
else:
reward = -0.1
done = False
self.state = (new_row, new_col)
return self.state, reward, done
# 测试一下
env = GridWorld()
state = env.reset()
print(f"起点: {state}")
state, reward, done = env.step(1) # 向下走
print(f"向下走一步: {state}, 奖励: {reward}")
state, reward, done = env.step(1) # 再向下
print(f"再向下: {state}, 奖励: {reward}")
4.6 核心逻辑流程图
下面这张图展示了 Grid World 仿真器的核心运行逻辑。从初始化到每一步的状态更新,再到终止判断,一目了然。
4.7 避坑指南
最后分享几个我踩过的坑:
- 状态表示的一致性:我曾经在状态用 (row, col) 元组,但在 Q-table 里用一维索引,结果 debug 了一下午。建议从一开始就统一表示方式。
- 奖励尺度:奖励值不要太大也不要太小。我见过有人给终点奖励 1000,结果 Q 值爆炸。一般控制在 [-10, 10] 之间比较稳妥。
- 随机种子:如果加了随机转移,记得固定随机种子,否则复现结果时会很痛苦。
好了,第一个仿真器就搭完了。虽然简单,但它是后续所有复杂仿真环境的基础。把这个吃透了,后面学连续控制、多智能体都会轻松很多。
扩展思考: 如果想把 Grid World 变成 3D 的,或者加入动态障碍物,你会在哪些地方改代码?