Source code for neural_trees.classical.naive_bayes
"""
Naive Bayes Classifier
======================
From: Alpaydın, E. (2020). Introduction to Machine Learning (4th ed.), Chapter 3.
Naive Bayes assumes feature independence given the class label:
P(C | x) ∝ P(C) · ∏_d P(x_d | C)
Supports Gaussian, Bernoulli, and Multinomial likelihoods.
"""
import numpy as np
from scipy.special import logsumexp
from sklearn.base import BaseEstimator, ClassifierMixin
from sklearn.preprocessing import LabelEncoder
from sklearn.utils.multiclass import check_classification_targets
from sklearn.utils.validation import check_array, check_is_fitted, check_X_y
from neural_trees._validation import check_predict_input
[docs]
class NaiveBayesClassifier(ClassifierMixin, BaseEstimator):
"""
Naive Bayes Classifier with selectable likelihood.
Parameters
----------
likelihood : str, default="gaussian"
Type of feature likelihood: "gaussian", "bernoulli", or "multinomial".
alpha : float, default=1.0
Laplace smoothing parameter (for bernoulli/multinomial).
var_smoothing : float, default=1e-9
Variance stabilizer for Gaussian likelihood.
References
----------
Alpaydın, E. (2020). Introduction to Machine Learning, Chapter 3. MIT Press.
"""
def __init__(self, likelihood: str = "gaussian", alpha: float = 1.0, var_smoothing: float = 1e-9):
self.likelihood = likelihood
self.alpha = alpha
self.var_smoothing = var_smoothing
def fit(self, X, y) -> "NaiveBayesClassifier":
if self.likelihood not in ("gaussian", "bernoulli", "multinomial"):
raise ValueError(
"likelihood must be 'gaussian', 'bernoulli' or 'multinomial', got "
f"{self.likelihood!r}"
)
X, y = check_X_y(X, y)
check_classification_targets(y)
self.le_ = LabelEncoder()
y_enc = self.le_.fit_transform(y)
self.classes_ = self.le_.classes_
self.n_features_in_ = X.shape[1]
n_classes = len(self.classes_)
self.class_log_prior_ = np.zeros(n_classes)
self.theta_: list = [] # per-class sufficient statistics
for c in range(n_classes):
X_c = X[y_enc == c]
self.class_log_prior_[c] = np.log(len(X_c) / len(X))
if self.likelihood == "gaussian":
mean = X_c.mean(axis=0)
var = X_c.var(axis=0) + self.var_smoothing
self.theta_.append({"mean": mean, "var": var})
elif self.likelihood == "bernoulli":
p = (X_c.sum(axis=0) + self.alpha) / (len(X_c) + 2 * self.alpha)
self.theta_.append({"p": p})
elif self.likelihood == "multinomial":
counts = X_c.sum(axis=0) + self.alpha
self.theta_.append({"log_p": np.log(counts / counts.sum())})
return self
def _log_likelihood(self, X: np.ndarray, c: int) -> np.ndarray:
params = self.theta_[c]
if self.likelihood == "gaussian":
log_probs = -0.5 * np.log(2 * np.pi * params["var"]) \
- 0.5 * ((X - params["mean"]) ** 2) / params["var"]
return log_probs.sum(axis=1)
elif self.likelihood == "bernoulli":
p = params["p"]
return (X * np.log(p + 1e-10) + (1 - X) * np.log(1 - p + 1e-10)).sum(axis=1)
elif self.likelihood == "multinomial":
return X.dot(params["log_p"])
raise ValueError(f"Unknown likelihood: {self.likelihood}")
def _joint_log_likelihood(self, X):
"""Unnormalized log P(y, x) per class, shape (n_samples, n_classes)."""
return np.column_stack([
self.class_log_prior_[c] + self._log_likelihood(X, c)
for c in range(len(self.classes_))
])
[docs]
def predict_log_proba(self, X) -> np.ndarray:
"""
Log of the posterior class probabilities, shape (n_samples, n_classes).
Normalized, so `np.exp(predict_log_proba(X))` equals `predict_proba(X)`
and each row of the exponential sums to 1. The unnormalized joint
log-likelihood is available as `_joint_log_likelihood`.
"""
check_is_fitted(self)
X = check_predict_input(self, X)
joint = self._joint_log_likelihood(X)
return joint - logsumexp(joint, axis=1, keepdims=True)
[docs]
def predict_proba(self, X) -> np.ndarray:
"""Posterior class probabilities, shape (n_samples, n_classes)."""
return np.exp(self.predict_log_proba(X))
def predict(self, X) -> np.ndarray:
check_is_fitted(self)
return self.le_.inverse_transform(self.predict_log_proba(X).argmax(axis=1))