Satu eksperimen deep learning melibatkan belasan keputusan: learning rate, ukuran hidden layer, dropout, jumlah epoch, seed, cara membagi data. Tanpa pencatatan, dua minggu kemudian kita hanya ingat bahwa “kemarin akurasinya lebih tinggi” — tanpa tahu konfigurasi mana yang menghasilkan angka itu. Experiment tracking menyelesaikan masalah tersebut dengan satu aturan sederhana: setiap run mencatat dirinya sendiri, lengkap dengan hiperparameter, hasil, dan versi kode. Chapter ini membangun polanya secara manual dengan dict dan JSON teruji nyata, lalu memetakan alat ekosistem yang mengotomatiskan pola yang sama.

Anatomi Satu Run #

Setiap eksperimen yang layak dibandingkan memiliki tiga komponen catatan. Pertama, config: seluruh nilai yang kita pilih sebelum training dimulai, termasuk seed — tanpa seed, hasil yang sama tidak bisa diulang dan perbandingan antar run menjadi bias. Kedua, hasil: metrik validasi final beserta loss, bukan sekadar “akurasi 90%” yang dicatat di kepala. Ketiga, identitas: nama run yang merangkum config sehingga tabel hasil bisa dibaca tanpa membuka berkas lain, ditambah versi kode (commit git) yang dipakai saat run dijalankan.

Alur kerjanya membentuk siklus tertutup — catatan bukan barang jadi, melainkan bahan baku perbandingan berikutnya:

Contoh Teruji: Tiga Konfigurasi dalam Satu Log #

Berkas experiment_tracking.py melatih tiga konfigurasi pada dataset make_moons yang sama. Setiap run disimpan sebagai satu objek di log_eksperimen.json, log dibaca ulang dari disk, lalu diringkas menjadi tabel perbandingan yang terurut berdasarkan akurasi validasi:

python
"""Experiment tracking pola manual: dict config -> nama run -> log JSON.

Tiga konfigurasi dilatih pada dataset make_moons yang sama. Setiap run
merekam hiperparameter, seed, dan hasil ke log_eksperimen.json. Log
dibaca ulang dari disk, lalu diringkas menjadi tabel perbandingan yang
urut berdasarkan akurasi validasi.
"""

import json

import torch
import torch.nn as nn
from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split

NAMA_LOG = "log_eksperimen.json"

X, y = make_moons(n_samples=300, noise=0.20, random_state=42)
X_latih, X_valid, y_latih, y_valid = train_test_split(
    X, y, test_size=80, random_state=7
)
X_latih = torch.tensor(X_latih, dtype=torch.float32)
y_latih = torch.tensor(y_latih, dtype=torch.long)
X_valid = torch.tensor(X_valid, dtype=torch.float32)
y_valid = torch.tensor(y_valid, dtype=torch.long)


def nama_run(cfg):
    """Nama run yang memuat seluruh nilai hiperparameter penting."""
    return f"lr{cfg['lr']}-h{cfg['hidden']}-do{cfg['dropout']}"


def latih_dan_catat(cfg):
    """Latih satu run, kembalikan catatan lengkap untuk log."""
    torch.manual_seed(cfg["seed"])
    model = nn.Sequential(
        nn.Linear(2, cfg["hidden"]),
        nn.Dropout(cfg["dropout"]),
        nn.ReLU(),
        nn.Linear(cfg["hidden"], 2),
    )
    criterion = nn.CrossEntropyLoss()
    optimizer = torch.optim.SGD(model.parameters(), lr=cfg["lr"])

    for _ in range(cfg["epoch"]):
        optimizer.zero_grad()
        loss = criterion(model(X_latih), y_latih)
        loss.backward()
        optimizer.step()

    model.eval()
    with torch.no_grad():
        loss_valid = criterion(model(X_valid), y_valid).item()
        prediksi = model(X_valid).argmax(dim=1)
        akurasi = (prediksi == y_valid).float().mean().item()

    return {
        "nama_run": nama_run(cfg),
        "config": cfg,
        "hasil": {
            "loss_valid": round(loss_valid, 4),
            "akurasi_valid": round(akurasi, 3),
        },
    }


KONFIGURASI = [
    {"lr": 0.01, "hidden": 8, "dropout": 0.0, "epoch": 150, "seed": 42},
    {"lr": 0.5, "hidden": 8, "dropout": 0.0, "epoch": 150, "seed": 42},
    {"lr": 0.5, "hidden": 32, "dropout": 0.3, "epoch": 150, "seed": 42},
]

print("== melatih 3 run dan menulis log ==")
daftar_run = []
for i, cfg in enumerate(KONFIGURASI, start=1):
    catatan = latih_dan_catat(cfg)
    daftar_run.append(catatan)
    print(f"[{i}/{len(KONFIGURASI)}] {catatan['nama_run']:<20} "
          f"loss_valid={catatan['hasil']['loss_valid']:.4f} "
          f"akurasi_valid={catatan['hasil']['akurasi_valid']:.3f}")

with open(NAMA_LOG, "w", encoding="utf-8") as f:
    json.dump(daftar_run, f, indent=2)
print(f"log ditulis ke {NAMA_LOG}")

print("\n== isi log yang tersimpan di disk ==")
print(json.dumps(daftar_run, indent=2))

print("\n== tabel perbandingan (dibaca ulang dari log) ==")
with open(NAMA_LOG, encoding="utf-8") as f:
    log_dibaca = json.load(f)
print(f"jumlah run di log: {len(log_dibaca)}")
urut = sorted(log_dibaca, key=lambda r: r["hasil"]["akurasi_valid"],
              reverse=True)
print(f"{'nama_run':<20} {'lr':>5} {'hidden':>7} {'dropout':>8} "
      f"{'loss_valid':>11} {'akurasi_valid':>14}")
for r in urut:
    c, h = r["config"], r["hasil"]
    print(f"{r['nama_run']:<20} {c['lr']:>5} {c['hidden']:>7} "
          f"{c['dropout']:>8.1f} {h['loss_valid']:>11.4f} "
          f"{h['akurasi_valid']:>14.3f}")
terbaik = urut[0]
print(f"run terbaik: {terbaik['nama_run']} "
      f"(akurasi_valid={terbaik['hasil']['akurasi_valid']:.3f})")

Output yang Dihasilkan:

text
== melatih 3 run dan menulis log ==
[1/3] lr0.01-h8-do0.0      loss_valid=0.6005 akurasi_valid=0.625
[2/3] lr0.5-h8-do0.0       loss_valid=0.2688 akurasi_valid=0.900
[3/3] lr0.5-h32-do0.3      loss_valid=0.2406 akurasi_valid=0.913
log ditulis ke log_eksperimen.json

== isi log yang tersimpan di disk ==
[
  {
    "nama_run": "lr0.01-h8-do0.0",
    "config": {
      "lr": 0.01,
      "hidden": 8,
      "dropout": 0.0,
      "epoch": 150,
      "seed": 42
    },
    "hasil": {
      "loss_valid": 0.6005,
      "akurasi_valid": 0.625
    }
  },
  {
    "nama_run": "lr0.5-h8-do0.0",
    "config": {
      "lr": 0.5,
      "hidden": 8,
      "dropout": 0.0,
      "epoch": 150,
      "seed": 42
    },
    "hasil": {
      "loss_valid": 0.2688,
      "akurasi_valid": 0.9
    }
  },
  {
    "nama_run": "lr0.5-h32-do0.3",
    "config": {
      "lr": 0.5,
      "hidden": 32,
      "dropout": 0.3,
      "epoch": 150,
      "seed": 42
    },
    "hasil": {
      "loss_valid": 0.2406,
      "akurasi_valid": 0.913
    }
  }
]

== tabel perbandingan (dibaca ulang dari log) ==
jumlah run di log: 3
nama_run                lr  hidden  dropout  loss_valid  akurasi_valid
lr0.5-h32-do0.3        0.5      32      0.3      0.2406          0.913
lr0.5-h8-do0.0         0.5       8      0.0      0.2688          0.900
lr0.01-h8-do0.0       0.01       8      0.0      0.6005          0.625
run terbaik: lr0.5-h32-do0.3 (akurasi_valid=0.913)

Mari kita telaah kode di atas baris per baris:

  1. Baris 16-25 (data): dataset dan pembagian validasi dibuat sekali di luar fungsi training dengan random_state tetap — semua run menghadapi data yang sama, sehingga satu-satunya hal yang berbeda antar run adalah config.
  2. Baris 28-30 (nama_run): nama run dirangkai langsung dari nilai config, sehingga lr0.5-h32-do0.3 bisa dibaca tanpa membuka isi log.
  3. Baris 33-64 (latih_dan_catat): satu run utuh dalam satu fungsi. Seed dipasang di dalam (baris 35) sebelum model dibuat, model dibangun dari config, training loop mini mengikuti pola chapter Training Loop , evaluasi berjalan di model.eval() dengan torch.no_grad(), dan keluarannya adalah dict catatan — bukan sekadar angka akurasi.
  4. Baris 67-71 (KONFIGURASI): tiga eksperimen dideklarasikan sebagai data, bukan kode yang disalin-tempel; menambah run keempat cukup menambah satu baris dict.
  5. Baris 73-84 (loop + tulis): seluruh catatan dikumpulkan lalu ditulis sekali dengan json.dump. Demo ini sengaja tidak menyimpan stempel waktu agar keluarannya deterministik; log produksi menambahkan waktu mulai dan hash commit git di tempat yang sama.
  6. Baris 86-104 (baca ulang): tabel perbandingan dibangun dari log yang dibaca kembali dari disk — membuktikan bahwa log benar-benar berdiri sendiri, bukan variabel di memori.

Perbandingannya membaca seperti cerita: learning rate 0.01 dengan 150 epoch menghasilkan akurasi validasi 0.625 (undertrained), menaikkan learning rate ke 0.5 melompat ke 0.900, dan menambah kapasitas (hidden 32) plus dropout 0.3 menyentuh 0.913. Setiap angka tersebut tertambat pada config yang bisa dibuka kapan pun. Riwayat loss per epoch juga layak dicatat bila kita ingin membedakan “konvergen lambat” dari “berayun” — pola pembacaannya dibahas pada chapter Kurva Learning .

Field Wajib Setiap Eksperimen #

FieldIsiAlasan
nama_runnama unik yang merangkum configtabel hasil terbaca tanpa membuka isi log
configseluruh hiperparameter + seed + jumlah epochrun dapat diulang dan perbedaan antar run eksplisit
hasilmetrik validasi final (loss + akurasi)keputusan didasarkan pada angka objektif
datasumber dataset, versi, dan cara pembagihasil tidak bisa ditafsirkan tanpa konteks data
kodehash commit git saat run dijalankanmenjawab “kode versi mana yang menghasilkan ini”
waktustempel waktu mulai dan selesairekonstruksi kronologi saat membandingkan banyak run

Alat Ekosistem #

Pola manual di atas bekerja untuk puluhan run; untuk ratusan run dengan beberapa orang sekaligus, ekosistem menyediakan tiga alat utama. MLflow menyimpan parameter, metrik, dan artefak per run pada server bersama dengan antarmuka perbandingan. Weights & Biases menambahkan sinkronisasi otomatis gradien dan visualisasi live yang kaya. TensorBoard menjadi pilihan klasik untuk kurva loss/akurasi per epoch; pada TensorFlow 2.21 TensorBoard tidak lagi menjadi dependensi inti dan dipasang terpisah lewat pip install tensorboard, sementara pengguna PyTorch dapat memakai torch.utils.tensorboard.SummaryWriter mengikuti resep resmi di pytorch.org/tutorials/recipes/recipes/tensorboard_with_pytorch.html .

Log yang tertib menjawab “apa yang sudah dicoba dan hasilnya berapa”, namun belum menjawab “mengapa kolega Anda menjalankan ulang kode yang sama dan mendapat angka berbeda”. Chapter berikutnya, Reproducible Experiments , melengkapi bangunan ini dengan seed, versi environment, dan hash dataset.