用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的代码并看到智能体学会了最优策略时,那种成就感是阅读理论文章无法比拟的。我建议你在现有代码基础上尝试修改参数:调整网格大小、改变特殊格子的位置和奖励、增加随机性,然后观察算法的行为变化。这种亲手实验获得的直觉理解,比任何书本知识都更加深刻。

更多推荐