前言
之前本來想要做基于ZCU106的Vitis-AI開發(fā),但是官方對106缺少相關文檔說明,而我需要移植的yolov5模型需要使用Vitis-AI的2.0往后的版本來支持更新的pytorch版本,相對應的也需要更新Vitis等工具的版本,所以在缺少參考資料的情況下我選擇找實驗室換成了ZCU102開發(fā)板先把基本流程走一遍,這篇博客就記錄了我移植yolov5模型的整個過程。
開發(fā)環(huán)境
硬件環(huán)境:Zcu102開發(fā)板
PC機操作系統(tǒng):Ubuntu18.04.4(錯誤的Ubuntu版本會讓Xilinx相關軟件報各種奇怪的錯誤,Xilinx相關工具支持的Ubuntu版本在各個技術文檔里面都有說明,經(jīng)典反例:Ubuntu18.04.6就不是Xilinx支持的系統(tǒng),但是是在官網(wǎng)自動下載的Ubuntu18系統(tǒng))
PC機目標檢測模型運行環(huán)境:Pytorch1.8.0+Cuda11.1
PC機Xilinx相關開發(fā)環(huán)境:Vitis2022.1+Petalinux2022.1+Xilinx Runtime2022.1+Vitis-AI2.5.0
目標檢測模型:Yolov5(6.0版本)
整體流程
模型移植的整體流程如下圖:
1.模型訓練
訓練前,先查閱Zcu102對應的DPUCZDX8G產(chǎn)品指南,了解到該DPU支持的神經(jīng)網(wǎng)絡算子如下圖所示(文檔中還有對各個算子的輸入輸出大小的限制,這里沒有列出來,如果有自己改動yolov5模型的算子的話,請對照其中內容做詳細比對):
由于yolov5的6.0版本激活函數(shù)已經(jīng)被是SiLU函數(shù)了,而該DPU是不支持該激活函數(shù)的,在Vitis-AI的定制OP功能中應該可以實現(xiàn)SiLU函數(shù),但是我還沒有摸索清楚,所以這里將模型中的SiLU激活函數(shù)替換回了老版本yolov5模型的LeakyReLU函數(shù)。具體需要修改的文件為common.py和experimental.py文件,作如下修改。我一共修改了3處激活函數(shù),解決了在量化時因為SiLU激活函數(shù)報錯的問題。
# 修改前
self.act = nn.SiLU
# 修改后
self.act = nn.LeakyReLU(0.1, inplace=True)
修改完激活函數(shù)后,只需要按照yolov5模型正常方法進行訓練即可。得到一個針對自己的數(shù)據(jù)集有目標檢測能力的yolov5模型。
2.模型量化
UG1414文檔中提到了模型量化的全過程,流程圖如下:
同時,文檔中提到了在對用戶自定義模型進行量化時需要做到:
這里就需要從代碼層面來分析yolov5模型的特征提取過程,整個特征提取過程都是直接使用pytorch的torch張量的相關算子對數(shù)據(jù)進行處理的,但是在檢測層,有一段對最終的三層特征進行處理的代碼沒有使用torch張量的相關算子,所以在對模型做量化時,需要注釋掉這一段代碼,并將其添加在檢測函數(shù)中。該代碼位于yolo.py文件的Detect類中,如下所示:
def forward(self, x):
z = [] # inference output
for i in range(self.nl):
x[i] = self.m[i](x[i]) # conv
bs, _, ny, nx = x[i].shape # x[i](bs,self.no * self.na,20,20) to x[i](bs,self.na,20,20,self.no)
x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
if not self.training: # inference
if self.onnx_dynamic or self.grid[i].shape[2:4] != x[i].shape[2:4]:
self.grid[i], self.anchor_grid[i] = self._make_grid(nx, ny, i)
y = x[i].sigmoid() # (tensor): (b, self.na, h, w, self.no)
if self.inplace:
y[..., 0:2] = (y[..., 0:2] * 2 - 0.5 + self.grid[i]) * self.stride[i] # xy
y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i] # wh
else: # for YOLOv5 on AWS Inferentia https://github.com/ultralytics/yolov5/pull/2953
xy = (y[..., 0:2] * 2 - 0.5 + self.grid[i]) * self.stride[i] # xy
wh = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i] # wh
y = torch.cat((xy, wh, y[..., 4:]), -1)
z.append(y.view(bs, -1, self.no)) # z (list[P3_pred]): Torch.Size(b, n_anchors, self.no)
return x if self.training else (torch.cat(z, 1), x)
修改后如下所示:
def forward(self, x):
z = [] # inference output
for i in range(self.nl):
x[i] = self.m[i](x[i]) # conv
bs, _, ny, nx = x[i].shape # x[i](bs,self.no * self.na,20,20) to x[i](bs,self.na,20,20,self.no)
x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
return x
在量化時,需要將這段代碼寫到補充到量化后的模型輸出之后,才能繼續(xù)使用yolov5后續(xù)的特征分析,得到目標檢測結果,這段代碼中還用到Detect類中的_make_grid函數(shù),也需要寫進量化程序中,如下圖所示。這里主要是要把Detect類中的用到的相關參數(shù)都搬出來,如果你自定義的yolov5模型改動了這些參數(shù),那就也需要跟著改。
# 模型推理
x=model(im) # 這里的model已經(jīng)是量化后的模型了,x代表量化后模型的輸出
nc = 11 # 1
no = nc + 5 + 180
anchors = [[1.25, 1.625, 2, 3.75, 4.125, 2.875], [1.875, 3.8125, 3.875, 2.8125, 3.6875, 7.4375], [3.625, 2.8125, 4.875, 6.1875, 11.65625, 10.1875]]
nl = 3 # number of detection layers
na = 3 # number of anchors
grid = [torch.zeros(1).to(device)] * nl # init grid
anchors = torch.tensor(anchors).float().to(device).view(nl, -1, 2)
anchor_grid=[torch.zeros(1).to(device)] * nl
stride = [8, 16, 32]
z = []
for i in range(nl):
bs, _, ny, nx, _no = x[i].shape
if grid[i].shape[2:4] != x[i].shape[2:4]:
grid[i], anchor_grid[i] = _make_grid(anchors, stride, nx, ny, i)
y = x[i].sigmoid() # (tensor): (b, self.na, h, w, self.no)
y[..., 0:2] = (y[..., 0:2] * 2 - 0.5 + grid[i]) * stride[i] # xy
y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * anchor_grid[i] # wh
z.append(y.view(bs, -1, no)) # z (list[P3_pred]): Torch.Size(b, n_anchors, self.no)
out, train_out = torch.cat(z, 1), x
def _make_grid(anchors,stride,nx=20, ny=20, i=0):
d = anchors[i].device
shape = 1, 3, ny, nx, 2
y, x = torch.arange(ny, device=d), torch.arange(nx, device=d)
yv, xv = torch.meshgrid([y, x])
grid = torch.stack((xv, yv), 2).expand(shape).float() # add grid offset, i.e. y = 2.0 * x - 0.5
anchor_grid = (anchors[i].clone() * stride[i]).view((1, 3, 1, 1, 2)).expand(shape).float()
return grid, anchor_grid
按照官方文檔要求對yolov5模型進行調整后,接著參考一下官方提供的pytorch模型量化代碼(minst數(shù)據(jù)集手寫體識別)來寫一個量化腳本。量化分為兩步,第一步生成量化設置文件:
from pytorch_nndct.apis import torch_quantizer
# 加載yolov5模型
model = DetectMultiBackend(file_path)
input = torch.randn([1, 3, 1024, 1024],device=device)
quantizer = torch_quantizer(
quant_mode, model, (input), device=device,bitwidth=8)
quant_model = quantizer.quant_model
quant_model = quant_model.to(device)
# 運行量化后模型,evaluate函數(shù)參考yolov5的val.py和之前提到的特征處理部分做修改即可
print(evaluate(model=quant_model))
# 生成量化設置文件
quantizer.export_quant_config()
第二步生成量化后的xmodel模型:
from pytorch_nndct.apis import torch_quantizer
# 加載yolov5模型
model = DetectPrunedMultiBackend(file_path)
input = torch.randn([1, 3, 1024, 1024],device=device)
quantizer = torch_quantizer(
quant_mode, model, (input), device=device,bitwidth=8)
quant_model = quantizer.quant_model
quant_model = quant_model.to(device)
print(evaluate(model=quant_model))
# 生成xmodel模型
if deploy:
quantizer.export_xmodel(deploy_check=False)
這兩段代碼其實很接近,主要是由于官方提供的pytorch模型量化代碼還有有一部分內容用于對量化后的模型進行快速微調,放到這兩步中,第一步用于訓練量化后的模型并快速微調量化后參數(shù),第二步直接讀取第一步保存的參數(shù)生成xmodel文件。但是由于缺少對該API函數(shù)的說明,所以我還沒有摸清楚這里的模型訓練損失應該怎么整,再加上我給我自己的yolov5模型添加的改動里面對損失函數(shù)的改動較大,所以暫時擱置了快速微調功能,如果有大佬會用的話,歡迎在評論區(qū)中賜教。
寫好相關python腳本后,需要在vitis-AI的docker環(huán)境下來運行,我使用的是目前最新的vitis-AI2.5,docker鏡像為cpu版本。在docker中的pytorch環(huán)境下運行模型量化腳本,得到一個編譯前的xmodel文件,在后續(xù)過程中需要將該模型編譯為ZCU102板子對應的DPUCZDX8G版本。只不過我為了能夠能更好地調試量化過程,選擇了將Vitis-AI的量化器的python源碼安裝到了我的Ubuntu電腦的conda環(huán)境中,pytorch版本的Vitis-AI量化器源碼位于該目錄下,在conda環(huán)境下安裝這個部分,就可以在docker外使用Vitis-AI量化器了,便于調試。
3.模型編譯
這一步其實挺輕松的,在docker中的pytorch環(huán)境下使用pytorch模型的編譯器工具vai_c_xir對上一步生成的xmodel文件做編譯即可。我使用的指令如下所示。其中-x參數(shù)指定了上一步得到的xmodel文件,-a參數(shù)指定了DPU和開發(fā)板的架構文件,-o參數(shù)指定了輸出結果的目錄,-n參數(shù)指定了輸出模型的名稱。這一步不報錯的話,會得到一個擁有1個dpu字圖(subgraph)的模型,正確的編譯情況下輸出如下圖所示:
vai_c_xir -x ./DetectMultiBackend_int.xmodel -a /opt/vitis_ai/compiler/arch/DPUCZDX8G/ZCU102/arch.json -o ./ -n model
如果你在編譯時得到了一個有多個DPU子圖的模型,說明你的模型并沒有被完整的量化編譯過來,原因有兩點。第一點是沒有按要求將前傳方法以外的函數(shù)全部移出去,第二點是模型中有DPU不能識別的算子,這兩點都會導致在量化編譯中將模型拆開成多個子圖,運行這樣的模型需要自己在代碼中按順序讀取多個子圖的輸出并補充沒有被量化編譯的函數(shù)/算子,這樣會極大地加大工作量,非常麻煩。
我編譯后的模型是編譯前模型的1/3左右大小,僅供參考。
4.開發(fā)板運行
得到編譯模型之后,就需要準備開發(fā)板運行的環(huán)境了。
第一步是給開發(fā)板布置嵌入式系統(tǒng)。ZCU102的官方指導很多,在UG1414文檔中直接下載ZCU102的嵌入式系統(tǒng)鏡像即可,該鏡像為2022.1版本,在PL端添加了DPU設備,在PS端也設置了驅動,屬于是下載即用的DPU開發(fā)嵌入式環(huán)境。下載完后使用SD卡燒寫工具把鏡像燒寫到SD卡上,就制作好ZCU102的嵌入式系統(tǒng)啟動盤了。
開發(fā)板選擇SD卡啟動模式,PC機使用minicom對開發(fā)板進行uart調試,配置好網(wǎng)絡接口后讓開發(fā)板可以連通外網(wǎng),這些就不細講了,網(wǎng)上的相關資料也挺多的。
第二步是把torch編譯到開發(fā)板上的python環(huán)境中。雖然編譯后的模型不使用torch.nn算子進行運行,但是yolov5代碼的預處理和后處理部分用到了很多使用tensor張量的相關函數(shù)來對數(shù)據(jù)做處理,由于沒有時間一點點改成numpy,所以我還是選擇了把pytorch編譯到開發(fā)板上。我這里直接選擇把源碼copy到板子上,在板子上做編譯。按照github上的流程先下載源碼并git到全部組件后,在編譯時使用如下指令(因為板子上缺這缺那,所以不能完全按照github上那些簡單的指令來編譯安裝torch),編譯時間大概是6個小時左右:
git submodule update --remote third_party/protobuf
USE_CUDA=0 USE_MKLDNN=0 USE_QNNPACK=0 USE_NNPACK=0 USE_DISTRIBUTED=0 BUILD_CAFFE2=0 BUILD_CAFFE2_OPS=0 python3 setup.py build
python3 setup.py develop && python3 -c "import torch"
第三步是安裝其他的yolov5 python依賴。這些依賴中只有pytorch是用到了C++,其他的都是純py,所以只有torch需要用開發(fā)板自帶的編譯器做編譯,其他的直接用pip安裝whl文件即可。(PS:有一點要吐槽的是這個鏡像里面不帶pip,所以還需要先自己安裝pip,在這個過程中需要用date指令提前給板子設置好時間,最好是保持和日期同步,不然下載東西的時候會報奇怪的錯誤)。
第四步,在環(huán)境全部準備好之后,就只需要一個板子上的測試腳本了,官方有一個針對pytorch模型的測試腳本,用來將模型放入DPU并運行的相關API都可以參考該腳本來使用,相關代碼如下:
def get_child_subgraph_dpu(graph: "Graph") -> List["Subgraph"]:
assert graph is not None, "'graph' should not be None."
root_subgraph = graph.get_root_subgraph()
assert (root_subgraph is not None), "Failed to get root subgraph of input Graph object."
if root_subgraph.is_leaf:
return []
child_subgraphs = root_subgraph.toposort_child_subgraph()
assert child_subgraphs is not None and len(child_subgraphs) > 0
return [
cs
for cs in child_subgraphs
if cs.has_attr("device") and cs.get_attr("device").upper() == "DPU"
]
# 讀取模型的全部子圖(這里量化后只有一個子圖),將模型加載到DPU中
g = xir.Graph.deserialize(model)
subgraphs = get_child_subgraph_dpu(g)
all_dpu_runners = []
for i in range(threads):
all_dpu_runners.append(vart.Runner.create_runner(subgraphs[0], "run"))
在這里我們需要根據(jù)DPU模型的輸入輸出格式來改動yolov5的測試程序val.py。在量化過程中,模型的輸入和輸出都由量化前的浮點數(shù)變?yōu)榱肆炕蟮亩c數(shù),其小數(shù)點位置都保存在模型中。為了方便觀察,這里使用netron工具打開xmodel文件查看模型結構。
輸入模塊如下圖。這里有兩點非常重要,第一點是輸入數(shù)據(jù)為8位定點數(shù),小數(shù)點在第7位,針對這一點,yolov5模型輸入的本來是歸一化的浮點數(shù)圖像數(shù)據(jù),這里就需要乘2的7次冪128后將數(shù)據(jù)格式改變?yōu)?位整形,這樣就實現(xiàn)了浮點數(shù)轉定點數(shù)的過程,具體代碼如下所示,第一段代碼讀取模型輸入的小數(shù)點位置,第二段對輸入圖像做處理。(我這里直接把第一步得到的input_scale加入到了dataloader類中,在下一步對圖像做維度變換時一并做了乘算和格式轉換,具體請看下一點的相關代碼)
# 讀取量化后模型對輸入的定點數(shù)數(shù)據(jù)的小數(shù)點位置,得出在浮點數(shù)轉定點數(shù)時需要乘的系數(shù)input_scale
input_fixpos = all_dpu_runners[0].get_input_tensors()[0].get_attr("fix_point")
input_scale = 2 ** input_fixpos
第二點是輸入圖像的維度為batchsize×w×h×3(1×1024×1024×3),而我們使用dataloader讀取的圖像數(shù)據(jù)維度為1×3×1024×1024。所以需要修改dataloader類,讓輸入符合DPU模型的輸入,在datasets.py的LoadImagesAndLabels類中修改__getitem__方法,在末尾添加這樣一段代碼,就將維度和數(shù)據(jù)存儲格式都修改好了。
# 將圖像維度調整到DPU要求的定點輸入
img = torch.from_numpy(img)
img = img.permute(1, 2, 0).float().numpy() / 255 * self.inputscale + 0.5
img = img.astype(np.int8)
輸出一共有三層特征層,這里以最小的一層舉例:
重點:1.DPU的輸出為download處的輸出1×32×32×588,而并非fix節(jié)點處的1×3×32×32×196
所以在后續(xù)處理中,需要我們將1×32×32×588轉變?yōu)?×3×32×32×196,這里需要參考原始yolov5模型的流程,先將1×32×32×588轉為1×588×32×32,再轉為1×3×196×32×32,最后轉為1×3×32×32×196。
2.在fix節(jié)點可以看到該輸出為8位有符號定點數(shù),小數(shù)點為第3位,實際上存儲格式為整形。所以在后續(xù)處理中,需要將該整形數(shù)據(jù)轉化為浮點數(shù)據(jù),并且除以2的3次冪8,才能用于后續(xù)的NMS等后處理。
3.三個不同的特征層的定點數(shù)據(jù)小數(shù)點可能不一樣?。。?!我這里就是其中兩層小數(shù)點位為3,一層為4,這一點千萬要注意。
具體代碼如下所示:文章來源:http://www.zghlxwxcb.cn/news/detail-792282.html
output[0] = (output[0].float() / 8).permute(0, 3, 1, 2).view(1, 3, 196, 128, 128).permute(0, 1, 3, 4, 2)
output[1] = (output[1].float() / 8).permute(0, 3, 1, 2).view(1, 3, 196, 64, 64).permute(0, 1, 3, 4, 2)
output[2] = (output[2].float() / 16).permute(0, 3, 1, 2).view(1, 3, 196, 32, 32).permute(0, 1, 3, 4, 2)
結語
完成這幾步之后,就已經(jīng)可以在ZCU102開發(fā)板上解析出目標檢測的目標框了,目前特征提取的速率能夠在輸入為1024×1024圖像的前提下達到30fps,檢測性能也沒有很大的影響,算是達成了一個階段性目標。而在博客中分享的這些差不多就是我在這個過程中踩過的主要坑點,踩坑的關鍵原因還是因為相關手冊對這些輸入輸出的維度以及格式的說明太少了,每一步都需要我去自己用各種工具翻來覆去地看,然后來揣度官方給的那幾個基本沒注釋的代碼的含義,這個過程雖然很麻煩,但是也幫我加深了對yolov5模型的數(shù)據(jù)處理的理解,也算是學到了點東西吧。完整的代碼因為一些原因不能在這里公開,所以如果各位看官有沒有看懂的地方,希望能直接在評論區(qū)提問,我也會盡我所能,和大家一起交流學習。后續(xù)我還會繼續(xù)在這一個部分上做一些工作,也希望有相同目標的小伙伴能夠多多發(fā)言討論,相互指教。文章來源地址http://www.zghlxwxcb.cn/news/detail-792282.html
到了這里,關于[ZCU102嵌入式開發(fā)]基于Vitis-AI的yolov5目標檢測模型在ZCU102開發(fā)板上的部署過程分享的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!