N-gram Language Models

Imagine typing "I want to eat Chinese" into a phone keyboard. You see "food" suggested next. Somewhere behind that suggestion is a probability judgment. Given the words seen so far, which word is most likely to come next? This is the core problem of language modeling. It means assigning probabilities to sequences of words, or predicting the next word given a history. Long before transformers and neural embeddings, this problem had a solution. It was clean, elegant, and surprisingly effective, built entirely from counting words in text. N-gram language models solve this by making a deliberate simplification. Instead of conditioning on the entire, unbounded history of a sentence, they assume something simpler. Only the last few words matter. This Markov assumption turns an intractable estimation problem into a tractable one. The original problem was the probability of a word given an arbitrarily long context. The tractable version is solvable by counting short substrings in a corpus. The resulting models power spell-checkers, speech recognizers, and machine translation systems. They were the backbone of virtually all applied NLP systems for decades. This lasted before neural approaches took over. Understanding n-gram models is not merely a history lesson. Three things matter here: the Markov assumption, maximum-likelihood estimation from counts, and perplexity-based evaluation. These form the conceptual scaffolding for every modern language model. This includes large neural and transformer-based ones. Even GPT-style models are, mathematically, still autoregressive language models. They predict the next token from context. N-grams are the simplest possible instance of that idea. They are stripped of all learned representations. This article builds n-gram models from first principles. It covers how the Markov assumption reduces sequence probability to local counts. It shows how maximum-likelihood estimation (MLE) computes those probabilities. It explains how to implement the model efficiently. It also shows where the approach succeeds and catastrophically fails. This includes the zero-probability problem, which motivates smoothing. By the end, you should be able to derive an n-gram probability by hand. You should be able to trace through the counting algorithm. And you should be able to explain precisely why bigger n is both more powerful and more dangerous.

Key takeaways
  • Language modeling assigns probabilities to word sequences; n-gram models do this via short, fixed-length context windows.
  • The Markov assumption approximates P(wi∣w1,…,wi−1)P(w_i \mid w_1, \dots, w_{i-1}) by P(wi∣wi−n+1,…,wi−1)P(w_i \mid w_{i-n+1}, \dots, w_{i-1}).
  • Probabilities are estimated by maximum-likelihood estimation: simple relative-frequency counts from a training corpus.
  • The chain rule decomposes a sentence's probability into a product of these local conditional probabilities.
  • Larger nn captures richer context but suffers from data sparsity and zero-count problems — motivating smoothing.
  • Model quality is measured with perplexity, a normalized inverse likelihood on held-out text.
The core insight

An n-gram model predicts the next word using only the previous n−1n-1 words, turning language modeling into a counting problem over short substrings.

The idea

A language model (LM) is a probability distribution over sequences of words. For a sentence W=w1w2⋯wTW = w_1 w_2 \cdots w_T, the model assigns P(W)P(W), and by the chain rule of probability this factors exactly as

P(w1,…,wT)=∏i=1TP(wi∣w1,…,wi−1).P(w_1, \dots, w_T) = \prod_{i=1}^{T} P(w_i \mid w_1, \dots, w_{i-1}).

This factorization is exact — no assumption yet — but it is useless in practice because the conditioning context w1,…,wi−1w_1, \dots, w_{i-1} grows without bound, and no corpus is large enough to have seen every possible long history even once. Estimating P(wi∣w1,…,wi−1)P(w_i \mid w_1, \dots, w_{i-1}) directly from counts would require the exact prefix to have occurred before in training data, which becomes vanishingly likely as ii grows. The n-gram model resolves this with the Markov assumption: the probability of the next word depends only on the previous n−1n-1 words, not the entire history.

P(wi∣w1,…,wi−1)≈P(wi∣wi−n+1,…,wi−1)P(w_i \mid w_1, \dots, w_{i-1}) \approx P(w_i \mid w_{i-n+1}, \dots, w_{i-1})

An n-gram itself is simply a contiguous sequence of nn tokens. A unigram model (n=1n=1) assumes words are independent; a bigram model (n=2n=2) conditions on one previous word; a trigram model (n=3n=3) conditions on two. The name is a slight misnomer in common usage: "bigram model" refers to a model that uses two-word windows to estimate one-word-ahead conditional probabilities, i.e., it conditions on a history of n−1n-1 words. Combining the chain rule with the Markov approximation gives the full sentence probability under an n-gram model:

P(w1,…,wT)≈∏i=1TP(wi∣wi−n+1i−1)P(w_1, \dots, w_T) \approx \prod_{i=1}^{T} P(w_i \mid w_{i-n+1}^{i-1})

where wi−n+1i−1w_{i-n+1}^{i-1} denotes the shorthand for the (n−1)(n-1)-word history. Sentences are typically padded with special boundary tokens `` (start) and `` (end) so that the model can assign probability to the first few words and learn when a sentence is likely to terminate. The remaining question is how to obtain the conditional probabilities P(wi∣history)P(w_i \mid \text{history}). This is where maximum-likelihood estimation (MLE) enters: count how often the history is followed by each candidate word in a training corpus, and normalize.

How it works

Step 1 — Preprocessing. Tokenize the corpus (this is assumed prior knowledge; see Text Preprocessing and Tokenization), and wrap each sentence with n−1n-1 start markers `` and one end marker ``. This padding ensures every position has a well-defined history and that sentence-ending behavior is itself learned.

Step 2 — Counting. Scan the corpus once and count every n-gram occurrence, and every (n−1)(n-1)-gram (context) occurrence. These counts are stored in a hash map or trie keyed by the token tuple.

Step 3 — Estimation (MLE). The maximum-likelihood estimate of a bigram probability is the relative frequency of the bigram among all bigrams sharing that history:

P(wi∣wi−1)=C(wi−1,wi)∑wC(wi−1,w)=C(wi−1,wi)C(wi−1)P(w_i \mid w_{i-1}) = \frac{C(w_{i-1}, w_i)}{\sum_{w} C(w_{i-1}, w)} = \frac{C(w_{i-1}, w_i)}{C(w_{i-1})}

Generalizing to arbitrary nn:

P(wi∣wi−n+1i−1)=C(wi−n+1i−1,wi)C(wi−n+1i−1)P(w_i \mid w_{i-n+1}^{i-1}) = \frac{C(w_{i-n+1}^{i-1}, w_i)}{C(w_{i-n+1}^{i-1})}

Here C(⋅)C(\cdot) denotes raw count in the training corpus. This is literally "count and divide" — no iterative optimization is needed because the log-likelihood of the training data is maximized in closed form by relative frequencies (shown in Complexity & correctness below).

Step 4 — Scoring a new sentence. To compute the probability (or, more commonly, the log-probability, to avoid numerical underflow from multiplying many small numbers) of a new sentence, slide an nn-word window across it and multiply the per-position conditional probabilities looked up from the count tables.

Step 5 — Handling unseen n-grams. If a test n-gram was never observed in training, C(⋅)=0C(\cdot) = 0, and the MLE probability is exactly zero — collapsing the entire sentence probability to zero regardless of how plausible the rest of the sentence is. This is the central weakness of pure MLE n-grams and is the reason smoothing techniques (Laplace, Kneser-Ney, back-off) exist; those are developed as their own topic, but the pseudocode below flags exactly where they intervene. ```

# ---- TRAINING ---- function TRAIN_NGRAM(corpus, n): ngram_counts = empty hash map # maps (w_{i-n+1}, ..., w_i) -> count context_counts = empty hash map # maps (w_{i-n+1}, ..., w_{i-1}) -> count for sentence in corpus: tokens = [] * (n - 1) + tokenize(sentence) + [] for i from (n - 1) to length(tokens) - 1: ngram = tokens[i - n + 1 .. i] # length-n tuple context = tokens[i - n + 1 .. i - 1] # length-(n-1) tuple ngram_counts[ngram] += 1 context_counts[context] += 1 return ngram_counts, context_counts

# ---- PROBABILITY OF ONE NEXT WORD ---- function PROB(word, context, ngram_counts, context_counts, V): ngram = context + (word,) if context_counts[context] == 0: return UNDEFINED # never-seen context; hand off to smoothing/back-off if ngram_counts[ngram] == 0: return 0.0 # MLE gives zero probability -- the sparsity problem return ngram_counts[ngram] / context_counts[context]

# ---- SCORING A FULL SENTENCE ---- function SENTENCE_LOGPROB(sentence, n, ngram_counts, context_counts): tokens = [] * (n - 1) + tokenize(sentence) + [] logP = 0.0 for i from (n - 1) to length(tokens) - 1: context = tokens[i - n + 1 .. i - 1] word = tokens[i] p = PROB(word, context, ngram_counts, context_counts, V=vocab_size) if p == 0.0 or p == UNDEFINED: return -infinity # whole sentence assigned zero probability logP += log(p) return logP

# ---- GREEDY / SAMPLING GENERATION ---- function GENERATE(n, ngram_counts, context_counts, max_len): context = (,) * (n - 1) output = [] repeat max_len times: w = argmax_or_sample over vocabulary V of PROB(w, context, ...) if w == : break output.append(w) context = (context[1:] + (w,)) # slide the window forward return output ``` The two hash maps (`ngram_counts`, `context_counts`) are the entire model — there is no training loop, no gradient descent, no iteration to convergence. This is one of the appeals of the classical approach: training is a single linear pass over the corpus.

Figure
Sliding the trigram window across a sentence and looking up counts
Trigram (n=3) scoring of "the cat sat down" <s> <s> the cat sat down </s> context=(<s>,<s>) → predict "the" context=(<s>,the) → predict "cat" context=(the,cat) → predict "sat"

Complexity & correctness

Time complexity. Training scans the corpus once: for a corpus of TT total tokens, building the count tables takes O(T)O(T) time (assuming O(1)O(1) amortized hash-map insert/increment). Scoring a sentence of length LL takes O(L)O(L) lookups, each O(1)O(1) amortized. This is asymptotically optimal: any language model must at minimum read every token of the sentence once.

Space complexity. In the worst case the number of distinct n-grams is O(T)O(T) (every window is unique), but in practice with natural language's Zipfian structure the number of distinct n-grams is much smaller and grows sublinearly after the vocabulary saturates. Space is O(number of distinct n-grams and contexts observed)O(\text{number of distinct n-grams and contexts observed}), which grows with both corpus size and nn: larger nn means exponentially more possible distinct windows (∣V∣n|V|^n in principle), so storage and sparsity both worsen as nn increases. This tension — bigger context window vs. exploding parameter count — is the central engineering tradeoff of n-gram modeling.

Why MLE is the "correct" estimator here. Given a fixed context cc, let n1,…,nkn_1, \dots, n_k be the counts of each vocabulary word following cc in the training data, with N=∑jnjN = \sum_j n_j. We want probabilities p1,…,pkp_1, \dots, p_k (with ∑pj=1\sum p_j = 1) that maximize the likelihood of the observed continuations, i.e., maximize ∏jpjnj\prod_j p_j^{n_j}, equivalently maximize ∑jnjlog⁡pj\sum_j n_j \log p_j subject to ∑jpj=1\sum_j p_j = 1. Using a Lagrange multiplier λ\lambda:

L=∑jnjlog⁡pj−λ(∑jpj−1),∂L∂pj=njpj−λ=0  ⟹  pj=njλ.\mathcal{L} = \sum_j n_j \log p_j - \lambda\Big(\sum_j p_j - 1\Big), \qquad \frac{\partial \mathcal{L}}{\partial p_j} = \frac{n_j}{p_j} - \lambda = 0 \implies p_j = \frac{n_j}{\lambda}.

Summing both sides over jj and using ∑jpj=1\sum_j p_j = 1 gives λ=N\lambda = N, so pj=nj/Np_j = n_j / N — exactly the relative-frequency count. This proves that the count-and-divide rule is not an ad hoc heuristic but the unique maximizer of the training-data log-likelihood under the categorical (multinomial) model implicit in the n-gram assumption. It is correct in the narrow but precise sense of being the maximum-likelihood estimator for this model class.

The correctness caveat — generalization, not fit. MLE guarantees the best possible fit to the training data, not good behavior on unseen data. Any n-gram absent from training receives probability exactly zero, which is provably too pessimistic: absence of evidence is not evidence of impossibility. This is why n-gram models are always paired with a smoothing method in practice; MLE is correct as an in-sample optimizer but systematically miscalibrated out-of-sample, an issue formalized and measured via Perplexity on held-out data. That pairing (MLE counts + smoothing) is covered under Smoothing for Language Models.

Markov assumption validity. The approximation P(wi∣w1i−1)≈P(wi∣wi−n+1i−1)P(w_i \mid w_1^{i-1}) \approx P(w_i \mid w_{i-n+1}^{i-1}) is exact only if the true generative process for language were actually a finite-order Markov chain — it is not; long-range syntactic and discourse dependencies exist (subject-verb agreement across clauses, pronoun coreference many sentences later). The assumption is a deliberate bias-variance tradeoff: it sacrifices some modeling fidelity (bias) in exchange for the ability to estimate parameters reliably from finite data (variance reduction). Larger nn reduces bias but increases variance (more parameters, sparser counts per parameter) — a textbook bias–variance tradeoff instantiated in an NLP setting.

Worked example

Consider this toy training corpus of three sentences (already tokenized, lowercase): ``` I am Sam Sam I am I do not like green eggs ``` We train a bigram model. First tally the counts C(wi−1,wi)C(w_{i-1}, w_i) and context counts C(wi−1)C(w_{i-1}):

Context wi−1w_{i-1}Next word wiw_iC(wi−1,wi)C(w_{i-1},w_i)C(wi−1)C(w_{i-1})P(wi∣wi−1)P(w_i \mid w_{i-1})
``I232/3
``Sam131/3
Iam232/3
Ido131/3
amSam121/2
am``121/2
SamI121/2
Sam``121/2
donot111
notlike111
likegreen111
greeneggs111
eggs``111

Now score the new sentence ` I am Sam ` (it happens to appear verbatim in training, chosen deliberately so we can sanity-check against the counts):

P(I am Sam)=P(I∣<s>)⋅P(am∣I)⋅P(Sam∣am)⋅P(</s>∣Sam)P(\text{I am Sam}) = P(\text{I}\mid\text{\texttt{<s>}}) \cdot P(\text{am}\mid\text{I}) \cdot P(\text{Sam}\mid\text{am}) \cdot P(\text{\texttt{</s>}}\mid\text{Sam})
=23×23×12×12=436=19≈0.111= \tfrac{2}{3} \times \tfrac{2}{3} \times \tfrac{1}{2} \times \tfrac{1}{2} = \frac{4}{36} = \frac{1}{9} \approx 0.111

Now try a novel sentence: ` I like eggs `. Every bigram it needs — (I, like), (like, eggs) — was never observed jointly in training (we only saw I am, I do, not like, green eggs). Look up C(I,like)C(\text{I},\text{like}): it is 0, so P(like∣I)=0P(\text{like}\mid\text{I}) = 0, and the entire sentence probability collapses to exactly zero — even though "I like eggs" is a perfectly ordinary English sentence and every individual word appears in training. This starkly demonstrates the sparsity problem flagged above: MLE n-grams cannot generalize past exact substrings seen during training, which is precisely the gap that additive smoothing, back-off, and interpolation (Kneser-Ney) are designed to close.

Common pitfalls & traps

PitfallWhy it happensFix
Zero probability for unseen n-gramsMLE assigns 0 to anything uncounted; one zero kills the whole sentence productApply smoothing (Laplace/add-k, back-off, Kneser-Ney)
Missing ``/`` paddingWithout padding, first/last words have ill-defined or asymmetric contextPad every sentence with n−1n-1 start tokens and one end token
Underflow from multiplying many probabilitiesProducts of many numbers <1<1 shrink toward 0 in floating pointWork in log-space; sum log-probabilities instead
Confusing nn in "n-gram" with context lengthA trigram model conditions on 2 previous words, not 3Remember: context length is n−1n-1, not nn
Vocabulary mismatch between train/testTest set contains out-of-vocabulary (OOV) words unseen in trainingReserve an `` token; map rare/unseen training words to it too
Choosing nn too large for corpus sizeDistinct n-gram count grows combinatorially; most contexts seen once or neverCross-validate nn against held-out perplexity; prefer back-off/interpolation

Pioneers & history

The mathematical seed of the n-gram idea predates computing entirely: it comes from the theory of stochastic processes with limited memory.

Origins · 1 minAndrey Andreyevich Markov and how this was discovered
AA
Andrey Andreyevich Markov
1856–1922 · Russian mathematician

Markov developed the theory of stochastic processes now named after him — sequences where the future depends on the past only through a finite recent window. He famously demonstrated the idea in 1913 by hand-counting vowel/consonant transitions across tens of thousands of letters of Pushkin's Eugene Onegin, an early and direct ancestor of the character-level n-gram model.

History
1913
— Markov applies chain models to letter sequences in Pushkin's poetry, establishing the finite-memory stochastic process.
1948
— Claude Shannon's A Mathematical Theory of Communication formalizes entropy and applies n-gram-style word/letter models to English text generation.
1980
— IBM's speech recognition group (Jelinek and colleagues) develops statistical n-gram language models as the core component of practical speech recognizers.
1992
— Kneser and Ney (with later refinements by Chen and Goodman) introduce smoothing methods that make n-gram models competitive across widely varying corpus sizes.
2003
— Bengio et al.'s neural probabilistic language model begins the shift from count-based n-grams toward learned distributed representations, foreshadowing the transformer era.

Where this matters

N-gram language models were, for decades, the statistical backbone of speech recognition (scoring candidate transcriptions for fluency), statistical machine translation (favoring fluent target-language output), spelling and grammar correction (flagging low-probability word sequences), and predictive text / autocomplete on early mobile keyboards. Search engines used n-gram statistics for query completion and did-you-mean suggestions. Information-retrieval systems used n-gram overlap for text similarity and plagiarism detection. Even though large neural language models have superseded n-grams for state-of-the-art generation quality, n-grams remain in active use: as fast, cheap baselines; inside evaluation metrics like BLEU and ROUGE (which compare n-gram overlap between generated and reference text); in lightweight on-device applications where a neural model is too large or slow; and in smoothing/back-off techniques that continue to be studied as interpolation weights in hybrid neural-statistical systems. Conceptually, every autoregressive neural language model — including GPT-style transformers — is still factoring sentence probability via the same chain rule that opens this article; only the conditional probability estimator has changed, from counting to a deep network. Understanding n-grams gives you, essentially for free, the probabilistic scaffolding underneath every modern LM, and sets up the two natural next questions: how do we fix the zero-probability problem (see Smoothing for Language Models), and how do we numerically compare two language models' quality (see Perplexity).

Ask the tutor: How does add-one (Laplace) smoothing modify the MLE formula to avoid zero probabilities, and what problems does that fix introduce in turn?

Why do we use the Markov assumption instead of the exact chain rule?

The exact chain rule requires P(wi∣w1,…,wi−1)P(w_i \mid w_1, \dots, w_{i-1}), conditioning on the entire preceding history. As sentences grow, the exact prefix w1,…,wi−1w_1, \dots, w_{i-1} becomes increasingly unlikely to have ever occurred verbatim in any finite training corpus, making direct counting infeasible — there would be no data to estimate from. The Markov assumption truncates the context to the last n−1n-1 words, so the same short context recurs many times across a corpus, giving enough repeated observations to estimate a probability reliably. This trades some modeling accuracy (ignoring long-range dependencies) for the ability to actually estimate parameters from finite data — a bias–variance tradeoff, not a free simplification.

How do you choose the right value of n?

There is no universally correct nn; it depends on corpus size and application. Unigrams (n=1n=1) ignore all context and perform poorly. Bigrams and trigrams are common defaults for small-to-medium corpora because higher-order n-grams need exponentially more data to see each context multiple times. In practice, the value of nn is chosen empirically by measuring Perplexity on a held-out validation set for several candidate values and selecting the one that minimizes it, often combined with back-off or interpolation across multiple orders of nn simultaneously rather than committing to a single fixed order.

What is the difference between an n-gram and an n-gram model?

An n-gram is simply a contiguous sequence of nn tokens extracted from text — e.g., "the cat sat" is a trigram. An n-gram model is the probabilistic model built from counting all such n-grams in a corpus and using maximum-likelihood estimation to derive conditional next-word probabilities from those counts. The n-gram is the unit of counting; the model is the resulting probability distribution over sequences constructed from those counts via the chain rule and the Markov assumption.

Why does multiplying probabilities cause underflow, and how is it fixed?

Word probabilities are typically small fractions (often 10−210^{-2} to 10−410^{-4}), and multiplying dozens of them together in a long sentence produces a product that shrinks toward zero far faster than standard floating-point precision can represent accurately, eventually underflowing to exactly 0.0. The standard fix is to work in log-space: since log⁡(ab)=log⁡a+log⁡b\log(ab) = \log a + \log b, the sentence's log-probability is computed as a sum of log-probabilities instead of a product of probabilities. Sums of moderately negative numbers do not underflow, so this reformulation is both numerically stable and computationally cheaper, since addition is faster than multiplication.

How is an n-gram model actually used to generate text?

Starting from a context of n−1n-1 start tokens, the model looks up the conditional probability distribution over the entire vocabulary given that context, then either samples a word from that distribution or greedily picks the highest-probability word. The chosen word is appended to the output and slides into the context window, dropping the oldest word, and the process repeats until an end token is generated or a maximum length is reached. Because contexts are short, generated text from low-order n-gram models tends to be locally fluent but globally incoherent, drifting topic every few words since no long-range memory is retained.

Does the order of counting (n-grams before or after context) matter for correctness?

No — both `ngram_counts` and `context_counts` are accumulated by a single linear scan over the tokenized corpus, and the final conditional probability is simply their ratio computed at query time, not during counting. What does matter is consistency: the context count for a given history must equal the sum of all n-gram counts sharing that same history, i.e., C(wi−n+1i−1)=∑wC(wi−n+1i−1,w)C(w_{i-n+1}^{i-1}) = \sum_{w} C(w_{i-n+1}^{i-1}, w). This identity is what guarantees the estimated conditional distribution P(⋅∣context)P(\cdot \mid \text{context}) sums to exactly 1 over the vocabulary, which is essential for the model to be a valid probability distribution.

How do n-gram models relate to neural language models like transformers?

Both are instances of the same chain-rule factorization: P(w1,…,wT)=∏iP(wi∣context)P(w_1, \dots, w_T) = \prod_i P(w_i \mid \text{context}). N-gram models estimate each conditional probability via counting over a fixed, short window; neural models instead use a learned function (an RNN or transformer) that can, in principle, condition on the entire preceding sequence and generalize to unseen contexts via distributed word representations rather than exact-match counting. The neural approach solves n-grams' sparsity problem directly, since similar contexts map to nearby vectors and share statistical strength, but it requires iterative gradient-based training rather than a single closed-form counting pass.

Before this

Where this leads