Kesadaran Nested CV

Estimasi performa yang jujur bagi model yang telah ditala.

Daftar isi

Topik ini membahas nested CV, yaitu skema cross-validation yang menilai model sekaligus mengulang penalaannya di dalam setiap lipatan penilaian. Pembahasan mencakup mengapa skor cross-validation dari model yang ditala pada lipatan yang sama cenderung menggelembung, mekanisme loop luar dan loop dalam, serta implementasi ringkasnya dengan membungkus GridSearchCV di dalam cross_val_score. Contohnya memperlihatkan selisih nyata antara kedua angka pada dataset breast cancer, sekaligus batas keadaan ketika selisih itu nyaris tidak terbaca.

Skor penalaan yang ikut memilih model#

Objek GridSearchCV yang di-fit pada seluruh data menghasilkan best_score_, yaitu rata-rata skor CV dari kombinasi pemenang. Angka ini sering digunakan sebagai estimasi kinerja, tetapi posisinya sudah tidak netral karena data validasi pada setiap lipatan ikut menentukan kombinasi mana yang menang. Model dilaporkan melalui lipatan yang juga digunakan untuk memilihnya, sehingga kombinasi yang kebetulan unggul pada derau data validasi cenderung terpilih. Mekanismenya halus karena setiap langkahnya terlihat sah, yaitu cross-validation menilai setiap kombinasi dengan benar, kemudian skor kombinasi pemenangnya yang diangkat sebagai estimasi.

Akibatnya, best_score_ cenderung sedikit lebih tinggi daripada kinerja sebenarnya pada data baru. Kecenderungan ini serumpun dengan kebocoran data, tetapi sumbernya bukan praproses, melainkan pemakaian ganda atas data yang sama untuk memilih dan untuk melaporkan. Gelembung ini murni perkara statistik pemilihan dan tidak memunculkan error apa pun sepanjang prosesnya. Besarnya gelembung bergantung pada model dan datanya, dan bisa saja nyaris nol pada model yang sangat stabil, sehingga angkanya perlu dibaca dengan memperhatikan asal-usulnya.

Mekanisme nested CV#

Nested CV memisahkan kedua peran tersebut ke dalam dua loop. Loop luar membagi data menjadi lipatan penilaian sebagaimana cross-validation biasa, sedangkan loop dalam menjalankan penalaan lengkap pada bagian latih setiap lipatan luar. Skor lipatan luar dihitung pada bagian ujinya yang tidak pernah disentuh penalaan, sehingga rangkaian skor luar mengukur prosedur penalaan secara utuh dan bukan hanya model pemenangnya. Objek grid yang masuk ke loop luar tidak pernah melihat baris data ujinya, karena seluruh penalaannya berlangsung di dalam bagian latih masing-masing lipatan.

Biayanya berlipat karena setiap lipatan luar memuat penalaan penuh. Grid tiga kandidat dengan lima lipatan dalam menuntut 15 kali pelatihan per lipatan luar, dan lima lipatan luar menjadikan totalnya sekitar 75 kali pelatihan untuk satu angka estimasi. Angka sebesar itu menjelaskan kedudukan nested CV sebagai alat penilaian untuk kebutuhan yang menuntut kejujuran tinggi, dan bukan rutinitas yang dijalankan pada setiap eksperimen penalaan.

Skor nested dengan cross_val_score#

Objek GridSearchCV adalah estimator seperti model lainnya, sehingga dapat langsung dibungkus oleh cross_val_score. Bungkusan itu persis nested CV, yaitu cross_val_score menggandakan grid untuk setiap lipatan luar, grid tersebut menala pada bagian latihnya, lalu skornya diambil pada bagian ujinya. Kode berikut membandingkan kedua angka pada pohon keputusan untuk dataset breast cancer, dengan kisi max_depth berisi empat kandidat dan dua loop lima lipatan.

python gelembung-pohon.py
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import GridSearchCV, StratifiedKFold, cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.tree import DecisionTreeClassifier

X, y = load_breast_cancer(return_X_y=True)

pipeline = Pipeline([
    ("model", DecisionTreeClassifier(random_state=42)),
])
param_grid = {"model__max_depth": [2, 3, 5, None]}
cv_dalam = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
cv_luar = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

grid = GridSearchCV(pipeline, param_grid, cv=cv_dalam)

# penalaan dan pelaporan memakai lipatan yang sama: skor cenderung menggelembung
grid.fit(X, y)
print(f"best_score_ CV dalam    : {grid.best_score_:.4f}")

# nested CV: penalaan diulang di dalam setiap lipatan luar
skor_nested = cross_val_score(grid, X, y, cv=cv_luar)
print("Skor nested per lipatan :", skor_nested.round(4))
print(f"Rata-rata nested        : {skor_nested.mean():.4f}")
print(f"Selisih (dalam - nested): {grid.best_score_ - skor_nested.mean():+.4f}")
text Keluaran
best_score_ CV dalam    : 0.9280
Skor nested per lipatan : [0.9386 0.8684 0.9298 0.8947 0.9558]
Rata-rata nested        : 0.9175
Selisih (dalam - nested): +0.0105

Nilai best_score_ dari penalaan pada seluruh data tercatat 0.9280, sedangkan rata-rata nested CV hanya 0.9175, dan selisih +0.0105 itulah gelembungnya. Selisih semacam ini wajar muncul pada pohon keputusan karena modelnya lentur, sehingga pemilihan max_depth ikut menangkap derau data validasi setiap lipatan. Rentang skor lipatan luarnya juga lebar, yaitu 0.8684 hingga 0.9558, dan angka itu menegaskan bahwa satu lipatan tunggal dapat memberi gambaran yang jauh dari rata-ratanya. Pohon keputusan tidak memerlukan scaling, sehingga pipeline pada contoh ini hanya berisi langkah model, sedangkan strukturnya tetap menunjukkan bahwa loop dalam menerima pipeline yang utuh.

Gelembung bergantung pada kestabilan model#

Kode berikut mengulang pengukuran yang sama pada pipeline StandardScaler dan LogisticRegression dengan tiga kandidat C. Regresi logistik pada dataset ini jauh lebih stabil daripada pohon keputusan, sehingga perilaku kedua angkanya layak diamati sebagai pembanding. Kisi yang diperkecil menjadi tiga kandidat juga menjaga biaya nested-nya tetap ringan.

python nested-cv.py
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV, StratifiedKFold, cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

X, y = load_breast_cancer(return_X_y=True)

pipeline = Pipeline([
    ("scaler", StandardScaler()),
    ("model", LogisticRegression(max_iter=1000)),
])
param_grid = {"model__C": [0.01, 0.1, 1.0]}
cv_dalam = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
cv_luar = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

grid = GridSearchCV(pipeline, param_grid, cv=cv_dalam)

# nested CV: penalaan diulang di dalam setiap lipatan luar
skor_nested = cross_val_score(grid, X, y, cv=cv_luar)
print("Skor nested per lipatan :", skor_nested.round(4))
print(f"Rata-rata nested        : {skor_nested.mean():.4f}")
print(f"Simpangan baku nested   : {skor_nested.std():.4f}")

# pembanding: skor CV dalam dari grid yang di-fit pada seluruh data
grid.fit(X, y)
print(f"best_score_ CV dalam    : {grid.best_score_:.4f}")
print(f"Selisih (dalam - nested): {grid.best_score_ - skor_nested.mean():+.4f}")
text Keluaran
Skor nested per lipatan : [0.9737 0.9474 0.9649 0.9912 0.9912]
Rata-rata nested        : 0.9737
Simpangan baku nested   : 0.0166
best_score_ CV dalam    : 0.9737
Selisih (dalam - nested): +0.0000

Kedua angka berakhir sama pada 0.9737 dan selisihnya +0.0000. Hasil ini tidak menyalahkan konsepnya, melainkan menunjukkan bahwa gelembung penalaan bergantung pada kestabilan model, dan pada model yang stabil selisihnya bisa terlalu kecil untuk terbaca pada empat angka desimal. Kestabilan regresi logistik pada dataset ini berakar pada batas keputusannya yang linear dan permukaan skornya yang datar, sehingga pergantian lipatan tidak banyak mengubah kandidat yang menang. Nilai nested tetap menjadi angka yang sah karena penilaiannya berlangsung di luar penalaan, sedangkan best_score_ yang sama pada model lain, sebagaimana pohon keputusan di atas, dapat berbalik menggelembung.

Hyperparameter terpilih berubah antarlipatan#

Penalaan yang diulang pada setiap lipatan luar juga memperlihatkan sesuatu yang tersembunyi di balik satu best_params_ tunggal, yaitu kemenangan kombinasi yang bergantung pada data latihnya. Kode berikut menjalankan loop luar secara manual pada pipeline regresi logistik dan mencatat pemenang setiap lipatan.

python Contoh: hyperparameter terpilih pada setiap lipatan luar
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV, StratifiedKFold
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

X, y = load_breast_cancer(return_X_y=True)
pipeline = Pipeline([
    ("scaler", StandardScaler()),
    ("model", LogisticRegression(max_iter=1000)),
])
param_grid = {"model__C": [0.01, 0.1, 1.0]}
cv_dalam = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
cv_luar = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
grid = GridSearchCV(pipeline, param_grid, cv=cv_dalam)

for i, (idx_latih, _) in enumerate(cv_luar.split(X, y), start=1):
    grid.fit(X[idx_latih], y[idx_latih])
    print(
        f"Lipatan luar {i}: C terpilih {grid.best_params_['model__C']}, "
        f"skor dalam {grid.best_score_:.4f}"
    )
text Keluaran
Lipatan luar 1: C terpilih 0.1, skor dalam 0.9714
Lipatan luar 2: C terpilih 1.0, skor dalam 0.9824
Lipatan luar 3: C terpilih 1.0, skor dalam 0.9802
Lipatan luar 4: C terpilih 1.0, skor dalam 0.9780
Lipatan luar 5: C terpilih 1.0, skor dalam 0.9671

Lipatan pertama memilih C=0.1, sedangkan empat lipatan lainnya memilih C=1.0. Variasi ini normal karena setiap lipatan luar menala pada bagian latih yang berbeda, dan variasi itu menegaskan bahwa nested CV menilai prosedur penalaannya dan bukan satu model akhir. Apabila pemenang yang sama dipilih di semua lipatan luar, kisi berikutnya dapat dipersempit di sekitar nilai itu, sedangkan kemenangan yang berpindah-pindah menandakan kisi yang perlu diperiksa ulang. Pembacaan semacam ini hanya mungkin ketika penalaan benar-benar diulang, karena satu kali fit pada seluruh data menyembunyikan variasinya. Model final untuk digunakan tetap ditala sekali lagi pada seluruh data latih setelah estimasi nested selesai.

Kesalahan umum#

  • Melaporkan best_score_ sebagai kinerja model pada data baru. Angka itu rata-rata CV dari kombinasi yang ikut dipilih oleh CV yang sama, sehingga condong menggelembung. Perbaikannya dengan nested CV atau data uji yang disimpan terpisah sejak awal.
  • Menjalankan nested CV pada setiap eksperimen penalaan kecil. Biaya pelatihannya berlipat-lipat untuk satu angka estimasi. Perbaikannya dengan menyimpan nested CV untuk pengukuran akhir yang menuntut kejujuran tinggi.
  • Membaca skor nested yang lebih rendah sebagai model yang lebih buruk. Kedua angka menilai hal yang berbeda, yaitu prosedur penalaan dan kombinasi pemenangnya. Perbaikannya dengan membaca selisihnya sebagai ukuran gelembung penalaan.

Ringkasan#

  • best_score_ milik GridSearchCV cenderung menggelembung karena data validasi yang sama ikut memilih kombinasi pemenang yang dilaporkan.
  • Nested CV memisahkan kedua peran tersebut, yaitu loop luar menilai dan loop dalam menala, sehingga skor luar mengukur prosedur penalaan secara utuh.
  • Membungkus GridSearchCV dalam cross_val_score adalah bentuk ringkas nested CV, dan contoh pohon keputusan memperlihatkan gelembung +0.0105 terhadap best_score_.
  • Besarnya gelembung bergantung pada kestabilan model dan bisa nyaris nol, sehingga nested CV berkedudukan sebagai alat penilaian penting dan bukan rutinitas harian.

Referensi#