Sejak chapter pertama, semua data yang kita olah berbentuk angka yang rapi di kolom: nilai transaksi, suhu, koordinat. Padahal justru data paling melimpah di dunia berbentuk teks bebas — ulasan pelanggan, pesan singkat, keluhan di media sosial — dan model machine learning tidak bisa membaca; model hanya menghitung. Natural language processing (NLP) adalah bidang yang menjembatani keduanya, dan jembatannya lebih pendek daripada dugaan kita: teks diubah menjadi tabel hitungan kata, lalu peralatan klasifikasi yang sudah kita kuasai bekerja seperti biasa. Pada chapter ini kita membangun penyaring sentimen ulasan kedai kopi: membersihkan teks, membentuk fitur Bag-of-Words dan TF-IDF, lalu mengklasifikasikannya dengan Multinomial Naive Bayes — model yang mesin teorema Bayes-nya sudah kita bahas tuntas pada chapter Naive Bayes .

Teks sebagai Data: Membuat Kalimat Jadi Tabel #

Inti seluruh bab ini adalah satu transformasi: sebuah dokumen teks menjadi satu baris angka. Caranya sederhana — pecah teks menjadi potongan (token, umumnya kata), lalu hitung berapa kali setiap kata muncul. Susunan kalimat diabaikan; yang tersisa hanyalah kantong kata — dari situlah nama Bag-of-Words. Pengabaian tata bahasa ini terdengar naif, tetapi untuk tugas klasifikasi pendek seperti sentimen ulasan, kantong kata ternyata membawa cukup banyak sinyal: ulasan yang memuat “enak” dan “murah” hampir pasti beda suasana dengan yang memuat “pahit” dan “lambat”.

Sebelum menghitung, teks perlu dibakukan. Dua langkah paling mendasar adalah meratakan huruf besar-kecil dan memotong kata:

python
ulasan_mentah = "Kopi SUSUnya enak sekali!! Dan tempatnya nyaman."

kecil = ulasan_mentah.lower()
token = kecil.split()

print("Teks asli   :", ulasan_mentah)
print("Huruf kecil :", kecil)
print("Terpotong   :", token)
print("Jumlah token:", len(token))

Output yang Dihasilkan:

text
Teks asli   : Kopi SUSUnya enak sekali!! Dan tempatnya nyaman.
Huruf kecil : kopi susunya enak sekali!! dan tempatnya nyaman.
Terpotong   : ['kopi', 'susunya', 'enak', 'sekali!!', 'dan', 'tempatnya', 'nyaman.']
Jumlah token: 7

Perhatikan tiga masalah yang tampak pada token hasil .split(): “SUSUnya” dan “susunya” baru seragam setelah .lower(); tanda baca menempel pada kata (“sekali!!”, “nyaman.”); dan kata penghubung seperti “dan” ikut terhitung padahal nyaris tak membawa makna. CountVectorizer menangani dua masalah pertama secara bawaan — pola token-nya membuang tanda baca dan hanya menerima rangkaian huruf atau angka — sedangkan kata penghubung (stop words) bisa dibuang lewat daftar khusus bila diperlukan.

Bag-of-Words dengan CountVectorizer #

Kita pakai sepuluh ulasan mini kedai kopi, lima positif dan lima negatif:

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

ulasan = [
    "kopi susu enak harganya murah",
    "kopinya enak tempat nyaman",
    "barista ramah kopinya enak",
    "tempat nyaman wifi kencang",
    "es kopi susu enak porsinya pas",
    "kopinya pahit harganya mahal",
    "tempat sempit dan kotor",
    "pelayanan lambat kopinya dingin",
    "wifi lelet pelayanan lambat",
    "harga mahal porsinya sedikit",
]
label = ["positif"] * 5 + ["negatif"] * 5

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

print(f"Jumlah dokumen : {hitungan.shape[0]}")
print(f"Jumlah kosakata: {hitungan.shape[1]}")
kosakata = sorted(vektor.vocabulary_)
for i in range(0, len(kosakata), 6):
    print("  " + ", ".join(kosakata[i:i + 6]))

df_hitung = pd.DataFrame(
    hitungan.toarray(), columns=vektor.get_feature_names_out(), index=label
)
print()
print("Hitungan kata (indeks = label ulasan):")
print(df_hitung[["enak", "lambat", "mahal"]].to_string())

Output yang Dihasilkan:

text
Jumlah dokumen : 10
Jumlah kosakata: 26
  barista, dan, dingin, enak, es, harga
  harganya, kencang, kopi, kopinya, kotor, lambat
  lelet, mahal, murah, nyaman, pahit, pas
  pelayanan, porsinya, ramah, sedikit, sempit, susu
  tempat, wifi

Hitungan kata (indeks = label ulasan):
         enak  lambat  mahal
positif     1       0      0
positif     1       0      0
positif     1       0      0
positif     0       0      0
positif     1       0      0
negatif     0       0      1
negatif     0       0      0
negatif     0       1      0
negatif     0       1      0
negatif     0       0      1

Mari kita telaah hasilnya:

  1. fit_transform menghasilkan matriks 10 x 26: sepuluh dokumen, dua puluh enam kata unik — setiap ulasan kini satu baris, setiap kata satu kolom, dan sel-selnya berisi hitungan kemunculan. Inilah teks versi mesin.
  2. Kolom yang terpisah untuk “kopi” dan “kopinya”: imbuhan bahasa Indonesia tidak dibongkar oleh CountVectorizer, sehingga dua kata yang maknanya sama dihitung dua kolom berbeda. Pada data besar hal ini menggemukkan kosakata; praktik NLP Indonesia biasanya menambahkan stemming (pembubuhan imbuhan) sebagai langkah pra-pemrosesan.
  3. Tabel hitungan memperlihatkan pemisahan kelas: kata “enak” hanya muncul di baris berlabel positif, “lambat” dan “mahal” hanya di baris negatif — persis sinyal yang nanti dipakai model untuk menebak label ulasan baru.

TF-IDF: Menimbang Kata yang Membedakan #

Hitungan mentah memperlakukan semua kata sama penting, padahal tidak: kata yang muncul di hampir semua dokumen (“kopi” di ulasan kedai kopi!) hampir tak memberi informasi pembeda, sedangkan kata langka seperti “lelet” sangat khas. TF-IDF (term frequency - inverse document frequency) menimbang setiap kata dengan mengalikan dua faktor:

text
bobot(t, d) = TF(t, d) x IDF(t)

TF(t, d) : frekuensi kata t pada dokumen d, dinormalkan panjang dokumen
IDF(t)   : log((1 + N) / (1 + df(t))) + 1
N        : jumlah dokumen
df(t)    : jumlah dokumen yang memuat kata t

Kata yang muncul di banyak dokumen mendapat IDF kecil sehingga bobotnya ditindas; kata langka mendapat IDF besar. TfidfVectorizer menggantikan hitungan dengan bobot ini tanpa mengubah cara pakainya:

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

ulasan = [
    "kopi susu enak harganya murah",
    "kopinya enak tempat nyaman",
    "barista ramah kopinya enak",
    "tempat nyaman wifi kencang",
    "es kopi susu enak porsinya pas",
    "kopinya pahit harganya mahal",
    "tempat sempit dan kotor",
    "pelayanan lambat kopinya dingin",
    "wifi lelet pelayanan lambat",
    "harga mahal porsinya sedikit",
]
label = ["positif"] * 5 + ["negatif"] * 5

tfidf = TfidfVectorizer()
bobot = tfidf.fit_transform(ulasan)

df_bobot = pd.DataFrame(
    bobot.toarray().round(3), columns=tfidf.get_feature_names_out(), index=label
)
print("Bobot TF-IDF (indeks = label ulasan):")
print(df_bobot[["enak", "lelet", "nyaman"]].to_string())

Output yang Dihasilkan:

text
Bobot TF-IDF (indeks = label ulasan):
          enak  lelet  nyaman
positif  0.348  0.000   0.000
positif  0.451  0.000   0.580
positif  0.390  0.000   0.000
positif  0.000  0.000   0.491
positif  0.308  0.000   0.000
negatif  0.000  0.000   0.000
negatif  0.000  0.000   0.000
negatif  0.000  0.000   0.000
negatif  0.000  0.562   0.000
negatif  0.000  0.000   0.000

Bacaan hasilnya:

  1. “enak” berbobot rendah (0,308-0,451): kata ini muncul di empat dari sepuluh dokumen — cukup umum sehingga IDF-nya ditindas. Bandingkan dengan “lelet” yang hanya muncul sekali di seluruh kumpulan: bobotnya 0,562, paling tinggi di antara ketiga kolom yang ditampilkan.
  2. Bobot “enak” bervariasi antar dokumen (0,348 vs 0,451): TF dinormalkan panjang dokumen — ulasan pendek yang memuat “enak” memberi bobot lebih besar daripada ulasan panjang dengan hitungan yang sama, karena setiap kata di ulasan pendek membawa porsi perhatian lebih besar.
  3. Angka nol yang rapi: kata yang tidak muncul di sebuah dokumen berbobot nol — matriks tetap selebar kosakata, tetapi sebagian besar selnya kosong (sparse), wujud dari kenyataan bahwa satu dokumen hanya memuat sebagian kecil kosakata.

Klasifikasi Sentimen dengan MultinomialNB #

Fitur siap; tersisa melatih model — dan karena fitur kita berupa hitungan kata, pilihan yang paling alami adalah MultinomialNB, varian Naive Bayes untuk data hitungan. Seluruh bekal teorema Bayes, asumsi kemerdekaan, dan Laplace smoothing dari chapter Naive Bayes berlaku apa adanya; kali ini bukti-buktinya berupa kata:

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

ulasan = [
    "kopi susu enak harganya murah",
    "kopinya enak tempat nyaman",
    "barista ramah kopinya enak",
    "tempat nyaman wifi kencang",
    "es kopi susu enak porsinya pas",
    "kopinya pahit harganya mahal",
    "tempat sempit dan kotor",
    "pelayanan lambat kopinya dingin",
    "wifi lelet pelayanan lambat",
    "harga mahal porsinya sedikit",
]
label = ["positif"] * 5 + ["negatif"] * 5

vektor = CountVectorizer()
hitungan = vektor.fit_transform(ulasan)
model = MultinomialNB().fit(hitungan, label)

baru = ["kopi susunya enak dan tempatnya nyaman",
        "pelayanannya lambat sekali harganya mahal"]
hitungan_baru = vektor.transform(baru)
prediksi = model.predict(hitungan_baru)
peluang = model.predict_proba(hitungan_baru)

for teks, kelas, p in zip(baru, prediksi, peluang):
    print(f"Ulasan  : {teks}")
    print(f"Prediksi: {kelas} | P(negatif)={p[0]:.3f} P(positif)={p[1]:.3f}")

Output yang Dihasilkan:

text
Ulasan  : kopi susunya enak dan tempatnya nyaman
Prediksi: positif | P(negatif)=0.054 P(positif)=0.946
Ulasan  : pelayanannya lambat sekali harganya mahal
Prediksi: negatif | P(negatif)=0.916 P(positif)=0.084

Bedah langkah-langkahnya:

  1. vektor.transform(baru) — bukan fit_transform: ulasan baru hanya diproyeksikan ke kosakata lama; memfit ulang berarti membiarkan data uji ikut menentukan fitur, satu bentuk data leakage yang sudah kita bahas pada chapter Mencegah Data Leakage .
  2. Kata tak dikenal hilang diam-diam: “susunya” dan “pelayanannya” tidak ada di kosakata latih, jadi kolomnya tidak ada — teks tetap terproses karena kata lain (“enak”, “nyaman”, “lambat”, “mahal”) yang dikenal cukup memuat sinyal. Ulasan yang seluruh katanya asing akan menjadi vektor nol dan prediksinya murni mengikuti proporsi kelas.
  3. Probabilitas yang tegas tapi tidak mutlak: 0,946 dan 0,916 — model cukup yakin, dan Laplace smoothing dari Naive Bayes menjaga agar satu kata langka tidak melipatgandakan keyakinan tanpa batas.

Dua ulasan ini sengaja kita buat mudah. Jujur perlu dikatakan: sepuluh dokumen latih jauh dari cukup untuk menilai kualitas model sungguhan — kosakatanya sempit dan hasil validasi akan bergoyang lebar. Praktik nyata memakai ratusan hingga ribuan ulasan berlabel dan menilai dengan validasi silang, bukan dua contoh pilihan; arsitektur evaluasinya sama dengan yang kita latih di section Evaluasi dan Validasi Model .

Rangkuman #

  • Teks diubah menjadi data lewat Bag-of-Words: token yang dihitung kemunculannya per dokumen — urutan dan tata bahasa dibuang, sinyal kelas tetap tersisa.
  • Pembersihan mendasar: huruf kecil dan pemotongan kata; CountVectorizer membuang tanda baca secara bawaan, sedangkan imbuhan bahasa Indonesia (“kopi” vs “kopinya”) tetap terpisah sampai kita menambahkan stemming.
  • TF-IDF menimbang kata dengan frekuensi yang dinormalkan panjang dokumen dikali kebalikan kelaziman kata — “lelet” yang langka berbobot 0,562, “enak” yang umum tertindas ke 0,308-0,451.
  • MultinomialNB mengklasifikasikan ulasan baru dari hitungan katanya; ulasan di luar kosakata latih menghasilkan vektor nol, dan evaluasi serius menuntut data berlabel yang jauh lebih banyak.

Kantong kata mengubah teks menjadi angka, tetapi setiap kata tetap diperlakukan sebagai satuan yang berdiri sendiri — sistem yang paling mirip cara mesin lama membaca. Ada satu keluarga model yang belajar representasinya sendiri berlapis-lapis, dari fitur sederhana menuju konsep yang makin abstrak, dan keluarga itulah yang mengubah wajah NLP serta pengolahan gambar dalam satu dekade terakhir. Kita tutup section teknik dengan pengantar mereka: Pengantar Deep Learning .

Referensi: