Multi-Head Attention with Einsum - Einstein Summation (Einsum)
Implement Multi-Head Attention with Einsum in Python with NumPy. hard exercise with examples, hints, and instant feedback. No setup or signup required.
Multi-Head Attention with Einsum
What you'll build
A function multi_head_attention_einsum(Q, K, V, num_heads) that splits d_model into heads, runs scaled dot-product attention in every head at once with einsum, and merges the heads back.
Background
Multi-head attention lets each head attend to different positions using its own slice of the features. The usual implementation reshapes to (batch, seq, heads, d_k) and then transposes to (batch, heads, seq, d_k) before two batched matmuls. With einsum you can skip the transposes: put the head letter h where it already is and let the subscripts reorder axes for you. This problem covers only the attention core; the learned projections W_Q, W_K, W_V, W_O of a full layer are left out (see Multi-Head Attention in the Transformers section).
Algorithm
Let H = num_heads and d_k = d_model / H.
- Split heads: reshape
Qto(batch, seq_q, H, d_k)andK,Vto(batch, seq_k, H, d_k). Headhowns feature channelsh*d_kto(h+1)*d_k - 1. - Scores per head:
scores[b, h, q, k] = sum_d Q[b, q, h, d] * K[b, k, h, d] / sqrt(d_k) einsum 'bqhd,bkhd->bhqk' - Softmax over the key axis (last axis) of
scores, givingAof shape(batch, H, seq_q, seq_k). - Weighted values:
out[b, q, h, d] = sum_k A[b, h, q, k] * V[b, k, h, d] einsum 'bhqk,bkhd->bqhd' - Merge heads: reshape
outto(batch, seq_q, d_model).
Inputs
Q: queries, float array of shape(batch, seq_q, d_model)K: keys, float array of shape(batch, seq_k, d_model)V: values, float array of shape(batch, seq_k, d_model)num_heads: positive integer that dividesd_model
Expected Return Format
A float array of shape (batch, seq_q, d_model). Output channels h*d_k .. (h+1)*d_k - 1 come from head h.
Edge cases and conventions
- Scale by
sqrt(d_k)(the per-head size), notsqrt(d_model). - Use a numerically stable softmax (subtract the row max before
exp). - With
num_heads = 1the result is plain single-head attention.
Constraints
Use reshape and np.einsum; no explicit transpose, no loops over heads.
Follow-up
- Count the FLOPs: does splitting into
Hheads change the total cost compared with one head of sized_model? - How would you add a causal mask, and in which step?
Examples
Input: X = np.array([[[1., 0.], [0., 1.]]]) # (batch=1, seq=2, d_model=2)
np.round(multi_head_attention_einsum(X, X, X, 2), 4)
Output: array([[[0.7311, 0.5 ],
[0.5 , 0.7311]]])Head 0 sees channel 0: query 0 scores the keys [1, 0], softmax [0.731, 0.269], output 0.731. Head 1 sees channel 1: query 0 scores [0, 0], uniform weights, output 0.5.
Input: np.round(multi_head_attention_einsum(X, X, X, 1), 4)
Output: array([[[0.6698, 0.3302],
[0.3302, 0.6698]]])With one head every channel shares the same weights: softmax([1, 0] / sqrt(2)) = [0.670, 0.330].
Practice in Python
import numpy as np
def multi_head_attention_einsum(Q: np.ndarray, K: np.ndarray, V: np.ndarray,
num_heads: int) -> np.ndarray:
"""
Multi-head scaled dot-product attention using einsum (no projections).
Args:
Q: Queries (batch, seq_q, d_model)
K: Keys (batch, seq_k, d_model)
V: Values (batch, seq_k, d_model)
num_heads: Number of heads; divides d_model
Returns:
output: (batch, seq_q, d_model); channels h*d_k:(h+1)*d_k come from head h
"""
batch, seq_q, d_model = Q.shape
seq_k = K.shape[1]
d_k = d_model // num_heads
# Your code here
pass
The interactive editor loads with Python and NumPy. Run your implementation against 7 tests, or use the progressive hints.