Eksperimen deep learning yang baik seharusnya diperlakukan seperti objek terkelola: ia punya identitas (config dan seed), punya asal-usul data (versi dan hash dataset), punya lingkungan tempat ia berjalan (versi pustaka), dan punya hasil yang tersimpan sebagai berkas — bukan sekadar proses yang berjalan sekali lalu berakhir. Chapter ini menyusun perlakuan tersebut dalam tiga level yang bertingkat: seed untuk mengulang hasil di mesin yang sama, rekaman config dan kode untuk menjawab “eksperimen mana yang menghasilkan angka ini”, dan environment lock agar kolega di mesin lain memperoleh angka yang sama. Seluruh contoh diuji nyata pada CPU; level API (seed torch, NumPy, random, generator DataLoader) sudah dibedah tuntas pada chapter Reproducibility — di sini kita naik ke tingkat eksperimen sebagai satu kesatuan.

Tiga Level Reproducibility #

LevelYang dikunciAlat utamaPertanyaan yang dijawab
1 — Seedseluruh generator acaktorch.manual_seed, np.random.seed, random.seedjalankan ulang di mesin ini, hasilnya sama?
2 — Rekamanconfig, versi kode, versi datadict hasil + JSON + commit git + hash dataseteksperimen mana yang menghasilkan angka ini?
3 — Environmentversi seluruh pustakarequirements.txt hasil pip freezedi mesin kolega, angkanya tetap sama?

Level 1 tanpa level 2 menghasilkan paradoks yang sering terjadi: hasil bisa diulang, tetapi tidak ada yang tahu hasil mana yang harus diulang. Level 2 tanpa level 3 menghasilkan paradoks kedua: catatan lengkap tersimpan, tetapi upgrade torch satu versi menggeser angka di digit akhir sehingga rekaman lama tidak bisa dibandingkan dengan run baru. Ketiga level saling melengkapi, dan pola perekamannya persis melanjutkan log JSON dari chapter Experiment Tracking .

Contoh Teruji: run_experiment dan Rekaman JSON #

Berkas reproducible_experiments.py membungkus satu eksperimen utuh ke dalam fungsi run_experiment(seed, config). Dua pemanggilan dengan seed sama dibandingkan sebagai dict utuh, lalu rekaman lengkap — config, hash dataset, hasil, versi environment — ditulis ke hasil_eksperimen.json dan dibaca ulang:

python
"""Eksperimen sebagai objek terkelola: seed, metadata, hash, environment.

Fungsi run_experiment(seed) mengunci seluruh generator acak, melatih
model kecil, lalu mengembalikan dict hasil. Dua pemanggilan dengan seed
sama dibandingkan bit demi bit; rekaman eksperimen lengkap (config,
hash dataset, versi environment) disimpan ke hasil_eksperimen.json dan
dibaca ulang untuk membuktikan hasilnya identik dengan yang di memori.
"""

import hashlib
import json
import random
import sys

import numpy as np
import sklearn
import torch
import torch.nn as nn
from sklearn.datasets import make_moons


def kunci_seed(seed):
    """Kunci ketiga generator acak (random, NumPy, torch) ke satu angka."""
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)


def hash_dataset(X, y):
    """SHA-256 (16 digit pertama) atas byte mentah fitur dan label."""
    gabungan = X.tobytes() + y.tobytes()
    return hashlib.sha256(gabungan).hexdigest()[:16]


def run_experiment(seed, config):
    """Satu eksperimen utuh: kunci seed -> latih -> kembalikan dict hasil."""
    kunci_seed(seed)

    X, y = make_moons(n_samples=300, noise=0.20, random_state=42)
    pembagi = 240
    X_latih = torch.tensor(X[:pembagi], dtype=torch.float32)
    y_latih = torch.tensor(y[:pembagi], dtype=torch.long)
    X_valid = torch.tensor(X[pembagi:], dtype=torch.float32)
    y_valid = torch.tensor(y[pembagi:], dtype=torch.long)

    model = nn.Sequential(
        nn.Linear(2, config["hidden"]),
        nn.ReLU(),
        nn.Linear(config["hidden"], 2),
    )
    criterion = nn.CrossEntropyLoss()
    optimizer = torch.optim.SGD(model.parameters(), lr=config["lr"])

    riwayat_loss = []
    for _ in range(config["epoch"]):
        optimizer.zero_grad()
        loss = criterion(model(X_latih), y_latih)
        loss.backward()
        optimizer.step()
        riwayat_loss.append(round(loss.item(), 6))

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

    return {
        "seed": seed,
        "riwayat_loss": riwayat_loss,
        "akurasi_valid": round(akurasi, 4),
        "hash_dataset": hash_dataset(X, y),
    }


CONFIG = {"hidden": 16, "lr": 0.5, "epoch": 80}

print("== level 1: seed yang sama, hasil identik ==")
hasil_a = run_experiment(seed=42, config=CONFIG)
hasil_b = run_experiment(seed=42, config=CONFIG)
hasil_c = run_experiment(seed=7, config=CONFIG)
print(f"seed 42 run-1: loss akhir={hasil_a['riwayat_loss'][-1]:.4f} "
      f"akurasi_valid={hasil_a['akurasi_valid']:.4f}")
print(f"seed 42 run-2: loss akhir={hasil_b['riwayat_loss'][-1]:.4f} "
      f"akurasi_valid={hasil_b['akurasi_valid']:.4f}")
print(f"seed  7      : loss akhir={hasil_c['riwayat_loss'][-1]:.4f} "
      f"akurasi_valid={hasil_c['akurasi_valid']:.4f}")
print(f"seed sama -> dict hasil identik : {hasil_a == hasil_b}")
print(f"seed beda -> dict hasil berbeda : {hasil_a != hasil_c}")
print(f"hash dataset : {hasil_a['hash_dataset']} (sama di semua run: "
      f"{hasil_a['hash_dataset'] == hasil_c['hash_dataset']})")

print("\n== level 2-3: rekaman eksperimen + environment lock ==")
rekaman = {
    "nama_eksperimen": "moons-mlp-v1",
    "config": CONFIG,
    "hasil": hasil_a,
    "environment": {
        "python": ".".join(str(v) for v in sys.version_info[:3]),
        "torch": torch.__version__,
        "numpy": np.__version__,
        "sklearn": sklearn.__version__,
    },
}
NAMA_BERKAS = "hasil_eksperimen.json"
with open(NAMA_BERKAS, "w", encoding="utf-8") as f:
    json.dump(rekaman, f, indent=2)
print(f"rekaman ditulis ke {NAMA_BERKAS}")

with open(NAMA_BERKAS, encoding="utf-8") as f:
    dibaca = json.load(f)
print(f"nama eksperimen : {dibaca['nama_eksperimen']}")
print(f"config          : {dibaca['config']}")
print(f"environment     : {dibaca['environment']}")
print(f"hash dataset    : {dibaca['hasil']['hash_dataset']}")
print(f"loss epoch 1/akhir : {dibaca['hasil']['riwayat_loss'][0]:.4f} / "
      f"{dibaca['hasil']['riwayat_loss'][-1]:.4f}")
print(f"rekaman dibaca ulang identik dengan memori : {dibaca == rekaman}")

print("\n== requirements.txt (cuplikan) ==")
paksa = [
    f"torch=={torch.__version__}",
    f"numpy=={np.__version__}",
    f"scikit-learn=={sklearn.__version__}",
]
for baris in paksa:
    print(baris)
print("# hasil pip freeze lengkap -> requirements.txt")

Output yang Dihasilkan:

text
== level 1: seed yang sama, hasil identik ==
seed 42 run-1: loss akhir=0.2740 akurasi_valid=0.8167
seed 42 run-2: loss akhir=0.2740 akurasi_valid=0.8167
seed  7      : loss akhir=0.2598 akurasi_valid=0.8500
seed sama -> dict hasil identik : True
seed beda -> dict hasil berbeda : True
hash dataset : f4cab69be672239c (sama di semua run: True)

== level 2-3: rekaman eksperimen + environment lock ==
rekaman ditulis ke hasil_eksperimen.json
nama eksperimen : moons-mlp-v1
config          : {'hidden': 16, 'lr': 0.5, 'epoch': 80}
environment     : {'python': '3.12.15', 'torch': '2.14.1+cpu', 'numpy': '2.1.3', 'sklearn': '1.5.2'}
hash dataset    : f4cab69be672239c
loss epoch 1/akhir : 0.6340 / 0.2740
rekaman dibaca ulang identik dengan memori : True

== requirements.txt (cuplikan) ==
torch==2.14.1+cpu
numpy==2.1.3
scikit-learn==1.5.2
# hasil pip freeze lengkap -> requirements.txt

Mari kita telaah kode di atas baris per baris:

  1. Baris 22-26 (kunci_seed): ketiga generator dikunci dengan satu angka sebelum data dan model dibuat — urutan ini mutlak, karena seed yang dipasang setelahnya tidak memulihkan angka acak yang sudah terpakai.
  2. Baris 29-32 (hash_dataset): X.tobytes() membaca byte mentah array NumPy, digabung dengan label, lalu diringkas menjadi SHA-256. Hash yang sama berarti data yang identik bit demi bit — jauh lebih kokoh daripada mencatat “make_moons 300 sampel” yang tidak menangkap perubahan noise sekalipun.
  3. Baris 35-72 (run_experiment): eksperimen utuh dalam satu fungsi: seed dikunci, dataset dibuat dengan random_state=42 (tetap di semua run sehingga perbedaan antar run murni efek seed training), model dilatih, dan hasil dikembalikan sebagai dict — termasuk hash dataset yang dihitung saat eksperimen berjalan, bukan diketik manual.
  4. Baris 77-90 (level 1): hasil_a == hasil_b membandingkan seluruh riwayat loss, akurasi, dan hash sebagai satu objek — True berarti identik penuh. hasil_c memakai seed 7 dan menghasilkan jalur berbeda (0.8500) dengan hash dataset yang tetap sama: seed mengubah training, bukan data.
  5. Baris 92-117 (level 2-3): rekaman menyatukan nama eksperimen, config, hasil, dan versi environment lalu ditulis ke JSON. Baris terakhir membuktikan rekaman yang dibaca ulang dari disk identik dengan objek di memori — angka yang akan dilaporkan adalah angka yang tersimpan.
  6. Baris 119-127 (requirements): cuplikan versi pustaka dicetak dari atribut __version__; di proyek nyata, pip freeze > requirements.txt dari dalam virtual environment mengunci seluruh grafik dependensi sekaligus.

Identik Itu Diukur, Bukan Diasumsikan #

Perhatikan bahwa seluruh klaim reproduktif pada keluaran diambil dari perbandingan eksplisit — True pada baris dict hasil identik, True pada hash dataset sama, True pada rekaman dibaca ulang identik. Tidak ada satu pun angka yang kita percaya karena “biasanya begitu”. Kebiasaan ini penting karena identik penuh hanya berlaku pada mesin dan versi pustaka yang sama: memindahkan kode ke GPU atau memperbarui torch dapat menggeser angka di digit-digit akhir meski seed sama. Dokumentasi resmi seluruh sumber ketaktertentuan di torch dibahas di docs.pytorch.org/docs/stable/notes/randomness.html , dan panduan manajemen dependensi Python tersedia di packaging.python.org/en/latest/tutorials/managing-dependencies/ .

Checklist Praktik #

Sebelum sebuah eksperimen dinyatakan selesai, pastikan hal-hal berikut:

  • Seed dipasang sebelum pembuatan data dan model, dan nilainya ikut tersimpan di config.
  • Hasil disimpan bersama config, hash dataset, dan versi environment dalam satu berkas JSON — bukan hanya di keluaran terminal.
  • Kode yang menjalankan eksperimen dikomit ke git, dan hash commit dicatat di rekaman.
  • requirements.txt dibuat dari virtual environment proyek melalui pip freeze, lalu dikomit bersama kode.
  • Sebelum melaporkan, satu run verifikasi dijalankan ulang dari berkas JSON untuk memastikan angka yang dilaporkan adalah angka yang terekam.

Dengan metrik diagnosis, pemantauan gradien, pencatatan eksperimen, dan reproducibility tiga level, perangkat evaluasi kita kini lengkap dari angka tunggal hingga keandalan hasil. Perkara berikutnya berpindah ke arsitektur: Convolutional Neural Network menghadirkan lapisan convolution yang membaca pola spasial pada citra — dan seluruh kebiasaan evaluasi pada section ini akan tetap menjadi alat ukurnya.