博客專欄

EEPW首頁 > 博客 > 實用教程詳解:模型部署,用DNN模塊部署YOLOv5目標檢測

實用教程詳解:模型部署,用DNN模塊部署YOLOv5目標檢測

發(fā)布人:計算機視覺工坊 時間:2021-12-15 來源:工程師 發(fā)布文章

一、什么是模型部署?

在典型的機器學習和深度學習項目中,我們通常從定義問題陳述開始,然后是數(shù)據(jù)收集和準備(數(shù)據(jù)預處理)和模型構(gòu)建(模型訓練),對吧?但是,最后,我們希望我們的模型能夠提供給最終用戶,以便他們能夠利用它。模型部署是任何機器學習項目的最后階段之一,可能有點棘手。如何將機器學習模型傳遞給客戶/利益相關(guān)者?模型的部署大致分為以下三個步驟:

· 模型持久化

持久化,通俗得講,就是臨時數(shù)據(jù)(比如內(nèi)存中的數(shù)據(jù),是不能永久保存的)持久化為持久數(shù)據(jù)(比如持久化至數(shù)據(jù)庫中,能夠長久保存)。那我們訓練好的模型一般都是存儲在內(nèi)存中,這個時候就需要用到持久化方式,在Python中,常用的模型持久化方式一般都是以文件的方式持久化。

· 選擇適合的服務器加載已經(jīng)持久化的模型

· 提高服務接口,拉通前后端數(shù)據(jù)交流

1.png

PPLNN

二、案例,運行操作:

· 準備ONNX模型

我們在tests/testdata下準備了一個分類模型mnasnet0_5.onnx,可用于測試。

通過如下手段可以獲取更多的ONNX模型:

可以從OpenMMLab/PyTorch導出ONNX模型:model-convert-guide.md

從ONNX Model Zoo獲取模型:https://github.com/onnx/models

ONNX Model Zoo的模型opset版本都較低,可以通過tools下的convert_onnx_opset_version.py將opset轉(zhuǎn)換為11:

python convert_onnx_opset_version.py --input_model input_model.onnx --output_model output_model.onnx --output_opset 11

轉(zhuǎn)換opset具體請參考:onnx-model-opset-convert-guide.md

· 準備測試圖片

測試圖片使用任何格式均可。我們在tests/testdata下準備了cat0.png和cat1.jpg(ImageNet 的驗證集圖片):

2.jpg

任意大小的圖片都可以正常運行,如果想要resize到224 x 224的話,可以修改程序里的如下變量:

const bool resize_input = false; // 想要resize的話,修改為true即可

· 測試推理服務

運行
pplnn-build/samples/cpp/run_model/classification <image_file> <onnx_model_file>

推理完成后,會得到如下輸出:

image preprocess succeed!
[INFO][2021-07-23 17:29:31.341][simple_graph_partitioner.cc:107] total partition(s) of graph[torch-jit-export]: 1.
successfully create runtime builder!
successfully build runtime!
successfully set input data to tensor [input]!
successfully run network!
successfully get outputs!
top 5 results:
1th: 3.416199   284        n02123597 Siamese cat, Siamese
2th: 3.049764   285        n02124075 Egyptian cat
3th: 2.989676   606        n03584829 iron, smoothing iron
4th: 2.812310   283        n02123394 Persian cat
5th: 2.796991   749        n04033901 quill, quill pen

不難看出,這個程序正確判斷貓是真貓。至此OpenPPL的安裝與圖像分類模型推理已完成。另外,在pplnn-build/tools目錄下有可執(zhí)行文件pplnn,可以進行任意模型推理、dump輸出數(shù)據(jù)、benchmark等操作,具體用法可使用--help選項查看。大家可以基于該示例進行改動,從而更熟悉OpenPPL的用法。

三、DNN模塊部署Yolov5

用opencv的dnn模塊做yolov5目標檢測的程序,包含兩個步驟:1)、把pytorch的訓練模型pth文件轉(zhuǎn)換到onnx文件;2)、opencv的dnn模塊讀取onnx文件做前向計算。

1)、把pytorch的訓練模型pth文件轉(zhuǎn)換到onnx文件

yolov5官方代碼:https://github.com/ultralytics/yolov5

這套程序里的代碼比較亂,在pytorch里,通常是在py文件里定義網(wǎng)絡結(jié)構(gòu)的,但是官方代碼是在yaml文件定義網(wǎng)絡結(jié)構(gòu),利用pytorch動態(tài)圖特性,解析yaml文件自動生成網(wǎng)絡結(jié)構(gòu)。

在yaml文件里有depth_multiple和width_multiple,它是控制網(wǎng)絡的深度和寬度的參數(shù)。這么做的好處是能夠靈活的配置網(wǎng)絡結(jié)構(gòu),但是不利于理解網(wǎng)絡結(jié)構(gòu),假如你想設斷點查看某一層的參數(shù)和輸出數(shù)值,那就沒辦法了。

3.jpg

因此,在編寫的轉(zhuǎn)換到onnx文件的程序里,網(wǎng)絡結(jié)構(gòu)是在py文件里定義的。其次,在官方代碼里,還有一個奇葩的地方,那就是pth文件。起初,下載官方代碼到本地運行時,torch.load讀取pth文件總是出錯,后來把pytorch升級到1.7,就讀取成功了。可以看到版本兼容性不好,這是它的一個不足之處。設斷點查看讀取的pth文件里的內(nèi)容,可以看到ultralytics的pt文件里既存儲有模型參數(shù),也存儲有網(wǎng)絡結(jié)構(gòu),還儲存了一些超參數(shù),包括anchors,stride等等。

self.register_buffer('anchors', a)  #shape(nl,na,2)
self.register_buffer('anchor_grid', a.clone().view(self.nl, 1, -1, 1, 1, 2))

嘗試過把這兩行代碼改成:

self.anchors = a
self.anchor_grid = a.clone().view(self.nl, 1, -1, 1, 1, 2)

程序依然能正常運行,但是torch.save保存模型文件后,可以看到pth文件里沒有存儲anchors和anchor_grid了,在百度搜索register_buffer,解釋是:pytorch中register_buffer模型保存和加載的時候可以寫入和讀出。在這兩行代碼的下一行:

self.m = nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch)   # output conv

它的作用是做特征圖的輸出通道對齊,通過1x1卷積把三種尺度特征圖的輸出通道都調(diào)整到num_anchors*(num_classes+5)。閱讀Detect類的forward函數(shù)代碼,可以看出它的作用是根據(jù)偏移公式計算出預測框的中心坐標和高寬,這里需要注意的是,計算高和寬的代碼:

pwh = (ps[:, 2:4].sigmoid() * 2) ** 2 * anchors[i]

沒有采用exp操作,而是直接乘上anchors[i],這是yolov5與yolov3v4的一個最大區(qū)別(還有一個區(qū)別就是在訓練階段的loss函數(shù)里,yolov5采用鄰域的正樣本anchor匹配策略,增加了正樣本。其它的是一些小區(qū)別,比如yolov5的第一個模塊采用FOCUS把輸入數(shù)據(jù)2倍下采樣切分成4份,在channel維度進行拼接,然后進行卷積操作,yolov5的激活函數(shù)沒有使用Mish)。

現(xiàn)在可以明白Detect類的作用是計算預測框的中心坐標和高寬,簡單來說就是生成proposal,作為后續(xù)NMS的輸入,進而輸出最終的檢測框。我覺得在Detect類里定義的1x1卷積是不恰當?shù)?,應該把它定義在Detect類的外面,緊鄰著Detect類之前定義1x1卷積。

在官方代碼里,有轉(zhuǎn)換到onnx文件的程序:

python models/export.py --weights yolov5s.pt --img 640 --batch 1

在pytorch1.7版本里,程序是能正常運行生成onnx文件的。觀察export.py里的代碼,在執(zhí)行torch.onnx.export之前,有這么一段代碼:

# Input
img = torch.zeros(opt.batch_size, 3, *opt.img_size)   # image size (1, 3, 320, 192) iDetection
# Update model
for k, m in model.named_modules():
    m._non_persistent_buffers_set = set()    # pytorch 1.6.0 compatibility
    if isinstance(m, models.common.Conv):    # assign export-friendly activations
        if isinstance(m.act, nn.Hardswish):
            m.act = Hardswish()
        elif isinstance(m.act, nn.SiLU):
            m.act = SiLU()
    # elif isinstance(m, models.yolo.Detect):
    #     m.forward = m.forward_export     #assign  forward (optional)
model.model[-1].export = True   # set Detect() Layer export = True
y = model(img)   # dry run

注意其中的for循環(huán),我試驗過注釋掉它,重新運行就會出錯,打印出的錯誤如下:

4.png

由此可見,這段for循環(huán)代碼是必需的。SiLU其實就是swish激活函數(shù),而在onnx模型里是不直接支持swish算子的,因此在轉(zhuǎn)換生成onnx文件時,SiLU激活函數(shù)不能直接使用nn.Module里提供的接口,而需要自定義實現(xiàn)它。

2)、opencv的dnn模塊讀取.onnx文件做前向計算

在生成onnx文件后,就可以用opencv的dnn模塊里的cv2.dnn.readNet讀取它。然而,在讀取時,出現(xiàn)了如下錯誤:

其實是:

5.png

于是查看yolov5的代碼,在common.py文件的Focus類,torch.cat的輸入里有4次切片操作,代碼如下:

6.png

那么現(xiàn)在需要更換索引式的切片操作,觀察到注釋的Contract類,它就是用view和permute函數(shù)完成切片操作的,于是修改代碼如下:

7.png

其次,在models\yolo.py里的Detect類里,也有切片操作,代碼如下:

y[..., 0:2] = (y[..., 0:2] * 2. - 0.5 + self.grid[i].to(x[i].device)) * self.stride[i]
y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i]   # wh

前面說過,Detect類的作用是計算預測框的中心坐標和高寬,生成proposal,這個是屬于后處理的,因此不需要把它寫入到onnx文件里。

總結(jié)一下,按照上面的截圖代碼,修改Focus類,把Detect類里面的1x1卷積定義在緊鄰著Detect類之前的外面,然后去掉Detect類,組成新的model,作為torch.onnx.export的輸入:

torch.onnx.export(model, inputs, output_onnx, verbose=False, opset_version=12, input_names=['images'], output_names=['out0', 'out1', 'out2'])

最后生成的onnx文件,opencv的dnn模塊就能成功讀取了,接下來對照Detect類里的forward函數(shù),用python或者C++編寫計算預測框的中心坐標和高寬的功能。

在github上,地址是https://github.com/hpc203/yolov5-dnn-cpp-python

四、后處理模塊

后處理模塊,python版本用numpy array實現(xiàn)的,C++版本的用vector和數(shù)組實現(xiàn)的,整套程序只依賴opencv庫(opencv4版本以上的)就能正常運行,徹底擺脫對深度學習框架pytorch,tensorflow,caffe,mxnet等等的依賴。用openvino作目標檢測,需要把onnx文件轉(zhuǎn)換到.bin和.xml文件,相比于用dnn模塊加載onnx文件做目標檢測是多了一個步驟的。因此,我就想編寫一套用opencv的dnn模塊做yolov5目標檢測的程序,用opencv的dnn模塊做深度學習目標檢測,在win10和ubuntu,在cpu和gpu上都能運行,可見dnn模塊的通用性更好,很接地氣。

生成yolov5s_param.pth 的步驟:

首先下載https://github.com/ultralytics/yolov5的源碼到本地,在yolov5-master主目錄(注意不是我發(fā)布的github代碼目錄)里新建一個.py文件,把下面的代碼復制到.py文件里。

import torch
from collections import OrderedDict
import pickle
import os
device = 'cuda' if torch.cuda.is_available() else 'cpu'
if __name__=='__main__':
    choices = ['yolov5s', 'yolov5l', 'yolov5m', 'yolov5x']
    modelfile = choices[0]+'.pt'
    utl_model = torch.load(modelfile, map_location=device)
    utl_param = utl_model['model'].model
    torch.save(utl_param.state_dict(), os.path.splitext(modelfile)[0]+'_param.pth')
    own_state = utl_param.state_dict()
    print(len(own_state))
    numpy_param = OrderedDict()
    for name in own_state:
        numpy_param[name] = own_state[name].data.cpu().numpy()
    print(len(numpy_param))
    with open(os.path.splitext(modelfile)[0]+'_numpy_param.pkl', 'wb') as fw:
        pickle.dump(numpy_param, fw)

運行這個.py文件,這時候就可以生成yolov5s_param.pth文件。之所以要進行這一步,我在上面講到過:ultralytics的.pt文件里既存儲有模型參數(shù),也存儲有網(wǎng)絡結(jié)構(gòu),還儲存了一些超參數(shù),包括anchors,stride等等的。torch.load加載ultralytics的官方.pt文件,也就是utl_model = torch.load(modelfile, map_location=device)這行代碼,在這行代碼后設斷點查看utl_model里的內(nèi)容,截圖如下:

8.png

可以看到utl_model里含有既存儲有模型參數(shù),也存儲有網(wǎng)絡結(jié)構(gòu),還儲存了一些超參數(shù)等等的,這會嚴重影響轉(zhuǎn)onnx文件。此外,我還發(fā)現(xiàn),如果pytorch的版本低于1.7,那么在torch.load加載.pt文件時就會出錯的。

因此在程序里,我把模型參數(shù)轉(zhuǎn)換到cpu.numpy形式的,最后保存在.pkl文件里。這時候在win10系統(tǒng)cpu環(huán)境里,即使你的電腦沒有安裝pytorch,也能通過python程序訪問到模型參數(shù)。

五、pytorch轉(zhuǎn)onnx常見坑:

onnx只能輸出靜態(tài)圖,因此不支持if-else分支。一次只能走一個分支。如果代碼中有if-else語句,需要改寫。

onnx不支持步長為2的切片。例如a[::2,::2]

onnx不支持對切片對象賦值。例如a[0,:,:,:]=b, 可以用torch.cat改寫

onnx里面的resize要求output shape必須為常量??梢杂靡韵麓a解決:

if isinstance(size, torch.Size):
    size = tuple(int(x) for x in size)

此外,在torch.onnx.export(model, inputs, output_onnx)的輸入?yún)?shù)model里,應該只包含網(wǎng)絡結(jié)構(gòu),也就是說model里只含有nn.Conv2d, nn.MaxPool2d, nn.BatchNorm2d, F.relu等等的這些算子組件,而不應該含有后處理模塊的。圖像預處理和后處理模塊需要自己使用C++或者Python編程實現(xiàn)。

在明白了這些之后,在轉(zhuǎn)換生成onnx文件,你需要執(zhí)行兩個步驟,第一步把原始訓練模型.pt文件里的參數(shù)保存到新的.pth文件里,第二步編寫yolov5.py文件,把yolov5的往來結(jié)構(gòu)定義在.py文件里,此時需要注意網(wǎng)絡結(jié)構(gòu)里不能包含切片對象賦值操作,F(xiàn).interpolate里的size參數(shù)需要加int強制轉(zhuǎn)換。在執(zhí)行完這兩步之后才能生成一個opencv能成功讀取并且做前向推理的onnx文件。

不過,最近我發(fā)現(xiàn)在yolov5-pytorch程序里,其實可以直接把原始訓練模型.pt文件轉(zhuǎn)換生成onnx文件的,而且我在一個yolov5檢測人臉+關(guān)鍵點的程序里實驗成功了。

*博客內(nèi)容為網(wǎng)友個人發(fā)布,僅代表博主個人觀點,如有侵權(quán)請聯(lián)系工作人員刪除。



關(guān)鍵詞: 深度學習

相關(guān)推薦

技術(shù)專區(qū)

關(guān)閉