Akurasi global merangkum seluruh prediksi menjadi satu angka, dan satu angka itu dapat menyembunyikan kegagalan besar pada kelas tertentu. Topik ini melengkapi akurasi dengan dua alat pemeriksaan, yaitu confusion matrix sebagai tabel persilangan antara label sebenarnya dan prediksi model, serta akurasi per kelas yang dihitung darinya. Seluruh perhitungan menggunakan operasi torch murni pada data sintetis yang sengaja dibuat tidak seimbang, sehingga kelemahan akurasi terlihat langsung pada keluaran.
Mengapa akurasi global menyesatkan#
Ketika jumlah sampel antara kelas tidak seimbang, akurasi global didominasi oleh kelas dengan anggota terbanyak. Hitungannya sederhana, misalnya 1000 sampel dengan 900 sampel kelas 0: model yang selalu menebak kelas 0 tanpa memandang masukan memperoleh akurasi 90 persen, meskipun kemampuannya nol untuk dua kelas lainnya.
Ketidakseimbangan semacam ini bukan keadaan langka. Deteksi transaksi tidak wajar, pemeriksaan cacat produk, dan pencarian dokumen langka semuanya memuat kelas minoritas yang justru paling penting dikenali. Pada data tersebut, model yang mengejar akurasi global cenderung mengabaikan kelas minoritas, karena menebak kelas mayoritas saja sudah memberi angka yang tampak tinggi. Karena itu, keputusan penerimaan model pada data tidak seimbang tidak boleh bertumpu pada satu angka akurasi global.
Confusion matrix dengan operasi torch murni#
Confusion matrix berukuran (K, K) untuk K kelas, dengan baris menyatakan label sebenarnya dan kolom menyatakan prediksi model. Sel diagonal berarti prediksi benar, sedangkan sel di luar diagonal berarti prediksi keliru beserta arah kekeliruannya, yaitu kelas asli yang diprediksi sebagai kelas lain. Matriks disimpan sebagai tensor int64 berisi penghitungan, dan setiap pasangan sampel menambah satu pada sel dengan indeks [label, pred].
Orientasi baris dan kolom tersebut digambarkan sebagai berikut:
Skrip berikut melatih model mini pada data 1000 sampel dengan komposisi 900, 90, dan 10, lalu mencetak confusion matrix dalam bentuk tabel beserta akurasi global dan akurasi per kelas:
"""Confusion matrix dan akurasi per kelas dengan operasi torch murni."""
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset
torch.manual_seed(0)
# data sintetis tiga kelas yang sengaja tidak seimbang
jumlah = {0: 900, 1: 90, 2: 10}
pusat = torch.tensor([[0.0, 0.0], [2.8, 2.8], [-2.8, 2.8]])
fitur = []
label = []
for k, n in jumlah.items():
fitur.append(pusat[k] + 0.8 * torch.randn(n, 2))
label.append(torch.full((n,), k))
fitur = torch.cat(fitur)
label = torch.cat(label)
dataset = TensorDataset(fitur, label)
loader_latih = DataLoader(dataset, batch_size=32, shuffle=True,
generator=torch.Generator().manual_seed(0))
loader_eval = DataLoader(dataset, batch_size=100, shuffle=False)
model = nn.Sequential(nn.Linear(2, 16), nn.ReLU(), nn.Linear(16, 3))
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
criterion = nn.CrossEntropyLoss()
# pelatihan singkat, 5 epoch
model.train()
for epoch in range(5):
for xb, yb in loader_latih:
optimizer.zero_grad()
loss = criterion(model(xb), yb)
loss.backward()
optimizer.step()
# kumpulkan prediksi dalam mode evaluasi
model.eval()
pred_semua = []
label_semua = []
with torch.no_grad():
for xb, yb in loader_eval:
pred_semua.append(model(xb).argmax(dim=1))
label_semua.append(yb)
pred = torch.cat(pred_semua)
label = torch.cat(label_semua)
# confusion matrix: baris = label sebenarnya, kolom = prediksi
K = 3
cm = torch.zeros(K, K, dtype=torch.int64)
for l, p in zip(label.tolist(), pred.tolist()):
cm[l, p] += 1
akurasi_global = cm.diagonal().sum().item() / cm.sum().item()
akurasi_kelas = cm.diagonal().float() / cm.sum(dim=1).float()
# cetak confusion matrix sebagai tabel rapi
print("confusion matrix (baris = label sebenarnya, kolom = prediksi)")
print(" " + " ".join(f"{'pred ' + str(j):>7}" for j in range(K)))
for i in range(K):
sel = " ".join(f"{cm[i, j].item():7d}" for j in range(K))
print(f"label {i} {sel}")
print()
print(f"akurasi global : {akurasi_global:.4f}")
for i in range(K):
print(f"akurasi kelas {i} : {akurasi_kelas[i].item():.4f}")confusion matrix (baris = label sebenarnya, kolom = prediksi)
pred 0 pred 1 pred 2
label 0 896 4 0
label 1 2 88 0
label 2 10 0 0
akurasi global : 0.9840
akurasi kelas 0 : 0.9956
akurasi kelas 1 : 0.9778
akurasi kelas 2 : 0.0000Keluarannya memperlihatkan masalah yang tidak tampak pada akurasi global 0.9840. Baris label 2 menunjukkan seluruh 10 sampel kelas 2 diprediksi sebagai kelas 0, sehingga akurasi kelas 2 bernilai 0.0000, sedangkan kolom pred 2 kosong sama sekali, yang berarti model tidak pernah menebak kelas 2. Angka global tetap tinggi karena 90 persen data adalah kelas 0, persis seperti model pembanding yang selalu menebak kelas mayoritas.
Akurasi per kelas dihitung dari matriks dengan rumus berikut, dengan menyatakan confusion matrix dan menyatakan indeks kelas:
Pembilang rumus tersebut adalah sel diagonal kelas , sedangkan penyebutnya jumlah seluruh baris , yaitu banyaknya sampel yang benar-benar berasal dari kelas tersebut.
Jebakan indeks duplikat pada pengisian matriks#
Baris cm[label, pred] += 1 tampak seperti cara ringkas untuk mengisi matriks sekaligus, tetapi baris itu diam-diam menghasilkan hitungan yang keliru ketika ada pasangan indeks yang berulang. Python mengeksekusi pernyataan tersebut sebagai operasi baca, tambah satu, lalu tulis kembali, sehingga semua kemunculan pasangan yang sama membaca nilai lama yang identik sebelum ada penulisan, dan sel yang bersangkutan hanya bertambah satu. Skrip kecil berikut membandingkan kedua cara pada lima sampel yang tiga di antaranya berpasangan sama:
"""Perbandingan cm[label, pred] += 1 dengan iterasi pasangan indeks."""
import torch
label = torch.tensor([0, 0, 0, 1, 2])
pred = torch.tensor([1, 1, 1, 1, 0])
K = 3
# cara yang tampak ringkas, tetapi salah untuk indeks berulang
cm_salah = torch.zeros(K, K, dtype=torch.int64)
cm_salah[label, pred] += 1
# cara yang benar: iterasi pasangan label dan prediksi
cm_benar = torch.zeros(K, K, dtype=torch.int64)
for l, p in zip(label.tolist(), pred.tolist()):
cm_benar[l, p] += 1
print("label :", label.tolist())
print("pred :", pred.tolist())
print("cm_salah:")
print(cm_salah)
print("cm_benar:")
print(cm_benar)label : [0, 0, 0, 1, 2]
pred : [1, 1, 1, 1, 0]
cm_salah:
tensor([[0, 1, 0],
[0, 1, 0],
[1, 0, 0]])
cm_benar:
tensor([[0, 3, 0],
[0, 1, 0],
[1, 0, 0]])Tiga sampel dengan pasangan (0, 1) seharusnya mengisi sel tersebut dengan angka tiga, tetapi cara berindeks hanya meninggalkan angka satu. Cara yang benar adalah mengiterasi pasangan label dan prediksi satu per satu, sebagaimana digunakan pada skrip utama. Alternatif lain yang tetap berbasis torch adalah torch.bincount(label * K + pred, minlength=K * K).reshape(K, K), yaitu menghitung kemunculan setiap pasangan pada satu vektor datar sebelum dibentuk ulang menjadi matriks.
Kesalahan umum#
Kesalahan pertama adalah menukar orientasi, misalnya menulis cm[pred, label]. Matriks yang terbalik tetap memiliki diagonal yang sama, sehingga akurasi global tidak mendeteksi kekeliruannya, sedangkan seluruh akurasi per kelas dan arah kekeliruan menjadi salah. Perbaikannya adalah menetapkan satu konvensi sejak awal, yaitu baris untuk label sebenarnya dan kolom untuk prediksi, lalu menggunakannya secara konsisten.
Kesalahan kedua adalah menyimpan matriks dengan tipe float. Penghitungan seharusnya bulat, dan tipe pecahan hanya memperbesar risiko ketidaktepatan serta memboroskan memori. Perbaikannya adalah membuat matriks dengan dtype=torch.int64, lalu memanggil .float() tepat sebelum pembagian akurasi per kelas.
Kesalahan ketiga adalah membagi tanpa memeriksa baris kosong. Kelas yang tidak muncul sama sekali pada data evaluasi menghasilkan penyebut nol, dan pembagian itu menghasilkan nan yang merusak tabel laporan. Perbaikannya adalah memeriksa jumlah baris terlebih dahulu, lalu melaporkan kelas tanpa sampel sebagai data yang belum terukur, bukan sebagai akurasi nol.
Ringkasan#
- Confusion matrix berukuran
(K, K)bertipeint64menghitung pasangan label sebenarnya sebagai baris dan prediksi sebagai kolom. - Akurasi per kelas adalah sel diagonal dibagi jumlah barisnya, dan nilainya membongkar kegagalan yang disembunyikan akurasi global.
cm[label, pred] += 1tidak aman untuk indeks berulang, sehingga pengisian menggunakan iterasi pasangan atautorch.bincount.- Akurasi per kelas identik dengan recall kelas yang bersangkutan.
Referensi#
- PyTorch documentation — PyTorch
— Rujukan API resmi untuk
torch.zeros,Tensor.diagonal, dantorch.bincount. - Learn the Basics — PyTorch — Pengantar resmi yang mencakup evaluasi model dan penghitungan akurasi.