HardNLP

Bigram Probabilities (Add-1 Smoothing)

NLP

Hard

Problem

Build a bigram language model with add-one smoothing. Sort the unique tokens to obtain vocabulary vocab. Row i represents context token vocab[i], and column j represents next token vocab[j].

P(w_j\mid w_i)=\frac{C_{ij}+1}{\sum_{u=1}^{V}C_{iu}+V}

Here, C_{ij} is the number of adjacent occurrences of w_i followed by w_j, and V is vocabulary size. Return vocab as a list, counts as an integer NumPy matrix, and probabilities as a floating-point NumPy matrix in a dictionary.

Theory

A bigram is a sequence of two consecutive elements. In NLP, bigrams are pairs of consecutive words (or characters):

Word bigrams in "I love machine learning":

Character bigrams in "hello":

Bigrams capture local context: what word or character tends to follow another.


The Bigram Language Model

A bigram language model estimates the probability of a word given only the previous word:

P(w_n | w_1, w_2, ..., w_{n-1}) \approx P(w_n | w_{n-1})

This is the Markov assumption: the next word depends only on the current word, not the entire history.

The probability of a full sentence is the product of bigram probabilities:

P(w_1, w_2, ..., w_n) = P(w_1) \times P(w_2|w_1) \times P(w_3|w_2) \times ... \times P(w_n|w_{n-1})


Computing Bigram Probabilities

Bigram probabilities are estimated from a corpus using maximum likelihood estimation:

P(w_2 | w_1) = \frac{\text{Count}(w_1, w_2)}{\text{Count}(w_1)}

This is the number of times the bigram (w_1, w_2) appears, divided by the number of times w_1 appears.


A Worked Example

Corpus: "the cat sat on the mat the cat slept"

Step 1: Count all bigrams

Step 2: Count all unigrams (single words)

Step 3: Compute bigram probabilities

P(\text{cat} | \text{the}) = \frac{\text{Count(the, cat)}}{\text{Count(the)}} = \frac{2}{3} \approx 0.667

P(\text{mat} | \text{the}) = \frac{\text{Count(the, mat)}}{\text{Count(the)}} = \frac{1}{3} \approx 0.333

P(\text{sat} | \text{cat}) = \frac{\text{Count(cat, sat)}}{\text{Count(cat)}} = \frac{1}{2} = 0.5

P(\text{slept} | \text{cat}) = \frac{\text{Count(cat, slept)}}{\text{Count(cat)}} = \frac{1}{2} = 0.5


Start and End Tokens

Real text has beginnings and endings. We use special tokens:

Sentence: "the cat sat"

With special tokens: " the cat sat "

Bigrams:

This lets the model learn:


The Zero Probability Problem

What if we encounter a bigram that never appeared in training?

Training corpus: "the cat sat"

New sentence: "the dog sat"

The bigram (the, dog) has count 0, so:

P(\text{dog} | \text{the}) = \frac{0}{\text{Count(the)}} = 0

This is catastrophic. The probability of the entire sentence becomes 0, even though "the dog sat" is perfectly valid English.


Smoothing Techniques

Laplace (Add-One) Smoothing:

Add 1 to all bigram counts:

P(w_2 | w_1) = \frac{\text{Count}(w_1, w_2) + 1}{\text{Count}(w_1) + V}

where V is the vocabulary size.

This ensures no probability is ever exactly zero.

Add-k Smoothing:

Add a small constant k (e.g., 0.01) instead of 1:

P(w_2 | w_1) = \frac{\text{Count}(w_1, w_2) + k}{\text{Count}(w_1) + k \times V}

Backoff and Interpolation:

If a bigram is unseen, "back off" to the unigram probability, or interpolate between bigram and unigram estimates.


Building the Probability Matrix

For a vocabulary of size V, bigram probabilities form a V \times V matrix where:

Each row sums to 1 (it is a probability distribution over possible next words).


Using Bigram Probabilities

Text generation:

  1. Start with
  2. Sample next word from P(w | \text{<s>})
  3. Sample next word from P(w | \text{previous word})
  4. Repeat until is sampled

Sentence scoring:

Perplexity:


Limitations of Bigrams

Limited context: Bigrams only see one word of history. They cannot capture:

Data sparsity: Even with smoothing, rare word combinations are poorly estimated.

No semantic understanding: Bigrams are purely statistical. They do not understand meaning.

Despite these limitations, bigram models are:

Examples

Example 1

Input
tokens = ["a", "b", "a"]
Output
{"vocab": ["a", "b"], "counts": [[0, 1], [1, 0]], "probabilities": [[0.333333, 0.666667], [0.666667, 0.333333]]}
Explanation
The observed transitions are a to b and b to a; add-one smoothing also assigns probability to unseen pairs.

Example 2

Input
tokens = ["i", "love", "ml", "love", "ml"]
Output
{"vocab": ["i", "love", "ml"], "counts": [[0, 1, 0], [0, 0, 2], [0, 1, 0]], "probabilities": [[0.25, 0.5, 0.25], [0.2, 0.2, 0.6], [0.25, 0.5, 0.25]]}

Hints

  1. Create an index dictionary from sorted(set(tokens)).
  2. Increment counts[index[first], index[second]] for adjacent pairs.
  3. Divide counts + 1 by (counts.sum(axis=1, keepdims=True) + vocab_size).

Requirements

Constraints

Starter Code

import numpy as np

def bigram_probabilities(tokens: list) -> dict:
    """
    Returns a dictionary with vocab, counts, and probabilities.
    """
    # Write code here
    pass

Test Cases

CaseMatches
Tiny sequencepublic
Repeated patternspublic