如果你正在学习强化学习,大概率已经接触过“基于模型的动态规划”——状态转移概率已知,价值函数可以用贝尔曼方程直接求解。但真实项目里,这种理想条件几乎不存在。机器人的关节摩擦无法精确建模,游戏环境的规则可能只对玩家隐藏一部分,交易市场的状态转移更是无法用一张概率表描述。这时候,你需要换一种思路:不再试图推导环境的完整模型,而是直接与环境交互、收集样本,从样本中估计价值。
蒙特卡洛方法(Monte Carlo Method)就是这种“无模型”(model-free)思路的第一块基石。它名字听起来很数学,核心思想却非常朴素:用大量随机采样的平均值来逼近真实期望。放到强化学习里,就是让智能体完整地跑完若干局游戏,记录每局从某个状态出发最终获得了多少回报,然后把这些回报取平均,作为该状态的价值估计。
这篇笔记是“2026新版强化学习从入门到精通”系列的第17篇,正式进入蒙特卡洛方法。文章会讲清楚三件事:第一,为什么动态规划之后必须学蒙特卡洛;第二,首次访问(first-visit)和每次访问(every-visit)两种预测模式有什么区别;第三,如何用 Python 从零实现蒙特卡洛预测和蒙特卡洛控制,并在一个标准环境里跑出结果。读完之后,你能动手写出第一个不依赖环境模型的强化学习算法,也能理解后续时序差分(TD)方法到底改进了什么。
1. 动态规划的最大局限:环境模型从哪来
在蒙特卡洛之前,系列文章里讨论的强化学习问题主要是用动态规划(DP)求解的。动态规划的核心是贝尔曼方程,它要求我们知道环境的完整模型,也就是状态转移概率 (P(s'|s,a)) 和奖励函数 (R(s,a,s'))。有了这两样东西,才能做策略评估(policy evaluation)和策略改进(policy improvement)的迭代。
但这里有一个很实际的问题:状态转移概率在绝大多数真实场景里是拿不到的。
举几个例子:
- 推荐系统:用户看到推荐内容后会不会点击,这个概率不是一个固定表格,它取决于用户偏好、当前时间、内容质量,甚至网络加载速度;
- 机器人控制:电机输出力矩后,机械臂末端实际到达的位置受负载、磨损、温度影响,很难写出精确的转移概率;
- 棋类游戏:围棋的状态转移只取决于对手策略,但对手策略是未知的,无法预先写成静态概率分布。
动态规划要求先有模型,再求解;现实往往是模型不可知,只能通过与真实环境不断交互来获得经验。这就是“无模型强化学习”(model-free reinforcement learning)出现的原因。它的基本逻辑是:不建模环境,只用环境反馈的样本(状态、动作、奖励)来学习。
蒙特卡洛方法正是无模型强化学习里最自然、也最容易理解的一种。它不要求任何先验知识,只需要智能体能够在一个任务里完整走到结束,并通过采样统计出真实的价值。许多初学者会在这一节产生一个误区:认为蒙特卡洛只是“用随机数做模拟”的数学工具,和强化学习没关系。实际上,随机模拟只是手段,在强化学习里它切换成了“与真实环境交互并收集样本”,本质目标仍然是估计价值函数和寻找最优策略。
2. 蒙特卡洛方法的核心思想与数学基础
蒙特卡洛方法并不是某个具体算法的名字,而是一大类“用随机采样解决确定性问题”的方法。它的名字来源于摩纳哥的蒙特卡洛赌场,因为赌场里的轮盘、骰子天然就是随机过程,而这种方法的核心就是随机数。
2.1 大数定律:平均值收敛于期望
严格支撑蒙特卡洛方法的数学定理是大数定律(Law of Large Numbers)。它的直观含义是:当独立同分布的随机样本数量足够多时,样本均值会依概率收敛到随机变量的期望。
用公式表示:假设 (X_1, X_2, ..., X_n) 是从某一分布中独立采样得到的随机变量,则:
[ \frac{1}{n}\sum_{i=1}^n X_i \rightarrow \mathbb{E}[X] ]
当 (n \rightarrow \infty) 时。
这个性质在强化学习里意味着什么?如果每次完整回合(episode)结束时,我们都能拿到一批从某一状态出发的回报样本,那么这批回报的平均值,就无限接近该状态的真实价值。
为了更直观地理解,可以先做一个经典实验:用蒙特卡洛方法估算圆周率 (\pi)。在正方形内随机撒点,统计落在内切圆中的比例。当样本量从1000增加到100万时,估算结果会越来越接近 3.14159。这个实验看起来和强化学习毫无关系,但背后的思想完全一致:用大量随机采样逼近真实值。
2.2 强化学习中的蒙特卡洛:从概率表到回报样本
在强化学习语境下,蒙特卡洛方法做的是这样一件事:
- 环境模型未知,但我们能让智能体在环境中执行动作并推进到回合结束;
- 每个完整回合都能从某个初始状态开始,产生一条轨迹: [ S_0, A_0, R_1, S_1, A_1, R_2, ..., S_T ]
- 对回合中出现的每一个状态 (s),计算从这个状态之后累计的回报(return): [ G_t = R_{t+1} + \gamma R_{t+2} + \gamma^2 R_{t+3} + ... ]
- 用多个回合中同一状态的回报平均值来估计状态价值: [ V(s) \approx \frac{1}{N(s)} \sum_{i=1}^{N(s)} G_i(s) ]
这里面有一个非常重要的区别:动态规划用的是“期望”(需要模型),蒙特卡洛用的是“样本均值”(只需要采样)。当样本量足够大时,两者会收敛到同一个结果——这正好是蒙特卡洛方法不需要环境模型的数学基础。
2.3 蒙特卡洛方法在强化学习里的基本条件
不是所有MDP问题都能直接用蒙特卡洛方法求解,它有两个适用条件:
- 必须是分幕式任务(episodic task):智能体与环境的交互能够分成一个个有终止状态回合,例如游戏到终局、迷宫走到出口。对于没有明确终止状态的持续性任务,需要先改造成分幕形式,或者换用时序差分方法。
- 必须能观察到完整回合:因为计算回报 (G_t) 需要知道未来所有奖励,所以必须等到回合结束才能更新,更新是“回合级”的,而不是“步级”的。
第一个条件把蒙特卡洛和后续的时序差分方法清晰地区分开:TD算法可以在每一步之后立即更新,不必等回合结束;蒙特卡洛必须等回合结束再更新。这种时间粒度的差异,会直接影响到样本效率、方差和收敛速度。
3. 强化学习中的蒙特卡洛:两种访问模式与控制策略
理解了核心思想后,接下来要把蒙特卡洛落到强化学习的两个核心任务上:预测(prediction)和控制(control)。预测解决“给定一个策略,估计状态的价值”;控制解决“如何找到最优策略”。
3.1 首次访问蒙特卡洛与每次访问蒙特卡洛
在同一个回合中,某个状态可能被访问多次。例如在一个网格世界任务里,智能体可能会反复经过同一格。此时计算回报有两种模式:
- 首次访问蒙特卡洛(first-visit MC):只使用该状态在回合中第一次被访问时后续的回报进行平均。也就是说,一个回合最多给一个状态贡献一个回报样本。
- 每次访问蒙特卡洛(every-visit MC):该状态在回合中每次被访问时,都分别计算一次回报并参与平均。一个回合可能给同一个状态贡献多个回报样本。
两种模式都能在大数定律下收敛到真实状态价值,但性质不同:
| 对比维度 | 首次访问 MC | 每次访问 MC |
|---|---|---|
| 样本利用率 | 一个回合对同一状态只记一次 | 同一状态每次访问都记录 |
| 偏差 | 无偏估计 | 每笔样本间存在关联,但同样收敛 |
| 方差 | 方差相对更大 | 经验上方差略低 |
| 实现复杂度 | 需要标记回合内已访问状态 | 实现更简单 |
在入门阶段,通常推荐先实现首次访问版本,因为它理论更简洁、更容易证明收敛,也是 Sutton 教材里默认的经典实现。每次访问版本在后续的批量更新和函数近似方法里更常见。
3.2 蒙特卡洛控制:如何从价值到策略
预测解决的是“这个策略到底好不好”,控制解决的是“如何找到最好的策略”。蒙特卡洛控制遵循的还是广义策略迭代(GPI)框架:策略评估 + 策略改进,不断交替。
但这里有一个新问题:如果我们永远只选择当前价值函数下的最优动作,那么很多状态永远不会被访问到,估计就会失真。比如某个状态明明可以通过走“探索路线”找到更高回报,但因为贪心策略不选它,算法永远不知道这个选项更好。为了让控制算法能够不断发现更好的动作,需要给动作选择引入随机性。
两种经典做法:
- 探索性初始化(exploring starts):强制每个回合的状态和动作都从不同的起点开始,保证所有状态-动作对都有概率被访问到。这种方法理论简单,但现实中很难做到——你无法控制真实环境的初始状态。
- epsilon-贪婪策略((\varepsilon)-greedy):以较小的概率 (\varepsilon) 随机选择一个动作,以 (1-\varepsilon) 的概率选择当前价值最高的动作。这一策略简单且实用,是入门阶段最推荐的探索方式。
在蒙特卡洛控制中,评估的对象从状态价值 (V(s)) 扩展到了动作价值 (Q(s,a))。因为控制过程需要比较同一状态下不同动作的优劣,只有估计出每个状态-动作对的价值 (Q(s,a)),才能知道哪个动作更优。如果只看状态价值 (V(s)),就无法在没有环境模型的情况下推导出最优动作。
3.3 蒙特卡洛方法在强化学习算法体系里的位置
从整个强化学习算法家族来看,蒙特卡洛方法是一个重要的分水岭。在它之前,动态规划是“有模型方法”;在它之后,时序差分(TD)、Q-learning、SARSA 等算法都是“无模型方法”。蒙特卡洛和 TD 都从样本中学习,但蒙特卡洛必须等回合结束才能更新,TD 则可以每步更新。理解蒙特卡洛的“回合级更新”逻辑,能帮助你更好地理解为什么 TD 方法在样本效率上通常优于蒙特卡洛,以及为什么蒙特卡洛的梯度估计方差较大。
4. 环境准备与实验设计
从这一节开始进入实操。为了让蒙特卡洛方法有直观的落地场景,我们需要一个环境。在强化学习入门里,Blackjack(21点)是最经典的蒙特卡洛实验环境之一,原因有三:
- 它是分幕式任务,每局结束很快,符合蒙特卡洛的更新要求;
- 状态空间是离散的且维度不高,可视化容易;
- 它天然适合用首次访问蒙特卡洛,Sutton 的经典教材也用它作为主示例。
4.1 安装环境
本文使用 Python 和 Gymnasium(Gym 的维护版本)。Gymnasium 提供了现成的 Blackjack 环境,不需要自己实现游戏逻辑。
pip install gymnasium pip install matplotlib版本方面,Gymnasium 目前是持续维护的库,建议直接安装最新的稳定版本。Python 建议使用 3.9 及以上版本。安装完成后,可以用下面的代码验证环境是否可用:
import gymnasium as gym env = gym.make("Blackjack-v1", natural=True, sab=False) obs, info = env.reset() print("初始状态:", obs)初始状态是一个三元组,分别代表玩家当前点数、庄家明牌点数、玩家是否有可用A(usable ace)。如果输出正常,说明环境已经就绪。
4.2 理解 Blackjack 环境的状态与动作
- 状态:((\text{玩家点数}, \text{庄家明牌点数}, \text{是否有可用A}))。
- 动作:0 表示“要牌”(hit),1 表示“停牌”(stick)。
- 奖励:如果玩家点数超过21则立即结束,奖励为 -1;双方停牌后比较点数,玩家赢为 +1,输为 -1,平局为 0。
- 自然21点:如果玩家前两张牌恰好是21点且庄家不是自然21点,直接获胜,奖励为 +1。
这里“可用A”是指手中有可当作11点计算的A。如果A被当作1点了,就是不可用A。这个细节对状态表示很重要,需要保留,否则状态不完整,价值估计会失真。
5. 从零实现:蒙特卡洛预测
5.1 实现一个简单的固定策略
预测的第一步是给定一个策略。为了演示,可以先实现一个非常简单的固定策略:玩家点数小于 18 就要牌,大于等于 18 就停牌。这个策略不一定是好策略,但足够用来测试预测算法的正确性。
import random import gymnasium as gym import numpy as np def simple_policy(state): """简单的固定策略:点数小于 18 要牌,否则停牌""" player_sum, dealer_card, usable_ace = state return 0 if player_sum < 18 else 1 # 0 = hit, 1 = stick5.2 首次访问蒙特卡洛预测的完整实现
接下来实现核心算法。我们需要维护两个字典:一个记录每个状态累积的总回报,一个记录每个状态被首次访问的次数。对每个回合,先执行完整的轨迹,然后反向遍历每一步,计算回报并更新状态价值。
def generate_episode(env, policy): """执行一个回合,返回 [(state, action, reward)] 列表""" episode = [] obs, _ = env.reset() done = False while not done: action = policy(obs) next_obs, reward, terminated, truncated, _ = env.step(action) episode.append((obs, action, reward)) obs = next_obs done = terminated or truncated return episode def first_visit_mc_prediction(env, policy, num_episodes, gamma=1.0): """首次访问蒙特卡洛策略评估""" returns_sum = {} returns_count = {} V = {} for _ in range(num_episodes): episode = generate_episode(env, policy) visited_states = set() G = 0 # 从回合末尾反向计算回报 for t in reversed(range(len(episode))): state, action, reward = episode[t] G = reward + gamma * G if state not in visited_states: visited_states.add(state) returns_sum[state] = returns_sum.get(state, 0) + G returns_count[state] = returns_count.get(state, 0) + 1 V[state] = returns_sum[state] / returns_count[state] return V这段代码里有一个容易忽略的细节:为什么要从回合末尾反向遍历?因为正向遍历时,我们无法预知未来的累计回报;反向遍历时,可以逐步把后面的奖励累加进来,保证每一步的 (G) 都是完整的回报。对首次访问模式,visited_states集合保证了每个状态在这个回合里只被统计一次。
5.3 为什么使用回报的反向累积
假设一个回合的轨迹是 ((s_0, a_0, r_1), (s_1, a_1, r_2), (s_2, a_2, r_3)),从末尾开始:
- 第 2 步:(G_2 = r_3)
- 第 1 步:(G_1 = r_2 + \gamma r_3)
- 第 0 步:(G_0 = r_1 + \gamma r_2 + \gamma^2 r_3)
这样每一步的回报都包含了未来的所有奖励,和回报定义完全一致。如果你从前往后计算,就需要额外维护一个队列来记录未来的奖励,代码会复杂很多。反向遍历是解决这一问题的标准做法。
6. 从零实现:蒙特卡洛控制
预测做完之后,进入控制阶段。控制的最终目标是找到最优策略,而不是评估固定策略。
6.1 epsilon-贪婪策略的蒙特卡洛控制
这里实现的是带 (\varepsilon)-贪婪探索的首次访问蒙特卡洛控制。它会同时维护动作价值 (Q(s,a)),并在每个回合结束后根据当前 (Q) 值更新策略。
def make_epsilon_greedy_policy(Q, epsilon, num_actions=2): """根据 Q 值表生成 epsilon-greedy 策略函数""" def policy(state): if random.random() < epsilon: return random.randint(0, num_actions - 1) else: q_values = [Q.get((state, a), 0.0) for a in range(num_actions)] max_q = max(q_values) # 如果有多个动作拥有相同的最大值,随机选一个 best_actions = [a for a, q in enumerate(q_values) if q == max_q] return random.choice(best_actions) return policy def mc_control_epsilon_greedy(env, num_episodes, gamma=1.0, epsilon=0.1): """首次访问蒙特卡洛控制""" Q = {} returns_sum = {} returns_count = {} for _ in range(num_episodes): policy = make_epsilon_greedy_policy(Q, epsilon) episode = generate_episode(env, policy) visited_state_actions = set() G = 0 for t in reversed(range(len(episode))): state, action, reward = episode[t] G = reward + gamma * G sa_pair = (state, action) if sa_pair not in visited_state_actions: visited_state_actions.add(sa_pair) returns_sum[sa_pair] = returns_sum.get(sa_pair, 0) + G returns_count[sa_pair] = returns_count.get(sa_pair, 0) + 1 Q[sa_pair] = returns_sum[sa_pair] / returns_count[sa_pair] return Q注意一个关键点:policy是在每个回合开始时重新生成的。这意味着当前回合的探索策略会基于此前所有回合积累的 (Q) 值,既能利用已学到的知识,又能保留一定的随机性继续探索。如果把policy放在循环外面,策略就会一直是初始随机策略,无法完成策略迭代。
6.2 从 Q 表推导最终策略
控制完成后,从中提取确定性策略:对每个状态,选择 (Q(s,a)) 最大的动作。
def derive_policy_from_Q(Q): policy = {} state_keys = set(s for s, _ in Q.keys()) for state in state_keys: q_vals = [Q.get((state, a), 0.0) for a in range(2)] policy[state] = int(np.argmax(q_vals)) return policy这个策略就是算法最终学到的最优策略。在 Blackjack 环境中,可以用它来模拟新的对局,验证胜率。
7. 运行结果与效果验证
7.1 运行预测并画图
为了观察预测结果是否正确,可以把每个状态的价值函数绘制成三维曲面图。这里固定“没有可用A”的情况,绘制玩家点数与庄家明牌对应的价值。
import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D env = gym.make("Blackjack-v1", natural=True, sab=False) V = first_visit_mc_prediction(env, simple_policy, num_episodes=50000, gamma=1.0) # 提取无可用A的状态价值 states_plot = [] values_plot = [] for (player, dealer, usable_ace), v in V.items(): if not usable_ace and 11 <= player <= 21: states_plot.append((player, dealer)) values_plot.append(v) player_scores = [p for p, d in states_plot] dealer_cards = [d for p, d in states_plot] fig = plt.figure(figsize=(10, 6)) ax = fig.add_subplot(111, projection="3d") ax.scatter(player_scores, dealer_cards, values_plot, c=values_plot, cmap="viridis", s=30) ax.set_xlabel("Player Sum") ax.set_ylabel("Dealer Card") ax.set_zlabel("Value") plt.title("First-Visit MC Prediction: No Usable Ace") plt.show()运行后,你会看到价值曲面呈现出合理的规律:玩家点数低时价值为负,点数接近 20 或 21 时价值为正,这和 21 点游戏的直觉完全吻合。如果出现大面积乱数值,优先检查回报计算和状态标记逻辑。
7.2 运行控制并统计收益
控制算法的验证可以看两个指标:平均每一回合的累计奖励是否在训练过程中上升,以及最终策略是否合理。
env = gym.make("Blackjack-v1", natural=True, sab=False) total_rewards = [] for _ in range(100): Q = mc_control_epsilon_greedy(env, num_episodes=10000, gamma=1.0, epsilon=0.1) policy = derive_policy_from_Q(Q) # 用最终策略独立测试 1000 局 wins = 0 total = 1000 for _ in range(total): obs, _ = env.reset() done = False while not done: action = policy.get(obs, 0) # 默认要牌 obs, reward, terminated, truncated, _ = env.step(action) done = terminated or truncated if reward > 0: wins += 1 total_rewards.append(wins / total) print("平均胜率:", np.mean(total_rewards))如果算法实现正确,收益率会在 0.38 到 0.42 附近波动(Blackjack 扣除庄家优势后,最优策略的胜率大致在这个范围)。如果你的胜率只有 0.2 左右,通常说明策略没有学到最优,常见原因是探索参数设置不当或回报计算错误。
7.3 成功与否的判断标准
- 预测阶段:价值曲面应呈现单调合理的趋势,而不是随机噪声;
- 控制阶段:独立测试胜率应显著高于纯随机策略(纯随机胜率大概在 0.28 左右);
- 训练曲线:如果记录每批回合的平均回报,应有整体上升趋势,后期趋于平稳。
如果运行结果不理想,第一步检查反向遍历计算回报的逻辑,第二步检查visited_state_actions标记的位置,第三步检查策略生成函数是否在每个回合都根据最新 (Q) 值重新生成。
8. 常见问题与排查思路
蒙特卡洛方法的代码量不大,但新手容易在几个隐蔽细节上出错。这里整理出最常见的五类问题。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 价值函数长时间不收敛 | Gamma 参数设置不当或回报计算使用了未经过折扣的累计奖励 | 打印若干状态的回报样本,人工核算几步 | 确认 G 的更新公式:G = reward + gamma * G |
| 同一状态出现多个不同价值 | 使用了每次访问模式但没有意识到状态在一个回合内可能多次出现 | 在回合内打印状态序列 | 确认是否采用首次访问标记;若用每次访问则接受其收敛性质 |
| 控制算法学到固定动作、无探索 | 策略函数在循环外生成,或 epsilon 设置过小 | 检查策略生成位置,打印每个回合的动作分布 | 确保每个回合都基于最新 Q 重新生成 epsilon-greedy 策略 |
| 独立测试胜率很低 | 训练回合数不足,或 epsilon 过大导致策略始终接近随机 | 统计每个状态-动作对的访问频次 | 增大回合数,动态降低 epsilon |
| Blackjack 环境初始状态为 None | 使用的 API 与 Gymnasium 版本不一致 | 打印 env.reset() 返回值 | 使用 obs, info = env.reset() 并确认环境名 |
排查时有一个通用的思路:把问题拆成“环境”和“算法”两部分。先用一个最简单的策略(比如固定动作)跑通环境,确认环境交互正常;再用较大训练轮数专门验证算法,避免两个因素混在一起更难定位。
9. 最佳实践与工程建议
9.1 关于探索策略
- 如果使用固定 epsilon,一般设置在 0.1 到 0.2 之间,太小会过早收敛到次优策略,太大会导致策略接近随机。
- 更推荐的做法是让 epsilon 随训练进度衰减:开始阶段大探索,后期小探索。例如设定初始 epsilon=0.5,每 1000 回合乘以 0.95,最终保持在 0.01。
- 探索性初始化在 Blackjack 这类环境里很容易实现,但真实项目中往往不可行,所以建议直接以 epsilon-greedy 作为默认探索方案,它更接近实际约束。
9.2 关于训练稳定性
- 蒙特卡洛方法的方差比较大,如果训练曲线剧烈波动,不必立刻怀疑代码错误,增加样本量通常是第一选择。
- 如果资源有限,可以采用“批量平均”:每跑完 1000 个回合计算一次平均回报,而不是每个回合都打点。
- 对状态空间较大的问题,最好不要用字典存储 (Q(s,a)),应该改用二维数组或更紧凑的数据结构,避免哈希查找开销。
9.3 关于学习过程的观察指标
建议在训练过程中记录三类数据:
- 每个回合的累计奖励(用于观察策略是否变好);
- 状态-动作对的访问覆盖率(用于判断探索是否充分);
- 代表性状态的价值变化曲线(例如 Blackjack 中“玩家20点 vs 庄家6点”的价值应接近正值且稳定)。
这些比单纯看最终胜率更能帮助你诊断问题出现在哪个环节。
9.4 关于环境 API 的兼容性
Gymnasium 沿用了较新的 reset/step 返回格式:reset()返回(obs, info),step()返回(obs, reward, terminated, truncated, info)。如果你之前用过老版本 Gym,会发现返回值数量不同。在写通用代码时,建议统一按较新的 API 格式处理,并在代码开头做环境边界检查。
10. 总结与下一步
这篇文章从“动态规划需要环境模型”的现实局限出发,引出了蒙特卡洛方法。它用随机采样的均值替代期望,用完整回合的回报估计状态价值,虽然等待回合结束才能更新,却因此摆脱了对状态转移概率的依赖,成为无模型强化学习的起点。文中通过 Blackjack 环境完整实现了首次访问蒙特卡洛预测和基于 epsilon-greedy 的蒙特卡洛控制,并给出了验证标准。掌握这套代码,你已经具备了在未知环境中训练一个简单智能体的能力。
如果对这个主题继续深挖,建议按以下顺序推进:
- 尝试把 epsilon 衰减策略加进控制算法,观察胜率变化;
- 在同一个 Blackjack 环境上手动计算一两个状态的理论价值,与蒙特卡洛结果对比;
- 换成每次访问蒙特卡洛实现一遍,对比两者在训练曲线上的差异;
- 再往后学习时序差分(TD)和 Q-learning,你会明显感受到“每步更新”和“回合结束更新”的效率差异。
蒙特卡洛方法最值得记住的一句话是:当环境模型不可知时,经验本身就是最好的模型。把所有采样到的回报取平均,就是对这个未知世界最诚实的估计。下一篇文章会进一步讨论蒙特卡洛方法的方差缩减、重要性采样和离策略学习,这些是连接蒙特卡洛与更现代强化学习算法的关键桥梁。