Skip to content

Commit e3a8329

Browse files
committed
Add MultinomialNaiveBayesClassifier Implementation
1 parent 742162c commit e3a8329

2 files changed

Lines changed: 257 additions & 0 deletions

File tree

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package com.thealgorithms.machinelearning;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
6+
/**
7+
* Multinomial Naive Bayes classifier.
8+
*
9+
* <p>Suited to discrete, count-based features (e.g. word frequencies in text
10+
* classification). Class priors and feature likelihoods are estimated from
11+
* training data with Laplace (add-alpha) smoothing to avoid zero
12+
* probabilities for unseen feature/class combinations. Predictions are made
13+
* by comparing summed log-probabilities across classes, which avoids the
14+
* numerical underflow that repeated multiplication of small probabilities
15+
* would cause.
16+
*
17+
* <p>Reference: <a href="https://en.wikipedia.org/wiki/Naive_Bayes_classifier">
18+
* Naive Bayes classifier</a>
19+
*
20+
* @author Vraj Prajapati(Rosander0)
21+
*/
22+
public final class MultinomialNaiveBayesClassifier {
23+
24+
private final double alpha;
25+
private final Map<Integer, Double> logPriors;
26+
private final Map<Integer, double[]> logLikelihoods;
27+
private int numFeatures;
28+
29+
/**
30+
* Constructs a classifier with the given Laplace smoothing parameter.
31+
*
32+
* @param alpha smoothing constant; must be greater than 0. A value of 1.0
33+
* corresponds to standard Laplace smoothing.
34+
*/
35+
public MultinomialNaiveBayesClassifier(double alpha) {
36+
if (alpha <= 0) {
37+
throw new IllegalArgumentException("alpha must be greater than 0");
38+
}
39+
this.alpha = alpha;
40+
this.logPriors = new HashMap<>();
41+
this.logLikelihoods = new HashMap<>();
42+
}
43+
44+
/** Constructs a classifier using the standard Laplace smoothing constant of 1.0. */
45+
public MultinomialNaiveBayesClassifier() {
46+
this(1.0);
47+
}
48+
49+
/**
50+
* Fits the classifier on the given feature matrix and labels.
51+
*
52+
* @param features training samples, each row a vector of non-negative
53+
* feature counts
54+
* @param labels class label for each row of {@code features}
55+
*/
56+
public void fit(double[][] features, int[] labels) {
57+
if (features.length == 0 || features.length != labels.length) {
58+
throw new IllegalArgumentException("features and labels must be non-empty and of equal length");
59+
}
60+
numFeatures = features[0].length;
61+
62+
Map<Integer, Integer> classCounts = new HashMap<>();
63+
Map<Integer, double[]> featureSums = new HashMap<>();
64+
Map<Integer, Double> totalFeatureCount = new HashMap<>();
65+
66+
for (int i = 0; i < features.length; i++) {
67+
int label = labels[i];
68+
classCounts.merge(label, 1, Integer::sum);
69+
double[] sums = featureSums.computeIfAbsent(label, k -> new double[numFeatures]);
70+
double total = totalFeatureCount.getOrDefault(label, 0.0);
71+
for (int j = 0; j < numFeatures; j++) {
72+
sums[j] += features[i][j];
73+
total += features[i][j];
74+
}
75+
totalFeatureCount.put(label, total);
76+
}
77+
78+
int totalSamples = features.length;
79+
for (Map.Entry<Integer, double[]> entry : featureSums.entrySet()) {
80+
int label = entry.getKey();
81+
double[] sums = entry.getValue();
82+
int count = classCounts.getOrDefault(label, 0);
83+
double total = totalFeatureCount.getOrDefault(label, 0.0);
84+
85+
logPriors.put(label, Math.log((double) count / totalSamples));
86+
87+
double denom = total + alpha * numFeatures;
88+
double[] logLikelihood = new double[numFeatures];
89+
for (int j = 0; j < numFeatures; j++) {
90+
logLikelihood[j] = Math.log((sums[j] + alpha) / denom);
91+
}
92+
logLikelihoods.put(label, logLikelihood);
93+
}
94+
}
95+
96+
/**
97+
* Predicts the most likely class for a single sample.
98+
*
99+
* @param sample feature vector of non-negative counts
100+
* @return the predicted class label
101+
*/
102+
public int predict(double[] sample) {
103+
if (logPriors.isEmpty()) {
104+
throw new IllegalStateException("classifier has not been fitted");
105+
}
106+
if (sample.length != numFeatures) {
107+
throw new IllegalArgumentException("sample length must match training feature count");
108+
}
109+
110+
int bestLabel = -1;
111+
double bestScore = Double.NEGATIVE_INFINITY;
112+
113+
for (Map.Entry<Integer, double[]> entry : logLikelihoods.entrySet()) {
114+
int label = entry.getKey();
115+
double[] logLikelihood = entry.getValue();
116+
double score = logPriors.getOrDefault(label, Double.NEGATIVE_INFINITY);
117+
for (int j = 0; j < numFeatures; j++) {
118+
score += sample[j] * logLikelihood[j];
119+
}
120+
if (score > bestScore) {
121+
bestScore = score;
122+
bestLabel = label;
123+
}
124+
}
125+
return bestLabel;
126+
}
127+
128+
/**
129+
* Predicts class labels for a batch of samples.
130+
*
131+
* @param samples feature vectors of non-negative counts
132+
* @return predicted class label for each row of {@code samples}
133+
*/
134+
public int[] predict(double[][] samples) {
135+
int[] predictions = new int[samples.length];
136+
for (int i = 0; i < samples.length; i++) {
137+
predictions[i] = predict(samples[i]);
138+
}
139+
return predictions;
140+
}
141+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package com.thealgorithms.machinelearning;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertThrows;
5+
import static org.junit.jupiter.api.Assertions.assertTrue;
6+
7+
import org.junit.jupiter.api.Test;
8+
9+
class MultinomialNaiveBayesClassifierTest {
10+
11+
@Test
12+
void predictsCorrectClassOnSeparableToyDataset() {
13+
// Class 0 samples are dominated by feature 0; class 1 samples by feature 1.
14+
double[][] features = {
15+
{5, 1},
16+
{6, 0},
17+
{4, 1},
18+
{1, 5},
19+
{0, 6},
20+
{1, 4},
21+
};
22+
int[] labels = {0, 0, 0, 1, 1, 1};
23+
24+
MultinomialNaiveBayesClassifier classifier = new MultinomialNaiveBayesClassifier();
25+
classifier.fit(features, labels);
26+
27+
assertEquals(0, classifier.predict(new double[] {5, 0}));
28+
assertEquals(1, classifier.predict(new double[] {0, 5}));
29+
}
30+
31+
@Test
32+
void predictBatchMatchesIndividualPredictions() {
33+
double[][] features = {
34+
{3, 0},
35+
{2, 0},
36+
{0, 3},
37+
{0, 2},
38+
};
39+
int[] labels = {0, 0, 1, 1};
40+
41+
MultinomialNaiveBayesClassifier classifier = new MultinomialNaiveBayesClassifier();
42+
classifier.fit(features, labels);
43+
44+
double[][] samples = {{4, 0}, {0, 4}};
45+
int[] predictions = classifier.predict(samples);
46+
47+
assertEquals(classifier.predict(samples[0]), predictions[0]);
48+
assertEquals(classifier.predict(samples[1]), predictions[1]);
49+
}
50+
51+
@Test
52+
void laplaceSmoothingKeepsZeroCountFeatureLogProbabilityFinite() {
53+
// Feature index 1 never appears for class 0 in training data.
54+
double[][] features = {
55+
{2, 0},
56+
{3, 0},
57+
{0, 2},
58+
{0, 3},
59+
};
60+
int[] labels = {0, 0, 1, 1};
61+
62+
MultinomialNaiveBayesClassifier classifier = new MultinomialNaiveBayesClassifier();
63+
classifier.fit(features, labels);
64+
65+
// A sample that hits class 0's zero-count feature should still produce
66+
// a finite, usable prediction instead of -Infinity collapsing the score.
67+
int prediction = classifier.predict(new double[] {1, 1});
68+
assertTrue(prediction == 0 || prediction == 1);
69+
}
70+
71+
@Test
72+
void predictBeforeFitThrowsIllegalStateException() {
73+
MultinomialNaiveBayesClassifier classifier = new MultinomialNaiveBayesClassifier();
74+
assertThrows(IllegalStateException.class, () -> classifier.predict(new double[] {1, 2}));
75+
}
76+
77+
@Test
78+
void nonPositiveAlphaThrowsIllegalArgumentException() {
79+
assertThrows(IllegalArgumentException.class, () -> new MultinomialNaiveBayesClassifier(0.0));
80+
assertThrows(IllegalArgumentException.class, () -> new MultinomialNaiveBayesClassifier(-1.0));
81+
}
82+
83+
@Test
84+
void mismatchedSampleLengthThrowsIllegalArgumentException() {
85+
double[][] features = {
86+
{1, 2},
87+
{3, 4},
88+
};
89+
int[] labels = {0, 1};
90+
91+
MultinomialNaiveBayesClassifier classifier = new MultinomialNaiveBayesClassifier();
92+
classifier.fit(features, labels);
93+
94+
assertThrows(IllegalArgumentException.class, () -> classifier.predict(new double[] {1, 2, 3}));
95+
}
96+
97+
@Test
98+
void mismatchedFeatureAndLabelLengthsThrowsIllegalArgumentException() {
99+
double[][] features = {
100+
{1, 2},
101+
{3, 4},
102+
};
103+
int[] labels = {0};
104+
105+
MultinomialNaiveBayesClassifier classifier = new MultinomialNaiveBayesClassifier();
106+
assertThrows(IllegalArgumentException.class, () -> classifier.fit(features, labels));
107+
}
108+
109+
@Test
110+
void emptyFeaturesArrayThrowsIllegalArgumentException() {
111+
double[][] features = {};
112+
int[] labels = {};
113+
MultinomialNaiveBayesClassifier classifier = new MultinomialNaiveBayesClassifier();
114+
assertThrows(IllegalArgumentException.class, () -> classifier.fit(features, labels));
115+
}
116+
}

0 commit comments

Comments
 (0)