A complaint bot that has to decide what "it" means
A food-delivery platform operating at Swiggy or Zomato scale processes millions of support messages a day. A customer types: "My order was late and it was cold." Before any reply can be generated, the system has to route the ticket — is this a delivery-time complaint, a food-quality complaint, or both? The word carrying the actual complaint, "cold," is unambiguous. The word "it" is not: on its own it has no meaning at all. Its meaning is borrowed entirely from another word earlier in the sentence, and a keyword-matching rule engine has no way to make that connection, because "it" contains no keyword to match.
To resolve the reference, the token "it" has to look back across the sentence and decide, numerically, how relevant every other token is to it — not a binary yes/no lookup, but a graded weighting: strongly relevant to "order," mildly relevant to "late," irrelevant to "was." That graded, quantitative weighting is exactly what scaled dot-product attention computes, one number per pair of tokens, every time a transformer layer runs. This chapter works through the arithmetic of a single attention head end to end: how the Query, Key, and Value matrices are built from a shared input, why the raw similarity scores must be scaled before they are turned into weights, and why getting that scaling wrong silently breaks learning rather than crashing the program.
From an exact-match lookup to a soft lookup: defining Query, Key, Value
In the SQL you write for a Grade 11 database assignment, a clause like WHERE keyword = 'cold' is a hard lookup: it returns exactly the rows that match and nothing else. Attention was designed around the same shape of idea — one thing issuing a lookup, a set of things being checked against it — but with the equality test replaced by a continuous relevance score, and the returned row replaced by a learned vector of content. Formally: stack a sequence of n token embeddings as rows of a matrix X ∈ ℝn×d_model. Single-head scaled dot-product attention learns three weight matrices, W_Q, W_K ∈ ℝd_model×d_k and W_V ∈ ℝd_model×d_v, and computes three linear projections of that same X:
Q = X·W_Q (queries, shape n × d_k)
K = X·W_K (keys, shape n × d_k)
V = X·W_V (values, shape n × d_v)
Row i of Q is "what token i is looking for." Row j of K is "what token j offers, for the purpose of being matched." Row j of V is "what token j actually hands over if it gets picked." The three matrices must not be pictured as three different pieces of data pulled from the sentence — every token passes through all three projections. Q and K live in the same d_k-dimensional space on purpose, because they are about to be compared directly by dot product; V is free to live in a different dimension d_v, since it only has to supply content for the final weighted sum, never to be compared against anything.
The raw similarity between query i and key j is their dot product, Q_i · K_j, and the full matrix of every pair at once is S = Q·KT ∈ ℝn×n. Geometrically, q·k = |q||k|cos θ, so when the projected vectors have roughly comparable magnitude, the dot product tracks how aligned two vectors are in the learned space — a large positive score means "these two tokens' projections point the same way," which is precisely what a trained network learns to make true for token pairs that are semantically related.
The pipeline in one picture
Before working through the arithmetic, it helps to see the full sequence of operations a single head performs — three projections, one similarity matrix, one scale, one softmax, one weighted sum:
Why the raw scores cannot go straight into softmax
Suppose, as is standard once a network is past initialization and running through normalized layers, that each entry of Q and K behaves like an independent random variable with mean 0 and variance 1. Take one query vector q and one key vector k, both in ℝd_k. Their dot product is a sum of d_k terms, q·k = Σm=1d_k q_m k_m. Because q_m and k_m are independent and zero-mean, each individual term has:
Var(q_m k_m) = E[(q_m k_m)2] − (E[q_m k_m])2 = E[q_m2]·E[k_m2] − 0 = 1 · 1 = 1
Summing d_k independent terms adds their variances: Var(q·k) = Σm=1d_k Var(q_m k_m) = d_k. The variance of the raw score grows linearly with the dimension of the query and key vectors, so its standard deviation grows as √d_k — not a fixed number, but one that keeps climbing as the architecture uses wider representations. A Monte Carlo check confirms the derivation rather than just asserting it: sampling 200,000 independent (q, k) pairs at several dimensions and measuring the empirical variance of q·k gives:
| d_k | predicted Var(q·k) = d_k | empirical Var(q·k) | empirical std | √d_k |
|---|---|---|---|---|
| 4 | 4 | 3.996 | 2.00 | 2.00 |
| 16 | 16 | 15.903 | 3.99 | 4.00 |
| 64 | 64 | 63.949 | 8.00 | 8.00 |
| 256 | 256 | 255.301 | 15.98 | 16.00 |
| 1024 | 1024 | 1025.952 | 32.03 | 32.00 |
The empirical numbers track the predicted d_k almost exactly, and the empirical standard deviation tracks √d_k just as closely — the derivation is not an approximation, it is what actually happens. Now trace the consequence through softmax: softmax(x)i = ex_i / Σj ex_j. If raw scores have a standard deviation of 32 (the d_k = 1024 row), it is routine for the top score and the runner-up to differ by 20–40. A gap of 30 means the top token receives e30 ≈ 1013 times more weight than the runner-up — softmax collapses to a vector that is, to floating-point precision, one-hot. Dividing every score by √d_k before the softmax exactly cancels the growth: Var(score / √d_k) = Var(score) / d_k = d_k / d_k = 1, regardless of how large d_k is chosen to be. That single division is the entire content of the "scaled" in scaled dot-product attention.
Worked example: computing attention for "it" by hand
To make every step traceable, use a deliberately tiny setup: the four tokens of "Order was late it" (a compressed stand-in for "My order was late and it was cold"), with model dimension d_model = 4 and, to isolate the scaling question from head-splitting, a single head with d_k = d_v = 4. The embeddings and weight matrices below are fixed by hand for this example, not learned — that is what makes the arithmetic checkable by hand and by code, not a claim about what a trained model would actually produce.
Embeddings (rows of X, one per token): Order = [1,0,1,0], was = [0,1,0,1], late = [1,1,0,0], it = [0,0,1,1]. Weight matrices W_Q, W_K, W_V are each fixed 4×4 matrices. Multiplying X by each gives:
Q = [[2,0,1,1], [0,1,1,1], [1,1,0,1], [1,0,2,1]]
K = [[1,1,2,0], [1,1,0,2], [1,1,1,1], [1,1,1,1]]
V = [[2,0,3,1], [1,1,0,1], [2,1,0,1], [1,0,3,1]]
(rows in order Order, was, late, it in every matrix). Take the query row for "it": q_it = Q[3] = [1,0,2,1]. Its dot product against every key row, computed term by term:
q_it · K_Order = (1)(1)+(0)(1)+(2)(2)+(1)(0) = 1+0+4+0 = 5
q_it · K_was = (1)(1)+(0)(1)+(2)(0)+(1)(2) = 1+0+0+2 = 3
q_it · K_late = (1)(1)+(0)(1)+(2)(1)+(1)(1) = 1+0+2+1 = 4
q_it · K_it = (1)(1)+(0)(1)+(2)(1)+(1)(1) = 1+0+2+1 = 4
Raw scores: [5, 3, 4, 4]. Since d_k = 4, the scale divisor is √4 = 2, giving scaled scores [2.5, 1.5, 2.0, 2.0]. Run softmax on both:
softmax(raw) = softmax([5,3,4,4]) = [0.5344, 0.0723, 0.1966, 0.1966]
softmax(scaled) = softmax([2.5,1.5,2,2]) = [0.3875, 0.1425, 0.2350, 0.2350]
Even at this tiny d_k = 4, the unscaled distribution is visibly more concentrated on the top score: 53.4% of the weight lands on "Order" without scaling, versus 38.8% with scaling — the same qualitative sharpening that becomes catastrophic at the d_k = 1024 scale worked through above. Multiplying each weight vector into V gives the final context vector for "it":
output(raw weights) = 0.5344·[2,0,3,1] + 0.0723·[1,1,0,1] + 0.1966·[2,1,0,1] + 0.1966·[1,0,3,1] = [1.7311, 0.2689, 2.1932, 1.0000]
output(scaled weights) = 0.3875·[2,0,3,1] + 0.1425·[1,1,0,1] + 0.2350·[2,1,0,1] + 0.2350·[1,0,3,1] = [1.6225, 0.3775, 1.8674, 1.0000]
Both vectors point in a similar direction, but the unscaled one is pulled harder toward V_Order = [2,0,3,1] and away from the contributions of "was" and "late" — the sharper softmax has effectively discarded partial evidence from competing tokens that the scaled version still blends in. The same computation, run as code rather than by hand, must reproduce these exact numbers:
import numpy as np
def scaled_dot_product_attention(Q, K, V):
d_k = Q.shape[-1]
scores = Q @ K.T
scaled_scores = scores / np.sqrt(d_k)
weights = np.exp(scaled_scores - scaled_scores.max(axis=-1, keepdims=True))
weights = weights / weights.sum(axis=-1, keepdims=True)
return weights @ V, weights
X = np.array([
[1, 0, 1, 0], # "Order"
[0, 1, 0, 1], # "was"
[1, 1, 0, 0], # "late"
[0, 0, 1, 1], # "it"
], dtype=float)
W_Q = np.array([[1,0,0,1],[0,1,0,0],[1,0,1,0],[0,0,1,1]], dtype=float)
W_K = np.array([[1,0,1,0],[0,1,0,1],[0,1,1,0],[1,0,0,1]], dtype=float)
W_V = np.array([[2,0,0,1],[0,1,0,0],[0,0,3,0],[1,0,0,1]], dtype=float)
Q, K, V = X @ W_Q, X @ W_K, X @ W_V
output, weights = scaled_dot_product_attention(Q, K, V)
np.set_printoptions(precision=4, suppress=True)
print("weights[3] =", weights[3]) # attention weights for "it"
print("output[3] =", output[3]) # context vector for "it"
Expected output:
weights[3] = [0.3875 0.1425 0.235 0.235 ]
output[3] = [1.6225 0.3775 1.8674 1. ]
This matches the hand-computed scaled-weight result exactly, because scaled_dot_product_attention is doing nothing more than the same four operations traced above — matrix multiply, divide by √d_k, row-wise softmax, matrix multiply — applied to all four rows of Q at once instead of one row at a time.
Common misconception: "we scale to avoid overflow"
A frequent misreading is that dividing by √d_k exists to keep the numbers going into exp() from overflowing. This does not survive a check of the actual numbers. float32 overflows around e88.7 ≈ 3.4 × 1038; the code above already subtracts the row maximum before exponentiating (scaled_scores - scaled_scores.max(...)), which is standard practice in every real softmax implementation and by itself makes overflow impossible regardless of how large or small the raw scores are, because the largest exponent computed is always e0 = 1. If overflow prevention were the actual purpose of the √d_k division, max-subtraction alone would already have solved the problem completely, and the scaling term would be redundant.
What max-subtraction does not touch is the spread between scores — it is a shift, and shifting every score by the same constant changes none of the differences between them. The real disease diagnosed by the variance derivation above is that those differences themselves grow with d_k, and growing differences are exactly what drive softmax toward a one-hot output and its gradient toward zero everywhere except the single winning position. That is a training-dynamics failure, not a numerical-overflow failure, and it is why the fix has to be a variance-correcting scale factor applied before the exponential, not a bigger float type or a shift applied after it.
Where multi-head attention picks this up
A real transformer layer does not run this mechanism once at the full d_model width. It splits d_model into h parallel heads of size d_k = d_model / h each, runs the exact procedure derived above independently inside every head — its own W_Q, W_K, W_V, its own Q·KT, its own division by that head's √d_k, its own softmax — and only then concatenates the h outputs and applies one further linear projection. The variance derivation above is why a standard 512-dimensional, 8-head configuration uses d_k = 64 per head rather than 512: a smaller per-head d_k means a smaller score variance to begin with, which is one more reason the scale factor is always keyed to whatever d_k the architecture is actually using inside that head, not to d_model as a whole. Why splitting the representation into several smaller subspaces in parallel, instead of running one large head, lets the model track more than one kind of relationship at once is the subject of the companion chapter, Multi-Head Attention: Deep Mathematical Analysis.
Active recall
Attempt every question in full before reading the worked answers below.
- Write the three projection equations for single-head scaled dot-product attention, and state the shape of every matrix involved — X, W_Q, W_K, W_V, Q, K, V — for a sequence of n tokens with model dimension d_model, key dimension d_k, and value dimension d_v.
- A classmate says: "Query, Key, and Value are three different features extracted from the sentence — like three different embeddings someone hand-picked." Explain precisely why that description is wrong, and give the correct one-sentence description.
- Derive Var(q·k) for q, k ∈ ℝd_k with independent, zero-mean, unit-variance entries, and state the scaling factor this derivation justifies.
- In the worked example, K_late and K_it turned out to be numerically identical: both equal [1,1,1,1]. Now compute attention using the query for "late" instead of "it" — that is, use q_late = Q[2] = [1,1,0,1] in place of q_it, with every other embedding and weight matrix unchanged. Trace the full effect: the four raw scores, the four scaled scores, both softmax weight vectors, and the final scaled-weight output vector. Which individual attention weight(s), if any, are guaranteed to stay equal to each other regardless of which query is used, and why?
- For d_k = 1024, the Monte Carlo table above gives a raw-score standard deviation of about 32. Using the softmax formula and its gradient, explain — mechanism, not just magnitude — why omitting the 1/√d_k scale at this dimension makes a network fail to learn, rather than merely producing large numbers.
- Suppose a network's learned W_Q, W_K produce query/key entries with variance σ² instead of 1. Re-derive the correct scaling denominator for this case. Does the textbook factor 1/√d_k still exactly cancel the variance growth?
Worked answers
1. X ∈ ℝn×d_model; W_Q, W_K ∈ ℝd_model×d_k; W_V ∈ ℝd_model×d_v. Q = X·W_Q ∈ ℝn×d_k; K = X·W_K ∈ ℝn×d_k; V = X·W_V ∈ ℝn×d_v. Q and K are forced to share dimension d_k because they are compared directly by dot product; V's dimension d_v is independent of d_k because V is never compared to anything — it only supplies the content of the final weighted sum.
2. The description is wrong because Q, K, and V are not three separately observed inputs — every token's single embedding passes through all three projection matrices. The "three different things" the classmate is noticing is entirely the effect of three different learned weight matrices acting on one shared input X, not three different pieces of data about the sentence.
3. Each term Var(q_m k_m) = E[q_m2]E[k_m2] − (E[q_m]E[k_m])2 = 1·1 − 0 = 1, using independence and zero mean. Summing d_k independent terms adds their variances: Var(Σ q_m k_m) = d_k · 1 = d_k. Dividing the score by √d_k gives Var(score/√d_k) = d_k / d_k = 1, independent of d_k — this is the scaling factor used in scaled dot-product attention.
4. Raw scores with q_late = [1,1,0,1]: against K_Order=[1,1,2,0] → 1+1+0+0=2; K_was=[1,1,0,2] → 1+1+0+2=4; K_late=[1,1,1,1] → 1+1+0+1=3; K_it=[1,1,1,1] → 1+1+0+1=3. Raw scores: [2, 4, 3, 3] (compare to [5, 3, 4, 4] for the "it" query — the Order and was scores have swapped which one is larger). Scaled (÷2): [1.0, 2.0, 1.5, 1.5]. softmax(raw) = [0.0723, 0.5344, 0.1966, 0.1966]; softmax(scaled) = [0.1425, 0.3875, 0.2350, 0.2350]. Output with scaled weights = 0.1425·[2,0,3,1] + 0.3875·[1,1,0,1] + 0.2350·[2,1,0,1] + 0.2350·[1,0,3,1] = [1.3775, 0.6225, 1.1326, 1.0000]. Comparing to the "it" case: the weight on "Order" and the weight on "was" have essentially traded places (0.5344 moves from Order to was; 0.3875 moves the same way in the scaled version), because q_late points more toward K_was than K_Order while q_it pointed the opposite way. The weights on late and it (indices 2 and 3), however, are guaranteed to equal each other for any query whatsoever — 0.1966 = 0.1966 and 0.235 = 0.235 in both cases — because K_late and K_it are exactly the same vector, so q·K_late = q·K_it identically no matter what q is; equal keys always receive equal attention weight to each other, by definition of the dot product, regardless of which token is asking. (That the two queries happen to produce the same paired value, 0.1966 and 0.235 respectively, is a coincidence of this particular toy number set, not a general property — only the pairwise equality within each query is guaranteed.) The output vector shifts accordingly: the "was"-weighted dimension (index 1) rises from 0.38 to 0.62 and the "late"-weighted dimension (index 2) falls from 1.87 to 1.13, tracking exactly which of Order's or was's value vector is currently winning the largest share of the softmax.
5. softmax(x)_i = ex_i/Σ_j ex_j. With raw-score standard deviation ≈32, a gap of roughly 30 between the top score and the runner-up is routine, and e30 ≈ 1013, so the softmax output is one-hot to floating-point precision. The gradient of softmax output p_i with respect to input x_j contains terms of the form p_i(δ_ij − p_j); once p is essentially one-hot, every such term is ≈0 except at the single selected index. So ∂loss/∂x_j ≈ 0 for every non-selected token — the network gets no error signal telling it to change the attention it pays to any position except the one that already dominates, and cannot learn to redistribute attention even when doing so would reduce the loss. Dividing by √d_k keeps the score spread near std ≈1, keeping every p_i away from the 0/1 boundary and those gradient terms non-negligible.
6. Let Var(q_m) = σ_q² and Var(k_m) = σ_k², with q_m and k_m still independent and zero-mean. Then Var(q_m k_m) = E[q_m²]E[k_m²] − 0 = σ_q²σ_k², and summing d_k such independent terms gives Var(Σ q_m k_m) = d_k·σ_q²·σ_k², so the standard deviation is √d_k · σ_q · σ_k. The correct scaling denominator is therefore √d_k · σ_q · σ_k, not √d_k alone. The textbook factor 1/√d_k is exactly correct only under the assumption σ_q = σ_k = 1 — which is precisely why transformer architectures pair this scale factor with initialization and normalization schemes (Xavier/Glorot-style initialization, LayerNorm before the projections) deliberately engineered to keep projected activations near unit variance. If σ_q or σ_k drift away from 1, 1/√d_k alone under- or over-corrects, and the variance-collapse or variance-explosion problem returns in a different form.
Think About It
Think about this: How would you explain attention mechanism: mathematical deep dive to a friend who has never seen a computer? What real-world analogy would you use? Imagine you had to build a system using these concepts — what would be your first step? Try this: before moving on, write down three things you learned and one question you still have.
Key Takeaways — Summary and Recap
Let us recap what we covered: the core ideas behind attention mechanism: mathematical deep dive, how they connect to real-world applications, and why they matter for your journey in computer science. Remember these key points as you move forward. For competitive exam preparation (CBSE, JEE, BITSAT), focus on understanding the WHY behind each concept, not just the WHAT.