<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【深度學(xué)習(xí)】基于web端和C++的兩種深度學(xué)習(xí)模型部署方式

          共 9462字,需瀏覽 19分鐘

           ·

          2020-08-22 06:52


          深度學(xué)習(xí)


          Author:louwill

          Machine Learning Lab

          ? ? ?

          ? ? ? 本文對(duì)深度學(xué)習(xí)兩種模型部署方式進(jìn)行總結(jié)和梳理。一種是基于web服務(wù)端的模型部署,一種是基于C++軟件集成的方式進(jìn)行部署。


          ? ? ? 基于web服務(wù)端的模型部署,主要是通過REST API的形式來提供接口方便調(diào)用。而基于C++的深度學(xué)習(xí)模型部署,主要是通過深度學(xué)習(xí)框架的C++前端版本,將模型集成到軟件服務(wù)中。


          ? ? ? 本文分別對(duì)上述兩種模型部署方式進(jìn)行流程梳理,并分別舉例進(jìn)行說明。


          1. 基于web端的模型部署

          1.1 web服務(wù)與技術(shù)框架

          ? ? ?下面以ResNet50預(yù)訓(xùn)練模型為例,旨在展示一個(gè)輕量級(jí)的深度學(xué)習(xí)模型部署,寫一個(gè)較為簡(jiǎn)單的圖像分類的REST API。主要技術(shù)框架為Keras+Flask+Redis。其中Keras作為模型框架、Flask作為后端Web框架、Redis則是方便以鍵值形式存儲(chǔ)圖像的數(shù)據(jù)庫(kù)。各主要package版本:

          tensorflow 1.14keras 2.2.4flask 1.1.1redis 3.3.8

          ???? 先簡(jiǎn)單說一下Web服務(wù),一個(gè)Web應(yīng)用的本質(zhì)無非就是客戶端發(fā)送一個(gè)HTTP請(qǐng)求,然后服務(wù)器收到請(qǐng)求后生成一個(gè)HTML文檔作為響應(yīng)返回給客戶端的過程。在部署深度學(xué)習(xí)模型時(shí),大多時(shí)候我們不需要搞一個(gè)前端頁(yè)面出來,一般是以REST API的形式提供給開發(fā)調(diào)用。那么什么是API呢?很簡(jiǎn)單,如果一個(gè)URL返回的不是HTML,而是機(jī)器能直接解析的數(shù)據(jù),這樣的一個(gè)URL就可以看作是一個(gè)API。

          先開啟Redis服務(wù):

          redis-server


          1.2 服務(wù)配置

          ???? 定義一些配置參數(shù):

          IMAGE_WIDTH = 224IMAGE_HEIGHT = 224IMAGE_CHANS = 3IMAGE_DTYPE = "float32"IMAGE_QUEUE = "image_queue"BATCH_SIZE = 32SERVER_SLEEP = 0.25CLIENT_SLEEP = 0.25


          ? ? ?指定輸入圖像大小、類型、batch_size大小以及Redis圖像隊(duì)列名稱。


          ???? 然后創(chuàng)建Flask對(duì)象實(shí)例,建立Redis數(shù)據(jù)庫(kù)連接:

          app = flask.Flask(__name__)db = redis.StrictRedis(host="localhost", port=6379, db=0)model = None


          ???? 因?yàn)閳D像數(shù)據(jù)作為numpy數(shù)組不能直接存儲(chǔ)到Redis中,所以圖像存入到數(shù)據(jù)庫(kù)之前需要將其序列化編碼,從數(shù)據(jù)庫(kù)取出時(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ù)測(cè)圖像還需要進(jìn)行簡(jiǎn)單的預(yù)處理,定義預(yù)處理函數(shù)如下:

          def prepare_image(image, target):    # if the image mode is not RGB, convert it    if image.mode != "RGB":        image = image.convert("RGB")    # resize the input image and preprocess it    image = 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 image    return image


          1.3 預(yù)測(cè)接口定義

          ???? 準(zhǔn)備工作完畢之后,接下來就是主要的兩大部分:模型預(yù)測(cè)部分和app后端響應(yīng)部分。先定義模型預(yù)測(cè)函數(shù)如下:

          def classify_process():    # 導(dǎo)入模型    print("* Loading model...")    model = ResNet50(weights="imagenet")    print("* Model loaded")    while True:        # 從數(shù)據(jù)庫(kù)中創(chuàng)建預(yù)測(cè)圖像隊(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            # 合并batch            else:                batch = np.vstack([batch, image])            # 更新圖像ID            imageIDs.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ù)測(cè)結(jié)果并打印            for (imageID, resultSet) in zip(imageIDs, results):                # initialize the list of output predictions                output = []                # loop over the results and add them to the list of                # output predictions                for (imagenetID, label, prob) in resultSet:                    r = {"label": label, "probability": float(prob)}                    output.append(r)                # 保存結(jié)果到數(shù)據(jù)庫(kù)                db.set(imageID, json.dumps(output))            # 從隊(duì)列中刪除已預(yù)測(cè)過的圖像            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語(yǔ)言存儲(chǔ)順序存儲(chǔ)            image = image.copy(order="C")            # 生成圖像ID            k = 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)                    break                time.sleep(CLIENT_SLEEP)            data["success"] = True        return flask.jsonify(data)


          ???? Flask使用Python裝飾器在內(nèi)部自動(dòng)將請(qǐng)求的URL和目標(biāo)函數(shù)關(guān)聯(lián)了起來,這樣方便我們快速搭建一個(gè)Web服務(wù)。


          1.4 接口測(cè)試

          ???? 服務(wù)搭建好了之后我們可以用一張圖片來測(cè)試一下效果:

          curl -X POST -F image=@test.jpg 'http://127.0.0.1:5000/predict'

          模型端的返回:

          預(yù)測(cè)結(jié)果返回:


          ???? 最后我們可以給搭建好的服務(wù)進(jìn)行一個(gè)壓力測(cè)試,看看服務(wù)的并發(fā)等性能如何,定義一個(gè)壓測(cè)文件stress_test.py 如下:

          from threading import Threadimport requestsimport time# 請(qǐng)求的URLKERAS_REST_API_URL = "http://127.0.0.1:5000/predict"# 測(cè)試圖片IMAGE_PATH = "test.jpg"# 并發(fā)數(shù)NUM_REQUESTS = 500# 請(qǐng)求間隔SLEEP_COUNT = 0.05def call_predict_endpoint(n):    # 上傳圖像    image = open(IMAGE_PATH, "rb").read()    payload = {"image": image}    # 提交請(qǐng)求    r = requests.post(KERAS_REST_API_URL, files=payload).json()    # 確認(rèn)請(qǐng)求是否成功    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)用api    t = Thread(target=call_predict_endpoint, args=(i,))    t.daemon = True    t.start()    time.sleep(SLEEP_COUNT)time.sleep(300)

          測(cè)試效果如下:



          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è)簡(jiǎn)單的推理演示了。但在生產(chǎn)環(huán)境下,Python的可移植性和速度性能遠(yuǎn)不如C++。所以對(duì)于深度學(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方法對(duì)模型構(gòu)建TorchScripttraced_model?=?torch.jit.trace(my_model,?(x,?h))#?保存轉(zhuǎn)換后的模型traced_model.save('model.pt')


          ???? 在這段代碼中,我們先是定義了一個(gè)簡(jiǎn)單模型并創(chuàng)建模型實(shí)例,然后給定輸入示例,Tracing方法最關(guān)鍵的一步在于使用torch.jit.trace方法對(duì)模型進(jìn)行TorchScript轉(zhuǎn)化。我們可以獲得轉(zhuǎn)化后的traced_model對(duì)象獲得其計(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)過這種方式保存下來的模型可以加載到其他其他語(yǔ)言環(huán)境中。


          ???? TorchScript的另一種實(shí)現(xiàn)方式是Script的方式,可以算是對(duì)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為我們提供了一種表示形式,可以對(duì)代碼進(jìn)行編譯器優(yōu)化以提供更有效的執(zhí)行。


          2.3 libtorch

          ???? 在Python環(huán)境下對(duì)訓(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后可簡(jiǎn)單測(cè)試下是否正常。比如我們用TorchScript轉(zhuǎn)換一個(gè)預(yù)訓(xùn)練模型,示例如下:

          import torchimport torchvision.models as modelsvgg16 = 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?#include?using?namespace?std;
          int?main(int?argc,?const?char*?argv[]){????if?(argc?!=?2)?{????????std::cerr?<"usage:?example-app?\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?inputs; inputs.push_back(torch::ones({1, 3, 224, 224}).to(at::kCUDA));
          ????//?執(zhí)行模型推理并輸出tensor????at::Tensor?output?=?module->forward(inputs).toTensor(); std::cout << output.slice(/*dim=*/1, /*start=*/0, /*end=*/5) << '\n';}


          ???? 編譯test.cpp并執(zhí)行,輸出如下。對(duì)比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 完整部署流程

          ???? 通過前面對(duì)TorchScript和libtorch的描述,其實(shí)我們已經(jīng)基本將PyTorch的C++部署已經(jīng)基本講到了,這里我們?cè)賮硗暾睦硪幌抡麄€(gè)流程。基于C++的PyTorch模型部署流程如下。


          第一步:

          ???? 通過torch.jit.trace方法將PyTorch模型轉(zhuǎn)換為TorchScript,示例如下:

          import?torchfrom?torchvision.models?import?resnet18model?=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頭文件.#include #include 
          int?main(int?argc,?const?char*?argv[])?{??if?(argc?!=?2)?{????std::cerr?<"usage:?example-app?\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è)文件編寫完成之后便可對(duì)其執(zhí)行編譯:

          mkdir?example_testcd?example_testcmake?-DCMAKE_PREFIX_PATH=/path/to/libtorch?..cmake --example_test . --config Release


          第四步:

          給example-app.cpp添加模型推理代碼并執(zhí)行:

          std::vector?inputs;inputs.push_back(torch::ones({1,?3,?224,?224}));//?執(zhí)行推理并將模型轉(zhuǎn)化為Tensoroutput = 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é)

          ? ? ?模型部署對(duì)于算法工程師而言非常重要,關(guān)系到你的工作能否產(chǎn)生實(shí)際價(jià)值。相應(yīng)的也需要大家具備足夠的工程能力,比如MySQL、Redis、C++、前端和后端的一些知識(shí)和開發(fā)技術(shù),需要各位算法工程師都能夠基本了解和能夠使用。


          往期精彩回顧





          獲取一折本站知識(shí)星球優(yōu)惠券,復(fù)制鏈接直接打開:

          https://t.zsxq.com/662nyZF

          本站qq群1003271085。

          加入微信群請(qǐng)掃碼進(jìn)群(如果是博士或者準(zhǔn)備讀博士請(qǐng)說明):

          瀏覽 35
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  麻豆国产 | 大香蕉之大香蕉之国产沙发 | 时逼高清视频免费少妞 | 成人综合中文字幕 | 成人精品黄色稫利视频 |