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 None tidak 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:

python
"""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:

text
== 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-10

Mari kita telaah kode di atas baris per baris:

  1. Baris 17-23 (data): make_classification dari scikit-learn menghasilkan 200 sampel 20 fitur yang deterministik berkat random_state=42; hasilnya dibungkus torch.tensor agar siap dipakai autograd.
  2. Baris 27-40 (model): tiga nn.Linear bertingkat dengan ReLU di antaranya. Nama atribut fc1-fc3 sengaja eksplisit agar laporan gradien mudah dibaca per layer, bukan indeks angka.
  3. Baris 43-50 (norm_gradien_per_layer): named_parameters() memberi nama seperti fc1.weight dan fc1.bias; kuadrat norm keduanya diakumulasi lalu diakarkan, sehingga satu layer dilaporkan dengan satu angka gabungan.
  4. 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.
  5. 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).
  6. Baris 84-111 (kasus None): requires_grad_(False) membekukan parameter, persis seperti teknik transfer learning. Setelah backward, parameter beku memiliki .grad is None sementara layer lain normal — dan optimizer yang dibuat dari parameter beku tidak akan pernah menyentuhnya. Memanggil requires_grad_(True) lalu backward sekali lagi mengembalikan gradien fc1 sebagaimana baris akhir keluaran.
  7. Baris 113-140 (gradient check): model mini dikonversi ke float64 supaya beda hingga presisi; tiap elemen parameter digeser +h dan −h, loss dihitung ulang dua kali, dan kemiringan numerik (L⁺ − L⁻) / 2h dibandingkan dengan gradien autograd. Selisih maksimum hanya 2.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 .gradPenyebab umumObat
None pada seluruh parameterbackward() 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 layerparameter dibekukan requires_grad_(False) dan lupa dinyalakan kembalipanggil 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_gradhilangkan detach/no_grad dari jalur loss; pastikan loss dibangun dari parameter
Norm layer awal menyusut drastis tiap epochvanishing gradient: aktivasi saturasi atau kedalaman berlebihganti aktivasi ke ReLU, tambah normalisasi atau residual connection
Norm melonjak tiba-tiba lalu loss menjadi nanexploding gradient, biasanya learning rate terlalu besarturunkan learning rate, pasang gradient clipping
Banyak elemen tepat nolunit mati ReLU atau input yang selalu negatifperiksa 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.