Build a Text Intent Classifier in Java

Build a Naive Bayes intent classifier from scratch in Java with no ML libraries.

Introduction

30 Second Summary

Every time you message a chatbot or talk to a virtual assistant, the first thing it does is figure out what you meant. It classifies your sentence into a category like "greeting" or "help request" before deciding how to respond.

In this project, you will build that classification engine from scratch in Java using the Naive Bayes algorithm, with zero ML libraries.

What You'll Build

A console-based AI that reads any sentence you type and predicts which intent category it belongs to, powered by probability math you implement yourself.

By the end of this project, you'll have:

  • A Maven Java project with a text tokenizer that preprocesses natural language into machine-readable word lists.
  • A Naive Bayes classifier trained on example phrases that computes log-probabilities to predict whether a sentence is a greeting, goodbye, thanks, or help request.
  • An interactive console application where you type any sentence and see the predicted intent and score in real time.
  • Secret Mission: Add confidence percentage output and a fifth "weather" intent to your classifier.

Are there any prerequisites?

You should be comfortable with basic Java concepts like classes, collections, and loops. Familiarity with Maven project structure is helpful but not required.

Set Up Your Maven Project

Every Java project needs a structured foundation before you write any code. Maven provides that structure by managing your project layout, compilation settings, and build process.

In this step, you will create a new Maven project in IntelliJ IDEA and configure it to compile with Java 21. This gives you a clean starting point for building your intent classifier.

In this step, get ready to:
  • Create a Maven project using the quickstart archetype.
  • Configure the project for Java 21 compilation.
  • Verify the project compiles successfully.
Create a new Maven project in IntelliJ IDEA

The Maven quickstart archetype gives you a ready-made project skeleton with the standard directory layout. You will use it to scaffold your classifier project.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type IntelliJ IDEA and press Enter to open it.
  • On the Welcome screen, click New Project.
  • From the options on the left, select Maven Archetype.
  • In the Name field, type intent-classifier.
  • Set the JDK dropdown to 21.
  • Leave the Catalog set to Internal.
  • In the Archetype dropdown, search for and select org.apache.maven.archetypes:maven-archetype-quickstart.
  • Set the Version to 1.5.
  • Expand Advanced Settings and set the GroupId to com.classifier.
  • Click Create.

What just happened?

IntelliJ IDEA used the Maven archetype to generate a complete project structure. You now have a pom.xml (the project's configuration file), a src/main/java folder for your code, and a default App.java file.

The archetype targets Java 17 by default. In the next substep, you will update it to Java 21.

Search by typing in the archetype list

The archetype list can be long. Start typing quickstart directly into the dropdown to filter and find the right archetype quickly.

Configure pom.xml for Java 21

The generated pom.xml includes JUnit test dependencies and targets Java 17. You need to strip it down and point it at Java 21 instead.

  • In the Project tool window on the left, double-click pom.xml to open it in the editor.
  • Select all the content in the file (Cmd+A on macOS, Ctrl+A on Windows).
  • Replace it with the following configuration:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!-- Project coordinates -->
    <groupId>com.classifier</groupId>
    <artifactId>intent-classifier</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <!-- Encode source files as UTF-8 -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <!-- Compile with Java 21 features -->
        <maven.compiler.release>21</maven.compiler.release>
    </properties>

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <!-- Pin the compiler plugin version -->
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.15.0</version>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>

What does this configuration do?

  • maven.compiler.release set to 21 tells Maven to compile your code with Java 21 language features.
  • project.build.sourceEncoding set to UTF-8 ensures consistent character encoding across all platforms.
  • maven-compiler-plugin version 3.15.0 is pinned to avoid version drift across environments.
  • The JUnit dependencies and dependencyManagement section from the archetype are removed because this project has no external dependencies.
  • Save the file with Cmd+S (macOS) or Ctrl+S (Windows).
  • If IntelliJ shows a popup asking to reload the Maven project, click Load Maven Changes (or the refresh icon that appears in the top-right of the editor).

IntelliJ shows red underlines in pom.xml?

Make sure there are no extra characters or whitespace before the <?xml declaration. The XML declaration must be the very first line of the file.

Also confirm you deleted all the original content before pasting. Duplicate XML tags will cause parsing errors. Help me fix red underlines in my pom.xml

✔️ Awesome, I've got everything!

Great. Make sure you have saved pom.xml before moving on.

ⓧ I'd like to double check the full code

Here is the complete pom.xml you should have:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.classifier</groupId>
    <artifactId>intent-classifier</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.release>21</maven.compiler.release>
    </properties>

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.15.0</version>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>
Verify the project compiles

Running a Maven compile confirms that your project structure and Java version configuration are correct.

  • Open IntelliJ's built-in terminal by clicking View in the menu bar.
  • Hover over Tool Windows and click Terminal.
  • Run the Maven compile command:
mvn compile

You should see BUILD SUCCESS near the bottom of the terminal output.

Seeing BUILD FAILURE?

  • Check that maven.compiler.release is set to 21 in your pom.xml properties, not 17.
  • Confirm your JDK is version 21 by running java -version in the same terminal.
  • Make sure you saved pom.xml after editing and that IntelliJ reloaded the Maven changes.

Help me fix my Maven BUILD FAILURE

Your project foundation is locked in. Next up, you will create the training data and tokenizer that teach your classifier how to read sentences.

Build Training Data and Tokenizer

Your Maven project compiles and is ready for code. Now it is time to give your classifier something to learn from.

Every AI model needs training data. In this step, you will create the example sentences that teach your classifier what each intent looks like. You will also build a tokenizer that breaks raw text into individual words the algorithm can process.

In this step, get ready to:
  • Create a training data class with example sentences for 4 intents.
  • Build a tokenizer that converts raw text into clean word lists.
  • Test the tokenizer to confirm it preprocesses sentences correctly.
Create the training data class

Before the Naive Bayes algorithm can classify anything, it needs examples of what each intent looks like. You will provide a handful of sentences for each category so the model can learn the patterns.

  • In the IntelliJ Project tool window on the left, expand src/main/java/com/classifier.
  • Right-click the classifier package folder.
  • Select New, then Java Class.
  • Type TrainingData as the name and press Enter.
  • Replace the generated file contents with the following code:
package com.classifier;

import java.util.List;
import java.util.Map;

public class TrainingData {

    // Returns a map of intent names to their example sentences
    public static Map<String, List<String>> getData() {
        return Map.of(
            "greeting", List.of(
                "hello",
                "hi there",
                "hey",
                "good morning",
                "howdy",
                "hi how are you"
            ),
            "goodbye", List.of(
                "bye",
                "goodbye",
                "see you later",
                "take care",
                "good night",
                "see you soon"
            ),
            "thanks", List.of(
                "thank you",
                "thanks a lot",
                "thanks",
                "i appreciate it",
                "thank you so much",
                "many thanks"
            ),
            "help", List.of(
                "can you help me",
                "i need help",
                "help me please",
                "i need assistance",
                "can you assist me",
                "i have a question"
            )
        );
    }
}

What does this code do?

The getData() method returns a Map where each key is an intent name (like "greeting" or "help") and each value is a list of example sentences that belong to that intent.

These 24 sentences (6 per intent) are what the classifier will study during training. The more varied the examples, the better the model generalises to sentences it has never seen before.

  • Save TrainingData.java with Cmd+S (macOS) or Ctrl+S (Windows).
  • Check that IntelliJ shows no red underlines or error indicators in the editor. The file should compile cleanly.

Seeing red underlines?

  • Make sure the package com.classifier; line is at the top of your file. IntelliJ expects this because the file lives inside the com/classifier directory.
  • Check that both imports (java.util.List and java.util.Map) are present. Without them, the compiler cannot resolve the types.

Need more help?

✔️ Awesome, I've got everything!

Great. Make sure you have saved your file before moving on.

ⓧ I'd like to double check the full code

package com.classifier;

import java.util.List;
import java.util.Map;

public class TrainingData {

    public static Map<String, List<String>> getData() {
        return Map.of(
            "greeting", List.of(
                "hello",
                "hi there",
                "hey",
                "good morning",
                "howdy",
                "hi how are you"
            ),
            "goodbye", List.of(
                "bye",
                "goodbye",
                "see you later",
                "take care",
                "good night",
                "see you soon"
            ),
            "thanks", List.of(
                "thank you",
                "thanks a lot",
                "thanks",
                "i appreciate it",
                "thank you so much",
                "many thanks"
            ),
            "help", List.of(
                "can you help me",
                "i need help",
                "help me please",
                "i need assistance",
                "can you assist me",
                "i have a question"
            )
        );
    }
}
Create the Tokenizer class

Raw text is messy. "Hello!" and "hello" are the same word to a human, but completely different strings to a computer. A tokenizer cleans and splits text so that the classifier only sees normalised words.

What is tokenization?

Tokenization is the process of breaking a sentence into individual units (tokens) that an algorithm can work with. In our case, each token is a single lowercase word with all punctuation stripped away.

"Hello, how are you?" becomes [hello, how, are, you]. This consistent format lets the classifier compare words across sentences without tripping over capitalisation or special characters.

  • Right-click the classifier package folder in the Project tool window.
  • Select New, then Java Class.
  • Type Tokenizer as the name and press Enter.
  • Replace the generated file contents with the following code:
package com.classifier;

import java.util.Arrays;
import java.util.List;

public class Tokenizer {

    public static List<String> tokenize(String text) {
        // Convert to lowercase and strip everything except letters and spaces
        String cleaned = text.toLowerCase().replaceAll("[^a-z\\s]", "");
        // Split the cleaned text into individual word tokens
        String[] tokens = cleaned.trim().split("\\s+");
        // Filter out any empty strings and return as an immutable list
        return Arrays.stream(tokens)
                .filter(t -> !t.isEmpty())
                .toList();
    }
}

What does this code do?

  • toLowerCase() normalises casing so "Hello" and "hello" are treated as the same word.
  • replaceAll("[^a-z\\s]", "") removes anything that is not a lowercase letter or whitespace. Commas, question marks, and numbers all disappear.
  • split("\\s+") breaks the cleaned string on one or more whitespace characters, producing an array of individual words.
  • The final stream filters out empty strings that could sneak in from leading or trailing whitespace.
  • Save Tokenizer.java.
  • Confirm there are no red underlines in the editor. You will not see output yet because the next substep wires up a test.

Red underlines on the regex?

  • Java requires double backslashes in regex strings. Make sure your regex reads "[^a-z\\s]" with two backslashes, not one.
  • Verify that the imports for java.util.Arrays and java.util.List are both at the top of the file.

Still stuck? Ask for help.

✔️ Awesome, I've got everything!

Great. Make sure you have saved your file before moving on.

ⓧ I'd like to double check the full code

package com.classifier;

import java.util.Arrays;
import java.util.List;

public class Tokenizer {

    public static List<String> tokenize(String text) {
        String cleaned = text.toLowerCase().replaceAll("[^a-z\\s]", "");
        String[] tokens = cleaned.trim().split("\\s+");
        return Arrays.stream(tokens)
                .filter(t -> !t.isEmpty())
                .toList();
    }
}
Test tokenization manually

Time to prove your tokenizer works. You will update the existing App.java to call the tokenizer and print the result.

  • In the Project tool window, open App.java (inside src/main/java/com/classifier).
  • Replace the entire file contents with the following code:
package com.classifier;

public class App {

    public static void main(String[] args) {
        // Test the tokenizer with a sample sentence
        System.out.println(Tokenizer.tokenize("Hello, how are you?"));
    }
}

What does this code do?

This sends "Hello, how are you?" through your tokenizer and prints the resulting list to the console. If everything works, you will see the sentence converted to clean lowercase tokens with no punctuation.

  • Save App.java.
  • Click the green play icon in the gutter next to public static void main.
  • Select Run 'App.main()' from the menu that appears.
  • In the Run tool window at the bottom, you should see this output:
[hello, how, are, you]

The tokenizer lowercased everything, stripped the comma and question mark, and split the sentence into four clean tokens.

Not seeing the expected output?

  • If IntelliJ shows a compilation error mentioning Tokenizer, make sure Tokenizer.java is saved and that its package declaration matches package com.classifier;.
  • If the output looks different (extra characters or missing words), double-check the regex in your Tokenizer.java matches exactly: "[^a-z\\s]".
  • Make sure you are running the correct class. The green play icon should be next to the main method in App.java, not in another file.

Need more help debugging?

✔️ Awesome, I've got everything!

Great. Your tokenizer is working and ready for the classifier to use.

ⓧ I'd like to double check the full code

package com.classifier;

public class App {

    public static void main(String[] args) {
        System.out.println(Tokenizer.tokenize("Hello, how are you?"));
    }
}

Your training data is defined and your tokenizer is proven. Next, you will build the core Naive Bayes algorithm that learns patterns from this data and makes predictions.

Implement the Naive Bayes Training Algorithm

Your tokenizer can break sentences into clean word lists. Now you need the algorithm that learns from those words.

In this step, you will implement Naive Bayes, the classification algorithm that powers spam filters, chatbot intent detection, and sentiment analysis. It works by learning which words appear most often in each intent category, then using probability to predict which category a new sentence belongs to.

In this step, get ready to:
  • Create the NaiveBayesClassifier class with its data structures.
  • Implement the training algorithm with Laplace smoothing.
  • Verify the classifier trains successfully on your intent data.
Create the NaiveBayesClassifier class structure

The classifier needs four data structures to store everything it learns during training. These maps and sets hold the probabilities that drive predictions later.

  • Create a new Java class called NaiveBayesClassifier.java in src/main/java/com/classifier/.
  • Right-click the com.classifier package in the Project tool window, then select New > Java Class.
  • Name it NaiveBayesClassifier and press Enter.
  • Replace the generated file contents with this class skeleton:
package com.classifier;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class NaiveBayesClassifier {

    // Stores log P(class) for each intent
    private Map<String, Double> classPriors;
    // Stores log P(word|class) for every word in every intent
    private Map<String, Map<String, Double>> wordLikelihoods;
    // Stores log probability for words the model has never seen in a class
    private Map<String, Double> classDefaultLikelihood;
    // All unique words across all training data
    private Set<String> vocabulary;
}

What do these fields store?

  • classPriors stores how common each intent is. If "greeting" has 6 out of 24 total sentences, its prior is log(6/24).
  • wordLikelihoods stores the probability of each word appearing in each class. For example, the probability of "hello" given the class is "greeting".
  • classDefaultLikelihood handles words the model has never seen during training. Without a fallback, unseen words would make the entire probability zero.
  • vocabulary collects every unique word across all training sentences. The classifier uses this to know which words it has learned about.
  • Save NaiveBayesClassifier.java.

You will not see a change yet. The next chunk adds the training logic that populates these fields.

Implement the train method

The train method is where the math lives. It reads every training sentence, counts word frequencies per class, and converts those counts into log-probabilities using Laplace smoothing.

  • In NaiveBayesClassifier.java, add the following methods inside the class body, below the field declarations:
    public void train(Map<String, List<String>> trainingData) {
        classPriors = new HashMap<>();
        wordLikelihoods = new HashMap<>();
        classDefaultLikelihood = new HashMap<>();
        vocabulary = new HashSet<>();

        // Count total sentences across all classes for prior calculation
        int totalSentences = trainingData.values().stream()
                .mapToInt(List::size)
                .sum();

        // Temporary storage for word counts per class
        Map<String, Map<String, Integer>> classWordCounts = new HashMap<>();
        Map<String, Integer> classTotalWords = new HashMap<>();

        // First pass: compute priors and count word frequencies
        for (Map.Entry<String, List<String>> entry : trainingData.entrySet()) {
            String className = entry.getKey();
            List<String> sentences = entry.getValue();

            // Prior = log(sentences in this class / total sentences)
            double prior = Math.log((double) sentences.size() / totalSentences);
            classPriors.put(className, prior);

            Map<String, Integer> wordCounts = new HashMap<>();
            int totalWords = 0;

            // Tokenize each sentence and count every word
            for (String sentence : sentences) {
                List<String> tokens = Tokenizer.tokenize(sentence);
                for (String token : tokens) {
                    vocabulary.add(token);
                    wordCounts.merge(token, 1, Integer::sum);
                    totalWords++;
                }
            }

            classWordCounts.put(className, wordCounts);
            classTotalWords.put(className, totalWords);
        }

        int vocabSize = vocabulary.size();

        // Second pass: compute Laplace-smoothed log likelihoods
        for (String className : trainingData.keySet()) {
            Map<String, Integer> wordCounts = classWordCounts.get(className);
            int totalWords = classTotalWords.get(className);

            Map<String, Double> likelihoods = new HashMap<>();
            for (String word : vocabulary) {
                int count = wordCounts.getOrDefault(word, 0);
                // Laplace smoothing: (count + 1) / (total + vocabSize)
                double logLikelihood = Math.log((double) (count + 1) / (totalWords + vocabSize));
                likelihoods.put(word, logLikelihood);
            }

            wordLikelihoods.put(className, likelihoods);
            // Default for completely unseen words
            classDefaultLikelihood.put(className,
                    Math.log(1.0 / (totalWords + vocabSize)));
        }
    }

    public int getVocabularySize() {
        return vocabulary.size();
    }

    public int getClassCount() {
        return classPriors.size();
    }

What does this code do?

The train method implements the Multinomial Naive Bayes algorithm in two passes:

  • First pass: for each intent class, it computes the prior probability (how common is this intent?) and counts how often each word appears.
  • Second pass: it converts raw word counts into log-probabilities using Laplace smoothing. Every word gets +1 added to its count so that no probability is ever zero.
  • The helper methods getVocabularySize() and getClassCount() let you inspect what the model learned after training.

Why log probabilities instead of raw probabilities?

When you multiply many small probabilities together (one per word in a sentence), the result quickly becomes too small for a computer to store accurately. This is called floating-point underflow.

Adding logarithms of probabilities is mathematically equivalent to multiplying the original probabilities, but the numbers stay in a manageable range.

What is Laplace smoothing?

Without smoothing, if a word never appears in a class, its probability is 0. Multiplying by 0 wipes out all other evidence for that class. Laplace smoothing adds 1 to every word count, guaranteeing no probability is ever zero.

The formula is: P(word|class) = (count + 1) / (total words in class + vocabulary size).

  • Save NaiveBayesClassifier.java.

You will not see a change yet. The next substep wires up the classifier in your main method to confirm training works.

Seeing red underlines in IntelliJ?

  • Make sure the methods are inside the class braces but outside any other method.
  • Check that all six imports are at the top of the file: HashMap, HashSet, List, Map, and Set.
  • Confirm the package declaration says package com.classifier; matching your folder structure.

Help me fix compiler errors in my NaiveBayesClassifier class.

Verify training completes

Time to wire up the classifier and confirm it can learn from your training data without errors.

  • Switch back to App.java and replace the entire file contents with this code:
package com.classifier;

public class App {

    public static void main(String[] args) {
        // Create and train the classifier on our intent data
        NaiveBayesClassifier classifier = new NaiveBayesClassifier();
        classifier.train(TrainingData.getData());

        // Print stats to verify training succeeded
        System.out.println("Vocabulary size: " + classifier.getVocabularySize());
        System.out.println("Classes: " + classifier.getClassCount());
    }
}

What does this code do?

This creates a new classifier instance, feeds it all 24 training sentences across 4 intents, and prints two numbers that prove training worked: the vocabulary size (all unique words it learned) and the class count (number of intent categories).

  • Save App.java.
  • Run the application by clicking the green play button next to the main method, or press Ctrl+Shift+F10 (Windows) or Ctrl+Shift+R (macOS).
  • In the Run output at the bottom of IntelliJ, you should see output similar to:
Vocabulary size: 43
Classes: 4

Your exact vocabulary size may differ slightly (expect somewhere between 40-50 words), but the class count should be exactly 4.

Seeing a compilation error?

  • If you see "cannot find symbol" for NaiveBayesClassifier, make sure the class file is in the same package (com.classifier) and saved.
  • If you see "cannot find symbol" for getVocabularySize or getClassCount, confirm you added both helper methods inside the class body after the train method.
  • If you see a NullPointerException at runtime, the fields were not initialized. Make sure the first four lines inside your train method create new instances for each map and set.

Help me debug my classifier training errors.

✔️ Awesome, I've got everything!

Great. Make sure you have saved both NaiveBayesClassifier.java and App.java.

ⓧ I'd like to double check the full code

Here is the complete NaiveBayesClassifier.java:

package com.classifier;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class NaiveBayesClassifier {

    private Map<String, Double> classPriors;
    private Map<String, Map<String, Double>> wordLikelihoods;
    private Map<String, Double> classDefaultLikelihood;
    private Set<String> vocabulary;

    public void train(Map<String, List<String>> trainingData) {
        classPriors = new HashMap<>();
        wordLikelihoods = new HashMap<>();
        classDefaultLikelihood = new HashMap<>();
        vocabulary = new HashSet<>();

        int totalSentences = trainingData.values().stream()
                .mapToInt(List::size)
                .sum();

        Map<String, Map<String, Integer>> classWordCounts = new HashMap<>();
        Map<String, Integer> classTotalWords = new HashMap<>();

        for (Map.Entry<String, List<String>> entry : trainingData.entrySet()) {
            String className = entry.getKey();
            List<String> sentences = entry.getValue();

            double prior = Math.log((double) sentences.size() / totalSentences);
            classPriors.put(className, prior);

            Map<String, Integer> wordCounts = new HashMap<>();
            int totalWords = 0;

            for (String sentence : sentences) {
                List<String> tokens = Tokenizer.tokenize(sentence);
                for (String token : tokens) {
                    vocabulary.add(token);
                    wordCounts.merge(token, 1, Integer::sum);
                    totalWords++;
                }
            }

            classWordCounts.put(className, wordCounts);
            classTotalWords.put(className, totalWords);
        }

        int vocabSize = vocabulary.size();

        for (String className : trainingData.keySet()) {
            Map<String, Integer> wordCounts = classWordCounts.get(className);
            int totalWords = classTotalWords.get(className);

            Map<String, Double> likelihoods = new HashMap<>();
            for (String word : vocabulary) {
                int count = wordCounts.getOrDefault(word, 0);
                double logLikelihood = Math.log((double) (count + 1) / (totalWords + vocabSize));
                likelihoods.put(word, logLikelihood);
            }

            wordLikelihoods.put(className, likelihoods);
            classDefaultLikelihood.put(className,
                    Math.log(1.0 / (totalWords + vocabSize)));
        }
    }

    public int getVocabularySize() {
        return vocabulary.size();
    }

    public int getClassCount() {
        return classPriors.size();
    }
}

Here is the complete App.java:

package com.classifier;

public class App {

    public static void main(String[] args) {
        NaiveBayesClassifier classifier = new NaiveBayesClassifier();
        classifier.train(TrainingData.getData());
        System.out.println("Vocabulary size: " + classifier.getVocabularySize());
        System.out.println("Classes: " + classifier.getClassCount());
    }
}

Your classifier now has a trained brain full of probabilities. Next up, you will teach it to use those probabilities to predict the intent of any sentence you type.

Implement Prediction and Build the Interactive Console

Your classifier has a trained brain full of word probabilities, but no way to use them yet.

In this step, you will build the prediction engine that takes any sentence the user types, scores it against every intent using the trained probabilities, and returns the best match. Then you will wrap it in an interactive console loop so you can chat with your AI in real time.

In this step, get ready to:
  • Implement the predict method that scores sentences against all intents.
  • Build an interactive console loop for real-time classification.
  • Test the classifier with both seen and unseen sentences.
Implement the predict method

The prediction algorithm is the mirror image of training. During training, you computed P(word|class) for every word in every class. During prediction, you multiply those probabilities together (by adding log values) to find which class best explains the input sentence.

Before writing the predict method, you need a small class to hold the result. Java records are perfect for this.

  • Create a new file called PredictionResult.java in src/main/java/com/classifier/ and add the following code:
package com.classifier;

public record PredictionResult(String intent, double score) {

    @Override
    public String toString() {
        return String.format("Intent: %s (score: %.2f)", intent, score);
    }
}

What does this code do?

A Java record automatically generates a constructor, getters, and equals/hashCode for the fields you declare.

This record holds two values: the predicted intent name (like "greeting") and the log-probability score that the model computed. The toString() override formats the output so it reads nicely in the console.

  • Save PredictionResult.java.

Now you will add the actual prediction logic to your classifier. The algorithm loops through every class, adds up the log-probabilities for each word in the input, and picks the class with the highest total score.

  • Open NaiveBayesClassifier.java and add the following predict method below the closing brace of the train method:
    public PredictionResult predict(String sentence) {
        List<String> tokens = Tokenizer.tokenize(sentence);

        String bestClass = null;
        double bestScore = Double.NEGATIVE_INFINITY;

        for (String className : classPriors.keySet()) {
            double score = classPriors.get(className);

            Map<String, Double> likelihoods = wordLikelihoods.get(className);
            double defaultLikelihood = classDefaultLikelihood.get(className);

            for (String token : tokens) {
                if (vocabulary.contains(token)) {
                    score += likelihoods.get(token);
                } else {
                    score += defaultLikelihood;
                }
            }

            if (score > bestScore) {
                bestScore = score;
                bestClass = className;
            }
        }

        return new PredictionResult(bestClass, bestScore);
    }

What does this code do?

  • Tokenizer.tokenize(sentence) preprocesses the input the same way training data was processed (lowercase, remove punctuation, split into words).
  • For each class, the score starts at the class prior (log P(class)), then each word's log-likelihood is added.
  • If a word was never seen during training, defaultLikelihood is used instead. This is the Laplace-smoothed probability for unknown words.
  • The class with the highest cumulative score wins. This is the argmax step of the Naive Bayes algorithm.

Why log scores instead of raw probabilities?

Multiplying many small probabilities together causes floating-point underflow (the number becomes so tiny Java rounds it to zero). Adding logarithms is mathematically equivalent and keeps the numbers in a range the computer can handle.

  • Save NaiveBayesClassifier.java.

Seeing a compile error?

Make sure the predict method is placed inside the NaiveBayesClassifier class body (after the train method's closing brace, but before the class's final closing brace).

Also confirm that PredictionResult.java is saved in the same com.classifier package.

Help me fix a compile error in my NaiveBayesClassifier

Implement the interactive console loop

The classifier can now predict intents, but you need a way to feed it sentences interactively. You will replace the temporary print statements in App.java with a loop that reads user input and prints predictions until the user types "quit".

  • Replace the entire contents of App.java with the following code:
package com.classifier;

import java.util.Scanner;

public class App {

    public static void main(String[] args) {
        // Train the classifier on startup
        NaiveBayesClassifier classifier = new NaiveBayesClassifier();
        classifier.train(TrainingData.getData());

        System.out.println("Intent Classifier Ready!");
        System.out.println("Vocabulary size: " + classifier.getVocabularySize());
        System.out.println("Classes: " + classifier.getClassCount());
        System.out.println();

        // Set up the interactive input loop
        Scanner scanner = new Scanner(System.in);

        while (true) {
            System.out.print("Type a sentence (or 'quit' to exit): ");
            String input = scanner.nextLine().trim();

            // Exit condition
            if (input.equalsIgnoreCase("quit")) {
                System.out.println("Goodbye!");
                break;
            }

            // Skip empty input
            if (input.isEmpty()) {
                continue;
            }

            // Classify and display result
            PredictionResult result = classifier.predict(input);
            System.out.println(">> " + result);
            System.out.println();
        }

        scanner.close();
    }
}

What does this code do?

  • The classifier trains immediately when the app starts, then prints a ready message with the vocabulary size and class count.
  • A Scanner reads from System.in (the keyboard). The while (true) loop keeps asking for input until the user types "quit".
  • Each non-empty input is passed to classifier.predict(input), and the formatted result is printed with a >> prefix.
  • Save App.java.

Import error on Scanner?

Make sure the import java.util.Scanner; line is at the top of the file, right after the package declaration.

Help me fix an import error in App.java

Test with various sentences

Time to see your AI in action. You will run the application and test it with sentences the model has seen, sentences it has never seen, and ambiguous sentences.

  • Run App.java by clicking the green play button next to the main method in IntelliJ IDEA.
  • You should see this output in the console:
Intent Classifier Ready!
Vocabulary size: 42
Classes: 4

Type a sentence (or 'quit' to exit):

What if my vocabulary size is slightly different?

The vocabulary size depends on the exact number of unique words across all training sentences. A value in the range of 40-50 is expected. The exact number may vary slightly.

  • Type hello and press Enter. You should see it predict "greeting".
  • Type bye and press Enter. You should see it predict "goodbye".
  • Type thanks a lot and press Enter. You should see it predict "thanks".
  • Type can you help me and press Enter. You should see it predict "help".

Now try a sentence the model has never seen before.

  • Type good evening friend and press Enter. It should still predict "greeting" because "good" appears in the greeting training data.
  • Type quit to exit the application.

Why does it classify unseen sentences correctly?

The model does not memorize full sentences. It scores individual words. If even one word in your input has a high probability in a particular class, that class gets a boost.

The word "good" appeared in "good morning" during training, so it has a higher likelihood in the "greeting" class than in any other class. This is why "good evening friend" still maps to greeting.

Application exits immediately without waiting for input?

Make sure you are running the application from IntelliJ IDEA's built-in terminal (the **Run** tool window), not an external terminal. IntelliJ needs to attach System.in to the console for Scanner to read input.

Help me fix my interactive console not accepting input

✔️ Awesome, I've got everything!

Great. Double check you have saved all three files.

ⓧ I'd like to double check the full code

Here is the full code for each file at this point in the project.

package com.classifier;

public record PredictionResult(String intent, double score) {

    @Override
    public String toString() {
        return String.format("Intent: %s (score: %.2f)", intent, score);
    }
}
package com.classifier;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class NaiveBayesClassifier {

    private Map<String, Double> classPriors;
    private Map<String, Map<String, Double>> wordLikelihoods;
    private Map<String, Double> classDefaultLikelihood;
    private Set<String> vocabulary;

    public void train(Map<String, List<String>> trainingData) {
        classPriors = new HashMap<>();
        wordLikelihoods = new HashMap<>();
        classDefaultLikelihood = new HashMap<>();
        vocabulary = new HashSet<>();

        int totalSentences = trainingData.values().stream()
                .mapToInt(List::size)
                .sum();

        Map<String, Map<String, Integer>> classWordCounts = new HashMap<>();
        Map<String, Integer> classTotalWords = new HashMap<>();

        for (Map.Entry<String, List<String>> entry : trainingData.entrySet()) {
            String className = entry.getKey();
            List<String> sentences = entry.getValue();

            double prior = Math.log((double) sentences.size() / totalSentences);
            classPriors.put(className, prior);

            Map<String, Integer> wordCounts = new HashMap<>();
            int totalWords = 0;

            for (String sentence : sentences) {
                List<String> tokens = Tokenizer.tokenize(sentence);
                for (String token : tokens) {
                    vocabulary.add(token);
                    wordCounts.merge(token, 1, Integer::sum);
                    totalWords++;
                }
            }

            classWordCounts.put(className, wordCounts);
            classTotalWords.put(className, totalWords);
        }

        int vocabSize = vocabulary.size();

        for (String className : trainingData.keySet()) {
            Map<String, Integer> wordCounts = classWordCounts.get(className);
            int totalWords = classTotalWords.get(className);

            Map<String, Double> likelihoods = new HashMap<>();
            for (String word : vocabulary) {
                int count = wordCounts.getOrDefault(word, 0);
                double logLikelihood = Math.log((double) (count + 1) / (totalWords + vocabSize));
                likelihoods.put(word, logLikelihood);
            }

            wordLikelihoods.put(className, likelihoods);
            classDefaultLikelihood.put(className,
                    Math.log(1.0 / (totalWords + vocabSize)));
        }
    }

    public PredictionResult predict(String sentence) {
        List<String> tokens = Tokenizer.tokenize(sentence);

        String bestClass = null;
        double bestScore = Double.NEGATIVE_INFINITY;

        for (String className : classPriors.keySet()) {
            double score = classPriors.get(className);

            Map<String, Double> likelihoods = wordLikelihoods.get(className);
            double defaultLikelihood = classDefaultLikelihood.get(className);

            for (String token : tokens) {
                if (vocabulary.contains(token)) {
                    score += likelihoods.get(token);
                } else {
                    score += defaultLikelihood;
                }
            }

            if (score > bestScore) {
                bestScore = score;
                bestClass = className;
            }
        }

        return new PredictionResult(bestClass, bestScore);
    }

    public int getVocabularySize() {
        return vocabulary.size();
    }

    public int getClassCount() {
        return classPriors.size();
    }
}
package com.classifier;

import java.util.Scanner;

public class App {

    public static void main(String[] args) {
        NaiveBayesClassifier classifier = new NaiveBayesClassifier();
        classifier.train(TrainingData.getData());

        System.out.println("Intent Classifier Ready!");
        System.out.println("Vocabulary size: " + classifier.getVocabularySize());
        System.out.println("Classes: " + classifier.getClassCount());
        System.out.println();

        Scanner scanner = new Scanner(System.in);

        while (true) {
            System.out.print("Type a sentence (or 'quit' to exit): ");
            String input = scanner.nextLine().trim();

            if (input.equalsIgnoreCase("quit")) {
                System.out.println("Goodbye!");
                break;
            }

            if (input.isEmpty()) {
                continue;
            }

            PredictionResult result = classifier.predict(input);
            System.out.println(">> " + result);
            System.out.println();
        }

        scanner.close();
    }
}

Your AI can now understand the intent behind any sentence you throw at it. Next up in the Secret Mission, you will add confidence percentages and teach it a brand new intent.

Secret mission

Add Confidence Scores and a New Intent

Your classifier says "greeting" but how sure is it? Right now the raw log-probability score is hard for humans to interpret. In this mission you will convert those scores into confidence percentages using the softmax function, and teach your AI a brand new intent category.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your project files for future experimentation or delete them to free up disk space.

Resources you used:

  • The intent-classifier Maven project folder (contains all Java source files, pom.xml, and compiled classes).

Keep everything running

No action needed. Choose this if you want to keep experimenting with new intents, training data, or classification approaches.

  • Your intent-classifier project remains on your machine wherever IntelliJ IDEA created it.
  • You can reopen it anytime from IntelliJ IDEA's welcome screen under recent projects.

Pause - I'll come back to this later

Close IntelliJ IDEA to free up memory. Your project files stay on disk and you can reopen them later.

  • Close IntelliJ IDEA.
  • Your project files remain saved on disk. When you want to return, open IntelliJ IDEA and select the intent-classifier project from the welcome screen.

Delete - I don't want to use this again

Remove all project files from your machine. This cannot be undone.

  • Close IntelliJ IDEA.
  • Find your project folder. In IntelliJ IDEA, you can check the folder location in the title bar or by right-clicking the project root and selecting Open In > Finder (macOS) or Explorer (Windows) before closing.
  • Delete the intent-classifier folder. On macOS/Linux, open Terminal and run:
rm -rf ~/IdeaProjects/intent-classifier

On Windows, open PowerShell and run:

Remove-Item -Recurse -Force ~\IdeaProjects\intent-classifier

Check your project path first

IntelliJ IDEA defaults to ~/IdeaProjects/ on most systems. If you chose a different location when creating the project, adjust the path in the command above to match.

Nice Work!

Nice Work!

Nice work! You just built a fully functional AI text classifier from pure math and Java, with zero machine learning libraries.

You've learned how to:

  • Implement the Multinomial Naive Bayes algorithm from scratch using Bayes' theorem with Laplace smoothing and log-probability scoring.
  • Build a text tokenizer that preprocesses natural language into machine-readable tokens for classification.
  • Create an interactive console application that classifies user input into intents in real time using probability.
  • Secret Mission: Added softmax confidence scoring and extended the model with a fifth weather intent.

Ready to quiz yourself?