MobileNet V1 for MCU (STM32)
1.引言
本文的主要内容是对tflite参数提取的后续补充(Tensorflow中tflite权重参数提取与推理过程示意:https://www.cnblogs.com/ruidongwu/p/14294009.html)。当获取到tflite后的参数,如果需要在嵌入式平台(例如MCU),现有能够支持的研究包括(以下仅为个人观点):
(1)谷歌Tensorflow官方提供的TF-Lite Micro,优点是官方支持、血统纯正、具有良好的技术支持、功能相当齐全,缺点是采用C++,需要对tflite文件进行动态加载;(相关资料:https://blog.tensorflow.org/2021/02/accelerated-inference-on-arm-microcontrollers-with-tensorflow-lite.html)
(2)ARM官方提供的CMSIS-NN库,里面提供了优化后的卷积计算等基础算子函数,但是貌似不能够直接支持tflite文件;(相关资料:https://github.com/ARM-software/CMSIS_5)
(3)意大利-博洛尼亚大学Alessandro Capotondi, Manuele Rusci在github上公开的用于MobileNet推理的加速框架;(该资料对笔者在STM32上完成MobileNet推理的研究具有很大的启发作用,再次特地提出感谢。相关资料:https://github.com/EEESlab/mobilenet_v1_stm32_cmsis_nn)
(4)麻省理工韩松团队的MCUNet推理框架(https://arxiv.org/abs/2007.10319),速度和性能均为No.1,笔者心目中永远的神,现在已经开源(https://github.com/mit-han-lab/mcunet);
(5)意法半导体官方也有自己的神经网络框架,但是笔者没有对此进行特意的研究。
上述方法其实对卷积神经网络在嵌入式平台的发展提供了相当好的研究思路。但是秉承着不仅要知其然,还要知其所以然的态度,笔者决定亲自来重新造一遍轮子。性能上当然比不上最优的设计,但是从易读性,还有对网络的理解,笔者认为还有一定的优势。同时在编写的时候采用了ANSI C的方式,不包含任何浮点操作,全为整形的算术运算和逻辑运算,可以很方便的移植到任意嵌入式平台,包括但不限于ARM,前提是有足够的RAM(>=128kB)和ROM(>=512kB),后续篇幅会给出空间占用的理论推导。
同时笔者也希望本文能够起到抛砖引玉的作用,也可以作为加强对MobileNet理解的短文,由于笔者编程能力有限,代码后续还有很多待优化的地方,优化的地方包括时间和空间两方面,也希望能够对读者起到启发的作用。如果觉得本文对你有帮助,可以在评论区给笔者点个赞,源码在最后。
2. 主要程序概述
2.1 权重文件概述
权重文件的存储笔者采用const数组的形式进行存储,因此不涉及对tflite的文件解析功能,这在一定程度上减少了类似于TF-Lite Micro的动态内存申请,提升了空间的使用效率,舍弃的代价为不能够任意的更换网络,如果需要更新网络的权重参数,需要重新更改对应的权重文件。如下为MobileNet第一层卷积层对应的权重:
1 #ifndef SRC_L0_W_H_ 2 #define SRC_L0_W_H_ 3 4 #define LAYER0_OFFSET 157 5 6 #define LAYER0_SCALE ((long long)1621425058) 7 #define LAYER0_SHIFT 39 8 9 const unsigned char l0_w[3*3*8*4]={ 10 160,159,159,0, 11 134,210,130,0, 12 165,164,163,0, 13 125,165,182,0, 14 209,247,170,0, 15 158,157,157,0, 16 130,116,147,0, 17 147,138,152,0, 18 159,159,158,0, 19 116,238,118,0, 20 165,162,161,0, 21 123,165,184,0, 22 201,239,178,0, 23 160,160,161,0, 24 113,90,140,0, 25 147,140,152,0, 26 160,160,159,0, 27 133,205,133,0, 28 162,160,159,0, 29 150,159,163,0, 30 141,138,163,0, 31 157,158,162,0, 32 135,128,150,0, 33 158,156,156,0, 34 159,157,157,0, 35 128,219,125,0, 36 164,162,161,0, 37 115,169,188,0, 38 67,1,137,0, 39 161,159,161,0, 40 151,154,154,0, 41 133,120,147,0, 42 159,158,158,0, 43 106,255,108,0, 44 165,162,160,0, 45 113,169,190,0, 46 91,17,128,0, 47 162,161,164,0, 48 142,146,153,0, 49 135,122,147,0, 50 161,160,158,0, 51 130,212,128,0, 52 164,161,159,0, 53 148,160,163,0, 54 164,173,149,0, 55 162,163,167,0, 56 147,150,156,0, 57 156,157,155,0, 58 159,157,156,0, 59 148,177,145,0, 60 165,162,161,0, 61 140,164,166,0, 62 190,228,163,0, 63 161,160,164,0, 64 172,174,161,0, 65 142,139,152,0, 66 159,158,158,0, 67 137,197,135,0, 68 166,163,160,0, 69 139,162,169,0, 70 184,226,172,0, 71 162,161,164,0, 72 177,180,163,0, 73 143,138,152,0, 74 160,159,159,0, 75 153,171,147,0, 76 168,163,160,0, 77 159,155,156,0, 78 143,137,158,0, 79 164,163,168,0, 80 164,165,162,0, 81 156,161,157,0, 82 }; 83 84 const int l0_b[8]={ 85 -10309, 86 33272, 87 11574, 88 65872, 89 33794, 90 4138, 91 39818, 92 49862, 93 }; 94 95 #endif /* SRC_L0_W_H_ */
上面的LAYER0_OFFSET对应权重自身存在的固定偏置,LAYER0_SCALE和LAYER0_SHIFT是对层间的数据位宽转换功能的重构,从运行原理上层间数据转换需要使用浮点类型的算术运算,考虑到有的MCU平台不具备浮点处理单元(FPU),因此笔者采用“Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference”文献中的方法,将浮点乘法运算转换成整形乘法和移位操作。其中LAYER0_SCALE为整形数据,LAYER0_SHIFT为移位的位宽。
2.2 卷积运算示例
在有了权重文件后,我们可以继续对输入的图像进行卷积计算,本文使用的输入图像大小为128*128*3,同时为了存储空间中字节对齐,通过补零的方式将输入图像大小扩展为128*128*4=65536,因此输入图像需要64kB大小的RAM。具体卷积计算过程如下:
1 //input 128*128*3 2 //output 64*64*8 3 void mobilenet_conv0(L0_IN *DataIn, L1_DW_IN *DataOut) 4 { 5 for (int r = 0; r < 128; r += 2) 6 { 7 for (int c = 0; c < 128; c += 2) 8 { 9 for (int n = 0; n < 8; n++) 10 { 11 int sum = l0_b[n]; 12 for (int k0 = 0; k0 < 3; k0++) 13 for (int k1 = 0; k1 < 3; k1++) 14 { 15 if (((r+k0)<128)&&((c+k1)<128)) 16 for (int m = 0; m < 3; m++) 17 { 18 sum += ((short int)(DataIn->Data[r + k0][c + k1][m])-128) * ((short int)(pL0_W->Data[k0][k1][n][m]) - LAYER0_OFFSET); 19 }//end m 20 }//end k 21 sum = (sum*LAYER0_SCALE) >> LAYER0_SHIFT; 22 if (sum>255) 23 sum = 255; 24 else if (sum < 0) 25 sum = 0; 26 DataOut->Data[r >> 1][c >> 1][n] = sum; 27 }//end n 28 }//end c 29 }//end r 30 }
经过卷积计算后得到的输出尺寸为64*64*8=32768,至少32kB。从程序中可以看出,在完成乘累加后对sum进行长整数乘法与移位,同时完成0~255区间的截断(对应训练过程中的Relu6),作为下一层的8bit数据输入。
2.3 基于乒乓存储的MobileNet推理结构
在完成上述基本操作后,我们可以基于乒乓存储的方式逐层完成计算,并在最后返回分类结果中类别的编号,必要时可查看最后一层全连接层的结果。程序主要内容如下:
1 unsigned int mobilenet_proc(unsigned char Buf0[128*128*4], unsigned char Buf1[65536], int FC_Res[1001]) 2 { 3 mobilenet_conv0((L0_IN*)Buf0, (L1_DW_IN*)Buf1); 4 5 mobilenet_dw1((L1_DW_IN*)Buf1, (L1_PW_IN*)Buf0); 6 mobilenet_pw1((L1_PW_IN*)Buf0, (L2_DW_IN*)Buf1); 7 8 mobilenet_dw2((L2_DW_IN*)Buf1, (L2_PW_IN*)Buf0); 9 mobilenet_pw2((L2_PW_IN*)Buf0, (L3_DW_IN*)Buf1); 10 11 mobilenet_dw3((L3_DW_IN*)Buf1, (L3_PW_IN*)Buf0); 12 mobilenet_pw3((L3_PW_IN*)Buf0, (L4_DW_IN*)Buf1); 13 14 mobilenet_dw4((L4_DW_IN*)Buf1, (L4_PW_IN*)Buf0); 15 mobilenet_pw4((L4_PW_IN*)Buf0, (L5_DW_IN*)Buf1); 16 17 mobilenet_dw5((L5_DW_IN*)Buf1, (L5_PW_IN*)Buf0); 18 mobilenet_pw5((L5_PW_IN*)Buf0, (L6_DW_IN*)Buf1); 19 20 mobilenet_dw6((L6_DW_IN*)Buf1, (L6_PW_IN*)Buf0); 21 mobilenet_pw6((L6_PW_IN*)Buf0, (L7_DW_IN*)Buf1); 22 23 mobilenet_dw7((L7_DW_IN*)Buf1, (L7_PW_IN*)Buf0); 24 mobilenet_pw7((L7_PW_IN*)Buf0, (L8_DW_IN*)Buf1); 25 26 mobilenet_dw8((L8_DW_IN*)Buf1, (L8_PW_IN*)Buf0); 27 mobilenet_pw8((L8_PW_IN*)Buf0, (L9_DW_IN*)Buf1); 28 29 mobilenet_dw9((L9_DW_IN*)Buf1, (L9_PW_IN*)Buf0); 30 mobilenet_pw9((L9_PW_IN*)Buf0, (L10_DW_IN*)Buf1); 31 32 mobilenet_dw10((L10_DW_IN*)Buf1, (L10_PW_IN*)Buf0); 33 mobilenet_pw10((L10_PW_IN*)Buf0, (L11_DW_IN*)Buf1); 34 35 mobilenet_dw11((L11_DW_IN*)Buf1, (L11_PW_IN*)Buf0); 36 mobilenet_pw11((L11_PW_IN*)Buf0, (L12_DW_IN*)Buf1); 37 38 mobilenet_dw12((L12_DW_IN*)Buf1, (L12_PW_IN*)Buf0); 39 mobilenet_pw12((L12_PW_IN*)Buf0, (L13_DW_IN*)Buf1); 40 41 mobilenet_dw13((L13_DW_IN*)Buf1, (L13_PW_IN*)Buf0); 42 mobilenet_pw13((L13_PW_IN*)Buf0, (L14_POOL_IN*)Buf1); 43 44 mobilenet_pooling((L14_POOL_IN*)Buf1, (L14_FC_IN*)Buf0); 45 return mobilenet_fc((L14_FC_IN*)Buf0, (L14_FC_OUT*)FC_Res); 46 }
采用乒乓存储的前提是卷积神经网络中层级特征图的层次特性,只需要一次使用。前提是不存在残差模块,即每层特征图由单一层生成,并且用于单一层的输入,不存在二次使用。如果存在残差模块,需要额外的内存空间存储临时的特征图。根据MobileNet网络每一层特征图的大小,选取乒乓存储对应的最大值,基本可以确定乒乓存储的大小分别为64kB和64kB,即对应前文所述的128kB的RAM大小。至于512kB的ROM大小,主要用于网络中权重参数的参数,对此笔者不再对其具体数值大小进行分析。
2.4 未来应用中可行的优化方法
在实际应用中取决于任务的类型,不可避免的需要对网络进行压缩,此处包含对网络层数的压缩,原本的MobileNet中包含卷积计算的层数共有28层,如果将深度可分离卷积合并为一层,一共有15层,对应1000类物体类别。具体的压缩方法有:
(1)实际中用于分类的类别可能只需要简单的十几类,甚至几类,此时通过减少网络层数来实现对网络的压缩,可以减少网络的计算量,减少权重的数量,对应减少ROM空间的需求。
(2)减少输入的特征图大小是另一种网络压缩的方法,实际应用中可能不需要128*128*4大小的特征图,对于某些典型特征,特征图甚至可以减少到64*64或者48*48(参考MCUNet设计方法),该方法同样减少网络计算量,减少层间特征图的大小,对应减少RAM空间的需求。
3. 实验测试结果
3.1 基于STM32的测试
测试程序如下,其中测试平台为STM32H743,时钟频率我们直接拉满480MHz,并通过定时器的计数器统计运行的时间。(完整工程评论区留言,或者发送邮件至笔者邮箱 wuruidong@hotmail.com)
1 uint8_t Buf0[65536]; 2 uint8_t Buf1[65536]; 3 int FC_Res[1001]={0}; 4 FATFS fs[1]; 5 FIL file; 6 u32 res; 7 8 f_mount(fs,"0:",1); 9 res = f_open(&file, "img128.bin", FA_READ); 10 if(res==FR_OK) 11 { 12 printf("Read img128.bin!\r\n"); 13 f_read(&file, Buf0, 128*128*4, &res); 14 f_close(&file); 15 printf("img128.bin is 0x%x!\r\n", Buf0[0]); 16 17 class_n = mobilenet_proc(Buf0, Buf1, FC_Res); 18 19 printf("Class number is %d!\r\n", class_n); 20 printf("Class number is %s!\r\n", mobilenet_get_label_en(class_n)); 21 printf("标签名称是 %s!\r\n", mobilenet_get_label_cn(class_n)); 22 }
编译选项勾选"Optimize for Time"。空间大小为:Total RW Size (RW Data + ZI Data)=146776 ( 143.34kB),Total ROM Size (Code + RO Data + RW Data) =542156 ( 529.45kB)。
完成MobileNet网络推理的时间为115889us,对应帧率约为8.63FPS。该测试结果与意大利-博洛尼亚大学Alessandro Capotondi, Manuele Rusci(https://github.com/EEESlab/mobilenet_v1_stm32_cmsis_nn)对比,性能大约为后者的75%,说明后续还有很大的优化空间。
3.2 基于ZYNQ UltraScale系列PS端的测试
笔者手边另外的平台是ZYNQ UltraScale系列的XAZU3EG异构处理器,其中PS端为4核Cortex-A53,主频为1.2GHz,基于Xilinx SDK的Baremetal裸机开发,GCC编译器选择"-O2"。完成MobileNet网络推理的时间为28982us,对应帧率约为34.50FPS。该帧率结果与STM32平台相比,刚好等于主频的比例关系。
4. 总结与未来计划
通过上述的分析,在MCU上实现卷积神经网络具备可行性,虽然对于原始的网络推理速度不能够达到视频图像的实时处理,但是在未来的应用中,可以通过网络压缩的方法提升推理速度,减少内存空间(RAM)和存储空间(ROM)的需求。同时由于笔者能力有限,现有代码还未进行指令集级别的优化,在未来中还有待进一步的研究。
此处为源码:点我下载
应网友需求,将完整工程开源,仅供学术研究与教育:点我下载
除了上面的优化方法,有的读者可能会问:“如果不想对网络进行压缩,因为网络压缩降低了网络的分类精度,但是还想减少推理时间和提升帧率,最好还把低功耗给做了,那怎么办?”
细心的读者可能发现了最后一组实验是在ZYNQ异构平台上完成的,那么ZYNQ的PL部分的FPGA加速是否为下一步的研究内容?
笔者:“没错,做了这么久的铺垫,未来将介绍基于FPGA的卷积神经网络加速方法,该部分研究早已完成,性能绝对远远超过上述实验结果,请各位拭目以待。”