Keras ships MultiHeadAttention, so writing your own is never about production — it's about understanding. Attention has a reputation for being mystical, but the mechanism is a handful of matrix multiplies. Building it once, by hand, with nothing but keras.ops turns the equation in every Transformer paper into code you can read at a glance.
The three pieces of a custom layer
Subclassing keras.layers.Layer means filling in three methods. __init__ stores config (here, the projection width units). build(input_shape) creates the weights once the input shape is known — three projection matrices for query, key, and value, via add_weight. call(inputs) runs the forward pass. Splitting __init__ from build is what lets Keras infer weight shapes lazily from whatever feeds the layer.
Scaled dot-product attention, line by line
The call body is the famous formula. Project the input into Q, K, V. Score every query against every key with matmul(q, transpose(k)). Divide by sqrt(units) — the scaling that keeps the softmax from saturating as dimension grows. Softmax the scores into attention weights, then use those weights to take a weighted sum of the values. Because every op is keras.ops, the same layer runs unchanged on TensorFlow, PyTorch, and JAX.
Code
SimpleAttention — scaled dot-product attention from scratch·python
Extend SimpleAttention into multi-head self-attention as a Subclassed Layer using only keras.ops: project to Q/K/V, reshape into num_heads heads, run scaled dot-product attention per head, then concatenate and apply a final output projection. Verify it produces the same output (within float tolerance) as keras.layers.MultiHeadAttention when you copy the same weights into both.
Hint
The reshape from (batch, seq, units) to (batch, num_heads, seq, head_dim) — and the transpose back after attention — is where most bugs live. Print shapes at every step and check against MultiHeadAttention's output one head at a time.
Progress
Progress is local-only — sign in to sync across devices.