Deep learning penuh angka acak: bobot awal diacak, urutan batch diacak, dropout menghapus neuron secara acak. Tanpa kontrol, dua eksekusi program yang sama menghasilkan dua model dengan akurasi berbeda — dan debugging menjadi sulit karena kita tidak tahu apakah perubahan hasil berasal dari kode baru atau sekadar keberuntungan angka acak. Reproducibility memecahkan masalah ini dengan mengunci seluruh sumber ketidakpastian pada satu angka seed, lalu merekam versi pustaka agar orang lain dapat mengulang hasil kita di mesin mereka. Chapter penutup section ini membuktikan konsepnya secara nyata: dua run ber-seed sama mencetak loss yang identik bit demi bit, sementara seed berbeda menghasilkan jalur training yang lain.

Tiga Generator yang Harus Dikunci #

Python, NumPy, dan torch masing-masing punya generator acak sendiri, dan ketiganya dipakai di pipeline deep learning — random oleh augmentasi pustaka standar, NumPy oleh banyak fungsi data, torch oleh inisialisasi bobot serta dropout. DataLoader menambah satu sumber lagi: generator untuk shuffle yang sengaja dipisahkan agar urutan batch tidak bergantung pada state global. Satu seed harus mengalir ke keempatnya:

Pemetaan lengkap sumber ketidakpastian dan penawarnya dirangkum di tabel berikut:

Sumber ketaktertentuanEfek bila tak dikunciSolusi
Inisialisasi bobottiap run mulai dari titik berbedatorch.manual_seed(seed) sebelum model dibuat
Urutan batch (shuffle)gradien dihitung dari urutan berbedagenerator=torch.Generator().manual_seed(seed)
Dropout saat forwardneuron yang terhapus berbedaseed torch global dikunci sebelum training
Acak di __getitem__ / workertiap worker memakai seed berbedaworker_init_fn yang menurunkan seed per worker
Operasi CUDA tertentuurutan penjumlahan paralel berubahtorch.use_deterministic_algorithms(True)
Versi pustaka berbedahasil numerik bergeser sedikitrekam versi paket, kunci environment

Eksperimen: Seed Sama, Loss Identik #

Berkas reproducibility.py merangkum seluruh teknik ke satu fungsi latih_mini(seed), lalu menjalankannya tiga kali — dua kali dengan seed 42 dan sekali dengan seed 7:

python
"""Reproducibility: seed yang sama menghasilkan training yang identik.

Alur: fungsi latih_mini(seed) mengunci seluruh sumber acak (torch, NumPy,
random, generator DataLoader) -> jalankan seed 42 dua kali dan seed 7
sebagai pembanding -> bandingkan riwayat loss. Ditutup uji mode
torch.use_deterministic_algorithms(True) di CPU dan snapshot versi
paket environment.
"""

import random
import sys

import numpy as np
import sklearn
import torch
import torch.nn as nn
from sklearn.datasets import make_moons
from torch.utils.data import DataLoader, TensorDataset


def kunci_semua_seed(seed):
    """Kunci seluruh generator acak Python, NumPy, dan torch ke satu seed."""
    torch.manual_seed(seed)
    np.random.seed(seed)
    random.seed(seed)


def atur_seed_worker(worker_id):
    """Seed NumPy/random di tiap worker (dipakai saat num_workers > 0)."""
    seed = torch.initial_seed() % 2**32
    np.random.seed(seed)
    random.seed(seed)


def latih_mini(seed, jumlah_epoch=10):
    """Training loop mini dengan seluruh sumber acak dikunci ke `seed`."""
    kunci_semua_seed(seed)
    X, y = make_moons(n_samples=256, noise=0.20, random_state=42)
    X = (X - X.mean(axis=0)) / X.std(axis=0)
    dataset = TensorDataset(
        torch.tensor(X, dtype=torch.float32),
        torch.tensor(y, dtype=torch.long),
    )
    generator = torch.Generator()
    generator.manual_seed(seed)
    loader = DataLoader(
        dataset,
        batch_size=32,
        shuffle=True,
        num_workers=0,
        generator=generator,
        worker_init_fn=atur_seed_worker,
    )
    model = nn.Sequential(nn.Linear(2, 12), nn.ReLU(), nn.Linear(12, 2))
    criterion = nn.CrossEntropyLoss()
    optimizer = torch.optim.SGD(model.parameters(), lr=0.5)
    riwayat = []
    for epoch in range(jumlah_epoch):
        total = 0.0
        for X_batch, y_batch in loader:
            optimizer.zero_grad()
            loss = criterion(model(X_batch), y_batch)
            loss.backward()
            optimizer.step()
            total += loss.item()
        riwayat.append(total / len(loader))
    return riwayat


run_a = latih_mini(42)
run_b = latih_mini(42)
run_c = latih_mini(7)

print("== eksperimen seed ==")
print("seed 42, run pertama :", [f"{v:.3f}" for v in run_a])
print("seed 42, run kedua   :", [f"{v:.3f}" for v in run_b])
print("seed 7, pembanding   :", [f"{v:.3f}" for v in run_c])
print("seed sama -> loss identik :", run_a == run_b)
print("seed beda -> loss berbeda :", run_a != run_c)

print("\n== algoritma deterministik ==")
torch.use_deterministic_algorithms(True)
print("mode aktif :", torch.are_deterministic_algorithms_enabled())
riwayat_det = latih_mini(42)
torch.use_deterministic_algorithms(False)
print("loss epoch ke-10 :", round(riwayat_det[-1], 3))
print("identik dengan run pertama:", riwayat_det == run_a)

print("\n== snapshot environment ==")
print("torch   :", torch.__version__)
print("numpy   :", np.__version__)
print("sklearn :", sklearn.__version__)
print("python  :", ".".join(str(v) for v in sys.version_info[:3]))

Output yang Dihasilkan:

text
== eksperimen seed ==
seed 42, run pertama : ['0.486', '0.344', '0.325', '0.322', '0.316', '0.306', '0.299', '0.300', '0.298', '0.294']
seed 42, run kedua   : ['0.486', '0.344', '0.325', '0.322', '0.316', '0.306', '0.299', '0.300', '0.298', '0.294']
seed 7, pembanding   : ['0.497', '0.337', '0.328', '0.322', '0.320', '0.315', '0.318', '0.316', '0.313', '0.313']
seed sama -> loss identik : True
seed beda -> loss berbeda : True

== algoritma deterministik ==
mode aktif : True
loss epoch ke-10 : 0.294
identik dengan run pertama: True

== snapshot environment ==
torch   : 2.14.1+cpu
numpy   : 2.1.3
sklearn : 1.5.2
python  : 3.12.15

Mari kita telaah kode di atas baris per baris:

  1. Baris 21-25 (kunci_semua_seed): tiga generator dikunci dengan satu angka. Fungsi dipanggil di awal latih_mini — urutan mutlak: seed harus dipasang sebelum model dibuat dan sebelum data acak dihasilkan, karena menunggalkan seed setelahnya tidak memulihkan angka yang sudah terpakai.
  2. Baris 28-32 (atur_seed_worker): torch.initial_seed() di dalam worker mengembalikan seed dasar ditambah nomor worker; nilai itu diturunkan ke NumPy dan random. Dengan num_workers=0 fungsi ini tak pernah terpanggil, tetapi tetap dipasang agar cetakan langsung aman begitu worker dinyalakan.
  3. Baris 37-53 (data, generator, dan DataLoader): dataset dibuat random_state=42 sehingga identik di semua run, sementara generator DataLoader dikunci terpisah dengan seed yang sama — persis pola chapter Dataset dan DataLoader — dan worker_init_fn ikut dipasang pada baris 52.
  4. Baris 70-79 (eksperimen): run_a == run_b membuktikan reproducibility penuh; run_a != run_c membuktikan bahwa seed benar-benar mengganti jalur — perbedaan antar seed adalah variasi yang wajar, bukan galat.
  5. Baris 81-87 (algoritma deterministik): mode ini memaksa torch memilih kernel yang hasilnya selalu sama; pada training CPU kita mode tersebut lolos tanpa galat dan riwayat loss tetap identik.
  6. Baris 89-93 (snapshot environment): versi pustaka dicatat karena dua angka floating-point yang dihitung kernel berbeda bisa berbeda di digit terakhir — kunci terakhir reproducibility adalah menjaga environment tetap.

Perbandingan run_a dan run_c menyimpan pelajaran tambahan: keduanya sama-sama konvergen ke loss sekitar 0,29-0,31 meski jalurnya berbeda. Seed yang berbeda bukan galat, melainkan variasi antar-run yang alami — dan karena itu praktik penelitian jarang berhenti pada satu seed. Eksperimen serius melatih tiga sampai lima seed berbeda lalu melaporkan rata-rata dan simpangan metriknya, sehingga klaim “arsitektur A lebih baik dari B” tidak tenggelam oleh keberuntungan satu inisialisasi.

Kerangka lengkap section ini — dari nn.Module, Dataset, training loop, evaluasi, hingga checkpoint — kini membentuk satu alur kerja PyTorch yang utuh dan dapat diulang. Catatan resmi lengkap tentang semua sumber ketaktertentuan tersedia di docs.pytorch.org/docs/stable/notes/randomness.html . Section berikutnya mengulang seluruh alur yang sama di framework pesaingnya: TensorFlow dengan API Keras 3.