Chapter Embedding Layer menuntaskan cara mengubah satu kata menjadi vektor yang bisa dipelajari. Text classification naik satu tingkat: satu keseluruhan teks — ulasan, berita, email, tiket keluhan — harus dipetakan ke satu label. Tugas ini menjadi dasar filter spam, deteksi bahasa, kategorisasi berita, dan triase tiket layanan pelanggan. Tantangan teknisnya khas NLP: panjang teks bervariasi, sedangkan classifier linear mengharapkan vektor berdimensi tetap. Chapter ini merangkai seluruh rantai transformasi dari enam chapter pertama menjadi satu model PyTorch utuh yang dilatih dan diuji nyata.
Pipeline: Dari Teks ke Label #
Alur kerjanya berupa rantai lima tahap yang masing-masing sudah kita bahas tersendiri:
Tahap pertama dan kedua mengikuti chapter Tokenization
dan Vocabulary dan Token ID
: teks dipecah menjadi token lalu dipetakan ke id integer. Tahap ketiga mengambil embedding dari tabel yang dilatih — persis mekanisme chapter Embedding Layer
. Tahap keempat menjadi pemecah masalah panjang variabel: encoder merangkum T vektor kata menjadi satu vektor dokumen berdimensi tetap D, sebelum classifier linear dari section Dasar Neural Network
menghasilkan skor kelas.
Arsitektur Encoder yang Umum #
Pilihan encoder menentukan batas kemampuan model. Tabel berikut merangkum lima keluarga arsitektur yang paling sering dipakai:
| Encoder | Cara merangkum | Kekuatan | Batasan |
|---|---|---|---|
| Bag of Words | hitung kemunculan kata | paling sederhana, cepat | urutan kata terbuang |
| Mean pooling | rata-rata embedding kata | baseline kuat, minim parameter | urutan kata terbuang |
| RNN / LSTM | baca berurutan dengan hidden state | menangkap urutan | lambat pada teks panjang, gradien menyusut |
| CNN teks | filter 1D geser atas n-gram | menangkap frasa lokal | konteks jarak jauh terbatas |
| Transformer encoder | self-attention antar semua token | konteks panjang, standar saat ini | butuh data dan komputasi besar |
Model demo chapter ini memilih mean pooling karena paling mudah dijelaskan dan sering menjadi pembanding pertama sebelum arsitektur yang lebih berat. RNN dibahas tuntas di section Sequence Modeling , sementara transformer menanti di section Attention dan Transformer .
Demo: Klasifikasi 200 Ulasan Mini dengan PyTorch #
Berkas klasifikasi_teks.py membangun 200 ulasan sintetis dari pola templat — 100 positif dengan kata kunci seperti “bagus” dan “senang”, 100 negatif dengan “buruk” dan “kecewa” — lalu melatih model Embedding(32) -> mean pooling -> Linear(32 -> 2):
"""Klasifikasi teks end-to-end dengan PyTorch.
Pipeline: teks -> token -> vocabulary/token id -> embedding ->
mean pooling -> linear classifier. Dataset mini buatan 200 ulasan toko
daring (100 positif + 100 negatif) dari pola templat dengan kata kunci
jelas. Dibagi 160 latih / 40 uji. Model: Embedding(dim 32) ->
mean pooling -> Linear(32 -> 2), Adam full-batch 80 epoch.
"""
import random
import torch
import torch.nn as nn
torch.manual_seed(42)
acak = random.Random(42)
# ---------- 1. Dataset mini buatan: 100 positif + 100 negatif ----------
KATA_POSITIF = ["bagus", "mantap", "memuaskan", "nyaman", "enak", "awet"]
KATA_POSITIF_SUBJEK = ["senang", "puas", "terkesan"]
KATA_NEGATIF = ["buruk", "mengecewakan", "rusak", "lambat", "parah", "menyebalkan"]
KATA_NEGATIF_SUBJEK = ["kecewa", "kesal", "menyesal"]
TEMPLAT_POSITIF = [
"produk ini {kata} dan tahan lama",
"pengiriman sangat {kata}",
"kualitasnya {kata} sekali",
"pelayanan tokonya {kata}",
"saya {kata_subjek} dengan produk ini",
"harga sebanding dengan kualitas yang {kata}",
"belanja di toko ini {kata} sekali",
]
TEMPLAT_NEGATIF = [
"produk ini {kata} dan cepat rusak",
"pengiriman sangat {kata}",
"kualitasnya {kata} sekali",
"pelayanan tokonya {kata}",
"saya {kata_subjek} dengan produk ini",
"harga tidak sebanding dengan kualitas yang {kata}",
"paket tiba dalam kondisi {kata}",
]
def buat_dataset(jumlah_per_kelas):
data = []
for _ in range(jumlah_per_kelas):
kata = acak.choice(KATA_POSITIF)
kata_subjek = acak.choice(KATA_POSITIF_SUBJEK)
teks = acak.choice(TEMPLAT_POSITIF).format(kata=kata, kata_subjek=kata_subjek)
data.append((teks, 1)) # label 1 = positif
for _ in range(jumlah_per_kelas):
kata = acak.choice(KATA_NEGATIF)
kata_subjek = acak.choice(KATA_NEGATIF_SUBJEK)
teks = acak.choice(TEMPLAT_NEGATIF).format(kata=kata, kata_subjek=kata_subjek)
data.append((teks, 0)) # label 0 = negatif
acak.shuffle(data)
return data
data = buat_dataset(100)
latih, uji = data[:160], data[160:]
# ---------- 2. Vocabulary: dibangun dari data latih saja ----------
def tokenisasi(teks):
return teks.lower().split()
frekuensi = {}
for teks, _ in latih:
for token in tokenisasi(teks):
frekuensi[token] = frekuensi.get(token, 0) + 1
vocab = {"<pad>": 0, "<unk>": 1}
for token, jumlah in sorted(frekuensi.items(), key=lambda x: (-x[1], x[0])):
if jumlah >= 1:
vocab[token] = len(vocab)
PANJANG_MAKS = max(len(tokenisasi(teks)) for teks, _ in latih)
def ubah_ke_ids(teks):
ids = [vocab.get(t, vocab["<unk>"]) for t in tokenisasi(teks)]
return ids[:PANJANG_MAKS] + [0] * (PANJANG_MAKS - len(ids))
print(f"contoh latih {len(latih)} | contoh uji {len(uji)} | "
f"ukuran vocab {len(vocab)} | panjang maks {PANJANG_MAKS}")
print("contoh positif:", next(t for t, l in latih if l == 1))
print("contoh negatif:", next(t for t, l in latih if l == 0))
X_latih = torch.tensor([ubah_ke_ids(t) for t, _ in latih])
y_latih = torch.tensor([l for _, l in latih])
X_uji = torch.tensor([ubah_ke_ids(t) for t, _ in uji])
y_uji = torch.tensor([l for _, l in uji])
# ---------- 3. Model: embedding -> mean pooling -> linear ----------
class KlasifikasiTeks(nn.Module):
def __init__(self, jumlah_vocab, dim_embedding=32, jumlah_kelas=2):
super().__init__()
self.embedding = nn.Embedding(jumlah_vocab, dim_embedding, padding_idx=0)
self.classifier = nn.Linear(dim_embedding, jumlah_kelas)
def forward(self, token_ids):
vektor = self.embedding(token_ids) # (B, T, D)
mask = (token_ids != 0).unsqueeze(-1).float() # (B, T, 1)
jumlah_kata = mask.sum(dim=1).clamp(min=1.0) # (B, 1)
rata2 = (vektor * mask).sum(dim=1) / jumlah_kata # (B, D)
return self.classifier(rata2) # (B, 2) logits
model = KlasifikasiTeks(len(vocab))
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.05)
# ---------- 4. Training loop full-batch ----------
JUMLAH_EPOCH = 80
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 epoch == 1 or epoch % 20 == 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}")
# ---------- 5. Evaluasi akhir + prediksi kalimat baru ----------
model.eval()
with torch.no_grad():
akurasi_latih = (model(X_latih).argmax(dim=1) == y_latih).float().mean().item()
akurasi_uji = (model(X_uji).argmax(dim=1) == y_uji).float().mean().item()
print(f"\nakurasi akhir: latih {akurasi_latih:.3f} | uji {akurasi_uji:.3f}")
NAMA_KELAS = ["negatif", "positif"]
kalimat_baru = [
"kualitasnya bagus sekali dan tahan lama",
"pengiriman lambat dan paket rusak",
"pelayanan tokonya menyebalkan sekali",
"saya puas dengan produk ini",
]
print("\nprediksi kalimat baru:")
for teks in kalimat_baru:
with torch.no_grad():
peluang = torch.softmax(model(torch.tensor([ubah_ke_ids(teks)])), dim=1)[0]
prediksi = peluang.argmax().item()
print(f" '{teks}'")
print(f" -> {NAMA_KELAS[prediksi]} "
f"(peluang negatif {peluang[0]:.3f} | positif {peluang[1]:.3f})")Skrip di atas menghasilkan keluaran berikut saat dijalankan di mesin uji (CPU, PyTorch 2.14):
Output yang Dihasilkan:
contoh latih 160 | contoh uji 40 | ukuran vocab 45 | panjang maks 7
contoh positif: pengiriman sangat memuaskan
contoh negatif: harga tidak sebanding dengan kualitas yang mengecewakan
epoch 1 | loss latih 0.7495 | akurasi uji 0.650
epoch 20 | loss latih 0.0011 | akurasi uji 1.000
epoch 40 | loss latih 0.0000 | akurasi uji 1.000
epoch 60 | loss latih 0.0000 | akurasi uji 1.000
epoch 80 | loss latih 0.0000 | akurasi uji 1.000
akurasi akhir: latih 1.000 | uji 1.000
prediksi kalimat baru:
'kualitasnya bagus sekali dan tahan lama'
-> positif (peluang negatif 0.000 | positif 1.000)
'pengiriman lambat dan paket rusak'
-> negatif (peluang negatif 1.000 | positif 0.000)
'pelayanan tokonya menyebalkan sekali'
-> negatif (peluang negatif 1.000 | positif 0.000)
'saya puas dengan produk ini'
-> positif (peluang negatif 0.000 | positif 1.000)Mari kita telaah kode di atas baris per baris:
- Baris 15-16 (seed):
torch.manual_seed(42)mengunci inisialisasi bobot danrandom.Random(42)mengunci pemilihan templat — dataset dan model identik pada setiap eksekusi. - Baris 19-61 (dataset templat): dua kosakata perasaan menjadi sinyal pembeda; 100 ulasan per kelas dibangkitkan dari tujuh templat lalu diacak, 160 latih dan 40 uji.
- Baris 63-76 (vocabulary): kamus dibangun hanya dari token data latih, diawali
<pad>ber-id 0 dan<unk>ber-id 1 — data uji tidak boleh memperluas kamus agar evaluasi jujur. - Baris 78-83 (encoding): setiap teks dipotong atau dipanjangkan dengan id 0 hingga
PANJANG_MAKS = 7token sehingga batch berbentuk tensor(B, 7). - Baris 98-109 (model):
forwardmengambil embedding(B, T, D), membangun mask token asli, lalu merata-ratakan hanya token non-padding — tanpa mask, kalimat pendek tersedot ke arah rata-rata padding. - Baris 112-114 (komponen latih):
CrossEntropyLossmenerima logits mentah; Adam dengan learning rate 0.05 cukup untuk model sekecil ini. - Baris 117-131 (training loop): pola
zero_grad -> forward -> loss -> backward -> stepdari chapter Training Loop , dijalankan full-batch dengan log tiap 20 epoch. - Baris 133-154 (evaluasi dan inferensi): model berpindah ke mode
eval, akurasi akhir dihitung, lalu empat kalimat baru diprediksi lengkap dengan peluang softmax.
Membaca Hasil #
Loss latih turun 0,7495 menjadi 0,0000 dan akurasi uji menyentuh 1,000 — seluruh 40 ulasan held-out terklasifikasi benar, begitu pula keempat kalimat baru. Hasil setinggi ini wajar untuk data buatan berkata kunci jelas: satu kata “bagus” atau “buruk” hampir selalu menentukan label, sehingga rata-rata embedding saja memisahkan kedua kelas.
Mean pooling sengaja membuang urutan kata — keputusan yang murah tetapi berakibat: frasa “tidak bagus” dan “bagus, tidak masalah” menghasilkan rata-rata yang mirip. Rujukan API untuk nn.Embedding tersedia di docs.pytorch.org/docs/stable/generated/torch.nn.Embedding.html
. Chapter berikutnya menghadapi kelemahan ini langsung: studi kasus sentimen tiga kelas di mana negasi menggagalkan model kata-per-kata.