LLM memproses teks lewat tokenizer subword seperti BPE dengan ratusan ribu token; model mini kita menurunkan skala tersebut menjadi tokenisasi per karakter dengan 25 token. Konsekuensinya sederhana namun fundamental: satu kalimat menjadi rangkaian token yang lebih panjang, dan loss dihitung per posisi karakter alih-alih per kata. Bagian ini membahas tiga fungsi di src/model_bahasa.py yang mengubah data/korpus.txt menjadi tensor masukan dan target — pembahasan level karakter ini adalah padanan langsung dari chapter Tokenization untuk LLM .

Dari Teks ke Indeks Token #

Dua fungsi pertama membangun kamus dua arah antara karakter dan indeks:

python src/model_bahasa.py — kosakata dan enkode/dekode
def buat_kosakata(kalimat_latih: list[str]) -> tuple[dict, list]:
    """Kosakata karakter dari seluruh teks latih (urutan terurut stabil)."""
    huruf = sorted(set("".join(kalimat_latih)))
    sto = {h: i for i, h in enumerate(huruf)}
    return sto, huruf

def enkode(teks: str, sto: dict) -> list[int]:
    return [sto[h] for h in teks if h in sto]

def dekode(indeks: list[int], huruf: list[str]) -> str:
    return "".join(huruf[i] for i in indeks)

Fungsi buat_kosakata memakai sorted(set(...)) sehingga urutan token stabil antar eksekusi — indeks 0 selalu karakter spasi, bukan bergantung pada urutan kemunculan. Fungsi enkode menyaring karakter yang tidak ada di kamus (if h in sto); karena kosakata dibangun dari kalimat terpisah per baris, karakter baris baru tidak punya token dan otomatis hilang saat kalimat-kalimat dirangkai menjadi satu stream oleh gabung_stream. Jendela latih karena itu boleh melintasi batas kalimat — perilaku yang lazim juga pada pra-latih LLM yang memotong korpus besar menjadi potongan berukuran tetap. Penyaringan if h in sto sekaligus menjadi penjaga masukan baru: teks dengan huruf asing (misalnya “é”) tidak menggugat kamus, karakter tak dikenalnya hanya dilewati — padanan sederhana dari masalah token di luar kosakata (OOV) pada tokenizer nyata yang dibahas pada chapter Vocabulary dan Token ID .

Jendela Latih dengan Target Bergeser Satu #

Fungsi susun_jendela memotong stream token menjadi pasangan (masukan, target):

python src/model_bahasa.py — penyusun jendela
def susun_jendela(token: list[int], panjang: int = 32, langkah: int = 16):
    """Potong satu stream token menjadi jendela (masukan, target) geser 1.

    Setiap jendela berisi `panjang` token masukan dan `panjang` token
    target yang bergeser satu posisi (tugas memprediksi karakter berikut).
    """
    daftar_x, daftar_y = [], []
    for awal in range(0, len(token) - panjang, langkah):
        daftar_x.append(token[awal:awal + panjang])
        daftar_y.append(token[awal + 1:awal + 1 + panjang])
    if not daftar_x:  # stream terlalu pendek: pakai satu jendela penuh
        return torch.tensor([], dtype=torch.long).view(0, 0)
    return (torch.tensor(daftar_x, dtype=torch.long),
            torch.tensor(daftar_y, dtype=torch.long))

Mari kita telaah fungsi di atas baris per baris:

  1. token[awal:awal + panjang]: mengambil 32 token masukan mulai posisi awal — konteks yang dibaca model.
  2. token[awal + 1:awal + 1 + panjang]: target adalah potongan yang sama digeser satu posisi, sehingga pada setiap langkah t model belajar memprediksi karakter di posisi t+1 — persis tugas next-token prediction.
  3. langkah=16: jendela berikutnya mulai 16 token kemudian, sehingga dua jendela bertetangga tumpang-tindih setengah panjangnya; korpus kecil menghasilkan lebih banyak sampel tanpa menambah teks.
  4. Penjaga stream pendek: bila stream lebih pendek dari jendela, fungsi mengembalikan tensor kosong berbentuk (0, 0) agar pemanggil tidak menerima galat indeks.

Menjalankan python3 src/model_bahasa.py mencetak hasil penyusunan jendela beserta anatomi model:

text
jendela pra-latih        : 46 (panjang 32, langkah 16)
loss pra-latih ep1       : 3.2306
loss pra-latih ep60      : 2.1119
ppl pra-latih ep60       : 8.16
== demo selesai; skrip lain memakai modul ini ==

Subset umum menghasilkan 46 jendela; subset kopi latih menghasilkan 17 jendela, dan dua kalimat validasi kopi menghasilkan 3 jendela — angka-angka yang tercetak ulang oleh src/evaluasi.py pada Bagian 4. Loss turun dari 3.2306 ke 2.1119 setelah 60 epoch demo, pertanda arsitektur dan format data sudah benar sebelum kita melangkah ke pra-latih penuh.

Format data selesai: tiga subset telah menjadi jendela 32 token dengan target geser satu. Bagian berikutnya membekukan model dasar hasil pra-latih lalu menempelkan adapter LoRA pada lapisan terberatnya.