Probabilistic Modeling & Naive Bayes
Derive Bayes' Theorem from first principles, explore the generative vs discriminative paradigm, Gaussian and Multinomial likelihoods, Laplace smoothing, and zero-dependency implementations in Python and Rust.
While discriminative algorithms like Logistic Regression model the posterior boundary directly, Generative Classifiers model the joint probability distribution .
By modeling how the data was generated within each target class, Bayes' theorem allows us to invert the conditional likelihoods to determine the most probable class label for novel inputs.
The conditional independence assumption collapses the exponential parameter space from O(C^d) to O(d * C).
1. First-Principles Derivation of Bayes' Theorem#
To understand the mechanics of Bayesian classification, we begin with the definition of conditional probability across events and .
Step 1: Definition of Conditional Probability
For two events and where , the probability of given that has occurred is defined as:
Similarly, the conditional probability of given is:
Step 2: Equate the Joint Intersection
Multiply both equations by their respective denominators to isolate the joint probability :
Because the joint event is symmetric, we equate both expressions:
Step 3: Isolate the Posterior Probability
Dividing both sides by the evidence yields Bayes' Theorem:
Step 4: Marginalize the Evidence Denominator
In classification, let denote the target class and denote the feature vector. By the Law of Total Probability:
Substituting this into Bayes' Theorem gives:
2. The Conditional Independence Assumption#
Evaluating the full joint likelihood requires estimating interactions among all features simultaneously. If features are binary, this requires estimating parameters per class, creating an intractable curse of dimensionality.
The Naive Bayes Assumption posits that all feature attributes are conditionally independent given the class label :
Maximum A Posteriori (MAP) Classification Rule#
To assign a new observation to the most probable class :
Because the denominator is constant across all candidate classes , it can be dropped:
Taking the natural logarithm transforms the product of small probabilities into a numerically stable sum of log-likelihoods:
3. Likelihood Models: Gaussian Naive Bayes#
When features are continuous, we model each feature within class as a one-dimensional Gaussian normal distribution:
Where parameters are estimated via Maximum Likelihood from training samples in class :
Log-Likelihood Expansion#
Taking the logarithm of the Gaussian density yields:
The constant term can be omitted during classification comparisons.
4. Laplace (Additive) Smoothing for Discrete Models#
In text classification using Multinomial Naive Bayes, word counts represent term occurrences. If a specific word never appeared in class during training, its empirical probability evaluates to zero:
A single zero probability nullifies the entire product , completely blinding the model to all other strong evidentiary features.
To resolve this, we apply Laplace Smoothing () or Lidstone Smoothing ():
Where is the total vocabulary size (number of distinct features). This guarantees all probabilities remain strictly positive.
5. From-Scratch Implementation: Zero Dependencies#
The following implementations build a full Gaussian Naive Bayes classifier from scratch in Python and Rust using only built-in math and standard collections.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475import math from typing import List, Dict, Tuple class GaussianNaiveBayesFromScratch: def __init__(self, epsilon: float = 1e-9): self.epsilon = epsilon self.class_priors: Dict[int, float] = {} # Stores (mean, variance) per feature for each class self.class_stats: Dict[int, List[Tuple[float, float]]] = {} def fit(self, X: List[List[float]], y: List[int]) -> None: num_samples = len(X) num_features = len(X[0]) # Partition data points by class label grouped: Dict[int, List[List[float]]] = {} for sample, label in zip(X, y): if label not in grouped: grouped[label] = [] grouped[label].append(sample) # Estimate Gaussian parameters per class for label, samples in grouped.items(): count = len(samples) self.class_priors[label] = count / num_samples stats = [] for j in range(num_features): mean_j = sum(row[j] for row in samples) / count var_j = sum((row[j] - mean_j) ** 2 for row in samples) / count + self.epsilon stats.append((mean_j, var_j)) self.class_stats[label] = stats @staticmethod def _calculate_gaussian_log_pdf(x: float, mean: float, var: float) -> float: # ln( 1 / sqrt(2 * pi * var) * exp(-(x - mean)^2 / (2 * var)) ) # = -0.5 * ln(2 * pi * var) - ((x - mean)^2 / (2 * var)) coeff = -0.5 * math.log(2.0 * math.pi * var) exponent = -((x - mean) ** 2) / (2.0 * var) return coeff + exponent def predict_single(self, sample: List[float]) -> int: best_label = None highest_log_posterior = -float("inf") for label, prior in self.class_priors.items(): # Start with log prior: ln P(y = c) log_posterior = math.log(prior) # Accumulate sum of log likelihoods: sum ln P(x_j | y = c) for j, (mean_j, var_j) in enumerate(self.class_stats[label]): log_posterior += self._calculate_gaussian_log_pdf(sample[j], mean_j, var_j) if log_posterior > highest_log_posterior: highest_log_posterior = log_posterior best_label = label return best_label def predict(self, X_test: List[List[float]]) -> List[int]: return [self.predict_single(row) for row in X_test] if __name__ == "__main__": X_train = [ [1.2, 0.7], [0.8, 1.1], [1.5, 1.3], # Class 0 [5.2, 4.8], [4.9, 5.5], [6.1, 5.2] # Class 1 ] y_train = [0, 0, 0, 1, 1, 1] gnb = GaussianNaiveBayesFromScratch() gnb.fit(X_train, y_train) X_test = [[1.0, 1.0], [5.5, 5.0]] preds = gnb.predict(X_test) print(f"Predictions: {preds}")
6. Active Knowledge Checks#
In Bayesian classification, what is the fundamental mathematical reason that allows practitioners to drop the denominator P(x) when computing the MAP class assignment?
What fatal failure occurs in an un-smoothed Multinomial Naive Bayes model if a single word in a test email never appeared in the spam class training set?