📚 Backpropagation — Derivasi Gradient dari Chain Rule
Sesi: 05:00 Deep Learning · Level: Advanced · Kategori: AI/ML
🎯 Tujuan Belajar
Setelah sesi ini, kamu harus bisa: 1. Menurunkan gradient setiap parameter di multilayer perceptron (MLP) dari nol, tanpa mengandalkan rumus hafalan. 2. Menjelaskan mengapa backprop sebenarnya bukan algoritma baru, melainkan aplikasi reverse-mode automatic differentiation pada computational graph. 3. Mengidentifikasi sumber kesalahan umum: sumbu yang salah saat operasi matriks, gradient yang tidak di-reset, dan derivative activation yang salah. 4. Menyambungkan konsep ini ke autograd PyTorch/JAX secara mental.
🧠 Penjelasan Inti
Apa itu backpropagation?
Kita punya jaringan saraf yang, secara matematis, hanyalah fungsi komposit:
L = loss( ŷ, y )
ŷ = f_L( f_{L-1}( ... f_1(x; W_1) ... ; W_L ) )
Untuk melatihnya kita butuh ∂L/∂W_i untuk setiap W_i. Cara naif (numerical differentiation): perturbasi tiap parameter satu per satu dan lihat perubahan loss. Satu parameter = satu forward pass penuh. Untuk model dengan 10⁹ parameter, itu mustahil.
Masalah yang diselesaikan backprop: menghitung semua gradient ini dalam satu kali forward pass + satu kali backward pass, dengan biaya komputasi yang secara asimptotik sebanding dengan forward pass itu sendiri (≈4× biaya forward untuk kasus matmul).
Kenapa ada?
- Efisiensi: O(1) pass per parameter menjadi O(1) pass total.
- Korektus: hasil exact (hingga presisi floating point), bukan aproksimasi finite-difference.
- Komposabilitas: karena bekerja di level computational graph, arsitektur apa pun (CNN, RNN, Transformer) bisa ditangani dengan aturan lokal yang sama.
Fondasi: chain rule
Untuk fungsi skalar dari komposisi skalar:
z = f(y), y = g(x)
dz/dx = (dz/dy) · (dy/dx)
Untuk fungsi multi-variabel, ini menjadi penjumlahan atas semua jalur kontribusi:
∂z/∂x = Σ_i (∂z/∂y_i) · (∂y_i/∂x)
Perhatikan: jumlah, bukan kali. Kalau satu variabel x mengalir ke banyak node, gradient dari tiap jalur dijumlahkan. Ini akar dari banyak bug saat menulis backprop manual.
⚙️ Cara Kerjanya
1. Forward pass — bangun graph + simpan aktivasi
Setiap operasi menghasilkan node di graph. Untuk tiap node kita simpan: - nilai output, - referensi ke input node, - local Jacobian (turunan operasi terhadap inputnya).
Contoh MLP sederhana, satu hidden layer, loss MSE:
z1 = x @ W1 + b1 # pre-activation layer 1
a1 = σ(z1) # aktivasi
z2 = a1 @ W2 + b2 # pre-activation layer 2 (output)
L = mean( (z2 - y)^2 ) # scalar loss
2. Backward pass — reverse topological order
Mulai dari ∂L/∂L = 1, lalu "mendorong" gradient ke belakang. Definisikan δ_k = ∂L/∂z_k (gradient terhadap pre-activation). Ini praktisnya yang kita propagasikan.
Langkah demi langkah (pakai chain rule + aturan penjumlahan):
Loss layer:
∂L/∂z2 = ∂L/∂z2 = δ2 (untuk MSE: δ2 = 2(z2 - y)/N)
Perhatikan z2 muncul di setiap elemen loss → itulah kenapa ∂L/∂z2 dijumlahkan atas semua elemen z2.
Layer 2 parameter:
z2 = a1 @ W2 + b2
∂L/∂W2 = a1ᵀ @ δ2 # bentuk (hidden × output)
∂L/∂b2 = sum(δ2, axis=0) # broadcast b2 → jumlahkan gradient
Alirkan ke layer 1:
∂L/∂a1 = δ2 @ W2ᵀ
∂L/∂z1 = ∂L/∂a1 ⊙ σ'(z1) # ⊙ = elementwise (Hadamard)
Kunci: σ bekerja elementwise, jadi Jacobian-nya diagonal → cukup perkalian elementwise, bukan matmul.
Layer 1 parameter:
∂L/∂W1 = xᵀ @ δ1
∂L/∂b1 = sum(δ1, axis=0)
3. Pola yang harus kamu hafal sebagai kerangka, bukan rumus
Lihat simetri-nya. Untuk node generik z = a @ W + b:
- gradient weight = aᵀ @ δ
- gradient bias = sum(δ)
- gradient input (untuk diteruskan) = δ @ Wᵀ
Ukuran selalu cocok: ∂L/∂W harus berdimensi sama dengan W. Ini sanity check paling ampuh.
4. Generalisasi: computational graph & AD
Yang baru saja kita lakukan adalah reverse-mode automatic differentiation. Dua mode:
| Mode | Arah | Bagus untuk | Biaya per gradient |
|---|---|---|---|
| Forward-mode | maju, sepanjang graph | input sedikit, output banyak | 1 pass per kolom Jacobian |
| Reverse-mode (backprop) | mundur | output skalar, input banyak (neural net!) | 1 pass total untuk 1 output |
Karena loss kita skalar dan parameter banyak, reverse-mode menang telak. Itu sebabnya training pakai backprop; kalkulasi Jacobian penuh dengan input banyak (misal semua elemen x) lebih efisien dengan forward-mode.
💡 Contoh / Analogi
Analogi tanggung jawab kerugian. Bayangkan L itu total kerugian perusahaan. W2 adalah keputusan di level cabang; W1 keputusan di level staf. Backprop bertanya: "Kalau staf ini mengubah outputnya sedikit, seberapa besar total kerugian berubah?" Untuk menjawab, kita mulai dari kerugian → besarnya pengaruh tiap cabang → lalu turun ke tiap staf. Aturan rantai = mengalikan faktor pengaruh di sepanjang rantai; aturan jumlah = menambah pengaruh dari semua jalur yang dilalui.
Contoh angka (satu neuron).
z = w·x + b, w=2, x=3, b=1 → z=7
L = z² → ∂L/∂z = 2z = 14
∂L/∂w = 14 · x = 42
∂L/∂b = 14 · 1 = 14
Cek numerik: perturbasi w ke 2.0001 → z = 7.0003, L = 49.00420009, ΔL/Δw ≈ 42.0009 → cocok.
Contoh graph bercabang (sumber bug).
a = 3
b = a * 2 # b = 6
c = a * 5 # c = 15
d = b + c # d = 21
∂d/∂a = ∂d/∂b·∂b/∂a + ∂d/∂c·∂c/∂a = 1·2 + 1·5 = 7. Bukan 2 atau 5 saja. Setiap kali variabel dipakai lebih dari sekali, gradient-nya dijumlahkan. Di framework, ini terjadi otomatis; saat menulis backprop manual, ini yang paling sering salah.
⚠️ Miskonsepsi Umum
-
"Backprop adalah cara belajar." Bukan. Backprop hanya menghitung gradient. Bagaimana gradient dipakai (SGD, Adam, dsb.) adalah hal terpisah. Backprop = kalkulus; optimizer = keputusan algoritmik.
-
"Backprop = gradient descent." Beda. Gradient descent adalah strategi update parameter; backprop adalah metode efisien menghitung gradient untuk dipakai strategi apa pun.
-
"Backprop sekadar chain rule biasa." Ia chain rule, tapi dengan memoisasi: hasil antara dipakai ulang, bukan dihitung ulang per jalur eksponensial. Tanpa memoisasi, kompleksitasnya bisa eksponensial.
-
"Graph harus dibangun backward." Tidak. Graph dibangun saat forward pass; backward hanya traversal terurut-topologis-terbalik. Ini kesalahan pemahaman umum soal "reverse-mode".
-
"Derivative activation cukup dikali saja." Hanya benar bila aktivasi elementwise. Untuk operasi yang mencampur elemen (softmax, matmul, batchnorm) Jacobian-nya bukan diagonal — harus diperlakukan sebagai transformasi penuh.
-
"Gradient tidak perlu di-reset." Di framework, akumulasi gradient adalah fitur (berguna untuk gradient accumulation) tapi jadi bug kalau lupa
zero_grad()antar-langkah. Konsekuensi: update meledak atau salah.
⚖️ Trade-off / Limitasi
-
Memory vs compute. Backprop butuh menyimpan aktivasi semuanya (kecuali ada checkpointing). Ini bottleneck memori untuk model dalam. Gradient checkpointing menukar compute (forward ulang) dengan memori — trade-off yang sama persis kelasnya dengan time-memory trade-off di algoritma lain.
-
Reverse-mode buruk untuk input banyak. Kalau kamu butuh Jacobian penuh terhadap input berdimensi besar (misal per-piksel di adversarial attack), forward-mode atau vjp/jvp hybrid lebih efisien. Pilihan mode AD bukan preferensi, tapi konsekuensi dimensi.
-
Ekspresivitas graph statis vs dinamis. Graph statis (TF1) cepat tapi kaku; graph dinamis (PyTorch eager, JAX dengan
jit) fleksibel dan mudah di-debug. JAX menyelesaikan ini dengan transformasi fungsional (grad,jvp,vjp) yang dikompilasi via XLA. -
Tidak menyelesaikan masalah optimisasi non-konveks. Backprop memberi gradient yang benar; ia tidak menjamin konvergen ke global minimum. Vanishing/exploding gradient, saddle point, plateau — semua masih ada. Backprop hanya memastikan kamu "menuruni bukit dengan arah yang benar di titik ini", bukan bahwa global optimum tercapai.
-
Presisi floating point. Gradient panjang yang dikomposisikan secara berantai bisa underflow/overflow. Mixed precision + gradient scaling adalah patch praktis, bukan solusi fundamental.
🛠️ Latihan Praktis
Latihan 1 — Derivasi manual. Turunkan ∂L/∂W1 dan ∂L/∂W2 untuk MLP dua-layer dengan σ = tanh dan L = ½‖z2 − y‖². Tulis dalam notasi matriks.
Latihan 2 — Gradient check. Implementasi forward+backward MLP dari nol (NumPy), lalu verifikasi tiap ∂L/∂W dengan finite difference:
(grad_analytic - grad_numeric) / (|grad_analytic| + |grad_numeric| + eps) < 1e-6
Kalau tidak lolos, curigai: salah sumbu matmul, salah transpose, atau lupa jumlahkan jalur bercabang.
Latihan 3 — Bug yang disengaja. Ambil graph bercabang di atas (d = 2a + 5a), lalu implementasi backward yang hanya memakai satu jalur. Bandingkan dengan torch.autograd — selisih faktor berapa? Mengapa?
Latihan 4 (advanced). Turunkan backward pass untuk operasi softmax + cross-entropy digabung. Tunjukkan bahwa hasilnya menyederhana menjadi (ŷ − y) — hasil ini adalah alasan mengapa fused softmax-crossentropy lebih stabil secara numerik.
✅ Cek Pemahaman
-
Mengapa backprop memerlukan one pass untuk seluruh gradient, sedangkan finite-difference memerlukan satu pass per parameter? Diagramkan aliran data-nya.
-
Untuk operasi
z = a @ W, jelaskan mengapa∂L/∂W = aᵀ @ δdan bukanδ @ aᵀ. Apa peran dimensi di sini? -
Sebuah jaringan kehilangan konvergensi setelah menambahkan cabang residual (skip connection). Mengapa ini bisa terjadi (petunjuk: skala gradient), dan bagaimana arsitektur modern mengatasinya?
🔑 Poin Kunci
- Backprop = reverse-mode automatic differentiation pada computational graph; chain rule + aturan penjumlahan untuk jalur bercabang.
- Biaya: ≈ 2× forward untuk backward, satu total pass — inilah lompatan dari O(N_params) ke O(1).
- Pola generik node affine:
∂L/∂W = aᵀ·δ,∂L/∂b = Σδ,∂L/∂a = δ·Wᵀ. Ukuran harus selalu cocok. - Derivative elementwise → perkalian Hadamard; operasi campur-elemen → transformasi Jacobian penuh.
- Gradient di jumlahkan bila variabel mengalir ke banyak jalur — sumber bug nomor satu.
- Backprop hanya menghitung gradient; tidak menjamin optimisasi berhasil.
➡️ Eksplorasi Lanjut
Backprop through time (BPTT) dan vanishing/exploding gradient pada RNN. Bagaimana chain rule yang sama menghasilkan produk tak hingga dari Wᵀ yang mengeksploitasi/meredam gradient? Lanjut ke LSTM/GRU dan mengapa gating adalah trik rekayasa untuk menstabilkan aliran gradient.