4. 蒙特卡洛方法:从随机采样到智能决策
蒙特卡洛方法,名字听着挺唬人,其实说白了就是——用大量随机采样来估计真实值。我在做机器人轨迹规划时,经常遇到环境模型不明确的情况,这时候蒙特卡洛就成了我的救命稻草。
你想想看,强化学习的核心问题是什么?是评估一个策略好不好,以及怎么找到更好的策略。蒙特卡洛方法恰好能解决这两个问题,而且思路非常直观。
核心思想:通过完整episode的采样,用平均回报来估计状态值或动作值。不需要环境模型,完全靠经验。
4.1 蒙特卡洛预测:如何评估一个策略?
预测问题,就是给定一个策略π,求它的状态值函数Vπ(s)或动作值函数Qπ(s, a)。
蒙特卡洛的做法很简单:跑很多次完整的episode,记录每个状态后的累计回报,然后取平均。
举个例子,假设你在迷宫里走,从起点到终点算一次episode。你记录了每次经过某个路口后,最终获得的奖励总和。跑100次后,把这些总和平均一下,就是这个路口的价值估计。
我习惯用首次访问型MC和每次访问型MC来区分实现方式:
- 首次访问型:每个episode中,只记录状态第一次出现时的回报
- 每次访问型:每个episode中,记录状态每次出现时的回报
理论上两者都会收敛到真实值,但首次访问型在数学上更容易分析。我在项目中一般用首次访问型,因为它的方差更小。
# 首次访问型MC预测伪代码
def mc_prediction(env, policy, num_episodes, gamma=0.9):
returns = {} # 存储每个状态的回报列表
V = {} # 状态值函数
for _ in range(num_episodes):
episode = generate_episode(env, policy)
G = 0
visited_states = set()
# 从后往前遍历
for t in range(len(episode)-1, -1, -1):
state, reward = episode[t]
G = gamma * G + reward
# 首次访问检查
if state not in visited_states:
visited_states.add(state)
returns[state].append(G)
V[state] = np.mean(returns[state])
return V
我的经验:实际应用中,episode数量至少需要1000以上才能得到稳定估计。如果状态空间很大,可以考虑用函数近似来替代查表。
4.2 蒙特卡洛控制:从评估到优化
光会评估还不够,我们得找到最优策略。蒙特卡洛控制的核心是广义策略迭代:
- 策略评估:用MC方法估计当前策略的Q值
- 策略改进:基于Q值贪心地选择动作
这里有个关键问题:探索与利用的平衡。如果一直贪心,你可能永远发现不了更好的路径。
我早期做迷宫导航时就踩过这个坑。机器人只走已知的短路径,结果错过了另一条更优的路线。后来我用了ε-贪心策略,才解决了这个问题。
# ε-贪心策略
def epsilon_greedy(Q, state, epsilon):
if np.random.random() < epsilon:
return np.random.choice(env.action_space.n) # 探索
else:
return np.argmax(Q[state]) # 利用
我曾经犯过的错:ε值设得太大会导致策略永远无法收敛,设得太小又探索不够。我建议从0.5开始,随着训练逐渐衰减到0.01左右。
4.3 探索性初始化:一个简单但有效的技巧
探索性初始化,说白了就是让每个episode从不同的状态-动作对开始。这样能保证所有状态都被访问到,避免某些区域永远被忽略。
为什么需要这个?你想想看,如果机器人每次都在同一个起点出发,它可能永远不知道地图另一侧有什么。探索性初始化强制它从不同位置开始,相当于给了它一张全局地图。
具体做法:
- 每个episode开始时,随机选择一个状态作为起点
- 随机选择一个动作作为第一步
- 然后按照当前策略继续执行
但要注意,探索性初始化只适用于可重置的环境。在真实机器人上,你不能每次都把它搬到不同位置重新开始。这时候就需要用在线探索的方法了。
4.4 在迷宫导航中的应用
好了,我们把上面这些串起来,看看怎么用蒙特卡洛方法解决迷宫导航问题。
迷宫导航的设定:
- 状态:机器人在迷宫中的坐标 (x, y)
- 动作:上、下、左、右
- 奖励:到达终点+10,撞墙-1,每步-0.1
- 目标:找到从起点到终点的最短路径
我实际做过一个5x5的迷宫项目,用蒙特卡洛控制训练了5000个episode。下面是核心流程:
def mc_control_maze(env, num_episodes, gamma=0.9, epsilon=0.1):
Q = defaultdict(lambda: np.zeros(env.action_space.n))
policy = defaultdict(lambda: np.random.choice(env.action_space.n))
for episode in range(num_episodes):
# 探索性初始化
state = env.reset_random()
episode_data = []
# 生成一个episode
done = False
while not done:
action = epsilon_greedy(Q[state], epsilon)
next_state, reward, done = env.step(action)
episode_data.append((state, action, reward))
state = next_state
# 反向计算回报并更新Q值
G = 0
visited = set()
for t in range(len(episode_data)-1, -1, -1):
state, action, reward = episode_data[t]
G = gamma * G + reward
if (state, action) not in visited:
visited.add((state, action))
Q[state][action] = update_mc(Q[state][action], G, t)
# 策略改进
for state in Q:
policy[state] = np.argmax(Q[state])
return policy, Q
效果对比:经过训练后,机器人从起点到终点的平均步数从最初的50步降到了12步,接近最优解。而且策略对噪声有很好的鲁棒性。
4.5 蒙特卡洛方法的优缺点
| 优点 | 缺点 |
|---|---|
| 不需要环境模型 | 只能用于episodic任务 |
| 实现简单直观 | 方差较大,需要大量采样 |
| 对状态空间无偏估计 | 无法处理连续状态空间 |
| 容易并行化 | 探索效率较低 |
我个人觉得,蒙特卡洛方法最适合小规模、离散状态空间的问题。如果你的任务有明确的终止条件(比如迷宫有终点),而且状态数不超过几千个,那蒙特卡洛是个好选择。
但如果状态空间很大,或者任务没有明确的episode边界,那就得考虑时序差分学习了。嗯,那是下一章的内容了。
避坑指南:我曾经在连续控制任务上硬套蒙特卡洛方法,结果跑了三天三夜都没收敛。后来才意识到,蒙特卡洛只适合离散episode的任务。选对方法比调参更重要。
这张图把蒙特卡洛方法的三个核心模块串起来了。预测是基础,控制是目标,探索性初始化是保障。三者结合,就能在迷宫导航这类任务中发挥作用。
最后说一句,蒙特卡洛方法虽然简单,但它是理解更复杂强化学习算法(比如Q-learning、SARSA)的基石。把这块啃透了,后面的路就好走了。