Build a Neural Network in Java

Implement a feedforward neural network from scratch to solve XOR.

Introduction

30 Second Summary

Every time your phone unlocks with your face or a chatbot answers your question, a neural network is doing the heavy lifting behind the scenes. Most people treat these networks as magic, but they follow a straightforward mathematical recipe.

In this project, you will build a complete neural network from scratch in pure Java. No libraries, no frameworks. Just you, the math, and a program that learns to solve a problem on its own.

What You'll Build

A single-file Java program implementing a 2-2-1 feedforward neural network that learns the XOR function through backpropagation.

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

  • A working feedforward neural network implemented in pure Java with zero external libraries.
  • A trained model that correctly solves the XOR problem, proving backpropagation works.
  • A deep, code-level understanding of how forward propagation, loss computation, and gradient descent weight updates connect.
  • Secret Mission: Expand to a 2-3-1 architecture and experiment with learning rate tuning to observe convergence behavior.

Are there any prerequisites?

You should be comfortable with basic Java concepts like classes, arrays, and methods. Familiarity with calculus (derivatives and the chain rule) will help you understand the math behind weight updates, but the project walks through each formula step by step.

Set Up Your Project

Before writing any neural network logic, you need a verified Java environment and a clean project folder.

In this step, you'll confirm that the JDK compiler and runtime are accessible from VS Code's terminal, then create the single file that will hold your entire neural network.

In this step, get ready to:
  • Verify Java is ready in VS Code's integrated terminal.
  • Create the project folder and NeuralNetwork.java starter file.
Verify your Java installation

The Java Development Kit includes two commands you'll use throughout this project: java (which runs compiled programs) and javac (which compiles your source code into bytecode).

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • Open the integrated terminal by pressing Ctrl+` (backtick key, same shortcut on both macOS and Windows).
  • Check your Java runtime version by running this command:
java -version

You should see output containing 17.0.1 in the version string.

  • Verify the compiler is also available by running:
javac -version

You should see javac 17.0.1 (or a similar format confirming version 17.0.1).

Command not found?

If your terminal does not recognize java or javac, your JDK installation may not be on your system PATH. Close and reopen VS Code to refresh environment variables.

If that doesn't help, help me troubleshoot my Java PATH setup.

Create the project folder and starter file

Your neural network lives in a single Java file inside its own folder. No build tools or dependencies needed.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir neural-network
cd neural-network
  • Confirm the folder exists by running:
ls ~/Desktop

You should see neural-network listed in the output.

  • Create the Java source file by running:
touch NeuralNetwork.java
  • Open this file in VS Code by running:
code NeuralNetwork.java

Opening the folder instead

You can also click File in the top menu bar, then Open Folder, and select the neural-network folder on your Desktop. This gives you the file explorer sidebar for easy navigation.

  • In NeuralNetwork.java, paste the following class skeleton:
import java.util.Random;

public class NeuralNetwork {

    public static void main(String[] args) {
        System.out.println("Hello from NeuralNetwork!");
    }
}

What does this code do?

import java.util.Random brings in the random number generator you'll use later to initialize weights. You won't use it yet, but having it ready avoids a recompile later.

The main method is the entry point Java calls when you run the program. For now it just prints a message to confirm everything is wired up.

  • Save NeuralNetwork.java by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Compile the file by running:
javac NeuralNetwork.java
  • Run the compiled program:
java NeuralNetwork

You should see Hello from NeuralNetwork! printed in your terminal.

Seeing a compilation error?

Check that the filename is exactly NeuralNetwork.java with a capital N in both words. Java requires the filename to match the public class name.

Also make sure you're running javac from the same folder where the file is saved. Run ls to confirm NeuralNetwork.java appears in the current directory.

Still stuck? Help me fix my Java compilation error.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file, then move on to the next step.

ⓧ I'd like to double check the full code

Here is the complete NeuralNetwork.java file at this point:

import java.util.Random;

public class NeuralNetwork {

    public static void main(String[] args) {
        System.out.println("Hello from NeuralNetwork!");
    }
}

Your environment is ready and your skeleton compiles cleanly. Next up, you'll declare the network's weights and build the forward pass that turns inputs into predictions.

Build the Forward Pass

Your Java environment is ready and your file compiles cleanly. Now it's time to give your neural network a brain.

A forward pass is how a neural network transforms inputs into a prediction. Data flows from the input layer, through a hidden layer of neurons, and out to an output neuron. Each connection has a weight that controls how much signal passes through, and each neuron has a bias that shifts its activation threshold.

In this step, get ready to:
  • Declare the network's 9 trainable parameters (weights and biases) and initialize them randomly.
  • Implement the sigmoid activation function and its derivative.
  • Build the forward pass method and test it on XOR inputs.
Declare weights, biases, and the constructor

Your 2-2-1 network has 9 trainable parameters. Four weights connect the 2 inputs to 2 hidden neurons. Two weights connect the hidden neurons to the single output. Three biases give each neuron its own activation threshold.

  • In NeuralNetwork.java, find the opening class brace. Add the following instance variables and constructor directly below it (above the existing main method):
    // Weights: input to hidden layer
    private double w1, w2, w3, w4;
    // Weights: hidden to output layer
    private double w5, w6;
    // Biases: hidden neurons and output neuron
    private double b1, b2, b3;
    // Learning rate
    private double learningRate;

    public NeuralNetwork(double learningRate) {
        this.learningRate = learningRate;
        Random rand = new Random();
        w1 = rand.nextGaussian();
        w2 = rand.nextGaussian();
        w3 = rand.nextGaussian();
        w4 = rand.nextGaussian();
        w5 = rand.nextGaussian();
        w6 = rand.nextGaussian();
        b1 = rand.nextGaussian();
        b2 = rand.nextGaussian();
        b3 = rand.nextGaussian();
    }

What does this code do?

The instance variables declare all 9 parameters the network will learn to adjust. Right now they start as random numbers from a Gaussian distribution (mean 0, standard deviation 1).

The constructor accepts a learningRate that controls how aggressively the network updates its weights during training. nextGaussian() gives each weight a small random starting value so that no two neurons behave identically.

Why random initialization?

If all weights started at zero, every neuron would compute the same output and receive the same gradient update. The network would never learn anything useful. Random values break this symmetry.

  • Save NeuralNetwork.java.

Seeing a compile error?

Make sure the instance variables and constructor sit inside the class braces but above the main method. The import java.util.Random line should already be at the top of your file from Step 1.

Help me fix a compile error in my NeuralNetwork constructor

Implement the sigmoid activation function

Neurons need a way to squash their raw output into a useful range. The sigmoid function maps any number to a value between 0 and 1, making it a natural fit for binary classification problems like XOR.

  • Below the constructor, add these two methods:
    private double sigmoid(double x) {
        return 1.0 / (1.0 + Math.exp(-x));
    }

    private double sigmoidDerivative(double sigmoidOutput) {
        return sigmoidOutput * (1.0 - sigmoidOutput);
    }

What does this code do?

  • sigmoid(double x) computes 1 / (1 + e^(-x)). Large positive inputs produce values near 1. Large negative inputs produce values near 0.
  • sigmoidDerivative(double sigmoidOutput) computes how steeply the sigmoid curve is changing at a given point. You will need this during backpropagation in the next step to calculate how much each weight contributed to the error.
  • Save NeuralNetwork.java.

Cannot find symbol Math.exp?

This should not happen since Math is part of java.lang and requires no import. Double-check that you spelled Math.exp with a capital M and that the method sits inside the class body.

Help me fix a Math.exp error in my sigmoid method

Implement the forward pass and test with XOR

The forward pass chains the sigmoid activation across two layers. Each hidden neuron takes both inputs, multiplies them by its own weights, adds its bias, and squishes the result through sigmoid. The output neuron does the same thing using the hidden layer's outputs as its inputs.

  • Below the sigmoidDerivative method, add the forward method:
    public double[] forward(double x1, double x2) {
        double h1 = sigmoid(x1 * w1 + x2 * w2 + b1);
        double h2 = sigmoid(x1 * w3 + x2 * w4 + b2);
        double output = sigmoid(h1 * w5 + h2 * w6 + b3);
        return new double[]{h1, h2, output};
    }

What does this code do?

  • h1 is the first hidden neuron's output. It combines both inputs with weights w1 and w2, adds bias b1, then passes the sum through sigmoid.
  • h2 is the second hidden neuron. It uses a different pair of weights (w3, w4) so it learns a different feature of the input.
  • output combines the hidden neurons' outputs using weights w5 and w6. This is the network's final prediction.
  • The method returns all three values because backpropagation will need h1 and h2 to calculate gradients.

Now you need to update main to construct the network, define the XOR dataset, and print predictions.

  • Find the existing main method. Replace its entire body (everything between the method's curly braces) with the following:
    public static void main(String[] args) {
        NeuralNetwork nn = new NeuralNetwork(2.0);

        // XOR training data
        double[][] inputs = {
            {0, 0},
            {0, 1},
            {1, 0},
            {1, 1}
        };
        double[] targets = {0, 1, 1, 0};

        System.out.println("Initial (untrained) predictions:");
        for (int i = 0; i < inputs.length; i++) {
            double[] result = nn.forward(inputs[i][0], inputs[i][1]);
            System.out.printf("Input: [%.0f, %.0f] -> Output: %.4f (Expected: %.0f)%n",
                inputs[i][0], inputs[i][1], result[2], targets[i]);
        }
    }

What does this code do?

  • new NeuralNetwork(2.0) creates the network with a learning rate of 2.0. This rate is high compared to typical deep learning (where you might use 0.001), but works well for a tiny 4-sample dataset with only 9 parameters.
  • The inputs array holds the four XOR truth table rows. The targets array holds the expected output for each row: 0 XOR 0 = 0, 0 XOR 1 = 1, 1 XOR 0 = 1, 1 XOR 1 = 0.
  • The loop calls forward on each input pair and prints result[2] (the output neuron's value).

Why does XOR need a hidden layer?

Simple problems like AND or OR can be solved by drawing a single straight line through the input space. XOR cannot. The two "true" cases (0,1 and 1,0) sit diagonally opposite the two "false" cases (0,0 and 1,1). The hidden layer lets the network learn two separate boundaries and combine them.

  • Save NeuralNetwork.java.
  • Compile and run the program by running these commands in your terminal:
javac NeuralNetwork.java
java NeuralNetwork

You should see four lines of output, each showing an input pair and a prediction value between 0 and 1. The predictions will be random and wrong. That is expected since the network has not been trained yet.

Seeing identical outputs for every input?

If all four predictions are the same number, check that you are using different weight pairs for each hidden neuron. Neuron 1 should use w1 and w2. Neuron 2 should use w3 and w4. If both neurons share the same weights, they produce identical outputs.

Help me debug why my neural network outputs the same value for every input

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete NeuralNetwork.java file at this point:

import java.util.Random;

public class NeuralNetwork {

    // Weights: input to hidden layer
    private double w1, w2, w3, w4;
    // Weights: hidden to output layer
    private double w5, w6;
    // Biases: hidden neurons and output neuron
    private double b1, b2, b3;
    // Learning rate
    private double learningRate;

    public NeuralNetwork(double learningRate) {
        this.learningRate = learningRate;
        Random rand = new Random();
        w1 = rand.nextGaussian();
        w2 = rand.nextGaussian();
        w3 = rand.nextGaussian();
        w4 = rand.nextGaussian();
        w5 = rand.nextGaussian();
        w6 = rand.nextGaussian();
        b1 = rand.nextGaussian();
        b2 = rand.nextGaussian();
        b3 = rand.nextGaussian();
    }

    private double sigmoid(double x) {
        return 1.0 / (1.0 + Math.exp(-x));
    }

    private double sigmoidDerivative(double sigmoidOutput) {
        return sigmoidOutput * (1.0 - sigmoidOutput);
    }

    public double[] forward(double x1, double x2) {
        double h1 = sigmoid(x1 * w1 + x2 * w2 + b1);
        double h2 = sigmoid(x1 * w3 + x2 * w4 + b2);
        double output = sigmoid(h1 * w5 + h2 * w6 + b3);
        return new double[]{h1, h2, output};
    }

    public static void main(String[] args) {
        NeuralNetwork nn = new NeuralNetwork(2.0);

        double[][] inputs = {
            {0, 0},
            {0, 1},
            {1, 0},
            {1, 1}
        };
        double[] targets = {0, 1, 1, 0};

        System.out.println("Initial (untrained) predictions:");
        for (int i = 0; i < inputs.length; i++) {
            double[] result = nn.forward(inputs[i][0], inputs[i][1]);
            System.out.printf("Input: [%.0f, %.0f] -> Output: %.4f (Expected: %.0f)%n",
                inputs[i][0], inputs[i][1], result[2], targets[i]);
        }
    }
}

Your network can now think. It is producing wrong answers, but it is producing them by computing weighted sums through layers of neurons. Next up, you will teach it to learn from its mistakes with backpropagation.

Implement Backpropagation

In the previous step, you built the forward pass. The network can compute outputs from inputs, but it has no way to learn from its mistakes.

Backpropagation fixes this. It measures how wrong the network is, then works backward through the layers to figure out how much each weight contributed to the error. Each weight gets nudged in the direction that reduces the mistake.

In this step, get ready to:
  • Implement a training loop that runs forward passes and computes error.
  • Compute deltas for each layer using the chain rule.
  • Update all weights and biases using gradient descent.
Add the train method

The train method is where the network actually learns. It loops through the training data thousands of times, comparing its predictions to the correct answers and adjusting weights after each sample.

  • In NeuralNetwork.java, find the closing brace of your forward method.
  • Add the following train method below it:
    public void train(double[][] inputs, double[] targets, int epochs) {
        for (int epoch = 0; epoch < epochs; epoch++) {
            double totalLoss = 0.0;

            for (int i = 0; i < inputs.length; i++) {
                double x1 = inputs[i][0];
                double x2 = inputs[i][1];
                double target = targets[i];

                // Forward pass
                double h1 = sigmoid(x1 * w1 + x2 * w2 + b1);
                double h2 = sigmoid(x1 * w3 + x2 * w4 + b2);
                double output = sigmoid(h1 * w5 + h2 * w6 + b3);

                // Calculate error
                double error = target - output;
                totalLoss += error * error;

                // Backpropagation: output layer delta
                double deltaOutput = error * sigmoidDerivative(output);

                // Backpropagation: hidden layer deltas
                double deltaH1 = deltaOutput * w5 * sigmoidDerivative(h1);
                double deltaH2 = deltaOutput * w6 * sigmoidDerivative(h2);

                // Update weights: hidden to output
                w5 += learningRate * deltaOutput * h1;
                w6 += learningRate * deltaOutput * h2;
                b3 += learningRate * deltaOutput;

                // Update weights: input to hidden neuron 1
                w1 += learningRate * deltaH1 * x1;
                w2 += learningRate * deltaH1 * x2;
                b1 += learningRate * deltaH1;

                // Update weights: input to hidden neuron 2
                w3 += learningRate * deltaH2 * x1;
                w4 += learningRate * deltaH2 * x2;
                b2 += learningRate * deltaH2;
            }
        }
    }

What does this method do at a high level?

The outer loop runs for the specified number of epochs (full passes through the dataset). The inner loop processes each training sample one at a time.

For each sample, the method performs a forward pass (computing h1, h2, and output inline), measures the error, computes how much each weight contributed to that error, and nudges every weight in the direction that reduces the mistake.

  • Save NeuralNetwork.java.
  • Compile by running:
javac NeuralNetwork.java
  • You should see no output if compilation succeeds. If you see errors, check that the method is placed between the closing brace of forward and the main method.

Seeing a compilation error?

  • Check that you placed the method between the forward method's closing brace and the main method. It should be at the same indentation level as forward.
  • Count your braces. The method has three opening braces (method, outer loop, inner loop) and three corresponding closing braces.
  • Make sure all variable names match exactly: deltaOutput, deltaH1, deltaH2.

Help me debug a compilation error in my train method.

Trace how error flows backward through the network

The delta computation is the mathematical core of backpropagation. Focus on these lines in your train method:

                // Backpropagation: output layer delta
                double deltaOutput = error * sigmoidDerivative(output);

                // Backpropagation: hidden layer deltas
                double deltaH1 = deltaOutput * w5 * sigmoidDerivative(h1);
                double deltaH2 = deltaOutput * w6 * sigmoidDerivative(h2);

What is the chain rule doing here?

The chain rule answers: "how much did THIS neuron contribute to the final error?" It works by multiplying responsibility backward through the network.

For the output neuron, its delta is simply: (how wrong were we) times (how sensitive is sigmoid at this point). That second factor is sigmoidDerivative(output).

For hidden neurons, the error traveled through a weight to get here. So the hidden delta multiplies the output delta by the connecting weight (w5 or w6) and by that hidden neuron's own sigmoid sensitivity. Error responsibility flows backward along the same paths the signal traveled forward.

Trace how weights update via gradient descent

Once each neuron knows its delta (how responsible it was for the error), the weight updates follow a single formula. Focus on these lines:

                // Update weights: hidden to output
                w5 += learningRate * deltaOutput * h1;
                w6 += learningRate * deltaOutput * h2;
                b3 += learningRate * deltaOutput;

                // Update weights: input to hidden neuron 1
                w1 += learningRate * deltaH1 * x1;
                w2 += learningRate * deltaH1 * x2;
                b1 += learningRate * deltaH1;

                // Update weights: input to hidden neuron 2
                w3 += learningRate * deltaH2 * x1;
                w4 += learningRate * deltaH2 * x2;
                b2 += learningRate * deltaH2;

Why does every update follow the same pattern?

Every weight update is: weight += learningRate * delta * inputToThatWeight. This is gradient descent in action.

  • The learning rate controls how big each step is.
  • The delta captures how wrong this neuron was (its share of the blame).
  • The input to that weight determines direction. A large input means that weight had a large effect, so it gets a larger correction.
  • Biases use the same formula with an implicit input of 1, which is why their update simplifies to learningRate * delta.

Why update hidden-to-output weights before input-to-hidden?

The hidden-to-output weights (w5, w6) are updated first because the hidden deltas (deltaH1, deltaH2) were computed using the OLD values of w5 and w6. If you updated w5 and w6 after using them to compute deltas, you avoid stale-value bugs.

✔️ Awesome, I've got everything!

Great. Make sure you've saved NeuralNetwork.java and that it compiles cleanly.

ⓧ I'd like to double check the full code

Here is the full NeuralNetwork.java file as it should look after this step:

import java.util.Random;

public class NeuralNetwork {

    // Weights: input to hidden layer
    private double w1, w2, w3, w4;
    // Weights: hidden to output layer
    private double w5, w6;
    // Biases: hidden neurons and output neuron
    private double b1, b2, b3;
    // Learning rate
    private double learningRate;

    public NeuralNetwork(double learningRate) {
        this.learningRate = learningRate;
        Random rand = new Random();
        w1 = rand.nextGaussian();
        w2 = rand.nextGaussian();
        w3 = rand.nextGaussian();
        w4 = rand.nextGaussian();
        w5 = rand.nextGaussian();
        w6 = rand.nextGaussian();
        b1 = rand.nextGaussian();
        b2 = rand.nextGaussian();
        b3 = rand.nextGaussian();
    }

    private double sigmoid(double x) {
        return 1.0 / (1.0 + Math.exp(-x));
    }

    private double sigmoidDerivative(double sigmoidOutput) {
        return sigmoidOutput * (1.0 - sigmoidOutput);
    }

    public double[] forward(double x1, double x2) {
        double h1 = sigmoid(x1 * w1 + x2 * w2 + b1);
        double h2 = sigmoid(x1 * w3 + x2 * w4 + b2);
        double output = sigmoid(h1 * w5 + h2 * w6 + b3);
        return new double[]{h1, h2, output};
    }

    public void train(double[][] inputs, double[] targets, int epochs) {
        for (int epoch = 0; epoch < epochs; epoch++) {
            double totalLoss = 0.0;

            for (int i = 0; i < inputs.length; i++) {
                double x1 = inputs[i][0];
                double x2 = inputs[i][1];
                double target = targets[i];

                // Forward pass
                double h1 = sigmoid(x1 * w1 + x2 * w2 + b1);
                double h2 = sigmoid(x1 * w3 + x2 * w4 + b2);
                double output = sigmoid(h1 * w5 + h2 * w6 + b3);

                // Calculate error
                double error = target - output;
                totalLoss += error * error;

                // Backpropagation: output layer delta
                double deltaOutput = error * sigmoidDerivative(output);

                // Backpropagation: hidden layer deltas
                double deltaH1 = deltaOutput * w5 * sigmoidDerivative(h1);
                double deltaH2 = deltaOutput * w6 * sigmoidDerivative(h2);

                // Update weights: hidden to output
                w5 += learningRate * deltaOutput * h1;
                w6 += learningRate * deltaOutput * h2;
                b3 += learningRate * deltaOutput;

                // Update weights: input to hidden neuron 1
                w1 += learningRate * deltaH1 * x1;
                w2 += learningRate * deltaH1 * x2;
                b1 += learningRate * deltaH1;

                // Update weights: input to hidden neuron 2
                w3 += learningRate * deltaH2 * x1;
                w4 += learningRate * deltaH2 * x2;
                b2 += learningRate * deltaH2;
            }
        }
    }

    public static void main(String[] args) {
        NeuralNetwork nn = new NeuralNetwork(2.0);

        double[][] inputs = {
            {0, 0},
            {0, 1},
            {1, 0},
            {1, 1}
        };
        double[] targets = {0, 1, 1, 0};

        System.out.println("Initial (untrained) predictions:");
        for (int i = 0; i < inputs.length; i++) {
            double[] result = nn.forward(inputs[i][0], inputs[i][1]);
            System.out.printf("Input: [%.0f, %.0f] -> Output: %.4f (Expected: %.0f)%n",
                inputs[i][0], inputs[i][1], result[2], targets[i]);
        }
    }
}

Your network now knows how to learn. It can measure its mistakes, trace them backward to every weight, and nudge each weight in the right direction. Next up, you'll put this to the test by training on the XOR dataset and watching the loss drop toward zero.

Train and Validate

Your network knows how to learn from its mistakes. Each time it sees a training sample, it adjusts its weights in the direction that reduces error.

Now you need to see that learning in action. In this step, you will add loss reporting so you can watch the network converge, then run the full training loop and confirm the network actually solves XOR.

In this step, get ready to:
  • Add MSE loss printing every 1,000 epochs.
  • Update the main method to train for 10,000 epochs and print final predictions.
  • Run the program and observe convergence.
Add loss tracking to the training loop

Your train method already accumulates totalLoss for each sample. You need to print that value periodically so you can see whether the network is actually improving.

  • In NeuralNetwork.java, find the closing } of the inner for (int i = 0; ...) loop inside the train method.
  • Add the following code directly after that closing brace, but still inside the outer for (int epoch = 0; ...) loop:
            // Print loss every 1000 epochs
            if (epoch % 1000 == 0) {
                double mse = totalLoss / inputs.length;
                System.out.printf("Epoch %5d - MSE: %.6f%n", epoch, mse);
            }

What does this code do?

Every 1,000 epochs, this block divides the accumulated squared error by the number of samples (4) to get the Mean Squared Error (MSE).

MSE is your single number for "how wrong is the network overall." If training works, this number drops toward zero over time.

  • Save NeuralNetwork.java.

Seeing a compilation error?

Make sure the if block sits after the inner loop's closing brace but before the outer loop's closing brace. The indentation should match the for (int i = 0; ...) line above it.

Help me fix my training loop structure.

Update main to train and display results

The current main method only prints untrained predictions. You need it to actually train the network, then show the final predictions so you can confirm learning happened.

  • Replace your entire main method with the following:
    public static void main(String[] args) {
        NeuralNetwork nn = new NeuralNetwork(2.0);

        // XOR training data
        double[][] inputs = {
            {0, 0},
            {0, 1},
            {1, 0},
            {1, 1}
        };
        double[] targets = {0, 1, 1, 0};

        System.out.println("Training neural network on XOR...");
        System.out.println("Architecture: 2 inputs -> 2 hidden -> 1 output");
        System.out.println("Activation: Sigmoid | Loss: MSE | Learning Rate: 2.0");
        System.out.println("-------------------------------------------");

        nn.train(inputs, targets, 10000);

        System.out.println("-------------------------------------------");
        System.out.println("Results after training:");
        for (int i = 0; i < inputs.length; i++) {
            double[] result = nn.forward(inputs[i][0], inputs[i][1]);
            double output = result[2];
            System.out.printf("Input: [%.0f, %.0f] -> Output: %.4f (Expected: %.0f)%n",
                inputs[i][0], inputs[i][1], output, targets[i]);
        }
    }

What does this code do?

  • The header prints tell you the architecture and hyperparameters at a glance.
  • nn.train(inputs, targets, 10000) runs the full training loop for 10,000 epochs.
  • After training, the loop calls forward on each XOR input and prints the network's final prediction next to the expected value.
  • Save NeuralNetwork.java.

Getting a 'method already defined' error?

Make sure you replaced the old main method entirely. You should have exactly one public static void main(String[] args) in the file. Check that you did not accidentally leave the old version above or below the new one.

Help me fix my duplicate main method.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before compiling.

ⓧ I'd like to double check the full code

import java.util.Random;

public class NeuralNetwork {

    // Weights: input to hidden layer
    private double w1, w2, w3, w4;
    // Weights: hidden to output layer
    private double w5, w6;
    // Biases: hidden neurons and output neuron
    private double b1, b2, b3;
    // Learning rate
    private double learningRate;

    public NeuralNetwork(double learningRate) {
        this.learningRate = learningRate;
        Random rand = new Random();
        w1 = rand.nextGaussian();
        w2 = rand.nextGaussian();
        w3 = rand.nextGaussian();
        w4 = rand.nextGaussian();
        w5 = rand.nextGaussian();
        w6 = rand.nextGaussian();
        b1 = rand.nextGaussian();
        b2 = rand.nextGaussian();
        b3 = rand.nextGaussian();
    }

    private double sigmoid(double x) {
        return 1.0 / (1.0 + Math.exp(-x));
    }

    private double sigmoidDerivative(double sigmoidOutput) {
        return sigmoidOutput * (1.0 - sigmoidOutput);
    }

    public double[] forward(double x1, double x2) {
        double h1 = sigmoid(x1 * w1 + x2 * w2 + b1);
        double h2 = sigmoid(x1 * w3 + x2 * w4 + b2);
        double output = sigmoid(h1 * w5 + h2 * w6 + b3);
        return new double[]{h1, h2, output};
    }

    public void train(double[][] inputs, double[] targets, int epochs) {
        for (int epoch = 0; epoch < epochs; epoch++) {
            double totalLoss = 0.0;

            for (int i = 0; i < inputs.length; i++) {
                double x1 = inputs[i][0];
                double x2 = inputs[i][1];
                double target = targets[i];

                // Forward pass
                double h1 = sigmoid(x1 * w1 + x2 * w2 + b1);
                double h2 = sigmoid(x1 * w3 + x2 * w4 + b2);
                double output = sigmoid(h1 * w5 + h2 * w6 + b3);

                // Calculate error
                double error = target - output;
                totalLoss += error * error;

                // Backpropagation: output layer delta
                double deltaOutput = error * sigmoidDerivative(output);

                // Backpropagation: hidden layer deltas
                double deltaH1 = deltaOutput * w5 * sigmoidDerivative(h1);
                double deltaH2 = deltaOutput * w6 * sigmoidDerivative(h2);

                // Update weights: hidden to output
                w5 += learningRate * deltaOutput * h1;
                w6 += learningRate * deltaOutput * h2;
                b3 += learningRate * deltaOutput;

                // Update weights: input to hidden neuron 1
                w1 += learningRate * deltaH1 * x1;
                w2 += learningRate * deltaH1 * x2;
                b1 += learningRate * deltaH1;

                // Update weights: input to hidden neuron 2
                w3 += learningRate * deltaH2 * x1;
                w4 += learningRate * deltaH2 * x2;
                b2 += learningRate * deltaH2;
            }

            // Print loss every 1000 epochs
            if (epoch % 1000 == 0) {
                double mse = totalLoss / inputs.length;
                System.out.printf("Epoch %5d - MSE: %.6f%n", epoch, mse);
            }
        }
    }

    public static void main(String[] args) {
        NeuralNetwork nn = new NeuralNetwork(2.0);

        // XOR training data
        double[][] inputs = {
            {0, 0},
            {0, 1},
            {1, 0},
            {1, 1}
        };
        double[] targets = {0, 1, 1, 0};

        System.out.println("Training neural network on XOR...");
        System.out.println("Architecture: 2 inputs -> 2 hidden -> 1 output");
        System.out.println("Activation: Sigmoid | Loss: MSE | Learning Rate: 2.0");
        System.out.println("-------------------------------------------");

        nn.train(inputs, targets, 10000);

        System.out.println("-------------------------------------------");
        System.out.println("Results after training:");
        for (int i = 0; i < inputs.length; i++) {
            double[] result = nn.forward(inputs[i][0], inputs[i][1]);
            double output = result[2];
            System.out.printf("Input: [%.0f, %.0f] -> Output: %.4f (Expected: %.0f)%n",
                inputs[i][0], inputs[i][1], output, targets[i]);
        }
    }
}
Compile and run the program

Time to see 10,000 epochs of learning happen in a few seconds.

  • Compile and run the program by running these commands in the integrated terminal:
javac NeuralNetwork.java
java NeuralNetwork

What should I see?

Your terminal should display a header line, followed by 10 MSE checkpoints (one every 1,000 epochs). The MSE starts at a relatively high value and decreases toward near-zero.

After the training output, you should see four final predictions. The outputs for [0,1] and [1,0] should be close to 1.0. The outputs for [0,0] and [1,1] should be close to 0.0. All four should be within 0.1 of their expected values.

Why does the MSE decrease?

Each epoch, backpropagation nudges every weight in the direction that reduces error. Over thousands of repetitions, the network finds a combination of weights that maps XOR inputs to their correct outputs.

The MSE dropping from a high number to near-zero is concrete proof that gradient descent is working. Your network started with random guesses and learned the correct function.

Predictions stuck near 0.5?

Occasionally the random weight initialization lands in a local minimum where the network cannot escape. This is normal for small networks with random seeds.

  • Run the program again. A different random seed usually converges correctly.
  • If it happens repeatedly, double check that your weight update signs use += (not -=). The sign convention in this code uses error = target - output, which means you add the gradient.

Help me debug my neural network training.

Secret mission

Expand to 2-3-1 and Tune the Learning Rate

Your 2-2-1 network solves XOR, but can a wider network learn faster? Add a third hidden neuron to create a 2-3-1 architecture with 13 parameters, then experiment with learning rates of 0.5, 2.0, and 5.0 to discover the tradeoff between convergence speed and stability.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs or cloud resources to worry about. Decide whether to keep your project files or remove them.

Resources you used:

  • Project folder on your Desktop containing NeuralNetwork.java and compiled NeuralNetwork.class files.

Keep everything running

No action needed. Choose this if you want to keep experimenting with your neural network, trying different architectures, or referencing the code later.

  • Your project folder stays on your Desktop.
  • There are no background processes or running services to manage.
  • JDK 17 and VS Code remain installed on your machine as they were before this project.

Pause - I'll come back to this later

Since this project is entirely local with no running processes, there is nothing to pause. Your files will be waiting for you whenever you return.

  • No servers, containers, or cloud resources are running.
  • Your NeuralNetwork.java file is saved on disk and ready to compile again anytime.

Delete - I don't want to use this again

Remove all project files from your machine. This deletes the source code and compiled class files permanently.

  • Delete your project folder by running this command in your terminal:
cd ~/Desktop
rm -rf [[PROJECT_FOLDER="your-project-folder-name"]]

On Windows, use this command instead:

cd ~\Desktop
rmdir /s /q [[PROJECT_FOLDER="your-project-folder-name"]]
  • Confirm the folder is gone by running ls (macOS/Linux) or dir (Windows) on your Desktop. Your project folder should no longer appear in the output.

Nice Work!

Nice Work!

You just built a neural network entirely from scratch in pure Java. No frameworks, no libraries, no magic. Every weight update and gradient calculation came from code you wrote and understood.

You've learned how to:

  • Build a feedforward neural network from scratch in pure Java with zero external dependencies.
  • Implement forward propagation and backpropagation using the chain rule to compute gradients and update weights with gradient descent.
  • Train the network to solve XOR, a non-linearly-separable problem that proves your hidden layer and loss convergence work.
  • Secret Mission: Expanded to a 2-3-1 architecture and experimented with learning rate tuning to understand the speed/stability tradeoff.

Ready to quiz yourself?