一个开源的FPGA神经网络加速器。
软件环境: HLS:2020.1 Vivado+SDK: 2018.3/2019.1 硬件环境: 正点原子ZYNQ-7020 xc7z020clg400-2
PS:打包IP Core时如遇ERROR: [IMPL 213-28] Failed to generate IP,则尝试将Windows系统时间更改为2021年,即可解决问题。
IP Core BUG提示!!!,此BUG将会导致Network运行结果错误
点我查看BUG及解决方案-1(无需编译综合,直接使用我验证后的IP Core替换原有工程IP Core)
点我查看BUG及解决方案-2(需要自己编译综合,从HLS源码级别重新导出IP Core并替换原有工程IP Core)
PS: 以下为针对Network的demo工程,若要测试单个算子PE,可仿照下述自行构建。
cpp: C/C++的算子/网络
hls: 针对HLS优化过的C/C++的算子/网络
img: 图片资源
neural network: 论文中的实现手写数字识别的网络(Tensorflow h5文件)
paper: 论文
sdk: vivado sdk 裸机代码(/local, sdk) + tcp/ip通信代码(/tcpip, python+sdk)
OpenNNA(Opensource Neural Network Accelerator)计划使用Xilinx ZYNQ 7020平台实现通用神经网络加速器。实现对神经网络的计算加速。
详细MobileNET网络结构见OpenNNA-Mobilenet网络结构.xlsx文件。
大牛说:卷积运算是卷积神经最关键的计算方法
一句话概述卷积运算:一个对输入矩阵和卷积核矩阵求内积的运算。
矩阵内积运算:两个矩阵对应位置元素相乘后后的所有结果相加。
一些计算例子:
对于卷积层来说,输入矩阵通常为图像矩阵(灰度图像为单通道矩阵,RGB图像为三通道矩阵)。通常情况下,输入图像矩阵的大小远大于卷积核矩阵大小(卷积核矩阵大小一般为3*3,且通常为奇数[当为奇数时,卷积核会有一个中心对称像素,在图像处理中有一个特殊点是非常好的])。所以要对一个输入图像矩阵进行卷积运算时,需要运用滑动窗口的方式进行计算。 引入滑动窗口方式进行对输入矩阵的卷积运算后,会再引入一个变量——步长(Stride)。Stride为进行完一次卷积运算后,滑动窗口的移动距离,单位为1个像素(Pix)。 具体的滑动过程如下图所示:
不同Stride下滑动过程如下图所示:
随着我们对上述卷积过程的理解,我们发现卷积运算是一个降采样运算。为什么这么说呢?假设一个输入图像矩阵大小为8* 8,经过一个3* 3卷积核的卷积运算后(Stride=1),输出结果矩阵大小为6* 6(8-3+1)。结果矩阵大小比图像矩阵小,假设进行n次卷积后,图像矩阵将会越来越小。这并不是我们所希望的; 同时,我们发现位于图像四周(边缘)的像素参与卷积卷积运算的次数非常少,而其他地方的像素多次参与卷积计算。这样导致丢失了边界上许多信息。 为了解决如上两个问题,我们引入了卷积层的另外一个特性填充(Padding)。 填充,顾名思义就是在输入矩阵外围添加一圈新的矩阵元素(添加的矩阵元素可为任意值,一般为1或0)。 填充的过程如下:
填充只有一个目标,保留图像边缘像素信息,所以在tensorflow等框架上可以便捷的设置填充目标{vaild(无填充)/same(填充至输出图像与输入图像大小相同)}。 对于上面我们举的例子来说: 输入图像矩阵大小为8* 8,经过一个3* 3卷积核的卷积运算后(Stride=1),输出结果矩阵大小为6* 6(8-3+1)。 假如我们想让输出结果矩阵大小也为8* 8的话,我们需要在行和列上分别补充两个像素点(8-6=2,Padding=1,对称补充)。
提炼图像特征信息
深度可分离卷积由逐通道卷积(Channelwise Convolution)和逐点卷积(Pointwise Convolution)构成,Depthwise Convolution的一个卷积核负责一个通道,一个通道只被一个卷积核卷积,然后生成一个特征图(Feature Map)。在卷积核为3 * 3大小下使用深度可分离卷积能够使网络参数量下降为原来的1/8至1/9。
为什么参数量会减少? 对于常规卷积来说,特征的提取和融合是放在一起的。每个卷积核都需要对图像的所有通道(如果是RGB的话,就是三个通道,卷积核也需要有三个通道)进行矩阵内积计算。 而对于深度可分离卷积来说,特征的提取和融合是分开的(即逐通道卷积实现特征提取,逐点卷积实现特征融合)。在特征提取过程(也就是逐通道卷积过程)中,一个卷积核只负责提取一个通道的信息,卷积核只有一个通道。在特征融合过程中,常规卷积需要输出n个特征图则需要引入n个3 * 3 *3 的卷积核;而对于逐点卷积融合来说,需要输出n个特征图需要引入n个1 * 1 *3的卷积核。无论从参数量还是计算量来说,深度可分离卷积都优于常规卷积。
对于逐通道卷积来说,一张5×5像素、三通道彩色输入图片(shape为5×5×3),Depthwise Convolution首先经过第一次卷积运算,DW完全是在二维平面内进行。卷积核的数量与上一层的通道数相同(通道和卷积核一一对应)。所以一个三通道的图像经过运算后生成了3个Feature map(如果有same padding则尺寸与输入层相同为5×5),如下图所示。
其中一个Filter只包含一个大小为3×3的Kernel,卷积部分的参数个数计算如下: N_depthwise = 3 × 3 × 3 = 27
Depthwise Convolution完成后的Feature map数量与输入层的通道数相同,无法扩展Feature map。而且这种运算对输入层的每个通道独立进行卷积运算,没有有效的利用不同通道在相同空间位置上的feature信息。因此需要逐点卷积(Pointwise Convolution,也就是1 * 1 * Chanel的卷积核)来将这些Feature map进行组合生成新的Feature map。
在神经网络训练中,训练集的数据分布会影响训练的结果。如果训练集的数值太大或太小会让激活函数处于饱和阶段,无法正确理解训练数据的特征。比如某个神经元X的值为1, 某个Weights的初始值为0.1, 这样后一层神经元计算结果就是Wx = 1 * 0.1 = 0.1; 又或者当x=20, 这样Wx的结果就为2。当我们加上一层激励函数, 激活这个Wx值的时候, 问题就来了. 如果使用 像tanh的激励函数,Wx的激活值就变成了 0.1 和 1, 接近于 1 的部已经处在了 激励函数的饱和阶段, 也就是如果X无论再怎么扩大, tanh 激励函数输出值也还是 接近1. 换句话说, 神经网络在初始阶段已经不对那些比较大的X特征范围敏感了. 这样很糟糕, 想象我轻轻拍自己的感觉和重重打自己的感觉居然没什么差别, 这就证明我的感官系统失效了. 当然我们是可以用之前提到的对数据做Normalization预处理, 使得输入的X变化范围不会太大, 让输入值经过激励函数的敏感部分. 但刚刚这个不敏感问题不仅仅发生在神经网络的输入层, 而且在隐藏层中也经常会发生.
函数公式 f(x) = max(0,x)
概率有两个性质:1)预测的概率为非负数;2)各种预测结果概率之和等于1。 softmax就是将在负无穷到正无穷上的预测结果按照这两步转换为概率的。
1)将预测结果转化为非负数 下图为y=exp(x)的图像,我们可以知道指数函数的值域取值范围是零到正无穷。softmax第一步就是将模型的预测结果转化到指数函数上,这样保证了概率的非负性。 .jpg)
2)各种预测结果概率之和等于1 为了确保各个预测结果的概率之和等于1。我们只需要将转换后的结果进行归一化处理。方法就是将转化后的结果除以所有转化后结果之和,可以理解为转化后结果占总数的百分比。这样就得到近似的概率。
下面为大家举一个例子,假如模型对一个三分类问题的预测结果为-3、1.5、2.7。我们要用softmax将模型结果转为概率。步骤如下:
1)将预测结果转化为非负数
y1 = exp(x1) = exp(-3) = 0.05 y2 = exp(x2) = exp(1.5) = 4.48 y3 = exp(x3) = exp(2.7) = 14.88
2)各种预测结果概率之和等于1
z1 = y1/(y1+y2+y3) = 0.05/(0.05+4.48+14.88) = 0.0026 z2 = y2/(y1+y2+y3) = 4.48/(0.05+4.48+14.88) = 0.2308 z3 = y3/(y1+y2+y3) = 14.88/(0.05+4.48+14.88) = 0.7666
总结一下softmax如何将多分类输出转换为概率,可以分为两步: 1)通过指数函数,将实数输出映射到零到正无穷。 2)将所有结果相加,进行归一化。
Global Average Pooling 2D就是依次将将输入矩阵的每个通道的矩阵元素相加再求平均,每个通道输出一个平均值。
import tensorflow as tf
x = tf.random.normal((1,2,2,3))
x.shape
# TensorShape([1, 2, 2, 3])
x
#
ax = tf.keras.layers.GlobalAveragePooling2D()(x)
ax.shape
# TensorShape([1, 3])
ax
#
标定权重下载链接:https://github.com/fchollet/deep-learning-models/releases Tflite量化参考链接:https://www.tensorflow.org/lite/performance/post_training_quantization Tensorflow INT8量化规范:https://www.tensorflow.org/lite/performance/quantization_spec (Netron开源链接:https://github.com/lutzroeder/netron) 量化后模型性能评估工具:https://github.com/tensorflow/tensorflow/tree/master/tensorflow/lite/tools/evaluation/tasks 面向ImageNet数据集的评估工具:https://github.com/tensorflow/tensorflow/tree/master/tensorflow/lite/tools/evaluation/tasks/imagenet_image_classification#image-classification-evaluation-based-on-ilsvrc-2012-task ImageNet数据集:https://image-net.org/challenges/LSVRC/2012/2012-downloads.php ImageNet2012数据集说明:https://www.tensorflow.org/datasets/catalog/imagenet2012 基准MobileNETV1 TFLite模型:https://www.tensorflow.org/lite/guide/hosted_models bazel安装说明:https://docs.bazel.build/versions/main/install-ubuntu.html
本次标定测试权重采用基于ImageNET数据训练的1000分类权重(MobileNet 224*224 α=1.0 top)进行测试,由于本项目主要研究重点为FPGA内部卷积神经网络加速器结构设计,针对于网络模型的优化,剪枝并不是重点。为了极大程度上的展现所设计项目的性能,我们优先基于ImageNET数据集实现1000分类网络(大部分的论文以在ImageNET数据集上的性能来展示其论文的卓越)。
关于本次所使用ILSVRC 2012数据集的信息如下:
There are a total of 50,000 validation images. They are named as ILSVRC2012_val_00000001.JPEG ILSVRC2012_val_00000002.JPEG ... ILSVRC2012_val_00049999.JPEG ILSVRC2012_val_00050000.JPEG There are 50 validation images for each synset. The ground truth of the validation images is in data/ILSVRC2012_validation_ground_truth.txt, where each line contains one ILSVRC2012_ID for one image, in the ascending alphabetical order of the image file names.
使用tensorflow tflite转换工具将权重精度为float32模型强制转换为UINT8精度(量化)的TFlite模型。为了保证量化的精度,我们应该在量化过程中提供含有400-500张数据集图片的量化参考集。
对于嵌入式系统和本项目中的FPGA来说,他们有如下特性:1.计算性能较弱 2.有限的RAM空间。如果神经网络权重精度为Float32,则会消耗较大的计算性能,降低网络推理速度。同时大量的Float32精度权重对于内存是及不友好的(Float32精度权重将比INT8权重更占用内存空间)。对于推理速度来说,虽然使用INT8权重进行推理,可能会降低神经网络的准确率,但是能够获得较高的推理速度。
为了方便观察神经网络结构和神经网络权重,使用开源工具Netron进行对网络参数的可视化查看。在经过量化后,我们们使用Netron开源工具进行对量化结果的查看如下图,可以看到我们模型的权重已被量化为INT8,模型体积下降为原来的1/4。
在观察量化过程时,我们发现Batch Normilation批标准化操作和Bias(偏置)是等价的,在进行对TFLite文件量化到UINT8精度的过程中,会将Batch Normilation转换成加法偏置操作。
评估tflite模型性能操作需要Bazel 3.7.2
cd "/home/xdd/.bazel/bin" && curl -fLO https://releases.bazel.build/3.7.2/release/bazel-3.7.2-linux-x86_64 && chmod +x bazel-3.7.2-linux-x86_64
我们采用ILSVRC2012验证集(1000类,50,000张图片)对量化后的TFLite模型进行性能评估,性能评估的结果如下: MobileNET V1 UINT8 i5-1135G7 @ 2.40GHz × 1
MobileNET V2 Float32 i5-1135G7 @ 2.40GHz × 1
MobileNET V2 UINT8 i5-1135G7 @ 2.40GHz × 1
PYNQ DMA Libary:https://pynq.readthedocs.io/en/v2.6.1/pynq_libraries/dma.html#pynq-libraries-dma AXI DMA:https://www.xilinx.com/support/documentation/ip_documentation/axi_dma/v7_1/pg021_axi_dma.pdf
以上为本设备PE单元结构图,在位于ZYNQ PL端的Neural Network Accelerator中将会实现以上结构。其中Process Element为处理核心。根据最后FPGA资源的情况,将会分布多个PE。PE的一些基本参数(开启/关闭)通过AXI Lite总线被Dual Core Arm-A9控制。同时PE运行后的结果通过AXI DMA BUS或AXI Stream高速传递至Dual Core Arm-A9。
受限于ZYNQ 7020 内部BRAM大小(0.6125MB)远小于MobileNET网络参数量(4.3MB)。所以我们放弃了将权重完全存储到BRAM中的想法。由于PL端外挂1GB DDR3内存,所以我们采用ZYNQ 7020内部高性能AXI总线 DMA(直接内存访问)方案,力争将权重访问与计算结果存储造成的时间延迟设计至最低水平。 (以上方案被放弃) 众所周知,ZYNQ FPGA端BRAM只有0.625MB大小,如果按照以上方案,不把网络放在BRAM中,而是放在1GB DDR3内存中,经过最近的研究将会引起如下问题:
由于以上问题,我思考了第二个解决办法:如果想把MobileNet放入FPGA中。MobileNET的网络深度因子必须等于0.25(当网络深度因子等于0.25时,网络权重大小差不多为0.5MB左右)。但是这个办法也失败了。经过Vivado HLS的评估,此时一层模型的效率为:
一层Conv运行竟然需要35ms,且已经占用了非常庞大的FPGA硬件资源。由于MobileNET有26层,如果将上述资源占用*26,即便考虑硬件复用,也是行不通的。
在调试的过程中,我还发现Vivado HLS的 C++ Debugger并不高效。于是我转向了使用Visual Studio来调试我的C++代码。在调试过程中,我越来越觉得,之前选择的网络实在是太大了。无
No open issues yet, or sync has not completed.