Self-Attention Concept › The Computational Pattern

Michael BrenndoerferUpdated February 6, 202611 min read
Language AI HandbookAll chapters
Part 13 of 67Self-Attention
Chapter 90Self-Attention Concept › The Computational Pattern
Reading section 5 of 13 · The Computational Pattern · 11 minSection 5 of 13 · 11 min

Book contents

Section 5 of 13 · 11 min read

Self-Attention Concept › The Computational PatternOne section · Sections in this chapter5 / 13 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Now that we understand why self-attention is useful, let's examine how it works. The core idea is elegantly simple: to build a contextual representation for any token, we take a weighted average of all tokens in the sequence. The weights reflect how relevant each token is to the one we're updating.

Think of it like asking every word in a sentence: "How much should I pay attention to you when trying to understand myself?" Words that are more relevant get higher weights, and their information contributes more to the final representation.

The elegance of this design is that relevance is computed from the same embeddings that we are trying to enrich. There are no separate relevance signals or external knowledge sources. The model learns, from data, which kinds of tokens should attend to which other kinds of tokens. A verb learns to attend to nouns. A pronoun learns to attend to candidate antecedents. An adjective learns to attend to the noun it modifies. All of this emerges from training on text, supervised only by the task objective.

In practice, this means that self-attention is a form of dynamic, input-dependent aggregation. Unlike a convolution, which always applies the same kernel to local neighborhoods regardless of content, self-attention computes different aggregation weights for every input. Two sentences with different words will produce entirely different attention patterns. The same word in two different sentences will aggregate information from different neighbors with different weights. This input-dependence is what allows self-attention to be sensitive to context in a way that convolutions and bag-of-words models cannot be.

The Three-Stage Pipeline

Self-attention follows a consistent three-stage pattern:

  1. Compute similarity scores: For each pair of positions, calculate a number indicating how relevant one is to the other. High scores mean strong relevance.

  2. Normalize to weights: Raw scores can be any real number. We need to convert them into proper weights that sum to 1, so we can interpret them as "how much attention to pay." The softmax function handles this conversion.

  3. Aggregate values: Finally, compute a weighted sum of all positions' representations. Positions with higher weights contribute more to the output.

This pipeline runs for every position simultaneously, producing a new set of contextual embeddings where each token has gathered information from the entire sequence.

These three stages map onto a natural information-theoretic story. In stage one, we are asking a question: "How related are these two tokens?" In stage two, we are converting raw relatedness scores into a probability distribution over which tokens to consult. In stage three, we are reading the answer: a blend of all tokens' information, weighted by how likely each one is to be the right source.

The beauty of this formulation is that it is fully differentiable. Every step can be computed using standard matrix operations, and gradients flow cleanly through all three stages. This is what makes it possible to train self-attention end-to-end with gradient descent. The model does not need to know in advance which tokens should attend to which. It learns this from data by adjusting the embeddings so that tokens that should attend to each other have high dot products.

From Intuition to Formula

Let's formalize this intuition. Suppose we have a sequence of nn tokens, each represented by a dd-dimensional embedding vector. We can write the input as a matrix:

X=[x1,x2,…,xn]\mathbf{X} = [\mathbf{x}_1, \mathbf{x}_2, \ldots, \mathbf{x}_n]

where each xi∈Rd\mathbf{x}_i \in \mathbb{R}^d is the embedding for token ii.

Stage 1: Similarity Scores

How do we measure relevance between two tokens? The simplest approach uses the dot product. If two embedding vectors point in similar directions, their dot product is large and positive. If they're orthogonal (unrelated), the dot product is zero. This gives us a natural measure of similarity.

The dot product is not the only possible similarity measure. You could use cosine similarity, a learned bilinear form, or even a neural network. But the dot product has the virtue of simplicity, efficiency, and a clean geometric interpretation. It is also naturally suited to the way embeddings are trained: if you want two tokens to have high similarity under dot product, you train their embeddings to point in the same direction. This is exactly how the model learns to encode semantic relationships.

For positions ii and jj, the similarity score is:

sij=xi⋅xj=∑k=1dxi,k⋅xj,ks_{ij} = \mathbf{x}_i \cdot \mathbf{x}_j = \sum_{k=1}^{d} x_{i,k} \cdot x_{j,k}

where:

  • sijs_{ij}: the raw similarity score between positions ii and jj
  • xi⋅xj\mathbf{x}_i \cdot \mathbf{x}_j: the dot product of the two embedding vectors
  • dd: the embedding dimension
  • xi,kx_{i,k}: the kk-th component of the embedding at position ii

The dot product captures semantic similarity: tokens with similar meanings tend to have similar embeddings, producing high dot products. This is only true if the embeddings have been trained appropriately, but in practice, transformer embeddings rapidly learn to encode semantic similarity in ways that the dot product can exploit.

Stage 2: Softmax Normalization

Raw dot products can be any real number, positive or negative, with no upper bound. To use them as weights for averaging, we need to transform them into a probability distribution. The softmax function accomplishes this:

αij=exp⁡(sij)∑k=1nexp⁡(sik)\alpha_{ij} = \frac{\exp(s_{ij})}{\sum_{k=1}^{n} \exp(s_{ik})}

where:

  • αij\alpha_{ij}: the attention weight from position ii to position jj
  • exp⁡(sij)\exp(s_{ij}): the exponential function applied to the similarity score. This keeps positivity
  • ∑k=1nexp⁡(sik)\sum_{k=1}^{n} \exp(s_{ik}): the normalizing constant that makes all weights from position ii sum to 1

Why softmax? It has two useful properties:

  1. Positivity: The exponential function maps any real number to a positive value, so all weights are positive.
  2. Normalization: Dividing by the sum ensures ∑j=1nαij=1\sum_{j=1}^{n} \alpha_{ij} = 1, giving us valid weights for a weighted average.

Additionally, softmax amplifies differences: if one score is much higher than others, its weight dominates. This allows the model to focus sharply on the most relevant tokens when appropriate.

There is an important practical consideration here: the scale of the dot products. If the embeddings have dimension dd and their components are approximately unit variance, then the dot product of two random embeddings will have variance approximately dd. For large dd, this means dot products can be very large in magnitude, pushing the softmax into a regime where its gradient is nearly zero. This is the motivation for the scaled dot product that you will encounter in the full query-key-value formulation in the next chapter: dividing by d\sqrt{d} keeps the dot products in a reasonable range where softmax remains informative. For now, the basic formulation is sufficient to understand the core mechanism.

The softmax function also has an interesting normalization property: it computes a local competition. Each token ii does not have a global attention budget that it must divide among all other tokens in the sequence (which would be a simplex constraint across all ii and jj). Instead, for each query position ii, the weights from ii to all key positions jj must sum to 1. This means position ii always attends to something, even if no token is truly relevant. In practice, when no token is highly relevant, the attention spreads uniformly across all tokens, and the output is close to the average of all embeddings.

Stage 3: Weighted Aggregation

With attention weights in hand, we compute the output representation for each position as a weighted sum of all input embeddings:

yi=∑j=1nαijxj\mathbf{y}_i = \sum_{j=1}^{n} \alpha_{ij} \mathbf{x}_j

where:

  • yi\mathbf{y}_i: the output representation for position ii, now enriched with contextual information
  • αij\alpha_{ij}: the attention weight determining how much position jj contributes to position ii's output
  • xj\mathbf{x}_j: the input embedding at position jj

This formula is the heart of self-attention. Each output yi\mathbf{y}_i is a blend of all inputs, with the blending proportions determined by the learned attention weights. Tokens that are highly relevant to position ii contribute strongly; irrelevant tokens contribute little.

Notice that the output lives in the same vector space as the input: yi∈Rd\mathbf{y}_i \in \mathbb{R}^d, just like xi\mathbf{x}_i. This is important for stacking multiple layers of self-attention. The output of one layer can serve directly as the input to the next, with no change in dimension. Each successive layer can build on the contextual representations computed by the previous one, creating increasingly abstract and task-relevant representations. This is exactly how transformers work: multiple self-attention layers applied in sequence, each one refining the representations produced by the previous layer.

Putting It All Together

The complete self-attention computation flows naturally from these three stages. For every position ii in the sequence:

  1. Compute dot products with all positions: sij=xi⋅xjs_{ij} = \mathbf{x}_i \cdot \mathbf{x}_j for j=1,…,nj = 1, \ldots, n
  2. Apply softmax to get attention weights: αij=softmax(si1,si2,…,sin)j\alpha_{ij} = \text{softmax}(s_{i1}, s_{i2}, \ldots, s_{in})_j
  3. Compute weighted sum: yi=∑j=1nαijxj\mathbf{y}_i = \sum_{j=1}^{n} \alpha_{ij} \mathbf{x}_j

Because each position's computation is independent of the others, we can process all nn positions in parallel. This parallelism is what makes self-attention so efficient on modern GPU hardware.

In matrix form, the entire computation collapses into three operations: a matrix multiplication to compute scores, a row-wise softmax, and another matrix multiplication to compute outputs. The notation also exposes the implementation: the entire self-attention operation can be executed as two large matrix multiplications, which GPUs and TPUs are specifically designed to perform as fast as possible. The theoretical O(n2d)O(n^2 d) complexity, which sounds expensive, is quite manageable in practice because modern accelerators can execute these matrix operations with extreme parallelism.

Implementation

Let's translate this mathematical framework into code. We'll implement a simple self-attention function that takes a sequence of embeddings and returns the contextual outputs along with the attention weights.

In[7]:
Code
def simple_self_attention(embeddings):
    """
    Simplified self-attention using dot product similarity.

    Args:
        embeddings: numpy array of shape (seq_len, embed_dim)

    Returns:
        output: numpy array of shape (seq_len, embed_dim)
        attention_weights: numpy array of shape (seq_len, seq_len)
    """
    # Step 1: Compute pairwise similarity scores (dot product)
    # Matrix multiplication gives us all n^2 dot products at once
    # scores[i,j] = x_i · x_j
    scores = embeddings @ embeddings.T

    # Step 2: Normalize with softmax (row-wise)
    # Subtract max for numerical stability (prevents overflow in exp)
    scores_stable = scores - scores.max(axis=1, keepdims=True)
    exp_scores = np.exp(scores_stable)
    attention_weights = exp_scores / exp_scores.sum(axis=1, keepdims=True)

    # Step 3: Compute weighted sum of embeddings
    # output[i] = sum_j alpha[i,j] * embeddings[j]
    output = attention_weights @ embeddings

    return output, attention_weights

Notice how the implementation mirrors the three-stage formula. The matrix multiplication embeddings @ embeddings.T computes all n2n^2 dot products simultaneously: entry (i,j)(i, j) contains xi⋅xj\mathbf{x}_i \cdot \mathbf{x}_j. The softmax is applied row-wise, so each row of attention weights sums to 1. Finally, multiplying the attention weights by the embeddings computes all weighted sums in parallel.

The numerical stability trick of subtracting the row maximum before exponentiating is worth understanding. The exponential function grows extremely quickly: e100e^{100} is a number so large it overflows standard 32-bit floating point. However, softmax has a mathematical property that allows us to subtract any constant from all inputs without changing the output. If we subtract the maximum value in each row, we ensure that the largest exponentiated value is e0=1e^0 = 1, and all other values are between 0 and 1. This does not change the softmax output but prevents numerical overflow.

Let's test this on a small sequence to see the attention mechanism in action:

In[8]:
Code
# Create simple embeddings for demonstration
np.random.seed(42)
seq_len, embed_dim = 4, 8
embeddings = np.random.randn(seq_len, embed_dim)

# Apply self-attention
output, attention_weights = simple_self_attention(embeddings)
Out[9]:
Console
Input shape: (4, 8)
Output shape: (4, 8)
Attention weights shape: (4, 4)

Attention weight matrix (rows sum to 1):
[[0.997 0.    0.    0.003]
 [0.    0.99  0.008 0.001]
 [0.    0.007 0.993 0.   ]
 [0.003 0.007 0.    0.99 ]]

Row sums: [1. 1. 1. 1.]

The attention weight matrix is n×nn \times n, where entry (i,j)(i, j) tells us how much position ii attends to position jj. Each row sums to exactly 1.0, confirming that softmax produces valid probability distributions. The output has the same shape as the input: each of our 4 tokens now has a new 8-dimensional representation that incorporates information from all other tokens.