Model neural network hanya bekerja dengan bilangan, sehingga daftar token yang kita peroleh dari tokenizer masih harus diubah menjadi angka. Vocabulary adalah kamus yang memetakan setiap token unik ke satu bilangan bulat — token ID — dan kamus inilah yang menjadi titik temu antara dunia teks dan dunia tensor. Cara kita menyusun kamus tersebut menentukan bagaimana kata asing diperlakukan, bagaimana kalimat berbeda panjang disatukan dalam satu batch, dan indeks mana yang nanti dipakai tabel embedding. Chapter ini membangun vocabulary dari korpus mini 8 kalimat, lalu menjalankan seluruh rantai: encode, tangani OOV, dan padding.

Membangun Vocabulary dari Korpus #

Aturan pembuatannya sederhana: kumpulkan semua token unik dari korpus, urutkan, lalu beri nomor mulai dari nol. Pengurutan abjad membuat kamus deterministik — dua orang yang membangun vocabulary dari korpus yang sama akan mendapat ID yang identik. Empat slot pertama disisakan untuk token khusus yang bukan berasal dari korpus, melainkan dari kebutuhan teknis model:

TokenIDPeran
<pad>0mengisi posisi kosong saat sekuens di-pad agar sejajar
<unk>1pengganti token di luar vocabulary (OOV)
<sos>2penanda awal sekuens (start of sequence)
<eos>3penanda akhir sekuens (end of sequence)

Program berikut menerapkan seluruh rantai tersebut pada korpus delapan kalimat bertema kedai minuman:

python
"""Membangun vocabulary dari korpus 8 kalimat, mengubah kalimat menjadi
token ID, menangani OOV dengan <unk>, dan menyetarakan panjang dengan <pad>.

Alur: token khusus diberi ID paling awal (0-3), kata korpus diurutkan sesuai
abjad lalu diberi ID berikutnya. Dua kalimat di-encode (satu memuat kata
OOV) lalu di-pad ke panjang seragam T = 7.
"""

KORPUS = [
    "saya suka kopi pagi",
    "kopi ini enak sekali",
    "teh manis itu mahal",
    "kami minum kopi bersama",
    "harga teh naik kemarin",
    "kopi pagi itu menyegarkan",
    "saya beli teh manis",
    "kami suka teh dingin",
]

TOKEN_KHUSUS = ["<pad>", "<unk>", "<sos>", "<eos>"]


def build_vocab(daftar_kalimat: list[str]) -> dict[str, int]:
    """Kamus token -> ID; token khusus lebih dulu, kata urut abjad."""
    kata_unik = set()
    for kalimat in daftar_kalimat:
        kata_unik.update(kalimat.split())
    vocab = {token: i for i, token in enumerate(TOKEN_KHUSUS)}
    for kata in sorted(kata_unik):
        vocab[kata] = len(vocab)
    return vocab


def encode(kalimat: str, vocab: dict[str, int]) -> list[int]:
    """Kata -> ID (OOV menjadi <unk>), dibingkai <sos> dan <eos>."""
    isi = [vocab.get(kata, vocab["<unk>"]) for kata in kalimat.split()]
    return [vocab["<sos>"]] + isi + [vocab["<eos>"]]


def pad_token(ids: list[int], panjang: int, id_pad: int = 0) -> list[int]:
    """Potong bila kepanjangan, tambahkan <pad> bila kurang."""
    return ids[:panjang] + [id_pad] * (panjang - len(ids))


# ---------- 1. Vocabulary ----------
vocab = build_vocab(KORPUS)
id_ke_token = {i: t for t, i in vocab.items()}

print("=== 1. Vocabulary dari 8 kalimat korpus ===")
print("ukuran vocab:", len(vocab), "(4 token khusus +", len(vocab) - 4, "kata)")
for kata, i in vocab.items():
    print(f"  {kata:14s} -> {i}")
print()

# ---------- 2. Encode dua kalimat (satu mengandung OOV) ----------
kalimat_a = "teh manis enak"
kalimat_b = "saya pesan kopi dingin"  # "pesan" tidak ada di korpus

print("=== 2. Encode dua kalimat ===")
for kalimat in (kalimat_a, kalimat_b):
    ids = encode(kalimat, vocab)
    nama = " ".join(id_ke_token[i] for i in ids)
    print(f"{kalimat!r}")
    print(f"  id    : {ids}")
    print(f"  token : {nama}")
print()

# ---------- 3. Padding ke panjang seragam ----------
T = 7
print("=== 3. Padding ke panjang seragam T =", T, "===")
for kalimat in (kalimat_a, kalimat_b):
    ids = pad_token(encode(kalimat, vocab), T)
    nama = " ".join(id_ke_token[i] for i in ids)
    print(f"  {ids}")
    print(f"  {nama}")

Output yang Dihasilkan:

text
=== 1. Vocabulary dari 8 kalimat korpus ===
ukuran vocab: 24 (4 token khusus + 20 kata)
  <pad>          -> 0
  <unk>          -> 1
  <sos>          -> 2
  <eos>          -> 3
  beli           -> 4
  bersama        -> 5
  dingin         -> 6
  enak           -> 7
  harga          -> 8
  ini            -> 9
  itu            -> 10
  kami           -> 11
  kemarin        -> 12
  kopi           -> 13
  mahal          -> 14
  manis          -> 15
  menyegarkan    -> 16
  minum          -> 17
  naik           -> 18
  pagi           -> 19
  saya           -> 20
  sekali         -> 21
  suka           -> 22
  teh            -> 23

=== 2. Encode dua kalimat ===
'teh manis enak'
  id    : [2, 23, 15, 7, 3]
  token : <sos> teh manis enak <eos>
'saya pesan kopi dingin'
  id    : [2, 20, 1, 13, 6, 3]
  token : <sos> saya <unk> kopi dingin <eos>

=== 3. Padding ke panjang seragam T = 7 ===
  [2, 23, 15, 7, 3, 0, 0]
  <sos> teh manis enak <eos> <pad> <pad>
  [2, 20, 1, 13, 6, 3, 0]
  <sos> saya <unk> kopi dingin <eos> <pad>

Mari kita telaah kode di atas baris per baris:

  1. Baris 23-30 (build_vocab): himpunan kata_unik menghilangkan duplikasi otomatis karena set menyimpan nilai unik; sorted() mengurutkan hasilnya sehingga ID stabil antar eksekusi. Kamus dibangun dari TOKEN_KHUSUS lebih dulu dengan enumerate, lalu setiap kata baru mengambil nomor len(vocab) yang terus bertambah — pola umum pengisian dict bertahap.
  2. Baris 34-36 (encode): vocab.get(kata, vocab["<unk>"]) adalah inti penanganan OOV — kata yang tidak ditemukan dikembalikan sebagai ID 1, bukan galat. Bingkai <sos> dan <eos> ditambahkan di kedua ujung untuk tugas generatif yang perlu tahu di mana sekuens dimulai dan berakhir.
  3. Baris 40-41 (pad_token): penggalan ids[:panjang] memotong sekuens yang terlalu panjang, sedangkan [id_pad] * selisih menambah nol sampai panjang target tercapai. Kedua operasi berjalan amu karena ID <pad> sudah diseediakan di slot 0.
  4. Baris 46-47: kamus dibalik menjadi id_ke_token agar hasil encode bisa dibaca kembali sebagai nama token — proses decode untuk pemeriksaan visual.

OOV: Saat Kamus Tidak Cukup #

Keluaran bagian kedua memperlihatkan OOV secara nyata. Kata “pesan” tidak pernah muncul di korpus delapan kalimat, sehingga encode memetakan kata tersebut ke ID 1 (<unk>): kalimat “saya pesan kopi dingin” menjadi [2, 20, 1, 13, 6, 3]. Semua kata lain tetap terwakili, tetapi informasi kata “pesan” hilang total — model hanya tahu bahwa di posisi itu ada sesuatu yang asing.

Vocabulary berbasis frekuensi menawarkan jalan tengah: batasi kamus hanya ke K kata teringan (misalnya 30.000 kata pertama), lalu biarkan sisanya menjadi <unk>. Semakin besar ambang K, semakin sedikit OOV tetapi semakin besar tabel embedding yang harus ditraining. Tokenizer subword dari chapter sebelumnya menekan jumlah OOV jauh lebih drastis karena kata baru masih bisa dipecah menjadi potongan yang ada di kamus.

Padding dan Batch #

Tensor batch menuntut setiap sampel berpanjang sama, sebagaimana dibahas pada Data Sekuens . Keluaran bagian ketiga menunjukkan mekanismenya: kalimat “teh manis enak” (5 ID setelah bingkai) ditambah dua <pad>, sedangkan “saya pesan kopi dingin” (6 ID) ditambah satu <pad> — keduanya kini sejajar di panjang T = 7 dan siap ditumpuk menjadi tensor berbentuk (2, 7).

Token ID kini lengkap: kalimat apapun berubah menjadi barisan bilangan bulat yang seragam panjangnya. Tetapi ID 23 untuk “teh” dan ID 15 untuk “manis” hanyalah nomor indeks — dua kata yang mirip maknanya bisa mendapat nomor yang berjauhan, dan nomor 24 tidak secara otomatis lebih “besar” daripada nomor 13. Chapter berikutnya mengisi kekosongan ini dengan merepresentasikan setiap token sebagai vektor berdimensi rendah yang maknanya bisa dihitung: word embedding.