一
先把舞台搭好。2012 年前后,有一个叫 Arcade Learning Environment 的研究平台,把几十款 1970 至 80 年代的 Atari 2600 游戏(打砖块、太空侵略者、乒乓、吃豆人等)包装成统一的强化学习接口:每一帧,智能体看到的是一张 210×160 的游戏画面,能做的是几个手柄动作(上下左右、开火),环境返回的是游戏分数的变化。
这个设定的迷人之处,在于它的通用性。同一套算法、同一个网络结构,不针对任何一款游戏做特别设计,要能玩几十款规则、目标、画面完全不同的游戏。打砖块要把球弹上去,太空侵略者要躲子弹打外星人,赛车要沿赛道开——如果一个智能体只靠看像素和分数就能全部学会,那它学到的就不是某个游戏的技巧,而是某种更一般的“从感知到决策”的能力。
2013 年,Volodymyr Mnih 等人(当时在 DeepMind)发表了《用深度强化学习玩 Atari》,第一次做到了这件事1。论文标题里“直接从高维感官输入学习控制策略”这句话,是对整个领域的宣告:在此之前,强化学习要么用在状态可以手工设计成几个数字的小问题上,要么依赖人类先把原始输入加工成特征。DQN 把这两步合一了——卷积网络负责“看懂画面”,Q-learning 负责“决定怎么动”,端到端一起训练。
二
DQN 的网络结构,是第三章讲的卷积网络与本卷强化学习的直接嫁接。输入不是单帧,而是把最近 4 帧画面叠在一起(这样网络能感知运动方向和速度——单看一帧无法判断球在往哪飞),经过几层卷积提取空间特征,再接全连接层,最后一层输出每个动作的 Q 值1。
注意这个输出设计的巧妙:网络不是输入“状态+动作”输出一个 Q 值,而是输入状态、一次性输出所有动作的 Q 值。这样选动作只需前向传播一次(取输出最大的那个动作),不必对每个动作各跑一遍网络——在动作多、要实时决策的游戏里,这个效率差别很关键。
训练目标,就是上一章那条 Q-learning 更新,被改写成了一个回归问题。对采样到的一条经验 ,构造目标值
然后让网络的输出 去回归这个 ,损失就是均方误差:
对 求梯度、反向传播、梯度下降——和第二章训练任何监督网络的流程一模一样。强化学习在这里被巧妙地转化成了一连串监督回归:每一步用“即时奖励 + 下一状态的最优估值”现造一个标签,让网络去拟合。
但正是这个“现造标签”埋着两颗雷。
三
第一颗雷:样本相关性。强化学习的经验是一条连续的轨迹,相邻几帧画面几乎一样,连续的状态、动作、奖励高度相关。而神经网络的训练(随机梯度下降)有一个隐含假设——每个小批量的样本应该是独立同分布的。如果你按时间顺序、用连续相关的样本去训练,网络会被最近这一小段经历带偏,学了新的忘了旧的,剧烈震荡。
DQN 的解法叫经验回放(experience replay)1。智能体不立即用刚产生的经验去训练,而是把每条 存进一个很大的缓冲区(buffer)。训练时,从缓冲区里随机抽取一个小批量来更新网络。随机抽样打散了时间顺序——一个批量里可能混着十分钟前和一秒前的经验、不同游戏阶段的片段——大大降低了样本间的相关性,让训练回到接近独立同分布的假设。
经验回放还附带一个好处:样本复用。一条经验进了缓冲区,会被随机抽中很多次,反复参与训练。在和真实环境交互很昂贵(每一步都要实际玩一帧游戏)的强化学习里,这种数据效率的提升非常实在。这个想法本身不是 DQN 首创(经验回放早在 1990 年代就有人提出),但 DQN 把它和深度网络结合,证明了它是稳定训练的关键一环。
四
第二颗雷更微妙:目标在动。回到那条损失 ,目标 里也含着 。也就是说,你每更新一次网络参数 ,你正在追的那个目标 自己也跟着动了。这就像你想射中一个靶子,可每当你瞄准、扣下扳机的瞬间,靶子就朝你瞄的方向挪一下——很容易陷入正反馈式的震荡或发散。
2015 年发表在《自然》上的 DQN 升级版,给出了第二个关键技巧:目标网络(target network)2。具体做法是维护两份网络:一份是不断更新的“在线网络”,另一份是参数被周期性冻结的“目标网络”。计算 TD 目标时,用冻结的目标网络:
在线网络照常每步更新,但目标网络的参数 每隔固定步数(比如一万步)才从在线网络复制一次。这样,在两次同步之间,靶子是静止的——网络在追一个不动的目标,训练稳定下来;隔一段时间,把这个目标更新到当前更准的估计上,再静止一段。这个简单的“冻结—追赶—再冻结”节奏,是 DQN 从“能跑通几个游戏”到“稳定玩 49 个游戏达人类水平”的分水岭。
2015 年那篇《自然》论文的标题是《通过深度强化学习达到人类水平的控制》2。同一套网络、同一组超参数,不为任何游戏单独调整,在 49 款 Atari 游戏上测试——智能体在其中过半数的游戏里达到或超过专业人类玩家的水平,有些游戏(如打砖块)它甚至发现了人类没想到的策略:先在墙的一侧凿穿一条通道,把球送到砖块后面,让球在顶部来回弹射、自动清场。
五
把目标网络和经验回放这两个机制写进代码,比文字更清楚。配套的 code/14_dqn_min.py(纯 numpy,手写单隐层网络与反向传播)在上一章那个 4×4 网格世界上,搭了一个最小可运行的 DQN。状态被编码成长度 16 的 one-hot 向量喂给网络,网络输出 4 个动作的 Q 值。训练循环里两个机制清晰可见:
展开代码 · 14_dqn_min.py
"""
第 14 章配套代码:最小 DQN 思想 demo(经验回放 + 目标网络)。
纯 numpy 实现一个单隐层 Q 网络,在前面同一个 gridworld 上学 Q(s,a)。
重点不是性能,而是把 DQN 的两个关键工程机制写清楚:
(1) 经验回放 replay buffer:存 (s,a,r,s',done),训练时随机小批量采样,打破时间相关性;
(2) 目标网络 target net:用周期性冻结的参数算 TD 目标 r + gamma*max Q_target(s'),稳住训练。
Runnable with: numpy only. python3 14_dqn_min.py
"""
import numpy as np
ROWS, COLS = 4, 4
START, GOAL, TRAP = (0, 0), (3, 3), (1, 3)
WALLS = {(1, 1), (2, 1)}
ACTIONS = [(-1, 0), (1, 0), (0, -1), (0, 1)]
def step(s, a):
if s in (GOAL, TRAP):
return s, 0.0, True
dr, dc = ACTIONS[a]
ns = (s[0] + dr, s[1] + dc)
if ns[0] < 0 or ns[0] >= ROWS or ns[1] < 0 or ns[1] >= COLS or ns in WALLS:
ns = s
if ns == GOAL:
return ns, 1.0, True
if ns == TRAP:
return ns, -1.0, True
return ns, -0.04, False
def encode(s):
"""状态 one-hot 编码成长度 16 的向量(作为网络输入)。"""
v = np.zeros(ROWS * COLS)
v[s[0] * COLS + s[1]] = 1.0
return v
class QNet:
"""单隐层 MLP: 16 -> H -> 4,输出每个动作的 Q 值。纯 numpy + 手写反传。"""
def __init__(self, H=32, seed=0):
rng = np.random.default_rng(seed)
self.W1 = rng.normal(0, 0.1, (16, H))
self.b1 = np.zeros(H)
self.W2 = rng.normal(0, 0.1, (H, 4))
self.b2 = np.zeros(4)
def forward(self, X):
self.X = X
self.z1 = X @ self.W1 + self.b1
self.h = np.maximum(0, self.z1) # ReLU
return self.h @ self.W2 + self.b2 # Q 值(线性输出)
def params(self):
return [self.W1, self.b1, self.W2, self.b2]
def copy_from(self, other):
self.W1, self.b1 = other.W1.copy(), other.b1.copy()
self.W2, self.b2 = other.W2.copy(), other.b2.copy()
def train_step(self, X, a_idx, target, lr=0.01):
"""对 Q(s,a) 做一步均方误差回归到 target,只在所选动作 a 上回传。"""
Q = self.forward(X)
pred = Q[np.arange(len(X)), a_idx]
dQ = np.zeros_like(Q)
dQ[np.arange(len(X)), a_idx] = (pred - target) / len(X) # MSE 梯度
dW2 = self.h.T @ dQ
db2 = dQ.sum(0)
dh = dQ @ self.W2.T
dz1 = dh * (self.z1 > 0)
dW1 = self.X.T @ dz1
db1 = dz1.sum(0)
self.W2 -= lr * dW2; self.b2 -= lr * db2
self.W1 -= lr * dW1; self.b1 -= lr * db1
return float(np.mean((pred - target) ** 2))
def train(episodes=1500, gamma=0.95, eps=0.2, batch=32,
target_sync=50, seed=0):
rng = np.random.default_rng(seed)
online, target = QNet(seed=seed), QNet(seed=seed)
target.copy_from(online)
buffer = [] # 经验回放缓冲
states = [(r, c) for r in range(ROWS) for c in range(COLS)
if (r, c) not in WALLS]
for ep in range(episodes):
s = START
for _ in range(50):
if rng.random() < eps:
a = rng.integers(4)
else:
a = int(np.argmax(online.forward(encode(s)[None])[0]))
ns, r, done = step(s, a)
buffer.append((s, a, r, ns, done))
if len(buffer) > 5000:
buffer.pop(0)
s = ns
# 从回放缓冲随机采样一个 minibatch 训练
if len(buffer) >= batch:
idx = rng.choice(len(buffer), batch, replace=False)
bs = [buffer[i] for i in idx]
X = np.array([encode(t[0]) for t in bs])
A = np.array([t[1] for t in bs])
R = np.array([t[2] for t in bs])
NS = np.array([encode(t[3]) for t in bs])
D = np.array([t[4] for t in bs], dtype=float)
# TD 目标用【目标网络】算,稳住训练
Qn = target.forward(NS)
td_target = R + gamma * np.max(Qn, axis=1) * (1 - D)
online.train_step(X, A, td_target)
if done:
break
if ep % target_sync == 0: # 周期性同步目标网络
target.copy_from(online)
return online, states
def show(net, states):
print("=== 最小 DQN (经验回放 + 目标网络) on 4x4 gridworld ===")
arrow = {0: "↑", 1: "↓", 2: "←", 3: "→"}
print("\n学到的贪婪策略:")
for r in range(ROWS):
row = []
for c in range(COLS):
if (r, c) in WALLS:
row.append(" # ")
elif (r, c) == GOAL:
row.append(" G ")
elif (r, c) == TRAP:
row.append(" T ")
else:
a = int(np.argmax(net.forward(encode((r, c))[None])[0]))
row.append(f" {arrow[a]} ")
print(" " + "".join(row))
print("\n机制要点: replay 打破样本相关性; target net 让 TD 目标不随每步抖动。")
if __name__ == "__main__":
net, states = train()
show(net, states)
经验回放——每走一步,把 存进 buffer,然后从缓冲区随机抽 32 条做一个小批量训练,而不是用刚刚那一步:
buffer.append((s, a, r, ns, done))
idx = rng.choice(len(buffer), batch, replace=False) # 随机抽样去相关
...
td_target = R + gamma * np.max(target.forward(NS), axis=1) * (1 - D)
目标网络——TD 目标用一份独立的 target 网络算,它每隔 50 局才从在线网络同步一次参数:
if ep % target_sync == 0:
target.copy_from(online) # 周期性冻结/同步,稳住靶子
训练一千五百局后,网络学出的贪婪策略:
↓ ↓ ↓ ←
↓ # ↓ T
↓ # → ↓
→ → → G
从任何一格出发,沿箭头走都能绕开墙、避开陷阱 T、流向目标 G。这个小程序当然玩不了 Atari——真正的 DQN 网络深得多、缓冲区存上百万条经验、训练上千万帧。但它和那个登上《自然》封面的智能体共享完全相同的骨架:一个神经网络逼近 Q,经验回放喂给它去相关的批量,目标网络给它一个暂时不动的靶子。
六
DQN 之后,研究者很快发现这个基础版本有不少可改进处,于是衍生出一整条改进谱系,几年内把 Atari 上的表现一路推高。
高估偏差。Q-learning 的更新用了 ,而 一个含噪声的估计会系统性地偏高——总是挑出那些“恰好被高估”的动作。2015 年的 Double DQN 用一个简单的拆分缓解这个问题:用在线网络选最优动作,用目标网络评估这个动作的价值,把“选”和“评”解耦,显著减小了过度乐观3。
价值与优势分离。很多状态下,做哪个动作其实差别不大,真正重要的是“这个状态本身好不好”。2016 年的 Dueling DQN 把网络拆成两个头,分别估计状态价值 和每个动作相对的优势 ,再合成 ——让网络能高效地学“这个局面值不值”,而不必为每个动作各学一遍4。
不是所有经验一样重要。基础版经验回放是均匀随机抽样,但有些经验(TD 误差大的、出乎意料的)信息量更高。2015 年的优先经验回放(Prioritized Experience Replay)按 TD 误差大小给经验加权,让“意外”的经验被更频繁地回放,在 49 个游戏中的 41 个上超过均匀回放5。
学分布而非期望。2017 年的 C51 提出一个更激进的视角:不要只估计回报的期望 ,而要估计回报的整个概率分布——同样是平均赢 10 分,“稳定赢 10 分”和“一半时候赢 20、一半输 0”是不同的,分布信息能让学习更稳健6。这开启了分布式强化学习这一支。
2018 年,DeepMind 的 Rainbow 把上述这些改进——Double、Dueling、优先回放、C51、多步回报、噪声网络——全部组合进一个智能体,在 Atari 基准上集大成,验证了这些改进大体是互补而非冲突的7。Rainbow 这个名字本身就是个隐喻:深度强化学习的进步,很多时候不是单个惊艳的新想法,而是一组朴素改进的稳健叠加。
七
退一步看 DQN 在这部技术史里的位置。它证明的核心命题是:第三章的卷积网络(负责感知)和第十二章的 Q-learning(负责决策)可以端到端地焊接在一起,用反向传播一并训练。 在它之前,“看懂世界”和“在世界里行动”是两个分开的研究领域;DQN 让一个智能体直接从原始像素学到了行动策略,把感知与决策第一次真正打通。
但 DQN 这一支有它的局限。它学的是动作价值 ,天然适合离散、有限的动作空间(Atari 手柄就那么几个键)。可现实里有大量连续控制问题——机器人的关节要转多少度、油门踩多深、方向盘打多少角度——动作是连续的实数,无法对所有动作取 。对这类问题,与其费力地学一个 再从中挑动作,不如直接学一个策略:输入状态,输出动作(或动作的概率分布)。
这就把我们带回上一章末尾提过的另一条线索——策略梯度。它不绕道价值,而是直接对“策略”这个函数做梯度上升。下一章从 1992 年的 REINFORCE 讲起,经过策略梯度定理、actor-critic 架构、信赖域方法,一直讲到 PPO——那个先被用来打游戏、后来又被搬去对齐语言模型(第十一章的 RLHF)的算法。两条线,将在那里第一次交汇。
配套的 manim 动画 assets/manim/ch13_dqn.py(ReplayBuffer 与 TargetNetwork 两个 Scene)把两个稳定机制演出来:前者展示智能体产生的连续经验(时间上强相关)落入回放池被打散,训练时随机抽一个 minibatch、近似独立——打破相关性;后者展示在线网络不断更新,而一份被冻结的目标网络负责给出 Bellman 目标 ,在线网络去拟合这个不抖动的目标,每隔若干步才把参数复制过去——把发散的拟合变成稳定的回归。
本质
把神经网络直接塞进 Q-learning 之所以会发散,是因为它同时踩中了两个雷:一是强化学习采到的相邻样本高度相关,违背了梯度下降“样本近似独立”的隐含假设;二是它的回归目标里含着正在被训练的那个网络自己——你在追一个随自己移动的靶子。DQN 的两个机制恰好各拆一个雷:经验回放把经历存进一个池子、训练时随机抽样,把强相关的时间序列打散成近似独立的样本;目标网络冻结一份参数专门用来算目标,让靶子在一段时间内不动。它真正的贡献不是某个新的学习规则,而是让一个早已知道、却一直不稳定的组合(深度网络 + 时序差分)第一次能在一般问题上稳定收敛——并由此证明了一件更大的事:负责“看懂世界”的卷积网络和负责“在世界里行动”的 Q-learning,可以端到端地焊在一起,用同一套反向传播一并训练。感知与决策,第一次被真正打通。
参考文献
-
Mnih, V., Kavukcuoglu, K., Silver, D., Graves, A., Antonoglou, I., Wierstra, D., & Riedmiller, M. (2013). Playing Atari with Deep Reinforcement Learning. NIPS 2013 Deep Learning Workshop. 首个从像素端到端学控制策略的深度模型;CNN 拟合 Q 值 + 经验回放;7 个 Atari 游戏,6 个超此前方法。arXiv:https://arxiv.org/abs/1312.5602
-
Mnih, V., Kavukcuoglu, K., Silver, D., Rusu, A. A., Veness, J., Bellemare, M. G., Graves, A., Riedmiller, M., et al. (2015). Human-level control through deep reinforcement learning. Nature, 518, 529–533. 引入目标网络(周期性冻结 算 TD 目标)+ 奖励裁剪;同一套网络/超参在 49 个 Atari 游戏过半达人类水平。Nature:https://www.nature.com/articles/nature14236 ;PDF:https://web.stanford.edu/class/psych209/Readings/MnihEtAlHassibis15NatureControlDeepRL.pdf
-
van Hasselt, H., Guez, A., & Silver, D. (2015/2016). Deep Reinforcement Learning with Double Q-learning. AAAI 2016. 用在线网络选动作、目标网络估值,拆解 导致的高估偏差。arXiv:https://arxiv.org/abs/1509.06461
-
Wang, Z., Schaul, T., Hessel, M., van Hasselt, H., Lanctot, M., & de Freitas, N. (2016). Dueling Network Architectures for Deep Reinforcement Learning. ICML 2016. 双头分流,提升状态价值估计。arXiv:https://arxiv.org/abs/1511.06581 ;PMLR:https://proceedings.mlr.press/v48/wangf16.pdf
-
Schaul, T., Quan, J., Antonoglou, I., & Silver, D. (2015). Prioritized Experience Replay. ICLR 2016. 按 TD 误差大小加权采样经验,41/49 游戏胜均匀回放。arXiv:https://arxiv.org/abs/1511.05952
-
Bellemare, M. G., Dabney, W., & Munos, R. (2017). A Distributional Perspective on Reinforcement Learning. ICML 2017. 学回报的分布而非期望,分布式 Bellman 算子,51 个原子(C51)。arXiv:https://arxiv.org/abs/1707.06887
-
Hessel, M., Modayil, J., van Hasselt, H., Schaul, T., Ostrovski, G., Dabney, W., Horgan, D., Piot, B., Azar, M., & Silver, D. (2018). Rainbow: Combining Improvements in Deep Reinforcement Learning. AAAI 2018. 合并 Double/Dueling/优先回放/C51/多步/噪声网络,Atari 集大成 SOTA。arXiv:https://arxiv.org/abs/1710.02298