Tiga chapter sebelumnya mengajarkan cara mencari: grid menyapu semua kombinasi, random search menyampel distribusi, halving memangkas kandidat lewat turnamen anggaran. Semuanya mengjawab pertanyaan “konfigurasi mana yang menang” — tetapi tidak satu pun memberitahu kita bentuk medannya: hyperparameter mana yang benar-benar mengubah kinerja, di wilayah nilai mana akurasinya berbalik arah, dan kapan tambahan kompleksitas berhenti membayar. validation_curve menjawab pertanyaan-pertanyaan itu dengan eksperimen yang jauh lebih murah: pilih satu hyperparameter, sapu beberapa nilainya, lalu baca sepasang kurva skor latih dan validasi. Mesinnya persis mesin Chapter Learning Curve pada Section Evaluasi dan Validasi Model — hanya sumbu-x nya berganti dari jumlah sampel menjadi nilai hyperparameter. Chapter ini membedah mekanismenya, tiga pola bentuk kurva yang layak dihafal, lalu membaca dua kurva nyata pada data kampanye pemasaran.

Mekanisme validation_curve #

Fungsi validation_curve dari sklearn.model_selection menjalankan eksperimen bersarang: untuk tiap nilai pada param_range, fungsi ini melatih estimator yang benar-benar baru pada tiap lipatan cross-validation, lalu mencatat skor pada data latih dan data validasi lipatan tersebut. Hasilnya dua matriks berbentuk (jumlah nilai × jumlah lipatan) — rata-rata per baris (.mean(axis=1)) menghasilkan dua kurva yang siap dibaca, dan .std(axis=1) mengukur seberapa banyak kurva itu bergoyang antar lipatan. Tidak ada best_params_, tidak ada model final yang di-refit — validation_curve adalah alat diagnosis, bukan alat pencarian.

Karena estimator dilatih dari nol untuk tiap nilai, perubahan skor murni berasal dari hyperparameter — bukan sisa pelatihan sebelumnya. Estimator boleh berupa Pipeline penuh: tulis nama parameternya dengan sintaks dua garis bawah, misalnya "kneighborsclassifier__n_neighbors"; seluruh pipeline — termasuk StandardScaler di dalamnya — di-refit pada tiap lipatan sehingga scaling tidak bocor ke fold validasi.

Tiga Pola yang Layak Dikenali #

Hampir semua kurva validasi yang sehat jatuh ke salah satu dari tiga pola berikut. Membacanya lebih penting daripada mencari angka puncaknya:

Pola pada kurvaDiagnosisTindakan
Latih menanjak terus, validasi memuncak lalu menurun; gap melebarOverfitting — kapasitas melampaui dataPilih nilai di sekitar puncak; tolak ujung kapasitas tinggi
Kedua kurva rendah dan berdempet di seluruh rentangUnderfitting, atau hyperparameter tidak berpengaruhPerbaiki model/fitur; jangan membakar anggaran tuning di sini
Kurva berplateau lebar dengan gap sempitWilayah stabil — hyperparameter tidak sensitifPilih nilai sederhana/murah di dalam plateau

Pola pertama adalah wajah klasik kapasitas: max_depth, n_neighbors kecil, num_leaves besar. Pola kedua sering dijumpai pada model yang salah spesifikasi — kinerjanya tidak bergerak karena sumber masalahnya bukan tombol yang sedang diputar. Pola ketiga adalah kabar baik: plateau berarti pilihan Anda tidak rapuh, dan anggaran pencarian lebih baik dialihkan ke hyperparameter lain yang kurvanya benar-benar berbentuk.

Eksperimen: Dua Kurva pada Data Kampanye #

Skrip berikut menjalankan dua sapuan pada data kampanye pemasaran yang sama — 400 sampel, 12 fitur, 10% label terbalik. Bagian 1 menyapu max_depth pada decision tree; Bagian 2 menyapu n_neighbors pada k-NN yang dibungkus pipeline bersama StandardScaler.

python
# Kurva validasi: efek satu hyperparameter terhadap skor latih dan validasi.
# Bagian 1: max_depth pada DecisionTreeClassifier (pola overfitting klasik).
# Bagian 2: n_neighbors pada KNeighborsClassifier dalam pipeline (pola U).

import numpy as np
from sklearn.datasets import make_classification
from sklearn.model_selection import StratifiedKFold, validation_curve
from sklearn.neighbors import KNeighborsClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.tree import DecisionTreeClassifier

# Data kampanye pemasaran: 400 sampel, 12 fitur, 10% label terbalik.
X, y = make_classification(n_samples=400, n_features=12, n_informative=6,
                           n_redundant=3, flip_y=0.10, class_sep=1.0,
                           random_state=42)
cv = StratifiedKFold(n_splits=3, shuffle=True, random_state=42)


def tabel_kurva(rentang, skor_latih, skor_validasi):
    """Cetak tabel kurva validasi: nilai, rata-rata latih/validasi, gap."""
    print(f"  {'nilai':<12} {'latih':>6} {'validasi':>10} {'+- std':>7} {'gap':>7}")
    for nilai, a, b, s in zip(rentang, skor_latih.mean(axis=1),
                              skor_validasi.mean(axis=1),
                              skor_validasi.std(axis=1)):
        print(f"  {str(nilai):<12} {a:>6.3f} {b:>10.3f} {s:>7.3f} {a - b:>7.3f}")


# --- Bagian 1: max_depth pada decision tree ----------------------------
rentang_kedalaman = [1, 2, 3, 4, 6, None]
latih, validasi = validation_curve(
    DecisionTreeClassifier(random_state=42), X, y,
    param_name="max_depth", param_range=rentang_kedalaman,
    cv=cv, scoring="accuracy", n_jobs=1)

print("Bagian 1 - max_depth pada DecisionTreeClassifier:")
tabel_kurva(rentang_kedalaman, latih, validasi)
i_terbaik = int(np.argmax(validasi.mean(axis=1)))
print(f"  validasi tertinggi     : max_depth = "
      f"{rentang_kedalaman[i_terbaik]} "
      f"({validasi.mean(axis=1)[i_terbaik]:.3f})")

# --- Bagian 2: n_neighbors pada k-NN dalam pipeline --------------------
rentang_tetangga = [1, 3, 5, 10, 20, 50]
pipeline = make_pipeline(StandardScaler(), KNeighborsClassifier())
latih, validasi = validation_curve(
    pipeline, X, y,
    param_name="kneighborsclassifier__n_neighbors",
    param_range=rentang_tetangga, cv=cv, scoring="accuracy", n_jobs=1)

print("\nBagian 2 - n_neighbors pada KNeighborsClassifier (dalam pipeline):")
tabel_kurva(rentang_tetangga, latih, validasi)
i_terbaik = int(np.argmax(validasi.mean(axis=1)))
print(f"  validasi tertinggi     : n_neighbors = "
      f"{rentang_tetangga[i_terbaik]} "
      f"({validasi.mean(axis=1)[i_terbaik]:.3f})")

Output yang Dihasilkan:

text
Bagian 1 - max_depth pada DecisionTreeClassifier:
  nilai         latih   validasi  +- std     gap
  1             0.759      0.752   0.027   0.006
  2             0.815      0.790   0.027   0.025
  3             0.850      0.817   0.019   0.033
  4             0.896      0.770   0.025   0.126
  6             0.960      0.767   0.006   0.193
  None          1.000      0.777   0.025   0.223
  validasi tertinggi     : max_depth = 3 (0.817)

Bagian 2 - n_neighbors pada KNeighborsClassifier (dalam pipeline):
  nilai         latih   validasi  +- std     gap
  1             1.000      0.783   0.047   0.217
  3             0.881      0.840   0.027   0.041
  5             0.871      0.845   0.020   0.026
  10            0.871      0.850   0.012   0.021
  20            0.859      0.840   0.020   0.019
  50            0.817      0.805   0.027   0.012
  validasi tertinggi     : n_neighbors = 10 (0.850)

Mari kita telaah bagian-bagian kunci skrip tersebut:

  1. Baris 14–17 (data dan lipatan tetap): make_classification(random_state=42) menyediakan masalah dengan 10% label terbalik; StratifiedKFold(n_splits=3, shuffle=True, random_state=42) menjaga proporsi kelas di tiap lipatan sekaligus menjamin tabel yang sama pada setiap eksekusi.
  2. Baris 20–26 (pencetak tabel): fungsi tabel_kurva merata-ratakan tiap baris matriks skor (.mean(axis=1)), menghitung deviasi standar antar lipatan, dan menambahkan kolom gap = skor latih dikurangi skor validasi — tiga angka yang menjadi bahan baku diagnosis.
  3. Baris 30–34 (sapuan pertama): satu pemanggilan validation_curve menggantikan enam loop pelatihan manual; param_name="max_depth" dan param_range berisi enam nilai termasuk None (pohon tumbuh penuh), scoring="accuracy" eksplisit, n_jobs=1.
  4. Baris 38–41 (puncak otomatis): np.argmax pada rata-rata validasi menandai nilai terbaik — dilakukan untuk kenyamanan pembacaan, bukan untuk dijadikan keputusan final.
  5. Baris 44–49 (sapuan kedua): estimator berupa Pipeline StandardScaler + k-NN; param_name="kneighborsclassifier__n_neighbors" menembus pipeline hingga hyperparameter k-NN, dan seluruh pipeline di-refit per nilai per lipatan.

Membaca Hasil Eksperimen #

Bagian 1 menampilkan pola overfitting paling klasik. Skor latih menanjak monoton dari 0,759 pada kedalaman 1 sampai 1,000 pada pohon tumbuh penuh — makin dalam pohon, makin luas ruang hafalannya. Skor validasi mengikuti hanya sampai kedalaman 3 (0,817), lalu berbalik turun ke kisaran 0,77: kapasitas tambahan setelah titik itu dipakai menghafal derau label, buangannya terlihat langsung pada gap yang membengkak dari 0,006 menjadi 0,223. Perhatikan pula kolom deviasi standar yang kecil (0,006–0,027) — puncak 0,817 itu berdiri di atas tiga lipatan yang cukup jujur menyepakati kisaran 0,79–0,82, sehingga pola “puncak di 3” layak dipercaya sebagai wilayah, bukan kebetulan.

Bagian 2 melengkapi dengan pola lengkap dari kiri ke kanan. Pada n_neighbors=1, k-NN menghafal tiap titik latih (skor latih 1,000) dan langsung dihukum di validasi (0,783, gap 0,217) — overfitting versi instance-based. Ke arah kanan, gap menutup perlahan dan validasi memuncak 0,850 pada k=10, sebelum menurun ke 0,805 pada k=50 — tetangga terlalu banyak mulai menghaluskan batas kelas hingga informasi lokal tercuci (underfitting). Pelajaran terpenting ada di wilayah tengah: k=3 sampai k=20 semuanya beradu dalam rentang 0,840–0,850, selisihnya lebih kecil daripada deviasi standar antar lipatan. Kurva ini tidak menunjuk satu angka juara tunggal; kurva ini menunjuk plateau — dan pilihan mana pun di dalamnya akan berperilaku nyaris sama di lapangan.

Kurva validasi menjawab “hyperparameter mana dan di wilayah mana”. Satu tingkat lagi tetap terbuka: bagaimana bila satu evaluasi saja sudah mahal — model besar, data besar, anggaran terbatas? Di titik itu pencarian perlu belajar dari evaluasi yang sudah selesai dan berhenti lebih dini pada kandidat yang jelas tertinggal. Dua gagasan itu — optimasi Bayesian dan multi-fidelity — menutup Section ini pada Chapter Strategi Optimasi Lanjutan.

Referensi Resmi #