Chapter Learning Rate menunjukkan satu angka yang salah dapat membuat training merayap atau berosilasi. Namun angka terbaik itu sendiri berubah seiring usia training: awalnya kita butuh langkah besar untuk bergerak cepat, di tengah jalan langkah sedang untuk menjelajahi lereng, dan di akhir langkah kecil untuk berpijak presisi pada dasar minimum. Learning rate scheduling mengotomatiskan perubahan tersebut lewat objek scheduler PyTorch yang menimpa learning rate optimizer pada titik waktu yang kita tentukan. Chapter ini membahas tiga scheduler paling sering dipakai, posisi pemanggilan scheduler.step() yang benar, dan jebakan paling umum: lupa memanggilnya sama sekali.

Mengapa Learning Rate Perlu Diatur Ulang #

Tiga pola waktu menguasai praktik modern. Pertama, warm-up: memulai dari learning rate kecil lalu menaikkannya selama beberapa epoch awal, karena gradien pada bobot acak masih liar dan langkah besar di fase ini mudah melempar training ke wilayah yang sulit dipulihkan. Kedua, decay bertahap: menurunkan learning rate saat loss mulai mendatar agar parameter berpindah dalam langkah halus, tidak melompati minimum. Ketiga, cosine annealing: menurunkan learning rate mengikuti setengah kurva kosinus — mulai landai, curam di tengah, lalu melandai lagi mendekati nol — sehingga penurunan terasa mulus tanpa perlu memilih titik potong manual seperti pada step decay.

Tabel berikut memetakan scheduler yang tersedia di modul torch.optim.lr_scheduler:

JadwalPola lrKapan dipakai
StepLRturun dikali gamma tiap step_size epochbaseline sederhana, mudah dibaca
MultiStepLRturun di daftar epoch tertentumengikuti jadwal manual (mis. ep 30/60/90)
ExponentialLRdikali gamma tiap epochdecay agresif dan monoton
CosineAnnealingLRkurva kosinus menuju eta_minpelatihan panjang tanpa titik potong manual
OneCycleLRnaik ke max_lr lalu cosine turunsatu siklus penuh, sering tercepat konvergen
warm-up + cosinenaik singkat lalu cosine turuntransformer dan model besar

Tiga Scheduler dalam Praktik #

Berkas jadwal_learning_rate.py tidak melatih model sungguhan; skrip ini memutar kerangka training 16 epoch sambil mencatat nilai learning rate yang benar-benar dipakai — cara tercepat memahami perilaku tiap scheduler tanpa derau loss. Bagian pertama membandingkan StepLR dan CosineAnnealingLR, bagian kedua membedah OneCycleLR per batch, dan bagian ketiga menyimpan skenario kelalaian:

python
"""Mengamati nilai learning rate yang dipakai tiap epoch untuk tiga scheduler.

Bagian 1: StepLR dan CosineAnnealingLR — lr dicatat di awal epoch
(lr yang benar-benar dipakai), lalu scheduler.step() dipanggil satu
kali per epoch SETELAH optimizer.step().
Bagian 2: OneCycleLR — scheduler.step() dipanggil per batch; lr naik
saat pemanasan, menyentuh puncak, lalu melandai menuju nilai akhir.
Bagian 3: demonstrasi lupa scheduler.step() — lr tidak pernah berubah.
"""

import torch
import torch.nn as nn

torch.manual_seed(42)


def buat_model():
    torch.manual_seed(7)
    return nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 3))


EPOCH = 16

# ---------- 1. Dua scheduler per-epoch ----------
model_step = buat_model()
opt_step = torch.optim.SGD(model_step.parameters(), lr=0.1)
sch_step = torch.optim.lr_scheduler.StepLR(opt_step, step_size=4, gamma=0.5)

model_cos = buat_model()
opt_cos = torch.optim.SGD(model_cos.parameters(), lr=0.1)
sch_cos = torch.optim.lr_scheduler.CosineAnnealingLR(
    opt_cos, T_max=EPOCH, eta_min=0.001
)

lr_step, lr_cos = [], []
for epoch in range(EPOCH):
    lr_step.append(opt_step.param_groups[0]["lr"])   # lr epoch ini
    lr_cos.append(opt_cos.param_groups[0]["lr"])
    # ... training loop sesungguhnya berjalan di sini ...
    opt_step.step()          # 1) optimizer memakai lr epoch ini
    opt_cos.step()
    sch_step.step()          # 2) scheduler mengatur lr epoch berikutnya
    sch_cos.step()

print(f"{'epoch':>5s} {'StepLR':>10s} {'Cosine':>10s}")
for e in range(EPOCH):
    if e < 6 or e == EPOCH - 1:
        print(f"{e + 1:5d} {lr_step[e]:10.4f} {lr_cos[e]:10.4f}")
print(f"setelah epoch {EPOCH}: lr StepLR = "
      f"{opt_step.param_groups[0]['lr']:.4f} | lr Cosine = "
      f"{opt_cos.param_groups[0]['lr']:.4f}")

# ---------- 2. OneCycleLR: scheduler per-batch ----------
BATCH_PER_EPOCH = 4
model_one = buat_model()
opt_one = torch.optim.SGD(model_one.parameters(), lr=0.002)
sch_one = torch.optim.lr_scheduler.OneCycleLR(
    opt_one, max_lr=0.1, epochs=EPOCH,
    steps_per_epoch=BATCH_PER_EPOCH,
)

lr_one = []
for epoch in range(EPOCH):
    for batch in range(BATCH_PER_EPOCH):
        lr_one.append(opt_one.param_groups[0]["lr"])  # lr langkah ini
        # ... satu batch training berlangsung di sini ...
        opt_one.step()
        sch_one.step()        # per batch, bukan per epoch

print()
print("OneCycleLR (64 langkah, 16 epoch x 4 batch):")
for langkah in [0, 15, 19, 31, 47]:
    print(f"  langkah {langkah + 1:2d}: lr = {lr_one[langkah]:.4f}")
print(f"  langkah 64 (terakhir): lr = {lr_one[63]:.7f}")

# ---------- 3. Lupa memanggil scheduler.step() ----------
model_lupa = buat_model()
opt_lupa = torch.optim.SGD(model_lupa.parameters(), lr=0.1)
sch_lupa = torch.optim.lr_scheduler.StepLR(opt_lupa, step_size=4, gamma=0.5)
for epoch in range(EPOCH):
    opt_lupa.step()           # optimizer jalan ...
    # ... tapi sch_lupa.step() tidak pernah dipanggil
print()
print(f"lr setelah {EPOCH} epoch TANPA scheduler.step(): "
      f"{opt_lupa.param_groups[0]['lr']:.4f}")

Output yang Dihasilkan:

text
epoch     StepLR     Cosine
    1     0.1000     0.1000
    2     0.1000     0.0990
    3     0.1000     0.0962
    4     0.1000     0.0917
    5     0.0500     0.0855
    6     0.0500     0.0780
   16     0.0125     0.0020
setelah epoch 16: lr StepLR = 0.0063 | lr Cosine = 0.0010

OneCycleLR (64 langkah, 16 epoch x 4 batch):
  langkah  1: lr = 0.0040
  langkah 16: lr = 0.0929
  langkah 20: lr = 0.0999
  langkah 32: lr = 0.0812
  langkah 48: lr = 0.0283
  langkah 64 (terakhir): lr = 0.0000004

lr setelah 16 epoch TANPA scheduler.step(): 0.1000

Mari kita telaah kode di atas baris per baris:

  1. Baris 27 (StepLR): step_size=4, gamma=0.5 berarti learning rate menjadi setengah setiap 4 epoch — terlihat pada keluaran: epoch 1-4 bernilai 0,1000, epoch 5-6 bernilai 0,0500, dan seterusnya hingga 0,0125 pada epoch 16.
  2. Baris 31-33 (CosineAnnealingLR): T_max=16 menempatkan dasar kurva kosinus tepat di epoch ke-16 dengan lantai eta_min=0.001. Nilainya turun mulus 0,0990 → 0,0962 → 0,0917 — kontras dengan penurunan bertangga milik StepLR.
  3. Baris 37-38 (pencatatan lr): nilai dicatat di awal epoch, sebelum optimizer.step() — inilah learning rate yang benar-benar dipakai epoch tersebut, bukan nilai yang sudah ditimpa scheduler.
  4. Baris 40-43 (urutan step): optimizer melangkah lebih dulu, scheduler menyusul. Scheduler yang dipanggil sebelum optimizer.step() akan membuat optimizer memakai learning rate epoch berikutnya — meleset satu epoch sejak awal.
  5. Baris 57-60 (OneCycleLR): scheduler ini merencanakan seluruh perjalanan naik-turun dalam satu kurva; karena steps_per_epoch=4, kurvanya terbagi 64 langkah, dan scheduler.step() wajib dipanggil per batch (baris 68), bukan per epoch.
  6. Baris 72-74 (bacaan kurva OneCycle): lr memanas dari 0,0040 menuju puncak 0,0999 di sekitar langkah 20, lalu menurun cosine hingga 0,0000004 di langkah terakhir — pola warm-up dan annealing lengkap dalam satu objek.
  7. Baris 80-82 (skenario kelalaian): sch_lupa dibuat tetapi step()-nya tidak pernah dipanggil. Hasilnya pada baris terakhir keluaran: learning rate diam di 0,1000 padahal 16 epoch telah berlalu — scheduler yang tidak dipanggil hanyalah objek mati.

Jebakan Posisi dan Kelalaian #

Dua aturan praktis merangkum seluruh bab ini. Aturan pertama: untuk scheduler per-epoch, panggil scheduler.step() satu kali setelah seluruh batch epoch itu selesai — posisinya di akhir blok epoch, setelah optimizer.step() terakhir. Aturan kedua: untuk OneCycleLR, panggil di akhir setiap batch. PyTorch juga akan menempelkan peringatan di terminal bila mendeteksi urutan scheduler.step() sebelum optimizer.step(), namun program tetap berjalan — kesalahan ini tidak menghentikan training, hanya diam-diam menggeser jadwal.

Rujukan resmi untuk seluruh scheduler dan aturan urutan pemanggilannya tersedia di docs.pytorch.org/docs/stable/optim.html . Scheduler mengatur besar langkah; bab berikutnya berjaga-jaga terhadap masalah yang datang dari arah berbeda — gradien yang tiba-tiba membengkak hingga satu langkah optimizer merusak bobot yang sudah terlatih. Alatnya bernama gradient clipping.