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:
| Keputusan | Pilihan | Alasan |
|---|---|---|
| Pembagian data | 240/60 dengan random.Random(42) | proporsi 80/20, acakan dapat direproduksi |
| Vocabulary | dari data latih saja | data uji tidak boleh bocor ke kamus |
| Optimizer | Adam, learning rate 0.05 | model kecil konvergen cepat tanpa penyetelan |
| Batch | full-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 #
"""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:
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:
- Baris 30-35 (
bagi_latih_uji): salinan data diacak dengan generator ber-seed 42 lalu dipotong 240/60. Karenarandom.Randomdibuat lokal, pemanggilan fungsi tidak mengubah status acakan global program. - Baris 37-59 (
latih_model, bagian siapan): seluruh pipeline dirangkum satu fungsi — muat CSV, bagi data, bangun vocabulary, ubah teks menjadi tensor, inisialisasi model dengantorch.manual_seed(SEED)pada baris 56, lalu susun criterion dan optimizer. Parametercetakmemungkinkanprediksi.pymemanggil fungsi yang sama tanpa log epoch. - Baris 66-79 (loop epoch): pola
zero_grad -> forward -> loss -> backward -> steppersis chapter Training Loop ; evaluasi uji hanya dilakukan pada epoch ke-1 dan tiap kelipatan 10 agar keluaran ringkas. - 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.