Engee documentation
Notebook

Embedded code from a convolutional neural network

Let's train a neural network for a toy task - predicting geometric shapes, and check whether it can be compiled into C code, and then into a binary library to use in blocks or as part of another project.

Introduction

In this practical guide, we will train a neural network to recognize geometric shapes on a toy dataset, and then export the trained model to C code, compile it into a shared library, and test the possibility of integration into third-party projects or C code blocks in your project.

During the conversion, you will notice a slight loss of accuracy. This may be caused by differences in the implementation of some operations in Julia and in C (for example, batch normalization) or simple rounding of coefficients when translated into code, but it opens the way to deployment on embedded systems.

Preparation

At this stage, we download the necessary libraries, fix a random number generator, create a synthetic dataset of squares, circles, and triangles, and then visualize sample images of each class.

The generation of a controlled, balanced dataset with known properties (64×64 size, normalization to the range [-1.1]) allows you to check each stage of the pipeline in isolation without the influence of external factors.

Let's install the necessary libraries and initialize the random number generator so that our experiment is easily reproducible.:

In [ ]:
# Installing the necessary packages
# Pkg.add(["Flux", "BSON", "ImageTransformations"])
In [ ]:
using Random
Random.seed!(5);

Synthesizing a data set

Let's create a toy dataset consisting of three classes. Some of the objects are placed in the "unknown" folder, that is, their class, although it is written in the file name, will be unknown to the system. You can call this a validation dataset. The rest - training and test - are arranged in the appropriate folders.

In [ ]:
include("$(@__DIR__)/_scripts/generate_shape_dataset.jl")
generate_shape_dataset(samples_per_class=200, test_samples=30, img_size=64)
Dataset generated:
  200 images of each class for training
  30 test images
  Image size: 64 x 64

At this stage, we try to generate a fairly diverse dataset (with triangle rotations), but at the same time do not overcomplicate the code, for example, we did not do augmentation in the learning process. Overall, this stage turned out to be the least problematic.

A look at the training dataset

Here are sample objects from our training dataset:

In [ ]:
include("$(@__DIR__)/_scripts/show_dataset_samples.jl")
DATA_DIR = "$(@__DIR__)/training data";
gr()
show_dataset_samples(DATA_DIR, samples_per_class=10)
Out[0]:
No description has been provided for this image

Model training and analysis

Here we start the learning process of a convolutional neural network, save the history of metrics, analyze the dynamics of accuracy and loss, and display a mosaic of predictions on test images.

Monitoring precision/recall metrics by class and early stopping for validation accuracy helps to detect overfitting in time and select the best model for subsequent export.

In [ ]:
include("$(@__DIR__)/_scripts/train_model.jl");
DATA_DIR = "$(@__DIR__)/training data";
model, classes = train_model(DATA_DIR; epochs=100, imsize=64, batch_size=32, lr=0.0005, test_split=0.25, patience_limit=8);
Batch size: 32, Learning rate: 0.0005
Percentage of the test sample: 25.0%
Найдено классов: 3: ["square", "a circle", "triangle"]

=== Class distribution ===

Total images: 600 (64×64)
  Square: 200 images (33.3%)
  Circle: 200 images (33.3%)
  Triangle: 200 images (33.3%)

=== Data separation ===
  Training: 450 (75.0%)
  Test cases: 150 (25.0%)
Model parameters: 16035

=== Training ===
  Epoch 1/100, Train Loss: 1.2839, Train Acc: 41.3%, Test Acc: 39.3% ★ (precision/recall by class: square: 31.8%/42.0%, Circle: 45.7%/32.0%, Triangle: 49.0%/48.0%)
  Epoch 2/100, Train Loss: 1.1397, Train Acc: 46.2%, Test Acc: 46.7% ★ (precision/recall by class: square: 41.9%/52.0%, Circle: 41.9%/26.0%, Triangle: 45.6%/52.0%)
  Epoch 3/100, Train Loss: 1.0425, Train Acc: 63.8%, Test Acc: 58.0% ★ (precision/recall by class: square: 46.6%/54.0%, Circle: 34.0%/36.0%, Triangle: 59.0%/46.0%)
  Epoch 4/100, Train Loss: 0.9699, Train Acc: 65.8%, Test Acc: 55.3% (precision/recall by class: square: 62.3%/66.0%, Circle: 45.7%/42.0%, Triangle: 60.8%/62.0%)
  Epoch 5/100, Train Loss: 0.9363, Train Acc: 71.3%, Test Acc: 72.0% ★ (precision/recall by class: square: 59.3%/64.0%, Circle: 38.6%/34.0%, Triangle: 53.8%/56.0%)
  Epoch 6/100, Train Loss: 0.862, Train Acc: 73.1%, Test Acc: 67.3% (precision/recall by class: square: 65.1%/82.0%, Circle: 53.2%/50.0%, Triangle: 55.0%/44.0%)
  Epoch 7/100, Train Loss: 0.7955, Train Acc: 82.4%, Test Acc: 77.3% ★ (precision/recall by class: square: 68.7%/92.0%, Circle: 52.5%/42.0%, Triangle: 72.1%/62.0%)
  Epoch 8/100, Train Loss: 0.7538, Train Acc: 78.7%, Test Acc: 78.0% ★ (precision/recall by class: square: 74.2%/92.0%, Circle: 65.7%/46.0%, Triangle: 67.9%/72.0%)
  Epoch 9/100, Train Loss: 0.6834, Train Acc: 75.8%, Test Acc: 75.3% (precision/recall by class: square: 73.0%/92.0%, circle: 46.9%/46.0%, triangle: 57.9%/44.0%)
  Epoch 10/100, Train Loss: 0.6379, Train Acc: 86.4%, Test Acc: 82.7% ★ (precision/recall by class: square: 79.7%/94.0%, Circle: 71.1%/54.0%, Triangle: 71.7%/76.0%)
  Epoch 11/100, Train Loss: 0.609, Train Acc: 84.0%, Test Acc: 83.3% ★ (precision/recall by class: square: 87.5%/98.0%, Circle: 70.7%/58.0%, Triangle: 71.7%/76.0%)
  Epoch 12/100, Train Loss: 0.5567, Train Acc: 82.0%, Test Acc: 82.7% (precision/recall by class: square: 92.6%/100.0%, Circle: 63.8%/60.0%, Triangle: 67.3%/66.0%)
  Epoch 13/100, Train Loss: 0.5446, Train Acc: 65.8%, Test Acc: 63.3% (precision/recall by class: square: 92.3%/96.0%, Circle: 70.8%/68.0%, Triangle: 76.0%/76.0%)
  Epoch 14/100, Train Loss: 0.5065, Train Acc: 79.8%, Test Acc: 82.0% (precision/recall by class: square: 94.2%/98.0%, Circle: 68.5%/74.0%, Triangle: 77.3%/68.0%)
  Epoch 15/100, Train Loss: 0.4701, Train Acc: 88.9%, Test Acc: 86.0% ★ (precision/recall by class: square: 90.7%/98.0%, Circle: 76.1%/70.0%, triangle: 80.0%/80.0%)
  Epoch 16/100, Train Loss: 0.433, Train Acc: 67.8%, Test Acc: 66.0% (precision/recall by class: square: 94.1%/96.0%, Circle: 73.6%/78.0%, Triangle: 78.3%/72.0%)
  Epoch 17/100, Train Loss: 0.4185, Train Acc: 90.2%, Test Acc: 88.7% ★ (precision/recall by class: square: 96.2%/100.0%, Circle: 77.1%/74.0%, triangle: 78.0%/78.0%)
  Epoch 18/100, Train Loss: 0.3876, Train Acc: 95.3%, Test Acc: 92.7% ★ (precision/recall by class: square: 98.0%/98.0%, Circle: 77.8%/84.0%, Triangle: 82.6%/76.0%)
  Epoch 19/100, Train Loss: 0.3864, Train Acc: 94.2%, Test Acc: 92.7% (precision/recall by class: square: 94.0%/94.0%, Circle: 76.4%/84.0%, triangle: 84.4%/76.0%)
  Epoch 20/100, Train Loss: 0.3226, Train Acc: 94.9%, Test Acc: 91.3% (precision/recall by class: square: 90.6%/96.0%, Circle: 77.8%/84.0%, Triangle: 88.4%/76.0%)
  Epoch 21/100, Train Loss: 0.276, Train Acc: 86.0%, Test Acc: 86.0% (precision/recall by class: square: 94.1%/96.0%, Circle: 77.1%/74.0%, triangle: 80.4%/82.0%)
  Epoch 22/100, Train Loss: 0.2853, Train Acc: 91.8%, Test Acc: 89.3% (precision/recall by class: square: 94.3%/100.0%, Circle: 84.0%/84.0%, Triangle: 87.2%/82.0%)
  Epoch 23/100, Train Loss: 0.255, Train Acc: 82.7%, Test Acc: 79.3% (precision/recall by class: square: 98.0%/98.0%, Circle: 87.2%/82.0%, Triangle: 83.0%/88.0%)
  Epoch 24/100, Train Loss: 0.2077, Train Acc: 100.0%, Test Acc: 94.7% ★ (precision/recall by class: square: 92.3%/96.0%, Circle: 79.2%/76.0%, Triangle: 84.0%/84.0%)

   Early stop to achieve 100% accuracy on the training dataset

The best model loaded (Test Acc: 94.7%)

=== Results ===
  Best accuracy on the test: 94.7%
  Train/test accuracy: 94.9% / 89.3%
  ✓ No retraining (5.6% gap)
The training is completed! 🚀
The model is saved in model.bson

Let's look at the quality of the training conducted:

In [ ]:
include("$(@__DIR__)/_scripts/analyze_training_log.jl")
gr()
df, classes, p = analyze_training_log("training_log.txt")
display(p)
No description has been provided for this image

It is interesting to interpret each graph separately. For example, precision grew almost equally for all classes, but the recall score immediately became better for squares, and was always behind for triangles, remaining not the highest by the end of the learning process.

We did not continue training after achieving 100% quality on the test, because there was no point in comparing implementations with each other. But we definitely should have generated more objects for the dataset, since, on average, by the end of training, the model accurately identified squares and circles, but out of the five proposed triangles, on average, one of them "did not notice". Although those that she marked as triangles were indeed triangles (the network showed more "false positive" errors for the "circle" class).

Forecasts from the Julia (Flux) neural network

In [ ]:
include("$(@__DIR__)/_scripts/simple_mosaic.jl")
UNKNOWN_DIR = "$(@__DIR__)/unknown";
gr()
plot(create_simple_mosaic(UNKNOWN_DIR, imsize=64))
Out[0]:
No description has been provided for this image

We see pretty good predictions, but this is not so much the result of successful training as the result of long-term work by the designer. The most time-consuming was the selection of the network architecture (number of layers, channels, use of BatchNorm and Dropout) and hyperparameters (learning rate, batch size, augmentation) in order to achieve stable convergence and avoid overfitting on a limited data set. As a result, for example, augmentation was moved to the dataset generating function to simplify the example, as well as due to the fact that this procedure is needed only for triangles.

Export to C and testing

Now we convert the preprocessed images to binary format, generate the C code of the neural network, compile it into an executable file, and visualize the predictions obtained from the C implementation. We knowingly assume that the code will work on platforms where there is no PNG library. Therefore, we convert images to binary format using a separate script. These binary files contain matrices, the elements of which include each color channel of each pixel, represented by a single UInt8 number.

In [ ]:
include("$(@__DIR__)/_scripts/convert_png_to_rgb8.jl")
convert_png_to_rgb8("$(@__DIR__)/unknown", "$(@__DIR__)/unknown_rgb8", 64)

Now that we have a dataset with binary images ready, we can download the already trained model and translate it into C code. The key requirement for successful export is full alignment of data formats (RGB8 for images, HWC order of coefficients) and the order of weight traversal between Julia and C, which is achieved by explicit control of indexing and normalization at all stages.

In [ ]:
include("$(@__DIR__)/_scripts/generate_cnn_code.jl")

using Flux, BSON
BSON.@load "$(@__DIR__)/model.bson" model classes
model = Flux.testmode!(model)

# Generating the library and the main program
generate_shared_lib(model, 64, length(classes))
generate_main_program(64, length(classes))
Generated neural_net.c and neural_net.h
Generated main.c

We will compile the neural network itself into a library. We also generated the main program, which feeds images from the "unknown_rgb8" folder to the neural network and processes the classification results.

In [ ]:
;gcc -shared -fPIC neural_net.c -o libneuralnet.so -lm
In [ ]:
;gcc main.c -o classify_unknown -ldl -lm

Interestingly, to run this neural network, we don't need any libraries, either Julia or C. It runs on any system that has a C compiler.

In [ ]:
;./classify_unknown
File                 Prediction      Confidence
------------------------------------------------
circle_009.rgb circle 0.983
circle_010.rgb circle 0.998
circle_011.rgb circle 0.955
circle_012.rgb circle 0.993
circle_015.rgb circle 0.997
circle_016.rgb circle 0.964
circle_017.rgb circle 0.966
circle_020.rgb circle 0.943
circle_024.rgb circle 0.996
circle_025.rgb circle 1.000
circle_026.rgb circle 0.999
square_001.rgb square 0.701
square_003.rgb square 0.920
square_004.rgb square 0.739
square_005.rgb square 0.815
square_008.rgb square 0.923
square_013.rgb square 0.681
square_014.rgb square 0.743
square_018.rgb square 0.904
square_019.rgb square 0.937
square_021.rgb square 0.739
square_029.rgb square 0.817
triangle_002.rgb triangle 0.664
triangle_006.rgb triangle 0.626
triangle_007.rgb triangle 0.584
triangle_022.rgb circle 0.511
triangle_023.rgb circle 0.754
triangle_027.rgb triangle 0.664
triangle_028.rgb triangle 0.973
triangle_030.rgb circle 0.778
circle_014.rgb circle 0.999
circle_018.rgb circle 0.929
square_009.rgb circle 0.529
square_010.rgb square 0.921
square_015.rgb square 0.992
square_020.rgb square 0.926
square_023.rgb circle 0.567
square_024.rgb square 0.668
square_028.rgb square 0.879
square_030.rgb square 0.927
triangle_001.rgb triangle 0.702
triangle_008.rgb circle 0.564
triangle_011.rgb circle 0.580
triangle_012.rgb triangle 0.6666
triangle_013.rgb triangle 0.698
triangle_021.rgb triangle 0.626
triangle_029.rgb circle 0.707
circle_001.rgb circle 0.948
circle_002.rgb circle 0.990
circle_003.rgb circle 0.992
circle_004.rgb circle 1.000
circle_005.rgb circle 0.793
circle_007.rgb circle 0.985
circle_021.rgb circle 0.995
circle_022.rgb circle 0.912
circle_023.rgb circle 0.973
circle_028.rgb circle 0.989
circle_029.rgb circle 0.948
circle_030.rgb circle 0.992
square_002.rgb circle 0.498
square_007.rgb circle 0.649
square_016.rgb circle 0.715
square_026.rgb square 0.729
square_027.rgb square 0.868
triangle_003.rgb triangle 0.665
triangle_004.rgb triangle 0.558
triangle_009.rgb circle 0.810
triangle_010.rgb triangle 0.539
triangle_014.rgb triangle 0.922
triangle_016.rgb circle 0.707
triangle_017.rgb circle 0.564
triangle_020.rgb circle 0.510
triangle_025.rgb circle 0.497
triangle_026.rgb triangle 0.834

When transferring the model to C, we had to solve several non-trivial tasks: manually implementing convolutions and BatchNorm without third—party libraries, reducing all operations to a single HWC format, accurately reproducing the order of weight traversal (especially critical for multi-channel layers), as well as working with binary image files due to the lack of a PNG library in the target environment - all these The difficulties were successfully overcome.

Forecasts from the neural network in C

In [ ]:
include("$(@__DIR__)/_scripts/create_mosaic_from_c_predictions.jl")
run(pipeline(`./classify_unknown`, stdout="pred.txt"))
UNKNOWN_DIR = "$(@__DIR__)/unknown";
gr()
mosaic_grouped = create_mosaic_from_c_predictions("is unknown", "pred.txt", max_images=8)
Warning: detected a stack overflow; program state may be corrupted, so further execution might be unreliable.
Out[0]:
No description has been provided for this image

Despite these difficulties, we have demonstrated a full working pipeline, proving that exporting neural networks from Julia to C is possible even with limited resources of the target platform.

In [ ]:
include("$(@__DIR__)/_scripts/predict_to_csv.jl")
UNKNOWN_DIR = "$(@__DIR__)/unknown";
predict_to_csv(UNKNOWN_DIR, confidence_threshold=0.4, output_csv="$(@__DIR__)/predictions.csv")
run(pipeline(`./classify_unknown`, stdout="pred.txt"))
include("$(@__DIR__)/_scripts/compare_c_and_julia.jl")
df = compare_c_and_julia()
sort(df)
Processed files: 74
  Square: 24
  Circle: 25
  Triangle: 25

=== Comparison of C and Julia ===
Total files: 74
Matching predictions: 58
Accuracy: 78.38%

Statistics of the difference in confidence:
  Average difference: 0.1674
  Max difference: 0.4689
  Min difference: 0.0056
Out[0]:
74×12 DataFrame
49 rows omitted
RowFileC_PredictionC_ConfidenceBaseNameФайлJulia_PredictionJulia_ConfidenceВероятность_квадратВероятность_кругВероятность_треугольникMatchConfidence_Diff
StringStringFloat64StringString31String31Float64Float64Float64Float64BoolFloat64
1circle_001.rgbкруг0.948circle_001circle_001.pngкруг0.8142610.03648750.8142610.149251true0.133738
2circle_002.rgbкруг0.99circle_002circle_002.pngкруг0.9430210.01381410.9430210.0431649true0.0469789
3circle_003.rgbкруг0.992circle_003circle_003.pngкруг0.9190590.01131650.9190590.0696241true0.0729406
4circle_004.rgbкруг1.0circle_004circle_004.pngкруг0.9835570.007013320.9835570.00943003true0.0164434
5circle_005.rgbкруг0.793circle_005circle_005.pngкруг0.5793090.05147360.5793090.369217true0.213691
6circle_007.rgbкруг0.985circle_007circle_007.pngкруг0.9111080.05219510.9111080.0366972true0.0738923
7circle_009.rgbкруг0.983circle_009circle_009.pngкруг0.822990.03692180.822990.140089true0.16001
8circle_010.rgbкруг0.998circle_010circle_010.pngкруг0.9563120.005266250.9563120.0384219true0.0416882
9circle_011.rgbкруг0.955circle_011circle_011.pngкруг0.5309450.004447270.5309450.464608true0.424055
10circle_012.rgbкруг0.993circle_012circle_012.pngкруг0.9364520.01600090.9364520.047547true0.056548
11circle_014.rgbкруг0.999circle_014circle_014.pngкруг0.9611150.006560260.9611150.032325true0.0378853
12circle_015.rgbкруг0.997circle_015circle_015.pngкруг0.9537650.005638460.9537650.040597true0.0432354
13circle_016.rgbкруг0.964circle_016circle_016.pngкруг0.8631250.05490230.8631250.0819724true0.100875
63triangle_016.rgbкруг0.707triangle_016triangle_016.pngтреугольник0.8879470.002696110.1093570.887947false0.180947
64triangle_017.rgbкруг0.564triangle_017triangle_017.pngтреугольник0.881780.01175230.1064680.88178false0.31778
65triangle_020.rgbкруг0.51triangle_020triangle_020.pngтреугольник0.9789090.0004155540.0206760.978909false0.468909
66triangle_021.rgbтреугольник0.626triangle_021triangle_021.pngтреугольник0.9980941.40282e-50.00189150.998094true0.372094
67triangle_022.rgbкруг0.511triangle_022triangle_022.pngтреугольник0.9425090.001728090.05576310.942509false0.431509
68triangle_023.rgbкруг0.754triangle_023triangle_023.pngтреугольник0.8040480.0211330.1748190.804048false0.0500482
69triangle_025.rgbкруг0.497triangle_025triangle_025.pngтреугольник0.8247580.0257680.1494740.824758false0.327758
70triangle_026.rgbтреугольник0.834triangle_026triangle_026.pngтреугольник0.9553930.0004918250.04411540.955393true0.121393
71triangle_027.rgbтреугольник0.664triangle_027triangle_027.pngтреугольник0.8856340.007675210.1066910.885634true0.221634
72triangle_028.rgbтреугольник0.973triangle_028triangle_028.pngтреугольник0.9937267.13982e-50.006202120.993726true0.0207265
73triangle_029.rgbкруг0.707triangle_029triangle_029.pngтреугольник0.8879470.002696110.1093570.887947false0.180947
74triangle_030.rgbкруг0.778triangle_030triangle_030.pngтреугольник0.8271150.002612720.1702720.827115false0.0491154

Conclusion

We have shown how to go through the full cycle of creating a program with a neural network inside: from creating a dataset and training a model on Julia to exporting to C and checking performance, which confirms the fundamental possibility of using the generated code far beyond the Engee engineering platform.