AI 学习课程 / LESSON 16

第十六节:深度强化学习与 DQN

上一节用一张 Q 表记住每个状态—动作的价值。这一节把“查表”换成“输入状态,神经网络输出四个 Q 值”,再用真实训练结果理解经验回放、目标网络与评估波动。

PyTorch · CPUDQN 更新经验回放目标网络3 个训练种子离线 HTML

01 / 从表格到函数

Q 表会随状态数增长

第十五节只有 6 个状态、4 个动作,一张 6×4 的表就够了。如果状态包含地图位置、钥匙有无、障碍开关等组合,每种组合都要一行。改变下面的数量,看看 Q 表需要多少个数。

神经网络用共享参数把状态映射为 Q 值,因此不需要为每种状态组合显式存一行。这不保证它在没见过的状态上一定预测正确;数据覆盖、状态表示和训练稳定性仍然重要。

本节仍用只有 6 个状态的旧网格,专门练机制;这个环境用 Q 表更简单。网络并没有因为在这个小例子中胜出就证明比表格方法更好。

02 / 输入与输出都要看清形状

一次前向计算给出四个动作的估计

把状态 S/A/B/D/C/G 编成长度 6 的 one-hot 向量。网络 6 → 32 → 32 → 4,末尾四个数按「右、下、上、左」排列。训练时的一个批次是 (32, 6) → (32, 4)。本例输入编码仍与状态数量有关,只是没有再为每个状态—动作对显式存表项;更丰富的环境可改用坐标、图像等输入。

状态 s
one-hot (6,)
→Linear + ReLU
(32,)
→Linear + ReLU
(32,)
→Q(s,·)
(4,)

输入状态

训练结束后的四个输出

Q_online(s, ·) = 神经网络(s) # 输出 4 个数 Q_online(s, a) = 取第 a 个输出 # 此步选择的动作 a_greedy = argmax_a Q_online(s, a) # 评估时选最大值

和第十五节一样,进入 C 或 G 后回合终止,不再做动作选择。终止状态的网络输出没有在页面中当作可行动作价值来解释。

03 / DQN 的核心数学

在线网络求当前 Q,目标网络造 TD 目标

一条经验是 (s, a, r, s′, terminal)。在线网络的 Q(s,a) 是要被调整的预测;另一个参数暂时冻结的目标网络给出下一状态的最大 Q。终止时未来项必须归零。

y = r + γ · (1 − terminal) · maxₐ′ Q_target(s′, a′) L = 平均 SmoothL1( Q_online(s,a) − y ) 在线网络:梯度回传,更新 θ 目标网络:不求梯度,每 100 次优化把在线参数复制一次

这里使用 SmoothL1(Huber)损失:小误差附近像平方误差,大误差附近增长更平缓。下方用固定的玩具数值演算,和上面的真实训练结果分开。

本课把最多 20 步视为采样截断,不当成真正终止。只有进入 C/G 时才把 TD 目标的未来项清零。评估回报也只累计最多 20 步,所以与无限时域价值并不完全同一量。
chosen_q = online(states).gather(1, actions).squeeze(1)  # (32,)
with torch.no_grad():
    next_best = target(next_states).max(dim=1).values
    td_target = rewards + gamma * next_best * (~terminals).float()
loss = torch.nn.functional.smooth_l1_loss(chosen_q, td_target)
optimizer.zero_grad()
loss.backward()
optimizer.step()

04 / 两个缓解训练不稳定的装置

把经历存起来;让目标晚一点动

经验回放 Replay buffer

智能体连续走的几步高度相关。先把经历放进容量 1000 的队列,再随机抽 32 条组成训练批次,既能反复利用经历,也打散了相邻样本的顺序。

环境一步→存经验→随机抽 32 条→更新网络

目标网络 Target network

如果用于计算 y 的网络每次梯度更新后也立刻变化,就像追一个不断移动的答案。本课的目标网络参数保持不变,每 100 次优化才复制一次在线网络参数。

Q_online
每批更新
→复制参数
每 100 次优化
→Q_target
批内冻结

看一次实际抽取的前 8 条经验。表中「原动作」是在环境执行前选的动作;随机替换时环境重抽执行动作,也可能抽回原动作。Q-learning / DQN 的更新仍记录原动作,因为它学习的是“选择这个动作后,在随机环境里预期怎样”。

下表只展示第一次优化批次中的 8/32 条。缓存里的经验会继续被后续随机批次复用;这张表不表示所有经验都只来自这些回合。

05 / 真正在 CPU 上训练并评估

用多次运行分清探索成绩与贪心成绩

环境沿用旧网格:S→C 立即得 +2,S↓A→B→D→G 在终点得 +10,γ=0.8。与上一节的确定性网格相比,每一步有 10% 概率把预定动作替换为随机方向,因此同一策略的回报也会变化。训练 600 回合,ε 从 1 降到 0.05;评估关闭 ε 探索,但环境随机扰动仍存在。

各训练种子的贪心评估均值

各检查点使用同一组 40 个环境随机种子;最终结果另外用 100 回合评估。曲线点之间只是连线,没有在中间回合做评估。

当前检查点

最后的对照

随机策略始终在四个方向均匀选择,不学习;用 300 回合估计平均折扣回报。三个 DQN 模型每个用 100 回合,评估环境随机种子相同,所以是配对比较。表里的「标准差」反映每回合回报波动,不是三个训练种子的统计置信区间。

这 3 次运行在第 600 回合都学到通往 G 的策略,但训练早期收敛速度不同。小网格、少量训练种子和共享评估种子只能说明这次演示的表现,不能推出 DQN 在更复杂环境里也可靠。训练回报含 ε 探索;评估回报不含 ε,但仍受环境扰动。

看一条实际评估轨迹

06 / 亲手改一处,解释一个现象

完成本节练习

配套程序在 lesson-16-dqn.py。在项目根目录用 uv 管理的 Python 运行 uv run python lessons/lesson-16-dqn.py,结果写入 lessons/lesson-16-output/results.json。不需要下载环境、数据或预训练权重。

  1. 找到 chosen_q、td_target、loss 三行,分别写出形状,并说明为什么终止时不能加未来 Q。
  2. 把 TARGET_SYNC 从 100 改为 20,重新运行。比较三个种子的检查点评估均值;只描述这个实验的变化,不把一次实验当成普遍结论。
  3. 恢复参数,再把全局的 SLIP 改为 0,预测回报的回合间标准差会怎样变化;实际运行验证。注意这同时改变了训练环境,若只想比较评估环境,需要给评估和训练分设扰动参数。
  4. 解释为什么本例用 Q 表更省事,换到大量状态时神经网络又可能有用。
验收:能画出“环境 → 回放缓存 → 在线网络更新 → 周期性复制到目标网络”的信息流;能手算一条终止和一条非终止转移的 TD 目标;能区分训练回报、贪心评估均值和回合间标准差。

07 / 快速自测

能否解释 DQN 的每个部件?

1. 批次形状 (32,6) 输入网络后,输出应为?
2. 下一状态已经进入 G,目标是?
3. 经验回放在这份代码中做什么?
4. 评估时把 ε 设为 0,环境随机扰动会自动消失吗?
下一节:策略梯度与大语言模型对齐。 DQN 先估计动作价值再选动作;下一节将直接优化行动策略,理解 REINFORCE、Actor-Critic 与 RLHF 的联系。