Tim kecil biasanya memulai dengan catatan manual: angka akurasi dicatat di pesan obrolan, file notebook diberi nama final_v2_beneran, dan konfigurasi pelatihan tinggal di kepala pemiliknya. Pola ini bertahan sampai seseorang bertanya “model yang kemarin akurasinya 94 persen itu pakai learning rate berapa?” — dan tidak ada yang bisa menjawab. Experiment management menyelesaikan masalah tersebut dengan menjadikan pencatatan bagian dari proses training itu sendiri: setiap run merekam dirinya sendiri ke satu log terstruktur yang bisa dibaca ulang dan di-query. Chapter ini membangun sistem tracker mini yang benar-benar berjalan, lalu memetakan alat ekosistem yang mengotomatiskan pola yang sama.

Dari Catatan Manual ke Sistem #

Evolusi pencatatan eksperimen biasanya berjalan tiga tahap. Tahap pertama, catatan bebas: teks tersebar, tidak ada format, mustahil dibandingkan antar run. Tahap kedua, spreadsheet: kolom mulai rapi, tetapi sel bisa diubah tanpa jejak dan satu baris bisa tertimpa baris lain. Tahap ketiga, sistem tracking: kode training yang menulis catatan, bukan manusia — sehingga catatan tidak bisa lupa mencatat, dan formatnya seragam untuk semua run dari semua orang.

Kunci tahap ketiga ada pada kelengkapan field. Tabel berikut merangkum enam field yang wajib ada pada setiap run:

FieldIsiKonsekuensi bila hilang
configseluruh hiperparameter + seed + epochrun tidak bisa diulang
versi_kodehash commit git saat run dijalankantidak tahu kode mana yang menghasilkan angka
versi_datahash atau versi datasetdua run dengan data berbeda terlihat sebanding
metricsmetrik validasi final (bukan sekadar terbaik)keputusan didasarkan ingatan, bukan angka
artifactberkas bobot/model yang dihasilkanangka tercatat tetapi modelnya tidak bisa dimuat
identitas runid unik + nama yang merangkum configtabel hasil tidak terbaca tanpa membuka isi log

Pola manual dengan dict dan JSON pernah kita bangun di chapter Experiment Tracking pada section Evaluasi dan Debugging. Sekarang kita naikkan satu tingkat: log yang bisa ditambah (append) tanpa menimpa run lama, dan fungsi query yang membaca log dari disk.

Contoh Teruji: Tracker Mini dengan JSON Lines #

Berkas tracker_eksperimen.py melatih tiga run dengan learning rate berbeda pada data klasifikasi yang sama. Setiap run ditulis sebagai satu baris JSON ke runs.jsonl — format JSON Lines memungkinkan penambahan run baru tanpa membaca seluruh log terlebih dahulu. Setelah itu log dibaca ulang dari disk dan di-query:

python
"""Experiment tracker mini: setiap run menulis satu baris JSON Lines.

Tiga konfigurasi learning rate dilatih pada data klasifikasi yang
sama. Setiap run mencatat field wajib — config, versi kode, versi
data, metrics, artifact — ke berkas runs.jsonl. Log dibaca ulang
dari disk, lalu di-query untuk membandingkan ketiga run.
"""

import hashlib
import json
import os
import shutil

import numpy as np

STORE = "tracker_store"


def buat_dataset(seed, n_per_kelas):
    """Dua gumpalan Gaussian dua kelas, lalu diacak deterministik."""
    rng = np.random.default_rng(seed)
    kelas0 = rng.normal(loc=[-1.0, 0.0], scale=0.7, size=(n_per_kelas, 2))
    kelas1 = rng.normal(loc=[1.0, 0.4], scale=0.7, size=(n_per_kelas, 2))
    X = np.vstack([kelas0, kelas1])
    y = np.array([0] * n_per_kelas + [1] * n_per_kelas)
    urut = rng.permutation(len(y))
    return X[urut], y[urut]


def versi_data(X, y):
    """Sidik jari dataset: sha256 dari bytes fitur dan label."""
    return hashlib.sha256(X.tobytes() + y.tobytes()).hexdigest()[:8]


def latih_logreg(X, y, epoch, lr=1.0):
    """Logistic regression NumPy murni dengan gradient descent."""
    n = len(y)
    w = np.zeros(X.shape[1] + 1)  # indeks 0 = bias
    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 evaluasi(w, X, y):
    skor = np.clip(X @ w[1:] + w[0], -30, 30)
    p = 1.0 / (1.0 + np.exp(-skor))
    prediksi = (p >= 0.5).astype(int)
    akurasi = float((prediksi == y).mean())
    eps = 1e-12
    loss = float(-np.mean(y * np.log(p + eps) + (1 - y) * np.log(1 - p + eps)))
    return akurasi, loss


def catat_run(cfg, versi_kode, versi_data_run, akurasi, loss, nama_artifact):
    """Satu run = satu objek = satu baris JSON Lines."""
    catatan = {
        "run_id": f"run_{cfg['id']:04d}",
        "config": cfg,
        "versi_kode": versi_kode,
        "versi_data": versi_data_run,
        "metrics": {"akurasi_valid": round(akurasi, 3),
                    "loss_valid": round(loss, 4)},
        "artifact": nama_artifact,
    }
    with open(f"{STORE}/runs.jsonl", "a", encoding="utf-8") as f:
        f.write(json.dumps(catatan, sort_keys=True) + "\n")
    return catatan


def baca_log():
    """Baca ulang seluruh run dari disk — log berdiri sendiri."""
    with open(f"{STORE}/runs.jsonl", encoding="utf-8") as f:
        return [json.loads(baris) for baris in f if baris.strip()]


def main():
    shutil.rmtree(STORE, ignore_errors=True)
    os.makedirs(STORE)
    X_latih, y_latih = buat_dataset(seed=42, n_per_kelas=120)  # data sama utk semua run
    X_valid, y_valid = buat_dataset(seed=99, n_per_kelas=100)  # tolok ukur tetap
    vdata = versi_data(X_latih, y_latih)
    VKODE = "9f2ab41"  # tiruan commit git; produksi: git rev-parse HEAD

    konfigurasi = [
        {"id": 1, "lr": 0.02, "epoch": 150, "seed": 42},
        {"id": 2, "lr": 0.3, "epoch": 150, "seed": 42},
        {"id": 3, "lr": 2.0, "epoch": 150, "seed": 42},
    ]

    print("== menulis 3 run ke tracker_store/runs.jsonl ==")
    for cfg in konfigurasi:
        w = latih_logreg(X_latih, y_latih, cfg["epoch"], lr=cfg["lr"])
        akurasi, loss = evaluasi(w, X_valid, y_valid)
        np.save(f"{STORE}/{cfg['id']:04d}_bobot.npy", w)
        catat_run(cfg, VKODE, vdata, akurasi, loss,
                  f"{cfg['id']:04d}_bobot.npy")
        print(f"run_{cfg['id']:04d}  lr={cfg['lr']:<5} "
              f"akurasi_valid={akurasi:.3f}  loss_valid={loss:.4f}")

    print("\n== satu baris log apa adanya (format JSON Lines) ==")
    with open(f"{STORE}/runs.jsonl", encoding="utf-8") as f:
        print(f.readline().rstrip())

    print("\n== query: bandingkan 3 run, urut loss validasi menaik ==")
    log = baca_log()
    print(f"jumlah run di log : {len(log)}")
    print(f"versi kode        : {log[0]['versi_kode']}")
    print(f"versi data        : {log[0]['versi_data']}")
    print(f"{'run_id':<8} {'lr':>5} {'epoch':>5} {'akurasi':>8} "
          f"{'loss':>8}  artifact")
    for r in sorted(log, key=lambda r: r["metrics"]["loss_valid"]):
        c, m = r["config"], r["metrics"]
        print(f"{r['run_id']:<8} {c['lr']:>5} {c['epoch']:>5} "
              f"{m['akurasi_valid']:>8.3f} {m['loss_valid']:>8.4f}  "
              f"{r['artifact']}")
    terbaik = min(log, key=lambda r: r["metrics"]["loss_valid"])
    print(f"run terbaik: {terbaik['run_id']} "
          f"(loss_valid={terbaik['metrics']['loss_valid']:.4f})")

    lolos = [r for r in log if r["metrics"]["akurasi_valid"] >= 0.92]
    print(f"\nquery akurasi >= 0.92 : {len(lolos)} run lolos "
          f"({', '.join(r['run_id'] for r in lolos)})")


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

Output yang Dihasilkan:

text
== menulis 3 run ke tracker_store/runs.jsonl ==
run_0001  lr=0.02  akurasi_valid=0.915  loss_valid=0.3663
run_0002  lr=0.3   akurasi_valid=0.925  loss_valid=0.2221
run_0003  lr=2.0   akurasi_valid=0.925  loss_valid=0.2285

== satu baris log apa adanya (format JSON Lines) ==
{"artifact": "0001_bobot.npy", "config": {"epoch": 150, "id": 1, "lr": 0.02, "seed": 42}, "metrics": {"akurasi_valid": 0.915, "loss_valid": 0.3663}, "run_id": "run_0001", "versi_data": "847db855", "versi_kode": "9f2ab41"}

== query: bandingkan 3 run, urut loss validasi menaik ==
jumlah run di log : 3
versi kode        : 9f2ab41
versi data        : 847db855
run_id      lr epoch  akurasi     loss  artifact
run_0002   0.3   150    0.925   0.2221  0002_bobot.npy
run_0003   2.0   150    0.925   0.2285  0003_bobot.npy
run_0001  0.02   150    0.915   0.3663  0001_bobot.npy
run terbaik: run_0002 (loss_valid=0.2221)

query akurasi >= 0.92 : 2 run lolos (run_0002, run_0003)

Mari kita telaah kode di atas baris per baris:

  1. Baris 19-27 (buat_dataset): data dibuat dengan default_rng(seed) lalu diacak dengan permutation — acak tetapi deterministik, sehingga hash dataset sama di setiap eksekusi.
  2. Baris 30-32 (versi_data): sidik jari dataset dihitung dari sha256 atas bytes fitur dan label. Field inilah yang membuat dua run pada data berbeda tidak pernah tersamar sebagai sebanding.
  3. Baris 58-71 (catat_run): satu run dibungkus menjadi satu objek dengan keenam field wajib, lalu ditulis sebagai satu baris dengan mode "a" (append). Run baru tidak pernah menimpa run lama.
  4. Baris 74-77 (baca_log): log dibaca ulang dari disk baris demi baris. Query apa pun — filter, sort, agregasi — berjalan di atas log yang sudah tertutup, bukan variabel di memori.
  5. Baris 94-102 (loop training): satu-satunya hal yang berbeda antar run adalah lr di config — disiplin ubah-satu-variabel yang kita latih di chapter Experiment Tracking .
  6. Baris 108-126 (query): log diurutkan berdasarkan loss_valid menaik, lalu difilter dengan ambang akurasi. Hasilnya berbicara sendiri: lr=0.02 konvergen terlalu lambat (loss 0.3663), lr=0.3 paling baik, dan lr=2.0 sedikit meleset.

Alur lengkap tracker membentuk siklus: training menulis log, query membaca log, hasil query menentukan config run berikutnya.

Alat Ekosistem #

Tracker dua belas fungsi di atas membuktikan mekanismenya; untuk kerja tim yang serius, ekosistem menyediakan alat yang lebih lengkap. MLflow Tracking menyimpan parameter, metrik per step, dan artifact per run pada server bersama, lengkap dengan antarmuka perbandingan run dan API query. Weights & Biases menambahkan visualisasi live saat training berjalan dan kolaborasi tim. Keduanya memakai pola yang sama dengan tracker mini kita: kode yang memanggil log_param/log_metric selama training, dan log yang bertahan setelah training selesai.

Log run yang tertib menjawab “eksperimen apa saja yang sudah dicoba dan hasilnya berapa”. Pertanyaan berikutnya muncul begitu satu run terpilih menjadi kandidat produksi: model hasil run itu menjadi artifact yang mana, dan bagaimana riwayatnya dikelola? Chapter berikutnya, Model Versioning , membahas penomoran versi untuk artifact tersebut.