Dua chapter terakhir memberi kita jaringan yang bisa menghitung keluaran dari bobot apa pun — tetapi bobotnya masih angka acak, dan jaringan belum tahu seberapa jauh keluarannya meleset. Proses belajar membutuhkan dua komponen yang hilang: loss function yang mengubah “salah” menjadi satu angka yang bisa diminimalkan, dan backpropagation yang menghitung ke arah mana setiap bobot harus digeser. Keduanya adalah jantung pelatihan neural network, dan keduanya bisa kita bangun serta uji dengan NumPy. Chapter ini diakhiri dengan teknik yang menjadi standar industri untuk membuktikan kebenaran implementasi: gradient check, membandingkan gradien analitik hasil backpropagation dengan hampiran numerik — selisih terkecil kita nanti berorde sepuluh pangkat minus sebelas.
Loss Function: Alat Ukur Galat #
Loss function memetakan pasangan (prediksi, target) menjadi satu angka; semakin kecil semakin baik. Pilihan loss mengikuti jenis masalah, dan ketiganya sudah familiar dari section regresi dan klasifikasi:
MSE = mean((y_hat - y)^2) -> regresi
BCE = -mean( y*log(p) + (1-y)*log(1-p) ) -> klasifikasi biner
CCE = -mean( log( softmax(z)_kelas_benar ) ) -> klasifikasi banyak kelasMSE menghukum galat besar secara kuadratik sehingga pencilan terasa berat. BCE membandingkan peluang prediksi p dengan label biner y — prediksi 0,9 pada label 1 nyaris tanpa hukuman, prediksi 0,1 pada label 1 dihukum keras. CCE adalah generalisasi BCE untuk banyak kelas: ambil peluang kelas yang benar hasil softmax, lalu ambil negatif logaritmanya. Mari kita hitung ketiganya, sekaligus membuktikan jebakan numerik CCE:
# Tiga loss function inti dan masalah stabilitas numeriknya:
# MSE untuk regresi, binary cross-entropy untuk klasifikasi biner,
# categorical cross-entropy untuk banyak kelas (versi naif vs stabil).
import numpy as np
def mse(y_hat, y):
"""Mean squared error untuk regresi."""
selisih = y_hat - y
return float(np.mean(selisih ** 2))
def bce(peluang, y):
"""Binary cross-entropy; peluang diklip agar log tetap terdefinisi."""
p = np.clip(peluang, 1e-12, 1.0 - 1e-12)
return float(-np.mean(y * np.log(p) + (1.0 - y) * np.log(1.0 - p)))
def cce_stabil(logit, indeks_kelas):
"""Categorical cross-entropy versi stabil lewat log-sum-exp."""
z = logit - logit.max()
log_jumlah = np.log(np.exp(z).sum())
log_peluang = z - log_jumlah
return float(-log_peluang[indeks_kelas])
print("== MSE: prediksi harga rumah (dalam ratus juta rupiah) ==")
y = np.array([3.0, 2.0, 4.5, 5.0])
y_hat = np.array([2.5, 2.3, 4.2, 5.4])
print(f"target = {y.tolist()}")
print(f"prediksi = {y_hat.tolist()}")
print(f"MSE = {mse(y_hat, y):.4f}")
print("\n== BCE: prediksi peluang email spam ==")
peluang = np.array([0.9, 0.2, 0.7, 0.6])
label = np.array([1.0, 0.0, 1.0, 0.0])
print(f"peluang = {peluang.tolist()}")
print(f"label = {label.astype(int).tolist()}")
print(f"BCE = {bce(peluang, label):.4f}")
print("\n== CCE: logit ekstrem, versi naif vs stabil ==")
logit_besar = np.array([1000.0, 1000.0, 1000.0])
with np.errstate(over="ignore", invalid="ignore"):
peluang_naive = np.exp(logit_besar) / np.exp(logit_besar).sum()
cce_naive = float(-np.log(peluang_naive[0]))
z = logit_besar - logit_besar.max()
peluang_stabil = np.exp(z) / np.exp(z).sum()
print(f"logit = {logit_besar.tolist()}")
print(f"CCE naif (exp dulu) = {cce_naive}")
print(f"CCE stabil (log-sum) = {cce_stabil(logit_besar, 0):.4f}")
print(f"peluang stabil = {np.round(peluang_stabil, 4).tolist()}")Output yang Dihasilkan:
== MSE: prediksi harga rumah (dalam ratus juta rupiah) ==
target = [3.0, 2.0, 4.5, 5.0]
prediksi = [2.5, 2.3, 4.2, 5.4]
MSE = 0.1475
== BCE: prediksi peluang email spam ==
peluang = [0.9, 0.2, 0.7, 0.6]
label = [1, 0, 1, 0]
BCE = 0.4004
== CCE: logit ekstrem, versi naif vs stabil ==
logit = [1000.0, 1000.0, 1000.0]
CCE naif (exp dulu) = nan
CCE stabil (log-sum) = 1.0986
peluang stabil = [0.3333, 0.3333, 0.3333]Tiga pengamatan dari output di atas:
- Baris 28–33 (MSE): keempat galat prediksi harga bernilai −0,5 hingga +0,4, tetapi kuadratnya menjadikan MSE 0,1475 — galat terbesar (0,5) menyumbang porsi terbesar.
- Baris 35–40 (BCE): fungsi
bcepada baris 14–17 mengklip peluang ke rentang[1e-12, 1-1e-12]sebelum mengambil log. Tanpa pengklipan, prediksi 0 atau tepat 1 menghasilkanlog(0)= tak hingga. - Baris 42–52 (CCE): inilah jebakan stabilitas. Hitung softmax secara naif pada logit 1000 membuat
np.exp(1000)meluap menjadiinf, laluinf/infmenjadinan. Versi stabil pada baris 20–25 mengurangi nilai maksimum logit lebih dulu (log-sum-exp trick): hasilnya loss 1,0986 (tepatlog 3, karena ketiga kelas identik) dan peluang bersih 0,3333 untuk tiap kelas.
Backpropagation: Aturan Rantai yang Dijalankan Mundur #
Backpropagation bukan algoritme baru — ia adalah aturan rantai kalkulus yang dieksekusi secara efisien dari belakang ke depan. Bayangkan jaringan sebagai graf komputasi: X -> Z1 -> A1 -> Z2 -> y_hat -> L. Untuk setiap simpul, gradien dari loss dihitung dengan resep seragam:
gradien_lokal = gradien_dari_hulu x turunan_fungsi_lokalUntuk jaringan 1 lapisan tersembunyi (tanh) dengan keluaran linier dan loss MSE, seluruh turunannya dapat dituliskan ringkas:
Maju : Z1 = X @ W1 + b1 A1 = tanh(Z1)
Z2 = A1 @ W2 + b2 y_hat = Z2
L = mean((y_hat - y)^2)
Mundur: dL/dZ2 = 2(y_hat - y)/n (turunan loss langsung)
dL/dW2 = A1^T @ dL/dZ2 (aturan rantai, bentuk matriks)
dL/db2 = jumlah baris dL/dZ2
dL/dA1 = dL/dZ2 @ W2^T (gradien mengalir ke belakang)
dL/dZ1 = dL/dA1 * (1 - A1^2) (turunan tanh, bab sebelumnya)
dL/dW1 = X^T @ dL/dZ1
dL/db1 = jumlah baris dL/dZ1Perhatikan pola simetrinya: bobot selalu dihitung sebagai masukan^T @ gradien_keluaran, dan gradien yang dikirim ke lapisan sebelumnya selalu gradien_keluaran @ W^T. Aturan turunan tanh 1 - A1^2 persis kolom d/dx pada tabel chapter Fungsi Aktivasi — di sinilah turunan-turunan itu akhirnya dipakai. Efisiensinya luar biasa: satu kali hitung maju + satu kali hitung mundur menghasilkan gradien untuk semua parameter sekaligus, dibandingkan menghitung ulang seluruh jaringan untuk tiap parameter satu per satu.
Gradient Check: Menguji Backprop dengan Beda Hingga #
Bagaimana kita tahu rumus mundur di atas tidak salah ketik? Jawaban standarnya adalah gradient check: dekati tiap turunan secara numerik dengan beda hingga pusat, (L(θ+ε) − L(θ−ε)) / 2ε, lalu bandingkan dengan gradien analitik. Implementasi yang benar akan menghasilkan selisih berorde 1e-8 atau lebih kecil. Berikut demo lengkapnya — pemeriksaan dulu, lalu pelatihan singkat sebagai bukti bahwa gradien yang benar memang menurunkan loss:
# Gradient check: gradien analitik hasil backpropagation dibandingkan
# dengan gradien numerik beda hingga pada jaringan 1 lapisan tersembunyi.
# Selisih maksimum yang sehat berorde 1e-8 atau lebih kecil. Setelah lolos
# pemeriksaan, jaringan dilatih beberapa epoch dan loss ditampilkan.
import numpy as np
rng = np.random.default_rng(42)
# Data regresi sintetis kecil: 8 contoh, 3 fitur, target nonlinear.
X = rng.uniform(-1.0, 1.0, size=(8, 3))
y = (X[:, 0] * X[:, 1] + 0.5 * X[:, 2] ** 2 + 0.3).reshape(-1, 1)
# Arsitektur: 3 masukan -> 5 neuron tanh -> 1 keluaran linier.
W1 = rng.normal(0.0, 1.0 / np.sqrt(3), size=(3, 5))
b1 = np.zeros(5)
W2 = rng.normal(0.0, 1.0 / np.sqrt(5), size=(5, 1))
b2 = np.zeros(1)
param = {"W1": W1, "b1": b1, "W2": W2, "b2": b2}
def hitung_loss(param, X, y):
"""Forward pass + MSE; bobot diambil dari kamus param."""
A1 = np.tanh(X @ param["W1"] + param["b1"])
y_hat = A1 @ param["W2"] + param["b2"]
return float(np.mean((y_hat - y) ** 2))
def gradien_analitik(param, X, y):
"""Backpropagation untuk MSE pada jaringan 3-5-1."""
A1 = np.tanh(X @ param["W1"] + param["b1"])
y_hat = A1 @ param["W2"] + param["b2"]
n = len(y)
dZ2 = 2.0 * (y_hat - y) / n # gradien loss thd Z2
dW2 = A1.T @ dZ2
db2 = dZ2.sum(axis=0)
dA1 = dZ2 @ param["W2"].T # gradien loss thd A1
dZ1 = dA1 * (1.0 - A1 ** 2) # turunan tanh
dW1 = X.T @ dZ1
db1 = dZ1.sum(axis=0)
return {"W1": dW1, "b1": db1, "W2": dW2, "b2": db2}
def gradien_numerik(param, X, y, eps=1e-6):
"""Beda hingga pusat: (L(theta+eps) - L(theta-eps)) / (2 eps)."""
hasil = {}
for nama in param:
P = param[nama].copy()
G = np.zeros_like(P)
for idx in np.ndindex(P.shape):
naik = param.copy()
turun = param.copy()
P_naik = P.copy()
P_naik[idx] += eps
P_turun = P.copy()
P_turun[idx] -= eps
naik[nama] = P_naik
turun[nama] = P_turun
G[idx] = (hitung_loss(naik, X, y) - hitung_loss(turun, X, y)) / (2 * eps)
hasil[nama] = G
return hasil
print("== Gradient check (sebelum pelatihan) ==")
analitik = gradien_analitik(param, X, y)
numerik = gradien_numerik(param, X, y)
selisih_maks = 0.0
for nama in ("W1", "b1", "W2", "b2"):
selisih = np.abs(analitik[nama] - numerik[nama]).max()
selisih_maks = max(selisih_maks, float(selisih))
print(f" {nama}: selisih maksimum analitik vs numerik = {selisih:.3e}")
print(f" keseluruhan: {selisih_maks:.3e} (ambang sehat <= 1e-6)")
print("\n== Pelatihan 300 epoch (gradient descent penuh) ==")
laju = 0.1
for epoch in range(301):
G = gradien_analitik(param, X, y)
for nama in param:
param[nama] -= laju * G[nama]
if epoch % 60 == 0:
loss = hitung_loss(param, X, y)
print(f" epoch {epoch:3d} loss {loss:.6f}")
loss_akhir = hitung_loss(param, X, y)
print(f" loss akhir = {loss_akhir:.6f}")Output yang Dihasilkan:
== Gradient check (sebelum pelatihan) ==
W1: selisih maksimum analitik vs numerik = 5.027e-11
b1: selisih maksimum analitik vs numerik = 3.286e-11
W2: selisih maksimum analitik vs numerik = 2.353e-11
b2: selisih maksimum analitik vs numerik = 1.960e-11
keseluruhan: 5.027e-11 (ambang sehat <= 1e-6)
== Pelatihan 300 epoch (gradient descent penuh) ==
epoch 0 loss 0.208730
epoch 60 loss 0.011757
epoch 120 loss 0.011285
epoch 180 loss 0.010949
epoch 240 loss 0.010620
epoch 300 loss 0.010292
loss akhir = 0.010292Hasilnya melewati ekspektasi dengan margin besar:
- Baris 65–73 (pemeriksaan): selisih maksimum tiap kelompok parameter berkisar 1,960e-11 hingga 5,027e-11 — tiga orde magnitudo di bawah ambang sehat 1e-6, bahkan lebih baik dari patokan umum 1e-8. Backpropagation pada baris 30–42 terbukti konsisten dengan definisi turunan.
- Baris 45–62 (gradien numerik): fungsi ini sengaja lambat — untuk setiap elemen parameter ia menjalankan forward pass dua kali (baris 60). Untuk 21 parameter ini sepele, tetapi pada jaringan bermiliar parameter pendekatan ini tidak lagi terjangkau; itulah alasan backpropagation ada.
- Baris 75–86 (pelatihan): dengan gradien terverifikasi, gradient descent penuh menurunkan loss dari 0,208730 ke 0,010292 dalam 300 epoch — jaringan benar-benar belajar fungsi nonlinear dari data delapan contoh tersebut.
Pelatihan di atas memakai satu pilihan yang belum kita pertanyakan: gradient descent penuh dengan laju tetap 0,1. Kenyataannya, cara mengambil langkah — seberapa banyak data per pembaruan, seberapa besar langkahnya, dan bagaimana laju berubah sepanjang pelatihan — sangat menentukan kecepatan dan kestabilan belajar. Chapter Gradient Descent dan Variasinya membedah pilihan-pilihan tersebut, dari mini-batch SGD hingga intuisi Adam.
Referensi Resmi #
- Log loss — scikit-learn Model Evaluation — definisi resmi log loss (BCE/CCE) beserta pengklipan peluangnya.
- numpy.clip — NumPy
— pengklipan nilai yang menjaga
logtetap terdefinisi. - Neural network supervised — scikit-learn User Guide
— catatan resmi tentang loss dan solver pada
MLPClassifier/MLPRegressor.