Language Models by Counting

Computational Linguistics

Course 1 · Chapter 5 · Language Models by Counting

Phones suggest the next word, and speech recognisers choose between "recognise speech" and "wreck a nice beach". Both rely on a language model: a way of saying how likely a word is given the words before it. The oldest and simplest kind is built from nothing but counting.

The idea in a box
A language model is a machine for saying how surprised it is by a piece of text. A counting model learns that surprise from a corpus (Chapter 3), and it fails in an instructive way on anything it has not seen.

What a Language Model Is

Wikipedia's Language model article defines it as a computational model that predicts sequences in natural language, with uses including speech recognition, machine translation and natural language generation. Its history section runs: in the 1950s Noam Chomsky developed formal grammar-based approaches; in the 1970s Frederick Jelinek and colleagues at IBM developed statistical models using probabilistic n-grams; in the 2000s Yoshua Bengio and co-authors introduced a neural probabilistic language model; in 2010 Tomáš Mikolov and colleagues showed recurrent neural network models could substantially outperform n-gram models; and large language models, based mainly on transformers, are the most advanced form as of 2026. This chapter stays with the statistical stage; NLP 6 covers the neural sequence models.

n-grams

The N-gram article defines an n-gram as "a sequence of n adjacent symbols in a particular order"; the symbols can be letters, syllables, words, phonemes or DNA base pairs. Size 1 is a unigram, 2 a bigram, 3 a trigram. It says n-grams let bag-of-words models capture word order, and cites Shannon's 1951 examples and Google's n-gram corpus. The Language model article describes an n-gram model as one that "calculates the probability of the next word in a sequence from a fixed size window of previous words": a bigram model uses one previous word, a trigram two, an n-gram model n − 1.

Estimating by counting is my explanation. Neither page, in the parts I read, explains how the probabilities are computed from counts. The usual idea, which I demonstrate below, is: P(next word | previous word) = (how often the pair occurred) ÷ (how often the previous word occurred).

A bigram model of 66 words

The text is Chapter 3's invented lighthouse paragraph. The code and outputs are mine, run with py -X utf8.

import re, math from collections import Counter text = ("The old lighthouse stood on the cliff. The keeper climbed the stairs every night " "and lit the lamp. Strong tea warmed the keeper after the climb. " "On stormy nights the keeper drank strong tea and watched the sea. " "The sea was grey. The lamp was bright. The keeper made a decision: " "he would stay another winter. Strong winds shook the lighthouse, " "but the lamp never failed.") tokens = re.findall(r"[a-z]+", text.lower()) uni = Counter(tokens) bi = Counter(zip(tokens, tokens[1:])) V = len(uni) print(len(tokens), V) def p_bigram(w1, w2): return bi[(w1, w2)] / uni[w1] after_the = {w2: bi[("the", w2)] for (w1, w2) in bi if w1 == "the"} print(uni["the"], sorted(after_the.items(), key=lambda kv: -kv[1])) print(round(p_bigram("the", "keeper"), 3), round(p_bigram("the", "sea"), 3), p_bigram("the", "dog"))
66 40 14 [('keeper', 4), ('lamp', 3), ('sea', 2), ('old', 1), ('cliff', 1), ('stairs', 1), ('climb', 1), ('lighthouse', 1)] 0.286 0.143 0.0

The occurs 14 times. Four of those are followed by keeper, so the model says P(keeper | the) = 4/14 ≈ 0.286; sea follows twice, 0.143; and dog, which never follows the in this text, gets 0.0.

The Zero Problem

A sentence's probability, in this model, is the product of its bigram probabilities. Try a perfectly sensible sentence the model has not seen:

test = "the keeper watched the sea".split() p = 1.0 for w1, w2 in zip(test, test[1:]): print(w1, w2, bi[(w1, w2)], round(p_bigram(w1, w2), 3)) p *= p_bigram(w1, w2) print(p)
the keeper 4 0.286 keeper watched 0 0.0 watched the 1 1.0 the sea 2 0.143 0.0

The pair keeper watched never occurs in the text (the keeper drank and made and climbed), so the whole sentence gets probability 0.0, as if it were impossible. That is the data sparsity problem. The Language model article treats it as a core problem: unseen word sequences would otherwise get zero probability, so seen probabilities are slightly lowered to make room for unseen ones; the methods it names are add-one smoothing, Good–Turing discounting and back-off models. It says skip-gram models were an attempt at "overcoming the data sparsity problem" of word n-gram models, and that neural embeddings are said to help alleviate the curse of dimensionality, which causes data sparsity as vocabulary size grows. The N-gram page, in the part I read, does not cover sparsity, smoothing or the limits of n-gram models.

Smoothing

The Additive smoothing article calls it "a technique used to smooth count data, eliminating issues caused by certain values having 0 occurrences", also called Laplace or Lidstone smoothing. For counts xi out of N trials across d categories the smoothed estimate is (xi + α) / (N + αd), where α > 0 is the pseudocount; α = 1 is add-one smoothing, and common values are 0, ½ and 1. The estimate lies between the observed frequency and the uniform probability 1/d, so it is a shrinkage estimator. The article says treating an unseen event's observed zero as a probability of zero is "an oversimplification". It reports studies finding it more effective than some other smoothing methods for tasks such as language-model-based pseudo-relevance feedback and recommender systems, and notes it is a common component of naive Bayes classifiers. Its limits, which it implies: the choice of α is partly subjective, and larger pseudocounts pull estimates toward uniform.

Applied to the bigram model, with V = 40 word types as the vocabulary size (my mapping of the article's formula: N = count of the first word, d = V):

def p_add1(w1, w2): return (bi[(w1, w2)] + 1) / (uni[w1] + V) p = 1.0 for w1, w2 in zip(test, test[1:]): print(w1, w2, round(p_add1(w1, w2), 4)) p *= p_add1(w1, w2) print(round(p, 8)) print(round(sum(p_add1("the", w) for w in uni), 3))
the keeper 0.0926 keeper watched 0.0227 watched the 0.0488 the sea 0.0556 5.7e-06 1.0

The impossible pair now has probability 0.0227, so the sentence has a small but nonzero probability (5.7 × 10-6). The last line is a check: the smoothed probabilities of every possible next word after the still add up to 1.0. The price is that every seen pair lost some probability: P(keeper | the) fell from 0.286 to 0.0926. With a 40-word vocabulary and only 66 words of text, add-one smoothing takes a large bite.

Perplexity

The Perplexity article defines it as "a measurement of how well a probability distribution or probability model predicts a sample": low perplexity means it predicts the sample well. It is "the exponentiation of the entropy", and a random variable with perplexity k has the same uncertainty as a fair k-sided die. For a language model, a model q is judged on a separate test sample, and better models "will tend to assign higher probabilities" to it, hence lower perplexity; for language the article uses perplexity per word. Its word-level example is a model "as confused on test data as if it had to choose uniformly and independently among 247 possibilities for each word." It cautions that perplexity "is sometimes used as a measure of the difficulty of a prediction problem," but "it's not always an accurate representation". The Language model article's own evaluation section does not cover perplexity; it appears there only in the history section.

n = len(test) - 1 # four predictions print(round(p ** (-1 / n), 1))
20.5

Perplexity 20.5: on this sentence the smoothed model is about as unsure as if it were choosing among roughly 20 equally likely words each time (the vocabulary has 40). I normalise by the four predictions, not five words, because a bigram model does not predict the first word; that is my choice, and other conventions exist.

Watch out: perplexity needs held-out text, and its numbers move
My "test" sentence is made from the same vocabulary as the training text, and the training text is 66 words. Perplexity here is only a demonstration. The article defines it on a separate test sample, and a toy model scored on a toy sentence cannot be compared with any other system's perplexity.
Note on sources
This chapter uses Wikipedia's N-gram, Additive smoothing and Perplexity articles (read in full) and Language model (read to 100,000 of 132,843 characters; the fetch reported the visible text ending partway through navigation). The N-gram page, in the part I read, does not explain counting, sparsity, smoothing or limits; the Language model page does not name the Markov assumption, give a general formula for the probability of a sequence, or discuss count-based estimation or back-off in detail. The "next word equals pair count over word count" description, the vocabulary-size mapping of the smoothing formula, the code, the 66-word text, the outputs and the choice to divide by four are mine. NLP 6 is cited from its chapter banner ("Sequence Models for Text"), not re-read.

Try It Yourself

Exercise 1

Using uni and bi, find P(tea | strong) and P(winds | strong). Then ask: what would the model predict after strong if the text had only the single sentence "Strong tea warmed the keeper"?

📄 View solution
Exercise 2

Recompute the test sentence's probability and perplexity with α = 1, 0.5, 0.1 and 0.01 in (count + α) / (uni + α·V). Which α gives the lowest perplexity, and why is picking it using this same test sentence a problem?

📄 View solution
Exercise 3

Starting from the, always choose the most frequent next word (ties go to whichever came first in the counts) for ten words. What does the output look like, and what does that say about using a bigram model to generate text?

📄 View solution

Chapter 5 Quick Reference

  • n-gram: n adjacent symbols; a bigram model predicts from one previous word
  • Counting estimate (my explanation): pair count ÷ previous-word count
  • Unseen sequences get zero, so smoothing: (count + α) / (N + αd); add-one is α = 1
  • Perplexity: how well a model predicts a held-out sample; lower is better; like choosing among k equal options
Coming next
Computational Linguistics 6 moves from words in a row to structure: grammars and parse trees.