Naive Bayes untuk Teks

Naive Bayes sebagai pengklasifikasi teks klasik yang cepat dan tangguh.

Daftar isi

Naive Bayes adalah keluarga model klasifikasi yang menerapkan teorema Bayes dengan satu penyederhanaan besar, yaitu setiap fitur dianggap saling bebas ketika kelasnya diketahui. Penyederhanaan itu terdengar terlalu berani untuk teks karena kata-kata dalam kalimat saling bergantung, tetapi pada praktiknya model ini tetap cepat, hemat memori, dan tangguh sebagai pengklasifikasi dokumen. Topik ini membahas rumusnya, peran penghalusan Laplace, serta pelatihan MultinomialNB pada korpus mini 30 dokumen berbahasa Indonesia melalui skrip klasifikasi-naive-bayes.py.

Teorema Bayes dengan asumsi independensi#

Model ini menghitung peluang setiap kelas bagi sebuah dokumen yang berisi fitur x1x_1 sampai xnx_n, lalu memilih kelas dengan peluang terbesar. Menghitung peluang bersama seluruh fitur secara langsung tidak mungkin pada teks karena jumlah kombinasinya meledak. Karena itu, asumsi naive diterapkan, yaitu setiap fitur dianggap bebas dari fitur lain apabila kelasnya sudah diketahui. Peluang kelas kemudian sebanding dengan hasil kali berikut.

P(y∣x1,…,xn)∝P(y)∏i=1nP(xi∣y) P(y \mid x_1, \ldots, x_n) \propto P(y) \prod_{i=1}^{n} P(x_i \mid y)

Varianta yang dipakai untuk teks adalah Multinomial Naive Bayes. Pada varianta ini, dokumen dipandang sebagai kantong hitungan kata sebagaimana skema bag-of-words, dan P(xi∣y)P(x_i \mid y) adalah peluang kata ke-ii muncul pada dokumen kelas yy. Semua peluang kata per kelas dirangkum dari frekuensi kemunculan pada data latih, sehingga pelatihan berupa penghitungan frekuensi saja dan selesai dalam satu kali lintasan data. Sifat itulah yang membuat model ini jauh lebih cepat daripada model yang memerlukan optimasi berulang.

Asumsi bebas yang menopang rumus itu jarang benar pada teks karena kata-kata saling menopang maknanya, misalnya “hujan” dan “gerimis” yang sering hadir bersamaan. Akibatnya, bukti yang saling berkaitan terhitung dua kali, sehingga probabilitas kelas yang menang cenderung melebihi nilai sebenarnya. Untuk tugas pemilihan kelas, peringkat antar kelas pada umumnya tetap andal, dan itulah yang dibutuhkan pada sebagian besar penggunaan pengklasifikasi dokumen.

Penghalusan Laplace#

Penghitungan frekuensi murni menyimpan satu jebakan. Apabila sebuah kata tidak pernah muncul pada dokumen kelas tertentu di data latih, peluang kata itu pada kelas tersebut bernilai nol. Satu peluang nol saja cukup menjadikan hasil kali seluruh peluang kata bernilai nol, sekalipun kata-kata lain menunjuk kelas tersebut dengan sangat kuat. Korpus kecil menimbulkan kejadian ini hampir di setiap kelas karena tidak mungkin semua kata teramati pada semua kelas.

Pemecahannya adalah penghalusan (smoothing) Laplace, yaitu menambahkan bilangan α\alpha ke setiap hitungan. scikit-learn menghitung peluang kata ii pada kelas yy dengan rumus berikut. Dalam rumus itu, NyiN_{yi} menyatakan hitungan kata tersebut pada kelas yy, NyN_y menyatakan total hitungan semua kata pada kelas yy, dan nn menyatakan banyaknya kata pada kosakata.

θ^yi=Nyi+αNy+αn \hat{\theta}_{yi} = \frac{N_{yi} + \alpha}{N_y + \alpha n}

Nilai bawaan α=1\alpha = 1 memberi satu hitungan tambahan untuk setiap pasangan kata dan kelas, sehingga tidak ada peluang yang menyentuh nol. Kata yang belum pernah terlihat tetap mendapat porsi kecil tanpa mengganggu dominasi kata-kata yang benar-benar sering muncul. Nilai α\alpha dapat dinaikkan apabila data latih sangat kecil dan diturunkan apabila data latih sangat besar. Pengaturannya termasuk hyperparameter yang sah untuk disetel pada data validasi.

Melatih MultinomialNB pada korpus mini#

Skrip berikut memakai korpus mini yang sama dengan topik vektorisasi teks, memecahnya secara berstratifikasi dengan data uji 30 persen, lalu melatih MultinomialNB di atas fitur TF-IDF. Setelah akurasi dihitung, skrip menampilkan kata paling pembeda tiap kelas menurut selisih log peluang, kemudian menghitung probabilitas tiga teks baru di luar korpus.

python klasifikasi-naive-bayes.py
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
from sklearn.naive_bayes import MultinomialNB

# korpus mini berbahasa Indonesia: tiga kelas, sepuluh dokumen per kelas
# kelas 1: pesan status perangkat
# kelas 2: catatan cuaca harian
# kelas 3: arsip catatan rapat
korpus = [
    "Sensor suhu prosesor membaca enam puluh dua derajat selama sepuluh menit terakhir.",
    "Perangkat melakukan booting ulang setelah pembaruan sistem selesai dipasang.",
    "Kapasitas baterai perangkat tersisa empat puluh persen pada pemantauan pagi ini.",
    "Sensor kelembapan tidak mengirim pembacaan selama lima menit dan kembali normal.",
    "Memori tersisa sedikit sehingga layanan pemantauan dihentikan sementara.",
    "Perangkat terputus dari jaringan lokal dan tersambung kembali secara otomatis.",
    "Log perangkat mencatat kesalahan baca pada satu sektor penyimpanan.",
    "Suhu rak meningkat sehingga kipas perangkat berputar lebih cepat.",
    "Perangkat masuk mode hemat daya di luar jam pemantauan.",
    "Pesan peringatan suhu tinggi muncul pada panel status perangkat.",
    "Hujan gerimis turun sejak pagi dan berhenti menjelang siang.",
    "Langit cerah tanpa awan sepanjang hari dengan angin tenang.",
    "Curah hujan bulan ini lebih rendah daripada rata-rata tahun sebelumnya.",
    "Awan tebal menggelayut pada sore hari dan gerimis turun sebentar.",
    "Kecepatan angin mencapai dua puluh kilometer per jam pada siang hari.",
    "Suhu udara pagi tercatat dua puluh satu derajat dengan udara lembap.",
    "Prakiraan malam ini berawan dengan peluang hujan ringan.",
    "Penguapan tinggi membuat udara siang terasa lebih panas dari catatan termometer.",
    "Kabut tipis menghilang setelah matahari terbit dan jarak pandang membaik.",
    "Catatan cuaca pekan ini menunjukkan tiga hari hujan dan empat hari cerah.",
    "Peserta rapat menyepakati jadwal peluncuran tahap uji pada bulan depan.",
    "Notulen rapat mencatat tiga keputusan dan dua tindak lanjut.",
    "Agenda rapat membahas hasil evaluasi dan pembagian tugas pekan berikutnya.",
    "Rapat koordinasi menghadirkan wakil dari setiap tim kerja.",
    "Keputusan rapat menyatakan proposal anggaran ditinjau ulang.",
    "Pimpinan rapat meminta ringkasan kemajuan sebelum pertemuan berikutnya.",
    "Catatan rapat menyebut kebutuhan perangkat uji tambahan untuk laboratorium.",
    "Semua usulan dipindahkan ke agenda rapat berikutnya karena waktu terbatas.",
    "Hasil rapat direkam dan diarsipkan pada pustaka dokumen internal.",
    "Rapat evaluasi bulanan menutup pembahasan dengan target pekan berikutnya.",
]
label = (["perangkat"] * 10) + (["cuaca"] * 10) + (["rapat"] * 10)

# pemecahan berstratifikasi: proporsi kelas tetap sama di dua himpunan
X_latih, X_uji, y_latih, y_uji = train_test_split(
    korpus, label, test_size=0.30, random_state=42, stratify=label
)

# vektorisasi di-fit pada data latih saja, data uji hanya di-transform
vektor = TfidfVectorizer()
Xt_latih = vektor.fit_transform(X_latih)
Xt_uji = vektor.transform(X_uji)

# pelatihan MultinomialNB dilakukan satu kali tanpa iterasi berulang
model = MultinomialNB().fit(Xt_latih, y_latih)
prediksi = model.predict(Xt_uji)
print(f"Data latih: {len(X_latih)} dokumen, data uji: {len(X_uji)} dokumen")
print(f"Akurasi data uji : {accuracy_score(y_uji, prediksi):.4f}")
print()

# kata paling pembeda tiap kelas menurut selisih log probabilitas
kosa = vektor.get_feature_names_out()
log_prob = model.feature_log_prob_
rerata = log_prob.mean(axis=0)
print("Kata paling pembeda tiap kelas:")
for ci, nama_kelas in enumerate(model.classes_):
    teratas = np.argsort(log_prob[ci] - rerata)[::-1][:4]
    print(f"  {nama_kelas:<10} " + ", ".join(kosa[i] for i in teratas))
print()

# probabilitas tiga teks baru di luar korpus
baru = [
    "Suhu prosesor naik dan kipas perangkat berputar cepat",
    "Gerimis turun sejak pagi dengan langit berawan",
    "Notulen rapat mencatat dua keputusan tindak lanjut",
]
Xt_baru = vektor.transform(baru)
prob = model.predict_proba(Xt_baru)
kelas = model.classes_
print("Probabilitas tiga teks baru:")
for teks, p in zip(baru, prob):
    print(f"Teks     : {teks}")
    for k, v in zip(kelas, p):
        print(f"  P({k:<10}) = {v:.4f}")
    print(f"Prediksi : {kelas[np.argmax(p)]}")
text Keluaran
Data latih: 21 dokumen, data uji: 9 dokumen
Akurasi data uji : 0.8889

Kata paling pembeda tiap kelas:
  cuaca      hari, udara, hujan, siang
  perangkat  perangkat, sehingga, pemantauan, kembali
  rapat      rapat, berikutnya, evaluasi, hasil

Probabilitas tiga teks baru:
Teks     : Suhu prosesor naik dan kipas perangkat berputar cepat
  P(cuaca     ) = 0.2441
  P(perangkat ) = 0.5171
  P(rapat     ) = 0.2389
Prediksi : perangkat
Teks     : Gerimis turun sejak pagi dengan langit berawan
  P(cuaca     ) = 0.5260
  P(perangkat ) = 0.2248
  P(rapat     ) = 0.2492
Prediksi : cuaca
Teks     : Notulen rapat mencatat dua keputusan tindak lanjut
  P(cuaca     ) = 0.2953
  P(perangkat ) = 0.2958
  P(rapat     ) = 0.4089
Prediksi : rapat

Akurasi 0.8889 pada data uji berarti delapan dari sembilan dokumen diprediksi benar, dan pemecahan berstratifikasi menjaga data uji tetap berisi tiga dokumen per kelas. Daftar kata pembeda memperlihatkan cara model “membaca”, yaitu murni dari hitungan kata. Kelas cuaca diwakili “hari”, “udara”, dan “hujan”, sedangkan kelas rapat diwakili “rapat” dan “berikutnya”. Satu-satunya kesalahan terjadi pada dokumen perangkat yang memuat kata “mencatat”. Pada korpus ini, kata tersebut hampir selalu hadir di catatan rapat, sehingga hitungan frekuensinya menarik dokumen itu ke kelas rapat. Kesalahan semacam ini wajar pada model yang tidak memahami urutan maupun makna kata.

Probabilitas tiga teks baru menunjukkan perilaku yang sehat. Setiap teks diprediksi masuk kelas yang tepat dengan probabilitas terbesar, tetapi pada teks ketiga, selisih antara kelas rapat 0.4089 dan perangkat 0.2958 hanya sekitar 0.11. Jarak yang tipis itu merupakan informasi yang berharga karena model sedang menyatakan keraguan, dan korpus berukuran 30 dokumen memang tidak cukup untuk keyakinan yang kokoh.

Kapan Naive Bayes menjadi pilihan awal#

Model ini menjadi pilihan awal ketika korpus berukuran kecil hingga menengah, sumber daya terbatas, dan kebutuhan utamanya adalah titik acuan yang cepat. Pelatihan hanya berupa penghitungan frekuensi, prediksi berupa perkalian vektor tunggal, dan kebutuhan memori sebanding dengan ukuran kosakata. Puluhan ribu dokumen tetap dapat dilayani pada perangkat biasa. Ketangguhan pada korpus kecil juga terlihat di eksperimen topik ini karena dua puluh satu dokumen latih sudah cukup untuk delapan prediksi benar dari sembilan.

Keterbatasannya perlu diakui dengan jelas. Asumsi independensi membuat model tidak memahami urutan kata dan makna frasa, dan probabilitas keluarannya sering tidak terkalibrasi. Pada korpus besar dengan fitur yang saling bergantung, regresi logistik maupun support vector machine linear biasanya mencatat akurasi yang lebih tinggi. Apabila angka probabilitas dipakai langsung dalam keputusan, kalibrasi ulang melalui pembungkus seperti CalibratedClassifierCV perlu dipertimbangkan. Karena itu, pola pemakaiannya lazim berupa langkah pertama, yaitu membangun baseline Naive Bayes, kemudian menggantinya hanya bila model yang lebih berat terbukti lebih baik pada data uji yang sama.

Kesalahan umum#

Kesalahan pada topik ini umumnya menyangkut pemilihan varianta model dan pembacaan angka probabilitas.

  • Memakai GaussianNB untuk vektor kata. Model tersebut mengasumsikan fitur berupa bilangan kontinu berdistribusi normal, asumsi yang tidak cocok untuk hitungan dan bobot kata. Varianta yang tepat untuk teks adalah MultinomialNB.
  • Memberi masukan bernilai negatif, misalnya hasil StandardScaler. MultinomialNB menuntut semua nilai fitur tak negatif dan menampilkan pesan error bila syarat itu dilanggar. Fitur dari CountVectorizer atau TfidfVectorizer memenuhi syarat tanpa penskalaan tambahan.
  • Memperlakukan probabilitas keluaran sebagai keyakinan yang terkalibrasi. Penghalusan dan asumsi independensi membuat angkanya cenderung bergeser, sehingga peringkat antar kelasnya lebih dapat diandalkan daripada nilai mutlaknya. Kalibrasi diperiksa terlebih dahulu bila angka probabilitas akan dipakai langsung.
  • Menyetel α\alpha ke nol agar model tidak “memalsukan” data. Tanpa penghalusan, satu kata yang tidak pernah teramati pada suatu kelas langsung menjadikan peluang kelas itu nol, sehingga teks yang memuat kata tersebut tidak pernah dipilih sebagai kelasnya. Nilai α\alpha dibiarkan pada nilai bawaan atau disetel melalui data validasi.

Ringkasan#

  • Multinomial Naive Bayes menerapkan teorema Bayes dengan asumsi bebas antar kata, sehingga pelatihan berupa penghitungan frekuensi dalam satu lintasan data.
  • Penghalusan Laplace dengan α\alpha menghindari peluang nol pada kata yang belum pernah terlihat di suatu kelas.
  • Pada korpus mini, akurasi data uji mencapai 0.8889, dan satu-satunya kesalahan berasal dari kata “mencatat” yang kuat berasosiasi dengan kelas rapat.
  • Model ini cepat, hemat memori, dan tangguh pada korpus kecil, sehingga lazim dijadikan baseline sebelum model yang lebih berat dicoba.

Referensi#