Q 网络的诞生

在 Q-Learning 中,我们用表格记录 $Q(s,a)$。但当状态空间爆炸(如围棋或高清游戏画面)时,表格根本存不下。 DQN 的核心思想是用一个深度神经网络 $Q_\theta(s,a)$ 来近似这个 Q 表。

输入状态 S
深度神经网络
输出 Q 值
$$ Q_\theta(s_t, a_t) \leftarrow Q_\theta(s_t, a_t) + \alpha [y_t - Q_\theta(s_t, a_t)] $$

我们的目标是最小化预测值 $Q_\theta(s_t, a_t)$ 和目标值 $y_t$ 之间的均方误差(TD Error):

$$ L(\theta) = \mathbb{E}[(y_t - Q_\theta(s_t, a_t))^2] $$

其中目标值 $y_t$ 的计算方式为: $$ y_t = \begin{cases} r_t & \text{for terminal state } s_{t+1} \\ r_t + \gamma \max_{a'} Q_\theta(s_{t+1}, a') & \text{otherwise} \end{cases} $$

关键技术一:经验回放 (Experience Replay)

直接用与环境交互产生的连续数据训练神经网络会导致两个问题: 1. 样本相关性强:连续的几帧画面非常相似,这违反了神经网络训练“独立同分布”的假设,容易导致过拟合。 2. 数据利用率低:样本用一次就扔,太浪费了。

解决方案: 建立一个“经验回放池” $D$,把每一步的 $(s_t, a_t, r_t, s_{t+1})$ 都存进去。训练时,从池子里随机抽取一批样本进行训练。

随机采样就像把一副按顺序排列的扑克牌洗乱,打破了时间上的连续性,让训练更稳定。

关键技术二:目标网络 (Target Network)

在计算目标值 $y_t$ 时,我们用到了网络本身:$r + \gamma \max Q_\theta(s', a')$。 这意味着我们在更新网络参数 $\theta$ 的同时,我们要追逐的目标也在变(因为目标也由 $\theta$ 决定)。这就像狗追自己的尾巴,极易导致震荡发散。

解决方案: 引入一个参数为 $\theta^-$ 的目标网络。 $$ y_t = r_t + \gamma \max_{a'} Q_{\theta^-}(s_{t+1}, a') $$ 这个目标网络的参数 $\theta^-$ 会定期(例如每 1000 步)从主网络 $\theta$ 复制过来(硬更新),或者每一步只更新一点点(软更新)。这样靶子在一段时间内是固定的,就好打多了。

DQN 训练全流程

1. 交互与存储

使用 $\epsilon$-greedy 策略选择动作,与环境交互,将 $(s,a,r,s')$ 存入 Replay Buffer。

2. 随机采样

从 Buffer 中随机抽取一个 Batch 的数据 $(s_i, a_i, r_i, s_{i+1})$。

3. 计算 Loss 并更新

计算 $Q_{eval}$ 和 $Q_{target}$,通过梯度下降更新主网络参数 $\theta$。

4. 更新目标网络

每隔 $C$ 步,将 $\theta$ 复制给 $\theta^-$(硬更新)。

常见问题与思考

相比于 Q-learning,DQN 做了哪些改进?
1. 引入深度神经网络:能处理高维、连续的状态空间(如图像),不再受限于表格。
2. 经验回放:打破样本相关性,提高数据利用率,稳定训练。
3. 目标网络:固定优化目标,避免“追尾巴”导致的震荡。
4. 奖励裁剪:将奖励限制在小范围内,提高稳定性。
为什么要使用 $\epsilon$-greedy 策略?
为了平衡探索 (Exploration)利用 (Exploitation)。 如果不探索($\epsilon=0$),智能体可能陷入局部最优,永远发现不了更好的策略。 如果不利用($\epsilon=1$),智能体就是瞎蒙,学不到东西。 通常我们会让 $\epsilon$ 随时间逐渐衰减。
为什么 DQN 要多加一个目标网络?
如果只用一个网络,更新参数 $\theta$ 会同时改变预测值 $Q(s,a)$ 和目标值 $r + \gamma \max Q(s',a')$。 这会导致目标不稳定,训练难以收敛。目标网络把目标值暂时固定住,让训练变成一个稳定的监督学习问题。
经验回放的作用是什么?
1. 缓解样本相关性:随机采样打破了时间序列的相关性,满足神经网络独立同分布的训练假设。
2. 提高稳定性:平均化了参数更新的方差。
3. 数据复用:珍贵的样本可以被多次利用,而不是用完即弃。

PyTorch 核心代码

dqn_agent.py
import torch
import torch.nn as nn
import torch.optim as optim

class DQN(nn.Module):
    def __init__(self, state_dim, action_dim):
        super(DQN, self).__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)

# 训练循环中的核心部分
def optimize_model():
    if len(memory) < BATCH_SIZE:
        return
    
    transitions = memory.sample(BATCH_SIZE)
    # ... 解包数据 ...

    # 计算 Q(s_t, a) - 模型计算出的
    state_action_values = policy_net(state_batch).gather(1, action_batch)

    # 计算 V(s_{t+1}) - 目标网络计算出的
    next_state_values = target_net(next_state_batch).max(1)[0].detach()
    expected_state_action_values = (next_state_values * GAMMA) + reward_batch

    # 计算 Huber Loss
    loss = F.smooth_l1_loss(state_action_values, expected_state_action_values.unsqueeze(1))

    # 优化模型
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
下一步:更稳定的策略优化 (PPO)