Dua chapter sebelumnya membedah dua ujung spektrum: SGD polos yang sederhana dan Adam yang penuh mekanisme. Sekarang kita menarik mundur dan memandang keduanya sebagai bagian dari satu riwayat inovasi yang panjang. Setiap optimizer baru muncul karena kelemahan nyata pada generasi sebelumnya: SGD tersendat di lembah berbentuk garis, AdaGrad melambat permanen, RMSProp membuang ingatan panjangnya, dan Adam menyatukan semuanya. Chapter ini memetakan keluarga besar optimizer, menuliskan intuisi rumus tiap generasi, lalu mengujikan lima varian pada masalah klasifikasi yang sama persis.

Lima Generasi dalam Satu Peta #

Alur evolusi berikut dirangkum dalam satu diagram; setiap panah menandai masalah yang diselesaikan generasi berikutnya:

Tabel berikut merangkum ide inti tiap generasi beserta tahun publikasinya:

OptimizerIde intiTahun
SGDikuti gradien batch saat ini1951
SGD + momentumakumulasi arah beberapa langkah (velocity)1964
Nesterov momentumhitung gradien sedikit di depan posisi sekarang1983
AdaGradlearning rate per parameter dari akumulasi kuadrat gradien2011
RMSPropakumulasi kuadrat diberi peluruhan, ingatan terbatas2012
Adammomentum + RMSProp + koreksi bias langkah awal2014
AdamWweight decay dipisahkan dari penskalaan adaptif2017

Intuisi Rumus Tiap Generasi #

Kita baca rumusnya sebagai perilaku, bukan hafalan simbol. SGD menggeser bobot searah kebalikan gradien: w ← w − lr·g. Momentum menambahkan kecepatan v ← μ·v + g lalu w ← w − lr·v: komponen gradien yang konsisten antar langkah saling menumpuk, komponen yang bergoyang saling meniadikan — inilah alasan momentum cepat melintas lembah berbentuk garis. AdaGrad mengganti penyebut seragam dengan akumulasi G ← G + g², sehingga parameter yang gradiennya besar menerima langkah kecil dan yang gradiennya kecil menerima langkah besar. Karena G hanya bertambah, learning rate efektif menyusut terus sampai training berhenti — kelemahan yang diperbaiki RMSProp dengan rata-rata bergerak G ← ρ·G + (1−ρ)·g² yang melupakan gradien lama. Adam menggabungkan keduanya: momentum pada pembilang, RMSProp pada penyebut, ditambah koreksi bias agar langkah awal tidak mengecil karena statistik masih kosong. AdamW memindahkan weight decay keluar dari penyebut adaptif sehingga regularisasi berlaku sama kuat untuk seluruh parameter.

Lima Optimizer pada Satu Masalah #

Teori terasa abstrak sampai kelima varian diadu di lapangan. Berkas perbandingan_optimizer.py melatih MLP [2, 32, 2] pada dataset make_moons 400 sampel, full-batch, 30 epoch. Kuncinya ada pada fungsi buat_model() yang me-reset seed sebelum membangun model, sehingga kelima percobaan berangkat dari bobot awal yang identik — perbedaan hasil murni datang dari strategi optimizer:

python
"""Membandingkan lima varian optimizer pada masalah klasifikasi yang sama.

Dataset make_moons 400 sampel, MLP [2, 32, 2] dengan aktivasi Tanh,
full-batch gradient descent, 30 epoch. Bobot awal setiap percobaan
di-reset ke nilai identik lewat seed agar perbedaan hasil murni
datang dari strategi optimizer, bukan dari keberuntungan inisialisasi.
"""

import torch
import torch.nn as nn
from sklearn.datasets import make_moons

torch.manual_seed(42)

# ---------- 1. Data ----------
X, y = make_moons(n_samples=400, noise=0.25, random_state=42)
X_t = torch.tensor(X, dtype=torch.float32)
y_t = torch.tensor(y, dtype=torch.long)


# ---------- 2. Model dengan bobot awal identik ----------
def buat_model():
    torch.manual_seed(123)  # reset seed -> bobot awal selalu sama
    return nn.Sequential(
        nn.Linear(2, 32),
        nn.Tanh(),
        nn.Linear(32, 2),
    )


# ---------- 3. Konfigurasi optimizer yang dibandingkan ----------
optimizers = {
    "SGD":           dict(kelas=torch.optim.SGD,     lr=0.5),
    "SGD+momentum":  dict(kelas=torch.optim.SGD,     lr=0.5,  momentum=0.9),
    "AdaGrad":       dict(kelas=torch.optim.Adagrad, lr=0.5),
    "RMSProp":       dict(kelas=torch.optim.RMSprop, lr=0.05),
    "Adam":          dict(kelas=torch.optim.Adam,    lr=0.05),
}

criterion = nn.CrossEntropyLoss()
JUMLAH_EPOCH = 30

print("dataset : make_moons 400 sampel | full-batch | 30 epoch")
print("model   : [2, 32, 2] Tanh | bobot awal identik tiap percobaan")
print()
print(f"{'optimizer':14s} {'loss ep 1':>10s} {'loss ep 10':>11s} "
      f"{'loss ep 30':>11s} {'akurasi':>8s}")
print("-" * 60)

for nama, cfg in optimizers.items():
    model = buat_model()
    optimizer = cfg["kelas"](model.parameters(), **{
        k: v for k, v in cfg.items() if k != "kelas"
    })
    for epoch in range(1, JUMLAH_EPOCH + 1):
        optimizer.zero_grad()
        loss = criterion(model(X_t), y_t)
        loss.backward()
        optimizer.step()
        if epoch == 1:
            loss_awal = loss.item()
        elif epoch == 10:
            loss_tengah = loss.item()
    with torch.no_grad():
        akurasi = (model(X_t).argmax(dim=1) == y_t).float().mean().item()
    print(f"{nama:14s} {loss_awal:10.4f} {loss_tengah:11.4f} "
          f"{loss.item():11.4f} {akurasi:8.3f}")

Output yang Dihasilkan:

text
dataset : make_moons 400 sampel | full-batch | 30 epoch
model   : [2, 32, 2] Tanh | bobot awal identik tiap percobaan

optimizer       loss ep 1  loss ep 10  loss ep 30  akurasi
------------------------------------------------------------
SGD                0.5528      0.3462      0.3328    0.840
SGD+momentum       0.5528      0.3764      0.2797    0.890
AdaGrad            0.5528      0.3427      0.2627    0.890
RMSProp            0.5528      0.3419      0.2670    0.887
Adam               0.5528      0.3600      0.2285    0.910

Mari kita telaah kode di atas baris per baris:

  1. Baris 16-18 (data): make_moons dengan random_state=42 menghasilkan dataset yang sama untuk kelima percobaan — satu variabel pengganggu sudah dihilangkan sejak awal.
  2. Baris 22-28 (buat_model): torch.manual_seed(123) dipanggil di dalam fungsi, jadi setiap pemanggilan membangun model dari urutan bilangan acak yang sama. Kolom “loss ep 1” yang seragam 0,5528 pada keluaran menjadi buktinya: semua optimizer berangkat dari titik yang identik.
  3. Baris 32-38 (konfigurasi): learning rate sengaja tidak diseragamkan. SGD dan AdaGrad menerima lr=0.5, sedangkan RMSProp dan Adam menerima lr=0.05 — skala efektif tiap keluarga berbeda karena penyebutnya sudah menyetaraakan besaran gradien.
  4. Baris 52-54 (konstruksi optimizer): kamus dikupas dengan comprehension yang membuang kunci kelas, sisanya (lr, momentum) diteruskan sebagai argumen bernama ke konstruktor optimizer.
  5. Baris 55-63 (siklus training): pola zero_grad → forward → loss → backward → step persis seperti cetakan Training Loop ; loss.item() disalin pada epoch 1 dan 10 sebagai titik banding.
  6. Baris 64-67 (evaluasi): prediksi akhir dihitung tanpa graf gradien, dan loss.item() pada baris cetak otomatis memakai loss epoch terakhir (epoch 30).

Hasilnya jelas berjenjang: SGD polos tertinggal di 0,3328, momentum dan keluarga adaptif memeras loss ke rentang 0,23–0,28, dan Adam memimpin dengan 0,2285 serta akurasi 0,910. Perlu dicatat bahwa pada epoch 10 SGD polos sempat unggul — optimizer adaptif menang karena penyetaraan skala per parameter yang bekerja pada horizon panjang, bukan karena setiap langkahnya lebih cepat.

Adaptif Cepat, Generalisasi Belum Tentu #

Kemenangan Adam di atas jangan dibaca sebagai vonis. Studi klasik Wilson dkk. (2017) menunjukkan bahwa metode adaptif sering konvergen lebih cepat pada data latih namun berakhir pada solusi yang generalisasinya sedikit lebih buruk dibanding SGD+momentum pada beberapa tugas cita. Praktik industri hari ini memilih sesuai domain: SGD+momentum masih dipakai untuk pelatihan CNN besar satu domain, sementara AdamW menjadi standar nyaris mutlak untuk transformer dan tugas bahasa karena stabilitasnya pada gradien berskala tidak seragam.

Daftar lengkap optimizer PyTorch beserta parameter standarnya tercantum di docs.pytorch.org/docs/stable/optim.html . Optimizer menentukan arah dan besaran langkah, tetapi besaran itu sendiri — learning rate — paling produktif bila diubah sepanjang usia training. Chapter berikutnya membahas jadwal perubahan tersebut: dari step decay sampai cosine annealing.