Menyempurnakan model secara disiplin: cross-validation k-fold dan stratified, validation curves untuk membaca bias-variance, lalu tuning hyperparameter dengan GridSearchCV, RandomizedSearchCV, Optuna, dan early stopping agar evaluasi jujur dan reproduksibel.

Setelah di episode 9 kita membangun dan memilih fitur, pada episode ini kita menjawab pertanyaan yang selalu mengikuti: model mana yang terbaik, dan dengan parameter apa? Jawabannya tidak bisa "kira-kira" — karena keputusan yang salah di sini membuat model overfit dan menipu evaluasi.
Mengapa topik ini krusial? Karena tuning adalah tempat kalian bisa membuang waktu berhari-hari jika tidak disiplin. Dengan cross-validation yang benar, strategi pencarian yang tepat, dan pemahaman bias-variance, kalian bisa mencapai performa optimal dengan cara yang terukur dan jujur.
Ingat pelajaran episode 2: test set hanya disentuh sekali di akhir. Lalu bagaimana cara memilih hyperparameter tanpa menyentuh test set? Jawabannya: cross-validation — membagi train set menjadi beberapa lipatan (fold) dan menguji model bergantian.
Dengan k=5, train set dipecah menjadi 5 bagian. Model dilatih pada 4 bagian, dievaluasi pada 1 bagian tersisa, dan ini berulang 5 kali sehingga setiap bagian pernah menjadi evaluasi. Hasil akhir: rata-rata dan standar deviasi dari 5 skor.
from sklearn.model_selection import cross_val_score
scores = cross_val_score(
pipeline, X_train, y_train,
cv=5, scoring="roc_auc",
)
print(f"CV mean = {scores.mean():.4f} ± {scores.std():.4f}")Perhatikan ± std: jika varian antar fold besar, performa model tidak stabil — menandakan model terlalu sensitif terhadap data tertentu.
Untuk classification dengan data tidak seimbang, gunakan StratifiedKFold: setiap fold menjaga proporsi kelas yang sama dengan data penuh. Ini menghindari fold yang tidak punya kelas minoritas sama sekali — yang akan merusak evaluasi.
from sklearn.model_selection import StratifiedKFold
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(pipeline, X_train, y_train, cv=cv, scoring="roc_auc")shuffle=True dengan random_state penting agar pembagian fold deterministik — bagian dari reproducibility (episode 2).
Validation curve menunjukkan bagaimana performa berubah saat satu hyperparameter naik, pada train dan validation secara bersamaan:
import numpy as np
from sklearn.model_selection import validation_curve
from sklearn.ensemble import RandomForestClassifier
param_range = np.arange(1, 21)
train_scores, val_scores = validation_curve(
RandomForestClassifier(random_state=42),
X_train, y_train,
param_name="max_depth",
param_range=param_range,
cv=5, scoring="roc_auc",
)
val_mean = val_scores.mean(axis=1)
best_depth = param_range[np.argmax(val_mean)]
print(f"max_depth terbaik = {best_depth}")Interpretasi kurva inilah inti bias-variance trade-off:
| Pola kurva | Diagnosis | Tindakan |
|---|---|---|
| Train rendah, val rendah | Underfit (bias tinggi) | Model terlalu sederhana — tambah kapasitas |
| Train tinggi, val rendah | Overfit (variance tinggi) | Model hafal data latih — kurangi kapasitas / regularisasi |
| Train & val tinggi & berdekatan | Seimbang | Zona ideal |
Kurva kalian idealnya menunjukkan titik di mana performa validation memuncak — di sanalah max_depth terbaik berada, bukan di titik train tertinggi.
Grid search mencoba semua kombinasi parameter secara sistematis:
from sklearn.model_selection import GridSearchCV
param_grid = {
"model__max_depth": [4, 8, 12],
"model__n_estimators": [100, 300],
"model__min_samples_leaf": [1, 5],
}
search = GridSearchCV(
pipeline, param_grid,
cv=5, scoring="roc_auc", n_jobs=-1,
)
search.fit(X_train, y_train)
print(search.best_params_)
print(f"CV terbaik = {search.best_score_:.4f}")
print(f"Test = {search.score(X_test, y_test):.4f}")Perhatikan model__max_depth (dua underscore) — ini cara menyebut parameter estimator di dalam Pipeline. Kelemahan grid search: kombinasi tumbuh eksponensial. Contoh di atas saja sudah 3×2×2×5 = 60 kali training.
Random search mengambil sampel acak dari ruang parameter dalam jumlah iterasi tetap. Hasilnya sering lebih baik daripada grid search untuk ruang berdimensi banyak, dengan biaya yang sama.
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import randint, uniform
param_dist = {
"model__max_depth": randint(3, 20),
"model__n_estimators": randint(100, 800),
"model__learning_rate": uniform(0.01, 0.3),
}
search = RandomizedSearchCV(
pipeline, param_dist,
n_iter=50, cv=5, scoring="roc_auc",
random_state=42, n_jobs=-1,
)
search.fit(X_train, y_train)Dengan scipy.stats kita memberi distribusi, bukan daftar nilai — sehingga ruang pencarian jauh lebih halus.
Warning
Grid/random search mengoptimalkan skor CV di data train. Risiko tersembunyi: jika data sangat tidak seimbang, gunakan metrik CV yang sesuai (roc_auc, f1, precision) — jangan accuracy yang bisa menyesatkan.
Optuna adalah framework tuning modern berbasis Bayesian optimization: ia belajar dari percobaan sebelumnya dan menebak area parameter yang paling menjanjikan, bukan sekadar mencoba acak.
import optuna
from sklearn.model_selection import cross_val_score
from sklearn.ensemble import RandomForestClassifier
def objective(trial):
params = {
"max_depth": trial.suggest_int("max_depth", 3, 20),
"n_estimators": trial.suggest_int("n_estimators", 100, 800),
"min_samples_leaf": trial.suggest_int("min_samples_leaf", 1, 20),
"max_features": trial.suggest_float("max_features", 0.1, 1.0),
}
model = RandomForestClassifier(**params, random_state=42, n_jobs=-1)
return cross_val_score(model, X_train, y_train, cv=5,
scoring="roc_auc").mean()
study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=50)
print(study.best_params)
print(f"CV terbaik = {study.best_value:.4f}")Optuna biasanya menemukan parameter yang sama bagusnya dengan grid search dalam jauh lebih sedikit percobaan — terutama saat ruang parameter besar.
Early stopping adalah cara berbeda untuk menangani n_estimators pada boosting: berhenti saat validation error tidak membaik dalam N iterasi, alih-alih menebak angka estimasi:
import lightgbm as lgb
model = lgb.LGBMClassifier(
n_estimators=5000,
learning_rate=0.05,
random_state=42,
)
model.fit(
X_train, y_train,
eval_set=[(X_val, y_val)],
eval_metric="auc",
callbacks=[lgb.early_stopping(50)],
)Model berhenti pada iterasi optimal secara otomatis — tidak perlu tuning n_estimators manual.
Poin pentingnya: test set tetap suci. Semua keputusan tuning memakai CV di train set; test set hanya untuk konfirmasi terakhir.
random_state pada search dan CV — hasil tidak reproduksibel.Pada episode 10 ini, kalian telah menguasai evaluasi dan tuning yang disiplin.
Inti yang harus dibawa pulang:
Di episode 11 selanjutnya kita memasuki fase baru: experiment tracking & reproducibility — logging metric dan artifact dengan MLflow, perbandingan dengan Weights & Biases, seed/hashing untuk reproducibility, dan eksperimen berbasis konfigurasi. Sampai jumpa di episode 11!