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 (size 64×64, 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
neededLibs = ["Flux", "BSON", "ImageTransformations", "Random"]
for lib in neededLibs
    try
        eval(Meta.parse("using $lib"))
    catch ex
        Pkg.add(lib)
        eval(Meta.parse("using $lib"))
    end
end
In [ ]:
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)
Датасет сгенерирован:
  200 изображений каждого класса для обучения
  30 тестовых изображений
  Размер изображений: 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__)/учебные данные";
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__)/учебные данные";
model, classes = train_model(DATA_DIR; epochs=100, imsize=64, batch_size=32, lr=0.0005, test_split=0.25, patience_limit=8);
Размер батча: 32, Скорость обучения: 0.0005
Доля тестовой выборки: 25.0%
Найдено классов: 3: ["квадрат", "круг", "треугольник"]

=== Распределение классов ===

Всего изображений: 600 (64×64)
  квадрат: 200 изображений (33.3%)
  круг: 200 изображений (33.3%)
  треугольник: 200 изображений (33.3%)

=== Разделение данных ===
  Тренировочных: 450 (75.0%)
  Тестовых: 150 (25.0%)
┌ Warning: `Flux.params(m...)` is deprecated. Use `Flux.trainable(model)` for parameter collection,
│ and the explicit `gradient(m -> loss(m, x, y), model)` for gradient computation.
└ @ Flux /opt/depot/demos/packages/Flux/c30bR/src/deprecations.jl:93
Параметров модели: 16035

=== Обучение ===
  Эпоха  1/100, Train Loss: 1.2926, Train Acc: 39.1%, Test Acc: 36.7% ★  (precision/recall по классам: квадрат: 31.7%/40.0%, круг: 46.9%/30.0%, треугольник: 43.6%/48.0%)
  Эпоха  2/100, Train Loss: 1.1301, Train Acc: 50.9%, Test Acc: 43.3% ★  (precision/recall по классам: квадрат: 51.7%/60.0%, круг: 48.8%/42.0%, треугольник: 51.0%/50.0%)
  Эпоха  3/100, Train Loss: 1.0281, Train Acc: 60.7%, Test Acc: 55.3% ★  (precision/recall по классам: квадрат: 49.2%/62.0%, круг: 40.8%/40.0%, треугольник: 52.6%/40.0%)
Warning: detected a stack overflow; program state may be corrupted, so further execution might be unreliable.
  Эпоха  4/100, Train Loss: 0.9695, Train Acc: 67.6%, Test Acc: 63.3% ★  (precision/recall по классам: квадрат: 60.4%/58.0%, круг: 51.1%/48.0%, треугольник: 60.0%/66.0%)
  Эпоха  5/100, Train Loss: 0.9029, Train Acc: 61.6%, Test Acc: 57.3%    (precision/recall по классам: квадрат: 63.0%/68.0%, круг: 37.0%/34.0%, треугольник: 58.0%/58.0%)
  Эпоха  6/100, Train Loss: 0.8281, Train Acc: 72.7%, Test Acc: 66.0% ★  (precision/recall по классам: квадрат: 67.7%/84.0%, круг: 51.1%/46.0%, треугольник: 58.1%/50.0%)
  Эпоха  7/100, Train Loss: 0.7604, Train Acc: 77.6%, Test Acc: 75.3% ★  (precision/recall по классам: квадрат: 71.4%/90.0%, круг: 65.0%/52.0%, треугольник: 74.5%/70.0%)
  Эпоха  8/100, Train Loss: 0.7174, Train Acc: 80.4%, Test Acc: 79.3% ★  (precision/recall по классам: квадрат: 72.4%/84.0%, круг: 57.5%/46.0%, треугольник: 69.2%/72.0%)
  Эпоха  9/100, Train Loss: 0.6826, Train Acc: 83.1%, Test Acc: 78.0%    (precision/recall по классам: квадрат: 73.4%/94.0%, круг: 46.8%/44.0%, треугольник: 59.0%/46.0%)
  Эпоха 10/100, Train Loss: 0.6091, Train Acc: 73.8%, Test Acc: 67.3%    (precision/recall по классам: квадрат: 76.7%/92.0%, круг: 67.6%/50.0%, треугольник: 67.9%/72.0%)
  Эпоха 11/100, Train Loss: 0.5658, Train Acc: 85.1%, Test Acc: 82.0% ★  (precision/recall по классам: квадрат: 85.7%/96.0%, круг: 72.5%/58.0%, треугольник: 74.1%/80.0%)
  Эпоха 12/100, Train Loss: 0.5198, Train Acc: 87.1%, Test Acc: 84.7% ★  (precision/recall по классам: квадрат: 88.7%/94.0%, круг: 62.2%/56.0%, треугольник: 67.3%/70.0%)
  Эпоха 13/100, Train Loss: 0.5058, Train Acc: 63.6%, Test Acc: 59.3%    (precision/recall по классам: квадрат: 94.0%/94.0%, круг: 68.8%/66.0%, треугольник: 73.1%/76.0%)
  Эпоха 14/100, Train Loss: 0.5111, Train Acc: 84.9%, Test Acc: 84.0%    (precision/recall по классам: квадрат: 90.9%/100.0%, круг: 72.0%/72.0%, треугольник: 75.6%/68.0%)
Warning: detected a stack overflow; program state may be corrupted, so further execution might be unreliable.
  Эпоха 15/100, Train Loss: 0.462, Train Acc: 74.9%, Test Acc: 69.3%    (precision/recall по классам: квадрат: 92.6%/100.0%, круг: 78.0%/78.0%, треугольник: 82.6%/76.0%)
  Эпоха 16/100, Train Loss: 0.4234, Train Acc: 95.1%, Test Acc: 94.0% ★  (precision/recall по классам: квадрат: 92.3%/96.0%, круг: 76.5%/78.0%, треугольник: 83.0%/78.0%)
  Эпоха 17/100, Train Loss: 0.3856, Train Acc: 82.9%, Test Acc: 80.0%    (precision/recall по классам: квадрат: 98.0%/100.0%, круг: 78.7%/74.0%, треугольник: 76.9%/80.0%)
  Эпоха 18/100, Train Loss: 0.3667, Train Acc: 96.7%, Test Acc: 94.7% ★  (precision/recall по классам: квадрат: 96.0%/96.0%, круг: 69.5%/82.0%, треугольник: 82.9%/68.0%)
  Эпоха 19/100, Train Loss: 0.3262, Train Acc: 97.1%, Test Acc: 95.3% ★  (precision/recall по классам: квадрат: 92.2%/94.0%, круг: 81.2%/78.0%, треугольник: 82.4%/84.0%)
  Эпоха 20/100, Train Loss: 0.2852, Train Acc: 98.4%, Test Acc: 96.7% ★  (precision/recall по классам: квадрат: 98.0%/98.0%, круг: 79.6%/86.0%, треугольник: 87.0%/80.0%)
  Эпоха 21/100, Train Loss: 0.2544, Train Acc: 96.4%, Test Acc: 92.0%    (precision/recall по классам: квадрат: 96.0%/96.0%, круг: 81.0%/94.0%, треугольник: 97.6%/82.0%)
Warning: detected a stack overflow; program state may be corrupted, so further execution might be unreliable.
  Эпоха 22/100, Train Loss: 0.2461, Train Acc: 81.6%, Test Acc: 80.7%    (precision/recall по классам: квадрат: 94.3%/100.0%, круг: 88.5%/92.0%, треугольник: 97.8%/88.0%)
  Эпоха 23/100, Train Loss: 0.2229, Train Acc: 99.3%, Test Acc: 98.0% ★  (precision/recall по классам: квадрат: 98.0%/96.0%, круг: 87.8%/86.0%, треугольник: 84.6%/88.0%)
  Эпоха 24/100, Train Loss: 0.1788, Train Acc: 99.1%, Test Acc: 97.3%    (precision/recall по классам: квадрат: 96.0%/96.0%, круг: 84.0%/84.0%, треугольник: 88.0%/88.0%)
  Эпоха 25/100, Train Loss: 0.1765, Train Acc: 99.6%, Test Acc: 97.3%    (precision/recall по классам: квадрат: 98.0%/98.0%, круг: 86.8%/92.0%, треугольник: 93.6%/88.0%)
  Эпоха 26/100, Train Loss: 0.1426, Train Acc: 96.4%, Test Acc: 92.7%    (precision/recall по классам: квадрат: 100.0%/98.0%, круг: 90.4%/94.0%, треугольник: 93.9%/92.0%)
  Эпоха 27/100, Train Loss: 0.1478, Train Acc: 97.3%, Test Acc: 94.0%    (precision/recall по классам: квадрат: 98.0%/100.0%, круг: 93.9%/92.0%, треугольник: 94.0%/94.0%)
  Эпоха 28/100, Train Loss: 0.1513, Train Acc: 99.3%, Test Acc: 97.3%    (precision/recall по классам: квадрат: 98.0%/100.0%, круг: 93.9%/92.0%, треугольник: 94.0%/94.0%)
Warning: detected a stack overflow; program state may be corrupted, so further execution might be unreliable.
  Эпоха 29/100, Train Loss: 0.1345, Train Acc: 99.6%, Test Acc: 98.0%    (precision/recall по классам: квадрат: 100.0%/98.0%, круг: 96.0%/96.0%, треугольник: 96.1%/98.0%)
  Эпоха 30/100, Train Loss: 0.0976, Train Acc: 95.1%, Test Acc: 89.3%    (precision/recall по классам: квадрат: 92.6%/100.0%, круг: 89.4%/84.0%, треугольник: 89.8%/88.0%)
  Эпоха 31/100, Train Loss: 0.1058, Train Acc: 89.6%, Test Acc: 83.3%    (precision/recall по классам: квадрат: 98.0%/100.0%, круг: 95.8%/92.0%, треугольник: 94.1%/96.0%)

  ⏹ Ранний останов после 31 эпох (нет улучшения 8 эпох)

Загружена лучшая модель (Test Acc: 98.0%)

=== Результаты ===
  Лучшая точность на тесте: 98.0%
  Точность train/test: 95.1% / 88.0%
  ✓ Нет переобучения (разрыв 7.1%)
Обучение завершено! 🚀
Модель сохранена в 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__)/неизвестно";
gr()
plot(create_simple_mosaic(UNKNOWN_DIR, imsize=64))
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

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__)/неизвестно", "$(@__DIR__)/неизвестно_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")

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_001.rgb       круг        0.998
circle_002.rgb       круг        0.999
circle_003.rgb       круг        0.992
circle_004.rgb       круг        1.000
circle_005.rgb       круг        0.876
circle_007.rgb       круг        0.885
circle_009.rgb       круг        0.929
circle_010.rgb       круг        0.991
circle_011.rgb       круг        0.985
circle_012.rgb       круг        0.990
circle_014.rgb       круг        0.994
circle_015.rgb       круг        0.999
circle_016.rgb       круг        0.966
circle_017.rgb       круг        0.896
circle_018.rgb       круг        0.847
circle_020.rgb       круг        0.921
circle_021.rgb       круг        0.981
circle_022.rgb       круг        0.867
circle_023.rgb       круг        0.994
circle_024.rgb       круг        0.977
circle_025.rgb       круг        1.000
circle_026.rgb       круг        0.998
circle_028.rgb       круг        0.983
circle_029.rgb       круг        0.997
circle_030.rgb       круг        0.976
square_001.rgb       квадрат  0.525
square_002.rgb       квадрат  0.504
square_003.rgb       квадрат  0.892
square_004.rgb       квадрат  0.854
square_005.rgb       квадрат  0.877
square_007.rgb       квадрат  0.729
square_008.rgb       круг        0.577
square_009.rgb       квадрат  0.786
square_010.rgb       квадрат  0.929
square_013.rgb       квадрат  0.801
square_014.rgb       квадрат  0.942
square_015.rgb       квадрат  0.952
square_016.rgb       квадрат  0.892
square_018.rgb       квадрат  0.927
square_019.rgb       квадрат  0.974
square_020.rgb       квадрат  0.887
square_021.rgb       квадрат  0.894
square_023.rgb       квадрат  0.786
square_024.rgb       квадрат  0.984
square_026.rgb       квадрат  0.725
square_027.rgb       квадрат  0.869
square_028.rgb       квадрат  0.918
square_029.rgb       квадрат  0.925
square_030.rgb       квадрат  0.898
triangle_001.rgb     треугольник 0.550
triangle_002.rgb     круг        0.692
triangle_003.rgb     круг        0.667
triangle_004.rgb     круг        0.554
triangle_006.rgb     круг        0.770
triangle_007.rgb     круг        0.811
triangle_008.rgb     круг        0.553
triangle_009.rgb     круг        0.897
triangle_010.rgb     треугольник 0.647
triangle_011.rgb     треугольник 0.544
triangle_012.rgb     круг        0.502
triangle_013.rgb     круг        0.931
triangle_014.rgb     треугольник 0.644
triangle_016.rgb     круг        0.800
triangle_017.rgb     круг        0.546
triangle_020.rgb     круг        0.897
triangle_021.rgb     круг        0.892
triangle_022.rgb     круг        0.928
triangle_023.rgb     круг        0.546
triangle_025.rgb     треугольник 0.571
triangle_026.rgb     круг        0.552
triangle_027.rgb     круг        0.506
triangle_028.rgb     круг        0.594
triangle_029.rgb     треугольник 0.684
triangle_030.rgb     круг        0.783
circle_027.rgb       круг        0.973
square_006.rgb       квадрат  0.882
square_011.rgb       квадрат  0.784
square_012.rgb       квадрат  0.970
square_017.rgb       квадрат  0.971
square_025.rgb       круг        0.577
triangle_019.rgb     круг        0.569
triangle_024.rgb     круг        0.979

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__)/неизвестно";
gr()
mosaic_grouped = create_mosaic_from_c_predictions("неизвестно", "pred.txt", max_images=8)
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__)/неизвестно";
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)
Обработано файлов: 82
  квадрат: 30
  круг: 27
  треугольник: 25

=== Сравнение C и Julia ===
Всего файлов: 82
Совпадающих предсказаний: 58
Точность: 70.73%

Статистика разницы в уверенности:
  Средняя разница: 0.1468
  Макс разница: 0.4452
  Мин разница: 0.0026
Warning: detected a stack overflow; program state may be corrupted, so further execution might be unreliable.
Out[0]:
82×12 DataFrame
57 rows omitted
RowFileC_PredictionC_ConfidenceBaseNameФайлJulia_PredictionJulia_ConfidenceВероятность_квадратВероятность_кругВероятность_треугольникMatchConfidence_Diff
StringStringFloat64StringString31String31Float64Float64Float64Float64BoolFloat64
1circle_001.rgbкруг0.998circle_001circle_001.pngкруг0.9119490.05539550.9119490.0326554true0.0860508
2circle_002.rgbкруг0.999circle_002circle_002.pngкруг0.9763530.008158840.9763530.0154881true0.0226469
3circle_003.rgbкруг0.992circle_003circle_003.pngкруг0.952730.02631530.952730.0209551true0.0392704
4circle_004.rgbкруг1.0circle_004circle_004.pngкруг0.9892990.005832470.9892990.00486871true0.0107013
5circle_005.rgbкруг0.876circle_005circle_005.pngкруг0.6770330.06953770.6770330.253429true0.198967
6circle_007.rgbкруг0.885circle_007circle_007.pngкруг0.8530360.1026440.8530360.0443203true0.0319639
7circle_009.rgbкруг0.929circle_009circle_009.pngкруг0.7493890.1471960.7493890.103415true0.179612
8circle_010.rgbкруг0.991circle_010circle_010.pngкруг0.9198560.01826790.9198560.0618764true0.0711443
9circle_011.rgbкруг0.985circle_011circle_011.pngкруг0.8211480.007267880.8211480.171584true0.163852
10circle_012.rgbкруг0.99circle_012circle_012.pngкруг0.6564440.3010540.6564440.0425026true0.333556
11circle_014.rgbкруг0.994circle_014circle_014.pngкруг0.9397310.01862430.9397310.0416447true0.0542691
12circle_015.rgbкруг0.999circle_015circle_015.pngкруг0.9706860.00608770.9706860.0232258true0.0283136
13circle_016.rgbкруг0.966circle_016circle_016.pngкруг0.8432020.03315850.8432020.12364true0.122798
⋮⋮⋮⋮⋮⋮⋮⋮⋮⋮⋮⋮⋮
71triangle_019.rgbкруг0.569triangle_019triangle_019.pngтреугольник0.466660.08558060.4477590.46666false0.10234
72triangle_020.rgbкруг0.897triangle_020triangle_020.pngтреугольник0.8207940.002196020.177010.820794false0.0762059
73triangle_021.rgbкруг0.892triangle_021triangle_021.pngтреугольник0.9412310.001349090.05741970.941231false0.0492312
74triangle_022.rgbкруг0.928triangle_022triangle_022.pngтреугольник0.9777810.000122910.0220960.977781false0.0497811
75triangle_023.rgbкруг0.546triangle_023triangle_023.pngтреугольник0.8183180.01850380.1631780.818318false0.272318
76triangle_024.rgbкруг0.979triangle_024triangle_024.pngкруг0.7807410.1150860.7807410.104173true0.198259
77triangle_025.rgbтреугольник0.571triangle_025triangle_025.pngтреугольник0.8574690.01879770.1237340.857469true0.286469
78triangle_026.rgbкруг0.552triangle_026triangle_026.pngтреугольник0.9535730.002183540.0442430.953573false0.401573
79triangle_027.rgbкруг0.506triangle_027triangle_027.pngтреугольник0.9088860.006617020.08449650.908886false0.402886
80triangle_028.rgbкруг0.594triangle_028triangle_028.pngтреугольник0.7929390.01448990.1925720.792939false0.198939
81triangle_029.rgbтреугольник0.684triangle_029triangle_029.pngтреугольник0.9475950.003543110.04886240.947595true0.263594
82triangle_030.rgbкруг0.783triangle_030triangle_030.pngтреугольник0.872280.003308110.1244120.87228false0.0892798

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.