Weeks
Deep Learning and GenAIWeek 8 · GRU, Seq2Seq, Beam Search and BLEU
Week 8 Notes

Deep Learning,
Week 8

Introduction to DL and GenAI, Week 8: gated cells and sequence-to-sequence. The GRU, bidirectional RNNs, the encoder-decoder and its context-vector bottleneck, teacher forcing and exposure bias, greedy decoding and beam search with length normalisation, and BLEU. Every number was really run, including a copy task that measures the bottleneck.

6 chapters10 worked examples~34 min read
GRUBidirectional RNNEncoder-decoderBottleneckTeacher forcingBeam searchBLEUInterview prep

Chapters

  1. 01The GRUPart 1
  2. 02Bidirectional RNNsPart 2
  3. 03Encoder-Decoder (Seq2Seq)Part 3
  4. 04Beam Search and BLEUPart 4
  5. 05Interview Question BankInterview
  6. 06Week 8 Summary SheetSummary
01
Part 1

The GRU

The LSTM works but is heavy: two state vectors and four weight blocks. The Gated Recurrent Unit (GRU) (Cho et al., 2014) keeps the central idea, a gate that lets the state pass through unchanged, with a lighter design: one state vector, two gates.

1.1The equations

h(t-1)x(t)reset gate rhow much past to useupdate gate zhow much old to keepcandidate ntanh( x, r * h(t-1) )h(t)(1-z)*n + z*h(t-1)two gates, one state vector: z near 1 copies the old state, z near 0 replaces itno separate cell state, and 3 weight blocks instead of the LSTM's 4
Figure 1. Two gates decide how the new state is built from the old state and a candidate
rt=σ(Wr[ht−1,xt])reset gate: how much of the past to use when proposing a new statezt=σ(Wz[ht−1,xt])update gate: how much of the old state to keeph~t=tanh⁡(W[ rt⊙ht−1, xt])candidate stateht=(1−zt)⊙h~t+zt⊙ht−1new state\begin{aligned}r_t&=\sigma(W_r[h_{t-1},x_t])&&\text{reset gate: how much of the past to use when proposing a new state}\\ z_t&=\sigma(W_z[h_{t-1},x_t])&&\text{update gate: how much of the old state to keep}\\ \tilde h_t&=\tanh\big(W[\,r_t\odot h_{t-1},\,x_t]\big)&&\text{candidate state}\\ h_t&=(1-z_t)\odot\tilde h_t+z_t\odot h_{t-1}&&\text{new state}\end{aligned}

The reset gate lets the candidate ignore the past (r≈0r\approx0) when a fresh start is appropriate, for instance at the start of a new clause. The update gate is a dimmer between "copy the old state" (z≈1z\approx1) and "replace it with the candidate" (z≈0z\approx0). Write the cell by hand and compare it with nn.GRU:

main.py
import torch, torch.nn as nn
torch.manual_seed(0)
d, h, T = 3, 4, 6
gru = nn.GRU(d, h, batch_first=True)
x = torch.randn(1, T, d)
Wi, Wh, bi, bh = gru.weight_ih_l0, gru.weight_hh_l0, gru.bias_ih_l0, gru.bias_hh_l0     # blocks stacked in the order: reset, update, candidate
sig = torch.sigmoid

hs, h_t = [], torch.zeros(h)
for t in range(T):
    gi, gh = Wi @ x[0, t] + bi, Wh @ h_t + bh
    r = sig(gi[:h] + gh[:h])                           # reset gate: how much of the past to use when proposing a new state
    z = sig(gi[h:2 * h] + gh[h:2 * h])                 # update gate: how much of the old state to KEEP
    n = torch.tanh(gi[2 * h:] + r * gh[2 * h:])        # candidate state
    h_t = (1 - z) * n + z * h_t                        # PyTorch convention: z is the weight on the OLD state
    hs.append(h_t)
print("hand-written GRU matches nn.GRU:", torch.allclose(torch.stack(hs), gru(x)[0][0], atol=1e-6))
print("gate values at the last step: reset", [round(v, 2) for v in r.tolist()], " update", [round(v, 2) for v in z.tolist()])
count = lambda m: sum(p.numel() for p in m.parameters())
d, h = 10, 20
print(f"\nparameters for d={d}, h={h}:  RNN {count(nn.RNN(d, h))},  GRU {count(nn.GRU(d, h))},  LSTM {count(nn.LSTM(d, h))}   (GRU = 3x RNN, LSTM = 4x)")
print("GRU has ONE state vector (h); the LSTM has two (h and c)")
Output
hand-written GRU matches nn.GRU: True
gate values at the last step: reset [0.65, 0.59, 0.3, 0.29]  update [0.82, 0.46, 0.61, 0.32]

parameters for d=10, h=20:  RNN 640,  GRU 1920,  LSTM 2560   (GRU = 3x RNN, LSTM = 4x)
GRU has ONE state vector (h); the LSTM has two (h and c)
Two conventions for z, same model

The source notes (and the original paper) write ht=(1−zt)⊙ht−1+zt⊙h~th_t=(1-z_t)\odot h_{t-1}+z_t\odot\tilde h_t, so zz is the weight on the new candidate. PyTorch writes ht=(1−zt)⊙nt+zt⊙ht−1h_t=(1-z_t)\odot n_t+z_t\odot h_{t-1}, so zz is the weight on the old state. They are the same network with z↔1−zz\leftrightarrow1-z. PyTorch also applies the reset gate after the matrix product, n=tanh⁡(Winx+b+r⊙(Whnh+b))n=\tanh(W_{in}x+b+r\odot(W_{hn}h+b)), instead of before it, tanh⁡(W[r⊙h,x])\tanh(W[r\odot h,x]); again the same idea, a slightly different formula. In an exam use the convention of your notes and say which one you use.

1.2What the update gate really does

Force the update gate to extremes in a real nn.GRU by setting its bias to ±30\pm30, and see what happens to the state over 30 steps:

main.py
import torch, torch.nn as nn
torch.manual_seed(0)
d, h, T = 3, 5, 30
def make(update_bias):
    gru = nn.GRU(d, h, batch_first=True)
    with torch.no_grad():
        gru.bias_ih_l0[h:2 * h] = update_bias          # the update-gate block of the bias
        gru.bias_hh_l0[h:2 * h] = 0.0
    return gru
x = torch.randn(1, T, d)
h0 = torch.randn(1, 1, h)
for name, bias in (("update gate forced to ~1 (keep)", 30.0), ("update gate forced to ~0 (overwrite)", -30.0)):
    gru = make(bias)
    out, hT = gru(x, h0)
    print(f"{name:40s}  max |h_T - h_0| = {(hT - h0).abs().max().item():.6f}")
x2 = torch.randn(1, T, d)                               # a different input sequence
for name, bias in (("update gate ~1", 30.0), ("update gate ~0", -30.0)):
    gru = make(bias)
    a = gru(x, h0)[1]; b = gru(x2, h0)[1]
    print(f"{name}: final state for two DIFFERENT inputs differs by {(a - b).abs().max().item():.6f}")
print("z ~ 1 copies the old state through every step (the input is ignored); z ~ 0 replaces it by the new candidate each step")
Output
update gate forced to ~1 (keep)           max |h_T - h_0| = 0.000000
update gate forced to ~0 (overwrite)      max |h_T - h_0| = 1.829469
update gate ~1: final state for two DIFFERENT inputs differs by 0.000000
update gate ~0: final state for two DIFFERENT inputs differs by 0.637222
z ~ 1 copies the old state through every step (the input is ignored); z ~ 0 replaces it by the new candidate each step

With the gate pinned at "keep", the state after 30 steps is identical to the starting state (difference 0.000000), and two completely different input sequences give the same final state: the input is ignored and the memory is perfectly preserved. Pinned at "overwrite", the final state has nothing to do with the start and differs for different inputs (by 0.64). A trained GRU sits between these, per dimension and per step. This is the GRU's analogue of the LSTM's forget gate: the gradient also takes the copy path, with derivative zz per step, so near 1 the signal survives long ranges. And, exactly as with the LSTM, this only helps when the gate actually learns to stay open.

1.3GRU versus LSTM

GRULSTM
State vectorsone (hh)two (hh and cc)
Gates2: reset, update3: forget, input, output (+ candidate)
Weight blocks3 (1,920 at d=10, h=20)4 (2,560)
Memory controlone gate does both forgetting and writing (zz versus 1−z1-z)forgetting and writing are separate (ff, ii)
Outputthe whole state is exposedan output gate filters the state

The GRU is smaller (25% fewer parameters) and slightly faster; the LSTM has a little more control. Which is better? The honest answer is "it depends on the data, try both": on many tasks they perform about the same. The source notes' rule "LSTM for long sentences, GRU for medium" is a common heuristic, but not a law and not something proved in this course.

1.4Chapter summary

  • GRU: reset rr, update zz, candidate h~\tilde h; one state hh; ht=(1−z)⊙h~+z⊙ht−1h_t=(1-z)\odot\tilde h+z\odot h_{t-1} (PyTorch convention; the notes swap zz and 1−z1-z).
  • Measured: update gate ≈1\approx1 copies the state exactly across 30 steps; ≈0\approx0 overwrites it.
  • 3 weight blocks against the LSTM's 4; comparable accuracy on many tasks: try both.
Next · Part 2Bidirectional RNNs
02
Part 2

Bidirectional RNNs

A normal RNN reads left to right, so the state at position tt knows only x1…xtx_1\ldots x_t. Often the meaning of a word depends on what comes after it: "I saw Apple announce a new phone" (company) against "I ate an apple pie" (fruit); "not bad at all"; filling in a missing word. A bidirectional RNN reads the sequence twice.

x1f1b1[f;b]1x2f2b2[f;b]2x3f3b3[f;b]3x4f4b4[f;b]4x5f5b5[f;b]5forwardbackwardeach position sees its left context (forward RNN) and its right context (backward RNN)states are concatenated: output is 2h wide; unusable when the future does not exist yet
Figure 2. Two RNNs, one forward and one backward, with their states concatenated
  • A forward RNN reads x1→xTx_1\to x_T giving h→t\overrightarrow{h}_t (the left context).
  • A backward RNN reads xT→x1x_T\to x_1 giving h←t\overleftarrow{h}_t (the right context). The two RNNs have separate weights.
  • At each position concatenate ht=[h→t;h←t]h_t=[\overrightarrow{h}_t;\overleftarrow{h}_t]: the output is 2h2h wide, and the parameter count doubles.

2.1A task that needs the future

Make the difference measurable. A sequence of random symbols from 5 choices; the task at each position is to output either the previous symbol or the next symbol. Same GRU size, same training, one network reading one way and one reading both ways:

main.py
import torch, torch.nn as nn
torch.set_num_threads(1)
V, T = 5, 10

def batch(B, offset):
    x = torch.randint(0, V, (B, T))
    y = torch.roll(x, shifts=-offset, dims=1)           # offset=+1: target is the NEXT token; offset=-1: the PREVIOUS token
    return x, y

class Tagger(nn.Module):
    def __init__(self, bidirectional):
        super().__init__()
        self.emb = nn.Embedding(V, 16)
        self.rnn = nn.GRU(16, 32, batch_first=True, bidirectional=bidirectional)
        self.out = nn.Linear(32 * (2 if bidirectional else 1), V)
    def forward(self, x):
        return self.out(self.rnn(self.emb(x))[0])

def accuracy(bidirectional, offset, seed=0, steps=700):
    torch.manual_seed(seed)
    m = Tagger(bidirectional); opt = torch.optim.Adam(m.parameters(), lr=5e-3)
    for _ in range(steps):
        x, y = batch(64, offset)
        loss = nn.functional.cross_entropy(m(x).reshape(-1, V), y.reshape(-1))
        opt.zero_grad(); loss.backward(); opt.step()
    x, y = batch(2000, offset)
    pred = m(x).argmax(-1)
    inner = slice(1, T - 1)                              # skip the sequence ends, where roll wraps around
    return (pred[:, inner] == y[:, inner]).float().mean().item()

print("per-token accuracy (chance = 0.20)            unidirectional   bidirectional")
for name, off in (("tag each position with the PREVIOUS token", -1), ("tag each position with the NEXT token    ", +1)):
    print(f"  {name}      {accuracy(False, off):10.2f}      {accuracy(True, off):10.2f}")

m = Tagger(True); out, hn = m.rnn(m.emb(torch.randint(0, V, (3, T))))
print("\nbidirectional GRU output:", tuple(out.shape), "= (batch, T, 2*hidden): forward and backward states concatenated;  final states:", tuple(hn.shape))
Output
per-token accuracy (chance = 0.20)            unidirectional   bidirectional
  tag each position with the PREVIOUS token            1.00            1.00
  tag each position with the NEXT token                0.20            1.00

bidirectional GRU output: (3, 10, 64) = (batch, T, 2*hidden): forward and backward states concatenated;  final states: (2, 3, 32)

Predicting the previous symbol is easy for both (1.00): the information is already in the past. Predicting the next symbol is impossible for the unidirectional network (0.20, exactly chance for 5 symbols, since random symbols cannot be guessed) but trivial for the bidirectional one (1.00), because its backward half has already read it. The output shape confirms the doubling: (batch,T,2h)(\text{batch},T,2h), with the final states of the two directions stacked as (2,batch,h)(2,\text{batch},h).

2.2When to use it, and when not

Good fitBad fit
Named-entity recognition, part-of-speech tagging: whole sentence availableReal-time use: live speech recognition, streaming, any task that must answer before the future arrives
Sentiment of a complete review ("not bad at all")Generation (language modelling, the decoder of seq2seq): the next word does not exist yet, and reading the "future" of the training text would be cheating
The encoder of a seq2seq model (the whole source sentence is known)Anything that predicts the next token
The rule in one line Use a bidirectional RNN when the whole sequence is available at once and you want to understand it (tag, classify, encode); never when you must generate it or answer in real time. The same split reappears in Weeks 9-10: BERT is bidirectional (understanding), GPT is not (generation).

2.3Chapter summary

  • Two RNNs with separate weights, forward and backward; concatenate the states (2h2h wide, double the parameters).
  • Measured: tagging with the NEXT token: 0.20 unidirectional (chance), 1.00 bidirectional; the PREVIOUS token: 1.00 for both.
  • For understanding with the whole input available; not for generation or real-time use.
Next · Part 3Encoder-Decoder (Seq2Seq)
03
Part 3

Encoder-Decoder (Seq2Seq)

So far the output had the same length as the input, or was a single label. Translation breaks that: "I love cats" (3 words) becomes "mujhe billiyan pasand hain" (4 words) and the second sentence cannot be produced one-to-one with the first. The architecture for variable-length input to variable-length output is the encoder-decoder, or sequence-to-sequence (seq2seq) model (Sutskever et al., 2014).

3.1The architecture

encIencloveenccatsenc<eos>contextvectordec<bos>mujhedecmujhebilliyandecbilliyanpasanddecpasandhainencoder reads the inputall inputsqueezed intoONE vectordecoder writes the outputthe only link between the two halves is the context vector: a bottleneck that attention (Week 9) removes
Figure 3. The encoder squeezes the input into a context vector; the decoder unrolls it into the output
  • Encoder: an RNN (LSTM or GRU) that reads the whole input; its final hidden state is the context vector, a summary of the input.
  • Decoder: another RNN whose initial state is the context vector. It produces the output one token at a time, each step taking the previous output token as input, until it emits <eos>.
  • The two halves are trained jointly, end to end, with cross-entropy on every output token.

The class below is a complete, runnable seq2seq model (a GRU encoder and decoder). The test task is deliberately simple, copy the input digits, because the answer is known exactly and any failure must be the architecture's. Note the two different ways the decoder is driven in forward (training) and generate (inference). The table reports three numbers per setting; the next two sections explain them.

main.py
import torch, torch.nn as nn
torch.set_num_threads(1)
V, BOS = 12, 10                                          # tokens 0..9 are digits, 10 = start-of-output

class Seq2Seq(nn.Module):
    def __init__(self, hidden):
        super().__init__()
        self.emb = nn.Embedding(V, 32)
        self.encoder = nn.GRU(32, hidden, batch_first=True)
        self.decoder = nn.GRU(32, hidden, batch_first=True)
        self.out = nn.Linear(hidden, V)
    def forward(self, src, tgt_in):                      # TRAINING, teacher forcing: the decoder is fed the TRUE previous tokens
        context = self.encoder(self.emb(src))[1]         # the final encoder state: ONE vector for the whole input
        return self.out(self.decoder(self.emb(tgt_in), context)[0])
    @torch.no_grad()
    def generate(self, src, length):                     # INFERENCE: the decoder is fed its OWN previous outputs
        state = self.encoder(self.emb(src))[1]
        tok, outputs = torch.full((src.size(0), 1), BOS), []
        for _ in range(length):
            o, state = self.decoder(self.emb(tok), state)
            tok = self.out(o).argmax(-1); outputs.append(tok)
        return torch.cat(outputs, 1)

def evaluate(length, hidden, seed=0, steps=2500):
    torch.manual_seed(seed)
    model = Seq2Seq(hidden); opt = torch.optim.Adam(model.parameters(), lr=3e-3)
    def batch(B):
        src = torch.randint(0, 10, (B, length))          # the task: copy the input sequence
        tgt_in = torch.cat([torch.full((B, 1), BOS), src[:, :-1]], 1)
        return src, tgt_in
    for _ in range(steps):
        src, tgt_in = batch(64)
        loss = nn.functional.cross_entropy(model(src, tgt_in).reshape(-1, V), src.reshape(-1))
        opt.zero_grad(); loss.backward(); nn.utils.clip_grad_norm_(model.parameters(), 1.0); opt.step()
    src, tgt_in = batch(1000)
    with torch.no_grad():
        forced = (model(src, tgt_in).argmax(-1) == src).float().mean().item()
        free = model.generate(src, length)
    return forced, (free == src).float().mean().item(), (free == src).all(1).float().mean().item()

print("copy task, chance = 0.10 per token")
print("sequence length  hidden   teacher-forced acc   free-running acc   whole sequence exact")
for length, hidden in ((4, 32), (8, 32), (16, 32), (32, 32), (16, 16), (16, 64), (16, 128)):
    a, b, c = evaluate(length, hidden)
    print(f"{length:10d}      {hidden:6d}   {a:14.3f}      {b:14.3f}      {c:14.3f}")
Output
copy task, chance = 0.10 per token
sequence length  hidden   teacher-forced acc   free-running acc   whole sequence exact
         4          32            0.998               0.997               0.993
         8          32            0.921               0.794               0.514
        16          32            0.641               0.303               0.000
        32          32            0.396               0.147               0.000
        16          16            0.470               0.197               0.000
        16          64            0.829               0.564               0.028
        16         128            0.947               0.870               0.405

3.2The bottleneck

sequence length (hidden 32)1.0040.7980.30160.1532hidden size (length 16)0.20160.56640.87128free-running token accuracy on the copy task (chance 0.10)a longer input squeezed into the same context vector is copied worse; a bigger vector helps
Figure 4. Free-running copy accuracy: it falls with length and rises with hidden size

Read the free-running column. With a hidden size of 32, copying 4 digits is perfect (0.997), 8 digits drops to 0.79, 16 to 0.30 and 32 to 0.15, close to the 0.10 of random guessing. At length 16, growing the hidden vector from 16 to 64 to 128 lifts accuracy from 0.20 to 0.56 to 0.87. Everything the decoder will ever know about the input has to pass through that one fixed-size vector, the encoder's last state, which is also overwritten at every step. The longer the input, the more must be crammed in and the more is lost. This is the bottleneck problem of the plain encoder-decoder, and it is exactly what the attention mechanism of Week 9 is invented to remove: instead of one vector, let the decoder look back at all the encoder states.

A careful reading This is a trained-from-scratch toy with a modest training budget, so the exact numbers (0.30 at length 16) depend on how long and how well it is trained; a bigger run would copy longer sequences. The direction, however, is robust and is the same one that made researchers add attention: accuracy falls as the input grows and recovers as the context vector grows.

3.3Teacher forcing and exposure bias

TRAINING, teacher forcing: each step is fed the TRUE previous token<bos>decy1 (true)decy2 (true)decy3 (true)decINFERENCE, free running: each step is fed its OWN previous output<bos>decy1 (own)decy2 (own)decy3 (own)dectraining never shows the decoder its own mistakes, inference does: errors can compound (exposure bias)
Figure 5. Training feeds the truth; inference feeds the model's own outputs

How should the decoder be trained? At step tt it needs the previous token as input. Two choices: feed it its own previous prediction (what will happen at test time) or feed it the true previous token from the training data. Teacher forcing chooses the second: the decoder is "forced" to follow the teacher's answer, whatever it would have said itself. It makes training fast and stable, because an early mistake does not poison every later step. At inference there is no teacher, so the model's own outputs are fed back, always. (Never use teacher forcing at inference: there is no truth to feed.)

The price is a train-test mismatch called exposure bias: the model is only ever trained on perfect histories, then deployed on its own imperfect ones, so one early slip puts it in a situation it never practised, and errors can compound. The experiment shows the gap directly. At length 8 the decoder gets 0.92 of tokens right when fed the truth but 0.79 when fed itself; at length 16, 0.64 against 0.30; at length 32, 0.40 against 0.15. The same network, the same weights, tested the two ways. Remedies (beyond this course) include scheduled sampling (gradually feed the model its own outputs during training) and training objectives defined on the whole sequence.

3.4Chapter summary

  • Encoder RNN reads the input to a context vector; decoder RNN starts from it and writes the output token by token until <eos>.
  • Bottleneck: one fixed vector for the whole input. Measured free-running copy accuracy at hidden 32: 0.997, 0.79, 0.30, 0.15 for length 4, 8, 16, 32; hidden 16, 64, 128 at length 16: 0.20, 0.56, 0.87. Fix: attention (Week 9).
  • Teacher forcing in training only (fast, stable); at inference feed own outputs. The mismatch (exposure bias) is visible: 0.92 against 0.79 at length 8.
Next · Part 4Beam Search and BLEU
04
Part 4

Beam Search and BLEU

Training gives a decoder that, at each step, outputs a probability for every word in the vocabulary. Decoding is the separate question of how to turn those probabilities into a sentence, and evaluation is how to judge the sentence. Both are exam favourites.

4.1Greedy decoding and its trap

Greedy decoding picks the most probable word at every step. It is fast, but the best first word is not always part of the best sentence: a likely start can lead to a dead end where the model is unsure of everything that follows.

4.2Beam search

starta 0.5b 0.4c 0.1x 0.30.15y 0.30.15z 0.40.20x 0.90.36greedy: takes the largerfirst step (a) and endswith az = 0.20beam search (k >= 2) keeps balive and finds bx = 0.36= the exhaustive optimumthe best first word is not always on the best sequence
Figure 6. The best first word (a, 0.5) leads to a worse sentence than the second best (b, 0.4)

Beam search keeps the kk best partial sentences (the beam width) at each step, scored by their total log-probability, extends each by every possible next word, and prunes back to the best kk. k=1k=1 is greedy decoding; k=∞k=\infty is exhaustive search; practical values are 4 to 10. It is not guaranteed to find the best sequence, only a good one. Code it, and run the notes' example (vocabulary {a,b,c,d}\{a,b,c,d\}, k=2k=2) and a case built so that greedy fails:

main.py
import math

def beam_search(step_probs, k, length_norm=False, max_len=3, eos=None):
    """step_probs(prefix) -> {token: probability}. Keep the k best prefixes by total log-probability at every step."""
    beams = [((), 0.0)]
    for _ in range(max_len):
        candidates = []
        for prefix, score in beams:
            if eos is not None and prefix and prefix[-1] == eos:
                candidates.append((prefix, score)); continue                  # a finished hypothesis stays as it is
            for tok, p in step_probs(prefix).items():
                candidates.append((prefix + (tok,), score + math.log(p)))
        norm = (lambda c: c[1] / len(c[0])) if length_norm else (lambda c: c[1])
        beams = sorted(candidates, key=norm, reverse=True)[:k]
    return beams

# --- the worked example from the notes: vocabulary {a, b, c, d}, beam width 2 -------------------------------
table = {(): {"a": 0.4, "b": 0.3, "c": 0.2, "d": 0.1},
         ("a",): {"a": 0.2, "c": 0.6, "b": 0.1, "d": 0.1},
         ("b",): {"a": 0.3, "d": 0.5, "b": 0.1, "c": 0.1}}
step = lambda prefix: table.get(prefix, {"a": 0.25, "b": 0.25, "c": 0.25, "d": 0.25})
print("notes' example, k=2, after 2 steps:")
for prefix, score in beam_search(step, k=2, max_len=2):
    print("   ", "".join(prefix), "probability", round(math.exp(score), 3))

# --- a case where the greedy choice is a trap ----------------------------------------------------------------
trap = {(): {"a": 0.5, "b": 0.4, "c": 0.1},
        ("a",): {"x": 0.3, "y": 0.3, "z": 0.4},                   # after 'a' the model is unsure: its best next word has 0.4
        ("b",): {"x": 0.9, "y": 0.05, "z": 0.05},                 # after 'b' it is almost certain
        ("c",): {"x": 0.34, "y": 0.33, "z": 0.33}}
step = lambda prefix: trap[prefix]
print("\nthe trap: P(a)=0.5, P(b)=0.4 at step 1, but 'b' is followed by x with probability 0.9")
for k in (1, 2, 3):
    best = beam_search(step, k=k, max_len=2)[0]
    print(f"   beam width {k}: best sequence {''.join(best[0])} with probability {math.exp(best[1]):.3f}" + ("   <- greedy decoding" if k == 1 else ""))
exhaustive = max(((p1 + (t2,), math.log(trap[()][p1[0]] * trap[p1][t2])) for p1 in [("a",), ("b",), ("c",)] for t2 in trap[p1]), key=lambda c: c[1])
print(f"   exhaustive search (all 9 sequences): {''.join(exhaustive[0])} with probability {math.exp(exhaustive[1]):.3f}")
Output
notes' example, k=2, after 2 steps:
    ac probability 0.24
    bd probability 0.15

the trap: P(a)=0.5, P(b)=0.4 at step 1, but 'b' is followed by x with probability 0.9
   beam width 1: best sequence az with probability 0.200   <- greedy decoding
   beam width 2: best sequence bx with probability 0.360
   beam width 3: best sequence bx with probability 0.360
   exhaustive search (all 9 sequences): bx with probability 0.360

The notes' example reproduces: ac with 0.24 and bd with 0.15 are the two survivors. In the trap, greedy (k=1k=1) picks a because 0.5 beats 0.4, then the best that follows is only 0.4, ending at 0.20. With k=2k=2 the beam also keeps b, finds that it is followed by x with probability 0.9, and returns 0.36, the same as the exhaustive search over all 9 sequences. Bigger beams cost more time and memory, roughly kk times, and the gains flatten quickly.

Length normalisation

There is a hidden bias. A sentence's probability is a product of per-word probabilities, each below 1, so every extra word lowers the total and beam search drifts toward short outputs. The standard fix is to score by the length-normalised log-probability, 1∣y∣∑tlog⁡P(yt∣…)\tfrac1{|y|}\sum_t\log P(y_t\mid\ldots) (the notes say "normalized by sequence length"). A model where an early <eos> is cheap shows the bias:

main.py
import math

# every extra word costs probability, so a plain product of probabilities favours SHORT outputs
def step(prefix):
    table = {(): {"ok": 0.5, "the": 0.5},
             ("ok",): {"<eos>": 1.0},
             ("the",): {"cat": 0.95, "dog": 0.05},
             ("the", "cat"): {"sat": 0.95, "ran": 0.05},
             ("the", "cat", "sat"): {"down": 0.95, "up": 0.05},
             ("the", "cat", "sat", "down"): {"<eos>": 0.95, "now": 0.05}}
    return table.get(prefix, {"<eos>": 1.0})

def beam(k, normalise, steps=6):
    key = (lambda c: c[1] / len(c[0])) if normalise else (lambda c: c[1])
    beams = [((), 0.0)]
    for _ in range(steps):
        cand = []
        for prefix, score in beams:
            if prefix and prefix[-1] == "<eos>":
                cand.append((prefix, score)); continue                  # a finished sentence stays as it is
            for tok, p in step(prefix).items():
                cand.append((prefix + (tok,), score + math.log(p)))
        beams = sorted(cand, key=key, reverse=True)[:k]
    return max(beams, key=key)

for normalise in (False, True):
    seq, score = beam(3, normalise)
    print(f"{'with   ' if normalise else 'without'} length normalisation: {' '.join(seq):24s} probability {math.exp(score):.3f}   log-prob per word {score / len(seq):6.3f}")
print("'ok <eos>' has the larger TOTAL probability (0.50 against 0.41) but every word of the long sentence is more certain (-0.18 against -0.35 per word)")
Output
without length normalisation: ok <eos>                 probability 0.500   log-prob per word -0.347
with    length normalisation: the cat sat down <eos>   probability 0.407   log-prob per word -0.180
'ok <eos>' has the larger TOTAL probability (0.50 against 0.41) but every word of the long sentence is more certain (-0.18 against -0.35 per word)

Without normalisation the beam returns ok <eos> (total probability 0.50) because the long sentence's total, 0.41, is lower, although every word of the long sentence is more certain (−0.18-0.18 against −0.35-0.35 per word). With normalisation it returns the long sentence.

4.3BLEU: judging a translation

How good is "the cat sat on the mat" as a translation of the reference "the cat is on the mat"? BLEU (Bilingual Evaluation Understudy) compares the machine output to one or more human references by n-gram precision: what fraction of the output's 1-grams, 2-grams, 3-grams and 4-grams also appear in the reference. Two details make it work.

BLEU=BP⋅exp⁡(∑n=1414log⁡pn),BP={1∣c∣>∣r∣e 1−∣r∣/∣c∣∣c∣≤∣r∣\mathrm{BLEU}=\mathrm{BP}\cdot\exp\Big(\sum_{n=1}^{4}\tfrac14\log p_n\Big),\qquad \mathrm{BP}=\begin{cases}1&|c|>|r|\\ e^{\,1-|r|/|c|}&|c|\le|r|\end{cases}
  • Clipped precision. An output n-gram is credited at most as many times as it occurs in the reference. Otherwise "the the the the the the" would score 100%.
  • Brevity penalty (BP). Precision alone rewards short outputs (say only the one word you are sure of), so outputs shorter than the reference are penalised.
main.py
import math
from collections import Counter

def ngrams(tokens, n):
    return Counter(tuple(tokens[i:i + n]) for i in range(len(tokens) - n + 1))

def precision(hyp, ref, n):
    h, r = ngrams(hyp, n), ngrams(ref, n)
    clipped = sum(min(c, r[g]) for g, c in h.items())             # a hypothesis n-gram counts at most as often as it occurs in the reference
    return clipped, max(sum(h.values()), 1)

def bleu(hyp, ref, max_n=4):
    ps = []
    for n in range(1, max_n + 1):
        num, den = precision(hyp, ref, n); ps.append(num / den)
    bp = 1.0 if len(hyp) > len(ref) else math.exp(1 - len(ref) / len(hyp))      # brevity penalty
    geo = math.exp(sum(math.log(p) for p in ps) / max_n) if min(ps) > 0 else 0.0
    return bp * geo, ps, bp

ref = "the cat is on the mat".split()
cases = {"perfect": "the cat is on the mat",
         "one word wrong": "the cat sat on the mat",
         "word salad, same words": "mat the on is cat the",
         "all 'the'": "the the the the the the",
         "too short": "the cat"}
print("reference: 'the cat is on the mat'")
for name, text in cases.items():
    score, ps, bp = bleu(text.split(), ref)
    score2 = bleu(text.split(), ref, 2)[0]
    print(f"{name:24s} precisions 1..4 = {[round(p, 2) for p in ps]}  brevity penalty {bp:.2f}  BLEU-2 = {score2:.3f}  BLEU-4 = {score:.3f}")
c, t = precision("the the the the the the".split(), ref, 1)
print("one short sentence is a harsh test for BLEU-4 (no matching 4-gram means 0); real BLEU is computed over a whole corpus")
print(f"\nwithout clipping, 'the the the the the the' would have unigram precision 6/6; clipped it is {c}/{t} (the reference has 'the' only twice)")
Output
reference: 'the cat is on the mat'
perfect                  precisions 1..4 = [1.0, 1.0, 1.0, 1.0]  brevity penalty 1.00  BLEU-2 = 1.000  BLEU-4 = 1.000
one word wrong           precisions 1..4 = [0.83, 0.6, 0.25, 0.0]  brevity penalty 1.00  BLEU-2 = 0.707  BLEU-4 = 0.000
word salad, same words   precisions 1..4 = [1.0, 0.0, 0.0, 0.0]  brevity penalty 1.00  BLEU-2 = 0.000  BLEU-4 = 0.000
all 'the'                precisions 1..4 = [0.33, 0.0, 0.0, 0.0]  brevity penalty 1.00  BLEU-2 = 0.000  BLEU-4 = 0.000
too short                precisions 1..4 = [1.0, 1.0, 0.0, 0.0]  brevity penalty 0.14  BLEU-2 = 0.135  BLEU-4 = 0.000
one short sentence is a harsh test for BLEU-4 (no matching 4-gram means 0); real BLEU is computed over a whole corpus

without clipping, 'the the the the the the' would have unigram precision 6/6; clipped it is 2/6 (the reference has 'the' only twice)

Read the rows. The perfect copy scores 1. "the cat sat on the mat" keeps unigram precision 0.83 (5 of 6 words) and bigram precision 0.60, so BLEU-2 is a respectable 0.707, but no 4-gram of the output is in the reference, so BLEU-4 is 0 (on one sentence; real BLEU is computed over a whole corpus, where such zeros average out). "mat the on is cat the" has all the right words, unigram precision 1.0, and scores 0: BLEU rewards matching order, not just vocabulary. "the the the the the the" is clipped to 2/6, and "the cat" gets a brevity penalty of 0.14. BLEU is cheap and correlates with human judgement on average, but it ignores meaning (a good paraphrase scores badly) and treats all words as equally important.

4.4Chapter summary

  • Greedy takes the best word each step and can fall into a trap (0.20); beam search keeps the kk best partial sentences (found 0.36 = the exhaustive optimum at k=2k=2); k=1k=1 is greedy.
  • Normalise by length or beam search prefers short outputs (0.50 short against 0.41 long, yet the long one is better per word).
  • BLEU = brevity penalty times the geometric mean of clipped n-gram precisions; order matters (word salad scores 0).
Next · InterviewInterview Question Bank
05
Interview

Interview Question Bank

What is likely to be asked from Week 8, with 30-second answers, deeper explanations, a lecture-or-extra tag on each question, and code you can run.

Week 8 questions are about the gated variants, how sequences are read in both directions, and how an encoder-decoder produces and is judged on its output. Each answer starts with a 30-second version. Questions are tagged From the notes or Extra.

1Definition

One clear sentence: what it is.

2Intuition or tiny example

Why it works, in one picture or three lines.

3The failure it prevents

What goes wrong without it.

5.1Interview heat-map: what is most likely

Very likely6 topics
GRU equations"Write the GRU equations and explain the two gates."Notes 1
GRU versus LSTM"Compare GRU and LSTM. When would you choose each?"Notes 5.3
Bidirectional RNN"What is a bidirectional RNN and when can it not be used?"Notes 5.4
Encoder-decoder bottleneck"What is the bottleneck of seq2seq and how is it solved?"Notes 5.4
Teacher forcing"Explain teacher forcing and exposure bias."Notes 5.4, 7
Beam search"Explain beam search. Why is greedy decoding not enough?"Notes 5.4, Example 4
Likely4 topics
Length normalisation"Why normalise beam-search scores by length?"Notes 5.4
BLEU"Explain the BLEU score, clipped precision and the brevity penalty."Notes 5.4
Types of sequence tasks"Which architecture for which sequence task?"Notes 5
Greedy versus sampling"How can a decoder choose the next token besides greedy and beam search?"Extra
Sometimes0 topics

5.2GRU and bidirectional RNNs

Interview 1Write the GRU equations and explain the reset and update gates.
Week 8Very likelyNotes 1From the lecture

Two gates, one state.

Prints
hand-written GRU matches nn.GRU: True
gate values at the last step: reset [0.65, 0.59, 0.3, 0.29]  update [0.82, 0.46, 0.61, 0.32]
Prints
update gate forced to ~1 (keep)           max |h_T - h_0| = 0.000000
update gate forced to ~0 (overwrite)      max |h_T - h_0| = 1.829469
update gate ~1: final state for two DIFFERENT inputs differs by 0.000000
update gate ~0: final state for two DIFFERENT inputs differs by 0.637222
z ~ 1 copies the old state through every step (the input is ignored); z ~ 0 replaces it by the new candidate each step
Answer
Say this first (30 seconds)

r=σ(Wr[ht−1,xt])r=\sigma(W_r[h_{t-1},x_t]) (reset: how much of the past to use for the candidate); z=σ(Wz[ht−1,xt])z=\sigma(W_z[h_{t-1},x_t]) (update: balance between old state and new candidate); h~=tanh⁡(W[r⊙ht−1,xt])\tilde h=\tanh(W[r\odot h_{t-1},x_t]); ht=(1−z)⊙ht−1+z⊙h~h_t=(1-z)\odot h_{t-1}+z\odot\tilde h (notes' convention; PyTorch uses zz for the old state, same model with z↔1−zz\leftrightarrow1-z). One state vector; the cell and hidden states are merged.

Go deeper
  • Verified against nn.GRU (PyTorch convention). With the update gate pinned at "keep", the state was copied exactly over 30 steps.
Follow-up they may askWhat happens if z=1z=1 everywhere (in the notes' convention)?
Interview 2Compare the GRU and the LSTM. When would you choose each?
Week 8Very likelyNotes 5.3From the lecture

Fewer parameters against more control; try both.

Prints
parameters for d=10, h=20:  RNN 640,  GRU 1920,  LSTM 2560   (GRU = 3x RNN, LSTM = 4x)
GRU has ONE state vector (h); the LSTM has two (h and c)
Answer
Say this first (30 seconds)

GRU: one state hh, 2 gates (reset, update), 3 weight blocks, fewer parameters, faster. LSTM: two states (cc and hh), forget/input/output gates, 4 weight blocks, separate forgetting and writing, more expressive. Often similar accuracy. Pick the GRU when data or compute is limited; the LSTM when you suspect very long dependencies; in practice try both and keep the better one. (The notes' "LSTM for long sentences" is a heuristic, not a theorem.)

Go deeper
  • Counted: d=10, h=20 gives RNN 640, GRU 1,920, LSTM 2,560.
Follow-up they may askWhich has no separate output gate?
Interview 3What is a bidirectional RNN, when is it useful and when is it unusable?
Week 8Very likelyNotes 5.4From the lecture

Left context plus right context; needs the whole input.

Prints
per-token accuracy (chance = 0.20)            unidirectional   bidirectional
  tag each position with the PREVIOUS token            1.00            1.00
  tag each position with the NEXT token                0.20            1.00

bidirectional GRU output: (3, 10, 64) = (batch, T, 2*hidden): forward and backward states concatenated;  final states: (2, 3, 32)
Answer
Say this first (30 seconds)

Two RNNs with separate weights read the sequence left-to-right and right-to-left; their states are concatenated at each position (output 2h2h wide), so every position sees both its past and its future. Useful when the whole input is known and you want to understand it: NER, POS tagging, sentiment ("not bad at all"), encoders. Unusable for real-time/streaming tasks and for generation, because the future tokens do not exist yet.

Go deeper
  • Measured: tagging with the next token 0.20 (unidirectional) against 1.00 (bidirectional).
Follow-up they may askHow many parameters does a bidirectional LSTM have compared with a unidirectional one?

5.3Encoder-decoder

Interview 4Explain the encoder-decoder (seq2seq) architecture and its bottleneck. How is the bottleneck removed?
Week 8Very likelyNotes 5.4From the lecture

One vector for the whole input; attention looks at all of it.

Prints
sequence length  hidden   teacher-forced acc   free-running acc   whole sequence exact
         4          32            0.998               0.997               0.993
         8          32            0.921               0.794               0.514
        16          32            0.641               0.303               0.000
        32          32            0.396               0.147               0.000
Answer
Say this first (30 seconds)

An encoder RNN reads the input and its final hidden state is the context vector; a decoder RNN, initialised with it, generates the output token by token until <eos>. The bottleneck: all information about a possibly long input must pass through one fixed-size vector, so quality drops on long inputs. Attention (Week 9) lets the decoder look at all encoder states at every step, with learned weights.

Go deeper
  • Measured copy accuracy (free-running, hidden 32): 0.997, 0.79, 0.30, 0.15 for lengths 4, 8, 16, 32.
Follow-up they may askWhy does a bigger hidden size only partly help?
Interview 5What is teacher forcing, what is its drawback, and is it used at inference?
Week 8Very likelyNotes 5.4, 7From the lecture

True tokens in training, own tokens in testing.

Prints
sequence length  hidden   teacher-forced acc   free-running acc   whole sequence exact
         4          32            0.998               0.997               0.993
         8          32            0.921               0.794               0.514
        16          32            0.641               0.303               0.000
Answer
Say this first (30 seconds)

During training, the decoder's input at step tt is the ground-truth previous token rather than its own prediction: faster, more stable convergence. At inference it is not used (no ground truth exists): the model feeds back its own previous output. Drawback: exposure bias, a train-test mismatch; the model never practised recovering from its own mistakes, so errors can compound. Remedy: scheduled sampling.

Go deeper
  • Measured at length 8: 0.92 teacher-forced against 0.79 free-running; length 16: 0.64 against 0.30.
Follow-up they may askWhy is the free-running accuracy always lower here?
Interview 6Which recurrent architecture would you use for sentiment classification, named-entity tagging, translation and text generation?
Week 8LikelyNotes 5Extra

Understanding: bidirectional. Generation: unidirectional. Translation: encoder-decoder.

Answer
Say this first (30 seconds)

Sentiment (sequence to label): a (bi)LSTM/GRU whose final or pooled state feeds a classifier. NER (label per token, whole sentence known): a bidirectional LSTM/GRU. Translation (sequence to sequence): an encoder-decoder (bidirectional encoder, unidirectional decoder; today with attention). Text generation: a unidirectional language model sampled one token at a time.

Go deeper
  • The decoder is always unidirectional: it generates.
Follow-up they may askWhy can the encoder, but not the decoder, be bidirectional?

5.4Decoding and evaluation

Interview 7Explain beam search and why greedy decoding is not enough. What do k=1k=1 and k=∞k=\infty mean?
Week 8Very likelyNotes 5.4, Ex. 4From the lecture

Keep the k best partial sentences.

Prints
notes' example, k=2, after 2 steps:
    ac probability 0.24
    bd probability 0.15

the trap: P(a)=0.5, P(b)=0.4 at step 1, but 'b' is followed by x with probability 0.9
   beam width 1: best sequence az with probability 0.200   <- greedy decoding
   beam width 2: best sequence bx with probability 0.360
   beam width 3: best sequence bx with probability 0.360
   exhaustive search (all 9 sequences): bx with probability 0.360
Answer
Say this first (30 seconds)

At each step keep the kk best partial sequences (by total log-probability), extend each with every possible next word, and prune to the best kk. Greedy decoding (k=1k=1) takes the locally best word and can miss a better overall sentence; k=∞k=\infty is exhaustive search (the optimum, but exponentially costly); k=5k=5-10 is typical. Beam search is not guaranteed optimal. Normalise scores by length.

Go deeper
  • Notes example (k=2): ac = 0.24 and bd = 0.15 survive. Trap example: greedy 0.20, beam 0.36 (= exhaustive).
Follow-up they may askCompute the cost of beam search in time relative to greedy.
Interview 8Why does beam search favour short outputs, and how is it fixed?
Week 8LikelyNotes 5.4From the lecture

Every extra word multiplies by a number below 1.

Prints
without length normalisation: ok <eos>                 probability 0.500   log-prob per word -0.347
with    length normalisation: the cat sat down <eos>   probability 0.407   log-prob per word -0.180
'ok <eos>' has the larger TOTAL probability (0.50 against 0.41) but every word of the long sentence is more certain (-0.18 against -0.35 per word)
Answer
Say this first (30 seconds)

A sentence's probability is a product of per-word probabilities, each <1<1, so each extra word lowers the total log-probability and shorter hypotheses win. Divide the log-probability by the length (length normalisation, or a length penalty) so that hypotheses are compared per word.

Go deeper
  • Measured: without it the beam returned "ok <eos>" (0.50); with it, the longer sentence (0.41 total, -0.18 per word).
Follow-up they may askWhy does greedy decoding not need this?
Interview 9How is BLEU computed? Why clipping and why a brevity penalty?
Week 8LikelyNotes 5.4From the lecture

Clipped n-gram precisions, geometric mean, times a length penalty.

Prints
reference: 'the cat is on the mat'
perfect                  precisions 1..4 = [1.0, 1.0, 1.0, 1.0]  brevity penalty 1.00  BLEU-2 = 1.000  BLEU-4 = 1.000
one word wrong           precisions 1..4 = [0.83, 0.6, 0.25, 0.0]  brevity penalty 1.00  BLEU-2 = 0.707  BLEU-4 = 0.000
word salad, same words   precisions 1..4 = [1.0, 0.0, 0.0, 0.0]  brevity penalty 1.00  BLEU-2 = 0.000  BLEU-4 = 0.000
all 'the'                precisions 1..4 = [0.33, 0.0, 0.0, 0.0]  brevity penalty 1.00  BLEU-2 = 0.000  BLEU-4 = 0.000
too short                precisions 1..4 = [1.0, 1.0, 0.0, 0.0]  brevity penalty 0.14  BLEU-2 = 0.135  BLEU-4 = 0.000
one short sentence is a harsh test for BLEU-4 (no matching 4-gram means 0); real BLEU is computed over a whole corpus

without clipping, 'the the the the the the' would have unigram precision 6/6; clipped it is 2/6 (the reference has 'the' only twice)
Answer
Say this first (30 seconds)

For n=1…4n=1\ldots4 compute the clipped n-gram precision: the fraction of the output's n-grams found in the reference, each counted at most as often as it appears there (so "the the the" cannot score 100%). BLEU =BP⋅exp⁡(14∑log⁡pn)=\mathrm{BP}\cdot\exp(\tfrac14\sum\log p_n) with brevity penalty BP=e1−∣r∣/∣c∣\mathrm{BP}=e^{1-|r|/|c|} when the output is shorter than the reference (precision alone would reward saying very little). Higher is better; computed over a corpus.

Go deeper
  • Measured: "the the the the the the" clipped to 2/6; "the cat" brevity penalty 0.14; word salad with all the right words scores 0.
Follow-up they may askName two weaknesses of BLEU.
Interview 10Besides greedy and beam search, how else can the next token be chosen?
Week 8LikelyNotes -Extra

Sample from the distribution.

main.py
logits = model(x)[:, -1]                        # scores for the next token
probs = torch.softmax(logits / temperature, -1)
next_token = torch.multinomial(probs, 1)             # sample; temperature < 1 sharpens, > 1 flattens
Answer
Say this first (30 seconds)

Sampling draws the token from the predicted distribution (as the character model of Week 7 did), giving varied, creative text but sometimes poor words; a temperature <1<1 sharpens the distribution (safer), >1>1 flattens it (more random). Top-k and top-p (nucleus) sampling restrict the draw to the most probable tokens. Beam search suits tasks with one right answer (translation); sampling suits open-ended generation.

Go deeper
  • Used again for large language models in Weeks 10 and 12.
Follow-up they may askWhat does temperature 0 correspond to?
Next · SummaryWeek 8 Summary Sheet
06
Summary

Week 8 Summary Sheet

Week 8 on one page: chapter glance, the GRU equations, the pairs to tell apart, a decoding comparison, a checklist and corrections to the source notes.

6.1Week 8 at a glance

C1GRU

Reset rr, update zz, candidate h~\tilde h; one state; 3 weight blocks (1,920 vs LSTM 2,560). Update gate ≈1\approx1 copies the state exactly.

C2Bidirectional

Forward + backward RNN, states concatenated (2h2h). NEXT-token tagging: 0.20 against 1.00. Not for generation or real time.

C3Seq2seq

Encoder (to a context vector) + decoder. Bottleneck: copy accuracy 0.997, 0.79, 0.30, 0.15 for length 4, 8, 16, 32. Teacher forcing: 0.92 vs 0.79 free-running.

C4Decoding / BLEU

Beam search keeps the kk best prefixes (greedy trap 0.20 against 0.36). Normalise by length. BLEU = BP times clipped n-gram precisions.

6.2GRU and LSTM side by side

GRULSTM
Stateshhh, ch,\ c
Gatesreset rr, update zzforget ff, input ii, output oo
Candidatetanh⁡(W[r⊙h,x])\tanh(W[r\odot h,x])g=tanh⁡(Wg[h,x])g=\tanh(W_g[h,x])
New state(1−z)⊙h+z⊙h~(1-z)\odot h+z\odot\tilde hc=f⊙c+i⊙gc=f\odot c+i\odot g, h=o⊙tanh⁡ch=o\odot\tanh c
Parameters (d=10, h=20)1,9202,560

6.3Key terms

C1-C2
reset gate
how much past the candidate uses.
update gate
keep old vs take new.
bidirectional
forward + backward, concatenated.
real time
no future tokens available.
C3
encoder
reads input to a context vector.
decoder
writes output token by token.
teacher forcing
feed true tokens in training.
exposure bias
train-test mismatch.
C4
beam width
kk hypotheses kept.
length norm
score per word.
clipped precision
cap by reference count.
brevity penalty
punish short output.

6.4The pairs you must tell apart

trainingteacher forcing (true tokens)
VS
inferenceown previous output
greedyk=1k=1
VS
beam searchkk best prefixes
bidirectionalunderstanding (tag, classify)
VS
unidirectionalgeneration, real time
GRUone state, 3 blocks
VS
LSTMtwo states, 4 blocks
perplexitylanguage model, lower better
VS
BLEUtranslation, higher better
raw beam scorefavours short
VS
length-normalisedper word

6.5Last-minute checklist

C1-C2
  • GRU: r, z, candidate; one state
  • state which convention of z you use
  • biRNN: concat, 2h; not for real time
C3
  • context vector = bottleneck, attention fixes it
  • teacher forcing: training only
  • exposure bias
C4
  • beam keeps k prefixes by total log-prob
  • length normalise
  • BLEU: clipped precision + brevity penalty

6.6Corrections and additions to the source notes

WhereThe source saysBetter
GRUht=(1−z)⊙ht−1+z⊙h~h_t=(1-z)\odot h_{t-1}+z\odot\tilde hvalid, but PyTorch swaps the role of zz and puts the reset gate after the matrix product; the same model, so state your convention
LSTM or GRU"long sentences: LSTM preferred"a heuristic only; they are usually comparable, so try both
Teacher forcing"faster convergence but training-inference mismatch"correct; the mismatch is measurable: 0.92 teacher-forced against 0.79 free-running at length 8
Context vector"bottleneck for long sequences"measured: copy accuracy 0.997 to 0.15 as the length grows from 4 to 32; also depends on the hidden size and training
Beam search"highest joint log-probability"it is not guaranteed to find the optimum; and raw scores favour short outputs (0.50 short vs 0.41 long)
BLEU"low priority"still asked; know clipped precision and brevity penalty, and that word salad of the right words scores 0
Revision overview
GRUreset, update
one state
3 blocks
Bidirectionalforward + backward
concat 2h
not for generation
Week 8Gated cells and seq2seq
Seq2seqencoder, decoder
context bottleneck
teacher forcing
Decodinggreedy trap
beam search
length norm
BLEUclipped precision
brevity penalty
order matters
Revision: all of Week 8 on one page
Back to top