Computing Attention Scores for a Query
The raw scores are only numbers that measure how strongly each input element relates to the query.
Why Raw Scores Need Rescaling
Self-attention starts from a simple question: for one token in a sequence, how strongly is it connected to each of the other tokens? Answering it requires a number for every pair of tokens. This article builds that number, called an attention score, from scratch, and then explains how the scores are rescaled into attention weights.
The raw scores are only numbers that measure how strongly each input element relates to the query. Once you have them, rescaling them so that they add up to 1 turns them into shares of a whole, which is easier to interpret. Normalization is also a convention that helps maintain training stability in a large language model. The source treats it as a practical choice rather than a mathematical necessity for every possible use.
What do you think happens?
Once you have a set of attention scores, does the exact way you rescale them matter, as long as the rescaled values add up to 1?
Reveal answer
Yes, the methods differ, even though both meet the sum-to-1 requirement.
Both simple division and softmax meet the sum-to-1 goal, which is the purpose of normalization. The mistake is assuming that meeting that one requirement makes the methods interchangeable. Softmax adds positivity and better handling of extreme values, and PyTorch's built-in softmax adds numerical stability. The rest of this article shows what separates them.
Choosing the Query Token
The process is concrete. You pick one input token to act as the query, then compare it against every input token, including itself, and record one number per comparison. The result is a list of scores with one entry per input token. Before running any code, decide what kind of list you expect: how many entries it should have, and which entry you would expect to be the most meaningful.
The query is a single vector taken from the input. In the source's example it is the second input token, selected by indexing position 1, because Python lists and tensors count from zero. Position matters because the query is the fixed reference point. Changing which position is the query changes which comparisons are made, and therefore changes every score in the list.
tensor([0., 2., 1.])- inputs holds one 3-element vector per token, so it has 3 rows
- inputs[1] selects row 1 because indexing starts at zero
- query is a fixed vector that every comparison will use
Looping Over the Input Tokens
The loop visits every input token in order, computes a dot product between that token and the query, and stores the result in a preallocated tensor at the same index. Three moving parts matter. The line that sets query fixes the comparison target. The line that creates the score tensor reserves one slot per input token, sized by the number of rows in inputs. The loop body fills each slot with one dot product. Because the query is fixed and only the current token changes, every score answers one question: how does token i relate to the chosen query?
tensor([1., 5., 2.])- torch.empty reserves one slot per input token
- enumerate yields the index i and the token vector x_i together
torch.dot(x_i, query)computes one scalar score per pass- The query never changes inside the loop; only x_i does
The Dot Product as Multiply and Sum
A dot product multiplies two vectors element-wise and sums the products into a single scalar. The element-wise step pairs the first element of one vector with the first element of the other, the second with the second, and so on. The products are then added together into one number. In PyTorch, torch.dot performs this in one call.
Scoring the cat token against the query
Compute the dot product of the token vector [1, 0, 1] (cat) with the query vector [0, 2, 1] (sat).
- 1Pair the elements
Match position 1 with 0, position 2 with 2, and position 3 with 1, giving the pairs (1, 0), (0, 2), and (1, 1).
- 2Multiply each pair
1 times 0 is 0, 0 times 2 is 0, and 1 times 1 is 1.
- 3Sum the products
0 + 0 + 1 = 1, so the score for cat is 1.
The attention score for the cat token is 1.
The source writes the same calculation two ways: as an explicit loop that accumulates products into a running total, and as a single torch.dot call. The two printed values should agree. That agreement shows that torch.dot is a shortcut for the element-wise multiply-and-sum you can write yourself. When you later debug a score, this pair is a useful check. If the manual loop and torch.dot disagree, the problem lies in the inputs you passed, not in the idea of the dot product.
Reading a Score as Alignment
A dot product is more than arithmetic. It measures similarity because it quantifies how closely two vectors are aligned. When two vectors point in roughly the same direction, their element-wise products tend to be large and of the same sign, so the sum is large. When they are less aligned, the products partly cancel or stay small, so the sum is smaller. A higher dot product therefore indicates greater alignment.
In self-attention, this reading turns a list of numbers into a statement about focus. The dot product determines the extent to which each element in a sequence attends to any other element. A large value in attn_scores_2 means the corresponding input token is considered closely related to the query, and a small value means it is considered less related. In the running example, the query sat scores 5 against itself, 2 against mat, and 1 against cat, so sat attends most strongly to itself.
Normalizing Scores by Division
The most direct normalization method divides every attention score by the sum of all the attention scores. Each weight is the score at its position divided by a shared total. Because every score is divided by the same total, the weights keep the same proportions to one another as the original scores, now scaled so they add to 1.
tensor([0.1250, 0.6250, 0.2500])
tensor(1.)attn_scores_2.sum()is the shared total used as the denominator- Summing the new weights is the check that they total 1
Softmax and the Overflow Trap
The source recommends softmax as the more common and advisable choice. Softmax first applies the exponential function to each score, then divides each result by the sum of all those exponentials. Because each output is an exponential divided by a total of exponentials, every weight is positive. The output can be read as relative importance, where a higher weight signals greater importance. Softmax is also described as better at managing extreme values.
Applying softmax to the scores 1, 5, and 2 gives weights of about 0.017, 0.936, and 0.047. The dominant score now takes most of the weight, which is a sharper contrast than simple division gave (0.125, 0.625, 0.25). Both results sum to 1, but they are not the same distribution. This is the difference the predict-and-reveal question pointed to earlier.
[0.017147..., 0.936239..., 0.046616...]- The function follows the softmax definition directly
- This version works on this small input but can fail on extreme values
| Method | Sums to 1 | Always positive | Numerically robust |
|---|---|---|---|
| Simple division | Yes | Only if all scores are positive | No safeguard |
| Naive softmax | Yes | Yes | No, can overflow or underflow |
| PyTorch softmax | Yes | Yes | Yes, optimized and stable |
All three methods meet the sum-to-1 goal, but they differ in positivity and robustness.
Using PyTorch's Built-In Softmax
Because of the stability concern, the source advises using PyTorch's softmax implementation in practice. It is extensively optimized for performance and avoids the instability of the hand-written version. The call takes the score tensor and a dim argument that selects the dimension to normalize along. In the source, the call uses dim=0 on the one-dimensional score tensor.
tensor([0.0171, 0.9362, 0.0466])
tensor(1.)- dim=0 normalizes across the single dimension of the score tensor
- On this small input the result matches the naive function
On this small example the built-in call and the naive function give the same weights, so the practical difference is not visible here. The reason to prefer the built-in version is robustness across inputs, which a small example does not test.
Mistakes When Computing Scores
Common mistake Treating any rescaling that sums to 1 as equivalent
What it looks likeDividing by the total and applying softmax both give weights that sum to 1, yet they produce different distributions.
Why it failsSumming to 1 is necessary for normalization but does not make the methods interchangeable.
Do this insteadChoose the method based on positivity, interpretability, and numerical robustness, and prefer torch.softmax in practice.
Common mistake Using the query as a value that changes inside the loop
What it looks likeReassigning query on each pass, so later scores compare different tokens to different references.
Why it failsThe scores are only meaningful as comparisons against one fixed query.
Do this insteadSet query once before the loop and let only x_i change on each pass.
Common mistake Assuming Python indexing starts at 1
What it looks likeWriting inputs[2] expecting the second token.
Why it failsLists and tensors count from zero, so index 1 is the second token.
Do this insteadUse inputs[1] to select the second token.
Common mistake Using a hand-written softmax on extreme values
What it looks likePassing very large scores to softmax_naive and receiving inf or zero-filled results.
Why it failsOverflow and underflow can corrupt results even when the formula is correct.
Do this insteadUse torch.softmax, which is optimized and avoids the naive version's instability.
Locating Where a Score Goes Wrong
When a score disagrees with expectations, trace the query assignment, the loop index, and the dot product, in that order, to find the failure. Suppose the loop is correct, but a score for position 2 is printed as 1 instead of 2. Work through the steps in order: first confirm the query was set to inputs[1]; then confirm that the loop writes to index i for the token being compared; finally recompute torch.dot for that token by hand. The first step that does not match your expectation is where the error lies.
Suppose the query is set to inputs[2] instead of inputs[1] in the running example with inputs [[1,0,1],[0,2,1],[1,1,0]]. Which score changes, and what are the new raw scores for cat, sat, and mat? Predict before computing.
Hints
- The query becomes the vector [1, 1, 0].
- Dot the query with [1, 0, 1], then with [0, 2, 1], then with itself [1, 1, 0].
- Compare the three results with the original scores 1, 5, and 2.
Key Takeaways
- Attention scores are computed by taking the dot product of one chosen query token with every input token, including itself, and storing one score per token.
- A dot product multiplies vectors element-wise and sums the products; a higher result indicates greater alignment between the vectors.
- Normalization turns raw scores into weights that sum to 1. Simple division preserves the proportions of the scores, while softmax produces positive weights that can be read as relative importance.
- A naive softmax can suffer overflow and underflow on large or small inputs, so PyTorch's built-in softmax, torch.softmax, is the recommended choice in practice.
- When a score is wrong, trace the query assignment, the loop index, and the dot product in that order to find where the computation diverges.