Query, Key, Value › The QKV Mechanism: From Intuition to Formula

Michael BrenndoerferUpdated June 4, 202521 min read
Language AI HandbookAll chapters
Part 13 of 67Self-Attention
Chapter 91Query, Key, Value › The QKV Mechanism: From Intuition to Formula
Reading section 3 of 11 · The QKV Mechanism: From Intuition to Formula · 21 minSection 3 of 11 · 21 min

Book contents

Section 3 of 11 · 21 min read

Query, Key, Value › The QKV Mechanism: From Intuition to FormulaOne section · Sections in this chapter3 / 11 ↓
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 we need separate representations for querying, being queried, and contributing content, let's build up the mathematical machinery that makes this work. We'll develop each component step by step, showing how the formulas directly address the challenge of learning flexible attention patterns.

The journey from intuition to formula follows a natural progression:

  1. Project tokens into specialized QKV spaces
  2. Measure compatibility between queries and keys using dot products
  3. Scale the scores to prevent numerical instability
  4. Aggregate values according to the attention weights

Each step solves a specific problem that arises from the previous one. By the end, you'll see how these pieces combine into a single elegant formula that powers modern transformers.

Step 1: Projecting Into Specialized Spaces

The core insight is simple: if we want tokens to play different roles, we should give them different representations for each role. We accomplish this through learned linear projections, separate transformations that map each token's embedding into QKV spaces.

Why three separate projections? Consider what happens when a pronoun like "it" needs to find its antecedent. The pronoun's embedding encodes "third-person singular neuter pronoun," but that's not what it should search for. It should search for "noun phrase that could be referred to." Meanwhile, a noun like "cat" shouldn't advertise "I'm a cat" but rather "I'm a noun phrase available for reference." And when "cat" contributes information, it should provide its semantic content (the concept of a cat), not its grammatical role.

These are three fundamentally different jobs:

  • Querying: What information am I looking for?
  • Being queried: What information do I have to offer?
  • Contributing: What content should I transmit?

A single embedding can't optimally serve all three purposes. So we learn three different transformations, each specialized for its role.

Given an input embedding x∈Rd\mathbf{x} \in \mathbb{R}^d for a single token, we compute:

q=xWQ,k=xWK,v=xWV\mathbf{q} = \mathbf{x} \mathbf{W}_Q, \quad \mathbf{k} = \mathbf{x} \mathbf{W}_K, \quad \mathbf{v} = \mathbf{x} \mathbf{W}_V

where:

  • x∈Rd\mathbf{x} \in \mathbb{R}^d: the input embedding vector (a row vector with dd dimensions)
  • q∈Rdk\mathbf{q} \in \mathbb{R}^{d_k}: the resulting query vector, expressing what this token is looking for
  • k∈Rdk\mathbf{k} \in \mathbb{R}^{d_k}: the resulting key vector, advertising what this token contains
  • v∈Rdv\mathbf{v} \in \mathbb{R}^{d_v}: the resulting value vector, carrying the content to be aggregated
  • WQ∈Rd×dk\mathbf{W}_Q \in \mathbb{R}^{d \times d_k}: the query projection matrix (learned during training)
  • WK∈Rd×dk\mathbf{W}_K \in \mathbb{R}^{d \times d_k}: the key projection matrix (learned during training)
  • WV∈Rd×dv\mathbf{W}_V \in \mathbb{R}^{d \times d_v}: the value projection matrix (learned during training)
  • dd: the input embedding dimension
  • dkd_k: the dimension of queries and keys (must match for dot product compatibility)
  • dvd_v: the dimension of values (can differ from dkd_k)

Each projection is a matrix multiplication, a linear transformation where each output dimension is a weighted combination of input dimensions. These weights are learned during training. The model doesn't know in advance which embedding features matter for querying versus being queried versus contributing content. Instead, gradient descent discovers these patterns from data:

  • WQ\mathbf{W}_Q learns which combinations of embedding features make effective queries (what patterns indicate "I need information about X")
  • WK\mathbf{W}_K learns which combinations make effective keys (what patterns indicate "I have information about X")
  • WV\mathbf{W}_V learns which content is worth transmitting when attention flows

This learning happens implicitly through the training objective. If attending from a verb to its subject improves next-word prediction, the projection matrices gradually adjust to make verb queries match subject keys.

Scaling to sequences. For a complete sequence of nn tokens, we stack all embeddings into a matrix X∈Rn×d\mathbf{X} \in \mathbb{R}^{n \times d} (where each row is one token) and project all tokens simultaneously:

Q=XWQ,K=XWK,V=XWV\mathbf{Q} = \mathbf{X} \mathbf{W}_Q, \quad \mathbf{K} = \mathbf{X} \mathbf{W}_K, \quad \mathbf{V} = \mathbf{X} \mathbf{W}_V

The resulting matrices have shapes Q∈Rn×dk\mathbf{Q} \in \mathbb{R}^{n \times d_k}, K∈Rn×dk\mathbf{K} \in \mathbb{R}^{n \times d_k}, and V∈Rn×dv\mathbf{V} \in \mathbb{R}^{n \times d_v}. Row ii of each matrix is the query, key, or value vector for token ii.

In[4]:
Code
def project_qkv(embeddings, W_q, W_k, W_v):
    """
    Project input embeddings into query, key, and value spaces.

    Args:
        embeddings: Input matrix of shape (seq_len, embed_dim)
        W_q: Query projection matrix of shape (embed_dim, d_k)
        W_k: Key projection matrix of shape (embed_dim, d_k)
        W_v: Value projection matrix of shape (embed_dim, d_v)

    Returns:
        Q, K, V matrices
    """
    Q = embeddings @ W_q  # (seq_len, d_k)
    K = embeddings @ W_k  # (seq_len, d_k)
    V = embeddings @ W_v  # (seq_len, d_v)
    return Q, K, V

Why might these projections learn different things? Consider an embedding that encodes both syntactic information (part of speech, grammatical role) and semantic information (meaning, topic). The query projection WQ\mathbf{W}_Q might learn to emphasize syntactic features, helping verbs find their subjects by grammatical role rather than semantic similarity. Meanwhile, the value projection WV\mathbf{W}_V might preserve semantic content, so that when attention flows, it transfers meaning rather than grammatical markers.

The separation between keys and values is particularly powerful. A word's key determines which queries it matches (what attention it receives), while its value determines what information it contributes. This decoupling means a word can attract attention for one reason (syntactic role) while contributing entirely different information (semantic content).

Step 2: Measuring Compatibility with Dot Products

With queries and keys in hand, we face the next challenge: how do we measure whether a query matches a key? We need a scoring function that takes two vectors and returns a single number indicating compatibility. Higher numbers should mean better matches.

Several options exist, including additive attention, multiplicative attention, and others, but the dot product has become the standard choice for its simplicity and efficiency. The geometric interpretation is intuitive:

  • Vectors pointing in the same direction → large positive dot product (strong match)
  • Orthogonal vectors → zero dot product (no relationship)
  • Vectors pointing in opposite directions → negative dot product (poor match)

This gives us exactly the ranking behavior we want: the most compatible keys get the highest scores, and training can learn arbitrary matching patterns by adjusting the projection matrices.

For positions ii and jj, the compatibility score is:

scoreij=qi⋅kj=∑m=1dkqim⋅kjm\text{score}_{ij} = \mathbf{q}_i \cdot \mathbf{k}_j = \sum_{m=1}^{d_k} q_{im} \cdot k_{jm}

where:

  • scoreij\text{score}_{ij}: the raw attention score measuring how well position ii's query matches position jj's key
  • qi∈Rdk\mathbf{q}_i \in \mathbb{R}^{d_k}: the query vector for position ii
  • kj∈Rdk\mathbf{k}_j \in \mathbb{R}^{d_k}: the key vector for position jj
  • qimq_{im}, kjmk_{jm}: the mm-th components of these vectors
  • dkd_k: the dimension of query and key vectors (must match for the dot product to be defined)

The beauty of the dot product is that it's differentiable, fast to compute, and captures alignment in learned representation space. Because qi\mathbf{q}_i and kj\mathbf{k}_j come from learned projections, the model can discover arbitrary matching patterns during training.

To compute all n2n^2 scores at once (every query against every key), we use matrix multiplication:

S=QKT\mathbf{S} = \mathbf{Q} \mathbf{K}^T

where:

  • S∈Rn×n\mathbf{S} \in \mathbb{R}^{n \times n}: the score matrix containing all pairwise attention scores
  • Q∈Rn×dk\mathbf{Q} \in \mathbb{R}^{n \times d_k}: the query matrix (row ii is the query for position ii)
  • KT∈Rdk×n\mathbf{K}^T \in \mathbb{R}^{d_k \times n}: the transposed key matrix (column jj is the key for position jj)

Entry SijS_{ij} is the dot product of row ii of Q\mathbf{Q} with column jj of KT\mathbf{K}^T, which equals qi⋅kj\mathbf{q}_i \cdot \mathbf{k}_j. This single matrix multiplication gives us all n2n^2 pairwise scores in one highly optimized operation, a key reason why attention scales well on modern hardware.

In[5]:
Code
def compute_attention_scores(Q, K):
    """
    Compute raw attention scores from queries and keys.

    Args:
        Q: Query matrix of shape (seq_len, d_k)
        K: Key matrix of shape (seq_len, d_k)

    Returns:
        Scores matrix of shape (seq_len, seq_len)
    """
    # Matrix multiplication: Q @ K.T gives all pairwise dot products
    scores = Q @ K.T
    return scores

The power of learned projections. This is where QKV attention differs fundamentally from raw embedding similarity. In the previous chapter, tokens matched based on how similar their embeddings were, a fixed computation that couldn't adapt to context. Now they match based on how well their learned projections align.

Consider the implications: a verb's query can learn to match the key of a noun playing the subject role, even if "run" and "dog" have very different embeddings. The word "it" can learn a query that matches noun phrase keys, even though pronouns and nouns occupy different regions of embedding space. The projections transform the matching problem from "what words are similar?" to "what words are relevant?", and relevance is learned from data.

Step 3: The Scaling Problem and Its Solution

We have scores, but there's a subtle problem lurking in the mathematics. As the dimension dkd_k grows, dot products tend to become larger in magnitude, not because the vectors are more aligned, but simply because we're summing more terms. This creates numerical instability that can derail training. To understand why and how we fix it, let's work through the statistics step by step.

Setting up the problem. Assume each component of q\mathbf{q} and k\mathbf{k} has zero mean and unit variance (a reasonable assumption for normalized embeddings). We want to understand how the variance of the dot product depends on dimension.

Step 1: Write the dot product as a sum. The dot product is:

q⋅k=∑m=1dkqm⋅km\mathbf{q} \cdot \mathbf{k} = \sum_{m=1}^{d_k} q_m \cdot k_m

where:

  • qmq_m: the mm-th component of the query vector q\mathbf{q}
  • kmk_m: the mm-th component of the key vector k\mathbf{k}
  • dkd_k: the dimension of both vectors

Step 2: Compute variance of each term. Each product qm⋅kmq_m \cdot k_m is the product of two independent random variables with zero mean and unit variance. For independent random variables XX and YY with E[X]=E[Y]=0\mathbb{E}[X] = \mathbb{E}[Y] = 0 and Var(X)=Var(Y)=1\text{Var}(X) = \text{Var}(Y) = 1:

Var(X⋅Y)=E[X2]⋅E[Y2]=1⋅1=1\text{Var}(X \cdot Y) = \mathbb{E}[X^2] \cdot \mathbb{E}[Y^2] = 1 \cdot 1 = 1

So each term qm⋅kmq_m \cdot k_m has variance 1.

Step 3: Sum the variances. For independent random variables, the variance of a sum equals the sum of variances:

Var(∑m=1dkqm⋅km)=∑m=1dkVar(qm⋅km)=∑m=1dk1=dk\text{Var}\left(\sum_{m=1}^{d_k} q_m \cdot k_m\right) = \sum_{m=1}^{d_k} \text{Var}(q_m \cdot k_m) = \sum_{m=1}^{d_k} 1 = d_k

Conclusion. The dot product has variance dkd_k, so its standard deviation is dk\sqrt{d_k}. At dk=64d_k = 64 (common in practice), scores have standard deviation 8. At dk=512d_k = 512, it's about 22.6. This growth in magnitude isn't a feature; it's a bug that we need to fix.

In[6]:
Code
# Demonstrate how dot product variance grows with dimension
def measure_dot_product_variance(d_k, n_samples=10000):
    """Compute empirical variance of dot products for random unit-variance vectors."""
    q = np.random.randn(n_samples, d_k)
    k = np.random.randn(n_samples, d_k)
    dot_products = np.sum(q * k, axis=1)
    return dot_products.var()


dimensions = [4, 8, 16, 32, 64, 128, 256, 512]
empirical_variances = [measure_dot_product_variance(d) for d in dimensions]
theoretical_variances = dimensions  # Variance equals d_k
Out[7]:
Visualization
Line plot showing empirical and theoretical dot product variance increasing linearly with dimension from 4 to 512.
Dot product variance grows linearly with dimension. For random vectors with unit-variance components, the variance of their dot product equals the dimension. This means larger dimensions produce larger score magnitudes, which can saturate softmax.

The plot confirms our theoretical prediction: variance grows linearly with dimension. The empirical measurements (red squares) lie almost perfectly on the theoretical line (blue circles), validating our statistical analysis.

Why does this matter? The problem becomes clear when we consider what happens next: we apply softmax to convert scores into probability-like attention weights.

The softmax function converts a vector of real numbers into a probability distribution. For a vector of scores s=[s1,s2,…,sn]\mathbf{s} = [s_1, s_2, \ldots, s_n], softmax computes:

softmax(si)=esi∑j=1nesj\text{softmax}(s_i) = \frac{e^{s_i}}{\sum_{j=1}^{n} e^{s_j}}

where:

  • sis_i: the ii-th score (the raw attention score for position ii)
  • esie^{s_i}: the exponential of sis_i. This keeps all values become positive
  • ∑j=1nesj\sum_{j=1}^{n} e^{s_j}: the sum of exponentials across all nn positions, serving as a normalizing constant
  • nn: the number of positions (sequence length)

The exponential function amplifies differences between inputs: larger scores get exponentially larger outputs. When one score is much larger than others, softmax assigns nearly all probability mass to that element. The attention becomes "hard," focusing on essentially one position while ignoring all others.

More critically, this creates gradient problems. In the softmax function, elements with very low probability receive vanishingly small gradients. If attention is sharply focused due to large score magnitudes, the model struggles to learn that other positions might also be relevant. Training becomes slow and unstable.

In[8]:
Code
# Demonstrate softmax saturation with different score magnitudes
def softmax(x):
    exp_x = np.exp(x - x.max())
    return exp_x / exp_x.sum()


# Same relative differences, different scales
scores_small = np.array([1.0, 0.8, 0.5, 0.3])
scores_medium = scores_small * 5
scores_large = scores_small * 20

weights_small = softmax(scores_small)
weights_medium = softmax(scores_medium)
weights_large = softmax(scores_large)
Out[9]:
Visualization
Bar chart showing relatively uniform softmax weights across four positions.
Small scores (scale=1): Softmax produces a soft distribution where all positions receive meaningful weight. Gradients flow to all elements.
Bar chart showing softmax weights increasingly concentrated on the maximum position.
Medium scores (scale=5): Distribution sharpens. The maximum element dominates, while others receive less weight.
Bar chart showing nearly all softmax weight on a single position.
Large scores (scale=20): Near-hard attention. Almost all weight goes to one element. Gradients for other elements nearly vanish.

The visualization makes the problem viscerally clear. All three plots use scores with the same relative differences, where position 1's score is always 25% higher than position 2's, and so on. But scaling up the magnitudes completely changes the output distribution. In the rightmost plot, position 1 receives 99.9% of the weight, and the model can barely learn to attend elsewhere.

The elegant solution. Since the problem is that score variance grows with dkd_k, we fix it by dividing scores by dk\sqrt{d_k}. This simple scaling operation normalizes the variance back to approximately 1, regardless of dimension:

  • Original variance: dkd_k
  • After scaling by dk\sqrt{d_k}: variance becomes dk/dk=1d_k / d_k = 1

The result is the complete scaled dot-product attention formula:

Attention(Q,K,V)=softmax(QKTdk)V\text{Attention}(\mathbf{Q}, \mathbf{K}, \mathbf{V}) = \text{softmax}\left(\frac{\mathbf{Q}\mathbf{K}^T}{\sqrt{d_k}}\right)\mathbf{V}

where:

  • Q∈Rn×dk\mathbf{Q} \in \mathbb{R}^{n \times d_k}: the query matrix, with row ii containing the query vector for position ii
  • K∈Rn×dk\mathbf{K} \in \mathbb{R}^{n \times d_k}: the key matrix, with row jj containing the key vector for position jj
  • V∈Rn×dv\mathbf{V} \in \mathbb{R}^{n \times d_v}: the value matrix, with row jj containing the value vector for position jj
  • QKT∈Rn×n\mathbf{Q}\mathbf{K}^T \in \mathbb{R}^{n \times n}: the raw score matrix, where entry (i,j)(i, j) is the dot product qi⋅kj\mathbf{q}_i \cdot \mathbf{k}_j
  • dk\sqrt{d_k}: the scaling factor that stabilizes score magnitudes
  • softmax(⋅)\text{softmax}(\cdot): applied row-wise, converts each row of scaled scores into a probability distribution (attention weights)
  • nn: the sequence length (number of tokens)
  • dkd_k: the query/key dimension
  • dvd_v: the value dimension (determines the output dimension)

The formula works in three stages. First, QKT\mathbf{Q}\mathbf{K}^T computes all pairwise query-key dot products. Second, dividing by dk\sqrt{d_k} and applying softmax converts these scores into attention weights. Third, multiplying by V\mathbf{V} aggregates value vectors according to these weights, producing context-enriched output representations.

After scaling, scores have controlled variance regardless of dkd_k, and softmax operates in a regime where gradients flow to all positions.

In[10]:
Code
# Compare attention weights with and without scaling
def compute_attention_comparison(d_k, n_positions=8):
    """Compare attention distributions with and without scaling."""
    np.random.seed(42)
    Q = np.random.randn(1, d_k)
    K = np.random.randn(n_positions, d_k)

    # Unscaled scores
    unscaled = (Q @ K.T).flatten()

    # Scaled scores
    scaled = unscaled / np.sqrt(d_k)

    # Softmax
    def softmax(x):
        exp_x = np.exp(x - x.max())
        return exp_x / exp_x.sum()

    return unscaled, scaled, softmax(unscaled), softmax(scaled)


# High dimension where scaling matters most
d_k_demo = 64
unscaled_scores, scaled_scores, unscaled_weights, scaled_weights = (
    compute_attention_comparison(d_k_demo)
)
Out[11]:
Visualization
Bar chart showing highly concentrated attention weights without scaling.
Without scaling: The large score variance pushes softmax toward hard attention. Position 3 receives 91% of the weight, leaving little gradient for learning to attend elsewhere.
Bar chart showing more distributed attention weights with scaling.
With scaling: Dividing by sqrt(64)=8 compresses the score range. The attention distribution is softer, allowing gradients to flow to multiple positions during training.

The side-by-side comparison demonstrates scaling's importance. Without it (left), attention collapses to a near-hard distribution where one position dominates. With scaling (right), the model retains flexibility to attend broadly. Gradients then flow to all positions during training, allowing the model to learn attention patterns that spread weight across multiple positions.

In[12]:
Code
def scaled_dot_product_attention(Q, K, V):
    """
    Compute scaled dot-product attention.

    Args:
        Q: Query matrix of shape (seq_len, d_k)
        K: Key matrix of shape (seq_len, d_k)
        V: Value matrix of shape (seq_len, d_v)

    Returns:
        output: Attention output of shape (seq_len, d_v)
        attention_weights: Attention weights of shape (seq_len, seq_len)
    """
    d_k = Q.shape[-1]

    # Compute scaled scores
    scores = Q @ K.T / np.sqrt(d_k)

    # Apply softmax (row-wise)
    # Subtract max for numerical stability (doesn't change the result)
    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)

    # Aggregate values
    output = attention_weights @ V

    return output, attention_weights

Let's verify that the dimensions work out correctly by tracing through a concrete example:

In[13]:
Code
# Example dimensions
seq_len = 4  # 4 tokens in the sequence
embed_dim = 8  # each token has 8-dimensional embedding
d_k = 6  # query and key dimension
d_v = 6  # value dimension

# Initialize random embeddings and projection matrices
np.random.seed(42)
X = np.random.randn(seq_len, embed_dim)
W_q = np.random.randn(embed_dim, d_k) * 0.1
W_k = np.random.randn(embed_dim, d_k) * 0.1
W_v = np.random.randn(embed_dim, d_v) * 0.1

# Project to Q, K, V
Q, K, V = project_qkv(X, W_q, W_k, W_v)

# Compute attention
output, attention_weights = scaled_dot_product_attention(Q, K, V)
Out[14]:
Console
Shape analysis:
  Input X:            (4, 8)  (seq_len × embed_dim)
  Query projection:   (8, 6)  (embed_dim × d_k)
  Key projection:     (8, 6)  (embed_dim × d_k)
  Value projection:   (8, 6)  (embed_dim × d_v)

  Queries Q:          (4, 6)  (seq_len × d_k)
  Keys K:             (4, 6)  (seq_len × d_k)
  Values V:           (4, 6)  (seq_len × d_v)

  Attention weights:  (4, 4)  (seq_len × seq_len)
  Output:             (4, 6)  (seq_len × d_v)

The shapes confirm our understanding: input embeddings flow through projections to create QKV. The attention weight matrix is always n×nn \times n because we compute all pairwise interactions. The output has the same sequence length as the input but takes on the value dimension dvd_v. In practice, dvd_v is often chosen to equal dkd_k and the original embedding dimension, enabling residual connections.

Step 4: Aggregating Values

We've now computed attention weights, a probability distribution over positions for each query. But weights alone don't produce useful outputs. The final step uses these weights to aggregate value vectors, producing new representations enriched with contextual information.

This is where the separation of keys and values proves its worth. Keys determined which positions receive attention (through query-key matching). Values determine what content flows. A word might attract attention because of its syntactic role (captured in its key) while contributing semantic information (carried in its value).

For position ii, the output is a weighted sum of all value vectors:

oi=∑j=1nαijvj\mathbf{o}_i = \sum_{j=1}^{n} \alpha_{ij} \mathbf{v}_j

where:

  • oi∈Rdv\mathbf{o}_i \in \mathbb{R}^{d_v}: the output vector for position ii, now enriched with contextual information
  • αij∈[0,1]\alpha_{ij} \in [0, 1]: the attention weight from position ii to position jj, indicating how much position ii attends to position jj
  • vj∈Rdv\mathbf{v}_j \in \mathbb{R}^{d_v}: the value vector at position jj, carrying the content that can be transferred
  • nn: the sequence length (total number of positions)
  • dvd_v: the value dimension

The attention weights αij\alpha_{ij} come from applying softmax to the scaled scores, so they satisfy ∑j=1nαij=1\sum_{j=1}^{n} \alpha_{ij} = 1 for each position ii. This constraint ensures the output is a proper weighted average.

Geometrically, this weighted average has an elegant interpretation. Position ii's output is a blend of all value vectors, with weights determined by query-key compatibility. Positions with keys that matched ii's query (high αij\alpha_{ij}) contribute more; positions with mismatched keys (low αij\alpha_{ij}) contribute less. Because the weights sum to 1, the output oi\mathbf{o}_i lies within the convex hull of the value vectors, a point "inside" the space spanned by all possible values.

The information flow paradigm. Self-attention can be understood as a message-passing system. Each position broadcasts its value vector, and each position receives a custom blend of all broadcasts, with the blend weights determined by query-key compatibility. Unlike recurrent networks that pass information sequentially, or convolutional networks that only see local neighborhoods, attention allows any position to directly receive information from any other position in a single step.

This direct, content-addressable communication is what makes transformers so effective. A pronoun can directly access its antecedent, regardless of distance. A verb can simultaneously gather information from its subject plus object and modifiers. The network doesn't need to learn complex routing through intermediate positions; it learns which positions are relevant and attends to them directly.

Let's visualize the complete attention flow for a simple sentence:

In[15]:
Code
# Create example with interpretable structure
words = ["The", "quick", "fox", "jumps"]
n_words = len(words)

# Create embeddings that capture some semantic structure
# (In practice these would come from an embedding layer)
np.random.seed(123)
word_embeddings = np.random.randn(n_words, embed_dim)

# Initialize projection matrices
W_q = np.random.randn(embed_dim, d_k) * 0.2
W_k = np.random.randn(embed_dim, d_k) * 0.2
W_v = np.random.randn(embed_dim, d_v) * 0.2

# Compute QKV attention
Q, K, V = project_qkv(word_embeddings, W_q, W_k, W_v)
output, attn_weights = scaled_dot_product_attention(Q, K, V)
Out[16]:
Visualization
Heatmap of attention weights for four words showing query-key matching patterns.
Attention weight heatmap showing query-key matching for a four-word sequence. Each row shows how one word's query matches against all words' keys. These weights determine how much each word's value contributes to the output at each position.

The heatmap shows how each word's query matches against all keys. Each row sums to 1.0 because softmax normalizes the weights. In this random initialization, the patterns aren't meaningful, but after training, we would see linguistically interpretable patterns: articles attending to their nouns, verbs attending to subjects and objects, and so on.