Concepts / Computing Context Vectors with Attention Weights

Computing Context Vectors with Attention Weights

Attention scores are dot products between input vectors and can be computed with nested loops or with inputs @ inputs.T.

  • Programming

Why Attention Starts With a Grid of Comparisons

Attention weights tell us how strongly each input token should influence a chosen query token. On their own, though, they are only numbers that sum to 1.0. The step that turns them into something a model can use is the context vector: a single vector that blends the whole input sequence according to those weights. This article walks through the full pipeline, from raw comparisons between input vectors, to normalized weights, to the blended context vector, and it checks at each stage what the output should look like.

Self-attention begins by measuring how strongly each input token relates to every other input token. For a sequence of six input vectors, that means comparing every vector against every other vector, which produces a grid with six rows and six columns. The first version of this computation fills that grid one cell at a time with nested loops. The same grid can later be produced with a single matrix multiplication, and then the raw grid is turned into weights whose rows behave like proportions.

Attention scoresdot products of all inputpairsAttention weightssoftmax with dim=-1, rowssum to 1Context vectorsweighted sum of all inputsnormalizeweight inputs
Which stage produces the scores, which produces the attention weights, and which produces the context vectors? Each stage has a checkpoint with an expected shape and row behavior.

Read the stages as a pipeline with a checkpoint at each stage. Each stage produces a tensor, and each tensor has an expected shape and expected row behavior. Knowing what each stage should produce is what lets you find the exact place where a result goes wrong.

Computing Scores With Nested Loops

The loop-based computation starts by allocating an empty six by six tensor. Two nested loops visit every pair of input vectors, and each cell receives the dot product of the two vectors that index it. The outer loop picks a row i and the inner loop picks a column j, so the cell at position (i, j) holds the dot product of input i with input j. Every cell gets exactly one dot product, so the grid has thirty six entries.

What do you think happens?

Before reading on: what do you expect about the cell at row i, column j compared with the cell at row j, column i?

  • They are always different numbers

  • They are equal, because the same two vectors are being compared

Reveal answer
Answer:

They are equal. The dot product of input i with input j is the same value as the dot product of input j with input i, so the score grid is symmetric across the diagonal.

The diagonal cells pair each vector with itself. The off-diagonal cells pair different tokens, and each pair appears twice, once above the diagonal and once below it.

Compare that prediction with the source output. The scores are symmetric across the diagonal, so the cell at row 0, column 1 and the cell at row 1, column 0 both hold 0.9544. The diagonal, where each vector is paired with itself, is worth noticing as well, because it is the one place where a vector meets its own copy.

python
Output (expected)
torch.Size([6, 6])
Notes on the code
  • torch.empty allocates the grid before any values are filled in
  • The outer loop chooses the row i and the inner loop chooses the column j
  • Each cell holds the dot product of the two inputs that index it

Replacing Loops With One Matrix Multiplication

The loop version is correct, but for loops are generally slow. The same grid of scores can be produced with a single matrix multiplication, written inputs @ inputs.T. The input matrix multiplied by its own transpose contains, in each cell, the dot product of the corresponding row vectors. The transpose turns the columns of the second copy into rows, so the product lines up every vector against every other vector in one operation.

python
Output (expected)
torch.Size([6, 6])
Notes on the code
  • inputs.T flips the rows and columns of the second copy
  • The product places the dot product of every pair of rows into one grid

The source notes that the matrix version produces the same results as the loop version, and the values can be checked visually. This is the verification habit to carry forward: when you change how a computation is written, compare the new output against the old one, element by element or as a whole grid.

Normalizing Scores With Softmax

The second stage normalizes the score grid so that the values in each row behave like proportions. The result of this step is called the attention weight tensor, and attention weights are the normalized version of the attention scores. Each raw score is passed through the softmax function, which makes each value positive and divides it by the total of its row, so that the row sums to 1.

Formula

In this expression s_{ij} is the raw score at row i and column j, and a_{ij} is the resulting attention weight. The denominator sums over the columns k of the same row i, which is exactly why each row ends up summing to 1.

The dim argument decides which axis the normalization runs across. In PyTorch, dim=-1 tells softmax to work along the last dimension. For a two-dimensional score tensor shaped as rows and columns, this means each row is normalized across its columns. Changing dim changes which direction is normalized, and that changes what the rows mean. This is why the choice of dim is part of the correctness of the result, not a cosmetic setting.

python
Output (expected)
tensor([1., 1., 1., 1., 1., 1.])
Notes on the code
  • dim=-1 normalizes along the columns, one row at a time
  • Summing along dim=-1 gives one total per row, and each total should be 1
row 0, col 1score 0.9544row 0, col 1weight 0.2006row 1, col 0score 0.9544row 1, col 0weight 0.1385softmax over row 0softmax over row 1
Which cell holds the dot product of which pair of inputs, and how does the mirror cell relate to it? Scores match across the diagonal, but normalized weights in the same positions differ.

The source output shows why the normalized weights are not symmetric. In the score grid, the cells at row 0, column 1 and row 1, column 0 both hold 0.9544. In the weight grid, the same two positions hold 0.2006 and 0.1385, which differ. Each row is normalized by its own total, so each row changes by a different amount, and the mirror symmetry of the scores is lost in the weights. Scores are symmetric, but the normalized weights generally are not.

Checking That Each Row Sums to One

After normalization, the expected property is simple: every row of the weight tensor should sum to 1. Checking this is the fastest way to catch a normalization step that was applied along the wrong axis. The check can be done for a single row, by adding the values of one row, or for the whole tensor, by summing along the last dimension and confirming that every total is 1.

Building a Context Vector as a Weighted Sum

A context vector is the weighted sum of all input vectors, where each input is multiplied by its attention weight for one chosen query. Read this definition as a recipe with three ingredients. You need the full list of input vectors, the attention weight attached to each of them for one chosen query, and a running total that the weighted vectors are added into. Nothing in the recipe depends on the order of the inputs except that each weight must stay paired with its own input vector.

Formula

In this notation z^{2} is the context vector for the second token, x^{i} is the i-th input vector, and a^{2}_{i} is the attention weight that the second token assigns to input i. The sum runs over all T input tokens, so every input contributes to the result, even the ones that receive a very small weight.

input x0scaled by weight a0running totalsum of scaled inputscontext vector zone vector for the queryinput x1scaled by weight a1input x2scaled by weight a2addaddaddresult
How does each input vector flow into the weighted sum for a single query row, scaled by its attention weight, to form one context vector?

Tracing the Accumulation Loop

The implementation follows the formula directly. It starts from a zero vector with the same shape as the query, then walks through the inputs one at a time. On each pass it scales the current input vector by its matching weight and adds the result into the running vector. Because the loop uses enumerate, the index i selects the right weight for the input x_i at the same position.

python
Output (expected)
tensor([0.4419, 0.6515, 0.5683])
Notes on the code
  • torch.zeros starts the running vector empty, with the same shape as the query
  • attn_weights[query_idx] selects the row of weights for the chosen query
  • enumerate pairs each index i with its own input x_i and its own weight
Worked example

Predicting the shape of the context vector

Six input vectors, each of dimension 3, and a chosen query. Predict the shape of the context vector and the number of multiplications the loop performs.

  1. 1
    Identify the query shape

    Each context vector has the same shape as the query, which is one input vector of dimension 3.

  2. 2
    Count the passes

    The loop visits every input once, so there are six passes, one per input token.

  3. 3
    Count the scalings

    Each pass multiplies one input vector by one scalar weight, giving six scale-and-add operations.

  4. 4
    Check the running total

    The zero start means the first addition carries no leftover values, and after six passes the running vector holds the full sum.

Result

The context vector has shape 3, and the loop performs six scale-and-add operations, one for each input token.

The zero start matters. The running vector has to begin empty so that the first addition does not carry leftover values from anywhere else. After the loop finishes, the running vector holds the complete sum, and printing it shows the context vector. The source's printed output for its query is three values, which the trace above is designed to let you reproduce by reasoning about the pass-by-pass totals.

Mistakes That Corrupt a Context Vector

Errors in this computation can be located by checking the query index, the normalization of the weights, and the pairing of each weight with its input. The following mistakes are the most common ways the pipeline fails.

  • Common mistake

    Normalizing along the wrong axis

    What it looks like
    attn_weights = torch.softmax(attn_scores, dim=0), followed by a row-sum check that returns column-like totals
    Why it fails

    With dim=0 each column is normalized instead of each row, so the rows no longer represent proportions for a single query.

    Do this instead

    Use dim=-1 so each row of the score grid is normalized across its columns.

  • Common mistake

    Starting the running total from leftover values

    What it looks like
    context_vec = torch.empty(query.shape) before the loop, with no zeroing
    Why it fails

    The first addition then adds to whatever values happened to be in memory, so the sum is contaminated before the loop begins.

    Do this instead

    Start from torch.zeros with the same shape as the query.

  • Common mistake

    Pairing a weight with the wrong input

    What it looks like

    Using attn_weights[0][i] while looping over the inputs of query 1

    Why it fails

    The weight for one query is paired with input vectors it was not computed for, so each contribution is scaled incorrectly.

    Do this instead

    Select the row for the chosen query, attn_weights[query_idx], and pair each weight with its input through enumerate.

  • Common mistake

    Assuming the score grid is the final answer, when it has not yet been normalized into weights

Computing Every Context Vector at Once

Computing one context vector answers a single question: how should the second token's representation be blended from the whole sequence? A model needs an answer for every token. The source handles this by repeating the same three steps for each query rather than inventing a new method. Attention weights are first computed for every input token as a query, producing a full table with one row per query. Each row of that table is then used in the same weighted sum to produce that query's context vector.

The source notes that the change needed to compute all context vectors, instead of only one, is a modification to the code rather than to the underlying steps. The three steps remain the same: take the query's row of weights, scale each input by its matching weight, and add the results into a running total. A matrix formulation organizes these repeated passes into one pass, but each output row is still a weighted sum of all input vectors for its query.

Practice: Diagnosing a Column-Normalized Weight Tensor

MEDIUM

A teammate builds the weight tensor and then checks its rows by summing along dim=0. Every column total comes out to 1, but the row totals do not. Using the pipeline, name the stage that introduced the error, state what each row of the correct weight tensor should sum to, and explain why the context vector for token 1 would be wrong if you used the column-normalized weights.

Hints
  • Ask which axis the softmax normalized before you look at the checks.
  • Each row of the weight tensor corresponds to one query token.
  • The context vector for a query uses that query's row of weights.

Summary of the Context Vector Pipeline

  1. 1
    Attention scores are dot products between every pair of input vectors. They can be computed with nested loops or with a single matrix multiplication, inputs @ inputs.T, and the two versions should agree.
  2. 2
    Softmax with dim=-1 normalizes each row of the score grid so that each row of the attention weights sums to 1. The dim argument decides the normalization axis, and a failed row-sum check usually points to it.
  3. 3
    A context vector is the weighted sum of all input vectors for one query. It starts from a zero vector shaped like the query and adds each weighted input at its matching index.
  4. 4
    Computing every context vector repeats the same three steps for every query, using one row of the attention weight table per query.

At a glance

  • Attention scores are dot products between every pair of input vectors and can be computed with nested loops or with inputs @ inputs.T.
  • Softmax along dim=-1 turns each row of the score grid into attention weights whose rows sum to 1.
  • A context vector is the weighted sum of all input vectors for one query, built by an accumulation loop that starts from a zero vector.
  • When a result is wrong, check the row sums first, since a failed check usually points to the normalization axis.

Related concepts

More concepts starting with C