Klasifikasi gambar menjawab satu pertanyaan: apa isi gambar ini secara keseluruhan. Seluruh section Convolutional Neural Network berfokus pada cara melatih model yang menjawab pertanyaan itu; chapter ini membalik sudut pandang ke sisi pemakaian — apa yang sebenarnya terjadi ketika model terlatih menerima gambar baru. Kita menelusuri pipeline inferensi dari muat gambar sampai keluarnya label, membaca skor keyakinan lewat top-k prediksi, dan menghadapi kenyataan yang sering dilewatkan: model bisa salah sambil memberi keyakinan yang tinggi.

Alur Pipeline Klasifikasi Gambar #

Pipeline inferensi adalah rangkaian tahap yang berjalan berurutan, identik baik untuk satu gambar maupun seribu gambar dalam satu batch. Diagram berikut memetakan alurnya:

Setiap tahap punya kontrak masukan-keluaran yang tetap. Tabel berikut merinci tahap, operasi, dan wujud datanya pada dataset digits:

TahapOperasiWujud data pada digits
Loadbaca piksel ke array1797 gambar 8x8, nilai 0-16
Resize/normalizeskala ke 0-1, standarkan dengan mean/std data latihtensor (N, 1, 8, 8)
Model forwardblok konvolusi + classifier headlogits (N, 10)
Softmaxubah skor mentah jadi probabilitastiap baris berjumlah 1.0
Top-k + labelambil k kelas berprobabilitas tertinggikelas dan skor keyakinan

Top-k Prediksi dan Skor Keyakinan #

Logits hanyalah skor mentah tanpa satuan; chapter Arsitektur CNN sengaja menahan softmax di luar model karena CrossEntropyLoss sudah memuatnya secara internal. Saat inferensi, kita menerapkan softmax sendiri untuk mengubah logits menjadi probabilitas. Dari sepuluh probabilitas tersebut, dua pertanyaan praktis muncul: kelas mana yang dipilih, dan seberapa yakin model dengan pilihan itu. Top-k menjawab keduanya sekaligus — ambil tiga probabilitas tertinggi, lihat apakah label benar masuk daftar, dan perbandingan antar probabilitas menunjukkan margin keyakinan.

Praktik: Inferensi Batch pada CNN Mini #

Berkas inferensi_klasifikasi.py melatih CNN mini pola chapter Arsitektur CNN selama 10 epoch, lalu memusatkan perhatian pada tahap inferensi: batch forward untuk 450 sampel uji, top-3 probabilitas untuk sampel pertama, daftar prediksi salah, dan akurasi per kelompok keyakinan:

python
"""Pipeline inferensi klasifikasi gambar pada CNN mini dataset digits.

CNN mini dilatih sekali (10 epoch), lalu fokusnya pada tahap inferensi:
batch forward -> logits -> softmax -> top-k probabilitas -> label.
Contoh mencakup prediksi benar dengan keyakinan tinggi, prediksi salah
dengan keyakinan tinggi, serta akurasi per kelompok keyakinan sebagai
gambaran kalibrasi sederhana.
"""

import torch
import torch.nn as nn
import torch.nn.functional as F
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split

torch.manual_seed(42)

# ---------- 1. Data: 1797 gambar 8x8, piksel 0..16 ----------
digits = load_digits()
X = digits.data.astype("float32") / 16.0
y = digits.target.astype("int64")
X_latih, X_uji, y_latih, y_uji = train_test_split(
    X, y, test_size=0.25, random_state=42, stratify=y
)

mu, sigma = X_latih.mean(), X_latih.std()
X_latih = (X_latih - mu) / sigma
X_uji = (X_uji - mu) / sigma


def ke_gambar(matriks):
    """(N, 64) -> (N, 1, 8, 8): tambah dimensi channel."""
    return torch.tensor(matriks).reshape(-1, 1, 8, 8)


X_latih_t, y_latih_t = ke_gambar(X_latih), torch.tensor(y_latih)
X_uji_t, y_uji_t = ke_gambar(X_uji), torch.tensor(y_uji)

# ---------- 2. CNN mini (pola arsitektur-cnn) ----------
model = nn.Sequential(
    nn.Conv2d(1, 16, kernel_size=3, padding=1),
    nn.ReLU(),
    nn.MaxPool2d(2),
    nn.Conv2d(16, 32, kernel_size=3, padding=1),
    nn.ReLU(),
    nn.MaxPool2d(2),
    nn.Flatten(),
    nn.Linear(32 * 2 * 2, 64),
    nn.ReLU(),
    nn.Linear(64, 10),
)

criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)

model.train()
for epoch in range(10):
    for awal in range(0, len(X_latih_t), 32):
        pilih = slice(awal, min(awal + 32, len(X_latih_t)))
        optimizer.zero_grad()
        loss = criterion(model(X_latih_t[pilih]), y_latih_t[pilih])
        loss.backward()
        optimizer.step()

# ---------- 3. Pipeline inferensi batch ----------
def inferensi_topk(model, X, k=3):
    """Forward batch -> logits -> softmax -> top-k (kelas, probabilitas)."""
    model.eval()
    with torch.no_grad():
        logits = model(X)                    # (N, 10) skor mentah
        probs = F.softmax(logits, dim=1)     # jumlah per baris = 1
        nilai, indeks = probs.topk(k, dim=1)
    return indeks, nilai


indeks_top3, nilai_top3 = inferensi_topk(model, X_uji_t, k=3)

benar_top1 = indeks_top3[:, 0] == y_uji_t
benar_top3 = (indeks_top3 == y_uji_t.unsqueeze(1)).any(dim=1)
print(f"sampel uji    : {len(y_uji_t)}")
print(f"akurasi top-1 : {benar_top1.float().mean().item():.3f}")
print(f"akurasi top-3 : {benar_top3.float().mean().item():.3f}")

# ---------- 4. Top-3 untuk lima sampel pertama ----------
print("\ntop-3 prediksi untuk 5 sampel uji pertama:")
for i in range(5):
    kelas = indeks_top3[i].tolist()
    prob = nilai_top3[i].tolist()
    status = "benar" if kelas[0] == y_uji_t[i].item() else "SALAH"
    teks = " ".join(f"{c}:{p:.3f}" for c, p in zip(kelas, prob))
    print(f"  sampel {i} | label {y_uji_t[i].item()} | {teks} | {status}")

# ---------- 5. Prediksi salah: keyakinan model ----------
idx_salah = (~benar_top1).nonzero(as_tuple=True)[0]
print(f"\njumlah prediksi salah: {len(idx_salah)}")
print("tiga prediksi salah pertama (label vs top-3):")
for i in idx_salah[:3].tolist():
    kelas = indeks_top3[i].tolist()
    prob = nilai_top3[i].tolist()
    teks = " ".join(f"{c}:{p:.3f}" for c, p in zip(kelas, prob))
    print(f"  sampel {i} | label {y_uji_t[i].item()} | {teks}")

# ---------- 6. Kalibrasi: akurasi per kelompok keyakinan ----------
keyakinan = nilai_top3[:, 0]
ambang = [0.00, 0.50, 0.75, 0.90, 1.01]
print("\nakurasi per kelompok keyakinan (top-1):")
print(f"  {'keyakinan':>14s} {'jumlah':>7s} {'akurasi':>8s}")
for lo, hi in zip(ambang[:-1], ambang[1:]):
    m = (keyakinan >= lo) & (keyakinan < hi)
    if m.sum() > 0:
        ak = benar_top1[m].float().mean().item()
        print(f"  [{lo:.2f}, {hi:.2f}) {m.sum().item():>7d} {ak:>8.3f}")

Output yang Dihasilkan:

text
sampel uji    : 450
akurasi top-1 : 0.969
akurasi top-3 : 1.000

top-3 prediksi untuk 5 sampel uji pertama:
  sampel 0 | label 1 | 1:0.844 8:0.052 5:0.043 | benar
  sampel 1 | label 0 | 0:1.000 9:0.000 2:0.000 | benar
  sampel 2 | label 9 | 9:0.996 0:0.002 5:0.001 | benar
  sampel 3 | label 1 | 1:0.957 2:0.024 3:0.012 | benar
  sampel 4 | label 5 | 5:0.985 9:0.014 6:0.001 | benar

jumlah prediksi salah: 14
tiga prediksi salah pertama (label vs top-3):
  sampel 25 | label 8 | 1:0.686 2:0.188 8:0.102
  sampel 62 | label 0 | 4:0.920 0:0.057 6:0.021
  sampel 146 | label 4 | 6:0.929 4:0.070 5:0.000

akurasi per kelompok keyakinan (top-1):
       keyakinan  jumlah  akurasi
  [0.00, 0.50)       7    0.714
  [0.50, 0.75)      19    0.737
  [0.75, 0.90)      40    0.950
  [0.90, 1.01)     384    0.987

Mari kita telaah kode di atas baris per baris:

  1. Baris 16 (torch.manual_seed(42)): mengunci inisialisasi bobot CNN sehingga keluaran identik pada setiap eksekusi — kebiasaan dari chapter Reproducibility .
  2. Baris 19-28: load_digits() memuat 1797 citra 8x8; piksel diskalakan ke 0-1; train_test_split memisahkan 25% data uji; normalisasi memakai mu, sigma dari data latih saja, sesuai kontrak tahap pipeline kedua.
  3. Baris 31-37: ke_gambar menambah dimensi channel menjadi format NCHW (N, 1, 8, 8) — bentuk masukan Conv2d dari chapter Representasi Gambar .
  4. Baris 40-63: CNN mini dua blok konvolusi ditutup head fully connected, dilatih 10 epoch dengan mini-batch 32 — cetakan lengkap dari chapter Arsitektur CNN ; bagian ini bukan fokus chapter dan hanya menyiapkan model terlatih.
  5. Baris 66-73 (inferensi_topk): inti pipeline inferensi — model.eval() mematikan perilaku training, torch.no_grad() menghentikan pelacakan gradien agar hemat memori, F.softmax(logits, dim=1) mengubah logits menjadi probabilitas per baris, lalu probs.topk(k, dim=1) mengambil k probabilitas tertinggi beserta indeks kelasnya.
  6. Baris 78-82: akurasi top-1 membandingkan kelas peringkat pertama dengan label; akurasi top-3 memakai .any(dim=1) — benar bila label asli muncul di salah satu dari tiga kandidat teratas.
  7. Baris 85-91: lima sampel pertama dicetak sebagai kelas:probabilitas — pola 1:0.844 8:0.052 5:0.043 berarti kelas 1 unggul dengan margin jelas.
  8. Baris 94-101: (~benar_top1).nonzero() mengumpulkan indeks seluruh prediksi salah, lalu tiga yang pertama dicetak lengkap dengan top-3-nya.
  9. Baris 103-112: tabel kalibrasi sederhana — prediksi dikelompokkan berdasarkan skor keyakinan, lalu akurasi dihitung per kelompok; kalau model terkalibrasi baik, akurasi kelompok mendekati batas bawah kelompoknya.

Membaca Hasil: Saat Model Salah dengan Percaya Diri #

Tiga temuan menonjol dari keluaran. Pertama, akurasi top-1 0.969 berbanding top-3 1.000 — seluruh kesalahan model masih menempatkan label benar di tiga kandidat teratas, pola khas kesalahan antar digit yang bentuknya mirip. Kedua, sampel 62 memperlihatkan masalah kalibrasi: label aslinya 0, model memilih kelas 4 dengan keyakinan 0.920. Model tidak ragu-ragu; model salah dengan yakin. Ketiga, tabel keyakinan menunjukkan arah yang benar namun tidak sempurna: kelompok keyakinan di atas 0.90 memang berakurasi 0.987, tetapi kelompok 0.50-0.75 hanya 0.737 padahal batas bawah kelompoknya menjanjikan setengah benar — selisih antara janji keyakinan dan akurasi nyata inilah yang disebut miskalibrasi.

Skor mentah, softmax, dan operasi top-k yang kita pakai di sini terdokumentasi resmi di docs.pytorch.org/docs/stable/generated/torch.nn.functional.softmax.html . Model yang sudah terkalibrasi pada data terkumpul pun tetap rentan saat data baru berbeda sebarannya — dan ketahanan itulah yang diperbaiki lewat perbanyakan data latih pada chapter berikutnya: Image Augmentation .