Satu lapisan scaled dot-product attention menghasilkan satu matriks bobot — satu pola relasi untuk seluruh pasangan token. Kalimat nyata membutuhkan banyak pola sekaligus: relasi sintaks antara kata kerja dan objeknya, relasi makna antara kata dan topiknya, relasi posisi antara kata dan tetangganya. Transformer menjawabnya dengan menjalankan h lapisan attention kecil secara paralel pada subspace yang berbeda — multi-head attention — lalu merangkai hasilnya dengan satu proyeksi W_O. Chapter ini memperlihatkan dua head yang menghasilkan dua pola berbeda pada token yang sama, membangun class PyTorch manual untuk mekanisme lengkapnya, dan memverifikasi angkanya terhadap torch.nn.MultiheadAttention.
Satu Head Hanya Menangkap Satu Pola #
Bayangkan satu pembaca yang hanya boleh menyorot satu aspek kalimat: ia harus memilih — menyorot struktur sintaks ataukah topik pembicaraan. Satu head attention berada pada posisi itu: matriks bobotnya memaksa seluruh pasangan token berbagi satu pola takaran. Multi-head mengajak beberapa pembaca sekaligus, masing-masing dengan sorotannya sendiri. Rumusnya ringkas:
MultiHead(X) = Concat(head_1, ..., head_h) @ W_O
head_i = Attention(X @ W_Q^i, X @ W_K^i, X @ W_V^i)Tiap head memproyeksikan input ke subspace berukuran d_model / h, menjalankan scaled dot-product attention di sana, lalu hasil h potongan dirangkai (concat) dan diproyeksikan W_O. Trik pembagian subspace membuat biayanya hemat: total parameter proyeksi tetap 4 x d_model^2 — sama dengan satu head penuh — karena keempat matriks tetap berukuran d_model x d_model dan hanya dipecah pakainya.
Dua Head, Dua Pola #
Contoh NumPy berikut memakai tiga token berdimensi enam. Kolom 0-2 menjadi subspace kelas kata (token 0 dan 2 sekelas, token 1 berbeda) dan kolom 3-5 menjadi subspace topik (token 0 dan 1 satu topik, token 2 berbeda). Proyeksi blok mengarahkan head 1 untuk hanya membaca kolom 0-2 dan head 2 hanya kolom 3-5 — dua sorotan yang tidak saling mengganggu:
"""Multi-head attention: beberapa head, beberapa pola relasi.
Bagian A (NumPy): tiga token diproyeksikan ke dua subspace terpisah.
Head 1 (kolom 0-2, "kelas kata") dan head 2 (kolom 3-5, "topik")
menghasilkan matriks bobot yang berbeda; keluaran keduanya dirangkai
lewat concat lalu proyeksi W_O.
Bagian B (PyTorch): class MultiHeadAttention manual berbasis nn.Linear,
diverifikasi angka demi angka terhadap torch.nn.MultiheadAttention
dengan bobot yang disalin identik (PyTorch 2.14, CPU).
"""
import numpy as np
import torch
import torch.nn as nn
np.set_printoptions(precision=3, suppress=True)
def softmax_baris(M):
"""Softmax per baris, stabil terhadap luapan exp."""
e = np.exp(M - M.max(axis=1, keepdims=True))
return e / e.sum(axis=1, keepdims=True)
def attention(Q, K, V):
"""Scaled dot-product attention satu head; kembalikan bobot dan hasil."""
d_k = Q.shape[1]
A = softmax_baris(Q @ K.T / np.sqrt(d_k))
return A, A @ V
print("== Bagian A: dua head, dua pola (NumPy) ==")
print()
X = np.array(
[
[1.0, 0.0, 0.0, 0.0, 1.0, 0.0], # token 0: kelas kata 1, topik 2
[0.0, 1.0, 0.0, 0.0, 1.0, 0.0], # token 1: kelas kata 2, topik 2
[1.0, 0.0, 0.0, 1.0, 0.0, 0.0], # token 2: kelas kata 1, topik 1
]
)
# Proyeksi blok: head 1 membaca kolom 0-2, head 2 membaca kolom 3-5
matriks3, nol = np.eye(3), np.zeros((3, 3))
W_Q1 = W_K1 = W_V1 = np.vstack([matriks3, nol]) # (6, 3)
W_Q2 = W_K2 = W_V2 = np.vstack([nol, matriks3]) # (6, 3)
W_O = np.eye(6)
A1, Z1 = attention(X @ W_Q1, X @ W_K1, X @ W_V1)
A2, Z2 = attention(X @ W_Q2, X @ W_K2, X @ W_V2)
Z = np.hstack([Z1, Z2]) @ W_O
print("Head 1 -- subspace 'kelas kata' (kolom 0-2):")
print(" bobot attention:")
print(A1)
print()
print("Head 2 -- subspace 'topik' (kolom 3-5):")
print(" bobot attention:")
print(A2)
print()
print("-> head 1: token 0 dan 2 saling menoleh (kelas kata sama)")
print("-> head 2: token 0 dan 1 saling menoleh (topik sama)")
print("-> pola bobot KEDUA HEAD BERBEDA pada token yang sama")
print()
print("Concat keluaran head 1 + head 2, lalu proyeksi W_O (identitas):")
print(Z)
print()
print("== Bagian B: nn.Module manual vs torch.nn.MultiheadAttention ==")
print()
class MultiHeadAttention(nn.Module):
"""Multi-head attention manual: proyeksi, pecah head, hitung, rangkai."""
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, dtype=torch.float64)
self.wk = nn.Linear(d_model, d_model, dtype=torch.float64)
self.wv = nn.Linear(d_model, d_model, dtype=torch.float64)
self.wo = nn.Linear(d_model, d_model, dtype=torch.float64)
def forward(self, x):
n, panjang, d = x.shape
q = self.wq(x).view(n, panjang, self.h, self.d_k).transpose(1, 2)
k = self.wk(x).view(n, panjang, self.h, self.d_k).transpose(1, 2)
v = self.wv(x).view(n, panjang, self.h, self.d_k).transpose(1, 2)
skor = q @ k.transpose(-2, -1) / self.d_k ** 0.5
a = skor.softmax(dim=-1)
z = (a @ v).transpose(1, 2).contiguous().view(n, panjang, d)
return self.wo(z), a
torch.manual_seed(0)
D_MODEL, NUM_HEADS = 6, 2
referensi = nn.MultiheadAttention(
D_MODEL, NUM_HEADS, batch_first=True, dtype=torch.float64
)
manual = MultiHeadAttention(D_MODEL, NUM_HEADS)
with torch.no_grad():
manual.wq.weight.copy_(referensi.in_proj_weight[:D_MODEL])
manual.wq.bias.copy_(referensi.in_proj_bias[:D_MODEL])
manual.wk.weight.copy_(referensi.in_proj_weight[D_MODEL : 2 * D_MODEL])
manual.wk.bias.copy_(referensi.in_proj_bias[D_MODEL : 2 * D_MODEL])
manual.wv.weight.copy_(referensi.in_proj_weight[2 * D_MODEL :])
manual.wv.bias.copy_(referensi.in_proj_bias[2 * D_MODEL :])
manual.wo.weight.copy_(referensi.out_proj.weight)
manual.wo.bias.copy_(referensi.out_proj.bias)
x = torch.randn(1, 4, D_MODEL, dtype=torch.float64)
out_manual, bobot_manual = manual(x)
out_ref, bobot_ref = referensi(x, x, x, average_attn_weights=False)
print(f"masukan x : bentuk {tuple(x.shape)}")
print(f"keluaran manual : bentuk {tuple(out_manual.shape)}")
print(f"bobot per head (average=False) : bentuk {tuple(bobot_ref.shape)}")
print()
print("Bobot attention tiap head (manual):")
for head in range(NUM_HEADS):
print(f" head {head + 1}:")
print(np.round(bobot_manual[0, head].detach().numpy(), 3))
print()
sel_out = (out_manual - out_ref).abs().max().item()
sel_bobot = (bobot_manual - bobot_ref).abs().max().item()
print(f"selisih maksimum keluaran : {sel_out:.3e}")
print(f"selisih maksimum bobot : {sel_bobot:.3e}")
print("Cocok dengan torch.nn.MultiheadAttention:",
bool(sel_out < 1e-12 and sel_bobot < 1e-12))Output yang Dihasilkan:
== Bagian A: dua head, dua pola (NumPy) ==
Head 1 -- subspace 'kelas kata' (kolom 0-2):
bobot attention:
[[0.39 0.219 0.39 ]
[0.264 0.471 0.264]
[0.39 0.219 0.39 ]]
Head 2 -- subspace 'topik' (kolom 3-5):
bobot attention:
[[0.39 0.39 0.219]
[0.39 0.39 0.219]
[0.264 0.264 0.471]]
-> head 1: token 0 dan 2 saling menoleh (kelas kata sama)
-> head 2: token 0 dan 1 saling menoleh (topik sama)
-> pola bobot KEDUA HEAD BERBEDA pada token yang sama
Concat keluaran head 1 + head 2, lalu proyeksi W_O (identitas):
[[0.781 0.219 0. 0.219 0.781 0. ]
[0.529 0.471 0. 0.219 0.781 0. ]
[0.781 0.219 0. 0.471 0.529 0. ]]
== Bagian B: nn.Module manual vs torch.nn.MultiheadAttention ==
masukan x : bentuk (1, 4, 6)
keluaran manual : bentuk (1, 4, 6)
bobot per head (average=False) : bentuk (1, 2, 4, 4)
Bobot attention tiap head (manual):
head 1:
[[0.345 0.12 0.38 0.155]
[0.176 0.31 0.147 0.367]
[0.278 0.216 0.29 0.216]
[0.208 0.322 0.205 0.265]]
head 2:
[[0.541 0.171 0.222 0.066]
[0.029 0.535 0.031 0.405]
[0.685 0.109 0.178 0.028]
[0.17 0.486 0.105 0.239]]
selisih maksimum keluaran : 1.110e-16
selisih maksimum bobot : 2.220e-16
Cocok dengan torch.nn.MultiheadAttention: TrueMari kita telaah kode di atas baris per baris:
- Baris 19-29: dua fungsi bantu —
softmax_barisdari chapter sebelumnya, danattentionyang menjalankan scaled dot-product attention satu head sekaligus mengembalikan bobot dan hasilnya. - Baris 35-41: matriks
Xberbentuk (3, 6). Token 0 dan 2 menyala pada kolom 0 (kelas kata sama); token 0 dan 1 menyala pada kolom 4 (topik sama). - Baris 43-47: proyeksi blok.
np.vstack([I3, 0])berukuran (6, 3) membuat head 1 hanya melihat kolom 0-2;np.vstack([0, I3])kebalikannya.W_Oidentitas agar hasil rangkaian terbaca langsung. - Baris 49-51: kedua head menjalankan attention penuh pada subspace masing-masing, hasilnya dirangkai
np.hstackmenjadi (3, 6), lalu dikalikanW_O. - Baris 76-84: constructor class manual. Empat
nn.Linearberukurand_model x d_model—wq,wk,wv,wo;assertmemastikand_modelhabis dibaginum_heads, dand_k = d_model // num_heads. - Baris 86-94: alur forward.
view(n, panjang, h, d_k).transpose(1, 2)memecah hasil proyeksi menjadi (batch, head, panjang, d_k); skor dihitung per head dalam satu batch tensor;.transpose(1, 2).contiguous().view(...)merangkai kembali hasil semua head sebelum masukwo. - Baris 97-103: layer referensi
nn.MultiheadAttentiondibuat denganbatch_first=Truedandtype=torch.float64; class manual dibuat setelahtorch.manual_seed(0)— seed hanya menentukan bobot referensi karena bobot manual disalin. - Baris 105-113: penyalinan bobot.
in_proj_weightberukuran (18, 6) adalah gabungan[W_Q; W_K; W_V]yang dipotong tiga blok berurutan;out_projmenjadiwo. - Baris 115-117: masukan
xacak berbentuk (1, 4, 6); kedua model dijalankan;average_attn_weights=Falsemembuat PyTorch mengembalikan bobot per head, bukan rata-ratanya. - Baris 129-134: selisih maksimum keluaran dan bobot dihitung; ambang 1e-12 menentukan keputusan akhir.
Hasil bagian A memperlihatkan inti multi-head: baris token 0 pada head 1 bernilai [0,390; 0,219; 0,390] — menoleh ke token 0 dan 2 yang sekelas — sedangkan pada head 2 bernilai [0,390; 0,390; 0,219] — menoleh ke token 0 dan 1 yang satu topik. Dua pola berbeda dari token yang sama, tanpa satu pun head menimpa informasi head lain. Baris pertama keluaran akhir [0,781; 0,219; 0,0; 0,219; 0,781; 0,0] menyimpan keduanya berdampingan: potongan kelas kata (tiga kolom pertama) dan potongan topik (tiga kolom terakhir). Pada bagian B, kedua head juga menunjukkan pola berbeda pada bobot acak, dan selisih 1,11e-16 terhadap torch.nn.MultiheadAttention membuktikan rumus class manual identik dengan implementasi resmi.
| Aspek | 1 head (d_model penuh) | h head (d_model/h tiap head) |
|---|---|---|
| Pola relasi per lapisan | 1 | h |
| Total parameter proyeksi | 4 d_model² | 4 d_model² (sama) |
| Lebar subspace tiap head | d_model | d_model / h |
| Resolusi tiap pola | tinggi | lebih rendah, ditukar dengan jumlah pola |
Dokumentasi resmi torch.nn.MultiheadAttention memuat daftar lengkap konvensi bentuk dan parameter yang kita ikuti di atas; contoh chapter ini diuji di CPU dengan PyTorch 2.14 dan selesai dalam hitungan detik.
Seluruh perlengkapan attention kini lengkap: proyeksi tiga peran, skor terkalakan, softmax per baris, dan beberapa head paralel. Namun satu bahan masih kurang — mekanisme kita sama sekali tidak peduli urutan token. Kalimat ikan makan dan makan ikan menghasilkan perhitungan yang identik karena X hanya berisi isi token. Kekurangan terakhir ini ditambal positional encoding, pokok bahasan chapter berikutnya.