Model deep learning tidak membaca huruf; model hanya membaca angka. Teks mentah dari pengguna justru penuh variasi: huruf besar dan kecil bercampur, tanda baca, angka harga, karakter unicode hasil salin-tempel dari web, dan kata berimbuhan panjang khas bahasa Indonesia. Preprocessing adalah rangkaian pembersihan yang menjembatani kedua dunia tersebut — hasilnya menentukan ukuran vocabulary, kualitas embedding, dan akhirnya kualitas model NLP kita. Chapter ini membangun pipeline pembersihan bertahap pada lima kalimat contoh bahasa Indonesia, lalu membahas dua keputusan yang sering salah: kapan stop words boleh dibuang, dan kapan stemming justru merugikan.

Empat Tahap Preprocessing #

Pipeline klasik terdiri atas empat tahap yang bisa dipilih sesuai kebutuhan. Tidak semua tahap wajib — pilihan bergantung pada tugas akhir yang dituju:

TahapMasalah yang dipecahkanAlat di Python
Normalisasi unicodekarakter serupa dengan kode berbeda (spasi tak putus, elipsis)unicodedata.normalize("NFKC", ...)
Pembersihanhuruf besar, tanda baca, angka, spasi gandastr.lower() + re.sub()
Stop wordskata frekuensi tinggi yang miskin muatandaftar stop words + filter
Stemming / lemmatizationvariasi imbuhan dari satu kata dasaraturan awalan/akhiran atau pustaka Sastrawi

Normalisasi dan Pembersihan #

Normalisasi berjalan lebih dulu karena unicode menyimpan banyak karakter yang terlihat sama tetapi berbeda kode. Spasi tak putus \u00a0 sering menempel pada teks hasil salin-tempel dari browser, dan elipsis … adalah satu karakter sebelum akhirnya menjadi tiga titik. Bentuk NFKC (compatibility decomposition) menyeragamkan seluruh varian tersebut menjadi bentuk kanoniknya — dokumentasi modul unicodedata merinci seluruh bentuk normalisasi yang tersedia.

Setelah normalisasi, pembersihan menghapus komponen yang tidak relevan bagi model kata: lower() menyamakan besar kecil huruf, satu re.sub() mengganti semua karakter selain huruf a–z dan spasi menjadi spasi (angka dan tanda baca ikut terbuang), lalu re.sub() kedua merapatkan spasi beruntun. Berikut implementasi lengkapnya pada lima kalimat uji:

python
"""Preprocessing teks bahasa Indonesia: normalisasi unicode, pembersihan,
buang stop words dengan perlindungan negasi, dan stemming berbasis aturan.

Tiga tahap ditunjukkan pada lima kalimat contoh: (1) normalisasi NFKC dan
pembersihan karakter, (2) stop words dengan dan tanpa perlindungan kata
negasi, (3) stemming sederhana berbasis aturan awalan/akhiran.
"""

import re
import unicodedata

# ---------- 1. Data mentah: 5 kalimat dari media sosial ----------
kalimat_mentah = [
    "Siti membeli 3 kg jeruk di Pasar Baru!!!",
    "Filmnya bagus\u2026 tapi tiketnya Rp75.000.",
    "Kopi\u00a0ini\u00a0tidak\u00a0enak.",
    "Budi menonton konser musik pada hari Sabtu.",
    "Harga naik 12,5% dalam sepekan.",
]


def normalisasi_unicode(teks: str) -> str:
    """Samakan bentuk unicode: NFKC menyeragamkan karakter serupa."""
    return unicodedata.normalize("NFKC", teks)


def bersihkan_teks(teks: str) -> str:
    """Lowercase, buang angka dan tanda baca, rapikan spasi."""
    teks = normalisasi_unicode(teks)      # \u2026 -> ..., \u00a0 -> spasi
    teks = teks.lower()                   # huruf besar -> kecil
    teks = re.sub(r"[^a-z\s]", " ", teks)  # angka & tanda baca -> spasi
    teks = re.sub(r"\s+", " ", teks)      # banyak spasi -> satu spasi
    return teks.strip()


print("=== 1. Pembersihan: sebelum -> sesudah ===")
for i, mentah in enumerate(kalimat_mentah, start=1):
    bersih = bersihkan_teks(mentah)
    print(f"{i}. {mentah!r}")
    print(f"   menjadi -> {bersih!r}")
print()

# ---------- 2. Stop words: dengan vs tanpa perlindungan negasi ----------
STOP_WORDS = {
    "di", "ke", "dari", "pada", "dalam", "ini", "itu",
    "dan", "atau", "yang", "dengan",
}
NEGASI = {"tidak", "bukan", "jangan", "belum"}


def buang_stop_words(teks: str, lindungi_negasi: bool) -> str:
    """Buang stop words; negasi ikut terbuang bila tidak dilindungi."""
    stop = STOP_WORDS if lindungi_negasi else STOP_WORDS | NEGASI
    return " ".join(k for k in teks.split() if k not in stop)


uji_sentimen = "kopi ini tidak enak"
print("=== 2. Stop words pada:", repr(uji_sentimen), "===")
print("negasi dilindungi :", repr(buang_stop_words(uji_sentimen, True)))
print("negasi dibuang    :", repr(buang_stop_words(uji_sentimen, False)))
print()

# ---------- 3. Stemming sederhana: awalan dan akhiran ----------
AWALAN = ("mem", "men", "ber", "ter", "peng")
AKHIRAN = ("nya", "kan", "an")


def stem_sederhana(kata: str) -> str:
    """Lepas satu akhiran lalu satu awalan (hasil minimal 3 huruf)."""
    for akhiran in AKHIRAN:
        if kata.endswith(akhiran) and len(kata) - len(akhiran) >= 3:
            kata = kata[: -len(akhiran)]
            break
    for awalan in AWALAN:
        if kata.startswith(awalan) and len(kata) - len(awalan) >= 3:
            kata = kata[len(awalan):]
            break
    return kata


kata_uji = ["membeli", "membaca", "makanan", "tiketnya", "bermain", "menonton"]
print("=== 3. Stemming sederhana ===")
for kata in kata_uji:
    print(f"  {kata:10s} -> {stem_sederhana(kata)}")

Output yang Dihasilkan:

text
=== 1. Pembersihan: sebelum -> sesudah ===
1. 'Siti membeli 3 kg jeruk di Pasar Baru!!!'
   menjadi -> 'siti membeli kg jeruk di pasar baru'
2. 'Filmnya bagus… tapi tiketnya Rp75.000.'
   menjadi -> 'filmnya bagus tapi tiketnya rp'
3. 'Kopi\xa0ini\xa0tidak\xa0enak.'
   menjadi -> 'kopi ini tidak enak'
4. 'Budi menonton konser musik pada hari Sabtu.'
   menjadi -> 'budi menonton konser musik pada hari sabtu'
5. 'Harga naik 12,5% dalam sepekan.'
   menjadi -> 'harga naik dalam sepekan'

=== 2. Stop words pada: 'kopi ini tidak enak' ===
negasi dilindungi : 'kopi tidak enak'
negasi dibuang    : 'kopi enak'

=== 3. Stemming sederhana ===
  membeli    -> beli
  membaca    -> baca
  makanan    -> makan
  tiketnya   -> tiket
  bermain    -> main
  menonton   -> onton

Mari kita telaah kode di atas baris per baris:

  1. Baris 22-24 (normalisasi_unicode): fungsi memanggil unicodedata.normalize("NFKC", teks) sehingga \u00a0 menjadi spasi biasa dan … menjadi tiga titik. Tanpa langkah ini, kata “ini” pada kalimat ketiga akan menempel pada kata sebelumnya.
  2. Baris 27-33 (bersihkan_teks): empat langkah berurutan — normalisasi, lower(), penggantian karakter non-huruf menjadi spasi, perapian spasi. Pola regex [^a-z\s] berarti “semua karakter yang bukan huruf kecil a–z dan bukan spasi” dijatuhkan; dokumentasi modul re merinci sintaks pola tersebut.
  3. Baris 37-40: perulangan mencetak pasangan sebelum/sesudah dengan repr() agar karakter tak terlihat seperti \xa0 tetap terbaca di terminal.
  4. Baris 44-54 (buang_stop_words): parameter lindungi_negasi mengatur apakah himpunan NEGASI ikut dibuang. Union STOP_WORDS | NEGASI menggabungkan kedua himpunan saat negasi tidak dilindungi.
  5. Baris 68-78 (stem_sederhana): satu akhiran dilepas lebih dulu, lalu satu awalan; syarat panjang minimal tiga huruf mencegah kata pendek terpotong habis.

Perhatikan dua jejak pada keluaran bagian pertama: “Rp75.000” menyisakan kata “rp” tanpa angka, dan “12,5%” hilang seluruhnya. Pembersihan berbasis whitelist huruf memang sederhana, tetapi kita perlu memeriksa hasilnya — untuk tugas yang peduli harga, angka justru penting dan pola pembersihnya harus menahannya.

Stop Words: Kapan Membuang, Kapan Menyimpan #

Stop words adalah kata yang muncul hampir di semua kalimat (“di”, “yang”, “dan”) sehingga hampir tidak memilahkan dokumen. Untuk pencarian kata kunci dan topic modeling, membuang stop words memangkas noise dengan aman. Keluaran bagian kedua menunjukkan kasus yang berbeda: kalimat “kopi ini tidak enak” berarti negatif, tetapi ketika kata “tidak” ikut dibuang sebagai stop word, kalimat berubah menjadi “kopi enak” — makna berbalik arah persis sebelum masuk model sentimen.

Stemming dan Lemmatization #

Stemming memotong imbuhan dengan aturan tanpa memeriksa kamus, sedangkan lemmatization mengembalikan kata ke bentuk kamus (lemma) dengan bantuan informasi kelas kata — lebih akurat tetapi butuh sumber daya yang lebih berat. Morfologi Indonesia menuntut keduanya bekerja ekstra: awalan me- berubah bentuk menjadi mem-, men-, meny- tergantung huruf awal kata dasar (“membeli” dari “beli”, “menonton” dari “tonton”), dan akhiran -kan, -an, -nya menempel pada ribuan kata.

Keluaran bagian ketiga menunjukkan keduanya sekaligus: lima kata pertama berhasil (“membeli” → “beli”, “makanan” → “makan”), tetapi “menonton” menjadi “onton” karena aturan sederhana kita melepas “men” secara mentah tanpa mengetahui asimilasi me- + tonton. Pustaka Sastrawi mengemas aturan asimilasi Indonesia secara lengkap untuk kebutuhan produksi.

Teks kini bersih dan seragam, tetapi masih berupa satu untaian karakter yang belum bisa diproses model. Langkah berikutnya adalah memecah untaian itu menjadi unit terkecil yang dapat dihitung — token — dengan tiga tingkat granularitas yang berbeda konsekuensinya.