深度Q网络(DQN):经验回放、目标网络

一句话概述

深度Q网络(DQN)是深度学习与强化学习结合的里程碑——它用神经网络替代Q表,解决了高维状态空间(如Atari游戏画面)的Q-learning问题。DQN的两大创新是经验回放(Experience Replay)和目标网络(Target Network),前者打破数据相关性,后者稳定训练目标。2015年DQN在49款Atari游戏中达到甚至超越人类水平,开启了深度强化学习时代。

💡 核心要点:①DQN用神经网络Q(s,a;θ)近似Q函数,输入状态输出每个动作的Q值;②经验回放将交互数据存入经验池,随机采样训练,打破数据相关性;③目标网络使用旧参数计算TD目标,减少训练的不稳定性;④DQN的损失函数是TD误差的平方:L(θ)=E[(r+γmax_{a'}Q(s',a';θ^-)-Q(s,a;θ))²]。

教学与演示

一、从Q表到神经网络——为什么需要DQN

是什么(定义):DQN(Deep Q-Network)使用深度神经网络来近似Q函数Q(s,a;θ)≈Q*(s,a),其中θ是网络参数。输入是状态s(如游戏画面),输出是每个动作a的Q值。这使得DQN能够处理高维、连续的状态空间,而传统的Q表方法只能处理离散小状态空间。

大白话 Q表就像一本"棋谱"——每个局面(状态)对应一个最佳走法。但如果局面太多(如Atari游戏的像素画面),棋谱就写不下了。DQN用神经网络代替棋谱——输入画面,输出每个操作的"评分",不需要记住所有局面,而是学会"看画面给评分"。

为什么(原理):Q表需要为每个状态-动作对存储一个Q值,当状态空间很大(如210×160像素的游戏画面)时完全不可行。神经网络通过参数共享和泛化能力,可以为相似的状态输出相似的Q值,大大减少了参数数量。此外,卷积神经网络(CNN)可以自动提取图像特征,无需人工设计特征。

怎么做(实现)

import numpy as np

# ==================== DQN网络结构概念演示 ====================
# 实际DQN使用PyTorch/TensorFlow,这里用numpy展示概念

class DQNNetwork:
    """DQN神经网络的概念实现(简化版)"""
    def __init__(self, input_dim, hidden_dim, output_dim):
        # 简化:使用两层全连接网络
        self.W1 = np.random.randn(input_dim, hidden_dim) * 0.1
        self.b1 = np.zeros(hidden_dim)
        self.W2 = np.random.randn(hidden_dim, output_dim) * 0.1
        self.b2 = np.zeros(output_dim)

    def forward(self, state):
        """前向传播:输入状态,输出所有动作的Q值"""
        h = np.maximum(0, state @ self.W1 + self.b1)  # ReLU激活
        q_values = h @ self.W2 + self.b2  # 输出Q值
        return q_values  # shape: (n_actions,)

# 对比:Q表 vs DQN
print("=== Q表 vs DQN网络 ===")
print("Q表方法:")
print("  状态空间 10^6 个状态 × 4 个动作 = 4×10^6 个参数")
print("  需要存储和更新每个状态-动作对的Q值")
print("  无法泛化到相似状态")
print()
print("DQN方法:")
print("  输入层(84×84×4=28224) → 卷积层 → 全连接层 → 输出(4或18)")
print("  参数量约 170万(与状态数无关)")
print("  相似状态自动输出相似Q值(泛化)")
print("  端到端学习:从像素到Q值")

# 演示:神经网络的泛化能力
network = DQNNetwork(input_dim=4, hidden_dim=8, output_dim=3)
np.random.seed(42)
state1 = np.array([1.0, 2.0, 3.0, 4.0])
state2 = np.array([1.1, 2.0, 3.0, 4.0])  # 与state1非常相似
state3 = np.array([10.0, 20.0, 30.0, 40.0])  # 与state1完全不同

q1 = network.forward(state1)
q2 = network.forward(state2)
q3 = network.forward(state3)

print(f"\n神经网络的泛化能力演示:")
print(f"  状态1的Q值: {[f'{q:.2f}' for q in q1]}")
print(f"  状态2的Q值(相似): {[f'{q:.2f}' for q in q2]}")
print(f"  状态3的Q值(不同): {[f'{q:.2f}' for q in q3]}")
print(f"  Q1与Q2相似(泛化),Q1与Q3不同(区分)")
DQN的Q函数近似\(Q(s, a; \theta) \approx Q^{*}(s, a), \quad \text{输入: } s, \quad \text{输出: } Q(s, a_1), Q(s, a_2), \ldots, Q(s, a_n)\)

什么用(应用):DQN在Atari 2600游戏上取得了突破性成果,在49款游戏中达到或超越人类水平。DQN的思想启发了后续所有深度RL方法。在推荐系统中,DQN可以处理高维用户特征;在机器人控制中,DQN可以处理原始传感器输入。

哪些坑(缺点):神经网络训练不稳定,需要大量技巧;过估计问题在DQN中同样存在;CNN需要大量训练数据;训练时间很长(原始DQN需要训练数百万帧)。

二、经验回放——打破数据相关性

是什么(定义):经验回放(Experience Replay)是DQN的核心创新之一。智能体将每次交互的经验(s, a, r, s')存储到经验池(Replay Buffer)中,训练时从经验池中随机采样一个小批量(mini-batch)来更新网络。这打破了连续交互数据之间的相关性,使得训练更稳定。

大白话 经验回放就像"复习笔记"——智能体不只看最近发生的事,而是把所有的经历都记下来,训练时随机抽取一些来"复习"。这样就不会"好了伤疤忘了疼",也不会"只记住最近的事而忘记以前的教训"。

为什么(原理):连续交互数据存在强相关性——相邻的状态几乎相同,这会导致神经网络训练不稳定(梯度更新方向高度相关,可能震荡)。经验回放通过随机采样打破了这种相关性,使训练数据更接近独立同分布(i.i.d.),这是监督学习中梯度下降有效的前提。此外,经验回放还提高了样本效率——每条经验可以被多次使用。

怎么做(实现)

import numpy as np
from collections import deque

# ==================== 经验回放池实现 ====================
class ReplayBuffer:
    """经验回放池"""
    def __init__(self, capacity=10000):
        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=32):
        """随机采样一个batch"""
        indices = np.random.choice(len(self.buffer), batch_size, replace=False)
        batch = [self.buffer[i] for i in indices]
        # 解压为独立的数组
        states = np.array([b[0] for b in batch])
        actions = np.array([b[1] for b in batch])
        rewards = np.array([b[2] for b in batch])
        next_states = np.array([b[3] for b in batch])
        dones = np.array([b[4] for b in batch])
        return states, actions, rewards, next_states, dones

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

# 演示经验回放
np.random.seed(42)
buffer = ReplayBuffer(capacity=100)

# 模拟收集经验
print("=== 经验回放演示 ===")
print("收集经验...")
for step in range(50):
    state = np.random.randn(4)  # 模拟状态
    action = np.random.randint(4)
    reward = np.random.randn()
    next_state = np.random.randn(4)
    done = False
    buffer.push(state, action, reward, next_state, done)

print(f"经验池大小: {len(buffer)}")

# 采样一个batch
states, actions, rewards, next_states, dones = buffer.sample(batch_size=8)
print(f"\n采样batch大小: {len(states)}")
print(f"状态shape: {states.shape}")  # (8, 4)
print(f"动作shape: {actions.shape}")  # (8,)
print(f"奖励shape: {rewards.shape}")  # (8,)

# 展示经验回放如何打破相关性
print(f"\n连续收集的3条经验(高相关性):")
for i in range(3):
    print(f"  经验{i}: state={[f'{x:.2f}' for x in buffer.buffer[i][0][:2]]}...")

print(f"\n随机采样的3条经验(低相关性):")
indices = np.random.choice(len(buffer), 3, replace=False)
for i, idx in enumerate(indices):
    print(f"  经验{i}: state={[f'{x:.2f}' for x in buffer.buffer[idx][0][:2]]}...")

print(f"\n经验回放的优势:")
print(f"  1. 打破数据相关性:随机采样而非连续采样")
print(f"  2. 提高样本效率:每条经验可被多次使用")
print(f"  3. 稳定训练:减少梯度更新的方差")
经验回放中的损失函数\(\mathcal{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]\)

什么用(应用):经验回放是DQN成功的关键,也是后续Off-Policy方法(如DDPG、SAC)的标准组件。在推荐系统中,经验回放存储用户的历史交互;在机器人控制中,经验回放存储传感器数据和控制信号。HER(Hindsight Experience Replay)通过"事后诸葛亮"的方式重新标记经验,进一步提高了样本效率。

哪些坑(缺点):经验池需要大量内存;随机采样忽略了经验的重要性差异(催生了优先经验回放);只适用于Off-Policy方法;经验池中的旧数据可能过时(非平稳环境)。

三、目标网络——稳定训练目标

是什么(定义):目标网络(Target Network)是DQN的第二个核心创新。DQN维护两个Q网络:当前Q网络Q(s,a;θ)(用于选择动作和更新)和目标Q网络Q(s,a;θ^-)(用于计算TD目标)。目标网络的参数θ^-定期从θ复制(如每C步复制一次),在其他时间保持不变。

大白话 目标网络就像"锚"——普通Q-learning中,TD目标r+γmax Q(s',a')使用的Q值和要更新的Q值是同一个,这就像"移动靶子"(你瞄准的同时靶子也在动)。目标网络把目标计算用的Q网络"冻结"一段时间,让靶子固定,训练更稳定。

为什么(原理):在标准Q-learning中,TD目标r+γmax Q(s',a';θ)和当前估计Q(s,a;θ)使用相同的参数θ,这导致更新目标和参数之间高度相关。θ更新后,TD目标也立即改变,形成"追逐自己的尾巴"的不稳定动力学。目标网络通过固定θ^-,让TD目标在一段时间内保持稳定,打破了这种相关性。

怎么做(实现)

import numpy as np

# ==================== DQN训练(概念实现) ====================
class DQN:
    """DQN的简化概念实现"""
    def __init__(self, state_dim, n_actions, hidden_dim=32, lr=0.001, gamma=0.99):
        self.state_dim = state_dim
        self.n_actions = n_actions
        self.gamma = gamma
        self.lr = lr

        # 当前Q网络(Q-network)
        self.W1 = np.random.randn(state_dim, hidden_dim) * 0.1
        self.b1 = np.zeros(hidden_dim)
        self.W2 = np.random.randn(hidden_dim, n_actions) * 0.1
        self.b2 = np.zeros(n_actions)

        # 目标Q网络(Target Network)
        self.target_W1 = self.W1.copy()
        self.target_b1 = self.b1.copy()
        self.target_W2 = self.W2.copy()
        self.target_b2 = self.b2.copy()

        self.update_counter = 0
        self.target_update_freq = 100  # 每100步更新一次目标网络

    def forward(self, state, use_target=False):
        """前向传播计算Q值"""
        if use_target:
            W1, b1, W2, b2 = self.target_W1, self.target_b1, self.target_W2, self.target_b2
        else:
            W1, b1, W2, b2 = self.W1, self.b1, self.W2, self.b2
        h = np.maximum(0, state @ W1 + b1)
        return h @ W2 + b2

    def update(self, states, actions, rewards, next_states, dones):
        """使用经验回放和双网络更新"""
        batch_size = len(states)

        # 当前网络计算Q(s,a)
        q_values = self.forward(states)  # (batch, n_actions)
        q_current = q_values[np.arange(batch_size), actions]  # 选中动作的Q值

        # 目标网络计算max Q(s',a')
        next_q_values = self.forward(next_states, use_target=True)  # 使用目标网络!
        max_next_q = np.max(next_q_values, axis=1)
        # TD目标(考虑done)
        q_target = rewards + self.gamma * max_next_q * (1 - dones)

        # 计算梯度(简化:只展示概念)
        td_error = q_target - q_current
        loss = np.mean(td_error ** 2)

        # 更新计数器
        self.update_counter += 1
        if self.update_counter % self.target_update_freq == 0:
            self.update_target_network()

        return loss

    def update_target_network(self):
        """将当前网络的参数复制到目标网络"""
        self.target_W1 = self.W1.copy()
        self.target_b1 = self.b1.copy()
        self.target_W2 = self.W2.copy()
        self.target_b2 = self.b2.copy()
        print(f"  [目标网络已更新] (step {self.update_counter})")

# 演示
np.random.seed(42)
dqn = DQN(state_dim=4, n_actions=3)
print("=== DQN训练演示 ===")
print("训练10个batch,每100步更新目标网络...")
for step in range(10):
    states = np.random.randn(32, 4)
    actions = np.random.randint(0, 3, 32)
    rewards = np.random.randn(32)
    next_states = np.random.randn(32, 4)
    dones = np.zeros(32)
    loss = dqn.update(states, actions, rewards, next_states, dones)
    if step % 3 == 0:
        print(f"  Step {step+1}: loss={loss:.4f}")

print(f"\n目标网络的作用:")
print(f"  1. 固定TD目标的计算,避免'移动靶子'问题")
print(f"  2. 定期从当前网络复制参数(如每100-10000步)")
print(f"  3. 打破Q值和TD目标之间的相关性")
DQN的损失函数与目标网络\(\mathcal{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]\)

什么用(应用):目标网络是DQN稳定训练的关键,也是后续DDPG、TD3、SAC等算法的标配。在DDPG中,Actor和Critic都有目标网络;在SAC中,使用了软更新(Polyak averaging)而非硬复制。目标网络的思想可以推广到任何需要稳定训练目标的场景。

哪些坑(缺点):目标网络更新频率需要调参——太频繁则不稳定,太稀疏则收敛慢;硬更新(直接复制)可能导致目标突变,软更新(滑动平均)更平滑但收敛更慢;目标网络增加了内存开销。

四、DQN完整训练流程

是什么(定义):DQN的完整训练流程包括:环境交互(使用ε-greedy)、经验存储(push到经验池)、经验采样(从经验池随机采样)、网络更新(计算TD误差和梯度)、目标网络更新(定期复制参数)。这个流程循环进行,直到策略收敛。

大白话 DQN的训练就像"边玩边学"——智能体玩游戏(ε-greedy探索),把所有经历记下来,时不时从记忆里随机抽取一些来"复习"(经验回放),复习时用"老的评分标准"(目标网络)来给自己打分,防止自己骗自己。玩久了,就越来越会玩了。

为什么(原理):DQN训练流程的每个环节都有其设计目的:ε-greedy确保探索,经验回放确保数据多样性和独立性,目标网络确保训练稳定,梯度下降确保参数向最优方向更新。这些组件协同工作,使得在原始高维像素输入下学习成为可能。

怎么做(实现)

import numpy as np
from collections import deque

# ==================== DQN完整训练流程 ====================
# 使用简单的网格世界环境

n_states = 9; n_actions = 4
actions = [(-1, 0), (1, 0), (0, -1), (0, 1)]

def step(state, action):
    row, col = state // 3, state % 3
    dr, dc = actions[action]
    nr = max(0, min(2, row + dr))
    nc = max(0, min(2, col + dc))
    next_state = nr * 3 + nc
    if next_state == 8:   r = 10.0
    elif next_state == 7: r = -5.0
    else:                 r = -0.1
    return next_state, r, (next_state in [7, 8])

# 将状态编码为one-hot向量(模拟高维输入)
def state_to_vector(state):
    vec = np.zeros(n_states)
    vec[state] = 1.0
    return vec

class SimpleDQN:
    def __init__(self, state_dim, n_actions, hidden_dim=32):
        # 网络参数
        self.W1 = np.random.randn(state_dim, hidden_dim) * 0.1
        self.b1 = np.zeros(hidden_dim)
        self.W2 = np.random.randn(hidden_dim, n_actions) * 0.1
        self.b2 = np.zeros(n_actions)
        # 目标网络
        self.tW1 = self.W1.copy(); self.tb1 = self.b1.copy()
        self.tW2 = self.W2.copy(); self.tb2 = self.b2.copy()
        self.n_actions = n_actions

    def get_q(self, state_vec, use_target=False):
        W1, b1, W2, b2 = (self.tW1, self.tb1, self.tW2, self.tb2) if use_target else (self.W1, self.b1, self.W2, self.b2)
        h = np.maximum(0, state_vec @ W1 + b1)
        return h @ W2 + b2

    def update(self, batch, gamma=0.9, lr=0.01):
        states, actions, rewards, next_states, dones = batch
        batch_size = len(states)
        # 当前网络Q值
        q_values = self.get_q(states)
        q_curr = q_values[np.arange(batch_size), actions]
        # 目标网络Q值
        next_q = self.get_q(next_states, use_target=True)
        max_next_q = np.max(next_q, axis=1)
        q_target = rewards + gamma * max_next_q * (1 - dones)
        # 简化梯度更新(概念演示)
        td_error = q_target - q_curr
        return np.mean(td_error ** 2)

    def update_target(self):
        self.tW1 = self.W1.copy(); self.tb1 = self.b1.copy()
        self.tW2 = self.W2.copy(); self.tb2 = self.b2.copy()

# 训练DQN
np.random.seed(42)
dqn = SimpleDQN(n_states, n_actions, hidden_dim=32)
buffer = deque(maxlen=500)
epsilon = 1.0
episode_rewards = []

for episode in range(200):
    state = 0; total = 0
    while state not in [7, 8]:
        # ε-greedy
        if np.random.random() < epsilon:
            action = np.random.randint(n_actions)
        else:
            q = dqn.get_q(state_to_vector(state))
            action = np.argmax(q)

        next_state, reward, done = step(state, action)
        buffer.append((state_to_vector(state), action, reward, state_to_vector(next_state), float(done)))
        total += reward; state = next_state

        # 经验回放训练
        if len(buffer) >= 32:
            indices = np.random.choice(len(buffer), 32, replace=False)
            batch = [buffer[i] for i in indices]
            s_batch = np.array([b[0] for b in batch])
            a_batch = np.array([b[1] for b in batch])
            r_batch = np.array([b[2] for b in batch])
            ns_batch = np.array([b[3] for b in batch])
            d_batch = np.array([b[4] for b in batch])
            dqn.update((s_batch, a_batch, r_batch, ns_batch, d_batch))

    epsilon = max(0.05, epsilon * 0.995)
    if episode % 20 == 0:
        dqn.update_target()
    episode_rewards.append(total)

print("=== DQN训练结果 ===")
print(f"前10ep奖励: {[f'{r:.1f}' for r in episode_rewards[:10]]}")
print(f"最后10ep平均: {np.mean(episode_rewards[-10:]):.2f}")
print(f"\nDQN训练流程总结:")
print(f"  1. ε-greedy与环境交互,收集经验")
print(f"  2. 经验存入回放池")
print(f"  3. 从回放池随机采样训练")
print(f"  4. 目标网络计算TD目标")
print(f"  5. 定期更新目标网络")

什么用(应用):DQN的训练流程是深度RL的经典范式。DDPG、TD3、SAC等算法都沿用了类似的经验回放+目标网络架构。理解这个流程,就理解了深度RL工程实现的核心。

哪些坑(缺点):训练不稳定(需要精心调参);需要大量交互数据(DQN通常需要数百万帧);超参数(学习率、ε衰减、目标网络更新频率、经验池大小)对性能影响大。

五、DQN的改进方向

是什么(定义):DQN虽然取得了突破性成果,但仍有多个改进方向:Double DQN(解决过估计)、Dueling DQN(改进网络结构)、优先经验回放(提高采样效率)、Rainbow(整合多种改进)。这些改进推动了DQN系列算法的持续发展。

大白话 DQN就像第一代iPhone——很惊艳但还有很多可以改进的地方。后续的Double DQN、Dueling DQN等就像iPhone的升级版,在各自的方面做了优化。而Rainbow就把所有升级打包在一起,成为了"终极版"。

为什么(原理):DQN的每个改进方向都针对一个具体问题:Double DQN解决过估计,Dueling DQN改进网络结构使其更好地区分状态价值和动作优势,优先经验回放让重要经验被更频繁地学习,Noisy DQN用参数噪声替代ε-greedy进行更高效的探索。Rainbow将这些改进整合到一起,在Atari游戏上取得了最佳结果。

怎么做(实现)

import numpy as np

# ==================== DQN改进方向概述 ====================
print("=== DQN改进方向总览 ===")
print()
improvements = [
    ("原始DQN", "2015", "经验回放 + 目标网络", "基础架构"),
    ("Double DQN", "2016", "分离动作选择和评估", "解决过估计"),
    ("Dueling DQN", "2016", "V(s) + A(s,a) 分解", "更好的网络结构"),
    ("Prioritized Replay", "2016", "按TD误差大小优先采样", "提高样本效率"),
    ("Noisy DQN", "2018", "参数噪声替代ε-greedy", "更高效的探索"),
    ("Rainbow", "2018", "整合以上所有改进", "当前最佳DQN"),
]
for name, year, idea, benefit in improvements:
    print(f"  {name:<20} ({year})  {idea:<30} → {benefit}")

print(f"\nRainbow整合了6大改进,在Atari 57款游戏中达到最优")
print(f"截至2018年,Rainbow是离散动作空间中最强的RL算法之一")

# 量化对比:理论上各改进对性能的贡献
print(f"\n各改进在Atari游戏上的性能提升(相对DQN):")
print(f"  DQN基准:              100%")
print(f"  + Double DQN:         ~115%")
print(f"  + Dueling DQN:        ~120%")
print(f"  + Prioritized Replay: ~130%")
print(f"  + Noisy Nets:         ~140%")
print(f"  Rainbow (全部):       ~180%")

什么用(应用):理解DQN的改进方向有助于在实际项目中选择合适的架构。离散动作任务→Rainbow(如果资源充足)或Double DQN(如果追求简单);需要高效探索→Noisy DQN;数据收集成本高→Prioritized Replay。

哪些坑(缺点):Rainbow集成了多种改进,实现复杂,调试困难;各改进的超参数相互影响,调参难度大;在某些简单任务上,改进可能没有明显收益。

六、实战:构建DQN智能体

是什么(定义):本节综合DQN的三个核心组件(神经网络、经验回放、目标网络),构建一个完整的DQN智能体,在网格世界上训练并评估其表现。

怎么做(实现)

import numpy as np
from collections import deque

n_states = 9; n_actions = 4
actions = [(-1, 0), (1, 0), (0, -1), (0, 1)]

def step(state, action):
    row, col = state // 3, state % 3
    dr, dc = actions[action]
    nr = max(0, min(2, row + dr))
    nc = max(0, min(2, col + dc))
    next_state = nr * 3 + nc
    if next_state == 8:   r = 10.0
    elif next_state == 7: r = -5.0
    else:                 r = -0.1
    return next_state, r, next_state in [7, 8]

def to_vec(s):
    vec = np.zeros(n_states); vec[s] = 1.0; return vec

class DQNAgent:
    def __init__(self):
        hidden = 32
        self.W1 = np.random.randn(n_states, hidden) * 0.1; self.b1 = np.zeros(hidden)
        self.W2 = np.random.randn(hidden, n_actions) * 0.1; self.b2 = np.zeros(n_actions)
        self.tW1 = self.W1.copy(); self.tb1 = self.b1.copy()
        self.tW2 = self.W2.copy(); self.tb2 = self.b2.copy()
        self.buffer = deque(maxlen=1000)
        self.epsilon = 1.0

    def get_q(self, s_vec, target=False):
        w1, b1, w2, b2 = (self.tW1, self.tb1, self.tW2, self.tb2) if target else (self.W1, self.b1, self.W2, self.b2)
        h = np.maximum(0, s_vec @ w1 + b1)
        return h @ w2 + b2

    def act(self, state):
        if np.random.random() < self.epsilon:
            return np.random.randint(n_actions)
        return np.argmax(self.get_q(to_vec(state)))

    def train(self, batch_size=32, gamma=0.9, lr=0.01):
        if len(self.buffer) < batch_size: return
        idx = np.random.choice(len(self.buffer), batch_size, replace=False)
        batch = [self.buffer[i] for i in idx]
        s = np.array([b[0] for b in batch]); a = np.array([b[1] for b in batch])
        r = np.array([b[2] for b in batch]); ns = np.array([b[3] for b in batch])
        d = np.array([b[4] for b in batch])
        q = self.get_q(s); q_curr = q[np.arange(batch_size), a]
        nq = self.get_q(ns, target=True); max_nq = np.max(nq, axis=1)
        target = r + gamma * max_nq * (1 - d)
        td = target - q_curr
        # 简化的梯度更新
        h = np.maximum(0, s @ self.W1 + self.b1)
        dW2 = h.T @ (td[:, None] * (np.eye(n_actions)[a] - np.eye(n_actions)[a] * 0)) / batch_size
        self.W2[a] += lr * td.reshape(-1, 1) * h[a]  # 简化

    def update_target(self):
        self.tW1 = self.W1.copy(); self.tb1 = self.b1.copy()
        self.tW2 = self.W2.copy(); self.tb2 = self.b2.copy()

agent = DQNAgent(); rewards = []
for ep in range(300):
    state = 0; total = 0
    while state not in [7, 8]:
        action = agent.act(state)
        next_state, reward, done = step(state, action)
        agent.buffer.append((to_vec(state), action, reward, to_vec(next_state), float(done)))
        total += reward; state = next_state
        agent.train()
    agent.epsilon = max(0.05, agent.epsilon * 0.995)
    if ep % 50 == 0: agent.update_target()
    rewards.append(total)

print(f"DQN智能体训练完成!")
print(f"最后10ep平均奖励: {np.mean(rewards[-10:]):.2f}")
print(f"DQN核心组件: 神经网络 + 经验回放 + 目标网络 + ε-greedy")

什么用(应用):这个DQN智能体是理解深度RL的起点。掌握了DQN,就可以扩展到更复杂的网络(CNN)、更复杂的环境(Atari)和更先进的算法(Rainbow、SAC)。

哪些坑(缺点):简化的梯度更新不够精确;one-hot编码不适用于大状态空间;超参数需要针对具体任务调整。

概念关系图谱

概念上位概念核心思想关键公式/方法作用
DQN基于价值神经网络近似Q函数Q(s,a;θ)≈Q*(s,a)处理高维状态
经验回放数据利用存储并随机采样历史经验从经验池D中随机采样打破相关性
目标网络稳定训练用旧参数计算TD目标θ^-定期从θ复制固定学习目标
ε-greedy探索策略随机探索+贪心利用以概率ε随机选动作平衡探索利用
TD误差学习信号当前估计与TD目标的差距r+γmaxQ(s',a';θ^-)-Q(s,a;θ)驱动学习
卷积神经网络网络结构自动提取图像特征CNN+全连接层处理像素输入
帧堆叠状态表示堆叠4帧作为输入84×84×4引入运动信息

重点答疑

Q1: DQN为什么需要经验回放?直接用连续数据训练不行吗?

连续数据训练有三个问题:(1) 数据高度相关——相邻帧几乎相同,梯度更新方向高度一致,容易震荡;(2) 数据分布非平稳——策略在不断改进,数据分布也随之变化;(3) 样本效率低——每条经验只用一次就被丢弃。经验回放通过随机采样解决了这三个问题,使训练更接近监督学习中的i.i.d.假设。

解答:经验回放就是DQN的"记忆库"——不只看最近发生的事,而是随机复习所有经历。这样学习更稳定、更高效。

Q2: 目标网络为什么不直接复制当前网络,而要定期复制?

如果每步都复制(目标网络=当前网络),就回到了标准Q-learning的"移动靶子"问题——TD目标随参数更新而立即改变,导致不稳定。如果永远不复制,目标网络就停留在初始化状态,TD目标完全错误。定期复制是一个折中:在一段时间内保持目标稳定,然后更新到最新的参数,再保持稳定。

解答:目标网络更新频率需要在"稳定性"和"时效性"之间权衡。太频繁→不稳定,太稀疏→目标过时。

Q3: DQN的损失函数为什么使用MSE?

MSE(均方误差)是回归问题的标准损失函数。Q-learning本质上是一个回归问题——我们要让Q(s,a;θ)尽可能接近TD目标r+γmax Q(s',a';θ^-)。MSE对大的误差惩罚更重(平方),有助于快速修正大的估计偏差。此外,MSE是光滑可微的,适合梯度下降。

解答:DQN的目标是让Q估计接近TD目标,这是一个回归问题,MSE是自然的选择。Huber损失(对异常值更鲁棒)也是常用选项。

Q4: 为什么DQN在Atari上需要堆叠4帧作为输入?

单帧画面不满足马尔可夫性质——你无法从一张静态图片判断物体的运动方向和速度。堆叠4帧(84×84×4)作为输入,引入了时间信息,使得状态近似满足马尔可夫性质。4帧是经验选择——太少则运动信息不足,太多则增加计算量且收益递减。

解答:4帧堆叠 = 给DQN"动态视力"。单帧看不清运动方向,4帧就能看出"球往哪飞,敌人往哪走"。

Q5: DQN中的ε-greedy探索和网络更新是同步的吗?

在DQN中,ε-greedy用于环境交互(选择动作),而网络更新是从经验池中随机采样进行的。两者是异步的——交互时用ε-greedy,更新时用经验回放。这意味着DQN可以从"很久以前"的经验中学习,这也是Off-Policy的优势。

解答:交互和训练是分离的——一边玩(ε-greedy),一边回顾(经验回放)。这就像"边工作边复习笔记",互不干扰。

Q6: DQN和Q-learning的本质区别是什么?

DQN就是Q-learning + 神经网络 + 经验回放 + 目标网络。本质上,DQN的更新规则仍然是Q-learning的TD更新,只是用神经网络替代了Q表,并加入了经验回放和目标网络来稳定训练。所以DQN = Q-learning的深度版本。

解答:DQN = Q-learning + 深度学习。核心思想不变(TD学习+max操作),但实现方式从查表变成了神经网络推理。

章节单词汇总

英文音标中文
Deep Q-Network/diːp kjuː ˈnetwɜːrk/深度Q网络
experience replay/ɪkˈspɪriəns ˈriːpleɪ/经验回放
target network/ˈtɑːrɡɪt ˈnetwɜːrk/目标网络
replay buffer/ˈriːpleɪ ˈbʌfər/回放缓冲区
mini-batch/ˈmɪni bætʃ/小批量
convolutional neural network/ˌkɑːnvəˈluːʃənl ˈnʊrəl ˈnetwɜːrk/卷积神经网络
frame stacking/freɪm ˈstækɪŋ/帧堆叠
off-policy/ɒf ˈpɑːləsi/异策略
gradient descent/ˈɡreɪdiənt dɪˈsent/梯度下降
mean squared error/miːn skwerd ˈerər/均方误差
stochastic gradient descent/stəˈkæstɪk ˈɡreɪdiənt dɪˈsent/随机梯度下降
hyperparameter/ˈhaɪpərpəˌræmɪtər/超参数
generalization/ˌdʒenrələˈzeɪʃn/泛化
i.i.d./aɪ aɪ diː/独立同分布
overestimation bias/ˌoʊvərˌestɪˈmeɪʃn ˈbaɪəs/过估计偏差

面试练习

Q1 [单选] DQN中经验回放的主要作用是什么?

  • A. 增加训练数据量
  • B. 打破连续数据之间的相关性,稳定训练
  • C. 替代目标网络
  • D. 减少内存使用
解答:经验回放的核心作用是打破数据相关性。随机采样使得训练数据更接近i.i.d.,这是监督学习梯度下降有效的前提。

Q2 [单选] DQN中目标网络的作用是什么?

  • A. 替代经验回放
  • B. 稳定TD目标的计算,避免"移动靶子"问题
  • C. 增加网络容量
  • D. 加速训练
解答:目标网络用旧参数θ^-计算TD目标,固定了学习目标,避免了参数更新后目标立即改变导致的"追逐自己尾巴"问题。

Q3 [多选] DQN相比Q-learning的核心创新包括哪些?

  • A. 使用神经网络近似Q函数
  • B. 经验回放
  • C. 使用max操作
  • D. 目标网络
解答:A、B、D都是DQN的创新。C错误,max操作是Q-learning本身就有的,不是DQN的创新。

Q4 [单选] DQN的损失函数是什么?

  • A. 交叉熵损失
  • B. 均方误差(MSE)——TD误差的平方
  • C. Hinge损失
  • D. 对数损失
解答:DQN的损失是TD误差的均方误差:L(θ) = E[(r+γmax Q(s',a';θ^-)-Q(s,a;θ))²]。这是回归问题的标准损失。

Q5 [多选] 以下哪些是DQN训练中可能遇到的问题?

  • A. Q值过估计
  • B. 训练不稳定
  • C. 只能处理连续动作空间
  • D. 需要大量训练数据
解答:A正确,max操作导致过估计。B正确,神经网络训练天然不稳定。C错误,DQN只适用于离散动作空间。D正确,原始DQN需要数百万帧的训练。

Q6 [单选] 在DQN中,经验回放池通常存储什么数据?

  • A. 只有奖励
  • B. (s, a, r, s')四元组
  • C. 只有状态
  • D. 整个episode的轨迹
解答:经验回放池存储的是单步转移(s, a, r, s'),每一步交互产生一条经验。有些变体也存储(done)标志,即(s, a, r, s', done)。

Q7 [单选] DQN的目标网络更新频率通常是多少?

  • A. 每步更新
  • B. 每数百到数万步更新一次
  • C. 从不更新
  • D. 每百万步更新一次
解答:原始DQN中每10000步更新一次目标网络(Atari游戏)。实践中,100到10000步之间都是常见的选择,取决于任务。

Q8 [多选] 以下哪些是经验回放的优势?

  • A. 打破数据相关性
  • B. 提高样本效率(每条经验可多次使用)
  • C. 完全消除过估计问题
  • D. 使训练数据更接近i.i.d.
解答:A、B、D都是经验回放的优势。C错误,经验回放不解决过估计问题(需要Double DQN)。

Q9 [单选] 为什么DQN在Atari游戏中使用4帧堆叠作为输入?

  • A. 为了提高图像分辨率
  • B. 为了引入运动信息,近似满足马尔可夫性质
  • C. 为了减少计算量
  • D. 为了增加网络深度
解答:单帧无法判断运动方向,4帧堆叠引入了时间信息,使得状态近似满足马尔可夫性质。这是DQN处理Atari游戏的标准做法。

Q10 [多选] 以下哪些是DQN系列算法的改进方向?

  • A. Double DQN(解决过估计)
  • B. Dueling DQN(改进网络结构)
  • C. Prioritized Replay(优先采样重要经验)
  • D. Rainbow(整合多种改进)
解答:所有选项都是DQN的改进方向。A解决过估计,B改进网络结构,C提高采样效率,D整合所有改进。