Tutorials › Understanding Transformers › Self-Attention, Step by Step

Understanding Transformers · Part 7 of 16

Self-Attention, Step by Step

The central mechanism of the transformer, built up one operation at a time and run on a worked example.

Three roles every token plays

Each token's vector is projected through three learned matrices to produce three new vectors, each with a job:

These names describe how the projections behave. The roles emerge from training, which looks for projections that make prediction work.

Q = X WQ     K = X WK     V = X WV

The five steps

  1. Score. Each Query takes a dot product with every Key. For n tokens this gives an n×n score matrix; row i holds token i's scores against every token.
    scores = Q KT
  2. Scale. With many dimensions, dot products grow large because they sum more terms: if the entries are independent with zero mean and unit variance, the dot product's variance is dk. Large scores push softmax toward all-or-nothing, which starves the gradients (Vaswani et al., 2017). Dividing by √dk keeps the scores in a workable range.
    scaled = Q KT / √dk
  3. Mask (when needed). A decoder-only model applies a causal mask: future positions are set to −∞ before softmax so they get zero weight. Token i may use tokens 1..i, never i+1 onward.
  4. Normalize. Softmax turns each row of scores into positive weights that sum to one, an attention budget the token spends across the others.
    softmax(zi) = exp(zi) / Σj exp(zj)
  5. Mix. Multiply each Value by its weight and add. The result is a weighted blend of information from every allowed position.
    Attention(Q,K,V) = softmax(Q KT / √dk) V

A worked example: “Mouse eats cheese”

Three tokens, two features each, so we can follow every number. Embeddings:

Mouse = [2, 0]    Eats = [1, 2]    Cheese = [2, 0]
X = [[2,0], [1,2], [2,0]]

Choose a Query projection that swaps the two features, and use identity matrices for Keys and Values so we can watch the attention mechanism alone:

WQ = [[0,1],[1,0]]  →  Q = [[0,2], [2,1], [0,2]]
WK = WV = I  →  K = V = [[2,0], [1,2], [2,0]]

Step 1 — scores (QKT)

Every Query dotted with every Key gives a 3×3 matrix. Rows are “who is asking,” columns are “who is being asked about.”

· Mouse· Eats· Cheese
Mouse asks040
Eats asks444
Cheese asks040

Step 2 — scale by √dk = √2 ≈ 1.414

Every score of 4 becomes about 2.83; the zeros stay zero.

Steps 3–4 — softmax each row into weights

(No mask here, so every token may attend to all three.)

on Mouseon Eatson Cheese
Mouse0.0530.8940.053
Eats0.3330.3330.333
Cheese0.0530.8940.053

Step 5 — mix the Values

Each row of weights blends the Value vectors [2,0], [1,2], [2,0]:

tokennew representation
Mouse[1.11, 1.79]
Eats[1.67, 0.67]
Cheese[1.11, 1.79]

Reading the result: Mouse and Cheese start from the same embedding, so they ask the same question and both spend most of their attention budget on “Eats,” the one token whose Key aligns with their Query. Their new vectors move toward Eats' information. “Eats,” whose Query matches all three Keys equally, takes an even blend of the whole sentence. Every token runs this process at once.

import numpy as np

# Mouse, Eats, Cheese
X  = np.array([[2, 0], [1, 2], [2, 0]])
Wq = np.array([[0, 1], [1, 0]])
Wk = Wv = np.eye(2)

# Q=[[0,2],[2,1],[0,2]]  K=V=X
Q, K, V = X @ Wq, X @ Wk, X @ Wv

scores = Q @ K.T                    # step 1: score
scaled = scores / np.sqrt(2)        # step 2: scale by sqrt(d_k)

# step 4: normalize
weights = np.exp(scaled) / np.exp(scaled).sum(axis=1, keepdims=True)

# step 5: mix -- matches the table above
output = weights @ V

Keys and Values are different jobs

The example above used identity matrices for WK and WV to isolate the mechanism. In a trained model they are different learned projections, because they solve different problems: Keys are optimized for matching, Values for what gets passed along once a match is made. A second small example makes the difference visible. Embeddings:

Chef = [2, 0]    Cuts = [1, 2]    Bread = [2, 1]
WK = [[1,0],[-0.5,1]]    WV = [[1,1],[0.5,1]]

Applying each projection to the same three vectors:

Keys:  Chef = [2, 0]    Cuts = [0, 2]    Bread = [1.5, 1]
Values:  Chef = [2, 2]    Cuts = [2, 3]    Bread = [2.5, 3]

The same starting vector now has one set of coordinates for being found and a different set for what it delivers. A Key answers “should you attend to me?”; a Value answers “now that you have, what do you take away?”

# Chef, Cuts, Bread
X  = np.array([[2, 0], [1, 2], [2, 1]])
Wk = np.array([[1, 0], [-0.5, 1]])
Wv = np.array([[1, 1], [0.5, 1]])

K = X @ Wk   # Chef=[2,0]  Cuts=[0,2]  Bread=[1.5,1]
V = X @ Wv   # Chef=[2,2]  Cuts=[2,3]  Bread=[2.5,3]
Watch the shapes. A 3×3 attention matrix times a 3×2 Value matrix gives a 3×2 output: three tokens, each with an updated two-feature description. Tracking shapes is the quickest way to keep any attention calculation straight.