无需环境模型的强化学习:蒙特卡洛与时序差分算法详解及21点游戏实践
30款热门AI模型一站整合DeepSeek/GLM/Qwen 随心用限时 5 折。 点击领海量免费额度在实际学习强化学习的过程中很多同学在理解了马尔可夫决策过程MDP和动态规划DP后会遇到一个关键瓶颈现实世界中的环境模型即状态转移概率和奖励函数往往是未知的。面对这种“模型未知”的挑战如何评估策略和寻找最优策略这正是蒙特卡洛方法和时序差分算法大显身手的地方。对于生物、化学、材料等交叉学科的研究者而言理解这两种无需模型的经典方法是将强化学习应用于实验设计、药物筛选、路径规划等实际问题的关键一步。本文旨在为具备一定概率论和编程基础如Python的读者特别是非计算机专业的理工科学生提供一个清晰、可操作的入门指南。我们将从零开始解释蒙特卡洛方法和时序差分算法的核心思想并通过一个经典的“21点”纸牌游戏案例手把手带你完成代码实现、策略评估和优化。你将不仅理解算法公式更能掌握如何用代码实现它们并学会分析结果、排查常见错误最终将这套方法迁移到你自己的研究场景中。1. 理解“模型未知”与两种核心思想在强化学习中“模型”特指对环境的完整描述即已知状态转移概率P(s|s, a)和奖励函数R(s, a, s)。动态规划DP方法正是在此基础上进行策略迭代和价值迭代。然而在生物信息学分析、机器人控制、游戏对战等绝大多数现实场景中我们无法预先知道这些概率分布。1.1 从动态规划到无需模型的跨越动态规划的核心是“自举”bootstrapping它利用已知的模型通过贝尔曼方程迭代更新状态价值。但当模型未知时我们失去了这个计算基础。此时智能体必须通过与环境的真实交互来学习。这引出了两种根本不同的学习范式蒙特卡洛方法核心思想是“用经验平均代替期望”。智能体通过运行完整的回合从开始到终止收集到一条真实的轨迹状态、动作、奖励序列然后用这个轨迹上获得的实际回报Return的平均值来估计状态或状态-动作对的价值。它必须等到回合结束才能进行更新是一种“离线”学习。时序差分方法核心思想是“用估计来更新估计”。它结合了蒙特卡洛的采样思想和动态规划的自举思想。智能体在每一步交互后利用当前对下一个状态的价值的估计来更新当前状态的价值。它不需要等到回合结束可以“在线”学习。为了更直观地理解它们的区别与联系我们可以从更新目标、偏差/方差权衡和学习方式三个维度进行对比特性维度蒙特卡洛方法时序差分方法更新目标实际回报 ( G_t )即时奖励 折扣后的下一状态估计值 ( R_{t1} \gamma V(S_{t1}) )偏差/方差无偏估计但方差高依赖单次完整轨迹有偏估计但方差低利用了现有的价值估计学习方式必须等到回合结束离线每一步都可以更新在线对环境的依赖需要回合有明确的终止状态可以处理连续无终止的任务收敛速度通常较慢但最终收敛通常更快但可能收敛到次优点1.2 关键概念回报、价值函数与探索在深入算法前需要明确几个贯穿始终的概念回报 (G_t)从时刻t开始未来所有奖励的折扣总和(G_t R_{t1} \gamma R_{t2} \gamma^2 R_{t3} ...)。蒙特卡洛方法直接使用它。状态价值函数 (V_\pi(s))在策略 (\pi) 下从状态s出发能获得的期望回报。动作价值函数 (Q_\pi(s, a))在策略 (\pi) 下在状态s执行动作a后能获得的期望回报。在模型未知时我们更常学习 (Q) 函数因为它能直接指导动作选择。探索与利用智能体需要在尝试新动作探索和选择当前认为最好的动作利用之间取得平衡。这是所有无需模型方法的核心挑战。2. 环境准备与问题定义21点游戏为了将理论付诸实践我们选择“21点”作为实验环境。它是一个经典的、有明确规则的序列决策问题非常适合演示蒙特卡洛和时序差分方法。2.1 环境搭建与规则理解我们将使用 OpenAI Gym 库中的Blackjack-v1环境。首先通过 pip 安装必要的库。pip install gym numpy matplotlib21点游戏规则简述目标使手中牌的点数之和尽可能接近21点但不能超过爆牌同时要大于庄家点数。牌值2-10为面值J、Q、K为10点A可计为1点或11点。状态在Gym环境中一个状态是一个三元组(玩家当前点数, 庄家明牌点数, 是否有可用A)。动作0 表示“停牌”1 表示“要牌”。奖励回合结束时赢1平局0输-1。2.2 初始化环境与策略定义我们先创建一个简单的固定策略作为基线当玩家点数达到18或以上时停牌否则要牌。import gym import numpy as np from collections import defaultdict import matplotlib.pyplot as plt # 创建环境 env gym.make(Blackjack-v1, sabTrue) # sabTrue 使用简化规则 def simple_policy(observation): 一个简单的固定策略。 观察值 observation: (玩家点数, 庄家明牌点数, 是否有可用Ace) player_score, dealer_score, usable_ace observation # 如果玩家点数 18选择停牌0否则要牌1 return 0 if player_score 18 else 1 # 测试策略与环境交互 print(测试一个回合) observation, info env.reset() done False while not done: action simple_policy(observation) # 根据策略选择动作 observation, reward, terminated, truncated, info env.step(action) done terminated or truncated print(f状态: {observation}, 动作: {action}, 奖励: {reward}, 结束: {done}) env.close()运行上述代码你会看到智能体根据简单策略与环境进行一个完整回合的交互过程。这是所有后续算法的基础。3. 蒙特卡洛方法首次访问与每次访问蒙特卡洛策略评估的目标是在给定策略 (\pi) 下通过大量回合的经验来估计状态价值 (V_\pi(s))。3.1 算法核心首次访问MC预测首次访问蒙特卡洛预测算法流程如下输入待评估的策略 (\pi)。初始化价值字典V用于记录每个状态的平均回报返回字典returns用于记录每个状态遇到的所有回报列表。循环大量回合 a. 根据策略 (\pi) 生成一个回合的轨迹(S_0, A_0, R_1, S_1, A_1, R_2, ..., S_T)。 b. 初始化回报G 0。 c. 从回合最后一步向前遍历 (t T-1, T-2, ..., 0) -G γ * G R_{t1}折扣累积回报 - 如果状态S_t在本回合中是第一次出现首次访问 - 将G加入到returns[S_t]的列表中。 - 更新V[S_t] average(returns[S_t])。以下是该算法的Python实现def mc_prediction_first_visit(policy, env, num_episodes, gamma1.0): 首次访问蒙特卡洛策略评估。 返回状态价值函数 V字典形式。 # 初始化记录每个状态的所有回报 returns defaultdict(list) # 初始化状态价值表 V defaultdict(float) for i_episode in range(1, num_episodes 1): # 生成一个回合 episode [] state, _ env.reset() done False while not done: action policy(state) next_state, reward, terminated, truncated, _ env.step(action) done terminated or truncated # 记录 (状态, 动作, 奖励) episode.append((state, action, reward)) state next_state # 计算回报并更新价值函数 G 0.0 # 从后向前遍历方便计算累积回报 visited_states_in_episode set() for t in range(len(episode) - 1, -1, -1): state, action, reward episode[t] # 回报是向未来累积的 G gamma * G reward # 首次访问判断 if state not in visited_states_in_episode: visited_states_in_episode.add(state) returns[state].append(G) V[state] np.mean(returns[state]) # 更新为平均值 # 可选每10000回合打印进度 if i_episode % 10000 0: print(f已完成 {i_episode} 个回合) return V # 使用简单策略进行评估 print(\n开始蒙特卡洛策略评估首次访问...) V_1m mc_prediction_first_visit(simple_policy, env, num_episodes100000) print(f评估完成共评估了 {len(V_1m)} 个不同的状态。) # 查看几个典型状态的价值 sample_states [(21, 10, True), (15, 6, False), (10, 5, False)] for s in sample_states: print(f状态 {s} 的价值 V(s) ≈ {V_1m.get(s, 0.0):.4f})3.2 结果可视化与分析得到状态价值函数V后我们可以将其可视化以理解策略在不同情况下的表现。def plot_value_function(V, title状态价值函数): 绘制21点游戏的状态价值函数区分有无可用Ace。 # 提取数据我们需要两个矩阵分别对应有无可用Ace player_range range(12, 22) # 关注玩家点数12-21 dealer_range range(1, 11) # 庄家明牌点数1-10 (Ace计为11) Z_no_ace np.zeros((len(player_range), len(dealer_range))) Z_usable_ace np.zeros((len(player_range), len(dealer_range))) for i, player in enumerate(player_range): for j, dealer in enumerate(dealer_range): Z_no_ace[i, j] V.get((player, dealer, False), 0) Z_usable_ace[i, j] V.get((player, dealer, True), 0) fig, axes plt.subplots(1, 2, figsize(14, 5), shareyTrue) fig.suptitle(title, fontsize16) # 无可用Ace im1 axes[0].imshow(Z_no_ace, cmapcoolwarm, originlower, aspectauto) axes[0].set_title(无可用 Ace) axes[0].set_xlabel(庄家明牌点数) axes[0].set_ylabel(玩家点数) axes[0].set_xticks(range(len(dealer_range)), dealer_range) axes[0].set_yticks(range(len(player_range)), player_range) plt.colorbar(im1, axaxes[0]) # 有可用Ace im2 axes[1].imshow(Z_usable_ace, cmapcoolwarm, originlower, aspectauto) axes[2].set_title(有可用 Ace) axes[1].set_xlabel(庄家明牌点数) axes[1].set_xticks(range(len(dealer_range)), dealer_range) axes[1].set_yticks(range(len(player_range)), player_range) plt.colorbar(im2, axaxes[1]) plt.tight_layout() plt.show() print(\n绘制简单策略下的状态价值函数...) plot_value_function(V_1m, title首次访问MC评估的简单策略价值函数)通过热图你可以清晰地看到当玩家点数较高如20、21时价值普遍为正接近1因为胜率高当玩家点数较低如12且庄家牌面大时价值为负。有可用Ace时价值普遍更高因为Ace的灵活性降低了爆牌风险。3.3 常见问题与排查在实现蒙特卡洛方法时初学者常遇到以下问题价值估计不收敛或波动大现象V(s)的值在不同运行间差异很大或随着回合数增加变化剧烈。原因回合数num_episodes不足。蒙特卡洛方法依赖大数定律需要大量样本才能稳定。此外探索不充分导致某些状态很少被访问。排查增加回合数至50万或100万。检查returns[s]列表的长度确认状态是否被足够访问。解决确保有足够的探索。对于固定策略如果某些状态永远无法到达其价值将无法被评估。折扣因子gamma设置错误现象价值估计的绝对值异常大或小。原因gamma通常应小于等于1。如果gamma1且回合可能很长回报G可能累积得很大。如果gamma1在无限回合任务中会导致发散。排查检查G的计算公式G gamma * G reward。对于21点这种回合制游戏gamma1是合理的。解决对于有终止状态的任务如游戏gamma可以设为1。对于连续任务gamma应小于1如0.99。首次访问 vs 每次访问现象使用“每次访问”算法即每次遇到状态都更新时价值估计可能略有不同。原因在同一回合中一个状态可能被多次访问。“首次访问”是无偏估计“每次访问”也是无偏但通常方差更小、收敛更快。选择大多数情况下两者都能工作。首次访问更易于理解和实现。如果你想尝试每次访问只需移除visited_states_in_episode的判断即可。4. 时序差分学习TD(0) 与 SARSA时序差分学习是蒙特卡洛思想和动态规划思想的结合。最基础的算法是 TD(0)也称为一步时序差分。4.1 TD(0) 预测用估计更新估计TD(0) 用于预测给定策略下的状态价值 (V_\pi)。其更新公式为 [ V(S_t) \leftarrow V(S_t) \alpha [R_{t1} \gamma V(S_{t1}) - V(S_t)] ] 其中 (\alpha) 是学习率。括号内的部分 (R_{t1} \gamma V(S_{t1}) - V(S_t)) 称为TD误差它代表了当前估计与更优估计目标值之间的差异。def td_prediction(policy, env, num_episodes, alpha0.01, gamma1.0): TD(0) 策略评估。 # 初始化状态价值表 V defaultdict(float) for i_episode in range(1, num_episodes 1): state, _ env.reset() done False while not done: action policy(state) next_state, reward, terminated, truncated, _ env.step(action) done terminated or truncated # TD(0) 更新 td_target reward gamma * V[next_state] td_error td_target - V[state] V[state] alpha * td_error state next_state if i_episode % 10000 0: print(fTD(0) 预测已完成 {i_episode} 回合) return V print(\n开始TD(0)策略评估...) V_td td_prediction(simple_policy, env, num_episodes100000, alpha0.05) print(fTD(0)评估完成。) plot_value_function(V_td, titleTD(0)评估的简单策略价值函数)你会发现TD(0) 评估的结果与蒙特卡洛方法相似但它是在线更新的无需等待回合结束。4.2 SARSA在线策略TD控制预测是为了评估策略的好坏而控制是为了找到最优策略。SARSA 是一种在线策略的TD控制算法它直接学习动作价值函数 (Q(s, a))并基于此改进策略通常是 (\epsilon)-贪婪策略。SARSA 得名于其更新所涉及的序列 ((S_t, A_t, R_{t1}, S_{t1}, A_{t1}))。其更新公式为 [ Q(S_t, A_t) \leftarrow Q(S_t, A_t) \alpha [R_{t1} \gamma Q(S_{t1}, A_{t1}) - Q(S_t, A_t)] ]def epsilon_greedy_policy(Q, state, epsilon0.1): 基于Q表的 epsilon-贪婪策略。 if np.random.random() epsilon: # 探索随机选择动作 return env.action_space.sample() else: # 利用选择当前状态下的最优动作 # 如果Q值都未初始化为0则随机选一个 q_values [Q.get((state, a), 0.0) for a in range(env.action_space.n)] max_q max(q_values) # 如果有多个动作具有相同的最大Q值从中随机选一个 best_actions [a for a, q in enumerate(q_values) if q max_q] return np.random.choice(best_actions) def sarsa(env, num_episodes, alpha0.1, gamma1.0, epsilon0.1): SARSA 算法在线策略TD控制学习最优动作价值函数 Q*。 # 初始化 Q表。使用defaultdict默认值为0.0 Q defaultdict(float) for i_episode in range(1, num_episodes 1): state, _ env.reset() # 根据当前Q表选择初始动作 action epsilon_greedy_policy(Q, state, epsilon) done False while not done: # 执行动作得到下一个状态和奖励 next_state, reward, terminated, truncated, _ env.step(action) done terminated or truncated # 在下一个状态根据当前策略选择下一个动作 next_action epsilon_greedy_policy(Q, next_state, epsilon) # SARSA 更新 current_q Q[(state, action)] # 如果下一个状态是终止状态则下一个Q值为0 next_q Q[(next_state, next_action)] if not done else 0.0 td_target reward gamma * next_q td_error td_target - current_q Q[(state, action)] current_q alpha * td_error state, action next_state, next_action if i_episode % 50000 0: print(fSARSA 已完成 {i_episode} 回合) # 从Q表推导出最优策略 policy {} for state_action, q_value in Q.items(): state, action state_action if state not in policy: policy[state] {} policy[state][action] q_value # 对于每个状态选择Q值最大的动作作为最优策略 optimal_policy {} for state, action_dict in policy.items(): optimal_action max(action_dict, keyaction_dict.get) optimal_policy[state] optimal_action return Q, optimal_policy print(\n开始SARSA算法寻找最优策略...) Q_sarsa, optimal_policy_sarsa sarsa(env, num_episodes500000, alpha0.01, epsilon0.1) print(SARSA 学习完成。)4.3 从Q表到可视化策略学习到Q表后我们可以从中提取最优策略并将其可视化与最初的简单策略进行对比。def plot_policy(policy, title最优策略): 绘制21点游戏的最优策略图0:停牌1:要牌。 player_range range(12, 22) dealer_range range(1, 11) Z_no_ace np.zeros((len(player_range), len(dealer_range))) Z_usable_ace np.zeros((len(player_range), len(dealer_range))) for i, player in enumerate(player_range): for j, dealer in enumerate(dealer_range): # 默认动作为要牌1如果策略中指定了停牌0则改为0 Z_no_ace[i, j] policy.get((player, dealer, False), 1) # 1是要牌 Z_usable_ace[i, j] policy.get((player, dealer, True), 1) fig, axes plt.subplots(1, 2, figsize(14, 5)) fig.suptitle(title, fontsize16) # 无可用Ace的策略 im1 axes[0].imshow(Z_no_ace, cmapbinary, vmin0, vmax1, originlower, aspectauto) axes[0].set_title(无可用 Ace (0:停牌, 1:要牌)) axes[0].set_xlabel(庄家明牌点数) axes[0].set_ylabel(玩家点数) axes[0].set_xticks(range(len(dealer_range)), dealer_range) axes[0].set_yticks(range(len(player_range)), player_range) # 添加网格线以便看清每个单元格 axes[0].set_xticks(np.arange(-.5, len(dealer_range), 1), minorTrue) axes[0].set_yticks(np.arange(-.5, len(player_range), 1), minorTrue) axes[0].grid(whichminor, colorgray, linestyle-, linewidth0.5) # 有可用Ace的策略 im2 axes[1].imshow(Z_usable_ace, cmapbinary, vmin0, vmax1, originlower, aspectauto) axes[1].set_title(有可用 Ace (0:停牌, 1:要牌)) axes[1].set_xlabel(庄家明牌点数) axes[1].set_xticks(range(len(dealer_range)), dealer_range) axes[1].set_yticks(range(len(player_range)), player_range) axes[1].set_xticks(np.arange(-.5, len(dealer_range), 1), minorTrue) axes[1].set_yticks(np.arange(-.5, len(player_range), 1), minorTrue) axes[1].grid(whichminor, colorgray, linestyle-, linewidth0.5) plt.tight_layout() plt.show() print(\n绘制SARSA学习到的最优策略...) plot_policy(optimal_policy_sarsa, titleSARSA学习的最优策略)在策略图中黑色0代表“停牌”白色1代表“要牌”。你会看到学习到的策略比我们最初设定的简单策略点数18停牌要精细得多。例如当玩家点数较低时无论庄家牌面如何几乎总是要牌而当点数较高如20时几乎总是停牌。在中间区域如玩家16点庄家7点策略会做出更复杂的决策。5. 算法对比、调参与生产环境考量5.1 蒙特卡洛 vs 时序差分深入对比通过实践我们可以总结出两种方法更细致的差异对比项蒙特卡洛方法时序差分方法更新时机回合结束后离线每一步后在线偏差/方差无偏高方差有偏依赖当前估计低方差收敛性对函数近似可能不稳定通常更稳定能保证收敛在表格型情况下数据效率必须使用完整轨迹数据利用率低可以复用经验数据效率高对初始值敏感度不敏感最终收敛到真值敏感不同的初始值可能导致不同收敛点适用场景回合制任务需要精确评估连续任务需要快速在线学习注意TD方法因为有偏在非表格型如使用神经网络情况下可能不收敛而蒙特卡洛虽然方差大但在函数近似中有时更稳定。5.2 关键参数调优指南在实际应用中参数设置对学习效果至关重要。学习率 (\alpha)作用控制每次更新的步长。设置通常设置在0.01到0.1之间。太大会导致震荡不收敛太小则学习过慢。技巧可以使用衰减学习率例如 (\alpha_t \frac{1}{1 t}) 或 (\alpha_t \frac{0.1}{1 0.01*t})。探索率 (\epsilon)作用在ε-贪婪策略中控制探索的概率。设置初始可设为0.1或0.2。随着学习进行可以逐渐衰减如指数衰减让智能体后期更多利用学到的知识。代码示例衰减探索率initial_epsilon 0.2 decay_rate 0.999995 min_epsilon 0.01 for episode in range(num_episodes): epsilon max(min_epsilon, initial_epsilon * (decay_rate ** episode)) # 在每一步使用当前的 epsilon折扣因子 (\gamma)作用衡量未来奖励的重要性。γ0表示只关心即时奖励γ接近1表示非常重视长远回报。设置对于有明确终止状态的任务如游戏可以设为1。对于持续任务通常设为0.9、0.99或0.999。5.3 从实验到生产注意事项与扩展在学术实验或小型项目中跑通算法只是第一步。若要应用于更严肃的生物信息学分析或机器人控制还需考虑状态/动作空间爆炸21点的状态空间很小约200个。实际问题中状态可能是高维连续空间如图像、基因序列。此时需要引入函数近似如线性函数、神经网络即深度强化学习来逼近Q函数或策略。采样效率与经验回放与环境交互收集数据成本可能很高如真实生物实验。经验回放技术可以存储过去的经验(s, a, r, s)并随机抽样用于训练打破数据间的相关性大幅提升数据利用效率。DQN算法就采用了这一技术。探索策略的改进ε-贪婪策略简单但低效。可以尝试上置信界、汤普森采样或基于内在好奇心的探索方法在复杂环境中更有效地探索。算法选择SARSA是在线策略算法学习的是执行策略本身的价值。Q-Learning是离线策略算法它学习的是最优动作价值函数 (Q^*)更新时使用max_a Q(s, a)通常比SARSA更激进收敛更快但也可能因为过度估计而带来风险。其更新公式为 [ Q(S_t, A_t) \leftarrow Q(S_t, A_t) \alpha [R_{t1} \gamma \max_{a} Q(S_{t1}, a) - Q(S_t, A_t)] ]评估与监控在生产中不能只关注最终策略。需要持续监控学习曲线绘制每个回合的总奖励随时间的变化观察是否收敛。策略稳定性定期测试当前策略的性能避免策略震荡。Q值分布检查Q值是否出现异常大的数值这可能意味着学习率过高或奖励设置不合理。6. 总结与下一步方向通过本文我们完成了从理论到代码的跨越实现了蒙特卡洛策略评估、TD(0)预测以及SARSA控制算法并在21点游戏中验证了其有效性。你应当已经理解蒙特卡洛方法通过平均完整回报来估计价值无需模型但需要回合终止且方差较高。时序差分方法通过自举和采样相结合实现了在线、增量式学习是强化学习的核心思想。SARSA作为一种在线策略TD控制算法通过ε-贪婪探索能够学习到一个实用的最优策略。作为生物等领域的研究者你可以将这套框架迁移到你的问题中将“状态”定义为你的实验条件或系统观测值将“动作”定义为你的操作选择如添加何种试剂、选择哪个参数将“奖励”定义为实验结果的量化评价如产物产量、细胞活性。虽然真实问题会更复杂但蒙特卡洛和时序差分提供了无需知道精确环境模型的强大学习起点。下一步你可以沿着以下方向深入实现Q-Learning修改SARSA代码将更新目标改为reward gamma * max_a Q(s, a)对比两者在21点游戏中的表现差异。引入函数近似当状态空间变大时用神经网络代替Q表。尝试用PyTorch或TensorFlow实现一个简单的DQN来解决CartPole-v1环境。应用于你的领域问题尝试用SARSA或Q-Learning解决一个简单的生物学模型问题例如设计一个虚拟的“药物剂量优化”环境。学习更高级算法理解多步TD如TD(λ)、策略梯度方法以及演员-评论家架构这些是解决复杂连续控制问题的主流工具。强化学习是一个在实践中不断试错和调参的领域。最好的学习方式就是选择一个明确的问题亲手实现算法观察其行为分析失败原因并持续迭代。从这个简单的21点游戏开始你已经掌握了打开这个领域大门的钥匙。 30款热门AI模型一站整合DeepSeek/GLM/Qwen 随心用限时 5 折。 点击领海量免费额度