Siksha Sarovar

Siksha Sarovar (sikshasarovar.com) is a free educational web application that helps students in India learn programming and prepare for academic and competitive exams. The platform offers structured coding courses (C, C++, Python, Java, HTML, CSS, PHP, Power BI, AI, Machine Learning, Data Science), complete university curriculum notes for BCA/MCA students with previous year question papers, Class 10 and Class 12 CBSE/HBSE school notes, and dedicated preparation material for SSC, UPSC, Banking, Railway and other government exams. Browsing the site is completely free and requires no account. Users may optionally sign in with Google solely to save their learning progress, quiz scores and personal preferences across devices.

Privacy Policy | Terms of Service | Contact Siksha Sarovar | About Siksha Sarovar

v4.0.9 · PWA
Siksha Sarovar logo
Siksha Sarovar
Your Learning Universe

Siksha Sarovar is a free e-learning platform for coding courses, BCA university notes and competitive exam preparation. Optional Google sign-in saves your learning progress across devices.

Initializing knowledge base…
Compiling modules 0%

Unit 2 — Smoothing Techniques for N-gram Models

Lesson 22 of 39 in the free Natural Language Processing notes on Siksha Sarovar, written by Rohit Jangra.

Smoothing Techniques for N-gram Models

As shown in the previous lesson, raw Maximum Likelihood Estimation (MLE) assigns zero probability to any n-gram unseen during training — which then zeroes out the probability of the entire sentence. Smoothing techniques reserve some probability mass for unseen events.

1. Laplace (Add-One) Smoothing

The simplest fix: pretend every possible n-gram was seen one extra time.

P_Laplace(w_i | w_{i-1}) = (Count(w_{i-1}, w_i) + 1) / (Count(w_{i-1}) + V)

where V is the size of the vocabulary (so probabilities still sum to 1).

from collections import Counter
from nltk import word_tokenize, bigrams

corpus = "the cat sat on the mat the cat ate the fish"
tokens = word_tokenize(corpus)
vocab = set(tokens)
V = len(vocab)

unigram_counts = Counter(tokens)
bigram_counts = Counter(bigrams(tokens))

def laplace_bigram_prob(w1, w2):
    return (bigram_counts[(w1, w2)] + 1) / (unigram_counts[w1] + V)

print(laplace_bigram_prob("the", "cat"))    # (2+1)/(4+7) = 0.27  (was 0.5 before)
print(laplace_bigram_prob("fish", "sat"))   # (0+1)/(1+7) = 0.125 (was 0.0 before!)
  • Pros: extremely simple, guarantees no zero probabilities.
  • Cons: takes too much probability mass away from seen events when the vocabulary is large — tends to over-smooth.

2. Add-k Smoothing (Generalized Laplace)

Instead of adding exactly 1, add a smaller fractional constant k (0 < k < 1) to reduce over-smoothing.

def add_k_bigram_prob(w1, w2, k=0.1):
    return (bigram_counts[(w1, w2)] + k) / (unigram_counts[w1] + k * V)

print(add_k_bigram_prob("the", "cat", k=0.1))

3. Good-Turing Smoothing

Re-estimates the probability of unseen events using the count of events that occurred exactly once ("singletons"). The core idea: the total probability mass reserved for unseen n-grams is estimated from how many n-grams were seen only once — if many bigrams appeared exactly once, it suggests many more unseen bigrams are still "out there" waiting to occur.

P_GT(unseen) ≈ N1 / N        where N1 = number of n-grams seen exactly once,
                                    N  = total n-gram count

4. Backoff and Interpolation

Instead of purely guessing for unseen n-grams, back off to a lower-order model when the higher-order n-gram is unseen.

Katz Backoff:
  If trigram count > 0: use the trigram probability
  Else if bigram count > 0: back off to the bigram probability (discounted)
  Else: back off further to the unigram probability

Interpolation goes further and always blends all orders together using learned weights:

P_interp(w_i | w_{i-2}, w_{i-1}) =
      λ1 · P(w_i)                          -- unigram
    + λ2 · P(w_i | w_{i-1})                -- bigram
    + λ3 · P(w_i | w_{i-2}, w_{i-1})       -- trigram
    where λ1 + λ2 + λ3 = 1
# Simple linear interpolation example
def interpolated_prob(w1, w2, l1=0.2, l2=0.8):
    unigram_p = unigram_counts[w2] / sum(unigram_counts.values())
    bigram_p = bigram_counts[(w1, w2)] / unigram_counts[w1] if unigram_counts[w1] else 0
    return l1 * unigram_p + l2 * bigram_p

print(interpolated_prob("the", "fish"))

5. Kneser-Ney Smoothing (Industry Standard for N-grams)

The most sophisticated classical smoothing method, and the standard baseline before neural LMs. It improves on simple backoff by considering not just how often a word occurs, but how many distinct contexts it appears in (its "versatility") — e.g. "Francisco" is common but almost always follows "San", so it should get a low backed-off unigram probability despite decent raw frequency.

Summary — Choosing a Smoothing Method

MethodComplexityTypical Use
Laplace (add-1)Very simpleTeaching/toy examples only
Add-kSimpleSlightly better toy/small-corpus baseline
Good-TuringModerateClassical speech recognition systems
Backoff / InterpolationModerate-HighPractical statistical LMs
Kneser-NeyHighProduction-grade statistical LMs (pre-neural era)

Smoothing was the dominant technique for handling data sparsity before neural language models. Modern approaches (Word2Vec embeddings, Transformers, LLMs — Unit 4) sidestep this problem almost entirely by representing words as dense vectors that generalize naturally to unseen combinations.