Gradient clipping

Menjaga stabilitas pelatihan jaringan dalam dengan gradient clipping.

Daftar isi

Topik ini membahas gradient clipping, yaitu pembatasan besar gradien sebelum gradien itu digunakan untuk memperbarui bobot. Perulangan pelatihan yang dikenal sejauh ini menempatkan loss.backward() tepat sebelum optimizer.step(), dan topik ini menyisipkan satu baris di antara keduanya. Fungsi torch.nn.utils.clip_grad_norm_ dibedah melalui pengukuran norm secara langsung, kemudian manfaatnya dibuktikan pada jaringan dalam yang sengaja dibuat tidak stabil. Urutan pemanggilan yang benar di dalam perulangan pelatihan juga ditetapkan agar clipping tidak berjalan sia-sia.

Gradien yang meledak#

Pada jaringan yang dalam, gradien yang mengalir mundur dari loss ke layer-layer awal dapat membesar berlipat di setiap layer, suatu kejadian yang disebut exploding gradient. Gejala ini paling mudah muncul pada jaringan berulang yang memproses sekuens panjang dan pada jaringan dalam yang tidak menggunakan normalisasi. Akibatnya langsung terlihat pada pelatihan, yaitu langkah pembaruan bobot yang raksasa, loss yang melompat ke inf, dan model yang berhenti belajar begitu angkanya berubah menjadi nan. Gradient clipping menahan gejala itu dengan membatasi besarnya gradien sebelum digunakan.

Penyebab pertumbuhannya berada pada aturan rantai, yaitu gradien layer awal merupakan perkalian kontribusi seluruh layer di depannya, sehingga faktor yang lebih besar dari satu pada setiap layer menumpuk menjadi nilai raksasa. Batch normalization pada arsitektur modern meredam sebagian besar kejadian ini, tetapi jaringan berulang dan arsitektur tanpa normalisasi masih menyisakan celah yang sama. Pada keadaan itulah clipping menjadi pengaman yang murah, karena biayanya satu baris kode dan akibatnya mudah dipahami.

Besaran yang dipantau adalah norm L2 dari seluruh gradien model yang dirangkai menjadi satu vektor gg:

∥g∥2=∑igi2 \lVert g \rVert_2 = \sqrt{\sum_i g_i^2}

Nilai inilah yang dibandingkan dengan batas yang ditetapkan, dan seluruh gradien direskalasi sekaligus apabila batasnya terlampaui.

clip_grad_norm_#

torch.nn.utils.clip_grad_norm_ menerima kumpulan parameter dan nilai max_norm, lalu memeriksa norm total gradien yang tersimpan pada atribut .grad parameter tersebut. Apabila norm total melebihi max_norm, seluruh gradien dikalikan dengan faktor max_norm/∥g∥2 \text{max\_norm} / \lVert g \rVert_2 sehingga normnya menjadi tepat max_norm. Apabila norm berada di bawah batas, gradien dibiarkan apa adanya. Arah vektor gradien tidak pernah diubah, sehingga arah pembaruan bobot tetap sama dan hanya besarnya yang dikurangi.

Gradient clipping mereskalasi vektor gradien agar normnya tidak melebihi max_normg, norm 3.4g', norm 1.0batas max_norm = 1.0Bila norm g melebihi max_norm, seluruh vektorgradien dikurangi skalanya hingga normnya tepatmax_norm. Arahnya tidak berubah, sehinggaarah pembaruan bobot tetap sama.
Reskalasi vektor gradien oleh clip_grad_norm_

Skrip berikut mengukur norm sebelum dan sesudah clipping. Loss dibesarkan seribu kali agar gradiennya jauh melebihi batas, sehingga proses reskalasinya benar-benar terjadi.

python norm-sebelum-sesudah.py
import torch
import torch.nn as nn

torch.manual_seed(0)

model = nn.Linear(4, 2)
x = torch.randn(3, 4)  # shape (3, 4)
y = torch.randn(3, 2)  # shape (3, 2)

# loss dibesarkan seribu kali agar gradiennya jauh melebihi max_norm
loss = nn.functional.mse_loss(model(x), y) * 1000
loss.backward()

# clip_grad_norm_ mengembalikan norm total SEBELUM clipping
total = torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
print(f"Norm sebelum clipping : {total:.4f}")

norm_sesudah = torch.norm(torch.stack([p.grad.norm() for p in model.parameters()]))
print(f"Norm sesudah clipping : {norm_sesudah:.4f}")
text Keluaran
Norm sebelum clipping : 865.2150
Norm sesudah clipping : 1.0000

Norm 865.2150 jauh melampaui batas 1.0, sehingga seluruh gradien direduksi menjadi norm 1.0000 tanpa mengubah arahnya. Fungsi ini mengembalikan norm sebelum clipping, sehingga angka tersebut dapat dicatat ke log untuk memantau seberapa sering clipping aktif. Nilai max_norm=1.0 adalah titik awal yang lazim digunakan, dan nilai tersebut dapat dinaikkan bila pelatihan terasa lambat berkembang.

Fungsi clipping bawaan lain yang sering muncul adalah clip_grad_value_. Keduanya bekerja pada objek yang sama tetapi dengan satuan yang berbeda.

Aspekclip_grad_norm_clip_grad_value_
Besaran yang dibatasinorm vektor gabungannilai setiap elemen
Batas lazim1.00.5 sampai 1.0
Arah gradiendipertahankandapat berubah
Pemakaian umumpilihan utama untuk stabilisasikasus khusus

Urutan backward, clipping, step#

Clipping membaca dan mengubah isi .grad, sehingga posisinya wajib berada setelah loss.backward() dan sebelum optimizer.step(). Apabila dipanggil sebelum backward, atribut .grad masih kosong atau masih berisi gradien lama, dan pemanggilan itu tidak memberi efek apa pun pada gradien yang baru. Posisi yang benar dalam satu iterasi tampak sebagai berikut.

python Posisi clipping dalam satu iterasi
# tidak-dijalankan: fragmen merujuk variabel dari skrip lengkap di bawah
optimizer.zero_grad()
loss = criterion(model(x_batch), y_batch)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()

Skrip berikut membuktikan manfaat clipping pada jaringan yang mudah menjadi tidak stabil, yaitu 24 layer linear kecil tanpa normalisasi yang bobot awalnya diperbesar. Pelatihan dijalankan dua kali pada data yang sama, sekali tanpa clipping dan sekali dengan clipping.

python latih-jaringan-dalam.py
import torch
import torch.nn as nn

torch.manual_seed(0)


def buat_model():
    # jaringan dalam: 24 lapisan linear kecil tanpa normalisasi
    model = nn.Sequential()
    for i in range(24):
        model.add_module(f"linear{i}", nn.Linear(16, 16))
        model.add_module(f"relu{i}", nn.ReLU())
    with torch.no_grad():
        # bobot diperbesar agar gradien mudah meledak
        for m in model:
            if isinstance(m, nn.Linear):
                m.weight.mul_(3.0)
    return model


def latih(gunakan_clip):
    model = buat_model()
    optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
    x = torch.randn(32, 16)  # shape (32, 16)
    y = torch.randn(32, 16)  # shape (32, 16)
    print("--- clipping aktif ---" if gunakan_clip else "--- clipping nonaktif ---")
    for epoch in range(10):
        optimizer.zero_grad()
        loss = nn.functional.mse_loss(model(x), y)
        loss.backward()
        if gunakan_clip:
            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
        optimizer.step()
        print(f"epoch {epoch + 1:2d}: loss {loss.item():.4f}")


latih(gunakan_clip=False)
latih(gunakan_clip=True)
text Keluaran
--- clipping nonaktif ---
epoch  1: loss 1420.7220
epoch  2: loss inf
epoch  3: loss nan
epoch  4: loss nan
epoch  5: loss nan
epoch  6: loss nan
epoch  7: loss nan
epoch  8: loss nan
epoch  9: loss nan
epoch 10: loss nan
--- clipping aktif ---
epoch  1: loss 46.5683
epoch  2: loss 121.2312
epoch  3: loss 17.6611
epoch  4: loss 20.5379
epoch  5: loss 22.8233
epoch  6: loss 18.1880
epoch  7: loss 7.6651
epoch  8: loss 7.0142
epoch  9: loss 5.9996
epoch 10: loss 6.8439

Tanpa clipping, loss meluap menjadi inf pada epoch kedua dan berubah menjadi nan sejak epoch ketiga, sehingga pelatihan sesungguhnya sudah berakhir. Dengan clipping, loss tetap terhitung pada seluruh epoch dan cenderung menurun meskipun tidak rata, karena arah gradien dipertahankan tetapi besarnya selalu dibatasi. Fluktuasi angka pada awal pelatihan adalah hal yang wajar, karena bobot awal yang diperbesar memang menjadikan lanskap loss sangat curam.

Kesalahan umum#

Tiga kesalahan berikut paling sering muncul saat gradient clipping dipraktikkan.

Yang pertama adalah memanggil clip_grad_norm_ sebelum loss.backward(). Pada posisi itu, .grad masih kosong atau masih menyimpan gradien iterasi sebelumnya, sehingga clipping membatasi objek yang salah. Perbaikannya, pertahankan urutan backward, clipping, lalu step.

Yang kedua adalah memilih max_norm yang terlalu kecil. Pemotongan norm yang berlangsung terus-menerus mempertahankan arah gradien, tetapi langkah pembaruannya menjadi nyaris nol, sehingga loss turun sangat lambat. Perbaikannya, mulai dari 1.0, lalu naikkan nilainya bila norm yang dicatat biasanya jauh di bawah batas.

Yang ketiga adalah menganggap clipping sebagai pengobatan penyebab. Gradien yang meledak berakar pada inisialisasi bobot, learning rate yang besar, atau arsitektur tanpa normalisasi, sedangkan clipping hanya menahan gejalanya. Perbaikannya, gunakan clipping sebagai pengaman sambil meninjau penyebab utamanya.

Ringkasan#

  • Gradient clipping membatasi norm gabungan seluruh gradien pada nilai max_norm, lazimnya 1.0, dengan menjaga arah vektor gradien tetap sama.
  • Posisi clip_grad_norm_ wajib di antara loss.backward() dan optimizer.step(), karena fungsi itu membaca dan mengubah isi .grad.
  • Tanpa clipping, jaringan yang tidak stabil dapat menghasilkan loss inf lalu nan dalam beberapa langkah, sedangkan dengan clipping pelatihan tetap berjalan dan loss tetap terhitung.

Referensi#