Semua algoritma sejauh ini belajar nilai, lalu menurunkan kebijakan dari nilai. Ada cabang kedua yang menempuh jalan lebih langsung: tulis kebijakan sebagai fungsi berparameter — jaringan neural — dan optimalkan parameternya agar return yang diharapkan membesar. Inilah keluarga policy gradient, dan anggota paling tuanya bernama REINFORCE. Kita akhirnya mempertemukan dua dunia course ini: kerangka RL dari section ini, dan gradient ascent yang kita dalami sejak chapter Gradient Descent . Pada chapter ini kita merumuskan kebijakan sebagai distribusi softmax, membaca rumus REINFORCE dengan intuisi, lalu melatih implementasi PyTorch mini pada dunia 1D dan melihat probabilitas aksi yang benar merayap naik menuju 1.

Kebijakan sebagai Distribusi #

Kebijakan tabular chapter sebelumnya bersifat pasti: satu state, satu aksi. Kebijakan berparameter justru berupa distribusi π_θ(a|s) — pada state s, setiap aksi menerima probabilitas. Distribusi itu dibangun dari skor jaringan melalui softmax: skor tertinggi menerima probabilitas terbesar, tetapi aksi lain tetap menyisakan peluang. Dua keuntungan mengikuti bentuk ini. Aksi acak menyatu di dalam kebijakan, sehingga eksplorasi tidak perlu ditempelkan dari luar; dan seluruh konstruksi — layer, softmax, sampling — dapat dibediferensiasi, sehingga gradien return terhadap parameter θ dapat dihitung dengan autograd, alat yang sama dengan chapter Automatic Differentiation .

Rumus REINFORCE #

Untuk satu episode, penaksir gradien yang dipakai REINFORCE berbunyi:

∇J ≈ mean_t [ ∇log π(a_t|s_t) · G_t ]

Bacalah sebagai klasifikasi berbobot: setiap aksi yang diambil adalah “kelas” yang diprediksi, dan bobotnya adalah return G_t yang menyusul aksi itu. Aksi yang diikuti return tinggi mendapat log π-nya didorong naik; aksi yang diikuti return rendah justru didorong turun. Arah penyetelan sama persis dengan supervised learning — hanya label dan bobotnya kini datang dari konsekuensi, bukan dari anotasi. Satu masalah nyata muncul dari bentuk ini: G_t berubah-ubah lebar antar episode, sehingga gradien berisik dan training bergoyang. Penawar pertama adalah baseline — mengurangi nilai acuan (misalnya rata-rata return) sebelum mengalikan, sehingga yang tersisa adalah seberapa lebih baik suatu aksi dari kebiasaan; pada demo kita memakai standardisasi return sebagai baseline sederhana.

Demo: REINFORCE Mini dengan PyTorch #

Dunianya dunia 1D 5 sel dari chapter Dasar Reinforcement Learning : mulai sel 0, target sel 4, reward -0.2 per langkah dan +1 saat tiba. Kebijakan adalah MLP kecil 1 → 32 → 2 yang menerima posisi ternormalisasi dan menghasilkan dua logits (kiri, kanan). Berkas reinforce_mini.py menjalankan 400 episode; satu episode dijalankan penuh dulu, return dihitung dari belakang, lalu loss −mean(log π × G) digerakkan mundur:

python
"""REINFORCE mini: policy gradient pada GridWorld 1D 5 sel (PyTorch).

Policy adalah MLP kecil (1 -> 32 -> 2) yang menerima posisi ternormalisasi
dan menghasilkan logits untuk aksi "kiri" dan "kanan". Satu episode
dijalankan penuh, return diskonto dihitung untuk setiap langkah, lalu
loss = -mean(log pi(a|s) x G_t) digerakkan mundur lewat autograd.
"""

import torch
import torch.nn as nn

torch.manual_seed(23)

JUMLAH_SEL = 5
MAKS_LANGKAH = 25
GAMMA = 0.99


def step(posisi, aksi):
    """Satu interaksi dunia 1D; kembalikan (posisi, reward, selesai)."""
    if aksi == 1:
        posisi = min(posisi + 1, JUMLAH_SEL - 1)
    else:
        posisi = max(posisi - 1, 0)
    selesai = posisi == JUMLAH_SEL - 1
    reward = 1.0 if selesai else -0.2
    return posisi, reward, selesai


policy = nn.Sequential(
    nn.Linear(1, 32),
    nn.Tanh(),
    nn.Linear(32, 2),          # 2 logits: kiri, kanan
)
optimizer = torch.optim.Adam(policy.parameters(), lr=0.01)


def prob_kanan(policy_net, posisi):
    """Probabilitas aksi "kanan" pada suatu posisi (untuk laporan)."""
    with torch.no_grad():
        logits = policy_net(torch.tensor([[posisi / (JUMLAH_SEL - 1)]]))
    return torch.softmax(logits, dim=1)[0, 1].item()


def jalankan_episode(policy_net):
    """Kumpulkan satu episode; kembalikan (log_prob, reward, total)."""
    posisi, log_probs, rewards = 0, [], []
    for _ in range(MAKS_LANGKAH):
        logits = policy_net(torch.tensor([[posisi / (JUMLAH_SEL - 1)]]))
        distribusi = torch.distributions.Categorical(logits=logits)
        aksi = distribusi.sample()
        log_probs.append(distribusi.log_prob(aksi))
        posisi, reward, selesai = step(posisi, int(aksi.item()))
        rewards.append(reward)
        if selesai:
            break
    return log_probs, rewards, sum(rewards)


print("pi(kanan | posisi) sebelum latih:")
print("  " + "  ".join(f"s={s}: {prob_kanan(policy, s):.2f}"
                        for s in range(4)))

riwayat, riwayat_loss = [], []
for episode in range(1, 401):
    log_probs, rewards, total = jalankan_episode(policy)
    G, returns = 0.0, []
    for r in reversed(rewards):                # hitung return dari belakang
        G = r + GAMMA * G
        returns.append(G)
    returns = torch.tensor(list(reversed(returns)))
    standar = (returns - returns.mean()) / (returns.std() + 1e-8)
    loss = torch.stack([-lp * g for lp, g in zip(log_probs, standar)]).mean()
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    riwayat.append(total)
    riwayat_loss.append(loss.item())

print("\nMean total reward per blok 50 episode:")
for i in range(0, 400, 50):
    print(f"  episode {i + 1:3d}-{i + 50:3d}: "
          f"{torch.tensor(riwayat[i:i + 50]).mean():+.3f}")

print("\npi(kanan | posisi) setelah latih:")
print("  " + "  ".join(f"s={s}: {prob_kanan(policy, s):.2f}"
                        for s in range(4)))

print(f"\nLoss episode 1   : {riwayat_loss[0]:+.4f}")
print(f"Loss episode 400 : {riwayat_loss[-1]:+.4f}")
print(f"Rata-rata |loss| 50 episode terakhir: "
      f"{sum(abs(l) for l in riwayat_loss[-50:]) / 50:.4f}")

Output yang Dihasilkan:

text
pi(kanan | posisi) sebelum latih:
  s=0: 0.37  s=1: 0.36  s=2: 0.37  s=3: 0.37

Mean total reward per blok 50 episode:
  episode   1- 50: +0.024
  episode  51-100: +0.388
  episode 101-150: +0.388
  episode 151-200: +0.400
  episode 201-250: +0.400
  episode 251-300: +0.400
  episode 301-350: +0.400
  episode 351-400: +0.392

pi(kanan | posisi) setelah latih:
  s=0: 1.00  s=1: 1.00  s=2: 1.00  s=3: 1.00

Loss episode 1   : +0.0895
Loss episode 400 : -0.0007
Rata-rata |loss| 50 episode terakhir: 0.0390

Dua sinyal pada keluaran menunjukkan pembelajaran yang sehat. Probabilitas π(kanan|s) sebelum latih hampir rata di 0.36-0.37 — kebijakan awal nyaris melempar koin — dan setelah 400 episode menjadi 1.00 di seluruh posisi: jaringan menyimpulkan bahwa “kanan” selalu menjadi aksi terbaik di dunia ini. Mean total reward per blok mengikuti: dari +0.024 pada 50 episode pertama naik ke +0.388 lalu menempel di +0.400 — nilai optimal dunia (empat langkah: -0.2×3 + 1). Loss episode 1 sebesar +0.0895 merosot ke -0.0007 seiring log-probabilitas aksi terpilih menyatu. Mari kita telaah kode di atas baris per baris:

  1. Baris 12: torch.manual_seed(23) — sampling aksi dari Categorical kini dapat direproduksi; seluruh angka demo identik di setiap eksekusi.
  2. Baris 19-27 (step): dunia 1D — min/max menjaga posisi tetap di papan, reward -0.2, +1 di target.
  3. Baris 30-35: kebijakan sebagai nn.Sequential dua layer; optimizer Adam bertugas menyetel parameter — perannya dibahas pada chapter Loss dan Optimizer .
  4. Baris 38-42 (prob_kanan): pembaca laporan — softmax atas logits, dihitung tanpa gradien karena hanya untuk dicetak.
  5. Baris 45-57 (jalankan_episode): menjalankan satu episode penuh; Categorical(logits=...) (distributions PyTorch ) menyediakan sample() untuk memilih aksi dan log_prob() untuk menyimpan log π(a|s) tiap langkah.
  6. Baris 67-71: return dihitung dari belakang dengan G = r + γ·G — pola yang sama dengan hitungan manual chapter Markov Decision Process.
  7. Baris 72: standardisasi return (G − mean)/std berperan sebagai baseline, mengecilkan varians gradien.
  8. Baris 73-76: loss −mean(log π × G); optimizer.zero_grad(), loss.backward(), optimizer.step() — siklus training standar PyTorch (torch.optim ).
  9. Baris 80-92: laporan akhir — mean reward per blok, probabilitas kanan setelah latih, dan statistik loss.

Kekurangan yang Membentuk Chapter Berikutnya #

REINFORCE menunggu episode selesai sebelum satu pembaruan dilakukan, lalu menilai seluruh rangkaian aksi dengan satu angka return yang sama besar. Konsekuensinya: gradien berisik dan episode panjang berbiaya mahal. Ada jalan lain memakai jaringan di dunia besar — bukan menaksir kebijakan, melainkan menaksir Q(s,a) seperti Q-learning chapter sebelumnya, kini dengan input state yang terenkoder. Perpaduan jaringan dan tabel Q itulah Deep Q-Network, topik chapter berikutnya.