文章搬運(yùn)自本人知乎
VGG16網(wǎng)絡(luò)結(jié)構(gòu)介紹
VGG在2014年由牛津大學(xué)Visual GeometryGroup提出,獲得該年lmageNet競賽中Localization Task(定位任務(wù))第一名和 Classification Task (分類任務(wù))第二名。與AlexNet相比,VGG使用了3個(gè)3x3卷積核來代替7x7卷積核,使用了2個(gè)3x3卷積核來代替5x5卷積核,從而在保證具有相同感知野的條件下,提升了網(wǎng)絡(luò)的深度,在一定程度上提升了神經(jīng)網(wǎng)絡(luò)的效果。下表中,C即為VGG16的網(wǎng)絡(luò)結(jié)構(gòu),其中,VGG16中的16是指該網(wǎng)絡(luò)具有16個(gè)包含權(quán)重的網(wǎng)絡(luò)層(卷積層和全連接層)。更具體地,VGG16由13個(gè)卷積層和3個(gè)全連接層構(gòu)成,此外,VGG16還包含了5個(gè)2×2的最大池化層。
在原始的VGG16模型中,并未包含批歸一化層(Batch Normalization,BN),這給VGG16的訓(xùn)練帶來了難度。因此,在本文中,我們對VGG16進(jìn)行了一些修改,如下所示:
- 在每一層卷積層后都加上批歸一化層(BN層)。
- 將三個(gè)全連接層替換為一個(gè)全局平均池化層和一個(gè)全連接層。
修改后的網(wǎng)絡(luò)結(jié)構(gòu)可由pytorch代碼描述如下:
import torch
import torch.nn as nn
# VGG16
class VGG16(nn.Module):
def __init__(self):
super(VGG16, self).__init__()
# 特征提取層
self.features = nn.Sequential(
nn.Conv2d(in_channels=3,out_channels=64,kernel_size=3,stride=1,padding=1),
nn.BatchNorm2d(num_features=64),
nn.ReLU(),
nn.Conv2d(in_channels=64,out_channels=64,kernel_size=3,stride=1,padding=1),
nn.BatchNorm2d(num_features=64),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2,stride=2),
nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(num_features=128),
nn.ReLU(),
nn.Conv2d(in_channels=128, out_channels=128, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(num_features=128),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(in_channels=128, out_channels=256, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(num_features=256),
nn.ReLU(),
nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(num_features=256),
nn.ReLU(),
nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(num_features=256),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2,stride=2),
nn.Conv2d(in_channels=256, out_channels=512, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(num_features=512),
nn.ReLU(),
nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(num_features=512),
nn.ReLU(),
nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(num_features=512),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(num_features=512),
nn.ReLU(),
nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(num_features=512),
nn.ReLU(),
nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(num_features=512),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),
)
#
self.pool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(in_features=512,out_features=5)
def forward(self,x):
x = self.features(x)
x = self.pool(x)
x = x.view(x.size(0),-1)
result = self.fc(x)
return result
if __name__=='__main__':
net=VGG16()
x=torch.randn(1,3,224,224)
print(net(x).size())
卷積BN融合
在模型訓(xùn)練完畢后,我們對卷積層和BN層的權(quán)重進(jìn)行了BN融合操作,其原理也十分簡單。由于該操作十分常見,因此本文不再贅述,詳細(xì)可見博客。
基于FPGA的加速器設(shè)計(jì)
由于VGG16的絕大部分計(jì)算量均集中在卷積層,因此,我們僅設(shè)計(jì)針對卷積的硬件加速IP核。此外,考慮到卷積層后有時(shí)會緊接著一個(gè)池化層,因此,如需要進(jìn)行池化操作,則該硬件加速IP會在卷積完成后立即進(jìn)行池化操作,然后再寫回片外存儲器。這樣做的好處是省去了不必要的片外訪存開銷,既降低了延遲又減少了功耗開銷。
加速器的設(shè)計(jì)是平凡的,主要采用的優(yōu)化方法包括:
循環(huán)分片(loop tiling):對卷積層輸出特征圖的高、寬以及卷積的輸出和輸入通道進(jìn)行了分塊操作,分塊大小分別記為 T r T_r Tr?, T c T_c Tc?, T m T_m Tm?, T n T_n Tn?,目前其值分別取為14,14,4,48。
定點(diǎn)數(shù)量化:將32位浮點(diǎn)數(shù)量化為了16位的定點(diǎn)數(shù),其中,小數(shù)部分占10位,整數(shù)部分占6位,最高位為符號位。在HLS代碼中,該數(shù)據(jù)類型可以表示為ap_fixed<16,6,AP_RND,AP_SAT>。實(shí)驗(yàn)表明,16位定點(diǎn)數(shù)量化顯著減少了加速器的硬件資源消耗(包括存儲資源BRAM以及計(jì)算資源DSP),同時(shí)較好地保持了模型精度。(1個(gè)32位浮點(diǎn)數(shù)乘法需要消耗3個(gè)DSP48E1,而1個(gè)16位定點(diǎn)數(shù)乘法僅需消耗1個(gè)DSP48E1)
流水線:可以有效提升加速器的吞吐率。在HLS中可以簡單的通過#pragma HLS PIPELINE II=1實(shí)現(xiàn)。
循環(huán)展開:所謂循環(huán)展開,體現(xiàn)在硬件層面,就是復(fù)制多個(gè)運(yùn)算單元,并行執(zhí)行循環(huán)中所需的運(yùn)算。在本文中,我們在卷積層的輸入和輸出通道進(jìn)行了展開,展開的大小等于分塊的大小,即 P m = T m P_m=T_m Pm?=Tm?, P n = T n P_n=T_n Pn?=Tn?。在HLS中,循環(huán)展開可以通過#pragma HLS UNROLL實(shí)現(xiàn)。
乒乓操作:乒乓操作是一個(gè)常常應(yīng)用于數(shù)據(jù)流控制的設(shè)計(jì)思想,典型的乒乓操作如下圖所示,其處理流程為:輸入數(shù)據(jù)流通過“輸入數(shù)據(jù)選擇單元”將數(shù)據(jù)流等時(shí)分配到兩個(gè)“數(shù)據(jù)緩沖模塊”, 數(shù)據(jù)緩沖模塊可以為任何存儲模塊,比較常用的存儲單元為雙口RAM (DPRAM)、單口RAM (SPRAM)、FIFO等。 在第2個(gè)緩沖周期, 通過“輸入數(shù)據(jù)選擇單元”的切換,將輸入的數(shù)據(jù)流緩存到“數(shù)據(jù)緩沖模塊2”,同時(shí)將“數(shù)據(jù)緩沖模塊1”緩存的第1個(gè)周期數(shù)據(jù)通過“輸出數(shù)據(jù)選擇單元”的選擇,送到“數(shù)據(jù)流運(yùn)算處理模塊”進(jìn)行運(yùn)算處理。事實(shí)上,乒乓操作是一種粗粒度的流水線技術(shù),它以硬件資源(或面積)為代價(jià),提升了系統(tǒng)整體的吞吐率。
PS部分設(shè)計(jì)
HLS代碼編寫完畢后,可以通過C仿真、C/RTL協(xié)同仿真驗(yàn)證其正確性,然后通過C綜合將它轉(zhuǎn)化為RTL代碼,最后導(dǎo)出為Vivado可用的IP核。在Vivado中,我們將該IP核與ZYNQ IP核通過AXI總線連接在一起,然后綜合、實(shí)現(xiàn)、生成比特流文件。
上述步驟完畢后,我們便可以在Vitis中編寫C/C++代碼,通過調(diào)用掛載在ZYNQ上的加速器,實(shí)現(xiàn)對VGG16網(wǎng)絡(luò)的加速。如下代碼展示了如何在PS上調(diào)用FPGA部分的加速器。
void conv_and_pool_init(){
XStd_conv_Initialize(&hls_inst, 0);
}
void conv_and_pool_pl(short* in,short* weight,short* bias,short* out,int ch_in,int ch_out,int h,int w,int pool){
//Xil_DCacheFlushRange((u32)in,ch_in*h*w*sizeof(short));
XStd_conv_Set_in1_V(&hls_inst, (u32)in);
XStd_conv_Set_in2_V(&hls_inst, (u32)in);
XStd_conv_Set_in3_V(&hls_inst, (u32)in);
XStd_conv_Set_in4_V(&hls_inst, (u32)in);
//
XStd_conv_Set_w1_V(&hls_inst, (u32)weight);
XStd_conv_Set_w2_V(&hls_inst, (u32)weight);
XStd_conv_Set_w3_V(&hls_inst, (u32)weight);
XStd_conv_Set_w4_V(&hls_inst, (u32)weight);
//
XStd_conv_Set_bias_V(&hls_inst, (u32)bias);
XStd_conv_Set_out1_V(&hls_inst, (u32)out);
XStd_conv_Set_out2_V(&hls_inst, (u32)out);
XStd_conv_Set_out3_V(&hls_inst, (u32)out);
XStd_conv_Set_out4_V(&hls_inst, (u32)out);
//
XStd_conv_Set_ch_in(&hls_inst, (u32)ch_in);
XStd_conv_Set_ch_out(&hls_inst, (u32)ch_out);
XStd_conv_Set_fm_size(&hls_inst, (u32)h);
XStd_conv_Set_pool(&hls_inst, (u32)pool);
XStd_conv_Start(&hls_inst);
while(XStd_conv_IsDone(&hls_inst)==0);
//if(pool==1)
// Xil_DCacheInvalidateRange((u32)((unsigned int)out&0xffffffe0), 32*((ch_out*h*w/4*sizeof(short))/32+2));
//else
// Xil_DCacheInvalidateRange((u32)((unsigned int)out&0xffffffe0), 32*((ch_out*h*w*sizeof(short))/32+2));
}
其中,conv_and_pool_init函數(shù)用于初始化加速器,而conv_and_pool_pl函數(shù)用于調(diào)用加速器進(jìn)行計(jì)算,其參數(shù)的含義解釋如下:
in: 輸入特征圖在DDR中的的起始地址。
weight: 權(quán)重在DDR中的起始地址。
bias: 偏置在DDR中的起始地址。
out: 輸出特征圖在DDR中的起始地址。
ch_in: 卷積的輸入通道數(shù)。
ch_out: 卷積的輸出通道數(shù)。
h: 輸入特征圖的高度。
w: 輸入特征圖的寬度。
pool: 是否進(jìn)行池化操作,為1表示需要進(jìn)行池化操作,為0則表示無需進(jìn)行池化操作。
上述兩個(gè)函數(shù)中用到的驅(qū)動(dòng)函數(shù)可在頭文件xstd_conv.h中查詢(如下圖),這些驅(qū)動(dòng)函數(shù)是由HLS工具自動(dòng)生成的,可以大大簡化程序員調(diào)用加速器的難度。
結(jié)果
實(shí)驗(yàn)的硬件平臺為zynq7020開發(fā)板(xc7z020clg400-2),所用的vivado版本為2019.2。硬件加速器的資源、功耗情況(時(shí)鐘頻率130MHz)如下圖所示。
由圖可知,加速器的片上功耗為2.939W,共消耗了35874個(gè)LUTs,40766個(gè)FF以及220個(gè)DSP。可見,加速器的規(guī)模主要受限于DSP的數(shù)目(zynq7020的DSP總數(shù)僅為220個(gè))。
推理測試
共測試了100張圖片,精度為0.95,總共耗時(shí)約74000000us,故單張圖片的推理延遲為0.74s,考慮到VGG16的計(jì)算量為15.5GMACs(15.5G乘累加操作),如果將一次乘累加操作算作2次運(yùn)算,則VGG16的GOPs為31GOPs,因此,加速器的吞吐率為31/0.74=41.9GOP/s。
(CNN計(jì)算量的計(jì)算方法可以參見博客)文章來源:http://www.zghlxwxcb.cn/news/detail-461487.html
附:整個(gè)工程有償出售(包括Python代碼,HLS代碼以及Vitis代碼),若有意向可私聊。文章來源地址http://www.zghlxwxcb.cn/news/detail-461487.html
到了這里,關(guān)于基于FPGA的VGG16卷積神經(jīng)網(wǎng)絡(luò)加速器的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!