Ketika training mulai berperilaku aneh, loss adalah gejala yang datang paling akhir. Gradien adalah sinyal yang jauh lebih dulu berubah: bobot hanya bergeser sebesar gradien, sehingga apa pun yang terjadi pada gradien hari ini akan menjadi nasib loss beberapa epoch ke depan. Chapter ini melatih kebiasaan memeriksa gradien secara langsung — norm per layer, sebaran nilainya, sampai kasus gradien yang sama sekali tidak terisi — sebelum kita menyalahkan learning rate atau dataset. Seluruh contoh dijalankan nyata pada CPU dengan PyTorch 2.14 dan data sintetis kecil dari scikit-learn.
Gradien sebagai Sinyal Kesehatan Training #
Autograd mengisi atribut .grad pada setiap parameter setiap kali loss.backward() dipanggil, sebagaimana dibahas pada chapter Automatic Differentiation
. Atribut tersebut adalah bahan baku seluruh perbaikan model: optimizer membaca .grad lalu menggeser bobot searah kebalikannya. Karena itu, tiga pertanyaan diagnosis selalu bisa dijawab dari .grad tanpa alat tambahan:
- Apakah gradien terisi? Parameter dengan
.grad is Nonetidak akan pernah diperbarui — layer tersebut diam total meski loss terus berjalan. - Seberapa besar gradien tiap layer? Norma L2 gradien per layer menunjukkan arah aliran sinyal; layer dengan norm yang jauh lebih kecil dari layer lain belajar jauh lebih lambat.
- Bagaimana sebarannya? Sebaran gradien yang sehat mengisi rentang menengah; banyak nilai tepat nol menandakan unit yang tidak pernah aktif.
Norm per layer layak dipantau setiap epoch karena polanya bercerita: norm yang menyusut konsisten dari layer akhir ke layer awal adalah tanda awal vanishing gradient, sedangkan norm yang tiba-tiba melonjak berlipat menandakan batch bermasalah. Pembahasan mendalam tentang kedua fenomena tersebut tersedia di chapter Exploding dan Vanishing Gradient ; di sini kita fokus pada cara memasang instrumen pemantauannya.
Contoh Teruji: Norm, Histogram, Kasus None, dan Gradient Check #
Berkas debugging_gradient.py merangkum empat alat diagnosis dalam satu program: pelatihan model tiga layer dengan pencetakan norm gradien per epoch, histogram bucket gradien tiap layer, reproduksi kasus gradien None beserta perbaikannya, dan gradient check beda hingga sebagai verifikasi kebenaran turunan:
"""Debugging gradien: norm per layer, histogram, kasus grad None, gradient check.
Alur: latih model 3 layer pada data sintetis sambil mencetak norm gradien
tiap layer per epoch -> periksa sebaran gradien tiap layer (bucket
histogram + deteksi unit mati) -> reproduksi kasus "grad is None" beserta
perbaikannya -> tutup dengan gradient check beda hingga pada model kecil
berpresisi ganda.
"""
import numpy as np
import torch
import torch.nn as nn
from sklearn.datasets import make_classification
torch.manual_seed(42)
# ---------- 1. data sintetis kecil ----------
fitur, label = make_classification(
n_samples=200, n_features=20, n_informative=10, n_redundant=5,
n_classes=2, random_state=42,
)
X = torch.tensor(fitur, dtype=torch.float32)
y = torch.tensor(label, dtype=torch.long)
# ---------- 2. model 3 layer ----------
class ModelTigaLayer(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(20, 16)
self.fc2 = nn.Linear(16, 8)
self.fc3 = nn.Linear(8, 2)
def forward(self, x):
return self.fc3(torch.relu(self.fc2(torch.relu(self.fc1(x)))))
model = ModelTigaLayer()
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
def norm_gradien_per_layer(model):
"""Norma L2 gabungan (weight + bias) gradien tiap layer."""
laporan = {}
for nama, p in model.named_parameters():
lapis = nama.split(".")[0]
if p.grad is not None:
laporan[lapis] = laporan.get(lapis, 0.0) + p.grad.norm().item() ** 2
return {k: v ** 0.5 for k, v in laporan.items()}
print("== norm gradien per layer, 4 epoch pertama ==")
for epoch in range(1, 5):
optimizer.zero_grad()
loss = criterion(model(X), y)
loss.backward()
optimizer.step()
norm = norm_gradien_per_layer(model)
print(f"epoch {epoch}: loss={loss.item():.4f} | "
f"fc1={norm['fc1']:.3f} fc2={norm['fc2']:.3f} "
f"fc3={norm['fc3']:.3f}")
# ---------- 3. histogram bucket gradien tiap layer ----------
print("\n== histogram gradien weight per layer (epoch ke-4) ==")
TEPI = [0.0, 1e-4, 1e-2, 1e-1]
print(f"{'layer':<12}{'nol':>5}{'<1e-4':>8}{'1e-4..1e-2':>13}"
f"{'1e-2..1e-1':>12}{'>=1e-1':>9}{'unit mati':>11}")
for nama in ("fc1", "fc2", "fc3"):
g = getattr(model, nama).weight.grad
absolut = g.abs()
jumlah = [
(g == 0).sum().item(),
((absolut > 0) & (absolut < TEPI[1])).sum().item(),
((absolut >= TEPI[1]) & (absolut < TEPI[2])).sum().item(),
((absolut >= TEPI[2]) & (absolut < TEPI[3])).sum().item(),
(absolut >= TEPI[3]).sum().item(),
]
baris_nol = (g.abs().sum(dim=1) == 0).sum().item()
print(f"{nama + '.weight':<12}{jumlah[0]:>5}{jumlah[1]:>8}"
f"{jumlah[2]:>13}{jumlah[3]:>12}{jumlah[4]:>9}"
f"{baris_nol:>7}/{g.shape[0]}")
# ---------- 4. kasus gradien None: penyebab dan solusi ----------
print("\n== kasus gradien None ==")
model_bug = ModelTigaLayer()
# BUG: layer pertama dibekukan (mis. sisa transfer learning) dan lupa
# dinyalakan kembali sebelum training.
model_bug.fc1.weight.requires_grad_(False)
model_bug.fc1.bias.requires_grad_(False)
optimizer_bug = torch.optim.SGD(
[p for p in model_bug.parameters() if p.requires_grad], lr=0.1
)
optimizer_bug.zero_grad()
loss_bug = criterion(model_bug(X), y)
loss_bug.backward()
print("setelah backward pada model yang membekukan fc1:")
for nama, p in model_bug.named_parameters():
status = "None" if p.grad is None else f"norm {p.grad.norm().item():.3f}"
print(f" {nama:<12} grad: {status}")
# SOLUSI: nyalakan kembali requires_grad, lalu backward sekali lagi.
model_bug.fc1.weight.requires_grad_(True)
model_bug.fc1.bias.requires_grad_(True)
optimizer_bug.zero_grad()
loss_bug = criterion(model_bug(X), y)
loss_bug.backward()
print("setelah requires_grad_(True) pada fc1:")
print(f" fc1.weight grad: norm {model_bug.fc1.weight.grad.norm().item():.3f}")
print(f" fc1.bias grad: norm {model_bug.fc1.bias.grad.norm().item():.3f}")
# ---------- 5. gradient check beda hingga ----------
print("\n== gradient check: autograd vs beda hingga (float64) ==")
torch.manual_seed(7)
model_cek = nn.Sequential(nn.Linear(4, 3), nn.Tanh(), nn.Linear(3, 1))
model_cek = model_cek.double()
x_cek = torch.randn(6, 4, dtype=torch.float64)
target_cek = torch.randn(6, 1, dtype=torch.float64)
def hitung_loss_cek():
return ((model_cek(x_cek) - target_cek) ** 2).mean()
loss_cek = hitung_loss_cek()
loss_cek.backward()
h = 1e-6
for nama, p in model_cek.named_parameters():
numerik = torch.zeros_like(p)
for idx in np.ndindex(*p.shape):
asli = p.data[idx].item()
p.data[idx] = asli + h
l_plus = hitung_loss_cek().item()
p.data[idx] = asli - h
l_minus = hitung_loss_cek().item()
p.data[idx] = asli
numerik[idx] = (l_plus - l_minus) / (2 * h)
selisih = (p.grad - numerik).abs().max().item()
print(f" {nama:<10} maks |analitik - numerik| = {selisih:.2e}")Output yang Dihasilkan:
== norm gradien per layer, 4 epoch pertama ==
epoch 1: loss=0.6732 | fc1=0.113 fc2=0.080 fc3=0.130
epoch 2: loss=0.6695 | fc1=0.116 fc2=0.082 fc3=0.134
epoch 3: loss=0.6657 | fc1=0.120 fc2=0.085 fc3=0.137
epoch 4: loss=0.6615 | fc1=0.126 fc2=0.089 fc3=0.142
== histogram gradien weight per layer (epoch ke-4) ==
layer nol <1e-4 1e-4..1e-2 1e-2..1e-1 >=1e-1 unit mati
fc1.weight 0 16 264 40 0 0/16
fc2.weight 0 6 109 13 0 0/8
fc3.weight 0 0 2 14 0 0/2
== kasus gradien None ==
setelah backward pada model yang membekukan fc1:
fc1.weight grad: None
fc1.bias grad: None
fc2.weight grad: norm 0.136
fc2.bias grad: norm 0.016
fc3.weight grad: norm 0.076
fc3.bias grad: norm 0.032
setelah requires_grad_(True) pada fc1:
fc1.weight grad: norm 0.189
fc1.bias grad: norm 0.014
== gradient check: autograd vs beda hingga (float64) ==
0.weight maks |analitik - numerik| = 2.38e-10
0.bias maks |analitik - numerik| = 2.16e-10
2.weight maks |analitik - numerik| = 1.40e-10
2.bias maks |analitik - numerik| = 1.87e-10Mari kita telaah kode di atas baris per baris:
- Baris 17-23 (data):
make_classificationdari scikit-learn menghasilkan 200 sampel 20 fitur yang deterministik berkatrandom_state=42; hasilnya dibungkustorch.tensoragar siap dipakai autograd. - Baris 27-40 (model): tiga
nn.Linearbertingkat dengan ReLU di antaranya. Nama atributfc1-fc3sengaja eksplisit agar laporan gradien mudah dibaca per layer, bukan indeks angka. - Baris 43-50 (
norm_gradien_per_layer):named_parameters()memberi nama sepertifc1.weightdanfc1.bias; kuadrat norm keduanya diakumulasi lalu diakarkan, sehingga satu layer dilaporkan dengan satu angka gabungan. - Baris 53-62 (pemantauan): loop empat epoch melakukan siklus penuh
zero_grad→ forward →backward→step, lalu mencetak loss bersama norm gradien ketiga layer — pola yang bisa dipindahkan ke Training Loop Anda. - Baris 64-82 (histogram): gradien tiap layer dikelompokkan ke lima bucket berdasarkan besaran mutlaknya. Kolom terakhir menghitung baris weight yang seluruh gradiennya nol — unit yang tidak pernah aktif pada satu pun sampel (unit mati).
- Baris 84-111 (kasus None):
requires_grad_(False)membekukan parameter, persis seperti teknik transfer learning. Setelahbackward, parameter beku memiliki.grad is Nonesementara layer lain normal — dan optimizer yang dibuat dari parameter beku tidak akan pernah menyentuhnya. Memanggilrequires_grad_(True)lalubackwardsekali lagi mengembalikan gradien fc1 sebagaimana baris akhir keluaran. - Baris 113-140 (gradient check): model mini dikonversi ke
float64supaya beda hingga presisi; tiap elemen parameter digeser+hdan−h, loss dihitung ulang dua kali, dan kemiringan numerik(L⁺ − L⁻) / 2hdibandingkan dengan gradien autograd. Selisih maksimum hanya2.38e-10— tanda rantai turunan yang benar.
Membaca Angka Pemantauan #
Keluaran norm gradien pada contoh menunjukkan pola sehat: ketiga layer berada di kisaran 0,08-0,14 dan tumbuh perlahan seiring loss menurun — tidak ada layer yang tertinggal jauh. Histogram melengkapi gambaran tersebut: mayoritas gradien fc1 berada di bucket 1e-4..1e-2, hanya 16 dari 320 elemen yang lebih kecil dari 1e-4, dan tidak ada unit mati. Model yang bermasalah biasanya memperlihatkan angka ekstrem: ratusan elemen di bawah 1e-4, atau bucket >=1e-1 yang penuh.
Bagian gradient check adalah jaring pengaman terakhir. Autograd memang menghitung turunan otomatis, tetapi kesalahan masih bisa masuk lewat jalur lain: custom loss yang memutus graph dengan .detach(), operasi in-place, atau indexing yang salah sasaran. Gradient check tidak bergantung pada autograd — ia mengukur kemiringan loss secara langsung — sehingga dua metode independen saling mengunci. Dokumentasi lengkap mekanisme .grad dan graph tersedia di docs.pytorch.org/docs/stable/autograd.html
.
Tabel Diagnosis Gejala, Penyebab, dan Obat #
Pola yang paling sering muncul dalam praktik dirangkum pada tabel berikut:
Gejala pada .grad | Penyebab umum | Obat |
|---|---|---|
None pada seluruh parameter | backward() belum pernah dipanggil; atau seluruh jalur forward berada di dalam torch.no_grad() | pastikan pemeriksaan setelah backward(); keluarkan forward dari blok no_grad() |
None hanya pada beberapa layer | parameter dibekukan requires_grad_(False) dan lupa dinyalakan kembali | panggil requires_grad_(True) sebelum membuat optimizer |
Galat element 0 ... does not require grad saat backward() | graph terputus: .detach(), torch.no_grad(), atau loss dihitung dari tensor non-leaf tanpa requires_grad | hilangkan detach/no_grad dari jalur loss; pastikan loss dibangun dari parameter |
| Norm layer awal menyusut drastis tiap epoch | vanishing gradient: aktivasi saturasi atau kedalaman berlebih | ganti aktivasi ke ReLU, tambah normalisasi atau residual connection |
Norm melonjak tiba-tiba lalu loss menjadi nan | exploding gradient, biasanya learning rate terlalu besar | turunkan learning rate, pasang gradient clipping |
| Banyak elemen tepat nol | unit mati ReLU atau input yang selalu negatif | periksa skala input, ganti inisialisasi, coba aktivasi lain |
Alur Diagnosis #
Ketika kelima alat tersebut dipadukan, proses debugging gradien menjadi prosedur yang runtut — bukan tebakan:
Diagram tersebut dibaca dari atas ke bawah: pemeriksaan paling murah (kelengkapan .grad) dilakukan lebih dulu, disusul pengukuran norm, lalu sebaran. Setiap cabang berakhir pada tindakan spesifik yang dibahas pada chapter terkait — Gradient Clipping
untuk rem exploding gradient, dan Weight Initialization
untuk pemilihan inisialisasi. Dengan instrumen ini terpasang, kita siap membedah dua penyakit gradien yang paling terkenal secara kuantitatif pada chapter berikutnya.