Sebelum Bisa Baca Kalimat, Model Harus Bisa Kenal Satu Huruf Dulu

Daftar Isi
Konteks: Kenapa Tulisan Ini Ada
Di tulisan sebelumnya soal Text Recognition dengan CRNN + CTC, saya bahas gimana model bisa "membaca" deretan karakter sebagai satu urutan utuh. Tapi ada satu tugas yang lebih dasar yang sering jadi fondasi sebelum sampai ke situ: mengenali satu karakter tunggal.
Bayangin gini — sebelum sebuah sistem bisa baca kata "HALO" sebagai satu rangkaian, dia (atau bagian dari pipeline-nya) perlu bisa jawab pertanyaan yang lebih sederhana dulu: "ini gambar huruf apa?" Nah, tulisan ini eksplorasi saya soal itu, dengan dataset dan arsitektur yang beda dari CRNN+CTC.
Coba Tebak Dulu
Kalau kamu kasih model 370 ribu-an gambar huruf tulisan tangan A sampai Z, tapi jumlah contoh tiap huruf nggak sama rata — misalnya huruf "O" punya puluhan ribu contoh sementara huruf "F" cuma seribuan — menurut kamu, apa yang bakal terjadi ke akurasi model buat masing-masing huruf?
Spoiler: ini beneran jadi perhatian di eksperimen ini. Tapi beda dari biasanya, kali ini saya nggak cuma nebak-nebak — saya ukur beneran, dan hasilnya ada di bagian bawah.
Bedanya dengan Text Recognition (CRNN + CTC)
Supaya jelas beda tugasnya, ini perbandingannya:
| Text Recognition (CRNN + CTC) | Handwritten Character Recognition (tulisan ini) | |
|---|---|---|
| Input | Gambar berisi urutan karakter | Gambar berisi satu karakter |
| Output | Rangkaian teks | Satu label huruf (A–Z) |
| Arsitektur | CNN + BiLSTM + CTC Loss | CNN classifier + softmax |
| Perlu urutan/alignment? | Ya | Tidak |
Nggak ada LSTM, nggak ada CTC loss di sini — murni klasifikasi gambar biasa, tapi jadi pelengkap yang bagus buat ngerti kenapa text recognition "beneran" butuh komponen tambahan itu.
Dataset: A-Z Handwritten Alphabets
Saya pakai dataset A-Z Handwritten Alphabets dari Kaggle — sekitar 370 ribu gambar grayscale 28×28 huruf kapital tulisan tangan, diunduh lewat kagglehub.
Pas saya plot distribusi kelasnya, ketahuan jelas nggak seimbang: huruf O dan S masing-masing punya puluhan ribu sampel, sementara F dan I cuma sekitar 1.000-an. Ini bukan hal langka di dataset dunia nyata, tapi penting buat diinget sebelum baca angka akurasi di bawah.
Arsitektur dan Setup Training
CNN kecil: 3 blok konvolusi (Conv2D + MaxPool) yang masuk ke dense layer dengan Dropout(0.3) di antaranya, berakhir di softmax 26-kelas (satu untuk tiap huruf). Di-compile dengan Adam (lr=1e-3) dan categorical cross-entropy.
Tiga hal yang saya tambahkan supaya training-nya lebih terkontrol, bukan cuma "jalanin epoch sebanyak-banyaknya":
Class weighting (
compute_class_weightdari sklearn) — kesalahan di huruf yang jarang muncul dihitung lebih berat saat trainingEarlyStopping(pantauval_accuracy, kembalikan bobot terbaik) danReduceLROnPlateau(turunkan learning rate kalauval_lossmandek) — training berhenti/menyesuaikan diri sendiri, bukan asal jalan sampai epoch habistrain_test_splitdenganstratify=y— supaya proporsi tiap huruf di data train dan test tetap terjaga
Hasil Training
Angka di bawah ini dari run Colab yang benar-benar saya jalankan, 20 epoch dengan setup di atas:
| Metrik | Nilai |
|---|---|
| Validation accuracy terbaik | 99.10% (epoch 20) |
| Validation loss (epoch sama) | 0.0391 |
| Training accuracy (epoch sama) | 99.18% |
| Epoch dilatih | 20 (EarlyStopping tidak terpicu — akurasi masih terus naik sampai akhir) |
Kurva training-nya rapi: akurasi train dan validation naik bareng tanpa ada tanda overfitting parah, dan learning rate otomatis turun dua kali lewat ReduceLROnPlateau waktu val_loss mulai mandek.
Evaluasi Per-Kelas: Beneran Dibuktikan, Bukan Diasumsikan
Ini bagian yang paling saya pedulikan. Nggak cukup cuma bilang "sudah dikasih class weighting" — saya cek betulan lewat classification_report per huruf:
| Huruf | Jumlah sampel | Precision | Recall | F1 |
|---|---|---|---|---|
F (minoritas) |
233 | 0.983 | 0.991 | 0.987 |
I (minoritas) |
224 | 0.978 | 0.991 | 0.984 |
D (precision terendah) |
2.027 | 0.916 | 0.990 | 0.951 |
O (mayoritas) |
11.565 | 0.998 | 0.981 | 0.990 |
S (mayoritas) |
9.684 | 0.999 | 0.994 | 0.996 |
Huruf F dan I — dua yang paling sedikit sampelnya — ternyata F1-nya ada di rentang yang mirip dengan O dan S yang sampelnya puluhan ribu. Macro-average F1 (0.989) dan weighted-average F1 (0.991) juga berdekatan, artinya angka akurasi keseluruhan nggak "ditarik naik" cuma oleh kelas mayoritas doang.
Tapi ada satu temuan yang nggak saya duga: huruf D — yang sampelnya nggak sedikit (2.027) — justru punya precision paling rendah di seluruh laporan (0.916), lebih rendah dari F maupun I yang notabene minoritas. Pas saya cek confusion matrix-nya, ternyata sejumlah sampel O (kelas paling besar) salah diprediksi jadi D. Jadi ini bukan soal D kurang data — ini murni soal bentuk O dan D yang mirip di mata model, dan itu nggak ada hubungannya sama class weighting.
Plot Twist: Dari Baseline yang Belum Tervalidasi ke Angka yang Terbukti
Versi pertama tulisan ini jujur saya tutup dengan satu catatan yang belum terjawab: saya nggak punya breakdown akurasi per huruf buat mastiin dataset yang timpang itu beneran ngaruh atau nggak ke huruf-huruf minoritas.
Saya nggak puas berhenti di situ. Jadi saya balik lagi, tambahkan class weighting, EarlyStopping, ReduceLROnPlateau, dan yang paling penting — evaluasi per-kelas yang eksplisit. Hasilnya bukan cuma angka validation accuracy yang tinggi, tapi ada bukti konkret bahwa huruf F dan I yang tadinya saya khawatirkan performanya rendah, ternyata di run ini F1-nya sebanding dengan huruf yang sampelnya paling banyak.
Satu hal yang tetap saya jaga jujur: test set-nya masih dari distribusi dataset yang sama dengan data training (sumber dan proses pengumpulan sama). Ini belum menguji gimana model ini menangani gaya tulisan tangan atau hasil scan yang beda jauh dari dataset ini — itu butuh test set eksternal yang belum jadi bagian dari eksperimen ini.
Kuis Kecil
1. Kenapa ReduceLROnPlateau dipakai bersamaan dengan EarlyStopping? A. Supaya training berjalan lebih lambat B. ReduceLROnPlateau menurunkan learning rate saat progres mandek (biar model bisa "menyempurnakan" belajarnya), sementara EarlyStopping menghentikan training kalau memang sudah tidak ada perbaikan lagi C. Karena keduanya melakukan hal yang persis sama D. Supaya model jadi lebih besar
Jawaban: B. Keduanya saling melengkapi — satu menyesuaikan learning rate, satu lagi menghentikan training di waktu yang tepat.
2. Kenapa metrik per-kelas (classification report) lebih meyakinkan dibanding cuma akurasi keseluruhan buat mengecek dampak class weighting? A. Karena lebih cepat dihitung B. Karena akurasi keseluruhan bisa "ditutupi" performa bagus di kelas mayoritas, sementara classification report menunjukkan performa tiap kelas secara terpisah C. Karena classification report tidak butuh test set D. Karena hasilnya selalu 100%
Jawaban: B. Ini persis alasan kenapa evaluasi per-kelas ditambahkan di iterasi kedua eksperimen ini.
Ringkasan
[x] Klasifikasi karakter tunggal adalah tugas yang lebih sederhana dari text recognition berbasis urutan
[x] Nggak ada LSTM/CTC di sini — murni CNN classifier dengan softmax 26-kelas, ditambah Dropout untuk regularisasi
[x] Dataset A-Z Handwritten Alphabets (~370K gambar) diunduh dari Kaggle, kelasnya timpang (O/S puluhan ribu, F/I seribuan)
[x] Iterasi kedua: class weighting + EarlyStopping + ReduceLROnPlateau, 20 epoch, validation accuracy terbaik 99.10%
[x] Evaluasi per-kelas membuktikan huruf minoritas (F, I) F1-nya sebanding dengan huruf mayoritas (O, S) — bukan lagi asumsi
[x] Temuan baru: huruf
D(bukan minoritas) punya precision terendah karena sering ketuker samaO, bukan soal jumlah data[x] Keterbatasan yang masih ada: test set berasal dari distribusi dataset yang sama, belum diuji ke data eksternal
Kode lengkapnya ada di repo GitHub handwritten-character-recognition.






