Blok encoder dari chapter Transformer Encoder
bagus untuk memahami urutan, tetapi penghasilan urutan baru menuntut aturan yang berbeda: saat memprediksi token ke-t, decoder belum boleh tahu isi token ke-t+1 — kalau boleh, tugas prediksi menjadi curang dan model tidak belajar apa-apa. Decoder karenanya membawa dua mekanisme yang tidak ada di encoder: masked self-attention yang memutus pandangan ke masa depan, dan cross-attention yang membaca keluaran encoder sebagai informasi sumber. Chapter ini membangun DecoderBlock tiga sub-layer dalam PyTorch, mengujinya dengan dua verifikasi nyata — kausalitas dan jalur memori — lalu menutup dengan tabel perbandingan terhadap blok encoder.
Tiga Sub-Layer dan Dua Jalur Masukan #
Alur satu blok decoder pre-LN berjalan sebagai berikut. Pertama, masked self-attention memproses token decoder dengan mask kausal: skor attention pada posisi masa depan diisi -inf sebelum softmax sehingga bobotnya menjadi nol — token hanya boleh menoleh ke dirinya dan token-token sebelumnya. Kedua, cross-attention menghubungkan decoder ke sumber: query diambil dari representasi decoder, sedangkan key dan value diambil dari memori encoder — keluaran tumpukan encoder atas urutan sumber. Ketiga, feed-forward memproses tiap token secara mandiri, sama persis dengan blok encoder. Ketiga sub-layer memakai residual connection dan layer normalization.
Diagram di atas memperlihatkan posisi berbeda kedua attention: self-attention membaca jalur decoder sendiri, sedangkan cross-attention menerima dua jalur — query dari decoder, key-value dari memori encoder. Mekanisme mask kausalnya sendiri akan kita bedah angka demi angka di chapter Masked Self-Attention ; chapter ini cukup memakai hasilnya.
Implementasi DecoderBlock #
Program berikut memakai class MultiHeadAttention yang diperluas dari chapter Multi-Head Attention
— kini forward menerima q, k, v terpisah plus mask boolean. Konfigurasi uji: d_model = 16, empat head, d_ff = 32; token decoder enam posisi, memori encoder lima posisi:
"""Blok decoder Transformer: masked self-attention + cross-attention + FFN.
Masukan dua jalur: token decoder x (B, Tdec, D) dan memori encoder
(B, Tsrc, D). Tiga sub-layer, semuanya pre-LN: self-attention ber-mask
kausal, cross-attention (Q dari decoder, K/V dari memori encoder),
feed-forward. Dua verifikasi nyata: (1) kausalitas -- mengubah token
decoder pada posisi >= 4 tidak mengubah keluaran posisi 0..3; (2) jalur
memori -- mengubah satu token memori encoder mengubah keluaran decoder.
PyTorch 2.14, CPU.
"""
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
torch.manual_seed(0)
np.set_printoptions(precision=3, suppress=True)
class MultiHeadAttention(nn.Module):
"""MHA chapter multi-head-attention, diperluas: q/k/v terpisah + mask.
mask: tensor boolean (Tq, Tk); nilai True berarti posisi DILARANG
diperhatikan (skornya diisi -inf sebelum softmax).
"""
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 DecoderBlock(nn.Module):
"""Tiga sub-layer pre-LN: masked self-attn, cross-attn, feed-forward."""
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):
"""x: token decoder (B, Tdec, D); memori: keluaran encoder (B, Tsrc, D)."""
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
D_MODEL, NUM_HEADS, D_FF = 16, 4, 32
blok = DecoderBlock(D_MODEL, NUM_HEADS, D_FF)
x_dec = torch.randn(2, 6, D_MODEL) # token decoder (mis. target geser kanan)
memori = torch.randn(2, 5, D_MODEL) # keluaran encoder atas urutan sumber
out, bobot_cross = blok(x_dec, memori)
print("== Blok decoder: masked self-attention + cross-attention + FFN ==")
print()
print(f"token decoder x_dec : bentuk {tuple(x_dec.shape)}")
print(f"memori encoder : bentuk {tuple(memori.shape)}")
print(f"keluaran blok : bentuk {tuple(out.shape)}")
kausal = torch.triu(torch.ones(6, 6, dtype=torch.bool), diagonal=1)
_, bobot_self = blok.self_attn(blok.ln1(x_dec), blok.ln1(x_dec),
blok.ln1(x_dec), kausal)
print(f"bobot self-attention : bentuk {tuple(bobot_self.shape)}")
print(f"bobot cross-attention : bentuk {tuple(bobot_cross.shape)}")
print()
print("Bobot cross-attention (rata-rata 4 head, batch 0):")
print("baris = posisi decoder, kolom = posisi sumber")
print(np.round(bobot_cross[0].mean(dim=0).detach().numpy(), 3))
print("jumlah tiap baris:",
np.round(bobot_cross[0].mean(dim=0).sum(dim=1).detach().numpy(), 3))
print()
print("== Verifikasi 1: mask kausal memutus arah menuju masa depan ==")
x_ubah = x_dec.clone()
x_ubah[:, 4:, :] += 1.0 # ubah hanya token posisi 4 dan 5
out_ubah, _ = blok(x_ubah, memori)
s_awal = (out[:, :4] - out_ubah[:, :4]).abs().max().item()
s_akhir = (out[:, 4:] - out_ubah[:, 4:]).abs().max().item()
print(f"token posisi 4-5 diubah -> selisih keluaran posisi 0..3 : {s_awal:.3e}")
print(f" selisih keluaran posisi 4..5 : {s_akhir:.3f}")
print("-> keluaran posisi 0..3 identik; masa depan tidak terbaca")
print()
print("== Verifikasi 2: jalur memori (cross-attention) ==")
mem_ubah = memori.clone()
mem_ubah[:, 2, 0] += 1.0 # ubah satu fitur satu token sumber
out_mem, _ = blok(x_dec, mem_ubah)
s_mem = (out - out_mem).abs().max().item()
print(f"memori posisi 2 diubah -> selisih maksimum keluaran decoder: {s_mem:.3f}")
print("-> isi encoder mengalir ke decoder lewat cross-attention")
print()
n_total = sum(p.numel() for p in blok.parameters())
print(f"parameter total blok decoder: {n_total}")
print("(2 MHA self+cross + 1 FFN + 3 layer norm)")Output yang Dihasilkan:
== Blok decoder: masked self-attention + cross-attention + FFN ==
token decoder x_dec : bentuk (2, 6, 16)
memori encoder : bentuk (2, 5, 16)
keluaran blok : bentuk (2, 6, 16)
bobot self-attention : bentuk (2, 4, 6, 6)
bobot cross-attention : bentuk (2, 4, 6, 5)
Bobot cross-attention (rata-rata 4 head, batch 0):
baris = posisi decoder, kolom = posisi sumber
[[0.203 0.178 0.22 0.184 0.215]
[0.227 0.207 0.172 0.2 0.195]
[0.173 0.204 0.209 0.228 0.187]
[0.224 0.165 0.251 0.154 0.206]
[0.162 0.178 0.224 0.203 0.233]
[0.242 0.186 0.17 0.203 0.199]]
jumlah tiap baris: [1. 1. 1. 1. 1. 1.]
== Verifikasi 1: mask kausal memutus arah menuju masa depan ==
token posisi 4-5 diubah -> selisih keluaran posisi 0..3 : 0.000e+00
selisih keluaran posisi 4..5 : 1.000
-> keluaran posisi 0..3 identik; masa depan tidak terbaca
== Verifikasi 2: jalur memori (cross-attention) ==
memori posisi 2 diubah -> selisih maksimum keluaran decoder: 0.067
-> isi encoder mengalir ke decoder lewat cross-attention
parameter total blok decoder: 3344
(2 MHA self+cross + 1 FFN + 3 layer norm)Mari kita telaah kode di atas baris per baris:
- Baris 21-49: class
MultiHeadAttentionversi diperluas.forwardkini menerimaq,k,vterpisah sehingga layer yang sama bisa berperan sebagai self-attention (q = k = v) maupun cross-attention (qdari decoder,k/vdari encoder); baris 45-46 mengisi skor posisi terlarang dengan-inflewatmasked_fillsebelum softmax. - Baris 52-61: class
FeedForward— identik dengan blok encoder: dua layer linear dengan GELU, bekerja per token. - Baris 64-86: class
DecoderBlock. Konstruktor baris 67-74 merangkai dua MHA, satu FFN, dan tiga layer normalization. Padaforward, baris 79 membangun mask kausal:torch.triu(..., diagonal=1)menghasilkan segitiga atasTrue— posisi kolom lebih besar dari baris; baris 81 menjalankan self-attention ber-mask; baris 82-83 menjalankan cross-attention dengan query dariln2(h)dan key-value dariln2(memori); baris 85 menutup dengan FFN. - Baris 89-106: uji bentuk. Token decoder
(2, 6, 16)dan memori(2, 5, 16)menghasilkan keluaran(2, 6, 16); bobot self-attention(2, 4, 6, 6)dan bobot cross-attention(2, 4, 6, 5)— jumlah kolom mengikuti panjang urutan yang menjadi key. - Baris 108-112: matriks bobot cross-attention 6x5 (rata-rata empat head) — bobot mainan acak ini tersebar nyaris merata karena bobot layer belum dilatih; tiap baris tetap berjumlah 1.
- Baris 115-123: verifikasi kausalitas. Hanya token posisi 4 dan 5 yang diubah; keluaran posisi 0..3 selisih persis
0.000e+00sedangkan posisi 4..5 berubah — bukti angka bahwa informasi masa depan tidak pernah sampai ke posisi sebelumnya. - Baris 126-132: verifikasi jalur memori. Satu fitur pada token sumber posisi 2 diubah; keluaran decoder bergeser 0,067 — isi encoder memang mengalir ke decoder.
- Baris 135-137: total parameter 3.344 — dua MHA (2.176), satu FFN (1.072), tiga layer normalization (96).
Encoder lawan Decoder #
| Aspek | EncoderBlock | DecoderBlock |
|---|---|---|
| Jumlah sub-layer | 2: self-attention + FFN | 3: masked self-attn + cross-attn + FFN |
| Self-attention | tanpa mask, dua arah | mask kausal, hanya masa lalu |
| Sumber key-value attention | token sendiri | token sendiri + memori encoder |
| Masukan | satu jalur x | dua jalur x dan memori |
| Layer normalization | 2 buah | 3 buah |
| Parameter (contoh kita) | 2.224 | 3.344 |
Dokumentasi torch.nn.MultiheadAttention
mengikuti konvensi yang sama dengan implementasi manual kita: argumen attn_mask bernilai True berarti posisi diblokir, dan masukan key-value boleh datang dari tensor berbeda dengan query — prasyarat cross-attention. Contoh chapter ini diuji pada CPU dengan PyTorch 2.14.
Encoder dan decoder kini sama-sama siap sebagai unit. Yang belum ada adalah rakitan yang menghubungkannya menjadi satu model penerjemah utuh: embedding, positional encoding, tumpukan blok, hingga layer linear penghasil distribusi token. Chapter berikutnya merakit semuanya dan melatih Transformer mainan sampai aturan tugasnya benar-benar terpelajari.