Setelah memahami bahwa hyperparameter harus dicoba, pertanyaan berikutnya adalah: dicoba dengan cara apa? Grid search menjawabnya dengan pendekatan paling jujur dan paling sederhana: daftarkan semua kandidat nilai yang layak, evaluasi setiap kombinasi lewat cross-validation, lalu ambil yang terbaik. scikit-learn membungkus seluruh proses itu dalam satu kelas GridSearchCV — termasuk langkah akhir yang sering luput: melatih ulang model juara pada seluruh data latih. Chapter ini membongkar mekanismenya, membaca atribut hasilnya, dan memverifikasi bahwa hasilnya memang sama dengan perulangan manual yang kita tulis sendiri.

Cara Kerja Pencarian Menyeluruh #

GridSearchCV menerima param_grid, sebuah kamus yang memetakan nama hyperparameter ke daftar kandidat nilainya. Dari kamus itu ia membangun hasil kali Kartesius: grid {"max_depth": [3, 5, 7], "min_samples_leaf": [1, 5, 10]} menghasilkan 3 × 3 = 9 konfigurasi. Setiap konfigurasi dievaluasi dengan cross-validation pada data latih, dan kombinasi dengan rata-rata skor tertinggi menjadi pemenang. Tidak ada konfigurasi yang terlewat — itulah arti “menyeluruh”.

Bentuk kamus tunggal mengasumsikan semua hyperparameter relevan untuk semua konfigurasi. Untuk ruang bersyarat, param_grid juga menerima daftar kamus: setiap kamus dijelajahi terpisah, lalu hasilnya digabung. Pola ini penting saat hyperparameter tertentu hanya bermakna jika hyperparameter lain bernilai tertentu:

python
param_grid = [
    {"kernel": ["linear"], "C": [0.1, 1, 10]},
    {"kernel": ["rbf"], "C": [0.1, 1, 10], "gamma": [0.01, 0.1]},
]

Daftar di atas menghasilkan 3 konfigurasi linier plus 6 konfigurasi RBF. Tanpa bentuk daftar, kombinasi kernel="linear" dengan gamma tertentu akan membuat galat karena SVC menolak gamma saat kernel-nya linier.

Empat Atribut Hasil yang Wajib Dibaca #

Setelah fit(), objek GridSearchCV menyimpan hasilnya di empat tempat utama. best_params_ berisi kamus konfigurasi juara; best_score_ berisi rata-rata skor cross-validation konfigurasi itu; best_estimator_ berisi estimator yang sudah dilatih ulang pada seluruh data latih; dan cv_results_ berisi tabel lengkap seluruh konfigurasi — kolom mean_test_score, std_test_score, dan rank_test_score adalah tiga kolom yang paling sering kita baca.

Contoh Kode: Grid Penuh, Verifikasi Manual #

Skrip berikut menjalankan grid 3 × 3 pada pohon keputusan, mencetak ringkasan cv_results_, lalu memverifikasi setiap angkanya dengan perulangan manual cross_val_score — pola verifikasi yang menunjukkan bahwa hasil otomatisasi tidak berbeda dari perulangan yang kita tulis sendiri.

python
# GridSearchCV menjelajahi SELURUH kombinasi hyperparameter dalam param_grid,
# lalu hasilnya diverifikasi dengan perulangan manual cross_val_score.

import numpy as np
from sklearn.datasets import make_classification
from sklearn.model_selection import (GridSearchCV, StratifiedKFold,
                                     cross_val_score, train_test_split)
from sklearn.tree import DecisionTreeClassifier

X, y = make_classification(n_samples=200, n_features=8, n_informative=5,
                           n_redundant=0, n_clusters_per_class=1, random_state=42)
X_latih, X_uji, y_latih, y_uji = train_test_split(X, y, test_size=0.3,
                                                   stratify=y, random_state=42)

param_grid = {"max_depth": [3, 5, 7], "min_samples_leaf": [1, 5, 10]}
cv = StratifiedKFold(n_splits=3, shuffle=True, random_state=42)

n_kombinasi = 1
for nilai in param_grid.values():
    n_kombinasi *= len(nilai)
print(f"Jumlah kombinasi      : {n_kombinasi} (3 x 3)")
print(f"Total pemanggilan fit : {n_kombinasi * cv.n_splits} (9 kombinasi x 3 lipatan)")

grid = GridSearchCV(DecisionTreeClassifier(random_state=42), param_grid, cv=cv)
grid.fit(X_latih, y_latih)

print(f"\nbest_params_  : {grid.best_params_}")
print(f"best_score_   : {grid.best_score_:.4f} (rata-rata 3 lipatan CV)")
print(f"Akurasi uji   : {grid.best_estimator_.score(X_uji, y_uji):.3f} "
      "(best_estimator_ sudah refit pada seluruh data latih)")

hasil = grid.cv_results_
indeks = {(p["max_depth"], p["min_samples_leaf"]): i
          for i, p in enumerate(hasil["params"])}

print("\n== cv_results_: 4 peringkat teratas ==")
print(f"{'max_depth':>10} {'min_samples_leaf':>17} {'mean':>7} {'std':>6} {'rank':>5}")
for i in np.argsort(hasil["rank_test_score"])[:4]:
    p = hasil["params"][i]
    print(f"{p['max_depth']:>10} {p['min_samples_leaf']:>17} "
          f"{hasil['mean_test_score'][i]:>7.3f} {hasil['std_test_score'][i]:>6.3f} "
          f"{hasil['rank_test_score'][i]:>5}")

# Pola verifikasi: perulangan manual cross_val_score HARUS cocok dengan mean_test_score.
print("\n== Verifikasi manual: cross_val_score per konfigurasi ==")
n_identik = 0
for kedalaman in param_grid["max_depth"]:
    for min_daun in param_grid["min_samples_leaf"]:
        est = DecisionTreeClassifier(max_depth=kedalaman,
                                     min_samples_leaf=min_daun, random_state=42)
        skor_manual = cross_val_score(est, X_latih, y_latih, cv=cv).mean()
        skor_grid = hasil["mean_test_score"][indeks[(kedalaman, min_daun)]]
        sama = abs(skor_manual - skor_grid) < 1e-12
        n_identik += int(sama)
        print(f"  depth={kedalaman} leaf={min_daun:>2}: "
              f"manual={skor_manual:.4f} grid={skor_grid:.4f} "
              f"{'IDENTIK' if sama else 'BEDA!'}")
print(f"Konfigurasi identik   : {n_identik}/{n_kombinasi}")

# Aritmetika pertumbuhan kombinasi (hitungan saja, tidak dieksekusi):
komb_besar = 3 * 4 * 3
print(f"\nGrid 3 x 4 x 3        : {komb_besar} kombinasi")
print(f"Dengan cv=5           : {komb_besar * 5} pemanggilan fit (belum termasuk refit)")

Output yang Dihasilkan:

text
Jumlah kombinasi      : 9 (3 x 3)
Total pemanggilan fit : 27 (9 kombinasi x 3 lipatan)

best_params_  : {'max_depth': 5, 'min_samples_leaf': 1}
best_score_   : 0.9430 (rata-rata 3 lipatan CV)
Akurasi uji   : 0.917 (best_estimator_ sudah refit pada seluruh data latih)

== cv_results_: 4 peringkat teratas ==
 max_depth  min_samples_leaf    mean    std  rank
         5                 1   0.943  0.020     1
         7                 1   0.936  0.030     2
         3                 1   0.929  0.020     3
         3                 5   0.907  0.010     4

== Verifikasi manual: cross_val_score per konfigurasi ==
  depth=3 leaf= 1: manual=0.9288 grid=0.9288 IDENTIK
  depth=3 leaf= 5: manual=0.9072 grid=0.9072 IDENTIK
  depth=3 leaf=10: manual=0.8928 grid=0.8928 IDENTIK
  depth=5 leaf= 1: manual=0.9430 grid=0.9430 IDENTIK
  depth=5 leaf= 5: manual=0.9072 grid=0.9072 IDENTIK
  depth=5 leaf=10: manual=0.8928 grid=0.8928 IDENTIK
  depth=7 leaf= 1: manual=0.9359 grid=0.9359 IDENTIK
  depth=7 leaf= 5: manual=0.9072 grid=0.9072 IDENTIK
  depth=7 leaf=10: manual=0.8928 grid=0.8928 IDENTIK
Konfigurasi identik   : 9/9

Grid 3 x 4 x 3        : 36 kombinasi
Dengan cv=5           : 180 pemanggilan fit (belum termasuk refit)

Bedah baris-baris pentingnya:

  1. Baris 15–16 (param_grid, cv): grid 3 × 3 dideklarasikan sebagai kamus, dan lipatan diatur dengan StratifiedKFold(n_splits=3, shuffle=True, random_state=42) agar pembagian fold reproducible dan proporsi kelas terjaga.
  2. Baris 18–22 (hitungan kombinasi): jumlah kombinasi dihitung programatik dari panjang tiap daftar, lalu dikalikan jumlah lipatan — 27 pemanggilan fit(), angka yang sebaiknya selalu kita perkirakan sebelum menekan tombol jalankan.
  3. Baris 24–30 (fit + atribut hasil): best_params_ jatuh pada max_depth=5, min_samples_leaf=1 dengan best_score_ 0,943; akurasi pada data uji 0,917 berasal dari best_estimator_ yang sudah di-refit pada seluruh data latih.
  4. Baris 36–42 (tabel cv_results_): empat peringkat teratas menampilkan mean, std, dan rank. Perhatikan jarak 0,943 dan 0,936 — lebih kecil daripada std masing-masing, tanda bahwa dua konfigurasi teratas secara statistik sulit dibedakan.
  5. Baris 44–58 (verifikasi manual): untuk setiap kombinasi, cross_val_score dipanggil dengan estimator dan pemisah lipatan yang sama persis. Sembilan dari sembilan konfigurasi menghasilkan rata-rata IDENTIK hingga toleransi 1e-12 — bukti bahwa GridSearchCV benar-benar hanya mengotomatiskan perulangan yang bisa kita tulis tangan.
  6. Baris 60–63 (aritmetika pertumbuhan): grid hipotesis 3 × 4 × 3 menghasilkan 36 kombinasi; dengan cv=5 menjadi 180 pemanggilan fit() — belum termasuk satu refit final.

Memilih Lipatan dan Skor #

Nilai cv antara 3 sampai 5 adalah wilayah yang wajar untuk penyetelan: lipatan lebih banyak memberi estimasi lebih stabil tetapi melipatgandakan biaya. Untuk klasifikasi, gunakan StratifiedKFold dengan shuffle=True dan random_state tetap agar hasil penyetelan bisa direproduksi. Parameter scoring mengubah angka yang dioptimalkan: "accuracy" adalah bawaan untuk klasifikasi, tetapi string lain seperti "recall" atau "f1" tersedia untuk masalah dengan kelas timpang — keputusan yang seharusnya mengikuti kerangka Chapter Memilih Metrik yang Tepat.

Untuk kebutuhan yang lebih kompleks, scoring juga menerima daftar metrik sekaligus, misalnya scoring=["accuracy", "recall"]. Dalam mode ini cv_results_ memuat kolom untuk tiap metrik (mis. mean_test_recall), dan parameter refit harus menyebut metrik yang menjadi penentu juara — misalnya refit="recall" — karena best_estimator_ hanya boleh dibangun dari satu kriteria.

Harga yang Dibayar Kesistematisan #

Kekuatan grid search sekaligus menjadi kelemahannya. Biaya total tumbuh secara perkalian: menambah satu dimensi dengan 4 nilai ke grid yang sudah berisi 36 kombinasi melonjak menjadi 144 kombinasi. Resolusi grid juga kasar — kalau nilai optimal max_depth sebenarnya 4, grid [3, 5, 7] tidak akan pernah menemukannya. Terakhir, grid menghabiskan anggaran sama besar untuk dimensi yang tidak berpengaruh: kalau min_samples_leaf ternyata tidak mengubah skor, separuh dari 27 pelatihan kita tetap terpakai untuk variasi yang tidak berarti.

AspekKonsekuensi pada Grid Search
Jumlah kombinasiTumbuh perkalian tiap dimensi baru (3 × 4 × 3 = 36)
Biaya totalKombinasi × lipatan (36 × 5 = 180 pemanggilan fit)
Cakupan nilaiHanya nilai yang didaftarkan; nilai di antaranya terlewat
Kepastian hasilMenyeluruh — konfigurasi terbaik dalam grid pasti ditemukan

Keterbatasan terakhir itulah yang mendorong pendekatan berbeda: daripada menguji semua titik grid, ambil sampel acak dari rentang kontinu. Pendekatan ini terdengar lebih ceroboh, tetapi pada ruang berdimensi banyak justru lebih efisien — kisah yang kita bedah tuntas di Chapter Random Search.

Referensi Resmi #