Topik ini membahas cross-validation sebagai cara mengukur kinerja model yang lebih stabil daripada satu pemecahan data tunggal. Pembahasan mencakup kelemahan satu pemecahan train/test, mekanisme k-fold, perbedaan KFold dan StratifiedKFold, serta penggunaan cross_val_score untuk memperoleh skor tiap lipatan beserta rata-rata dan simpangan bakunya. Seluruh contoh dijalankan pada dataset bawaan scikit-learn dengan benih acak yang dapat direproduksi.
Satu pemecahan tunggal mudah beruntung#
Evaluasi dengan train_test_split menilai model pada satu pemecahan data saja. Skor yang dihasilkan bergantung pada baris mana yang kebetulan jatuh ke data uji, sehingga pemecahan yang berbeda pada model yang sama dapat menghasilkan skor yang berbeda pula. Pergeseran tersebut murni berasal dari keberuntungan pembagian, bukan dari perubahan kemampuan model, karena dataset dan konfigurasinya sama persis.
Kode berikut melatih DecisionTreeClassifier yang sama pada dataset breast cancer dengan empat benih acak pemecahan yang berbeda. Model, dataset, dan argumen lainnya identik, sehingga satu-satunya yang berubah adalah komposisi data ujinya.
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
X, y = load_breast_cancer(return_X_y=True)
model = DecisionTreeClassifier(random_state=42)
for seed in [42, 7, 0, 2024]:
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.25, random_state=seed, stratify=y
)
model.fit(X_train, y_train)
print(f"random_state={seed}: akurasi = {model.score(X_test, y_test):.4f}")random_state=42: akurasi = 0.9231
random_state=7: akurasi = 0.9580
random_state=0: akurasi = 0.9301
random_state=2024: akurasi = 0.9371Akurasinya bergerak dari 0.9231 hingga 0.9580, yaitu selisih sekitar tiga setengah poin persen pada model yang sama. Apabila hanya satu pemecahan yang dilakukan, angka yang diperoleh bisa jadi yang terendah, tertinggi, atau di antaranya, dan tidak ada cara mengetahuinya dari satu skor saja. Ketidakpastian inilah yang ingin ditekan oleh cross-validation dengan menilai model pada beberapa pemecahan sekaligus lalu merangkum hasilnya.
Mekanisme k-fold cross-validation#
Cross-validation k-fold membagi data menjadi k bagian sama besar yang disebut lipatan (fold). Pelatihan dijalankan sebanyak k kali, dan pada setiap putaran satu lipatan berperan sebagai data uji sedangkan k-1 lipatan sisanya menjadi data latih. Setiap baris data diuji tepat satu kali karena posisi lipatan ujinya bergilir, sehingga penilaian tidak lagi bertumpu pada satu kelompok baris yang beruntung.
Diagram berikut menggambarkan skema lima lipatan. Setiap baris mewakili satu putaran evaluasi, dan blok data uji bergeser satu posisi pada setiap barisnya.
Dengan k=5, setiap model dilatih pada sekitar 80 persen data dan diuji pada 20 persen sisanya, kemudian k skor yang dihasilkan dirangkum menjadi satu. Biayanya waktu pelatihan yang berlipat karena model dibangun k kali, sedangkan keuntungannya adalah estimasi yang tidak lagi bergantung pada satu pembagian. Nilai k yang lazim digunakan adalah 5 atau 10, dengan pertukaran antara biaya komputasi dan kestabilan estimasi.
KFold dan StratifiedKFold#
Kelas KFold membagi data menjadi k lipatan tanpa memperhatikan label, sedangkan StratifiedKFold menjaga proporsi kelas pada setiap lipatan agar mendekati proporsi data penuh. Untuk masalah klasifikasi, scikit-learn menggunakan StratifiedKFold secara bawaan ketika argumen cv diisi bilangan bulat, sehingga pemanggilan cv=5 pada classifier sudah menghasilkan lipatan berstrata. KFold tetap diperlukan untuk regresi dan data tanpa label kelas. Kedua kelas menerima argumen n_splits, shuffle, dan random_state dengan makna yang sama, sehingga kebiasaan yang dibangun pada salah satunya langsung berlaku pada yang lain.
Perbedaannya paling terlihat pada data yang tidak seimbang. Dataset breast cancer memuat 212 sampel kelas pertama dan 357 sampel kelas kedua, dan kode berikut membandingkan komposisi kelas pada kelima lipatan yang dihasilkan kedua pembagi tersebut.
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import KFold, StratifiedKFold
import numpy as np
X, y = load_breast_cancer(return_X_y=True)
print("Jumlah baris per kelas di seluruh data:", np.bincount(y))
kfold = KFold(n_splits=5, shuffle=True, random_state=42)
strat = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
for nama, cv in [("KFold", kfold), ("StratifiedKFold", strat)]:
print(f"{nama}:")
for i, (_, idx_uji) in enumerate(cv.split(X, y), start=1):
print(f" lipatan {i}: {np.bincount(y[idx_uji])}")Jumlah baris per kelas di seluruh data: [212 357]
KFold:
lipatan 1: [43 71]
lipatan 2: [37 77]
lipatan 3: [43 71]
lipatan 4: [43 71]
lipatan 5: [46 67]
StratifiedKFold:
lipatan 1: [43 71]
lipatan 2: [43 71]
lipatan 3: [42 72]
lipatan 4: [42 72]
lipatan 5: [42 71]Lipatan KFold memuat antara 37 dan 46 sampel kelas pertama, sedangkan StratifiedKFold menahannya pada kisaran 42 sampai 43. Data uji yang komposisinya berpindah-pindah membuat skor antarlipatan sulit diperbandingkan karena perbedaannya bisa berasal dari komposisi kelas dan bukan dari model. Stratifikasi menghilangkan sumber goyah ini dengan biaya yang hampir nol.
Skor per lipatan dengan cross_val_score#
Fungsi cross_val_score menjalankan seluruh skema k-fold dan mengembalikan array berisi skor tiap lipatan. Estimator yang diberikan digandakan untuk setiap lipatan, dilatih pada bagian latihnya, lalu dinilai pada bagian ujinya, sehingga praproses yang berada di dalam pipeline ikut dipasang ulang hanya dari data latih setiap lipatan. Argumen cv menerima bilangan bulat maupun objek pembagi seperti StratifiedKFold.
Kode berikut mengevaluasi pipeline StandardScaler dan LogisticRegression pada dataset breast cancer, pertama dengan cv=5 bawaan dan kedua dengan StratifiedKFold eksplisit yang diacak.
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
X, y = load_breast_cancer(return_X_y=True)
# cv=5: untuk classifier, StratifiedKFold tanpa shuffle dipakai secara bawaan
skor_default = cross_val_score(
make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000)),
X, y, cv=5,
)
# StratifiedKFold eksplisit dengan shuffle dan random_state
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
skor_shuffle = cross_val_score(
make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000)),
X, y, cv=cv,
)
print("Skor per lipatan (cv=5) :", skor_default.round(4))
print(f"Rata-rata: {skor_default.mean():.4f}, simpangan baku: {skor_default.std():.4f}")
print("Skor per lipatan (shuffle):", skor_shuffle.round(4))
print(f"Rata-rata: {skor_shuffle.mean():.4f}, simpangan baku: {skor_shuffle.std():.4f}")Skor per lipatan (cv=5) : [0.9825 0.9825 0.9737 0.9737 0.9912]
Rata-rata: 0.9807, simpangan baku: 0.0065
Skor per lipatan (shuffle): [0.9737 0.9474 0.9649 0.9912 0.9912]
Rata-rata: 0.9737, simpangan baku: 0.0166Setiap angka pada array keluaran adalah skor dari satu lipatan, dan rata-ratanya menjadi estimasi kinerja model. Simpangan baku antarlipatan melengkapi angka tersebut, yaitu ukuran seberapa jauh skor berubah bila data dipilah secara berbeda. Nilai 0.0166 pada versi shuffle lebih besar daripada 0.0065 pada versi tanpa shuffle, dan kedua versi sama-sama sah karena keduanya menggunakan lipatan berstrata, sedangkan perbedaan rata-ratanya berasal dari keanggotaan lipatan yang berbeda.
Praktik pelaporannya sederhana, yaitu mencantumkan rata-rata disertai simpangan baku atau rentang antarlipatan, misalnya 0.9737 dengan simpangan baku 0.0166. Skor satu lipatan tidak dilaporkan sebagai kinerja model karena angka itu milik satu pemecahan, bukan milik skema evaluasinya.
Kesalahan umum#
- Memakai
KFoldpolos pada klasifikasi tidak seimbang. Komposisi kelas tiap lipatannya bergeser sehingga skor antarlipatan tidak setara. Perbaikannya dengancv=5pada classifier atauStratifiedKFoldeksplisit. - Melaporkan skor satu lipatan sebagai kinerja model. Satu angka milik satu pemecahan dan bisa beruntung atau sial. Perbaikannya dengan melaporkan rata-rata beserta simpangan baku antarlipatan.
- Membandingkan dua model pada lipatan yang berbeda. Selisih skornya bisa berasal dari komposisi lipatan dan bukan dari model. Perbaikannya dengan objek pembagi yang sama, benih dan semua, untuk semua model yang diperbandingkan.
- Menaruh praproses di luar pipeline sebelum cross-validation. Statistik data uji ikut terhitung dan skornya menggelembung. Perbaikannya dengan menaruh seluruh praproses ke dalam
Pipelinesebagaimana dibahas pada modul pipeline.
Ringkasan#
- Satu pemecahan train/test menghasilkan skor yang bergantung pada keberuntungan pembagian, dan contoh pada dataset yang sama memberi rentang akurasi 0.9231 hingga 0.9580.
- k-fold membagi data menjadi k lipatan, melatih k kali, dan menilai setiap baris tepat satu kali, sehingga estimasi tidak lagi bertumpu pada satu pemecahan.
StratifiedKFoldmenjaga proporsi kelas tiap lipatan dan digunakan secara bawaan ketika argumen cv diisi bilangan bulat pada classifier.cross_val_scoremengembalikan skor tiap lipatan, dan rata-rata beserta simpangan baku keduanya dilaporkan sebagai estimasi kinerja.
Referensi#
- Cross-validation: evaluating estimator performance — scikit-learn — Panduan resmi mekanisme k-fold dan variasi pembagian lipatannya.
- sklearn.model_selection.cross_val_score — scikit-learn — Dokumentasi fungsi evaluasi yang mengembalikan skor per lipatan.
- sklearn.model_selection.KFold — scikit-learn — Dokumentasi pembagi k-fold beserta argumen shuffle dan random_state.
- sklearn.model_selection.StratifiedKFold — scikit-learn — Dokumentasi pembagi berstrata yang menjaga proporsi kelas per lipatan.