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 ketaktertentuan | Efek bila tak dikunci | Solusi |
|---|---|---|
| Inisialisasi bobot | tiap run mulai dari titik berbeda | torch.manual_seed(seed) sebelum model dibuat |
Urutan batch (shuffle) | gradien dihitung dari urutan berbeda | generator=torch.Generator().manual_seed(seed) |
| Dropout saat forward | neuron yang terhapus berbeda | seed torch global dikunci sebelum training |
Acak di __getitem__ / worker | tiap worker memakai seed berbeda | worker_init_fn yang menurunkan seed per worker |
| Operasi CUDA tertentu | urutan penjumlahan paralel berubah | torch.use_deterministic_algorithms(True) |
| Versi pustaka berbeda | hasil numerik bergeser sedikit | rekam 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:
"""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:
== 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.15Mari kita telaah kode di atas baris per baris:
- Baris 21-25 (
kunci_semua_seed): tiga generator dikunci dengan satu angka. Fungsi dipanggil di awallatih_mini— urutan mutlak: seed harus dipasang sebelum model dibuat dan sebelum data acak dihasilkan, karena menunggalkan seed setelahnya tidak memulihkan angka yang sudah terpakai. - Baris 28-32 (
atur_seed_worker):torch.initial_seed()di dalam worker mengembalikan seed dasar ditambah nomor worker; nilai itu diturunkan ke NumPy danrandom. Dengannum_workers=0fungsi ini tak pernah terpanggil, tetapi tetap dipasang agar cetakan langsung aman begitu worker dinyalakan. - Baris 37-53 (data, generator, dan
DataLoader): dataset dibuatrandom_state=42sehingga identik di semua run, sementara generator DataLoader dikunci terpisah dengan seed yang sama — persis pola chapter Dataset dan DataLoader — danworker_init_fnikut dipasang pada baris 52. - Baris 70-79 (eksperimen):
run_a == run_bmembuktikan reproducibility penuh;run_a != run_cmembuktikan bahwa seed benar-benar mengganti jalur — perbedaan antar seed adalah variasi yang wajar, bukan galat. - 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.
- 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.