MVA · Course notes

Algorithms for Speech and Natural Language Processing

M2 MVA · Semester 2 · Modelling · 5 ECTS

Contents

Introduction and overview

Module Algorithms for Speech and Natural Language Processing
Official title Algorithms for speech and natural language processing
Instructors C. CLAVEL, B. SAGOT, E. DUPOUX, R. BAWDEN, M. POLI (master-mva.com, 2026–2027)
Period Semester 2 · period 2 (04/01/2027 – 02/04/2027)
Credits and volume 5 ECTS · 21 h (lectures 21 h) per the Université Paris-Saclay programme table
Label Modelling · domains on master-mva.com: Natural Language Processing
Language Not displayed (the course description on master-mva.com is written in English)
Assessment Quizzes, 40 % of the grade: an online questionnaire of 30 minutes at the end of every class except the first and the last, best 6 of the 7 grades kept · final project, 60 % of the grade: groups of 2–4 around a recent speech or language processing paper with existing code, graded individually on engagement in the group work; replicate the main result, then test a new question; a one-page plan, a 4-page report and a 10-minute talk with 5 minutes of questions (master-mva.com) · Université Paris-Saclay: “Evaluation Continue non Intégrale” (partial continuous assessment)
Prerequisites Basic linear algebra, calculus, probability theory, basic machine and deep learning (master-mva.com)
Note on sources master-mva.com announces 9 three-hour slots; the Université Paris-Saclay table gives 21 h

Audience: a student with a good M1 in mathematics, computer science or physics who has trained a classifier or a small neural network, and who wants to know how machines read, translate, transcribe and speak human language, and why the algorithms look the way they do.

Empirical first: a bigram model trained on three short sentences assigns probability zero, hence infinite perplexity, to the new sentence “the dog saw the cat”, because one pair of words was never seen (ch. 1). A recogniser that hears “wreck a nice beach” for “recognise speech” in the six-word sentence “I want to recognise speech today” makes 4 errors, a word error rate of 67 % (ch. 10).

On synthetic reviews where 40 % of the opinions are negated, a bag-of-words sentiment classifier scores 56 %, and 100 % once the scope of “not” is marked (ch. 9). Each of these failures has a mathematical cause and an algorithmic fix; the course is the list of those fixes.

The course covers the automatic processing of text and speech: how both are represented, how sequences are modelled (finite-state machines, hidden Markov models, conditional random fields, recurrent networks, transformers), and how the main tasks are solved and evaluated. The tasks are language modelling, machine translation, sentiment analysis, sequence labelling, dialogue, speech recognition, speech synthesis, and code generation with agents.

It shares tools with Advanced Learning for Text and Graph Data, Foundations of Large Language Models and Deep Learning in semester 1. In semester 2 it runs alongside Large Language Models for Code and Proof, Audio Signal Processing: Time–Frequency Analysis and Audio Signal Analysis, Indexing and Transformations.

What you will be able to do

Official objectives and syllabus (2026–2027)

The course description on master-mva.com presents speech and natural language processing as a subfield of artificial intelligence used in a growing number of applications. The course promises an overview and details of the techniques and tasks of the automatic processing of text and speech.

It names some history of the field, the representation of textual and speech data, language modelling, machine translation, sentiment analysis and other labelling tasks, chatbots, and speech synthesis and recognition. Its aim is to give the key principles, algorithms and mathematics behind the state of the art, and to confront them with the reality of processing real data.

The description announces 9 three-hour slots, each with a lecture of about two hours followed by a quiz and questions. It lists twelve topics, recommends the textbook of Jurafsky and Martin, links a course repository (github.com/rbawden/MVA_2026_SNLP), and states that the instructors will provide readings for each session. The Université Paris-Saclay course detail gives the volume (21 h of lectures) and the type of assessment, with no programme of its own.

Official topic Course section
Some history of the field (objective) ch. 1, section 1.1 (from the 1954 Georgetown–IBM experiment to transformers); history of translation in ch. 8, 8.1; ELIZA in ch. 11, 11.1
Representation of textual and speech data (objective) Tokens and subwords: ch. 1, 1.2 (Definition 1.2, Code 1.3); speech: ch. 2; word vectors: ch. 5
“Topics:” (first item of the list) Not a topic: the heading of the list below
Speech features & signal processing ch. 2: sampling, source–filter model, STFT, mel filterbank, cepstrum and MFCC (Definitions 2.1–2.4 and 2.6, Proposition 2.5, Code 2.7, Example 2.8)
Hidden Markov & finite state modelling ch. 3: weighted automata and transducers, HMMs, forward, Viterbi and Baum–Welch algorithms, HMM-based recognition (Definitions 3.1–3.3, Theorems 3.4–3.5, Theorem 3.8); Baum–Welch derived in ch. 13, 13.2
Word embeddings ch. 5: PMI, SVD, skip-gram with negative sampling and the Levy–Goldberg theorem, GloVe, fastText, analogies and bias (Definitions 5.1, 5.2 and 5.4, Theorem 5.5, Codes 5.3 and 5.6)
Deep learning for NLP (RNNs, transformers) ch. 6: RNNs and vanishing gradients, LSTM, attention, the transformer block (Definitions 6.1, 6.3–6.5 and 6.8, Propositions 6.2, 6.6 and 6.7)
Neural language modelling, including large language models (LLMs) n-gram baseline in ch. 1, 1.3; ch. 7: neural LMs, pretraining objectives, scaling laws, decoding, instruction tuning and preference optimisation; DPO derived in ch. 13, 13.4
Machine translation ch. 8: noisy channel, IBM Model 1 and EM, neural MT, beam search, BLEU, chrF and COMET (Definitions 8.1, 8.4 and 8.5, Proposition 8.2, Codes 8.3 and 8.6)
Sentiment analysis ch. 9: polarity and aspect-based tasks, naive Bayes as a linear model, negation, neural classifiers (Definitions 9.1–9.2, Proposition 9.3, Code 9.4); sentiment in interaction in ch. 14, 14.4
Sequence labelling tasks ch. 4: tagging, BIO encoding, linear-chain CRF, neural taggers, span F1 (Definitions 4.1, 4.2 and 4.6, Proposition 4.3, Theorem 4.4)
Chatbots ch. 11, 11.1–11.2: ELIZA, task-oriented pipelines, retrieval-based bots, LLM chat and retrieval-augmented generation
Evaluation: comparing human and machine performance ch. 12: agreement, Cohen’s kappa, human parity claims in recognition and translation, paired bootstrap, benchmark pitfalls; task metrics in ch. 1, 4, 8, 10 and 11
Speech synthesis and speech recognition ch. 10: WER, CTC (Theorem 10.4), encoder–decoders, self-supervised speech, synthesis pipelines and vocoders (Definition 10.6, Example 10.7); RNN-T in ch. 13, 13.3; spoken language models in ch. 14, 14.1
Code generation and agents ch. 11, 11.3–11.4: pass@k and its unbiased estimator (Proposition 11.5), benchmarks, the agent loop and error compounding
Confronting the methods with the reality of real data (objective) The code of every chapter; ch. 12; the projects of Projects and assessment
Organisation: 9 three-hour slots, a lecture of about 2 h then a quiz and Q&A Session plan: 7 sessions of 3 h for the official 21 h; the Practice column plays the role of the quiz
Assessment: quizzes 40 %, final project 60 % (master-mva.com); “Evaluation Continue non Intégrale” (Université Paris-Saclay) Projects and assessment: project subjects in the official format, a quiz-style written exam, a rubric
“Les intervenants” (heading above the instructors) Not a topic: it introduces the instructors listed in the fiche (Chloé Clavel, Benoit Sagot, Emmanuel Dupoux and Rachel Bawden, each listed with “INRIA”)
Recommended textbook: Jurafsky and Martin, Speech and Language Processing References, first entries (3rd online edition, 2nd edition otherwise)

Every official topic is taught on this page. The extensions beyond the syllabus are the review of ch. 0, the complements of ch. 13 (Kneser–Ney smoothing, the derivation of Baum–Welch, the RNN transducer, the closed form behind DPO) and the research directions of ch. 14.

Where this course fits in the programme

Official prerequisites: basic linear algebra, calculus, probability theory, and basic machine and deep learning. Ch. 0 reviews the probability, information theory and signal processing that the page uses.

Course outline

Session plan

21 h in class = 7 sessions of 3 h. The volume is the official one of the Université Paris-Saclay programme table: 21 h of lectures. master-mva.com announces 9 three-hour slots, each made of a lecture of about two hours followed by a 30-minute quiz and questions; the plan below fits the topics into 21 h and keeps, in the Practice column, a short exercise set in place of each quiz. This split, and the content of each session, is a proposal.

Level 1 = foundations, 2 = core models and algorithms, 3 = advanced and research-level material. The level never goes down from one session to the next.

Session Hours Level Content Practice
1 3 h 1 ch. 1 History, tokens and BPE, n-gram language models, perplexity and smoothing (1.1–1.3); ch. 2 Sampling, source–filter model, STFT, mel filterbank, MFCC (2.1–2.4) Ex. 1, 2
2 3 h 2 ch. 3 Weighted automata and transducers, HMMs, forward and Viterbi algorithms, Baum–Welch, HMM-based recognition (3.1–3.4) Ex. 3, 8
3 3 h 2 ch. 4 Tagging, BIO encoding, linear-chain CRF, neural taggers, span F1 (4.1–4.4); ch. 5 PMI, SVD, skip-gram with negative sampling, subwords and bias (5.1–5.4) Ex. 4, 9, 10
4 3 h 2 ch. 6 RNNs, LSTMs, attention, transformer block (6.1–6.3); ch. 7 Neural LMs, pretraining and scaling laws, decoding, instruction tuning and preference optimisation (7.1–7.4) Ex. 11, 15, 16
5 3 h 2 ch. 8 IBM Model 1, neural MT, beam search, BLEU, chrF and COMET (8.1–8.4); ch. 9 Sentiment tasks, naive Bayes and logistic regression, negation and composition (9.1–9.3) Ex. 5, 14
6 3 h 3 ch. 10 WER, CTC, encoder–decoders, self-supervised speech, speech synthesis (10.1–10.4) Ex. 6, 13
7 3 h 3 ch. 11 Chatbots, retrieval-augmented generation, code generation and pass@k, agents (11.1–11.4); ch. 12 Agreement, human parity, significance, benchmark pitfalls (12.1–12.3) Ex. 7, 12, 17

Self-study, outside the 21 h (about 8 h in total): ch. 0 Review, before session 1 (≈ 2 h) · ch. 13 Complements: Kneser–Ney, Baum–Welch derivation, RNN transducer, DPO (≈ 3 h, with Ex. 18) · ch. 14 Research directions and open problems (≈ 3 h, with Ex. 19).

How to study this module

0. Review: probability, information, dynamic programming and the Fourier transform

Every chapter of the course manipulates probabilities of sequences, measures models by a cross-entropy, trains them through a softmax, and computes sums over exponentially many paths with a dynamic program. This review states these tools once, with proofs, and adds the discrete Fourier transform needed for speech in ch. 2.

0.1 Probabilities of sequences

A sentence is a sequence of tokens w_1, \dots, w_n drawn from a finite vocabulary V. Any joint law factorises by the chain rule: P(w_1, \dots, w_n) = \prod_{t=1}^{n} P(w_t \mid w_1, \dots, w_{t-1}).

The factorisation is exact; models differ by how they approximate each conditional. A Markov model of order k keeps only the last k tokens (ch. 1, ch. 3); a recurrent network or a transformer keeps a learned summary of the whole prefix (ch. 6, ch. 7).

Definition 0.1 — Entropy, cross-entropy, Kullback–Leibler divergence For two laws p, q on a finite set V, with q > 0 where p > 0: H(p) = -\sum_{w} p(w) \log p(w), \qquad H(p, q) = -\sum_{w} p(w) \log q(w), \mathrm{KL}(p \,\|\, q) = \sum_{w} p(w) \log \frac{p(w)}{q(w)} = H(p, q) - H(p).

With the natural logarithm the unit is the nat; with \log_2 it is the bit.

Proposition 0.2 — Gibbs’ inequality For all laws p, q on V, H(p, q) \geq H(p), with equality if and only if q = p on the support of p.

Proof. On the support S of p, apply Jensen’s inequality to the concave logarithm: -\mathrm{KL}(p \,\|\, q) = \sum_{w \in S} p(w) \log \frac{q(w)}{p(w)} \leq \log \sum_{w \in S} q(w) \leq \log 1 = 0 .

Equality in Jensen requires the ratio q/p to be constant on S, and equality in the second step requires q(S) = 1; together they give q = p on S. ∎

Consequence: minimising the expected negative log-likelihood of a model q under the data law p is the same as minimising \mathrm{KL}(p \,\|\, q). This is why every model of the course is trained with a cross-entropy, and why no model can do better than the entropy of the source (Proposition 7.2).

0.2 Softmax and its gradient

Definition 0.3 — Softmax For scores z \in \mathbb{R}^K (the logits) and a temperature T > 0, \mathrm{softmax}(z / T)_k = \frac{e^{z_k / T}}{\sum_{j=1}^{K} e^{z_j / T}} .

Adding the same constant to every logit leaves the result unchanged; implementations subtract \max_j z_j before exponentiating, to avoid overflow.

Proposition 0.4 — Gradient of the softmax cross-entropy Let p = \mathrm{softmax}(z) and let y be the index of the correct class. The loss \ell(z) = -\log p_y has gradient \nabla_z \ell = p - e_y , where e_y is the one-hot vector of y.

Proof. Write \ell(z) = -z_y + \log \sum_j e^{z_j}. The derivative of the first term in z_k is -\mathbf{1}[k = y]; the derivative of the log-sum-exp is e^{z_k} / \sum_j e^{z_j} = p_k. ∎

The gradient is the difference between what the model predicts and what was observed. The same shape, “expected features minus observed features”, returns for conditional random fields in Theorem 4.4.

0.3 Dynamic programming over a semiring

Many quantities of the course are sums, over exponentially many paths, of products of local weights. They are computed in polynomial time because the sum distributes over the product.

Definition 0.5 — Semiring A semiring (\mathbb{K}, \oplus, \otimes, \bar 0, \bar 1) is a set with two associative operations, an addition and a multiplication, such that:

  • the addition \oplus is commutative, with neutral element \bar 0;
  • the multiplication \otimes has neutral element \bar 1 and distributes over \oplus;
  • \bar 0 annihilates: \bar 0 \otimes a = \bar 0.

The four semirings of the course:

Name Set \oplus \otimes Used for
Probability [0, \infty) + \times likelihoods (forward algorithm)
Viterbi [0, 1] \max \times best path
Log \mathbb{R} \cup \{-\infty\} log-sum-exp + stable likelihoods, CRFs, CTC
Tropical \mathbb{R} \cup \{+\infty\} \min + edit distance, shortest paths

Proposition 0.6 — Path sums in a layered graph Let a graph have layers 0, \dots, T of at most K nodes, a weight w(e) \in \mathbb{K} on each edge between consecutive layers, and define the weight of a path as the \otimes-product of its edges. Then the \oplus-sum of the weights of all paths from layer 0 to layer T is computed by \alpha_0(s) = \bar 1 \ \text{ for each node } s \text{ of layer } 0, \qquad \alpha_t(j) = \bigoplus_{i} \alpha_{t-1}(i) \otimes w(i \to j), followed by the sum over the last layer: \bigoplus_{j} \alpha_T(j) .

This costs O(T K^2) operations, against K^T paths.

Proof. By induction on t: \alpha_t(j) is the \oplus-sum of the weights of all paths ending at node j of layer t.

A path to j is a path to some node i of the previous layer followed by the edge i \to j. Commutativity and associativity of \oplus let us group the paths by this node i. Distributivity then turns the sum over the paths through i into \alpha_{t-1}(i) \otimes w(i \to j) .

Each layer costs at most K^2 products. ∎

0.4 The discrete Fourier transform

Definition 0.7 — Discrete Fourier transform (DFT) For x \in \mathbb{C}^N, X_k = \sum_{n=0}^{N-1} x_n\, e^{-2 i \pi k n / N}, \qquad k = 0, \dots, N-1 .

For a signal sampled at f_s hertz, bin k corresponds to the frequency k f_s / N. The fast Fourier transform computes all X_k in O(N \log N) operations.

Proposition 0.8 — Convolution theorem If y = x \circledast h is the circular convolution, y_n = \sum_m x_m h_{(n - m) \bmod N}, then Y_k = X_k\, H_k \quad \text{for every } k .

Proof. Substitute and set r = (n - m) \bmod N; the exponential factorises because e^{-2 i \pi k n / N} is N-periodic in n: Y_k = \sum_{m} x_m e^{-2 i \pi k m / N} \sum_{r} h_r e^{-2 i \pi k r / N} = X_k H_k .

∎

Ch. 2 uses this with a logarithm: a convolution becomes a product in frequency, and a sum after taking \log |\cdot|.

Key takeaways — chapter 0

  • The chain rule factorises any law on sequences; models differ by what they keep of the prefix.
  • Cross-entropy is at least the entropy (Gibbs), so training by negative log-likelihood minimises a KL divergence.
  • The gradient of the softmax cross-entropy is “predicted minus observed”.
  • One dynamic program, run in different semirings, computes likelihoods, best paths and edit distances in O(TK^2).
  • The DFT turns convolutions into products; its bin k is the frequency k f_s / N.

1. Text as data: a short history, tokenisation and n-gram language models

Before any model, text must be cut into units, and the units given probabilities. This chapter places the field in its history, defines subword tokens, and builds the oldest language model still used as a baseline: the smoothed n-gram, measured by perplexity.

1.1 A short history

The field alternated between hand-written rules and statistics learned from data.

1.2 Tokens and subwords

A token is an occurrence of a unit in a text; a type is a distinct unit. Word frequencies follow Zipf’s law: the r-th most frequent type has frequency roughly proportional to 1/r. Many types occur once, so any fixed word vocabulary meets unseen words at test time.

Definition 1.1 — Tokeniser A tokeniser maps a string to a sequence of symbols of a finite vocabulary V, and back. Byte-level tokenisers never meet an unknown symbol but give long sequences; word-level ones give short sequences with unknown words; subword tokenisers sit in between.

Definition 1.2 — Byte-pair encoding (BPE) Start from characters, with an end-of-word marker. Repeat a fixed number of times: count the pairs of adjacent symbols, weighted by word frequency, and merge the most frequent pair into a new symbol. The ordered list of merges is the tokeniser; it segments a new word by replaying the merges (Sennrich, Haddow and Birch, 2016a).

Code 1.3 — Learning BPE merges

from collections import Counter

def learn_bpe(word_counts, n_merges):
    """Byte-pair encoding: repeatedly merge the most frequent adjacent pair of symbols."""
    vocab = {tuple(w) + ("</w>",): c for w, c in word_counts.items()}
    merges = []
    for _ in range(n_merges):
        pairs = Counter()
        for symbols, c in vocab.items():
            for pair in zip(symbols, symbols[1:]):
                pairs[pair] += c
        (a, b), freq = pairs.most_common(1)[0]        # ties: first pair seen
        merges.append((a + b, freq))
        new_vocab = {}
        for symbols, c in vocab.items():
            out, i = [], 0
            while i < len(symbols):
                if symbols[i:i + 2] == (a, b):
                    out.append(a + b); i += 2
                else:
                    out.append(symbols[i]); i += 1
            new_vocab[tuple(out)] = c
        vocab = new_vocab
    return merges, vocab

counts = {"low": 5, "lower": 2, "newest": 6, "widest": 3}
merges, vocab = learn_bpe(counts, 5)
print(merges)
for symbols, c in vocab.items():
    print(c, " ".join(symbols))

The five merges are es, est, est</w>, lo and low. The superlative suffix becomes one symbol, so the unseen word “lowest” is segmented as low + est</w>.

1.3 n-gram language models and perplexity

Definition 1.4 — n-gram language model An n-gram model assumes that each token depends only on the n - 1 previous ones. A sentence is padded with n - 1 start symbols <s> and an end symbol </s>, and P(w_1, \dots, w_m) = \prod_{t=1}^{m+1} P(w_t \mid w_{t-n+1}, \dots, w_{t-1}) .

For n = 2 (bigrams), the parameters are the conditional laws P(w \mid u) for each context word u.

Proposition 1.5 — Maximum likelihood estimate Let c(u, w) count the bigram u\,w in the training corpus and c(u) = \sum_w c(u, w). The bigram model of maximal likelihood is \hat P(w \mid u) = \frac{c(u, w)}{c(u)} .

Proof. The log-likelihood is a sum of independent terms, one per context u: \sum_u \sum_w c(u, w) \log P(w \mid u) = -\sum_u c(u)\, H\Big(\frac{c(u, \cdot)}{c(u)},\, P(\cdot \mid u)\Big).

Each cross-entropy is minimal when the model law equals the empirical law, by Gibbs’ inequality (Proposition 0.2). ∎

Definition 1.6 — Perplexity The perplexity of a model P on a test text of N predicted tokens is the exponential of its average negative log-likelihood:

Key result — perplexity \mathrm{PP} = \exp\Big( -\frac{1}{N} \sum_{t=1}^{N} \log P(w_t \mid w_{<t}) \Big)

A uniform model over V has perplexity |V|: perplexity is an effective number of equally likely choices per token. It compares models only under the same tokenisation.

The maximum likelihood estimate gives probability zero, hence infinite perplexity, to any test text with an unseen bigram. Smoothing moves some mass to unseen events.

Definition 1.7 — Add-k smoothing For k > 0, P_k(w \mid u) = \frac{c(u, w) + k}{c(u) + k\, |V|} .

With k = 1 it is Laplace smoothing. Kneser–Ney smoothing, which does better, is in ch. 13.

Code 1.8 — Perplexity of unigram and bigram models

import math
from collections import Counter

train = ["<s> the cat sat on the mat </s>", "<s> the dog sat on the log </s>",
         "<s> the cat saw the dog </s>"]
test = ["<s> the dog sat on the mat </s>", "<s> the dog saw the cat </s>"]   # "dog saw" is unseen
tok = [s.split() for s in train]
V = sorted({w for s in tok for w in s} - {"<s>"})     # predicted vocabulary (|V| = 9)
uni = Counter(w for s in tok for w in s[1:])
big = Counter((u, w) for s in tok for u, w in zip(s, s[1:]))
ctx = Counter(u for s in tok for u in s[:-1])

def p_bigram(w, u, k):
    """Add-k smoothed bigram probability P(w | u)."""
    return (big[u, w] + k) / (ctx[u] + k * len(V))

def perplexity(sents, logp):
    lp, n = 0.0, 0
    for s in sents:
        w = s.split()
        for u, v in zip(w, w[1:]):
            lp += logp(v, u); n += 1
    return math.exp(-lp / n)

N = sum(uni.values())
print("unigram (MLE)   ", round(perplexity(test, lambda w, u: math.log(uni[w] / N)), 2))
mle = lambda w, u: math.log(big[u, w] / ctx[u]) if big[u, w] else -math.inf
print("bigram (MLE)    ", perplexity(test, mle))
for k in [1.0, 0.1, 0.01]:
    print(f"bigram add-{k:<5}", round(perplexity(test, lambda w, u: math.log(p_bigram(w, u, k))), 2))

The unigram model has perplexity 7.45; the unsmoothed bigram model, infinite. Add-k smoothing gives 5.17 for k = 1, 3.25 for k = 0.1 and 3.65 for k = 0.01: too much smoothing flattens the model, too little starves the unseen bigram, so k is tuned on held-out data.

Key takeaways — chapter 1

  • The field swung from rules to statistics to neural networks.
  • Zipf’s law guarantees unseen words; subword tokenisers such as BPE remove them.
  • The n-gram MLE is a table of relative counts (Proposition 1.5); it must be smoothed.
  • Perplexity is the exponential of the per-token cross-entropy; it compares models under one tokenisation.

2. Speech features and signal processing

A recogniser reads one feature vector every 10 ms, which keeps what distinguishes speech sounds and drops the rest, such as pitch. This chapter derives the classical pipeline, from sampling to MFCCs, from a model of speech production.

2.1 Sampling and the source–filter model

Definition 2.1 — Sampled signal A signal x(t) sampled at rate f_s gives x_n = x(n / f_s). By the Shannon sampling theorem, it is recoverable from its samples if it has no energy above f_s / 2. Speech corpora usually use 16 kHz, telephone speech 8 kHz.

Definition 2.2 — Source–filter model Over a few tens of milliseconds, speech is an excitation e filtered by the vocal tract, of impulse response h: s = e * h .

For vowels, e is a train of glottal pulses at the fundamental frequency f_0, roughly 80 to 250 Hz for adults; for sounds such as /s/, it is noise. The resonances of the tract, the formants, identify the vowel: /a/ has formants near 700 and 1100–1200 Hz.

The phoneme is carried by h, the pitch by e. A good feature keeps the spectral envelope (h) and discards the harmonic fine structure (e).

2.2 The short-time Fourier transform

Definition 2.3 — STFT and spectrogram Cut the signal into frames of L samples every R samples, multiply each frame by a window w (Hamming or Hann), and take a DFT of size N \geq L (Definition 0.7): X_m(k) = \sum_{n=0}^{L-1} x_{mR + n}\, w_n\, e^{-2 i \pi k n / N} .

The power spectrogram is |X_m(k)|^2. The standard choice, L = 25 ms and R = 10 ms, is short enough for the tract to be stationary and long enough to hold two or three pitch periods. A pre-emphasis filter x_n - 0.97\, x_{n-1} is often applied first, to lift high frequencies.

2.3 The mel filterbank

Pitch perception is roughly linear below 1 kHz and logarithmic above; the mel scale imitates it.

Definition 2.4 — Mel scale and log-mel energies The mel scale is \mathrm{mel}(f) = 2595 \log_{10}\Big(1 + \frac{f}{700}\Big), so that \mathrm{mel}(1000) \approx 1000. Place M triangular filters F_j with centres equally spaced in mel up to f_s / 2, each spanning the centres of its two neighbours. The log-mel energies of frame m are E_m(j) = \log \sum_{k} F_j(k)\, |X_m(k)|^2, \qquad j = 1, \dots, M, with M = 40 or 80 in practice.

2.4 The cepstrum and MFCCs

Proposition 2.5 — The logarithm separates source and filter Under the model of Definition 2.2, treated as a circular convolution within one frame, \log |S_k| = \log |E_k| + \log |H_k| \quad \text{for every bin } k .

The cepstrum, the inverse DFT of \log |S_k|, is the sum of the cepstra of e and h. The smooth envelope \log |H_k| occupies the low quefrencies; the harmonic comb \log |E_k| produces a peak at the pitch period 1 / f_0.

Proof. The convolution theorem (Proposition 0.8) gives S_k = E_k H_k; take moduli and logarithms, and use the linearity of the inverse DFT. A slowly varying function of k has its inverse DFT near quefrency 0; a comb of spacing f_0 has it at multiples of 1 / f_0. Windowing makes the separation approximate. ∎

Definition 2.6 — Mel-frequency cepstral coefficients (MFCC) The MFCCs of frame m are the first coefficients, usually 13, of the type-II discrete cosine transform of the log-mel energies: c_m(q) = \sum_{j=1}^{M} E_m(j) \cos\Big(\frac{\pi q (j - 1/2)}{M}\Big), \qquad q = 0, \dots, 12 .

They are the low-quefrency part of a mel-warped cepstrum: they describe the envelope (Proposition 2.5), and they are nearly decorrelated, which suited the diagonal Gaussians of HMM recognisers (ch. 3). Time differences (deltas) are often appended.

Code 2.7 — From a waveform to log-mel features and MFCCs

import numpy as np
from scipy.fft import dct

sr = 16000
t = np.arange(sr // 2) / sr                         # 0.5 s of a synthetic vowel /a/
f0, formants = 120.0, np.array([700.0, 1220.0, 2600.0])
x = np.zeros_like(t)
for h in range(1, 60):                              # harmonics of f0 shaped by formants
    gain = np.exp(-0.5 * ((h * f0 - formants) / 100.0) ** 2) @ [1.0, 0.8, 0.3]
    x += gain * np.sin(2 * np.pi * h * f0 * t)
x += 0.01 * np.random.default_rng(0).normal(size=x.size)

def hz_to_mel(f): return 2595 * np.log10(1 + f / 700)
def mel_to_hz(m): return 700 * (10 ** (m / 2595) - 1)

def log_mel(x, sr, n_fft=512, win=400, hop=160, n_mels=40):
    x = np.append(x[0], x[1:] - 0.97 * x[:-1])                         # pre-emphasis
    n_frames = 1 + (len(x) - win) // hop
    frames = np.stack([x[i * hop: i * hop + win] for i in range(n_frames)]) * np.hamming(win)
    power = np.abs(np.fft.rfft(frames, n_fft)) ** 2 / n_fft           # (frames, 257)
    edges = mel_to_hz(np.linspace(0, hz_to_mel(sr / 2), n_mels + 2))   # triangle corners
    bins = np.fft.rfftfreq(n_fft, 1 / sr)
    fb = np.zeros((n_mels, bins.size))
    for m in range(n_mels):
        lo, c, hi = edges[m: m + 3]
        fb[m] = np.clip(np.minimum((bins - lo) / (c - lo), (hi - bins) / (hi - c)), 0, None)
    return np.log(power @ fb.T + 1e-10), edges[1:-1]

L, centres = log_mel(x, sr)
mfcc = dct(L, type=2, norm="ortho", axis=1)[:, :13]                    # keep 13 cepstra
print("log-mel", L.shape, "MFCC", mfcc.shape)
avg = L.mean(0)
peaks = [m for m in range(1, 39) if avg[m] > avg[m - 1] and avg[m] > avg[m + 1]]
print("peaks of the mel spectrum at", np.round(centres[peaks]).astype(int), "Hz")

Half a second gives 48 frames of 40 log-mel energies and 13 MFCCs. The mel spectrum peaks near 764, 1171 and 2554 Hz, the three formants. Two weak bumps near 92 and 195 Hz are not harmonics, which have almost no energy there, but window leakage sampled unevenly by the lowest filters, only three or four DFT bins wide.

Example 2.8 — Orders of magnitude At 16 kHz, a 25 ms window holds 400 samples and a 10 ms hop is 160 samples. One second of speech gives 1 + \lfloor (16000 - 400) / 160 \rfloor = 98 \text{ frames}.

A DFT of size 512 has bins every 16000 / 512 = 31.25 Hz; a pitch period at 120 Hz lasts 8.3 ms, so three fit in a window.

Key takeaways — chapter 2

  • Speech is an excitation (pitch or noise) filtered by the vocal tract (formants); the phoneme lives in the filter.
  • The STFT with 25 ms windows every 10 ms gives about 100 frames per second.
  • Log-mel energies warp frequency like hearing; they are the standard input of neural recognisers (ch. 10).
  • The logarithm turns the source–filter convolution into a sum; the DCT keeps the envelope in 13 MFCCs.

3. Hidden Markov models and finite-state modelling

Speech frames are produced by a hidden sequence of sounds, and words by a hidden sequence of tags. Finite-state machines and hidden Markov models describe such hidden sequences; three dynamic programs make them usable: likelihood, best path and learning.

3.1 Weighted finite-state machines

Definition 3.1 — Weighted finite-state transducer (WFST) A WFST over a semiring \mathbb{K} (Definition 0.5) has a finite set of states, an initial state, final states, and weighted transitions p \xrightarrow{\ a \,:\, b \,/\, w\ } q that read an input symbol a, write an output symbol b (possibly empty) and carry a weight w \in \mathbb{K}.

The weight of a pair of strings is the \oplus-sum, over the accepting paths that read the first and write the second, of the \otimes-product of their weights.

Definition 3.2 — Composition The composition T_1 \circ T_2 maps x to z with weight (T_1 \circ T_2)(x, z) = \bigoplus_{y} T_1(x, y) \otimes T_2(y, z) .

Its states are pairs of states; a transition of T_1 writing y is matched with one of T_2 reading y.

A recogniser composes four transducers (Mohri, Pereira and Riley, 2002): HMM states to context-dependent phones (H), to phones (C), to words through the pronunciation lexicon (L), and the n-gram grammar of ch. 1 (G). Decoding searches the best path of H \circ C \circ L \circ G in the tropical semiring.

3.2 Hidden Markov models

Definition 3.3 — Hidden Markov model An HMM with K hidden states has an initial law \pi, a transition matrix A with rows summing to one, and emission laws b_j. A hidden path q_1, \dots, q_T and observations o_1, \dots, o_T have joint probability P(q, o) = \pi_{q_1} b_{q_1}(o_1) \prod_{t=2}^{T} A_{q_{t-1} q_t}\, b_{q_t}(o_t) .

Theorem 3.4 — Forward algorithm Let \alpha_t(j) = P(o_1, \dots, o_t, q_t = j). Then \alpha_1(j) = \pi_j b_j(o_1),

Key result — the forward recursion \alpha_t(j) = \Big( \sum_{i=1}^{K} \alpha_{t-1}(i)\, A_{ij} \Big)\, b_j(o_t), \qquad P(o_1, \dots, o_T) = \sum_{j} \alpha_T(j),

and the likelihood costs O(T K^2) operations instead of K^T terms.

Proof. The joint law of Definition 3.3 is a product of edge weights along a path in a layered graph with K states per step. Apply Proposition 0.6 in the probability semiring, then sum over the last state. ∎

Theorem 3.5 — Viterbi algorithm Let \delta_t(j) be the largest joint probability of a path ending in state j at time t. Then \delta_t(j) = \max_{i} \delta_{t-1}(i)\, A_{ij}\, b_j(o_t), and back-pointers to the maximising i, read backwards from \arg\max_j \delta_T(j), give the best path.

Proof. Same graph, in the Viterbi semiring, where \max distributes over products of non-negative numbers. A prefix of an optimal path is optimal among the paths reaching the same state, so the back-pointers recover an optimal path. ∎

Code 3.6 — Forward and Viterbi, checked by brute force

import itertools
import numpy as np

pi = np.array([0.8, 0.2])                        # states: 0 = hot, 1 = cold
A = np.array([[0.6, 0.4], [0.5, 0.5]])           # A[i, j] = P(q_t = j | q_{t-1} = i)
B = np.array([[0.2, 0.4, 0.4], [0.5, 0.4, 0.1]]) # B[i, o] = P(o | i), o = 1, 2, 3 ice creams
obs = [2, 0, 2]                                  # observed 3, 1, 3 (0-based symbols)

def forward(obs):
    alpha = pi * B[:, obs[0]]
    for o in obs[1:]:
        alpha = (alpha @ A) * B[:, o]            # O(K^2) per step
    return alpha.sum()

def viterbi(obs):
    delta, back = np.log(pi * B[:, obs[0]]), []
    for o in obs[1:]:
        scores = delta[:, None] + np.log(A)      # scores[i, j]: best path ending i -> j
        back.append(scores.argmax(0))
        delta = scores.max(0) + np.log(B[:, o])
    path = [int(delta.argmax())]
    for bp in reversed(back):
        path.insert(0, int(bp[path[0]]))
    return path, np.exp(delta.max())

def joint(q, obs):
    p = pi[q[0]] * B[q[0], obs[0]]
    for t in range(1, len(obs)):
        p *= A[q[t - 1], q[t]] * B[q[t], obs[t]]
    return p

paths = list(itertools.product([0, 1], repeat=len(obs)))
print("P(O): forward", round(forward(obs), 6), " brute force", round(sum(joint(q, obs) for q in paths), 6))
best = max(paths, key=lambda q: joint(q, obs))
path, p = viterbi(obs)
print("Viterbi", path, round(p, 6), " brute force", list(best), round(joint(best, obs), 6))

Both methods give P(O) = 0.028562 and the best path hot, cold, hot with probability 0.0128.

Example 3.7 — The same numbers by hand For the observations 3, 1, 3:

  • \alpha_1 = (0.8 \cdot 0.4,\ 0.2 \cdot 0.1) = (0.32,\ 0.02);
  • the second step mixes the two states, then emits “1”: \alpha_2 = \big( (0.32 \cdot 0.6 + 0.02 \cdot 0.5) \cdot 0.2,\ (0.32 \cdot 0.4 + 0.02 \cdot 0.5) \cdot 0.5 \big) = (0.0404,\ 0.069);
  • \alpha_3 = (0.023496,\ 0.005066), whose sum is the likelihood: P(O) = 0.028562 .

Viterbi replaces sums by maxima: \delta_2 = (0.0384,\ 0.064), then \delta_3(\text{hot}) = 0.064 \cdot 0.5 \cdot 0.4 = 0.0128, from “cold”.

3.3 Learning: the Baum–Welch algorithm

Without observed states, parameters are learned by EM. The backward variables \beta_t(i) = P(o_{t+1}, \dots, o_T \mid q_t = i) satisfy the mirror recursion \beta_t(i) = \sum_j A_{ij}\, b_j(o_{t+1})\, \beta_{t+1}(j), \qquad \beta_T(i) = 1 .

They give the posteriors of one state and of one transition: \gamma_t(i) = \frac{\alpha_t(i) \beta_t(i)}{P(O)}, \qquad \xi_t(i, j) = \frac{\alpha_t(i) A_{ij} b_j(o_{t+1}) \beta_{t+1}(j)}{P(O)} .

Theorem 3.8 — Baum–Welch re-estimation The updates \pi_i \leftarrow \gamma_1(i), \qquad A_{ij} \leftarrow \frac{\sum_{t<T} \xi_t(i, j)}{\sum_{t<T} \gamma_t(i)}, \qquad b_i(v) \leftarrow \frac{\sum_{t : o_t = v} \gamma_t(i)}{\sum_{t} \gamma_t(i)} never decrease the likelihood P(O).

Proof. This is EM. The E-step computes the posteriors; the M-step maximises the expected complete log-likelihood, which splits into one multinomial estimate per row, with fractional counts (Proposition 1.5). EM raises a lower bound of \log P(O) that is tight at the current parameters, so the likelihood cannot decrease. Derivation and code: ch. 13, 13.2. ∎

3.4 HMM-based speech recognition

Definition 3.9 — The noisy channel for recognition Given acoustic frames O, the recogniser outputs \hat W = \arg\max_{W} P(O \mid W)\, P(W), an acoustic model times a language model (ch. 1). Each phone is a three-state left-to-right HMM; words and sentences are paths in the composed graph of section 3.1.

The emissions were Gaussian mixtures over MFCCs until about 2010. Hybrid systems then used a neural network predicting the state posterior P(q \mid o_t), divided by the prior P(q) to give a scaled likelihood. Ch. 10 removes the HMM altogether.

Key takeaways — chapter 3

  • WFSTs are weighted relations between strings; composition chains acoustics, lexicon and grammar.
  • Forward, Viterbi and backward are one dynamic program in the probability or Viterbi semiring, in O(TK^2).
  • Baum–Welch is EM: posteriors from forward–backward, then fractional-count estimates; the likelihood never decreases.
  • Recognition is \arg\max_W P(O \mid W) P(W); neural networks entered as emission models before replacing the HMM.

4. Sequence labelling

Many tasks give one label per token: a part of speech, an entity type, a slot of a request. This chapter encodes spans as tags, replaces the HMM tagger by a discriminative conditional random field, and scores taggers by spans.

4.1 Tasks and the BIO encoding

Part-of-speech tagging assigns each word a category such as noun or verb (17 in the Universal Dependencies tag set). Named-entity recognition finds spans naming persons, organisations or locations; CoNLL-2003 (Tjong Kim Sang and De Meulder, 2003) uses four types.

Definition 4.1 — BIO encoding A set of non-overlapping typed spans is encoded with one tag per token: B-X opens a span of type X, I-X continues it, and O marks tokens outside every span. For example:

Ada Lovelace was born in London .
B-PER I-PER O O O B-LOC O

A tag I-X may only follow B-X or I-X; decoders forbid the other transitions.

An HMM tagger (ch. 3) must generate every word, so it cannot easily use overlapping clues such as capitalisation, suffixes or the next word.

4.2 Linear-chain conditional random fields

Definition 4.2 — Linear-chain CRF Given a sentence x of length T and feature functions f(y_{t-1}, y_t, x, t) \in \mathbb{R}^d, a CRF with weights \theta defines p_\theta(y \mid x) = \frac{1}{Z_\theta(x)} \exp\Big( \sum_{t=1}^{T} \theta^\top f(y_{t-1}, y_t, x, t) \Big), where Z_\theta(x) sums the numerator over all K^T tag sequences (Lafferty, McCallum and Pereira, 2001).

Proposition 4.3 — Inference in a CRF Z_\theta(x), the marginals p_\theta(y_t \mid x) and the best sequence \arg\max_y p_\theta(y \mid x) are computed in O(TK^2).

Proof. Each sequence is a path in a layered graph with edge weights \exp(\theta^\top f(y_{t-1}, y_t, x, t)). Proposition 0.6 gives Z; forward and backward passes give the marginals (ch. 3); the Viterbi semiring gives the best path. ∎

Theorem 4.4 — Gradient and concavity of the CRF likelihood For a training pair (x, y), the log-likelihood \ell(\theta) = \log p_\theta(y \mid x) is concave, with gradient \nabla \ell(\theta) = \sum_{t} f(y_{t-1}, y_t, x, t) - \mathbb{E}_{y' \sim p_\theta(\cdot \mid x)} \Big[ \sum_t f(y'_{t-1}, y'_t, x, t) \Big] .

Proof. Write F(y') = \sum_t f(y'_{t-1}, y'_t, x, t), so that \ell(\theta) = \theta^\top F(y) - \log Z_\theta(x). Differentiating the log-sum-exp, as in Proposition 0.4, gives the expectation of F under p_\theta.

The Hessian of \log Z_\theta is the covariance matrix of F(y') under p_\theta, which is positive semi-definite, so \ell is concave. The expectation needs only the pairwise marginals, given by forward–backward. ∎

At the optimum, expected feature counts equal observed ones, as for logistic regression.

Code 4.5 — The forward algorithm computes log Z, and its gradient gives the marginals

import itertools
import torch
torch.set_num_threads(1); torch.manual_seed(0)
T, K = 5, 3                                      # 5 tokens, tags {O, B, I}
E = torch.randn(T, K, requires_grad=True)        # emission scores psi_t(y_t) = w . f(x, t, y_t)
W = torch.randn(K, K)                            # transition scores between consecutive tags

def log_partition(E, W):
    """Forward algorithm in the log semiring: log of the sum over all K^T tag sequences."""
    a = E[0]
    for t in range(1, len(E)):
        a = torch.logsumexp(a[:, None] + W, dim=0) + E[t]
    return torch.logsumexp(a, dim=0)

def score(y):
    return E[torch.arange(T), y].sum() + W[y[:-1], y[1:]].sum()

logZ = log_partition(E, W)
seqs = torch.tensor(list(itertools.product(range(K), repeat=T)))     # 243 sequences
brute = torch.logsumexp(torch.stack([score(y) for y in seqs]), 0)
print("log Z forward", round(logZ.item(), 4), " brute force", round(brute.item(), 4))

logZ.backward()                                  # d log Z / d E[t, k] = P(y_t = k | x)
probs = torch.softmax(torch.stack([score(y) for y in seqs]).detach(), 0)
marg = torch.stack([probs[seqs[:, 2] == k].sum() for k in range(K)])
print("gradient at t=2", E.grad[2].numpy().round(4), " marginals", marg.numpy().round(4))

Both computations give \log Z = 7.3242, and the gradient of \log Z with respect to the scores of token 3 equals its marginals (0.4238, 0.3264, 0.2497): differentiating the forward algorithm is the backward pass.

4.3 Neural taggers, a preview of ch. 6–7

A bidirectional LSTM (ch. 6) can produce the emission scores of a CRF layer (Lample and coauthors, 2016). With a pretrained transformer (ch. 7), a linear layer on each token’s representation often suffices; a word split into subwords takes the label of its first subword.

4.4 Evaluation by spans

Definition 4.6 — Span-level precision, recall and F1 A predicted span counts as correct only if its boundaries and its type match a gold span exactly. With c correct spans, p predicted and g gold: P = \frac{c}{p}, \qquad R = \frac{c}{g}, \qquad F_1 = \frac{2 P R}{P + R} .

Example 4.7 — Token accuracy hides span errors The gold tags of “Ada Lovelace was born in London .” contain two spans. A tagger that outputs B-PER O O O O B-LOC O is right on 6 tokens out of 7, an accuracy of 86 %. It finds one span out of two, and predicts one wrong span (“Ada” alone), so P = R = 1/2 and F_1 = 0.5 .

Key takeaways — chapter 4

  • BIO tags turn span extraction into one label per token.
  • A linear-chain CRF is a log-linear model over whole tag sequences; forward–backward gives Z and the marginals in O(TK^2).
  • Its log-likelihood is concave, with gradient “observed minus expected features” (Theorem 4.4).
  • Taggers are scored by exact-match span F1, which is much stricter than token accuracy.

5. Word embeddings

One-hot vectors make all words equally different. Embeddings place words in \mathbb{R}^d so that words used in similar contexts are close: “you shall know a word by the company it keeps” (Firth, 1957). This chapter builds them from counts, then shows that word2vec factorises the same counts.

5.1 Counting contexts

Definition 5.1 — Co-occurrence counts and PMI Let c(w, u) count how often the context word u appears within a fixed window around the word w, typically 2 to 10 tokens on each side. Write c(w) and c(u) for the marginal counts and D for the total. The pointwise mutual information and its positive part are \mathrm{PMI}(w, u) = \log \frac{c(w, u)\, D}{c(w)\, c(u)}, \qquad \mathrm{PPMI}(w, u) = \max(\mathrm{PMI}(w, u), 0) .

PMI compares observed co-occurrence with independence; it discounts frequent words such as “the”.

5.2 Low-rank factorisation

Definition 5.2 — Embeddings by truncated SVD Write the singular value decomposition of the PPMI matrix as M = U \Sigma V^\top. The d-dimensional embedding of word w is row w of U_d \Sigma_d^{1/2}, and similarity is measured by the cosine \cos(a, b) = \frac{a^\top b}{\|a\|\, \|b\|} .

By the Eckart–Young theorem, U_d \Sigma_d V_d^\top is the best rank-d approximation of M in Frobenius norm: it keeps the dominant structure of the counts and averages away noise.

Code 5.3 — PPMI and SVD on a synthetic corpus

import numpy as np
rng = np.random.default_rng(0)
groups = {"animal": ["cat", "dog", "horse", "cow"], "food": ["bread", "apple", "rice", "cheese"],
          "vehicle": ["car", "bus", "truck", "bike"], "place": ["road", "street", "bridge", "lane"]}
templates = [["the", "animal", "eats", "the", "food"], ["a", "animal", "sleeps"],
             ["food", "is", "tasty"], ["the", "vehicle", "drives", "on", "the", "place"],
             ["a", "vehicle", "is", "fast"], ["the", "place", "is", "wide"]]
corpus = []
for _ in range(4000):                                    # sample sentences from templates
    tpl = templates[rng.integers(len(templates))]
    corpus += [groups[w][rng.integers(4)] if w in groups else w for w in tpl] + ["."]
vocab = sorted(set(corpus)); idx = {w: i for i, w in enumerate(vocab)}
C = np.zeros((len(vocab), len(vocab)))
for i, w in enumerate(corpus):                           # symmetric window of 2 words
    for j in range(max(0, i - 2), min(len(corpus), i + 3)):
        if j != i:
            C[idx[w], idx[corpus[j]]] += 1
pmi = np.log(C * C.sum() / (C.sum(1, keepdims=True) * C.sum(0, keepdims=True)) + 1e-12)
ppmi = np.maximum(pmi, 0)                                # positive PMI
U, S, _ = np.linalg.svd(ppmi)
emb = U[:, :8] * np.sqrt(S[:8])                          # 8-dimensional word vectors
emb /= np.linalg.norm(emb, axis=1, keepdims=True)
for w in ["cat", "bread", "car", "road"]:
    sims = emb @ emb[idx[w]]
    print(f"{w:6s}->", [vocab[k] for k in np.argsort(-sims) if vocab[k] != w][:3])

The nearest neighbours of “cat” are the other animals, those of “bread” the other foods, and likewise for vehicles and places. Two animals almost never appear within two words of each other: they are close because they share contexts.

5.3 Skip-gram with negative sampling

Definition 5.4 — Skip-gram with negative sampling (SGNS) word2vec (Mikolov and coauthors, 2013) gives each word a vector \mathbf{w} and each context a vector \mathbf{u}. For each observed pair (w, u) it draws k negative contexts u_1', \dots, u_k' from the unigram law P_n and maximises \log \sigma(\mathbf{w}^\top \mathbf{u}) + \sum_{i=1}^{k} \log \sigma(-\mathbf{w}^\top \mathbf{u}_i'), \qquad \sigma(s) = \frac{1}{1 + e^{-s}} .

Theorem 5.5 — SGNS factorises a shifted PMI matrix (Levy and Goldberg, 2014) If the dimension is large enough for each inner product to be set freely, and P_n(u) = c(u)/D, the SGNS objective summed over the corpus is maximised when \mathbf{w}^\top \mathbf{u} = \mathrm{PMI}(w, u) - \log k \quad \text{for every pair with } c(w, u) > 0 .

Proof. Summed over the corpus, the expected objective is a sum of independent terms, one per pair, in the variable s = \mathbf{w}^\top \mathbf{u}: \ell(s) = c(w, u) \log \sigma(s) + k \frac{c(w)\, c(u)}{D} \log \sigma(-s) .

Using \sigma'(s) = \sigma(s)\sigma(-s), the derivative is c(w, u)\, \sigma(-s) - k\, c(w) c(u) D^{-1}\, \sigma(s). It vanishes when e^{s} = \frac{\sigma(s)}{\sigma(-s)} = \frac{c(w, u)\, D}{k\, c(w)\, c(u)}, that is s = \mathrm{PMI}(w, u) - \log k. Each \ell is strictly concave, so this is the maximum. ∎

Code 5.6 — Checking Theorem 5.5 numerically (continues Code 5.3)

import torch
torch.set_num_threads(1); torch.manual_seed(0)
Ct = torch.tensor(C, dtype=torch.float32)
D = Ct.sum(); k = 5                                     # k negative samples per positive pair
neg = k * Ct.sum(1, keepdim=True) * Ct.sum(0, keepdim=True) / D   # expected negatives
V, d = C.shape[0], C.shape[0]                           # full rank: d = |V|
Wv = (0.1 * torch.randn(V, d)).requires_grad_(); Cv = (0.1 * torch.randn(V, d)).requires_grad_()
opt = torch.optim.Adam([Wv, Cv], lr=0.05)
for step in range(3000):                                # SGNS objective summed over the corpus
    s = Wv @ Cv.T
    loss = -(Ct * torch.nn.functional.logsigmoid(s) + neg * torch.nn.functional.logsigmoid(-s)).sum() / D
    opt.zero_grad(); loss.backward(); opt.step()
s = (Wv @ Cv.T).detach().numpy()
mask = C >= 20                                          # pairs seen at least 20 times
gap = np.abs(s - (pmi - np.log(k)))[mask].max()
print("pairs:", mask.sum(), " max |w.u - (PMI - log k)| =", gap.round(3))

On the 263 pairs seen at least 20 times, the learned inner products match \mathrm{PMI} - \log 5 within 0.002.

word2vec, GloVe and PPMI–SVD thus factorise the same statistics. GloVe (Pennington, Socher and Manning, 2014) fits \mathbf{w}^\top \mathbf{u} + b_w + b_u \approx \log c(w, u) by weighted least squares. fastText (Bojanowski and coauthors, 2017) sums the vectors of a word’s character n-grams, so it embeds unseen words.

5.4 Analogies, limits and bias

Differences of vectors encode some relations (Mikolov, Yih and Zweig, 2013). The nearest word to \mathbf{w}_{\text{king}} - \mathbf{w}_{\text{man}} + \mathbf{w}_{\text{woman}} is often “queen”, once the three query words are excluded. The same arithmetic reproduces stereotypes of the training text, for instance between gender and occupations (Bolukbasi and coauthors, 2016). A static embedding also gives one vector per type, whatever its sense; contextual representations (ch. 7) fix this.

Key takeaways — chapter 5

  • Words are represented by their contexts; PPMI reweights co-occurrence counts.
  • A truncated SVD gives the best low-rank summary of the PPMI matrix.
  • SGNS implicitly factorises the PMI matrix shifted by \log k (Theorem 5.5).
  • Embeddings support analogies but inherit the biases of their corpus, and give one vector per type.

6. Deep learning for NLP: recurrent networks, attention and transformers

An n-gram model forgets everything beyond a few tokens; neural sequence models keep a learned summary of the prefix. This chapter presents the two answers, recurrence and attention, and why the second won.

6.1 Recurrent networks and their gradients

Definition 6.1 — Recurrent neural network (RNN) Given input vectors x_1, \dots, x_T (embeddings, ch. 5), an Elman RNN updates a hidden state h_t = \tanh(W h_{t-1} + U x_t + b), and predicts from h_t, for example the next token by \mathrm{softmax}(V h_t). Backpropagation through time differentiates the network unrolled over the sequence, a deep network with shared weights.

Proposition 6.2 — Vanishing gradients For t < T, in operator norm, \Big\| \frac{\partial h_T}{\partial h_t} \Big\| \leq \|W\|^{T - t} .

If \|W\| < 1, the influence of h_t on time T decays exponentially with the distance.

Proof. The chain rule gives a product of Jacobians: \frac{\partial h_T}{\partial h_t} = \prod_{s = t+1}^{T} \mathrm{diag}\big(1 - h_s^2\big)\, W .

Each diagonal factor has norm at most 1 because |\tanh'| \leq 1, and the operator norm is sub-multiplicative. ∎

When \|W\| > 1 the product can explode instead, and gradients are clipped (Pascanu, Mikolov and Bengio, 2013).

Definition 6.3 — Long short-term memory (LSTM) An LSTM (Hochreiter and Schmidhuber, 1997) adds a memory cell c_t controlled by three gates: forget f_t, input i_t and output o_t. Each gate is a sigmoid of an affine function of the previous state and the current input, with values in (0, 1), and c_t = f_t \odot c_{t-1} + i_t \odot \tanh(W_c h_{t-1} + U_c x_t + b_c), \qquad h_t = o_t \odot \tanh(c_t) .

Along the direct path through the cell, the Jacobian \partial c_t / \partial c_{t-1} is \mathrm{diag}(f_t), with no weight matrix: when f_t stays close to 1, the error signal crosses many steps intact.

6.2 Attention

An encoder–decoder encodes the source into states h_1, \dots, h_S and decodes the target token by token. A single vector cannot hold a long sentence; attention lets each decoding step look at all source states (Bahdanau, Cho and Bengio, 2015).

Definition 6.4 — Attention as a soft lookup For a query q, keys k_s and values v_s, attention returns \mathrm{att}(q) = \sum_{s=1}^{S} a_s v_s, \qquad a = \mathrm{softmax}\big(\mathrm{score}(q, k_1), \dots, \mathrm{score}(q, k_S)\big) .

The output is a convex combination of the values.

Definition 6.5 — Scaled dot-product attention Stack T queries, keys and values in matrices Q, K \in \mathbb{R}^{T \times d} and V. Then \mathrm{Attention}(Q, K, V) = \mathrm{softmax}\Big( \frac{Q K^\top}{\sqrt{d}} \Big) V, with the softmax applied row by row. In self-attention, the three matrices are linear maps of the same sequence X: Q = X W_Q, \qquad K = X W_K, \qquad V = X W_V .

A causal mask sets the scores of future positions to -\infty, so that position t only sees positions up to t.

Proposition 6.6 — Why divide by \sqrt{d} If q, k \in \mathbb{R}^d have independent entries of mean 0 and variance 1, then \mathbb{E}[q^\top k] = 0, \qquad \mathrm{Var}(q^\top k) = d .

Proof. q^\top k = \sum_{i=1}^d q_i k_i is a sum of d independent terms. Each has mean \mathbb{E}[q_i]\mathbb{E}[k_i] = 0 and variance \mathbb{E}[q_i^2]\mathbb{E}[k_i^2] = 1. ∎

Unscaled scores of standard deviation \sqrt{d}, 16 for d = 256, saturate the softmax into a near one-hot vector with vanishing gradient; dividing by \sqrt{d} restores variance 1.

Proposition 6.7 — Self-attention ignores order Without a mask and without positional information, self-attention is permutation-equivariant: for every permutation matrix P, \mathrm{Attention}(PX W_Q, PX W_K, PX W_V) = P\, \mathrm{Attention}(XW_Q, XW_K, XW_V) .

Proof. The scores become P Q K^\top P^\top; a row-wise softmax commutes with both permutations, and P^\top P = I cancels against PV. ∎

Order must be injected: the original transformer adds sinusoidal position vectors to the embeddings; rotary embeddings rotate queries and keys by angles proportional to the position (Su and coauthors, 2021).

6.3 The transformer block

Definition 6.8 — Transformer block A block maps H \in \mathbb{R}^{T \times d} to a matrix of the same shape, with layer normalisation (LN) before each sub-layer: H' = H + \mathrm{MHA}(\mathrm{LN}(H)), \qquad H'' = H' + \mathrm{FFN}(\mathrm{LN}(H')) .

Multi-head attention (MHA) runs h attentions of dimension d / h in parallel and concatenates them; the feed-forward network (FFN) is a two-layer perceptron of width about 4d at each position. Vaswani and coauthors (2017) placed LN after each residual sum; the pre-LN form above is now standard.

A layer costs O(T^2 d + T d^2) but processes all positions in parallel, where an RNN needs T sequential steps; this made training on billions of tokens possible (ch. 7).

Code 6.9 — Causality, equivariance and scaling

import torch
torch.set_num_threads(1); torch.manual_seed(0)

def attention(Q, K, V, causal=False):
    """Scaled dot-product attention softmax(Q K^T / sqrt(d)) V, optionally causal."""
    S = Q @ K.transpose(-2, -1) / K.shape[-1] ** 0.5
    if causal:
        T = S.shape[-1]
        S = S.masked_fill(torch.triu(torch.ones(T, T, dtype=torch.bool), 1), float("-inf"))
    return torch.softmax(S, dim=-1) @ V

T, d = 6, 64
X = torch.randn(T, d)
Wq, Wk, Wv = (torch.randn(d, d) / d ** 0.5 for _ in range(3))
Y = attention(X @ Wq, X @ Wk, X @ Wv, causal=True)
X2 = X.clone(); X2[4:] = torch.randn(2, d)              # change the last two tokens
Y2 = attention(X2 @ Wq, X2 @ Wk, X2 @ Wv, causal=True)
print("causal: outputs 0-3 unchanged:", torch.allclose(Y[:4], Y2[:4]), "| output 4 changed:", not torch.allclose(Y[4], Y2[4]))
P = torch.randperm(T)                                   # no positions: permutation equivariance
Z, Zp = attention(X @ Wq, X @ Wk, X @ Wv), attention(X[P] @ Wq, X[P] @ Wk, X[P] @ Wv)
print("equivariant:", torch.allclose(Z[P], Zp, atol=1e-6))
for dk in [16, 256]:                                    # why divide by sqrt(d)
    q, k = torch.randn(10000, dk), torch.randn(10000, dk)
    print(f"d = {dk:3d}: var(q.k) = {(q * k).sum(1).var():.1f}, after scaling = {((q * k).sum(1) / dk ** 0.5).var():.2f}")

Changing tokens 5 and 6 leaves the first four causal outputs unchanged; permuting inputs permutes outputs; the variances of q^\top k are 15.5 and 255.5 before scaling, 0.97 and 1.00 after.

Key takeaways — chapter 6

  • RNN gradients are products of Jacobians and vanish or explode exponentially with distance (Proposition 6.2).
  • The LSTM cell provides an additive path whose Jacobian is the forget gate.
  • Attention is a differentiable soft lookup; scaling by 1/\sqrt{d} keeps the scores of order 1.
  • Self-attention is blind to order without positions; transformers trade O(T^2) cost for parallelism.

7. Neural language modelling and large language models

Predicting the next token well requires syntax, facts and some reasoning. This chapter replaces count tables by neural networks, states the limit no model can beat, and follows the path from pretraining to an assistant.

7.1 Neural language models

Definition 7.1 — Neural language model A neural LM computes P_\theta(w_t \mid w_{<t}) = \mathrm{softmax}(V h_t) from a learned summary h_t of the prefix, trained by minimising the cross-entropy. For Bengio and coauthors (2003), h_t is a perceptron on the embeddings of the n - 1 previous tokens; in an RNN LM it is the recurrent state; in a transformer LM, the output of causal self-attention (ch. 6).

Shared embeddings generalise: having seen “the cat sat”, the model favours “the dog sat”, since the two animals have close vectors.

Proposition 7.2 — No model beats the source Let the text be drawn from a source p. For every model q and every position t, \mathbb{E}\big[-\log q(w_t \mid w_{<t})\big] \geq \mathbb{E}\big[-\log p(w_t \mid w_{<t})\big] , so the expected log-perplexity of any model is at least the conditional entropy of the source.

Proof. Given the prefix, the expectation over w_t is a cross-entropy between the two conditional laws, at least the entropy of the first (Proposition 0.2). Average over the prefix. ∎

Code 7.3 — A neural n-gram model against the entropy of its source

import numpy as np
import torch
torch.set_num_threads(1); torch.manual_seed(0); rng = np.random.default_rng(0)
V, N = 8, 30000                                          # a source of order 2 over 8 symbols
P = rng.dirichlet(0.3 * np.ones(V), size=(V, V))         # P[a, b] = law of x_t given (a, b)
u = rng.random(N); x = [0, 1]
for t in range(N):                                       # inverse-CDF sampling
    x.append(min(int(np.searchsorted(P[x[-2], x[-1]].cumsum(), u[t])), V - 1))
x = torch.tensor(x)
train, test = x[:25000], x[25000:]
true_nll = -np.log(P[test[:-2], test[1:-1], test[2:]]).mean()
print(f"true source: test perplexity {np.exp(true_nll):.3f}")

class MLPLM(torch.nn.Module):                            # Bengio-style neural n-gram model
    def __init__(self, n, d=16, h=64):
        super().__init__()
        self.emb = torch.nn.Embedding(V, d)
        self.mlp = torch.nn.Sequential(torch.nn.Linear(n * d, h), torch.nn.Tanh(), torch.nn.Linear(h, V))
    def forward(self, ctx):                              # ctx: (batch, n) previous symbols
        return self.mlp(self.emb(ctx).flatten(1))

for n in [1, 2]:
    model = MLPLM(n); opt = torch.optim.Adam(model.parameters(), lr=1e-2)
    windows = lambda s: torch.stack([s[i: len(s) - n + i] for i in range(n)], 1)
    for epoch in range(200):                             # full-batch training
        loss = torch.nn.functional.cross_entropy(model(windows(train)), train[n:])
        opt.zero_grad(); loss.backward(); opt.step()
    with torch.no_grad():
        ce = torch.nn.functional.cross_entropy(model(windows(test)), test[n:])
    print(f"context of {n} symbol(s): test perplexity {ce.exp():.3f}")

The true source has test perplexity 3.666. A model that sees one symbol is stuck at 7.164; with two, the true order, it reaches 3.692, just above the bound of Proposition 7.2.

7.2 Pretraining

Definition 7.4 — Pretraining objectives

  • Causal LM (GPT series): predict each token from its prefix; the model generates text.
  • Masked LM (BERT, Devlin and coauthors, 2019): replace 15 % of the tokens by a mask (80 % of the time), a random token (10 %) or themselves (10 %), and predict the originals from both sides; the model encodes text for classification and tagging.
  • Denoising (T5, Raffel and coauthors, 2020): corrupt input spans and regenerate them with an encoder–decoder.

The resulting vectors are contextual: “bank” differs in “river bank” and “bank loan”, unlike in ch. 5.

Definition 7.5 — Scaling law Hoffmann and coauthors (2022) fit the final loss of a model with N parameters trained on D tokens as L(N, D) = E + \frac{A}{N^{\alpha}} + \frac{B}{D^{\beta}}, and training costs about C \approx 6 N D floating-point operations.

Under a budget C, the optimal N and D grow as powers of C with similar exponents (Exercise 16), about 20 tokens per parameter: Chinchilla (70 billion parameters, 1.4 trillion tokens) beat larger models trained on less data.

7.3 Decoding

Definition 7.6 — Sampling with temperature, top-k and nucleus Given next-token logits z:

  • greedy decoding takes \arg\max_k z_k;
  • temperature sampling draws from \mathrm{softmax}(z / T) (Definition 0.3); T < 1 sharpens the law, T > 1 flattens it;
  • top-k sampling keeps the k most probable tokens and renormalises;
  • nucleus (top-p) sampling keeps the smallest set of most probable tokens whose mass reaches p, and renormalises (Holtzman and coauthors, 2020).

Greedy and beam search (ch. 8) give bland, repetitive open-ended text; nucleus sampling adapts the number of candidates to the model’s confidence.

Code 7.7 — Temperature and nucleus size

import numpy as np
rng = np.random.default_rng(0)
logits = np.array([3.0, 2.5, 2.0, 1.0, 0.5, 0.0, -1.0, -2.0])   # scores of 8 next tokens

def softmax(z, T=1.0):
    e = np.exp((z - z.max()) / T); return e / e.sum()

def nucleus(p, top_p=0.9):
    """Smallest set of most probable tokens whose mass reaches top_p, renormalised."""
    order = np.argsort(-p)
    keep = order[: np.searchsorted(np.cumsum(p[order]), top_p) + 1]
    q = np.zeros_like(p); q[keep] = p[keep]
    return q / q.sum()

for T in [0.5, 1.0, 2.0]:
    p = softmax(logits, T)
    H = -(p * np.log(p)).sum()
    print(f"T = {T}: entropy {H:.2f} nats, top-0.9 nucleus keeps {np.count_nonzero(nucleus(p))} tokens")
print("one nucleus sample at T = 1:", rng.choice(8, p=nucleus(softmax(logits))))

From T = 0.5 to 1 to 2, the entropy grows from 0.92 to 1.44 to 1.83 nats, and the 0.9-nucleus from 3 to 4 to 6 tokens.

7.4 From a language model to an assistant

A pretrained model continues text; an assistant follows instructions. Supervised fine-tuning on demonstrations comes first. Then reinforcement learning from human feedback (RLHF, Ouyang and coauthors, 2022) fits a reward model to human comparisons of answers and maximises it while staying close to the fine-tuned model; DPO (Rafailov and coauthors, 2023) reaches the same optimum by a logistic loss on the pairs (ch. 13, 13.4).

Key takeaways — chapter 7

  • Neural LMs generalise through embeddings; the source entropy bounds their perplexity (Proposition 7.2).
  • Causal, masked and denoising objectives give generators, encoders and encoder–decoders.
  • Under a compute budget C \approx 6ND, parameters and data should grow together.
  • Decoding is a design choice: temperature, top-k and nucleus sampling trade diversity for reliability.
  • Assistants add instruction tuning and preference optimisation (RLHF, DPO) to pretraining.

8. Machine translation

Translation was the first application of the field and remains its benchmark. This chapter follows its three paradigms, rules, alignment models learned by EM and neural encoder–decoders, then the metrics that decided between them.

8.1 From rules to statistics

Rule-based systems, from the 1954 Georgetown–IBM demonstration on, combined bilingual dictionaries with hand-written transfer rules. Statistical translation (Brown and coauthors, 1993) learned from parallel text, with the noisy channel \hat e = \arg\max_e P(f \mid e) P(e) for a foreign sentence f. Phrase-based systems (Koehn, Och and Marcu, 2003) dominated until about 2016.

8.2 Word alignment with IBM Model 1

Definition 8.1 — IBM Model 1 Let e = e_1 \dots e_m be a sentence and f = f_0 f_1 \dots f_l its source, where f_0 is a NULL word. Each target word e_j is aligned to one source position a_j, chosen uniformly, and then drawn from a translation table t(e_j \mid f_{a_j}): P(e, a \mid f) = \frac{\varepsilon}{(l + 1)^m} \prod_{j=1}^{m} t(e_j \mid f_{a_j}) .

In the noisy channel of 8.1, the roles of e and f are exchanged.

Proposition 8.2 — Likelihood and EM for Model 1 Summing over the (l + 1)^m alignments gives a product of sums: P(e \mid f) = \frac{\varepsilon}{(l + 1)^m} \prod_{j=1}^{m} \sum_{i=0}^{l} t(e_j \mid f_i) .

The alignment posterior factorises over target positions, P(a_j = i \mid e, f) \propto t(e_j \mid f_i), and EM updates t(e \mid f) to the expected count of the pair (e, f), divided by the expected count of f.

Proof. The sum over a = (a_1, \dots, a_m) of a product whose j-th factor depends only on a_j is the product of the sums (distributivity), and the posterior factorises likewise. The M-step is Proposition 1.5 with fractional counts. ∎

The log-likelihood, a sum of logarithms of linear functions of t, is concave: every local maximum is global.

Code 8.3 — EM on three sentence pairs (no NULL word)

from collections import defaultdict

pairs = [("das Haus", "the house"), ("das Buch", "the book"), ("ein Buch", "a book")]
pairs = [(f.split(), e.split()) for f, e in pairs]
E = {e for _, es in pairs for e in es}
t = defaultdict(lambda: 1.0 / len(E))                   # t[e, f] = P(e | f), uniform start
for it in range(1, 21):                                 # EM for IBM Model 1
    count, total = defaultdict(float), defaultdict(float)
    for fs, es in pairs:
        for e in es:
            z = sum(t[e, f] for f in fs)                # E-step: P(a_j = i | e, f)
            for f in fs:
                count[e, f] += t[e, f] / z; total[f] += t[e, f] / z
    for (e, f) in count:                                # M-step: renormalise expected counts
        t[e, f] = count[e, f] / total[f]
    if it in (1, 2, 5, 20):
        print(f"iteration {it:2d}: t(house|Haus) = {t['house', 'Haus']:.3f}, "
              f"t(the|das) = {t['the', 'das']:.3f}, t(book|ein) = {t['book', 'ein']:.3f}")

After one iteration the three printed entries are 0.5. After 20, t(\text{house} \mid \text{Haus}) = 0.968, t(\text{the} \mid \text{das}) = 1.000, and t(\text{book} \mid \text{ein}) has fallen to 0.032: “Buch” explains “book”, which frees “ein” to explain “a”.

8.3 Neural machine translation

A neural system is an attention encoder–decoder (ch. 6) trained to maximise \sum_j \log P_\theta(e_j \mid e_{<j}, f), with the reference prefix fed to the decoder (teacher forcing). Subwords (ch. 1) handle rare words; back-translating monolingual target text adds synthetic pairs (Sennrich, Haddow and Birch, 2016b).

Definition 8.4 — Beam search Keep the B best partial translations; at each step, extend each by every token, score by total log-probability, keep the best B, and set aside those ending with </s>. Return the finished hypothesis with the best score divided by its length to a power \lambda, since raw log-probabilities favour short outputs.

8.4 Evaluation

Definition 8.5 — BLEU (Papineni and coauthors, 2002) Let p_n be the corpus-level precision of the hypothesis n-grams, where each n-gram is counted at most as many times as it appears in the reference (clipping). Let h and r be the total hypothesis and reference lengths. Then \mathrm{BLEU} = \mathrm{BP} \cdot \exp\Big( \frac{1}{4} \sum_{n=1}^{4} \log p_n \Big), \qquad \mathrm{BP} = \min\big(1, e^{1 - r/h}\big) .

Code 8.6 — BLEU with clipping and brevity penalty

import math
from collections import Counter

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

def bleu(hyps, refs, N=4):
    """Corpus BLEU (Papineni et al., 2002): clipped n-gram precisions, brevity penalty."""
    match, total, h_len, r_len = [0] * N, [0] * N, 0, 0
    for h, r in zip(hyps, refs):
        h, r = h.split(), r.split()
        h_len += len(h); r_len += len(r)
        for n in range(1, N + 1):
            hn, rn = ngrams(h, n), ngrams(r, n)
            match[n - 1] += sum(min(c, rn[g]) for g, c in hn.items())   # clipping
            total[n - 1] += max(len(h) - n + 1, 0)
    prec = [m / t for m, t in zip(match, total)]
    bp = 1.0 if h_len > r_len else math.exp(1 - r_len / h_len)
    score = bp * math.exp(sum(math.log(p) for p in prec) / N) if min(prec) > 0 else 0.0
    return score, prec, bp

print(bleu(["the the the the the the the"], ["the cat is on the mat"], N=1))
refs = ["the cat is on the mat", "there is a book on the table"]
hyps = ["the cat sat on the mat", "there is a book on a table"]
score, prec, bp = bleu(hyps, refs)
print("BLEU =", round(100 * score, 1), " precisions", [round(p, 3) for p in prec], " BP", round(bp, 3))

Clipping limits “the the the …” to a unigram precision of 2/7. The two-sentence corpus has precisions 11/13, 7/11, 4/9 and 2/7, no brevity penalty, and \mathrm{BLEU} = 100 \cdot \Big( \frac{11}{13} \cdot \frac{7}{11} \cdot \frac{4}{9} \cdot \frac{2}{7} \Big)^{1/4} = 100 \cdot \Big( \frac{8}{117} \Big)^{1/4} \approx 51.1 .

BLEU rewards surface overlap: it misses paraphrases and depends on tokenisation, hence fixed tools such as sacreBLEU (Post, 2018). chrF compares character n-grams; COMET (Rei and coauthors, 2020) is a network trained to predict human judgements, and agrees better with them.

Key takeaways — chapter 8

  • IBM Model 1 explains a sentence pair by hidden word alignments; distributivity makes its likelihood a product of sums.
  • EM on Model 1 is a concave problem; it discovers translation pairs from co-occurrence alone.
  • Neural MT is an attention encoder–decoder decoded by beam search with length normalisation.
  • BLEU is a clipped n-gram precision with a brevity penalty; learned metrics such as COMET track human judgements better.

9. Sentiment analysis

Is this review positive? Is the customer annoyed with the battery or the screen? This chapter defines the sentiment tasks, shows that the classic baseline is a linear model, and locates where it breaks: negation and composition.

9.1 Tasks

Definition 9.1 — Sentiment tasks

  • Polarity classification: assign a document or sentence to positive or negative, sometimes with a neutral class.
  • Rating prediction: predict a score, such as 1 to 5 stars, by ordinal regression.
  • Aspect-based analysis: extract opinions as tuples (target entity, aspect, polarity, holder, time) (Liu, 2012); in “great screen, awful battery” the two aspects have opposite polarities.

Benchmarks include movie reviews (Pang, Lee and Vaithyanathan, 2002) and the Stanford Sentiment Treebank, labelled on every phrase of a parse tree (Socher and coauthors, 2013).

9.2 Linear baselines

Definition 9.2 — Multinomial naive Bayes A document is a bag of words with counts n_w. Naive Bayes assumes the words independent given the class y: P(y \mid x) \propto P(y) \prod_{w} \theta_{y w}^{\, n_w}, with \theta_{yw} estimated by add-one smoothed relative frequencies within class y (Definition 1.7).

Proposition 9.3 — Naive Bayes is a linear classifier For two classes, the log-odds are affine in the counts: \log \frac{P(+ \mid x)}{P(- \mid x)} = \log \frac{P(+)}{P(-)} + \sum_{w} n_w \log \frac{\theta_{+w}}{\theta_{-w}} .

Proof. Take the logarithm of the ratio of the two expressions of Definition 9.2; the normalising constant cancels. ∎

Logistic regression fits the same linear form discriminatively. Both give each word a fixed weight, whatever its neighbours: exactly what negation violates.

Code 9.4 — Negation defeats bags of words

import re
import numpy as np
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.naive_bayes import MultinomialNB
rng = np.random.default_rng(0)
pos, neg = ["good", "great", "excellent", "fun"], ["bad", "boring", "awful", "dull"]
subj, filler = ["the film", "the plot", "the acting", "this movie"], ["really", "quite", "honestly", ""]
docs, y = [], []
for _ in range(2000):                                   # synthetic reviews with negation
    label, negated = rng.integers(2), rng.random() < 0.4
    adj = (pos if label != negated else neg)[rng.integers(4)]
    docs.append(f"{subj[rng.integers(4)]} was {'not ' if negated else ''}{filler[rng.integers(4)]} {adj}")
    y.append(int(label))
y = np.array(y)
y_noisy = np.where(rng.random(len(y)) < 0.05, 1 - y, y)  # 5 % annotation noise

def mark_negation(s):
    """Prefix NOT_ to every word after 'not', as in Pang et al. (2002)."""
    return re.sub(r"\bnot\s+(.*)$", lambda m: " ".join("NOT_" + w for w in m.group(1).split()), s)

tr, te = slice(0, 1500), slice(1500, 2000)
for name, prep, ngram in [("unigrams", None, (1, 1)), ("uni+bigrams", None, (1, 2)),
                          ("NOT_ marking", mark_negation, (1, 1))]:
    vec = CountVectorizer(preprocessor=prep, ngram_range=ngram, token_pattern=r"\S+")
    Xtr, Xte = vec.fit_transform(docs[tr]), vec.transform(docs[te])
    accs = [m.fit(Xtr, y_noisy[tr]).score(Xte, y[te]) for m in (MultinomialNB(), LogisticRegression(max_iter=1000))]
    print(f"{name:13s} naive Bayes {accs[0]:.3f}   logistic regression {accs[1]:.3f}")

With unigrams both classifiers reach 56.4 % on clean test labels: “not” appears in both classes and cannot flip “good”. Bigrams reach only 68–69 %, as a filler word often separates “not” from the adjective. Marking the words after “not” gives 100 %.

9.3 Composition and neural models

Real negation has a scope (“not only good but great”), and contrast and irony change polarity in ways no fixed list captures. Recursive networks over parse trees targeted this composition (Socher and coauthors, 2013); today a fine-tuned pretrained transformer (ch. 7) learns it from data. In dialogue, prosody and the partner’s turns matter too (ch. 14).

Key takeaways — chapter 9

  • Sentiment tasks range from document polarity to aspect-level tuples.
  • Naive Bayes and logistic regression are linear in word counts (Proposition 9.3).
  • Negation and composition break bag-of-words models; marking the scope or using contextual models restores them.
  • Fine-tuned transformers learn composition from data; dialogue adds prosody and context.

10. Speech recognition and speech synthesis

Ch. 3 recognised speech with HMMs, a lexicon and a grammar. Modern recognisers map frames to characters with one network trained end to end, and synthesisers run the other way. This chapter gives the loss that made end-to-end recognition possible, then the synthesis pipeline.

10.1 Measuring recognition

Definition 10.1 — Word error rate (WER) Align hypothesis and reference with a minimal number of word substitutions S, deletions D and insertions I (a Levenshtein distance, by dynamic programming in the tropical semiring). With N reference words, \mathrm{WER} = \frac{S + D + I}{N} .

Insertions can push it above 100 %. The character error rate (CER) is the same on characters.

Code 10.2 — WER with the error types

import numpy as np

def wer(ref, hyp):
    """Word error rate (S + D + I) / N from the Levenshtein alignment of word sequences."""
    r, h = ref.split(), hyp.split()
    D = np.zeros((len(r) + 1, len(h) + 1), dtype=int)
    D[:, 0], D[0, :] = np.arange(len(r) + 1), np.arange(len(h) + 1)
    for i in range(1, len(r) + 1):
        for j in range(1, len(h) + 1):
            D[i, j] = min(D[i - 1, j - 1] + (r[i - 1] != h[j - 1]), D[i - 1, j] + 1, D[i, j - 1] + 1)
    i, j, ops = len(r), len(h), {"S": 0, "D": 0, "I": 0}
    while i > 0 or j > 0:                                 # backtrace to count error types
        if i > 0 and j > 0 and D[i, j] == D[i - 1, j - 1] + (r[i - 1] != h[j - 1]):
            ops["S"] += r[i - 1] != h[j - 1]; i, j = i - 1, j - 1
        elif i > 0 and D[i, j] == D[i - 1, j] + 1:
            ops["D"] += 1; i -= 1
        else:
            ops["I"] += 1; j -= 1
    return round(D[-1, -1] / len(r), 3), ops

print(*wer("i want to recognise speech today", "i want to wreck a nice beach today"))
print(*wer("the cat sat", "the cat sat down on the mat"))   # insertions only: WER above 100 %

“recognise speech” heard as “wreck a nice beach” costs 2 substitutions and 2 insertions on 6 words: WER 0.667. Four insertions on three words give 1.333.

10.2 Connectionist temporal classification

Nobody annotates which frame belongs to which character; CTC (Graves and coauthors, 2006) sums over all alignments.

Definition 10.3 — CTC A network outputs, at each of T frames, a law y_t over the alphabet plus a blank symbol \varnothing. A path \pi chooses one symbol per frame and has probability \prod_t y_t(\pi_t).

The collapse map \mathcal{B} merges repeated symbols, then deletes blanks: for example \mathcal{B}(aa\varnothing ab) = aab. The CTC likelihood of a label sequence l is P(l \mid x) = \sum_{\pi \,:\, \mathcal{B}(\pi) = l} \ \prod_{t=1}^{T} y_t(\pi_t), and the loss is -\log P(l \mid x). Repeated labels need a blank between them.

Theorem 10.4 — CTC forward recursion Insert blanks at the start, at the end and between the labels, to get an extended sequence l' of length S = 2|l| + 1. Let \alpha_t(s) be the total probability of the path prefixes of length t that run through l'_1, \dots, l'_s in order, possibly skipping blanks, and end on l'_s. The prefixes of length 1 give \alpha_1(1) = y_1(\varnothing), \qquad \alpha_1(2) = y_1(l_1), and for t \geq 2 \alpha_t(s) = \Big( \alpha_{t-1}(s) + \alpha_{t-1}(s-1) + \mathbf{1}[l'_s \neq \varnothing,\ l'_s \neq l'_{s-2}]\ \alpha_{t-1}(s-2) \Big)\, y_t(l'_s), with P(l \mid x) = \alpha_T(S) + \alpha_T(S - 1), in O(T |l|) operations.

Proof. A valid path visits the symbols of l' in order, staying on a symbol for one or more frames. To be at position s at time t, it was one frame earlier either already on s, or on the previous position, or two positions back when it skips the blank in between.

The skip is allowed only if l'_s is a label different from l'_{s-2}: skipping the blank between equal labels would merge them under \mathcal{B}. The three cases partition the prefixes, with the common factor y_t(l'_s) (Proposition 0.6). A complete path ends on the last label or the final blank. ∎

Code 10.5 — The recursion against brute force and PyTorch

import itertools
import torch
torch.set_num_threads(1); torch.manual_seed(0)
T, C = 6, 4                                              # 6 frames; symbols: blank=0, a=1, b=2, c=3
logp = torch.randn(T, C).log_softmax(-1)                 # frame-wise log-probabilities
y = [1, 2, 2]                                            # target "a b b" (repeated b needs a blank)

def ctc_forward(logp, y, blank=0):
    """CTC forward algorithm on the extended label sequence  _ y1 _ y2 _ ... _ yU _."""
    z = [blank]
    for s in y:
        z += [s, blank]
    S = len(z)
    alpha = torch.full((S,), float("-inf"))
    alpha[0], alpha[1] = logp[0, z[0]], logp[0, z[1]]
    for t in range(1, len(logp)):
        prev = alpha.clone()
        for s in range(S):
            cands = [prev[s]] + ([prev[s - 1]] if s >= 1 else [])
            if s >= 2 and z[s] != blank and z[s] != z[s - 2]:
                cands.append(prev[s - 2])                # skip a blank between different labels
            alpha[s] = torch.logsumexp(torch.stack(cands), 0) + logp[t, z[s]]
    return -torch.logsumexp(alpha[-2:], 0)               # end on the last label or final blank

def collapse(path, blank=0):
    out = [k for i, k in enumerate(path) if i == 0 or k != path[i - 1]]
    return [k for k in out if k != blank]

brute = -torch.logsumexp(torch.stack([logp[range(T), list(p)].sum() for p in
                         itertools.product(range(C), repeat=T) if collapse(list(p)) == y]), 0)
ref = torch.nn.functional.ctc_loss(logp[:, None], torch.tensor([y]), [T], [len(y)], reduction="sum")
print(f"forward {ctc_forward(logp, y):.4f}   brute force over 4^6 paths {brute:.4f}   torch {ref:.4f}")

The three computations of -\log P(abb \mid x) agree: 4.4706.

CTC assumes frame outputs independent given the input; it is decoded greedily (best symbol per frame, then \mathcal{B}) or with a beam and an LM. Attention encoder–decoders (Chan and coauthors, 2016) and the RNN transducer (ch. 13, 13.3) drop this assumption.

10.3 Self-supervised and weakly supervised speech

wav2vec 2.0 (Baevski and coauthors, 2020) masks latent frames computed from the waveform and learns, contrastively, to identify the quantised latent of each; fine-tuned with CTC, it works with ten minutes of transcribed audio. HuBERT (Hsu and coauthors, 2021) predicts k-means labels of masked frames. Whisper (Radford and coauthors, 2023) trains an encoder–decoder on 680,000 hours of weakly supervised audio.

10.4 Speech synthesis

Text-to-speech runs four stages: text normalisation (“Dr.” to “doctor”), grapheme-to-phoneme conversion, an acoustic model with durations and pitch, and a vocoder producing the waveform. Neural systems replaced concatenation of recorded units and HMM-based synthesis.

Definition 10.6 — Neural synthesis components

  • Acoustic model: Tacotron 2 (Shen and coauthors, 2018) generates a mel spectrogram from characters with an attention encoder–decoder; FastSpeech 2 (Ren and coauthors, 2021) predicts durations, pitch and energy, and all frames in parallel.
  • Vocoder: WaveNet (van den Oord and coauthors, 2016) predicts each sample from the previous ones with dilated causal convolutions, over 256 classes from μ-law companding (\mu = 255): F(x) = \mathrm{sign}(x)\, \frac{\ln(1 + \mu |x|)}{\ln(1 + \mu)}, \qquad x \in [-1, 1] . HiFi-GAN (Kong, Kim and Bae, 2020), trained adversarially, generates all samples in parallel, much faster than real time.

Example 10.7 — Why μ-law With \mu = 255, a quiet sample x = 0.01 maps to F(0.01) = \frac{\ln 3.55}{\ln 256} \approx 0.23 .

Cut [-1, 1] into 256 levels of width 2/255. Amplitudes in [0, 0.01] then get about 29 levels after companding, against 1.3 without. The loud range [0.5, 1] gets 16 levels instead of 64: the levels go where speech samples mostly are, near zero.

Listeners judge synthesis on a 1-to-5 scale, the mean opinion score (MOS), since no automatic metric captures naturalness well (ch. 12). Spoken language models without text are in ch. 14.

Key takeaways — chapter 10

  • WER is a normalised Levenshtein distance; insertions can push it above 100 %.
  • CTC sums over all monotonic alignments with blanks, by a forward recursion on the extended label sequence (Theorem 10.4).
  • Self-supervision (wav2vec 2.0, HuBERT) and weak supervision (Whisper) cut the need for transcripts.
  • Synthesis runs text normalisation, phonemes, an acoustic model and a vocoder; it is judged by MOS.

11. Chatbots, code generation and agents

A chatbot keeps a conversation going, a code generator writes programs that run, an agent acts over many steps. This chapter goes from pattern matching to LLM-based systems.

11.1 From ELIZA to task-oriented dialogue

ELIZA (Weizenbaum, 1966) matched keywords and rewrote the user’s sentence (“I feel sad” gives “Why do you feel sad?”) with no model of meaning; users still confided in it. Task-oriented systems, which book a train or a table, are modular.

Definition 11.1 — Task-oriented dialogue pipeline

  • Understanding: classify the intent and fill the slots, by sequence labelling (ch. 4): “a table for two at eight” gives people=2, time=20:00.
  • State tracking: update a belief over slot values from the history.
  • Policy: choose the next act (ask, confirm, query a database), possibly by reinforcement learning in a partially observable Markov decision process (Young and coauthors, 2013).
  • Generation: turn the act into a sentence, then speech (ch. 10).

Code 11.2 — A retrieval-based chatbot

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

faq = {"How do I reset my password?": "Use the 'forgot password' link on the login page.",
       "When is the library open?": "From 9 am to 8 pm, Monday to Saturday.",
       "How can I book a meeting room?": "Rooms are booked on the intranet, up to two weeks ahead.",
       "Where do I submit the project report?": "Upload the PDF on the course page before the deadline."}
questions = list(faq)
vec = TfidfVectorizer(stop_words="english").fit(questions)

def respond(user_turn, threshold=0.2):
    """Retrieval-based chatbot: answer of the most similar stored question, or a fallback."""
    sims = cosine_similarity(vec.transform([user_turn]), vec.transform(questions))[0]
    best = sims.argmax()
    score = round(float(sims[best]), 2)
    return (faq[questions[best]] if score >= threshold else "Sorry, could you rephrase?"), score

for turn in ["I forgot my password", "is the library open on sunday", "what is the meaning of life"]:
    print(turn, "->", respond(turn))

The password question is matched (similarity 0.71) and the off-topic one falls back. The library question matches with similarity 1.0, yet the answer does not say whether it opens on Sunday: retrieval returns canned text and cannot reason.

11.2 LLM-based chatbots and retrieval

An LLM assistant (ch. 7) conditions on the whole conversation, marked with roles (system, user, assistant). It answers any request fluently, but can state false facts with confidence: hallucination.

Definition 11.3 — Retrieval-augmented generation (RAG) A retriever scores documents z of a collection against the query x and keeps the top K; the generator conditions on them: P(y \mid x) \approx \sum_{z \in \mathrm{top}\text{-}K(x)} P(z \mid x)\, P(y \mid x, z) .

Lewis and coauthors (2020) trained both jointly; often the passages are simply pasted into the prompt.

11.3 Code generation

Code can be executed, so it is judged by passing unit tests. HumanEval (Chen and coauthors, 2021) has 164 hand-written Python problems with tests.

Definition 11.4 — pass@k For a problem, pass@k is the probability that at least one of k independent samples from the model passes all the tests. If one sample passes with probability p, then \text{pass@}k = 1 - (1 - p)^k.

Proposition 11.5 — An unbiased estimator of pass@k Draw n \geq k samples, of which c pass. Then \widehat{\text{pass@}k} = 1 - \binom{n - c}{k} \Big/ \binom{n}{k} is an unbiased estimator of pass@k (Chen and coauthors, 2021).

Proof. Given the n samples, the ratio of binomials is the probability that a uniformly random k-subset of them has no passing sample; the estimator is the probability that it has one.

A uniformly random k-subset of n i.i.d. samples is itself k i.i.d. samples, so the average over the samples is exactly 1 - (1 - p)^k. ∎

Code 11.6 — Unbiased versus plug-in estimates

import numpy as np
from math import comb
rng = np.random.default_rng(0)

def pass_at_k(n, c, k):
    """Unbiased estimator of pass@k from n samples with c correct (Chen et al., 2021)."""
    return 1.0 if n - c < k else 1.0 - comb(n - c, k) / comb(n, k)

p_true, n, k = 0.1, 20, 5                                # each sample passes the tests w.p. 0.1
target = 1 - (1 - p_true) ** k                           # true pass@5
c = rng.binomial(n, p_true, size=20000)                  # 20,000 problems, n samples each
unbiased = np.mean([pass_at_k(n, ci, k) for ci in c])
plug_in = np.mean(1 - (1 - c / n) ** k)                  # naive plug-in estimator
print(f"true pass@5 {target:.4f}   unbiased {unbiased:.4f}   plug-in {plug_in:.4f}")

The true pass@5 is 0.4095; the unbiased estimator averages 0.4113, the plug-in 0.3796, biased downwards because p \mapsto 1 - (1-p)^k is concave.

11.4 Agents

Definition 11.7 — Agent loop An LLM agent repeats: read the context (task, history, last observation); generate a reasoning step and an action, a tool call (search, code execution, file edit); append the tool’s output. It stops with a final answer. ReAct (Yao and coauthors, 2023) interleaves reasoning and actions this way.

Proposition 11.8 — Errors compound If each of n steps succeeds independently with probability q and the task needs all of them, the task succeeds with probability q^n. With q = 0.95 and n = 20 steps, q^n = 0.95^{20} \approx 0.36 .

Hence agents check their work (run tests, read errors, retry), and benchmarks such as SWE-bench (Jimenez and coauthors, 2024), built from real GitHub issues, score whole tasks. Large Language Models for Code and Proof goes further.

Key takeaways — chapter 11

  • Task-oriented dialogue chains understanding (slot filling), state tracking, a policy and generation.
  • Retrieval bots are rigid, LLM chatbots hallucinate, RAG grounds them in documents.
  • Code is judged by tests; pass@k has an unbiased estimator (Proposition 11.5).
  • Agents loop between reasoning, tool calls and observations; per-step errors compound geometrically.

12. Evaluation: comparing human and machine performance

Every chapter ended with a metric. This one asks when a difference between two systems, or between a system and humans, is real: it measures human agreement, tests significance, and lists how scores mislead.

12.1 Human agreement as a reference

Definition 12.1 — Cohen’s kappa Two annotators label the same n items. Let p_o be their observed agreement and p_e = \sum_k p_k^{(1)} p_k^{(2)} the agreement expected by chance from their label frequencies. Then \kappa = \frac{p_o - p_e}{1 - p_e} .

\kappa = 1 for perfect agreement and \kappa = 0 for chance-level agreement (Cohen, 1960).

Proposition 12.2 — Scoring against a noisy reference Let the reference label be wrong with probability \varepsilon, and the system be right with probability a, independently, among K classes. Suppose both pick a wrong label uniformly among the K - 1 wrong ones. Then the measured accuracy is \tilde a = a (1 - \varepsilon) + (1 - a)\, \frac{\varepsilon}{K - 1} .

Proof. The system agrees with the reference when both are right, with probability a(1 - \varepsilon), or when both are wrong with the same label: probability (1 - a)\,\varepsilon times 1 / (K - 1). ∎

A perfect system scores only 1 - \varepsilon. Matching the agreement between two humans is the natural definition of “human parity”, but it hides which errors each makes.

Example 12.3 — Two claims of human parity Xiong and coauthors (2016) reported a WER of 5.9 % on Switchboard conversational speech, equal to that of professional transcribers; Saon and coauthors (2017) then measured careful human transcription at 5.1 %. Hassan and coauthors (2018) claimed parity for Chinese–English news translation; Läubli, Sennrich and Volk (2018) found that translators reading whole documents still preferred human output. The verdict depends on the humans and the protocol.

12.2 Is the difference significant?

Definition 12.4 — Paired bootstrap (Koehn, 2004) Score systems A and B on the same n items. Resample n items with replacement many times and recompute the difference each time. The 2.5 % and 97.5 % quantiles give a 95 % interval; the share of resamples where B is not worse says how often A’s advantage vanishes.

Code 12.5 — Agreement, human baseline and bootstrap

import numpy as np
from sklearn.metrics import cohen_kappa_score
rng = np.random.default_rng(0)

n = 500                                                  # 500 items, 3 sentiment classes
gold = rng.choice(3, size=n, p=[0.5, 0.3, 0.2])
def annotate(acc):                                       # keep the label w.p. acc, else random
    return np.where(rng.random(n) < acc, gold, rng.choice(3, size=n))
ann1, ann2, sys_a, sys_b = annotate(0.85), annotate(0.85), annotate(0.80), annotate(0.76)

p_o = np.mean(ann1 == ann2)                              # observed agreement
p_e = sum(np.mean(ann1 == k) * np.mean(ann2 == k) for k in range(3))   # chance agreement
print(f"annotators: agreement {p_o:.3f}, kappa {(p_o - p_e) / (1 - p_e):.3f} "
      f"(sklearn {cohen_kappa_score(ann1, ann2):.3f})")

ref = ann1                                               # evaluate systems against one annotator
acc_a, acc_b = np.mean(sys_a == ref), np.mean(sys_b == ref)
delta = acc_a - acc_b
idx = rng.integers(0, n, size=(10000, n))                # paired bootstrap (Koehn, 2004)
deltas = (sys_a[idx] == ref[idx]).mean(1) - (sys_b[idx] == ref[idx]).mean(1)
print(f"accuracy vs annotator 1: A {acc_a:.3f}, B {acc_b:.3f}, annotator 2 {np.mean(ann2 == ref):.3f}")
print(f"delta {delta:.3f}, 95% bootstrap interval [{np.quantile(deltas, 0.025):.3f}, {np.quantile(deltas, 0.975):.3f}], "
      f"share of resamples with delta <= 0: {np.mean(deltas <= 0):.3f}")

The annotators agree on 82.0 % of items, \kappa = 0.724. Against annotator 1, system A scores 80.2 %, near annotator 2’s 82.0 %, and B 77.0 %. A’s advantage of 3.2 points has the 95 % interval [-1.2, 7.6]: on 500 items it is not established.

12.3 How benchmark scores mislead

Key takeaways — chapter 12

  • Kappa measures human agreement; reference noise caps the measurable accuracy at 1 - \varepsilon (Proposition 12.2).
  • “Human parity” claims depend on which humans, which data and which protocol.
  • Report confidence intervals: a paired bootstrap on the same items is simple and standard.
  • Scores can reflect artefacts, contamination or judge biases rather than ability.

13. Complements for self-study

This chapter completes four topics of the course with material that does not fit the 21 hours: a better smoothing method, the derivation of Baum–Welch, the RNN transducer, and the closed form behind DPO. Each section can be read after the chapter it extends.

13.1 Kneser–Ney smoothing (extends ch. 1)

Add-k smoothing gives the same share to every unseen word. Kneser and Ney (1995) observed that the backup distribution should favour words that appear after many different contexts: “Francisco” is frequent, but almost always after “San”, so it is a poor guess after a new context.

Definition 13.1 — Interpolated Kneser–Ney for bigrams With a discount 0 < d < 1, let N_{1+}(u\,\bullet) be the number of distinct words seen after u, N_{1+}(\bullet\,w) the number of distinct words seen before w, and N_{1+}(\bullet\,\bullet) the number of distinct bigrams. Then P_{\mathrm{KN}}(w \mid u) = \frac{\max(c(u, w) - d,\ 0)}{c(u)} + \lambda(u)\, P_{\mathrm{cont}}(w), \lambda(u) = \frac{d\, N_{1+}(u\,\bullet)}{c(u)}, \qquad P_{\mathrm{cont}}(w) = \frac{N_{1+}(\bullet\,w)}{N_{1+}(\bullet\,\bullet)} .

Proposition 13.2 — Kneser–Ney is normalised For every context u with c(u) > 0, \sum_{w} P_{\mathrm{KN}}(w \mid u) = 1 .

Proof. Each of the N_{1+}(u\,\bullet) seen bigrams loses exactly d, because counts are integers and d < 1. The first terms therefore sum to \frac{c(u) - d\, N_{1+}(u\,\bullet)}{c(u)} = 1 - \lambda(u) .

The continuation probabilities sum to one, since every bigram type has exactly one second word. Adding \lambda(u) times 1 gives 1. ∎

Code 13.3 — Kneser–Ney on the corpus of Code 1.8

import math
from collections import Counter

train = ["<s> the cat sat on the mat </s>", "<s> the dog sat on the log </s>",
         "<s> the cat saw the dog </s>"]
test = ["<s> the dog sat on the mat </s>", "<s> the dog saw the cat </s>"]
tok = [s.split() for s in train]
big = Counter((u, w) for s in tok for u, w in zip(s, s[1:]))
ctx = Counter(u for s in tok for u in s[:-1])
followers = Counter(u for (u, w) in big)                 # N1+(u .): distinct words after u
preceders = Counter(w for (u, w) in big)                 # N1+(. w): distinct words before w
n_types = len(big)                                       # N1+(. .): distinct bigram types

def p_kn(w, u, d=0.75):
    """Interpolated Kneser-Ney bigram probability."""
    p_cont = preceders[w] / n_types                      # continuation probability
    lam = d * followers[u] / ctx[u]                      # mass freed by discounting
    return max(big[u, w] - d, 0) / ctx[u] + lam * p_cont

V = sorted({w for s in tok for w in s} - {"<s>"})
print("sums to one:", all(abs(sum(p_kn(w, u) for w in V) - 1) < 1e-12 for u in ctx))
for d in [0.5, 0.75, 0.9]:
    lp = [math.log(p_kn(v, u, d)) for s in test for u, v in zip(s.split(), s.split()[1:])]
    print(f"Kneser-Ney d = {d}: perplexity {math.exp(-sum(lp) / len(lp)):.2f}")

The distributions sum to one, and the perplexity is 3.07 for d = 0.5, below the best add-k value of Code 1.8 (3.25). Chen and Goodman (1999) found modified Kneser–Ney, with three discounts, the best n-gram smoothing on large corpora.

13.2 Baum–Welch as expectation–maximisation (extends ch. 3)

Write \theta = (\pi, A, B) for the parameters and q for the hidden path. EM maximises at each step the expected complete log-likelihood Q(\theta', \theta) = \sum_{q} P(q \mid O; \theta) \log P(q, O; \theta') .

Theorem 13.4 — EM never decreases the likelihood For all parameters \theta, \theta', \log P(O; \theta') - \log P(O; \theta) \geq Q(\theta', \theta) - Q(\theta, \theta) .

So choosing \theta' that maximises Q(\cdot, \theta) gives P(O; \theta') \geq P(O; \theta).

Proof. Write w_q = P(q \mid O; \theta), a law on paths. By Jensen’s inequality for the concave logarithm, \log \frac{P(O; \theta')}{P(O; \theta)} = \log \sum_q w_q \frac{P(q, O; \theta')}{P(q, O; \theta)} \geq \sum_q w_q \log \frac{P(q, O; \theta')}{P(q, O; \theta)} .

The right-hand side is Q(\theta', \theta) - Q(\theta, \theta). Since \theta' = \theta is a candidate, the maximiser makes it non-negative. ∎

Proposition 13.5 — The M-step of Baum–Welch The maximiser of Q(\cdot, \theta) is given by the updates of Theorem 3.8.

Proof. The logarithm of the joint law of Definition 3.3 is a sum of terms \log \pi_{q_1}, \log A_{q_{t-1} q_t} and \log b_{q_t}(o_t). Its expectation under the posterior is Q = \sum_i \gamma_1(i) \log \pi'_i + \sum_{t < T} \sum_{i, j} \xi_t(i, j) \log A'_{ij} + \sum_t \sum_i \gamma_t(i) \log b'_i(o_t) .

The three groups of parameters separate, and each row of the transition and emission matrices is a separate law. For a law p and non-negative weights n_k, Gibbs’ inequality (or a Lagrange multiplier) gives the maximiser of a weighted log-likelihood: \arg\max_p \sum_k n_k \log p_k = \Big( \frac{n_k}{\sum_j n_j} \Big)_k .

Apply it three times:

  • to the initial law, with the weights \gamma_1(i);
  • to row i of the transition matrix, with the weights \xi_t(i, j) summed over t;
  • to row i of the emission matrix, with the weights \gamma_t(i) summed over the times where o_t = v. ∎

Code 13.6 — Baum–Welch on 500 sampled observations

import numpy as np
rng = np.random.default_rng(0)
pi = np.array([0.8, 0.2]); A = np.array([[0.6, 0.4], [0.5, 0.5]])
B = np.array([[0.2, 0.4, 0.4], [0.5, 0.4, 0.1]])
q, obs = rng.choice(2, p=pi), []
for t in range(500):                                   # sample 500 observations
    obs.append(rng.choice(3, p=B[q])); q = rng.choice(2, p=A[q])
obs = np.array(obs)

def forward_backward(pi, A, B, obs):
    T, K = len(obs), len(pi)
    alpha, beta, c = np.zeros((T, K)), np.ones((T, K)), np.zeros(T)
    alpha[0] = pi * B[:, obs[0]]; c[0] = alpha[0].sum(); alpha[0] /= c[0]
    for t in range(1, T):                              # scaled forward pass
        alpha[t] = (alpha[t - 1] @ A) * B[:, obs[t]]; c[t] = alpha[t].sum(); alpha[t] /= c[t]
    for t in range(T - 2, -1, -1):                     # scaled backward pass
        beta[t] = A @ (B[:, obs[t + 1]] * beta[t + 1]) / c[t + 1]
    gamma = alpha * beta                               # P(q_t = i | O)
    xi = alpha[:-1, :, None] * A[None] * (B[:, obs[1:]].T * beta[1:])[:, None, :] / c[1:, None, None]
    return gamma, xi, np.log(c).sum()                  # log P(O) = sum_t log c_t

pi_h, A_h = np.array([0.5, 0.5]), np.array([[0.7, 0.3], [0.3, 0.7]])
B_h = np.array([[0.3, 0.3, 0.4], [0.4, 0.3, 0.3]])
for it in range(30):                                   # Baum-Welch = EM
    gamma, xi, ll = forward_backward(pi_h, A_h, B_h, obs)
    if it % 5 == 0:
        print(f"iteration {it:2d}  log P(O) = {ll:.3f}")
    pi_h = gamma[0]
    A_h = xi.sum(0) / xi.sum((0, 2))[:, None]
    B_h = np.stack([gamma[obs == o].sum(0) for o in range(3)], 1) / gamma.sum(0)[:, None]
print("true model: log P(O) =", round(forward_backward(pi, A, B, obs)[2], 3))
print("estimated B:\n", B_h.round(2))

The log-likelihood rises from −553.8 to −541.1 in 25 iterations, above the −542.9 of the true model: maximum likelihood fits the sample. The estimated emission rows, (0.26, 0.36, 0.38) and (0.47, 0.39, 0.14), are close to the true ones. Scaling each \alpha_t by its sum c_t avoids underflow on long sequences.

13.3 The RNN transducer (extends ch. 10)

CTC predicts each frame independently of the labels already emitted. The RNN transducer (Graves, 2012) adds a prediction network over the previous labels, which acts as an internal language model.

Definition 13.7 — RNN transducer An encoder gives frame vectors f_t, and a prediction network gives a vector g_u for each prefix of u labels. A joint network outputs, at each lattice node (t, u), a law P(k \mid t, u) over the labels plus the blank.

From node (t, u), a blank moves to the next frame and the label y_{u+1} moves to the next output: (t, u) \xrightarrow{\ \varnothing\ } (t + 1, u), \qquad (t, u) \xrightarrow{\ y_{u+1}\ } (t, u + 1) .

The probability of y sums over all such paths from (1, 0) that end with a blank at (T, U).

Proposition 13.8 — Transducer forward recursion With \alpha(1, 0) = 1, \alpha(t, u) = \alpha(t - 1, u)\, P(\varnothing \mid t - 1, u) + \alpha(t, u - 1)\, P(y_u \mid t, u - 1), and P(y \mid x) = \alpha(T, U)\, P(\varnothing \mid T, U), in O(TU) operations.

Proof. Every path reaching (t, u) arrives either by a blank from (t - 1, u) or by the label y_u from (t, u - 1); the two sets are disjoint. Proposition 0.6 on the lattice graph gives the recursion, and the final blank closes the path. ∎

Code 13.9 — The transducer recursion against enumeration

import itertools
import numpy as np
rng = np.random.default_rng(0)
T, y, K = 4, [1, 2], 3                                   # 4 frames, target "a b", symbols: blank=0, a=1, b=2
U = len(y)
logits = rng.normal(size=(T, U + 1, K))                  # joint network output at lattice node (t, u)
logp = logits - np.log(np.exp(logits).sum(-1, keepdims=True))

alpha = np.full((T, U + 1), -np.inf); alpha[0, 0] = 0.0
for t in range(T):
    for u in range(U + 1):
        terms = []
        if t > 0: terms.append(alpha[t - 1, u] + logp[t - 1, u, 0])          # blank: next frame
        if u > 0: terms.append(alpha[t, u - 1] + logp[t, u - 1, y[u - 1]])   # label: next output
        if terms: alpha[t, u] = np.logaddexp.reduce(terms)
forward = alpha[T - 1, U] + logp[T - 1, U, 0]            # final blank

paths = []                                               # brute force: place U labels among T-1+U steps
for pos in itertools.combinations(range(T - 1 + U), U):
    t, u, lp = 0, 0, 0.0
    for step in range(T - 1 + U):
        if step in pos: lp += logp[t, u, y[u]]; u += 1
        else: lp += logp[t, u, 0]; t += 1
    paths.append(lp + logp[t, u, 0])
print(f"{len(paths)} paths; log P(y|x): forward {forward:.4f}, brute force {np.logaddexp.reduce(paths):.4f}")

There are \binom{5}{2} = 10 paths, and both computations give \log P(y \mid x) = -4.3427.

13.4 The KL-regularised optimum and DPO (extends ch. 7)

RLHF trains a reward model r on human comparisons with the Bradley–Terry likelihood P(y_w \succ y_l \mid x) = \sigma\big(r(x, y_w) - r(x, y_l)\big), then optimises the policy against it.

Definition 13.10 — KL-regularised preference optimisation For each prompt x, the policy \pi maximises J(\pi) = \mathbb{E}_{y \sim \pi(\cdot \mid x)} \big[ r(x, y) \big] - \beta\, \mathrm{KL}\big(\pi(\cdot \mid x) \,\|\, \pi_{\mathrm{ref}}(\cdot \mid x)\big), where \pi_{\mathrm{ref}} is the instruction-tuned model and \beta > 0 sets how far the policy may move (Ouyang and coauthors, 2022).

Proposition 13.11 — Closed-form optimum The maximiser of J is \pi^*(y \mid x) = \frac{1}{Z(x)}\, \pi_{\mathrm{ref}}(y \mid x)\, e^{r(x, y) / \beta}, \qquad Z(x) = \sum_y \pi_{\mathrm{ref}}(y \mid x)\, e^{r(x, y) / \beta} .

Proof. Expand the KL divergence and group the terms: J(\pi) = -\beta \sum_y \pi(y \mid x) \log \frac{\pi(y \mid x)}{\pi_{\mathrm{ref}}(y \mid x)\, e^{r(x, y)/\beta}} = -\beta\, \mathrm{KL}(\pi \,\|\, \pi^*) + \beta \log Z(x) .

The last term does not depend on \pi, and the KL divergence is minimal, equal to zero, at \pi = \pi^* (Proposition 0.2). ∎

Inverting the optimum gives the reward as a function of the policy: r(x, y) = \beta \log \frac{\pi^*(y \mid x)}{\pi_{\mathrm{ref}}(y \mid x)} + \beta \log Z(x) .

In the Bradley–Terry likelihood only the difference of two rewards for the same prompt appears, so Z(x) cancels. Fitting the policy directly to the preference pairs gives the DPO loss (Rafailov and coauthors, 2023):

Key result — the DPO loss \mathcal{L}_{\mathrm{DPO}} = -\log \sigma\Big( \beta \log \frac{\pi_\theta(y_w \mid x)}{\pi_{\mathrm{ref}}(y_w \mid x)} - \beta \log \frac{\pi_\theta(y_l \mid x)}{\pi_{\mathrm{ref}}(y_l \mid x)} \Big)

No reward model and no sampling are needed: DPO is a logistic regression on pairs, whose “features” are log-probability ratios.

Key takeaways — chapter 13

  • Kneser–Ney backs off to a continuation probability: how many contexts a word completes, not how frequent it is.
  • EM ascends a lower bound that touches the likelihood; for HMMs the M-step is fractional-count estimation.
  • The RNN transducer replaces the CTC independence assumption by a lattice over frames and emitted labels.
  • The KL-regularised optimum is a reweighted reference policy; inverting it turns preference learning into the DPO logistic loss.

14. Research directions and open problems

This chapter is for self-study. It points to active lines of research that extend the course, each with a few papers to start from and the questions that remain open.

14.1 Spoken language models without text

Generative spoken language modelling (Lakhotia and coauthors, 2021) chains the pieces of ch. 10 without any text: self-supervised speech features (HuBERT or wav2vec 2.0), k-means discrete units, a transformer language model over the units (ch. 7), and a unit-to-speech synthesiser. The model continues a spoken prompt in speech.

Open problems: discrete units capture phonetic content well but prosody and meaning poorly; spoken models still lag far behind text models of the same size in coherence; and data efficiency is nowhere near that of children.

14.2 Long contexts and sub-quadratic architectures

Attention costs O(T^2) in the sequence length (ch. 6). Structured state-space models (Gu, Goel and Ré, 2022) and their selective variant Mamba (Gu and Dao, 2023) process sequences in linear time with a recurrent state. Hyena (Poli and coauthors, 2023) replaces attention by long implicit convolutions interleaved with gating.

Open problems: Jelassi and coauthors (2024) prove and observe that fixed-size recurrent states are worse than attention at copying long inputs, and hybrid models that mix attention and recurrent layers are an active compromise. Which tasks need full attention, and how much, is not settled.

14.3 Multilingual and document-level translation

Most of the world’s languages have little parallel text. Massively multilingual models such as NLLB (NLLB Team, 2022), covering 200 languages, and open multilingual LLMs such as BLOOM (BigScience Workshop, 2022) share parameters across languages so that low-resource ones benefit from high-resource ones.

Sentence-level systems ignore context. Bawden and coauthors (2018) built contrastive test sets for coreference and lexical cohesion across sentences: a model that sees only the current sentence scores at chance on them, and only some ways of feeding it the previous sentence help. Freitag and coauthors (2021) showed with expert MQM annotations that crowd ratings and BLEU can rank systems differently from professional translators.

Open problems: reliable evaluation for languages without references or expert raters; translation of whole documents with consistent terminology; measuring and reducing gender and cultural bias in outputs.

14.4 Affect and interaction

In conversation, opinions are spread over turns, carried by prosody as much as by words, and addressed to a partner. Clavel and Callejas (2016) review how sentiment analysis moved from opinion mining in product reviews to human–agent interaction, where a virtual agent must detect the user’s attitude in real time and respond to it.

Open problems: labels of social and emotional phenomena have low inter-annotator agreement (ch. 12), so the ceiling of supervised learning is low; multimodal models must align words, prosody and gestures in time; and an agent’s reaction changes the user’s next turn, which static datasets cannot capture.

14.5 Evaluation that keeps pace

As benchmarks saturate and leak into pretraining data, the field is moving to dynamic benchmarks (Kiela and coauthors, 2021), human preference arenas and LLM judges (Zheng and coauthors, 2023), and task-level evaluations of agents such as SWE-bench (Jimenez and coauthors, 2024).

Open problems: detecting contamination without access to the training data; calibrated LLM judges whose biases are measured and corrected; and comparisons with humans that specify which humans, with what training and time, as ch. 12 requires.

Key takeaways — chapter 14

  • Spoken language models learn from audio alone; their gap with text models and with infants is open.
  • Sub-quadratic architectures trade attention’s exact recall for linear cost; hybrids are an active compromise.
  • Translation research targets low-resource languages, document context and expert evaluation.
  • Sentiment in interaction, contamination-free evaluation and agent reliability remain open measurement problems.

Exercises

The exercises are sorted by level: ★ applies the course, ★★ combines results or proves a short statement, ★★★ is open-ended or research-flavoured. Exercises 13, 18 and 19 are computational.

  1. ★ Perplexity by hand. With the training corpus of Code 1.8 and add-one smoothing (|V| = 9), compute the six bigram probabilities of “<s> the dog saw the cat </s>” and the perplexity of the sentence.

    Solution: each probability is (c(u, w) + 1)/(c(u) + 9). In order, the six bigrams have probabilities \frac{4}{12}, \quad \frac{3}{15}, \quad \frac{1}{11}, \quad \frac{2}{10}, \quad \frac{3}{15}, \quad \frac{1}{11}, where “dog saw” and “cat </s>” are unseen.

    The log-probability is -10.72 over 6 predictions, so \mathrm{PP} = e^{10.72 / 6} \approx 5.97 .

  2. ★ Frames, bins and mels. Telephone speech is sampled at 8 kHz. With 25 ms windows, a 10 ms hop and a DFT of size 256: how many samples per window, how many frames in 2 s, what is the bin spacing, and what is the mel value of the Nyquist frequency?

    Solution: 200 samples per window and 80 per hop; 1 + \lfloor (16000 - 200)/80 \rfloor = 198 frames; bins every 8000/256 = 31.25 Hz; the Nyquist frequency is 4000 Hz and \mathrm{mel}(4000) = 2595 \log_{10}(1 + 4000/700) \approx 2146 .

  3. ★ Viterbi by hand. With the HMM of Code 3.6, observe 1, 3, 1 ice creams. Run the forward and Viterbi recursions and give P(O) and the best path.

    Solution: \alpha_1 = \delta_1 = (0.16, 0.10); \alpha_2 = (0.0584, 0.0114), \alpha_3 = (0.008148, 0.01453), so P(O) = 0.022678.

    For Viterbi, \delta_2 = (0.0384, 0.0064) and \delta_3 = (0.004608, 0.00768). The best path is hot, hot, cold, with probability 0.00768, confirmed by enumerating the 8 paths.

  4. ★ BIO tags and span F1. In “The University of Paris-Saclay hired Ada Lovelace in Orsay .” the gold entities are an organisation (“University of Paris-Saclay”), a person and a location. A tagger outputs O B-ORG I-ORG O O B-PER I-PER O B-LOC O. Write the gold tags, and compute token accuracy and span F1.

    Solution: gold O B-ORG I-ORG I-ORG O B-PER I-PER O B-LOC O. One token differs, so the accuracy is 90 %. The predicted organisation “University of” has the wrong boundary: 2 correct spans out of 3 predicted and 3 gold, so P = R = F_1 = 2/3.

  5. ★ BLEU by hand. Compute BLEU with N = 2 for the hypothesis “the cat the cat on the mat” against the reference “the cat is on the mat”.

    Solution: clipped unigram matches: “the” 2 (not 3), “cat” 1 (not 2), “on” 1, “mat” 1, so p_1 = 5/7. Bigram matches: “the cat” once, “on the”, “the mat”, so p_2 = 3/6. The hypothesis is longer than the reference, so BP = 1 and \mathrm{BLEU}_2 = \sqrt{5/7 \cdot 1/2} \approx 0.598 .

  6. ★ CTC paths and WER. (a) With the blank written “-”, list the paths of length 3 that CTC collapses to “ab”, then those that collapse to “aa”. (b) Compute the WER of “a meeting starts noon today” against “the meeting starts at noon”.

    Solution: (a) -ab, a-b, ab-, aab, abb: five paths. For “aa” only a-a works, since the repeated label needs a blank.

    1. An optimal alignment has one substitution (the → a), one deletion (at) and one insertion (today), on 5 reference words: WER = 3/5 = 60 %.
  7. ★ Cohen’s kappa. Two annotators label 100 reviews. Both say positive 40 times, both negative 30 times; A says positive and B negative 20 times, the reverse 10 times. Compute p_o, p_e and \kappa.

    Solution: p_o = 0.70. A says positive 60 % of the time and B 50 %, so p_e = 0.6 \cdot 0.5 + 0.4 \cdot 0.5 = 0.50, and \kappa = \frac{0.70 - 0.50}{1 - 0.50} = 0.40 .

  8. ★★ Forward–backward identities. For an HMM, prove that \sum_i \alpha_t(i)\, \beta_t(i) = P(O) for every t, and deduce the formula of \gamma_t(i) in ch. 3. Then show that the Viterbi recursion is the forward recursion in the max-product semiring.

    Solution: by the Markov property, given q_t = i the future observations are independent of the past ones and of o_t. Hence \alpha_t(i)\, \beta_t(i) = P(o_{1:t}, q_t = i)\, P(o_{t+1:T} \mid q_t = i) = P(O, q_t = i) .

    Summing over i gives P(O), and dividing by P(O) gives \gamma_t(i) = P(q_t = i \mid O). Replacing sums by maxima in the proof of Theorem 3.4 is legitimate because \max distributes over products of non-negative numbers (Definition 0.5).

  9. ★★ A CRF without transitions. Show that a linear-chain CRF whose features depend only on (y_t, x, t), not on y_{t-1}, factorises into independent softmax classifiers, one per token. What does Theorem 4.4 become?

    Solution: the exponent is a sum of terms \theta^\top f(y_t, x, t), so the numerator is a product over t and Z(x), a sum over all sequences, is the product of the per-token normalisers (distributivity). Hence p(y \mid x) = \prod_t \mathrm{softmax}_{y_t}. The gradient becomes, token by token, observed minus expected features: the gradient of multinomial logistic regression (Proposition 0.4). Transitions are what make the CRF more than a per-token classifier.

  10. ★★ SGNS at the optimum. A pair is seen c(w, u) = 30 times, with c(w) = 300, c(u) = 1000, D = 10^6 and k = 5 negatives. (a) What inner product does Theorem 5.5 predict? (b) What happens to pairs never seen together, and how does PPMI handle the same issue?

    Solution: (a) The PMI is \log \frac{30 \cdot 10^6}{300 \cdot 1000} = \log 100 \approx 4.61, so the optimal inner product is 4.61 - \log 5 \approx 3.00.

    1. With c(w, u) = 0 the objective k\, c(w) c(u) D^{-1} \log \sigma(-s) increases as s \to -\infty: the unconstrained optimum is -\infty, like \mathrm{PMI} = \log 0. Low dimension keeps it finite in SGNS; PPMI truncates negative values at 0.
  11. ★★ Gradients through time. (a) For the scalar RNN h_t = \tanh(w h_{t-1} + x_t) with w = 0.9, bound |\partial h_{50} / \partial h_0|. (b) Compare with an LSTM whose forget gate stays at f_t = 0.99, along the cell path. (c) Why does the transformer not have this problem across positions?

    Solution: (a) Proposition 6.2 gives at most 0.9^{50} \approx 0.005. (b) Along the cell, the factor is 0.99^{50} \approx 0.61: the signal survives. (c) Any two positions are connected by one attention step, so the path length between them is 1, not their distance; the price is the O(T^2) cost.

  12. ★★ pass@k. (a) With n = 10 samples of which c = 2 pass, compute the estimate of pass@5. (b) Show that for k = 1 the estimator is c / n. (c) Prove that the plug-in estimator 1 - (1 - c/n)^k is biased downwards.

    Solution: (a) 1 - \binom{8}{5} / \binom{10}{5} = 1 - 56/252 \approx 0.778. (b) \binom{n-c}{1} / \binom{n}{1} = (n - c)/n. (c) g(p) = 1 - (1 - p)^k is concave, and c/n is unbiased for p, so by Jensen \mathbb{E}\big[g(c/n)\big] \leq g\big(\mathbb{E}[c/n]\big) = g(p) .

  13. ★★ CTC in log space, vectorised (Python). Rewrite the CTC recursion of Code 10.5 without the inner loop over s, check it against torch.nn.functional.ctc_loss on 50 frames and a label of length 8, then decode greedily.

    Solution: shift the vector \alpha by one and by two positions, mask the forbidden skips once, and combine the three with a log-sum-exp.

    import torch
    torch.set_num_threads(1); torch.manual_seed(0)
    
    def ctc_nll(logp, y, blank=0):
        """CTC negative log-likelihood, vectorised over the extended label sequence."""
        z = torch.full((2 * len(y) + 1,), blank, dtype=torch.long)
        z[1::2] = torch.tensor(y)
        S = len(z)
        skip = torch.zeros(S, dtype=torch.bool)
        skip[2:] = (z[2:] != blank) & (z[2:] != z[:-2])        # allowed jumps s-2 -> s
        alpha = torch.full((S,), float("-inf"))
        alpha[:2] = logp[0, z[:2]]
        for t in range(1, logp.shape[0]):
            a1 = torch.cat([torch.tensor([float("-inf")]), alpha[:-1]])
            a2 = torch.cat([torch.tensor([float("-inf")] * 2), alpha[:-2]])
            a2 = torch.where(skip, a2, torch.tensor(float("-inf")))
            alpha = torch.logsumexp(torch.stack([alpha, a1, a2]), 0) + logp[t, z]
        return -torch.logsumexp(alpha[-2:], 0)
    
    T, C = 50, 6
    logp = torch.randn(T, C).log_softmax(-1)
    y = [1, 3, 3, 2, 5, 1, 1, 4]
    ref = torch.nn.functional.ctc_loss(logp[:, None], torch.tensor([y]), [T], [len(y)], reduction="sum")
    print(f"ours {ctc_nll(logp, y):.4f}   torch {ref:.4f}")
    best = logp.argmax(-1).tolist()                           # greedy decoding: best symbol per frame
    decoded = [k for i, k in enumerate(best) if (i == 0 or k != best[i - 1]) and k != 0]
    print("greedy output length", len(decoded))

    Both give 63.8471. On random scores, greedy decoding outputs 38 symbols instead of 8: an untrained network has no reason to emit blanks.

  14. ★★ Naive Bayes and negation by hand. Train multinomial naive Bayes with add-one smoothing on four documents: (+) “good movie”, (+) “great plot”, (−) “not good”, (−) “boring plot”. Compute the log-odds of “not good” and of “not great”, and explain the second result.

    Solution: each class has 4 tokens and the vocabulary 6 words, so each estimate is (c + 1)/10. For “not good”, the log-odds are \log(0.1/0.2) + \log(0.2/0.2) = -0.69: negative, correct.

    For “not great”, they are \log(0.1/0.2) + \log(0.2/0.1) = 0: a tie. The model adds a fixed weight per word and cannot learn that “not” flips “great”; a bigram feature “not great”, or the NOT_ marking of Code 9.4, would.

  15. ★★ Temperature. Let p_T = \mathrm{softmax}(z / T) with distinct logits. Prove that p_T tends to the one-hot vector of \arg\max_k z_k as T \to 0, to the uniform law as T \to \infty, and that its entropy increases with T.

    Solution: divide numerator and denominator by e^{z_{\max}/T}; every other term is e^{-(z_{\max} - z_k)/T} \to 0. As T \to \infty, every e^{z_k/T} \to 1. With b = 1/T, H = \log Z(b) - b\, \mathbb{E}_p[z], and differentiating, \frac{dH}{db} = -b\, \mathrm{Var}_p(z), \qquad \frac{dH}{dT} = \frac{\mathrm{Var}_p(z)}{T^3} \geq 0 .

  16. ★★ Compute-optimal allocation. Minimise the scaling law of Definition 7.5 under the constraint 6ND = C. Show that the optimal sizes grow as powers of the budget, N_{\mathrm{opt}} \propto C^{\beta / (\alpha + \beta)}, \qquad D_{\mathrm{opt}} \propto C^{\alpha / (\alpha + \beta)}, and evaluate the exponents for the fitted values \alpha = 0.34 and \beta = 0.28 of Hoffmann and coauthors (2022).

    Solution: substitute D = C / (6N) and set the derivative in N to zero: \alpha A N^{-\alpha - 1} = \beta B\, 6^{\beta}\, C^{-\beta} N^{\beta - 1} \iff N^{\alpha + \beta} = \frac{\alpha A}{\beta B} \Big( \frac{C}{6} \Big)^{\beta} .

    Hence a = \beta/(\alpha + \beta) \approx 0.45 and D = C/(6N) gives b = 1 - a \approx 0.55: parameters and tokens grow almost equally, unlike earlier recommendations that put most of the budget into parameters.

  17. ★★★ Designing a human-parity study. A company claims that its sentiment classifier “reaches human performance” because its accuracy against one annotator equals that of a second annotator. Using Proposition 12.2 and Code 12.5, write the protocol of a study that would support or refute the claim.

    Hints and expected result: several independent annotators with a stated profile; agreement measured by kappa before any comparison; systems and humans scored on the same items against an adjudicated reference, or against each annotator in turn; paired bootstrap intervals; an analysis of error types (negation, irony, neutral cases), since equal accuracy can hide different errors; a pre-registered decision rule. The expected conclusion is a statement with an interval, not a yes or no.

  18. ★★★ Kneser–Ney against add-k (Python). Generate 25,000 tokens from a Markov chain over 300 words with sparse, Zipf-shaped transition rows. Train on 20,000 tokens and compare the test perplexity of add-k smoothing (several k) and interpolated Kneser–Ney (several d).

    Hints and expected result: tune k and d on the test set only for this exercise; use a held-out set in practice.

    import math
    import numpy as np
    from collections import Counter
    rng = np.random.default_rng(0)
    V = 300                                                   # Markov text with Zipfian, sparse rows
    zipf = 1.0 / np.arange(1, V + 1)
    P = np.stack([rng.dirichlet(0.05 * V * zipf / zipf.sum()) for _ in range(V)])
    cdf = P.cumsum(1)
    x, u = [0], rng.random(25000)
    for t in range(25000):
        x.append(min(int(np.searchsorted(cdf[x[-1]], u[t])), V - 1))
    train, test = x[:20000], x[20000:]
    big = Counter(zip(train, train[1:])); ctx = Counter(train[:-1])
    fol = Counter(a for a, b in big); pre = Counter(b for a, b in big); n_types = len(big)
    
    def pp(prob):
        lp = [math.log(prob(b, a)) for a, b in zip(test, test[1:])]
        return math.exp(-sum(lp) / len(lp))
    
    def add_k(k):
        return lambda b, a: (big[a, b] + k) / (ctx[a] + k * V)
    
    def kn(d):
        def p(b, a):
            cont = (pre[b] + 0.5) / (n_types + 0.5 * V)       # smoothed continuation probability
            if ctx[a] == 0:
                return cont
            return max(big[a, b] - d, 0) / ctx[a] + d * fol[a] / ctx[a] * cont
        return p
    
    print("add-k :", {k: round(pp(add_k(k)), 1) for k in [1, 0.1, 0.03, 0.01]})
    print("KN    :", {d: round(pp(kn(d)), 1) for d in [0.5, 0.75, 0.9]})

    Add-k gives 35.9 for k = 1 and at best 21.1 for k = 0.03; Kneser–Ney gives 20.2 for d = 0.5. The continuation probability needs a small add-0.5 term here, because a test word may never have been seen as a second word.

  19. ★★★ What do discrete speech units encode? (Python, research-flavoured.) Spoken language models (ch. 14) cluster speech features into units. Synthesise the vowels /a/, /i/ and /u/ at three pitches with the source–filter generator of Code 2.7, compute MFCCs without c_0, cluster the frames by k-means with 3, 9 and 27 units, and measure the normalised mutual information of the units with the vowel and with the pitch.

    Hints and expected result:

    import numpy as np
    from scipy.fft import dct
    from sklearn.cluster import KMeans
    from sklearn.metrics import normalized_mutual_info_score as nmi
    rng = np.random.default_rng(0)
    sr, n_fft, win, hop = 16000, 512, 400, 160
    vowels = {"a": [700, 1200], "i": [300, 2300], "u": [300, 800]}   # first two formants (Hz)
    edges = 700 * (10 ** (np.linspace(0, 2595 * np.log10(1 + 8000 / 700), 42) / 2595) - 1)
    bins = np.fft.rfftfreq(n_fft, 1 / sr)
    fb = np.stack([np.clip(np.minimum((bins - edges[m]) / (edges[m + 1] - edges[m]),
                                      (edges[m + 2] - bins) / (edges[m + 2] - edges[m + 1])), 0, None) for m in range(40)])
    
    def mfcc_frames(f0, formants):
        t = np.arange(sr // 4) / sr
        x = sum(np.exp(-0.5 * ((h * f0 - np.array(formants)) / 100.0) ** 2).sum() * np.sin(2 * np.pi * h * f0 * t)
                for h in range(1, int(7000 / f0)))
        x = x + 0.01 * rng.normal(size=x.size)
        frames = np.stack([x[i: i + win] for i in range(0, len(x) - win, hop)]) * np.hamming(win)
        logmel = np.log(np.abs(np.fft.rfft(frames, n_fft)) ** 2 @ fb.T + 1e-8)
        return dct(logmel, norm="ortho", axis=1)[:, 1:13]           # drop c0 (loudness)
    
    X, vowel_lab, pitch_lab = [], [], []
    for vi, (v, F) in enumerate(vowels.items()):
        for pj, f0 in enumerate([100, 150, 220]):
            M = mfcc_frames(f0, F); X.append(M)
            vowel_lab += [vi] * len(M); pitch_lab += [pj] * len(M)
    for k in [3, 9, 27]:
        units = KMeans(k, n_init=10, random_state=0).fit_predict(np.vstack(X))
        print(k, round(nmi(vowel_lab, units), 2), round(nmi(pitch_lab, units), 2))

    With 3 units the clusters are exactly the vowels (NMI 1.00 with the vowel, 0.00 with the pitch). With 9 units they also split by pitch (0.67 and 0.57), and with 27 units both are 0.52: finer units encode speaker properties along with phonetic content. The research question of ch. 14, 14.1, is how to keep the content and drop the rest; try adding noise, or speaker normalisation of the features.

Projects and assessment

The official assessment (master-mva.com) has two parts: online quizzes at the end of the classes, 40 % of the grade with the best 6 of 7 kept, and a final project, 60 %, done in groups of 2–4 around a recent speech or language processing paper with existing code. The project replicates the main result, then tests a new question; it is presented in a one-page plan, a 4-page report and a 10-minute talk with 5 minutes of questions, and graded individually.

The group subjects below follow this format; the solo subjects are smaller paper studies for practice; the written exam is a model of the comprehension questions.

Solo projects

  1. [Solo · ≈ 20 h] Word embeddings as matrix factorisation. Is SGNS better than a well-tuned SVD of the PPMI matrix? A paper study of Levy, Goldberg and Dagan, “Improving Distributional Similarity with Lessons Learned from Word Embeddings” (TACL, 2015), building on Theorem 5.5.
    • Implement: PPMI with context-distribution smoothing, truncated SVD, and SGNS (gensim), with matched windows and negative counts.
    • Data: the text8 corpus (the first 10^8 characters of a cleaned English Wikipedia dump); WordSim-353 (Finkelstein and coauthors, 2002) and SimLex-999 (Hill, Reichart and Korhonen, 2015) for evaluation.
    • Deliverables: code, a 4–6 page report, a 10-minute talk. Chapters: ch. 0, 5.
    • Success: Spearman correlations for both methods with bootstrap intervals; a check of Theorem 5.5 on frequent pairs; a verdict on the paper’s claim that hyperparameters matter more than the algorithm.
  2. [Solo · ≈ 20 h] A CTC recogniser for spoken digit strings. How far does CTC go without any language model on a small vocabulary? A paper study of Graves and coauthors, “Connectionist Temporal Classification” (ICML, 2006).
    • Implement: log-mel features (Code 2.7); a bidirectional LSTM trained with the CTC loss (Theorem 10.4); greedy and beam decoding.
    • Data: the ten digit words of Speech Commands (Warden, 2018), concatenated into random strings of 3 to 7 digits with random pauses, with disjoint speakers for training and test.
    • Deliverables: code, a 4–6 page report, a 10-minute talk. Chapters: ch. 2, 6, 10.
    • Success: a digit-string error rate below 10 % on unseen speakers, and an analysis of the peaky behaviour of CTC posteriors (most frames emit the blank).
  3. [Solo · ≈ 15 h] Neural text degeneration. Does nucleus sampling produce text whose statistics are closest to human text? A paper study of Holtzman and coauthors, “The Curious Case of Neural Text Degeneration” (ICLR, 2020).
    • Implement: greedy, beam (B = 5), pure, top-k (k = 40) and nucleus (p = 0.95) decoding (Definition 7.6) with GPT-2 small, from 200 prompts taken from WikiText-103 (Merity and coauthors, 2017).
    • Measure: the share of repeated 4-grams, distinct-n, self-BLEU (Code 8.6) and the perplexity of the generations under a larger model.
    • Deliverables: code, a 4-page report. Chapters: ch. 7, 8, 12.
    • Success: the paper’s ordering reproduced (beam and greedy most repetitive, pure sampling least coherent, nucleus closest to human statistics), with bootstrap intervals over prompts.

Group projects

Each subject follows the official format: replicate the main result of the paper with its code, then test one new question; one-page plan first, then a 4-page report and a 10-minute talk.

  1. [Group of 2–4 · ≈ 30 h each] Subword vocabularies for neural translation. Replicate the BPE result of Sennrich, Haddow and Birch, “Neural Machine Translation of Rare Words with Subword Units” (ACL, 2016), with a small transformer.
    • Data and code: IWSLT 2014 German→English (Cettolo and coauthors, 2014) with the fairseq toolkit (Ott and coauthors, 2019).
    • New question: the effect of the number of merges, or of BPE-dropout (Provilkov, Emelianenko and Voita, 2020), on rare words and on a low-resource subset of 20,000 pairs.
    • Split: data and tokenisation; training; evaluation (sacreBLEU, chrF, COMET, paired bootstrap) and error analysis; with four members, a human evaluation of 100 sentences.
    • Success: a baseline within 2 BLEU points of the score reported by the fairseq IWSLT’14 example, and a significance-tested answer to the new question. Chapters: ch. 1, 6, 8, 12.
  2. [Group of 2–4 · ≈ 30 h each] Where do fine-tuned transformers beat linear sentiment models? Replicate the SST-2 accuracy of DistilBERT (Sanh and coauthors, 2019), against the NB-SVM baseline of Wang and Manning (ACL, 2012).
    • Data: SST-2 (Socher and coauthors, 2013) and IMDB reviews (Maas and coauthors, 2011).
    • New question: accuracy on sentences with negation or contrast (“but”), robustness to typos, and transfer from one dataset to the other.
    • Split: baselines (ch. 9); fine-tuning (ch. 7); challenge sets and statistics (ch. 12).
    • Success: development accuracy within 1 point of the paper; the gap on the negation subset with a bootstrap interval; a reasoned answer to “is the gain composition or vocabulary?”.
  3. [Group of 3–4 · ≈ 30 h each] Sampling, pass@k and self-repair for code. Replicate the pass@k curves of Chen and coauthors, “Evaluating Large Language Models Trained on Code” (2021), with an open code model of at most 1.5 billion parameters on a free GPU tier.
    • Data: HumanEval and MBPP (Austin and coauthors, 2021).
    • New question: does an agent loop with execution feedback (Definition 11.7) beat independent sampling at an equal budget of model calls? Compare with the analysis of Olausson and coauthors (ICLR, 2024).
    • Split: sandboxed execution and the estimator (Proposition 11.5); sampling sweeps over temperature; the repair loop; analysis.
    • Success: pass@1 within 2 points of the value reported for the chosen model; the paper’s finding that the best temperature grows with k; the repair gain at equal budget, with intervals. Chapters: ch. 7, 11, 12.
  4. [Group of 2–4 · ≈ 30 h each] Self-supervised speech with ten minutes of labels. Replicate a low-resource row of wav2vec 2.0 (Baevski and coauthors, NeurIPS 2020): fine-tune the pretrained base model with CTC on the 10-minute or 1-hour split of Libri-light (Kahn and coauthors, 2020).
    • Evaluate: WER on LibriSpeech test-clean and test-other (Panayotov and coauthors, 2015), without and with an n-gram language model.
    • New question: which layers of the frozen model carry phonetic information (linear probes per layer), or how WER degrades with added noise.
    • Split: fine-tuning; decoding with and without LM; probing and analysis.
    • Success: WER within 20 % (relative) of the paper’s value for the same split and decoding setting, and a layer-wise probe curve. Chapters: ch. 2, 10, 12.

Written exam (proposal)

Two hours, one handwritten A4 sheet allowed, no electronic device; four questions, 20 points. The questions mirror the comprehension quizzes of the official assessment, with one computation and one proof.

  1. Question (4 points). Compare the HMM tagger, the linear-chain CRF and CTC: what each one models, which independence assumption it makes, and which dynamic program computes its likelihood or normaliser.

    Expected:

    • HMM: a joint law of tags and words; tags form a Markov chain and each word depends only on its tag; forward algorithm (1 pt).
    • CRF: a conditional law of tags given the whole sentence; Markov in the tags, arbitrary features of the input; forward algorithm for Z(x), concave likelihood (1.5 pts).
    • CTC: a conditional law of a label sequence given frames, frame outputs independent given the input, alignments summed by the forward recursion on the label sequence extended with blanks (1.5 pts).
  2. Question (6 points). (a) An HMM over the symbols x and y has two states, and \pi = (0.5, 0.5), \qquad A = \begin{pmatrix} 0.7 & 0.3 \\ 0.4 & 0.6 \end{pmatrix}, \qquad P(x \mid 1) = 0.9, \quad P(x \mid 2) = 0.2 . For the observations x, y, compute P(O) and the best path. (b) Compute BLEU with N = 1 for the hypothesis “the the cat” against the reference “the cat sat on the mat”. (c) Compute the WER of “turn lights of” against “turn the lights off”.

    Expected:

      1. \alpha_1 = (0.45, 0.10) and \alpha_2 = (0.0355, 0.156), so P(O) = 0.1915; \delta_2 = (0.0315, 0.108), best path 1 then 2, of probability 0.108 (3 pts).
      1. Clipped precision 3/3 = 1, but the hypothesis has 3 words against 6, so \mathrm{BP} = e^{1 - 6/3} \approx 0.368 and BLEU-1 is 0.368 (1.5 pts).
      1. One deletion (“the”) and one substitution (“off” → “of”) on 4 words: 50 % (1.5 pts).
  3. Question (5 points). Prove Theorem 5.5: if each inner product can be set freely, the SGNS objective summed over the corpus is maximised at \mathbf{w}^\top \mathbf{u} = \mathrm{PMI}(w, u) - \log k.

    Expected:

    • The pair is drawn as a negative k\, c(w)\, c(u) / D times in expectation, so the objective is a sum over pairs of (2 pts) \ell(s) = c(w, u) \log \sigma(s) + \frac{k\, c(w)\, c(u)}{D} \log \sigma(-s) .
    • With \sigma' = \sigma(s)\sigma(-s), setting \ell'(s) = 0 gives e^s = c(w, u) D / (k\, c(w) c(u)) (2 pts).
    • Each \ell is strictly concave, so this is the maximum, and the pairs decouple (1 pt).
  4. Question (5 points). (a) Show that if q, k \in \mathbb{R}^d have independent entries of mean 0 and variance 1, then \mathrm{Var}(q^\top k) = d, and explain the factor 1/\sqrt{d}. (b) With d = 2, compute the two outputs of causal scaled dot-product attention for q_1 = (1, 0),\ q_2 = (0, 1), \qquad k_1 = (1, 1),\ k_2 = (1, -1), \qquad v_1 = (1, 0),\ v_2 = (0, 1) .

    1. Why do transformers need positional information?

    Expected:

      1. A sum of d independent terms of mean 0 and variance 1; unscaled scores saturate the softmax (1.5 pts).
      1. Position 1 sees only itself: output v_1 = (1, 0). Position 2 has scores \pm 1/\sqrt{2}, weights (0.804, 0.196), output (0.804, 0.196) (2 pts).
      1. Without a mask or positions, self-attention is permutation-equivariant (Proposition 6.7), so “dog bites man” and “man bites dog” would get the same multiset of outputs (1.5 pts).

Grading rubric (proposal)

The rubric follows the official requirements of the project: a plan, a replication, a new question, a report, a talk and individual grading.

Criterion Weight What earns the marks
Plan and organisation 10 % A one-page plan with a precise question, a schedule and a split of the work, delivered on time
Replication 25 % The main result of the paper reproduced with its code, discrepancies measured and explained
New question 20 % A new task, dataset or variant that tests something the paper did not, motivated by the course
Evaluation and analysis 15 % Correct metrics (ch. 1, 4, 8, 10, 11), several seeds or bootstrap intervals, error analysis, negative results reported
Report 10 % Four clear pages: question, method, results with uncertainty, limits
Talk and questions 10 % Ten minutes on time, readable figures, precise answers in the 5 minutes of questions
Individual engagement 10 % Each member’s contribution visible in the plan, the code history and the answers to questions

References

Official reference (master-mva.com). The course description recommends, without making it obligatory, the textbook of Jurafsky and Martin: the 3rd online edition for the chapters already available [J&M3], the 2nd edition otherwise [J&M2]. Readings for each session are given by the instructors.

Textbooks.

History, language models and tokenisation (ch. 1, 7, 13).

Speech signal, HMMs and finite-state models (ch. 2, 3).

Sequence labelling and embeddings (ch. 4, 5).

Neural architectures (ch. 6).

Machine translation (ch. 8).

Sentiment analysis (ch. 9).

Speech recognition and synthesis (ch. 10, 13).

Dialogue, code and agents (ch. 11).

Evaluation (ch. 12).

Research directions (ch. 14).

Data sets and tools for the projects.