Actor-critic

Actor ditambah critic dan kompromi praktisnya.

Daftar isi

Actor-critic mengganti return kumulatif REINFORCE dengan estimasi nilai dari jaringan critic, sehingga pembaruan dapat dilakukan setiap langkah alih-alih menunggu episode selesai. Topik ini menjelaskan pemisahan actor dan critic, menurunkan target TD(0) yang dipakai critic, lalu menjalankan actor-critic pada CartPole-v1 selama 500 langkah. Pembaca akan melihat contoh delta beberapa langkah awal yang mendekati satu, norma gradien rata-rata, dan return rata-rata episode terakhir yang masih rendah karena pelatihan sangat singkat.

Pemisahan actor dan critic#

Pada REINFORCE, bobot pembaruan adalah return GtG_t yang baru tersedia setelah episode selesai. Actor-critic mengganti GtG_t dengan estimasi nilai Vϕ(st)V_\phi(s_t) dari jaringan critic berparameter ϕ\phi, sehingga actor dapat diperbarui setiap langkah. Pembaruan actor memakai advantage satu langkah At=rt+1+γVϕ(st+1)−Vϕ(st)A_t = r_{t+1} + \gamma V_\phi(s_{t+1}) - V_\phi(s_t), yang sering disebut delta TD. Karena advantage hanya bergantung pada satu transisi, pembaruan tidak menunggu episode selesai, sehingga umpan balik lebih cepat dan varians lebih rendah dibanding REINFORCE.

Critic sendiri dilatih untuk meminimalkan kesalahan antara Vϕ(st)V_\phi(s_t) dan target TD rt+1+γVϕ(st+1)r_{t+1} + \gamma V_\phi(s_{t+1}), yang merupakan target TD(0) yang diperkenalkan pada topik temporal-difference sebelumnya. Pasangan actor-critic menjadi on-policy, sebab keduanya memakai data dari kebijakan saat ini. Kompromi praktisnya adalah biaya: critic menambah jaringan dan pembaruan terpisah, namun mengganti varians besar return kumulatif dengan varians satu transisi yang lebih kecil. Konsekuensi lain, critic yang buruk menyesatkan actor, sehingga kualitas critic menjadi penentu mutu pelatihan.

Actor-critic pada CartPole-v1#

Berkas actor_critic.py berikut menjalankan actor-critic penuh selama 500 langkah interaksi, dengan actor dan critic masing-masing satu lapis tersembunyi berukuran 32, γ=0,99\gamma=0{,}99, dan learning rate 10−210^{-2}. Actor diperbarui dengan −log⁡πθ(at∣st)⋅At-\log \pi_\theta(a_t\mid s_t) \cdot A_t, sedangkan critic diperbarui dengan kuadrat delta. Laporan akhir mencakup jumlah episode, return rata-rata lima episode terakhir, contoh delta lima langkah awal, dan norma gradien rata-rata actor.

python actor_critic.py
import warnings; warnings.filterwarnings("ignore")
import numpy as np, torch, random
np.random.seed(0); torch.manual_seed(0); random.seed(0)
import gymnasium as gym

GAMMA = 0.99
LR = 1e-2
MAX_STEPS = 500


class Actor(torch.nn.Module):
    def __init__(self, obs_dim, n_aksi, hidden=32):
        super().__init__()
        self.net = torch.nn.Sequential(
            torch.nn.Linear(obs_dim, hidden),
            torch.nn.Tanh(),
            torch.nn.Linear(hidden, n_aksi),
        )

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


class Critic(torch.nn.Module):
    def __init__(self, obs_dim, hidden=32):
        super().__init__()
        self.net = torch.nn.Sequential(
            torch.nn.Linear(obs_dim, hidden),
            torch.nn.Tanh(),
            torch.nn.Linear(hidden, 1),
        )

    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)

actor = Actor(4, 2)
critic = Critic(4)
opt_a = torch.optim.Adam(actor.parameters(), lr=LR)
opt_c = torch.optim.Adam(critic.parameters(), lr=LR)

step_count = 0
episode_returns = []
sample_deltas = []
grad_norms = []

s = obs
ep_return = 0.0
while step_count < MAX_STEPS:
    x = torch.tensor(s, dtype=torch.float32).unsqueeze(0)
    logits = actor(x)
    dist = torch.distributions.Categorical(logits=logits)
    a = dist.sample()
    logp = dist.log_prob(a)
    v = critic(x).squeeze(-1)
    s_next, r, term, trunc, _ = env.step(int(a.item()))
    done = term or trunc
    x_next = torch.tensor(s_next, dtype=torch.float32).unsqueeze(0)
    with torch.no_grad():
        v_next = critic(x_next).squeeze(-1)
    v_next_val = 0.0 if term else float(v_next.item())
    delta = float(r + GAMMA * v_next_val - float(v.item()))
    target = torch.tensor(r + GAMMA * v_next_val, dtype=torch.float32)
    loss_c = (v - target).pow(2)
    opt_c.zero_grad()
    loss_c.backward()
    opt_c.step()
    loss_a = -(logp * delta)
    opt_a.zero_grad()
    loss_a.backward()
    gn = 0.0
    for p in actor.parameters():
        if p.grad is not None:
            gn += float(p.grad.pow(2).sum())
    grad_norms.append(gn ** 0.5)
    if step_count < 5:
        sample_deltas.append(delta)
    opt_a.step()
    ep_return += r
    step_count += 1
    if done:
        episode_returns.append(ep_return)
        ep_return = 0.0
        s, _ = env.reset()
    else:
        s = s_next

last_eps = episode_returns[-5:] if len(episode_returns) >= 5 else episode_returns
avg_ret = float(np.mean(last_eps)) if last_eps else 0.0
avg_gn = float(np.mean(grad_norms))
print(f"steps                    = {step_count}")
print(f"jumlah episode           = {len(episode_returns)}")
print(f"avg return 5 ep terakhir = {avg_ret:.4f}")
print(f"contoh delta(5 awal)     = " + " ".join(f"{d:.4f}" for d in sample_deltas))
print(f"norm gradien rata-rata   = {avg_gn:.4f}")
env.close()
text Keluaran
steps                    = 500
jumlah episode           = 22
avg return 5 ep terakhir = 18.2000
contoh delta(5 awal)     = 0.9959 0.9868 0.9994 0.9935 0.9867
norm gradien rata-rata   = 1.9610

Membaca keluaran awal pelatihan#

Lima ratus langkah menghasilkan 22 episode, dengan return rata-rata lima episode terakhir 18,2000. Angka ini masih rendah dibanding batas atas 500 langkah CartPole-v1, yang menggambarkan keadaan awal pelatihan dengan learning rate 10−210^{-2} selama 500 langkah. Contoh delta lima langkah awal berkisar 0,99, yang konsisten dengan reward 1,0 per langkah dan estimasi nilai mendekati nol pada awal pelatihan, sehingga At≈rt+1A_t \approx r_{t+1}.

Norma gradien rata-rata 1,9610 jauh lebih kecil daripada norma gradien REINFORCE yang mencapai ribuan pada topik sebelumnya. Penurunan ini adalah inti manfaat actor-critic: dengan mengganti return kumulatif berskala besar dengan advantage satu langkah berskala kecil, pembaruan menjadi lebih stabil. Bila langkah pelatihan ditingkatkan ke beberapa ribu, return biasanya naik ke puluhan dan kebijakan mulai menyeimbangkan tiang, namun 500 langkah dipilih agar keluaran dapat direproduksi dengan cepat.

Kesalahan umum#

Memakai return kumulatif sebagai pengganti delta. Penyebabnya, critic diabaikan dan actor tetap memakai GtG_t seperti REINFORCE. Akibatnya, manfaat actor-critic hilang dan varians kembali besar, sekaligus critic tidak digunakan. Perbaikannya, hitung advantage satu langkah At=rt+1+γVϕ(st+1)−Vϕ(st)A_t = r_{t+1} + \gamma V_\phi(s_{t+1}) - V_\phi(s_t) dan pakai sebagai bobot pembaruan actor.

Memperbarui critic dengan target yang sama yang dipakai actor. Penyebabnya, gradien critic ikut mengalir ke actor atau sebaliknya, sehingga kedua jaringan saling memengaruhi. Akibatnya, pelatihan tidak stabil karena target dan prediksi saling bergeser. Perbaikannya, putuskan gradien antara actor dan critic dengan memakai with torch.no_grad() saat menghitung nilai critic untuk advantage.

Ringkasan#

  • Actor-critic mengganti return kumulatif REINFORCE dengan estimasi nilai dari critic, sehingga actor diperbarui setiap langkah dengan advantage At=rt+1+γVϕ(st+1)−Vϕ(st)A_t = r_{t+1} + \gamma V_\phi(s_{t+1}) - V_\phi(s_t).
  • Critic dilatih dengan target TD(0) yang diperkenalkan pada topik temporal-difference, sehingga pasangan actor-critic bersifat on-policy dan varians pembaruan jauh lebih rendah.
  • Pada CartPole-v1 selama 500 langkah, return rata-rata lima episode terakhir 18,2000 dengan norma gradien 1,9610, jauh lebih kecil daripada ribuan pada REINFORCE di topik sebelumnya.

Referensi#