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
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.
Objectives
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.
Section
Part 1 of 4
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.
h_T = compressed source summary.<EOS> token.h_T; it attends over ALL encoder hidden states {h_1, ..., h_T} at each generation step.| component | input | output | shape (B=1) |
|---|---|---|---|
| GRU Encoder | src tokens (T,) | h_1..h_T + final h | (1, T, H) + (1,1,H) |
| Bahdanau Attn | h_1..h_T, dec_hid | alpha, context | (T,) + (H,) |
| GRU Decoder | prev token + context | next-step hidden | (1, H) |
| Linear head | dec_hid + context | logits over vocab | (1, V) |
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.
| component | input | output | shape (B=1) |
|---|---|---|---|
| GRU Encoder | src tokens (T,) | h_1..h_T + final h | (1, T, H) + (1,1,H) |
| Bahdanau Attn | h_1..h_T, dec_hid | alpha, context | (T,) + (H,) |
| GRU Decoder | prev token + context | next-step hidden | (1, H) |
| Linear head | dec_hid + context | logits over vocab | (1, V) |
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.
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.
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 position | energy | alpha (weight) | contribution to ctx |
|---|---|---|---|
| 0 | -0.8377 | 0.0931 (9.3%) | small |
| 1 | 1.2709 | 0.7668 (76.7%) | dominant |
| 2 | -0.4288 | 0.1401 (14.0%) | minor |
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 position | energy | alpha (weight) | contribution to ctx |
|---|---|---|---|
| 0 | -0.8377 | 0.0931 (9.3%) | small |
| 1 | 1.2709 | 0.7668 (76.7%) | dominant |
| 2 | -0.4288 | 0.1401 (14.0%) | minor |
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.
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.
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.
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.
Section
Part 2 of 4
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 t | decoder input (TF) | decoder input (free-run) |
|---|---|---|
| t=0 | <BOS> (always) | <BOS> (always) |
| t=1 | y_1 (ground truth) | predicted token from t=0 |
| t=2 | y_2 (ground truth) | predicted token from t=1 |
| t=3 | y_3 (ground truth) | predicted token from t=2 |
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.
| regime | step t input | seen during training? | consequence |
|---|---|---|---|
| teacher forcing | y_{t-1} (GT) | yes | fast convergence, clean gradients |
| free-run (infer) | y-hat_{t-1} (model) | no | error accumulation, BLEU drop |
| scheduled samp. | mix (GT or model) | yes (both) | bridges the 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.
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.
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.
| step | TF input | TF pred | FR input | FR pred | same? |
|---|---|---|---|---|---|
| 0 | 0 (BOS) | 3 | 0 (BOS) | 3 | yes |
| 1 | 4 (GT) | 0 | 3 (own) | 2 | no |
| 2 | 6 (GT) | 0 | 2 (own) | 3 | no |
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?
Section
Part 3 of 4
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).
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).
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.
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.
| epoch | epsilon | % teacher tokens | mode |
|---|---|---|---|
| 0 | 1.00 | 100% | pure teacher |
| 10 | 0.90 | 90% | teacher |
| 25 | 0.75 | 75% | mixed |
| 50 | 0.50 | 50% | mixed |
| 75 | 0.25 | 25% | free-run |
| 100 | 0.00 | 0% | pure free-run |
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?
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_gen: a gating scalar in [0,1] learned from context. Controls how much output comes from the vocabulary vs. copying.P(w) = p_gen * P_vocab(w) + (1 - p_gen) * sum_{i: src_i=w} alpha_i| p_gen | behavior | typical use |
|---|---|---|
| ~1.0 | generate from vocab | common words, grammar words |
| ~0.5 | mix: attend + generate | partially OOV tokens |
| ~0.0 | copy from source | named entities, numbers, OOV |
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.
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.
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 token | copy probability | rank |
|---|---|---|
| the | 0.3141 | 1st |
| tea | 0.3018 | 2nd |
| customer | 0.2117 | 3rd |
| ordered | 0.1724 | 4th |
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 token | copy probability | rank |
|---|---|---|
| the | 0.3141 | 1st |
| tea | 0.3018 | 2nd |
| customer | 0.2117 | 3rd |
| ordered | 0.1724 | 4th |
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.
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.
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.
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.
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.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 directlySection
Part 4 of 4
Concept
Concrete shapes for B=1, src_len=4, H=8, E=6, VOCAB=10 (our toy from the worked example).
| operation | input shape | output shape | note |
|---|---|---|---|
| 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.
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).
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:
nn.Embedding + nn.GRU(batch_first=True). Keep ALL hidden states enc_out (B,T,H), not just the final one.energy = v^T tanh(W_h enc_out + W_s s), alpha = softmax(energy), ctx = sum(alpha * enc_out).cat([embed(prev_token), ctx], dim=-1) -- (E+H)-dim input at each step.Linear(H*2, V) applied to cat([dec_out, ctx]) -- concatenating context again improves performance.y_{t-1} (GT) as decoder input; cross-entropy loss on predicted vocab distribution.p_gen * P_vocab + (1-p_gen) * sum_i alpha_i [src_i=w] over the output distribution.Pattern
nn.Embedding + nn.GRU(batch_first=True). Keep ALL hidden states enc_out (B,T,H), not just the final one.energy = v^T tanh(W_h enc_out + W_s s), alpha = softmax(energy), ctx = sum(alpha * enc_out).cat([embed(prev_token), ctx], dim=-1) -- (E+H)-dim input at each step.Linear(H*2, V) applied to cat([dec_out, ctx]) -- concatenating context again improves performance.y_{t-1} (GT) as decoder input; cross-entropy loss on predicted vocab distribution.p_gen * P_vocab + (1-p_gen) * sum_i alpha_i [src_i=w] over the output distribution.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:
nn.Embedding + nn.GRU(batch_first=True). Keep ALL hidden states enc_out (B,T,H), not just the final one.energy = v^T tanh(W_h enc_out + W_s s), alpha = softmax(energy), ctx = sum(alpha * enc_out).cat([embed(prev_token), ctx], dim=-1) -- (E+H)-dim input at each step.Linear(H*2, V) applied to cat([dec_out, ctx]) -- concatenating context again improves performance.y_{t-1} (GT) as decoder input; cross-entropy loss on predicted vocab distribution.p_gen * P_vocab + (1-p_gen) * sum_i alpha_i [src_i=w] over the output distribution.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.
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.
Check
Compute on paper before clicking.
Check your understanding
In Bahdanau attention, the energy score e_{t,i} is computed as:
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.
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).
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:
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).
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.
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.
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:
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.
Section
Project
Concept
Build a complete Encoder + BahdanauAttn + Decoder from scratch in PyTorch. Three milestones: attention math -> full forward pass -> teacher-forcing training loop.
| # | milestone | key tools |
|---|---|---|
| 1 | Implement BahdanauAttn, verify alpha sums to 1 over src_len | nn.Linear, torch.softmax, tensor shapes |
| 2 | Implement Encoder + Decoder, trace shapes through a single forward step | nn.GRU batch_first=True, torch.cat, argmax |
| 3 | Teacher-forcing training loop, compare TF vs free-run predictions on 3 steps | CrossEntropyLoss, 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.
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.
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)| tensor | shape | sanity 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 |
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.
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()])| tensor | shape | value / 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) | scalar | 3 (argmax of logits) |
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}')| epoch | loss | note |
|---|---|---|
| 0 | ~7.3 | random init; 3 steps x cross-entropy |
| 1 | lower | grads from teacher forcing update weights |
| 2 | lower still | convergence with correct GT inputs |
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.
| epoch | loss | note |
|---|---|---|
| 0 | ~7.3 | random init; 3 steps x cross-entropy |
| 1 | lower | grads from teacher forcing update weights |
| 2 | lower still | convergence with correct GT inputs |
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 choice | value | rationale |
|---|---|---|
| VOCAB | 10 | toy; real NMT uses 30k-50k BPE tokens |
| H | 8 | tiny hidden dim for CPU verification |
| attention | Bahdanau | additive; original NMT attention (2015) |
| training | teacher forcing | step uses GT y_{t-1}, not own pred |
| inference | free-run | feed own argmax back at each step |
| epochs | 200 | converges to near-zero loss on this toy |
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 choice | value | rationale |
|---|---|---|
| VOCAB | 10 | toy; real NMT uses 30k-50k BPE tokens |
| H | 8 | tiny hidden dim for CPU verification |
| attention | Bahdanau | additive; original NMT attention (2015) |
| training | teacher forcing | step uses GT y_{t-1}, not own pred |
| inference | free-run | feed own argmax back at each step |
| epochs | 200 | converges to near-zero loss on this toy |
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.
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.
Recap
enc_out (B,T,H) -> Bahdanau attention -> context -> decoder GRU -> logitse_i = v^T tanh(W_h h_i + W_s s), softmax alpha, and weighted-sum context vectorp_gen * P_vocab + (1-p_gen) * sum_i alpha_i [src_i=w] lets the decoder point to source tokens| concept | the one thing to remember |
|---|---|
| Bahdanau attention | additive: tanh(W_h h + W_s s) then dot v -- NOT dot-product |
| teacher forcing | feed y_{t-1} GT at training; decoder never sees its own errors |
| exposure bias | train/inference mismatch: GT inputs vs own predictions |
| scheduled sampling | epsilon_k = max(0, 1-k/K); mixes GT and model pred each step |
| copy mechanism | p_gen gates vocab vs pointing: (1-p_gen) * alpha over src positions |
Want this taught 1-on-1? Alexander tutors Machine Learning — $55/session, free consultation.