Optimizer adalah komponen yang mengubah gradien menjadi pembaruan parameter — zero_grad(), step(), dan lr yang selama ini kita pakai di chapter Training Loop berasal dari kelas torch.optim.SGD. SGD polos sebenarnya cukup untuk melatih banyak model, tetapi satu tambahan argumen bernama momentum sering mengubah nasib training: gradien yang konsisten arah mendapatkan langkah yang membesar, gradien yang berganti-ganti arah saling meniadakan. Chapter ini membedah aturan pembaruan SGD beserta versi bermomentum, lalu mengadu keduanya pada kondisi yang disamakan penuh — dan mengintip isi optimizer.state untuk melihat akumulasi kecepatan itu sendiri.

SGD di torch.optim #

SGD (stochastic gradient descent) menjalankan aturan paling sederhana dari chapter Gradient Descent : parameter digeser searah kebalikan gradien, sepanjang lr kali besar gradien. Konstruksinya dua baris:

python
optimizer = torch.optim.SGD(model.parameters(), lr=0.05)

Kata stochastic merujuk pada sumber keacakannya: gradien dihitung dari mini-batch, bukan dari seluruh data, sehingga tiap iterasi membawa perkiraan yang bergejolak. Argumen momentum menambahkan memori arah pada aturan tersebut.

Momentum: Kecepatan yang Terakumulasi #

Bayangkan bola yang menggelinding menuruni lembah. Bola tanpa momentum berhenti dan bergerak ulang setiap saat — arahnya sepenuhnya ditentukan kemiringan di titik itu. Bola bermomentum menyimpan kecepatan: kemiringan yang konsisten mendorong bola semakin cepat, sedangkan kemiringan yang berganti arah (getaran mini-batch) hanya menggoyangkan bola tanpa membalik arahnya. Secara matematis, SGD bermomentum menambah variabel kecepatan v per parameter:

python
v = mu * v + grad   # kecepatan lama dipertahankan sebagian, gradien menambah
w = w - lr * v      # parameter digeser searah kecepatan, bukan gradien mentah

Koefisien mu (biasanya 0.9) berarti sembilan per sepuluh kecepatan sebelumnya bertahan pada setiap langkah. Gradien yang terus berarah sama terakumulasi hingga kira-kira 1/(1-mu) = 10 kali lipat; gradien yang berayun positif-negatif saling menghapus. Analogi bola memiliki batas: momentum di training tidak punya gesekan dan tidak pernah benar-benar berhenti.

Contoh: SGD Polos Melawan SGD Momentum #

Berkas sgd_momentum.py mengadukan dua konfigurasi pada kondisi identik: make_moons 400 sampel, MLP [2, 16, 2], batch 32, lr=0.05, 30 epoch, bobot awal sama. Bagian kedua skrip lalu membuka optimizer.state pada model kecil untuk mengamati akumulasi kecepatan selama empat langkah:

python
"""SGD dengan momentum: API torch.optim.SGD dan efek nyatanya.

Bagian 1: perlombaan SGD vs SGD + momentum 0.9 pada make_moons dengan
learning rate, batch, epoch, dan inisialisasi bobot yang identik.
Bagian 2: mengintip momentum_buffer di optimizer.state untuk melihat
akumulasi kecepatan. Semua contoh diuji pada CPU (torch 2.14.1+cpu).
"""

import torch
import torch.nn as nn
from sklearn.datasets import make_moons
from torch.utils.data import DataLoader, TensorDataset

torch.manual_seed(42)

# ---------- 1. Dataset dan komponen yang disamakan ----------
X, y = make_moons(n_samples=400, noise=0.20, random_state=42)
X_t = torch.tensor(X, dtype=torch.float32)
y_t = torch.tensor(y, dtype=torch.long)
dataset = TensorDataset(X_t, y_t)

JUMLAH_EPOCH = 30


def buat_model():
    """Model dengan inisialisasi bobot identik untuk tiap optimizer."""
    torch.manual_seed(7)
    return nn.Sequential(
        nn.Linear(2, 16),
        nn.ReLU(),
        nn.Linear(16, 2),
    )


def latih(momentum):
    """Latih dengan SGD; momentum=0.0 berarti SGD polos."""
    model = buat_model()
    loss_fn = nn.CrossEntropyLoss()
    optimizer = torch.optim.SGD(model.parameters(), lr=0.05, momentum=momentum)
    loader = DataLoader(
        dataset,
        batch_size=32,
        shuffle=True,
        generator=torch.Generator().manual_seed(42),
    )
    for epoch in range(1, JUMLAH_EPOCH + 1):
        total_loss = 0.0
        for X_batch, y_batch in loader:
            optimizer.zero_grad()
            loss = loss_fn(model(X_batch), y_batch)
            loss.backward()
            optimizer.step()
            total_loss += loss.item()
        if epoch in (1, 5, 10, 20, 30):
            print(f"  epoch {epoch:2d} | loss = {total_loss / len(loader):.4f}")
    with torch.no_grad():
        akurasi = (model(X_t).argmax(dim=1) == y_t).float().mean().item()
    print(f"  akurasi akhir: {akurasi:.3f}")


print("===== PERLOMBANAN: SGD vs SGD + momentum =====")
print("data: make_moons 400 sampel | MLP [2, 16, 2] | batch 32 | lr 0.05")
print()
print("--- SGD polos (momentum=0.0) ---")
latih(0.0)
print()
print("--- SGD momentum=0.9 ---")
latih(0.9)

# ---------- 2. Mengintip momentum_buffer: akumulasi kecepatan ----------
print()
print("===== ISI optimizer.state: momentum_buffer =====")
torch.manual_seed(7)
model_kecil = nn.Linear(4, 3)
optimizer = torch.optim.SGD(model_kecil.parameters(), lr=0.1, momentum=0.9)
x = torch.randn(8, 4)
target = torch.randn(8, 3) * 0.1
loss_fn = nn.MSELoss()


def norma(array_2d):
    return sum((p**2).sum().item() for p in array_2d) ** 0.5


for langkah in (1, 2, 3, 4):
    optimizer.zero_grad()
    loss = loss_fn(model_kecil(x), target)
    loss.backward()
    norma_grad = norma([p.grad for p in model_kecil.parameters()])
    optimizer.step()
    norma_buffer = norma(
        [optimizer.state[p]["momentum_buffer"] for p in model_kecil.parameters()]
    )
    print(f"langkah {langkah} | norma gradien {norma_grad:.4f} "
          f"| norma momentum_buffer {norma_buffer:.4f}")
print("buffer langkah 1 = gradien; setelah itu buffer = 0.9*buffer + gradien")

Output yang Dihasilkan:

text
===== PERLOMBANAN: SGD vs SGD + momentum =====
data: make_moons 400 sampel | MLP [2, 16, 2] | batch 32 | lr 0.05

--- SGD polos (momentum=0.0) ---
  epoch  1 | loss = 0.6006
  epoch  5 | loss = 0.4384
  epoch 10 | loss = 0.3675
  epoch 20 | loss = 0.3155
  epoch 30 | loss = 0.3081
  akurasi akhir: 0.840

--- SGD momentum=0.9 ---
  epoch  1 | loss = 0.5377
  epoch  5 | loss = 0.2916
  epoch 10 | loss = 0.2671
  epoch 20 | loss = 0.1661
  epoch 30 | loss = 0.1136
  akurasi akhir: 0.960

===== ISI optimizer.state: momentum_buffer =====
langkah 1 | norma gradien 0.5745 | norma momentum_buffer 0.5745
langkah 2 | norma gradien 0.5126 | norma momentum_buffer 1.0293
langkah 3 | norma gradien 0.4058 | norma momentum_buffer 1.3299
langkah 4 | norma gradien 0.2807 | norma momentum_buffer 1.4659
buffer langkah 1 = gradien; setelah itu buffer = 0.9*buffer + gradien

Contoh di atas diuji nyata dengan PyTorch 2.14.1+cpu pada CPU. Mari kita telaah kode di atas baris per baris:

  1. Baris 17-20 (data): 400 sampel make_moons dibuat tensor sekali dan dipakai kedua perlombaan.
  2. Baris 25-33 (buat_model): seed 7 di dalam fungsi menjamin SGD polos dan SGD bermomentum memulai dari bobot identik — perbedaan hasil murni efek momentum.
  3. Baris 35-45 (konfigurasi optimizer): satu-satunya perbedaan ada pada argumen momentum: 0.0 berarti SGD polos (variabel kecepatan tidak dipakai), 0.9 adalah nilai lazim industri.
  4. Baris 46-58 (pelatihan): siklus lima langkah standar — zero_grad, forward, loss, backward, step; step() yang sama kini menjalankan aturan berkecepatan di balik layar.
  5. Baris 73-78 (model pengintip): nn.Linear(4, 3) dengan target buatan dijalankan empat langkah penuh — cukup kecil untuk dibedah state-nya.
  6. Baris 85-95 (membaca optimizer.state): setelah step(), PyTorch menyimpan tensor momentum_buffer per parameter; normanya dicetak berdampingan dengan norma gradien pada langkah yang sama.

Hasil perlombaan tampak jelas. SGD polos turun dari 0.6006 ke 0.3081 dengan akurasi 0.840; SGD momentum 0.9 mencapai 0.1136 dengan akurasi 0.960 pada epoch yang sama — hampir tiga kali lebih dalam turunannya. Bagian kedua memperlihatkan mekanismenya: norma gradien justru mengecil dari 0.5745 ke 0.2807 sepanjang empat langkah (model makin mendekati solusi), tetapi norma momentum_buffer terus membesar 0.5745 → 1.0293 → 1.3299 → 1.4659 — kecepatan yang terakumulasi karena gradien-gradien sebelumnya searah. Pada langkah pertama keduanya sama persis, bukti bahwa buffer memang diinisialisasi dari gradien itu sendiri.

Dokumentasi resmi seluruh argumen torch.optim.SGD — termasuk momentum, weight_decay, dan nesterov — tersedia di docs.pytorch.org/docs/stable/generated/torch.optim.SGD.html , sedangkan ikhtisar seluruh optimizer di docs.pytorch.org/docs/stable/optim.html . Momentum memperbaiki ritme langkah, tetapi panjang langkah masih seragam untuk seluruh parameter — padahal ada parameter yang gradiennya stabil dan ada yang gradiennya nyaris nol. Optimizer berikutnya, Adam, memberi setiap parameter learning rate yang menyesuaikan diri.