Grid, random, dan halving search menguji setiap kandidat seolah evaluasi itu murah. Kenyataan di lapangan sering sebaliknya: satu evaluasi berarti melatih model pada seluruh data latih dengan lipatan CV penuh — biaya yang bisa mencapai puluhan menit untuk model besar. Ketika setiap evaluasi semahal itu, pencarian tidak boleh bersifat lupa: ia harus belajar dari evaluasi yang sudah selesai, menghentikan pelatihan yang jelas stagnan, dan mencatat setiap percobaan agar keputusan berikutnya berdiri di atas riwayat. Chapter terakhir Section ini merangkum tiga gagasan itu: optimasi Bayesian sebagai pencarian yang punya ingatan, early stopping sebagai pemangkas epoch yang sia-sia, dan peta ekosistem alat modern beserta praktik yang menentukan hasilnya di dunia nyata.

Pencarian yang Belajar dari Evaluasi Sebelumnya #

Grid search dan random search bersifat amnesic: kandidat ke-40 dievaluasi tanpa peduli apa yang terjadi pada kandidat 1 sampai 39. Optimasi Bayesian mengambil pendekatan berbeda dengan menjaga dua komponen yang bekerja bergantian. Komponen pertama adalah surrogate model — model statistik yang murah dilatih dan meniru perilaku fungsi objektif yang mahal (skor CV sebagai fungsi dari hyperparameter). Komponen kedua adalah acquisition function — rumus yang membaca surrogate lalu memutuskan titik mana yang paling layak dievaluasi berikutnya.

Surrogate yang paling umum adalah Gaussian Process (GP): untuk setiap titik kandidat, GP memberikan dua angka sekaligus — perkiraan nilai objektif (mean) dan ketidakpastian perkiraan itu (standard deviation). Pasangan mean-std inilah yang membuat keputusan menjadi cerdas: wilayah yang belum pernah dievaluasi pun std besar, wilayah yang sudah sering dikunjungi punya std kecil. Acquisition function Expected Improvement (EI) kemudian menggabungkan keduanya menjadi satu angka:

text
EI(x) = (mu(x) - f_terbaik - xi) * Phi(z) + sigma(x) * phi(z)
z      = (mu(x) - f_terbaik - xi) / sigma(x)

Notasi Phi dan phi adalah CDF dan PDF distribusi normal standar. Rumus ini memuat dua naluri sekaligus: jika mu(x) jauh di atas rekor f_terbaik, EI tinggi karena perkiraan optimis (eksploitasi); jika sigma(x) besar, EI juga tinggi karena ada peluang tersembunyi di wilayah yang belum dipelajari (eksplorasi). Iterasi kecil xi (misalnya 0,01) mendorong sedikit eksplorasi tambahan agar pencarian tidak cepat terkurung di satu bukit. Siklusnya sederhana: evaluasi → perbarui surrogate → pilih titik EI tertinggi → evaluasi → ulangi sampai anggaran habis.

Contoh Kode: Optimasi Bayesian Mini dengan NumPy #

Pustaka optimasi Bayesian seperti Optuna tidak terpasang di lingkungan kursus ini, jadi kita membangun versi mini dari nol — dan justru versi mini inilah yang membuat mekanismenya transparan. Fungsi objektif di bawah ini adalah stand-in untuk skor CV yang mahal: pencarian tidak tahu bentuk fungsinya, hanya boleh “membeli” nilainya lewat evaluasi.

python
# Optimasi Bayesian mini dengan NumPy dan SciPy murni.
# Surrogate Gaussian Process + akuisisi Expected Improvement,
# lalu dibandingkan dengan pencarian acak beranggaran evaluasi sama.

import numpy as np
from scipy.stats import norm


def f_objektif(x):
    """Objektif 1-D deterministik: berbukit halus, puncak di sekitar x=2.9."""
    return 2.0 - (x - 2.8) ** 2 / 3.0 + 0.3 * np.sin(2.0 * x)


def kernel_rbf(a, b, panjang=0.7):
    """Kernel RBF: kovarians antar titik, makin dekat makin berkorelasi."""
    selisih = a[:, None] - b[None, :]
    return np.exp(-(selisih ** 2) / (2.0 * panjang ** 2))


def posterior_gp(x_latih, y_latih, x_uji, panjang=0.7):
    """Regresi Gaussian Process: kembalikan mean dan std per titik uji."""
    K = kernel_rbf(x_latih, x_latih, panjang)
    Ks = kernel_rbf(x_latih, x_uji, panjang)
    Kss = kernel_rbf(x_uji, x_uji, panjang)
    sol = np.linalg.solve(K, Ks)          # K^-1 @ Ks tanpa invers eksplisit
    mu = sol.T @ y_latih                  # mean posterior
    var = np.diag(Kss) - np.sum(sol * Ks, axis=0)
    return mu, np.sqrt(np.clip(var, 1e-12, None))


def expected_improvement(mu, sigma, y_terbaik, xi=0.01):
    """EI untuk maksimisasi: harapan perbaikan di atas rekor terbaik."""
    perbaikan = mu - y_terbaik - xi
    z = perbaikan / sigma
    ei = perbaikan * norm.cdf(z) + sigma * norm.pdf(z)
    ei[sigma <= 1e-10] = 0.0              # titik yang sudah diamati: EI nol
    return ei


kandidat = np.linspace(0.0, 6.0, 241)     # kisi tempat akuisisi dipilih

# --- Inisialisasi: dua evaluasi awal yang berjauhan ---------------------
x_amat = np.array([0.5, 5.0])
y_amat = f_objektif(x_amat)

print("Optimasi Bayesian mini (maksimisasi f_objektif di [0, 6]):")
print(f"{'iter':>4} {'x dipilih':>10} {'f(x)':>7} {'rekor':>7} {'sumber':>9}")
for i in range(1, 9):
    mu, sigma = posterior_gp(x_amat, y_amat, kandidat)
    ei = expected_improvement(mu, sigma, y_amat.max())
    x_baru = kandidat[int(np.argmax(ei))]
    y_baru = f_objektif(x_baru)
    sumber = "eksplorasi" if sigma[int(np.argmax(ei))] > 0.4 else "eksploitasi"
    print(f"{i:>4} {x_baru:>10.3f} {y_baru:>7.3f} "
          f"{max(y_amat.max(), y_baru):>7.3f} {sumber:>9}")
    x_amat = np.append(x_amat, x_baru)
    y_amat = np.append(y_amat, y_baru)

# --- Pembanding: pencarian acak dengan anggaran evaluasi sama ----------
rng = np.random.default_rng(42)
x_acak = rng.uniform(0.0, 6.0, size=len(x_amat))
y_acak = f_objektif(x_acak)

# --- Kebenaran sebenarnya dari kisi sangat rapat ------------------------
kisi_rajat = np.linspace(0.0, 6.0, 600_001)
x_opt, y_opt = kisi_rajat[np.argmax(f_objektif(kisi_rajat))], f_objektif(kisi_rajat).max()

print(f"\nanggaran evaluasi      : {len(x_amat)} titik")
print(f"Bayesian  -> x = {x_amat[np.argmax(y_amat)]:.3f}, f = {y_amat.max():.3f}")
print(f"acak      -> x = {x_acak[np.argmax(y_acak)]:.3f}, f = {y_acak.max():.3f}")
print(f"optimum   -> x = {x_opt:.3f}, f = {y_opt:.3f}")

Dengan anggaran total 10 evaluasi (2 titik awal + 8 iterasi), optimasi Bayesian mendarat sangat dekat pada optimum sejati. Output yang Dihasilkan:

text
Optimasi Bayesian mini (maksimisasi f_objektif di [0, 6]):
iter  x dipilih    f(x)   rekor    sumber
   1      1.400   1.447   1.447 eksplorasi
   2      1.925   1.550   1.550 eksplorasi
   3      1.725   1.524   1.550 eksploitasi
   4      2.800   1.811   1.811 eksplorasi
   5      3.400   2.028   2.028 eksplorasi
   6      3.850   1.929   2.028 eksploitasi
   7      3.575   2.028   2.028 eksploitasi
   8      6.000  -1.574   2.028 eksplorasi

anggaran evaluasi      : 10 titik
Bayesian  -> x = 3.575, f = 2.028
acak      -> x = 2.702, f = 1.766
optimum   -> x = 3.490, f = 2.034

Bedah kode per bagian membuat logika di balik angka-angka itu terlihat. Fungsi f_objektif pada baris 9-11 adalah objektif berbukit yang deterministik — puncaknya di sekitar x = 3,5, tetapi pencarian tidak mengetahuinya. Kernel RBF pada baris 14-16 mendefinisikan kemiripan antar titik: dua titik yang berdekatan dianggap berkorelasi kuat, dan panjang skala 0,7 menentukan seberapa cepat korelasi itu memudar. Regresi GP pada baris 20-27 lalu menghitung posterior: baris 25 memakai np.linalg.solve alih-alih invers eksplisit (np.linalg.inv(K) @ Ks) — bentuk solve lebih stabil secara numerik dan lebih murah; baris 26 adalah mean posterior, baris 27 adalah varians posterior yang di-clip agar tidak negatif akibat galat pembulatan float.

Fungsi expected_improvement pada baris 31-38 menerjemahkan rumus EI di atas ke kode, dengan satu penjaga penting di baris 36: titik yang sudah diamati punya sigma nyaris nol, dan tanpa penjaga itu rumus EI bisa membagi dengan nol atau memilih ulang titik yang sama. Loop utama pada baris 48-57 adalah siklus lengkap — posterior (baris 49), EI (baris 50), pilih kandidat EI tertinggi dari kisi 241 titik (baris 51) — sementara baris 60-66 menyiapkan pembanding yang adil: pencarian acak ber-rng berseed dengan jumlah evaluasi persis sama, plus kisi sangat rapat sebagai kebenaran acuan.

Hasilnya layak dibaca pelan. Empat iterasi pertama berlabel eksplorasi karena yang dipilih adalah wilayah dengan std besar; begitu wilayah puncak teridentifikasi, iterasi 6-7 berlabel eksploitasi dan menghaluskan posisi terbaik ke x = 3,575 — hanya 0,085 dari optimum sejati 3,490. Pencarian acak dengan anggaran sama hanya menemukan f = 1,766, jauh di bawah 2,028 milik Bayesian. Iterasi terakhir yang melompat ke x = 6,0 bukan kegagalan melainkan sisa eksplorasi: masih ada ketidakpastian di tepi kanan wilayah pencarian, dan EI mengirim satu prob terakhir ke sana sebelum anggaran habis.

Early Stopping: Memangkas Epoch yang Tidak Berkembang #

Halving search menghentikan kandidat yang kalah dari kandidat lain. Early stopping menjawab pertanyaan yang berbeda: bagaimana menghentikan satu pelatihan ketika metrik validasinya sudah berhenti berkembang? Mekanismenya sederhana dan sudah menjadi standar de-facto di semua framework modern: evaluasi model pada set validasi di akhir setiap epoch, simpan rekor terbaik, dan berhenti setelah kesabaran (patience) epoch berturut-turut tanpa perbaikan. Epoch ke-15 sampai ke-50 yang hanya mendapat peningkatan 0,001 tidak lagi dikonsumsi — anggaran komputasi dialihkan ke kandidat lain.

scikit-learn menampilkan mekanisme ini lewat API incremental partial_fit, yang melatih model satu batch demi satu batch tanpa memulai dari nol. Kita menulis loop early stopping secara manual agar setiap komponennya kelihatan:

python
# Early stopping pada SGDClassifier: latih per epoch dengan partial_fit,
# pantau akurasi validasi, berhenti setelah beberapa epoch tanpa perbaikan.

import numpy as np
from sklearn.datasets import make_classification
from sklearn.linear_model import SGDClassifier
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

# Data kampanye pemasaran: 800 sampel, 12 fitur, 8% label terbalik.
X, y = make_classification(n_samples=800, n_features=12, n_informative=6,
                           n_redundant=3, flip_y=0.08, class_sep=1.0,
                           random_state=42)
X_latih, X_uji, y_latih, y_uji = train_test_split(
    X, y, test_size=0.25, stratify=y, random_state=42)

skala = StandardScaler().fit(X_latih)
X_latih = skala.transform(X_latih)
X_uji = skala.transform(X_uji)

# Dari data latih, sisihkan 25% sebagai set validasi untuk early stopping.
X_fit, X_val, y_fit, y_val = train_test_split(
    X_latih, y_latih, test_size=0.25, stratify=y_latih, random_state=42)

model = SGDClassifier(loss="log_loss", learning_rate="constant",
                      eta0=0.01, random_state=42)
kelas = np.unique(y_fit)
rng = np.random.default_rng(42)

KESABARAN = 5          # epoch tanpa perbaikan sebelum berhenti
EPOCH_MAKS = 50
rekor, tanpa_perbaikan, epoch_berhenti = -1.0, 0, EPOCH_MAKS

print(f"{'epoch':>5} {'akurasi val':>11} {'rekor':>7} {'tanpa perbaikan':>16}")
for epoch in range(1, EPOCH_MAKS + 1):
    urutan = rng.permutation(len(X_fit))     # urutan batch baru per epoch
    for mulai in range(0, len(urutan), 64):
        idx = urutan[mulai:mulai + 64]
        model.partial_fit(X_fit[idx], y_fit[idx], classes=kelas)
    akurasi = accuracy_score(y_val, model.predict(X_val))
    if akurasi > rekor:
        rekor, tanpa_perbaikan = akurasi, 0
    else:
        tanpa_perbaikan += 1
    print(f"{epoch:>5} {akurasi:>11.3f} {rekor:>7.3f} {tanpa_perbaikan:>16}")
    if tanpa_perbaikan >= KESABARAN:
        epoch_berhenti = epoch
        print(f"berhenti di epoch {epoch}: {KESABARAN} epoch tanpa perbaikan")
        break

print(f"\nepoch dijalankan      : {epoch_berhenti} dari {EPOCH_MAKS}")
print(f"akurasi validasi rekor: {rekor:.3f}")
print(f"akurasi uji akhir     : {accuracy_score(y_uji, model.predict(X_uji)):.3f}")

Pelatihan berhenti jauh sebelum batas atas epoch. Output yang Dihasilkan:

text
epoch akurasi val   rekor  tanpa perbaikan
    1       0.800   0.800                0
    2       0.793   0.800                1
    3       0.800   0.800                2
    4       0.813   0.813                0
    5       0.813   0.813                1
    6       0.813   0.813                2
    7       0.813   0.813                3
    8       0.820   0.820                0
    9       0.827   0.827                0
   10       0.820   0.827                1
   11       0.827   0.827                2
   12       0.820   0.827                3
   13       0.820   0.827                4
   14       0.820   0.827                5
berhenti di epoch 14: 5 epoch tanpa perbaikan

epoch dijalankan      : 14 dari 50
akurasi validasi rekor: 0.827
akurasi uji akhir     : 0.745

Tiga baris layak dibedah. Baris 37 mengocok urutan sampel dengan rng.permutation segar di tiap epoch — pengocokan inilah sumber stokastisitas SGD, dan karena rng berseed 42, seluruh lintasan pelatihan dapat direproduksi persis. Baris 40 memanggil partial_fit per batch 64 sampel dengan classes yang eksplisit — argumen itu wajib pada pemanggilan pertama agar model tahu label apa yang mungkin muncul. Baris 42 dan 47 membentuk logika kesabaran: rekor diperbarui hanya jika akurasi validasi membaik, penghitung tanpa_perbaikan naik jika tidak, dan break menghentikan semuanya begitu penghitung menyentuh KESABARAN = 5.

Angkanya bercerita lurus: rekor validasi 0,827 dicapai di epoch 9, pelatihan berhenti di epoch 14, sehingga 36 dari 50 epoch tidak pernah dijalankan — pemangkasan 72% anggaran epoch tanpa kehilangan rekor. Perhatikan juga bahwa akurasi uji (0,745) berada di bawah rekor validasi — celah wajar yang sudah dibahas di Section evaluasi, dan alasan kuat mengapa keputusan berhenti harus dibaca dari validasi, bukan dari uji. Satu kelemahan implementasi manual ini layak diakui: model yang tersimpan adalah model terakhir (epoch 14), bukan model rekor (epoch 9). Framework seperti LightGBM dan XGBoost menyelesaikannya dengan menyimpan snapshot terbaik otomatis; pada implementasi manual, salin bobot model setiap kali rekor pecah.

Ekosistem Optimasi di Luar scikit-learn #

Dunia nyata jarang berhenti pada GridSearchCV. Tiga keluarga alat melengkapi strategi di atas, dan semuanya membangun di atas gagasan yang sudah kita bahas. Optuna mengotomatiskan optimasi Bayesian dengan sampler TPE (Tree-structured Parzen Estimator, sepupu GP yang lebih tahan terhadap dimensi tinggi) dan menawarkan pruning: uji antara pelatihan yang berjalan dapat dihentikan lebih awal oleh alatnya sendiri — pernikahan halving search dan early stopping. Ray Tune menambahkan skala klaster untuk pencarian paralel di banyak mesin. MLflow mengambil peran berbeda: mencatat setiap percobaan (parameter, metrik, artefak model) sehingga riwayat optimasi bisa diaudit dan dibandingkan lintas sesi kerja.

StrategiBelajar dari apaProfil anggaranSituasi paling cocok
Grid searchTidak ada — daftar tetapSeragam, membengkak di dimensi banyak1-2 hyperparameter dengan kandidat jelas
Random searchTidak ada — distribusi tetapSeragam per sampelDimensi banyak, anggaran tetap, ruang tak seragam
Halving searchHasil kandidat antar tahapAwal murah, final mahalBanyak kandidat, sumber daya bisa bertahap
Bayesian + pruningSeluruh riwayat evaluasiTerkonsentrasi di kandidat menjanjikanEvaluasi mahal, anggaran ketat

Praktik yang Menentukan Hasil #

Empat kebiasaan memisahkan pencarian yang efisien dari yang hanya panjang. Pertama, desain ruang pencarian: hyperparameter skala-pangkat seperti C, gamma, dan learning rate harus disampel logaritmik — pelajaran dari Chapter Random Search berlaku juga di sini, dan ruang yang salah membuat algoritma terpintar pun tersesat. Kedua, hitung anggaran dengan jujur: grid 24 kombinasi pada CV 3-lipat berarti 72 pelatihan penuh; kalau tiap pelatihan 10 menit, pencarian itu 12 jam — angka semacam ini seharusnya diketahui sebelum menekan enter, bukan sesudahnya.

Ketiga, tenangkan objektif yang riuh: skor CV pada data kecil berfluktuasi antar seed, dan optimizer akan dengan setia mengejar derau itu. Rata-rata beberapa seed atau lipatan lebih banyak menghabiskan anggaran per evaluasi, tetapi menghemat total evaluasi karena optimizer tidak mengejar bayangan. Keempat, jangan menyetel yang tidak berpengaruh: kurva validasi dari chapter sebelumnya mengidentifikasi hyperparameter yang benar-benar menggerakkan skor — menyempitkan perhatian ke dua-tiga hyperparameter tersebut memangkas ruang pencarian lebih drastis daripada teknik apa pun di atas ini. Dan satu aturan yang tidak bisa ditawar: keputusan optimasi hanya boleh membaca data validasi; menyentuh data uji sebelum pencarian usai adalah kebocoran yang membatalkan makna seluruh angka.

Optimasi hyperparameter memperlakukan model sebagai kotak yang mengeluarkan angka skor. Pertanyaan berikutnya justru menantang cara pandang itu: bagaimana jika model sendiri bisa menyatakan keyakinannya — bukan sekadar memprediksi kelas, tetapi mendistribusikan peluang atas semua kemungkinan? Itu arah Section berikutnya, dimulai dari Chapter Pandangan Probabilistik atas Machine Learning.

Referensi Resmi #