Attention Is All You Need
Explained as a program, because that's the only way I could understand it.
Download attention_from_scratch.py — everything below runs from it. NumPy only.
I'm an AI Engineer. I've shipped models, done the MLOps, and I still couldn't read this paper. The maths wasn't the problem — the framing was. Nobody told me the core idea is something I've written a thousand times.
Attention is a dictionary lookup
You know this:
d = {"cat": vec_a, "dog": vec_b}
result = d["cat"] # exact match, or KeyError
Attention is that, except the match is fuzzy and the result is a blend:
def attention(query, keys, values):
scores = [dot(query, k) for k in keys] # how well does query match each key?
weights = softmax(scores) # turn into percentages summing to 1
return sum(w * v for w, v in zip(weights, values)) # weighted blend
That's the whole paper's beating heart. Three lines.
| Term | Means | Library analogy |
|---|---|---|
| Query | What am I looking for? | The question you ask |
| Key | What do I advertise about myself? | The book's title |
| Value | What do I hand over if picked? | The book's contents |
Why this beat the RNN
An RNN is a for loop:
state = init
for token in sentence: # strictly sequential — can't parallelize
state = update(state, token)
Two problems. Token 50 sees token 1 only through 49 rounds of lossy state-squeezing. And your GPU sits idle, because step n needs step n−1 to finish first.
Attention is a matrix multiply. Every token talks to every other token in one shot. Path length between any two tokens: 1. And it parallelizes perfectly. That's the title — throw out the recurrence, keep only attention, and it works better and trains faster.
The one formula
Read it right-to-left, as pipeline stages:
| Step | Shape | What it does |
|---|---|---|
Q | (n, dk) | n queries stacked as rows — batch your lookups |
QKT | (n, n) | Score matrix. Cell [i][j] = how much token i should care about token j |
/ √dk | scalar | Numerical hygiene — see below |
softmax | row-wise | Each row becomes percentages summing to 1 |
@ V | (n, dv) | Blend the values using those percentages |
Why divide by √dk?
Dot products of dk-dimensional vectors grow with dk. Big numbers into softmax → one weight becomes 0.9999, the rest ≈ 0 → gradients die → training stops. Dividing keeps the variance ≈ 1.
It's numerical hygiene, like normalising before a float comparison. Not deep.
Don't take my word for it. Here's the actual output:
d_k = 4 → raw score std: 1.24 → max softmax weight: 0.6125
d_k = 64 → raw score std: 5.73 → max softmax weight: 0.8319
d_k = 512 → raw score std: 25.12 → max softmax weight: 1.0000 ← dead
after dividing by √d_k, every case sits at std ≈ 1 ✓
Multi-head: eight questions at once
One attention pass computes one weighted average — it can only track one kind of relationship. Eight heads track eight (syntax, coreference, position…) simultaneously.
The model doesn't get wider: dmodel=512 with 8 heads means each head works in 64 dims. Same total compute, split eight ways. Like sharding a query across 8 workers with different indexes, then merging.
Position: the thing attention can't do
Attention is a set operation. A weighted sum doesn't care about order — so "cat ate food" and "food ate cat" produce identical output. An RNN got order for free from its loop. We threw the loop away.
The fix: add a unique sine/cosine wave pattern to each position before attention. Not learned — just computed.
Proof, from the running code:
without positional encoding, output sets identical? True ← order invisible
with positional encoding, output sets identical? False ← order now matters
That single + pe is the only thing giving a transformer any notion of word order.
The causal mask: how GPT is stopped from cheating
Set forbidden positions to −∞ before the softmax, so they come
out as exactly 0. Not 0 — because 0 is a perfectly ordinary
score that softmax would happily give weight to.
[[1. 0. 0. 0. 0. ] ← "the" sees only itself
[0.29 0.71 0. 0. 0. ]
[0.283 0.36 0.357 0. 0. ]
[0.317 0.267 0.261 0.155 0. ]
[0.228 0.134 0.172 0.153 0.314]] ← "it" sees everything before it
Everything else is wrapping
| Piece | What it really is |
|---|---|
Residual x + f(x) | A skip link. The sublayer proposes a diff, not a replacement. Same reasoning as ResNet — gradients need a short path back. |
| LayerNorm | Normalise each token's vector. Numerical hygiene again. |
| Feedforward | A plain 2-layer MLP, 512→2048→512, applied per position. |
Attention mixes information between tokens. The FFN processes it within each token. Mix, process, mix, process — that alternation is the entire stack.
def block(x):
x = layernorm(x + multi_head_attention(x)) # tokens talk to each other
x = layernorm(x + feedforward(x)) # each token thinks alone
return x # shape unchanged → stackable
Stack six. That's the encoder. That's the paper.
நான் ஒரு AI Engineer. Models ship செய்திருக்கிறேன், MLOps செய்திருக்கிறேன் — இருந்தும் இந்தக் கட்டுரையைப் படிக்க முடியவில்லை. கணிதம் பிரச்சனை இல்லை. அணுகுமுறைதான் பிரச்சனை. இதன் மையக் கருத்து நான் ஆயிரம் முறை எழுதியிருக்கும் ஒன்றுதான் என்று யாரும் சொல்லவில்லை.
Attention என்பது ஒரு dictionary lookup
இது உங்களுக்குத் தெரியும்:
d = {"cat": vec_a, "dog": vec_b}
result = d["cat"] # சரியாகப் பொருந்த வேண்டும், இல்லையேல் KeyError
Attention-ம் இதுதான் — ஆனால் பொருத்தம் மென்மையானது (fuzzy), விடை ஒரு கலவை:
def attention(query, keys, values):
scores = [dot(query, k) for k in keys] # query ஒவ்வொரு key-உடன் எவ்வளவு பொருந்துகிறது?
weights = softmax(scores) # சதவீதமாக்கு (கூட்டினால் 1)
return sum(w * v for w, v in zip(weights, values)) # எடையிட்ட கலவை
Attention-um idhu dhaan — aana matching fuzzy, answer oru kalavai. Moonu line dhaan. Idhu dhaan full paper-oda idhayam.
| சொல் | அர்த்தம் | நூலக உதாரணம் |
|---|---|---|
| Query | நான் என்ன தேடுகிறேன்? | நீங்கள் கேட்கும் கேள்வி |
| Key | நான் என்னைப் பற்றி என்ன விளம்பரம் செய்கிறேன்? | புத்தகத்தின் தலைப்பு |
| Value | தேர்ந்தெடுத்தால் என்ன தருவேன்? | புத்தகத்தின் உள்ளடக்கம் |
RNN-ஐ ஏன் இது வென்றது?
RNN ஒரு for loop மாதிரி:
state = init
for token in sentence: # வரிசையாகத்தான் — parallel முடியாது
state = update(state, token)
இரண்டு பிரச்சனைகள். 50-வது வார்த்தை, முதல் வார்த்தையைப் பார்க்க வேண்டுமென்றால் 49 முறை state-ஐக் கடந்து வர வேண்டும் — ஒவ்வொரு முறையும் கொஞ்சம் தகவல் இழக்கப்படுகிறது. மேலும் GPU சும்மா உட்கார்ந்திருக்கும், ஏனெனில் step n-க்கு step n−1 முதலில் முடிய வேண்டும்.
Attention ஒரு matrix multiply. எல்லா வார்த்தைகளும் ஒரே நேரத்தில் பேசிக்கொள்கின்றன. இரு வார்த்தைகளுக்கு இடையேயான பாதை நீளம்: 1. முழுமையாக parallel. இதுதான் தலைப்பின் அர்த்தம் — recurrence-ஐத் தூக்கி எறியுங்கள், attention மட்டும் வையுங்கள்.
Attention oru matrix multiply. Ellaa words-um ore neram pesikkidhu. Rendu words-kku idaila path length: 1. Fully parallel. Idhu dhaan title-oda artham.
அந்த ஒரு சூத்திரம்
| படி | செயல் |
|---|---|
Q @ K.T | மதிப்பெண். [i][j] = வார்த்தை i, வார்த்தை j-ஐ எவ்வளவு கவனிக்க வேண்டும். Dot product = ஒற்றுமை அளவு. |
/ √dk | அளவீடு. கீழே விளக்கம் |
softmax | சதவீதமாக்கு. ஒவ்வொரு வரியும் கூட்டினால் 1.0 |
@ V | கலவை. அந்த சதவீதங்களை வைத்து values-ஐ கலக்கு |
√dk-ஆல் ஏன் வகுக்க வேண்டும்?
dk பரிமாணங்கள் கொண்ட vectors-ன் dot product, dk-க்கு ஏற்ப வளர்கிறது. பெரிய எண்களை softmax-ல் போட்டால் ஒரு weight மட்டும் 0.9999 ஆகி, மற்றவை பூஜ்ஜியமாகி, gradient இறந்துவிடும் — training நிற்கும். வகுத்தால் variance ≈ 1-ஆகவே இருக்கும்.
இது ஆழமான கணிதம் அல்ல. வெறும் எண் சுகாதாரம் மட்டுமே.
என் பேச்சை நம்ப வேண்டாம். இதோ உண்மையான output:
d_k = 4 → raw score std: 1.24 → max softmax weight: 0.6125
d_k = 64 → raw score std: 5.73 → max softmax weight: 0.8319
d_k = 512 → raw score std: 25.12 → max softmax weight: 1.0000 ← செத்துவிட்டது
√d_k-ஆல் வகுத்த பிறகு எல்லாவற்றிலும் std ≈ 1 ✓
Multi-head: ஒரே நேரத்தில் எட்டு கேள்விகள்
ஒரு attention pass ஒரே ஒரு weighted average-ஐத்தான் கணக்கிடும் — அதனால் ஒரே ஒரு வகையான உறவை மட்டுமே கவனிக்க முடியும். 8 heads = 8 வகையான உறவுகள் ஒரே நேரத்தில்.
Model பெரிதாகிவிடாது: dmodel=512, 8 heads என்றால் ஒவ்வொரு head-ம் 64 பரிமாணத்தில் வேலை செய்யும். அதே வேலை, 8-ஆகப் பிரிக்கப்பட்டது.
வரிசை: attention-ஆல் செய்ய முடியாதது
Attention ஒரு set operation. கூட்டுத்தொகைக்கு வரிசை முக்கியமில்லை — அதனால் "பூனை சாப்பிட்டது உணவு" என்பதற்கும் "உணவு சாப்பிட்டது பூனை" என்பதற்கும் ஒரே விடைதான்! RNN-ல் loop-ஆல் வரிசை இலவசமாகக் கிடைத்தது. இங்கே அந்த loop-ஐ நாம் தூக்கி எறிந்துவிட்டோம்.
தீர்வு: ஒவ்வொரு position-க்கும் ஒரு தனித்துவமான sin/cos அலை வடிவத்தை embedding-ல் கூட்டிவிடுவது. இது கற்றுக்கொள்ளப்படுவதில்லை — வெறும் கணக்கீடு.
Code-ல் நிரூபணம்:
positional encoding இல்லாமல் → விடை ஒன்றுதானா? True ← வரிசை தெரியவில்லை
positional encoding-உடன் → விடை ஒன்றுதானா? False ← இப்போது தெரிகிறது
அந்த ஒரு + pe மட்டும்தான் Transformer-க்கு வார்த்தை வரிசையைச் சொல்கிறது.
Causal mask: GPT ஏமாற்றாமல் தடுப்பது
அனுமதிக்கப்படாத இடங்களை softmax-க்கு முன்பே −∞
ஆக்கிவிடுகிறோம். 0 ஆக்கினால் போதாது — ஏனெனில் 0 என்பது ஒரு
சாதாரண score, softmax அதற்கும் weight கொடுத்துவிடும்!
[[1. 0. 0. 0. 0. ] ← "the" தன்னை மட்டும் பார்க்கிறது
[0.29 0.71 0. 0. 0. ]
[0.283 0.36 0.357 0. 0. ]
[0.317 0.267 0.261 0.155 0. ]
[0.228 0.134 0.172 0.153 0.314]] ← "it" எல்லாவற்றையும் பார்க்கிறது
மீதி எல்லாம் வெறும் சுற்றுக் கட்டமைப்பு
| பகுதி | உண்மையில் என்ன? |
|---|---|
Residual x + f(x) | குறுக்குவழி. Sub-layer ஒரு திருத்தத்தை (diff) தருகிறது, மாற்றை அல்ல. Gradient திரும்ப வர குறுகிய பாதை வேண்டும். |
| LayerNorm | ஒவ்வொரு vector-ஐயும் சீராக்குதல். மீண்டும் எண் சுகாதாரம். |
| Feedforward | சாதாரண 2-அடுக்கு MLP, 512→2048→512. |
Attention = வார்த்தைகளுக்கு இடையே தகவல் கலக்கிறது. FFN = ஒவ்வொரு வார்த்தைக்கு உள்ளே அதை செயலாக்குகிறது. கலக்கு → செயலாக்கு → கலக்கு → செயலாக்கு. இதுதான் முழு stack-ன் தாளம்.
def block(x):
x = layernorm(x + multi_head_attention(x)) # வார்த்தைகள் பேசிக்கொள்கின்றன
x = layernorm(x + feedforward(x)) # ஒவ்வொன்றும் தனியே யோசிக்கிறது
return x # shape மாறாது → அடுக்கலாம்
ஆறு முறை அடுக்குங்கள். அதுதான் encoder. அதுதான் கட்டுரை.
Aaru thadava adukkunga. Adhu dhaan encoder. Adhu dhaan paper.
मैं एक AI Engineer हूँ। मैंने models ship किए हैं, MLOps किया है — फिर भी यह paper मुझसे पढ़ा नहीं जाता था। समस्या गणित नहीं थी। समस्या नज़रिया थी। किसी ने नहीं बताया कि इसका मूल विचार वही है जो मैंने हज़ार बार लिखा है।
Attention एक dictionary lookup है
यह आप जानते हैं:
d = {"cat": vec_a, "dog": vec_b}
result = d["cat"] # पूरा match होना चाहिए, वरना KeyError
Attention भी यही है — बस match धुँधला (fuzzy) है, और नतीजा एक मिश्रण है:
def attention(query, keys, values):
scores = [dot(query, k) for k in keys] # query हर key से कितना मिलता है?
weights = softmax(scores) # प्रतिशत में बदलो (जोड़ = 1)
return sum(w * v for w, v in zip(weights, values)) # भारित मिश्रण
यही पूरे paper का दिल है। तीन लाइनें।
| शब्द | मतलब | पुस्तकालय का उदाहरण |
|---|---|---|
| Query | मैं क्या ढूँढ रहा हूँ? | आपका सवाल |
| Key | मैं अपने बारे में क्या बताता हूँ? | किताब का शीर्षक |
| Value | चुने जाने पर मैं क्या दूँगा? | किताब की सामग्री |
इसने RNN को क्यों हराया?
RNN एक for loop है:
state = init
for token in sentence: # क्रम में ही — parallel नहीं हो सकता
state = update(state, token)
दो समस्याएँ। 50वें शब्द तक पहली बात पहुँचने में 49 बार state से गुज़रना पड़ता है — हर बार थोड़ी जानकारी खो जाती है। और GPU खाली बैठा रहता है, क्योंकि step n के लिए step n−1 का पूरा होना ज़रूरी है।
Attention एक matrix multiply है। सारे शब्द एक ही बार में एक-दूसरे से बात करते हैं। किन्हीं दो शब्दों के बीच रास्ते की लंबाई: 1। और पूरी तरह parallel। यही title का मतलब है — recurrence हटा दो, सिर्फ़ attention रखो।
वह एक सूत्र
| चरण | क्या करता है |
|---|---|
Q @ K.T | अंक (score)। [i][j] = शब्द i को शब्द j पर कितना ध्यान देना चाहिए। Dot product = समानता का माप। |
/ √dk | पैमाना। नीचे समझाया है |
softmax | प्रतिशत में बदलो। हर पंक्ति का जोड़ = 1.0 |
@ V | मिश्रण। उन प्रतिशतों से values को मिलाओ |
√dk से भाग क्यों?
dk आयामों वाले vectors का dot product dk के साथ बढ़ता है। बड़ी संख्याएँ softmax में डालो तो एक weight 0.9999 हो जाता है, बाक़ी लगभग शून्य — gradient मर जाता है, training रुक जाती है। भाग देने से variance ≈ 1 रहता है।
यह गहरा गणित नहीं है। बस संख्यात्मक सफ़ाई है।
मेरी बात मत मानिए। यह रहा असली output:
d_k = 4 → raw score std: 1.24 → max softmax weight: 0.6125
d_k = 64 → raw score std: 5.73 → max softmax weight: 0.8319
d_k = 512 → raw score std: 25.12 → max softmax weight: 1.0000 ← मर गया
√d_k से भाग देने के बाद हर हाल में std ≈ 1 ✓
Multi-head: एक साथ आठ सवाल
एक attention pass सिर्फ़ एक weighted average निकालता है — इसलिए वह सिर्फ़ एक तरह का रिश्ता पकड़ सकता है। 8 heads = 8 तरह के रिश्ते, एक साथ।
Model बड़ा नहीं होता: dmodel=512 और 8 heads का मतलब हर head 64 आयामों में काम करता है। वही काम, आठ हिस्सों में बँटा हुआ।
क्रम: जो attention नहीं कर सकता
Attention एक set operation है। जोड़ को क्रम की परवाह नहीं — इसलिए "बिल्ली ने खाना खाया" और "खाने ने बिल्ली खाई" का नतीजा एक जैसा आता है! RNN को क्रम loop से मुफ़्त मिल जाता था। हमने वह loop हटा दिया।
हल: हर position के लिए एक अलग sin/cos तरंग embedding में जोड़ दो। यह सीखा नहीं जाता — बस गणना है।
Code से प्रमाण:
positional encoding के बिना → नतीजा एक जैसा? True ← क्रम दिखता ही नहीं
positional encoding के साथ → नतीजा एक जैसा? False ← अब क्रम मायने रखता है
वह एक + pe ही Transformer को शब्दों का क्रम बताता है।
Causal mask: GPT को नक़ल करने से रोकना
मना किए गए स्थानों को softmax से पहले ही −∞ कर देते
हैं। 0 करना काफ़ी नहीं — क्योंकि 0 एक सामान्य score है,
softmax उसे भी weight दे देगा!
[[1. 0. 0. 0. 0. ] ← "the" सिर्फ़ ख़ुद को देखता है
[0.29 0.71 0. 0. 0. ]
[0.283 0.36 0.357 0. 0. ]
[0.317 0.267 0.261 0.155 0. ]
[0.228 0.134 0.172 0.153 0.314]] ← "it" सब कुछ देखता है
बाक़ी सब सिर्फ़ ढाँचा है
| हिस्सा | असल में क्या है |
|---|---|
Residual x + f(x) | एक शॉर्टकट। Sub-layer एक सुधार (diff) देता है, बदलाव नहीं। Gradient को लौटने के लिए छोटा रास्ता चाहिए। |
| LayerNorm | हर vector को सामान्य करना। फिर वही संख्यात्मक सफ़ाई। |
| Feedforward | सादा 2-परत MLP, 512→2048→512। |
Attention = शब्दों के बीच जानकारी मिलाता है। FFN = हर शब्द के अंदर उसे संसाधित करता है। मिलाओ → संसाधित करो → मिलाओ → संसाधित करो। यही पूरे stack की लय है।
def block(x):
x = layernorm(x + multi_head_attention(x)) # शब्द आपस में बात करते हैं
x = layernorm(x + feedforward(x)) # हर शब्द अकेले सोचता है
return x # shape वही → ढेर लगा सकते हैं
छह बार ढेर लगाओ। वही encoder है। वही paper है।
The code
Pure NumPy. No torch, no autograd, no training — just the forward pass, so you can watch the numbers move. Every function under 15 lines.
Download the full script — 60 lines plus five runnable demos. Then:
python3 attention_from_scratch.py
import numpy as np
def softmax(x, axis=-1):
# the `- max` isn't in the paper — it's overflow safety.
# exp(1000) is inf; exp(1000-1000) is 1. Cancels in the ratio.
shifted = x - np.max(x, axis=axis, keepdims=True)
e = np.exp(shifted)
return e / np.sum(e, axis=axis, keepdims=True)
def scaled_dot_product_attention(Q, K, V, mask=None):
d_k = Q.shape[-1]
# (1) SCORE — scores[i][j] = how much token i should care about token j
scores = Q @ K.T
# (2) SCALE — keep variance ~1 so softmax doesn't saturate
scores = scores / np.sqrt(d_k)
# (3) MASK — -inf, not 0, so softmax gives exactly zero
if mask is not None:
scores = np.where(mask, scores, -np.inf)
# (4) NORMALISE + BLEND
weights = softmax(scores, axis=-1) # each row sums to 1.0
return weights @ V, weights
def positional_encoding(n_positions, d_model):
pos = np.arange(n_positions)[:, None]
i = np.arange(d_model)[None, :]
angle = pos / np.power(10000, (2 * (i // 2)) / d_model)
pe = np.zeros((n_positions, d_model))
pe[:, 0::2] = np.sin(angle[:, 0::2]) # even dims
pe[:, 1::2] = np.cos(angle[:, 1::2]) # odd dims
return pe
def causal_mask(n):
return np.tril(np.ones((n, n), dtype=bool))
Now break it
This is the part that actually makes it stick. Reading won't do it. You debug for a living — reverse-engineering from broken behaviour is your native mode, and it's far faster than re-reading section 3.2 for the fifth time.
| Break this | Watch for |
|---|---|
Delete / np.sqrt(d_k) | One softmax weight goes to 1.0000 as dk grows. Gradient dead. |
Remove + pe | "cat ate food" and "food ate cat" become indistinguishable. |
Flip np.tril → np.triu | The decoder now reads the future. It's cheating. |
Use 0 instead of -np.inf in the mask | Masked positions still get real weight. The mask silently does nothing. |
On the maths anxiety
Honestly: for AI engineering — shipping systems, not writing papers — this is roughly the ceiling of maths you need, and you've just cleared it. Matrix shapes (that's array indexing), dot product as similarity, softmax as normalisation. That's it.
You don't need to derive backprop. loss.backward() is a library
call, the same way you don't hand-roll TCP.
What actually separates AI engineers is different: tokenisation edge cases, KV-cache and memory maths, quantisation tradeoffs, eval design, latency and cost budgeting, retrieval quality. None of it is research maths.
Where to go next
This page gave you one thing: attention as a lookup, in code you can break. That's deliberately narrow. Four other people explain the parts I skipped, and each is better at its own angle than I'd be at copying it.
-
Visualizing seq2seq models with attention
Read this before the transformer one. It shows the problem attention was invented to fix — the RNN encoder crushing a whole sentence into one vector — step by step. My "why this beat the RNN" section is one paragraph; this is that paragraph as a movie.
-
The Illustrated Transformer
The canonical picture version. Where I gave you array shapes, this gives you the tensors as coloured boxes flowing through the encoder stack. Best answer anywhere to "what is multi-head actually doing to the matrix" if diagrams land better for you than
.shape. -
Transformers from scratch
The step up in rigour, without becoming a paper. PyTorch instead of my NumPy, and it actually justifies the design choices I asserted — why self-attention is permutation-equivariant, why the √dk argument holds. Go here when "trust me" stops being enough.
-
The Annotated Transformer
The whole paper, line by line, as working PyTorch — including everything I called "wrapping": label smoothing, the warmup learning rate schedule, real training on real data. This is the last stop, and the one that trains.