深度Q网络实战:从理论到工程实践的完整指南

1 阅读1分钟

🔥 爆款提示:本文将带你深入DQN的工程实现细节,从理论到完整的可运行代码,涵盖所有关键技术点。你将学会如何构建一个能在复杂环境中稳定训练的DQN系统,这是通往高级强化学习算法的必经之路!

从理论到实践:DQN全面解析

在上一节中,我们学习了控制论和强化学习的基础知识。现在,我们将深入探索深度强化学习中的里程碑式算法——深度Q网络(DQN)。本节将从理论和实践两个角度,通过完整的代码实现和详细的设计过程,带你掌握这一突破性技术。

什么是深度Q网络?

深度Q网络(Deep Q-Network,DQN)是由DeepMind在2015年提出的开创性算法,它成功地将深度学习与Q-learning结合起来,在多个Atari游戏中达到了超越人类水平的表现。DQN解决了传统Q-learning在处理高维状态空间时的局限性。

DQN的关键创新

DQN主要有两大关键创新:

  1. 经验回放(Experience Replay):将智能体的经验存储在一个回放缓冲区中,并从中随机采样进行训练,打破数据间的相关性,提高样本效率。

  2. 固定Q目标(Fixed Q-targets):使用一个独立的目标网络来计算目标Q值,定期更新目标网络参数,提高训练的稳定性。

让我们用一个流程图来展示DQN的工作原理:

graph TD
    A[环境状态 St] --> B{ε贪婪策略}
    B -->|1-ε概率| C[主网络选择最优动作]
    B -->|ε概率| D[随机动作]
    C --> E[执行动作 At]
    D --> E
    E --> F[获得奖励 Rt+1 和新状态 St+1]
    F --> G[存储经验到回放缓冲区]
    G --> H[从缓冲区采样一批经验]
    H --> I[使用目标网络计算目标Q值]
    I --> J[计算损失函数]
    J --> K[更新主网络参数]
    K --> L[定期同步目标网络参数]
    L --> M[继续下一个步骤]

DQN算法详解

DQN算法的核心在于使用神经网络来近似Q函数,解决了传统Q-learning在面对复杂状态空间时的维度灾难问题。

算法步骤

  1. 初始化主网络和目标网络
  2. 初始化经验回放缓冲区
  3. 在每个时间步:
    • 根据ε-贪婪策略选择动作
    • 执行动作并观察奖励和下一状态
    • 将经验存储到回放缓冲区
    • 从缓冲区采样一批经验
    • 使用目标网络计算目标Q值
    • 更新主网络参数
    • 定期同步目标网络参数

损失函数

DQN使用均方误差损失函数:

Li(θi)=E(s,a,r,s)U(D)[(r+γmaxaQ(s,a;θi)Q(s,a;θi))2]L_i(\theta_i) = \mathbb{E}_{(s,a,r,s') \sim U(D)} \left[ (r + \gamma \max_{a'} Q(s',a';\theta_i^-) - Q(s,a;\theta_i))^2 \right]

其中:

  • θi\theta_i 是主网络的参数
  • θi\theta_i^- 是目标网络的参数
  • DD 是经验回放缓冲区
  • γ\gamma 是折扣因子

实战:用DQN玩CartPole游戏

接下来,我们将使用PyTorch实现一个完整的DQN智能体来解决CartPole环境。这个环境相对简单,非常适合理解DQN的工作原理。

环境介绍

CartPole是一个经典的控制问题:一个小车在一个无摩擦的轨道上移动,顶部有一个可以摆动的杆子。目标是通过施加力使小车左右移动,以保持杆子竖直向上。

状态空间包括四个值:

  • 小车位置
  • 小车速度
  • 杆子角度
  • 杆子角速度

动作空间有两个离散动作:

  • 向左施加力
  • 向右施加力

代码实现

首先,我们需要导入必要的库:

import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import numpy as np
import random
from collections import deque, namedtuple
import gym
from typing import Tuple, List
import matplotlib.pyplot as plt

# 检查是否有GPU可用
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")

# 定义经验元组
Experience = namedtuple('Experience', ['state', 'action', 'reward', 'next_state', 'done'])

接下来,我们定义Q网络结构:

class DQN(nn.Module):
    """
    深度Q网络架构
    """
    def __init__(self, input_size: int, hidden_sizes: List[int], output_size: int):
        """
        初始化DQN网络
        
        Args:
            input_size: 输入维度
            hidden_sizes: 隐藏层大小列表
            output_size: 输出维度(动作空间大小)
        """
        super(DQN, self).__init__()
        
        # 构建网络层
        layers = []
        prev_size = input_size
        
        # 添加隐藏层
        for hidden_size in hidden_sizes:
            layers.append(nn.Linear(prev_size, hidden_size))
            layers.append(nn.ReLU())
            prev_size = hidden_size
        
        # 输出层
        layers.append(nn.Linear(prev_size, output_size))
        
        self.network = nn.Sequential(*layers)
    
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        前向传播
        
        Args:
            x: 输入张量
            
        Returns:
            网络输出
        """
        return self.network(x)

class DuelingDQN(nn.Module):
    """
    Dueling DQN网络架构
    将Q值分解为状态值函数和优势函数
    """
    def __init__(self, input_size: int, hidden_sizes: List[int], output_size: int):
        """
        初始化Dueling DQN网络
        
        Args:
            input_size: 输入维度
            hidden_sizes: 隐藏层大小列表
            output_size: 输出维度(动作空间大小)
        """
        super(DuelingDQN, self).__init__()
        
        # 特征提取层
        feature_layers = []
        prev_size = input_size
        for hidden_size in hidden_sizes[:-1]:  # 保留最后一层用于值流和优势流
            feature_layers.append(nn.Linear(prev_size, hidden_size))
            feature_layers.append(nn.ReLU())
            prev_size = hidden_size
        
        self.feature_layer = nn.Sequential(*feature_layers)
        
        # 状态值流 (Value Stream)
        self.value_stream = nn.Sequential(
            nn.Linear(prev_size, hidden_sizes[-1]),
            nn.ReLU(),
            nn.Linear(hidden_sizes[-1], 1)
        )
        
        # 优势流 (Advantage Stream)
        self.advantage_stream = nn.Sequential(
            nn.Linear(prev_size, hidden_sizes[-1]),
            nn.ReLU(),
            nn.Linear(hidden_sizes[-1], output_size)
        )
    
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        前向传播
        
        Args:
            x: 输入张量
            
        Returns:
            网络输出
        """
        features = self.feature_layer(x)
        values = self.value_stream(features)
        advantages = self.advantage_stream(features)
        # 合并值函数和优势函数
        qvals = values + (advantages - advantages.mean(dim=1, keepdim=True))
        return qvals

然后,我们实现DQN智能体:

class DQNAgent:
    """
    DQN智能体实现
    """
    def __init__(self, 
                 state_size: int, 
                 action_size: int, 
                 lr: float = 0.001,
                 gamma: float = 0.99,
                 epsilon_start: float = 1.0,
                 epsilon_end: float = 0.01,
                 epsilon_decay: float = 0.995,
                 buffer_size: int = 10000,
                 batch_size: int = 32,
                 target_update_freq: int = 100,
                 network_type: str = "dqn"):
        """
        初始化DQN智能体
        
        Args:
            state_size: 状态空间维度
            action_size: 动作空间大小
            lr: 学习率
            gamma: 折扣因子
            epsilon_start: ε-贪婪策略起始值
            epsilon_end: ε-贪婪策略结束值
            epsilon_decay: ε衰减率
            buffer_size: 经验回放缓冲区大小
            batch_size: 批次大小
            target_update_freq: 目标网络更新频率
            network_type: 网络类型 ("dqn" 或 "dueling")
        """
        self.state_size = state_size
        self.action_size = action_size
        self.lr = lr
        self.gamma = gamma
        self.epsilon = epsilon_start
        self.epsilon_end = epsilon_end
        self.epsilon_decay = epsilon_decay
        self.batch_size = batch_size
        self.target_update_freq = target_update_freq
        
        # 经验回放缓冲区
        self.memory = deque(maxlen=buffer_size)
        
        # 网络类型选择
        if network_type == "dueling":
            self.q_network = DuelingDQN(state_size, [64, 64], action_size).to(device)
            self.target_network = DuelingDQN(state_size, [64, 64], action_size).to(device)
        else:
            self.q_network = DQN(state_size, [64, 64], action_size).to(device)
            self.target_network = DQN(state_size, [64, 64], action_size).to(device)
        
        # 优化器
        self.optimizer = optim.Adam(self.q_network.parameters(), lr=lr)
        
        # 训练步数计数器
        self.t_step = 0
        
        # 初始化目标网络
        self.update_target_network()
    
    def update_target_network(self):
        """同步目标网络参数"""
        self.target_network.load_state_dict(self.q_network.state_dict())
    
    def remember(self, state: np.ndarray, action: int, reward: float, next_state: np.ndarray, done: bool):
        """
        存储经验到回放缓冲区
        
        Args:
            state: 当前状态
            action: 执行的动作
            reward: 获得的奖励
            next_state: 下一状态
            done: 是否结束
        """
        experience = Experience(state, action, reward, next_state, done)
        self.memory.append(experience)
    
    def act(self, state: np.ndarray, training: bool = True) -> int:
        """
        根据ε-贪婪策略选择动作
        
        Args:
            state: 当前状态
            training: 是否处于训练模式
            
        Returns:
            选择的动作
        """
        # 训练模式下使用ε-贪婪策略,测试模式下直接选择最优动作
        if training and random.random() <= self.epsilon:
            return random.randrange(self.action_size)
        
        # 转换状态为张量
        state_tensor = torch.FloatTensor(state).unsqueeze(0).to(device)
        
        # 前向传播获取Q值
        q_values = self.q_network(state_tensor)
        
        # 选择Q值最大的动作
        return np.argmax(q_values.cpu().data.numpy())
    
    def replay(self) -> float:
        """
        从经验回放缓冲区中采样并训练
        
        Returns:
            当前批次的损失值
        """
        # 检查缓冲区中是否有足够经验
        if len(self.memory) < self.batch_size:
            return 0.0
        
        # 从缓冲区随机采样一批经验
        experiences = random.sample(self.memory, self.batch_size)
        batch = Experience(*zip(*experiences))
        
        # 转换为张量
        states = torch.FloatTensor(np.array(batch.state)).to(device)
        actions = torch.LongTensor(np.array(batch.action)).to(device)
        rewards = torch.FloatTensor(np.array(batch.reward)).to(device)
        next_states = torch.FloatTensor(np.array(batch.next_state)).to(device)
        dones = torch.BoolTensor(np.array(batch.done)).to(device)
        
        # 计算当前Q值
        current_q_values = self.q_network(states).gather(1, actions.unsqueeze(1))
        
        # 计算目标Q值
        with torch.no_grad():
            next_q_values = self.target_network(next_states).max(1)[0]
            target_q_values = rewards + (self.gamma * next_q_values * ~dones)
        
        # 计算损失
        loss = F.mse_loss(current_q_values.squeeze(), target_q_values)
        
        # 执行优化步骤
        self.optimizer.zero_grad()
        loss.backward()
        self.optimizer.step()
        
        # 降低ε值(探索率衰减)
        if self.epsilon > self.epsilon_end:
            self.epsilon *= self.epsilon_decay
        
        return loss.item()
    
    def step(self, state: np.ndarray, action: int, reward: float, next_state: np.ndarray, done: bool) -> float:
        """
        执行一步操作:存储经验并训练
        
        Args:
            state: 当前状态
            action: 执行的动作
            reward: 获得的奖励
            next_state: 下一状态
            done: 是否结束
            
        Returns:
            当前批次的损失值
        """
        # 存储经验
        self.remember(state, action, reward, next_state, done)
        
        # 增加步数计数器
        self.t_step = (self.t_step + 1) % self.target_update_freq
        
        # 训练
        loss = 0.0
        if len(self.memory) > self.batch_size:
            loss = self.replay()
        
        # 定期更新目标网络
        if self.t_step == 0:
            self.update_target_network()
        
        return loss

最后,我们训练智能体:

def train_dqn(env_name: str = 'CartPole-v1', 
              episodes: int = 1000, 
              network_type: str = "dqn",
              save_model: bool = True) -> Tuple[DQNAgent, List[float], List[float]]:
    """
    训练DQN智能体
    
    Args:
        env_name: 环境名称
        episodes: 训练回合数
        network_type: 网络类型
        save_model: 是否保存模型
        
    Returns:
        训练好的智能体、得分历史、损失历史
    """
    # 创建环境
    env = gym.make(env_name)
    state_size = env.observation_space.shape[0]
    action_size = env.action_space.n
    
    # 创建智能体
    agent = DQNAgent(state_size, action_size, network_type=network_type)
    
    # 记录训练过程
    scores = []  # 每回合得分
    losses = []  # 每回合平均损失
    
    print(f"开始训练 {network_type} 智能体...")
    print(f"状态空间大小: {state_size}, 动作空间大小: {action_size}")
    
    for episode in range(episodes):
        # 重置环境
        state = env.reset()
        if isinstance(state, tuple):
            state = state[0]  # 新版本gym返回元组
            
        total_reward = 0
        total_loss = 0
        steps = 0
        done = False
        
        # 一回合训练
        while not done:
            # 选择动作
            action = agent.act(state)
            
            # 执行动作
            result = env.step(action)
            
            # 处理不同版本gym的返回值
            if len(result) == 4:
                next_state, reward, done, _ = result
            else:
                next_state, reward, terminated, truncated, _ = result
                done = terminated or truncated
            
            # 执行一步操作
            loss = agent.step(state, action, reward, next_state, done)
            
            # 更新状态
            state = next_state
            total_reward += reward
            total_loss += loss
            steps += 1
        
        # 记录回合结果
        scores.append(total_reward)
        losses.append(total_loss / steps if steps > 0 else 0)
        
        # 每100回合打印一次信息
        if (episode + 1) % 100 == 0:
            avg_score = np.mean(scores[-100:])
            avg_loss = np.mean(losses[-100:])
            print(f"回合 {episode+1:4d}/{episodes} | "
                  f"平均得分: {avg_score:7.2f} | "
                  f"平均损失: {avg_loss:7.4f} | "
                  f"ε: {agent.epsilon:5.3f}")
    
    # 保存模型
    if save_model:
        model_path = f"dqn_{env_name}_{network_type}.pth"
        torch.save(agent.q_network.state_dict(), model_path)
        print(f"模型已保存到: {model_path}")
    
    env.close()
    return agent, scores, losses

def test_agent(agent: DQNAgent, 
               env_name: str = 'CartPole-v1', 
               episodes: int = 5, 
               render: bool = True) -> List[float]:
    """
    测试训练好的智能体
    
    Args:
        agent: 训练好的智能体
        env_name: 环境名称
        episodes: 测试回合数
        render: 是否渲染环境
        
    Returns:
        测试得分列表
    """
    # 创建环境
    env = gym.make(env_name, render_mode='human' if render else None)
    scores = []
    
    print(f"\n开始测试智能体 ({episodes} 回合)...")
    
    for episode in range(episodes):
        # 重置环境
        state = env.reset()
        if isinstance(state, tuple):
            state = state[0]
            
        total_reward = 0
        done = False
        
        # 一回合测试
        while not done:
            # 渲染环境(如果需要)
            if render:
                env.render()
            
            # 选择动作(测试模式)
            action = agent.act(state, training=False)
            
            # 执行动作
            result = env.step(action)
            
            # 处理不同版本gym的返回值
            if len(result) == 4:
                next_state, reward, done, _ = result
            else:
                next_state, reward, terminated, truncated, _ = result
                done = terminated or truncated
            
            # 更新状态
            state = next_state
            total_reward += reward
        
        scores.append(total_reward)
        print(f"测试回合 {episode+1}: 得分 = {total_reward:.2f}")
    
    avg_score = np.mean(scores)
    print(f"平均测试得分: {avg_score:.2f}")
    
    env.close()
    return scores

def plot_training_results(scores: List[float], losses: List[float]):
    """
    绘制训练结果
    
    Args:
        scores: 得分历史
        losses: 损失历史
    """
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5))
    
    # 绘制得分
    ax1.plot(scores)
    ax1.set_title('训练过程得分')
    ax1.set_xlabel('回合数')
    ax1.set_ylabel('得分')
    ax1.grid(True)
    
    # 绘制平均得分(每100回合)
    if len(scores) > 100:
        avg_scores = [np.mean(scores[i:i+100]) for i in range(0, len(scores)-100)]
        ax1.plot(range(100, 100+len(avg_scores)), avg_scores, 'r-', linewidth=2, label='平均得分(100回合)')
        ax1.legend()
    
    # 绘制损失
    ax2.plot(losses)
    ax2.set_title('训练过程损失')
    ax2.set_xlabel('回合数')
    ax2.set_ylabel('损失')
    ax2.grid(True)
    
    # 绘制平均损失(每100回合)
    if len(losses) > 100:
        avg_losses = [np.mean(losses[i:i+100]) for i in range(0, len(losses)-100)]
        ax2.plot(range(100, 100+len(avg_losses)), avg_losses, 'r-', linewidth=2, label='平均损失(100回合)')
        ax2.legend()
    
    plt.tight_layout()
    plt.show()

执行训练和测试:

def main():
    """主函数:执行完整的训练和测试流程"""
    # 设置随机种子以确保结果可重现
    random.seed(42)
    np.random.seed(42)
    torch.manual_seed(42)
    
    # 训练标准DQN
    print("=" * 60)
    print("训练标准DQN智能体")
    print("=" * 60)
    
    dqn_agent, dqn_scores, dqn_losses = train_dqn(
        env_name='CartPole-v1',
        episodes=1000,
        network_type="dqn",
        save_model=True
    )
    
    # 绘制训练结果
    plot_training_results(dqn_scores, dqn_losses)
    
    # 测试标准DQN
    test_agent(dqn_agent, 'CartPole-v1', episodes=5, render=False)
    
    # 训练Dueling DQN
    print("\n" + "=" * 60)
    print("训练Dueling DQN智能体")
    print("=" * 60)
    
    dueling_agent, dueling_scores, dueling_losses = train_dqn(
        env_name='CartPole-v1',
        episodes=1000,
        network_type="dueling",
        save_model=True
    )
    
    # 绘制训练结果
    plot_training_results(dueling_scores, dueling_losses)
    
    # 测试Dueling DQN
    test_agent(dueling_agent, 'CartPole-v1', episodes=5, render=False)
    
    # 比较两种算法性能
    print("\n" + "=" * 60)
    print("性能比较")
    print("=" * 60)
    
    dqn_avg_score = np.mean(dqn_scores[-100:])
    dueling_avg_score = np.mean(dueling_scores[-100:])
    
    print(f"标准DQN最后100回合平均得分: {dqn_avg_score:.2f}")
    print(f"Dueling DQN最后100回合平均得分: {dueling_avg_score:.2f}")
    
    if dueling_avg_score > dqn_avg_score:
        print("Dueling DQN 表现更好!")
    else:
        print("标准DQN 表现更好!")

# 运行主函数
if __name__ == "__main__":
    main()

DQN的改进版本

随着研究的深入,研究人员提出了许多DQN的改进版本:

Double DQN (DDQN)

Double DQN 解决了DQN中Q值过高估计的问题:

class DoubleDQNAgent(DQNAgent):
    """
    Double DQN智能体实现
    """
    def replay(self) -> float:
        """
        重写replay方法以实现Double DQN算法
        """
        if len(self.memory) < self.batch_size:
            return 0.0
        
        experiences = random.sample(self.memory, self.batch_size)
        batch = Experience(*zip(*experiences))
        
        states = torch.FloatTensor(np.array(batch.state)).to(device)
        actions = torch.LongTensor(np.array(batch.action)).to(device)
        rewards = torch.FloatTensor(np.array(batch.reward)).to(device)
        next_states = torch.FloatTensor(np.array(batch.next_state)).to(device)
        dones = torch.BoolTensor(np.array(batch.done)).to(device)
        
        # 计算当前Q值
        current_q_values = self.q_network(states).gather(1, actions.unsqueeze(1))
        
        # Double DQN计算目标Q值
        with torch.no_grad():
            # 使用主网络选择最优动作
            next_actions = self.q_network(next_states).max(1)[1].unsqueeze(1)
            # 使用目标网络评估这些动作的Q值
            next_q_values = self.target_network(next_states).gather(1, next_actions).squeeze()
            target_q_values = rewards + (self.gamma * next_q_values * ~dones)
        
        # 计算损失
        loss = F.mse_loss(current_q_values.squeeze(), target_q_values)
        
        # 执行优化步骤
        self.optimizer.zero_grad()
        loss.backward()
        self.optimizer.step()
        
        # 降低ε值
        if self.epsilon > self.epsilon_end:
            self.epsilon *= self.epsilon_decay
        
        return loss.item()

Dueling DQN

Dueling DQN 将Q值分解为状态值函数和优势函数,我们在上面的代码中已经实现了这个网络结构。

DQN在Atari游戏中的应用

虽然我们的例子使用的是CartPole环境,但DQN最初是在Atari游戏中取得突破性成果的。在Atari游戏中,输入是原始像素帧,这对深度神经网络来说是一个巨大的挑战。

以下是DQN处理Atari游戏的关键技术:

  1. 预处理:将原始图像转换为灰度图并下采样
  2. 帧堆叠:将最近的4帧作为输入,以捕捉运动信息
  3. 卷积神经网络:使用CNN处理高维图像输入
  4. 跳帧:每隔几帧才采取一个动作,减少计算负担
class AtariDQN(nn.Module):
    """
    用于Atari游戏的DQN网络
    """
    def __init__(self, input_shape: Tuple[int, int, int], n_actions: int):
        """
        初始化Atari DQN网络
        
        Args:
            input_shape: 输入形状 (通道数, 高度, 宽度)
            n_actions: 动作数量
        """
        super(AtariDQN, self).__init__()
        
        self.conv = nn.Sequential(
            # 第一个卷积层
            nn.Conv2d(input_shape[0], 32, kernel_size=8, stride=4),
            nn.ReLU(),
            
            # 第二个卷积层
            nn.Conv2d(32, 64, kernel_size=4, stride=2),
            nn.ReLU(),
            
            # 第三个卷积层
            nn.Conv2d(64, 64, kernel_size=3, stride=1),
            nn.ReLU()
        )
        
        # 计算卷积层输出大小
        conv_out_size = self._get_conv_out(input_shape)
        
        # 全连接层
        self.fc = nn.Sequential(
            nn.Linear(conv_out_size, 512),
            nn.ReLU(),
            nn.Linear(512, n_actions)
        )
    
    def _get_conv_out(self, shape: Tuple[int, int, int]) -> int:
        """
        计算卷积层输出大小
        
        Args:
            shape: 输入形状
            
        Returns:
            卷积层输出大小
        """
        o = self.conv(torch.zeros(1, *shape))
        return int(np.prod(o.size()))
    
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        前向传播
        
        Args:
            x: 输入张量
            
        Returns:
            网络输出
        """
        conv_out = self.conv(x).view(x.size()[0], -1)
        return self.fc(conv_out)

def preprocess_frame(frame: np.ndarray) -> np.ndarray:
    """
    预处理Atari游戏帧
    
    Args:
        frame: 原始帧
        
    Returns:
        预处理后的帧
    """
    # 转换为灰度图
    gray = np.dot(frame[...,:3], [0.299, 0.587, 0.114])
    
    # 下采样到84x84
    from PIL import Image
    gray = Image.fromarray(gray)
    gray = gray.resize((84, 84), Image.BILINEAR)
    gray = np.array(gray)
    
    # 归一化
    gray = gray.astype(np.float32) / 255.0
    
    return gray
graph LR
    A[原始图像输入] --> B[预处理<br/>灰度化+下采样]
    B --> C[帧堆叠<br/>最近4帧]
    C --> D[CNN特征提取]
    D --> E[全连接层]
    E --> F[Q值输出]

性能优化与调试技巧

超参数调优

def hyperparameter_search():
    """
    超参数搜索示例
    """
    # 定义超参数搜索空间
    learning_rates = [0.001, 0.0005, 0.0001]
    batch_sizes = [32, 64, 128]
    gamma_values = [0.9, 0.95, 0.99]
    
    best_score = -float('inf')
    best_params = {}
    
    for lr in learning_rates:
        for batch_size in batch_sizes:
            for gamma in gamma_values:
                print(f"测试参数组合: lr={lr}, batch_size={batch_size}, gamma={gamma}")
                
                # 训练智能体
                agent, scores, _ = train_dqn(
                    episodes=500,
                    lr=lr,
                    batch_size=batch_size,
                    gamma=gamma
                )
                
                # 计算平均得分
                avg_score = np.mean(scores[-100:])
                print(f"平均得分: {avg_score:.2f}")
                
                # 更新最佳参数
                if avg_score > best_score:
                    best_score = avg_score
                    best_params = {
                        'lr': lr,
                        'batch_size': batch_size,
                        'gamma': gamma
                    }
    
    print(f"最佳参数: {best_params}")
    print(f"最佳得分: {best_score:.2f}")

本章小结

DQN是深度强化学习的一个重要里程碑,它成功地将深度学习的强大表征能力和Q-learning的强化学习框架结合起来。通过经验回放和固定Q目标这两个关键技术,DQN大大提高了训练的稳定性和效率。

在本节中,我们:

  1. 深入理解了DQN的原理和关键创新
  2. 动手实现了完整的DQN智能体,包括标准DQN和Dueling DQN
  3. 成功训练智能体解决了CartPole问题
  4. 了解了DQN的改进版本和在Atari游戏中的应用
  5. 掌握了性能优化和调试技巧

掌握了DQN之后,你已经具备了深度强化学习的基础,可以继续学习更高级的算法如PPO、A3C等。在下一节中,我们将深入探讨这些先进的深度强化学习算法。

练习题

  1. 修改网络结构,尝试更深的网络层数,观察训练效果变化
  2. 实现Double DQN算法并比较与标准DQN的性能差异
  3. 尝试调整超参数(学习率、折扣因子等),找出最佳组合
  4. 将DQN应用到其他Gym环境中,如MountainCar或Acrobot
  5. 实现优先经验回放(Prioritized Experience Replay)机制

💡 提示:强化学习算法的调试往往比监督学习更困难,因为其性能依赖于与环境的交互。建议在简单环境中充分测试算法后再扩展到复杂场景。使用训练监控工具可以帮助你更好地理解算法行为。