Pembangunan Pipeline Penuh

Dari ingestion hingga model tertala dalam satu pipeline.

Daftar isi

Model pada tahap ini tidak lagi dibangun potong demi potong, melainkan dirangkai sebagai satu pipeline utuh dari data mentah hingga model tertala. Topik ini menyisihkan test set sejak awal, memasangkan praproses scaling dengan model di dalam Pipeline, membandingkan tiga kandidat model dengan protokol cross-validation yang sama pada data train, lalu menala kandidat terbaik dengan GridSearchCV berukuran kecil. Seluruh angka pada topik ini dihitung tanpa menyentuh test set sama sekali, sehingga test set tetap murni untuk evaluasi akhir.

Menyisihkan test set sejak awal#

Pembagian pertama yang dilakukan bukan pemodelan, melainkan pemisahan data menjadi train dan test dengan train_test_split. Parameter test_size=0.25 menyisihkan seperempat data sebagai test, random_state=42 membuat pembagiannya dapat direproduksi, dan stratify=y menjaga proporsi kedua kelas tetap sama di kedua sisi. Pada dataset proyek, pembagian ini menghasilkan 426 baris train dengan 159 tumor ganas dan 267 tumor jinak, serta 143 baris test dengan 53 tumor ganas dan 90 tumor jinak.

Kedua potongan itu memikul peran yang berbeda. Pembagian semacam ini disebut holdout, yakni menyisihkan sebagian data yang hanya digunakan pada pengujian paling akhir. Data train menjadi satu-satunya bahan pada seluruh tahap ini, yaitu tempat cross-validation berjalan dan hyperparameter, yaitu pengaturan model yang ditetapkan sebelum pelatihan, ditala. Data test disimpan untuk evaluasi akhir dan disentuh tepat satu kali pada saat itu. Disiplin ini mencegah kebocoran informasi dari test ke keputusan pemodelan, penyimpangan yang membuat skor tampak bagus tetapi tidak mewakili data baru.

Dataset569 barisData train 426 barisruang CV dan penalaanData test 143 barisdisisihkan sejak awalModel tertaladinilai sekali di akhirsekali
Alur data proyek: test set disisihkan sejak awal dan disentuh sekali untuk evaluasi akhir

Diagram di atas merangkum alur data proyek. Panah dari dataset menuju train dan test memperlihatkan pembagian yang terjadi sekali di awal, sedangkan panah dari train menuju penalaan menandakan seluruh kerja pemodelan. Panah putus-putus dari test hanya muncul di akhir proyek, satu kali, setelah model final ditetapkan.

Praproses dan model dalam satu Pipeline#

Data dimuat sebagai DataFrame pandas agar nama kolom terbawa sampai ke transformer, kemudian praproses dan model dirangkai dalam satu Pipeline. Langkah pertamanya adalah ColumnTransformer bernama prep yang memilih semua kolom numerik melalui make_column_selector(dtype_include=np.number) dan menerapkan StandardScaler padanya. Semua kolom proyek ini numerik, sehingga pemilihannya untuk saat ini mencakup seluruh kolom, tetapi strukturnya tetap siap menerima kolom kategorikal tanpa menulis ulang pipeline. Langkah kedua bernama clf berisi LogisticRegression(max_iter=1000).

Penempatan StandardScaler di dalam Pipeline bukan sekadar kerapian. Ketika cross-validation dijalankan di atas pipeline, scaler dilatih ulang pada setiap lipatan hanya terhadap bagian latihnya, lalu menerapkan parameter itu pada bagian uji lipatan yang sama. Penyetaraan skala dengan demikian tidak pernah mencuri informasi dari data uji. Pola ini sekaligus menuntaskan keputusan desain dari audit data, yaitu menetralkan rentang fitur yang timpang hingga lima orde besaran.

Skor yang dilaporkan juga perlu disiapkan dengan sengaja. Metrik biner scikit-learn secara default memperlakukan label 1 sebagai kelas positif, padahal label 1 pada dataset ini adalah jinak. Tanpa penyesuaian, recall yang dilaporkan justru mengukur kelas jinak. Scorer, yakni fungsi penilaian yang digunakan cross-validation, dibangun melalui make_scorer(recall_score, pos_label=0) sehingga skor yang keluar adalah recall kelas ganas, metrik utama proyek.

python pipeline-cv.py
import numpy as np
import pandas as pd
from sklearn.datasets import load_breast_cancer
from sklearn.compose import ColumnTransformer, make_column_selector
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split, cross_val_score, StratifiedKFold
from sklearn.metrics import recall_score, make_scorer

# dataset proyek dimuat sebagai DataFrame agar nama kolom terbawa
cancer = load_breast_cancer()
X = pd.DataFrame(cancer.data, columns=cancer.feature_names)
y = cancer.target

# test set disisihkan sejak awal dan tidak dipakai di bagian ini
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.25, random_state=42, stratify=y
)
print("Bentuk train:", X_train.shape, " Bentuk test:", X_test.shape)
print("Kelas train :", np.bincount(y_train))
print("Kelas test  :", np.bincount(y_test))

# scaling seluruh kolom numerik, dirangkai dengan model dalam satu Pipeline
praproses = ColumnTransformer([
    ("num", StandardScaler(), make_column_selector(dtype_include=np.number)),
])
pipeline = Pipeline([
    ("prep", praproses),
    ("clf", LogisticRegression(max_iter=1000)),
])

# label 0 = ganas, sehingga recall harus diminta eksplisit untuk kelas 0
recall_ganas = make_scorer(recall_score, pos_label=0)
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
skor = cross_val_score(pipeline, X_train, y_train, cv=cv, scoring=recall_ganas)
print("Recall ganas per lipatan:", np.round(skor, 3))
print(f"Recall ganas train (CV) : {skor.mean():.3f} ± {skor.std():.3f}")
text Keluaran
Bentuk train: (426, 30)  Bentuk test: (143, 30)
Kelas train : [159 267]
Kelas test  : [53 90]
Recall ganas per lipatan: [0.938 0.938 0.938 0.969 1.   ]
Recall ganas train (CV) : 0.956 ± 0.025

Keluaran menegaskan komposisi kedua potongan data dan protokol penilaiannya. Empat lipatan pertama mencetak recall ganas di kisaran 0.938 hingga 0.969, sedangkan lipatan kelima mencapai 1.000 karena bagian ujinya kecil, hanya sekitar 32 baris ganas, sehingga lipatan tanpa satu pun kesalahan langsung bernilai penuh. Rata-ratanya 0.956 dengan simpangan baku 0.025, dan angka ini menjadi estimasi kerja untuk kandidat pertama.

Membandingkan tiga kandidat dengan protokol yang sama#

Perbandingan kandidat hanya bermakna bila seluruh kondisinya identik kecuali model pada langkah terakhirnya. Protokolnya tetap StratifiedKFold lima lipatan dengan shuffle=True dan random_state=42, metriknya tetap recall ganas melalui scorer yang sama, dan pipelinenya tetap ColumnTransformer plus scaler. Tiga kandidat yang diadu adalah LogisticRegression(max_iter=1000) sebagai model linear, RandomForestClassifier(random_state=42) sebagai ansambel pohon berbasis bagging, yaitu pelatihan banyak pohon secara paralel pada sampel acak, dan HistGradientBoostingClassifier(random_state=42) sebagai ansambel pohon berbasis boosting, yaitu pelatihan pohon secara berurutan untuk memperbaiki kesalahan sebelumnya.

python Perbandingan tiga kandidat model pada data train
import numpy as np
import pandas as pd
from sklearn.datasets import load_breast_cancer
from sklearn.compose import ColumnTransformer, make_column_selector
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier, HistGradientBoostingClassifier
from sklearn.model_selection import train_test_split, cross_val_score, StratifiedKFold
from sklearn.metrics import recall_score, make_scorer

cancer = load_breast_cancer()
X = pd.DataFrame(cancer.data, columns=cancer.feature_names)
y = cancer.target
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.25, random_state=42, stratify=y
)

praproses = ColumnTransformer([
    ("num", StandardScaler(), make_column_selector(dtype_include=np.number)),
])
recall_ganas = make_scorer(recall_score, pos_label=0)
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

# tiga kandidat diadu dengan pipeline, CV, dan scorer yang sama persis
kandidat = {
    "LogisticRegression": LogisticRegression(max_iter=1000),
    "RandomForestClassifier": RandomForestClassifier(random_state=42),
    "HistGradientBoostingClassifier": HistGradientBoostingClassifier(random_state=42),
}

for nama, model in kandidat.items():
    pipeline = Pipeline([("prep", praproses), ("clf", model)])
    skor = cross_val_score(pipeline, X_train, y_train, cv=cv, scoring=recall_ganas)
    print(f"{nama:33s} recall ganas = {skor.mean():.3f} ± {skor.std():.3f}")
text Keluaran
LogisticRegression                recall ganas = 0.956 ± 0.025
RandomForestClassifier            recall ganas = 0.950 ± 0.032
HistGradientBoostingClassifier    recall ganas = 0.944 ± 0.023

Ketiganya jauh melampaui baseline recall ganas 0.000, dan selisih antar kandidat tergolong kecil. LogisticRegression memimpin dengan 0.956 ± 0.025, disusul RandomForestClassifier dengan 0.950 ± 0.032 dan HistGradientBoostingClassifier dengan 0.944 ± 0.023. Simpangan baku yang lebih besar pada random forest menunjukkan performanya lebih goyah antar lipatan. Selisih unggul 0.006 memang tipis, tetapi kandidat terpilih juga paling sederhana, paling cepat dilatih, dan koefisiennya dapat dibaca, sehingga pilihan jatuh pada LogisticRegression.

Penalaan GridSearchCV berukuran kecil#

GridSearchCV menelaah hyperparameter dengan mencoba setiap kombinasi grid dan menilainya menggunakan cross-validation di dalamnya. Parameter milik model di dalam pipeline ditulis dengan awalan nama langkah dan dua garis bawah, sehingga C milik LogisticRegression menjadi clf__C. Grid penalaan proyek ini sengaja kecil, yaitu empat nilai C yang membentuk empat kombinasi, sehingga total pelatihan berjumlah 4 × 5 = 20 kali dan seluruhnya berjalan di atas data train.

python Penalaan GridSearchCV kecil pada LogisticRegression
import numpy as np
import pandas as pd
from sklearn.datasets import load_breast_cancer
from sklearn.compose import ColumnTransformer, make_column_selector
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split, GridSearchCV, StratifiedKFold
from sklearn.metrics import recall_score, make_scorer

cancer = load_breast_cancer()
X = pd.DataFrame(cancer.data, columns=cancer.feature_names)
y = cancer.target
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.25, random_state=42, stratify=y
)

praproses = ColumnTransformer([
    ("num", StandardScaler(), make_column_selector(dtype_include=np.number)),
])
pipeline = Pipeline([
    ("prep", praproses),
    ("clf", LogisticRegression(max_iter=1000)),
])

# grid kecil: empat kombinasi, seluruhnya dinilai pada train saja
param_grid = {"clf__C": [0.01, 0.1, 1.0, 10.0]}
recall_ganas = make_scorer(recall_score, pos_label=0)
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

cari = GridSearchCV(pipeline, param_grid, cv=cv, scoring=recall_ganas)
cari.fit(X_train, y_train)

for C, skor in zip(param_grid["clf__C"], cari.cv_results_["mean_test_score"]):
    print(f"C = {C:5.2f}  recall ganas rata-rata = {skor:.3f}")
print("Parameter terbaik:", cari.best_params_)
print(f"Skor CV terbaik  : {cari.best_score_:.3f}")
text Keluaran
C =  0.01  recall ganas rata-rata = 0.869
C =  0.10  recall ganas rata-rata = 0.944
C =  1.00  recall ganas rata-rata = 0.956
C = 10.00  recall ganas rata-rata = 0.956
Parameter terbaik: {'clf__C': 1.0}
Skor CV terbaik  : 0.956

Hasil penalaan menunjukkan pola yang informatif. Nilai C=0.01 yang menekan model terlalu kuat merusak recall hingga 0.869, C=0.1 memulihkannya ke 0.944, sedangkan C=1.0 dan C=10.0 sama-sama mencetak 0.956. GridSearchCV memilih C=1.0 karena pada skor yang seri ia menempati urutan pertama dalam grid. Dengan best_params_ dan best_score_ yang tercatat, konfigurasi final proyek menjadi LogisticRegression(max_iter=1000, C=1.0) di dalam pipeline yang sama, dan test set masih belum disentuh sama sekali.

Kesalahan umum#

  • Menjalankan scaling sebelum pemisahan data. Scaler yang dilatih pada seluruh data membawa statistik test ke dalam train dan menggelembungkan skor CV. Perbaikannya dengan menaruh scaler di dalam Pipeline dan membiarkan cross-validation melatih ulang scaler pada tiap lipatan.
  • Menala hyperparameter dengan melihat skor test. Test yang digunakan berulang pada penalaan berhenti menjadi pengukur jujur karena keputusan sudah menyesuaikan diri dengan test. Perbaikannya dengan membatasi grid dan penilaian pada train.
  • Melaporkan recall tanpa pos_label. Nilai default mengukur kelas label 1, yaitu jinak pada dataset ini, sehingga angkanya terlihat tinggi tanpa mengukur kelas ganas. Perbaikannya dengan make_scorer(recall_score, pos_label=0).

Ringkasan#

  • Test set 143 baris disisihkan sejak awal melalui train_test_split dengan stratify=y dan random_state=42, dan tidak disentuh sepanjang tahap ini.
  • Praproses dan model hidup dalam satu Pipeline, sehingga StandardScaler dilatih ulang pada tiap lipatan CV tanpa mencuri informasi dari bagian uji.
  • Dengan scorer recall ganas (pos_label=0), tiga kandidat menghasilkan 0.956, 0.950, dan 0.944, dan LogisticRegression dipilih.
  • Penalaan empat kombinasi clf__C menghasilkan C=1.0 dengan skor CV 0.956.

Referensi#