理解Q学习Q-Learning完整指南:Python从零实现

🧠 向所有学习者致敬!

“学习不是装满一桶水,而是点燃一把火。” —— 叶芝


我的博客主页: https://lizheng.blog.csdn.net

🌐 欢迎点击加入AI人工智能社区

🚀 让我们一起努力,共创AI未来! 🚀


Q 学习Q-Learning是什么?

Q 学习Q-Learning是一种强化学习算法,能够让智能体通过不断试错,发现哪些动作能获得最高奖励,从而学会在任何给定情境下采取最优动作。它是一种无模型、基于价值的学习算法,也就是说,它不需要环境的模型就能从环境中学习。相反,它是通过在不同状态下采取动作后获得的奖励来学习的。

Q 学习中的 “Q” 代表 “质量”,本质上表示某个动作在获取未来奖励方面的有用程度。

Q 学习Q-Learning的应用场景和方式

Q 学习Q-Learning广泛应用于以下领域:

  1. 机器人技术:教机器人如何在环境中导航、抓取物体或完成任务。
  2. 游戏对战:创建能够掌握游戏(比如 Atari 游戏或棋类游戏)的智能体。
  3. 资源管理:优化诸如电梯控制或交通灯管理等系统中的决策。
  4. 推荐系统:学习用户偏好,以推荐产品或内容。
  5. 自动驾驶车辆:帮助自动驾驶汽车做出决策。

Q 学习特别适用于以下环境:

  • 环境的规则或模型未知。
  • 环境有明确的状态和动作。
  • 存在定义良好的奖励信号。
  • 环境是完全可观测的(智能体能够看到完整状态)。

Q 学习Q-Learning的数学基础

复杂的原始版本

Q 学习Q-Learning算法使用贝尔曼方程来更新 Q 值:

Q ( s t , a t ) ← Q ( s t , a t ) + α [ r t + γ max ⁡ a Q ( s t + 1 , a ) − Q ( s t , a t ) ] Q(s_t, a_t) \leftarrow Q(s_t, a_t) + \alpha \left[r_t + \gamma \max_{a} Q(s_{t+1}, a) - Q(s_t, a_t)\right] Q(st,at)Q(st,at)+α[rt+γamaxQ(st+1,a)Q(st,at)]

其中:

  • Q ( s t , a t ) Q(s_t, a_t) Q(st,at) 是状态 s t s_t st和动作 a t a_t at 的 Q 值。
  • α \alpha α 是学习率(0 < α \alpha α ≤ 1)。
  • r t r_t rt 是在状态 $\s_t$下采取动作 (a_t) 后获得的奖励。
  • γ \gamma γ是未来奖励的折扣因子(0 ≤ γ \gamma γ ≤ 1)。
  • max ⁡ a Q ( s t + 1 , a ) \max_{a} Q(s_{t+1}, a) maxaQ(st+1,a) 是下一个状态 s t + 1 s_{t+1} st+1 在所有可能动作中的最大 Q 值。
  • 括号内的项 [ r t + γ max ⁡ a Q ( s t + 1 , a ) − Q ( s t , a t ) ] [r_t + \gamma \max_{a} Q(s_{t+1}, a) - Q(s_t, a_t)] [rt+γmaxaQ(st+1,a)Q(st,at)]是时间差分(TD)误差。

简化版本

简单来说,Q 学习的更新可以理解为:

Q new = Q old + α [ R + γ max ⁡ Q future − Q old ] Q_{\text{new}} = Q_{\text{old}} + \alpha \left[ R + \gamma \max Q_{\text{future}} - Q_{\text{old}} \right] Qnew=Qold+α[R+γmaxQfutureQold]

或者更简单地:

Q new = Q old + α [ 目标 − Q old ] Q_{\text{new}} = Q_{\text{old}} + \alpha \left[ \text{目标} - Q_{\text{old}} \right] Qnew=Qold+α[目标Qold]

其中 “目标” 是奖励加上折扣后的未来值。

Q 学习的逐步解释

  1. 初始化 Q 表:创建一个表,行代表每个状态,列代表每个动作,初始值用零或随机值填充。

  2. 选择动作:在状态 s s s下,使用探索策略(如 epsilon-greedy)选择动作 a a a

  3. 执行动作:执行动作 a a a,观察奖励 r r r 和新状态 s ′ s' s

  4. 更新 Q 值:应用 Q 学习更新公式来调整状态 - 动作对的 Q Q Q 值。

  5. 转移到下一个状态:将当前状态设置为新状态 s ′ s' s

  6. 重复步骤 2-5:继续这个过程,直到达到终止状态或完成最大步数。

  7. 重复多个回合:运行多个回合,让智能体探索不同的状态 - 动作对并优化其 Q 值。

Q 学习的关键组成部分

Q 表

Q 表是一个查找表,其中:

  • 行代表环境中的状态。
  • 列代表可能的动作。
  • 每个单元格包含一个 Q 值,表示在该状态下采取该动作的预期未来奖励。

例如,在一个简单的网格世界中:

状态UpDownLeftRight
(0,0)0.00.00.00.0
(0,1)0.00.00.00.0

探索与利用

强化学习中的一个核心挑战是平衡:

  • 探索:尝试新动作以发现更好的奖励(冒险)。
  • 利用:使用智能体已知的知识来最大化奖励(保守)。

常用的 epsilon-greedy 策略用于平衡这两者:

  • 以概率 ϵ \epsilon ϵ 选择随机动作(探索)。
  • 以概率 1 − ϵ 1-\epsilon 1ϵ 选择具有最高 Q 值的动作(利用)。
  • ϵ \epsilon ϵ通常会随着时间的推移而减小,因为智能体对环境的了解越来越多。

学习率 ( α \alpha α)

  • 控制新信息覆盖旧信息的程度。
  • 高学习率(接近 1):学习速度快,但可能不稳定。
  • 低学习率(接近 0):学习速度慢,但更稳定。
  • 典型值:0.1 到 0.5

折扣因子 ((\gamma))

  • 决定未来奖励与即时奖励的相对重要性。
  • (\gamma = 0):智能体只考虑即时奖励(目光短浅)。
  • (\gamma = 1):智能体将未来奖励与即时奖励同等看待(目光长远)。
  • 典型值:0.9 到 0.99

实践示例:网格世界

在笔记本中的示例中,Q 学习被应用于一个网格世界,其中:

  • 智能体在 4×4 的网格中导航。
  • 终止状态位于 (0,0)(奖励为 1)和 (3,3)(奖励为 10)。
  • 其他所有状态的奖励为 0。
  • 智能体可以向上、向下、向左或向右移动。
  • 目标是学习通往最高奖励的最优路径。

随着智能体的探索,它更新其 Q 值,并逐渐学会通过环境中的最佳路径。可视化显示了智能体最初是如何随机探索的,最终又如何收敛到最优策略的。

设置环境

导入必要的库,包括用于数值运算的 NumPy 和用于可视化的 Matplotlib。

# 导入必要的库
import numpy as np  # 用于数值运算
import matplotlib.pyplot as plt  # 用于可视化

# 导入类型提示
from typing import List, Tuple, Dict, Optional

# 设置随机种子以确保结果可复现
np.random.seed(42)

# 为 Jupyter Notebook 启用内联绘图
%matplotlib inline

创建简单环境

为了创建一个适用于 Q 学习算法的简单环境,我们将定义一个 4×4 的网格世界。网格世界具有以下属性:

  • 4 行 4 列
  • 可能的动作:‘up’(向上)、‘down’(向下)、‘left’(向左)、‘right’(向右)
# 定义网格世界环境
def create_gridworld(
    rows: int, 
    cols: int, 
    terminal_states: List[Tuple[int, int]], 
    rewards: Dict[Tuple[int, int], int]
) -> Tuple[np.ndarray, List[Tuple[int, int]], List[str]]:
    """
    创建一个简单的网格世界环境。
    
    参数:
    - rows (int):网格的行数。
    - cols (int):网格的列数。
    - terminal_states (List[Tuple[int, int]]):终止状态列表,以 (行, 列) 元组形式表示。
    - rewards (Dict[Tuple[int, int], int]):字典,将 (行, 列) 映射到奖励值。
    
    返回值:
    - grid (np.ndarray):一个二维数组,表示带有奖励的网格。
    - state_space (List[Tuple[int, int]]):网格中所有可能状态的列表。
    - action_space (List[str]):可能动作的列表('up'、'down'、'left'、'right')。
    """
    # 初始化网格为零
    grid = np.zeros((rows, cols))
    
    # 为指定状态分配奖励
    for (row, col), reward in rewards.items():
        grid[row, col] = reward
    
    # 定义状态空间为所有可能的 (行, 列) 组合
    state_space = [
        (row, col) 
        for row in range(rows) 
        for col in range(cols)
    ]
    
    # 定义动作空间为四种可能的移动方式
    action_space = ['up', 'down', 'left', 'right']
    
    return grid, state_space, action_space

接下来,我们需要定义状态转换函数,该函数以当前状态和动作为输入,并返回下一个状态。可以将其视为智能体根据所采取的动作在网格中移动。

# 定义状态转换函数
def state_transition(state: Tuple[int, int], action: str, rows: int, cols: int) -> Tuple[int, int]:
    """
    根据当前状态和动作计算下一个状态。
    
    参数:
    - state (Tuple[int, int]):当前状态,以 (行, 列) 表示。
    - action (str):要采取的动作('up'、'down'、'left'、'right')。
    - rows (int):网格的行数。
    - cols (int):网格的列数。
    
    返回值:
    - Tuple[int, int]:采取动作后的结果状态 (行, 列)。
    """
    # 将当前状态解包为行和列
    row, col = state

    # 根据动作更新行或列,同时确保边界得到尊重
    if action == 'up' and row > 0:  # 如果不在最上面一行,则向上移动
        row -= 1
    elif action == 'down' and row < rows - 1:  # 如果不在最下面一行,则向下移动
        row += 1
    elif action == 'left' and col > 0:  # 如果不在最左边一列,则向左移动
        col -= 1
    elif action == 'right' and col < cols - 1:  # 如果不在最右边一列,则向右移动
        col += 1

    # 返回新状态作为元组
    return (row, col)

现在我们的智能体可以与环境进行交互了,接下来我们需要定义奖励函数。这个函数将返回给定状态的奖励,这将在训练过程中用于更新 Q 值。

# 定义奖励函数
def get_reward(state: Tuple[int, int], rewards: Dict[Tuple[int, int], int]) -> int:
    """
    获取给定状态的奖励。

    参数:
    - state (Tuple[int, int]):当前状态,以 (行, 列) 表示。
    - rewards (Dict[Tuple[int, int], int]):字典,将 (行, 列) 映射到奖励值。

    返回值:
    - int:给定状态的奖励。如果状态不在奖励字典中,则返回 0。
    """
    # 使用奖励字典获取给定状态的奖励。
    # 如果找不到该状态,则返回默认奖励 0。
    return rewards.get(state, 0)

现在我们已经定义了网格世界环境以及必要的辅助函数,让我们通过一个简单示例来测试它们。我们将创建一个 4×4 的网格,其中 (0, 0) 和 (3, 3) 为终止状态,分别具有奖励值 1 和 10。然后,我们将通过从状态 (2, 2) 向上移动来测试状态转换和奖励函数。

# 示例:使用网格世界环境

# 定义网格尺寸(4×4)、终止状态和奖励
rows, cols = 4, 4  # 网格的行数和列数
terminal_states = [(0, 0), (3, 3)]  # 具有奖励的终止状态
rewards = {(0, 0): 1, (3, 3): 10}  # 终止状态的奖励

# 创建网格世界环境
grid, state_space, action_space = create_gridworld(rows, cols, terminal_states, rewards)

# 测试状态转换和奖励函数
current_state = (2, 2)  # 初始状态
action = 'up'  # 要采取的动作
next_state = state_transition(current_state, action, rows, cols)  # 计算下一个状态
reward = get_reward(next_state, rewards)  # 获取下一个状态的奖励

# 打印结果
print("网格世界:")  # 显示带有奖励的网格
print(grid)
print(f"当前状态:{current_state}")  # 显示当前状态
print(f"采取的动作:{action}")  # 显示采取的动作
print(f"下一个状态:{next_state}")  # 显示结果下一个状态
print(f"奖励:{reward}")  # 显示下一个状态的奖励
网格世界:
[[ 1.  0.  0.  0.]
 [ 0.  0.  0.  0.]
 [ 0.  0.  0.  0.]
 [ 0.  0.  0. 10.]]
当前状态:(2, 2)
采取的动作:up
下一个状态:(1, 2)
奖励:0

你可以看到,已经按照指定的尺寸、终止状态和奖励创建了网格世界环境。我们随机选择了初始状态 (2, 2) 和动作 (‘up’)。计算出的下一个状态是 (1, 2),由于我们的终止状态字典中没有这个状态,所以奖励为 0。

实现 Q 学习算法

所以,我们已经成功地实现了网格世界环境以及状态转换和奖励函数。现在,我们可以使用这个环境来实现 Q 学习算法。我们首先需要初始化 Q 表,这是一个字典,将状态 - 动作对映射到 Q 值。Q 值表示在给定状态下采取特定动作时的预期累积奖励。

# 初始化 Q 表
def initialize_q_table(state_space: List[Tuple[int, int]], action_space: List[str]) -> Dict[Tuple[int, int], Dict[str, float]]:
    """
    用零初始化所有状态 - 动作对的 Q 表。

    参数:
    - state_space (List[Tuple[int, int]]):环境中所有可能状态的列表,以 (行, 列) 元组形式表示。
    - action_space (List[str]):可能动作的列表(例如,'up'、'down'、'left'、'right')。

    返回值:
    - q_table (Dict[Tuple[int, int], Dict[str, float]]):一个字典,其中每个状态映射到另一个字典。
      内部字典将每个动作映射到其对应的 Q 值,初始化为 0。
    """
    q_table: Dict[Tuple[int, int], Dict[str, float]] = {}
    for state in state_space:
        # 将所有动作在给定状态下的 Q 值初始化为 0
        q_table[state] = {action: 0.0 for action in action_space}
    return q_table

接下来,我们定义使用 epsilon-greedy 策略选择动作的函数。如果随机值小于 epsilon,我们选择随机动作,否则,我们选择当前状态下具有最高 Q 值的动作。

# 使用 epsilon-greedy 策略选择动作
def choose_action(state: Tuple[int, int], q_table: Dict[Tuple[int, int], Dict[str, float]], action_space: List[str], epsilon: float) -> str:
    """
    使用 epsilon-greedy 策略选择动作。

    参数:
    - state (Tuple[int, int]):当前状态,以 (行, 列) 表示。
    - q_table (Dict[Tuple[int, int], Dict[str, float]]):Q 表,将状态 - 动作对映射到 Q 值。
    - action_space (List[str]):可能动作的列表(例如,'up'、'down'、'left'、'right')。
    - epsilon (float):探索率(0 <= epsilon <= 1)。

    返回值:
    - str:所选择的动作。
    """
    # 以概率 epsilon 选择随机动作(探索)
    if np.random.rand() < epsilon:
        return np.random.choice(action_space)
    # 否则,选择当前状态下具有最高 Q 值的动作(利用)
    else:
        return max(q_table[state], key=q_table[state].get)

一旦我们采取了动作并观察到奖励和下一个状态,我们就可以使用 Q 学习更新规则来更新 Q 值。更新规则如下:

$ Q ( s , a ) = Q ( s , a ) + α ∗ [ R ( s ) + γ ∗ max ⁡ ( Q ( s ′ , a ′ ) ) − Q ( s , a ) ] Q(s, a) = Q(s, a) + \alpha * [R(s) + \gamma * \max(Q(s', a')) - Q(s, a)] Q(s,a)=Q(s,a)+α[R(s)+γmax(Q(s,a))Q(s,a)]$

# 更新 Q 值
def update_q_value(
    q_table: Dict[Tuple[int, int], Dict[str, float]], 
    state: Tuple[int, int], 
    action: str, 
    reward: int, 
    next_state: Tuple[int, int], 
    alpha: float, 
    gamma: float, 
    action_space: List[str]
) -> None:
    """
    使用 Q 学习更新规则更新 Q 值。

    参数:
    - q_table (Dict[Tuple[int, int], Dict[str, float]]):Q 表,将状态 - 动作对映射到 Q 值。
    - state (Tuple[int, int]):当前状态,以 (行, 列) 表示。
    - action (str):所采取的动作。
    - reward (int):所获得的奖励。
    - next_state (Tuple[int, int]):下一个状态,以 (行, 列) 表示。
    - alpha (float):学习率(0 < alpha <= 1)。
    - gamma (float):折扣因子(0 <= gamma <= 1)。
    - action_space (List[str]):可能动作的列表。

    返回值:
    - None:在原地更新 Q 表。
    """
    # 获取下一个状态在所有可能动作下的最大 Q 值
    max_next_q: float = max(q_table[next_state].values()) if next_state in q_table else 0.0

    # 使用 Q 学习公式更新当前状态 - 动作对的 Q 值
    q_table[state][action] += alpha * (reward + gamma * max_next_q - q_table[state][action])

到目前为止,我们已经定义了网格世界环境、状态转换函数、奖励函数以及 Q 学习更新规则。我们还实现了 Q 表的初始化、使用 epsilon-greedy 策略的动作选择以及 Q 值更新函数。现在,我们可以将所有内容整合在一起,在网格世界环境中运行多个回合的 Q 学习。

# 运行单个回合
def run_episode(
    q_table: Dict[Tuple[int, int], Dict[str, float]], 
    state_space: List[Tuple[int, int]], 
    action_space: List[str], 
    rewards: Dict[Tuple[int, int], int], 
    rows: int, 
    cols: int, 
    alpha: float, 
    gamma: float, 
    epsilon: float, 
    max_steps: int
) -> int:
    """
    运行 Q 学习的一个回合。

    参数:
    - q_table (Dict[Tuple[int, int], Dict[str, float]]):Q 表,将状态 - 动作对映射到 Q 值。
    - state_space (List[Tuple[int, int]]):环境中所有可能状态的列表。
    - action_space (List[str]):可能动作的列表(例如,'up'、'down'、'left'、'right')。
    - rewards (Dict[Tuple[int, int], int]):字典,将状态 (行, 列) 映射到奖励值。
    - rows (int):网格的行数。
    - cols (int):网格的列数。
    - alpha (float):学习率(0 < alpha <= 1)。
    - gamma (float):折扣因子(0 <= gamma <= 1)。
    - epsilon (float):探索率(0 <= epsilon <= 1)。
    - max_steps (int):每个回合允许的最大步数。

    返回值:
    - int:该回合积累的总奖励。
    """
    # 从随机状态开始
    state: Tuple[int, int] = state_space[np.random.choice(len(state_space))]
    total_reward: int = 0  # 初始化该回合的总奖励

    # 循环最多 max_steps 次
    for _ in range(max_steps):
        # 使用 epsilon-greedy 策略选择动作
        action: str = choose_action(state, q_table, action_space, epsilon)
        
        # 根据所选动作计算下一个状态
        next_state: Tuple[int, int] = state_transition(state, action, rows, cols)
        
        # 获取下一个状态的奖励
        reward: int = get_reward(next_state, rewards)
        
        # 更新当前状态 - 动作对的 Q 值
        update_q_value(q_table, state, action, reward, next_state, alpha, gamma, action_space)
        
        # 累积奖励
        total_reward += reward
        
        # 转移到下一个状态
        state = next_state
        
        # 检查智能体是否到达终止状态
        if state in terminal_states:
            break
    
    # 返回该回合积累的总奖励
    return total_reward

现在我们可以运行多个回合的 Q 学习,以在网格世界环境中训练智能体。我们将跟踪每个回合积累的总奖励,并使用折线图可视化随时间变化的奖励。

# 设置 Q 学习算法的超参数
alpha = 0.1  # 学习率:决定新信息覆盖旧信息的程度
gamma = 0.9  # 折扣因子:决定未来奖励的重要性
epsilon = 0.1  # 探索率:选择随机动作的概率(探索与利用)
max_steps = 100  # 每个回合允许的最大步数
episodes = 500  # 要运行的总回合数

# 用零初始化所有状态 - 动作对的 Q 表
q_table = initialize_q_table(state_space, action_space)

# 用于存储每个回合积累的总奖励的列表
rewards_per_episode = []

# 运行多个回合的 Q 学习
for episode in range(episodes):
    # 运行一个回合并获取总奖励
    total_reward = run_episode(q_table, state_space, action_space, rewards, rows, cols, alpha, gamma, epsilon, max_steps)
    # 将该回合的总奖励追加到奖励列表中
    rewards_per_episode.append(total_reward)

# 调整图形大小以便更好地查看
plt.figure(figsize=(20, 3))

# 绘制随回合变化的总奖励
plt.plot(rewards_per_episode)
plt.xlabel('回合')  # x 轴标签
plt.ylabel('总奖励')  # y 轴标签
plt.title('随回合变化的奖励')  # 图表标题
plt.show()  # 显示图表

在这里插入图片描述

观察到的学习行为:

  • 早期回合:大多为零奖励,意味着智能体在探索。
  • 后期回合:高奖励峰值表明智能体有时能找到目标,但还不稳定。
  • 波动:表明策略尚未稳定。

Q 表的初始化和更新

实现基于经验初始化和更新 Q 表的函数,使用 Q 学习更新规则。

# 初始化 Q 表
def initialize_q_table(state_space, action_space):
    """
    用零初始化 Q 表。
    
    参数:
    - state_space:所有可能状态的列表。
    - action_space:可能动作的列表。
    
    返回值:
    - q_table:一个字典,将状态 - 动作对映射到 Q 值。
    """
    q_table = {}
    for state in state_space:
        q_table[state] = {action: 0 for action in action_space}
    return q_table

# 使用 epsilon-greedy 策略选择动作
def choose_action(state, q_table, action_space, epsilon):
    """
    使用 epsilon-greedy 策略选择动作。
    
    参数:
    - state:当前状态,以 (行, 列) 表示。
    - q_table:Q 表,将状态 - 动作对映射到 Q 值。
    - action_space:可能动作的列表。
    - epsilon:探索率。
    
    返回值:
    - action:所选择的动作。
    """
    if np.random.rand() < epsilon:
        return np.random.choice(action_space)  # 探索
    else:
        return max(q_table[state], key=q_table[state].get)  # 利用

# 更新 Q 值
def update_q_value(q_table, state, action, reward, next_state, alpha, gamma, action_space):
    """
    使用 Q 学习更新规则更新 Q 值。
    
    参数:
    - q_table:Q 表,将状态 - 动作对映射到 Q 值。
    - state:当前状态,以 (行, 列) 表示。
    - action:所采取的动作。
    - reward:所获得的奖励。
    - next_state:下一个状态,以 (行, 列) 表示。
    - alpha:学习率。
    - gamma:折扣因子。
    - action_space:可能动作的列表。
    
    返回值:
    - None(在原地更新 q_table)。
    """
    max_next_q = max(q_table[next_state].values()) if next_state in q_table else 0
    q_table[state][action] += alpha * (reward + gamma * max_next_q - q_table[state][action])

探索与利用策略

现在,为了正确使用探索与利用策略,我们需要实现它。第一个函数是 epsilon-greedy 策略,它根据 epsilon 值选择动作。

# 定义 epsilon-greedy 策略
def epsilon_greedy_policy(
    q_table: Dict[Tuple[int, int], Dict[str, float]], 
    state: Tuple[int, int], 
    action_space: List[str], 
    epsilon: float
) -> str:
    """
    实现 epsilon-greedy 策略以选择动作。
    
    参数:
    - q_table (Dict[Tuple[int, int], Dict[str, float]]):Q 表,将状态 - 动作对映射到 Q 值。
    - state (Tuple[int, int]):当前状态,以 (行, 列) 表示。
    - action_space (List[str]):可能动作的列表。
    - epsilon (float):探索率。
    
    返回值:
    - str:所选择的动作。
    """
    # 以概率 epsilon 选择随机动作(探索)
    if np.random.rand() < epsilon:
        return np.random.choice(action_space)
    # 否则,选择当前状态下具有最高 Q 值的动作(利用)
    else:
        return max(q_table[state], key=q_table[state].get)

第二个函数是动态 epsilon 调整,它随着时间调整 epsilon 值,以平衡探索与利用。

# 定义动态 epsilon 调整
def adjust_epsilon(
    initial_epsilon: float, 
    min_epsilon: float, 
    decay_rate: float, 
    episode: int
) -> float:
    """
    随时间动态调整 epsilon,以平衡探索与利用。
    
    参数:
    - initial_epsilon (float):初始探索率。
    - min_epsilon (float):最小探索率。
    - decay_rate (float):epsilon 衰减率。
    - episode (int):当前回合数。
    
    返回值:
    - float:调整后的探索率。
    """
    # 计算衰减后的 epsilon 值,确保不低于最小 epsilon
    return max(min_epsilon, initial_epsilon * np.exp(-decay_rate * episode))

最后,我们可以使用这些函数来跟踪每个回合的 epsilon 值,并绘制其衰减过程。

# 示例:使用 epsilon-greedy 策略和动态 epsilon 调整
initial_epsilon: float = 1.0  # 初始时完全探索
min_epsilon: float = 0.1  # 最小探索率
decay_rate: float = 0.01  # epsilon 衰减率
episodes: int = 500  # 回合数

# 跟踪每个回合的 epsilon 值
epsilon_values: List[float] = []
for episode in range(episodes):
    # 调整当前回合的 epsilon
    epsilon = adjust_epsilon(initial_epsilon, min_epsilon, decay_rate, episode)
    epsilon_values.append(epsilon)

# 调整图形大小以便更好地查看
plt.figure(figsize=(20, 3))

# 绘制随回合变化的 epsilon 衰减
plt.plot(epsilon_values)
plt.xlabel('回合')  # x 轴标签
plt.ylabel('Epsilon')  # y 轴标签
plt.title('随回合变化的 Epsilon 衰减')  # 图表标题
plt.show()  # 显示图表

在这里插入图片描述

  1. 早期探索(高 Epsilon ≈1.0)

    • 在前 100-150 回合,智能体因高 epsilon 而 随机移动
    • 它探索了各种路径,包括 次优路径,这也就是为什么我们在奖励图中看到低奖励的原因。
  2. 中期过渡(Epsilon 衰减)

    • 在大约 150-250 回合,epsilon 减小,智能体开始 更倾向于选择更好的动作,同时仍进行探索。
    • 这与我们在奖励图中看到的 首次出现的高奖励 相符——意味着智能体 偶尔找到了目标
  3. 后期利用(Epsilon 平稳 ≈0.1)

    • 250+ 回合 后,epsilon 非常低,意味着智能体 主要遵循已知的最佳路径
    • 我们的奖励图显示 偶尔的高奖励,表明智能体已经 学会了目标,但还不完全一致

这对我们的网格世界训练意味着什么

  • Epsilon 衰减有助于智能体学习——它最初广泛探索,然后优化策略。
  • 不稳定的高奖励表明 智能体仍然难以 一致地找到目标
  • 可能的解决方案:尝试 更慢的 epsilon 衰减自适应调度,以提高学习稳定性。

运行 Q 学习算法

让我们在网格世界环境中运行 Q 学习算法,并可视化智能体学到的 Q 值。我们将绘制每个状态 - 动作对的 Q 值随时间的变化,以观察它们在训练过程中的演变。

# 在多个回合中执行 Q 学习算法,并跟踪性能指标
def run_q_learning(
    q_table: Dict[Tuple[int, int], Dict[str, float]], 
    state_space: List[Tuple[int, int]], 
    action_space: List[str], 
    rewards: Dict[Tuple[int, int], int], 
    rows: int, 
    cols: int, 
    alpha: float, 
    gamma: float, 
    initial_epsilon: float, 
    min_epsilon: float, 
    decay_rate: float, 
    episodes: int, 
    max_steps: int
) -> Tuple[List[int], List[int]]:
    """
    在多个回合中执行 Q 学习算法。
    
    参数:
    - q_table:Q 表,将状态 - 动作对映射到 Q 值。
    - state_space:所有可能状态的列表。
    - action_space:可能动作的列表。
    - rewards:字典,将 (行, 列) 映射到奖励值。
    - rows:网格的行数。
    - cols:网格的列数。
    - alpha:学习率。
    - gamma:折扣因子。
    - initial_epsilon:初始探索率。
    - min_epsilon:最小探索率。
    - decay_rate:epsilon 衰减率。
    - episodes:要运行的回合数。
    - max_steps:每个回合的最大步数。
    
    返回值:
    - rewards_per_episode:每个回合的总奖励列表。
    - episode_lengths:每个回合的长度列表。
    """
    # 初始化用于存储指标的列表
    rewards_per_episode: List[int] = []
    episode_lengths: List[int] = []
    
    # 遍历每个回合
    for episode in range(episodes):
        # 从随机状态开始
        state: Tuple[int, int] = state_space[np.random.choice(len(state_space))]
        total_reward: int = 0  # 初始化该回合的总奖励
        steps: int = 0  # 初始化步数计数器
        # 调整当前回合的 epsilon
        epsilon: float = adjust_epsilon(initial_epsilon, min_epsilon, decay_rate, episode)
        
        # 遍历最多 max_steps 次
        for _ in range(max_steps):
            # 使用 epsilon-greedy 策略选择动作
            action: str = epsilon_greedy_policy(q_table, state, action_space, epsilon)
            # 根据所选动作计算下一个状态
            next_state: Tuple[int, int] = state_transition(state, action, rows, cols)
            # 获取下一个状态的奖励
            reward: int = get_reward(next_state, rewards)
            # 更新当前状态 - 动作对的 Q 值
            update_q_value(q_table, state, action, reward, next_state, alpha, gamma, action_space)
            # 累积奖励
            total_reward += reward
            # 转移到下一个状态
            state = next_state
            # 增加步数计数器
            steps += 1
            # 检查智能体是否到达终止状态
            if state in terminal_states:
                break
        
        # 将当前回合的指标追加到列表中
        rewards_per_episode.append(total_reward)
        episode_lengths.append(steps)
    
    # 返回指标
    return rewards_per_episode, episode_lengths

接下来,我们将运行 Q 学习算法,总共运行 500 个回合,每个回合最多 100 步。让我们执行算法并可视化结果。

# 设置 Q 学习的超参数
alpha: float = 0.1  # 学习率
gamma: float = 0.9  # 折扣因子
initial_epsilon: float = 1.0  # 初始探索率
min_epsilon: float = 0.1  # 最小探索率
decay_rate: float = 0.01  # epsilon 衰减率
episodes: int = 500  # 回合数
max_steps: int = 100  # 每个回合的最大步数

# 初始化 Q 表
q_table: Dict[Tuple[int, int], Dict[str, float]] = initialize_q_table(state_space, action_space)

# 执行 Q 学习算法
rewards_per_episode, episode_lengths = run_q_learning(
    q_table, state_space, action_space, rewards, rows, cols, alpha, gamma,
    initial_epsilon, min_epsilon, decay_rate, episodes, max_steps
)

让我们通过绘制每个回合积累的总奖励和每个回合的长度来可视化训练过程。

# 绘制每个回合的总奖励
plt.figure(figsize=(20, 3))

# 绘制每个回合的总奖励
plt.subplot(1, 2, 1)
plt.plot(rewards_per_episode)
plt.xlabel('回合')  # x 轴标签
plt.ylabel('总奖励')  # y 轴标签
plt.title('每个回合的总奖励')  # 图表标题

# 绘制每个回合的长度
plt.subplot(1, 2, 2)
plt.plot(episode_lengths)
plt.xlabel('回合')  # x 轴标签
plt.ylabel('回合长度')  # y 轴标签
plt.title('每个回合的长度')  # 图表标题

# 调整布局并显示图表
plt.tight_layout()
plt.show()

在这里插入图片描述

让我们来分析一下:

左图:每个回合的总奖励

  • 每个回合的 总奖励在初期波动,显示出成功与失败的混合。
  • 后期回合稳定地获得最大奖励(10),表明智能体已经学会了最优策略。
  • 偶尔还会出现低奖励的回合,表明智能体偶尔会进行次优的探索或表现出随机行为。

与网格世界的关系:

  • 智能体正朝着 (3,3),提供奖励 10 的目标移动
  • 最初,它探索了低效的路径,导致奖励各异。
  • 随着训练的进行,它找到了 更短、更优的路径

右图:每个回合的长度

  • 初期回合长度较长(多达 50 步),意味着智能体走了低效的路线。
  • 随着训练的进行,回合长度减少,表明智能体更快地找到了目标。
  • 大多数回合稳定在较短的步数(约 3-6 步),意味着智能体已经优化了其路径。

与网格世界的关系:

  • 网格世界只有 4×4(16 个状态),因此最优策略应该能快速解决它。
  • 最初,智能体采取了 随机或探索性的动作,增加了步数。
  • 一旦 Q 值收敛,智能体 选择了通往目标的直接路线,减少了步数。

可视化学习过程

我们需要可视化 Q 表中实际发生的情况。我们可以将 Q 值作为热图可视化,针对每个动作绘制热图,并将学到的策略作为网格上的箭头可视化。让我们定义一些可视化函数来帮助我们完成这项工作。

# 可视化 Q 值的函数
def plot_q_values(q_table: Dict[Tuple[int, int], Dict[str, float]], rows: int, cols: int, action_space: List[str]) -> None:
    """
    将 Q 值作为热图可视化,针对每个动作绘制热图。

    参数:
    - q_table (Dict[Tuple[int, int], Dict[str, float]]):Q 表,将状态 - 动作对映射到 Q 值。
    - rows (int):网格的行数。
    - cols (int):网格的列数。
    - action_space (List[str]):可能动作的列表。

    返回值:
    - None:显示每个动作的 Q 值热图。
    """
    # 为每个动作创建子图
    fig, axes = plt.subplots(1, len(action_space), figsize=(15, 5))
    for i, action in enumerate(action_space):
        # 初始化一个网格以存储当前动作的 Q 值
        q_values = np.zeros((rows, cols))
        for (row, col), actions in q_table.items():
            q_values[row, col] = actions[action]  # 提取当前动作的 Q 值

        # 绘制当前动作的热图
        ax = axes[i]
        cax = ax.matshow(q_values, cmap='viridis')
        fig.colorbar(cax, ax=ax)
        ax.set_title(f"动作 {action} 的 Q 值")
        ax.set_xlabel("列")
        ax.set_ylabel("行")

    # 调整布局并显示热图
    plt.tight_layout()
    plt.show()
# 可视化学到的策略的函数
def plot_policy(q_table: Dict[Tuple[int, int], Dict[str, float]], rows: int, cols: int) -> None:
    """
    将学到的策略作为网格上的箭头可视化。

    参数:
    - q_table (Dict[Tuple[int, int], Dict[str, float]]):Q 表,将状态 - 动作对映射到 Q 值。
    - rows (int):网格的行数。
    - cols (int):网格的列数。

    返回值:
    - None:显示策略可视化。
    """
    # 初始化一个网格以存储每个状态的最佳动作
    policy_grid = np.empty((rows, cols), dtype=str)
    action_symbols = {'up': '↑', 'down': '↓', 'left': '←', 'right': '→'}  # 动作对应的符号

    # 根据 Q 值确定每个状态的最佳动作
    for (row, col), actions in q_table.items():
        best_action = max(actions, key=actions.get)  # 获取具有最高 Q 值的动作
        policy_grid[row, col] = action_symbols[best_action]  # 将动作映射为其符号

    # 绘制策略网格,宽度增加
    fig, ax = plt.subplots(figsize=(16, 3))  # 将宽度从 12 增加到 16,以使网格更水平拉伸
    for i in range(rows):
        for j in range(cols):
            ax.text(j, i, policy_grid[i, j], ha='center', va='center', fontsize=14)  # 稍微增大字体大小
    
    # 创建一个更宽的网格,增加水平空间
    ax.set_xlim(-0.5, cols - 0.5)
    ax.set_ylim(-0.5, rows - 0.5)
    ax.matshow(np.zeros((rows, cols)), cmap='Greys', alpha=0.1)  # 添加一个淡灰色的背景网格
    ax.set_xticks(range(cols))
    ax.set_yticks(range(rows))
    ax.set_xticklabels([])
    ax.set_yticklabels([])
    ax.set_title("学到的策略")
    plt.tight_layout()
    plt.show()
# 并排绘制 Q 值热图和学到的策略
fig, axes = plt.subplots(1, len(action_space) + 1, figsize=(20, 5))

# 绘制每个动作的 Q 值热图
for i, action in enumerate(action_space):
    q_values = np.zeros((rows, cols))
    for (row, col), actions in q_table.items():
        q_values[row, col] = actions[action]
    cax = axes[i].matshow(q_values, cmap='viridis')
    fig.colorbar(cax, ax=axes[i])
    axes[i].set_title(f"动作 {action} 的 Q 值")
    axes[i].set_xlabel("列")
    axes[i].set_ylabel("行")

# 绘制学到的策略
policy_grid = np.empty((rows, cols), dtype=str)
action_symbols = {'up': '↑', 'down': '↓', 'left': '←', 'right': '→'}
for (row, col), actions in q_table.items():
    best_action = max(actions, key=actions.get)
    policy_grid[row, col] = action_symbols[best_action]

axes[-1].matshow(np.zeros((rows, cols)), cmap='Greys', alpha=0.1)
for i in range(rows):
    for j in range(cols):
        axes[-1].text(j, i, policy_grid[i, j], ha='center', va='center', fontsize=14)
axes[-1].set_title("学到的策略")
axes[-1].set_xlabel("列")
axes[-1].set_ylabel("行")

plt.tight_layout()
plt.show()

在这里插入图片描述

这个可视化展示了 Q 值和学到的策略,适用于你的 4×4 网格世界。让我们来分析一下:

Q 值热图(左侧四个图)
每个热图代表特定动作 (up、down、left、right) 在 4×4 网格中每个状态的学到的 Q 值。

  • 较高的 Q 值(更亮的颜色)表示在这些状态中更偏好的动作
  • “down” 和 “right” 动作具有最高的 Q 值,这意味着智能体更倾向于在这些方向上移动以到达目标。
  • 右下角(目标状态)在 “up” 和 “right” 方向上具有非常高的值,表明智能体认识到这是一个有价值的位置。

学到的策略(最右侧的图)

  • 这显示了每个状态下基于学到的 Q 值的最佳动作
  • 智能体主要遵循 向右和向下的运动,这在目标位于右下角时是有意义的。
  • 某些 向上箭头出现在某些位置,表明可能存在障碍物或对特定动作的惩罚。

与网格世界对比

  1. 智能体主要学会了高效的策略(向右和向下移动以到达目标)。
  2. 一些小的不一致之处(比如偶尔的向上或向左移动)可能是由于:
    • 探索仍在进行中(epsilon-greedy 策略尚未完全贪婪)。
    • 学习率或折扣因子影响了值的传播
    • 可能存在障碍物或次优的奖励结构

分析 Q 值和最优策略

让我们以表格形式查看 Q 学习算法学到的最优策略。

# 创建一个字典列表以表示 Q 表数据
q_policy_data = []
for state, actions in q_table.items():
    # 为每个状态追加一个字典,其中包含所有动作的 Q 值以及最佳动作
    q_policy_data.append({
        'State': state,  # 当前状态 (行, 列)
        'up': actions['up'],  # 动作 'up' 的 Q 值
        'down': actions['down'],  # 动作 'down' 的 Q 值
        'left': actions['left'],  # 动作 'left' 的 Q 值
        'right': actions['right'],  # 动作 'right' 的 Q 值
        'Optimal Action': max(actions, key=actions.get)  # 具有最高 Q 值的动作
    })

# 以表格格式显示 Q 表数据
header = ['State', 'up', 'down', 'left', 'right', 'Optimal Action']  # 定义表头
# 打印表头并适当对齐
print(f"{header[0]:<10} {header[1]:<10} {header[2]:<10} {header[3]:<10} {header[4]:<10} {header[5]:<15}")
print("-" * 65)  # 打印分隔线以提高可读性

# 遍历 Q 表数据并打印每一行
for row in q_policy_data:
    # 打印状态、所有动作的 Q 值以及最佳动作
    print(f"{row['State']!s:<10} {row['up']:<10.2f} {row['down']:<10.2f} {row['left']:<10.2f} {row['right']:<10.2f} {row['Optimal Action']:<15}")
状态      up         down       left       right      最佳动作      
-----------------------------------------------------------------
(0, 0)     2.30       0.20       0.39       0.11       up             
(0, 1)     0.59       0.07       2.59       0.14       left           
(0, 2)     0.87       0.24       0.12       10.21      right          
(0, 3)     0.00       17.18      0.85       1.29       down           
(1, 0)     2.98       0.28       0.68       0.79       up             
(1, 1)     0.34       1.51       2.36       0.23       left           
(1, 2)     0.63       1.91       0.31       15.36      right          
(1, 3)     1.96       22.86      2.02       4.82       down           
(2, 0)     2.09       0.41       0.38       0.08       up             
(2, 1)     0.25       3.11       0.29       15.87      right          
(2, 2)     1.45       3.59       3.79       21.67      right          
(2, 3)     8.28       26.89      7.53       12.69      down           
(3, 0)     0.38       0.68       0.18       13.42      right          
(3, 1)     1.33       1.85       0.59       20.41      right          
(3, 2)     4.75       4.86       2.59       25.13      right          
(3, 3)     19.23      2.60       0.00       0.00       up             

关键观察结果

  1. 不同的动作偏好

    • 智能体仍然主要向 右和下 移动,但比之前有更多的 左和上 移动。
    • 特别是,状态 (0,0) 偏好 “up”,而 (0,1) 偏好 “left”,这可能表明存在障碍物或另一种最优路径。
  2. (3,3) 偏好 “Up” 而不是停留

    • 如果 (3,3) 是目标状态,我们期望其他动作的 Q 值为 或更低,因为会终止。
    • 相反,“up” 的 Q 值非常高(19.23),这意味着智能体仍然考虑移动而不是停止。
  3. 更高的 Q 值总体上

    • 最大 Q 值已经增加(例如,状态 (2,3) 的 “down” 为 26.89)。
    • 这表明 学习已经取得进展,可能是由于更多的训练回合或不同的超参数(学习率、折扣因子等)。
  4. 潜在的探索问题

    • 有些动作看起来 不太直观,比如 (0,0) 向上移动而不是向右。
    • 这可能意味着 探索率 (ε) 仍然很高,导致动作选择中存在一些随机性。

与策略可视化的对比

  • Q 值应该与 策略可视化箭头(从之前显示的热图中)相匹配。
  • 然而,如果它们 不完全一致,可能意味着:
    • 策略仍在波动,因为学习仍在进行中。
    • 存在 奖励不一致或障碍物,影响 Q 值。
    • 某些 动作的 Q 值相似,使得在打平手时的选择不太明显。

使用不同超参数进行测试(可选)

你可以尝试不同的超参数,看看它们如何影响智能体的学习过程。

# 使用不同的超参数进行实验
learning_rates = [0.1, 0.5]  # 要测试的不同学习率(alpha)
discount_factors = [0.5]  # 要测试的不同折扣因子(gamma)
exploration_rates = [1.0]  # 要测试的不同初始探索率(epsilon)

# 存储结果以便比较
results = []

# 运行不同超参数组合的实验
for alpha in learning_rates:  # 遍历不同的学习率
    for gamma in discount_factors:  # 遍历不同的折扣因子
        for initial_epsilon in exploration_rates:  # 遍历不同的初始探索率
            # 为当前实验初始化 Q 表
            q_table = initialize_q_table(state_space, action_space)
            
            # 使用当前超参数集运行 Q 学习
            rewards_per_episode, episode_lengths = run_q_learning(
                q_table, state_space, action_space, rewards, rows, cols, alpha, gamma,
                initial_epsilon, min_epsilon, decay_rate, episodes, max_steps
            )
            
            # 存储当前实验的结果
            results.append({
                'alpha': alpha,  # 学习率
                'gamma': gamma,  # 折扣因子
                'initial_epsilon': initial_epsilon,  # 初始探索率
                'rewards_per_episode': rewards_per_episode,  # 每个回合的奖励
                'episode_lengths': episode_lengths  # 每个回合的长度
            })

# 创建一个较大的图形以可视化所有超参数组合
plt.figure(figsize=(20, 5))

# 计算子图网格的行数和列数
num_rows = len(learning_rates)  # 行数对应于学习率的数量
num_cols = len(discount_factors) * len(exploration_rates)  # 列数对应于折扣因子和探索率的组合数量

# 绘制每个实验的结果
for i, result in enumerate(results):  # 遍历所有结果
    plt.subplot(num_rows, num_cols, i + 1)  # 为每个实验创建一个子图
    plt.plot(result['rewards_per_episode'])  # 绘制每个回合的奖励
    plt.title(f"α={result['alpha']}, γ={result['gamma']}, ε={result['initial_epsilon']}")  # 添加标题,显示超参数值
    plt.xlabel('回合')  # x 轴标签
    plt.ylabel('总奖励')  # y 轴标签

# 调整布局以防止重叠并显示图表
plt.tight_layout()
plt.show()

在这里插入图片描述

在不同环境中应用 Q 学习(悬崖行走)

让我们将环境改为悬崖行走场景,智能体必须在网格中导航,避开悬崖以到达目标状态。智能体在掉下悬崖时会收到高额负奖励,在到达目标状态时会收到正奖励。我们将 Q 学习应用于这个新环境,并可视化结果。

# 定义悬崖行走环境 
def create_cliff_walking_env(
    rows: int, 
    cols: int, 
    cliff_states: List[Tuple[int, int]], 
    terminal_state: Tuple[int, int], 
    rewards: Dict[Tuple[int, int], int]
) -> Tuple[np.ndarray, List[Tuple[int, int]], List[str]]:
    """
    创建悬崖行走环境。

    参数:
    - rows (int):网格的行数。
    - cols (int):网格的列数。
    - cliff_states (List[Tuple[int, int]]):悬崖状态列表,以 (行, 列) 元组形式表示。
    - terminal_state (Tuple[int, int]):终止状态,以 (行, 列) 元组形式表示。
    - rewards (Dict[Tuple[int, int], int]):字典,将 (行, 列) 映射到奖励值。

    返回值:
    - Tuple[np.ndarray, List[Tuple[int, int]], List[str]]:
        - grid (np.ndarray):一个二维数组,表示带有奖励的网格。
        - state_space (List[Tuple[int, int]]):网格中所有可能状态的列表。
        - action_space (List[str]):可能动作的列表('up'、'down'、'left'、'right')。
    """
    # 用零初始化网格
    grid = np.zeros((rows, cols))

    # 为指定状态分配奖励
    for (row, col), reward in rewards.items():
        grid[row, col] = reward

    # 为悬崖状态分配高额负奖励
    for row, col in cliff_states:
        grid[row, col] = -100

    # 为终止状态分配正奖励
    grid[terminal_state] = 10

    # 定义状态空间为所有可能的 (行, 列) 组合
    state_space = [(r, c) for r in range(rows) for c in range(cols)]

    # 定义动作空间为四种可能的移动方式
    action_space = ['up', 'down', 'left', 'right']

    return grid, state_space, action_space

让我们创建一个悬崖行走环境。

# 定义悬崖行走环境
rows, cols = 4, 12  # 网格的尺寸(4 行 12 列)

# 定义悬崖状态(底部行,不包括起始和目标位置)
cliff_states = [(3, c) for c in range(1, 11)]  

# 定义目标状态(目标位置)
terminal_state = (3, 11)

# 定义环境的奖励
# 到达目标状态的奖励为 10
rewards = {(3, 11): 10}  

# 使用辅助函数创建悬崖行走环境
# 该函数返回网格、状态空间和动作空间
cliff_grid, cliff_state_space, cliff_action_space = create_cliff_walking_env(
    rows, cols, cliff_states, terminal_state, rewards
)

为了绘制奖励,我们可以使用以下代码。

# 绘制悬崖行走环境的奖励
def plot_rewards(rewards_per_episode: List[int], ax: plt.Axes = None) -> plt.Axes:
    """
    绘制每个回合积累的总奖励。

    参数:
    - rewards_per_episode (List[int]):每个回合的总奖励列表。
    - ax (plt.Axes, 可选):Matplotlib 轴,用于绘图。如果为 None,则创建一个新的图形和轴。

    返回值:
    - plt.Axes:包含绘图的 Matplotlib 轴。
    """
    # 如果未提供轴,则创建一个新的图形和轴
    if ax is None:
        fig, ax = plt.subplots(figsize=(8, 6))
    
    # 绘制随回合变化的奖励
    ax.plot(rewards_per_episode)
    ax.set_xlabel('回合')  # x 轴标签
    ax.set_ylabel('总奖励')  # y 轴标签
    ax.set_title('随回合变化的奖励')  # 图表标题
    
    # 返回轴,以便在需要时进行进一步自定义
    return ax

让我们创建几个函数来绘制悬崖行走环境并可视化学到的策略。

# 可视化悬崖行走环境
def plot_cliff_walking_env(
    grid: np.ndarray, 
    cliff_states: List[Tuple[int, int]], 
    terminal_state: Tuple[int, int], 
    ax: Optional[plt.Axes] = None
) -> plt.Axes:
    """
    可视化悬崖行走环境。

    参数:
    - grid:二维 numpy 数组,表示网格。
    - cliff_states:悬崖状态列表,以 (行, 列) 元组形式表示。
    - terminal_state:终止状态,以 (行, 列) 元组形式表示。
    - ax:可选的 Matplotlib 轴,用于绘图。

    返回值:
    - ax:包含环境可视化的 Matplotlib 轴。
    """
    if ax is None:
        fig, ax = plt.subplots(figsize=(8, 6))
        
    for r in range(grid.shape[0]):
        for c in range(grid.shape[1]):
            if (r, c) in cliff_states:
                ax.text(c, r, 'C', ha='center', va='center', color='red', fontsize=12)
            elif (r, c) == terminal_state:
                ax.text(c, r, 'G', ha='center', va='center', color='green', fontsize=12)
            elif (r, c) == (3, 0):  # 起始位置
                ax.text(c, r, 'S', ha='center', va='center', color='blue', fontsize=12)
            else:
                ax.text(c, r, '.', ha='center', va='center', color='black', fontsize=12)
    
    ax.matshow(np.zeros_like(grid), cmap='Greys', alpha=0.1)
    ax.set_xticks(range(grid.shape[1]))
    ax.set_yticks(range(grid.shape[0]))
    ax.set_xticklabels([])
    ax.set_yticklabels([])
    ax.set_title("悬崖行走环境")
    return ax
# 可视化每个动作的 Q 值
def plot_q_values(
    q_table: Dict[Tuple[int, int], Dict[str, float]], 
    rows: int, 
    cols: int, 
    action_space: List[str], 
    ax: Optional[plt.Axes] = None
) -> plt.Axes:
    """
    可视化每个动作的 Q 值。

    参数:
    - q_table:包含每个状态 - 动作对 Q 值的字典。
    - rows:网格的行数。
    - cols:网格的列数。
    - action_space:可能动作的列表。
    - ax:可选的 Matplotlib 轴,用于绘图。

    返回值:
    - ax:包含 Q 值可视化的 Matplotlib 轴。
    """
    if ax is None:
        fig, ax = plt.subplots(figsize=(8, 6))
    
    # 为 Q 值创建网格
    q_values = np.zeros((rows, cols))
    
    # 对于每个状态,找到具有最高 Q 值的动作
    for r in range(rows):
        for c in range(cols):
            state = (r, c)
            if state in q_table:
                q_values[r, c] = max(q_table[state].values())
    
    im = ax.imshow(q_values, cmap='viridis')
    plt.colorbar(im, ax=ax, label='最大 Q 值')
    
    # 将状态值作为文本添加
    for r in range(rows):
        for c in range(cols):
            if (r, c) in cliff_states:
                text_color = 'red'
            elif (r, c) == terminal_state:
                text_color = 'green'
            else:
                text_color = 'white'
            ax.text(c, r, f"{q_values[r, c]:.1f}", ha='center', va='center', color=text_color)
    
    ax.set_xticks(range(cols))
    ax.set_yticks(range(rows))
    ax.set_title('最大 Q 值')
    return ax
# 可视化学到的策略
def plot_policy(
    q_table: Dict[Tuple[int, int], Dict[str, float]], 
    rows: int, 
    cols: int, 
    ax: Optional[plt.Axes] = None
) -> plt.Axes:
    """
    可视化学到的策略。

    参数:
    - q_table:包含每个状态 - 动作对 Q 值的字典。
    - rows:网格的行数。
    - cols:网格的列数。
    - ax:可选的 Matplotlib 轴,用于绘图。

    返回值:
    - ax:包含策略可视化的 Matplotlib 轴。
    """
    if ax is None:
        fig, ax = plt.subplots(figsize=(8, 6))
    
    # 定义动作符号
    action_symbols = {'up': '↑', 'down': '↓', 'left': '←', 'right': '→'}
    
    # 为策略创建网格
    policy_grid = np.empty((rows, cols), dtype='U1')
    
    # 对于每个状态,找到具有最高 Q 值的动作
    for r in range(rows):
        for c in range(cols):
            state = (r, c)
            if state in q_table:
                best_action = max(q_table[state], key=q_table[state].get)
                policy_grid[r, c] = action_symbols[best_action]
            else:
                policy_grid[r, c] = ' '
    
    # 显示策略网格
    ax.imshow(np.zeros((rows, cols)), cmap='Greys', alpha=0.1)
    
    # 将策略箭头作为文本添加
    for r in range(rows):
        for c in range(cols):
            if (r, c) in cliff_states:
                text_color = 'red'
            elif (r, c) == terminal_state:
                text_color = 'green'
            else:
                text_color = 'black'
            ax.text(c, r, policy_grid[r, c], ha='center', va='center', color=text_color, fontsize=20)
    
    ax.set_xticks(range(cols))
    ax.set_yticks(range(rows))
    ax.set_title('学到的策略')
    return ax
# 在悬崖行走环境中运行 Q 学习
alpha = 0.1  # 学习率
gamma = 0.9  # 折扣因子
initial_epsilon = 1.0  # 初始探索率
min_epsilon = 0.1  # 最小探索率
decay_rate = 0.01  # epsilon 衰减率
episodes = 500  # 回合数
max_steps = 100  # 每个回合的最大步数

# 初始化 Q 表
cliff_q_table = initialize_q_table(cliff_state_space, cliff_action_space)

# 执行 Q 学习
cliff_rewards_per_episode, cliff_episode_lengths = run_q_learning(
    cliff_q_table, cliff_state_space, cliff_action_space, rewards, rows, cols, alpha, gamma,
    initial_epsilon, min_epsilon, decay_rate, episodes, max_steps
)

# 创建 2×2 的可视化网格
fig, axs = plt.subplots(2, 2, figsize=(20, 12))

# 在左上角的子图中绘制奖励
plot_rewards(cliff_rewards_per_episode, ax=axs[0, 0])

# 在右上角的子图中绘制环境
plot_cliff_walking_env(cliff_grid, cliff_states, terminal_state, ax=axs[0, 1])

# 在左下角的子图中绘制 Q 值
plot_q_values(cliff_q_table, rows, cols, cliff_action_space, ax=axs[1, 0])

# 在右下角的子图中绘制策略
plot_policy(cliff_q_table, rows, cols, ax=axs[1, 1])

plt.tight_layout()
plt.show()

在这里插入图片描述

关键观察结果:

  1. 左上角:随回合变化的奖励

    • 总奖励 稳步增加,表明智能体随着时间的推移正在学习更好的策略。
    • 出现了 一些突然的下降(奖励接近零),可能是因为智能体掉下悬崖(会收到高额负奖励)。
  2. 右上角:悬崖行走网格表示

    • 环境是一个 4 行 12 列的网格
    • 起始位置 (S) 在左下角。
    • 目标位置 (G) 在右下角。
    • 悬崖 ©,用红色表示,沿着底部行,意味着如果智能体走到那里,就会掉下去并收到高额惩罚。
  3. 左下角:每个状态的最大 Q 值

    • 这个热图表示 每个状态的最高 Q 值
    • 随着接近目标而增加,这是预期之中的。
    • 悬崖区域(第 3 行,第 1-10 列)的值非常低甚至为负(用红色突出显示),表明这是智能体应该避免的区域。
  4. 右下角:学到的策略

    • 这显示了 每个状态的最佳动作,用箭头表示。
    • 大多数箭头指向 右 (→) 或下 (↓),引导智能体朝目标前进。
    • 然而,在 悬崖区域(底部行),箭头显示为左 (←) 和上 (↑) 的组合,表明智能体已经学会了 避免掉下悬崖

分析:

  • 策略学习得很好,智能体主要避免悬崖。
  • 探索导致早期失败,正如奖励图中的奖励下降所见。
  • 智能体可能使用了 Q 学习或 SARSA 来解决这个环境。
    • 如果是 Q 学习,它学习的是最佳长期策略。
    • 如果是 SARSA,它学习的是更安全的策略,避免冒险的步骤。

常见挑战及解决方案

挑战:在大状态空间中学习缓慢

解决方案:有几种方法可以解决这个问题:

  • 使用函数近似(使用神经网络近似 Q 值)
  • 状态聚合(将相似的状态分组)
  • 经验回放(重用过去的经历)

挑战:平衡探索与利用

解决方案

  • 从高探索率(高 ε)开始
  • 随时间逐渐降低探索率
  • 使用更复杂的探索策略,如玻尔兹曼探索

挑战:选择合适的超参数

解决方案

  • 学习率 (α):从 0.1 到 0.3 开始
  • 折扣因子 (γ):通常为 0.9 到 0.99
  • 探索率 (ε):从高值(0.9 到 1.0)开始,并衰减到低值(0.01 到 0.1)
  • 系统地测试不同的组合,以找到最优设置

Q 学习与其他强化学习算法的比较

Q 学习的优点

  • 简单易懂且易于实现
  • 无需环境模型(无模型)
  • 可以直接学习最优策略
  • 在离散状态和动作空间中表现良好

Q 学习的局限性

  • 在大或连续状态空间中表现不佳
  • 收敛可能很慢
  • 可能高估 Q 值(通过双 Q 学习解决)
  • 不直接适用于连续动作空间

相关算法

  • SARSA:与 Q 学习类似,但使用实际的下一个动作,而不是最大 Q 值
  • 深度 Q 网络 (DQN):使用神经网络近似 Q 值,适用于大状态空间
  • 双 Q 学习:解决 Q 学习的高估问题
  • 带函数近似的 Q 学习:使用函数近似处理大或连续状态空间

结论

Q 学习是一种强大且直观的强化学习算法,已成功应用于许多问题。它的优势在于简单性以及无需环境模型即可学习。尽管在非常大的状态空间中存在挑战,但诸如深度 Q 网络等扩展已解决了许多这些问题。

理解 Q 学习为探索更高级的强化学习算法和概念提供了坚实的基础。Q 学习的核心思想——通过经验学习状态 - 动作对的值,并据此做出决策——在现代强化学习中随处可见。

评论 30
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AI仙人掌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值