diff --git a/README.MD b/README.MD index 7630fb4..15f7e14 100644 --- a/README.MD +++ b/README.MD @@ -46,7 +46,7 @@ Edux supports a variety of image augmentations, which can be used to increase th #### Single Image -````java +``` AugmentationSequence augmentationSequence= new AugmentationBuilder() .addAugmentation(new ResizeAugmentation(250,250)) @@ -54,11 +54,11 @@ Edux supports a variety of image augmentations, which can be used to increase th .build(); BufferedImage augmentedImage=augmentationSequence.applyTo(image); -```` +``` #### Run for all images in a directory -```java +``` AugmentationSequence augmentationSequence= new AugmentationBuilder() .addAugmentation(new ResizeAugmentation(250,250)) @@ -127,78 +127,74 @@ A multi-layer perceptron (MLP) is a feedforward artificial neural network that g input features. An MLP is characterized by several layers of input nodes connected as a directed graph between the input and output layers. -![Neural Network](https://hc-linux.eu/github/iris-nn.png) +### Step 0: Get Familiar with the Dataset -### Step 1: Data Processing +In this example we use the famouse MNIST Dataset. The MNIST database contains 60,000 training images and 10,000 testing -Firstly, we will load and prepare the IRIS dataset: +![](https://hc-linux.eu/edux/mnist-examples.png) -| sepal.length | sepal.width | petal.length | petal.width | variety | -|--------------|-------------|--------------|-------------|---------| -| 5.1 | 3.5 | 1.4 | 0.2 | Setosa | - -```java -var featureColumnIndices=new int[]{0,1,2,3}; // Specify your feature columns - var targetColumnIndex=4; // Specify your target column +### Step 1: Data Processing - var dataProcessor=new DataProcessor(new CSVIDataReader()); - var dataset=dataProcessor.loadDataSetFromCSV( - "path/to/your/data.csv", // Replace with your CSV file path - ',', // CSV delimiter - true, // Whether to skip the header - featureColumnIndices, - targetColumnIndex - ); - dataset.shuffle(); - dataset.normalize(); - dataProcessor.split(0.8); // Replace with your train-test split ratio ``` + String trainImages = "train-images.idx3-ubyte"; + String trainLabels = "train-labels.idx1-ubyte"; + String testImages = "t10k-images.idx3-ubyte"; + String testLabels = "t10k-labels.idx1-ubyte"; + Loader trainLoader = new ImageLoader(trainImages, trainLabels, batchSize); + Loader testLoader = new ImageLoader(testImages, testLabels, batchSize); -### Step 2: Preparing Training and Test Sets: +``` -Extract the features and labels for both training and test sets: +### Step 2: Configure the MultilayerPerceptron -```java - var trainFeatures=dataProcessor.getTrainFeatures(featureColumnIndices); - var trainLabels=dataProcessor.getTrainLabels(targetColumnIndex); - var testFeatures=dataProcessor.getTestFeatures(featureColumnIndices); - var testLabels=dataProcessor.getTestLabels(targetColumnIndex); ``` + int batchSize = 100; + int threads = 1; + int epochs = 10; + float initialLearningRate = 0.1f; + float finalLearningRate = 0.001f; + + MetaData trainMetaData = trainLoader.open(); + int inputSize = trainMetaData.getInputSize(); + int outputSize = trainMetaData.getExpectedSize(); + trainLoader.close(); +``` + +### Step 3: Build the Network -### Step 3: Network Configuration +We use the NetworkBuilder Class -```java -var networkConfiguration=new NetworkConfiguration( - trainFeatures[0].length, // Number of input neurons - List.of(128,256,512), // Number of neurons in each hidden layer - 3, // Number of output neurons - 0.01, // Learning rate - 300, // Number of epochs - ActivationFunction.LEAKY_RELU, // Activation function for hidden layers - ActivationFunction.SOFTMAX, // Activation function for output layer - LossFunction.CATEGORICAL_CROSS_ENTROPY, // Loss function - Initialization.XAVIER, // Weight initialization for hidden layers - Initialization.XAVIER // Weight initialization for output layer - ); ``` + new NetworkBuilder() + .addLayer(new DenseLayer(inputSize, 32)) //32 Neurons as output size + .addLayer(new ReLuLayer()) + .addLayer(new DenseLayer(32, outputSize)) //32 Neurons as input size + .addLayer(new SoftmaxLayer()) + .withBatchSize(batchSize) + .withLearningRates(initialLearningRate, finalLearningRate) + .withExecutionMode(singleThread) + .withEpochs(epochs) + .build() + .printArchitecture() + .fit(trainLoader, testLoader) + .saveModel("model.edux"); // Save the trained model +``` + +### Step 4: Load the model and continue training -### Step 4: Training and Evaluation +Load 'model.edux' and continue training for 10 epochs. -```java -MultilayerPerceptron multilayerPerceptron=new MultilayerPerceptron( - networkConfiguration, - testFeatures, - testLabels - ); - multilayerPerceptron.train(trainFeatures,trainLabels); - multilayerPerceptron.evaluate(testFeatures,testLabels); +``` + NeuralNetwork nn = + new NetworkBuilder().withEpochs(10).loadModel("model.edux").fit(trainLoader, testLoader); ``` ### Results ```output +........................Epoch: 1, Loss: 1,14, Accuracy: 91,04 ... -MultilayerPerceptron - Best accuracy after restoring best MLP model: 98,56% +........................Epoch: 10, Loss: 0,13, Accuracy: 96,16 ``` ### Working examples diff --git a/example/datasets/mnist/t10k-images.idx3-ubyte b/example/datasets/mnist/t10k-images.idx3-ubyte new file mode 100644 index 0000000..1170b2c Binary files /dev/null and b/example/datasets/mnist/t10k-images.idx3-ubyte differ diff --git a/example/datasets/mnist/t10k-labels.idx1-ubyte b/example/datasets/mnist/t10k-labels.idx1-ubyte new file mode 100644 index 0000000..d1c3a97 Binary files /dev/null and b/example/datasets/mnist/t10k-labels.idx1-ubyte differ diff --git a/example/datasets/mnist/train-images.idx3-ubyte b/example/datasets/mnist/train-images.idx3-ubyte new file mode 100644 index 0000000..bbce276 Binary files /dev/null and b/example/datasets/mnist/train-images.idx3-ubyte differ diff --git a/example/datasets/mnist/train-labels.idx1-ubyte b/example/datasets/mnist/train-labels.idx1-ubyte new file mode 100644 index 0000000..d6b4c5d Binary files /dev/null and b/example/datasets/mnist/train-labels.idx1-ubyte differ diff --git a/example/src/main/java/de/example/benchmark/Benchmark.java b/example/src/main/java/de/example/benchmark/Benchmark.java index 059c252..3e682b2 100644 --- a/example/src/main/java/de/example/benchmark/Benchmark.java +++ b/example/src/main/java/de/example/benchmark/Benchmark.java @@ -8,8 +8,6 @@ import de.edux.functions.loss.LossFunction; import de.edux.ml.decisiontree.DecisionTree; import de.edux.ml.knn.KnnClassifier; -import de.edux.ml.nn.config.NetworkConfiguration; -import de.edux.ml.nn.network.MultilayerPerceptron; import de.edux.ml.randomforest.RandomForest; import de.edux.ml.svm.SVMKernel; import de.edux.ml.svm.SupportVectorMachine; @@ -37,8 +35,6 @@ public class Benchmark { private double[][] trainLabels; private double[][] testFeatures; private double[][] testLabels; - private MultilayerPerceptron multilayerPerceptron; - private NetworkConfiguration networkConfiguration; private DataProcessor dataProcessor; public Benchmark() { @@ -46,7 +42,6 @@ public Benchmark() { results.put("DecisionTree", new ArrayList<>()); results.put("RandomForest", new ArrayList<>()); results.put("SVM", new ArrayList<>()); - results.put("MLP", new ArrayList<>()); init(); } @@ -80,38 +75,20 @@ private void run() { Classifier randomForest = new RandomForest(500, 10, 2, 3, 3, 60); Classifier svm = new SupportVectorMachine(SVMKernel.LINEAR, 1); - networkConfiguration = - new NetworkConfiguration( - trainFeatures[0].length, - List.of(64, 256, 512), - 3, - 0.01, - 300, - ActivationFunction.LEAKY_RELU, - ActivationFunction.SOFTMAX, - LossFunction.CATEGORICAL_CROSS_ENTROPY, - Initialization.XAVIER, - Initialization.XAVIER); - multilayerPerceptron = - new MultilayerPerceptron(networkConfiguration, testFeatures, testLabels); - knn.train(trainFeatures, trainLabels); decisionTree.train(trainFeatures, trainLabels); randomForest.train(trainFeatures, trainLabels); svm.train(trainFeatures, trainLabels); - multilayerPerceptron.train(trainFeatures, trainLabels); double knnAccuracy = knn.evaluate(testFeatures, testLabels); double decisionTreeAccuracy = decisionTree.evaluate(testFeatures, testLabels); double randomForestAccuracy = randomForest.evaluate(testFeatures, testLabels); double svmAccuracy = svm.evaluate(testFeatures, testLabels); - double multilayerPerceptronAccuracy = multilayerPerceptron.evaluate(testFeatures, testLabels); results.get("KNN").add(knnAccuracy); results.get("DecisionTree").add(decisionTreeAccuracy); results.get("RandomForest").add(randomForestAccuracy); results.get("SVM").add(svmAccuracy); - results.get("MLP").add(multilayerPerceptronAccuracy); init(); } diff --git a/example/src/main/java/de/example/mlp/MlpExampleOnMNIST.java b/example/src/main/java/de/example/mlp/MlpExampleOnMNIST.java new file mode 100644 index 0000000..4d7431f --- /dev/null +++ b/example/src/main/java/de/example/mlp/MlpExampleOnMNIST.java @@ -0,0 +1,87 @@ +package de.example.mlp; + +import de.edux.ml.api.ExecutionMode; +import de.edux.ml.mlp.core.network.NetworkBuilder; +import de.edux.ml.mlp.core.network.layers.DenseLayer; +import de.edux.ml.mlp.core.network.layers.ReLuLayer; +import de.edux.ml.mlp.core.network.layers.SoftmaxLayer; +import de.edux.ml.mlp.core.network.loader.image.ImageLoader; +import de.edux.ml.mlp.core.network.loader.Loader; +import de.edux.ml.mlp.core.network.loader.MetaData; +import java.io.File; + +public class MlpExampleOnMNIST { + public static void main(String[] args) { + String trainImages = + "example" + + File.separator + + "datasets" + + File.separator + + "mnist" + + File.separator + + "train-images.idx3-ubyte"; + String trainLabels = + "example" + + File.separator + + "datasets" + + File.separator + + "mnist" + + File.separator + + "train-labels.idx1-ubyte"; + String testImages = + "example" + + File.separator + + "datasets" + + File.separator + + "mnist" + + File.separator + + "t10k-images.idx3-ubyte"; + String testLabels = + "example" + + File.separator + + "datasets" + + File.separator + + "mnist" + + File.separator + + "t10k-labels.idx1-ubyte"; + + int batchSize = 100; + ExecutionMode singleThread = ExecutionMode.SINGLE_THREAD; + int epochs = 5; + float initialLearningRate = 0.1f; + float finalLearningRate = 0.001f; + + Loader trainLoader = new ImageLoader(trainImages, trainLabels, batchSize); + Loader testLoader = new ImageLoader(testImages, testLabels, batchSize); + + MetaData trainMetaData = trainLoader.open(); + int inputSize = trainMetaData.getInputSize(); + int outputSize = trainMetaData.getExpectedSize(); + trainLoader.close(); + + // Training from scratch + new NetworkBuilder() + .addLayer(new DenseLayer(inputSize, 128)) + .addLayer(new ReLuLayer()) + .addLayer(new DenseLayer(128, 128)) + .addLayer(new ReLuLayer()) + .addLayer(new DenseLayer(128, outputSize)) + .addLayer(new SoftmaxLayer()) + .withBatchSize(batchSize) + .withLearningRates(initialLearningRate, finalLearningRate) + .withExecutionMode(singleThread) + .withEpochs(epochs) + .build() + .printArchitecture() + .fit(trainLoader, testLoader) + .saveModel("mnist_trained.edux"); + + // Loading a trained model + new NetworkBuilder() + .withExecutionMode(singleThread) + .withEpochs(5) + .withLearningRates(0.01f, 0.001f) + .loadModel("mnist_trained.edux") + .fit(trainLoader, testLoader); + } +} diff --git a/example/src/main/java/de/example/nn/MultilayerNeuralNetworkExampleOnIrisDataset.java b/example/src/main/java/de/example/nn/MultilayerNeuralNetworkExampleOnIrisDataset.java deleted file mode 100644 index 82a5a34..0000000 --- a/example/src/main/java/de/example/nn/MultilayerNeuralNetworkExampleOnIrisDataset.java +++ /dev/null @@ -1,77 +0,0 @@ -package de.example.nn; - -import de.edux.data.provider.DataProcessor; -import de.edux.data.reader.CSVIDataReader; -import de.edux.functions.activation.ActivationFunction; -import de.edux.functions.initialization.Initialization; -import de.edux.functions.loss.LossFunction; -import de.edux.ml.nn.config.NetworkConfiguration; -import de.edux.ml.nn.network.MultilayerPerceptron; -import java.io.File; -import java.util.List; - -public class MultilayerNeuralNetworkExampleOnIrisDataset { - - private static final double TRAIN_TEST_SPLIT_RATIO = 0.70; - private static final File CSV_FILE = - new File( - "example" - + File.separator - + "datasets" - + File.separator - + "iris" - + File.separator - + "iris.csv"); - private static final boolean SKIP_HEAD = true; - - public static void main(String[] args) { - var featureColumnIndices = new int[] {0, 1, 2, 3}; - var targetColumnIndex = 4; - - var dataProcessor = new DataProcessor(new CSVIDataReader()); - var dataset = - dataProcessor.loadDataSetFromCSV( - CSV_FILE, ',', SKIP_HEAD, featureColumnIndices, targetColumnIndex); - dataset.shuffle(); - dataset.normalize(); - dataProcessor.split(TRAIN_TEST_SPLIT_RATIO); - - var trainFeatures = dataProcessor.getTrainFeatures(featureColumnIndices); - var trainLabels = dataProcessor.getTrainLabels(targetColumnIndex); - var testFeatures = dataProcessor.getTestFeatures(featureColumnIndices); - var testLabels = dataProcessor.getTestLabels(targetColumnIndex); - - var classMap = dataProcessor.getClassMap(); - - System.out.println("Class Map: " + classMap); - - // Configure Network with: - // - 4 Input Neurons - // - 2 Hidden Layer with 12 and 6 Neurons - // - 3 Output Neurons - // - Learning Rate of 0.1 - // - 300 Epochs - // - Leaky ReLU as Activation Function for Hidden Layers - // - Softmax as Activation Function for Output Layer - // - Categorical Cross Entropy as Loss Function - // - Xavier as Weight Initialization for Hidden Layers - // - Xavier as Weight Initialization for Output Layer - var networkConfiguration = - new NetworkConfiguration( - trainFeatures[0].length, - List.of(128, 256, 512), - 3, - 0.005, - 300, - ActivationFunction.LEAKY_RELU, - ActivationFunction.SOFTMAX, - LossFunction.CATEGORICAL_CROSS_ENTROPY, - Initialization.XAVIER, - Initialization.XAVIER); - - MultilayerPerceptron multilayerPerceptron = - new MultilayerPerceptron(networkConfiguration, testFeatures, testLabels); - multilayerPerceptron.train(trainFeatures, trainLabels); - multilayerPerceptron.evaluate(testFeatures, testLabels); - } -} diff --git a/example/src/main/java/de/example/nn/MultilayerNeuralNetworkExampleOnPenguinsDataset.java b/example/src/main/java/de/example/nn/MultilayerNeuralNetworkExampleOnPenguinsDataset.java deleted file mode 100644 index 98c3bf5..0000000 --- a/example/src/main/java/de/example/nn/MultilayerNeuralNetworkExampleOnPenguinsDataset.java +++ /dev/null @@ -1,94 +0,0 @@ -package de.example.nn; - -import de.edux.data.provider.DataProcessor; -import de.edux.data.reader.CSVIDataReader; -import de.edux.functions.activation.ActivationFunction; -import de.edux.functions.imputation.ImputationStrategy; -import de.edux.functions.initialization.Initialization; -import de.edux.functions.loss.LossFunction; -import de.edux.ml.nn.config.NetworkConfiguration; -import de.edux.ml.nn.network.MultilayerPerceptron; -import java.io.File; -import java.util.List; - -public class MultilayerNeuralNetworkExampleOnPenguinsDataset { - - private static final double TRAIN_TEST_SPLIT_RATIO = 0.70; - private static final File CSV_FILE = - new File( - "example" - + File.separator - + "datasets" - + File.separator - + "seaborn-penguins" - + File.separator - + "penguins.csv"); - private static final boolean SKIP_HEAD = true; - private static final ImputationStrategy averageImputation = ImputationStrategy.AVERAGE; - private static final ImputationStrategy modeImputation = ImputationStrategy.MODE; - - public static void main(String[] args) { - /* Penguins Dataset... - +--------+--------+---------------+--------------+------------------+------------------+ - | species| island| bill_length_mm| bill_depth_mm| flipper_length_mm| body_mass_g| sex| - +--------+--------+---------------+--------------+------------------+------------------+ - | Gentoo | Biscoe | 49.6 | 16 | 225 | 5700 | MALE| - +--------+--------+---------------+--------------+------------------+------------------+ - */ - - var featureColumnIndices = new int[] {1, 2, 3, 4, 5, 6}; - var targetColumnIndex = 0; - - var penguinsDataProcessor = - new DataProcessor(new CSVIDataReader()) - .loadDataSetFromCSV(CSV_FILE, ',', SKIP_HEAD, featureColumnIndices, targetColumnIndex) - .imputation(0, modeImputation) - .imputation(1, modeImputation) - .imputation(2, averageImputation) - .imputation(3, averageImputation) - .imputation(4, averageImputation) - .imputation(5, averageImputation) - .imputation(6, modeImputation) - .normalize() - .shuffle() - .split(TRAIN_TEST_SPLIT_RATIO); - - var trainFeatures = penguinsDataProcessor.getTrainFeatures(featureColumnIndices); - var trainLabels = penguinsDataProcessor.getTrainLabels(targetColumnIndex); - var testFeatures = penguinsDataProcessor.getTestFeatures(featureColumnIndices); - var testLabels = penguinsDataProcessor.getTestLabels(targetColumnIndex); - - var classMap = penguinsDataProcessor.getClassMap(); - - System.out.println("Class Map: " + classMap); - - // Configure Network with: - // - 4 Input Neurons - // - 2 Hidden Layer with 12 and 6 Neurons - // - 3 Output Neurons - // - Learning Rate of 0.1 - // - 1000 Epochs - // - Leaky ReLU as Activation Function for Hidden Layers - // - Softmax as Activation Function for Output Layer - // - Categorical Cross Entropy as Loss Function - // - Xavier as Weight Initialization for Hidden Layers - // - Xavier as Weight Initialization for Output Layer - var networkConfiguration = - new NetworkConfiguration( - trainFeatures[0].length, - List.of(128, 256, 512), - 3, - 0.01, - 300, - ActivationFunction.LEAKY_RELU, - ActivationFunction.SOFTMAX, - LossFunction.CATEGORICAL_CROSS_ENTROPY, - Initialization.XAVIER, - Initialization.XAVIER); - - MultilayerPerceptron multilayerPerceptron = - new MultilayerPerceptron(networkConfiguration, testFeatures, testLabels); - multilayerPerceptron.train(trainFeatures, trainLabels); - multilayerPerceptron.evaluate(testFeatures, testLabels); - } -} diff --git a/lib/src/main/java/de/edux/ml/api/ExecutionMode.java b/lib/src/main/java/de/edux/ml/api/ExecutionMode.java new file mode 100644 index 0000000..fce0992 --- /dev/null +++ b/lib/src/main/java/de/edux/ml/api/ExecutionMode.java @@ -0,0 +1,37 @@ +package de.edux.ml.api; + +/** + * This mode determines how batches are processed during training and testing. + * + *
Regardless of the chosen execution mode, all matrix operations are executed in parallel, with + * the ExecutionMode parallelism referring to the processing of batches. Currently supported: + * + *
+ * This method performs an element-wise subtraction between two matrices. + * It requires that both matrices have the same dimensions. If the matrices + * do not have the same dimensions, an IllegalArgumentException is thrown. + *
+ * + * @param matrix The matrix to be subtracted from this matrix. + * @return A new Matrix object representing the result of the subtraction. + * @throws IllegalArgumentException if the input matrix and this matrix do not have the same dimensions. + */ + public Matrix subtract(Matrix matrix) { + if (this.rows != matrix.rows || this.cols != matrix.cols) { + throw new IllegalArgumentException("Matrices must have the same size."); + } + + Matrix result = new Matrix(this.rows, this.cols); + for (int i = 0; i < this.data.length; i++) { + result.data[i] = this.data[i] - matrix.getData()[i]; + } + return result; + } + + public Matrix relu() { + return this.apply((index, value) -> Math.max(0, value)); + } + + public Matrix reluDerivative(Matrix input) { + return this.apply((index, value) -> input.get(index) > 0 ? value : 0); + } + + public Matrix print() { + System.out.println(this); + return this; + } + + public void set(int row, int col, double value) { + data[row * cols + col] = value; + } + + public double get(int row, int col) { + return data[row * cols + col]; + } + + public Matrix addIncrement(int row, int col, double increment) { + Matrix result = apply((index, value) -> data[index]); + double originalValue = result.get(row, col); + double newValue = originalValue + increment; + result.set(row, col, newValue); + + return result; + } + + public Matrix transpose() { + Matrix result = new Matrix(cols, rows); + for (int row = 0; row < rows; row++) { + for (int col = 0; col < cols; col++) { + result.data[col * rows + row] = data[row * cols + col]; + } + } + return result; + } + + public Matrix transposeParallel() { + Matrix result = new Matrix(cols, rows); + + IntStream.range(0, rows).parallel().forEach(row -> { + for (int col = 0; col < cols; col++) { + result.data[col * rows + row] = data[row * cols + col]; + } + }); + + return result; + } + + public double get(int index) { + return this.getData()[index]; + } + + public Matrix multiply(double rate) { + return this.apply((index, value) -> value * rate); + } + + public Matrix copy() { + Matrix result = new Matrix(rows, cols); + System.arraycopy(data, 0, result.data, 0, data.length); + return result; + } + + + public interface RowColumnProducer { + double produce(int row, int col, double value); + } + + public interface Producer { + double produce(int index); + } + + public interface IndexValueProducer { + double produce(int index, double value); + } + + public interface ValueProducer { + double produce(double value); + } + + public interface IndexValueConsumer { + void consume(int index, double value); + } + + public interface RowColValueConsumer { + void consume(int row, int col, double value); + } + + + public interface RowColIndexValueConsumer { + void consume(int row, int col, int index, double value); + } + + public double[] getData() { + return data; + } + + + + public Matrix(int rows, int cols) { + data = new double[rows * cols]; + this.rows = rows; + this.cols = cols; + } + + public Matrix(int rows, int cols, Producer producer) { + this(rows, cols); + for (int i = 0; i < data.length; i++) { + data[i] = producer.produce(i); + } + } + + public Matrix(int rows, int cols, double[] values) { + this.rows = rows; + this.cols = cols; + + Matrix temp = new Matrix(cols, rows); + temp.data = values; + Matrix transposed = temp.transpose(); + data = transposed.data; + } + + public Matrix(double[][]values){ + this.rows = values.length; + this.cols = values[0].length; + this.data = new double[rows*cols]; + for(int i = 0; i < rows; i++){ + for(int j = 0; j < cols; j++){ + this.data[i*cols+j] = values[i][j]; + } + } + } + + public Matrix apply(IndexValueProducer function) { + Matrix result = new Matrix(rows, cols); + for (int i = 0; i < data.length; i++) { + result.data[i] = function.produce(i, data[i]); + } + return result; + } + + public Matrix multiply(Matrix other) { + if (cols != other.rows) { + throw new IllegalArgumentException("Matrix dimensions do not match"); + } + Matrix result = new Matrix(rows, other.cols); + for (int row = 0; row < rows; row++) { + for (int col = 0; col < other.cols; col++) { + double sum = 0; + for (int i = 0; i < cols; i++) { + sum += data[row * cols + i] * other.data[i * other.cols + col]; + } + result.data[row * other.cols + col] = sum; + } + } + return result; + } + + public Matrix multiplyParallel(Matrix other) { + if (cols != other.rows) { + throw new IllegalArgumentException("Matrix dimensions do not match"); + } + Matrix result = new Matrix(rows, other.cols); + IntStream.range(0, rows) + .parallel() + .forEach( + row -> { + for (int col = 0; col < other.cols; col++) { + double sum = 0; + for (int i = 0; i < cols; i++) { + sum += data[row * cols + i] * other.data[i * other.cols + col]; + } + result.data[row * other.cols + col] = sum; + } + }); + + return result; + } + + public Matrix averageColumn() { + Matrix result = new Matrix(rows, 1); + forEach((row, col, value) -> { + result.data[row] += value / cols; + }); + return result; + } + + public Matrix add(Matrix other) { + // Überprüfen, ob die andere Matrix eine Spaltenmatrix ist, die als Bias verwendet werden kann + if (this.cols != other.cols && other.cols != 1) { + throw new IllegalArgumentException( + "Für die Addition muss die zweite Matrix entweder dieselbe Größe haben oder eine Spaltenmatrix sein."); + } + + Matrix result = new Matrix(rows, cols); + for (int row = 0; row < this.rows; row++) { + for (int col = 0; col < this.cols; col++) { + if (other.cols == 1) { + // Addiere den Bias, wenn die zweite Matrix eine Spaltenmatrix ist + result.data[row * cols + col] = this.data[row * cols + col] + other.data[row]; + } else { + // Normale elementweise Addition, wenn die zweite Matrix dieselbe Größe hat + result.data[row * cols + col] = + this.data[row * cols + col] + other.data[row * cols + col]; + } + } + } + + return result; + } + + public Matrix modify(RowColumnProducer function) { + int index = 0; + for (int row = 0; row < rows; row++) { + for (int col = 0; col < cols; col++, index++) { + data[index] = function.produce(row, col, data[index]); + } + } + return this; + } + + public Matrix modify(ValueProducer function) { + for (int i = 0; i < data.length; i++) { + data[i] = function.produce(data[i]); + } + return this; + } + + public Matrix modify(IndexValueProducer function) { + for (int i = 0; i < data.length; i++) { + data[i] = function.produce(i, data[i]); + } + return this; + } + + + public void forEach(IndexValueConsumer consumer) { + for (int i = 0; i < data.length; i++) { + consumer.consume(i, data[i]); + } + } + + public void forEach(RowColIndexValueConsumer consumer) { + int index = 0; + for (int row = 0; row < rows; row++) { + for (int col = 0; col < cols; col++) { + consumer.consume(row, col, index, data[index++]); + } + } + } + + public void forEach(RowColValueConsumer consumer) { + int index = 0; + for (int row = 0; row < rows; row++) { + for (int col = 0; col < cols; col++) { + consumer.consume(row, col, data[index++]); + } + } + } + + public void setTolerance(double tolerance) { + this.tolerance = tolerance; + } + + public Matrix sumColumns() { + Matrix result = new Matrix(1, cols); + int index = 0; + for (int row = 0; row < rows; row++) { + for (int col = 0; col < cols; col++) { + result.data[col] += data[index++]; + } + } + + return result; + } + + public Matrix softmax() { + Matrix result = new Matrix(rows, cols, i -> Math.exp(data[i])); + Matrix colSum = result.sumColumns(); + + result.modify((row, col, value) -> { + return value / colSum.getData()[col]; + }); + return result; + } + + public Matrix getGreatestRowNumber() { + Matrix result = new Matrix(1, cols); + double[] greatest = new double[cols]; + for (int i = 0; i < cols; i++) { + greatest[i] = Double.MIN_VALUE; + } + + forEach((row, col, value) -> { + if (value > greatest[col]) { + greatest[col] = value; + result.data[col] = row; + } + }); + return result; + } + + public int getRows() { + return rows; + } + + public int getCols() { + return cols; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Matrix matrix = (Matrix) o; + + for (int i = 0; i < data.length; i++) { + if (Math.abs(data[i] - matrix.data[i]) > tolerance) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + int result = Objects.hash(rows, cols); + result = 31 * result + Arrays.hashCode(data); + return result; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + + // Berechnen der maximalen Breite jeder Spalte + int[] maxWidth = new int[cols]; + for (int row = 0; row < rows; row++) { + for (int col = 0; col < cols; col++) { + int length = String.format(NUMBER_FORMAT, data[row * cols + col]).length(); + if (length > maxWidth[col]) { + maxWidth[col] = length; + } + } + } + + // Hinzufügen der Rahmenlinien und der Daten + String rowSeparator = + "+" + + Arrays.stream(maxWidth) + .mapToObj(width -> "-".repeat(width + 2)) + .collect(Collectors.joining("+")) + + "+\n"; + + for (int row = 0; row < rows; row++) { + sb.append(rowSeparator); + sb.append("|"); + for (int col = 0; col < cols; col++) { + String formattedNumber = + String.format( + "%" + maxWidth[col] + "s", String.format(NUMBER_FORMAT, data[row * cols + col])); + sb.append(" ").append(formattedNumber).append(" |"); + } + sb.append("\n"); + } + sb.append(rowSeparator); + + return sb.toString(); + } + + public String toString(boolean showValues) { + if (showValues) { + return toString(); + } else { + return "{" + + "rows=" + rows + + ", cols=" + cols + + '}'; + } + } +} diff --git a/lib/src/main/java/de/edux/ml/mlp/core/transformer/Transform.java b/lib/src/main/java/de/edux/ml/mlp/core/transformer/Transform.java new file mode 100644 index 0000000..16caad6 --- /dev/null +++ b/lib/src/main/java/de/edux/ml/mlp/core/transformer/Transform.java @@ -0,0 +1,5 @@ +package de.edux.ml.mlp.core.transformer; + +public enum Transform { + DENSE, RELU, SOFTMAX; +} diff --git a/lib/src/main/java/de/edux/ml/mlp/exceptions/LoaderException.java b/lib/src/main/java/de/edux/ml/mlp/exceptions/LoaderException.java new file mode 100644 index 0000000..f6ae931 --- /dev/null +++ b/lib/src/main/java/de/edux/ml/mlp/exceptions/LoaderException.java @@ -0,0 +1,7 @@ +package de.edux.ml.mlp.exceptions; + +public class LoaderException extends RuntimeException{ + public LoaderException(String message) { + super(message); + } +} diff --git a/lib/src/main/java/de/edux/ml/mlp/exceptions/UnsupportedLayerException.java b/lib/src/main/java/de/edux/ml/mlp/exceptions/UnsupportedLayerException.java new file mode 100644 index 0000000..a1eb422 --- /dev/null +++ b/lib/src/main/java/de/edux/ml/mlp/exceptions/UnsupportedLayerException.java @@ -0,0 +1,28 @@ +package de.edux.ml.mlp.exceptions; + +/** + * This class represents an exception that is thrown when an unsupported + * layer type is encountered in the MLP context. It extends RuntimeException + * to indicate that this is an unchecked exception that might occur during + * the runtime of the application, particularly when configuring or building + * MLP models with incompatible or unsupported layer types. + */ +public class UnsupportedLayerException extends RuntimeException { + + /** + * Constructs a new UnsupportedLayerException with the default message. + */ + public UnsupportedLayerException() { + super("The specified layer type is not supported."); + } + + /** + * Constructs a new UnsupportedLayerException with a custom message. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the Throwable.getMessage() method. + */ + public UnsupportedLayerException(String message) { + super(message); + } +} diff --git a/lib/src/main/java/de/edux/ml/mlp/exceptions/UnsupportedLossFunction.java b/lib/src/main/java/de/edux/ml/mlp/exceptions/UnsupportedLossFunction.java new file mode 100644 index 0000000..79203d9 --- /dev/null +++ b/lib/src/main/java/de/edux/ml/mlp/exceptions/UnsupportedLossFunction.java @@ -0,0 +1,28 @@ +package de.edux.ml.mlp.exceptions; + +/** + * Represents an exception that is thrown when an unsupported loss function is encountered. + * This class is part of the MLP (Multi-Layer Perceptron) framework and is used to signal + * that the specified loss function is not supported by the current implementation. + * + * It extends RuntimeException, which allows this exception to be thrown and propagated + * through the call stack without being explicitly declared in method signatures. + */ +public class UnsupportedLossFunction extends RuntimeException { + + /** + * Constructs a new UnsupportedLossFunction exception with the default message. + */ + public UnsupportedLossFunction() { + super("Unsupported loss function."); + } + + /** + * Constructs a new UnsupportedLossFunction exception with a custom message. + * + * @param message The custom message that describes this exception. + */ + public UnsupportedLossFunction(String message) { + super(message); + } +} diff --git a/lib/src/main/java/de/edux/ml/mlp/util/TrainingArrays.java b/lib/src/main/java/de/edux/ml/mlp/util/TrainingArrays.java new file mode 100644 index 0000000..070f6ac --- /dev/null +++ b/lib/src/main/java/de/edux/ml/mlp/util/TrainingArrays.java @@ -0,0 +1,27 @@ +package de.edux.ml.mlp.util; + +public class TrainingArrays { + private double[] input; + private double[] output; + + public TrainingArrays(double[] input, double[] output) { + this.input = input; + this.output = output; + } + + public double[] getInput() { + return input; + } + + public void setInput(double[] input) { + this.input = input; + } + + public double[] getOutput() { + return output; + } + + public void setOutput(double[] output) { + this.output = output; + } +} diff --git a/lib/src/main/java/de/edux/ml/mlp/util/TrainingMatrices.java b/lib/src/main/java/de/edux/ml/mlp/util/TrainingMatrices.java new file mode 100644 index 0000000..b79ee07 --- /dev/null +++ b/lib/src/main/java/de/edux/ml/mlp/util/TrainingMatrices.java @@ -0,0 +1,29 @@ +package de.edux.ml.mlp.util; + +import de.edux.ml.mlp.core.tensor.Matrix; + +public class TrainingMatrices { + private Matrix input; + private Matrix output; + + public TrainingMatrices(Matrix input, Matrix output) { + this.input = input; + this.output = output; + } + + public Matrix getInput() { + return input; + } + + public Matrix getOutput() { + return output; + } + + public void setInput(Matrix input) { + this.input = input; + } + + public void setOutput(Matrix output) { + this.output = output; + } +} diff --git a/lib/src/main/java/de/edux/ml/mlp/util/Util.java b/lib/src/main/java/de/edux/ml/mlp/util/Util.java new file mode 100644 index 0000000..0be9be0 --- /dev/null +++ b/lib/src/main/java/de/edux/ml/mlp/util/Util.java @@ -0,0 +1,81 @@ +package de.edux.ml.mlp.util; + +import de.edux.ml.mlp.core.tensor.Matrix; +import java.util.Random; + +public class Util { + + private static final Random random = new Random(); + + public static Matrix generateInputMatrix(int rows, int cols) { + return new Matrix(rows, cols, i -> random.nextGaussian()); + } + + public static Matrix generateExpectedMatrix(int rows, int cols) { + Matrix expected = new Matrix(rows, cols, i -> 0); + for (int col = 0; col < cols; col++) { + int randowmRow = random.nextInt(rows); + expected.set(randowmRow, col, 1); + } + + return expected; + } + + public static Matrix generateTrainableExpectedMatrix(int outputRows, Matrix input) { + Matrix expected = new Matrix(outputRows, input.getCols()); + + Matrix columnSum = input.sumColumns(); + columnSum.forEach((row, col, value) -> { + int rowIndex = (int) (outputRows * (Math.sin(value) + 1) / 2.0); + expected.set(rowIndex, col, 1); + }); + + return expected; + + } + + /** + * Generates a matrix with a gaussian distribution and a radius between 0 and outputRows + * + * @param inputRows number of rows + * @param outputRows number of rows + * @param cols number of columns + * @return a matrix with a gaussian distribution and a radius between 0 and outputRows + */ + public static TrainingMatrices generateTrainingMatrices(int inputRows, int outputRows, int cols) { + var io = generateTrainingArrays(inputRows, outputRows, cols); + Matrix input = new Matrix(inputRows, cols, io.getInput()); + Matrix output = new Matrix(outputRows, cols, io.getOutput()); + + return new TrainingMatrices(input, output); + } + + public static TrainingArrays generateTrainingArrays(int inputSize, int outputSize, int numberItems) { + double[] input = new double[inputSize * numberItems]; + double[] output = new double[outputSize * numberItems]; + + int inputPos = 0; + int outputPos = 0; + for (int col = 0; col < numberItems; col++) { + int radius = random.nextInt(outputSize); + + double[] values = new double[inputSize]; + double initialRadius = 0; + for (int row = 0; row < inputSize; row++) { + values[row] = random.nextGaussian(); + initialRadius += values[row] * values[row]; + } + initialRadius = Math.sqrt(initialRadius); + + for (int row = 0; row < inputSize; row++) { + input[inputPos++] = values[row] * radius / initialRadius; + + } + output[outputPos + radius] = 1; + outputPos += outputSize; + + } + return new TrainingArrays(input, output); + } + +} diff --git a/lib/src/main/java/de/edux/ml/nn/config/NetworkConfiguration.java b/lib/src/main/java/de/edux/ml/nn/config/NetworkConfiguration.java deleted file mode 100644 index 4864d01..0000000 --- a/lib/src/main/java/de/edux/ml/nn/config/NetworkConfiguration.java +++ /dev/null @@ -1,18 +0,0 @@ -package de.edux.ml.nn.config; - -import de.edux.functions.activation.ActivationFunction; -import de.edux.functions.initialization.Initialization; -import de.edux.functions.loss.LossFunction; -import java.util.List; - -public record NetworkConfiguration( - int inputSize, - ListThis implementation utilizes a backpropagation algorithm for training the neural network to - * adjust weights and biases, considering a set configuration defined by {@link - * NetworkConfiguration}. The network's architecture is multi-layered, comprising one or more hidden - * layers in addition to the input and output layers. Neurons within these layers utilize activation - * functions defined per layer through the configuration. - * - *
The training process adjusts the weights and biases of neurons within the network based on the - * error between predicted and expected outputs. Additionally, the implementation provides - * functionality to save and restore the best model achieved during training based on accuracy. - * Early stopping is applied during training to prevent overfitting and unnecessary computational - * expense by monitoring the performance improvement across epochs. - * - *
Usage example: - * - *
- * NetworkConfiguration config = ... ; - * double[][] testFeatures = ... ; - * double[][] testLabels = ... ; - * - * MultilayerPerceptron mlp = new MultilayerPerceptron(config, testFeatures, testLabels); - * mlp.train(features, labels); - * - * double accuracy = mlp.evaluate(testFeatures, testLabels); - * double[] prediction = mlp.predict(singleInput); - *- * - *
Note: This implementation logs informative messages, such as accuracy per epoch, using SLF4J
- * logging.
- *
- * @see de.edux.api.Classifier
- * @see de.edux.ml.nn.network.Neuron
- * @see de.edux.ml.nn.config.NetworkConfiguration
- * @see de.edux.functions.activation.ActivationFunction
- */
-public class MultilayerPerceptron implements Classifier {
- private static final Logger LOG = LoggerFactory.getLogger(MultilayerPerceptron.class);
-
- private final NetworkConfiguration config;
- private final ActivationFunction hiddenLayerActivationFunction;
- private final ActivationFunction outputLayerActivationFunction;
- private final double[][] testFeatures;
- private final double[][] testLabels;
- private List