基于FPGA的Yolov4 tiny目标检测网络加速器

介绍

之前实现了基于FPGA的Winograd CNN加速器(VGG16)和基于FPGA的MobileNet v2加速器,但这两个算法在本质上区别不大:一个是VGG16,另一个是轻量级的MobileNet v2,所实现的功能都是图像分类。因此,为了尝试更多的应用,本文在FPGA上实现了一个目标检测网络—-Yolov4 tiny。yolo4 tiny的结构是YOLOv4的精简版,属于轻量化模型,参数只有600万相当于原来的十分之一,这类网络不仅能实现对图像的分类任务,还可以找出目标的位置,因此,更加贴近实际应用中的需求,如行人检测、口罩检测等。

网络结构

这部分的内容在博主的另一篇文章中有详细介绍,这里不再赘述。

FPGA IP核的设计

根据对网络结构的分析,我们发现,yolo4 tiny主要由以下几个计算组成:
1.3×3标准卷积
2.1x1point-wise卷积
3.上采样、下采样(2×2最大池化)
4.concat操作
我们将上述计算分成两类,第一类是需要重点加速的,如3×3标准卷积和1x1point-wise卷积,它们占网络总体计算量的95%以上,因此是我们需要重点关注的地方。第二类是一些轻量级的运算,如上采样、下采样和concat操作,其中concat可以通过设置起始地址偏移的方式实现,因此不予以考虑,而其他两个运算,计算量相对于整个网络来说,也是微乎其微的,因此简单的在FPGA上实现即可。

3×3标准卷积设计

关于这部分的设计,我们主要参考了论文。

获取部分

由于FPGA片上存储资源(BRAM)十分有限,而卷积神经网络的权重以及中间计算结果特征图都具有较大的存储占用,因此,缓存整幅特征图显然是不现实的,因此,我们采取了循环分块策略。
详情如下:
%E5%81%87%E8%AE%BE%E8%BE%93%E5%85%A5%E7%89%B9%E5%BE%81%E5%9B%BEI%E4%B8%BAN%5Ctimes%20H_%7Bin%7D%5Ctimes%20W_%7Bin%7D%2C%E6%9D%83%E9%87%8D%E4%B8%BAM%5Ctimes%20N%5Ctimes%20K%5Ctimes%20K%28K%3D3%29%EF%BC%8C%E8%BE%93%E5%87%BA%E7%89%B9%E5%BE%81%E5%9B%BEO%E4%B8%BAM%5Ctimes%20H%5Ctimes%20W%2C令输入通道、输出通道、输出特征图高、宽的分块因子分别为T_n%2CT_m%2CT_r%2CT_c,则每次计算的时候,我们加载T_n%5Ctimes%20%28T_r%2BK-1%29%5Ctimes%20%28T_c%2BK-1%29大小的输入特征图块,T_m%5Ctimes%20T_n%20%5Ctimes%20K%20%5Ctimes%20K大小的权重,然后进行卷积计算,得到T_m%5Ctimes%20T_r%5Ctimes%20T_c大小的输出特征块(部分和或者最终结果),本次计算完成后,我们再读取下一块输入特征和权重,再进行计算。如下图所示(下图中load output feature maps可以省去,部分和一直存储在片上即可)
基于FPGA的Yolov4 tiny目标检测网络加速器

计算部分

计算部分就是一个标准的3×3卷积,而卷积运算的并行度共有4个维度,分别是
1.输入通道并行。即同时计算多个输入通道的特征图和权重的卷积。
2.输出通道并行。即同时计算多个输出特征图的结果或部分和。
3.卷积窗口内并行。即卷积窗口内K%5Ctimes%20K个神经元和权重之间乘法的并行。
4.输出像素之间的并行。即同时计算输出特征图上多个神经元。
其中后两者的实现较为不易,例如3需要使用行缓冲,4的设计则更加复杂,因此,在本加速器的设计中,我们采用1,2这两种并行方式。假设并行度为T_n%2CT_m,则有
基于FPGA的Yolov4 tiny目标检测网络加速器
不难想象,它的硬件架构应该是并行乘法单元+加法树,如下图所示
基于FPGA的Yolov4 tiny目标检测网络加速器

总结

上面已经讨论了计算部分和内存访问部分的设计方法,所以可以得到整个加速过程,用伪代码表示,即

for(r=0;r<H;r+=Tr){
  for(c=0;c<W;c+=Tc){
    for(m=0;m<M;m+=Tm){
      for(n=0;n<N;n+=Tn){                    
            load I[n:n+Tn][r:r+Tr+K-1][c:c+Tc+K-1] to ifm_buff;
            load W[m:m+Tm][n:n+Tn][:][:] to wt_buff;    //片外访存
            //片上计算
            for(ii=0;ii<K;ii++)
                for(jj=0;jj<K;jj++)
                    for(rr=0;rr<Tr;rr++)
                        for(cc=0;cc<Tc;cc++)
                            for(mm=0;mm<Tm;mm++)
                                for(nn=0;nn<Tn;nn++)
                            		ofm_buff[mm][rr][cc]+=
                            		ifm_buff[nn][rr+ii-1][cc+jj-1]*wt_buff[mm][nn][ii][jj];
        }
        store ofm_buff to O[m:m+Tm][r:r+Tr][c:c+Tc]; //片外访存
    }
  }
}

另外,为了进一步提高系统的吞吐量,我们还做了一个乒乓操作来屏蔽数据的传输时间,如下图,这是一种以区域换速度的方法。
基于FPGA的Yolov4 tiny目标检测网络加速器

1×1 Point-Wise卷积设计

1×1卷积,实际上是一个矩阵乘法,因此设计相对比较简单,按照分块矩阵乘法进行设计即可。
分析:假设特征图为N%5Ctimes%20H%20%5Ctimes%20W,权重为M%5Ctimes%20N,则1×1卷积相当于矩阵W_%7BM%5Ctimes%20N%7D和矩阵I_%7BN%5Ctimes%20%28H%5Ctimes%20W%29%7D的乘积,因此,我们设置分块系数如下:
输出通道分块系数为T_m,输入通道分块系数为T_n,而二维特征图我们把它看成一维向量,其分块系数为T_p%28pixel%29,然后进行分块矩阵乘法IP核的设计。
同3×3标准卷积的设计一样,我们也进行乒乓操作。

采样

上采样

在yolo4-tiny中,上采样操作是nearest模式的,具体可参见博客
HLS实现在此处略去,因为对最终加速器的吞吐率影响不大,下同。

下采样

在yolo4-tiny中,就是2×2的最大池化层。

CPU端设计

我们的方法是,在block design中例化卷积和采样IP核,然后通过在PS端多次调用PL端的IP核,来对yolo4 tiny进行加速。
CPU端代码的编写,我们采用的类的方法

class BasicConv{
public:
    int h;
    int w;
    int s;
    int k;
    int p;
    int ch_in;
    int ch_out;
    data_t* weight;
    data_t* bias;
};

class Resblock_body{
public:
    int h;
    int w;
    int ch_in;
    int ch_out;
    BasicConv* conv1;
    BasicConv* conv2;
    BasicConv* conv3;
    BasicConv* conv4;
};

class CSPDarkNet{
public:
    BasicConv* basic_conv1;
    BasicConv* basic_conv2;
    BasicConv* basic_conv3;
    Resblock_body* resblock1;
    Resblock_body* resblock2;
    Resblock_body* resblock3;
};

如上面的代码所示,有BasicConv类,Resblock_body类和CSPDarkNet类,其中BasicConv即一个卷积+BN层+leakyrelu层,Resblock_body为yolov4 tiny中的一个重要结构,共4个,最后,BasicConv和Resblock_body组成了yolo4 tiny的backbone:CSPDarkNet。
CSPDarkNet和FPN以及yolo_head,共同组成了yolov4 tiny网络:

class Yolo4_Tiny{
public:
    CSPDarkNet* backbone;
    //conv_forP5
    BasicConv* conv_forP5_conv;
    //yolo_headP4
    BasicConv* yolo_headP4_basic_conv1;
    data_t* yolo_headP4_w2;
    data_t* yolo_headP4_b2;
    //yolo_headP5
    BasicConv* yolo_headP5_basic_conv1;
    data_t* yolo_headP5_w2;
    data_t* yolo_headP5_b2;
    //upsample
    BasicConv* upsample_conv;
    Yolo4_Tiny(){           //���캯��
        this->backbone=new CSPDarkNet();
        //conv_forP5
        this->conv_forP5_conv=new BasicConv(13,13,1,1,0,512,256);         //K=1
        //yolo_headP4
        this->yolo_headP4_basic_conv1=new BasicConv(26,26,3,1,1,384,256);
        this->yolo_headP4_w2=new data_t[75*256*1*1];                              //K=1
        this->yolo_headP4_b2=new data_t[75];
        //yolo_headP5
        this->yolo_headP5_basic_conv1=new BasicConv(13,13,3,1,1,256,512);
        this->yolo_headP5_w2=new data_t[75*512*1*1];                              //K=1
        this->yolo_headP5_b2=new data_t[75];
        //upsample
        this->upsample_conv=new BasicConv(13,13,1,1,0,256,128);           //K=1
    }
    void load_weight(string dir){
        this->backbone->load_weight(dir);
        //conv_forP5
        this->conv_forP5_conv->load_weight(dir+"\\conv_forP5");
        //yolo_headP4
        read_params(dir+"\\yolo_headP4\\w1.bin",this->yolo_headP4_basic_conv1->weight,256*384*3*3);
        read_params(dir+"\\yolo_headP4\\b1.bin",this->yolo_headP4_basic_conv1->bias,256);
        read_params(dir+"\\yolo_headP4\\w2.bin",this->yolo_headP4_w2,75*256*1*1);
        read_params(dir+"\\yolo_headP4\\b2.bin",this->yolo_headP4_b2,75);
        //yolo_headP5
        read_params(dir+"\\yolo_headP5\\w1.bin",this->yolo_headP5_basic_conv1->weight,512*256*9);
        read_params(dir+"\\yolo_headP5\\b1.bin",this->yolo_headP5_basic_conv1->bias,512);
        read_params(dir+"\\yolo_headP5\\w2.bin",this->yolo_headP5_w2,75*512*1*1);
        read_params(dir+"\\yolo_headP5\\b2.bin",this->yolo_headP5_b2,75);
        //upsample
        this->upsample_conv->load_weight(dir+"\\upsample");
    }
    //
    void forward(data_t* in,data_t* out0,data_t* out1){
    	static data_t feat1[256*26*26];
		static data_t feat2[512*13*13];
		static data_t P5[256*13*13];
		static data_t out0_tmp[512*13*13];
		static data_t P5_Upsample[128*13*13];
		static data_t P4[384*26*26];
		static data_t out1_tmp[256*26*26];
        //compute
        this->backbone->forward(in,P4+128*26*26,feat2);                 //相当于concat,P4=concat(Upsample_out,feat1)
        //cout<<"backbone end\n";
        //conv_forP5
        this->conv_forP5_conv->forward(feat2,P5);
        //out0
        this->yolo_headP5_basic_conv1->forward(P5,out0_tmp);
        conv_leakyrelu(512,75,0,1,1,13,13,out0_tmp,this->yolo_headP5_w2,this->yolo_headP5_b2,out0,0);
        //upsample_conv
        this->upsample_conv->forward(P5,P5_Upsample);
        //upsample
        sampling(P5_Upsample,P4,128,13,0);
        //out1
        this->yolo_headP4_basic_conv1->forward(P4,out1_tmp);
        conv_leakyrelu(256,75,0,1,1,26,26,out1_tmp,this->yolo_headP4_w2,this->yolo_headP4_b2,out1,0);
    }
};

模型训练和导出

由于算力的限制,模型并没有训练,而是直接使用训练好的权重(数据集为VOC),详细可参见链接
在送入加速器之前,我们进行了卷积-BN融合操作,从而节省了一部分的计算量,经过编写的python脚本融合后的权重按如下形式存储
基于FPGA的Yolov4 tiny目标检测网络加速器

结果显示

展示:本工程只加速了yolo4 tiny网络本身,并将yolo4 tiny网络的输出存储在SD卡上,后续的解码以及可视化则是在PC上完成的,下图是结果展示,可以看到,虽然进行了16bit定点数量化,但对最终目标检测的结果并无很大影响。
基于FPGA的Yolov4 tiny目标检测网络加速器
模型的性能: 模型部署在zynq7020(xc7z020clg400-2)开发板上,单张图片的推理时间为383ms

附录

整个项目文件夹结构如下
基于FPGA的Yolov4 tiny目标检测网络加速器
包含HLS源码(hls)、CPU端源码(sdk)、已经处理好的权重(folded_weights)以及解码、可视化工具(tools),令附有一个演示视频。
工程源码和更多细节详情可加qq 1830010557

版权声明:本文为博主FPGA硅农原创文章,版权归属原作者,如果侵权,请联系我们删除!

原文链接:https://blog.csdn.net/qq_40268672/article/details/123164466

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
心中带点小风骚的头像心中带点小风骚普通用户
上一篇 2022年2月28日 下午6:47
下一篇 2022年2月28日

相关推荐