AI Knowledge Tree

N-gram language models

Derive the Markov n-gram approximation, build an MLE bigram table by counting, and compute sentence probabilities.

⏱ 30 min

A language model assigns a probability to every sequence of tokens. That probability is the central object of NLP: generate text by sampling from it, score a candidate translation by evaluating it, measure how surprised a model is by a new document with it. Before neural networks, the canonical parameterization was the n-gram model[1, 2]: simple, interpretable, and fast to build, but with fundamental structural limits that motivate every architecture that follows.

The autoregressive factorization

The chain rule of probability factors any joint distribution over a sequence x1,…,xTx_1, \dots, x_T into a product of conditionals:

p(x1,…,xT)=∏t=1Tp(xt∣x<t).p(x_1, \dots, x_T) = \prod_{t=1}^{T} p(x_t \mid x_{<t}).

This factorization is exact, no approximation so far. The challenge is representing p(xt∣x<t)p(x_t \mid x_{<t}), which in principle conditions on the entire prefix x1,…,xt−1x_1, \dots, x_{t-1}.

The Markov approximation

An n-gram model truncates the conditioning context to the last n−1n-1 tokens:

p(xt∣x<t)≈p(xt∣xt−n+1,…,xt−1).p(x_t \mid x_{<t}) \approx p(x_t \mid x_{t-n+1}, \dots, x_{t-1}).

This is the (n−1)(n-1)th-order Markov assumption. The special cases:

  • n=1n=1 (unigram): p(xt)p(x_t), no context, just a bag-of-words distribution.
  • n=2n=2 (bigram): p(xt∣xt−1)p(x_t \mid x_{t-1}), conditions on one previous token.
  • n=3n=3 (trigram): p(xt∣xt−2,xt−1)p(x_t \mid x_{t-2}, x_{t-1}), two-token context.

Maximum-likelihood estimation

Given a corpus C\mathcal{C}, let c(w1,…,wk)c(w_1, \dots, w_k) count how often the sequence (w1,…,wk)(w_1, \dots, w_k) appears. The MLE bigram probability is:

p^(b∣a)=c(a,b)c(a).\hat{p}(b \mid a) = \frac{c(a, b)}{c(a)}.

Training is just counting. There are no parameters to learn by gradient descent.

To handle sentence boundaries, prepend ⟨bos⟩\langle\text{bos}\rangle and append ⟨eos⟩\langle\text{eos}\rangle to each sentence. This lets the model assign probability to "the sentence ends here" and ensures the joint distribution over sequences of any length normalizes to 1.

A worked bigram

Toy corpus (three sentences):

C={I am Sam, Sam I am, I do not like green eggs}.\mathcal{C} = \{\text{I am Sam},\ \text{Sam I am},\ \text{I do not like green eggs}\}.

Selected bigram counts:

paircount
(⟨bos⟩,I)(\langle\text{bos}\rangle, \text{I})2
(⟨bos⟩,Sam)(\langle\text{bos}\rangle, \text{Sam})1
(I,am)(\text{I}, \text{am})2
(I,do)(\text{I}, \text{do})1
(am,Sam)(\text{am}, \text{Sam})1

Bigram MLE table (partial):

p^(I∣⟨bos⟩)=23,p^(Sam∣⟨bos⟩)=13,p^(am∣I)=23,p^(do∣I)=13.\hat{p}(\text{I} \mid \langle\text{bos}\rangle) = \tfrac{2}{3}, \quad \hat{p}(\text{Sam} \mid \langle\text{bos}\rangle) = \tfrac{1}{3}, \quad \hat{p}(\text{am} \mid \text{I}) = \tfrac{2}{3}, \quad \hat{p}(\text{do} \mid \text{I}) = \tfrac{1}{3}.

Probability of "Sam I am":

p=p^(Sam∣⟨bos⟩)⋅p^(I∣Sam)⋅p^(am∣I)⋅p^(⟨eos⟩∣am)=13⋅12⋅23⋅12=118.p = \hat{p}(\text{Sam} \mid \langle\text{bos}\rangle) \cdot \hat{p}(\text{I} \mid \text{Sam}) \cdot \hat{p}(\text{am} \mid \text{I}) \cdot \hat{p}(\langle\text{eos}\rangle \mid \text{am}) = \tfrac{1}{3} \cdot \tfrac{1}{2} \cdot \tfrac{2}{3} \cdot \tfrac{1}{2} = \tfrac{1}{18}.

The three structural limits

1. Sparsity. Most n-grams never appear, even in billion-token corpora. The MLE assigns them probability zero, so any test sentence containing an unseen bigram gets probability zero and infinite cross-entropy loss.

2. Parameter explosion. For vocabulary size VV and order nn, the table has VnV^n entries. At V=50,000V = 50{,}000 and n=3n = 3: 1.25×10141.25 \times 10^{14} entries, impossible to store or estimate.

3. Fixed range. The Markov assumption guarantees that any dependency spanning more than n−1n-1 tokens is invisible. The model can never resolve long-distance coreference or syntactic agreement.

These limits are the structural motivation for neural language models.

Word frequencies follow Zipf's law: frequency falls roughly as 1/rank, a near-straight line on log-log axes. The long tail of rare words is why high-order n-grams hit unseen contexts and need smoothing.
Word frequencies follow Zipf's law: frequency falls roughly as 1/rank, a near-straight line on log-log axes. The long tail of rare words is why high-order n-grams hit unseen contexts and need smoothing.

Perplexity

Perplexity is the standard metric for comparing language models on held-out text:

PPL=exp⁡ ⁣(−1T∑t=1Tlog⁡p(xt∣x<t)).\text{PPL} = \exp\!\left( -\frac{1}{T} \sum_{t=1}^{T} \log p(x_t \mid x_{<t}) \right).

It equals the exponentiated average per-token cross-entropy loss. Lower is better: a perfect model (assigns probability 1 to every correct next token) has PPL=1\text{PPL} = 1. Smoothed trigrams on news text achieve PPL≈200\text{PPL} \approx 200; modern LLMs achieve ∼10\sim 10.

python · numpy
import numpy as np
from collections import Counter, defaultdict

corpus = [
    ["<bos>", "I", "am", "Sam", "<eos>"],
    ["<bos>", "Sam", "I", "am", "<eos>"],
    ["<bos>", "I", "do", "not", "like", "green", "eggs", "<eos>"],
]

# Count unigrams and bigrams
unigrams = Counter()
bigrams = Counter()
for sent in corpus:
    for w in sent:
        unigrams[w] += 1
    for a, b in zip(sent, sent[1:]):
        bigrams[(a, b)] += 1

def bigram_prob(a, b):
    return bigrams[(a, b)] / unigrams[a] if unigrams[a] > 0 else 0.0

# Score a test sentence
test = ["<bos>", "Sam", "I", "am", "<eos>"]
log_prob = sum(
    np.log(bigram_prob(a, b) + 1e-30)
    for a, b in zip(test, test[1:])
)
n_tokens = len(test) - 1  # predict all but <bos>
ppl = np.exp(-log_prob / n_tokens)
print(f"log p = {log_prob:.3f}, perplexity = {ppl:.2f}")

The zero-probability problem is immediate: any unseen bigram sends log probability to −∞-\infty.

Exercises

Exercise 1. Using the toy corpus C={I am Sam, Sam I am, I do not like green eggs}\mathcal{C} = \{\text{I am Sam},\ \text{Sam I am},\ \text{I do not like green eggs}\} with ⟨bos⟩\langle\text{bos}\rangle and ⟨eos⟩\langle\text{eos}\rangle markers, compute the bigram MLE probability of the sentence "I am Sam" by hand.

Solution

Step 1. Collect the needed counts. The contexts that matter: c(⟨bos⟩)=3c(\langle\text{bos}\rangle) = 3 (three sentences), c(I)=3c(\text{I}) = 3 (I appears three times, heading the bigrams I-am, I-am, I-do), c(am)=2c(\text{am}) = 2, c(Sam)=2c(\text{Sam}) = 2.

Step 2. Read off the four bigram probabilities for the path ⟨bos⟩→I→am→Sam→⟨eos⟩\langle\text{bos}\rangle \to \text{I} \to \text{am} \to \text{Sam} \to \langle\text{eos}\rangle:

p^(I∣⟨bos⟩)=23,p^(am∣I)=23,p^(Sam∣am)=12,p^(⟨eos⟩∣Sam)=12.\hat{p}(\text{I} \mid \langle\text{bos}\rangle) = \tfrac{2}{3}, \quad \hat{p}(\text{am} \mid \text{I}) = \tfrac{2}{3}, \quad \hat{p}(\text{Sam} \mid \text{am}) = \tfrac{1}{2}, \quad \hat{p}(\langle\text{eos}\rangle \mid \text{Sam}) = \tfrac{1}{2}.

The bigram (am,Sam)(\text{am}, \text{Sam}) occurs once out of two am tokens; Sam ends one sentence ("I am Sam") out of its two occurrences.

Step 3. Multiply:

p=23⋅23⋅12⋅12=436=19.p = \tfrac{2}{3} \cdot \tfrac{2}{3} \cdot \tfrac{1}{2} \cdot \tfrac{1}{2} = \tfrac{4}{36} = \tfrac{1}{9}.

Exercise 2. A bigram model with vocabulary size VV has at most V2V^2 free parameters. Show that the bigram rows are not all independent: how many genuinely free numbers does the model carry, given that each row p^(⋅∣a)\hat{p}(\cdot \mid a) must sum to 1?

Solution

Step 1. There are VV possible context tokens aa, so VV conditional rows, each a distribution over the VV next tokens. Naively that is V⋅V=V2V \cdot V = V^2 numbers.

Step 2. Each row is a probability distribution, so its VV entries are constrained by ∑bp^(b∣a)=1\sum_b \hat{p}(b \mid a) = 1. That one linear constraint removes one degree of freedom per row: a row over VV outcomes has only V−1V - 1 free numbers.

Step 3. With VV rows each carrying V−1V - 1 free numbers, the model has

V(V−1)=V2−VV (V - 1) = V^2 - V

free parameters. The count is still O(V2)O(V^2), which is the parameter-explosion limit: the normalization constraint shaves off only a lower-order VV term.

Exercise 3 (code). Build a bigram model from the toy corpus and verify that every context row sums to 1 (a valid conditional distribution), and that p^(am∣I)=2/3\hat{p}(\text{am} \mid \text{I}) = 2/3.

Solution
python · numpy
import numpy as np
from collections import Counter, defaultdict

corpus = [
    ["<bos>", "I", "am", "Sam", "<eos>"],
    ["<bos>", "Sam", "I", "am", "<eos>"],
    ["<bos>", "I", "do", "not", "like", "green", "eggs", "<eos>"],
]

unigrams = Counter(w for sent in corpus for w in sent)
bigrams = Counter((a, b) for sent in corpus for a, b in zip(sent, sent[1:]))

# Group next-token probabilities by context
rows = defaultdict(dict)
for (a, b), c in bigrams.items():
    rows[a][b] = c / unigrams[a]

# Every context that starts a bigram must define a proper distribution.
# <eos> never starts a bigram, so it has no row.
for a, dist in rows.items():
    total = sum(dist.values())
    assert np.isclose(total, 1.0), (a, total)

assert np.isclose(rows["I"]["am"], 2 / 3)
print("rows valid; p(am | I) =", round(rows["I"]["am"], 4))

Each context row normalizes to 1 because its bigram counts sum to the context's count as a left token, and "am" follows "I" in two of the three I-contexts.

The next lesson introduces smoothing to fix the zero-probability problem.


References

  1. [1]Jurafsky, D., & Martin, J. H. (2024). Speech and Language Processing (3rd ed. draft). N-gram language models, the Markov assumption, and perplexity.↩
  2. [2]Manning, C. D., & Schütze, H. (1999). Foundations of Statistical Natural Language Processing. MIT Press. Statistical n-gram estimation and evaluation.↩