Multinomial Naive Bayes Classifier
O
package com.thealgorithms.machinelearning;
import java.util.HashMap;
import java.util.Map;
/**
* Multinomial Naive Bayes classifier.
*
* <p>Suited to discrete, count-based features (e.g. word frequencies in text
* classification). Class priors and feature likelihoods are estimated from
* training data with Laplace (add-alpha) smoothing to avoid zero
* probabilities for unseen feature/class combinations. Predictions are made
* by comparing summed log-probabilities across classes, which avoids the
* numerical underflow that repeated multiplication of small probabilities
* would cause.
*
* <p>Reference: <a href="https://en.wikipedia.org/wiki/Naive_Bayes_classifier">
* Naive Bayes classifier</a>
*
* @author Vraj Prajapati(Rosander0)
*/
public final class MultinomialNaiveBayesClassifier {
private final double alpha;
private final Map<Integer, Double> logPriors;
private final Map<Integer, double[]> logLikelihoods;
private int numFeatures;
/**
* Constructs a classifier with the given Laplace smoothing parameter.
*
* @param alpha smoothing constant; must be greater than 0. A value of 1.0
* corresponds to standard Laplace smoothing.
*/
public MultinomialNaiveBayesClassifier(double alpha) {
if (alpha <= 0) {
throw new IllegalArgumentException("alpha must be greater than 0");
}
this.alpha = alpha;
this.logPriors = new HashMap<>();
this.logLikelihoods = new HashMap<>();
}
/** Constructs a classifier using the standard Laplace smoothing constant of 1.0. */
public MultinomialNaiveBayesClassifier() {
this(1.0);
}
/**
* Fits the classifier on the given feature matrix and labels.
*
* @param features training samples, each row a vector of non-negative
* feature counts
* @param labels class label for each row of {@code features}
*/
public void fit(double[][] features, int[] labels) {
if (features.length == 0 || features.length != labels.length) {
throw new IllegalArgumentException("features and labels must be non-empty and of equal length");
}
logPriors.clear();
logLikelihoods.clear();
numFeatures = features[0].length;
Map<Integer, Integer> classCounts = new HashMap<>();
Map<Integer, double[]> featureSums = new HashMap<>();
Map<Integer, Double> totalFeatureCount = new HashMap<>();
for (int i = 0; i < features.length; i++) {
int label = labels[i];
classCounts.merge(label, 1, Integer::sum);
double[] sums = featureSums.computeIfAbsent(label, k -> new double[numFeatures]);
double total = totalFeatureCount.getOrDefault(label, 0.0);
for (int j = 0; j < numFeatures; j++) {
sums[j] += features[i][j];
total += features[i][j];
}
totalFeatureCount.put(label, total);
}
int totalSamples = features.length;
for (Map.Entry<Integer, double[]> entry : featureSums.entrySet()) {
int label = entry.getKey();
double[] sums = entry.getValue();
int count = classCounts.getOrDefault(label, 0);
double total = totalFeatureCount.getOrDefault(label, 0.0);
logPriors.put(label, Math.log((double) count / totalSamples));
double denom = total + alpha * numFeatures;
double[] logLikelihood = new double[numFeatures];
for (int j = 0; j < numFeatures; j++) {
logLikelihood[j] = Math.log((sums[j] + alpha) / denom);
}
logLikelihoods.put(label, logLikelihood);
}
}
/**
* Predicts the most likely class for a single sample.
*
* @param sample feature vector of non-negative counts
* @return the predicted class label
*/
public int predict(double[] sample) {
if (logPriors.isEmpty()) {
throw new IllegalStateException("classifier has not been fitted");
}
if (sample.length != numFeatures) {
throw new IllegalArgumentException("sample length must match training feature count");
}
int bestLabel = -1;
double bestScore = Double.NEGATIVE_INFINITY;
for (Map.Entry<Integer, double[]> entry : logLikelihoods.entrySet()) {
int label = entry.getKey();
double[] logLikelihood = entry.getValue();
double score = logPriors.getOrDefault(label, Double.NEGATIVE_INFINITY);
for (int j = 0; j < numFeatures; j++) {
score += sample[j] * logLikelihood[j];
}
if (score > bestScore) {
bestScore = score;
bestLabel = label;
}
}
return bestLabel;
}
/**
* Predicts class labels for a batch of samples.
*
* @param samples feature vectors of non-negative counts
* @return predicted class label for each row of {@code samples}
*/
public int[] predict(double[][] samples) {
int[] predictions = new int[samples.length];
for (int i = 0; i < samples.length; i++) {
predictions[i] = predict(samples[i]);
}
return predictions;
}
}