0


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


一 系统分析

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 <stdio.h>
  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 <HLS/hls.h>
  2. #include <stdio.h>
  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 <HLS/hls.h>
  2. #include <stdio.h>
  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 <HLS/hls.h>
  2. #include <HLS/stdio.h>
  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 <stdio.h>
  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 <stdio.h>
  23. #include <unistd.h>
  24. #include <fcntl.h>
  25. #include <sys/mman.h>
  26. #include <stdlib.h>
  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的各种接口案例实现

全连接神经网络


本文转载自: https://blog.csdn.net/qq_52445967/article/details/126364115
版权归原作者 藏进小黑屋 所有, 如有侵权,请联系我们删除。

“【FPGA】基于HLS的全连接神经网络手写体识别”的评论:

还没有评论