Support Vector Machine pada chapter sebelumnya menuntut fitur numerik yang rapi dan diskalakan, padahal begitu banyak data nyata berbentuk teks bebas: pesan singkat, surel, ulasan pelanggan. Naive Bayes adalah model klasifikasi yang justru merasa paling betah di wilayah ini — ia mengubah pertanyaan “kelas apa pesan ini” menjadi perhitungan probabilitas atas kata-kata di dalamnya, dan melakukannya dengan kecepatan yang sulit ditandingi model mana pun. Pada chapter penutup Section ini kita membangun penyaring pesan singkat bertema Indonesia dengan 28 pesan berlabel spam dan biasa, membedah teorema Bayes yang menjadi mesinnya, lalu menguji seberapa jauh asumsi “polos” yang menjadi nama aliasnya benar-benar berlaku.

Teorema Bayes: Membalik Arah Pertanyaan #

Yang ingin kita ketahui adalah peluang sebuah pesan merupakan spam setelah membaca isinya — misalnya setelah menemukan kata “gratis”. Yang tersedia dari data latih justru arah sebaliknya: dari pesan-pesan yang sudah berlabel, kita bisa menghitung seberapa sering kata “gratis” muncul di pesan spam dan di pesan biasa. Teorema Bayes adalah jembatan resmi untuk membalik arah pertanyaan tersebut:

text
P(spam | gratis) = P(gratis | spam) x P(spam) / P(gratis)

P(spam | gratis) : peluang pesan spam, justru setelah melihat kata "gratis"
P(gratis | spam) : seberapa sering kata "gratis" muncul pada pesan spam
P(spam)          : peluang awal sebuah pesan adalah spam (tanpa membaca isi)
P(gratis)        : peluang kata "gratis" muncul pada pesan apa pun

Kita hitung langsung dengan angka yang mudah diperiksa. Andaikan 30% pesan yang masuk adalah spam; kata “gratis” muncul pada 80% pesan spam tetapi hanya 5% pesan biasa:

python
p_spam = 0.30
p_biasa = 0.70
p_gratis_jika_spam = 0.80
p_gratis_jika_biasa = 0.05

p_gratis = p_gratis_jika_spam * p_spam + p_gratis_jika_biasa * p_biasa
p_spam_jika_gratis = (p_gratis_jika_spam * p_spam) / p_gratis

print("P(spam)                     : %.2f" % p_spam)
print("P(gratis|spam)              : %.2f" % p_gratis_jika_spam)
print("P(gratis|biasa)             : %.2f" % p_gratis_jika_biasa)
print("P(gratis)                   : %.2f" % p_gratis)
print("P(spam|gratis)              : %.2f" % p_spam_jika_gratis)

Output yang Dihasilkan:

text
P(spam)                     : 0.30
P(gratis|spam)              : 0.80
P(gratis|biasa)             : 0.05
P(gratis)                   : 0.27
P(spam|gratis)              : 0.87

Mari kita telaah baris-baris kode di atas:

  1. p_gratis dihitung lewat hukum total peluang: kata “gratis” bisa muncul dari dua jalur — pesan spam (0,80 × 0,30) atau pesan biasa (0,05 × 0,70) — dan keduanya dijumlahkan menjadi 0,27.
  2. p_spam_jika_gratis memakai rumus Bayes penuh: bukti “gratis” (0,80 vs 0,05) mengangkat keyakinan kita dari peluang awal 0,30 menjadi 0,87. Satu kata saja sudah membalik keputusan dari “kemungkinan bukan spam” menjadi “hampir pasti spam”.
  3. Arah pembalikan inilah seluruh inti Naive Bayes: model memperbarui peluang awal suatu kelas setiap kali menemukan bukti baru, kata demi kata.

Asumsi Kemerdekaan Bersyarat: Bagian yang “Polos” #

Satu kata jarang cukup; pesan nyata memuat belasan kata. Idealnya kita menghitung P(kata₁, kata₂, …, kataₙ | spam) — peluang gabungan seluruh kombinasi kata. Masalahnya, mengestimasi joint probability untuk setiap kombinasi kata butuh data yang luar biasa banyak. Di sinilah bagian “naive” (polos) bekerja: model mengasumsikan bahwa setiap kata bersifat merdeka satu sama lain begitu kelasnya diketahui — asumsi conditional independence — sehingga peluang gabungan cukup dilakukan perkalian:

text
P(kata1, kata2, ..., kata_n | spam) ≈ P(kata1|spam) x P(kata2|spam) x ... x P(kata_n|spam)

Asumsi ini jelas tidak benar-benar tepat: kata “menang” dan “hadiah” berjalan beriringan di pesan undian, jadi berhitung dua-duanya berarti menghitung bukti yang sama dua kali. Keajaibannya (yang terukur di banyak riset) adalah kesalahan ini biasanya hanya menggeser skala keyakinan, bukan urutan kelas mana yang paling mungkin — dan karena klasifikasi hanya membutuhkan kelas berpeluang terbesar, Naive Bayes tetap akurat meski probabilitasnya tidak rapi. Kita akan lihat efek sampingnya sebentar lagi pada keluaran model.

Mengubah Pesan Menjadi Hitungan Kata #

Sebelum teorema Bayes bisa bekerja, teks harus diubah menjadi angka. CountVectorizer melakukan pekerjaan ini dengan tiga langkah: memecah pesan menjadi kata (lowercase otomatis), menyusun daftar kata unik dari seluruh pesan (kosakata), lalu menghitung berapa kali tiap kata muncul di tiap pesan. Hasilnya adalah matriks hitungan kata — barisnya pesan, kolomnya kata:

python
import pandas as pd
from sklearn.feature_extraction.text import CountVectorizer

pesan_mini = [
    "kopi gratis untuk Anda",
    "rapat tim besok pagi",
    "gratis kuota internet sekarang",
    "rapat diadakan besok sore",
]

vektor = CountVectorizer()
hitungan = vektor.fit_transform(pesan_mini)

pd.set_option("display.max_columns", None)
pd.set_option("display.width", 120)
tabel = pd.DataFrame(hitungan.toarray(), index=["pesan 1", "pesan 2", "pesan 3", "pesan 4"],
                     columns=vektor.get_feature_names_out())
print(tabel)
pd.reset_option("display.max_columns")
pd.reset_option("display.width")

Output yang Dihasilkan:

text
         anda  besok  diadakan  gratis  internet  kopi  kuota  pagi  rapat  sekarang  sore  tim  untuk
pesan 1     1      0         0       1         0     1      0     0      0         0     0    0      1
pesan 2     0      1         0       0         0     0      0     1      1         0     0    1      0
pesan 3     0      0         0       1         1     0      1     0      0         1     0    0      0
pesan 4     0      1         1       0         0     0      0     0      1         0     1    0      0

Mari kita telaah hasilnya:

  1. Kosakata berisi 13 kata unik dari empat pesan — setiap kata menjadi satu kolom, tersusun alfabetis lewat get_feature_names_out().
  2. Setiap pesan menjadi vektor hitungan: pesan 1 bernilai 1 pada kolom anda, gratis, kopi, dan untuk. Kata “rapat” muncul di pesan 2 dan 4 — dan di sinilah letak informasinya: kata yang muncul pada satu kelas tetapi tidak pada kelas lain akan menjadi bukti yang kuat.
  3. Matriks ini yang dikonsumsi model — MultinomialNB merangkum matriks menjadi peluang tiap kata per kelas, lalu memakai asumsi kemerdekaan untuk mengalikan semuanya.

Melatih MultinomialNB pada Pesan Spam #

Kini kita rakit seluruh alurnya pada dataset lengkap: 28 pesan singkat — 14 penawaran khas pesan berantai (undian, hadiah, pinjaman, diskon) dan 14 pesan sehari-hari (rapat, tugas kuliah, arisan keluarga). Model dilatih pada sebagian pesan, diuji pada sisanya, lalu diadu pada tiga pesan baru:

python
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.model_selection import train_test_split
from sklearn.naive_bayes import MultinomialNB

pesan = [
    "Selamat Anda menang undian berhadiah satu juta rupiah",
    "Klik tautan ini untuk mengklaim hadiah gratis Anda",
    "Promo besar hari ini diskon 70 persen khusus Anda",
    "Pinjaman cair cepat tanpa agunan proses lima menit",
    "Gratis kuota internet 10 GB untuk pengguna terpilih",
    "Menang undian berhadiah klik sekarang sebelum habis",
    "Hadiah langsung transfer ke rekening Anda klaim sekarang",
    "Diskon khusus member baru daftar gratis dapat bonus",
    "Anda terpilih mendapatkan hadiah spesial minggu depan",
    "Promo pinjaman tanpa bunga cicilan ringan segera klik",
    "Klaim voucher gratis belanja seratus ribu rupiah sekarang",
    "Undian akhir tahun Anda menang hadiah utama mobil",
    "Voucher diskon seratus persen khusus pelanggan setia segera klaim",
    "Juta rupiah hadiah langsung cair tanpa syarat ribet klik tautan",
    "Rapat tim besok pagi jam sembilan di ruang rapat",
    "Tugas statistika dikumpulkan hari Jumat mohon dikerjakan",
    "Kita ngopi di kedai langganan sore ini ya",
    "Ibu meminta dibelikan beras dan gula pasir di pasar",
    "Notifikasi rekening listrik bulan ini sudah terbit",
    "Kelas machine learning diadakan daring hari Senin",
    "Dokumen laporan bulanan sudah saya kirim via surel",
    "Jangan lupa bawa payung musim hujan mulai pekan ini",
    "Arisan keluarga di rumah Bu Siti hari Minggu datang ya",
    "Hasil ujian kelas data science sudah diumumkan hari ini",
    "Kue nastar buatan Dewi lezat sekali silakan coba saat rapat",
    "Sepeda lipat Budi diparkir di halaman kantor belakang",
    "Cuaca hujan sepanjang pekan ini jangan lupa bawa payung",
    "Pembayaran tagihan listrik bulan ini jatuh tempo akhir pekan",
]
label = ["spam"] * 14 + ["biasa"] * 14

X_latih, X_uji, y_latih, y_uji = train_test_split(pesan, label, test_size=0.3,
                                                   stratify=label,
                                                   random_state=42)

vektor = CountVectorizer()
X_latih_hitung = vektor.fit_transform(X_latih)
X_uji_hitung = vektor.transform(X_uji)

model = MultinomialNB()
model.fit(X_latih_hitung, y_latih)

print("Akurasi data uji: %.2f" % model.score(X_uji_hitung, y_uji))
print()
print("Prediksi pesan baru:")
pesan_baru = [
    "Klik tautan ini untuk mendapatkan hadiah gratis segera",
    "Rapat evaluasi kantor diadakan besok sore",
    "Diskon gratis khusus Anda menang undian",
]
prediksi = model.predict(vektor.transform(pesan_baru))
probabilitas = model.predict_proba(vektor.transform(pesan_baru))
for teks, kelas, prob in zip(pesan_baru, prediksi, probabilitas):
    print("  %-55s -> %s (p=%.2f)" % (teks, kelas, prob.max()))

Output yang Dihasilkan:

text
Akurasi data uji: 1.00

Prediksi pesan baru:
  Klik tautan ini untuk mendapatkan hadiah gratis segera  -> spam (p=1.00)
  Rapat evaluasi kantor diadakan besok sore               -> biasa (p=0.98)
  Diskon gratis khusus Anda menang undian                 -> spam (p=1.00)

Mari kita telaah baris-baris kode di atas:

  1. train_test_split memisahkan teks mentah, bukan matriks: pemisahan dilakukan pada daftar pesan agar pasangan teks-label tetap sejajar. Baru setelah itu CountVectorizer di-fit pada X_latih saja (fit_transform) lalu diterapkan ke X_uji (transform) — kosakata data uji yang tidak dikenal latih tidak boleh ikut membentuk kolom, prinsip anti-kebocoran yang sama dengan StandardScaler pada dua chapter sebelumnya.
  2. MultinomialNB() tanpa penyetelan apa pun mencapai akurasi 1,00 pada 9 pesan uji — kosakata spam kita memang sangat khas. Pada data nyata, penyaring spam profesional menghadapi pesan yang sengaja mengelabui (ejaan diubah, disingkat), sehingga akurasi selalu jauh di bawah angka mainan ini.
  3. predict_proba mengembalikan peluang tiap kelas dan kita mencetak yang tertinggi. Nilai 0,98-1,00 terlihat percaya diri berlebihan — inilah efek samping asumsi kemerdekaan yang menghitung bukti berulang, dan alasan probabilitas Naive Bayes sebaiknya dibaca sebagai kekuatan sinyal, bukan angka kalibrasi yang presisi.

Membaca Kata Paling Khas Tiap Kelas #

Salah satu kelebihan praktis Naive Bayes adalah transparansinya: kita bisa membuka isi kepala model dan bertanya kata mana yang paling menentukan tiap kelas. Atribut feature_log_prob_ menyimpan logaritma peluang tiap kata per kelas — logaritma dipakai karena hasil kali ratusan peluang kecil bisa menyusut sampai dibulatkan menjadi nol di bilangan pecahan; menjumlahkan log-nya adalah cara numerik yang stabil. Kita balik logaritma itu kembali menjadi peluang, lalu urutkan:

python
import numpy as np
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.naive_bayes import MultinomialNB

pesan = [
    "Selamat Anda menang undian berhadiah satu juta rupiah",
    "Klik tautan ini untuk mengklaim hadiah gratis Anda",
    "Promo besar hari ini diskon 70 persen khusus Anda",
    "Pinjaman cair cepat tanpa agunan proses lima menit",
    "Gratis kuota internet 10 GB untuk pengguna terpilih",
    "Menang undian berhadiah klik sekarang sebelum habis",
    "Hadiah langsung transfer ke rekening Anda klaim sekarang",
    "Diskon khusus member baru daftar gratis dapat bonus",
    "Anda terpilih mendapatkan hadiah spesial minggu depan",
    "Promo pinjaman tanpa bunga cicilan ringan segera klik",
    "Klaim voucher gratis belanja seratus ribu rupiah sekarang",
    "Undian akhir tahun Anda menang hadiah utama mobil",
    "Voucher diskon seratus persen khusus pelanggan setia segera klaim",
    "Juta rupiah hadiah langsung cair tanpa syarat ribet klik tautan",
    "Rapat tim besok pagi jam sembilan di ruang rapat",
    "Tugas statistika dikumpulkan hari Jumat mohon dikerjakan",
    "Kita ngopi di kedai langganan sore ini ya",
    "Ibu meminta dibelikan beras dan gula pasir di pasar",
    "Notifikasi rekening listrik bulan ini sudah terbit",
    "Kelas machine learning diadakan daring hari Senin",
    "Dokumen laporan bulanan sudah saya kirim via surel",
    "Jangan lupa bawa payung musim hujan mulai pekan ini",
    "Arisan keluarga di rumah Bu Siti hari Minggu datang ya",
    "Hasil ujian kelas data science sudah diumumkan hari ini",
    "Kue nastar buatan Dewi lezat sekali silakan coba saat rapat",
    "Sepeda lipat Budi diparkir di halaman kantor belakang",
    "Cuaca hujan sepanjang pekan ini jangan lupa bawa payung",
    "Pembayaran tagihan listrik bulan ini jatuh tempo akhir pekan",
]
label = ["spam"] * 14 + ["biasa"] * 14

vektor = CountVectorizer()
X_hitung = vektor.fit_transform(pesan)
model = MultinomialNB()
model.fit(X_hitung, label)

kata = vektor.get_feature_names_out()
print("Kata berpeluang tertinggi menurut kelas:")
for indeks, kelas in enumerate(model.classes_):
    peluang = np.exp(model.feature_log_prob_[indeks])
    teratas = np.argsort(-peluang)[:6]
    baris = ", ".join("%s (%.3f)" % (kata[i], peluang[i]) for i in teratas)
    print("  %-5s: %s" % (kelas, baris))

Output yang Dihasilkan:

text
Kata berpeluang tertinggi menurut kelas:
  biasa: ini (0.025), di (0.022), hari (0.018), rapat (0.015), pekan (0.015), sudah (0.015)
  spam : anda (0.026), hadiah (0.022), gratis (0.019), klik (0.019), menang (0.015), klaim (0.015)

Tabel ini sekaligus menjadi diagnosa model kita:

  1. Daftar kata spam terbaca seperti kamus pesan berantai: anda, hadiah, gratis, klik, menang, klaim — bukti bahwa model menangkap sinyal yang benar, bukan kebetulan.
  2. Daftar kata biasa lebih bertaburan (ini, di, hari, rapat, pekan, sudah) karena pesan sehari-hari kita justru beragam topiknya — inilah watak Naive Bayes pada data kecil: kelas dengan kosakata padat mudah dipelajari, kelas yang beragam butuh lebih banyak contoh.
  3. Perhatikan pasangan seperti hadiah dan menang yang sama-sama tinggi di kelas spam padahal kerap muncul bersama di pesan yang sama — bukti nyata asumsi kemerdekaan dilanggar setiap hari, dan probabilitas 1,00 di bagian sebelumnya adalah tagihannya.

Kekuatan dan Kelemahan Naive Bayes #

AspekKeterangan
Sangat cepat dilatihHanya menghitung frekuensi kata per kelas; jutaan dokumen tetap terjangkau
Baseline teks yang kuatAkurasi baik tanpa penyetelan; patokan awal yang layak sebelum model berat
Bekerja pada dimensi tinggiRibuan kolom kata bukan persoalan — jarak antar titik tidak ikut dihitung seperti KNN
TransparanKata khas tiap kelas bisa dibuka dan diperiksa langsung
Asumsi kemerdekaan keliruBukti berkorelasi dihitung berulang; probabilitas tidak terkalibrasi baik
Butuh teks bersihSingkatan, ejaan yang diubah, dan bahasa gaul merenggangkan kosakata
Posisi kata diabaikan“Bukan gratis” dan “gratis” dihitung sama; urutan kata tidak dibaca

Rangkuman #

  • Teorema Bayes membalik arah pertanyaan: dari P(bukti | kelas) pada data latih menuju P(kelas | bukti) untuk prediksi — kata “gratis” mengangkat keyakinan spam dari 0,30 ke 0,87 pada contoh kita.
  • Asumsi kemerdekaan bersyarat membuat peluang gabungan cukup diperkalian per kata; asumsi ini keliru secara teknis tetapi biasanya tidak sampai menukar urutan kelas.
  • CountVectorizer mengubah teks menjadi matriks hitungan kata; vektorisasi hanya di-fit pada data latih untuk mencegah kebocoran kosakata.
  • MultinomialNB mencapai akurasi 1,00 pada 9 pesan uji tanpa penyetelan, dengan kata khas yang mudah diperiksa lewat feature_log_prob_ — hadiah dan gratis di puncak kelas spam.
  • Probabilitas keluarannya cenderung ekstrem karena bukti berulang dihitung berkali-kali; angkanya adalah kekuatan sinyal, bukan peluang terkalibrasi.

Dengan Naive Bayes, perjalanan kita di supervised learning tuntas: sembilan model dari regresi linear hingga penyaring teks, semuanya belajar dari data berlabel. Pertanyaan berikutnya bersifat menggugah: bagaimana kalau labelnya tidak ada sama sekali? Bagaimana menemukan kelompok pelanggan, pola beli, atau struktur tersembunyi tanpa ada yang memberi tahu jawabannya? Itulah medan Section Unsupervised Learning — dan kita mulai dari gagasan clustering.

Referensi #

  1. scikit-learn — FAQ (memilih model untuk data teks)
  2. scikit-learn — Model evaluation (akurasi klasifikasi)
  3. scikit-learn — train_test_split