Referensi cepat transformer dan attention mechanism. Scaled dot-product, multi-head attention, positional encoding, encoder-decoder, masking, dan inference. Buat kamu yang belajar AI engineering.
Sebelum transformer, model sekuensial seperti RNN membaca token satu per satu. Setiap token hanya bisa melihat hidden state token sebelumnya. Ini lambat untuk paralelisasi dan sulit membawa informasi jarak jauh.
Attention membalik pendekatan itu. Setiap token melihat seluruh token lain sekaligus dan memutuskan seberapa penting masing-masing. Tidak ada urutan wajib baca. Semua posisi diproses paralel, dan informasi antar token yang jauh tetap bisa dihubungkan langsung.
sumber: "saya" "suka" "belajar" "koding"
target: "aku" "senang" "coding"
perhatian token target "senang" tersebar ke token sumber
yang paling relevan, misalnya "suka" dan "belajar"Bobot perhatian adalah distribusi probabilitas. Jumlahnya satu untuk tiap posisi query. Nilai besar berarti token itu dianggap relevan.
Rumus inti attention adalah satu baris. Query dikalikan dengan key, dibagi akar dimensi key, di-softmax, lalu dipakai membobot value.
attention(Q, K, V) = softmax(Q K^T / sqrt(d_k)) VPembagian dengan sqrt(d_k) menjaga dot product agar tidak membesar saat dimensi key naik. Tanpa pembagian itu, nilai dot product masuk ke daerah saturasi softmax dan gradient jadi kecil.
import math
import torch
import torch.nn.functional as F
def scaled_dot_product_attention(query, key, value, mask=None):
"""Query, key, value ber-shape (batch, heads, seq_len, dim)."""
dim_key = key.size(-1)
scores = query @ key.transpose(-2, -1) / math.sqrt(dim_key)
if mask is not None:
scores = scores.masked_fill(mask == 0, float("-inf"))
weights = F.softmax(scores, dim=-1)
return weights @ value
batch, heads, seq_len, dim = 2, 4, 10, 32
query = torch.randn(batch, heads, seq_len, dim)
key = torch.randn(batch, heads, seq_len, dim)
value = torch.randn(batch, heads, seq_len, dim)
output = scaled_dot_product_attention(query, key, value)
assert output.shape == (batch, heads, seq_len, dim)key.transpose(-2, -1) menukar dua dimensi terakhir sehingga perkalian menghasilkan skor antara tiap query dan tiap key. masked_fill menempatkan -inf pada posisi yang harus diabaikan. Softmax dari -inf menjadi nol.
Satu kepala attention hanya menangkap satu jenis hubungan. Delapan kepala memakai proyeksi berbeda sehingga model bisa melihat hubungan sintaksis dan semantik dalam subspace yang terpisah.
MultiHead(Q, K, V) = concat(head_1, ..., head_h) W_O
head_i = attention(Q W_Q_i, K W_K_i, V W_V_i)import torch
from torch import nn
class MultiHeadAttention(nn.Module):
def __init__(self, dim: int, num_heads: int, dropout: float = 0.0) -> None:
super().__init__()
assert dim % num_heads == 0, "dim harus habis dibagi num_heads"
self.dim = dim
self.num_heads = num_heads
self.head_dim = dim // num_heads
self.wq = nn.Linear(dim, dim, bias=False)
self.wk = nn.Linear(dim, dim, bias=False)
self.wv = nn.Linear(dim, dim, bias=False)
self.wo = nn.Linear(dim, dim, bias=False)
self.dropout = nn.Dropout(dropout)
def forward(self, query, key, value, mask=None):
batch, q_len, _ = query.shape
_, kv_len, _ = key.shape
q = self.wq(query).view(batch, q_len, self.num_heads, self.head_dim)
k = self.wk(key).view(batch, kv_len, self.num_heads, self.head_dim)
v = self.wv(value).view(batch, kv_len, self.num_heads, self.head_dim)
q = q.transpose(1, 2) # (batch, heads, q_len, head_dim)
k = k.transpose(1, 2)
v = v.transpose(1, 2)
scores = q @ k.transpose(-2, -1) / math.sqrt(self.head_dim)
if mask is not None:
scores = scores.masked_fill(mask == 0, float("-inf"))
weights = self.dropout(F.softmax(scores, dim=-1))
context = weights @ v
context = context.transpose(1, 2).contiguous().view(batch, q_len, self.dim)
return self.wo(context)bias=False pada proyeksi mengikuti praktik umum transformer modern, tapi ini bukan keharusan. view lalu transpose mengubah layout dari (batch, seq, heads, dim) menjadi (batch, heads, seq, dim) supaya operasi per kepala lebih rapi.
Attention tidak peduli urutan token. ["kucing", "kejar", "tikus"] dan ["tikus", "kejar", "kucing"] menghasilkan distribusi perhatian yang berbeda makna, namun tanpa informasi posisi keduanya terlihat sama secara aritmetika.
Dua cara memberi informasi posisi: positional encoding sinyal sinus yang ditambahkan sebelum layer pertama, atau positional embedding yang ikut dipelajari.
class SinusoidalPositionalEncoding(nn.Module):
def __init__(self, dim: int, max_len: int = 512) -> None:
super().__init__()
position = torch.arange(max_len).unsqueeze(1)
div_term = torch.exp(
torch.arange(0, dim, 2) * (-math.log(10000.0) / dim)
)
encoding = torch.zeros(max_len, dim)
encoding[:, 0::2] = torch.sin(position * div_term)
encoding[:, 1::2] = torch.cos(position * div_term)
self.register_buffer("encoding", encoding.unsqueeze(0))
def forward(self, x: torch.Tensor) -> torch.Tensor:
return x + self.encoding[:, : x.size(1)]
seq_len, dim = 20, 64
pos_encoder = SinusoidalPositionalEncoding(dim)
x = torch.randn(2, seq_len, dim)
out = pos_encoder(x)
assert out.shape == (2, seq_len, dim)register_buffer memastikan tensor encoding ikut pindah ke device dan tersimpan di state_dict, tetapi tidak diperbarui optimizer karena bukan parameter. Sinyal sinus memakai frekuensi berbeda tiap dimensi sehingga model dapat belajar hubungan posisi relatif.
Encoder memproses seluruh input sekaligus. Setiap block berisi multi-head self-attention, skip connection, layer norm, lalu feed-forward network. Skip connection menjaga gradient mengalir ke layer yang dalam.
block:
x = x + attention(layer_norm(x))
x = x + feed_forward(layer_norm(x))Urutan pre-norm ini adalah standar transformer modern. Layer norm ditaruh sebelum sublayer, bukan sesudah, supaya training lebih stabil.
class TransformerEncoderBlock(nn.Module):
def __init__(self, dim: int, num_heads: int, ff_dim: int, dropout: float = 0.1) -> None:
super().__init__()
self.attention = MultiHeadAttention(dim, num_heads, dropout)
self.feed_forward = nn.Sequential(
nn.Linear(dim, ff_dim),
nn.GELU(),
nn.Dropout(dropout),
nn.Linear(ff_dim, dim),
nn.Dropout(dropout),
)
self.norm1 = nn.LayerNorm(dim)
self.norm2 = nn.LayerNorm(dim)
self.dropout = nn.Dropout(dropout)
def forward(self, x: torch.Tensor, mask=None) -> torch.Tensor:
attended = self.attention(self.norm1(x), self.norm1(x), self.norm1(x), mask)
x = x + self.dropout(attended)
x = x + self.feed_forward(self.norm2(x))
return xSelf-attention memakai query == key == value, karena token membandingkan dirinya dengan token lain dalam urutan yang sama. Cross-attention nanti memakai query dari satu urutan dan key/value dari urutan lain.
Dua mask muncul terus. Padding mask menyembunyikan token kosong supaya tidak ikut memengaruhi perhatian. Causal mask mencegah token melihat token sesudahnya, wajib untuk decoder autoregressive.
def create_padding_mask(tokens: torch.Tensor, pad_idx: int) -> torch.Tensor:
# 1 untuk posisi valid, 0 untuk padding. Shape (batch, seq).
return (tokens != pad_idx).to(torch.long)
def create_causal_mask(seq_len: int) -> torch.Tensor:
# Segitiga bawah bernilai 1, token hanya melihat posisi <= dirinya.
return torch.tril(torch.ones(seq_len, seq_len, dtype=torch.long))
pad_mask = create_padding_mask(torch.tensor([[2, 5, 0, 0]]), pad_idx=0)
causal_mask = create_causal_mask(4)
print(pad_mask) # tensor([[1, 1, 0, 0]])
print(causal_mask)Pada attention dengan shape (batch, heads, q_len, kv_len), mask juga harus ber-shape broadcastable ke (q_len, kv_len) atau (batch, 1, q_len, kv_len). Satu baris masked_fill(mask == 0, float("-inf")) sudah cukup selama mask mengembang ke dimensi batch dan head.
Decoder autoregressive menghasilkan output satu token per langkah. Setiap block decoder punya dua attention: self-attention dengan causal mask, lalu cross-attention yang menarik informasi dari encoder.
class TransformerDecoderBlock(nn.Module):
def __init__(self, dim: int, num_heads: int, ff_dim: int, dropout: float = 0.1) -> None:
super().__init__()
self.self_attention = MultiHeadAttention(dim, num_heads, dropout)
self.cross_attention = MultiHeadAttention(dim, num_heads, dropout)
self.feed_forward = nn.Sequential(
nn.Linear(dim, ff_dim),
nn.GELU(),
nn.Linear(ff_dim, dim),
)
self.norm1 = nn.LayerNorm(dim)
self.norm2 = nn.LayerNorm(dim)
self.norm3 = nn.LayerNorm(dim)
def forward(self, x, encoder_output, causal_mask=None, padding_mask=None):
# Self-attention memakai query, key, value dari urutan target.
x = x + self.self_attention(self.norm1(x), self.norm1(x), self.norm1(x), causal_mask)
# Cross-attention: query dari target, key dan value dari encoder.
x = x + self.cross_attention(self.norm2(x), encoder_output, encoder_output, padding_mask)
x = x + self.feed_forward(self.norm3(x))
return xPerhatikan perbedaan isi tiap attention. Self-attention di decoder memakai tiga input yang sama. Cross-attention memakai query dari decoder dan key/value dari encoder. Di sinilah informasi sumber masuk ke proses generasi.
PyTorch menyediakan komponen jadi: nn.TransformerEncoderLayer, nn.TransformerEncoder, dan nn.Transformer untuk model encoder-decoder utuh. Tulis sendiri saat belajar supaya paham, lalu pakai komponen bawaan untuk produksi.
import torch
from torch import nn
encoder_layer = nn.TransformerEncoderLayer(
d_model=512,
nhead=8,
dim_feedforward=2048,
dropout=0.1,
activation="gelu",
batch_first=True,
norm_first=True,
)
encoder = nn.TransformerEncoder(encoder_layer, num_layers=6)
src = torch.randn(2, 10, 512) # (batch, seq_len, d_model) karena batch_first
memory = encoder(src)
assert memory.shape == src.shapebatch_first=True membuat input ber-shape (batch, seq_len, dim) seperti kebiasaan umum. Default PyTorch tanpa flag itu adalah (seq_len, batch, dim), jadi pastikan kamu konsisten antara layer dan input.
nn.Transformer menggabungkan encoder dan decoder. Target memakai embedding sendiri dan mask kausal untuk menutupi token masa depan.
transformer = nn.Transformer(
d_model=512,
nhead=8,
num_encoder_layers=6,
num_decoder_layers=6,
dim_feedforward=2048,
dropout=0.1,
batch_first=True,
)
src = torch.randint(0, 1000, (2, 10)) # token sumber
tgt = torch.randint(0, 1000, (2, 15)) # token target
src_embed = nn.Embedding(1000, 512)(src)
tgt_embed = nn.Embedding(1000, 512)(tgt)
tgt_mask = transformer.generate_square_subsequent_mask(tgt.size(1))
output = transformer(src_embed, tgt_embed, tgt_mask=tgt_mask)
assert output.shape == (2, 15, 512)generate_square_subsequent_mask membuat mask kausal untuk panjang urutan target. Output decoder sepanjang target lalu diproyeksikan ke distribusi vocab memakai layer linear terpisah.
scaled_dot_product_attention BawaanPyTorch punya implementasi fused yang lebih cepat dan hemat memori dari menulis attention manual. Gunakan ini di kode produksi.
import torch.nn.functional as F
query = torch.randn(2, 8, 10, 64) # (batch, heads, seq, dim)
key = torch.randn(2, 8, 10, 64)
value = torch.randn(2, 8, 10, 64)
# Attention biasa
out = F.scaled_dot_product_attention(query, key, value)
# Attention kausal tanpa membangun mask manual
causal_out = F.scaled_dot_product_attention(
query, key, value, is_causal=True
)
# Dengan dropout selama training
dropout_out = F.scaled_dot_product_attention(
query, key, value, dropout_p=0.1, training=True
)is_causal=True menggantikan mask segitiga bawah manual dan membuka kernel yang lebih efisien. nn.MultiheadAttention bawaan juga memakai kernel fused ini otomatis saat need_weights=False.
Saat menghasilkan teks, decoder dipanggil berulang. Tiap langkah menghasilkan satu token baru, token itu ditambahkan ke urutan, lalu proses diulang sampai token akhir atau panjang maksimum tercapai.
@torch.no_grad()
def greedy_decode(transformer, src_tokens, tgt_embed, projection,
bos_idx, eos_idx, max_len=50):
transformer.eval()
src_embed = tgt_embed.weight # contoh sederhana, sharing embedding
memory = transformer.encoder(src_embed[src_tokens].unsqueeze(0))
generated = [bos_idx]
for _ in range(max_len):
tgt_tokens = torch.tensor([generated]).to(src_tokens.device)
tgt_mask = transformer.generate_square_subsequent_mask(len(generated))
tgt_hidden = tgt_embed(tgt_tokens)
out = transformer.decoder(tgt_hidden, memory, tgt_mask=tgt_mask)
next_logits = projection(out[:, -1, :])
next_token = next_logits.argmax(dim=-1).item()
generated.append(next_token)
if next_token == eos_idx:
break
return generatedargmax tiap langkah adalah greedy decoding. top-k dan nucleus sampling memilih dari subset token berprobabilitas tinggi untuk hasil lebih beragam. Suhu menajamkan atau meratakan distribusi sebelum sampling. Selalu kirim tgt_mask saat decode supaya model tidak melihat token masa depan.
Model generatif modern seperti GPT memakai arsitektur decoder-only. Tidak ada encoder terpisah, dan self-attention memakai causal mask. Saat menghasilkan teks, menghitung ulang attention untuk seluruh urutan di tiap langkah itu boros.
KV cache menyimpan key dan value dari langkah sebelumnya. Pada langkah baru, model hanya menghitung key dan value untuk token terakhir, lalu menyambungkannya ke cache. Biaya komputasi tiap langkah turun drastis untuk urutan panjang.
langkah 1: hitung K, V untuk [t1] -> simpan di cache
langkah 2: hitung K, V untuk [t2] saja -> gabung dengan cache
langkah 3: hitung K, V untuk [t3] saja -> gabung dengan cacheKonsekuensinya penting untuk inferensi: batch dengan urutan beda panjang memakai padding, cache tiap sample dipotong sesuai panjang aslinya, dan memori tumbuh seiring panjang urutan dikali jumlah layer. Implementasi yang cermat menyimpan cache per layer dan memakai cache_position untuk indexing.
Model terbaru sering meninggalkan positional encoding sinus tambahan dan memakai RoPE. Alih-alih menambahkan vektor posisi ke embedding, RoPE memutar query dan key berdasarkan posisi relatifnya. Informasi posisi masuk ke perkalian dot product lewat sudut rotasi.
def apply_rotary_embedding(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
"""Rotasi separuh dimensi pertama x dan separuh terakhir."""
half = x.shape[-1] // 2
x1, x2 = x[..., :half], x[..., half:]
rotated = torch.cat((-x2, x1), dim=-1)
return x * cos + rotated * sinKeuntungan utama RoPE adalah kemampuannya menangani posisi relatif secara alami, sehingga model lebih mudah melakukan ekstrapolasi ke panjang urutan melebihi data training. RoPE juga tidak membutuhkan tabel posisi maksimum yang besar.
Greedy decoding memilih token dengan probabilitas tertinggi dan bisa menghasilkan teks berulang. Sampling memakai distribusi probabilitas dengan suhu yang mengatur keacakan. Suhu rendah mendekati greedy, suhu tinggi membuat output lebih bervariasi.
@torch.no_grad()
def sample_next_token(logits: torch.Tensor, temperature: float = 1.0) -> torch.Tensor:
if temperature <= 0:
return logits.argmax(dim=-1)
scaled = logits / temperature
probabilities = F.softmax(scaled, dim=-1)
return torch.multinomial(probabilities, num_samples=1).squeeze(-1)torch.multinomial menarik sampel berdasarkan distribusi probabilitas. Gabungkan dengan top_k atau top_p untuk memotong ekor distribusi yang berisi token sangat tidak mungkin. Jangan pakai suhu sembarangan pada output yang harus faktual.
Bug di transformer biasanya muncul dari shape dan mask yang tidak sejajar. Periksa hal ini lebih dulu:
d_model.d_model habis dibagi num_heads.(batch, 1, q_len, kv_len) atau (q_len, kv_len).masked_fill memakai float("-inf"), bukan nol.batch_first konsisten antara layer dan tensor input.# Tes perhatian pada satu token. Bobot harus menyebar, dan
# tiap baris bobot harus berjumlah 1.
q = torch.randn(1, 1, 3, 8)
k = torch.randn(1, 1, 5, 8)
v = torch.randn(1, 1, 5, 8)
out = F.scaled_dot_product_attention(q, k, v)
assert out.shape == (1, 1, 3, 8)
# Softmax per baris harus berjumlah 1.
scores = (q @ k.transpose(-2, -1)) / math.sqrt(8)
row_sum = F.softmax(scores, dim=-1).sum(dim=-1)
assert torch.allclose(row_sum, torch.ones_like(row_sum))Kalau loss nan, cek pembagian sqrt(dim_key), pastikan tidak ada softmax pada nilai inf, dan periksa apakah learning rate terlalu besar. Bobot perhatian yang seragam ke semua token biasanya tanda model belum belajar atau positional encoding hilang.
Attention: mekanisme pembobotan nilai berdasarkan kesesuaian query dan key.
Autoregressive: model menghasilkan token satu per satu memakai output sebelumnya.
Causal mask: mask yang mencegah token melihat posisi sesudahnya.
Cross-attention: attention yang query-nya dari satu urutan dan key/value dari urutan lain.
Decoder: bagian transformer yang menghasilkan output secara autoregressive.
Encoder: bagian transformer yang memproses seluruh input sekaligus.
Feed-forward network: dua layer linear dengan activation di antara, diterapkan per posisi.
Layer norm: normalisasi per sample yang menstabilkan training.
Logits: skor mentah sebelum softmax, dipakai untuk memilih token berikutnya.
Multi-head attention: beberapa kepala attention dengan proyeksi terpisah.
Padding mask: mask yang menyembunyikan token kosong.
Positional encoding: representasi posisi token yang ditambahkan ke embedding.
Self-attention: attention dengan query, key, dan value dari urutan yang sama.
Softmax: fungsi yang mengubah skor menjadi distribusi probabilitas berjumlah satu.
Token: satuan input, bisa kata, subword, atau karakter.
Vocab: daftar seluruh token yang dikenal model.
Login atau daftar akun gratis untuk membaca cheat sheet ini.