深度强化学习DQN实战入门:基于2015 DeepMind论文的代码实现与避坑指南

1次阅读
没有评论

共计 3742 个字符,预计需要花费 10 分钟才能阅读完成。

image.webp

为什么需要 DQN:传统 Q -Learning 的局限性

传统 Q -Learning 在离散状态空间表现良好,但当面对像图像像素这样的高维输入时,会遇到两个致命问题:

深度强化学习 DQN 实战入门:基于 2015 DeepMind 论文的代码实现与避坑指南

  1. 维度灾难 :状态 - 动作组合呈指数级增长,传统 Q 表无法存储。例如 Atari 游戏的 210×160 像素画面有 256^(210×160) 种可能状态
  2. 泛化困难:相似状态需要独立学习,无法共享经验。人类看到新场景能借鉴相似经验,但 Q -Learning 做不到

2015 年 DeepMind 的突破性论文《Human-level control through deep reinforcement learning》提出用神经网络替代 Q 表,实现端到端学习。这种 Deep Q-Network(DQN)解决了上述问题:

  • 神经网络自动提取特征,将原始像素映射到低维表示
  • 参数共享机制使相似状态能复用学习到的模式

DQN vs 其他强化学习算法

算法 适用场景 训练稳定性 数据效率 实现难度
DQN 离散动作空间 中等(需目标网络) 高(经验回放) 中等
Policy Gradient 连续 / 离散动作 低(高方差) 较高
A3C 连续 / 离散动作 较高(并行探索)

DQN 两大核心技术实现

经验回放缓冲区(带环形队列优化)

import numpy as np
from collections import deque
import random

class ReplayBuffer:
    def __init__(self, capacity):
        self.buffer = deque(maxlen=capacity)  # 自动丢弃旧数据的环形队列

    def push(self, state, action, reward, next_state, done):
        self.buffer.append((state, action, reward, next_state, done))

    def sample(self, batch_size):
        transitions = random.sample(self.buffer, batch_size)
        # 解压成五个独立的 numpy 数组
        return zip(*transitions)

    def __len__(self):
        return len(self.buffer)

关键点:
– 使用 dequemaxlen参数自动维护固定容量
– 随机采样打破时间相关性,提高数据效率
– 典型容量设为 1e5~1e6,batch_size 常用 32~256

目标网络数学原理

原始 Q -learning 的更新目标:

y = r + γ * max_a Q(s',a)

会导致 ” 追逐自己尾巴 ” 的问题——用不断变化的 Q 值估计去更新自身。

DQN 引入目标网络 Q ’ 后:

y = r + γ * max_a Q'(s',a)

其中 Q ’ 的参数 θ ’ 每隔 C 步从主网络 Q 复制而来。数学上证明这个操作能使 TD 误差的方差降低:

Var[r + γQ'(s',a') - Q(s,a)] ≤ Var[r + γQ(s',a') - Q(s,a)]

完整 PyTorch 实现(CartPole 环境)

网络架构定义

import torch
import torch.nn as nn
import torch.optim as optim

class DQN(nn.Module):
    """
    输入:state 维度 (4,)
    输出:各 action 的 Q 值 (2,)
    """
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(4, 128)  # CartPole 状态有 4 个特征
        self.fc2 = nn.Linear(128, 128)
        self.fc3 = nn.Linear(128, 2)  # 两个动作:左 / 右

    def forward(self, x):
        x = torch.relu(self.fc1(x))
        x = torch.relu(self.fc2(x))
        return self.fc3(x)

带 ε 衰减的探索策略

class EpsilonGreedy:
    def __init__(self, start=1.0, end=0.01, decay=0.995):
        self.start = start
        self.end = end
        self.decay = decay
        self.epsilon = start

    def get_action(self, q_values):
        if random.random() < self.epsilon:
            return random.randint(0, 1)  # 随机探索
        return torch.argmax(q_values).item()  # 选择最优动作

    def update_epsilon(self):
        self.epsilon = max(self.end, self.epsilon * self.decay)

训练循环关键片段

def train_step(batch, policy_net, target_net, optimizer, gamma=0.99):
    states, actions, rewards, next_states, dones = batch

    # 计算当前 Q 值 (s,a)
    current_q = policy_net(states).gather(1, actions.unsqueeze(1))

    # 计算目标 Q 值
    with torch.no_grad():  # 目标网络不计算梯度
        next_q = target_net(next_states).max(1)[0]
        target = rewards + gamma * next_q * (1 - dones)  # done 时忽略下一步

    # 计算 MSE 损失
    loss = nn.MSELoss()(current_q.squeeze(), target)

    # 反向传播
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    return loss.item()

注意处理 done 信号:当回合结束时,next_state的 Q 值不应参与计算。

实战调参指南

超参数经验值

参数 推荐范围 影响
学习率 1e-4 ~ 1e-3 过大导致震荡,过小收敛慢
batch_size 32 ~ 256 太小噪声大,太大收敛慢
γ (折扣因子) 0.9 ~ 0.99 越小越关注即时奖励
ε 衰减 0.99 ~ 0.999 控制探索到利用的转换速度

常见问题诊断

出现 NaN 损失时:
1. 检查 reward 是否未归一化(特别是 Atari 游戏)
2. 添加梯度裁剪:nn.utils.clip_grad_norm_(model.parameters(), 10)
3. 检查网络输出是否出现 inf(添加 torch.isnan().any() 检测)

TensorBoard 监控要点:

from torch.utils.tensorboard import SummaryWriter

writer = SummaryWriter()
# 在训练循环中添加:writer.add_scalar('Loss/train', loss, step)
writer.add_scalar('Reward/episode', episode_reward, episode)
writer.add_scalar('Epsilon', epsilon, step)

进阶方向思考

Double DQN 的改进

原始 DQN 的 max 操作会导致 Q 值过估计:

y = r + γ * Q'(s', argmax_a Q(s',a))

Double DQN 将动作选择和值评估解耦:

y = r + γ * Q'(s', argmax_a Q(s',a))

实现只需修改训练代码:

with torch.no_grad():
    # 用 policy_net 选择动作
    next_actions = policy_net(next_states).max(1)[1]  
    # 用 target_net 评估值
    next_q = target_net(next_states).gather(1, next_actions.unsqueeze(1))

扩展为 Dueling DQN

修改网络结构即可:

class DuelingDQN(nn.Module):
    def __init__(self):
        super().__init__()
        self.feature = nn.Sequential(nn.Linear(4, 128),
            nn.ReLU())
        self.advantage = nn.Sequential(nn.Linear(128, 128),
            nn.ReLU(),
            nn.Linear(128, 2)
        )
        self.value = nn.Sequential(nn.Linear(128, 128),
            nn.ReLU(),
            nn.Linear(128, 1)
        )

    def forward(self, x):
        x = self.feature(x)
        advantage = self.advantage(x)
        value = self.value(x)
        return value + (advantage - advantage.mean(1, keepdim=True))

结语

通过这次实现,我深刻体会到 DQN 将深度学习与强化学习结合的巧妙之处。建议初学者:
1. 先用 CartPole 这类简单环境验证代码正确性
2. 逐步添加优先级回放、n-step return 等扩展
3. 尝试在 Atari 游戏上复现论文结果

强化学习就像教小孩学骑车——需要平衡探索(尝试新动作)和利用(使用已知好动作),而 DQN 给出了一个优雅的解决方案。

正文完
 0
评论(没有评论)