Teks sebagai Fitur

Mengubah teks menjadi fitur melalui bag-of-words dan TF-IDF.

Daftar isi

Model pada scikit-learn membaca matriks angka, sehingga kumpulan dokumen harus disandikan menjadi angka sebelum dipelajari. Topik ini membahas dua skema penyandian teks yang paling sering dipakai, yaitu penghitungan kata pada skema bag-of-words dan pembobotan term frequency-inverse document frequency (TF-IDF). Praktiknya menggunakan korpus mini berbahasa Indonesia berisi 30 dokumen dari tiga kelas, yaitu pesan status perangkat, catatan cuaca harian, dan arsip catatan rapat, yang dibangun di dalam skrip teks-ke-fitur.py.

Skema bag-of-words: dari kalimat ke hitungan#

Skema bag-of-words menyandikan setiap dokumen sebagai vektor hitungan kata dengan tiga langkah. Pertama, teks dipecah menjadi potongan yang disebut token, dan pada pengaturan bawaan scikit-learn potongan itu berupa rangkaian huruf atau angka berpanjang dua karakter ke atas. Kedua, seluruh token unik dari korpus disusun menjadi kosakata. Ketiga, setiap dokumen direpresentasikan sebagai baris yang panjangnya sama dengan banyaknya kata dalam kosakata, dan setiap selnya berisi jumlah kemunculan kata tersebut pada dokumen itu. Urutan kata dibuang sama sekali, sehingga dua kalimat dengan isi kata sama tetapi susunan berbeda menghasilkan baris yang identik, dan inilah asal nama “kantong kata”.

Hasilnya adalah matriks yang barisnya menyatakan dokumen dan kolomnya menyatakan kata dalam kosakata. Untuk teks alami, matriks ini bersifat sparse, yaitu hampir seluruh selnya bernilai nol karena satu dokumen hanya memuat sebagian kecil kata dari kosakata. scikit-learn menyimpan matriks itu sebagai matriks jarang sehingga memori yang terpakai sebanding dengan jumlah kata yang benar-benar muncul, bukan dengan ukuran matriks penuhnya.

Pembobotan TF-IDF#

Hitungan mentah memberi bobot besar pada kata yang sering muncul, padahal kata demikian sering kali kurang bermakna untuk membedakan dokumen. Kata “dan” bisa hadir di hampir setiap dokumen tanpa membawa jejak topik, sedangkan kata “baterai” yang muncul sekali pun langsung menunjuk kelas tertentu. Pembobotan TF-IDF menyeimbangkan dua sisi itu dengan mengalikan frekuensi kata pada dokumen (term frequency) dengan kebalikan frekuensi dokumen (inverse document frequency).

tf-idf(t,d)=tf(t,d)×idf(t) \text{tf-idf}(t, d) = \text{tf}(t, d) \times \text{idf}(t)

Komponen kedua menurunkan bobot kata yang hadir di banyak dokumen. scikit-learn menghitungnya dengan rumus penghalusan berikut, dengan nn menyatakan jumlah dokumen dan df(t)\mathrm{df}(t) menyatakan banyaknya dokumen yang memuat kata tt.

idf(t)=ln⁡1+n1+df(t)+1 \mathrm{idf}(t) = \ln \frac{1 + n}{1 + \mathrm{df}(t)} + 1

Kata yang muncul di seluruh dokumen memperoleh idf terkecil, sedangkan kata yang muncul di satu dokumen memperoleh idf terbesar. Setelah perkalian, setiap vektor dokumen dinormalisasi sehingga panjangnya satu, dan bobot tf-idf sebuah kata pada akhirnya menyatakan seberapa khas kata itu bagi dokumen yang bersangkutan. Vektor hasil pembobotan ini menjadi masukan yang jauh lebih informatif bagi model klasifikasi maupun pencarian dokumen.

Memeriksa matriks pada korpus mini#

Skrip berikut membangun korpus 30 dokumen, membentuk matriks hitungan dengan CountVectorizer, membongkar isi satu baris untuk memperlihatkan mekanismenya, lalu membandingkan bobot tf-idf yang dihasilkan TfidfVectorizer.

python teks-ke-fitur.py
import numpy as np
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer

# 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)

# bag-of-words: setiap dokumen menjadi baris hitungan kata
penghitung = CountVectorizer()
X = penghitung.fit_transform(korpus)
kosa = penghitung.get_feature_names_out()

print(f"Jumlah dokumen : {X.shape[0]}")
print(f"Ukuran matriks : {X.shape[0]} dokumen x {X.shape[1]} kata unik")
print()
print("Sebagian kosakata:", ", ".join(kosa[:10]))
print()

# isi satu baris matriks untuk memperlihatkan mekanismenya
dok = 2
baris = X[dok].toarray()[0]
print(f"Baris ke-{dok}: {korpus[dok]}")
print("Kata beserta jumlah kemunculannya pada dokumen ini:")
for i in np.nonzero(baris)[0]:
    print(f"  {kosa[i]:<12} {int(baris[i])}")
print()

# TF-IDF: bobot turun untuk kata yang muncul di banyak dokumen
tfidf = TfidfVectorizer()
Xt = tfidf.fit_transform(korpus)
kosa_t = tfidf.get_feature_names_out()
print("Nilai idf untuk tiga kata:")
for kata in ("dan", "hujan", "baterai"):
    i = int(np.where(kosa_t == kata)[0][0])
    print(f"  {kata:<10} idf = {tfidf.idf_[i]:.4f}")
print()

# bobot tertinggi dua dokumen dari kelas berbeda
for dok in (2, 11):
    baris_t = Xt[dok].toarray()[0]
    teratas = np.argsort(baris_t)[::-1][:5]
    print(f"Lima bobot tf-idf tertinggi baris {dok} (label {label[dok]}):")
    print("  " + ", ".join(f"{kosa_t[i]} ({baris_t[i]:.3f})" for i in teratas))
text Keluaran
Jumlah dokumen : 30
Ukuran matriks : 30 dokumen x 190 kata unik

Sebagian kosakata: agenda, anggaran, angin, awan, baca, baterai, berawan, berhenti, berikutnya, berputar

Baris ke-2: Kapasitas baterai perangkat tersisa empat puluh persen pada pemantauan pagi ini.
Kata beserta jumlah kemunculannya pada dokumen ini:
  baterai      1
  empat        1
  ini          1
  kapasitas    1
  pada         1
  pagi         1
  pemantauan   1
  perangkat    1
  persen       1
  puluh        1
  tersisa      1

Nilai idf untuk tiga kata:
  dan        idf = 2.1314
  hujan      idf = 2.8245
  baterai    idf = 3.7408

Lima bobot tf-idf tertinggi baris 2 (label perangkat):
  kapasitas (0.358), persen (0.358), baterai (0.358), empat (0.319), tersisa (0.319)
Lima bobot tf-idf tertinggi baris 11 (label cuaca):
  tenang (0.365), tanpa (0.365), sepanjang (0.365), langit (0.365), angin (0.325)

Keluaran tersebut mengonfirmasi setiap bagian mekanisme yang dijelaskan sebelumnya. Matriks berukuran 30 kali 190, sedangkan baris kedua hanya berisi 11 sel tak nol, sehingga lebih dari 94 persen selnya kosong dan sifat sparse itu terlihat langsung. Urutan idf mempertegas peran pembobotan. Kata “dan” yang hadir di sembilan dokumen memperoleh 2.1314, “hujan” yang hadir di empat dokumen memperoleh 2.8245, dan “baterai” yang hanya hadir pada satu dokumen memperoleh 3.7408. Bobot tertinggi tiap baris pun jatuh pada kata yang paling mewakili kelasnya, seperti “kapasitas” dan “baterai” untuk pesan perangkat, serta “langit” dan “angin” untuk catatan cuaca.

Pada pemodelan sungguhan, vektorisasi dipasangkan dengan disiplin kebocoran data sebagaimana pembahasan pemisahan data. Aturannya, fit_transform hanya dijalankan pada data latih, sedangkan data uji cukup di-transform dengan kosakata dan idf milik data latih. Praktik itu menjaga agar kosakata data uji tidak ikut membentuk penyandian yang dipakai melatih model. Menyatukan vektorisasi dan model di dalam satu pipeline, yaitu rangkaian proses yang dijalankan sebagai satu objek, menjadi cara paling aman karena urutan fit dan transform tidak mungkin tertukar.

Kesalahan umum#

Kesalahan pada topik ini umumnya menyangkut pemisahan data dan penanganan bentuk matriksnya.

  • Menjalankan fit_transform pada data latih dan data uji secara terpisah. Kosakata dan idf pada kedua matriks menjadi berbeda, sehingga kolom yang sama tidak lagi menyatakan kata yang sama dan model menerima masukan yang tidak konsisten. Data uji cukup di-transform dengan vektorisasi milik data latih.
  • Mengubah matriks jarang menjadi matriks padat dengan toarray pada korpus besar. Ukuran memori meledak karena seluruh sel kosong ikut disimpan, meskipun sel tak nolnya sedikit. Matriks jarang dibiarkan dalam format aslinya karena seluruh estimator scikit-learn menerimanya.
  • Mengharapkan skema ini memahami makna dan urutan kata. Bag-of-words membuang keduanya, sehingga frasa “tidak normal” dan “normal” sulit dibedakan dari sisi kata “normal” saja. Ketika urutan kata penting, rentang n-gram dinaikkan agar pasangan kata ikut menjadi kolom.
  • Menganggap urutan kolom matriks mengikuti urutan kata pada dokumen. Kolom tersusun alfabetis menurut kosakata, seperti terlihat pada keluaran yang diawali kata “agenda” dan “anggaran”. Pemetaan indeks ke kata selalu diambil dari get_feature_names_out, bukan dari asumsi urutan kemunculan.

Ringkasan#

  • Bag-of-words menyandikan dokumen menjadi baris hitungan kata dan membuang urutannya, menghasilkan matriks jarang berukuran dokumen kali kosakata.
  • TF-IDF mengalikan frekuensi kata dengan idf, sehingga kata yang hadir di banyak dokumen tersusut dan kata pembeda menguat.
  • Korpus 30 dokumen topik ini menghasilkan matriks 30 kali 190 dengan lebih dari 94 persen sel bernilai nol.
  • Vektorisasi di-fit pada data latih saja, dan data uji hanya di-transform agar tidak terjadi kebocoran kosakata.

Referensi#