Komponen penyusunnya sudah kita bangun satu per satu: embedding token, positional encoding sinus, blok encoder, blok decoder dengan masked self-attention dan cross-attention. Chapter ini adalah puncak perakitannya — arsitektur Transformer lengkap sebagaimana diperkenalkan makalah Attention Is All You Need
untuk terjemahan mesin. Kita merakit model utuh berukuran mainan (d_model = 16, satu blok encoder dan satu blok decoder), melatihnya pada tugas salin-geser — diberi enam digit acak, model harus mengeluarkan tiap token sumber yang berikutnya — sampai loss turun nyata dan aturan tugasnya terpelajari. Puncaknya bukan sekadar angka akurasi: bobot cross-attention model yang sudah dilatih akan kita baca langsung, dan pola pergeseran yang dipelajarinya terlihat menonjol tanpa perlu memeriksa satu pun parameter.
Rakitan Lengkap #
Alur data model utuh berjalan seperti diagram berikut. Urutan sumber diubah menjadi embedding, dijumlahkan positional encoding, lalu diproses tumpukan N blok encoder menjadi memori. Di jalur decoder, urutan target yang digeser kanan (diawali token BOS) menjalani proses serupa melalui tumpukan N blok decoder — dan cross-attention pada tiap blok membaca memori encoder sebagai key-value. Layer linear di ujung decoder memproyeksikan tiap posisi ke ukuran vocabulary, dan softmax mengubahnya menjadi distribusi token berikutnya.
Model asli memakai N = 6, d_model = 512, delapan head, dan vocabulary puluhan ribu token subword. Model kita memakai N = 1, d_model = 16, dua head, dan vocabulary 11 token — cukup kecil untuk dilatih dalam hitungan detik di CPU, cukup lengkap untuk memperlihatkan setiap bagian bekerja.
Pelatihan dan Inferensi Berjalan Berbeda #
Selama pelatihan, decoder menerima target asli yang digeser kanan — teknik teacher forcing yang sudah kita ukur di chapter Teacher Forcing
. Seluruh posisi diprediksi sekaligus dalam satu forward: mask kausal menjamin posisi ke-t hanya melihat token target sebelum t, sehingga satu pass menghasilkan semua prediksi tanpa kebocoran. Loss dihitung dengan cross-entropy pada seluruh posisi sekaligus.
Saat inferensi tidak ada target asli. Decoder dimulai dari token BOS saja, memprediksi token pertama, lalu prediksi itu sendiri menjadi masukan langkah berikutnya — proses autoregressive. Satu kesalahan prediksi menjadi masukan yang keliru pada langkah berikutnya, sehingga standar penilaiannya ketat: sebuah urutan dihitung benar hanya jika keenam token prediksi cocok penuh.
Tugas Salin-Geser #
Tugas uji kita dirancang agar aturannya jelas: sumber enam digit acak 0-9, target adalah sumber yang digeser siklik satu posisi — y_t = x_(t+1), dan token terakhir target kembali ke x_0. Model tidak bisa menyelesaikan tugas ini dari isi token decoder saja; satu-satunya jalan adalah cross-attention yang menoleh ke posisi sumber yang tepat, sehingga keberhasilan model sekaligus membuktikan jalur encoder-decoder hidup.
Program Lengkap #
Program berikut memuat seluruh rakitan — kelas MultiHeadAttention, FeedForward, EncoderBlock, dan DecoderBlock dari dua chapter sebelumnya, plus kelas perakit TransformerMini. Pelatihan berjalan 600 epoch (300 epoch lr 3e-3 lalu 300 epoch lr 3e-4) dengan full-batch 512 urutan:
"""Transformer mainan lengkap pada tugas salin-geser siklik.
Sumber: 6 digit acak (0-9). Target: tiap token adalah token sumber
BERIKUTNYA (y_t = x_{(t+1) mod 6}). Rakitan: embedding + positional
encoding sinus + 1 blok encoder + 1 blok decoder (masked self-attention,
cross-attention, FFN) + layer linear. Pelatihan memakai teacher forcing;
inferensi berjalan autoregressive (greedy, tanpa cache). Akurasi diukur
sebagai kecocokan penuh urutan pada 256 urutan uji baru. Bobot
cross-attention per head dicetak untuk satu contoh uji.
PyTorch 2.14, CPU.
"""
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
torch.manual_seed(3)
np.random.seed(3)
np.set_printoptions(precision=2, suppress=True)
PANJANG, BOS = 6, 10 # panjang urutan; id token <bos>
def positional_encoding(panjang, d_model):
"""PE sinus-kosinus dari chapter positional-encoding."""
pos = np.arange(panjang)[:, None]
dua_i = np.arange(0, d_model, 2)[None, :]
sudut = pos / 10000 ** (dua_i / d_model)
pe = np.zeros((panjang, d_model))
pe[:, 0::2] = np.sin(sudut)
pe[:, 1::2] = np.cos(sudut)
return torch.tensor(pe, dtype=torch.float32)
class MultiHeadAttention(nn.Module):
"""MHA manual: q/k/v terpisah + mask boolean (True = posisi diblokir)."""
def __init__(self, d_model, num_heads):
super().__init__()
assert d_model % num_heads == 0
self.h = num_heads
self.d_k = d_model // num_heads
self.wq = nn.Linear(d_model, d_model)
self.wk = nn.Linear(d_model, d_model)
self.wv = nn.Linear(d_model, d_model)
self.wo = nn.Linear(d_model, d_model)
def forward(self, q, k, v, mask=None):
n, tq, d = q.shape
tk = k.shape[1]
qq = self.wq(q).view(n, tq, self.h, self.d_k).transpose(1, 2)
kk = self.wk(k).view(n, tk, self.h, self.d_k).transpose(1, 2)
vv = self.wv(v).view(n, tk, self.h, self.d_k).transpose(1, 2)
skor = qq @ kk.transpose(-2, -1) / self.d_k ** 0.5
if mask is not None:
skor = skor.masked_fill(mask, float("-inf"))
a = skor.softmax(dim=-1)
z = (a @ vv).transpose(1, 2).contiguous().view(n, tq, d)
return self.wo(z), a
class FeedForward(nn.Module):
"""Position-wise feed-forward: Linear -> GELU -> Linear."""
def __init__(self, d_model, d_ff):
super().__init__()
self.l1 = nn.Linear(d_model, d_ff)
self.l2 = nn.Linear(d_ff, d_model)
def forward(self, x):
return self.l2(F.gelu(self.l1(x)))
class EncoderBlock(nn.Module):
"""Blok encoder pre-LN dari chapter transformer-encoder."""
def __init__(self, d_model, num_heads, d_ff):
super().__init__()
self.attn = MultiHeadAttention(d_model, num_heads)
self.ffn = FeedForward(d_model, d_ff)
self.ln1 = nn.LayerNorm(d_model)
self.ln2 = nn.LayerNorm(d_model)
def forward(self, x):
n = self.ln1(x)
h = x + self.attn(n, n, n)[0]
return h + self.ffn(self.ln2(h))
class DecoderBlock(nn.Module):
"""Blok decoder pre-LN dari chapter transformer-decoder."""
def __init__(self, d_model, num_heads, d_ff):
super().__init__()
self.self_attn = MultiHeadAttention(d_model, num_heads)
self.cross_attn = MultiHeadAttention(d_model, num_heads)
self.ffn = FeedForward(d_model, d_ff)
self.ln1 = nn.LayerNorm(d_model)
self.ln2 = nn.LayerNorm(d_model)
self.ln3 = nn.LayerNorm(d_model)
def forward(self, x, memori):
t = x.shape[1]
kausal = torch.triu(torch.ones(t, t, dtype=torch.bool), diagonal=1)
n = self.ln1(x)
h = x + self.self_attn(n, n, n, kausal)[0]
m = self.ln2(memori)
z, bobot_cross = self.cross_attn(self.ln2(h), m, m)
h = h + z
h = h + self.ffn(self.ln3(h))
return h, bobot_cross
class TransformerMini(nn.Module):
"""Embedding + PE + encoder + decoder + linear (vocab digit 0-9 + <bos>)."""
def __init__(self, d_model, num_heads, d_ff, panjang):
super().__init__()
self.d_model = d_model
self.embed = nn.Embedding(11, d_model) # digit 0-9 + <bos> id 10
self.register_buffer("pe", positional_encoding(panjang, d_model))
self.encoder = EncoderBlock(d_model, num_heads, d_ff)
self.decoder = DecoderBlock(d_model, num_heads, d_ff)
self.head = nn.Linear(d_model, 10) # prediksi digit 0-9
def forward(self, src, tgt_in):
"""src: (B, T) id sumber; tgt_in: (B, T') id target geser kanan."""
t = tgt_in.shape[1]
memori = self.embed(src) * self.d_model ** 0.5 + self.pe
z = self.embed(tgt_in) * self.d_model ** 0.5 + self.pe[:t]
out, bobot_cross = self.decoder(z, memori)
return self.head(out), bobot_cross
def buat_data(n):
"""Sumber acak; target = sumber digeser siklik satu posisi ke kiri."""
src = torch.randint(0, 10, (n, PANJANG))
tgt = src.roll(-1, dims=1) # y_t = x_{(t+1) mod 6}
return src, tgt
def geser_kanan(tgt):
"""Masukan decoder saat teacher forcing: [<bos>, y_0, ..., y_{T-2}]."""
n = tgt.shape[0]
return torch.cat([torch.full((n, 1), BOS), tgt[:, :-1]], dim=1)
model = TransformerMini(16, 2, 64, PANJANG)
src_contoh, tgt_contoh = buat_data(1)
print("== Transformer mainan pada tugas salin-geser ==")
print(f"sumber contoh : {src_contoh[0].tolist()}")
print(f"target contoh : {tgt_contoh[0].tolist()} (tiap token = token sumber berikutnya)")
print("rakitan: d_model=16, 2 head, d_ff=64, 1 blok encoder + 1 blok decoder")
print("pelatihan : teacher forcing, full-batch 512, Adam")
print(" 300 epoch lr 3e-3, lalu 300 epoch lr 3e-4")
print()
src_latih, tgt_latih = buat_data(512)
tgt_in = geser_kanan(tgt_latih)
opt = torch.optim.Adam(model.parameters(), lr=3e-3)
for epoch in range(601):
if epoch == 300:
for grup in opt.param_groups:
grup["lr"] = 3e-4
logits, _ = model(src_latih, tgt_in)
loss = F.cross_entropy(logits.reshape(-1, 10), tgt_latih.reshape(-1))
opt.zero_grad()
loss.backward()
opt.step()
if epoch % 100 == 0:
print(f" epoch {epoch:3d} : loss {loss.item():.4f}")
print()
@torch.no_grad()
def greedy(src):
"""Inferensi autoregressive: prediksi token demi token tanpa guru."""
b = src.shape[0]
urutan = torch.full((b, 1), BOS)
for _ in range(PANJANG):
logits, _ = model(src, urutan)
nxt = logits[:, -1].argmax(dim=1)
urutan = torch.cat([urutan, nxt[:, None]], dim=1)
return urutan[:, 1:]
src_uji, tgt_uji = buat_data(256)
pred = greedy(src_uji)
cocok = int((pred == tgt_uji).all(dim=1).sum())
print(f"inferensi autoregressive pada 256 urutan uji baru:")
print(f" kecocokan penuh urutan : {cocok}/256")
print()
contoh = src_uji[0]
print("contoh inferensi langkah demi langkah:")
print(f" sumber : {contoh.tolist()}")
urutan = torch.full((1, 1), BOS)
for k in range(PANJANG):
logits, _ = model(contoh[None], urutan)
nxt = int(logits[0, -1].argmax())
print(f" langkah {k + 1}: masukan {urutan[0].tolist()} "
f"-> prediksi token berikutnya {nxt}")
urutan = torch.cat([urutan, torch.tensor([[nxt]])], dim=1)
print(f" prediksi: {urutan[0, 1:].tolist()} target: {tgt_uji[0].tolist()}")
print()
with torch.no_grad():
_, bobot = model(contoh[None], geser_kanan(tgt_uji[:1]))
print("bobot cross-attention contoh (per head):")
print("baris = posisi target, kolom = posisi sumber")
for head in range(2):
print(f" head {head + 1}:")
print(bobot[0, head].numpy())
print("-> head 1: baris t menoleh ke kolom t+1 (termasuk baris 5 -> kolom 0,")
print(" pembungkusan siklik); inilah aturan geser yang dipelajari model,")
print(" terbaca langsung dari bobot attention tanpa memeriksa parameter")Output yang Dihasilkan:
== Transformer mainan pada tugas salin-geser ==
sumber contoh : [2, 4, 6, 9, 2, 4]
target contoh : [4, 6, 9, 2, 4, 2] (tiap token = token sumber berikutnya)
rakitan: d_model=16, 2 head, d_ff=64, 1 blok encoder + 1 blok decoder
pelatihan : teacher forcing, full-batch 512, Adam
300 epoch lr 3e-3, lalu 300 epoch lr 3e-4
epoch 0 : loss 4.4132
epoch 100 : loss 1.7184
epoch 200 : loss 0.0472
epoch 300 : loss 0.0007
epoch 400 : loss 0.0006
epoch 500 : loss 0.0006
epoch 600 : loss 0.0005
inferensi autoregressive pada 256 urutan uji baru:
kecocokan penuh urutan : 254/256
contoh inferensi langkah demi langkah:
sumber : [3, 5, 7, 0, 6, 9]
langkah 1: masukan [10] -> prediksi token berikutnya 5
langkah 2: masukan [10, 5] -> prediksi token berikutnya 7
langkah 3: masukan [10, 5, 7] -> prediksi token berikutnya 0
langkah 4: masukan [10, 5, 7, 0] -> prediksi token berikutnya 6
langkah 5: masukan [10, 5, 7, 0, 6] -> prediksi token berikutnya 9
langkah 6: masukan [10, 5, 7, 0, 6, 9] -> prediksi token berikutnya 3
prediksi: [5, 7, 0, 6, 9, 3] target: [5, 7, 0, 6, 9, 3]
bobot cross-attention contoh (per head):
baris = posisi target, kolom = posisi sumber
head 1:
[[0.04 0.77 0.19 0. 0. 0. ]
[0.04 0.06 0.63 0.25 0.01 0.01]
[0.02 0.03 0.3 0.55 0.07 0.03]
[0. 0. 0.04 0.32 0.54 0.1 ]
[0.17 0.01 0.03 0.1 0.21 0.47]
[0.67 0.18 0.03 0.01 0.02 0.1 ]]
head 2:
[[0.01 0.85 0.13 0. 0. 0. ]
[0.03 0.11 0.8 0.03 0.02 0.01]
[0.03 0.05 0.44 0.15 0.24 0.08]
[0.01 0. 0.02 0.1 0.76 0.11]
[0.09 0.01 0.01 0.02 0.2 0.66]
[0.48 0.11 0.03 0.01 0.05 0.31]]
-> head 1: baris t menoleh ke kolom t+1 (termasuk baris 5 -> kolom 0,
pembungkusan siklik); inilah aturan geser yang dipelajari model,
terbaca langsung dari bobot attention tanpa memeriksa parameterMari kita telaah kode di atas baris per baris:
- Baris 25-33: fungsi
positional_encoding— rumus sinus-kosinus dari chapter Positional Encoding , dipakai pada kedua jalur. - Baris 36-72: class
MultiHeadAttentiondanFeedForward— mesin attention dan FFN yang sama dengan dua chapter sebelumnya. - Baris 75-112: class
EncoderBlockdanDecoderBlock— kedua blok pre-LN persis dari chapter Transformer Encoder dan Transformer Decoder , kini menjadi sub-modul, bukan topik utama. - Baris 115-133: class
TransformerMini, sang perakit. Konstruktor menyiapkannn.Embedding(11, d_model)untuk sepuluh digit plus tokenBOS, PE yang disimpan sebagai buffer lewatregister_buffer(baris 122) agar ikut pindah bersama model, satu blok encoder, satu blok decoder, dan layer lineard_model -> 10. Padaforward, baris 130-131 mengalikan embedding dengansqrt(d_model)sebelum menjumlahkan PE — penskalaan standar agar skala embedding tidak menenggelamkan sinyal posisi. - Baris 136-146: dua fungsi data.
buat_datamembangkitkan sumber acak dan target geser siklik lewatroll(-1);geser_kananmenyiapkan masukan decoder teacher forcing — kolomBOSdi depan, target dipotong satu di belakang. - Baris 164-174: siklus pelatihan. Satu forward memprediksi keenam posisi sekaligus (mask kausal menjaga keabsahan), cross-entropy dihitung atas seluruh posisi, dan learning rate diturunkan ke
3e-4pada epoch 300 lewatparam_groups(baris 165-167). - Baris 178-187: fungsi
greedy— inferensi autoregressive. Decoder dimulai dariBOS; tiap langkah mengambil prediksi posisi terakhir (logits[:, -1]) lalu menempelkannya ke masukan. Demo ini sengaja tanpa cache: seluruh prefiks dihitung ulang tiap langkah — jalan sederhana yang masih terjangkau pada urutan enam token. - Baris 190-207: evaluasi. Dari 256 urutan uji yang belum pernah dilihat, 254 cocok penuh — standar enam-token-semua-benar, jauh lebih ketat daripada akurasi per token.
- Baris 210-219: pencetakan bobot cross-attention per head untuk contoh uji, dihitung dengan masukan teacher forcing agar keenam baris tersedia sekaligus.
Membaca Aturan yang Dipelajari Model #
Bagian paling berharga ada di dua matriks terakhir. Head 1 menunjukkan pola yang rapi: baris 0 memusatkan bobot pada kolom 1 (0,77), baris 1 pada kolom 2 (0,63), baris 2 pada kolom 3 (0,55), baris 3 pada kolom 4 (0,54), baris 4 pada kolom 5 (0,47), dan baris 5 — posisi target terakhir yang harus kembali ke awal siklus — pada kolom 0 (0,67). Itu persis aturan y_t = x_(t+1) milik tugas, termasuk pembungkusannya. Head 2 memperlihatkan pola serupa dengan puncak yang sedikit lebih tumpul. Model tidak pernah diberi tahu aturan ini; aturan itu muncul dari pelatihan dan terbaca langsung dari bobot attention — cara kerja interpretability paling sederhana yang juga dipakai untuk memeriksa model terjemahan sungguhan.
Rangkaian lengkap Transformer kini tuntas dari embedding sampai distribusi keluaran, dan seluruh jalurnya terbukti hidup. Satu komponen masih menyimpan rincian yang layak dibedah sendiri: mask kausal dan mask padding yang selama ini kita pakai lewat masked_fill. Chapter berikutnya menghitung keduanya dengan angka nyata — skor segitiga, softmax yang menghasilkan bobot nol persis, dan gabungan dua mask dalam satu matriks.