This project provides a C++ library with tools for creating and training neural networks. The core components of the library are:
- The
NeuroNetclass (fromsrc/neural_network/neuronet.h): Used for defining the structure and operations of neural networks. - The
GeneticAlgorithmclass (fromsrc/optimization/genetic_algorithm.h): Used for training and optimizing these neural networks using evolutionary strategies. It also includes a matrix library for underlying mathematical operations.
This section provides details on the core classes and data structures used in the NeuroNet library.
- Role: The
NeuroNetclass is the main container and manager for a neural network. It encapsulates all the layers and provides an interface for interacting with the network as a whole. - Capabilities: It is responsible for managing the overall architecture of the network, including the number and size of its layers. It handles the flow of data through the network, from input to output.
- Key Functionalities:
ResizeNeuroNet(int pLayerCount): Adjusts the total number of layers in the network.ResizeLayer(int pLayerIndex, int pLayerSize): Modifies the number of neurons in a specific layer. The input size of the first layer is set bySetInputSize, and for subsequent layers, it's determined by the output size of the preceding layer.SetInputSize(int pInputSize): Defines the number of input features the network expects.SetInput(const Matrix::Matrix<float>& pInputMatrix): Provides an input matrix to the first layer of the network.GetOutput(): Processes the current input through all layers and returns the final output matrix from the last layer.get_all_layer_weights(),set_all_layer_weights(...): Access and modify the weights of all layers, typically as a vector ofLayerWeightsstructs.get_all_layer_biases(),set_all_layer_biases(...): Access and modify the biases of all layers, typically as a vector ofLayerBiasesstructs.get_all_weights_flat(),set_all_weights_flat(...): Get or set all network weights as a single flat vector of floats, useful for optimization algorithms.get_all_biases_flat(),set_all_biases_flat(...): Get or set all network biases as a single flat vector of floats.save_model(const std::string& filename) const: Saves the network structure and parameters to a JSON file.static NeuroNet load_model(const std::string& filename): Loads a network from a JSON file.
- Source: Defined in
src/neural_network/neuronet.h.
- Role: The
NeuroNetLayerclass represents an individual layer within aNeuroNet. It's a fundamental building block of the network. - Responsibilities: Each
NeuroNetLayerinstance manages its own set of weights and biases. Its primary responsibility is to perform the forward propagation calculation: taking an input matrix, multiplying it by its weight matrix, adding its bias matrix, and producing an output matrix. It also handles applying a configured activation function. - Interaction: The
NeuroNetclass contains astd::vector<NeuroNetLayer>to store and manage all the layers that constitute the network. WhenNeuroNet::GetOutput()is called, it iteratively passes the output of one layer as the input to the next. - Key Functionalities:
ResizeLayer(int pInputSize, int pLayerSize): Configures the dimensions of the layer's weight and bias matrices.SetInput(const Matrix::Matrix<float>& pInputMatrix): Sets the input for this specific layer.CalculateOutput(): Computes the layer's output based on its current input, weights, biases, and selected activation function.ReturnOutputMatrix(): Returns the last computed output matrix.SetWeights(LayerWeights pWeights),get_weights(): Set or get the layer's weights.SetBiases(LayerBiases pBiases),get_biases(): Set or get the layer's biases.SetActivationFunction(ActivationFunctionType pActivationFunction): Sets the activation function for the layer.get_activation_type() const: Gets the currently configured activation function type.
- Source: Defined in
src/neural_network/neuronet.h.
- Purpose: These are simple C++ structures designed for data aggregation.
LayerWeights: Holds anint WeightCountand astd::vector<float> WeightsVector. It's used to package all weight values for a singleNeuroNetLayer.LayerBiases: Holds anint BiasCountand astd::vector<float> BiasVector. It's used to package all bias values for a singleNeuroNetLayer.
- Usage: They provide a convenient way to get and set weights and biases for layers, facilitating the transfer of these parameters, for example, when initializing a network or when a genetic algorithm modifies them.
- Source: Defined in
src/neural_network/neuronet.h.
- Role: The
Optimization::GeneticAlgorithmclass is used to train or evolve the parameters (weights and biases) ofNeuroNetinstances. It employs evolutionary strategies to search for optimal network configurations. - Concepts:
- Population: The GA maintains a collection (
std::vector<NeuroNet::NeuroNet>) ofNeuroNetindividuals. Each individual represents a potential solution (a specific set of weights and biases). - Fitness Function: The user must provide a fitness function (
std::function<double(NeuroNet::NeuroNet&)>). This function evaluates a givenNeuroNetindividual and returns a score (typically, higher is better) indicating how well it performs a target task. - Selection: Based on their fitness scores, individuals are selected to become "parents" for the next generation. This class uses tournament selection and elitism (ensuring the best individual from one generation is carried over to the next).
- Crossover: Selected parents are combined to produce "offspring." This involves exchanging genetic material (parts of their flattened weight and bias vectors) with a certain
crossover_rate. The goal is to combine beneficial traits from different parents. - Mutation: Each weight and bias in an offspring's network has a small chance (
mutation_rate) of being randomly altered. This introduces new genetic variations into the population, helping to avoid premature convergence and explore more of the solution space.
- Population: The GA maintains a collection (
- Key Functionalities:
- Constructor
GeneticAlgorithm(...): Initializes the GA with parameters like population size, mutation rate, crossover rate, number of generations, and atemplate_networkwhich defines the architecture of the individuals. initialize_population(): Creates the initial population of randomNeuroNetindividuals based on the template network.run_evolution(const std::function<double(NeuroNet::NeuroNet&)>& fitness_function): Executes the main evolutionary loop for a specified number of generations. This involves repeated cycles of fitness evaluation, selection, crossover, and mutation.evolve_one_generation(...): Carries out a single step of the evolutionary process.get_best_individual(): After the evolution process, this function returns theNeuroNetindividual that achieved the highest fitness score.
- Constructor
- Source: Defined in
src/optimization/genetic_algorithm.h.
NeuroNetLayer now supports pluggable activation functions. You can set an activation function for each layer individually. After the standard linear transformation (InputMatrix * WeightMatrix) + BiasMatrix, the selected activation function is applied to the result.
Supported activation functions (defined in NeuroNet::ActivationFunctionType):
None(0): No activation (linear output). This is the default.ReLU(1): Rectified Linear Unit. Output ismax(0, x).LeakyReLU(2): Leaky Rectified Linear Unit. Output isxifx > 0, otherwisealpha*x(current alpha = 0.01).ELU(3): Exponential Linear Unit. Output isxifx > 0, otherwisealpha*(exp(x)-1)(current alpha = 1.0).Softmax(4): Softmax function. Normalizes outputs to a probability distribution, suitable for output layers in classification tasks.
#include "neural_network/neuronet.h" // Or appropriate path
#include "math/matrix.h" // For Matrix::Matrix
int main() {
// Create a layer
NeuroNet::NeuroNetLayer myLayer;
myLayer.ResizeLayer(10, 5); // 10 inputs, 5 neurons
// Set ReLU activation for this layer
myLayer.SetActivationFunction(NeuroNet::ActivationFunctionType::ReLU);
// --- Setup: Provide dummy input, weights, and biases for demonstration ---
Matrix::Matrix<float> input(1, 10); // 1 sample, 10 features
for(int i=0; i<10; ++i) input[0][i] = (i % 3) - 1.0f; // Some -1, 0, 1 values
NeuroNet::LayerWeights weights;
weights.WeightCount = 10 * 5;
weights.WeightsVector.assign(weights.WeightCount, 0.1f); // All weights 0.1
NeuroNet::LayerBiases biases;
biases.BiasCount = 5;
biases.BiasVector.assign(biases.BiasCount, 0.05f); // All biases 0.05
myLayer.SetInput(input);
myLayer.SetWeights(weights);
myLayer.SetBiases(biases);
// --- End Setup ---
// Calculate output - it will have ReLU applied
Matrix::Matrix<float> layerOutput = myLayer.CalculateOutput();
// Example with Softmax for an output layer
NeuroNet::NeuroNetLayer outputLayer;
outputLayer.ResizeLayer(20, 3); // 20 inputs from previous layer, 3 output classes
outputLayer.SetActivationFunction(NeuroNet::ActivationFunctionType::Softmax);
// ... further setup for outputLayer (input, weights, biases) ...
// (Code to print layerOutput would go here if desired)
return 0;
}
This library supports saving and loading NeuroNet models to and from a human-readable JSON format using its internal custom JSON utilities. This allows for model persistence, inspection, and transfer primarily within the library's ecosystem.
The library supports exporting detailed training progress and model architectures using its internal custom JSON tools.
training_metrics.json: This file is generated byGeneticAlgorithm::export_training_metrics_json()and uses the library's custom JSON format. It captures:- Overall statistics for a training run (start/end times, total generations, overall best fitness).
- The best model's architecture, embedded as an escaped JSON string (which itself is also in the custom library format).
- Per-generation metrics (average fitness, best fitness, loss, accuracy).
- Model Files (e.g.,
best_model_custom_format.json): These files are generated byNeuroNet::save_model()and also use the library's custom JSON format. They are primarily intended for use withNeuroNet::load_model().
An example script examples/basic_usage_export.cpp (target name BasicUsageExport after building) demonstrates how to generate these files. This script:
- Configures and trains a simple
NeuroNetusingGeneticAlgorithm. - Calls
ga_instance.export_training_metrics_json("training_metrics.json")to save the training log. - Retrieves the best model and saves it using
best_model.save_model("best_model_custom_format.json").
The key output files demonstrated are:
training_metrics.jsonbest_model_custom_format.json
This entire file is generated as a string by the custom JsonValue::ToString() method.
// Content of training_metrics.json
{
"start_time": "YYYY-MM-DDTHH:MM:SSZ",
"end_time": "YYYY-MM-DDTHH:MM:SSZ",
"total_generations": 100.0, // Custom JSON often outputs numbers as doubles
"overall_best_fitness": 0.95,
"best_model_architecture_params_custom_json_string": "{\"input_size\":2.0,\"layer_count\":2.0,\"layers\":[...]}", // Escaped JSON string
"generation_data": [
{
"generation_number": 1.0,
"average_fitness": 0.85,
"best_fitness": 0.92,
"loss": null, // Custom JSON null
"accuracy": null // Custom JSON null
}
// ... more generations
]
}start_time/end_time: Timestamps for the training run (string).total_generations: The number of generations run (number, likely double).overall_best_fitness: The best fitness score achieved during the run (number, likely double).best_model_architecture_params_custom_json_string: An escaped JSON string representing the best model's architecture, generated byNeuroNet::to_custom_json_string(). This string itself is in the custom JSON format.generation_data: An array of objects, where each object represents metrics for one generation.loss/accuracy: Placeholders; default tonullif not explicitly computed by the fitness function.
- Note: The custom JSON library might have specific output characteristics (e.g., numbers as doubles
100.0,nullrepresentation as the string "null").
This refers to the content of the model_custom_json_string (once unescaped and parsed from training_metrics.json) and the format of files saved by NeuroNet::save_model() (e.g., best_model_custom_format.json).
// Content of model_custom_json_string (after parsing the string)
// or content of a file from NeuroNet::save_model()
{
"input_size": 2.0,
"layer_count": 2.0,
"layers": [
{
"input_size": 2.0,
"layer_size": 3.0,
"activation_function": "ReLU", // String representation
"weights": {
"rows": 2.0,
"cols": 3.0,
"data": [0.1, -0.2, /* ... */, 0.5]
},
"biases": {
"rows": 1.0,
"cols": 3.0,
"data": [0.01, 0.02, 0.03]
}
}
// ... more layers
]
}activation_function: Stored as a string (e.g., "ReLU", "Softmax", "None").- Numeric values (like sizes, rows, cols) are typically output as doubles (e.g.,
2.0) by the custom JSON library. - The structure is defined by
NeuroNet::to_custom_json_string()and used byNeuroNet::save_model()/NeuroNet::load_model().
The following Python script demonstrates how to load training_metrics.json, parse its content (including the embedded model string), and visualize training progress.
import json
import matplotlib.pyplot as plt
import pandas as pd
file_path = "training_metrics.json"
metrics_data = None
# Load and parse the main training metrics file
try:
with open(file_path, "r") as f:
# The entire file is a string output from the custom JSON library.
# We assume Python's json.loads() can handle this format.
# If the custom format is very non-standard (e.g., uses single quotes, unquoted keys),
# this step might require custom preprocessing or a more lenient parser.
metrics_data_str = f.read()
metrics_data = json.loads(metrics_data_str)
except json.JSONDecodeError as e:
print(f"Error decoding {file_path}: {e}")
print("Note: The custom JSON format from the C++ library might require specific parsing "
"if it's not strictly standard (e.g., regarding quotes, trailing commas, number formats).")
exit()
except FileNotFoundError:
print(f"Error: {file_path} not found.")
exit()
if not metrics_data:
exit()
# Extract and parse the embedded custom JSON model string
model_json_str_key = "best_model_architecture_params_custom_json_string"
overall_fitness_key = "overall_best_fitness"
if model_json_str_key in metrics_data:
best_model_json_string = metrics_data[model_json_str_key]
if isinstance(best_model_json_string, str) and best_model_json_string: # Check if it's a non-empty string
try:
# Python's json.loads handles unescaping of strings from a parent JSON object.
best_model_structure = json.loads(best_model_json_string)
print("Best Model Structure (from custom JSON string):")
print(json.dumps(best_model_structure, indent=4))
except json.JSONDecodeError as e:
print(f"Error decoding embedded model string: {e}")
print(f"Model string was: {best_model_json_string}")
elif best_model_json_string: # It might already be parsed if not properly escaped, or be an error object
print("Embedded model data (potentially pre-parsed or error object):")
print(json.dumps(best_model_json_string, indent=4)) # Print what we got
else:
print("Embedded model string is empty or missing.")
else:
print(f"Key '{model_json_str_key}' not found in metrics_data.")
if overall_fitness_key in metrics_data:
print(f"Overall best fitness: {metrics_data[overall_fitness_key]}")
else:
print(f"Key '{overall_fitness_key}' not found in metrics_data.")
# Convert generation data to a Pandas DataFrame for plotting
generation_data_list = metrics_data.get("generation_data", [])
if generation_data_list: # Check if list is not empty
df_generations = pd.DataFrame(generation_data_list)
required_cols = ["generation_number", "average_fitness", "best_fitness"]
if all(col in df_generations.columns for col in required_cols):
plt.figure(figsize=(10, 6))
plt.plot(df_generations["generation_number"], df_generations["average_fitness"], label="Average Fitness")
plt.plot(df_generations["generation_number"], df_generations["best_fitness"], label="Best Fitness")
plt.xlabel("Generation")
plt.ylabel("Fitness")
plt.title("Training Progress")
plt.legend()
plt.grid(True)
plt.show()
else:
print("Generation data is missing required columns for plotting.")
else:
print("No generation data to plot or 'generation_data' key missing.")Ensure you have matplotlib and pandas installed (pip install matplotlib pandas).
Files saved by NeuroNet::save_model() (e.g., best_model_custom_format.json) are also in the custom JSON format. They can be loaded and parsed in Python similarly:
# Example for loading a model file directly (e.g., best_model_custom_format.json)
# with open("best_model_custom_format.json", "r") as f:
# try:
# model_file_str = f.read()
# direct_model_data = json.loads(model_file_str)
# print("\nDirectly Loaded Model Data (from custom format file):")
# print(json.dumps(direct_model_data, indent=4))
# except json.JSONDecodeError as e:
# print(f"\nError decoding direct model file: {e}")
# except FileNotFoundError:
# print("\nError: Direct model file not found.")Note on Parsing Custom JSON: The library's custom JsonValue::ToString() method aims to produce valid JSON. However, standard Python JSON parsers are strict. If the custom output has minor non-standard features (e.g., specific number formatting like always including .0, handling of trailing commas, or non-standard null representation if it differs from JSON null), direct parsing with json.loads() might require adjustments or a more lenient parser. For use within the C++ library, JsonParser::Parse() is designed to understand the output of JsonValue::ToString().
This library includes tools and features for performance analysis and optimization:
- Timing Instrumentation: Instrumentation for core operations such as matrix multiplication, neural network forward pass (full pass and per-layer), and key genetic algorithm steps (selection, crossover, mutation, evaluation) has been added. This allows for detailed performance monitoring.
- Activation: To enable the timing output from this instrumentation, the project must be compiled with the
ENABLE_BENCHMARKINGmacro defined. You can define this in your build system or by adding#define ENABLE_BENCHMARKINGat the top of specific.cppfiles before including library headers. - Benchmark Scenarios: The file
tests/test_benchmarks.cppprovides example usage of the instrumented features and serves as a suite for running these benchmarks. It demonstrates how to set up and execute tests for matrix operations, neural network forward passes of varying sizes, and genetic algorithm operations. - OpenMP Optimization: Matrix multiplication (
Matrix<T>::operator*) has been parallelized using OpenMP to leverage multi-core processors for improved performance, especially with large matrices. - Compiling with OpenMP: To benefit from this parallelization, the project must be compiled with OpenMP support enabled. This typically involves setting a compiler flag (e.g.,
/openmpfor MSVC,-fopenmpfor GCC/Clang).
Refer to tests/test_benchmarks.cpp for an example of how to run these benchmarks.
The NeuroNet model is serialized into a JSON object with the following structure:
{
"input_size": <integer>, // Number of input features for the network
"layer_count": <integer>, // Total number of layers in the network
"layers": [ // Array of layer objects
{
"input_size": <integer>, // Number of inputs for this specific layer
"layer_size": <integer>, // Number of neurons (outputs) in this layer
"activation_function": <integer>, // Integer code for ActivationFunctionType (0: None, 1: ReLU, 2: LeakyReLU, 3: ELU, 4: Softmax - refer to src/neural_network/neuronet.h for exact mapping)
"weights": {
"rows": <integer>, // Number of rows in the weight matrix (equal to layer's input_size)
"cols": <integer>, // Number of columns in the weight matrix (equal to layer_size)
"data": [<float>, ...] // Flattened array of weight values (row-major order)
},
"biases": {
"rows": <integer>, // Number of rows in the bias matrix (typically 1)
"cols": <integer>, // Number of columns in the bias matrix (equal to layer_size)
"data": [<float>, ...] // Flattened array of bias values
}
},
// ... more layers ...
]
}To save a NeuroNet model to a file, use the save_model member function.
#include "neural_network/neuronet.h" // Adjust path as needed
#include <string>
#include <iostream> // For std::cout, std::cerr
// Assuming 'my_model' is an instance of NeuroNet::NeuroNet
// and is already configured and trained.
int main() {
// Example: Create a simple model to save
NeuroNet::NeuroNet my_model;
my_model.SetInputSize(2);
my_model.ResizeNeuroNet(1);
my_model.ResizeLayer(0,1);
my_model.NeuroNetVector[0].SetActivationFunction(NeuroNet::ActivationFunctionType::ReLU); // Example activation
// Add some dummy weights/biases for a complete file
NeuroNet::LayerWeights lw;
lw.WeightCount = 2*1;
lw.WeightsVector = {0.5f, -0.5f};
my_model.NeuroNetVector[0].SetWeights(lw);
NeuroNet::LayerBiases lb;
lb.BiasCount = 1;
lb.BiasVector = {0.1f};
my_model.NeuroNetVector[0].SetBiases(lb);
std::string filename = "my_neural_network.json";
bool success = my_model.save_model(filename);
if (success) {
std::cout << "Model saved successfully to " << filename << std::endl;
} else {
std::cerr << "Error saving model to " << filename << std::endl;
}
return 0;
}To load a NeuroNet model from a file, use the static load_model function. This function returns a new NeuroNet object.
#include "neural_network/neuronet.h" // Adjust path as needed
#include <string>
#include <stdexcept> // For std::runtime_error
#include <iostream> // For std::cout, std::cerr
// Assuming "my_neural_network.json" exists from the saving example.
int main() {
std::string load_filename = "my_neural_network.json";
NeuroNet::NeuroNet loaded_model;
try {
loaded_model = NeuroNet::NeuroNet::load_model(load_filename);
std::cout << "Model loaded successfully from " << load_filename << std::endl;
// Model loaded successfully, use 'loaded_model'
// Example: Print basic info about the loaded model
std::cout << "Loaded model input size: " << loaded_model.InputSize << std::endl;
std::cout << "Loaded model has " << loaded_model.NeuroNetVector.size() << " layers." << std::endl;
if (!loaded_model.NeuroNetVector.empty()) {
std::cout << "First layer output size: " << loaded_model.NeuroNetVector[0].LayerSize() << std::endl;
std::cout << "First layer activation: " << static_cast<int>(loaded_model.NeuroNetVector[0].get_activation_type()) << std::endl;
}
} catch (const std::runtime_error& e) {
std::cerr << "Error loading model: " << e.what() << std::endl;
}
return 0;
}This section provides C++ code examples to illustrate how to use the NeuroNet library for common tasks.
This example shows how to instantiate and configure a NeuroNet::NeuroNet.
#include "neural_network/neuronet.h" // For NeuroNet::NeuroNet
#include <iostream>
#include <vector>
int main() {
// Create a NeuroNet instance
NeuroNet::NeuroNet myNetwork;
// Set the input size for the network (e.g., 5 input features)
int inputFeatureCount = 5;
myNetwork.SetInputSize(inputFeatureCount);
// Configure the network architecture: 2 layers (1 hidden, 1 output)
myNetwork.ResizeNeuroNet(2); // Total number of layers
// Define layer sizes
int hiddenLayerNeurons = 10;
int outputLayerNeurons = 2;
// Size the first layer (hidden layer)
// Input size for layer 0 is taken from myNetwork.SetInputSize()
// Output size is hiddenLayerNeurons
myNetwork.ResizeLayer(0, hiddenLayerNeurons);
// Size the second layer (output layer)
// Input size for layer 1 is taken from layer 0's output size (hiddenLayerNeurons)
// Output size is outputLayerNeurons
myNetwork.ResizeLayer(1, outputLayerNeurons);
std::cout << "Neural network created with " << inputFeatureCount << " inputs, "
<< "1 hidden layer with " << hiddenLayerNeurons << " neurons, and "
<< "1 output layer with " << outputLayerNeurons << " neurons." << std::endl;
return 0;
}You might want to set weights and biases manually if you're loading a pre-trained model or for specific testing scenarios. You can do this per layer or for the entire network using flattened vectors.
#include "neural_network/neuronet.h"
#include <vector>
#include <iostream>
// Helper function to calculate total weights for a given architecture
// (This is a simplified example; NeuroNet does this internally when you use flat vectors)
int calculate_total_weights(int input_size, const std::vector<int>& layer_sizes) {
int total_weights = 0;
int current_input_size = input_size;
for (int neurons_in_layer : layer_sizes) {
total_weights += current_input_size * neurons_in_layer;
current_input_size = neurons_in_layer; // Output of current layer is input to next
}
return total_weights;
}
// Helper function to calculate total biases
int calculate_total_biases(const std::vector<int>& layer_sizes) {
int total_biases = 0;
for (int neurons_in_layer : layer_sizes) {
total_biases += neurons_in_layer;
}
return total_biases;
}
int main() {
NeuroNet::NeuroNet network;
int inputSize = 3;
network.SetInputSize(inputSize);
std::vector<int> layer_neuron_counts = {4, 2}; // Hidden layer: 4 neurons, Output layer: 2 neurons
network.ResizeNeuroNet(layer_neuron_counts.size());
network.ResizeLayer(0, layer_neuron_counts[0]); // Hidden layer
network.ResizeLayer(1, layer_neuron_counts[1]); // Output layer
// Example using flat vectors (set_all_weights_flat / set_all_biases_flat)
// This is generally more convenient if you have all parameters in a single sequence.
int total_weights_count = calculate_total_weights(inputSize, layer_neuron_counts);
std::vector<float> all_weights_flat(total_weights_count);
// Populate all_weights_flat with your desired weight values
// For demonstration, let's fill with 0.1
for(size_t i = 0; i < all_weights_flat.size(); ++i) all_weights_flat[i] = 0.1f * (i + 1);
int total_biases_count = calculate_total_biases(layer_neuron_counts);
std::vector<float> all_biases_flat(total_biases_count);
// Populate all_biases_flat with your desired bias values
// For demonstration, let's fill with 0.05
for(size_t i = 0; i < all_biases_flat.size(); ++i) all_biases_flat[i] = 0.05f * (i + 1);
bool weights_set = network.set_all_weights_flat(all_weights_flat);
bool biases_set = network.set_all_biases_flat(all_biases_flat);
if (weights_set && biases_set) {
std::cout << "Weights and biases set successfully using flat vectors." << std::endl;
} else {
std::cerr << "Error setting weights or biases using flat vectors." << std::endl;
}
// Alternatively, using LayerWeights and LayerBiases structs (per-layer basis)
// This requires more manual construction of vectors for each layer.
// NeuroNet::LayerWeights layer0_weights;
// layer0_weights.WeightCount = inputSize * layer_neuron_counts[0];
// layer0_weights.WeightsVector.assign(layer0_weights.WeightCount, 0.1f);
// // ... and so on for other layers and biases ...
// std::vector<NeuroNet::LayerWeights> all_layer_weights_structs;
// std::vector<NeuroNet::LayerBiases> all_layer_biases_structs;
// // ... populate these vectors ...
// network.set_all_layer_weights(all_layer_weights_structs);
// network.set_all_layer_biases(all_layer_biases_structs);
return 0;
}This example demonstrates how to feed input data to the network and get its output.
It assumes myNetwork is an already configured NeuroNet::NeuroNet object (e.g., from Example 1).
#include "neural_network/neuronet.h"
#include "math/matrix.h" // For Matrix::Matrix<float>. Ensure "math/matrix.h" is accessible (e.g., by adding the "src/" directory to your include paths).
#include <iostream>
#include <vector>
int main() {
// Assume myNetwork is already created and configured (e.g., from Example 1)
NeuroNet::NeuroNet myNetwork;
int inputFeatureCount = 5;
myNetwork.SetInputSize(inputFeatureCount);
myNetwork.ResizeNeuroNet(2);
myNetwork.ResizeLayer(0, 10); // Hidden layer
myNetwork.ResizeLayer(1, 2); // Output layer
// For this example, weights and biases would be randomly initialized by default
// or could be set as in Example 2.
// Create an input matrix (1 sample, inputFeatureCount features)
Matrix::Matrix<float> inputData(1, inputFeatureCount);
// Populate the input matrix with some data
std::cout << "Input data: ";
for (int i = 0; i < inputFeatureCount; ++i) {
inputData[0][i] = static_cast<float>(i + 1) * 0.5f;
std::cout << inputData[0][i] << " ";
}
std::cout << std::endl;
// Set the input to the network
if (myNetwork.SetInput(inputData)) {
// Get the output from the network
Matrix::Matrix<float> outputData = myNetwork.GetOutput();
// Access the output data
// outputData is a matrix, typically 1xN where N is the number of neurons in the output layer.
std::cout << "Network output: ";
for (int i = 0; i < outputData.cols(); ++i) {
std::cout << outputData[0][i] << " ";
}
std::cout << std::endl;
} else {
std::cerr << "Error: Failed to set input to the network." << std::endl;
std::cerr << "Check if the network has layers and input size is configured correctly." << std::endl;
}
return 0;
}This example outlines the steps to train a neural network using the Optimization::GeneticAlgorithm.
#include "optimization/genetic_algorithm.h" // For Optimization::GeneticAlgorithm
#include "neural_network/neuronet.h" // For NeuroNet::NeuroNet
#include "math/matrix.h" // For Matrix::Matrix (used in fitness function)
#include <iostream>
#include <vector>
#include <numeric> // For std::accumulate in more complex fitness functions
#include <cmath> // For std::fabs or other math in fitness functions
#include <limits> // For std::numeric_limits
// --- Main Application ---
int main() {
// a. Create a Template Network for the GA
NeuroNet::NeuroNet templateNetwork;
int inputSize = 3; // Example: 3 input features
int outputSize = 1; // Example: 1 output neuron
templateNetwork.SetInputSize(inputSize);
templateNetwork.ResizeNeuroNet(2); // 1 hidden layer, 1 output layer
templateNetwork.ResizeLayer(0, 5); // Hidden layer with 5 neurons
templateNetwork.ResizeLayer(1, outputSize); // Output layer with 1 neuron
// b. Define a Fitness Function
// This function evaluates a NeuroNet individual and returns a score (higher is better).
// The logic is highly problem-specific.
// This conceptual example tries to make the network output the sum of its inputs.
auto fitness_function = [&](NeuroNet::NeuroNet& nn) -> double {
// The 'inputSize' variable (captured by the lambda) defines the expected input dimension
// for this specific fitness evaluation. The network 'nn' should be configured
// (e.g., via its template in the GA) to match this inputSize.
// A public NeuroNet::GetInputSize() method is not available, but the network's
// internal InputSize member is set via SetInputSize().
// Create some test input data
Matrix::Matrix<float> testInput(1, inputSize);
float expectedOutputSum = 0.0f;
for (int i = 0; i < inputSize; ++i) {
testInput[0][i] = static_cast<float>(rand() % 100) / 10.0f; // Random input 0.0 to 9.9
expectedOutputSum += testInput[0][i];
}
nn.SetInput(testInput);
Matrix::Matrix<float> output = nn.GetOutput();
if (output.cols() != outputSize) {
// Output layer not configured as expected or GetOutput() failed
return 0.0; // Low fitness for misconfigured network
}
// Calculate fitness: inverse of absolute error from the expected sum
// (assuming output[0][0] is the relevant output for this problem)
double error = std::fabs(output[0][0] - expectedOutputSum);
// Add 1.0 to avoid division by zero and to ensure fitness is positive.
// Smaller error -> higher fitness.
return 1.0 / (1.0 + error);
};
// c. Instantiate GeneticAlgorithm
int populationSize = 100; // Number of NeuroNet individuals in the population
double mutationRate = 0.05; // Probability of mutating a weight/bias
double crossoverRate = 0.7; // Probability of parents creating offspring
int numGenerations = 200; // Number of generations to evolve
Optimization::GeneticAlgorithm ga(
populationSize,
mutationRate,
crossoverRate,
numGenerations,
templateNetwork // The configured template network
);
// d. Run Evolution
std::cout << "Starting genetic algorithm evolution..." << std::endl;
ga.run_evolution(fitness_function);
std::cout << "Evolution finished." << std::endl;
// e. Get Best Individual
NeuroNet::NeuroNet bestNetwork = ga.get_best_individual();
// Now, 'bestNetwork' contains the weights and biases of the network
// that performed best according to your fitness function.
// You can use it for inference, save its parameters, etc.
std::cout << "Best network obtained. Evaluating its fitness one last time:" << std::endl;
double bestFitness = fitness_function(bestNetwork); // Re-evaluate or use stored best fitness
std::cout << "Fitness of the best individual: " << bestFitness << std::endl;
// Example: Test the best network with a new input
Matrix::Matrix<float> finalTestInput(1, inputSize);
float finalExpectedSum = 0.0f;
std::cout << "Testing best network with new input: ";
for (int i = 0; i < inputSize; ++i) {
finalTestInput[0][i] = static_cast<float>(i + 1) * 1.1f;
finalExpectedSum += finalTestInput[0][i];
std::cout << finalTestInput[0][i] << " ";
}
std::cout << std::endl;
std::cout << "Expected sum for this input: " << finalExpectedSum << std::endl;
bestNetwork.SetInput(finalTestInput);
Matrix::Matrix<float> finalOutput = bestNetwork.GetOutput();
std::cout << "Actual output from best network: " << finalOutput[0][0] << std::endl;
return 0;
}These examples should provide a solid starting point for using the NeuroNet library. Remember to adapt the network architectures, fitness functions, and data handling to your specific application.
This section provides guidelines for users who want to extend the NeuroNet library or integrate it into their own C++ projects.
Before making significant modifications, it's recommended to thoroughly understand the existing class structures, particularly NeuroNet::NeuroNet and NeuroNet::NeuroNetLayer.
-
Activation Functions: (This part is now updated by the library)
- The
NeuroNetLayer::CalculateOutputmethod now applies the configured activation function after the linear transformation. - Supported functions are listed in the "Activation Functions" section.
- To add new activation functions:
- Add a new value to the
ActivationFunctionTypeenum insrc/neural_network/neuronet.h. - Implement the corresponding private helper method in
NeuroNetLayer(e.g.,ApplyMyNewFunction). - Add a case for your new function type in
NeuroNetLayer::CalculateOutput. - Update documentation and serialization logic if necessary.
- Add a new value to the
- The
-
New Layer Types:
- The current
NeuroNetLayerprovides a standard fully connected layer. To introduce specialized layer behaviors (e.g., convolutional layers, recurrent layers):- You could define new classes that are distinct from
NeuroNetLayerand manage them within theNeuroNetclass. This would likely require modifications toNeuroNetto handle different layer types. - Alternatively, if a common interface for layers can be established (e.g., an abstract
BaseLayerclass),NeuroNetLayerand your new layer types could inherit from it.NeuroNetwould then manage a collection ofBaseLayerpointers or references. This is a more significant architectural change.
- You could define new classes that are distinct from
- The current
-
Custom Genetic Algorithm Components:
- The
Optimization::GeneticAlgorithmclass provides a standard implementation. If you require different selection strategies (e.g., roulette wheel instead of tournament), crossover methods (e.g., multi-point crossover), or mutation operators, you might consider:- Modifying the existing
GeneticAlgorithmclass methods (selection,crossover,mutate). - Creating a new class, perhaps inheriting from
GeneticAlgorithmif you want to reuse some of its functionality (though C++ inheritance might not be the most straightforward approach here without a base class designed for extension). More likely, you would write a new GA class inspired by the current one.
- Modifying the existing
- The
-
General Advice:
- Start by understanding the data flow: how
NeuroNetmanagesNeuroNetLayerinstances, how weights and biases are stored and updated, and how theGeneticAlgorithminteracts withNeuroNetindividuals. - Refer to the existing code for patterns on matrix operations, parameter handling, and class interactions.
- Start by understanding the data flow: how
-
Linking:
- The NeuroNet library is built as a static library by default (as per
CMakeLists.txt, which specifiesadd_library(neuronet STATIC ...)). - When you build this project, it will produce a static library file (e.g.,
libneuronet.aon Linux/macOS,neuronet.libon Windows). - Your C++ project will need to link against this compiled static library file. How you do this depends on your build system (e.g., for g++, you'd use the
-Loption for the directory and-lneuronetto link).
- The NeuroNet library is built as a static library by default (as per
-
Header Files:
- Your project must include the relevant header files to use the library's classes and functions. Primarily, these are:
neural_network/neuronet.h(forNeuroNet::NeuroNet,NeuroNet::NeuroNetLayer, etc.)optimization/genetic_algorithm.h(forOptimization::GeneticAlgorithm)math/matrix.h(as this is used in the interfaces, e.g.NeuroNet::SetInput())utilities/json/json.hpp(if you interact with serialization or need JSON utilities, though it's included byneuronet.h)
- Your project must include the relevant header files to use the library's classes and functions. Primarily, these are:
-
Include Paths:
- You need to configure your compiler's include paths so it can find the library's header files. This typically involves adding the
srcdirectory (or a custom install directory if you set one up) of the NeuroNet library to your project's include directories. - For example, if you clone this repository into
libs/neuronet-ga, you might addlibs/neuronet-ga/srcto your include paths.
- You need to configure your compiler's include paths so it can find the library's header files. This typically involves adding the
-
Runtime (for DLL - Not applicable by default):
- Since the library is built as a static library by default, there's no separate DLL file to distribute or manage at runtime in the same way as a dynamic library. The library's code is linked directly into your executable. If you were to change it to a DLL, then the DLL would need to be accessible at runtime.
-
CMake Integration (Recommended):
- If your project uses CMake, you can integrate the NeuroNet library more easily. After building the NeuroNet library (or if you have it as a pre-built package):
- Ensure CMake can find the NeuroNet library. This might involve setting
CMAKE_PREFIX_PATHif installed to a non-standard location, or usingfind_packageif NeuroNet provided a CMake config file (it currently doesn't, so manual path setup is more likely). - Use
find_libraryto locate the compiledneuronetstatic library if needed, or directly specify the path to the.a/.libfile. - Use
target_link_librariesin yourCMakeLists.txtto link your executable or library againstneuronet.# Example (assuming NeuroNet library is found or path is known) # target_link_libraries(YourTarget PRIVATE path/to/libneuronet.a)
- Use
target_include_directoriesto add the path to the NeuroNet header files (e.g., thesrcdirectory).# Example # target_include_directories(YourTarget PRIVATE path/to/neuronet-ga/src)
- Ensure CMake can find the NeuroNet library. This might involve setting
- If you are building NeuroNet as part of a larger CMake project (e.g., using
add_subdirectory), you can directly link against theneuronettarget:This automatically handles linking and include directories if NeuroNet's# In your main project's CMakeLists.txt, after add_subdirectory(path/to/neuronet-ga) # target_link_libraries(YourExecutableTarget PRIVATE neuronet)
CMakeLists.txtcorrectly defines itsINTERFACE_INCLUDE_DIRECTORIES.
- If your project uses CMake, you can integrate the NeuroNet library more easily. After building the NeuroNet library (or if you have it as a pre-built package):
- Matrix Library:
* The NeuroNet library relies on an internal header-only
Matrixclass, located insrc/math/matrix.h. * Thismatrix.hfile provides theMatrix::Matrix<T>template class used for all underlying mathematical operations and is included as part of the NeuroNet library's source. - Custom JSON Library:
* The library utilizes an internal custom JSON parsing and manipulation library located in
src/utilities/json/(specificallyjson.hppfor the interface andjson.cppfor the implementation). * This library is used for model serialization/deserialization and other JSON tasks. It is compiled directly into the NeuroNet library. - Setup:
- If building NeuroNet library yourself:
matrix.h(header-only) and the custom JSON library (json.hpp,json.cpp) are part of this repository.matrix.his included directly, and the custom JSON implementation (json.cpp) is compiled into the NeuroNet library. - If integrating NeuroNet into your project: You need to ensure that your compiler can find the NeuroNet header files (which include
src/math/matrix.handsrc/utilities/json/json.hpp). This means thesrcdirectory from the NeuroNet library (or a relevant install path for headers) should be in your include paths. The compiled code for the JSON library and the definitions for the matrix library will be part of the NeuroNet static library you link against.
- If building NeuroNet library yourself:
This README.md provides a general overview of the project. For more detailed information, please refer to the following:
- Module-specific Documentation:
- API Reference (Doxygen):
- Due to potential environment differences, Doxygen HTML generation might not work reliably in all cloud-based IDEs.
- The
Doxyfilein the root of the repository is configured to generate API documentation from the source code comments. - To generate the API documentation locally:
- Ensure you have Doxygen installed (see doxygen.nl).
- Navigate to the root directory of this repository in your terminal.
- Run the command:
doxygen Doxyfile - The HTML documentation will be generated in the
docs/api/htmldirectory. Opendocs/api/html/index.htmlin your browser to view it.
- The generated API documentation (
docs/api/) should not be committed to the repository (it's included in.gitignore).
- Contributing Guidelines: See CONTRIBUTING.md for details on how to contribute to this project.
- Changelog: See CHANGELOG.md for the history of changes.
- Core Neural Network (
NeuroNet,NeuroNetLayer):- Customizable feed-forward neural network architecture.
- Layer-wise configuration of neuron counts and activation functions.
- Support for various activation functions: None (Linear), ReLU, LeakyReLU, ELU, Softmax.
- Serialization/deserialization of network architecture and parameters to/from JSON.
- Vocabulary loading and string input processing for NLP tasks.
- Optimization (
GeneticAlgorithm):- Genetic Algorithm for evolving
NeuroNetweights and biases. - Configurable population size, mutation rate, crossover rate, and generations.
- Tournament selection and elitism.
- Exportable training metrics (fitness per generation, best model architecture).
- Genetic Algorithm for evolving
- Transformer Module (Encoder-Only):
- Implementation of core Transformer components:
EmbeddingLayerfor token embeddings.PositionalEncodingfor sequence position information.ScaledDotProductAttentionandMultiHeadAttention.TransformerFFN(Position-wise Feed-Forward Network).TransformerEncoderLayerstacking attention and FFN.
TransformerModelclass to build encoder-only transformer architectures.- Serialization/deserialization for
TransformerModel.
- Implementation of core Transformer components:
- Math Utilities:
- Generic
Matrix<T>class for 2D matrix operations.- Standard arithmetic (+, -, *), scalar operations.
- Transpose, determinant, inverse.
- OpenMP parallelization for matrix multiplication.
- Extended math functions: GELU, Layer Normalization, Softmax.
- Matrix splitting and combining utilities.
- Generic
- JSON Utilities:
- Custom lightweight JSON parser (
JsonParser) and data representation (JsonValue). - Supports parsing JSON strings into a tree of
JsonValueobjects. - Serialization of
JsonValuetrees back to JSON strings. - Handles basic JSON types: null, boolean, number, string, array, object.
- Includes support for skipping comments in JSON files.
- Custom lightweight JSON parser (
- Build and Testing:
- Built with CMake for cross-platform compilation.
- Unit tests using Google Test framework (
ctestrunnable).
- Benchmarking:
- Optional timing instrumentation for core operations (e.g., matrix multiplication, GA steps) via
ENABLE_BENCHMARKINGmacro. - Example benchmark tests in
tests/test_benchmarks.cpp.
- Optional timing instrumentation for core operations (e.g., matrix multiplication, GA steps) via
- C++ Compiler (supporting C++17 or later)
- CMake (version 3.10 or later)
-
Clone the repository:
git clone <repository_url> cd <repository_name>
-
Create a build directory:
mkdir build cd build -
Configure with CMake:
cmake ..
-
Build the project (library and tests):
make # Or, on Windows with MSVC # cmake --build .
Note on OpenMP: To enable OpenMP for parallelized matrix multiplication, ensure your compiler supports OpenMP and set the appropriate compiler flags (e.g.,
/openmpfor MSVC,-fopenmpfor GCC/Clang). This might be done by modifying theCMAKE_CXX_FLAGSin CMake or setting environment variables before configuring.
This will build the neuronet static library and the test executables.
The project includes a suite of unit tests to ensure functionality and correctness. Tests are located in the tests/ directory, primarily in tests/test_neuronet.cpp.
The tests are built using Google Test.
General Steps to Build and Run Tests:
- Ensure your development environment is set up with a C++ compiler and CMake.
- Navigate to your build directory (or create one if it doesn't exist):
# From the project root mkdir -p build cd build
- Configure the project with CMake:
cmake ..
- Build the project, including the test executables:
cmake --build . # Alternatively, use 'make' if that's your build tool # make
- Run the tests:
- The primary test executable defined in
tests/CMakeLists.txtistest_neuronet. If compiled successfully, it would be located in thetestssubdirectory of your build folder. ctestcan be used if the tests are correctly registered with CTest in CMake (which they are):Alternatively, you can run the test executable directly (e.g.,# From the build directory ctesttests/runTestson Linux/macOS ortests\Debug\runTests.exeon Windows, path may vary based on build type). The previous mention of Catch2 has been removed as the tests primarily use Google Test.
- The primary test executable defined in
This project is licensed under the MIT License - see the LICENSE file for details.
For security reasons, functions that load files—such as NeuroNet::load_model, TransformerModel::load_model, and Vocabulary::load_from_json—enforce a strict path policy:
- No Absolute Paths: Paths starting with a root directory separator (
/or\) or a Windows drive letter (e.g.,C:) are explicitly rejected. - No Directory Traversal: Paths containing the sequence
..are explicitly rejected.
All files must be loaded using relative paths that point to locations within the intended working directory structure of the executing application. This policy prevents potential Local File Inclusion (LFI) vulnerabilities where an attacker might attempt to read arbitrary system files.