diff --git a/src/main/java/com/thealgorithms/machinelearning/MultinomialNaiveBayesClassifier.java b/src/main/java/com/thealgorithms/machinelearning/MultinomialNaiveBayesClassifier.java
new file mode 100644
index 000000000000..5826d7108caa
--- /dev/null
+++ b/src/main/java/com/thealgorithms/machinelearning/MultinomialNaiveBayesClassifier.java
@@ -0,0 +1,141 @@
+package com.thealgorithms.machinelearning;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Multinomial Naive Bayes classifier.
+ *
+ *
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.
+ *
+ *
Reference:
+ * Naive Bayes classifier
+ *
+ * @author Vraj Prajapati(Rosander0)
+ */
+public final class MultinomialNaiveBayesClassifier {
+
+ private final double alpha;
+ private final Map logPriors;
+ private final Map 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");
+ }
+ numFeatures = features[0].length;
+
+ Map classCounts = new HashMap<>();
+ Map featureSums = new HashMap<>();
+ Map 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 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 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;
+ }
+}
diff --git a/src/test/java/com/thealgorithms/machinelearning/MultinomialNaiveBayesClassifierTest.java b/src/test/java/com/thealgorithms/machinelearning/MultinomialNaiveBayesClassifierTest.java
new file mode 100644
index 000000000000..fa268d3bd44a
--- /dev/null
+++ b/src/test/java/com/thealgorithms/machinelearning/MultinomialNaiveBayesClassifierTest.java
@@ -0,0 +1,116 @@
+package com.thealgorithms.machinelearning;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+class MultinomialNaiveBayesClassifierTest {
+
+ @Test
+ void predictsCorrectClassOnSeparableToyDataset() {
+ // Class 0 samples are dominated by feature 0; class 1 samples by feature 1.
+ double[][] features = {
+ {5, 1},
+ {6, 0},
+ {4, 1},
+ {1, 5},
+ {0, 6},
+ {1, 4},
+ };
+ int[] labels = {0, 0, 0, 1, 1, 1};
+
+ MultinomialNaiveBayesClassifier classifier = new MultinomialNaiveBayesClassifier();
+ classifier.fit(features, labels);
+
+ assertEquals(0, classifier.predict(new double[] {5, 0}));
+ assertEquals(1, classifier.predict(new double[] {0, 5}));
+ }
+
+ @Test
+ void predictBatchMatchesIndividualPredictions() {
+ double[][] features = {
+ {3, 0},
+ {2, 0},
+ {0, 3},
+ {0, 2},
+ };
+ int[] labels = {0, 0, 1, 1};
+
+ MultinomialNaiveBayesClassifier classifier = new MultinomialNaiveBayesClassifier();
+ classifier.fit(features, labels);
+
+ double[][] samples = {{4, 0}, {0, 4}};
+ int[] predictions = classifier.predict(samples);
+
+ assertEquals(classifier.predict(samples[0]), predictions[0]);
+ assertEquals(classifier.predict(samples[1]), predictions[1]);
+ }
+
+ @Test
+ void laplaceSmoothingKeepsZeroCountFeatureLogProbabilityFinite() {
+ // Feature index 1 never appears for class 0 in training data.
+ double[][] features = {
+ {2, 0},
+ {3, 0},
+ {0, 2},
+ {0, 3},
+ };
+ int[] labels = {0, 0, 1, 1};
+
+ MultinomialNaiveBayesClassifier classifier = new MultinomialNaiveBayesClassifier();
+ classifier.fit(features, labels);
+
+ // A sample that hits class 0's zero-count feature should still produce
+ // a finite, usable prediction instead of -Infinity collapsing the score.
+ int prediction = classifier.predict(new double[] {1, 1});
+ assertTrue(prediction == 0 || prediction == 1);
+ }
+
+ @Test
+ void predictBeforeFitThrowsIllegalStateException() {
+ MultinomialNaiveBayesClassifier classifier = new MultinomialNaiveBayesClassifier();
+ assertThrows(IllegalStateException.class, () -> classifier.predict(new double[] {1, 2}));
+ }
+
+ @Test
+ void nonPositiveAlphaThrowsIllegalArgumentException() {
+ assertThrows(IllegalArgumentException.class, () -> new MultinomialNaiveBayesClassifier(0.0));
+ assertThrows(IllegalArgumentException.class, () -> new MultinomialNaiveBayesClassifier(-1.0));
+ }
+
+ @Test
+ void mismatchedSampleLengthThrowsIllegalArgumentException() {
+ double[][] features = {
+ {1, 2},
+ {3, 4},
+ };
+ int[] labels = {0, 1};
+
+ MultinomialNaiveBayesClassifier classifier = new MultinomialNaiveBayesClassifier();
+ classifier.fit(features, labels);
+
+ assertThrows(IllegalArgumentException.class, () -> classifier.predict(new double[] {1, 2, 3}));
+ }
+
+ @Test
+ void mismatchedFeatureAndLabelLengthsThrowsIllegalArgumentException() {
+ double[][] features = {
+ {1, 2},
+ {3, 4},
+ };
+ int[] labels = {0};
+
+ MultinomialNaiveBayesClassifier classifier = new MultinomialNaiveBayesClassifier();
+ assertThrows(IllegalArgumentException.class, () -> classifier.fit(features, labels));
+ }
+
+ @Test
+ void emptyFeaturesArrayThrowsIllegalArgumentException() {
+ double[][] features = {};
+ int[] labels = {};
+ MultinomialNaiveBayesClassifier classifier = new MultinomialNaiveBayesClassifier();
+ assertThrows(IllegalArgumentException.class, () -> classifier.fit(features, labels));
+ }
+}