Tiga proyeksi Q, K, V dari chapter sebelumnya belum menghasilkan apa-apa sebelum kecocokannya dihitung dan dirangkai menjadi keluaran. Transformer melakukannya dalam satu baris rumus — softmax(Q K^T / sqrt(d_k)) V — yang menggantikan skor additive Bahdanau dengan perkalian titik: murah, dan dapat dihitung untuk semua pasangan sekaligus sebagai satu operasi matriks. Rumus ini juga memuat satu konstanta pembagi sqrt(d_k) yang tampak sepele tetapi menentukan apakah softmax masih berada di wilayah yang bisa belajar. Chapter ini membuktikan peran pembagi itu dengan angka nyata, mengimplementasikan rumus penuh dengan NumPy, lalu memverifikasi hasilnya terhadap fungsi resmi PyTorch.
Rumus dan Alurnya #
Attention(Q, K, V) = softmax(Q @ K^T / sqrt(d_k)) @ VEmpat potongan rumus di atas bekerja berurutan. Q @ K^T menghasilkan matriks skor berukuran (jumlah query, jumlah key) — kecocokan setiap pasangan dalam satu perkalian matriks. Pembagian sqrt(d_k) menjaga skala skor; alasannya kita buktikan di bagian berikutnya. Softmax yang diterapkan per baris mengubah skor tiap query menjadi bobot berjumlah 1 — baris ke-i adalah distribusi perhatian query ke-i. Terakhir, perkalian dengan V merangkai rata-rata berbobot: keluaran tiap query adalah campuran seluruh value menurut bobotnya.
Diagram tersebut adalah alur baku yang juga kita pakai pada implementasi di bawah; satu-satunya bagian yang tidak terlihat di diagram adalah urutan pecahnya matriks skor menjadi bobot per baris.
Kenapa Dibagi sqrt(d_k) #
Perkalian titik dua vektor berdimensi d_k menjumlahkan d_k suku. Jika komponen q dan k masing-masing bersebar dengan rata-rata 0 dan variansi 1, setiap suku q_i k_i berkontribusi variansi 1 dan suku-suku itu saling bebas — skor q · k berakhir dengan variansi d_k dan simpangan baku sqrt(d_k). Pada d_k = 64, skor mentah berfluktuasi pada kisaran ±8: selisih antar kunci mudah mencapai belasan. Softmax yang menerima masukan begitu besar jatuh ke wilayah jenuh — satu kunci menyerap hampir seluruh bobot dan gradien terhadap kunci lain nyaris nol, sehingga attention berhenti belajar jauh sebelum training selesai. Membagi skor dengan sqrt(d_k) mengembalikan variansi ke 1 dan menaruh softmax kembali ke wilayah responsif.
Program berikut membuktikannya dengan satu query dan delapan key acak berdimensi 64, lalu mengimplementasikan rumus penuh pada matriks kecil dan memverifikasinya terhadap PyTorch:
"""Scaled dot-product attention: rumus, alasan pembagi sqrt(d), verifikasi.
Bagian 1 membuktikan kenapa skor pada d_k = 64 tanpa skala membuat softmax
nyaris one-hot. Bagian 2 mengimplementasikan
Attention(Q, K, V) = softmax(Q K^T / sqrt(d_k)) V pada matriks kecil.
Bagian 3 memverifikasi hasil NumPy terhadap
torch.nn.functional.scaled_dot_product_attention (PyTorch 2.14, CPU).
"""
import numpy as np
import torch
import torch.nn.functional as F
np.set_printoptions(precision=4, 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)
print("== Bagian 1: kenapa dibagi sqrt(d_k), d_k = 64 ==")
print()
np.random.seed(0)
d = 64
q = np.random.randn(d)
K64 = np.random.randn(8, d)
skor_mentah = K64 @ q
skor_skala = skor_mentah / np.sqrt(d)
print(f"Skor mentah q.k_j (8 kunci): {np.round(skor_mentah, 2)}")
print(f" simpangan baku: {skor_mentah.std():.2f}"
f" (teori sqrt(64) = {np.sqrt(d):.0f})")
print(f"Skor dibagi sqrt(64) = 8 : {np.round(skor_skala, 2)}")
print(f" simpangan baku: {skor_skala.std():.2f}")
print()
p_mentah = softmax_baris(skor_mentah[None, :])[0]
p_skala = softmax_baris(skor_skala[None, :])[0]
print("Softmax TANPA skala (nyaris one-hot):")
print(f" {p_mentah}")
print(f" bobot terbesar: {p_mentah.max():.4f}")
print("Softmax DENGAN skala (tersebar):")
print(f" {p_skala}")
print(f" bobot terbesar: {p_skala.max():.4f}")
print()
print("== Bagian 2: implementasi Q (2x4), K (3x4), V (3x3) ==")
print()
Q = np.array(
[
[1.0, 0.0, 1.0, 0.0],
[0.0, 1.0, 0.0, 1.0],
]
)
K = np.array(
[
[1.0, 0.0, 1.0, 0.0],
[0.0, 1.0, 0.0, 1.0],
[1.0, 1.0, 0.0, 0.0],
]
)
V = np.array(
[
[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0],
[7.0, 8.0, 9.0],
]
)
d_k = Q.shape[1]
skor = Q @ K.T / np.sqrt(d_k)
A = softmax_baris(skor)
out = A @ V
print(f"skor = Q @ K^T / sqrt({d_k}):")
print(skor)
print()
print("bobot attention A = softmax per baris query:")
print(A)
print()
print("output = A @ V:")
print(out)
print()
print("== Bagian 3: verifikasi terhadap PyTorch ==")
print()
out_torch = F.scaled_dot_product_attention(
torch.from_numpy(Q).unsqueeze(0),
torch.from_numpy(K).unsqueeze(0),
torch.from_numpy(V).unsqueeze(0),
)[0].numpy()
selisih = np.abs(out - out_torch).max()
print(f"selisih maksimum NumPy vs PyTorch: {selisih:.3e}")
print("Cocok:", bool(selisih < 1e-12))Output yang Dihasilkan:
== Bagian 1: kenapa dibagi sqrt(d_k), d_k = 64 ==
Skor mentah q.k_j (8 kunci): [-17.77 -0.19 4.29 -8.34 -14.58 -2.57 -5.09 -11.05]
simpangan baku: 6.97 (teori sqrt(64) = 8)
Skor dibagi sqrt(64) = 8 : [-2.22 -0.02 0.54 -1.04 -1.82 -0.32 -0.64 -1.38]
simpangan baku: 0.87
Softmax TANPA skala (nyaris one-hot):
[0. 0.0112 0.9877 0. 0. 0.001 0.0001 0. ]
bobot terbesar: 0.9877
Softmax DENGAN skala (tersebar):
[0.0225 0.2028 0.3551 0.0733 0.0336 0.1506 0.1099 0.0522]
bobot terbesar: 0.3551
== Bagian 2: implementasi Q (2x4), K (3x4), V (3x3) ==
skor = Q @ K^T / sqrt(4):
[[1. 0. 0.5]
[0. 1. 0.5]]
bobot attention A = softmax per baris query:
[[0.5065 0.1863 0.3072]
[0.1863 0.5065 0.3072]]
output = A @ V:
[[3.4021 4.4021 5.4021]
[4.3626 5.3626 6.3626]]
== Bagian 3: verifikasi terhadap PyTorch ==
selisih maksimum NumPy vs PyTorch: 0.000e+00
Cocok: TrueMari kita telaah kode di atas baris per baris:
- Baris 17-20: fungsi
softmax_barismenerapkan softmax per baris dengan mengurangi nilai terbesar tiap baris sebelumexp— trik stabilitas yang sama dengan chapter sebelumnya. - Baris 26-29: satu query
qdan delapan keyK64, keduanya acak berdimensi 64 dengan seed tetap;d_k = 64adalah kondisi yang membuat pembagi benar-benar berperan. - Baris 31-32: skor mentah
K64 @ qdan versinya yang dibagisqrt(64) = 8. - Baris 34-38: pencetakan kedua jenis skor beserta simpangan bakunya — 6,97 pada skor mentah (teori 8, wajar untuk delapan sampel) dan 0,87 setelah dibagi.
- Baris 41-48: softmax atas kedua jenis skor. Tanpa skala, bobot terbesar 0,9877 — distribusi nyaris one-hot; dengan skala, bobot terbesar turun ke 0,3551 dan delapan kunci semua kebagian.
- Baris 54-73: matriks uji kecil. Query pertama sama persis dengan key pertama, query kedua sama dengan key kedua, dan key ketiga adalah campuran keduanya;
Vberukuran (3, 3) — dimensi value boleh berbeda darid_k. - Baris 75-78: isi rumus dalam tiga baris — skor
Q @ K.T / sqrt(d_k), bobotsoftmax_baris(skor), keluaranA @ V. Pembagi memakaid_k = 4sehingga skornya dibagi 2. - Baris 80-87: pencetakan skor, bobot, dan keluaran untuk dibaca pembaca.
- Baris 93-100: pemanggilan
F.scaled_dot_product_attentiondenganunsqueeze(0)menambah sumbu batch karena API mengharapkan bentuk(..., panjang, dimensi); selisih maksimum terhadap hasil NumPy dicetak sebagai keputusan akhir.
Bagian 1 memperlihatkan inti pembagi sqrt(d_k) dengan angka: skor dengan simpangan baku 6,97 menghasilkan softmax yang menyerahkan 98,77% bobot kepada satu kunci, sementara skor yang dibagi 8 menghasilkan sebaran dengan bobot terbesar 35,51%. Pada bagian 2, kedua baris bobot [0,5065; 0,1863; 0,3072] dan [0,1863; 0,5065; 0,3072] adalah cermin satu sama lain — tiap query paling tertarik pada key kembarannya, dan key campuran mendapat porsi menengah 0,3072. Keluaran query pertama [3,4021; 4,4021; 5,4021] adalah campuran tiga baris V dengan takaran tersebut — angka yang bisa kita periksa dengan perkalian tangan.
Satu lapisan attention seperti di atas hanya menghasilkan satu matriks bobot — satu pola relasi untuk seluruh pasangan token. Kalimat nyata membutuhkan banyak pola sekaligus: sintaks, makna, posisi. Chapter berikutnya menambah jumlah attention menjadi beberapa head paralel dan merangkai hasilnya menjadi multi-head attention.