Episode ini tentang momok pertama yang membuat saya merasa bodoh saat belajar machine learning. Saya melatih decision tree untuk klasifikasi sederhana, akurasi di data training 99 persen. Senang setengah mati. Begitu diuji ke data yang belum pernah dilihat, nilainya anjlok ke kisaran 60-an. Saya cek ulang kode, curiga ada bug, cari-cari jam kerja yang hilang. Ternyata tidak ada bug. Yang terjadi adalah overfitting, dan episode ini adalah catatan saya memahaminya lewat kacamata seorang programmer.
Model Itu Fungsi, Training Itu Optimasi
Ingat analogi yang kita pakai sepanjang seri ini: model adalah fungsi berparameter, misalnya f(x; θ), dan training adalah proses mencari nilai θ yang mengecilkan loss di data training. Masalahnya, \”mengecilkan loss di data training\” bukan tujuan sesungguhnya. Tujuan kita adalah performa di data baru yang belum pernah dilihat model. Dua hal ini sering dicampuradukkan, dan di situlah masalah dimulai.
Analogi favorit saya: unit test. Bayangkan seorang programmer nakal yang membuat implementasi yang isinya cuma membaca daftar kasus uji lalu mengembalikan jawaban yang sudah dihardcode. Semua test hijau, coverage bagus, tapi programnya tidak berguna sama sekali karena tidak menggeneralisasi. Model yang overfit persis begitu: dia menghafal pasangan input-output di data training, termasuk noise-nya, bukan mempelajari polanya.
Underfitting: Bias Tinggi
Kebalikannya, underfitting terjadi kalau model terlalu kaku untuk menangkap pola yang ada. Contoh klasik: memaksa regresi linear untuk data yang polanya melengkung. Errornya tinggi di data training, dan pasti tinggi juga di data validasi. Rasanya seperti memakai regex untuk mem-parsing HTML yang bersarang — alatnya salah dari awal, sebaik apa pun Anda mengatur parameternya hasilnya tetap jelek.
Dalam istilah statistik, underfitting disebut bias tinggi: prediksi model secara sistematis meleset dari kenyataan karena asumsi fungsinya terlalu sederhana. Overfitting disebut varians tinggi: model terlalu sensitif, perubahan kecil di data training bisa menghasilkan model yang sangat berbeda. Tradeoff keduanya adalah salah satu kerangka berpikir paling berguna di machine learning.
Tabel Diagnosa Cepat
| Aspek | Underfitting | Overfitting |
|---|---|---|
| Error training | Tinggi | Sangat rendah |
| Error validasi | Tinggi | Tinggi |
| Jarak train vs validasi | Rapat | Gap lebar |
| Penyebab umum | Model terlalu sederhana, fitur kurang informatif | Model terlalu kompleks, data terlalu sedikit |
| Obat pertama | Model lebih fleksibel, feature engineering | Regularisasi, lebih banyak data, sederhanakan model |
Cara Deteksi di Lapangan
- Pisahkan data menjadi train, validation, dan test sejak awal. Jangan pernah menilai generalisasi dari data training.
- Bandingkan metrik di train versus validation. Gap besar adalah tanda overfitting; keduanya buruk sekaligus adalah underfitting.
- Gambar learning curve: bagaimana error berubah seiring bertambahnya data dan iterasi training.
- Gunakan cross-validation supaya penilaian tidak bergantung pada satu pembagian data yang kebetulan mujur.
Kode berikut adalah ritual yang sekarang saya jalankan di hampir setiap eksperimen: latih, lalu bandingkan akurasi train dan validasi secara eksplisit.
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
X_tr, X_val, y_tr, y_val = train_test_split(
X, y, test_size=0.2, random_state=42
)
model = DecisionTreeClassifier(max_depth=None) # bebas dalam: rawan overfit
model.fit(X_tr, y_tr)
print("train :", accuracy_score(y_tr, model.predict(X_tr)))
print("valid :", accuracy_score(y_val, model.predict(X_val)))
# train 1.00, valid 0.61 --> gap lebar, overfitting
# coba max_depth=4 lalu bandingkan lagiLearning Curves: Visualisasi Overfitting
Cara paling intuitif untuk mendeteksi overfitting dan underfitting adalah dengan menggambar learning curves: grafik training loss dan validation loss seiring waktu training. Perilakunya memberi petunjuk langsung tentang diagnosis.
Pola yang saya hafalkan: jika training loss terus turun tapi validation loss mulai naik, itu overfitting. Jika keduanya tinggi dan tidak turun, itu underfitting. Jika keduanya turun lalu stabil dengan gap kecil, itu sweet spot. Persis seperti monitor CPU dan RAM di dashboard server — angka tunggal tidak bercerita, tapi grafik perubahannya jelas membaca kondisi sistem.
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import learning_curve
from sklearn.tree import DecisionTreeClassifier
def plot_learning_curve(estimator, X, y, title="Learning Curve"):
train_sizes, train_scores, val_scores = learning_curve(
estimator, X, y, cv=5,
train_sizes=np.linspace(0.1, 1.0, 10),
scoring="neg_mean_squared_error"
)
train_mean = -train_scores.mean(axis=1)
val_mean = -val_scores.mean(axis=1)
plt.figure(figsize=(8, 5))
plt.plot(train_sizes, train_mean, label="Training error")
plt.plot(train_sizes, val_mean, label="Validation error")
plt.xlabel("Jumlah data training")
plt.ylabel("MSE")
plt.title(title)
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
plot_learning_curve(
DecisionTreeClassifier(max_depth=None),
X, y, title="Decision Tree (tanpa batasan kedalaman)"
)Gambar ini langsung terlihat: training error hampir nol (model menghafal), tapi validation error jauh lebih tinggi. Gap lebar itu adalah sidik jari overfitting. Kode di atas saya simpan sebagai fungsi utilitas dan panggil setiap kali eksperimen baru — lebih cepat daripada menebak-nebak dari angka belaka.
Regularisasi: Menghukum Model yang Terlalu Pintar
Regularisasi adalah teknik menambahkan penalty ke loss function supaya parameter model tidak terlalu bebas. Ada tiga varian utama yang perlu diketahui:
L1 Regularization (Lasso) menambahkan penalty berupa nilai absolut dari parameter. Efeknya: beberapa parameter ditekan tepat ke nol, sehingga fitur yang tidak penting secara otomatis dihilangkan. Ini sangat berguna saat Anda suspect banyak fitur yang tidak relevan — mirip seperti grep untuk codebase: saring yang tidak perlu.
L2 Regularization (Ridge) menambahkan penalty berupa kuadrat dari parameter. Semua parameter ditekan lebih kecil, tapi tidak ada yang sampai nol. Efeknya: model tidak terlalu bergantung pada satu fitur tertentu. Ini adalah pilihan default yang aman untuk sebagian besar kasus.
Elastic Net menggabungkan L1 dan L2. Ketika Anda ragu apakah perlu seleksi fitur (L1) atau penekan bobot merata (L2), Elastic Net menawarkan keduanya dengan parameter yang bisa diatur rasionya.
from sklearn.linear_model import Ridge, Lasso, ElasticNet
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_tr)
X_val_scaled = scaler.transform(X_val)
# L2 (Ridge)
ridge = Ridge(alpha=1.0)
ridge.fit(X_train_scaled, y_tr)
print("Ridge val score:", ridge.score(X_val_scaled, y_val))
# L1 (Lasso) — bisa menghilangkan fitur
lasso = Lasso(alpha=0.1)
lasso.fit(X_train_scaled, y_tr)
print("Fitur yang masih hidup:", (lasso.coef_ != 0).sum())
# Elastic Net — gabungan L1 + L2
enet = ElasticNet(alpha=0.1, l1_ratio=0.5)
enet.fit(X_train_scaled, y_tr)
print("ElasticNet val score:", enet.score(X_val_scaled, y_val))Cara saya memilih: mulai dari Ridge (paling aman). Kalau fitur banyak dan suspect yang tidak berguna, coba Lasso. Kalau hasilnya tidak memuaskan, coba Elastic Net dengan l1_ratio dijajaki. Ini mirip seperti memilih antara sedikit dependensi yang tepat vs banyak dependensi kecil — keduanya valid, tergantung kodebasenya.
Cross-Validation: Tidak Bergantung pada Satu Keberuntungan
Pembagian train-test sekali hanya memberi satu angka. Angka itu bisa saja beruntung atau sial. Cross-validation memecah data menjadi k lipatan, lalu melatih dan mengevaluasi k kali sehingga kita mendapat rata-rata yang lebih stabil.
K-fold cross-validation membagi data secara acak menjadi k subset. Tiap iterasi, satu subset jadi validation, sisanya training. Stratified k-fold memastikan proporsi tiap kelas tetap konsisten di setiap lipatan — penting saat data tidak seimbang, misalnya 90% kelas negatif dan 10% kelas positif. Tanpa stratifikasi, bisa terjadi satu lipatan yang kebetulan berisi semua kelas mayoritas, sehingga metriknya palsu.
from sklearn.model_selection import (
cross_val_score, KFold, StratifiedKFold
)
# K-fold biasa
kf = KFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(
DecisionTreeClassifier(max_depth=4),
X, y, cv=kf, scoring="accuracy"
)
print(f"K-Fold: {scores.mean():.3f} ± {scores.std():.3f}")
# Stratified K-fold — menjaga proporsi kelas
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores_s = cross_val_score(
DecisionTreeClassifier(max_depth=4),
X, y, cv=skf, scoring="accuracy"
)
print(f"Stratified: {scores_s.mean():.3f} ± {scores_s.std():.3f}")
# std tinggi = model tidak stabil = tanda overfittingCatatan pribadi: std tinggi pada cross-validation score sama berharganya dengan mean-nya. Jika std besar, model Anda tidak stabil — hari ini akurasi tinggi, besok rendah. Itu red flag yang harus dijawab sebelum memikirkan optimasi lebih lanjut.
Early Stopping: Berhenti Sebelum Terlambat
Pada model iteratif seperti neural network, training terlalu lama adalah penyebab utama overfitting. Early stopping memantau error di validation set dan menghentikan training saat error mulai naik, meskipun error di training masih turun.
Analogi yang saya pakai: seperti mengerjakan tugas kuliah. Ada titik di mana revisi masih memperbaiki isi, tapi setelah titik tertentu, Anda hanya mengulang-ulang detail yang tidak mengubah kualitas — malah merusak fokus. Early stopping menangkap titik optimal itu secara otomatis.
import numpy as np
from sklearn.neural_network import MLPClassifier
# Simulasi early stopping manual
best_val_loss = float("inf")
patience = 5
wait = 0
best_model = None
mlp = MLPClassifier(
hidden_layer_sizes=(64, 32),
max_iter=1, warm_start=True, # train satu epoch per panggilan
random_state=42
)
for epoch in range(100):
mlp.fit(X_tr, y_tr)
val_loss = -mlp.score(X_val, y_val) # negatif supaya bisa di-min
if val_loss < best_val_loss:
best_val_loss = val_loss
best_model = (mlp.coefs_.copy(), mlp.intercepts_.copy())
wait = 0
else:
wait += 1
if wait >= patience:
print(f"Early stop di epoch {epoch}")
break
print(f"Best validation loss: {best_val_loss:.4f}")Kebanyakan framework sudah menyediakan EarlyStopping callback bawaan, termasuk PyTorch dan Keras. Tapi memahami logikanya secara manual membuat saya lebih percaya diri dalam mengatur hyperparameter seperti patience, min_delta, dan restore_best_weights.
Ensemble Methods: Kekuatan Berkelompok
Jika satu model overfit, coba gabungkan beberapa model. Inilah filosofi di balik ensemble methods. Prinsip dasarnya: banyak model yang masing-masing sedikit overfit, kalau dikombinasikan dengan benar, overfittingnya saling meniadakan.
Bagging (Bootstrap Aggregating) melatih banyak model secara paralel pada subset data yang diambil secara acak (dengan pengembalian), lalu menggabungkan prediksi mereka. Random Forest adalah implementasi paling populer: ratusan decision tree dilatih, masing-masing pada subset data DAN subset fitur secara acak, lalu hasilnya di-voting. Random Forest adalah salah satu model \”go-to\” saya karena tahan overfitting dan jarang memerlukan tuning yang rumit.
Boosting melatih model secara sekuensial: model berikutnya fokus pada kesalahan model sebelumnya. XGBoost, LightGBM, dan CatBoost adalah varian boosting yang mendominasi kompetisi Kaggle. Boosting lebih rentan overfitting dari bagging kalau tidak dikontrol, tapi kalau diatur dengan benar (learning rate, max_depth, n_estimators), hasilnya sangat kuat.
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
# Bagging via Random Forest
rf = RandomForestClassifier(
n_estimators=200, max_depth=6,
random_state=42
)
rf.fit(X_tr, y_tr)
print("RF train:", rf.score(X_tr, y_tr))
print("RF val :", rf.score(X_val, y_val))
# gap lebih kecil dari single tree
# Boosting
gb = GradientBoostingClassifier(
n_estimators=100, max_depth=3,
learning_rate=0.1, random_state=42
)
gb.fit(X_tr, y_tr)
print("GB train:", gb.score(X_tr, y_tr))
print("GB val :", gb.score(X_val, y_val))Perhatikan bagaimana gap antara training dan validation score pada ensemble jauh lebih kecil dibanding single decision tree yang kita coba di awal. Itulah kekuatan ensemble dalam melawan overfitting.
Data Augmentation: Memperbanyak Data Tanpa Pergi ke Lapangan
Cara paling jujur melawan overfitting adalah menambah data. Tapi mengumpulkan data baru itu mahal dan lambat. Data augmentation menawarkan alternatif: buat varian baru dari data yang sudah ada tanpa mengubah label.
Untuk citra: rotasi, flip, crop, perubahan kecerahan, atau penambahan noise. Untuk teks: synonym replacement, back-translation, atau random insertion. Untuk data tabular: SMOTE (Synthetic Minority Over-sampling Technique) yang menghasilkan sampel sintetis dari kelas minoritas.
from imblearn.over_sampling import SMOTE
import numpy as np
# Augmentasi data tabular via SMOTE
# Misal: kelas minoritas hanya 50 sampel vs 500 mayoritas
smote = SMOTE(random_state=42)
X_aug, y_aug = smote.fit_resample(X_tr, y_tr)
print(f"Sebelum: {len(X_tr)} sampel")
print(f"Sesudah: {len(X_aug)} sampel")
print(f"Distribusi: {np.bincount(y_aug)}")
# Untuk citra, contoh sederhana dengan numpy
def augment_image(img, noise_factor=0.1):
noise = np.random.normal(0, noise_factor, img.shape)
return np.clip(img + noise, 0, 1)Data augmentation terutama ampuh untuk dataset kecil. Ketika saya bekerja dengan dataset medis yang hanya punya ratusan sampel, augmentasi citra (rotasi + flip) meningkatkan akurasi validasi sekitar 5-8 poin persentase. Angka kecil, tapi di domain medis, angka itu berarti.
Kompleksitas Model vs Performa: Kurva U
Jika kita plot error terhadap kompleksitas model, hasilnya berbentuk huruf U. Di satu ujung (model terlalu sederhana) error tinggi karena underfitting. Di ujung lain (model terlalu kompleks) error validasi tinggi karena overfitting. Di tengah ada titik minimum — sweet spot di mana generalisasi terbaik.
Analogi dari dunia programming: menggunakan framework yang terlalu ringan untuk proyek kompleks (banyak manual wiring, error-prone) vs framework yang terlalu berat untuk proyek kecil (terlalu banyak konvensi, over-engineered). Keduanya membuat produktivitas menurun. Pilih yang pas ukurannya untuk masalahnya.
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
import matplotlib.pyplot as plt
depths = range(1, 25)
train_acc, val_acc = [], []
for d in depths:
clf = DecisionTreeClassifier(max_depth=d, random_state=42)
clf.fit(X_tr, y_tr)
train_acc.append(accuracy_score(y_tr, clf.predict(X_tr)))
val_acc.append(accuracy_score(y_val, clf.predict(X_val)))
plt.figure(figsize=(8, 5))
plt.plot(list(depths), train_acc, label="Training accuracy")
plt.plot(list(depths), val_acc, label="Validation accuracy")
plt.xlabel("Kedalaman Tree (kompleksitas)")
plt.ylabel("Akurasi")
plt.title("Model Complexity vs Performance")
plt.legend()
plt.grid(True)
plt.show()
# Cari titik di mana val accuracy maksimumGrafik ini yang saya gambar pertama kali saat memahami bahwa semakin kompleks model belum tentu semakin bagus. Setelah depth tertentu, training accuracy terus naik tapi validation accuracy mulai menurun. Itulah titik di mana model mulai menghafal alih-alih belajar.
Tabel Ringkasan Lengkap: Overfitting vs Underfitting
Tabel yang saya buat sendiri ini saya tempel di Notion sebagai cheat sheet. Lebih detail dari tabel sebelumnya, dengan penekanan pada gejala, penyebab, dan solusi konkret:
| Aspek | Underfitting (Bias Tinggi) | Overfitting (Varians Tinggi) |
|---|---|---|
| Gejala | Train error tinggi, val error tinggi, keduanya rapat | Train error rendah, val error tinggi, gap lebar |
| Penyebab | Model terlalu sederhana, fitur tidak informatif, training kurang lama | Model terlalu kompleks, data terlalu sedikit, training terlalu lama |
| Learning curve | Keduanya stagnan di level tinggi, gap kecil | Train turun terus, val mulai naik setelah titik tertentu |
| Regularisasi | Tidak diperlukan, bahkan kontra-produktif | Sangat membantu (L1, L2, Elastic Net) |
| Ensemble | Boosting lebih berguna (tambah kapasitas) | Bagging/RF lebih berguna (kurangi varians) |
| Data | Data augmentation kurang efektif tanpa perbaikan model | Data augmentation sangat efektif |
| Solusi utama | Tambah kompleksitas, fitur baru, training lebih lama | Regularisasi, early stopping, lebih banyak data, sederhanakan model |
Daftar Obat yang Saya Pakai
- Tambah data, kalau memungkinkan. Cara paling jujur dan paling mahal.
- Regularisasi L1/L2: menghukum bobot yang terlalu besar supaya model tidak bergantung pada pola rapuh.
- Sederhanakan model: batasi kedalaman tree, kurangi jumlah layer atau neuron.
- Early stopping: berhenti training sebelum model mulai menghafal.
- Dropout untuk neural network: mematikan neuron secara acak saat training supaya tidak ada yang jadi \”single point of failure\”.
- Ensemble methods: gabungkan banyak model supaya error saling meniadakan.
- Data augmentation: perbanyak data tanpa biaya pengumpulan.
- Untuk underfitting: arah sebaliknya — model lebih kompleks, fitur lebih baik, training lebih lama.
Catatan Pribadi: Momen Pertama Sadar Model Overfit
Momen yang tidak pernah saya lupa adalah kali pertama saya sadar bahwa model saya overfit — bukan karena membaca teori, tapi karena merasakan sendiri sakitnya. Saat itu saya sedang membuat model untuk memprediksi churn pelanggan. Data hanya 200 baris, tapi saya pakai random forest dengan 500 pohon dan tidak ada regularisasi sama sekali. Akurasi di training: 100 persen sempurna. Saya kira ini pencapaian luar biasa.
Begitu saya deploy model tersebut dan prediksi mulai berantakan di production, saya baru belajar dengan cara yang paling menyakitkan: model yang sempurna di data training belum tentu berguna. Sejak saat itu, saya selalu mulai dengan model sederhana, cek learning curve dulu, dan baru naikkan kompleksitas secara bertahap. Filosofi itu ternyata juga berlaku di software engineering: mulai dari yang paling sederhana yang bisa bekerja, baru kompleksifikasi saat ada bukti kebutuhan.
Bagian yang paling perlu saya terima sebagai orang yang suka optimasi: tidak ada model yang bebas dari kedua penyakit ini. Menekan varians hampir selalu menaikkan bias sedikit, dan sebaliknya. Yang penting bukan mengejar nol error, tapi tahu posisi Anda di kurva tradeoff, dan punya alat untuk bergeser ke arah yang tepat saat metrik bilang perlu.
Komentar Terbaru