EVERYTHING AIAI engineering, made visual
0/18 complete
LESSON 14 · MACHINE LEARNING × AI · BUILD

Count the words.
Flip the odds.

P(spam | words) ∝ P(spam) · Π P(word | spam) is the whole classifier: one honestly wrong assumption (words arrive independently), a table you fill by counting, and a verdict read off the bigger log score.

75 MIN · 7 CHAPTERSPREREQ · PHASE 2, LESSONS 01–07
FIG. 14 / ONE MESSAGE, WORD BY WORD
P(SPAM | “FREE MONEY FREE”) = 0.400 0.997 spam score not-spam score live verdict
LESSON 14TYPE · BUILD~75 MINPREREQ · PHASE 2 · LESSONS 01–07ORIGINAL LESSON ↗
THE 60-SECOND VERSIONThen show me the engine ↓
01 / POSTERIOR ∝ PRIOR × LIKELIHOOD

Start with a belief; update it with evidence.

The corpus says 40% of emails are spam, so that is the prior. Seeing “free” is 81/153 likely under spam but only 6/118 likely under not-spam, and the posterior jumps to 0.874; two more words take it to 0.997. Bayes' rule is the engine — Naive Bayes only decides how to compute the likelihood.

0.40 → 0.874 → 0.997
02 / WRONG ASSUMPTION, RIGHT RANKING

Count each word as if it arrived alone.

A 10,000-word vocabulary has 2^10,000 possible word bundles — no dataset could estimate that joint. Assuming words are independent given the class collapses it to one count per word. Correlated words then get counted twice, which inflates confidence but usually keeps the correct winner.

naive ratio 15 · true ratio 5
03 / COUNTS, BITS OR BELLS

Match the variant to the feature type.

Word frequencies go to Multinomial NB. Short texts where only presence/absence is reliable go to Bernoulli NB — it also penalizes missing words. Continuous measurements go to Gaussian NB, one mean and variance per feature per class. In every variant, training is counting and prediction is a dot product.

counts · bits · measurements
MENTAL MODEL IN ONE SENTENCE

Naive Bayes is Bayes’ rule with a countable likelihood: count how often each word appears per class, add a little smoothing so nothing is impossible, add up each word’s log-likelihood, and pick the class with the larger score — a wrong assumption used to produce a right ranking.

By the end you will be able to derive the smoothed probability formula from the counts, trace a message through priors, likelihoods and logs to a posterior by hand, explain why the independence assumption still ranks well, tell the three variants apart on sight, guard against the unseen-word zero, and know when to reach for logistic regression instead.

BAYES' RULE IS THE ENGINE

Flip the conditional.
Count what fits.

Bayes’ rule turns “how likely are these words in spam?” into “how likely is spam given these words?” Four pieces, one multiplication — and Naive Bayes only has to decide how to count one of them.

We want P(class | features) — the probability that a message belongs to a class once we have read its words. That is hard to measure directly, but the reverse is easy to count: take all the spam in the training set and ask how often each word appears. Bayes’ rule connects the two directions:

P(class | features) = P(features | class) · P(class) / P(features) └─ likelihood ─┘ └prior┘ └evidence┘ posterior what we believe about the class after reading prior how common the class was before reading (40% spam) likelihood how well the words fit that class evidence how common the words are overall — the same for every class, so it cancels out when we compare classes

Plain English: start from what you believed, multiply by how well the evidence fits each story, then compare. Because the evidence term is identical for spam and not-spam, only prior × likelihood matters for picking the winner. The posterior we report at the end is just that product rescaled so the two classes sum to 1.

Worked check: one word moves the belief (exact counts)

The corpus for the whole lesson: spam messages contain free 80 times, money 60, meeting 10 — 150 words total. Not-spam messages contain free 5, money 10, meeting 100 — 115 words total. And 40% of emails are spam. One email arrives containing free once.

prior: P(spam) = 0.40 P(not-spam) = 0.60 likelihood: P(free | spam) = 80/150 = 0.533333 P(free | not-spam) = 5/115 = 0.043478 unnormalized: spam 0.40 × 0.533333 = 0.213333 not-spam 0.60 × 0.043478 = 0.026087 normalize: 0.213333 + 0.026087 = 0.239420 P(spam | free) = 0.213333 / 0.239420 = 0.891 P(not-spam) = 0.026087 / 0.239420 = 0.109 check: 0.891 + 0.109 = 1.000 ✓

One word moved the belief from 0.40 to 0.89, because “free” is about twelve times more common in spam (0.5333 / 0.0435 = 12.27). Two more words and the posterior reaches 0.997 — the arithmetic the lab below performs live with Laplace smoothing switched on.

The spam filter, word by word

Edit the message. Every known word adds its own log-likelihood to each class score; the bigger score wins. Nothing here is trained — the two tables below are the whole model.

P(SPAM | MESSAGE) = 0.9968 log P(spam) = -3.108 log P(ham) = -8.841 log-odds = +5.734 verdict: SPAM confidence: 99.7%

bar = P(ham) blue · P(spam) orange. Priors are 0.4 / 0.6; the smoothing strength is α = 1.

wordcount · spamcount · hamP(w | spam) = (c+1)/(150+3)P(w | ham) = (c+1)/(115+3)
free8050.52940.0508
money60100.39870.0932
meeting101000.07190.8559
term in messagenn · log P(spam)n · log P(ham)log-likelihood ratio per occurrence
free2-1.272-5.958+2.343
money1-0.920-2.373+1.453
log priors-0.916-0.511-0.405
Derivation: dropping the evidence and getting the posterior back

Comparing two classes, the shared denominator P(features) appears in both and can be removed. What survives is a score per class. To turn scores back into probabilities that sum to 1, divide each score by the sum of scores — for two classes that is the familiar logistic shape:

score(class) = exp(log P(class) + Σᵢ countᵢ · log P(wordᵢ | class)) P(spam | features) = score(spam) / (score(spam) + score(not-spam)) = 1 / (1 + exp(−(logscore(spam) − logscore(not-spam)))) number check, α = 1: logscore spam = −3.107832 logscore ham = −8.841465 difference = +5.733633 P(spam) = 1/(1 + e^(−5.733633)) = 0.996775 P(ham) = 1 − 0.996775 = 0.003225 check: 0.996775 + 0.003225 = 1.000000 ✓

The difference of logs is the log-odds: e5.7336 = 309.1, so the odds are about 309 to 1 in favor of spam. Logs turn multiplication into addition, and classification into comparing two sums.

Quick check

A word is 10× more likely in spam than in ham, and the priors are equal. A message contains it once. What is P(spam | message)?

WRONG, YET RIGHT

One impossible joint.
Many countable tables.

Estimating P(all words together | class) needs more data than exists. Assuming the words do not interact makes the likelihood countable — and, surprisingly, keeps the ranking.

The likelihood in Bayes’ rule is a joint event: these exact words appearing together. With a 10,000-word vocabulary there are 210,000 possible word bundles; even the web could not fill that table with reliable counts. The naive assumption fixes this:

P(w₁, w₂, …, wₙ | class) = P(w₁ | class) · P(w₂ | class) · … · P(wₙ | class) "conditional independence": once you know the class, each word's presence tells you nothing new about whether the next word appears. The words still correlate in the real world — that part is the "naive" lie. What it buys: one small table per word per class.

The assumption is obviously false — “machine” and “learning” travel together in every real document. Three things explain why it still classifies well. First, ranking beats calibration: we need the right top class, not a right-to-the-last decimal posterior. Second, high bias is low variance: a strongly constrained model may be wrong about the world but is stable when data is scarce. Third, redundant evidence cancels: correlated words are double-counted — but double-counted for the class they support, so the winner often survives. A fourth, practical reason: training is one pass of counting, and prediction is a single matrix multiply.

What the independence assumption costs

Two words in a 100-document TECH corpus and a 100-document SPORTS corpus. Slide the coupling ρ: at 0 the words are independent, at 1 they always appear together. The product the naive model multiplies never changes.

joint counts (both / machine only / learning only) TECH 50 / 10 / 0 SPORTS 10 / 10 / 0 naive product TECH 0.300 SPORTS 0.020 true joint TECH 0.500 SPORTS 0.100 naive ratio 15.00× true ratio 5.00× naive posterior P(TECH) = 62.5% real posterior P(TECH) = 35.7% rankings disagree at this prior

The assumption inflates confidence by counting correlated words twice; the ranking survives while the posterior gap between classes is wide. Set P(TECH) = 0.10 with ρ = 1 and the double-counted evidence is enough to flip the verdict — confidence became the decision.

Numeric check: how wrong is the product, exactly?

Take the lab’s corpora. In TECH, machine appears in 60 of 100 documents and learning in 50. In SPORTS, machine is in 20 and learning in 10. Independence says the joint probability is the product:

naive joint TECH (60/100)·(50/100) = 0.30 SPORTS (20/100)·(10/100) = 0.02 likelihood ratio = 0.30 / 0.02 = 15.0× reality if the two words always travel together, the joint count becomes min(60, 50) = 50: TECH 50/100 = 0.50, SPORTS 10/100 = 0.10 true ratio = 0.50 / 0.10 = 5.0× the product over-states the evidence 3× — and the posterior with prior P(TECH) = 0.10: naive: 0.10·0.30 = 0.030 vs 0.90·0.02 = 0.018 → TECH 0.625 truth: 0.10·0.50 = 0.050 vs 0.90·0.10 = 0.090 → SPORTS 0.643 the ranking itself flips when the prior is strong enough.

So the honest statement is not “the assumption does not matter”. It is: with many individually weak and roughly class-aligned features, the systematic error inflates confidence without usually changing the winner; when a few strongly correlated features carry the decision, it can flip. The lab lets you find both regimes.

Quick check

Naive Bayes reports P(spam) = 0.99999 where the true posterior is about 0.70. Why is the classifier still useful?

COUNTS, BITS OR BELLS

Three likelihoods.
One counting habit.

Naive Bayes is a family, not a model. The variant only changes how P(feature | class) is estimated — Multinomial counts, Bernoulli checks presence, Gaussian measures a bell curve.

Pick the variant by asking what a feature is. A word that can appear several times is a count. A short message where one mention is all you get is a bit. A sensor reading or measurement is a real number. Each choice gives a different likelihood formula, but the Bayes’ rule skeleton and the log-space sum stay identical.

VariantP(feature | class)Feature typeBest forExample
Multinomial(count + α) / (total + α·|V|)non-negative countsbag-of-words, TF-IDFemail spam, topic labels
Bernoulli(docs with word + α) / (docs + 2α)0 / 1 presenceshort texts, binary vectorsSMS spam, symptoms
Gaussian𝒩(x; μ, σ²) per featurecontinuous valuestabular, sensor featuresiris flowers, measurements

Gaussian NB: a boundary from means and variances

Two continuous features, two classes. Every class learns one mean and one variance per feature; the decision line is where the two log likelihoods tie. Move the probe and watch the posterior.

class A μ = (2.01, 2.67) σ² = (0.6473, 0.8076) class B μ = (5.48, 3.87) σ² = (0.3509, 0.4300) probe (5.0, 4.0) log p(A) = -10.229 log p(B) = -1.932 P(B | x) = 0.9998 verdict: class B

This is why sklearn’s GaussianNB adds var_smoothing: it keeps every variance positive so the log never becomes −∞. Continuous features do not need scaling — the per-class statistics already absorb it.

Derivation: Gaussian likelihood, with a numeric check

For a continuous feature, each class stores a mean μ and a variance σ² per feature. The likelihood of a value x is the height of the normal bell at x:

P(x | class) = 1/√(2πσ²) · exp( −(x − μ)² / (2σ²) ) in log space: −½·ln(2πσ²) − (x − μ)² / (2σ²) class A: μ = 0, σ² = 1 class B: μ = 2, σ² = 1 x = 1.4 log p(x | A) = −½·ln(2π) − 1.4²/2 = −0.9189 − 0.9800 = −1.8989 log p(x | B) = −½·ln(2π) − 0.6²/2 = −0.9189 − 0.1800 = −1.0989 difference = 0.8000 → likelihood ratio = e^0.8 = 2.2255 in favor of B equal priors → P(B | x) = 2.2255/(1 + 2.2255) = 0.6900 ✓ the boundary sits at x = 1, where both logs equal −1.4189.

With equal variances the boundary is the midpoint of the means. When the variances differ, the quadratic term tilts the boundary, which is why Gaussian NB draws curved (quadratic) boundaries — visible in the lab.

Numeric check: the same message through Multinomial and Bernoulli

Treat the lesson corpus as 40 spam documents and 60 ham documents with presence counts: in spam, free appears in 32 docs, money in 28, meeting in 6; in ham, free in 4, money in 7, meeting in 45. With α = 1, Bernoulli uses (present docs + 1) / (docs + 2), and it must also score every absent word.

Bernoulli P(present | spam): free 33/42 = 0.7857 money 29/42 = 0.6905 meeting 7/42 = 0.1667 Bernoulli P(present | ham) : free 5/62 = 0.0806 money 8/62 = 0.1290 meeting 46/62 = 0.7419 message "free meeting" — free and meeting present, money absent: log spam = ln0.4 + ln(33/42) + ln(7/42) + ln(13/42) = −4.1219 log ham = ln0.6 + ln(5/62) + ln(46/62) + ln(54/62) = −3.4652 posterior P(spam) = 0.3415 same message, Multinomial (counts, α = 1): log spam = ln0.4 + ln(81/153) + ln(11/153) = −4.1848 log ham = ln0.6 + ln(6/118) + ln(101/118) = −3.6453 posterior P(spam) = 0.3683 message "free" alone: Multinomial 0.8741, Bernoulli 0.8817 — Bernoulli penalizes the missing "money" and "meeting" too.

Bernoulli and Multinomial disagree in detail but not in verdict here. Bernoulli loses the repetition information (“free free free” looks like “free”) and gains explicit absence evidence — which is why it wins on very short texts, where counts are noisy but presence is reliable.

Quick check

Your features are TF-IDF weights: non-negative but real-valued, with most entries 0. Which variant fits, and why?

TRAINING IS COUNTING

One pass. Three columns.
That is training.

There is no gradient descent hiding in this lesson. Fitting a Multinomial Naive Bayes model visits every token once, writes counts into a table, and turns the table into log probabilities.

fit(X, y) does four things. For each class: count how often every word appears in its documents; add α to each count (next chapter); divide by the class’s total (plus α·|V|) to get probabilities; store the log of each. It also stores the prior — the log of the class’s share of the training examples. The result is a table with one row per class and one column per word, plus a bias per class.

The vocabulary itself is built from the training split only. Words that never appear in training have no column; they are simply ignored at prediction time. Everything the model knows is visible on this page — no hidden state, no epochs, no learning rate.

raw texttokenizevocabularycount per classsmooth +αtake logsargmaxTRAINING = COUNT AND STORE LOGSPREDICTION = RIGHT OF THE STORED LOG TABLE
Training counts and stores logs; prediction only multiplies a count vector by the stored log table and adds the log priors.
wordspam countham countP(w | spam), α = 1P(w | ham), α = 1
free80581/153 = 0.5294126/118 = 0.050847
money601061/153 = 0.39869311/118 = 0.093220
meeting1010011/153 = 0.071895101/118 = 0.855932
total150115priors: 40 spam / 100 emails → 0.4 · 60 ham → 0.6
The entire MultinomialNBpython
class MultinomialNB:
    def __init__(self, alpha=1.0):
        self.alpha = alpha

    def fit(self, X, y):
        self.classes_ = np.unique(y)
        for i, c in enumerate(self.classes_):
            X_c = X[y == c]
            self.class_log_prior_[i] = np.log(X_c.shape[0] / X.shape[0])
            counts = X_c.sum(axis=0) + self.alpha
            self.feature_log_prob_[i] = np.log(counts / counts.sum())
        return self

    def predict_log_proba(self, X):
        # counts @ log-prob table + log priors
        return X @ self.feature_log_prob_.T + self.class_log_prior_
fit() loops over classes; predict_log_proba() is one matrix multiply. Counting is the whole training algorithm.
Numeric check: prediction is a 3 × 2 matrix and a bias

Stack the log probabilities as rows = words, columns = classes, and the message “free free money” as the count vector [2, 1, 0]. Prediction is literally the product below — the same operation as a single linear layer.

spam ham log P table: free −0.635989 −2.978925 money −0.919564 −2.372789 meeting −2.632543 −0.155564 log priors −0.916291 −0.510826 [2, 1, 0] @ table + bias spam: 2(−0.635989) + 1(−0.919564) + 0 + (−0.916291) = −3.107832 ham: 2(−2.978925) + 1(−2.372789) + 0 + (−0.510826) = −8.841465 argmax(−3.108, −8.841) → spam (scale by the evidence: P(spam) = 0.996775)

Training cost: one pass over the documents. Prediction cost: for n messages, d words and k classes, one (n × d) @ (d × k) multiply. That is linear in every dimension — the reason this model runs on millions of documents.

SMOOTHING

Add a phantom count.
Delete the impossible.

One word with probability zero multiplies an entire class score down to zero. Laplace smoothing gives every word a small positive count so no single gap can veto the evidence.

Suppose “discombobulate” never appears in a spam training message: its count for spam is 0, so P(discombobulate | spam) = 0/150 = 0 and the whole spam score becomes 0 — even if the message also contains “free” ten times. A probability is a product, and one zero destroys a product. The fix is to add a small α to every count before dividing:

P(word | class) = (count(word, class) + α) / (total words in class + α · |V|) α = 0 the raw maximum-likelihood estimate — zeros survive α = 1 add-one smoothing: one phantom occurrence of every word α huge every word tends to 1/|V|, and the evidence disappears

Smoothing: no zero left behind

The corpus plus one rare word, “discombobulate”. Slide α from 0 to 1 and watch P(word | class) = (count + α) / (total + α·|V|) move every word away from zero.

α = 0.50 · |V| = 4 words spam total = 150 · ham total = 118 P(discombobulate | spam) = (0 + 0.50) / (150 + 2.00) = 0.50 / 152.00 = 0.003289 P(discombobulate | ham) = (3 + 0.50) / (118 + 2.00) = 3.50 / 120.00 = 0.029167
message “free discombobulate” free n=1 -0.636 spam -3.083 ham discombobulate n=1 -5.717 spam -3.535 ham log priors -0.916 spam -0.511 ham ────────────────────────────────────────── log score -7.269 spam -7.128 ham P(spam | message) = 0.4649 → NOT SPAM

With α = 1 the standard Laplace rule adds one phantom occurrence of every vocabulary word; with α = 0.1 it adds a tenth. Larger α lifts rare words more, which helps with small corpora and unseen words — but it also drags common words toward uniform and blurs the signal.

Derivation: the denominator, the sum check, and the Dirichlet story

Why total + α·|V| and not just total? Because the smoothed values must still be a probability distribution that sums to 1. Work it through with the lesson’s spam counts (free 80, money 60, meeting 10, N = 150, |V| = 3), α = 1:

raw counts: 80 60 10 sum = 150 add α: 81 61 11 sum = 153 = 150 + 1·3 normalize: 81/153 61/153 11/153 sum = 153/153 = 1.000000 ✓ P(free | spam) = 81/153 = 0.529412 P(money | spam) = 61/153 = 0.398693 P(meeting | spam) = 11/153 = 0.071895 check: 0.529412 + 0.398693 + 0.071895 = 1.000000

A probability interpretation: smoothing is what Bayes would do. Place a uniform Dirichlet prior with α pseudo-counts on every word, observe counts, and the posterior mean is exactly (count + α) / (total + α|V|). α is how many phantom documents you are willing to add; it is a knob, not a fudge.

Laplace smoothing is not the only option — Lidstone smoothing uses the same formula with α < 1, and interpolation smoothing blends the class-specific estimate with a global one — but they all share the goal: never let a zero vote.

Worked check: the unseen word changes a verdict

Extend the corpus with discombobulate: it appeared 3 times in the ham training words and 0 times in spam, so the totals become 150 (spam) and 118 (ham), |V| = 4. Message: “free discombobulate”.

α = 1.0 P(free | spam) = 81/154 = 0.525974 P(free | ham) = 6/122 = 0.049180 P(discombobulate | spam) = 1/154 = 0.006494 P(discombobulate | ham) = 4/122 = 0.032787 log score spam = ln0.4 + ln0.525974 + ln0.006494 = −6.5957 log score ham = ln0.6 + ln0.049180 + ln0.032787 = −6.9408 difference = +0.3451 → P(spam) = 0.5854 (barely spam) α = 0.5: difference = −0.1406 → P(spam) = 0.4649 (ham wins) α = 0.0: spam score = −∞ → P(spam) = 0.0000 the zero swallows the strong "free" evidence. same message with the word marked never seen (0/0) at α = 0: both scores −∞ → 0/0 → posterior undefined (NaN).

The same message gives three different verdicts depending on α, because the rare word is the only thing separating them. That is why α is chosen on validation data, and why α = 1 is the safe starting point rather than a law.

αEffectWhen to use it
0.001almost no smoothing — trust the countsvery large corpus, no unseen words expected
0.1light smoothinglarge corpus
1.0standard Laplace smoothingthe default starting point
10.0heavy smoothing — distributions flattentiny corpus, many unseen words expected
Quick check

As α → ∞, what happens to P(spam | “free free money”)?

WORK IN LOG SPACE

Multiply outside.
Add inside.

A message is a product of hundreds of probabilities. Done directly it underflows to zero; done in logs it is a sum that stays finite — and the prediction becomes the dot product from the counting chapter.

Multiply enough numbers smaller than 1 and the result leaves the range of floating point. A double can represent values down to about 1e−308, so a product of a few hundred small probabilities rounds to exactly 0.0 — and once it is 0, every class that shares it is tied at 0 and the comparison is meaningless. The logarithm fixes this without changing the ranking, because log is increasing and turns products into sums:

log(a · b) = log(a) + log(b) log(xⁿ) = n · log(x) score(class) = log P(class) + Σᵢ countᵢ · log P(wordᵢ | class) in matrix form: log_scores = X @ log_feature_probs.T + log_priors prediction = argmax(log_scores)

The product dies, the log sum does not

A message of n identical words that each have probability p under a class. The raw product is what a naive implementation multiplies; the log score is what the classifier should add.

n = 200 · p = 0.05 raw product pⁿ = 6.223e-261 log score n·ln p = -599.15 log₁₀(product) = -260.21 product still finite; 48 more multiplications before it rounds to 0. underflow threshold: n = 249

log(a·b) = log(a) + log(b), so adding logs is the same arithmetic without the tiny intermediate numbers. A single repeated word is the worst case; real messages mix probabilities and die far sooner.

Worked classification, end to end (both messages)

The full model at α = 1: each word’s smoothed probability, its log, then the weighted sum. Message A is “free free money”; message B is “free meeting”.

α = 1 smoothed probabilities spam ham free ( 80+1)/153 = 0.529412 ( 5+1)/118 = 0.050847 money ( 60+1)/153 = 0.398693 ( 10+1)/118 = 0.093220 meeting ( 10+1)/153 = 0.071895 (100+1)/118 = 0.855932 priors 0.40 / 0.60 message A: "free free money" — counts (2, 1, 0) log spam = ln 0.4 + 2·ln 0.529412 + 1·ln 0.398693 = −0.916291 + 2(−0.635989) + (−0.919564) = −3.107832 log ham = ln 0.6 + 2·ln 0.050847 + 1·ln 0.093220 = −0.510826 + 2(−2.978925) + (−2.372789) = −8.841465 difference = +5.733633 → P(spam) = 1/(1 + e^(−5.733633)) = 0.996775 → P(ham) = 0.003225 check: sum = 1 ✓ verdict: SPAM (odds ≈ 309 : 1) message B: "free meeting" — counts (1, 0, 1) log spam = −0.916291 + (−0.635989) + (−2.632543) = −4.184823 log ham = −0.510826 + (−2.978925) + (−0.155564) = −3.645315 difference = −0.539508 → P(spam) = 1/(1 + e^(+0.539508)) = 0.368302 verdict: NOT SPAM — one strong "meeting" outweighs "free".

Both traces used only additions. The exponentials are needed once, at the end, only if you want probabilities rather than a ranking — and a numerically careful implementation subtracts the maximum log score before exponentiating (the log-sum-exp trick) so even the final step cannot overflow.

Derivation: why the log sum is the same number

The log score is the log of the unnormalized product, so exponentiating it must return the product. Numeric check with message A:

log score spam = −3.107832 → e^(−3.107832) = 0.044698 log score ham = −8.841465 → e^(−8.841465) = 0.0001446 ratio = 0.044698 / 0.0001446 = 309.1 = e^(5.733633) ✓ normalize: 0.044698 / (0.044698 + 0.0001446) = 0.996775 ✓ the raw products are the same numbers, just computed by addition first: p_free² · p_money · p_prior = 0.044698 instead of computing 0.044698 directly, we computed 2(−0.635989) + (−0.919564) + (−0.916291) = −3.107832

The numbers agree exactly because log and exp are inverses. The difference is only in what the machine can represent along the way: the raw product of 300 small probabilities is not representable, while the sum of their logs is an ordinary negative number.

WHEN NAIVE WINS — AND WHEN IT LOSES

Stable and wrong.
Or flexible and right.

The independence assumption is a bias knob. With little data it is a gift; with lots of data it is a ceiling. Know which regime you are in before choosing the classifier.

Naive Bayes fails when the independence assumption changes the ranking, not merely the probabilities. Three failure modes are worth memorizing. Strong interactions: if the class depends on a combination of features (an XOR-like pattern) and neither feature alone carries signal, the product of marginals sees nothing. Correlated features with opposing evidence: if A and B always agree in reality but the table says A favors spam and B favors ham, the double count fabricates a conflict. Plenty of data: with enough examples, logistic regression learns the true boundary and overtakes the constrained model.

It wins on the opposite regime: many features, each individually weak, few labeled examples, and a text-shaped problem where errors wash out. That is exactly why it is the baseline everyone keeps.

AspectNaive BayesLogistic regression
Typegenerative: models P(X | Y) and P(Y)discriminative: models P(Y | X) directly
Trainingcount frequencies, one passoptimize a loss, many passes
Small databetter — the strong prior keeps it stableworse — not enough data to fit the weights
Large dataworse — the wrong assumption starts to bindbetter — the flexible boundary wins
Featuresassumes conditional independencehandles correlations
Speedsingle pass; prediction is one matmuliterative optimization
Calibrationpoor — probabilities are overconfidentbetter probabilities

The confusion-matrix console

Forty held-out messages with their model scores. Slide the decision threshold and watch precision, recall and F1 move — accuracy barely notices.

0P(spam) 0.51spamham

positive class = spam · green = correct · red = error. The threshold line is drawn from the slider, and every number on the right is recounted from these forty rows.

n = 40predicted spampredicted hamrow total
actual spamTP 17FN 320
actual hamFP 5TN 1520
column total221840
threshold = 0.50 precision TP/(TP+FP) = 17/22 = 0.7727 recall TP/(TP+FN) = 17/20 = 0.8500 F1 2PR/(P+R) = 0.8095 accuracy (TP+TN)/n = 32/40 = 0.8000 5 legitimate messages would be sent to the spam folder. 3 spam messages reach the inbox.
Worked check: the threshold trade-off, computed

The console’s forty held-out messages with their model scores. At the default threshold 0.5 on P(spam):

threshold 0.50: TP 17 FN 3 FP 5 TN 15 precision = TP/(TP+FP) = 17/22 = 0.7727 recall = TP/(TP+FN) = 17/20 = 0.8500 F1 = 2PR/(P+R) = 2(0.7727)(0.85)/(0.7727+0.85) = 0.8095 accuracy = (17+15)/40 = 0.8000 threshold 0.75: TP 13 FN 7 FP 0 TN 20 precision = 13/13 = 1.0000 recall = 13/20 = 0.6500 F1 = 0.7879 accuracy = 33/40 = 0.8250 the higher threshold removes every false alarm and misses 7 real spam messages. Accuracy barely moves (0.800 → 0.825): it is the wrong headline metric.

Why accuracy alone misleads. Suppose 95% of a corpus is ham. The constant classifier “always ham” scores 95% accuracy and catches 0% of spam. Accuracy answers “how often are we right?” when the question that matters is “of the spam, how much did we catch, and how much good mail did we burn?” — precision and recall, read together, answer that.

Which threshold ships? It depends on the cost ratio: a false positive hides a real email, a false negative lets spam through. If the company can tolerate a spam or two but not a lost invoice, 0.75 is right; if a spam message is expensive (phishing), move the threshold left and accept more false alarms.

CHECK YOURSELF

Six questions.
Then the terms worth keeping.

Answer before you look. The independence question and the Multinomial versus Gaussian question are the two that separate a memorized definition from a working instinct.

0 / 6 answered · 0 correct

01What is the “naive” assumption in Naive Bayes?

02What does Laplace smoothing prevent in Naive Bayes?

03The naive independence assumption is clearly wrong for text. Why does Naive Bayes still classify well?

04When should you use Multinomial NB versus Gaussian NB?

05An email contains “free” twice and “money” once. In Multinomial NB with log probabilities, how is the spam score computed?

06Why does Naive Bayes predict in log space instead of multiplying probabilities directly?

Key terms, demystified

Click a card to swap the lazy description for what it actually means.

Exercises from the lesson

Four problems with exact numbers — an α sweep, an independence test, a Bernoulli implementation and a confusion matrix. Try first; a worked answer is one click away.

  1. Smoothing experiment. On the lesson corpus (spam: free 80, money 60, meeting 10, total 150; ham: free 5, money 10, meeting 100, total 115; priors 0.4 / 0.6), compute P(spam | “free free money”) for α = 0.01, 0.1, 1.0, 10 and 100. Where is the evidence strongest, and why does very large α hurt?
    Show one worked answer

    Using P(word | class) = (count + α) / (total + 3α), the posterior for “free free money” is 0.997828 at α = 0.01, 0.997741 at α = 0.1, 0.996775 at α = 1, 0.977733 at α = 10 and 0.690897 at α = 100. Small α keeps the data's counts and the evidence stays sharp; at α = 100 every word probability drifts toward 1/3 (P(free | spam) = 180/450 = 0.400, P(money | spam) = 160/450 = 0.356, P(meeting | spam) = 110/450 = 0.244), so the likelihood ratio between spam and ham compresses and the posterior slides back toward the prior 0.4. The unseen-word test is the other extreme: at α = 0 an unseen word makes both class products 0, so the verdict is 0/0 — undefined. α is a hyperparameter: start at 1.0 and tune it on validation data, never on the test set.

  2. Feature independence test. In a 100-document TECH corpus, “machine” appears in 60 and “learning” in 50; in a 100-document SPORTS corpus, “machine” appears in 20 and “learning” in 10. Independent estimates would predict the joint counts 60·50/100 = 30 and 20·10/100 = 2. Take both words as present and compute the naive likelihood ratio P(machine)P(learning) for TECH over SPORTS, the true joint ratio when both words always travel together (joint counts 50 and 10), and the posterior with prior P(TECH) = 0.10. Does the ranking change?
    Show one worked answer

    Naive estimate: P(machine)P(learning) is (60/100)(50/100) = 0.30 for TECH and (20/100)(10/100) = 0.02 for SPORTS, a likelihood ratio of 15. True joint with full correlation: 50/100 = 0.50 for TECH and 10/100 = 0.10 for SPORTS, a ratio of 5 — the naive model overstates the evidence threefold. Posterior check with prior P(TECH) = 0.10: naive sees 0.10 × 0.30 = 0.030 for TECH against 0.90 × 0.02 = 0.018 for SPORTS, so it still picks TECH (0.625). The true joints give 0.10 × 0.50 = 0.050 against 0.90 × 0.10 = 0.090, so the Bayes-optimal answer is SPORTS (0.643). The independence assumption did not just muddy the probability — it flipped the winner, because the correlated words double-count evidence that a strong prior was already arguing against.

  3. Bernoulli implementation. Treat the lesson corpus as documents instead of words: of 40 spam emails, free appears in 32, money in 28, meeting in 6; of 60 ham emails, free appears in 4, money in 7, meeting in 45. With α = 1 compute Bernoulli NB's P(spam | “free meeting”) — using presence for free and meeting and explicit absence for money — and compare it with Multinomial NB's 0.368302 for the same message. Which variant is more confident, and why?
    Show one worked answer

    Bernoulli probabilities with α = 1: P(free | spam) = 33/42 = 0.785714, P(money | spam) = 29/42 = 0.690476, P(meeting | spam) = 7/42 = 0.166667; P(free | ham) = 5/62 = 0.080645, P(money | ham) = 8/62 = 0.129032, P(meeting | ham) = 46/62 = 0.741935. For the message, “money” is absent, so use 1 − p: log-spam = log 0.4 + log 0.785714 + log 0.833333 + log 0.309524 = −4.121933; log-ham = log 0.6 + log 0.080645 + log 0.258065 + log 0.870968 = −3.465165. Posterior P(spam) = 0.341466, slightly lower (less confident for ham) than Multinomial's 0.368302. Bernoulli uses presence/absence only — it treats “free free meeting” like “free meeting”, losing the repetition that Multinomial counts — but it gains the explicit absence test for “money”, which is rare in spam and common in ham here. For very short texts where counts are noisy, that presence/absence signal is often the more reliable one.

  4. Confusion metrics. The lesson's held-out corpus gives, at threshold 0.5 on P(spam): TP = 17, FN = 3, FP = 5, TN = 15. Compute precision, recall, F1 and accuracy. Then move the threshold to 0.75, where TP = 13, FN = 7, FP = 0, TN = 20, and recompute. Which threshold would you ship, and what does accuracy alone hide?
    Show one worked answer

    At 0.5: precision = 17/(17+5) = 0.7727, recall = 17/(17+3) = 0.85, F1 = 0.8095, accuracy = (17+15)/40 = 0.80. At 0.75: precision = 13/13 = 1.0, recall = 13/20 = 0.65, F1 = 0.7879, accuracy = 33/40 = 0.825. The higher threshold ships zero false alarms but lets 7 spam through; whether that is better depends on the cost of a false positive (a real email lost in the spam folder) versus a false negative (one spam in the inbox). Accuracy hides all of it: a corpus with 95% ham can be “95% accurate” by predicting ham for everything, with recall 0 on spam — which is why precision, recall and F1 come out together.

Terms this lesson borrows from later lessons (or outside)

You do not need to master these here. Each one gets a proper treatment in its own lesson; the one-line meaning is enough to keep reading. Orange dotted underlines in the prose point back to this list.

  • conditional probabilityP(A | B): the probability of A once B is known. Bayes' rule is built from two conditional probabilities and one prior. (Phase 1, Lesson 06)
  • Bayes' theoremposterior ∝ prior × likelihood: update a belief with evidence. Naive Bayes is this theorem plus one big simplifying assumption. (Phase 1, Lesson 07)
  • Dirichlet priorA probability distribution over probability distributions. Laplace smoothing is exactly the result of placing a uniform Dirichlet prior on the per-class word distribution. (Outside scope — named for orientation)
  • floating-point underflowWhen a positive number gets so small that the computer rounds it to 0.0. Doubles die below about 1e−308, which is why Naive Bayes adds logs instead of multiplying probabilities. (Phase 1, Lesson 13)
  • bag-of-wordsA feature vector of word counts that ignores order: “free money free” and “money free free” become the same vector (2, 1, 0). Naive Bayes is the classic classifier for this representation. (Phase 2, Lesson 08)
  • TF-IDFTerm frequency times inverse document frequency: re-weights word counts so common words count less and rare, discriminative words count more. It is non-negative, so Multinomial NB can consume it directly. (Phase 2, Lesson 08)
  • logistic regressionThe discriminative linear classifier NB is most often compared with. It learns weights by optimization and handles correlated features better, but needs more data than counting does. (Phase 2, Lesson 03)
  • precision and recallOf the emails flagged spam, precision is the fraction that really were; of the real spam, recall is the fraction that were caught. They trade off as you move the probability threshold. (Phase 2, Lesson 09)
KEEP GOING

A picture is a start.
Practice is the rest.

This lesson is a port of an open course. Everything here traces back to it — and the next step is running the code yourself.

Lesson text adapted from AI Engineering from Scratch (Phase 02, Lesson 14) and the Math Foundations Notebook reference build. Interactive figures, the six labs, the worked Bayes' rule and smoothing traces, the Bernoulli-versus-Multinomial table, the Gaussian numeric check, the underflow thresholds and the worked exercise answers are original to this page. Every probability a lab displays is computed from the counts printed next to it.