Query, Key, Value › Putting It All Together

Michael BrenndoerferUpdated June 4, 20252 min read
Language AI HandbookAll chapters
Part 13 of 67Self-Attention
Chapter 91Query, Key, Value › Putting It All Together
Reading section 6 of 11 · Putting It All Together · 2 minSection 6 of 11 · 2 min

Book contents

Section 6 of 11 · 2 min read

Query, Key, Value › Putting It All TogetherOne section · Sections in this chapter6 / 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

We've now built up all the components of QKV attention: projections that create specialized representations, dot products that measure compatibility, scaling that ensures stable gradients, and value aggregation that transmits information. Let's combine these into a complete self-attention layer:

In[19]:
Code
class SelfAttention:
    """
    Complete self-attention layer with QKV projections.
    """

    def __init__(self, embed_dim, d_k, d_v, seed=None):
        """
        Initialize projection matrices.

        Args:
            embed_dim: Dimension of input embeddings
            d_k: Dimension of queries and keys
            d_v: Dimension of values
            seed: Random seed for reproducibility
        """
        if seed is not None:
            np.random.seed(seed)

        # Xavier/Glorot initialization for stable gradients
        scale_qk = np.sqrt(2.0 / (embed_dim + d_k))
        scale_v = np.sqrt(2.0 / (embed_dim + d_v))

        self.W_q = np.random.randn(embed_dim, d_k) * scale_qk
        self.W_k = np.random.randn(embed_dim, d_k) * scale_qk
        self.W_v = np.random.randn(embed_dim, d_v) * scale_v
        self.d_k = d_k

    def forward(self, X):
        """
        Compute self-attention output.

        Args:
            X: Input embeddings of shape (seq_len, embed_dim)

        Returns:
            output: Attention output of shape (seq_len, d_v)
            attention_weights: Weights of shape (seq_len, seq_len)
        """
        # Project to Q, K, V
        Q = X @ self.W_q
        K = X @ self.W_k
        V = X @ self.W_v

        # Scaled dot-product attention
        scores = Q @ K.T / np.sqrt(self.d_k)

        # Softmax
        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
In[20]:
Code
# Test the complete layer
embed_dim = 8
d_k = d_v = 6
seq_len = 5

np.random.seed(42)
X = np.random.randn(seq_len, embed_dim)

# Create and apply self-attention layer
attention = SelfAttention(embed_dim, d_k, d_v, seed=123)
output, weights = attention.forward(X)
Out[21]:
Console
Self-Attention Layer Test
========================================
Input shape:            (5, 8)
Output shape:           (5, 6)
Attention weights shape: (5, 5)

Attention weight matrix (rows sum to 1):
[[0.068 0.446 0.094 0.171 0.221]
 [0.012 0.476 0.085 0.148 0.28 ]
 [0.046 0.24  0.251 0.096 0.367]
 [0.177 0.324 0.158 0.208 0.132]
 [0.457 0.169 0.077 0.125 0.172]]

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

The attention weights matrix reveals the core of what self-attention computes: a soft routing table that determines how information flows between positions. Each row sums to exactly 1.0 (confirming proper softmax normalization), and each entry indicates how much one position attends to another.

With random initialization, these patterns are meaningless, just noise from the random projection matrices. But after training on language data, we would see interpretable patterns emerge: determiners attending to nouns, verbs gathering information from subjects and objects, pronouns reaching back to their antecedents.