Phase 5 · Lesson 9 of 29

Sequence-to-Sequence Models

Two RNNs pretending to be a translator. The bottleneck they hit is the reason attention exists.

Lesson 9 of 29Build~75 minutesPython

Before this: Lesson 8 of Phase 5 (CNNs + RNNs for Text), lesson 11 of Phase 3 (PyTorch Intro)

On this page

The Problem

Classification maps a variable-length sequence to a single label. Translation maps a variable-length sequence to another variable-length sequence. The input and output live in different vocabularies, possibly different languages, with no guarantee of length parity.

The seq2seq architecture (Sutskever, Vinyals, Le, 2014) cracked this with a deliberately simple recipe. Two RNNs. One reads the source sentence and produces a fixed-size context vector. The other reads that vector and generates the target sentence token by token. Same code you wrote for lesson 08, glued together differently.

This is worth studying for two reasons. First, the context-vector bottleneck is the most pedagogically useful failure in NLP. It motivates everything attention and transformers are good at. Second, the training recipe (teacher forcing, scheduled sampling, beam search at inference) still applies to every modern generation system including LLMs.

The Concept

Encoder. An RNN that reads the source sentence. Its final hidden state is the context vector — a fixed-size summary of the entire input. Lose nothing but the source, supposedly.

Decoder. Another RNN initialized from the context vector. At each step it takes the previously generated token as input and produces a distribution over the target vocabulary. Sample or argmax to pick the next token. Feed it back in. Repeat until an <EOS> token is produced or max length is hit.

Training: Cross-entropy loss at each decoder step, summed over the sequence. Standard backprop through time through both networks.

Teacher forcing. During training, the decoder's input at step t is the ground-truth token at position t-1, not the decoder's own previous prediction. This stabilizes training; without it, early mistakes cascade and the model never learns. At inference, you have to use the model's own predictions, so there is always a train/inference distribution gap. That gap is called exposure bias.

The bottleneck. Everything the encoder learned about the source must be squeezed into that one context vector. Long sentences lose detail. Rare words get blurred. Reordering (chat noir vs. black cat) has to be memorized, not computed.

Attention (lesson 10) fixes this by letting the decoder look at every encoder hidden state, not just the last one. That is the whole pitch.

lstm-gates

Build It

Step 1: an encoder

import torch
import torch.nn as nn


class Encoder(nn.Module):
    def __init__(self, src_vocab_size, embed_dim, hidden_dim):
        super().__init__()
        self.embed = nn.Embedding(src_vocab_size, embed_dim, padding_idx=0)
        self.gru = nn.GRU(embed_dim, hidden_dim, batch_first=True)

    def forward(self, src):
        e = self.embed(src)
        outputs, hidden = self.gru(e)
        return outputs, hidden

outputs has shape [batch, seq_len, hidden_dim] — one hidden state per input position. hidden has shape [1, batch, hidden_dim] — the final step. Lesson 08 said "pool over outputs for classification." Here we keep the last hidden state as the context vector, and ignore the per-step outputs.

Step 2: a decoder

class Decoder(nn.Module):
    def __init__(self, tgt_vocab_size, embed_dim, hidden_dim):
        super().__init__()
        self.embed = nn.Embedding(tgt_vocab_size, embed_dim, padding_idx=0)
        self.gru = nn.GRU(embed_dim, hidden_dim, batch_first=True)
        self.fc = nn.Linear(hidden_dim, tgt_vocab_size)

    def forward(self, token, hidden):
        e = self.embed(token)
        out, hidden = self.gru(e, hidden)
        logits = self.fc(out)
        return logits, hidden

Decoder is called one step at a time. Input: a batch of single tokens and the current hidden state. Output: vocabulary logits for the next token and the updated hidden state.

Step 3: training loop with teacher forcing

def train_batch(encoder, decoder, src, tgt, bos_id, optimizer, teacher_forcing_ratio=0.9):
    optimizer.zero_grad()
    _, hidden = encoder(src)
    batch_size, tgt_len = tgt.shape
    input_token = torch.full((batch_size, 1), bos_id, dtype=torch.long)
    loss = 0.0
    loss_fn = nn.CrossEntropyLoss(ignore_index=0)

    for t in range(tgt_len):
        logits, hidden = decoder(input_token, hidden)
        step_loss = loss_fn(logits.squeeze(1), tgt[:, t])
        loss += step_loss
        use_teacher = torch.rand(1).item() < teacher_forcing_ratio
        if use_teacher:
            input_token = tgt[:, t].unsqueeze(1)
        else:
            input_token = logits.argmax(dim=-1)

    loss.backward()
    optimizer.step()
    return loss.item() / tgt_len

Two knobs worth naming. ignore_index=0 skips loss on padding tokens. teacher_forcing_ratio is the probability of using the true token vs. the model's prediction at each step. Start at 1.0 (full teacher forcing) and anneal down to ~0.5 over training to close the exposure-bias gap.

Step 4: inference loop (greedy)

@torch.no_grad()
def greedy_decode(encoder, decoder, src, bos_id, eos_id, max_len=50):
    _, hidden = encoder(src)
    batch_size = src.shape[0]
    input_token = torch.full((batch_size, 1), bos_id, dtype=torch.long)
    output_ids = []
    for _ in range(max_len):
        logits, hidden = decoder(input_token, hidden)
        next_token = logits.argmax(dim=-1)
        output_ids.append(next_token)
        input_token = next_token
        if (next_token == eos_id).all():
            break
    return torch.cat(output_ids, dim=1)

Greedy decoding picks the highest-probability token at every step. It can wander off: once you commit to a token, you cannot unsay it. Beam search keeps the top-k partial sequences alive and picks the highest-scoring complete one at the end. Beam width 3-5 is standard.

Step 5: the bottleneck, demonstrated

Train the model on a toy copy task: source [a, b, c, d, e], target [a, b, c, d, e]. Increase sequence length. Observe accuracy.

seq_len=5   copy accuracy: 98%
seq_len=10  copy accuracy: 91%
seq_len=20  copy accuracy: 62%
seq_len=40  copy accuracy: 23%

A single GRU hidden state cannot losslessly memorize a 40-token input. The information is there at every encoder step, but the decoder only sees the last state. Attention fixes this directly.

Use It

PyTorch has nn.Transformer and nn.LSTM-based seq2seq templates. Hugging Face's transformers library ships full encoder-decoder models (BART, T5, mBART, NLLB) trained on billions of tokens.

from transformers import AutoTokenizer, AutoModelForSeq2SeqLM

tok = AutoTokenizer.from_pretrained("facebook/bart-base")
model = AutoModelForSeq2SeqLM.from_pretrained("facebook/bart-base")

src = tok("Translate this to French: Hello, how are you?", return_tensors="pt")
out = model.generate(**src, max_new_tokens=50, num_beams=4)
print(tok.decode(out[0], skip_special_tokens=True))

Modern encoder-decoders dropped RNNs for transformers. The high-level shape (encoder, decoder, generate-token-by-token) is identical to the 2014 seq2seq paper. The mechanism inside each block is different.

When to still reach for RNN-based seq2seq

Almost never, for new projects. Specific exceptions:

  • Streaming translation where you consume input one token at a time with bounded memory.
  • On-device text generation where transformer memory cost is prohibitive.
  • Pedagogy. Understanding the encoder-decoder bottleneck is the fastest path to understanding why transformers won.

Exposure bias and its mitigations

  • Scheduled sampling. Anneal teacher forcing ratio during training so the model learns to recover from its own mistakes.
  • Minimum risk training. Train on sentence-level BLEU score instead of token-level cross-entropy. Closer to what you actually want.
  • Reinforcement learning fine-tuning. Reward the sequence generator with a metric. Used in modern LLM RLHF.

All three still apply to transformer-based generation.

Ship It

Save as outputs/prompt-seq2seq-design.md:

---
name: seq2seq-design
description: Design a sequence-to-sequence pipeline for a given task.
phase: 5
lesson: 09
---

Given a task (translation, summarization, paraphrase, question rewrite), output:

1. Architecture. Pretrained transformer encoder-decoder (BART, T5, mBART, NLLB) is the default. RNN-based seq2seq only for specific constraints.
2. Starting checkpoint. Name it (`facebook/bart-base`, `google/flan-t5-base`, `facebook/nllb-200-distilled-600M`). Match the checkpoint to task and language coverage.
3. Decoding strategy. Greedy for deterministic output, beam search (width 4-5) for quality, sampling with temperature for diversity. One sentence justification.
4. One failure mode to verify before shipping. Exposure bias manifests as generation drift on longer outputs; sample 20 outputs at the 90th-percentile length and eyeball.

Refuse to recommend training a seq2seq from scratch for under a million parallel examples. Flag any pipeline that uses greedy decoding for user-facing content as fragile (greedy repeats and loops).

Exercises

  1. Easy. Implement the toy copy task. Train a GRU seq2seq on input-output pairs where the target equals the source. Measure accuracy at lengths 5, 10, 20. Reproduce the bottleneck.
  2. Medium. Add beam search decoding with beam width 3. Measure BLEU on a small parallel corpus against greedy. Document where beam search wins (usually last tokens) and where it makes no difference.
  3. Hard. Fine-tune facebook/bart-base on a 10k-pair paraphrase dataset. Compare the fine-tuned model's beam-4 output to the base model's on held-out inputs. Report BLEU and pick 10 qualitative examples.

Key Terms

Term What people say What it actually means
Encoder Input RNN Reads source. Produces per-step hidden states and a final context vector.
Decoder Output RNN Initialized from context vector. Generates target tokens one at a time.
Context vector The summary Final encoder hidden state. Fixed size. The bottleneck attention solves.
Teacher forcing Use true tokens Feed the ground-truth previous token at training time. Stabilizes learning.
Exposure bias Train/test gap Model trained on true tokens never practiced recovering from its own mistakes.
Beam search Better decoding Keep top-k partial sequences alive at each step instead of committing greedily.

Further Reading

Check yourself

Quick quiz

  1. What is the role of the encoder in a 2014-style seq2seq model?

    1. Generates target tokens
    2. Performs beam search
    3. Computes attention weights
    4. Reads the source and produces a fixed-size context vector summarizing it
    Show the answer

    D. Reads the source and produces a fixed-size context vector summarizing it The encoder RNN compresses the source into a final hidden state used by the decoder.

  2. What is teacher forcing during seq2seq training?

    1. Manually labeling each decoder step
    2. Adding a teacher network during inference
    3. Doubling the batch size
    4. Feeding the ground-truth previous token (instead of the model's prediction) as decoder input
    Show the answer

    D. Feeding the ground-truth previous token (instead of the model's prediction) as decoder input Teacher forcing stabilizes training by using true previous tokens; without it early errors cascade.

  3. Why does fixed context-vector seq2seq accuracy fall as input length grows?

    1. Cross-entropy diverges
    2. Padding tokens accumulate
    3. Vocabulary becomes too large
    4. All information about the source must fit in a single fixed-size encoder hidden state, which loses detail on long inputs
    Show the answer

    D. All information about the source must fit in a single fixed-size encoder hidden state, which loses detail on long inputs The fixed context-vector bottleneck means long inputs cannot be losslessly summarized.

  4. What is exposure bias?

    1. Bias from class imbalance
    2. Bias in encoder embeddings
    3. Annotator disagreement
    4. The train/inference gap from training on ground-truth tokens but generating from the model's own predictions at inference
    Show the answer

    D. The train/inference gap from training on ground-truth tokens but generating from the model's own predictions at inference The model never practiced recovering from its own mistakes during training, so errors cascade at inference.

  5. Why does beam search often outperform greedy decoding for generation?

    1. Beam search is faster
    2. Beam search keeps the top-k partial sequences alive at each step instead of irrevocably committing to one token
    3. Beam search avoids exposure bias
    4. Beam search lowers the loss
    Show the answer

    B. Beam search keeps the top-k partial sequences alive at each step instead of irrevocably committing to one token Greedy commits per step; beam search explores multiple hypotheses, then picks the best complete one.

  6. Which architectural family replaced RNN seq2seq for general generation tasks?

    1. Naive Bayes
    2. Graph neural networks
    3. 1D CNNs
    4. Transformer encoder-decoder models (BART, T5, mBART, NLLB)
    Show the answer

    D. Transformer encoder-decoder models (BART, T5, mBART, NLLB) Transformer encoder-decoders dropped recurrence and now dominate generation tasks.

  7. What does scheduled sampling do?

    1. Adds random noise to embeddings
    2. Anneals the teacher-forcing ratio downward during training so the model learns to recover from its own predictions
    3. Reorders the training set
    4. Schedules learning-rate decay
    Show the answer

    B. Anneals the teacher-forcing ratio downward during training so the model learns to recover from its own predictions Scheduled sampling gradually mixes in model predictions to close the train/inference gap.

  8. Why does greedy decoding alone often fail for user-facing generation?

    1. It always picks <EOS> first
    2. It cannot use embeddings
    3. Greedy can repeat or loop and cannot backtrack from a locally good but globally poor token choice
    4. It requires more memory
    Show the answer

    C. Greedy can repeat or loop and cannot backtrack from a locally good but globally poor token choice Greedy decoding's irrevocable per-step choice causes loops and repetition without beam search or sampling.

Questions people ask

What is an encoder-decoder model?

It is a model with two parts. The encoder reads the input and turns it into an internal representation. The decoder uses that representation to generate the output one token at a time. Translation and summarization are classic uses.

What is the bottleneck problem in seq2seq models?

Early seq2seq models squeezed the whole input into one fixed-size vector before decoding. Long inputs lost detail in that single summary, so output quality dropped as sentences got longer. Attention was invented to let the decoder look back at every input word.

Next lessonAttention Mechanism — The BreakthroughPrevious: CNNs and RNNs for Text

Lessons from AI Engineering from Scratch by Rohit Ghumare and contributors, MIT license. Notes for owners by Lozo. License

Next step

Rather have it built for you?

Reading is optional. Tell me what's stuck and I'll build it into the business you already run.

  • A real conversation, not a pitch
  • You keep your accounts
  • Nothing sends without your yes