Building the attention weights
Part 1 ended with a weighted average and no way to compute the weights. This part derives them — additive scoring first, because that is what came first, then the dot product and the scale factor that makes it usable.
We need : how much output step should read from input position . Three requirements, all forced by what it is for.
- It must depend on both sides — on what is looking for and on what contains. A weight that ignores either is not doing selection.
- The weights over must be non-negative and sum to one, or “weighted average” is a lie and the scale of drifts with sequence length.
- It must be differentiable, or none of this trains.
The standard route is: compute an unnormalised score , then normalise.
So the real question is .
Attempt one: learn the comparison
Bahdanau’s answer is to let a small network decide. Concatenate the decoder state and the encoder state, push them through a tanh layer, project to a scalar.
This works, and it has a real advantage: and need not have the same dimension, because and map them into a shared space independently.
It also has a cost that took me a while to see as decisive. For each of the pairs, eq. 2 requires a tanh over a -vector and a dot with . Those cannot be folded into a single matrix multiplication, because the nonlinearity sits in the middle. You get small operations rather than one big one — which on a GPU is close to the worst possible shape for a computation.
Attempt two: just use the inner product
Drop the learned comparator. Two vectors in the same space already have a similarity function.
Every score for every pair is now a single matrix product, . This is the change that mattered — not because dot products are more expressive than a learned comparator (they are less), but because the whole score matrix becomes one GEMM.
The expressivity is bought back by projecting first. and are learned, so the model still chooses which subspace similarity is measured in; it just does not get to choose the similarity function itself.
The scale factor is not cosmetic
Here is the part I originally skipped, and skipping it is why I could not reproduce a working implementation from memory.
Suppose and have independent components with mean 0 and variance 1. Then
so the logits have standard deviation . At that is 8, and a gap of a few standard deviations between the largest logit and the rest sends softmax to a one-hot vector.
That is bad not because the attention is sharp but because of what it does to the gradient. The softmax Jacobian is ; as approaches one-hot, it approaches zero, and no gradient reaches or at all. The layer stops learning while looking perfectly healthy in the forward pass.
Dividing by restores unit variance and the problem goes away.
Checking it myself
I did not believe the saturation argument until I plotted it, and it is a five-line experiment worth doing rather than reading.
import torch
for d in (8, 64, 512):
q = torch.randn(4096, d)
k = torch.randn(4096, d)
logits = q @ k.T
for name, s in (("unscaled", logits), ("scaled", logits / d**0.5)):
a = s.softmax(dim=-1)
# Entropy in nats: log(4096) = 8.32 is uniform, 0 is one-hot.
entropy = -(a * a.clamp_min(1e-12).log()).sum(-1).mean()
print(f"d={d:4d} {name:9s} std={s.std():6.2f} entropy={entropy:5.2f}")At the unscaled entropy collapses towards zero while the scaled version stays near . The scaled row is not sharp yet — sharpness is something the model learns by growing the magnitude of and during training. The point of the scale factor is that it starts diffuse and lets training decide, rather than starting saturated with no gradient to escape with.
Next
Part 3 takes the finished operation and looks at what the resulting matrix actually contains — and at how much of what people read off attention maps is really there.
Series
Reference
The settled statements of what this note works through.
References
·