Versioning memberi setiap model identitas permanen, tetapi belum menjawab pertanyaan organisasi: di antara belas versi yang tersimpan, yang mana yang sedang melayani pengguna? Jawaban yang bergantung pada ingatan orang — “yang terakhir kali kita deploy itu lho” — adalah sumber insiden klasik. Model registry menutup celah tersebut dengan menjadi satu-satunya tempat yang mencatat status setiap versi: mana yang masih kandidat, mana yang melayani produksi, dan mana yang sudah pensiun. Chapter ini membangun registry mini lengkap dengan gerbang kualitas dan siklus stage yang benar-benar dieksekusi.

Peran Registry #

Registry adalah single source of truth untuk pertanyaan “model apa yang berjalan sekarang”. Tiga fungsi intinya:

  • Status terpusat: setiap versi berada pada satu stage yang jelas; tidak ada jawaban produksi yang bersumber dari ingatan orang.
  • Gerbang promosi: versi tidak bisa naik stage tanpa melewati pemeriksaan — metrik minimum, hasil evaluasi, persetujuan.
  • Lineage: setiap entri menautkan versi ke run eksperimen, dataset, dan kode yang menghasilkannya, sehingga pertanyaan “dari mana model ini berasal” selalu bisa dijawab.

Siklus Hidup Stage #

Praktik umum (mengikuti terminologi MLflow Model Registry ) memakai empat stage:

StageArtiDipindahkan oleh
none / belum terdaftarhasil eksperimen, belum kandidat rilispencatat run
stagingkandidat yang sedang divalidasi di lingkungan mirip produksipipeline evaluasi atau data scientist
productionmelayani traffic nyatakeputusan promosi dengan gerbang
archivedpensiun, tidak melayani, tetap bisa diauditpromosi versi pengganti

Siklusnya membentuk alur satu arah dengan satu cabangan penolakan:

Aturan penting yang jarang ditulis eksplisit: satu registry memelihara paling banyak satu versi di production untuk satu nama model. Ketika versi baru naik, versi lama otomatis turun ke archived — mekanisme inilah yang mencegah dua “model produksi” hidup bersamaan tanpa disadari.

Contoh Teruji: Promosi dengan Gerbang Akurasi #

Berkas registry_mini.py melatih tiga kandidat, mendaftarkannya ke registry.json, lalu mempromosikan satu per satu melewati gerbang akurasi 0.85. Kandidat ketiga sengaja dilatih pada data yang bergeser distribusinya — gerbang harus menahannya:

python
"""Model registry mini: stage staging -> production -> archived.

Tiga kandidat model dilatih pada data klasifikasi. Setiap kandidat
didaftarkan ke registry (berkas registry.json), lalu dipromosikan
melewati gerbang validasi akurasi. Model yang naik ke production
otomatis mengarsipkan pendahulunya; kandidat yang gagal gerbang
tetap tinggal di daftar tanpa naik stage.
"""

import json
import shutil

import numpy as np

STORE = "registry_store"
AMBANG_AKURASI = 0.85  # gerbang kualitas: akurasi validasi minimum


def buat_data(seed, n=120, geser=0.0):
    """Dua gumpalan Gaussian dua kelas, diacak secara deterministik.

    Parameter geser memindahkan kedua gumpalan sejauh nilai tersebut —
    simulasi data latih yang berasal dari distribusi berbeda.
    """
    rng = np.random.default_rng(seed)
    kelas0 = rng.normal(loc=[-1.0 + geser, 0.0], scale=0.7, size=(n, 2))
    kelas1 = rng.normal(loc=[1.0 + geser, 0.4], scale=0.7, size=(n, 2))
    X = np.vstack([kelas0, kelas1])
    y = np.array([0] * n + [1] * n)
    urut = rng.permutation(len(y))
    X, y = X[urut], y[urut]
    batas = min(160, len(y))
    return X[:batas], y[:batas], X[160:], y[160:]


def latih(X, y, epoch=300, lr=1.0):
    n = len(y)
    w = np.zeros(3)  # [bias, w1, w2]
    for _ in range(epoch):
        skor = np.clip(X @ w[1:] + w[0], -30, 30)
        p = 1.0 / (1.0 + np.exp(-skor))
        galat = p - y
        w[1:] -= lr * (X.T @ galat) / n
        w[0] -= lr * galat.mean()
    return w


def akurasi(w, X, y):
    skor = X @ w[1:] + w[0]
    return float((((skor >= 0).astype(int)) == y).mean())


def baca_registry():
    with open(f"{STORE}/registry.json", encoding="utf-8") as f:
        return json.load(f)


def tulis_registry(reg):
    with open(f"{STORE}/registry.json", "w", encoding="utf-8") as f:
        json.dump(reg, f, indent=2)


def daftarkan(reg, nama, versi, metrik):
    reg.append({"nama": nama, "versi": versi, "stage": "tidak_lolos",
                "akurasi_valid": round(metrik, 3)})
    tulis_registry(reg)
    print(f"  daftar   {nama} v{versi}: akurasi_valid={metrik:.3f}")


def promosikan(reg, nama, versi, target):
    entri = next(e for e in reg if e["nama"] == nama and e["versi"] == versi)
    if entri["akurasi_valid"] < AMBANG_AKURASI:
        print(f"  GATE GAGAL {nama} v{versi} -> {target}: akurasi "
              f"{entri['akurasi_valid']:.3f} < {AMBANG_AKURASI:.2f}")
        return
    if target == "production":
        for lain in reg:  # pendahulu di production otomatis diarsipkan
            if lain["stage"] == "production" and lain is not entri:
                lain["stage"] = "archived"
                print(f"  {lain['nama']} v{lain['versi']} -> archived "
                      f"(digantikan di production)")
    entri["stage"] = target
    tulis_registry(reg)
    print(f"  promosi  {nama} v{versi} -> {target}")


def tampilkan(reg, judul):
    print(f"\n{judul}")
    print(f"  {'model':<12} {'versi':>5} {'akurasi':>8}  stage")
    for e in reg:
        print(f"  {e['nama']:<12} {e['versi']:>5} "
              f"{e['akurasi_valid']:>8.3f}  {e['stage']}")


def main():
    shutil.rmtree(STORE, ignore_errors=True)
    import os

    os.makedirs(STORE)
    # data validasi tetap — semua kandidat diukur dengan tolok ukur sama
    _, _, X_valid, y_valid = buat_data(seed=42)

    print("== melatih tiga kandidat model ==")
    X1, y1, _, _ = buat_data(seed=42)                    # data sehat
    X2, y2, _, _ = buat_data(seed=43, n=240)             # data 2x lebih besar
    X3, y3, _, _ = buat_data(seed=44, geser=1.5)         # data bergeser
    ak1, ak2, ak3 = (akurasi(latih(X1, y1), X_valid, y_valid),
                     akurasi(latih(X2, y2, epoch=600), X_valid, y_valid),
                     akurasi(latih(X3, y3), X_valid, y_valid))
    print(f"  kandidat A (data sehat)          : {ak1:.3f}")
    print(f"  kandidat B (data 2x + epoch 2x)  : {ak2:.3f}")
    print(f"  kandidat C (data latih bergeser) : {ak3:.3f}")

    reg = []
    print("\n== operasi registry ==")
    daftarkan(reg, "klasifikasi", "1", ak1)
    promosikan(reg, "klasifikasi", "1", "staging")
    promosikan(reg, "klasifikasi", "1", "production")
    daftarkan(reg, "klasifikasi", "2", ak2)
    promosikan(reg, "klasifikasi", "2", "staging")
    promosikan(reg, "klasifikasi", "2", "production")
    daftarkan(reg, "klasifikasi", "3", ak3)
    promosikan(reg, "klasifikasi", "3", "staging")

    reg = baca_registry()
    tampilkan(reg, "== status akhir registry (dibaca ulang dari disk) ==")
    produksi = next(e for e in reg if e["stage"] == "production")
    print(f"\nmodel produksi saat ini: {produksi['nama']} v{produksi['versi']}"
          f" (akurasi {produksi['akurasi_valid']:.3f})")


if __name__ == "__main__":
    try:
        main()
    finally:
        shutil.rmtree(STORE, ignore_errors=True)

Output yang Dihasilkan:

text
== melatih tiga kandidat model ==
  kandidat A (data sehat)          : 0.925
  kandidat B (data 2x + epoch 2x)  : 0.938
  kandidat C (data latih bergeser) : 0.738

== operasi registry ==
  daftar   klasifikasi v1: akurasi_valid=0.925
  promosi  klasifikasi v1 -> staging
  promosi  klasifikasi v1 -> production
  daftar   klasifikasi v2: akurasi_valid=0.938
  promosi  klasifikasi v2 -> staging
  klasifikasi v1 -> archived (digantikan di production)
  promosi  klasifikasi v2 -> production
  daftar   klasifikasi v3: akurasi_valid=0.738
  GATE GAGAL klasifikasi v3 -> staging: akurasi 0.738 < 0.85

== status akhir registry (dibaca ulang dari disk) ==
  model        versi  akurasi  stage
  klasifikasi      1    0.925  archived
  klasifikasi      2    0.938  production
  klasifikasi      3    0.738  tidak_lolos

model produksi saat ini: klasifikasi v2 (akurasi 0.938)

Mari kita telaah kode di atas baris per baris:

  1. Baris 19-33 (buat_data): parameter geser memindahkan kedua gumpalan kelas sejauh nilai tersebut. Kandidat C dilatih pada data bergeser 1.5 — simulasi dataset dari sumber berbeda yang sekilas tampak wajar.
  2. Baris 63-67 (daftarkan): pendaftaran hanya menulis fakta (nama, versi, metrik) tanpa penilaian. Stage awalnya tidak_lolos — registry tidak menganggap siapa pun layak sebelum dibuktikan.
  3. Baris 70-84 (promosikan): inti registry. Pemeriksaan akurasi_valid < AMBANG menghentikan promosi dengan pesan yang menyebutkan angka, ambang, dan versi. Untuk target production, seluruh entri lain yang sedang berada di production dipindahkan ke archived lebih dulu.
  4. Baris 87-92 (tampilkan): status registry dibaca ulang dari disk dan dicetak sebagai tabel — satu tabel inilah jawaban resmi atas “model apa yang berjalan sekarang”.
  5. Baris 116-129 (urutan operasi): v1 naik dua tahap menjadi production pertama; v2 naik dan otomatis mengarsipkan v1; v3 ditahan gerbang. Tiga pola dasar siklus — promosi, penggantian, penolakan — semuanya terjadi dalam belasan baris.

Dua hal pada keluaran layak digarisbawahi. Pertama, baris klasifikasi v1 -> archived (digantikan di production) terjadi sebelum promosi klasifikasi v2 -> production — pengarsipan pendahulu adalah bagian dari transaksi promosi, bukan tindakan terpisah yang bisa terlupakan. Kedua, kandidat C tetap berada di registry dengan metriknya — gagal gerbang bukan berarti dihapus; jejak kegagalan adalah bahan audit juga.

Dari Registry File ke Alat Ekosistem #

Registry mini dengan registry.json cukup untuk satu tim kecil. MLflow Model Registry menambahkan di atasnya: antarmuka web untuk melihat dan memindahkan stage, API transition_model_version_stage yang bisa dipanggil pipeline otomatis, anotasi dan deskripsi per versi, serta notifikasi ketika stage berubah. Stage dan aturan gerbangnya tetap milik kita — alat hanya menyediakan tempat dan mekanismenya.

Registry menjawab “versi mana yang boleh melayani”. Pertanyaan berikutnya: siapa yang melatih kandidat-kandidat itu secara teratur, dan siapa yang memanggil fungsi promosikan tanpa menunggu manusia? Chapter berikutnya, Pipeline Training Otomatis , merangkai seluruh proses menjadi alur yang berjalan sendiri.