Artefak dari Bagian 1 perlu dibungkus menjadi layanan agar aplikasi lain — aplikasi web, aplikasi seluler, atau skrip batch — bisa memakai model lewat protokol standar tanpa mengimpor kode pelatihan. Skrip src/api.py membangun REST API dengan FastAPI mengikuti desain endpoint yang dibahas pada chapter REST API untuk Model : satu endpoint pemeriksaan kesehatan untuk sistem pemantau, satu endpoint prediksi untuk pengguna.

Model Dimuat Sekali saat Proses Menyala #

Baris-baris pembuka src/api.py adalah keputusan performa terpenting dalam seluruh proyek:

python src/api.py — pemuatan model dan konstanta kontrak
from fastapi import FastAPI
from pydantic import BaseModel, field_validator

from latih_model import muat_model

MODEL, KONFIG, METRIK = muat_model()
NORMALISASI = KONFIG["preprocessing"]["normalisasi"]
RENTANG = KONFIG["preprocessing"]["rentang_masukan"]
AMBANG = KONFIG["ambang_keyakinan"]

app = FastAPI(title="API Digit Mini", version=KONFIG["versi"])

Pemuatan terjadi di level modul, bukan di dalam handler. Saat uvicorn menjalankan api:app, baris muat_model() dieksekusi sekali sebelum server menerima permintaan pertama; setelah itu setiap request /predict cukup membaca model yang sudah berada di memori. Memuat model di dalam handler akan membaca berkas 13757 byte dan membangun ulang jaringan pada setiap permintaan — pola yang benar untuk demo, keliru untuk produksi. Tiga konstanta kontrak (normalisasi, rentang, ambang) juga dibaca dari konfigurasi sekali di sini, bukan ditempel sebagai angka di dalam handler.

Validasi Input dari Skema, Bukan dari Kode #

Pydantic memvalidasi body permintaan sebelum menyentuh model:

python src/api.py — skema permintaan dan respons
class PermintaanPrediksi(BaseModel):
    """Skema body POST /predict: 64 nilai piksel 0..16."""

    fitur: list[float]

    @field_validator("fitur")
    @classmethod
    def cek_panjang_dan_rentang(cls, nilai):
        if len(nilai) != 64:
            raise ValueError("fitur harus berisi tepat 64 angka")
        if nilai and (min(nilai) < RENTANG[0] or max(nilai) > RENTANG[1]):
            raise ValueError(
                f"nilai piksel harus di rentang {RENTANG[0]}..{RENTANG[1]}")
        return nilai


class HasilPrediksi(BaseModel):
    """Skema respons POST /predict."""

    kelas: int
    label: str
    probabilitas: float
    keyakinan_cukup: bool
    versi: str

Validator memeriksa dua sifat: tepat 64 nilai (satu gambar 8x8) dan seluruhnya dalam rentang 0..16 yang diambil dari konfigurasi. Permintaan yang melanggar ditolak otomatis dengan status 422 sebelum handler berjalan — model tidak pernah menerima masukan tidak sahih. Skema respons ikut dideklarasikan sehingga kontrak keluaran terdokumentasi di OpenAPI.

Dua Endpoint #

Handler /health melaporkan status beserta metadata model; handler /predict menjalankan preprocessing persis seperti pelatihan lalu menghitung probabilitas:

python src/api.py — handler prediksi
@app.post("/predict", response_model=HasilPrediksi)
def predict(permintaan: PermintaanPrediksi):
    """Prediksi digit dari 64 nilai piksel gambar 8x8."""
    import torch

    x = torch.tensor([permintaan.fitur], dtype=torch.float32) / NORMALISASI
    with torch.no_grad():
        probabilitas = torch.softmax(MODEL(x), dim=1)[0]
    kelas = int(probabilitas.argmax())
    nilai = float(probabilitas[kelas])
    return HasilPrediksi(
        kelas=kelas,
        label=f"digit_{kelas}",
        probabilitas=round(nilai, 4),
        keyakinan_cukup=nilai >= AMBANG,
        versi=KONFIG["versi"],
    )

Tiga detail pada handler ini wajib dibedah. Pembagian / NORMALISASI mengulang preprocessing pelatihan (piksel 0..16 menjadi 0..1) — layanan yang preprocessing-nya berbeda dari pelatihan akan menghasilkan prediksi yang meleset meski bobotnya benar. torch.no_grad() mematikan pelacakan gradient pada jalur inferensi. Terakhir, field keyakinan_cukup membandingkan probabilitas terhadap ambang 0.9 dari konfigurasi, sehingga pemanggil API bisa menandai prediksi yang perlu ditinjau ulang.

Menjalankan python3 src/api.py mencetak ringkasan layanan tanpa membuka port:

text
== inference API digit mini ==
model       : mlp-digit-mini versi 1.0.0
akurasi val : 0.9511
ambang      : 0.9 (probabilitas di bawah ambang ditandai)
endpoint    :
  GET,HEAD /docs
  GET,HEAD /docs/oauth2-redirect
  GET     /health
  GET,HEAD /openapi.json
  POST    /predict
  GET,HEAD /redoc

menjalankan server pengembangan:
  uvicorn api:app --host 127.0.0.1 --port 8000
dokumentasi interaktif: http://127.0.0.1:8000/docs

Server sungguhan dinyalakan dari folder src/ dengan perintah uvicorn api:app --host 127.0.0.1 --port 8000; endpoint /docs lalu menyajikan dokumentasi interaktif yang dibangkitkan otomatis dari skema Pydantic, sehingga pemanggil baru bisa mempelajari kontrak request-response tanpa membaca kode sumber.

Satu kebiasaan kecil pada handler patut dijelaskan karena terlihat janggal: import torch ditulis di dalam fungsi predict, bukan di baris atas berkas. Modul torch sebenarnya sudah dimuat oleh latih_model saat proses menyala, jadi import lokal ini hanya mengambil referensi dari cache modul — biayanya nyaris nol. Manfaatnya, berkas api.py tetap bisa dibaca sebagai “kerangka layanan” tanpa dependensi berat di baris import, dan tubuh handler memperlihatkan secara eksplisit pustaka apa yang dipakai di jalur prediksi.

API sudah berdiri; tinggal membuktikan bahwa endpoint-nya bekerja benar dari sisi pemanggil — tugas Bagian 3.