Optimizer pada chapter sebelumnya membuat pelatihan cepat dan stabil — tetapi kecepatan itu justru memperparah masalah yang belum kita tangani: kapasitas. Jaringan saraf menyimpan parameter dalam jumlah besar; jaringan mainan 6-8-1 saja sudah membawa 65 parameter, dan arsitektur sesungguhnya membawa ribuan hingga miliaran. Ketika parameter jauh lebih banyak daripada contoh data, jaringan memiliki ruang untuk menghafal derau data latih satu per satu — overfitting dalam bentuk paling ekstrem. Chapter ini mengukur fenomenanya secara konkret, lalu membangun tiga pengaman klasik yang tetap dipakai sampai sekarang: penalti bobot L2, dropout, dan early stopping, semuanya diimplementasikan tangan di NumPy agar mekanismenya tidak terselubung.
Parameter Banyak, Data Sedikit #
Demo berikut sengaja dibuat membosankan secara struktural: fungsi sejati hanyalah sin(3x) ditambah derau berdeviasi 0,3, dan kita hanya menyediakan 20 contoh latih. Jaringan 1-32-32-1 kemudian dilatih 12.000 epoch penuh — cukup lama bagi gradient descent untuk memeras data latih semaksimal mungkin:
# Overfitting pada jaringan saraf: parameter dihitung dulu, lalu jaringan
# 1-32-32-1 dilatih pada 20 contoh bernoise. Loss latih yang jauh di bawah
# loss uji adalah tanda klasik jaringan menghafal data, bukan mempelajari
# pola fungsi sin(3x) yang menjadi sumber datanya.
import numpy as np
rng = np.random.default_rng(42)
def buat_data(n):
X = rng.uniform(-1.0, 1.0, size=(n, 1))
y = np.sin(3.0 * X) + rng.normal(0.0, 0.3, size=(n, 1))
return X, y
X_latih, y_latih = buat_data(20)
X_uji, y_uji = buat_data(80)
# Bobot awal jaringan 1 -> 32 -> 32 -> 1 (tanh, keluaran linier).
param = {
"W1": rng.normal(0.0, 1.0, size=(1, 32)) * 0.5,
"b1": np.zeros(32),
"W2": rng.normal(0.0, 1.0, size=(32, 32)) * (1.0 / np.sqrt(32)),
"b2": np.zeros(32),
"W3": rng.normal(0.0, 1.0, size=(32, 1)) * (1.0 / np.sqrt(32)),
"b3": np.zeros(1),
}
print("Jumlah parameter jaringan 1-32-32-1:")
total = 0
for nama in ("W1", "b1", "W2", "b2", "W3", "b3"):
P = param[nama]
total += P.size
print(f" {nama} {str(P.shape):<9} = {P.size:>4} parameter")
print(f" total = {total} parameter untuk {len(X_latih)} contoh latih")
def maju(param, X):
A1 = np.tanh(X @ param["W1"] + param["b1"])
A2 = np.tanh(A1 @ param["W2"] + param["b2"])
return A1, A2, A2 @ param["W3"] + param["b3"]
def latih(param, X, y, epoch=12000, laju=0.1):
"""Full-batch gradient descent; backpropagation dua lapisan tersembunyi."""
n = len(y)
for _ in range(epoch):
A1, A2, y_hat = maju(param, X)
dZ3 = 2.0 * (y_hat - y) / n
dW3 = A2.T @ dZ3
db3 = dZ3.sum(axis=0)
dZ2 = (dZ3 @ param["W3"].T) * (1.0 - A2 ** 2)
dW2 = A1.T @ dZ2
db2 = dZ2.sum(axis=0)
dZ1 = (dZ2 @ param["W2"].T) * (1.0 - A1 ** 2)
dW1 = X.T @ dZ1
db1 = dZ1.sum(axis=0)
for nama, dP in (("W1", dW1), ("b1", db1), ("W2", dW2),
("b2", db2), ("W3", dW3), ("b3", db3)):
param[nama] -= laju * dP
def loss(param, X, y):
_, _, y_hat = maju(param, X)
return float(np.mean((y_hat - y) ** 2))
latih(param, X_latih, y_latih)
print(f"\nloss latih (20 contoh) = {loss(param, X_latih, y_latih):.6f}")
print(f"loss uji (80 contoh) = {loss(param, X_uji, y_uji):.6f}")
print("varians derau data = 0.090000")
print("loss latih di bawah varians derau -> jaringan ikut menghafal derau")Output yang Dihasilkan:
Jumlah parameter jaringan 1-32-32-1:
W1 (1, 32) = 32 parameter
b1 (32,) = 32 parameter
W2 (32, 32) = 1024 parameter
b2 (32,) = 32 parameter
W3 (32, 1) = 32 parameter
b3 (1,) = 1 parameter
total = 1153 parameter untuk 20 contoh latih
loss latih (20 contoh) = 0.029369
loss uji (80 contoh) = 0.143562
varians derau data = 0.090000
loss latih di bawah varians derau -> jaringan ikut menghafal derauDua angka pada output adalah alat diagnosisnya:
- Baris 30–36 (hitung parameter): jaringan mungil ini menyimpan 1.153 parameter — hampir 58 kali lipat jumlah contoh latihnya. Hampir seluruhnya berasal dari
W2(1.024 parameter): matriks 32×32 itulah biaya satu lapisan penuh. - Baris 69–73 (loss): loss latih 0,029369 lebih rendah daripada varians derau data itu sendiri (0,090000) — secara matematis mustahil dicapai tanpa memodelkan derau spesifik pada 20 titik tersebut. Loss uji 0,143562 hampir lima kali lipatnya. Ini adalah bias-variance tradeoff dari Section Evaluasi dan Validasi Model dalam wujud paling telanjang: varians model menguasai galat.
Dua Pengaman Klasik: L2 dan Dropout #
Penalti L2 menambahkan jumlah kuadrat bobot ke dalam loss:
L_total = L_data + lambda * (jumlah W^2 seluruh bobot)
gradien bobot menjadi: dL_data/dW + 2*lambda*WGradien tambahan 2*lambda*W menarik setiap bobot menuju nol setiap epoch; bobot yang tidak benar-benar diperlukan untuk memodelkan pola akan menyusut. Fungsi yang dihasilkan menjadi lebih halus dan tidak bisa menciptakan lonjakan tajam untuk menampung satu titik derau. Dropout menyerang dari arah lain: pada tiap epoch pelatihan, setiap neuron tersembunyi “dimatikan” sementara dengan peluang tertentu, sehingga tidak ada neuron tunggal yang boleh menjadi satu-satunya jalur informasi. Varian modern yang dipakai demo kita adalah inverted dropout: neuron yang selamat dikalikan 1/p_aktif saat pelatihan, sehingga inferensi cukup menjalankan jaringan apa adanya tanpa penskalaan apa pun.
# Regularisasi L2 dan dropout (bentuk terbalik) pada jaringan dan data yang
# sama dengan demo overfitting. Empat varian dilatih dari bobot awal identik:
# baseline, L2, dropout ringan, dan dropout agresif -- kekuatan regularisasi
# pun ternyata hyperparameter yang perlu disetel.
import copy
import numpy as np
rng = np.random.default_rng(42)
def buat_data(n):
X = rng.uniform(-1.0, 1.0, size=(n, 1))
y = np.sin(3.0 * X) + rng.normal(0.0, 0.3, size=(n, 1))
return X, y
X_latih, y_latih = buat_data(20)
X_uji, y_uji = buat_data(80)
# Bobot awal jaringan 1 -> 32 -> 32 -> 1 (tanh, keluaran linier).
bobot_awal = {
"W1": rng.normal(0.0, 1.0, size=(1, 32)) * 0.5,
"b1": np.zeros(32),
"W2": rng.normal(0.0, 1.0, size=(32, 32)) * (1.0 / np.sqrt(32)),
"b2": np.zeros(32),
"W3": rng.normal(0.0, 1.0, size=(32, 1)) * (1.0 / np.sqrt(32)),
"b3": np.zeros(1),
}
def maju_latih(param, X, rng_dropout, p_aktif):
"""Forward pass pelatihan: dropout terbalik pada kedua lapisan tersembunyi.
Neuron yang tidak terpilih dinolkan, lalu aktivasi dibagi p_aktif agar
nilai harapan tetap sama -- inferensi karenanya tidak perlu penskalaan.
"""
A1 = np.tanh(X @ param["W1"] + param["b1"])
mask1 = (rng_dropout.random(A1.shape) < p_aktif).astype(float) / p_aktif
A1d = A1 * mask1
A2 = np.tanh(A1d @ param["W2"] + param["b2"])
mask2 = (rng_dropout.random(A2.shape) < p_aktif).astype(float) / p_aktif
A2d = A2 * mask2
y_hat = A2d @ param["W3"] + param["b3"]
return A1, A1d, mask1, A2, A2d, mask2, y_hat
def maju_uji(param, X):
"""Forward pass inferensi: tanpa dropout maupun penskalaan."""
A1 = np.tanh(X @ param["W1"] + param["b1"])
A2 = np.tanh(A1 @ param["W2"] + param["b2"])
return A2 @ param["W3"] + param["b3"]
def latih(param, lambda_l2, p_aktif, epoch=12000, laju=0.1):
rng_dropout = np.random.default_rng(7)
n = len(y_latih)
for _ in range(epoch):
A1, A1d, mask1, A2, A2d, mask2, y_hat = maju_latih(
param, X_latih, rng_dropout, p_aktif)
dZ3 = 2.0 * (y_hat - y_latih) / n
dW3 = A2d.T @ dZ3 + 2.0 * lambda_l2 * param["W3"]
db3 = dZ3.sum(axis=0)
dZ2 = (dZ3 @ param["W3"].T) * mask2 * (1.0 - A2 ** 2)
dW2 = A1d.T @ dZ2 + 2.0 * lambda_l2 * param["W2"]
db2 = dZ2.sum(axis=0)
dZ1 = (dZ2 @ param["W2"].T) * mask1 * (1.0 - A1 ** 2)
dW1 = X_latih.T @ dZ1 + 2.0 * lambda_l2 * param["W1"]
db1 = dZ1.sum(axis=0)
for nama, dP in (("W1", dW1), ("b1", db1), ("W2", dW2), ("b2", db2),
("W3", dW3), ("b3", db3)):
param[nama] -= laju * dP
def loss(param, X, y):
y_hat = maju_uji(param, X)
return float(np.mean((y_hat - y) ** 2))
varian = (
("baseline (tanpa regularisasi)", 0.0, 1.0),
("L2 (lambda = 0.003)", 0.003, 1.0),
("dropout ringan (p_aktif = 0.90)", 0.0, 0.90),
("dropout agresif (p_aktif = 0.75)", 0.0, 0.75),
)
print(f"{'varian':<34}{'loss latih':>12}{'loss uji':>12}")
for label, lambda_l2, p_aktif in varian:
param = copy.deepcopy(bobot_awal)
latih(param, lambda_l2, p_aktif)
print(f"{label:<34}{loss(param, X_latih, y_latih):>12.6f}"
f"{loss(param, X_uji, y_uji):>12.6f}")Output yang Dihasilkan:
varian loss latih loss uji
baseline (tanpa regularisasi) 0.029369 0.143562
L2 (lambda = 0.003) 0.032772 0.124104
dropout ringan (p_aktif = 0.90) 0.031711 0.132864
dropout agresif (p_aktif = 0.75) 0.119599 0.235441Tabel hasilnya mengandung satu pelajaran jujur yang jarang ditampilkan:
- Baris L2 dan dropout ringan: keduanya menaikkan loss latih (0,032772 dan 0,031711) sambil menurunkan loss uji (0,124104 dan 0,132864) — persis definisi kerja regularisasi yang berhasil: menukar sedikit kesesuaian pada data latih demi kemampuan generalisasi.
- Baris dropout agresif: mematikan 25% neuron setiap langkah terbukti terlalu keras untuk jaringan kecil ini — loss uji 0,235441, lebih buruk daripada baseline. Regularisasi adalah hyperparameter: terlalu lemah tidak mengubah apa pun, terlalu kuat mendorong ke underfitting.
- Baris 63–69 (gradien L2): suku
+ 2.0 * lambda_l2 * param[...]hanya menempel pada bobotW, bukan bias — bias tidak berkontribusi pada kekasaran fungsi sehingga lazim dibiarkan tanpa penalti. - Baris 40 dan 43 (inverted dropout): pembagian
/ p_aktifterjadi saat pelatihan;maju_uji(baris 49–53) tidak memakai mask maupun penskalaan sama sekali.
Early Stopping dan Batch Normalization #
Dua pengaman lain melengkapi gambar. Early stopping memanfaatkan fakta sederhana: jaringan mempelajari pola umum lebih dulu, baru kemudian menghafal derau. Dengan memantau loss pada himpunan validasi setiap beberapa epoch dan berhenti saat loss validasi berhenti menurun, kita mendapatkan efek regularisasi tanpa mengubah loss function sama sekali — teknik yang sudah kita pakai lewat kurva validasi pada Chapter Kurva Validasi untuk Pemilihan Hyperparameter, dan scikit-learn menyediakannya langsing lewat MLPClassifier(early_stopping=True). Batch normalization lebih teknis: aktivasi tiap lapisan dinormalisasi memakai statistik batch selama pelatihan (dengan parameter geser-skala yang dipelajari), menjalankan skala sinyal tetap terkendali. Efeknya double — pelatihan jauh lebih stabil pada learning rate besar, dan kegejolakan normalisasi bertindak sebagai regularisasi ringan.
| Metode | Mengubah Apa | Saat Pelatihan vs Inferensi | Biaya Utama |
|---|---|---|---|
| L2 / weight decay | loss + gradien bobot | sama | hyperparameter lambda |
| Dropout | struktur jaringan tiap epoch | hanya pelatihan | p_aktif perlu disetel |
| Early stopping | jadwal berhenti | hanya pelatihan | butuh himpunan validasi |
| Batch normalization | skala aktivasi antar lapisan | statistik berbeda | kompleksitas + batch cukup besar |
Dengan mesin lengkap — forward pass, aktivasi, backpropagation, optimizer, dan regularisasi — kita akhirnya siap menjawab pertanyaan manajerial yang menentukan: kapan seluruh perangkat ini layak dipakai, dan kapan model klasik yang berlatih dalam hitungan detik justru pilihan yang lebih bertanggung jawab? Chapter Kapan Menggunakan Deep Learning menutup Section ini dengan kerangka keputusan tersebut.
Referensi Resmi #
- Neural network supervised — scikit-learn User Guide
— parameter
alpha(L2) danearly_stoppingpadaMLPClassifier. - Random Generator — NumPy — pembangkit mask dropout berseed yang menjamin determinisme.