Tabel Q pada chapter Value Function
menyimpan satu angka untuk setiap pasangan state-aksi — nyaman untuk 16 state, mustahil untuk ruang state besar: jumlah kombinasi piksel pada papan Atari berukuran 84×84×4 frame saja sudah melampaui jumlah atom semesta, dan state nyata jarang dikunjungi dua kali persis. Deep Q-Network (DQN) mengganti tabel dengan jaringan neural Q_θ(s,a): satu penaksir parametrik yang menggeneralisasi ke state yang belum pernah dilihat. Persatuan ini tidak serta-merta stabil, sehingga DQN membawa dua mekanisme penopang — replay buffer dan target network — yang kita bangun dan buktikan satu per satu pada demo mini PyTorch.
Dari Tabel ke Jaringan #
Jaringan Q_θ menerima state dan mengeluarkan satu nilai per aksi — pada dunia kita, input one-hot 16 dimensi dan keluaran 4 nilai. Loss-nya tetap galat temporal difference chapter sebelumnya, kini dibungkus sebagai regresi:
L = mean [ (r + γ · max_a' Q_target(s',a') − Q_θ(s,a))² ]
Bagian dalam kurung adalah selisih dugaan baru lawan dugaan lama — kerabat loss kuadrat yang kita kenal dari chapter Loss dan Optimizer . Aksi tetap dipilih epsilon-greedy dari keluaran jaringan, dan gradien dihitung autograd seperti biasa.
Dua Masalah Stabilitas #
Menggabungkan jaringan dengan TD loss secara polos membuat training mudah goyah, karena dua sifat data RL yang tidak ada di supervised learning:
- Data berkorelasi. Transisi yang datang berurutan
(s → s' → s'')nyaris saling menempel; memakainya beruntun menghasilkan batch yang tidak mewakili keseluruhan dunia dan gradien yang berayun. Replay buffer menyimpan transisi ke dalam antrean besar lalu mengambil sampel acak dari sana — korelasi urutan putus, dan satu transisi nyata dipakai berulang kali untuk beberapa pembaruan (efisiensi data naik). - Target berpindah. Rumus loss memuat
max Q(s',a')yang dihitung dari jaringan yang sama yang sedang dilatih — setiap pembaruan bobot menggeser target yang mengejarnya, seperti mengejar sesuatu yang ikut berlari. Target network adalah salinan beku dari jaringan Q; target dihitung dari salinan ini dan bobotnya disalin ulang secara berkala, sehingga target stabil dalam jangka satu periode.
Diagram ini merangkum alur data: transisi mengalir ke buffer, batch acak memberi makan jaringan Q, target datang dari salinan beku, dan bobot disalin berkala. Tabel berikut merangkum peran tiap komponen dalam demo:
| Komponen | Peran |
|---|---|
q_net | menaksir Q(s,a); satu-satunya yang menerima gradien |
target_net | salinan beku; pemasang nilai target yang stabil |
buffer | antrean 1000 transisi; sumber batch acak |
| jadwal epsilon | 0.99^episode, dibatasi bawah 0.05 — eksplorasi meluruh |
mse_loss + Adam | menekan galat TD dan menyetel bobot |
Demo: DQN Mini #
Dunianya GridWorld 4x4 yang sama dengan chapter Value Function, agar hasilnya bisa dibandingkan langsung dengan Q-learning tabular. Berkas dqn_mini.py berjalan 300 episode dengan buffer 1000 dan sinkronisasi target tiap 50 episode:
"""DQN mini pada GridWorld 4x4 dengan replay buffer dan target network.
Dunia sama dengan chapter Value Function: mulai (0,0), target (3,3),
reward -0.05 per langkah dan +1 saat tiba; menabrak dinding = diam.
State dienkode one-hot 16 dimensi; jaringan Q memetakan state ke 4 nilai
Q (satu per aksi). Dua mekanisme stabilitas dipakai: replay buffer
(kapasitas 1000) dan target network yang disalin setiap 50 episode.
"""
import collections
import numpy as np
import torch
import torch.nn as nn
torch.manual_seed(7)
torch.set_num_threads(1) # model kecil: satu thread justru lebih cepat
rng = np.random.default_rng(7)
UKURAN = 4
START, GOAL = (0, 0), (3, 3)
AKSI = [(-1, 0), (1, 0), (0, -1), (0, 1)] # atas, bawah, kiri, kanan
PANAH = ["^", "v", "<", ">"]
MAKS_LANGKAH = 50
IDENT = torch.eye(UKURAN * UKURAN) # enkoder one-hot
def step(state, aksi):
"""Satu interaksi; kembalikan (state_baru, reward, selesai)."""
db, dk = AKSI[aksi]
nb, nk = state[0] + db, state[1] + dk
state_baru = (nb, nk) if (0 <= nb < UKURAN and 0 <= nk < UKURAN) else state
selesai = state_baru == GOAL
return state_baru, (1.0 if selesai else -0.05), selesai
def buat_jaringan():
"""Jaringan Q: input one-hot 16 -> 64 ReLU -> 4 nilai Q."""
return nn.Sequential(nn.Linear(16, 64), nn.ReLU(), nn.Linear(64, 4))
q_net = buat_jaringan()
target_net = buat_jaringan()
target_net.load_state_dict(q_net.state_dict()) # awal: bobot identik
optimizer = torch.optim.Adam(q_net.parameters(), lr=2.5e-3)
buffer = collections.deque(maxlen=1000)
GAMMA, BATCH = 0.9, 32
riwayat_loss = []
riwayat = []
for episode in range(1, 301):
epsilon = max(0.05, 0.99 ** (episode - 1))
state, total = START, 0.0
for _ in range(MAKS_LANGKAH):
s_idx = state[0] * UKURAN + state[1]
if rng.random() < epsilon: # eksplorasi
aksi = int(rng.integers(0, 4))
else: # eksploitasi
with torch.no_grad():
q = q_net(IDENT[s_idx:s_idx + 1])
aksi = int(q.argmax(dim=1).item())
state_baru, reward, selesai = step(state, aksi)
s2_idx = state_baru[0] * UKURAN + state_baru[1]
buffer.append((s_idx, aksi, reward, s2_idx, float(selesai)))
total += reward
state = state_baru
if len(buffer) >= 100: # mulai latih
pilihan = rng.choice(len(buffer), size=BATCH, replace=False)
contoh = [buffer[i] for i in pilihan]
s_t = IDENT[[c[0] for c in contoh]]
a_t = torch.tensor([c[1] for c in contoh])
r_t = torch.tensor([c[2] for c in contoh])
s2_t = IDENT[[c[3] for c in contoh]]
d_t = torch.tensor([c[4] for c in contoh])
q_sa = q_net(s_t).gather(1, a_t.unsqueeze(1)).squeeze(1)
with torch.no_grad():
target = r_t + GAMMA * (1 - d_t) * target_net(s2_t).max(dim=1).values
loss = nn.functional.mse_loss(q_sa, target)
optimizer.zero_grad()
loss.backward()
optimizer.step()
riwayat_loss.append(loss.item())
if selesai:
break
if episode % 50 == 0: # salin ke target
target_net.load_state_dict(q_net.state_dict())
riwayat.append(total)
print("DQN mini: 300 episode, buffer 1000, target sync tiap 50 episode")
print("epsilon per 50 episode:",
", ".join(f"{max(0.05, 0.99 ** (e - 1)):.2f}"
for e in [1, 50, 100, 150, 200, 250, 300]))
print("\nMean total reward per blok 50 episode:")
for i in range(0, 300, 50):
print(f" episode {i + 1:3d}-{i + 50:3d}: {np.mean(riwayat[i:i + 50]):+.3f}")
print(f"\nUkuran buffer : {len(buffer)}/1000")
print(f"Transisi reward +1 di buffer: {sum(1 for c in buffer if c[2] > 0)}")
print(f"Mean TD loss 100 update pertama : {np.mean(riwayat_loss[:100]):.4f}")
print(f"Mean TD loss 100 update terakhir: {np.mean(riwayat_loss[-100:]):.4f}")
print("\nKebijakan greedy dari jaringan Q:")
for b in range(UKURAN):
sel = []
for k in range(UKURAN):
if (b, k) == GOAL:
sel.append(" G ")
else:
with torch.no_grad():
q = q_net(IDENT[b * UKURAN + k:b * UKURAN + k + 1])
sel.append(f" {PANAH[int(q.argmax(dim=1).item())]} ")
print(" " + "".join(sel))
with torch.no_grad():
q_start = q_net(IDENT[0:1])[0]
nama_aksi = ["atas", "bawah", "kiri", "kanan"]
print("\nQ(start=(0,0)) : " + " ".join(
f"{n}={v:+.2f}" for n, v in zip(nama_aksi, q_start.tolist())))
state, rute, total = START, [START], 0.0
for _ in range(MAKS_LANGKAH):
s_idx = state[0] * UKURAN + state[1]
with torch.no_grad():
aksi = int(q_net(IDENT[s_idx:s_idx + 1]).argmax(dim=1).item())
state, reward, selesai = step(state, aksi)
rute.append(state)
total += reward
if selesai:
break
print("Jalur greedy : " + " -> ".join(f"({b},{k})" for b, k in rute))
print(f"Panjang jalur : {len(rute) - 1} langkah, total reward = {total:+.2f}")Output yang Dihasilkan:
DQN mini: 300 episode, buffer 1000, target sync tiap 50 episode
epsilon per 50 episode: 1.00, 0.61, 0.37, 0.22, 0.14, 0.08, 0.05
Mean total reward per blok 50 episode:
episode 1- 50: -1.351
episode 51-100: +0.109
episode 101-150: +0.243
episode 151-200: +0.316
episode 201-250: +0.540
episode 251-300: +0.729
Ukuran buffer : 1000/1000
Transisi reward +1 di buffer: 113
Mean TD loss 100 update pertama : 0.0082
Mean TD loss 100 update terakhir: 0.0000
Kebijakan greedy dari jaringan Q:
> > v v
v > v v
> > > v
> > > G
Q(start=(0,0)) : atas=-0.10 bawah=+0.39 kiri=-0.10 kanan=+0.39
Jalur greedy : (0,0) -> (0,1) -> (0,2) -> (1,2) -> (2,2) -> (2,3) -> (3,3)
Panjang jalur : 6 langkah, total reward = +0.75Kurva belajarnya lebih lambat memulai daripada Q-learning tabular — blok pertama masih -1.351 karena epsilon di awal 1.00 (perilaku nyaris acak) dan buffer baru terisi — lalu menanjak bertahap ke +0.729 saat epsilon meluruh ke 0.05. Mean TD loss 100 update pertama 0.0082 merosot ke 0.0000: jaringan Q_θ dan targetnya akhirnya sepakat. Hasil akhirnya setara tabular: kebijakan greedy menghasilkan jalur enam langkah dengan total reward +0.75 — kembali ke return optimal dunia ini, meski Q(start) hanya menaksir (atas=-0.10, bawah=+0.39, kiri=-0.10, kanan=+0.39) dan tidak lagi eksak seperti tabel. Mari kita telaah kode di atas baris per baris:
- Baris 16-18: tiga sumber ketidakpastian dikunci — seed PyTorch, seed NumPy untuk eksplorasi, dan
set_num_threads(1)agar eksekusi CPU tunggal ini deterministik. - Baris 25:
IDENT = torch.eye(16)— matriks identitas dipakai sebagai enkoder one-hot state papan. - Baris 28-34 (
step): dunia GridWorld 4x4, sama persis dengan chapter Value Function. - Baris 37-39: jaringan Q
16 → 64 ReLU → 4— empat keluaran, satu per aksi; konstruksi layer dibahas pada torch.nn . - Baris 42-46: dua jaringan dibuat;
load_state_dictmenyalin bobotq_netketarget_netagar keduanya berangkat identik; buffer adalahcollections.deque(maxlen=1000)yang otomatis membuang transisi tertua saat penuh. - Baris 52: epsilon meluruh
max(0.05, 0.99^(episode-1))— jadwal eksplorasi yang akan kita bandingkan lawan strategi lain pada chapter Exploration vs Exploitation . - Baris 56-61: pemilihan aksi — acak bila kalah taruhan epsilon, selain itu
argmaxkeluaranq_netdalam mode tanpa gradien. - Baris 64-70: transisi
(s, a, r, s', selesai)disimpan, lalu batch 32 contoh diambil acak dari buffer begitu isinya mencapai 100;IDENT[[...]]merakit matriks one-hot satu baris per contoh. - Baris 76-79: loss TD —
gathermengambilQ(s,a)untuk aksi yang benar-benar diambil; target dihitung ditorch.no_grad()daritarget_netlalu diskonto dinolkan pada state terminal. - Baris 86-87: setiap 50 episode bobot
q_netdisalin ketarget_net— periode stabilitas target. - Baris 103-131: pembacaan hasil — peta panah greedy, nilai
Q(start), dan ujian jalur greedy murni.
DQN menghasilkan penaksir nilai yang cakap di ruang state besar, tetapi kebijakannya tetap diturunkan secara tidak langsung — argmax atas nilai. Bagaimana bila kita ingin kebijakan itu sendiri sebagai jaringan, sekaligus menahan varians REINFORCE yang kita keluhkan pada chapter Policy Gradient? Jawabannya adalah menyatukan keduanya dalam satu arsitektur dua kepala: actor dan critic, topik chapter berikutnya.