Model yang kita pilih masih menyimpan pilihan tersembunyi: seberapa dalam sebuah pohon boleh tumbuh, kriteria pemisahan apa yang dipakai, berapa banyak tetangga yang boleh dilihat. Pilihan-pilihan ini — hyperparameter — tidak dipelajari dari data, melainkan kita tentukan sebelum pelatihan dimulai, dan nilai yang tepat bisa menggeser performa jauh. Hyperparameter tuning adalah proses mencari kombinasi terbaik secara sistematis, bukan menebak-nebak satu per satu sambil berharap kebetungan. Pada chapter ini kita pisahkan dulu parameter dari hyperparameter, lalu gunakan GridSearchCV untuk menguji grid kecil secara adil dengan validasi silang.

Parameter Model dan Hyperparameter Berbeda #

Kedua istilah ini sering tertukar padahal berdiri di sisi yang berlawanan. Parameter adalah nilai yang dipelajari model dari data selama fit() berjalan; hyperparameter adalah pengaturan yang kita tetapkan sebelum fit() dipanggil.

AspekParameterHyperparameter
Kapan ditentukanOtomatis, selama pelatihan berjalanManual, sebelum pelatihan dimulai
Contoh pada pohon keputusanAmbang pemisah tiap simpul, fitur yang dipakaimax_depth, criterion, min_samples_leaf
Cara memperoleh nilaiDipelajari dari data latihDicari lewat eksperimen (tuning)
PerilakunyaBerganti setiap kali data atau pengaturan latih bergantiTetap sampai kita mengubahnya

Kode singkat berikut memperlihatkan batas keduanya pada pohon keputusan yang memprediksi pelanggan membeli menu musiman (fitur: kunjungan per bulan dan poin loyalitas; target beli, 24 dari 60 pelanggan):

python
import numpy as np
from sklearn.tree import DecisionTreeClassifier

rng = np.random.default_rng(42)
n = 60
kunjungan = rng.integers(1, 15, n)
poin = rng.integers(0, 500, n)
minat = 0.6 * kunjungan + 0.02 * poin + rng.normal(0, 2, n)
beli = (minat > np.quantile(minat, 0.6)).astype(int)
X = np.column_stack([kunjungan, poin])
fitur = ["kunjungan", "poin"]

print("kelas beli:", beli.sum(), "dari", n)

model = DecisionTreeClassifier(max_depth=3, criterion="entropy", random_state=42)
print("sebelum fit -- max_depth:", model.max_depth, "| criterion:", model.criterion)

model.fit(X, beli)
print("setelah fit -- jumlah simpul:", model.tree_.node_count)
print("setelah fit -- ambang pemisah di akar:", model.tree_.threshold[0])
print("setelah fit -- fitur di akar:", fitur[model.tree_.feature[0]])

Output yang Dihasilkan:

text
kelas beli: 24 dari 60
sebelum fit -- max_depth: 3 | criterion: entropy
setelah fit -- jumlah simpul: 13
setelah fit -- ambang pemisah di akar: 279.5
setelah fit -- fitur di akar: poin

Perhatikan pembagiannya: max_depth=3 dan criterion="entropy" kita tulis sendiri di konstruktur — keduanya hyperparameter, nilainya sudah ada sebelum data disentuh. Sebaliknya, jumlah simpul (13), ambang pemisah di akar (279,5), dan fitur yang terpilih di akar (poin) baru muncul setelah fit() — itulah parameter yang dipelajari model dari data. Jika kita mengganti max_depth menjadi 4, hyperparameter berubah dan seluruh parameter akan dipelajari ulang; jika hanya datanya berganti, parameter ikut berganti sementara hyperparameter menunggu kita menyentuhnya.

GridSearchCV: Menguji Kombinasi Secara Sistematis #

Cara paling naif mencari hyperparameter adalah menguji satu nilai, lihat skornya, ganti nilai, ulangi — pada data uji. Cara ini merusak evaluasi karena data uji pelan-pelan berubah fungsi menjadi data latih kedua. GridSearchCV menuntaskannya dengan benar: ia mendefinisikan grid kombinasi, menguji setiap kombinasi dengan cross-validation, dan melaporkan yang terbaik. Kita uji tiga kedalaman dan dua kriteria — enam kombinasi, grid kecil yang cukup untuk melihat polanya:

python
import numpy as np
import pandas as pd
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import GridSearchCV

rng = np.random.default_rng(42)
n = 60
kunjungan = rng.integers(1, 15, n)
poin = rng.integers(0, 500, n)
minat = 0.6 * kunjungan + 0.02 * poin + rng.normal(0, 2, n)
beli = (minat > np.quantile(minat, 0.6)).astype(int)
X = np.column_stack([kunjungan, poin])

grid = {"max_depth": [2, 4, 8], "criterion": ["gini", "entropy"]}
cari = GridSearchCV(DecisionTreeClassifier(random_state=42), grid, cv=5)
cari.fit(X, beli)

hasil = pd.DataFrame({
    "max_depth": [p["max_depth"] for p in cari.cv_results_["params"]],
    "criterion": [p["criterion"] for p in cari.cv_results_["params"]],
    "skor_rerata": cari.cv_results_["mean_test_score"].round(3),
    "simpangan": cari.cv_results_["std_test_score"].round(3),
}).sort_values(["skor_rerata", "max_depth"], ascending=[False, True])

print("total kombinasi diuji:", len(cari.cv_results_["params"]))
print()
print(hasil.to_string(index=False))
print()
print("best_params_:", cari.best_params_)
print("best_score_ :", round(cari.best_score_, 3))

Output yang Dihasilkan:

text
total kombinasi diuji: 6

 max_depth criterion  skor_rerata  simpangan
         4   entropy        0.900      0.082
         4      gini        0.883      0.113
         8   entropy        0.817      0.082
         2      gini        0.800      0.085
         2   entropy        0.800      0.067
         8      gini        0.783      0.113

best_params_: {'criterion': 'entropy', 'max_depth': 4}
best_score_ : 0.9

Mari kita telaah bagian-bagian pentingnya:

  1. Baris 14-15 (grid): dictionary ini mendefinisikan ruang pencarian — hasil kartesiusnya adalah 3 × 2 = 6 kombinasi. GridSearchCV akan melatih dan menilai keenamnya, masing-masing dengan cv=5, sehingga total 30 pelatihan pohon kecil.
  2. Baris 17-24 (tabel hasil): cv_results_ menyimpan skor setiap kombinasi; kita susun ulang jadi tabel berisi rata-rata dan simpangan skor lipatan, diurutkan dari terbaik. Kolom simpangan menunjukkan seberapa stabil kombinasi itu antar lipatan.
  3. Baris 29-30 (best_params_ dan best_score_): ringkasan pemenang — kedalaman 4 dengan kriteria entropy.

Pola pada tabelnya sendiri adalah pelajaran overfitting yang nyata: kedalaman 4 menang (0,900), tetapi membiarkan pohon tumbuh penuh sampai kedalaman 8 justru menurunkan skor ke 0,783-0,817. Pohon yang terlalu dalam menghafal data latih termasuk deraunya, sehingga gagal memprediksi lipatan validasi; kedalaman 2 kurang cukup untuk menangkap pola (0,800). Titik tengahnya menang — persis kisah bias-variance dari chapter Overfitting dan Underfitting, kini dalam bentuk angka pencarian hyperparameter.

Protokol yang Jujur: Tuning di Data Latih, Data Uji Tetap Terpisah #

GridSearchCV memakai validasi silang, tetapi tahap ini masih menempel pada data. Protokol yang jujur menyusunnya tiga lapis: pisahkan data uji lebih dulu, jalankan pencarian grid hanya pada data latih, lalu ukur kombinasi terbaik pada data uji yang belum pernah disentuh:

python
import numpy as np
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import GridSearchCV, train_test_split

rng = np.random.default_rng(42)
n = 60
kunjungan = rng.integers(1, 15, n)
poin = rng.integers(0, 500, n)
minat = 0.6 * kunjungan + 0.02 * poin + rng.normal(0, 2, n)
beli = (minat > np.quantile(minat, 0.6)).astype(int)
X = np.column_stack([kunjungan, poin])

X_latih, X_uji, y_latih, y_uji = train_test_split(
    X, beli, test_size=0.25, stratify=beli, random_state=42)

grid = {"max_depth": [2, 4, 8], "criterion": ["gini", "entropy"]}
cari = GridSearchCV(DecisionTreeClassifier(random_state=42), grid, cv=5)
cari.fit(X_latih, y_latih)

print("kombinasi terbaik :", cari.best_params_)
print("skor CV terbaik   :", round(cari.best_score_, 3))

model_tuning = cari.best_estimator_
print("akurasi data uji  :", round(model_tuning.score(X_uji, y_uji), 3))

model_tanpa_tuning = DecisionTreeClassifier(random_state=42).fit(X_latih, y_latih)
print("pohon tanpa batas:", round(model_tanpa_tuning.score(X_uji, y_uji), 3))

Output yang Dihasilkan:

text
kombinasi terbaik : {'criterion': 'gini', 'max_depth': 4}
skor CV terbaik   : 0.889
akurasi data uji  : 0.8
pohon tanpa batas: 0.733

Tiga hal patut dicermati dari protokol ini. Pertama, cari.fit(X_latih, y_latih) — pencarian grid tidak pernah melihat data uji; kelima lipatan validasi dibangun dari 45 baris data latih saja. Kedua, best_estimator_ sudah dilatih ulang pada seluruh data latih dengan kombinasi terbaik, siap diuji pada 15 baris data uji. Ketiga, hasilnya membenarkan upayanya: pohon hasil tuning mencatat 0,800 pada data uji, sementara pohon yang dibiarkan tumbuh tanpa batas hanya 0,733. Perhatikan juga kombinasi terbaiknya bergeser (gini, bukan entropy) dibanding pencarian pada data penuh — wajar, karena data yang dinilai berbeda; inilah pentingnya menilai pada data yang benar-benar terpisah.

Rangkuman #

  • Parameter dipelajari model selama fit(); hyperparameter (max_depth, criterion, dan kawan-kawannya) kita atur sebelum pelatihan dan harus dicari lewat eksperimen.
  • GridSearchCV menguji setiap kombinasi grid dengan cross-validation, lalu merangkumnya di cv_results_, best_params_, dan best_score_.
  • Pada contoh kita, kedalaman 4 menang (0,900) sementara kedalaman 8 justru terpuruk (0,783) — tuning hyperparameter adalah pengendalian overfitting dalam praktik.
  • Protokol jujur: data uji dipisahkan lebih dulu, grid search berjalan hanya pada data latih, dan best_estimator_ diuji sekali di data uji.
  • Grid raksasa berisiko menghafal validation set; untuk ruang besar, RandomizedSearchCV adalah alternatif yang lebih hemat.

Dengan model yang kini dikonfigurasi optimal, satu skenario masih sanggup meruntuhkan semua angka yang telah kita hitung: data yang timpang, di mana kelas minoritas hanya lima persen dari baris. Chapter berikutnya, Evaluasi Model pada Data Imbalanced, membedah mengapa akurasi 95% bisa berarti sama sekali buruk.

Referensi #

  1. scikit-learn — Model selection: cross-validation
  2. scikit-learn — Metrics and scoring: model evaluation
  3. scikit-learn — train_test_split