Disiplin eksperimen

Seed, config, dan kebiasaan yang membuat eksperimen dapat direproduksi.

Daftar isi

Topik ini membahas disiplin yang membuat eksperimen pelatihan dapat direproduksi, yaitu kemampuan menjalankan ulang kode yang sama dan memperoleh angka yang sama persis. Mixed precision, gradient clipping, dan jadwal learning rate mengatur cara pelatihan berjalan, sedangkan topik ini mengatur cara pelatihan dicatat sehingga perbandingan antara eksperimen dapat dipercaya. Tiga kebiasaan dibahas berurutan, yaitu mengunci seluruh sumber keacakan dengan seed, menyimpan konfigurasi pada file config terpisah, dan mencatat hasil beserta lingkungannya. Setiap kebiasaan dibuktikan dengan contoh yang dapat dieksekusi.

Tiga sumber keacakan#

Pembacaan angka acak pada satu proses Python berasal dari tiga mesin yang berbeda, yaitu modul random bawaan Python, pustaka NumPy, dan PyTorch sendiri. Masing-masing memelihara keadaan (state) acaknya sendiri, sehingga mengunci salah satunya tidak mengunci dua lainnya. Pelatihan PyTorch menyentuh ketiganya, misalnya pengacakan inisialisasi bobot berasal dari PyTorch, augmentasi data sering berbasis NumPy, dan pustaka bawaan Python sesekali digunakan untuk pengacakan kecil. Kebiasaan yang benar adalah menetapkan satu nilai seed, lalu menyerahkannya kepada ketiga mesin tersebut sebelum apa pun yang acak dijalankan:

python latih-terikat-seed.py
import random

import numpy as np
import torch
import torch.nn as nn

seed = 42
random.seed(seed)         # keacakan modul random
np.random.seed(seed)      # keacakan NumPy
torch.manual_seed(seed)   # keacakan pembuatan dan inisialisasi tensor

model = nn.Linear(4, 2)
x = torch.randn(3, 4)     # shape (3, 4)
y = torch.randn(3, 2)     # shape (3, 2)
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)

for epoch in range(3):
    optimizer.zero_grad()
    loss = nn.functional.mse_loss(model(x), y)
    loss.backward()
    optimizer.step()
    print(f"epoch {epoch + 1}: loss {loss.item():.6f}")

print("bobot[0][0]:", f"{model.weight[0, 0].item():.6f}")
text Keluaran
epoch 1: loss 0.918849
epoch 2: loss 0.605733
epoch 3: loss 0.404062
bobot[0][0]: 0.283889

Reproduksi berarti file yang sama menghasilkan keluaran yang sama pada proses yang berbeda. Jalankan ulang file yang sama tanpa perubahan apa pun:

bash
# jalankan ulang berkas yang sama untuk membuktikan reproduksinya
python3 latih-terikat-seed.py
text Keluaran
epoch 1: loss 0.918849
epoch 2: loss 0.605733
epoch 3: loss 0.404062
bobot[0][0]: 0.283889

Keempat baris keluaran identik pada kedua proses, dan itulah tanda eksperimen terkunci. Pada pelatihan yang menggunakan GPU, kebiasaan yang sama ditambah torch.cuda.manual_seed_all(seed) karena device CUDA memelihara keadaan acaknya tersendiri.

Seed yang berbeda, hasil yang berbeda#

Seed tidak menghapus keacakan, melainkan memilih titik awal keacakan itu. Dua seed berbeda menghasilkan dua perjalanan pelatihan yang berbeda, dan keduanya sama sahnya sebagai contoh. Skrip berikut menjalankan pelatihan yang sama dengan dua seed:

python banding-seed.py
import random

import numpy as np
import torch
import torch.nn as nn


def latih_dengan_seed(seed):
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    model = nn.Linear(4, 2)
    x = torch.randn(3, 4)   # shape (3, 4)
    y = torch.randn(3, 2)   # shape (3, 2)
    optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
    for epoch in range(3):
        optimizer.zero_grad()
        loss = nn.functional.mse_loss(model(x), y)
        loss.backward()
        optimizer.step()
    return loss.item()


print(f"seed 0 : loss akhir {latih_dengan_seed(0):.6f}")
print(f"seed 1 : loss akhir {latih_dengan_seed(1):.6f}")
text Keluaran
seed 0 : loss akhir 0.531727
seed 1 : loss akhir 1.474046

Loss akhirnya berbeda cukup jauh pada pelatihan sekecil ini, karena tiga epoch terlalu pendek untuk menyatukan perjalanan yang titik awalnya berbeda. Konsekuensinya bagi praktik adalah satu angka dari satu seed tidak cukup untuk mengklaim keunggulan. Perbandingan yang dapat dipercaya diperoleh dengan menjalankan beberapa seed per konfigurasi, lalu membandingkan rata-rata dan sebarannya, terutama pada pelatihan pendek.

Nilai seed yang dipilih tidak penting, sedangkan yang penting adalah nilai itu dicatat dan digunakan ulang. Angka 0, 1, atau 42 sama-sama sah, dan tidak ada nilai yang dianggap lebih baik. Ketika seed menjadi bagian dari konfigurasi sebagaimana pada bagian berikutnya, menjalankan ulang eksperimen dengan seed yang sama menjadi hal yang otomatis.

Config sebagai file#

Konfigurasi eksperimen adalah kumpulan nilai yang menentukan pelatihan, misalnya seed, learning rate, ukuran batch, dan jumlah epoch. Menyimpan nilai-nilai itu secara tersebar di dalam kode membuat dua eksperimen sulit dibedakan, karena perbedaannya harus dicari baris demi baris. Kebiasaan yang lebih aman menaruh seluruh nilai tersebut pada satu file config, dan file inilah yang berubah antara eksperimen:

json config.json
{
  "seed": 42,
  "learning_rate": 0.1,
  "batch_size": 8,
  "epoch": 3
}

Skrip pelatihan kemudian membaca file yang sama, sehingga kode tidak pernah disunting untuk mengganti konfigurasi:

python baca-config.py
import json

with open("config.json") as f:
    config = json.load(f)

print("seed          :", config["seed"])
print("learning rate :", config["learning_rate"])
print("batch size    :", config["batch_size"])
print("epoch         :", config["epoch"])
text Keluaran
seed          : 42
learning rate : 0.1
batch size    : 8
epoch         : 3

File config yang disimpan bersama hasil pelatihan menjadi jejak yang tidak dapat diingkari, karena isi file itulah yang benar-benar digunakan saat pelatihan berjalan. Dengan kebiasaan ini, satu eksperimen ditandai oleh satu file config yang berbeda dari pembandingnya, dan seluruh perbedaannya terbaca dalam sekali pandang. Format JSON dipilih karena dapat dibaca tanpa pustaka tambahan dan dapat dimuat dengan pustaka bawaan Python, tetapi bentuk lain seperti YAML atau Python biasa juga sah selama digunakan secara konsisten.

Catatan hasil dan lingkungan#

Reproduksi penuh juga menyertakan lingkungan tempat eksperimen berjalan, karena versi pustaka dapat memengaruhi angka yang keluar. Informasi itu ditampilkan dengan skrip kecil dan dicatat sekali per lingkungan:

python tampilkan-lingkungan.py
import sys

import torch

print("versi python :", sys.version.split()[0])
print("versi torch  :", torch.__version__)
cuda = torch.version.cuda
print("build cuda   :", cuda if cuda else "tidak ada (build cpu)")
text Keluaran
versi python : 3.12.16
versi torch  : 2.15.0+cpu
build cuda   : tidak ada (build cpu)

Hasil setiap eksperimen kemudian dicatat pada satu tabel yang tetap, bukan pada ingatan. Bentuk tabelnya sederhana, sebagaimana contoh berikut.

IDPerubahan configVal lossCatatan
e01config awal0.404nilai pembanding
e02learning_rate 0.050.451konvergensi lebih lambat
e03config awal, seed 10.417sebanding dengan e01

Tabel tersebut menjawab dua pertanyaan sekaligus, yaitu apa yang berubah antara eksperimen dan apa akibatnya. Kebiasaan yang menopangnya adalah mengubah satu nilai config per eksperimen, sehingga selisih hasil dapat dihubungkan dengan perubahan tunggal itu. Mengubah dua nilai sekaligus membuat selisihnya tidak dapat dihubungkan dengan salah satunya. Tempat penyimpanan catatan cukup bebas, misalnya file catatan di samping checkpoint, asalkan kolomnya konsisten sehingga entri-entri lama tetap sebanding berminggu-minggu kemudian.

Kesalahan umum#

Tiga kebiasaan buruk berikut paling sering merusak reproduksibilitas.

Yang pertama adalah mengunci satu sumber keacakan dan menganggap pekerjaan selesai. Hasilnya berubah diam-diam melalui sumber yang terlewat, sebagaimana dijelaskan pada peringatan bagian awal. Perbaikannya, kunci random, NumPy, dan PyTorch dalam satu blok seed yang sama, ditambah torch.cuda.manual_seed_all() bila GPU digunakan.

Yang kedua adalah mengandalkan ingatan mengenai konfigurasi eksperimen lama. Ingatan tentang nilai learning rate atau jumlah epoch hampir pasti bercampur antara eksperimen. Perbaikannya, setiap eksperimen memasangkan dirinya dengan file config yang disimpan bersama hasilnya.

Yang ketiga adalah mengubah lebih dari satu nilai sekaligus pada eksperimen berikutnya. Selisih hasil kemudian tidak dapat dihubungkan dengan sebab yang jelas, sehingga eksperimen itu hampir tidak memberi informasi. Perbaikannya, pecah menjadi dua eksperimen dengan satu perubahan masing-masing.

Ringkasan#

  • Reproduksi dimulai dari mengunci ketiga sumber keacakan, yaitu random, NumPy, dan PyTorch, dengan satu nilai seed sebelum kode yang acak dijalankan.
  • Konfigurasi eksperimen disimpan sebagai satu file config yang dibaca skrip, sehingga perbedaan antara eksperimen terbaca tanpa membuka kode.
  • Hasil, lingkungan, dan perubahan config dicatat pada tabel yang tetap dengan satu perubahan per eksperimen, sehingga perbandingan antara eksperimen dapat dipercaya.

Referensi#