Training yang sehat bisa berubah kritis dalam sekejap: loss yang tadinya menurun mendadak melonjak ke puluhan, dan gradien yang biasanya kecil membengkak ratusan kali lipat. Satu langkah optimizer dengan gradien sebesar itu cukup untuk melempar bobot jauh dari wilayah baik yang telah dibangun berjam-jam — fenomena exploding gradient. Gradient clipping adalah rem daruratnya: sebelum optimizer.step(), gradien diperiksa dan dipangkas bila melewati batas yang kita tentukan. Chapter ini membedah dua alat PyTorch (clip_grad_norm_ dan clip_grad_value_), membedakan keduanya lewat gradien yang bisa dikendalikan secara analitis, lalu menguji daya selamatnya ketika satu batch rusak disisipkan ke tengah training.
Dua Cara Memangkas Gradien #
torch.nn.utils.clip_grad_norm_ memandang seluruh parameter sebagai satu vektor raksasa, menghitung norma L2 gabungannya, lalu menskalakan seluruh gradien dengan faktor yang sama bila norma itu melebihi max_norm — arah gradien dipertahankan, hanya panjangnya yang dipotong. clip_grad_value_ bekerja lebih kasar: setiap elemen gradien yang melewati clip_value (positif atau negatif) dipotong tepat ke batas itu, tanpa memandang norma keseluruhan. Tabel ringkas berikut memperjelas pembagian peran:
| Alat | Yang dipotong | Sifat hasil | Kebiasaan pemakaian |
|---|---|---|---|
clip_grad_norm_ | norma L2 gabungan seluruh gradien | arah sama, panjang maksimum max_norm | standar umum, RNN/transformer |
clip_grad_value_ | nilai tiap elemen satu per satu | arah dapat berubah | kasus khusus, jarang di training modern |
Berkas clipping_dasar.py memakai model mainan loss = (w * x).sum() dengan w = [3.0, 4.0], sehingga gradien w secara analitis sama persis dengan x — kita bisa memesan gradien bermassa berapa pun lalu mengamati apa yang dilakukan masing-masing alat:
"""Membandingkan gradien sebelum dan sesudah clipping.
Model mainan: loss = (w * x).sum() dengan w = Parameter([3.0, 4.0]).
Gradien w secara analitis sama dengan x, sehingga norma gradien
bisa dikendalikan lewat skala x: 0.05 (kecil) dan 50.0 (besar).
"""
import torch
torch.manual_seed(42)
MAX_NORM = 1.0
MAX_VALUE = 0.5
def hitung_gradien(x_nilai):
w = torch.nn.Parameter(torch.tensor([3.0, 4.0]))
x = torch.tensor(x_nilai)
loss = (w * x).sum()
loss.backward() # gradien w = x
return w
def bulat(nilai):
return [round(v, 2) for v in nilai]
def uji_norm(x_nilai, label):
w = hitung_gradien(x_nilai)
grad_awal = bulat(w.grad.tolist())
sebelum = w.grad.norm().item()
torch.nn.utils.clip_grad_norm_([w], max_norm=MAX_NORM)
sesudah = w.grad.norm().item()
skala = sesudah / sebelum if sebelum > 0 else 0.0
print(f"{label}")
print(f" gradien awal = {grad_awal} | norm = {sebelum:6.2f}")
print(f" gradien akhir = {bulat(w.grad.tolist())} | norm = "
f"{sesudah:5.2f} | skala = {skala:.3f}")
def uji_value(x_nilai, label):
w = hitung_gradien(x_nilai)
grad_awal = bulat(w.grad.tolist())
sebelum = w.grad.norm().item()
torch.nn.utils.clip_grad_value_([w], clip_value=MAX_VALUE)
sesudah = w.grad.norm().item()
print(f"{label}")
print(f" gradien awal = {grad_awal} | norm = {sebelum:6.2f}")
print(f" gradien akhir = {bulat(w.grad.tolist())} | norm = "
f"{sesudah:5.2f}")
print(f"max_norm = {MAX_NORM} | clip_value = {MAX_VALUE}")
print()
print("=== clip_grad_norm_: batasi norma L2 gabungan ===")
uji_norm([0.03, 0.04], "gradien kecil (norma 0.05 < max_norm):")
uji_norm([30.0, 40.0], "gradien besar (norma 50 > max_norm):")
print()
print("=== clip_grad_value_: potong nilai tiap elemen ===")
uji_value([30.0, 40.0], "gradien besar (semua elemen > clip_value):")Output yang Dihasilkan:
max_norm = 1.0 | clip_value = 0.5
=== clip_grad_norm_: batasi norma L2 gabungan ===
gradien kecil (norma 0.05 < max_norm):
gradien awal = [0.03, 0.04] | norm = 0.05
gradien akhir = [0.03, 0.04] | norm = 0.05 | skala = 1.000
gradien besar (norma 50 > max_norm):
gradien awal = [30.0, 40.0] | norm = 50.00
gradien akhir = [0.6, 0.8] | norm = 1.00 | skala = 0.020
=== clip_grad_value_: potong nilai tiap elemen ===
gradien besar (semua elemen > clip_value):
gradien awal = [30.0, 40.0] | norm = 50.00
gradien akhir = [0.5, 0.5] | norm = 0.71Mari kita telaah kode di atas baris per baris:
- Baris 16-21 (
hitung_gradien): karenaloss = (w·x).sum(), turunan loss terhadapwmemangx— baris 20 menjalankanbackward()dan gradien yang terpasang padaw.gradadalah salinanxyang kita kirim. - Baris 33 (
clip_grad_norm_): gradien[30.0, 40.0]memiliki norma 50 — melebihimax_norm=1.0— sehingga seluruh vektor dikalikan faktor1.0/50 = 0.020menjadi[0.6, 0.8]: arah 3:4 tetap utuh, hanya panjangnya yang menjadi tepat 1. - Baris 35 (faktor skala): keluaran menunjukkan
skala = 0.020untuk gradien besar danskala = 1.000untuk gradien kecil — gradien yang sudah di bawah batas tidak disentuh sama sekali. - Baris 48 (
clip_grad_value_): gradien yang sama[30.0, 40.0]dipotong per elemen menjadi[0.5, 0.5]. Norma akhirnya 0,71 — di bawah normaclip_grad_norm_— tetapi rasio 3:4 hilang: kedua komponen kini setara padahal semula 40 lebih besar dari 30. Inilah perbedaan mendasar keduanya.
Satu Batch Rusak di Tengah Training #
Sekarang kita lihat konsekuensinya pada training sungguhan. Berkas clipping_spike.py melatih MLP pada make_moons 200 sampel selama 12 epoch; pada epoch ke-6, satu batch rusak menggantikan data bersih — 32 sampel dengan input dikali 200 dan label acak. Dua skenario dijalankan dari bobot awal yang identik:
"""Efek clipping ketika training menabrak batch bermasalah.
Dataset make_moons 200 sampel dilatih full-batch dengan SGD lr=0.5
selama 12 epoch. Pada epoch 6, satu batch rusak disisipkan: 32 sampel
dengan input dikali 200 dan label acak. Gradien saat itu melonjak;
tanpa clipping, update parameter ikut melonjak dan loss pada data
bersih terlempar jauh, lalu pulih perlahan. Dengan clip_grad_norm_,
lonjakan sama tidak menjatuhkan training.
"""
import torch
import torch.nn as nn
from sklearn.datasets import make_moons
torch.manual_seed(42)
# ---------- 1. Data bersih + satu batch rusak ----------
X, y = make_moons(n_samples=200, noise=0.25, random_state=42)
X_t = torch.tensor(X, dtype=torch.float32)
y_t = torch.tensor(y, dtype=torch.long)
X_rusak = X_t[:32] * 200.0 # input ekstrem
torch.manual_seed(999)
y_rusak = torch.randint(0, 2, (32,)) # label acak
def buat_model():
torch.manual_seed(123) # bobot awal identik
return nn.Sequential(nn.Linear(2, 32), nn.ReLU(), nn.Linear(32, 2))
def norma_total(model):
jumlah = sum((p.grad ** 2).sum() for p in model.parameters())
return torch.sqrt(jumlah).item()
def jalankan(pakai_clipping, max_norm, judul):
print(f"=== {judul} ===")
model = buat_model()
optimizer = torch.optim.SGD(model.parameters(), lr=0.5)
criterion = nn.CrossEntropyLoss()
for epoch in range(1, 13):
xb, yb = (X_rusak, y_rusak) if epoch == 6 else (X_t, y_t)
optimizer.zero_grad()
loss = criterion(model(xb), yb)
loss.backward()
norma = norma_total(model)
if pakai_clipping:
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm)
optimizer.step()
with torch.no_grad(): # loss pada data bersih
loss_bersih = criterion(model(X_t), y_t).item()
print(f"epoch {epoch:2d} | loss batch {loss.item():8.4f} | "
f"norma gradien {norma:7.2f} | loss data bersih "
f"{loss_bersih:7.4f}")
print()
print("batch rusak: 32 sampel, input x200, label acak — epoch 6")
print("SGD lr=0.5 | MLP [2, 32, 2] ReLU | full-batch | 12 epoch")
print()
jalankan(False, None, "TANPA gradient clipping")
jalankan(True, 5.0, "DENGAN clip_grad_norm_(max_norm=5.0)")Output yang Dihasilkan:
batch rusak: 32 sampel, input x200, label acak — epoch 6
SGD lr=0.5 | MLP [2, 32, 2] ReLU | full-batch | 12 epoch
=== TANPA gradient clipping ===
epoch 1 | loss batch 0.5688 | norma gradien 0.40 | loss data bersih 0.4996
epoch 2 | loss batch 0.4996 | norma gradien 0.31 | loss data bersih 0.4557
epoch 3 | loss batch 0.4557 | norma gradien 0.26 | loss data bersih 0.4257
epoch 4 | loss batch 0.4257 | norma gradien 0.21 | loss data bersih 0.4046
epoch 5 | loss batch 0.4046 | norma gradien 0.18 | loss data bersih 0.3892
epoch 6 | loss batch 105.7863 | norma gradien 146.20 | loss data bersih 45.2813
epoch 7 | loss batch 45.2813 | norma gradien 7.42 | loss data bersih 22.4051
epoch 8 | loss batch 22.4051 | norma gradien 4.88 | loss data bersih 13.9114
epoch 9 | loss batch 13.9114 | norma gradien 6.08 | loss data bersih 9.8635
epoch 10 | loss batch 9.8635 | norma gradien 3.13 | loss data bersih 6.0145
epoch 11 | loss batch 6.0145 | norma gradien 2.03 | loss data bersih 4.1107
epoch 12 | loss batch 4.1107 | norma gradien 1.88 | loss data bersih 2.2496
=== DENGAN clip_grad_norm_(max_norm=5.0) ===
epoch 1 | loss batch 0.5688 | norma gradien 0.40 | loss data bersih 0.4996
epoch 2 | loss batch 0.4996 | norma gradien 0.31 | loss data bersih 0.4557
epoch 3 | loss batch 0.4557 | norma gradien 0.26 | loss data bersih 0.4257
epoch 4 | loss batch 0.4257 | norma gradien 0.21 | loss data bersih 0.4046
epoch 5 | loss batch 0.4046 | norma gradien 0.18 | loss data bersih 0.3892
epoch 6 | loss batch 105.7863 | norma gradien 146.20 | loss data bersih 1.1109
epoch 7 | loss batch 1.1109 | norma gradien 0.86 | loss data bersih 0.8131
epoch 8 | loss batch 0.8131 | norma gradien 0.54 | loss data bersih 0.6895
epoch 9 | loss batch 0.6895 | norma gradien 0.40 | loss data bersih 0.6161
epoch 10 | loss batch 0.6161 | norma gradien 0.34 | loss data bersih 0.5624
epoch 11 | loss batch 0.5624 | norma gradien 0.30 | loss data bersih 0.5206
epoch 12 | loss batch 0.5206 | norma gradien 0.27 | loss data bersih 0.4864Mari kita telaah kode di atas baris per baris:
- Baris 22-24 (batch rusak): 32 sampel pertama dikalikan 200 sehingga inputnya bernilai ratusan, dan labelnya diganti bilangan acak 0/1 — gradien yang lahir dari batch seperti ini tidak membawa informasi belajar sama sekali.
- Baris 32-34 (
norma_total): norma dihitung manual dengan menjumlahkan kuadrat seluruh elemen.gradsemua parameter lalu diakarkan — definisi yang sama persis dengan yang dipakaiclip_grad_norm_, sehingga angka pada kolom “norma gradien” adalah norma sebelum pemangkasan. - Baris 43 (jadwal batch): operator bersyarat memilih batch rusak hanya pada epoch 6; epoch lain memakai data bersih — keduanya berangkat dari kondisi identik sehingga perbandingannya adil.
- Baris 47-49 (posisi clipping):
normadicatat lebih dulu, pemangkasan baru terjadi setelahnya, laluoptimizer.step()— urutan wajibnya adalahbackward()→clip→step(). Memanggil clip sebelumbackward()sia-sia karena.gradmasih kosong. - Baris 51-52 (loss data bersih): setelah setiap update, model dievaluasi ulang pada data bersih — inilah kolom yang menunjukkan kerusakan sesungguhnya, karena loss batch yang besar belum tentu berarti model rusak.
Bacaan keluarannya lugas. Pada epoch 6 kedua skenario mencatat norma gradien 146,20 — sumber masalahnya sama. Tanpa clipping, loss pada data bersih terlempar dari 0,3892 ke 45,2813 dan baru turun ke 2,2496 setelah 6 epoch pemulihan; model nyaris memulai dari awal. Dengan max_norm=5.0, gradien raksasa itu dipangkas menjadi lima, loss data bersih hanya tersentuh sampai 1,1109, dan epoch berikutnya langsung kembali ke jalur normal. Kedua skenario juga menunjukkan gradien kecil (0,18-0,40 pada epoch 1-5) tidak tersentuh sama sekali — pemangkasan hanya aktif saat dibutuhkan.
Dokumentasi resmi kedua fungsi ini tersedia di docs.pytorch.org/docs/stable/generated/torch.nn.utils.clip_grad_norm_.html dan docs.pytorch.org/docs/stable/generated/torch.nn.utils.clip_grad_value_.html . Clipping menyelamatkan training yang sudah berjalan, tetapi nasib training juga ditentukan sebelum epoch pertama: pada bobot awal yang buruk, sinyal bisa terlalu kecil atau terlalu besar sejak layer pertama. Chapter berikutnya membahas weight initialization — seni menaruh titik berangkat yang sehat.