• 基于HLS的全连接神经网络手写体识别


    目录

    一 系统分析

    1.1 全连接神经网络简介

     二 通过HLS 编写全连接神经网络传入权重参数和偏置参数文件

    2.1  获得图片、权重以及偏置的参数

    2.2 编写C语言的全连接算子

    2.3 Slave Interfaces

    2.3.1 hls_avalon_slave_component

     2.3.2 hls_avalon_slave_register_argument

    2.3.3  slave_memory_argument

    三 输入图片进行测试并生成IP

    3.1 编译、测试

    3.1.1 初始化环境

    3.1.2 编译

    3.2 添加IP进Quartus并添加到SOC工程中生成硬件

    3.2.1 将IP文件夹复制到黄金工程的IP文件夹下

     3.2.2 打开黄金工程

    四 更新SD卡

    4.1 生成设备树

    4.2 生成rbf文件

    4.3 更新头文件

    ​编辑

    五 设计软件

     5.1 新建C工程

     5.2 代码设计

    六 调试

    七 参考链接



    一 系统分析

    1、手写体输入为28x28的黑白图片,所以输入为784个
    2、输出为识别0-9的数字的概率,所以有10个输出
    3、输入只能是-1~1的小数,主要是防止计算溢出

    1.1 全连接神经网络简介

    全连接神经网络模型是一种多层感知机(MLP),感知机的原理是寻找类别间最合理、最具有鲁棒性的超平面,最具代表的感知机是SVM支持向量机算法。神经网络同时借鉴了感知机和仿生学,通常来说,动物神经接受一个信号后会发送各个神经元,各个神经元接受输入后根据自身判断,激活产生输出信号后汇总从而实现对信息源实现识别、分类,一个典型的神经网络如下图所示:

     上图是典型的全连接神经网络模型(DNN),有的场合也称作深度神经网络,与传统的感知机不同,每个结点和下一层所有结点都有运算关系,这就是名称中‘全连接’的含义,上图的中间层也成为隐藏层,全连接神经网络通常有多个隐藏层,增加隐藏层可以更好分离数据的特征,但过多的隐藏层也会增加训练时间以及产生过拟合。
     

    观察上图,输入数据是一个3维向量,隐藏层有5个结点,意味着通过线性映射将3维向量映射为一个5维向量,最后再变为一个2维向量输出。当原输入数据是线性不可分时,全连接神经网络是通过激活函数产生出非线性输出,常见的激活函数有Sigmoid,Tanh,Relu,分别如下图所示:

     

     全连接神经网络训练分为前向传播、后向传播两个过程,前向传播数据沿输入到输出后计算损失函数值,后向传播则是一个优化过程,利用梯度下降法减小前向传播产生的损失函数值,从而优化、更新参数。

     简言之:

    输入层输入数据,在经过中间隐藏层计算,最后通过右边输出层输出数据

     本次项目做的手写体识别就是基于全连接神经网络来实现的

     二 通过HLS 编写全连接神经网络传入权重参数和偏置参数文件

    2.1  获得图片、权重以及偏置的参数

    python+tensorflow对mnist数据集的神经网络训练和推理 加参数提取

    参数提取

    2.2 编写C语言的全连接算子

    头文件导入 :

    1. #include
    2. #include "HLS/hls.h"
    3. #include "input_0.h"//十幅图片
    4. #include "input_1.h"
    5. #include "input_2.h"
    6. #include "input_3.h"
    7. #include "input_4.h"
    8. #include "input_5.h"
    9. #include "input_6.h"
    10. #include "input_7.h"
    11. #include "input_8.h"
    12. #include "input_9.h"
    13. #include "layer1_bias.h" //第一层偏置常数
    14. #include "layer1_weight.h" //第一层权重
    15. #include "layer2_bias.h" //第二层偏置常数
    16. #include "layer2_weight.h" //第二层权重值

    将十幅图像导入,并且将权重和偏置参数头文件加入进去

    2.3 Slave Interfaces

    Intel HLS Compiler提供了两种不同类型的从接口,您可以在组件中使用它们。一般来说,较小的标量输入应该使用从寄存器。如果您打算将大数组复制到组件中或从组件中复制出来,那么应该使用从属内存。

     

    2.3.1 hls_avalon_slave_component

    1. #include
    2. #include
    3. hls_avalon_slave_component
    4. component int dut(int a,int b)
    5. {
    6. return a*b;
    7. }
    8. int main()
    9. {
    10. int a=2;
    11. int b=3;
    12. int y;
    13. y = dut(a,b);
    14. printf("y=%d",y);
    15. return 0;
    16. }

     2.3.2 hls_avalon_slave_register_argument

    1. #include
    2. #include
    3. hls_avalon_slave_component
    4. component int dut(
    5. int a,
    6. hls_avalon_slave_register_argument int b)
    7. {
    8. return a*b;
    9. }
    10. int main()
    11. {
    12. int a=2;
    13. int b=3;
    14. int y;
    15. y = dut(a,b);
    16. printf("y=%d",y);
    17. return 0;
    18. }

     可见 b 变成了寄存器

    2.3.3  slave_memory_argument

    1. #include
    2. #include
    3. hls_avalon_slave_component
    4. component int dut(
    5. hls_avalon_slave_memory_argument(5*sizeof(int)) int *a,
    6. hls_avalon_slave_memory_argument(5*sizeof(int)) int *b
    7. )
    8. {
    9. int i;
    10. int sum=0;
    11. for(i=0;i<5;i++)
    12. {
    13. sum = sum + a[i] * b[i];
    14. //printf("a[%d]%d",i,a[i]);
    15. }
    16. return sum;
    17. }
    18. int main()
    19. {
    20. int a[5] = {1,2,3,4,5};
    21. int b[5] = {1,2,3,4,5};
    22. int sum;
    23. sum = dut(a,b);
    24. printf("sum=%d",sum);
    25. return 0;
    26. }

    这样子 a、b都变成了存储器类型

     本次实验就是使用HLS将输入图片、权重、偏置生成为从存储器类型的电路元件,方便后续在软件端将数据存入从存储器中并调用。

    全连接代码:

    1. #include
    2. #include "HLS/hls.h"
    3. #include "input_0.h"//十幅图片
    4. #include "input_1.h"
    5. #include "input_2.h"
    6. #include "input_3.h"
    7. #include "input_4.h"
    8. #include "input_5.h"
    9. #include "input_6.h"
    10. #include "input_7.h"
    11. #include "input_8.h"
    12. #include "input_9.h"
    13. #include "layer1_bias.h" //第一层偏置常数
    14. #include "layer1_weight.h" //第一层权重
    15. #include "layer2_bias.h" //第二层偏置常数
    16. #include "layer2_weight.h" //第二层权重值
    17. hls_avalon_slave_component component
    18. int my_predit(
    19. hls_avalon_slave_memory_argument(784*sizeof(float)) float *img,
    20. hls_avalon_slave_memory_argument(64*sizeof(float)) float *b1,
    21. hls_avalon_slave_memory_argument(784*64*sizeof(float)) float *w1,
    22. hls_avalon_slave_memory_argument(10*sizeof(float)) float *b2,
    23. hls_avalon_slave_memory_argument(64*10*sizeof(float)) float *w2){
    24. float res1[64]={0},res2[10]={0}; //创建两个浮点数数组 yongyu
    25. //循环1
    26. /* w1权重在 layer1_weight.h 中按照一行64个,784列顺序排列,
    27. 但实际上是一维数组,我们计算第一层64个神经元的输出*/
    28. for (int i = 0; i < 64; i++)
    29. {
    30. for (int j = 0; j < 784; j++)
    31. {
    32. res1[i] = res1[i]+ img[j] * w1[i+j*64]; //w1x1+w2x2 ... wnxn+b
    33. }
    34. res1[i] +=b1[i]; //得到第一层的输出
    35. //printf("%f \n",res1[i]);
    36. }
    37. //循环2
    38. for (int i = 0; i < 10; i++)
    39. {
    40. for (int j = 0; j < 64; j++)
    41. {
    42. res2[i] = res2[i]+ res1[j] * w2[i+j*10]; //输入第一层的输出
    43. }
    44. res2[i] +=b2[i];
    45. //printf("%f \n",res2[i]);
    46. }
    47. //输出
    48. float temp = 0; //用一个中间值来寄存特征值最大值
    49. int res3;
    50. for (int i = 0; i < 10; i++)
    51. {
    52. //printf("%f \n",res2[i]);
    53. if (res2[i] > temp) //比较10个特征值,找出最大值
    54. {
    55. temp = res2[i];
    56. res3 = i; //res3的值即为输出层数组中特征值最大值对应的下标 ,也是我们想要的结果
    57. }
    58. }
    59. return res3; //最后返回i,即是我们的预测结果
    60. }
    61. int main()
    62. {
    63. //用指针数组来表示10幅图片
    64. float *a[10] = {input_0,input_1,input_2,input_3,input_4,input_5,input_6,input_7,input_8,input_9};
    65. for (int i = 0; i < 10; i++) //循环输出训练结果
    66. {
    67. int res = my_predit(a[i],layer1_bias,layer1_weight,layer2_bias,layer2_weight);//调用函数输出返回值
    68. printf("input_%d.h预测结果为:%d\n",i,res);
    69. }
    70. return 0;
    71. }

    三 输入图片进行测试并生成IP

    main函数的作用仅仅是测试用的,并没有实际的意义,目的就是将十幅图像的像素输入,得到返回结果并输出。

    3.1 编译、测试

    3.1.1 初始化环境

    就是进入到你Quartus安装目录下的HLS路径下,用终端运行后,初始化hls环境。

    下面以我的安装目录为例,作为示范:

    1 :先找到路径

     2 : 敲cmd 回车

     3 :输入初始化命令

     

     初始化完成。

    3.1.2 编译

    终端先不关闭,还要进行编译工作

    回到你代码编写的路径下

    在 x86-64平台上编译:

     -v : 作用是显示信息

    -0 full :生成名为 full 的可执行文件

    运行结果:

     在FPGA平台上编译测试:

     

     生成IP文件夹

    到这里神经网络IP制作完成。

    3.2 添加IP进Quartus并添加到SOC工程中生成硬件

    3.2.1 将IP文件夹复制到黄金工程的IP文件夹下

     3.2.2 打开黄金工程

    1. 打开platform designer

    2 添加神经网络IP到工程并连线

     

     将 Avalon Memory Mapped Slave接口的 权重、偏置、图片、控制状态存储器连接到 mm_bridgeavalon Memory Mapped Masterm0上 ,时钟和复位都连到mm_bridge上,irq连接到 f2h_irq0.

    3. 然后分配基地址

    4. generate

    一般会编译十几分钟,慢慢等吧。

    5. 全编译

    这一步会更久,半小时加,可以直接去设计软件端。

    编译完后会生成sof文件

    四 更新SD卡

    4.1 生成设备树

    打开EDS工具,是Intel专门为SOC FPGA开发设计的一款工具,类似于终端。里面包含了很多工具。

    进入到黄金工程目录后,

    更新设备树文件:

    make dtb    

    生成设备树文件

     

    4.2 生成rbf文件

    进入黄金工程目录下的output_files目录下,双击sof_to_rbf.bat

     

     二进制文件更新完毕。

    4.3 更新头文件

    ./generate_hps_qsys_header.sh

     

     将更新的后的二进制文件和设备树文件更换SD卡中的文件。

    五 设计软件

     5.1 新建C工程

    创建完项目,再创建c程序,

    添加库文件路径:

     路径是根据自己安装目录下去寻找。

    编写源代码,添加权重、偏置、测试图片文件

    将全连接生成的权重、偏置、测试图片的头文件以及hps_0.h复制到工程中。

     5.2 代码设计

    1. /*
    2. * full.c
    3. *
    4. * Created on: 2022年7月27日
    5. * Author: 药石无医
    6. */
    7. #include "layer1_bias.h"
    8. #include "layer1_weight.h"
    9. #include "layer2_bias.h"
    10. #include "layer2_weight.h"
    11. #include "input_0.h"//十幅图片
    12. #include "input_1.h"
    13. #include "input_2.h"
    14. #include "input_3.h"
    15. #include "input_4.h"
    16. #include "input_5.h"
    17. #include "input_6.h"
    18. #include "input_7.h"
    19. #include "input_8.h"
    20. #include "input_9.h"
    21. //gcc标准头文件
    22. #include
    23. #include
    24. #include
    25. #include
    26. #include
    27. //HPS厂家提供的底层定义头文件
    28. #define soc_cv_av //开发平台Cyclone V 系列
    29. #include "hwlib.h"
    30. #include "socal/socal.h"
    31. #include "socal/hps.h"
    32. //与用户具体的HPS 应用系统相关的硬件描述头文件
    33. #include "hps_0.h"
    34. #define HW_REGS_BASE (ALT_STM_OFST) //HPS外设地址段基地址
    35. #define HW_REGS_SPAN (0x04000000) //HPS外设地址段地址空间 64MB大小
    36. #define HW_REGS_MASK (HW_REGS_SPAN - 1) //HPS外设地址段地址掩码
    37. static volatile unsigned long long *dout = NULL;
    38. static float *img_virtual_base = NULL;
    39. static float *b1_virtual_base = NULL;
    40. static float *b2_virtual_base = NULL;
    41. static float *w1_virtual_base = NULL;
    42. static float *w2_virtual_base = NULL;
    43. int full_init(int *virtual_base){
    44. int fd;
    45. void *virtual_space;
    46. //使能mmu
    47. if((fd = open("/dev/mem",(O_RDWR | O_SYNC))) == -1){
    48. printf("can't open the file");
    49. return fd;
    50. }
    51. //映射用户空间
    52. virtual_space = mmap(NULL,HW_REGS_SPAN,(PROT_READ | PROT_WRITE),MAP_SHARED,fd,HW_REGS_BASE);
    53. //得到偏移的外设地址
    54. dout = virtual_space + ((unsigned)(ALT_LWFPGASLVS_OFST+PREDIT_0_MY_PREDIT_INTERNAL_INST_AVS_CRA_BASE)
    55. &(unsigned)(HW_REGS_MASK));
    56. b1_virtual_base = virtual_space + ((unsigned)(ALT_LWFPGASLVS_OFST+PREDIT_0_MY_PREDIT_INTERNAL_INST_AVS_B1_BASE)
    57. &(unsigned)(HW_REGS_MASK));
    58. b2_virtual_base = virtual_space + ((unsigned)(ALT_LWFPGASLVS_OFST+PREDIT_0_MY_PREDIT_INTERNAL_INST_AVS_B2_BASE)
    59. &(unsigned)(HW_REGS_MASK));
    60. w1_virtual_base = virtual_space + ((unsigned)(ALT_LWFPGASLVS_OFST+PREDIT_0_MY_PREDIT_INTERNAL_INST_AVS_W1_BASE)
    61. &(unsigned)(HW_REGS_MASK));
    62. w2_virtual_base = virtual_space + ((unsigned)(ALT_LWFPGASLVS_OFST+PREDIT_0_MY_PREDIT_INTERNAL_INST_AVS_W2_BASE)
    63. &(unsigned)(HW_REGS_MASK));
    64. img_virtual_base = virtual_space + ((unsigned)(ALT_LWFPGASLVS_OFST+PREDIT_0_MY_PREDIT_INTERNAL_INST_AVS_IMG_BASE)
    65. &(unsigned)(HW_REGS_MASK));
    66. *virtual_base = virtual_space;
    67. return fd ;
    68. }
    69. int main(){
    70. int fd,virtual_base,i;
    71. fd = full_init(&virtual_base);
    72. float *image[10] = {input_0,input_1,input_2,input_3,input_4,input_5,input_6,input_7,input_8,input_9};
    73. //先将权重和偏置赋值
    74. memcpy(w1_virtual_base,layer1_weight,784*64*sizeof(float));
    75. memcpy(b1_virtual_base,layer1_bias,64*sizeof(float));
    76. memcpy(w2_virtual_base,layer2_weight,64*10*sizeof(float));
    77. memcpy(b2_virtual_base,layer2_bias,10*sizeof(float));
    78. //一层for循环输出十张图片的值
    79. for(i=0;i<10;i++)
    80. {
    81. memcpy(img_virtual_base,image[i],784*sizeof(float));
    82. while((*(dout + 0)&(unsigned)1) != 0);
    83. *(dout + 2) = 1;
    84. *(dout + 3) = 1;
    85. *(dout + 1) = 1;
    86. while((*(dout + 3) & 0x2) == 0 );
    87. printf("input:%d 预测结果:%d \n",i,*(dout + 4));
    88. *(dout + 1) = 0;
    89. }
    90. //取消映射
    91. //取消地址映射
    92. if(munmap(virtual_base,HW_REGS_SPAN)==-1){
    93. printf("取消映射失败..\n");
    94. close(fd);
    95. }
    96. //关闭mmu
    97. close(fd);
    98. return 0;
    99. }

    保存之后编译生成二进制可执行文件。

    六 调试

    上板验证,这一步就偷个懒,就是连接开发板和电脑,将可执行文件复制到

    /opt 目录下

    给可执行文件赋予权限

    chmod 777 full

    之后就可以运行了。

    最终可实现对28 *28 的手写体图片的识别。并显示出结果

    七 参考链接

    HLS的各种接口案例实现

     全连接神经网络

  • 相关阅读:
    【C++】C++入门(上)--命名空间 输入输出 缺省参数 函数重载
    多线程_线程状态
    c#string常用方法总结
    springboot 使用RocketMQ客户端生产消费消息DEMO
    onnx_graphsurgeon修改onnx计算图
    kubeadm升级k8s
    Vue3中的Ref与Reactive:深入理解响应式编程
    基于单片机开发的酒精浓度测试仪方案
    k8s相关命令-命名空间
    Docker Desktop 可以直接启用Kubernetes 1.25 了
  • 原文地址:https://blog.csdn.net/qq_52445967/article/details/126364115