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:

python
"""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:

text
== 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:

  1. Baris 21-49: class MultiHeadAttention versi diperluas. forward kini menerima q, k, v terpisah sehingga layer yang sama bisa berperan sebagai self-attention (q = k = v) maupun cross-attention (q dari decoder, k/v dari encoder); baris 45-46 mengisi skor posisi terlarang dengan -inf lewat masked_fill sebelum softmax.
  2. Baris 52-61: class FeedForward — identik dengan blok encoder: dua layer linear dengan GELU, bekerja per token.
  3. Baris 64-86: class DecoderBlock. Konstruktor baris 67-74 merangkai dua MHA, satu FFN, dan tiga layer normalization. Pada forward, baris 79 membangun mask kausal: torch.triu(..., diagonal=1) menghasilkan segitiga atas True — posisi kolom lebih besar dari baris; baris 81 menjalankan self-attention ber-mask; baris 82-83 menjalankan cross-attention dengan query dari ln2(h) dan key-value dari ln2(memori); baris 85 menutup dengan FFN.
  4. 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.
  5. 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.
  6. Baris 115-123: verifikasi kausalitas. Hanya token posisi 4 dan 5 yang diubah; keluaran posisi 0..3 selisih persis 0.000e+00 sedangkan posisi 4..5 berubah — bukti angka bahwa informasi masa depan tidak pernah sampai ke posisi sebelumnya.
  7. 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.
  8. Baris 135-137: total parameter 3.344 — dua MHA (2.176), satu FFN (1.072), tiga layer normalization (96).

Encoder lawan Decoder #

AspekEncoderBlockDecoderBlock
Jumlah sub-layer2: self-attention + FFN3: masked self-attn + cross-attn + FFN
Self-attentiontanpa mask, dua arahmask kausal, hanya masa lalu
Sumber key-value attentiontoken sendiritoken sendiri + memori encoder
Masukansatu jalur xdua jalur x dan memori
Layer normalization2 buah3 buah
Parameter (contoh kita)2.2243.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.