Confusion matrix memberi tahu di mana model keliru; kurva learning memberi tahu kapan kecenderungan itu terbentuk. Analisis kesalahan menjawab pertanyaan yang lebih dalam: mengapa model keliru pada sampel tertentu. Jawabannya tidak pernah muncul dari angka rangkuman — praktisi harus membuka data, memeriksa sampel yang salah satu per satu, mencari pola, lalu mengubah sesuatu yang terukur. Kebiasaan sederhana ini membedakan perbaikan yang tepat sasaran dari eksperimen coba-coba yang tidak terarah. Chapter ini menjalankan siklusnya secara utuh pada model pengenal angka tulisan tangan yang nyata keliru, dari pengumpulan sampel sampai hipotesis yang bisa ditindaklanjuti.

Siklus Analisis Kesalahan #

Analisis kesalahan bukan prosedur sekali jalan, melainkan siklus. Setiap tindakan yang kita ambil mengubah model, sehingga pola kesalahan lama bisa hilang dan pola baru muncul. Diagram berikut memetakan siklusnya:

Setiap langkah pada siklus di atas punya alat dan keluaran yang jelas. Tabel berikut merangkum kelimanya — pola yang sama akan kita jalankan pada eksperimen di bawah:

LangkahAlatKeluaran
Kumpulkanmask pred != y_ujidaftar sampel yang salah
Kelompokkanconfusion matrix, urutkan sel luar diagonalpasangan kelas tertukar teratas
Amati polarender citra, statistik fiturdeskripsi pola dan fitur pembeda
Hipotesispenalaran domaindugaan penyebab yang dapat diuji
Tindakandata, fitur, ambang keputusanperubahan yang terukur

Eksperimen: Model Digits yang Keliru #

Berkas analisis_kesalahan.py melatih logistic regression pada 30% dataset digits — 1.797 citra angka tulisan tangan resolusi 8×8 piksel dari scikit-learn — lalu menjalankan seluruh siklus: mengumpulkan 50 prediksi salah, mengurutkan pasangan kelas tertukar, merender contoh yang salah sebagai peta karakter, mengukur statistik fiturnya, dan mengiris performa per subset.

python
"""Analisis kesalahan: prosedur sistematis pada model digits.

Alur: latih logistic regression pada 30% dataset digits (citra 8x8) ->
kumpulkan seluruh prediksi salah -> kelompokkan lewat pasangan kelas
tertukar dari confusion matrix -> amati pola lewat render ASCII dan
statistik fitur (tinta, asimetri kiri-kanan) -> iris performa per kelas
dan per keyakinan model -> rumuskan hipotesis dan tindakan.
"""

import numpy as np
from sklearn.datasets import load_digits
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, confusion_matrix
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

# ---------- 1. Model kecil pada dataset digits ----------
digits = load_digits()
X, y = digits.data, digits.target
X_latih, X_uji, y_latih, y_uji = train_test_split(
    X, y, train_size=0.3, random_state=42, stratify=y
)
skala = StandardScaler().fit(X_latih)
model = LogisticRegression(max_iter=3000)
model.fit(skala.transform(X_latih), y_latih)
pred = model.predict(skala.transform(X_uji))
keyakinan = model.predict_proba(skala.transform(X_uji)).max(axis=1)

salah = pred != y_uji
print(f"model: LogisticRegression | latih {len(y_latih)} | uji {len(y_uji)} sampel")
print(f"akurasi: {accuracy_score(y_uji, pred):.3f} | "
      f"prediksi salah: {int(salah.sum())} sampel")

# ---------- 2. Kelompokkan: pasangan kelas tertukar ----------
cm = confusion_matrix(y_uji, pred)
pasangan = sorted(
    ((int(cm[i, j]), i, j) for i in range(10) for j in range(10) if i != j),
    reverse=True,
)
print("\npasangan tertukar teratas (aktual -> prediksi : jumlah):")
for jumlah, i, j in pasangan[:5]:
    print(f"  {i} -> {j} : {jumlah} sampel")
tiga_atas = sum(jumlah for jumlah, _, _ in pasangan[:3])
print(f"3 pasangan teratas menyerap {tiga_atas} dari {int(salah.sum())} kesalahan")

# ---------- 3. Amati: render ASCII contoh yang salah ----------
CHARS = " .:-=+*#@@"


def render(citra):
    return "\n".join(
        "".join(CHARS[min(int(v * 10 / 17), 9)] for v in baris)
        for baris in citra.reshape(8, 8)
    )


indeks_salah = np.where(salah)[0]
aktual_teratas, prediksi_teratas = pasangan[0][1], pasangan[0][2]
contoh = indeks_salah[
    (y_uji[indeks_salah] == aktual_teratas)
    & (pred[indeks_salah] == prediksi_teratas)
]
print(f"\ncontoh salah untuk pasangan teratas "
      f"{aktual_teratas} -> {prediksi_teratas}:")
for k in contoh[:2]:
    print(f"indeks uji {k} | aktual {y_uji[k]} | diprediksi {pred[k]} | "
          f"keyakinan {keyakinan[k]:.2f}")
    print(render(X_uji[k]))

# ---------- 4. Statistik fitur: cari pembeda ----------
kolom = X_uji.reshape(-1, 8, 8).sum(axis=1)  # tinta per kolom citra
tinta = X_uji.sum(axis=1)  # total intensitas piksel
asimetri = np.abs(kolom[:, :4].sum(axis=1) - kolom[:, 4:].sum(axis=1))
puncak = kolom.max(axis=1)  # kolom paling padat

kelompok = {
    "8 benar": (y_uji == 8) & ~salah,
    "8 salah": (y_uji == 8) & salah,
    "1 benar": (y_uji == 1) & ~salah,
}
print("\nstatistik fitur tiap kelompok:")
print(f"{'kelompok':10s} | {'n':>3s} | {'tinta':>6s} | {'asimetri':>8s} | {'puncak':>6s}")
for nama, m in kelompok.items():
    print(f"{nama:10s} | {int(m.sum()):3d} | {tinta[m].mean():6.1f} | "
          f"{asimetri[m].mean():8.1f} | {puncak[m].mean():6.1f}")

# ---------- 5. Iris performa: per kelas dan per keyakinan ----------
print("\nrecall per kelas (irisan pertama):")
for k in range(10):
    m = y_uji == k
    benar_k = int((pred[m] == k).sum())
    print(f"  kelas {k}: {benar_k:3d}/{int(m.sum()):3d} benar "
          f"(recall {accuracy_score(y_uji[m], pred[m]):.3f})")

print("\niris keyakinan model (irisan kedua):")
for ambang in (0.9, 0.99):
    m = keyakinan >= ambang
    print(f"  keyakinan >= {ambang:.2f}: {int(m.sum()):4d} sampel, "
          f"akurasi {accuracy_score(y_uji[m], pred[m]):.3f} | "
          f"di bawah ambang: {int((~m).sum()):4d} sampel, "
          f"akurasi {accuracy_score(y_uji[~m], pred[~m]):.3f}")

# ---------- 6. Simpulan: hipotesis dan tindakan ----------
print("\nringkasan temuan:")
print("1. Pasangan 8 -> 1 teratas; recall kelas 8 paling rendah.")
print("2. Delapan yang salah memiliki asimetri kiri-kanan menyerupai angka 1:")
print("   satu lengkungan tidak terbaca pada resolusi 8x8 piksel.")
print("3. Kesalahan berkumpul pada prediksi berkeyakinan rendah, jadi sampel")
print("   dengan keyakinan < 0.9 layak diteruskan ke pemeriksaan manusia.")

Skrip di atas menghasilkan keluaran berikut saat dijalankan di mesin uji.

Output yang Dihasilkan:

text
model: LogisticRegression | latih 539 | uji 1258 sampel
akurasi: 0.960 | prediksi salah: 50 sampel

pasangan tertukar teratas (aktual -> prediksi : jumlah):
  8 -> 1 : 6 sampel
  8 -> 3 : 4 sampel
  3 -> 8 : 4 sampel
  9 -> 7 : 3 sampel
  8 -> 5 : 3 sampel
3 pasangan teratas menyerap 14 dari 50 kesalahan

contoh salah untuk pasangan teratas 8 -> 1:
indeks uji 116 | aktual 8 | diprediksi 1 | keyakinan 0.47
  :@@:  
  =@@@  
  :@@+  
   @@   
   @#   
  *@@:  
  ##=@  
  :@@@. 
indeks uji 221 | aktual 8 | diprediksi 1 | keyakinan 0.46
  .==   
  -@@.  
 -@*@:  
 :@+@   
 .@@#   
  .@#+. 
  :@ #@ 
   #+=  

statistik fitur tiap kelompok:
kelompok   |   n |  tinta | asimetri | puncak
8 benar    | 108 |  330.8 |     32.1 |   95.6
8 salah    |  14 |  326.0 |     60.7 |  103.6
1 benar    | 123 |  311.4 |     69.2 |  118.2

recall per kelas (irisan pertama):
  kelas 0: 125/125 benar (recall 1.000)
  kelas 1: 123/127 benar (recall 0.969)
  kelas 2: 122/124 benar (recall 0.984)
  kelas 3: 120/128 benar (recall 0.938)
  kelas 4: 120/127 benar (recall 0.945)
  kelas 5: 124/127 benar (recall 0.976)
  kelas 6: 125/127 benar (recall 0.984)
  kelas 7: 124/125 benar (recall 0.992)
  kelas 8: 108/122 benar (recall 0.885)
  kelas 9: 117/126 benar (recall 0.929)

iris keyakinan model (irisan kedua):
  keyakinan >= 0.90: 1006 sampel, akurasi 0.995 | di bawah ambang:  252 sampel, akurasi 0.821
  keyakinan >= 0.99:  601 sampel, akurasi 1.000 | di bawah ambang:  657 sampel, akurasi 0.924

ringkasan temuan:
1. Pasangan 8 -> 1 teratas; recall kelas 8 paling rendah.
2. Delapan yang salah memiliki asimetri kiri-kanan menyerupai angka 1:
   satu lengkungan tidak terbaca pada resolusi 8x8 piksel.
3. Kesalahan berkumpul pada prediksi berkeyakinan rendah, jadi sampel
   dengan keyakinan < 0.9 layak diteruskan ke pemeriksaan manusia.

Mari kita telaah kode di atas baris per baris:

  1. Baris 18-27 (model): load_digits memuat 1.797 citra 8×8; pembagian latih-uji memakai stratify=y agar tiap kelas terwakili sebanding; StandardScaler dipasang hanya pada data latih; predict_proba(...).max(axis=1) menyimpan keyakinan model pada prediksinya.
  2. Baris 29-32 (kumpulkan): mask salah memilih seluruh prediksi yang tidak cocok dengan label — 50 dari 1.258 sampel uji.
  3. Baris 35-44 (kelompokkan): sel luar diagonal confusion matrix diurutkan menurun; tiga pasangan teratas menyerap 14 dari 50 kesalahan, cukup untuk dijadikan sasaran perbaikan pertama.
  4. Baris 47-68 (amati): string CHARS memetakan intensitas piksel 0-16 ke sepuluh karakter dari spasi hingga @; dua contoh salah dari pasangan teratas dirender lengkap dengan keyakinan model.
  5. Baris 71-85 (statistik fitur): tiga kandidat fitur dihitung untuk semua sampel uji — total tinta, asimetri tinta kiri-kanan, dan puncak kepadatan kolom — lalu dibandingkan antara kelompok “8 benar”, “8 salah”, dan “1 benar”.
  6. Baris 88-101 (iris): performa diiris dua cara — recall per kelas, lalu akurasi di atas dan di bawah ambang keyakinan 0,9 dan 0,99.
  7. Baris 104-109 (simpulan): tiga temuan utama dirangkum sebagai bahan hipotesis dan tindakan.

Membaca Temuan: Dari Angka ke Dugaan #

Pasangan tertukar teratas adalah 8 → 1 dengan 6 sampel, disusul 8 → 3, 3 → 8, dan 9 → 7. Pada dataset MNIST yang lebih besar, pasangan klasik yang terkenal sulit adalah 1 ↔ 7 dan 4 ↔ 9; data 8×8 kita punya struktur kesalahannya sendiri. Pelajaran pertamanya justru di situ: jangan menebak pola kesalahan dari ingatan atau literatur — selalu baca confusion matrix milik data Anda sendiri.

Render ASCII memperlihatkan polanya. Kedua contoh angka 8 yang diprediksi 1 sama-sama ditulis ramping dan tegak: tinta menumpuk di kolom tengah, dan salah satu lengkungannya nyaris tak terlihat pada resolusi 8 piksel. Statistik fitur mengonfirmasi pengamatan visual itu dengan angka — total tinta tidak membedakan (330,8 vs 326,0), tetapi asimetri kiri-kanan memisahkan tegas: 32,1 untuk delapan yang benar, 60,7 untuk delapan yang salah, dan 69,2 untuk angka 1 yang benar. Delapan yang salah secara ukuran bentuk memang telah jatuh ke wilayah angka 1.

Hipotesisnya: lengkungan tertutup yang tidak terbaca pada resolusi rendah. Hipotesis ini sekaligus menjelaskan pasangan lain — 8 → 3 dan 8 → 5 adalah tetangga lengkung, sedangkan 9 → 7 dan 4 → 7 terjadi karena garis horizontal di bagian atas terbaca sebagai palang angka 7. Tindakan yang mengikuti: tambah data latih untuk bentuk ramping, naikkan resolusi fitur, atau gabungkan fitur bentuk yang menghitung lubang tertutup.

Mengiris Performa per Subset #

Irisan pertama — recall per kelas — menempatkan kelas 8 paling bawah (0,885), disusul kelas 9 (0,929), kelas 3 (0,938), dan kelas 4 (0,945): empat kelas terlemah semuanya adalah kelas yang bentuknya bergantung pada lengkungan tertutup. Irisan kedua memakai keyakinan model: 1.006 sampel dengan keyakinan ≥ 0,9 mencapai akurasi 0,995, sedangkan 252 sampel di bawah ambang itu hanya 0,821. Artinya, dua puluh persen sampel paling tidak yakin menampung hampir seluruh kesalahan.

Temuan irisan kedua langsung bisa ditindaklanjuti tanpa menyentuh model: sampel dengan keyakinan di bawah 0,9 diteruskan ke pemeriksaan manusia. Dokumentasi metrik klasifikasi dan fungsi load_digits tersedia di scikit-learn.org/stable/modules/model_evaluation.html dan halaman load_digits .

Ketika kesalahan sudah dikelompokkan dan diiris, langkah lanjutnya adalah memastikan mekanisme di baliknya: apakah gradien, data, atau arsitektur yang menjadi akarnya. Chapter berikutnya, Debugging Gradient , membuka kotak hitam tersebut lewat norm gradien per layer.