Akurasi model kamu mentok di 0.93 dan sudah seminggu tidak naik-naik. Kamu ganti n_estimators jadi 300, naik tipis. Kamu ubah max_depth jadi 20, malah turun. Begitu terus sampai bosan dan kamu menerima hasil apa adanya. Kalau cara tuning kamu masih begini, ada alat yang jauh lebih rapi: Optuna. Library Python ini mencari kombinasi hyperparameter terbaik secara otomatis, bukan dengan menebak acak, tapi dengan belajar dari trial-trial sebelumnya.

Kalau kamu belum pernah membangun model klasifikasi sama sekali, mulai dulu dari Membuat Model Klasifikasi Pertama dengan Python (Scikit-learn). Di sana saya jelaskan cara melatih model sederhana. Artikel ini berangkat dari titik itu: modelnya sudah jalan, sekarang kita cari parameter terbaiknya.

Hyperparameter vs parameter: kenapa ruang pencariannya bisa meledak

Model seperti Random Forest punya dua jenis pengaturan. Parameter adalah bobot yang dipelajari model dari data, misalnya ambang split di tiap pohon. Hyperparameter adalah pengaturan yang kamu tentukan sebelum training, seperti jumlah pohon (n_estimators) atau kedalaman maksimum (max_depth). Masalahnya, hyperparameter saling mempengaruhi. Kombinasi max_depth=10 bisa bagus di n_estimators=100 tapi jelek di n_estimators=300. Kalau tiap hyperparameter punya 10 nilai, dua hyperparameter saja sudah 100 kombinasi. Enam hyperparameter? Jutaan. Tidak mungkin dicoba satu-satu.

Install Optuna

Optuna butuh Python 3.8 ke atas. Install lewat pip:

bash
pip install optuna scikit-learn plotly

plotly dipakai untuk visualisasi hasil, bukan wajib untuk tuning-nya sendiri. Kalau kamu tidak butuh grafik, cukup optuna dan scikit-learn.

Siapkan masalahnya

Kita pakai dataset breast cancer bawaan scikit-learn. Tujuannya klasifikasi biner: membedakan tumor ganas dan jinak dari 30 fitur numerik. Dataset ini kecil, cuma 569 baris, jadi tiap trial berjalan cepat — pas untuk latihan tuning. Sebelum tuning, biasakan eksplorasi data dulu, cek distribusi target dan nilai kosong. Kalau belum terbiasa, baca Pandas untuk Analisis Data dengan Python.

Tulis objective function

Inti Optuna adalah satu fungsi: objective(trial). Fungsi ini menerima objek trial, mengambil saran nilai hyperparameter lewat method suggest_*, melatih model, lalu mengembalikan skor yang mau kita maksimalkan. Optuna yang bertugas memilih nilai untuk tiap trial berikutnya.

python
import optuna
from sklearn.datasets import load_breast_cancer
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score

data = load_breast_cancer()
X, y = data.data, data.target

def objective(trial):
    n_estimators = trial.suggest_int("n_estimators", 50, 400)
    max_depth = trial.suggest_int("max_depth", 3, 30)
    min_samples_split = trial.suggest_int("min_samples_split", 2, 20)
    min_samples_leaf = trial.suggest_int("min_samples_leaf", 1, 10)
    max_features = trial.suggest_categorical("max_features", ["sqrt", "log2", None])
    criterion = trial.suggest_categorical("criterion", ["gini", "entropy"])

    model = RandomForestClassifier(
        n_estimators=n_estimators,
        max_depth=max_depth,
        min_samples_split=min_samples_split,
        min_samples_leaf=min_samples_leaf,
        max_features=max_features,
        criterion=criterion,
        random_state=42,
        n_jobs=-1,
    )

    score = cross_val_score(model, X, y, cv=5, scoring="accuracy")
    return score.mean()

suggest_int mengambil bilangan bulat dalam rentang yang kita kasih, suggest_categorical mengambil salah satu dari daftar nilai. Perhatikan max_features=None itu valid — artinya Random Forest memakai semua fitur. Skor dihitung lewat cross_val_score dengan 5-fold supaya hasilnya stabil, tidak kebetulan bagus hanya di satu split.

Jalankan study-nya

python
optuna.logging.set_verbosity(optuna.logging.WARNING)

study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=50)

print("Akurasi terbaik:", study.best_value)
print("Parameter terbaik:", study.best_params)

direction="maximize" berarti kita mau skor setinggi mungkin; untuk metrik error seperti MSE, pakai "minimize". n_trials=50 artinya Optuna menjalankan objective 50 kali dengan kombinasi berbeda. set_verbosity(WARNING) cuma meredam log per trial supaya terminalmu tidak banjir.

Mulai dari n_trials kecil dulu, misalnya 20, untuk memastikan objective function-mu benar. Setelah yakin tidak ada bug, naikkan ke 100 atau lebih. Trial yang gagal di tengah tidak merusak hasil, tapi buang waktu.

Apa yang Optuna lakukan di balik layar

Sampler default Optuna bernama TPE (Tree-structured Parzen Estimator), bagian dari optimasi Bayesian. Trial pertama diambil hampir acak. Setelah itu, TPE membagi hasil yang sudah ada jadi dua kelompok — yang bagus dan yang jelek — lalu menebak wilayah parameter yang lebih mungkin menghasilkan skor bagus. Makanya makin banyak trial, tebakannya makin tajam. Ini beda dengan GridSearchCV yang mencoba semua kombinasi secara buta atau RandomizedSearchCV yang murni acak.

TPE bekerja bagus untuk pencarian 10 sampai 100 trial. Kalau ruang pencarianmu besar dan budget waktumu ratusan trial, ganti sampler ke optuna.samplers.NSGAIISampler atau CMAESSampler lewat argumen sampler= di create_study.

Pruning: hentikan trial yang jelas jelek

Random Forest dengan 400 pohon lebih lambat dari 50 pohon. Kalau ada trial yang sejak fold pertama sudah jelek, buat apa diteruskan? Optuna bisa memangkas (prune) trial di tengah jalan lewat trial.report() dan trial.should_prune().

python
from optuna.pruners import MedianPruner
from sklearn.model_selection import StratifiedKFold

def objective_pruned(trial):
    n_estimators = trial.suggest_int("n_estimators", 50, 400)
    max_depth = trial.suggest_int("max_depth", 3, 30)
    min_samples_split = trial.suggest_int("min_samples_split", 2, 20)
    min_samples_leaf = trial.suggest_int("min_samples_leaf", 1, 10)

    model = RandomForestClassifier(
        n_estimators=n_estimators,
        max_depth=max_depth,
        min_samples_split=min_samples_split,
        min_samples_leaf=min_samples_leaf,
        random_state=42,
        n_jobs=-1,
    )

    skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
    scores = []
    for step, (train_idx, val_idx) in enumerate(skf.split(X, y)):
        model.fit(X[train_idx], y[train_idx])
        score = model.score(X[val_idx], y[val_idx])
        scores.append(score)

        trial.report(score, step)
        if trial.should_prune():
            raise optuna.TrialPruned()

    return sum(scores) / len(scores)

study = optuna.create_study(
    direction="maximize",
    pruner=MedianPruner(n_startup_trials=5, n_warmup_steps=2),
)
study.optimize(objective_pruned, n_trials=50)

Di sini kita melatih manual per fold supaya bisa melaporkan skor bertahap. MedianPruner membandingkan skor trial yang sedang berjalan dengan median trial lain di step yang sama; kalau di bawah median, trial dipangkas. n_startup_trials=5 membiarkan 5 trial pertama jalan penuh supaya ada baseline yang adil.

Lihat hasilnya dalam grafik

python
import optuna.visualization as vis

vis.plot_optimization_history(study).show()
vis.plot_param_importances(study).show()

Grafik pertama menunjukkan skor terbaik yang makin lama makin naik, tanda pencarian berjalan sehat. Grafik kedua memeringkat hyperparameter berdasarkan pengaruhnya terhadap skor — biasanya max_depth dan n_estimators yang paling berpengaruh di Random Forest. Grafik ini butuh Plotly, makanya tadi kita install.

Simpan study dan pakai parameter terbaik

Proses tuning bisa makan waktu menit sampai jam. Jangan buang hasilnya. Simpan study ke file supaya bisa dimuat ulang nanti:

python
import joblib

joblib.dump(study, "optuna_study.pkl")

# di sesi lain
study_lagi = joblib.load("optuna_study.pkl")
print(study_lagi.best_params)

Terakhir, latih model final pakai parameter terbaik dan ukur di data test yang belum pernah disentuh tuning:

python
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

best_model = RandomForestClassifier(**study.best_params, random_state=42, n_jobs=-1)
best_model.fit(X_train, y_train)

y_pred = best_model.predict(X_test)
print("Akurasi di data test:", accuracy_score(y_test, y_pred))

Dari sini kamu bisa eksplorasi sendiri: ganti dataset, ganti model ke XGBoost atau LightGBM, atau tambah hyperparameter baru di objective. Kalau kamu mulai tuning banyak eksperimen, catat hasilnya biar tidak hilang — Melacak Eksperimen Machine Learning dengan MLflow membahas cara menyimpan parameter, metrik, dan model tiap eksperimen dalam satu tempat.