跳转至

DQN 系列算法

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


补充:值函数近似

20260320164718

20260320160829

1、定义目标方程

20260320160907

20260320160927

20260320160955

20260320161111

20260320161149

2、优化算法

20260320161638

20260320161954

20260320162021 不同于上一章表格法的Q值更新,这里是参数 \(w\) 的更新。每次迭代都要计算 TD 误差,并用它来调整 \(w\),使得 \(V(s; w)\) 更接近 TD 目标。

3、TD算法 | 实例总结

20260320162350 采用贝尔曼方程求解如下(真实的state value): 20260320162434 采用TD算法(基于表格)求解如下(近似的state value): 20260320162637

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

  • 一次方近似: 20260320162901 20260320162943
  • 二次方、三次方近似: 20260320163102 20260320163157

20260320162316

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

20260320183244

20260320183427 不同于表格法直接对比索引到的Q值,这里对比前都要先用函数计算出来。

20260320183632 用上面的式子去更新计算也是可以的,但这些计算过于底层,现在神经网络已经非常成熟了,我们直接用神经网络来近似这个函数就好了,这就是 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 的核心思路

\[ \text{Q-table}[s][a] \quad \Longrightarrow \quad Q(s, a; \theta) \approx Q^*(s, a) \]
  • 输入:状态 \(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 损失函数

\[ L(\theta) = \mathbb{E}_{(s,a,r,s') \sim \mathcal{D}} \left[ \left( r + \gamma \max_{a'} Q(s', a'; \theta^-) - Q(s, a; \theta) \right)^2 \right] \]

本质上是一个回归问题——让网络的输出逼近 TD 目标。

20260320184859 因为有2个\(w\),直接求导会很麻烦,所以引出2个network,把目标网络的参数\(wT\)固定住,不对它求导。这样就只需要对主要网络的参数\(w\)求导了。 20260320185209

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'
20260320193248

2.3 网络结构

状态向量 [4维] → FC(128) → ReLU → FC(128) → ReLU → Q值 [动作数]
画面 [84×84×4] → Conv(32,8,4) → ReLU → Conv(64,4,2) → ReLU 
→ Conv(64,3,1) → ReLU → FC(512) → ReLU → Q值 [动作数]

为什么输入是 4 帧?

单帧画面无法判断物体运动方向和速度。将连续 4 帧堆叠作为输入,让网络能感知"动态信息"。


三、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} = \{(s_i, a_i, r_i, s_i', \text{done}_i)\}_{i=1}^{N} \]

训练时从 \(\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 的更新目标是:

\[ y = r + \gamma \max_{a'} Q(s', a'; \theta) \]

\(Q\) 自身也在被训练更新——这就像在追赶一个不断移动的标靶,训练容易振荡甚至发散。

解决方案

维护两个结构相同的网络:

  • 在线网络(Online Network) \(Q(s, a; \theta)\):每一步都更新
  • 目标网络(Target Network) \(Q(s, a; \theta^-)\):参数冻结,每隔 \(C\) 步才同步
\[ y = r + \gamma \max_{a'} Q(s', a'; \theta^-) \quad \leftarrow \text{用固定的目标网络计算} \]

# 每 C 步同步一次
if step % target_update_freq == 0:
    target_net.load_state_dict(online_net.state_dict())
20260320190042

直觉理解

目标网络就像考试时的参考答案——你每隔一段时间才拿到一份新的参考答案,在这期间你朝着固定答案学习。这比答案时刻在变要稳定得多。


四、DQN 的改进变体

4.1 Double DQN(DDQN)

问题:Q 值过估计

标准 DQN 使用 \(\max_{a'} Q(s', a'; \theta^-)\),但 \(\max\) 操作会系统性地高估 Q 值:

\[ \mathbb{E}[\max_a Q(s, a)] \geq \max_a \mathbb{E}[Q(s, a)] \]

当 Q 值估计不准确时(尤其是训练初期),\(\max\) 会选到那些恰巧被高估的动作,导致"盲目乐观"。

解决方案:动作选择与价值评估分离

\[ y = r + \gamma Q\left(s', \underbrace{\arg\max_{a'} Q(s', a'; \theta)}_{\text{在线网络选动作}}\;;\; \theta^-\right) \quad \leftarrow \text{目标网络评估} \]

只需修改一行代码:

# 标准 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 值

\[ Q(s, a; \theta) = \underbrace{V(s; \theta_V)}_{\text{状态有多好}} + \underbrace{A(s, a; \theta_A)}_{\text{动作的相对优势}} - \frac{1}{|\mathcal{A}|}\sum_{a'} A(s, a'; \theta_A) \]

直觉理解

站在悬崖边上(坏状态),无论你选什么动作都很危险——此时 \(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 误差成正比:

\[ p_i = (|\delta_i| + \epsilon)^\alpha \]
\[ P(i) = \frac{p_i}{\sum_k p_k} \]
  • \(|\delta_i|\):TD 误差绝对值
  • \(\epsilon\):小常数,防止优先级为 0
  • \(\alpha\):控制优先级的程度(\(\alpha = 0\) 退化为均匀采样)

重要性采样修正

非均匀采样会引入偏差,需要用重要性权重修正:

\[ w_i = \left(\frac{1}{N \cdot P(i)}\right)^\beta \]

\(\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\)