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 nodeTurunan lokalPerilaku mundur
u = a × bdu/da = b, du/db = agradien dikali nilai operand pasangan
z = a + bdz/da = 1, dz/db = 1gradien diteruskan utuh ke kedua sumber
e = a − bde/da = 1, de/db = −1diteruskan utuh, tanda dibalik untuk pengurang
a = ReLU(z)da/dz = 0 atau 1gradien 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:

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

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

Keluaran 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:

  1. Baris 14-18 (__init__): setiap node lahir dengan nilai data, gradien grad nol, prosedur backward kosong, dan daftar _sumber kosong; tiga yang terakhir diisi oleh operasi yang membuat node tersebut.
  2. 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 +=.
  3. Baris 31-40 (__add__): pola yang sama untuk penjumlahan; turunan lokal bernilai 1 membuat gradien diteruskan utuh ke kedua sumber.
  4. Baris 42-51 (__sub__): pengurangan menyalin pola penjumlahan dengan tanda operand kedua dibalik menjadi −1.
  5. Baris 53-67 (backward): jantung mesinnya. Fungsi kunjungi melakukan 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.
  6. Baris 70-79: membangun graf cukup dengan menulis ekspresi biasa x * W, u + b, z - target, err * err — operator Python memanggil metode Node dan merangkai graph secara implisit, persis cara kode PyTorch biasa terlihat.
  7. 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 .