![]() |
|---|
Please use with caution and wait for the proper release
Currently, PetalFlow has only been tested on Linux and Windows with x86_64 architecture
In the future, it is planned to extend its usability even to AVR / ARM / ESP (such as Arduino / STM / ESP boards) In the future it is planned to use it even on AVR (like Arduino)
- Add convolution layers
- Python bindings
- Test on ARM and AVR
- More tests and examples
It's possible to build shared library using cmake
cmake -B build -DTESTS=OFF -DLOGGER_LEVEL=1
cmake --build build --config ReleaseShared library will be located inside build directory
Below is an example of an extremely simple classifier capable of just comparing two numbers
-
Include base libraries
#include <stdint.h> #include <stdio.h> #include <stdlib.h> #include "flower.h" #include "random.h"
-
Add functions to print array and create demo dataset
Click to show code
/** * @brief Prints 1D array as 1D or multidimensional array * * @param array pointer to array * @param rows number of rows (height) * @param cols number of columns (width) * @param depth number of channel (depth) */ void print_array(float *array, uint32_t rows, uint32_t cols, uint32_t depth) { for (uint32_t row = 0; row < rows; ++row) { for (uint32_t col = 0; col < cols; ++col) { if (depth > 1) printf("("); for (uint32_t channel = 0; channel < depth; ++channel) { printf("%.4f", array[(row * cols + col) * depth + channel]); if (channel < depth - 1) printf(", "); } if (depth > 1) printf(")"); printf("\t"); } printf("\n"); } } /** * @brief Generates 2D array of random floats from -10.0 to 10.0 * * @param rows number of rows (outer array length) * @param cols number of elements in each internal array * @return float** 2D array of random floats */ float **dense_generate_input_data(uint32_t rows, uint32_t cols) { // Allocate memory for the outer array (rows) float **array = (float **) malloc(rows * sizeof(float *)); if (!array) { printf("Error allocating array for dense_generate_input_data!\n"); return NULL; } for (uint32_t row = 0; row < rows; ++row) { // Allocate memory for each internal array (columns) array[row] = (float *) malloc(cols * sizeof(float)); if (!array[row]) { printf("Error allocating array[row] for dense_generate_input_data!\n"); return NULL; } // Populate the internal array with random float values in (-10, 10] interval for (uint32_t col = 0; col < cols; ++col) { array[row][col] = rk_float_() * 20.f - 10.f; } } return array; } /** * @brief Generates 2D array of expected outputs by comparing 1st and 2nd * elements of input_data array * * @param input_data 2D array of random floats * @param rows number of rows (outer array length) * @return float** true outputs (cols = 2) */ float **dense_generate_output_data(float **input_data, uint32_t rows) { // Allocate memory for the outer array (rows) float **array = (float **) malloc(rows * sizeof(float *)); if (!array) { printf("Error allocating array for dense_generate_output_data!\n"); return NULL; } for (uint32_t row = 0; row < rows; ++row) { // Allocate memory for each internal array (columns) array[row] = (float *) calloc(2U, sizeof(float)); if (!array[row]) { printf("Error allocating array[row] for " "dense_generate_output_data!\n"); return NULL; } // 1 > 2 if (input_data[row][0] > input_data[row][1]) array[row][0] = 1.f; // 1 <= 2 else array[row][1] = 1.f; } return array; }
-
Initialize datasets
Click to show code
// Set random seed // rk_seed_(time(NULL) & 0xFFFFFFFFUL); rk_seed_(0); // 1000 numbers from -10 to 10: 80% train, 20% validation uint32_t train_dataset_length = 800; uint32_t validation_dataset_length = 200; // Generate validation datasets float **train_dataset_inputs = dense_generate_input_data(train_dataset_length, 2U); float **validation_dataset_inputs = dense_generate_input_data(validation_dataset_length, 2U); if (!train_dataset_inputs || !validation_dataset_inputs) { printf("train_dataset_inputs or validation_dataset_inputs allocation failed\n"); return 1U; } // Generate outputs float **train_dataset_outputs = dense_generate_output_data(train_dataset_inputs, train_dataset_length); float **validation_dataset_outputs = dense_generate_output_data(validation_dataset_inputs, validation_dataset_length); if (!train_dataset_outputs || !validation_dataset_outputs) { printf("train_dataset_outputs or est_dataset_outputs allocation failed\n"); return 1U; }
-
Initialize petals
Initializes petal's struct and petal's weights if needed Sets petal->error_code in case of error.
Parameters
petal_type: type of the petal (PETAL_TYPE_...)first: true if it's the first petal (output_left is input data) to prevent error_on_input calculationinput_shape: pointer topetal_shape_sstruct:rows: height of input datacols: width (or size for 1D) of input datadepth: number of channels of input datalength: calculates internally
output_shape: pointer topetal_shape_sstruct:rows: height of output datacols: width (or size for 1D) of output datadepth: number of channels of output datalength: calculates internally
weights: pointer toweights_sstruct (forPETAL_TYPE_DENSE_1D) or NULL for other types:trainable: 1 if weights will be trained or 0 if notinitializer: weights initializer (WEIGHTS_INIT_...)weights: pass NULL to initialize weights or pointer to previously initialized weightscenter: constant forWEIGHTS_INIT_CONSTANTor center of distribution for other initializersdeviation: deviation of distribution (ignored forWEIGHTS_INIT_CONSTANT)
bias_weights: pointer toweights_sstruct (forPETAL_TYPE_DENSE_1D) or NULL for other types:trainable: 1 if bias weights will be trained or 0 if notinitializer: bias weights initializer (WEIGHTS_INIT_...)weights: pass NULL to initialize bias weights or pointer to previously initialized bias weightscenter: constant forWEIGHTS_INIT_CONSTANTor center of distribution for other initializersdeviation: deviation of distribution (ignored forWEIGHTS_INIT_CONSTANT)
activation: pointer toactivation_sstruct or NULL to disable activation:type: activation function (ACTIVATION_...)linear_alpha: factor for linear activation (ax + c) (forACTIVATION_LINEARonly). Default = 1.0linear_const: constant for linear activation (ax + c) (forACTIVATION_LINEARonly). Default = 0.0relu_leak: leak amount (forACTIVATION_RELUonly). Default = 0.01elu_alpha: the value to which an ELU saturates for negative net inputs (forACTIVATION_ELUonly). Default = 0.01swish_beta: beta for turning Swish into E-Swish (forACTIVATION_SWISHonly). Default = 1.0
dropout: ratio of dropped outputs (0 to 1)center: center of normalization forPETAL_TYPE_NORMALIZE_...Default: 0.0deviation: deviation of normalization forPETAL_TYPE_NORMALIZE_...Default: 1.0
Returns
petal_s*: petal's struct
Available weights initializers:
WEIGHTS_INIT_CONSTANTWEIGHTS_INIT_RANDOM_UNIFORMWEIGHTS_INIT_RANDOM_GAUSSIANWEIGHTS_INIT_XAVIER_GLOROT_UNIFORMWEIGHTS_INIT_XAVIER_GLOROT_GAUSSIANWEIGHTS_INIT_KAIMING_HE_UNIFORMWEIGHTS_INIT_KAIMING_HE_GAUSSIAN
Available activation functions:
ACTIVATION_LINEARACTIVATION_RELUACTIVATION_ELUACTIVATION_SOFTSIGNACTIVATION_SIGMOIDACTIVATION_HARD_SIGMOIDACTIVATION_SWISHACTIVATION_SOFTMAXACTIVATION_TANH
petal_s *petal_hidden1 = petal_init(PETAL_TYPE_DENSE_1D, true, &(petal_shape_s){1U, 2U, 1U, 0UL}, &(petal_shape_s){1U, 2U, 1U, 0UL}, &(weights_s){true, WEIGHTS_INIT_XAVIER_GLOROT_GAUSSIAN, 4U, NULL, NULL, 0.f, 1.f, NULL, NULL, 0U}, &(weights_s){true, WEIGHTS_INIT_CONSTANT, 2U, NULL, NULL, 0.f, 1.f, NULL, NULL, 0U}, &(activation_s){ACTIVATION_RELU, 1.f, 0.f, 0.0f, 0.00f, 1.f, NULL}, 0.0f, 0.f, 1.f); petal_s *petal_hidden2 = petal_init(PETAL_TYPE_DENSE_1D, false, &(petal_shape_s){1U, 2U, 1U, 0UL}, &(petal_shape_s){1U, 2U, 1U, 0UL}, &(weights_s){true, WEIGHTS_INIT_XAVIER_GLOROT_GAUSSIAN, 4U, NULL, NULL, 0.f, 1.f, NULL, NULL, 0U}, &(weights_s){true, WEIGHTS_INIT_CONSTANT, 2U, NULL, NULL, 0.f, 1.f, NULL, NULL, 0U}, &(activation_s){ACTIVATION_RELU, 1.f, 0.f, 0.0f, 0.00f, 1.f, NULL}, 0.0f, 0.f, 1.f); petal_s *petal_output = petal_init(PETAL_TYPE_DENSE_1D, false, &(petal_shape_s){1U, 2U, 1U, 0UL}, &(petal_shape_s){1U, 2U, 1U, 0UL}, &(weights_s){true, WEIGHTS_INIT_XAVIER_GLOROT_GAUSSIAN, 6U, NULL, NULL, 0.f, 1.f, NULL, NULL, 0U}, &(weights_s){true, WEIGHTS_INIT_CONSTANT, 2U, NULL, NULL, 0.f, 1.f, NULL, NULL, 0U}, &(activation_s){ACTIVATION_SOFTMAX, 1.f, 0.f, 0.0f, 0.01f, 1.f, NULL}, 0.0f, 0.f, 1.f);
-
Review generated weights
printf("In -> hidden 1 weights:\n"); print_array(petal_hidden1->weights->weights, 2U, 2U, 1U); printf("In -> hidden 1 bias weights:\n"); print_array(petal_hidden1->bias_weights->weights, 1U, 2U, 1U); printf("hidden 1 -> hidden 2 weights:\n"); print_array(petal_hidden2->weights->weights, 2U, 2U, 1U); printf("hidden 1 -> hidden 2 bias weights:\n"); print_array(petal_hidden2->bias_weights->weights, 1U, 2U, 1U); printf("hidden 2 -> out weights:\n"); print_array(petal_output->weights->weights, 2U, 2U, 1U); printf("hidden 2 -> out bias weights:\n"); print_array(petal_output->bias_weights->weights, 1U, 2U, 1U);
In -> hidden 1 weights: 0.0464 0.6727 0.7127 -1.2468 In -> hidden 1 bias weights: 0.0000 0.0000 hidden 1 -> hidden 2 weights: -0.2981 -0.2883 -0.7801 -0.4236 hidden 1 -> hidden 2 bias weights: 0.0000 0.0000 hidden 2 -> out weights: 1.5173 -1.2947 -0.9410 0.3021 hidden 2 -> out bias weights: 0.0000 0.0000 -
Initialize flower
Initializes flower using array of petals.
Parameters
petals: pointer to an array of pointers of petalspetals_length: number of petals
Returns
flower_s*: initialized flower
petal_s *petals[] = {petal_hidden1, petal_hidden2, petal_output}; flower_s *flower = flower_init(petals, 3U);
-
Show model output before training
X1 = 1, X2 = 2
// Show prediction before training printf("Before training [1.0, 2.0] -> [1 > 2, 1 <= 2]:\t\t"); print_array(flower_predict(flower, (float[]){1.f, 2.f}), 1U, 2U, 1U);
Before training [1.0, 2.0] -> [1 > 2, 1 <= 2]: 0.5000 0.5000 -
Initialize optimizer and metrics
Parameters
type: optimizer type (OPTIMIZER_...)learning_rate: learning rate (required for all optimizer types) Default: 0.01momentum: accelerates gradient descent and dampens oscillations (forOPTIMIZER_SGD_MOMENTUM)beta_1: hyperparameter (forOPTIMIZER_RMS_PROPandOPTIMIZER_ADAM) Default: 0.9beta_2: hyperparameter (forOPTIMIZER_ADAM) Default: 0.999
Available metrics:
METRICS_TIME_ELAPSEDMETRICS_LOSS_TRAINMETRICS_ACCURACY_TRAINMETRICS_LOSS_VALIDATIONMETRICS_ACCURACY_VALIDATION
// Initialize optimizer optimizer_s optimizer = (optimizer_s){OPTIMIZER_ADAM, .01f, 0.f, .89f, .99f}; // Initialize metrics metrics_s *metrics = metrics_init(1); metrics_add(metrics, METRICS_TIME_ELAPSED); metrics_add(metrics, METRICS_LOSS_TRAIN); metrics_add(metrics, METRICS_ACCURACY_TRAIN); metrics_add(metrics, METRICS_LOSS_VALIDATION); metrics_add(metrics, METRICS_ACCURACY_VALIDATION);
-
Train model
Early implementation of backpropagation learning.
Parameters
flower: pointer to initializedflower_sstructloss_type: loss function (LOSS_...)optimizer: pointer to initializedoptimizer_sstruct with the following parameters:type: optimizer type (OPTIMIZER_...)learning_rate: learning rate (required for all optimizer types) Default: 0.01momentum: accelerates gradient descent and dampens oscillations (forOPTIMIZER_SGD_MOMENTUM)beta_1: hyperparameter (forOPTIMIZER_RMS_PROPandOPTIMIZER_ADAM) Default: 0.9beta_2: hyperparameter (forOPTIMIZER_ADAM) Default: 0.999
metrics: pointer to initializedmetrics_sstructinputs_train: pointer to array of arrays of training input data (train dataset)outputs_true_train: pointer to array of arrays of training output data (train dataset)outputs_true_train_sparse: pointer to array oflabel_sarrays of sparse training output data (1 = [0, 1, ...])train_length: number of training samples (size of training dataset)inputs_validation: pointer to array of arrays of validation input data (validation dataset)outputs_true_validation: pointer to array of arrays of validation output data (train dataset)outputs_true_validation_sparse: pointer to array oflabel_sarrays of sparse validation output datavalidation_length: number of validation samples (size of validation dataset)batch_size: samples per batchepochs: total number of training epochs
Available loss functions:
LOSS_MEAN_SQUARED_ERRORLOSS_MEAN_SQUARED_LOG_ERRORLOSS_ROOT_MEAN_SQUARED_LOG_ERRORLOSS_MEAN_ABS_ERRORLOSS_BINARY_CROSSENTROPYLOSS_CATEGORICAL_CROSSENTROPY
uint32_t epochs = 10; uint32_t batch_size = 40; flower_train(flower, LOSS_CATEGORICAL_CROSSENTROPY, &optimizer, metrics, train_dataset_inputs, train_dataset_outputs, NULL, train_dataset_length, validation_dataset_inputs, validation_dataset_outputs, NULL, validation_dataset_length, batch_size, epochs);
[2024-04-16 00:28:45] [INFO] [flower_train] Training started [2024-04-16 00:28:45] [INFO] [flower_train] Epoch: 1/10 [====================] 20/20 | 00:00:00 | Tloss: 0.2788 | Tacc: 95.00% | Vloss: 0.2481 | Vacc: 93.50% [2024-04-16 00:28:45] [INFO] [flower_train] Epoch: 2/10 [====================] 20/20 | 00:00:00 | Tloss: 0.1589 | Tacc: 92.50% | Vloss: 0.1467 | Vacc: 96.00% [2024-04-16 00:28:45] [INFO] [flower_train] Epoch: 3/10 [====================] 20/20 | 00:00:00 | Tloss: 0.1022 | Tacc: 95.00% | Vloss: 0.1076 | Vacc: 95.50% [2024-04-16 00:28:45] [INFO] [flower_train] Epoch: 4/10 [====================] 20/20 | 00:00:00 | Tloss: 0.0770 | Tacc: 97.50% | Vloss: 0.0761 | Vacc: 96.50% [2024-04-16 00:28:45] [INFO] [flower_train] Epoch: 5/10 [====================] 20/20 | 00:00:00 | Tloss: 0.0519 | Tacc: 100.00% | Vloss: 0.0515 | Vacc: 98.50% [2024-04-16 00:28:45] [INFO] [flower_train] Epoch: 6/10 [====================] 20/20 | 00:00:00 | Tloss: 0.0199 | Tacc: 100.00% | Vloss: 0.0334 | Vacc: 99.00% [2024-04-16 00:28:45] [INFO] [flower_train] Epoch: 7/10 [=============> ] 20/20 | 00:00:00 | Tloss: 0.0109 | Tacc: 100.00% | Vloss: 0.0185 | Vacc: 100.00% ... -
Test the result
// Test training result on a new data float *result; printf("After training [1.0, 10.0] -> [1 > 2, 1 <= 2]:\t\t"); result = flower_predict(flower, (float[]){1.f, 10.f}); printf("After training [20.0, 10.0] -> [1 > 2, 1 <= 2]:\t\t"); result = flower_predict(flower, (float[]){20.f, 10.f}); print_array(result, 1U, 2U, 1U); printf("After training [-1.0, 10.0] -> [1 > 2, 1 <= 2]:\t\t"); result = flower_predict(flower, (float[]){-1.f, 10.f}); print_array(result, 1U, 2U, 1U);
After training [1.0, 10.0] -> [1 > 2, 1 <= 2]: 0.0072 0.9928 After training [20.0, 10.0] -> [1 > 2, 1 <= 2]: 0.9339 0.0661 After training [-1.0, 10.0] -> [1 > 2, 1 <= 2]: 0.0072 0.9928 -
Free memory
void weights_destroy(weights_s *weights, bool destroy_struct, bool destroy_internal_array)
Frees memory allocated by weights struct.
Parameters
weights: pointer toweights_sstruct or NULLdestroy_struct: true to destroy struct itself (set to false if struct was defined manually)destroy_internal_array: true to also destroyweights->weightsarray
void flower_destroy(flower_s *flower, bool destroy_petals, bool destroy_weights_array, bool destroy_bias_weights_array)
Frees memory allocated by flower struct.
Parameters
flower: pointer toflower_sstructdestroy_petals: true to also destroy each petaldestroy_weights_array: true to also destroyweights->weightsarray for each petal, false to notdestroy_bias_weights_array: true to also destroybias_weights->weightsarray for each petal, false to not
// Destroy internal array of weights weights_destroy(petal_hidden1->weights, false, true); weights_destroy(petal_hidden1->bias_weights, false, true); weights_destroy(petal_hidden2->weights, false, true); weights_destroy(petal_hidden2->bias_weights, false, true); weights_destroy(petal_output->weights, false, true); weights_destroy(petal_output->bias_weights, false, true); // Destroy flower without destroying petals flower_destroy(flower, false, false, false); // Destroy metrics metrics_destroy(metrics); // Destroy datasets for (uint16_t i = 0; i < train_dataset_length; ++i) { free(train_dataset_inputs[i]); free(train_dataset_outputs[i]); } for (uint16_t i = 0; i < validation_dataset_length; ++i) { free(validation_dataset_inputs[i]); free(validation_dataset_outputs[i]); } free(train_dataset_inputs); free(train_dataset_outputs); free(validation_dataset_inputs); free(validation_dataset_outputs);
PetalFlow has a simple logging implementation for formatting debug, info, warning and error messages
logger()
void logger(uint8_t level, const char *tag, const char *message_or_format, ... )Formats and prints logging entry.
Parameters
level: logging level (LOG_D,LOG_I,LOG_W,LOG_E, orLOG_NONE)tag: logging tag (for example, name of function)message_or_format: message to log or format for other arguments...: other logger arguments, for example:logger(LOG_I, "TAG", "Hello world %d, %.2f", 123, 4.5678f);would result in[YYYY-MM-DD HH:MM:SS] [INFO] [TAG] Hello world 123, 4.57
You can enable logging by defining LOGGING (for gcc: -DLOGGING). Other available definitions:
LOGGER_LEVEL <value>- Minimal allowed logging level. Default:LOG_ILOG_D- 0LOG_I- 1LOG_W- 2LOG_E- 3LOG_NONE- 255
LOGGER_DISABLE_TIME- Define to disable printing current timeLOGGER_TIME_FORMAT <value>- Time formatter. Default:"[%Y-%m-%d %H:%M:%S]"LOGGER_DISABLE_LEVEL- Define to disable printing current entry levelLOGGER_LEVEL_FIXED- Define to enable fixed-width logging level printingLOGGER_LEVEL_FIXED_FORMAT <value>- Fixed-width level formatter. Default:"[%-7s]"LOGGER_DISABLE_TAG- Define to disable printing tag
You can find more examples in test/main.c file
You can build petalflow_tests target by using cmake:
cmake -B build -DTESTS=ON -DLOGGER_LEVEL=1
cmake --build build --config Release
build/petalflow_testsOr by using gcc:
gcc -o petalflow test/main.c src/*.c -Iinclude -DLOGGING -DLOGGER_LEVEL=1 -lm
./petalflowYou can build Doxygen documentation using provided Doxygen file
Just clone repo and run:
doxygenThis will generate HTML and LaTeX with the following structure:
docs
βββ html
βΒ Β βββ activation_8c.html
...
βΒ Β βββ index.html
...
βΒ Β βββ weights_8h_source.html
βββ latex
βββ activation_8c.tex
...
βββ Makefile
...
βββ weights_8h.tex

