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:
"""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:
== 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:
- Baris 16-25 (data): dataset dan pembagian validasi dibuat sekali di luar fungsi training dengan
random_statetetap — semua run menghadapi data yang sama, sehingga satu-satunya hal yang berbeda antar run adalah config. - Baris 28-30 (
nama_run): nama run dirangkai langsung dari nilai config, sehinggalr0.5-h32-do0.3bisa dibaca tanpa membuka isi log. - 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 dimodel.eval()dengantorch.no_grad(), dan keluarannya adalahdictcatatan — bukan sekadar angka akurasi. - Baris 67-71 (
KONFIGURASI): tiga eksperimen dideklarasikan sebagai data, bukan kode yang disalin-tempel; menambah run keempat cukup menambah satu baris dict. - 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. - 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 #
| Field | Isi | Alasan |
|---|---|---|
nama_run | nama unik yang merangkum config | tabel hasil terbaca tanpa membuka isi log |
config | seluruh hiperparameter + seed + jumlah epoch | run dapat diulang dan perbedaan antar run eksplisit |
hasil | metrik validasi final (loss + akurasi) | keputusan didasarkan pada angka objektif |
data | sumber dataset, versi, dan cara pembagi | hasil tidak bisa ditafsirkan tanpa konteks data |
kode | hash commit git saat run dijalankan | menjawab “kode versi mana yang menghasilkan ini” |
waktu | stempel waktu mulai dan selesai | rekonstruksi 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.