模型部署之dll打包

       本文主要講解模型訓練好後,怎麼封裝成dll接口,以供其他語言調用。神經網絡框架以ncnn爲例,其他框架大體思想都差不多,可以參考本文的思想,或者將模型轉成ncnn,直接使用本文的教程亦可。

       在打包前,首先需要明白打包的目標,如下:

       1. 打包的文件。我們的目標是生成dll文件,如果僅僅將代碼打包成dll,那麼模型文件將會獨立出來,從而打包好後的dll內僅僅包含代碼,部署時,還需將模型文件一起發佈,即dll+model的組合發佈,這樣是極其不便利的,也不符合簡約美。本文采用另一種方式,將模型文件讀進內存,和代碼一起整合進dll中,進而發佈時,僅需dll文件即可。此處涉及到ncnn模型的加載方式問題,可以參考上一篇博客:https://blog.csdn.net/Enchanted_ZhouH/article/details/106063552,本文以mobilenet_v2爲示例講解打包的過程,關於mobilenet_v2對應的ncnn模型的獲取,只需將這篇博客的resnet18換成mobilenet_v2即可。

       2. 打包的方式。靜態打包or動態打包?靜態打包是指將所有依賴的庫一起打包進dll中,這樣dll不管在哪個環境下均可以運行。動態打包是指dll在運行時,自動根據依賴關係在當前設備上尋找依賴庫,如果兩臺設備(打包設備/部署設備)環境不一致,那麼調用dll時將會報錯,提示找不到xxx.dll。比較關鍵的是vc runtime庫,不同的設備下,不一定安裝了此庫,或者版本不同等。爲了得到更好的可移植性,本文將採用靜態打包,需要注意的一點是,靜態打包時,所有的依賴庫均需要靜態編譯。

       3. 打包的位數。操作系統分爲32位和64位,32位的dll在32位環境中運行,64位的dll在64位環境中運行。實際測試中,32位的dll只能在32位的python下運行,64位同理。本文以常用的64位進行打包,並用python調用dll進行測試。

       綜上,本文在64位環境下,採用靜態打包的方式,將代碼和模型一起整合進dll。

       全文的代碼、讀進內存的模型和打包好的dll等放在了github上,地址在這:https://github.com/PigTS/model-package-dll

       一、ncnn的編譯

       ncnn的編譯參考官網:https://github.com/Tencent/ncnn/wiki/how-to-build#build-for-windows-x64-using-visual-studio-community-2017,主要改動在於打開靜態編譯選項。

       打開VS編譯工具,本文以VS2015爲例,其他版本操作步驟基本一樣,如下:開始 -> 項目 -> Visual Studio 2015 -> VS2015 x64 本機工具命令提示符。

       如若需要打包32位的dll,此處的工具選擇x86即可,後續所有庫均在x86下編譯,本文後續均在x64下進行編譯。

       首先,編譯protobuf庫,如下:

download protobuf-3.4.0 from https://github.com/google/protobuf/archive/v3.4.0.zip
> cd <protobuf-root-dir>
> mkdir build-static
> cd build-static
> cmake -G"NMake Makefiles" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=%cd%/install -Dprotobuf_BUILD_TESTS=OFF -Dprotobuf_MSVC_STATIC_RUNTIME=ON ../cmake
> nmake
> nmake install

       重要改變在於-Dprotobuf_MSVC_STATIC_RUNTIME=ON,即打開靜態編譯,官方編譯選項默認是關閉的,在protobuf的CMakeLists.txt文件中,該選項的內容如下(124~132行):

if (MSVC AND protobuf_MSVC_STATIC_RUNTIME)
    foreach(flag_var
        CMAKE_CXX_FLAGS CMAKE_CXX_FLAGS_DEBUG CMAKE_CXX_FLAGS_RELEASE
        CMAKE_CXX_FLAGS_MINSIZEREL CMAKE_CXX_FLAGS_RELWITHDEBINFO)
      if(${flag_var} MATCHES "/MD")
        string(REGEX REPLACE "/MD" "/MT" ${flag_var} "${${flag_var}}")
      endif(${flag_var} MATCHES "/MD")
    endforeach(flag_var)
  endif (MSVC AND protobuf_MSVC_STATIC_RUNTIME)

       主要就是將/MD全部替換成/MT,其中,/MD爲動態編譯,/MT爲靜態編譯,且均在release下,如果後綴加上d,如/MDd和/MTd,那麼就是對應的debug版本。

       接下來,編譯ncnn,官方ncnn的CMakeLists.txt文件中沒有靜態編譯這個選項,那麼我們按照protobuf的CMakeLists.txt文件加上即可,定義一個NCNN_MSVC_STATIC_RUNTIME編譯選項,更新後的CMakeLists.txt文件已經在上面給出的github地址中,文件放在ncnn目錄下,編譯命令如下:

cd <ncnn-root-dir>
> mkdir build-static
> cd build-static
> cmake -G"NMake Makefiles" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=%cd%/install -DProtobuf_INCLUDE_DIR=<protobuf-root-dir>/build-vs2017/install/include -DProtobuf_LIBRARIES=<protobuf-root-dir>/build-vs2017/install/lib/libprotobuf.lib -DProtobuf_PROTOC_EXECUTABLE=<protobuf-root-dir>/build-vs2017/install/bin/protoc.exe -DNCNN_MSVC_STATIC_RUNTIME=ON -DNCNN_VULKAN=OFF ..
> nmake
> nmake install

       注意加上-DNCNN_MSVC_STATIC_RUNTIME=ON,打開ncnn的靜態編譯。

       至此,ncnn編譯完成,我們只需要build-static/install裏面的頭文件和庫文件即可。

       二、dll的打包

       dll是一個接口,可供其他語言調用,或者移植到另一臺機器上使用。打包dll,首先需要明白要暴露出來的接口是什麼,本文設計的接口頭文件是src/interface.h,主要接口如下:

//init model
void initModel();
//identify
const char* identify(const char* img_base64);
//free model
void freeModel();

       主要實現如下功能:1. 初始化模型;2. 識別,輸入爲圖像的base64編碼流,方便傳輸,返回爲類別+概率的char指針;3. 釋放模型內存。

       關於代碼的實現部分,直接看src/interface.cpp即可。讀取圖像這塊,使用了stb庫(https://github.com/nothings/stb),使用該庫僅用來讀取圖像,小巧輕便,方便打包。

       接下來,準備打包dll,步驟如下:1. 在VS中創建一個空項目,將ncnn的頭文件和庫導入;2. 將github中src下的代碼(.h/.cpp)導入對應的頭文件和源文件中;3. 添加配置文件,即源文件中添加src/interface.def。

       interface.def文件爲模板定義文件,其中定義了輸出的方法,即dll中的接口,內容如下:

LIBRARY ImageRecognitionEngine

EXPORTS
initModel
identify
freeModel

       最後,在項目的屬性頁中,運行庫選擇多線程(/MT),和前面保持一致。操作步驟爲:右鍵項目 -> 屬性 -> C/C++ -> 代碼生成 -> 運行庫 -> 選擇多線程(/MT)。

       編譯整個項目即可生成dll文件,生成的dll文件見github的dll目錄,dll/static目錄下爲靜態編譯的dll文件,dll/dynamic目錄下爲動態編譯的dll文件。

       三、python調用dll進行測試

       python調用dll的示例如下(文件:python/dll_test.py):

import ctypes
import base64
import time

#test img
img_path = "../img/test.jpg"
with open(img_path, 'rb') as f:
    img_base64 = base64.b64encode(f.read())
#load dll
IREngine = ctypes.CDLL("../dll/static/ImageRecognitionEngine.dll")
#IREngine = ctypes.CDLL("../dll/dynamic/ImageRecognitionEngine.dll")
#config interface argtypes and restypes
IREngine.initModel.argtypes = []
IREngine.initModel.restype = ctypes.c_void_p
IREngine.identify.argtypes = [ctypes.c_char_p]
IREngine.identify.restype = ctypes.c_char_p
IREngine.freeModel.argtypes = []
IREngine.freeModel.restypes = ctypes.c_void_p
#init model
IREngine.initModel()
#indentify
count = 0
while count < 100:
    res = IREngine.identify(img_base64).decode()
    cls, value = res.split()
    print("[dll]--->predicted class: %s, predicted value: %s" % (cls, value))
    count += 1
    time.sleep(150/1000) #sleep 150ms
#free model
IREngine.freeModel()

       dll預測結果如下:

[dll]--->predicted class: 920, predicted value: 19.291550
...

       和pytorch運行的結果進行對比,pytorch測試的代碼如下(python/pytorch_test.py):

import torch
import torchvision
import numpy as np
import cv2

#test image
img_path = "../img/test.jpg"
img = cv2.imread(img_path)
img = cv2.resize(img, (224, 224))
img = np.transpose(img, (2, 0, 1)).astype(np.float32)
img = torch.from_numpy(img)
img = img.unsqueeze(0)

#pytorch test
model = torchvision.models.mobilenet_v2(pretrained=True)
model.eval()
output = model.forward(img)
val, cls = torch.max(output.data, 1)
print("[pytorch]--->predicted class: %d, predicted value: %.6f" % (cls.item(), val.item()))

       pytorch預測結果如下:

[pytorch]--->predicted class: 920, predicted value: 19.230936

       由此可見,dll和pytorch預測類別保持一致,由於計算庫的不同,預測值有些許偏差。

       四、總結

       文末做個小結,如下:

       1. 打包模型時,如果要將模型和代碼一起打包,那麼可將模型讀進內存,這樣打包出來只有一個dll文件,方便部署的同時,又加密了模型。

       2. 靜態編譯將vc runtime等庫一起打包進dll,使得dll的可移植性更好,動態編譯需要部署機器和打包機器環境一致才能運行dll。靜態編譯的dll會比動態編譯的dll的體積略大一些,具體可見dll目錄。

       使用VS自帶的dumpbin工具分析dll依賴關係,靜態編譯的dll依賴關係如下:

dumpbin /dependents static/ImageRecognitionEngine.dll

運行結果:

Dump of file ImageRecognitionEngine.dll

File Type: DLL

  Image has the following dependencies:

    VCOMP140.DLL
    KERNEL32.dll

       可見,靜態編譯的dll僅僅依賴VCOMP140.DLL和KERNEL32.dll這兩個庫,KERNEL32.dll是win系統自帶的,VCOMP140.DLL一般win上也有,查看VCOMP140.DLL的依賴關係,結果如下:

dumpbin /dependents VCOMP140.DLL

運行結果:

Dump of file VCOMP140.DLL

File Type: DLL

  Image has the following dependencies:

    KERNEL32.dll
    USER32.dll

       可見,VCOMP140.DLL依賴的庫都是系統自帶庫,若部署機器上沒有VCOMP140.DLL,將打包機器上的VCOMP140.DLL同ImageRecognitionEngine.dll一起復制到部署機器上即可。

       接着,分析動態編譯的dll依賴關係,如下:

dumpbin /dependents dynamic/ImageRecognitionEngine.dll

運行結果:

Dump of file ImageRecognitionEngine.dll

File Type: DLL

  Image has the following dependencies:

    MSVCP140.dll
    VCOMP140.DLL
    VCRUNTIME140.dll
    api-ms-win-crt-string-l1-1-0.dll
    api-ms-win-crt-runtime-l1-1-0.dll
    api-ms-win-crt-stdio-l1-1-0.dll
    api-ms-win-crt-heap-l1-1-0.dll
    api-ms-win-crt-convert-l1-1-0.dll
    api-ms-win-crt-math-l1-1-0.dll
    KERNEL32.dll

       可見,動態編譯的dll依賴的庫有些多,主要有個VCRUNTIME140.dll,還有MSVCP140.dll和一系列api-ms-win-xxx等庫,這些庫還動態依賴了一些其他庫,導致部署起來很麻煩,如果部署機器上沒有安裝vc runtime等一系列庫,那麼調用動態編譯的dll很容易報錯,提示找不到xxx.dll。

       所以,強烈推薦採用靜態編譯的方式部署dll。

       3. 打包位數根據自己的需求進行選擇,32位or64位。

       4. 一般圖像都會進行一些預處理,如:歸一化等,然後再送進網絡進行識別,數據預處理在python和C++中需要統一。

       至此,dll打包的核心內容就介紹完畢了,dll主要是部署在Windows機器上,如若打算部署在移動端,如:Android,則需要打包成so接口,下一篇博客將介紹使用本文的代碼如何打包成so接口,使用Android機器調用so接口進行圖像識別,感興趣的小夥伴可以留意下。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章