forked from TheAlgorithms/Java
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMultinomialNaiveBayesClassifier.java
More file actions
143 lines (128 loc) · 5.21 KB
/
Copy pathMultinomialNaiveBayesClassifier.java
File metadata and controls
143 lines (128 loc) · 5.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
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;
}
}