Teks yang sudah bersih masih berupa untaian karakter, sedangkan model memproses satuan yang jauh lebih terstruktur. Token adalah satuan tersebut: potongan teks yang diperlakukan sebagai satu kesatuan oleh model — bisa satu kata, potongan kata, atau satu karakter. Keputusan granularitas token menentukan tiga hal sekaligus: ukuran vocabulary, seberapa sering model bertemu kata di luar kamus (OOV, out-of-vocabulary), dan panjang sekuens yang harus diurai. Chapter ini membangun ketiga jenis tokenizer pada kalimat bahasa Indonesia dan membandingkan konsekuensinya, termasuk mengapa tokenizer subword ala BPE menjadi standar model modern.
Token Adalah Satuan Model #
Model NLP tidak menghitung frekuensi huruf atau menghafal string; model menghitung token. Ketika sebuah kalimat dipecah menjadi enam token, model memproses enam satuan berurutan — dan inilah bentuk data sekuens yang sudah kita kenal pada section Sequence Modeling . Tiga granularitas yang lazim dipakai menghasilkan token yang sangat berbeda dari kalimat yang sama:
Diagram di atas memperlihatkan trade-off dasarnya: makin halus granularitasnya, makin pendek vocabulary tetapi makin panjang sekuens yang harus diproses model. Tabel berikut merangkum ketiganya:
| Granularitas | Contoh “memakan” | Ukuran vocabulary | Risiko OOV | Panjang sekuens |
|---|---|---|---|---|
| Word | memakan | sangat besar (ratusan ribu) | tinggi | pendek |
| Subword | mem + akan | sedang (30-50 ribu) | rendah | sedang |
| Karakter | m + e + m + a + k + a + n | kecil (puluhan) | tidak ada | sangat panjang |
Word Tokenizer dengan Regex #
Tokenizer word-level tampak sederhana — cukup pisahkan spasi — tetapi bahasa Indonesia punya dua jebakan: tanda baca yang menempel pada kata (“enak,” dan “enak” harus menjadi token yang sama) dan kata hubung yang sah (“sehari-hari”, “anak-anak”) yang tidak boleh terpotong. Pola regex [a-z]+(?:-[a-z]+)* menangkap keduanya: satu blok huruf boleh diikuti segmen hubung berulang. Program berikut menerapkannya bersama dua granularitas lainnya:
"""Tokenization tiga granularitas: word, subword, dan karakter.
(1) Word tokenizer berbasis regex yang menjaga kata hubung (sehari-hari)
dan memisahkan tanda baca, (2) character tokenizer sebagai batas bawah
granularitas, (3) subword tokenizer greedy longest-match yang meniru
perilaku BPE: kata tak dikenal dipecah, karakter jadi cadangan terakhir.
"""
import re
# ---------- 1. Word tokenizer: huruf + kata hubung ----------
POLA_KATA = re.compile(r"[a-z]+(?:-[a-z]+)*")
def tokenisasi_kata(teks: str) -> list[str]:
"""Ambil kata utuh termasuk bentuk hubung seperti sehari-hari."""
return POLA_KATA.findall(teks.lower())
kalimat_uji = [
"Anak-anak bermain sehari-hari di halaman sekolah.",
"Kopi ini enak, tetapi harganya Rp25.000!",
"Buku itu dibeli Siti di toko dekat stasiun.",
]
print("=== 1. Tokenization level kata ===")
for kalimat in kalimat_uji:
token = tokenisasi_kata(kalimat)
print(f"{kalimat!r}")
print(f" -> {len(token)} token: {token}")
print()
# ---------- 2. Character tokenizer: batas bawah granularitas ----------
contoh = "kopi enak"
def tokenisasi_karakter(teks: str) -> list[str]:
"""Pecah menjadi karakter; spasi diberi simbol _ agar tetap terhitung."""
return list(teks.replace(" ", "_"))
token_char = tokenisasi_karakter(contoh)
print("=== 2. Tokenization level karakter ===")
print(f"{contoh!r} -> {len(token_char)} token:")
print(" ", token_char)
print()
# ---------- 3. Subword tokenizer: greedy longest-match (mini BPE) ----------
KAMUS_SUBWORD = {
"anak", "ber", "main", "se", "hari", "pe", "cah",
"mem", "akan", "makan", "an", "jalan", "kata", "di",
}
def tokenisasi_subword(kata: str, kamus: set[str]) -> list[str]:
"""Cocokkan prefiks terpanjang; karakter menjadi cadangan terakhir."""
hasil = []
sisa = kata
while sisa:
cocok = False
for panjang in range(len(sisa), 0, -1):
if sisa[:panjang] in kamus:
hasil.append(sisa[:panjang])
sisa = sisa[panjang:]
cocok = True
break
if not cocok:
hasil.append(sisa[0]) # tak ada prefiks cocok: 1 karakter
sisa = sisa[1:]
return hasil
kata_uji = ["bermain", "pecah", "memakan", "makanan", "memecah"]
print("=== 3. Tokenization level subword (greedy longest-match) ===")
for kata in kata_uji:
potongan = tokenisasi_subword(kata, KAMUS_SUBWORD)
print(f" {kata:9s} -> {' + '.join(potongan)} ({len(potongan)} token)")Output yang Dihasilkan:
=== 1. Tokenization level kata ===
'Anak-anak bermain sehari-hari di halaman sekolah.'
-> 6 token: ['anak-anak', 'bermain', 'sehari-hari', 'di', 'halaman', 'sekolah']
'Kopi ini enak, tetapi harganya Rp25.000!'
-> 6 token: ['kopi', 'ini', 'enak', 'tetapi', 'harganya', 'rp']
'Buku itu dibeli Siti di toko dekat stasiun.'
-> 8 token: ['buku', 'itu', 'dibeli', 'siti', 'di', 'toko', 'dekat', 'stasiun']
=== 2. Tokenization level karakter ===
'kopi enak' -> 9 token:
['k', 'o', 'p', 'i', '_', 'e', 'n', 'a', 'k']
=== 3. Tokenization level subword (greedy longest-match) ===
bermain -> ber + main (2 token)
pecah -> pe + cah (2 token)
memakan -> mem + akan (2 token)
makanan -> makan + an (2 token)
memecah -> mem + e + cah (3 token)Mari kita telaah kode di atas baris per baris:
- Baris 12 (
POLA_KATA): pola regex dibuat sekali di luar fungsi agar tidak dikompilasi ulang setiap pemanggilan. Grup non-pencatat(?:-[a-z]+)*mengizinkan nol atau beberapa segmen hubung, sehingga “sehari-hari” tetap utuh sementara tanda baca lain diabaikan karena tidak pernah cocok dengan pola. - Baris 15-17 (
tokenisasi_kata):teks.lower()menyamakan besar huruf lebih dulu, lalufindall()mengembalikan semua substring yang cocok dengan pola. Tanda baca tidak perlu dihapus secara eksplisit — pola hanya menangkap huruf, sehingga koma dan tanda seru jatuh dengan sendirinya. - Baris 37-39 (
tokenisasi_karakter): spasi diganti simbol_sebelum dipecah agar pemisah antarkata tetap menjadi token yang bisa dipelajari, bukan sekadar dihilangkan. - Baris 55-70 (
tokenisasi_subword): perulanganpanjangdari panjang sisa turun ke satu mencari prefiks terpanjang yang ada di kamus. Bila tidak ada prefiks yang cocok sama sekali, satu karakter diambil sebagai cadangan — mekanisme inilah yang membuat subword tokenizer tidak pernah gagal total pada kata asing.
Keluaran bagian pertama juga memperlihatkan satu keterbatasan semantik word tokenizer: kata “di” sebagai preposisi (“di toko”) dan awalan di- pada “dibeli” sama-sama menjadi token utuh yang berbeda. Pemisahan makna semacam itu bukan pekerjaan tokenizer — tokenizer hanya memotong bentuk, sedangkan konteks akan dibedah oleh model itu sendiri.
Subword ala BPE dan Karakter sebagai Cadangan #
Keluaran bagian ketiga adalah inti dari pendekatan subword. Kamus kecil berisi potongan seperti “ber”, “mem”, “akan”, “makan”: kata berimbuhan seperti “bermain” dan “memakan” terpecah menjadi potongan yang bermakna, dan potongan yang sama dipakai ulang lintas kata — “makan”, “makanan”, dan “memakan” berbagi unit “makan” dan “akan”. Tokenizer nyata seperti BPE (byte pair encoding) membangun kamus tersebut secara otomatis: algoritme mulai dari karakter, lalu berulang kali menggabungkan pasangan yang paling sering muncul di korpus hingga ukuran kamus tercapai.
Hasil “memecah” → mem + e + cah memperlihatkan perilaku penting lain: greedy longest-match tidak selalu menemukan pemecahan terbaik (“me + mem”? tidak ada di kamus), sehingga sisa yang tak tercakup jatuh ke karakter. Tokenizer BPE nyata menangani ini dengan byte fallback — setiap byte selalu punya ID sehingga kata apa pun tetap bisa di-tokenisasi. Pustaka tokenizers dari Hugging Face
menyediakan implementasi BPE, WordLevel, dan Unigram yang siap pakai dengan dukungan pelatihan dari korpus.
Kelima keluaran di atas masih berupa string. Model membutuhkan bilangan bulat — ID unik untuk setiap token — agar bisa mengindeks tabel embedding. Pemetaan sistematis dari token ke bilangan itulah yang kita bangun pada chapter berikutnya.