Deep Q-Network (DQN)
当表格存不下时:深度学习与强化学习的结合。
Q 网络的诞生
在 Q-Learning 中,我们用表格记录 $Q(s,a)$。但当状态空间爆炸(如围棋或高清游戏画面)时,表格根本存不下。 DQN 的核心思想是用一个深度神经网络 $Q_\theta(s,a)$ 来近似这个 Q 表。
我们的目标是最小化预测值 $Q_\theta(s_t, a_t)$ 和目标值 $y_t$ 之间的均方误差(TD Error):
其中目标值 $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^-$(硬更新)。
常见问题与思考
2. 经验回放:打破样本相关性,提高数据利用率,稳定训练。
3. 目标网络:固定优化目标,避免“追尾巴”导致的震荡。
4. 奖励裁剪:将奖励限制在小范围内,提高稳定性。
2. 提高稳定性:平均化了参数更新的方差。
3. 数据复用:珍贵的样本可以被多次利用,而不是用完即弃。
PyTorch 核心代码
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()