表格型 Q-learning 可以替每個狀態、每個行動各放一格數字。但如果狀態是一張遊戲畫面,幾乎每一張畫面都不同,逐格儲存就很難靠過去經驗處理下一張畫面。
DQN 的第一步,是用神經網路近似 Q:輸入狀態表示,輸出各個行動的價值。這讓不同輸入可以共享參數,也帶來新的問題:更新一個地方,其他地方的預測可能跟著改;下一步 target 又來自 Q,於是連標籤都在移動。
本篇接著上一個可手算的 MDP,拆解網路輸出、selected-Q loss、experience replay 與 target network。完整程式會真的在同一個小環境訓練,但也會回到原始 Atari CNN,說明這個小例子保留了什麼機制,又沒有重現什麼。
從六格 Q 表,到一次輸出兩個行動價值
上一個環境有三個非終止狀態、兩個行動,表格共六格。神經網路版本改用 one-hot 表示狀態:
state 0 → [1,0,0]
state 1 → [0,1,0]
state 2 → [0,0,1]
one-hot 3 → Linear(3,8) → ReLU → Linear(8,2)
輸出:[Q(s,L), Q(s,R)]
最後一層沒有 softmax。Q 是折扣後的報酬估計,可以為負,也不必加總為 1;它不是每個行動的機率。需要實際選行動時,behavior policy 才從這兩個值決定 greedy 或探索。
這個 MLP 的 online 參數數量為:
第一層:3×8 weights + 8 biases = 32
第二層:8×2 weights + 2 biases = 18
合計:50 個可訓練 scalar parameters
和表格的六個獨立儲存位置相比,網路的隱藏層被多個輸入、行動共用。這種共享讓神經網路有機會利用相似特徵,也表示一次更新可能改變不只一格 Q。本例用 one-hot 沒有引入視覺特徵;50 個參數反而比六格表多,目的是把共享與梯度機制拆開,不是證明小問題需要深度模型。
真正的 Atari DQN,如何從畫面走到 Q 值?
「把 Q-learning 接上神經網路」並不只剩一個抽象方塊。原始工作把畫面處理成固定大小的近期影格堆疊,用 CNN 提取表示,再一次輸出所有可用行動的 Q 值。這樣不需為每個候選行動各跑一次完整網路。
2013 年的 Playing Atari with Deep Reinforcement Learning v1,與 2015 年 Nature 的 Human-level control through deep reinforcement learning,有幾個不能混成同一版的細節:
- 灰階、縮至 110×84,再裁成 84×84
- 先對當前與前一影格取逐色值最大值以減少閃爍,再取亮度 Y、縮至 84×84
- 最近 4 張處理後影格,84×84×4
- 最近 4 張處理後影格,84×84×4
- 16 個 8×8、stride 4;32 個 4×4、stride 2
- 32 個 8×8、stride 4;64 個 4×4、stride 2;64 個 3×3、stride 1
- 全連接 256 units
- 全連接 512 units
- 隱藏層 ReLU;每個有效行動一個線性 Q 輸出
- 隱藏層 ReLU;每個有效行動一個線性 Q 輸出
- 已有 experience replay;Algorithm 1 未列獨立週期 target clone
- 明列獨立 target network,每 C 次更新複製 online 參數
堆疊近期影格可以提供單張畫面缺少的變化資訊,但不能據此宣稱任何部分可觀察問題都已變成完全 Markov。原文從完整歷史的描述,走到實作上固定長度的影格表示,本身就是一項表示選擇。
兩篇工作都不是每次做梯度更新就重新得到真實的最優 Q。標籤仍含估計值,也仍需要探索和環境經驗。本文後面的 one-hot MLP 沒有影像、CNN、frame stacking、Atari reward 處理或原始訓練規模;只是用能驗算的狀態表示,把 replay 和 target 的作用做出來。
Experience replay:重用觀察過的事,不補造沒做過的行動
假如每次拿最新的一筆 transition 馬上更新,連續樣本可能高度相關;策略改變後,接下來收集的資料也跟著改。Replay buffer 把過去經驗存起來,更新時再從裡面抽一個 minibatch。
本例每筆紀錄包含狀態、已執行動作、reward、下一狀態,以及是否真正終止。只能把環境真的回傳過的資料放進去。Agent 在 0 選了 L,不能順便把「如果選 R 會如何」的答案填進 buffer,即使我們作為作者知道這個小模型的完整轉移表。
本篇設定容量 512,滿了就移除最舊紀錄。每批從當時 buffer 均勻、不放回抽 16 筆;不同批次之間可以重複使用同一筆。第一批前先收集 16 個 transition,前 15 筆沒有 optimizer update。
用來選行動的 RNG 與 replay 抽樣 RNG 分開:前者 seed 7,後者 seed 7007。多抽幾次 batch 不會偷偷消耗下一次 behavior 的隨機數;同樣也不能拿 oracle 的答案挑 replay 資料。
Replay 讓一筆觀察可以支持多次更新,並打散直接沿時間順序取樣的關係。但資料仍由先前策略蒐集,容量有限,也可能缺少重要的狀態—行動。均勻抽樣不等於保證整個訓練分布 iid,更不代表已校正曝光或覆蓋問題。
一個 minibatch,究竟對哪個 Q 做 loss?
網路對一個 batch 輸出全部行動價值,但一筆經驗只提供其中一個已執行動作的更新依據。程式使用 gather,從每列挑出那個 action 的預測:
狀態 inputs [16,3]
online(inputs) [16,2]
已選 actions [16],int64
挑出的 chosen_Q [16]
rewards、terminal、labels [16]
例如某列 action=R,就取這列的第二個 Q。不是對兩個 Q 都指定相同 reward,也不是把未選 action 當成零分標籤。
本例的 target 來自另一份 target network:
next_max = max_a Q_target(next_state,a)
label = reward + gamma × (1 − terminal) × next_max
loss = mean((chosen_Q − label)²)
這個 loss 是平均平方誤差,沒有另外乘 1/2。求梯度時,label 停止求導,target parameters 也不放進 optimizer;梯度只經由 online 的 chosen_Q 回傳。
這裡的 δ 是預測與 target 的差。單筆誤差為 .5 時,圖中的 Huber 值為 .125;誤差為 2 時則為 1.5。本文 MSE 對應的單筆值分別是 .25 和 4。這只是定義的算術對照,沒有另外訓練 Huber 模型。選擇 reduction、誤差尺度或 loss 都會影響梯度,不能只因都叫 DQN,就把兩個 loss 數值直接比較。
本次第一個 minibatch 的真實非終止紀錄 0R→1,reward=−.1,當時 target network 的 next max 約 .34335555,label 因而為:
−0.1 + 0.9×0.3433555501 ≈ 0.2090199951
這不是我們先手填好的 .62。它是早期網路的估計,因此可能離最後的正確值很遠。DQN 不需要拿到最優 Q 標籤才開始學習,但也不能把早期自產 target 當作真值。
「只對 chosen_Q 做 loss」還有一個細節:其他輸出在這筆資料上沒有直接被指定 label,卻可能因共享隱藏層而間接改變。若同一批還有別的 action,它們也會透過各自的 chosen_Q 參與更新。這正是神經網路和獨立表格的重要差別。
Target network:暫時固定的是參數,不是整個世界
初始化時,把 online network 完整複製一份給 target。之後 online 透過 optimizer 改參數,target 則維持不動;每 50 次 optimizer update,才把 online 當時的參數硬複製過去。
因此,同一個 transition 在這 50 次更新期間,使用的 target network 參數相同;但每批抽到的 transition、reward 和下一狀態可以不同。不要把它說成整個訓練期間的所有 label 都固定,更不能說 target 已經知道正確答案。
硬複製也不是另一個 gradient update。這裡沒有對 target 求 loss,也沒有讓 target 一步一步追 online;那種按比例混合新舊參數的 soft update 是另一種設定,本例沒有使用。
三個時鐘在本次實跑裡是:
- 選行動並收到新 transition
- 1858
- buffer≥16 後,每收一筆更新一次
- 1843
- 每 50 個 optimizer updates 後硬複製,不含初始化 clone
- 36
第一個 optimizer update 發生在第 16 次觀察,所以第一個 target copy 在 update 50、environment decision 65。最後一次 copy 是 update 1800、decision 1815;訓練最後還有 43 次 online updates,因此最終 online 和 target 不必是同一份參數。
程式保存全部 36 次 copy 的兩網路 digest,並檢查每次 optimizer step 前後 target 是否未變、target gradients 是否始終為 None。另有一個受控 probe:在獨立複本上擾動 online、檢查 target 不動,再 copy 後確認相同。這個 probe 沒有修改已訓練網路,也不是額外訓練過程。
真正終止、零向量與步數上限:別讓 mask 漏掉未來
沿用前篇的小 MDP:只有進 goal 3 或 trap 4 才是真正 terminal。這時 label 只用 reward,不加 continuation。
本例只為非終止狀態 0、1、2 建 one-hot,terminal ID 3、4 會得到零向量。網路對零向量仍可能輸出非零 Q,那只是有 bias 的函數輸出,沒有 terminal state value 的語意;label 裡乘上 terminal mask,才讓 continuation 確實為零。
第一個 minibatch 裡,0L 進 trap 的記錄,target 對零輸入的最大輸出約 .19521120,但 continuation 乘數是 0,因此 label 仍為 −1。不能只因編碼是零向量,就假設網路也一定輸出 0。
Collector 的最多 20 步則是另一件事:若只是收集器停止,next state 仍非終止,就要保留 bootstrap。這次 600 回合全在上限內真終止,truncated=0;程式沒有把停止收集產生的 reset 學成一個新的環境轉移,也沒有宣稱原 run 實際走過截斷分支。
若任務本身真的只剩有限時間,應把剩餘時間與終止定義放回 MDP 設計。本例繼承的是外部 collector limit,而非每個 time limit 都應繼續估無限期 return。
實際學到什麼?先看 Q、策略,再分清 loss 的比較條件
這次只做一個固定 seed 的訓練,沒有看到 oracle error 後再調參。設定為 600 回合、γ=.9、ε=.2、Adam lr=.01、batch16、capacity512、target 每50 updates硬複製,CPU float64、單 thread。
實際到 goal 507 回合、trap 93 回合、截斷 0;六個 pair 的訪次為:
| State | L 訪次 | R 訪次 | 最後 online Q(L) | 最後 online Q(R) |
|---|---|---|---|---|
| 0 | 93 | 570 | −1.000 | .620 |
| 1 | 63 | 566 | .458 | .800 |
| 2 | 59 | 507 | .620 | 1.000 |
4 個 online 參數 tensor 都改變。訓練完成後,才用完整已知模型作 Bellman oracle 對照:最大 Q 誤差與最大 Bellman residual 約 1.11×10⁻¹⁶。Oracle 沒有放入 replay,也沒有用來提供訓練 label。
凍結 online、移除探索的 greedy evaluation 走 0R→1R→2R→goal,未折扣 return=.8、折扣 return=.62。這些結果支持本次小環境學習與實作;它沒有未見過的新狀態評估,也不是 Atari 泛化或神經 Q-learning 的一般穩定性證明。
Loss 還需要特別分開看:
| 記錄 | 值 | 比較限制 |
|---|---|---|
| 第一個 update 的 loss | 1.2495918025 | 使用首批資料與當時 target |
| 最後 update 的 loss | 約 7.70×10⁻³⁴ | 批次和 target 都已改變 |
| 前 50 updates 平均 loss | .2487867831 | 不是固定驗證集 |
| 最後 50 updates 平均 loss | 約 6.82×10⁻³⁴ | 不是同一批 label 的配對比較 |
| 最後網路對首批「舊 label」的 MSE | .03324919385 | 固定首批與舊 label 的額外 diagnostic |
最後一列為什麼沒有接近零?因為第一批的非終止 label 本來就是早期網路估計。隨著 target 改善,online 不需要永遠重現當時的 .2090 等舊值。這個 diagnostic 用來說明 target 在動,不是拿舊 label 組成新的驗證集,也不以 loss 降得很低就代替策略評估。
完整可重跑程式:把觀察、更新與複製紀錄對起來
實際環境為 Python 3.10.12、torch 2.3.1+cu118,device 明確設為 CPU,float64、單 thread;CUDA 標記是已裝 wheel 的版本字串,不表示本例使用或驗證過 GPU。依賴檔為:
torch==2.3.1
把完整程式存成 tiny_dqn.py,在已有相容 PyTorch 的環境與同目錄執行:
python3 tiny_dqn.py > stdout.json
已實測的是這個既有環境的完整執行,沒有聲稱新 pip 環境或各平台都已安裝驗證。成功時 exit 0、JSON 可解析,能核對 1858 decisions、1843 optimizer updates、36 copies,以及 50 個 online scalar parameters。
還應打開真實 target 範例、copy log 和初始/最後權重,而不是只看 summary。對同一輸入,target 在 copy 之間參數不變;更新 loss 的梯度只進 online。完整輸出也保留同一首批舊 label 的 diagnostic,避免把 changing-batch loss 誤作固定資料上的比較。
"""Original one-hot MDP DQN mechanics, not an Atari CNN or DQN benchmark."""
import collections
import copy
import hashlib
import json
import math
import platform
import random
import torch
from torch import nn
SEED, REPLAY_SEED = 7, 7007
EPISODES, MAX_STEPS = 600, 20
GAMMA, EPSILON, LEARNING_RATE = .9, .2, .01
BATCH_SIZE, CAPACITY, TARGET_PERIOD = 16, 512, 50
GOAL, TRAP = 3, 4
DTYPE = torch.float64
def environment_step(state, action):
table = {(0, 0): (TRAP, -1., True), (0, 1): (1, -.1, False),
(1, 0): (0, -.1, False), (1, 1): (2, -.1, False),
(2, 0): (1, -.1, False), (2, 1): (GOAL, 1., True)}
return table[state, action]
def encode(states):
# Terminal input is never used for choosing actions. Its zero vector is masked below.
return torch.tensor([[float(state == k) for k in range(3)] for state in states], dtype=DTYPE)
def make_network():
return nn.Sequential(nn.Linear(3, 8), nn.ReLU(), nn.Linear(8, 2)).to(dtype=DTYPE, device="cpu")
def parameter_digest(network):
payload = json.dumps([p.detach().tolist() for p in network.parameters()], separators=(",", ":"))
return hashlib.sha256(payload.encode("ascii")).hexdigest()
def batch_tensors(transitions):
states, actions, rewards, next_states, terminated = zip(*transitions)
return (encode(states), torch.tensor(actions, dtype=torch.int64),
torch.tensor(rewards, dtype=DTYPE), encode(next_states),
torch.tensor(terminated, dtype=torch.bool))
def targets_for_batch(target, rewards, next_inputs, terminated):
with torch.no_grad():
next_max = target(next_inputs).max(dim=1).values
values = rewards + GAMMA * (~terminated).to(DTYPE) * next_max
return values, next_max
def choose(online, state, rng):
if rng.random() < EPSILON:
return rng.randrange(2)
with torch.no_grad():
# torch.argmax returns the first index in a tie.
return int(online(encode([state]))[0].argmax())
def model_oracle():
q = [[0., 0.] for _ in range(3)]
for sweep in range(1, 1001):
updated = []
for state in range(3):
row = []
for action in range(2):
next_state, reward, terminal = environment_step(state, action)
row.append(reward if terminal else reward + GAMMA * max(q[next_state]))
updated.append(row)
delta = max(abs(updated[s][a] - q[s][a]) for s in range(3) for a in range(2))
q = updated
if delta < 1e-14:
return q, sweep
raise RuntimeError("Oracle did not stabilize")
def greedy_evaluate(network):
state, trajectory = 0, []
with torch.no_grad():
for _ in range(MAX_STEPS):
action = int(network(encode([state]))[0].argmax())
next_state, reward, terminal = environment_step(state, action)
trajectory.append({"state": state, "action": action, "reward": reward,
"next_state": next_state, "terminated": terminal})
state = next_state
if terminal:
break
return {"trajectory": trajectory,
"undiscounted_return": math.fsum(x["reward"] for x in trajectory),
"discounted_return": math.fsum(GAMMA**t * x["reward"] for t, x in enumerate(trajectory)),
"terminated": trajectory[-1]["terminated"], "truncated": not trajectory[-1]["terminated"]}
def main():
torch.set_num_threads(1)
torch.manual_seed(SEED)
behavior_rng, replay_rng = random.Random(SEED), random.Random(REPLAY_SEED)
online = make_network()
initial = copy.deepcopy(online)
target = copy.deepcopy(online)
target.requires_grad_(False)
target.eval()
optimizer = torch.optim.Adam(online.parameters(), lr=LEARNING_RATE, foreach=False)
replay = collections.deque(maxlen=CAPACITY)
visits = [[0, 0] for _ in range(3)]
endings = {"goal": 0, "trap": 0, "truncated": 0}
updates, decisions, terminal_batch_entries, nonterminal_batch_entries = 0, 0, 0, 0
copies, losses, samples = [], [], {"terminal": [], "nonterminal": []}
first_batch = None
target_unchanged_during_optimizer = True
target_grad_always_none = True
first_episode = []
for episode in range(EPISODES):
state = 0
for step in range(1, MAX_STEPS + 1):
action = choose(online, state, behavior_rng)
next_state, reward, terminal = environment_step(state, action)
truncated = step == MAX_STEPS and not terminal
transition = (state, action, reward, next_state, terminal)
replay.append(transition)
visits[state][action] += 1
decisions += 1
if episode == 0:
first_episode.append(list(transition))
if len(replay) >= BATCH_SIZE:
batch = replay_rng.sample(list(replay), BATCH_SIZE) # without replacement within batch
inputs, actions, rewards, next_inputs, terminated = batch_tensors(batch)
labels, next_max = targets_for_batch(target, rewards, next_inputs, terminated)
selected = online(inputs).gather(1, actions[:, None]).squeeze(1)
loss = ((selected - labels)**2).mean()
if first_batch is None:
first_batch = {"transitions": batch, "labels": labels.detach().clone(),
"initial_selected_predictions": selected.detach().tolist()}
target_before = parameter_digest(target)
optimizer.zero_grad(set_to_none=True)
loss.backward()
target_grad_always_none &= all(p.grad is None for p in target.parameters())
optimizer.step()
updates += 1
target_unchanged_during_optimizer &= parameter_digest(target) == target_before
losses.append(float(loss.detach()))
terminal_batch_entries += int(terminated.sum())
nonterminal_batch_entries += int((~terminated).sum())
for j, trans in enumerate(batch):
kind = "terminal" if trans[4] else "nonterminal"
if len(samples[kind]) < 3:
samples[kind].append({"optimizer_update": updates,
"transition": list(trans), "target_network_next_max": float(next_max[j]),
"continuation_multiplier": 0 if trans[4] else 1,
"label": float(labels[j]), "selected_q_before_step": float(selected.detach()[j]),
"label_requires_grad": labels.requires_grad})
if updates % TARGET_PERIOD == 0:
target.load_state_dict(online.state_dict())
copies.append({"optimizer_update": updates, "environment_decisions": decisions,
"online_digest": parameter_digest(online), "target_digest": parameter_digest(target),
"equal_after_copy": parameter_digest(target) == parameter_digest(online)})
if terminal:
endings["goal" if next_state == GOAL else "trap"] += 1
break
if truncated:
endings["truncated"] += 1
break
state = next_state
with torch.no_grad():
final_q = online(encode(range(3))).tolist()
initial_q = initial(encode(range(3))).tolist()
inputs, _, _, _, _ = batch_tensors(first_batch["transitions"])
first_actions = torch.tensor([t[1] for t in first_batch["transitions"]], dtype=torch.int64)
final_on_first = online(inputs).gather(1, first_actions[:, None]).squeeze(1)
frozen_first_label_mse_after = float(((final_on_first - first_batch["labels"])**2).mean())
oracle_q, sweeps = model_oracle() # Evaluator only, never training labels.
oracle_error = max(abs(final_q[s][a] - oracle_q[s][a]) for s in range(3) for a in range(2))
bellman_residual = []
for state in range(3):
for action in range(2):
ns, reward, terminal = environment_step(state, action)
label = reward if terminal else reward + GAMMA * max(final_q[ns])
bellman_residual.append(abs(final_q[state][action] - label))
# Controlled frozen-target/sync probe, separate from and not modifying trained networks.
probe_online, probe_target = copy.deepcopy(online), copy.deepcopy(target)
probe_before = parameter_digest(probe_target)
with torch.no_grad():
next(probe_online.parameters()).add_(.123)
unchanged_before_copy = parameter_digest(probe_target) == probe_before
different_before_copy = parameter_digest(probe_target) != parameter_digest(probe_online)
probe_target.load_state_dict(probe_online.state_dict())
equal_after_probe_copy = parameter_digest(probe_target) == parameter_digest(probe_online)
changed_tensors = sum(not torch.equal(a, b) for a, b in zip(initial.parameters(), online.parameters()))
assert sum(endings.values()) == EPISODES
assert sum(map(sum, visits)) == decisions
assert updates == decisions - BATCH_SIZE + 1
assert len(copies) == updates // TARGET_PERIOD
assert target_unchanged_during_optimizer and target_grad_always_none
assert all(x["equal_after_copy"] for x in copies)
assert unchanged_before_copy and different_before_copy and equal_after_probe_copy
print(json.dumps({
"scope": "Original single-seed deterministic one-hot MDP neural Q-learning with replay and hard targets; "
"not Atari, a CNN benchmark, a multi-seed study or a convergence guarantee.",
"versions": {"python": platform.python_version(), "torch": torch.__version__,
"device": "cpu", "dtype": "float64", "threads": torch.get_num_threads()},
"settings": {"episodes": EPISODES, "max_steps": MAX_STEPS, "gamma": GAMMA, "epsilon": EPSILON,
"torch_and_behavior_seed": SEED, "separate_replay_seed": REPLAY_SEED,
"network": "onehot3 -> Linear3x8+bias -> ReLU -> Linear8x2+bias;50parameters",
"optimizer": "Adam lr.01 foreach=False, no weight decay", "batch_size": BATCH_SIZE,
"capacity": CAPACITY, "target_copy_every_optimizer_updates": TARGET_PERIOD,
"warmup": "first15observations collected without optimization; first update at decision16",
"sampling": "uniform without replacement within each minibatch, repeated reuse across updates",
"update_frequency": "one optimizer update per new environment decision once replay size>=16",
"loss": "mean squared error on chosen action Q only; target from frozen target network, no gradient",
"terminal": "r only for true goal/trap; zero-input terminal Q is masked and has no semantic state value",
"truncation": "collector limit only, retains bootstrap on observed nonterminal next state"},
"actual_training": {"decisions": decisions, "optimizer_updates": updates,
"target_copies_excluding_initial_clone": len(copies), "episode_endings": endings,
"visits_L_R": visits, "final_replay_size": len(replay),
"terminal_batch_entries": terminal_batch_entries,
"nonterminal_batch_entries": nonterminal_batch_entries,
"first_episode_observed_transitions": first_episode,
"first_update_loss": losses[0], "last_update_loss": losses[-1],
"mean_first_50_update_losses": math.fsum(losses[:50]) / len(losses[:50]),
"mean_last_50_update_losses": math.fsum(losses[-50:]) / len(losses[-50:]),
"loss_scope": "different sampled batches and changing targets; not a fixed validation learning curve",
"changed_parameter_tensors": changed_tensors,
"parameter_count_online_only": sum(p.numel() for p in online.parameters()),
"target_unchanged_across_every_optimizer_step": target_unchanged_during_optimizer,
"target_grad_always_none": target_grad_always_none},
"initial_online_q_L_R": initial_q, "final_online_q_L_R": final_q,
"oracle_after_training_only": {"q_L_R": oracle_q, "sweeps": sweeps,
"max_online_abs_error": oracle_error, "max_online_Bellman_residual": max(bellman_residual)},
"greedy_evaluation_frozen_online": greedy_evaluate(online),
"saved_training_target_examples": samples, "actual_target_copy_log": copies,
"fixed_first_minibatch_old_target_diagnostic": {
"description": "Same first minibatch and its original labels, NOT new Bellman targets or validation",
"initial_mse": losses[0], "final_mse_against_old_labels": frozen_first_label_mse_after,
"old_labels": first_batch["labels"].tolist()},
"controlled_copy_probe_not_training": {"target_unchanged_after_perturbing_probe_online": unchanged_before_copy,
"probe_networks_different_before_copy": different_before_copy,
"probe_networks_equal_after_copy": equal_after_probe_copy},
"parameter_records": {"initial_online": {k: v.tolist() for k, v in initial.state_dict().items()},
"final_online": {k: v.tolist() for k, v in online.state_dict().items()},
"final_target": {k: v.tolist() for k, v in target.state_dict().items()}},
"limits": ["600episodes and allhyperparameters fixed before this one run; no retuning to oracle error.",
"Oracle and allstate diagnostics are computed after learning, never used for replay or labels.",
"Replay reduces serial coupling but is finite/policy-generated, not a proof of iid coverage.",
"Hardtarget copies are counted by optimizer updates, not episodes or environment-only warmup.",
"No-gradient is enforced for labels and target params; only online params are in optimizer.",
"MLP shares parameters: unchosen outputs can change indirectly despite chosen-only loss.",
"ReLU/onehot/Adam/MSE/fixedepsilon/.9gamma differ from original Atari CNN/RMSProp/preprocessing.",
"2013paper has replay;2015algorithm explicitly uses separate periodically copied target network.",
"Initial/final changingbatch losses and oracle error are not generalization/online-performance proof.",
"CPUfloat64 only;installedCUDA-taggedwheel does not mean GPU was used or tested."]}, indent=2))
if __name__ == "__main__":
main()
Replay 與 target 是穩定學習的設計,不是普遍保證
DQN 讓我們能從很大的狀態表示學 Q,不必先替每個狀態列一格。但這同時結合了共享函數近似、bootstrap 與 off-policy 資料:參數改變可能影響多處預測,資料分布又會被策略影響。Replay 和延遲 target 是針對這些困難的設計,不會自動消除它們。
如果模型表現不好,先追實際資訊流:buffer 裡是否真的都是已觀察資料?chosen_Q 是否挑對 action?terminal 是否只留下 r?target 是否意外收到梯度?你說的「每50步」究竟是哪一個時鐘?這些問題比把更多神經網路層接上去更早影響結果。
本例把狀態縮到三個,讓每個 Q 都可對答案。真正走向影像或較大環境時,表示能力、探索、回饋尺度與覆蓋問題都重新出現;不能把這裡單 seed 的成功當成它們已經解決。
來源與延伸閱讀:
-
Mnih 等(2013),Playing Atari with Deep Reinforcement Learning,本文核對 v1 的 §2、§4、Algorithm 1 與完整 §4.1:replay、影格處理與兩層 CNN。原論文像素只作連結,未重刊。
-
Mnih 等(2015),Human-level control through deep reinforcement learning,作者 PDF。Methods 的 preprocessing、model architecture 與 Algorithm 1 支持本篇歷史結構對照,以及每 C updates 複製獨立 target 的機制;本文未重現其 Atari 訓練或效能數據。
-
Juanciño(2026),DQN 概念圖作品頁、檔案歷史,及其加利西亞語條目脈絡。圖 1、2 與封面依 CC BY-SA 4.0 使用及改作;不代表原論文作者繪製或背書。
-
Adam Paszke/Mark Towers,PyTorch DQN 教學;對照的固定版本程式與 BSD-3-Clause 授權。來源教學使用 CartPole、soft update 與 SmoothL1Loss;本文使用的 MLP、硬複製與 MSE 設定另見完整程式。
-
公式顯示涉及的 KaTeX 固定版本及 KaTeX 字型專案,必要完整 MIT 聲明見下方;本文未散布字型二進位檔。
圖像署名、改作與授權副本:
Source: “Figura 03 arquitectura dqn.svg” / “Compoñentes dun axente DQN” by Juanciño, https://commons.wikimedia.org/wiki/File:Figura_03_arquitectura_dqn.svg, licensed CC BY-SA 4.0, https://creativecommons.org/licenses/by-sa/4.0/. Changes: rendered the full SVG as PNG; Figure 02 is a crop of two complete boxes and their connecting arrow with 6 px white padding; the cover is a proportional full-figure fit on a white canvas. These adaptations are provided under CC BY-SA 4.0. This later explanatory schematic is not a Mnih et al. paper figure.
Source: PyTorch Tutorials, “Reinforcement Learning (DQN) Tutorial,” by Adam Paszke and Mark Towers, https://docs.pytorch.org/tutorials/intermediate/reinforcement_q_learning.html. The data-flow diagram comes from the tutorial repository at commit 5672cd98a7671eda4d9122b10f8eec580f74d03e; the Huber equation is a screenshot of the page’s KaTeX-rendered piecewise definition only (the adjacent batch mean is outside the crop); the duration chart is the tutorial’s CartPole-v1 output. Changes: data-flow image reproduced as provided; Huber screenshot cropped only to remove white margins; result chart reproduced whole. The diagram shows the tutorial’s soft target update, and the result is CartPole, not Atari or this project’s run. The tutorial repository’s BSD 3-Clause license is reproduced below.
BSD 3-Clause License (verbatim from pinned PyTorch Tutorials repository)
BSD 3-Clause License
Copyright (c) 2017-2022, Pytorch contributors
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
KaTeX renderer and fonts used in the formula screenshot
The live page reports KaTeX 0.16.10; the font files were served from the katex@0.10.0-beta package URL. The screenshot contains rendered glyphs only and does not redistribute the font binaries. Their license notices are reproduced below.
KaTeX code MIT License (verbatim from KaTeX v0.10.0-beta)
The MIT License (MIT)
Copyright (c) 2015 Khan Academy
This software also uses portions of the underscore.js project, which is
MIT licensed with the following copyright:
Copyright (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative
Reporters & Editors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
KaTeX font package MIT License (verbatim from KaTeX fonts repository)
The MIT License (MIT)
Copyright (c) 2018 Khan Academy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
留言0
登入後就能留言。
登入