Arsitektur dari Bagian 2 belum berharga sebelum bobotnya dilatih. Bagian ini menjalankan loop training lengkap: data dibagi menjadi 240 latih dan 60 uji, vocabulary dibangun hanya dari data latih, model dioptimasi dengan Adam, dan akurasi diukur pada data uji yang tidak pernah ikut pelatihan. Seluruh sumber acakan dikunci seed tetap sehingga setiap eksekusi menghasilkan kurva loss yang identik — praktik yang dibahas chapter Reproducibility .

Keputusan Desain Training #

Empat keputusan pada latih.py menentukan kualitas pelatihan:

KeputusanPilihanAlasan
Pembagian data240/60 dengan random.Random(42)proporsi 80/20, acakan dapat direproduksi
Vocabularydari data latih sajadata uji tidak boleh bocor ke kamus
OptimizerAdam, learning rate 0.05model kecil konvergen cepat tanpa penyetelan
Batchfull-batch (240 sekaligus)data kecil; gradien stabil; tanpa DataLoader

Membangun vocabulary dari data latih saja adalah bentuk disiplin yang sering terlewat: kalau token data uji ikut dihitung frekuensinya, informasi data uji telah memengaruhi pipeline sebelum evaluasi — pola kebocoran data yang dibedah pada chapter Data Leakage .

Loop Training Lengkap #

python Loop training (src/latih.py)
"""Training dan evaluasi model sentimen ulasan produk.

Alur latih.py:
1. Muat data/ulasan.csv (300 ulasan tiga kelas).
2. Bagi 240 latih / 60 uji dengan acakan ber-seed tetap.
3. Bangun vocabulary dari data latih saja.
4. Latih ModelSentimen (embedding + mean pooling + linear) full-batch Adam.
5. Laporkan akurasi latih dan uji tiap interval epoch.
"""

import random

import torch
import torch.nn as nn

from model import ModelSentimen, hitung_parameter, peluang_kelas
from preprocessing import (
    bangun_vocab,
    muat_ulasan,
    panjang_maksimum,
    teks_ke_ids,
)

SEED = 42
JUMLAH_LATIH = 240
JUMLAH_EPOCH = 40
NAMA_KELAS = ["negatif", "netral", "positif"]


def bagi_latih_uji(data, jumlah_latih=JUMLAH_LATIH, seed=SEED):
    """Acak data dengan seed tetap lalu potong menjadi latih dan uji."""
    salinan = list(data)
    random.Random(seed).shuffle(salinan)
    return salinan[:jumlah_latih], salinan[jumlah_latih:]


def latih_model(cetak=True):
    """Latih model dari data CSV; kembalikan model, vocab, data uji.

    Seluruh sumber acakan (pembagian data, inisialisasi bobot) dikunci
    seed tetap sehingga hasil identik pada setiap eksekusi.
    """
    data = muat_ulasan()
    latih, uji = bagi_latih_uji(data)

    teks_latih = [t for t, _ in latih]
    label_ke_id = {nama: i for i, nama in enumerate(NAMA_KELAS)}
    vocab = bangun_vocab(teks_latih)
    pm = panjang_maksimum(teks_latih)

    X_latih = torch.tensor([teks_ke_ids(t, vocab, pm) for t in teks_latih])
    y_latih = torch.tensor([label_ke_id[l] for _, l in latih])
    X_uji = torch.tensor([teks_ke_ids(t, vocab, pm) for t, _ in uji])
    y_uji = torch.tensor([label_ke_id[l] for _, l in uji])

    torch.manual_seed(SEED)
    model = ModelSentimen(jumlah_vocab=len(vocab))
    criterion = nn.CrossEntropyLoss()
    optimizer = torch.optim.Adam(model.parameters(), lr=0.05)

    if cetak:
        print(f"data latih {len(latih)} | data uji {len(uji)} | "
              f"vocab {len(vocab)} | panjang maks {pm} | "
              f"parameter {hitung_parameter(model)}")

    for epoch in range(1, JUMLAH_EPOCH + 1):
        model.train()
        optimizer.zero_grad()
        logits = model(X_latih)
        loss = criterion(logits, y_latih)
        loss.backward()
        optimizer.step()

        if cetak and (epoch == 1 or epoch % 10 == 0):
            model.eval()
            with torch.no_grad():
                benar = (model(X_uji).argmax(dim=1) == y_uji).sum().item()
            print(f"epoch {epoch:3d} | loss latih {loss.item():.4f} | "
                  f"akurasi uji {benar / len(uji):.3f}")

    model.eval()
    return model, vocab, pm, uji


if __name__ == "__main__":
    model, vocab, pm, uji = latih_model()
    label_ke_id = {nama: i for i, nama in enumerate(NAMA_KELAS)}

    with torch.no_grad():
        benar_per_kelas = [0, 0, 0]
        for teks, label in uji:
            ids = torch.tensor([teks_ke_ids(teks, vocab, pm)])
            prediksi = model(ids).argmax(dim=1).item()
            if prediksi == label_ke_id[label]:
                benar_per_kelas[prediksi] += 1

    print(f"\nakurasi uji akhir : {sum(benar_per_kelas) / len(uji):.3f}")
    print("benar per kelas   :",
          {n: b for n, b in zip(NAMA_KELAS, benar_per_kelas)})

Skrip di atas menghasilkan keluaran berikut saat dijalankan di mesin uji (CPU, PyTorch 2.14):

Output yang Dihasilkan:

text
data latih 240 | data uji 60 | vocab 90 | panjang maks 7 | parameter 3459
epoch   1 | loss latih 1.1085 | akurasi uji 0.517
epoch  10 | loss latih 0.0080 | akurasi uji 0.983
epoch  20 | loss latih 0.0000 | akurasi uji 1.000
epoch  30 | loss latih 0.0000 | akurasi uji 1.000
epoch  40 | loss latih 0.0000 | akurasi uji 1.000

akurasi uji akhir : 1.000
benar per kelas   : {'negatif': 16, 'netral': 25, 'positif': 19}

Mari kita telaah kode di atas baris per baris:

  1. Baris 30-35 (bagi_latih_uji): salinan data diacak dengan generator ber-seed 42 lalu dipotong 240/60. Karena random.Random dibuat lokal, pemanggilan fungsi tidak mengubah status acakan global program.
  2. Baris 37-59 (latih_model, bagian siapan): seluruh pipeline dirangkum satu fungsi — muat CSV, bagi data, bangun vocabulary, ubah teks menjadi tensor, inisialisasi model dengan torch.manual_seed(SEED) pada baris 56, lalu susun criterion dan optimizer. Parameter cetak memungkinkan prediksi.py memanggil fungsi yang sama tanpa log epoch.
  3. Baris 66-79 (loop epoch): pola zero_grad -> forward -> loss -> backward -> step persis chapter Training Loop ; evaluasi uji hanya dilakukan pada epoch ke-1 dan tiap kelipatan 10 agar keluaran ringkas.
  4. Baris 85-99 (__main__): setelah pelatihan, model dipakai satu per satu pada 60 teks uji untuk menghitung jumlah benar per kelas — netral paling banyak (25) karena templatnya paling panjang dan khas.

Membaca Hasil Pelatihan #

Loss latih jatuh dari 1.1085 pada epoch pertama menjadi 0.0080 pada epoch ke-10, lalu menyentuh 0.0000 — model menyelesaikan seluruh 240 contoh latih. Akurasi uji naik dari 0.517 ke 0.983 pada epoch ke-10 dan mencapai 1.000 sejak epoch ke-20: seluruh 60 ulasan held-out terklasifikasi benar, terbagi 16 negatif, 25 netral, dan 19 positif.

Model terlatih dan metriknya terukur, tetapi proyek belum selesai: nilai sebenarnya dari pengklasifikasi teks adalah kemampuannya menangani teks yang belum pernah dilihat. Bagian berikutnya memakai model ini sebagai CLI prediksi.