Training yang baik memakan waktu — dan kehilangan hasilnya karena proses berhenti atau lupa menyimpan berarti mengulang semuanya dari nol. PyTorch menyerahkan penyimpanan model kepada torch.save dan torch.load, tetapi keputusan yang benar-benar penting ada di tangan kita: apa yang disimpan. Framework ini menawarkan dua pilihan — kamus parameter bernama state_dict, atau seluruh objek model yang dipickle utuh — dan keduanya berbeda jauh soal portabilitas serta keamanan. Chapter ini mempraktikkan kedua pola, menyusun checkpoint yang memuat epoch dan state optimizer, lalu membuktikan bahwa training yang dijeda dan dilanjutkan menghasilkan bobot yang identik dengan training tanpa gangguan.
state_dict: Simpan Parameter, Bukan Objek #
state_dict adalah pemetaan dari nama parameter ke tensor — pada model dua layer, isinya satu.weight, satu.bias, dua.weight, dua.bias. Menyimpan state_dict berarti menyimpan angka-angka hasil training tanpa membawa kode class, struktur folder, atau objek Python lain. Objek model dibangun ulang dari kode kita sendiri, lalu load_state_dict() menyalin nilai tensor ke parameter yang namanya cocok. Dua akibat baiknya: berkas menjadi kecil dan netral terhadap perombakan kode, serta pemuatan dapat berjalan dalam mode aman weights_only=True yang menolak menjalankan kode sembarangan dari dalam berkas.
Berkas simpan_muat_state_dict.py melatih model kecil, menyimpan state_dict, memuatnya ke objek kedua, lalu memverifikasi keduanya:
"""Simpan dan muat state_dict: dua objek model terpisah, hasil identik.
Alur: latih model kecil -> periksa isi state_dict -> torch.save ->
buat objek model baru -> load_state_dict(torch.load(...)) -> verifikasi
prediksi dan bobot identik. Ditutup demonstrasi menyimpan seluruh
objek model yang ditolak mode aman weights_only=True.
"""
from pathlib import Path
import torch
import torch.nn as nn
from sklearn.datasets import make_moons
from torch.utils.data import DataLoader, TensorDataset
class MLP(nn.Module):
"""Multilayer perceptron kecil dengan dua layer Linear."""
def __init__(self, jumlah_fitur, jumlah_hidden, jumlah_kelas):
super().__init__()
self.satu = nn.Linear(jumlah_fitur, jumlah_hidden)
self.dua = nn.Linear(jumlah_hidden, jumlah_kelas)
def forward(self, x):
return self.dua(torch.relu(self.satu(x)))
torch.manual_seed(42)
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),
)
loader = DataLoader(
dataset,
batch_size=32,
shuffle=True,
num_workers=0,
generator=torch.Generator().manual_seed(42),
)
# ---------- 1. Latih model asli ----------
model = MLP(2, 12, 2)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.5)
for epoch in range(20):
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()
print("training 20 epoch selesai | loss rata-rata epoch terakhir:",
round(total / len(loader), 4))
# ---------- 2. state_dict adalah pemetaan nama parameter -> tensor ----------
print("\nisi state_dict:")
for nama, tensor in model.state_dict().items():
print(f" {nama:14s} {tuple(tensor.shape)}")
# ---------- 3. Simpan state_dict, muat ke objek model baru ----------
torch.save(model.state_dict(), "model_terlatih.pth")
model_muat = MLP(2, 12, 2)
model_muat.load_state_dict(torch.load("model_terlatih.pth", weights_only=True))
X_cek = torch.tensor(X, dtype=torch.float32)
with torch.no_grad():
logits_asli = model(X_cek)
logits_muat = model_muat(X_cek)
print("\nprediksi model asli == model muat :",
torch.equal(logits_asli, logits_muat))
print("bobot 'dua.weight' identik :",
torch.equal(model.state_dict()["dua.weight"],
model_muat.state_dict()["dua.weight"]))
# ---------- 4. Perbandingan: menyimpan seluruh objek model ----------
torch.save(model, "model_utuh.pth")
try:
torch.load("model_utuh.pth")
except Exception as galat:
print("memuat seluruh model (pickle) ditolak:", type(galat).__name__)
Path("model_terlatih.pth").unlink()
Path("model_utuh.pth").unlink()
print("berkas demo .pth dihapus dari bundle")Output yang Dihasilkan:
training 20 epoch selesai | loss rata-rata epoch terakhir: 0.2518
isi state_dict:
satu.weight (12, 2)
satu.bias (12,)
dua.weight (2, 12)
dua.bias (2,)
prediksi model asli == model muat : True
bobot 'dua.weight' identik : True
memuat seluruh model (pickle) ditolak: UnpicklingError
berkas demo .pth dihapus dari bundleMari kita telaah kode di atas baris per baris:
- Baris 17-26 (
class MLP): nama atributsatudanduamenjadi bagian kuncistate_dict— kunci inilah jembatan saat memuat: model tujuan harus punya parameter bernama sama dan berbentuk sama. - Baris 65-67 (simpan dan muat):
torch.savemenulis kamus tensor ke berkas.pth;torch.load(..., weights_only=True)membacanya dalam mode aman yang hanya mengizinkan tensor dan tipe primitif;load_state_dictmenyalin nilainya. - Baris 69-77 (verifikasi):
torch.equalmembandingkan elemen demi elemen — prediksi kedua model identik penuh pada seluruh 256 sampel, bukan sekadar mirip. - Baris 80-84 (seluruh model):
torch.save(model, ...)memickle objek utuh bersama kodenya; pemuatan tanpaweights_only=Falseditolak denganUnpicklingError— perilaku bawaan PyTorch sejak versi 2.6 demi melindungi pengguna dari berkas tak dipercaya.
Checkpoint dan Resume Training #
Menyimpan state_dict saja tidak cukup untuk training panjang yang bisa terputus. Checkpoint yang layak menyimpan konteks berjalannya training: nomor epoch, loss terakhir, state optimizer, dan — agar urutan acak batch bisa dilanjutkan — state generator DataLoader. Berkas checkpoint_resume.py menjalankan skenario penuh: sesi pertama berhenti di epoch 6, checkpoint disimpan, sesi kedua memuat semuanya dan melanjutkan hingga epoch 10, lalu hasilnya dibandingkan dengan training 10 epoch tanpa gangguan:
"""Pola checkpoint: simpan titik henti training lalu lanjutkan.
Alur: sesi pertama latih 6 epoch -> simpan checkpoint (epoch, loss,
state_dict model, state_dict optimizer, state generator DataLoader) ->
sesi kedua muat checkpoint ke model dan optimizer baru -> lanjut 4
epoch -> bandingkan dengan training 10 epoch tanpa gangguan.
"""
from pathlib import Path
import torch
import torch.nn as nn
from sklearn.datasets import make_moons
from torch.utils.data import DataLoader, TensorDataset
torch.manual_seed(42)
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),
)
criterion = nn.CrossEntropyLoss()
def buat_model():
"""Objek model baru dengan bobot awal yang selalu sama (seed 42)."""
torch.manual_seed(42)
return nn.Sequential(nn.Linear(2, 12), nn.ReLU(), nn.Linear(12, 2))
def latih(model, optimizer, jumlah_epoch, generator):
"""Training loop mini; kembalikan riwayat loss rata-rata tiap epoch."""
loader = DataLoader(
dataset,
batch_size=32,
shuffle=True,
num_workers=0,
generator=generator,
)
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
# ---------- 1. Sesi pertama: 6 epoch, lalu simpan checkpoint ----------
model = buat_model()
optimizer = torch.optim.SGD(model.parameters(), lr=0.5)
generator = torch.Generator().manual_seed(42)
riwayat_sesi1 = latih(model, optimizer, 6, generator)
checkpoint = {
"epoch": 6,
"loss": riwayat_sesi1[-1],
"model_state_dict": model.state_dict(),
"optimizer_state_dict": optimizer.state_dict(),
"generator_state": generator.get_state(),
}
torch.save(checkpoint, "checkpoint.pth")
# ---------- 2. Sesi kedua: muat checkpoint, lanjut 4 epoch ----------
model_baru = buat_model()
optimizer_baru = torch.optim.SGD(model_baru.parameters(), lr=0.5)
checkpoint = torch.load("checkpoint.pth", weights_only=True)
print("kunci checkpoint :", list(checkpoint.keys()))
print("epoch tersimpan :", checkpoint["epoch"])
print("loss tersimpan :", round(checkpoint["loss"], 4))
model_baru.load_state_dict(checkpoint["model_state_dict"])
optimizer_baru.load_state_dict(checkpoint["optimizer_state_dict"])
generator_baru = torch.Generator()
generator_baru.set_state(checkpoint["generator_state"])
riwayat_sesi2 = latih(model_baru, optimizer_baru, 4, generator_baru)
# ---------- 3. Pembanding: 10 epoch tanpa gangguan ----------
model_utuh = buat_model()
optimizer_utuh = torch.optim.SGD(model_utuh.parameters(), lr=0.5)
riwayat_utuh = latih(model_utuh, optimizer_utuh, 10,
torch.Generator().manual_seed(42))
# ---------- 4. Verifikasi kesetaraan ----------
print("\nloss sesi 1 (epoch 1-6) :", [f"{v:.3f}" for v in riwayat_sesi1])
print("loss sesi 2 (epoch 7-10) :", [f"{v:.3f}" for v in riwayat_sesi2])
print("loss tanpa gangguan :", [f"{v:.3f}" for v in riwayat_utuh])
print("riwayat loss identik :", riwayat_sesi1 + riwayat_sesi2 == riwayat_utuh)
bobot_identik = all(
torch.equal(w1, w2)
for w1, w2 in zip(model_baru.state_dict().values(),
model_utuh.state_dict().values())
)
print("seluruh bobot identik :", bobot_identik)
Path("checkpoint.pth").unlink()
print("berkas checkpoint.pth dihapus dari bundle")Output yang Dihasilkan:
kunci checkpoint : ['epoch', 'loss', 'model_state_dict', 'optimizer_state_dict', 'generator_state']
epoch tersimpan : 6
loss tersimpan : 0.3063
loss sesi 1 (epoch 1-6) : ['0.486', '0.344', '0.325', '0.322', '0.316', '0.306']
loss sesi 2 (epoch 7-10) : ['0.299', '0.300', '0.298', '0.294']
loss tanpa gangguan : ['0.486', '0.344', '0.325', '0.322', '0.316', '0.306', '0.299', '0.300', '0.298', '0.294']
riwayat loss identik : True
seluruh bobot identik : True
berkas checkpoint.pth dihapus dari bundleMari kita telaah kode di atas baris per baris:
- Baris 26-29 (
buat_model):torch.manual_seed(42)dipanggil di dalam fungsi agar setiap objek model — sesi pertama, sesi kedua, dan pembanding — mulai dari bobot awal yang persis sama. - Baris 61-68 (kamus checkpoint): lima kunci mencakup seluruh state yang menentukan kelanjutan training.
generator_statedarigenerator.get_state()adalah byte internal generator; tanpa kunci ini, epoch ke-7 akan mengulang urutan batch epoch pertama alih-alih melanjutkan deretnya. - Baris 79-82 (muat ulang state): model, optimizer, dan generator tiga-tiganya dipulihkan. SGD tanpa momentum nyaris tak menyimpan state, tetapi pola ini wajib untuk optimizer seperti Adam yang membawa rata-rata gradian dan varians per parameter.
- Baris 87-90 (pembanding): training 10 epoch tanpa henti dijalankan sebagai referensi kebenaran.
- Baris 93-102 (verifikasi): riwayat loss sesi lanjutan menyambung tepat di angka pembandingnya, dan
torch.equalper parameter menyatakan seluruh bobot identik — bukti resume yang benar sampai ke bit terakhir.
Memilih Pola Penyimpanan #
| Pola | Isi berkas | Kapan dipakai |
|---|---|---|
state_dict | tensor parameter saja | berbagi model, inferensi produksi, pemuatan lintas kode |
| Seluruh objek model | objek terpickle + kode class terpikat | prototipe cepat satu mesin, struktur kode sudah stabil |
| Checkpoint penuh | epoch, loss, model, optimizer, generator | training panjang yang harus bisa dilanjutkan |
| Checkpoint terbaik | checkpoint penuh + metrik validasi | menyimpan bobot validasi terbaik saat training berlanjut |
Panduan resmi serialisasi PyTorch — termasuk alasan weights_only dan konvensi ekstensi berkas — dibahas lengkap di docs.pytorch.org/docs/stable/notes/serialization.html
. Bobot yang tersimpan rapi menuntut satu syarat terakhir agar hasilnya bisa dipercaya: eksekusi yang bisa diulang dengan hasil sama persis — masalah reproducibility pada chapter penutup section ini.