【深度学习】基于web端和C++的两种深度学习模型部署方式
深度學(xué)習(xí)
Author:louwill
Machine Learning Lab
? ? ?
? ? ? 本文對深度學(xué)習(xí)兩種模型部署方式進(jìn)行總結(jié)和梳理。一種是基于web服務(wù)端的模型部署,一種是基于C++軟件集成的方式進(jìn)行部署。
? ? ? 基于web服務(wù)端的模型部署,主要是通過REST API的形式來提供接口方便調(diào)用。而基于C++的深度學(xué)習(xí)模型部署,主要是通過深度學(xué)習(xí)框架的C++前端版本,將模型集成到軟件服務(wù)中。
? ? ? 本文分別對上述兩種模型部署方式進(jìn)行流程梳理,并分別舉例進(jìn)行說明。
1. 基于web端的模型部署
1.1 web服務(wù)與技術(shù)框架
? ? ?下面以ResNet50預(yù)訓(xùn)練模型為例,旨在展示一個(gè)輕量級的深度學(xué)習(xí)模型部署,寫一個(gè)較為簡單的圖像分類的REST API。主要技術(shù)框架為Keras+Flask+Redis。其中Keras作為模型框架、Flask作為后端Web框架、Redis則是方便以鍵值形式存儲圖像的數(shù)據(jù)庫。各主要package版本:
tensorflow 1.14 keras 2.2.4 flask 1.1.1 redis 3.3.8???? 先簡單說一下Web服務(wù),一個(gè)Web應(yīng)用的本質(zhì)無非就是客戶端發(fā)送一個(gè)HTTP請求,然后服務(wù)器收到請求后生成一個(gè)HTML文檔作為響應(yīng)返回給客戶端的過程。在部署深度學(xué)習(xí)模型時(shí),大多時(shí)候我們不需要搞一個(gè)前端頁面出來,一般是以REST API的形式提供給開發(fā)調(diào)用。那么什么是API呢?很簡單,如果一個(gè)URL返回的不是HTML,而是機(jī)器能直接解析的數(shù)據(jù),這樣的一個(gè)URL就可以看作是一個(gè)API。
先開啟Redis服務(wù):
redis-server1.2 服務(wù)配置
???? 定義一些配置參數(shù):
IMAGE_WIDTH = 224 IMAGE_HEIGHT = 224 IMAGE_CHANS = 3 IMAGE_DTYPE = "float32" IMAGE_QUEUE = "image_queue" BATCH_SIZE = 32 SERVER_SLEEP = 0.25 CLIENT_SLEEP = 0.25? ? ?指定輸入圖像大小、類型、batch_size大小以及Redis圖像隊(duì)列名稱。
???? 然后創(chuàng)建Flask對象實(shí)例,建立Redis數(shù)據(jù)庫連接:
app = flask.Flask(__name__) db = redis.StrictRedis(host="localhost", port=6379, db=0) model = None???? 因?yàn)閳D像數(shù)據(jù)作為numpy數(shù)組不能直接存儲到Redis中,所以圖像存入到數(shù)據(jù)庫之前需要將其序列化編碼,從數(shù)據(jù)庫取出時(shí)再將其反序列化解碼即可。分別定義編碼和解碼函數(shù):
def base64_encode_image(img):return base64.b64encode(img).decode("utf-8")def base64_decode_image(img, dtype, shape):if sys.version_info.major == 3:img = bytes(img, encoding="utf-8")img = np.frombuffer(base64.decodebytes(img), dtype=dtype)img = img.reshape(shape)return img???? 另外待預(yù)測圖像還需要進(jìn)行簡單的預(yù)處理,定義預(yù)處理函數(shù)如下:
def prepare_image(image, target):# if the image mode is not RGB, convert itif image.mode != "RGB":image = image.convert("RGB")# resize the input image and preprocess itimage = image.resize(target)image = img_to_array(image)# expand image as one batch like shape (1, c, w, h)image = np.expand_dims(image, axis=0)image = imagenet_utils.preprocess_input(image)# return the processed imagereturn image1.3 預(yù)測接口定義
???? 準(zhǔn)備工作完畢之后,接下來就是主要的兩大部分:模型預(yù)測部分和app后端響應(yīng)部分。先定義模型預(yù)測函數(shù)如下:
def classify_process():# 導(dǎo)入模型print("* Loading model...")model = ResNet50(weights="imagenet")print("* Model loaded")while True:# 從數(shù)據(jù)庫中創(chuàng)建預(yù)測圖像隊(duì)列queue = db.lrange(IMAGE_QUEUE, 0, BATCH_SIZE - 1)imageIDs = []batch = None# 遍歷隊(duì)列for q in queue:# 獲取隊(duì)列中的圖像并反序列化解碼q = json.loads(q.decode("utf-8"))image = base64_decode_image(q["image"], IMAGE_DTYPE,(1, IMAGE_HEIGHT, IMAGE_WIDTH, IMAGE_CHANS))# 檢查batch列表是否為空if batch is None:batch = image# 合并batchelse:batch = np.vstack([batch, image])# 更新圖像IDimageIDs.append(q["id"])if len(imageIDs) > 0:print("* Batch size: {}".format(batch.shape))preds = model.predict(batch)results = imagenet_utils.decode_predictions(preds)# 遍歷圖像ID和預(yù)測結(jié)果并打印for (imageID, resultSet) in zip(imageIDs, results):# initialize the list of output predictionsoutput = []# loop over the results and add them to the list of# output predictionsfor (imagenetID, label, prob) in resultSet:r = {"label": label, "probability": float(prob)}output.append(r)# 保存結(jié)果到數(shù)據(jù)庫db.set(imageID, json.dumps(output))# 從隊(duì)列中刪除已預(yù)測過的圖像db.ltrim(IMAGE_QUEUE, len(imageIDs), -1)time.sleep(SERVER_SLEEP)???? 然后定義app服務(wù):
@app.route("/predict", methods=["POST"]) def predict():# 初始化數(shù)據(jù)字典data = {"success": False}# 確保圖像上傳方式正確if flask.request.method == "POST":if flask.request.files.get("image"):# 讀取圖像數(shù)據(jù)image = flask.request.files["image"].read()image = Image.open(io.BytesIO(image))image = prepare_image(image, (IMAGE_WIDTH, IMAGE_HEIGHT))# 將數(shù)組以C語言存儲順序存儲image = image.copy(order="C")# 生成圖像IDk = str(uuid.uuid4())d = {"id": k, "image": base64_encode_image(image)}db.rpush(IMAGE_QUEUE, json.dumps(d))# 運(yùn)行服務(wù)while True:# 獲取輸出結(jié)果output = db.get(k)if output is not None:output = output.decode("utf-8")data["predictions"] = json.loads(output)db.delete(k)breaktime.sleep(CLIENT_SLEEP)data["success"] = Truereturn flask.jsonify(data)???? Flask使用Python裝飾器在內(nèi)部自動(dòng)將請求的URL和目標(biāo)函數(shù)關(guān)聯(lián)了起來,這樣方便我們快速搭建一個(gè)Web服務(wù)。
1.4 接口測試
???? 服務(wù)搭建好了之后我們可以用一張圖片來測試一下效果:
curl -X POST -F image=@test.jpg 'http://127.0.0.1:5000/predict'模型端的返回:
預(yù)測結(jié)果返回:
???? 最后我們可以給搭建好的服務(wù)進(jìn)行一個(gè)壓力測試,看看服務(wù)的并發(fā)等性能如何,定義一個(gè)壓測文件stress_test.py 如下:
from threading import Thread import requests import time # 請求的URL KERAS_REST_API_URL = "http://127.0.0.1:5000/predict" # 測試圖片 IMAGE_PATH = "test.jpg" # 并發(fā)數(shù) NUM_REQUESTS = 500 # 請求間隔 SLEEP_COUNT = 0.05 def call_predict_endpoint(n):# 上傳圖像image = open(IMAGE_PATH, "rb").read()payload = {"image": image}# 提交請求r = requests.post(KERAS_REST_API_URL, files=payload).json()# 確認(rèn)請求是否成功if r["success"]:print("[INFO] thread {} OK".format(n))else:print("[INFO] thread {} FAILED".format(n)) # 多線程進(jìn)行 for i in range(0, NUM_REQUESTS):# 創(chuàng)建線程來調(diào)用apit = Thread(target=call_predict_endpoint, args=(i,))t.daemon = Truet.start()time.sleep(SLEEP_COUNT) time.sleep(300)測試效果如下:
2. 基于C++的模型部署
2.1 引言
???? PyTorch作為一款端到端的深度學(xué)習(xí)框架,在1.0版本之后已具備較好的生產(chǎn)環(huán)境部署條件。除了在web端撰寫REST API進(jìn)行部署之外(參考),軟件端的部署也有廣泛需求。尤其是最近發(fā)布的1.5版本,提供了更為穩(wěn)定的C++前端API。
???? 工業(yè)界與學(xué)術(shù)界最大的區(qū)別在于工業(yè)界的模型需要落地部署,學(xué)界更多的是關(guān)心模型的精度要求,而不太在意模型的部署性能。一般來說,我們用深度學(xué)習(xí)框架訓(xùn)練出一個(gè)模型之后,使用Python就足以實(shí)現(xiàn)一個(gè)簡單的推理演示了。但在生產(chǎn)環(huán)境下,Python的可移植性和速度性能遠(yuǎn)不如C++。所以對于深度學(xué)習(xí)算法工程師而言,Python通常用來做idea的快速實(shí)現(xiàn)以及模型訓(xùn)練,而用C++作為模型的生產(chǎn)工具。目前PyTorch能夠完美的將二者結(jié)合在一起。實(shí)現(xiàn)PyTorch模型部署的核心技術(shù)組件就是TorchScript和libtorch。
???? 所以基于PyTorch的深度學(xué)習(xí)算法工程化流程大體如下圖所示:
2.2 TorchScript
???? TorchScript可以視為PyTorch模型的一種中間表示,TorchScript表示的PyTorch模型可以直接在C++中進(jìn)行讀取。PyTorch在1.0版本之后都可以使用TorchScript的方式來構(gòu)建序列化的模型。TorchScript提供了Tracing和Script兩種應(yīng)用方式。
???? Tracing應(yīng)用示例如下:
class?MyModel(torch.nn.Module):def?__init__(self):super(MyModel,?self).__init__()self.linear = torch.nn.Linear(4, 4)def?forward(self,?x,?h):new_h?=?torch.tanh(self.linear(x)?+?h)return new_h, new_h#?創(chuàng)建模型實(shí)例? my_model?=?MyModel() #?輸入示例 x,?h?=?torch.rand(3,?4),?torch.rand(3,?4) #?torch.jit.trace方法對模型構(gòu)建TorchScript traced_model?=?torch.jit.trace(my_model,?(x,?h)) #?保存轉(zhuǎn)換后的模型 traced_model.save('model.pt')???? 在這段代碼中,我們先是定義了一個(gè)簡單模型并創(chuàng)建模型實(shí)例,然后給定輸入示例,Tracing方法最關(guān)鍵的一步在于使用torch.jit.trace方法對模型進(jìn)行TorchScript轉(zhuǎn)化。我們可以獲得轉(zhuǎn)化后的traced_model對象獲得其計(jì)算圖屬性和代碼屬性。計(jì)算圖屬性:
print(traced_model.graph) graph(%self.1?:?__torch__.torch.nn.modules.module.___torch_mangle_1.Module,%input?:?Float(3,?4),%h?:?Float(3,?4)):%19?:?__torch__.torch.nn.modules.module.Module?=?prim::GetAttr[name="linear"](%self.1)%21?:?Tensor?=?prim::CallMethod[name="forward"](%19,?%input)%12?:?int?=?prim::Constant[value=1]()?#?/var/lib/jenkins/workspace/beginner_source/Intro_to_TorchScript_tutorial.py:188:0%13?:?Float(3,?4)?=?aten::add(%21,?%h,?%12)?#?/var/lib/jenkins/workspace/beginner_source/Intro_to_TorchScript_tutorial.py:188:0%14?:?Float(3,?4)?=?aten::tanh(%13)?#?/var/lib/jenkins/workspace/beginner_source/Intro_to_TorchScript_tutorial.py:188:0%15?:?(Float(3,?4),?Float(3,?4))?=?prim::TupleConstruct(%14,?%14)return (%15)代碼屬性:
print(traced_cell.code) def?forward(self,input:?Tensor,h:?Tensor)?->?Tuple[Tensor,?Tensor]:_0?=?torch.add((self.linear).forward(input,?),?h,?alpha=1)_1?=?torch.tanh(_0)return (_1, _1)???? 這樣我們就可以將整個(gè)模型都保存到硬盤上了,并且經(jīng)過這種方式保存下來的模型可以加載到其他其他語言環(huán)境中。
???? TorchScript的另一種實(shí)現(xiàn)方式是Script的方式,可以算是對Tracing方式的一種補(bǔ)充。當(dāng)模型代碼中含有if或者for-loop等控制流程序時(shí),使用Tracing方式是無效的,這時(shí)候可以采用Script方式來進(jìn)行實(shí)現(xiàn)TorchScript。實(shí)現(xiàn)方法跟Tracing差異不大,關(guān)鍵在于把jit.tracing換成jit.script方法,示例如下。
scripted_model?=?torch.jit.script(MyModel) scripted_model.save('model.pt')???? 除了Tracing和Script之外,我們也可以混合使用這兩種方式,這里不做詳述。總之,TorchScript為我們提供了一種表示形式,可以對代碼進(jìn)行編譯器優(yōu)化以提供更有效的執(zhí)行。
2.3 libtorch
???? 在Python環(huán)境下對訓(xùn)練好的模型進(jìn)行轉(zhuǎn)換之后,我們需要C++環(huán)境下的PyTorch來讀取模型并進(jìn)行編譯部署。這種C++環(huán)境下的PyTorch就是libtorch。因?yàn)閘ibtorch通常用來作為PyTorch模型的C++接口,libtorch也稱之為PyTorch的C++前端。
???? 我們可以直接從PyTorch官網(wǎng)下載已經(jīng)編譯好的libtorch安裝包,當(dāng)然也可以下載源碼自行進(jìn)行編譯。這里需要注意的是,安裝的libtorch版本要與Python環(huán)境下的PyTorch版本一致。
???? 安裝好libtorch后可簡單測試下是否正常。比如我們用TorchScript轉(zhuǎn)換一個(gè)預(yù)訓(xùn)練模型,示例如下:
import torch import torchvision.models as models vgg16 = models.vgg16() example = torch.rand(1, 3, 224, 224).cuda() model = model.eval() traced_script_module = torch.jit.trace(model, example) output = traced_script_module(torch.ones(1,3,224,224).cuda()) traced_script_module.save('vgg16-trace.pt') print(output)輸出為:
tensor([[?-0.8301,?-35.6095,?12.4716]],?device='cuda:0',grad_fn=<AddBackward0>)???? 然后切換到C++環(huán)境,編寫CmakeLists文件如下:
cmake_minimum_required(VERSION?3.0.0?FATAL_ERROR) project(libtorch_test) find_package(Torch?REQUIRED) message(STATUS?"Pytorch?status:") message(STATUS?"libraries:?${TORCH_LIBRARIES}") add_executable(libtorch_test?test.cpp) target_link_libraries(libtorch_test?"${TORCH_LIBRARIES}") set_property(TARGET libtorch_test PROPERTY CXX_STANDARD 11)???? 繼續(xù)編寫test.cpp代碼如下:
#include?"torch/script.h" #include?"torch/torch.h" #include?<iostream> #include?<memory> using?namespace?std;int?main(int?argc,?const?char*?argv[]){if?(argc?!=?2)?{std::cerr?<<?"usage:?example-app?<path-to-exported-script-module>\n";return?-1;}//?讀取TorchScript轉(zhuǎn)化后的模型torch::jit::script::Module?module;try?{module?=?torch::jit::load(argv[1]);}catch?(const?c10::Error&?e)?{std::cerr?<<?"error?loading?the?model\n";return?-1;}module->to(at::kCUDA);assert(module?!=?nullptr);std::cout << "ok\n";//?構(gòu)建示例輸入std::vector<torch::jit::IValue>?inputs;inputs.push_back(torch::ones({1, 3, 224, 224}).to(at::kCUDA));//?執(zhí)行模型推理并輸出tensorat::Tensor?output?=?module->forward(inputs).toTensor();std::cout << output.slice(/*dim=*/1, /*start=*/0, /*end=*/5) << '\n';}???? 編譯test.cpp并執(zhí)行,輸出如下。對比Python環(huán)境下的的運(yùn)行結(jié)果,可以發(fā)現(xiàn)基本是一致的,這也說明當(dāng)前環(huán)境下libtorch安裝沒有問題。
ok -0.8297,?-35.6048,?12.4823 [Variable[CUDAFloatType]{1,3}]2.4 完整部署流程
???? 通過前面對TorchScript和libtorch的描述,其實(shí)我們已經(jīng)基本將PyTorch的C++部署已經(jīng)基本講到了,這里我們再來完整的理一下整個(gè)流程?;贑++的PyTorch模型部署流程如下。
第一步:
???? 通過torch.jit.trace方法將PyTorch模型轉(zhuǎn)換為TorchScript,示例如下:
import?torch from?torchvision.models?import?resnet18 model?=resnet18() example?=?torch.rand(1,?3,?224,?224) tracing.traced_script_module = torch.jit.trace(model, example)第二步:
???? 將TorchScript序列化為.pt模型文件。
traced_script_module.save("traced_resnet_model.pt")第三步:
???? 在C++中導(dǎo)入序列化之后的TorchScript模型,為此我們需要分別編寫包含調(diào)用程序的cpp文件、配置和編譯用的CMakeLists.txt文件。CMakeLists.txt文件示例內(nèi)容如下:
cmake_minimum_required(VERSION?3.0?FATAL_ERROR) project(custom_ops) find_package(Torch?REQUIRED) add_executable(example-app?example-app.cpp) target_link_libraries(example-app?"${TORCH_LIBRARIES}") set_property(TARGET example-app PROPERTY CXX_STANDARD 14)???? 包含模型調(diào)用程序的example-app.cpp示例編碼如下:
#include?<torch/script.h>?//?torch頭文件. #include <iostream>#include <memory>int?main(int?argc,?const?char*?argv[])?{if?(argc?!=?2)?{std::cerr?<<?"usage:?example-app?<path-to-exported-script-module>\n";return?-1;}torch::jit::script::Module?module;try?{//?反序列化:導(dǎo)入TorchScript模型module?=?torch::jit::load(argv[1]);}catch?(const?c10::Error&?e)?{std::cerr?<<?"error?loading?the?model\n";return?-1;}std::cout << "ok\n";}???? 兩個(gè)文件編寫完成之后便可對其執(zhí)行編譯:
mkdir?example_test cd?example_test cmake?-DCMAKE_PREFIX_PATH=/path/to/libtorch?.. cmake --example_test . --config Release第四步:
給example-app.cpp添加模型推理代碼并執(zhí)行:
std::vector<torch::jit::IValue>?inputs;inputs.push_back(torch::ones({1,?3,?224,?224})); //?執(zhí)行推理并將模型轉(zhuǎn)化為Tensor output = module.forward(inputs).toTensor();std::cout << output.slice(/*dim=*/1, /*start=*/0, /*end=*/5) << '\n';???? 以上便是C++中部署PyTorch模型的全過程,相關(guān)教程可參考PyTorch官方:
https://pytorch.org/tutorials/
總結(jié)
? ? ?模型部署對于算法工程師而言非常重要,關(guān)系到你的工作能否產(chǎn)生實(shí)際價(jià)值。相應(yīng)的也需要大家具備足夠的工程能力,比如MySQL、Redis、C++、前端和后端的一些知識和開發(fā)技術(shù),需要各位算法工程師都能夠基本了解和能夠使用。
往期精彩回顧適合初學(xué)者入門人工智能的路線及資料下載機(jī)器學(xué)習(xí)及深度學(xué)習(xí)筆記等資料打印機(jī)器學(xué)習(xí)在線手冊深度學(xué)習(xí)筆記專輯《統(tǒng)計(jì)學(xué)習(xí)方法》的代碼復(fù)現(xiàn)專輯 AI基礎(chǔ)下載機(jī)器學(xué)習(xí)的數(shù)學(xué)基礎(chǔ)專輯獲取一折本站知識星球優(yōu)惠券,復(fù)制鏈接直接打開:https://t.zsxq.com/662nyZF本站qq群1003271085。加入微信群請掃碼進(jìn)群(如果是博士或者準(zhǔn)備讀博士請說明):總結(jié)
以上是生活随笔為你收集整理的【深度学习】基于web端和C++的两种深度学习模型部署方式的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【深度学习】9 大主题卷积神经网络(CN
- 下一篇: 指针都没搞懂,还能算得上 C++ 老司机