从代码学习深度强学习 - Dyna-Q 算法 PyTorch版

发布于:2025-06-12 ⋅ 阅读:(20) ⋅ 点赞:(0)


前言

在强化学习(Reinforcement Learning, RL)的广阔天地中,智能体(Agent)通过与环境(Environment)的交互来学习如何做出最优决策。根据智能体是否学习环境的模型,RL算法可以分为两大类:无模型的强化学习(Model-Free RL)基于模型的强化学习(Model-Based RL)

  • 无模型RL:不尝试理解环境的动态变化,而是直接从与环境交互采样到的数据中学习策略或价值函数。我们熟悉的Q-learning、Sarsa、DQN等都属于这一类。它们通常更通用,但学习效率(即样本复杂度)较低。
  • 基于模型RL:尝试构建一个环境模型,这个模型可以预测状态转移的概率和奖励。一旦有了模型,智能体就可以在“脑海中”进行推演和规划,而无需与真实环境进行昂贵的交互。

Dyna-Q算法正是基于模型RL领域一个经典且基础的算法。它巧妙地将无模型的Q-learning与基于模型的规划(Planning)结合起来,旨在大幅提升学习效率。它的核心思想非常直观:智能体不仅从与真实世界的交互中学习,还会利用这些经验来构建一个内部世界模型。然后,它可以在这个模型中进行“想象”或“排练”,生成大量模拟经验,从而加速学习过程。

本文将从理论和代码两个层面,深入剖明Dyna-Q算法。我们将首先理解其背后的工作原理,然后通过一个经典的“悬崖漫步”环境,一步步用Python(基于NumPy)实现整个算法,并最终通过实验结果来直观感受其学习效率的提升。

:本文作为系列博客的一部分,虽然标题中含有“PyTorch”,但Dyna-Q作为一种表格型方法,使用NumPy实现更为直接清晰。在后续更复杂的算法中,我们将看到PyTorch在处理函数近似、神经网络等方面的强大威力。

完整代码:下载链接

Dyna-Q 算法详解

Dyna-Q算法的框架非常优雅,它在标准的Q-learning流程中加入了“模型学习”和“规划”两个环节。

核心思想

我们可以通过下面这张图来理解Dyna-Q的整个交互和学习循环:

在这里插入图片描述

  1. 真实交互 (Real Interaction):智能体在真实环境中执行一个动作,获得一个反馈(下一个状态、奖励)。
  2. 直接学习 (Direct RL):利用这次真实的经验 (s, a, r, s'),通过Q-learning更新一次Q值表。这是从真实世界中学习。
  3. 模型学习 (Model Learning):同样利用这次经验,更新内部的环境模型。对于一个确定的环境,模型学习非常简单,就是记录下“在状态s下执行动作a,会得到奖励r并转移到状态s’”。
  4. 规划/模拟 (Planning):这是Dyna-Q的核心。在完成一次真实交互后,算法会额外进行 N 次“规划”步骤。在每一步规划中,它会:
    • 已经访问过的状态-动作对中随机抽取一个。
    • 利用模型来预测这个状态-动作对会产生的奖励和下一个状态。
    • 用这个模拟出的经验,再次通过Q-learning来更新Q值表。

通过这种方式,每一次与真实环境的交互都被“反复利用”,大大提高了数据的利用率和学习效率。

算法伪代码

下面是Dyna-Q算法的具体流程伪代码,它清晰地展示了上述步骤:

  • 初始化 Q(s,a) 和模型 M(s,a)
  • 循环 对每个序列 e = 1 → E:
    • 得到初始状态 s
    • 循环 对每一步 t = 1 → T:
      • (1) 用 ε-贪婪策略根据 Q 选择当前状态 s 下的动作 a
      • (2) 得到环境反馈的 r, s’
      • (3) (直接学习) Q(s, a) ← Q(s, a) + α[r + γ * max_a' Q(s', a') - Q(s, a)]
      • (4) (模型学习) M(s, a) ← r, s'
      • (5) (规划) 循环 n 次:
        • 随机选择一个曾经访问过的状态 sm
        • 采取一个曾经在状态 sm 下执行过的动作 am
        • 通过模型得到 rm, s'm ← M(sm, am)
        • (间接学习) Q(sm, am) ← Q(sm, am) + α[rm + γ * max_a' Q(s'm, a') - Q(sm, am)]
      • s ← s’
    • 循环结束
  • 循环结束

值得注意的是,伪代码中 M(s, a) ← r, s' 的模型更新方式仅适用于确定性环境,即在状态s执行动作a,结果是唯一确定的。本文的实验环境“悬崖漫步”就是这样一个例子。

实验环境:悬崖漫步 (The Environment: Cliff Walking)

为了验证算法,我们需要一个实验环境。悬崖漫步(Cliff Walking)是一个非常经典的网格世界问题。
在这里插入图片描述

  • 目标:智能体从左下角的起点(S)出发,移动到右下角的目标(G)。
  • 规则
    • 每走一步,奖励为 -1。
    • 中间有一片区域是悬崖(Cliff)。如果智能体进入悬崖区域,会获得 -100 的巨大负奖励,并被传送回起点S,当前回合结束。
    • 到达终点G,回合结束。
  • 最优路径:沿着悬崖上方边缘行走,总步数为13步,总回报为-13。

环境代码实现

我们使用Python和NumPy来实现这个环境。

import matplotlib.pyplot as plt
import numpy as np
from tqdm import tqdm
import random
import time


class CliffWalkingEnv:
    """
    悬崖漫步环境类
    
    这是一个经典的强化学习环境,智能体需要从起点走到终点,
    同时避免掉入悬崖。环境是一个矩形网格世界。
    """
    
    def __init__(self, ncol, nrow):
        """
        初始化悬崖漫步环境
        
        参数:
            ncol (int): 网格的列数,标量
            nrow (int): 网格的行数,标量
        """
        self.nrow = nrow  # 网格行数,标量
        self.ncol = ncol  # 网格列数,标量
        self.x = 0        # 当前智能体位置的横坐标(列索引),标量,取值范围[0, ncol-1]
        self.y = self.nrow - 1  # 当前智能体位置的纵坐标(行索引),标量,取值范围[0, nrow-1]
        
    def step(self, action):
        """
        执行一个动作,返回下一个状态、奖励和是否结束的标志
        
        参数:
            action (int): 动作编号,标量,取值范围[0, 3]
                         0: 上移, 1: 下移, 2: 左移, 3: 右移
        
        返回:
            next_state (int): 下一个状态编号,标量,取值范围[0, nrow*ncol-1]
            reward (int): 奖励值,标量,通常为-1或-100
            done (bool): 是否结束当前回合,标量布尔值
        """
        # 定义4种动作对应的坐标变化
        # change是一个4x2的列表,每行代表一个动作的坐标变化[dx, dy]
        # change[0]: 上移 [0, -1], change[1]: 下移 [0, 1]
        # change[2]: 左移 [-1, 0], change[3]: 右移 [1, 0]
        # 坐标系原点(0,0)定义在左上角
        change = [[0, -1], [0, 1], [-1, 0], [1, 0]]  # 形状: (4, 2)
        
        # 更新智能体位置,确保不超出边界
        # min和max函数确保坐标在有效范围内
        self.x = min(self.ncol -