Attention Before Transformers (Additive and Bahdanau)
Attention did not arrive with the transformer. It arrived four years earlier as a patch to a specific, measurable failure in recurrent encoder–decoders, and understanding that failure is what makes the transformer’s design look inevitable rather than arbitrary.
This page builds the patch: additive attention, the mechanism from Bahdanau et al. (2014), on a task where the correct alignment is known in advance so the attention weights can be checked rather than admired.
What you’ll learn
Section titled “What you’ll learn”- Why a fixed-size encoder state is a bottleneck, and what specifically it drops.
- Additive attention derived and implemented — scores, softmax, context vector.
- The measured gap: 0.0017 exact matches against 1.0000 on the same task.
- Why alignment maps are evidence only when you already know the right alignment.
- What the transformer changed, and what it kept.
The task
Section titled “The task”Date normalisation. A date written the way a person writes it goes in; ISO format comes out.
| Input | Output |
|---|---|
19/3/1994 | 1994-03-19 |
wednesday 19 september 1977 | 1977-09-19 |
the 4 of august, 2003 | 2003-08-04 |
Six input formats, 6,000 training strings, 1,200 held out, characters in and characters
out. This task is chosen deliberately: it is synthetic, so no corpus is downloaded;
it is hard for a bottleneck, because the output must reorder its input; and the true
alignment is known — the 1977 in the output comes from the 1977 in the input and
nowhere else — which makes the attention weights checkable.
The bottleneck
Section titled “The bottleneck”A plain encoder–decoder reads the whole input, produces one final state , and hands that single vector to the decoder:
Every character of the answer is then produced from , which descends from . At 64 units that is 64 floating-point numbers carrying a 27-character input. Everything the decoder will ever know about the input has to fit there.
The prediction is that widening the encoder should help the plain model a great deal and the attention model very little, because attention does not have to fit anything into a fixed budget — it can look back at the encoder’s per-timestep outputs:
| Encoder width | Plain, exact | Attention, exact | Plain, per character | Attention, per character |
|---|---|---|---|---|
| 8 | 0.0000 | 0.0000 | 0.1432 | 0.4995 |
| 16 | 0.0000 | 0.0133 | 0.5524 | 0.7450 |
| 32 | 0.0008 | 0.9042 | 0.6467 | 0.9871 |
| 64 | 0.0017 | 1.0000 | 0.7277 | 1.0000 |
Exact match is a harsh metric on a ten-character output — one wrong digit fails the whole string — so the per-character columns are there to show that the plain model is not producing noise. At 64 units it gets 72.77% of output characters right while getting essentially no complete date right.
What the bottleneck actually drops
Section titled “What the bottleneck actually drops”That combination — most characters right, almost no strings right — is only interesting if you look at which characters:
| Output position | Plain | Attention |
|---|---|---|
Y Y Y | 1.0000 | 1.0000 |
Y (4th) | 0.9625 | 1.0000 |
- (both) | 1.0000 | 1.0000 |
M (1st) | 0.7467 | 1.0000 |
M (2nd) | 0.1108 | 1.0000 |
D (1st) | 0.3658 | 1.0000 |
D (2nd) | 0.0908 | 1.0000 |
Read the plain column as a memory decay curve. The dashes are free — they are at fixed positions and need no input at all. The year is perfect because it appears last in every one of the six input formats, so it is the most recent thing the encoder saw. The month is partially recoverable. The day, which appears earliest, is almost gone: 0.0908 on its second digit is close to the 0.10 you get by guessing a digit uniformly.
Additive attention
Section titled “Additive attention”The fix is to stop discarding the encoder’s intermediate states. Keep all of them — — and let the decoder build a fresh summary at every output step, weighted toward whichever inputs are relevant right now.
At decoder step , with previous decoder state :
Three pieces, and each earns its place:
- — the additive part, and the reason for the name. Query and key are projected and summed, then squashed. Scaled dot-product attention replaces this whole expression with , which has no parameters and is one matrix multiply.
- The softmax makes the weights a distribution — they sum to 1 across input positions, so the context vector is a weighted average of real encoder states rather than an arbitrary combination.
- is recomputed every step. That is the whole difference from the plain model, which computes its summary once.
The context vector is then concatenated with the embedded previous output token and fed to the decoder cell:
query = self.state_projection(state)[:, None, :] # W_d s_{t-1}
scores = self.score(tf.nn.tanh(projected + query))[..., 0]
alpha = tf.nn.softmax(scores, axis=1) # over input positions
context = tf.reduce_sum(alpha[..., None] * encoded, axis=1)
cell_input = tf.concat([embedded[:, step, :], context], axis=-1)
output, [state] = self.cell(cell_input, [state])At 64 units this costs 20,544 extra parameters (36,893 → 57,437) and roughly double the wall clock, 43.6 s → 83.5 s. It buys exact-match accuracy of 1.0000 against 0.0017.
flowchart TD X["input characters"] --> E["GRU encoder"] E --> H["h_1 ... h_T
one state per input character"] H -->|"plain: keep only h_T"| B["64 numbers
for the whole input"] B --> D1["decoder
year 1.0000, day 0.0908"] H -->|"attention: keep all of them"| S["score each h_j
against s_(t-1)"] S --> A["softmax -> alpha_t"] A --> C["context c_t = sum alpha_tj h_j"] C --> D2["decoder
every position 1.0000"]
Reading the alignment
Section titled “Reading the alignment”Attention produces a weight for every (output position, input position) pair, which plots as a matrix. This is the part that gets over-interpreted, so the task was chosen so the correct answer is known before looking.
The measured peaks, character by character:
| Output | Peak input position | Character there |
|---|---|---|
1 9 7 | 25 | 7 (inside 1977) |
7 | 22 | space before 1977 |
- | 22 | space |
0 | 21 | r (end of september) |
9 | 15 | p (inside september) |
- | 22 | space |
1 9 | 11 | 9 (inside 19) |
The three groups land on the three spans that carry the answer, and they land out of order — the year first, then the month word, then the day. That is what “the decoder chooses where to look” means concretely.
What the transformer kept and what it changed
Section titled “What the transformer kept and what it changed”Every piece of this mechanism survives into the transformer, with two substitutions.
| Bahdanau, 2014 | Transformer, 2017 |
|---|---|
| score = | score = |
| query is the decoder’s recurrent state | query is a projection of a token, no recurrence |
| one attention head | many heads in parallel |
| encoder is a GRU, so steps are sequential | encoder is attention, so steps are parallel |
| context concatenated into the recurrent cell | context is the residual stream itself |
The important change is the second row. Bahdanau’s query comes from a recurrent state, so the decoder still runs one step at a time and the encoder still walks the input sequentially. Removing recurrence — replacing the query source with a projection of the token itself — is what makes the whole sequence computable in parallel, and that is the transformer’s actual contribution. The scoring function got simpler along the way, but that was a bonus, not the point.
Pitfalls
Section titled “Pitfalls”- Concluding “attention beats recurrence”. Attention here is added to a recurrent encoder–decoder. Both models are recurrent; only one has a bottleneck.
- Reading exact match alone. 0.0017 against 1.0000 suggests the plain model learned nothing. Per character it reached 0.7277, and the per-position split is where the actual finding is.
- Assuming longer inputs are the problem. Measured spread across length buckets: 0.0300, no trend. Distance from the end of the input is the axis that matters.
- Narrating an alignment map without knowing the answer. The year digits peak on a space. Encoder states are contextual, so the bright cell is not always where a human would point.
- Forgetting the cost. 20,544 extra parameters and 1.9× the wall clock, and the context vector is recomputed at every output step, so attention costs score evaluations where the bottleneck costs a constant number.
- Comparing at one encoder width. At 8 units both models score 0.0000 exact. A single-width comparison could have shown attention making no difference at all.
- A plain encoder–decoder passes the entire input through one fixed-size vector; at 64 units that is 64 numbers for a 27-character string.
- On date normalisation the plain model reached 0.0017 exact match against attention’s 1.0000, and per character 0.7277 against 1.0000.
- The per-position split shows what the bottleneck drops: year and separators 1.0000, day second digit 0.0908 — the failure tracks distance from the end of the input, not input length (spread across length buckets: 0.0300).
- Additive attention scores every encoder state against the previous decoder state, softmaxes, and averages — one context vector per output step instead of one per sequence.
- The measured alignment jumps between three spans out of order, and peaks on positions whose states are informative rather than on the characters a human would choose.
- The transformer keeps the score-softmax-average structure and replaces the recurrent query with a token projection, which is what removes the sequential dependency.
Phase 5 takes the mechanism on this page, removes the recurrence around it, and asks what is left: Attention from Scratch (Queries, Keys and Values).
-
The plain encoder–decoder scored 1.0000 on all four year digits and 0.0908 on the day's second digit. What explains the pattern?
pch.quizShowAnswer
B — The year appears last in every input format, so it is the most recent thing the fixed-size final state absorbed; the day appears earliest and has been overwritten
-
Bucketing the same results by input length gave 0.7300, 0.7211, 0.7386 and 0.7086 — a spread of 0.0300 with no trend. Why report a null result?
pch.quizShowAnswer
B — Because it rules out the obvious explanation: the bottleneck is not about how long the input is but about how far the needed information sits from its end
-
In additive attention, what makes the mechanism 'additive'?
pch.quizShowAnswer
B — The encoder and decoder projections are summed inside a tanh before scoring — W_e h + W_d s — rather than multiplied as a dot product
-
For the input 'wednesday 19 september 1977', the year digits peaked at input position 22 and 25 — one of which is a space. Is the alignment wrong?
pch.quizShowAnswer
B — No — the model attends to encoder STATES, and a GRU state at position 22 has already absorbed the characters around it, so it can be the most informative place to look
-
What did the transformer actually change relative to this mechanism?
pch.quizShowAnswer
B — It replaced the recurrent decoder state as the query source with a projection of the token itself, which removes the sequential dependency and lets the whole sequence be computed in parallel
-
At 8 encoder units both models scored 0.0000 exact match. What would a single-width comparison have concluded?
pch.quizShowAnswer
B — That attention makes no difference, which the 32- and 64-unit columns refute (0.9042 and 1.0000 against 0.0008 and 0.0017)
🧪 Try It Yourself
Section titled “🧪 Try It Yourself”pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading