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.
- Language modeling assigns probabilities to word sequences; n-gram models do this via short, fixed-length context windows.
- The Markov assumption approximates by .
- 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 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.
An n-gram model predicts the next word using only the previous 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 , the model assigns , and by the chain rule of probability this factors exactly as
This factorization is exact — no assumption yet — but it is useless in practice because the conditioning context grows without bound, and no corpus is large enough to have seen every possible long history even once. Estimating directly from counts would require the exact prefix to have occurred before in training data, which becomes vanishingly likely as grows. The n-gram model resolves this with the Markov assumption: the probability of the next word depends only on the previous words, not the entire history.
An n-gram itself is simply a contiguous sequence of tokens. A unigram model () assumes words are independent; a bigram model () conditions on one previous word; a trigram model () 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 words. Combining the chain rule with the Markov approximation gives the full sentence probability under an n-gram model:
where denotes the shorthand for the -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 . 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 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 -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:
Generalizing to arbitrary :
Here 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 -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, , 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.
Complexity & correctness
Time complexity. Training scans the corpus once: for a corpus of total tokens, building the count tables takes time (assuming amortized hash-map insert/increment). Scoring a sentence of length takes lookups, each 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 (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 , which grows with both corpus size and : larger means exponentially more possible distinct windows ( in principle), so storage and sparsity both worsen as 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 , let be the counts of each vocabulary word following in the training data, with . We want probabilities (with ) that maximize the likelihood of the observed continuations, i.e., maximize , equivalently maximize subject to . Using a Lagrange multiplier :
Summing both sides over and using gives , so — 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 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 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 and context counts :
| Context | Next word | |||
|---|---|---|---|---|
| ` | I | 2 | 3 | 2/3 |
| ` | Sam | 1 | 3 | 1/3 |
| I | am | 2 | 3 | 2/3 |
| I | do | 1 | 3 | 1/3 |
| am | Sam | 1 | 2 | 1/2 |
| am | `` | 1 | 2 | 1/2 |
| Sam | I | 1 | 2 | 1/2 |
| Sam | `` | 1 | 2 | 1/2 |
| do | not | 1 | 1 | 1 |
| not | like | 1 | 1 | 1 |
| like | green | 1 | 1 | 1 |
| green | eggs | 1 | 1 | 1 |
| eggs | `` | 1 | 1 | 1 |
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):
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 : it is 0, so , 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
| Pitfall | Why it happens | Fix |
|---|---|---|
| Zero probability for unseen n-grams | MLE assigns 0 to anything uncounted; one zero kills the whole sentence product | Apply smoothing (Laplace/add-k, back-off, Kneser-Ney) |
| Missing ` | Without padding, first/last words have ill-defined or asymmetric context | Pad every sentence with start tokens and one end token |
| Underflow from multiplying many probabilities | Products of many numbers shrink toward 0 in floating point | Work in log-space; sum log-probabilities instead |
| Confusing in "n-gram" with context length | A trigram model conditions on 2 previous words, not 3 | Remember: context length is , not |
| Vocabulary mismatch between train/test | Test set contains out-of-vocabulary (OOV) words unseen in training | Reserve an `` token; map rare/unseen training words to it too |
| Choosing too large for corpus size | Distinct n-gram count grows combinatorially; most contexts seen once or never | Cross-validate 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
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.
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 , conditioning on the entire preceding history. As sentences grow, the exact prefix 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 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 ; it depends on corpus size and application. Unigrams () 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 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 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 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 to ), 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 , 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 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., . This identity is what guarantees the estimated conditional distribution 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: . 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.