DQN 系列算法¶
当状态空间太大(如游戏画面),Q 表格装不下时,用神经网络来近似 Q 函数——这就是 Deep Q-Network(DQN)。DQN 是深度强化学习的开山之作,也是连接表格法与现代 RL 的桥梁。
补充:值函数近似¶


1、定义目标方程¶





2、优化算法¶


不同于上一章表格法的Q值更新,这里是参数 \(w\) 的更新。每次迭代都要计算 TD 误差,并用它来调整 \(w\),使得 \(V(s; w)\) 更接近 TD 目标。
3、TD算法 | 实例总结¶
采用贝尔曼方程求解如下(真实的state value):
采用TD算法(基于表格)求解如下(近似的state value):

采用TD算法(基于线性函数近似)求解如下(近似的state value):
- 一次方近似:

- 二次方、三次方近似:


4、SarSA算法、Q-learning算法(基于function approximation)¶

不同于表格法直接对比索引到的Q值,这里对比前都要先用函数计算出来。
用上面的式子去更新计算也是可以的,但这些计算过于底层,现在神经网络已经非常成熟了,我们直接用神经网络来近似这个函数就好了,这就是 DQN 的核心思想。
一、从 Q-Table 到 DQN¶
1.1 问题的由来¶
Q-learning 的 Q 表格有一个致命瓶颈:
| 环境 | 状态数量 | Q 表大小 |
|---|---|---|
| FrozenLake(4×4) | 16 | 16 × 4 = 64 |
| CartPole | 连续(无穷) | ❌ 无法建表 |
| Atari 游戏画面 | \(256^{210 \times 160 \times 3}\) | ❌ 天文数字 |
解决方案:用一个参数为 \(\theta\) 的神经网络 \(Q(s, a; \theta)\) 来代替 Q 表格。
1.2 DQN 的核心思路¶
- 输入:状态 \(s\)(如游戏画面像素)
- 输出:该状态下所有动作的 Q 值(\(Q(s, a_1), Q(s, a_2), \ldots\))
- 选动作:\(a = \arg\max_a Q(s, a; \theta)\)
为什么输出所有动作的 Q 值?
如果每次只输出一个 \((s, a)\) 对应的 Q 值,选动作时要把所有动作分别喂入网络——效率低。直接一次输出所有动作的 Q 值,一个前向传播搞定。
二、DQN 完整算法¶
2.1 损失函数¶
本质上是一个回归问题——让网络的输出逼近 TD 目标。
因为有2个\(w\),直接求导会很麻烦,所以引出2个network,把目标网络的参数\(wT\)固定住,不对它求导。这样就只需要对主要网络的参数\(w\)求导了。

2.2 算法伪代码¶
初始化在线网络 Q(θ) 和目标网络 Q(θ⁻),令 θ⁻ ← θ
初始化回放缓冲区 D(容量 N)
循环每个回合:
获取初始状态 s
循环每一步:
1. 用 ε-贪心选动作:a = argmax Q(s, ·; θ)(概率 1-ε)或随机(概率 ε)
2. 执行 a,观察 r, s', done
3. 将 (s, a, r, s', done) 存入 D
4. 从 D 中随机采样 mini-batch
5. 计算目标:y = r + γ·max_a' Q(s', a'; θ⁻) (若 done 则 y = r)
6. 梯度下降最小化 (y - Q(s, a; θ))²
7. 每 C 步:θ⁻ ← θ
8. s ← s'
2.3 网络结构¶
三、DQN 的两大关键技术 ⭐¶
直接把神经网络塞进 Q-learning 会导致训练极不稳定甚至发散。DeepMind 在 2013/2015 年提出了两个关键技术来解决这个问题。
3.1 经验回放(Experience Replay)¶
问题¶
在线 Q-learning 中,连续的样本 \((s_t, a_t, r_{t+1}, s_{t+1})\) 高度相关——前后两帧游戏画面几乎一样。用高度相关的数据训练神经网络会导致:
- 梯度估计偏差大
- 训练不稳定
- 容易遗忘之前学到的知识
解决方案¶
维护一个回放缓冲区(Replay Buffer) \(\mathcal{D}\),存储历史经验:
训练时从 \(\mathcal{D}\) 中随机采样一个小批量(mini-batch),打破数据的时间相关性。
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):
batch = random.sample(self.buffer, batch_size)
states, actions, rewards, next_states, dones = zip(*batch)
return np.array(states), actions, rewards, np.array(next_states), dones
回放缓冲区的关键参数
- 容量:通常 \(10^4\) ~ \(10^6\),太小不够多样,太大浪费内存
- 批量大小:通常 32 ~ 256
- 最小启动量:缓冲区至少积累一定数量的经验后才开始训练
3.2 目标网络(Target Network)¶
问题¶
Q-learning 的更新目标是:
但 \(Q\) 自身也在被训练更新——这就像在追赶一个不断移动的标靶,训练容易振荡甚至发散。
解决方案¶
维护两个结构相同的网络:
- 在线网络(Online Network) \(Q(s, a; \theta)\):每一步都更新
- 目标网络(Target Network) \(Q(s, a; \theta^-)\):参数冻结,每隔 \(C\) 步才同步
直觉理解
目标网络就像考试时的参考答案——你每隔一段时间才拿到一份新的参考答案,在这期间你朝着固定答案学习。这比答案时刻在变要稳定得多。
四、DQN 的改进变体¶
4.1 Double DQN(DDQN)¶
问题:Q 值过估计¶
标准 DQN 使用 \(\max_{a'} Q(s', a'; \theta^-)\),但 \(\max\) 操作会系统性地高估 Q 值:
当 Q 值估计不准确时(尤其是训练初期),\(\max\) 会选到那些恰巧被高估的动作,导致"盲目乐观"。
解决方案:动作选择与价值评估分离¶
只需修改一行代码:
# 标准 DQN
target = reward + gamma * target_net(next_state).max(1)[0]
# Double DQN
best_action = online_net(next_state).argmax(1) # 在线网络选动作
target = reward + gamma * target_net(next_state).gather(1, best_action.unsqueeze(1))
4.2 Dueling DQN¶
核心思想:分解 Q 值¶
直觉理解
站在悬崖边上(坏状态),无论你选什么动作都很危险——此时 \(V(s)\) 很低。在安全的大路上(好状态),大部分动作都不错——此时 \(V(s)\) 高。
将"状态好不好"和"动作好不好"分开评估,让网络学得更快更准。
网络结构¶
状态输入 → 共享特征提取层
├──→ V 分支 → FC → V(s) [标量]
└──→ A 分支 → FC → A(s,a) [动作数个值]
组合 → Q(s,a) = V(s) + A(s,a) - mean(A)
4.3 优先经验回放(PER)¶
问题¶
标准经验回放均匀随机采样,但有些经验比其他经验"价值"更高——那些 TD 误差大的经验说明模型对它们"很意外",应该优先学习。
解决方案¶
给每条经验赋予优先级,优先级与 TD 误差成正比:
- \(|\delta_i|\):TD 误差绝对值
- \(\epsilon\):小常数,防止优先级为 0
- \(\alpha\):控制优先级的程度(\(\alpha = 0\) 退化为均匀采样)
重要性采样修正
非均匀采样会引入偏差,需要用重要性权重修正:
\(\beta\) 从某个初始值逐渐增大到 1,在训练后期完全消除偏差。
五、DQN 变体对比总结¶
| 变体 | 解决的问题 | 核心改动 | 代码改动量 |
|---|---|---|---|
| DQN | 高维状态空间 | 经验回放 + 目标网络 | 基础版 |
| Double DQN | Q 值过估计 | 选动作和评估用不同网络 | ~3 行 |
| Dueling DQN | 状态价值和动作优势混为一谈 | 分为 V 分支和 A 分支 | 修改网络结构 |
| PER DQN | 经验利用效率低 | 按 TD 误差优先采样 | 更换采样器 |
| D3QN | 综合以上 | Double + Dueling | 组合应用 |
| DQN + CNN | 像素级输入 | 卷积网络提取视觉特征 | 修改网络结构 |
六、DQN 实战关键代码¶
6.1 网络定义(PyTorch)¶
import torch
import torch.nn as nn
class DQN(nn.Module):
def __init__(self, state_dim, action_dim):
super().__init__()
self.net = nn.Sequential(
nn.Linear(state_dim, 128),
nn.ReLU(),
nn.Linear(128, 128),
nn.ReLU(),
nn.Linear(128, action_dim)
)
def forward(self, x):
return self.net(x)
6.2 训练循环核心¶
def train_step(batch, online_net, target_net, optimizer, gamma):
states, actions, rewards, next_states, dones = batch
# 当前 Q 值
q_values = online_net(states).gather(1, actions.unsqueeze(1)).squeeze()
# 目标 Q 值(目标网络,无梯度)
with torch.no_grad():
next_q = target_net(next_states).max(1)[0]
target = rewards + gamma * next_q * (1 - dones)
# 损失 & 反向传播
loss = nn.MSELoss()(q_values, target)
optimizer.zero_grad()
loss.backward()
optimizer.step()
6.3 超参数参考¶
| 超参数 | 典型值 | 说明 |
|---|---|---|
| 学习率 | \(10^{-3}\) ~ \(10^{-4}\) | Adam 优化器 |
| 折扣因子 \(\gamma\) | 0.99 | 大多数任务 |
| 回放缓冲区大小 | \(10^4\) ~ \(10^6\) | 视内存而定 |
| 批量大小 | 32 ~ 64 | 常用 32 |
| 目标网络更新频率 | 100 ~ 1000 步 | 太频繁则不稳定 |
| \(\epsilon\) 起始/终止 | 1.0 → 0.01 | 线性衰减或指数衰减 |
| \(\epsilon\) 衰减步数 | \(10^4\) ~ \(10^5\) | 前期充分探索 |
关键公式速查¶
| 名称 | 公式 |
|---|---|
| DQN 目标 | \(y = r + \gamma \max_{a'} Q(s', a'; \theta^-)\) |
| DQN 损失 | \(L = (y - Q(s, a; \theta))^2\) |
| Double DQN 目标 | \(y = r + \gamma Q(s', \arg\max_{a'} Q(s', a'; \theta); \theta^-)\) |
| Dueling Q 值 | \(Q(s,a) = V(s) + A(s,a) - \text{mean}(A)\) |
| PER 优先级 | \(p_i = (\lvert\delta_i\rvert + \epsilon)^\alpha\) |