Concepts / Computing Attention Scores for a Query

Computing Attention Scores for a Query

The raw scores are only numbers that measure how strongly each input element relates to the query.

  • Programming

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?

  • No, any procedure that makes the values sum to 1 produces a valid set of weights

  • Yes, the methods can give different weights even though both sum to 1

Reveal answer
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.

Index 0catIndex 1sat (query)Index 2matscored againstscored against itselfscored against
Position 1 is selected as the query, so every other token is scored against the token at that index. Moving the query to another index changes which pairs are compared.
python
Output (expected)
tensor([0., 2., 1.])
Notes on the code
  • 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?

Pass 0cat, dot = 1Pass 1sat, dot = 5Pass 2mat, dot = 2attn_scores_2[1, 5, 2]next tokennext tokenloop ends
On each pass the query stays fixed, the loop variable selects one input token, and the dot product with the query is stored at that token's index. The finished tensor holds one score per token.
python
Output (expected)
tensor([1., 5., 2.])
Notes on the code
  • 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

Definition

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.

Formula
Worked example

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).

  1. 1
    Pair 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).

  2. 2
    Multiply each pair

    1 times 0 is 0, 0 times 2 is 0, and 1 times 1 is 1.

  3. 3
    Sum the products

    0 + 0 + 1 = 1, so the score for cat is 1.

Result

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.

Formula
catscore 1catweight 0.125satscore 5satweight 0.625matscore 2matweight 0.25divide by 8divide by 8divide by 8
The raw scores 1, 5, and 2 total 8. Dividing each one by 8 gives weights 0.125, 0.625, and 0.25, which keep the same proportions and sum to 1.
python
Output (expected)
tensor([0.1250, 0.6250, 0.2500])
tensor(1.)
Notes on the code
  • 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.

Formula

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.

python
Output (expected)
[0.017147..., 0.936239..., 0.046616...]
Notes on the code
  • The function follows the softmax definition directly
  • This version works on this small input but can fail on extreme values
MethodSums to 1Always positiveNumerically robust
Simple divisionYesOnly if all scores are positiveNo safeguard
Naive softmaxYesYesNo, can overflow or underflow
PyTorch softmaxYesYesYes, 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.

python
Output (expected)
tensor([0.0171, 0.9362, 0.0466])
tensor(1.)
Notes on the code
  • 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 like

    Dividing by the total and applying softmax both give weights that sum to 1, yet they produce different distributions.

    Why it fails

    Summing to 1 is necessary for normalization but does not make the methods interchangeable.

    Do this instead

    Choose 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 like

    Reassigning query on each pass, so later scores compare different tokens to different references.

    Why it fails

    The scores are only meaningful as comparisons against one fixed query.

    Do this instead

    Set 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 like

    Writing inputs[2] expecting the second token.

    Why it fails

    Lists and tensors count from zero, so index 1 is the second token.

    Do this instead

    Use inputs[1] to select the second token.

  • Common mistake

    Using a hand-written softmax on extreme values

    What it looks like

    Passing very large scores to softmax_naive and receiving inf or zero-filled results.

    Why it fails

    Overflow and underflow can corrupt results even when the formula is correct.

    Do this instead

    Use 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.

MEDIUM

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.

Related concepts

More concepts starting with C