随着日益火红的各种应用 ,机器学习的场景正迅速的移动到终端装置. 人们期望嵌入式装置上能够提供更多智能服务,例如具有语音识别的智能音箱,能够及时识别脸孔的安控摄影机等等... 为了达到这个目标,在开发初期建立一个稳固的评估方案成为各家芯片开发商必要的课题.
在本篇文章中 ,我会利用一个简单的卷积神经网络(Convolutional Neural Network, CNN)作为一个终端装置上的应用范例,以推断( inference)运行时间,神经网络大小, CPU负载以及内存存取 四个面向分析不同神经网络实作方式在嵌入式装置端的差异.
神经网络推断效能表
对于硬件资源有限的嵌入式装置 ,运行时间跟神经网络大小绝对开发机器学习两个重要的因素. 我会在接下来的篇幅如何利用 Arm提供的开发工具包来优化在终端装置上的推断程序. 除了静态分析之外,神经网络推断实际运行时间则是另外一个关键部分需要探讨,各层网络巨量的数据处理以及模型参数的读取也是系统上亟需要分析的课题. 我会利用一个例子来解释如何利用图像式工具来分析.
这篇文章中 ,我选择28x28灰阶像素手写识别(MNIST dataset)作为例子说明如何使用Arm 开发工具帮助用户开发并分析效能. 选择这个神经网络的因为它单纯,成熟 且容易使用. 再继续接下来阅读之前,强烈推荐读者先参考这篇文章. 我会附上几个相关链接在文章最后附录.
卷积网络范例
上图的例子说明一张 28x28灰阶图片会在第一层网络被降级为32张14x14的影像,第二层网络再执行一次相似的流程. 之后执行两次全链接 (fully connection) 得到最后十个分别代表数字0,1,2,... 9机率的数值,
参考这篇文章 blog,你可以很容易地用Python实作,仅仅两次conv2D layers及两次 max_pooling即可以完成下图的神经网络模型.
经过四小时在我计算机上重复的训练后 ,最终我得到99.2%精准度的模型. 而这样的模型已足够让我让我进行下一阶段,装置端推断(device inference).
为了建构一个在 Arm CPU架构上探索机器学习的平台,我采用Armv8.2 LITTLE-core Cortex-A55 作为我目标平台. 由于目前是市场上仍没有 Cortex-A55的单芯片可以开发软件应用,我需要一个虚拟平台(Virtual Platform)在硬件量产之前早期开发软件并且分析系统效能. 而 Arm Fast Model 便是我需要的完美方案,这篇文章有详细的介绍.
由于我们已经找到最佳的训练模型 ,继续使用Python在嵌入式装置建立神经网络并不是好主意. 使用 C语言实作神经网络可以大幅度减少软件的额外的资源浪费. 这时候我需要一套有效率的开发环境可以让我快速的从无到有开发一套神经网络算法,此外,我还需要图像式的分析工具能让我完整掌握整体系统效能的趋势. 无庸置疑的 Arm DS5 和Streamline 提供完整工具链(toolchain) 满足我开发的需求.
首先 ,我在DS5增加一个Cortex-A55 Fixed Virtual Platforms (FVP) debug connection ,在这里我使用bare-metal设定保持单纯. 你可以将虚拟平台视为真实的芯片,接下来所有的实作都是在虚拟平台执行.
另外一个 DS5的好处是脚本(script)的支持. 在这个例子中,初始化脚本将图片以及模型参数在开机时分别加载地址0x80150000 和 0x80100000 来减少额外韧体实作的工作.
我采用 DS5里PMU范例作为我开发的基础,你可以在这里下载我的项目.
正准备要实作神经网络时 ,我发现日本办公室里绝顶聪明的Ryuji Tanaka已经实作了一版应用程序编程接口(Application Programming Interface, API) 放在他的 GitHub,于是我参考他的程序作为我第一个版本. 感谢你, Ryuji-san.
三个主要的 API分别为:
Creates a convolution kernel that is convoluted with the layer input to produce a tensor of outputs
使用上述的 API,我可以很快地建构出一个 MNIST 的网络,虚拟程序代码(pseudo code)如下
mnist_cnn_eval() { // Pre process convolution(&lay, layer0, layer1, layer0_paramter); max_pooling(&lay, layer1, layer2); convolution (&lay, layer2, layer3, layer2_paramter); max_pooling (&lay, layer3, layer4); fully_connected (&lay, layer4, layer5, layer5_paramter); fully_connected (&lay, layer5, layer6, layer6_paramter); // Post process }
呼叫 PMU函式来将指令执行数目存在软件变量 instr .
试着加载 image_8.jpg ()来测试.
看起来不错 ,执行结果没有问题.
接下来我利用 DS5的脚本来验证所有的MNIST数据集(dataset)来确认C程序实作与Python的结果一致.
搞定 ,收工?
不幸地是 ,观察PMU发现初版的神经网络需要花费高达一千八百万个指令来运算单一张图,这表示在 100 MIPS的终端装置下,一秒钟至多只能处理7.6张(**) 28x28灰阶影像.
Streamline Profiling
为了探索实际 CPU使用的行为,我需要利用Streamline.
参考这篇的设定,我可以开始观察CPU行为.
在 100 MIPS的设定下, Streamline呈现完整运行时间为133毫秒(ms). 用户可以改变不同的time units观察不同时间区间的行为,点击这里了解更多.
有趣的发现是一个多重循环函式居然消耗了 96.7% CPU负载. 多年的编码经验告诉我该试着从编译程序进行优化.
优化编译程序选项
Bingo!
调整 Arm Compiler 6优化程度可以大幅缩短45% CPU运行时间. (减少6百万的指令)
Streamline里的函式使用统计表显示, convolution()仍占据87% CPU资源,若是我能降低这部分花费的时间, 整体运行时间必定可以再进一步提升.
重新设计 API
Streamline除了提供指令执行的信息外,也提供每次采样时间的CPU平均存取次数, 这样的信息可以帮助我大致评估各个神经网络推断阶段对内存使用状况. 以目前的神经网络执行结果来看
计算出来的存取次数比预期地来的多 ,这提醒我该检视API实作.
利用 DS5解析汇编语言的功能,我发现将卷积网络实作分开会得到比较好的效果.
//pseudo code float int convolution_filter2() { // Load parameter for (current_filter_row = 0; current_filter_row < filter_rows; current_filter_row++) { for (current_filter_col = 0; current_filter_col < filter_cols; current_filter_col++) { for (in_ch = 0; in_ch < input_channel; in_ch++) { current_input = ((float*)inputs)[ ((stride_row + current_filter_row) * intput_columns * input_channel) + ((stride_col + current_filter_col) * input_channel) + in_ch]; for (out_ch = 0; out_ch < output_channel; out_ch++) { current_weight = ((float*)weights)[ (current_filter_row * filter_cols * input_channel * output_channel) + (current_filter_col * input_channel * output_channel) + (in_ch * output_channel) + out_ch]; current_result = current_input * current_weight; ((float*)outputs)[ (stride_row * output_columns * output_channel) + (stride_col * output_channel) + out_ch] += current_result; } } } } // ... }
让我们再测试一次.
Nice,新的API省了3 million指令,执行效率是前一个版本的3.3倍
再次观察 Streamline函式使用统计表,似乎没有空间在C程序上再提升效能. (***)
计算内存读写次数.
现在我们可以建出一个表格比较三个版本神经网络实作:
经过一连串的持续编码优化 ,发现软件效能似乎没有额外提升的空间. 设计平行处理的矩阵乘法加速器来取代convolution_filter2()可能是另外一个可以思考的方向.
我有一个加速器设计的想法来降低 CPU负载,这个平行处理的矩阵乘法加速器可以直接存取输入数据,训练数据再将运算结果写入输出端. 这样的想法非常直觉,但是我该怎么在没有对应硬件设计下验证我的想法?
//pseudo code float int convolution_filter3() { // Pre-load *CONV3_Eng_ROW = stride_row; *CONV3_Eng_COL = stride_col; *CONV3_Eng_EN = 1; while (!*CONV3_Eng_READY) ; return *CONV3_Eng_Z; } convolution_conv3() { // Initial for (stride_row = 0; stride_row < lay->output_rows; stride_row++) { for (stride_col = 0; stride_col < lay->output_columns; stride_col++) { output[output_row][output_col] = convolution_filter3( stride_row, stride_col ); } } // ... }
SystemC/TLM提供一个预估时序行为模型而且可以整合到Arm Fast Model,参考这里,我可以在一定的缓存器读写时序的假设下估计系统效能.
使用这样的虚拟平台方案 ,我能够早期探索不同加速器实作下的系统级效能
以我目前的例子 ,我假定四个cycle的缓存器读取延迟以及两个cycle的缓存器写入延迟下,整体执行缩短到指令数
结论
利用这个范例 ,我尝试演示如何在Arm-Based嵌入式装置导入机器学习的能力.
DS-5 和 Fast Model 提供绝佳的开发体验协助用户开发并分析机器学习的算法,接下来我会持续介绍在Arm CPU 架构下相关机器学习的有趣项目.
请照访我们的开发者网站 developer.arm.com 了解更多信息,有任何问题欢迎在下方留言,我会尽快回复
参考
http://yann.lecun.com/exdb/mnist/
http://adventuresinmachinelearning.com/keras-tutorial-cnn-11-lines/
https://github.com/keras-team/keras/blob/master/examples/mnist_cnn.py
https://elitedatascience.com/keras-tutorial-deep-learning-in-python
Arm tools:
https://developer.arm.com/products/software-development-tools/ds-5-development-studio
https://developer.arm.com/products/software-development-tools/ds-5-development-studio/streamline/overview
https://developer.arm.com/products/system-design/fast-models
GitHub:
https://github.com/odinshen/ArmMLVP_MNIST
*: 在我Jupyter实作中, 实际上使用16个输出来取代范例中的32个, 因为实验发现使用32个输出并没有对于模型精准度有帮助
**: 基于CPU执行能力为100 MIPS的假设下
***: 事实上 我尝试用NEON组合语言提升软体效能, 我会在之后的文章探讨
非常赞,感谢Odin Shen 兄的鼎力支持