AnyMath 文档
Notebook

来自卷积神经网络的嵌入式代码

让我们为一个玩具任务训练一个神经网络-预测几何形状,并检查它是否可以编译成C代码,然后编译成二进制库以用于块或作为另一个项目的一部分。

导言

在本实用指南中,我们将训练神经网络以识别玩具数据集上的几何形状,然后将训练好的模型导出为C代码,将其编译为共享库,并测试集成到第三方项目或项目中的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测试图像
  图像大小:64x64

在这个阶段,我们试图生成一个相当多样化的数据集(带有三角形旋转),但与此同时不要过分复杂代码,例如,我们在学习过程中没有做增强。 总的来说,这个阶段的问题最小。

看看训练数据集

以下是我们训练数据集中的样本对象:

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

模型训练与分析

在这里,我们开始卷积神经网络的学习过程,保存度量的历史记录,分析准确性和损失的动态,并在测试图像上显示预测的马赛克。

按类别监控精度/召回指标并提前停止验证精度有助于及时检测过拟合并为后续导出选择最佳模型。

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,火车损失:1.2839,火车Acc:41.3%,测试Acc:39.3%★(精度/按类召回:正方形:31.8%/42.0%,圆形:45.7%/32.0%,三角形:49.0%/48.0%)
  Epoch2/100,火车损失:1.1397,火车Acc:46.2%,测试Acc:46.7%★(精度/按类召回:正方形:41.9%/52.0%,圆形:41.9%/26.0%,三角形:45.6%/52.0%)
  Epoch3/100,火车损失:1.0425,火车Acc:63.8%,测试Acc:58.0%★(精度/按类召回:正方形:46.6%/54.0%,圆形:34.0%/36.0%,三角形:59.0%/46.0%)
  Epoch4/100,列车损耗:0.9699,列车Acc:65.8%,测试Acc:55.3%(精度/按类召回:正方形:62.3%/66.0%,圆形:45.7%/42.0%,三角形:60.8%/62.0%)
  Epoch5/100,火车损失:0.9363,火车Acc:71.3%,测试Acc:72.0%★(精度/按类召回:正方形:59.3%/64.0%,圆形:38.6%/34.0%,三角形:53.8%/56.0%)
  Epoch6/100,列车损耗:0.862,列车Acc:73.1%,测试Acc:67.3%(精度/按类召回:正方形:65.1%/82.0%,圆形:53.2%/50.0%,三角形:55.0%/44.0%)
  Epoch7/100,火车损失:0.7955,火车Acc:82.4%,测试Acc:77.3%★(精度/按类召回:正方形:68.7%/92.0%,圆形:52.5%/42.0%,三角形:72.1%/62.0%)
  Epoch8/100,火车损失:0.7538,火车Acc:78.7%,测试Acc:78.0%★(精度/按类召回:正方形:74.2%/92.0%,圆形:65.7%/46.0%,三角形:67.9%/72.0%)
  Epoch9/100,列车损耗:0.6834,列车Acc:75.8%,测试Acc:75.3%(精度/按类召回:正方形:73.0%/92.0%,圆形:46.9%/46.0%,三角形:57.9%/44.0%)
  Epoch10/100,火车损失:0.6379,火车Acc:86.4%,测试Acc:82.7%★(精度/按类召回:正方形:79.7%/94.0%,圆形:71.1%/54.0%,三角形:71.7%/76.0%)
  Epoch11/100,火车损失:0.609,火车Acc:84.0%,测试Acc:83.3%★(精度/按类召回:正方形:87.5%/98.0%,圆形:70.7%/58.0%,三角形:71.7%/76.0%)
  Epoch12/100,列车损耗:0.5567,列车Acc:82.0%,测试Acc:82.7%(精度/按类召回:正方形:92.6%/100.0%,圆形:63.8%/60.0%,三角形:67.3%/66.0%)
  Epoch13/100,列车损耗:0.5446,列车Acc:65.8%,测试Acc:63.3%(精度/按类召回:正方形:92.3%/96.0%,圆形:70.8%/68.0%,三角形:76.0%/76.0%)
  Epoch14/100,列车损耗:0.5065,列车Acc:79.8%,测试Acc:82.0%(精度/按类召回:正方形:94.2%/98.0%,圆形:68.5%/74.0%,三角形:77.3%/68.0%)
  Epoch15/100,火车损失:0.4701,火车Acc:88.9%,测试Acc:86.0%★(精度/按类召回:正方形:90.7%/98.0%,圆形:76.1%/70.0%,三角形:80.0%/80.0%)
  Epoch16/100,列车损耗:0.433,列车Acc:67.8%,测试Acc:66.0%(精度/按类召回:正方形:94.1%/96.0%,圆形:73.6%/78.0%,三角形:78.3%/72.0%)
  Epoch17/100,火车损失:0.4185,火车Acc:90.2%,测试Acc:88.7%★(精度/按类召回:正方形:96.2%/100.0%,圆形:77.1%/74.0%,三角形:78.0%/78.0%)
  Epoch18/100,火车损失:0.3876,火车Acc:95.3%,测试Acc:92.7%★(精度/按类召回:正方形:98.0%/98.0%,圆形:77.8%/84.0%,三角形:82.6%/76.0%)
  Epoch19/100,列车损耗:0.3864,列车Acc:94.2%,测试Acc:92.7%(精度/按类召回:正方形:94.0%/94.0%,圆形:76.4%/84.0%,三角形:84.4%/76.0%)
  Epoch20/100,列车损耗:0.3226,列车Acc:94.9%,测试Acc:91.3%(精度/按类召回:正方形:90.6%/96.0%,圆形:77.8%/84.0%,三角形:88.4%/76.0%)
  Epoch21/100,列车损耗:0.276,列车Acc:86.0%,测试Acc:86.0%(精度/按类召回:正方形:94.1%/96.0%,圆形:77.1%/74.0%,三角形:80.4%/82.0%)
  Epoch22/100,列车损耗:0.2853,列车Acc:91.8%,测试Acc:89.3%(精度/按类召回:正方形:94.3%/100.0%,圆形:84.0%/84.0%,三角形:87.2%/82.0%)
  Epoch23/100,列车损耗:0.255,列车Acc:82.7%,测试Acc:79.3%(精度/按类召回:正方形:98.0%/98.0%,圆形:87.2%/82.0%,三角形:83.0%/88.0%)
  Epoch24/100,火车损失:0.2077,火车Acc:100.0%,测试Acc:94.7%★(精度/按类召回:正方形:92.3%/96.0%,圆形:79.2%/76.0%,三角形:84.0%/84.0%)

   提前停止,在训练数据集上实现100%的准确性

最佳加载模型(测试Acc:94.7%)

===结果===
  测试的最佳精度:94.7%
  列车/测试精度:94.9%/89.3%
  ✓没有再培训(5.6%差距)
培训完成! 🚀
模型保存在model中。布森

让我们来看看所进行的培训的质量:

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

有趣的是分别解释每个图形。 例如,所有类的精度增长几乎相同,但对于正方形,召回分数立即变得更好,并且对于三角形总是落后,在学习过程结束时仍然不是最高的。

在测试中达到100%质量后,我们没有继续培训,因为相互比较实现是没有意义的。 但我们肯定应该为数据集生成更多对象,因为平均而言,到训练结束时,模型准确地识别了正方形和圆形,但在五个建议的三角形中,平均而言,其中一个"没 虽然她标记为三角形的那些确实是三角形(网络为"圆"类显示了更多的"假阳性"错误)。

Julia(通量)神经网络的预测

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__)/unknown_rgb8", 64)

现在我们已经准备好二进制图像的数据集,我们可以下载已经训练好的模型并将其转换为C代码。 成功导出的关键要求是数据格式的完全对齐(图像的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)

# 生成库和主程序
generate_shared_lib(model, 64, length(classes))
generate_main_program(64, length(classes))
Generated neural_net.c and neural_net.h
Generated main.c

我们将神经网络本身编译成一个库。 我们还生成了主程序,该程序将"unknown_rgb8"文件夹中的图像馈送到神经网络并处理分类结果。

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

有趣的是,要运行这个神经网络,我们不需要任何库,无论是Julia还是C。它运行在任何具有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
平方_020。rgb平方0.926
square_023.rgb圆0.567
square_024.rgb平方0.668
square_028.rgb平方0.879
平方_030。rgb平方0.927
triangle_001.rgb三角形0.702
triangle_008.rgb圆0.564
triangle_011.rgb圆0.580
triangle_012.rgb三角形0.6666
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库而处理二进制图像文件-所有这些困难都被成功地克服了。

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)
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工程平台的基本可能性。