You open your email. 200 new messages. Your brain instantly sorts:
"FREE GIFT," "Claim your prize," "Nigerian prince" → spam.
"Team standup at 10am," "Invoice attached," "Happy birthday!" → not spam.
You didn't read those emails carefully. You pattern-matched on a few words.
Naive Bayes does exactly this — and it does it with math.
It makes an assumption that is almost never true, and achieves 99% accuracy anyway.
01 — What Bayes' Theorem Actually Says
Before the algorithm, understand the theorem it's built on. Bayes' theorem answers one type of question: given that I've observed some evidence, how should I update my belief about a hypothesis?
In email terms: given that this email contains the word "FREE," what is the probability it is spam?
P(spam | "FREE") = P("FREE" | spam) × P(spam) / P("FREE")
P(spam | "FREE") = 0.80 × 0.30 / 0.40 = 0.60
After seeing the word "FREE," the probability this email is spam jumps from 30% to 60%. The evidence updated our belief.
A detective is investigating a crime. Before finding any evidence, she thinks there's a 20% chance Suspect A is guilty (the prior). She finds a size-12 boot print at the scene — and knows that Suspect A wears size-12 boots (strong likelihood). After updating with this evidence, her posterior belief rises to 70%.
Bayes' theorem is just the formal version of this mental update: prior belief × strength of evidence → updated belief. Every time you get new evidence, you run Bayes again.
02 — The "Naive" Assumption: Multiplying Independent Probabilities
A real email has many words. Applying Bayes' theorem properly to multiple words at once would require knowing the probability of every possible combination of words appearing together — for a vocabulary of 50,000 words, that's an astronomically large table. Completely impractical.
Naive Bayes makes one bold simplification that makes the problem tractable: assume all features (words) are conditionally independent of each other given the class label. Treat each word as a completely separate, independent piece of evidence. Then just multiply all the individual probabilities together.
Email arrives: "FREE MONEY". Is it spam or not spam?
Training data told us:
P(spam) = 0.30 · P(not spam) = 0.70
P("FREE" | spam) = 0.80 · P("FREE" | not spam) = 0.10
P("MONEY" | spam) = 0.60 · P("MONEY" | not spam) = 0.05
Step 1 — Score for spam (numerator only, we compare ratios):
P(spam) × P("FREE"|spam) × P("MONEY"|spam) = 0.30 × 0.80 × 0.60 = 0.144
Step 2 — Score for not spam:
P(not spam) × P("FREE"|not spam) × P("MONEY"|not spam) = 0.70 × 0.10 × 0.05 = 0.0035
Step 3 — Compare: 0.144 > 0.0035 → the spam score is 41× higher → classify as spam ✓
Note: these raw scores don't sum to 1 (they're not normalized), but we only need to compare them, not interpret them as probabilities. The class with the bigger score wins.
This is "naive" because "FREE" and "MONEY" are obviously not independent — spammers deliberately write them together. But notice: the classification was still correct. The probabilities are wrong (0.144 is not the true probability of spam) but the ranking is right — spam scored higher than not-spam, and that's all we need.
03 — The Zero Problem and Laplace Smoothing
There's a critical bug hiding in the multiplication approach. Suppose the word "CRYPTOCURRENCY" never appeared in any training spam email. Then:
P("CRYPTOCURRENCY" | spam) = 0/total_spam_words = 0.0
And now every spam score for any email containing "CRYPTOCURRENCY" becomes:
P(spam) × ... × 0.0 × ... = 0.0
One unseen word wipes out all other evidence, no matter how many other strong spam signals the email contains. This is called the zero-frequency problem.
Add 1 to every word count before computing probabilities — even words that never appeared get a count of 1 instead of 0.
Without smoothing: P("CRYPTOCURRENCY" | spam) = 0 / 500 = 0.0 — kills the entire product
With Laplace smoothing: P("CRYPTOCURRENCY" | spam) = (0 + 1) / (500 + V) ≈ a very small number — not zero
Where V is the vocabulary size (number of unique words). The denominator grows too, keeping the probabilities valid. The result is a tiny but non-zero probability — the model says "I've never seen this word in spam before, but I won't let that destroy all my other evidence."
sklearn's MultinomialNB applies Laplace smoothing by default via the alpha=1.0 parameter. Setting alpha=0 disables it — rarely a good idea.
04 — Three Types of Naive Bayes: Which to Use When
Use for: spam detection, sentiment analysis, topic classification — any task where you convert text to word counts (TF or TF-IDF).
sklearn:
MultinomialNB()
Use for: short texts where repetition isn't informative. Also good when vocabulary is small and you just care about what topics are touched.
sklearn:
BernoulliNB()
Use for: sensor readings, temperature, height, medical measurements — anything with real-valued, approximately normally distributed features.
sklearn:
GaussianNB()
Use for: heavily imbalanced text datasets where one class has far more examples. Often outperforms Multinomial NB on text tasks.
sklearn:
ComplementNB()
05 — From Scratch: Hand-Computing Spam Detection
Before sklearn, let's compute probabilities manually on a tiny training set. This makes every step of the algorithm visible.
from collections import defaultdict
import math
# Training data — 4 emails: 1=spam, 0=not spam
train = [
("free money win prize", 1),
("claim free gift now", 1),
("meeting agenda friday", 0),
("invoice project report", 0),
]
# Step 1: count words per class
word_counts = {0: defaultdict(int), 1: defaultdict(int)}
class_totals = {0: 0, 1: 0}
class_doc_counts = {0: 0, 1: 0}
vocabulary = set()
for text, label in train:
words = text.split()
class_doc_counts[label] += 1
for word in words:
word_counts[label][word] += 1
class_totals[label] += 1
vocabulary.add(word)
V = len(vocabulary) # vocabulary size (for Laplace smoothing)
total_docs = len(train)
# Step 2: define log-probability function (uses Laplace smoothing)
# We use log-probabilities to avoid floating-point underflow from
# multiplying many tiny probabilities (0.001 × 0.002 × ... → 0.0)
def log_prob_word(word, label):
count = word_counts[label].get(word, 0)
return math.log((count + 1) / (class_totals[label] + V)) # Laplace
# Step 3: classify a new email
def classify(email):
words = email.split()
scores = {}
for label in [0, 1]:
# Prior: log P(class)
score = math.log(class_doc_counts[label] / total_docs)
# Likelihood: add log P(word | class) for each word
for word in words:
score += log_prob_word(word, label)
scores[label] = score
prediction = 1 if scores[1] > scores[0] else 0
return prediction, scores
pred, scores = classify("free money report")
print(f"Email: 'free money report'")
print(f" Log-score spam = {scores[1]:.3f}")
print(f" Log-score not spam = {scores[0]:.3f}")
print(f" Prediction: {'Spam' if pred else 'Not spam'}")
We switched from multiplying probabilities to adding log-probabilities. This is mathematically identical — log(a × b × c) = log(a) + log(b) + log(c) — but avoids a serious practical problem.
Multiplying many tiny probabilities together causes floating-point underflow: numbers like 0.001 × 0.002 × 0.003 × ... quickly shrink below what a computer can represent (below ~10−308), and become exactly 0.0. Once everything is 0, you can't compare them.
Log-probabilities are always negative numbers (log of a fraction), but they stay in a representable range. Larger log-score = higher probability. The spam score (−7.824) is larger than the not-spam score (−9.103), so the email is classified as spam — same conclusion as raw multiplication, but numerically stable.
06 — Spam Classifier in sklearn
The sklearn version handles tokenization, counting, smoothing, and log-probabilities automatically. CountVectorizer converts raw text into word-count vectors — it builds a vocabulary from all training emails, then represents each email as a list of how many times each vocabulary word appears. That representation is exactly what MultinomialNB expects.
from sklearn.naive_bayes import MultinomialNB
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.metrics import classification_report
# Training corpus — in production you'd have thousands of emails
emails = [
"free money click now win prize", # spam
"free gift claim your reward now", # spam
"win cash prize limited offer", # spam
"meeting at 3pm agenda attached", # not spam
"invoice for project milestone", # not spam
"quarterly report due friday", # not spam
]
labels = [1, 1, 1, 0, 0, 0] # 1=spam, 0=not spam
# CountVectorizer: text → word-count vectors
# Each email becomes a row: [count("agenda"), count("attached"), ...]
vec = CountVectorizer()
X = vec.fit_transform(emails)
model = MultinomialNB(alpha=1.0).fit(X, labels) # alpha=1 = Laplace smoothing
# Classify new emails
test_emails = [
"claim your free money reward",
"project deadline reminder friday",
]
test_X = vec.transform(test_emails)
predictions = model.predict(test_X)
probabilities = model.predict_proba(test_X)
for email, pred, proba in zip(test_emails, predictions, probabilities):
label = "Spam" if pred else "Not spam"
print(f"'{email}'")
print(f" → {label} (spam prob: {proba[1]:.1%})")
07 — Why It Still Powers Production at Scale
MultinomialNB supports this via .partial_fit(). Almost no other classification algorithm supports true online updates as simply.The Bayesian reasoning at the heart of Naive Bayes underlies how modern LLMs are evaluated for calibration — the question of whether a model's confidence scores actually match reality. A well-calibrated model that says "I'm 90% confident" should be correct 90% of the time. One that's overconfident says 95% but is only right 70% of the time.
Techniques like temperature scaling and Platt scaling use Bayesian ideas to correct miscalibrated neural network outputs — making them more reliable in high-stakes settings like medical diagnosis and legal decision support.
The math that filters your spam in milliseconds is the same framework used to decide whether to trust GPT-4's answer in a hospital.
08 — A Thought to Sit With
Naive Bayes works brilliantly despite a demonstrably false assumption. "FREE" and "MONEY" are not independent — spammers write them together on purpose. The model "knows" its probability estimates are wrong. Yet it still classifies correctly.
What does this tell you about the relationship between mathematical correctness and practical usefulness in AI? Can you think of other situations — in life, not just ML — where a simplified, "wrong" model outperforms a theoretically correct but impractical one?
09 — Coming Up Next
Tomorrow — Day 8: Decision Trees. We move from probabilistic models to rule-based ones. Decision trees split data by asking yes/no questions — and they're the foundational building block for the most powerful tabular ML methods: Random Forests and XGBoost.
10 — Go Deeper
▶ StatQuest: Naive Bayes, Clearly Explained
YouTube · 15 min · The best walkthrough of priors, likelihoods, and posteriors — with visual examples that make Bayes' theorem click.
▶ Kaggle: Intro to Machine Learning
Free interactive course · Apply Naive Bayes and other classifiers to real datasets in your browser — the fastest bridge from reading to hands-on practice.
- Bayes' theorem — P(class | evidence) = P(evidence | class) × P(class) / P(evidence). Updates prior belief with observed evidence to get a posterior.
- Prior — baseline class frequency before seeing any features. P(spam) = fraction of all training emails that are spam.
- Likelihood — P(word | class). For each word in the new email, how often does it appear in training examples of this class?
- Naive assumption — all features are conditionally independent. Allows multiplying individual word probabilities instead of modeling all combinations.
- Why it works despite being wrong — classification only needs correct ranking, not accurate probabilities. The rank is usually right even with wrong numbers.
- Zero-frequency problem — unseen words get P=0, collapsing the entire score. Fixed by Laplace smoothing: add 1 to all counts.
- Log-probabilities — use addition of logs instead of multiplication to prevent floating-point underflow on long documents.
- Multinomial NB — word counts. Best for text. Bernoulli NB — word presence. Gaussian NB — continuous features.
- Advantages — trains in one pass, works on tiny datasets, handles 100k+ features, supports online learning via partial_fit().
- Limitation — probability estimates are miscalibrated. Don't trust the raw probabilities — use them only for ranking (classification), not as confidence scores.
- What is the "naive" assumption in Naive Bayes?
It assumes all features are conditionally independent given the class label. For text, each word's probability is computed independently. This is clearly false — correlated words exist — but the classifier is still accurate because classification requires only correct ranking, not accurate probabilities. - Why does Naive Bayes work well despite the independence assumption being wrong?
For classification we only need the rank of class probabilities to be correct, not the probabilities themselves. Even with heavily miscalibrated probability estimates, the class with the higher score is usually the correct one. The wrong model gets the right answer. - What is Laplace smoothing and why is it needed?
If a word never appeared in training examples for a class, its conditional probability is zero. Multiplying by zero collapses the entire class score, regardless of other evidence. Laplace smoothing adds 1 to every word count before computing probabilities, ensuring no probability is exactly zero. sklearn's alpha=1.0 parameter controls this. - When would you choose Gaussian Naive Bayes over Multinomial?
When features are continuous and approximately normally distributed — sensor readings, temperature, height, medical measurements. Multinomial NB is for discrete count data like word frequencies in text. - What are the main advantages of Naive Bayes over logistic regression for text?
Faster training (single pass counting), works with tiny datasets, handles very high-dimensional input naturally, and supports online/incremental learning via partial_fit(). Logistic regression typically outperforms it with enough data, but Naive Bayes wins in low-data, high-speed, or streaming scenarios.
No attachments for this topic yet.