← Back to list

Implementing a Perceptron from Scratch (Part 2)

In the previous article, we explored the theory behind the perceptron and learned how it transforms inputs into decisions using weights, a…

Ivan Polovyi in Level Up Coding · 2026-07-13 15:31 · 72 claps · 13.4 min read paywalled
#ai #artificial-intelligence #java #programming #llm
Open on Medium ↗
Wiki topics: LLM · Large Language Models AI · AI · General 💻 · Programming

Implementing a Perceptron from Scratch (Part 2)

In the previous article, we explored the theory behind the perceptron and learned how it transforms inputs into decisions using weights, a bias, and an activation function. Now it’s time to bring those ideas to life. In this article, we’ll implement a perceptron from scratch, map every line of code to the concepts we’ve already learned, and use it to make predictions on the same examples from the previous post. This friendly link is for those without a subscription.

1. Introduction

Brief recap of Part 1.

In the previous article, we built a perceptron on paper. We learned how inputs, weights, the weighted sum, the bias, and the activation function work together to produce a binary decision.

Understanding the theory is important, but seeing it implemented in code makes the concepts much more concrete.

In this article, we’ll build exactly the same perceptron we designed previously and use it to answer the same question:

Should I eat?

2. Why Java?

If you’ve read other machine learning tutorials, you’ve probably noticed that most of them use Python — and for good reason. Python has an incredible ecosystem of libraries, such as TensorFlow, PyTorch, and scikit-learn, that make building machine learning models both simple and efficient.

So why are we using Java?

The first reason is practical: Java is my primary programming language, so it’s the language I’m most comfortable using to explain new concepts.

More importantly, however, Java forces us to focus on the fundamentals. Unlike Python, where it’s easy to rely on high-level machine learning libraries, we’ll implement the perceptron ourselves using only core Java. Every line of code will correspond directly to a concept we learned in the previous article.

This approach has two advantages. First, it removes the “magic” often hidden behind machine learning frameworks, making it easier to understand what a perceptron is actually doing. Second, once you understand the implementation, translating it to another programming language becomes straightforward. The underlying ideas remain exactly the same — the syntax is the only thing that changes.

Our goal isn’t to build a production-ready machine learning library. It’s to understand how a perceptron works by implementing each of its building blocks ourselves.

3. Before We Write Any Code

Before opening our IDE, let’s take a moment to recall what we built in the previous article.

A perceptron follows a surprisingly simple sequence of steps:

  1. Receive one or more inputs.
  2. Multiply each input by its corresponding weight.
  3. Add the bias to compute the weighted sum.
  4. Apply the step function.
  5. Produce the final prediction.

Visually, the process looks like this:

Inputs
   │
   ▼
Weighted Sum
 (includes bias)
   │
   ▼
Step Function
   │
   ▼
Prediction

You can think of a perceptron as a small assembly line. Each station has a single responsibility and passes its result to the next one.

The first station receives the inputs. The second combines them using the weights and the bias to produce a single score. The third applies the step function to that score and decides whether the output should be 0 or 1. Finally, that output becomes our prediction: Eat or Don’t eat.

Our Java implementation will follow this same assembly line. Each stage of the process becomes a small, focused piece of code:

  • The weights and a bias become fields of the Perceptron class.
  • The weighted sum is implemented in the weightedSum() method.
  • The step function is implemented in the stepFunction() method.
  • The predict() method ties everything together by executing the same sequence of steps we've just described.

Rather than writing the entire class at once, we’ll build this assembly line one piece at a time. By the end of the article, you’ll see that the Java implementation is simply another way of expressing the same ideas we learned in the previous post.

4. Designing the Perceptron Class

Now that we’ve reviewed how a perceptron works, it’s time to represent it in code.

The first question we should ask is:

What information does a perceptron need to store?

From the previous article, we know that a perceptron doesn’t need much. It only needs two things:

  • The weights, which determine how strongly each input influences the decision.
  • The bias, which shifts the weighted sum and makes a positive prediction easier or harder to produce.

These naturally become the fields of our Perceptron class:

public class Perceptron {
    private final double[] weights;
    private final double bias;
}

You might notice that there are no inputs stored inside the class. That’s intentional.

The weights and a bias define the perceptron itself — they are part of the model. The inputs, however, change every time we make a prediction. One moment, we might ask the perceptron to evaluate whether someone with a hunger level of 0.9 and food availability of 0.8 should eat. The next moment, we might provide completely different values. Since the inputs vary from one prediction to the next, they shouldn’t be stored as part of the perceptron’s state.

Another detail worth mentioning is the use of the final keyword. Once we've created a perceptron with a particular set of weights and a bias, those values shouldn't change while it's making predictions. In this article, we're building a fixed perceptron, so making these fields immutable helps express that idea in code.

Later in this series, when we implement the learning algorithm, we’ll revisit this design and allow the perceptron to update its weights and bias during training. But for now, keeping them fixed lets us focus entirely on understanding the prediction process.

5. Creating the Perceptron

Now that we’ve defined the structure of our Perceptron class, we need a way to create an instance of it.

In Java, this is the responsibility of the constructor. The constructor receives the information required to initialize the perceptron — in our case, the weights and the bias.

public Perceptron(double[] weights, double bias) {
    this.weights = weights;
    this.bias = bias;
}

For this article, we’ll use exactly the same values we chose in the previous post:

  • Hunger weight = 0.8
  • Food availability weight = 0.6
  • Bias = -0.5

Using the same values allows us to verify that our Java implementation produces the same predictions we calculated by hand.

Creating the perceptron is therefore as simple as:

Perceptron perceptron = new Perceptron(
    new double[]{0.8, 0.6},
    -0.5
);

At this point, our perceptron already has everything it needs to make predictions. It knows how important each input is through its weights, and it knows how easy or difficult it should be to produce a positive prediction through its bias.

One question you might be asking is:

Where did these values come from?

The answer is simple: we chose them ourselves.

In the previous article, we selected these values to demonstrate how a perceptron works. A real perceptron, however, doesn’t rely on manually chosen weights and bias. Instead, it learns them from data through a training process.

For now, we’ll provide the weights and bias manually. This allows us to focus entirely on understanding how a perceptron transforms a set of inputs into a prediction before introducing how those values can be learned automatically.

6. Computing the Weighted Sum

With our perceptron created, it’s time to implement the first piece of logic: computing the weighted sum.

If you remember the previous article, the weighted sum is where the perceptron combines all the available information into a single numerical score.

Mathematically, we defined it as:

z = (w₀ × x₀) + (w₁ × x₁) + ... + bias

Our Java implementation follows this equation almost line by line:

    public double weightedSum(double[] inputs) {

        if (inputs.length != weights.length) {
            throw new IllegalArgumentException(
                    "Expected " + weights.length +
                            " inputs but received " + inputs.length + "."
            );
        }

        double z = bias;

        for (int i = 0; i < weights.length; i++) {
            z += weights[i] * inputs[i];
        }

        return z;
    }

Let’s break it down.

The method begins by validating that the number of inputs matches the number of weights. Since each input must have a corresponding weight, a mismatch indicates that something is wrong, so we immediately throw an exception.

Next, we initialize the variable z with the bias.

double z = bias;

This might look a little strange at first. Why don’t we start from zero?

Remember the equation from the previous article. The bias is part of the weighted sum, so it’s easier to include it from the beginning and then add the contribution of each input.

The next part is the heart of the computation:

for (int i = 0; i < weights.length; i++) {
    z += weights[i] * inputs[i];
}

The loop iterates through every input and performs the same operation:

  1. Take an input.
  2. Multiply it by its corresponding weight.
  3. Add the result to the running total.

For our “Should I Eat?” example:

  • Hunger = 0.9
  • Food available = 0.8

The loop performs the following calculations:

z = -0.5
z += 0.8 × 0.9   → 0.22
z += 0.6 × 0.8   → 0.70

By the time the loop finishes, z contains the final weighted sum.

Notice that this method doesn’t make a decision. It simply computes a score that summarizes all the available information.

This separation of responsibilities is intentional. The weightedSum() method has only one job: calculate the score. Deciding whether that score means Eat or Don't eat is the responsibility of the activation function, which we'll implement next.

7. The Step Function

At this point, our perceptron knows how to compute the weighted sum. Given a set of inputs, it produces a single numerical score.

But we’re not interested in the score itself. We want an answer to a simple question:

Should I eat?

This is where the step function comes in.

Its job is to transform the weighted sum into a binary decision. If the score is greater than or equal to zero, the perceptron predicts 1 (“Eat”). Otherwise, it predicts 0 (“Don’t eat”).

The implementation is remarkably simple:

private int stepFunction(double z) {
    return z >= 0 ? 1 : 0;
}

Let’s revisit the examples from the previous chapter.

For our first scenario, the weighted sum was:

z = 0.70

Since 0.70 >= 0, the step function returns:

1

which means:

Eat

For the second scenario, we calculated:

z = -0.16

Since -0.16 < 0, the step function returns:

0

which means:

Don't eat

Notice how little logic the step function contains. It doesn’t know anything about hunger, food availability, weights, or bias. It simply receives a number and decides on which side of the decision boundary it falls.

This separation of responsibilities makes the implementation easy to understand:

  • weightedSum() computes the score.
  • stepFunction() converts the score into a binary decision.

8. Making a Prediction

At this point, we have all the pieces we need:

  • We know how to compute the weighted sum.
  • We know how to convert that score into a binary decision using the step function.

The only thing left is to connect these two operations.

That’s exactly what the predict() method does:

public int predict(double[] inputs) {
    double z = weightedSum(inputs);
    return stepFunction(z);
}

Although this method contains only two lines of code, it represents the entire prediction pipeline we’ve been building throughout this series.

Let’s walk through it.

The first line computes the weighted sum:

double z = weightedSum(inputs);

For our first example:

  • Hunger = 0.9
  • Food available = 0.8

the weightedSum() method returns:

z = 0.70

The second line passes that value to the step function:

return stepFunction(z);

Since 0.70 is greater than or equal to zero, the step function returns:

1

which means:

Eat

If we use our second example instead:

  • Hunger = 0.2
  • Food available = 0.3

The weighted sum becomes:

z = -0.16

Passing this value to the step function produces:

0

which means:

Don't eat

You can think of the predict() method as the conductor of an orchestra. It doesn't perform the calculations itself. Instead, it coordinates the other methods, ensuring they execute in the correct order.

The sequence is exactly the same one we introduced in the previous article:

Inputs
   │
   ▼
weightedSum()
   │
   ▼
stepFunction()
   │
   ▼
Prediction

At this point, our Perceptron class is complete. It has everything it needs to receive inputs and produce predictions. The only thing left is to put it into action and see it make decisions using the same examples we explored throughout this series.

9. Putting Everything Together

We’ve now implemented every building block of our perceptron:

  • The weights determine how much each input influences the decision.
  • The bias shifts the weighted sum.
  • The weightedSum() method computes the perceptron's score.
  • The stepFunction() converts that score into a binary decision.
  • The predict() method orchestrates the entire process.

Individually, each method is simple. Together, they form a complete perceptron capable of making predictions.

Here’s the complete implementation:

package com.polovyi.ivan;

public class Perceptron {

    private final double[] weights;
    private final double bias;

    public Perceptron(double[] weights, double bias) {
        this.weights = weights;
        this.bias = bias;
    }

    public int predict(double[] inputs) {
        double z = weightedSum(inputs);
        return stepFunction(z);
    }

    public int stepFunction(double z) {
        return z >= 0 ? 1 : 0;
    }

    public double weightedSum(double[] inputs) {

        if (inputs.length != weights.length) {
            throw new IllegalArgumentException(
                    "Expected " + weights.length +
                            " inputs but received " + inputs.length + "."
            );
        }

        double z = bias;

        for (int i = 0; i < weights.length; i++) {
            z += weights[i] * inputs[i];
        }

        return z;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("Perceptron { weights=[");

        for (int i = 0; i < weights.length; i++) {
            if (i > 0) {
                sb.append(", ");
            }
            sb.append(String.format("%.1f", weights[i]));
        }

        return sb.append(String.format("], bias=%.1f }", bias)).toString();
    }

    public double[] getWeights() {
        return weights;
    }

    public double getBias() {
        return bias;
    }
}

If you compare this implementation with the diagram from the previous article, you’ll notice that every concept now has a direct representation in code.

The perceptron isn’t a complicated algorithm hidden behind dozens of classes or hundreds of lines of code. In fact, the core logic fits in just a few methods. Most of the complexity comes from understanding why each method exists — not from writing the code itself.

This is one of the reasons the perceptron is such a great introduction to machine learning. It demonstrates the complete prediction process in a way that’s easy to understand, both mathematically and programmatically.

Now that our perceptron is fully implemented, it’s time to see it in action. In the next section, we’ll execute the program using the same “Should I Eat?” examples from the previous article and verify that our implementation produces exactly the predictions we expect.

10. Running the Perceptron

With our Perceptron class complete, all that's left is to use it.

To keep things simple, we’ll create a small Main class that instantiates our perceptron and runs the same examples we've been using throughout this series.

Our Main class runs two scenarios:

  • Example 1: Very hungry and plenty of food available.
  • Example 2: Not very hungry and little food available.

For each scenario, the program displays:

  • The input values.
  • The weighted sum (z).
  • The output of the step function.
  • The final prediction.

Rather than simply printing Eat or Don’t eat, we’ll also display the intermediate calculations. Seeing the weighted sum before the activation function is applied makes it much easier to understand how the perceptron arrives at its decision.

Here’s the complete Main class:

package com.polovyi.ivan;

public class Main {

    public static void main(String[] args) {

        // Same weights and bias used throughout the article.
        Perceptron perceptron = new Perceptron(
                new double[]{0.8, 0.6},
                -0.5
        );

        System.out.println("=== Perceptron: Should I Eat? ===\n");

        System.out.println(perceptron);
        System.out.println();

        // ──────────────────────────────────────────────────────────────
        // Example 1 — Very hungry, plenty of food
        // ──────────────────────────────────────────────────────────────

        double[] input1 = {0.9, 0.8};

        double z1 = perceptron.weightedSum(input1);
        int prediction1 = perceptron.predict(input1);

        System.out.println("Example 1 — Very hungry, plenty of food");
        System.out.printf("Inputs:%n");
        System.out.printf("  Hunger          = %.1f%n", input1[0]);
        System.out.printf("  Food available  = %.1f%n%n", input1[1]);

        System.out.println("Weighted sum:");
        System.out.println("  z = (0.9 × 0.8) + (0.8 × 0.6) - 0.5");
        System.out.println("    = 0.72 + 0.48 - 0.50");
        System.out.printf ("    = %.2f%n%n", z1);

        System.out.printf("Step function:%n");
        System.out.printf("  step(%.2f) = %d%n%n", z1, prediction1);

        System.out.printf("Prediction: %s%n%n",
                prediction1 == 1 ? "Eat 🍽️" : "Don't eat");

        // ──────────────────────────────────────────────────────────────
        // Example 2 — Not very hungry, little food
        // ──────────────────────────────────────────────────────────────

        double[] input2 = {0.2, 0.3};

        double z2 = perceptron.weightedSum(input2);
        int prediction2 = perceptron.predict(input2);

        System.out.println("Example 2 — Not very hungry, little food");
        System.out.printf("Inputs:%n");
        System.out.printf("  Hunger          = %.1f%n", input2[0]);
        System.out.printf("  Food available  = %.1f%n%n", input2[1]);

        System.out.println("Weighted sum:");
        System.out.println("  z = (0.2 × 0.8) + (0.3 × 0.6) - 0.5");
        System.out.println("    = 0.16 + 0.18 - 0.50");
        System.out.printf ("    = %.2f%n%n", z2);

        System.out.printf("Step function:%n");
        System.out.printf("  step(%.2f) = %d%n%n", z2, prediction2);

        System.out.printf("Prediction: %s%n",
                prediction2 == 1 ? "Eat 🍽️" : "Don't eat");
    }
}

When we execute the program, we should see the same results we calculated by hand in the previous article:

  • A large positive weighted sum produces the prediction Eat.
  • A negative weighted sum produces the prediction Don’t eat.

This is an important milestone. For the first time, we’ve translated the mathematical model of a perceptron into a working Java implementation that behaves exactly as expected.

When you run the application, you’ll see the perceptron process the same examples we explored throughout this series. For each example, the program displays:

  • The input values.
  • The weighted sum (z).
  • The output of the step function.
  • The final prediction.

I encourage you to run the application yourself and compare the console output with the manual calculations from the previous article. You should see that every value matches exactly, demonstrating that our Java implementation behaves just like the perceptron we built on paper.

Once you’ve verified the results, try experimenting with different input values. Change the hunger level or the amount of food available and observe how the weighted sum changes. Pay close attention to what happens when the weighted sum crosses zero — that’s the moment the step function changes the prediction from Don’t eat to Eat, or vice versa.

At this point, we’ve successfully implemented a complete perceptron capable of making predictions.

In the next article, we’ll take the next logical step: instead of manually choosing the weights and bias, we’ll teach the perceptron how to learn them automatically from data.

The complete code can be found here:

[embed]GitHub - polovyivan/perceptron-llm Contribute to polovyivan/perceptron-llm development by creating an account on GitHub.github.com

Conclusion

In this article, we’ve transformed the perceptron from a theoretical concept into a working Java implementation.

Starting with the building blocks from the previous article, we implemented each component one step at a time: storing the weights and bias, computing the weighted sum, applying the step function, and finally combining everything into a predict() method capable of making binary decisions.

Although the implementation is surprisingly small, it captures the complete prediction process of a perceptron. More importantly, every line of code has a clear purpose and maps directly to a concept we’ve already explored.

At this point, our perceptron can make predictions — but there’s one obvious limitation. The weights and a bias are still values that we chose ourselves. In a real machine learning model, we wouldn’t manually decide how important each input should be.

So, how does a perceptron discover the correct weights? How does it improve when it makes a mistake?

That’s exactly what we’ll explore in the next article. We’ll implement the perceptron learning algorithm from scratch and watch our model adjust its weights and bias automatically, learning from data one example at a time.

If you’ve followed along so far, I encourage you to experiment with the code before moving on. Change the weights, modify the bias, try different input values, and observe how each change affects the prediction. Understanding these relationships will make the learning algorithm much easier to grasp in the next part of the series.

Thank you for reading! If you enjoyed this post, please like and follow. If you have any questions or suggestions, feel free to leave a comment.

➡️ Part 3 — How a Perceptron Learns


메타데이터
post_id
e3ca97f8f0ea
slug
implementing-a-perceptron-from-scratch-e3ca97f8f0ea
url
https://levelup.gitconnected.com/implementing-a-perceptron-from-scratch-e3ca97f8f0ea
canonical_url
https://levelup.gitconnected.com/implementing-a-perceptron-from-scratch-e3ca97f8f0ea
author_url
https://medium.com/@polovyiivan
status
ok
fetched_at
2026-07-15 18:46:35