1 The sequence-to-sequence problem
Imagine two friends who don't speak the same language, with a translator sitting between them. The first friend whispers a whole sentence to the translator. The translator listens to the entire thing, nods, holds the idea in their head — and only then starts speaking it out loud in the other language, one word at a time. Listen fully first, then speak. That "listen, then speak" dance is exactly what today's network does.
So far our networks did things like "read a sentence, output one label" (is this review positive or negative?) or "read numbers, predict the next number." But a huge family of real problems is different: the input is a sequence and the output is a sequence — and the two can be different lengths. This is the sequence-to-sequence (often written seq2seq) problem.
Where it shows up
| Task | Input sequence | Output sequence |
|---|---|---|
| Machine translation | "I am hungry" (English) | "J'ai faim" (French) |
| Summarization | A 500-word article | A 2-sentence summary |
| Chatbot / dialogue | A user's message | The bot's reply |
| Speech-to-text | Audio frames | Words |
| Code generation | A description | Lines of code |
Notice the key wrinkle: "I am hungry" is 3 words but "J'ai faim" is 2. The input and output don't line up one-to-one, and we don't know the output length in advance. A plain RNN that emits one output per input step can't handle that. We need an architecture that reads the whole input first, then generates an output of whatever length it needs.
The encoder–decoder architecture
The classic 2014 solution (Sutskever et al., and Cho et al.) splits the job into two RNNs:
- The encoder — an RNN (in practice usually an LSTM or GRU, recall Session 13) that reads the input sequence one token at a time and squeezes everything it understood into a single fixed-size summary vector.
- The context vector — that final summary, often written
c. It's the encoder's last hidden state: a fixed-length list of numbers that's supposed to capture the meaning of the whole input. - The decoder — a second RNN that starts from that context vector and generates the output sequence one token at a time, feeding each word it just produced back in as the next input (just like the autoregressive loop you've seen for language models).
The flow, step by step
Two special tokens make this work. <EOS> ("end of sequence") tells the
encoder where the input ends, and is the signal the decoder emits when it's done. A start token, often
<SOS> or <BOS>, is the decoder's very first
input to kick off generation.
Let's write the math compactly. The encoder runs its recurrence over input tokens
x₁ … xₜ, producing hidden states h₁ … hₜ:
| Step | Equation | In words |
|---|---|---|
| Encoder | hᵗ = f(hᵗ₋₁, xᵗ) | Update hidden state from previous state + current input word. |
| Context | c = hₜ | The context is just the last encoder state. |
| Decoder | sᵖ = g(sᵖ₋₁, yᵖ₋₁, c) | New decoder state from previous state + previous output word + context. |
| Output | P(yᵖ) = softmax(W·sᵖ) | Turn the decoder state into a probability over the vocabulary. |
Here's a tiny PyTorch-flavoured sketch of an encoder–decoder so you can see the two RNNs and the single context hand-off concretely:
class Seq2Seq(nn.Module): def __init__(self, vocab_in, vocab_out, emb, hid): self.enc_emb = nn.Embedding(vocab_in, emb) self.encoder = nn.LSTM(emb, hid) # reads the source self.dec_emb = nn.Embedding(vocab_out, emb) self.decoder = nn.LSTM(emb, hid) # writes the target self.out = nn.Linear(hid, vocab_out) # hidden -> vocab scores def forward(self, src, tgt): # 1) ENCODE: read whole source, keep only final state _, (h, c) = self.encoder(self.enc_emb(src)) # 'h' is THE context vector — the bottleneck (Topic 2) # 2) DECODE: start decoder from the encoder's final state dec_in = self.dec_emb(tgt) # <SOS> J'ai faim ... dec_out, _ = self.decoder(dec_in, (h, c)) return self.out(dec_out) # scores for each output step
Walking through it: the encoder reads I, am,
hungry, <EOS> and its final hidden state
h becomes the context. The decoder, seeded with that h,
is fed <SOS> and predicts J'ai; that's fed back in
and it predicts faim; fed back in, it predicts <EOS>
and stops. Three input steps, three output steps — different content, free to differ in length.
During training, we usually feed the decoder the correct previous word (from the real translation) rather than its own guess — this is called teacher forcing. It makes training faster and more stable. At inference time there's no answer key, so the decoder must feed back its own predictions.
<EOS>.
2 The information bottleneck
Imagine you have to read someone a whole storybook, but you're only allowed to write yourself one tiny sticky note first — and then close the book forever and tell the story from just that note. For a one-line story, easy. For a long fairy tale with lots of characters? That little note can't possibly hold everything, and you'll forget the beginning. The single context vector is that one sticky note.
Look back at the encoder–decoder. Everything the decoder ever sees about the input is
squeezed through that single fixed-length context vector c. Whether the input is
3 words or 300, it all has to fit into the same number of slots (say 512 numbers). This is the
information bottleneck, and it's the core limitation of plain seq2seq.
A fixed-size vector cannot faithfully store an arbitrarily long input. As sentences get longer, more meaning gets crushed into the same number of slots, so detail inevitably leaks out — especially the words read earliest.
Why long sequences suffer most
- Compression loss. 300 words of meaning forced into 512 numbers means the encoder must throw information away. The decoder simply never receives it.
- Recency bias. Because the context is the last hidden state, the words read most recently (the end of the input) are best represented; the beginning has been overwritten through many recurrence steps — the classic vanishing-gradient / forgetting problem from Session 13, now hurting us at the architecture level.
- No way to revisit. Once encoding is done, the original words are gone. If the decoder, halfway through, really needs that one noun from the start of the sentence, it has no way to go back and look — it only has the blurry summary.
Researchers measured translation quality (BLEU score — higher is better) against input sentence length for a fixed-context seq2seq model. The pattern was stark:
| Sentence length | Translation quality | What's happening |
|---|---|---|
| ≤ 10 words | Good | The whole meaning fits in the context vector. |
| ~20 words | Still okay | Getting tight; some early detail blurs. |
| 30+ words | Drops sharply | The vector overflows; quality falls off a cliff. |
Translate a 40-word legal sentence and the model often produces a fluent-sounding output that quietly drops or scrambles the parts it couldn't fit — a date here, a clause there. The decoder isn't lying on purpose; it genuinely never got that information.
Sutskever et al. found that simply reversing the input (encoding "hungry am I" instead of "I am hungry") improved results, because it put the start of the source closer to where decoding begins. That this hack helped at all is a flashing warning sign: the bottleneck, not the vocabulary, was the problem.
You might think "just make the context vector huge!" — but a bigger vector means more parameters, more compute, slower training, and easier overfitting, and it still can't grow with the input. Any fixed size is eventually too small. We need a fundamentally different idea, not a bigger note.
3 Attention: letting the decoder look back
Back to our translator — but now they're allowed to keep the open book on the table. As they say each word of the translation, they glance back at the parts of the original that matter for that word. Saying the French word for "hungry"? They glance at "hungry" in the English. They don't memorize the whole book into one note; they look back, on demand, at the bits they need right now. That glancing-back is attention.
Bahdanau et al. (2014) asked: why throw away all the encoder's per-word hidden states and keep only the
last one? Instead, keep all of them — h₁ … hₜ, one per
input word — and let the decoder build a fresh context vector at every output step,
focusing on whichever input words are relevant for the word it's about to produce.
Don't summarize the input once. At each output step, let the decoder look at all encoder states and take a weighted average of them, where the weights say "how much should I attend to each input word right now?" The context becomes dynamic — different for every word produced.
The three ingredients
At decoder step t, with the decoder's current state
sᵖ₋₁, attention does three things:
| Step | Equation | What it means |
|---|---|---|
| 1. Score | eᵖᵢ = score(sᵖ₋₁, hᵢ) | For each input word i, how relevant is it to what I'm producing now? These are the alignment scores. |
| 2. Normalize | αᵖᵢ = softmax(eᵖᵢ) | Turn raw scores into attention weights that are positive and sum to 1 (a probability distribution over input words). |
| 3. Blend | cᵖ = Σᵢ αᵖᵢ · hᵢ | Build a context vector for this step as the weighted sum of encoder states. |
That step-specific cᵖ is then combined with the decoder state to predict
the output word: sᵖ = g(sᵖ₋₁, yᵖ₋₁, cᵖ), then
P(yᵖ) = softmax(W·[sᵖ; cᵖ]). Crucially, because the weights
αᵖᵢ change every step, the decoder reads a different
mix of the source for every word it writes. The bottleneck is gone: there's no longer a single vector that
has to hold everything.
The flow with attention
Translating "I am hungry" → "J'ai faim". Suppose the encoder produced one state per
English word: h(I), h(am),
h(hungry). Watch the attention weights shift as the decoder writes each French
word:
| Decoder is producing | α on "I" | α on "am" | α on "hungry" |
|---|---|---|---|
| "J'ai" (I have) | 0.7 | 0.25 | 0.05 |
| "faim" (hunger) | 0.05 | 0.10 | 0.85 |
Each row sums to 1. When writing J'ai the model leans on "I"/"am"; when
writing faim it leans almost entirely on "hungry." That learned mapping between
output words and the input words they correspond to is called alignment — and
here's the lovely part: nobody labelled it. The model discovered which words translate
to which, purely from the attention math.
A minimal forward pass for one step:
# enc_states: (T, hid) — one row per input word # s_prev: (hid,) — decoder state from the last step scores = enc_states @ W_a @ s_prev # e_{t,i}: relevance of each input word alpha = softmax(scores) # weights, sum to 1 c_t = (alpha.unsqueeze(1) * enc_states).sum(0) # weighted blend out = predict(s_prev, y_prev, c_t) # produce this output word
Attention replaces "compress everything once" with "look back, weighted, every step." The decoder gets a direct line to every input word, so long-sequence quality stops falling off a cliff — and as a bonus, the attention weights give us a window into what the model is looking at.
4 Attention variants & intuition
"How relevant is this word to me?" can be measured in different ways. One way is to hand both words to a little helper who studies them and gives a relevance score (that's the additive way). A faster way is to just see how much the two words "point in the same direction" — multiply them together (the multiplicative way). Same goal, two recipes: one a bit smarter, one a lot faster.
The key choice in attention is how you compute the alignment score
eᵖᵢ = score(sᵖ₋₁, hᵢ). Two famous families appeared
within a year of each other.
Additive (Bahdanau) attention
Additive attention, from Bahdanau et al. (2014), feeds the decoder state and an encoder state through a tiny one-hidden-layer neural network and reads off a score:
e_ti = vᵀ · tanh( W₁·s_{t-1} + W₂·h_i )
It's called "additive" because the two inputs are added inside the
tanh. The learned vectors W₁, W₂, v let the model
figure out, flexibly, what "relevant" means. It's expressive and works even when the encoder and decoder
states have different sizes — but the little network makes it slower.
Multiplicative (Luong) attention
Multiplicative attention (also called dot-product attention), from Luong et al. (2015), scores relevance with a simple dot product — essentially asking "how aligned are these two vectors?":
| Form | Score | Note |
|---|---|---|
| Dot | eᵖᵢ = sᵖ₋₁ · hᵢ | Cheapest; needs matching sizes. |
| General | eᵖᵢ = sᵖ₋₁ᵀ · W · hᵢ | A learned matrix W; handles different sizes. |
Because a dot product is just one big matrix multiply — and GPUs love matrix multiplies — this is much faster than the additive recipe. That speed is precisely why the dot-product style won out and sits at the heart of the Transformer.
Side by side
| Aspect | Additive (Bahdanau) | Multiplicative (Luong) |
|---|---|---|
| Score function | Tiny feed-forward net with tanh | Dot product (optionally with a learned W) |
| Speed | Slower (extra layer) | Faster (one matmul, GPU-friendly) |
| Different state sizes | Handles naturally | Needs W (the "general" form) |
| Year / paper | 2014, "Jointly Learning to Align and Translate" | 2015, "Effective Approaches…" |
| Legacy | Introduced attention | Led to scaled dot-product attention |
When vectors are long, plain dot products grow large, which pushes softmax into flat regions where
gradients vanish. The Transformer's fix (next session) is scaled dot-product
attention — divide the score by √d (the square root of the vector
size) before softmax. Same multiplicative idea, just kept numerically calm.
Reading the weights: attention as an X-ray
Because the attention weights αᵖᵢ form a tidy grid (output words
down one side, input words across the top, each cell a number 0–1), you can plot them as a
heatmap and literally see what the model looked at.
Translating "the agreement was signed" → French "l'accord a été signé". Darker = more attention (per-row weights, each row sums to ~1):
| ↓ output / input → | the | agreement | was | signed |
|---|---|---|---|---|
| l'accord | 0.30 | 0.65 | 0.03 | 0.02 |
| a été | 0.05 | 0.10 | 0.80 | 0.05 |
| signé | 0.02 | 0.03 | 0.05 | 0.90 |
The bright diagonal-ish band shows the model aligning each French word to its English source — and for languages that reorder words, this band bends, showing attention jumping around the sentence to grab the right word out of order. This visual is both a debugging tool and a confidence check.
Additive and multiplicative attention are two recipes for the same "how relevant?" question. Multiplicative (dot-product) won on speed, and once you realize attention is just "query × keys → weights → weighted sum of values," you're one step from the Transformer, which throws out the RNN entirely and uses attention everywhere. That's Session 15.
tanh
network — expressive but slower; multiplicative (Luong) uses a dot product — faster and
GPU-friendly, leading to the scaled dot-product attention of the Transformer. Plotting
the weights as a heatmap reveals the learned word alignments and sets up next session perfectly.
★ Putting it all together
Today's session is one tight story about reading and writing sequences — and the single idea that unlocked modern NLP. Here's the whole arc in a paragraph:
The sequence-to-sequence problem maps one sequence to another of possibly different length, and the encoder–decoder architecture solves it by encoding the input into a single context vector and decoding from it. But cramming everything into one fixed vector is an information bottleneck that breaks on long inputs. Attention fixes this by keeping all encoder states and, at every output step, computing alignment scores, softmaxing them into attention weights, and blending the states into a fresh per-step context — letting the decoder look back at exactly the words it needs. The score can be computed the additive (Bahdanau) way or the faster multiplicative (Luong) way, and that dot-product recipe — once scaled — is the very heart of the Transformer we'll build next session.
Quick self-check
What makes a problem "sequence-to-sequence," and why can't a plain RNN do it?
Both the input and output are sequences, often of different lengths (e.g. 3 input words → 2 output words). A plain RNN emits one output per input step, so it can't produce an output of a different, unknown length. The encoder–decoder design (read fully, then generate) handles it.
In one sentence, what is the information bottleneck?
The entire input must be squeezed into a single fixed-size context vector, so long inputs lose information — especially the earliest words — because a fixed vector can't hold an arbitrarily long sequence.
What are the three steps attention performs at each decoder step?
(1) Score each encoder state against the current decoder state (alignment scores), (2) softmax the scores into attention weights that sum to 1, and (3) take a weighted sum of the encoder states to form a fresh, step-specific context vector.
Why does the context vector with attention beat the single fixed context vector?
It's dynamic: a different weighted blend is built for every output word, giving the decoder a direct line to all input words rather than one blurry summary. So long-sequence quality no longer collapses.
Additive vs multiplicative attention — what's the practical difference?
Additive (Bahdanau) scores relevance with a small tanh network —
expressive but slower. Multiplicative (Luong) uses a dot product — faster and GPU-friendly. The
dot-product form, scaled by √d, became the Transformer's attention.
What does an attention heatmap show you?
The learned alignment between output and input words: each cell is how much an output word attended to an input word. A bright band reveals word correspondences the model discovered on its own, which is great for debugging and interpretability.
📚 References & Further Reading
Class material
- SST Deep Learning handout (Session 14) — your course handout for this session.
Papers, docs & deep dives
- SST Deep Learning handout (online) — the companion notes for this session's material.
- Sutskever, Vinyals & Le — "Sequence to Sequence Learning with Neural Networks" (2014) — the paper that introduced the LSTM encoder–decoder and the input-reversal trick.
- Bahdanau, Cho & Bengio — "Neural Machine Translation by Jointly Learning to Align and Translate" (2014) — the original attention mechanism (additive); read it for where attention came from.
- Luong, Pham & Manning — "Effective Approaches to Attention-based NMT" (2015) — introduces multiplicative / dot-product and global vs local attention.
- Vaswani et al. — "Attention Is All You Need" (2017) — where attention goes next: the Transformer (preview of Session 15).