### Instruction:
Write a Java program that implements a simple neural network with backpropagation for training. The network should have configurable layers and activation functions.


### Response:

Here's a simple Java implementation of a neural network with backpropagation for training. The network has configurable layers and activation functions.

```java
import java.util.Random;
import java.util.function.Function;

public class NeuralNetwork {
    private final double[][][] weights;
    private final Function<Double, Double> activationFunction;
    private final Function<Double, Double> activationDerivative;

    public NeuralNetwork(int[] layerSizes, Function<Double, Double> activationFunction,
                         Function<Double, Double> activationDerivative) {
        this.activationFunction = activationFunction;
        this.activationDerivative = activationDerivative;

        weights = new double[layerSizes.length - 1][][];
        Random random = new Random();
        for (int i = 0; i < weights.length; i++) {
            int inputSize = layerSizes[i] + 1;
            int outputSize = layerSizes[i + 1];
            weights[i] = new double[inputSize][outputSize];
            for (int j = 0; j < inputSize; j++) {
                for (int k = 0; k < outputSize; k++) {
                    weights[i][j][k] = random.nextDouble() * 2 - 1;
                }
            }
        }
    }

    public double[] predict(double[] input) {
        double[] activations = input;
        for (double[][] layerWeights : weights) {
            activations = activate(activations, layerWeights);
        }
        return activations;
    }

    private double[] activate(double[] input, double[][] layerWeights) {
        double[] extendedInput = new double[input.length + 1];
        System.arraycopy(input, 0, extendedInput, 0, input.length);
        extendedInput[input.length] = 1; // bias

        double[] output = new double[layerWeights[0].length];
        for (int i = 0; i < output.length; i++) {
            double sum = 0;
            for (int j = 0; j < extendedInput.length; j++) {
                sum += extendedInput[j] * layerWeights[j][i];
            }
            output[i] = activationFunction.apply(sum);
        }
        return output;
    }

    public void train(double[] input, double[] targetOutput, double learningRate) {
        double[][][] weightDeltas = new double[weights.length][][];
        double[] error = null;

        for (int layerIndex = weights.length - 1; layerIndex >= 0; layerIndex--) {
            double[][] layerWeights = weights[layerIndex];
            int inputSize = layerWeights.length;
            int outputSize = layerWeights[0].length;

            if (error == null) {
                error = new double[outputSize];
                for (int i = 0; i < outputSize; i++) {
                    error[i] = targetOutput[i] - predict(input)[i];
                }
            } else {
                double[] nextError = new double[inputSize - 1]; // exclude bias
                for (int i = 0; i < nextError.length; i++) {
                    double sum = 0;
                    for (int j = 0; j < outputSize; j++) {
                        sum += layerWeights[i][j] * error[j];
                    }
                    nextError[i] = sum;
                }
                error = nextError;
            }

            double[] activations = input;
            if (layerIndex > 0) {
                for (int i = 0; i < layerIndex; i++) {
                    activations = activate(activations, weights[i]);
                }
            }

            double[] extendedActivations = new double[activations.length + 1];
            System.arraycopy(activations, 0, extendedActivations, 0, activations.length);
            extendedActivations[activations.length] = 1; // bias

            weightDeltas[layerIndex] = new double[inputSize][outputSize];
            for (int i = 0; i < inputSize; i++) {
                for (int j = 0; j < outputSize; j++) {
                    double delta = learningRate * error[j] * activationDerivative.apply(extendedActivations[i]);
                    weightDeltas[layerIndex][i][j] = delta;
                }
            }
        }

        for (int layerIndex = 0; layerIndex < weights.length; layerIndex++) {
            for (int i = 0; i < weights[layerIndex].length; i++) {
                for (int j = 0; j < weights[layerIndex][i].length; j++) {
                    weights[layerIndex][i][j] += weightDeltas[layerIndex][i][j];
                }
            }
        }
    }

    public static void main(String[] args) {
        int[] layerSizes = {2, 4, 1};
        Function<Double, Double> sigmoid = x -> 1 / (1 + Math.exp(-x));
        Function<Double, Double> sigmoidDerivative = x -> x * (1 - x);

        NeuralNetwork nn = new NeuralNetwork(layerSizes, sigmoid, sigmoidDerivative);

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

        for (int epoch = 0; epoch < 10000; epoch++) {
            for (int i = 0; i < inputs.length; i++) {
                nn.train(inputs[i], outputs[i], 0.1);
            }
        }

        for (double[] input : inputs) {
            double[] prediction = nn.predict(input);
            System.out.printf("Input: [%f, %f], Output: %f%n", input[0], input[1], prediction[0]);
        }
    }
}
```

This program creates a simple neural network with one hidden layer and trains it to learn the XOR function. The activation function used is the sigmoid function, but you can easily change it by modifying the `activationFunction` and `activationDerivative` parameters in the constructor.

Please note that this implementation is quite basic and not optimized for performance or flexibility. For more advanced use cases, consider using a dedicated library like DL4J or TensorFlow Java API.