DQN

Q-network, replay buffer, dan target network.

Daftar isi

DQN menggabungkan aproksimasi fungsi nilai dengan dua teknik penstabil, yaitu replay buffer dan target network, sehingga pelatihan Q-learning menjadi layak pada jaringan saraf. Topik ini menyusun ketiga komponen DQN menjadi satu loop pelatihan pada CartPole-v1, menjelaskan peran tiap komponen, lalu melaporkan keluaran setelah 400 langkah. Pembaca akan melihat loss rata-rata, reward jendela bergerak, ukuran buffer, dan nilai QQ pada satu observasi sampel yang dihasilkan oleh jaringan terlatih parsial.

Tiga komponen DQN#

Inti DQN adalah Q-learning dengan aproksimasi fungsi nilai. Pada setiap langkah, agen mengambil aksi berdasarkan q^(s,⋅;θ)\hat{q}(s,\cdot;\theta) melalui epsilon-greedy, lalu menyimpan transisi (s,a,r,s′,done)(s,a,r,s',\text{done}) ke dalam buffer. Pembaruan Q-learning dilakukan dengan menarik batch acak dari buffer, menghitung target y=r+γmax⁡a′q^(s′,a′;θ−)y = r + \gamma \max_{a'} \hat{q}(s',a';\theta^-), lalu menurunkan loss antara q^(s,a;θ)\hat{q}(s,a;\theta) dan yy. Karena batch diambil dari transisi masa lalu, pembaruan dapat berlangsung beberapa kali per langkah interaksi.

Replay buffer menstabilkan pelatihan dengan dua cara. Pertama, batch acak memutus korelasi temporal antar transisi yang berurutan, sehingga gradien tidak didominasi oleh satu trajektori pendek. Kedua, transisi lama dipakai ulang, sehingga data interaksi yang mahal dimanfaatkan beberapa kali. Target network dengan bobot θ−\theta^- yang diperbarui secara berkala menjaga target tetap stabil dalam jangka pendek, sebab bila target dihitung dari θ\theta yang sedang diupdate, target ikut bergeser pada setiap langkah dan pelatihan mengejar nilai yang bergerak.

Loop pelatihan pada CartPole-v1#

Berkas dqn.py berikut menjalankan DQN penuh selama 400 langkah interaksi, dengan jaringan tiga lapis, buffer berkapasitas 1000, batch 32, γ=0,99\gamma=0{,}99, ϵ=0,1\epsilon=0{,}1, dan target network diperbarui setiap 50 langkah. Laporan akhir mencakup loss rata-rata, reward jendela bergerak 20 langkah, ukuran buffer, dan nilai QQ pada observasi terakhir.

python dqn.py
import warnings; warnings.filterwarnings("ignore")
import numpy as np, torch
np.random.seed(0); torch.manual_seed(0)
import random; random.seed(0)
import gymnasium as gym
from collections import deque
torch.set_printoptions(precision=4, sci_mode=False)


class QNet(torch.nn.Module):
    def __init__(self, obs_dim, n_actions, hidden=64):
        super().__init__()
        self.net = torch.nn.Sequential(
            torch.nn.Linear(obs_dim, hidden),
            torch.nn.ReLU(),
            torch.nn.Linear(hidden, hidden),
            torch.nn.ReLU(),
            torch.nn.Linear(hidden, n_actions),
        )

    def forward(self, x):
        return self.net(x)


env = gym.make("CartPole-v1")
obs, info = env.reset(seed=0)
env.action_space.seed(0)
env.observation_space.seed(0)

OBS_DIM = 4
N_ACTIONS = 2
online = QNet(OBS_DIM, N_ACTIONS, 64)
target = QNet(OBS_DIM, N_ACTIONS, 64)
target.load_state_dict(online.state_dict())

buffer = deque(maxlen=1000)
BATCH = 32
GAMMA = 0.99
EPS = 0.1
TARGET_UPDATE = 50
TOTAL_STEPS = 400
opt = torch.optim.Adam(online.parameters(), lr=1e-3)
loss_fn = torch.nn.SmoothL1Loss()

recent_rewards = []
recent_losses = []
epsilon_final = EPS

for step in range(TOTAL_STEPS):
    if random.random() < EPS:
        a = env.action_space.sample()
    else:
        with torch.no_grad():
            q = online(torch.tensor(obs, dtype=torch.float32).unsqueeze(0))
        a = int(q.argmax(dim=1).item())
    next_obs, r, term, trunc, _ = env.step(a)
    buffer.append((obs, a, r, next_obs, term))
    recent_rewards.append(r)
    if len(recent_rewards) > 20:
        recent_rewards.pop(0)
    obs = next_obs
    done = term or trunc
    if done:
        obs, info = env.reset()
    if len(buffer) >= BATCH:
        batch = random.sample(list(buffer), BATCH)
        s = torch.tensor([t[0] for t in batch], dtype=torch.float32)
        ac = torch.tensor([t[1] for t in batch], dtype=torch.long)
        rw = torch.tensor([t[2] for t in batch], dtype=torch.float32)
        sn = torch.tensor([t[3] for t in batch], dtype=torch.float32)
        dn = torch.tensor([t[4] for t in batch], dtype=torch.float32)
        with torch.no_grad():
            qn = target(sn)
            qn_max = qn.max(dim=1).values
            y = rw + GAMMA * (1 - dn.float()) * qn_max
        q_pred = online(s).gather(1, ac.unsqueeze(1)).squeeze(1)
        loss = loss_fn(q_pred, y)
        opt.zero_grad()
        loss.backward()
        opt.step()
        recent_losses.append(loss.item())
        if len(recent_losses) > 30:
            recent_losses.pop(0)
    if (step + 1) % TARGET_UPDATE == 0:
        target.load_state_dict(online.state_dict())

avg_reward_window = sum(recent_rewards) / len(recent_rewards) if recent_rewards else 0.0
avg_loss_recent = sum(recent_losses) / len(recent_losses) if recent_losses else 0.0
sample_obs = obs
with torch.no_grad():
    q_sample = online(torch.tensor(sample_obs, dtype=torch.float32).unsqueeze(0))

print(f"epsilon_final      = {epsilon_final:.4f}")
print(f"avg_loss_recent    = {avg_loss_recent:.4f}")
print(f"avg_reward_window  = {avg_reward_window:.4f}")
print(f"buffer_size        = {len(buffer)}")
print(f"sample_obs         = {sample_obs}")
print(f"Q(s_sample, .)     = {q_sample.flatten()}")
text Keluaran
epsilon_final      = 0.1000
avg_loss_recent    = 0.4490
avg_reward_window  = 1.0000
buffer_size        = 400
sample_obs         = [-0.00830109  0.3832396  -0.00506989 -0.53812236]
Q(s_sample, .)     = tensor([7.9322, 8.1030])

Membaca keluaran parsial#

Empat ratus langkah belum cukup untuk membuat kebijakan konvergen, sehingga reward jendela bergerak masih 1,0000, yang berarti episode rata-rata sangat pendek. Akan tetapi, loss rata-rata 0,4490 menunjukkan jaringan sedang belajar menyesuaikan target Smooth L1, dan nilai QQ pada observasi sampel sekitar [7,9,8,1][7{,}9, 8{,}1] mengindikasikan jaringan mulai membedakan kedua aksi. Angka-angka ini menggambarkan keadaan awal pelatihan, bukan keberhasilan, dan pemilihan 400 langkah memang ditujukan agar keluaran dapat direproduksi dalam waktu singkat.

Buffer berukuran 400 karena setiap langkah menambahkan satu transisi, dan batch 32 mulai diambil setelah buffer terisi cukup. Target network telah diperbarui delapan kali pada akhir pelatihan (setiap 50 langkah dari 400), sehingga target mengikuti perubahan online network dengan keterlambatan yang menstabilkan. Bila langkah pelatihan ditingkatkan ke beberapa ribu, reward jendela bergerak biasanya naik ke puluhan, dan kebijakan serakah mulai menyeimbangkan tiang.

Kesalahan umum#

Memakai online network sebagai target tanpa target network. Penyebabnya, target yy dihitung dari θ\theta yang sedang diupdate sehingga target ikut bergeser tiap langkah. Akibatnya, pelatihan menjadi tidak stabil atau divergen pada observasi berdimensi tinggi. Perbaikannya, gunakan target network terpisah yang bobotnya disalin dari online network secara berkala.

Lupa mengalikan target dengan (1−done)(1-\text{done}). Penyebabnya, γmax⁡Q(s′)\gamma \max Q(s') tetap ditambahkan pada transisi terminal, padahal tidak ada state berikutnya. Akibatnya, nilai QQ pada state terminal terlalu tinggi dan bias menyebar ke state sebelumnya. Perbaikannya, kalikan suku bootstrap dengan (1−done)(1-\text{done}) sehingga target terminal menjadi tepat rr.

Ringkasan#

  • DQN adalah Q-learning dengan aproksimasi fungsi nilai, replay buffer yang memutus korelasi temporal dan memakai ulang transisi, serta target network yang menstabilkan target dalam jangka pendek.
  • Pada CartPole-v1 selama 400 langkah, loss rata-rata mencapai 0,4490 dengan reward jendela bergerak 1,0000 dan Q(s,⋅)Q(s,\cdot) sekitar [7,9,8,1][7{,}9, 8{,}1], yang menggambarkan keadaan awal pelatihan.
  • Target terminal harus dikalikan (1−done)(1-\text{done}) agar nilai QQ pada state akhir tepat sama dengan reward, dan target network diperbarui berkala bukan tiap langkah.

Referensi#