Diagnosis pada chapter Overfitting dan Underfitting
menunjuk satu pola berulang: jaringan besar punya cukup kapasitas untuk menghafal data latih. Dropout menyerang masalah itu dengan cara yang terdengar kontraproduktif — selama pelatihan, sebagian neuron dimatikan secara acak pada tiap forward pass, sehingga jaringan tidak pernah bisa bergantung penuh pada jalur atau neuron tertentu. Hasilnya adalah jaringan yang berperilaku seperti ansambel banyak sub-jaringan kecil yang berbagi bobot. Chapter ini membedah mekanismenya di nn.Dropout, membuktikan perbedaan mode latih dan evaluasi, lalu mengukur manfaatnya lewat eksperimen terkendali pada CPU dengan PyTorch 2.14.1.
Mekanisme: Mask Acak dan Penskalaan Terbalik #
Saat mode latih, nn.Dropout(p) menarik mask biner acak untuk tiap elemen input: elemen tersebut dinolkan dengan peluang p, dan elemen yang bertahan dikalikan 1/(1-p). Penskalaan ini — disebut inverted dropout — menjaga besaran aktifasi rata-rata tetap sama antara mode latih dan mode evaluasi, sehingga kita tidak perlu mengubah apa pun saat beralih ke inferensi. Pada mode evaluasi, layer Dropout meneruskan input tanpa diubah sama sekali: seluruh neuron menyala dan tidak ada pengacakan.
Mengapa acak-acakan begini justru menolong? Tanpa dropout, neuron-neuron dapat berko-adaptasi: neuron A mengandalkan neuron B selalu hadir dengan pola tertentu, dan bersama-sama keduanya menghafal kombinasi khusus di data latih. Dengan dropout, pasangan andalan itu sering dipisahkan secara paksa, sehingga tiap neuron terdorong belajar fitur yang berguna dalam banyak konteks — persis obat untuk variance tinggi yang dianalisis pada chapter Bias-Variance .
Perilaku Layer: Mode Latih versus Mode Evaluasi #
Berkas dropout_experiment.py dibuka dengan demonstrasi mekanisme pada satu layer nn.Dropout(p=0.5) yang menerima 10 input bernilai 1:
"""Dropout: mekanisme inverted dropout dan efeknya pada data kecil.
Bagian 1 memperlihatkan perilaku nn.Dropout: keluaran mode latih
berubah tiap panggilan (mask acak) dengan unit yang bertahan diskalakan
1/(1-p), sedangkan mode evaluasi meneruskan input tanpa perubahan.
Bagian 2 membandingkan dua MLP [2, 128, 128, 2] berbobot awal identik
pada make_moons 150 sampel: tanpa dropout versus dropout p=0.5.
"""
import torch
import torch.nn as nn
from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split
torch.manual_seed(42)
criterion = nn.CrossEntropyLoss()
# ---------- 1. Mekanisme nn.Dropout pada input tetap ----------
print("bagian 1: mekanisme nn.Dropout(p=0.5) pada input 10 unit bernilai 1")
lapisan = nn.Dropout(p=0.5)
masukan = torch.ones(1, 10)
torch.manual_seed(7)
keluaran_latih_1 = lapisan(masukan)
keluaran_latih_2 = lapisan(masukan)
lapisan.eval()
keluaran_eval = lapisan(masukan)
print("mode latih, panggilan 1:", keluaran_latih_1.tolist())
print("mode latih, panggilan 2:", keluaran_latih_2.tolist())
print("mode evaluasi :", keluaran_eval.tolist())
nol = (keluaran_latih_1 == 0).sum().item()
print(f"unit yang dinolkan pada panggilan 1: {nol} dari 10")
print("nilai unit yang bertahan: 2.0 = 1/(1-0.5) x 1.0 (inverted dropout)")
print()
# ---------- 2. Dua model identik: tanpa dropout vs dropout 0.5 ----------
X, y = make_moons(n_samples=150, noise=0.30, random_state=42)
X_latih, X_val, y_latih, y_val = train_test_split(
X, y, test_size=0.40, random_state=42
)
rata2 = X_latih.mean(axis=0)
simpangan = X_latih.std(axis=0)
X_latih = (X_latih - rata2) / simpangan
X_val = (X_val - rata2) / simpangan
X_latih_t = torch.tensor(X_latih, dtype=torch.float32)
y_latih_t = torch.tensor(y_latih, dtype=torch.long)
X_val_t = torch.tensor(X_val, dtype=torch.float32)
y_val_t = torch.tensor(y_val, dtype=torch.long)
def buat_model(pakai_dropout):
torch.manual_seed(42)
def reg():
return nn.Dropout(p=0.5) if pakai_dropout else nn.Identity()
return nn.Sequential(
nn.Linear(2, 128),
nn.ReLU(),
reg(),
nn.Linear(128, 128),
nn.ReLU(),
reg(),
nn.Linear(128, 2),
)
def latih(model, jumlah_epoch=1500, lr=0.01):
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
riwayat = {} # akurasi validasi hanya dicatat pada epoch terpilih
for epoch in range(1, jumlah_epoch + 1):
model.train()
optimizer.zero_grad()
loss = criterion(model(X_latih_t), y_latih_t)
loss.backward()
optimizer.step()
if epoch in EPOK_LOG:
model.eval()
with torch.no_grad():
ak_val = (model(X_val_t).argmax(dim=1)
== y_val_t).float().mean().item()
riwayat[epoch] = ak_val
return riwayat
model_polos = buat_model(pakai_dropout=False)
model_dropout = buat_model(pakai_dropout=True)
model_dropout.load_state_dict(model_polos.state_dict())
EPOK_LOG = (1, 500, 1000, 1500)
print("bagian 2: make_moons 150 sampel | latih", len(y_latih_t),
"| validasi", len(y_val_t), "| MLP [2, 128, 128, 2]")
print("kedua model berangkat dari bobot awal identik | Adam lr=0.01 |",
"1500 epoch full-batch\n")
riwayat_polos = latih(model_polos)
riwayat_dropout = latih(model_dropout)
def evaluasi(model):
model.eval()
with torch.no_grad():
ak_latih = (model(X_latih_t).argmax(dim=1)
== y_latih_t).float().mean().item()
ak_val = (model(X_val_t).argmax(dim=1)
== y_val_t).float().mean().item()
return ak_latih, ak_val
ak_latih_polos, ak_val_polos = evaluasi(model_polos)
ak_latih_drop, ak_val_drop = evaluasi(model_dropout)
print(f"{'model':<15s} | {'akurasi latih':>13s} | {'akurasi val':>11s} | "
f"{'gap':>6s}")
print(f"{'tanpa dropout':<15s} | {ak_latih_polos:13.3f} | "
f"{ak_val_polos:11.3f} | {ak_latih_polos - ak_val_polos:6.3f}")
print(f"{'dropout p=0.5':<15s} | {ak_latih_drop:13.3f} | "
f"{ak_val_drop:11.3f} | {ak_latih_drop - ak_val_drop:6.3f}")
print("\nakurasi val pada epoch 1/500/1000/1500:")
for nama, riwayat in (("tanpa dropout", riwayat_polos),
("dropout p=0.5", riwayat_dropout)):
nilai = [riwayat[e] for e in EPOK_LOG]
print(f" {nama:<15s}: {[f'{v:.3f}' for v in nilai]}")Output yang Dihasilkan:
bagian 1: mekanisme nn.Dropout(p=0.5) pada input 10 unit bernilai 1
mode latih, panggilan 1: [[2.0, 2.0, 0.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0]]
mode latih, panggilan 2: [[2.0, 2.0, 2.0, 2.0, 2.0, 0.0, 2.0, 0.0, 2.0, 0.0]]
mode evaluasi : [[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]]
unit yang dinolkan pada panggilan 1: 7 dari 10
nilai unit yang bertahan: 2.0 = 1/(1-0.5) x 1.0 (inverted dropout)
bagian 2: make_moons 150 sampel | latih 90 | validasi 60 | MLP [2, 128, 128, 2]
kedua model berangkat dari bobot awal identik | Adam lr=0.01 | 1500 epoch full-batch
model | akurasi latih | akurasi val | gap
tanpa dropout | 1.000 | 0.767 | 0.233
dropout p=0.5 | 0.956 | 0.850 | 0.106
akurasi val pada epoch 1/500/1000/1500:
tanpa dropout : ['0.717', '0.800', '0.767', '0.767']
dropout p=0.5 : ['0.767', '0.850', '0.800', '0.850']Mari kita telaah kode di atas baris per baris:
- Baris 15-16 (seed dan loss): seed global 42 mengunci seluruh alur acak;
CrossEntropyLossdipakai bersama oleh kedua eksperimen di skrip ini. - Baris 19-21 (layer dan input): satu layer
nn.Dropout(p=0.5)menghadapi input(1, 10)bernilai 1 — pilihan yang membuat mask dan penskalaan terlihat telanjang. - Baris 23-27 (tiga panggilan): dua panggilan mode latih menghasilkan keluaran berbeda karena mask diacak ulang tiap forward; setelah
lapisan.eval(), panggilan ketiga mengembalikan input apa adanya. - Baris 29-35 (cetak mekanisme): keluaran panggilan 1 menunjukkan 7 dari 10 unit bernolkan, sisanya bernilai 2,0 — tepat
1/(1-0.5) × 1,0; keluaran mode evaluasi seluruhnya 1,0. - Baris 38-50 (data): make_moons 150 sampel dibagi 90 latih / 60 validasi, standardisasi dihitung dari data latih saja — pola jujur dari chapter Training Loop .
- Baris 52-66 (
buat_model): fungsireg()mengembalikannn.Dropout(p=0.5)ataunn.Identity()(layer kosong) — kedua arsitektur sama persis kecuali lapisan dropout, sehingga perbandingannya adil. - Baris 86-88 (bobot awal identik):
load_state_dictmenyalin bobot model polos ke model dropout — keduanya berangkat dari titik yang sama. - Baris 68-84 (
latih): siklus epoch standar; akurasi validasi hanya dicatat pada epoch diEPOK_LOG(baris 90) agar skrip tetap ringkas. - Baris 100-110 (
evaluasi): pengukuran akhir dilakukan setelahmodel.eval()— dropout mati, seluruh neuron bekerja. - Baris 112-123 (dua tabel): ringkasan akhir dan lintasan akurasi validasi pada empat epoch terpilih.
Membaca Hasil: Gap Menyempit, Validasi Membaik #
Tabel akhir memperlihatkan efek yang kita cari. Tanpa dropout, model mencapai akurasi latih 1,000 tetapi akurasi validasi hanya 0,767 — gap 0,233, tanda tangan overfitting. Dengan dropout p=0.5, akurasi latih turun ke 0,956 (wajar — dropout menyulitkan hafalan) sementara akurasi validasi naik ke 0,850; gap menyempit menjadi 0,106. Lintasan epoch menegaskan pola yang sehat: model dropout mempertahankan akurasi validasi 0,800-0,850 sepanjang paruh kedua pelatihan, sedangkan model polos tergelincir ke 0,767 dan diam di sana. Perbaikan +0,083 poin akurasi validasi ini berasal murni dari regularisasi — data, arsitektur, optimizer, dan bobot awal persis sama.
Memilih Nilai p #
Parameter tunggal dropout adalah peluang p — porsi unit yang dinolkan. Nilai yang lazim berada di rentang 0,2 sampai 0,5, dengan intuisi sederhana: p kecil memberi regularisasi ringan, p besar membuat tiap forward pass kehilangan terlalu banyak informasi hingga pelatihan melambat atau tidak konvergen. Panduan praktisnya dirangkum pada tabel berikut:
| Posisi | Nilai p umum | Alasan |
|---|---|---|
| Hidden layer fully-connected | 0,2 - 0,5 | area klasik dropout; paling terbukti pada MLP dan kepala klasifikasi |
| Layer konvolusi | 0,1 - 0,3 (atau pakai spatial dropout) | tetangga spasial berkorelasi, unit tunggal kurang independen |
| Layer terakhir sebelum output | 0 (tanpa dropout) | logits perlu stabil; noise di ujung mengganggu loss |
| Jaringan kecil / data banyak | 0 - 0,2 | overfitting ringan tidak butuh obat keras |
| RNN / transformer modern | bervariasi, sering digantikan | arsitektur modern kerap memakai dropout di attention dan embedding |
Dokumentasi resmi layer dropout beserta varian Dropout1d-Dropout3d tersedia di docs.pytorch.org/docs/stable/nn.html#dropout
. Dropout menekan variance dengan mengacak jalur komputasi; teknik berikutnya menekannya dari arah lain — membatasi besaran bobot itu sendiri. Chapter Weight Decay
membahas penalti L2, kesetaraannya dengan weight_decay pada SGD, dan alasan modern memilih AdamW.