Skip to content

MLE, Laplace, absolute discounting

ModuleM07.2 · build · Python · Pass 3 · 2 to 3 h
You buildpython/tinyllm/prob/mle.py: mle, log_likelihood, laplace, absolute_discount, ney_discount
Contractcourse/contracts/py/tinyllm/prob/mle.pyi
Testscourse/tests/M07.2/ (what they check: section 4)
Needsno code from earlier modules · reading: M00.1 logs and nats, S-M05 counting
Used byL1.4 Unigram EM re-estimates piece probabilities · later: L2.1 interpolated Kneser-Ney, M11.4 PMI from co-occurrence counts
MilestoneMS-P3 (tokens and data)
Optional depthJurafsky and Martin, Speech and Language Processing (3rd ed. draft), ch. 3 “N-gram Language Models”; Chen and Goodman, “An Empirical Study of Smoothing Techniques for Language Modeling” (1998)
  • The maximum-likelihood estimate of a categorical distribution is the observed frequency ck/Nc_k / N; no other distribution gives the data a higher log-likelihood (test_hand_example_mle, test_mle_maximizes_the_likelihood).
  • The MLE gives every unseen outcome probability 0, so a single unseen word in new text makes its likelihood 0 and its perplexity infinite (test_log_likelihood_zero_terms_and_impossible_data).
  • Add-alpha smoothing divides by N+αVN + \alpha V, one pseudo-count per word of the whole vocabulary, seen or not (test_hand_example_laplace, test_laplace_full_distribution_sums_to_one).
  • Absolute discounting takes a fixed dd from every seen count and gives exactly that mass, dT/NdT/N, to a backoff distribution: the core of Kneser-Ney (test_hand_example_absolute_discount, test_absolute_discount_sums_to_one).
Terminal window
ol start M07.2 # stubs mle.py into your repo
ol tests M07.2 # read the test catalog first
ol check M07.2 # exit code is the verdict
ol diff M07.2 # after passing: your code against the reference

Your tracer bigram (L0.0) already smooths: it adds one to every count, because a byte pair never seen in training would otherwise get probability 0, the sampler could never produce it, and its NLL on new text would be infinite. That was one formula in one place. Pass 3 estimates distributions from counts all over the system: the Unigram tokenizer (L1.4) re-estimates its piece probabilities from expected counts on every EM step, the n-gram model (L2.1) estimates the next word for millions of contexts most of which were seen once or never, and PMI (M11.4) divides co-occurrence counts. Each needs the same three decisions made correctly: what the best estimate from counts is, how much probability to keep for what was not seen, and where that probability goes. This module makes those decisions once, as functions with exact answers.

SymbolMeaningType / shape
VVthe vocabulary: every outcome the model can assign probability to; ∣V∣\lvert V \rvert its sizeint
ckc_khow many times outcome kk was observed: an integer from counting, or a fraction when it is an expected count from EMint or float
N=∑kckN = \sum_k c_kthe number of observationsint
qqany distribution over VV: qk≥0q_k \ge 0, ∑kqk=1\sum_k q_k = 1dict[K, float]
ℓ(q)=∑kckln⁡qk\ell(q) = \sum_k c_k \ln q_kthe log-likelihood of the counts under qq, in natsfloat
p^k\hat p_kan estimate of the true probability of kkfloat
α>0\alpha > 0the pseudo-count of add-alpha smoothingfloat
d∈(0,1]d \in (0, 1]the absolute discountfloat
T=∣{k:ck>0}∣T = \lvert \{k : c_k > 0\} \rvertthe number of distinct outcomes seenint
bkb_ka backoff distribution over VV (a simpler model)dict[K, float]
nrn_rthe number of outcomes seen exactly rr times (count of counts)int

Likelihood. If the observations are independent draws from qq, the probability of the data is ∏kqkck\prod_k q_k^{c_k} (each observation of kk contributes a factor qkq_k; the order does not matter for estimating qq). Its log is ℓ(q)=∑kckln⁡qk\ell(q) = \sum_k c_k \ln q_k. Terms with ck=0c_k = 0 contribute nothing, whatever qkq_k is, by the convention 0⋅ln⁡0=00 \cdot \ln 0 = 0. A term with ck>0c_k > 0 and qk=0q_k = 0 is −∞-\infty: the data is impossible under qq.

The MLE is the frequency. The maximum-likelihood estimate is the qq that maximizes ℓ(q)\ell(q). It is p^k=ck/N\hat p_k = c_k / N, and here is why. For any qq,

ℓ(q)−ℓ(p^)=∑kckln⁡qkp^k=N∑kp^kln⁡qkp^k=−N KL(p^ ∥ q)≤0,\ell(q) - \ell(\hat p) = \sum_k c_k \ln \frac{q_k}{\hat p_k} = N \sum_k \hat p_k \ln \frac{q_k}{\hat p_k} = -N \, \mathrm{KL}(\hat p \,\Vert\, q) \le 0,

because the Kullback-Leibler divergence is never negative (Gibbs’ inequality, M11.1), and it is 0 only when q=p^q = \hat p. So the frequency wins against every other distribution, which is what test_mle_maximizes_the_likelihood checks numerically. It needs N>0N > 0: with no data, 0/00/0 is not an estimate.

The zero problem. The MLE puts all its mass on what was seen. A word with ck=0c_k = 0 gets p^k=0\hat p_k = 0, and new text containing it gets likelihood 0 and perplexity ∞\infty (M11.2). In language most words are rare, so most of the vocabulary has ck=0c_k = 0 in any given context: this is the normal case, not an edge case.

Add-alpha (Laplace) smoothing. Pretend every word of the vocabulary was seen α\alpha extra times:

p^k=ck+αN+α∣V∣.\hat p_k = \frac{c_k + \alpha}{N + \alpha \lvert V \rvert}.

The denominator adds α\alpha once per word of VV, including the words that are not keys of counts at all; summing the numerator over all of VV gives N+α∣V∣N + \alpha \lvert V \rvert, so the estimate is a distribution. Every word gets at least α/(N+α∣V∣)>0\alpha / (N + \alpha \lvert V \rvert) > 0. α→0\alpha \to 0 recovers the MLE and α→∞\alpha \to \infty the uniform 1/∣V∣1/\lvert V \rvert; with no data it is exactly uniform. (Bayesian reading: this is the posterior mean under a symmetric Dirichlet(α\alpha) prior; α=1\alpha = 1 is Laplace’s rule of succession.) Its weakness is that it takes mass in proportion to α∣V∣\alpha \lvert V \rvert, which for a 50 000-word vocabulary and a context seen 10 times hands almost everything to unseen words.

Absolute discounting. Church and Gale (1991) counted bigrams in one half of a corpus and looked them up in the other: a bigram seen c≥2c \ge 2 times in the first half appeared on average about c−0.75c - 0.75 times in the second. Seen counts overstate the future by a roughly constant amount. So subtract a constant dd from every seen count and give the freed mass to a backoff distribution bb (in L2.1, the next lower-order model):

p^k=max⁡(ck−d,0)N+λ bk,λ=1N∑kmin⁡(ck,d).\hat p_k = \frac{\max(c_k - d, 0)}{N} + \lambda \, b_k, \qquad \lambda = \frac{1}{N} \sum_k \min(c_k, d).

Each word gives up min⁡(ck,d)\min(c_k, d): dd if it has that much, its whole count if not. So λ\lambda is precisely the freed fraction and ∑kp^k=(N−Nλ)/N+λ∑kbk=1\sum_k \hat p_k = (N - N\lambda)/N + \lambda \sum_k b_k = 1. For integer counts with d≤1d \le 1 every seen word has at least dd and every unseen word has nothing, so λ=dT/N\lambda = dT/N, the form in most textbooks. The max⁡\max matters for keys whose count is 0, and the min⁡\min for the fractional expected counts of EM (L1.4): a word expected 0.3 times cannot lose 0.5. A seen word keeps (ck−d)/N(c_k - d)/N plus its share of the backoff; an unseen one gets only λbk\lambda b_k. With N=0N = 0 there is nothing to discount and the answer is bb itself.

Estimating dd. Ney, Essen and Kneser (1994) derived D=n1/(n1+2n2)D = n_1 / (n_1 + 2 n_2) from leaving one observation out at a time, where n1n_1 and n2n_2 count the words seen once and twice. It lies in (0,1](0, 1] when n1>0n_1 > 0. Modified Kneser-Ney (L2.1) uses three such discounts, for counts 1, 2, and 3 or more.

Counts: a = 3, b = 2, c = 1, d = 0, over the vocabulary a b c d e (∣V∣=5\lvert V \rvert = 5; e is not even a key). N=6N = 6, T=3T = 3.

MLE. p^=(3/6,2/6,1/6,0)=(0.5,0.3333,0.1667,0)\hat p = (3/6, 2/6, 1/6, 0) = (0.5, 0.3333, 0.1667, 0) for a b c d, and e would be 0 too.

Log-likelihood. ℓ(p^)=3ln⁡12+2ln⁡13+ln⁡16=−2.0794−2.1972−1.7918=−6.0684\ell(\hat p) = 3 \ln \tfrac12 + 2 \ln \tfrac13 + \ln \tfrac16 = -2.0794 - 2.1972 - 1.7918 = -6.0684 nats. The d term is 0⋅ln⁡0=00 \cdot \ln 0 = 0.

Laplace, α=1\alpha = 1. Denominator 6+1⋅5=116 + 1 \cdot 5 = 11:

wordabcde (not a key)
ck+1c_k + 143211
p^k\hat p_k4/113/112/111/111/11

The five sum to 11/1111/11. Dividing by 6+1⋅46 + 1 \cdot 4 (only the keys) would give 4/10+3/10+2/10+1/10=14/10 + 3/10 + 2/10 + 1/10 = 1 over the keys and leave 1/101/10 for e on top: a total of 1.11.1. Its log-likelihood is 3ln⁡411+2ln⁡311+ln⁡211=−7.33823 \ln \tfrac4{11} + 2 \ln \tfrac3{11} + \ln \tfrac2{11} = -7.3382, lower than the MLE’s, as it must be.

Absolute discounting, d=1/2d = 1/2, uniform backoff bk=1/5b_k = 1/5. The freed mass is λ=dT/N=0.5⋅3/6=1/4\lambda = d T / N = 0.5 \cdot 3 / 6 = 1/4, so each word gets λbk=1/20\lambda b_k = 1/20 from the backoff:

wordmax⁡(ck−d,0)/N\max(c_k - d, 0)/N+λbk+ \lambda b_kp^k\hat p_k
a2.5/6=5/122.5/6 = 5/121/201/207/15=0.46677/15 = 0.4667
b1.5/6=1/41.5/6 = 1/41/201/203/103/10
c0.5/6=1/120.5/6 = 1/121/201/202/15=0.13332/15 = 0.1333
dmax⁡(−0.5,0)/6=0\max(-0.5, 0)/6 = 01/201/201/201/20
e001/201/201/201/20

Sum: 28/60+18/60+8/60+3/60+3/60=128/60 + 18/60 + 8/60 + 3/60 + 3/60 = 1. Without the clamp, d would get −1/12+1/20<0-1/12 + 1/20 < 0.

Ney’s discount. One word seen once (c), one seen twice (b): D=1/(1+2⋅1)=1/3D = 1/(1 + 2 \cdot 1) = 1/3.

These numbers are the first cases in section 4: test_hand_example_mle, test_hand_example_laplace, test_hand_example_absolute_discount, test_hand_example_ney_discount, and test_hand_example_log_likelihood.

python/tinyllm/prob/mle.py
def mle(counts: Mapping[K, int]) -> dict[K, float] # c_k / N
def log_likelihood(counts: Mapping[K, int], probs: Mapping[K, float]) -> float # nats; -inf if impossible
def laplace(counts: Mapping[K, int], vocab_size: int, alpha: float = 1.0) -> dict[K, float]
def absolute_discount(counts: Mapping[K, int], d: float, backoff: Mapping[K, float]) -> dict[K, float]
def ney_discount(counts: Mapping[K, int]) -> float # n1 / (n1 + 2 n2)

Counts are finite non-negative numbers: integers from counting (numpy integers are fine) or floats from EM; NaN, infinity, bools, and strings are a ValueError. laplace returns the keys of counts only; the contract states the probability of every other word. absolute_discount returns every key of backoff.

TestKINDChecksWhy it matters downstream
test_hand_example_mleunit1/2, 1/3, 1/6, 0you and the test agree on the definition
test_hand_example_laplaceunit4/11, 3/11, 2/11, 1/11, and 1/11 for ethe denominator counts the whole vocabulary
test_hand_example_absolute_discountunit7/15, 3/10, 2/15, 1/20, 1/20the formula L2.1 builds on
test_hand_example_ney_discountunitD=1/3D = 1/3the starting discount of L2.1
test_hand_example_log_likelihoodunit−6.0684-6.0684 for the MLE, −7.3382-7.3382 for Laplacesmoothing costs training likelihood
test_mle_maximizes_the_likelihoodpropertyrandom competitors and small perturbations score lowerthe defining property
test_mle_sums_to_one_and_zero_counts_get_zeropropertya distribution; zeros stay 0no NaN, no missing key
test_laplace_full_distribution_sums_to_onepropertykeys plus the unseen words sum to 1 for three α\alphathe whole vocabulary is normalized
test_laplace_limitsunittiny α\alpha is the MLE, huge α\alpha and no data are uniformα\alpha interpolates
test_absolute_discount_sums_to_onepropertyrandom counts with zeros, random backoffs, four ddλ=dT/N\lambda = dT/N exactly
test_absolute_discount_zero_counts_are_clampedboundarya zero-count key gets only λbk\lambda b_kno negative probability
test_absolute_discount_with_no_data_is_the_backoffboundaryN=0N = 0 returns a copy of bban unseen context in L2.1
test_absolute_discount_d_one_removes_singletonsboundaryd=1d = 1the edge of the allowed range
test_rejects_bad_argumentsboundarynegative, NaN, infinite, bool, or string counts; no data; bad α\alpha, dd, VV, or backoffupstream counting bugs fail here
test_fractional_counts_from_emunitthe MLE of expected counts; λ=∑kmin⁡(ck,d)/N\lambda = \sum_k \min(c_k, d)/N when a count is below ddEM in L1.4 produces fractions
test_ney_discount_casesunitall singletons give 1; no singletons raiseD∈(0,1]D \in (0, 1]
test_log_likelihood_zero_terms_and_impossible_databoundary0ln⁡0=00 \ln 0 = 0; an observed outcome with q=0q = 0 gives −∞-\inftyinfinite perplexity in M11.2
PitfallSymptomCaught by
1. dividing by the number of distinct words instead of NN“probabilities” that sum to N/∣keys∣N / \lvert \mathrm{keys} \rverttest_hand_example_mle (mutant s01)
2. Laplace denominator N+α⋅N + \alpha \cdot (number of keys)the keys alone sum to 1, so the unseen words’ mass is extra: the total is above 1test_laplace_full_distribution_sums_to_one (mutant s02)
3. counting TT over every key, zero counts includedmore mass handed to the backoff than was freedtest_hand_example_absolute_discount (mutant s04)
4. no clamp at 0a key counted zero times gets a negative probabilitytest_absolute_discount_zero_counts_are_clamped (mutant s06)
5. scoring zero-count terms, or indexing a missing probability0ln⁡00 \ln 0 turns into −∞-\infty or a KeyErrortest_log_likelihood_zero_terms_and_impossible_data (mutants s09, s10)
6. λ=dT/N\lambda = dT/N with fractional countsa count of 0.3 is charged 0.5, so the result sums to more than 1test_fractional_counts_from_em (mutant s05)
7. returning the caller’s backoff dict when N=0N = 0L2.1 edits one context’s distribution and corrupts the shared lower ordertest_absolute_discount_with_no_data_is_the_backoff (mutant s07)
8. Ney’s D=n1/(n1+n2)D = n_1 / (n_1 + n_2)a discount that is too largetest_hand_example_ney_discount (mutant s08)
9. accepting NaN or infinite counts, or estimating from no dataNaN probabilities, or a ZeroDivisionErrortest_rejects_bad_arguments (mutants s11, s12)
DirectionModuleHow it uses this
BackM00.1natural logs, so likelihoods are in nats
BackS-M05counting outcomes and the size of a vocabulary
ForwardL1.4seeds the piece probabilities with mle of substring counts; each EM step is the same estimate over expected counts
ForwardL2.1interpolated Kneser-Ney is absolute_discount per context with ney_discount-style D1,D2,D3+D_1, D_2, D_{3+} and a continuation-count backoff
ForwardM11.4PMI divides joint and marginal MLEs from co-occurrence counts
ForwardM11.2−ℓ(q)/N-\ell(q)/N is the NLL per token whose exponential is perplexity

If you skip this module, ol check L1.4 stops with L1.4 needs M07.2: build it, or rerun with --ref-deps.

Your pieceProduction equivalentWhat it addsWhere to look
absolute_discount, ney_discountKenLMmodified Kneser-Ney with three discounts per order estimated from count-of-counts, on disk-backed sorted n-gram streamslm/builder/adjust_counts.cc (discounts from the count of counts)
laplacescikit-learn MultinomialNBthe same add-alpha estimate per class, as its alpha parametersklearn/naive_bayes.py
mle over expected countsSentencePiece Unigram trainerEM where the M-step is this MLE plus a digamma correction (a Bayesian variant) and pruningsrc/unigram_model_trainer.cc