Chapter Autograd dan Automatic Differentiation sudah menjawab bagaimana gradien dihitung: graph direkam selama forward, lalu backward() menelusurinya dengan aturan rantai. Chapter ini menjawab pertanyaan berikutnya yang lebih praktis: bagaimana autograd dipakai di dalam loop pelatihan sungguhan, baris demi baris, bersama optimizer. Di sini pula tiga kecelakaan paling umum pemula PyTorch kita bedah satu per satu — gradien yang menumpuk karena lupa zero_grad, backward() yang dipanggil dua kali, dan backward() pada tensor non-skalar — masing-masing dengan gejala, pesan galat aslinya, dan penangkalnya.

Siklus Lima Langkah Pelatihan #

Setiap loop training PyTorch — dari regresi mainan sampai model ratusan juta parameter — berputar pada lima langkah yang sama:

Urutannya tidak bisa ditukar. backward() menambahkan ke .grad, bukan menimpa — perilaku yang sudah kita lihat di section sebelumnya — sehingga .grad harus dikosongkan lebih dulu agar gradien epoch ini tidak tercampur gradien epoch lalu. step() hanya membaca .grad dan menggeser parameter; step tidak menyentuh maupun membersihkan .grad, sehingga pengosongan menunggu iterasi berikutnya (atau dilakukan eksplisit setelah step). Ini pengetahuan kecil yang menjelaskan banyak gejala aneh di chapter-chapter berikutnya.

Melatih Satu Parameter dari Nol ke Konvergen #

Berkas latih_satu_parameter.py menjalankan siklus penuh pada model sekecil mungkin — regresi linear satu parameter prediksi = w * x — supaya setiap angka bisa diikuti dengan mata telanjang. Gradien dicetak sebelum dan sesudah optimizer.step() untuk membuktikan dua sifat sekaligus: gradien terisi oleh backward(), dan step() mengubah w tanpa mengubah w.grad:

python
"""Melatih satu parameter dengan siklus autograd + optimizer yang lengkap.

Model mainan: prediksi = w * x (regresi linear satu parameter, tanpa bias).
Siklus tiap epoch: zero_grad -> forward -> loss -> backward -> step.
"""

import torch

torch.manual_seed(42)

# Data sintetis: y sekitar 2.5 * x dengan derau kecil yang ditulis manual
x = torch.tensor([1.0, 2.0, 3.0, 4.0, 5.0])
y = torch.tensor([2.5, 4.9, 7.6, 10.1, 12.4])

# Parameter yang dilatih; nilai awal sengaja 0 supaya perjalanan terlihat
w = torch.tensor(0.0, requires_grad=True)

optimizer = torch.optim.SGD([w], lr=0.03)

print("Siklus epoch 1 dibedah selangkah demi selangkah:")
for epoch in range(1, 13):
    optimizer.zero_grad()                    # 1. kosongkan gradien lama
    prediksi = w * x                         # 2. forward pass
    loss = torch.mean((prediksi - y) ** 2)   # 3. loss skalar (MSE)

    loss.backward()                          # 4. isi w.grad lewat graph

    if epoch == 1:
        print(f"  loss                    = {loss.item():.4f}")
        print(f"  w sebelum step          = {w.item():.4f}")
        print(f"  w.grad sesudah backward = {w.grad.item():.4f}")

    optimizer.step()                         # 5. w <- w - lr * w.grad

    if epoch == 1:
        print(f"  w sesudah step          = {w.item():.4f}")
        print(f"  w.grad sesudah step     = {w.grad.item():.4f}")
        print()
        print("Dua belas epoch dikompakkan (nilai dicetak sebelum step):")
    else:
        print(f"  epoch {epoch:2d}: loss = {loss.item():8.4f} | "
              f"w = {w.item():.4f} | grad = {w.grad.item():8.4f}")

print()
print(f"nilai w akhir: {w.item():.4f} (nilai optimal least squares: 2.5000)")

Output yang Dihasilkan:

text
Siklus epoch 1 dibedah selangkah demi selangkah:
  loss                    = 68.7580
  w sebelum step          = 0.0000
  w.grad sesudah backward = -55.0000
  w sesudah step          = 1.6500
  w.grad sesudah step     = -55.0000

Dua belas epoch dikompakkan (nilai dicetak sebelum step):
  epoch  2: loss =   7.9555 | w = 2.2110 | grad = -18.7000
  epoch  3: loss =   0.9267 | w = 2.4017 | grad =  -6.3580
  epoch  4: loss =   0.1142 | w = 2.4666 | grad =  -2.1617
  epoch  5: loss =   0.0203 | w = 2.4886 | grad =  -0.7350
  epoch  6: loss =   0.0094 | w = 2.4961 | grad =  -0.2499
  epoch  7: loss =   0.0082 | w = 2.4987 | grad =  -0.0850
  epoch  8: loss =   0.0080 | w = 2.4996 | grad =  -0.0289
  epoch  9: loss =   0.0080 | w = 2.4998 | grad =  -0.0098
  epoch 10: loss =   0.0080 | w = 2.4999 | grad =  -0.0033
  epoch 11: loss =   0.0080 | w = 2.5000 | grad =  -0.0011
  epoch 12: loss =   0.0080 | w = 2.5000 | grad =  -0.0004

nilai w akhir: 2.5000 (nilai optimal least squares: 2.5000)

Mari kita telaah kode di atas baris per baris:

  1. Baris 11-13 (data): lima pasang (x, y) ditulis manual — y menempel di sekitar garis 2.5x dengan derau kecil, sehingga solusi least squares persisnya w = 2.5 dan loss minimumnya tidak nol.
  2. Baris 15-16 (parameter): w adalah tensor daun dengan requires_grad=True — syarat agar autograd merekam operasi yang melibatkannya. Data x dan y sengaja tidak diberi tanda ini; yang dilatih hanya parameter.
  3. Baris 18 (optimizer): torch.optim.SGD menerima daftar parameter dan learning rate lr — aturan update w ← w − lr · grad persis yang kita tulis manual di chapter Gradient Descent , kini diambil alih pustaka.
  4. Baris 21-24 (tiga langkah pertama): zero_grad() mengosongkan .grad, forward pass menghitung prediksi, lalu torch.mean(...) mereduksi galat kuadrat menjadi satu angka — loss wajib skalar karena backward() butuh satu nilai untuk mulai menelusuri graph.
  5. Baris 26 (backward()): gradien dloss/dw terisi otomatis — epoch 1 memberi -55.0 karena w masih 0 dan seluruh prediksi terlalu kecil.
  6. Baris 28-42 (step() dan cetak): setelah step, w bergeser dari 0.0 ke 1.65 (= 0 − 0.03 × (−55)), sementara w.grad masih -55.0 — bukti step() membaca gradien tanpa membersihkannya.
  7. Baris 40-45 (riwayat): loss runtuh dari 68.7580 ke 0.0080 dan gradien menyusut dari -55 menuju -0.0004 — tanda khas mendekati lembah: tanjakan makin landai, langkah makin pendek. Nilai w bertemu 2.5000, tepat solusi least squares-nya.

Tiga Jebakan Klasik dan Penangkalnya #

Berkas jebakan_autograd.py sengaja melakukan tiga kesalahan tersebut satu per satu dan menangkap pesan galat aslinya:

python
"""Tiga jebakan autograd dalam training nyata beserta cara mengatasinya."""

import torch

torch.manual_seed(42)

x = torch.tensor([1.0, 2.0, 3.0, 4.0, 5.0])
y = torch.tensor([2.5, 4.9, 7.6, 10.1, 12.4])

# ---- Jebakan 1: lupa zero_grad -> gradien menumpuk antar epoch ----
w = torch.tensor(0.0, requires_grad=True)
optimizer = torch.optim.SGD([w], lr=0.03)

print("Jebakan 1: tanpa zero_grad (gradien menumpuk)")
for epoch in range(1, 4):
    loss = torch.mean((w * x - y) ** 2)
    loss.backward()
    optimizer.step()          # zero_grad sengaja dilupakan
    print(f"  epoch {epoch}: loss = {loss.item():8.4f} | "
          f"w = {w.item():7.4f} | w.grad = {w.grad.item():8.4f}")

print("  loss sempat turun ke 7.9555 lalu melonjak ke 20.3835:")
print("  gradien epoch lama ikut terjumlahkan")
print()

# ---- Jebakan 2: backward dua kali pada graph yang sama ----
print("Jebakan 2: backward dua kali pada graph yang sama")
w2 = torch.tensor(3.0, requires_grad=True)
z = (w2 * 2.0).sum()
z.backward()
try:
    z.backward()
except RuntimeError as galat:
    print("  galat:", str(galat)[:64] + "...")

w3 = torch.tensor(3.0, requires_grad=True)
z2 = (w3 * 2.0).sum()
z2.backward(retain_graph=True)
z2.backward()
print("  dengan retain_graph=True, backward kedua berhasil: "
      f"w3.grad = {w3.grad.item():.1f} (2 + 2)")
print()

# ---- Jebakan 3: backward pada tensor bukan skalar ----
print("Jebakan 3: backward menuntut skalar")
w4 = torch.tensor([1.0, 2.0], requires_grad=True)
v = w4 * 3.0
try:
    v.backward()
except RuntimeError as galat:
    print("  galat:", str(galat)[:64] + "...")

v.sum().backward()
print("  setelah .sum():  w4.grad =", w4.grad.tolist())

Output yang Dihasilkan:

text
Jebakan 1: tanpa zero_grad (gradien menumpuk)
  epoch 1: loss =  68.7580 | w =  1.6500 | w.grad = -55.0000
  epoch 2: loss =   7.9555 | w =  3.8610 | w.grad = -73.7000
  epoch 3: loss =  20.3835 | w =  5.1737 | w.grad = -43.7580
  loss sempat turun ke 7.9555 lalu melonjak ke 20.3835:
  gradien epoch lama ikut terjumlahkan

Jebakan 2: backward dua kali pada graph yang sama
  galat: Trying to backward through the graph a second time (or directly ...
  dengan retain_graph=True, backward kedua berhasil: w3.grad = 4.0 (2 + 2)

Jebakan 3: backward menuntut skalar
  galat: grad can be implicitly created only for scalar outputs...
  setelah .sum():  w4.grad = [3.0, 3.0]

Mari kita telaah tiga jebakan tersebut satu per satu:

  1. Jebakan 1 (baris 10-23): tanpa zero_grad(), w.grad epoch 2 bernilai -73.7 — gabungan -55 epoch 1 dan -18.7 epoch 2. w melompat terlalu jauh ke 3.861, loss sempat turun lalu melonjak naik ke 20.3835. Penangkalnya: panggil optimizer.zero_grad() di awal tiap iterasi, persis siklus lima langkah.
  2. Jebakan 2 (baris 26-41): graph dihapus begitu backward() selesai — nilainya antara sudah dipakai dan dibuang demi memori. Panggilan kedua menolak dengan galat Trying to backward through the graph a second time. Argumen retain_graph=True meminta graph dipertahankan; w3.grad menjadi 4.0 karena dua turunan (masing-masing 2) dijumlahkan.
  3. Jebakan 3 (baris 44-54): v = w4 * 3.0 berbentuk vektor dua elemen; backward() menolak dengan grad can be implicitly created only for scalar outputs karena tidak jelas gradien berapa kali loss harus dihitung. Reduksi .sum() (atau .mean()) menjadikannya skalar, dan gradien [3.0, 3.0] mengalir ke tiap elemen.

Dokumentasi lengkap perilaku backward(), retain_graph, dan pengecualian autograd ada di docs.pytorch.org/docs/stable/autograd.html , sedangkan keluarga optimizer di docs.pytorch.org/docs/stable/optim.html . Siklus lima langkah kini berjalan, tetapi model kita masih satu parameter telanjang yang dikelola manual. Chapter berikutnya memperkenalkan nn.Module — kerangka yang merapikan parameter menjadi kelas Python rapi dengan __init__ dan forward.