Setiap contoh sejauh ini menghitung gradien dengan rumus yang diturunkan manual untuk kasus tertentu, dan pendekatan itu tidak tumbuh seiring kompleksitas model: network nyata adalah susunan puluhan jenis operasi yang bentuknya berubah setiap arsitektur. Computational graph menyelesaikan persoalan struktur tersebut: seluruh perhitungan model direpresentasikan sebagai graf berarah tanpa siklus — node adalah operasi (perkalian, penjumlahan, aktivasi, loss) dan edge adalah nilai yang mengalir antar operasi. Forward pass menjalankan graf dari input ke loss; backward pass menyebarkan gradien dari loss ke input tepat di sepanjang edge yang sama. Dengan representasi ini, aturan rantai dari chapter Backpropagation tidak lagi diturunkan per kasus, melainkan dijalankan secara seragam oleh mesin — inilah fondasi autograd pada framework modern. Chapter ini membuktikannya dengan membangun mesin autograd mini dalam sekitar 60 baris Python murni, lalu menghubungkannya dengan cara kerja PyTorch dan TensorFlow.
Operasi sebagai Node, Gradien sebagai Edge #
Sebuah computational graph menyimpan tiga hal untuk setiap node: nilai hasil operasi, daftar node sumber, dan prosedur backward lokal yang tahu cara membagi gradien yang datang ke node-nodenya. Prosedur lokal inilah seluruh “pengetahuan” yang dibutuhkan mesin — turunan setiap operasi dasar ditulis sekali, lalu operasi apa pun yang dibangun darinya otomatis benar. Tabel berikut merangkum turunan lokal untuk operasi yang telah kita bahas sepanjang section:
| Operasi node | Turunan lokal | Perilaku mundur |
|---|---|---|
u = a × b | du/da = b, du/db = a | gradien dikali nilai operand pasangan |
z = a + b | dz/da = 1, dz/db = 1 | gradien diteruskan utuh ke kedua sumber |
e = a − b | de/da = 1, de/db = −1 | diteruskan utuh, tanda dibalik untuk pengurang |
a = ReLU(z) | da/dz = 0 atau 1 | gradien nol untuk z < 0, utuh untuk z > 0 |
a = sigmoid(z) | da/dz = a(1 − a) | mengecil di daerah jenuh |
Mode penyebaran gradien dari loss mundur ke semua parameter disebut reverse-mode autodiff. Satu putaran backward menghasilkan gradien loss terhadap seluruh node sekaligus dengan biaya sebanding satu forward pass — persis properti efisiensi yang membuat backpropagation praktis pada model berjuta parameter.
Graf Kecil dengan Angka Konkret #
Mari kita bangun graf paling kecil yang tetap memuat inti sebuah layer: x dikali W, hasilnya ditambah b, selisih terhadap target dikuadratkan. Diagram berikut menampilkan nilai forward pada setiap node (garis penuh) dan gradien backward pada setiap edge (garis putus-putus):
Forward pass dengan tangan: u = 2 × 3 = 6, lalu z = 6 + 1 = 7, lalu err = 7 − 8 = −1, lalu loss = (−1)² = 1. Backward pass juga sederhana: dL/derr = 2 × err = −2; karena err terbentuk dari z − t, gradien −2 diteruskan ke z dan (dengan tanda dibalik) +2 ke t; karena z terbentuk dari u + b, gradien −2 diteruskan ke u dan b; terakhir u = x × W membagi gradien −2 menjadi dL/dW = x × (−2) = −4 dan dL/dx = W × (−2) = −6. Satu detail penting: loss = err × err memakai node err dua kali sebagai operand, sehingga kedua kontribusi gradiennya terakumulasi menjadi 2 × err = −2, bukan sekali err.
Mesin Autograd Mini dalam Python Murni #
Daripada menghitung gradien tersebut dengan tangan, kita bangun mesin yang menjalankannya untuk graf apa pun. Kelas Node berikut menyimpan nilai, gradien, sumber, dan prosedur backward lokal; metode backward() mengurutkan node secara topologis lalu menjalankan prosedur mundur dari loss:
"""Computational graph sederhana: node yang menyebarkan gradien sendiri.
Graf yang dibangun: u = x*W, lalu z = u + b, lalu err = z - target,
lalu loss = err*err (MSE satu sampel). Setiap node menyimpan nilai,
daftar node sumber, dan fungsi _backward untuk menyebarkan gradien
mundur. Satu panggilan loss.backward() menurunkan gradien loss ke
seluruh node lewat pengurutan topologis.
"""
class Node:
"""Satu operasi dalam computational graph."""
def __init__(self, data):
self.data = data
self.grad = 0.0
self._backward = lambda: None
self._sumber = ()
def __mul__(self, other):
out = Node(self.data * other.data)
out._sumber = (self, other)
def _backward():
self.grad += other.data * out.grad
other.grad += self.data * out.grad
out._backward = _backward
return out
def __add__(self, other):
out = Node(self.data + other.data)
out._sumber = (self, other)
def _backward():
self.grad += 1.0 * out.grad
other.grad += 1.0 * out.grad
out._backward = _backward
return out
def __sub__(self, other):
out = Node(self.data - other.data)
out._sumber = (self, other)
def _backward():
self.grad += 1.0 * out.grad
other.grad += -1.0 * out.grad
out._backward = _backward
return out
def backward(self):
"""Urutkan node secara topologis, lalu jalankan mundur."""
urutan, dikunjungi = [], set()
def kunjungi(v):
if v not in dikunjungi:
dikunjungi.add(v)
for anak in v._sumber:
kunjungi(anak)
urutan.append(v)
kunjungi(self)
self.grad = 1.0
for v in reversed(urutan):
v._backward()
# ---------- Bangun graf: loss = ((x*W + b) - target)^2 ----------
x = Node(2.0)
W = Node(3.0)
b = Node(1.0)
target = Node(8.0)
u = x * W # 6.0
z = u + b # 7.0
err = z - target # -1.0
loss = err * err # 1.0
# ---------- Forward: nilai tiap node ----------
print("=== FORWARD PASS ===")
print(f"u = x * W : {u.data}")
print(f"z = u + b : {z.data}")
print(f"err = z - target : {err.data}")
print(f"loss = err * err : {loss.data}")
# ---------- Backward: gradien tiap node ----------
loss.backward()
print("\n=== BACKWARD PASS ===")
print(f"dL/derr (akumulasi 2 jalur) : {err.grad:+.1f}")
print(f"dL/dx : {x.grad:+.1f}")
print(f"dL/dW : {W.grad:+.1f}")
print(f"dL/db : {b.grad:+.1f}")
print(f"dL/dtarget : {target.grad:+.1f}")
# ---------- Verifikasi beda hingga ----------
def loss_langsung(Wv, bv, xv):
return ((xv * Wv + bv) - 8.0) ** 2
print("\n=== VERIFIKASI BEDA HINGGA ===")
h = 1e-6
cek = [
("dL/dx", x.grad,
(loss_langsung(W.data, b.data, x.data + h)
- loss_langsung(W.data, b.data, x.data - h)) / (2 * h)),
("dL/dW", W.grad,
(loss_langsung(W.data + h, b.data, x.data)
- loss_langsung(W.data - h, b.data, x.data)) / (2 * h)),
("dL/db", b.grad,
(loss_langsung(W.data, b.data + h, x.data)
- loss_langsung(W.data, b.data - h, x.data)) / (2 * h)),
]
for nama, analitik, numerik in cek:
print(f"{nama}: analitik={analitik:+.4f} numerik={numerik:+.4f} "
f"selisih={abs(analitik - numerik):.2e}")Output yang Dihasilkan:
=== FORWARD PASS ===
u = x * W : 6.0
z = u + b : 7.0
err = z - target : -1.0
loss = err * err : 1.0
=== BACKWARD PASS ===
dL/derr (akumulasi 2 jalur) : -2.0
dL/dx : -6.0
dL/dW : -4.0
dL/db : -2.0
dL/dtarget : +2.0
=== VERIFIKASI BEDA HINGGA ===
dL/dx: analitik=-6.0000 numerik=-6.0000 selisih=1.05e-10
dL/dW: analitik=-4.0000 numerik=-4.0000 selisih=5.04e-10
dL/db: analitik=-2.0000 numerik=-2.0000 selisih=3.35e-10Keluaran mesin cocok persis dengan hitungan tangan di atas, dan verifikasi beda hingga (selisih terbesar 5.04e-10) membuktikan graf menghitung gradien yang benar. Tidak ada satu baris pun di program yang tahu rumus dL/dW = 2·x·(z−t) secara utuh — gradien tersebut muncul dari kerja sama empat prosedur backward lokal yang masing-masing hanya mengenal aturan turunannya sendiri. Mari kita telaah kelas intinya baris per baris:
- Baris 14-18 (
__init__): setiap node lahir dengan nilaidata, gradiengradnol, prosedur backward kosong, dan daftar_sumberkosong; tiga yang terakhir diisi oleh operasi yang membuat node tersebut. - Baris 20-29 (
__mul__): mengalikan dua node menghasilkan node baru yang mencatat kedua sumbernya, lalu memasang closure_backward— aturan rantai lokal untuk perkalian: gradien keluaran dikali nilai operand pasangannya, dan diteruskan ke kedua sumber lewat+=. - Baris 31-40 (
__add__): pola yang sama untuk penjumlahan; turunan lokal bernilai 1 membuat gradien diteruskan utuh ke kedua sumber. - Baris 42-51 (
__sub__): pengurangan menyalin pola penjumlahan dengan tanda operand kedua dibalik menjadi −1. - Baris 53-67 (
backward): jantung mesinnya. Fungsikunjungimelakukan penelusuran mendalam untuk menghasilkan urutan topologis — setiap node ditempatkan setelah semua sumbernya. Gradien loss di set ke 1.0, lalu seluruh urutan dijalankan terbalik sehingga gradien selalu tersedia sebelum sebuah node menyebarkannya. - Baris 70-79: membangun graf cukup dengan menulis ekspresi biasa
x * W,u + b,z - target,err * err— operator Python memanggil metodeNodedan merangkai graph secara implisit, persis cara kode PyTorch biasa terlihat. - Baris 100-119: verifikasi independen dengan beda hingga dua sisi atas fungsi yang sama ditulis sebagai fungsi Python biasa.
Hubungan dengan Autograd Framework Modern #
Mesin 60 baris di atas adalah versi sengaja disederhanakan dari yang dijalankan framework produksi. torch.autograd
membangun graf secara implisit setiap kali operasi tensor dijalankan, menyimpan nilai antara yang diperlukan backward, lalu loss.backward() menjalankan penelusuran mundur dan mengisi atribut .grad setiap tensor parameter — konsep urutan topologis dan gradien lokal persis sama dengan implementasi kita. TensorFlow memakai pola perekaman eksplisit: operasi di dalam blok tf.GradientTape
direkam sebagai graf, lalu tape.gradient() menghitung gradien parameter yang diminta.
Yang belum dimiliki mesin mini kita: operasi tensor berdimensi banyak (termasuk perkalian matriks, yang gradiennya membutuhkan transposisi), pembuangan memori untuk nilai antara yang tidak diperlukan, gradien orde tinggi (turunan dari turunan), dan kernel berkinerja tinggi. Semua topik tersebut dibahas tuntas pada section Deep Learning dengan PyTorch dan Deep Learning dengan TensorFlow .
Dengan computational graph, keping terakhir konseptual section ini telah terpasang: kita tahu cara menyusun operasi, mengukur kesalahan dengan loss , mengalirkan gradien dengan aturan rantai, memperbarui parameter dengan gradient descent , dan mengorganisasi semuanya sebagai graf. Tersisa satu pekerjaan: merangkai seluruh keping menjadi satu program utuh — MLP yang benar-benar dilatih dari nol dengan NumPy di chapter penutup, Implementasi Neural Network dari Nol .