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:
"""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:
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:
- Baris 11-13 (data): lima pasang
(x, y)ditulis manual —ymenempel di sekitar garis2.5xdengan derau kecil, sehingga solusi least squares persisnyaw = 2.5dan loss minimumnya tidak nol. - Baris 15-16 (parameter):
wadalah tensor daun denganrequires_grad=True— syarat agar autograd merekam operasi yang melibatkannya. Dataxdanysengaja tidak diberi tanda ini; yang dilatih hanya parameter. - Baris 18 (
optimizer):torch.optim.SGDmenerima daftar parameter dan learning ratelr— aturan updatew ← w − lr · gradpersis yang kita tulis manual di chapter Gradient Descent , kini diambil alih pustaka. - Baris 21-24 (tiga langkah pertama):
zero_grad()mengosongkan.grad, forward pass menghitung prediksi, lalutorch.mean(...)mereduksi galat kuadrat menjadi satu angka — loss wajib skalar karenabackward()butuh satu nilai untuk mulai menelusuri graph. - Baris 26 (
backward()): gradiendloss/dwterisi otomatis — epoch 1 memberi-55.0karenawmasih 0 dan seluruh prediksi terlalu kecil. - Baris 28-42 (
step()dan cetak): setelah step,wbergeser dari0.0ke1.65(=0 − 0.03 × (−55)), sementaraw.gradmasih-55.0— buktistep()membaca gradien tanpa membersihkannya. - Baris 40-45 (riwayat): loss runtuh dari
68.7580ke0.0080dan gradien menyusut dari-55menuju-0.0004— tanda khas mendekati lembah: tanjakan makin landai, langkah makin pendek. Nilaiwbertemu2.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:
"""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:
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:
- Jebakan 1 (baris 10-23): tanpa
zero_grad(),w.gradepoch 2 bernilai-73.7— gabungan-55epoch 1 dan-18.7epoch 2.wmelompat terlalu jauh ke3.861, loss sempat turun lalu melonjak naik ke20.3835. Penangkalnya: panggiloptimizer.zero_grad()di awal tiap iterasi, persis siklus lima langkah. - Jebakan 2 (baris 26-41): graph dihapus begitu
backward()selesai — nilainya antara sudah dipakai dan dibuang demi memori. Panggilan kedua menolak dengan galatTrying to backward through the graph a second time. Argumenretain_graph=Truememinta graph dipertahankan;w3.gradmenjadi4.0karena dua turunan (masing-masing2) dijumlahkan. - Jebakan 3 (baris 44-54):
v = w4 * 3.0berbentuk vektor dua elemen;backward()menolak dengangrad can be implicitly created only for scalar outputskarena 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.