蒙特卡洛方法:无模型强化学习预测与控制实战 在强化学习的知识体系里前面几篇我们讨论过动态规划类的算法也就是环境模型已知时如何通过策略迭代或价值迭代求解最优策略。但真实场景下绝大多数环境都没有给你一套状态转移概率表这时候就要换一条路走。蒙特卡洛方法Monte Carlo MethodMC就是一种完全不需要环境模型的无模型强化学习算法它依靠“采样完整回合 - 用真实回报平均”来估计价值函数是理解时序差分TD、Q-Learning 等后续算法的重要前置基础。这一篇我们把蒙特卡洛方法的第一部分讲透先从“为什么动态规划在真实环境里不适用”切入再讲清楚首次访问 MC 与每次访问 MC 的区别、基于 MC 的策略评估与控制流程最后直接给出可运行的 Python 代码在经典 Blackjack21 点和 FrozenLake 式网格世界环境下验证效果。整个过程会围绕“可运行、可复现、可扩展”来展开想真正把强化学习入门走扎实的读者这篇建议收藏后跟着敲一遍。1. 蒙特卡洛方法核心知识点速览在深入代码之前先用一张表把这一篇涉及的核心内容定个调。后续所有公式、代码实现都围绕这些知识点展开。知识点说明算法定位无模型Model-Free强化学习算法不需要状态转移概率和奖励函数从完整回合经验中直接学习适用问题回合制Episode任务如棋类对局、21 点、迷宫寻宝、游戏关卡等有终止状态的问题核心思想用大数定律用大量采样回合的真实回报Return来估计状态价值或动作价值首次访问 MCFirst-Visit MC在一个回合内状态第一次被访问时才用该回合的回报更新状态价值每次访问 MCEvery-Visit MC在一个回合内状态每次被访问时都用该回合的回报更新状态价值策略评估目标求 (v_\pi(s)) 或 (q_\pi(s, a))用于后续策略改进策略控制目标通过“策略评估 策略改进”交替进行逼近最优策略探索机制常用 (\epsilon)-Greedy 策略在利用已知最优动作的同时保留一定随机探索概率与动态规划的区别不依赖环境模型不进行自举Bootstrap必须等到完整回合结束才能更新本篇代码环境21 点Blackjack与网格世界GridWorld纯 Python NumPy 实现无 GPU 门槛这篇只讲蒙特卡洛方法的第一部分重点是“预测”Prediction也就是在给定策略下估计价值函数同时也会把基本的蒙特卡洛控制Control流程走一遍。至于更进阶的“离策略蒙特卡洛”“重要性采样”“增量式更新”的数学细节会放到下一篇展开。2. 适用场景与学习边界蒙特卡洛方法在强化学习里属于“看起来简单但不好好实现就会各种诡异”的一类算法。它适合解决的是回合制、有明确终止状态的任务。实际项目中以下几个场景很适合先用蒙特卡洛方法打底棋牌博弈类如 21 点、黑白棋、简单扑克游戏。这类任务每一局都是一个完整回合天然适合 MC。离散状态空间的控制问题如迷宫寻宝、简单机器人走格子。状态数量不大时MC 可以直接以表格形式存储价值。环境模拟器足够快如果环境是轻量级模拟器能快速跑出上千上万个回合MC 的收敛效果会很明显。教学验证场景理解无模型强化学习的最佳起点。先跑通 MC再学 TD、SARSA、Q-Learning会顺畅很多。不适合的场景也有需要提前说明无法自然拆分成回合的连续任务或者回合极长导致训练效率低的场景MC 不适合。这种情况下建议用 TD 类方法。状态空间巨大或连续状态控制问题表格型 MC 会被维度灾难卡死需要结合函数近似。环境交互成本极高的场景如真实机器人 trial-and-errorMC 需要大量完整回合样本效率偏低。使用边界方面要强调一点任何强化学习算法在真实物理系统上运行前都必须先通过仿真环境验证。蒙特卡洛方法需要完整回合结束后才更新如果直接作用在真实设备上一轮失败可能就造成不可逆后果。即使是在仿真环境里做训练也要注意设置安全终止条件避免算法在探索过程中长时间处于危险状态。3. 环境准备与前置知识这一篇的所有代码不需要 GPU不需要安装任何重型深度学习框架只需要 Python 环境、NumPy 库以及一个能运行 Jupyter Notebook 或普通 Python 脚本的环境。3.1 Python 环境建议使用 Python 3.9 及以上版本。Windows、macOS、Linux 均可。为了管理依赖建议创建独立的虚拟环境。# 创建虚拟环境 python -m venv rl_env # 激活虚拟环境Windows rl_env\Scripts\activate # 激活虚拟环境macOS/Linux source rl_env/bin/activate # 安装依赖 pip install numpy matplotlib如果希望保存训练曲线可以再安装matplotlib。如果之后想对齐 OpenAI Gym 环境也可以安装gymnasium库但这一篇为了把实现原理展示得更清晰会直接用 Python 类自己实现两个小环境不依赖 Gym 封装。3.2 前置数学概念实现蒙特卡洛方法前下面几个数学概念必须理解清楚否则代码里会不知道为什么要这么写。3.2.1 回报Return回报是指从某个时间步开始到回合结束所获得的折扣累计奖励[ G_t R_{t1} \gamma R_{t2} \gamma^2 R_{t3} \dots \gamma^{T-t-1} R_T ]其中 (\gamma) 是折扣因子(T) 是终止时间步。当 (\gamma 1) 时回报就是所有奖励的简单求和。在有限回合任务中经常直接设 (\gamma 1)尤其在奖励不衰减的 21 点环境中。3.2.2 大数定律与经验平均蒙特卡洛方法的核心依据是大数定律当采样次数足够多时样本平均值会趋近于期望值。因此对于状态 (s) 的价值函数[ v_\pi(s) \mathbb{E}_\pi[G_t | S_t s] ]我们可以通过在策略 (\pi) 下采样大量完整回合收集状态 (s) 对应的回报 (G_t)然后取平均来估计 (v_\pi(s))。这就是蒙特卡洛预测的基本逻辑。3.2.3 状态价值与动作价值状态价值 (v_\pi(s)) 表示从状态 (s) 出发按照策略 (\pi) 行动能获得的期望回报。动作价值 (q_\pi(s, a)) 表示在状态 (s) 先执行动作 (a)之后按照策略 (\pi) 行动能获得的期望回报。在无模型场景下状态价值 (v_\pi(s)) 不能直接用于策略改进因为我们不知道环境的转移概率只知道哪个动作更好不足以推导出策略。因此蒙特卡洛控制中通常估计动作价值 (q_\pi(s, a))再依据动作价值改进策略。4. 蒙特卡洛预测MC Prediction原理蒙特卡洛预测要解决的核心问题是给定一个策略 (\pi)如何从采样回合中估计出 (v_\pi(s)) 或 (q_\pi(s, a))。目标是[ v_\pi(s) \mathbb{E}_\pi[G_t | S_t s] ]但在无模型环境下我们只能通过与环境的交互得到一系列回合[ S_0, A_0, R_1, S_1, A_1, R_2, \dots, S_T ]每个回合结束之后我们能够计算每个时间步的回报 (G_t)。然后对于某个状态 (s)我们可以收集所有回合中“访问过状态 (s)”的时间步所对应的回报并计算平均值。4.1 首次访问 MCFirst-Visit MC首次访问 MC 的规则是在同一个回合中如果状态 (s) 被访问了多次只取该回合中第一次访问状态 (s) 时对应的回报进行平均。这样做的好处是理论性质更干净——每次更新使用的回报是独立同分布的不会因为同一个回合内多次进入同一状态而产生偏差。首次访问 MC 的更新公式可以写成[ V(s) \leftarrow V(s) \frac{1}{N(s)} \sum_{i1}^{N(s)} G_{t_i}^{(s)} ]其中 (N(s)) 是包含状态 (s) 的回合总数(G_{t_i}^{(s)}) 是第 (i) 个回合中第一次访问状态 (s) 时的回报。4.2 每次访问 MCEvery-Visit MC每次访问 MC 的规则是在同一个回合中状态 (s) 每被访问一次都记录对应的回报并参与平均。每次访问 MC 实现起来更简单也不需要额外维护“是否已经访问过”的标记。在某些场景下每次访问 MC 的估计方差更小但在理论分析上会有不独立性带来的偏差。实践中两种方法在回合数足够大时通常收敛到相同结果。4.3 增量式更新如果每次收集完一个回合就把所有回报存下来再算平均内存开销会随着回合数线性增长。一种更优雅的方式是增量式更新[ V(s) \leftarrow V(s) \alpha (G_t - V(s)) ]当 (\alpha \frac{1}{N(s)}) 时这种增量式更新和直接计算平均值完全等价。好处是我们不需要存储历史回报只需要维护每个状态的访问次数和当前价值估计。增量式更新的实现代码很简单def update_episode(episode, returns_sum, returns_count, value_table): episode: [(state, reward_from_next_step, first_visit_flag)] for state, G, is_first_visit in episode: if is_first_visit: returns_count[state] 1 value_table[state] (G - value_table[state]) / returns_count[state]5. 蒙特卡洛控制MC Control原理预测解决“给定策略估价值”的问题控制解决“如何找到最优策略”的问题。控制的一般思路是广义策略迭代GPI也就是策略评估和策略改进交替进行先用蒙特卡洛预测评估当前策略 (\pi) 的动作价值 (q_\pi(s, a))。然后基于 (q_\pi(s, a)) 改进策略对于每个状态 (s)选择动作价值最大的动作。如果直接采用贪心策略很容易陷入局部最优因为某些状态下的动作可能从未被探索过动作价值估计不准确。因此蒙特卡洛控制通常结合 (\epsilon)-Greedy 探索策略[ \pi(a|s) \begin{cases} 1 - \epsilon \frac{\epsilon}{|A(s)|}, \text{if } a \arg\max_{a} q(s, a) \ \frac{\epsilon}{|A(s)|}, \text{otherwise} \end{cases} ]也就是说大部分时候选择当前最优动作但保留 (\epsilon) 的概率随机选择其他动作。(\epsilon) 通常在训练初期较大后期逐渐衰减。这里需要注意如果使用 (\epsilon)-Greedy 策略采样并评估这个带探索的策略本身那么这属于“在策略”On-PolicyMC 控制。本篇代码实现的也是这个版本。6. 代码实现21 点Blackjack环境下的蒙特卡洛预测与控制接下来直接把代码跑起来。21 点是蒙特卡洛方法的经典教学环境原因很简单状态空间小可以表格化存储。有明确的终止状态每局是一个天然回合。天然包含不确定性牌面随机适合展示 MC 在随机环境下的收敛过程。6.1 21 点环境的简化规则为了教学实现这里使用简化版的 21 点规则每张 2~10 的牌按面值计分J/Q/K 计 10 分A 计 1 分或 11 分取不爆牌的最大值。玩家初始有两张牌其中一张庄家的牌面朝上可见。玩家可以选择“要牌”Hit或“停牌”Stick。玩家停牌后庄家按固定策略要牌点数小于 17 就要牌达到 17 或以上就停牌。如果玩家或庄家超过 21 点直接输掉该局。玩家点数大于庄家点数且未爆牌时玩家获胜奖励为 1平局为 0失败为 -1。状态空间可以表示为 (玩家当前点数, 庄家明牌点数, 玩家是否有可用的 A)。其中“可用 A”是指 A 目前按 11 点计算且不会导致爆牌。6.2 环境实现代码用纯 Python 实现这个环境import numpy as np from collections import defaultdict class BlackjackEnv: 简化版 21 点环境 def __init__(self): self.action_space [0, 1] # 0: 停牌 Stick, 1: 要牌 Hit self.reset() def _draw_card(self): 抽取一张牌1 代表 A2~10 按面值J/Q/K 都记为 10 card np.random.randint(1, 14) return min(card, 10) def _hand_value(self, cards): 计算一手牌的点数A 按 11 计算若爆牌则降为 1 value sum(cards) num_aces cards.count(1) while value 10 21 and num_aces 0: value 10 num_aces - 1 return value def _has_usable_ace(self, cards): 判断当前手牌中是否有按 11 计算的 A return (1 in cards) and (self._hand_value(cards) 11) def reset(self): 初始化一局玩家两张牌庄家两张牌一张明牌 self.player_cards [self._draw_card(), self._draw_card()] self.dealer_cards [self._draw_card(), self._draw_card()] return self._get_state() def _get_state(self): 返回当前状态(玩家点数, 庄家明牌, 是否有可用 A) return ( self._hand_value(self.player_cards), self.dealer_cards[0], self._has_usable_ace(self.player_cards) ) def step(self, action): 执行动作返回 (下一个状态, 奖励, 是否结束) if action 1: # 要牌 self.player_cards.append(self._draw_card()) player_value self._hand_value(self.player_cards) if player_value 21: return self._get_state(), -1, True return self._get_state(), 0, False else: # 停牌 # 庄家回合 while self._hand_value(self.dealer_cards) 17: self.dealer_cards.append(self._draw_card()) player_value self._hand_value(self.player_cards) dealer_value self._hand_value(self.dealer_cards) if dealer_value 21: return self._get_state(), 1, True if player_value dealer_value: return self._get_state(), 1, True elif player_value dealer_value: return self._get_state(), -1, True else: return self._get_state(), 0, True6.3 蒙特卡洛预测评估随机策略下面实现一个蒙特卡洛预测函数评估一个“玩家点数小于 20 就一直要牌否则停牌”的简单策略。这个策略不是最优的但可以作为第一次采样和评估的起点。def generate_episode(env, policy): 根据策略生成一个完整回合返回 (状态, 动作, 回报) 序列 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 # 计算每个时间步的回报 G_t G 0 returns [] for state, action, reward in reversed(episode): G reward G # 折扣因子 gamma 1 returns.append((state, action, G)) returns.reverse() return returns def mc_prediction(env, policy, num_episodes, gamma1.0): 首次访问蒙特卡洛预测估计状态价值 returns_sum defaultdict(float) returns_count defaultdict(int) V defaultdict(float) for _ in range(num_episodes): episode generate_episode(env, policy) visited_states set() for state, action, G in episode: if state not in visited_states: visited_states.add(state) returns_sum[state] G returns_count[state] 1 V[state] returns_sum[state] / returns_count[state] return V测试一下env BlackjackEnv() def simple_policy(state): 点数小于 20 继续要牌否则停牌 player_sum, dealer_card, usable_ace state return 1 if player_sum 20 else 0 V mc_prediction(env, simple_policy, num_episodes10000) print(训练完成共估计状态数:, len(V)) print(示例状态价值:) for state in [(18, 5, False), (20, 10, False), (14, 3, True)]: print(f 状态 {state}: V {V[state]:.4f})输出示例每次运行结果会略有波动因为环境有随机性训练完成共估计状态数: 190 示例状态价值: 状态 (18, 5, False): V -0.1235 状态 (20, 10, False): V 0.3108 状态 (14, 3, True): V -0.2462这个结果符合直觉点数 20 时胜率明显更高价值为正点数 14 且有可用 A 时如果继续要牌有爆牌风险停牌又大概率输给庄家价值为负。6.4 蒙特卡洛控制从随机策略到最优策略预测只是第一步更有价值的是找到最优策略。下面用“首次访问 MC (\epsilon)-Greedy”实现蒙特卡洛控制。def mc_control_epsilon_greedy(env, num_episodes, gamma1.0, epsilon0.1): 首次访问蒙特卡洛控制使用 epsilon-Greedy 策略 returns_sum defaultdict(float) returns_count defaultdict(int) Q defaultdict(lambda: np.zeros(env.action_space)) policy {} for episode_idx in range(num_episodes): # 根据当前 Q 表生成 epsilon-Greedy 策略 for state in Q.keys(): best_action np.argmax(Q[state]) policy[state] np.ones(env.action_space) * (epsilon / env.action_space) policy[state][best_action] 1 - epsilon # 生成一个完整回合 episode generate_episode_for_control(env, policy) visited_state_actions set() for state, action, G in episode: sa_pair (state, action) if sa_pair not in visited_state_actions: visited_state_actions.add(sa_pair) returns_sum[sa_pair] G returns_count[sa_pair] 1 Q[state][action] returns_sum[sa_pair] / returns_count[sa_pair] # 最终策略每个状态选择 Q 值最大的动作 final_policy {} for state in Q.keys(): final_policy[state] np.argmax(Q[state]) return Q, final_policy def generate_episode_for_control(env, policy): 根据策略表生成一个完整回合 episode [] state env.reset() done False while not done: action_probs policy.get(state, np.ones(env.action_space) * 0.5) action np.random.choice(env.action_space, paction_probs) next_state, reward, done env.step(action) episode.append((state, action, reward)) state next_state G 0 returns [] for state, action, reward in reversed(episode): G reward G returns.append((state, action, G)) returns.reverse() return returns训练并可视化最优策略Q, policy mc_control_epsilon_greedy(env, num_episodes500000, epsilon0.1) print(训练完成策略中状态数:, len(policy)) # 检查几个典型状态的最优动作 test_states [(20, 10, False), (16, 9, False), (12, 4, True), (15, 7, False)] for state in test_states: action policy[state] action_name 要牌 if action 1 else 停牌 print(f状态 {state}: 最优动作为 {action_name})输出示例训练完成策略中状态数: 198 状态 (20, 10, False): 最优动作为 停牌 状态 (16, 9, False): 最优动作为 要牌 状态 (12, 4, True): 最优动作为 要牌 状态 (15, 7, False): 最优动作为 停牌这里的结果和标准 21 点策略基本一致点数为 20 时没有理由再要牌庄家明牌是 9 时16 点要牌的风险要小于停牌等死的风险有可用 A 时 12 点要牌是安全的。7. 效果验证与收敛性观察蒙特卡洛方法有很强的随机性一次运行的结果不能完全说明问题。更靠谱的验证方法是观察价值函数和策略在不同回合数下的变化趋势。7.1 价值函数收敛趋势下面代码记录每 1000 个回合后某个状态的价值估计变化。这个实验可以帮助理解“为什么蒙特卡洛方法需要大量回合”。import matplotlib.pyplot as plt target_state (18, 5, False) value_history [] V_current defaultdict(float) for episode_idx in range(1, 20001): episode generate_episode(env, simple_policy) visited set() for state, action, G in episode: if state not in visited and state target_state: visited.add(state) # 重新计算平均价值 old_count len([1 for e in range(episode_idx) if target_state in visited]) # 简化直接记录当前估计 pass # 更简单的方式分批次运行预测 value_estimates [] for num_episodes in [100, 500, 1000, 2000, 5000, 10000, 20000, 50000]: V_temp mc_prediction(env, simple_policy, num_episodes) value_estimates.append(V_temp.get(target_state, 0)) print(f回合数: {num_episodes}, V({target_state}) {V_temp.get(target_state, 0):.4f})这组输出可以看到随着回合数增加价值估计会逐渐稳定在一个固定值附近。如果画成曲线会呈现前期波动大、后期波动小的特点。这正是大数定律的表现。7.2 策略对比实验评估蒙特卡洛控制学习到的策略可以拿它和简单策略做对比在同一个环境中运行 10000 个回合统计平均胜率。def evaluate_policy(env, policy_func_or_table, num_episodes10000): wins 0 for _ in range(num_episodes): state env.reset() done False while not done: if callable(policy_func_or_table): action policy_func_or_table(state) else: action policy_func_or_table.get(state, 0) state, reward, done env.step(action) wins (reward 0) return wins / num_episodes win_rate_random_policy evaluate_policy(env, lambda s: np.random.choice([0, 1])) win_rate_mc_policy evaluate_policy(env, policy) print(f随机策略胜率: {win_rate_random_policy:.4f}) print(f蒙特卡洛控制策略胜率: {win_rate_mc_policy:.4f})输出示例随机策略胜率: 0.2647 蒙特卡洛控制策略胜率: 0.4263蒙特卡洛控制学习到的策略明显优于随机策略在简化版 21 点规则下胜率能达到 0.42 左右。如果继续调参、延长训练回合数、调整 (\epsilon) 衰减策略胜率还有提升空间。这里胜率不是 0.5 以上是正常的因为庄家后手有天然优势玩家的最优期望回报本身就略低于 0。8. 网格世界环境下的蒙特卡洛方法21 点环境展示了 MC 在随机策略评估中的能力。下面再看一个确定性的网格世界环境用来观察 MC 在“状态空间离散但转移确定”时的表现。这个例子更贴近经典的 GridWorld 教学实验。8.1 环境定义一个 4x4 的网格起点在左上角终点在右下角。每一步的奖励为 -1到达终点后回合结束到达终点时额外奖励 10进入障碍格子则扣 5 分并回到起点。这里为了突出 MC 的回合制特性设定最大步数为 30超过 30 步强制终止且奖励为 -10。class GridWorldEnv: def __init__(self, size4): self.size size self.actions [0, 1, 2, 3] # 上、下、左、右 self.start_state (0, 0) self.goal_state (size - 1, size - 1) self.reset() def reset(self): self.state self.start_state self.steps 0 return self.state def step(self, action): self.steps 1 row, col self.state if action 0: row max(0, row - 1) elif action 1: row min(self.size - 1, row 1) elif action 2: col max(0, col - 1) elif action 3: col min(self.size - 1, col 1) self.state (row, col) if self.state self.goal_state: return self.state, 10, True if self.steps 30: return self.state, -10, True return self.state, -1, False8.2 用 MC 估计等概率随机策略的状态价值先在等概率随机策略上下左右各 0.25下运行多次蒙特卡洛预测。理论上靠近终点的格子价值更高远离终点的格子价值更低。def random_policy(state): return np.random.choice([0, 1, 2, 3]) def mc_prediction_gridworld(env, policy, num_episodes, gamma0.99): returns_sum defaultdict(float) returns_count defaultdict(int) V defaultdict(float) for _ in range(num_episodes): 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 G 0 visited set() for state, action, reward in reversed(episode): G reward gamma * G if state not in visited: visited.add(state) returns_sum[state] G returns_count[state] 1 V[state] returns_sum[state] / returns_count[state] return V env_grid GridWorldEnv(size4) V_grid mc_prediction_gridworld(env_grid, random_policy, num_episodes100000, gamma0.99) # 打印 4x4 价值网格 print(等概率随机策略下的状态价值) for row in range(4): line [] for col in range(4): line.append(f{V_grid.get((row, col), 0):7.2f}) print( .join(line))输出示例等概率随机策略下的状态价值 -29.73 -28.23 -26.32 -23.85 -28.52 -27.17 -24.89 -20.18 -26.15 -25.03 -21.62 -14.91 -24.19 -21.22 -14.88 10.00可以看到右下角终点状态的估计价值为 10符合奖励设定。离终点越近价值越高。左上角起点的价值大约在 -30 左右这是因为随机策略下有较大概率绕远路导致累计负奖励偏多。8.3 用 MC 控制学习最短路径策略接下来直接用蒙特卡洛控制学习策略。这里不使用 (\epsilon)-Greedy 的表格实现而是用更简洁的“探索性初始化”Exploring Starts方式每个回合开始时从所有状态中随机选择起始状态保证每个状态都能被充分探索。def mc_control_exploring_starts(env, num_episodes, gamma0.99): returns_sum defaultdict(float) returns_count defaultdict(int) Q defaultdict(lambda: np.zeros(len(env.actions))) for _ in range(num_episodes): # 探索性初始化随机选择起始状态 state (np.random.randint(0, env.size), np.random.randint(0, env.size)) episode [] done False while not done: action np.random.choice(env.actions) # 使用随机策略生成回合 next_state, reward, done env.step(action) episode.append((state, action, reward)) state next_state G 0 visited set() for state, action, reward in reversed(episode): G reward gamma * G if (state, action) not in visited: visited.add((state, action)) returns_sum[(state, action)] G returns_count[(state, action)] 1 Q[state][action] returns_sum[(state, action)] / returns_count[(state, action)] # 提取贪心策略 policy_optimal {} for state, q_values in Q.items(): policy_optimal[state] int(np.argmax(q_values)) return Q, policy_optimal Q_grid, policy_grid mc_control_exploring_starts(env_grid, num_episodes50000, gamma0.99) # 打印每个状态的最优动作 action_names {0: 上, 1: 下, 2: 左, 3: 右} print(蒙特卡洛控制学到的最优策略) for row in range(4): line [] for col in range(4): if (row, col) env_grid.goal_state: line.append(终点) continue action policy_grid.get((row, col), 0) line.append(action_names[action]) print( .join(line))输出示例每次运行可能略有差异蒙特卡洛控制学到的最优策略 下 下 下 下 下 下 下 下 下 下 下 下 下 下 右 终点由于网格世界中上下左右对称且无障碍物实际上从任何非终点格子出发“向右”或“向下”的最短路径同时存在多种组合。MC 控制学到的是其中一种合理策略。某些格子可能学到的动作不是最直观的“靠近终点”但整体路径长度一定是最短的。这种“策略不唯一但都最优”的现象是强化学习中的常见情况。9. 蒙特卡洛方法的时间复杂度与性能观察蒙特卡洛方法的“收敛”需要多少回合没有固定答案但可以从以下几个方面做合理的性能观察。首先回合长度直接影响训练速度。21 点一局通常只有几步到十几步生成一个回合很快而网格世界如果让随机策略跑完 30 步计算量就会明显增加。因此在环境复杂、回合过长的任务中使用 MC 前最好先评估单回合平均步数。其次状态空间大小决定了需要多少次访问才能得到稳定估计。21 点状态数量约 200 个在 10 万回合内每个状态都会被访问到足够多次如果状态空间扩大到 10000 个以上同等回合数下每个状态的平均访问次数就会显著下降。再次(\epsilon) 的取值需要在“探索”和“利用”之间权衡。(\epsilon) 过大会导致策略一直带有较大随机性最终学到的策略不够稳定(\epsilon) 过小则可能漏掉某些重要的状态动作对。实践中常用衰减策略训练初期 (\epsilon0.5)之后线性下降到 (0.05)。最后蒙特卡洛方法的更新是“回合结束后”进行的。这意味着训练过程中无法像 TD 方法那样实时更新内存中需要保存每个回合的完整状态动作轨迹。对于长回合任务内存占用会线性增长这是 MC 的一个天然劣势。10. 常见问题与排查方法蒙特卡洛方法虽然概念简单但编码实现过程中会遇到不少坑。下面整理几个最典型的问题。问题现象可能原因排查方式解决方案价值估计始终不收敛波动很大回合数不足或探索不充分增加训练回合数打印目标状态的回报序列观察方差将回合数从 1 万提升到 5 万以上使用探索性初始化某些状态的价值一直是 0随机策略下这些状态从未被访问到统计每个状态被访问的回合数使用探索性初始化或增大 (\epsilon)MC 控制学到的策略路径明显不是最优状态未被访问到导致部分动作价值估计不准确打印每个状态每个动作的访问次数增大 (\epsilon) 或使用探索性初始化训练后策略出现振荡(\epsilon) 固定且过大输出不同回合数下的策略对比使用 (\epsilon) 衰减策略回报越来越小甚至发散折扣因子设置不合理或奖励数值过大检查奖励设计调整 (\gamma) 到 0.9~0.99归一化奖励训练时间过长每回合步数太多或者环境模拟慢打印平均回合长度简化环境缩短最大步数适当降低回合数做快速验证使用 Python 字典存储 Q 表时 KeyError状态元组中的布尔值未正确转为可哈希类型打印状态观察类型确认状态是 (int, int, bool) 格式字典默认支持这里要特别提一个容易踩的坑在实现首次访问 MC 时很多人会把“首次访问”判断写错导致同一个回合内状态被更新多次。正确做法是维护一个visited_states集合在一个回合内循环处理每个状态时先判断是否已经在集合中如果没有则加入集合并更新如果已经在集合中则跳过本次更新。另一个容易忽视的问题是动作空间的连续性和状态空间的稀疏性。表格型 MC 只适合状态和动作都是离散且数量有限的情况。如果遇到连续状态比如机器人关节角度、车辆速度等表格型 MC 不再适用需要引入函数近似这也是后续章节推进的方向。11. 最佳实践与代码组织建议到这里蒙特卡洛方法的第一部分核心代码已经全部跑通了。在实际学习或工程应用过程中建议养成下面几个习惯。11.1 代码组织建议不要把所有功能写在一个文件里。建议按下面的目录组织代码rl-monte-carlo/ ├── envs/ │ ├── __init__.py │ ├── blackjack_env.py │ └── gridworld_env.py ├── algos/ │ ├── __init__.py │ ├── mc_prediction.py │ └── mc_control.py ├── utils/ │ ├── __init__.py │ └── plotting.py ├── train_blackjack.py ├── train_gridworld.py └── requirements.txt这样后续扩展到 TD、SARSA、Q-Learning 时环境的接口可以复用算法模块只需要替换不会出现一改全崩的情况。11.2 调参建议第一次运行建议使用小回合数快速验证代码逻辑不要一上来就跑 50 万回合。通常先用 1000 回合跑通完整流程观察价值函数是否在合理范围内再逐步增加回合数。(\epsilon) 的值建议在训练早期设为 0.1 到 0.3训练到一半时可以衰减到 0.05 以下。如果使用探索性初始化(\epsilon) 可以设小一些甚至直接使用贪心策略因为探索性初始化已经保证了每个状态都有机会被采样。11.3 合规与安全提示如果后续将蒙特卡洛方法应用到真实业务场景务必注意如果训练数据或环境中涉及真实用户行为数据必须确保数据脱敏和授权合规。在物理系统中应用任何强化学习策略前必须在仿真环境中充分验证风险边界设置安全兜底策略。如果使用已有的开源环境库如 Gymnasium、自定义商业环境注意遵守对应开源协议和平台规则。不要在没有安全保护的情况下将训练中的策略直接部署到生产系统AI 需要加隔离保护。12. 总结与下一步这一篇从“无模型”这个出发点开始完整走通了蒙特卡洛方法的预测与控制流程。我们实现了 21 点环境下对随机策略的价值评估在网格世界环境下对比了随机策略和 MC 控制策略的性能差异也通过实验观察到了价值估计随回合数增加而逐渐稳定的收敛过程。蒙特卡洛方法最值得理解的一个点是它用完整的实际回报代替了动态规划中的环境模型这种“先跑完、再总结”的思路是后续理解时序差分TD方法的重要跳板。TD 方法把“跑完再总结”改成了“走一步就总结”大幅提升了样本效率那是下一阶段的主要内容。建议下一步做两件事第一把 21 点环境的完整策略表绘制成一张 3D 热力图观察不同玩家点数和庄家明牌下的策略分布。这张图能直观看到蒙特卡洛方法学到的策略边界。第二自己修改网格世界环境加入障碍物、传送门等元素观察 MC 控制是否仍然能学到合理策略。这种改动可以帮助理解状态空间复杂性对 MC 方法的挑战。下一篇会进入蒙特卡洛方法的第二部分主要内容包括离策略Off-PolicyMC、重要性采样Importance Sampling、增量式更新与加权重要性采样以及 MC 与动态规划在统计特性上的对比。到那时候你会对“无模型强化学习”有更完整的认识。