4. 蒙特卡洛方法:从随机采样中学习

蒙特卡洛方法,名字听着挺唬人,其实说白了就是——用大量随机采样来估计真实值。我在做机器人控制之前,最早接触蒙特卡洛是在金融领域做期权定价。后来转到强化学习,发现这方法简直太适合处理那些「不知道环境模型」的问题了。

你想想看,现实中的机器人控制,有几个能给你精确的转移概率?几乎没有。蒙特卡洛方法的好处就是:不需要模型,只需要采样。你让机器人跑一遍,记录下轨迹,然后算回报,就这么简单粗暴。

核心思想: 用大量完整轨迹的回报平均值,来近似状态或状态-动作对的价值函数。

4.1 蒙特卡洛预测:估计价值函数

预测问题,就是给定一个策略 π,估计它的状态价值函数 Vπ(s) 或者动作价值函数 Qπ(s,a)。

蒙特卡洛的做法很直接:

  1. 用策略 π 生成完整轨迹(从初始状态到终止状态)
  2. 计算每个时间步的回报 Gt = Rt+1 + γRt+2 + ... + γT-t-1RT
  3. 对同一个状态 s 出现的所有回报取平均

嗯,这里有个关键问题:同一个状态可能在一条轨迹中出现多次。比如机器人走到一个位置,绕了一圈又回来了。那这个状态的回报怎么算?

4.2 首次访问 vs 每次访问

这就是蒙特卡洛预测的两个变种:

方法 做法 特点
首次访问 MC 只取状态 s 在轨迹中第一次出现时的回报 偏差小,理论分析方便
每次访问 MC 状态 s 每次出现都记录回报 数据利用率高,方差可能更小

我个人习惯用首次访问。为什么?因为我在项目中遇到过一个问题:用每次访问时,如果策略是确定性的,同一个状态反复出现,回报之间高度相关,反而让估计不稳定。首次访问虽然「浪费」了一些数据,但每个状态只贡献一次,独立性更好。

我的经验: 如果轨迹长度较短(比如几十步),用每次访问;如果轨迹很长(几百上千步),用首次访问更稳。

4.3 蒙特卡洛控制:从预测到优化

预测搞定了,接下来就是控制——找到最优策略。蒙特卡洛控制用的是广义策略迭代的框架:

  1. 策略评估: 用 MC 方法估计当前策略的 Q(s,a)
  2. 策略改进: 对每个状态,选择 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 就不适用了,得用时序差分方法。

知识体系总览

下面这张图是我自己画的,把蒙特卡洛方法的核心逻辑串起来了:

蒙特卡洛方法知识体系 蒙特卡洛方法 MC 预测 MC 控制 首次访问 每次访问 策略评估 策略改进 探索性初始化 Gymnasium 中的 MC 实现 核心逻辑:采样 → 回报计算 → 平均 → 策略改进 → 迭代

这张图把蒙特卡洛方法的几个核心模块串起来了。从预测到控制,再到探索性初始化,最后落地到 Gymnasium 的实现。你写代码的时候,可以照着这个结构来组织你的思路。

一个小建议: 刚开始学 MC 的时候,别急着上复杂环境。先在 Blackjack 或者 Gridworld 这种小环境上跑通,理解透了再上机器人控制。我当年就是太心急,直接上机械臂仿真,结果 debug 了三天才发现是 MC 实现的问题。


公众号:蓝海资料掘金营,微信deep3321