Concepts / Simple Self-Attention Without Trainable Weights

Simple Self-Attention Without Trainable Weights

SelfAttention_v2 builds its query, key, and value projections with nn.Linear layers, configured by d_in, d_out, and qkv_bias.

  • Programming

Reading the Class Before Running It

This article starts with code rather than theory. SelfAttention_v2 is an improved implementation of self-attention in PyTorch. Instead of managing its three projection matrices by hand, it builds them from PyTorch's Linear layers. The approach is to read the class once, predict what a call to it returns, run it, and then use the explanation to make sense of the printed result.

What do you think happens?

The class is called on an input tensor x that uses the same d_in and d_out as the earlier self-attention example. What does the call sa_v2(inputs) return? Think about how many rows it has, how many columns, and what the last dimension is tied to.

Reveal answer
Answer:

A tensor with one row per input token and d_out columns. Its last dimension is set by d_out.

The context vector is built as attention weights multiplied by values. The values come from a projection to d_out, so the last dimension of the output is d_out. The attention weights form a square matrix with one row and one column per token, so the number of rows of the output matches the number of rows of the queries, which is the number of input tokens. The exact printed numbers depend on the random seed and inputs, but the shape follows from the layer dimensions.

The Constructor and Its Three Linear Layers

The constructor takes d_in and d_out, which are the input and output dimensions, and an optional qkv_bias flag that defaults to False. From these arguments it creates three Linear layers, one each for queries, keys, and values. Each layer maps an input of size d_in to an output of size d_out. Because all three layers share the same dimensions, the queries, keys, and values all have width d_out.

Definition

The bias argument, qkv_bias, is passed straight through to each of the three Linear layers. The same setting therefore applies to all three projections at once. The class does not allow the query projection and the value projection to have different bias settings.

Key point

The three projections are the only place where the input x is transformed into queries, keys, and values. Everything after the constructor works on those three outputs, so reading the constructor tells you which layers exist and what size each one maps between.

Tracing the Data Path

The forward method runs in a fixed order. It first projects the input x three times, producing keys, queries, and values. It then computes raw attention scores by multiplying the queries with the transpose of the keys. Those scores are divided by the square root of the key dimension, which is read from keys.shape[-1], and a softmax is applied along the last dimension. The result is the attention weights. Finally, the attention weights are multiplied by the values, and the product is the context vector that the method returns.

Input xQueriesRaw scoresAttention weightsContext vectorKeysValuesLinear layerLinear layerLinear layerquery times keys transposescaledsoftmaxweights times valuesweighted sum
Where x, the three projections, the score matrix, the softmax weights, and the context vectors feed into one another, and where each tensor flows next.

Attention Weights as a Score Matrix

The raw scores are produced by multiplying queries with the transpose of keys. Each query row is compared against each key column, so the result is a square matrix. If the input has T tokens, the score matrix is T by T. The entry in row i and column j measures how strongly token i's query matches token j's key. Each row therefore describes one token's view of every token in the input, including itself.

score[1,1]query 1 vs key 1score[2,1]query 2 vs key 1score[3,1]query 3 vs key 1score[1,2]query 1 vs key 2score[2,2]query 2 vs key 2score[3,2]query 3 vs key 2score[1,3]query 1 vs key 3score[2,3]query 2 vs key 3score[3,3]query 3 vs key 3
Which query row is compared against which key column to produce each attention score, and how the matrix maps back to input positions.
Rule

Here $d_k$ is the key dimension, read from keys.shape[-1]. The division by the square root of $d_k$ scales the raw scores before the softmax. The softmax is taken along the last dimension, so each row of weights is normalized on its own and each row describes how one token distributes its attention across all tokens.

From Attention Weights to the Context Vector

The final step multiplies the attention weights by the values. Each row of the attention weights picks out a mixture of the value vectors. The row for token i gives one weight to every value vector, and the context vector for token i is the weighted sum of those value vectors. Value vectors that receive larger weights contribute more to that token's context vector. One context vector is produced per input token.

Formula
Key point

The context vector is built from the values projection, which is why the output width is set by d_out. The queries and keys determine how much each value counts. The values determine what gets carried into the output.

Predicting the Output Shape

Worked example

Shape of the output for six tokens

An input x has 6 tokens, each with d_in equal to 3. The layer is built with d_out equal to 2. What shape does the call sa_v2(x) return?

  1. 1
    Count the input tokens

    The input has 6 rows, one per token, so queries, keys, and values each have 6 rows.

  2. 2
    Build the score matrix

    Queries multiplied by the transpose of keys gives a 6 by 6 score matrix, and the softmax keeps that shape.

  3. 3
    Read the value width

    Each value vector has width d_out, which is 2.

  4. 4
    Multiply weights by values

    A 6 by 6 weight matrix times a 6 by 2 value matrix gives a 6 by 2 context matrix.

Result

The output has shape 6 by 2: one context vector of width 2 for each of the 6 tokens. The printed numbers would differ with a different seed or different inputs, but the shape would not change as long as d_out stays at 2.

Replacing Hand-Built Projections with nn.Linear

The earlier version handled its projections by hand, meaning the weight matrices were managed directly in the class. The Linear-layer version uses nn.Linear modules instead. Each module holds its own weight matrix that maps d_in inputs to d_out outputs, and it can optionally hold a bias term controlled by qkv_bias. The class no longer tracks these matrices itself. It creates three modules and calls them on x, and the modules handle the stored weights.

Weight matrixManual multiplyProjected xnn.Linear(d_in,d_out)Weights stored inmoduleProjected xOptional bias
What changes between the hand-built weight-matrix version and the nn.Linear version, and what the module adds.

Mistakes Beginners Make

  • Common mistake

    Assuming the output has the same width as the input

    What it looks like

    Reading an input with d_in equal to 3 and expecting a 3-column result.

    Why it fails

    The context vector is built from the values projection, which maps to d_out.

    Do this instead

    Read the last dimension of the output from d_out, not from d_in.

  • Common mistake

    Expecting the printed numbers to be the same on every run

    What it looks like

    Changing the seed and expecting the same printed context vectors.

    Why it fails

    The printed values are specific to the seed and the inputs.

    Do this instead

    Expect the shape to stay the same when d_out stays the same, and expect the numbers to change.

  • Common mistake

    Looking for queries and values to interact early in the forward pass

    Why it fails

    Queries and keys meet in the score computation, and values meet the attention weights only at the end.

    Do this instead

    Follow the fixed order: projections, scores, scaled softmax, then weights times values.

  • Common mistake

    Treating the bias setting as separate for each projection, when qkv_bias applies to all three layers

Practice Tracing the Data Path

MEDIUM

An input x has 4 tokens, d_in equal to 5, and d_out equal to 3. Without running the code, state the shape of the queries, the keys, the values, the raw score matrix, the attention weights, and the context vector. Then name which tensors meet in the score computation and which tensor is multiplied by the attention weights.

Hints
  • Each projection maps d_in to d_out, so queries, keys, and values all have width d_out.
  • The score matrix is square, with one row and one column per token.
  • The context vector is the attention weights multiplied by the values.

Key Takeaways

  • SelfAttention_v2 creates its query, key, and value projections with three nn.Linear layers, each configured by d_in, d_out, and the shared qkv_bias flag.
  • The forward method runs in a fixed order: projections, raw scores from queries times keys transpose, a scaled softmax over the last dimension, and finally weights times values to produce the context vector.
  • The output width is set by d_out because the context vector is built from the values projection, and the printed numbers depend on the seed and inputs while the shape does not.
  • The nn.Linear version replaces hand-built projection matrices with modules that store their own weights and an optional bias.

Related concepts

More concepts starting with S