Server TCP pada chapter sebelumnya menunjukkan inti model serving, tetapi protokol baris-per-baris itu hanya nyaman bagi klien yang kita tulis sendiri. Dunia aplikasi berbicara dalam HTTP: endpoint, status code, JSON, dan validasi terstandar. REST API untuk model membuat layanan prediksi bisa diakses dari bahasa pemrograman mana pun, didokumentasikan otomatis, dan ditolak dengan rapi saat input tidak valid. Chapter ini membangun endpoint POST /predict dengan FastAPI — mulai dari skema input, inference, sampai perilaku galat — lalu mengujinya lewat TestClient tanpa membuka port sungguhan.
Anatomi Endpoint Prediksi #
Sebuah endpoint prediksi minimal memuat empat bagian: skema input yang tervalidasi, konversi JSON ke tensor, pemanggilan model, dan skema respons. FastAPI menjahit keempatnya lewat anotasi tipe: parameter fungsi endpoint bertipe BaseModel otomatis divalidasi Pydantic sebelum fungsi berjalan, dan nilai kembali bertipe BaseModel otomatis diserialisasi ke JSON.
| Bagian | Alat | Peran |
|---|---|---|
| Skema input | BaseModel + field_validator | menolak JSON yang salah bentuk sebelum menyentuh model |
| Konversi | torch.tensor(...) | mengubah daftar angka menjadi tensor batch 1 |
| Inference | model(x) dalam no_grad() | menghitung probabilitas kelas |
| Skema respons | response_model=... | menjamin bentuk JSON keluar yang konsisten |
Endpoint GET /health melengkapi layanan: jalur ringan tanpa model yang dipakai load balancer dan sistem pemantau untuk memeriksa kehidupan server — pola yang sudah kita lihat pada health check di Model Serving
.
Praktik: FastAPI dengan Validasi dan Uji Nyata #
Berkas rest_api_predict.py memuat model terlatih di tingkat modul (sekali saat proses menyala), mendefinisikan dua skema Pydantic, lalu menguji empat skenario: dua prediksi valid, satu input salah panjang, satu input salah tipe, dan satu health check. TestClient menjalankan seluruh tumpukan ASGI di dalam proses yang sama sehingga pengujian tidak membutuhkan server terpisah:
"""REST API inferensi dengan FastAPI + TestClient.
Alur: definisi skema Pydantic (validasi input) -> aplikasi FastAPI
dengan endpoint POST /predict dan GET /health -> uji lewat TestClient:
dua prediksi valid, input salah panjang (422), input salah tipe (422),
dan health check.
"""
import torch
import torch.nn as nn
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, field_validator
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)
loader = DataLoader(
TensorDataset(
torch.tensor(X, dtype=torch.float32),
torch.tensor(y, dtype=torch.long),
),
batch_size=32,
shuffle=True,
num_workers=0,
generator=torch.Generator().manual_seed(42),
)
model = nn.Sequential(nn.Linear(2, 12), nn.ReLU(), nn.Linear(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()
app = FastAPI(title="API Klasifikasi Mini", version="1.0.0")
class PermintaanPrediksi(BaseModel):
"""Skema body untuk POST /predict."""
fitur: list[float]
@field_validator("fitur")
@classmethod
def cek_panjang(cls, nilai):
if len(nilai) != 2:
raise ValueError("fitur harus berisi tepat 2 angka")
return nilai
class HasilPrediksi(BaseModel):
"""Skema respons dari POST /predict."""
kelas: int
label: str
probabilitas: list[float]
@app.get("/health")
def health():
return {"status": "ok", "versi": app.version}
@app.post("/predict", response_model=HasilPrediksi)
def predict(permintaan: PermintaanPrediksi):
x = torch.tensor([permintaan.fitur], dtype=torch.float32)
with torch.no_grad():
probabilitas = torch.softmax(model(x), dim=1)[0].tolist()
kelas = probabilitas.index(max(probabilitas))
return HasilPrediksi(
kelas=kelas,
label=f"kelas_{kelas}",
probabilitas=[round(p, 4) for p in probabilitas],
)
client = TestClient(app)
daftar_uji = [
{"fitur": [0.5, -0.8]},
{"fitur": [-1.2, 0.9]},
{"fitur": [1.0, 2.0, 3.0]},
{"fitur": ["nol", "satu"]},
]
for body in daftar_uji:
respons = client.post("/predict", json=body)
if respons.status_code == 200:
print(f"POST /predict {str(body):26s} -> 200 {respons.json()}")
else:
pesan = respons.json()["detail"][0]["msg"]
print(f"POST /predict {str(body):26s} -> {respons.status_code} {pesan}")
respons = client.get("/health")
print(f"GET /health -> {respons.status_code} {respons.json()}")Output yang Dihasilkan:
POST /predict {'fitur': [0.5, -0.8]} -> 200 {'kelas': 1, 'label': 'kelas_1', 'probabilitas': [0.1313, 0.8687]}
POST /predict {'fitur': [-1.2, 0.9]} -> 200 {'kelas': 0, 'label': 'kelas_0', 'probabilitas': [0.9718, 0.0282]}
POST /predict {'fitur': [1.0, 2.0, 3.0]} -> 422 Value error, fitur harus berisi tepat 2 angka
POST /predict {'fitur': ['nol', 'satu']} -> 422 Input should be a valid number, unable to parse string as a number
GET /health -> 200 {'status': 'ok', 'versi': '1.0.0'}Mari kita telaah kode di atas baris per baris:
- Baris 16-37 (model di tingkat modul): training berjalan sekali saat modul diimpor — pola yang di produksi diganti pemuatan paket dari Menyimpan Model untuk Produksi . Endpoint tidak pernah melatih ulang apa pun.
- Baris 40 (
app = FastAPI(...)): satu objek aplikasi menampung seluruh endpoint, lengkap dengan judul dan versi yang muncul di dokumentasi otomatis/docs. - Baris 43-52 (
PermintaanPrediksi): tipelist[float]menolak string dan objek sejak validasi;field_validatormenambah aturan panjang tepat dua. Input[1.0, 2.0, 3.0]gagal di validator kustom kita, sedangkan["nol", "satu"]sudah ditolak Pydantic sebelum sampai ke sana — keduanya pulang sebagai 422 tanpa pernah menyentuh model. - Baris 55-60 (
HasilPrediksi): skema respons memaksa bentuk keluaran konsisten:kelasbilangan bulat,labelteks,probabilitasdaftar angka. Klien dapat menulis kode terhadap kontrak ini tanpa membaca isi server. - Baris 71-80 (endpoint
predict): alur lurus — bangun tensor batch 1, inference tanpa gradian, lalu bungkus hasil. Dekoratorresponse_modelmemvalidasi nilai kembali terhadapHasilPrediksisebelum dikirim. - Baris 91-99 (loop uji):
TestClientmengirim JSON seperti HTTP client sungguhan dan melaporkan status code. Status422adalah konvensi FastAPI untuk body yang gagal validasi — klien mendapat pesan galat yang menjelaskan sebabnya, bukan kegagalan internal 500.
Menjalankan server sungguhan untuk pengembangan lokal cukup satu perintah setelah menyimpan bagian aplikasi ke berkas terpisah — misalnya api.py dengan objek app:
uvicorn api:app --host 127.0.0.1 --port 8000Dokumentasi interaktif Swagger UI tersedia otomatis di http://127.0.0.1:8000/docs, dan pola request body serta pengujian dengan TestClient dibahas resmi di fastapi.tiangolo.com/tutorial/body
dan fastapi.tiangolo.com/tutorial/testing
.
Endpoint per-permintaan seperti ini menuntut satu hal yang belum kita ukur: waktu. Berapa lama satu prediksi benar-benar berjalan, dan berapa banyak yang bisa dilayani per detik? Dua pertanyaan itu membawa kita ke pemisahan antara batch dan real-time inference pada dua chapter berikutnya.