用Python从零实现Grid World强化学习环境(附完整代码)
用Python从零构建Grid World:一个强化学习环境的工程化实践
如果你对强化学习感兴趣,可能已经看过不少关于马尔可夫决策过程、贝尔曼方程的理论推导。但真正想理解一个智能体如何“学会”决策,最直接的方式就是亲手搭建一个环境,看着代码一步步运行起来。Grid World,这个经典的网格世界环境,就像是强化学习领域的“Hello World”——它足够简单,能让你聚焦于核心概念,又足够丰富,能容纳策略迭代、值迭代等关键算法。今天,我们不谈复杂的数学公式,而是直接打开编辑器,用Python和NumPy,从第一行代码开始,构建一个模块化、可扩展的Grid World环境。这个过程本身,就是一次绝佳的工程思维训练。
1. 环境蓝图:定义世界的规则与骨架
在敲下任何一行与环境交互的代码之前,我们必须像建筑师绘制蓝图一样,清晰地定义这个网格世界的所有规则。一个设计良好的环境类,应该将世界的静态属性(如大小、特殊格子)与动态规则(状态转移、奖励)清晰地分离开来,这不仅利于代码维护,也方便我们后续测试不同的世界配置。
1.1 核心参数与世界的初始化
我们首先创建一个名为 GridWorld 的类。它的构造函数将接收所有定义这个世界的基本参数。这里的关键是使用类属性来封装状态,避免在后续的函数中传递大量参数。
import numpy as np
from typing import Tuple, Dict, List
class GridWorld:
def __init__(self, size: int = 5,
special_cells: Dict[Tuple[int, int], Dict] = None,
discount_factor: float = 0.9):
"""
初始化Grid World环境。
参数:
size: 网格世界的边长,默认为5,即5x5网格。
special_cells: 一个字典,定义特殊格子及其行为。
格式: {(row, col): {'jump_to': (row', col'), 'reward': value}}
discount_factor: 强化学习中的折扣因子γ。
"""
self.size = size
self.discount = discount_factor
# 定义动作空间:上(0), 右(1), 下(2), 左(3)
self.actions = [0, 1, 2, 3]
self.action_names = ['Up', 'Right', 'Down', 'Left']
# 动作对应的行列坐标变化量 (d_row, d_col)
self.action_deltas = [(-1, 0), (0, 1), (1, 0), (0, -1)]
# 初始化特殊格子。如果未提供,则使用经典Grid World的A和B格子。
if special_cells is None:
self.special_cells = {
(0, 1): {'jump_to': (4, 1), 'reward': 10.0}, # 格子A
(0, 3): {'jump_to': (2, 3), 'reward': 5.0}, # 格子B
}
else:
self.special_cells = special_cells
# 初始化状态值函数V(s),全零矩阵
self.state_values = np.zeros((self.size, self.size))
# 预计算每个状态-动作对的下一个状态和即时奖励,提升运行时效率
self._precompute_transitions()
提示:将动作定义为整数索引而非字符串(如用0代表‘上’),能显著提升后续在NumPy数组中进行批量计算的速度。
_precompute_transitions这个私有方法是我们性能优化的关键一步。
1.2 预计算状态转移与奖励
在强化学习的迭代过程中,我们需要反复查询“在某个格子执行某个动作后,会去到哪、得到多少奖励”。与其在每次查询时都进行边界检查和特殊格子判断,不如在初始化时一次性计算并存储所有结果。这是一种典型的“空间换时间”策略。
def _precompute_transitions(self):
"""预计算所有状态-动作对的下一个状态和即时奖励。"""
# 使用两个字典来存储,键为(state, action),值为(next_state, reward)
self.transition_cache = {}
self.reward_cache = {}
for row in range(self.size):
for col in range(self.size):
current_state = (row, col)
for action_idx, (d_row, d_col) in enumerate(self.action_deltas):
next_row = row + d_row
next_col = col + d_col
# 处理特殊格子逻辑:在A或B格,无论什么动作都跳转并得到固定奖励
if current_state in self.special_cells:
spec = self.special_cells[current_state]
next_state = spec['jump_to']
reward = spec['reward']
else:
# 处理边界碰撞:试图走出网格则留在原地,并得到-1惩罚
if not (0 <= next_row < self.size and 0 <= next_col < self.size):
next_state = current_state
reward = -1.0
else:
next_state = (next_row, next_col)
reward = 0.0
# 缓存结果
self.transition_cache[(current_state, action_idx)] = next_state
self.reward_cache[(current_state, action_idx)] = reward
现在,我们环境的基础骨架已经搭建完毕。通过 self.step(state, action) 方法,我们可以快速获得转移结果:
def step(self, state: Tuple[int, int], action: int) -> Tuple[Tuple[int, int], float]:
"""
执行一步动作。
返回:
next_state: 下一个状态坐标 (row, col)
reward: 获得的即时奖励
"""
return (self.transition_cache[(state, action)],
self.reward_cache[(state, action)])
这种设计模式——在初始化时完成所有确定性计算并缓存——是构建高效模拟环境的一个常用技巧。当你的环境状态空间很大或需要被查询数百万次时(例如在深度强化学习训练中),这能带来巨大的性能提升。
2. 可视化引擎:让学习过程“看得见”
强化学习算法常常需要迭代成千上万次,如果只是盯着终端里滚动的数字,我们很难直观理解智能体到底学到了什么。一个强大的可视化模块不仅能帮助我们调试代码,更能直观展示值函数收敛、策略形成的过程。我们将使用Matplotlib来创建动态更新的网格图。
2.1 绘制静态值函数热图
首先,我们实现一个绘制当前状态值函数 self.state_values 的方法。每个格子的值将用颜色深浅和数字两种形式呈现,特殊格子会有特殊标记。
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.colors import LinearSegmentedColormap
class GridWorldVisualizer:
def __init__(self, grid_world):
self.env = grid_world
self.fig, self.ax = plt.subplots(figsize=(8, 8))
self.ax.set_axis_off()
# 创建一个从浅蓝到深蓝的渐变色系,用于表示值函数大小
colors = [(0.9, 0.95, 1.0), (0.1, 0.2, 0.6)] # 浅蓝 -> 深蓝
self.cmap = LinearSegmentedColormap.from_list('value_cmap', colors, N=100)
def draw_values(self, values: np.ndarray, title: str = "State Values"):
"""绘制值函数网格图。"""
self.ax.clear()
self.ax.set_axis_off()
self.ax.set_title(title, fontsize=16, pad=20)
# 绘制网格背景和数值
for i in range(self.env.size):
for j in range(self.env.size):
# 计算格子中心坐标
x, y = j, self.env.size - 1 - i # 调整y轴,使(0,0)在左上角
value = values[i, j]
# 根据值的大小确定颜色
norm_value = (value - values.min()) / (values.max() - values.min() + 1e-8)
cell_color = self.cmap(norm_value)
# 绘制矩形格子
rect = patches.Rectangle((x, y), 1, 1, linewidth=2,
edgecolor='black', facecolor=cell_color)
self.ax.add_patch(rect)
# 在格子中心添加数值文本
text_color = 'white' if norm_value > 0.7 else 'black'
self.ax.text(x + 0.5, y + 0.5, f'{value:.2f}',
ha='center', va='center', fontsize=12, color=text_color)
# 标记特殊格子
state = (i, j)
if state in self.env.special_cells:
spec = self.env.special_cells[state]
label = 'A' if spec['reward'] == 10.0 else 'B'
self.ax.text(x + 0.1, y + 0.9, label,
fontsize=14, weight='bold', color='darkred')
# 设置坐标轴范围
self.ax.set_xlim(0, self.env.size)
self.ax.set_ylim(0, self.env.size)
self.ax.set_aspect('equal')
# 添加网格线
self.ax.set_xticks(np.arange(self.env.size + 1))
self.ax.set_yticks(np.arange(self.env.size + 1))
self.ax.grid(which='both', color='gray', linestyle='-', linewidth=0.5, alpha=0.5)
plt.tight_layout()
调用这个方法,你就能得到一张清晰的值函数热图。但静态图还不够,我们更希望看到值函数随着迭代逐步更新的动态过程。
2.2 创建动态更新动画
为了实时观察算法收敛过程,我们可以利用Matplotlib的动画模块。下面的代码展示了如何将值迭代过程可视化为一个动态更新的动画。
from matplotlib.animation import FuncAnimation
def create_value_iteration_animation(env, max_iterations=50, save_path=None):
"""
创建并展示值迭代过程的动画。
参数:
env: GridWorld环境实例
max_iterations: 最大迭代次数
save_path: 如需保存为GIF,提供文件路径
"""
fig, ax = plt.subplots(figsize=(8, 8))
visualizer = GridWorldVisualizer(env)
# 存储每一轮迭代的值函数用于动画帧
value_history = [env.state_values.copy()]
# 执行值迭代并记录历史
for _ in range(max_iterations):
new_values = np.zeros_like(env.state_values)
for i in range(env.size):
for j in range(env.size):
state = (i, j)
# 对每个动作计算Q值
action_values = []
for action in env.actions:
next_state, reward = env.step(state, action)
next_i, next_j = next_state
# Q(s,a) = R(s,a) + γ * V(s')
q_value = reward + env.discount * env.state_values[next_i, next_j]
action_values.append(q_value)
# 值迭代:V(s) = max_a Q(s,a)
new_values[i, j] = np.max(action_values)
# 检查收敛
if np.max(np.abs(new_values - env.state_values)) < 1e-4:
print(f"值迭代在 {len(value_history)} 次迭代后收敛。")
break
env.state_values = new_values.copy()
value_history.append(new_values.copy())
# 创建动画更新函数
def update(frame):
ax.clear()
visualizer.draw_values(value_history[frame],
title=f"Value Iteration - Step {frame}")
return ax,
# 生成动画
anim = FuncAnimation(fig, update, frames=len(value_history),
interval=500, blit=False, repeat_delay=2000)
if save_path:
anim.save(save_path, writer='pillow', fps=2)
plt.show()
return anim
运行这段代码,你会看到一个动态变化的值函数图。最初,所有格子的值都是0(或初始值),随着迭代进行,靠近高奖励特殊格子(A和B)的格子值会首先上升,然后这种“价值”像涟漪一样逐渐扩散到整个网格。这种视觉反馈能让你立刻理解值迭代的本质:通过局部最优选择的不断传播,最终达到全局最优。
3. 算法核心:实现策略评估与优化
有了环境和可视化工具,我们现在可以深入算法的核心部分。在强化学习中,我们通常面临两个基本问题:1) 给定一个策略,如何评估它的好坏(策略评估)?2) 如何找到一个最优策略(策略优化)?我们将分别用代码实现这两个过程。
3.1 策略评估:计算给定策略下的值函数
假设我们有一个策略π(a|s),它定义了在状态s下选择动作a的概率。策略评估的目标是计算这个策略下的状态值函数Vπ(s)。我们采用迭代策略评估的方法,即反复应用贝尔曼期望方程直到收敛。
def policy_evaluation(env, policy, theta=1e-4, max_iterations=1000):
"""
迭代策略评估算法。
参数:
env: GridWorld环境实例
policy: 策略函数,输入状态,返回动作概率分布(长度为4的数组)
theta: 收敛阈值
max_iterations: 最大迭代次数
返回:
V: 收敛后的状态值函数
iteration: 实际迭代次数
"""
V = np.zeros((env.size, env.size))
for iteration in range(max_iterations):
delta = 0 # 记录本轮迭代中值函数的最大变化量
V_new = np.zeros_like(V)
# 遍历所有状态
for i in range(env.size):
for j in range(env.size):
state = (i, j)
v_new = 0
# 对每个可能的动作,根据策略概率加权计算期望值
for action in env.actions:
next_state, reward = env.step(state, action)
next_i, next_j = next_state
# 贝尔曼期望方程
v_new += policy(state)[action] * (reward + env.discount * V[next_i, next_j])
V_new[i, j] = v_new
delta = max(delta, abs(v_new - V[i, j]))
V = V_new.copy()
# 检查收敛
if delta < theta:
print(f"策略评估在 {iteration + 1} 次迭代后收敛,最大变化量: {delta:.6f}")
break
return V, iteration + 1
让我们定义一个简单的策略来测试这个函数:随机策略,即在每个状态下以均等概率选择四个方向之一。
def random_policy(state):
"""随机策略:每个动作被选中的概率均为0.25。"""
return np.ones(4) / 4.0
# 使用示例
env = GridWorld(size=5)
V_random, iters = policy_evaluation(env, random_policy)
print(f"随机策略下的状态值函数:\n{np.round(V_random, 2)}")
运行这段代码,你会得到一个5x5的矩阵,它量化了在随机游走策略下,每个格子的“长期期望回报”。靠近高奖励格子A和B的位置会有较高的值,而边缘格子因为容易撞墙受罚,值会略低。
3.2 策略迭代:在评估与改进间循环
策略迭代是强化学习中寻找最优策略的经典算法。它交替进行两个步骤:1) 策略评估(如上所述);2) 策略改进,即根据当前值函数,在每个状态选择能带来最大期望回报的动作。
def policy_iteration(env, initial_policy=None, eval_theta=1e-4, max_policy_iters=100):
"""
策略迭代算法。
参数:
env: GridWorld环境实例
initial_policy: 初始策略函数,若为None则使用随机策略
eval_theta: 策略评估的收敛阈值
max_policy_iters: 最大策略迭代次数
返回:
optimal_policy: 最优策略函数
V_star: 最优值函数
history: 记录每次迭代的策略和值函数(用于分析)
"""
# 初始化策略
if initial_policy is None:
# 初始策略为随机策略
current_policy = lambda s: np.ones(4) / 4.0
else:
current_policy = initial_policy
history = {'policies': [], 'values': []}
for policy_iter in range(max_policy_iters):
print(f"\n策略迭代第 {policy_iter + 1} 轮:")
# 步骤1: 策略评估 - 计算当前策略下的值函数
V, eval_iters = policy_evaluation(env, current_policy, theta=eval_theta)
history['values'].append(V.copy())
# 步骤2: 策略改进 - 基于当前值函数改进策略
policy_stable = True # 假设策略已稳定
new_policy_funcs = {} # 存储每个状态的新策略
for i in range(env.size):
for j in range(env.size):
state = (i, j)
# 计算每个动作的Q值
action_values = []
for action in env.actions:
next_state, reward = env.step(state, action)
next_i, next_j = next_state
q_value = reward + env.discount * V[next_i, next_j]
action_values.append(q_value)
# 找出具有最大Q值的动作
max_q = np.max(action_values)
best_actions = [a for a, q in enumerate(action_values) if q == max_q]
# 创建新策略:均匀分布在所有最优动作上
new_policy = np.zeros(4)
for a in best_actions:
new_policy[a] = 1.0 / len(best_actions)
# 检查策略是否发生变化
old_policy = current_policy(state)
if not np.allclose(new_policy, old_policy):
policy_stable = False
# 存储新策略(使用状态元组作为键)
new_policy_funcs[state] = new_policy
# 将新策略字典转换为函数
def make_policy_func(policy_dict):
return lambda state: policy_dict[state]
current_policy = make_policy_func(new_policy_funcs)
history['policies'].append(new_policy_funcs.copy())
# 如果策略不再变化,则已找到最优策略
if policy_stable:
print(f"策略在 {policy_iter + 1} 轮迭代后收敛至最优。")
break
return current_policy, V, history
为了直观展示策略迭代的效果,我们可以将最终的最优策略用箭头可视化出来:
def visualize_policy(env, policy, title="Optimal Policy"):
"""用箭头可视化策略。"""
fig, ax = plt.subplots(figsize=(8, 8))
ax.set_axis_off()
ax.set_title(title, fontsize=16, pad=20)
# 箭头长度和样式
arrow_length = 0.3
arrow_style = dict(arrowstyle="->", color="darkred", lw=2)
for i in range(env.size):
for j in range(env.size):
state = (i, j)
x, y = j + 0.5, env.size - 1 - i + 0.5 # 格子中心坐标
# 获取该状态的策略分布
action_probs = policy(state)
# 为概率大于0的动作绘制箭头
for action_idx, prob in enumerate(action_probs):
if prob > 0:
dx, dy = env.action_deltas[action_idx]
# 根据概率调整箭头长度和透明度
scale = arrow_length * (0.5 + prob * 0.5)
alpha = 0.3 + prob * 0.7
ax.annotate("", xy=(x + dx*scale, y + dy*scale),
xytext=(x, y), arrowprops={**arrow_style, 'alpha': alpha})
# 绘制网格
for i in range(env.size + 1):
ax.axhline(i, color='gray', linestyle='-', linewidth=0.5, alpha=0.5)
ax.axvline(i, color='gray', linestyle='-', linewidth=0.5, alpha=0.5)
ax.set_xlim(0, env.size)
ax.set_ylim(0, env.size)
ax.set_aspect('equal')
plt.tight_layout()
plt.show()
# 运行策略迭代并可视化
env = GridWorld()
optimal_policy, V_opt, history = policy_iteration(env)
visualize_policy(env, optimal_policy)
运行这段代码,你会看到一张带有箭头的网格图。在大多数格子上,箭头会指向一个明确的方向(如朝向A或B格),这表示最优策略是确定性的。但在某些特殊位置(如两个动作能带来相同期望回报时),你可能会看到多个箭头,这表示最优策略在这些状态下允许随机选择。
4. 值迭代:更高效的优化算法
策略迭代虽然直观,但每一轮都需要进行完整的策略评估(可能需多次迭代),计算成本较高。值迭代是一种更直接的方法,它将策略改进融入值函数更新中,通常收敛更快。
4.1 值迭代算法实现
值迭代的核心思想是直接应用贝尔曼最优方程进行迭代,而不显式地维护一个策略。
def value_iteration(env, theta=1e-4, max_iterations=1000):
"""
值迭代算法。
参数:
env: GridWorld环境实例
theta: 收敛阈值
max_iterations: 最大迭代次数
返回:
V_star: 最优值函数
optimal_policy: 从最优值函数推导出的最优策略
iterations: 实际迭代次数
"""
V = np.zeros((env.size, env.size))
for iteration in range(max_iterations):
delta = 0
V_new = np.zeros_like(V)
for i in range(env.size):
for j in range(env.size):
state = (i, j)
# 计算每个动作的Q值
action_values = []
for action in env.actions:
next_state, reward = env.step(state, action)
next_i, next_j = next_state
q_value = reward + env.discount * V[next_i, next_j]
action_values.append(q_value)
# 贝尔曼最优方程:V(s) = max_a Q(s,a)
best_value = np.max(action_values)
V_new[i, j] = best_value
delta = max(delta, abs(best_value - V[i, j]))
V = V_new.copy()
# 可选:每10次迭代打印一次进度
if iteration % 10 == 0:
print(f"迭代 {iteration}: 最大变化量 = {delta:.6f}")
if delta < theta:
print(f"\n值迭代在 {iteration + 1} 次迭代后收敛。")
break
# 从最优值函数推导出确定性最优策略
optimal_policy_dict = {}
for i in range(env.size):
for j in range(env.size):
state = (i, j)
action_values = []
for action in env.actions:
next_state, reward = env.step(state, action)
next_i, next_j = next_state
q_value = reward + env.discount * V[next_i, next_j]
action_values.append(q_value)
# 找出最优动作
best_actions = np.where(np.isclose(action_values, np.max(action_values)))[0]
policy = np.zeros(4)
for a in best_actions:
policy[a] = 1.0 / len(best_actions)
optimal_policy_dict[state] = policy
# 将策略字典转换为函数
def optimal_policy(state):
return optimal_policy_dict[state]
return V, optimal_policy, iteration + 1
让我们比较一下值迭代和策略迭代的性能:
# 性能对比测试
import time
env = GridWorld()
print("=" * 50)
print("策略迭代测试:")
print("=" * 50)
start_time = time.time()
opt_policy_pi, V_pi, hist_pi = policy_iteration(env, max_policy_iters=10)
pi_time = time.time() - start_time
print(f"策略迭代耗时: {pi_time:.3f} 秒")
print("\n" + "=" * 50)
print("值迭代测试:")
print("=" * 50)
start_time = time.time()
V_vi, opt_policy_vi, vi_iters = value_iteration(env, theta=1e-4)
vi_time = time.time() - start_time
print(f"值迭代耗时: {vi_time:.3f} 秒")
print(f"值迭代次数: {vi_iters}")
# 检查两种方法得到的最优值函数是否一致
max_diff = np.max(np.abs(V_pi - V_vi))
print(f"\n两种方法得到的最优值函数最大差异: {max_diff:.6f}")
if max_diff < 1e-4:
print("✓ 两种方法收敛到相同的最优值函数。")
else:
print("⚠ 两种方法的结果存在差异。")
在我的测试中,值迭代通常比策略迭代更快收敛,这是因为值迭代每次更新都直接朝着最优值函数前进,而策略迭代需要在策略评估和策略改进之间切换。但值得注意的是,策略迭代每次迭代的计算量可能更大,因为它需要多次扫描状态空间进行策略评估。
4.2 算法扩展:异步动态规划
上面实现的算法都是同步更新——我们计算所有状态的V_new,然后一次性替换旧的V。这种方法简单但内存效率不高。在实际问题中,状态空间可能非常大,我们无法同时存储两套值函数。异步动态规划允许我们就地更新值函数,即用新计算的值立即覆盖旧值。
def in_place_value_iteration(env, theta=1e-4, max_iterations=1000):
"""
就地值迭代算法(异步更新)。
注意:遍历状态的顺序会影响收敛速度!
"""
V = np.zeros((env.size, env.size))
for iteration in range(max_iterations):
delta = 0
# 按行优先顺序遍历所有状态
for i in range(env.size):
for j in range(env.size):
old_value = V[i, j]
# 计算该状态所有动作的Q值
action_values = []
for action in env.actions:
next_state, reward = env.step((i, j), action)
next_i, next_j = next_state
q_value = reward + env.discount * V[next_i, next_j]
action_values.append(q_value)
# 就地更新
V[i, j] = np.max(action_values)
delta = max(delta, abs(V[i, j] - old_value))
if delta < theta:
print(f"就地值迭代在 {iteration + 1} 次迭代后收敛。")
break
return V, iteration + 1
异步更新的一个有趣特性是,更新顺序会影响收敛速度。例如,如果我们从高奖励格子开始更新,信息会更快传播到整个网格。这引出了高斯-赛德尔风格的值迭代,它是一种更高效的异步更新方法。
5. 工程实践:模块化设计与扩展性思考
当我们完成了基本算法的实现后,接下来要考虑的是如何将这些代码组织成可维护、可扩展的工程化项目。一个好的强化学习环境应该像乐高积木一样,允许我们轻松更换组件、添加新功能。
5.1 创建可配置的环境工厂
与其硬编码Grid World的大小和特殊格子,不如创建一个工厂函数,允许我们动态生成不同配置的环境。
def create_grid_world(config_name="classic"):
"""
根据配置名称创建不同的Grid World环境。
支持的配置:
- "classic": 经典的5x5网格,包含A和B特殊格子
- "large": 10x10大网格,包含更多特殊格子
- "cliff": 悬崖行走环境(简化版)
- "custom": 完全自定义配置
"""
if config_name == "classic":
return GridWorld(size=5)
elif config_name == "large":
# 10x10网格,添加更多特殊传送门
special_cells = {
(0, 2): {'jump_to': (9, 2), 'reward': 10.0},
(0, 7): {'jump_to': (5, 7), 'reward': 5.0},
(4, 4): {'jump_to': (7, 8), 'reward': 3.0},
(9, 9): {'jump_to': (0, 0), 'reward': -2.0}, # 惩罚性传送
}
return GridWorld(size=10, special_cells=special_cells)
elif config_name == "cliff":
# 悬崖行走环境:底部一行除了起点和终点都是悬崖
size = 4
special_cells = {}
# 将(3,1)和(3,2)设为悬崖,掉下去得-100奖励并回到起点
for col in [1, 2]:
special_cells[(3, col)] = {'jump_to': (3, 0), 'reward': -100.0}
# 终点(3,3)给予正奖励
special_cells[(3, 3)] = {'jump_to': (3, 3), 'reward': 10.0}
env = GridWorld(size=size, special_cells=special_cells)
# 覆盖边界惩罚,使悬崖环境更合理
env._precompute_transitions() # 重新计算转移
return env
else:
raise ValueError(f"未知的配置名称: {config_name}")
# 使用示例
envs = {}
for config in ["classic", "large", "cliff"]:
envs[config] = create_grid_world(config)
print(f"创建了 {config} 环境: {envs[config].size}x{envs[config].size} 网格")
5.2 添加随机性与不确定性
到目前为止,我们的环境都是确定性的:给定状态和动作,下一个状态和奖励完全确定。但现实世界充满不确定性。我们可以扩展环境,引入随机转移概率。
class StochasticGridWorld(GridWorld):
"""带有随机转移概率的Grid World变体。"""
def __init__(self, size=5, slip_probability=0.1, **kwargs):
"""
参数:
slip_probability: 执行动作时"打滑"的概率,即实际执行其他随机动作的概率
"""
super().__init__(size, **kwargs)
self.slip_prob = slip_probability
# 重新定义动作执行逻辑,考虑打滑
self._precompute_stochastic_transitions()
def _precompute_stochastic_transitions(self):
"""预计算考虑随机性的状态转移。"""
# 对于确定性环境,我们只需要存储每个(state,action)的结果
# 但对于随机环境,我们需要存储每个(state,action)可能的所有结果及其概率
self.stochastic_transitions = {}
for row in range(self.size):
for col in range(self.size):
state = (row, col)
for intended_action in self.actions:
# 存储可能的结果列表,每个元素为(概率, 下一个状态, 奖励)
outcomes = []
# 情况1: 以(1-slip_prob)概率执行预期动作
next_state, reward = self.step(state, intended_action)
outcomes.append((1.0 - self.slip_prob, next_state, reward))
# 情况2: 以slip_prob概率随机执行其他动作
other_actions = [a for a in self.actions if a != intended_action]
prob_each = self.slip_prob / len(other_actions)
for actual_action in other_actions:
next_state, reward = self.step(state, actual_action)
outcomes.append((prob_each, next_state, reward))
self.stochastic_transitions[(state, intended_action)] = outcomes
def stochastic_step(self, state, action):
"""
执行动作,考虑随机性。
返回:
next_state: 实际到达的下一个状态
reward: 获得的即时奖励
"""
outcomes = self.stochastic_transitions[(state, action)]
# 根据概率随机选择一个结果
probs = [p for p, _, _ in outcomes]
idx = np.random.choice(len(outcomes), p=probs)
_, next_state, reward = outcomes[idx]
return next_state, reward
引入随机性后,我们的算法需要相应调整。例如,在策略评估中,我们需要考虑所有可能的结果而不仅仅是预期结果:
def stochastic_policy_evaluation(env, policy, theta=1e-4):
"""适用于随机环境的策略评估。"""
V = np.zeros((env.size, env.size))
while True:
delta = 0
for i in range(env.size):
for j in range(env.size):
state = (i, j)
v_new = 0
# 对每个可能执行的动作(根据策略概率)
action_probs = policy(state)
for action, prob_action in enumerate(action_probs):
if prob_action == 0:
continue
# 获取该动作所有可能的结果
outcomes = env.stochastic_transitions[(state, action)]
# 对每个可能的结果
for prob_outcome, next_state, reward in outcomes:
next_i, next_j = next_state
# 总概率 = 选择该动作的概率 × 得到该结果的概率
total_prob = prob_action * prob_outcome
v_new += total_prob * (reward + env.discount * V[next_i, next_j])
delta = max(delta, abs(v_new - V[i, j]))
V[i, j] = v_new
if delta < theta:
break
return V
5.3 性能优化技巧
当状态空间增大时,上述双重甚至三重循环的效率会成为瓶颈。以下是一些性能优化技巧:
1. 向量化计算
def vectorized_value_iteration(env, theta=1e-4):
"""使用NumPy向量化操作加速的值迭代。"""
V = np.zeros((env.size, env.size))
# 预计算所有状态-动作对的奖励和下一状态索引
# 这里需要将状态坐标转换为线性索引以便向量化操作
state_count = env.size * env.size
action_count = len(env.actions)
# 创建R和P矩阵:R[s,a]是奖励,P[s,a]是下一状态的线性索引
R = np.zeros((state_count, action_count))
P = np.zeros((state_count, action_count), dtype=int)
for i in range(env.size):
for j in range(env.size):
s = i * env.size + j # 线性索引
for a in env.actions:
next_state, reward = env.step((i, j), a)
next_i, next_j = next_state
s_next = next_i * env.size + next_j
R[s, a] = reward
P[s, a] = s_next
# 向量化值迭代
while True:
# Q[s,a] = R[s,a] + γ * V[P[s,a]]
Q = R + env.discount * V.flat[P]
V_new_flat = np.max(Q, axis=1)
delta = np.max(np.abs(V_new_flat - V.flat))
V = V_new_flat.reshape((env.size, env.size))
if delta < theta:
break
return V
2. 使用Numba JIT编译 对于特别大的状态空间,可以使用Numba将关键循环编译为机器码:
# 注意:这需要安装numba库
from numba import jit
@jit(nopython=True)
def value_iteration_numba(size, discount, R, P, theta=1e-4):
"""使用Numba加速的值迭代核心循环。"""
state_count = size * size
V = np.zeros(state_count)
while True:
V_new = np.zeros(state_count)
delta = 0.0
for s in range(state_count):
max_q = -np.inf
for a in range(4): # 4个动作
q = R[s, a] + discount * V[P[s, a]]
if q > max_q:
max_q = q
V_new[s] = max_q
delta = max(delta, abs(V_new[s] - V[s]))
V = V_new.copy()
if delta < theta:
break
return V
在实际项目中,我通常先实现清晰易懂的版本进行原型验证,然后在性能确实成为瓶颈时,再考虑引入向量化或JIT编译优化。过早优化往往会让代码变得难以理解和维护。
6. 从Grid World到真实问题:思维迁移
虽然Grid World是一个简化环境,但其中包含的概念和算法可以直接迁移到更复杂的实际问题中。理解这种迁移的关键是识别不同问题之间的共性模式。
6.1 识别问题中的MDP元素
任何可以建模为马尔可夫决策过程的问题,都可以用我们刚刚实现的算法框架来解决。关键在于识别以下元素:
| MDP元素 | Grid World示例 | 真实问题示例(如机器人导航) |
|---|---|---|
| 状态 (State) | 网格坐标 (i, j) | 机器人位置、传感器读数、电量 |
| 动作 (Action) | 上、下、左、右 | 前进、后退、转向、停止 |
| 转移函数 (Transition) | 确定性移动或随机打滑 | 物理动力学模型+传感器噪声 |
| 奖励函数 (Reward) | 到达特殊格子得正分,撞墙得负分 | 到达目标得正分,碰撞得负分,耗电得小负分 |
| 折扣因子 (γ) | 0.9(未来奖励的衰减率) | 0.99(更重视长期回报) |
一旦你将实际问题抽象成这个框架,就可以直接应用值迭代或策略迭代算法。例如,在机器人导航中,你可以将环境离散化为网格(就像Grid World),或者使用函数逼近方法(如神经网络)来处理连续状态空间。
6.2 扩展学习:从表格方法到函数逼近
我们目前实现的方法属于表格型方法,即为每个状态存储一个值。这在状态空间较小时很有效,但当状态空间很大或连续时,这种方法就不适用了。这时需要函数逼近方法,即用一个参数化函数(如线性模型、神经网络)来近似值函数。
# 伪代码:使用神经网络近似值函数
import torch
import torch.nn as nn
class ValueNetwork(nn.Module):
"""用神经网络近似状态值函数。"""
def __init__(self, input_dim, hidden_dims=[64, 32]):
super().__init__()
layers = []
prev_dim = input_dim
for hidden_dim in hidden_dims:
layers.append(nn.Linear(prev_dim, hidden_dim))
layers.append(nn.ReLU())
prev_dim = hidden_dim
layers.append(nn.Linear(prev_dim, 1)) # 输出单个值:V(s)
self.net = nn.Sequential(*layers)
def forward(self, state):
# state: 状态特征向量
return self.net(state).squeeze(-1)
# 使用示例:将Grid World状态转换为特征向量
def state_to_features(state, size):
"""将(i,j)状态转换为特征向量。"""
i, j = state
# 一种简单的特征化:one-hot编码行和列
features = np.zeros(2 * size)
features[i] = 1.0
features[size + j] = 1.0
return torch.FloatTensor(features)
# 训练循环(简化版)
def train_value_network(env, network, optimizer, num_episodes=1000):
"""使用TD学习训练值网络。"""
for episode in range(num_episodes):
# 随机初始状态
state = (np.random.randint(env.size), np.random.randint(env.size))
state_tensor = state_to_features(state, env.size)
# 选择动作(ε-贪婪策略)
if np.random.random() < 0.1: # 探索
action = np.random.choice(env.actions)
else: # 利用
# 计算每个动作的Q值(需要环境模型或从经验中学习)
# 这里简化处理
pass
# 执行动作,观察下一个状态和奖励
next_state, reward = env.step(state, action)
next_state_tensor = state_to_features(next_state, env.size)
# TD目标:r + γ * V(s')
with torch.no_grad():
target = reward + env.discount * network(next_state_tensor)
# 当前估计:V(s)
current = network(state_tensor)
# TD误差:目标 - 当前估计
loss = (target - current) ** 2
# 反向传播更新网络
optimizer.zero_grad()
loss.backward()
optimizer.step()
从表格方法到函数逼近是强化学习中的一个重要飞跃。虽然增加了复杂性,但它使得处理高维状态空间成为可能,这也是深度强化学习(如DQN、A3C等算法)的基础。
6.3 实用建议与常见陷阱
在实现自己的强化学习环境时,有几个容易踩坑的地方值得注意:
1. 奖励设计 奖励函数是引导智能体学习的关键。设计不当的奖励可能导致意外行为:
- 稀疏奖励问题:只在最终成功时给予正奖励,智能体很难学习。解决方案是设计塑形奖励,提供中间反馈。
- 奖励黑客:智能体可能找到绕过你意图的方法获得高奖励。比如,在一个收集金币的游戏中,如果智能体发现反复进出获得金币的区域比实际探索更高效,它就会卡在那里。
2. 折扣因子选择 折扣因子γ控制未来奖励的重要性:
- γ接近0:智能体极度短视,只关心即时奖励
- γ接近1:智能体非常有远见,但学习可能不稳定
- 在Grid World中,0.9是一个常用值;在长期任务中(如围棋),可能需要0.99或更高
3. 收敛判断 我们的代码使用值函数变化量小于阈值来判断收敛,但要注意:
- 阈值太小可能导致不必要的迭代
- 某些问题可能永远达不到严格收敛(特别是随机环境)
- 实践中,可以结合最大迭代次数和阈值双重判断
4. 随机种子 为了结果可复现,记得设置随机种子:
np.random.seed(42)
random.seed(42)
torch.manual_seed(42) # 如果使用PyTorch
最后,当你成功运行了Grid World的代码并看到智能体学会了最优策略时,那种成就感是阅读理论文章无法比拟的。我建议你在现有代码基础上尝试修改参数:调整网格大小、改变特殊格子的位置和奖励、增加随机性,然后观察算法的行为变化。这种亲手实验获得的直觉理解,比任何书本知识都更加深刻。
更多推荐


所有评论(0)