GenAI Course · Module 4 · Session 16

Encoder–Decoder (Seq2Seq)

⏱ 13 min read🎯 Intermediate🖼 3 diagrams🧪 1 live demo🐍 Python code

Translation, summarisation and chatbots all turn one sequence into another of a different length. The sequence-to-sequence (seq2seq) model (2014) does this with two RNNs: an encoder that reads and a decoder that writes. It was a breakthrough, but it has one weak spot, the context vector. We train a real model and watch that weakness appear, which sets up Module 5.

In this session

  • Encoder–decoder architecture
  • Training with teacher forcing; generating one token at a time
  • A real seq2seq model trained in PyTorch
  • The context-vector bottleneck, measured
  • Where sequence models break, and what attention changes

01The seq2seq architecture

मेराencखाताencबंदencहैencccontext vector(one fixed size)decmydecaccountdecisdecclosedENCODER reads the inputDECODER writes the output
Seq2seq: the encoder compresses the whole Hindi sentence into one context vector c; the decoder writes the English translation from it, one word at a time.
📞

Like translating over a phone call with a bad line. Your friend hears a whole paragraph, then must summarise it into one short voice note before you translate it. A short sentence survives; a long paragraph loses details.

02Teacher forcing and generation

Training: teacher forcing• decoder gets the CORRECT previous word• all steps can be computed together• fast and stableInference: generation• decoder gets its OWN previous word• one step at a time• an early mistake can snowball
The same difference you saw for transformer decoders in Session 21.

Let us train a tiny GRU seq2seq model to reverse digit sequences (3 1 4 1 5 → 5 1 4 1 3). We train on lengths 3–12, then test on several lengths:

seq2seq_reverse.py
# A tiny encoder-decoder (GRU) that learns to REVERSE a sequence of digits: 3 1 4 -> 4 1 3
import torch, torch.nn as nn
torch.manual_seed(0); torch.set_num_threads(1)     # reproducible results
V, SOS, H = 12, 10, 64                 # digits 0-9, 10 = start token, 11 = padding

class Seq2Seq(nn.Module):
    def __init__(self):
        super().__init__()
        self.emb = nn.Embedding(V, 32)
        self.encoder = nn.GRU(32, H, batch_first=True)
        self.decoder = nn.GRU(32, H, batch_first=True)
        self.out = nn.Linear(H, V)
    def forward(self, src, tgt_in):
        _, context = self.encoder(self.emb(src))          # the WHOLE input squeezed into one vector
        dec, _ = self.decoder(self.emb(tgt_in), context)   # teacher forcing: feed the right previous token
        return self.out(dec)
    @torch.no_grad()
    def generate(self, src):
        _, h = self.encoder(self.emb(src)); tok = torch.full((src.size(0), 1), SOS); outs = []
        for _ in range(src.size(1)):
            d, h = self.decoder(self.emb(tok), h); tok = self.out(d).argmax(-1); outs.append(tok)
        return torch.cat(outs, 1)

def batch(n, length):
    src = torch.randint(0, 10, (n, length)); tgt = src.flip(1)
    return src, torch.cat([torch.full((n, 1), SOS), tgt[:, :-1]], 1), tgt

model = Seq2Seq(); opt = torch.optim.Adam(model.parameters(), 3e-3); lossf = nn.CrossEntropyLoss()
for step in range(3000):
    L = torch.randint(3, 13, (1,)).item()                  # train on lengths 3-12
    src, tgt_in, tgt = batch(64, L)
    loss = lossf(model(src, tgt_in).reshape(-1, V), tgt.reshape(-1))
    opt.zero_grad(); loss.backward(); opt.step()

s = torch.tensor([[3, 1, 4, 1, 5]]); print("input", s.tolist()[0], "-> output", model.generate(s).tolist()[0])
for L in [4, 8, 12, 16, 20]:
    src, _, tgt = batch(500, L)
    acc = (model.generate(src) == tgt).all(1).float().mean().item()
    print(f"length {L:2d}: {acc * 100:5.1f}% of sequences reversed perfectly")
input [3, 1, 4, 1, 5] -> output [5, 1, 4, 1, 3] length 4: 100.0% of sequences reversed perfectly length 8: 70.2% of sequences reversed perfectly length 12: 7.6% of sequences reversed perfectly length 16: 0.2% of sequences reversed perfectly length 20: 0.0% of sequences reversed perfectly

03The context-vector bottleneck

Look at the accuracy: perfect at length 4, about 70% at length 8, and under 10% at length 12, even though length 12 was in the training data. Lengths 16 and 20 were never seen in training, so near 0% is expected there. Everything the decoder knows must squeeze through one vector of 64 numbers.

Demo · Accuracy vs sequence length (from the run above)
Real translation systems showed the same thing: Cho et al. (2014) found that RNN encoder–decoder quality drops sharply as sentences get longer. Sutskever et al. even reversed the input sentence as a trick to help.
LimitationWhy it hurtsFixed by
Fixed-size context vectorlong inputs are compressed and details lostattention (Bahdanau, 2015): the decoder looks back at every encoder state
Sequential processingno parallelism: slow to train on GPUstransformers (2017): process all tokens at once
Long-range memorygradients still fade over very long textself-attention: any token reaches any other in one step

04What this implies for the next module

The fix began with a simple idea: instead of one context vector, let the decoder look back at all encoder hidden states and decide which ones matter for the current word. That is attention. Then the 2017 paper “Attention Is All You Need” asked: what if we drop the RNN entirely and use only attention?

RNNmemory fadesLSTM / GRUgates helpSeq2seqone bottleneck vector+ Attentionlook at every inputTransformerattention only, parallel
The road from Module 4 to Module 5.
Up next, Session 17: self-attention, where every word looks directly at every other word. You now know exactly which problems it solves.

05Summary

06Quick quiz

1. What is the context vector?

2. Teacher forcing means the decoder is fed…

3. Our model reversed 100% at length 4 but under 10% at length 12. Why?

4. Which idea lets the decoder look at every encoder state?

5. What else do transformers fix compared with RNNs?

07References

Created by Ajeetkumar