化學(xué)習(xí):經(jīng)驗(yàn)回放與目標(biāo)網(wǎng)絡(luò)拆解)
《強(qiáng)化學(xué)習(xí)從入門到精通》2026 版系列寫到第 49 篇這次我們把 DQN 重新完整拆一遍。標(biāo)題里專門寫了“再聊 Q Learning 方法”不是說(shuō) DQN 又多難而是要強(qiáng)調(diào)它的根基就是 Q Learning把原來(lái)查表的 Q 表格換成一個(gè)深度神經(jīng)網(wǎng)絡(luò)來(lái)做函數(shù)逼近。這個(gè)替換解決了高維狀態(tài)空間下表格存不下的問(wèn)題但同時(shí)也帶來(lái)兩個(gè)經(jīng)典的新麻煩——樣本相關(guān)性和訓(xùn)練目標(biāo)不穩(wěn)定。DQN 當(dāng)年能成功核心就是靠經(jīng)驗(yàn)回放Experience Replay和目標(biāo)網(wǎng)絡(luò)Target Network兩個(gè)機(jī)制把這兩個(gè)麻煩按住了。這篇文章不是單純念公式而是按“為什么需要 DQN - 關(guān)鍵機(jī)制拆解 - 最小可運(yùn)行代碼 - 訓(xùn)練效果觀察 - 參數(shù)調(diào)優(yōu)”的順序走一遍。如果你已經(jīng)學(xué)完 Q Learning正處在“表格法到深度強(qiáng)化學(xué)習(xí)”的過(guò)渡階段這一篇可以直接收藏。文章最后會(huì)給一套在 CartPole-v1 上能直接跑起來(lái)的最小實(shí)現(xiàn)以及訓(xùn)練過(guò)程中最容易踩的坑和排查思路。1. 核心知識(shí)點(diǎn)速覽能力項(xiàng)說(shuō)明算法定位基于價(jià)值Value-Based的深度強(qiáng)化學(xué)習(xí)算法深度化后的 Q Learning算法來(lái)源DeepMind 在 2015 年發(fā)表的 DQN 工作核心貢獻(xiàn)是經(jīng)驗(yàn)回放與目標(biāo)網(wǎng)絡(luò)適合狀態(tài)空間高維連續(xù)狀態(tài)輸入比如游戲畫面、傳感器數(shù)據(jù)、環(huán)境特征向量適合動(dòng)作空間離散動(dòng)作動(dòng)作數(shù)量不多時(shí)可以枚舉 Q 值前置知識(shí)Q Learning 表格更新、神經(jīng)網(wǎng)絡(luò)基礎(chǔ)、PyTorch 基礎(chǔ)訓(xùn)練環(huán)境CartPole-v1單 CPU 即可完成訓(xùn)練不需要 GPU本文代碼框架Python PyTorch gymnasium是否覆蓋 API本篇不涉及 HTTP API重點(diǎn)是訓(xùn)練算法本身是否支持批量任務(wù)不支持批量推理但支持批量調(diào)參實(shí)驗(yàn)和并行訓(xùn)練多個(gè)種子最容易踩的坑gym 與 gymnasium 的 API 差異、目標(biāo)網(wǎng)絡(luò)更新節(jié)奏、epsilon 衰減過(guò)快這篇的內(nèi)容重點(diǎn)不在“背公式”而在建立一條清晰的邏輯鏈Q(jìng) Learning 哪里不行 - DQN 怎么改 - 改完以后為什么能穩(wěn)定訓(xùn)練 - 代碼里哪個(gè)參數(shù)對(duì)應(yīng)哪個(gè)機(jī)制。2. 從 Q Learning 到 DQN為什么要換一張“Q 表”2.1 Q Learning 的表格假設(shè)經(jīng)典的 Q Learning 維護(hù)一張 Q 表格表格的行是狀態(tài) s列是動(dòng)作 a單元格存對(duì)應(yīng)的動(dòng)作價(jià)值 Q(s, a)。更新公式是Q(s, a) - Q(s, a) alpha * [r gamma * max_a Q(s, a) - Q(s, a)]這個(gè)式子統(tǒng)計(jì)了 Q Learning 的核心邏輯用下一步所有動(dòng)作里最大的那個(gè) Q 值加上即時(shí)獎(jiǎng)勵(lì)去修正當(dāng)前動(dòng)作的 Q 值估計(jì)。表格法的優(yōu)點(diǎn)是干凈、直觀、理論性質(zhì)清楚狀態(tài)空間和動(dòng)作空間都是有限離散時(shí)收斂性有保證。但它有一個(gè)天然前提狀態(tài)必須能被枚舉出來(lái)而且數(shù)量不能太大。一旦狀態(tài)是一個(gè)連續(xù)值比如小車的位置、速度、角度就沒(méi)法直接塞進(jìn)表格里如果狀態(tài)是游戲畫面每個(gè)像素都有 256 種取值任何一張表都存不下。對(duì) CartPole 這種 4 維連續(xù)狀態(tài)來(lái)說(shuō)強(qiáng)行用 Q Learning 也不是不行辦法是把每個(gè)維度手動(dòng)分桶離散化比如角度分 10 個(gè)區(qū)間、速度分 10 個(gè)區(qū)間組合出來(lái)的狀態(tài)就有上萬(wàn)種。這樣處理能做但有兩個(gè)問(wèn)題離散化的粒度很難拍太粗會(huì)丟失關(guān)鍵信息太細(xì)表格會(huì)迅速膨脹換一個(gè)環(huán)境又得重新設(shè)計(jì)分桶方案完全沒(méi)有通用性。2.2 用神經(jīng)網(wǎng)絡(luò)替代 Q 表格DQN 的思路非常直接不再維護(hù)表格而是訓(xùn)練一個(gè)神經(jīng)網(wǎng)絡(luò)輸入狀態(tài) s輸出每個(gè)動(dòng)作的 Q 值估計(jì)也就是 Q(s, a; theta)其中 theta 是網(wǎng)絡(luò)參數(shù)。這樣面對(duì)連續(xù)高維狀態(tài)不用再手動(dòng)分桶網(wǎng)絡(luò)自己提取特征。但“把 Q Learning 的表格換成神經(jīng)網(wǎng)絡(luò)”聽(tīng)起來(lái)簡(jiǎn)單直接在原來(lái)的更新公式上套用一個(gè)非線性網(wǎng)絡(luò)訓(xùn)練很容易發(fā)散。原因有兩個(gè)第一個(gè)是樣本相關(guān)性問(wèn)題。強(qiáng)化學(xué)習(xí)產(chǎn)生的樣本是時(shí)序相關(guān)的當(dāng)前狀態(tài)和下一個(gè)狀態(tài)只差一個(gè)動(dòng)作前后樣本非常相似。訓(xùn)練神經(jīng)網(wǎng)絡(luò)時(shí)假設(shè)輸入的樣本是獨(dú)立同分布的如果拿一串高度相關(guān)的狀態(tài)連續(xù)更新網(wǎng)絡(luò)參數(shù)的梯度方向會(huì)被這批樣本帶偏訓(xùn)練極不穩(wěn)定。第二個(gè)是目標(biāo)不穩(wěn)定問(wèn)題???Q Learning 的更新公式等號(hào)右邊用到了 max_a Q(s, a)在表格法里這是一個(gè)查表操作表本身更新是漸進(jìn)式的。但換成神經(jīng)網(wǎng)絡(luò)后如果直接用同一個(gè)網(wǎng)絡(luò)去計(jì)算目標(biāo)值那么每一步更新都在“追逐一個(gè)會(huì)移動(dòng)的目標(biāo)”。網(wǎng)絡(luò)參數(shù)一變目標(biāo)值也變于是損失函數(shù)的靶心一直在動(dòng)訓(xùn)練自然震蕩。DQN 文章里提出的兩個(gè)關(guān)鍵機(jī)制就是分別對(duì)應(yīng)這兩個(gè)問(wèn)題。后面兩節(jié)逐個(gè)拆。3. DQN 的兩個(gè)關(guān)鍵機(jī)制與損失函數(shù)3.1 經(jīng)驗(yàn)回放打斷樣本相關(guān)性經(jīng)驗(yàn)回放的做法是維護(hù)一個(gè)固定容量的緩沖區(qū) Replay Buffer。訓(xùn)練時(shí)每一步環(huán)境交互產(chǎn)生的s, a, r, s, done元組先放進(jìn)緩沖區(qū)而不是立刻用于更新網(wǎng)絡(luò)。更新網(wǎng)絡(luò)時(shí)從緩沖區(qū)里隨機(jī)均勻抽一小批樣本出來(lái)計(jì)算梯度。隨機(jī)抽樣的效果是一批樣本來(lái)自不同時(shí)間點(diǎn)的交互記錄它們之間的時(shí)序相關(guān)性被打散了更接近神經(jīng)網(wǎng)絡(luò)訓(xùn)練要求的獨(dú)立同分布假設(shè)。同時(shí)一條樣本可以被多次抽到樣本利用率比“用一次就扔”的方式高很多這在訓(xùn)練早期數(shù)據(jù)量少的時(shí)候尤其重要。實(shí)踐中要注意緩沖區(qū)容量和抽樣方式。容量太小緩沖區(qū)里都是最近幾局的數(shù)據(jù)相關(guān)性還是很大容量太大早期隨機(jī)探索產(chǎn)生的舊樣本占比太高又會(huì)拖慢新策略的學(xué)習(xí)。常見(jiàn)做法是設(shè)一個(gè)較大的容量比如 10000 到 100000并且只在緩沖區(qū)樣本數(shù)超過(guò) batch size 之后再開(kāi)始訓(xùn)練。3.2 目標(biāo)網(wǎng)絡(luò)固定一個(gè)穩(wěn)定的靶心目標(biāo)不穩(wěn)定的解決辦法是準(zhǔn)備兩個(gè)結(jié)構(gòu)一模一樣的網(wǎng)絡(luò)一個(gè)是在線網(wǎng)絡(luò) online_net負(fù)責(zé)選擇動(dòng)作和計(jì)算當(dāng)前 Q 值梯度只更新這個(gè)網(wǎng)絡(luò)另一個(gè)是目標(biāo)網(wǎng)絡(luò) target_net負(fù)責(zé)計(jì)算目標(biāo)值里的 max_a Q(s, a)。目標(biāo)網(wǎng)絡(luò)的參數(shù)是從在線網(wǎng)絡(luò)復(fù)制過(guò)來(lái)的但不是每一步都復(fù)制而是每隔固定步數(shù)同步一次。這樣在一個(gè)同步周期內(nèi)目標(biāo)網(wǎng)絡(luò)是固定的損失函數(shù)里的 target 穩(wěn)定不變?cè)诰€網(wǎng)絡(luò)就有機(jī)會(huì)朝著一個(gè)平穩(wěn)的目標(biāo)去擬合。等到同步的時(shí)候再把在線網(wǎng)絡(luò)的最新參數(shù)復(fù)制過(guò)去刷新一次靶心。兩個(gè)網(wǎng)絡(luò)的同步有兩種常見(jiàn)方式同步方式做法特點(diǎn)硬更新Hard Update每 N 步直接令 target_net 參數(shù)等于 online_net 參數(shù)簡(jiǎn)單直接N 通常取 100 到 1000實(shí)現(xiàn)成本低軟更新Soft Update / Polyak每步都做 tau 比例的融合更新參數(shù)更平滑tau 常取 0.005 到 0.01適合對(duì)穩(wěn)定性要求更高的場(chǎng)景對(duì)于入門實(shí)現(xiàn)硬更新最簡(jiǎn)單。先用硬更新跑通訓(xùn)練流程再改成軟更新看效果差異這個(gè)對(duì)比本身就能加深理解。3.3 DQN 的損失函數(shù)DQN 的損失函數(shù)就是把 Q Learning 的更新差值改成均方誤差L E[(r gamma * max_a Q(s, a; theta_target) - Q(s, a; theta_online))^2]注意右邊第一項(xiàng)里的 Q 用的是目標(biāo)網(wǎng)絡(luò)參數(shù) theta_target而且梯度不回傳到這一路左邊的 Q 才是在線網(wǎng)絡(luò)輸出梯度從這里反傳。用 PyTorch 實(shí)現(xiàn)時(shí)目標(biāo)值部分要加 detach()否則會(huì)把梯度傳回到目標(biāo)網(wǎng)絡(luò)上破壞固定目標(biāo)機(jī)制。完整的一次更新流程可以概括成五步在環(huán)境中執(zhí)行動(dòng)作收集樣本s, a, r, s, done。把樣本寫入 Replay Buffer。從 Buffer 隨機(jī)采樣一個(gè) batch。用在線網(wǎng)絡(luò)計(jì)算當(dāng)前動(dòng)作的 Q 值。用目標(biāo)網(wǎng)絡(luò)計(jì)算下一狀態(tài)的最大 Q 值拼出 target做一次梯度下降。這五步會(huì)在每個(gè)訓(xùn)練 step 里反復(fù)執(zhí)行直到訓(xùn)練結(jié)束。理解了這五步DQN 的代碼結(jié)構(gòu)就清楚了。4. 環(huán)境準(zhǔn)備與版本說(shuō)明本文代碼基于 Python 3.10、PyTorch 2.x、gymnasium。這里單獨(dú)說(shuō)一下環(huán)境庫(kù)的選擇gymnasium 是 OpenAI Gym 維護(hù)權(quán)移交后的社區(qū)維護(hù)版本也是目前大多數(shù)強(qiáng)化學(xué)習(xí)教程在用的新接口。老版本 gym 的 API 在新的 Python 版本上容易報(bào)兼容問(wèn)題所以 2026 版系列統(tǒng)一建議使用 gymnasium。安裝依賴pip install torch gymnasium numpy如果本機(jī)已經(jīng)裝了老版 gym建議把它卸載或隔離到獨(dú)立虛擬環(huán)境避免兩個(gè)庫(kù)的接口混在一起。剛安裝完可以先跑一個(gè)環(huán)境自檢腳本python -c import gymnasium as gym; env gym.make(CartPole-v1); print(env.observation_space); print(env.action_space)正常輸出會(huì)顯示觀察空間形狀為 Box(4,)動(dòng)作空間為 Discrete(2)。硬件方面CartPole-v1 是經(jīng)典小型控制環(huán)境只有 4 維狀態(tài)輸入和 2 個(gè)離散動(dòng)作單 CPU 訓(xùn)練完全足夠不需要 GPU。如果后續(xù)要跑圖像類環(huán)境比如 Atari 游戲才需要認(rèn)真考慮顯存和算力。運(yùn)行訓(xùn)練腳本時(shí)建議在項(xiàng)目根目錄建三個(gè)文件夾models 保存網(wǎng)絡(luò)權(quán)重logs 保存訓(xùn)練日志results 保存評(píng)估結(jié)果。這樣后面做多組參數(shù)實(shí)驗(yàn)時(shí)每一組實(shí)驗(yàn)的產(chǎn)物都能對(duì)應(yīng)到具體目錄不會(huì)一鍋亂放。5. DQN 最小可運(yùn)行實(shí)現(xiàn)下面是一份可以直接復(fù)制運(yùn)行的 DQN 實(shí)現(xiàn)按“網(wǎng)絡(luò)定義 - 經(jīng)驗(yàn)回放 - 訓(xùn)練主循環(huán)”三塊組織。這個(gè)實(shí)現(xiàn)不做任何花哨包裝目的是看清 DQN 的核心邏輯。5.1 Q 網(wǎng)絡(luò)定義輸入是狀態(tài)維度輸出是每個(gè)動(dòng)作的 Q 值。用兩層隱藏層加 ReLU 激活對(duì) CartPole 來(lái)說(shuō)容量足夠。import random from collections import deque import numpy as np import torch import torch.nn as nn import torch.optim as optim import gymnasium as gym class DQN(nn.Module): def __init__(self, state_dim, action_dim, hidden_dim128): super().__init__() self.net nn.Sequential( nn.Linear(state_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, action_dim) ) def forward(self, x): return self.net(x)5.2 經(jīng)驗(yàn)回放緩沖區(qū)緩沖區(qū)用 deque 實(shí)現(xiàn)容量固定自動(dòng)覆蓋最老的樣本。class ReplayBuffer: def __init__(self, capacity10000): self.buffer deque(maxlencapacity) def push(self, state, action, reward, next_state, done): self.buffer.append((state, action, reward, next_state, done)) def sample(self, batch_size): batch random.sample(self.buffer, batch_size) states, actions, rewards, next_states, dones zip(*batch) states torch.FloatTensor(np.array(states)) actions torch.LongTensor(np.array(actions)).unsqueeze(1) rewards torch.FloatTensor(np.array(rewards)).unsqueeze(1) next_states torch.FloatTensor(np.array(next_states)) dones torch.FloatTensor(np.array(dones)).unsqueeze(1) return states, actions, rewards, next_states, dones def __len__(self): return len(self.buffer)dones 用 0/1 表示是否終止計(jì)算 target 時(shí)直接乘上 (1 - done)終止?fàn)顟B(tài)的下一狀態(tài) Q 值不參與計(jì)算。5.3 訓(xùn)練主循環(huán)env gym.make(CartPole-v1) state_dim env.observation_space.shape[0] action_dim env.action_space.n online_net DQN(state_dim, action_dim) target_net DQN(state_dim, action_dim) target_net.load_state_dict(online_net.state_dict()) target_net.eval() optimizer optim.Adam(online_net.parameters(), lr1e-3) buffer ReplayBuffer(capacity10000) gamma 0.99 batch_size 64 target_update_freq 200 epsilon_start 1.0 epsilon_end 0.01 epsilon_decay 0.995 num_episodes 400 step_count 0 for episode in range(num_episodes): state, _ env.reset() total_reward 0 done False while not done: epsilon epsilon_end (epsilon_start - epsilon_end) * (epsilon_decay ** step_count) if random.random() epsilon: action env.action_space.sample() else: with torch.no_grad(): q_values online_net(torch.FloatTensor(state).unsqueeze(0)) action q_values.argmax().item() next_state, reward, terminated, truncated, _ env.step(action) done terminated or truncated buffer.push(state, action, reward, next_state, float(done)) state next_state total_reward reward step_count 1 if len(buffer) batch_size: states, actions, rewards, next_states, dones buffer.sample(batch_size) q_values online_net(states).gather(1, actions) with torch.no_grad(): next_q_values target_net(next_states).max(1, keepdimTrue).values targets rewards gamma * next_q_values * (1 - dones) loss nn.MSELoss()(q_values, targets) optimizer.zero_grad() loss.backward() optimizer.step() if step_count % target_update_freq 0: target_net.load_state_dict(online_net.state_dict()) if (episode 1) % 20 0: print(fEpisode {episode 1}, reward: {total_reward:.1f}, epsilon: {epsilon:.3f}) torch.save(online_net.state_dict(), models/dqn_cartpole.pt)訓(xùn)練結(jié)束后權(quán)重會(huì)保存到 models 目錄下。epoch 循環(huán)里的 epsilon 計(jì)算取了 step_count也可以換成 episode 維度但步數(shù)維度的衰減更平滑在長(zhǎng) episode 里不會(huì)衰減太快。6. 訓(xùn)練效果驗(yàn)證與觀察點(diǎn)6.1 正常訓(xùn)練應(yīng)該看到什么CartPole-v1 每一局最多持續(xù) 500 步步數(shù)越多代表策略越好。第一次運(yùn)行訓(xùn)練腳本時(shí)不要只盯著最后的數(shù)字看應(yīng)該重點(diǎn)觀察哪幾個(gè)階段第一階段是隨機(jī)探索期。大約前幾十個(gè) episodereward 會(huì)在幾十步上下波動(dòng)這是 epsilon 還很大、策略接近隨機(jī)的情況。這個(gè)階段損失函數(shù)也會(huì)忽高忽低不要因?yàn)?loss 不降就認(rèn)為代碼寫錯(cuò)。第二階段是快速學(xué)習(xí)期。reward 會(huì)從幾十步開(kāi)始快速爬升甚至出現(xiàn)連勝。這時(shí)候能明顯看到損失函數(shù)整體下降但噪聲仍然很大。第三階段是收斂平臺(tái)期。reward 會(huì)接近 500 的上限中間偶爾會(huì)出現(xiàn)一個(gè)很差的 episode把平均分?jǐn)?shù)拉下來(lái)。這種現(xiàn)象在 DQN 里非常正常因?yàn)?epsilon 沒(méi)有完全衰減到 0探索仍然存在目標(biāo)網(wǎng)絡(luò)每 200 步同步一次刷新后還會(huì)帶來(lái)一段短暫的不穩(wěn)定。判斷訓(xùn)練成功的標(biāo)準(zhǔn)是最近 100 個(gè) episode 的平均 reward 穩(wěn)定超過(guò) 450并且持續(xù)觀察到多個(gè)接近 500 的 episode。如果你看到某個(gè) episode 一次性達(dá)到 500先別急著下結(jié)論繼續(xù)訓(xùn)練幾十局確認(rèn)不是偶然。訓(xùn)練過(guò)程中還可以在環(huán)境可視化窗口打開(kāi)的情況下運(yùn)行觀察小車是否能在桿子接近失衡時(shí)快速反向修正角度。這個(gè)直觀的畫面比 scalar 曲線更容易判斷策略是否“真的學(xué)會(huì)了”。6.2 DQN 與 Q Learning 的對(duì)比驗(yàn)證這一節(jié)回到標(biāo)題里的“再聊 Q Learning”。如果你想親眼確認(rèn)“為什么 DQN 值得存在”可以做一個(gè)對(duì)比實(shí)驗(yàn)在同一份 CartPole-v1 環(huán)境里寫一個(gè)最樸素的表格 Q Learning 實(shí)現(xiàn)把 4 維連續(xù)狀態(tài)手動(dòng)離散化。最常見(jiàn)的做法是把位置、速度、角度、角速度各分 6 到 10 個(gè)格子然后查表更新。實(shí)驗(yàn)做下來(lái)你會(huì)發(fā)現(xiàn)幾個(gè)現(xiàn)象第一表格 Q Learning 也能學(xué)習(xí)但需要精心調(diào)離散化的邊界。分桶太粗策略會(huì)忽略細(xì)微狀態(tài)差異很難穩(wěn)定到 500 分分桶太細(xì)需要探索的狀態(tài)組合變成幾十萬(wàn)訓(xùn)練時(shí)間明顯拉長(zhǎng)。第二DQN 不需要任何狀態(tài)處理直接把原始 4 維浮點(diǎn)輸入交給網(wǎng)絡(luò)代碼結(jié)構(gòu)完全通用。換一個(gè)環(huán)境只改網(wǎng)絡(luò)輸入維度和動(dòng)作維度訓(xùn)練主循環(huán)不用動(dòng)。第三目標(biāo)網(wǎng)絡(luò)的作用可以和 Q Learning 對(duì)照理解。表格法里的 Q(s, a) 更新是單步小幅修正可以理解為天然有一個(gè)“惰性目標(biāo)”DQN 如果不做目標(biāo)網(wǎng)絡(luò)在線網(wǎng)絡(luò)自己追自己訓(xùn)練就容易震蕩甚至發(fā)散。這個(gè)對(duì)比實(shí)驗(yàn)不需要寫很多代碼但它能幫你在同一個(gè)環(huán)境里同時(shí)看清兩個(gè)算法的成本和收益比單純背公式更有用。7. 常見(jiàn)問(wèn)題與排查方法DQN 訓(xùn)練類代碼的排查方向和普通深度學(xué)習(xí)不太一樣要區(qū)分“網(wǎng)絡(luò)結(jié)構(gòu)問(wèn)題”和“RL 訓(xùn)練問(wèn)題”。下面表格列出 CartPole 訓(xùn)練中最高頻的幾類情況。問(wèn)題現(xiàn)象可能原因排查方式解決方案reward 一直停留在 20 步以下epsilon 衰減太慢或動(dòng)作選擇邏輯錯(cuò)誤打印每個(gè) episode 的 epsilon確認(rèn)是否在下降調(diào)大 epsilon_decay 的衰減速度檢查是否在 done 后用 argmax 選擇動(dòng)作訓(xùn)練初期 loss 突然爆炸學(xué)習(xí)率過(guò)大或 reward 未歸一化觀察 loss 是否超過(guò)初始值幾個(gè)數(shù)量級(jí)降低 lr 到 3e-4 或 1e-4檢查 reward 是否有異常大值reward 穩(wěn)步上升后突然回退到低位目標(biāo)網(wǎng)絡(luò)更新頻率太高目標(biāo)移動(dòng)過(guò)快記錄 reward 回退發(fā)生的位置是否與 target 更新對(duì)齊調(diào)大 target_update_freq或者改用軟更新訓(xùn)練很久卡在 200 分上不去網(wǎng)絡(luò)容量不足或 epsilon 衰減過(guò)早終止看策略是否在動(dòng)作邊緣反復(fù)猶豫增加隱藏層寬度到 256重新放開(kāi)末尾段 epsilon運(yùn)行報(bào) reset() 相關(guān)錯(cuò)誤老 gym 接口與新 gymnasium 沖突檢查環(huán)境庫(kù)版本與調(diào)用代碼統(tǒng)一使用 gymnasium并確認(rèn)代碼里 reset 接收兩個(gè)返回值模型保存后無(wú)法復(fù)現(xiàn)分?jǐn)?shù)加載模型后忘記設(shè) eval 模式在推理代碼里檢查是否調(diào)用了 eval()推理時(shí)調(diào)用 model.eval()并關(guān)閉梯度計(jì)算loss 下降但 reward 不漲Q 值估計(jì)和策略改進(jìn)存在滯后用隨機(jī)策略跑 100 局看 baseline確認(rèn)隨機(jī) baseline 分?jǐn)?shù)再判斷模型是否有效其中最容易忽略的是老 gym 的接口差異。新版 gymnasium 的 env.reset() 返回observation, info兩個(gè)值env.step() 返回observation, reward, terminated, truncated, info五個(gè)值。如果你從 2020 年左右的教程復(fù)制代碼很有可能會(huì)寫錯(cuò)這一步導(dǎo)致 terminated 沒(méi)有正確讀取最后一局的終止?fàn)顟B(tài)被當(dāng)成正常狀態(tài)反復(fù)訓(xùn)練策略會(huì)變得非常差。環(huán)境安裝方面如果 torch 安裝時(shí)顯卡驅(qū)動(dòng)異常建議先切到 CPU 跑通流程python -c import torch; print(torch.cuda.is_available())輸出 False 也能正常訓(xùn)練 CartPole因?yàn)檫@篇代碼里的網(wǎng)絡(luò)規(guī)模非常小GPU 不是必須條件。等后續(xù)換到圖像類環(huán)境再回頭處理 CUDA 環(huán)境。8. 參數(shù)調(diào)優(yōu)與工程化建議訓(xùn)練跑通以后下一步不是直接加復(fù)雜算法而是把 DQN 的幾個(gè)核心參數(shù)逐個(gè)做對(duì)照實(shí)驗(yàn)。這里給一套適合入門的調(diào)參順序。參數(shù)默認(rèn)值調(diào)參方向經(jīng)驗(yàn)說(shuō)明學(xué)習(xí)率 lr1e-31e-4 到 3e-4 更穩(wěn)學(xué)習(xí)率過(guò)大是 DQN 訓(xùn)練爆炸的常見(jiàn)原因gamma0.99連接近 1 表示更看重長(zhǎng)線收益任務(wù)周期長(zhǎng)時(shí)提高 gamma噪聲大時(shí)適當(dāng)降低epsilon_decay0.9950.99 到 0.999衰減過(guò)快會(huì)導(dǎo)致探索不足reward 停在低位batch_size6432 到 256batch 太大訓(xùn)練慢太小梯度噪聲明顯回放容量1000010000 到 100000容量小相關(guān)性大容量大舊樣本占比高隱藏層寬度128128 到 256CartPole 用 128 足夠復(fù)雜環(huán)境再加大target_update_freq200100 到 1000更新太頻繁目標(biāo)不穩(wěn)太慢學(xué)習(xí)滯后工程化上建議做三件事。第一每次實(shí)驗(yàn)固定一個(gè)隨機(jī)種子。RL 訓(xùn)練的隨機(jī)性遠(yuǎn)大于監(jiān)督學(xué)習(xí)同一個(gè)參數(shù)跑兩次可能得到明顯不同的曲線。固定種子后才能公平地比較不同參數(shù)的優(yōu)劣。第二按“一組參數(shù)一個(gè)目錄”的方式管理實(shí)驗(yàn)。比如參數(shù)目錄下分 models、logs、results 三個(gè)子目錄訓(xùn)練腳本自動(dòng)寫入 config.json 記錄當(dāng)前超參數(shù)。這樣復(fù)盤的時(shí)候不用猜“這次實(shí)驗(yàn)到底用的是哪個(gè) lr”。第三用腳本批量跑多組參數(shù)實(shí)驗(yàn)。常見(jiàn)的做法是寫一個(gè)參數(shù)掃描器循環(huán)生成多個(gè)配置文件逐個(gè)啟動(dòng)訓(xùn)練進(jìn)程。CartPole 訓(xùn)練一次只需要十幾分鐘完全可以在本機(jī)按順序跑完。注意每個(gè)實(shí)驗(yàn)要單獨(dú)指定日志文件避免多個(gè)進(jìn)程同時(shí)寫同一個(gè) log。接口 API 方面本片算法本身不涉及 HTTP 接口。如果你后面想把訓(xùn)練好的 DQN 策略部署成服務(wù)最簡(jiǎn)單的做法是保留訓(xùn)練腳本里的模型加載邏輯再寫一個(gè)很薄的推理接口輸入狀態(tài)向量輸出動(dòng)作編號(hào)。這個(gè)部署方式只適合教學(xué)演示在真實(shí)控制場(chǎng)景中還需要考慮動(dòng)作平滑限制、安全約束、環(huán)境異?;謴?fù)等額外機(jī)制不能直接把仿真環(huán)境的策略原樣接進(jìn)物理設(shè)備。9. 總結(jié)與下一步這篇你應(yīng)該重點(diǎn)掌握的不是 DQN 網(wǎng)絡(luò)的某層結(jié)構(gòu)而是兩條邏輯主線一是 Q Learning 的表格更新如何一步步演變成 DQN 的均方誤差損失二是經(jīng)驗(yàn)回放和目標(biāo)網(wǎng)絡(luò)分別解決了時(shí)序相關(guān)性和訓(xùn)練目標(biāo)不穩(wěn)定性。這兩條線串起來(lái)DQN 就不再是一堆零散代碼而是一個(gè)有明確設(shè)計(jì)動(dòng)機(jī)的算法框架。建議你先做的最小驗(yàn)證是把第 5 節(jié)的代碼原樣跑一遍確認(rèn)能看到 reward 從隨機(jī)水平爬升到接近 500并把訓(xùn)練過(guò)程打印的 episode 日志畫成曲線。這一步跑通了再依次截圖記錄一組 lr、epsilon_decay 的對(duì)照實(shí)驗(yàn)就不難看出每個(gè)參數(shù)的影響。最容易踩的坑按優(yōu)先級(jí)排列g(shù)ymnasium 的 reset 返回值寫錯(cuò)、epsilon 衰減過(guò)快導(dǎo)致探索不足、target 網(wǎng)絡(luò)更新太頻繁導(dǎo)致訓(xùn)練回退。這三類問(wèn)題在入門階段出現(xiàn)頻率最高排查時(shí)先看這三個(gè)位置。下一步可以在 DQN 主干上做四個(gè)方向的擴(kuò)展Double DQN把目標(biāo)值里的 max 拆成“在線網(wǎng)絡(luò)選動(dòng)作、目標(biāo)網(wǎng)絡(luò)算值”緩解 Q 值過(guò)估計(jì)。Dueling DQN把 Q 值拆成狀態(tài)價(jià)值和動(dòng)作優(yōu)勢(shì)兩部分在動(dòng)作空間大的任務(wù)里收斂更穩(wěn)。優(yōu)先經(jīng)驗(yàn)回放PER讓 TD 誤差大的樣本有更高概率被抽中提高樣本利用率。Noisy DQN用帶噪聲的網(wǎng)絡(luò)層替代 epsilon-greedy 探索讓探索本身變成可學(xué)習(xí)的一部分。這四個(gè)方向每一個(gè)都可以在現(xiàn)有代碼框架上做一個(gè)多小時(shí)就能完成的改造也是后續(xù)擴(kuò)展到連續(xù)動(dòng)作控制類算法PPO、SAC、TD3之前值得打的底座。下一篇我們接著從 Double DQN 入手重點(diǎn)看 Q 值過(guò)估計(jì)的問(wèn)題是怎么被緩解的。