Nama \”backpropagation\” dulu terdengar seperti materi doktoral yang sebaiknya saya hindari. Saya menunda mempelajarinya berminggu-minggu, puas memakai library tanpa paham apa yang terjadi di balik layar. Ketika akhirnya berani menghadapinya, temuan saya mengejutkan: backpropagation hanyalah aturan rantai (chain rule) dari kalkulus SMA, diotomatisasi dan dijalankan mundur dari output ke input. Episode ini catatan cara saya akhirnya paham — lewat analogi, bukan lewat deretan rumus.
Training Adalah Optimasi Parameter
Pakai kerangka yang sudah kita sepakati: model adalah fungsi f(x; θ) dengan parameter θ, dan training adalah proses mencari θ yang mengecilkan loss. Loss adalah angka tunggal yang mengukur seberapa salah prediksi model. Gradient descent adalah cara mengecilkan angka itu: hitung arah penurunan paling tajam (gradien), lalu melangkah sedikit ke arah sana. Secara simbol: θ baru = θ lama dikurangi learning rate dikali gradien loss.
Pertanyaan kuncinya satu: bagaimana menghitung gradien loss terhadap setiap parameter, padahal parameter itu tersembunyi di dalam tumpukan layer yang panjang? Di situlah backpropagation masuk.
Analogi: Rantai Dampak
Bayangkan menghitung gaji bersih lewat beberapa langkah: gaji pokok dikurangi potongan pajak, dikurangi cicilan, dikurangi iuran. Sekarang pertanyaannya: kalau gaji pokok naik satu rupiah, berapa rupiah gaji bersih naik? Jawabannya bukan satu langkah, tapi perkalian pengaruh setiap tahap: berapa dampak gaji pokok ke pajak, dikali dampak pajak ke subtotal, dikali dampak subtotal ke bersih. Persis seperti itu cara kerja chain rule: turunan terhadap variabel di hulu dihitung dengan mengalikan turunan-turunan di sepanjang rantai.
Dalam neural network, rantainya: loss bergantung pada output, output bergantung pada aktivasi layer terakhir, yang bergantung pada layer sebelumnya, dan seterusnya sampai bobot paling awal. Backpropagation menghitung ∂L/∂w dengan mengalikan turunan lokal di sepanjang jalur itu, dari belakang ke depan. Karena gradien layer awal bisa dipakai ulang hasil perhitungan layer belakang, total biayanya efisien — ini alasan metode ini revolutiner.
Backpropagation Step-by-Step: Contoh Manual
Agar konsep benar-benar melekat, mari kita jalankan backpropagation dengan angka pada jaringan sederhana: satu hidden layer, satu neuron. Misalkan input x = 2, bobot input-ke-hidden w1 = 0.5, bias hidden b1 = 0.1, bobot hidden-ke-output w2 = 0.8, bias output b2 = 0.3. Fungsi aktivasi sigmoid. Target output t = 1.
Forward pass: z1 = w1 × x + b1 = 0.5 × 2 + 0.1 = 1.1. Aktivasi hidden: a1 = sigmoid(1.1) ≈ 0.750. Output: z2 = w2 × a1 + b2 = 0.8 × 0.750 + 0.3 = 0.900. Prediksi: ŷ = sigmoid(0.900) ≈ 0.711. Loss (MSE): L = (1 − 0.711)² = 0.0835.
Backward pass — mulai dari output. ∂L/∂ŷ = 2(ŷ − t) = 2(0.711 − 1) = −0.578. Turunan sigmoid di z2: σ'(z2) = ŷ(1 − ŷ) = 0.711 × 0.289 = 0.205. Jadi ∂L/∂z2 = −0.578 × 0.205 = −0.119. Gradien terhadap w2 = ∂L/∂z2 × a1 = −0.119 × 0.750 = −0.089. Gradien terhadap b2 = −0.119.
Meneruskan ke hidden layer: ∂L/∂a1 = ∂L/∂z2 × w2 = −0.119 × 0.8 = −0.095. Turunan sigmoid di z1: σ'(z1) = 0.750 × 0.250 = 0.188. ∂L/∂z1 = −0.095 × 0.188 = −0.0178. Gradien terhadap w1 = ∂L/∂z1 × x = −0.0178 × 2 = −0.0357. Gradien terhadap b1 = −0.0178. Perhatikan betapa gradien w1 (−0.036) jauh lebih kecil dari gradien w2 (−0.089) — ini preview vanishing gradient yang akan dibahas nanti.
Computational Graph
Jaringan saraf sebenarnya bisa direpresentasikan sebagai graf komputasi — kumpulan node (operasi) yang dihubungkan panah (data flow). Backpropagation berjalan mundur di graf ini, menghitung gradien lokal di setiap node lalu mengalikannya sepanjang jalur. Berikut cara visualisasi sederhananya:
# Visualisasi computational graph sederhana
# L = (w*x + b - t)^2
# Node: mul -> add -> sub -> pow
graph = {
\"mul(w,x)\": {\"inputs\": [\"w\", \"x\"], \"output\": \"h1\"},
\"add(h1,b)\": {\"inputs\": [\"h1\", \"b\"], \"output\": \"h2\"},
\"sub(h2,t)\": {\"inputs\": [\"h2\", \"t\"], \"output\": \"diff\"},
\"pow(diff,2)\": {\"inputs\": [\"diff\"], \"output\": \"L\"},
}
# Backprop: mulai dari L, turunkan gradien ke setiap node
# ∂L/∂diff = 2*diff
# ∂diff/∂h2 = -1, ∂h2/∂h1 = 1, ∂h1/∂w = x
# Chain: ∂L/∂w = 2*diff * (-1) * 1 * xPyTorch membangun graf ini secara dinamis setiap kali Anda menjalankan forward pass — ini yang disebut dynamic computation graph atau \”define-by-run\”. Kelebihannya: graf berubah tiap iterasi, cocok untuk input berukuran bervariasi.
Jacobian Matrix: Gradien untuk Vektor
Selama ini kita membahas gradien output skalar terhadap parameter. Tapi bagaimana jika output adalah vektor — misalnya layer menghasilkan 128 nilai sekaligus? Jawabannya: Jacobian matrix. Jacobian J adalah matriks di mana J_ij = ∂y_i/∂x_j, yaitu turunan parsial setiap komponen output terhadap setiap komponen input. Dalam backpropagation, kita tidak pernah membentuk Jacobian penuh secara eksplisit — itu terlalu besar (O(n × m) memori). Sebagai gantinya, algoritma melakukan vector-Jacobian product, yang efisien secara komputasi. Ini konsep yang perlu dipahami agar Anda tidak bingung saat membaca literatur tentang backprop di arsitektur transformer.
Automatic Differentiation di PyTorch
PyTorch menyediakan torch.autograd — mesin yang membangun graf komputasi dan menghitung gradien secara otomatis. Kuncinya: requires_grad=True memberi tahu PyTorch bahwa tensor itu parameter yang perlu dihitung gradiennya.
import torch
# Definisikan parameter dengan requires_grad
w = torch.randn(1, requires_grad=True)
b = torch.randn(1, requires_grad=True)
x = torch.tensor([2.0])
t = torch.tensor([1.0])
# Forward pass — PyTorch merekam graf otomatis
for epoch in range(3):
pred = torch.sigmoid(w * x + b)
loss = (pred - t) ** 2
print(f\"Epoch {epoch}: loss={loss.item():.4f}\")
loss.backward() # hitung gradien semua parameter
# Lihat gradien
print(f\" grad(w)={w.grad.item():.4f}, grad(b)={b.grad.item():.4f}\")
# Update manual (tanpa optimizer)
with torch.no_grad():
w -= 0.5 * w.grad
b -= 0.5 * b.grad
# Reset gradien — penting! tanpa ini, gradien menumpuk
w.grad.zero_()
b.grad.zero_()Perhatikan dua hal penting: (1) .backward() mengisi atribut .grad pada setiap tensor yang requires_grad=True, (2) Anda harus mereset gradien secara manual dengan .grad.zero_() sebelum iterasi berikutnya — jika tidak, gradien baru ditambahkan ke gradien lama. Di training loop yang menggunakan optimizer (torch.optim), reset ini dilakukan oleh optimizer.zero_grad().
Lihat Langsung di PyTorch
Framework modern seperti PyTorch mengotomatiskan semuanya lewat autograd. Anda cukup definisikan perhitungan maju, panggil backward, dan gradien semua parameter terisi:
import torch w = torch.tensor([2.0], requires_grad=True) x = torch.tensor([3.0]) y = w * x # forward pass loss = (y - 10) ** 2 # loss = (wx - 10)^2 loss.backward() # backward pass print(w.grad) # dL/dw = 2*(wx - 10)*x = -24.0
Cek manualnya: y = 2 × 3 = 6, loss = (6 − 10)² = 16. Turunan loss terhadap w adalah 2 × (6 − 10) × 3 = −24. Angka yang dicetak PyTorch persis sama. Gradien negatif artinya: naikkan sedikit nilai w, loss akan turun. Itulah arah yang diambil optimizer.
Vanishing Gradient: Penyebab dan Solusi
Di sinilah teori bertemu sakit kepala saya yang pertama. Karena gradien dihitung dengan perkalian berantai, jika tiap faktornya kecil, hasilnya menyusut eksponensial menuju nol.
Penyebab detail: Fungsi sigmoid memiliki turunan maksimal hanya 0.25 (terjadi di titik tengah, y = 0). Di sisi-sisi, sigmoid jenuh — nilai output mendekati 0 atau 1, dan turunannya mendekati nol. Tanh sedikit lebih baik (turunan maksimal = 1.0 di titik nol), tetapi juga memiliki zona saturasi. Ketika jaringan memiliki banyak layer, gradien harus melewati banyak fungsi aktivasi ini. Jika rata-rata turunannya 0.25, setelah 10 layer gradien sudah berkurang 0.25^10 ≈ 0.000001 — praktis nol. Layer paling awal tidak pernah belajar.
Solusi pertama: ReLU. Rectified Linear Unit f(x) = max(0, x) memiliki turunan 0 untuk x < 0 dan turunan 1 untuk x > 0. Tidak ada zona saturasi di sisi positif, sehingga gradien bisa mengalir tanpa penyusutan eksponensial. Ini alasan utama ReLU menggantikan sigmoid di hidden layer hampir di seluruh arsitektur modern.
Solusi kedua: LSTM gates. Pada jaringan rekuren (RNN), vanishing gradient membuat model sulit mengingat informasi jangka panjang. Long Short-Term Memory (LSTM) memecah masalah ini dengan mekanisme gating — forget gate, input gate, dan output gate — yang mengontrol aliran informasi dan gradien. Forget gate yang terbuka memungkinkan gradien mengalir langsung melewati banyak time step tanpa penyusutan.
Exploding Gradient dan Gradient Clipping
Kejadian sebaliknya juga terjadi: jika tiap faktor gradien lebih besar dari 1, perkalian berantai menyebabkan gradien membengkak hingga angka raksasa. Hasilnya: parameter berubah secara dramatis, loss meledak ke NaN atau infinity, training hancur. Ini terutama umum pada RNN yang memproses sequence panjang.
Solusinya: gradient clipping — membatasi magnitudo gradien agar tidak melewati ambang tertentu. Dua cara utama:
# Cara 1: Clip by norm (paling umum) torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) # Cara 2: Clip by value torch.nn.utils.clip_grad_value_(model.parameters(), clip_value=0.5)
Clip by norm menormalisasi seluruh vektor gradien jika normanya melebihi max_norm, menjaga arah tetap sama tetapi membatasi panjangnya. Clip by value memotong setiap elemen secara individual. Dalam praktik, clip by norm lebih umum digunakan di arsitektur transformer dan RNN.
Residual Connections: ResNet dan Kedalaman Ekstrem
Pertanyaan yang muncul: kalau vanishing gradient menjadi semakin parah seiring bertambahnya layer, bagaimana ResNet bisa memiliki 152 layer dan tetap training dengan baik? Jawabannya: residual connection (skip connection).
Ide intinya sangat sederhana: alih-alih memaksa layer belajar transformasi lengkap H(x), beri jalan pintas yang mengalirkan input langsung ke output: y = H(x) + x. Layer hanya perlu belajar residual F(x) = H(x) − x. Karena ada jalur langsung (skip connection) dari input ke output, gradien bisa mengalir lurus melewati banyak layer tanpa harus melewati bobot dan aktivasi di setiap layer. Secara matematis, gradien di jalur langsung ini bernilai 1 — tidak ada penyusutan, tidak ada ledakan. Inilah alasan ResNet bisa training dengan sangat deep.
Tabel Perbandingan Fungsi Aktivasi
Berikut ringkasan fungsi aktivasi yang paling relevan dalam konteks backpropagation:
| Fungsi | Rumus | Turunan Maks | Kelebihan | Kekurangan |
|---|---|---|---|---|
| Sigmoid | 1 / (1 + e^(-x)) | 0.25 | Output terbatas 0-1, cocok untuk probabilitas | Saturasi, vanishing gradient, bukan zero-centered |
| Tanh | (e^x – e^(-x)) / (e^x + e^(-x)) | 1.0 | Zero-centered, range -1 hingga 1 | Tetap saturasi di ekstrem |
| ReLU | max(0, x) | 1.0 | Gradien stabil di sisi positif, komputasi cepat | Dead neuron jika input negatif, tidak zero-centered |
| GELU | x × Φ(x) | Bervariasi | Smooth, menangani noise, unggul di Transformer | Lebih mahal dari ReLU |
Optimizer: Cara Melangkah
- SGD: gradient descent murni dengan mini-batch. Sederhana, tapi langkahnya bising dan bisa lambat di lembah yang sempit.
- Momentum: menumpuk sebagian arah langkah sebelumnya, seperti bola yang menggelinding dan menembus dataran kecil.
- Adam: menyesuaikan learning rate per-parameter berdasarkan riwayat gradien. Default aman untuk mulai eksperimen.
Catatan Pribadi: Momen \”Oh, Ternyata\”
Ada satu momen spesifik saat backpropagation benar-benar masuk akal bagi saya. Saya sedang membaca kode manual backprop dari scratch — bukan di PyTorch, melainkan implementation murni dengan numpy. Ketika melihat bahwa setiap baris hanyalah perkalian dua angka dan penjumlahan, saya terdiam sejenak. Selama ini saya mengira backpropagation adalah sesuatu yang kompleks, padahal intinya tidak lebih dari \”alikan hasil derivatif satu operasi ke operasi sebelumnya.\” Saya menulis catatan kecil: \”Backprop = chain rule + computational graph + urutan mundur.\” Tiga hal itu saja. Tidak ada sihir.
Penutup yang jujur: Anda bisa produktif tanpa pernah menghitung gradien manual, karena autograd mengerjakannya. Tapi begitu training Anda macet — loss stagnan, gradien meledak, model tidak belajar — pemahaman backpropagation adalah pembeda antara menebak-nebak dan mendiagnosis. Luangkan waktu untuknya. Saya menyesal sudah menunda lama.
Komentar Terbaru