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:

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

text
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 derau

Dua angka pada output adalah alat diagnosisnya:

  1. 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.
  2. 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:

text
L_total = L_data + lambda * (jumlah W^2 seluruh bobot)
gradien bobot menjadi: dL_data/dW + 2*lambda*W

Gradien 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.

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

text
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.235441

Tabel hasilnya mengandung satu pelajaran jujur yang jarang ditampilkan:

  1. 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.
  2. 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.
  3. Baris 63–69 (gradien L2): suku + 2.0 * lambda_l2 * param[...] hanya menempel pada bobot W, bukan bias — bias tidak berkontribusi pada kekasaran fungsi sehingga lazim dibiarkan tanpa penalti.
  4. Baris 40 dan 43 (inverted dropout): pembagian / p_aktif terjadi 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.

MetodeMengubah ApaSaat Pelatihan vs InferensiBiaya Utama
L2 / weight decayloss + gradien bobotsamahyperparameter lambda
Dropoutstruktur jaringan tiap epochhanya pelatihanp_aktif perlu disetel
Early stoppingjadwal berhentihanya pelatihanbutuh himpunan validasi
Batch normalizationskala aktivasi antar lapisanstatistik berbedakompleksitas + 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 #