在之前验证了PC环境的基础上,继续测试原开源项目中的手写数字识别。这是中文简介链接,
docs/example_mnist_simple_cn.md · RT-Thread-Mirror/nnom - 码云 - 开源中国 (gitee.com)
一、编译原模型
打开项目文件mnist-simple,编译运行mnist_simple.py文件,如果之前跑过Auto-test实例,环境应该没有问题,在目录下会生成几个新文件,主要关注如下几个文件,
image.h是图像测试文件,weight.h是模型训练完成后的权重参数文件。
二、MCU移植
原项目作者用的是RT-Thread系统,移植文件也仅限于MCU文件夹下的image.h、weight.h和main.c文件,并且在测试时还使用了RT-Thread下的测试模块?我并没有接触过RT-Thread,NNOM是一个纯C的框架,在其他操作系统和MCU上应该也是可以运行的,在研究了移植文件的代码后,选择用Freertos+STM32H7来测试。
1、文件移植
nnom ├───docs │ ├───figures // 文档图片 │ └───*.md // 文档 ├───examples // 例子 ├───inc // 头文件 ├───port // 移植文件 ├───scripts // 脚本工具,模型转换工具 ├───src // 源代码 │ LICENSE // 软件包许可证 │ README.md // 软件包简介 └───SConscript // 构建脚本
按照目录结构的解释,NNOM需要移植src、port、inc三个文件夹,image.h、weight.h是图像测试和权重参数,因此也需要移植。
在keil的项目目录下建立对应目录,添加相应的.c和.h文件。
keil的目录管理是不能建立多级目录的,因此要把整个NNOM的.c文件都加在一个目录下,如下图所示,
不要遗漏,全部加过来。另外不要忘了添加对应的.h文件路径,除了NNOM的头文件路径,还要image.h、weight.h路径,
2、部分接口的移植
在nnom_port.h文件中,找到以下代码,
#define nnom_malloc(n) pvPortMalloc(n)
#define nnom_free(p) vPortFree(p)
...
#define nnom_us_get() 0 // return a microsecond timestamp
#define nnom_ms_get() 0 // return a millisecond timestamp
#define NNOM_LOG(...) App_Printf(__VA_ARGS__)
需要在现有环境下提供几个接口,如上图中的内存申请/释放接口,日志打印接口。内存接口我直接用的freertos下的内存接口,测试下来能用;log打印的接口,重映射一个串口到printf或者随便什么名字就行,函数写好后,把它贴过来(STM32重映射串口到printf,网上教程一堆);还有获取时间什么的,还没研究咋给。
另外,代码中的NNOM_LOG打印都是\n结尾,实际打印是换不了行的,要以‘\r\n’结尾才行,我直接在代码中replace替换的,或者在给log打印接口的函数那里做个逻辑判断,检测到\n结尾就自动打印一个\r也行。
NNOM_LOG("Model version: %d.%d.%d\r\n", major, sub, rev);
3、main.c文件移植
结合原项目的一些注释,原本的main.c大概就是用一个调试接口FINSH_FUNCTION_EXPORT(mnist, mnist(4) )(可能是串口?)给板子发了一个指令,然后板子收到指令后正常跑模型,完事以后把对应结果再打印出来。所以我们只需要移植main函数部分就行,如果需要接收上位机命令,可以另做逻辑(接个摄像头也可以),更改后的代码如下所示,
#include "ModelRun.h"
#include "printtask.h"
#include "weights.h"
nnom_model_t *model;
// extern static nnom_model_t* nnom_model_create(void);
//串口画图用的字符串
const char codeLib[] = "@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^`'. ";
void model_test()
{
model = nnom_model_create(); //创建模型
model_run(model); //运行模型
}
void mnist_pre()
{
uint32_t tick, time;
uint32_t predic_label; //预测出的标签
float prob;
unsigned char index=6; //实际的标签的数组位号
int Probability;
model_stat(model);
App_Printf("Total Memory cost (Network and NNoM): %d\r\n", nnom_mem_stat());
memcpy(nnom_input_data, (int8_t*)&img[index][0], 784);
nnom_predict(model, &predic_label, &prob);
print_img((int8_t*)&img[index][0]);
// printf("Time: %d tick\n", time);
App_Printf("Truth label: %d\r\n", label[index]);
App_Printf("Predicted label: %d\r\n", predic_label);
App_Printf("Probability: %d%%\r\n", (int)(prob*100));
}
void print_img(int8_t * buf)
{
for(int y = 0; y < 28; y++)
{
for (int x = 0; x < 28; x++)
{
int index = 69 / 127.0 * (127 - buf[y*28+x]);
if(index > 69) index =69;
if(index < 0) index = 0;
App_Printf("%c",codeLib[index]);
App_Printf("%c",codeLib[index]);
}
App_Printf("\r\n");
}
}
void StartBSSTask(void *argument)
{
/* USER CODE BEGIN StartBSSTask */
unsigned int len = 0;
char *p = NULL;
/* Infinite loop */
for(;;)
{
//fft_test();
//len = xPortGetFreeHeapSize();
//App_Printf("Free Heap Size is:\t%d\r\n",len);
model_test();
osDelay(200);
mnist_pre();
osThreadSuspend(BSSTaskHandle);
}
/* USER CODE END StartBSSTask */
}
代码放在ModelRun.c和ModelRun.h中,在freertos中新建测试任务,调用函数即可。这里测试的预测标签位号是6,在对应的image.h中找到img数组,IMG6是LABLE 4,也就是实际图像数字是4。
...
#define IMG6_LABLE 4
...
static const int8_t img[10][784] = {IMG0,IMG1,IMG2,IMG3,IMG4,IMG5,IMG6,IMG7,IMG8,IMG9};
...
4、运行输出
编译下载,上位机的串口接收结果如下所示,可以正确预测出结果,速度的话,体感很快,具体需要做时间测试的接口才知道。
Model version: 0.4.3
NNoM version 0.4.3
To disable logs, please void the marco 'NNOM_LOG(...)' in 'nnom_port.h'.
Data format: Channel last (HWC)
Start compiling model...
Layer(#) Activation output shape ops(MAC) mem(in, out, buf) mem blk lifetime
-------------------------------------------------------------------------------------------------
#1 Input - - ( 28, 28, 1,) ( 784, 784, 0) 1 - - - - - - -
#2 Conv2D - ReLU - ( 28, 28, 12,) 84k ( 784, 9408, 0) 1 1 - - - - - -
#3 MaxPool - - ( 14, 14, 12,) ( 9408, 2352, 0) 1 1 1 - - - - -
#4 Conv2D - ReLU - ( 14, 14, 24,) 508k ( 2352, 4704, 0) 1 - 1 - - - - -
#5 MaxPool - - ( 7, 7, 24,) ( 4704, 1176, 0) 1 1 1 - - - - -
#6 Conv2D - ReLU - ( 7, 7, 48,) 508k ( 1176, 2352, 0) 1 - 1 - - - - -
#7 MaxPool - - ( 4, 4, 48,) ( 2352, 768, 0) 1 1 1 - - - - -
#8 Flatten - - ( 768, ) ( 768, 768, 0) - - 1 - - - - -
#9 Dense - ReLU - ( 96, ) 73k ( 768, 96, 1536) 1 1 1 - - - - -
#10 Dense - - ( 10, ) 960 ( 96, 10, 192) 1 1 1 - - - - -
#11 Softmax - - ( 10, ) ( 10, 10, 0) 1 - 1 - - - - -
#12 Output - - ( 10, ) ( 10, 10, 0) 1 - - - - - - -
-------------------------------------------------------------------------------------------------
Memory cost by each block:
blk_0:4704 blk_1:9408 blk_2:2352 blk_3:0 blk_4:0 blk_5:0 blk_6:0 blk_7:0
Memory cost by network buffers: 16464 bytes
Total memory occupied: 18632 bytes
Print running stat..
Layer(#) - Time(us) ops(MACs) ops/us
--------------------------------------------------------
#1 Input - 0
#2 Conv2D - 0 84k
#3 MaxPool - 0
#4 Conv2D - 0 508k
#5 MaxPool - 0
#6 Conv2D - 0 508k
#7 MaxPool - 0
#8 Flatten - 0
#9 Dense - 0 73k
#10 Dense - 0 960
#11 Softmax - 0
#12 Output - 0
Summary:
Total ops (MAC): 1175424(1.17M)
Prediction time :0us
Total memory:18632
Total Memory cost (Network and NNoM): 18632
rr@@II
''{{[[ ]]@@ww
{{@@LL !!WW##``
``**OO CChh``
,,@@++ ""##&&^^
~~@@'' !!@@XX
``oodd.. ^^pp%%
__%%~~ }}@@&&
ZZMM ..pp88__
{{@@}} ??@@qq
11@@ll ..{{##oo::
11@@cc[[>>}}pp@@@@00
!!&&@@@@BBWWOOqq@@]]
<<XXXX11::..ZZCC
zz@@ff
,,oo##""
ww@@xx
[[@@00..
``&&WWll
{{WW>>
Truth label: 4
Predicted label: 4
Probability: 100%