Документация Engee
Notebook

Встраиваемый код из свёрточной нейросети

Обучим нейросеть игрушечной задаче - предсказывать геометрические фигуры, и проверим, можно ли скомпилировать ее в код на Си, а затем и в бинарную библиотеку чтобы использовать в блоках или как часть другого проекта.

Введение

В этом практическом руководстве мы обучим нейросеть распознавать геометрические фигуры на игрушечном датасете, а затем экспортируем обученную модель в код на C, скомпилируем его в разделяемую библиотеку и проверим возможность интеграции в сторонние проекты или блоки кода на Си в вашем проекте.

В ходе конвертации вы заметите небольшую потерю точности. Это может быть вызвано разницей в реализации некоторых операций в Julia и в C (например, батч-нормализация) или простым округлением коэффициентов при переводе в код, но зато открывает путь к развёртыванию на встраиваемых системах.

Подготовка

На этом этапе мы загружаем необходимые библиотеки, фиксируем генератор случайных чисел, создаём синтетический датасет из квадратов, кругов и треугольников, а затем визуализируем примеры изображений каждого класса.

Генерация контролируемого, сбалансированного датасета с известными свойствами (размер 64×64, нормализация в диапазон [-1,1]) позволяет изолированно проверить каждый этап конвейера без влияния внешних факторов.

Установим нужные библиотеки и инициализируем генератор случайных чисел, чтобы наш эксперимент был легко возпроизводимым:

In [ ]:
# Установка необходимых пакетов
# Pkg.add(["Flux", "BSON", "ImageTransformations"])
In [ ]:
using Random
Random.seed!(5);

Синтезируем набор данных

Создадим игрушечный датасет, состоящий из трех классов. Часть объектов помещается в папку "неизвестно", то есть их класс, хоть он и прописан в названии файлов, системе будет неизвестен. Можно назвать это валидационным датасетом. Остальные - тренировочный и тестовый - разложены по соответствующим папкам.

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

На этом этапе мы стараемся сгенерировать достаточно разнообразный датасет (с поворотами треугольников), но при этом не переусложнять код, например мы не стали делать аугментацию в процессе обучения. В целом, этот этап оказался наименее проблемным.

Взгляд на учебный набор данных

Вот образцы объектов из нашего учебного набора данных:

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

Обучение и анализ модели

Здесь мы запускаем процесс обучения свёрточной нейросети, сохраняем историю метрик, анализируем динамику точности и потерь, а также отображаем мозаику предсказаний на тестовых изображениях.

Мониторинг метрик precision/recall по классам и ранняя остановка по валидационной точности помогают вовремя обнаружить переобучение и выбрать лучшую модель для последующего экспорта.

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%)
Параметров модели: 16035

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

  ⏹ Ранний останов по достижению 100% точности на учебном датасете

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

=== Результаты ===
  Лучшая точность на тесте: 94.7%
  Точность train/test: 94.9% / 89.3%
  ✓ Нет переобучения (разрыв 5.6%)
Обучение завершено! 🚀
Модель сохранена в model.bson

Посмотрим на качество проведенного обучения:

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

Каждый график интересно интерпретировать по-отдельности. Например, precision рос для всех классов практически одинаково, но показатель recall сразу стал лучше для квадратов, и всегда был позади для треугольников, оставаясь не самым высоким и к концу процесса обучения.

Мы не стали продолжать обучение после достижения качества 100% на тесте, потому что исчез смысл сравнивать реализации между собой. Но нам определенно стоило бы породить больше объектов для датасета, поскольку, в среднем, к концу обучения модель достаточно точно определяла квадраты и круги, но из пяти предложенных треугольников в среднем "не замечала" один из них. Хотя те, которе она отмечала как треугольники действительно ими были (больше ошибок "ложного срабатывания" сеть демонстрировала для класса "круг").

Прогнозы от нейросети на Julia (Flux)

In [ ]:
include("$(@__DIR__)/_scripts/simple_mosaic.jl")
UNKNOWN_DIR = "$(@__DIR__)/неизвестно";
gr()
plot(create_simple_mosaic(UNKNOWN_DIR, imsize=64))
Out[0]:
No description has been provided for this image

Мы видим довольно хорошие предсказания, но это не столько результат успешного обучения, сколько результат долгой работы проектировщика. Самым трудоёмким оказался подбор архитектуры сети (количество слоёв, каналов, использование BatchNorm и Dropout) и гиперпараметров (скорость обучения, размер батча, аугментация), чтобы достичь стабильной сходимости и избежать переобучения на ограниченном наборе данных. В итоге, например, аугментация была перенесена в функцию порождающую датасет, чтобы упростить пример, а также в силу того, что эта процедура нужна только треугольникам.

Экспорт в C и тестирование

Теперь мы конвертируем предварительно обработанные изображения в бинарный формат, генерируем C-код нейросети, компилируем его в исполняемый файл и визуализируем предсказания, полученные от C-реализации. Мы заведомо предполагаем, что код будет работать на платформах, где нет библиотеки PNG. Поэтому переводим изображения в бинарный формат при помощи отдельного скрипта. В этих бинарных файлах лежат матрицы, в качестве элементов которых фигурирует каждый цветовой канал каждого пикселя, представленный одним числом UInt8.

In [ ]:
include("$(@__DIR__)/_scripts/convert_png_to_rgb8.jl")
convert_png_to_rgb8("$(@__DIR__)/неизвестно", "$(@__DIR__)/неизвестно_rgb8", 64)

Теперь, когда у нас готов датасет с бинарными изображениями, можно загрузить уже обученную модель и перевести ее в код на Си. Ключевое требование к успешному экспорту — полное согласование форматов данных (RGB8 для изображений, HWC порядок коэффициентов) и порядка обхода весов между Julia и C, что достигается явным контролем индексации и нормализации на всех этапах.

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

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

# Генерируем библиотеку и main программу
generate_shared_lib(model, 64, length(classes))
generate_main_program(64, length(classes))
Generated neural_net.c and neural_net.h
Generated main.c

Саму нейросеть мы скомпилируем в библиотеку. Мы также сгенерировали программу main, которая подаёт в нейросеть изображения из папки "неизвестно_rgb8" и обрабатывает результаты классификации.

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

Что любопытно, чтобы запустить эту нейросеть, нам не потребуются никакие библиотеки - ни Julia, ни C. Она выполняется на любой системе, где есть компилятор кода на Си.

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

При переносе модели в C пришлось решить несколько нетривиальных задач: ручная реализация свёрток и BatchNorm без сторонних библиотек, приведение всех операций к единому формату HWC, точное воспроизведение порядка обхода весов (особенно критичного для многоканальных слоёв), а также работа с бинарными файлами изображений из-за отсутствия библиотеки PNG в целевой среде — все эти трудности были успешно преодолены.

Прогнозы от нейросети на Си

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)
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

Несмотря на перечисленные сложности, мы продемонстрировали полный рабочий конвейер, доказывающий, что экспорт нейросетей из Julia в C возможен даже при ограниченных ресурсах целевой платформы.

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)
Обработано файлов: 74
  квадрат: 24
  круг: 25
  треугольник: 25

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

Статистика разницы в уверенности:
  Средняя разница: 0.1674
  Макс разница: 0.4689
  Мин разница: 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

Заключение

Мы показали, как пройти полный цикл создания программы с нейросетью внутри: от создания датасета и обучения модели на Julia до экспорта в C и проверки работоспособности, что подтверждает принципиальную возможность использования сгенерированного кода далеко за пределами инженерной платформы Engee.