N-gram Language Models › Maximum Likelihood Estimation
Language AI HandbookAll chapters N-gram Language Models › Maximum Likelihood EstimationOne section · Sections in this chapter4 / 15 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
With the Markov assumption in hand, we have reduced our problem to estimating conditional probabilities like . But how do we compute these from data?
The most straightforward approach is maximum likelihood estimation (MLE): count how often each n-gram appears in our training corpus, then normalize. For a given context, the MLE probability of the next word is simply the fraction of times that word followed that context in training.
Deriving the MLE Formula
The MLE formula follows directly from the definition of conditional probability. Recall that conditional probability is defined as the joint probability of both events divided by the marginal probability of the conditioning event:
To estimate from training data, we approximate each probability with its empirical relative frequency. The joint probability is approximated by how often the pair occurs, divided by the total number of bigrams. The marginal probability is approximated by how often occurs, divided by the total number of words.
When we compute the ratio, the denominators cancel, and we are left with:
where:
- : the count of times the bigram appears consecutively in the training corpus
- : the count of times the context word appears anywhere in the corpus
- The ratio gives the proportion of occurrences that are followed by
Worked example: If "the" appears 100 times in our corpus and "the cat" appears 8 times, then:
This means 8% of the time "the" is followed by "cat."
For trigrams, we extend the same logic to two-word contexts:
where:
- : the count of the trigram (three consecutive words)
- : the count of the two-word context alone (the bigram prefix)
The general MLE formula for any n-gram order is:
where:
- : the word whose probability we want to estimate
- : the context of words immediately preceding
- : the count of the complete n-gram in the training corpus
- : the count of the -gram context alone
The formula always divides the count of the full n-gram by the count of its -gram prefix. This relative frequency estimate is the maximum likelihood solution because it maximizes the probability of observing the training data under the model. No other choice of conditional probability parameters would assign a higher likelihood to the observed training corpus. If you were to increase any single above the relative frequency estimate, you would have to decrease the probability of other words following that context, and the net effect would be a decrease in training data likelihood.
Implementing MLE Estimation
Let us implement this and see it work on a small corpus:
from collections import Counter
def count_ngrams(tokens, n):
"""Count n-grams in a token sequence."""
ngram_counts = Counter()
for i in range(len(tokens) - n + 1):
ngram = tuple(tokens[i : i + n])
ngram_counts[ngram] += 1
return ngram_counts
# Sample corpus
corpus = """
the cat sat on the mat
the cat ate the fish
the dog sat on the mat
the dog chased the cat
"""
# Tokenize
tokens = corpus.lower().split()
# Count unigrams and bigrams
unigram_counts = count_ngrams(tokens, 1)
bigram_counts = count_ngrams(tokens, 2)Corpus tokens: 22
Unique unigrams: 9
Unique bigrams: 14
Top 10 bigrams:
('the', 'cat'): 3
('sat', 'on'): 2
('on', 'the'): 2
('the', 'mat'): 2
('mat', 'the'): 2
('the', 'dog'): 2
('cat', 'sat'): 1
('cat', 'ate'): 1
('ate', 'the'): 1
('the', 'fish'): 1Our small corpus contains 24 tokens but only 9 unique words. The most frequent bigrams involve "the," reflecting its role as the most common English word. Notice the Zipf-like distribution: a few bigrams appear multiple times while most appear only once. This pattern explains why n-gram models struggle with data sparsity. As we move to higher-order n-grams, the long tail grows longer and most n-grams remain unseen even in large corpora.
Now let us apply the MLE formula to compute actual probabilities:
def bigram_probability(w1, w2, bigram_counts, unigram_counts):
"""Calculate P(w2|w1) using MLE."""
bigram = (w1, w2)
unigram = (w1,)
bigram_count = bigram_counts.get(bigram, 0)
unigram_count = unigram_counts.get(unigram, 0)
if unigram_count == 0:
return 0.0
return bigram_count / unigram_count
# Calculate example probabilities
examples = [
("the", "cat"),
("the", "dog"),
("the", "mat"),
("cat", "sat"),
("sat", "on"),
("on", "the"),
]
probabilities = [
(w1, w2, bigram_probability(w1, w2, bigram_counts, unigram_counts))
for w1, w2 in examples
]Bigram Probabilities P(w2|w1): --------------------------------------------- P(cat|the) = 3/8 = 0.375 P(dog|the) = 2/8 = 0.250 P(mat|the) = 2/8 = 0.250 P(sat|cat) = 1/3 = 0.333 P(on|sat) = 2/2 = 1.000 P(the|on) = 2/2 = 1.000
The probabilities reveal patterns in our corpus. After "the," both "cat" and "dog" appear with equal probability (25% each), while "mat" also appears 25% of the time. After "sat," the word "on" appears with 100% probability. This reflects the fixed phrase "sat on" that appears in every relevant sentence. After "on," the word "the" follows every time.
These probabilities capture the local structure of our tiny corpus. With only four sentences, these patterns are artifacts of limited data. A larger corpus would reveal more detailed distributions, but the estimation procedure is exactly the same.
We can visualize the complete bigram probability matrix as a heatmap, where each cell shows , the probability of word given context word :

The heatmap reveals the sparse structure of bigram probabilities. Most cells are empty (zero probability), indicating word pairs that never occurred in our corpus. The non-zero cells cluster around specific patterns: "the" can be followed by several words, but "sat" is always followed by "on." This sparsity becomes even more pronounced with larger vocabularies and higher-order n-grams. With a real vocabulary of 50,000 words, the matrix would be 50,000 by 50,000 with the overwhelming majority of cells set to zero.