Proyek machine learning jarang berhenti pada satu notebook. Topik ini membahas cara mengorganisasi kode proyek machine learning melampaui notebook, yaitu memisahkan data, kode sumber, eksplorasi, dan artefak ke dalam direktori yang berperan tetap. Pembahasannya mencakup batas notebook, kerangka direktori proyek, disiplin yang ditegakkan setiap direktori, serta satu skrip mini yang mempraktikkan pemisahan tahap tersebut pada dataset kanker payudara.
Notebook dan batasnya#
Notebook sangat cocok untuk eksplorasi karena setiap sel dijalankan segera, grafiknya tampil di tempat, dan urutan berpikirnya terdokumentasi bersama kodenya. Pekerjaan seperti memahami data, mencoba fitur, dan memeriksa hasil antara nyaman dikerjakan di notebook. Posisi itu tidak perlu disaingi oleh bentuk lain selama pekerjaannya memang eksplorasi.
Persoalan muncul ketika notebook menjadi satu-satunya tempat kode proyek hidup. Keadaan programnya tersembunyi, karena sel dijalankan dalam urutan yang tidak selalu terbaca dari berkasnya. Hasil di layar pun dapat berbeda dari hasil yang diperoleh orang lain ketika notebook dijalankan ulang. Kodenya juga sulit dipakai ulang karena tidak ada fungsi yang dapat diimpor dari luar notebook, dan pengujian otomatis sulit dilakukan karena tidak ada titik masuk yang jelas.
Karena itu kriteria pindahnya kode sudah cukup jelas. Fungsi yang sudah mantap dan dipakai lebih dari satu kali, misalnya pemuatan data atau preprocessing, dipindahkan ke berkas kode sumber, sedangkan notebook tetap memakainya lewat impor. Notebook lalu berperan sebagai buku catatan eksperimen yang memanggil kode yang sama dengan yang dijalankan produksi.
Kerangka direktori proyek#
Struktur berikut adalah kerangka lazim untuk proyek klasifikasi berskala kecil hingga menengah. Setiap direktori memegang satu peran, sehingga tempat setiap jenis berkas tidak lagi menjadi keputusan baru setiap hari.
proyek-klasifikasi/
├── data/
│ └── mentah/ ← data mentah, hanya dibaca
├── notebook/
│ └── eksplorasi-awal.ipynb ← eksplorasi, bukan kode produksi
├── src/
│ ├── data.py ← memuat dan memisahkan data
│ ├── model.py ← definisi pipeline dan pelatihan
│ └── evaluasi.py ← metrik dan laporan hasil
├── artefak/
│ └── model-v1.joblib ← keluaran pelatihan, diberi versi
├── requirements.txt ← versi pustaka yang dikunci
└── README.md ← cara menjalankan proyekDirektori data/ menyimpan data mentah yang hanya dibaca, sedangkan setiap hasil pemrosesan disimpan ke lokasi terpisah sehingga sumbernya tidak pernah rusak. Direktori src/ menyimpan kode sumber yang ditulis sebagai fungsi yang dapat diimpor. Direktori notebook/ menyimpan eksplorasi yang mengimpor dari src/, sedangkan direktori artefak/ menampung keluaran pelatihan yang diberi versi sebagaimana dibahas pada topik persistensi. Nama direktorinya boleh berbeda antar proyek, tetapi pemisahan peran inilah yang perlu dipertahankan.
Berkas README.md menutup kerangka ini dengan tugas yang paling sering diabaikan, yaitu menjelaskan cara menjalankan proyek secara ringkas. Isinya memuat perintah untuk menyiapkan lingkungan, melatih model, dan menghasilkan evaluasi, ditambah penjelasan singkat tentang peran setiap direktori. Proyek yang strukturnya rapi tetapi hanya penulis aslinya yang tahu cara menjalankannya akan berhenti ketika penulisnya berpindah.
Disiplin yang ditegakkan struktur#
Struktur hanya berguna bila disiplinnya dijalankan. Data mentah tidak pernah disunting di tempat, karena perbaikan yang tidak lewat kode tidak tercatat dan tidak dapat diulang. Perubahan data dilakukan oleh fungsi di src/, dan keluarannya disimpan sebagai berkas baru yang dapat dilacak. Kode di src/ ditulis sebagai fungsi murni yang dapat diimpor dan diuji. Penggabungannya diletakkan di balik penjaga if __name__ == "__main__", yaitu bagian yang hanya berjalan ketika berkas dieksekusi sebagai program. Bagian itu tidak berjalan ketika berkasnya diimpor.
Skrip berikut adalah versi mini dari disiplin itu dalam satu berkas, yaitu empat fungsi yang mewakili empat tahap terpisah dan satu blok utama yang menyusunnya. Pada proyek sesungguhnya, keempat fungsi itu tinggal dipindahkan ke berkas-berkas di src/ tanpa mengubah isinya.
"""Versi mini struktur proyek: pemisahan tahap dalam satu berkas teruji."""
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
def muat_data():
"""Tahap data: satu-satunya tempat sumber data dibaca."""
data = load_breast_cancer()
return data.data, data.target, data.feature_names
def siapkan(X, y):
"""Tahap penyiapan: pemisahan latih-uji dengan seed yang dipatok."""
return train_test_split(X, y, test_size=0.25, random_state=42, stratify=y)
def latih(X_latih, y_latih):
"""Tahap pelatihan: pipeline agar penskalaan ikut dalam satu objek."""
model = make_pipeline(StandardScaler(), LogisticRegression(max_iter=5000))
return model.fit(X_latih, y_latih)
def evaluasi(model, X_uji, y_uji):
"""Tahap evaluasi: satu angka yang dilaporkan apa adanya."""
return model.score(X_uji, y_uji)
if __name__ == "__main__":
X, y, nama_fitur = muat_data()
X_latih, X_uji, y_latih, y_uji = siapkan(X, y)
model = latih(X_latih, y_latih)
print(f"Jumlah fitur : {len(nama_fitur)}")
print(f"Baris latih : {X_latih.shape[0]}, baris uji : {X_uji.shape[0]}")
print(f"Skor data uji: {evaluasi(model, X_uji, y_uji):.4f}")Jumlah fitur : 30
Baris latih : 426, baris uji : 143
Skor data uji: 0.9860Setiap fungsi melakukan satu hal dan tidak menyentuh keadaan di luar argumennya, sehingga fungsi mana pun dapat diuji terpisah atau dipanggil dari notebook tanpa menjalankan sisanya. Pipeline pada fungsi latih menjaga agar penskalaan dan model tidak pernah terpisah, dan blok utama hanya menyusun urutan panggilannya. Skor data uji 0.9860 pada dataset kanker payudara dihitung dengan fungsi evaluasi tersebut, dan fungsi yang sama dipakai ulang di notebook maupun skrip pelatihan resminya.
Kesalahan umum#
- Menyunting data mentah di tempat. Perbaikan tidak tercatat, tidak dapat dibalik, dan tidak dapat diulang di mesin lain. Perbaiki lewat kode di
src/dan simpan keluarannya sebagai berkas terpisah. - Membiarkan notebook menjadi satu-satunya tempat logika hidup. Fungsi penting terkunci di dalam sel dan tidak dapat diuji maupun diimpor. Pindahkan fungsi yang sudah mantap ke
src/dan impor dari sana. - Menyalin-tempel kode preprocessing ke skrip penyajian. Dua salinan yang terpisah perlahan berbeda tanpa pesan apa pun. Gunakan pipeline yang tersimpan sebagai satu artefak sebagaimana dibahas pada topik persistensi model.
- Memperlakukan struktur sebagai formalitas awal proyek. Seiring waktu, berkas data dan skrip menumpuk di akar proyek sehingga peran setiap direktori bercampur lagi. Letakkan setiap berkas baru langsung pada direktori yang sesuai dengan perannya sejak hari pertama.
- Menyebarkan nilai konfigurasi seperti lokasi data dan seed di banyak berkas. Satu perubahan kecil menuntut penyuntingan di beberapa tempat, dan satu tempat yang terlewat menghasilkan pelatihan yang berbeda. Kumpulkan nilai semacam itu pada satu berkas konfigurasi yang diimpor oleh kode lain.
Ringkasan#
- Struktur proyek memisahkan data, kode sumber, notebook, dan artefak agar setiap direktori memegang satu peran.
- Data mentah hanya dibaca, perubahannya dikerjakan kode, dan hasilnya disimpan sebagai berkas baru yang terlacak.
- Fungsi yang dapat diimpor membuat kode dapat diuji dan dipakai ulang, sedangkan notebook tetap menjadi ruang eksplorasinya.
- Blok
if __name__ == "__main__"memisahkan berkas sebagai modul yang diimpor dan sebagai program yang dijalankan.
Referensi#
- PEP 8 — Style Guide for Python Code — Python — Panduan gaya kode Python yang menjadi dasar penataan berkas kode sumber.
- Pipelines and composite estimators — scikit-learn — Panduan pipeline yang menyatukan preprocessing dan model pada fungsi pelatihan.
- main — Top-level code environment — Python
— Dokumentasi lingkungan
__main__yang menjelaskan perilaku blokif __name__ == "__main__".