4.1 Gym 環境與 DQN:經驗回放、目標網路(2013-2015 Atari)#
前言:兩件事同時發生,才有了深度 RL#
2013 年 12 月,DeepMind 在 NIPS 發表《Playing Atari with Deep Reinforcement Learning》;2015 年 2 月,升級版登上 Nature《Human-level control through deep reinforcement learning》。這兩篇論文做的不只是「用神經網路玩遊戲」,而是同時完成兩件事:(1)用深度卷積網路取代 Q 表,一舉擊碎第 3.2 節的維度災難——網路從原始像素直接學價值,泛化到沒見過的局面;(2)DeepMind 與 OpenAI(2016)把「環境」標準化成 Gym,讓整個領域第一次擁有可複現的基準。本節將逐行拆解 DQN 的兩大發明——經驗回放(experience replay)與目標網路(target network)——並說明它們各自馴服「致命三要素」的哪一環。理解了 DQN,你就理解了後續所有深度 RL 演算法(PPO、SAC、AlphaZero)都必須回答的同樣問題:如何讓「自舉 + 神經網路 + off-policy 資料」這個組合不崩潰?
歷史背景:為什麼是 2013?三塊拼圖同時到位#
第一塊:卷積網路成熟。 2012 年 AlexNet 在 ImageNet 爆發,證明 CNN 能從原始像素學出層次化表示。此前 RL 界不是沒想過用神經網路近似 Q(Tesauro 的 TD-Gammon 1992 就用了,見第 6 章),但淺層網路的表示力有限。第二塊:Atari 作為基準。 2013 年前 RL 基準極度零散(cart-pole、迷宮、模擬機器人各說各話)。Bellemare 等人 2013 年的 Arcade Learning Environment(ALE)把 57 款 Atari 遊戲包裝成統一介面——「只看畫面、只給分數」,排除了人工特徵工程的作弊空間。OpenAI Gym(2016)隨後把 ALE 與幾十個環境統一成一個 pip 套件與 API(reset()/step()/observation_space),成為全書程式碼的基礎。第三塊:算力與訓練技巧。 GPU 讓「百萬步的採樣 + 反覆的小批次梯度更新」成為可能;經驗回放則把 RL 的「線上、順序、相關」資料流改造成監督學習熟悉的「離線、洗牌、獨立」小批次。
值得強調的因果鏈:1992 年 TD-Gammon 已證明「TD + 網路」可行(第 6 章),但它是 on-policy(自我對弈即行為),避開了 off-policy 的不穩定;DQN 是 off-policy(回放舊經驗),踩進致命三要素的核心,所以必須發明新的穩定化手段。DQN 的每一個補丁都是對理論失效的工程回應——這是深度 RL「理論讓位給工程」時代的開端。
DQN 的完整架構#
flowchart TD
subgraph 環境互動
E[Atari 模擬器] -->|畫面 210x160x3| P[預處理: 灰階+縮小到84x84]
P --> F[疊 4 幀: 84x84x4<br>解決 POMDP(球速/方向)]
F --> NET[Q 網路 CNN]
NET -->|Q(s,·) 對 18 個搖桿動作| POL[ε-greedy 選動作]
POL --> E
end
subgraph 學習迴圈
F -->|s,a,r,s'| REP[(經驗回放池<br>容量 100 萬)]
REP -->|隨機小批次| LOSS[TD 損失<br>r + γ max Q_target s' - Q s]²
NET2[目標網路 Q_target<br>每 C 步從 Q 網路複製] --> LOSS
LOSS -->|反向傳播| NET
end輸入表示:把 POMDP 折回 MDP#
單幀畫面無法判斷移動方向與速度(第 2.1 節的部分可觀測問題)。DQN 的解法樸素而有效:疊最近 4 幀 \(x_t = (f_{t-3}, f_{t-2}, f_{t-1}, f_t)\) ,讓「位移」成為可見資訊。加上灰階、縮小到 \(84 \times 84\) 、取最後 2 幀取 max 去除 Atari 的閃爍精靈——這些「環境前處理」後來成為整個領域的標準配方。
Q 網路#
輸入 \(84 \times 84 \times 4\) ,三層卷積 + 兩層全連接,輸出每個動作一個 Q 值(而非輸入 \((s,a)\) 對輸出一個值)——這樣一次前向就能對所有動作取 argmax,且天然支援第 4.2 節的 Dueling 改造。
數學:TD 損失與兩大補丁的針對性#
基本損失#
DQN 最小化的目標是把 Q-Learning 更新式改寫成監督學習形式:
其中 \(D\) 是回放池、 \(U(\cdot)\) 是均匀採樣、 \(\theta^-\) 是目標網路參數。注意這就是第 3.2 節 Q-Learning 更新式的平方損失版,只是(1)期望用小批次近似,(2)目標裡的 \(Q\) 換成了凍結的 \(\theta^-\) 。
補丁一:經驗回放——消除「資料相關性」#
問題:線上學習中連續樣本是強相關的(同一路軌跡、同一局勢的連續幀), SGD 假設的 iid 被打破;且舊經驗用完即丟,樣本效率低,還可能被近期資料的自相關震盪拖著跑(學壞循環)。解法(Lin 1992 的回放思想 + DQN 大規模化):把 \((s,a,r,s',done)\) 存進大小 \(10^6\) 的環形池,訓練時均匀隨機抽小批次。效果:
- 相鄰樣本去相關 → 梯度更接近 iid 假設;
- 每條寶貴經驗被重複使用 → 樣本效率提升;
- 打破「最近的經驗主導梯度」→ 抑制振盪與災難遺忘。
代價:off-policy 程度加深(池中資料來自舊策略)→ 恰好踩進致命三要素,需要補丁二。後續演化:Prioritized Replay(2015,按 TD 誤差大小優先採樣,成為 Rainbow 一員,第 4.2 節)。
補丁二:目標網路——凍結「老師」#
問題:若目標 \(r + \gamma \max Q(s',\cdot;\theta)\) 用同一套 \(\theta\) ,則每步梯度更新會同時移動「學生」與「老師」——像追自己影子,自舉的誤差會即時放大並自強化,訓練可能發散(Atari 實驗中常見 Q 值飆到天文數字)。解法:維護一份延遲同步的目標網路 \(\theta^-\) ,每 \(C = 10^4\) 步才從主網路複製一次。數學含義:把「移動目標」凍結成「半固定目標」,讓每次小批次的優化問題近似一個良定的回歸問題;而 \(C\) 步一次的更新則讓目標的移動速度遠慢於學生,抑制正回饋迴路。代價:目標過時(bias 進一步增大)→ 這正是第 4.2 節 Double DQN 要修的過估問題。
兩個補丁對致命三要素的分工#
| 致命三要素 | DQN 是否踩到 | 誰在馴服它 |
|---|---|---|
| 自舉(target 用自己的 Q) | 是 | 目標網路減緩;但本質仍在 |
| 函數近似(CNN 代替表格) | 是 | 表示學習的收益來源,無法捨棄 |
| off-policy(回放舊資料) | 是 | 經驗回放本身;ε-greedy 控制分布 |
理論上三者同在可發散;DQN 用「延遲 + 重放 + 保守步長」把實務推向可用。這種「理論保證缺失、工程補丁護航」的姿態,定義了 2015-2020 深度 RL 的美學,直到 PPO(第 5 章)選擇另一條路:乾脆避開 off-policy。
程式碼:用 Gymnasium 實作精簡版 DQN#
以下是一份完整可跑的精簡 DQN(CartPole,CPU 可訓),保留兩大補丁的核心結構:
# pip install gymnasium torch numpy
import gymnasium as gym, random
import numpy as np, torch, torch.nn as nn
ENV, GAMMA, EPS_END, EPS_DECAY = "CartPole-v1", 0.99, 0.05, 500
BATCH, CAPACITY, SYNC, LR = 64, 20_000, 500, 1e-3
device = "cpu"
torch.manual_seed(0); random.seed(0)
env = gym.make(ENV)
A = env.action_space.n
class QNet(nn.Module):
"""狀態是 4 維向量(車位置/速度/桿角度/角速度); 遊戲版把這層換成 CNN 即可"""
def __init__(self):
super().__init__()
self.net = nn.Sequential(nn.Linear(4,128), nn.ReLU(),
nn.Linear(128,128), nn.ReLU(),
nn.Linear(128,A))
def forward(self, x): return self.net(x)
q_net, tgt_net = QNet().to(device), QNet().to(device)
tgt_net.load_state_dict(q_net.state_dict())
opt = torch.optim.Adam(q_net.parameters(), lr=LR)
buf = [] # 經驗回放池
step_count = 0
def act(state, eps):
if random.random() < eps: return env.action_space.sample()
with torch.no_grad():
return int(q_net(torch.tensor(state, dtype=torch.float32)).argmax())
for ep in range(600):
obs, _ = env.reset(seed=ep); done = False
eps = max(EPS_END, 1.0 - ep / EPS_DECAY) # ε-greedy 線性衰減
while not done:
step_count += 1
a = act(obs, eps)
ns, r, term, trunc, _ = env.step(a)
buf.append((obs, a, r, ns, term)) # 注意存的是 term 而非 trunc
if len(buf) > CAPACITY: buf.pop(0)
obs, done = ns, term or trunc
if len(buf) < BATCH or step_count % 4: continue
batch = random.sample(buf, BATCH) # 補丁一: 均匀隨機抽樣(去相關)
s, a, r, s2, d = map(np.array, zip(*batch))
s = torch.tensor(s, dtype=torch.float32)
a = torch.tensor(a, dtype=torch.int64)
r = torch.tensor(r, dtype=torch.float32)
s2 = torch.tensor(s2, dtype=torch.float32)
d = torch.tensor(d.astype(np.float32))
with torch.no_grad():
# 補丁二: 目標用凍結的 tgt_net
target = r + GAMMA * (1 - d) * tgt_net(s2).max(1).values
q = q_net(s).gather(1, a.unsqueeze(1)).squeeze(1)
loss = nn.functional.smooth_l1_loss(q, target)
opt.zero_grad(); loss.backward()
nn.utils.clip_grad_norm_(q_net.parameters(), 10.0) # 梯度裁剪: 再一道保險
opt.step()
if step_count % SYNC == 0: # 補丁二: 每 SYNC 步同步目標網路
tgt_net.load_state_dict(q_net.state_dict())
if (ep + 1) % 100 == 0:
# 簡易評估: 貪婪策略跑 20 回合取平均步數
scores = []
for ev in range(20):
o, _ = env.reset(seed=9000+ev); dn = False; sc = 0
while not dn:
o, r, t, tr, _ = env.step(act(o, 0.0)); dn = t or tr; sc += r
scores.append(sc)
print(f"ep {ep+1:4d} eps={eps:.2f} 平均存活步數={np.mean(scores):.0f}")
觀察要點:(1)eps 從 1.0 衰減——早期全隨機探索,逐漸轉向利用;(2)buf.pop(0) 的環形池在小環境夠用,Atari 版需 collections.deque 或環形陣列;(3)target = r + γ(1-d)·maxQ 的 (1-d) 等價於第 3.2 節的 (not term)——終止狀態截斷自舉;(4)去掉 tgt_net(把目標改回 q_net(s2))再跑一次,多數情況訓練會震盪或崩壞——這是致命三要素最直觀的展示。
結果與意義#
DQN(2015 Nature 版)在 49 款 Atari 中,於多數遊戲達到或超越專業人類測試者水平(以同一套網路與超參數!),並在 Breakout 展示了湧現行為:網路自己發現「挖穿牆讓球從頂上反覆得分」的高分策略——沒有人教它,獎勵訊號自己長出了策略。更深遠的意義:「同一套演算法 + 同一套架構玩所有遊戲」 首次兌現了 Sutton 多年倡議的「通用智能靠獎勵訊號學習」的路線,也直接定調了後來 OpenAI/DeepMind 的研究範式——這條線一路延伸到 RLHF(把遊戲換成對話)與 o1(把遊戲換成數學題)。
本節重點#
- 2013-2015 年 DQN 的突破 = CNN 表達力 + ALE/Atari 標準基準 + GPU 算力三塊拼圖同時到位;OpenAI Gym(2016)隨後把環境 API 標準化,成為領域基礎設施。
- DQN 核心仍是第 3.2 節 Q-Learning 更新式,只是把表格換成 CNN,並寫成平方損失的小批次優化。
- 經驗回放去相關、重複利用樣本、抑制振盪——馴服「off-policy 資料」這一環;目標網路凍結 TD 目標、減緩自舉的正回饋——馴服「移動目標」這一環。
- 致命三要素(自舉 + 函數近似 + off-policy)在 DQN 中全部踩到;理論保證讓位給工程補丁,是深度 RL 時代的起點。
- 疊 4 幀解決 Atari 的部分可觀測問題——「用歷史折疊出 Markov 表示」的標準工程手法(呼應第 2.1 節)。
思考問題#
- 把程式中
target的tgt_net(s2)改回q_net(s2)重跑,觀察訓練曲線。請用「老師與學生同時移動」的比喻解釋你觀察到的不穩定。 - 經驗回放把舊資料留下來重用,但第 3.1 節思考題提過「非平穩環境要忘掉舊資料」。這兩者矛盾嗎?Atari 為何不矛盾,而對話式 RLHF(第 8 章)可能矛盾?
- DQN 輸出「每個動作一個 Q 值」而非「輸入 (s,a) 輸出一個值」。請分析這個設計在計算 \(\max_{a'} Q(s',a')\) 時的效率差異,以及它如何為第 4.2 節 Dueling 架構鋪路。
- Breakout 中網路自行發現「挖洞」策略。這在 RL 語言中叫什麼現象?它為什麼必然發生(提示:它是唯一被獎勵訊號強化的行為,無關乎人類意圖)——這個機制與第 8.2 節 reward hacking 有何同構之處?
- 若把疊 4 幀改成疊 1 幀,Breakout(需要知道球的方向)與 Pong 會分別發生什麼?請用 POMDP 的語言說明疊幀是在做什麼數學操作。