来自卷积神经网络的嵌入式代码
让我们为一个玩具任务训练一个神经网络-预测几何形状,并检查它是否可以编译成C代码,然后编译成二进制库以用于块或作为另一个项目的一部分。
导言
在本实用指南中,我们将训练神经网络以识别玩具数据集上的几何形状,然后将训练好的模型导出为C代码,将其编译为共享库,并测试集成到第三方项目或项目中的C代码块的可能性。
在转换过程中,您会注意到精度略有下降。 这可能是由于Julia和C中某些操作的实现差异(例如,批量归一化)或转换为代码时系数的简单舍入引起的,但它为在嵌入式系统上部署开辟了道路。
准备工作
在这个阶段,我们下载必要的库,修复随机数生成器,创建正方形,圆形和三角形的合成数据集,然后可视化每个类的样本图像。
生成具有已知属性(64×64大小,归一化到范围[-1.1])的受控平衡数据集允许您孤立地检查管道的每个阶段,而不受外部因素的影响。
让我们安装必要的库并初始化随机数生成器,以便我们的实验易于重现。:
# 安装必要的软件包
# Pkg.add(["Flux", "BSON", "ImageTransformations"])
using Random
Random.seed!(5);
合成数据集
让我们创建一个由三个类组成的玩具数据集。 其中一些对象放在"未知"文件夹中,即它们的类,虽然写在文件名中,但对系统来说将是未知的。 您可以将其称为验证数据集。 其余的-培训和测试-安排在适当的文件夹中。
include("$(@__DIR__)/_scripts/generate_shape_dataset.jl")
generate_shape_dataset(samples_per_class=200, test_samples=30, img_size=64)
在这个阶段,我们试图生成一个相当多样化的数据集(带有三角形旋转),但与此同时不要过分复杂代码,例如,我们在学习过程中没有做增强。 总的来说,这个阶段的问题最小。
看看训练数据集
以下是我们训练数据集中的样本对象:
include("$(@__DIR__)/_scripts/show_dataset_samples.jl")
DATA_DIR = "$(@__DIR__)/训练数据";
gr()
show_dataset_samples(DATA_DIR, samples_per_class=10)
模型训练与分析
在这里,我们开始卷积神经网络的学习过程,保存度量的历史记录,分析准确性和损失的动态,并在测试图像上显示预测的马赛克。
按类别监控精度/召回指标并提前停止验证精度有助于及时检测过拟合并为后续导出选择最佳模型。
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);
让我们来看看所进行的培训的质量:
include("$(@__DIR__)/_scripts/analyze_training_log.jl")
gr()
df, classes, p = analyze_training_log("training_log.txt")
display(p)
有趣的是分别解释每个图形。 例如,所有类的精度增长几乎相同,但对于正方形,召回分数立即变得更好,并且对于三角形总是落后,在学习过程结束时仍然不是最高的。
在测试中达到100%质量后,我们没有继续培训,因为相互比较实现是没有意义的。 但我们肯定应该为数据集生成更多对象,因为平均而言,到训练结束时,模型准确地识别了正方形和圆形,但在五个建议的三角形中,平均而言,其中一个"没 虽然她标记为三角形的那些确实是三角形(网络为"圆"类显示了更多的"假阳性"错误)。
Julia(通量)神经网络的预测
include("$(@__DIR__)/_scripts/simple_mosaic.jl")
UNKNOWN_DIR = "$(@__DIR__)/未知";
gr()
plot(create_simple_mosaic(UNKNOWN_DIR, imsize=64))
我们看到相当不错的预测,但这与其说是成功培训的结果,不如说是设计师长期工作的结果。 最耗时的是网络体系结构的选择(层数,通道,使用BatchNorm和Dropout)和超参数(学习率,批量大小,增强),以实现稳定的收敛并避免在有限的数据集上过度拟合。 因此,例如,增强被移动到数据集生成函数以简化示例,以及由于仅对三角形需要此过程的事实。
出口到C和测试
现在我们将预处理的图像转换为二进制格式,生成神经网络的C代码,将其编译为可执行文件,并将从C实现中获得的预测可视化。 我们故意假设代码将在没有PNG库的平台上工作。 因此,我们使用单独的脚本将图像转换为二进制格式。 这些二进制文件包含矩阵,其元素包括每个像素的每个颜色通道,由单个UInt8数字表示。
include("$(@__DIR__)/_scripts/convert_png_to_rgb8.jl")
convert_png_to_rgb8("$(@__DIR__)/未知", "$(@__DIR__)/unknown_rgb8", 64)
现在我们已经准备好二进制图像的数据集,我们可以下载已经训练好的模型并将其转换为C代码。 成功导出的关键要求是数据格式的完全对齐(图像的RGB8,系数的HWC顺序)以及Julia和C之间的权重遍历顺序,这是通过在所有阶段显式控制索引和归一化
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))
我们将神经网络本身编译成一个库。 我们还生成了主程序,该程序将"unknown_rgb8"文件夹中的图像馈送到神经网络并处理分类结果。
;gcc -shared -fPIC neural_net.c -o libneuralnet.so -lm
;gcc main.c -o classify_unknown -ldl -lm
有趣的是,要运行这个神经网络,我们不需要任何库,无论是Julia还是C。它运行在任何具有C编译器的系统上。
;./classify_unknown
当将模型转移到C时,必须解决几个非平凡的任务:在没有第三方库的情况下手动实现卷积和BatchNorm,将所有操作转换为单一的HWC格式,准确地再现权重遍历的顺序(对于多通道层尤其重要),以及由于目标环境中缺少PNG库而处理二进制图像文件-所有这些困难都被成功地克服了。
C语言神经网络的预测
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)
尽管有这些困难,我们已经展示了一个完整的工作管道,证明即使目标平台的资源有限,也可以将神经网络从Julia导出到C。
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)
结论
我们已经展示了如何通过创建一个包含神经网络的程序的整个周期:从创建数据集和在Julia上训练模型到导出到C并检查性能,这证实了使用生成的代码远远超出Engee工程平台的基本可能性。
.png)
.png)
.png)
.png)