趋近智
首次访问蒙特卡洛预测算法的实现,旨在估计给定策略 的状态价值函数 。这种估计仅利用在环境中遵循该策略生成的样本片段。不需要了解环境的转移概率或奖励动态。
对于本练习,我们将使用通过 Gymnasium 库提供的经典二十一点环境。回顾上一节,蒙特卡洛预测通过平均多次片段中首次访问每个状态后观察到的回报来工作。
首先,请确保您已安装 Gymnasium (pip install gymnasium[classic_control])。二十一点环境 (Blackjack-v1) 模拟了这款流行的纸牌游戏。
(player_sum, dealer_showing_card, usable_ace)。
player_sum:玩家当前手牌点数总和(整数,介于 4 到 21 之间)。dealer_showing_card:庄家明牌的点数(整数,介于 1 到 10 之间,其中 1 代表 A)。usable_ace:玩家是否拥有一张可以计为 11 点而不会爆牌的 A(布尔值,0 或 1)。我们需要一个策略 来生成片段。我们将使用一个简单的、固定的策略,它常作为基准使用:
策略 : 如果玩家当前点数总和为 20 或 21,则停牌。否则,要牌。
这个策略是合理的但并非最优。我们的目标不是现在找到最佳策略,而是仅仅使用蒙特卡洛预测来评估这个特定的策略。
我们来概述步骤,然后用 Python 实现它们。
state_returns_sum) 以及每个状态的访问次数 (state_visit_count)。state_visit_count[S_t] += 1。state_returns_sum[S_t] += G。现在,我们将其转换为 Python 代码。
import gymnasium as gym
import numpy as np
from collections import defaultdict
import random # 仅在使策略随机化时使用,对于此固定策略不需要
# 初始化环境
env = gym.make('Blackjack-v1', sab=True) # sab=True 使用略有不同的规则,结果相似
# 定义简单策略:20 或 21 停牌,否则要牌。
def simple_policy(state):
"""
参数:
state:元组 (player_sum, dealer_showing, usable_ace)
返回:
action:0(停牌)或 1(要牌)
"""
player_sum, _, _ = state
if player_sum >= 20:
return 0 # 停牌
else:
return 1 # 要牌
# 蒙特卡洛预测算法(首次访问)
def mc_prediction_first_visit(policy, env, num_episodes, gamma=1.0):
"""
执行首次访问蒙特卡洛预测。
参数:
policy:一个接受状态并返回动作的函数。
env:OpenAI Gym 环境。
num_episodes:要生成的片段数量。
gamma:折扣因子。
返回:
V:一个将状态映射到价值估计的字典。
"""
# 初始化字典
# defaultdict 避免检查是否存在
state_returns_sum = defaultdict(float)
state_visit_count = defaultdict(int)
V = defaultdict(float) # 最终价值估计
for i_episode in range(num_episodes):
# 打印进度
if (i_episode + 1) % 10000 == 0:
print(f"\rEpisode {i_episode+1}/{num_episodes}", end="")
# 按照策略生成一个片段
episode = []
state, _ = env.reset() # 获取初始状态
terminated = False
truncated = False # 处理 gymnasium 可能的截断
while not terminated and not truncated:
action = policy(state)
next_state, reward, terminated, truncated, _ = env.step(action)
episode.append((state, reward)) # 存储状态和*下一个*奖励
state = next_state
# 为首次访问蒙特卡洛处理片段
states_in_episode = set([s for (s, r) in episode]) # 获取访问过的唯一状态
visited_in_episode = set() # 跟踪此片段中的首次访问
# 向后循环遍历片段
G = 0.0 # 初始化回报
# 片段存储 (S_t, R_{t+1}) 对。
# 我们向后迭代以计算 G_t = R_{t+1} + gamma*R_{t+2} + ...
for t in range(len(episode) - 1, -1, -1):
state, reward = episode[t]
G = gamma * G + reward
# 如果这是此片段中首次访问“state”
if state not in visited_in_episode:
visited_in_episode.add(state)
state_returns_sum[state] += G
state_visit_count[state] += 1
# 增量更新价值估计
V[state] = state_returns_sum[state] / state_visit_count[state]
print(f"\nFinished {num_episodes} episodes.")
return V
# 运行预测
num_episodes = 500000
V_simple_policy = mc_prediction_first_visit(simple_policy, env, num_episodes)
# 打印一些示例值(可选)
# 示例:状态 (18, 7, False) 的价值 - 玩家点数 18,庄家显示 7,无可用 A
print(f"\nExample state value V(18, 7, False): {V_simple_policy.get((18, 7, False), '未访问')}")
# 示例:状态 (21, 10, True) 的价值 - 玩家点数 21,庄家显示 10,有可用 A
print(f"Example state value V(21, 10, True): {V_simple_policy.get((21, 10, True), '未访问')}")
env.close()
simple_policy(state): 此函数实现我们固定的策略。它接受当前状态元组,如果玩家点数总和为 20 或 21,则返回 0(停牌),否则返回 1(要牌)。mc_prediction_first_visit(...):
defaultdict 初始化 state_returns_sum、state_visit_count 和 V。defaultdict(float) 将新键初始化为 0.0,defaultdict(int) 初始化为 0。num_episodes 次。env.step(policy(state)) 逐步生成一个片段。我们存储 (state, reward) 元组,其中 reward 是在 state 中之后收到的 。states_in_episode)。注意:此行对于逻辑并非严格必需,但对于调试或分析可能有用。visited_in_episode 集合对算法来说才是重要的。t = T-1, ..., 0)。if state not in visited_in_episode: 检查确保我们只处理该特定片段中状态首次出现时的回报。visited_in_episode,更新该状态的总回报和访问计数,并重新计算平均回报,即我们的估计值 。估计所有可能二十一点状态的值是有用的,但可视化它们能提供更多认识。由于状态空间是三维的 (player_sum, dealer_showing, usable_ace),我们可以通过固定一个维度来创建二维图。我们将针对有可用 A 和没有可用 A 的情况,分别绘制估计的状态值 与 player_sum 的关系图,同时保持 dealer_showing_card 不变(例如,庄家显示 7)。
import matplotlib.pyplot as plt
def plot_blackjack_values(V, dealer_card=7):
"""绘制 V(s) 与玩家点数总和关系的辅助函数"""
player_sums = np.arange(12, 22) # 相关的玩家点数总和
# 没有可用 A 的状态值
values_no_ace = [V.get((s, dealer_card, False), 0) for s in player_sums]
# 有可用 A 的状态值
values_usable_ace = [V.get((s, dealer_card, True), 0) for s in player_sums]
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(player_sums, values_no_ace, label='无可用 A', marker='o')
ax.plot(player_sums, values_usable_ace, label='有可用 A', marker='x')
ax.set_xlabel("玩家点数总和")
ax.set_ylabel("状态价值(估计的 V)")
ax.set_title(f"价值函数与玩家点数总和的关系(庄家显示 {dealer_card})")
ax.legend()
ax.grid(True)
plt.show()
# 运行预测后生成图
# 使用之前计算的 V_simple_policy
plot_blackjack_values(V_simple_policy, dealer_card=7)
plot_blackjack_values(V_simple_policy, dealer_card=2) # 另一个庄家牌的示例
这些图显示了在庄家显示特定牌(例如 7 或 2)时,不同玩家点数总和的估计价值(预期回报)。您通常会发现,较高的玩家点数总和通常具有较高的价值,并且拥有可用 A 通常会增加价值,特别是对于爆牌风险不高的较低点数总和。简单策略的效果(20/21 点停牌)应该反映在那些点数总和附近的值中。
这项实践练习展示了蒙特卡洛预测如何直接从经验中学习状态值,而无需环境模型。通过运行片段并平均每个状态观察到的回报,我们可以有效地估计 。这为蒙特卡洛控制方法奠定了基础,在这些方法中,我们将使用类似的想法来估计动作值()并寻找最优策略。
这部分内容有帮助吗?
© 2026 ApX Machine Learning用心打造