GenAI Course · Module 5 · Session 18

Scaled Dot-Product & Multi-Head Attention

⏱ 12 min read🎯 Intermediate🖼 3 diagrams🧪 2 live demos🐍 Python code

In Session 17 we saw what attention computes. Now let us write the exact formula used inside every transformer, understand the small but very important “÷ √dₖ” in it, and see why transformers use not one but many attention heads at the same time.

In this session

  • The scaled dot-product attention formula, piece by piece
  • Why we divide by √dₖ, with a slider you can play with
  • Masks: hiding padding and hiding the future
  • Multi-head attention: why a panel of heads beats one head
  • Code in NumPy and PyTorch

01The formula

This one line is the heart of the transformer:

Attention(Q, K, V) = softmax( Q·Kᵀ / √dₖ ) · V

Here Q, K and V are matrices: one row per token. If the sentence has n tokens and each key has dₖ numbers, then Q·Kᵀ is an n × n table of scores: every token compared with every token, all in one matrix multiplication.

Q K V MatMulQ·Kᵀ (n × n) Scale÷ √dₖ Maskoptional Softmaxrow by row MatMulweights · V Output (n × dᵥ)
Five small steps. The only new piece compared with Session 17 is Scale (and the optional Mask).

02Why divide by √dₖ?

A dot product adds up dₖ multiplications. The more numbers you add, the bigger the result can become. If query and key values are roughly random with spread 1, the spread of their dot product grows like √dₖ:

dₖ (key size)Typical spread of q·k√dₖ
42.02.0
648.18.0
51222.622.6

Big scores are bad for softmax. When one score is much bigger than the others, softmax gives it almost all the weight (close to 1.0) and the rest get almost 0. The model stops mixing information, and the gradients become tiny, so learning slows down. Dividing by √dₖ brings the scores back to a spread of about 1.

📝

Like converting marks to percentages. If one paper is marked out of 500 and another out of 10, the first one will always look “bigger”. Bringing everything to the same scale makes the comparison fair.

Demo · Softmax with and without scaling

03Masks: what a token is not allowed to see

The optional mask step sets some scores to minus infinity before softmax, so their weight becomes exactly 0. Two common masks:

Causal mask for “my card is blocked”. Each row (a query) can only look at its own column and the ones to the left.

04Multi-head attention

One attention head produces one set of weights, so it can focus on one kind of relationship at a time. But language has many relationships at once: grammar, meaning, which noun a pronoun refers to, nearby words. So transformers run several attention heads in parallel, each with its own W_Q, W_K and W_V.

👩‍🏫

Like a panel of examiners. One checks grammar, one checks meaning, one checks handwriting. Each gives a separate view, and the final result combines all of them.

Input (n × 512) Concat 8 × 64 = 512 × W_O → Output (n × 512)
The original transformer: d_model = 512 split into 8 heads of 64. Each head attends on its own, then results are joined and mixed by W_O.

Here is what different heads typically learn. Switch between heads:

Demo · Three heads, three jobs

Does it cost more?

Not really. Each head works on a smaller slice (64 instead of 512 numbers), so 8 heads together cost about the same as one big head. You get many views for the price of one.

Modeld_modelHeadsSize per head (dₖ)
Original Transformer512864
BERT-Base7681264
BERT-Large1,0241664

05Code

Scaled attention in NumPy

Same code as Session 17, with one change: / np.sqrt(d_k).

scaled_attention.py
import numpy as np
np.random.seed(42)

X = np.random.randn(4, 8)
Wq, Wk, Wv = [np.random.randn(8, 4) for _ in range(3)]
Q, K, V = X @ Wq, X @ Wk, X @ Wv
d_k = K.shape[1]

scores = Q @ K.T / np.sqrt(d_k)               # the only change
weights = np.exp(scores - scores.max(axis=1, keepdims=True))
weights /= weights.sum(axis=1, keepdims=True)
print(weights.round(2))
[[0.11 0. 0.46 0.43] [0.02 0.62 0. 0.36] [0. 0.15 0.85 0. ] [0.99 0. 0. 0.01]]

Compare with Session 17: the 0.97 row became 0.85 and the 0.75 row became 0.62. The weights are softer, so more words contribute.

Multi-head attention in PyTorch

multihead.py
import torch
import torch.nn as nn

mha = nn.MultiheadAttention(embed_dim=512, num_heads=8, batch_first=True)
x = torch.randn(1, 4, 512)                    # 1 sentence, 4 tokens, 512 numbers each

out, weights = mha(x, x, x)                   # self-attention: Q, K and V all come from x
print(out.shape)
print(weights.shape)                          # averaged over the 8 heads by default
torch.Size([1, 4, 512]) torch.Size([1, 4, 4])

06Summary

07Quick quiz

1. Why do we divide the scores by √dₖ?

2. For a sentence of n tokens, what is the shape of Q·Kᵀ?

3. Which mask stops a token from looking at future tokens?

4. Main benefit of multiple heads?

5. With d_model = 512 and 8 heads, how big is each head?

08References

Created by Ajeetkumar