Lesson 103: Seq2Seq Encoder-Decoder with Bahdanau Attention

USAAIO Lesson 103, from Phase 3, on the seq2seq encoder-decoder architecture, in which a GRU encoder reads the source and a GRU decoder generates the target one token at a time. It covers Bahdanau additive attention through its energy scores, softmax alpha, and context vector, then teacher forcing for fast training convergence, exposure bias and the mismatch between training and inference, scheduled sampling to bridge that gap, and the copy mechanism as an attention-based pointer to source tokens. All the attention weights, copy probabilities, and decoder-step predictions were verified with torch 2.7.1+cpu at seed 99. The lesson runs to 31 slides.

Subject: Machine Learning · 59 slides · code lesson

Open the interactive version of this deck · Homework for this lesson

What this lesson covers

The lesson, slide by slide

1. Seq2Seq Encoder-Decoder + Bahdanau Attention

Title

USAAIO -- Lesson 103 -- Phase 3

The architecture behind neural machine translation, summarization, and code generation: a GRU encoder compresses the source into hidden states, a decoder attends over them to generate each target token. Today: attention math, teacher forcing, exposure bias, scheduled sampling, and copy mechanisms.

2. By the end of this lesson you can

Objectives

  1. Trace the seq2seq forward pass (encoder hidden states -> context -> decoder output) with concrete tensor shapes
  2. Compute Bahdanau attention energy scores, softmax weights, and the context vector by hand
  3. Explain teacher forcing: why it speeds convergence and what the training objective actually is
  4. Define exposure bias and explain why it causes train-inference mismatch in seq2seq models
  5. Describe scheduled sampling as a remedy and state the linear decay schedule
  6. Explain the copy mechanism as an attention-based pointer distribution over source tokens

3. What survived from RLHF & DPO — Instruction Fine-Tuning?

Warm-up

Discussion prompt

Before we open Lesson 103: Seq2Seq Encoder-Decoder with Bahdanau Attention: without looking back, what was the main idea of RLHF & DPO — Instruction Fine-Tuning, and what could you do by the end of it that you could not do before?

Hint: One sentence for the idea, one for the skill. If the second one is blank, that is the part to revisit.

Answer:

full RLHF pipeline (SFT → reward model → PPO with KL penalty), Bradley-Terry pairwise loss, shaped reward r_env − β·KL, and DPO as a closed-form alternative that eliminates the reward model.

4. Seq2Seq architecture -- encoder to decoder

Section

Part 1 of 4

5. The encoder-decoder idea

Concept

A seq2seq model maps a variable-length input sequence to a variable-length output sequence. Unlike classification (fixed output), both lengths are unknown at design time: a 7-word English sentence can become a 5-word French sentence or a 20-word German one.

componentinputoutputshape (B=1)
GRU Encodersrc tokens (T,)h_1..h_T + final h(1, T, H) + (1,1,H)
Bahdanau Attnh_1..h_T, dec_hidalpha, context(T,) + (H,)
GRU Decoderprev token + contextnext-step hidden(1, H)
Linear headdec_hid + contextlogits over vocab(1, V)

6. Fill in: input for The encoder-decoder idea

Comparison

Comparison matrix

From The encoder-decoder idea: refill the input column from what you know. The rest of the table is as it appeared.

componentinputoutputshape (B=1)
GRU Encodersrc tokens (T,)h_1..h_T + final h(1, T, H) + (1,1,H)
Bahdanau Attnh_1..h_T, dec_hidalpha, context(T,) + (H,)
GRU Decoderprev token + contextnext-step hidden(1, H)
Linear headdec_hid + contextlogits over vocab(1, V)

7. Bahdanau (additive) attention

Concept

At decoder step t, Bahdanau attention assigns a soft weight alpha_i to each encoder hidden state h_i. The weighted sum is the context vector fed into the decoder GRU alongside the input embedding.

\[ e_{t,i} = v^\top \tanh(W_h\, h_i + W_s\, s_{t-1}) \]

\[ \alpha_{t,i} = \frac{\exp(e_{t,i})}{\sum_{j=1}^{T}\exp(e_{t,j})}, \quad c_t = \sum_{i=1}^{T} \alpha_{t,i}\, h_i \]

Three learned parameters: W_h in R^{H x H}, W_s in R^{H x H}, v in R^H. No query-key dot product -- the additive tanh(W_h h + W_s s) is the distinguishing feature of Bahdanau vs Luong (dot-product) attention.

8. Guess the shape of the answer: Bahdanau attention -- numeric trace

Estimation

Predict first

Toy example: 3-token source, H=4. All weights drawn from torch.randn with torch.manual_seed(0). Predict which source position gets the highest attention weight before reading on.

Commit before you compute: what does Bahdanau attention -- numeric trace come out to? A rough magnitude and the right form is enough — the point is to have something concrete to be wrong about.

Correct: energy = [-0.8377, 1.2709, -0.4288]; softmax alpha = [0.0931, 0.7668, 0.1401]

Why: A prediction you can defend turns the computation into a check rather than a leap of faith — and an answer that contradicts it is caught on the spot. Token 1 (0-indexed) receives 76.68% of attention mass -- the decoder strongly attends to the second source token.

9. Bahdanau attention -- numeric trace

Worked example

Toy example: 3-token source, H=4. All weights drawn from torch.randn with torch.manual_seed(0). Predict which source position gets the highest attention weight before reading on.

import torch
torch.manual_seed(0)
SRC_LEN, H = 3, 4
h  = torch.randn(SRC_LEN, H)  # encoder states
s  = torch.randn(H)           # decoder hidden state
Wh = torch.randn(H, H)
Ws = torch.randn(H, H)
v  = torch.randn(H)
Wh_h   = h @ Wh.T                     # (3, H)
Ws_s   = (Ws @ s).unsqueeze(0)        # (1, H) broadcasts
energy = torch.tanh(Wh_h + Ws_s) @ v  # (3,)
alpha  = torch.softmax(energy, dim=0)
ctx    = (alpha.unsqueeze(1) * h).sum(0)
print([round(x,4) for x in energy.tolist()])
print([round(x,4) for x in alpha.tolist()])
print([round(x,4) for x in ctx.tolist()])

energy = [-0.8377, 1.2709, -0.4288]; softmax alpha = [0.0931, 0.7668, 0.1401]

Why: Token 1 (0-indexed) receives 76.68% of attention mass -- the decoder strongly attends to the second source token. This corresponds to the highest energy score of 1.2709.

src positionenergyalpha (weight)contribution to ctx
0-0.83770.0931 (9.3%)small
1 1.27090.7668 (76.7%)dominant
2-0.42880.1401 (14.0%)minor

10. What each one costs: Bahdanau attention -- numeric trace

Trade off

Comparison matrix

From Bahdanau attention -- numeric trace: every row here is a choice with a cost. Fill the contribution to ctx column, then say which row you would actually pick and what you give up for it.

src positionenergyalpha (weight)contribution to ctx
0-0.83770.0931 (9.3%)small
11.27090.7668 (76.7%)dominant
2-0.42880.1401 (14.0%)minor

11. Something is wrong here: Bahdanau vs Luong attention

Anomaly

Predict first

A student writes this, and it looks reasonable:

Bahdanau attention computes energy as score = h_i^T s_t (dot product of encoder and decoder hidden). If H differs between encoder and decoder, just project one side.

It is wrong. Say what breaks — and say it before you turn the page.

Correct: That is Luong (multiplicative) attention, not Bahdanau.

Bahdanau (additive) attention uses tanh(W_h h_i + W_s s_t) projected by v. Two learned weight matrices, a tanh nonlinearity, and a learned vector v.

Why: That is Luong (multiplicative) attention, not Bahdanau. Confusing them is extremely common on olympiad short-answer sections.

12. Trap: Bahdanau vs Luong attention

Trap

The trap

Bahdanau attention computes energy as score = h_i^T s_t (dot product of encoder and decoder hidden). If H differs between encoder and decoder, just project one side.

energy_i = dot(h_i, s_t)

Why: That is Luong (multiplicative) attention, not Bahdanau. Confusing them is extremely common on olympiad short-answer sections.

The fix

Bahdanau (additive) attention uses tanh(W_h h_i + W_s s_t) projected by v. Two learned weight matrices, a tanh nonlinearity, and a learned vector v.

energy_i = v^T tanh(W_h h_i + W_s s_{t-1}); alpha = softmax(energy)

Why: Additive = two projections summed under tanh. Luong = dot/bilinear. Both give a distribution over source positions, but the parameter count and expressiveness differ. Bahdanau is the original 2015 formulation.

13. Break it on purpose: Bahdanau vs Luong attention

Break the constraint

Discussion prompt

The rule this trap just fixed:

Bahdanau (additive) attention uses tanh(W_h h_i + W_s s_t) projected by v. Two learned weight matrices, a tanh nonlinearity, and a learned vector v.

Now break it on purpose. Build a case that violates it and follow the consequences until something visibly fails. Where does the failure first show up — and would you have noticed it if you had not been looking?

Hint: The dangerous rules are the ones whose violation still produces an answer. If yours fails loudly, try to find one that fails quietly.

Answer:

That is Luong (multiplicative) attention, not Bahdanau. Confusing them is extremely common on olympiad short-answer sections.

14. Teacher forcing & exposure bias

Section

Part 2 of 4

15. Teacher forcing -- the training trick

Concept

At training time, the decoder knows the ground-truth target sequence. Teacher forcing feeds the true previous token as the decoder's input at each step, regardless of what the model actually predicted.

training step tdecoder input (TF)decoder input (free-run)
t=0<BOS> (always)<BOS> (always)
t=1y_1 (ground truth)predicted token from t=0
t=2y_2 (ground truth)predicted token from t=1
t=3y_3 (ground truth)predicted token from t=2

16. Exposure bias -- the train-inference gap

Concept

At inference time, there is no ground-truth available. The decoder must use its own previous prediction as the next input -- the free-running regime.

This creates exposure bias: the decoder was never exposed to its own errors during training, so it has never learned to recover from them. One early mistake cascades through the rest of the sequence.

regimestep t inputseen during training?consequence
teacher forcingy_{t-1} (GT)yesfast convergence, clean gradients
free-run (infer)y-hat_{t-1} (model)noerror accumulation, BLEU drop
scheduled samp.mix (GT or model)yes (both)bridges the gap

17. Break it if you can: Exposure bias -- the train-inference gap

Counterexample

Discussion prompt

At inference time, there is no ground-truth available. The decoder must use its own previous prediction as the next input -- the free-running regime.

That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.

Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.

18. What has to be given first: Teacher forcing vs free-run -- divergence…

Missing information

Discussion prompt

Tiny seq2seq: VOCAB=10, E=6, H=8, src=[2,5,1,3], tgt=[0,4,6,3] (BOS then real tokens). With torch.manual_seed(99): compare TF inputs to free-run inputs over 3 steps.

What do you need to know — or decide — before the first line can be written? List everything the problem has to hand you.

Hint: Anything you would have to invent to get started is a thing the problem must supply.

Answer:

Free-run feeds its own prediction (3) back as the step-1 input instead of ground-truth (4). By step 2, the input token differs entirely. The decoder is in distribution-shifted territory.

19. Teacher forcing vs free-run -- divergence demo

Worked example

Tiny seq2seq: VOCAB=10, E=6, H=8, src=[2,5,1,3], tgt=[0,4,6,3] (BOS then real tokens). With torch.manual_seed(99): compare TF inputs to free-run inputs over 3 steps.

import torch, torch.nn as nn
torch.manual_seed(99)
VOCAB, E, H = 10, 6, 8
enc = Encoder(VOCAB, E, H)   # defined in milestone
dec = Decoder(VOCAB, E, H)
src = torch.tensor([[2, 5, 1, 3]])
tgt_seq = torch.tensor([[0, 4, 6, 3]])
enc_out, enc_hid = enc(src)
for mode in ['teacher_forcing', 'free_run']:
    hid = enc_hid
    inp_list, pred_list = [], []
    tok = tgt_seq[:, 0:1]  # BOS
    for t in range(3):
        logit, hid, _ = dec(tok, hid, enc_out)
        pred = logit.argmax(-1).item()
        inp_list.append(tok.item())
        pred_list.append(pred)
        tok = (tgt_seq[:, t+1:t+2] if mode == 'teacher_forcing'
               else torch.tensor([[pred]]))
    print(f'{mode}: inputs={inp_list}, preds={pred_list}')

teacher_forcing: inputs=[0, 4, 6], preds=[3, 0, 0]; free_run: inputs=[0, 3, 2], preds=[3, 2, 3]

Why: Free-run feeds its own prediction (3) back as the step-1 input instead of ground-truth (4). By step 2, the input token differs entirely. The decoder is in distribution-shifted territory.

stepTF inputTF predFR inputFR predsame?
00 (BOS)30 (BOS)3yes
14 (GT)03 (own)2no
26 (GT)02 (own)3no

20. Watch it run: Teacher forcing vs free-run -- divergence demo

Pattern

Step through it

Step through Teacher forcing vs free-run -- divergence demo one row at a time. What is driving the change, and what would the row after the last one be?

  1. Step 1: step is 0
  2. Step 2: step is 1
  3. Step 3: step is 2

21. Scheduled sampling & copy mechanism

Section

Part 3 of 4

22. Scheduled sampling -- bridging the gap

Concept

Scheduled sampling (Bengio 2015) mixes teacher forcing and free-running during training. At each step, with probability epsilon the decoder receives the ground-truth token; with probability 1 - epsilon it receives its own prediction.

\[ \text{input}_t = \begin{cases} y_{t-1} & \text{with prob } \epsilon_k \\ \hat{y}_{t-1} & \text{with prob } 1 - \epsilon_k \end{cases} \]

epsilon_k is annealed from 1.0 (pure teacher forcing) toward 0.0 (pure free-run) as training epoch k increases. The most common schedule is linear: epsilon_k = max(0, 1 - k/K).

23. By analogy: Scheduled sampling -- bridging the gap

Analogy

Discussion prompt

Explain Scheduled sampling -- bridging the gap by analogy to something with no Machine Learning in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.

Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.

Answer:

epsilon_k is annealed from 1.0 (pure teacher forcing) toward 0.0 (pure free-run) as training epoch k increases. The most common schedule is linear: epsilon_k = max(0, 1 - k/K).

24. Guess the shape of the answer: Scheduled sampling -- linear decay trace

Estimation

Predict first

Linear schedule: epsilon_k = max(0, 1 - k/K) with K=100. At each epoch, a Bernoulli draw decides which input token the decoder sees. Trace epsilon across epochs.

Commit before you compute: what does Scheduled sampling -- linear decay trace come out to? A rough magnitude and the right form is enough — the point is to have something concrete to be wrong about.

Correct: At epoch 25, epsilon=0.75 (threshold between pure teacher and mixed); by epoch 75, epsilon=0.25 (mostly free-run)

Why: A prediction you can defend turns the computation into a check rather than a leap of faith — and an answer that contradicts it is caught on the spot. The annealing forces the model to experience increasingly realistic (self-generated) inputs as training progresses.

25. Scheduled sampling -- linear decay trace

Worked example

Linear schedule: epsilon_k = max(0, 1 - k/K) with K=100. At each epoch, a Bernoulli draw decides which input token the decoder sees. Trace epsilon across epochs.

import numpy as np
K = 100
steps = [0, 10, 25, 50, 75, 100]
print(f"{'epoch':>8} {'epsilon':>10} {'mode':>12}")
for k in steps:
    eps = max(0.0, 1.0 - k / K)
    mode = ('teacher' if eps > 0.75
            else 'mixed' if eps > 0.25
            else 'free-run')
    print(f"{k:>8} {eps:>10.2f} {mode:>12}")

At epoch 25, epsilon=0.75 (threshold between pure teacher and mixed); by epoch 75, epsilon=0.25 (mostly free-run)

Why: The annealing forces the model to experience increasingly realistic (self-generated) inputs as training progresses. By the final epoch (eps=0), training IS free-run -- no more exposure bias.

epochepsilon% teacher tokensmode
01.00100%pure teacher
100.90 90%teacher
250.75 75%mixed
500.50 50%mixed
750.25 25%free-run
1000.00 0%pure free-run

26. Watch it run: Scheduled sampling -- linear decay trace

Pattern

Step through it

Step through Scheduled sampling -- linear decay trace one row at a time. What is driving the change, and what would the row after the last one be?

  1. Step 1: epoch is 0
  2. Step 2: epoch is 10
  3. Step 3: epoch is 25
  4. Step 4: epoch is 50
  5. Step 5: epoch is 75
  6. Step 6: epoch is 100

27. Copy mechanism -- pointing to the source

Concept

In summarization and question answering, the decoder often needs to copy a token verbatim from the input (named entities, numbers, rare words not in the vocab). The copy mechanism (Gu 2016, Vinyals pointer networks 2015) reuses the attention distribution as a pointer to source positions.

p_genbehaviortypical use
~1.0generate from vocabcommon words, grammar words
~0.5mix: attend + generatepartially OOV tokens
~0.0copy from sourcenamed entities, numbers, OOV

28. Where does each piece belong: Lesson 103: Seq2Seq Encoder-Decoder with…

Sorting

Sort into buckets

These are the pieces of Lesson 103: Seq2Seq Encoder-Decoder with Bahdanau Attention, out of order. Put each one back under the part of the lesson it belongs to.

Seq2Seq architecture -- encoder to decoder
The encoder-decoder idea; Bahdanau (additive) attention; Bahdanau attention -- numeric trace
Teacher forcing & exposure bias
Teacher forcing -- the training trick; Exposure bias -- the train-inference gap; Teacher forcing vs free-run -- divergence demo
Scheduled sampling & copy mechanism
Scheduled sampling -- bridging the gap; Scheduled sampling -- linear decay trace; Copy mechanism -- pointing to the source
s1
Seq2Seq architecture -- encoder to decoder is where Lesson 103: Seq2Seq Encoder-Decoder with Bahdanau Attention puts The encoder-decoder idea, Bahdanau (additive) attention, Bahdanau attention -- numeric trace. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.
s2
Teacher forcing & exposure bias is where Lesson 103: Seq2Seq Encoder-Decoder with Bahdanau Attention puts Teacher forcing -- the training trick, Exposure bias -- the train-inference gap, Teacher forcing vs free-run -- divergence demo. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.
s3
Scheduled sampling & copy mechanism is where Lesson 103: Seq2Seq Encoder-Decoder with Bahdanau Attention puts Scheduled sampling -- bridging the gap, Scheduled sampling -- linear decay trace, Copy mechanism -- pointing to the source. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.

29. Guess the shape of the answer: Copy mechanism -- pointer distribution

Estimation

Predict first

Source: ['the', 'customer', 'ordered', 'tea']. Simulate the copy distribution: torch.manual_seed(1), 4-dim softmax over source positions. Which token does the model most likely copy?

Commit before you compute: what does Copy mechanism -- pointer distribution come out to? A rough magnitude and the right form is enough — the point is to have something concrete to be wrong about.

Correct: copy probs: 'the'=0.3141, 'customer'=0.2117, 'ordered'=0.1724, 'tea'=0.3018; most-likely copy: 'the'

Why: A prediction you can defend turns the computation into a check rather than a leap of faith — and an answer that contradicts it is caught on the spot. The attention distribution acts as a pointer.

30. Copy mechanism -- pointer distribution

Worked example

Source: ['the', 'customer', 'ordered', 'tea']. Simulate the copy distribution: torch.manual_seed(1), 4-dim softmax over source positions. Which token does the model most likely copy?

import torch
torch.manual_seed(1)
src_tokens = ['the', 'customer', 'ordered', 'tea']
attn_logits = torch.randn(4)          # from attention module
copy_probs  = torch.softmax(attn_logits, dim=0)
for tok, p in zip(src_tokens, copy_probs.tolist()):
    print(f"  '{tok}': {round(p, 4)}")
best = src_tokens[copy_probs.argmax().item()]
print(f"  -> most-likely copied: '{best}'")

copy probs: 'the'=0.3141, 'customer'=0.2117, 'ordered'=0.1724, 'tea'=0.3018; most-likely copy: 'the'

Why: The attention distribution acts as a pointer. With p_gen near 0, the decoder directly samples from {source position probabilities} -- effectively a soft lookup into the input sequence.

source tokencopy probabilityrank
the0.31411st
tea0.30182nd
customer0.21173rd
ordered0.17244th

31. Fill in: rank for Copy mechanism -- pointer distribution

Comparison

Comparison matrix

From Copy mechanism -- pointer distribution: refill the rank column from what you know. The rest of the table is as it appeared.

source tokencopy probabilityrank
the0.31411st
tea0.30182nd
customer0.21173rd
ordered0.17244th

32. Something is wrong here: teacher forcing causes overfitting

Anomaly

Predict first

A student writes this, and it looks reasonable:

Teacher forcing is equivalent to feeding the ground truth at every step, so the model memorizes the training sequences. At inference the model is fine because it has learned the exact target distribution.

It is wrong. Say what breaks — and say it before you turn the page.

Correct: The model has never seen its own errors.

Teacher forcing causes exposure bias, not overfitting. The issue is a distribution mismatch between training (always sees GT tokens) and inference (sees own predictions).

Why: The model has never seen its own errors. At inference it generates a wrong token at t=1, then receives that wrong token as input at t=2 -- a distribution it never encountered during training. Quality collapses on longer sequences.

33. Trap: teacher forcing causes overfitting

Trap

The trap

Teacher forcing is equivalent to feeding the ground truth at every step, so the model memorizes the training sequences. At inference the model is fine because it has learned the exact target distribution.

Use teacher forcing for all 200 epochs; at test time use the model directly

Why: The model has never seen its own errors. At inference it generates a wrong token at t=1, then receives that wrong token as input at t=2 -- a distribution it never encountered during training. Quality collapses on longer sequences.

The fix

Teacher forcing causes exposure bias, not overfitting. The issue is a distribution mismatch between training (always sees GT tokens) and inference (sees own predictions).

Anneal with scheduled sampling: epsilon_k from 1.0 to 0.0 over K epochs

Why: Scheduled sampling gradually exposes the model to its own predictions during training. At eval time the distribution is familiar -- the model has practiced recovering from its own mistakes. BLEU scores on long outputs improve substantially over pure teacher forcing.

34. Which of these survive contact with Lesson 103: Seq2Seq Encoder-Decoder with…?

Two truths and a lie

Sort into buckets

Some of these hold up and some are the exact mistakes this lesson is built to prevent. Sort them.

Holds up
At inference time, there is no ground-truth available. The decoder must use its own previous prediction as the next input -- the free-running regime.; Concrete shapes for B=1, src_len=4, H=8, E=6, VOCAB=10 (our toy from the worked example).; Build a complete Encoder + BahdanauAttn + Decoder from scratch in PyTorch. Three milestones: attention math -> full forward pass -> teacher-forcing training loop.
Breaks
Bahdanau attention computes energy as score = h_i^T s_t (dot product of encoder and decoder hidden). If H differs between encoder and decoder, just project one side.; Use teacher forcing for all 200 epochs; at test time use the model directly
sound
These are stated as this lesson states them — each one survives the edge cases Lesson 103: Seq2Seq Encoder-Decoder with Bahdanau Attention puts it through.
flawed
Each of these is lifted from a trap in this deck: reasonable-sounding, and wrong in a way that only shows up once you rely on it.

35. Full Seq2Seq in PyTorch from scratch

Section

Part 4 of 4

36. Architecture summary: shapes at every step

Concept

Concrete shapes for B=1, src_len=4, H=8, E=6, VOCAB=10 (our toy from the worked example).

operationinput shapeoutput shapenote
Encoder Embedding(1, 4)(1, 4, 6)token IDs -> dense vectors
Encoder GRU(1, 4, 6)enc_out (1,4,8), hid (1,1,8)all hidden states + final
Bahdanau Attn(1,4,8) + (1,8)alpha (1,4), ctx (1,8)soft-align over src
Decoder GRU input(1,1,6+8)=(1,1,14)(1,1,8)emb cat ctx
Decoder head(1, 8+8)=(1,16)(1, 10)concat [dec_out, ctx]

Total parameters (VOCAB=10, E=6, H=8): encoder=444, decoder=942, total=1,386 (verified with sum(p.numel()...)). Scale to VOCAB=30000, H=512 for real NMT: ~30M params.

37. By analogy: Architecture summary: shapes at every step

Analogy

Discussion prompt

Explain Architecture summary: shapes at every step by analogy to something with no Machine Learning in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.

Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.

Answer:

Concrete shapes for B=1, src_len=4, H=8, E=6, VOCAB=10 (our toy from the worked example).

38. Without one step: The seq2seq + attention recipe

Constraint

Discussion prompt

Run The seq2seq + attention recipe with this step confiscated:

Output head: Linear(H*2, V) applied to cat([dec_out, ctx]) -- concatenating context again improves performance.

Is it still possible? If it is, say what takes its place and what it costs you. If it is not, say exactly what that step was providing that nothing else does.

Hint: A step you can drop for free was never load-bearing. If you cannot drop it, name the thing that goes wrong the moment it is gone.

Answer:

  1. Encoder: nn.Embedding + nn.GRU(batch_first=True). Keep ALL hidden states enc_out (B,T,H), not just the final one.
  2. Bahdanau attention: at each decoder step, energy = v^T tanh(W_h enc_out + W_s s), alpha = softmax(energy), ctx = sum(alpha * enc_out).
  3. Decoder GRU input: cat([embed(prev_token), ctx], dim=-1) -- (E+H)-dim input at each step.
  4. Output head: Linear(H*2, V) applied to cat([dec_out, ctx]) -- concatenating context again improves performance.
  5. Teacher forcing (training): feed y_{t-1} (GT) as decoder input; cross-entropy loss on predicted vocab distribution.
  6. Scheduled sampling: use Bernoulli(epsilon_k) to decide GT vs model prediction; anneal epsilon_k linearly from 1.0 to 0.0.
  7. Copy mechanism (if needed): gate p_gen * P_vocab + (1-p_gen) * sum_i alpha_i [src_i=w] over the output distribution.

39. The seq2seq + attention recipe

Pattern

  1. Encoder: nn.Embedding + nn.GRU(batch_first=True). Keep ALL hidden states enc_out (B,T,H), not just the final one.
  2. Bahdanau attention: at each decoder step, energy = v^T tanh(W_h enc_out + W_s s), alpha = softmax(energy), ctx = sum(alpha * enc_out).
  3. Decoder GRU input: cat([embed(prev_token), ctx], dim=-1) -- (E+H)-dim input at each step.
  4. Output head: Linear(H*2, V) applied to cat([dec_out, ctx]) -- concatenating context again improves performance.
  5. Teacher forcing (training): feed y_{t-1} (GT) as decoder input; cross-entropy loss on predicted vocab distribution.
  6. Scheduled sampling: use Bernoulli(epsilon_k) to decide GT vs model prediction; anneal epsilon_k linearly from 1.0 to 0.0.
  7. Copy mechanism (if needed): gate p_gen * P_vocab + (1-p_gen) * sum_i alpha_i [src_i=w] over the output distribution.

40. Where does it stop working: The seq2seq + attention recipe

Edge cases

Discussion prompt

The seq2seq + attention recipe works on the cases you have just seen. Push it to the edge: what is the most degenerate input it still handles — empty, zero, one item, everything equal — and what is the first case where it stops being true? Name the case, not just "it breaks".

Hint: Try the smallest legal input, then the largest, then the one where two things collide. Methods are specified at their edges; the middle takes care of itself.

Answer:

  1. Encoder: nn.Embedding + nn.GRU(batch_first=True). Keep ALL hidden states enc_out (B,T,H), not just the final one.
  2. Bahdanau attention: at each decoder step, energy = v^T tanh(W_h enc_out + W_s s), alpha = softmax(energy), ctx = sum(alpha * enc_out).
  3. Decoder GRU input: cat([embed(prev_token), ctx], dim=-1) -- (E+H)-dim input at each step.
  4. Output head: Linear(H*2, V) applied to cat([dec_out, ctx]) -- concatenating context again improves performance.
  5. Teacher forcing (training): feed y_{t-1} (GT) as decoder input; cross-entropy loss on predicted vocab distribution.
  6. Scheduled sampling: use Bernoulli(epsilon_k) to decide GT vs model prediction; anneal epsilon_k linearly from 1.0 to 0.0.
  7. Copy mechanism (if needed): gate p_gen * P_vocab + (1-p_gen) * sum_i alpha_i [src_i=w] over the output distribution.

41. Rule out three: Check yourself -- Bahdanau energy

Elimination

Eliminate the wrong options

In Bahdanau attention, the energy score e_{t,i} is computed as:

3 of these 4 are wrong. Strike them one at a time, and say what rules each one out before you strike the next. The survivor is the answer.

  • A. v^T tanh(W_h h_i + W_s s_{t-1})
  • B. h_i^T s_{t-1} / sqrt(H)
  • C. softmax(h_i^T W s_{t-1})
  • D. ReLU(W_h h_i) dot ReLU(W_s s_{t-1})

Survives elimination: A

Why: Bahdanau additive attention: project each encoder state h_i with W_h, project the decoder hidden s_{t-1} with W_s, sum under tanh (nonlinearity), then dot with the learned vector v to collapse to a scalar energy. Verified in the numeric trace: energy = [-0.8377, 1.2709, -0.4288] for our toy example.

42. Check yourself -- Bahdanau energy

Check

Compute on paper before clicking.

Check your understanding

In Bahdanau attention, the energy score e_{t,i} is computed as:

  • A. v^T tanh(W_h h_i + W_s s_{t-1}) (correct)
  • B. h_i^T s_{t-1} / sqrt(H)
  • C. softmax(h_i^T W s_{t-1})
  • D. ReLU(W_h h_i) dot ReLU(W_s s_{t-1})

Answer: A

Why: Bahdanau additive attention: project each encoder state h_i with W_h, project the decoder hidden s_{t-1} with W_s, sum under tanh (nonlinearity), then dot with the learned vector v to collapse to a scalar energy. Verified in the numeric trace: energy = [-0.8377, 1.2709, -0.4288] for our toy example.

Why B tempts people
That is scaled dot-product attention (Vaswani 2017 / Luong multiplicative). Dividing by sqrt(H) is not part of Bahdanau's formulation.
Why C tempts people
The softmax is applied to the energies to get alpha, not inside the energy computation itself. Applying softmax here would make the energy non-differentiable in the standard sense.
Why D tempts people
ReLU is not used in Bahdanau attention. The nonlinearity is tanh, and the final projection is a dot product with v, not another dot product between two ReLU outputs.

43. Answer it before you see the options: Check yourself -- teacher forcing…

Prediction

Predict first

Training a seq2seq model with teacher forcing, the loss at each decoder step is:

Answer it in your own words, now, with nothing to choose from. The options are on the next slide — and picking the right one off a list is an easier skill than producing it.

Correct: Cross-entropy between the predicted logits and the ground-truth next token

Why: At each decoder step t, teacher forcing feeds the ground-truth y_{t-1} and computes cross-entropy between the softmax output and y_t. This is the standard language modeling objective -- the same as Lesson 88 (transformer LM) but applied to the conditional seq2seq distribution P(y_t | y_{<t}, x).

44. Check yourself -- teacher forcing objective

Check

Think about what loss is actually being optimized.

Check your understanding

Training a seq2seq model with teacher forcing, the loss at each decoder step is:

  • A. Cross-entropy between the predicted logits and the ground-truth next token (correct)
  • B. MSE between the predicted hidden state and the encoder's final hidden state
  • C. Cross-entropy between the predicted logits and the model's own previous prediction
  • D. BLEU score between the generated sequence and the reference

Answer: A

Why: At each decoder step t, teacher forcing feeds the ground-truth y_{t-1} and computes cross-entropy between the softmax output and y_t. This is the standard language modeling objective -- the same as Lesson 88 (transformer LM) but applied to the conditional seq2seq distribution P(y_t | y_{<t}, x).

Why B tempts people
MSE on hidden states is not the seq2seq training objective. The model's output is a distribution over the vocabulary, not a continuous embedding to match.
Why C tempts people
With pure teacher forcing, the model's own predictions are never used as inputs -- only ground-truth tokens. Comparing logits to own predictions would be self-distillation, not standard seq2seq training.
Why D tempts people
BLEU is a non-differentiable evaluation metric (it cannot be directly backpropagated through). Training directly on BLEU requires RL-based approaches like REINFORCE -- not standard teacher forcing.

45. Rule out three: Check yourself -- scheduled sampling

Elimination

Eliminate the wrong options

With a linear scheduled sampling decay (epsilon_k = max(0, 1 - k/K), K=100), at epoch 60 the decoder receives the ground-truth token at each step with probability:

3 of these 4 are wrong. Strike them one at a time, and say what rules each one out before you strike the next. The survivor is the answer.

  • A. 0.40
  • B. 0.60
  • C. 1.00
  • D. 0.00

Survives elimination: A

Why: epsilon_60 = max(0, 1 - 60/100) = max(0, 0.40) = 0.40. So with probability 0.40 the decoder sees the ground-truth token, and with probability 0.60 it sees its own last prediction. Verified in the trace table: epoch 50 gives epsilon=0.50, epoch 75 gives epsilon=0.25.

46. Check yourself -- scheduled sampling

Check

Apply the linear schedule formula.

Check your understanding

With a linear scheduled sampling decay (epsilon_k = max(0, 1 - k/K), K=100), at epoch 60 the decoder receives the ground-truth token at each step with probability:

  • A. 0.40 (correct)
  • B. 0.60
  • C. 1.00
  • D. 0.00

Answer: A

Why: epsilon_60 = max(0, 1 - 60/100) = max(0, 0.40) = 0.40. So with probability 0.40 the decoder sees the ground-truth token, and with probability 0.60 it sees its own last prediction. Verified in the trace table: epoch 50 gives epsilon=0.50, epoch 75 gives epsilon=0.25.

Why B tempts people
0.60 is 1 - epsilon_60, which is the probability of feeding the model's OWN prediction -- not the ground-truth token.
Why C tempts people
1.00 is epsilon at epoch 0 (pure teacher forcing). By epoch 60 we have annealed significantly away from pure teacher forcing.
Why D tempts people
0.00 is epsilon at epoch 100 (pure free-run). Epoch 60 is not the end of training under this schedule.

47. Your turn: Seq2Seq from scratch

Section

Project

48. Project: seq2seq with Bahdanau attention

Concept

Build a complete Encoder + BahdanauAttn + Decoder from scratch in PyTorch. Three milestones: attention math -> full forward pass -> teacher-forcing training loop.

#milestonekey tools
1Implement BahdanauAttn, verify alpha sums to 1 over src_lennn.Linear, torch.softmax, tensor shapes
2Implement Encoder + Decoder, trace shapes through a single forward stepnn.GRU batch_first=True, torch.cat, argmax
3Teacher-forcing training loop, compare TF vs free-run predictions on 3 stepsCrossEntropyLoss, zero_grad loop

Build rules: print every tensor shape after each operation; manually verify alpha.sum(-1) == 1; use torch.manual_seed(99) throughout to match the deck's verified numbers.

49. Break it if you can: Project: seq2seq with Bahdanau attention

Counterexample

Discussion prompt

Build a complete Encoder + BahdanauAttn + Decoder from scratch in PyTorch. Three milestones: attention math -> full forward pass -> teacher-forcing training loop.

That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.

Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.

Answer:

Build rules: print every tensor shape after each operation; manually verify alpha.sum(-1) == 1; use torch.manual_seed(99) throughout to match the deck's verified numbers.

50. Milestone 1 -- BahdanauAttn module

Worked example

Your turn: implement BahdanauAttn(H) with three nn.Linear layers (Wh, Ws, v). Given enc_out (B, T, H) and decoder hidden dec_h (B, H), return (alpha, context). Predict: does alpha.sum(dim=1) equal [1.0]?

Hint: self.Ws(dec_h).unsqueeze(1) broadcasts over the time dimension -- shape (B, 1, H) adds to self.Wh(enc_out) of shape (B, T, H) correctly.

import torch, torch.nn as nn

class BahdanauAttn(nn.Module):
    def __init__(self, H):
        super().__init__()
        self.Wh = nn.Linear(H, H, bias=False)
        self.Ws = nn.Linear(H, H, bias=False)
        self.v  = nn.Linear(H, 1, bias=False)
    def forward(self, enc_out, dec_h):
        # enc_out: (B,T,H)   dec_h: (B,H)
        energy = self.v(
            torch.tanh(
                self.Wh(enc_out)
                + self.Ws(dec_h).unsqueeze(1)
            )
        )                         # (B, T, 1)
        alpha = torch.softmax(energy, dim=1)  # (B, T, 1)
        ctx   = (alpha * enc_out).sum(1)       # (B, H)
        return alpha.squeeze(-1), ctx

torch.manual_seed(99)
attn = BahdanauAttn(8)
enc_out = torch.randn(1, 4, 8)
dec_h   = torch.randn(1, 8)
alpha, ctx = attn(enc_out, dec_h)
print(alpha.shape, alpha.sum(dim=1))   # (1,4)  tensor([[1.0]])
print(ctx.shape)                       # (1, 8)
tensorshapesanity check
energy (before softmax)(1, 4, 1)one score per src position
alpha (after softmax)(1, 4, 1)sums to 1 over dim=1
alpha squeezed(1, 4)copy probabilities over src
context vector ctx(1, 8)weighted sum of enc_out rows

51. Which is which, by shape

Discrimination

Sort into buckets

Sort these by shape, from memory, without looking back at Milestone 1 -- BahdanauAttn module. Telling them apart on the spot is the skill; the table is only where the answer happens to be written down.

(1, 4, 1)
energy (before softmax); alpha (after softmax)
(1, 4)
alpha squeezed
(1, 8)
context vector ctx
g1
shape is "(1, 4, 1)" for energy (before softmax), alpha (after softmax) — that is what the table on "Milestone 1 -- BahdanauAttn module" records, and it is the single property separating this group from the rest.
g2
shape is "(1, 4)" for alpha squeezed — that is what the table on "Milestone 1 -- BahdanauAttn module" records, and it is the single property separating this group from the rest.
g3
shape is "(1, 8)" for context vector ctx — that is what the table on "Milestone 1 -- BahdanauAttn module" records, and it is the single property separating this group from the rest.

52. Milestone 2 -- Encoder + Decoder, single step

Worked example

Your turn: implement Encoder and Decoder using the BahdanauAttn you just built. Run one decoder step from BOS and print the predicted token. Predict: with random weights, will the prediction be stable across multiple runs without manual_seed?

Hint: Decoder GRU input is cat([emb(prev_tok), ctx], dim=-1) -- shape (B, 1, E+H). Output head is Linear(H*2, V) applied to cat([dec_gru_out, ctx]).

class Encoder(nn.Module):
    def __init__(self, V, E, H):
        super().__init__()
        self.emb = nn.Embedding(V, E)
        self.gru = nn.GRU(E, H, batch_first=True)
    def forward(self, x):
        return self.gru(self.emb(x))  # (B,T,H), (1,B,H)

class Decoder(nn.Module):
    def __init__(self, V, E, H):
        super().__init__()
        self.emb  = nn.Embedding(V, E)
        self.attn = BahdanauAttn(H)
        self.gru  = nn.GRU(E + H, H, batch_first=True)
        self.fc   = nn.Linear(H * 2, V)
    def forward(self, tok, hid, enc_out):
        e = self.emb(tok)                             # (B,1,E)
        h = hid.squeeze(0)                            # (B,H)
        alpha, ctx = self.attn(enc_out, h)
        inp = torch.cat([e, ctx.unsqueeze(1)], -1)   # (B,1,E+H)
        out, new_hid = self.gru(inp, hid)
        logit = self.fc(
            torch.cat([out.squeeze(1), ctx], -1))    # (B,V)
        return logit, new_hid, alpha

torch.manual_seed(99)
VOCAB, E, H = 10, 6, 8
enc = Encoder(VOCAB, E, H)
dec = Decoder(VOCAB, E, H)
src = torch.tensor([[2, 5, 1, 3]])
enc_out, enc_hid = enc(src)
logit, _, alpha = dec(torch.tensor([[0]]), enc_hid, enc_out)
print('enc_out:', list(enc_out.shape))
print('pred:', logit.argmax(-1).item(), '  alpha:', [round(x,3) for x in alpha[0].tolist()])
tensorshapevalue / note
enc_out[1, 4, 8]4 src positions, H=8
enc_hid[1, 1, 8]final hidden -> init decoder
alpha (step 0)[1, 4][0.232, 0.237, 0.264, 0.267]
pred (step 0)scalar3 (argmax of logits)

53. Milestone 3 -- teacher-forcing training loop

Worked example

Your turn: write the teacher-forcing training loop. At each decoder step, feed the GROUND-TRUTH previous token. Accumulate cross-entropy loss across all steps and backprop once.

Hint: slice tgt_seq[:, t:t+1] as the input at step t (the (t)th ground-truth token, offset by 1 from the target). The loss target is tgt_seq[:, t+1] (the next token to predict).

import torch, torch.nn as nn
torch.manual_seed(99)
VOCAB, E, H, TGT_LEN = 10, 6, 8, 3
enc = Encoder(VOCAB, E, H)
dec = Decoder(VOCAB, E, H)
params = list(enc.parameters()) + list(dec.parameters())
opt = torch.optim.Adam(params, lr=1e-3)
loss_fn = nn.CrossEntropyLoss()
src = torch.tensor([[2, 5, 1, 3]])
tgt = torch.tensor([[0, 4, 6, 3]])  # BOS, tok4, tok6, EOS
for epoch in range(3):   # show 3 epochs
    opt.zero_grad()
    enc_out, hid = enc(src)
    loss = torch.tensor(0.0)
    for t in range(TGT_LEN):
        logit, hid, _ = dec(tgt[:, t:t+1], hid, enc_out)
        loss = loss + loss_fn(logit, tgt[:, t + 1])
    loss.backward(); opt.step()
    print(f'epoch {epoch}: loss = {loss.item():.4f}')
epochlossnote
0~7.3random init; 3 steps x cross-entropy
1lowergrads from teacher forcing update weights
2lower stillconvergence with correct GT inputs

54. What each one costs: Milestone 3 -- teacher-forcing training loop

Trade off

Comparison matrix

From Milestone 3 -- teacher-forcing training loop: every row here is a choice with a cost. Fill the note column, then say which row you would actually pick and what you give up for it.

epochlossnote
0~7.3random init; 3 steps x cross-entropy
1lowergrads from teacher forcing update weights
2lower stillconvergence with correct GT inputs

55. The full program

Concept

import torch, torch.nn as nn
torch.manual_seed(99)

class BahdanauAttn(nn.Module):
    def __init__(self, H):
        super().__init__()
        self.Wh = nn.Linear(H, H, bias=False)
        self.Ws = nn.Linear(H, H, bias=False)
        self.v  = nn.Linear(H, 1, bias=False)
    def forward(self, enc_out, dec_h):
        e = self.v(torch.tanh(self.Wh(enc_out) + self.Ws(dec_h).unsqueeze(1)))
        alpha = torch.softmax(e, dim=1)
        return alpha.squeeze(-1), (alpha * enc_out).sum(1)

class Encoder(nn.Module):
    def __init__(self, V, E, H):
        super().__init__()
        self.emb = nn.Embedding(V, E)
        self.gru = nn.GRU(E, H, batch_first=True)
    def forward(self, x): return self.gru(self.emb(x))

class Decoder(nn.Module):
    def __init__(self, V, E, H):
        super().__init__()
        self.emb  = nn.Embedding(V, E)
        self.attn = BahdanauAttn(H)
        self.gru  = nn.GRU(E + H, H, batch_first=True)
        self.fc   = nn.Linear(H * 2, V)
    def forward(self, tok, hid, enc_out):
        e = self.emb(tok); h = hid.squeeze(0)
        alpha, ctx = self.attn(enc_out, h)
        out, new_hid = self.gru(torch.cat([e, ctx.unsqueeze(1)], -1), hid)
        return self.fc(torch.cat([out.squeeze(1), ctx], -1)), new_hid, alpha

VOCAB, E, H = 10, 6, 8
enc = Encoder(VOCAB, E, H); dec = Decoder(VOCAB, E, H)
opt = torch.optim.Adam(list(enc.parameters())+list(dec.parameters()), lr=1e-3)
loss_fn = nn.CrossEntropyLoss()
src = torch.tensor([[2,5,1,3]]); tgt = torch.tensor([[0,4,6,3]])
for ep in range(200):
    opt.zero_grad(); enc_out, hid = enc(src); loss = torch.tensor(0.)
    for t in range(3):
        logit, hid, _ = dec(tgt[:, t:t+1], hid, enc_out)
        loss = loss + loss_fn(logit, tgt[:, t+1])
    loss.backward(); opt.step()
print(f'final loss: {loss.item():.4f}')
design choicevaluerationale
VOCAB10toy; real NMT uses 30k-50k BPE tokens
H8tiny hidden dim for CPU verification
attentionBahdanauadditive; original NMT attention (2015)
trainingteacher forcingstep uses GT y_{t-1}, not own pred
inferencefree-runfeed own argmax back at each step
epochs200converges to near-zero loss on this toy

56. Fill in: rationale for The full program

Comparison

Comparison matrix

From The full program: refill the rationale column from what you know. The rest of the table is as it appeared.

design choicevaluerationale
VOCAB10toy; real NMT uses 30k-50k BPE tokens
H8tiny hidden dim for CPU verification
attentionBahdanauadditive; original NMT attention (2015)
trainingteacher forcingstep uses GT y_{t-1}, not own pred
inferencefree-runfeed own argmax back at each step
epochs200converges to near-zero loss on this toy

57. Show it off

Concept

Out loud, slides closed: (1) explain the Bahdanau attention computation from energy scores to context vector, naming every matrix and its shape; (2) define exposure bias and state exactly which distribution mismatch causes it; (3) describe scheduled sampling and give the linear decay formula.

Stretch (homework from the lesson plan): implement scheduled sampling -- add a Bernoulli draw at each decoder step and anneal epsilon linearly over 200 epochs; compare final BLEU on a held-out sequence vs pure teacher forcing. Next: Lesson 104 -- advanced NLP and pretraining strategies.

58. Connect it up: Lesson 103: Seq2Seq Encoder-Decoder with Bahdanau Attention

Connect it up

Draw it

One page, no notation unless you need it: draw how these connect — Seq2Seq architecture -- encoder to decoder · Teacher forcing & exposure bias · Scheduled sampling & copy mechanism · Full Seq2Seq in PyTorch from scratch · Your turn: Seq2Seq from scratch. Put an arrow wherever one of them is what makes another possible, and label the arrow with why.

59. What you can do now

Recap

conceptthe one thing to remember
Bahdanau attentionadditive: tanh(W_h h + W_s s) then dot v -- NOT dot-product
teacher forcingfeed y_{t-1} GT at training; decoder never sees its own errors
exposure biastrain/inference mismatch: GT inputs vs own predictions
scheduled samplingepsilon_k = max(0, 1-k/K); mixes GT and model pred each step
copy mechanismp_gen gates vocab vs pointing: (1-p_gen) * alpha over src positions

Sources

  1. USAAIO Year-Long Master Lesson Plan, Lesson 103 -- Seq2Seq Encoder-Decoder — Barron -- USAAIO Round 2 Preparation, 2026
  2. Bahdanau et al. 'Neural Machine Translation by Jointly Learning to Align and Translate' (ICLR 2015) — arXiv:1409.0473
  3. Bengio et al. 'Scheduled Sampling for Sequence Prediction with Recurrent Neural Networks' (NeurIPS 2015) — arXiv:1506.03099
  4. Gu et al. 'Incorporating Copying Mechanism in Sequence-to-Sequence Learning' (ACL 2016) — arXiv:1603.06393
  5. Seq2Seq Bahdanau attention, teacher forcing vs free-run, scheduled sampling, copy mechanism verified with torch 2.7.1+cpu, June 2026 — Real execution, verified

Want this taught 1-on-1? Alexander tutors Machine Learning — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108