共计 3742 个字符,预计需要花费 10 分钟才能阅读完成。
为什么需要 DQN:传统 Q -Learning 的局限性
传统 Q -Learning 在离散状态空间表现良好,但当面对像图像像素这样的高维输入时,会遇到两个致命问题:

- 维度灾难 :状态 - 动作组合呈指数级增长,传统 Q 表无法存储。例如 Atari 游戏的 210×160 像素画面有 256^(210×160) 种可能状态
- 泛化困难:相似状态需要独立学习,无法共享经验。人类看到新场景能借鉴相似经验,但 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)
关键点:
– 使用 deque 的maxlen参数自动维护固定容量
– 随机采样打破时间相关性,提高数据效率
– 典型容量设为 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 给出了一个优雅的解决方案。
