4. 蒙特卡洛方法:从随机采样中学习
蒙特卡洛方法,名字听着挺唬人,其实说白了就是——用大量随机采样来估计真实值。我在做机器人控制之前,最早接触蒙特卡洛是在金融领域做期权定价。后来转到强化学习,发现这方法简直太适合处理那些「不知道环境模型」的问题了。
你想想看,现实中的机器人控制,有几个能给你精确的转移概率?几乎没有。蒙特卡洛方法的好处就是:不需要模型,只需要采样。你让机器人跑一遍,记录下轨迹,然后算回报,就这么简单粗暴。
核心思想: 用大量完整轨迹的回报平均值,来近似状态或状态-动作对的价值函数。
4.1 蒙特卡洛预测:估计价值函数
预测问题,就是给定一个策略 π,估计它的状态价值函数 Vπ(s) 或者动作价值函数 Qπ(s,a)。
蒙特卡洛的做法很直接:
- 用策略 π 生成完整轨迹(从初始状态到终止状态)
- 计算每个时间步的回报 Gt = Rt+1 + γRt+2 + ... + γT-t-1RT
- 对同一个状态 s 出现的所有回报取平均
嗯,这里有个关键问题:同一个状态可能在一条轨迹中出现多次。比如机器人走到一个位置,绕了一圈又回来了。那这个状态的回报怎么算?
4.2 首次访问 vs 每次访问
这就是蒙特卡洛预测的两个变种:
| 方法 | 做法 | 特点 |
|---|---|---|
| 首次访问 MC | 只取状态 s 在轨迹中第一次出现时的回报 | 偏差小,理论分析方便 |
| 每次访问 MC | 状态 s 每次出现都记录回报 | 数据利用率高,方差可能更小 |
我个人习惯用首次访问。为什么?因为我在项目中遇到过一个问题:用每次访问时,如果策略是确定性的,同一个状态反复出现,回报之间高度相关,反而让估计不稳定。首次访问虽然「浪费」了一些数据,但每个状态只贡献一次,独立性更好。
我的经验: 如果轨迹长度较短(比如几十步),用每次访问;如果轨迹很长(几百上千步),用首次访问更稳。
4.3 蒙特卡洛控制:从预测到优化
预测搞定了,接下来就是控制——找到最优策略。蒙特卡洛控制用的是广义策略迭代的框架:
- 策略评估: 用 MC 方法估计当前策略的 Q(s,a)
- 策略改进: 对每个状态,选择 Q 值最大的动作
但这里有个坑:如果某个动作从来没被选过,它的 Q 值就是未知的。你永远不知道那个没试过的动作会不会更好。这就是探索与利用的经典矛盾。
4.4 探索性初始化
解决这个问题的一个简单方法就是探索性初始化。说白了就是:每次开始新轨迹时,随机选择一个状态-动作对作为起点。
这样做的好处是:
- 保证每个状态-动作对都有机会被访问到
- 理论上可以收敛到最优策略
我曾经在一个机械臂抓取任务中用过这个方法。当时环境状态空间很大,如果不做探索性初始化,机械臂永远只会尝试它「觉得」好的动作,结果卡在局部最优里出不来。加了探索性初始化之后,收敛速度快了一倍不止。
注意: 探索性初始化要求所有状态-动作对都能被选为起点。如果状态空间是连续的,这个方法就不太现实了。这时候需要用其他探索策略,比如 ε-贪心。
4.5 Gymnasium 中的 MC 实现
光说不练假把式。咱们用 Gymnasium(也就是之前的 Gym)来写一个简单的蒙特卡洛控制。我用的是经典的 Blackjack 环境,状态空间小,非常适合演示。
import gymnasium as gym
import numpy as np
from collections import defaultdict
env = gym.make('Blackjack-v1')
def generate_episode(policy, env):
"""生成一条完整轨迹"""
episode = []
state, _ = env.reset()
done = False
while not done:
action = policy(state)
next_state, reward, done, _, _ = env.step(action)
episode.append((state, action, reward))
state = next_state
return episode
def mc_control_first_visit(env, num_episodes=500000, gamma=1.0):
"""首次访问蒙特卡洛控制"""
Q = defaultdict(lambda: np.zeros(env.action_space.n))
returns = defaultdict(list)
for episode_idx in range(num_episodes):
# 探索性初始化:随机选择初始策略
policy = lambda s: np.random.choice(env.action_space.n) if np.random.random() < 0.1 \
else np.argmax(Q[s])
episode = generate_episode(policy, env)
# 计算回报并更新 Q
G = 0
visited = set()
for t in range(len(episode)-1, -1, -1):
state, action, reward = episode[t]
G = gamma * G + reward
# 首次访问检查
if (state, action) not in visited:
visited.add((state, action))
returns[(state, action)].append(G)
Q[state][action] = np.mean(returns[(state, action)])
# 返回最优策略
optimal_policy = {s: np.argmax(Q[s]) for s in Q.keys()}
return optimal_policy, Q
# 跑一下试试
policy, Q = mc_control_first_visit(env, num_episodes=500000)
print("训练完成!策略已收敛。")
这段代码里,我用了 ε-贪心策略来做探索,同时保留了探索性初始化的思想。你可能会问:为什么不用纯探索性初始化?因为 Blackjack 的状态空间虽然有限,但有些状态在实际游戏中很少出现,纯随机起点效率太低。
关键点: 蒙特卡洛方法只适用于片段式任务(有终止状态)。如果任务是持续性的(比如机器人一直走不停),MC 就不适用了,得用时序差分方法。
知识体系总览
下面这张图是我自己画的,把蒙特卡洛方法的核心逻辑串起来了:
这张图把蒙特卡洛方法的几个核心模块串起来了。从预测到控制,再到探索性初始化,最后落地到 Gymnasium 的实现。你写代码的时候,可以照着这个结构来组织你的思路。
一个小建议: 刚开始学 MC 的时候,别急着上复杂环境。先在 Blackjack 或者 Gridworld 这种小环境上跑通,理解透了再上机器人控制。我当年就是太心急,直接上机械臂仿真,结果 debug 了三天才发现是 MC 实现的问题。
公众号:蓝海资料掘金营,微信deep3321