Chapter Transformer Kustom ditutup dengan satu artefak PangkasKuantil → StandardScaler → Ridge yang kini merepresentasikan seluruh keputusan analisis — tetapi artefak tersebut masih hidup sebagai objek di memori sesi Python. Begitu sesi berakhir, seluruh status terpelajari ikut hilang: batas kuantil, rata-rata scaler, koefisien model. Sementara itu, Chapter Model Serving dengan FastAPI di Section Deployment dan Operasional justru memulai dari sisi sebaliknya: API prediksi membutuhkan model yang sudah tersimpan di disk untuk dimuat saat layanan dinyalakan. Chapter ini menjembatani kedua dunia tersebut dengan joblib — menyimpan pipeline utuh ke satu berkas, memuatnya kembali di sesi berbeda, lalu melengkapi berkas model dengan manifes yang bisa diaudit tanpa harus memuatnya.
Dari Memori ke Disk: Masalah Artefak #
Model yang hanya hidup di sesi pelatihan tidak bisa dipakai oleh siapa pun. Tiga kebutuhan nyata menuntut artefak berkas. Pertama, inferensi terpisah dari pelatihan: pelatihan mahal dan sesekali, prediksi murah dan terus-menerus — keduanya tidak mungkin berbagi satu sesi Python. Kedua, distribusi: tim lain, server lain, atau layanan API perlu memuat model yang sama tanpa mengulang pelatihan. Ketiga, audit dan reproduktivitas: saat hasil model dipertanyakan enam bulan kemudian, kita harus bisa menunjuk berkas persis yang menghasilkan prediksi tersebut, lengkap dengan versi pustaka pembentuknya.
Format penyimpanan yang kita pakai adalah pickle joblib — serialisasi objek Python yang dikemas ulang oleh pustaka joblib dengan penanganan efisien untuk array NumPy besar. joblib dipilih bukan karena satu-satunya opsi, melainkan karena dirancang khusus untuk objek scikit-learn dan menjadi rekomendasi resmi dokumentasinya.
| Alternatif | Kelebihan | Batasan |
|---|---|---|
joblib.dump | Bawaan ekosistem sklearn, efisien untuk array | Hanya bisa dimuat Python + pustaka kompatibel |
pickle standar | Tanpa dependensi tambahan | Lambat untuk array besar, format sama-sama rapuh |
| ONNX / PMML | Lintas bahasa | Perlu konversi; tidak semua langkah kustom terdukung |
joblib: Simpan dan Muat Pipeline Utuh #
Contoh pertama melatih pipeline tiga langkah — imputasi median, standardisasi, regresi Ridge — lalu menyimpannya dengan joblib.dump(). Yang kritis diperiksa: apakah objek yang dimuat kembali menghasilkan prediksi yang identik byte demi byte, dan apakah seluruh status terpelajari ikut tertanam.
# Menyimpan pipeline terlatih dengan joblib.dump() lalu memuatnya kembali
# dengan joblib.load(). Prediksi objek asli dan objek dimuat dibandingkan
# byte demi byte; seluruh status terpelajari (statistik imputer, scaler,
# koefisien) harus ikut tertanam di dalam artefak.
import os
import joblib
import numpy as np
from sklearn.impute import SimpleImputer
from sklearn.linear_model import Ridge
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
# ---------- Data: 300 rumah, 23 nilai sensor hilang ----------
rng = np.random.default_rng(42)
n = 300
X = rng.normal(50, 10, size=(n, 4))
y = 2.0 * X[:, 0] + 1.2 * X[:, 1] - 1.5 * X[:, 2] \
+ rng.normal(0, 1.0, n)
baris_hilang = rng.choice(n, size=23, replace=False)
X[baris_hilang, 1] = np.nan
pipeline = Pipeline(
[
("imputasi", SimpleImputer(strategy="median")),
("skala", StandardScaler()),
("model", Ridge(alpha=1.0)),
]
)
pipeline.fit(X, y)
# ---------- Prediksi dengan objek asli ----------
baru = rng.normal(50, 10, size=(5, 4))
prediksi_asli = pipeline.predict(baru)
BERKAS = "pipeline_harga.joblib"
joblib.dump(pipeline, BERKAS)
ukuran = os.path.getsize(BERKAS)
muat = joblib.load(BERKAS)
prediksi_muat = muat.predict(baru)
print("== Prediksi 5 baris baru oleh objek asli ==")
print(np.array2string(prediksi_asli, precision=2, separator=" "))
print(f"\n== Artefak {BERKAS} ({ukuran:,} byte) disimpan lalu dimuat ==")
print(f"kelas objek : {type(muat).__name__}")
print(f"langkah : {list(muat.named_steps.keys())}")
print("\n== Status terpelajari ikut terbawa ==")
print(f"median imputer : {muat.named_steps['imputasi'].statistics_}")
print(f"rata-rata skala: {np.round(muat.named_steps['skala'].mean_, 2)}")
print(f"koefisien model: {np.round(muat.named_steps['model'].coef_, 2)}")
print("\n== Prediksi objek dimuat identik dengan objek asli ==")
print(f"identik byte demi byte: {np.array_equal(prediksi_asli, prediksi_muat)}")
print("\n== Versi pustaka saat artefak dibuat ==")
import sklearn
print(f"scikit-learn {sklearn.__version__} | numpy {np.__version__} "
f"| joblib {joblib.__version__}")
os.remove(BERKAS)
print(f"\nartefak {BERKAS} dihapus dari folder kerja (contoh selesai)")Output yang Dihasilkan:
== Prediksi 5 baris baru oleh objek asli ==
[ 60.18 46.93 121.62 100.59 100.11]
== Artefak pipeline_harga.joblib (1,438 byte) disimpan lalu dimuat ==
kelas objek : Pipeline
langkah : ['imputasi', 'skala', 'model']
== Status terpelajari ikut terbawa ==
median imputer : [49.73794053 49.59237474 50.06177871 50.45690003]
rata-rata skala: [49.57 49.45 49.9 49.89]
koefisien model: [ 19.37 11.6 -15.52 0.18]
== Prediksi objek dimuat identik dengan objek asli ==
identik byte demi byte: True
== Versi pustaka saat artefak dibuat ==
scikit-learn 1.5.2 | numpy 2.1.3 | joblib 1.5.3
artefak pipeline_harga.joblib dihapus dari folder kerja (contoh selesai)Mari kita bedah bagian-bagian kuncinya:
- Baris 15–22 (data dengan nilai hilang):
ydihitung dari kolom 0–2 sebelum 23 nilai kolom 1 ditandai hilang — urutan ini penting agar target tetap utuh sementara fitur membutuhkan imputasi. Median kolom 1 kelak menjadi status terpelajari yang wajib ikut artefak. - Baris 37–42 (dump lalu load):
joblib.dump(pipeline, BERKAS)menulis seluruh objek — termasuk daftar langkah dan setiap statusfit()— ke satu berkas 1.438 byte.joblib.load()membangun ulang objek yang sama persis di memori. Tidak ada ekspor-impor parameter manual, tidak ada perakitan ulang langkah. - Baris 51–54 (status selamat): median imputer
[49,74, 49,59, 50,06, 50,46], rata-rata scaler, dan koefisien[19,37, 11,60, −15,52, 0,18]terbaca utuh dari objek dimuat — bukti serialisasi membawa seluruh atribut berakhiran_. - Baris 56–57 (ujung benang merah):
np.array_equalmengembalikanTrue— prediksi objek asli dan objek dimuat identik penuh. Inilah kontrak yang dipertaruhkan: satu angka beda berarti artefak rusak. - Baris 59–63 (versi tercatat): versi pustaka dicetak saat artefak dibuat — informasi yang kelak kita bawa ke manifes. scikit-learn 1.5.2, numpy 2.1.3, joblib 1.5.3.
- Baris 65 (kebersihan): artefak dihapus di akhir contoh; di proyek nyata, langkah ini digantikan pemindahan ke direktori artefak yang dikelola versinya.
Apa yang Ikut Tersimpan #
Serialisasi pipeline bukan sekadar menyalin koefisien. Seluruh struktur objek ikut tertanam, dan tabel berikut merangkum apa saja yang harus hadir agar artefak berdiri sendiri.
| Komponen | Tersimpan sebagai | Kegunaan saat dimuat |
|---|---|---|
| Urutan langkah pipeline | Daftar tuple bernama | predict() langsung bisa dipanggil |
| Status transformer | Atribut statistics_, mean_, batas_*_ | Preprocessing data baru konsisten |
| Parameter model | coef_, intercept_, classes_ | Prediksi tanpa pelatihan ulang |
Kelas kustom (mis. PangkasKuantil) | Referensi kelas + atributnya | Definisi kelas harus tetap bisa diimpor |
| Versi pustaka implisit | Struktur pickle bergantung versi | Dimuat di lingkungan yang sama versinya |
Baris terakhir tabel itu menyimpan dua jebakan yang paling sering menggigit di praktik — dan keduanya bukan masalah teknis semata, melainkan masalah kontrak antar tim.
Manifes Artefak untuk Audit #
Berkas model tanpa keterangan adalah kotak hitam: tidak ada yang tahu pustaka versi apa yang membentuknya, hyperparameter apa yang dipakai, atau apakah isinya masih utuh sejak disimpan. Manifes menutup ketiganya. Contoh kedua menuliskan berkas JSON berdampingan dengan artefak — versi pustaka, struktur pipeline, hyperparameter, dan sidik jari SHA-256 isi berkas — lalu memverifikasi integritasnya sebelum artefak dinyatakan layak muat.
# Manifes artefak: keterangan lengkap yang menyertai berkas model agar
# artefak bisa diaudit tanpa harus memuatnya -- versi pustaka, struktur
# pipeline, hyperparameter, dan sidik jari SHA-256 isi berkas. Hash
# diverifikasi ulang sebelum artefak dipakai; satu bit berubah, integritas
# gagal.
import hashlib
import json
import os
import joblib
import numpy as np
import sklearn
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
# ---------- Pipeline klasifikasi kecil: prediksi churn ----------
rng = np.random.default_rng(42)
n = 400
X = rng.normal(0, 1, size=(n, 5))
logit = 1.5 * X[:, 0] - 1.0 * X[:, 1] + 0.5 * X[:, 2] \
+ rng.normal(0, 0.8, n)
y = (logit > 0).astype(int)
baris_hilang = rng.choice(n, size=15, replace=False)
X[baris_hilang, 3] = np.nan
pipeline = Pipeline(
[
("imputasi", SimpleImputer(strategy="median")),
("skala", StandardScaler()),
("model", LogisticRegression(C=1.0, max_iter=1000)),
]
)
pipeline.fit(X, y)
# ---------- Simpan artefak lalu bangun manifes ----------
BERKAS = "model_churn.joblib"
MANIFES = "model_churn.manifest.json"
joblib.dump(pipeline, BERKAS)
def hash_sha256(berkas):
isi = open(berkas, "rb").read()
return hashlib.sha256(isi).hexdigest()
manifes = {
"nama_artefak": BERKAS,
"tugas": "klasifikasi biner churn",
"n_baris_latih": n,
"pustaka": {
"scikit-learn": sklearn.__version__,
"numpy": np.__version__,
"joblib": joblib.__version__,
},
"langkah_pipeline": list(pipeline.named_steps.keys()),
"hyperparameter_model": {
"C": pipeline.named_steps["model"].C,
"max_iter": pipeline.named_steps["model"].max_iter,
},
"hash_artefak_sha256": hash_sha256(BERKAS),
}
with open(MANIFES, "w", encoding="utf-8") as f:
json.dump(manifes, f, indent=2, sort_keys=True)
print("== Manifes artefak ==")
print(json.dumps(manifes, indent=2, sort_keys=True))
# ---------- Verifikasi integritas sebelum memuat ----------
with open(MANIFES, encoding="utf-8") as f:
manifes_dimuat = json.load(f)
hash_sekarang = hash_sha256(BERKAS)
print("\n== Verifikasi integritas ==")
print(f"hash tersimpan : {manifes_dimuat['hash_artefak_sha256'][:16]}...")
print(f"hash dihitung : {hash_sekarang[:16]}...")
print(f"integritas : "
f"{'UTUH' if hash_sekarang == manifes_dimuat['hash_artefak_sha256'] else 'GAGAL'}")
# ---------- Simulasi satu bit berubah ----------
isi = bytearray(open(BERKAS, "rb").read())
isi[100] ^= 0b00000001
hash_sabotase = hashlib.sha256(bytes(isi)).hexdigest()
print(f"\n== Simulasi satu bit dibalik ==")
print(f"hash berubah : {hash_sabotase != manifes_dimuat['hash_artefak_sha256']}")
print(f"integritas : "
f"{'UTUH' if hash_sabotase == manifes_dimuat['hash_artefak_sha256'] else 'GAGAL'}")
# ---------- Artefak dimuat hanya setelah integritas lolos ----------
muat = joblib.load(BERKAS)
akurasi = muat.score(X, y)
print(f"\n== Artefak dimuat setelah integritas lolos ==")
print(f"akurasi pada data latih: {akurasi:.4f}")
os.remove(BERKAS)
os.remove(MANIFES)
print(f"\n{BERKAS} dan {MANIFES} dihapus (contoh selesai)")Output yang Dihasilkan:
== Manifes artefak ==
{
"hash_artefak_sha256": "81e599640d20e17952d57cc9d1e29ed18eff5fe6789c6d757142473114216137",
"hyperparameter_model": {
"C": 1.0,
"max_iter": 1000
},
"langkah_pipeline": [
"imputasi",
"skala",
"model"
],
"n_baris_latih": 400,
"nama_artefak": "model_churn.joblib",
"pustaka": {
"joblib": "1.5.3",
"numpy": "2.1.3",
"scikit-learn": "1.5.2"
},
"tugas": "klasifikasi biner churn"
}
== Verifikasi integritas ==
hash tersimpan : 81e599640d20e179...
hash dihitung : 81e599640d20e179...
integritas : UTUH
== Simulasi satu bit dibalik ==
hash berubah : True
integritas : GAGAL
== Artefak dimuat setelah integritas lolos ==
akurasi pada data latih: 0.8675
model_churn.joblib dan model_churn.manifest.json dihapus (contoh selesai)Alurnya membentuk prosedur muat yang disiplin:
- Baris 46–48 (fungsi hash): membaca seluruh byte berkas lalu menghitung SHA-256. Karena
joblib.dumpbersifat deterministik untuk objek dan versi pustaka yang sama, hash yang sama menandakan artefak yang sama. - Baris 51–68 (manifes): kamus berisi identitas artefak ditulis sebagai JSON dengan
sort_keys=True— urutan kunci stabil sehingga manifes bisa dibandingkan antar waktu. Hash penuh 64 heksadesimal menjadi sidik jari resmi artefak. - Baris 73–82 (verifikasi): hash dihitung ulang saat artefak hendak dipakai. Pada contoh ini hasilnya
UTUHkarena berkas tidak tersentuh. - Baris 84–92 (simulasi kerusakan): satu bit di posisi byte ke-100 dibalik lewat operasi XOR
0b00000001— perubahan terkecil yang mungkin. Hash langsung berubah dan integritas jatuh keGAGAL: deteksi kerusakan transfer, tulis-ulang tidak sengaja, hingga penggantian berkas. - Baris 94–97 (muat bersyarat):
joblib.load()baru dipanggil setelah integritas lolos; akurasi 0,8675 pada data latih membuktikan objek berfungsi penuh.
Dua Jebakan yang Sering Menggigit #
Jebakan pertama menyangkut kelas kustom. pickle menyimpan referensi ke kelas, bukan definisinya — PangkasKuantil dari chapter sebelumnya tersimpan sebagai nama kelas plus atributnya. Saat artefak dimuat di lingkungan yang tidak bisa mengimpor kelas tersebut (modul hilang, nama berubah), joblib.load() gagal dengan ModuleNotFoundError atau AttributeError. Konsekuensinya: modul tempat transformer kustom didefinisikan harus ikut didistribusikan bersama artefak, dengan nama dan lokasi yang sama.
Jebakan kedua menyangkut versi pustaka. Struktur internal objek scikit-learn berubah antar versi; artefak yang dibuat dengan scikit-learn 1.5.2 bisa saja gagal dimuat di versi yang jauh berbeda, atau lebih buruk — termuat tanpa galat tetapi memberi hasil berbeda. Karena itu versi pustaka milik manifes bukan informasi pelengkap, melainkan prasyarat muat: lingkungan inferensi wajib menyamainya, biasanya lewat berkas requirements.txt yang di-pin persis.
Prosedur yang dibangun chapter ini menjadikan setiap pelatihan menghasilkan artefak bernilai: berkas model yang utuh dan terverifikasi. Namun satu pertanyaan produksi masih terbuka — dari sepuluh pelatihan dengan kombinasi hyperparameter berbeda, manakah yang layak menjadi artefak final? Menjawabnya menuntut kita mencatat setiap percobaan secara sistematis, dan itulah pekerjaan Chapter Manajemen Eksperimen.
Referensi Resmi #
- Persistence — joblib documentation
— panduan resmi
dump/load, kompresi, dan batasan format. - Model persistence — scikit-learn User Guide — rekomendasi resmi, peringatan keamanan pickle, dan pola pin versi.
- sklearn.pipeline.Pipeline — API
— atribut
named_stepsyang dibaca ulang dari artefak.