Jadwal learning rate

StepLR, cosine, dan warmup untuk mengatur learning rate sepanjang pelatihan.

Daftar isi

Topik ini membahas jadwal learning rate, yaitu aturan yang mengubah nilai learning rate sepanjang pelatihan alih-alih membiarkannya tetap. Objek yang mengurus aturan itu disebut scheduler, dan tiga pola yang paling sering digunakan dibahas berurutan, yaitu penurunan bertangga dengan StepLR, penurunan melengkung dengan CosineAnnealingLR, dan warmup yang menaikkan learning rate di awal pelatihan. Tempat pemanggilan scheduler.step() yang benar, baik per epoch maupun per batch, juga ditetapkan. Semua contoh mencetak nilai learning rate per epoch, sehingga polanya dapat dibaca langsung dari keluaran.

Learning rate tetap dan trade-off-nya#

Learning rate menentukan seberapa jauh bobot bergerak setiap kali diperbarui. Nilai yang besar membuat pelatihan bergerak cepat pada awalnya tetapi mudah bergoyang di sekitar solusi, sedangkan nilai yang kecil berjalan stabil tetapi lambat dan mudah berhenti di tempat yang belum optimal. Pola yang sehat biasanya menggabungkan keduanya, yaitu learning rate besar di awal pelatihan dan mengecil menjelang akhir.

Untuk pelatihan pendek dengan model kecil, learning rate yang tetap sering kali sudah cukup. Keputusan mengaturnya menjadi penting ketika pelatihan berlangsung lama, kurva loss mulai mendatar lebih awal, atau langkah terakhir masih bergoyang. Pada PyTorch, aturan tersebut diwujudkan oleh objek scheduler dari modul torch.optim.lr_scheduler yang dibuat di atas sebuah optimizer. Scheduler menulis ulang nilai lr pada param_groups milik optimizer sesuai jadwalnya, sehingga optimizer tetap menggunakan nilai itu pada setiap langkah pembaruan, sedangkan besarannya ditentukan oleh jadwal.

StepLR#

StepLR menurunkan learning rate secara bertangga, yaitu melipatgandakan nilai saat ini dengan gamma setiap kali jumlah epoch mencapai kelipatan step_size. Jadwal ini paling mudah diprediksi, karena seluruh perjalanan learning rate dapat dihitung dari tiga angka saja, yaitu nilai awal, step_size, dan gamma. Skrip berikut menggunakan nilai awal 0.1 dengan step_size=3 dan gamma=0.1, sehingga learning rate terbelah sepuluh kali setiap tiga epoch:

python uji-steplr.py
import torch

model = torch.nn.Linear(4, 2)
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=3, gamma=0.1)

for epoch in range(10):
    print(f"epoch {epoch + 1:2d}: lr {optimizer.param_groups[0]['lr']:.4f}")
    optimizer.step()
    scheduler.step()
text Keluaran
epoch  1: lr 0.1000
epoch  2: lr 0.1000
epoch  3: lr 0.1000
epoch  4: lr 0.0100
epoch  5: lr 0.0100
epoch  6: lr 0.0100
epoch  7: lr 0.0010
epoch  8: lr 0.0010
epoch  9: lr 0.0010
epoch 10: lr 0.0001

Keluaran memperlihatkan tangga yang dimaksud, yaitu nilai 0.1 bertahan tiga epoch, turun menjadi 0.01 untuk tiga epoch berikutnya, dan terus mengecil. Perhatikan pula posisi baris cetak sebelum optimizer.step() dan scheduler.step(), sehingga angka yang tampil adalah learning rate yang berlaku untuk epoch tersebut. Rasio penurunan 0.1 tergolong agresif, dan nilai gamma 0.5 digunakan pula pada banyak pelatihan agar tangganya landai. Pemilihan step_size lazimnya mengikuti total epoch, misalnya sepertiga atau setengah durasi pelatihan, sehingga penurunan terbesar terjadi sebelum pelatihan berakhir.

CosineAnnealingLR#

CosineAnnealingLR menurunkan learning rate mengikuti separuh gelombang kosinus, dari nilai awal menuju nilai minimum eta_min, dengan kelengkungan yang melandai di kedua ujung. Bentuk ringkasnya adalah:

ηt=ηmin⁡+12 (ηmax⁡−ηmin⁡)(1+cos⁡(πtTmax⁡)) \eta_t = \eta_{\min} + \tfrac{1}{2}\,(\eta_{\max} - \eta_{\min})\left(1 + \cos\left(\tfrac{\pi t}{T_{\max}}\right)\right)

dengan Tmax⁡T_{\max} sebagai panjang separuh gelombang dalam satuan epoch. Keunggulan jadwal ini terletak pada penurunannya yang mulus tanpa keputusan keras kapan harus turun, sehingga hanya perlu menetapkan total durasi pelatihan. Skrip berikut menggunakan nilai awal 0.1 dengan T_max=10 dan eta_min=0.001:

python uji-cosine.py
import torch

model = torch.nn.Linear(4, 2)
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=10, eta_min=0.001)

for epoch in range(10):
    print(f"epoch {epoch + 1:2d}: lr {optimizer.param_groups[0]['lr']:.4f}")
    optimizer.step()
    scheduler.step()
text Keluaran
epoch  1: lr 0.1000
epoch  2: lr 0.0976
epoch  3: lr 0.0905
epoch  4: lr 0.0796
epoch  5: lr 0.0658
epoch  6: lr 0.0505
epoch  7: lr 0.0352
epoch  8: lr 0.0214
epoch  9: lr 0.0105
epoch 10: lr 0.0034

Penurunannya lambat pada awal dan akhir, lalu paling curam di tengah, persis seperti bentuk gelombang kosinus. Nilai pada epoch kesepuluh sudah mendekati eta_min, yaitu 0.001. Perlu diketahui bahwa T_max sebaiknya dekat dengan jumlah epoch yang direncanakan, karena nilai di luar durasi tersebut membuat jadwal tidak mencapai bagian akhirnya. Jadwal kosinus menjadi pilihan yang lazim pada pelatihan modern karena tidak menuntut keputusan kapan harus menurunkan learning rate.

Warmup pada awal pelatihan#

Warmup menaikkan learning rate secara bertahap selama beberapa langkah pertama sebelum jadwal utama dimulai. Alasannya, pada awal pelatihan bobot masih acak sehingga arah gradien belum dapat dipercaya, dan learning rate penuh sejak langkah pertama berisiko membuat loss melompat liar. Implementasi manualnya mudah dengan LambdaLR, yaitu scheduler yang mengalikan learning rate dasar dengan nilai fungsi yang menerima nomor epoch. Skrip berikut menggabungkan warmup selama tiga epoch dengan peluruhan kosinus:

python uji-warmup.py
import math

import torch

model = torch.nn.Linear(4, 2)
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)

warmup = 3   # epoch pemanasan
total = 12   # total epoch pelatihan


def jadwal(epoch):
    if epoch < warmup:
        return (epoch + 1) / warmup
    sisa = (epoch - warmup) / (total - warmup)
    return 0.5 * (1.0 + math.cos(math.pi * sisa))


scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, jadwal)

for epoch in range(total):
    print(f"epoch {epoch + 1:2d}: lr {optimizer.param_groups[0]['lr']:.4f}")
    optimizer.step()
    scheduler.step()
text Keluaran
epoch  1: lr 0.0333
epoch  2: lr 0.0667
epoch  3: lr 0.1000
epoch  4: lr 0.1000
epoch  5: lr 0.0970
epoch  6: lr 0.0883
epoch  7: lr 0.0750
epoch  8: lr 0.0587
epoch  9: lr 0.0413
epoch 10: lr 0.0250
epoch 11: lr 0.0117
epoch 12: lr 0.0030

Tiga epoch pertama menaikkan learning rate dari sepertiga nilai dasar menuju nilai penuh, kemudian jadwal kosinus mengambil alih hingga akhir pelatihan. Durasi warmup yang lazim berkisar beberapa epoch atau beberapa ratus langkah pertama, cukup untuk memberi waktu bobot menemukan arah yang stabil. Diagram berikut menyandingkan ketiga pola yang dibahas, yaitu tangga StepLR, kurva kosinus, dan kombinasi warmup.

Tiga pola jadwal learning rate sepanjang epoch0.0250.0500.0750.100epochlearning rateStepLR (gamma 0.5)CosineAnnealingLRwarmup + cosine
Tiga pola jadwal learning rate sepanjang epoch

Per epoch atau per batch#

scheduler.step() dipanggil satu kali pada setiap titik jadwal, dan satuan titik itu ditentukan penulis pelatihan. Pemanggilan per epoch merupakan kebiasaan yang paling umum untuk jadwal seperti pada contoh-contoh di atas, sedangkan pemanggilan per batch menghasilkan jadwal yang lebih halus dan menjadi pilihan bila warmup dilakukan dalam satuan langkah. Satuan juga mengubah makna angka pada scheduler, karena step_size pada StepLR yang dipanggil per batch berarti kelipatan batch, bukan kelipatan epoch. Apabila scheduler per epoch dipanggil di dalam perulangan batch, penurunannya berlangsung lebih cepat dari yang direncanakan, karena setiap batch dihitung sebagai satu titik jadwal. Posisi pemanggilan per batch ditunjukkan fragmen berikut:

python Penempatan scheduler.step() per batch
# tidak-dijalankan: fragmen merujuk variabel dari skrip pelatihan
for x_batch, y_batch in loader:
    optimizer.zero_grad()
    loss = criterion(model(x_batch), y_batch)
    loss.backward()
    optimizer.step()
    scheduler.step()   # satu kali per batch, tepat setelah optimizer.step()

Ketentuan yang berlaku pada kedua satuan adalah memanggil scheduler.step() setelah optimizer.step(). Apabila urutannya dibalik, PyTorch mengeluarkan peringatan dan jadwal bergeser satu langkah dari rencananya.

Kesalahan umum#

Tiga kesalahan berikut paling sering muncul saat scheduler digunakan pertama kali.

Yang pertama adalah memanggil scheduler.step() sebelum optimizer.step(). PyTorch menandai urutan ini dengan peringatan, dan titik-titik jadwal bergeser sehingga learning rate tidak sesuai rencana. Perbaikannya, letakkan pemanggilan scheduler tepat setelah pemanggilan optimizer.

Yang kedua adalah membuat objek scheduler baru di dalam perulangan epoch. Objek baru selalu memulai jadwalnya dari awal, sehingga learning rate terus berputar pada bagian pertama jadwal dan tidak pernah menurun. Perbaikannya, buat scheduler satu kali di luar perulangan, sebagaimana seluruh contoh pada topik ini.

Yang ketiga adalah memanggil scheduler.step() di dua tempat sekaligus, misalnya di dalam perulangan batch dan sekali lagi di akhir epoch. Jadwal berjalan dua kali lebih cepat dari yang direncanakan tanpa pesan error apa pun. Perbaikannya, tetapkan satu satuan titik jadwal dan panggil scheduler.step() hanya di tempat itu.

Ringkasan#

  • Scheduler mengatur nilai lr pada param_groups milik optimizer, dan scheduler.step() dipanggil satu kali per titik jadwal, baik per epoch maupun per batch, selalu setelah optimizer.step().
  • StepLR melipatgandakan learning rate dengan gamma setiap kelipatan step_size, sedangkan CosineAnnealingLR menurunkannya secara mulus dari nilai awal menuju eta_min sepanjang T_max.
  • Warmup menaikkan learning rate bertahap pada awal pelatihan dan diimplementasikan dengan mudah melalui LambdaLR yang mengalikan nilai dasar dengan pengali hasil fungsi.

Referensi#