Menyimpan model untuk keperluan riset dan menyimpannya untuk produksi menuntut hal yang berbeda. Saat bereksperimen di notebook, sebuah berkas checkpoint.pth yang berisi bobot sudah cukup — kode arsitektur, normalisasi, dan label kelas semuanya hidup di sel yang sama. Begitu model harus melayani pengguna, setiap informasi itu justru tidak boleh bergantung pada notebook: sistem produksi memuat model dari kumpulan berkas yang berdiri sendiri. Chapter ini menyusun paket artefak produksi yang memuat bobot, konfigurasi arsitektur, parameter preprocessing, versi, dan metadata, lalu membuktikan bahwa paket tersebut cukup untuk memulihkan model dengan prediksi yang identik.

Artefak Riset versus Artefak Produksi #

Artefak riset menjawab kebutuhan penulisnya sendiri: cepat disimpan, cukup untuk melanjutkan eksperimen. Artefak produksi menjawab kebutuhan sistem lain yang memuatnya tanpa konteks notebook. Tabel berikut memetakan isi minimum sebuah paket produksi:

KomponenIsiFungsi saat pemuatan
model.pthstate_dict (tensor parameter)mengisi bobot ke objek model yang dibangun ulang
config.jsonarsitektur, preprocessing, label kelasmembangun model dan mengolah input tanpa menebak
Versipenanda rilis, misal 1.0.0mencocokkan paket dengan kode pemuat yang sesuai
Metadataversi PyTorch, asal data, penulisaudit dan penelusuran saat terjadi masalah

Pola state_dict sudah kita kuasai pada chapter Penyimpanan dan Pemuatan Model ; di sini pola itu diperluas menjadi paket lengkap. Konfigurasi preprocessing wajib ikut disimpan karena model terlatih pada data yang sudah dinormalisasi — tanpa nilai rata-rata dan simpangan yang sama, input produksi tidak akan pernah menempati rentang yang dipelajari model.

Format .pt, TorchScript, dan ONNX #

Ada tiga keluarga format yang sering dipakai saat membawa model keluar dari sesi training:

FormatIsi berkasKapan dipakai
.pt / .pth (state_dict + config)tensor + JSON metadatapemuatan ulang di kode Python yang sama; paling fleksibel
TorchScriptgraph terkompilasimodel tanpa dependensi kode Python — kini deprecated, digantikan torch.export
ONNXgraph standar lintas frameworkinference di runtime lain (C++, ONNX Runtime) — dibahas di chapter ONNX dan Model Export

Praktik: Menyusun dan Memuat Paket Produksi #

Berkas paket_produksi.py melatih MLP kecil, menyusun folder paket_klasifikasi berisi model.pth dan config.json, lalu memuat ulang model hanya dari isi paket — seolah dilakukan oleh proses lain yang tidak mengenal sesi training:

python
"""Menyusun paket artefak produksi: bobot + konfigurasi + versi.

Alur: latih MLP kecil -> susun paket produksi (model.pth + config.json
di dalam satu folder) -> muat ulang model hanya dari isi paket ->
verifikasi prediksi identik dengan model asli -> bersihkan berkas demo.
"""

import json
import shutil
from pathlib import Path

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

PAKET = Path("paket_klasifikasi")


class MLP(nn.Module):
    """MLP dua layer untuk klasifikasi dua kelas."""

    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)
rata_rata = X.mean(axis=0)
simpangan = X.std(axis=0)
X_norm = (X - rata_rata) / simpangan

loader = DataLoader(
    TensorDataset(
        torch.tensor(X_norm, dtype=torch.float32),
        torch.tensor(y, dtype=torch.long),
    ),
    batch_size=32,
    shuffle=True,
    num_workers=0,
    generator=torch.Generator().manual_seed(42),
)
model = MLP(2, 12, 2)
kriteria = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.5)
for _ in range(20):
    for X_batch, y_batch in loader:
        optimizer.zero_grad()
        loss = kriteria(model(X_batch), y_batch)
        loss.backward()
        optimizer.step()
model.eval()

# ---------- 1. Susun paket produksi ----------
PAKET.mkdir(exist_ok=True)
torch.save(model.state_dict(), PAKET / "model.pth")
konfigurasi = {
    "versi": "1.0.0",
    "arsitektur": {
        "jenis": "MLP",
        "jumlah_fitur": 2,
        "jumlah_hidden": 12,
        "jumlah_kelas": 2,
    },
    "preprocessing": {
        "rata_rata": rata_rata.tolist(),
        "simpangan": simpangan.tolist(),
    },
    "label": ["kelas_0", "kelas_1"],
    "dibuat_dengan": {"torch": torch.__version__, "sklearn": sklearn.__version__},
}
(PAKET / "config.json").write_text(
    json.dumps(konfigurasi, indent=2), encoding="utf-8"
)
print("isi paket produksi:")
for nama in sorted(p.name for p in PAKET.iterdir()):
    ukuran = (PAKET / nama).stat().st_size
    print(f"  {nama:12s} {ukuran:6d} byte")

# ---------- 2. Muat ulang hanya dari isi paket ----------
cfg = json.loads((PAKET / "config.json").read_text(encoding="utf-8"))
arsitektur = cfg["arsitektur"]
model_muat = MLP(
    arsitektur["jumlah_fitur"],
    arsitektur["jumlah_hidden"],
    arsitektur["jumlah_kelas"],
)
model_muat.load_state_dict(
    torch.load(PAKET / "model.pth", weights_only=True)
)
model_muat.eval()

X_baru = (X - cfg["preprocessing"]["rata_rata"]) / cfg["preprocessing"]["simpangan"]
X_baru = torch.tensor(X_baru, dtype=torch.float32)
with torch.no_grad():
    logits_asli = model(X_baru)
    logits_muat = model_muat(X_baru)
print("\nversi paket                  :", cfg["versi"])
print("prediksi model asli == muat  :", torch.equal(logits_asli, logits_muat))
prediksi = logits_muat.argmax(dim=1).tolist()[:3]
print("tiga prediksi pertama        :", [cfg["label"][i] for i in prediksi])

# ---------- 3. Bersihkan berkas demo ----------
shutil.rmtree(PAKET)
print("folder paket_klasifikasi dihapus dari bundle")

Output yang Dihasilkan:

text
isi paket produksi:
  config.json     437 byte
  model.pth      2569 byte

versi paket                  : 1.0.0
prediksi model asli == muat  : True
tiga prediksi pertama        : ['kelas_1', 'kelas_1', 'kelas_0']
folder paket_klasifikasi dihapus dari bundle

Mari kita telaah kode di atas baris per baris:

  1. Baris 34-36 (rata_rata, simpangan): statistik preprocessing dihitung sebelum training dan disimpan terpisah — keduanya nanti masuk ke config.json, bukan tertanam di dalam bobot.
  2. Baris 58 (torch.save(model.state_dict(), ...)): hanya tensor parameter yang ditulis ke model.pth. Kode class MLP tetap berada di sisi pemuat, sehingga berkas tidak mengikat lokasi definisi class.
  3. Baris 59-75 (kamus konfigurasi): lima kunci membentuk kontrak pemuatan — arsitektur untuk membangun model, preprocessing untuk mengolah input, label untuk menerjemahkan indeks kelas, versi untuk pencocokan, dan metadata dibuat_dengan untuk audit.
  4. Baris 81-91 (pemuatan ulang): objek model dibangun murni dari nilai config.json, bukan dari konstanta yang ditulis tangan. Jika hidden layer diubah menjadi 24 pada training berikutnya, pemuat tetap benar tanpa perubahan kode.
  5. Baris 93-96 (verifikasi): torch.equal membandingkan logits elemen demi elemen — hasil True membuktikan paket memulihkan model sampai ke bit terakhir, termasuk lewat jalur preprocessing yang dibaca dari JSON.

Paket yang berdiri sendiri seperti ini adalah syarat awal sebelum model bisa disajikan ke pengguna. Pertanyaan berikutnya bukan lagi apa yang disimpan, melainkan bagaimana model itu menerima permintaan dan mengembalikan hasil — urusan pola model serving yang kita bahas pada chapter berikutnya. Panduan resmi serialisasi PyTorch tersedia di docs.pytorch.org/docs/stable/notes/serialization.html .