Sebuah model terlatih pada dasarnya adalah berkas biner: sekumpulan bobot yang tidak bisa dibaca dengan mata. Tanpa sistem versi, berkas model.npz hari ini dan berkas model.npz minggu lalu adalah dua benda berbeda yang tidak bisa dibedakan — sampai salah satunya dipakai dan hasilnya salah. Versioning memberi setiap artifact identitas permanen: nomor versi yang tidak dipakai ulang, metadata yang menjelaskan asal-usulnya, dan janji bahwa isi versi 1.1.0 tidak akan pernah berubah setelah ditetapkan. Chapter ini menerapkan pola tersebut pada tiga versi model regresi yang benar-benar disimpan dan dimuat kembali.
Mengapa Model Perlu Versi #
Empat kebutuhan operasional menuntut penomoran versi model. Pertama, A/B testing: dua versi harus bisa dipanggil dengan nama yang jelas (“v1.1.0” lawan “v2.0.0”) agar hasil perbandingan traffic tidak ambigu. Kedua, audit: ketika regulator atau tim kepatuhan bertanya “model versi mana yang membuat keputusan pada tanggal tertentu”, jawabannya harus berupa nomor versi yang bisa ditelusuri. Ketiga, rollback: membatalkan rilis buruk berarti kembali ke versi sebelumnya — mustahil bila versi sebelumnya sudah tertimpa. Keempat, reproduksi: versi menautkan model ke data dan kode yang menghasilkannya.
Skema Penomoran #
Praktik umum meminjam semantic versioning dari dunia perangkat lunak, dengan makna yang disesuaikan untuk model:
| Komponen | Kapan dinaikkan | Contoh |
|---|---|---|
MAJOR | perilaku berubah: arsitektur baru, dataset baru, hasil prediksi berbeda signifikan | data latih diganti: 1.1.0 -> 2.0.0 |
MINOR | perbaikan kompatibel: lebih banyak epoch, data tambahan, tuning hiperparameter | epoch dinaikkan: 1.0.0 -> 1.1.0 |
PATCH | perbaikan non-model: preprocessing, metadata, dokumentasi | koreksi skala fitur: 1.1.0 -> 1.1.1 |
Prinsip kedua adalah immutability: begitu versi 1.1.0 ditetapkan, artifact-nya dikunci. Perbaikan apa pun menjadi versi baru, bukan menimpa versi lama. Pola penyimpanan model produksi lengkap (state_dict, konfigurasi arsitektur, parameter preprocessing) sudah kita bangun di chapter Menyimpan Model untuk Produksi ; di sini kita menambahkan lapisan penomoran di atasnya.
Contoh Teruji: Tiga Versi dan Sebuah Registry File #
Berkas versi_model.py melatih regresi linear NumPy tiga kali dengan perbaikan bertahap: rilis perdana, patch yang menambah epoch, dan rilis major dengan dataset baru. Setiap versi disimpan sebagai .npz plus entri di index.json, lalu versi 1.1.0 dimuat kembali untuk memprediksi:
"""Model versioning mini: tiga versi model + berkas index.json.
Regresi linear NumPy dilatih tiga kali dengan perbaikan bertahap.
Setiap versi disimpan sebagai artifact (berkas .npz) yang tidak dapat
diubah, plus metadata di index.json: versi semver, versi dataset,
versi kode, metrik, dan hash bobot. Riwayat dibaca ulang dari disk,
versi tertentu dimuat kembali, lalu dipakai untuk memprediksi.
"""
import hashlib
import json
import os
import shutil
import numpy as np
STORE = "model_store"
def buat_data(n, seed):
"""Regresi y = 3x + 4 + derau, deterministik lewat seed."""
rng = np.random.default_rng(seed)
X = rng.uniform(0, 4, size=(n, 1))
y = 3.0 * X[:, 0] + 4.0 + rng.normal(0, 0.8, size=n)
return X, y
def latih(X, y, epoch, lr=0.05):
"""Gradient descent untuk regresi linear (bias di w[0])."""
n = len(y)
w = np.zeros(2)
for _ in range(epoch):
pred = X[:, 0] * w[1] + w[0]
galat = pred - y
w[0] -= lr * galat.mean()
w[1] -= lr * (X[:, 0] @ galat) / n
return w
def rmse(w, X, y):
pred = X[:, 0] * w[1] + w[0]
return float(np.sqrt(np.mean((pred - y) ** 2)))
def hash_bobot(w):
return hashlib.sha256(w.tobytes()).hexdigest()[:12]
def simpan_versi(versi, w, metrik, vdata, vkode, versi_data_ke):
"""Simpan artifact + metadata; versi yang sama ditolak (immutable)."""
index_path = f"{STORE}/index.json"
index = {"model": "regresi_harga", "versi": []}
if os.path.exists(index_path):
with open(index_path, encoding="utf-8") as f:
index = json.load(f)
if any(v["versi"] == versi for v in index["versi"]):
print(f" TOLAK: versi {versi} sudah ada -> artifact tidak "
f"ditimpa (immutability)")
return None
np.savez(f"{STORE}/model_{versi}.npz", bobot=w)
entri = {
"versi": versi,
"versi_data": vdata,
"versi_kode": vkode,
"rmse_valid": round(metrik, 4),
"hash_bobot": hash_bobot(w),
"dilatih_dengan": versi_data_ke,
}
index["versi"].append(entri)
with open(index_path, "w", encoding="utf-8") as f:
json.dump(index, f, indent=2)
print(f" versi {versi:<7} tersimpan rmse={metrik:.4f} "
f"hash={entri['hash_bobot'][:8]} data={vdata}")
return entri
def muat_versi(versi):
"""Muat kembali bobot versi tertentu dari artifact di disk."""
index_path = f"{STORE}/index.json"
with open(index_path, encoding="utf-8") as f:
index = json.load(f)
entri = next(v for v in index["versi"] if v["versi"] == versi)
with np.load(f"{STORE}/model_{versi}.npz") as paket:
w = paket["bobot"]
cocok = hash_bobot(w) == entri["hash_bobot"]
print(f" muat {versi}: bobot={w.round(3).tolist()} "
f"hash cocok dengan index.json: {cocok}")
return w
def main():
shutil.rmtree(STORE, ignore_errors=True)
os.makedirs(STORE)
X_valid, y_valid = buat_data(80, seed=99) # data uji tetap utk semua versi
print("== melatih dan menyimpan 3 versi ==")
# v1.0.0 : data 60 sampel, 50 epoch (rilis perdana)
X, y = buat_data(60, seed=7)
w1 = latih(X, y, epoch=50)
simpan_versi("1.0.0", w1, rmse(w1, X_valid, y_valid), "ds-001", "9f2ab41",
{"n": 60, "epoch": 50})
# v1.1.0 : patch — epoch dinaikkan, data sama
w2 = latih(X, y, epoch=150)
simpan_versi("1.1.0", w2, rmse(w2, X_valid, y_valid), "ds-001", "9f2ab41",
{"n": 60, "epoch": 150})
# v2.0.0 : major — dataset baru 4x lebih besar
X_besar, y_besar = buat_data(240, seed=21)
w3 = latih(X_besar, y_besar, epoch=400)
simpan_versi("2.0.0", w3, rmse(w3, X_valid, y_valid), "ds-002", "b73c90e",
{"n": 240, "epoch": 400})
print("\n== percobaan menimpa versi 1.1.0 (harus ditolak) ==")
simpan_versi("1.1.0", np.array([4.0, 3.0]), rmse(w2, X_valid, y_valid),
"ds-001", "9f2ab41", {"n": 60, "epoch": 150})
print("\n== riwayat versi (dibaca ulang dari index.json) ==")
with open(f"{STORE}/index.json", encoding="utf-8") as f:
index = json.load(f)
print(f"{'versi':<7} {'rmse_valid':>10} {'versi_data':>10} "
f"{'versi_kode':>10} hash_bobot")
for v in index["versi"]:
print(f"{v['versi']:<7} {v['rmse_valid']:>10.4f} "
f"{v['versi_data']:>10} {v['versi_kode']:>10} "
f"{v['hash_bobot'][:8]}")
print("\n== muat versi 1.1.0 dan prediksi dua sampel ==")
w_v11 = muat_versi("1.1.0")
for x in (1.0, 3.5):
print(f" prediksi x={x:.1f} -> y={w_v11[0] + w_v11[1] * x:.3f}")
if __name__ == "__main__":
try:
main()
finally:
shutil.rmtree(STORE, ignore_errors=True)Output yang Dihasilkan:
== melatih dan menyimpan 3 versi ==
versi 1.0.0 tersimpan rmse=0.9659 hash=f948b1a0 data=ds-001
versi 1.1.0 tersimpan rmse=0.8010 hash=07805872 data=ds-001
versi 2.0.0 tersimpan rmse=0.7864 hash=6bf8d886 data=ds-002
== percobaan menimpa versi 1.1.0 (harus ditolak) ==
TOLAK: versi 1.1.0 sudah ada -> artifact tidak ditimpa (immutability)
== riwayat versi (dibaca ulang dari index.json) ==
versi rmse_valid versi_data versi_kode hash_bobot
1.0.0 0.9659 ds-001 9f2ab41 f948b1a0
1.1.0 0.8010 ds-001 9f2ab41 07805872
2.0.0 0.7864 ds-002 b73c90e 6bf8d886
== muat versi 1.1.0 dan prediksi dua sampel ==
muat 1.1.0: bobot=[3.351, 3.245] hash cocok dengan index.json: True
prediksi x=1.0 -> y=6.596
prediksi x=3.5 -> y=14.708Mari kita telaah kode di atas baris per baris:
- Baris 49-75 (
simpan_versi): fungsi ini adalah registry file mini. Berkasindex.jsondibaca (bila sudah ada), versi baru diperiksa terhadap daftar, lalu artifact bobot disimpan dengannp.savezdan entri metadata ditambahkan. Hashsha256atas bytes bobot ikut dicatat — sidik jari untuk memverifikasi artifact saat dimuat ulang. - Baris 56-59 (penolakan versi ganda): percobaan menyimpan versi yang sudah ada ditolak dengan pesan eksplisit. Empat baris inilah immutability dalam praktik: sistem menolak menimpa, sehingga riwayat tidak bisa rusak karena kelalaian.
- Baris 78-89 (
muat_versi): pemuatan ulang tidak percaya begitu saja — bobot yang dibaca dari disk di-hash kembali dan dibandingkan dengan hash diindex.json. Barishash cocok ... : Truemembuktikan artifact tidak berubah sejak disimpan. - Baris 98-114 (tiga versi): ketiga pelatihan hanya berbeda pada jumlah epoch dan dataset — persis kenaikan PATCH dan MAJOR pada tabel skema. RMSE validasi membaik dari 0.9659 ke 0.8010 ke 0.7864, dan setiap angka tertambat pada versi yang bisa dimuat ulang kapan pun.
- Baris 120-128 (riwayat): tabel riwayat dibaca ulang dari
index.json, bukan dari memori — registry berdiri sendiri sebagai sumber kebenaran.
Perhatikan satu hal yang jarang disadari: RMSE 1.1.0 (0.8010) dan 2.0.0 (0.7864) hampir sama, tetapi keduanya bukan model yang sama. Bobot v1.1.0 [3.351, 3.245] masih jauh dari parameter sejati [4.0, 3.0], sementara v2.0.0 [4.061, 2.999] hampir tepat. Satu angka metrik tidak cukup untuk membandingkan versi — metadata dan bobot ikut menentukan keputusan.
Registry file index.json sudah menjawab “versi apa saja yang ada dan berasal dari mana”. Pertanyaan berikutnya adalah proses: siapa yang boleh memindahkan sebuah versi dari status eksperimen menjadi model produksi, dan lewat pemeriksaan apa? Chapter berikutnya, Model Registry
, membahas siklus stage tersebut.