Siri Belajar AI: Mari Kita Tengok Perhatian Sekali Lagi
Ok. Dah lama tak menulis dalam bahasa Melayu. Bukan apa, kalau aku menulis pasal AI dalam bahasa inggeris selalu sangat, nanti adalah yang mula ragu-ragu. Nanti adalah yang mula kata: “Ek eleh, mamat ni mesti pakai AI tulis artikel”. Padahal memang aku dah lama menulis sejak dulu lagi. Sejak sebelum ChatGPT wujud lagi.
Lagi satu, ada juga permintaan dari pembaca-pembaca aku yang meminta aku menulis dalam bahasa melayu, supaya dapat memasyarakatkan lagi ilmu berkenaan AI ini untuk bangsa kita. Dengan harapan adalah di antara anak-anak muda yang membaca dan menanam benih minat pada mereka untuk menguasai satu teknologi yang cukup senang diakses.
Ok, kali ini aku nak sentuh berkenaan dengan mekanisma perhatian. Ataupun dalam bahasa inggerisnya, “attention mechanism”.
Dalam tahun lepas aku pernah sentuh pasal ni. Korang boleh baca di sini: https://medium.com/@maercaestro/siri-belajar-ai-mekanisma-perhatian-cd71853ec325
Cuma masa tu, aku hanya tahu sekadar di dasar sahaja. Aku cuma tahu formula dan bagaimana nak implement atau laksanakan dalam kod python. Tapi kali ini, aku nampak gambaran yang lebih besar. Setelah hampir setahun meneroka dan mempelajari banyak konsep-konsep asas rangkaian neural, aku dah faham hampir 80% bagaimana sebenarnya mekanisma perhatian berfungsi dalam transformer, dan mengapa ia begitu berkesan sekali sehingga membolehkan lonjatan yang begitu besar berlaku dalam perkembangan kecerdasan buatan.
Tanpa perhatian, AI yang kita ada sekarang mungkin tidak akan wujud. Untuk itu, seeloknya kita memahami sumbangan mekanisma perhatian ini terhadap kemajuan AI dan memahami sepenuhnya bagaimana ia berfungsi.
Perhatian adalah kunci kepada kejayaan AI
Tak keterlaluan untuk aku bagi ayat seperti di atas. Ya, kewujudan ChatGPT, Gemini, Grok dan banyak lagi model-model AI seperti di atas adalah kerana perhatian. Untuk memahami kenapa perhatian begitu berkuasa, kita perlu memahami dulu apa masalah yang diselesaikannya.
Secara asasnya, model-model AI yang kita ada sekarang terdiri daripada rangkaian neural. Ia bermodelkan otak manusia, yang diabstrakkan kepada persamaan matematik. Seperti kata Andrej Kaparthy, kita boleh bayangkan rangkaian neural ni sebagai satu kumpulan tombol-tombol yang boleh diputar mengikut nombor yang kita ingingkan. Ketika latihan, tombol-tombol ini akan diputar sehinggalah ia menghasilkan output yang kita inginkan.
Untuk tugas yang mudah seperti meramal hasil jualan, ataupun harga rumah, jumlah tombol-tombol yang diperlukan tidaklah terlalu banyak. Tapi kalau tugas yang diberikan itu sukar, sebagai contoh untuk meramal perkataan seterusnya (jika diberi sepotong ayat), jumlah tombol yang diperlukan mungkin mencecah beratus billion atau mungkin trillion. Sebab apa? Sebab jumlah perkataan dan teks yang ada dalam dunia ini mencecah ratusan trillion. Kalau kita nak satu model AI yang boleh meramalkan perkataan seterusnya, kita perlukan ia untuk memahami dan memproses hampir semua perkataan yang ada dalam dunia ini, barulah ia dengan yakinnya boleh meramalkan perkataan seterusnya.
Jadi, apa kena mengena dengan perhatian? Untuk lebih memahami perhatian kita kena tukar pulak simbologi kita. Kali ini, kita bayangkan rangkaian neural tu sebagai seorang budak sekolah. Ingat tak lagi masa zaman kita di sekolah, bila kita diminta untuk belajar subjek-subjek yang perlukan banyak pembacaan, apa yang cikgu kita ajar? Cikgu minta kita untuk cari kata kunci. Cikgu kata, tak perlu hafal. Hanya cari kata kunci. Tanya soalan, cari kata kunci, dan di situlah kita akan dapat jawapannya.
Percaya tak kalau saya cakap yang tips inilah yang AI gunakan untuk ‘menghafal’ bertrillion-trillion perkataan? Apa yang dia ingat hanyalah soalan, kata kunci dan jawapan. Dari situ, ia mengurangkan jumlah tombol yang diperlukan untuk menghasilkan output ynag diingini.
Perhatian dari segi matematiknya
Jadi, mari kita tengok balik tips-nya tadi.
Kita tanya soalan, kita panggil sebagai Query ataupun Q.
Kita cari kata kunci dari soalan tersebut, atau Key (K)
Dan dari situ kita dapat jawpaannya. Boleh kita panggil dia sebagai Value (V)
Dan itulah natijahnya mekanisma perhatian. Untuk setiap input yang dimasukkan kita perlu mencari nilai Q, K dan V. Semuanya perlu dilakukan melalui proses matematik seperti di bawah:
di mana K, Q dan V adalah Key, Query dan Value. d_k pula adalah dimensi vektor data kita. Untuk yang tak berapa biasa dengan istilah dimensi, kena ingat balik matematik tingkatan 5, matriks. Kat situ kita ada belajar tentang operasi matriks di mana kita boleh susun nombor dalam tatasusunan pelbagai jalur dan baris. Jalur dan baris itu dalam dunia AI kita boleh panggil sebagai dimensi.
Perhatian yang saya tunjukkan di atas adalah mekanisma perhatian kendiri, yang digunakan di dalam model Transformer. Sebenarnya ada banyak lagi jenis-jenis perhatian. Tapi saya lebih suka ulas yang ini sebab ia lebih senang untuk difahami dan ia lebih berkesan.
Untuk gambaran yang lebih besar, mekanisma perhatian ini dalam asas AI adalah sesuatu yang kita panggil sebagai feature selection (pemilihan ciri). Ia sama sahaja seperti convolutional, gated memory, PCA dan banyak lagi. Cuma perhatian terbukti berkesan untuk memilih feature yang betul-betul memberi kesan kepada latihan model AI.
Masa untuk kita Praktikkan
Ok, kita dah faham sikit matematiknya. Kita juga dah faham bagaiamana ia berfungsi secara teori. Tapi teori seeloknya kita praktikkan. Untuk itu, kita akan buat sedikit eksperimen mudah untuk menunjukkan kepada anda semua bagaimana mekanisma perhatian memberikan ‘perhatian’.
Kita akan gunakan mekanisma perhatian untuk melatih satu rangkaian neural untuk melakukan analisis sentimen (sentiment analysis) ulasan yang dikorek dari IMDB (bukan 1MDB). Dengan harapan, mekanisma perhatian kata kunci yang diperlukan bagi mennetukan sama ada ulasan itu positif ataupun negatif.
Pertama sekali kita muat turun semua pakej dan library yang diperlukan.
# pertama sekali kita muat turun semua pakej dan library yang diperlukan
import torch
import torch.nn as nn #kita akan menggunakan pytorch untuk latihan
import torch.optim as optim
import pandas as pd #pkita gunakan pandas untuk memuat turun data IMDB.
from torch.utils.data import DataLoader, Dataset #untuk memuat data ke dalam tensor
from torch.nn.utils.rnn import pad_sequence #untuk menyusun data ke dalam kelompok
import numpy as np
import spacy # penting untuk nlp
import matplotlib.pyplot as plt # matplotlib dengan seaborn untuk visualisasi
import seaborn as sns
from tqdm import tqdm #kita pakai ni untuk buat progress bar
import re
import stringJalankan dulu bash kod ini untuk membolehkan spacy dimuat dengan en_core_web_sm
!python -m spacy download en_core_web_smSeterusnya, kita perlu muat turun data IMDB Movie Review dataset yang mengumpulkan sehingga 50,000 baris ulasan filem yang ada di IMDB. Ia datang sekali dengan lable sama ada positif ataupun negatif. Ada banyak sumber sebenarnya untuk kita dapatkan dataset ni. Dari Huggingface, Kaggle, malah Pytorch sendiri ada memberikan kita akses kepada dataset IMDB melalui library torchtext dia. Cuma, aku dah cuba pakai Pytorch dan Huggingface, dua-dua gagal untuk aku muat turun. Akhirnya, aku pakai pandas jelah senang.
Untuk memastikan latihan kita ringan, dan tak terlalu berat, kita ambil sekadar 6000 sahaja baris ulasan (5000 untuk latihan dan 1000 untuk ujian)
#boleh cuba kod di bawah
try:
splits = {
'train': 'plain_text/train-00000-of-00001.parquet',
'test': 'plain_text/test-00000-of-00001.parquet'
}
print("Muat turun data latihan...")
train_df = pd.read_parquet("hf://datasets/stanfordnlp/imdb/" + splits["train"])
print("Muat turun data ujian...")
test_df = pd.read_parquet("hf://datasets/stanfordnlp/imdb/" + splits["test"])
# ambil 5000 je baris data untuk latihan dan ujian
print("Ambil 5000 sahaja subset untuk latihan dan 1000 untuk ujian...")
train_df = train_df.sample(n=5000, random_state=42).reset_index(drop=True)
test_df = test_df.sample(n=1000, random_state=42).reset_index(drop=True)
print(f"Sample latihan (subset): {len(train_df)}")
print(f"Sample ujian (subset): {len(test_df)}")
except Exception as e:
print(f"Error loading dataset: {e}")
exit(1)Kalau ok, kita akan dapat hasil seperti di bawah:
Seterusnya kita sediakan tokenizer untuk data kita:
#seterusnya kita sediakan tokenizer untuk data kita
def preprocess_and_tokenize(text, max_length=150):
"""fungsi untuk menyediakan data untuk tokenisasi"""
text = text.lower() #tukar kepada huruf kecil
text = re.sub(r'<[^>]+>', '', text) #buang tag2 istimewa seperti html tag
text = text.translate(str.maketrans('', '', string.punctuation)) #buang semua tanda seru, noktah, koma semua
text = re.sub(r'\s+', ' ', text).strip() #buang semua space
tokens = text.split()[:max_length]
return tokens
# proses kan data kita dan sediakan untuk tokenizer
print("Memproses data latihan...")
train_texts = []
train_labels = []
for idx, row in tqdm(train_df.iterrows(), total=len(train_df), desc="Memproses data latihan"):
tokens = preprocess_and_tokenize(row['text'])
train_texts.append(tokens)
train_labels.append(row['label'])
print("Memproses data ujian...")
test_texts = []
test_labels = []
for idx, row in tqdm(test_df.iterrows(), total=len(test_df), desc="Memproses data ujian"):
tokens = preprocess_and_tokenize(row['text'])
test_texts.append(tokens)
test_labels.append(row['label'])Dan kita akan bina kosa kata sebelum tukarkan teks kepada indeks (tokenize).
def build_vocabulary(texts, min_freq=5):
word_freq = {}
for tokens in tqdm(texts, desc="Mengira perkataan"):
for token in tokens:
word_freq[token] = word_freq.get(token, 0) + 1
vocab = {'<pad>': 0, '<unk>': 1} #kita letakkan dulu token untuk permulaan dan token untuk yang kita tak tahu (unknown)
idx = 2
for word, freq in word_freq.items(): #kita loopkan dan bina kosa kata
if freq >= min_freq: #frekuensi minimum kita letakkan sebagai 5 hanya untuk perkataan yang unik
vocab[word] = idx
idx += 1
return vocab
vocab = build_vocabulary(train_texts, min_freq=5)
PAD_IDX = vocab['<pad>']
UNK_IDX = vocab['<unk>']
VOCAB_SIZE = len(vocab)
print(f"Saiz kosa kata: {VOCAB_SIZE}")
# tukarkan teks kepada index
def text_to_indices(tokens, vocab):
return [vocab.get(token, UNK_IDX) for token in tokens]
print("Tukar teks kepada indeks...")
train_indices = [text_to_indices(tokens, vocab) for tokens in tqdm(train_texts, desc="Tukarkan teks latihan")]
test_indices = [text_to_indices(tokens, vocab) for tokens in tqdm(test_texts, desc="Tukarkna teks ujian")]Ok, sekarang kita persiapkan data kita untuk dimasukkan dalam tensor. ia perlu disusun setara mengikut kelompok yang mempunyai panjang yang sama
class IMDBDataset(Dataset):
def __init__(self, texts, labels):
self.texts = texts
self.labels = labels
def __len__(self):
return len(self.texts)
def __getitem__(self, idx):
return torch.tensor(self.texts[idx], dtype=torch.int64), torch.tensor(self.labels[idx], dtype=torch.float32)
#susun mengikut panjang yang sama. pastikan kita lapikkan urutan mengikut urutan terpanjang
def collate_batch(batch):
texts, labels = zip(*batch)
lengths = [len(text) for text in texts]
padded_texts = pad_sequence(texts, batch_first=True, padding_value=PAD_IDX)
return torch.stack(labels).to(device), padded_texts.to(device), torch.tensor(lengths).to(device)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
train_dataset = IMDBDataset(train_indices, train_labels)
test_dataset = IMDBDataset(test_indices, test_labels)
BATCH_SIZE = 32 #saiz kelompok
train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, collate_fn=collate_batch, num_workers=0)
test_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False, collate_fn=collate_batch, num_workers=0)
print(f"Menggunakan GPU atau CPU?: {device}")
print(f"Sampel latihan: {len(train_dataset)}")
print(f"Sampel ujian: {len(test_dataset)}")Kalau semuanya berjalan lancar, kita akan dapat output seperti di bawah:
Ok, kita dah muat turun dataset, dan dah sedia dengan data kita dalam bentuk tensor. Menunggu untuk dilatih. Sekarang masa untuk kita bina rangkaian neural kita. Untuk arkitekture ini aku ambil inspirasi dari transformer, cuma ia lebih ringkas dan tak adalah terlalu kompleks.
Kita kena ada embedding layer, untuk tukarkan token kepada dimensi aras tinggi (high dimensional space)
Lepas tu ia akan lalu dua lapisan rangkaian neural. Ia menukarkan dimensi embedding kepada dimensi tersembunyi (hidden dim)
Kemudian barulah dia akan masuk ke modul perhatian. Kat sinilah kita cari K, Q, V. Dari situ kita cari markah perhatian.
Modul perhatian terdiri daripada pelbagai kepala perhatian (attention heads). Ini bagi membolehkan ia memerhatikan keseluruhan teks.
Skor setiap kepala perhatian ini akan kita darabkan antara satu sama lain bagi membentuk output.
Output dari modul perhatian akan melalui dua lagi lapisan rangkaian neural.
Tapi sebelum itu, kita tambah sambungan berbaki (residual connection) antara input asal dan output perhatian, supaya model tak lupa maklumat asal.
Kemudian kita normalkan dengan layer normalization, dan tambahkan dropout untuk elakkan overfitting.
Sekarang kita ada output bagi setiap token. Tapi untuk klasifikasi, kita cuma nak satu output sahaja. Jadi kita buat global average pooling (atau dapatkan purata), sambil abaikan token <pad> dengan masking.
Output yang telah dipool ini akan masuk ke dua lapisan terakhir. Satu lapisan untuk kecilkan dimensi, satu lagi untuk buat keputusan akhir (klasifikasi).
Akhir sekali, kita dapat output . Contohnya, kebarangkalian untuk ulasan itu positif atau negatif.
# bina modul perhatian dulu
class SelfAttention(nn.Module):
def __init__(self, hidden_dim, num_heads=4):
super(SelfAttention, self).__init__()
self.hidden_dim = hidden_dim
self.num_heads = num_heads
self.head_dim = hidden_dim // num_heads
assert hidden_dim % num_heads == 0
self.query = nn.Linear(hidden_dim, hidden_dim)
self.key = nn.Linear(hidden_dim, hidden_dim)
self.value = nn.Linear(hidden_dim, hidden_dim)
self.fc_out = nn.Linear(hidden_dim, hidden_dim)
self.dropout = nn.Dropout(0.1)
def forward(self, x, mask=None):
batch_size, seq_len, hidden_dim = x.shape
Q = self.query(x)
K = self.key(x)
V = self.value(x)
# susun kepada perhatian berbagai kepala (multi-head attention)
Q = Q.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
K = K.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
V = V.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
# dapatkan markah perhatian
scores = torch.matmul(Q, K.transpose(-2, -1)) / torch.sqrt(torch.tensor(self.head_dim, dtype=torch.float32))
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)
attention_weights = torch.softmax(scores, dim=-1)
attention_weights = self.dropout(attention_weights)
attended = torch.matmul(attention_weights, V)
attended = attended.transpose(1, 2).contiguous().view(batch_size, seq_len, hidden_dim)
output = self.fc_out(attended)
return output, attention_weights
class SimpleNeuralNetworkWithAttention(nn.Module):
def __init__(self, vocab_size, embedding_dim=64, hidden_dim=128, output_dim=1, num_heads=4, dropout=0.2):
super(SimpleNeuralNetworkWithAttention, self).__init__()
# Embedding layer
self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx=PAD_IDX)
# lapisan rangkaian neural. tukar dimensi embedding kepada dimensi tersembunyi
self.fc1 = nn.Linear(embedding_dim, hidden_dim)
self.fc2 = nn.Linear(hidden_dim, hidden_dim)
# perhatian!!
self.self_attention = SelfAttention(hidden_dim, num_heads)
# kita normalizekan output
self.layer_norm = nn.LayerNorm(hidden_dim)
# jangan lupa droput untuk elakkan overfitting
self.dropout = nn.Dropout(dropout)
# lapisan rangkaian neural yang ke 3 dan ke 4 untuk hasilkan output klasifikasi, positif atau negatif
self.fc3 = nn.Linear(hidden_dim, hidden_dim // 2)
self.fc4 = nn.Linear(hidden_dim // 2, output_dim)
def create_padding_mask(self, x, lengths): #buat lapik untuk membolehkan model meramal
batch_size, seq_len = x.shape
mask = torch.zeros(batch_size, seq_len, dtype=torch.bool, device=x.device)
for i, length in enumerate(lengths):
mask[i, :length] = True
return mask.unsqueeze(1).unsqueeze(1)
def forward(self, x, lengths):
batch_size, seq_len = x.shape
# embedding
embedded = self.embedding(x) # (batch_size, seq_len, embedding_dim)
embedded = self.dropout(embedded)
hidden = torch.relu(self.fc1(embedded)) # (batch_size, seq_len, hidden_dim)
hidden = torch.relu(self.fc2(hidden)) # (batch_size, seq_len, hidden_dim)
hidden = self.dropout(hidden)
padding_mask = self.create_padding_mask(x, lengths)
attended_output, attention_weights = self.self_attention(hidden, padding_mask)
# hubungan berbaki berserta normalization
attended_output = self.layer_norm(attended_output + hidden)
attended_output = self.dropout(attended_output)
# ambil satu purata global
mask = torch.zeros(batch_size, seq_len, device=x.device)
for i, length in enumerate(lengths):
mask[i, :length] = 1.0
mask = mask.unsqueeze(-1) # (batch_size, seq_len, 1)
masked_output = attended_output * mask
pooled_output = masked_output.sum(dim=1) / mask.sum(dim=1) # (batch_size, hidden_dim)
# menghasilkan output klasifikasi
output = torch.relu(self.fc3(pooled_output))
output = self.dropout(output)
output = self.fc4(output)
return outputOk, model dah siap direka. mari kita bina model kita dalam CPU dan siapkan fungsi latihan dan ujian.
#parameter model
EMBEDDING_DIM = 64
HIDDEN_DIM = 128
OUTPUT_DIM = 1
NUM_HEADS = 4
DROPOUT = 0.2
LEARNING_RATE = 0.001 #kadar pembelajaran menentukan sejauh besar mana pemberat perlu dikemaskini
N_EPOCHS = 10 #10 kali latihan untuk semua kelompok data
model = SimpleNeuralNetworkWithAttention(VOCAB_SIZE, EMBEDDING_DIM, HIDDEN_DIM, OUTPUT_DIM, NUM_HEADS, DROPOUT).to(device)
criterion = nn.BCEWithLogitsLoss() #gunakan loss cross entropy dengna logits sebab klasifikasi
optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE) #pakai adam untuk optimizer
print(f"Model dibina dengan {sum(p.numel() for p in model.parameters())} parameters")
#bina fungsi latihan
def train_epoch(model, data_loader, criterion, optimizer, device):
model.train()
total_loss = 0
correct_predictions = 0
total_samples = 0
progress_bar = tqdm(data_loader, desc="Melatih", leave=False)
for batch_idx, (labels, texts, lengths) in enumerate(progress_bar):
optimizer.zero_grad()
outputs = model(texts, lengths).squeeze()
loss = criterion(outputs, labels)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
total_loss += loss.item()
predictions = torch.sigmoid(outputs) > 0.5
correct_predictions += (predictions == labels.bool()).sum().item()
total_samples += labels.size(0)
if batch_idx % 10 == 0:
progress_bar.set_postfix({
'Loss': f'{loss.item():.4f}',
'Acc': f'{correct_predictions/total_samples:.4f}'
})
return total_loss / len(data_loader), correct_predictions / total_samples
# bina fungsi ujian/penilaian
def evaluate(model, data_loader, criterion, device):
model.eval()
total_loss = 0
correct_predictions = 0
total_samples = 0
with torch.no_grad():
progress_bar = tqdm(data_loader, desc="Menilai", leave=False)
for labels, texts, lengths in progress_bar:
outputs = model(texts, lengths).squeeze()
loss = criterion(outputs, labels)
total_loss += loss.item()
predictions = torch.sigmoid(outputs) > 0.5
correct_predictions += (predictions == labels.bool()).sum().item()
total_samples += labels.size(0)
return total_loss / len(data_loader), correct_predictions / total_samplesSukses! Kalau model dah berjaya dibina, kita akan dapat mesej seperti di bawah:
Ok, sekarang kita bina kitaran latihan kita (training loop) dan mari kita visualkan proses latihan kita untuk melihat sejauh mana ketepatan (acc) latihan jika dibandingkan dengan ujian/penilaian (validation acc). Latihan akan dibuat dalam 10 epoch.
train_losses, train_accuracies = [], []
val_losses, val_accuracies = [], []
print("\nLatihan bermula...")
for epoch in range(N_EPOCHS):
print(f"\nEpoch {epoch+1}/{N_EPOCHS}")
train_loss, train_acc = train_epoch(model, train_loader, criterion, optimizer, device)
train_losses.append(train_loss)
train_accuracies.append(train_acc)
val_loss, val_acc = evaluate(model, test_loader, criterion, device)
val_losses.append(val_loss)
val_accuracies.append(val_acc)
print(f"TLoss Latihan: {train_loss:.4f}, Ketepatan Latihan: {train_acc:.4f}")
print(f"Loss Penilaian: {val_loss:.4f}, Ketepatan Penilaian: {val_acc:.4f}")
# --- 9. Plot Training Results ---
plt.figure(figsize=(15, 5))
# Plot 1: Loss curves
plt.subplot(1, 3, 1)
plt.plot(range(1, N_EPOCHS + 1), train_losses, 'b-', label='Loss Latihan', marker='o')
plt.plot(range(1, N_EPOCHS + 1), val_losses, 'r-', label='Loss Penilaian', marker='s')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Loss Latihan dan Penilaian')
plt.legend()
plt.grid(True, alpha=0.3)
# Plot 2: Accuracy curves
plt.subplot(1, 3, 2)
plt.plot(range(1, N_EPOCHS + 1), train_accuracies, 'b-', label='Ketepatan Latihan', marker='o')
plt.plot(range(1, N_EPOCHS + 1), val_accuracies, 'r-', label='Ketepatan Penilaian', marker='s')
plt.xlabel('Epoch')
plt.ylabel('Ketepatan')
plt.title('Ketepatan Latihan dan Penilaian')
plt.legend()
plt.grid(True, alpha=0.3)Jika semuanya berjalan lancar, kita akan mendapat graf seperti di bawah, yang menunjukkan sebenarnya tak perlulah kita latih sampai 10 epoch. 3–4 je dah cukup dah.
Ok, model dah dilatih, mari kita gunakan ia untuk melihat bagaimana mekanisma perhatian ini berfungsi. Ini bahagian yang paling menarik sekali. Untuk itu kita boleh gunakan fungsi di bawah
def visualize_attention(model, text, vocab, device, max_len=50):
"""Visualize attention weights for a given text"""
model.eval()
tokens = preprocess_and_tokenize(text, max_length=max_len)
token_indices = text_to_indices(tokens, vocab)
input_tensor = torch.tensor([token_indices], dtype=torch.int64).to(device)
lengths = torch.tensor([len(token_indices)]).to(device)
with torch.no_grad():
embedded = model.embedding(input_tensor)
embedded = model.dropout(embedded)
hidden = torch.relu(model.fc1(embedded))
hidden = torch.relu(model.fc2(hidden))
hidden = model.dropout(hidden)
# dapatkan berat perhaitan
padding_mask = model.create_padding_mask(input_tensor, lengths)
attended_output, attention_weights = model.self_attention(hidden, padding_mask)
avg_attention = attention_weights.mean(dim=1).squeeze(0) # Shape: (seq_len, seq_len)
# buat heatmap perhatian
plt.figure(figsize=(12, 10))
# plot heatmap
plt.subplot(2, 1, 1)
seq_len = len(tokens)
attention_matrix = avg_attention[:seq_len, :seq_len].cpu().numpy()
sns.heatmap(attention_matrix,
xticklabels=tokens, yticklabels=tokens,
cmap='Blues', cbar=True, square=True,
cbar_kws={'label': 'Berat Perhatian'})
plt.title(f'Berat Perhatian Kendiri\nText: "{text[:60]}..."')
plt.xlabel('Token Key')
plt.ylabel('Token Query')
plt.xticks(rotation=45, ha='right')
plt.yticks(rotation=0)
plt.subplot(2, 1, 2)
token_attention = attention_matrix.mean(axis=0) # Average attention each token receives
colors = plt.cm.Blues(token_attention / token_attention.max())
bars = plt.bar(range(len(tokens)), token_attention, color=colors)
plt.xlabel('Tokens')
plt.ylabel('Purata Perhatian Diterima')
plt.title('Kepentingan Token mengikut Purata Perhatian')
plt.xticks(range(len(tokens)), tokens, rotation=45, ha='right')
for i, (bar, val) in enumerate(zip(bars, token_attention)):
plt.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.001,
f'{val:.3f}', ha='center', va='bottom', fontsize=8)
plt.tight_layout()
plt.show()
# Print top attended words
top_indices = np.argsort(token_attention)[-5:][::-1]
print(f"\n5 Perkataan Utama yang diberikan perhatian:")
for i, idx in enumerate(top_indices):
print(f"{i+1}. '{tokens[idx]}' (attention: {token_attention[idx]:.3f})")
return avg_attention
# mari menguji dengan teks sampel
def predict_sentiment(text, model, vocab, device):
model.eval()
tokens = preprocess_and_tokenize(text)
indices = text_to_indices(tokens, vocab)
input_tensor = torch.tensor([indices], dtype=torch.int64).to(device)
lengths = torch.tensor([len(indices)]).to(device)
with torch.no_grad():
output = model(input_tensor, lengths).squeeze()
probability = torch.sigmoid(output).item()
sentiment = "Positive" if probability > 0.5 else "Negative"
return sentiment, probability
# Test predictions
test_texts = [
"This movie was absolutely fantastic! Amazing acting and great plot.",
"Terrible movie. Boring and poorly made.",
"It was okay, nothing special but not bad either."
]
print("\nMenguji ramalan:")
for text in test_texts:
sentiment, prob = predict_sentiment(text, model, vocab, device)
print(f"Teks: {text[:50]}...")
print(f"Rmaalan: {sentiment} (Nilai keyakina: {prob:.3f})")
print()
# --- 11. Visualize Attention for Sample Texts ---
print("\n" + "="*60)
print("VISUALISASI PERHATIAN")
print("="*60)
sample_texts = [
"This movie was absolutely fantastic! Amazing acting and great plot.",
"Terrible movie. Boring and poorly made.",
"The film had good moments but overall disappointing ending."
]
print(f"\nKita visualkan perhatian untuk teks contoh...")
for i, text in enumerate(sample_texts, 1):
print(f"\n--- Sampel {i} ---")
sentiment, confidence = predict_sentiment(text, model, vocab, device)
print(f"Teks: {text}")
print(f"Ramalan: {sentiment} (kadar keyakinan: {confidence:.3f})")
try:
attention_weights = visualize_attention(model, text, vocab, device, max_len=30)
print(f"✅ Selesai untuk {i}")
except Exception as e:
print(f"❌ Gagal untuk {i}: {e}")
print("-" * 50)Bila kita jalankan kod di atas, kita akan dapat 2 graf bagi setiap teks sampel yang kita ingin uji. Salah satu contohnya adalah di bawah:
Kita boleh lihat sendiri yang perhatian benar-benar membantu model tersebut untuk mengenal pasti sama aulasan yang diberikan itu positif ataupun negatif. Ia benar-benar mencari kata kunci seperti yang kita pernah diajar oleh cikgu di sekolah dulu.
Jadi AI ni sebenarnya macam kita je masa kat sekolah dulu. Sebelum jawab soalan, cari kata kunci dulu. Dan ia berfungsi dengan betul-betul baik.
Ok, cukuplah untuk kali ini. Kita jumpa lagi di sesi yang akan datang. Mungkin kita akan buat bersama-sama untuk membina GPT kita sendiri.









